├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── stale.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── build.py ├── docker-compose.yml ├── gulpfile.js ├── html ├── ailight.ttf ├── ailight.woff ├── app.js ├── favicon.png ├── index.html ├── logo.png └── style.scss ├── lib ├── AiLight │ ├── AiLight.cpp │ └── AiLight.hpp └── readme.txt ├── package.json ├── platformio.example.ini ├── src ├── _mqtt.ino ├── _ota.ino ├── _web.ino ├── _wifi.ino ├── config.example.h ├── html.gz.h ├── light.ino ├── main.h └── main.ino └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: https://www.buymeacoffee.com/sachatelgenhof 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: AiLight Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Set up Python 11 | uses: actions/setup-python@v1 12 | with: 13 | python-version: '3.x' 14 | - name: Install PlatformIO 15 | run: | 16 | python -m pip install --upgrade pip 17 | pip install -U platformio 18 | - name: Prepare Build 19 | run: | 20 | cp src/config.example.h src/config.h 21 | cp platformio.example.ini platformio.ini 22 | npm install 23 | - name: Build with PlatformIO 24 | run: platformio run 25 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | exempt-issue-label: 'release' 21 | exempt-pr-lable: 'release' 22 | days-before-stale: 60 23 | days-before-close: 10 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio* 2 | .clang_complete 3 | .gcc-flags.json 4 | config.h 5 | platformio.ini 6 | node_modules 7 | package-lock.json 8 | .yarnclean 9 | binaries 10 | .vscode -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "es5", 6 | "semi": false 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ![AiLight](https://raw.githubusercontent.com/wiki/stelgenhof/AiLight/images/ailight_logo.png) 2 | 3 | # Changelog 4 | 5 | All notable changes to this project will be documented in this file. 6 | 7 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org). 8 | 9 | ## [1.0.0] - 2021-08-22 10 | 11 | ### Added 12 | 13 | - Support for OpenHAB colorwheel [\#76](https://github.com/stelgenhof/AiLight/pull/76). ([aHcVolle](https://github.com/aHcVolle)) 14 | - GitHub Actions Workflow, to have the firmware automatically build upon a push of new code. 15 | - my92XX Support. This allows for support of light bulbs with the my9231 LED driver such as the LOHAS brand bulbs 16 | . [\#50](https://github.com/stelgenhof/AiLight/pull/50) ([Nick Wolff](https://github.com/darkfiberiru)) 17 | - Code of Conduct. 18 | - Prettier to nicely format the JavaScript files. 19 | 20 | ### Changed 21 | 22 | - All references to the Ai-Thinker brand name now that the AiLight Firmware also supports other types (based on the MY9231 LED driver). 23 | - Added the LED driver type to the Web UI and serial output information. 24 | - Locked ESP Libraries to v1.2.2 as the 1.2.3 version of the WebServer package fails to build. 25 | - Set data type of the count parameter to uint8_t as it is an integer. 26 | - The example PlatformIO configuration has been changed due to the release of PlatformIO v4.0. If you use PlatformIO make sure the configuration entry `env_default` is renamed to `default_envs` in your `platformio.ini` file! 27 | - Updated third-party dependencies and packages. 28 | - Various typo fixes, security updates and clean-up. 29 | 30 | ### Fixed 31 | 32 | - Connection to the MQTT broker was assumed to be always present and in case of a disconnect, a retry would be 33 | triggered. However in the case of the MQTT broker becoming unavailable, there was no possibility to get the 34 | connection back except for restarting the device. This has been resolved by checking at regular intervals if the connection is still there. [\#56](https://github.com/stelgenhof/AiLight/issues/56), [\#66](https://github.com/stelgenhof/AiLight/issues/66). 35 | - Building with VSCode + PlatformIO 4.0 extension gives error: "`.text' will not fit in region `iram1_0_seg'". [\#59](https://github.com/stelgenhof/AiLight/pull/59) ([Donnie](https://github.com/donkawechico)) 36 | - Corrected duplicate 'platform' JSON definition (to 'schema') for the MQTT discovery message. [\#55](https://github.com/stelgenhof/AiLight/pull/55) ([Ole-Kenneth](https://github.com/olekenneth)) 37 | - Added link type (MIME type) for the HTML style sheet to avoid potential misinterpretations. 38 | 39 | ### Removed 40 | 41 | - TravisCI configuration as now GitHub Actions is used. 42 | - 'manufacturer' information element from the Web UI now that the AiLight Firmware also supports other types (based on the MY9231 LED driver). 43 | - LiteServer for testing the UI as it was hardly used. 44 | 45 | ## [0.6.0] - 2019-04-08 46 | 47 | ### Added 48 | 49 | - Included additional HTTP response headers for the Web UI to increase security. 50 | - As it may be hard to identify which ESP Core version is used, this Core version information has been added to the About page on the Web UI and the debug output. 51 | - Enabled Travis CI so the AiLight firmware gets build and tested with each change. 52 | - Allow Gamma Correction option to be set via REST API and MQTT. [\#23](https://github.com/stelgenhof/AiLight/issues/23) 53 | - Possibility to define the state (ON/OFF) of the light upon power on / reset. Three options are available: Always On, Always Off and As Before. This can be extremely useful if for example you have your light connected to a regular switch and like it to behave like a general light switch. [\#24](https://github.com/stelgenhof/AiLight/issues/24). 54 | - MQTT availability topic for HomeAssistant is included and set to the same topic as the Last Will and Testament topic. [\#35](https://github.com/stelgenhof/AiLight/issues/35). 55 | 56 | ### Changed 57 | 58 | - Locked down versions of the platform and dependent libraries to safeguard stability. Altered the debug flag in the platformio.ini configuration file to show all warnings. 59 | - Home Assistant 0.84 (and up) changed the configuration syntax for the MQTT JSON platform type. A compiler directive is introduced to allow the firmware to be compiled for versions 0.84 or older of Home Assistant. [\#51](https://github.com/stelgenhof/AiLight/issues/51) 60 | - Added `monitor_speed` parameter for development environments in the `platformio.ini` configuration file to be able to override the default (9600). 61 | - In addition to the host name, if the MQTT state topic, command topic or state topic are changed, the device will re-register itself on Home Assistant via the MQTT Auto discovery (if enabled). These settings are part of the Home Assistant device configuration and therefor need to be re-initialized if they are changed. 62 | - Renamed the 'extra_script' parameter to 'extra_scripts' in the `platformio.ini` configuration file as 'extra_script' will be deprecated. 63 | - Increased buffer for MQTT discovery payload. Payload was too small causing Home Assistant not to recognize all configuration options. 64 | - Added DEBUG block for code parts that are only required in debug mode. 65 | - Suppressed message during build and included step to remove prior generated files for the WebUI (gulpfile.js). 66 | - Changed the name of the ESPAsyncWebServer library in the `platformio.ini` configuration file since original project has changed their naming. 67 | - Updated e-mail address and copyright year. 68 | 69 | ### Fixed 70 | 71 | - MQTT Reconnect timer was using the WiFi timeout directive rather than the applicable MQTT timeout directive. Created a new compiler directive to allow for setting the WiFi reconnect time to a custom value. 72 | - Corrected OTA port argument in the `platformio.ini` configuration file (Needs equal sign) 73 | - When settings in the Web UI were saved, the Lights tab was made active. This made verifying your changed settings quite cumbersome. After saving, the Settings tab now stays active. [\#14](https://github.com/stelgenhof/AiLight/issues/14). 74 | - Removed the Program Memory Macros for all constants used with AsyncWebServer methods. These were causing random Software WDT resets and removing these were the remedy. [\#27](https://github.com/stelgenhof/AiLight/issues/27). 75 | - Corrected debug flag in the `platformio.ini` configuration file. 76 | 77 | ### Removed 78 | 79 | ## [0.5.0] - 2017-08-25 80 | 81 | ### Added 82 | 83 | - REST API 84 | - Support for Home Assistant's MQTT Discovery. Have **AiLight** set up your light automatically! 85 | 86 | ### Changed 87 | 88 | - Moved MQTT Discovery notify event and reconnect timer to device callback function. MQTT functions become decoupled from device implementation. 89 | - Re-factored MQTT and WiFi connections using event driven methods that are executed asynchronously. 90 | - Changed signature use of MQTT callback handlers (based on AsyncMQTT's own examples.) 91 | - Updated Bulma CSS Framework to 0.4.4 (including other NPM packages). 92 | - Clear EEPROM space before loading factory defaults. 93 | - Replaced core function `memcpy` with the ESP8266 SDK counterpart. 94 | 95 | ### Fixed 96 | 97 | - #13 Ensured (new) configuration setting has a proper value before sending via WebSockets. 98 | - Decreased refresh time as UI is available again within 10 seconds. 99 | 100 | ### Removed 101 | 102 | ## [0.4.1] - 2017-06-29 103 | 104 | ### Added 105 | 106 | - Altered validation of WiFi Passwords in the Web UI to allow for WiFi networks without a password. 107 | - Added validation for WiFi SSID in the Web UI (required and cannot exceed 31 characters). 108 | - Added Gulp task for compiling release binaries. Compiled binaries are now available from this version going forward. 109 | 110 | ### Changed 111 | 112 | - The Web UI client side script has been upgraded to ES6 (ES2015). 113 | - Replaced core functions like 'strcpy', 'strcmp', etc. with the ESP8266 SDK counterpart. 114 | - Existing WebSocket client connections are now disconnected upon OTA start. 115 | 116 | ### Fixed 117 | 118 | - The OTA complete message in the Web UI was still showing multiple times due to a typo. 119 | 120 | ## [0.4.0] - 2017-06-17 121 | 122 | ### Added 123 | 124 | - In stead of immediately switching to a new state, you can now make the transition to a desired state (i.e. colours, brightness, etc.) perform more gradually. This will change the light (cross fade) from the current to the next state given the specified time. 125 | - Now the html.gz.h file is included for people that are not able to build this file themselves. 126 | - When an OTA update has been initiated, a message window - with a nice progress bar - is being displayed in the Web UI to indicate the user that an update is in progress. Subsequently, the Web UI is now reloaded automatically. 127 | - When the user chooses RESTART or RESET, a user friendly message window is being shown in the Web UI. 128 | - Gulpfile now includes task for generating the gamma correction table. 129 | 130 | ### Changed 131 | 132 | - Changed position of password visibility icon to be inside input box. 133 | - Adjusted path to gulp binary in the 'build.py' file to provide better support Windows OS. 134 | 135 | ### Fixed 136 | 137 | - Fixed issue [#10](https://github.com/stelgenhof/AiLight/issues/10): In the Web UI, the object holding the form's input values was not initialized, resulting in the user settings not being saved. 138 | - Ensured OTA 'complete' message isn't shown multiple times. 139 | - Fixed issue [\#8](https://github.com/stelgenhof/AiLight/issues/8): `[Violation] Added non-passive event listener to a scroll-blocking 'touchmove' event` in Web UI. 140 | - Included missing gulp-util package in the 'package.json' file. 141 | 142 | ### Removed 143 | 144 | - SVG font (used for icons) as most browsers started not supporting it anymore. Helps reducing the size of the firmware. 145 | - Removed unnecessary 'onConnect' handler. 146 | 147 | ## [0.3.0] - 2017-05-09 148 | 149 | ### Added 150 | 151 | - MQTT Last Will and Testament, giving the MQTT broker and other clients option to know if the Smart light has been disconnected gracefully or not. 152 | - Gamma Correction: makes the colours of the LED light to appear closer to what our eyes perceive. This allows for better colour representations. 153 | - favico added to HTML UI 154 | - Added model name to distinguish naming between AiLight and Ai-Thinker light bulb manufacturer/model name. 155 | 156 | ### Changed 157 | 158 | - Migrated to AsyncMQTTClient library (replacing PubSubClient Library) \* Please be aware of changes to the platformio.ini and config.h files! 159 | - `Build.py` script now uses locally installed Gulp binary instead of global one 160 | - HTML UI title includes now the device name so it's easier to identify which light you are looking at 161 | - Reduced size of HTML UI by removing unused style sheet elements, shrinking logo and removing unnecessary code. 162 | - The sliders are now accompanied with a value bubble to make it easier understanding what the actual value is. 163 | 164 | ### Fixed 165 | 166 | - Reset button now properly performs a factory reset. Previously it was executing a restart. 167 | - OTA upload was behaving erratically caused by incorrectly implementing the asynchronous 'onProgress' method (Wrong data-types used). 168 | 169 | ### Removed 170 | 171 | ## [0.2.0-alpha] - 2017-04-27 172 | 173 | After two weeks of hacking and modding, a first Alpha release is ready! The AiLight firmware allows you via WiFi to: 174 | 175 | - switch the light on or off 176 | - set the level of the 4 colour channels (Red, Green, Blue and White) 177 | - set the brightness level 178 | - set the light at a particular colour temperature 179 | - let the light flash (with a defined colour and brightness) 180 | - after powering on the light again, the last known settings are remembered (colour, brightness, etc.) 181 | This can all be done in HomeAssistant (using the MQTT integration) or the built in (mobile friendly) UI. 182 | 183 | ## [0.1.0] - 2017-04-21 184 | 185 | Initial Release of the AiLight: AiLight is a simple library to control an AiLight that contains the MY9291 LED driver. 186 | 187 | [1.0.0]: https://github.com/stelgenhof/AiLight/compare/v0.6.0...v1.0.0 188 | [0.6.0]: https://github.com/stelgenhof/AiLight/compare/v0.5.0...v0.6.0 189 | [0.5.0]: https://github.com/stelgenhof/AiLight/compare/v0.4.1...v0.5.0 190 | [0.4.1]: https://github.com/stelgenhof/AiLight/compare/v0.4.0...v0.4.1 191 | [0.4.0]: https://github.com/stelgenhof/AiLight/compare/v0.3.0...v0.4.0 192 | [0.3.0]: https://github.com/stelgenhof/AiLight/compare/v0.2.0-alpha...v0.3.0 193 | [0.2.0-alpha]: https://github.com/stelgenhof/AiLight/compare/v0.1.0...v0.2.0-alpha 194 | [0.1.0]: https://github.com/stelgenhof/AiLight/releases/tag/v0.1.0 195 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | me@sachatelgenhof.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2021 Sacha Telgenhof 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![AiLight Build](https://github.com/stelgenhof/AiLight/workflows/AiLight%20Build/badge.svg?branch=develop) [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | ![AiLight](https://raw.githubusercontent.com/wiki/stelgenhof/AiLight/images/ailight_logo.png) 4 | 5 | **AiLight** is a custom firmware for the inexpensive Ai-Thinker (or equivalent) RGBW WiFi light bulbs that has an ESP8266 on board and is controlled by the MY9291 or MY9231 LED driver. 6 | 7 | Current Stable Release: **v1.0.0** (Please read the [changelog](https://github.com/stelgenhof/AiLight/blob/master/CHANGELOG.md) for detailed information). 8 | 9 | ![Ai-Thinker RGBW Light bulb](https://github.com/stelgenhof/AiLight/wiki/images/aithinker_light.png) 10 | 11 | > **AiLight** is archived. The firmware has seen various improvements and fixes over the years and I feel it reached maturity. Go forth and fork **AiLight** if you like to improve it. 12 | 13 | ## Features 14 | 15 | With **AiLight** you can: 16 | 17 | - switch the light on or off 18 | - set the level of the 4 colour channels (Red, Green, Blue and White) 19 | - set the brightness level 20 | - set the light at a particular [colour temperature](https://github.com/stelgenhof/AiLight/wiki/Colour-Temperature) 21 | - let the light [flash](https://github.com/stelgenhof/AiLight/wiki/Flashing-the-Light) (i.e. blinking with a given colour and brightness) 22 | - enable [Gamma Correction](https://github.com/stelgenhof/AiLight/wiki/Gamma-Correction) to make the LED colours appear closer to what our eyes perceive 23 | - set the light to [transition](https://github.com/stelgenhof/AiLight/wiki/Transition) to the new state, rather than immediately. 24 | 25 | This can all be done with the built-in (mobile friendly) Web UI or in [Home Assistant](https://home-assistant.io 26 | ) (using the MQTT built-in integration via JSON). The Web UI also gives you the ability to configure your Smart Light remotely. You can easily change your WiFi settings or the configuration of your MQTT broker. 27 | 28 | ### Other 29 | - [REST API](https://github.com/stelgenhof/AiLight/wiki/REST-API) 30 | - MQTT Last Will and Testament enabled 31 | - Support for Home Assistant's [MQTT Discovery](https://github.com/stelgenhof/AiLight/wiki/Home-Assistant-MQTT-Discovery) 32 | - Support for [Over The Air](https://github.com/stelgenhof/AiLight/wiki/OTA-Updates) (OTA) firmware updates 33 | - Preserve light settings and configuration after power cycle or restart 34 | - Perform remote [restart](https://github.com/stelgenhof/AiLight/wiki/Restart-%26-Reset) using the built-in HTML UI. 35 | - [Reset](https://github.com/stelgenhof/AiLight/wiki/Restart-%26-Reset) to factory defaults using the built-in HTML UI (* 'factory' here means the default settings of the **AiLight** firmware upon compile time) 36 | 37 | 38 | Making this firmware was largely inspired by the [MY9291](https://github.com/xoseperez/my9291) LED driver and the [Espurna](https://github.com/xoseperez/espurna) firmware of [Xose Pérez](https://github.com/xoseperez). 39 | 40 | ## Getting started 41 | Got curious and want to use **AiLight** too? Head over to the [Wiki](https://github.com/stelgenhof/AiLight/wiki) where you can find all relevant topics on how to [connect](https://github.com/stelgenhof/AiLight/wiki/Connection), [flash](https://github.com/stelgenhof/AiLight/wiki/Flashing-the-Firmware) and use the **AiLight** firmware! 42 | 43 | 44 | ## Bugs and Feedback 45 | For bugs, questions and discussions, please use the [Github Issues](https://github.com/stelgenhof/AiLight/issues). 46 | 47 | ## Contributing 48 | 49 | Contributions are encouraged and welcome; I am always happy to get feedback or pull requests on Github :) Create [Github Issues](https://github.com/stelgenhof/AiLight/issues) for bugs and new features and comment on the ones you are interested in. 50 | 51 | If you enjoy what I am making, an extra cup of coffee is very much appreciated :). Your support helps me to put more time into Open-Source Software projects like this. 52 | 53 | Buy Me A Coffee 54 | 55 | ## Credits and License 56 | 57 | The **AiLight** Firmware is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). For the full copyright and license information, please see the file that was distributed with this source code. 58 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | # AiLight Firmware - PlatformIO Build script 4 | # 5 | # This file is part of the AiLight Firmware. 6 | # For the full copyright and license information, please view the LICENSE 7 | # file that was distributed with this source code. 8 | # 9 | # Created by Sacha Telgenhof 10 | # (https://www.sachatelgenhof.com) 11 | # Copyright (c) 2016 - 2021 Sacha Telgenhof 12 | 13 | Import("env") 14 | 15 | def before_build(source, target, env): 16 | env.Execute("$PROJECT_DIR/node_modules/.bin/gulp") 17 | 18 | env.AddPreAction("$BUILD_DIR/src/main.ino.cpp.o", before_build) 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | cppcheck: 5 | image: neszt/cppcheck-docker 6 | volumes: 7 | - .:/src 8 | working_dir: /src 9 | stdin_open: true 10 | tty: true -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware 3 | * 4 | * This file is part of the AiLight Firmware. 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | * 8 | * Created by Sacha Telgenhof 9 | * (https://www.sachatelgenhof.com) 10 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 11 | */ 12 | 13 | const { gulp, src, dest, series } = require('gulp') 14 | const fs = require('fs') 15 | const del = require('del') 16 | const plumber = require('gulp-plumber') 17 | const sass = require('gulp-sass')(require('sass')) 18 | const cssBase64 = require('gulp-css-base64') 19 | const favicon = require('gulp-base64-favicon') 20 | const inline = require('gulp-inline') 21 | const clean_css = require('gulp-clean-css') 22 | const html_min = require('gulp-htmlmin') 23 | const gzip = require('gulp-gzip') 24 | const uglify_js = require('uglify-es') 25 | const composer = require('gulp-uglify/composer') 26 | const minify = composer(uglify_js, console) 27 | 28 | // Configuration 29 | const sourceFolder = 'src/' 30 | const targetFolder = 'html/' 31 | 32 | // Convert the SCSS to CSS 33 | function scss(done) { 34 | return src(targetFolder + 'style.scss') 35 | .pipe(plumber()) 36 | .pipe(sass().on('error', sass.logError)) 37 | .pipe(dest('html')) 38 | } 39 | 40 | // Base 64 CSS 41 | function base64CSS(done) { 42 | return src(targetFolder + 'style.css') 43 | .pipe(cssBase64()) 44 | .pipe(dest('html')) 45 | } 46 | 47 | // Clean the generated output files 48 | function clean(done) { 49 | del([sourceFolder + 'html.*']) 50 | del([sourceFolder + '*.html']) 51 | del([targetFolder + '*.css']) 52 | 53 | done() 54 | } 55 | 56 | // Process HTML files 57 | function html(done) { 58 | return src(targetFolder + '*.html') 59 | .pipe(favicon()) 60 | .pipe( 61 | inline({ 62 | js: function () { 63 | return minify({ 64 | mangle: true, 65 | }) 66 | }, 67 | css: [clean_css], 68 | disabledTypes: ['svg'], 69 | }) 70 | ) 71 | .pipe( 72 | html_min({ 73 | collapseWhitespace: true, 74 | removeComments: true, 75 | removeEmptyAttributes: true, 76 | includeAutoGeneratedTags: false, 77 | minifyCSS: true, 78 | minifyJS: true, 79 | }) 80 | ) 81 | .pipe(gzip()) 82 | .pipe(dest(sourceFolder)) 83 | } 84 | 85 | // Build the C++ include header file 86 | function header(done) { 87 | const source = sourceFolder + 'index.html.gz' 88 | const destination = sourceFolder + 'html.gz.h' 89 | 90 | const ws = fs.createWriteStream(destination) 91 | 92 | ws.on('error', function (err) { 93 | console.log(err) 94 | }) 95 | 96 | const data = fs.readFileSync(source) 97 | 98 | ws.write('#define html_gz_len ' + data.length + '\n') 99 | ws.write('const uint8_t html_gz[] PROGMEM = {') 100 | 101 | for (let i = 0; i < data.length; i++) { 102 | if (i % 1000 === 0) ws.write('\n') 103 | ws.write('0x' + ('00' + data[i].toString(16)).slice(-2)) 104 | if (i < data.length - 1) ws.write(',') 105 | } 106 | 107 | ws.write('\n};') 108 | ws.end() 109 | 110 | // Remove intermediate files 111 | fs.unlinkSync(source) 112 | fs.unlinkSync(targetFolder + 'style.css') 113 | 114 | done() 115 | } 116 | 117 | // Creates a gamma correction table 118 | // Copy the contents of this file in the lib/AiLight/AiLight.hpp file 119 | function gamma(done) { 120 | const gamma = 2.8 // Correction factor 121 | const MAX_IN = 255 // End of INPUT range 122 | const MAX_OUT = 255 // End of OUTPUT range 123 | const destination = 'gamma.h' 124 | 125 | const ws = fs.createWriteStream(destination) 126 | 127 | ws.on('error', function (err) { 128 | console.log(err) 129 | }) 130 | 131 | ws.write('// This table remaps linear input values to nonlinear gamma-corrected output\n') 132 | ws.write('// values. The output values are specified for 8-bit colours with a gamma\n') 133 | ws.write('// correction factor of 2.8\n') 134 | ws.write('const static uint8_t PROGMEM gamma8[256] = {') 135 | for (let i = 0; i <= MAX_IN; i++) { 136 | if (i > 0) { 137 | ws.write(',') 138 | } 139 | 140 | if ((i & 15) === 0) { 141 | ws.write('\n') 142 | } 143 | 144 | const level = Math.floor(Math.pow(i / MAX_IN, gamma) * MAX_OUT + 0.5) 145 | ws.write((' ' + level).slice(-4)) 146 | } 147 | 148 | ws.write(' };\n') 149 | ws.end() 150 | 151 | done() 152 | } 153 | 154 | exports.default = series(clean, scss, base64CSS, html, header) 155 | exports.gamma = gamma 156 | -------------------------------------------------------------------------------- /html/ailight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelgenhof/AiLight/250f59ecccda18d9d39200aafb96ba30dafb9103/html/ailight.ttf -------------------------------------------------------------------------------- /html/ailight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelgenhof/AiLight/250f59ecccda18d9d39200aafb96ba30dafb9103/html/ailight.woff -------------------------------------------------------------------------------- /html/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware 3 | * 4 | * This file is part of the AiLight Firmware. 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | 8 | * Created by Sacha Telgenhof 9 | * (https://www.sachatelgenhof.com) 10 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 11 | */ 12 | 13 | /* jshint curly: true, undef: true, unused: true, eqeqeq: true, esversion: 6, varstmt: true, browser: true, devel: true */ 14 | 15 | // Key names as used internally and in Home Assistant 16 | const K_S = 'state' 17 | const K_BR = 'brightness' 18 | const K_CT = 'color_temp' 19 | const K_C = 'color' 20 | const K_R = 'r' 21 | const K_G = 'g' 22 | const K_B = 'b' 23 | const K_W = 'white_value' 24 | const K_GM = 'gamma' 25 | const K_HD = 'ha_discovery' 26 | const K_RA = 'rest_api' 27 | 28 | const S_ON = 'ON' 29 | const S_OFF = 'OFF' 30 | 31 | const WAIT = 10000 32 | 33 | /** 34 | * Object representing a Switch component 35 | * 36 | * @param id the DOM element to be rendered as a Switch component 37 | * @param du should the state of the Switch be broadcasted (WebSockets) or not? 38 | * 39 | * @return void 40 | */ 41 | function Switch(id, du = true) { 42 | this.id = id 43 | this.du = du 44 | this.init() 45 | } 46 | 47 | ;(function () { 48 | this.getState = function () { 49 | return this.state 50 | } 51 | 52 | this.setState = function (state) { 53 | const CLASS_CHECKED = 'checked' 54 | this.state = state 55 | this.el.checked = this.state 56 | 57 | if (this.el.checked) { 58 | this.el.parentNode.classList.add(CLASS_CHECKED) 59 | } else { 60 | this.el.parentNode.classList.remove(CLASS_CHECKED) 61 | } 62 | } 63 | 64 | this.toggleState = function () { 65 | this.state = !this.state 66 | this.setState(this.state) 67 | 68 | let state = {} 69 | let value = this.state 70 | 71 | if (this.id === 'state') { 72 | value = this.state ? S_ON : S_OFF 73 | } 74 | 75 | // Handle visibility of HA Discovery settings 76 | if (this.id === K_HD) { 77 | let ad = document.getElementById('mqtt_ha_discovery') 78 | ad.style.display = this.state ? 'flex' : 'none' 79 | } 80 | 81 | // Handle visibility of REST API settings 82 | if (this.id === K_RA) { 83 | let ap = document.getElementById('rest_api_key') 84 | ap.style.display = this.state ? 'flex' : 'none' 85 | } 86 | 87 | state[this.id] = value 88 | 89 | if (this.du) { 90 | websock.send(JSON.stringify(state)) 91 | } 92 | } 93 | 94 | this.init = function () { 95 | this.el = document.getElementById('switch_' + this.id) 96 | this.state = this.el.checked 97 | this.el.addEventListener('click', this.toggleState.bind(this), { 98 | passive: true, 99 | }) 100 | } 101 | }.call(Switch.prototype)) 102 | 103 | /** 104 | * Object representing a Slider component 105 | * 106 | * @param id the DOM element to be rendered as a Slider component 107 | * 108 | * @return void 109 | */ 110 | function Slider(id) { 111 | this.id = id 112 | this._init() 113 | } 114 | 115 | ;(function () { 116 | this.getValue = function () { 117 | return this.el.value 118 | } 119 | 120 | this.setValue = function (val) { 121 | this.el.value = val 122 | this._sethigh() 123 | } 124 | 125 | this._sethigh = function () { 126 | this._high = ((this.el.value - this.el.min) / (this.el.max - this.el.min)) * 100 + '%' 127 | this.el.style.setProperty('--high', this._high) 128 | 129 | let output = this.el.parentNode.getElementsByTagName('output')[0] 130 | if (typeof output !== 'undefined') { 131 | output.innerHTML = this.el.value 132 | } 133 | } 134 | 135 | this._send = function () { 136 | let msg = { 137 | state: S_ON, 138 | } 139 | msg[this.id] = this.el.value 140 | 141 | websock.send(JSON.stringify(msg)) 142 | } 143 | 144 | this._init = function () { 145 | this.el = document.getElementById('slider_' + this.id) 146 | this.el.style.setProperty('--low', '0%') 147 | this._sethigh() 148 | 149 | this.el.addEventListener('mousemove', this._sethigh.bind(this), { 150 | passive: true, 151 | }) 152 | this.el.addEventListener('touchmove', this._sethigh.bind(this), { 153 | passive: true, 154 | }) 155 | this.el.addEventListener('drag', this._sethigh.bind(this), { 156 | passive: true, 157 | }) 158 | this.el.addEventListener('click', this._sethigh.bind(this), { 159 | passive: true, 160 | }) 161 | 162 | let rgb = [K_R, K_G, K_B] 163 | if (rgb.includes(this.id)) { 164 | this.el.addEventListener('change', sendRGB.bind(this), { 165 | passive: true, 166 | }) 167 | } else { 168 | this.el.addEventListener('change', this._send.bind(this), { 169 | passive: true, 170 | }) 171 | } 172 | } 173 | }.call(Slider.prototype)) 174 | 175 | // Globals 176 | let websock 177 | let stSwitch = new Switch(K_S) 178 | let brSlider = new Slider(K_BR) 179 | let ctSlider = new Slider(K_CT) 180 | let rSlider = new Slider(K_R) 181 | let gSlider = new Slider(K_G) 182 | let bSlider = new Slider(K_B) 183 | let wSlider = new Slider(K_W) 184 | let gmSwitch = new Switch(K_GM) 185 | let hdSwitch = new Switch(K_HD, false) 186 | let raSwitch = new Switch(K_RA, false) 187 | let hS = false 188 | 189 | /** 190 | * Sends the RGB triplet state to the connected WebSocket clients 191 | * 192 | * @return void 193 | */ 194 | function sendRGB() { 195 | let msg = { 196 | state: S_ON, 197 | } 198 | 199 | msg[K_C] = {} 200 | msg[K_C][rSlider.id] = rSlider.getValue() 201 | msg[K_C][gSlider.id] = gSlider.getValue() 202 | msg[K_C][bSlider.id] = bSlider.getValue() 203 | 204 | websock.send(JSON.stringify(msg)) 205 | } 206 | 207 | /** 208 | * Parses a text string to a JSON representation 209 | * 210 | * @param str text string to be parsed 211 | * 212 | * @return mixed JSON structure when successful; false when not 213 | */ 214 | function getJSON(str) { 215 | try { 216 | return JSON.parse(str) 217 | } catch (e) { 218 | return false 219 | } 220 | } 221 | 222 | /** 223 | * Process incoming data from the WebSocket connection 224 | * 225 | * @param data received data structure from the WebSocket connection 226 | * 227 | * @return void 228 | */ 229 | function processData(data) { 230 | for (let key in data) { 231 | if (!data.hasOwnProperty(key)) { 232 | console.log('[WEBSOCKET] Unable to process message') 233 | return 234 | } 235 | 236 | // Process Device information 237 | if (key === 'd') { 238 | document.title = data.d.app_name 239 | 240 | // Bind data to DOM 241 | for (let dev in data[key]) { 242 | // Bind to span elements 243 | let d = document.querySelectorAll("span[data-s='" + dev + "']") 244 | ;[].forEach.call(d, function (item) { 245 | if (data[key].hasOwnProperty(dev)) { 246 | item.innerHTML = data[key][dev] 247 | } 248 | }) 249 | } 250 | } 251 | 252 | // Process settings 253 | if (key === 's') { 254 | document.title += ' - ' + data[key].hostname 255 | 256 | // Bind data to DOM 257 | for (let s in data[key]) { 258 | // Bind to span elements 259 | let a = document.getElementById('pagescontent').querySelectorAll("span[data-s='" + s + "']") 260 | ;[].forEach.call(a, function (item) { 261 | if (data[key].hasOwnProperty(s)) { 262 | item.innerHTML = data[key][s] 263 | } 264 | }) 265 | 266 | // Bind to specific DOM elements 267 | if (data[key].hasOwnProperty(s) && document.getElementById(s) !== null) { 268 | document.getElementById(s).value = data[key][s] 269 | } 270 | 271 | // Set HA Discovery switch and prefix field 272 | if (s === 'switch_ha_discovery') { 273 | if (data[key].hasOwnProperty(s)) { 274 | hdSwitch.setState(data[key][s]) 275 | 276 | let ad = document.getElementById('mqtt_ha_discovery') 277 | if (!data[key][s]) { 278 | ad.style.display = 'none' 279 | } 280 | } 281 | } 282 | 283 | // Set REST API switch and API Key field 284 | if (s === 'switch_rest_api') { 285 | if (data[key].hasOwnProperty(s)) { 286 | raSwitch.setState(data[key][s]) 287 | 288 | let ap = document.getElementById('rest_api_key') 289 | if (!data[key][s]) { 290 | ap.style.display = 'none' 291 | } 292 | } 293 | } 294 | } 295 | } 296 | 297 | // Set state 298 | if (key === K_S) { 299 | stSwitch.setState(data[key] !== S_OFF) 300 | } 301 | 302 | if (key === K_BR) { 303 | brSlider.setValue(data[key]) 304 | } 305 | 306 | if (key === K_CT) { 307 | ctSlider.setValue(data[key]) 308 | } 309 | 310 | if (key === 'color') { 311 | rSlider.setValue(data[key][K_R]) 312 | gSlider.setValue(data[key][K_G]) 313 | bSlider.setValue(data[key][K_B]) 314 | } 315 | 316 | if (key === K_W) { 317 | wSlider.setValue(data[key]) 318 | } 319 | 320 | if (key === K_GM) { 321 | gmSwitch.setState(data[key]) 322 | } 323 | } 324 | } 325 | 326 | /** 327 | * WebSocket client initialization and event processing 328 | * 329 | * @return void 330 | */ 331 | function wsConnect() { 332 | let host = window.location.hostname 333 | let port = location.port 334 | 335 | if (websock) { 336 | websock.close() 337 | } 338 | 339 | websock = new WebSocket('ws://' + host + ':' + port + '/ws') 340 | 341 | websock.onopen = function (e) { 342 | console.log('[WEBSOCKET] Connected to ' + e.target.url) 343 | } 344 | 345 | websock.onclose = function (e) { 346 | console.log('[WEBSOCKET] Connection closed') 347 | console.log(e) 348 | console.log(e.reason) 349 | } 350 | 351 | websock.onerror = function (e) { 352 | console.log('[WEBSOCKET] Error: ' + e) 353 | } 354 | 355 | websock.onmessage = function (e) { 356 | let data = getJSON(e.data) 357 | if (data) { 358 | processData(data) 359 | } 360 | } 361 | } 362 | 363 | /** 364 | * EventSource client initialization and event processing 365 | * 366 | * @return void 367 | */ 368 | function esConnect() { 369 | if (!!window.EventSource) { 370 | let source = new EventSource('/events') 371 | 372 | source.addEventListener( 373 | 'open', 374 | function (e) { 375 | console.log('[EVENTSOURCE] Connected to ' + e.target.url) 376 | }, 377 | false 378 | ) 379 | 380 | source.addEventListener( 381 | 'error', 382 | function (e) { 383 | if (e.target.readyState !== EventSource.OPEN) { 384 | console.log('[EVENTSOURCE] Connection closed') 385 | } 386 | }, 387 | false 388 | ) 389 | 390 | source.addEventListener( 391 | 'message', 392 | function (e) { 393 | console.log('message', e.data) 394 | }, 395 | false 396 | ) 397 | 398 | // Handling OTA events 399 | source.addEventListener( 400 | 'ota', 401 | function (e) { 402 | if (e.data.startsWith('p-')) { 403 | let pb = document.getElementById('op') 404 | let p = parseInt(e.data.split('-')[1]) 405 | 406 | pb.value = p 407 | 408 | if (p === 100 && !hS) { 409 | hS = true 410 | let f = document.createElement('p') 411 | f.innerHTML = 412 | 'Completed successfully! Please wait for your AiLight Smart Light to be restarted.' 413 | pb.parentNode.appendChild(f) 414 | reload(false) 415 | } 416 | } 417 | 418 | // Show OTA Modal 419 | if (e.data === 'start') { 420 | document.getElementById('om').classList.add('is-active') 421 | } 422 | }, 423 | false 424 | ) 425 | } 426 | } 427 | 428 | /** 429 | * Reloads the page after waiting certain time. 430 | * 431 | * The time before the page is being reloaded is defined by the 'WAIT' constant. 432 | * 433 | * @param show Indicates whether a modal windows needs to be displayed or not. 434 | * 435 | * @return void 436 | */ 437 | function reload(show) { 438 | if (show) { 439 | document.getElementById('rm').classList.add('is-active') 440 | } 441 | 442 | setTimeout(function () { 443 | location.reload() 444 | }, WAIT) 445 | } 446 | 447 | /** 448 | * Handler for the Restart button 449 | * 450 | * @return boolean true when user approves, false otherwise 451 | */ 452 | function restart() { 453 | let response = window.confirm('Are you sure you want to restart your AiLight Smart Light?') 454 | if (response === false) { 455 | return false 456 | } 457 | 458 | websock.send( 459 | JSON.stringify({ 460 | command: 'restart', 461 | }) 462 | ) 463 | 464 | // Wait for the device to have restarted before reloading the page 465 | reload(true) 466 | } 467 | 468 | /** 469 | * Handler for the Reset button 470 | * 471 | * @return boolean true when user approves, false otherwise 472 | */ 473 | function reset() { 474 | let response = window.confirm( 475 | 'You are about to reset your AiLight Smart Light to the factory defaults!\n Are you' + 476 | ' sure you' + 477 | ' want to reset?' 478 | ) 479 | if (response === false) { 480 | return false 481 | } 482 | 483 | websock.send( 484 | JSON.stringify({ 485 | command: 'reset', 486 | }) 487 | ) 488 | 489 | // Wait for the device to have restarted before reloading the page 490 | reload(true) 491 | } 492 | 493 | /** 494 | * Handler for making the password (in)visible 495 | * 496 | * @return void 497 | */ 498 | function togglePassword() { 499 | let ie = document.getElementById(this.dataset.input) 500 | ie.type = ie.type === 'text' ? 'password' : 'text' 501 | } 502 | 503 | /** 504 | * Adds validation message to the selected element 505 | * 506 | * @param el the DOM element to which the validation message needs to be added 507 | * @param message the validation message to be displayed 508 | * 509 | * @return void 510 | */ 511 | function addValidationMessage(el, message) { 512 | const CLASS_WARNING = 'is-danger' 513 | let v = document.createElement('p') 514 | v.innerHTML = message 515 | v.classList.add('help', CLASS_WARNING) 516 | el.parentNode.appendChild(v) 517 | el.classList.add(CLASS_WARNING) 518 | } 519 | 520 | /** 521 | * Handler for validating and saving user defined settings 522 | * 523 | * @return void 524 | */ 525 | function save() { 526 | let s = {} 527 | let msg = {} 528 | let isValid = true 529 | 530 | let Valid952HostnameRegex = 531 | /^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/i 532 | 533 | // Parse all input fields and validate where needed 534 | let inputs = document.forms[0].querySelectorAll('input') 535 | for (let i = 0; i < inputs.length; i++) { 536 | let id = inputs[i].id.split('.').pop() 537 | 538 | // Clear any validation messages 539 | inputs[i].classList.remove('is-danger') 540 | let p = inputs[i].parentNode 541 | let v = p.querySelectorAll('p.is-danger') 542 | ;[].forEach.call(v, function (item) { 543 | p.removeChild(item) 544 | }) 545 | 546 | // Validate hostname input 547 | if (id === 'hostname' && !Valid952HostnameRegex.test(inputs[i].value)) { 548 | addValidationMessage(inputs[i], 'This hostname is invalid.') 549 | isValid = false 550 | continue 551 | } 552 | 553 | // Validate WiFi ssid 554 | if (id === 'wifi_ssid') { 555 | if (!inputs[i].value || inputs[i].value.length === 0 || inputs[i].value > 31) { 556 | addValidationMessage(inputs[i], 'A WiFi SSID must be present with a maximum of 31 characters.') 557 | isValid = false 558 | continue 559 | } 560 | } 561 | 562 | // Validate WiFi PSK 563 | if (id === 'wifi_psk') { 564 | if ( 565 | inputs[i].value && 566 | inputs[i].value.length > 0 && 567 | (inputs[i].value.length > 63 || inputs[i].value.length < 8) 568 | ) { 569 | addValidationMessage(inputs[i], 'A WiFi Passphrase Key (Password) must be between 8 and 63 characters.') 570 | isValid = false 571 | continue 572 | } 573 | } 574 | 575 | // Validate API Key 576 | if (id === 'api_key') { 577 | if ( 578 | inputs[i].value && 579 | inputs[i].value.length > 0 && 580 | (inputs[i].value.length > 32 || inputs[i].value.length < 8) 581 | ) { 582 | addValidationMessage(inputs[i], 'An API Key must be between 8 and 32 characters.') 583 | isValid = false 584 | continue 585 | } 586 | } 587 | 588 | s[id] = inputs[i].type === 'checkbox' ? inputs[i].checked : inputs[i].value 589 | } 590 | 591 | // Parse all select fields 592 | let selects = document.forms[0].querySelectorAll('select') 593 | for (let j = 0; j < selects.length; j++) { 594 | let id = selects[j].id.split('.').pop() 595 | s[id] = selects[j].value 596 | } 597 | 598 | if (isValid) { 599 | msg.s = s 600 | websock.send(JSON.stringify(msg)) 601 | } 602 | } 603 | 604 | /** 605 | * Initializes tab functionality 606 | * 607 | * @return void 608 | */ 609 | function initTabs() { 610 | let container = document.getElementById('menu') 611 | const TABS_SELECTOR = 'div div a' 612 | 613 | // Enable click event to the tabs 614 | let tabs = container.querySelectorAll(TABS_SELECTOR) 615 | for (let i = 0; i < tabs.length; i++) { 616 | tabs[i].onclick = displayPage 617 | } 618 | 619 | // Set current tab 620 | let currentTab = container.querySelector(TABS_SELECTOR) 621 | 622 | // Store which tab is current one 623 | let id = currentTab.id.split('_')[1] 624 | currentTab.parentNode.setAttribute('data-current', id) 625 | currentTab.classList.add('is-active') 626 | 627 | // Hide tab contents we don't need 628 | let pages = document.getElementById('pagescontent').querySelectorAll('section') 629 | for (let j = 1; j < pages.length; j++) { 630 | pages[j].style.display = 'none' 631 | } 632 | } 633 | 634 | /** 635 | * Tab click / page display handler 636 | * 637 | * @return void 638 | */ 639 | function displayPage() { 640 | const CLASS_ACTIVE = 'is-active' 641 | const CURRENT_ATTRIBUTE = 'data-current' 642 | const ID_TAB_PAGE = 'page_' 643 | let current = this.parentNode.getAttribute(CURRENT_ATTRIBUTE) 644 | 645 | // Remove class of active tab and hide contents 646 | document.getElementById('tab_' + current).classList.remove(CLASS_ACTIVE) 647 | document.getElementById(ID_TAB_PAGE + current).style.display = 'none' 648 | 649 | // Add class to new active tab and show contents 650 | let id = this.id.split('_')[1] 651 | 652 | this.classList.add(CLASS_ACTIVE) 653 | document.getElementById(ID_TAB_PAGE + id).style.display = 'block' 654 | this.parentNode.setAttribute(CURRENT_ATTRIBUTE, id) 655 | } 656 | 657 | /** 658 | * Handler for displaying/hiding the hamburger menu (visible on mobile devices) 659 | * 660 | * @return void 661 | */ 662 | function toggleNav() { 663 | const CLASS_ACTIVE = 'is-active' 664 | let nav = document.getElementById('nav-menu') 665 | 666 | if (!nav.classList.contains(CLASS_ACTIVE)) { 667 | nav.classList.add(CLASS_ACTIVE) 668 | } else { 669 | nav.classList.remove(CLASS_ACTIVE) 670 | } 671 | } 672 | 673 | /** 674 | * Handler for generating an API Key 675 | * 676 | * The generated key is a standard UUID value excluding the hyphens. 677 | * Source: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript 678 | * 679 | * @return void 680 | */ 681 | function generateAPIKey() { 682 | let akv = document.getElementById('api_key') 683 | 684 | akv.value = ([1e7] + 1e3 + 4e3 + 8e3 + 1e11).replace(/[018]/g, (c) => 685 | (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) 686 | ) 687 | } 688 | 689 | /** 690 | * Main 691 | * 692 | * @return void 693 | */ 694 | document.addEventListener('DOMContentLoaded', function () { 695 | document.getElementById('button-restart').addEventListener('click', restart, { 696 | passive: true, 697 | }) 698 | document.getElementById('reset').addEventListener('click', reset, { 699 | passive: true, 700 | }) 701 | document.getElementById('nav-toggle').addEventListener('click', toggleNav, { 702 | passive: true, 703 | }) 704 | document.getElementById('save').addEventListener('click', save, { 705 | passive: true, 706 | }) 707 | 708 | let pw = document.getElementById('pagescontent').querySelectorAll('i.icon-eye') 709 | ;[].forEach.call(pw, function (item) { 710 | item.addEventListener('touchstart', togglePassword, { 711 | passive: true, 712 | }) 713 | item.addEventListener('click', togglePassword, { 714 | passive: true, 715 | }) 716 | }) 717 | 718 | let ak = document.getElementById('pagescontent').querySelector('i.icon-arrow-sync') 719 | ak.addEventListener('touchstart', generateAPIKey, { 720 | passive: true, 721 | }) 722 | ak.addEventListener('click', generateAPIKey, { 723 | passive: true, 724 | }) 725 | 726 | initTabs() 727 | wsConnect() 728 | esConnect() 729 | }) 730 | -------------------------------------------------------------------------------- /html/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelgenhof/AiLight/250f59ecccda18d9d39200aafb96ba30dafb9103/html/favicon.png -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | AiLight 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 | 63 |
64 |
65 |
66 | 67 | 68 | 77 | 78 |
79 | 80 | 81 |
82 |

