├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── README.md ├── backend ├── .eslintrc.js ├── package.json └── server.js ├── data └── index.html ├── frontend ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── postprocess.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── style │ │ │ └── custom.scss │ ├── components │ │ ├── Inputs │ │ │ ├── InputIP.vue │ │ │ ├── InputNumber.vue │ │ │ ├── InputSlider.vue │ │ │ ├── InputText.vue │ │ │ └── ToggleSwitch.vue │ │ ├── Ui │ │ │ ├── Chart.vue │ │ │ ├── Navbar.vue │ │ │ ├── Notification.vue │ │ │ └── ScheduleChart.vue │ │ ├── globalComponents.js │ │ └── globalOptions.js │ ├── eventBus.js │ ├── http.js │ ├── main.js │ ├── router.js │ └── views │ │ ├── About.vue │ │ ├── Home.vue │ │ ├── Schedule.vue │ │ ├── Settings.vue │ │ ├── Wifi.vue │ │ └── components │ │ ├── Leds.vue │ │ └── Services.vue ├── tests │ └── unit │ │ └── .eslintrc.js └── vue.config.js ├── lib ├── README └── readme.txt ├── platformio.ini ├── src ├── Network.cpp ├── Network.h ├── app_schedule.cpp ├── app_schedule.h ├── led.cpp ├── led.h ├── main.cpp ├── mqtt.cpp ├── mqtt.h ├── pwm.c ├── pwm.h ├── settings.cpp ├── settings.h ├── status.cpp ├── status.h ├── webpage.h ├── webui.cpp └── webui.h └── test └── README /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .pioenvs 3 | .piolibdeps 4 | .clang_complete 5 | .gcc-flags.json 6 | CMakeListsPrivate.txt 7 | backend/node_modules 8 | frontend/node_modules 9 | cmake-build-debug 10 | 11 | .DS_Store 12 | node_modules 13 | frontend/dist 14 | 15 | # local env files 16 | .env.local 17 | .env.*.local 18 | 19 | # Log files 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Editor directories and files 25 | .idea 26 | .vscode 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw* 32 | 33 | *package-lock.json 34 | *.eslintrc.js 35 | /data/js/app.js 36 | /data/js/app-legacy.js 37 | /cmake-build-all/ 38 | /cmake-build-8ch-esp12e-stm32/ 39 | /cmake-build-3ch-esp07/ 40 | /cmake-build-3ch-esp07-ota/ 41 | /cmake-build-5ch-esp12e/ 42 | /cmake-build-5ch-esp12e-ota/ 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < https://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < https://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < https://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choose one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # - platformio update 39 | # 40 | # script: 41 | # - platformio run 42 | 43 | 44 | # 45 | # Template #2: The project is intended to be used as a library with examples. 46 | # 47 | 48 | # language: python 49 | # python: 50 | # - "2.7" 51 | # 52 | # sudo: false 53 | # cache: 54 | # directories: 55 | # - "~/.platformio" 56 | # 57 | # env: 58 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 59 | # - PLATFORMIO_CI_SRC=examples/file.ino 60 | # - PLATFORMIO_CI_SRC=path/to/test/directory 61 | # 62 | # install: 63 | # - pip install -U platformio 64 | # - platformio update 65 | # 66 | # script: 67 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 68 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # !!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT AND USE 2 | # https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags 3 | # 4 | # If you need to override existing CMake configuration or add extra, 5 | # please create `CMakeListsUser.txt` in the root of project. 6 | # The `CMakeListsUser.txt` will not be overwritten by PlatformIO. 7 | 8 | cmake_minimum_required(VERSION 3.13) 9 | set(CMAKE_SYSTEM_NAME Generic) 10 | set(CMAKE_C_COMPILER_WORKS 1) 11 | set(CMAKE_CXX_COMPILER_WORKS 1) 12 | 13 | project("HV_CC_LED_DRIVER" C CXX) 14 | 15 | include(CMakeListsPrivate.txt) 16 | 17 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/CMakeListsUser.txt) 18 | include(CMakeListsUser.txt) 19 | endif() 20 | 21 | add_custom_target( 22 | Production ALL 23 | COMMAND platformio -c clion run "$<$>:-e${CMAKE_BUILD_TYPE}>" 24 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 25 | ) 26 | 27 | add_custom_target( 28 | Debug ALL 29 | COMMAND platformio -c clion run --target debug "$<$>:-e${CMAKE_BUILD_TYPE}>" 30 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 31 | ) 32 | 33 | add_executable(Z_DUMMY_TARGET ${SRC_LIST}) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP8266 a6211 led-driver 2 | esp8266 + A6211 up to 48v multichannel led driver 3 | 4 | This is a PlatformIO project using the Arduino core on the ESP8266 as an MCU and the A6211 as a dedicated LED driver. 5 | 6 | Hardware designed to control light in Reef Aqarium and Grow box. 7 | 8 | Project [log](https://hackaday.io/project/165103-wifi-constant-current-led-driver) 9 | 10 | ## Features 11 | 12 | - 6-48 volt input, can drive up to 13 serial connected led from 48v input! 13 | - 2 kHz PWM Dimming 14 | - Web Setup: 15 | - LED 16 | - MQTT 17 | - Schedule 18 | 19 | 20 | ### OTA Update 21 | 22 | ### 2 stage for 1mb flash (esp07) 23 | - 1st stage 24 | python espota.py -d -i 192.168.2.46 -f .pio/build/5ch-esp07-1st-stage-ota/firmware.bin 25 | 26 | - 2nd stage 27 | python espota.py -d -i 192.168.2.46 -f .pio/build/5ch-esp07-2nd-stage-ota/firmware.bin -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | } 17 | }; -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "Web Socket test server", 5 | "main": "server.js", 6 | "scripts": { 7 | "run": "node server.js", 8 | "watch": "nodemon ./server.js localhost 8088" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "async": "^2.6.2", 14 | "body-parser": "^1.18.3", 15 | "cors": "^2.8.5", 16 | "express": "^4.16.4", 17 | "morgan": "^1.9.1", 18 | "ws": "^5.2.0" 19 | }, 20 | "devDependencies": { 21 | "bulma": "^0.7.4", 22 | "eslint": "^5.15.3", 23 | "nodemon": "^1.18.10" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const http = require('http'); 3 | const WebSocket = require('ws'); 4 | const app = express(); 5 | const cors = require('cors'); 6 | const bodyParser = require('body-parser'); 7 | 8 | //initialize a simple http server 9 | const server = http.createServer(app); 10 | 11 | // create application/json parser 12 | var jsonParser = bodyParser.json(); 13 | 14 | var token; 15 | 16 | // Enable CORS on ExpressJS to avoid cross-origin errors when calling this server using AJAX 17 | // We are authorizing all domains to be able to manage information via AJAX (this is just for development) 18 | app.use(cors()); 19 | app.options('*', cors()); // include before other routes 20 | 21 | // Body parser middleware to auto-parse request text to JSON 22 | app.use(bodyParser.urlencoded({ extended: true })); 23 | app.use(bodyParser.json()); 24 | 25 | /* Home page */ 26 | app.get('/', function (req, res) { 27 | res.send('Hello World'); 28 | }); 29 | 30 | /* On-lne Status */ 31 | app.get('/status', function (req, res) { 32 | res.send(JSON.stringify(status)); 33 | }); 34 | 35 | /* Control channels */ 36 | app.post('/duty', function (req, res) { 37 | duty = req.body.duty 38 | res.send(JSON.stringify(duty)); 39 | }); 40 | 41 | app.post('/brightness', function (req, res) { 42 | brightness = req.body.brightness 43 | res.send(JSON.stringify(brightness)); 44 | }); 45 | 46 | /* Schedule ----> */ 47 | app.get('/schedule', function (req, res) { 48 | res.send(JSON.stringify(schedule)); 49 | }); 50 | 51 | app.post('/schedule', function (req, res) { 52 | console.log(req.body); 53 | schedule = req.body.schedule 54 | res.send(JSON.stringify(schedule)); 55 | }); 56 | 57 | 58 | /* Settings ----> */ 59 | app.get('/leds', function (req, res) { 60 | res.send(JSON.stringify(leds)); 61 | }); 62 | 63 | app.post('/leds', function (req, res) { 64 | console.log(req.body); 65 | leds = req.body.leds 66 | res.send(JSON.stringify(leds)); 67 | }); 68 | 69 | app.get('/networks', function (req, res) { 70 | res.send(JSON.stringify(networks)); 71 | }); 72 | 73 | app.post('/networks', function (req, res) { 74 | networks = req.body.networks 75 | res.send(JSON.stringify(networks)); 76 | }); 77 | 78 | app.get('/services', function (req, res) { 79 | res.send(JSON.stringify(services)); 80 | }); 81 | 82 | app.post('/services', function (req, res) { 83 | services = req.body.services 84 | res.send(JSON.stringify(services)); 85 | }); 86 | 87 | app.get('/time', function (req, res) { 88 | res.send(JSON.stringify(time)); 89 | }); 90 | 91 | app.post('/time', function (req, res) { 92 | time = req.body.time 93 | res.send(JSON.stringify(time)); 94 | }); 95 | 96 | app.post('/auth', jsonParser, function(req, res) { 97 | if (!req.body) return res.sendStatus(400); 98 | 99 | if (req.body.login === "login" && req.body.password === "password") { 100 | token = Math.random(); 101 | res.json({'success':true, 'token':token}); 102 | } 103 | else 104 | res.json({'success':false}); 105 | 106 | console.log(req.body); 107 | // console.log(req.text.pass); 108 | }); 109 | 110 | 111 | const wss = new WebSocket.Server({ 112 | server, 113 | path: "/ws", 114 | }); 115 | 116 | let services = { 117 | hostname: 'test', 118 | ntp_server_name: '', 119 | utc_offset_minutes: 0, 120 | ntp_dst: true, 121 | mqtt_ip_address: '', 122 | mqtt_port: '', 123 | mqtt_user: '', 124 | mqtt_password: '', 125 | mqtt_qos: 0, 126 | enable_ntp_service: false, 127 | enable_mqtt_service: false 128 | } 129 | 130 | let networks = [ 131 | { 132 | id: 0, 133 | ssid: 'Best WiFi', 134 | password: '', 135 | ip_address: '192.168.1.100', 136 | mask: '255.255.255.0', 137 | gateway: '192.168.1.1', 138 | dns: '192.168.1.1', 139 | dhcp: false 140 | }, 141 | { 142 | id: 1, 143 | ssid: 'Best WiFi 2', 144 | password: '', 145 | ip_address: '', 146 | mask: '', 147 | gateway: '', 148 | dns: '', 149 | dhcp: true 150 | } 151 | ]; 152 | 153 | let leds = [ 154 | { 155 | id: 0, 156 | color: '#DDEFFF', 157 | power: 50, 158 | state: 1 159 | }, 160 | { 161 | id: 1, 162 | color: '#DDEFFF', 163 | power: 30, 164 | state: 1 165 | }, 166 | { 167 | id: 2, 168 | color: '#DDEFFF', 169 | power: 40, 170 | state: 1 171 | }, 172 | { 173 | id: 3, 174 | color: '#DDEFFF', 175 | power: 40, 176 | state: 1 177 | }, 178 | { 179 | id: 4, 180 | color: '#DDEFFF', 181 | power: 40, 182 | state: 1 183 | } 184 | ]; 185 | 186 | let schedule = [ 187 | { 188 | time_hour: 9, 189 | time_minute: 0, 190 | brightness: 50, 191 | duty: [ 192 | 10, 20, 10, 20, 20 193 | ] 194 | }, 195 | { 196 | time_hour: 12, 197 | time_minute: 0, 198 | brightness: 100, 199 | duty: [ 200 | 40, 20, 10, 20, 20 201 | ] 202 | }, 203 | { 204 | time_hour: 13, 205 | time_minute: 0, 206 | brightness: 120, 207 | duty: [ 208 | 100, 100, 100, 20, 20 209 | ] 210 | }, 211 | { 212 | time_hour: 19, 213 | time_minute: 0, 214 | brightness: 20, 215 | duty: [ 216 | 0, 10, 0, 20, 20 217 | ] 218 | } 219 | 220 | ]; 221 | 222 | let status = { 223 | upTime: '1 day', 224 | localTime: '12:22', 225 | chipId: 1827, 226 | freeHeap: 23567, 227 | vcc: 20, 228 | wifiMode: 'STA', 229 | ipAddress: '192.168.1.199', 230 | macAddress: '0A:EE:00:00:01:90', 231 | mqttService: '', 232 | ntpService: '', 233 | brightness: 90, 234 | channels: [50, 100, 100, 20, 20] 235 | }; 236 | 237 | let time = { 238 | year: 20, 239 | month: 10, 240 | weekday: 1, 241 | day: 10, 242 | hour: 12, 243 | minute: 1, 244 | second: 1, 245 | dst: 0, 246 | utc: 1 247 | } 248 | 249 | let duty = [50, 100, 100, 20, 20]; 250 | 251 | let brightness = 100; 252 | 253 | function getRandomArbitrary(min, max) { 254 | return Math.random() * (max - min) + min; 255 | } 256 | 257 | function getRandomInt(min, max) { 258 | return Math.floor(Math.random() * (max - min)) + min; 259 | } 260 | 261 | 262 | function noop() {} 263 | 264 | function heartbeat() { 265 | this.isAlive = true; 266 | } 267 | 268 | wss.on('connection', function connection(ws) { 269 | ws.isAlive = true; 270 | ws.on('pong', heartbeat); 271 | 272 | ws.on('message', function incoming(message) { 273 | console.log('received: %s', message); 274 | 275 | let json = JSON.parse(message); 276 | if (json.command !== undefined) { 277 | switch (json.command) { 278 | case ('getStatus'): 279 | ws.send(JSON.stringify({ status: status })); 280 | console.log(status); 281 | break; 282 | 283 | case ('getSettings'): 284 | ws.send(JSON.stringify({ leds: leds, schedule: schedule })); 285 | console.log(leds, schedule); 286 | break; 287 | 288 | case ('saveSettings'): 289 | // save led color 290 | leds = json.leds; 291 | // save schedule 292 | schedule = json.schedule; 293 | ws.send(JSON.stringify({ response: 'success' })); 294 | break; 295 | 296 | case ('getLeds'): 297 | ws.send(JSON.stringify({ leds: leds })); 298 | console.log(schedule); 299 | break; 300 | 301 | case ('setLeds'): 302 | leds = json.leds; 303 | ws.send(JSON.stringify({ response: 'success' })); 304 | break; 305 | 306 | case ('getSchedule'): 307 | ws.send(JSON.stringify({ schedule: schedule, capacity: 10 })); 308 | console.log(schedule); 309 | break; 310 | 311 | case ('setSchedule'): 312 | schedule = json.schedule; 313 | ws.send(JSON.stringify({ response: 'success' })); 314 | break; 315 | 316 | case ('getNetworks'): 317 | ws.send(JSON.stringify({ networks: networks, capacity: 2 })); 318 | console.log(schedule); 319 | break; 320 | 321 | case ('setNetworks'): 322 | networks = json.networks; 323 | ws.send(JSON.stringify({ response: 'success' })); 324 | break; 325 | 326 | 327 | default: 328 | ws.send(Date.now()); 329 | break; 330 | } 331 | 332 | } 333 | 334 | // setTimeout(function timeout() { 335 | // ws.send(Date.now()); 336 | // }, 500); 337 | 338 | }); 339 | 340 | // ws.send('something'); 341 | }); 342 | 343 | console.log("Started"); 344 | 345 | const interval = setInterval(function ping() { 346 | wss.clients.forEach(function each(ws) { 347 | if (ws.isAlive === false) return ws.terminate(); 348 | 349 | ws.isAlive = false; 350 | ws.ping(noop); 351 | }); 352 | }, 30000); 353 | 354 | function broadcast(data) { 355 | wss.clients.forEach(function each(client) { 356 | if (client.readyState === WebSocket.OPEN) { 357 | client.send(data); 358 | } 359 | }); 360 | } 361 | 362 | //start our server 363 | server.listen(8081, () => { 364 | console.log(`Server started on port ${server.address().port} :)`); 365 | }); 366 | -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | frontend
-------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [*.{js,jsx,ts,tsx,vue}] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | max_line_length = 100 14 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/recommended', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'vue/v-bind-style': ['error', 'longform'] 14 | }, 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Run your unit tests 29 | ``` 30 | npm run test:unit 31 | ``` 32 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.2.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "test:unit": "vue-cli-service test:unit" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.19.2", 13 | "bulma": "^0.7.5", 14 | "compression-webpack-plugin": "^3.1.0", 15 | "frappe-charts": "^1.2.4", 16 | "node-sass": "^4.13.1", 17 | "tar": "^4.4.10", 18 | "vue": "^2.6.6", 19 | "vue-feather-icons": "^4.22.0", 20 | "vue-router": "^3.1.5", 21 | "vue-the-mask": "^0.11.1", 22 | "vue2-frappe": "^1.0.28", 23 | "webpack-shell-plugin": "^0.5.0" 24 | }, 25 | "devDependencies": { 26 | "@gfx/zopfli": "^1.0.14", 27 | "@vue/cli-plugin-babel": "^3.11.0", 28 | "@vue/cli-plugin-eslint": "^3.11.0", 29 | "@vue/cli-plugin-unit-mocha": "^3.11.0", 30 | "@vue/cli-service": "^4.1.2", 31 | "@vue/eslint-config-standard": "^4.0.0", 32 | "@vue/test-utils": "1.0.0-beta.29", 33 | "babel-eslint": "^10.0.3", 34 | "chai": "^4.1.2", 35 | "eslint": "^5.16.0", 36 | "eslint-plugin-vue": "^5.2.3", 37 | "sass-loader": "^7.3.1", 38 | "vue-template-compiler": "^2.5.21" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/postprocess.js: -------------------------------------------------------------------------------- 1 | // Finalize Nodejs Script 2 | // 1 - Append JS in HTML Document 3 | // 2 - Gzip HTML 4 | // 3 - Covert to Raw Bytes 5 | // 4 - ( Save to File: webpage.h ) in dist Folder 6 | 7 | const fs = require('fs') 8 | const zlib = require('zlib') 9 | const path = require('path') 10 | 11 | function getByteArray (file) { 12 | const fileData = file.toString('hex') 13 | const result = [] 14 | for (let i = 0; i < fileData.length; i += 2) { result.push(`0x${fileData[i]}${fileData[i + 1]}`) } 15 | return result 16 | } 17 | 18 | const js = fs.readFileSync(path.join(__dirname, 'dist/js/app.js')) 19 | const html = ` 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ESP8266 Led Driver 28 | 29 | 30 | 33 |
34 | 35 | 36 | 37 | ` 38 | // Gzip the index.html file with JS Code. 39 | const gzippedIndex = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION }) 40 | const indexHTML = getByteArray(gzippedIndex) 41 | 42 | const source = ` 43 | const uint32_t HTML_SIZE = ${indexHTML.length}; 44 | const uint8_t HTML[] PROGMEM = { ${indexHTML} }; 45 | ` 46 | 47 | fs.writeFileSync(path.join(__dirname, '/../src/webpage.h'), source, 'utf8') 48 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/9zigen/esp8266-a6211-led-driver/bd238fb067b80a79469f5c2f4b2d19a270518b14/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | frontend 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 73 | -------------------------------------------------------------------------------- /frontend/src/assets/style/custom.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import "../../../node_modules/bulma/sass/utilities/_all.sass"; 3 | @import "../../../node_modules/bulma/sass/base/_all.sass"; 4 | @import "../../../node_modules/bulma/sass/elements/button.sass"; 5 | @import "../../../node_modules/bulma/sass/elements/container.sass"; 6 | @import "../../../node_modules/bulma/sass/elements/title.sass"; 7 | @import "../../../node_modules/bulma/sass/elements/notification.sass"; 8 | @import "../../../node_modules/bulma/sass/form/shared.sass"; 9 | @import "../../../node_modules/bulma/sass/form/input-textarea.sass"; 10 | @import "../../../node_modules/bulma/sass/form/select.sass"; 11 | @import "../../../node_modules/bulma/sass/form/tools.sass"; 12 | @import "../../../node_modules/bulma/sass/components/navbar.sass"; 13 | @import "../../../node_modules/bulma/sass/grid/_all.sass"; 14 | 15 | #app { 16 | font-family: 'Avenir', Helvetica, Arial, sans-serif; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | text-align: center; 20 | color: #2c3e50; 21 | min-height: 100vh; 22 | padding-bottom: 25px; 23 | } 24 | 25 | #nav { 26 | padding: 30px; 27 | a { 28 | font-weight: bold; 29 | color: #2c3e50; 30 | &.router-link-exact-active { 31 | color: $primary; 32 | } 33 | } 34 | } 35 | 36 | .is-active > .navbar-icon { 37 | color: $green 38 | } 39 | 40 | .title { 41 | margin-top: 30px; 42 | color: $light; 43 | } 44 | 45 | .subtitle { 46 | color: $grey; 47 | } 48 | 49 | .text-left { 50 | text-align: left; 51 | } 52 | 53 | .control { 54 | &.is-text { 55 | padding-top: 0.375em; 56 | text-align: center; 57 | } 58 | } 59 | 60 | /* loading */ 61 | .is-disabled { 62 | opacity: 0.4; 63 | pointer-events: none; 64 | transition: all 1s ease-in-out; 65 | } 66 | 67 | /* Center the loader */ 68 | .loader { 69 | position: absolute; 70 | left: 50%; 71 | top: 50%; 72 | z-index: 1; 73 | width: 60px; 74 | height: 60px; 75 | margin: -40px 0 0 -40px; 76 | border: 8px solid $light; 77 | border-radius: 50%; 78 | border-top: 8px solid $warning; 79 | -webkit-animation: spin 1.5s linear infinite; 80 | animation: spin 1.5s linear infinite; 81 | } 82 | 83 | @-webkit-keyframes spin { 84 | 0% { -webkit-transform: rotate(0deg); } 85 | 100% { -webkit-transform: rotate(360deg); } 86 | } 87 | 88 | @keyframes spin { 89 | 0% { transform: rotate(0deg); } 90 | 100% { transform: rotate(360deg); } 91 | } 92 | 93 | .bg-notification { 94 | padding: 1.25rem 2.5rem 1.25rem !important; 95 | } 96 | 97 | /* vue transition LIST */ 98 | .list-item { 99 | display: inline-block; 100 | margin-right: 10px; 101 | } 102 | .list-enter-active { 103 | -webkit-transition: all .5s ease-in; 104 | transition: all .5s ease-in; 105 | } 106 | .list-leave-active { 107 | -webkit-transition: all .5s ease-out; 108 | transition: all .5s ease-out; 109 | } 110 | .list-enter, .list-leave-to /* .list-leave-active до версии 2.1.8 */ { 111 | opacity: 0; 112 | transform: translateX(-30px); 113 | } -------------------------------------------------------------------------------- /frontend/src/components/Inputs/InputIP.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 111 | 112 | 125 | -------------------------------------------------------------------------------- /frontend/src/components/Inputs/InputNumber.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /frontend/src/components/Inputs/InputSlider.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 69 | 70 | 209 | -------------------------------------------------------------------------------- /frontend/src/components/Inputs/InputText.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/Inputs/ToggleSwitch.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | 99 | -------------------------------------------------------------------------------- /frontend/src/components/Ui/Chart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 286 | -------------------------------------------------------------------------------- /frontend/src/components/Ui/Navbar.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 118 | 119 | 121 | -------------------------------------------------------------------------------- /frontend/src/components/Ui/Notification.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | 41 | 70 | -------------------------------------------------------------------------------- /frontend/src/components/Ui/ScheduleChart.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 82 | -------------------------------------------------------------------------------- /frontend/src/components/globalComponents.js: -------------------------------------------------------------------------------- 1 | import { CheckIcon, XIcon, PlusIcon, ClockIcon, TrelloIcon, SettingsIcon, InfoIcon, WifiIcon } from 'vue-feather-icons' 2 | import Chart from './Ui/Chart' 3 | import ScheduleChart from '@/components/Ui/ScheduleChart' 4 | import ToggleSwitch from '@/components/Inputs/ToggleSwitch' 5 | import Slider from '@/components/Inputs/InputSlider' 6 | import InputIP from '@/components/Inputs/InputIP' 7 | import InputNumber from './Inputs/InputNumber' 8 | 9 | export default { 10 | install (Vue, options) { 11 | Vue.component('vue-chart', Chart) 12 | Vue.component('toggle-switch', ToggleSwitch) 13 | Vue.component('slider', Slider) 14 | Vue.component('input-ip', InputIP) 15 | Vue.component('input-number', InputNumber) 16 | Vue.component('schedule-chart', ScheduleChart) 17 | 18 | /* feather-icons */ 19 | Vue.component('CheckIcon', CheckIcon) 20 | Vue.component('XIcon', XIcon) 21 | Vue.component('PlusIcon', PlusIcon) 22 | Vue.component('ClockIcon', ClockIcon) 23 | Vue.component('TrelloIcon', TrelloIcon) 24 | Vue.component('SettingsIcon', SettingsIcon) 25 | Vue.component('InfoIcon', InfoIcon) 26 | Vue.component('WifiIcon', WifiIcon) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/globalOptions.js: -------------------------------------------------------------------------------- 1 | const ledOptions = [ 2 | { text: 'Cold White', value: '#DDEFFF' }, 3 | { text: 'Warm White', value: '#FFFDDD' }, 4 | { text: 'Day White', value: '#EAEAEA' }, 5 | { text: 'UV', value: '#8A7AD4' }, 6 | { text: 'Deep Blue', value: '#7C9CFF' }, 7 | { text: 'Blue', value: '#42B8F3' }, 8 | { text: 'Cyan', value: '#4DF7FF' }, 9 | { text: 'Emerald', value: '#4DFFC5' }, 10 | { text: 'Green', value: '#6EB96E' }, 11 | { text: 'Yellow', value: '#FDFE90' }, 12 | { text: 'Amber', value: '#FCBB51' }, 13 | { text: 'Red', value: '#FB647A' }, 14 | { text: 'Deep Red', value: '#990000' } 15 | ] 16 | 17 | export default { 18 | install (Vue, options) { 19 | Vue.prototype.$appName = 'HV LED Driver App' 20 | Vue.prototype.$ledOptions = ledOptions 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/eventBus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const eventBus = new Vue() 4 | -------------------------------------------------------------------------------- /frontend/src/http.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const host = process.env.NODE_ENV === 'production' ? `http://${document.location.host}/` : 'http://192.168.2.38/' 4 | 5 | export const http = axios.create({ 6 | baseURL: host, 7 | headers: { 8 | Authorization: 'Bearer {token}' 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | import GlobalOptions from '@/components/globalOptions' 6 | import GlobalComponents from '@/components/globalComponents' 7 | 8 | import './assets/style/custom.scss' 9 | 10 | Vue.use(GlobalOptions) 11 | Vue.use(GlobalComponents) 12 | 13 | Vue.config.productionTip = false 14 | 15 | new Vue({ 16 | router, 17 | render: h => h(App) 18 | }).$mount('#app') 19 | -------------------------------------------------------------------------------- /frontend/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home' 4 | import Settings from './views/Settings' 5 | import About from './views/About' 6 | import Schedule from './views/Schedule' 7 | import Wifi from './views/Wifi' 8 | 9 | Vue.use(Router) 10 | 11 | export default new Router({ 12 | mode: 'history', 13 | base: process.env.BASE_URL, 14 | linkActiveClass: 'is-active', 15 | routes: [ 16 | { 17 | path: '/', 18 | name: 'home', 19 | component: Home, 20 | beforeEnter: (to, from, next) => { 21 | next() 22 | } 23 | }, 24 | { 25 | path: '/schedule', 26 | name: 'schedule', 27 | component: Schedule, 28 | beforeEnter: (to, from, next) => { 29 | next() 30 | } 31 | }, 32 | { 33 | path: '/wifi', 34 | name: 'wifi', 35 | component: Wifi, 36 | beforeEnter: (to, from, next) => { 37 | next() 38 | } 39 | }, 40 | { 41 | path: '/settings', 42 | name: 'settings', 43 | component: Settings, 44 | beforeEnter: (to, from, next) => { 45 | next() 46 | } 47 | }, 48 | { 49 | path: '/about', 50 | name: 'about', 51 | component: About 52 | } 53 | ] 54 | }) 55 | -------------------------------------------------------------------------------- /frontend/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 173 | 174 | 296 | -------------------------------------------------------------------------------- /frontend/src/views/Schedule.vue: -------------------------------------------------------------------------------- 1 | 198 | 199 | 367 | -------------------------------------------------------------------------------- /frontend/src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 57 | -------------------------------------------------------------------------------- /frontend/src/views/Wifi.vue: -------------------------------------------------------------------------------- 1 | 163 | 164 | 222 | -------------------------------------------------------------------------------- /frontend/src/views/components/Leds.vue: -------------------------------------------------------------------------------- 1 | 142 | 143 | 177 | -------------------------------------------------------------------------------- /frontend/src/views/components/Services.vue: -------------------------------------------------------------------------------- 1 | 210 | 211 | 258 | -------------------------------------------------------------------------------- /frontend/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = { 3 | pluginOptions: { 4 | 'style-resources-loader': { 5 | preProcessor: 'scss', 6 | patterns: [] 7 | } 8 | }, 9 | 10 | chainWebpack: (config) => { 11 | config.optimization.delete('splitChunks') 12 | }, 13 | 14 | // baseUrl: undefined, 15 | assetsDir: undefined, 16 | runtimeCompiler: undefined, 17 | productionSourceMap: false, 18 | parallel: undefined, 19 | css: { extract: false }, 20 | filenameHashing: false, 21 | 22 | // outputDir: '../data' 23 | outputDir: undefined, 24 | 25 | lintOnSave: undefined 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organized `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) http://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- readme.txt --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | Then in `src/main.c` you should use: 31 | 32 | #include 33 | #include 34 | 35 | // rest H/C/CPP code 36 | 37 | PlatformIO will find your libraries automatically, configure preprocessor's 38 | include paths and build them. 39 | 40 | More information about PlatformIO Library Dependency Finder 41 | - http://docs.platformio.org/page/librarymanager/ldf.html 42 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | 2 | [common] 3 | platform = espressif8266 4 | ;platform = https://github.com/platformio/platform-espressif8266.git#feature/stage 5 | ;platform = espressif8266@1.5.0 6 | 7 | framework = arduino 8 | upload_speed = 115200 9 | monitor_speed = 115200 10 | 11 | lib_deps = 12 | # Using a library name 13 | https://github.com/me-no-dev/ESPAsyncWebServer.git 14 | AsyncMqttClient 15 | ArduinoJson 16 | NtpClientLib 17 | EEPROM_Rotate 18 | 19 | # Available lwIP variants (macros): 20 | # -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH = v1.4 Higher Bandwidth (default) 21 | # -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY = v2 Lower Memory 22 | # -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH = v2 Higher Bandwidth 23 | 24 | build_flags = 25 | -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH 26 | -DDEBUG_UI_PORT=Serial 27 | ; -DDEBUG_ESP_WIFI 28 | ; -DDEBUG_ESP_PORT=Serial 29 | ; -DDEBUG_EEPROM_ROTATE_PORT=Serial 30 | ; -DNO_GLOBAL_EEPROM 31 | ; -DDEBUG_EEPROM 32 | ; -DDEBUG_WEB 33 | ; -DDEBUG_WEB_JSON 34 | -DDEBUG_NETWORK 35 | -DDEBUG_SCHEDULE 36 | -DDEBUG_MQTT 37 | ; -DDEBUG_MQTT_JSON 38 | 39 | ; LD Scripts 40 | ; https://github.com/esp8266/Arduino/tree/master/tools/sdk/ld 41 | 42 | [env:3ch-esp07] 43 | platform = ${common.platform} 44 | framework = ${common.framework} 45 | board = esp01_1m 46 | build_flags = ${common.build_flags} -DMAX_LED_CHANNELS=3 -Wl,-Teagle.flash.1m.ld 47 | lib_deps = ${common.lib_deps} 48 | 49 | [env:3ch-esp07-ota] 50 | platform = ${common.platform} 51 | framework = ${common.framework} 52 | board = esp01_1m 53 | build_flags = ${common.build_flags} -DMAX_LED_CHANNELS=3 -Wl,-Teagle.flash.1m.ld 54 | lib_deps = ${common.lib_deps} 55 | upload_port = 192.168.2.46 56 | ;upload_flags = --auth="" 57 | upload_protocol = espota 58 | upload_speed = 115200 59 | monitor_speed = 115200 60 | 61 | [env:5ch-esp07] 62 | platform = ${common.platform} 63 | framework = ${common.framework} 64 | board = esp01_1m 65 | build_flags = ${common.build_flags} -DMAX_LED_CHANNELS=5 -Wl,-Teagle.flash.1m.ld 66 | lib_deps = ${common.lib_deps} 67 | 68 | [env:5ch-esp07-1st-stage-ota] 69 | platform = ${common.platform} 70 | framework = ${common.framework} 71 | board = esp01_1m 72 | build_flags = ${common.build_flags} -DMAX_LED_CHANNELS=5 -DOTA_ONLY -Wl,-Teagle.flash.1m.ld 73 | lib_deps = ${common.lib_deps} 74 | upload_port = 192.168.2.46 75 | ;upload_flags = --auth="" 76 | upload_protocol = espota 77 | upload_speed = 115200 78 | monitor_speed = 115200 79 | 80 | [env:5ch-esp07-2nd-stage-ota] 81 | platform = ${common.platform} 82 | framework = ${common.framework} 83 | board = esp01_1m 84 | build_flags = ${common.build_flags} -DMAX_LED_CHANNELS=5 -Wl,-Teagle.flash.1m.ld 85 | lib_deps = ${common.lib_deps} 86 | upload_port = 192.168.2.46 87 | ;upload_flags = --auth="" 88 | upload_protocol = espota 89 | upload_speed = 115200 90 | monitor_speed = 115200 91 | 92 | [env:5ch-esp12e] 93 | platform = ${common.platform} 94 | framework = ${common.framework} 95 | board = esp12e 96 | build_flags = ${common.build_flags} -DMAX_LED_CHANNELS=5 97 | lib_deps = ${common.lib_deps} 98 | upload_speed = 115200 99 | monitor_speed = 115200 100 | 101 | [env:5ch-esp12e-ota] 102 | platform = ${common.platform} 103 | framework = ${common.framework} 104 | board = esp12e 105 | build_flags = ${common.build_flags} -DMAX_LED_CHANNELS=5 106 | lib_deps = ${common.lib_deps} 107 | upload_port = 192.168.2.25 108 | ;upload_flags = --auth="" 109 | upload_protocol = espota 110 | upload_speed = 115200 111 | monitor_speed = 115200 112 | 113 | # Serial 3Ch program: pio run -t upload -e 5ch-esp12e --upload-port /dev/tty.SLAB_USBtoUART 114 | # Serial 5Ch program: pio run -t upload -e 5ch-esp12e --upload-port /dev/tty.SLAB_USBtoUART 115 | # OTA Program example: pio run -t upload -e 3ch-esp07-ota -------------------------------------------------------------------------------- /src/Network.cpp: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-09. 3 | ***/ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "settings.h" 10 | #include "webui.h" 11 | #include "led.h" 12 | #include "Ticker.h" 13 | #include 14 | #include 15 | #include "Network.h" 16 | #include "mqtt.h" 17 | 18 | #define NTP_TIMEOUT 1500 19 | 20 | ESP8266WiFiMulti wifiMulti; 21 | DNSServer dnsServer; 22 | Ticker reconnect_timer; 23 | WiFiEventHandler gotIpEventHandler, disconnectedEventHandler; 24 | NTPSyncEvent_t ntpEvent; 25 | bool ntpSyncEventTriggered = false; 26 | 27 | /* NTP Sync Event */ 28 | void processSyncEvent (NTPSyncEvent_t ntpEvent) { 29 | switch (ntpEvent) { 30 | case noResponse: 31 | LOG_NETWORK("[NETWORK] NTP server not reachable \n"); 32 | break; 33 | case timeSyncd: 34 | LOG_NETWORK("[NETWORK] Got NTP time: %s \n", NTP.getTimeDateString (NTP.getLastNTPSync ()).c_str()); 35 | break; 36 | case invalidAddress: 37 | LOG_NETWORK("[NETWORK] Invalid NTP server address\n"); 38 | break; 39 | case requestSent: 40 | break; 41 | case errorSending: 42 | LOG_NETWORK("[NETWORK] Error sending request\n"); 43 | break; 44 | case responseError: 45 | LOG_NETWORK("[NETWORK] NTP response error\n"); 46 | break; 47 | } 48 | } 49 | 50 | void onSTAGotIP(const WiFiEventStationModeGotIP& event) { 51 | LOG_NETWORK("[NETWORK] Wi-Fi Got IP: %s\n", WiFi.localIP().toString().c_str()); 52 | LED.setMode(ONE_SHORT_BLINK); 53 | NETWORK.isConnected = true; 54 | NETWORK.stopAP(); 55 | 56 | WiFi.hostname(CONFIG.getHostname()); 57 | 58 | /* MDNS Setup */ 59 | MDNS.begin(CONFIG.getHostname()); 60 | MDNS.addService("http", "tcp", 80); 61 | MDNS.addServiceTxt("light", "tcp", "model", "5ch-esp8266"); 62 | 63 | /* NTP Connect */ 64 | NETWORK.startNtp = true; 65 | 66 | #ifndef OTA_ONLY 67 | /* MQTT Connect */ 68 | connectToMqtt(); 69 | #endif 70 | 71 | } 72 | 73 | void onSTADisconnect(const WiFiEventStationModeDisconnected& event) { 74 | LOG_NETWORK("[NETWORK] Wi-Fi Disconnected\n"); 75 | LED.setMode(THREE_SHORT_BLINK); 76 | NETWORK.isConnected = false; 77 | 78 | /* NTP sync can be disabled to avoid sync errors */ 79 | NTP.stop(); 80 | 81 | /* Scan and connect to best network */ 82 | NETWORK.connectSTA(); 83 | } 84 | 85 | void NetworkClass::init() { 86 | LOG_NETWORK("[NETWORK] WiFi First Time Connecting to AP...\n"); 87 | 88 | /* Start STA Mode with saved credentials */ 89 | startSTA(); 90 | 91 | /* Register events */ 92 | gotIpEventHandler = WiFi.onStationModeGotIP(onSTAGotIP); 93 | disconnectedEventHandler = WiFi.onStationModeDisconnected(onSTADisconnect); 94 | 95 | NTP.onNTPSyncEvent ([](NTPSyncEvent_t event) { 96 | ntpEvent = event; 97 | ntpSyncEventTriggered = true; 98 | }); 99 | } 100 | 101 | void NetworkClass::reloadSettings() { 102 | new_wifi_settings = true; 103 | new_mqtt_settings = true; 104 | check_wifi = true; 105 | } 106 | 107 | /* check WiFi connection in loop */ 108 | void NetworkClass::loop() { 109 | 110 | if (isConnected) { 111 | /* Allow MDNS processing */ 112 | MDNS.update(); 113 | } else { 114 | dnsServer.processNextRequest(); 115 | } 116 | 117 | if (startNtp) { 118 | /* Setup NTP */ 119 | int16_t tz_offset = CONFIG.getNtpOffset(); 120 | auto tz = (int8_t)(tz_offset / 60); 121 | auto tz_minutes = (int8_t)(tz_offset % 60); 122 | NTP.setInterval (63); 123 | NTP.setNTPTimeout (NTP_TIMEOUT); 124 | NTP.begin (CONFIG.getNtpServerName(), tz, true, tz_minutes); 125 | 126 | startNtp = false; 127 | } 128 | 129 | if (ntpSyncEventTriggered) { 130 | processSyncEvent (ntpEvent); 131 | ntpSyncEventTriggered = false; 132 | } 133 | 134 | if (!check_wifi) 135 | return; 136 | 137 | /* update MQTT client config */ 138 | if (new_mqtt_settings) { 139 | new_mqtt_settings = false; 140 | LOG_NETWORK("[NETWORK] MQTT New Settings Applied, connecting to Server...\n"); 141 | #ifndef OTA_ONLY 142 | initMqtt(); 143 | #endif 144 | } 145 | 146 | /* reconnect to AP requested */ 147 | if (new_wifi_settings) { 148 | new_wifi_settings = false; 149 | LOG_NETWORK("[NETWORK] WiFi New Settings Applied, checking...\n"); 150 | startSTA(); 151 | } 152 | 153 | /* If STA still not connected */ 154 | if (wifiMulti.run() != WL_CONNECTED) { 155 | isConnected = false; 156 | 157 | LOG_NETWORK("[NETWORK] WiFi Connect Timeout. WiFi Mode: %d \n", WiFi.getMode()); 158 | 159 | if (WiFi.getMode() != WIFI_AP) { 160 | /* start AP */ 161 | startAP(); 162 | } 163 | } else { 164 | /* Wait 2 sec to repeat check state */ 165 | connection_timer.once(2, std::bind(&NetworkClass::tik, this)); 166 | } 167 | 168 | check_wifi = false; 169 | 170 | } 171 | 172 | void NetworkClass::tik() { 173 | check_wifi = true; 174 | } 175 | 176 | void NetworkClass::connectSTA() { 177 | /* Scan and connect to best WiFi network */ 178 | if (wifiMulti.run() != WL_CONNECTED) { 179 | 180 | /* Wait 1000 msec to establish connection */ 181 | connection_timer.once(10, std::bind(&NetworkClass::tik, this)); 182 | } 183 | } 184 | 185 | 186 | void NetworkClass::startSTA() { 187 | /* Disable store WiFi config in SDK flash area */ 188 | WiFi.persistent(false); 189 | 190 | /* Start WiFi in Station mode */ 191 | WiFi.mode(WIFI_STA); 192 | 193 | for (uint8_t i = 0; i < MAX_NETWORKS; i++) 194 | { 195 | network_t * network = CONFIG.getNetwork(i); 196 | 197 | /* check if has wifi config */ 198 | if (network->active) { 199 | wifiMulti.addAP((char *)&network->ssid, (char *)&network->password); 200 | LOG_NETWORK("[NETWORK] Add config to WiFi list: %s [******] \n", network->ssid); 201 | } 202 | } 203 | 204 | /* Scan and connect to best WiFi network */ 205 | connectSTA(); 206 | 207 | } 208 | 209 | void NetworkClass::startAP() { 210 | LOG_NETWORK("[NETWORK] WiFi Starting AP. \n"); 211 | WiFi.disconnect(false); 212 | WiFi.mode(WIFI_AP); 213 | 214 | services_t * services = CONFIG.getService(); 215 | auth_t * auth = CONFIG.getAuth(); 216 | 217 | /* Start AP */ 218 | WiFi.softAP(services->hostname, auth->password); 219 | LOG_NETWORK("[NETWORK] AP Started with name: %s and password: %s \n", services->hostname, auth->password); 220 | 221 | /* Captive Portal */ 222 | delay(500); 223 | 224 | /* Setup the DNS server redirecting all the domains to the apIP */ 225 | dnsServer.setErrorReplyCode(DNSReplyCode::NoError); 226 | dnsServer.start(53, "*", WiFi.softAPIP()); 227 | 228 | /* MDNS Notify */ 229 | if (MDNS.isRunning()) { 230 | MDNS.notifyAPChange(); 231 | } 232 | } 233 | 234 | void NetworkClass::stopAP() { 235 | 236 | /* Disable AP Mode */ 237 | WiFi.softAPdisconnect(false); 238 | WiFi.mode(WIFI_STA); 239 | LOG_NETWORK("[NETWORK] Disabling AP. WiFi Mode: %d \n", WiFi.getMode()); 240 | 241 | /* Captive Portal */ 242 | dnsServer.stop(); 243 | 244 | /* MDNS Notify */ 245 | if (MDNS.isRunning()) { 246 | MDNS.notifyAPChange(); 247 | } 248 | } 249 | 250 | NetworkClass NETWORK; -------------------------------------------------------------------------------- /src/Network.h: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-09. 3 | ***/ 4 | 5 | #ifndef HV_CC_LED_DRIVER_NETWORK_H 6 | #define HV_CC_LED_DRIVER_NETWORK_H 7 | 8 | #include "Ticker.h" 9 | 10 | #ifdef DEBUG_NETWORK 11 | #define LOG_NETWORK(...) DEBUG_UI_PORT.printf( __VA_ARGS__ ) 12 | #else 13 | #define LOG_NETWORK(...) 14 | #endif 15 | 16 | class NetworkClass { 17 | 18 | public: 19 | bool isConnected = false; 20 | bool startNtp = false; 21 | 22 | void init(); 23 | void reloadSettings(); 24 | void connectSTA(); 25 | void startSTA(); 26 | void startAP(); 27 | void stopAP(); 28 | void loop(); 29 | 30 | private: 31 | 32 | bool new_wifi_settings = false; 33 | bool new_mqtt_settings = false; 34 | bool check_wifi = false; 35 | Ticker connection_timer; 36 | uint8_t connect_timeout = 10; 37 | void tik(); 38 | }; 39 | 40 | extern NetworkClass NETWORK; 41 | 42 | #endif //HV_CC_LED_DRIVER_NETWORK_H 43 | -------------------------------------------------------------------------------- /src/app_schedule.cpp: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-04-11. 3 | ***/ 4 | #ifndef OTA_ONLY 5 | 6 | #include "Arduino.h" 7 | #include "Ticker.h" 8 | #include 9 | #include 10 | #include "settings.h" 11 | #include "mqtt.h" 12 | #include "app_schedule.h" 13 | 14 | #define PWM_CHANNEL_NUM_MAX MAX_LED_CHANNELS 15 | extern "C" { 16 | #include "pwm.h" 17 | } 18 | 19 | Ticker shedule_refresh_timer; 20 | Ticker transition_timer; 21 | 22 | /* Current local time 23 | * Format: HH:MM 24 | * Update in loop, NOT callback */ 25 | char current_time[6] = {0}; 26 | 27 | #if MAX_LED_CHANNELS == 3 28 | uint8_t ledPin[MAX_LED_CHANNELS] = {LED_CH0_PIN, LED_CH1_PIN, LED_CH2_PIN}; 29 | #elif MAX_LED_CHANNELS == 5 30 | uint8_t ledPin[MAX_LED_CHANNELS] = {LED_CH0_PIN, LED_CH1_PIN, LED_CH2_PIN, LED_CH3_PIN, LED_CH4_PIN}; 31 | #endif 32 | 33 | void ScheduleClass::init() { 34 | /* Setup PWM */ 35 | uint32 pwm_duty_init[PWM_CHANNEL_NUM_MAX]; 36 | 37 | #if MAX_LED_CHANNELS == 3 38 | uint32 io_info[PWM_CHANNEL_NUM_MAX][3] = { 39 | // MUX, FUNC, PIN 40 | // {PERIPHS_IO_MUX_GPIO5_U, FUNC_GPIO5, 5}, // D1 41 | // {PERIPHS_IO_MUX_GPIO4_U, FUNC_GPIO4, 4}, // D2 42 | // {PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0, 0}, // D3 43 | // {PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2, 2}, // D4 44 | {PERIPHS_IO_MUX_MTMS_U, FUNC_GPIO14, 14}, // D5 45 | {PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12, 12}, // D6 46 | {PERIPHS_IO_MUX_MTCK_U, FUNC_GPIO13, 13}, // D7 47 | // {PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15 ,15}, // D8 48 | // D0 - not have PWM :-( 49 | }; 50 | #elif MAX_LED_CHANNELS == 5 51 | uint32 io_info[PWM_CHANNEL_NUM_MAX][3] = { 52 | // MUX, FUNC, PIN 53 | {PERIPHS_IO_MUX_GPIO4_U, FUNC_GPIO4, 4}, // D2 54 | {PERIPHS_IO_MUX_GPIO5_U, FUNC_GPIO5, 5}, // D1 55 | {PERIPHS_IO_MUX_MTCK_U, FUNC_GPIO13, 13}, // D7 56 | {PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12, 12}, // D6 57 | {PERIPHS_IO_MUX_MTMS_U, FUNC_GPIO14, 14}, // D5 58 | // {PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0, 0}, // D3 59 | // {PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2, 2}, // D4 60 | // {PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15 ,15}, // D8 61 | // D0 - not have PWM :-( 62 | }; 63 | #endif 64 | for (unsigned int i=0; i < MAX_LED_CHANNELS; i++) { 65 | pwm_duty_init[i] = 0; 66 | pinMode(ledPin[i], OUTPUT); 67 | } 68 | 69 | pwm_init(LIGHT_MAX_PWM, pwm_duty_init, PWM_CHANNEL_NUM_MAX, io_info); 70 | pwm_start(); 71 | 72 | 73 | 74 | /* Read RTC MEM packet */ 75 | led_state_rtc_mem_t rtc_mem; 76 | system_rtc_mem_read(RTC_LED_ADDR, &rtc_mem, sizeof(rtc_mem)); 77 | 78 | /* Check Magic if OK, apply RTC backup */ 79 | if (rtc_mem.magic_number == RTC_LED_MAGIC) { 80 | LOG_SCHEDULE("[SCHEDULE] RTC Magic is OK: %04x\n", rtc_mem.magic_number); 81 | 82 | /* Restore brightness */ 83 | brightness = rtc_mem.led_brightness; 84 | 85 | for (unsigned int i=0; i < MAX_LED_CHANNELS; i++) { 86 | /* Restore channel state */ 87 | channel[i].target_duty = rtc_mem.target_duty[i]; 88 | LOG_SCHEDULE("[SCHEDULE] GET RTC MEM LED %d Brightness: %d Target: %d\n", i, brightness, channel[i].target_duty); 89 | } 90 | } else { 91 | /* Apply initial led channel output state */ 92 | LOG_SCHEDULE("[SCHEDULE] LOAD LED Defaults from flash\n"); 93 | 94 | /* Restore brightness 95 | * ToDo: store brightness in flash */ 96 | brightness = MAX_BRIGHTNESS; 97 | 98 | for (uint8_t i=0; i < MAX_LED_CHANNELS; i++) { 99 | led_t * led = CONFIG.getLED(i); 100 | channel[i].target_duty = led->last_duty; 101 | } 102 | } 103 | 104 | /* Setup refresh timer */ 105 | shedule_refresh_timer.attach_ms(1000, std::bind(&ScheduleClass::refresh, this)); 106 | } 107 | 108 | void ScheduleClass::refresh() { 109 | process_schedule = true; 110 | } 111 | 112 | int ScheduleClass::minutesLeft(time_t t, uint8_t schedule_hour, uint8_t schedule_minute) { 113 | auto now_hour = (uint8_t)hour(t); 114 | auto now_minute = (uint8_t)minute(t); 115 | return (schedule_hour - now_hour) * 60 + schedule_minute - now_minute; 116 | } 117 | 118 | /* Return String in HH:MM format */ 119 | char * ScheduleClass::getCurrentTimeString() { 120 | return current_time; 121 | } 122 | 123 | /* Light loop */ 124 | void ScheduleClass::loop() { 125 | /* Every second action, check Schedule */ 126 | if (process_schedule) { 127 | /* wait next timer event */ 128 | process_schedule = false; 129 | 130 | /* get local time and store for later use */ 131 | time_t local_time = now(); 132 | auto now_hour = (uint8_t)hour(local_time); 133 | auto now_minute = (uint8_t)minute(local_time); 134 | snprintf(current_time, 6, "%02u:%02u", now_hour, now_minute); 135 | 136 | for (uint8_t j = 0; j < MAX_SCHEDULE; ++j) { 137 | schedule_t * _schedule = CONFIG.getSchedule(j); 138 | 139 | if (_schedule->active) 140 | { 141 | /* once, every min check */ 142 | if ((second(local_time) == 0)) { 143 | 144 | /* Count Minutes left */ 145 | auto left = (int) (minutesLeft(local_time, _schedule->time_hour, _schedule->time_minute)); 146 | 147 | LOG_SCHEDULE("[SCHEDULE] Now: %s Next Point: %d:%d Minutes left: %d\n", current_time, 148 | _schedule->time_hour, 149 | _schedule->time_minute, left); 150 | 151 | /* Set target duty from schedule */ 152 | if (left == 0) { 153 | for (uint8_t i = 0; i < MAX_LED_CHANNELS; ++i) { 154 | brightness = _schedule->brightness; 155 | channel[i].target_duty = _schedule->channel[i]; 156 | 157 | LOG_SCHEDULE("[SCHEDULE] LED %d current: %d target: %d real: %d brightness: %d left: %d\n", 158 | i, channel[i].current_duty, channel[i].target_duty, channel[i].real_duty, _schedule->brightness, left); 159 | 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | /* check for pending transition */ 167 | updateChannels(); 168 | } 169 | } 170 | 171 | void ScheduleClass::updateChannels() { 172 | bool transition = false; 173 | 174 | for (uint8_t i = 0; i < MAX_LED_CHANNELS; ++i) { 175 | 176 | /* Calc Real Duty */ 177 | uint8_t real_duty = channel[i].current_duty * brightness / MAX_BRIGHTNESS; 178 | 179 | if (channel[i].current_duty != channel[i].target_duty || channel[i].real_duty != real_duty) { 180 | 181 | /* Setup transition */ 182 | channel[i].steps_left = 50; 183 | transition = true; 184 | } 185 | } 186 | 187 | /* Start transition */ 188 | if (transition) { 189 | transition_timer.attach_ms(10, std::bind(&ScheduleClass::transition, this)); 190 | } 191 | 192 | } 193 | 194 | /* Save channel state to RTC Memory */ 195 | void ScheduleClass::saveChannelState() { 196 | /* Prepare RTC MEM */ 197 | led_state_rtc_mem_t rtc_mem = {}; 198 | rtc_mem.magic_number = RTC_LED_MAGIC; 199 | rtc_mem.led_brightness = brightness; 200 | 201 | for (uint8_t i = 0; i < MAX_LED_CHANNELS; ++i) { 202 | /* Save target duty and channel state */ 203 | rtc_mem.target_duty[i] = channel[i].target_duty; 204 | 205 | /* Duty or Brightness changed */ 206 | LOG_SCHEDULE("[SCHEDULE] RTC MEM SET LED %u Brightness: %u Target: %u\n", i, rtc_mem.led_brightness, rtc_mem.target_duty[i]); 207 | 208 | } 209 | 210 | /* Save channels to RTC mem */ 211 | system_rtc_mem_write(RTC_LED_ADDR, &rtc_mem, sizeof(rtc_mem)); 212 | } 213 | 214 | /* LED channels update status every second, the transition should be faster than this period */ 215 | void ScheduleClass::transition() { 216 | 217 | uint32_t steps_left = 0; 218 | for (uint8_t i = 0; i < MAX_LED_CHANNELS; ++i) { 219 | 220 | if (channel[i].steps_left > 0) { 221 | channel[i].steps_left--; 222 | 223 | if (channel[i].steps_left == 0) { 224 | channel[i].current_duty = channel[i].target_duty; 225 | } else { 226 | double difference = (0.0f + channel[i].target_duty - channel[i].current_duty) / (channel[i].steps_left + 1); 227 | channel[i].current_duty = (uint32_t) fabs(0.0f + channel[i].current_duty + difference); 228 | } 229 | 230 | /* Calc Real Duty */ 231 | uint8_t real_duty = channel[i].current_duty * brightness / MAX_BRIGHTNESS; 232 | 233 | /* Brightness changed */ 234 | if (channel[i].real_duty != real_duty) { 235 | double difference = (0.0f + real_duty - channel[i].real_duty) / (channel[i].steps_left + 1); 236 | channel[i].real_duty = (uint32_t) fabs(0.0f + channel[i].real_duty + difference); 237 | } 238 | 239 | /* Apply New Duty */ 240 | pwm_set_duty(toPWM(channel[i].real_duty), i); 241 | 242 | /* count steps left */ 243 | steps_left += channel[i].steps_left; 244 | } 245 | } 246 | 247 | /* END Transition if steps_left CH0 + CH1 + etc = 0 */ 248 | if (steps_left == 0 ) { 249 | /* Stop transition */ 250 | transition_timer.detach(); 251 | 252 | /* Publish channel duty */ 253 | publishChannelDuty(); 254 | 255 | /* Publish brightness */ 256 | publishBrightness(); 257 | 258 | /* Publish channel state */ 259 | publishChannelState(); 260 | 261 | /* Save channel state to RTC Memory */ 262 | saveChannelState(); 263 | } 264 | 265 | /* Apply new pwm */ 266 | pwm_start(); 267 | } 268 | 269 | /* Return 0 -> 100 Brightness Value */ 270 | uint8_t ScheduleClass::getBrightness() { 271 | return brightness; 272 | } 273 | 274 | /* Set New Brightness Value */ 275 | void ScheduleClass::setBrightness(uint8_t newBrightness) { 276 | if (newBrightness != brightness) { 277 | brightness = newBrightness; 278 | } else { 279 | return; 280 | } 281 | } 282 | 283 | /* Return 0 -> 100 Duty Value */ 284 | uint8_t ScheduleClass::getChannelDuty(uint8_t id) { 285 | return channel[id].current_duty; 286 | } 287 | 288 | uint8_t ScheduleClass::getTargetChannelDuty(uint8_t id) { 289 | return channel[id].target_duty; 290 | } 291 | 292 | /* Set New Channel Duty */ 293 | void ScheduleClass::setChannelDuty(uint8_t id, uint8_t duty) { 294 | channel[id].target_duty = duty; 295 | } 296 | 297 | /* Convert from 0 - MAX_DUTY to 0 - 5000 or 2500 PWM Duty */ 298 | uint32_t ScheduleClass::toPWM(uint8_t value) { 299 | return value * LIGHT_MAX_PWM / MAX_DUTY; 300 | } 301 | 302 | ScheduleClass SCHEDULE; 303 | 304 | #endif -------------------------------------------------------------------------------- /src/app_schedule.h: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-04-11. 3 | ***/ 4 | 5 | #ifndef HV_CC_LED_DRIVER_APP_SCHEDULE_H 6 | #define HV_CC_LED_DRIVER_APP_SCHEDULE_H 7 | 8 | #include "settings.h" 9 | #include "Ticker.h" 10 | 11 | #ifndef DEBUG_UI_PORT 12 | #define DEBUG_UI_PORT Serial 13 | #endif 14 | 15 | #ifdef DEBUG_SCHEDULE 16 | #define LOG_SCHEDULE(...) DEBUG_UI_PORT.printf( __VA_ARGS__ ) 17 | #else 18 | #define LOG_SCHEDULE(...) 19 | #endif 20 | 21 | #define MAX_BRIGHTNESS 255 22 | #define MAX_DUTY 255 23 | #define RTC_LED_MAGIC 0xAECE00EF 24 | #define RTC_LED_ADDR 65 25 | 26 | typedef struct { 27 | uint8_t current_duty; 28 | uint8_t target_duty; 29 | uint8_t real_duty; 30 | uint32_t steps_left; 31 | } led_schedule_t; 32 | 33 | typedef struct { 34 | uint8_t target_duty[MAX_LED_CHANNELS]; 35 | uint8_t led_brightness; 36 | uint32 magic_number; 37 | } led_state_rtc_mem_t; 38 | 39 | 40 | 41 | class ScheduleClass { 42 | 43 | public: 44 | void init(); 45 | void refresh(); 46 | static char * getCurrentTimeString(); 47 | void loop(); 48 | uint8_t getChannelDuty(uint8_t channel); 49 | void setChannelDuty(uint8_t channel, uint8_t duty); 50 | uint8_t getTargetChannelDuty(uint8_t id); 51 | uint8_t getBrightness(); 52 | void setBrightness(uint8_t newBrightness); 53 | uint8_t getChannelRealDuty(uint8_t channel); 54 | 55 | private: 56 | bool process_schedule = true; 57 | uint8_t brightness = MAX_BRIGHTNESS; 58 | led_schedule_t channel[MAX_LED_CHANNELS] = {}; 59 | 60 | void saveChannelState(); 61 | void updateChannels(); 62 | void transition(); 63 | static uint32_t toPWM(uint8_t percent); 64 | static int minutesLeft(time_t t, unsigned char schedule_hour, unsigned char schedule_minute); 65 | }; 66 | 67 | extern ScheduleClass SCHEDULE; 68 | 69 | #endif //HV_CC_LED_DRIVER_APP_SCHEDULE_H 70 | -------------------------------------------------------------------------------- /src/led.cpp: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-08. 3 | ***/ 4 | 5 | #include "Arduino.h" 6 | #include "Schedule.h" 7 | #include "Ticker.h" 8 | #include "settings.h" 9 | #include "led.h" 10 | 11 | 12 | uint8_t modes[] = { 13 | 0B00000000, /* Off */ 14 | 0B11111111, /* On */ 15 | 0B00001111, /* Half second blinking */ 16 | 0B00000001, /* Short flash once per second */ 17 | 0B00000101, /* Two short flashes once a second */ 18 | 0B00010101, /* Three short flashes once a second */ 19 | 0B01010101 /* Frequent short flashes (4 times per second) */ 20 | }; 21 | uint8_t blink_loop = 0; 22 | uint8_t blink_mode = 0; 23 | 24 | void LEDClass::init() { 25 | /* Setup Led Info */ 26 | pinMode(INFO_LED_PIN, OUTPUT); 27 | 28 | /* Setup blink timer 125 msec period */ 29 | led_blink_timer.once_ms(125, std::bind(&LEDClass::refresh, this)); 30 | 31 | /* Defaul mode */ 32 | blink_mode = modes[TWO_SHORT_BLINK]; 33 | } 34 | 35 | void LEDClass::refresh() { 36 | /* iterate on every bit "blink_mode", 1 = LED ON, 0 = LED OFF */ 37 | if( blink_mode & 1<<(blink_loop & 0x07) ) { 38 | digitalWrite(INFO_LED_PIN, LOW); 39 | } else { 40 | digitalWrite(INFO_LED_PIN, HIGH); 41 | } 42 | 43 | if ((blink_loop & 0x07) == 7) { 44 | /* pause */ 45 | led_blink_timer.once_ms(5000, std::bind(&LEDClass::refresh, this)); 46 | } else { 47 | led_blink_timer.once_ms(125, std::bind(&LEDClass::refresh, this)); 48 | } 49 | 50 | blink_loop++; 51 | } 52 | 53 | void LEDClass::setMode(led_mode_t mode) { 54 | blink_mode = modes[mode]; 55 | } 56 | 57 | LEDClass LED; -------------------------------------------------------------------------------- /src/led.h: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-08. 3 | ***/ 4 | 5 | #ifndef HV_CC_LED_DRIVER_LED_H 6 | #define HV_CC_LED_DRIVER_LED_H 7 | 8 | #include "Ticker.h" 9 | 10 | typedef enum { 11 | OFF, ON, HALF_SEC, ONE_SHORT_BLINK, TWO_SHORT_BLINK, THREE_SHORT_BLINK, SHORT_BLINKS 12 | } led_mode_t; 13 | 14 | class LEDClass { 15 | public: 16 | void init(); 17 | void loop(); 18 | void setMode(led_mode_t mode); 19 | 20 | private: 21 | Ticker led_blink_timer; 22 | bool led_refresh = false; 23 | void refresh(); 24 | }; 25 | 26 | extern LEDClass LED; 27 | 28 | #endif //HV_CC_LED_DRIVER_LED_H 29 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "app_schedule.h" 11 | #include "settings.h" 12 | #include "webui.h" 13 | #include "led.h" 14 | #include "Network.h" 15 | #include "mqtt.h" 16 | #include "status.h" 17 | 18 | #ifndef OTA_ONLY 19 | AsyncWebServer server(80); 20 | #endif 21 | 22 | void setup() { 23 | Serial.begin(115200); 24 | 25 | /* Load all CONFIG from eeprom */ 26 | CONFIG.init(); 27 | 28 | /* Setup and start WiFi */ 29 | NETWORK.init(); 30 | 31 | /* OTA Setup */ 32 | ArduinoOTA.onStart([]() { 33 | String type; 34 | if (ArduinoOTA.getCommand() == U_FLASH) { 35 | type = "sketch"; 36 | } else { // U_SPIFFS 37 | type = "filesystem"; 38 | } 39 | 40 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() 41 | Serial.println("Start updating " + type); 42 | }); 43 | ArduinoOTA.onEnd([]() { 44 | Serial.println("\nEnd"); 45 | }); 46 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 47 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 48 | }); 49 | ArduinoOTA.onError([](ota_error_t error) { 50 | Serial.printf("Error[%u]: ", error); 51 | if (error == OTA_AUTH_ERROR) { 52 | Serial.println("Auth Failed"); 53 | } else if (error == OTA_BEGIN_ERROR) { 54 | Serial.println("Begin Failed"); 55 | } else if (error == OTA_CONNECT_ERROR) { 56 | Serial.println("Connect Failed"); 57 | } else if (error == OTA_RECEIVE_ERROR) { 58 | Serial.println("Receive Failed"); 59 | } else if (error == OTA_END_ERROR) { 60 | Serial.println("End Failed"); 61 | } 62 | }); 63 | ArduinoOTA.begin(); 64 | 65 | /* Led setup */ 66 | LED.init(); 67 | 68 | #ifndef OTA_ONLY 69 | /* Scheduler setup */ 70 | SCHEDULE.init(); 71 | 72 | /* MQTT setup */ 73 | initMqtt(); 74 | 75 | /* Initiate WebUi and attach your Async webserver instance */ 76 | WEBUI.init(server); 77 | server.begin(); 78 | #endif 79 | } 80 | 81 | void loop() { 82 | /* OTA Loop */ 83 | ArduinoOTA.handle(); 84 | 85 | /* check connection loop */ 86 | NETWORK.loop(); 87 | 88 | #ifndef OTA_ONLY 89 | /* Schedule loop */ 90 | SCHEDULE.loop(); 91 | 92 | /* Device status */ 93 | statusLoop(); 94 | #endif 95 | } -------------------------------------------------------------------------------- /src/mqtt.cpp: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-16. 3 | ***/ 4 | 5 | #ifndef OTA_ONLY 6 | 7 | #include "Arduino.h" 8 | #include 9 | #include 10 | #include "ArduinoJson.h" 11 | #include 12 | #include 13 | #include "settings.h" 14 | #include "app_schedule.h" 15 | #include "mqtt.h" 16 | #include "status.h" 17 | #include "Network.h" 18 | 19 | AsyncMqttClient mqttClient; 20 | Ticker mqttReconnectTimer; 21 | bool mqtt_enabled = false; 22 | bool isConnected = false; 23 | uint8_t mqtt_qos = 0; 24 | 25 | void onMqttConnect(bool sessionPresent) { 26 | LOG_MQTT("[MQTT] Connected to server. \n"); 27 | isConnected = true; 28 | 29 | char buf[128]; 30 | 31 | /* Subscribe to Set Duty topic 32 | * [hostname]/channel/[channel_number]/set 33 | * */ 34 | for (int i = 0; i < MAX_LED_CHANNELS; ++i) { 35 | /* make topic string */ 36 | snprintf(buf, 128, "%s/channel/%d/set", CONFIG.getHostname(), i); 37 | LOG_MQTT("[MQTT] Subscribe to topic: %s\n", buf); 38 | 39 | /* subscribe to topic QoS */ 40 | if (!mqttClient.subscribe(buf, mqtt_qos)) 41 | LOG_MQTT("[MQTT] ERROR Subscribe to topic: %s\n", buf); 42 | } 43 | 44 | /* Subscribe to Switch topic 45 | * [hostname]/channel/[channel_number]/switch 46 | * */ 47 | for (int i = 0; i < MAX_LED_CHANNELS; ++i) { 48 | /* make topic string */ 49 | snprintf(buf, 128, "%s/channel/%d/switch", CONFIG.getHostname(), i); 50 | LOG_MQTT("[MQTT] Subscribe to topic: %s\n", buf); 51 | 52 | /* subscribe to topic QoS */ 53 | if (!mqttClient.subscribe(buf, mqtt_qos)) 54 | LOG_MQTT("[MQTT] ERROR Subscribe to topic: %s\n", buf); 55 | } 56 | 57 | /* Subscribe to Brightness topic 58 | * [hostname]/brightness/set 59 | * */ 60 | 61 | /* make topic string */ 62 | snprintf(buf, 128, "%s/brightness/set", CONFIG.getHostname()); 63 | LOG_MQTT("[MQTT] Subscribe to topic: %s\n", buf); 64 | 65 | /* subscribe to topic QoS */ 66 | if (!mqttClient.subscribe(buf, mqtt_qos)) 67 | LOG_MQTT("[MQTT] ERROR Subscribe to topic: %s\n", buf); 68 | 69 | /* publish current state */ 70 | publishBrightness(); 71 | publishChannelDuty(); 72 | publishChannelState(); 73 | 74 | } 75 | 76 | void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { 77 | LOG_MQTT("[MQTT] Disconnected from server. Reason: %d\n", (int)reason); 78 | isConnected = false; 79 | mqttReconnectTimer.once(2, connectToMqtt); 80 | } 81 | 82 | void onMqttSubscribe(uint16_t packetId, uint8_t qos) { 83 | LOG_MQTT("[MQTT] Subscribe acknowledged:\n"); 84 | LOG_MQTT("\tpacketID %d QoS: %d\n", packetId, qos); 85 | } 86 | 87 | void onMqttUnsubscribe(uint16_t packetId) { 88 | LOG_MQTT("[MQTT] Unsubscribe acknowledged:\n"); 89 | LOG_MQTT("\tpacketID: %d\n", packetId); 90 | } 91 | 92 | /* Topic examples: 93 | * Brightness: LED_11324571/brightness | payload: 0-255 94 | * Set Duty: LED_11324571/channel/0/set | payload: 0-255 95 | * Switch: LED_11324571/channel/0/switch | payload: 0-1 96 | * */ 97 | void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { 98 | LOG_MQTT("[MQTT] Publish received:\n"); 99 | LOG_MQTT("\ttopic: %s, qos: %d, dup: %d, retain: %d, len: %d, index: %d, total: %d\n", 100 | topic, properties.qos, properties.dup, properties.retain, len, index, total); 101 | 102 | char buf[128]; 103 | 104 | /* check brightness topic */ 105 | snprintf(buf, 128, "%s/brightness/set", CONFIG.getHostname()); 106 | 107 | /* Brightness received */ 108 | if (strncmp(topic, buf, 128) == 0) { 109 | uint8_t brightness = atoi(payload) & 0xff; 110 | 111 | LOG_MQTT("[MQTT] Set Brightness: %d\n", brightness); 112 | SCHEDULE.setBrightness(brightness); 113 | return; 114 | } 115 | 116 | /* check switch or set command */ 117 | uint32_t channel = 0; 118 | char command[7]; 119 | char scan[128]; 120 | 121 | /* "sscanf" template example: LED_11324571/channel/%u/%6s */ 122 | snprintf(scan, 128, "%s/channel/%%u/%%6s", CONFIG.getHostname()); 123 | sscanf(topic, scan, &channel, command); 124 | 125 | LOG_MQTT("[MQTT] Command: %s payload: %s\n", command, payload); 126 | 127 | if (strncmp(command, "set", 3) == 0) { 128 | /* Set command */ 129 | uint8_t duty = atoi(payload) & 0xff; 130 | LOG_MQTT("[MQTT] Set Duty: %u Channel: %u\n", duty, channel); 131 | SCHEDULE.setChannelDuty(channel, duty); 132 | 133 | } else if (strncmp(command, "switch", 6) == 0) { 134 | /* switch command */ 135 | uint8_t state = atoi(payload) & 0xff; 136 | LOG_MQTT("[MQTT] Set State Channel: %u to %u\n", channel, state); 137 | if (state) { 138 | /* Set max Duty */ 139 | SCHEDULE.setChannelDuty(channel, MAX_DUTY); 140 | } else { 141 | /* Off channel */ 142 | SCHEDULE.setChannelDuty(channel, 0); 143 | } 144 | } 145 | } 146 | 147 | void onMqttPublish(uint16_t packetId) { 148 | LOG_MQTT("[MQTT] Publish acknowledged:\n"); 149 | LOG_MQTT("\tpacketID: %d\n", packetId); 150 | } 151 | 152 | void initMqtt() { 153 | services_t * services = CONFIG.getService(); 154 | if (services->enable_mqtt) { 155 | mqtt_enabled = true; 156 | mqtt_qos = services->mqtt_qos; 157 | mqttClient.onConnect(onMqttConnect); 158 | mqttClient.onDisconnect(onMqttDisconnect); 159 | mqttClient.onSubscribe(onMqttSubscribe); 160 | mqttClient.onUnsubscribe(onMqttUnsubscribe); 161 | mqttClient.onMessage(onMqttMessage); 162 | mqttClient.onPublish(onMqttPublish); 163 | 164 | /* Load config from eeprom */ 165 | mqttClient.setServer(IPAddress(services->mqtt_server), services->mqtt_port); 166 | mqttClient.setClientId(services->hostname); 167 | 168 | LOG_MQTT("[MQTT] Server: %s Port: %d Client ID: %s\n", IPAddress(services->mqtt_server).toString().c_str(), services->mqtt_port, services->hostname); 169 | 170 | if ( (strlen(services->mqtt_user) > 0) && (strlen(services->mqtt_password) > 0) ) { 171 | mqttClient.setCredentials(services->mqtt_user, services->mqtt_password); 172 | LOG_MQTT("[MQTT] User:%s Password:%s\n", services->mqtt_user, services->mqtt_password); 173 | } 174 | } 175 | } 176 | 177 | void connectToMqtt() { 178 | /* return if MQTT Disabled */ 179 | if (!mqtt_enabled) 180 | return; 181 | 182 | /* return if not connected to AP */ 183 | if (!NETWORK.isConnected) 184 | return; 185 | 186 | LOG_MQTT("[MQTT] Connecting...\n"); 187 | mqttClient.connect(); 188 | } 189 | 190 | /* Publish all channels brightness */ 191 | void publishBrightness() { 192 | if (!mqtt_enabled || !isConnected) 193 | return; 194 | 195 | char buf[128]; 196 | char message_buf[10]; 197 | 198 | /* make topic string */ 199 | snprintf(buf, 128, "%s/brightness", CONFIG.getHostname()); 200 | 201 | /* make message string */ 202 | snprintf(message_buf, 128, "%d", SCHEDULE.getBrightness()); 203 | LOG_MQTT("[MQTT] Publish Brightness: %u\n", SCHEDULE.getBrightness()); 204 | 205 | /* publish led status to topic QoS 0, Retain */ 206 | if (!mqttClient.publish(buf, mqtt_qos, true, message_buf, strlen(message_buf))) 207 | LOG_MQTT("[MQTT] ERROR Publish to topic: %s\n", buf); 208 | } 209 | 210 | /* Publish led channels current duty 211 | * [hostname]/channel/[channel_number] 212 | * */ 213 | void publishChannelDuty() { 214 | if (!mqtt_enabled || !isConnected) 215 | return; 216 | 217 | char buf[128]; 218 | char message_buf[10]; 219 | 220 | /* Publish */ 221 | for (uint8_t i = 0; i < MAX_LED_CHANNELS; ++i) { 222 | /* make topic string */ 223 | snprintf(buf, 128, "%s/channel/%d", CONFIG.getHostname(), i); 224 | 225 | /* make message string */ 226 | snprintf(message_buf, 128, "%d", SCHEDULE.getChannelDuty(i)); 227 | LOG_MQTT("[MQTT] Publish Channel: %u duty: %u.\n", i, SCHEDULE.getChannelDuty(i)); 228 | 229 | /* publish led status to topic QoS 0, Retain */ 230 | if (!mqttClient.publish(buf, mqtt_qos, true, message_buf, strlen(message_buf))) 231 | LOG_MQTT("[MQTT] ERROR Publish to topic: %s\n", buf); 232 | } 233 | } 234 | 235 | /* Publish led channels current state 236 | * [hostname]/channel/[channel_number]/state 237 | * */ 238 | void publishChannelState() { 239 | if (!mqtt_enabled || !isConnected) 240 | return; 241 | 242 | char buf[128]; 243 | char message_buf[10]; 244 | 245 | /* Publish */ 246 | for (uint8_t i = 0; i < MAX_LED_CHANNELS; ++i) { 247 | /* make topic string */ 248 | snprintf(buf, 128, "%s/channel/%d/state", CONFIG.getHostname(), i); 249 | 250 | /* make message string */ 251 | uint8_t state = 0; 252 | if (SCHEDULE.getChannelDuty(i) > 0) { 253 | state = 1; 254 | } 255 | 256 | snprintf(message_buf, 128, "%d", state); 257 | LOG_MQTT("[MQTT] Publish Channel: %u state: %u.\n", i, state); 258 | 259 | /* publish led status to topic QoS 0, Retain */ 260 | if (!mqttClient.publish(buf, mqtt_qos, true, message_buf, strlen(message_buf))) 261 | LOG_MQTT("[MQTT] ERROR Publish to topic: %s\n", buf); 262 | } 263 | } 264 | 265 | /* Publish device status 266 | * [hostname]/status 267 | * */ 268 | void publishDeviceStatusToMqtt() { 269 | if (!mqtt_enabled || !isConnected) 270 | return; 271 | 272 | char buf[128]; 273 | char message_buf[512]; 274 | 275 | /* make topic string */ 276 | snprintf(buf, 128, "%s/status", CONFIG.getHostname()); 277 | 278 | /* make message json string */ 279 | const size_t capacity = JSON_OBJECT_SIZE(8) + 256; 280 | DynamicJsonDocument doc(capacity); 281 | JsonObject root = doc.to(); 282 | 283 | /* get device info */ 284 | status_t * device_info = getDeviceInfo(); 285 | 286 | root["up_time"] = NTP.getUptimeString(); 287 | root["chip_id"] = device_info->chip_id; 288 | root["free_heap"] = device_info->free_heap; 289 | root["cpu_freq"] = device_info->cpu_freq; 290 | root["vcc"] = device_info->vcc; 291 | root["wifi_mode"] = device_info->wifi_mode; 292 | root["ip_address"] = device_info->ip_address; 293 | root["mac_address"] = device_info->mac_address; 294 | 295 | serializeJson(doc, message_buf, 512); 296 | LOG_MQTT("[MQTT] Publish device status.\n"); 297 | 298 | #ifdef DEBUG_MQTT_JSON 299 | serializeJson(doc, Serial); 300 | Serial.println(); 301 | #endif 302 | 303 | /* publish led status to topic QoS 0, Retain */ 304 | if (!mqttClient.publish(buf, mqtt_qos, true, message_buf, strlen(message_buf))) 305 | LOG_MQTT("[MQTT] ERROR Publish to topic: %s\n", buf); 306 | } 307 | 308 | #endif -------------------------------------------------------------------------------- /src/mqtt.h: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-16. 3 | ***/ 4 | 5 | #ifndef HV_CC_LED_DRIVER_MQTT_H 6 | #define HV_CC_LED_DRIVER_MQTT_H 7 | 8 | #ifndef DEBUG_UI_PORT 9 | #define DEBUG_UI_PORT Serial 10 | #endif 11 | 12 | #ifdef DEBUG_MQTT 13 | #define LOG_MQTT(...) DEBUG_UI_PORT.printf( __VA_ARGS__ ) 14 | #else 15 | #define LOG_MQTT(...) 16 | #endif 17 | 18 | void initMqtt(); 19 | void connectToMqtt(); 20 | void publishBrightness(); 21 | void publishChannelDuty(); 22 | void publishChannelState(); 23 | void publishDeviceStatusToMqtt(); 24 | 25 | #endif //HV_CC_LED_DRIVER_MQTT_H 26 | -------------------------------------------------------------------------------- /src/pwm.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Stefan Brüns 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program; if not, write to the Free Software 16 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | */ 18 | 19 | /* Set the following three defines to your needs */ 20 | 21 | #ifndef SDK_PWM_PERIOD_COMPAT_MODE 22 | #define SDK_PWM_PERIOD_COMPAT_MODE 0 23 | #endif 24 | 25 | #ifndef PWM_MAX_CHANNELS 26 | #define PWM_MAX_CHANNELS 8 27 | #endif 28 | 29 | #define PWM_DEBUG 0 30 | #define PWM_USE_NMI 1 31 | 32 | /* no user servicable parts beyond this point */ 33 | 34 | #define PWM_MAX_TICKS 0x7fffff 35 | #if SDK_PWM_PERIOD_COMPAT_MODE 36 | #define PWM_PERIOD_TO_TICKS(x) (x * 0.2) 37 | #define PWM_DUTY_TO_TICKS(x) (x * 5) 38 | #define PWM_MAX_DUTY (PWM_MAX_TICKS * 0.2) 39 | #define PWM_MAX_PERIOD (PWM_MAX_TICKS * 5) 40 | #else 41 | #define PWM_PERIOD_TO_TICKS(x) (x) 42 | #define PWM_DUTY_TO_TICKS(x) (x) 43 | #define PWM_MAX_DUTY PWM_MAX_TICKS 44 | #define PWM_MAX_PERIOD PWM_MAX_TICKS 45 | #endif 46 | 47 | #include 48 | #include 49 | #include 50 | #include "pwm.h" 51 | 52 | // from SDK hw_timer.c 53 | #define TIMER1_DIVIDE_BY_16 0x0004 54 | #define TIMER1_ENABLE_TIMER 0x0080 55 | 56 | struct pwm_phase { 57 | uint32_t ticks; ///< delay until next phase, in 200ns units 58 | uint16_t on_mask; ///< GPIO mask to switch on 59 | uint16_t off_mask; ///< GPIO mask to switch off 60 | }; 61 | 62 | /* Three sets of PWM phases, the active one, the one used 63 | * starting with the next cycle, and the one updated 64 | * by pwm_start. After the update pwm_next_set 65 | * is set to the last updated set. pwm_current_set is set to 66 | * pwm_next_set from the interrupt routine during the first 67 | * pwm phase 68 | */ 69 | typedef struct pwm_phase (pwm_phase_array)[PWM_MAX_CHANNELS + 2]; 70 | static pwm_phase_array pwm_phases[3]; 71 | static struct { 72 | struct pwm_phase* next_set; 73 | struct pwm_phase* current_set; 74 | uint8_t current_phase; 75 | } pwm_state; 76 | 77 | static uint32_t pwm_period; 78 | static uint32_t pwm_period_ticks; 79 | static uint32_t pwm_duty[PWM_MAX_CHANNELS]; 80 | static uint16_t gpio_mask[PWM_MAX_CHANNELS]; 81 | static uint8_t pwm_channels; 82 | 83 | // 3-tuples of MUX_REGISTER, MUX_VALUE and GPIO number 84 | typedef uint32_t (pin_info_type)[3]; 85 | 86 | struct gpio_regs { 87 | uint32_t out; /* 0x60000300 */ 88 | uint32_t out_w1ts; /* 0x60000304 */ 89 | uint32_t out_w1tc; /* 0x60000308 */ 90 | uint32_t enable; /* 0x6000030C */ 91 | uint32_t enable_w1ts; /* 0x60000310 */ 92 | uint32_t enable_w1tc; /* 0x60000314 */ 93 | uint32_t in; /* 0x60000318 */ 94 | uint32_t status; /* 0x6000031C */ 95 | uint32_t status_w1ts; /* 0x60000320 */ 96 | uint32_t status_w1tc; /* 0x60000324 */ 97 | }; 98 | static struct gpio_regs* gpio = (struct gpio_regs*)(0x60000300); 99 | 100 | struct timer_regs { 101 | uint32_t frc1_load; /* 0x60000600 */ 102 | uint32_t frc1_count; /* 0x60000604 */ 103 | uint32_t frc1_ctrl; /* 0x60000608 */ 104 | uint32_t frc1_int; /* 0x6000060C */ 105 | uint8_t pad[16]; 106 | uint32_t frc2_load; /* 0x60000620 */ 107 | uint32_t frc2_count; /* 0x60000624 */ 108 | uint32_t frc2_ctrl; /* 0x60000628 */ 109 | uint32_t frc2_int; /* 0x6000062C */ 110 | uint32_t frc2_alarm; /* 0x60000630 */ 111 | }; 112 | static struct timer_regs* timer = (struct timer_regs*)(0x60000600); 113 | 114 | static void ICACHE_RAM_ATTR 115 | pwm_intr_handler(void) 116 | { 117 | if ((pwm_state.current_set[pwm_state.current_phase].off_mask == 0) && 118 | (pwm_state.current_set[pwm_state.current_phase].on_mask == 0)) { 119 | pwm_state.current_set = pwm_state.next_set; 120 | pwm_state.current_phase = 0; 121 | } 122 | 123 | do { 124 | // force write to GPIO registers on each loop 125 | asm volatile ("" : : : "memory"); 126 | 127 | gpio->out_w1ts = (uint32_t)(pwm_state.current_set[pwm_state.current_phase].on_mask); 128 | gpio->out_w1tc = (uint32_t)(pwm_state.current_set[pwm_state.current_phase].off_mask); 129 | 130 | uint32_t ticks = pwm_state.current_set[pwm_state.current_phase].ticks; 131 | 132 | pwm_state.current_phase++; 133 | 134 | if (ticks) { 135 | if (ticks >= 16) { 136 | // constant interrupt overhead 137 | ticks -= 9; 138 | timer->frc1_int &= ~FRC1_INT_CLR_MASK; 139 | WRITE_PERI_REG(&timer->frc1_load, ticks); 140 | return; 141 | } 142 | 143 | ticks *= 4; 144 | do { 145 | ticks -= 1; 146 | // stop compiler from optimizing delay loop to noop 147 | asm volatile ("" : : : "memory"); 148 | } while (ticks > 0); 149 | } 150 | 151 | } while (1); 152 | } 153 | 154 | /** 155 | * period: initial period (base unit 1us OR 200ns) 156 | * duty: array of initial duty values, may be NULL, may be freed after pwm_init 157 | * pwm_channel_num: number of channels to use 158 | * pin_info_list: array of pin_info 159 | */ 160 | void ICACHE_FLASH_ATTR 161 | pwm_init(uint32_t period, uint32_t *duty, uint32_t pwm_channel_num, 162 | uint32_t (*pin_info_list)[3]) 163 | { 164 | int i, j, n; 165 | 166 | pwm_channels = pwm_channel_num; 167 | if (pwm_channels > PWM_MAX_CHANNELS) 168 | pwm_channels = PWM_MAX_CHANNELS; 169 | 170 | for (i = 0; i < 3; i++) { 171 | for (j = 0; j < (PWM_MAX_CHANNELS + 2); j++) { 172 | pwm_phases[i][j].ticks = 0; 173 | pwm_phases[i][j].on_mask = 0; 174 | pwm_phases[i][j].off_mask = 0; 175 | } 176 | } 177 | pwm_state.current_set = pwm_state.next_set = 0; 178 | pwm_state.current_phase = 0; 179 | 180 | uint32_t all = 0; 181 | // PIN info: MUX-Register, Mux-Setting, PIN-Nr 182 | for (n = 0; n < pwm_channels; n++) { 183 | pin_info_type* pin_info = &pin_info_list[n]; 184 | PIN_FUNC_SELECT((*pin_info)[0], (*pin_info)[1]); 185 | gpio_mask[n] = 1 << (*pin_info)[2]; 186 | all |= 1 << (*pin_info)[2]; 187 | if (duty) 188 | pwm_set_duty(duty[n], n); 189 | } 190 | GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, all); 191 | GPIO_REG_WRITE(GPIO_ENABLE_W1TS_ADDRESS, all); 192 | 193 | pwm_set_period(period); 194 | 195 | #if PWM_USE_NMI 196 | ETS_FRC_TIMER1_NMI_INTR_ATTACH(pwm_intr_handler); 197 | #else 198 | ETS_FRC_TIMER1_INTR_ATTACH(pwm_intr_handler, NULL); 199 | #endif 200 | TM1_EDGE_INT_ENABLE(); 201 | 202 | timer->frc1_int &= ~FRC1_INT_CLR_MASK; 203 | timer->frc1_ctrl = 0; 204 | 205 | pwm_start(); 206 | } 207 | 208 | __attribute__ ((noinline)) 209 | static uint8_t ICACHE_FLASH_ATTR 210 | _pwm_phases_prep(struct pwm_phase* pwm) 211 | { 212 | uint8_t n, phases; 213 | 214 | for (n = 0; n < pwm_channels + 2; n++) { 215 | pwm[n].ticks = 0; 216 | pwm[n].on_mask = 0; 217 | pwm[n].off_mask = 0; 218 | } 219 | phases = 1; 220 | for (n = 0; n < pwm_channels; n++) { 221 | uint32_t ticks = PWM_DUTY_TO_TICKS(pwm_duty[n]); 222 | if (ticks == 0) { 223 | pwm[0].off_mask |= gpio_mask[n]; 224 | } else if (ticks >= pwm_period_ticks) { 225 | pwm[0].on_mask |= gpio_mask[n]; 226 | } else { 227 | if (ticks < (pwm_period_ticks/2)) { 228 | pwm[phases].ticks = ticks; 229 | pwm[0].on_mask |= gpio_mask[n]; 230 | pwm[phases].off_mask = gpio_mask[n]; 231 | } else { 232 | pwm[phases].ticks = pwm_period_ticks - ticks; 233 | pwm[phases].on_mask = gpio_mask[n]; 234 | pwm[0].off_mask |= gpio_mask[n]; 235 | } 236 | phases++; 237 | } 238 | } 239 | pwm[phases].ticks = pwm_period_ticks; 240 | 241 | // bubble sort, lowest to hightest duty 242 | n = 2; 243 | while (n < phases) { 244 | if (pwm[n].ticks < pwm[n - 1].ticks) { 245 | struct pwm_phase t = pwm[n]; 246 | pwm[n] = pwm[n - 1]; 247 | pwm[n - 1] = t; 248 | if (n > 2) 249 | n--; 250 | } else { 251 | n++; 252 | } 253 | } 254 | 255 | #if PWM_DEBUG 256 | int t = 0; 257 | for (t = 0; t <= phases; t++) { 258 | ets_printf("%d @%d: %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask); 259 | } 260 | #endif 261 | 262 | // shift left to align right edge; 263 | uint8_t l = 0, r = 1; 264 | while (r <= phases) { 265 | uint32_t diff = pwm[r].ticks - pwm[l].ticks; 266 | if (diff && (diff <= 16)) { 267 | uint16_t mask = pwm[r].on_mask | pwm[r].off_mask; 268 | pwm[l].off_mask ^= pwm[r].off_mask; 269 | pwm[l].on_mask ^= pwm[r].on_mask; 270 | pwm[0].off_mask ^= pwm[r].on_mask; 271 | pwm[0].on_mask ^= pwm[r].off_mask; 272 | pwm[r].ticks = pwm_period_ticks - diff; 273 | pwm[r].on_mask ^= mask; 274 | pwm[r].off_mask ^= mask; 275 | } else { 276 | l = r; 277 | } 278 | r++; 279 | } 280 | 281 | #if PWM_DEBUG 282 | for (t = 0; t <= phases; t++) { 283 | ets_printf("%d @%d: %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask); 284 | } 285 | #endif 286 | 287 | // sort again 288 | n = 2; 289 | while (n <= phases) { 290 | if (pwm[n].ticks < pwm[n - 1].ticks) { 291 | struct pwm_phase t = pwm[n]; 292 | pwm[n] = pwm[n - 1]; 293 | pwm[n - 1] = t; 294 | if (n > 2) 295 | n--; 296 | } else { 297 | n++; 298 | } 299 | } 300 | 301 | // merge same duty 302 | l = 0, r = 1; 303 | while (r <= phases) { 304 | if (pwm[r].ticks == pwm[l].ticks) { 305 | pwm[l].off_mask |= pwm[r].off_mask; 306 | pwm[l].on_mask |= pwm[r].on_mask; 307 | pwm[r].on_mask = 0; 308 | pwm[r].off_mask = 0; 309 | } else { 310 | l++; 311 | if (l != r) { 312 | struct pwm_phase t = pwm[l]; 313 | pwm[l] = pwm[r]; 314 | pwm[r] = t; 315 | } 316 | } 317 | r++; 318 | } 319 | phases = l; 320 | 321 | #if PWM_DEBUG 322 | for (t = 0; t <= phases; t++) { 323 | ets_printf("%d @%d: %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask); 324 | } 325 | #endif 326 | 327 | // transform absolute end time to phase durations 328 | for (n = 0; n < phases; n++) { 329 | pwm[n].ticks = 330 | pwm[n + 1].ticks - pwm[n].ticks; 331 | // subtract common overhead 332 | pwm[n].ticks--; 333 | } 334 | pwm[phases].ticks = 0; 335 | 336 | // do a cyclic shift if last phase is short 337 | if (pwm[phases - 1].ticks < 16) { 338 | for (n = 0; n < phases - 1; n++) { 339 | struct pwm_phase t = pwm[n]; 340 | pwm[n] = pwm[n + 1]; 341 | pwm[n + 1] = t; 342 | } 343 | } 344 | 345 | #if PWM_DEBUG 346 | for (t = 0; t <= phases; t++) { 347 | ets_printf("%d +%d: %04x %04x\n", t, pwm[t].ticks, pwm[t].on_mask, pwm[t].off_mask); 348 | } 349 | ets_printf("\n"); 350 | #endif 351 | 352 | return phases; 353 | } 354 | 355 | void ICACHE_FLASH_ATTR 356 | pwm_start(void) 357 | { 358 | pwm_phase_array* pwm = &pwm_phases[0]; 359 | 360 | if ((*pwm == pwm_state.next_set) || 361 | (*pwm == pwm_state.current_set)) 362 | pwm++; 363 | if ((*pwm == pwm_state.next_set) || 364 | (*pwm == pwm_state.current_set)) 365 | pwm++; 366 | 367 | uint8_t phases = _pwm_phases_prep(*pwm); 368 | 369 | // all with 0% / 100% duty - stop timer 370 | if (phases == 1) { 371 | if (pwm_state.next_set) { 372 | #if PWM_DEBUG 373 | ets_printf("PWM stop\n"); 374 | #endif 375 | timer->frc1_ctrl = 0; 376 | ETS_FRC1_INTR_DISABLE(); 377 | } 378 | pwm_state.next_set = NULL; 379 | 380 | GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, (*pwm)[0].on_mask); 381 | GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, (*pwm)[0].off_mask); 382 | 383 | return; 384 | } 385 | 386 | // refresh if not running 387 | if (!pwm_state.next_set) { 388 | #if PWM_DEBUG 389 | ets_printf("PWM refresh\n"); 390 | #endif 391 | pwm_state.current_set = pwm_state.next_set = *pwm; 392 | pwm_state.current_phase = phases - 1; 393 | ETS_FRC1_INTR_ENABLE(); 394 | TIMER_REG_WRITE(FRC1_LOAD_ADDRESS, 0); 395 | timer->frc1_ctrl = TIMER1_DIVIDE_BY_16 | TIMER1_ENABLE_TIMER; 396 | return; 397 | } 398 | 399 | pwm_state.next_set = *pwm; 400 | } 401 | 402 | void ICACHE_FLASH_ATTR 403 | pwm_set_duty(uint32_t duty, uint8_t channel) 404 | { 405 | if (channel >= PWM_MAX_CHANNELS) 406 | return; 407 | 408 | if (duty > PWM_MAX_DUTY) 409 | duty = PWM_MAX_DUTY; 410 | 411 | pwm_duty[channel] = duty; 412 | } 413 | 414 | uint32_t ICACHE_FLASH_ATTR 415 | pwm_get_duty(uint8_t channel) 416 | { 417 | if (channel >= PWM_MAX_CHANNELS) 418 | return 0; 419 | return pwm_duty[channel]; 420 | } 421 | 422 | void ICACHE_FLASH_ATTR 423 | pwm_set_period(uint32_t period) 424 | { 425 | pwm_period = period; 426 | 427 | if (pwm_period > PWM_MAX_PERIOD) 428 | pwm_period = PWM_MAX_PERIOD; 429 | 430 | pwm_period_ticks = PWM_PERIOD_TO_TICKS(period); 431 | } 432 | 433 | uint32_t ICACHE_FLASH_ATTR 434 | pwm_get_period(void) 435 | { 436 | return pwm_period; 437 | } 438 | 439 | uint32_t ICACHE_FLASH_ATTR 440 | get_pwm_version(void) 441 | { 442 | return 1; 443 | } 444 | 445 | void ICACHE_FLASH_ATTR 446 | set_pwm_debug_en(uint8_t print_en) 447 | { 448 | (void) print_en; 449 | } -------------------------------------------------------------------------------- /src/pwm.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Stefan Brüns 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program; if not, write to the Free Software 16 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | */ 18 | 19 | #ifndef HV_CC_LED_DRIVER_PWM_H 20 | #define HV_CC_LED_DRIVER_PWM_H 21 | 22 | //#define LIGHT_MAX_PWM 5000 // * 200ns ^= 1 kHz 23 | #define LIGHT_MAX_PWM 2500 // * 200ns ^= 1 kHz 24 | 25 | /*SUPPORT UP TO 8 PWM CHANNEL*/ 26 | #ifndef PWM_CHANNEL_NUM_MAX 27 | #define PWM_CHANNEL_NUM_MAX 8 28 | #endif 29 | 30 | struct pwm_param { 31 | uint32 period; 32 | uint32 freq; 33 | uint32 duty[PWM_CHANNEL_NUM_MAX]; //PWM_CHANNEL<=8 34 | }; 35 | 36 | /* pwm_init should be called only once, for now */ 37 | void pwm_init(uint32 period, uint32 *duty,uint32 pwm_channel_num,uint32 (*pin_info_list)[3]); 38 | void pwm_start(void); 39 | 40 | void pwm_set_duty(uint32 duty, uint8 channel); 41 | uint32 pwm_get_duty(uint8 channel); 42 | void pwm_set_period(uint32 period); 43 | uint32 pwm_get_period(void); 44 | 45 | uint32 get_pwm_version(void); 46 | void set_pwm_debug_en(uint8 print_en); 47 | 48 | #endif //HV_CC_LED_DRIVER_PWM_H 49 | 50 | 51 | //#define PWM_CHANNELS MAX_LED_CHANNELS 52 | //const uint32_t period = 5000; // * 200ns ^= 1 kHz 53 | // 54 | //// PWM setup 55 | //uint32 io_info[PWM_CHANNELS][3] = { 56 | // // MUX, FUNC, PIN 57 | // {PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12, 12}, 58 | // {PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15, 15}, 59 | // {PERIPHS_IO_MUX_MTCK_U, FUNC_GPIO13, 13}, 60 | // {PERIPHS_IO_MUX_MTMS_U, FUNC_GPIO14, 14}, 61 | // {PERIPHS_IO_MUX_GPIO5_U, FUNC_GPIO5 , 5}, 62 | //}; 63 | // 64 | //// initial duty: all off 65 | //uint32 pwm_duty_init[PWM_CHANNELS] = {0, 0, 0, 0, 0}; 66 | // 67 | //pwm_init(period, pwm_duty_init, PWM_CHANNELS, io_info); 68 | //pwm_start(); 69 | // 70 | //// do something like this whenever you want to change duty 71 | //pwm_set_duty(500, 1); // GPIO15: 10% 72 | //pwm_set_duty(5000, 1); // GPIO15: 100% 73 | //pwm_start(); // commit -------------------------------------------------------------------------------- /src/settings.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Ticker.h" 3 | #include "settings.h" 4 | 5 | Ticker save_timer; 6 | EEPROM_Rotate eepromRotate; 7 | 8 | void Settings::init() { 9 | // EEPROM Initialization 10 | eepromRotate.size(2); 11 | eepromRotate.begin(EEPROM_SIZE); 12 | 13 | #if DEBUG_EEPROM 14 | Serial.printf("[EEPROM] Sector pool size : %u\n", eepromRotate.size()); 15 | Serial.printf("[EEPROM] Sectors in use : "); 16 | for (uint32_t i = 0; i < eepromRotate.size(); i++) { 17 | if (i>0) Serial.print(", "); 18 | Serial.print(eepromRotate.base() - i); 19 | } 20 | Serial.println(); 21 | Serial.printf("[EEPROM] Current sector : %u\n", eepromRotate.current()); 22 | 23 | Serial.println(); 24 | Serial.printf("[EEPROM] Dumping data for sector #%u\n", eepromRotate.current()); 25 | eepromRotate.dump(Serial); 26 | #endif 27 | 28 | const char * empty_str = " "; 29 | 30 | /* init auth configuration */ 31 | eepromRotate.get(AUTH_OFFSET, auth); 32 | if (auth.magic_number != 10) { 33 | auth.magic_number = 10; 34 | strlcpy(auth.login, AUTH_LOGIN, 32); 35 | strlcpy(auth.password, AUTH_PASSWORD, 32); 36 | 37 | /* set default values */ 38 | eepromRotate.put(AUTH_OFFSET, auth); 39 | _update_requested = true; 40 | 41 | } 42 | 43 | /* init network configuration */ 44 | for(size_t i = 0; i < MAX_NETWORKS; i++) 45 | { 46 | LOG_EEPROM("[EEPROM] NETWORK Config ID: %d. \n", i); 47 | eepromRotate.get(NETWORK_OFFSET + sizeof(network_t) * i, network[i]); 48 | if (network[i].magic_number != (20 + i)) { 49 | LOG_EEPROM("[EEPROM] NETWORK Config incorrect: magic %d, loading default. \n", network[i].magic_number); 50 | network[i].magic_number = (uint8_t)(20 + i); 51 | strlcpy(network[i].ssid, empty_str, 32); 52 | strlcpy(network[i].password, empty_str, 32); 53 | 54 | network[i].ip_address[0] = 192; 55 | network[i].ip_address[0] = 168; 56 | network[i].ip_address[0] = 1; 57 | network[i].ip_address[0] = 100; 58 | 59 | network[i].mask[0] = 255; 60 | network[i].mask[0] = 255; 61 | network[i].mask[0] = 255; 62 | network[i].mask[0] = 0; 63 | 64 | network[i].gateway[0] = 192; 65 | network[i].gateway[0] = 168; 66 | network[i].gateway[0] = 1; 67 | network[i].gateway[0] = 1; 68 | 69 | network[i].dns[0] = 192; 70 | network[i].dns[0] = 168; 71 | network[i].dns[0] = 1; 72 | network[i].dns[0] = 1; 73 | 74 | network[i].dhcp = true; 75 | network[i].active = false; /* hide config in web ui */ 76 | 77 | /* set default values */ 78 | eepromRotate.put(NETWORK_OFFSET + sizeof(network_t) * i, network[i]); 79 | _update_requested = true; 80 | 81 | } 82 | LOG_EEPROM("[EEPROM] NETWORK Config ID: %d, Status: %d. \n", i, network[i].active); 83 | 84 | } 85 | 86 | /* init services configuration */ 87 | /* Global */ 88 | eepromRotate.get(SERVICES_OFFSET, service); 89 | if (service.magic_number != 30) { 90 | service.magic_number = 30; 91 | 92 | char hostname_buf[20]; 93 | sprintf(hostname_buf, "LED_%d", ESP.getChipId()); 94 | strlcpy(service.hostname, hostname_buf, 20); 95 | 96 | /* NTP */ 97 | strcpy(service.ntp_server, "es.pool.ntp.org"); 98 | service.utc_offset = 60; 99 | service.ntp_dst = true; 100 | service.enable_ntp = true; 101 | 102 | /*MQTT */ 103 | strcpy(service.mqtt_user, empty_str); 104 | strcpy(service.mqtt_password, empty_str); 105 | service.mqtt_port = 1883; 106 | service.enable_mqtt = false; 107 | service.mqtt_qos = 0; 108 | 109 | /* set default values */ 110 | eepromRotate.put(SERVICES_OFFSET, service); 111 | _update_requested = true; 112 | 113 | } 114 | 115 | /* init led configuration */ 116 | for(size_t i = 0; i < MAX_LED_CHANNELS; i++) 117 | { 118 | eepromRotate.get(LED_OFFSET + sizeof(led_t) * i, led[i]); 119 | if (led[i].magic_number != (40 + i)) { 120 | led[i].magic_number = (uint8_t)(40 + i); 121 | 122 | strlcpy(led[i].color, "#DDEFFF", 8); /* default color #DDEFFF -> 'Cold White' in UI */ 123 | led[i].last_duty = 0; /* 0 - 100 -> channel boot duty */ 124 | led[i].power = 0; /* 0 -> 0.0 in Watts x 10 */ 125 | led[i].state = 1; /* 0 -> OFF | 1 -> ON */ 126 | 127 | /* set default values */ 128 | eepromRotate.put(LED_OFFSET, led); 129 | _update_requested = true; 130 | 131 | } 132 | 133 | } 134 | 135 | /* init schedule configuration */ 136 | for(size_t i = 0; i < MAX_SCHEDULE; i++) 137 | { 138 | eepromRotate.get(SCHEDULE_OFFSET + sizeof(schedule_t) * i, schedule[i]); 139 | if (schedule[i].magic_number != (50 + i)) { 140 | schedule[i].magic_number = (uint8_t)(50 + i); 141 | 142 | schedule[i].time_hour = 0; 143 | schedule[i].time_minute = 0; 144 | schedule[i].channel[0] = 0; /* CH1 Default */ 145 | schedule[i].channel[1] = 0; /* CH2 Default */ 146 | schedule[i].channel[2] = 0; /* CH3 Default */ 147 | #if MAX_LED_CHANNELS == 5 148 | schedule[i].channel[3] = 0; /* CH4 Default */ 149 | schedule[i].channel[4] = 0; /* CH5 Default */ 150 | #endif 151 | schedule[i].brightness = 0; /* All Channels brightness */ 152 | schedule[i].active = false; 153 | 154 | /* set default values */ 155 | eepromRotate.put(SCHEDULE_OFFSET + sizeof(schedule_t) * i, schedule[i]); 156 | _update_requested = true; 157 | 158 | } 159 | 160 | } 161 | 162 | /* save new config if, some values updated */ 163 | if (_update_requested) { 164 | eepromRotate.commit(); 165 | } 166 | 167 | } 168 | 169 | /* Save Param to EEPROM, delayed to avoid wear */ 170 | void Settings::setSettings() { 171 | 172 | /* Store Auth */ 173 | eepromRotate.put(AUTH_OFFSET, auth); 174 | 175 | /* Store Network */ 176 | for(size_t i = 0; i < MAX_NETWORKS; i++) 177 | { 178 | eepromRotate.put(NETWORK_OFFSET + sizeof(network_t) * i, network[i]); 179 | } 180 | 181 | /* Store Service */ 182 | eepromRotate.put(SERVICES_OFFSET, service); 183 | 184 | /* Store Led */ 185 | for(size_t i = 0; i < MAX_LED_CHANNELS; i++) { 186 | eepromRotate.put(LED_OFFSET + sizeof(led_t) * i, led[i]); 187 | } 188 | 189 | /* Store Schedule */ 190 | for(size_t i = 0; i < MAX_SCHEDULE; i++) 191 | { 192 | eepromRotate.put(SCHEDULE_OFFSET + sizeof(schedule_t) * i, schedule[i]); 193 | } 194 | 195 | LOG_EEPROM("[EEPROM] Save timer started.\n"); 196 | 197 | /* Delay to avoid memory wear */ 198 | save_timer.once(1, std::bind(&Settings::save, this)); 199 | 200 | } 201 | 202 | bool Settings::save() { 203 | LOG_EEPROM("[EEPROM] Saved.\n"); 204 | return eepromRotate.commit(); 205 | } 206 | 207 | String Settings::getNtpServerName() { 208 | String name(service.ntp_server); 209 | if (name.length() > 0) 210 | return name; 211 | else 212 | return String("pool.ntp.org"); 213 | } 214 | 215 | int16_t Settings::getNtpOffset() { 216 | return service.utc_offset; 217 | } 218 | 219 | led_t * Settings::getLED(uint8_t id) { 220 | if (id > (MAX_LED_CHANNELS - 1)) id = MAX_LED_CHANNELS - 1; 221 | return &led[id]; 222 | } 223 | 224 | schedule_t * Settings::getSchedule(uint8_t id) { 225 | if (id > MAX_SCHEDULE - 1) id = MAX_SCHEDULE - 1; 226 | return &schedule[id]; 227 | }; 228 | 229 | network_t * Settings::getNetwork(uint8_t id) { 230 | if (id > MAX_NETWORKS - 1) id = MAX_NETWORKS - 1; 231 | return &network[id]; 232 | }; 233 | 234 | services_t * Settings::getService() { 235 | return &service; 236 | }; 237 | 238 | auth_t * Settings::getAuth() { 239 | return &auth; 240 | } 241 | 242 | char * Settings::getHostname() { 243 | return (char *)service.hostname; 244 | } 245 | 246 | /* Store new credentials in EEPROM */ 247 | void Settings::updateAuth() { 248 | eepromRotate.put(AUTH_OFFSET, auth); 249 | eepromRotate.commit(); 250 | } 251 | 252 | /* Erase EEPROM */ 253 | void Settings::erase() { 254 | for (int i = 0; i < EEPROM_SIZE; ++i) { 255 | eepromRotate.put(i, 0xFF); 256 | } 257 | eepromRotate.commit(); 258 | 259 | LOG_EEPROM("[EEPROM] Erased.\n"); 260 | 261 | } 262 | 263 | Settings CONFIG; -------------------------------------------------------------------------------- /src/settings.h: -------------------------------------------------------------------------------- 1 | #ifndef HV_CC_LED_DRIVER_SETTINGS_H 2 | #define HV_CC_LED_DRIVER_SETTINGS_H 3 | 4 | #if defined(ESP8266) || defined(ESP32) 5 | #include "Arduino.h" 6 | #include "stdlib_noniso.h" 7 | #endif 8 | 9 | #if defined(ESP8266) 10 | #define HARDWARE "ESP8266" 11 | #elif defined(ESP32) 12 | #define HARDWARE "ESP32" 13 | #endif 14 | 15 | #ifdef DEBUG_EEPROM 16 | #define LOG_EEPROM(...) DEBUG_UI_PORT.printf( __VA_ARGS__ ) 17 | #else 18 | #define LOG_EEPROM(...) 19 | #endif 20 | 21 | #define INFO_LED_PIN 2 22 | #define EEPROM_SIZE 1024 23 | 24 | /* EEPROM MAP */ 25 | /* |-- Version --|-- Auth --|-- Network --|-- Services --|-- LED --|-- Schedule --| 26 | * |-- 1x8b --|-- 64x8b --|-- 98x8b --|-- 63x8 --|-- 5x13x8b --|-- 12x10x8b --| 27 | * |-- offset 4 --|-- offset --|-- offset 97 --|-- offset 256 --|-- offset 512 --|-- offset 640 --| 28 | * */ 29 | 30 | /* Auth ---------------------- 31 | * 32 bytes of eeprom */ 32 | #define AUTH_OFFSET 32 33 | #define AUTH_LOGIN "admin" 34 | #define AUTH_PASSWORD "12345678" // Web and WIFI AP Password 35 | 36 | 37 | typedef struct { 38 | uint8_t magic_number; 39 | char login[32]; // Web Interface and OTA Update Login 40 | char password[32]; // Web Interface and OTA Update Password 41 | } auth_t; 42 | 43 | /* Network ------------------ 44 | * 49 * 2 45 | * 98 bytes of eeprom */ 46 | #define MAX_NETWORKS 2 47 | #define NETWORK_OFFSET 90 48 | 49 | typedef struct { 50 | uint8_t magic_number; 51 | char ssid[32]; // Wifi SSID Name 52 | char password[32]; // Wifi Password 53 | uint8_t ip_address[4]; // IP Address 54 | uint8_t mask[4]; // Mask 55 | uint8_t gateway[4]; // Gateway 56 | uint8_t dns[4]; // DNS 57 | bool dhcp; // Enable DHCP Client 58 | bool active; // Need send by WS to GUI 59 | } network_t; 60 | 61 | /* Services ------------------ 62 | * 63 bytes of eeprom */ 63 | #define SERVICES_OFFSET 256 64 | 65 | typedef struct { 66 | uint8_t magic_number; 67 | char hostname[20]; // Device Name 68 | char ntp_server[20]; // Wifi SSID Name 69 | int16_t utc_offset; // UTC offset in minutes 70 | bool ntp_dst; // Daylight save 71 | uint8_t mqtt_server[4]; // IP v4 Address Array 72 | uint16_t mqtt_port; // MQTT Server port 1883 default 73 | char mqtt_user[16]; // 16 Char MAX 74 | char mqtt_password[16]; // 16 Char MAX 75 | uint8_t mqtt_qos; 76 | bool enable_ntp; // Enable NTP Service 77 | bool enable_mqtt; // Enable MQTT Service 78 | } services_t; 79 | 80 | /* Led ----------------------- 81 | * 5 * 3 82 | * 15 bytes of eeprom 83 | * */ 84 | #define LED_OFFSET 512 // Led struct eeprom address 85 | #ifndef MAX_LED_CHANNELS 86 | #define MAX_LED_CHANNELS 3 // 8 - MAX 87 | #endif 88 | #if MAX_LED_CHANNELS == 3 89 | #define LED_CH0_PIN 14 // LED 1 PWM pin 90 | #define LED_CH1_PIN 12 // LED 2 PWM pin 91 | #define LED_CH2_PIN 13 // LED 3 PWM pin 92 | #elif MAX_LED_CHANNELS == 5 93 | #define LED_CH0_PIN 5 // LED 1 PWM pin 94 | #define LED_CH1_PIN 4 // LED 2 PWM pin 95 | #define LED_CH2_PIN 13 // LED 3 PWM pin 96 | #define LED_CH3_PIN 12 // LED 4 PWM pin 97 | #define LED_CH4_PIN 14 // LED 5 PWM pin 98 | #endif 99 | 100 | typedef struct { 101 | uint8_t magic_number; 102 | char color[8]; // RGB CSS Hex value FFFFFF -> white 103 | uint8_t last_duty; // On Boot led duty in percentage (0-100%) 104 | uint16_t power; // Real Channel Power in Watts x 10 (0-65535) 100 = 10.0 in Web UI 105 | uint8_t state; // Enable/Disable channel 106 | } led_t; 107 | 108 | /* Schedule ------------------ 109 | * 9 * 10 110 | * 120 bytes of eeprom */ 111 | #define MAX_SCHEDULE 10 112 | #define SCHEDULE_OFFSET 640 113 | 114 | typedef struct { 115 | uint8_t magic_number; 116 | uint8_t time_hour; // Schedule fire hour 117 | uint8_t time_minute; // Schedule fire minutes 118 | uint8_t channel[MAX_LED_CHANNELS]; // Channel Duty in percentage (0-100%) 119 | uint8_t brightness; // All channels brightness in percentage (0-100%) 120 | bool active; // Need send by WS to GUI 121 | } schedule_t; 122 | 123 | class Settings { 124 | 125 | public: 126 | void init(); 127 | void setSettings(); 128 | void updateAuth(); 129 | static void erase(); 130 | 131 | /* NTP */ 132 | String getNtpServerName(); 133 | int16_t getNtpOffset(); 134 | 135 | /* LED */ 136 | led_t * getLED(uint8_t id); 137 | 138 | /* Schedule */ 139 | schedule_t * getSchedule(uint8_t id); 140 | 141 | /* Network */ 142 | network_t * getNetwork(uint8_t id); 143 | 144 | /* Service */ 145 | services_t * getService(); 146 | 147 | /* Auth */ 148 | auth_t * getAuth(); 149 | 150 | /* Hostname */ 151 | char * getHostname(); 152 | 153 | private: 154 | auth_t auth = {};; // Auth initialization 155 | network_t network[MAX_NETWORKS] = {}; // Network initialization 156 | services_t service = {}; // Services initialization 157 | schedule_t schedule[MAX_SCHEDULE] = {}; // Schedule initialization 158 | led_t led[MAX_LED_CHANNELS] = {}; // Led initialization 159 | bool _update_requested = false; 160 | 161 | /* save to internal flash */ 162 | bool save(); 163 | }; 164 | 165 | extern Settings CONFIG; 166 | 167 | #endif //HV_CC_LED_DRIVER_SETTINGS_H -------------------------------------------------------------------------------- /src/status.cpp: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-24. 3 | ***/ 4 | 5 | #include 6 | #include 7 | #include 8 | #include "Ticker.h" 9 | #include "status.h" 10 | #include "mqtt.h" 11 | 12 | Ticker status_refresh_timer; 13 | status_t status = {0}; 14 | bool refresh = true; 15 | 16 | void refresh_tik() { 17 | refresh = true; 18 | }; 19 | 20 | status_t * getDeviceInfo() { 21 | return &status; 22 | } 23 | 24 | void statusLoop() { 25 | if (refresh) { 26 | refresh = false; 27 | 28 | /* get end store static values */ 29 | status.chip_id = system_get_chip_id(); 30 | status.free_heap = system_get_free_heap_size(); 31 | status.cpu_freq = system_get_cpu_freq(); 32 | status.vcc = analogRead(A0); 33 | 34 | /* WiFi Mode */ 35 | status.wifi_mode = wifi_get_opmode(); 36 | 37 | /* IP Address */ 38 | String ip_address; 39 | struct ip_info ip; 40 | wifi_get_ip_info(STATION_IF, &ip); 41 | ip_address = IPAddress(ip.ip.addr).toString(); 42 | strlcpy(status.ip_address, ip_address.c_str(), 16); 43 | 44 | /* WiFi MAC Address */ 45 | uint8_t mac[6]; 46 | char macStr[18] = { 0 }; 47 | wifi_get_macaddr(STATION_IF, mac); 48 | sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); 49 | strlcpy(status.mac_address, macStr, 18); 50 | 51 | /* Publish to MQTT topic {hostname}/status */ 52 | publishDeviceStatusToMqtt(); 53 | 54 | /* Setup refresh timer */ 55 | status_refresh_timer.once(30, refresh_tik); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/status.h: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-05-24. 3 | ***/ 4 | 5 | #ifndef HV_CC_LED_DRIVER_STATUS_H 6 | #define HV_CC_LED_DRIVER_STATUS_H 7 | 8 | #include "Arduino.h" 9 | 10 | typedef struct { 11 | uint32 chip_id; 12 | uint32 free_heap; 13 | uint8_t cpu_freq; 14 | uint16_t vcc; 15 | uint8 wifi_mode; 16 | char ip_address[16]; 17 | char mac_address[18]; 18 | } status_t; 19 | 20 | void refresh_tik(); 21 | void statusLoop(); 22 | status_t * getDeviceInfo(); 23 | 24 | #endif //HV_CC_LED_DRIVER_STATUS_H 25 | -------------------------------------------------------------------------------- /src/webui.cpp: -------------------------------------------------------------------------------- 1 | /*** 2 | ** Created by Aleksey Volkov on 2019-04-04. 3 | ***/ 4 | 5 | #ifndef OTA_ONLY 6 | 7 | #include "AsyncWebSocket.h" 8 | #include "Arduino.h" 9 | #include 10 | #include 11 | #include "AsyncJson.h" 12 | #include "ArduinoJson.h" 13 | #include "settings.h" 14 | #include "webui.h" 15 | #include "app_schedule.h" 16 | #include "webpage.h" 17 | #include "Network.h" 18 | #include "status.h" 19 | 20 | /* Public */ 21 | 22 | void WEBUIClass::init(AsyncWebServer& server) { 23 | /* Pages ----------------------------------- */ 24 | server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { 25 | // Send File 26 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", HTML, HTML_SIZE); 27 | response->addHeader("Content-Encoding", "gzip"); 28 | request->send(response); 29 | }); 30 | 31 | server.on("/schedule", HTTP_GET, [](AsyncWebServerRequest *request) { 32 | // Send File 33 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", HTML, HTML_SIZE); 34 | response->addHeader("Content-Encoding", "gzip"); 35 | request->send(response); 36 | }); 37 | 38 | server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request) { 39 | // Send File 40 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", HTML, HTML_SIZE); 41 | response->addHeader("Content-Encoding", "gzip"); 42 | request->send(response); 43 | }); 44 | 45 | server.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request) { 46 | // Send File 47 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", HTML, HTML_SIZE); 48 | response->addHeader("Content-Encoding", "gzip"); 49 | request->send(response); 50 | }); 51 | 52 | server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request) { 53 | // Send File 54 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", HTML, HTML_SIZE); 55 | response->addHeader("Content-Encoding", "gzip"); 56 | request->send(response); 57 | }); 58 | 59 | 60 | /* JSON ------------------------------------ */ 61 | /* GET device status */ 62 | server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { 63 | // Send Json Response 64 | AsyncJsonResponse *response = new AsyncJsonResponse(); 65 | JsonVariant &root = response->getRoot(); 66 | 67 | /* Device specific */ 68 | root["hardware"] = HARDWARE; 69 | 70 | /* Get service config */ 71 | services_t *service = CONFIG.getService(); 72 | root["mqttService"] = IPAddress(service->mqtt_server).toString(); 73 | root["ntpService"] = service->ntp_server; 74 | 75 | /* Get device info, refresh periodical */ 76 | status_t *device_info = getDeviceInfo(); 77 | 78 | root["upTime"] = NTP.getUptimeString(); 79 | root["localTime"] = SCHEDULE.getCurrentTimeString(); 80 | root["chipId"] = device_info->chip_id; 81 | root["cpuFreq"] = device_info->cpu_freq; 82 | root["vcc"] = device_info->vcc; 83 | root["freeHeap"] = device_info->free_heap; 84 | root["wifiMode"] = device_info->wifi_mode; 85 | root["ipAddress"] = device_info->ip_address; 86 | root["macAddress"] = device_info->mac_address; 87 | root["brightness"] = SCHEDULE.getBrightness(); 88 | 89 | JsonArray leds = root.createNestedArray("channels"); 90 | for (int i = 0; i < MAX_LED_CHANNELS; ++i) { 91 | leds.add(SCHEDULE.getChannelDuty(i)); 92 | } 93 | 94 | response->setLength(); 95 | request->send(response); 96 | }); 97 | 98 | /* GET led channels schedule config */ 99 | server.on("/config/schedule", HTTP_GET, [](AsyncWebServerRequest *request) { 100 | // Send Json Response 101 | AsyncJsonResponse *response = new AsyncJsonResponse(); 102 | JsonVariant& root = response->getRoot(); 103 | 104 | /* Schedule */ 105 | JsonArray schedule = root.createNestedArray("schedule"); 106 | for (uint8_t i = 0; i < MAX_SCHEDULE; i++) 107 | { 108 | /* Get One schedule */ 109 | schedule_t *_schedule = CONFIG.getSchedule(i); 110 | if (_schedule->active) { 111 | JsonObject scheduleItem = schedule.createNestedObject(); 112 | 113 | scheduleItem["time_hour"] = _schedule->time_hour; 114 | scheduleItem["time_minute"] = _schedule->time_minute; 115 | scheduleItem["brightness"] = _schedule->brightness; 116 | 117 | JsonArray ledDuty = scheduleItem.createNestedArray("duty"); 118 | 119 | for (unsigned char p : _schedule->channel) { 120 | ledDuty.add(p); // led channel 121 | } 122 | } 123 | } 124 | 125 | response->setLength(); 126 | request->send(response); 127 | }); 128 | 129 | /* SET led channels schedule config */ 130 | AsyncCallbackJsonWebHandler* schedule_handler = new AsyncCallbackJsonWebHandler("/config/schedule", [](AsyncWebServerRequest *request, JsonVariant &json) { 131 | JsonObject jsonObj = json.as(); 132 | 133 | /* Reset schedule cache */ 134 | for (uint8_t i = 0; i < MAX_SCHEDULE; ++i) { 135 | schedule_t *schedule = CONFIG.getSchedule(i); 136 | schedule->active = false; 137 | } 138 | 139 | /* Parse New Schedule config */ 140 | JsonArray schedules = jsonObj["schedule"]; 141 | for (uint8_t i = 0; i < schedules.size(); ++i) { 142 | if (i < MAX_SCHEDULE) { 143 | /* get point to current schedule config (eeprom cache) */ 144 | schedule_t *schedule = CONFIG.getSchedule(i); 145 | 146 | /* update value in (eeprom cache) */ 147 | schedule->time_hour = schedules[i]["time_hour"].as(); 148 | schedule->time_minute = schedules[i]["time_minute"].as(); 149 | schedule->active = true; 150 | schedule->brightness = schedules[i]["brightness"].as(); 151 | 152 | LOG_WEB("[WEBSERVER] Set Schedule: %d:%d\n", schedule->time_hour, schedule->time_minute); 153 | 154 | for (uint8_t j = 0; j < schedules[i]["duty"].size(); ++j) { 155 | if (j < MAX_LED_CHANNELS) 156 | { 157 | schedule->channel[j] = schedules[i]["duty"][j].as(); 158 | } 159 | } 160 | } 161 | } 162 | 163 | /* Store EEPROM settings (sync cache and eeprom) */ 164 | CONFIG.setSettings(); 165 | 166 | // Send Json Response 167 | AsyncJsonResponse *response = new AsyncJsonResponse(); 168 | JsonVariant& root = response->getRoot(); 169 | 170 | root["save"].set(true); 171 | 172 | response->setLength(); 173 | request->send(response); 174 | }); 175 | 176 | /* GET led channels config */ 177 | server.on("/config/leds", HTTP_GET, [](AsyncWebServerRequest *request) { 178 | // Send Json Response 179 | AsyncJsonResponse *response = new AsyncJsonResponse(); 180 | JsonVariant& root = response->getRoot(); 181 | 182 | /* Leds */ 183 | JsonArray leds = root.createNestedArray("leds"); 184 | for (uint8_t i = 0; i < MAX_LED_CHANNELS; i++) { 185 | JsonObject ledChannel = leds.createNestedObject(); 186 | /* Get single led channel config */ 187 | led_t *_led = CONFIG.getLED(i); 188 | 189 | ledChannel["id"] = i; 190 | ledChannel["color"] = _led->color; 191 | ledChannel["power"] = _led->power; 192 | ledChannel["state"] = _led->state; 193 | ledChannel["last_duty"] = _led->last_duty; 194 | } 195 | 196 | response->setLength(); 197 | request->send(response); 198 | }); 199 | 200 | /* SET led channels schedule config */ 201 | AsyncCallbackJsonWebHandler* leds_handler = new AsyncCallbackJsonWebHandler("/config/leds", [](AsyncWebServerRequest *request, JsonVariant &json) { 202 | JsonObject jsonObj = json.as(); 203 | 204 | /* Parse New LED config */ 205 | JsonArray leds = jsonObj["leds"]; 206 | 207 | for (uint8_t i = 0; i < leds.size(); ++i) { 208 | if (i < MAX_LED_CHANNELS) { 209 | 210 | /* get point to current led config (eeprom cache) */ 211 | led_t *_led = CONFIG.getLED(i); 212 | 213 | /* update value in (eeprom cache) */ 214 | strlcpy(_led->color, leds[i]["color"] | "#DDEFFF", 8); 215 | _led->last_duty = leds[i]["last_duty"].as(); 216 | _led->power = leds[i]["power"].as(); 217 | _led->state = leds[i]["state"].as(); 218 | } 219 | } 220 | 221 | /* Store EEPROM settings (sync cache and eeprom) */ 222 | CONFIG.setSettings(); 223 | 224 | // Send Json Response 225 | AsyncJsonResponse *response = new AsyncJsonResponse(); 226 | JsonVariant& root = response->getRoot(); 227 | 228 | root["save"].set(true); 229 | 230 | response->setLength(); 231 | request->send(response); 232 | }); 233 | 234 | /* GET services config */ 235 | server.on("/config/services", HTTP_GET, [](AsyncWebServerRequest *request) { 236 | // Send Json Response 237 | AsyncJsonResponse *response = new AsyncJsonResponse(); 238 | JsonVariant& root = response->getRoot(); 239 | 240 | /* Services */ 241 | JsonObject services = root.createNestedObject("services"); 242 | 243 | /* Get service config */ 244 | services_t *service = CONFIG.getService(); 245 | 246 | services["hostname"] = service->hostname; 247 | services["ntp_server"] = service->ntp_server; 248 | services["utc_offset"] = service->utc_offset; 249 | services["ntp_dst"] = service->ntp_dst; 250 | services["mqtt_server"] = IPAddress(service->mqtt_server).toString(); 251 | services["mqtt_port"] = service->mqtt_port; 252 | services["mqtt_qos"] = service->mqtt_qos; 253 | services["mqtt_user"] = service->mqtt_user; 254 | services["mqtt_password"] = service->mqtt_password; 255 | services["enable_ntp"] = service->enable_ntp; 256 | services["enable_mqtt"] = service->enable_mqtt; 257 | 258 | response->setLength(); 259 | request->send(response); 260 | }); 261 | 262 | /* SET services config */ 263 | AsyncCallbackJsonWebHandler* services_handler = new AsyncCallbackJsonWebHandler("/config/services", [](AsyncWebServerRequest *request, JsonVariant &json) { 264 | JsonObject jsonObj = json.as(); 265 | 266 | /* Parse Services config */ 267 | JsonObject services = jsonObj["services"]; 268 | 269 | /* get point to current services config (eeprom cache) */ 270 | services_t *service = CONFIG.getService(); 271 | strlcpy(service->hostname, services["hostname"] | "", 20); 272 | strlcpy(service->ntp_server, services["ntp_server"] | "es.pool.ntp.org", 20); 273 | strlcpy(service->mqtt_user, services["mqtt_user"] | "", 16); 274 | strlcpy(service->mqtt_password, services["mqtt_password"] | "", 16); 275 | WEBUI.stringToIP(services["mqtt_server"], service->mqtt_server); 276 | service->mqtt_port = services["mqtt_port"].as(); 277 | service->mqtt_qos = services["mqtt_qos"].as(); 278 | service->utc_offset = services["utc_offset"].as(); 279 | service->ntp_dst = services["ntp_dst"].as(); 280 | service->enable_ntp = services["enable_ntp"].as(); 281 | service->enable_mqtt = services["enable_mqtt"].as(); 282 | 283 | /* Store EEPROM settings (sync cache and eeprom) */ 284 | CONFIG.setSettings(); 285 | 286 | /* Reconfigure network and services */ 287 | NETWORK.reloadSettings(); 288 | 289 | // Send Json Response 290 | AsyncJsonResponse *response = new AsyncJsonResponse(); 291 | JsonVariant& root = response->getRoot(); 292 | 293 | 294 | root["save"].set(true); 295 | 296 | #ifdef DEBUG_WEB_JSON 297 | serializeJson(root, Serial); 298 | Serial.println(); 299 | #endif 300 | 301 | response->setLength(); 302 | request->send(response); 303 | }); 304 | 305 | /* GET Networks config */ 306 | server.on("/config/networks", HTTP_GET, [](AsyncWebServerRequest *request) { 307 | // Send Json Response 308 | AsyncJsonResponse *response = new AsyncJsonResponse(); 309 | JsonVariant& root = response->getRoot(); 310 | 311 | root["capacity"] = MAX_NETWORKS; 312 | 313 | /* Networks */ 314 | JsonArray networks = root.createNestedArray("networks"); 315 | for (uint8_t i = 0; i < MAX_NETWORKS; i++) { 316 | /* Get single networks config */ 317 | network_t *network = CONFIG.getNetwork(i); 318 | 319 | if (network->active) { 320 | JsonObject networkConfig = networks.createNestedObject(); 321 | networkConfig["id"] = i; 322 | networkConfig["ssid"] = network->ssid; 323 | networkConfig["password"] = network->password; 324 | networkConfig["ip_address"] = IPAddress(network->ip_address).toString(); 325 | networkConfig["mask"] = IPAddress(network->mask).toString(); 326 | networkConfig["gateway"] = IPAddress(network->gateway).toString(); 327 | networkConfig["dns"] = IPAddress(network->dns).toString(); 328 | networkConfig["dhcp"] = network->dhcp; 329 | } 330 | } 331 | 332 | response->setLength(); 333 | request->send(response); 334 | }); 335 | 336 | /* SET Networks config */ 337 | AsyncCallbackJsonWebHandler* networks_handler = new AsyncCallbackJsonWebHandler("/config/networks", [](AsyncWebServerRequest *request, JsonVariant &json) { 338 | JsonObject jsonObj = json.as(); 339 | 340 | /* Reset network cache */ 341 | for (uint8_t i = 0; i < MAX_NETWORKS; i++) { 342 | network_t *network = CONFIG.getNetwork(i); 343 | network->active = false; 344 | } 345 | 346 | /* Parse New NETWORK config */ 347 | JsonArray networks = jsonObj["networks"]; 348 | for (uint8_t i = 0; i < networks.size(); i++) { 349 | if (i < MAX_NETWORKS) { 350 | /* get point to current network config (eeprom cache) */ 351 | network_t *network = CONFIG.getNetwork(i); 352 | 353 | strlcpy(network->ssid, networks[i]["ssid"] | " ", 32); 354 | strlcpy(network->password, networks[i]["password"] | " ", 32); 355 | 356 | /* split ip address to 4x uint8_t */ 357 | WEBUI.stringToIP(networks[i]["ip_address"], network->ip_address); 358 | WEBUI.stringToIP(networks[i]["mask"], network->mask); 359 | WEBUI.stringToIP(networks[i]["gateway"], network->gateway); 360 | WEBUI.stringToIP(networks[i]["dns"], network->dns); 361 | 362 | network->dhcp = networks[i]["dhcp"].as(); 363 | network->active = true; 364 | LOG_WEB("[WEBSOCKET] ssid: %s password: %s\n", network->ssid, network->password); 365 | 366 | } 367 | } 368 | 369 | /* Store EEPROM settings (sync cache and eeprom) */ 370 | CONFIG.setSettings(); 371 | 372 | /* Reload WiFi settings */ 373 | NETWORK.reloadSettings(); 374 | 375 | // Send Json Response 376 | AsyncJsonResponse *response = new AsyncJsonResponse(); 377 | JsonVariant& root = response->getRoot(); 378 | 379 | root["save"].set(true); 380 | 381 | response->setLength(); 382 | request->send(response); 383 | }); 384 | 385 | /* ----- Set: Duty at Home page ----- */ 386 | AsyncCallbackJsonWebHandler* duty_handler = new AsyncCallbackJsonWebHandler("/set/duty", [](AsyncWebServerRequest *request, JsonVariant &json) { 387 | JsonObject jsonObj = json.as(); 388 | JsonArray duty = jsonObj["duty"]; 389 | 390 | for (uint8_t i = 0; i < duty.size(); i++) { 391 | if (i < MAX_LED_CHANNELS) { 392 | uint8_t new_duty = duty[i]; 393 | 394 | /* Apply new Channel Duty */ 395 | SCHEDULE.setChannelDuty(i, new_duty); 396 | LOG_WEB("[WEBSERVER] Set Led Duty: new: %d, old: %d\n", new_duty, SCHEDULE.getChannelDuty(i)); 397 | } 398 | } 399 | 400 | /* Send Response */ 401 | AsyncJsonResponse *response = new AsyncJsonResponse(); 402 | JsonVariant& root = response->getRoot(); 403 | 404 | 405 | JsonArray leds = root.createNestedArray("duty"); 406 | for (int i = 0; i < MAX_LED_CHANNELS; ++i) { 407 | leds.add(SCHEDULE.getTargetChannelDuty(i)); 408 | } 409 | 410 | response->setLength(); 411 | request->send(response); 412 | }); 413 | 414 | /* ----- Set: Brightness at Home page ----- */ 415 | AsyncCallbackJsonWebHandler* brightness_handler = new AsyncCallbackJsonWebHandler("/set/brightness", [](AsyncWebServerRequest *request, JsonVariant &json) { 416 | JsonObject jsonObj = json.as(); 417 | JsonVariant brightness = jsonObj["brightness"]; 418 | 419 | /* Apply new Brightness */ 420 | SCHEDULE.setBrightness(brightness.as()); 421 | 422 | LOG_WEB("[WEBSERVER] Set Brightness: %d\n", brightness.as()); 423 | 424 | /* Send Response */ 425 | AsyncJsonResponse *response = new AsyncJsonResponse(); 426 | JsonVariant& root = response->getRoot(); 427 | root.set(SCHEDULE.getBrightness()); 428 | 429 | response->setLength(); 430 | request->send(response); 431 | 432 | }); 433 | 434 | /* Reboot */ 435 | server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest *request) 436 | { 437 | /* Reboot */ 438 | ESP.restart(); 439 | 440 | // Send Json Response 441 | AsyncJsonResponse *response = new AsyncJsonResponse(); 442 | JsonVariant& root = response->getRoot(); 443 | 444 | root["success"].set(true); 445 | 446 | response->setLength(); 447 | request->send(response); 448 | }); 449 | 450 | /* Factory reset */ 451 | server.on("/factory", HTTP_GET, [](AsyncWebServerRequest *request) 452 | { 453 | /* Erase EEPROM */ 454 | CONFIG.erase(); 455 | 456 | // Send Json Response 457 | AsyncJsonResponse *response = new AsyncJsonResponse(); 458 | JsonVariant& root = response->getRoot(); 459 | 460 | root["success"].set(true); 461 | 462 | response->setLength(); 463 | request->send(response); 464 | }); 465 | 466 | /* Captive portal */ 467 | server.onNotFound([](AsyncWebServerRequest *request) { 468 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", HTML, HTML_SIZE); 469 | response->addHeader("Content-Encoding", "gzip"); 470 | request->send(response); 471 | }); 472 | 473 | server.addHandler(schedule_handler); 474 | server.addHandler(leds_handler); 475 | server.addHandler(services_handler); 476 | server.addHandler(networks_handler); 477 | server.addHandler(duty_handler); 478 | server.addHandler(brightness_handler); 479 | 480 | /* CORS */ 481 | // server.onNotFound([](AsyncWebServerRequest *request) { 482 | // if (request->method() == HTTP_OPTIONS) { 483 | // request->send(200); 484 | // } else { 485 | // request->send(404); 486 | // } 487 | // }); 488 | 489 | DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); 490 | } 491 | 492 | /* Private */ 493 | 494 | /* Return string from uint8_t[4] array. */ 495 | String WEBUIClass::ipAddressToString(uint8_t *ip) { 496 | char buf[15]; 497 | sprintf(buf, "%03d.%03d.%03d.%03d", ip[0], ip[1], ip[2], ip[3]); 498 | return String(buf); 499 | } 500 | 501 | /* Split ip address to uint8_t[4] array. */ 502 | void WEBUIClass::stringToIP(const char *ip_string, uint8_t *octets) { 503 | char * octet; 504 | char ip_address[16]; 505 | 506 | memset(ip_address, 0, 16); 507 | strcpy(ip_address, ip_string); 508 | 509 | octet = strtok(ip_address, "."); 510 | for (int j = 0; j < 4; ++j) { 511 | 512 | octets[j] = (uint8_t) atoi(octet); 513 | octet = strtok(nullptr, "."); 514 | } 515 | } 516 | 517 | WEBUIClass WEBUI; 518 | 519 | #endif -------------------------------------------------------------------------------- /src/webui.h: -------------------------------------------------------------------------------- 1 | /*** 2 | s 3 | ***/ 4 | 5 | #ifndef HV_CC_LED_DRIVER_WEBUI_H 6 | #define HV_CC_LED_DRIVER_WEBUI_H 7 | 8 | #include "Arduino.h" 9 | #include "stdlib_noniso.h" 10 | #include "ESP8266WiFi.h" 11 | #include "ESPAsyncTCP.h" 12 | #include "ESPAsyncWebServer.h" 13 | #include "ArduinoJson.h" 14 | #include "FS.h" 15 | 16 | #ifndef DEBUG_UI_PORT 17 | #define DEBUG_UI_PORT Serial 18 | #endif 19 | 20 | #ifdef DEBUG_WEB 21 | #define LOG_WEB(...) DEBUG_UI_PORT.printf( __VA_ARGS__ ) 22 | #else 23 | #define LOG_WEB(...) 24 | #endif 25 | 26 | class WEBUIClass { 27 | 28 | public: 29 | void init(AsyncWebServer& server); 30 | 31 | private: 32 | static void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len); 33 | void settingsJson(char *result, size_t len); 34 | void networksJson(char *result, size_t len); 35 | void statusJson(char *result, size_t len); 36 | void scheduleJson(char *result, size_t len); 37 | void lightJson(char *result, size_t len); 38 | 39 | String ipAddressToString(uint8_t *ip); 40 | void stringToIP(const char *ip_string, uint8_t *octets); 41 | }; 42 | 43 | extern WEBUIClass WEBUI; 44 | 45 | #endif //HV_CC_LED_DRIVER_WEBUI_H 46 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | --------------------------------------------------------------------------------