Control your 83 | AiLight Smart Light using the controls below

84 | 85 | 86 | 87 | 88 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 118 | 119 | 120 | 121 | 125 | 126 | 127 | 128 | 132 | 133 | 134 | 135 | 139 | 140 | 141 | 142 | 147 | 148 | 149 |
Switch 89 | 92 |
Brightness 97 | 98 | 99 |
Color Temperature 104 | 105 | 106 |
Red 115 | 116 | 117 |
Green 122 | 123 | 124 |
Blue 129 | 130 | 131 |
White 136 | 137 | 138 |
Gamma Correction 143 | 146 |
150 |
151 | 152 | 153 |
154 | 155 |
156 |
157 |
158 | 159 |

General

160 | 161 |
162 |
163 | 164 |
165 |
166 |
167 |
168 |
169 | 174 |
175 |
176 |
177 |
178 |
179 | 180 |
181 |

Network

182 | 183 |
184 |
185 | 186 |
187 |
188 |
189 |
190 | 192 |
193 |
194 |
195 |
196 | 197 |
198 |
199 | 200 |
201 |
202 |
203 |
204 | 206 |
207 |
208 |
209 |
210 | 211 |
212 |
213 | 214 |
215 |
216 |
217 |
218 | 221 | 222 | 223 | 224 |
225 |
226 |
227 |
228 | 229 |
230 |

MQTT

231 | 232 |
233 |
234 | 235 |
236 |
237 |
238 |
239 | 242 |
243 |
244 |
245 |
246 | 247 |
248 |
249 | 250 |
251 |
252 |
253 |
254 | 257 |
258 |
259 |
260 |
261 | 262 |
263 |
264 | 265 |
266 |
267 |
268 |
269 | 272 |
273 |
274 |
275 |
276 | 277 |
278 |
279 | 280 |
281 |
282 |
283 |
284 | 287 | 288 | 289 | 290 |
291 |
292 |
293 |
294 | 295 |
296 |
297 | 298 |
299 |
300 |
301 |
302 | 305 |
306 |
307 |
308 |
309 | 310 |
311 |
312 | 313 |
314 |
315 |
316 |
317 | 320 |
321 |
322 |
323 |
324 | 325 |
326 |
327 | 328 |
329 |
330 |
331 |
332 | 335 |
336 |
337 |
338 |
339 | 340 |
341 |
342 | 343 |
344 |
345 |
346 |
347 | 350 |
351 |
352 |
353 |
354 | 355 |
356 |
357 | 358 |
359 |
360 |
361 |
362 | 365 |
366 |
367 |
368 |
369 | 370 |
371 |

Developer

372 | 373 |
374 |
375 | 376 |
377 |
378 |
379 |
380 | 383 |
384 |
385 |
386 |
387 | 388 |
389 |
390 | 391 |
392 |
393 |
394 |
395 | 398 | 399 | 400 | 401 |
402 |
403 |
404 |
405 | 406 |
407 |
408 |
409 |
410 |
411 |
412 | 413 |
414 |
415 |
416 |
417 | 418 |
419 |
420 |
421 |
422 | 423 | 424 |
425 | 426 |
427 |

428 | 429 | 430 | 431 |

432 |

Version 433 | 434 |

435 |
436 | 437 | 438 | 439 | 440 | 441 | 444 | 445 | 446 | 447 | 450 | 451 | 452 | 453 | 457 | 458 | 459 | 460 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 473 | 474 | 475 | 476 | 479 | 480 | 481 | 482 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 495 | 496 | 497 | 498 | 501 | 502 | 503 | 504 | 507 | 508 | 509 |
Name 442 | 443 |
LED Driver 448 | 449 |
Firmware Build 454 |   455 | 456 |
ESP Core Version 461 | 462 |
WiFi IP Address 471 | 472 |
Hostname 477 | .local 478 |
MAC Address 483 | 484 |
Processor 493 | MHz 494 |
Memory 499 | bytes 500 |
Free Memory 505 | bytes 506 |
510 |
511 | 512 |
513 | 514 | 515 | 525 | 526 | 527 | 537 | 538 | 539 |
540 |
541 |
542 |

543 | AiLight is a project by 544 | Sacha Telgenhof © 2017 - 2021 545 |

546 |

547 | Source code is available on Github and licensed 548 | MIT. 549 |

550 |
551 |
552 |
553 | 554 | 555 | 556 | 557 | 558 | -------------------------------------------------------------------------------- /html/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelgenhof/AiLight/250f59ecccda18d9d39200aafb96ba30dafb9103/html/logo.png -------------------------------------------------------------------------------- /html/style.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /** 3 | * AiLight Firmware - Styling 4 | * 5 | * This file is part of the AiLight Firmware. 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | * 9 | * Slider styling is based on the Slider Component for Vue Bulma 10 | * (https://github.com/vue-bulma/slider) 11 | * 12 | * Switch styling is based on the Switch Component for Vue Bulma 13 | * (https://github.com/vue-bulma/switch) 14 | * 15 | * Created by Sacha Telgenhof 16 | * (https://www.sachatelgenhof.com) 17 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 18 | */ 19 | 20 | @import './node_modules/bulma/sass/utilities/_all'; 21 | @import './node_modules/bulma/sass/base/_all'; 22 | @import './node_modules/bulma/sass/elements/box'; 23 | @import './node_modules/bulma/sass/elements/button'; 24 | @import './node_modules/bulma/sass/elements/content'; 25 | @import './node_modules/bulma/sass/elements/form'; 26 | @import './node_modules/bulma/sass/elements/icon'; 27 | @import './node_modules/bulma/sass/elements/table'; 28 | @import './node_modules/bulma/sass/elements/tag'; 29 | @import './node_modules/bulma/sass/elements/title'; 30 | @import './node_modules/bulma/sass/elements/progress'; 31 | @import './node_modules/bulma/sass/elements/other'; 32 | @import './node_modules/bulma/sass/components/nav'; 33 | @import './node_modules/bulma/sass/components/modal'; 34 | @import './node_modules/bulma/sass/grid/columns'; 35 | @import './node_modules/bulma/sass/layout/_all'; 36 | 37 | @font-face { 38 | font-family: 'ailight'; 39 | src: url('ailight.ttf?vhsro1') format('truetype'), url('ailight.woff?vhsro1') format('woff'); 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | i { 45 | font-family: 'ailight', sans-serif !important; 46 | speak-as: normal; 47 | font-style: normal; 48 | font-weight: normal; 49 | font-variant: normal; 50 | text-transform: none; 51 | line-height: 1; 52 | vertical-align: -webkit-baseline-middle; 53 | -webkit-font-smoothing: antialiased; 54 | -moz-osx-font-smoothing: grayscale; 55 | cursor: pointer; 56 | } 57 | 58 | .icon-eye { 59 | font-size: 1.5rem; 60 | height: 2.75rem; 61 | 62 | &:before { 63 | content: "\e900"; 64 | color: $grey-lighter; 65 | vertical-align: -webkit-baseline-middle; 66 | } 67 | } 68 | 69 | .icon-arrow-loop:before { 70 | content: "\e901"; 71 | } 72 | 73 | .icon-power:before { 74 | content: "\e902"; 75 | } 76 | 77 | .icon-arrow-sync:before { 78 | content: "\e903"; 79 | color: $grey; 80 | } 81 | 82 | .footer { 83 | padding: 2rem 1.5rem; 84 | margin-top: 3rem; 85 | } 86 | 87 | #menu { 88 | z-index: 0; 89 | margin-bottom: 2rem; 90 | } 91 | 92 | #page_start { 93 | p { 94 | margin-bottom: 2rem; 95 | } 96 | 97 | .table { 98 | tr { 99 | &:hover { 100 | background: none; 101 | } 102 | } 103 | 104 | th { 105 | text-align: right; 106 | border: none; 107 | } 108 | 109 | td { 110 | vertical-align: middle; 111 | border: none; 112 | } 113 | } 114 | } 115 | 116 | #page_about { 117 | div:first-child { 118 | margin-bottom: 1rem; 119 | } 120 | 121 | .table { 122 | tr { 123 | &:hover { 124 | background: none; 125 | } 126 | } 127 | 128 | th { 129 | text-align: right; 130 | border: none; 131 | } 132 | 133 | td { 134 | vertical-align: middle; 135 | border: none; 136 | } 137 | } 138 | } 139 | 140 | .nav { 141 | height: 5rem; 142 | } 143 | 144 | .nav-item img { 145 | max-height: 3.75rem; 146 | } 147 | 148 | // Switch Component 149 | .switch { 150 | --height: 22px; 151 | 152 | input { 153 | opacity: 0; 154 | display: inline-block; 155 | width: 100%; 156 | height: 100%; 157 | position: absolute; 158 | z-index: 1; 159 | cursor: pointer; 160 | } 161 | 162 | appearance: none; 163 | position: relative; 164 | outline: 0; 165 | border-radius: calc(0.8 * var(--height)); 166 | width: calc(1.625 * var(--height)); 167 | height: var(--height); 168 | background-color: $border; 169 | border: 1px solid $border; 170 | cursor: pointer; 171 | box-sizing: border-box; 172 | display: inline-block; 173 | -webkit-tap-highlight-color: transparent; 174 | 175 | &:after, 176 | &:before { 177 | content: " "; 178 | position: absolute; 179 | top: 0; 180 | left: 0; 181 | height: calc(var(--height) - 2px); 182 | border-radius: calc((var(--height) - 2px) / 2); 183 | transition: 0.233s; 184 | } 185 | 186 | &:before { 187 | width: calc(1.625 * var(--height) - 2px); 188 | background-color: $grey-lighter; 189 | } 190 | 191 | &:after { 192 | width: calc(var(--height) - 2px); 193 | background-color: #FFF; 194 | box-shadow: 0 2px 3px rgba(17, 17, 17, 0.1); 195 | } 196 | 197 | &.checked { 198 | border-color: $text; 199 | background-color: $text; 200 | 201 | &:before { 202 | transform: scale(0); 203 | } 204 | 205 | &:after { 206 | transform: translateX(calc(0.625 * var(--height))); 207 | } 208 | } 209 | 210 | // Colors 211 | @each $name, 212 | $pair in ("primary": ($primary, 213 | $primary-invert)) { 214 | $color: nth($pair, 1); 215 | &.is-#{$name} { 216 | &.checked { 217 | border-color: $color; 218 | background-color: $color; 219 | } 220 | } 221 | } 222 | } 223 | 224 | // Slider Component 225 | input[type=range].slider { 226 | $radius: 290486px; 227 | --height: 8px; 228 | 229 | border: none; 230 | border-radius: $radius; 231 | height: var(--height); 232 | padding: 0; 233 | margin: 0; 234 | cursor: pointer; 235 | outline: none; 236 | background: $border; 237 | -webkit-tap-highlight-color: transparent; 238 | 239 | &:focus { 240 | outline: none; 241 | } 242 | 243 | // http://stackoverflow.com/questions/18794026/remove-dotted-outline-from-range-input-element-in-firefox 244 | &::-moz-focus-outer { 245 | border: none; 246 | } 247 | 248 | &, 249 | &::-webkit-slider-runnable-track, 250 | &::-webkit-slider-thumb { 251 | appearance: none; 252 | -webkit-appearance: none; 253 | } 254 | 255 | @mixin thumb-base() { 256 | border-radius: 50%; 257 | height: calc(var(--height) * 2.33); 258 | width: calc(var(--height) * 2.33); 259 | background-color: #FFF; 260 | border: calc(var(--height) / 2) solid $text; 261 | box-shadow: 0 2px 3px rgba(17, 17, 17, 0.1); 262 | transform: translateZ(0); 263 | transition: (0.233s / 2) ease-in-out; 264 | box-sizing: border-box; 265 | &:hover { 266 | transform: scale(1.25); 267 | } 268 | } 269 | @mixin thumb-base-active { 270 | cursor: grabbing; 271 | } 272 | @mixin track { 273 | display: flex; 274 | align-items: center; 275 | height: var(--height); 276 | border-radius: $radius; 277 | --track-background: linear-gradient(to right, transparent var(--low), $text calc(0%), $text var(--high), transparent calc(0%)) no-repeat 0 100%; 278 | background: var(--track-background); 279 | transform: translateZ(0); 280 | transition: (0.233s / 2); 281 | } 282 | 283 | &::-webkit-slider-thumb { 284 | @include thumb-base(); 285 | 286 | &:active { 287 | @include thumb-base-active(); 288 | } 289 | } 290 | 291 | &::-webkit-slider-runnable-track { 292 | @include track(); 293 | } 294 | 295 | &::-moz-range-thumb { 296 | @include thumb-base(); 297 | 298 | &:active { 299 | @include thumb-base-active(); 300 | } 301 | } 302 | 303 | &::-moz-range-progress:focus { 304 | outline: 0; 305 | border: 0; 306 | } 307 | 308 | &::-moz-range-track { 309 | background: transparent; 310 | } 311 | 312 | &::-moz-range-progress { 313 | display: flex; 314 | align-items: center; 315 | width: 100%; 316 | height: var(--height); 317 | border-radius: $radius; 318 | background-color: $text; 319 | } 320 | 321 | &::-ms-thumb { 322 | @include thumb-base(); 323 | 324 | &:active { 325 | @include thumb-base-active(); 326 | } 327 | } 328 | 329 | &::-ms-tooltip { 330 | display: none; 331 | } 332 | 333 | // Colors 334 | @each $name, 335 | $pair in ("primary": ($primary, 336 | $primary-invert), 337 | "info": ($info, 338 | $info-invert), 339 | "success": ($success, 340 | $success-invert), 341 | "danger": ($danger, 342 | $danger-invert)) { 343 | $color: nth($pair, 1); 344 | &.is-#{$name} { 345 | &::-webkit-slider-thumb { 346 | border-color: $color; 347 | } 348 | 349 | &::-webkit-slider-runnable-track { 350 | --track-background: linear-gradient(to right, transparent var(--low), $color calc(0%), $color var(--high), transparent calc(0%)) no-repeat 0 100%; 351 | background: var(--track-background); 352 | } 353 | 354 | // http://www.quirksmode.org/blog/archives/2015/11/styling_and_scr.html 355 | &::-moz-range-thumb { 356 | border-color: $color; 357 | } 358 | 359 | &::-moz-range-progress { 360 | background-color: $color; 361 | } 362 | 363 | &::-ms-thumb { 364 | border-color: $color; 365 | } 366 | 367 | &::-ms-fill-lower { 368 | background-color: $color; 369 | } 370 | } 371 | } 372 | } 373 | 374 | // Bubble showing slider's numerical value 375 | output { 376 | min-width: 2.6rem; 377 | margin-left: 0.5rem; 378 | } 379 | -------------------------------------------------------------------------------- /lib/AiLight/AiLight.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Library 3 | * 4 | * AiLight is a simple library to control an AiLight that contains the MY9291 5 | * LED driver and encapsulates the MY9291 LED driver made by Xose Pérez 6 | * 7 | * This file is part of the AiLight Firmware. 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | 11 | * Created by Sacha Telgenhof 12 | * (https://www.sachatelgenhof.com) 13 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 14 | */ 15 | 16 | #include "AiLight.hpp" 17 | 18 | AiLightClass::AiLightClass(my92xx_model_t model, uint8_t count) { 19 | _my92xx = new my92xx(model, count, MY92XX_DI_PIN, MY92XX_DCKI_PIN, 20 | MY92XX_COMMAND_DEFAULT); 21 | 22 | setRGBW(); // Initialise colour channels 23 | } 24 | 25 | AiLightClass::AiLightClass(my92xx_model_t model, uint8_t count, 26 | const AiLightClass &obj) { 27 | 28 | _my92xx = new my92xx(model, count, MY92XX_DI_PIN, MY92XX_DCKI_PIN, 29 | MY92XX_COMMAND_DEFAULT); 30 | *_my92xx = *obj._my92xx; 31 | 32 | setRGBW(); // Initialise colour channels 33 | } 34 | 35 | AiLightClass::~AiLightClass(void) { delete _my92xx; } 36 | 37 | uint8_t AiLightClass::getBrightness(void) { return _brightness; } 38 | 39 | void AiLightClass::setBrightness(uint16_t level) { 40 | _brightness = constrain(level, 0, MY92XX_LEVEL_MAX); // Force boundaries 41 | 42 | setRGBW(); 43 | } 44 | 45 | bool AiLightClass::getState(void) { return _my92xx->getState(); } 46 | 47 | void AiLightClass::setState(bool state) { 48 | _my92xx->setState(state); 49 | _my92xx->update(); 50 | } 51 | 52 | Color AiLightClass::getColor(void) { return _color; } 53 | 54 | void AiLightClass::setColor(uint8_t red, uint8_t green, uint8_t blue) { 55 | _color.red = red; 56 | _color.green = green; 57 | _color.blue = blue; 58 | 59 | setRGBW(); 60 | } 61 | 62 | void AiLightClass::setWhite(uint8_t white) { 63 | _color.white = white; 64 | 65 | setRGBW(); 66 | } 67 | 68 | uint16_t AiLightClass::getColorTemperature(void) { return _color_temp; } 69 | 70 | void AiLightClass::setColorTemperature(uint16_t temperature) { 71 | Color ctColor = colorTemperature2RGB(temperature); 72 | 73 | _color.red = ctColor.red; 74 | _color.green = ctColor.green; 75 | _color.blue = ctColor.blue; 76 | 77 | setRGBW(); 78 | } 79 | 80 | Color AiLightClass::colorTemperature2RGB(uint16_t temperature) { 81 | _color_temp = temperature; // Save colour temperature setting 82 | Color ctColor; 83 | 84 | temperature = temperature == 0 ? 1 : temperature; // Avoid division by zero 85 | 86 | // Convert from mired value to relative Kelvin temperature. The temperature 87 | // must fall between 1000 and 40000 degrees. All calculations require 88 | // tmpKelvin \ 100, so only do the conversion once 89 | uint16_t tmpKelvin = constrain(1000000UL / temperature, 1000, 40000) / 100; 90 | 91 | // Perform conversions from colour temperature to RGB values 92 | 93 | // Red 94 | float red = tmpKelvin <= 66 95 | ? MY92XX_LEVEL_MAX 96 | : 329.698727446 * pow((tmpKelvin - 60), -0.1332047592); 97 | 98 | ctColor.red = constrain(red, 0, MY92XX_LEVEL_MAX); // Force boundaries 99 | 100 | // Green 101 | float green = tmpKelvin <= 66 102 | ? 99.4708025861 * log(tmpKelvin) - 161.1195681661 103 | : 288.1221695283 * pow(tmpKelvin, -0.0755148492); 104 | 105 | ctColor.green = constrain(green, 0, MY92XX_LEVEL_MAX); // Force boundaries 106 | 107 | // Blue 108 | float blue = tmpKelvin >= 66 109 | ? MY92XX_LEVEL_MAX 110 | : tmpKelvin <= 19 ? 0 111 | : 138.5177312231 * log(tmpKelvin - 10) - 112 | 305.0447927307; 113 | 114 | ctColor.blue = constrain(blue, 0, MY92XX_LEVEL_MAX); // Force boundaries 115 | 116 | return ctColor; 117 | } 118 | 119 | bool AiLightClass::hasGammaCorrection(void) { return _gamma_correction; } 120 | 121 | void AiLightClass::useGammaCorrection(bool gamma) { 122 | _gamma_correction = gamma; 123 | setRGBW(); 124 | } 125 | 126 | void AiLightClass::setRGBW() { 127 | uint8_t red = 128 | (_gamma_correction) ? pgm_read_byte(&gamma8[_color.red]) : _color.red; 129 | uint8_t green = 130 | (_gamma_correction) ? pgm_read_byte(&gamma8[_color.green]) : _color.green; 131 | uint8_t blue = 132 | (_gamma_correction) ? pgm_read_byte(&gamma8[_color.blue]) : _color.blue; 133 | 134 | _my92xx->setChannel(MY92XX_RED, 135 | (uint32_t)map(red, 0, MY92XX_LEVEL_MAX, 0, _brightness)); 136 | _my92xx->setChannel( 137 | MY92XX_GREEN, (uint32_t)map(green, 0, MY92XX_LEVEL_MAX, 0, _brightness)); 138 | _my92xx->setChannel(MY92XX_BLUE, 139 | (uint32_t)map(blue, 0, MY92XX_LEVEL_MAX, 0, _brightness)); 140 | _my92xx->setChannel( 141 | MY92XX_WHITE, 142 | (uint32_t)map(_color.white, 0, MY92XX_LEVEL_MAX, 0, _brightness)); 143 | _my92xx->setChannel( 144 | MY92XX_WHITE + 1, 145 | (uint32_t)map(_color.white, 0, MY92XX_LEVEL_MAX, 0, _brightness)); 146 | 147 | _my92xx->setState(true); 148 | _my92xx->update(); 149 | } 150 | -------------------------------------------------------------------------------- /lib/AiLight/AiLight.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Library 3 | * 4 | * AiLight is a simple library to control an AiLight that contains a MY92XX 5 | * LED driver and encapsulates the MY92XX LED driver made by Xose Pérez 6 | * 7 | * This file is part of the AiLight Firmware. 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | 11 | * Created by Sacha Telgenhof 12 | * (https://www.sachatelgenhof.com) 13 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 14 | */ 15 | 16 | #ifndef AiLight_h 17 | #define AiLight_h 18 | 19 | #include 20 | 21 | // MY92XX settings 22 | #define MY92XX_MODEL MY92XX_MODEL_MY9231 23 | #define MY92XX_CHIPS 2 24 | #define MY92XX_DI_PIN 13 25 | #define MY92XX_DCKI_PIN 15 26 | #define MY92XX_RED 0 27 | #define MY92XX_GREEN 1 28 | #define MY92XX_BLUE 2 29 | #define MY92XX_WHITE 3 30 | 31 | // The maximum level used for colour channels and brightness 32 | #define MY92XX_LEVEL_MAX 255 33 | 34 | // Structure for holding the levels of all the colour channels 35 | 36 | struct Color { 37 | uint8_t red; 38 | uint8_t green; 39 | uint8_t blue; 40 | uint8_t white; 41 | }; 42 | 43 | // This table remaps linear input values to nonlinear gamma-corrected output 44 | // values. The output values are specified for 8-bit colours with a gamma 45 | // correction factor of 2.8 46 | const static uint8_t PROGMEM gamma8[256] = { 47 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 49 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 50 | 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 51 | 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 52 | 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 53 | 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 54 | 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 27, 28, 29, 29, 30, 55 | 31, 32, 32, 33, 34, 35, 35, 36, 37, 38, 39, 39, 40, 41, 42, 56 | 43, 44, 45, 46, 47, 48, 49, 50, 50, 51, 52, 54, 55, 56, 57, 57 | 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 70, 72, 73, 74, 58 | 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, 90, 92, 93, 95, 59 | 96, 98, 99, 101, 102, 104, 105, 107, 109, 110, 112, 114, 115, 117, 119, 60 | 120, 122, 124, 126, 127, 129, 131, 133, 135, 137, 138, 140, 142, 144, 146, 61 | 148, 150, 152, 154, 156, 158, 160, 162, 164, 167, 169, 171, 173, 175, 177, 62 | 180, 182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208, 210, 213, 63 | 215, 218, 220, 223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 64 | 255}; 65 | 66 | class AiLightClass { 67 | public: 68 | AiLightClass(); 69 | AiLightClass(my92xx_model_t model, uint8_t count); 70 | AiLightClass(my92xx_model_t model, uint8_t count, const AiLightClass &obj); 71 | ~AiLightClass(void); 72 | 73 | /** 74 | * @brief Returns the current state of the AiLight (i.e on or off) 75 | * 76 | * @return the current state of the AiLight 77 | */ 78 | bool getState(void); 79 | 80 | /** 81 | * @brief Sets the state of the AiLight (i.e on or off) 82 | * 83 | * @param state the desired state (true/false) 84 | * 85 | * @return void 86 | */ 87 | void setState(bool state); 88 | 89 | /** 90 | * @brief Returns the currently set level of brightness 91 | * 92 | * @return the currently set level of brightness 93 | */ 94 | uint8_t getBrightness(void); 95 | 96 | /** 97 | * @brief Sets the level for brightness 98 | * 99 | * This functions sets the level for brightness and switches on the AiLight. 100 | * 101 | * @param level the desired brightness level (range 0 - 255) 102 | * 103 | * @return void 104 | */ 105 | void setBrightness(uint16_t level); 106 | 107 | /** 108 | * @brief Returns the currently set levels of all 4 colour channels (RGBW) 109 | * 110 | * @return the set levels of all 4 colour channels (RGBW) 111 | */ 112 | Color getColor(void); 113 | 114 | /** 115 | * @brief Sets the levels for the red, green and blue colour channels 116 | * 117 | * This functions set the individual levels for the red, green and blue colour 118 | * channels and switches on the AiLight. Note that all four colour channels 119 | * work in conjunction: i.e. the white colour channel is not changed when the 120 | * levels of the RGB colour channels are changed. 121 | * 122 | * @param red the desired level for the red colour channel (range 0 - 255) 123 | * @param green the desired level for the green colour channel (range 0 - 255) 124 | * @param blue the desired level for the blue colour channel (range 0 - 255) 125 | * 126 | * @return void 127 | */ 128 | void setColor(uint8_t red, uint8_t green, uint8_t blue); 129 | 130 | /** 131 | * @brief Sets the level for the white colour channel 132 | * 133 | * This functions set the individual level for white colour channel and 134 | * switches on the AiLight. Note that all four colour channels work in 135 | * conjunction: i.e. the RGB colour channels are not changed when the level of 136 | * the white colour channel is changed. 137 | * 138 | * @param white the desired level for the white colour channel (range 0 - 255) 139 | * 140 | * @return void 141 | */ 142 | void setWhite(uint8_t white); 143 | 144 | /** 145 | * @brief Returns the currently set colour temperature 146 | * 147 | * @return the currently set colour temperature (in mired) 148 | */ 149 | uint16_t getColorTemperature(void); 150 | 151 | /** 152 | * @brief Sets the colour of the AiLight based on the given colour temperature 153 | * 154 | * This method sets the colour of the AiLight based on the given colour 155 | * temperature and switches on the AiLight. 156 | * 157 | * @param temperature the desired colour temperature (in mired) 158 | */ 159 | void setColorTemperature(uint16_t temperature); 160 | 161 | /** 162 | * @brief Converts a colour temperature to the equivalent RGB colours 163 | * 164 | * This method converts a given colour temperature to RGB colour values. 165 | * The colour temperature is defined in mired (micro reciprocal degree, the 166 | * mired is a unit of measurement used to express colour temperature). 167 | * (Conversion is based on the algorithm by Tanner Helland.) 168 | * 169 | * @param temperature the desired colour temperature (in mired) 170 | * @return Color a color object (RGBW) representing the given colour 171 | * temperature 172 | * 173 | * Sources: 174 | * http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ 175 | * https://en.wikipedia.org/wiki/Mired 176 | */ 177 | Color colorTemperature2RGB(uint16_t temperature); 178 | 179 | /** 180 | * @brief Returns whether Gamma Correction is enabled or disabled 181 | * 182 | * @return the current status whether Gamma Correction is enabled or disabled 183 | */ 184 | bool hasGammaCorrection(void); 185 | 186 | /** 187 | * @brief Use Gamma Correction or not (i.e on or off) 188 | * 189 | * Gamma correction controls the overall brightness of an image. Images which 190 | * are not properly corrected can look either bleached out, or too dark. 191 | * Trying to reproduce colors accurately also requires some knowledge of 192 | * gamma. Varying the amount of gamma correction changes not only the 193 | * brightness, but also the ratios of red to green to blue. 194 | * 195 | * Sources: 196 | * http://cgsd.com/papers/gamma_intro.html 197 | * https://learn.adafruit.com/led-tricks-gamma-correction/the-issue 198 | * 199 | * @param value the desired state for using Gamma Correction (true/false) 200 | * 201 | * @return void 202 | */ 203 | void useGammaCorrection(bool gamma); 204 | 205 | private: 206 | my92xx *_my92xx; // MY92XX driver handle 207 | 208 | // Current colour levels (RGBW). Initial values are 1/4th of maximum 209 | Color _color = {MY92XX_LEVEL_MAX >> 2, MY92XX_LEVEL_MAX >> 2, 210 | MY92XX_LEVEL_MAX >> 2, 0}; 211 | 212 | // Current brightness level. Initial value is 1/4th of maximum 213 | uint8_t _brightness = MY92XX_LEVEL_MAX >> 2; 214 | 215 | // Current colour temperature setting. Initial value is equivalent of 2700K 216 | uint16_t _color_temp = 370; 217 | 218 | /** 219 | * @brief Sets the levels of all colour levels (RGBW) to the MY92XX LED 220 | * driver. 221 | * 222 | * This internal method sets the levels of all colour levels (RGBW) to the 223 | * MY9291 LED driver including the brightness level. To switch on the AiLight, 224 | * the setState() method needs to be used subsequently. 225 | * 226 | * @return void 227 | */ 228 | void setRGBW(); 229 | 230 | // Gamma correction is enabled or disabled 231 | bool _gamma_correction = false; 232 | }; 233 | 234 | #endif 235 | -------------------------------------------------------------------------------- /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 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | More information about PlatformIO Library Dependency Finder 36 | - http://docs.platformio.org/page/librarymanager/ldf.html 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ailight", 3 | "version": "1.0.0", 4 | "description": "AiLight is a custom firmware for the inexpensive Ai-Thinker (or equivalent) RGBW WiFi light bulbs that has a ESP8266 on board and is controlled by the MY9291 or MY9231 LED driver.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "AiLight", 8 | "firmware", 9 | "ESP8266", 10 | "Arduino", 11 | "LED", 12 | "RGBW", 13 | "Bulb" 14 | ], 15 | "author": { 16 | "name": "Sacha Telgenhof", 17 | "email": "me@sachatelgenhof.com", 18 | "url": "https://www.sachatelgenhof.com" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/stelgenhof/AiLight.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/stelgenhof/AiLight/issues" 26 | }, 27 | "homepage": "https://github.com/stelgenhof/AiLight#readme", 28 | "scripts": { 29 | "test": "echo \"Error: no test specified\" && exit 1", 30 | "format": "prettier --write \"**/*.js\"" 31 | }, 32 | "devDependencies": { 33 | "bulma": "^0.4", 34 | "del": "^6.0", 35 | "gulp": "^4.0", 36 | "gulp-base64-favicon": "^1.0", 37 | "gulp-clean-css": "^4.0", 38 | "gulp-css-base64": "^2.0", 39 | "gulp-gzip": "^1.4", 40 | "gulp-htmlmin": "^5.0", 41 | "gulp-inline": "^0.1", 42 | "gulp-plumber": "^1.2", 43 | "gulp-sass": "^5.0", 44 | "gulp-uglify": "^3.0", 45 | "prettier": "2.3.2", 46 | "sass": "^1.38.0", 47 | "uglify-es": "^3.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /platformio.example.ini: -------------------------------------------------------------------------------- 1 | ; AiLight Firmware - Project Configuration File 2 | ; 3 | ; This file is part of the AiLight Firmware. 4 | ; For the full copyright and license information, please view the LICENSE 5 | ; file that was distributed with this source code. 6 | ; 7 | ; Created by Sacha Telgenhof 8 | ; (https://www.sachatelgenhof.com) 9 | ; Copyright (c) 2016 - 2021 Sacha Telgenhof 10 | 11 | [platformio] 12 | default_envs = dev 13 | build_dir = .pioenvs 14 | 15 | [common] 16 | platform = espressif8266@1.5.0 17 | framework = arduino 18 | monitor_speed = 115200 19 | upload_speed = 115200 20 | upload_port = ".local" 21 | ota_password = hinotori 22 | ota_port = 8266 23 | flag_flash_size = -Wl,-Tesp8266.flash.1m128.ld 24 | flag_debug = -DDEBUG -g -w 25 | lib_deps = 26 | ArduinoJson@5.13.4 27 | AsyncMqttClient@0.8.2 28 | xoseperez/my92xx 29 | ESPAsyncTCP@1.2.2 30 | ESP Async WebServer@1.2.2 31 | 32 | # Development/Debug environment 33 | [env:dev] 34 | platform = ${common.platform} 35 | board = esp8285 36 | framework = ${common.framework} 37 | monitor_speed = ${common.monitor_speed} 38 | build_flags = ${common.flag_debug} ${common.flag_flash_size} 39 | lib_deps = ${common.lib_deps} 40 | extra_scripts = build.py 41 | 42 | # Development/Debug environment for OTA Updates 43 | [env:dev-ota] 44 | platform = ${common.platform} 45 | board = esp8285 46 | framework = ${common.framework} 47 | monitor_speed = ${common.monitor_speed} 48 | build_flags = ${common.flag_debug} ${common.flag_flash_size} 49 | lib_deps = ${common.lib_deps} 50 | extra_scripts = build.py 51 | upload_speed = ${common.upload_speed} 52 | upload_port = ${common.upload_port} 53 | upload_flags = 54 | --auth=${common.ota_password} 55 | --port=${common.ota_port} 56 | 57 | # Production optimized environment 58 | [env:prod] 59 | platform = ${common.platform} 60 | board = esp8285 61 | framework = ${common.framework} 62 | build_flags = -Os ${common.flag_flash_size} 63 | lib_deps = ${common.lib_deps} 64 | extra_scripts = build.py 65 | 66 | # Production optimized environment for OTA Updates 67 | [env:prod-ota] 68 | platform = ${common.platform} 69 | board = esp8285 70 | framework = ${common.framework} 71 | build_flags = -Os ${common.flag_flash_size} 72 | lib_deps = ${common.lib_deps} 73 | extra_scripts = build.py 74 | upload_speed = ${common.upload_speed} 75 | upload_port = ${common.upload_port} 76 | upload_flags = 77 | --auth=${common.ota_password} 78 | --port=${common.ota_port} 79 | -------------------------------------------------------------------------------- /src/_mqtt.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware - MQTT Module 3 | * 4 | * The MQTT module holds all the code to manage all functions for communicating 5 | * with the MQTT broker. 6 | * 7 | * This file is part of the AiLight Firmware. 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * Created by Sacha Telgenhof 12 | * (https://www.sachatelgenhof.com) 13 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 14 | */ 15 | 16 | /** 17 | * @brief Publish a message to an MQTT topic 18 | * 19 | * @param topic the MQTT topic to publish the message to 20 | * @param message the message to be published 21 | */ 22 | void mqttPublish(const char *topic, const char *message) { 23 | // Don't do anything if we are not connected to the MQTT broker 24 | if (!mqtt.connected() || _mqtt_connecting) { 25 | return; 26 | } 27 | 28 | if ((os_strlen(topic) > 0) && (os_strlen(message) > 0)) { 29 | mqtt.publish(topic, MQTT_QOS_LEVEL, MQTT_RETAIN, message); 30 | 31 | DEBUGLOG("[MQTT] Published message to '%s'\n", topic); 32 | } 33 | } 34 | 35 | /** 36 | * @brief Subscribe to an MQTT topic 37 | * 38 | * @param topic the MQTT topic to subscribe to 39 | * @param qos the desired QoS level (defaults to MQTT_QOS_LEVEL) 40 | */ 41 | void mqttSubscribe(const char *topic, uint8_t qos = MQTT_QOS_LEVEL) { 42 | // Don't do anything if we are not connected to the MQTT broker 43 | if (!mqtt.connected() || _mqtt_connecting) { 44 | return; 45 | } 46 | 47 | if (os_strlen(topic) > 0) { 48 | mqtt.subscribe(topic, qos); 49 | 50 | DEBUGLOG("[MQTT] Subscribed to topic '%s'\n", topic); 51 | } 52 | } 53 | 54 | /** 55 | * @brief Unsubscribe from an MQTT topic 56 | * 57 | * @param topic the MQTT topic to unsubscribe from 58 | */ 59 | void mqttUnsubscribe(const char *topic) { 60 | // Don't do anything if we are not connected to the MQTT broker 61 | if (!mqtt.connected() || _mqtt_connecting) { 62 | return; 63 | } 64 | 65 | if (os_strlen(topic) > 0) { 66 | mqtt.unsubscribe(topic); 67 | 68 | DEBUGLOG("[MQTT] Unsubscribed from topic '%s'\n", topic); 69 | } 70 | } 71 | 72 | /** 73 | * @brief Register MQTT callback functions 74 | * 75 | * @param the callback function to register 76 | */ 77 | void mqttRegister(void (*callback)(uint8_t, const char *, const char *)) { 78 | _mqtt_callbacks.push_back(callback); 79 | } 80 | 81 | /** 82 | * @brief Event handler for when a connection to the MQTT has been established. 83 | * 84 | * @param bool sessionPresent 85 | */ 86 | void onMQTTConnect(bool sessionPresent) { 87 | DEBUGLOG("[MQTT] Connected\n"); 88 | 89 | _mqtt_connecting = false; 90 | 91 | // Notify subscribers (connected) 92 | for (uint8_t i = 0; i < _mqtt_callbacks.size(); i++) { 93 | (*_mqtt_callbacks[i])(MQTT_EVENT_CONNECT, NULL, NULL); 94 | } 95 | } 96 | 97 | /** 98 | * @brief Event handler for when the connection to the MQTT broker has been 99 | * disconnected. 100 | */ 101 | void onMQTTDisconnect(AsyncMqttClientDisconnectReason reason) { 102 | DEBUGLOG("[MQTT] Disconnected. Reason: %d\n", reason); 103 | 104 | _mqtt_connecting = false; 105 | 106 | // Notify subscribers (disconnected) 107 | for (uint8_t i = 0; i < _mqtt_callbacks.size(); i++) { 108 | (*_mqtt_callbacks[i])(MQTT_EVENT_DISCONNECT, NULL, NULL); 109 | } 110 | } 111 | 112 | /** 113 | * @brief Event handler for when a message is received from the MTT broker 114 | * 115 | * @param topic the MQTT topic to which the message has been published 116 | * @param payload the contents/message that has been published 117 | * @param properties additional properties of the published message 118 | * @param length size of the published message 119 | * @param index ? 120 | * @param total ? 121 | */ 122 | void onMQTTMessage(char *topic, char *payload, 123 | AsyncMqttClientMessageProperties properties, size_t length, 124 | size_t index, size_t total) { 125 | 126 | // Convert payload into char variable 127 | char message[length + 1]; 128 | os_memcpy(message, payload, length); 129 | message[length] = 0; 130 | 131 | DEBUGLOG("[MQTT] Received message on '%s'\n", topic, message); 132 | 133 | // Notify subscribers (message received) 134 | for (uint8_t i = 0; i < _mqtt_callbacks.size(); i++) { 135 | (*_mqtt_callbacks[i])(MQTT_EVENT_MESSAGE, topic, message); 136 | } 137 | } 138 | 139 | /** 140 | * @brief Handles the connection to the MQTT broker. 141 | */ 142 | void mqttConnect() { 143 | 144 | if (!WiFi.isConnected()) { 145 | return; 146 | } 147 | 148 | // Do not make a connection if already connected or trying to 149 | if (mqtt.connected() || _mqtt_connecting) { 150 | return; 151 | } 152 | 153 | if (!os_strlen(cfg.mqtt_server) > 0) { 154 | DEBUGLOG("[MQTT] MQTT Broker not configured.\n"); 155 | return; 156 | } 157 | 158 | DEBUGLOG("[MQTT] Connecting to broker '%s:%i'", cfg.mqtt_server, 159 | cfg.mqtt_port); 160 | 161 | if ((os_strlen(cfg.mqtt_user) > 0) && (os_strlen(cfg.mqtt_password) > 0)) { 162 | DEBUGLOG(" as user '%s'\n", cfg.mqtt_user); 163 | } 164 | 165 | _mqtt_connecting = true; 166 | 167 | mqtt.connect(); 168 | } 169 | 170 | /** 171 | * @brief Bootstrap function for the MQTT connection 172 | */ 173 | void setupMQTT() { 174 | mqtt.onConnect(onMQTTConnect); 175 | mqtt.onDisconnect(onMQTTDisconnect); 176 | mqtt.onMessage(onMQTTMessage); 177 | 178 | mqtt.setServer(cfg.mqtt_server, cfg.mqtt_port); 179 | mqtt.setKeepAlive(MQTT_KEEPALIVE); 180 | mqtt.setCleanSession(false); 181 | mqtt.setClientId(cfg.hostname); 182 | mqtt.setWill(cfg.mqtt_lwt_topic, 2, MQTT_RETAIN, MQTT_STATUS_OFFLINE); 183 | mqtt.setCredentials(cfg.mqtt_user, cfg.mqtt_password); 184 | 185 | mqttReconnectTimer.attach_ms(MQTT_RECONNECT_TIME, mqttConnect); 186 | } 187 | -------------------------------------------------------------------------------- /src/_ota.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware - OTA Module 3 | * 4 | * The OTA (Over The Air) module holds all the code to manage over the air 5 | * firmware updates. 6 | * 7 | * This file is part of the AiLight Firmware. 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * Created by Sacha Telgenhof 12 | * (https://www.sachatelgenhof.com) 13 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 14 | */ 15 | 16 | /** 17 | * @brief Bootstrap function for the OTA UDP service 18 | */ 19 | void setupOTA() { 20 | ArduinoOTA.setPort(OTA_PORT); 21 | ArduinoOTA.setHostname(cfg.hostname); 22 | ArduinoOTA.setPassword(ADMIN_PASSWORD); 23 | 24 | DEBUGLOG("[OTA ] Server running at %s:%u\n", ArduinoOTA.getHostname().c_str(), 25 | OTA_PORT); 26 | 27 | ArduinoOTA.onStart([]() { 28 | DEBUGLOG("[OTA ] Start\n"); 29 | events.send("start", "ota"); 30 | 31 | ws.enable(false); // Disable WebSocket client connections 32 | ws.closeAll(); // Close WebSocket client connections 33 | }); 34 | 35 | ArduinoOTA.onEnd([]() { 36 | DEBUGLOG("\n[OTA ] End\n"); 37 | events.send("end", "ota"); 38 | }); 39 | 40 | ArduinoOTA.onProgress([](uint32_t progress, uint32_t total) { 41 | uint8_t pp = (progress / (total / 100)); 42 | DEBUGLOG("Progress: %u%%\r", pp); 43 | 44 | char p[6]; 45 | sprintf(p, "p-%u", pp); 46 | events.send(p, "ota"); 47 | }); 48 | 49 | ArduinoOTA.onError([](ota_error_t error) { 50 | DEBUGLOG("\n[OTA ] Error[%u]: ", error); 51 | if (error == OTA_AUTH_ERROR) 52 | DEBUGLOG("Authentication Failed\n"); 53 | else if (error == OTA_BEGIN_ERROR) 54 | DEBUGLOG("Begin Failed\n"); 55 | else if (error == OTA_CONNECT_ERROR) 56 | DEBUGLOG("Connection Failed\n"); 57 | else if (error == OTA_RECEIVE_ERROR) 58 | DEBUGLOG("Receive Failed\n"); 59 | else if (error == OTA_END_ERROR) 60 | DEBUGLOG("End Failed\n"); 61 | }); 62 | 63 | ArduinoOTA.begin(); 64 | } 65 | 66 | /** 67 | * @brief Listen to OTA requests 68 | */ 69 | void loopOTA() { ArduinoOTA.handle(); } 70 | -------------------------------------------------------------------------------- /src/_web.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware - Web Module 3 | * 4 | * The Web module contains all the code for handling the HTTP User Interface. 5 | * 6 | * This file is part of the AiLight Firmware. 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | * Created by Sacha Telgenhof 11 | * (https://www.sachatelgenhof.com) 12 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 13 | */ 14 | 15 | /** 16 | * @brief Check whether the requester is authorized using the requested API 17 | * endpoint 18 | * 19 | * @param request the API endpoint request object 20 | * 21 | * @return bool true if authorized, otherwise false 22 | */ 23 | bool authorizeAPI(AsyncWebServerRequest *request) { 24 | 25 | // Check if API Key is provided 26 | if (!request->hasHeader(HTTP_HEADER_APIKEY)) { 27 | DynamicJsonBuffer jsonBuffer; 28 | JsonObject &root = jsonBuffer.createObject(); 29 | root["error"] = "400"; 30 | root["message"] = "The required API Key is missing"; 31 | 32 | char buffer[root.measureLength() + 1]; 33 | root.printTo(buffer, sizeof(buffer)); 34 | 35 | AsyncWebServerResponse *response = 36 | request->beginResponse(400, HTTP_MIMETYPE_JSON, buffer); 37 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 38 | request->send(response); 39 | 40 | return false; 41 | } else { 42 | AsyncWebHeader *h = request->getHeader(HTTP_HEADER_APIKEY); 43 | if (!h->value().equals(cfg.api_key)) { 44 | 45 | DynamicJsonBuffer jsonBuffer; 46 | JsonObject &root = jsonBuffer.createObject(); 47 | root["error"] = "401"; 48 | root["message"] = "The given API Key is incorrect"; 49 | 50 | char buffer[root.measureLength() + 1]; 51 | root.printTo(buffer, sizeof(buffer)); 52 | 53 | AsyncWebServerResponse *response = 54 | request->beginResponse(401, HTTP_MIMETYPE_JSON, buffer); 55 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 56 | request->send(response); 57 | 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * @brief Publishes data to WebSocket client upon connection 67 | * 68 | * @param id the WebSocket client identifier 69 | * 70 | * @return void 71 | */ 72 | void wsStart(uint8_t id) { 73 | DynamicJsonBuffer jsonBuffer; 74 | JsonObject &root = jsonBuffer.createObject(); 75 | 76 | // Operational state 77 | root[KEY_STATE] = AiLight->getState() ? MQTT_PAYLOAD_ON : MQTT_PAYLOAD_OFF; 78 | root[KEY_BRIGHTNESS] = AiLight->getBrightness(); 79 | root[KEY_WHITE] = AiLight->getColor().white; 80 | root[KEY_COLORTEMP] = AiLight->getColorTemperature(); 81 | 82 | JsonObject &color = root.createNestedObject(KEY_COLOR); 83 | color[KEY_COLOR_R] = AiLight->getColor().red; 84 | color[KEY_COLOR_G] = AiLight->getColor().green; 85 | color[KEY_COLOR_B] = AiLight->getColor().blue; 86 | 87 | root[KEY_GAMMA_CORRECTION] = AiLight->hasGammaCorrection(); 88 | 89 | // Device settings/state 90 | JsonObject &device = root.createNestedObject("d"); 91 | createAboutJSON(device); 92 | 93 | // User settings 94 | JsonObject &settings = root.createNestedObject("s"); 95 | settings[KEY_HOSTNAME] = cfg.hostname; 96 | settings[KEY_WIFI_SSID] = cfg.wifi_ssid; 97 | settings[KEY_WIFI_PSK] = cfg.wifi_psk; 98 | settings[KEY_MQTT_SERVER] = cfg.mqtt_server; 99 | settings[KEY_MQTT_PORT] = cfg.mqtt_port; 100 | settings[KEY_MQTT_USER] = cfg.mqtt_user; 101 | settings[KEY_MQTT_PASSWORD] = cfg.mqtt_password; 102 | settings[KEY_MQTT_STATE_TOPIC] = cfg.mqtt_state_topic; 103 | settings[KEY_MQTT_COMMAND_TOPIC] = cfg.mqtt_command_topic; 104 | settings[KEY_MQTT_LWT_TOPIC] = cfg.mqtt_lwt_topic; 105 | settings[KEY_MQTT_HA_USE_DISCOVERY] = cfg.mqtt_ha_use_discovery; 106 | settings[KEY_MQTT_HA_IS_DISCOVERED] = cfg.mqtt_ha_is_discovered; 107 | 108 | // Ensure HA MQTT Discovery Prefix has a proper value 109 | if (cfg.mqtt_ha_disc_prefix == NULL || cfg.mqtt_ha_disc_prefix[0] == 0xFF) { 110 | os_strcpy(cfg.mqtt_ha_disc_prefix, MQTT_HOMEASSISTANT_DISCOVERY_PREFIX); 111 | } 112 | settings[KEY_MQTT_HA_DISCOVERY_PREFIX] = cfg.mqtt_ha_disc_prefix; 113 | 114 | // REST API 115 | settings[KEY_REST_API_ENABLED] = cfg.api; 116 | if (cfg.api_key == NULL || cfg.api_key[0] == 0xFF) { 117 | os_strcpy(cfg.api_key, ADMIN_PASSWORD); 118 | } 119 | settings[KEY_REST_API_KEY] = cfg.api_key; 120 | 121 | settings[KEY_POWERUP_MODE] = cfg.powerup_mode; 122 | 123 | char buffer[root.measureLength() + 1]; 124 | root.printTo(buffer, sizeof(buffer)); 125 | 126 | ws.text(id, buffer); 127 | } 128 | 129 | void wsProcessMessage(uint8_t num, char *payload, size_t length) { 130 | DynamicJsonBuffer jsonBuffer; 131 | JsonObject &root = jsonBuffer.parseObject(payload); 132 | bool settings_changed = false; 133 | bool needRestart = false; 134 | 135 | if (!root.success()) { 136 | DEBUGLOG("[WEBSOCKET] Error parsing data\n"); 137 | return; 138 | } 139 | 140 | // Process commands 141 | if (root.containsKey("command")) { 142 | const char *command = root["command"]; 143 | 144 | DEBUGLOG("[WEBSOCKET] Client #%u requested a %s\n", num, command); 145 | 146 | // Execute restart command 147 | if (os_strcmp(command, "restart") == 0) { 148 | ESP.restart(); 149 | } 150 | 151 | // Execute reset command (load factory defaults) 152 | if (os_strcmp(command, "reset") == 0) { 153 | loadFactoryDefaults(); 154 | ESP.restart(); 155 | } 156 | } 157 | 158 | // Process new settings 159 | if (root.containsKey(KEY_SETTINGS) && root[KEY_SETTINGS].is()) { 160 | bool mqtt_changed = false; 161 | bool wifi_changed = false; 162 | settings_changed = true; 163 | 164 | JsonObject &settings = root[KEY_SETTINGS]; 165 | DEBUGLOG("[WEBSOCKET] Received new settings\n"); 166 | 167 | if (settings.containsKey(KEY_HOSTNAME)) { 168 | const char *hostname = settings[KEY_HOSTNAME]; 169 | if (os_strcmp(cfg.hostname, hostname) != 0) { 170 | os_strcpy(cfg.hostname, hostname); 171 | cfg.mqtt_ha_is_discovered = 172 | false; // Re-register the device via MQTT HASS Autodiscovery 173 | needRestart = true; 174 | } 175 | } 176 | 177 | if (settings.containsKey(KEY_MQTT_SERVER)) { 178 | const char *mqtt_server = settings[KEY_MQTT_SERVER]; 179 | if (os_strcmp(cfg.mqtt_server, mqtt_server) != 0) { 180 | os_strcpy(cfg.mqtt_server, mqtt_server); 181 | mqtt_changed = true; 182 | } 183 | } 184 | 185 | if (settings.containsKey(KEY_MQTT_PORT)) { 186 | uint16_t mqtt_port = (os_strlen(settings[KEY_MQTT_PORT]) > 0) 187 | ? settings[KEY_MQTT_PORT] 188 | : MQTT_PORT; 189 | if (cfg.mqtt_port != mqtt_port) { 190 | cfg.mqtt_port = mqtt_port; 191 | mqtt_changed = true; 192 | } 193 | } 194 | 195 | if (settings.containsKey(KEY_MQTT_USER)) { 196 | const char *mqtt_user = settings[KEY_MQTT_USER]; 197 | if (os_strcmp(cfg.mqtt_user, mqtt_user) != 0) { 198 | os_strcpy(cfg.mqtt_user, mqtt_user); 199 | mqtt_changed = true; 200 | } 201 | } 202 | 203 | if (settings.containsKey(KEY_MQTT_PASSWORD)) { 204 | const char *mqtt_password = settings[KEY_MQTT_PASSWORD]; 205 | if (os_strcmp(cfg.mqtt_password, mqtt_password) != 0) { 206 | os_strcpy(cfg.mqtt_password, mqtt_password); 207 | mqtt_changed = true; 208 | } 209 | } 210 | 211 | if (settings.containsKey(KEY_MQTT_STATE_TOPIC)) { 212 | const char *mqtt_state_topic = settings[KEY_MQTT_STATE_TOPIC]; 213 | if (os_strcmp(cfg.mqtt_state_topic, mqtt_state_topic) != 0) { 214 | os_strcpy(cfg.mqtt_state_topic, mqtt_state_topic); 215 | mqtt_changed = true; 216 | cfg.mqtt_ha_is_discovered = 217 | false; // Re-register the device via MQTT HASS Autodiscovery 218 | } 219 | } 220 | 221 | if (settings.containsKey(KEY_MQTT_COMMAND_TOPIC)) { 222 | const char *mqtt_command_topic = settings[KEY_MQTT_COMMAND_TOPIC]; 223 | if (os_strcmp(cfg.mqtt_command_topic, mqtt_command_topic) != 0) { 224 | os_strcpy(cfg.mqtt_command_topic, mqtt_command_topic); 225 | mqtt_changed = true; 226 | cfg.mqtt_ha_is_discovered = 227 | false; // Re-register the device via MQTT HASS Autodiscovery 228 | } 229 | } 230 | 231 | if (settings.containsKey(KEY_MQTT_LWT_TOPIC)) { 232 | const char *mqtt_lwt_topic = settings[KEY_MQTT_LWT_TOPIC]; 233 | if (os_strcmp(cfg.mqtt_lwt_topic, mqtt_lwt_topic) != 0) { 234 | os_strcpy(cfg.mqtt_lwt_topic, mqtt_lwt_topic); 235 | mqtt_changed = true; 236 | cfg.mqtt_ha_is_discovered = 237 | false; // Re-register the device via MQTT HASS Autodiscovery 238 | } 239 | } 240 | 241 | if (settings.containsKey(KEY_MQTT_HA_USE_DISCOVERY)) { 242 | bool mqtt_ha_use_discovery = settings[KEY_MQTT_HA_USE_DISCOVERY]; 243 | if (cfg.mqtt_ha_use_discovery != mqtt_ha_use_discovery) { 244 | cfg.mqtt_ha_use_discovery = mqtt_ha_use_discovery; 245 | 246 | // Reset that light has been discovered already 247 | if (!mqtt_ha_use_discovery) { 248 | cfg.mqtt_ha_is_discovered = false; 249 | } 250 | 251 | mqtt_changed = true; 252 | } 253 | } 254 | 255 | if (settings.containsKey(KEY_MQTT_HA_DISCOVERY_PREFIX)) { 256 | const char *mqtt_ha_disc_prefix = settings[KEY_MQTT_HA_DISCOVERY_PREFIX]; 257 | if (os_strcmp(cfg.mqtt_ha_disc_prefix, mqtt_ha_disc_prefix) != 0) { 258 | os_strcpy(cfg.mqtt_ha_disc_prefix, mqtt_ha_disc_prefix); 259 | mqtt_changed = true; 260 | } 261 | } 262 | 263 | if (settings.containsKey(KEY_WIFI_SSID)) { 264 | const char *wifi_ssid = settings[KEY_WIFI_SSID]; 265 | if (os_strcmp(cfg.wifi_ssid, wifi_ssid) != 0) { 266 | os_strcpy(cfg.wifi_ssid, wifi_ssid); 267 | wifi_changed = true; 268 | } 269 | } 270 | 271 | if (settings.containsKey(KEY_WIFI_PSK)) { 272 | const char *wifi_psk = settings[KEY_WIFI_PSK]; 273 | if (os_strcmp(cfg.wifi_psk, wifi_psk) != 0) { 274 | os_strcpy(cfg.wifi_psk, wifi_psk); 275 | wifi_changed = true; 276 | } 277 | } 278 | 279 | // REST API 280 | if (settings.containsKey(KEY_REST_API_ENABLED)) { 281 | bool rest_api_enabled = settings[KEY_REST_API_ENABLED]; 282 | if (cfg.api != rest_api_enabled) { 283 | cfg.api = rest_api_enabled; 284 | needRestart = true; 285 | } 286 | } 287 | 288 | if (settings.containsKey(KEY_REST_API_KEY)) { 289 | const char *api_key = settings[KEY_REST_API_KEY]; 290 | if (os_strcmp(cfg.api_key, api_key) != 0) { 291 | os_strcpy(cfg.api_key, api_key); 292 | } 293 | } 294 | 295 | if (settings.containsKey(KEY_POWERUP_MODE)) { 296 | uint8_t power_up_mode = (os_strlen(settings[KEY_POWERUP_MODE]) > 0) 297 | ? settings[KEY_POWERUP_MODE] 298 | : POWERUP_MODE; 299 | cfg.powerup_mode = power_up_mode; 300 | } 301 | 302 | // Reconnect to the MQTT broker due to new settings 303 | if (mqtt_changed) { 304 | EEPROM_write(cfg); 305 | mqtt.disconnect(); 306 | } 307 | 308 | // Reconnect to WiFi due to new settings 309 | if (wifi_changed) { 310 | EEPROM_write(cfg); 311 | setupWiFi(); 312 | } 313 | } 314 | 315 | // Process light parameters 316 | if (root.containsKey(KEY_BRIGHTNESS)) { 317 | AiLight->setBrightness(root[KEY_BRIGHTNESS]); 318 | } 319 | 320 | if (root.containsKey(KEY_COLORTEMP)) { 321 | AiLight->setColorTemperature(root[KEY_COLORTEMP]); 322 | } 323 | 324 | if (root.containsKey(KEY_COLOR)) { 325 | AiLight->setColor(root[KEY_COLOR][KEY_COLOR_R], 326 | root[KEY_COLOR][KEY_COLOR_G], 327 | root[KEY_COLOR][KEY_COLOR_B]); 328 | } 329 | 330 | if (root.containsKey(KEY_WHITE)) { 331 | AiLight->setWhite(root[KEY_WHITE]); 332 | } 333 | 334 | if (root.containsKey(KEY_STATE)) { 335 | AiLight->setState( 336 | (os_strcmp(root[KEY_STATE], MQTT_PAYLOAD_ON) == 0) ? true : false); 337 | } 338 | 339 | if (root.containsKey(KEY_GAMMA_CORRECTION)) { 340 | bool gamma = root[KEY_GAMMA_CORRECTION]; 341 | AiLight->useGammaCorrection(gamma); 342 | } 343 | 344 | // Store light parameters for persistence 345 | cfg.is_on = AiLight->getState(); 346 | cfg.brightness = AiLight->getBrightness(); 347 | cfg.color_temp = AiLight->getColorTemperature(); 348 | cfg.color = {AiLight->getColor().red, AiLight->getColor().green, 349 | AiLight->getColor().blue, AiLight->getColor().white}; 350 | cfg.gamma = AiLight->hasGammaCorrection(); 351 | EEPROM_write(cfg); 352 | 353 | if (needRestart) { 354 | ESP.restart(); 355 | } 356 | 357 | if (!settings_changed) { 358 | sendState(); 359 | } 360 | } 361 | 362 | /** 363 | * @brief Bootstrap function setting up the HTTP and WebSocket servers. 364 | */ 365 | void setupWeb() { 366 | server = new AsyncWebServer(80); 367 | 368 | // Setup WebSocket and handle WebSocket events 369 | ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, 370 | AwsEventType type, void *arg, uint8_t *data, size_t len) { 371 | if (type == WS_EVT_CONNECT) { 372 | 373 | #ifdef DEBUG 374 | IPAddress ip = client->remoteIP(); 375 | #endif 376 | 377 | DEBUGLOG("[WEBSOCKET] client #%u connected (IP: %s)\n", client->id(), 378 | ip.toString().c_str()); 379 | 380 | wsStart(client->id()); 381 | } else if (type == WS_EVT_DISCONNECT) { 382 | DEBUGLOG("[WEBSOCKET] client #%u disconnected\n", client->id()); 383 | } else if (type == WS_EVT_ERROR) { 384 | DEBUGLOG("[WEBSOCKET] client #%u error(%u): %s\n", server->url(), 385 | client->id(), *((uint16_t *)arg), (char *)data); 386 | } else if (type == WS_EVT_PONG) { 387 | DEBUGLOG("[WEBSOCKET] #%u pong(%u): %s\n", client->id(), len, 388 | (len) ? (char *)data : ""); 389 | } else if (type == WS_EVT_DATA) { 390 | AwsFrameInfo *info = (AwsFrameInfo *)arg; 391 | static char *message; 392 | 393 | // First packet 394 | if (info->index == 0) { 395 | message = (char *)malloc(info->len); 396 | } 397 | 398 | // Store data 399 | os_memcpy(message + info->index, data, len); 400 | 401 | // Last packet 402 | if (info->index + len == info->len) { 403 | wsProcessMessage(client->id(), message, info->len); 404 | free(message); 405 | } 406 | } 407 | }); 408 | server->addHandler(&ws); 409 | server->addHandler(&events); 410 | 411 | server->rewrite("/", HTTP_ROUTE_INDEX); 412 | 413 | // Send a file when /index is requested 414 | server->on(HTTP_ROUTE_INDEX, HTTP_GET, [](AsyncWebServerRequest *request) { 415 | AsyncWebServerResponse *response = 416 | request->beginResponse_P(200, HTTP_MIMETYPE_HTML, html_gz, html_gz_len); 417 | 418 | response->addHeader(HTTP_HEADER_CONTENT_ENCODING, 419 | HTTP_HEADER_CONTENT_ENCODING_VALUE); 420 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 421 | response->addHeader(HTTP_HEADER_XSS_PROTECTION, 422 | HTTP_HEADER_XSS_PROTECTION_VALUE); 423 | response->addHeader(HTTP_HEADER_CONTENT_TYPE_OPTIONS, 424 | HTTP_HEADER_CONTENT_TYPE_OPTIONS_VALUE); 425 | response->addHeader(HTTP_HEADER_FRAME_OPTIONS, 426 | HTTP_HEADER_FRAME_OPTIONS_VALUE); 427 | 428 | request->send(response); 429 | }); 430 | 431 | if (cfg.api) { 432 | server->onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, 433 | size_t len, size_t index, size_t total) { 434 | // Process requested changes for the light 435 | if (request->url().equals(HTTP_APIROUTE_LIGHT)) { 436 | 437 | // Check for appropriate HTTP method 438 | if (request->method() != HTTP_PATCH) { 439 | AsyncWebServerResponse *response = 440 | request->beginResponse(405, HTTP_MIMETYPE_JSON); 441 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 442 | response->addHeader(HTTP_HEADER_ALLOW, HTTP_HEADER_ALLOW_GET_PATCH); 443 | request->send(response); 444 | } 445 | 446 | if (!authorizeAPI(request)) { 447 | return; 448 | } 449 | 450 | if (!processJson((char *)data)) { 451 | DynamicJsonBuffer jsonBuffer; 452 | JsonObject &root = jsonBuffer.createObject(); 453 | root["error"] = "400"; 454 | root["message"] = "Unable to process the JSON message"; 455 | 456 | char buffer[root.measureLength() + 1]; 457 | root.printTo(buffer, sizeof(buffer)); 458 | 459 | AsyncWebServerResponse *response = 460 | request->beginResponse(400, HTTP_MIMETYPE_JSON, buffer); 461 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 462 | request->send(response); 463 | 464 | return; 465 | } 466 | 467 | sendState(); // Notify subscribers about the new state 468 | 469 | // Send response 470 | DynamicJsonBuffer jsonBuffer; 471 | JsonObject &root = jsonBuffer.createObject(); 472 | createStateJSON(root); 473 | 474 | char buffer[root.measureLength() + 1]; 475 | root.printTo(buffer, sizeof(buffer)); 476 | 477 | AsyncWebServerResponse *response = 478 | request->beginResponse(200, HTTP_MIMETYPE_JSON, buffer); 479 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 480 | request->send(response); 481 | } 482 | }); 483 | } 484 | 485 | if (cfg.api) { 486 | 487 | // 'Light' API Endpoint 488 | server->on( 489 | HTTP_APIROUTE_LIGHT, HTTP_GET, [](AsyncWebServerRequest *request) { 490 | // Check for appropriate HTTP method 491 | if (request->method() != HTTP_GET) { 492 | AsyncWebServerResponse *response = 493 | request->beginResponse(405, HTTP_MIMETYPE_JSON); 494 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 495 | response->addHeader(HTTP_HEADER_ALLOW, HTTP_HEADER_ALLOW_GET_PATCH); 496 | request->send(response); 497 | 498 | return; 499 | } 500 | 501 | if (!authorizeAPI(request)) { 502 | return; 503 | } 504 | 505 | // Send response 506 | DynamicJsonBuffer jsonBuffer; 507 | JsonObject &root = jsonBuffer.createObject(); 508 | createStateJSON(root); 509 | 510 | char buffer[root.measureLength() + 1]; 511 | root.printTo(buffer, sizeof(buffer)); 512 | 513 | AsyncWebServerResponse *response = 514 | request->beginResponse(200, HTTP_MIMETYPE_JSON, buffer); 515 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 516 | request->send(response); 517 | }); 518 | 519 | // 'About' API Endpoint 520 | server->on( 521 | HTTP_APIROUTE_ABOUT, HTTP_ANY, [](AsyncWebServerRequest *request) { 522 | // Only allow HTTP_GET method 523 | if (request->method() != HTTP_GET) { 524 | AsyncWebServerResponse *response = 525 | request->beginResponse(405, HTTP_MIMETYPE_JSON); 526 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 527 | response->addHeader(HTTP_HEADER_ALLOW, HTTP_HEADER_ALLOW_GET); 528 | request->send(response); 529 | 530 | return; 531 | } 532 | 533 | if (!authorizeAPI(request)) { 534 | return; 535 | } 536 | 537 | DynamicJsonBuffer jsonBuffer; 538 | JsonObject &root = jsonBuffer.createObject(); 539 | createAboutJSON(root); 540 | 541 | AsyncResponseStream *response = 542 | request->beginResponseStream(HTTP_MIMETYPE_JSON); 543 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 544 | root.printTo(*response); 545 | 546 | request->send(response); 547 | }); 548 | } 549 | 550 | // Handle unknown URI 551 | server->onNotFound([](AsyncWebServerRequest *request) { 552 | const char *mime_type = HTTP_MIMETYPE_HTML; 553 | 554 | if (cfg.api && request->url().startsWith(HTTP_APIROUTE_ROOT)) { 555 | mime_type = HTTP_MIMETYPE_JSON; 556 | } 557 | 558 | AsyncWebServerResponse *response = request->beginResponse(404, mime_type); 559 | response->addHeader(HTTP_HEADER_SERVER, SERVER_SIGNATURE); 560 | request->send(response); 561 | }); 562 | 563 | server->begin(); 564 | DEBUGLOG("[HTTP] Server started\n"); 565 | } 566 | -------------------------------------------------------------------------------- /src/_wifi.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware - WiFi Module 3 | * 4 | * The WiFi module holds all the code to manage all functions for setting up the 5 | * WiFi connection. 6 | * 7 | * This file is part of the AiLight Firmware. 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * Created by Sacha Telgenhof 12 | * (https://www.sachatelgenhof.com) 13 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 14 | */ 15 | 16 | /** 17 | * @brief Event Handler when an IP address has been assigned 18 | * 19 | * @param event WiFiEventStationModeGotIP Event 20 | */ 21 | void onSTAGotIP(const WiFiEventStationModeGotIP &event) { 22 | 23 | #ifdef DEBUG 24 | const char *const PHY_MODE_NAMES[]{"", "B", "G", "N"}; 25 | 26 | DEBUGLOG("[WIFI] SSID : %s\n", WiFi.SSID().c_str()); 27 | DEBUGLOG("[WIFI] IP Address : %s\n", WiFi.localIP().toString().c_str()); 28 | DEBUGLOG("[WIFI] MAC Address : %s\n", WiFi.macAddress().c_str()); 29 | DEBUGLOG("[WIFI] Gateway : %s\n", WiFi.gatewayIP().toString().c_str()); 30 | DEBUGLOG("[WIFI] DNS : %s\n", WiFi.dnsIP().toString().c_str()); 31 | DEBUGLOG("[WIFI] Subnet Mask : %s\n", WiFi.subnetMask().toString().c_str()); 32 | DEBUGLOG("[WIFI] Host : %s\n", WiFi.hostname().c_str()); 33 | DEBUGLOG("[WIFI] Channel : %d\n", WiFi.channel()); 34 | DEBUGLOG("[WIFI] PHY Mode : %s\n", PHY_MODE_NAMES[WiFi.getPhyMode()]); 35 | DEBUGLOG("[WIFI] Oper. Mode : STA\n"); 36 | DEBUGLOG("\n"); 37 | #endif 38 | 39 | mqttConnect(); 40 | } 41 | 42 | /** 43 | * @brief Event Handler when client connects when in AP Mode 44 | * 45 | * @param event WiFiEventSoftAPModeStationConnected Event 46 | */ 47 | void onAPConnected(const WiFiEventSoftAPModeStationConnected &event) { 48 | DEBUGLOG("[WIFI] SSID : %s\n", cfg.hostname); 49 | DEBUGLOG("[WIFI] Password : %s\n", ADMIN_PASSWORD); 50 | DEBUGLOG("[WIFI] IP Address : %s\n", WiFi.softAPIP().toString().c_str()); 51 | DEBUGLOG("[WIFI] MAC Address : %s\n", WiFi.softAPmacAddress().c_str()); 52 | DEBUGLOG("[WIFI] Oper. Mode : AP\n"); 53 | DEBUGLOG("\n"); 54 | } 55 | 56 | /** 57 | * @brief Event Handler when WiFi is disconnected 58 | * 59 | * @param event WiFiEventStationModeDisconnected Event 60 | */ 61 | void onSTADisconnected(const WiFiEventStationModeDisconnected &event) { 62 | DEBUGLOG("WiFi connection (%s) dropped.\n", event.ssid.c_str()); 63 | DEBUGLOG("Reason: %d\n", event.reason); 64 | 65 | DEBUGLOG("Trying to reconnect\n"); 66 | mqttReconnectTimer 67 | .detach(); // Ensure not to reconnect to MQTT while reconnecting to WiFi 68 | wifiReconnectTimer.once(WIFI_RECONNECT_TIMEOUT, setupWiFi); 69 | } 70 | 71 | /** 72 | * @brief Bootstrap function for the WiFi connection 73 | */ 74 | void setupWiFi() { 75 | static WiFiEventHandler gotIpEventHandler, apConnectedEventHandler, 76 | disconnectedEventHandler; 77 | gotIpEventHandler = WiFi.onStationModeGotIP(&onSTAGotIP); 78 | apConnectedEventHandler = WiFi.onSoftAPModeStationConnected(&onAPConnected); 79 | disconnectedEventHandler = WiFi.onStationModeDisconnected(&onSTADisconnected); 80 | 81 | // Set WiFi hostname 82 | if (os_strlen(cfg.hostname) == 0) { 83 | os_strcpy(cfg.hostname, getDeviceID()); 84 | EEPROM_write(cfg); 85 | } 86 | WiFi.hostname(cfg.hostname); 87 | 88 | // Set WiFi module to STA mode and set Power Output 89 | if (WiFi.getMode() != WIFI_STA) { 90 | WiFi.mode(WIFI_STA); 91 | WiFi.setOutputPower(WIFI_OUTPUT_POWER); 92 | delay(10); 93 | } 94 | 95 | // (Re)connect 96 | WiFi.disconnect(); 97 | DEBUGLOG("[WIFI] Connecting to %s\n", cfg.wifi_ssid); 98 | WiFi.begin(cfg.wifi_ssid, cfg.wifi_psk); 99 | 100 | MDNS.addService("http", "tcp", 80); 101 | 102 | // Check connection and switch to AP mode if no connection 103 | if (WiFi.waitForConnectResult() != WL_CONNECTED) { 104 | DEBUGLOG("[WIFI] Connection not established! Changing into AP mode...\n"); 105 | 106 | wifiReconnectTimer.detach(); // Ensure not to reconnect to WiFi while 107 | // changing into AP mode 108 | 109 | WiFi.mode(WIFI_AP); 110 | delay(10); 111 | 112 | WiFi.softAP(cfg.hostname, ADMIN_PASSWORD); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/config.example.h: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware - Configuration 3 | * 4 | * This file is part of the AiLight Firmware. 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | * 8 | * Created by Sacha Telgenhof 9 | * (https://www.sachatelgenhof.com) 10 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 11 | */ 12 | 13 | /** 14 | * Light 15 | * --------------------------- 16 | * Use the below variables to set the default behaviour of your Smart Light. 17 | * These will be used as the factory defaults of your device. 18 | */ 19 | #define LIGHT_STATE false 20 | #define LIGHT_BRIGHTNESS 0 21 | #define LIGHT_COLOR_TEMPERATURE 0 22 | #define LIGHT_COLOR_RED 64 23 | #define LIGHT_COLOR_GREEN 64 24 | #define LIGHT_COLOR_BLUE 64 25 | #define LIGHT_COLOR_WHITE 0 26 | 27 | #define HOSTNAME "AiLight" 28 | #define ADMIN_PASSWORD "hinotori" 29 | 30 | #define POWERUP_MODE POWERUP_OFF 31 | 32 | /** 33 | * LedDriver 34 | * -------------------------- 35 | * Define type and number of chips used. Allow firmware use with multiple 36 | * types/designs of lights 37 | */ 38 | #define MY92XX_TYPE MY92XX_MODEL_MY9291 39 | #define MY92XX_COUNT 1 40 | 41 | /** 42 | * OTA (Over The Air) Updates 43 | * --------------------------- 44 | */ 45 | #define OTA_PORT 8266 46 | 47 | /** 48 | * WiFi 49 | * --------------------------- 50 | * Use the below variables to set the default WiFi settings of your Smart Light. 51 | * These will be used as the factory defaults of your device. If no 52 | * SSID/PSK are provided, your Smart Light will start in AP mode. 53 | */ 54 | #define WIFI_SSID "" 55 | #define WIFI_PSK "" 56 | #define WIFI_OUTPUT_POWER 1.0 // 20.5 is the maximum output power 57 | 58 | /** 59 | * Timeout period for the device to keep trying to (re)connect to the 60 | * configured WiFi Access Point. If this timeout period has been reached, the 61 | * device will assume a WiFi connection can not be made and will switch to 62 | * Soft AP mode. 63 | #define WIFI_RECONNECT_TIMEOUT 60 // Timeout (in seconds) 64 | 65 | /** 66 | * MQTT 67 | * --------------------------- 68 | * Use the below variables to set the default MQTT settings of your Smart Light. 69 | * These will be used as the factory defaults of your device and 70 | * making the connection to your Home Assistant instance. Most of these settings 71 | * can also be changed in the UI environment. 72 | */ 73 | #define MQTT_PORT 1883 74 | #define MQTT_SERVER "" 75 | #define MQTT_USER "" 76 | #define MQTT_PASSWORD "" 77 | #define MQTT_RECONNECT_TIME 10000 78 | #define MQTT_QOS_LEVEL 0 79 | #define MQTT_RETAIN false 80 | #define MQTT_KEEPALIVE 30 81 | 82 | #define MQTT_PAYLOAD_ON "ON" 83 | #define MQTT_PAYLOAD_OFF "OFF" 84 | 85 | #define MQTT_STATUS_ONLINE "online" 86 | #define MQTT_STATUS_OFFLINE "offline" 87 | 88 | #define MQTT_HOMEASSISTANT_DISCOVERY_ENABLED false 89 | #define MQTT_HOMEASSISTANT_DISCOVERY_PREFIX "homeassistant" 90 | 91 | /** 92 | * Home Assistant 0.84 removed the "mqtt_json" platform type, replacing it with 93 | * a combination of "platform: mqtt" and "schema: json". If you are using 94 | * version 0.84 or older of Home Assistant and using the MQTT discovery feature, 95 | * set the following directive to "true" 96 | */ 97 | #define MQTT_HOMEASSISTANT_DISCOVERY_PRE_0_84 false 98 | 99 | /** 100 | * HTTP 101 | * --------------------------- 102 | * Use the below variables to set the default HTTP settings of your Smart Light. 103 | * These will be used as the factory defaults of your device. 104 | */ 105 | #define REST_API_ENABLED false 106 | 107 | /** 108 | * OpenHAB support 109 | * --------------------------- 110 | * To enable support for openHAB uncomment MQTT_OPENHAB_SUPPORT 111 | * To change the used JSON key modify KEY_COLOR_ARRAY (default: "color_array") 112 | */ 113 | //#define MQTT_OPENHAB_SUPPORT 114 | //#define KEY_COLOR_ARRAY "color_array" 115 | -------------------------------------------------------------------------------- /src/light.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware - Light Module 3 | * 4 | * The Light module contains all the code to process incoming commands and set 5 | * the light attributes (RGBW, brightness, etc.) accordingly. 6 | * 7 | * This file is part of the AiLight Firmware. 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * Created by Sacha Telgenhof 12 | * (https://www.sachatelgenhof.com) 13 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 14 | */ 15 | 16 | /** 17 | * @brief Handle the various MQTT Events (Connect, Disconnect, etc.) 18 | * 19 | * @param type the MQTT event type (e.g. 'connect', 'message', etc.) 20 | * @param topic the MQTT topic to which the message has been published 21 | * @param payload the contents/message that has been published 22 | */ 23 | void deviceMQTTCallback(uint8_t type, const char *topic, const char *payload) { 24 | 25 | // Handling the event of connecting to the MQTT broker 26 | if (type == MQTT_EVENT_CONNECT) { 27 | mqttSubscribe(cfg.mqtt_command_topic); 28 | mqttPublish(cfg.mqtt_lwt_topic, MQTT_STATUS_ONLINE); 29 | 30 | // MQTT discovery for Home Assistant 31 | if (cfg.mqtt_ha_use_discovery && !cfg.mqtt_ha_is_discovered) { 32 | static const int BUFFER_SIZE = 33 | JSON_OBJECT_SIZE(9) + 128; // '128' is an arbritrary number. Increase 34 | // if required by the payload 35 | StaticJsonBuffer mqttJsonBuffer; 36 | JsonObject &md_root = mqttJsonBuffer.createObject(); 37 | 38 | md_root["name"] = cfg.hostname; 39 | #ifdef MQTT_HOMEASSISTANT_DISCOVERY_PRE_0_84 40 | md_root["platform"] = "mqtt_json"; 41 | #else 42 | md_root["platform"] = "mqtt"; 43 | md_root["schema"] = "json"; 44 | #endif 45 | md_root["state_topic"] = cfg.mqtt_state_topic; 46 | md_root["command_topic"] = cfg.mqtt_command_topic; 47 | md_root["rgb"] = true; 48 | md_root[KEY_COLORTEMP] = true; 49 | md_root[KEY_BRIGHTNESS] = true; 50 | md_root[KEY_WHITE] = true; 51 | md_root["availability_topic"] = cfg.mqtt_lwt_topic; 52 | 53 | // Build the payload 54 | char md_buffer[md_root.measureLength() + 1]; 55 | md_root.printTo(md_buffer, sizeof(md_buffer)); 56 | 57 | // Construct the topic name for HA MQTT discovery 58 | char *dc_topic = new char[128]; 59 | sprintf_P(dc_topic, PSTR("%s/light/%s/config"), cfg.mqtt_ha_disc_prefix, 60 | cfg.hostname); 61 | 62 | mqttPublish(dc_topic, md_buffer); 63 | 64 | cfg.mqtt_ha_is_discovered = true; 65 | EEPROM_write(cfg); 66 | } 67 | } 68 | 69 | // Handling the event of disconnecting from the MQTT broker 70 | if (type == MQTT_EVENT_DISCONNECT) { 71 | mqttUnsubscribe(cfg.mqtt_command_topic); 72 | } 73 | 74 | // Handling the event a message is received from the MQTT broker 75 | if (type == MQTT_EVENT_MESSAGE) { 76 | 77 | // Listen to this lights' MQTT command topic 78 | if (os_strcmp(topic, cfg.mqtt_command_topic) == 0) { 79 | 80 | // Convert payload into char variable 81 | uint8_t length = os_strlen(payload); 82 | char message[length + 1]; 83 | os_memcpy(message, payload, length); 84 | message[length] = 0; 85 | 86 | if (!processJson(message)) { 87 | return; 88 | } 89 | 90 | // Store light parameters for persistence 91 | cfg.is_on = AiLight->getState(); 92 | cfg.brightness = AiLight->getBrightness(); 93 | cfg.color_temp = AiLight->getColorTemperature(); 94 | cfg.color = {AiLight->getColor().red, AiLight->getColor().green, 95 | AiLight->getColor().blue, AiLight->getColor().white}; 96 | 97 | EEPROM_write(cfg); 98 | sendState(); // Notify subscribers about the new state 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * @brief Publish the current state of the AiLight 105 | */ 106 | void sendState() { 107 | StaticJsonBuffer jsonBuffer; 108 | JsonObject &root = jsonBuffer.createObject(); 109 | 110 | createStateJSON(root); 111 | 112 | char buffer[root.measureLength() + 1]; 113 | root.printTo(buffer, sizeof(buffer)); 114 | 115 | mqttPublish(cfg.mqtt_state_topic, buffer); // Notify all MQTT subscribers 116 | ws.textAll(buffer); // Notify all WebSocket clients 117 | } 118 | 119 | /** 120 | * @brief Process the received JSON payload 121 | */ 122 | bool processJson(char *message) { 123 | StaticJsonBuffer jsonBuffer; 124 | JsonObject &root = jsonBuffer.parseObject(message); 125 | 126 | if (!root.success()) { 127 | DEBUGLOG("[LIGHT] Unable to parse message\n"); 128 | return false; 129 | } 130 | 131 | // Flash 132 | if (root.containsKey(KEY_FLASH)) { 133 | 134 | // Save current settings to be restored later 135 | currentColor = AiLight->getColor(); 136 | currentBrightness = AiLight->getBrightness(); 137 | currentState = AiLight->getState(); 138 | 139 | flashLength = (uint16_t)root[KEY_FLASH] * 1000U; 140 | 141 | flashBrightness = (root.containsKey(KEY_BRIGHTNESS)) ? root[KEY_BRIGHTNESS] 142 | : currentBrightness; 143 | 144 | if (root.containsKey(KEY_COLOR)) { 145 | flashColor = {root[KEY_COLOR][KEY_COLOR_R], root[KEY_COLOR][KEY_COLOR_G], 146 | root[KEY_COLOR][KEY_COLOR_B]}; 147 | } else { 148 | flashColor = {currentColor.red, currentColor.green, currentColor.blue}; 149 | } 150 | 151 | flashColor.red = map(flashColor.red, 0, 255, 0, flashBrightness); 152 | flashColor.green = map(flashColor.green, 0, 255, 0, flashBrightness); 153 | flashColor.blue = map(flashColor.blue, 0, 255, 0, flashBrightness); 154 | 155 | flash = true; 156 | startFlash = true; 157 | } else { 158 | flash = false; 159 | } 160 | 161 | if (root.containsKey(KEY_TRANSITION)) { 162 | transitionTime = root[KEY_TRANSITION]; // Time in seconds 163 | startTransTime = millis(); 164 | } else { 165 | transitionTime = 0; 166 | } 167 | 168 | if (root.containsKey(KEY_BRIGHTNESS)) { 169 | 170 | // In transition/fade 171 | if (transitionTime > 0) { 172 | transBrightness = root[KEY_BRIGHTNESS]; 173 | 174 | // If light is off, start fading from Zero 175 | if (!AiLight->getState()) { 176 | AiLight->setBrightness(0); 177 | } 178 | 179 | stepBrightness = calculateStep(AiLight->getBrightness(), transBrightness); 180 | stepCount = 0; 181 | } else { 182 | AiLight->setBrightness(root[KEY_BRIGHTNESS]); 183 | } 184 | } 185 | 186 | if (root.containsKey(KEY_COLOR)) { 187 | 188 | // In transition/fade 189 | if (transitionTime > 0) { 190 | transColor.red = root[KEY_COLOR][KEY_COLOR_R]; 191 | transColor.green = root[KEY_COLOR][KEY_COLOR_G]; 192 | transColor.blue = root[KEY_COLOR][KEY_COLOR_B]; 193 | 194 | // If light is off, start fading from Zero 195 | if (!AiLight->getState()) { 196 | AiLight->setColor(0, 0, 0); 197 | } 198 | 199 | stepR = calculateStep(AiLight->getColor().red, transColor.red); 200 | stepG = calculateStep(AiLight->getColor().green, transColor.green); 201 | stepB = calculateStep(AiLight->getColor().blue, transColor.blue); 202 | 203 | stepCount = 0; 204 | } else { 205 | AiLight->setColor(root[KEY_COLOR][KEY_COLOR_R], 206 | root[KEY_COLOR][KEY_COLOR_G], 207 | root[KEY_COLOR][KEY_COLOR_B]); 208 | } 209 | } 210 | 211 | #ifdef MQTT_OPENHAB_SUPPORT 212 | if (root.containsKey(KEY_COLOR_ARRAY)) { 213 | 214 | // In transition/fade 215 | if (transitionTime > 0) { 216 | transColor.red = root[KEY_COLOR_ARRAY][0]; 217 | transColor.green = root[KEY_COLOR_ARRAY][1]; 218 | transColor.blue = root[KEY_COLOR_ARRAY][2]; 219 | 220 | // If light is off, start fading from Zero 221 | if (!AiLight->getState()) { 222 | AiLight->setColor(0, 0, 0); 223 | } 224 | 225 | stepR = calculateStep(AiLight->getColor().red, transColor.red); 226 | stepG = calculateStep(AiLight->getColor().green, transColor.green); 227 | stepB = calculateStep(AiLight->getColor().blue, transColor.blue); 228 | 229 | stepCount = 0; 230 | } else { 231 | AiLight->setColor(root[KEY_COLOR_ARRAY][0], 232 | root[KEY_COLOR_ARRAY][1], 233 | root[KEY_COLOR_ARRAY][2]); 234 | } 235 | } 236 | #endif 237 | 238 | if (root.containsKey(KEY_WHITE)) { 239 | // In transition/fade 240 | if (transitionTime > 0) { 241 | transColor.white = root[KEY_WHITE]; 242 | 243 | // If light is off, start fading from Zero 244 | if (!AiLight->getState()) { 245 | AiLight->setWhite(0); 246 | } 247 | 248 | stepW = calculateStep(AiLight->getColor().white, transColor.white); 249 | 250 | stepCount = 0; 251 | } else { 252 | AiLight->setWhite(root[KEY_WHITE]); 253 | } 254 | } 255 | 256 | if (root.containsKey(KEY_COLORTEMP)) { 257 | // In transition/fade 258 | if (transitionTime > 0) { 259 | transColor = AiLight->colorTemperature2RGB(root[KEY_COLORTEMP]); 260 | 261 | // If light is off, start fading from Zero 262 | if (!AiLight->getState()) { 263 | AiLight->setColor(0, 0, 0); 264 | } 265 | 266 | stepR = calculateStep(AiLight->getColor().red, transColor.red); 267 | stepG = calculateStep(AiLight->getColor().green, transColor.green); 268 | stepB = calculateStep(AiLight->getColor().blue, transColor.blue); 269 | 270 | stepCount = 0; 271 | } else { 272 | AiLight->setColorTemperature(root[KEY_COLORTEMP]); 273 | } 274 | } 275 | 276 | if (root.containsKey(KEY_STATE)) { 277 | state = (os_strcmp(root[KEY_STATE], MQTT_PAYLOAD_ON) == 0) ? true : false; 278 | 279 | if (transitionTime > 0 && !state) { 280 | transColor.red = 0; 281 | transColor.green = 0; 282 | transColor.blue = 0; 283 | 284 | stepR = calculateStep(AiLight->getColor().red, transColor.red); 285 | stepG = calculateStep(AiLight->getColor().green, transColor.green); 286 | stepB = calculateStep(AiLight->getColor().blue, transColor.blue); 287 | } else { 288 | AiLight->setState(state); 289 | } 290 | } 291 | 292 | if (root.containsKey(KEY_GAMMA_CORRECTION)) { 293 | bool use_gamma_correction = root[KEY_GAMMA_CORRECTION]; 294 | AiLight->useGammaCorrection(use_gamma_correction); 295 | } 296 | 297 | return true; 298 | } 299 | 300 | /** 301 | * @brief Populate the given JsonObject with details about this firmware 302 | * 303 | * @param object the JsonObject that will hold details about this firmware 304 | * 305 | * @return void 306 | */ 307 | void createAboutJSON(JsonObject &object) { 308 | object["app_name"] = APP_NAME; 309 | object["app_version"] = APP_VERSION; 310 | object["build_date"] = __DATE__; 311 | object["build_time"] = __TIME__; 312 | object["memory"] = ESP.getFlashChipSize(); 313 | object["free_heap"] = ESP.getFreeHeap(); 314 | object["cpu_frequency"] = ESP.getCpuFreqMHz(); 315 | object["led_driver"] = led_driver_table[cfg.chip_type]; 316 | object["device_ip"] = (WiFi.getMode() == WIFI_AP) ? WiFi.softAPIP().toString() 317 | : WiFi.localIP().toString(); 318 | object["mac"] = WiFi.macAddress(); 319 | 320 | object["core"] = getESPCoreVersion(); 321 | } 322 | 323 | /** 324 | * @brief Populate the given JsonObject with the current state of this light 325 | * 326 | * @param object the JsonObject that will hold the current state of this light 327 | */ 328 | void createStateJSON(JsonObject &object) { 329 | object[KEY_STATE] = AiLight->getState() ? MQTT_PAYLOAD_ON : MQTT_PAYLOAD_OFF; 330 | object[KEY_BRIGHTNESS] = AiLight->getBrightness(); 331 | object[KEY_WHITE] = AiLight->getColor().white; 332 | object[KEY_COLORTEMP] = AiLight->getColorTemperature(); 333 | 334 | JsonObject &color = object.createNestedObject(KEY_COLOR); 335 | color[KEY_COLOR_R] = AiLight->getColor().red; 336 | color[KEY_COLOR_G] = AiLight->getColor().green; 337 | color[KEY_COLOR_B] = AiLight->getColor().blue; 338 | 339 | #ifdef MQTT_OPENHAB_SUPPORT 340 | JsonArray &color_array = object.createNestedArray(KEY_COLOR_ARRAY); 341 | color_array.add(AiLight->getColor().red); 342 | color_array.add(AiLight->getColor().green); 343 | color_array.add(AiLight->getColor().blue); 344 | #endif 345 | 346 | object[KEY_GAMMA_CORRECTION] = AiLight->hasGammaCorrection(); 347 | } 348 | 349 | /** 350 | * @brief Bootstrap function for the RGBW light 351 | */ 352 | void setupLight() { 353 | 354 | // Restore last used settings (Note: set colour temperature first as it 355 | // changed the RGB channels!) 356 | AiLight->setColorTemperature(cfg.color_temp); 357 | AiLight->setColor(cfg.color.red, cfg.color.green, cfg.color.blue); 358 | AiLight->setWhite(cfg.color.white); 359 | AiLight->setBrightness(cfg.brightness); 360 | AiLight->useGammaCorrection(cfg.gamma); 361 | 362 | switch (cfg.powerup_mode) { 363 | case POWERUP_ON: 364 | AiLight->setState(true); 365 | break; 366 | case POWERUP_SAME: 367 | AiLight->setState(cfg.is_on); 368 | break; 369 | case POWERUP_OFF: 370 | default: 371 | AiLight->setState(false); 372 | break; 373 | } 374 | 375 | mqttRegister(deviceMQTTCallback); 376 | } 377 | 378 | /** 379 | * @brief Process requests and keep on running... 380 | */ 381 | void loopLight() { 382 | 383 | // Flashing 384 | if (flash) { 385 | if (startFlash) { 386 | startFlash = false; 387 | flashStartTime = millis(); 388 | AiLight->setState(false); 389 | } 390 | 391 | // Run the flash sequence for the defined period. 392 | if ((millis() - flashStartTime) <= (flashLength - 100U)) { 393 | if ((millis() - flashStartTime) % 1000 <= 500) { 394 | AiLight->setColor(flashColor.red, flashColor.green, flashColor.blue); 395 | AiLight->setBrightness(flashBrightness); 396 | AiLight->setState(true); 397 | } else { 398 | AiLight->setState(false); 399 | } 400 | } else { 401 | // Return to the state before the flash 402 | flash = false; 403 | 404 | AiLight->setState(currentState); 405 | AiLight->setColor(currentColor.red, currentColor.green, 406 | currentColor.blue); 407 | AiLight->setBrightness(currentBrightness); 408 | 409 | sendState(); // Notify subscribers again about current state 410 | } 411 | } 412 | 413 | // Transitioning/Fading 414 | if (transitionTime > 0) { 415 | AiLight->setState(true); 416 | 417 | uint32_t currentTransTime = millis(); 418 | 419 | // Cross fade the RGBW channels every millisecond 420 | if (currentTransTime - startTransTime > transitionTime) { 421 | if (stepCount <= 1000) { 422 | startTransTime = currentTransTime; 423 | 424 | // Transition/fade RGB LEDS (if level is different from current) 425 | if (stepR != 0 || stepG != 0 || stepB != 0) { 426 | AiLight->setColor(calculateLevel(stepR, AiLight->getColor().red, 427 | stepCount, transColor.red), 428 | calculateLevel(stepG, AiLight->getColor().green, 429 | stepCount, transColor.green), 430 | calculateLevel(stepB, AiLight->getColor().blue, 431 | stepCount, transColor.blue)); 432 | } 433 | 434 | // Transition/fade white LEDS (if level is different from current) 435 | if (stepW != 0) { 436 | AiLight->setWhite(calculateLevel(stepW, AiLight->getColor().white, 437 | stepCount, transColor.white)); 438 | } 439 | 440 | // Transition/fade brightness (if level is different from current) 441 | if (stepBrightness != 0) { 442 | AiLight->setBrightness(calculateLevel(stepBrightness, 443 | AiLight->getBrightness(), 444 | stepCount, transBrightness)); 445 | } 446 | 447 | stepCount++; 448 | } else { 449 | transitionTime = 0; 450 | stepCount = 0; 451 | AiLight->setState(state); 452 | 453 | sendState(); // Notify subscribers again about current state 454 | 455 | // Update settings 456 | cfg.is_on = AiLight->getState(); 457 | cfg.brightness = AiLight->getBrightness(); 458 | cfg.color = {AiLight->getColor().red, AiLight->getColor().green, 459 | AiLight->getColor().blue, AiLight->getColor().white}; 460 | EEPROM_write(cfg); 461 | } 462 | } 463 | } 464 | } 465 | 466 | /** 467 | * @brief Determines the step needed to change to the target value 468 | * 469 | * @param currentLevel the current level 470 | * @param targetLevel the target level 471 | * 472 | * @return the step value needed to change to the target value 473 | */ 474 | int16_t calculateStep(uint8_t currentLevel, uint8_t targetLevel) { 475 | int16_t step = targetLevel - currentLevel; 476 | if (step) { 477 | step = 1000 / step; 478 | } 479 | 480 | return step; 481 | } 482 | 483 | /** 484 | * @brief Calculates the next level of a channel (RGBW/Brightness) 485 | * 486 | * @param step the step needed for changing to the target value 487 | * @param val the current value in the transitioning loop 488 | * @param i the current index in the transitioning loop 489 | * @param targetLevel the target level 490 | * 491 | * @return the next level of a channel (RGBW/Brightness) 492 | */ 493 | uint8_t calculateLevel(int step, int val, uint16_t i, uint8_t targetLevel) { 494 | if ((step) && i % step == 0) { 495 | if (step > 0) { 496 | val++; 497 | 498 | // Prevent overshooting the target level 499 | if (val > targetLevel) { 500 | val = targetLevel; 501 | } 502 | } else if (step < 0) { 503 | val--; 504 | 505 | // Prevent undershooting the target level 506 | if (val < targetLevel) { 507 | val = targetLevel; 508 | } 509 | } 510 | } 511 | 512 | val = constrain(val, 0, MY92XX_LEVEL_MAX); // Force boundaries 513 | 514 | return val; 515 | } 516 | -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware 3 | * 4 | * This file is part of the AiLight Firmware. 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | * 8 | * Created by Sacha Telgenhof 9 | * (https://www.sachatelgenhof.com) 10 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 11 | */ 12 | 13 | #define APP_NAME "AiLight" 14 | #define APP_VERSION "1.0.0" 15 | #define APP_AUTHOR "me@sachatelgenhof.com" 16 | 17 | #define DEVICE_MODEL "RGBW Light" 18 | 19 | // Power Up Modes 20 | #define POWERUP_OFF 0 21 | #define POWERUP_ON 1 22 | #define POWERUP_SAME 2 23 | 24 | #include "config.h" 25 | 26 | // Fallback 27 | #ifndef MQTT_HOMEASSISTANT_DISCOVERY_ENABLED 28 | #define MQTT_HOMEASSISTANT_DISCOVERY_ENABLED false 29 | #endif 30 | 31 | #ifndef MQTT_HOMEASSISTANT_DISCOVERY_PREFIX 32 | #define MQTT_HOMEASSISTANT_DISCOVERY_PREFIX "homeassistant" 33 | #endif 34 | 35 | #ifndef MQTT_HOMEASSISTANT_DISCOVERY_PRE_0_84 36 | #define MQTT_HOMEASSISTANT_DISCOVERY_PRE_0_84 false 37 | #endif 38 | 39 | #ifndef KEY_COLOR_ARRAY 40 | #define KEY_COLOR_ARRAY "color_array" 41 | #endif 42 | 43 | #ifndef REST_API_ENABLED 44 | #define REST_API_ENABLED false 45 | #endif 46 | 47 | #ifndef POWERUP_MODE 48 | #define POWERUP_MODE POWERUP_OFF 49 | #endif 50 | 51 | #ifndef WIFI_RECONNECT_TIMEOUT 52 | #define WIFI_RECONNECT_TIMEOUT 10 53 | #endif 54 | 55 | #include "AiLight.hpp" 56 | #include "ArduinoOTA.h" 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | #include 66 | #include 67 | #include 68 | 69 | extern "C" { 70 | #include "spi_flash.h" 71 | } 72 | 73 | #include "html.gz.h" 74 | 75 | #define EEPROM_START_ADDRESS 0 76 | #define INIT_HASH 0x5F 77 | #ifndef MQTT_OPENHAB_ENABLED 78 | static const int BUFFER_SIZE = JSON_OBJECT_SIZE(10); 79 | #else 80 | static const int BUFFER_SIZE = JSON_OBJECT_SIZE(13); 81 | #endif 82 | 83 | // Key names as used internally and in the WebUI 84 | #define KEY_SETTINGS "s" 85 | #define KEY_DEVICE "d" 86 | 87 | #define KEY_STATE "state" 88 | #define KEY_BRIGHTNESS "brightness" 89 | #define KEY_WHITE "white_value" 90 | #define KEY_COLORTEMP "color_temp" 91 | #define KEY_FLASH "flash" 92 | #define KEY_COLOR "color" 93 | #define KEY_COLOR_R "r" 94 | #define KEY_COLOR_G "g" 95 | #define KEY_COLOR_B "b" 96 | #define KEY_GAMMA_CORRECTION "gamma" 97 | #define KEY_TRANSITION "transition" 98 | 99 | #define KEY_HOSTNAME "hostname" 100 | #define KEY_WIFI_SSID "wifi_ssid" 101 | #define KEY_WIFI_PSK "wifi_psk" 102 | #define KEY_MQTT_SERVER "mqtt_server" 103 | #define KEY_MQTT_PORT "mqtt_port" 104 | #define KEY_MQTT_USER "mqtt_user" 105 | #define KEY_MQTT_PASSWORD "mqtt_password" 106 | #define KEY_MQTT_STATE_TOPIC "mqtt_state_topic" 107 | #define KEY_MQTT_COMMAND_TOPIC "mqtt_command_topic" 108 | #define KEY_MQTT_LWT_TOPIC "mqtt_lwt_topic" 109 | #define KEY_MQTT_HA_USE_DISCOVERY "switch_ha_discovery" 110 | #define KEY_MQTT_HA_IS_DISCOVERED "mqtt_ha_is_discovered" 111 | #define KEY_MQTT_HA_DISCOVERY_PREFIX "mqtt_ha_discovery_prefix" 112 | #define KEY_REST_API_ENABLED "switch_rest_api" 113 | #define KEY_REST_API_KEY "api_key" 114 | #define KEY_POWERUP_MODE "powerup_mode" 115 | 116 | // MQTT Event type definitions 117 | #define MQTT_EVENT_CONNECT 0 118 | #define MQTT_EVENT_DISCONNECT 1 119 | #define MQTT_EVENT_MESSAGE 2 120 | 121 | // HTTP 122 | #define HTTP_WEB_INDEX "index.html" 123 | #define HTTP_API_ROOT "api" 124 | #define HTTP_HEADER_APIKEY "API-Key" 125 | #define HTTP_HEADER_SERVER "Server" 126 | #define HTTP_HEADER_CONTENTTYPE "Content-Type" 127 | #define HTTP_HEADER_ALLOW "Allow" 128 | #define HTTP_MIMETYPE_HTML "text/html" 129 | #define HTTP_MIMETYPE_JSON "application/json" 130 | #define HTTP_HEADER_XSS_PROTECTION "X-XSS-Protection" 131 | #define HTTP_HEADER_XSS_PROTECTION_VALUE "1; mode=block" 132 | #define HTTP_HEADER_CONTENT_TYPE_OPTIONS "X-Content-Type-Options" 133 | #define HTTP_HEADER_CONTENT_TYPE_OPTIONS_VALUE "nosniff" 134 | #define HTTP_HEADER_FRAME_OPTIONS "X-Frame-Options" 135 | #define HTTP_HEADER_FRAME_OPTIONS_VALUE "deny" 136 | #define HTTP_HEADER_CONTENT_ENCODING "Content-Encoding" 137 | #define HTTP_HEADER_CONTENT_ENCODING_VALUE "gzip" 138 | #define HTTP_HEADER_ALLOW_GET "GET" 139 | #define HTTP_HEADER_ALLOW_GET_PATCH "GET, PATCH" 140 | 141 | const char *SERVER_SIGNATURE = APP_NAME "/" APP_VERSION; 142 | 143 | const char *HTTP_ROUTE_INDEX = "/" HTTP_WEB_INDEX; 144 | const char *HTTP_APIROUTE_ROOT = "/" HTTP_API_ROOT; 145 | const char *HTTP_APIROUTE_ABOUT = "/" HTTP_API_ROOT "/about"; 146 | const char *HTTP_APIROUTE_LIGHT = "/" HTTP_API_ROOT "/light"; 147 | 148 | AsyncWebSocket ws("/ws"); 149 | AsyncEventSource events("/events"); 150 | AsyncWebServer *server; 151 | AsyncMqttClient mqtt; 152 | std::vector _mqtt_callbacks; 153 | Ticker wifiReconnectTimer; 154 | Ticker mqttReconnectTimer; 155 | 156 | // Configuration structure that gets stored to the EEPROM 157 | struct config_t { 158 | uint8_t ic; // Initialization check 159 | bool is_on; // Operational state (true == on) 160 | uint8_t brightness; // Brightness level 161 | uint8_t color_temp; // Colour temperature 162 | Color color; // RGBW channel levels 163 | uint16_t mqtt_port; // MQTT Broker port 164 | char hostname[128]; // Hostname/Identifier 165 | char wifi_ssid[32]; // WiFi SSID 166 | char wifi_psk[63]; // WiFi Passphrase Key 167 | char mqtt_server[128]; // Server/hostname of the MQTT Broker 168 | char mqtt_user[64]; // Username used for connecting to the MQTT Broker 169 | char mqtt_password[64]; // Password used for connecting to the MQTT Broker 170 | char mqtt_state_topic[128]; // MQTT Topic for publishing the state 171 | char mqtt_command_topic[128]; // MQTT Topic for receiving commands 172 | char mqtt_lwt_topic[128]; // MQTT Topic for publishing Last Will and 173 | // Testament 174 | bool gamma; // Gamma Correction enabled or not 175 | bool mqtt_ha_use_discovery; // Home Assistant MQTT discovery enabled or not 176 | bool mqtt_ha_is_discovered; // Has this device already been discovered or 177 | // not 178 | char mqtt_ha_disc_prefix[32]; // MQTT Discovery prefix for Home Assistant 179 | bool api; // REST API enabled or not 180 | char api_key[32]; // API Key 181 | uint8_t powerup_mode; // Power Up Mode 182 | my92xx_model_t chip_type; // Device Type 183 | uint8_t chip_count; 184 | } cfg; 185 | 186 | AiLightClass *AiLight; 187 | 188 | const char *led_driver_table[2] = {"MY9291", "MY9231"}; 189 | 190 | // Globals for flash 191 | bool flash = false; 192 | bool startFlash = false; 193 | uint16_t flashLength = 0; 194 | uint32_t flashStartTime = 0; 195 | Color flashColor; 196 | uint8_t flashBrightness = 0; 197 | 198 | // Globals for current state 199 | Color currentColor; 200 | uint8_t currentBrightness; 201 | bool currentState; 202 | 203 | // Globals for transition/fade 204 | bool state = false; 205 | uint16_t transitionTime = 0; 206 | uint32_t startTransTime = 0; 207 | int stepR, stepG, stepB, stepW, stepBrightness = 0; 208 | uint16_t stepCount = 0; 209 | Color transColor; 210 | uint8_t transBrightness = 0; 211 | 212 | // Globals for MQTT 213 | bool _mqtt_connecting = false; 214 | 215 | #ifdef DEBUG 216 | #define SerialPrint(format, ...) \ 217 | StreamPrint_progmem(Serial, PSTR(format), ##__VA_ARGS__) 218 | #define StreamPrint(stream, format, ...) \ 219 | StreamPrint_progmem(stream, PSTR(format), ##__VA_ARGS__) 220 | #endif 221 | 222 | #ifdef DEBUG 223 | #define DEBUGLOG(...) SerialPrint(__VA_ARGS__) 224 | #else 225 | #define DEBUGLOG(...) 226 | #endif 227 | 228 | #ifdef DEBUG 229 | /** 230 | * @brief A program memory version of printf 231 | * 232 | * Copy of format string and result share a buffer so as to avoid too much 233 | * memory use. 234 | * 235 | * Credits: David Pankhurst 236 | * Source: http://www.utopiamechanicus.com/article/low-memory-serial-print/ 237 | * 238 | * @param out the output object (e.g. Serial) 239 | * @param format the format string (as used in the printf function and alike) 240 | * @return void 241 | */ 242 | void StreamPrint_progmem(Print &out, PGM_P format, ...) { 243 | char formatString[128], *ptr; 244 | 245 | // Copy in from program mem 246 | strncpy_P(formatString, format, sizeof(formatString)); 247 | 248 | // null terminate - leave last char since we might need it in worst case for 249 | // results \0 250 | formatString[sizeof(formatString) - 2] = '\0'; 251 | ptr = &formatString[os_strlen(formatString) + 1]; // our result buffer... 252 | 253 | va_list args; 254 | va_start(args, format); 255 | vsnprintf(ptr, sizeof(formatString) - 1 - os_strlen(formatString), 256 | formatString, args); 257 | va_end(args); 258 | formatString[sizeof(formatString) - 1] = '\0'; 259 | 260 | out.print(ptr); 261 | } 262 | #endif 263 | 264 | /** 265 | * @brief Template to allow any type of data to be written to the EEPROM 266 | * 267 | * Although this template allows any type of data, it will primarily be used to 268 | * save the config_t struct. 269 | * 270 | * @param value the data to be written to the EEPROM 271 | * 272 | * @return void 273 | */ 274 | template void EEPROM_write(const T &value) { 275 | uint16_t ee = EEPROM_START_ADDRESS; 276 | const byte *p = (const byte *)(const void *)&value; 277 | for (uint16_t i = 0; i < sizeof(value); i++) 278 | EEPROM.write(ee++, *p++); 279 | 280 | EEPROM.commit(); 281 | } 282 | 283 | /** 284 | * @brief Template to allow any type of data to be read from the EEPROM 285 | * 286 | * Although this template allows any type of data, it will primarily be used to 287 | * read the EEPROM contents into the config_t struct. 288 | * 289 | * @param value the data to be loaded into from the EEPROM 290 | * 291 | * @return void 292 | */ 293 | template void EEPROM_read(T &value) { 294 | uint16_t ee = EEPROM_START_ADDRESS; 295 | byte *p = (byte *)(void *)&value; 296 | for (uint16_t i = 0; i < sizeof(value); i++) 297 | *p++ = EEPROM.read(ee++); 298 | } 299 | -------------------------------------------------------------------------------- /src/main.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * AiLight Firmware 3 | * 4 | * This file is part of the AiLight Firmware. 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | * 8 | * Created by Sacha Telgenhof 9 | * (https://www.sachatelgenhof.com) 10 | * Copyright (c) 2016 - 2021 Sacha Telgenhof 11 | */ 12 | 13 | #include "main.h" 14 | 15 | /** 16 | * @brief Determines the ID of this Smart Light 17 | * 18 | * @return the unique identifier of this Smart RGBW Light 19 | */ 20 | const char *getDeviceID() { 21 | char *identifier = new char[30]; 22 | os_strcpy(identifier, HOSTNAME); 23 | strcat_P(identifier, PSTR("-")); 24 | 25 | char cidBuf[7]; 26 | sprintf(cidBuf, "%06X", ESP.getChipId()); 27 | os_strcat(identifier, cidBuf); 28 | 29 | return identifier; 30 | } 31 | 32 | /** 33 | * @brief Retrieves the (formatted) version of the ESP Core framework 34 | * used in this firmware. 35 | * 36 | * @return the (formatted) version of the ESP Core framework 37 | */ 38 | String getESPCoreVersion() { 39 | String version = ESP.getCoreVersion(); 40 | 41 | version.replace("_", "."); 42 | 43 | return version; 44 | } 45 | 46 | /** 47 | * @brief Loads the factory defaults for this Smart Light 48 | * 49 | * If you like to change 'your' factory defaults, please change the appropriate 50 | * settings in your config.h file. 51 | * 52 | * @return void 53 | */ 54 | void loadFactoryDefaults() { 55 | // Clear EEPROM space 56 | for (uint16_t i = 0; i < SPI_FLASH_SEC_SIZE; i++) { 57 | EEPROM.write(i, 0xFF); 58 | } 59 | EEPROM.commit(); 60 | 61 | // Device defaults 62 | cfg.ic = INIT_HASH; 63 | cfg.is_on = LIGHT_STATE; 64 | cfg.brightness = LIGHT_BRIGHTNESS; 65 | cfg.color_temp = LIGHT_COLOR_TEMPERATURE; 66 | cfg.color = {LIGHT_COLOR_RED, LIGHT_COLOR_GREEN, LIGHT_COLOR_BLUE, 67 | LIGHT_COLOR_WHITE}; 68 | cfg.chip_type = MY92XX_TYPE; 69 | cfg.chip_count = MY92XX_COUNT; 70 | 71 | // Configuration defaults 72 | os_strcpy(cfg.hostname, getDeviceID()); 73 | cfg.mqtt_port = MQTT_PORT; 74 | os_strcpy(cfg.mqtt_server, MQTT_SERVER); 75 | os_strcpy(cfg.mqtt_user, MQTT_USER); 76 | os_strcpy(cfg.mqtt_password, MQTT_PASSWORD); 77 | os_strcpy(cfg.mqtt_state_topic, getDeviceID()); 78 | 79 | // MQTT Topics 80 | char *cmd_topic = new char[128]; 81 | sprintf_P(cmd_topic, PSTR("%s/set"), getDeviceID()); 82 | os_strcpy(cfg.mqtt_command_topic, cmd_topic); 83 | 84 | char *lwt_topic = new char[128]; 85 | sprintf_P(lwt_topic, PSTR("%s/status"), getDeviceID()); 86 | os_strcpy(cfg.mqtt_lwt_topic, lwt_topic); 87 | 88 | cfg.mqtt_ha_use_discovery = MQTT_HOMEASSISTANT_DISCOVERY_ENABLED; 89 | cfg.mqtt_ha_is_discovered = false; 90 | os_strcpy(cfg.mqtt_ha_disc_prefix, MQTT_HOMEASSISTANT_DISCOVERY_PREFIX); 91 | 92 | os_strcpy(cfg.wifi_ssid, WIFI_SSID); 93 | os_strcpy(cfg.wifi_psk, WIFI_PSK); 94 | 95 | // REST API 96 | cfg.api = REST_API_ENABLED; 97 | os_strcpy(cfg.api_key, ADMIN_PASSWORD); 98 | 99 | cfg.powerup_mode = POWERUP_MODE; 100 | 101 | EEPROM_write(cfg); 102 | } 103 | 104 | /** 105 | * @brief Bootstrap/Initialization 106 | */ 107 | void setup() { 108 | EEPROM.begin(SPI_FLASH_SEC_SIZE); 109 | EEPROM_read(cfg); 110 | if (cfg.ic != INIT_HASH) { 111 | loadFactoryDefaults(); 112 | } 113 | 114 | // Serial Port Initialization 115 | #ifdef DEBUG 116 | Serial.begin(115200); 117 | DEBUGLOG("\n"); 118 | DEBUGLOG("\n"); 119 | DEBUGLOG("Welcome to %s!\n", APP_NAME); 120 | DEBUGLOG("Firmware Version : %s\n", APP_VERSION); 121 | DEBUGLOG("Firmware Build : %s - %s\n", __DATE__, __TIME__); 122 | DEBUGLOG("ESP Core Version : %s\n", getESPCoreVersion().c_str()); 123 | DEBUGLOG("Device Name : %s\n", cfg.hostname); 124 | DEBUGLOG("LED Driver : %s\n", led_driver_table[cfg.chip_type]); 125 | DEBUGLOG("\n"); 126 | #endif 127 | AiLight = new AiLightClass(cfg.chip_type, cfg.chip_count); 128 | setupLight(); 129 | setupMQTT(); 130 | setupWiFi(); 131 | setupOTA(); 132 | setupWeb(); 133 | 134 | sendState(); // Notify subscribers about current state 135 | } 136 | 137 | /** 138 | * @brief Main loop 139 | */ 140 | void loop() { 141 | loopOTA(); 142 | loopLight(); 143 | } 144 | --------------------------------------------------------------------------------