diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 6f5a5be862..5c87f432c1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -github: [Aircoookie,blazoncek] -custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek'] +github: [] +custom: ['https://www.paypal.com/donate?business=moonmodules@icloud.com'] diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 285ad419e4..937dfa9c50 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -34,18 +34,21 @@ body: id: install_format attributes: label: Install Method - description: How did you install WLED? + description: How did you install MoonModules WLED? options: + - From MoonModules Release Page + - From srg74 firmware repository + - From https://wled-install.github.io/ - Binary from WLED.me - - Self-Compiled + - Self-Compiled or other validations: required: true - type: input id: version attributes: - label: What version of WLED? + label: What version/release of MM WLED? description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message" - placeholder: "e.g. WLED 0.13.1 (build 2203150)" + placeholder: "e.g. build 2401290, WLEDMM_14.5.0-beta_esp32_4MB_M.bin" validations: required: true - type: dropdown @@ -54,12 +57,12 @@ body: label: Which microcontroller/board are you seeing the problem on? multiple: true options: - - ESP8266 - ESP32 - ESP32-S3 - ESP32-S2 - ESP32-C3 - Other + - ESP8266 validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 29a2f1b510..1278009d09 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -7,5 +7,5 @@ contact_links: url: https://wled.discourse.group/ about: For issues and ideas that might need longer discussion. - name: kno.wled.ge base - url: https://kno.wled.ge/basics/faq/ + url: https://mm.kno.wled.ge/basics/faq/ about: Take a look at the frequently asked questions and documentation, perhaps your question is already answered! \ No newline at end of file diff --git a/.github/workflows/wled-ci.yml b/.github/workflows/wled-ci.yml index 2b599e6f66..8a44db719d 100644 --- a/.github/workflows/wled-ci.yml +++ b/.github/workflows/wled-ci.yml @@ -6,17 +6,17 @@ jobs: get_default_envs: name: Gather Environments - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' - name: Install PlatformIO @@ -30,29 +30,29 @@ jobs: build: - name: Build Enviornments - runs-on: ubuntu-latest + name: Builds + runs-on: ubuntu-22.04 needs: get_default_envs strategy: fail-fast: false matrix: environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Cache PlatformIO - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.platformio - key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + key: ${{ runner.os }}-${{ matrix.environment}} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' - name: Install PlatformIO @@ -61,31 +61,31 @@ jobs: env: WLED_RELEASE: True run: pio run -e ${{ matrix.environment }} - - uses: actions/upload-artifact@v2 + - name: Rename Bin + run: mv -v .pio/build/${{ matrix.environment }}/firmware.bin firmware-${{ matrix.environment }}.bin + - uses: actions/upload-artifact@v4 with: name: firmware-${{ matrix.environment }} - path: | - build_output/firmware/*.bin - build_output/firmware/*.gz - - uses: actions/upload-artifact@v2 + path: firmware-${{ matrix.environment }}.bin + - uses: actions/upload-artifact@v4 if: startsWith(github.ref, 'refs/tags/') with: - name: firmware-release + name: firmware-release-${{ matrix.environment }} path: build_output/release/*.bin release: name: Create Release - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: [get_default_envs, build] if: startsWith(github.ref, 'refs/tags/') steps: - - uses: actions/download-artifact@v2 - with: - name: firmware-release + - uses: actions/download-artifact@v4 + - name: List Artifacts + run: find ./ -type f -name *.bin -exec mv -v {} ./ \; - name: Create draft release uses: softprops/action-gh-release@v1 with: draft: True files: | - *.bin + WLEDMM*.bin env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 789de0a9e7..5a9283257a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,28 @@ -.pio .cache -.pioenvs -.piolibdeps -.vscode -/wled00/Release -/wled00/extLibs -/platformio_override.ini -/wled00/my_config.h -/build_output +.clang-format +.direnv .DS_Store .gitignore -.clang-format -node_modules .idea -.direnv -wled-update.sh +.pio +.pioenvs +.piolibdeps +.vscode + esp01-update.sh -/wled00/LittleFS +platformio_override.ini replace_fs.py -wled00/wled00.ino.cpp +wled-update.sh +qodana.yaml +compile_commands.json + +/build/ +/build_output/ +/node_modules/ + +/wled00/extLibs +/wled00/LittleFS +/wled00/my_config.h +/wled00/Release +/wled00/wled00.ino.cpp +/wled00/html_*.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 594fa97e14..6369804db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,120 @@ -## WLED changelog +## [WLED upstream](https://github.com/Aircoookie/WLED/tree/0_14_1) changelog + +#### Build 2403290 +- WLED 0.14.3 release +- Fix for transition 0 (#3854, #3832, #3720) + +#### Build 2403170 +- WLED 0.14.2 release + +#### Build 2403110 +- Beta WLED 0.14.2-b2 +- Fixing a potential array bounds violation in ESPDMX +- LockedJsonResponse: Release early if possible (by @willmmiles) + +#### Build 2402120 +- Beta WLED 0.14.2-b1 +- Possible fix for #3589 & partial fix for #3605 +- Prevent JSON buffer clear after failed lock attempt +- Multiple analog button fix for #3549 +- UM Audioreactive: add two compiler options (#3732 by @wled-install) +- Fix for #3693 + +#### Build 2401141 +- Official release of WLED 0.14.1 +- Fix for #3566, #3665, #3672 +- Sorting of palettes in custom palette editor (#3674 by @WoodyLetsCode) + +#### Build 2401060 +- Version bump: 0.14.1-b3 +- Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447) +- Fix for #3632 +- Custom palette editor mobile UI enhancement (#3617 by @imeszaros) +- changelog update + +#### Build 2312290 +- Fix for #3622 +- NB: fix for #3613 #3609 are not needed in MoonModules fork +- Various tweaks and fixes + +#### Build 2312230 +- Fix for #2922 (option to force WiFi PHY mode to G on ESP8266) +- Fix for #3601, #3400 (incorrect sunrise/sunset, #3612 by @softhack007) + +#### Build 2312180 +- Bugfixes (#3593, #3490, #3573, #3517, #3561, #3555, #3541, #3536, #3522, #3533, #3508) + +#### Build 2311160 +- Bugfixes (#3496, #3487) +- New usermod: LDR sensor (#3490 by @JeffWDH) +- Effect: Twinklefox & Tinklecat metadata fix +- Effect: separate #HH and #MM for Scrolling Text (#3480) + +#### Build 2310010, build 2310130 +- Bugfixes for #3400, #3403, #3405 +- minor HTML optimizations +- audioreactive: bugfix for UDP sound sync (partly initialized packets) + +#### Build 2309240 +- Effect bugfixes and improvements (Meteor, Meteor Smooth, Scrolling Text) +- audioreactive: bugfixes for ES8388 and ES7243 init; minor improvements for analog inputs + +#### Build 2309050 +- Added receive and send sync groups to JSON API (#3317) (you can change sync groups using preset) +- Internal temperature usermod (#3246) +- New effect: Rolling Balls (a.k.a. linear bounce) (#1039) +- Various bug fixes and enhancements. + +#### Build 2308030 +- Fix ESP-NOW crash with AP mode Always + +#### Build 2307180 +- various fixes and improvements (ESP variants platform 5.3.0, effect optimizations, /json/cfg pin allocation) + +#### Build 2307130 +- larger `oappend()` stack buffer (3.5k) for ESP32 +- Preset cycle bugfix (#3262) +- effect updates (2D Plasmaball), `blur()` speedup +- On/Off toggle from nodes view (may show unknown device type on older versions) (#3291) +- various fixes and improvements (ABL, crashes when changing presets with different segments) + +#### Build 2306270 +- ESP-NOW remote support (#3237) +- Pixel Magic tool (display pixel art) (#3249) +- Add WiFi network scan RPC command to Improv Serial (#3271) +- various fixes and improvements + +#### Build 2306210 +- 0.14.0-b3 release +- Multi relay usermod compile-time enabled option (-D MULTI_RELAY_ENABLED=true|false) + +#### Build 2306180 +- Added client-side option for applying effect defaults from metadata +- Improved ESP8266 stability by reducing WebSocket response resends +- Updated ESP8266 core to 3.1.2 + +#### Build 2306180 + +- Added client-side option for applying effect defaults from metadata +- Improved ESP8266 stability by reducing WebSocket response resends +- Updated ESP8266 core to 3.1.2 + +#### Build 2306141 +- Lissajous improvements +- Scrolling Text improvements (leading 0) + +#### Build 2306140 +- Add settings PIN (un)locking to JSON post API #### Build 2306020 - Support for segment sets (PR #3171) -- Reduce sound simulation modes to 2 to facilitiate segment sets +- Reduce sound simulation modes to 2 to facilitate segment sets - Trigger button immediately on press if all configured presets are the same (PR #3226) - Changes for allowing Alexa to change light color to White when auto-calculating from RGB (PR #3211) #### Build 2305280 - DDP protocol update (#3193) -- added PCF8574 I2C port expander support for Multi relay usermod - MQTT multipacket (fragmented) message fix - added option to retain MQTT brightness and color messages - new ethernet board: @srg74 Ethernet Shield @@ -340,7 +445,7 @@ - Added application level pong websockets reply (#2139) - Use AsyncTCP 1.0.3 as it mitigates the flickering issue from 0.13.0-b2 -- Fixed transition manually updated in preset overriden by field value +- Fixed transition manually updated in preset overridden by field value #### Build 2108050 @@ -684,7 +789,7 @@ #### Build 2101040 - Replaced Red & Blue effect with Aurora effect (PR #1589) -- Fixed HTTP changing segments uncommanded (#1618) +- Fixed HTTP changing segments un-commanded (#1618) - Updated copyright year and contributor page link #### Build 2012311 @@ -869,7 +974,7 @@ #### Build 2011040 -- Inversed Rain direction (fixes #1147) +- Inverted Rain direction (fixes #1147) #### Build 2011010 @@ -1080,7 +1185,7 @@ - Added module info page to web UI - Added realtime override functionality to web UI -- Added individial segment power and brightness to web UI +- Added individual segment power and brightness to web UI - Added feature to one-click select single segment only by tapping segment name - Removed palette jumping to default if color is changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 560a709731..05c60b8b58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,16 @@ Here are a few suggestions to make it easier for you to contribute! +### Describe your PR + +Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing. + +A good description helps us to review and understand your proposed changes. For example, you could say a few words about +* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) +* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) +* testing you performed, known limitations, open ends you possibly could not solve. +* any areas where you like to get help from an experienced maintainer (yes WLEDMM has become big šŸ˜‰) + ### Code style When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) @@ -14,15 +24,22 @@ You are all set if you have enabled `Editor: Detect Indentation` in VS Code. #### Blocks -Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block braches is acceptable. +Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block braces is acceptable. Good: ```cpp if (a == b) { doStuff(a); +} else { + doOtherStuff(b); } ``` +```cpp +if (a == b) doStuff(a); +``` + +less readable, but acceptable in some cases: ```cpp if (a == b) { @@ -30,9 +47,6 @@ if (a == b) } ``` -```cpp -if (a == b) doStuff(a); -``` There should always be a space between a keyword and its condition and between the condition and brace. Within the condition, no space should be between the paranthesis and variables. @@ -75,4 +89,5 @@ Good: There is no set character limit for a comment within a line, though as a rule of thumb you should wrap your comment if it exceeds the width of your editor window. -Inline comments are OK if they describe that line only and are not exceedingly wide. \ No newline at end of file + +Inline comments are OK if they describe that line only and are not exceedingly wide. diff --git a/LICENSE b/LICENSE index 69325d21c8..22953c3cf2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,291 @@ -MIT License - -Copyright (c) 2016 Christian Schwinne - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL Ā© the European Union 2007, 2016 + +This European Union Public Licence (the ā€˜EUPL’) applies to the Work (as +defined below) which is provided under the terms of this Licence. Any use of +the Work, other than as authorised under this Licence is prohibited (to the +extent such use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ā€˜The Licence’: this Licence. + +- ā€˜The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ā€˜Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This + Licence does not define the extent of modification or dependence on the + Original Work required in order to classify a work as a Derivative Work; + this extent is determined by copyright law applicable in the country + mentioned in Article 15. + +- ā€˜The Work’: the Original Work or its Derivative Works. + +- ā€˜The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ā€˜The Executable Code’: any code which has generally been compiled and which + is meant to be interpreted by a computer as a program. + +- ā€˜The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ā€˜Contributor(s)’: any natural or legal person who modifies the Work under + the Licence, or otherwise contributes to the creation of a Derivative Work. + +- ā€˜The Licensee’ or ā€˜You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ā€˜Distribution’ or ā€˜Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright +vested in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case + may be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make +effective the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights +to any patents held by the Licensor, to the extent necessary to make use of +the rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, +in a notice following the copyright notice attached to the Work, a repository +where the Source Code is easily and freely accessible for as long as the +Licensor continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits +from any exception or limitation to the exclusive rights of the rights owners +in the Work, of the exhaustion of those rights or of other applicable +limitations thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and +a copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of +the Licence — for example by communicating ā€˜EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions +on the Work or Derivative Work that alter or restrict the terms of the +Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed +under a Compatible Licence, this Distribution or Communication can be done +under the terms of this Compatible Licence. For the sake of this clause, +ā€˜Compatible Licence’ refers to the licences listed in the appendix attached to +this Licence. Should the Licensee's obligations under the Compatible Licence +conflict with his/her obligations under this Licence, the obligations of the +Compatible Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the +Work, the Licensee will provide a machine-readable copy of the Source Code or +indicate a repository where this Source will be easily and freely available +for as long as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade +names, trademarks, service marks, or names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she +brings to the Work are owned by him/her or licensed to him/her and that he/she +has the power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +ā€˜bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ā€˜as is’ +basis and without warranties of any kind concerning the Work, including +without limitation merchantability, fitness for a particular purpose, absence +of defects or errors, accuracy, non-infringement of intellectual property +rights other than copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a +condition for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the +use of the Work, including without limitation, damages for loss of goodwill, +work stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such +damage. However, the Licensor will be liable under statutory product liability +laws as far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional +agreement, defining obligations or services consistent with this Licence. +However, if accepting obligations, You may act only on your own behalf and on +your sole responsibility, not on behalf of the original Licensor or any other +Contributor, and only if You agree to indemnify, defend, and hold each +Contributor harmless for any liability incurred by, or claims asserted against +such Contributor by the fact You have accepted any warranty or additional +liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ā€˜I +agree’ placed under the bottom of a window displaying the text of this Licence +or by affirming consent in any other similar way, in accordance with the rules +of applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this +Licence, such as the use of the Work, the creation by You of a Derivative Work +or the Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of +electronic communication by You (for example, by offering to download the Work +from a remote location) the distribution channel or media (for example, a +website) must at least provide to the public the information requested by the +applicable law regarding the Licensor, the Licence and the way it may be +accessible, concluded, stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions +of this Licence or updated versions of the Appendix, so far this is required +and reasonable, without reducing the scope of the rights granted by the +Licence. New versions of the Licence will be published with a unique version +number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty + on the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive + jurisdiction of the competent court where the Licensor resides or conducts + its primary business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +ā€˜Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- QuĆ©bec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the +above licences without producing a new version of the EUPL, as long as they +provide the rights granted in Article 2 of this Licence and protect the +covered Source Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a +new EUPL version. diff --git a/boards/esp32_16MB-poe.json b/boards/esp32_16MB-poe.json new file mode 100644 index 0000000000..a15ef9a355 --- /dev/null +++ b/boards/esp32_16MB-poe.json @@ -0,0 +1,38 @@ +{ +"build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV -DARDUINO_ESP32_POE", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "esp32-poe", + "partitions": "partitions_16M.csv" +}, +"connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" +], +"debug": { + "openocd_board": "esp-wroom-32.cfg" +}, +"frameworks": [ + "arduino", + "espidf" +], +"name": "ESP32 16MB", +"upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 2000000 +}, +"url": "https://en.wikipedia.org/wiki/ESP32", +"vendor": "Multiple" +} diff --git a/boards/esp32_16MB.json b/boards/esp32_16MB.json new file mode 100644 index 0000000000..288dbfe969 --- /dev/null +++ b/boards/esp32_16MB.json @@ -0,0 +1,38 @@ +{ +"build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "esp32", + "partitions": "partitions_16M.csv" +}, +"connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" +], +"debug": { + "openocd_board": "esp-wroom-32.cfg" +}, +"frameworks": [ + "arduino", + "espidf" +], +"name": "ESP32 16MB", +"upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 2000000 +}, +"url": "https://en.wikipedia.org/wiki/ESP32", +"vendor": "Multiple" +} diff --git a/boards/lilygo-t7-s3.json b/boards/lilygo-t7-s3.json new file mode 100644 index 0000000000..4bf071fc7e --- /dev/null +++ b/boards/lilygo-t7-s3.json @@ -0,0 +1,47 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_TTGO_T7_S3", + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0X303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi", + "bluetooth" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "LILYGO T3-S3", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.aliexpress.us/item/3256804591247074.html", + "vendor": "LILYGO" +} \ No newline at end of file diff --git a/boards/lolin_s3_mini.json b/boards/lolin_s3_mini.json new file mode 100644 index 0000000000..7f55f0bde2 --- /dev/null +++ b/boards/lolin_s3_mini.json @@ -0,0 +1,47 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_LOLIN_S3_MINI", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x8167" + ] + ], + "mcu": "esp32s3", + "variant": "lolin_s3_mini" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "WEMOS LOLIN S3 Mini", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.wemos.cc/en/latest/s3/index.html", + "vendor": "WEMOS" +} + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2ec2d887a4..74a18035d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,2176 @@ { "name": "wled", - "version": "0.14.0-b2", - "lockfileVersion": 1, + "version": "14.5.1-dev", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "wled", + "version": "14.5.1-dev", + "license": "EUPL-1.2", + "dependencies": { + "clean-css": "^4.2.3", + "html-minifier-terser": "^5.1.1", + "inliner": "^1.13.1", + "nodemon": "^2.0.20", + "zlib": "^1.0.5" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/cheerio": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", + "integrity": "sha1-dy5wFfLuKZZQltcepBdbdas1SSU=", + "dependencies": { + "css-select": "~1.0.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "~3.8.1", + "lodash": "^3.2.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "dependencies": { + "chalk": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "dependencies": { + "q": "^1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/configstore": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz", + "integrity": "sha1-w1eB0FAdJowlxUuLF/YkDopPsCE=", + "dependencies": { + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "object-assign": "^4.0.1", + "os-tmpdir": "^1.0.0", + "osenv": "^0.1.0", + "uuid": "^2.0.1", + "write-file-atomic": "^1.1.2", + "xdg-basedir": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/configstore/node_modules/uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details." + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/css-select": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", + "integrity": "sha1-sRIcpRhI3SZOIkTQWM7iVN7rRLA=", + "dependencies": { + "boolbase": "~1.0.0", + "css-what": "1.0", + "domutils": "1.4", + "nth-check": "~1.0.0" + } + }, + "node_modules/css-what": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", + "integrity": "sha1-18wt9FGAZm+Z0rFEYmOUaeAPc2w=", + "engines": { + "node": "*" + } + }, + "node_modules/csso": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz", + "integrity": "sha1-F4tDpEYhIhwndWCG9THgL0KQDug=", + "dependencies": { + "clap": "^1.0.9", + "source-map": "^0.5.3" + }, + "bin": { + "csso": "bin/csso" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dependencies": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "node_modules/domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", + "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/dot-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", + "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", + "dependencies": { + "no-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "node_modules/dot-case/node_modules/lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dependencies": { + "tslib": "^1.10.0" + } + }, + "node_modules/dot-case/node_modules/no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dependencies": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "node_modules/es6-promise": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz", + "integrity": "sha1-lu258v2wGZWCKyY92KratnSBgbw=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/got": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", + "integrity": "sha1-5dDtSvVfw+701WAHdp2YGSvLLso=", + "dependencies": { + "duplexify": "^3.2.0", + "infinity-agent": "^2.0.0", + "is-redirect": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "nested-error-stacks": "^1.0.0", + "object-assign": "^3.0.0", + "prepend-http": "^1.0.0", + "read-all-stream": "^3.0.0", + "timed-out": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/got/node_modules/object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dependencies": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier-terser/node_modules/camel-case": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", + "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", + "dependencies": { + "pascal-case": "^3.1.1", + "tslib": "^1.10.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/html-minifier-terser/node_modules/param-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", + "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", + "dependencies": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "node_modules/htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "dependencies": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/infinity-agent": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/infinity-agent/-/infinity-agent-2.0.3.tgz", + "integrity": "sha1-ReDi/3qesDCyfWK3SzdEt6esQhY=" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/inliner": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/inliner/-/inliner-1.13.1.tgz", + "integrity": "sha1-5QApgev1Dp2fMTcRSBz/Ei1PP8s=", + "dependencies": { + "ansi-escapes": "^1.4.0", + "ansi-styles": "^2.2.1", + "chalk": "^1.1.3", + "charset": "^1.0.0", + "cheerio": "^0.19.0", + "debug": "^2.2.0", + "es6-promise": "^2.3.0", + "iconv-lite": "^0.4.11", + "jschardet": "^1.3.0", + "lodash.assign": "^3.2.0", + "lodash.defaults": "^3.1.2", + "lodash.foreach": "^3.0.3", + "mime": "^1.3.4", + "minimist": "^1.1.3", + "request": "^2.74.0", + "svgo": "^0.6.6", + "then-fs": "^2.0.0", + "uglify-js": "^2.8.0", + "update-notifier": "^0.5.0" + }, + "bin": { + "inliner": "cli/index.js" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/jschardet": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz", + "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/latest-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-1.0.1.tgz", + "integrity": "sha1-cs/Ebj6NG+ZR4eu1Tqn26pbzdLs=", + "dependencies": { + "package-json": "^1.0.0" + }, + "bin": { + "latest-version": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "node_modules/lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=" + }, + "node_modules/lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dependencies": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "node_modules/lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" + }, + "node_modules/lodash._baseeach": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", + "integrity": "sha1-z4cGVyyhROjZ11InyZDamC+TKvM=", + "dependencies": { + "lodash.keys": "^3.0.0" + } + }, + "node_modules/lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" + }, + "node_modules/lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "dependencies": { + "lodash._bindcallback": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash.restparam": "^3.0.0" + } + }, + "node_modules/lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, + "node_modules/lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" + }, + "node_modules/lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "dependencies": { + "lodash._baseassign": "^3.0.0", + "lodash._createassigner": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "node_modules/lodash.defaults": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", + "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", + "dependencies": { + "lodash.assign": "^3.0.0", + "lodash.restparam": "^3.0.0" + } + }, + "node_modules/lodash.foreach": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-3.0.3.tgz", + "integrity": "sha1-b9fvt5aRrs1n/erCdhyY5wHWw5o=", + "dependencies": { + "lodash._arrayeach": "^3.0.0", + "lodash._baseeach": "^3.0.0", + "lodash._bindcallback": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + }, + "node_modules/lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, + "node_modules/lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dependencies": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "node_modules/lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dependencies": { + "mime-db": "1.44.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/nested-error-stacks": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz", + "integrity": "sha1-GfYZWRUZ8JZ2mlupqG5u7sgjw88=", + "dependencies": { + "inherits": "~2.0.1" + } + }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", + "integrity": "sha1-yOysCUInzfdqMWh07QXifMk5oOA=", + "dependencies": { + "got": "^3.2.0", + "registry-url": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pascal-case": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", + "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", + "dependencies": { + "no-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "node_modules/pascal-case/node_modules/lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dependencies": { + "tslib": "^1.10.0" + } + }, + "node_modules/pascal-case/node_modules/no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dependencies": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", + "dependencies": { + "pinkie-promise": "^2.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-all-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/read-all-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/read-all-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/read-all-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/repeating": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", + "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", + "dependencies": { + "is-finite": "^1.0.0" + }, + "bin": { + "repeating": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dependencies": { + "semver": "^5.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "engines": { + "node": "*" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/string-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", + "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", + "dependencies": { + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/svgo": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz", + "integrity": "sha1-s0CIkDbyD5tEdUMHfQ9Vc+0ETAg=", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "dependencies": { + "coa": "~1.0.1", + "colors": "~1.1.2", + "csso": "~2.0.0", + "js-yaml": "~3.6.0", + "mkdirp": "~0.5.1", + "sax": "~1.2.1", + "whet.extend": "~0.9.9" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/then-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/then-fs/-/then-fs-2.0.0.tgz", + "integrity": "sha1-cveS3Z0xcFqRrhnr/Piz+WjIHaI=", + "dependencies": { + "promise": ">=3.2 <8" + } + }, + "node_modules/timed-out": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", + "integrity": "sha1-84sK6B03R9YoAB9B2vxlKs5nHAo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, + "node_modules/update-notifier": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.5.0.tgz", + "integrity": "sha1-B7XcIGazYnqztPUwEw9+3doHpMw=", + "dependencies": { + "chalk": "^1.0.0", + "configstore": "^1.0.0", + "is-npm": "^1.0.0", + "latest-version": "^1.0.0", + "repeating": "^1.1.2", + "semver-diff": "^2.0.0", + "string-length": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", + "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", + "dependencies": { + "os-homedir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + }, + "node_modules/zlib": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", + "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=", + "hasInstallScript": true, + "engines": { + "node": ">=0.2.0" + } + } + }, "dependencies": { "abbrev": { "version": "1.1.1", @@ -609,7 +2777,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "he": { "version": "1.2.0", @@ -704,7 +2872,7 @@ "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" }, "imurmurhash": { "version": "0.1.4", @@ -1059,9 +3227,9 @@ } }, "nodemon": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", - "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", "requires": { "chokidar": "^3.5.2", "debug": "^3.2.7", @@ -1101,7 +3269,7 @@ "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", "requires": { "abbrev": "1" } @@ -1255,9 +3423,9 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "rc": { "version": "1.2.8", @@ -1488,6 +3656,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, "string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", @@ -1496,11 +3669,6 @@ "strip-ansi": "^3.0.0" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", diff --git a/package.json b/package.json index 5c4cfde408..2e82671207 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.14.0-b2", + "version": "14.5.1-dev", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -13,14 +13,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Aircoookie/WLED.git" + "url": "git+https://github.com/MoonModules/WLED.git" }, "author": "", - "license": "ISC", + "license": "EUPL-1.2", "bugs": { - "url": "https://github.com/Aircoookie/WLED/issues" + "url": "https://github.com/MoonModules/WLED/issues" }, - "homepage": "https://github.com/Aircoookie/WLED#readme", + "homepage": "https://github.com/MoonModules/WLED#readme", "dependencies": { "clean-css": "^4.2.3", "html-minifier-terser": "^5.1.1", diff --git a/pio-scripts/build-html.py b/pio-scripts/build-html.py new file mode 100644 index 0000000000..93df542698 --- /dev/null +++ b/pio-scripts/build-html.py @@ -0,0 +1,15 @@ +Import('env') +import glob +import os + +if not os.path.isdir('node_modules'): + env.Execute("npm ci") + +if not os.path.exists('wled00/html_ui.h'): + env.Execute("npm run build") + +latest_source = max(glob.glob('wled00/data/*.htm'), key=os.path.getmtime) +latest_export = max(glob.glob('wled00/html_*.h'), key=os.path.getmtime) + +if os.path.getmtime(latest_source) > os.path.getmtime(latest_export): + env.Execute("npm run build") diff --git a/pio-scripts/obj-dump.py b/pio-scripts/obj-dump.py index 91bc3de581..174df509c3 100644 --- a/pio-scripts/obj-dump.py +++ b/pio-scripts/obj-dump.py @@ -1,9 +1,24 @@ # Little convenience script to get an object dump +# You may add "-S" to the objdump commandline (i.e. replace "-D -C " with "-d -S -C ") +# to get source code intermixed with disassembly (SLOW !) Import('env') def obj_dump_after_elf(source, target, env): + platform = env.PioPlatform() + board = env.BoardConfig() + mcu = board.get("build.mcu", "esp32") + print("Create firmware.asm") - env.Execute("xtensa-lx106-elf-objdump "+ "-D " + str(target[0]) + " > "+ "${PROGNAME}.asm") - + if mcu == "esp8266": + env.Execute("xtensa-lx106-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32": + env.Execute("xtensa-esp32-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32s2": + env.Execute("xtensa-esp32s2-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32s3": + env.Execute("xtensa-esp32s3-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32c3": + env.Execute("riscv32-esp-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", [obj_dump_after_elf]) diff --git a/pio-scripts/output_bins.py b/pio-scripts/output_bins.py index 01223e93d5..3a55ced8cd 100644 --- a/pio-scripts/output_bins.py +++ b/pio-scripts/output_bins.py @@ -22,9 +22,93 @@ def _create_dirs(dirs=["firmware", "map"]): if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)): os.mkdir("{}{}".format(OUTPUT_DIR, d)) + +# trick for py2/3 compatibility +if 'basestring' not in globals(): + basestring = str + +# WLEDMM : custom print function +def print_my_item(items, flag = False): + if flag: print(" -D", end='') + else: print(" ", end='') + if isinstance(items, basestring): + # print a single string + print(items, end='') + else: + # print a list + first = True + for item in items: + if not first: print("=", end='') + print(item, end='') + first = False + +# WLEDMM : dump out buildflags : usermods, disable, enable, use_.. +def wledmm_print_build_info(env): + all_flags = env["CPPDEFINES"] + first = True + + found = False + for item in all_flags: + if 'WLED_RELEASE_NAME' in item[0] or 'WLED_VERSION' in item[0] or 'ARDUINO_USB_CDC_ON_BOOT' in item[0]: + if first: print("\nUsermods and Features:") + print_my_item(item) + first = False + found = True + if found: print("") + + found = False + for item in all_flags: + if 'USERMOD_' in item or 'UM_' in item: + if first: print("\nUsermods and Features:") + print_my_item(item) + first = False + found = True + if found: print("") + + found = False + for item in all_flags: + if 'WLED_DISABLE' in item or 'WIFI_FIX' in item: + if first: print("\nUsermods and Features:") + print_my_item(item) + first = False + found = True + if found: print("") + + found = False + for item in all_flags: + if 'WLED_' in item or 'WLED_' in item[0] or 'MAX_LED' in item[0]: + if not 'WLED_RELEASE_NAME' in item[0] and not 'WLED_VERSION' in item[0] and not 'WLED_WATCHDOG_TIMEOUT' in item[0] and not 'WLED_DISABLE' in item and not 'WLED_USE_MY_CONFIG' in item and not 'ARDUINO_PARTITION' in item: + if first: print("\nUsermods and Features:") + print_my_item(item) + first = False + found = True + if found: print("") + + first = True + found = False + for item in all_flags: + if 'WLEDMM_' in item[0] or 'WLEDMM_' in item or 'TROYHACKS' in item: + if first: print("\nWLEDMM Features:") + print_my_item(item) + first = False + found = True + if found: print("\n") + +def wledmm_print_all_defines(env): + all_flags = env["CPPDEFINES"] + found = False + for item in all_flags: + if not found: print("\nBuild Flags:") + print_my_item(item, True) + found = True + if found: print("\n") + + def bin_rename_copy(source, target, env): _create_dirs() variant = env["PIOENV"] + builddir = os.path.join(env["PROJECT_BUILD_DIR"], variant) + source_map = os.path.join(builddir, env["PROGNAME"] + ".map") # create string with location and file names based on variant map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) @@ -35,7 +119,7 @@ def bin_rename_copy(source, target, env): if release_name: _create_dirs(["release"]) version = _get_cpp_define_value(env, "WLED_VERSION") - release_file = "{}release{}WLED_{}_{}.bin".format(OUTPUT_DIR, os.path.sep, version, release_name) + release_file = "{}release{}WLEDMM_{}_{}.bin".format(OUTPUT_DIR, os.path.sep, version, release_name) #WLEDMM: add MM postfix to WLED shutil.copy(str(target[0]), release_file) # check if new target files exist and remove if necessary @@ -48,7 +132,14 @@ def bin_rename_copy(source, target, env): # copy firmware.map to map/.map if os.path.isfile("firmware.map"): - shutil.move("firmware.map", map_file) + print("Found linker mapfile firmware.map") + shutil.copy("firmware.map", map_file) + if os.path.isfile(source_map): + print(f"Found linker mapfile {source_map}") + shutil.copy(source_map, map_file) + + # wledmm_print_all_defines(env) + # wledmm_print_build_info(env) def bin_gzip(source, target, env): _create_dirs() diff --git a/platformio.ini b/platformio.ini index ae0e4abd45..93f4fdcd78 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,7 +11,7 @@ # CI binaries ;; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth # ESP32 variant builds are temporarily excluded from CI due to toolchain issues on the GitHub Actions Linux environment -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB +; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi # Release binaries ; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB @@ -40,10 +40,81 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_ ; default_envs = esp32dev_qio80 ; default_envs = esp32_eth_ota1mapp ; default_envs = esp32s2_saola +; default_envs = esp32c3dev +; default_envs = lolin_s2_mini +; default_envs = esp32s3dev_16MB_PSRAM_opi + +; MoonModules entries +; =================== + +default_envs = + esp32_4MB_S ;; experimental, optimized for speed + esp32_4MB_M ;; esp32 recommended default + esp32_4MB_M_eth + esp32_4MB_M_debug + esp32_4MB_XL + ; esp32_16MB_S ;; experimental, optimized for speed + esp32_16MB_M ;; esp32 recommended for boards with 16MB flash + esp32_16MB_M_eth + ; esp32_16MB_M_debug + ; esp32_16MB_XL + esp32_pico_4MB_M + ;; + wemos_shield_esp32_4MB_M + ; wemos_shield_esp32_4MB_ICS4343x_M + ; wemos_shield_esp32_4MB_SPM1423_M + ; wemos_shield_esp32_4MB_LineIn_M + ; wemos_shield_esp32_16MB_M + ; wemos_shield_esp32_16MB_ICS4343x_M + ; wemos_shield_esp32_16MB_SPM1423_M + ; wemos_shield_esp32_16MB_SPM1423_XL + ; wemos_shield_esp32_16MB_LineIn_M + ;; + esp32_4MB_V4_S ;; experimental; HUB75 supported + esp32_4MB_V4_HUB75_forum ;; experimental; HUB75 supported (forum pinout) + esp32_16MB_V4_S ;; experimental - optimized for speed; HUB75 supported + esp32_16MB_V4_M ;; experimental; HUB75 supported + ; esp32_16MB_V4_M_debug ;; experimental + ; esp32_pico_4MB_V4_S ;; experimental - may work better in case you experience wifi connectivity problems + ;; + esp32_4MB_PSRAM_S ;; WROVER experimental; HUB75 supported + ; esp32_4MB_PSRAM_REV3_S ;; WROVER-E experimental, optimized for WROVER-E with "revision3" chip + ;; + esp32S3_4MB_S ;; for HD-WF2 (HUB75 supported) + esp32S3_8MB_S ;; experimental, optimized for speed, HUB75 supported + esp32S3_8MB_M ;; HUB75 supported + esp32S3_4MB_PSRAM_S ;; for lolin s3 mini, S3 zero, S3 super mini - optimized for speed (no HUB75 support) + esp32S3_4MB_PSRAM_M ;; for lolin s3 mini, S3 zero, S3 super mini (no HUB75 support) + esp32S3_8MB_PSRAM_M ;; experiemental; HUB75 supported + esp32S3_16MB_PSRAM_M_HUB75 ;; for S3 with 16MB flash, HUB75 supported (MOONHUB HUB75 adapter board) + esp32S3_WROOM-2_M ;; for S3 WROOM-2; HUB75 supported + ;; + esp32s2_PSRAM_S ;; OTA-compatible with upstream + esp32s2_PSRAM_M ;; for lolin S2 mini + ;; esp32s2_tinyUF2_PSRAM_S ;; experimental - only for adafruit -S2 boards with tinyUF2 bootloader !!! + ;; + esp32c3dev_2MB_M ;; experimental - 2MB Flash, no OTA + esp32c3dev_4MB_M ;; experimental + esp32c3mini_dio_4MB_M ;; for boards that need "dio" flash mode (instead of qio) + seeed_esp32c3_4MB_S ;; experimental + ;; + esp8266_2MB_S + esp8266_4MB_S ;; recommended for 8266 with audio sync + esp8266_4MB_M + ; esp8266pro_16MB_S + esp8266pro_16MB_M + esp01_1MB_S + ;; + athom_music_esp32_4MB_M + adafruit_matrixportal_esp32s3_tinyUF2 ;; HUB75 supported, uses adafruit bootloader + ; adafruit_matrixportal_esp32s3_legacy ;; try this if the "tinyUF2" firmware doesn't run on your board + +; Go to MoonModules environments for environments + src_dir = ./wled00 data_dir = ./wled00/data -build_cache_dir = ~/.buildcache +build_cache_dir = ~/.platformio/buildcache extra_configs = platformio_override.ini @@ -55,40 +126,31 @@ extra_configs = # arduino core 2.6.3 = platformIO 2.3.2 # arduino core 2.7.0 = platformIO 2.5.0 # ------------------------------------------------------------------------------ -arduino_core_2_6_3 = espressif8266@2.3.3 -arduino_core_2_7_4 = espressif8266@2.6.2 -arduino_core_3_0_0 = espressif8266@3.0.0 -arduino_core_3_2_0 = espressif8266@3.2.0 -arduino_core_4_1_0 = espressif8266@4.1.0 +arduino_core_3_1_2 = espressif8266@4.2.1 # Development platforms arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/stage # Platform to use for ESP8266 -platform_wled_default = ${common.arduino_core_4_1_0} +platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization #platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 -platform_packages = platformio/framework-arduinoespressif8266 - platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 +platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 platformio/tool-esptool #@ ~1.413.0 platformio/tool-esptoolpy #@ ~1.30000.0 -## previous platform for 8266, in case of problems with the new one -## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_2_0, which does not support Ucs890x -;; platform_wled_default = ${common.arduino_core_3_2_0} -;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 -;; platformio/toolchain-xtensa @ ~2.40802.200502 -;; platformio/tool-esptool @ ~1.413.0 -;; platformio/tool-esptoolpy @ ~1.30000.0 # ------------------------------------------------------------------------------ # FLAGS: DEBUG -# +# esp8266 : see https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level +# esp32 : see https://docs.platformio.org/en/latest/platforms/espressif32.html#debug-level # ------------------------------------------------------------------------------ -debug_flags = -D DEBUG=1 -D WLED_DEBUG -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM -#if needed (for memleaks etc) also add; -DDEBUG_ESP_OOM -include "umm_malloc/umm_malloc_cfg.h" -#-DDEBUG_ESP_CORE is not working right now +debug_flags = -D DEBUG=1 -D WLED_DEBUG + -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM ;; for esp8266 + -DARDUINOJSON_DEBUG=1 ;; WLEDMM: this enables some debug asserts in arduinoJSON + # if needed (for memleaks etc) also add; -DDEBUG_ESP_OOM -include "umm_malloc/umm_malloc_cfg.h" + # -DDEBUG_ESP_CORE is not working right now # ------------------------------------------------------------------------------ # FLAGS: ldscript (available ldscripts at https://github.com/esp8266/Arduino/tree/master/tools/sdk/ld) @@ -118,7 +180,8 @@ build_flags = -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL -DBEARSSL_SSL_BASIC -D CORE_DEBUG_LEVEL=0 - -D NDEBUG + ;; WLEDMM espressif docs say NDEBUG is not recommended. see https://docs.espressif.com/projects/esp-idf/en/v4.4.4/esp32/api-guides/performance/speed.html#not-recommended + -D NDEBUG ;; however it seems neecessary for non-V4 builds, to avoid spurious "assert" crashes inside the wifi core. -Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus #build_flags for the IRremoteESP8266 library (enabled decoders have to appear here) -D _IR_ENABLE_DEFAULT_=false @@ -127,6 +190,7 @@ build_flags = -D DECODE_SONY=true -D DECODE_SAMSUNG=true -D DECODE_LG=true + -D FASTLED_NO_FASTLED ;-Dregister= # remove warnings in C++17 due to use of deprecated register keyword by the FastLED library ;; warning: this breaks framework code on ESP32-C3 and ESP32-S2 -DWLED_USE_MY_CONFIG ; -D USERMOD_SENSORSTOMQTT @@ -136,7 +200,7 @@ build_flags = build_unflags = build_flags_esp8266 = ${common.build_flags} ${esp8266.build_flags} -build_flags_esp32 = ${common.build_flags} ${esp32.build_flags} +build_flags_esp32 = ${common.build_flags} ${esp32_idf_V4.build_flags} build_flags_esp32_V4= ${common.build_flags} ${esp32_idf_V4.build_flags} ldscript_1m128k = eagle.flash.1m128.ld @@ -144,12 +208,19 @@ ldscript_2m512k = eagle.flash.2m512.ld ldscript_2m1m = eagle.flash.2m1m.ld ldscript_4m1m = eagle.flash.4m1m.ld +;; WLEDMM: additional partion layouts for 8266 "pro" with 8MB or 16MB. On 8266 the max program size is always 1MB +ldscript_auto = eagle.flash.auto.ld +ldscript_8m6m = eagle.flash.8m6m.ld +ldscript_16m14m = eagle.flash.16m14m.ld + [scripts_defaults] extra_scripts = pre:pio-scripts/set_version.py + pre:pio-scripts/build-html.py post:pio-scripts/output_bins.py post:pio-scripts/strip-floats.py pre:pio-scripts/user_config_copy.py + ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ # COMMON SETTINGS: @@ -172,10 +243,12 @@ upload_speed = 115200 # ------------------------------------------------------------------------------ lib_compat_mode = strict lib_deps = - fastled/FastLED @ 3.5.0 + fastled/FastLED @ 3.6.0 + ;; fastled/FastLED @ 3.7.1 + ;; https://github.com/softhack007/FastLED.git#ESP32-C6 ;; patched version needed for -C6 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.7.5 - https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.7 + ;;makuna/NeoPixelBus @ 2.7.5 ;; WLEDMM will be added in board specific sections + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI #For compatible OLED display uncomment following @@ -198,7 +271,7 @@ build_flags = -DESP8266 -DFP_IN_IROM ;-Wno-deprecated-declarations - -Wno-register ;; leaves some warnings when compiling C files: command-line option '-Wno-register' is valid for C++/ObjC++ but not for C + ;-Wno-register ;; leaves some warnings when compiling C files: command-line option '-Wno-register' is valid for C++/ObjC++ but not for C ;-Dregister= # remove warnings in C++17 due to use of deprecated register keyword by the FastLED library ;; warning: this can be dangerous -Wno-misleading-indentation ; NONOSDK22x_190703 = 2.2.2-dev(38a443e) @@ -211,65 +284,146 @@ build_flags = -DVTABLES_IN_FLASH ; restrict to minimal mime-types -DMIMETYPE_MINIMAL + ; other special-purpose framework flags (see https://docs.platformio.org/en/latest/platforms/espressif8266.html) + ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'" + ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown + -D USERMOD_AUDIOREACTIVE + -D NON32XFER_HANDLER ;; ask forgiveness for PROGMEM misuse lib_deps = #https://github.com/lorol/LITTLEFS.git ESPAsyncTCP @ 1.2.2 ESPAsyncUDP + ;; makuna/NeoPixelBus @ 2.6.9 ;; WLEDMM use if you have problems with 2.7.5 + makuna/NeoPixelBus @ 2.7.5 ${env.lib_deps} +;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 +build_flags_compat = + -DESP8266 + -DFP_IN_IROM + ;;-Wno-deprecated-declarations + -Wno-misleading-indentation + ;;-Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus + -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 + -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH + -DVTABLES_IN_FLASH + -DMIMETYPE_MINIMAL + -DWLED_SAVE_IRAM ;; needed to prevent linker error + +;; this platform version was used for WLED 0.14.0 +platform_compat = espressif8266@4.2.0 +platform_packages_compat = + platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 + platformio/tool-esptool #@ ~1.413.0 + platformio/tool-esptoolpy #@ ~1.30000.0 + + + [esp32] -#platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip -platform = espressif32@3.5.0 +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_flags = ${esp32_idf_V4.build_flags} -platform_packages = framework-arduinoespressif32 @ https://github.com/Aircoookie/arduino-esp32.git#1.0.6.4 +default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv ;; WLED standard for 4MB flash: 1.4MB firmware, 1MB filesystem +;default_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; WLEDMM alternative for 4MB flash: 1.8MB firmware, 256KB filesystem (esptool erase_flash needed before changing) -build_flags = -g - -DARDUINO_ARCH_ESP32 - #-DCONFIG_LITTLEFS_FOR_IDF_3_2 - -D CONFIG_ASYNC_TCP_USE_WDT=0 - #use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x - -D LOROL_LITTLEFS - ; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 +tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv +extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv +big_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem, coredump support +large_partitions = tools/WLED_ESP32_8MB.csv +extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +lib_deps = ${esp32_idf_V4.lib_deps} -lib_deps = - https://github.com/lorol/LITTLEFS.git - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 +;; Compatibility with upstream --> you should prefer using ${common_mm.build_flags_S} and ${common_mm.lib_deps_S} +AR_build_flags = ${common_mm.AR_build_flags} +AR_lib_deps = ${common_mm.AR_lib_deps} ;; optimized version, 10% faster on -S2/-C3 + +;; WLEDMM begin + +;; ** For compiling with latest Frameworks (IDF4.4.x and arduino-esp32 v2.0.x) ** +;;; previous standard V4 platform +platformV4_pre = espressif32@5.2.0 +platformV4_packages_pre = + toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 ; required for platform version < 5.3.0, remove this line when upgrading to platform >=5.3.0 +;;; standard V4 platform +platformV4 = espressif32@ ~6.3.2 +platformV4_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) + +;;; experimental: latest V4 platform with latest arduino-esp32 2.0.14 + ESP-IDF 4.4.6 (may or may not work) +platformV4_xp = espressif32@ ~6.5.0 +platformV4_packages_xp = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14 + +build_flagsV4 = -g + -DARDUINO_ARCH_ESP32 -DESP32 + -DCONFIG_LITTLEFS_FOR_IDF_3_2 -DLFS_THREADSAFE + -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D CONFIG_ASYNC_TCP_TASK_STACK_SIZE=9472 ;; WLEDMM increase stack by 1.25Kb, as audioreactive needs bigger SETTINGS_STACK_BUF_SIZE + ; -D WLEDMM_TWOPATH ;; use I2S1 as the second bus --> slightly faster on some setups + ; -D WLEDMM_SLOWPATH ;; don't use I2S for LED bus + ; -DARDUINO_USB_CDC_ON_BOOT=0 ;; mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + -D NO_GFX ; Disable the use of Adafruit_GFX by the HUB75 driver + +;;; V4.4.x libraries (without LOROL_LITTLEFS; with newer NeoPixelBus) +lib_depsV4 = + https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ;; WLEDMM this must be first in the list, otherwise Aircoookie/ESPAsyncWebServer pulls in an older version of AsyncTCP !! + makuna/NeoPixelBus @ 2.7.5 + ;; makuna/NeoPixelBus @ 2.7.9 ;; experimental + ${common_mm.HUB75_lib_deps} ${env.lib_deps} +;; WLEDMM end + [esp32_idf_V4] -;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 -;; very similar to the normal ESP32 flags, but omitting Lorol LittleFS, as littlefs is included in the new framework already. -;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. -platform = espressif32@5.2.0 + +;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +;; platform = espressif32@ ~6.3.2 +;; platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) + +platform = https://github.com/tasmota/platform-espressif32/releases/download/2023.06.02/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.9 with IPv6 support, based on IDF 4.4.4 platform_packages = - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 ; required for platform version < 5.3.0, remove this line when upgrading to platform >=5.3.0 +build_unflags = ${common.build_unflags} build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 #-DCONFIG_LITTLEFS_FOR_IDF_3_2 -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D CONFIG_ASYNC_TCP_TASK_STACK_SIZE=9472 ;; WLEDMM increase stack by 1.25Kb, as audioreactive needs bigger SETTINGS_STACK_BUF_SIZE -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + ; -D WLEDMM_TWOPATH ;; use I2S1 as the second bus --> slightly faster on some setups + ; -D WLEDMM_SLOWPATH ;; don't use I2S for LED bus default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + makuna/NeoPixelBus @ 2.7.5 + ;; makuna/NeoPixelBus @ 2.7.9 ;; experimental ${env.lib_deps} [esp32s2] ;; generic definitions for all ESP32-S2 boards -platform = espressif32@5.2.0 -platform_packages = - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 ; required for platform version < 5.3.0, remove this line when upgrading to platform >=5.3.0 +;; platform = espressif32@5.2.0 +;; platform_packages = +;; toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 +;; toolchain-xtensa-esp32s2 @ 8.4.0+2021r2-patch5 +;; standard platform +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +;; tasmota platform (optional) +;; platform = ${esp32_idf_V4.platform} +;; platform_packages = ${esp32_idf_V4.platform_packages} +board_build.flash_mode = dio ;; prevents build error: sdkconfig.h: No such file or directory + build_flags = -g - -DARDUINO_ARCH_ESP32 + -DARDUINO_ARCH_ESP32 -DESP32 ;; WLEDMM -DARDUINO_ARCH_ESP32S2 -DCONFIG_IDF_TARGET_ESP32S2=1 + -DCONFIG_LITTLEFS_FOR_IDF_3_2 -DLFS_THREADSAFE ;; WLEDMM -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D CONFIG_ASYNC_TCP_TASK_STACK_SIZE=8614 ;; WLEDMM increase stack by 1Kb, as audioreactive needs bigger SETTINGS_STACK_BUF_SIZE -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DCO -DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 ! @@ -278,18 +432,22 @@ build_flags = -g lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ;; makuna/NeoPixelBus @ 2.7.5 ;; standard + makuna/NeoPixelBus @ 2.7.9 ;; experimental - reduces LED glitches on -S2 ${env.lib_deps} [esp32c3] ;; generic definitions for all ESP32-C3 boards -platform = espressif32@5.2.0 -platform_packages = - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 ; required for platform version < 5.3.0, remove this line when upgrading to platform >=5.3.0 +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} build_flags = -g - -DARDUINO_ARCH_ESP32 + -DARDUINO_ARCH_ESP32 -DESP32 ;; WLEDMM -DARDUINO_ARCH_ESP32C3 -DCONFIG_IDF_TARGET_ESP32C3=1 + -DCONFIG_LITTLEFS_FOR_IDF_3_2 -DLFS_THREADSAFE ;; WLEDMM -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D CONFIG_ASYNC_TCP_TASK_STACK_SIZE=9472 ;; WLEDMM increase stack by 1.25Kb, as audioreactive needs bigger SETTINGS_STACK_BUF_SIZE -DCO -DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3 ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: @@ -297,26 +455,32 @@ build_flags = -g lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + makuna/NeoPixelBus @ 2.7.5 + ;; makuna/NeoPixelBus @ 2.7.9 ;; experimental ${env.lib_deps} [esp32s3] ;; generic definitions for all ESP32-S3 boards -platform = espressif32@5.2.0 -platform_packages = - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 ; required for platform version < 5.3.0, remove this line when upgrading to platform >=5.3.0 +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} build_flags = -g -DESP32 -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S3 -DCONFIG_IDF_TARGET_ESP32S3=1 + -DCONFIG_LITTLEFS_FOR_IDF_3_2 -DLFS_THREADSAFE ;; WLEDMM + -DWLEDMM_NO_SERIAL_WAIT ;; WLEDMM don't wait for serial on -S3 (unless WLED_DEBUG is set) -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D CONFIG_ASYNC_TCP_TASK_STACK_SIZE=9472 ;; WLEDMM increase stack by 1.25Kb, as audioreactive needs bigger SETTINGS_STACK_BUF_SIZE -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_DFU_ON_BOOT=0 -DCO ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT - lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + makuna/NeoPixelBus @ 2.7.5 + ;; makuna/NeoPixelBus @ 2.7.9 ;; experimental ${env.lib_deps} @@ -334,6 +498,11 @@ build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder +[env:nodemcuv2_160] +extends = env:nodemcuv2 +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D + [env:esp8266_2m] board = esp_wroom_02 platform = ${common.platform_wled_default} @@ -343,14 +512,26 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02 lib_deps = ${esp8266.lib_deps} -[env:esp01_1m_full] -board = esp01_1m -platform = ${common.platform_wled_default} -platform_packages = ${common.platform_packages} -board_build.ldscript = ${common.ldscript_1m128k} -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA -lib_deps = ${esp8266.lib_deps} +[env:esp8266_2m_160] +extends = env:esp8266_2m +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02_160 + +;WLEDMM: see below +; [env:esp01_1m_full] +; board = esp01_1m +; platform = ${common.platform_wled_default} +; platform_packages = ${common.platform_packages} +; board_build.ldscript = ${common.ldscript_1m128k} +; build_unflags = ${common.build_unflags} +; build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA +; ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM +; lib_deps = ${esp8266.lib_deps} +; [env:esp01_1m_full_160] +; extends = env:esp01_1m_full +; board_build.f_cpu = 160000000L +; build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA +; ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM [env:esp07] board = esp07 @@ -418,7 +599,6 @@ board_build.flash_mode = qio ;; please erase your device before installing. board = esp32dev platform = ${esp32_idf_V4.platform} -platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_V4_qio80 #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32_idf_V4.lib_deps} @@ -434,6 +614,7 @@ platform_packages = ${esp32.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} @@ -447,23 +628,42 @@ board_build.flash_mode = qio upload_speed = 460800 build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME=S2_saola + ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s2.lib_deps} +;; WLEDMM: use WLEDMM build environments +;; [env:esp32_wrover] +;; extends = esp32_idf_V4 +;; platform = ${esp32_idf_V4.platform} +;; platform_packages = ${esp32_idf_V4.platform_packages} +;; board = ttgo-t7-v14-mini32 +;; board_build.f_flash = 80000000L +;; board_build.flash_mode = qio +;; board_build.partitions = ${esp32.extended_partitions} +;; build_unflags = ${common.build_unflags} +;; build_flags = ${common.build_flags_esp32_V4} -D WLED_RELEASE_NAME=ESP32_WROVER +;; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html +;; -D LEDPIN=25 +;; ; ${esp32.AR_build_flags} +;; lib_deps = ${esp32_idf_V4.lib_deps} +;; ; ${esp32.AR_lib_deps} + [env:esp32c3dev] extends = esp32c3 platform = ${esp32c3.platform} -platform_packages = ${esp32c3.platform_packages} framework = arduino board = esp32-c3-devkitm-1 -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv -build_flags = ${common.build_flags} ${esp32c3.build_flags} #-D WLED_RELEASE_NAME=ESP32-C3 +board_build.partitions = ${esp32.default_partitions} +build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=ESP32-C3 -D WLED_WATCHDOG_TIMEOUT=0 - ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB - -DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip + -DLOLIN_WIFI_FIX ; seems to work much better with this + -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB + ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} +board_build.flash_mode = qio [env:esp32s3dev_8MB] ;; ESP32-S3-DevKitC-1 development board, with 8MB FLASH, no PSRAM (flash_mode: qio) @@ -472,23 +672,23 @@ platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 ; or 460800 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - ;-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=0 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + ;-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") ;-D WLED_DEBUG lib_deps = ${esp32s3.lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio ; board_build.flash_mode = dio ;; try this if you have problems at startup monitor_filters = esp32_exception_decoder -[env:esp32s3dev_8MB_PSRAM] -;; ESP32-TinyS3 development board, with 8MB FLASH and 8MB PSRAM (memory_type: qio_opi, qio_qspi, or opi_opi) -;board = um_tinys3 ; -> needs workaround from https://github.com/Aircoookie/WLED/pull/2905#issuecomment-1328049860 -;board = esp32s3box ; -> error: 'esp32_adc2gpio' was not declared in this scope -board = esp32-s3-devkitc-1 ; -> compiles, but does not support PSRAM +[env:esp32s3dev_8MB_PSRAM_opi] +;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) +board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 @@ -496,15 +696,29 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=0 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") ; -D WLED_RELEASE_NAME=ESP32-S3_PSRAM -D WLED_USE_PSRAM -DBOARD_HAS_PSRAM ; tells WLED that PSRAM shall be used + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap lib_deps = ${esp32s3.lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder +[env:esp32s3dev_16MB_PSRAM_opi] +extends = env:esp32s3dev_8MB_PSRAM_opi +board_build.partitions = tools/WLED_ESP32_16MB.csv +board_upload.flash_size = 16MB + +[env:esp32s3dev_8MB_PSRAM_qspi] +;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi) +extends = env:esp32s3dev_8MB_PSRAM_opi +;board = um_tinys3 ; -> needs workaround from https://github.com/Aircoookie/WLED/pull/2905#issuecomment-1328049860 +board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB + [env:esp8285_4CH_MagicHome] board = esp8285 platform = ${common.platform_wled_default} @@ -571,15 +785,20 @@ lib_deps = ${esp8266.lib_deps} platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv -build_unflags = ${common.build_unflags} -DARDUINO_USB_CDC_ON_BOOT=1 -build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME=LolinS2 +board_build.partitions = ${esp32.default_partitions} +board_build.flash_mode = dio ;; prevents build error: sdkconfig.h: No such file or directory +build_unflags = ${common.build_unflags} #-DARDUINO_USB_CDC_ON_BOOT=1 +;; board_build.flash_mode = qio +;; board_build.f_flash = 80000000L +build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=ESP32-S2 -DBOARD_HAS_PSRAM - -DARDUINO_USB_CDC_ON_BOOT=0 + -DARDUINO_USB_CDC_ON_BOOT=1 # try disabling and enabling unflag above in case of board-specific issues, will disable Serial -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_USE_PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 6792 bytes FLASH -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 -D LEDPIN=16 @@ -589,7 +808,7 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME -D HW_PIN_SCL=35 -D HW_PIN_SDA=33 -D HW_PIN_CLOCKSPI=7 - -D HW_PIN_DATASPI=11 + -D HW_PIN_MOSISPI=11 ;WLEDMM renamed from HW_PIN_DATASPI -D HW_PIN_MISOSPI=9 ; -D STATUSLED=15 lib_deps = ${esp32s2.lib_deps} @@ -615,29 +834,32 @@ upload_speed = 115200 lib_deps = ${esp32c3.lib_deps} board_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv board_build.flash_mode = dio - -[env:wemos_shield_esp32] -board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} -upload_speed = 460800 -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} - -D LEDPIN=16 - -D RLYPIN=19 - -D BTNPIN=17 - -D IRPIN=18 - -D UWLED_USE_MY_CONFIG - -D USERMOD_DALLASTEMPERATURE - -D USERMOD_FOUR_LINE_DISPLAY - -D TEMPERATURE_PIN=23 - -D USE_ALT_DISPlAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI - -D USERMOD_AUDIOREACTIVE -lib_deps = ${esp32.lib_deps} - OneWire@~2.3.5 - olikraus/U8g2 @ ^2.28.8 - https://github.com/blazoncek/arduinoFFT.git -board_build.partitions = ${esp32.default_partitions} +board_upload.flash_size = 2MB +board_upload.maximum_size = 2097152 + +;WLEDMM: see below +; [env:wemos_shield_esp32] +; board = esp32dev +; platform = ${esp32.platform} +; platform_packages = ${esp32.platform_packages} +; upload_speed = 460800 +; build_unflags = ${common.build_unflags} +; build_flags = ${common.build_flags_esp32} +; -D LEDPIN=16 +; -D RLYPIN=19 +; -D BTNPIN=17 +; -D IRPIN=18 +; -U WLED_USE_MY_CONFIG +; -D USERMOD_DALLASTEMPERATURE +; -D USERMOD_FOUR_LINE_DISPLAY +; -D TEMPERATURE_PIN=23 +; -D USE_ALT_DISPlAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI +; -D USERMOD_AUDIOREACTIVE +; lib_deps = ${esp32.lib_deps} +; OneWire@~2.3.5 +; olikraus/U8g2 @ ^2.28.8 +; https://github.com/blazoncek/arduinoFFT.git +; board_build.partitions = ${esp32.default_partitions} [env:m5atom] board = esp32dev @@ -778,3 +1000,1812 @@ lib_deps = ${esp32.lib_deps} TFT_eSPI @ ^2.3.70 board_build.partitions = ${esp32.default_partitions} + + + + + + + +# ------------------------------------------------------------------------------ +# MoonModules environments +# see https://mm.kno.wled.ge/moonmodules/platformio-entries/ +# ------------------------------------------------------------------------------ + +; shared build flags and lib deps for minimum and maximum environment +[common_mm] + +build_disable_sync_interfaces = + -D WLED_DISABLE_LOXONE + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_MQTT + -D WLED_DISABLE_INFRARED + -D WLED_DISABLE_ADALIGHT ;; WLEDMM this board does not have a serial-to-USB chip. Better to disable serial protocols, to avoid crashes (see upstream #3128) + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only + +AR_build_flags = -D USERMOD_AUDIOREACTIVE -D UM_AUDIOREACTIVE_USE_NEW_FFT ;; WLEDMM audioreactive usermod, licensed under EUPL-1.2 +AR_lib_deps = https://github.com/softhack007/arduinoFFT.git#develop @ 1.9.2 ;; used for USERMOD_AUDIOREACTIVE - optimized version, 10% faster on -S2/-C3 + +animartrix_build_flags = -D USERMOD_ANIMARTRIX ;; WLEDMM usermod: CC BY-NC 3.0 licensed effects by Stefan Petrick +animartrix_lib_deps = https://github.com/netmindz/animartrix.git#bd556cef92a6fee9777c0a7304c9de1a84cba184 ;; init state fix +animartrix_lib_ignore = animartrix ;; to remove the animartrix lib dependancy (saves a few bytes) + +DMXin_build_flags = -D WLED_ENABLE_DMX_INPUT ;; WLEDMM DMX physical input - requires ESP-IDF v4.4.x +DMXin_lib_deps = https://github.com/someweisguy/esp_dmx.git#47db25d ;; for DMX_INPUT +DMXin_lib_ignore = esp_dmx ;; to remove the esp-dmx lib dependancy (saves a few bytes) + +HUB75_build_flags = + -D WLED_ENABLE_HUB75MATRIX ;; - requires ESP-IDF v4.4.x + ;-D SPIRAM_FRAMEBUFFER ;; ONLY SUPPORTED ON ESP32-S3 VARIANTS WITH OCTAL (not quad) SPIRAM/PSRAM + -D NO_GFX ;; Disable the use of Adafruit_GFX by the HUB75 driver + -D NO_FAST_FUNCTIONS ;; If you are not using AdafruitGFX than you probably do not need this either, save memory/code size + -D NO_CIE1931 ;; Do not use LED brightness compensation described in CIE 1931. We use FastLED dimming already + -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips + -D WLEDMM_SLOWPATH ;; WLEDMM: do not use I2S for driving ws2812 LEDs (HUB75 driver needs I2S#1) +;; HUB75_lib_deps = https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a9 ;; S3_LCD_DIV_NUM fix +HUB75_lib_deps = https://github.com/softhack007/ESP32-HUB75-MatrixPanel-DMA_sh7.git#fix_dangling_pointer ;; S3 bugfix for crash in ~MatrixPanel_I2S_DMA() +HUB75_lib_ignore = ESP32 HUB75 LED MATRIX PANEL DMA Display ;; to remove the HUB75 lib dependancy (saves a few bytes) + +NetDebug_build_flags = + ;; WLEDMM: only setting WLED_DEBUG_HOST is enough, ip and port can be defined in sync settings as well + -D WLED_DEBUG_HOST='"192.168.x.x"' ;; to send debug messages over network to host 192.168.x.y - FQDN is also possible + -D WLED_DEBUG_PORT=1768 ;; port for network debugging. default = 7868 + + +build_flags_S = + -Wall -Wformat -Woverflow -Wuninitialized -Winit-self -Warray-bounds ; enables more warnings + -Wno-attributes -Wno-unused-variable -Wno-unused-function -Wno-deprecated-declarations ;disables some stupid warnings + ;-D WLED_DISABLE_BROWNOUT_DET ; enable if you get "brownout detected" errors at startup + -D WLED_USE_MY_CONFIG + ; -D WLED_DISABLE_2D ;; un-comment to build a firmware without 2D matrix support + ; -D WLED_USE_CIE_BRIGHTNESS_TABLE ;; experimental: use different color / brightness lookup table + ${common_mm.AR_build_flags} ; use latest (upstream) FFTLib, instead of older library modified by blazoncek. Slightly faster, more accurate, needs 2KB RAM extra + -D USERMOD_AUTO_PLAYLIST + ; -D USERMOD_ARTIFX ;; WLEDMM usermod - temporarily moved into "_M", due to problems in "_S" when compiling with -O2 + -D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + ; -D WLED_DEBUG_HEAP ;; WLEDMM enable heap debugging + +lib_deps_S = + ;; https://github.com/kosme/arduinoFFT#develop @ 1.9.2+sha.419d7b0 ;; used for USERMOD_AUDIOREACTIVE - using "known working" hash + ${common_mm.AR_lib_deps} ;; used for USERMOD_AUDIOREACTIVE - optimized version, 10% faster on -S2/-C3 + +build_flags_M = + -D USERMOD_ARTIFX ; WLEDMM usermod - temporarily moved into "_M", due to problems in "_S" when compiling with -O2 + -D WLED_MAX_USERMODS=25 ; default only 4-6, also for _XL configs takes 25 pointers in memory + ;; -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4 ewowi to softhack: move to build_flags_S? - We need a different solution + -D WLED_USE_MY_CONFIG ; include custom my_config.h ewowi to softhack: redundant as also in build_flags_S? + -D USERMOD_DALLASTEMPERATURE + -D USE_ALT_DISPLAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI + -D USERMOD_FOUR_LINE_DISPLAY + -D USERMOD_ROTARY_ENCODER_UI + -D USERMOD_AUTO_SAVE + ${common_mm.animartrix_build_flags} + ${common_mm.NetDebug_build_flags} + +lib_deps_M = + ;https://github.com/blazoncek/OneWire.git ; includes bugfixes for inconsistent readings + OneWire@~2.3.5 ; used for USERMOD_FOUR_LINE_DISPLAY and USERMOD_DALLASTEMPERATURE + olikraus/U8g2 @ ^2.28.8 ; used for USERMOD_FOUR_LINE_DISPLAY + ${common_mm.animartrix_lib_deps} + +lib_deps_V4_M = + ;https://github.com/blazoncek/OneWire.git ; includes bugfixes for inconsistent readings + paulstoffregen/OneWire@ ^2.3.7 ; used for USERMOD_DALLASTEMPERATURE -> need newer release with bugfixes for -S3; still requires TEMPERATURE_PIN < 46 + olikraus/U8g2@ ^2.34.5 ; used for USERMOD_FOUR_LINE_DISPLAY -> need newer version with bugfixes for arduino-esp32 v2.0.4 (Wire initialization) + ${common_mm.animartrix_lib_deps} + +build_flags_XL = + -D WLEDMM_SAVE_FLASH + -D USERMOD_WEATHER ; WLEDMM usermod + -D USERMOD_MPU6050_IMU ; gyro/accelero for USERMOD_GAMES (ONLY WORKS IF USERMOD_FOUR_LINE_DISPLAY NOT INCLUDED - I2C SHARING BUG) + -D USERMOD_GAMES ; WLEDMM usermod + -D USERMOD_BATTERY ;; enable Battery usermod + -D USERMOD_BATTERY_USE_LIPO ;; use new "discharging curve" for LiPo cells + -D USERMOD_BH1750 + -D USERMOD_ANIMATED_STAIRCASE + -D USERMOD_RTC ;; experimental + ; -D USERMOD_SENSORSTOMQTT ;; experimental ewowi causes error: fatal error: Adafruit_Sensor.h: No such file or directory + -D USERMOD_ANALOG_CLOCK + -D USERMOD_MULTI_RELAY + -D USERMOD_PIRSWITCH + -D USERMOD_PWM_FAN + ; -D USERMOD_PING_PONG_CLOCK ;; Removed as dots is confusing + ; -D USERMOD_BUZZER ;; Removed as it grabs gpio32 (pin needed by audioreactive) + -D USERMOD_SN_PHOTORESISTOR + -D USERMOD_BME280 + -D USERMOD_DHT + ; -D USERMOD_SHT ;; experimental + -D USERMOD_VL53L0X_GESTURES + -D WLED_ENABLE_PIXART + ${common_mm.animartrix_build_flags} + +lib_deps_XL = + ElectronicCats/MPU6050 @ 0.6.0 ; used for USERMOD_MPU6050_IMU + claws/BH1750 @^1.2.0 ; used for USERMOD_BH1750 + ; adafruit/Adafruit BMP280 Library @ 2.1.0 ;; experimental for usermod USERMOD_SENSORSTOMQTT + ; adafruit/Adafruit CCS811 Library @ 1.0.4 ;; experimental for usermod USERMOD_SENSORSTOMQTT + ; adafruit/Adafruit Si7021 Library @ 1.4.0 ;; experimental for usermod USERMOD_SENSORSTOMQTT + BME280@~3.0.0 ; for usermod USERMOD_BME280 + https://github.com/alwynallan/DHT_nonblocking ; for usermod USERMOD_DHT + ; robtillaart/SHT85 @ ~0.4.0 ;; for usermod USERMOD_SHT + pololu/VL53L0X @ ^1.3.0 ; for usermod USERMOD_VL53L0X_GESTURES + +; end of common + +; base entries (without WLED_RELEASE_NAME) + +; common defaults for all MM environments +[esp32_4MB_S_base] +board = esp32dev +platform = ${esp32.platform} +platform_packages = ${esp32.platform_packages} +upload_speed = 460800 ; or 921600 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp32} ${common_mm.build_flags_S} ${common_mm.build_disable_sync_interfaces} +lib_deps = ${esp32.lib_deps} ${common_mm.lib_deps_S} +board_build.partitions = ${esp32.default_partitions} +board_build.f_flash = 80000000L ; use full 80MHz speed for flash (default = 40Mhz) +board_build.flash_mode = dio ; (dio = dual i/o; more compatible than qio = quad i/o) +monitor_filters = esp32_exception_decoder + +;common default for all max environments +[esp32_4MB_M_base] +extends = esp32_4MB_S_base +build_flags = ${common.build_flags_esp32} ${common_mm.build_flags_S} ${common_mm.build_flags_M} ;; we don't want common_mm.build_disable_sync_interfaces, so we cannot inherit from esp32_4MB_S_base +build_unflags = ${esp32_4MB_S_base.build_unflags} +lib_deps = ${esp32_4MB_S_base.lib_deps} ${common_mm.lib_deps_M} + +[esp32_4MB_XL_base] +extends = esp32_4MB_M_base +build_flags = ${esp32_4MB_M_base.build_flags} ${common_mm.build_flags_XL} +build_unflags = ${esp32_4MB_M_base.build_unflags} +lib_deps = ${esp32_4MB_M_base.lib_deps} ${common_mm.lib_deps_XL} + +;common default for all V4 min environments, including -S3, -S2, -C3 +[esp32_4MB_V4_S_base] +board = esp32dev +upload_speed = 460800 ; or 921600 +platform = ${esp32.platformV4} +platform_packages = ${esp32.platformV4_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${common_mm.build_flags_S} ${common_mm.build_disable_sync_interfaces} ;; do not include ${esp32.build_flagsV4} here !!!! + -Wno-misleading-indentation -Wno-format-truncation + -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one + ;-Wstack-usage=2732 ;; warn if a function needs more that 30% of availeable stack ("stack usage might be unbounded", "stack usage is 2824 bytes") + ;-Wsuggest-attribute=const -Wsuggest-attribute=pure ;; ask compiler for hints on attributes + ${common_mm.DMXin_build_flags} +lib_deps = ${common_mm.lib_deps_S} ;; do not include ${esp32.lib_depsV4} here !!!! + ${common_mm.DMXin_lib_deps} +esp32_build_flags = ${esp32.build_flagsV4} ${esp32_4MB_V4_S_base.build_flags} ;; this is for esp32 only, including specific "V4" flags +esp32_lib_deps = ${esp32.lib_depsV4} ${esp32_4MB_V4_S_base.lib_deps} ;; this is for esp32 only, including specific "V4" flags +board_build.partitions = ${esp32.default_partitions} +board_build.f_flash = 80000000L ; use full 80MHz speed for flash (default = 40Mhz) +board_build.flash_mode = dio ; (dio = dual i/o; more compatible than qio = quad i/o) +;lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; monitor_filters = esp32_exception_decoder ; used to show crash details + +[esp32_4MB_V4_M_base] +extends = esp32_4MB_V4_S_base +build_flags = + ${common.build_flags} ${common_mm.build_flags_S} ;; do not include ${esp32.build_flagsV4} here !!!! + -Wno-misleading-indentation -Wno-format-truncation + -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one + ;-Wstack-usage=2732 ;; warn if a function needs more that 30% of availeable stack ("stack usage might be unbounded", "stack usage is 2824 bytes") + ;-Wsuggest-attribute=const -Wsuggest-attribute=pure ;; ask compiler for hints on attributes + -D WLED_ENABLE_DMX_INPUT + ${common_mm.build_flags_M} ;; generic, for all variants +lib_deps = ${esp32_4MB_V4_S_base.lib_deps} ${common_mm.lib_deps_V4_M} +esp32_build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags} ${common_mm.build_flags_M} ;; for esp32 only, including specific "V4" flags +esp32_lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} ${common_mm.lib_deps_V4_M} +board_build.partitions = ${esp32_4MB_V4_S_base.board_build.partitions} +;board_build.flash_mode = qio ; (dio = dual i/o; more compatible than qio = quad i/o) + +[Shield_ICS4343x] +build_flags = + -D SR_DMTYPE=1 -D I2S_SDPIN=32 -D I2S_WSPIN=15 -D I2S_CKPIN=14 ; for regular I2S microphone + -D SR_SQUELCH=10 -D SR_GAIN=30 -D SR_FREQ_PROF=5 ; ICS-43434 specific + +[Shield_SPM1423] +build_flags = + -D SR_DMTYPE=5 -D I2S_SDPIN=32 -D I2S_WSPIN=15 -D I2S_CKPIN=-1 ; for I2S PDM microphone + -D SR_SQUELCH=3 -D SR_GAIN=75 -D SR_FREQ_PROF=7 ; SPM1423 specific + +[Athom_PDMmic] +build_flags = + -D SR_DMTYPE=51 -D I2S_SDPIN=32 -D I2S_WSPIN=15 -D I2S_CKPIN=-1 ; for I2S PDM microphone Legacy mode! + -D SR_SQUELCH=10 -D SR_GAIN=40 -D SR_FREQ_PROF=7 ; SPM1423 specific + +[Shield_LineIn] +build_unflags = + -D BTNPIN=17 ;; remove - its in conflict with on-shield rotary + -D ENCODER_SW_PIN=5 ;; remove - its in conflict with on-shield rotary + -D BTNPIN=0 ;; remove - its in conflict with MCLK pin +build_flags = + -D BTNPIN=-1 + -D SR_DMTYPE=4 -D MCLK_PIN=0 -D I2S_SDPIN=26 -D I2S_WSPIN=25 -D I2S_CKPIN=27 ; for audio Line-In shield, final version + -D SR_LINE_DETECT=34 ; line-in plug insert detection sensor (future support) + -D SR_SQUELCH=8 -D SR_GAIN=40 -D SR_FREQ_PROF=1 ; CS5343 Line-In specific + -D ENCODER_DT_PIN=35 -D ENCODER_CLK_PIN=39 -D ENCODER_SW_PIN=17 ; on-shield rotary encoder + +[Debug_Flags] +build_flags = + -DDEBUG=1 ;; enable some debug assertions in the platform core + ; -D WLED_DEBUG_HEAP ;; WLEDMM enable heap debugging + -D WLED_DEBUG ; lots of generic debug messages + -D SR_DEBUG ; some extra debug messages from audioreactive + ; -D MIC_LOGGER ; for sound input monitoring & debugging (use arduino serial plotter) + ; NetDebug moved to build_flags_M + +[Speed_Flags] +build_flags = + -O2 ;; optimize for performance instead of size + ;-ffast-math ;; gives a few (2-5) percent speedup on ESP32-S3, but causes slight slowdown on classic ESP32 + -funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (10% faster on -S3) + ;; -mtarget-align ;; this one is included in -O2, so its not necessary to explicitly add it + -free -fipa-pta ;; these are very useful, too + -fno-jump-tables -fno-tree-switch-conversion ;; needed + -freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed +build_flags_V4 = ${Speed_Flags.build_flags} -fsingle-precision-constant ;; old framework does not like this flag +build_unflags = + -Os ;; to disable standard optimization for size + +; end of base entries + + +; bin entries (with WLED_RELEASE_NAME) +;; ESP32, 4MB, optimized for speed, basic features and audioreactive only +[env:esp32_4MB_S] +extends = esp32_4MB_S_base +build_unflags = ${esp32_4MB_S_base.build_unflags} + ${Speed_Flags.build_unflags} ;; to override -Os +build_flags = ${esp32_4MB_S_base.build_flags} + ${Speed_Flags.build_flags} ;; -O2 -> optimize for speed instead of size + -D WLED_RELEASE_NAME=esp32_4MB_S + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + ;-D WLEDMM_SAVE_FLASH + ;WLEDMM: disable the next line if you don't need "net Debug". It will free ~2% of flash + ${common_mm.NetDebug_build_flags} +;; optimized-for-speed build +; RAM: [== ] 23.6% (used 77188 bytes from 327680 bytes) +; Flash: [========= ] 93.8% (used 1475997 bytes from 1572864 bytes) + +[env:esp32_4MB_M] +extends = esp32_4MB_M_base +build_flags = ${esp32_4MB_M_base.build_flags} + -D WLED_RELEASE_NAME=esp32_4MB_M + -D WLEDMM_SAVE_FLASH +; RAM: [== ] 24.2% (used 79196 bytes from 327680 bytes) +; Flash: [==========] 97.4% (used 1532425 bytes from 1572864 bytes) + +[env:esp32_4MB_M_eth] +extends = esp32_4MB_M_base +build_flags = ${esp32_4MB_M_base.build_flags} + -D WLED_RELEASE_NAME=esp32_4MB_M_eth + -D WLED_USE_ETHERNET + -D RLYPIN=-1 -D BTNPIN=-1 ;; Prevent clash + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only + -D WLEDMM_SAVE_FLASH +; RAM: [== ] 24.2% (used 79404 bytes from 327680 bytes) +; Flash: [==========] 97.8% (used 1538025 bytes from 1572864 bytes) + +[env:esp32_4MB_XL] +extends = esp32_4MB_XL_base +build_flags = ${esp32_4MB_XL_base.build_flags} + -D WLED_RELEASE_NAME=esp32_4MB_XL + ;-D WLED_DISABLE_ALEXA + ;-D WLED_DISABLE_HUESYNC ;; Over the limits ? + -D WLED_DISABLE_LOXONE ;; Over the limits + ;-D WLED_DISABLE_MQTT + ;-D WLED_DISABLE_INFRARED + -D WLEDMM_SAVE_FLASH ;; a humble attempt to save a few extra bytes +build_unflags = ${esp32_4MB_XL_base.build_unflags} +board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv +; RAM: [== ] 24.5% (used 80140 bytes from 327680 bytes) +; Flash: [========= ] 85.8% (used 1630789 bytes from 1900544 bytes) + +;; standard framework build for 16MB flash, optimized for speed +[env:esp32_16MB_S] +extends = esp32_4MB_S_base +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +build_unflags = ${esp32_4MB_S_base.build_unflags} + ${Speed_Flags.build_unflags} ;; to override -Os +build_flags = ${esp32_4MB_S_base.build_flags} + -D WLED_RELEASE_NAME=esp32_16MB_S + ${Speed_Flags.build_flags} ;; optimize for speed instead of size + -D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + ${common_mm.animartrix_build_flags} +lib_deps = ${esp32_4MB_S_base.lib_deps} + ${common_mm.animartrix_lib_deps} +; lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +;; optimized-for-speed build +; RAM: [== ] 23.7% (used 77524 bytes from 327680 bytes) +; Flash: [======= ] 72.9% (used 1528541 bytes from 2097152 bytes) + +[env:esp32_16MB_M] +extends = esp32_4MB_M_base +build_flags = ${esp32_4MB_M_base.build_flags} + -D WLED_RELEASE_NAME=esp32_16MB_M +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +;lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; RAM: [== ] 24.2% (used 79196 bytes from 327680 bytes) +; Flash: [======= ] 73.6% (used 1542905 bytes from 2097152 bytes) + +[env:esp32_4MB_M_debug] +extends = esp32_4MB_M_base +build_unflags = ${common.build_unflags} + -D CORE_DEBUG_LEVEL=0 + -D USERMOD_ANIMARTRIX ;; Tips our memory usage over the limit + +build_flags = ${esp32_4MB_M_base.build_flags} + ${Debug_Flags.build_flags} + -D CORE_DEBUG_LEVEL=2 ;; 2=warning + -D WLED_RELEASE_NAME=esp32_4MB_M_debug +monitor_filters = esp32_exception_decoder +; RAM: [== ] 24.1% (used 79060 bytes from 327680 bytes) +; Flash: [==========] 99.3% (used 1561969 bytes from 1572864 bytes) + +[env:esp32_16MB_M_debug] +extends = esp32_4MB_M_base +build_unflags = ${common.build_unflags} + -D CORE_DEBUG_LEVEL=0 + -D NDEBUG +build_flags = ${esp32_4MB_M_base.build_flags} + ${Debug_Flags.build_flags} + ; -D WLED_DEBUG_HEAP ;; WLEDMM enable heap debugging (needs newer framework versions) + -D CORE_DEBUG_LEVEL=2 ;; 2=warning + -D WLED_RELEASE_NAME=esp32_16MB_M_debug +monitor_filters = esp32_exception_decoder +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +; RAM: [== ] 24.3% (used 79468 bytes from 327680 bytes) +; Flash: [======== ] 76.7% (used 1609205 bytes from 2097152 bytes) + +[env:esp32_16MB_XL] +extends = esp32_4MB_XL_base +build_flags = ${esp32_4MB_XL_base.build_flags} + -D WLED_RELEASE_NAME=esp32_16MB_XL +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +;lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; RAM: [== ] 24.5% (used 80140 bytes from 327680 bytes) +; Flash: [======== ] 77.8% (used 1631929 bytes from 2097152 bytes) + +[env:esp32_16MB_M_eth] +extends = esp32_4MB_M_base +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +board = esp32_16MB-poe ;; needed for ethernet boards (selects "esp32-poe" as variant) +build_flags = ${esp32_4MB_M_base.build_flags} + -D WLED_RELEASE_NAME=esp32_16MB_M_eth ; This will be included in the firmware.bin filename + -D WLED_USE_ETHERNET + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only +; RAM: [== ] 24.2% (used 79388 bytes from 327680 bytes) +; Flash: [======= ] 73.8% (used 1548525 bytes from 2097152 bytes) + + +[env:esp8266_2MB_S] +extends = env:esp8266_2m +upload_speed = 460800 ;115200 +board_build.f_cpu = 160000000L ;; we want 160Mhz (default = 80Mhz) +build_flags = ${common.build_flags_esp8266} + -D WLED_RELEASE_NAME=esp8266_2MB_S + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_ESPNOW ;; might help in case of WiFi connectivity problems + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + ;; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + ;; -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes + ; -D WLED_DISABLE_2D + ; -UWLED_USE_MY_CONFIG + ${common_mm.NetDebug_build_flags} + ; -D WLED_DEBUG +; monitor_filters = esp8266_exception_decoder +;; lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; RAM: [====== ] 60.8% (used 49836 bytes from 81920 bytes) +; Flash: [======== ] 83.3% (used 869783 bytes from 1044464 bytes) + +[env:esp8266_4MB_S] +extends = env:d1_mini +upload_speed = 460800 ;115200 +board_build.f_cpu = 160000000L ;; we want 160Mhz (default = 80Mhz) +build_flags = ${common.build_flags_esp8266} + -D WLED_RELEASE_NAME=esp8266_4MB_S + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_ESPNOW ;; might help in case of WiFi connectivity problems + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + ;; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + ;; -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes + ; -D WLED_DISABLE_2D + ; -UWLED_USE_MY_CONFIG + ${common_mm.NetDebug_build_flags} + ; -D WLED_DEBUG +; monitor_filters = esp8266_exception_decoder +;; lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; RAM: [====== ] 60.8% (used 49824 bytes from 81920 bytes) +; Flash: [======== ] 83.3% (used 869779 bytes from 1044464 bytes) +[env:esp8266_4MB_M] +extends = env:d1_mini +upload_speed = 460800 ;115200 +board_build.f_cpu = 160000000L ;; we want 160Mhz (default = 80Mhz) +build_flags = ${common.build_flags_esp8266} + -D WLED_RELEASE_NAME=esp8266_4MB_M + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_LOXONE + ; -D USERMOD_AUDIOREACTIVE + ; -UWLED_USE_MY_CONFIG + ; -D USERMOD_PIRSWITCH + ; -D USERMOD_DALLASTEMPERATURE ;; disabled because it hangs during usermod setup on -S3 (autodetect broken?) + ; -D USERMOD_MULTI_RELAY + -D USE_ALT_DISPLAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI + -D WLED_MAX_USERMODS=5 ; default only 4 on ESP8266 + -D USERMOD_FOUR_LINE_DISPLAY + -D USERMOD_MPU6050_IMU ; gyro/accelero for USERMOD_GAMES (ONLY WORKS IF USERMOD_FOUR_LINE_DISPLAY NOT INCLUDED - I2C SHARING BUG) + -D USERMOD_GAMES ; WLEDMM usermod + ; -D USERMOD_ARTIFX ; this is compiling but not working due to low memory on 8266 + -D USERMOD_BATTERY ;; enable Battery usermod + -D USERMOD_BATTERY_USE_LIPO ;; use new "discharging curve" for LiPo cells + ${common_mm.NetDebug_build_flags} + ; -D WLED_DEBUG +monitor_filters = esp8266_exception_decoder +lib_deps = ${esp8266.lib_deps} + OneWire@~2.3.5 ; used for USERMOD_FOUR_LINE_DISPLAY and USERMOD_DALLASTEMPERATURE + olikraus/U8g2 @ ^2.28.8 ; used for USERMOD_FOUR_LINE_DISPLAY + ElectronicCats/MPU6050 @ 0.6.0 ; used for USERMOD_MPU6050_IMU +; RAM: [====== ] 63.0% (used 51632 bytes from 81920 bytes) +; Flash: [========= ] 88.5% (used 924179 bytes from 1044464 bytes) + +; Blaz env (for reference purposes) +[env:d1_mini_temp] +extends = env:d1_mini +board_build.filesystem = littlefs +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_LOXONE + -D WLED_DISABLE_AUDIO ;WLEDMM not used anywhere + -D WLED_ENABLE_SIMPLE_UI + -D USERMOD_FOUR_LINE_DISPLAY + -D USE_ALT_DISPlAY + -D USERMOD_DALLASTEMPERATURE + -D TEMPERATURE_PIN=13 # (D7) + -D LEDPIN=2 # (D4) + -D RLYPIN=12 # (D6) + -D BTNPIN=0 # (D3) + -D IRPIN=14 # (D5) + -D USERMOD_MULTI_RELAY + -D MULTI_RELAY_MAX_RELAYS=2 + -D USERMOD_PIRSWITCH + -D PIR_SENSOR_PIN=16 + -D PIR_SENSOR_OFF_SEC=60 + -UWLED_USE_MY_CONFIG +lib_deps = ${esp8266.lib_deps} + paulstoffregen/OneWire@~2.3.7 ;WLEDMM Softhack, we need this as well (instead of 2.3.5)? + olikraus/U8g2 # @~2.33.15 + Wire ; WLEDMM ewowi, needed? + +[env:esp8266pro_16MB_S] +extends = env:d1_mini +board = d1_mini_pro ;; "D1 mini pro": ESP8266EX, 160MHz, 80KB RAM, 16MB Flash +board_build.f_cpu = 160000000L ;; we want 160Mhz (default = 80Mhz) +;board_build.f_flash = 80000000L ;; for 80Mhz flash speed, in case your chip can handle it (default = 40Mhz) +board_build.flash_mode = qio ;; quad IO - fastest speed, in case your chip can handle it. +;;board_build.flash_mode = dout ;; use if your esp8266 becomes unstable with "qio" +board_build.ldscript = ${common.ldscript_16m14m} ;; 16MB flash, use 14MB for LittleFS + +upload_speed = 460800 ;115200 +build_flags = ${common.build_flags_esp8266} + -D WLED_RELEASE_NAME=esp8266pro_16MB_S + -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + ; -D WLED_DEBUG ${common.debug_flags} ;; un-comment for debug messages + ${common_mm.NetDebug_build_flags} + ;; -D WLED_DISABLE_ESPNOW ;; might help in case of WiFi connectivity problems + ; -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + ; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + ; -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes ewowi: disabled to stay below 100% + ; -D WLED_DISABLE_2D + ; -D USERMOD_AUDIOREACTIVE + ; -D USERMOD_ARTIFX ; to be done + ; -UWLED_USE_MY_CONFIG +monitor_filters = esp8266_exception_decoder +; lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; RAM: [====== ] 59.3% (used 48616 bytes from 81920 bytes) +; Flash: [======== ] 77.0% (used 804236 bytes from 1044464 bytes) + +[env:esp8266pro_16MB_M] +extends = env:d1_mini +board = d1_mini_pro ;; "D1 mini pro": ESP8266EX, 160MHz, 80KB RAM, 16MB Flash +board_build.f_cpu = 160000000L ;; we want 160Mhz (default = 80Mhz) +;board_build.f_flash = 80000000L ;; for 80Mhz flash speed, in case your chip can handle it (default = 40Mhz) +board_build.flash_mode = qio ;; quad IO - fastest speed, in case your chip can handle it. +;;board_build.flash_mode = dout ;; use if your esp8266 becomes unstable with "qio" +board_build.ldscript = ${common.ldscript_16m14m} ;; 16MB flash, use 14MB for LittleFS + +upload_speed = 460800 ;115200 +build_flags = ${common.build_flags_esp8266} + -D WLED_RELEASE_NAME=esp8266pro_16MB_M + -D WLED_MAX_USERMODS=5 ; default only 4-6 + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_LOXONE + ; -D USERMOD_AUDIOREACTIVE + ; -D USERMOD_ARTIFX ; to be done + ;; -D WLED_DISABLE_ESPNOW ;; might help in case of WiFi connectivity problems + -D USERMOD_PIRSWITCH + -D USERMOD_DALLASTEMPERATURE ;; disabled because it hangs during usermod setup on -S3 (autodetect broken?) + -D USERMOD_MULTI_RELAY + -D USE_ALT_DISPLAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI + -D USERMOD_FOUR_LINE_DISPLAY + -D USERMOD_MPU6050_IMU ; gyro/accelero for USERMOD_GAMES (ONLY WORKS IF USERMOD_FOUR_LINE_DISPLAY NOT INCLUDED - I2C SHARING BUG) + -D USERMOD_GAMES ; WLEDMM usermod + ${common_mm.NetDebug_build_flags} + ; -D WLED_DEBUG +monitor_filters = esp8266_exception_decoder +lib_deps = ${esp8266.lib_deps} + OneWire@~2.3.5 ; used for USERMOD_FOUR_LINE_DISPLAY and USERMOD_DALLASTEMPERATURE + olikraus/U8g2 @ ^2.28.8 ; used for USERMOD_FOUR_LINE_DISPLAY + ElectronicCats/MPU6050 @ 0.6.0 ; used for USERMOD_MPU6050_IMU +; RAM: [====== ] 63.8% (used 52272 bytes from 81920 bytes) +; Flash: [========= ] 90.4% (used 944487 bytes from 1044464 bytes) + +[env:esp01_1MB_S] +board = esp01_1m +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_1m128k} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA + -D WLED_RELEASE_NAME=esp01_1MB_S + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_ESPNOW ;; exceeds flash size limits + -D WLED_DISABLE_INFRARED ;; exceeds flash size limits +lib_deps = ${esp8266.lib_deps} +lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; RAM: [====== ] 60.6% (used 49616 bytes from 81920 bytes) +; Flash: [==========] 99.8% (used 890835 bytes from 892912 bytes) + + +# ------------------------------------------------------------------------------ +# MoonModules environments for IDF V4.4.x +# ------------------------------------------------------------------------------ +;; Warning: the build-in LittleFS (arduino-esp32 2.0.x) seems to be "slightly different" from Lorol LittleFS. +;; When upgrading to the new framework, it might be necessary to first do a chip erase (make sure you have a backup of cfg.json and presets.json) + +; compiled with ESP-IDF 4.4.1; HUB75 supported +[env:esp32_4MB_V4_S] +extends = esp32_4MB_V4_S_base +build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags} + -D WLED_RELEASE_NAME=esp32_4MB_V4_S + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLEDMM_SAVE_FLASH + -D WLED_DISABLE_LOXONE + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_MQTT + -D WLED_DISABLE_INFRARED + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER + ${common_mm.HUB75_build_flags} +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} + ${common_mm.HUB75_lib_deps} +lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +; RAM: [=== ] 25.5% (used 83400 bytes from 327680 bytes) +; Flash: [==========] 97.3% (used 1530013 bytes from 1572864 bytes) + +;; softhack007: my favourite HUB75 buildenv - fastest possible +[env:esp32_4MB_V4_HUB75_forum] +extends = esp32_4MB_V4_S_base +platform_packages = ${esp32_4MB_V4_S_base.platform_packages} +;; toolchain-xtensa-esp32@~11.2 ;; for trying out newer gcc @11.2.0+2022r1 ;; 25fps -> 30fps;; FastLED 3.7.1 required !! + +build_unflags = ${esp32_4MB_V4_S_base.build_unflags} + ${Speed_Flags.build_unflags} ;; to override -Os + ;; -D WLED_ENABLE_DMX_INPUT + -DARDUINO_EVENT_RUNNING_CORE=1 + ;;-DCORE_DEBUG_LEVEL=0 + ;; -D NDEBUG + ;; -D NO_CIE1931 ;; Do use LED brightness compensation described in CIE 1931 + +build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags} + ;; -Wsuggest-attribute=const -Wsuggest-attribute=pure + ;; -Wmissing-noreturn -Wmissing-return + ;; -Wall -Wextra + -Wno-unused-value -Wno-format -Wno-type-limits + -D WLED_RELEASE_NAME=esp32_4MB_V4_HUB75_forum + ${Speed_Flags.build_flags_V4} ;; -O2 -> optimize for speed instead of size + ;; -D DEBUG + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLED_DISABLE_LOXONE + -D WLED_DISABLE_ALEXA + -D WLED_DISABLE_HUESYNC + -D WLED_DISABLE_MQTT + -D WLED_DISABLE_INFRARED + -D WLED_DISABLE_ADALIGHT + ;; -D WLED_DEBUG + ;; -D SR_DEBUG + -D WLED_BOOTUPDELAY=350 + ${common_mm.HUB75_build_flags} + -DESP32_FORUM_PINOUT + ${common_mm.animartrix_build_flags} + ;;-DARDUINO_EVENT_RUNNING_CORE=0 ;; assign Wifi to core0, to have more CPU on core#1 (arduino loop) + ;;-DARDUINO_RUNNING_CORE=1 ;; should be default, but does not hurt + -DCONFIG_MBEDTLS_DYNAMIC_BUFFER=1 ;; optional - seems to move more buffers into PSRAM + ;; -D CONFIG_ESP32_REV_MIN=3 ;; disables PSRAM bug workarounds in the core, reducing the code size and improving overall performance. + -D JSON_BUFFER_SIZE=18432 -D MIN_HEAP_SIZE=6144 + -D MAX_SEGMENT_DATA=40960 ;; default 32767 + -D WLEDMM_SAVE_FLASH + -D WLED_DISABLE_BROWNOUT_DET + ;;-DCORE_DEBUG_LEVEL=1 +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} +lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + ;; ${common_mm.DMXin_lib_ignore} + +board_build.partitions = ${esp32.big_partitions} +board_build.f_flash = 80000000L ; use full 80MHz speed for flash (default = 40Mhz) +board_build.flash_mode = qio ; (dio = dual i/o; more compatible than qio = quad i/o) +; RAM: [== ] 23.7% (used 77720 bytes from 327680 bytes) +; Flash: [========= ] 93.1% (used 1768897 bytes from 1900544 bytes) + +; compiled with ESP-IDF 4.4.1 +[env:esp32_4MB_V4_M] +extends = esp32_4MB_V4_M_base +build_flags = ${esp32_4MB_V4_M_base.esp32_build_flags} + -D WLED_RELEASE_NAME=esp32_4MB_V4_M + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + -D WLED_DISABLE_HUESYNC ;RAM 122 bytes; FLASH 6308 bytes + -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes ;; softhack007 disabled to stay below 100% flash size + -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes ;; softhack007 disabled to stay below 100% flash size + -D WLEDMM_SAVE_FLASH +lib_deps = ${esp32_4MB_V4_M_base.esp32_lib_deps} +lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + ${common_mm.HUB75_lib_ignore} ;; over the flash size limit + ${common_mm.animartrix_lib_ignore} +build_unflags = ${esp32_4MB_V4_M_base.build_unflags} + -D USERMOD_ANIMARTRIX ;; Tips our memory usage over the limit + -D USERMOD_ARTIFX + -D USERMOD_AUTO_SAVE + -D WLED_ENABLE_HUB75MATRIX +; RAM: [=== ] 26.1% (used 85632 bytes from 327680 bytes) +; Flash: [==========] 99.5% (used 1565065 bytes from 1572864 bytes) + +;; V4 build for 16MB flash, optimized for speed; HUB75 supported +[env:esp32_16MB_V4_S] +extends = esp32_4MB_V4_S_base +build_unflags = ${esp32_4MB_V4_S_base.build_unflags} + ${Speed_Flags.build_unflags} ;; to override -Os +build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags} + ${Speed_Flags.build_flags_V4} ;; optimize for speed instead of size + -D WLED_RELEASE_NAME=esp32_16MB_V4_S + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLED_DISABLE_BROWNOUT_DET + -D JSON_BUFFER_SIZE=18432 -D MIN_HEAP_SIZE=6144 + -D MAX_SEGMENT_DATA=40960 ;; default 32767 + ${common_mm.HUB75_build_flags} + ${common_mm.animartrix_build_flags} +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +;; optimized-for-speed build +; RAM: [== ] 23.7% (used 77704 bytes from 327680 bytes) +; Flash: [======== ] 84.4% (used 1770341 bytes from 2097152 bytes) + +; compiled with ESP-IDF 4.4.1; HUB75 included (may have PIN conflicts) +[env:esp32_16MB_V4_M] +extends = esp32_4MB_V4_M_base +build_flags = ${esp32_4MB_V4_M_base.esp32_build_flags} + -D WLED_RELEASE_NAME=esp32_16MB_V4_M + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + ;; -D WLED_ENABLE_DMX ;; disabled because it does not work with ESP-IDF 4.4.x (buggy driver in SparkFunDMX) + -D WLED_ENABLE_DMX_INPUT ;; needs more testing + -D LEDPIN=16 -D RLYPIN=19 + -D AUDIOPIN=-1 -D TEMPERATURE_PIN=23 -D PIR_SENSOR_PIN=-1 -D PWM_PIN=-1 + -D ENCODER_DT_PIN=35 -D ENCODER_CLK_PIN=39 -D ENCODER_SW_PIN=5 ; WLEDMM spec by @SERG74: use 35 and 39 instead of 18 and 19 (conflicts) + -D FLD_PIN_SCL=-1 -D FLD_PIN_SDA=-1 ; use global! + -D HW_PIN_SCL=22 -D HW_PIN_SDA=21 + ${common_mm.HUB75_build_flags} + ${common_mm.animartrix_build_flags} +lib_deps = ${esp32_4MB_V4_M_base.esp32_lib_deps} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +; RAM: [=== ] 25.7% (used 84104 bytes from 327680 bytes) +; Flash: [======== ] 80.7% (used 1692269 bytes from 2097152 bytes) + +[env:esp32_16MB_V4_M_debug] +extends = esp32_4MB_V4_M_base +build_unflags = ${common.build_unflags} + -D CORE_DEBUG_LEVEL=0 + -D NDEBUG +build_flags = ${esp32_4MB_V4_M_base.esp32_build_flags} + ${Debug_Flags.build_flags} + -D CORE_DEBUG_LEVEL=4 ;; 0=none, 1=error, 2=warning, 3=info, 4=debug, 5=verbose + -D WLED_DEBUG_HEAP ;; WLEDMM enable heap debugging + -D WLED_RELEASE_NAME=esp32_16MB_V4_M_debug + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup +lib_deps = ${esp32_4MB_V4_M_base.esp32_lib_deps} +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +monitor_filters = esp32_exception_decoder +; RAM: [=== ] 26.4% (used 86356 bytes from 327680 bytes) +; Flash: [======== ] 83.6% (used 1753461 bytes from 2097152 bytes) + +;; experimental environment for boards with PSRAM (needs ESP-IDF 4.4.1). HUB75 included (may have PIN conflicts) +[env:esp32_4MB_PSRAM_S] +extends = esp32_4MB_V4_S_base +board = lolin_d32_pro +;board = esp32cam +;board = ttgo-t7-v14-mini32 +board_build.partitions = ${esp32.extended_partitions} +board_build.f_flash = 80000000L +;board_build.flash_mode = dio + +build_unflags = ${esp32_4MB_V4_S_base.build_unflags} + -D WLED_ENABLE_DMX + -D WLED_ENABLE_DMX_INPUT + ; -D WLED_ENABLE_HUB75MATRIX ;; uses a lot of flash + ; -D USERMOD_ANIMARTRIX ;; uses a lot of memory +build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags} + -D WLED_RELEASE_NAME=esp32_4MB_PSRAM_S + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLEDMM_SAVE_FLASH + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM ;; WLED_USE_PSRAM causes major slow-down (slow LEDs) on some ESP32 boards + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + -D WLED_DISABLE_HUESYNC ; RAM 122 bytes; FLASH 6308 bytes + -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes ewowi: disabled to stay below 100% + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER + ${common_mm.HUB75_build_flags} +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} + ${common_mm.HUB75_lib_deps} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + ;; ${common_mm.HUB75_lib_ignore} + ${common_mm.animartrix_lib_ignore} + ${common_mm.DMXin_lib_ignore} +;; RAM: [== ] 17.8% (used 58236 bytes from 327680 bytes) +;; Flash: [========= ] 90.0% (used 1534209 bytes from 1703936 bytes) + +;; similar to 4MB_PSRAM_S, but optimized for WROVER-E (chip revision >= 3) that doesn't need any workarounds for PSRAM any more +;; tl;dr: its faster on PSRAM. But it will not work on all boards. +[env:esp32_4MB_PSRAM_REV3_S] +extends = esp32_4MB_V4_S_base +;board = esp32cam +board = lolin_d32_pro +;board = ttgo-t7-v14-mini32 +board_build.partitions = ${esp32.extended_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = dio + +build_unflags = ${esp32_4MB_V4_S_base.build_unflags} + ;;${Speed_Flags.build_unflags} ;; to override -Os + -DARDUINO_EVENT_RUNNING_CORE=1 ;; we want to run wifi on core0, so remove the standard flag + -mfix-esp32-psram-cache-issue ;; this fix is not needed any more for revision 3 + -mfix-esp32-psram-cache-strategy=memw ;; same as above + +build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags} + -DARDUINO_EVENT_RUNNING_CORE=0 ;; assign Wifi to core0, to have more CPU on core#1 (arduino loop) + -DARDUINO_RUNNING_CORE=1 ;; should be default, but does not hurt + -DCONFIG_MBEDTLS_DYNAMIC_BUFFER=1 ;; optional - seems to move more buffers into PSRAM + ;;${Speed_Flags.build_flags_V4} ;; optimize for speed instead of size --> over 100% flash, but works with 256KB filesystem (alternative partitions file) + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLEDMM_SAVE_FLASH + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM ;; WLED_USE_PSRAM causes major slow-down (slow LEDs) on some ESP32 boards + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + ;;-D CONFIG_ESP32_REV_MIN=3 ;; disables PSRAM bug workarounds in the core, reducing the code size and improving overall performance. + -D WLED_RELEASE_NAME=esp32_4MB_PSRAM_REV3_S + -D WLED_WATCHDOG_TIMEOUT=0 #-D WLED_DISABLE_BROWNOUT_DET + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + -D WLED_DISABLE_HUESYNC ; RAM 122 bytes; FLASH 6308 bytes + -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + ; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes ewowi: disabled to stay below 100% + ;-D WLED_DISABLE_ADALIGHT ;; To disable serial protocols (see upstream #3128) + ; -D WLED_ENABLE_DMX + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER + ${common_mm.HUB75_build_flags} +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} + ${common_mm.HUB75_lib_deps} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + ${common_mm.animartrix_lib_ignore} + ;; ${common_mm.HUB75_lib_ignore} +;; RAM: [== ] 18.0% (used 58920 bytes from 327680 bytes) +;; Flash: [========= ] 90.6% (used 1542929 bytes from 1703936 bytes) + + +# ------------------------------------------------------------------------------ +# esp32-S3 environments +# ------------------------------------------------------------------------------ + + +[env:esp32S3_4MB_S] ;; Use for HD-WF2 +extends = esp32_4MB_V4_M_base +board = esp32-s3-devkitc-1 +build_unflags = + -D USERMOD_DALLASTEMPERATURE ;; disabled because it hangs during usermod setup on -S3 (autodetect broken?) + -D USE_ALT_DISPLAY ;; four line display seems to have problems with I2C - it hangs during usermod setup + -D USERMOD_FOUR_LINE_DISPLAY ;; see above + -D USERMOD_ROTARY_ENCODER_UI ;; see above + -D WLED_ENABLE_DMX ;; disabled because it does not work with ESP-IDF 4.4.x (buggy driver in SparkFunDMX) + +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} + ${common_mm.HUB75_build_flags} + ${common_mm.animartrix_build_flags} + -D WLED_RELEASE_NAME=esp32S3_4MB_S + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 ;; for Serial-to-USB chip + ;;-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + -D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor) + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ${common_mm.build_disable_sync_interfaces} + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLEDMM_SAVE_FLASH + ; -D WLED_DEBUG + ; -D SR_DEBUG +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} + + +[env:esp32S3_8MB_M] +extends = esp32_4MB_V4_M_base +board = esp32-s3-devkitc-1 +build_unflags = + -D USERMOD_DALLASTEMPERATURE ;; disabled because it hangs during usermod setup on -S3 (autodetect broken?) + -D USE_ALT_DISPLAY ;; four line display seems to have problems with I2C - it hangs during usermod setup + -D USERMOD_FOUR_LINE_DISPLAY ;; see above + -D USERMOD_ROTARY_ENCODER_UI ;; see above + -D WLED_ENABLE_DMX ;; disabled because it does not work with ESP-IDF 4.4.x (buggy driver in SparkFunDMX) + +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} ${common_mm.build_flags_M} + ${common_mm.HUB75_build_flags} + ${common_mm.animartrix_build_flags} + ; -DBOARD_HAS_PSRAM -D WLED_USE_PSRAM_JSON -D WLED_USE_PSRAM ;; un-comment in case your board supports PSRAM + ;; -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -D WLED_RELEASE_NAME=esp32S3_8MB_M + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 ;; for Serial-to-USB chip + ;;-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + ;;-D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor) + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ; -D U8X8_HAVE_2ND_HW_I2C ;; experimental - tells U8g2 lib that a second HW I2C unit exists + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + ; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + ; -D WLED_DISABLE_HUESYNC ;RAM 122 bytes; FLASH 6308 bytes + ; -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes + ;;-D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D LEDPIN=4 + ;-D STATUSLED=39 + -D BTNPIN=48 + -D RLYPIN=21 + -D IRPIN=15 + -D HW_PIN_SDA=42 ;; ESP32-S3 default: SDA = 8 + -D HW_PIN_SCL=41 ;; ESP32-S3 default: SCL = 9 + -D AUDIOPIN=-1 + -D SR_DMTYPE=1 -D I2S_SDPIN=16 -D I2S_CKPIN=17 -D I2S_WSPIN=47 -D MCLK_PIN=40 + -D ES7243_SDAPIN=8 -D ES7243_SCLPIN=18 + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} + +;lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +board_build.partitions = tools/WLED_ESP32_8MB.csv +board_build.flash_mode = qio +; RAM: [=== ] 25.4% (used 83320 bytes from 327680 bytes) +; Flash: [======= ] 70.6% (used 1479573 bytes from 2097152 bytes) + +;; MM for ESP32-S3 PSRAM development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) +;; HUB75 supported, but may still hve pin conflicts +[env:esp32S3_8MB_PSRAM_M] +extends = esp32_4MB_V4_M_base + +board = esp32-s3-devkitc-1 ;; generic S3 dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB +;; board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB +board_build.flash_mode = qio ;; use "dio" if your board gets unstable with "qio"; also change then previous lines accordingly (memory_type) + +build_unflags = ${env:esp32S3_8MB_M.build_unflags} ;; use the same as "normal" S3 buildenv +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} ${common_mm.build_flags_M} + ${common_mm.HUB75_build_flags} + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM ;; your board supports PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -D WLED_RELEASE_NAME=esp32S3_8MB_PSRAM_M + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 ;; for Serial-to-USB chip + ;;-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + ;;-D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor) + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ;;-D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + ; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + ; -D WLED_DISABLE_HUESYNC ;RAM 122 bytes; FLASH 6308 bytes + ; -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes + -D LEDPIN=21 + ;;-D DATA_PINS=21,48,3 -D PIXEL_COUNTS=30,1,144 ;; just an example: my board has a builtin neopixel on gpio48 + ; -D STATUSLED=2 + -D BTNPIN=0 -D RLYPIN=1 -D IRPIN=-1 + -D HW_PIN_SDA=42 -D HW_PIN_SCL=41 + -D AUDIOPIN=-1 + -D SR_DMTYPE=1 -D I2S_SDPIN=14 -D I2S_CKPIN=45 -D I2S_WSPIN=47 -D MCLK_PIN=46 + ;;-D ES7243_SDAPIN=8 -D ES7243_SCLPIN=18 + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} + ${common_mm.HUB75_lib_deps} +;lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +board_build.partitions = tools/WLED_ESP32_8MB.csv +; RAM: [== ] 21.1% (used 69156 bytes from 327680 bytes) +; Flash: [======== ] 75.9% (used 1591817 bytes from 2097152 bytes) + +[env:esp32S3_8MB_S] +;; MM for ESP32-S3 boards - FASTPATH + optimize for speed; ; HUB75 support included (may still have pin conflicts) +extends = esp32_4MB_V4_M_base +board = esp32-s3-devkitc-1 ;; generic S3 dev board +board_build.flash_mode = qio ;; use "dio" if your board gets unstable with "qio" +build_unflags = ${env:esp32S3_8MB_M.build_unflags} ;; use the same as "normal" S3 buildenv + ${Speed_Flags.build_unflags} ;; to override -Os +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} + -D WLED_RELEASE_NAME=esp32S3_8MB_S + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 ;; for Serial-to-USB chip + ;;-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + ;;-D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor) + ${Speed_Flags.build_flags_V4} ;; optimize for speed instead of size + -D WLEDMM_FASTPATH ; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + ${common_mm.HUB75_build_flags} + ${common_mm.animartrix_build_flags} + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + -D WLED_DISABLE_HUESYNC ;RAM 122 bytes; FLASH 6308 bytes + -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes + -D LEDPIN=4 + ;-D STATUSLED=39 + -D BTNPIN=48 + -D RLYPIN=21 + -D IRPIN=15 + -D HW_PIN_SDA=42 ;; ESP32-S3 default: SDA = 8 + -D HW_PIN_SCL=41 ;; ESP32-S3 default: SCL = 9 + -D AUDIOPIN=-1 + -D SR_DMTYPE=1 -D I2S_SDPIN=16 -D I2S_CKPIN=17 -D I2S_WSPIN=47 -D MCLK_PIN=40 + -D ES7243_SDAPIN=8 -D ES7243_SCLPIN=18 + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} +lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +board_build.partitions = tools/WLED_ESP32_8MB.csv +;; optimized-for-speed build +; RAM: [=== ] 26.0% (used 85328 bytes from 327680 bytes) +; Flash: [======== ] 79.9% (used 1674885 bytes from 2097152 bytes) + + +;; for S3 with 16MB flash, octal PSRAM, +;; MOONHUB HUB75 adapter board +[env:esp32S3_16MB_PSRAM_M_HUB75] +extends = env:esp32S3_8MB_PSRAM_M +board = lilygo-t7-s3 +board_build.arduino.memory_type = qio_opi +board_build.flash_mode = qio + +build_unflags = ${env:esp32S3_8MB_PSRAM_M.build_unflags} + -D WLED_RELEASE_NAME=esp32S3_8MB_M + -D LEDPIN=21 + ${Speed_Flags.build_unflags} ;; to override -Os + +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} ${common_mm.build_flags_M} + ${Speed_Flags.build_flags_V4} ;; optimize for speed + ${common_mm.HUB75_build_flags} + -D MOONHUB_S3_PINOUT ;; HUB75 pinout + ${common_mm.animartrix_build_flags} + -D WLED_RELEASE_NAME=esp32S3_16MB_PSRAM_M_HUB75 + -D WLEDMM_FASTPATH + -D WLED_DISABLE_BROWNOUT_DET + ;; -D JSON_BUFFER_SIZE=18432 ;; default 54000 + -D MIN_HEAP_SIZE=6144 ;; default 8192 + -D MAX_SEGMENT_DATA=40960 ;; default 32767 + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM ;; your board supports PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 ;; for Serial-to-USB chip + ;; -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ;; -DUSERMOD_BATTERY_MEASUREMENT_PIN=2 ;; battery voltage pin + ;; -D STATUSLED=17 ;; onboard LED + -D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75 + -D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic + +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} +;lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + +board_build.partitions = ${esp32.extreme_partitions} +board_upload.flash_size = 16MB +board_upload.maximum_size =16777216 +; RAM: [== ] 19.0% (used 62332 bytes from 327680 bytes) +; Flash: [====== ] 57.7% (used 1813585 bytes from 3145728 bytes) + + +;; MM for ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1 +;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi) +;; includes HUB75 and AnimArtix +[env:esp32S3_WROOM-2_M] +extends = env:esp32S3_8MB_PSRAM_M +board_build.flash_mode = dout ;; dummy value - bootloader will switch to "opi" +board_build.arduino.memory_type = opi_opi +board = esp32s3camlcd ;; this is the only standard board with "opi_opi" +board_upload.flash_size = 16MB +board_upload.maximum_size =16777216 +board_build.partitions = ${esp32.extreme_partitions} +build_unflags = ${env:esp32S3_8MB_PSRAM_M.build_unflags} + -D WLED_RELEASE_NAME=esp32S3_8MB_M + -D LEDPIN=21 + ${Speed_Flags.build_unflags} ;; to override -Os +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} ${common_mm.build_flags_M} + ${Speed_Flags.build_flags_V4} ;; optimize for speed + ${common_mm.HUB75_build_flags} + ${common_mm.animartrix_build_flags} + -D WLED_RELEASE_NAME=esp32S3_WROOM-2_M + -D WLEDMM_FASTPATH + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM ;; your board supports PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 ;; for Serial-to-USB chip + ;;-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ;; -D LEDPIN=38 ;; buildin LED + ;; -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 + -D LEDPIN=12 -D BTNPIN=0 -D RLYPIN=-1 -D IRPIN=-1 -D AUDIOPIN=-1 ;; to avoid pin conflict with HUB75 + -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} + ${common_mm.HUB75_lib_deps} + ${common_mm.animartrix_lib_deps} + +;; MM for esp32-s3 zero/supermini and lolin S3 mini boards - fastpath, optimize for speed +[env:esp32S3_4MB_PSRAM_S] +extends = env:esp32S3_8MB_S +board = lolin_s3_mini ;; -S3 mini: 4MB flash 2MB PSRAM +board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem (esptool erase_flash needed when changing from "standard WLED" partitions) +;; board_build.partitions = tools/WLED_ESP32_4MB_512KB_FS.csv ;; 1.7MB firmware, 500KB filesystem (esptool erase_flash needed when changing from "standard WLED" partitions) +build_unflags = ${common.build_unflags} + -D WLED_ENABLE_HUB75MATRIX ;; board does not have enough pins for HUB75 +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} + -D WLED_RELEASE_NAME=esp32S3_4MB_PSRAM_S + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -DCONFIG_MBEDTLS_DYNAMIC_BUFFER=1 ;; optional - allows some buffers to use PSRAM + -DLOLIN_WIFI_FIX -DWLEDMM_WIFI_POWERON_HACK ;; seems to work much better with this + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Serial-to-USB chip + ;;-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + -D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor) + ${Speed_Flags.build_flags_V4} ;; optimize for speed instead of size + -D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. + -D WLEDMM_SAVE_FLASH + ${common_mm.animartrix_build_flags} + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes + -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + -D WLED_DISABLE_HUESYNC ;RAM 122 bytes; FLASH 6308 bytes + -D WLED_DISABLE_INFRARED ;RAM 136 bytes; FLASH 24492 bytes + -D LEDPIN=21 + -D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1 + -D HW_PIN_SDA=12 -D HW_PIN_SCL=13 + -D AUDIOPIN=-1 + -D SR_DMTYPE=1 -D I2S_SDPIN=5 -D I2S_WSPIN=6 -D I2S_CKPIN=4 -D MCLK_PIN=7 + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} + ${common_mm.animartrix_lib_deps} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + ${common_mm.HUB75_lib_ignore} + ${common_mm.DMXin_lib_ignore} +; RAM: [== ] 18.6% (used 60828 bytes from 327680 bytes) +; Flash: [======== ] 76.9% (used 1461829 bytes from 1900544 bytes) + +;; MM for esp32-s3 zero/supermini and lolin S3 mini boards - standard +[env:esp32S3_4MB_PSRAM_M] +extends = env:esp32S3_8MB_M +board = lolin_s3_mini ;; -S3 mini: 4MB flash 2MB PSRAM +board_build.partitions = ${esp32.default_partitions} +build_unflags = ${common.build_unflags} + -D WLED_ENABLE_HUB75MATRIX ;; board does not have enough pins for HUB75 + -D USERMOD_ANIMARTRIX ;; not enough flash +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} ${common_mm.build_flags_M} + -D WLED_RELEASE_NAME=esp32S3_4MB_PSRAM_M + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -DCONFIG_MBEDTLS_DYNAMIC_BUFFER=1 ;; optional - allows some buffers to use PSRAM + -DLOLIN_WIFI_FIX -DWLEDMM_WIFI_POWERON_HACK ;; seems to work much better with this + -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Serial-to-USB chip + ;;-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + -D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor) + ;; ${common_mm.animartrix_build_flags} ;; not enough flash + -D LEDPIN=21 + -D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1 -D AUDIOPIN=-1 + -D HW_PIN_SDA=12 -D HW_PIN_SCL=13 + -D SR_DMTYPE=1 -D I2S_SDPIN=5 -D I2S_WSPIN=6 -D I2S_CKPIN=4 -D MCLK_PIN=7 + -D WLED_DISABLE_LOXONE ; FLASH 1272 bytes - disabled to stay below 100% + -D WLED_DISABLE_HUESYNC ; RAM 122 bytes; FLASH 6308 bytes - disabled to stay below 100% + -D WLED_DISABLE_INFRARED ; RAM 136 bytes; FLASH 24492 bytes - disabled to stay below 100% + ;; -D WLED_DISABLE_ALEXA ; RAM 116 bytes; FLASH 13524 bytes + ;; -D WLED_DISABLE_MQTT ; RAM 216 bytes; FLASH 16496 bytes + -D WLEDMM_SAVE_FLASH + ; -D WLED_DEBUG + ; -D SR_DEBUG + ; -D MIC_LOGGER +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + ${common_mm.HUB75_lib_ignore} + ${common_mm.DMXin_lib_ignore} + ${common_mm.animartrix_lib_ignore} +; RAM: [== ] 18.7% (used 61172 bytes from 327680 bytes) +; Flash: [==========] 97.7% (used 1536157 bytes from 1572864 bytes) + + +# ------------------------------------------------------------------------------ +# esp32-S2 environments +# ------------------------------------------------------------------------------ + + +;; MM for Adafruit QT Py ESP32-S2 -> 4MB flash, PSRAM, and tinyUF2 bootloader +;; to ewowi - i'll optimize this entry later, as a few things can be inherited for sure. To softhack: sure ;-) +[env:esp32s2_tinyUF2_PSRAM_S] +extends = esp32_4MB_V4_S_base +;;platform = ${esp32s2.platform} ;; using 5.2.0, due to massive connectivity problems on -S2 with 5.3.0 +;;platform_packages = ${esp32s2.platform_packages} + +;; tasmota platform needed, as we are over flash size limits for the standard platform +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} + +board = adafruit_qtpy_esp32s2 +board_build.partitions = tools/partitions-4MB_spiffs-tinyuf2.csv ;; this is needed for tinyUF2 bootloader! Filename has to end in "tinyuf2.csv" +board_build.f_flash = 80000000L +board_build.flash_mode = qio +upload_speed = 256000 ;; 921600 +build_unflags = ${common.build_unflags} + -D USERMOD_DALLASTEMPERATURE ;; disabled because it hangs during usermod setup on -S3 (autodetect broken?) + -D WLED_ENABLE_DMX ;; disabled because it does not work with ESP-IDF 4.4.x (buggy driver in SparkFunDMX) + -D WLED_ENABLE_DMX_INPUT ;; needs more testing + -DWLEDMM_FASTPATH ;; needs more testing on -S2 + -DUSERMOD_RTC + -D WLED_ENABLE_HUB75MATRIX + +build_flags = ${common.build_flags} ${esp32s2.build_flags} + ; ${Debug_Flags.build_flags} ;ewowi: enabling debug causes Error: The program size (1463330 bytes) is greater than maximum allowed (1441792 bytes) + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ${common_mm.build_flags_S} + -Wno-misleading-indentation -Wno-format-truncation + -DCONFIG_MBEDTLS_DYNAMIC_BUFFER=1 ;; optional - allows some buffers to use PSRAM + -D WLED_RELEASE_NAME=esp32S2_4MB_UF2_S + -DARDUINO_USB_CDC_ON_BOOT=1 ;; mandatory, otherwise USB does not work!! + -D WLED_DISABLE_ADALIGHT ;; disables serial protocols when using CDC USB (Serial RX will receive junk commands, unless its pulled down by resistor) + -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 + -D SERVERNAME='"WLED-S2"' + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -D WLED_DISABLE_LOXONE ;; FLASH 1272 bytes + -D WLED_DISABLE_HUESYNC ;; RAM 122 bytes; FLASH 6308 bytes + -D WLED_DISABLE_ALEXA ;; RAM 116 bytes; FLASH 13524 bytes + ; -D WLED_DISABLE_MQTT ;; RAM 216 bytes; FLASH 16496 bytes + -D WLED_DISABLE_INFRARED ;; RAM 136 bytes; FLASH 24492 bytes + -D WLEDMM_SAVE_FLASH + -D LEDPIN=39 ;; onboard neopixel LED. Attach your own LEDs to GPIO 7 or GPIO 6 + -D BTNPIN=0 + ;-D RLYPIN=6 + ;-D IRPIN=7 + -D HW_PIN_SCL=40 -D HW_PIN_SDA=41 + -D HW_PIN_MOSISPI=35 ;WLEDMM renamed from HW_PIN_DATASPI + -D HW_PIN_CLOCKSPI=36 + -D HW_PIN_MISOSPI=37 + -D AUDIOPIN=-1 + -D SR_DMTYPE=1 -D I2S_SDPIN=9 -D I2S_WSPIN=8 -D I2S_CKPIN=17 -D MCLK_PIN=18 + ;-D STATUSLED=-1 + -D WLED_USE_MY_CONFIG +lib_deps = ${esp32s2.lib_deps} ${common_mm.lib_deps_S} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + OneWire ; not needed as we don't include USERMOD_DALLASTEMPERATURE +monitor_filters = esp32_exception_decoder +; RAM: [== ] 17.9% (used 58548 bytes from 327680 bytes) +; Flash: [======== ] 82.0% (used 1182554 bytes from 1441792 bytes) + +;; MM environment for generic ESP32-S2, with PSRAM, 4MB flash (300kB filesystem to have more program space) +;; PINs assignments optimized for use with serg74 "mini shield" +[env:esp32s2_PSRAM_M] +extends = esp32_4MB_V4_M_base +;; standard platform +platform = ${esp32s2.platform} ;; using 5.2.0, due to massive connectivity problems on -S2 with 5.3.0 +platform_packages = ${esp32s2.platform_packages} +;; tasmota platform (optional) +;; platform = ${esp32_idf_V4.platform} +;; platform_packages = ${esp32_idf_V4.platform_packages} + +board = lolin_s2_mini +board_build.partitions = ${esp32.extended_partitions} ;; 1.65MB firmware, 700KB filesystem (esptool erase_flash needed when changing from "standard WLED" partitions) +board_build.flash_mode = dio +upload_speed = 256000 ;; 921600 +build_unflags = ${common.build_unflags} + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_DFU_ON_BOOT=0 + -D USERMOD_DALLASTEMPERATURE ;; disabled because it hangs during usermod setup on -S3 (autodetect broken?) + -D WLED_ENABLE_DMX ;; disabled because it does not work with ESP-IDF 4.4.x (buggy driver in SparkFunDMX) + -D WLED_ENABLE_DMX_INPUT ;; needs more testing + -D WLED_ENABLE_HUB75MATRIX +build_flags = ${common.build_flags} ${esp32s2.build_flags} + ;; ${Debug_Flags.build_flags} + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ${common_mm.build_flags_S} ${common_mm.build_flags_M} + -Wno-misleading-indentation -Wno-format-truncation + -D WLED_RELEASE_NAME=esp32s2_4MB_M + -DBOARD_HAS_PSRAM ;; -D WLED_USE_PSRAM + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + -DLOLIN_WIFI_FIX -DWLEDMM_WIFI_POWERON_HACK ;; seems to work much better with this + -DARDUINO_USB_CDC_ON_BOOT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 + -D WLED_DISABLE_ADALIGHT ;; disables serial protocols, as the board only has CDC USB + -D WLED_DISABLE_INFRARED ;; save flash space + -D WLED_DISABLE_ALEXA ;; save flash space + -D WLED_DISABLE_HUESYNC ;; save flash space + -D WLED_DISABLE_LOXONE ;; save flash space + -D WLEDMM_SAVE_FLASH + -D AUDIOPIN=-1 + -D BTNPIN=-1 -D IRPIN=-1 + -D LEDPIN=16 ;; second led pin = 18 + -D HW_PIN_SCL=35 -D HW_PIN_SDA=33 + -D RLYPIN=9 + ;; -D HW_PIN_MOSISPI=11 -D HW_PIN_CLOCKSPI=7 -D HW_PIN_MISOSPI=9 ;; 9 already in use for RELAY, 7 for IR + -D SR_DMTYPE=1 -D I2S_SDPIN=34 -D I2S_CKPIN=14 -D I2S_WSPIN=17 -D MCLK_PIN=-1 ;; recommended for mini shield + ;; -D FFTTASK_PRIORITY=2 ;; useful for testing FFT timing. reduces audio latency, but makes effects slower. + ;; -D STATUSLED=15 + -D WLED_USE_MY_CONFIG +lib_deps = ${esp32s2.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + OneWire ; not needed as we don't include USERMOD_DALLASTEMPERATURE + ${common_mm.HUB75_lib_ignore} + ${common_mm.DMXin_lib_ignore} +monitor_filters = esp32_exception_decoder +; RAM: [== ] 20.5% (used 67256 bytes from 327680 bytes) +; Flash: [========= ] 93.3% (used 1590266 bytes from 1703936 bytes) + +[env:esp32s2_PSRAM_S] +extends = env:esp32s2_PSRAM_M +board_build.partitions = ${esp32.default_partitions} ;; 1.55MB firmware, 1MB filesystem +build_unflags = ${env:esp32s2_PSRAM_M.build_unflags} + -DWLED_DISABLE_ADALIGHT + -D WLED_RELEASE_NAME=esp32s2_4MB_M + -DUSE_ALT_DISPLAY + -DUSERMOD_FOUR_LINE_DISPLAY + -DUSERMOD_ROTARY_ENCODER_UI + -DUSERMOD_ANIMARTRIX + ;; -DUSERMOD_ARTIFX ;; uncomment to reduce flash size +build_flags = ${env:esp32s2_PSRAM_M.build_flags} + -D WLED_RELEASE_NAME=esp32s2_4MB_S +lib_deps = ${env:esp32s2_PSRAM_M.lib_deps} +lib_ignore = ${env:esp32s2_PSRAM_M.lib_ignore} + U8g2 + ${common_mm.animartrix_lib_ignore} +; RAM: [== ] 20.4% (used 66792 bytes from 327680 bytes) +; Flash: [========= ] 94.8% (used 1490390 bytes from 1572864 bytes) + + +# ------------------------------------------------------------------------------ +# esp32-C3 environments +# ------------------------------------------------------------------------------ + + +;; MM environment for generic ESP32-C3 -> 4MB flash, no PSRAM +[env:esp32c3dev_4MB_M] +extends = esp32_4MB_V4_S_base +;board_build.flash_mode = dout +platform = ${esp32.platformV4} +platform_packages = ${esp32.platformV4_packages} +board_build.flash_mode = qio +board = esp32-c3-devkitm-1 +;board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem (esptool erase_flash needed when changing from "standard WLED" partitions) +;board_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv ;; use this for boards with 2MB flash (like some Ai-Thinker ESP32-C3-12F models) +upload_speed = 230400 ;; 460800 ;; 256000 ;; 921600 + +build_unflags = ${common.build_unflags} + -D USERMOD_DALLASTEMPERATURE ;; disabled because it hangs during usermod setup on -S2 (autodetect broken?) + -D USE_ALT_DISPLAY ;; four line display seems to have problems with I2C - it hangs during usermod setup + -D USERMOD_FOUR_LINE_DISPLAY ;; see above + -D USERMOD_ROTARY_ENCODER_UI ;; see above + -D WLED_ENABLE_DMX ;; disabled because it does not work with ESP-IDF 4.4.x (buggy driver in SparkFunDMX) + -D WLED_ENABLE_DMX_INPUT ;; needs more testing + ;-D WLED_DEBUG_HOST='"192.168.x.x"' ;; to disable net print + -D USERMOD_ANIMARTRIX ;; Tips our memory usage over the limit + -DUSERMOD_ARTIFX ;; over the limit + -DWLEDMM_FASTPATH ;; needs more testing on -C3 + +build_flags = ${common.build_flags} ${esp32c3.build_flags} + ; -D WLED_DISABLE_OTA ;; OTA is not possible for boards with 2MB flash only (like some Ai-Thinker ESP32-C3-12F models) + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ${common_mm.build_flags_S} ${common_mm.build_flags_M} + -Wno-misleading-indentation -Wno-format-truncation + -D WLED_RELEASE_NAME=esp32c3dev_4MB_M + ; -D WLED_DISABLE_BROWNOUT_DET + ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; enable CDC USB -> needed for debugging over serial USB + -D WLED_DISABLE_ADALIGHT ;; to disable serial protocols for boards with CDC USB (Serial RX will receive junk commands, unless its pulled down by resistor) + -DARDUINO_USB_CDC_ON_BOOT=0 ;; disable CDC USB + -D SERVERNAME='"WLED-C3"' + -D WLEDMM_WIFI_POWERON_HACK -DLOLIN_WIFI_FIX ;; use this _only_ if your device is not able to make a WiFI connection! + ;-D WLED_DISABLE_INFRARED ;; save flash space + ;-D WLED_DISABLE_ALEXA ;; save flash space + -D WLEDMM_SAVE_FLASH + -D LEDPIN=8 ;; onboard neopixel 5x5 Matrix. Attach your own LEDs to GPIO 20 + -D BTNPIN=9 + ; -D STATUSLED=10 ;; onboard LED + -D RLYPIN=-1 -D IRPIN=-1 + ;-D HW_PIN_SDA=0 -D HW_PIN_SCL=1 ;; for I2C Qwiic connector + -D SR_DMTYPE=1 -D I2S_SDPIN=5 -D I2S_WSPIN=6 -D I2S_CKPIN=4 -D MCLK_PIN=7 + ; -D WLED_USE_MY_CONFIG + +lib_deps = ${esp32c3.lib_deps} ${common_mm.lib_deps_S} ${common_mm.lib_deps_V4_M} +lib_ignore = + ;IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + OneWire ; not needed as we don't include USERMOD_DALLASTEMPERATURE + U8g2 ; not needed as we don't include USERMOD_FOUR_LINE_DISPLAY +; RAM: [== ] 24.6% (used 80732 bytes from 327680 bytes) +; Flash: [==========] 97.5% (used 1533360 bytes from 1572864 bytes) +;; MM environment for ESP32-C3 "mini" and "super mini" -> flash mode "dio" instead of "qio" (see #101) +[env:esp32c3mini_dio_4MB_M] +extends = env:esp32c3dev_4MB_M +board = lolin_c3_mini +board_build.flash_mode = dio ;; some super-mini boards are unstable with "qio" mode +;; board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; optional: 1.8MB firmware, 256KB filesystem (esptool erase_flash needed when changing from "standard WLED" partitions) +;;; replace WLED_RELEASE_NAME, disable CDC_ON_BOOT +build_unflags = ${env:esp32c3dev_4MB_M.build_unflags} + -DARDUINO_USB_CDC_ON_BOOT=1 + -D WLED_RELEASE_NAME=esp32c3dev_4MB_M +build_flags = ${env:esp32c3dev_4MB_M.build_flags} + -DARDUINO_USB_CDC_ON_BOOT=0 + -D WLED_RELEASE_NAME=esp32c3mini_dio_4MB_M + -D WLED_DISABLE_BROWNOUT_DET ;; the board only has a 500mA LDO, better to disable brownout detection + -D WLED_DISABLE_ADALIGHT ;; to disable serial protocols for boards with CDC USB (Serial RX will receive junk commands, unless its pulled down by resistor) + -D HW_PIN_SDA=0 -D HW_PIN_SCL=1 ;; avoid pin conflicts +; RAM: [== ] 24.6% (used 80556 bytes from 327680 bytes) +; Flash: [==========] 97.3% (used 1530346 bytes from 1572864 bytes) + +[env:esp32c3dev_2MB_M] +extends = env:esp32c3dev_4MB_M +board = lolin_c3_mini +;;; replace WLED_RELEASE_NAME, disable CDC_ON_BOOT +build_unflags = ${env:esp32c3dev_4MB_M.build_unflags} + -DARDUINO_USB_CDC_ON_BOOT=1 + -D WLED_RELEASE_NAME=esp32c3dev_4MB_M + +;;; 2MB Flash, no OTA +board_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv +board_build.flash_mode = dio +board_upload.flash_size = 2MB +board_upload.maximum_size = 2097152 + +build_flags = ${env:esp32c3dev_4MB_M.build_flags} + -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_DISABLE_OTA + ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB + -DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip + -D WLED_RELEASE_NAME=esp32c3dev_2MB_M + -D WLED_DISABLE_BROWNOUT_DET ;; the board only has a 500mA LDO, better to disable brownout detection + -D WLED_DISABLE_ADALIGHT ;; to disable serial protocols for boards with CDC USB (Serial RX will receive junk commands, unless its pulled down by resistor) + -D HW_PIN_SDA=0 -D HW_PIN_SCL=1 ;; avoid pin conflicts + +; RAM: [== ] 24.0% (used 78676 bytes from 327680 bytes) +; Flash: [==========] 96.4% (used 1516068 bytes from 1572864 bytes) + +;; MM environment for "seeed xiao -C3" boards +[env:seeed_esp32c3_4MB_S] +extends = env:esp32c3dev_4MB_M +board = seeed_xiao_esp32c3 +platform = ${esp32.platformV4_pre} ;; standard IDF 4.4.1 platform - seems to work better for seeed xiao board +platform_packages = ${esp32.platformV4_packages_pre} +board_build.flash_mode = qio +upload_speed = 460800 +build_unflags = ${env:esp32c3dev_4MB_M.build_unflags} + -DWLEDMM_FASTPATH ;; needs more testing on -C3 + -D WLED_ENABLE_HUB75MATRIX ;; not enough pins +build_flags = ${common.build_flags} ${esp32c3.build_flags} + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + ${common_mm.build_flags_S} -Wno-misleading-indentation -Wno-format-truncation + -D WLED_RELEASE_NAME=seeed_esp32c3_4MB_S + -DARDUINO_USB_CDC_ON_BOOT=1 ;; enable CDC USB -> needed for debugging over serial USB + -D WLED_DISABLE_ADALIGHT ;; to disable serial protocols when using CDC USB (Serial RX pin will receive junk commands, unless its pulled down by resistor) + -D WLED_DISABLE_INFRARED ;; save flash space + ;-D WLED_DISABLE_ALEXA ;; save flash space + ;-D WLED_DISABLE_HUESYNC ;; save flash space + ;-D WLED_DISABLE_LOXONE ;; save flash space + ;-D WLEDMM_WIFI_POWERON_HACK -DLOLIN_WIFI_FIX ;; use this _only_ if your device is not able to make a WiFI connection! + -D LEDPIN=3 ;; attach your LEDs to GPIO3 aka "D1" / "A1" + -D BTNPIN=9 + -D RLYPIN=-1 -D IRPIN=-1 + -D SR_DMTYPE=1 + -D I2S_SDPIN=5 -D I2S_WSPIN=6 -D I2S_CKPIN=4 -D MCLK_PIN=7 + -D WLED_USE_MY_CONFIG + ;-D WLED_DEBUG -D SR_DEBUG +lib_deps = ${esp32c3.lib_deps} ${common_mm.lib_deps_S} +lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +monitor_filters = esp32_exception_decoder +; RAM: [== ] 22.4% (used 73388 bytes from 327680 bytes) +; Flash: [========= ] 92.7% (used 1458598 bytes from 1572864 bytes) +# ------------------------------------------------------------------------------ +# custom board environments +# ------------------------------------------------------------------------------ + + +# ------------------------------------------------------------------------------ +# wemos shields +# ------------------------------------------------------------------------------ +;https://www.tindie.com/products/serg74/wled-shield-board-for-addressable-leds/ +;https://www.tindie.com/products/moonmodules/shield-board-for-esp32-for-wled-addressable-leds/ +[wemos_shield_esp32_4MB_S_base] +extends = esp32_4MB_S_base +pinout_build_flags = + -D ABL_MILLIAMPS_DEFAULT=9500 ; Wemos max 10A + -D LEDPIN=16 + -D RLYPIN=19 + -D BTNPIN=17 + -D IRPIN=18 + -D AUDIOPIN=-1 + -D TEMPERATURE_PIN=23 + -D FLD_PIN_SCL=-1 -D FLD_PIN_SDA=-1 ; use global! + -D HW_PIN_SCL=22 -D HW_PIN_SDA=21 + -D HW_PIN_CLOCKSPI=-1 -D HW_PIN_MOSISPI=-1 -D HW_PIN_MISOSPI=-1 ; WLEDMM: is now also default but just to show we didn't agree on wemos pins for spi yet + -D ENCODER_DT_PIN=35 -D ENCODER_CLK_PIN=39 -D ENCODER_SW_PIN=5 ; WLEDMM spec by @SERG74: use 35 and 39 instead of 18 and 19 (conflicts) + -D PIR_SENSOR_PIN=-1 + -D PWM_PIN=-1 + ; -D WLED_USE_MY_CONFIG +build_flags = ${esp32_4MB_S_base.build_flags} ${common_mm.build_disable_sync_interfaces} ${wemos_shield_esp32_4MB_S_base.pinout_build_flags} + +[wemos_shield_esp32_4MB_M_base] +extends = wemos_shield_esp32_4MB_S_base +build_flags = ${esp32_4MB_M_base.build_flags} ${wemos_shield_esp32_4MB_S_base.pinout_build_flags} +lib_deps = ${wemos_shield_esp32_4MB_S_base.lib_deps} ${common_mm.lib_deps_M} + +[wemos_shield_esp32_4MB_XL_base] +extends = wemos_shield_esp32_4MB_M_base +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} ${common_mm.build_flags_XL} +lib_deps = ${wemos_shield_esp32_4MB_M_base.lib_deps} ${common_mm.lib_deps_XL} + +[env:wemos_shield_esp32_4MB_S] +extends = wemos_shield_esp32_4MB_S_base +build_flags = ${wemos_shield_esp32_4MB_S_base.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_4MB_S +; RAM: [=== ] 25.4% (used 83124 bytes from 327680 bytes) +; Flash: [======== ] 83.4% (used 1311629 bytes from 1572864 bytes) + +[env:wemos_shield_esp32_4MB_M] +extends = wemos_shield_esp32_4MB_M_base +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_4MB_M +; RAM: [=== ] 26.0% (used 85252 bytes from 327680 bytes) +; Flash: [==========] 97.4% (used 1531865 bytes from 1572864 bytes) + +[env:wemos_shield_esp32_4MB_ICS4343x_M] +extends = wemos_shield_esp32_4MB_M_base +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} ${Shield_ICS4343x.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_4MB_ICS4343x_M +; RAM: [== ] 24.4% (used 79820 bytes from 327680 bytes) +; Flash: [========= ] 88.6% (used 1393421 bytes from 1572864 bytes) + +[env:wemos_shield_esp32_4MB_SPM1423_M] +extends = wemos_shield_esp32_4MB_M_base +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} ${Shield_SPM1423.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_4MB_SPM1423_M +; RAM: [== ] 24.4% (used 79820 bytes from 327680 bytes) +; Flash: [========= ] 88.6% (used 1393421 bytes from 1572864 bytes) + +[env:wemos_shield_esp32_4MB_LineIn_M] +extends = wemos_shield_esp32_4MB_M_base +build_unflags = ${common.build_unflags} ${Shield_LineIn.build_unflags} +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} ${Shield_LineIn.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_4MB_LineIn_M + +[env:wemos_shield_esp32_16MB_S] +extends = wemos_shield_esp32_4MB_S_base +build_flags = ${wemos_shield_esp32_4MB_S_base.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_16MB_S +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem + +[env:wemos_shield_esp32_16MB_M] +extends = wemos_shield_esp32_4MB_M_base +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_16MB_M +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +;board_build.flash_mode = qio +; RAM: [== ] 24.4% (used 79820 bytes from 327680 bytes) +; Flash: [======= ] 66.4% (used 1393421 bytes from 2097152 bytes) + +[env:wemos_shield_esp32_16MB_ICS4343x_M] +extends = wemos_shield_esp32_4MB_M_base +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} ${Shield_ICS4343x.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_16MB_ICS4343x_M +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv +; RAM: [== ] 24.4% (used 79820 bytes from 327680 bytes) +; Flash: [========= ] 88.6% (used 1393421 bytes from 1572864 bytes) + +[env:wemos_shield_esp32_16MB_ICS4343x_XL] +extends = wemos_shield_esp32_4MB_XL_base +build_flags = ${wemos_shield_esp32_4MB_XL_base.build_flags} ${Shield_ICS4343x.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_16MB_ICS4343x_XL +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +; RAM: [== ] 24.4% (used 80044 bytes from 327680 bytes) +; Flash: [======= ] 67.9% (used 1424185 bytes from 2097152 bytes) + +[env:wemos_shield_esp32_16MB_SPM1423_M] +extends = wemos_shield_esp32_4MB_M_base +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} ${Shield_SPM1423.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_16MB_SPM1423_M +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +; RAM: [== ] 24.4% (used 79820 bytes from 327680 bytes) +; Flash: [========= ] 88.6% (used 1393421 bytes from 1572864 bytes) + +[env:wemos_shield_esp32_16MB_SPM1423_XL] +extends = wemos_shield_esp32_4MB_XL_base +build_flags = ${wemos_shield_esp32_4MB_XL_base.build_flags} ${Shield_SPM1423.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_16MB_SPM1423_XL +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +; RAM: [== ] 24.4% (used 79820 bytes from 327680 bytes) +; Flash: [========= ] 88.6% (used 1393421 bytes from 1572864 bytes) + +[env:wemos_shield_esp32_16MB_LineIn_M] +extends = wemos_shield_esp32_4MB_M_base +build_unflags = ${common.build_unflags} ${Shield_LineIn.build_unflags} +build_flags = ${wemos_shield_esp32_4MB_M_base.build_flags} ${Shield_LineIn.build_flags} + -D WLED_RELEASE_NAME=wemos_shield_esp32_16MB_LineIn_M +board = esp32_16MB +board_build.partitions = tools/WLED_ESP32_16MB.csv ;; WLED standard for 16MB flash: 2MB firmware, 12 MB filesystem +;board_build.partitions = tools/WLED_ESP32_16MB_9MB_FS.csv ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem + + +# ------------------------------------------------------------------------------ +# special boards and controlers +# ------------------------------------------------------------------------------ + +;https://www.athom.tech/blank-1/wled-esp32-music-addressable-led-strip-controller +[env:athom_music_esp32_4MB_M] +extends = esp32_4MB_M_base +build_unflags = ${common.build_unflags} + -D USERMOD_ARTIFX ;; disabled to save some program space in flash + -D USERMOD_DALLASTEMPERATURE ;; disabled - flash space is too tight for this + -D USERMOD_ROTARY_ENCODER_UI ;; see above +build_flags = ${esp32_4MB_M_base.build_flags} + ${Athom_PDMmic.build_flags} + -D WLED_AP_SSID_UNIQUE + -D WLED_RELEASE_NAME=athom_music_esp32_4MB_M + -D ABL_MILLIAMPS_DEFAULT=14500 ; max 15A + ; -D WLED_DISABLE_MQTT + -D WLED_DISABLE_LOXONE + -D WLED_DISABLE_ADALIGHT ;to get 4ld working + -D BTNPIN=0 -D RLYPIN=2 -D IRPIN=25 -D IRTYPE=9 -D LEDPIN=18 + -D AUDIOPIN=-1 + ; -D TEMPERATURE_PIN=23 + -D FLD_PIN_SCL=-1 -D FLD_PIN_SDA=-1 ; use global! + -D HW_PIN_SCL=3 -D HW_PIN_SDA=1 ;4ld uses rx and tx + -D HW_PIN_CLOCKSPI=-1 -D HW_PIN_MOSISPI=-1 -D HW_PIN_MISOSPI=-1 ; WLEDMM: is now also default but just to show we didn't agree on wemos pins for spi yet + ; -D ENCODER_DT_PIN=35 -D ENCODER_CLK_PIN=39 -D ENCODER_SW_PIN=5 ; WLEDMM spec by @SERG74: use 35 and 39 instead of 18 and 19 (conflicts) + ; -D PIR_SENSOR_PIN=-1 + ; -D PWM_PIN=-1 + ; -D WLED_USE_MY_CONFIG + -D WLEDMM_SAVE_FLASH +; RAM: [=== ] 26.3% (used 86204 bytes from 327680 bytes) +; Flash: [========= ] 93.6% (used 1471681 bytes from 1572864 bytes) + +;https://shop.myhome-control.de/Elektronik/ +[env:abc_wled_controller_v43_S] +extends = esp32_4MB_S_base +build_unflags = ${esp32_4MB_S_base.build_unflags} ${common_mm.build_disable_sync_interfaces} +build_flags = ${esp32_4MB_S_base.build_flags} + -D WLED_RELEASE_NAME=abc_wled_controller_v43_M + -D LEDPIN=16 + -D ABL_MILLIAMPS_DEFAULT=5000 ; 5A default. Max 13A depending on the wires connected + -D WLED_USE_ETHERNET + -D WLED_ETH_DEFAULT=9 ; ABC! WLED V43 & compatible + -D RLYPIN=-1 -D BTNPIN=-1 ;; Prevent clash + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only + -D AUDIOPIN=-1 + -D FLD_PIN_SCL=-1 -D FLD_PIN_SDA=-1 ; use global! + ; -D WLED_USE_MY_CONFIG + -D SR_DMTYPE=4 -D I2S_SDPIN=32 -D I2S_WSPIN=15 -D I2S_CKPIN=14 -D MCLK_PIN=0 ; generic i2s with mclk 0 + -D SR_SQUELCH=1 -D SR_GAIN=60 ; increrase squelch if noise, in test 0 is okay, but only slightly + -D SR_FREQ_PROF=1 ; Generic line in +; RAM: [=== ] 25.9% (used 84884 bytes from 327680 bytes) +; Flash: [========= ] 88.7% (used 1395249 bytes from 1572864 bytes) + +; ESP32 WLED pico board with builtin ICS-43432 microphpone +[env:esp32_pico_4MB_M] +extends = esp32_4MB_M_base +board = pico32 +board_build.flash_mode = dio ;; (dout = dual out; more compatible than qio = quad i/o) +upload_speed = 256000 ;; or 115200 ;; or 460800 ; or 921600 (slower speeds are better when flashing without a soldered connection) + +build_flags = ${esp32_4MB_M_base.build_flags} + -D WLED_RELEASE_NAME=esp32_pico_4MB_M + -D WLED_DISABLE_BROWNOUT_DET + -D SERVERNAME='"WLED-pico32"' + ; -D WLED_WATCHDOG_TIMEOUT=60 + -D WLED_DISABLE_ADALIGHT ;; WLEDMM this board does not have a serial-to-USB chip. Better to disable serial protocols, to avoid crashes (see upstream #3128) + ; -D WLED_DEBUG + ; -D SR_DEBUG + -D LEDPIN=2 + -D RLYPIN=19 + -D BTNPIN=0 + -D IRPIN=-1 -D TEMPERATURE_PIN=-1 ;; no default pins for IR, DallasTemp + -D ENCODER_DT_PIN=-1 -D ENCODER_CLK_PIN=-1 -D ENCODER_SW_PIN=-1 ;; no default pins for Rotary + -D HW_PIN_SCL=22 -D HW_PIN_SDA=21 + -D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14 + -D SR_SQUELCH=5 -D SR_GAIN=30 -D SR_FREQ_PROF=5 ;; ICS-4343x specific + ; -D MCLK_PIN=0 + -D SR_ENABLE_DEFAULT ;; enable at first start - no need to manually set "enable", then reboot + ; -D WLED_USE_MY_CONFIG + ; -D WLED_DISABLE_LOXONE + ; -D WLED_DISABLE_ALEXA + ; -D WLED_DISABLE_HUESYNC + ; -D WLED_DISABLE_MQTT + ; -D WLED_DISABLE_INFRARED + ; -D WLED_ENABLE_DMX +; RAM: [=== ] 26.0% (used 85236 bytes from 327680 bytes) +; Flash: [==========] 97.1% (used 1527049 bytes from 1572864 bytes) + +;; experimental +;; PICO environment with ESP-IDF v4.4.4 / arduino-esp32 v2.0.9 +[env:esp32_pico_4MB_V4_S] +extends = esp32_4MB_V4_S_base +board = pico32 +upload_speed = 256000 ;; or 115200 ;; or 460800 ; or 921600 (slower speeds are better when flashing without a soldered connection) + +build_unflags = ${esp32_4MB_V4_S_base.build_unflags} + -D WLED_ENABLE_HUB75MATRIX ;; not enough pins +build_flags = ${esp32_4MB_V4_S_base.esp32_build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=0 ; needed for arduino-esp32 >=2.0.4; avoids errors on startup + -D WLED_RELEASE_NAME=esp32_pico_4MB_V4_S + -D WLED_DISABLE_BROWNOUT_DET + -D SERVERNAME='"WLED-pico32-V4"' + -D WLED_WATCHDOG_TIMEOUT=0 + ; -D WLED_WATCHDOG_TIMEOUT=60 + -D WLED_DISABLE_ADALIGHT ;; WLEDMM this board does not have a serial-to-USB chip. Better to disable serial protocols, to avoid crashes (see upstream #3128) + ${common_mm.NetDebug_build_flags} ;; net debug is not included in normal _S builds + ; -D WLED_DEBUG + ; -D SR_DEBUG + -D LEDPIN=2 + -D RLYPIN=-1 -D BTNPIN=-1 -D IRPIN=-1 + -D HW_PIN_SCL=22 -D HW_PIN_SDA=21 + -D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14 + -D SR_SQUELCH=5 -D SR_GAIN=30 -D SR_FREQ_PROF=5 ; ICS-4343x specific + ; -D MCLK_PIN=0 + -D SR_ENABLE_DEFAULT ;; enable audioreactive at first start - no need to manually set "enable", then reboot + ; -D WLED_USE_MY_CONFIG +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + ${common_mm.HUB75_lib_ignore} +; RAM: [=== ] 27.9% (used 91528 bytes from 327680 bytes) +; Flash: [==========] 96.7% (used 1521457 bytes from 1572864 bytes) +; + + +[env:adafruit_matrixportal_esp32s3_legacy] +; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75 +extends = esp32_4MB_V4_M_base +platform = ${esp32.platformV4_xp} ;; 6.5.0 = first platform release supporting matrixportal +platform_packages = ${esp32.platformV4_packages_xp} ;; arduino-esp32 2.0.14 needed - previous versions were missing files for matrixportal +board = adafruit_matrixportal_esp32s3 + +board_build.partitions = ${esp32.large_partitions} ;; default 8MB WLED partitions +;; board_build.f_flash = 80000000L ;; this is the default for board = adafruit_matrixportal_esp32s3 +;; board_build.flash_mode = qio ;; ditto + +build_unflags = ${env:esp32S3_8MB_M.build_unflags} ;; use the same as "normal" S3 buildenv + -D ARDUINO_USB_CDC_ON_BOOT=1 ;; fix warning: "ARDUINO_USB_CDC_ON_BOOT" redefined; comment out for Serial debug + ${Speed_Flags.build_unflags} ;; to override -Os +build_flags = ${common.build_flags} ${esp32s3.build_flags} -Wno-misleading-indentation -Wno-format-truncation + ${common_mm.build_flags_S} + ${Speed_Flags.build_flags_V4} ;; -O2 -> optimize for speed instead of size + -D WLED_RELEASE_NAME=matrixportal_esp32s3_legacy + -D SERVERNAME='"WLED-MatrixPortalS3"' + ; Serial debug enabled -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=1 ;; for Hardware-CDC USB mode + -D ARDUINO_USB_CDC_ON_BOOT=0 + -D WLED_DISABLE_ADALIGHT ;; disables serial protocols - recommended for Hardware-CDC USB (Serial RX will receive junk commands when RX pin is unconnected, unless its pulled down by resistor) + ${common_mm.animartrix_build_flags} + ${common_mm.build_disable_sync_interfaces} + -D LOLIN_WIFI_FIX ;; try this in case Wifi does not work + -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D WLED_USE_PSRAM -DBOARD_HAS_PSRAM ; tells WLED that PSRAM shall be used + -D WLED_USE_PSRAM_JSON -DALL_JSON_TO_PSRAM ; WLEDMM --> force all JSON stuff into PSRAM; gives more free heap + ; -DCONFIG_MBEDTLS_DYNAMIC_BUFFER=1 ;; optional - seems to move more buffers into PSRAM + ${common_mm.HUB75_build_flags} + -D DEFAULT_LED_TYPE=101 +lib_deps = ${esp32s3.lib_deps} ${common_mm.lib_deps_S} ;; ;; do not include ${esp32.lib_depsV4} !!!! + ${common_mm.animartrix_lib_deps} + ${common_mm.HUB75_lib_deps} + +lib_ignore = IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation +monitor_filters = esp32_exception_decoder +; +; RAM: [== ] 18.4% (used 60456 bytes from 327680 bytes) +; Flash: [======== ] 79.9% (used 1675601 bytes from 2097152 bytes) + +;; this buildenv uses the original bootloader and partions from adafruit +[env:adafruit_matrixportal_esp32s3_tinyUF2] +extends = env:adafruit_matrixportal_esp32s3_legacy + +board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; use original adafruit 8MB tinyUF2 partitioning + +;; we just replace WLED_RELEASE_NAME +build_unflags = ${env:adafruit_matrixportal_esp32s3_legacy.build_unflags} + -D WLED_RELEASE_NAME=matrixportal_esp32s3_legacy +build_flags = ${env:adafruit_matrixportal_esp32s3_legacy.build_flags} + -D WLED_RELEASE_NAME=matrixportal_esp32s3_tinyUF2 +; +; RAM: [== ] 18.4% (used 60456 bytes from 327680 bytes) +; Flash: [======== ] 79.9% (used 1675601 bytes from 2097152 bytes) + +[env:adafruit_matrixportal_esp32s3] +;; this buildenv is just an alias for the matrixportal UF2 build, to keep 3rd party build tools happy. +extends = env:adafruit_matrixportal_esp32s3_tinyUF2 diff --git a/readme.md b/readme.md index dda6634a1e..a44a82b3ae 100644 --- a/readme.md +++ b/readme.md @@ -1,84 +1,36 @@

- - + + - - + + - +

-# Welcome to my project WLED! ✨ +# Welcome to WLED MoonModules! ✨ -A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102! +image -## āš™ļø Features -- WS2812FX library with more than 100 special effects -- FastLED noise effects and 50 palettes -- Modern UI with color, effect and segment controls -- Segments to set different effects and colors to user defined parts of the LED string -- Settings page - configuration via the network -- Access Point and station mode - automatic failsafe AP -- Up to 10 LED outputs per instance -- Support for RGBW strips -- Up to 250 user presets to save and load colors/effects easily, supports cycling through them. -- Presets can be used to automatically execute API calls -- Nightlight function (gradually dims down) -- Full OTA software updatability (HTTP + ArduinoOTA), password protectable -- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods) -- Configurable Auto Brightness limit for safe operation -- Filesystem-based config for easier backup of presets and settings +MoonModules/WLED is a fork of [Aircoookie/WLED](https://github.com/Aircoookie/WLED) which contains latest merge of v0.14 of WLED with [additional features](https://mm.kno.wled.ge/moonmodules/what-is-moonmodules/). -## šŸ’” Supported light control interfaces -- WLED app for [Android](https://play.google.com/store/apps/details?id=com.aircoookie.WLED) and [iOS](https://apps.apple.com/us/app/wled/id1475695033) -- JSON and HTTP request APIs -- MQTT -- E1.31, Art-Net, DDP and TPM2.net -- [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios)) -- [Hyperion](https://github.com/hyperion-project/hyperion.ng) -- UDP realtime -- Alexa voice control (including dimming and color) -- Sync to Philips hue lights -- Adalight (PC ambilight via serial) and TPM2 -- Sync color of multiple WLED devices (UDP notifier) -- Infrared remotes (24-key RGB, receiver required) -- Simple timers/schedules (time from NTP, timezones/DST supported) +This fork is created by members of the [Atuline/WLED](https://github.com/atuline/WLED) team to make development against v0.14 possible while still preserving [Atuline/WLED v0.13.x](https://github.com/atuline/WLED/tree/dev) as a stable and supported version. The Atuline/WLED fork is also called WLED SR (Sound Reactive). -## šŸ“² Quick start guide and documentation +More info here: what-is-moonmodules -See the [documentation on our official site](https://kno.wled.ge)! +HTML tutorial +Donations will be used to buy WLED related hardware, software or drinks shared with the contributors of this repo. -[On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials and tools to help you get your new project up and running! +## License +WLED-MM is licensed under the [EUPL-1.2](https://joinup.ec.europa.eu/collection/eupl) or later. +The official license text is [available in 23 languages](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12). -## šŸ–¼ļø User interface - +## Contributing +We welcome contributions to this project! See [contributing](https://github.com/MoonModules/WLED/blob/mdev/CONTRIBUTING.md) for more information. +> We would like to have this repository in a polite and friendly atmosphere, so please be kind and respectful to others. For more details, look at [Code of Conduct](https://github.com/MoonModules/WLED/blob/mdev/CODE_OF_CONDUCT.md). -## šŸ’¾ Compatible hardware - -See [here](https://kno.wled.ge/basics/compatible-hardware)! - -## āœŒļø Other - -Licensed under the MIT license -Credits [here](https://kno.wled.ge/about/contributors/)! - -Join the Discord server to discuss everything about WLED! - - - -Check out the WLED [Discourse forum](https://wled.discourse.group)! - -You can also send me mails to [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com), but please, only do so if you want to talk to me privately. - -If WLED really brightens up your day, you can [![](https://img.shields.io/badge/send%20me%20a%20small%20gift-paypal-blue.svg?style=flat-square)](https://paypal.me/aircoookie) - - -*Disclaimer:* - -If you are prone to photosensitive epilepsy, we recommended you do **not** use this software. -If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings. - -As per the MIT license, I assume no liability for any damage to you or any other person or equipment. +## *Disclaimer:* +Using this software is the users responsibility as it is not bug free. Therefore contributors of this repo are not reliable for anything including but not limited to spontaneous combustion of the entire led strip, the house and the inevitable heat death of the universe diff --git a/requirements.txt b/requirements.txt index bc536ed074..cca736f5ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ anyio==3.6.2 # via starlette bottle==0.12.25 # via platformio -certifi==2022.12.7 +certifi==2023.7.22 # via requests charset-normalizer==3.1.0 # via requests @@ -34,7 +34,7 @@ marshmallow==3.19.0 # via platformio packaging==23.1 # via marshmallow -platformio==6.1.6 +platformio==6.1.14 # via -r requirements.in pyelftools==0.29 # via platformio @@ -50,7 +50,7 @@ starlette==0.23.1 # via platformio tabulate==0.9.0 # via platformio -urllib3==1.26.15 +urllib3==1.26.18 # via requests uvicorn==0.20.0 # via platformio diff --git a/tools/ESP32-Chip_info.hpp b/tools/ESP32-Chip_info.hpp new file mode 100644 index 0000000000..f9c5ebecf4 --- /dev/null +++ b/tools/ESP32-Chip_info.hpp @@ -0,0 +1,620 @@ +#pragma once + +/* A tool that print some ESP32 hardware information + * to serial monitor. + * main function: showRealSpeed(); + */ + + +/* code below is based on https://github.com/Jason2866/ESP32_Show_Info + * that was created by Jason2866, subject to the GNU General Public License v3.0 + * detailed license conditions: https://github.com/Jason2866/ESP32_Show_Info/blob/main/LICENSE + */ +#include + +#ifdef ARDUINO_ARCH_ESP32 + +#include "soc/spi_reg.h" +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) + #include "esp_chip_info.h" // esp-idf v4.4.x +#else + #include "esp_system.h" // esp-idf v3.x +#endif + +#if CONFIG_IDF_TARGET_ESP32 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) + #include "esp32/rom/spi_flash.h" + #include "esp32/spiram.h" + #include "spiram_psram.h" +#else + #include "esp_spi_flash.h" + #include "rom/spi_flash.h" + #include "esp_spiram.h" + //#include "spiram_psram.h" +#endif +#elif CONFIG_IDF_TARGET_ESP32S2 // ESP32-S2 + #include "esp32s2/rom/spi_flash.h" +#elif CONFIG_IDF_TARGET_ESP32S3 // ESP32-S3 + #include "esp32s3/rom/spi_flash.h" +#elif CONFIG_IDF_TARGET_ESP32C3 // ESP32-C3 + #include "esp32c3/rom/spi_flash.h" +#endif + +#include +#include + +#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 + #ifndef REG_SPI_BASE + #define REG_SPI_BASE(i) (DR_REG_SPI1_BASE + (((i)>1) ? (((i)* 0x1000) + 0x20000) : (((~(i)) & 1)* 0x1000 ))) + #endif // REG_SPI_BASE +#endif // TARGET + +uint32_t my_ESP_getFlashChipId(void) +{ + uint32_t id = g_rom_flashchip.device_id; + id = ((id & 0xff) << 16) | ((id >> 16) & 0xff) | (id & 0xff00); + return id; +} + +uint32_t my_ESP_getFlashChipRealSize(void) +{ + uint32_t id = (my_ESP_getFlashChipId() >> 16) & 0xFF; + return 2 << (id - 1); +} + +String my_ESP_getFlashChipMode(void) { +#if CONFIG_IDF_TARGET_ESP32S2 +const uint32_t spi_ctrl = REG_READ(PERIPHS_SPI_FLASH_CTRL); +#else +const uint32_t spi_ctrl = REG_READ(SPI_CTRL_REG(0)); +#endif +/* Not all of the following constants are already defined in older versions of spi_reg.h, so do it manually for now*/ +if (spi_ctrl & BIT(24)) { //SPI_FREAD_QIO + return F("QIO"); +} else if (spi_ctrl & BIT(20)) { //SPI_FREAD_QUAD + return F("QOUT"); +} else if (spi_ctrl & BIT(23)) { //SPI_FREAD_DIO + return F("DIO"); +} else if (spi_ctrl & BIT(14)) { // SPI_FREAD_DUAL + return F("DOUT"); +} else if (spi_ctrl & BIT(13)) { //SPI_FASTRD_MODE + return F("Fast"); +} else { + return F("Slow"); +} +return F("DOUT"); +} + + +//******** Flash Chip Speed is NOT correct !!!! ***** +uint32_t my_ESP_getFlashChipSpeed(void) +{ + const uint32_t spi_clock = REG_READ(SPI_CLOCK_REG(0)); + if (spi_clock & BIT(31)) { + // spi_clk is equal to system clock + return getApbFrequency(); + } + return spiClockDivToFrequency(spi_clock); +} + +String my_GetDeviceHardware(void) { + // https://www.espressif.com/en/products/socs + +/* +Source: esp-idf esp_system.h and esptool + +typedef enum { + CHIP_ESP32 = 1, //!< ESP32 + CHIP_ESP32S2 = 2, //!< ESP32-S2 + CHIP_ESP32S3 = 9, //!< ESP32-S3 + CHIP_ESP32C3 = 5, //!< ESP32-C3 + CHIP_ESP32H2 = 6, //!< ESP32-H2 + CHIP_ESP32C2 = 12, //!< ESP32-C2 +} esp_chip_model_t; + +// Chip feature flags, used in esp_chip_info_t +#define CHIP_FEATURE_EMB_FLASH BIT(0) //!< Chip has embedded flash memory +#define CHIP_FEATURE_WIFI_BGN BIT(1) //!< Chip has 2.4GHz WiFi +#define CHIP_FEATURE_BLE BIT(4) //!< Chip has Bluetooth LE +#define CHIP_FEATURE_BT BIT(5) //!< Chip has Bluetooth Classic +#define CHIP_FEATURE_IEEE802154 BIT(6) //!< Chip has IEEE 802.15.4 +#define CHIP_FEATURE_EMB_PSRAM BIT(7) //!< Chip has embedded psram + + +// The structure represents information about the chip +typedef struct { + esp_chip_model_t model; //!< chip model, one of esp_chip_model_t + uint32_t features; //!< bit mask of CHIP_FEATURE_x feature flags + uint8_t cores; //!< number of CPU cores + uint8_t revision; //!< chip revision number +} esp_chip_info_t; + +*/ + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + + uint32_t chip_model = chip_info.model; + uint32_t chip_revision = chip_info.revision; +// uint32_t chip_revision = ESP.getChipRevision(); + bool rev3 = (3 == chip_revision); +// bool single_core = (1 == ESP.getChipCores()); + bool single_core = (1 == chip_info.cores); + + if (chip_model < 2) { // ESP32 +#ifdef CONFIG_IDF_TARGET_ESP32 +/* esptool: + def get_pkg_version(self): + word3 = self.read_efuse(3) + pkg_version = (word3 >> 9) & 0x07 + pkg_version += ((word3 >> 2) & 0x1) << 3 + return pkg_version +*/ + uint32_t chip_ver = REG_GET_FIELD(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_VER_PKG); + uint32_t pkg_version = chip_ver & 0x7; + +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision, chip_info.cores, chip_ver); + + switch (pkg_version) { + case 0: + if (single_core) { return F("ESP32-S0WDQ6"); } // Max 240MHz, Single core, QFN 6*6 + else if (rev3) { return F("ESP32-D0WDQ6-V3"); } // Max 240MHz, Dual core, QFN 6*6 + else { return F("ESP32-D0WDQ6"); } // Max 240MHz, Dual core, QFN 6*6 + case 1: + if (single_core) { return F("ESP32-S0WD"); } // Max 160MHz, Single core, QFN 5*5, ESP32-SOLO-1, ESP32-DevKitC + else if (rev3) { return F("ESP32-D0WD-V3"); } // Max 240MHz, Dual core, QFN 5*5, ESP32-WROOM-32E, ESP32_WROVER-E, ESP32-DevKitC + else { return F("ESP32-D0WD"); } // Max 240MHz, Dual core, QFN 5*5, ESP32-WROOM-32D, ESP32_WROVER-B, ESP32-DevKitC + case 2: return F("ESP32-D2WD"); // Max 160MHz, Dual core, QFN 5*5, 2MB embedded flash + case 3: + if (single_core) { return F("ESP32-S0WD-OEM"); } // Max 160MHz, Single core, QFN 5*5, Xiaomi Yeelight + else { return F("ESP32-D0WD-OEM"); } // Max 240MHz, Dual core, QFN 5*5 + case 4: + if (single_core) { return F("ESP32-U4WDH-S"); } // Max 160MHz, Single core, QFN 5*5, 4MB embedded flash, ESP32-MINI-1, ESP32-DevKitM-1 + else { return F("ESP32-U4WDH-D"); } // Max 240MHz, Dual core, QFN 5*5, 4MB embedded flash + case 5: + if (rev3) { return F("ESP32-PICO-V3"); } // Max 240MHz, Dual core, LGA 7*7, ESP32-PICO-V3-ZERO, ESP32-PICO-V3-ZERO-DevKit + else { return F("ESP32-PICO-D4"); } // Max 240MHz, Dual core, LGA 7*7, 4MB embedded flash, ESP32-PICO-KIT + case 6: return F("ESP32-PICO-V3-02"); // Max 240MHz, Dual core, LGA 7*7, 8MB embedded flash, 2MB embedded PSRAM, ESP32-PICO-MINI-02, ESP32-PICO-DevKitM-2 + case 7: return F("ESP32-D0WDR2-V3"); // Max 240MHz, Dual core, QFN 5*5, ESP32-WROOM-32E, ESP32_WROVER-E, ESP32-DevKitC + } +#endif // CONFIG_IDF_TARGET_ESP32 + return F("ESP32"); + } + else if (2 == chip_model) { // ESP32-S2 +#ifdef CONFIG_IDF_TARGET_ESP32S2 +/* esptool: + def get_flash_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version + + def get_psram_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 28) & 0x0F + return pkg_version +*/ + uint32_t chip_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_FLASH_VERSION); + uint32_t psram_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PSRAM_VERSION); + uint32_t pkg_version = (chip_ver & 0xF) + ((psram_ver & 0xF) * 100); + +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision, chip_info.cores, chip_ver); + + switch (pkg_version) { + case 0: return F("ESP32-S2"); // Max 240MHz, Single core, QFN 7*7, ESP32-S2-WROOM, ESP32-S2-WROVER, ESP32-S2-Saola-1, ESP32-S2-Kaluga-1 + case 1: return F("ESP32-S2FH2"); // Max 240MHz, Single core, QFN 7*7, 2MB embedded flash, ESP32-S2-MINI-1, ESP32-S2-DevKitM-1 + case 2: return F("ESP32-S2FH4"); // Max 240MHz, Single core, QFN 7*7, 4MB embedded flash + case 3: return F("ESP32-S2FN4R2"); // Max 240MHz, Single core, QFN 7*7, 4MB embedded flash, 2MB embedded PSRAM, , ESP32-S2-MINI-1U, ESP32-S2-DevKitM-1U + case 100: return F("ESP32-S2R2"); + case 102: return F("ESP32-S2FNR2"); // Max 240MHz, Single core, QFN 7*7, 4MB embedded flash, 2MB embedded PSRAM, , Lolin S2 mini + } +#endif // CONFIG_IDF_TARGET_ESP32S2 + return F("ESP32-S2"); + } + else if (9 == chip_model) { // ESP32-S3 +#ifdef CONFIG_IDF_TARGET_ESP32S3 + // no variants for now +#endif // CONFIG_IDF_TARGET_ESP32S3 + return F("ESP32-S3"); // Max 240MHz, Dual core, QFN 7*7, ESP32-S3-WROOM-1, ESP32-S3-DevKitC-1 + } + else if (5 == chip_model) { // ESP32-C3 +#ifdef CONFIG_IDF_TARGET_ESP32C3 +/* esptool: + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version +*/ + uint32_t chip_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PKG_VERSION); + uint32_t pkg_version = chip_ver & 0x7; +// uint32_t pkg_version = esp_efuse_get_pkg_ver(); + +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision, chip_info.cores, chip_ver); + + switch (pkg_version) { + case 0: return F("ESP32-C3"); // Max 160MHz, Single core, QFN 5*5, ESP32-C3-WROOM-02, ESP32-C3-DevKitC-02 + case 1: return F("ESP32-C3FH4"); // Max 160MHz, Single core, QFN 5*5, 4MB embedded flash, ESP32-C3-MINI-1, ESP32-C3-DevKitM-1 + } +#endif // CONFIG_IDF_TARGET_ESP32C3 + return F("ESP32-C3"); + } + else if (6 == chip_model) { // ESP32-S3(beta3) + return F("ESP32-S3"); + } + else if (7 == chip_model) { // ESP32-C6(beta) +#ifdef CONFIG_IDF_TARGET_ESP32C6 +/* esptool: + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version +*/ + uint32_t chip_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PKG_VERSION); + uint32_t pkg_version = chip_ver & 0x7; +// uint32_t pkg_version = esp_efuse_get_pkg_ver(); + +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision, chip_info.cores, chip_ver); + + switch (pkg_version) { + case 0: return F("ESP32-C6"); + } +#endif // CONFIG_IDF_TARGET_ESP32C6 + return F("ESP32-C6"); + } + else if (10 == chip_model) { // ESP32-H2 +#ifdef CONFIG_IDF_TARGET_ESP32H2 +/* esptool: + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version +*/ + uint32_t chip_ver = REG_GET_FIELD(EFUSE_RD_MAC_SPI_SYS_3_REG, EFUSE_PKG_VERSION); + uint32_t pkg_version = chip_ver & 0x7; +// uint32_t pkg_version = esp_efuse_get_pkg_ver(); + +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HDW: ESP32 Model %d, Revision %d, Core %d, Package %d"), chip_info.model, chip_revision, chip_info.cores, chip_ver); + + switch (pkg_version) { + case 0: return F("ESP32-H2"); + } +#endif // CONFIG_IDF_TARGET_ESP32H2 + return F("ESP32-H2"); + } + return F("ESP32"); +} + +String my_GetDeviceHardwareRevision(void) { + // ESP32-S2 + // ESP32-D0WDQ6 rev.1 + // ESP32-C3 rev.2 + // ESP32-C3 rev.3 + String result = my_GetDeviceHardware(); // ESP32-C3 + + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + char revision[24] = { 0 }; + if (chip_info.revision) { + snprintf_P(revision, sizeof(revision), PSTR(" rev.%d (0x%x)"), chip_info.revision, chip_info.revision); + } + result += revision; // ESP32-C3 rev.3 +#if CONFIG_ESP32_ECO3_CACHE_LOCK_FIX + result += soc_has_cache_lock_bug() ? F(", chip has cache lock bug") : F(", free of cache lock bug"); +#endif + + return result; +} + +static void my_show_chip_info(void) { + #ifndef CHIP_FEATURE_IEEE802154 + #define CHIP_FEATURE_IEEE802154 BIT(6) //!< Chip has IEEE 802.15.4 + #endif + #ifndef CHIP_FEATURE_EMB_PSRAM + #define CHIP_FEATURE_EMB_PSRAM BIT(7) //!< Chip has embedded psram + #endif + + esp_chip_info_t my_info; + //Serial.println(""); + esp_chip_info(&my_info); +#if 0 + Serial.println("ESP Chip Info:"); + switch ((int)my_info.model) { + case 1: Serial.print("ESP32 "); break; + case 2: Serial.print("ESP32-S2 "); break; + case 9: Serial.print("ESP32-S3 "); break; + case 5: Serial.print("ESP32-C3 "); break; + case 6: Serial.print("ESP32-H2/H4 "); break; // ESP-IFD 4.x + case 12: Serial.print("ESP32-C2 "); break; + case 13: Serial.print("ESP32-C6 "); break; + case 16: Serial.print("ESP32-H2 "); break; // ESP-IFD 5.x + case 17: Serial.print("ESP32-C5 beta3 "); break; + case 18: Serial.print("ESP32-P4 "); break; + case 999: Serial.print("POSIX/Linux simulator "); break; + default: Serial.print("(unknown) 0x"); Serial.print((int)my_info.model, HEX); Serial.print(" "); break; + } + Serial.print(" Rev "); + Serial.print(my_info.revision); + Serial.print(", "); + Serial.print(my_info.cores); + Serial.println(" cores."); + Serial.flush(); +#endif + + Serial.print("Chip feature flags: 0b"); + Serial.println(my_info.features, BIN); + if (my_info.features & CHIP_FEATURE_EMB_FLASH) Serial.println(" * 0b0000001 Chip has embedded FLASH memory"); + if (my_info.features & CHIP_FEATURE_WIFI_BGN) Serial.println(" * 0b0000010 Chip has 2.4GHz WiFi"); + if (my_info.features & CHIP_FEATURE_BLE) Serial.println(" * 0b0001000 Chip has Bluetooth LE"); + if (my_info.features & CHIP_FEATURE_BT) Serial.println(" * 0b0010000 Chip has Bluetooth Classic"); + if (my_info.features & CHIP_FEATURE_IEEE802154) Serial.println(" * 0b0100000 Chip has IEEE 802.15.4"); + if (my_info.features & CHIP_FEATURE_EMB_PSRAM) Serial.println(" * 0b1000000 Chip has embedded PSRAM"); + Serial.println(); +} + + +// see https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/ResetReason/ResetReason.ino +/* Print last reset reason of ESP32 +* ================================= +* Use either of the methods print_reset_reason +* or verbose_print_reset_reason to display the +* cause for the last reset of this device. +* +* Public Domain License. +* Author: +* Evandro Luis Copercini - 2017 +*/ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) +#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4 +#include +#elif CONFIG_IDF_TARGET_ESP32S2 +#include +#elif CONFIG_IDF_TARGET_ESP32C3 +#include +#elif CONFIG_IDF_TARGET_ESP32S3 +#include +#else +#error Target CONFIG_IDF_TARGET is not supported +#endif +#else // ESP32 Before IDF 4.0 +#include +#endif + +#if 0 +// ESP8266 does this a bit differently +// +// include +// +// struct rst_info * rst_info; +// system_rtc_mem_read(0, &rst_info, sizeof(rst_info)); +// reason = rst_info->reason; +// or +// reason = ESP.getResetInfoPtr()->reason; +// +// REASON_DEFAULT_RST = 0, /* normal startup by power on */ +// REASON_WDT_RST = 1, /* hardware watch dog reset */ +// REASON_EXCEPTION_RST = 2, /* exception reset, GPIO status won’t change */ +// REASON_SOFT_WDT_RST = 3, /* software watch dog reset, GPIO status won’t change */ +// REASON_SOFT_RESTART = 4, /* software restart ,system_restart , GPIO status won’t change */ +// REASON_DEEP_SLEEP_AWAKE = 5, /* wake up from deep-sleep */ +// REASON_EXT_SYS_RST = 6 /* external system reset */ +// reason > 6 = user-defined reason codes +#endif + +#if 0 // duplicate - this info is also printed by getCoreResetReason() +void my_print_reset_reason(int reason) +{ + Serial.print(reason); + switch (reason) + { + case 0 : Serial.print (" NO_MEAN"); break; + case 1 : Serial.print (" POWERON_RESET");break; /**<1, Vbat power on reset*/ + case 2 : Serial.print (" unknown exception"); break; /**<2, this code is not defined on ESP32 */ + case 3 : Serial.print (" SW_RESET");break; /**<3, Software reset digital core*/ + case 4 : Serial.print (" OWDT_RESET");break; /**<4, Legacy watch dog reset digital core*/ + case 5 : Serial.print (" DEEPSLEEP_RESET");break; /**<5, Deep Sleep wakeup reset digital core*/ + case 6 : Serial.print (" SDIO_RESET");break; /**<6, Reset by SLC module, reset digital core*/ + case 7 : Serial.print (" TG0WDT_SYS_RESET");break; /**<7, Timer Group0 Watch dog reset digital core*/ + case 8 : Serial.print (" TG1WDT_SYS_RESET");break; /**<8, Timer Group1 Watch dog reset digital core*/ + case 9 : Serial.print (" RTCWDT_SYS_RESET");break; /**<9, RTC Watch dog Reset digital core*/ + case 10 : Serial.print(" INTRUSION_RESET");break; /**<10, Instrusion tested to reset CPU*/ + case 11 : Serial.print(" TGWDT_CPU_RESET");break; /**<11, Time Group reset CPU*/ + case 12 : Serial.print(" SW_CPU_RESET");break; /**<12, Software reset CPU*/ + case 13 : Serial.print(" RTCWDT_CPU_RESET");break; /**<13, RTC Watch dog Reset CPU*/ + case 14 : Serial.print(" EXT_CPU_RESET");break; /**<14, for APP CPU, reset by PRO CPU*/ + case 15 : Serial.print(" RTCWDT_BROWN_OUT_RESET");break; /**<15, Reset when the vdd voltage is not stable*/ + case 16 : Serial.print(" RTCWDT_RTC_RESET");break; /**<16, RTC Watch dog reset digital core and rtc module*/ + case 17 : Serial.print(" TG1WDT_CPU_RESET");break; /**<17, Time Group1 reset CPU*/ + case 18 : Serial.print(" SUPER_WDT_RESET");break; /**<18, super watchdog reset digital core and rtc module*/ + case 19 : Serial.print(" GLITCH_RTC_RESET");break; /**<19, glitch reset digital core and rtc module*/ + case 20 : Serial.print(" EFUSE_RESET");break; /**<20, efuse reset digital core*/ + case 21 : Serial.print(" USB_UART_CHIP_RESET");break; /**<21, usb uart reset digital core */ + case 22 : Serial.print(" USB_JTAG_CHIP_RESET");break; /**<22, usb jtag reset digital core */ + case 23 : Serial.print(" POWER_GLITCH_RESET");break; /**<23, power glitch reset digital core and rtc module*/ + default : Serial.print (" NO_MEAN"); + } +} +void my_verbose_print_reset_reason(int reason) +{ + switch (reason) + { + case 0 : Serial.print ("none"); break; + case 1 : Serial.print ("Vbat power on reset");break; + case 3 : Serial.print ("Software reset digital core");break; + case 4 : Serial.print ("Legacy watch dog reset digital core");break; + case 5 : Serial.print ("Deep Sleep reset digital core");break; + case 6 : Serial.print ("Reset by SLC module, reset digital core");break; + case 7 : Serial.print ("Timer Group0 Watch dog reset digital core");break; + case 8 : Serial.print ("Timer Group1 Watch dog reset digital core");break; + case 9 : Serial.print ("RTC Watch dog Reset digital core");break; + case 10 : Serial.print ("Instrusion tested to reset CPU");break; + case 11 : Serial.print ("Time Group reset CPU");break; + case 12 : Serial.print ("Software reset CPU");break; + case 13 : Serial.print ("RTC Watch dog Reset CPU");break; + case 14 : Serial.print ("APP CPU reset by PRO CPU");break; + case 15 : Serial.print ("Reset when the vdd voltage is not stable");break; + case 16 : Serial.print ("RTC Watch dog reset digital core and rtc module");break; + default : Serial.print ("other "); Serial.print(reason); + } +} +#endif + +/* + * parts below were created by softhack007, licenced under EUPL-1.2 + */ + +void show_psram_info_part1(void) +{ +#if defined(BOARD_HAS_PSRAM) || defined(WLED_USE_PSRAM) + //if (esp_spiram_is_initialized() == false) esp_spiram_init(); + Serial.println(psramFound() ? "ESP32 PSRAM: found.": "ESP32 PSRAM: not found!"); + if (!psramFound()) return; + //psramInit(); // already doe by arduino framework + + // the next part only works on "classic" ESP32 + #if !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) + esp_spiram_size_t my_psram_size; + if (esp_spiram_is_initialized() == false) esp_spiram_init(); + my_psram_size = esp_spiram_get_chip_size(); + switch (my_psram_size) { + case ESP_SPIRAM_SIZE_16MBITS: Serial.println("* SPI RAM Chip availeable: 16MBits = 2MBytes"); break; + case ESP_SPIRAM_SIZE_32MBITS: Serial.println("* SPI RAM Chip availeable: 32MBits = 4MBytes"); break; + case ESP_SPIRAM_SIZE_64MBITS: Serial.println("* SPI RAM Chip availeable: 64MBits = 8MBytes"); break; + case ESP_SPIRAM_SIZE_INVALID: Serial.println("* SPI RAM size is invalid"); break; + default: Serial.print("* ?SPI RAM size is unknown (0x"); Serial.print(my_psram_size, HEX); Serial.println(")"); break; + } + #endif + #endif +#endif +} + +void show_psram_info_part2(void) +{ +#if defined(BOARD_HAS_PSRAM) || defined(WLED_USE_PSRAM) + if (!psramFound()) return; + + // usually the next part won't work ... + #ifdef PSRAM_MODE + switch (PSRAM_MODE) { + case PSRAM_VADDR_MODE_NORMAL: Serial.println("* PSRAM mode = PSRAM_VADDR_MODE_NORMAL"); break; + case PSRAM_VADDR_MODE_LOWHIGH: Serial.println("* PSRAM mode = SRAM_VADDR_MODE_LOWHIGH"); break; + case PSRAM_VADDR_MODE_EVENODD: Serial.println("* PSRAM mode = PSRAM_VADDR_MODE_EVENODD"); break; + } + #endif + #ifdef PSRAM_SPEED + switch (PSRAM_SPEED) { + case PSRAM_CACHE_F40M_S40M: Serial.println("* PSRAM speed = PSRAM_CACHE_F40M_S40M -> speed=40 esptool_flash=40 (by config)"); break; + case PSRAM_CACHE_F80M_S40M: Serial.println("* PSRAM speed = PSRAM_CACHE_F80M_S40M -> speed=40 esptool_flash=80 (by config)"); break; + case PSRAM_CACHE_F80M_S80M: Serial.println("* PSRAM speed = PSRAM_CACHE_F80M_S80M -> speed=80 esptool_flash=80 (by config)"); break; + } + #endif + + #if 0 // this test makes the "max used PSRAM" info unusable + // try to allocate PSRAM (one 640KB chunk so we can be sure it will not fit into DRAM) + void * buff2 = ps_malloc(640 * 1024); + uint8_t * buf = (uint8_t*)malloc(620 * 1024); + Serial.print("* PSRAM free after malloc / ps_malloc : "); Serial.print(ESP.getFreePsram() / 1024.0, 2); Serial.println(" KB (1.2MB allocated)"); + if (buff2 == NULL) + Serial.println("* can't allocate big memory with ps_malloc()"); + else { + Serial.println("* Can allocate big memory with ps_malloc()"); + free(buff2); + } + if (buf == NULL) + Serial.println("* can't allocate big memory with malloc()"); + else { + free(buf); + Serial.println("* Can allocate big memory with malloc()"); + } + #endif + +#endif +} + +void showRealSpeed() { + //Serial.begin(115200); + if (!Serial) return; // Avoid writing to unconnected USB-CDC + + Serial.flush(); + Serial.println(F("\n")); + for(int aa=0; aa<65; aa++) Serial.print("="); Serial.println(); +#if 0 // duplicate - same info is printed in wled.cpp + Serial.print( F("Chip info for ")); Serial.print(ESP.getChipModel()); + Serial.print(F(", ")); Serial.print(ESP.getChipCores()); Serial.print(F(" core(s)")); + Serial.print(F(", ")); Serial.print(ESP.getCpuFreqMHz()); Serial.println(F("MHz.")); +#endif + Serial.print( F("ESP32 device: ")); Serial.println(my_GetDeviceHardwareRevision()); + Serial.print( F("SDK: ")); Serial.println(ESP.getSdkVersion()); + for(int aa=0; aa<42; aa++) Serial.print("-"); Serial.println(); + + my_show_chip_info(); + + Serial.print(" XTAL FREQ: "); Serial.print(getXtalFrequencyMhz()); Serial.println(" MHz"); + Serial.print(" APB FREQ: "); Serial.print(getApbFrequency() / 1000000.0, 1); Serial.println(" MHz"); + Serial.print(" CPU FREQ: "); Serial.print(getCpuFrequencyMhz()); Serial.println(" MHz\n"); + for(int aa=0; aa<42; aa++) Serial.print("-"); Serial.println("\n"); + + Serial.print("FLASH CHIP FREQ (magic): "); Serial.print(ESP.getFlashChipSpeed()/1000000.0, 1); Serial.println(" MHz"); + Serial.print("FLASH SIZE (magic byte): "); Serial.print(ESP.getFlashChipSize() / (1024.0 * 1024), 2); Serial.println(" MB"); + Serial.print("FLASH MODE (magic byte): "); Serial.print(ESP.getFlashChipMode()); Serial.println(" ; 0=QIO, 1=QOUT, 2=DIO, 3=DOUT or other\n"); + + Serial.flush(); + Serial.print("FLASH CHIP ID: 0x"); Serial.println(my_ESP_getFlashChipId(), HEX); +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + //Serial.print("FLASH CHIP FREQ: "); Serial.print(my_ESP_getFlashChipSpeed() / 1000000.0, 1); Serial.println(" MHz"); // this seems to crash on -S2 +#endif + Serial.print("FLASH REAL SIZE: "); Serial.print(my_ESP_getFlashChipRealSize() / (1024.0 * 1024), 2); Serial.println(" MB"); + Serial.print("FLASH REAL MODE: "); Serial.println(my_ESP_getFlashChipMode()); + + for(int aa=0; aa<42; aa++) Serial.print("-"); Serial.println(); + Serial.flush(); + Serial.print( "RAM HEAP SIZE: "); Serial.print(ESP.getHeapSize() / 1024.0, 2); Serial.println(" KB"); + Serial.print( " FREE RAM: "); Serial.print(ESP.getFreeHeap() / 1024.0, 2); Serial.println(" KB"); + Serial.print( " MAX RAM alloc: "); Serial.print(ESP.getMaxAllocHeap() / 1024.0, 2); Serial.println(" KB"); + +#if defined(BOARD_HAS_PSRAM) || defined(WLED_USE_PSRAM) + Serial.println(); + show_psram_info_part1(); + if (psramFound()) { + Serial.print(" total PSRAM: "); Serial.print(ESP.getPsramSize() / 1024.0, 0); Serial.println(" KB"); + Serial.print(" FREE PSRAM: "); Serial.print(ESP.getFreePsram() / 1024.0, 2); Serial.println(" KB"); + Serial.print(" MAX PSRAM alloc:"); Serial.print(ESP.getMaxAllocPsram() / 1024.0, 2); Serial.println(" KB"); + Serial.print(" used PSRAM: "); Serial.print(ESP.getPsramSize() - ESP.getFreePsram()); Serial.println(" Bytes"); + Serial.println(); + show_psram_info_part2(); + } + Serial.flush(); +#endif + +#if 0 // duplicate - this info is also printed by getCoreResetReason() + Serial.println(); + Serial.print("CPU #0 - last reset reason = "); + my_print_reset_reason(rtc_get_reset_reason(0)); Serial.print("\t => "); + my_verbose_print_reset_reason(rtc_get_reset_reason(0)); + Serial.println(); + if (ESP.getChipCores() > 1) { + Serial.print("CPU #1 - last reset reason = "); + my_print_reset_reason(rtc_get_reset_reason(1));Serial.print("\t => "); + my_verbose_print_reset_reason(rtc_get_reset_reason(1)); + Serial.println(); + } +#endif + + for(int aa=0; aa<42; aa++) Serial.print("="); Serial.println("\n"); + Serial.flush(); +} + + +#else + #error this tool only supports ESP32 chips +#endif \ No newline at end of file diff --git a/tools/WLED_ESP32-wrover_4MB.csv b/tools/WLED_ESP32-wrover_4MB.csv deleted file mode 100644 index a179a89d0e..0000000000 --- a/tools/WLED_ESP32-wrover_4MB.csv +++ /dev/null @@ -1,6 +0,0 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x180000, -app1, app, ota_1, 0x190000,0x180000, -spiffs, data, spiffs, 0x310000,0xF0000, diff --git a/tools/WLED_ESP32_16MB_9MB_FS.csv b/tools/WLED_ESP32_16MB_9MB_FS.csv new file mode 100644 index 0000000000..f2f3f7783a --- /dev/null +++ b/tools/WLED_ESP32_16MB_9MB_FS.csv @@ -0,0 +1,8 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x300000, +app1, app, ota_1, 0x310000,0x300000, +spiffs, data, spiffs, 0x610000,0x9E0000, +coredump, data, coredump,,64K +# to create/use ffat, see https://github.com/marcmerlin/esp32_fatfsimage \ No newline at end of file diff --git a/tools/WLED_ESP32_4MB_256KB_FS.csv b/tools/WLED_ESP32_4MB_256KB_FS.csv new file mode 100644 index 0000000000..e54c22bbdc --- /dev/null +++ b/tools/WLED_ESP32_4MB_256KB_FS.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1D0000, +app1, app, ota_1, 0x1E0000,0x1D0000, +spiffs, data, spiffs, 0x3B0000,0x40000, +coredump, data, coredump,,64K \ No newline at end of file diff --git a/tools/WLED_ESP32_4MB_512KB_FS.csv b/tools/WLED_ESP32_4MB_512KB_FS.csv new file mode 100644 index 0000000000..5281a61244 --- /dev/null +++ b/tools/WLED_ESP32_4MB_512KB_FS.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1B0000, +app1, app, ota_1, 0x1C0000,0x1B0000, +spiffs, data, spiffs, 0x370000,0x80000, +coredump, data, coredump,,64K \ No newline at end of file diff --git a/tools/WLED_ESP32_4MB_700k_FS.csv b/tools/WLED_ESP32_4MB_700k_FS.csv new file mode 100644 index 0000000000..39c88e5437 --- /dev/null +++ b/tools/WLED_ESP32_4MB_700k_FS.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1A0000, +app1, app, ota_1, 0x1B0000,0x1A0000, +spiffs, data, spiffs, 0x350000,0xB0000, diff --git a/tools/WLED_ESP32_8MB.csv b/tools/WLED_ESP32_8MB.csv index 5e930b89a2..3cf3afc342 100644 --- a/tools/WLED_ESP32_8MB.csv +++ b/tools/WLED_ESP32_8MB.csv @@ -3,4 +3,5 @@ nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x200000, app1, app, ota_1, 0x210000,0x200000, -spiffs, data, spiffs, 0x410000,0x3F0000, \ No newline at end of file +spiffs, data, spiffs, 0x410000,0x3E0000, +coredump, data, coredump,,64K diff --git a/tools/WLED_ESP32_8MB_1500KB_FS.csv b/tools/WLED_ESP32_8MB_1500KB_FS.csv new file mode 100644 index 0000000000..9f732bf8e9 --- /dev/null +++ b/tools/WLED_ESP32_8MB_1500KB_FS.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x330000, +app1, app, ota_1, 0x340000,0x330000, +spiffs, data, spiffs, 0x670000,0x180000, +coredump, data, coredump,,64K diff --git a/tools/cdata.js b/tools/cdata.js index 97000436b9..561d390ef8 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -63,6 +63,7 @@ function adoptVersionAndRepo(html) { // Replace we html = strReplace(html, "https://github.com/atuline/WLED", repoUrl); html = strReplace(html, "https://github.com/Aircoookie/WLED", repoUrl); + html = strReplace(html, "https://github.com/MoonModules/WLED", repoUrl); //WLEDMM } let version = packageJson.version; if (version) { @@ -134,7 +135,7 @@ function writeHtmlGzipped(sourceFile, resultFile, page) { * Binary array for the Web UI. * gzip is used for smaller size and improved speeds. * - * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui + * Please see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ @@ -199,7 +200,7 @@ function writeChunks(srcDir, specs, resultFile) { let src = `/* * More web UI HTML source arrays. * This file is auto generated, please don't make any changes manually. - * Instead, see https://kno.wled.ge/advanced/custom-features/#changing-web-ui + * Instead, see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ `; @@ -222,6 +223,7 @@ writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index'); writeHtmlGzipped("wled00/data/simple.htm", "wled00/html_simple.h", 'simple'); writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart'); writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal'); +writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic'); /* writeChunks( "wled00/data", @@ -401,6 +403,13 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; method: "gzip", filter: "html-minify", }, + //WLEDMM + { + file: "peek.js", + name: "PAGE_peekJs", + method: "gzip", + filter: "js-minify", + }, { file: "404.htm", name: "PAGE_404", diff --git a/tools/partitions-4MB_spiffs-tinyuf2.csv b/tools/partitions-4MB_spiffs-tinyuf2.csv new file mode 100644 index 0000000000..4979c12722 --- /dev/null +++ b/tools/partitions-4MB_spiffs-tinyuf2.csv @@ -0,0 +1,11 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +# bootloader.bin,, 0x1000, 32K +# partition table, 0x8000, 4K + +nvs, data, nvs, 0x9000, 20K, +otadata, data, ota, 0xe000, 8K, +ota_0, 0, ota_0, 0x10000, 1408K, +ota_1, 0, ota_1, 0x170000, 1408K, +uf2, app, factory,0x2d0000, 256K, +spiffs, data, spiffs, 0x310000, 960K, diff --git a/tools/partitions-8MB_spiffs-tinyuf2.csv b/tools/partitions-8MB_spiffs-tinyuf2.csv new file mode 100644 index 0000000000..27ed4c2d6a --- /dev/null +++ b/tools/partitions-8MB_spiffs-tinyuf2.csv @@ -0,0 +1,10 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +# bootloader.bin,, 0x1000, 32K +# partition table,, 0x8000, 4K +nvs, data, nvs, 0x9000, 20K, +otadata, data, ota, 0xe000, 8K, +ota_0, app, ota_0, 0x10000, 2048K, +ota_1, app, ota_1, 0x210000, 2048K, +uf2, app, factory,0x410000, 256K, +spiffs, data, spiffs, 0x450000, 3776K, diff --git a/usermods/Analog_Clock/Analog_Clock.h b/usermods/Analog_Clock/Analog_Clock.h index 596f0acb3b..9d82f7670c 100644 --- a/usermods/Analog_Clock/Analog_Clock.h +++ b/usermods/Analog_Clock/Analog_Clock.h @@ -102,9 +102,9 @@ class AnalogClockUsermod : public Usermod { void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { uint32_t ms = time.ms % 1000; - uint8_t b0 = (cos8(ms * 64 / 1000) - 128) * 2; + uint8_t b0 = (cos8_t(ms * 64 / 1000) - 128) * 2; setPixelColor(secondLed, gamma32(scale32(secondColor, b0))); - uint8_t b1 = (sin8(ms * 64 / 1000) - 128) * 2; + uint8_t b1 = (sin8_t(ms * 64 / 1000) - 128) * 2; setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1))); } diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 151cf1d4a4..9365a17e2b 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -16,7 +16,7 @@ class Animated_Staircase : public Usermod { /* configuration (available in API and stored in flash) */ bool enabled = false; // Enable this usermod unsigned long segment_delay_ms = 150; // Time between switching each segment - unsigned long on_time_ms = 30000; // The time for the light to stay on + unsigned long on_time_ms = 5000; // The time for the light to stay on - TroyHacks: 5s for testing int8_t topPIRorTriggerPin = -1; // disabled int8_t bottomPIRorTriggerPin = -1; // disabled int8_t topEchoPin = -1; // disabled @@ -131,7 +131,7 @@ class Animated_Staircase : public Usermod { * received within this time, an object is detected * and the function will return true. * - * The speed of sound is 343 meters per second at 20 degress Celcius. + * The speed of sound is 343 meters per second at 20 degrees Celsius. * Since the sound has to travel back and forth, the detection * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. * @@ -161,28 +161,37 @@ class Animated_Staircase : public Usermod { if ((millis() - lastScanTime) > scanDelay) { lastScanTime = millis(); - bottomSensorRead = bottomSensorWrite || - (!useUSSensorBottom ? - (bottomPIRorTriggerPin<0 ? false : digitalRead(bottomPIRorTriggerPin)) : - ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxDist*59) // cm to us - ); - topSensorRead = topSensorWrite || - (!useUSSensorTop ? - (topPIRorTriggerPin<0 ? false : digitalRead(topPIRorTriggerPin)) : - ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxDist*59) // cm to us - ); + if (useUSSensorBottom) { + bottomSensorRead = ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxDist*59); // US + } else if (bottomPIRorTriggerPin > 0) { + bottomSensorRead = digitalRead(bottomPIRorTriggerPin); // PIR + } else { + bottomSensorRead = false; // DUNNO + } + + if (useUSSensorTop) { + topSensorRead = ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxDist*59); // US + } else if (topPIRorTriggerPin > 0) { + topSensorRead = digitalRead(topPIRorTriggerPin); // PIR + } else { + topSensorRead = false; // DUNNO + } if (bottomSensorRead != bottomSensorState) { bottomSensorState = bottomSensorRead; // change previous state sensorChanged = true; - publishMqtt(true, bottomSensorState ? "on" : "off"); + #ifndef WLED_DISABLE_MQTT + publishMqtt(true, bottomSensorState ? "on" : "off"); + #endif DEBUG_PRINTLN(F("Bottom sensor changed.")); } if (topSensorRead != topSensorState) { topSensorState = topSensorRead; // change previous state sensorChanged = true; - publishMqtt(false, topSensorState ? "on" : "off"); + #ifndef WLED_DISABLE_MQTT + publishMqtt(false, topSensorState ? "on" : "off"); + #endif DEBUG_PRINTLN(F("Top sensor changed.")); } @@ -224,7 +233,13 @@ class Animated_Staircase : public Usermod { if (bottomSensorState || topSensorState) return; // Swipe OFF in the direction of the last sensor detection - swipe = lastSensor; + // WLED-MM/TroyHacks: This should follow you up/down the stairs. + if (lastSensor == SWIPE_UP) { + swipe = SWIPE_DOWN; + } else { + swipe = SWIPE_UP; + } + on = false; DEBUG_PRINT(F("OFF -> Swipe ")); @@ -253,7 +268,7 @@ class Animated_Staircase : public Usermod { } } - // send sesnor values to JSON API + // send sensor values to JSON API void writeSensorsToJson(JsonObject& staircase) { staircase[F("top-sensor")] = topSensorRead; staircase[F("bottom-sensor")] = bottomSensorRead; @@ -302,7 +317,7 @@ class Animated_Staircase : public Usermod { seg.setOption(SEG_OPTION_ON, true); } strip.trigger(); // force strip update - stateChanged = true; // inform external dvices/UI of change + stateChanged = true; // inform external devices/UI of change colorUpdated(CALL_MODE_DIRECT_CHANGE); DEBUG_PRINTLN(F("Animated Staircase disabled.")); } @@ -418,6 +433,8 @@ class Animated_Staircase : public Usermod { } void appendConfigData() { + oappend(SET_F("addHB('staircase');")); + //oappend(SET_F("dd=addDropdown('staircase','selectfield');")); //oappend(SET_F("addOption(dd,'1st value',0);")); //oappend(SET_F("addOption(dd,'2nd value',1);")); @@ -484,7 +501,7 @@ class Animated_Staircase : public Usermod { bottomEchoPin = top[FPSTR(_bottomEcho_pin)] | bottomEchoPin; topMaxDist = top[FPSTR(_topEchoCm)] | topMaxDist; - topMaxDist = min(150,max(30,(int)topMaxDist)); // max distnace ~1.5m (a lag of 9ms may be expected) + topMaxDist = min(150,max(30,(int)topMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected) bottomMaxDist = top[FPSTR(_bottomEchoCm)] | bottomMaxDist; bottomMaxDist = min(150,max(30,(int)bottomMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected) diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 61c1cb2d65..320b744a55 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -11,7 +11,7 @@ The Animated Staircase can be controlled by the WLED API. Change settings such a speed, on/off time and distance by sending an HTTP request, see below. ## WLED integration -To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://github.com/Aircoookie/WLED/wiki/Compiling-WLED). +To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://kno.wled.ge/advanced/compiling-wled/). Before compiling, you have to make the following modifications: @@ -38,7 +38,7 @@ Maximum distance for ultrasonic sensor can be configured as the time needed for You _may_ need to use 10k pull-down resistors on the selected PIR pins, depending on the sensor. ## WLED configuration -1. In the WLED UI, confgure a segment for each step. The lowest step of the stairs is the +1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the lowest segment id. 2. Save your segments into a preset. 3. Ideally, add the preset in the config > LED setup menu to the "apply @@ -91,7 +91,7 @@ To enable the usermod again, use `"enabled":true`. Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. ### Changing animation parameters and detection range of the ultrasonic HC-SR04 sensor -Using _Usermod_ Settings page you can define different usermod parameters, includng sensor pins, delay between segment activation etc. +Using _Usermod_ Settings page you can define different usermod parameters, including sensor pins, delay between segment activation etc. When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. diff --git a/usermods/BH1750_v2/readme.md b/usermods/BH1750_v2/readme.md index 05a033cf00..6e6c693d45 100644 --- a/usermods/BH1750_v2/readme.md +++ b/usermods/BH1750_v2/readme.md @@ -9,7 +9,7 @@ The luminance is displayed in both the Info section of the web UI, as well as pu - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). - Data is published over MQTT - make sure you've enabled the MQTT sync interface. -## Compiliation +## Compilation To enable, compile with `USERMOD_BH1750` defined (e.g. in `platformio_override.ini`) ```ini diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h index b65332bc35..a6e7d653e4 100644 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -1,14 +1,15 @@ -// force the compiler to show a warning to confirm that this file is included -#warning **** Included USERMOD_BH1750 **** +// force the compiler to show a warning to confirm that this file is included WLEDMM: commented this warning as we want serious warnings ;-) +// #warning **** Included USERMOD_BH1750 **** #ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." +#warning "This user mod expects MQTT to be enabled." #endif #pragma once +#include // WLEDMM: make sure that I2C drivers have the "right" Wire Object + #include "wled.h" -#include #include // the max frequency to check photoresistor, 10 seconds @@ -18,7 +19,8 @@ // the min frequency to check photoresistor, 500 ms #ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL -#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 +//#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 +#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 2500 // WLEDMM this makes more sense #endif // how many seconds after boot to take first measurement, 10 seconds @@ -26,9 +28,9 @@ #define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 #endif -// only report if differance grater than offset value +// only report if difference grater than offset value #ifndef USERMOD_BH1750_OFFSET_VALUE -#define USERMOD_BH1750_OFFSET_VALUE 1 +#define USERMOD_BH1750_OFFSET_VALUE 2 // WLEDMM this makes more sense #endif class Usermod_BH1750 : public Usermod @@ -46,30 +48,35 @@ class Usermod_BH1750 : public Usermod bool getLuminanceComplete = false; // flag set at startup - bool enabled = true; + //bool enabled = true; //WLEDMM not needed as we use global attributes // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; + //static const char _name[]; //WLEDMM not needed as we use global attributes + //static const char _enabled[]; //WLEDMM not needed as we use global attributes static const char _maxReadInterval[]; static const char _minReadInterval[]; static const char _offset[]; static const char _HomeAssistantDiscovery[]; // set the default pins based on the architecture, these get overridden by Usermod menu settings - #ifdef ARDUINO_ARCH_ESP32 // ESP32 boards - #define HW_PIN_SCL 22 - #define HW_PIN_SDA 21 - #else // ESP8266 boards - #define HW_PIN_SCL 5 - #define HW_PIN_SDA 4 - #endif - int8_t ioPin[2] = {HW_PIN_SCL, HW_PIN_SDA}; // I2C pins: SCL, SDA...defaults to Arch hardware pins but overridden at setup() + // #ifdef ARDUINO_ARCH_ESP32 // ESP32 boards -- WLEDMM: don't override already defined HW pins + // #ifndef HW_PIN_SDA + // #define HW_PIN_SCL 22 + // #endif + // #ifndef HW_PIN_SDA + // #define HW_PIN_SDA 21 + // #endif + // #else // ESP8266 boards + // #define HW_PIN_SCL 5 + // #define HW_PIN_SDA 4 + // #endif + //int8_t ioPin[2] = {HW_PIN_SCL, HW_PIN_SDA}; // I2C pins: SCL, SDA...defaults to Arch hardware pins but overridden at setup() + int8_t ioPin[2] = {-1, -1}; // WLEDMM - I2C pins: wait until hw pins get allocated bool initDone = false; bool sensorFound = false; // Home Assistant and MQTT - String mqttLuminanceTopic = F(""); + String mqttLuminanceTopic; bool mqttInitialized = false; bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages @@ -84,14 +91,17 @@ class Usermod_BH1750 : public Usermod // set up Home Assistant discovery entries void _mqttInitialize() { +#ifdef WLED_ENABLED_MQTT mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness"); if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx")); +#endif } // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { +#ifdef WLED_ENABLED_MQTT String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); StaticJsonDocument<600> doc; @@ -108,8 +118,8 @@ class Usermod_BH1750 : public Usermod JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); - device[F("manufacturer")] = F("WLED"); - device[F("model")] = F("FOSS"); + device[F("manufacturer")] = F(WLED_BRAND); //WLEDMM + Moustachauve/Wled-Native + device[F("model")] = F(WLED_PRODUCT_NAME); //WLEDMM + Moustachauve/Wled-Native device[F("sw_version")] = versionString; String temp; @@ -118,26 +128,51 @@ class Usermod_BH1750 : public Usermod DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); +#endif } public: + Usermod_BH1750(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM + void setup() { - bool HW_Pins_Used = (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA); // note whether architecture-based hardware SCL/SDA pins used +#if 0 + bool HW_Pins_Used = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); // note whether architecture-based hardware SCL/SDA pins used PinOwner po = PinOwner::UM_BH1750; // defaults to being pinowner for SCL/SDA pins - PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; // allocate pins if (HW_Pins_Used) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins + if ((i2c_scl >= 0) && (i2c_sda >=0)) { // WLEDMM: make sure that global HW pins are used if defined + ioPin[0] = i2c_scl; + ioPin[1] = i2c_sda; + po = PinOwner::HW_I2C; + } + PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; // allocate pins // WLEDMM: after selecting final pins if (!pinManager.allocateMultiplePins(pins, 2, po)) return; - Wire.begin(ioPin[1], ioPin[0]); +#if defined(ARDUINO_ARCH_ESP32) + if (pins[1].pin < 0 || pins[0].pin < 0) { sensorFound=false; enabled=false; return; } //WLEDMM bugfix - ensure that "final" GPIO are valid and no "-1" sneaks trough + //Wire.begin(pins[1].pin, pins[0].pin); // WLEDMM this might silently fail, which is OK as it just means that I2C bus is already running. +#else + //Wire.begin(); // WLEDMM - i2c pins on 8266 are fixed. +#endif +#endif + if (!enabled) return; + if (!pinManager.joinWire()) { // WLEDMM - this allocates global I2C pins, then starts Wire - if not started previously + sensorFound = false; + //enabled = false; + USER_PRINTLN(F("[BH1750]: failed to join I2C bus.")); + return; + } - sensorFound = lightMeter.begin(); + sensorFound = lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE); // WLEDMM set mode explicitly + if (sensorFound) { USER_PRINTLN(F("[BH1750] sensor found.")); } + else{ USER_PRINTLN(F("[BH1750] sensor not found.")); } initDone = true; } void loop() { - if ((!enabled) || strip.isUpdating()) + if (!sensorFound || !initDone) return; // WLEDMM bugfix + if ((!enabled) || (strip.isUpdating() && (millis() - lastMeasurement < 450))) // WLEDMM be nice, but not too nice return; unsigned long now = millis(); @@ -151,10 +186,12 @@ class Usermod_BH1750 : public Usermod } bool shouldUpdate = now - lastSend > maxReadingInterval; - - float lux = lightMeter.readLightLevel(); - lastMeasurement = millis(); - getLuminanceComplete = true; + float lux = lastLux; + if (lightMeter.measurementReady()) { //WLEDMM do not block in case the sensor is still busy + lux = lightMeter.readLightLevel(); + lastMeasurement = millis(); + getLuminanceComplete = true; + } if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) { @@ -169,7 +206,7 @@ class Usermod_BH1750 : public Usermod mqttInitialized = true; } mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str()); - DEBUG_PRINTLN(F("Brightness: ") + String(lux) + F("lx")); + DEBUG_PRINTLN(String("Brightness: ") + String(lux) + String("lx")); // WLEDMM fix compilation warning } else { @@ -185,6 +222,8 @@ class Usermod_BH1750 : public Usermod void addToJsonInfo(JsonObject &root) { + if ((!enabled) || (!sensorFound)) return; // WLEDMM bugfix + JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); @@ -201,50 +240,66 @@ class Usermod_BH1750 : public Usermod lux_json.add(F(" sec until read")); return; } else { - lux_json.add(lastLux); + lux_json.add(round(lastLux)); // WLEDMM lux_json.add(F(" lx")); } } + void appendConfigData() { + oappend(SET_F("addHB('BH1750');")); + // WLEDMM this usermod can ONLY use HW_I2C - so always use globals + //oappend(SET_F("addInfo('BH1750:pin[]',0,'','I2C SCL');")); + //oappend(SET_F("rOpt('BH1750:pin[]',0,'use global (")); oappendi(i2c_scl); oappend(")',-1);"); + //oappend(SET_F("addInfo('BH1750:pin[]',1,'','I2C SDA');")); + //oappend(SET_F("rOpt('BH1750:pin[]',1,'use global (")); oappendi(i2c_sda); oappend(")',-1);"); + } + // (called from set.cpp) stores persistent properties to cfg.json void addToConfig(JsonObject &root) { // we add JSON object. JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; + top[F("enabled")] = enabled; top[FPSTR(_maxReadInterval)] = maxReadingInterval; top[FPSTR(_minReadInterval)] = minReadingInterval; top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; top[FPSTR(_offset)] = offset; - JsonArray io_pin = top.createNestedArray(F("pin")); - for (byte i=0; i<2; i++) io_pin.add(ioPin[i]); - top[F("help4Pins")] = F("SCL,SDA"); // help for Settings page - DEBUG_PRINTLN(F("BH1750 config saved.")); + // WLEDMM this usermod can ONLY use HW_I2C - so always use globals + //JsonArray io_pin = top.createNestedArray(F("pin")); + //WLEDMM: avoid global pin hijacking + //io_pin.add((ioPin[0]==i2c_scl)?-1:ioPin[0]); + //io_pin.add((ioPin[1]==i2c_sda)?-1:ioPin[1]); + + // top[F("help4Pins")] = F("SCL,SDA"); // help for Settings page + + DEBUG_PRINTLN(F("[BH1750] config saved.")); } // called before setup() to populate properties from values stored in cfg.json bool readFromConfig(JsonObject &root) { - int8_t newPin[2]; for (byte i=0; i<2; i++) newPin[i] = ioPin[i]; // prepare to note changed pins + int8_t newPin[2] = {-1, -1}; // WLEDMM this usermod can ONLY use HW_I2C - so always use globals + //for (byte i=0; i<2; i++) newPin[i] = ioPin[i]; // prepare to note changed pins // we look for JSON object. JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINT(F("BH1750")); + DEBUG_PRINT(F("[BH1750]")); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } bool configComplete = !top.isNull(); - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); + configComplete &= getJsonValue(top[F("enabled")], enabled, false); configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); - for (byte i=0; i<2; i++) configComplete &= getJsonValue(top[F("pin")][i], newPin[i], ioPin[i]); + // WLEDMM this usermod can ONLY use HW_I2C - so always use globals + //for (byte i=0; i<2; i++) configComplete &= getJsonValue(top[F("pin")][i], newPin[i], ioPin[i]); DEBUG_PRINT(FPSTR(_name)); if (!initDone) { @@ -254,15 +309,20 @@ class Usermod_BH1750 : public Usermod } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page +#if 0 bool pinsChanged = false; for (byte i=0; i<2; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } // check if any pins changed if (pinsChanged) { //if pins changed, deallocate old pins and allocate new ones PinOwner po = PinOwner::UM_BH1750; - if (ioPin[0]==HW_PIN_SCL && ioPin[1]==HW_PIN_SDA) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins + if (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins + if (ioPin[0]==-1 && ioPin[1]==-1) po = PinOwner::HW_I2C; // WLEDMM global HW I2C bus pins pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 2, po); // deallocate pins for (byte i=0; i<2; i++) ioPin[i] = newPin[i]; setup(); } +#else + if (enabled && !sensorFound) setup(); +#endif // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[F("pin")].isNull(); } @@ -279,8 +339,8 @@ class Usermod_BH1750 : public Usermod }; // strings to reduce flash memory usage (used more than twice) -const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; -const char Usermod_BH1750::_enabled[] PROGMEM = "enabled"; +//const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; //WLEDMM not needed as we use global attributes +//const char Usermod_BH1750::_enabled[] PROGMEM = "enabled"; //WLEDMM not needed as we use global attributes const char Usermod_BH1750::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; const char Usermod_BH1750::_minReadInterval[] PROGMEM = "min-read-interval-ms"; const char Usermod_BH1750::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscoveryLux"; diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/usermod_bme280.h index e643e62b5e..ed495ce5d9 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/usermod_bme280.h @@ -1,5 +1,5 @@ -// force the compiler to show a warning to confirm that this file is included -#warning **** Included USERMOD_BME280 version 2.0 **** +// force the compiler to show a warning to confirm that this file is included WLEDMM: commented this warning as we want serious warnings ;-) +// #warning **** Included USERMOD_BME280 version 2.0 **** #ifndef WLED_ENABLE_MQTT #error "This user mod requires MQTT to be enabled." @@ -32,7 +32,7 @@ class UsermodBME280 : public Usermod // set the default pins based on the architecture, these get overridden by Usermod menu settings #ifdef ESP8266 - //uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 + //uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif int8_t ioPin[2] = {i2c_scl, i2c_sda}; // I2C pins: SCL, SDA...defaults to Arch hardware pins but overridden at setup() bool initDone = false; @@ -80,10 +80,11 @@ class UsermodBME280 : public Usermod static const char _name[]; static const char _enabled[]; - // Read the BME280/BMP280 Sensor (which one runs depends on whether Celsius or Farenheit being set in Usermod Menu) + // Read the BME280/BMP280 Sensor (which one runs depends on whether Celsius or Fahrenheit being set in Usermod Menu) void UpdateBME280Data(int SensorType) { float _temperature, _humidity, _pressure; + if (!enabled || (sensorType == 0)) return; // WLEDMM bugfix if (UseCelsius) { BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); @@ -162,8 +163,8 @@ class UsermodBME280 : public Usermod JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); - device[F("manufacturer")] = F("WLED"); - device[F("model")] = F("FOSS"); + device[F("manufacturer")] = F(WLED_BRAND); //WLEDMM + Moustachauve/Wled-Native + device[F("model")] = F(WLED_PRODUCT_NAME); //WLEDMM + Moustachauve/Wled-Native device[F("sw_version")] = versionString; String temp; @@ -187,12 +188,13 @@ class UsermodBME280 : public Usermod void setup() { bool HW_Pins_Used = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); // note whether architecture-based hardware SCL/SDA pins used - PinOwner po = PinOwner::UM_BME280; // defaults to being pinowner for SCL/SDA pins + //PinOwner po = PinOwner::UM_BME280; // defaults to being pinowner for SCL/SDA pins // WLEDMM not needed PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; // allocate pins - if (HW_Pins_Used) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - if (!pinManager.allocateMultiplePins(pins, 2, po)) { sensorType=0; return; } - - Wire.begin(ioPin[1], ioPin[0]); + //if (HW_Pins_Used) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins // WLEDMM not needed + // WLEDMM join I2C HW wire + if (!pinManager.joinWire()) { sensorType=0; enabled = false; return; } + //if (!pinManager.allocateMultiplePins(pins, 2, po)) { sensorType=0; return; } + //Wire.begin(ioPin[1], ioPin[0]); if (!bme.begin()) { @@ -243,7 +245,7 @@ class UsermodBME280 : public Usermod // from the UI and values read from sensor, then publish to broker if (temperature != lastTemperature || PublishAlways) { - publishMqtt("temperature", String(temperature, TemperatureDecimals).c_str()); + publishMqtt("temperature", String(temperature, (unsigned)TemperatureDecimals).c_str()); } lastTemperature = temperature; // Update last sensor temperature for next loop @@ -256,17 +258,17 @@ class UsermodBME280 : public Usermod if (humidity != lastHumidity || PublishAlways) { - publishMqtt("humidity", String(humidity, HumidityDecimals).c_str()); + publishMqtt("humidity", String(humidity, (unsigned)HumidityDecimals).c_str()); } if (heatIndex != lastHeatIndex || PublishAlways) { - publishMqtt("heat_index", String(heatIndex, TemperatureDecimals).c_str()); + publishMqtt("heat_index", String(heatIndex, (unsigned)TemperatureDecimals).c_str()); } if (dewPoint != lastDewPoint || PublishAlways) { - publishMqtt("dew_point", String(dewPoint, TemperatureDecimals).c_str()); + publishMqtt("dew_point", String(dewPoint, (unsigned)TemperatureDecimals).c_str()); } lastHumidity = humidity; @@ -283,7 +285,7 @@ class UsermodBME280 : public Usermod if (pressure != lastPressure || PublishAlways) { - publishMqtt("pressure", String(pressure, PressureDecimals).c_str()); + publishMqtt("pressure", String(pressure, (unsigned)PressureDecimals).c_str()); } lastPressure = pressure; @@ -402,6 +404,18 @@ class UsermodBME280 : public Usermod return; } + void appendConfigData() { + oappend(SET_F("addHB('BME280');")); + + oappend(SET_F("addInfo('BME280/BMP280:pin[]',0,'','I2C/SPI CLK');")); + oappend(SET_F("dRO('BME280/BMP280:pin[]',0);")); // disable read only pins + oappend(SET_F("rOpt('BME280/BMP280:pin[]',0,'use global (")); oappendi(i2c_scl); oappend(")',-1);"); + + oappend(SET_F("addInfo('BME280/BMP280:pin[]',1,'','I2C/SPI DTA');")); + oappend(SET_F("rOpt('BME280/BMP280:pin[]',1,'use global (")); oappendi(i2c_sda); oappend(")',-1);"); + } + + // Save Usermod Config Settings void addToConfig(JsonObject& root) { @@ -416,8 +430,9 @@ class UsermodBME280 : public Usermod top[F("UseCelsius")] = UseCelsius; top[F("HomeAssistantDiscovery")] = HomeAssistantDiscovery; JsonArray io_pin = top.createNestedArray(F("pin")); - for (byte i=0; i<2; i++) io_pin.add(ioPin[i]); - top[F("help4Pins")] = F("SCL,SDA"); // help for Settings page + //WLEDMM: avoid global pin hijacking + io_pin.add((ioPin[0]==i2c_scl)?-1:ioPin[0]); + io_pin.add((ioPin[1]==i2c_sda)?-1:ioPin[1]); DEBUG_PRINTLN(F("BME280 config saved.")); } diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index 958bfe5263..60dd4c4b7a 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -3,7 +3,7 @@ // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html #ifndef USERMOD_BATTERY_MEASUREMENT_PIN #ifdef ARDUINO_ARCH_ESP32 - #define USERMOD_BATTERY_MEASUREMENT_PIN 35 + #define USERMOD_BATTERY_MEASUREMENT_PIN -1 //WLEDMM instead of 35 #else //ESP8266 boards #define USERMOD_BATTERY_MEASUREMENT_PIN A0 #endif @@ -56,7 +56,7 @@ // auto-off feature #ifndef USERMOD_BATTERY_AUTO_OFF_ENABLED - #define USERMOD_BATTERY_AUTO_OFF_ENABLED true + #define USERMOD_BATTERY_AUTO_OFF_ENABLED false #endif #ifndef USERMOD_BATTERY_AUTO_OFF_THRESHOLD @@ -78,4 +78,4 @@ #ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION #define USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION 5 -#endif \ No newline at end of file +#endif diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index d55573abe4..999c0a541e 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -19,7 +19,7 @@ If you have an ESP32 board, connect the positive side of the battery to ADC1 (GP - šŸ’Æ Displays current battery voltage - 🚄 Displays battery level - 🚫 Auto-off with configurable Threshold -- 🚨 Low power indicator with many configuration posibilities +- 🚨 Low power indicator with many configuration possibilities ## šŸŽˆ Installation @@ -41,7 +41,7 @@ define `USERMOD_BATTERY` in `wled00/my_config.h` | `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | battery check interval. defaults to 30 seconds | | `USERMOD_BATTERY_MIN_VOLTAGE` | v | minimum battery voltage. default is 2.6 (18650 battery standard) | | `USERMOD_BATTERY_MAX_VOLTAGE` | v | maximum battery voltage. default is 4.2 (18650 battery standard) | -| `USERMOD_BATTERY_TOTAL_CAPACITY` | mAh | the capacity of all cells in parralel sumed up | +| `USERMOD_BATTERY_TOTAL_CAPACITY` | mAh | the capacity of all cells in parallel summed up | | `USERMOD_BATTERY_CALIBRATION` | | offset / calibration number, fine tune the measured voltage by the microcontroller | | Auto-Off | --- | --- | | `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | enables auto-off | diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index be3d8748bf..bd3bf62c00 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -60,9 +60,9 @@ class UsermodBattery : public Usermod bool initializing = true; // strings to reduce flash memory usage (used more than twice) - static const char _name[]; + // static const char _name[]; static const char _readInterval[]; - static const char _enabled[]; + // static const char _enabled[]; static const char _threshold[]; static const char _preset[]; static const char _duration[]; @@ -117,7 +117,8 @@ class UsermodBattery : public Usermod float readVoltage() { #ifdef ARDUINO_ARCH_ESP32 - // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value + if ((batteryPin <0) || !pinManager.isPinAnalog(batteryPin)) return(-1.0f); // WLEDMM avoid reading from invalid pin + // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attenuation) and divide by 1000 to get from milliVolts to volts and multiply by voltage multiplier and apply calibration value return (analogReadMilliVolts(batteryPin) / 1000.0f) * voltageMultiplier + calibration; #else // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value @@ -127,6 +128,7 @@ class UsermodBattery : public Usermod public: //Functions called by WLED + UsermodBattery(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM: this shouldn't be necessary (passthrough of constructor), maybe because Usermod is an abstract class /* * setup() is called once at boot. WiFi is not yet connected at this point. @@ -134,10 +136,11 @@ class UsermodBattery : public Usermod */ void setup() { + if (!enabled) return; // WLEDMM #ifdef ARDUINO_ARCH_ESP32 bool success = false; DEBUG_PRINTLN(F("Allocating battery pin...")); - if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) + if ((batteryPin >= 0) && (digitalPinToAnalogChannel(batteryPin) >= 0)) if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); success = true; @@ -145,8 +148,9 @@ class UsermodBattery : public Usermod } if (!success) { - DEBUG_PRINTLN(F("Battery pin allocation failed.")); + USER_PRINTLN(F("Battery pin allocation failed.")); batteryPin = -1; // allocation failed + enabled = false; } else { pinMode(batteryPin, INPUT); } @@ -178,7 +182,13 @@ class UsermodBattery : public Usermod */ void loop() { - if(strip.isUpdating()) return; + // WLEDMM begin + if (!enabled) return; + if (batteryPin < 0) return; + unsigned long currentTime = millis(); // get the current elapsed time + if (strip.isUpdating() && (currentTime - lastTime < 30000)) return; // WLEDMM: be nice + lastTime = currentTime; + // WLEDMM end lowPowerIndicator(); @@ -206,7 +216,7 @@ class UsermodBattery : public Usermod */ #ifdef USERMOD_BATTERY_USE_LIPO batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); // basic mapping - // LiPo batteries have a differnt dischargin curve, see + // LiPo batteries have a different dischargin curve, see // https://blog.ampow.com/lipo-voltage-chart/ if (batteryLevel < 40.0f) batteryLevel = mapf(batteryLevel, 0, 40, 0, 12); // last 45% -> drops very quickly @@ -252,6 +262,7 @@ class UsermodBattery : public Usermod JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); + if (!enabled) return; // WLEDMM if (batteryPin < 0) { JsonArray infoVoltage = user.createNestedArray(F("Battery voltage")); infoVoltage.add(F("n/a")); @@ -360,6 +371,8 @@ class UsermodBattery : public Usermod void addToConfig(JsonObject& root) { JsonObject battery = root.createNestedObject(FPSTR(_name)); // usermodname + battery[F("enabled")] = enabled; // WLEDMM + #ifdef ARDUINO_ARCH_ESP32 battery[F("pin")] = batteryPin; #endif @@ -373,11 +386,11 @@ class UsermodBattery : public Usermod battery[FPSTR(_readInterval)] = readingInterval; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section - ao[FPSTR(_enabled)] = autoOffEnabled; + ao[F("auto-off-enabled")] = autoOffEnabled; ao[FPSTR(_threshold)] = autoOffThreshold; JsonObject lp = battery.createNestedObject(F("indicator")); // low power section - lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; + lp[F("indicator-enabled")] = lowPowerIndicatorEnabled; lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; lp[FPSTR(_duration)] = lowPowerIndicatorDuration; @@ -390,6 +403,8 @@ class UsermodBattery : public Usermod void appendConfigData() { + oappend(SET_F("addHB('Battery');")); + oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); oappend(SET_F("addInfo('Battery:capacity', 1, 'mAh');")); @@ -398,7 +413,7 @@ class UsermodBattery : public Usermod oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); oappend(SET_F("addInfo('Battery:indicator:duration', 1, 's');")); - // cannot quite get this mf to work. its exeeding some buffer limit i think + // cannot quite get this mf to work. its exceeding some buffer limit i think // what i wanted is a list of all presets to select one from // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); @@ -434,6 +449,10 @@ class UsermodBattery : public Usermod #endif JsonObject battery = root[FPSTR(_name)]; + + bool configComplete = !battery.isNull(); // WLEDMM + configComplete &= getJsonValue(battery[F("enabled")], enabled, true); // WLEDMM + if (battery.isNull()) { DEBUG_PRINT(FPSTR(_name)); @@ -453,11 +472,11 @@ class UsermodBattery : public Usermod setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); JsonObject ao = battery[F("auto-off")]; - setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); + setAutoOffEnabled(ao[F("auto-off-enabled")] | autoOffEnabled); // WLEDMM setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold); JsonObject lp = battery[F("indicator")]; - setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled); + setLowPowerIndicatorEnabled(lp[F("indicator-enabled")] | lowPowerIndicatorEnabled); // WLEDMM setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset); // dropdown trickery (int)lp["preset"] setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold); lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; @@ -488,7 +507,7 @@ class UsermodBattery : public Usermod } #endif - return !battery[FPSTR(_readInterval)].isNull(); + return configComplete && (!battery[FPSTR(_readInterval)].isNull()); // WLEDMM } /* @@ -593,7 +612,7 @@ class UsermodBattery : public Usermod /* - * Get the capacity of all cells in parralel sumed up + * Get the capacity of all cells in parallel summed up * unit: mAh */ unsigned int getTotalBatteryCapacity() @@ -769,7 +788,7 @@ class UsermodBattery : public Usermod /* - * Get low-power-indicator status when the indication is done thsi returns true + * Get low-power-indicator status when the indication is done this returns true */ bool getLowPowerIndicatorDone() { @@ -778,9 +797,9 @@ class UsermodBattery : public Usermod }; // strings to reduce flash memory usage (used more than twice) -const char UsermodBattery::_name[] PROGMEM = "Battery"; +// const char UsermodBattery::_name[] PROGMEM = "Battery"; const char UsermodBattery::_readInterval[] PROGMEM = "interval"; -const char UsermodBattery::_enabled[] PROGMEM = "enabled"; +//const char UsermodBattery::_enabled[] PROGMEM = "enabled"; const char UsermodBattery::_threshold[] PROGMEM = "threshold"; const char UsermodBattery::_preset[] PROGMEM = "preset"; const char UsermodBattery::_duration[] PROGMEM = "duration"; diff --git a/usermods/DHT/usermod_dht.h b/usermods/DHT/usermod_dht.h index b6142f4322..05a7267b5f 100644 --- a/usermods/DHT/usermod_dht.h +++ b/usermods/DHT/usermod_dht.h @@ -49,7 +49,7 @@ #endif // how many seconds after boot to take first measurement, 90 seconds -// 90 gives enough time to OTA update firmware if this crashses +// 90 gives enough time to OTA update firmware if this crashes #ifndef USERMOD_DHT_FIRST_MEASUREMENT_AT #define USERMOD_DHT_FIRST_MEASUREMENT_AT 90000 #endif diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 43648b5884..b2a257f0cd 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -20,15 +20,32 @@ * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp */ +/* WLEDMM: move usermod variables to class. + +As of March 2023 this is work in progress, more variables will be moved in the future. +See Example v2, Temperature, MPU6050 and weather and fastled (rest to be done) as examples which has been converted using the steps below: + +Part 1 +- remove bool enabled = false/true (now default false) +- remove static const char _name[] and _enabled[] +- add constructor which calls superclass (temp?): XXXUsermod(const char *name, bool enabled):Usermod(name, enabled) {} +- replace _enabled with "enabled" +- remove const char PROGMEM init for _name[] and _enabled[] +Part 2 +- Remove bool initDone = false; +- addToConfig: replace createNestedObject with Usermod::addToConfig(root); JsonObject top = root[FPSTR(_name)]; +- readFromConfig: replace !top.isNull and enabled with bool configComplete = Usermod::readFromConfig(root);JsonObject top = root[FPSTR(_name)]; +Part 3 +- remove unsigned long lastTime = 0; //WLEDMM + +*/ + //class name. Use something descriptive and leave the ": public Usermod" part :) class MyExampleUsermod : public Usermod { private: // Private class members. You can declare variables and functions only accessible to your usermod here - bool enabled = false; - bool initDone = false; - unsigned long lastTime = 0; // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) bool testBool = false; @@ -41,28 +58,25 @@ class MyExampleUsermod : public Usermod { long testLong; int8_t testPins[2]; - // string that are used multiple time (this will save some flash memory) - static const char _name[]; - static const char _enabled[]; - - - // any private methods should go here (non-inline methosd should be defined out of class) + // any private methods should go here (non-inline method should be defined out of class) void publishMqtt(const char* state, bool retain = false); // example for publishing MQTT message public: + MyExampleUsermod(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM + // non WLED related methods, may be used for data exchange between usermods (non-inline methods should be defined out of class) /** * Enable/Disable the usermod */ - inline void enable(bool enable) { enabled = enable; } + // inline void enable(bool enable) { enabled = enable; } /** * Get usermod enabled/disabled state */ - inline bool isEnabled() { return enabled; } + // inline bool isEnabled() { return enabled; } // in such case add the following to another usermod: // in private vars: @@ -222,8 +236,8 @@ class MyExampleUsermod : public Usermod { */ void addToConfig(JsonObject& root) { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; + Usermod::addToConfig(root); JsonObject top = root[FPSTR(_name)]; //WLEDMM + //save these vars persistently whenever settings are saved top["great"] = userVar0; top["testBool"] = testBool; @@ -258,9 +272,7 @@ class MyExampleUsermod : public Usermod { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) - JsonObject top = root[FPSTR(_name)]; - - bool configComplete = !top.isNull(); + bool configComplete = Usermod::readFromConfig(root);JsonObject top = root[FPSTR(_name)]; //WLEDMM configComplete &= getJsonValue(top["great"], userVar0); configComplete &= getJsonValue(top["testBool"], testBool); @@ -386,8 +398,6 @@ class MyExampleUsermod : public Usermod { // add more strings here to reduce flash memory usage -const char MyExampleUsermod::_name[] PROGMEM = "ExampleUsermod"; -const char MyExampleUsermod::_enabled[] PROGMEM = "enabled"; // implementation of non-inline member methods diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp index 1ca160501e..823ad74724 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp @@ -15,23 +15,23 @@ OneWire oneWire(13); DallasTemperature sensor(&oneWire); long temptimer = millis(); long lastMeasure = 0; -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -// --> First choise of cheap I2C OLED 128X32 0.91" +// --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // gets called once at boot. Do all initialization that doesn't depend on // network here void userSetup() { sensor.begin(); //Start Dallas temperature sensor u8x8.begin(); - //u8x8.setFlipMode(1); //Uncoment if using WLED Wemos shield + //u8x8.setFlipMode(1); //Un-comment if using WLED Wemos shield u8x8.setPowerSave(0); u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -71,7 +71,7 @@ void userLoop() { if (mqtt != nullptr) { sensor.requestTemperatures(); -//Gets prefered temperature scale based on selection in definitions section +//Gets preferred temperature scale based on selection in definitions section #ifdef Celsius float board_temperature = sensor.getTempCByIndex(0); #else @@ -138,11 +138,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp index d5fd4a0c2e..29a4332dbd 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp @@ -10,7 +10,7 @@ void UpdateBME280Data(); -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit BME280I2C bme; // Default : forced mode, standby time = 1000 ms // Oversampling = pressure Ɨ1, temperature Ɨ1, humidity Ɨ1, filter off, @@ -20,14 +20,14 @@ uint8_t SDA_PIN = 21; #else //ESP8266 boards uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; -// uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 +// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN -//#define U8X8_PIN_RESET RST_PIN // Uncoment for Heltec WiFi-Kit-8 +//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // If display does not work or looks corrupted check the // constructor reference: @@ -36,9 +36,9 @@ uint8_t SDA_PIN = 4; // https://github.com/olikraus/u8g2/wiki/gallery // --> First choise of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Third choise of Heltec WiFi-Kit-8 OLED 128X32 0.91" +// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here @@ -181,11 +181,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index 24d5ff5aaa..006eaf9f94 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -2,7 +2,7 @@ **Attention: This usermod compiles only for ESP8266** -This usermod-v2 modification performs a ping request to a local IP address every 60 seconds. This ensures WLED net services remain accessible in some problematic WLAN environments. +This usermod-v2 modification performs a ping request to a local IP address every 60 seconds. This ensures WLED net services remain accessible in some problematic WiFi environments. The modification works with static or DHCP IP address configuration. @@ -24,7 +24,7 @@ The usermod supports the following state changes: | JSON key | Value range | Description | |-------------|------------------|---------------------------------| -| PingDelayMs | 5000 to 18000000 | Deactivdate/activate the sensor | +| PingDelayMs | 5000 to 18000000 | Deactivate/activate the sensor | Changes also persist after a reboot. diff --git a/usermods/LDR_Dusk_Dawn_v2/README.md b/usermods/LDR_Dusk_Dawn_v2/README.md new file mode 100644 index 0000000000..5e33518a9d --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/README.md @@ -0,0 +1,26 @@ +# LDR_Dusk_Dawn_v2 +This usermod will obtain readings from a Light Dependent Resistor (LDR) and will turn on/off specific presets based on those readings. This is useful for exterior lighting situations where you want the lights to only be on when it is dark out. + +# Installation +Add "-D USERMOD_LDR_DUSK_DAWN" to your platformio.ini [common] build_flags and build. + +Example: +``` +[common] +build_flags = + -D USERMOD_LDR_DUSK_DAWN # Enable LDR Dusk Dawn Usermod +``` + +# Usermod Settings +Setting | Description | Default +--- | --- | --- +Enabled | Enable/Disable the LDR functionality. | Disabled +LDR Pin | The analog capable pin your LDR is connected to. | 34 +Threshold Minutes | The number of minutes of consistent readings above/below the on/off threshold before the LED state will change. | 5 +Threshold | The analog read value threshold from the LDR. Readings lower than this number will count towards changing the LED state to off. You can see the current LDR reading by going into the info section when LDR functionality is enabled. | 1000 +On Preset | The WLED preset to be used for the LED on state. | 1 +Off Preset | The WLED preset to be used for the LED off state. | 2 + +## Author +[@jeffwdh](https://github.com/jeffwdh) +jeffwdh@tarball.ca diff --git a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h new file mode 100644 index 0000000000..393fc22327 --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h @@ -0,0 +1,153 @@ +#pragma once +#include "wled.h" + +#ifndef ARDUINO_ARCH_ESP32 + // 8266 does not support analogRead on user selectable pins + #error only ESP32 is supported by usermod LDR_DUSK_DAWN +#endif + +class LDR_Dusk_Dawn_v2 : public Usermod { + private: + // Defaults + bool ldrEnabled = false; + int ldrPin = 34; //A2 on Adafruit Huzzah32 + int ldrThresholdMinutes = 5; // How many minutes of readings above/below threshold until it switches LED state + int ldrThreshold = 1000; // Readings higher than this number will turn off LED. + int ldrOnPreset = 1; // Default "On" Preset + int ldrOffPreset = 2; // Default "Off" Preset + + // Variables + bool initDone = false; + bool ldrEnabledPreviously = false; // Was LDR enabled for the previous check? First check is always no. + int ldrOffCount; // Number of readings above the threshold + int ldrOnCount; // Number of readings below the threshold + int ldrReading = 0; // Last LDR reading + int ldrLEDState; // Current LED on/off state + unsigned long lastMillis = 0; + static const char _name[]; + + public: + void setup() { + // register ldrPin + if ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)) { + if(!pinManager.allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod + else pinMode(ldrPin, INPUT); // alloc success -> configure pin for input + } else ldrEnabled = false; // invalid pin -> disable usermod + initDone = true; + } + + void loop() { + // Only update every 10 seconds + if (millis() - lastMillis > 10000) { + if ( (ldrEnabled == true) + && (ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0) ) { // make sure that pin is valid for analogread() + // Default state is off + if (ldrEnabledPreviously == false) { + applyPreset(ldrOffPreset); + ldrEnabledPreviously = true; + ldrLEDState = 0; + } + + // Get LDR reading and increment counter by number of seconds since last read + ldrReading = analogRead(ldrPin); + if (ldrReading <= ldrThreshold) { + ldrOnCount = ldrOnCount + 10; + ldrOffCount = 0; + } else { + ldrOffCount = ldrOffCount + 10; + ldrOnCount = 0; + } + + if (ldrOnCount >= (ldrThresholdMinutes * 60)) { + ldrOnCount = 0; + // If LEDs were previously off, turn on + if (ldrLEDState == 0) { + applyPreset(ldrOnPreset); + ldrLEDState = 1; + } + } + + if (ldrOffCount >= (ldrThresholdMinutes * 60)) { + ldrOffCount = 0; + // If LEDs were previously on, turn off + if (ldrLEDState == 1) { + applyPreset(ldrOffPreset); + ldrLEDState = 0; + } + } + } else { + // LDR is disabled, reset variables to default + ldrReading = 0; + ldrOnCount = 0; + ldrOffCount = 0; + ldrLEDState = 0; + ldrEnabledPreviously = false; + } + lastMillis = millis(); + } + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top["Enabled"] = ldrEnabled; + top["LDR Pin"] = ldrPin; + top["Threshold Minutes"] = ldrThresholdMinutes; + top["Threshold"] = ldrThreshold; + top["On Preset"] = ldrOnPreset; + top["Off Preset"] = ldrOffPreset; + } + + bool readFromConfig(JsonObject& root) { + int8_t oldLdrPin = ldrPin; + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top["Enabled"], ldrEnabled); + configComplete &= getJsonValue(top["LDR Pin"], ldrPin); + configComplete &= getJsonValue(top["Threshold Minutes"], ldrThresholdMinutes); + configComplete &= getJsonValue(top["Threshold"], ldrThreshold); + configComplete &= getJsonValue(top["On Preset"], ldrOnPreset); + configComplete &= getJsonValue(top["Off Preset"], ldrOffPreset); + + if (initDone && (ldrPin != oldLdrPin)) { + // pin changed - un-register previous pin, register new pin + if (oldLdrPin >= 0) pinManager.deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); + setup(); // setup new pin + } + return configComplete; + } + + void addToJsonInfo(JsonObject& root) { + // If "u" object does not exist yet we need to create it + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray LDR_Enabled = user.createNestedArray("LDR dusk/dawn enabled"); + LDR_Enabled.add(ldrEnabled); + if (!ldrEnabled) return; // do not add more if usermod is disabled + + JsonArray LDR_Reading = user.createNestedArray("LDR reading"); + LDR_Reading.add(ldrReading); + + JsonArray LDR_State = user.createNestedArray("LDR turned LEDs on"); + LDR_State.add(bool(ldrLEDState)); + + // Optional debug information: + //JsonArray LDR_On_Count = user.createNestedArray("LDR on count"); + //LDR_On_Count.add(ldrOnCount); + + //JsonArray LDR_Off_Count = user.createNestedArray("LDR off count"); + //LDR_Off_Count.add(ldrOffCount); + + //bool pinValid = ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)); + //if (pinManager.getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; + //JsonArray LDR_valid = user.createNestedArray(F("LDR pin")); + //LDR_valid.add(ldrPin); + //LDR_valid.add(pinValid ? F(" OK"): F(" invalid")); + } + + uint16_t getId() { + return USERMOD_ID_LDR_DUSK_DAWN; + } +}; + +const char LDR_Dusk_Dawn_v2::_name[] PROGMEM = "LDR_Dusk_Dawn_v2"; diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index 574bd06d8e..85a5a74c08 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -23,7 +23,7 @@ You can also use usermod's off timer instead of sensor's. In such case rotate th ## Usermod installation -**NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionaly `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. +**NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionally `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. ## API to enable/disable the PIR sensor from outside. For example from another usermod: @@ -31,7 +31,7 @@ To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` an When the PIR sensor state changes an MQTT message is broadcasted with topic `wled/deviceMAC/motion` and message `on` or `off`. Usermod can also be configured to send just the MQTT message but not change WLED state using settings page as well as responding to motion only at night -(assuming NTP and lattitude/longitude are set to determine sunrise/sunset times). +(assuming NTP and latitude/longitude are set to determine sunrise/sunset times). ### There are two options to get access to the usermod instance: @@ -85,7 +85,7 @@ Have fun - @gegu & @blazoncek 2021-11 * Added information about dynamic configuration options -* Added option to temporary enable/disble usermod from WLED UI (Info dialog) +* Added option to temporary enable/disable usermod from WLED UI (Info dialog) 2022-11 * Added compile time option for off timer. diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 7903fc9e0b..a91b396e18 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -4,11 +4,7 @@ #ifndef PIR_SENSOR_PIN // compatible with QuinLED-Dig-Uno - #ifdef ARDUINO_ARCH_ESP32 - #define PIR_SENSOR_PIN 23 // Q4 - #else //ESP8266 boards - #define PIR_SENSOR_PIN 13 // Q4 (D7 on D1 mini) - #endif + #define PIR_SENSOR_PIN -1 //WLEDMM not default 23 // Q4 for esp32 or otherwise 13 // Q4 (D7 on D1 mini) #endif #ifndef PIR_SENSOR_OFF_SEC @@ -193,8 +189,8 @@ class PIRsensorSwitch : public Usermod JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; - device[F("mf")] = "WLED"; - device[F("mdl")] = F("FOSS"); + device[F("mf")] = F(WLED_BRAND); //WLEDMM + Moustachauve/Wled-Native + device[F("mdl")] = F(WLED_PRODUCT_NAME); //WLEDMM + Moustachauve/Wled-Native device[F("sw")] = versionString; sprintf_P(buf, PSTR("homeassistant/binary_sensor/%s/config"), uid); @@ -209,7 +205,7 @@ class PIRsensorSwitch : public Usermod /** * Read and update PIR sensor state. - * Initilize/reset switch off timer + * Initialize/reset switch off timer */ bool updatePIRsensorState() { @@ -298,7 +294,8 @@ class PIRsensorSwitch : public Usermod void loop() { // only check sensors 4x/s - if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; + if (!enabled || millis() - lastLoop < 250) return; + if (strip.isUpdating() && (millis() - lastLoop < 250)) return; // WLEDMM be nice, but not too nice lastLoop = millis(); if (!updatePIRsensorState()) { @@ -433,6 +430,8 @@ class PIRsensorSwitch : public Usermod void appendConfigData() { + oappend(SET_F("addHB('PIRsensorSwitch');")); + oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field oappend(SET_F("addInfo('PIRsensorSwitch:notifications',1,'Periodic WS updates');")); // 0 is field type, 1 is actual field } diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md index 1fbfe0e6c9..6a44acf3b3 100644 --- a/usermods/PWM_fan/readme.md +++ b/usermods/PWM_fan/readme.md @@ -5,7 +5,7 @@ v2 Usermod to to control PWM fan with RPM feedback and temperature control This usermod requires the Dallas Temperature usermod to obtain temperature information. If it's not available, the fan will run at 100% speed. If the fan does not have _tachometer_ (RPM) output you can set the _tachometer-pin_ to -1 to disable that feature. -You can also set the thershold temperature at which fan runs at lowest speed. If the measured temperature is 3°C greater than the threshold temperature, the fan will run at 100%. +You can also set the threshold temperature at which fan runs at lowest speed. If the measured temperature is 3°C greater than the threshold temperature, the fan will run at 100%. If the _tachometer_ is supported, the current speed (in RPM) will be displayed on the WLED Info page. @@ -22,7 +22,7 @@ This includes: * PWM output pin (can be configured at compile time `-D PWM_PIN=xx`) * tachometer input pin (can be configured at compile time `-D TACHO_PIN=xx`) * sampling frequency in seconds -* threshold temperature in degees C +* threshold temperature in degrees Celsius _NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency. diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h index f7fe0e10f0..c9ccf14d68 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -75,7 +75,7 @@ class PWMFanUsermod : public Usermod { pinMode(tachoPin, INPUT); digitalWrite(tachoPin, HIGH); attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); - DEBUG_PRINTLN(F("Tacho sucessfully initialized.")); + DEBUG_PRINTLN(F("Tacho successfully initialized.")); } void deinitTacho(void) { @@ -118,12 +118,12 @@ class PWMFanUsermod : public Usermod { if (pwmChannel == 255) { //no more free LEDC channels deinitPWMfan(); return; } - // configure LED PWM functionalitites + // configure LED PWM functionalities ledcSetup(pwmChannel, 25000, 8); // attach the channel to the GPIO to be controlled ledcAttachPin(pwmPin, pwmChannel); #endif - DEBUG_PRINTLN(F("Fan PWM sucessfully initialized.")); + DEBUG_PRINTLN(F("Fan PWM successfully initialized.")); } void deinitPWMfan(void) { @@ -290,6 +290,10 @@ class PWMFanUsermod : public Usermod { } } + void appendConfigData() { + oappend(SET_F("addHB('PWM-fan');")); + } + /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) diff --git a/usermods/RTC/usermod_rtc.h b/usermods/RTC/usermod_rtc.h index fd9a405446..a68bf82173 100644 --- a/usermods/RTC/usermod_rtc.h +++ b/usermods/RTC/usermod_rtc.h @@ -1,34 +1,86 @@ #pragma once +#include // WLEDMM: make sure that I2C drivers have the "right" Wire Object +#include + #include "src/dependencies/time/DS1307RTC.h" #include "wled.h" -//Connect DS1307 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL)) +#define RTC_DELTA 2 // only modify RTC time if delta exceeds this number of seconds + +//Connect DS1307 or DS3231 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL)) class RTCUsermod : public Usermod { private: unsigned long lastTime = 0; - bool disabled = false; + bool RTCfound = false; // WLEDMM to prevent errors after anabling the usermod (Wire.cpp:526] write(): NULL TX buffer pointer) public: + RTCUsermod(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM: this shouldn't be necessary (passthrough of constructor), maybe because Usermod is an abstract class + void setup() { + RTCfound = false; // WLEDMM PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { disabled = true; return; } + if (pins[1].pin < 0 || pins[0].pin < 0) { enabled=false; return; } //WLEDMM bugfix - ensure that "final" GPIO are valid and no "-1" sneaks trough + if (!enabled) { DEBUG_PRINTLN(F("RTC usermod not enabled.")); return; } + + // WLEDMM join hardware I2C + if (!pinManager.joinWire()) { // WLEDMM - this allocates global I2C pins, then starts Wire - if not started previously + enabled = false; + return; + } + + //if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { disabled = true; return; } +#if defined(ARDUINO_ARCH_ESP32) + //Wire.begin(pins[1].pin, pins[0].pin); // WLEDMM this might silently fail, which is OK as it just means that I2C bus is already running. +#else + //Wire.begin(); // WLEDMM - i2c pins on 8266 are fixed. +#endif + RTC.begin(); time_t rtcTime = RTC.get(); if (rtcTime) { toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC); updateLocalTime(); + USER_PRINTLN(F("Localtime updated from RTC.")); } else { - if (!RTC.chipPresent()) disabled = true; //don't waste time if H/W error + if (!RTC.chipPresent()) { + enabled = false; //don't waste time if H/W error + USER_PRINTLN(F("RTC board not present.")); + } } + if (enabled) RTCfound = true; // WLEDMM } void loop() { - if (strip.isUpdating()) return; - if (!disabled && toki.isTick()) { + // WLEDMM begin + if (!enabled) return; + if (!RTCfound) return; // WLEDMM + unsigned long currentTime = millis(); // get the current elapsed time + if (strip.isUpdating() && (currentTime - lastTime < 10000)) return; // WLEDMM: be nice + // WLEDMM end + + if (enabled && toki.isTick()) { // WLEDMM time_t t = toki.second(); - if (t != RTC.get()) RTC.set(t); //set RTC to NTP/UI-provided value + lastTime = millis(); // WLEDMM + + if (abs(t - RTC.get())> RTC_DELTA) { // WLEDMM only consider time diffs > 2 seconds + if ( (toki.getTimeSource() == TOKI_TS_NTP) + ||( (toki.getTimeSource() != TOKI_TS_NONE) && (toki.getTimeSource() != TOKI_TS_RTC) + && (toki.getTimeSource() != TOKI_TS_BAD) && (toki.getTimeSource() != TOKI_TS_UDP_SEC) && (toki.getTimeSource() != TOKI_TS_UDP))) + { // WLEDMM update RTC if we have a reliable time source + RTC.set(t); //set RTC to NTP/UI-provided value - WLEDMM allow up to 3 sec deviation + USER_PRINTLN(F("RTC updated using localtime.")); + } else { + // WLEDMM if no reliable time -> update from RTC + time_t rtcTime = RTC.get(); + if (rtcTime) { + toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC); + updateLocalTime(); + USER_PRINTLN(F("Localtime updated from RTC.")); + } + } + } } } diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h index 60861e4c50..1e92d7d716 100644 --- a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h +++ b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h @@ -30,7 +30,7 @@ #define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f #endif -// only report if differance grater than offset value +// only report if difference grater than offset value #ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE #define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 #endif diff --git a/usermods/TTGO-T-Display/usermod.cpp b/usermods/TTGO-T-Display/usermod.cpp index b126d40a00..cbba07771d 100644 --- a/usermods/TTGO-T-Display/usermod.cpp +++ b/usermods/TTGO-T-Display/usermod.cpp @@ -3,7 +3,7 @@ * This file allows you to add own functionality to WLED more easily * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) - * bytes 2400+ are currently ununsed, but might be used for future wled features + * bytes 2400+ are currently unused, but might be used for future wled features */ /* @@ -144,7 +144,7 @@ void userLoop() { // First row with Wifi name tft.setCursor(1, 1); tft.print(knownSsid.substring(0, tftcharwidth > 1 ? tftcharwidth - 1 : 0)); - // Print `~` char to indicate that SSID is longer, than our dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > tftcharwidth) tft.print("~"); diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index 2657c6c8f3..b41e3e1199 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -18,7 +18,7 @@ Copy the example `platformio_override.ini` to the root directory. This file sho * `USERMOD_DALLASTEMPERATURE` - enables this user mod wled00/usermods_list.cpp * `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - number of milliseconds between measurements, defaults to 60000 ms (60s) -All parameters can be configured at runtime via the Usermods settings page, including pin, temperature in degrees Celsius or Farenheit and measurement interval. +All parameters can be configured at runtime via the Usermods settings page, including pin, temperature in degrees Celsius or Fahrenheit and measurement interval. ## Project link diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index a15baf8785..c591ae56e9 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -21,7 +21,6 @@ class UsermodTemperature : public Usermod { private: - bool initDone = false; OneWire *oneWire; // GPIO pin used for sensor (with a default compile-time fallback) int8_t temperaturePin = TEMPERATURE_PIN; @@ -45,13 +44,9 @@ class UsermodTemperature : public Usermod { // temperature if flashed to a board without a sensor attached byte sensorFound; - bool enabled = true; - bool HApublished = false; // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; static const char _readInterval[]; static const char _parasite[]; static const char _parasitePin[]; @@ -67,6 +62,7 @@ class UsermodTemperature : public Usermod { public: + UsermodTemperature(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM: this shouldn't be necessary (passthrough of constructor), maybe because Usermod is an abstract class /* * API calls te enable data exchange between WLED modules */ @@ -353,8 +349,10 @@ void UsermodTemperature::addToJsonInfo(JsonObject& root) { */ void UsermodTemperature::addToConfig(JsonObject &root) { // we add JSON object: {"Temperature": {"pin": 0, "degC": true}} - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; + //WLEDMM: call superclass + Usermod::addToConfig(root); + JsonObject top = root[FPSTR(_name)]; + top["pin"] = temperaturePin; // usermodparam top["degC"] = degC; // usermodparam top[FPSTR(_readInterval)] = readingInterval / 1000; @@ -370,16 +368,18 @@ void UsermodTemperature::addToConfig(JsonObject &root) { */ bool UsermodTemperature::readFromConfig(JsonObject &root) { // we look for JSON object: {"Temperature": {"pin": 0, "degC": true}} - int8_t newTemperaturePin = temperaturePin; + //WLEDMM call superclass + Usermod::readFromConfig(root); //WLEDMM: configComplete not implemented here (todo?) + JsonObject top = root[FPSTR(_name)]; DEBUG_PRINT(FPSTR(_name)); - JsonObject top = root[FPSTR(_name)]; + int8_t newTemperaturePin = temperaturePin; + if (top.isNull()) { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } - enabled = top[FPSTR(_enabled)] | enabled; newTemperaturePin = top["pin"] | newTemperaturePin; degC = top["degC"] | degC; readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000; @@ -410,6 +410,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { } void UsermodTemperature::appendConfigData() { + oappend(SET_F("addHB('Temperature');")); // WLEDMM oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasite)).c_str()); oappend(SET_F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); @@ -425,8 +426,6 @@ const char *UsermodTemperature::getTemperatureUnit() { } // strings to reduce flash memory usage (used more than twice) -const char UsermodTemperature::_name[] PROGMEM = "Temperature"; -const char UsermodTemperature::_enabled[] PROGMEM = "enabled"; const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s"; const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr"; const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin"; diff --git a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h index 39c2c3ef73..fb616e45be 100644 --- a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h +++ b/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h @@ -50,14 +50,21 @@ class UsermodVL53L0XGestures : public Usermod { public: void setup() { - PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { enabled = false; return; } - Wire.begin(); + // WLEDMM join hardware I2C + if (!pinManager.joinWire()) { // WLEDMM - this allocates global I2C pins, then starts Wire - if not started previously + enabled = false; + return; + } + + //PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } }; + //if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { enabled = false; return; } + //Wire.begin(); sensor.setTimeout(150); if (!sensor.init()) { DEBUG_PRINTLN(F("Failed to detect and initialize VL53L0X sensor!")); + enabled = false; // WLEDMM bugfix } else { sensor.setMeasurementTimingBudget(20000); // set high speed mode } diff --git a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp index 78cc32a81e..e7d1212a14 100644 --- a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp +++ b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp @@ -34,30 +34,30 @@ uint8_t DALLAS_PIN =23; uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; uint8_t DALLAS_PIN =13; -// uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 +// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN -//#define U8X8_PIN_RESET RST_PIN // Uncoment for Heltec WiFi-Kit-8 +//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // Dallas sensor reading timer long temptimer = millis(); long lastMeasure = 0; -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -// --> First choise of cheap I2C OLED 128X32 0.91" +// --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Third choise of Heltec WiFi-Kit-8 OLED 128X32 0.91" +// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here void userSetup() { @@ -97,7 +97,7 @@ void userLoop() { //----> Dallas temperature sensor MQTT publishing temptimer = millis(); -// Timer to publishe new temperature every 60 seconds +// Timer to publish new temperature every 60 seconds if (temptimer - lastMeasure > 60000) { lastMeasure = temptimer; @@ -106,7 +106,7 @@ void userLoop() { if (mqtt != nullptr) { // Serial.println(Dallas(DALLAS_PIN,0)); -//Gets prefered temperature scale based on selection in definitions section +//Gets preferred temperature scale based on selection in definitions section #ifdef Celsius int16_t board_temperature = Dallas(DALLAS_PIN,0); #else @@ -173,11 +173,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) diff --git a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp index c9d9a527e4..ff1cf7e534 100644 --- a/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp +++ b/usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp @@ -6,7 +6,7 @@ void UpdateBME280Data(); -#define Celsius // Show temperature mesaurement in Celcius otherwise is in Fahrenheit +#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit BME280I2C bme; // Default : forced mode, standby time = 1000 ms // Oversampling = pressure Ɨ1, temperature Ɨ1, humidity Ɨ1, filter off, @@ -16,25 +16,25 @@ uint8_t SDA_PIN = 21; #else //ESP8266 boards uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; -// uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 +// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN -//#define U8X8_PIN_RESET RST_PIN // Uncoment for Heltec WiFi-Kit-8 +//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -// --> First choise of cheap I2C OLED 128X32 0.91" +// --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Second choise of cheap I2C OLED 128X64 0.96" or 1.3" +// --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA -// --> Third choise of Heltec WiFi-Kit-8 OLED 128X32 0.91" +// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here @@ -179,11 +179,11 @@ void userLoop() { // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); - // Print `~` char to indicate that SSID is longer, than owr dicplay + // Print `~` char to indicate that SSID is longer, than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); - // Second row with IP or Psssword + // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) diff --git a/usermods/artifx/arti.h b/usermods/artifx/arti.h new file mode 100644 index 0000000000..21eb649fbb --- /dev/null +++ b/usermods/artifx/arti.h @@ -0,0 +1,2762 @@ +/* + @title Arduino Real Time Interpreter (ARTI) + @file arti.h + @date 20220818 + @author Ewoud Wijma + @Copyright (c) 2024 Ewoud Wijma + @repo https://github.com/ewoudwijma/ARTI + @remarks + - #define ARDUINOJSON_DEFAULT_NESTING_LIMIT 100 //set this in ArduinoJson!!!, currently not necessary... + - IF UPDATING THIS FILE IN THE WLED REPO, SEND A PULL REQUEST TO https://github.com/ewoudwijma/ARTI AS WELL!!! + @later + - Code improvement + - See 'for some weird reason this causes a crash on esp32' + - check why column/lineno not correct + - Definition improvements + - support string (e.g. for print) + - add integer and string stacks + - print every x seconds (to use it in loops. e.g. to show free memory) + - reserved words (ext functions and variables cannot be used as variables) + - check on return values + - arrays (indices) for varref + - WLED improvements + - rename program to sketch? + @progress + - SetPixelColor without colorwheel + - extend errorOccurred and add warnings (continue) next to errors (stop). Include stack full/empty + - WLED: *arti in SEGENV.data: not working well as change mode will free(data) + - move code from interpreter to analyzer to speed up interpreting + @done? + @done + - save log after first run of loop to get runtime errors included (or 30 iterations to also capture any stack overflows) + @todo + - check why statement is not 'shrinked' + - make default work in js (remove default as we have now load template) + - add PI + - color_fade_pulse because /pixelCount instead of ledCount should not crash + */ + +#pragma once + +#define ARTI_SERIAL 1 +#define ARTI_FILE 2 + +#if ARTI_PLATFORM == ARTI_ARDUINO //defined in arti_definition.h e.g. arti_wled.h! + #include "../../wled00/wled.h" + #include "../../wled00/src/dependencies/json/ArduinoJson-v6.h" + + File logFile; + + #define ARTI_ERRORWARNING 1 //shows lexer, parser, analyzer and interpreter errors + // #define ARTI_DEBUG 1 + // #define ARTI_ANDBG 1 + // #define ARTI_RUNLOG 1 //if set on arduino this will create massive amounts of output (as ran in a loop) + #define ARTI_MEMORY 1 //to do analyses of memory usage, trace memoryleaks (works only on arduino) + #define ARTI_PRINT 1 //will show the printf calls + + const char spaces[51] PROGMEM = " "; + #if defined(ARDUINO_ARCH_ESP32) + #define FREE_SIZE esp_get_free_heap_size() + #define asChar(x) x.as().c_str() //WLEDMM: tbd: find out why char * is causing crashes!!! + #else + #define FREE_SIZE ESP.getFreeHeap() + #define asChar(x) "" + //x.as() //WLEDMM: no arduinojson output for the moment as this is in testing phase + #endif + // #define OPTIMIZED_TREE 1 +#else //embedded + #include "dependencies/ArduinoJson-recent.h" + + FILE * logFile; // FILE needed to use in fprintf (std stream does not work) + + #define ARTI_ERRORWARNING 1 + #define ARTI_DEBUG 1 + #define ARTI_ANDBG 1 + #define ARTI_RUNLOG 1 + #define ARTI_MEMORY 1 + #define ARTI_PRINT 1 + + #include + #include + #include + #include + + const char spaces[51] = " "; + #define FREE_SIZE (unsigned int)0 //No free heap function found on embedded + #define asChar(x) x.as() + // #define OPTIMIZED_TREE 1 +#endif + +bool logToFile = true; //print output to file (e.g. default.wled.log) +uint32_t frameCounter = 0; //tbd move to class if more instances run + +void artiPrintf(char const * format, ...) +{ + va_list argp; + + va_start(argp, format); + + // if (!logToFile) + // { + // //WLEDMM: not working to use USER_PRINTF here in case of arduino so moving to rocket science part + // vprintf(format, argp); + // } + // else + #if ARTI_PLATFORM == ARTI_ARDUINO + // rocket science here! As logfile.printf causes crashes/wrong output we create our own printf here + // logFile.printf(format, argp); + for (size_t i = 0; i < strlen(format); i++) + { + if (format[i] == '%') + { + switch (format[i+1]) + { + case 's': + if (logToFile) logFile.print(va_arg(argp, const char *)); else USER_PRINT(va_arg(argp, const char *)); + break; + case 'u': + if (logToFile) logFile.print(va_arg(argp, unsigned int)); else USER_PRINT(va_arg(argp, unsigned int)); + break; + case 'c': + if (logToFile) logFile.print((char)va_arg(argp, int)); else USER_PRINT(va_arg(argp, int)); + break; + case 'f': + if (logToFile) logFile.print(va_arg(argp, double)); else USER_PRINT(va_arg(argp, double)); + break; + case '%': + if (logToFile) logFile.print("%"); else USER_PRINT("%"); // in case of %% + break; + default: + va_arg(argp, int); + // logFile.print(x); + if (logToFile) logFile.print(format[i]); else USER_PRINT(format[i]); + if (logToFile) logFile.print(format[i+1]); else USER_PRINT(format[i+1]); + } + i++; + } + else + { + if (logToFile) logFile.print(format[i]); else USER_PRINT(format[i]); + } + } + #else + vfprintf(logFile, format, argp); + #endif + va_end(argp); +} + +//add millis function for non arduino +#if ARTI_PLATFORM != ARTI_ARDUINO + uint32_t millis() + { + return std::chrono::system_clock::now().time_since_epoch().count(); + } +#endif + +#ifdef ARTI_DEBUG + #define DEBUG_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define DEBUG_ARTI(...) +#endif + +#ifdef ARTI_ANDBG + #define ANDBG_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define ANDBG_ARTI(...) +#endif + +#ifdef ARTI_RUNLOG + #define RUNLOG_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define RUNLOG_ARTI(...) +#endif + +#ifdef ARTI_PRINT + #define PRINT_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define PRINT_ARTI(...) +#endif + +#ifdef ARTI_ERRORWARNING + #define ERROR_ARTI(...) artiPrintf(__VA_ARGS__) + #define WARNING_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define ERROR_ARTI(...) + #define WARNING_ARTI(...) +#endif + +#ifdef ARTI_MEMORY + #define MEMORY_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define MEMORY_ARTI(...) +#endif + +#define charLength 30 +#define fileNameLength 50 +#define arrayLength 30 + +#define floatNull -32768 + +const char * stringOrEmpty(const char *charS) { + if (charS == nullptr) + return ""; + else + return charS; +} + +//define strupr as only supported in windows toolchain +char* strupr(char* s) +{ + char* tmp = s; + + for (;*tmp;++tmp) { + *tmp = toupper((unsigned char) *tmp); + } + + return s; +} + +enum Tokens +{ + F_integerConstant, + F_realConstant, + F_plus, + F_minus, + F_multiplication, + F_division, + F_modulo, + F_bitShiftLeft, + F_bitShiftRight, + F_equal, + F_notEqual, + F_lessThen, + F_lessThenOrEqual, + F_greaterThen, + F_greaterThenOrEqual, + F_and, + F_or, + F_plusplus, + F_minmin, + F_NoToken = 255 +}; + +const char * tokenToString(uint8_t key) +{ + switch (key) { + case F_integerConstant: + return "integer"; + break; + case F_realConstant: + return "real"; + break; + case F_plus: + return "+"; + break; + case F_minus: + return "-"; + break; + case F_multiplication: + return "*"; + break; + case F_division: + return "/"; + break; + case F_modulo: + return "%"; + break; + case F_bitShiftLeft: + return "<<"; + break; + case F_bitShiftRight: + return ">>"; + break; + case F_equal: + return "=="; + break; + case F_notEqual: + return "!="; + break; + case F_lessThen: + return "<"; + break; + case F_lessThenOrEqual: + return "<="; + break; + case F_greaterThen: + return ">"; + break; + case F_greaterThenOrEqual: + return ">="; + break; + case F_and: + return "&&"; + break; + case F_or: + return "||"; + break; + case F_plusplus: + return "++"; + break; + case F_minmin: + return "--"; + break; + } + return "unknown key"; +} + +uint8_t stringToToken(const char * token, const char * value) +{ + if (strcmp(token, "INTEGER_CONST") == 0) + return F_integerConstant; + else if (strcmp(token, "REAL_CONST") == 0) + return F_realConstant; + else if (strcmp(value, "+") == 0) + return F_plus; + else if (strcmp(value, "-") == 0) + return F_minus; + else if (strcmp(value, "*") == 0) + return F_multiplication; + else if (strcmp(value, "/") == 0) + return F_division; + else if (strcmp(value, "%") == 0) + return F_modulo; + else if (strcmp(value, "<<") == 0) + return F_bitShiftLeft; + else if (strcmp(value, ">>") == 0) + return F_bitShiftRight; + else if (strcmp(value, "==") == 0) + return F_equal; + else if (strcmp(value, "!=") == 0) + return F_notEqual; + else if (strcmp(value, ">") == 0) + return F_greaterThen; + else if (strcmp(value, ">=") == 0) + return F_greaterThenOrEqual; + else if (strcmp(value, "<") == 0) + return F_lessThen; + else if (strcmp(value, "<=") == 0) + return F_lessThenOrEqual; + else if (strcmp(value, "&&") == 0) + return F_and; + else if (strcmp(value, "||") == 0) + return F_or; + else if (strcmp(value, "+=") == 0) + return F_plus; + else if (strcmp(value, "-=") == 0) + return F_minus; + else if (strcmp(value, "*=") == 0) + return F_multiplication; + else if (strcmp(value, "/=") == 0) + return F_division; + else if (strcmp(value, "++") == 0) + return F_plusplus; + else if (strcmp(value, "--") == 0) + return F_minmin; + else + return F_NoToken; + +} + +enum Nodes +{ + F_Program, + F_Function, + F_Call, + F_VarDef, + F_Assign, + F_Formal, + F_VarRef, + F_For, + F_If, + F_Cex, + F_Expr, + F_Term, + #ifdef OPTIMIZED_TREE + F_Statement, + F_Indices, + F_Formals, + F_Factor, + F_Block, + F_Actuals, + F_Increment, + F_AssignOperator, + #endif + F_NoNode = 255 +}; + +//Tokens +// ID + +//Optimizer +// level +// index +// external + +// block +// formals +// actuals +// increment +// assignoperator +// type (not used yet in wled) + +//expr +//indices + +const char * nodeToString(uint8_t key) +{ + switch (key) { + case F_Program: + return "program"; + case F_Function: + return "function"; + case F_Call: + return "call"; + case F_VarDef: + return "variable"; + case F_Assign: + return "assign"; + case F_Formal: + return "formal"; + case F_VarRef: + return "varref"; + case F_For: + return "for"; + case F_If: + return "if"; + case F_Cex: + return "cex"; + case F_Expr: + return "expr"; + case F_Term: + return "term"; + #ifdef OPTIMIZED_TREEX + case F_Statement: + return "statement"; + case F_Indices: + return "indices"; + case F_Formals: + return "formals"; + case F_Factor: + return "factor"; + case F_Block: + return "block"; + case F_Actuals: + return "actuals"; + #endif + } + return "unknown key"; +} + +uint8_t stringToNode(const char * node) +{ + if (strcmp(node, "program") == 0) + return F_Program; + else if (strcmp(node, "function") == 0) + return F_Function; + else if (strcmp(node, "call") == 0) + return F_Call; + else if (strcmp(node, "variable") == 0) + return F_VarDef; + else if (strcmp(node, "assign") == 0) + return F_Assign; + else if (strcmp(node, "formal") == 0) + return F_Formal; + else if (strcmp(node, "varref") == 0) + return F_VarRef; + else if (strcmp(node, "for") == 0) + return F_For; + else if (strcmp(node, "if") == 0) + return F_If; + else if (strcmp(node, "cex") == 0) + return F_Cex; + else if (strcmp(node, "expr") == 0) + return F_Expr; + else if (strcmp(node, "term") == 0) + return F_Term; + #ifdef OPTIMIZED_TREE + else if (strcmp(node, "statement") == 0) + return F_Statement; + else if (strcmp(node, "indices") == 0) + return F_Indices; + else if (strcmp(node, "formals") == 0) + return F_Formals; + else if (strcmp(node, "factor") == 0) + return F_Factor; + else if (strcmp(node, "block") == 0) + return F_Block; + else if (strcmp(node, "actuals") == 0) + return F_Actuals; + else if (strcmp(node, "increment") == 0) + return F_Increment; + else if (strcmp(node, "assignoperator") == 0) + return F_AssignOperator; + #endif + return F_NoNode; +} + +bool errorOccurred = false; + +struct Token { + uint16_t lineno; + uint16_t column; + char type[charLength]; + char value[charLength]; +}; + +struct LexerPosition { + uint16_t pos; + char current_char; + uint16_t lineno; + uint16_t column; + char type[charLength]; + char value[charLength]; +}; + +#define nrOfPositions 20 + +class Lexer { + private: + public: + const char * text; + uint16_t pos; + char current_char; + uint16_t lineno; + uint16_t column; + JsonObject definitionJson; + Token current_token; + LexerPosition positions[nrOfPositions]; //should be array of pointers but for some reason get seg fault (because a struct and not a class...) + uint8_t positions_index = 0; + + Lexer(const char * programText, JsonObject definitionJson) { + this->text = programText; + this->definitionJson = definitionJson; + this->pos = 0; + this->current_char = this->text[this->pos]; + this->lineno = 1; + this->column = 1; + } + + ~Lexer() { + DEBUG_ARTI("Destruct Lexer\n"); + } + + void advance() { + if (this->current_char == '\n') + { + this->lineno += 1; + this->column = 0; + } + this->pos++; + + if (this->pos > strlen(this->text) - 1) + this->current_char = -1; + else + { + this->current_char = this->text[this->pos]; + this->column++; + } + } + + void skip_whitespace() + { + while (this->current_char != -1 && isspace(this->current_char)) + this->advance(); + } + + void skip_comment(const char * endTokens) + { + while (strncmp(this->text + this->pos, endTokens, strlen(endTokens)) != 0) + this->advance(); + for (size_t i=0; iadvance(); + } + + void number() + { + current_token.lineno = this->lineno; + current_token.column = this->column; + strcpy(current_token.type, ""); + strcpy(current_token.value, ""); + + char result[charLength] = ""; + while (this->current_char != -1 && isdigit(this->current_char)) + { + result[strlen(result)] = this->current_char; + this->advance(); + } + if (this->current_char == '.') + { + result[strlen(result)] = this->current_char; + this->advance(); + + while (this->current_char != -1 && isdigit(this->current_char)) + { + result[strlen(result)] = this->current_char; + this->advance(); + } + + result[strlen(result)] = '\0'; + strcpy(current_token.type, "REAL_CONST"); + strcpy(current_token.value, result); + } + else + { + result[strlen(result)] = '\0'; + strcpy(current_token.type, "INTEGER_CONST"); + strcpy(current_token.value, result); + } + + } + + void id() + { + current_token.lineno = this->lineno; + current_token.column = this->column; + strcpy(current_token.type, ""); + strcpy(current_token.value, ""); + + char result[charLength] = ""; + while (this->current_char != -1 && (isalnum(this->current_char) || this->current_char == '_')) + { + result[strlen(result)] = this->current_char; + this->advance(); + } + result[strlen(result)] = '\0'; + + char resultUpper[charLength]; + strcpy(resultUpper, result); + strupr(resultUpper); + + if (definitionJson["TOKENS"].containsKey(resultUpper)) + { + strcpy(current_token.type, definitionJson["TOKENS"][resultUpper]); + strcpy(current_token.value, resultUpper); + } + else + { + strcpy(current_token.type, "ID"); + strcpy(current_token.value, result); + } + } + + void get_next_token() + { + current_token.lineno = this->lineno; + current_token.column = this->column; + strcpy(current_token.type, ""); + strcpy(current_token.value, ""); + + if (errorOccurred) return; + + while (this->current_char != -1 && this->pos <= strlen(this->text) - 1 && !errorOccurred) + { + if (isspace(this->current_char)) { + this->skip_whitespace(); + continue; + } + + if (strncmp(this->text + this->pos, "/*", 2) == 0) + { + this->advance(); + skip_comment("*/"); + continue; + } + + if (strncmp(this->text + this->pos, "//", 2) == 0) + { + this->advance(); + skip_comment("\n"); + continue; + } + + if (isalpha(this->current_char)) + { + this->id(); + return; + } + + if (isdigit(this->current_char) || (this->current_char == '.' && isdigit(this->text[this->pos+1]))) + { + this->number(); + return; + } + + // findLongestMatchingToken + char token_type[charLength] = ""; + char token_value[charLength] = ""; + + uint8_t longestTokenLength = 0; + + for (JsonPair tokenPair: definitionJson["TOKENS"].as()) { + const char * value = tokenPair.value(); + char currentValue[charLength+1]; + strncpy(currentValue, this->text + this->pos, charLength); + currentValue[strlen(value)] = '\0'; + if (strcmp(value, currentValue) == 0 && strlen(value) > longestTokenLength) { + strcpy(token_type, tokenPair.key().c_str()); + strcpy(token_value, value); + longestTokenLength = strlen(value); + } + } + + if (strcmp(token_type, "") != 0 && strcmp(token_value, "") != 0) + { + strcpy(current_token.type, token_type); + strcpy(current_token.value, token_value); + for (size_t i=0; iadvance(); + return; + } + else { + ERROR_ARTI("Lexer error on %c line %u col %u\n", this->current_char, this->lineno, this->column); + errorOccurred = true; + } + } + } //get_next_token + + void eat(const char * token_type) { + // DEBUG_ARTI("try to eat %s %s\n", lexer->current_token.type, token_type); + if (strcmp(current_token.type, token_type) == 0) { + get_next_token(); + // DEBUG_ARTI("eating %s -> %s %s\n", token_type, lexer->current_token.type, lexer->current_token.value); + } + else { + ERROR_ARTI("Lexer Error: Unexpected token %s %s\n", current_token.type, current_token.value); + errorOccurred = true; + } + } + + void push_position() { + if (positions_index < nrOfPositions) { + positions[positions_index].pos = this->pos; + positions[positions_index].current_char = this->current_char; + positions[positions_index].lineno = this->lineno; + positions[positions_index].column = this->column; + strcpy(positions[positions_index].type, current_token.type); + strcpy(positions[positions_index].value, current_token.value); + positions_index++; + } + else + ERROR_ARTI("not enough positions %u\n", nrOfPositions); + } + + void pop_position() { + if (positions_index > 0) { + positions_index--; + this->pos = positions[positions_index].pos; + this->current_char = positions[positions_index].current_char; + this->lineno = positions[positions_index].lineno; + this->column = positions[positions_index].column; + strcpy(current_token.type, positions[positions_index].type); + strcpy(current_token.value, positions[positions_index].value); + } + else + ERROR_ARTI("no positions saved\n"); + } + +}; //Lexer + +#define ResultFail 0 +#define ResultStop 2 +#define ResultContinue 1 + +class ScopedSymbolTable; //forward declaration + +class Symbol { + private: + public: + + uint8_t symbol_type; + char name[charLength]; + uint8_t type; + uint8_t scope_level; + uint8_t scope_index; + ScopedSymbolTable* scope = nullptr; + ScopedSymbolTable* function_scope = nullptr; //used to find the formal parameters in the scope of a function node + + JsonVariant block; + + Symbol(uint8_t symbol_type, const char * name, uint8_t type = 9) { + this->symbol_type = symbol_type; + strcpy(this->name, name); + this->type = type; + this->scope_level = 0; + } + + ~Symbol() { + MEMORY_ARTI("Destruct Symbol %s (%u)\n", name, FREE_SIZE); + } + +}; //Symbol + +#define nrOfSymbolsPerScope 30 +#define nrOfChildScope 10 //add checks + +class ScopedSymbolTable { + private: + public: + + Symbol* symbols[nrOfSymbolsPerScope]; + uint8_t symbolsIndex = 0; + uint8_t nrOfFormals = 0; + char scope_name[charLength]; + uint8_t scope_level; + ScopedSymbolTable *enclosing_scope; + ScopedSymbolTable *child_scopes[nrOfChildScope]; + uint8_t child_scopesIndex = 0; + + ScopedSymbolTable(const char * scope_name, int scope_level, ScopedSymbolTable *enclosing_scope = nullptr) { + strcpy(this->scope_name, scope_name); + this->scope_level = scope_level; + this->enclosing_scope = enclosing_scope; + } + + ~ScopedSymbolTable() { + for (uint8_t i=0; iinsert(BuiltinTypeSymbol('INTEGER')); + // this->insert(BuiltinTypeSymbol('REAL')); + } + + void insert(Symbol* symbol) + { + #ifdef _SHOULD_LOG_SCOPE + ANDBG_ARTI("Log scope Insert %s\n", symbol->name.c_str()); + #endif + symbol->scope_level = this->scope_level; + symbol->scope = this; + symbol->scope_index = symbolsIndex; + if (symbolsIndex < nrOfSymbolsPerScope) + this->symbols[symbolsIndex++] = symbol; + else + ERROR_ARTI("ScopedSymbolTable %s symbols full (%d)", scope_name, nrOfSymbolsPerScope); + } + + Symbol* lookup(const char * name, bool current_scope_only=false) + { + for (uint8_t i=0; iname, name) == 0) + return symbols[i]; + } + + if (current_scope_only) + return nullptr; + // # recursively go up the chain and lookup the name; + if (this->enclosing_scope != nullptr) + return this->enclosing_scope->lookup(name); + + return nullptr; + } //lookup + +}; //ScopedSymbolTable + +#define nrOfVariables 20 + +class ActivationRecord +{ + private: + public: + char name[charLength]; + char type[charLength]; + int nesting_level; + // char charMembers[charLength][nrOfVariables]; + float floatMembers[nrOfVariables]; + char lastSet[charLength]; + uint8_t lastSetIndex; + + ActivationRecord(const char * name, const char * type, int nesting_level) + { + strcpy(this->name, name); + strcpy(this->type, type); + this->nesting_level = nesting_level; + } + + ~ActivationRecord() + { + RUNLOG_ARTI("Destruct activation record %s\n", name); + } + + // void set(uint8_t index, const char * value) + // { + // lastSetIndex = index; + // strcpy(charMembers[index], value); + // } + + void set(uint8_t index, float value) + { + lastSetIndex = index; + floatMembers[index] = value; + } + + // const char * getChar(uint8_t index) + // { + // return charMembers[index]; + // } + + float getFloat(uint8_t index) + { + return floatMembers[index]; + } + +}; //ActivationRecord + +#define nrOfRecords 20 + +class CallStack { +public: + ActivationRecord* records[nrOfRecords]; + uint8_t recordsCounter = 0; + + CallStack() { + } + + ~CallStack() + { + RUNLOG_ARTI("Destruct callstack\n"); + } + + void push(ActivationRecord* ar) + { + if (recordsCounter < nrOfRecords) + { + // RUNLOG_ARTI("%s\n", "Push ", ar->name); + this->records[recordsCounter++] = ar; + } + else + { + errorOccurred = true; + ERROR_ARTI("no space left in callstack\n"); + } + } + + ActivationRecord* pop() + { + if (recordsCounter > 0) + { + // RUNLOG_ARTI("%s\n", "Pop ", this->peek()->name); + return this->records[recordsCounter--]; + } + else + { + ERROR_ARTI("no ar left on callstack\n"); + return nullptr; + } + } + + ActivationRecord* peek() + { + return this->records[recordsCounter-1]; + } +}; //CallStack + +class ValueStack +{ +private: +public: + // char charStack[arrayLength][charLength]; //currently only floatStack used. + float floatStack[arrayLength]; + uint8_t stack_index = 0; + + ValueStack() + { + } + + ~ValueStack() + { + RUNLOG_ARTI("Destruct valueStack\n"); + } + + // void push(const char * value) { + // if (stack_index >= arrayLength) + // ERROR_ARTI("Push charStack full %u of %u\n", stack_index, arrayLength); + // else if (value == nullptr) { + // strcpy(charStack[stack_index++], "empty"); + // ERROR_ARTI("Push null pointer on float stack\n"); + // } + // else + // // RUNLOG_ARTI("calc push %s %s\n", key, value); + // strcpy(charStack[stack_index++], value); + // } + + void push(float value) + { + if (stack_index >= arrayLength) + { + ERROR_ARTI("Push floatStack full (check functions with result assigned) %u\n", arrayLength); + errorOccurred = true; + } + else if (value == floatNull) + ERROR_ARTI("Push null value on float stack\n"); + else + // RUNLOG_ARTI("calc push %s %s\n", key, value); + floatStack[stack_index++] = value; + } + + // const char * peekChar() { + // // RUNLOG_ARTI("Calc Peek %s\n", charStack[stack_index-1]); + // return charStack[stack_index-1]; + // } + + float peekFloat() + { + // RUNLOG_ARTI("Calc Peek %s\n", floatStack[stack_index-1]); + return floatStack[stack_index-1]; + } + + // const char * popChar() { + // if (stack_index>0) { + // stack_index--; + // return charStack[stack_index]; + // } + // else { + // ERROR_ARTI("Pop value stack empty\n"); + // errorOccurred = true; +// // RUNLOG_ARTI("Calc Pop %s\n", charStack[stack_index]); + // return "novalue"; + // } + // } + + float popFloat() + { + if (stack_index>0) + { + stack_index--; + return floatStack[stack_index]; + } + else + { + ERROR_ARTI("Pop floatStack empty\n"); + // RUNLOG_ARTI("Calc Pop %s\n", floatStack[stack_index]); + errorOccurred = true; + return -1; + } + } + +}; //ValueStack + +#define programTextSize 5000 + +class ARTI { +private: + Lexer *lexer = nullptr; + + PSRAMDynamicJsonDocument *definitionJsonDoc = nullptr; + PSRAMDynamicJsonDocument *parseTreeJsonDoc = nullptr; + JsonObject definitionJson; + JsonVariant parseTreeJson; + + ScopedSymbolTable *global_scope = nullptr; + CallStack *callStack = nullptr; + ValueStack *valueStack = nullptr; + + uint8_t stages = 5; //for debugging: 0:parseFile, 1:Lexer, 2:parse, 3:optimize, 4:analyze, 5:interpret should be 5 if no debugging + + char logFileName[fileNameLength]; + + uint32_t startMillis; + +public: + ARTI() + { + // MEMORY_ARTI("new Arti < %u\n", FREE_SIZE); //logfile not open here + } + + ~ARTI() + { + MEMORY_ARTI("Destruct ARTI\n"); + } + + //defined in arti_definition.h e.g. arti_wled.h! + float arti_external_function(uint8_t function, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull, float par4 = floatNull, float par5 = floatNull); + float arti_get_external_variable(uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); + void arti_set_external_variable(float value, uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); + bool loop(); + + uint8_t parse(JsonVariant parseTree, const char * node_name, char operatorx, JsonVariant expression, uint8_t depth = 0) + { + if (depth > 50) + { + ERROR_ARTI("Error: Parse recursion level too deep at %s (%u)\n", asChar(parseTree), depth); + errorOccurred = true; + } + if (errorOccurred) return ResultFail; + + uint8_t result = ResultContinue; + + uint8_t resultChild = ResultContinue; + + if (expression.is()) //should always be the case + { + for (JsonVariant expressionElement: expression.as()) //e.g. ["PROGRAM","ID","block"] + { + const char * nextNode_name = node_name; //e.g. "program": + JsonVariant nextExpression = expressionElement; // e.g. block + JsonVariant nextParseTree = parseTree; + + JsonVariant nodeExpression = lexer->definitionJson[expressionElement.as()]; + + if (!nodeExpression.isNull()) //is expressionElement a Node e.g. "block" : ["LCURL",{"*": ["statement"]},"RCURL"] + { + nextNode_name = expressionElement; //e.g. block + nextExpression = nodeExpression; // e.g. ["LCURL",{"*": ["statement"]},"RCURL"] + + // DEBUG_ARTI("%s %s %u\n", spaces+50-depth, nextNode_name, depth); //, asChar(parseTree) + + if (parseTree.is()) + { + parseTree[parseTree.size()][nextNode_name]["connect"] = "array"; + nextParseTree = parseTree[parseTree.size()-1]; //nextparsetree is last element in the array (which is always an object) + } + else //no list, create object + { + if (parseTree[node_name].isNull()) //no object yet + parseTree[node_name]["connect"] = "object"; //make the connection, new object item + + nextParseTree = parseTree[node_name]; + } + } + + if (operatorx == '|') + lexer->push_position(); + + if (nextExpression.is()) // e.g. {"?":["LPAREN","formals*","RPAREN"]} + { + JsonObject::iterator objectIterator = nextExpression.as().begin(); + char objectOperator = objectIterator->key().c_str()[0]; + JsonVariant objectElement = objectIterator->value(); + + if (objectElement.is()) + { + if (objectOperator == '*' || objectOperator == '+') + { + nextParseTree[nextNode_name]["*"][0] = "multiple"; // * is another object in the list of objects + nextParseTree = nextParseTree[nextNode_name]["*"]; + } + + //and: see 'is array' + if (objectOperator == '|') + { + resultChild = parse(nextParseTree, nextNode_name, objectOperator, objectElement, depth + 1); + if (resultChild != ResultFail) resultChild = ResultContinue; + } + else + { + uint8_t resultChild2 = ResultContinue; + uint8_t counter = 0; + while (resultChild2 == ResultContinue) + { + resultChild2 = parse(nextParseTree, nextNode_name, objectOperator, objectElement, depth + 1); //no assign to result as optional + + if (objectOperator == '?') { //zero or one iteration, also continue if parse not continue + resultChild2 = ResultContinue; + break; + } + else if (objectOperator == '+') { //one or more iterations, stop if first parse not continue + if (counter == 0) { + if (resultChild2 != ResultContinue) + break; + } + else + { + if (resultChild2 != ResultContinue) + { + resultChild2 = ResultContinue; //always continue + break; + } + } + } + else if (objectOperator == '*') //zero or more iterations, stop if parse not continue + { + if (resultChild2 != ResultContinue) + { + resultChild2 = ResultContinue; //always continue + break; + } + } + else + { + ERROR_ARTI("%s Programming error: undefined %c %s\n", spaces+50-depth, objectOperator, asChar(objectElement)); + resultChild2 = ResultFail; + } + counter++; + } //while + resultChild = resultChild2; + } //not or + } // element is array + else + ERROR_ARTI("%s Definition error: should be an array %s %c %s\n", spaces+50-depth, stringOrEmpty(nextNode_name), operatorx, asChar(objectElement)); + } + else if (nextExpression.is()) // e.g. ["LPAREN", "expr*", "RPAREN"] + { + resultChild = parse(nextParseTree, nextNode_name, '&', nextExpression, depth + 1); // every array element starts with '&' (operatorx is for result of all elements of array) + } + else if (lexer->definitionJson["TOKENS"].containsKey(nextExpression.as())) // token e.g. "ID" + { + const char * token_type = nextExpression; + if (strcmp(lexer->current_token.type, token_type) == 0) + { + DEBUG_ARTI("%s %s %s", spaces+50-depth, lexer->current_token.type, lexer->current_token.value); + + //only add 'semantic tokens' + if (strcmp(lexer->current_token.type, "ID") != 0 && strcmp(lexer->current_token.type, "INTEGER_CONST") != 0 && strcmp(lexer->current_token.type, "REAL_CONST") != 0 && + strcmp(lexer->current_token.type, "INTEGER") != 0 && strcmp(lexer->current_token.type, "REAL") != 0 && + strcmp(lexer->current_token.value, "+") != 0 && strcmp(lexer->current_token.value, "-") != 0 && strcmp(lexer->current_token.value, "*") != 0 && strcmp(lexer->current_token.value, "/") != 0 && strcmp(lexer->current_token.value, "%") != 0 && + strcmp(lexer->current_token.value, "+=") != 0 && strcmp(lexer->current_token.value, "-=") != 0 && strcmp(lexer->current_token.value, "*=") != 0 && strcmp(lexer->current_token.value, "/=") != 0 && + strcmp(lexer->current_token.value, "<<") != 0 && strcmp(lexer->current_token.value, ">>") != 0 && + strcmp(lexer->current_token.value, "==") != 0 && strcmp(lexer->current_token.value, "!=") != 0 && + strcmp(lexer->current_token.value, "&&") != 0 && strcmp(lexer->current_token.value, "||") != 0 && + strcmp(lexer->current_token.value, ">") != 0 && strcmp(lexer->current_token.value, ">=") != 0 && strcmp(lexer->current_token.value, "<") != 0 && strcmp(lexer->current_token.value, "<=") != 0 && + strcmp(lexer->current_token.value, "++") != 0 && strcmp(lexer->current_token.value, "--") != 0 + ) + {} + else + { + if (nextParseTree.is()) + nextParseTree.as()[nextParseTree.size()][lexer->current_token.type] = lexer->current_token.value; //add in last element of array + else + nextParseTree[nextNode_name][lexer->current_token.type] = lexer->current_token.value; + } + + lexer->eat(token_type); + + DEBUG_ARTI(" -> [%s %s] %d\n", lexer->current_token.type, lexer->current_token.value, depth); + + resultChild = ResultContinue; + } + else //deadend + resultChild = ResultFail; + } // if token + else //expressionElement is not a node, not a token, not an array and not an object + { + if (lexer->definitionJson.containsKey(nextExpression.as())) + ERROR_ARTI("%s Programming error: %s not a node, token, array or object in %s\n", spaces+50-depth, asChar(nextExpression), stringOrEmpty(nextNode_name)); + else + ERROR_ARTI("%s Definition error: \"%s\": \"%s\" node should be embedded in array\n", spaces+50-depth, stringOrEmpty(nextNode_name), asChar(nextExpression)); + } //nextExpression is not a token + + if (!nodeExpression.isNull()) //if node + { + if (nextParseTree.containsKey("connect")) + nextParseTree.remove("connect"); //remove connector + // for values which are not parsed deeper. e.g. : {"formal": {"ID": "z"}} + for (JsonPair parseTreePair : nextParseTree.as()) + { + JsonVariant value = parseTreePair.value(); + if (value.containsKey("connect")) + value.remove("connect"); //remove connector + } + + if (resultChild == ResultFail) { //remove result of parse + nextParseTree.remove(nextNode_name); //remove the failed stuff + + // DEBUG_ARTI("%s fail %s\n", spaces+50-depth, nextNode_name); + } + else //success + { + DEBUG_ARTI("%s found %s\n", spaces+50-depth, nextNode_name);//, asChar(nextParseTree)); + //parseTree optimization moved to optimize function + + // if (nextParseTree.is()) + // { + + // // optimize(nextParseTree, depth); + + // JsonObject innerObject = nextParseTree[nextNode_name].as(); + + // JsonObject::iterator begin = innerObject.begin(); + // if (fnextParseTree.size() == 1 && nextParseTree[nextNode_name].size() == 1 && lexer->definitionJson.containsKey(nextNode_name) && lexer->definitionJson.containsKey(nextNode_name) && lexer->definitionJson.containsKey(begin->key())) + // { + // // JsonObject nextParseTreeObject = nextParseTree.as(); + + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, asChar(parseTree)); + // DEBUG_ARTI("%s found replace %s by %s %s\n", spaces+50-depth, nextNode_name, begin->key().c_str(), asChar(nextParseTree)); + // DEBUG_ARTI("%s expression %s\n", spaces+50-depth, asChar(expression)); + // DEBUG_ARTI("%s found %s\n", spaces+50-depth, asChar(nextParseTree[nextNode_name])); + + // nextParseTree.remove(nextNode_name); + // // char temp[charLength]; + // // strcpy(temp, nextNode_name); + // // strcat(temp, "-"); + // // strcat(temp, begin->key().c_str()); + // nextParseTree[begin->key()] = begin->value(); + + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, asChar(parseTree)); + // } + // } + // else + // DEBUG_ARTI("%s no jsonobject??? %s\n", spaces+50-depth, asChar(parseTree)); + + } + } // if node + + //determine result of expressionElement + if (operatorx == '|') { + if (resultChild == ResultFail) {//if fail, go back and try another + // result = ResultContinue; + lexer->pop_position(); + } + else { + result = ResultStop; //Stop or continue is enough for an or + lexer->positions_index--; + } + } + else { + if (resultChild != ResultContinue) //for and, ?, + and *; each result should continue + result = ResultFail; + } + + if (result != ResultContinue) //if no reason to continue then stop + break; + + } //for expressionElement + + if (operatorx == '|') + { + if (result != ResultStop) //still looking but nothing to look for + result = ResultFail; + } + } + else { //should never happen + ERROR_ARTI("%s Programming error: no array %s %c %s\n", spaces+50-depth, stringOrEmpty(node_name), operatorx, asChar(expression)); + } + + return result; + + } //parse + + bool analyze(JsonVariant parseTree, const char * treeElement = nullptr, ScopedSymbolTable* current_scope = nullptr, uint8_t depth = 0) + { + // ANDBG_ARTI("%s Depth %u %s\n", spaces+50-depth, depth, asChar(parseTree)); + if (depth > 24) //otherwise stack canary errors on Arduino (value determined after testing, should be revisited) + { + ERROR_ARTI("Error: Analyze recursion level too deep at %s (%u)\n", asChar(parseTree), depth); + errorOccurred = true; + } + if (errorOccurred) return false; + + if (parseTree.is()) + { + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + if (treeElement == nullptr || strcmp(treeElement, key) == 0 ) //in case there are more elements in the object and you want to analyze only one + { + bool visitedAlready = false; + + if (strcmp(key, "*") == 0 ) // multiple + { + } + else if (strcmp(key, "token") == 0) // do nothing with added tokens + visitedAlready = true; + else if (this->definitionJson["TOKENS"].containsKey(key)) // if token + { + const char * valueStr = value; + + if (strcmp(key, "INTEGER_CONST") == 0) + parseTree["token"] = F_integerConstant; + else if (strcmp(key, "REAL_CONST") == 0) + parseTree["token"] = F_realConstant; + else if (strcmp(valueStr, "+") == 0) + parseTree["token"] = F_plus; + else if (strcmp(valueStr, "-") == 0) + parseTree["token"] = F_minus; + else if (strcmp(valueStr, "*") == 0) + parseTree["token"] = F_multiplication; + else if (strcmp(valueStr, "/") == 0) + parseTree["token"] = F_division; + else if (strcmp(valueStr, "%") == 0) + parseTree["token"] = F_modulo; + else if (strcmp(valueStr, "<<") == 0) + parseTree["token"] = F_bitShiftLeft; + else if (strcmp(valueStr, ">>") == 0) + parseTree["token"] = F_bitShiftRight; + else if (strcmp(valueStr, "==") == 0) + parseTree["token"] = F_equal; + else if (strcmp(valueStr, "!=") == 0) + parseTree["token"] = F_notEqual; + else if (strcmp(valueStr, ">") == 0) + parseTree["token"] = F_greaterThen; + else if (strcmp(valueStr, ">=") == 0) + parseTree["token"] = F_greaterThenOrEqual; + else if (strcmp(valueStr, "<") == 0) + parseTree["token"] = F_lessThen; + else if (strcmp(valueStr, "<=") == 0) + parseTree["token"] = F_lessThenOrEqual; + else if (strcmp(valueStr, "&&") == 0) + parseTree["token"] = F_and; + else if (strcmp(valueStr, "||") == 0) + parseTree["token"] = F_or; + else + ERROR_ARTI("%s Programming error: token not defined as operator %s %s (%u)\n", spaces+50-depth, key, asChar(value), depth); + + visitedAlready = true; + } + else //if key is node_name + { + uint8_t node = stringToNode(key); + + switch (node) + { + case F_Program: + { + const char * program_name = value["ID"]; + global_scope = new ScopedSymbolTable(program_name, 1, nullptr); //current_scope + + ANDBG_ARTI("%s Program %s %u %u\n", spaces+50-depth, global_scope->scope_name, global_scope->scope_level, global_scope->symbolsIndex); + + if (value["ID"].isNull()) { + ERROR_ARTI("program name null\n"); + errorOccurred = true; + } + if (value["block"].isNull()) { + ERROR_ARTI("%s Program %s: no block in parseTree\n", spaces+50-depth, program_name); + errorOccurred = true; + } + else { + analyze(value["block"], nullptr, global_scope, depth + 1); + } + + #ifdef ARTI_DEBUG + for (uint8_t i=0; isymbolsIndex; i++) { + Symbol* symbol = global_scope->symbols[i]; + ANDBG_ARTI("%s %u %s %s.%s of %u (%u)\n", spaces+50-depth, i, nodeToString(symbol->symbol_type), global_scope->scope_name, symbol->name, symbol->type, symbol->scope_level); + } + #endif + + visitedAlready = true; + break; + } + case F_Function: + { + //find the function name (so we must know this is a function...) + const char * function_name = value["ID"]; + Symbol* function_symbol = new Symbol(node, function_name); + current_scope->insert(function_symbol); + + ANDBG_ARTI("%s Function %s.%s\n", spaces+50-depth, current_scope->scope_name, function_name); + ScopedSymbolTable* function_scope = new ScopedSymbolTable(function_name, current_scope->scope_level + 1, current_scope); + if (current_scope->child_scopesIndex < nrOfChildScope) + current_scope->child_scopes[current_scope->child_scopesIndex++] = function_scope; + else + ERROR_ARTI("ScopedSymbolTable %s childs full (%d)", current_scope->scope_name, nrOfChildScope); + function_symbol->function_scope = function_scope; + + if (value.containsKey("formals")) + analyze(value["formals"], nullptr, function_scope, depth + 1); + + function_scope->nrOfFormals = function_scope->symbolsIndex; + + if (value["block"].isNull()) + ERROR_ARTI("%s Function %s: no block in parseTree\n", spaces+50-depth, function_name); + else + analyze(value["block"], nullptr, function_scope, depth + 1); + + #ifdef ARTI_DEBUG + for (uint8_t i=0; isymbolsIndex; i++) { + Symbol* symbol = function_scope->symbols[i]; + ANDBG_ARTI("%s %u %s %s.%s of %u (%u)\n", spaces+50-depth, i, nodeToString(symbol->symbol_type), function_scope->scope_name, symbol->name, symbol->type, symbol->scope_level); + } + #endif + + visitedAlready = true; + break; + } + case F_VarDef: + case F_Formal: + case F_Assign: + case F_VarRef: + { + JsonObject variable_value; + if (node == F_Assign) + variable_value = value["varref"]; + else + variable_value = value; + + const char * variable_name; + variable_name = variable_value["ID"]; + + if (variable_value.containsKey("indices")) + analyze(variable_value, "indices", current_scope, depth + 1); + + //check if external variable + bool externalFound = false; + uint8_t index = 0; + for (JsonPair externalsPair: definitionJson["EXTERNALS"].as()) { + if (strcmp(variable_name, externalsPair.key().c_str()) == 0) { + variable_value["external"] = index; //add external index to parseTree + ANDBG_ARTI("%s Ext Variable found %s (%u) %s\n", spaces+50-depth, variable_name, depth, key); + externalFound = true; + } + index++; + } + + if (!externalFound) + { + Symbol* var_symbol = current_scope->lookup(variable_name); //lookup here and parent scopes + if (node == F_VarRef) + { + if (var_symbol == nullptr) { + WARNING_ARTI("%s VarRef %s ID not found in scope of %s\n", spaces+50-depth, variable_name, current_scope->scope_name); + //only warning: value 0 in interpreter (div 0 is captured) + } else { + ANDBG_ARTI("%s VarRef found %s.%s (%u)\n", spaces+50-depth, var_symbol->scope->scope_name, variable_name, depth); + } + } + else //assign and var/formal + { + //if variable not already defined, then add + if (node != F_Assign || var_symbol == nullptr) //only assign needs to check if not exists + { + char param_type[charLength]; + if (variable_value.containsKey("type")) + serializeJson(variable_value["type"], param_type); //current_scope.lookup(param.type_node.value); //need string, lookup also used to find types... + else + strcpy(param_type, "notype"); + + var_symbol = new Symbol(node, variable_name, 9); // no type support yet + if (node == F_Assign) + global_scope->insert(var_symbol); // assigned variables are global scope + else + current_scope->insert(var_symbol); + + ANDBG_ARTI("%s %s %s.%s of %s\n", spaces+50-depth, key, var_symbol->scope->scope_name, variable_name, param_type); + } + else if (node != F_Assign && var_symbol != nullptr) + ERROR_ARTI("%s %s Duplicate ID %s.%s\n", spaces+50-depth, key, var_symbol->scope->scope_name, variable_name); + + } + + if (var_symbol != nullptr) + { + variable_value["level"] = var_symbol->scope_level; + variable_value["index"] = var_symbol->scope_index;; + } + } + + if (node == F_Assign) + { + ANDBG_ARTI("%s %s %s = (%u)\n", spaces+50-depth, key, variable_name, depth); + + if (value.containsKey("assignoperator")) + { + JsonObject asop = value["assignoperator"]; + JsonObject::iterator asopBegin = asop.begin(); + ANDBG_ARTI("%s %s\n", spaces+50-depth, asChar(asopBegin->value())); + if (strcmp(asopBegin->value(), "+=") == 0) + value["assignoperator"] = F_plus; + else if (strcmp(asopBegin->value(), "-=") == 0) + value["assignoperator"] = F_minus; + else if (strcmp(asopBegin->value(), "*=") == 0) + value["assignoperator"] = F_multiplication; + else if (strcmp(asopBegin->value(), "/=") == 0) + value["assignoperator"] = F_division; + else if (strcmp(asopBegin->value(), "++") == 0) + value["assignoperator"] = F_plusplus; + else if (strcmp(asopBegin->value(), "--") == 0) + value["assignoperator"] = F_minmin; + } + + if (value.containsKey("expr")) + analyze(value, "expr", current_scope, depth + 1); + else if (value["assignoperator"].as() != F_plusplus && value["assignoperator"].as() != F_minmin) + { + ERROR_ARTI("%s %s %s: Assign without expression\n", spaces+50-depth, key, variable_name); + errorOccurred = true; + } + } + + visitedAlready = true; + break; + } + case F_Call: + { + const char * function_name = value["ID"]; + + //check if external function + bool externalFound = false; + uint8_t index = 0; + for (JsonPair externalsPair: definitionJson["EXTERNALS"].as()) + { + if (strcmp(function_name, externalsPair.key().c_str()) == 0) + { + ANDBG_ARTI("%s Ext Function found %s (%u)\n", spaces+50-depth, function_name, depth); + value["external"] = index; //add external index to parseTree + externalFound = true; + } + index++; + } + + if (!externalFound) + { + Symbol* function_symbol = current_scope->lookup(function_name); //lookup here and parent scopes + if (function_symbol != nullptr) + analyze(function_symbol->block, nullptr, current_scope, depth + 1); + else + ERROR_ARTI("%s Function %s not found in scope of %s\n", spaces+50-depth, function_name, current_scope->scope_name); + } //external functions + + if (value.containsKey("actuals")) + analyze(value["actuals"], nullptr, current_scope, depth + 1); + + visitedAlready = true; + break; + } // case + default: //visitedalready false => recursive call + break; + } //switch + } // is node_name + + if (!visitedAlready && value.size() > 0) // if size == 0 then injected key/value like operator + analyze(value, nullptr, current_scope, depth + 1); + + } // key values + } ///for elements in object + } + else if (parseTree.is()) + { + for (JsonVariant newParseTree: parseTree.as()) + analyze(newParseTree, nullptr, current_scope, depth + 1); + } + else //not array + { + // string element = parseTree; + //for some weird reason this causes a crash on esp32 + // ERROR_ARTI("%s Error: parseTree should be array or object %s (%u)\n", spaces+50-depth, asChar(parseTree), depth); + } + + return !errorOccurred; + } //analyze + + //https://dev.to/lefebvre/compilers-106---optimizer--ig8 + bool optimize(JsonVariant parseTree, uint8_t depth = 0) + { + // DEBUG_ARTI("%s optimize %s (%u)\n", spaces+50-depth, asChar(parseTree), depth); + + if (parseTree.is()) + { + //make the parsetree as small as possible to let the interpreter run as fast as possible: + + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + + bool visitedAlready = false; + + // object: + // if key is node and value is object then optimize object after that if object empty then remove key + // else key is !ID and value is string then remove key + // else key is * and value is array then optimize array after that if array empty then remove key * + + // array + // if element is multiple then remove + + if (strcmp(key, "*") == 0 ) // multiple + { + optimize(value, depth + 1); + + if (value.size() == 0) + { + parseTree.remove(key); + // DEBUG_ARTI("%s optimize: remove empty %s %s (%u)\n", spaces+50-depth, key, asChar(value), depth); + } + + visitedAlready = true; + } + else if (this->definitionJson["TOKENS"].containsKey(key)) // if key is token (moved to parse) + { + visitedAlready = true; + } + else if (value.is()) //if key is node_name + { + optimize(value, depth + 1); + + if (value.size() == 0) + { + // DEBUG_ARTI("%s optimize: remove key %s with empty object (%u)\n", spaces+50-depth, key, depth); + parseTree.remove(key); + } + else if (value.size() == 1) //try to shrink, moved to below + { + //- check if a node is not used in analyzer / interpreter and has only one element: go to the parent and replace itself with its child (shrink) + + // DEBUG_ARTI("%s node try to shrink %s : %s (%u)\n", spaces+50-depth, key, asChar(value), value.size()); + + JsonObject::iterator objectIterator = value.as().begin(); + + if (definitionJson.containsKey(objectIterator->key().c_str())) // if value key is a node + { + // if (objectIterator->value().is() && objectIterator->value().size() == 1) // + // { + // // JsonObject::iterator objectIterator2 = objectIterator->value().as().begin(); + + // // if (objectIterator2->value().is() ) //&& objectIterator.size() == 1 + // { + // DEBUG_ARTI("%s node to shrink %s : %s from %s\n", spaces+50-depth, key, asChar(value), asChar(parseTree)); + // DEBUG_ARTI("%s node to shrink %s : %s\n", spaces+50-depth, objectIterator->key().c_str(), asChar(objectIterator->value())); + // // DEBUG_ARTI("%s node to shrink %s : %s\n", spaces+50-depth, objectIterator2->key().c_str(), asChar(objectIterator2->value())); + // DEBUG_ARTI("%s node to shrink replace %s\n", spaces+50-depth, asChar(parseTree[key])); + // DEBUG_ARTI("%s node to shrink by %s\n", spaces+50-depth, asChar(objectIterator->value())); + // // parseTree[key][objectIterator->key().c_str()] = objectIterator2->value(); + // parseTree[key] = objectIterator->value(); + // } + // // else + // // { + // // DEBUG_ARTI("%s node to shrink2 %s : %s\n", spaces+50-depth, objectIterator2->key().c_str(), asChar(objectIterator2->value())); + // // } + // } + // else + // DEBUG_ARTI("%s value should be an object %s in %s : %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, asChar(objectIterator->value()), asChar(value)); + if (stringToNode(objectIterator->key().c_str()) == F_NoNode) // if key not a node + // if (objectIterator->value().size() == 1) + { + DEBUG_ARTI("%s node to shrink %s in %s : %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, asChar(value), asChar(parseTree)); + // DEBUG_ARTI("%s node to shrink %s in %s = %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, asChar(objectIterator->value()), asChar(parseTree[key])); + parseTree[key] = objectIterator->value(); + + // parseTree[key]["old"] = objectIterator->key(); + + // parseTreePair.key() = objectIterator->key(); + // parseTreePair._key = objectIterator->key(); + // parseTree[objectIterator->key()] = objectIterator->value(); + // parseTree.remove(key); + // parseTree[key][objectIterator2->key().c_str()] = objectIterator2->value(); + } + } + } //shrink + + visitedAlready = true; + } + else + ERROR_ARTI("%s Programming Error: key no node and no token %s %s (%u)\n", spaces+50-depth, key, asChar(value), depth); + + if (!visitedAlready && value.size() > 0) // if size == 0 then injected key/value like operator + optimize(value, depth + 1); + + } ///for elements in object + + //shrink + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + + if (false && value.is() && parseTree.size() == 1 && value.size() == 1 && definitionJson.containsKey(key)) //if key is node_name + { + JsonObject::iterator objectIterator = value.as().begin(); + + // DEBUG_ARTI("%s try replace %s by %s %s\n", spaces+50-depth, key, objectIterator->key().c_str(), asChar(parseTree)); + + if (strcmp(objectIterator->key().c_str(), "ID") != 0) //&& definitionJson.containsKey(objectIterator->key())??? + { + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, asChar(parseTree)); + // DEBUG_ARTI("%s found replace %s by %s %s\n", spaces+50-depth, key, objectIterator->key().c_str(), asChar(parseTree)); + // DEBUG_ARTI("%s found %s\n", spaces+50-depth, asChar(value)); + + // DEBUG_ARTI("%s found %s\n", spaces+50-depth, asChar(parseTree)); + DEBUG_ARTI("%s replace %s by %s %s\n", spaces+50-depth, key, objectIterator->key().c_str(), asChar(parseTree)); + + parseTree.remove(key); + // parseTree[key] = value; + parseTree[objectIterator->key()] = objectIterator->value(); + + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, asChar(parseTree)); + + } + // else + // { + // DEBUG_ARTI("%s not shrinkable %s %s\n", spaces+50-depth, key, asChar(value)); + // if (depth > 12) { + // // parseTree.remove(key); + // char temp[charLength]; + // strcpy(temp, key); + // strcat(temp, "-"); + // // strcat(temp, objectIterator->key().c_str()); + // // parseTree[temp] = value; + // } + // } + + if (false && definitionJson.containsKey(objectIterator->key().c_str())) // if value key is a node + { + if (stringToNode(objectIterator->key().c_str()) == F_NoNode) // if key not a node + // if (objectIterator->value().size() == 1) + { + DEBUG_ARTI("%s node to shrink %s in %s : %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, asChar(value), asChar(parseTree)); + // DEBUG_ARTI("%s node to shrink %s in %s = %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, asChar(objectIterator->value()), asChar(parseTree[key])); + parseTree[key] = objectIterator->value(); + + // parseTree[key]["old"] = objectIterator->key(); + + // parseTreePair.key() = objectIterator->key(); + // parseTreePair._key = objectIterator->key(); + // parseTree[objectIterator->key()] = objectIterator->value(); + // parseTree.remove(key); + // parseTree[key][objectIterator2->key().c_str()] = objectIterator2->value(); + } + } + } //value is jsonObject + + } // for + } + else if (parseTree.is()) + { + JsonArray parseTreeArray = parseTree.as(); + + uint8_t arrayIndex = 0; + for (JsonArray::iterator it = parseTreeArray.begin(); it != parseTreeArray.end(); ++it) + { + JsonVariant element = *it; + + if (element == "multiple") + { + // DEBUG_ARTI("%s optimize: remove array element 'multiple' of array (%u)\n", spaces+50-depth, arrayIndex); + parseTreeArray.remove(it); + } + else if (it->size() == 0) //remove {} elements (added by * arrays, don't know where added) + { + // DEBUG_ARTI("%s optimize: remove array element {} of %s array (%u)\n", spaces+50-depth, element.as().c_str(), arrayIndex); + parseTreeArray.remove(it); + } + else + optimize(*it, depth + 1); + + arrayIndex++; + } + } + else //not array + { + // string element = parseTree; + //for some weird reason this causes a crash on esp32 + ERROR_ARTI("%s Error: parseTree should be array or object %s (%u)\n", spaces+50-depth, asChar(parseTree), depth); + } + + // DEBUG_ARTI("%s optimized %s (%u)\n", spaces+50-depth, asChar(parseTree), depth); + + return !errorOccurred; + } //optimize + + // bool visit_ID(JsonVariant parseTree, const char * treeElement = nullptr, ScopedSymbolTable* current_scope = nullptr, uint8_t depth = 0) + + bool interpret(JsonVariant parseTree, const char * treeElement = nullptr, ScopedSymbolTable* current_scope = nullptr, uint8_t depth = 0) + { + // RUNLOG_ARTI("%s Interpret %s %s (%u)\n", spaces+50-depth, stringOrEmpty(treeElement), asChar(parseTree), depth); + + if (depth >= 50) + { + ERROR_ARTI("Error: Interpret recursion level too deep at %s (%u)\n", asChar(parseTree), depth); + errorOccurred = true; + } + if (errorOccurred) return false; + + if (parseTree.is()) + { + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + if (treeElement == nullptr || strcmp(treeElement, key) == 0) + { + // RUNLOG_ARTI("%s Interpret object element %s %s\n", spaces+50-depth, key, asChar(value)); + + bool visitedAlready = false; + + if (strcmp(key, "*") == 0) + { + // do the recursive call below + } + else if (strcmp(key, "token") == 0 || strcmp(key, "variable") == 0) //variable decls done in analyze (see pas) + visitedAlready = true; + else if (parseTree.containsKey("token")) //key is token + { + // RUNLOG_ARTI("%s Token %s %s %s\n", spaces+50-depth, key, valueStr, asChar(parseTree)); + + const char * valueStr = value; + + switch (parseTree["token"].as()) + { + case F_integerConstant: + case F_realConstant: + valueStack->push(atof(valueStr)); //push value + #if ARTI_PLATFORM != ARTI_ARDUINO //for some weird reason this causes a crash on esp32 + RUNLOG_ARTI("%s %s %s (Push %u)\n", spaces+50-depth, key, valueStr, valueStack->stack_index); + #endif + break; + default: + valueStack->push(parseTree["token"].as()); // push Operator index + #if ARTI_PLATFORM != ARTI_ARDUINO //for some weird reason this causes a crash on esp32 + RUNLOG_ARTI("%s %s %s (Push %u)\n", spaces+50-depth, key, valueStr, valueStack->stack_index); + #endif + } + visitedAlready = true; + } + else //if key is node_name + { + uint8_t node = stringToNode(key); + + // RUNLOG_ARTI("%s Node %s\n", spaces+50-depth, key); + + switch (node) + { + case F_Program: + { + const char * program_name = value["ID"]; + RUNLOG_ARTI("%s program %s\n", spaces+50-depth, program_name); + + ActivationRecord* ar = new ActivationRecord(program_name, "PROGRAM", 1); + + this->callStack->push(ar); + + interpret(value["block"], nullptr, global_scope, depth + 1); + + // do not delete main stack and program ar as used in subsequent calls + // this->callStack->pop(); + // delete ar; ar = nullptr; + + visitedAlready = true; + break; + } + case F_Function: + { + const char * function_name = value["ID"]; + Symbol* function_symbol = current_scope->lookup(function_name); + RUNLOG_ARTI("%s Save block of %s\n", spaces+50-depth, function_name); + if (function_symbol != nullptr) + function_symbol->block = value["block"]; + else + ERROR_ARTI("%s Function %s: not found\n", spaces+50-depth, function_name); + + visitedAlready = true; + break; + } + case F_Call: + { + const char * function_name = value["ID"]; + + //check if external function + if (value.containsKey("external")) { + uint8_t oldIndex = valueStack->stack_index; + + if (value.containsKey("actuals")) + interpret(value["actuals"], nullptr, current_scope, depth + 1); + + float returnValue = floatNull; + + returnValue = arti_external_function(value["external"], valueStack->floatStack[oldIndex] + , (valueStack->stack_index - oldIndex>1)?valueStack->floatStack[oldIndex+1]:floatNull + , (valueStack->stack_index - oldIndex>2)?valueStack->floatStack[oldIndex+2]:floatNull + , (valueStack->stack_index - oldIndex>3)?valueStack->floatStack[oldIndex+3]:floatNull + , (valueStack->stack_index - oldIndex>4)?valueStack->floatStack[oldIndex+4]:floatNull); + + #if ARTI_PLATFORM != ARTI_ARDUINO // because arduino runs the code instead of showing the code + uint8_t lastIndex = oldIndex; + RUNLOG_ARTI("%s Call %s(", spaces+50-depth, function_name); + char sep[3] = ""; + for (int i = oldIndex; i< valueStack->stack_index; i++) { + RUNLOG_ARTI("%s%f", sep, valueStack->floatStack[i]); + strcpy(sep, ", "); + } + if ( returnValue != floatNull) + RUNLOG_ARTI(") = %f (Pop %u, Push %u)\n", returnValue, oldIndex, oldIndex + 1); + else + RUNLOG_ARTI(") (Pop %u)\n", oldIndex); + + #endif + + valueStack->stack_index = oldIndex; + + if (returnValue != floatNull) + valueStack->push(returnValue); + + } + else { //not an external function + Symbol* function_symbol = current_scope->lookup(function_name); + + if (function_symbol != nullptr) //calling undefined function: pre-defined functions e.g. print + { + ActivationRecord* ar = new ActivationRecord(function_name, "Function", function_symbol->scope_level + 1); + + RUNLOG_ARTI("%s %s %s\n", spaces+50-depth, key, function_name); + + uint8_t oldIndex = valueStack->stack_index; + uint8_t lastIndex = valueStack->stack_index; + + if (value.containsKey("actuals")) + interpret(value["actuals"], nullptr, current_scope, depth + 1); + + for (uint8_t i=0; ifunction_scope->nrOfFormals; i++) + { + //determine type, for now assume float + float result = valueStack->floatStack[lastIndex++]; + ar->set(function_symbol->function_scope->symbols[i]->scope_index, result); + RUNLOG_ARTI("%s Actual %s.%s = %f (pop %u)\n", spaces+50-depth, function_name, function_symbol->function_scope->symbols[i]->name, result, valueStack->stack_index); + } + + valueStack->stack_index = oldIndex; + + this->callStack->push(ar); + + interpret(function_symbol->block, nullptr, function_symbol->function_scope, depth + 1); + + this->callStack->pop(); + + delete ar; ar = nullptr; + + //tbd if syntax supports returnvalue + // char callResult[charLength] = "CallResult tbd of "; + // strcat(callResult, function_name); + // valueStack->push(callResult); + + } //function_symbol != nullptr + else { + RUNLOG_ARTI("%s %s not found %s\n", spaces+50-depth, key, function_name); + } + } //external functions + + visitedAlready = true; + break; + } + case F_VarRef: + case F_Assign: //get or set a variable + { + const char * variable_name; + uint8_t variable_level; + uint8_t variable_index; + uint8_t variable_external; + JsonObject variable_indices; + JsonObject variable_value; + + float resultValue = floatNull; + + if (node == F_Assign) + { + variable_value = value["varref"]; + + if (value.containsKey("expr")) //value assignment + { + interpret(value, "expr", current_scope, depth + 1); //value pushed + + resultValue = valueStack->popFloat(); //result of interpret expr (but not for -- and ++ !!!!) + } + } + else + variable_value = value; + + variable_name = variable_value["ID"]; + variable_level = variable_value["level"]; + variable_index = variable_value["index"]; + variable_external = variable_value["external"]; + variable_indices = variable_value["indices"]; + + uint8_t oldIndex = valueStack->stack_index; + + //array indices + char indices[charLength]; //used in RUNLOG_ARTI only + strcpy(indices, ""); + if (!variable_indices.isNull()) + { + strcat(indices, "["); + + interpret(variable_value, "indices", current_scope, depth + 1); //values of indices pushed + + char sep[3] = ""; + for (uint8_t i = oldIndex; i< valueStack->stack_index; i++) { + strcat(indices, sep); + char itoaChar[charLength]; + // itoa(valueStack->floatStack[i], itoaChar, 10); + snprintf(itoaChar, sizeof(itoaChar), "%f", valueStack->floatStack[i]); + strcat(indices, itoaChar); + strcpy(sep, ","); + } + + strcat(indices, "]"); + } + + //check if external variable + if (variable_value.containsKey("external")) //added by Analyze + { + + if (node == F_VarRef) { //get the value + + resultValue = arti_get_external_variable(variable_external, (valueStack->stack_index - oldIndex>0)?valueStack->floatStack[oldIndex]:floatNull, (valueStack->stack_index - oldIndex>1)?valueStack->floatStack[oldIndex+1]:floatNull); + valueStack->stack_index = oldIndex; + + if (resultValue != floatNull) + { + valueStack->push(resultValue); + RUNLOG_ARTI("%s %s ext.%s = %f (push %u)\n", spaces+50-depth, key, variable_name, resultValue, valueStack->stack_index); //key is variable_declaration name is ID + } + else + ERROR_ARTI("%s Error: %s ext.%s no value\n", spaces+50-depth, key, variable_name); + } + else //assign: set the external value... + { + arti_set_external_variable(resultValue, variable_external, (valueStack->stack_index - oldIndex>0)?valueStack->floatStack[oldIndex]:floatNull, (valueStack->stack_index - oldIndex>1)?valueStack->floatStack[oldIndex+1]:floatNull); + valueStack->stack_index = oldIndex; + + RUNLOG_ARTI("%s %s set ext.%s%s = %f (Pop %u)\n", spaces+50-depth, key, variable_name, indices, resultValue, oldIndex); + } + } + else //not external, get er set the variable + { + // Symbol* variable_symbol = current_scope->lookup(variable_name); + ActivationRecord* ar; + + //check already defined in this scope + + // RUNLOG_ARTI("%s levels %u-%u\n", spaces+50-depth, variable_level, variable_index ); + if (variable_level != 0) { //var already exist + //calculate the index in the call stack to find the right ar + uint8_t index = this->callStack->recordsCounter - 1 - (this->callStack->peek()->nesting_level - variable_level); + // RUNLOG_ARTI("%s %s %s.%s = %s (push) %s %d-%d = %d (%d)\n", spaces+50-depth, key, ar->name, variable_name, varValue, variable_symbol->name, this->callStack->peek()->nesting_level,variable_symbol->scope_level, index, this->callStack->recordsCounter); //key is variable_declaration name is ID + ar = this->callStack->records[index]; + } + else //var created here + ar = this->callStack->peek(); + + if (ar != nullptr) // variable found + { + if (node == F_VarRef) //get the value + { + //determine type, for now assume float + float varValue = ar->getFloat(variable_index); + + valueStack->push(varValue); + #if ARTI_PLATFORM != ARTI_ARDUINO //for some weird reason this causes a crash on esp32 + RUNLOG_ARTI("%s %s %s.%s = %f (push %u) %u-%u\n", spaces+50-depth, key, ar->name, variable_name, varValue, valueStack->stack_index, variable_level, variable_index); //key is variable_declaration name is ID + #endif + } + else { //assign: set the value + + if (value.containsKey("assignoperator")) + { + switch (value["assignoperator"].as()) + { + case F_plus: + ar->set(variable_index, ar->getFloat(variable_index) + resultValue); + break; + case F_minus: + ar->set(variable_index, ar->getFloat(variable_index) - resultValue); + break; + case F_multiplication: + ar->set(variable_index, ar->getFloat(variable_index) * resultValue); + break; + case F_division: + { + if (resultValue == 0) // divisor + { + resultValue = 1; + ERROR_ARTI("%s /= division by 0 not possible, divisor ignored for %f\n", spaces+50-depth, ar->getFloat(variable_index)); + } + ar->set(variable_index, ar->getFloat(variable_index) / resultValue); + break; + } + case F_plusplus: + ar->set(variable_index, ar->getFloat(variable_index) + 1); + break; + case F_minmin: + ar->set(variable_index, ar->getFloat(variable_index) - 1); + break; + } + + RUNLOG_ARTI("%s %s.%s%s %s= %f (pop %u) %u-%u\n", spaces+50-depth, ar->name, variable_name, indices, tokenToString(value["assignoperator"]), ar->getFloat(variable_index), valueStack->stack_index, variable_level, variable_index); + } + else + { + ar->set(variable_index, resultValue); + RUNLOG_ARTI("%s %s.%s%s := %f (pop %u) %u-%u\n", spaces+50-depth, ar->name, variable_name, indices, ar->getFloat(variable_index), valueStack->stack_index, variable_level, variable_index); + } + valueStack->stack_index = oldIndex; + } + } //ar != nullptr + else { //unknown variable + ERROR_ARTI("%s %s %s unknown\n", spaces+50-depth, key, variable_name); + valueStack->push(floatNull); + } + } // ! fouund + visitedAlready = true; + break; + } + case F_Expr: + case F_Term: + { + uint8_t oldIndex = valueStack->stack_index; + + // RUNLOG_ARTI("%s before expr term interpret %s %s\n", spaces+50-depth, key, asChar(value)); + interpret(value, nullptr, current_scope, depth + 1); //pushes results + + // RUNLOG_ARTI("%s %s interpret > (%u - %u = %u)\n", spaces+50-depth, key, valueStack->stack_index, oldIndex, valueStack->stack_index - oldIndex); + + // always 3, 5, 7 ... values + if (valueStack->stack_index - oldIndex >= 3) + { + float left = valueStack->floatStack[oldIndex]; + for (int i = 3; i <= valueStack->stack_index - oldIndex; i += 2) + { + uint8_t operatorx = valueStack->floatStack[oldIndex + i - 2]; + float right = valueStack->floatStack[oldIndex + i - 1]; + + float evaluation = 0; + + switch (operatorx) { + case F_plus: + evaluation = left + right; + break; + case F_minus: + evaluation = left - right; + break; + case F_multiplication: + evaluation = left * right; + break; + case F_division: { + if (right == 0) + { + right = 1; + ERROR_ARTI("%s division by 0 not possible, divisor ignored for %f\n", spaces+50-depth, left); + } + evaluation = left / right; + break; + } + case F_modulo: { + if (right == 0) { + evaluation = left; + ERROR_ARTI("%s mod 0 not possible, mod ignored %f\n", spaces+50-depth, left); + } + else + evaluation = fmod(left, right); + break; + } + case F_bitShiftLeft: + evaluation = (int)left << (int)right; //only works on integers + break; + case F_bitShiftRight: + evaluation = (int)left >> (int)right; //only works on integers + break; + case F_equal: + evaluation = left == right; + break; + case F_notEqual: + evaluation = left != right; + break; + case F_lessThen: + evaluation = left < right; + break; + case F_lessThenOrEqual: + evaluation = left <= right; + break; + case F_greaterThen: + evaluation = left > right; + break; + case F_greaterThenOrEqual: + evaluation = left >= right; + break; + case F_and: + evaluation = left && right; + break; + case F_or: + evaluation = left || right; + break; + default: + ERROR_ARTI("%s Programming error: unknown operator %u\n", spaces+50-depth, operatorx); + } + + RUNLOG_ARTI("%s %f %s %f = %f (pop %u, push %u)\n", spaces+50-depth, left, tokenToString(operatorx), right, evaluation, valueStack->stack_index - i, valueStack->stack_index - i + 1); + + left = evaluation; + } + + valueStack->stack_index = oldIndex; + + valueStack->push(left); + } + else if (valueStack->stack_index - oldIndex == 2) // unary: operator and 1 operand + { + uint8_t operatorx = valueStack->floatStack[oldIndex]; + if (operatorx == F_minus) + { + valueStack->stack_index = oldIndex; + valueStack->push(-valueStack->floatStack[oldIndex + 1]); + RUNLOG_ARTI("%s unary - %f (push %u)\n", spaces+50-depth, valueStack->floatStack[oldIndex + 1], valueStack->stack_index ); + } + else { + RUNLOG_ARTI("%s unary operator not supported %u %s\n", spaces+50-depth, operatorx, tokenToString(operatorx)); + } + } + + visitedAlready = true; + break; + } + case F_For: + { + RUNLOG_ARTI("%s For (%u)\n", spaces+50-depth, valueStack->stack_index); + + interpret(value, "assign", current_scope, depth + 1); //creates the assignment + ActivationRecord* ar = this->callStack->peek(); + + bool continuex = true; + uint16_t counter = 0; + while (continuex && counter < 2000) //to avoid endless loops + { + RUNLOG_ARTI("%s iteration\n", spaces+50-depth); + + RUNLOG_ARTI("%s check to condition\n", spaces+50-depth); + interpret(value, "expr", current_scope, depth + 1); //pushes result of to + + float conditionResult = valueStack->popFloat(); + + RUNLOG_ARTI("%s conditionResult (pop %u)\n", spaces+50-depth, valueStack->stack_index); + + if (conditionResult == 1) { //conditionResult is true + RUNLOG_ARTI("%s 1 => run block\n", spaces+50-depth); + interpret(value["block"], nullptr, current_scope, depth + 1); + + RUNLOG_ARTI("%s assign next value\n", spaces+50-depth); + interpret(value["increment"], nullptr, current_scope, depth + 1); //pushes increment result + // MEMORY_ARTI("%s Iteration %u %u\n", spaces+50-depth, counter, FREE_SIZE); + } + else + { + if (conditionResult == 0) { //conditionResult is false + RUNLOG_ARTI("%s 0 => end of For\n", spaces+50-depth); + continuex = false; + } + else // conditionResult is a value (e.g. in pascal) + { + //get the variable from assignment + float varValue = ar->getFloat(ar->lastSetIndex); + + float evaluation = varValue <= conditionResult; + RUNLOG_ARTI("%s %s.(%u) %f <= %f = %f\n", spaces+50-depth, ar->name, ar->lastSetIndex, varValue, conditionResult, evaluation); + + if (evaluation == 1) + { + RUNLOG_ARTI("%s 1 => run block\n", spaces+50-depth); + interpret(value["block"], nullptr, current_scope, depth + 1); + + //increment + ar->set(ar->lastSetIndex, varValue + 1); + } + else + { + RUNLOG_ARTI("%s 0 => end of For\n", spaces+50-depth); + continuex = false; + } + } + } + counter++; + }; + + if (continuex) + ERROR_ARTI("%s too many iterations in for loop %u\n", spaces+50-depth, counter); + + visitedAlready = true; + break; + } // case + case F_If: + { + RUNLOG_ARTI("%s If (stack %u)\n", spaces+50-depth, valueStack->stack_index); + + RUNLOG_ARTI("%s condition\n", spaces+50-depth); + if (value.containsKey("expr")) + interpret(value, "expr", current_scope, depth + 1); + // else if (value.containsKey("varref")) + // interpret(value, "varref", current_scope, depth + 1); + + float conditionResult = valueStack->popFloat(); + + RUNLOG_ARTI("%s (pop %u)\n", spaces+50-depth, valueStack->stack_index); + + if (conditionResult == 1) //conditionResult is true + interpret(value, "block", current_scope, depth + 1); + else + interpret(value, "elseBlock", current_scope, depth + 1); + + visitedAlready = true; + break; + } // case + case F_Cex: + { + RUNLOG_ARTI("%s Cex (stack %u)\n", spaces+50-depth, valueStack->stack_index); + + RUNLOG_ARTI("%s condition\n", spaces+50-depth); + interpret(value, "expr", current_scope, depth + 1); + + float conditionResult = valueStack->popFloat(); + + RUNLOG_ARTI("%s (pop %u)\n", spaces+50-depth, valueStack->stack_index); + + if (conditionResult == 1) //conditionResult is true + interpret(value, "trueExpr", current_scope, depth + 1); + else + interpret(value, "falseExpr", current_scope, depth + 1); + + visitedAlready = true; + break; + } // case + default: //visitedalready false => recursive call + break; + } + } // is key is node_name + + if (!visitedAlready && value.size() > 0) // if size == 0 then injected key/value like operator + interpret(value, nullptr, current_scope, depth + 1); + } // if treeelement + // RUNLOG_ARTI("%s before end for %u\n", spaces+50-depth, depth); + } // for (JsonPair) + } + else if (parseTree.is()) + { + for (JsonVariant newParseTree: parseTree.as()) + { + // RUNLOG_ARTI("%s\n", spaces+50-depth, "Array ", parseTree[i], " "; + interpret(newParseTree, nullptr, current_scope, depth + 1); + } + } + else { //not array + ERROR_ARTI("%s Error: parseTree should be array or object %s (%u)\n", spaces+50-depth, asChar(parseTree), depth); + } + + return !errorOccurred; + } //interpret + + void closeLog() + { + //non arduino stops log here + #if ARTI_PLATFORM == ARTI_ARDUINO + if (logToFile) + { + logFile.close(); + logToFile = false; + } + #else + if (logToFile) + { + fclose(logFile); + logToFile = false; + } + #endif + } + + bool setup(const char *definitionName, const char *programName) + { + errorOccurred = false; + frameCounter = 0; + + // softhack007 check that programName has max 43 chars: fileNameLength -7 ("/" +Name + ".wled\0") + if ((programName == NULL) || (strlen(programName) < 1) || (strlen(programName) > (fileNameLength-7))) { + if (!logFile) logToFile = false; // make sure this message gets to the user + ERROR_ARTI("ARTI-FX: Invalid program name '%s'. Name must be less than %u chars.\n", programName, (unsigned)fileNameLength-7); + return false; + } + + logToFile = true; + //open logFile + if (logToFile) + { + #if ARTI_PLATFORM == ARTI_ARDUINO + strcpy(logFileName, "/"); + #endif + strcat(logFileName, programName); // softhack007 this may overflow logFileName, in case programName has more than 44 chars + strcat(logFileName, ".log"); + + #if ARTI_PLATFORM == ARTI_ARDUINO + logFile = WLED_FS.open(logFileName,"w"); + if (!logFile) { + logToFile = false; + ERROR_ARTI("ARTI-FX: Failed to create logfile '%s'\n", logFileName); + //ERROR_ARTI("ARTI-FX: Failed to create logfile '%s': %s\n", logFileName, strerror(errno)); // unfortunately, errno is not supported on older platforms + } + #else + logFile = fopen (logFileName,"w"); + #endif + } + + MEMORY_ARTI("setup %u bytes free\n", FREE_SIZE); + + if (stages < 1) {close(); return true;} + bool loadParseTreeFile = false; + + #if ARTI_PLATFORM == ARTI_ARDUINO + File definitionFile; + definitionFile = WLED_FS.open(definitionName, "r"); + #else + std::fstream definitionFile; + definitionFile.open(definitionName, std::ios::in); + #endif + + MEMORY_ARTI("open %s %u āœ“\n", definitionName, FREE_SIZE); + + if (!definitionFile) + { + ERROR_ARTI("Definition file %s not found. Press Download wled json\n", definitionName); + return false; + } + + //open definitionFile + #if ARTI_PLATFORM == ARTI_ARDUINO + definitionJsonDoc = new PSRAMDynamicJsonDocument(8192); //currently 5335 + #else + definitionJsonDoc = new PSRAMDynamicJsonDocument(16384); //currently 9521 + #endif + + // mandatory tokens: + // "ID": "ID", + // "INTEGER_CONST": "INTEGER_CONST", + // "REAL_CONST": "REAL_CONST", + + MEMORY_ARTI("definitionTree %u => %u āœ“\n", (unsigned int)definitionJsonDoc->capacity(), FREE_SIZE); //unsigned int needed when running embedded to suppress warnings + + DeserializationError err = deserializeJson(*definitionJsonDoc, definitionFile); + if (err) + { + ERROR_ARTI("deserializeJson() of definition failed with code %s\n", err.c_str()); + return false; + } + definitionFile.close(); + definitionJson = definitionJsonDoc->as(); + + JsonObject::iterator objectIterator = definitionJson.begin(); + JsonObject metaData = objectIterator->value(); + const char * version = metaData["version"]; + if (strcmp(version, "v033") != 0) + { + ERROR_ARTI("Version of definition.json file (%s) should be v033.\nPress Download wled json\n", version); + return false; + } + const char * startNode = metaData["start"]; + if (startNode == nullptr) + { + ERROR_ARTI("Setup Error: No start node found in definition file %s\n", definitionName); + return false; + } + + char programFileName[fileNameLength]; + #if ARTI_PLATFORM == ARTI_ARDUINO + strcpy(programFileName, "/"); + #endif + strcat(programFileName, programName); // softhack007 this may overflow programFileName, in case programName has more than 43 chars + strcat(programFileName, ".wled"); + + #if ARTI_PLATFORM == ARTI_ARDUINO + File programFile; + programFile = WLED_FS.open(programFileName, "r"); + #else + std::fstream programFile; + programFile.open(programFileName, std::ios::in); + #endif + MEMORY_ARTI("open %s %u āœ“\n", programFileName, FREE_SIZE); + if (!programFile) + { + ERROR_ARTI("Program file '%s' not found\n", programFileName); + //ERROR_ARTI("Program file '%s' not found: %s\n", programFileName, strerror(errno)); // errno is not supported on older platforms + return false; + } + + //open programFile + char * programText = nullptr; + uint16_t programFileSize; + #if ARTI_PLATFORM == ARTI_ARDUINO + programFileSize = programFile.size(); + programText = (char *)malloc(programFileSize+1); + programFile.read((byte *)programText, programFileSize); + programText[programFileSize] = '\0'; + #else + programText = (char *)malloc(programTextSize); + programFile.read(programText, programTextSize); + DEBUG_ARTI("programFile size %lu bytes\n", programFile.gcount()); + programText[programFile.gcount()] = '\0'; + programFileSize = strlen(programText); + #endif + programFile.close(); + + #if ARTI_PLATFORM == ARTI_ARDUINO + parseTreeJsonDoc = new PSRAMDynamicJsonDocument(32768); //less memory on arduino: 32 vs 64 bit? + #else + parseTreeJsonDoc = new PSRAMDynamicJsonDocument(65536); + #endif + + MEMORY_ARTI("parseTree %u => %u āœ“\n", (unsigned int)parseTreeJsonDoc->capacity(), FREE_SIZE); + + //parse + + #ifdef ARTI_DEBUG // only read write file if debug is on + char parseTreeName[fileNameLength]; + #if ARTI_PLATFORM == ARTI_ARDUINO + strcpy(parseTreeName, "/"); + #endif + strcat(parseTreeName, programName); + // if (loadParseTreeFile) + // strcpy(parseTreeName, "Gen"); + strcat(parseTreeName, ".json"); + + #if ARTI_PLATFORM == ARTI_ARDUINO + File parseTreeFile; + parseTreeFile = WLED_FS.open(parseTreeName, loadParseTreeFile?"r":"w"); + #else + std::fstream parseTreeFile; + parseTreeFile.open(parseTreeName, loadParseTreeFile?std::ios::in:std::ios::out); + #endif + #endif + + if (stages < 1) { + if (nullptr != programText) free(programText); // softhack007 prevent memory leak + close(); + return true; + } + + if (!loadParseTreeFile) + { + parseTreeJson = parseTreeJsonDoc->as(); + + lexer = new Lexer(programText, definitionJson); + lexer->get_next_token(); + + if (stages < 2) { + //if (nullptr != programText) free(programText); // softhack007 needed to prevent memory leak? lexer has a pointer to programText so its still in use maybe? + close(); + return true; + } + + uint8_t result = parse(parseTreeJson, startNode, '&', lexer->definitionJson[startNode], 0); + + if (this->lexer->pos != strlen(this->lexer->text)) + { + ERROR_ARTI("Node %s Program not entirely parsed (%u,%u) %u of %u\n", startNode, this->lexer->lineno, this->lexer->column, this->lexer->pos, (unsigned int)strlen(this->lexer->text)); + //if (nullptr != programText) free(programText); // softhack007 needed to prevent memory leak? lexer has a pointer to programText so its still in use maybe? + return false; + } + else if (result == ResultFail) + { + ERROR_ARTI("Node %s Program parsing failed (%u,%u) %u of %u\n", startNode, this->lexer->lineno, this->lexer->column, this->lexer->pos, (unsigned int)strlen(this->lexer->text)); + //if (nullptr != programText) free(programText); // softhack007 needed to prevent memory leak? lexer has a pointer to programText so its still in use maybe? + return false; + } + else + { + DEBUG_ARTI("Node %s Parsed until (%u,%u) %u of %u\n", startNode, this->lexer->lineno, this->lexer->column, this->lexer->pos, (unsigned int)strlen(this->lexer->text)); + MEMORY_ARTI("parse %u āœ“\n", FREE_SIZE); + } + + MEMORY_ARTI("definitionTree %u / %u%% (%u %u %u)\n", (unsigned int)definitionJsonDoc->memoryUsage(), 100 * definitionJsonDoc->memoryUsage() / definitionJsonDoc->capacity(), (unsigned int)definitionJsonDoc->size(), definitionJsonDoc->overflowed(), (unsigned int)definitionJsonDoc->nesting()); + MEMORY_ARTI("parseTree %u / %u%% (%u %u %u)\n", (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->size(), parseTreeJsonDoc->overflowed(), (unsigned int)parseTreeJsonDoc->nesting()); + size_t memBefore = parseTreeJsonDoc->memoryUsage(); + parseTreeJsonDoc->garbageCollect(); + MEMORY_ARTI("garbageCollect %u / %u%% -> %u / %u%%\n", (unsigned int)memBefore, 100 * memBefore / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity()); + + delete lexer; lexer = nullptr; + } + else + { + // read parseTree + #ifdef ARTI_DEBUG // only write file if debug is on + DeserializationError err = deserializeJson(*parseTreeJsonDoc, parseTreeFile); + if (err) + { + ERROR_ARTI("deserializeJson() of parseTree failed with code %s\n", err.c_str()); + return false; + } + #endif + } + #if ARTI_PLATFORM == ARTI_ARDUINO //not on windows as cause crash??? + free(programText); + #endif + + if (stages >= 3) + { + DEBUG_ARTI("\nOptimizer\n"); + if (!optimize(parseTreeJson)) + { + ERROR_ARTI("Optimize failed\n"); + return false; + } + else + MEMORY_ARTI("optimize %u āœ“\n", FREE_SIZE); + + size_t memBefore = parseTreeJsonDoc->memoryUsage(); + parseTreeJsonDoc->garbageCollect(); + MEMORY_ARTI("garbageCollect %u / %u%% -> %u / %u%%\n", (unsigned int)memBefore, 100 * memBefore / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity()); + + if (stages >= 4) + { + ANDBG_ARTI("\nAnalyzer\n"); + if (!analyze(parseTreeJson)) + { + ERROR_ARTI("Analyze failed\n"); + errorOccurred = true; + } + else + MEMORY_ARTI("analyze %u āœ“\n", FREE_SIZE); + } + } + + size_t memBefore = parseTreeJsonDoc->memoryUsage(); + parseTreeJsonDoc->garbageCollect(); + MEMORY_ARTI("garbageCollect %u / %u%% -> %u / %u%%\n", (unsigned int)memBefore, 100 * memBefore / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity()); + + #ifdef ARTI_DEBUG // only write parseTree file if debug is on + if (!loadParseTreeFile) + serializeJsonPretty(*parseTreeJsonDoc, parseTreeFile); + parseTreeFile.close(); + #endif + + if (stages < 5 || errorOccurred) {close(); return !errorOccurred;} + + //interpret main + callStack = new CallStack(); + valueStack = new ValueStack(); + + if (global_scope != nullptr) //due to undefined functions??? wip + { + RUNLOG_ARTI("\ninterpret %s %u %u\n", global_scope->scope_name, global_scope->scope_level, global_scope->symbolsIndex); + + if (!interpret(parseTreeJson)) + { + ERROR_ARTI("Interpret main failed\n"); + return false; + } + } + else + { + ERROR_ARTI("\nInterpret global scope is nullptr\n"); + return false; + } + + MEMORY_ARTI("Interpret main %u āœ“\n", FREE_SIZE); + + return !errorOccurred; + } // setup + + void close() { + MEMORY_ARTI("closing Arti %u\n", FREE_SIZE); + + if (callStack != nullptr) {delete callStack; callStack = nullptr;} + if (valueStack != nullptr) {delete valueStack; valueStack = nullptr;} + if (global_scope != nullptr) {delete global_scope; global_scope = nullptr;} + + if (definitionJsonDoc != nullptr) { + MEMORY_ARTI("definitionJson %u / %u%% (%u %u %u)\n", (unsigned int)definitionJsonDoc->memoryUsage(), 100 * definitionJsonDoc->memoryUsage() / definitionJsonDoc->capacity(), (unsigned int)definitionJsonDoc->size(), definitionJsonDoc->overflowed(), (unsigned int)definitionJsonDoc->nesting()); + delete definitionJsonDoc; definitionJsonDoc = nullptr; + } + + if (parseTreeJsonDoc != nullptr) { + MEMORY_ARTI("parseTree %u / %u%% (%u %u %u)\n", (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->size(), parseTreeJsonDoc->overflowed(), (unsigned int)parseTreeJsonDoc->nesting()); + delete parseTreeJsonDoc; parseTreeJsonDoc = nullptr; + } + + MEMORY_ARTI("closed Arti %u āœ“\n", FREE_SIZE); + + closeLog(); + + #if ARTI_PLATFORM == ARTI_ARDUINO + WLED_FS.remove(logFileName); //cleanup the /edit folder a bit + #endif + } +}; //ARTI \ No newline at end of file diff --git a/usermods/artifx/arti_wled.h b/usermods/artifx/arti_wled.h new file mode 100644 index 0000000000..96e4627010 --- /dev/null +++ b/usermods/artifx/arti_wled.h @@ -0,0 +1,592 @@ +/* + @title Arduino Real Time Interpreter (ARTI) + @file arti_wled.h + @date 20220818 + @author Ewoud Wijma + @Copyright (c) 2024 Ewoud Wijma + @repo https://github.com/ewoudwijma/ARTI + */ + +#pragma once + +// For testing purposes, definitions should not only run on Arduino but also on Windows etc. +// Because compiling on arduino takes seriously more time than on Windows. +// The plugin.h files replace native arduino calls by windows simulated calls (e.g. setPixelColor will become printf) + +#define ARTI_ARDUINO 1 +#define ARTI_EMBEDDED 2 +// #ifdef ESP32 //ESP32 is set in wled context: small trick to set WLED context +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) //ESP32 is set in wled context: small trick to set WLED context + #define ARTI_PLATFORM ARTI_ARDUINO // else on Windows/Linux/Mac... +#endif + +#if ARTI_PLATFORM == ARTI_ARDUINO + #include "arti.h" +#else + #include "../arti.h" + #include + #include + #include +#endif + +//make sure the numbers here correspond to the order in which these functions are defined in wled000.json!! +enum Externals +{ + F_ledCount, + F_width, + F_height, + F_setPixelColor, + F_leds, + F_hsv, + F_rgbw, + + F_setRange, + F_fill, + F_colorBlend, + F_colorWheel, + F_colorFromPalette, + F_beatSin, + F_fadeToBlackBy, + F_iNoise, + F_fadeOut, + + F_counter, + F_segcolor, + F_speedSlider, + F_intensitySlider, + F_custom1Slider, + F_custom2Slider, + F_custom3Slider, + F_volume, + F_fftResult, + + F_shift, + F_circle2D, + F_drawLine, + F_drawArc, + + F_constrain, + F_map, + F_seed, + F_random, + F_sin, + F_cos, + F_abs, + F_min, + F_max, + F_floor, + + F_hour, + F_minute, + F_second, + F_millis, + + F_time, + F_triangle, + F_wave, + F_square, + F_clamp, + + F_print, + F_jsonToPixels, //reorder only when creating new wledvxyz.json + F_frameTime, + F_soundPressure +}; + +#if ARTI_PLATFORM != ARTI_ARDUINO + #define PI 3.141592654 +#endif +uint32_t frameTime = 0; + +float ARTI::arti_external_function(uint8_t function, float par1, float par2, float par3, float par4, float par5) +{ + // MEMORY_ARTI("fun %d(%f, %f, %f)\n", function, par1, par2, par3); + #if ARTI_PLATFORM == ARTI_ARDUINO + switch (function) { + case F_setPixelColor: { + if (par3 == floatNull) + SEGMENT.setPixelColor(((uint16_t)par1)%SEGLEN, (uint32_t)par2); + else + SEGMENT.setPixelColorXY((uint16_t)par1, (uint16_t)par2, (uint32_t)par3); + return floatNull; + } + case F_hsv: + { + CRGB color = CHSV((uint8_t)par1, (uint8_t)par2, (uint8_t)par3); + return RGBW32(color.r, color.g, color.b, 0); + } + case F_rgbw: + return RGBW32((uint8_t)par1, (uint8_t)par2, (uint8_t)par3, (uint8_t)par4); + + case F_setRange: { + strip.setRange((uint16_t)par1, (uint16_t)par2, (uint32_t)par3); + return floatNull; + } + case F_fill: { + SEGMENT.fill((uint32_t)par1); + return floatNull; + } + case F_colorBlend: + return color_blend((uint32_t)par1, (uint32_t)par2, (uint16_t)par3); + case F_colorWheel: + return SEGMENT.color_wheel((uint8_t)par1); + case F_colorFromPalette: + { + CRGB color; + if (par2 == floatNull) + color = ColorFromPalette(SEGPALETTE, (uint8_t)par1); + else + color = ColorFromPalette(SEGPALETTE, (uint8_t)par1, (uint8_t)par2); //brightness + return RGBW32(color.r, color.g, color.b, 0); + } + case F_beatSin: + return beatsin8((uint8_t)par1, (uint8_t)par2, (uint8_t)par3, (uint8_t)par4, (uint8_t)par5); + case F_fadeToBlackBy: + SEGMENT.fadeToBlackBy((uint8_t)par1); + return floatNull; + case F_iNoise: + return inoise16((uint32_t)par1, (uint32_t)par2); + case F_fadeOut: + SEGMENT.fade_out((uint8_t)par1); + return floatNull; + + case F_segcolor: + return SEGCOLOR((uint8_t)par1); + + case F_fftResult: + { + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + return fftResult[(uint8_t)par1%16]; + } + + case F_shift: { + uint32_t saveFirstPixel = SEGMENT.getPixelColor(0); + for (uint16_t i=0; i3.5->4) + int x = round(round((sin(radians(par1)) * halfLength + halfLength) * 10)/10) + deltaWidth; + int y = round(round((halfLength - cos(radians(par1)) * halfLength) * 10)/10) + deltaHeight; + return SEGMENT.XY(x,y); + } + case F_drawLine: + SEGMENT.drawLine(par1, par2, par3, par4, par5); + return floatNull; + case F_drawArc: + #ifndef WLED_DISABLE_2D + if (par5 == floatNull) + SEGMENT.drawArc(par1, par2, par3, par4); + else + SEGMENT.drawArc(par1, par2, par3, par4, par5); //fillColor + #endif + return floatNull; + case F_constrain: + return constrain(par1, par2, par3); + case F_map: + return map(par1, par2, par3, par4, par5); + case F_seed: + random16_set_seed((uint16_t)par1); + return floatNull; + case F_random: + return random16(); + + case F_millis: + return millis(); + +#ifndef WLED_DISABLE_2D + case F_jsonToPixels: + SEGMENT.jsonToPixels(SEGMENT.name,(uint8_t)par1); + return floatNull; +#endif + + default: {} + } + #else // not arduino + switch (function) + { + case F_setPixelColor: + PRINT_ARTI("%s(%f, %f, %f)\n", "setPixelColor", par1, par2, par3); + return floatNull; + case F_hsv: + PRINT_ARTI("%s(%f, %f, %f)\n", "hsv", par1, par2, par3); + return par1 + par2 + par3; + case F_rgbw: + PRINT_ARTI("%s(%f, %f, %f, %f)\n", "rgbw", par1, par2, par3, par4); + return par1 + par2 + par3 + par4; + + case F_setRange: + return par1 + par2 + par3; + case F_fill: + PRINT_ARTI("%s(%f)\n", "fill", par1); + return floatNull; + case F_colorBlend: + return par1 + par2 + par3; + case F_colorWheel: + return par1; + case F_colorFromPalette: + return par1 + par2; + case F_beatSin: + return par1+par2+par3+par4+par5; + case F_fadeToBlackBy: + return par1; + case F_iNoise: + return par1 + par2; + case F_fadeOut: + return par1; + + case F_segcolor: + return par1; + + case F_fftResult: + return par1; + + case F_shift: + PRINT_ARTI("%s(%f)\n", "shift", par1); + return floatNull; + case F_circle2D: + PRINT_ARTI("%s(%f)\n", "circle2D", par1); + return par1 / 2; + case F_drawLine: + return par1 + par2 + par3 + par4 + par5; + case F_drawArc: + return par1 + par2 + par3 + par4 + par5; + + case F_constrain: + return par1 + par2 + par3; + case F_map: + return par1 + par2 + par3 + par4 + par5; + case F_seed: + PRINT_ARTI("%s(%f)\n", "seed", par1); + return floatNull; + case F_random: + return rand(); + + case F_millis: + return 1000; + + case F_jsonToPixels: + return par1; + } + #endif + + //same on Arduino or Windows + switch (function) + { + case F_sin: + return sin(par1); + case F_cos: + return cos(par1); + case F_abs: + return fabs(par1); + case F_min: + return fmin(par1, par2); + case F_max: + return fmax(par1, par2); + case F_floor: + return floorf(par1); + + // Reference: https://github.com/atuline/PixelBlaze + case F_time: // A sawtooth waveform between 0.0 and 1.0 that loops about every 65.536*interval seconds. e.g. use .015 for an approximately 1 second. + { + float myVal = millis(); + myVal = myVal / 65535 / par1; // PixelBlaze uses 1000/65535 = .015259. + myVal = fmod(myVal, 1.0); // ewowi: with 0.015 as input, you get fmod(millis/1000,1.0), which has a period of 1 second, sounds right + return myVal; + } + case F_triangle: // Converts a sawtooth waveform v between 0.0 and 1.0 to a triangle waveform between 0.0 to 1.0. v "wraps" between 0.0 and 1.0. + return 1.0 - fabs(fmod(2 * par1, 2.0) - 1.0); + case F_wave: // Converts a sawtooth waveform v between 0.0 and 1.0 to a sinusoidal waveform between 0.0 to 1.0. Same as (1+sin(v*PI2))/2 but faster. v "wraps" between 0.0 and 1.0. + return (1 + sin(par1 * 2 * PI)) / 2; + case F_square: // Converts a sawtooth waveform v to a square wave using the provided duty cycle where duty is a number between 0.0 and 1.0. v "wraps" between 0.0 and 1.0. + { + float sinValue = arti_external_function(F_wave, par1); + return sinValue >= par2 ? 1 : 0; + } + case F_clamp: + { + const float t = par1 < par2 ? par2 : par1; + return t > par3 ? par3 : t; + } + + case F_print: { + if (par3 == floatNull) { + if (par2 == floatNull) { + PRINT_ARTI("%f\n", par1); + } + else + PRINT_ARTI("%f, %f\n", par1, par2); + } + else + PRINT_ARTI("%f, %f, %f\n", par1, par2, par3); + return floatNull; + } + } + + ERROR_ARTI("Error: arti_external_function: %u not implemented\n", function); + errorOccurred = true; + return function; +} + +float ARTI::arti_get_external_variable(uint8_t variable, float par1, float par2, float par3) +{ + // MEMORY_ARTI("get %d(%f, %f, %f)\n", variable, par1, par2, par3); + #if ARTI_PLATFORM == ARTI_ARDUINO + switch (variable) + { + case F_ledCount: + return SEGLEN; + case F_width: + return SEGMENT.virtualWidth(); + case F_height: + return SEGMENT.virtualHeight(); + case F_leds: + if (par1 == floatNull) { + ERROR_ARTI("arti_get_external_variable leds without indices not supported yet (get leds)\n"); + errorOccurred = true; + return floatNull; + } + else if (par2 == floatNull) + return SEGMENT.getPixelColor((uint16_t)par1); + else + return SEGMENT.getPixelColorXY((uint16_t)par1, (uint16_t)par2); //2D value!! + + case F_counter: + return SEGENV.call; + case F_speedSlider: + return SEGMENT.speed; + case F_intensitySlider: + return SEGMENT.intensity; + case F_custom1Slider: + return SEGMENT.custom1; + case F_custom2Slider: + return SEGMENT.custom2; + case F_custom3Slider: + return SEGMENT.custom3; + case F_volume: + case F_soundPressure: + { + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + if (variable == F_volume) + return *(float*) um_data->u_data[0]; //volumeSmth + else + return *(float*) um_data->u_data[9]; //soundPressure + } + case F_hour: + return ((float)hour(localTime)); + case F_minute: + return ((float)minute(localTime)); + case F_second: + return ((float)second(localTime)); + } + #else + switch (variable) + { + case F_ledCount: + return 3; // used in testing e.g. for i = 1 to ledCount + case F_width: + return 2; + case F_height: + return 4; + case F_leds: + if (par1 == floatNull) { + ERROR_ARTI("arti_get_external_variable leds without indices not supported yet (get leds)\n"); + errorOccurred = true; + return F_leds; + } + else if (par2 == floatNull) + return par1; + else + return par1 * par2; //2D value!! + + case F_counter: + return frameCounter; + case F_speedSlider: + return F_speedSlider; + case F_intensitySlider: + return F_intensitySlider; + case F_custom1Slider: + return F_custom1Slider; + case F_custom2Slider: + return F_custom2Slider; + case F_custom3Slider: + return F_custom3Slider; + case F_volume: + return F_volume; + case F_soundPressure: + return F_soundPressure; + + case F_hour: + return F_hour; + case F_minute: + return F_minute; + case F_second: + return F_second; + } + #endif + + ERROR_ARTI("Error: arti_get_external_variable: %u not implemented\n", variable); + errorOccurred = true; + return variable; +} + +void ARTI::arti_set_external_variable(float value, uint8_t variable, float par1, float par2, float par3) +{ + #if ARTI_PLATFORM == ARTI_ARDUINO + // MEMORY_ARTI("%s %s %u %u (%u)\n", spaces+50-depth, variable_name, par1, par2, FREE_SIZE); + switch (variable) + { + case F_leds: + if (par1 == floatNull) + { + ERROR_ARTI("arti_set_external_variable leds without indices not supported yet (set leds to %f)\n", value); + errorOccurred = true; + } + else if (par2 == floatNull) + SEGMENT.setPixelColor((uint16_t)par1%SEGLEN, value); + else + SEGMENT.setPixelColorXY((uint16_t)par1%SEGMENT.virtualWidth(), (uint16_t)par2%SEGMENT.virtualHeight(), value); //2D value!! + + return; + case F_frameTime: + frameTime = (uint16_t)value; + return; + } + #else + switch (variable) + { + case F_leds: + if (par1 == floatNull) + { + ERROR_ARTI("arti_set_external_variable leds without indices not supported yet (set leds to %f)\n", value); + errorOccurred = true; + } + else if (par2 == floatNull) + RUNLOG_ARTI("arti_set_external_variable: leds(%f) := %f\n", par1, value); + else + RUNLOG_ARTI("arti_set_external_variable: leds(%f, %f) := %f\n", par1, par2, value); + + return; + case F_frameTime: + frameTime = (uint16_t)value; + return; + } + #endif + + ERROR_ARTI("Error: arti_set_external_variable: %u not implemented\n", variable); + errorOccurred = true; +} + +bool ARTI::loop() +{ + if (stages < 5) {close(); return true;} + + if (parseTreeJsonDoc == nullptr || parseTreeJsonDoc->isNull()) + { + ERROR_ARTI("Loop: No parsetree created\n"); + errorOccurred = true; + return false; + } + else + { + uint8_t depth = 8; + + bool foundRenderFunction = false; + + const char * function_name = "renderFrame"; + Symbol* function_symbol = global_scope->lookup(function_name); + + if (function_symbol != nullptr) { //calling undefined function: pre-defined functions e.g. print + + foundRenderFunction = true; + + ActivationRecord* ar = new ActivationRecord(function_name, "Function", function_symbol->scope_level + 1); + + RUNLOG_ARTI("%s %s %s (%u)\n", spaces+50-depth, "Call", function_name, this->callStack->recordsCounter); + + this->callStack->push(ar); + + if (!interpret(function_symbol->block, nullptr, global_scope, depth + 1)) + return false; + + this->callStack->pop(); + + delete ar; ar = nullptr; + + } //function_symbol != nullptr + + function_name = "renderLed"; + function_symbol = global_scope->lookup(function_name); + + if (function_symbol != nullptr) { //calling undefined function: pre-defined functions e.g. print + + foundRenderFunction = true; + + ActivationRecord* ar = new ActivationRecord(function_name, "function", function_symbol->scope_level + 1); + + for (int i = 0; i< arti_get_external_variable(F_ledCount); i++) + { + if (function_symbol->function_scope->nrOfFormals == 2) {// 2D + ar->set(function_symbol->function_scope->symbols[0]->scope_index, i%Segment::maxWidth); // set x + ar->set(function_symbol->function_scope->symbols[1]->scope_index, i/Segment::maxWidth); // set y + } + else + ar->set(function_symbol->function_scope->symbols[0]->scope_index, i); // set x + + this->callStack->push(ar); + + if (!interpret(function_symbol->block, nullptr, global_scope, depth + 1)) + return false; + + this->callStack->pop(); + } + + delete ar; ar = nullptr; + + } + + if (!foundRenderFunction) + { + ERROR_ARTI("%s renderFrame or renderLed not found\n", spaces+50-depth); + errorOccurred = true; + return false; + } + } + frameCounter++; + + if (frameCounter == 1) + startMillis = millis(); + + if (millis() - startMillis > 3000) //startMillis != 0 && logToFile && + { + // ERROR_ARTI("time %u\n", millis() - startMillis); + closeLog(); + // startMillis = 0; + } + + return true; +} // loop \ No newline at end of file diff --git a/usermods/artifx/artifx.css b/usermods/artifx/artifx.css new file mode 100644 index 0000000000..39ef39676b --- /dev/null +++ b/usermods/artifx/artifx.css @@ -0,0 +1,12 @@ +.ceTextarea { + width: 90%; + height: 300px; + resize: none; + white-space: pre; +} + +#kceEditor { + max-width: 490px; + display: inline-block; +} + diff --git a/usermods/artifx/artifx.js b/usermods/artifx/artifx.js new file mode 100644 index 0000000000..f23864487a --- /dev/null +++ b/usermods/artifx/artifx.js @@ -0,0 +1,156 @@ + +var isCEEditor = false; + +function toggleCEEditor(name, segID) { + if (isInfo) toggleInfo(); + if (isNodes) toggleNodes(); + isCEEditor = !isCEEditor; + if (isCEEditor) populateCEEditor(name, segID); + d.getElementById('ceEditor').style.transform = (isCEEditor) ? "translateY(0px)":"translateY(100%)"; +} + +function loadLogFile(name, attempt) { + var ceLogArea = d.getElementById("ceLogArea"); + fetchAndExecute((loc?`http://${locip}`:'.') + "/", name, null, function(parms,logtext) + { + if (logtext == "") { + if (attempt < 10) { + ceLogArea.value = ("...........").substring(0, attempt + 1); + setTimeout(() => + { + loadLogFile(name, attempt + 1); + }, 1000); + } + else + ceLogArea.value = "log not found after 10 seconds"; + } + else + ceLogArea.value = logtext; + }, function(parms,error){ + showToast(error); + console.log(error); + }); +} + +function uploadFileWithText(name, text) +{ + var req = new XMLHttpRequest(); + req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)}); + req.addEventListener('error', function(e){showToast(e.stack,true);}); + req.open("POST", "/upload"); + var formData = new FormData(); + + var blob = new Blob([text], {type : 'application/text'}); + var fileOfBlob = new File([blob], name); + formData.append("upload", fileOfBlob); + + req.send(formData); +} + +function saveCE(name, segID) { + showToast("Saving " + name + ".wled"); + + var ceProgramArea = d.getElementById("ceProgramArea"); + + uploadFileWithText("/" + name + ".wled", ceProgramArea.value); + + var obj = {"seg": {"id": segID, "reset": true}}; + requestJson(obj); + + var ceLogArea = d.getElementById("ceLogArea"); + ceLogArea.value = "."; + setTimeout(() => + { + loadLogFile(name + ".log", 1); + }, 1000); +} + +function populateCEEditor(name, segID) +{ + fetchAndExecute((loc?`http://${locip}`:'.') + "/", name + ".wled", null, function(parms,text) + { + var cn=`ARTI-FX Editor
+ ${name}.wled
+
+ +
+ +
+ +
+ +
+
Compile and Run Log
+
+ Run log > 3 seconds is send to Serial Output.
+ 🄚 + 🄚 + 🄚`; + + d.getElementById('kceEditor').innerHTML = cn; + + var ceLogArea = d.getElementById("ceLogArea"); + ceLogArea.value = "."; + loadLogFile(name + ".log", 1); + + }, function(parms,error){ + showToast(error); + console.log(error); + }); +} + +function downloadGHFile(url, name, save=false, warn=false) { //Githubfile + if (url == "CE") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/ARTIFX/wled/"; + if (url == "HBB") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/Presets/HB_PresetPack210808_32x32_16seg/Base%20pack/"; + if (url == "HBE") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/Presets/HB_PresetPack210808_32x32_16seg/Effects%20pack/"; + if (url == "LM") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/Ledmaps/"; + + fetchAndExecute(url, name, null, function(parms,text) { + if (save) { + if (warn && !confirm('Are you sure to download/overwrite ' + name + '?')) + return; + uploadFileWithText("/" + name, text); + } + else + { + var ceProgramArea = d.getElementById("ceProgramArea"); + ceProgramArea.value = text; + } + }, function(parms,error){ + showToast(error); + console.log(url + name,error); + }); + + return; + + var request = new XMLHttpRequest(); + request.onload = function() { + if (name == "wledv033.json" || name == "presets.json") { + if (!confirm('Are you sure to download ' + name + '?')) + return; + uploadFileWithText("/" + name, request.response); + } + else + { + var ceProgramArea = d.getElementById("ceProgramArea"); + ceProgramArea.value = request.response; + } + } + request.open("GET", url); + request.send(); + } + +function loadCETemplate(name) { + var ceProgramArea = d.getElementById("ceProgramArea"); + ceProgramArea.value = `/* + ARTIFX Template + */ + program ${name} + { + function renderFrame() + { + setPixelColor(counter, colorFromPalette(counter, counter)) + } + }`; + +} \ No newline at end of file diff --git a/usermods/artifx/usermod_v2_artifx.h b/usermods/artifx/usermod_v2_artifx.h new file mode 100644 index 0000000000..1a91208138 --- /dev/null +++ b/usermods/artifx/usermod_v2_artifx.h @@ -0,0 +1,203 @@ +/* + @title Usermod ARTIFX (AF) + @file usermod_v2_artifx.h + @date 20220818 + @author Ewoud Wijma + @Copyright (c) 2024 Ewoud Wijma + @repo https://github.com/ewoudwijma/ARTI + */ + +#pragma once + +#include "wled.h" + +#include "arti_wled.h" + +//declare global variables +ARTI * arti; + +//effect function +uint16_t mode_ARTIFX(void) { + //tbd: move statics to SEGMENT.data + static bool successful; + static bool notEnoughHeap; + + static char previousEffect[charLength]; + if (SEGENV.call == 0) { + strcpy(previousEffect, ""); //force init + SEGMENT.fill(BLACK); //in case not all leds used e.g. when using expand 1d Circle. Tbd: fill black should never be used to allow for blends/transitions + } + + char currentEffect[charLength]; + strncpy(currentEffect, (SEGMENT.name != nullptr)?SEGMENT.name:"default", sizeof(currentEffect)-1); //note: switching preset with segment name to preset without does not clear the SEGMENT.name variable, but not gonna solve here ;-) + + if (strcmp(previousEffect, currentEffect) != 0) + { + strcpy(previousEffect, currentEffect); + + // if (artiWrapper != nullptr && artiWrapper->arti != nullptr) { + if (arti != nullptr) + { + arti->close(); + delete arti; arti = nullptr; + } + + // if (!SEGENV.allocateData(sizeof(ArtiWrapper))) return mode_static(); // We use this method for allocating memory for static variables. + // artiWrapper = reinterpret_cast(SEGENV.data); + arti = new ARTI(); + + successful = arti->setup("/wledv033.json", currentEffect); + + if (!successful) + ERROR_ARTI("Setup not successful\n"); + } + else + { + if (successful) // && SEGENV.call < 250 for each frame + { + if (FREE_SIZE <= 20000) + { + ERROR_ARTI("Not enough free heap (%u <= 30000)\n", FREE_SIZE); + notEnoughHeap = true; + successful = false; + } + else + { + // static int previousMillis; + // static int previousCall; + // if (millis() - previousMillis > 5000) { //tried SEGENV.aux0 but that looks to be overwritten!!! (dangling pointer???) + // previousMillis = millis(); + // MEMORY_ARTI("Heap renderFrame %u %u fps\n", FREE_SIZE, (SEGENV.call - previousCall)/5); + // previousCall = SEGENV.call; + // } + + successful = arti->loop(); + } + } + else + { + arti->closeLog(); + if (notEnoughHeap && FREE_SIZE > 20000) { + ERROR_ARTI("Again enough free heap, restart effect (%u > 30000)\n", FREE_SIZE); + successful = true; + notEnoughHeap = false; + strcpy(previousEffect, ""); // force new create + } + else { + //mode_static + SEGMENT.fill(SEGCOLOR(0)); + return 350; + } + } + } + + return MAX(frameTime,FRAMETIME); +} + +static const char _data_FX_MODE_ARTIFX[] PROGMEM = "āš™ļø ARTI-FX ☾@Speed,Intensity,Custom 1, Custom 2, Custom 3;!;!;1;mp12=0"; + +class ARTIFXUserMod : public Usermod { + private: + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; //usermod name + + char errorMessage[100] = ""; + + bool enabled = false; + bool initDone = false; + + public: + + void setup() { + if (!initDone) + strip.addEffect(FX_MODE_ARTIFX, &mode_ARTIFX, _data_FX_MODE_ARTIFX); + initDone = true; + enabled = true; + } + + void connected() { + } + + void loop() { + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject& root) + { + } + + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) + { + //root["user0"] = userVar0; + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod["on"] = enabled; + } + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) + { + // userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); + } + + + void addToConfig(JsonObject& root) + { + } + + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + + + // * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + return configComplete; + } + + void appendConfigData() + { + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + } + + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_ARTIFX; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char ARTIFXUserMod::_name[] PROGMEM = "ARTIFX"; diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 09cc931c9f..0be1bd7974 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -1,14 +1,27 @@ #pragma once +/* + @title MoonModules WLED - audioreactive usermod + @file audio_reactive.h + @repo https://github.com/MoonModules/WLED, submit changes to this file as PRs to MoonModules/WLED + @Authors https://github.com/MoonModules/WLED/commits/mdev/ + @Copyright Ā© 2024 Github MoonModules Commit Authors (contact moonmodules@icloud.com for details) + @license Licensed under the EUPL-1.2 or later + +*/ + + #include "wled.h" + +#ifdef ARDUINO_ARCH_ESP32 + #include #include -#ifndef ARDUINO_ARCH_ESP32 - #error This audio reactive usermod does not support the ESP8266. +#include #endif -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) #include #endif @@ -20,34 +33,164 @@ * .... */ + +#if defined(WLEDMM_FASTPATH) && defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) +#define FFT_USE_SLIDING_WINDOW // perform FFT with sliding window = 50% overlap +#endif + + +#define FFT_PREFER_EXACT_PEAKS // use different FFT windowing -> results in "sharper" peaks and less "leaking" into other frequencies +//#define SR_STATS + +#if !defined(FFTTASK_PRIORITY) +#if defined(WLEDMM_FASTPATH) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(ARDUINO_ARCH_ESP32) +// FASTPATH: use higher priority, to avoid that webserver (ws, json, etc) delays sample processing +//#define FFTTASK_PRIORITY 3 // competing with async_tcp +#define FFTTASK_PRIORITY 4 // above async_tcp +#else +#define FFTTASK_PRIORITY 1 // standard: looptask prio +//#define FFTTASK_PRIORITY 2 // above looptask, below async_tcp +#endif +#endif + +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +// this applies "pink noise scaling" to FFT results before computing the major peak for effects. +// currently only for ESP32-S3 and classic ESP32, due to increased runtime +#define FFT_MAJORPEAK_HUMAN_EAR +#endif + +// high-resolution type for input filters +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#define SR_HIRES_TYPE double // ESP32 and ESP32-S3 (with FPU) are fast enough to use "double" +#else +#define SR_HIRES_TYPE float // prefer faster type on slower boards (-S2, -C3) +#endif + // Comment/Uncomment to toggle usb serial debugging // #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter) // #define FFT_SAMPLING_LOG // FFT result debugging // #define SR_DEBUG // generic SR DEBUG messages #ifdef SR_DEBUG - #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) - #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUGSR_PRINT(x) DEBUGOUT(x) + #define DEBUGSR_PRINTLN(x) DEBUGOUTLN(x) + #define DEBUGSR_PRINTF(x...) DEBUGOUTF(x) #else #define DEBUGSR_PRINT(x) #define DEBUGSR_PRINTLN(x) #define DEBUGSR_PRINTF(x...) #endif +#if defined(SR_DEBUG) +#define ERRORSR_PRINT(x) DEBUGSR_PRINT(x) +#define ERRORSR_PRINTLN(x) DEBUGSR_PRINTLN(x) +#define ERRORSR_PRINTF(x...) DEBUGSR_PRINTF(x) +#else +#if defined(WLED_DEBUG) +#define ERRORSR_PRINT(x) DEBUG_PRINT(x) +#define ERRORSR_PRINTLN(x) DEBUG_PRINTLN(x) +#define ERRORSR_PRINTF(x...) DEBUG_PRINTF(x) +#else + #define ERRORSR_PRINT(x) + #define ERRORSR_PRINTLN(x) + #define ERRORSR_PRINTF(x...) +#endif +#endif + #if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) - #define PLOT_PRINT(x) DEBUGOUT.print(x) - #define PLOT_PRINTLN(x) DEBUGOUT.println(x) - #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) + #define PLOT_PRINT(x) DEBUGOUT(x) + #define PLOT_PRINTLN(x) DEBUGOUTLN(x) + #define PLOT_PRINTF(x...) DEBUGOUTF(x) + #define PLOT_FLUSH() DEBUGOUTFlush() #else #define PLOT_PRINT(x) #define PLOT_PRINTLN(x) #define PLOT_PRINTF(x...) + #define PLOT_FLUSH() +#endif + +// sanity checks +#ifdef ARDUINO_ARCH_ESP32 + // we need more space in for oappend() stack buffer -> SETTINGS_STACK_BUF_SIZE and CONFIG_ASYNC_TCP_TASK_STACK_SIZE + #if SETTINGS_STACK_BUF_SIZE < 3904 // 3904 is required for WLEDMM-0.14.0-b28 + #warning please increase SETTINGS_STACK_BUF_SIZE >= 3904 + #endif + #if (CONFIG_ASYNC_TCP_TASK_STACK_SIZE - SETTINGS_STACK_BUF_SIZE) < 4352 // at least 4096+256 words of free task stack is needed by async_tcp alone + #error remaining async_tcp stack will be too low - please increase CONFIG_ASYNC_TCP_TASK_STACK_SIZE + #endif +#endif + +// audiosync constants +#define AUDIOSYNC_NONE 0x00 // UDP sound sync off +#define AUDIOSYNC_SEND 0x01 // UDP sound sync - send mode +#define AUDIOSYNC_REC 0x02 // UDP sound sync - receiver mode +#define AUDIOSYNC_REC_PLUS 0x06 // UDP sound sync - receiver + local mode (uses local input if no receiving udp sound) +#define AUDIOSYNC_IDLE_MS 2500 // timeout for "receiver idle" (milliseconds) + +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +static uint8_t audioSyncEnabled = AUDIOSYNC_NONE; // bit field: bit 0 - send, bit 1 - receive, bit 2 - use local if not receiving +static bool audioSyncSequence = true; // if true, the receiver will drop out-of-sequence packets +static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! + +// audioreactive variables +#ifdef ARDUINO_ARCH_ESP32 +static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +static float sampleAgc = 0.0f; // Smoothed AGC sample +#ifdef SR_SQUELCH +static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) - enable AGC if default "squelch" was provided +#else +static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) #endif +#endif +static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample +static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData +static unsigned long timeOfPeak = 0; // time of last sample peak detection. +volatile bool haveNewFFTResult = false; // flag to directly inform UDP sound sender when new FFT results are available (to reduce latency). Flag is reset at next UDP send + +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0}; // Our calculated freq. channel result table to be used by effects +static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. (also used by dynamics limiter) +static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) + +static uint16_t zeroCrossingCount = 0; // number of zero crossings in the current batch of 512 samples + +// TODO: probably best not used by receive nodes +static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 + +// user settable parameters for limitSoundDynamics() +#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF +static bool limiterOn = false; // bool: enable / disable dynamics limiter +#else +static bool limiterOn = true; +#endif +static uint8_t micQuality = 0; // affects input filtering; 0 normal, 1 minimal filtering, 2 no filtering +#ifdef FFT_USE_SLIDING_WINDOW +static uint16_t attackTime = 24; // int: attack time in milliseconds. Default 0.024sec +static uint16_t decayTime = 250; // int: decay time in milliseconds. New default 250ms. +#else +static uint16_t attackTime = 50; // int: attack time in milliseconds. Default 0.08sec +static uint16_t decayTime = 300; // int: decay time in milliseconds. New default 300ms. Old default was 1.40sec +#endif + +// peak detection +#ifdef ARDUINO_ARCH_ESP32 +static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode +#endif +static void autoResetPeak(void); // peak auto-reset function +static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) +static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) + +#ifdef ARDUINO_ARCH_ESP32 + // use audio source class (ESP32 specific) #include "audio_source.h" -constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) // globals @@ -62,16 +205,15 @@ static uint8_t inputLevel = 128; // UI slider value #else uint8_t sampleGain = SR_GAIN; // sample gain (config value) #endif -static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) -static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group -// user settable parameters for limitSoundDynamics() -static bool limiterOn = true; // bool: enable / disable dynamics limiter -static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec // user settable options for FFTResult scaling -static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized sqare root +static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root +#ifndef SR_FREQ_PROF + static uint8_t pinkIndex = 0; // 0: default; 1: line-in; 2: IMNP441 +#else + static uint8_t pinkIndex = SR_FREQ_PROF; // 0: default; 1: line-in; 2: IMNP441 +#endif + // // AGC presets @@ -89,28 +231,43 @@ const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter +#if defined(WLEDMM_FASTPATH) +const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/8.f, 1/5.f, 1/12.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) +#else const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) +#endif // AGC presets end static AudioSource *audioSource = nullptr; -static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. -static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. +static uint8_t useInputFilter = 0; // enables low-cut filtering. Applies before FFT. -// audioreactive variables shared with FFT task -static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point -static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier -static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) -static float sampleAgc = 0.0f; // Smoothed AGC sample +//WLEDMM add experimental settings +static uint8_t micLevelMethod = 0; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you) +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) +static constexpr uint8_t averageByRMS = false; // false: use mean value, true: use RMS (root mean squared). use simpler method on slower MCUs. +#else +static constexpr uint8_t averageByRMS = true; // false: use mean value, true: use RMS (root mean squared). use better method on fast MCUs. +#endif +static uint8_t freqDist = 0; // 0=old 1=rightshift mode +static uint8_t fftWindow = 0; // FFT windowing function (0 = default) +#ifdef FFT_USE_SLIDING_WINDOW +static uint8_t doSlidingFFT = 1; // 1 = use sliding window FFT (faster & more accurate) +#endif -// peak detection -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() -static uint8_t maxVol = 10; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) -static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData -static unsigned long timeOfPeak = 0; // time of last sample peak detection. -static void detectSamplePeak(void); // peak detection function (needs scaled FFT reasults in vReal[]) -static void autoResetPeak(void); // peak auto-reset function +// variables used in effects +//static int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc +//static float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc +// shared vars for debugging +#ifdef MIC_LOGGER +static volatile float micReal_min = 0.0f; // MicIn data min from last batch of samples +static volatile float micReal_avg = 0.0f; // MicIn data average (from last batch of samples) +static volatile float micReal_max = 0.0f; // MicIn data max from last batch of samples +#if 0 +static volatile float micReal_min2 = 0.0f; // MicIn data min after filtering +static volatile float micReal_max2 = 0.0f; // MicIn data max after filtering +#endif +#endif //////////////////// // Begin FFT Code // @@ -119,75 +276,136 @@ static void autoResetPeak(void); // peak auto-reset function // some prototypes, to ensure consistent interfaces static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float static float fftAddAvg(int from, int to); // average of several FFT result bins -void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results +void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels, bool i2sFastpath); // post-processing and post-amp of GEQ channels -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! static TaskHandle_t FFT_Task = nullptr; // Table of multiplication factors so that we can even out the frequency response. -static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; +#define MAX_PINK 10 // 0 = standard, 1= line-in (pink noise only), 2..4 = IMNP441, 5..6 = ICS-43434, ,7=SPM1423, 8..9 = userdef, 10= flat (no pink noise adjustment) +static const float fftResultPink[MAX_PINK+1][NUM_GEQ_CHANNELS] = { + { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }, // 0 default from SR WLED + // { 1.30f, 1.32f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 2.39f, 3.09f, 4.34f }, // - Line-In Generic -> pink noise adjustment only + { 2.35f, 1.32f, 1.32f, 1.40f, 1.48f, 1.57f, 1.68f, 1.80f, 1.89f, 1.95f, 2.14f, 2.26f, 2.50f, 2.90f, 4.20f, 6.50f }, // 1 Line-In CS5343 + DC blocker + + { 1.82f, 1.72f, 1.70f, 1.50f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 2.90f, 3.86f, 6.29f}, // 2 IMNP441 datasheet response profile * pink noise + { 2.80f, 2.20f, 1.30f, 1.15f, 1.55f, 2.45f, 4.20f, 2.80f, 3.20f, 3.60f, 4.20f, 4.90f, 5.70f, 6.05f,10.50f,14.85f}, // 3 IMNP441 - big speaker, strong bass + // next one has not much visual differece compared to default IMNP441 profile + { 12.0f, 6.60f, 2.60f, 1.15f, 1.35f, 2.05f, 2.85f, 2.50f, 2.85f, 3.30f, 2.25f, 4.35f, 3.80f, 3.75f, 6.50f, 9.00f}, // 4 IMNP441 - voice, or small speaker + + { 2.75f, 1.60f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 1.75f, 2.55f, 3.60f }, // 5 ICS-43434 datasheet response * pink noise + { 2.90f, 1.25f, 0.75f, 1.08f, 2.35f, 3.55f, 3.60f, 3.40f, 2.75f, 3.45f, 4.40f, 6.35f, 6.80f, 6.80f, 8.50f,10.64f }, // 6 ICS-43434 - big speaker, strong bass + + { 1.65f, 1.00f, 1.05f, 1.30f, 1.48f, 1.30f, 1.80f, 3.00f, 1.50f, 1.65f, 2.56f, 3.00f, 2.60f, 2.30f, 5.00f, 3.00f }, // 7 SPM1423 + { 2.25f, 1.60f, 1.30f, 1.60f, 2.20f, 3.20f, 3.06f, 2.60f, 2.85f, 3.50f, 4.10f, 4.80f, 5.70f, 6.05f,10.50f,14.85f }, // 8 userdef #1 for ewowi (enhance median/high freqs) + { 4.75f, 3.60f, 2.40f, 2.46f, 3.52f, 1.60f, 1.68f, 3.20f, 2.20f, 2.00f, 2.30f, 2.41f, 2.30f, 1.25f, 4.55f, 6.50f }, // 9 userdef #2 for softhack (mic hidden inside mini-shield) + + { 2.38f, 2.18f, 2.07f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.70f, 1.95f, 1.70f, 2.13f, 2.47f } // 10 almost FLAT (IMNP441 but no PINK noise adjustments) +}; + + /* how to make your own profile: + * =============================== + * preparation: make sure your microphone has direct line-of-sigh with the speaker, 1-2meter distance is best + * Prepare your HiFi equipment: disable all "Sound enhancements" - like Loudness, Equalizer, Bass Boost. Bass/Treble controls set to middle. + * Your HiFi equipment should receive its audio input from Line-In, SPDIF, HDMI, or another "undistorted" connection (like CDROM). + * Try not to use Bluetooth or MP3 when playing the "pink noise" audio. BT-audio and MP3 both perform "acoustic adjustments" that we don't want now. + + * SR WLED: enable AGC ("standard" or "lazy"), set squelch to a low level, check that LEDs don't reacts in silence. + * SR WLED: select "Generic Line-In" as your Frequency Profile, "Linear" or "Square Root" as Frequency Scale + * SR WLED: Dynamic Limiter On, Dynamics Fall Time around 4200 - makes GEQ hold peaks for much longer + * SR WLED: Select GEQ effect, move all effect slider to max (i.e. right side) + + * Measure: play Pink Noise for 2-3 minutes - for examples from youtube https://www.youtube.com/watch?v=ZXtimhT-ff4 + * Measure: Take a Photo. Make sure that LEDs for each "bar" are well visible (ou need to count them later) + + * Your own profile: + * - Target for each LED bar is 50% to 75% of the max height --> 8(high) x 16(wide) panel means target = 5. 32 x 16 means target = 22. + * - From left to right - count the LEDs in each of the 16 frequency columns (that's why you need the photo). This is the barheight for each channel. + * - math time! Find the multiplier that will bring each bar to to target. + * * in case of square root scale: multiplier = (target * target) / (barheight * barheight) + * * in case of linear scale: multiplier = target / barheight + * + * - replace one of the "userdef" lines with a copy of the parameter line for "Line-In", + * - go through your new "userdef" parameter line, multiply each entry with the mutliplier you found for that column. + + * Compile + upload + * Test your new profile (same procedure as above). Iterate the process to improve results. + */ // globals and FFT Output variables shared with animations -static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects -#if defined(WLED_DEBUG) || defined(SR_DEBUG) -static uint64_t fftTime = 0; -static uint64_t sampleTime = 0; +static float FFT_MajPeakSmth = 1.0f; // FFT: (peak) frequency, smooth +#if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) +static float fftTaskCycle = 0; // avg cycle time for FFT task +static float fftTime = 0; // avg time for single FFT +static float sampleTime = 0; // avg (blocked) time for reading I2S samples +static float filterTime = 0; // avg time for filtering I2S samples #endif // FFT Task variables (filtering and post-processing) -static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. -static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) -#ifdef SR_DEBUG -static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. -#endif +static float lastFftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // backup of last FFT channels (before postprocessing) +#if !defined(CONFIG_IDF_TARGET_ESP32C3) // audio source parameters and constant constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms //constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms //constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms //constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms +#ifndef WLEDMM_FASTPATH #define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling +#else + #ifdef FFT_USE_SLIDING_WINDOW + #define FFT_MIN_CYCLE 8 // we only have 12ms to take 1/2 batch of samples + #else + #define FFT_MIN_CYCLE 15 // reduce min time, to allow faster catch-up when I2S is lagging + #endif +#endif //#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling //#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling //#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling +#else +// slightly lower the sampling rate for -C3, to improve stability +//constexpr SRate_t SAMPLE_RATE = 20480; // 20Khz; Physical sample time -> 25ms +//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. +constexpr SRate_t SAMPLE_RATE = 18000; // 18Khz; Physical sample time -> 28ms +#define FFT_MIN_CYCLE 25 // minimum time before FFT task is repeated. +// try 16Khz in case your device still lags and responds too slowly. +//constexpr SRate_t SAMPLE_RATE = 16000; // 16Khz -> Physical sample time -> 32ms +//#define FFT_MIN_CYCLE 30 // minimum time before FFT task is repeated. +#endif // FFT Constants constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 -constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. +constexpr uint16_t samplesFFT_2 = 256; // meaningful part of FFT results - only the "lower half" contains useful information. // the following are observed values, supported by a bit of "educated guessing" //#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels -#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels +//#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels +#define FFT_DOWNSCALE 0.40f // downscaling factor for FFT results, RMS averaging #define LOG_256 5.54517744f // log(256) // These are the input and output vectors. Input vectors receive computed results from FFT. -static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins -static float vImag[samplesFFT] = {0.0f}; // imaginary parts -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT -static float windowWeighingFactors[samplesFFT] = {0.0f}; +static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins +static float* vImag = nullptr; // imaginary parts + +#ifdef FFT_MAJORPEAK_HUMAN_EAR +static float* pinkFactors = nullptr; // "pink noise" correction factors +constexpr float pinkcenter = 23.66; // sqrt(560) - center freq for scaling is 560 hz. +constexpr float binWidth = SAMPLE_RATE / (float)samplesFFT; // frequency range of each FFT result bin #endif + // Create FFT object -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 -#define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc), and an a few other speedups -#define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt -#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 50% on ESP32 (as alternative to FFT_SQRT_APPROXIMATION) -#else -// lib_deps += https://github.com/blazoncek/arduinoFFT.git +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +// these options actually cause slow-down on -S2 (-S2 doesn't have floating point hardware) +//#define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc), and an a few other speedups - WLEDMM not faster on ESP32 +//#define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - WLEDMM slower on ESP32 #endif +#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 (as alternative to FFT_SQRT_APPROXIMATION) +#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 #include -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); -#else -static arduinoFFT FFT = arduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE); -#endif - // Helper functions // float version of map() @@ -195,24 +413,132 @@ static float mapf(float x, float in_min, float in_max, float out_min, float out_ return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } -// compute average of several FFT resut bins -static float fftAddAvg(int from, int to) { +// compute average of several FFT result bins +// linear average +static float fftAddAvgLin(int from, int to) { float result = 0.0f; for (int i = from; i <= to; i++) { result += vReal[i]; } return result / float(to - from + 1); } +// RMS average +static float fftAddAvgRMS(int from, int to) { + double result = 0.0; + for (int i = from; i <= to; i++) { + result += vReal[i] * vReal[i]; + } + return sqrtf(result / float(to - from + 1)); +} + +static float fftAddAvg(int from, int to) { + if (from == to) return vReal[from]; // small optimization + if (averageByRMS) return fftAddAvgRMS(from, to); // use RMS + else return fftAddAvgLin(from, to); // use linear average +} + +#if defined(CONFIG_IDF_TARGET_ESP32C3) +constexpr bool skipSecondFFT = true; +#else +constexpr bool skipSecondFFT = false; +#endif + +// allocate FFT sample buffers from heap +static bool alocateFFTBuffers(void) { + #ifdef SR_DEBUG + USER_PRINT(F("\nFree heap ")); USER_PRINTLN(ESP.getFreeHeap()); + #endif + + if (vReal) free(vReal); // should not happen + if (vImag) free(vImag); // should not happen + if ((vReal = (float*) calloc(sizeof(float), samplesFFT)) == nullptr) return false; // calloc or die + if ((vImag = (float*) calloc(sizeof(float), samplesFFT)) == nullptr) return false; +#ifdef FFT_MAJORPEAK_HUMAN_EAR + if (pinkFactors) free(pinkFactors); + if ((pinkFactors = (float*) calloc(sizeof(float), samplesFFT)) == nullptr) return false; +#endif + + #ifdef SR_DEBUG + USER_PRINTLN("\nalocateFFTBuffers() completed successfully."); + USER_PRINT(F("Free heap: ")); USER_PRINTLN(ESP.getFreeHeap()); + USER_PRINT("FFTtask free stack: "); USER_PRINTLN(uxTaskGetStackHighWaterMark(NULL)); + USER_FLUSH(); + #endif + return(true); // success +} + +// High-Pass "DC blocker" filter +// see https://www.dsprelated.com/freebooks/filters/DC_Blocker.html +static void runDCBlocker(uint_fast16_t numSamples, float *sampleBuffer) { + constexpr float filterR = 0.990f; // around 40hz + static float xm1 = 0.0f; + static SR_HIRES_TYPE ym1 = 0.0f; + + for (unsigned i=0; i < numSamples; i++) { + float value = sampleBuffer[i]; + SR_HIRES_TYPE filtered = (SR_HIRES_TYPE)(value-xm1) + filterR*ym1; + xm1 = value; + ym1 = filtered; + sampleBuffer[i] = filtered; + } +} // // FFT main task // void FFTcode(void * parameter) { - DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); + #ifdef SR_DEBUG + USER_FLUSH(); + USER_PRINT("AR: "); USER_PRINT(pcTaskGetTaskName(NULL)); + USER_PRINT(" task started on core "); USER_PRINT(xPortGetCoreID()); // causes trouble on -S2 + USER_PRINT(" [prio="); USER_PRINT(uxTaskPriorityGet(NULL)); + USER_PRINT(", min free stack="); USER_PRINT(uxTaskGetStackHighWaterMark(NULL)); + USER_PRINTLN("]"); USER_FLUSH(); + #endif // see https://www.freertos.org/vtaskdelayuntil.html const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; + const TickType_t xFrequencyDouble = FFT_MIN_CYCLE * portTICK_PERIOD_MS * 2; + static bool isFirstRun = false; + +#ifdef FFT_USE_SLIDING_WINDOW + static float* oldSamples = nullptr; // previous 50% of samples + static bool haveOldSamples = false; // for sliding window FFT + bool usingOldSamples = false; + if (!oldSamples) oldSamples = (float*) calloc(sizeof(float), samplesFFT_2); // allocate on first run + if (!oldSamples) { disableSoundProcessing = true; return; } // no memory -> die +#endif + + bool success = true; + if ((vReal == nullptr) || (vImag == nullptr)) success = alocateFFTBuffers(); // allocate sample buffers on first run + if (success == false) { disableSoundProcessing = true; return; } // no memory -> die + + // create FFT object - we have to do if after allocating buffers +#if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 + // arduinoFFT 2.x has a slightly different API + static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); +#else + // recommended version optimized by @softhack007 (API version 1.9) + #if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) + static float* windowWeighingFactors = nullptr; + if (!windowWeighingFactors) windowWeighingFactors = (float*) calloc(sizeof(float), samplesFFT); // cache for FFT windowing factors - use heap + #else + static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors - use global RAM + #endif + static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +#endif + + #ifdef FFT_MAJORPEAK_HUMAN_EAR + // pre-compute pink noise scaling table + for(uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) { + float binFreq = binInd * binWidth + binWidth/2.0f; + if (binFreq > (SAMPLE_RATE * 0.42f)) + binFreq = (SAMPLE_RATE * 0.42f) - 0.25 * (binFreq - (SAMPLE_RATE * 0.42f)); // suppress noise and aliasing + pinkFactors[binInd] = sqrtf(binFreq) / pinkcenter; + } + pinkFactors[0] *= 0.5; // suppress 0-42hz bin + #endif TickType_t xLastWakeTime = xTaskGetTickCount(); for(;;) { @@ -220,176 +546,384 @@ void FFTcode(void * parameter) // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. // Don't run FFT computing code if we're in Receive mode or in realtime mode - if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { + if (disableSoundProcessing || (audioSyncEnabled == AUDIOSYNC_REC)) { + isFirstRun = false; + #ifdef FFT_USE_SLIDING_WINDOW + haveOldSamples = false; + #endif vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers continue; } -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + // timing uint64_t start = esp_timer_get_time(); bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid + static uint64_t lastCycleStart = 0; + static uint64_t lastLastTime = 0; + if ((lastCycleStart > 0) && (lastCycleStart < start)) { // filter out overflows + uint64_t taskTimeInMillis = ((start - lastCycleStart) +5ULL) / 10ULL; // "+5" to ensure proper rounding + fftTaskCycle = (((taskTimeInMillis + lastLastTime)/2) *4 + fftTaskCycle*6)/10.0; // smart smooth + lastLastTime = taskTimeInMillis; + } + lastCycleStart = start; #endif // get a fresh batch of samples from I2S + memset(vReal, 0, sizeof(float) * samplesFFT); // start clean +#ifdef FFT_USE_SLIDING_WINDOW + uint16_t readOffset; + if (haveOldSamples && (doSlidingFFT > 0)) { + memcpy(vReal, oldSamples, sizeof(float) * samplesFFT_2); // copy first 50% from buffer + usingOldSamples = true; + readOffset = samplesFFT_2; + } else { + usingOldSamples = false; + readOffset = 0; + } + // read fresh samples, in chunks of 50% + do { + // this looks a bit cumbersome, but it onlyworks this way - any second instance of the getSamples() call delivers junk data. + if (audioSource) audioSource->getSamples(vReal+readOffset, samplesFFT_2); + readOffset += samplesFFT_2; + } while (readOffset < samplesFFT); +#else if (audioSource) audioSource->getSamples(vReal, samplesFFT); +#endif -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + // debug info in case that stack usage changes + static unsigned int minStackFree = UINT32_MAX; + unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); + if (minStackFree > stackFree) { + minStackFree = stackFree; + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minStackFree); //WLEDMM + } + // timing if (start < esp_timer_get_time()) { // filter out overflows uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding - sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth + sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10.0; // smooth } - start = esp_timer_get_time(); // start measuring FFT time + start = esp_timer_get_time(); // start measuring filter time #endif xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay + isFirstRun = !isFirstRun; // toggle throttle +#ifdef MIC_LOGGER + float datMin = 0.0f; + float datMax = 0.0f; + double datAvg = 0.0f; + for (int i=0; i < samplesFFT; i++) { + if (i==0) { + datMin = datMax = vReal[i]; + } else { + if (datMin > vReal[i]) datMin = vReal[i]; + if (datMax < vReal[i]) datMax = vReal[i]; + } + datAvg += vReal[i]; + } +#endif + +#if defined(WLEDMM_FASTPATH) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(ARDUINO_ARCH_ESP32) + // experimental - be nice to LED update task (trying to avoid flickering) - dual core only +#if FFTTASK_PRIORITY > 1 + if (strip.isServicing()) delay(1); +#endif +#endif + + // normal mode: filter everything + float *samplesStart = vReal; + uint16_t sampleCount = samplesFFT; + #ifdef FFT_USE_SLIDING_WINDOW + if (usingOldSamples) { + // sliding window mode: only latest 50% need filtering + samplesStart = vReal + samplesFFT_2; + sampleCount = samplesFFT_2; + } + #endif // band pass filter - can reduce noise floor by a factor of 50 // downside: frequencies below 100Hz will be ignored - if (useBandPassFilter) runMicFilter(samplesFFT, vReal); + bool doDCRemoval = false; // DCRemove is only necessary if we don't use any kind of low-cut filtering + if ((useInputFilter > 0) && (useInputFilter < 99)) { + switch(useInputFilter) { + case 1: runMicFilter(sampleCount, samplesStart); break; // PDM microphone bandpass + case 2: runDCBlocker(sampleCount, samplesStart); break; // generic Low-Cut + DC blocker (~40hz cut-off) + default: doDCRemoval = true; break; + } + } else doDCRemoval = true; - // find highest sample in the batch +#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + // timing measurement + if (start < esp_timer_get_time()) { // filter out overflows + uint64_t filterTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding + filterTime = (filterTimeInMillis*3 + filterTime*7)/10.0; // smooth + } + start = esp_timer_get_time(); // start measuring FFT time +#endif + + // set imaginary parts to 0 + memset(vImag, 0, sizeof(float) * samplesFFT); + + #ifdef FFT_USE_SLIDING_WINDOW + memcpy(oldSamples, vReal+samplesFFT_2, sizeof(float) * samplesFFT_2); // copy last 50% to buffer (for sliding window FFT) + haveOldSamples = true; + #endif + + // find highest sample in the batch, and count zero crossings float maxSample = 0.0f; // max sample from FFT batch + uint_fast16_t newZeroCrossingCount = 0; for (int i=0; i < samplesFFT; i++) { - // set imaginary parts to 0 - vImag[i] = 0; // pick our our current mic sample - we take the max value from all samples that go into FFT - if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts + if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) { //skip extreme values - normally these are artefacts + #ifdef FFT_USE_SLIDING_WINDOW + if (usingOldSamples) { + if ((i >= samplesFFT_2) && (fabsf(vReal[i]) > maxSample)) maxSample = fabsf(vReal[i]); // only look at newest 50% + } else + #endif if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); + } + // WLED-MM/TroyHacks: Calculate zero crossings + // + if (i < (samplesFFT-1)) { + if (__builtin_signbit(vReal[i]) != __builtin_signbit(vReal[i+1])) // test sign bit: sign changed -> zero crossing + newZeroCrossingCount++; + } } + newZeroCrossingCount = (newZeroCrossingCount*2)/3; // reduce value so it typically stays below 256 + zeroCrossingCount = newZeroCrossingCount; // update only once, to avoid that effects pick up an intermediate value + // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results. micDataReal = maxSample; - -#ifdef SR_DEBUG - if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization -#else - if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. +#ifdef MIC_LOGGER + micReal_min = datMin; + micReal_max = datMax; + micReal_avg = datAvg / samplesFFT; +#if 0 + // compute mix/max again after filering - usefull for filter debugging + for (int i=0; i < samplesFFT; i++) { + if (i==0) { + datMin = datMax = vReal[i]; + } else { + if (datMin > vReal[i]) datMin = vReal[i]; + if (datMax < vReal[i]) datMax = vReal[i]; + } + } + micReal_min2 = datMin; + micReal_max2 = datMax; +#endif #endif - // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT - FFT.dcRemoval(); // remove DC offset - FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy - //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection - FFT.compute( FFTDirection::Forward ); // Compute FFT - FFT.complexToMagnitude(); // Compute magnitudes -#else - FFT.DCRemoval(); // let FFT lib remove DC component, so we don't need to care about this in getSamples() + float wc = 1.0; // FFT window correction factor, relative to Blackman_Harris + + // run FFT (takes 3-5ms on ESP32) + if (fabsf(volumeSmth) > 0.25f) { // noise gate open + if ((skipSecondFFT == false) || (isFirstRun == true)) { + // run FFT (takes 2-3ms on ESP32, ~12ms on ESP32-S2, ~30ms on -C3) + if (doDCRemoval) FFT.dcRemoval(); // remove DC offset + switch(fftWindow) { // apply FFT window + case 1: + FFT.windowing(FFTWindow::Hann, FFTDirection::Forward); // recommended for 50% overlap + wc = 0.66415918066; // 1.8554726898 * 2.0 + break; + case 2: + FFT.windowing( FFTWindow::Nuttall, FFTDirection::Forward); + wc = 0.9916873881f; // 2.8163172034 * 2.0 + break; + case 5: + FFT.windowing( FFTWindow::Blackman, FFTDirection::Forward); + wc = 0.84762867875f; // 2.3673474360 * 2.0 + break; + case 3: + FFT.windowing( FFTWindow::Hamming, FFTDirection::Forward); + wc = 0.664159180663f; // 1.8549343278 * 2.0 + break; + case 4: + FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude preservation, low frequency accuracy + wc = 1.276771793156f; // 3.5659039231 * 2.0 + break; + case 0: // falls through + default: + FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection + wc = 1.0f; // 2.7929062517 * 2.0 + } + #ifdef FFT_USE_SLIDING_WINDOW + if (usingOldSamples) wc = wc * 1.10f; // compensate for loss caused by averaging + #endif - //FFT.Windowing( FFT_WIN_TYP_HAMMING, FFT_FORWARD ); // Weigh data - standard Hamming window - //FFT.Windowing( FFT_WIN_TYP_BLACKMAN, FFT_FORWARD ); // Blackman window - better side freq rejection - //FFT.Windowing( FFT_WIN_TYP_BLACKMAN_HARRIS, FFT_FORWARD );// Blackman-Harris - excellent sideband rejection - FFT.Windowing( FFT_WIN_TYP_FLT_TOP, FFT_FORWARD ); // Flat Top Window - better amplitude accuracy - FFT.Compute( FFT_FORWARD ); // Compute FFT - FFT.ComplexToMagnitude(); // Compute magnitudes -#endif + FFT.compute( FFTDirection::Forward ); // Compute FFT + FFT.complexToMagnitude(); // Compute magnitudes + vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT - FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant -#else - FFT.MajorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant -#endif - FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + float last_majorpeak = FFT_MajorPeak; + float last_magnitude = FFT_Magnitude; + + #ifdef FFT_MAJORPEAK_HUMAN_EAR + // scale FFT results + for(uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) + vReal[binInd] *= pinkFactors[binInd]; + #endif + + #if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 + // arduinoFFT 2.x has a slightly different API + FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); + #else + FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant + #endif + FFT_Magnitude *= wc; // apply correction factor + + if (FFT_MajorPeak < (SAMPLE_RATE / samplesFFT)) {FFT_MajorPeak = 1.0f; FFT_Magnitude = 0;} // too low - use zero + if (FFT_MajorPeak > (0.42f * SAMPLE_RATE)) {FFT_MajorPeak = last_majorpeak; FFT_Magnitude = last_magnitude;} // too high - keep last peak + + #ifdef FFT_MAJORPEAK_HUMAN_EAR + // undo scaling - we want unmodified values for FFTResult[] computations + for(uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) + vReal[binInd] *= 1.0f/pinkFactors[binInd]; + //fix peak magnitude + if ((FFT_MajorPeak > (binWidth/1.25f)) && (FFT_MajorPeak < (SAMPLE_RATE/2.2f)) && (FFT_Magnitude > 4.0f)) { + unsigned peakBin = constrain((int)((FFT_MajorPeak + binWidth/2.0f) / binWidth), 0, samplesFFT -1); + FFT_Magnitude *= fmaxf(1.0f/pinkFactors[peakBin], 1.0f); + } + #endif + FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + FFT_MajPeakSmth = FFT_MajPeakSmth + 0.42 * (FFT_MajorPeak - FFT_MajPeakSmth); // I like this "swooping peak" look -#if defined(WLED_DEBUG) || defined(SR_DEBUG) + } else { // skip second run --> clear fft results, keep peaks + memset(vReal, 0, sizeof(float) * samplesFFT); + } +#if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) haveDoneFFT = true; #endif } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. - memset(vReal, 0, sizeof(vReal)); + memset(vReal, 0, sizeof(float) * samplesFFT); FFT_MajorPeak = 1; FFT_Magnitude = 0.001; } - for (int i = 0; i < samplesFFT; i++) { - float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way - vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. - } // for() - - // mapping of FFT result bins to frequency channels - if (fabsf(sampleAvg) > 0.5f) { // noise gate open -#if 0 - /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. - * - * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. - * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. - * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then detetermine the bins. - * End frequency = Start frequency * multiplier ^ 16 - * Multiplier = (End frequency/ Start frequency) ^ 1/16 - * Multiplier = 1.320367784 - */ // Range - fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 - fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 - fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 - fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 - fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 - fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 - fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 - fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 - fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 - fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 - fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 - fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 - fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 - fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 - fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 - fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate -#else - /* new mapping, optimized for 22050 Hz by softhack007 */ - // bins frequency range - if (useBandPassFilter) { - // skip frequencies below 100hz - fftCalc[ 0] = 0.8f * fftAddAvg(3,4); - fftCalc[ 1] = 0.9f * fftAddAvg(4,5); - fftCalc[ 2] = fftAddAvg(5,6); - fftCalc[ 3] = fftAddAvg(6,7); - // don't use the last bins from 206 to 255. - fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping - } else { - fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass - fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass - fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass - fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange - // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) - fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping - } - fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange - fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange - fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange - fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! - fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange - fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange - fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid - fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid - fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid - fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid - fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping -#endif + if ((skipSecondFFT == false) || (isFirstRun == true)) { + for (int i = 0; i < samplesFFT; i++) { + float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way + vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. + } // for() + + // mapping of FFT result bins to frequency channels + //if (fabsf(sampleAvg) > 0.25f) { // noise gate open + if (fabsf(volumeSmth) > 0.25f) { // noise gate open + //WLEDMM: different distributions + if (freqDist == 0) { + /* new mapping, optimized for 22050 Hz by softhack007 --- update: removed overlap */ + // bins frequency range + if (useInputFilter==1) { + // skip frequencies below 100hz + fftCalc[ 0] = wc * 0.8f * fftAddAvg(3,3); + fftCalc[ 1] = wc * 0.9f * fftAddAvg(4,4); + fftCalc[ 2] = wc * fftAddAvg(5,5); + fftCalc[ 3] = wc * fftAddAvg(6,6); + // don't use the last bins from 206 to 255. + fftCalc[15] = wc * fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping + } else { + fftCalc[ 0] = wc * fftAddAvg(1,1); // 1 43 - 86 sub-bass + fftCalc[ 1] = wc * fftAddAvg(2,2); // 1 86 - 129 bass + fftCalc[ 2] = wc * fftAddAvg(3,4); // 2 129 - 216 bass + fftCalc[ 3] = wc * fftAddAvg(5,6); // 2 216 - 301 bass + midrange + // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) + fftCalc[15] = wc * fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping + } + fftCalc[ 4] = wc * fftAddAvg(7,9); // 3 301 - 430 midrange + fftCalc[ 5] = wc * fftAddAvg(10,12); // 3 430 - 560 midrange + fftCalc[ 6] = wc * fftAddAvg(13,18); // 5 560 - 818 midrange + fftCalc[ 7] = wc * fftAddAvg(19,25); // 7 818 - 1120 midrange -- 1Khz should always be the center ! + fftCalc[ 8] = wc * fftAddAvg(26,32); // 7 1120 - 1421 midrange + fftCalc[ 9] = wc * fftAddAvg(33,43); // 9 1421 - 1895 midrange + fftCalc[10] = wc * fftAddAvg(44,55); // 12 1895 - 2412 midrange + high mid + fftCalc[11] = wc * fftAddAvg(56,69); // 14 2412 - 3015 high mid + fftCalc[12] = wc * fftAddAvg(70,85); // 16 3015 - 3704 high mid + fftCalc[13] = wc * fftAddAvg(86,103); // 18 3704 - 4479 high mid + fftCalc[14] = wc * fftAddAvg(104,164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping + } else if (freqDist == 1) { //WLEDMM: Rightshift: note ewowi: frequencies in comments are not correct + if (useInputFilter==1) { + // skip frequencies below 100hz + fftCalc[ 0] = wc * 0.8f * fftAddAvg(1,1); + fftCalc[ 1] = wc * 0.9f * fftAddAvg(2,2); + fftCalc[ 2] = wc * fftAddAvg(3,3); + fftCalc[ 3] = wc * fftAddAvg(4,4); + // don't use the last bins from 206 to 255. + fftCalc[15] = wc * fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping + } else { + fftCalc[ 0] = wc * fftAddAvg(1,1); // 1 43 - 86 sub-bass + fftCalc[ 1] = wc * fftAddAvg(2,2); // 1 86 - 129 bass + fftCalc[ 2] = wc * fftAddAvg(3,3); // 2 129 - 216 bass + fftCalc[ 3] = wc * fftAddAvg(4,4); // 2 216 - 301 bass + midrange + // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) + fftCalc[15] = wc * fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping + } + fftCalc[ 4] = wc * fftAddAvg(5,6); // 3 301 - 430 midrange + fftCalc[ 5] = wc * fftAddAvg(7,8); // 3 430 - 560 midrange + fftCalc[ 6] = wc * fftAddAvg(9,10); // 5 560 - 818 midrange + fftCalc[ 7] = wc * fftAddAvg(11,13); // 7 818 - 1120 midrange -- 1Khz should always be the center ! + fftCalc[ 8] = wc * fftAddAvg(14,18); // 7 1120 - 1421 midrange + fftCalc[ 9] = wc * fftAddAvg(19,25); // 9 1421 - 1895 midrange + fftCalc[10] = wc * fftAddAvg(26,36); // 12 1895 - 2412 midrange + high mid + fftCalc[11] = wc * fftAddAvg(37,45); // 14 2412 - 3015 high mid + fftCalc[12] = wc * fftAddAvg(46,66); // 16 3015 - 3704 high mid + fftCalc[13] = wc * fftAddAvg(67,97); // 18 3704 - 4479 high mid + fftCalc[14] = wc * fftAddAvg(98,164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping + } } else { // noise gate closed - just decay old values + isFirstRun = false; for (int i=0; i < NUM_GEQ_CHANNELS; i++) { fftCalc[i] *= 0.85f; // decay to zero if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; - } + } } + + memcpy(lastFftCalc, fftCalc, sizeof(lastFftCalc)); // make a backup of last "good" channels + + } else { // if second run skipped + memcpy(fftCalc, lastFftCalc, sizeof(fftCalc)); // restore last "good" channels } - // post-processing of frequency channels (pink noise adjustment, AGC, smooting, scaling) - postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); + // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) + if (pinkIndex > MAX_PINK) pinkIndex = MAX_PINK; -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#ifdef FFT_USE_SLIDING_WINDOW + postProcessFFTResults((fabsf(volumeSmth) > 0.25f)? true : false, NUM_GEQ_CHANNELS, usingOldSamples); // this function modifies fftCalc, fftAvg and fftResult +#else + postProcessFFTResults((fabsf(volumeSmth) > 0.25f)? true : false, NUM_GEQ_CHANNELS, false); // this function modifies fftCalc, fftAvg and fftResult +#endif + +#if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + // timing + static uint64_t lastLastFFT = 0; if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding - fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth + fftTime = (((fftTimeInMillis + lastLastFFT)/2) *3 + fftTime*7)/10.0; // smart smooth + lastLastFFT = fftTimeInMillis; } #endif + // run peak detection autoResetPeak(); detectSamplePeak(); + + haveNewFFTResult = true; #if !defined(I2S_GRAB_ADC1_COMPLETELY) if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC #endif - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - + { + #ifdef FFT_USE_SLIDING_WINDOW + if (!usingOldSamples) { + vTaskDelayUntil( &xLastWakeTime, xFrequencyDouble); // we need a double wait when no old data was used + } else + #endif + if ((skipSecondFFT == false) || (fabsf(volumeSmth) < 0.25f)) { + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + } else if (isFirstRun == true) { + vTaskDelayUntil( &xLastWakeTime, xFrequencyDouble); // release CPU after performing FFT in "skip second run" mode + } + } } // for(;;)ever } // FFTcode() task end @@ -419,7 +953,7 @@ static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // p // FIR lowpass, to remove high frequency noise float highFilteredSample; if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes - else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // spcial handling for last sample in array + else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array last_vals[1] = last_vals[0]; last_vals[0] = sampleBuffer[i]; sampleBuffer[i] = highFilteredSample; @@ -429,37 +963,54 @@ static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // p } } -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels, bool i2sFastpath) // post-processing and post-amp of GEQ channels { for (int i=0; i < numberOfChannels; i++) { if (noiseGateOpen) { // noise gate open // Adjustment for frequency curves. - fftCalc[i] *= fftResultPink[i]; + fftCalc[i] *= fftResultPink[pinkIndex][i]; if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function // Manual linear adjustment of gain using sampleGain adjustment for different input types. fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment if(fftCalc[i] < 0) fftCalc[i] = 0; } - // smooth results - rise fast, fall slower - if(fftCalc[i] > fftAvg[i]) // rise fast - fftAvg[i] = fftCalc[i] *0.75f + 0.25f*fftAvg[i]; // will need approx 2 cycles (50ms) for converging against fftCalc[i] - else { // fall slow - if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero - else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero - else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero - else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero + float speed = 1.0f; // filter correction for sampling speed -> 1.0 in normal mode (43hz) + if (i2sFastpath) speed = 0.6931471805599453094f * 1.1f; // -> ln(2) from math, *1.1 from my gut feeling ;-) in fast mode (86hz) + + if(limiterOn == true) { + // Limiter ON -> smooth results + if(fftCalc[i] > fftAvg[i]) { // rise fast + fftAvg[i] += speed * 0.78f * (fftCalc[i] - fftAvg[i]); // will need approx 1-2 cycles (50ms) for converging against fftCalc[i] + } else { // fall slow + if (decayTime < 150) fftAvg[i] += speed * 0.50f * (fftCalc[i] - fftAvg[i]); + else if (decayTime < 250) fftAvg[i] += speed * 0.40f * (fftCalc[i] - fftAvg[i]); + else if (decayTime < 500) fftAvg[i] += speed * 0.33f * (fftCalc[i] - fftAvg[i]); + else if (decayTime < 1000) fftAvg[i] += speed * 0.22f * (fftCalc[i] - fftAvg[i]); // approx 5 cycles (225ms) for falling to zero + else if (decayTime < 2000) fftAvg[i] += speed * 0.17f * (fftCalc[i] - fftAvg[i]); // default - approx 9 cycles (225ms) for falling to zero + else if (decayTime < 3000) fftAvg[i] += speed * 0.14f * (fftCalc[i] - fftAvg[i]); // approx 14 cycles (350ms) for falling to zero + else if (decayTime < 4000) fftAvg[i] += speed * 0.10f * (fftCalc[i] - fftAvg[i]); + else fftAvg[i] += speed * 0.05f * (fftCalc[i] - fftAvg[i]); + } + } else { + // Limiter OFF + if (i2sFastpath) { + // fast mode -> average last two results + float tmp = fftCalc[i]; + fftCalc[i] = 0.7f * tmp + 0.3f * fftAvg[i]; + fftAvg[i] = tmp; // store current sample for next run + } else { + // normal mode -> no adjustments + fftAvg[i] = fftCalc[i]; // keep filters up-to-date + } } + // constrain internal vars - just to be sure fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); - float currentResult; - if(limiterOn == true) - currentResult = fftAvg[i]; - else - currentResult = fftCalc[i]; + float currentResult = limiterOn ? fftAvg[i] : fftCalc[i]; // continue with filtered result (limiter on) or unfiltered result (limiter off) switch (FFTScalingMode) { case 1: @@ -474,24 +1025,26 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // p case 2: // Linear scaling currentResult *= 0.30f; // needs a bit more damping, get stay below 255 - currentResult -= 4.0; // giving a bit more room for peaks + currentResult -= 2.0; // giving a bit more room for peaks if (currentResult < 1.0f) currentResult = 0.0f; currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies break; case 3: // square root scaling currentResult *= 0.38f; + //currentResult *= 0.34f; //experiment currentResult -= 6.0f; if (currentResult > 1.0) currentResult = sqrtf(currentResult); else currentResult = 0.0; // special handling, because sqrt(0) = undefined currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies + //currentResult *= 0.80f + (float(i)/5.6f); //experiment currentResult = mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] break; case 0: default: // no scaling - leave freq bins as-is - currentResult -= 4; // just a bit more room for peaks + currentResult -= 2; // just a bit more room for peaks break; } @@ -501,7 +1054,7 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // p if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; currentResult *= post_gain; } - fftResult[i] = constrain((int)currentResult, 0, 255); + fftResult[i] = max(min((int)(currentResult+0.5f), 255), 0); // +0.5 for proper rounding } } //////////////////// @@ -511,13 +1064,24 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // p // peak detection is called from FFT task when vReal[] contains valid FFT results static void detectSamplePeak(void) { bool havePeak = false; - +#if 1 + // softhack007: this code continuously triggers while volume in the selected bin is above a certain threshold. So it does not detect peaks - it detects volume in a frequency bin. // Poor man's beat detection by seeing if sample > Average + some value. // This goes through ALL of the 255 bins - but ignores stupid settings // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync. - if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 1) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { + if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { + havePeak = true; + } +#endif + +#if 0 + // alternate detection, based on FFT_MajorPeak and FFT_Magnitude. Not much better... + if ((binNum > 1) && (maxVol > 8) && (binNum < 10) && (sampleAgc > 127) && + (FFT_MajorPeak > 50) && (FFT_MajorPeak < 250) && (FFT_Magnitude > (16.0f * (maxVol+42.0)) /*my_magnitude > 136.0f*16.0f*/) && + (millis() - timeOfPeak > 80)) { havePeak = true; } +#endif if (havePeak) { samplePeak = true; @@ -526,15 +1090,16 @@ static void detectSamplePeak(void) { } } +#endif + static void autoResetPeak(void) { uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. samplePeak = false; - if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData + if (audioSyncEnabled == AUDIOSYNC_NONE) udpSamplePeak = false; // this is normally reset by transmitAudioData } } - //////////////////// // usermod class // //////////////////// @@ -543,6 +1108,13 @@ static void autoResetPeak(void) { class AudioReactive : public Usermod { private: +#ifdef ARDUINO_ARCH_ESP32 + +// HUB75 workaround - audio receive only +#ifdef WLED_ENABLE_HUB75MATRIX +#undef SR_DMTYPE +#define SR_DMTYPE 254 // "network receive only" +#endif #ifndef AUDIOPIN int8_t audioPin = -1; #else @@ -584,25 +1156,27 @@ class AudioReactive : public Usermod { #else int8_t mclkPin = MCLK_PIN; #endif - - // new "V2" audiosync struct - 40 Bytes - struct audioSyncPacket { - char header[6]; // 06 Bytes - float sampleRaw; // 04 Bytes - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting - float sampleSmth; // 04 Bytes - either "sampleAvg" or "sampleAgc" depending on soundAgc setting - uint8_t samplePeak; // 01 Bytes - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude - uint8_t reserved1; // 01 Bytes - for future extensions - not used yet - uint8_t fftResult[16]; // 16 Bytes - float FFT_Magnitude; // 04 Bytes - float FFT_MajorPeak; // 04 Bytes +#endif + // new "V2" audiosync struct - 44 Bytes + struct __attribute__ ((packed)) audioSyncPacket { // WLEDMM "packed" ensures that there are no additional gaps + char header[6]; // 06 Bytes offset 0 - "00002" for protocol version 2 ( includes \0 for c-style string termination) + uint8_t pressure[2]; // 02 Bytes, offset 6 - sound pressure as fixed point (8bit integer, 8bit fraction) + float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting + float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting + uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude + uint8_t frameCounter; // 01 Bytes offset 17 - rolling counter to track duplicate/out of order packets + uint8_t fftResult[16]; // 16 Bytes offset 18 - 16 GEQ channels, each channel has one byte (uint8_t) + uint16_t zeroCrossingCount; // 02 Bytes, offset 34 - number of zero crossings seen in 23ms + float FFT_Magnitude; // 04 Bytes offset 36 - largest FFT result from a single run (raw value, can go up to 4096) + float FFT_MajorPeak; // 04 Bytes offset 40 - frequency (Hz) of largest FFT result }; - // old "V1" audiosync struct - 83 Bytes - for backwards compatibility + // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total - for backwards compatibility struct audioSyncPacket_v1 { char header[6]; // 06 Bytes uint8_t myVals[32]; // 32 Bytes - int sampleAgc; // 04 Bytes - int sampleRaw; // 04 Bytes + int32_t sampleAgc; // 04 Bytes + int32_t sampleRaw; // 04 Bytes float sampleAvg; // 04 Bytes bool samplePeak; // 01 Bytes uint8_t fftResult[16]; // 16 Bytes @@ -610,33 +1184,46 @@ class AudioReactive : public Usermod { double FFT_MajorPeak; // 08 Bytes }; + #define UDPSOUND_MAX_PACKET 96 // max packet size for audiosync, with a bit of "headroom" + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + #if defined(SR_ENABLE_DEFAULT) || defined(UM_AUDIOREACTIVE_ENABLE) + bool enabled = true; // WLEDMM + #else bool enabled = false; + #endif bool initDone = false; // variables for UDP sound sync - WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) + WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) unsigned long lastTime = 0; // last time of running UDP Microphone Sync +#if defined(WLEDMM_FASTPATH) + const uint16_t delayMs = 5; // I don't want to sample too often and overload WLED +#else const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED +#endif uint16_t audioSyncPort= 11988;// default port for UDP sound sync + bool updateIsRunning = false; // true during OTA. + +#ifdef ARDUINO_ARCH_ESP32 // used for AGC int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) double control_integrated = 0.0; // persistent across calls to agcAvg(); "integrator control" = accumulated error // variables used by getSample() and agcAvg() - int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed - double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controler. + double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller float expAdjF = 0.0f; // Used for exponential filter. float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) int16_t rawSampleAgc = 0; // not smoothed AGC sample +#endif // variables used in effects - float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc + float soundPressure = 0; // Sound Pressure estimation, based on microphone raw readings. 0 ->5db, 255 ->105db // used to feed "Info" Page unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket @@ -649,7 +1236,9 @@ class AudioReactive : public Usermod { static const char _name[]; static const char _enabled[]; static const char _inputLvl[]; +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) static const char _analogmic[]; +#endif static const char _digitalmic[]; static const char UDP_SYNC_HEADER[]; static const char UDP_SYNC_HEADER_v1[]; @@ -661,22 +1250,30 @@ class AudioReactive : public Usermod { //////////////////// void logAudio() { - if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable + if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & AUDIOSYNC_REC) == 0))) return; // no audio available #ifdef MIC_LOGGER // Debugging functions for audio input and sound processing. Comment out the values you want to see - PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); - PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); - //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); - PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); + PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines + //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines + //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); + #ifdef ARDUINO_ARCH_ESP32 + PLOT_PRINT("micMin:"); PLOT_PRINT(0.5f * micReal_min); PLOT_PRINT("\t"); // scaled down to 50%, for better readability + PLOT_PRINT("micMax:"); PLOT_PRINT(0.5f * micReal_max); PLOT_PRINT("\t"); // scaled down to 50% + //PLOT_PRINT("micAvg:"); PLOT_PRINT(0.5f * micReal_avg); PLOT_PRINT("\t"); // scaled down to 50% + //PLOT_PRINT("micDC:"); PLOT_PRINT(0.5f * (micReal_min + micReal_max)/2.0f);PLOT_PRINT("\t"); // scaled down to 50% + PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines + PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines + // //PLOT_PRINT("filtmicMin:"); PLOT_PRINT(0.5f * micReal_min2); PLOT_PRINT("\t"); // scaled down to 50% + // //PLOT_PRINT("filtmicMax:"); PLOT_PRINT(0.5f * micReal_max2); PLOT_PRINT("\t"); // scaled down to 50% //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); - //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); - //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); + #endif PLOT_PRINTLN(); + PLOT_FLUSH(); #endif #ifdef FFT_SAMPLING_LOG @@ -686,7 +1283,7 @@ class AudioReactive : public Usermod { PLOT_PRINT("\t"); } PLOT_PRINTLN(); - #endif + #endif // OPTIONS are in the following format: Description \n Option // @@ -730,6 +1327,7 @@ class AudioReactive : public Usermod { #endif // FFT_SAMPLING_LOG } // logAudio() +#ifdef ARDUINO_ARCH_ESP32 ////////////////////// // Audio Processing // @@ -744,13 +1342,13 @@ class AudioReactive : public Usermod { * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal * 3. the amplification depends on signal level: * a) normal zone - very slow adjustment - * b) emergency zome (<10% or >90%) - very fast adjustment + * b) emergency zone (<10% or >90%) - very fast adjustment */ void agcAvg(unsigned long the_time) { const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function - float lastMultAgc = multAgc; // last muliplier used + float lastMultAgc = multAgc; // last multiplier used float multAgcTemp = multAgc; // new multiplier float tmpAgc = sampleReal * multAgc; // what-if amplified signal @@ -790,13 +1388,13 @@ class AudioReactive : public Usermod { if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) - control_integrated += control_error * 0.002 * 0.25; // 2ms = intgration time; 0.25 for damping + control_integrated += control_error * 0.002 * 0.25; // 2ms = integration time; 0.25 for damping else - control_integrated *= 0.9; // spin down that beasty integrator + control_integrated *= 0.9; // spin down that integrator beast // apply PI Control tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain - if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower emergy zone + if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower emergency zone multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; } else { // "normal zone" @@ -804,7 +1402,7 @@ class AudioReactive : public Usermod { multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; } - // limit amplification again - PI controler sometimes "overshoots" + // limit amplification again - PI controller sometimes "overshoots" //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; @@ -819,13 +1417,26 @@ class AudioReactive : public Usermod { // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc multAgc = multAgcTemp; + if (micQuality > 0) { + if (micQuality > 1) { + rawSampleAgc = 0.95f * tmpAgc + 0.05f * (float)rawSampleAgc; // raw path + sampleAgc += 0.95f * (tmpAgc - sampleAgc); // smooth path + } else { + rawSampleAgc = 0.70f * tmpAgc + 0.30f * (float)rawSampleAgc; // min filtering path + sampleAgc += 0.70f * (tmpAgc - sampleAgc); + } + } else { +#if defined(WLEDMM_FASTPATH) + rawSampleAgc = 0.65f * tmpAgc + 0.35f * (float)rawSampleAgc; +#else rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; +#endif // update smoothed AGC sample if (fabsf(tmpAgc) < 1.0f) sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero else sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path - + } sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value last_soundAgc = soundAgc; } // agcAvg() @@ -834,61 +1445,82 @@ class AudioReactive : public Usermod { void getSample() { float sampleAdj; // Gain adjusted sample value - float tmpSample; // An interim sample variable used for calculatioins. - const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. + float tmpSample; // An interim sample variable used for calculations. + const float weighting = 0.18f; // Exponential filter weighting. Will be adjustable in a future release. + const float weighting2 = 0.073f; // Exponential filter weighting, for rising signal (a bit more robust against spikes) const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function + static bool isFrozen = false; + static bool haveSilence = true; + static unsigned long lastSoundTime = 0; // for delaying un-freeze + static unsigned long startuptime = 0; // "fast freeze" mode: do not interfere during first 12 seconds (filter startup time) + + if (startuptime == 0) startuptime = millis(); // fast freeze mode - remember filter startup time + if ((micLevelMethod < 1) || !isFrozen) { // following the input level, UNLESS mic Level was frozen + micLev += (micDataReal-micLev) / 12288.0f; + } - #ifdef WLED_DISABLE_SOUND - micIn = inoise8(millis(), millis()); // Simulated analog read - micDataReal = micIn; - #else - #ifdef ARDUINO_ARCH_ESP32 - micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; - #else - // this is the minimal code for reading analog mic input on 8266. - // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. - static unsigned long lastAnalogTime = 0; - static float lastAnalogValue = 0.0f; - if (millis() - lastAnalogTime > 20) { - micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. - lastAnalogTime = millis(); - lastAnalogValue = micDataReal; - yield(); - } else micDataReal = lastAnalogValue; - micIn = int(micDataReal); - #endif - #endif - - micLev += (micDataReal-micLev) / 12288.0f; - if(micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align MicLev to lowest input signal + if(micDataReal < (micLev-0.24)) { // MicLev above input signal: + micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // always align MicLev to lowest input signal + if (!haveSilence) isFrozen = true; // freeze mode: freeze micLevel so it cannot rise again + } - micIn -= micLev; // Let's center it to 0 now // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. float micInNoDC = fabsf(micDataReal - micLev); - expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); + + if ((micInNoDC > expAdjF) && (expAdjF > soundSquelch)) // MicIn rising, and above squelch threshold? + expAdjF = (weighting2 * micInNoDC + (1.0f-weighting2) * expAdjF); // rise slower + else + expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); // fall faster + expAdjF = fabsf(expAdjF); // Now (!) take the absolute value - expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate - if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; // do something meaningfull when "squelch = 0" + if ((micLevelMethod == 2) && !haveSilence && (expAdjF >= (1.5f * float(soundSquelch)))) + isFrozen = true; // fast freeze mode: freeze micLevel once the volume rises 50% above squelch + + // simple noise gate + if ((expAdjF <= soundSquelch) || ((soundSquelch == 0) && (expAdjF < 0.25f))) { + expAdjF = 0.0f; + micInNoDC = 0.0f; + } + + if (expAdjF <= 0.5f) + haveSilence = true; + else { + lastSoundTime = millis(); + haveSilence = false; + } + + // un-freeze micLev + if (micLevelMethod == 0) isFrozen = false; + if ((micLevelMethod == 1) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 4000)) isFrozen = false; // normal freeze: 4 seconds silence needed + if ((micLevelMethod == 2) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 6000)) isFrozen = false; // fast freeze: 6 seconds silence needed + if ((micLevelMethod == 2) && (millis() - startuptime < 12000)) isFrozen = false; // fast freeze: no freeze in first 12 seconds (filter startup phase) tmpSample = expAdjF; - micIn = abs(micIn); // And get the absolute value of each sample - sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment - sampleReal = tmpSample; + // Adjust the gain. with inputLevel adjustment. + if (micQuality > 0) { + sampleAdj = micInNoDC * sampleGain / 40.0f * inputLevel/128.0f + micInNoDC / 16.0f; // ... using unfiltered sample + sampleReal = micInNoDC; + } else { + sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // ... using pre-filtered sample + sampleReal = tmpSample; + } - sampleAdj = fmax(fmin(sampleAdj, 255), 0); // Question: why are we limiting the value to 8 bits ??? + sampleAdj = fmax(fmin(sampleAdj, 255.0f), 0.0f); // Question: why are we limiting the value to 8 bits ??? sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! // keep "peak" sample, but decay value if current sample is below peak if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { sampleMax = sampleMax + 0.5f * (sampleReal - sampleMax); // new peak - with some filtering - // another simple way to detect samplePeak - if ((binNum < 10) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { +#if 1 + // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume + if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { samplePeak = true; timeOfPeak = millis(); udpSamplePeak = true; } +#endif } else { if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly @@ -897,11 +1529,75 @@ class AudioReactive : public Usermod { } if (sampleMax < 0.5f) sampleMax = 0.0f; + if (micQuality > 0) { + if (micQuality > 1) sampleAvg += 0.95f * (sampleAdj - sampleAvg); + else sampleAvg += 0.70f * (sampleAdj - sampleAvg); + } else { +#if defined(WLEDMM_FASTPATH) + sampleAvg = ((sampleAvg * 11.0f) + sampleAdj) / 12.0f; // make reactions a bit more "crisp" in fastpath mode +#else sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. +#endif + } sampleAvg = fabsf(sampleAvg); // make sure we have a positive value } // getSample() + // current sensitivity, based on AGC gain (multAgc) + float getSensitivity() + { + // start with AGC gain factor + float tmpSound = multAgc; + // experimental: this gives you a calculated "real gain" + // if ((sampleAvg> 1.0) && (sampleReal > 0.05)) tmpSound = (float)sampleRaw / sampleReal; // calculate gain from sampleReal + // else tmpSound = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // silence --> use values from user settings + + if (soundAgc == 0) + tmpSound = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // AGC off -> use non-AGC gain from presets + else + tmpSound /= (float)sampleGain/40.0f + 1.0f/16.0f; // AGC ON -> scale value so 1 = middle value + + // scale to 0..255. Actually I'm not absolutely happy with this, but it works + if (tmpSound > 1.0) tmpSound = sqrtf(tmpSound); + if (tmpSound > 1.25) tmpSound = ((tmpSound-1.25f)/3.42f) +1.25f; + // we have a value now that should be between 0 and 4 (representing gain 1/16 ... 16.0) + return fminf(fmaxf(128.0*tmpSound -6.0f, 0), 255.0); // return scaled non-inverted value // "-6" to ignore values below 1/24 + } + + // estimate sound pressure, based on some assumptions : + // * sample max = 32676 -> Acoustic overload point --> 105db ==> 255 + // * sample < squelch -> just above hearing level --> 5db ==> 0 + // see https://en.wikipedia.org/wiki/Sound_pressure#Examples_of_sound_pressure + // use with I2S digital microphones. Expect stupid values for analog in, and with Line-In !! + float estimatePressure() { + // some constants + constexpr float logMinSample = 0.8329091229351f; // ln(2.3) + constexpr float sampleMin = 2.3f; + constexpr float logMaxSample = 10.1895683436f; // ln(32767 - 6144) + constexpr float sampleMax = 32767.0f - 6144.0f; + + // take the max sample from last I2S batch. + float micSampleMax = fabsf(sampleReal); // from getSample() - nice results, however a bit distorted by MicLev processing + //float micSampleMax = fabsf(micDataReal); // from FFTCode() - better source, but more flickering + if (dmType == 0) micSampleMax *= 2.0f; // correction for ADC analog + //if (dmType == 4) micSampleMax *= 16.0f; // correction for I2S Line-In + if (dmType == 5) micSampleMax *= 2.0f; // correction for PDM + if (dmType == 4) { // I2S Line-In. This is a dirty trick to make sound pressure look interesting for line-in (which doesn't have "sound pressure" as its not a microphone) + micSampleMax /= 11.0f; // reduce to max 128 + micSampleMax *= micSampleMax; // blow up --> max 16000 + } + // make sure we are in expected ranges + if(micSampleMax <= sampleMin) return 0.0f; + if(micSampleMax >= sampleMax) return 255.0f; + + // apply logarithmic scaling + float scaledvalue = logf(micSampleMax); + scaledvalue = (scaledvalue - logMinSample) / (logMaxSample - logMinSample); // 0...1 + return fminf(fmaxf(256.0*scaledvalue, 0), 255.0); // scaled value + } +#endif + + /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) */ @@ -914,7 +1610,7 @@ class AudioReactive : public Usermod { if (limiterOn == false) return; long delta_time = millis() - last_time; - delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up float deltaSample = volumeSmth - last_volumeSmth; if (attackTime > 0) { // user has defined attack time > 0 @@ -932,6 +1628,38 @@ class AudioReactive : public Usermod { last_time = millis(); } + // MM experimental: limiter to smooth GEQ samples (only for UDP sound receiver mode) + // target value (if gotNewSample) : fftCalc + // last filtered value: fftAvg + void limitGEQDynamics(bool gotNewSample) { + constexpr float bigChange = 202; // just a representative number - a large, expected sample value + constexpr float smooth = 0.8f; // a bit of filtering + static unsigned long last_time = 0; + + if (limiterOn == false) return; + + if (gotNewSample) { // take new FFT samples as target values + for(unsigned i=0; i < NUM_GEQ_CHANNELS; i++) { + fftCalc[i] = fftResult[i]; + fftResult[i] = fftAvg[i]; + } + } + + long delta_time = millis() - last_time; + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up + float maxAttack = (attackTime <= 0) ? 255.0f : (bigChange * float(delta_time) / float(attackTime)); + float maxDecay = (decayTime <= 0) ? -255.0f : (-bigChange * float(delta_time) / float(decayTime)); + + for(unsigned i=0; i < NUM_GEQ_CHANNELS; i++) { + float deltaSample = fftCalc[i] - fftAvg[i]; + if (deltaSample > maxAttack) deltaSample = maxAttack; + if (deltaSample < maxDecay) deltaSample = maxDecay; + deltaSample = deltaSample * smooth; + fftAvg[i] = fmaxf(0.0f, fminf(255.0f, fftAvg[i] + deltaSample)); + fftResult[i] = fftAvg[i]; + } + last_time = millis(); + } ////////////////////// // UDP Sound Sync // @@ -943,74 +1671,139 @@ class AudioReactive : public Usermod { // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection static unsigned long last_connection_attempt = 0; - if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled + if ((audioSyncPort <= 0) || (audioSyncEnabled == AUDIOSYNC_NONE)) return; // Sound Sync not enabled + if (!(apActive || WLED_CONNECTED || interfacesInited)) { + if (udpSyncConnected) { + udpSyncConnected = false; + fftUdp.stop(); + receivedFormat = 0; + DEBUGSR_PRINTLN(F("AR connectUDPSoundSync(): connection lost, UDP closed.")); + } + return; // neither AP nor other connections available + } if (udpSyncConnected) return; // already connected - if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds + if (updateIsRunning) return; // don't reconnect during OTA // if we arrive here, we need a UDP connection but don't have one last_connection_attempt = millis(); connected(); // try to start UDP } - +#ifdef ARDUINO_ARCH_ESP32 void transmitAudioData() { if (!udpSyncConnected) return; + static uint8_t frameCounter = 0; //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); audioSyncPacket transmitData; + memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized + strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); // transmit samples that were not modified by limitSampleDynamics() transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; transmitData.samplePeak = udpSamplePeak ? 1:0; udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it - transmitData.reserved1 = 0; + transmitData.frameCounter = frameCounter; + transmitData.zeroCrossingCount = zeroCrossingCount; for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { - transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); + transmitData.fftResult[i] = fftResult[i]; } + // WLEDMM transmit soundPressure as 16 bit fixed point + uint32_t pressure16bit = max(0.0f, soundPressure) * 256.0f; // convert to fixed point, remove negative values + uint16_t pressInt = pressure16bit / 256; // integer part + uint16_t pressFract = pressure16bit % 256; // faction part + if (pressInt > 255) pressInt = 255; // saturation at 255 + transmitData.pressure[0] = (uint8_t)pressInt; + transmitData.pressure[1] = (uint8_t)pressFract; + transmitData.FFT_Magnitude = my_magnitude; transmitData.FFT_MajorPeak = FFT_MajorPeak; - fftUdp.beginMulticastPacket(); - fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); - fftUdp.endPacket(); - return; + if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error + fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); + fftUdp.endPacket(); + } + + frameCounter++; } // transmitAudioData() - +#endif static bool isValidUdpSyncVersion(const char *header) { - return strncmp_P(header, PSTR(UDP_SYNC_HEADER), 6) == 0; + return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; } static bool isValidUdpSyncVersion_v1(const char *header) { - return strncmp_P(header, PSTR(UDP_SYNC_HEADER_v1), 6) == 0; + return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; } - void decodeAudioData(int packetSize, uint8_t *fftBuff) { - audioSyncPacket *receivedPacket = reinterpret_cast(fftBuff); + bool decodeAudioData(int packetSize, uint8_t *fftBuff) { + if((0 == packetSize) || (nullptr == fftBuff)) return false; // sanity check + //audioSyncPacket *receivedPacket = reinterpret_cast(fftBuff); + audioSyncPacket receivedPacket; + memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean + memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles + + // validate sequence, discard out-of-sequence packets + static uint8_t lastFrameCounter = 0; + // add info for UI + if ((receivedPacket.frameCounter > 0) && (lastFrameCounter > 0)) receivedFormat = 3; // v2+ + else receivedFormat = 2; // v2 + // check sequence + bool sequenceOK = false; + if(receivedPacket.frameCounter > lastFrameCounter) sequenceOK = true; // sequence OK + if((lastFrameCounter < 12) && (receivedPacket.frameCounter > 248)) sequenceOK = false; // prevent sequence "roll-back" due to late packets (1->254) + if((lastFrameCounter > 248) && (receivedPacket.frameCounter < 12)) sequenceOK = true; // handle roll-over (255 -> 0) + if(audioSyncSequence == false) sequenceOK = true; // sequence checking disabled by user + if((sequenceOK == false) && (receivedPacket.frameCounter != 0)) { // always accept "0" - its the legacy value + DEBUGSR_PRINTF("Skipping audio frame out of order or duplicated - %u vs %u\n", lastFrameCounter, receivedPacket.frameCounter); + return false; // reject out-of sequence frame + } + else { + lastFrameCounter = receivedPacket.frameCounter; + } + // update samples for effects - volumeSmth = fmaxf(receivedPacket->sampleSmth, 0.0f); - volumeRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); + volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); + volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); +#ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = volumeRaw; sampleAvg = volumeSmth; rawSampleAgc = volumeRaw; sampleAgc = volumeSmth; - multAgc = 1.0f; + multAgc = 1.0f; +#endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); if (!samplePeak) { - samplePeak = receivedPacket->samplePeak >0 ? true:false; + samplePeak = receivedPacket.samplePeak >0 ? true:false; if (samplePeak) timeOfPeak = millis(); //userVar1 = samplePeak; } - //These values are only available on the ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; - my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); + //These values are only computed by ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; + my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects +#ifdef ARDUINO_ARCH_ESP32 + FFT_MajPeakSmth = FFT_MajPeakSmth + 0.42f * (FFT_MajorPeak - FFT_MajPeakSmth); // simulate smooth value +#endif + agcSensitivity = 128.0f; // substitute - V2 format does not include this value + zeroCrossingCount = receivedPacket.zeroCrossingCount; + + // WLEDMM extract soundPressure + if ((receivedPacket.pressure[0] != 0) || (receivedPacket.pressure[1] != 0)) { + // found something in gap "reserved2" + soundPressure = float(receivedPacket.pressure[1]) / 256.0f; // fractional part + soundPressure += float(receivedPacket.pressure[0]); // integer part + } else { + soundPressure = volumeSmth; // fallback + } + + return true; } void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { @@ -1018,12 +1811,14 @@ class AudioReactive : public Usermod { // update samples for effects volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample +#ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; sampleAgc = volumeSmth; rawSampleAgc = volumeRaw; multAgc = 1.0f; +#endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); @@ -1037,38 +1832,66 @@ class AudioReactive : public Usermod { my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0); FFT_Magnitude = my_magnitude; FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0, 11025.0); // restrict value to range expected by effects + soundPressure = volumeSmth; // substitute - V1 format does not include this value + agcSensitivity = 128.0f; // substitute - V1 format does not include this value } - bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. - { + bool receiveAudioData() { if (!udpSyncConnected) return false; bool haveFreshData = false; + size_t packetSize = 0; + static uint8_t fftUdpBuffer[UDPSOUND_MAX_PACKET + 1] = {0}; + size_t lastValidPacketSize = 0; + + // Loop to read all available packets + while (true) { + #if __cpp_exceptions + try { + packetSize = fftUdp.parsePacket(); + } catch (...) { + packetSize = 0; + #ifdef ARDUINO_ARCH_ESP32 + fftUdp.flush(); + #endif + DEBUG_PRINTLN(F("receiveAudioData: parsePacket out of memory exception caught!")); + USER_FLUSH(); + continue; // Skip to next iteration + } + #else + packetSize = fftUdp.parsePacket(); + #endif + + #ifdef ARDUINO_ARCH_ESP32 + if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) { + fftUdp.flush(); + continue; // Skip invalid packets + } + #endif - size_t packetSize = fftUdp.parsePacket(); - if (packetSize > 5) { - //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - uint8_t fftBuff[packetSize]; - fftUdp.read(fftBuff, packetSize); + if (packetSize == 0) break; // No more packets available - // VERIFY THAT THIS IS A COMPATIBLE PACKET - if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { - decodeAudioData(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); - haveFreshData = true; + if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { + fftUdp.read(fftUdpBuffer, packetSize); + lastValidPacketSize = packetSize; + } + } + + // Process only the last valid packet + if (lastValidPacketSize > 0) { + if (lastValidPacketSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftUdpBuffer))) { receivedFormat = 2; + haveFreshData = decodeAudioData(lastValidPacketSize, fftUdpBuffer); + } else if (lastValidPacketSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftUdpBuffer))) { + decodeAudioData_v1(lastValidPacketSize, fftUdpBuffer); + receivedFormat = 1; + haveFreshData = true; } else { - if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { - decodeAudioData_v1(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); - haveFreshData = true; - receivedFormat = 1; - } else receivedFormat = 0; // unknown format + receivedFormat = 0; // unknown format } } return haveFreshData; } - ////////////////////// // usermod functions// ////////////////////// @@ -1088,12 +1911,12 @@ class AudioReactive : public Usermod { // usermod exchangeable data // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers um_data = new um_data_t; - um_data->u_size = 8; + um_data->u_size = 12; um_data->u_type = new um_types_t[um_data->u_size]; um_data->u_data = new void*[um_data->u_size]; um_data->u_data[0] = &volumeSmth; //*used (New) um_data->u_type[0] = UMT_FLOAT; - um_data->u_data[1] = &volumeRaw; // used (New) + um_data->u_data[1] = &volumeRaw; // used (New) um_data->u_type[1] = UMT_UINT16; um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) um_data->u_type[2] = UMT_BYTE_ARR; @@ -1101,29 +1924,52 @@ class AudioReactive : public Usermod { um_data->u_type[3] = UMT_BYTE; um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) um_data->u_type[4] = UMT_FLOAT; - um_data->u_data[5] = &my_magnitude; // used (New) + um_data->u_data[5] = &my_magnitude; // used (New) um_data->u_type[5] = UMT_FLOAT; um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) um_data->u_type[6] = UMT_BYTE; um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) um_data->u_type[7] = UMT_BYTE; +#ifdef ARDUINO_ARCH_ESP32 + um_data->u_data[8] = &FFT_MajPeakSmth; // new + um_data->u_type[8] = UMT_FLOAT; +#else + um_data->u_data[8] = &FFT_MajorPeak; // substitute for 8266 + um_data->u_type[8] = UMT_FLOAT; +#endif + um_data->u_data[9] = &soundPressure; // used (New) + um_data->u_type[9] = UMT_FLOAT; + um_data->u_data[10] = &agcSensitivity; // used (New) - dummy value on 8266 + um_data->u_type[10] = UMT_FLOAT; + um_data->u_data[11] = &zeroCrossingCount; // for auto playlist usermod + um_data->u_type[11] = UMT_UINT16; } - // Reset I2S peripheral for good measure +#ifdef ARDUINO_ARCH_ESP32 + + // Reset I2S peripheral for good measure - not needed in esp-idf v4.4.x and later. + #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed #if !defined(CONFIG_IDF_TARGET_ESP32C3) delay(100); periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 #endif + #endif delay(100); // Give that poor microphone some time to setup. - useBandPassFilter = false; + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) + && ((dmType == 1) || (dmType == 4)) ) dmType = 51; // dummy user support: SCK == -1 --means--> PDM microphone + #endif + + useInputFilter = 2; // default: DC blocker switch (dmType) { #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) // stub cases for not-yet-supported I2S modes on other ESP32 chips case 0: //ADC analog #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) case 5: //PDM Microphone + case 51: //legacy PDM Microphone #endif #endif case 1: @@ -1134,9 +1980,16 @@ class AudioReactive : public Usermod { break; case 2: DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); + //useInputFilter = 0; // in case you need to disable low-cut software filtering audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); delay(100); - if (audioSource) audioSource->initialize(sdaPin, sclPin, i2swsPin, i2ssdPin, i2sckPin, mclkPin); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; case 3: DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); @@ -1147,6 +2000,7 @@ class AudioReactive : public Usermod { case 4: DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); + //audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f, false); // I2S SLAVE mode - does not work, unfortunately delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; @@ -1154,16 +2008,89 @@ class AudioReactive : public Usermod { case 5: DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); - useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) + useInputFilter = 1; // PDM bandpass filter - this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); + break; + case 51: + DEBUGSR_PRINT(F("AR: Legacy PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + useInputFilter = 1; // PDM bandpass filter delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); break; #endif + case 6: + #ifdef use_es8388_mic + DEBUGSR_PRINTLN(F("AR: ES8388 Source (Mic)")); + #else + DEBUGSR_PRINTLN(F("AR: ES8388 Source (Line-In)")); + #endif + audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 7: + #ifdef use_wm8978_mic + DEBUGSR_PRINTLN(F("AR: WM8978 Source (Mic)")); + #else + DEBUGSR_PRINTLN(F("AR: WM8978 Source (Line-In)")); + #endif + audioSource = new WM8978Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 8: + DEBUGSR_PRINTLN(F("AR: AC101 Source (Line-In)")); + audioSource = new AC101Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + case 9: + DEBUGSR_PRINTLN(F("AR: ES8311 Source (Mic)")); + audioSource = new ES8311Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + + case 255: // falls through + case 254: // dummy "network receive only" driver + if (audioSource) delete audioSource; + audioSource = nullptr; + disableSoundProcessing = true; + audioSyncEnabled = AUDIOSYNC_REC; // force udp sound receive mode + break; + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // ADC over I2S is only possible on "classic" ESP32 case 0: default: DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); + useInputFilter = 1; // PDM bandpass filter seems to work well for analog, too audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); delay(100); if (audioSource) audioSource->initialize(audioPin); @@ -1172,22 +2099,55 @@ class AudioReactive : public Usermod { } delay(250); // give microphone enough time to initialise - if (!audioSource) enabled = false; // audio failed to initialise - if (enabled) onUpdateBegin(false); // create FFT task - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed - if (enabled) disableSoundProcessing = false; // all good - enable audio processing + if (!audioSource && (dmType < 254)) enabled = false; // audio failed to initialise +#endif + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network +#ifdef ARDUINO_ARCH_ESP32 + if (audioSource && FFT_Task == nullptr) enabled = false; // FFT task creation failed if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync - #ifdef WLED_DEBUG - DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #else - DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #endif + + if (dmType < 254) { USER_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));} + else { USER_PRINTLN(F("AR: No sound input driver configured - network receive only."));} disableSoundProcessing = true; + } else { + USER_PRINTLN(F("AR: sound input driver initialized successfully.")); } - +#endif + if (enabled) disableSoundProcessing = false; // all good - enable audio processing + // try to start UDP + last_UDPTime = 0; + receivedFormat = 0; + delay(100); if (enabled) connectUDPSoundSync(); initDone = true; + DEBUGSR_PRINT(F("AR: init done, enabled = ")); + DEBUGSR_PRINTLN(enabled ? F("true.") : F("false.")); + USER_FLUSH(); + + // dump audiosync data layout + #if defined(SR_DEBUG) + { + audioSyncPacket data; + USER_PRINTF("\naudioSyncPacket_v1 size = %d\n", sizeof(audioSyncPacket_v1)); // size 88 + USER_PRINTF("audioSyncPacket size = %d\n", sizeof(audioSyncPacket)); // size 44 + USER_PRINTF("| char header[6] offset = %2d size = %2d\n", offsetof(audioSyncPacket, header[0]), sizeof(data.header)); // offset 0 size 6 + USER_PRINTF("| uint8_t pressure[2] offset = %2d size = %2d\n", offsetof(audioSyncPacket, pressure[0]), sizeof(data.pressure)); // offset 6 size 2 + USER_PRINTF("| float sampleRaw offset = %2d size = %2d\n", offsetof(audioSyncPacket, sampleRaw), sizeof(data.sampleRaw)); // offset 8 size 4 + USER_PRINTF("| float sampleSmth offset = %2d size = %2d\n", offsetof(audioSyncPacket, sampleSmth), sizeof(data.sampleSmth)); // offset 12 size 4 + USER_PRINTF("| uint8_t samplePeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, samplePeak), sizeof(data.samplePeak)); // offset 16 size 1 + USER_PRINTF("| uint8_t frameCounter offset = %2d size = %2d\n", offsetof(audioSyncPacket, frameCounter), sizeof(data.frameCounter)); // offset 17 size 1 + USER_PRINTF("| uint8_t fftResult[16] offset = %2d size = %2d\n", offsetof(audioSyncPacket, fftResult[0]), sizeof(data.fftResult)); // offset 18 size 16 + USER_PRINTF("| uint16_t zeroCrossingCount offset = %2d size = %2d\n", offsetof(audioSyncPacket, zeroCrossingCount), sizeof(data.zeroCrossingCount)); // offset 34 size 2 + USER_PRINTF("| float FFT_Magnitude offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_Magnitude), sizeof(data.FFT_Magnitude));// offset 36 size 4 + USER_PRINTF("| float FFT_MajorPeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_MajorPeak), sizeof(data.FFT_MajorPeak));// offset 40 size 4 + USER_PRINTLN(); USER_FLUSH(); + } + #endif + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } @@ -1200,15 +2160,28 @@ class AudioReactive : public Usermod { if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection udpSyncConnected = false; fftUdp.stop(); + receivedFormat = 0; + DEBUGSR_PRINTLN(F("AR connected(): old UDP connection closed.")); } - if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { - #ifndef ESP8266 + if ((audioSyncPort > 0) && (audioSyncEnabled > AUDIOSYNC_NONE)) { + #ifdef ARDUINO_ARCH_ESP32 udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); #else udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); #endif + receivedFormat = 0; + if (udpSyncConnected) last_UDPTime = millis(); + if (apActive && !(WLED_CONNECTED)) { + DEBUGSR_PRINTLN(udpSyncConnected ? F("AR connected(): UDP: connected using AP.") : F("AR connected(): UDP is disconnected (AP).")); + } else { + DEBUGSR_PRINTLN(udpSyncConnected ? F("AR connected(): UDP: connected to WIFI.") : F("AR connected(): UDP is disconnected (Wifi).")); + } } + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } @@ -1232,7 +2205,18 @@ class AudioReactive : public Usermod { return; } // We cannot wait indefinitely before processing audio data - if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice + if (strip.isServicing() && (millis() - lastUMRun < 2)) return; // WLEDMM isServicing() is the critical part (be nice, but not too nice) + + // sound sync "receive or local" + bool useNetworkAudio = false; + if (audioSyncEnabled > AUDIOSYNC_SEND) { // we are in "receive" or "receive+local" mode + if (udpSyncConnected && ((millis() - last_UDPTime) <= AUDIOSYNC_IDLE_MS)) + useNetworkAudio = true; + else + useNetworkAudio = false; + if (audioSyncEnabled == AUDIOSYNC_REC) + useNetworkAudio = true; // don't fall back to local audio in standard "receive mode" + } // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed @@ -1243,41 +2227,57 @@ class AudioReactive : public Usermod { ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed { #ifdef WLED_DEBUG - if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" + if ((disableSoundProcessing == false) && (audioSyncEnabled < AUDIOSYNC_REC)) { // we just switched to "disabled" DEBUG_PRINTLN("[AR userLoop] realtime mode active - audio processing suspended."); DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); } #endif disableSoundProcessing = true; + useNetworkAudio = false; } else { - #ifdef WLED_DEBUG - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) + if ((disableSoundProcessing == true) && (audioSyncEnabled < AUDIOSYNC_REC) && audioSource->isInitialized()) { // we just switched to "enabled" DEBUG_PRINTLN("[AR userLoop] realtime mode ended - audio processing resumed."); DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); } #endif - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping + if ((disableSoundProcessing == true) && (audioSyncEnabled != AUDIOSYNC_REC)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping disableSoundProcessing = false; } - if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode - if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode - if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source - + if (audioSyncEnabled == AUDIOSYNC_REC) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode + if (audioSyncEnabled == AUDIOSYNC_SEND) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode +#ifdef ARDUINO_ARCH_ESP32 + if (!audioSource || !audioSource->isInitialized()) { // no audio source + disableSoundProcessing = true; + if (audioSyncEnabled > AUDIOSYNC_SEND) useNetworkAudio = true; + } + if ((audioSyncEnabled == AUDIOSYNC_REC_PLUS) && useNetworkAudio) disableSoundProcessing = true; // UDP sound receiving - disable local audio + + #ifdef SR_DEBUG + // debug info in case that task stack usage changes + static unsigned int minLoopStackFree = UINT32_MAX; + unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); + if (minLoopStackFree > stackFree) { + minLoopStackFree = stackFree; + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minLoopStackFree); //WLEDMM + } + #endif // Only run the sampling code IF we're not in Receive mode or realtime mode - if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { + if ((audioSyncEnabled != AUDIOSYNC_REC) && !disableSoundProcessing && !useNetworkAudio) { if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) unsigned long t_now = millis(); // remember current time int userloopDelay = int(t_now - lastUMRun); if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. - #ifdef WLED_DEBUG - // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. - if ((userloopDelay > 23) && !disableSoundProcessing && (audioSyncEnabled == 0)) { - DEBUG_PRINTF("[AR userLoop] hickup detected -> was inactive for last %d millis!\n", userloopDelay); - } + #if defined(SR_DEBUG) + // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. + // softhack007 disabled temporarily - avoid serial console spam with MANY LEDs and low FPS + //if ((userloopDelay > /*23*/ 65) && !disableSoundProcessing && (audioSyncEnabled == AUDIOSYNC_NONE)) { + //DEBUG_PRINTF("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n", userloopDelay); + //} #endif // run filters, and repeat in case of loop delays (hick-up compensation) @@ -1298,8 +2298,23 @@ class AudioReactive : public Usermod { if (soundAgc) my_magnitude *= multAgc; if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute + // get AGC sensitivity and sound pressure + static unsigned long lastEstimate = 0; +#ifdef WLEDMM_FASTPATH + if (millis() - lastEstimate > 7) { +#else + if (millis() - lastEstimate > 12) { +#endif + lastEstimate = millis(); + agcSensitivity = getSensitivity(); + if (limiterOn) + soundPressure = soundPressure + 0.38f * (estimatePressure() - soundPressure); // dynamics limiter on -> some smoothing + else + soundPressure = soundPressure + 0.95f * (estimatePressure() - soundPressure); // dynamics limiter on -> raw value + } limitSampleDynamics(); } // if (!disableSoundProcessing) +#endif autoResetPeak(); // auto-reset sample peak after strip minShowDelay if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected @@ -1307,18 +2322,49 @@ class AudioReactive : public Usermod { connectUDPSoundSync(); // ensure we have a connection - if needed // UDP Microphone Sync - receive mode - if ((audioSyncEnabled & 0x02) && udpSyncConnected) { + if ((audioSyncEnabled & AUDIOSYNC_REC) && udpSyncConnected) { // Only run the audio listener code if we're in Receive mode static float syncVolumeSmth = 0; bool have_new_sample = false; if (millis() - lastTime > delayMs) { + // DEBUG_PRINTF(F("AR reading at %d compared to %d max\n"), millis() - lastTime, delayMs); // TroyHacks have_new_sample = receiveAudioData(); - if (have_new_sample) last_UDPTime = millis(); + if (have_new_sample) { + last_UDPTime = millis(); + useNetworkAudio = true; // UDP input arrived - use it + } lastTime = millis(); + } else { +#ifdef ARDUINO_ARCH_ESP32 + fftUdp.flush(); // WLEDMM: Flush this if we haven't read it. Does not work on 8266. +#endif + } + if (useNetworkAudio) { + if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample + else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter + limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + limitGEQDynamics(have_new_sample); // WLEDMM experimental: smooth FFT (GEQ) samples } - if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample - else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter - limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + } else { + receivedFormat = 0; + } + + if ( (audioSyncEnabled & AUDIOSYNC_REC) // receive mode + && udpSyncConnected // connected + && (receivedFormat > 0) // we actually received something in the past + && ((millis() - last_UDPTime) > 25000)) { // close connection after 25sec idle + udpSyncConnected = false; + receivedFormat = 0; + fftUdp.stop(); + volumeSmth =0.0f; + volumeRaw =0; + my_magnitude = 0.1; FFT_Magnitude = 0.01; FFT_MajorPeak = 2; + soundPressure = 1.0f; + agcSensitivity = 64.0f; +#ifdef ARDUINO_ARCH_ESP32 + multAgc = 1; +#endif + DEBUGSR_PRINTLN(F("AR loop(): UDP closed due to inactivity.")); } #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) @@ -1330,6 +2376,7 @@ class AudioReactive : public Usermod { #endif // Info Page: keep max sample from last 5 seconds +#ifdef ARDUINO_ARCH_ESP32 if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { sampleMaxTimer = millis(); maxSample5sec = (0.15 * maxSample5sec) + 0.85 *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing @@ -1337,16 +2384,37 @@ class AudioReactive : public Usermod { } else { if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume } +#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing + if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate + if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values + } else { + if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume + } +#endif +#ifdef ARDUINO_ARCH_ESP32 //UDP Microphone Sync - transmit mode - if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { + #if defined(WLEDMM_FASTPATH) + if ((audioSyncEnabled & AUDIOSYNC_SEND) && (haveNewFFTResult || (millis() - lastTime > 24))) { // fastpath: send data once results are ready, or each 25ms as fallback (max sampling time is 23ms) + #else + if ((audioSyncEnabled & AUDIOSYNC_SEND) && (millis() - lastTime > 20)) { // standard: send data each 20ms + #endif + haveNewFFTResult = false; // reset notification // Only run the transmit code IF we're in Transmit mode transmitAudioData(); lastTime = millis(); } - +#endif } +#if defined(_MoonModules_WLED_) && defined(WLEDMM_FASTPATH) + void loop2(void) { + loop(); + } +#endif bool getUMData(um_data_t **data) { @@ -1356,10 +2424,11 @@ class AudioReactive : public Usermod { } +#ifdef ARDUINO_ARCH_ESP32 void onUpdateBegin(bool init) { #ifdef WLED_DEBUG - fftTime = sampleTime = 0; + fftTime = sampleTime = filterTime = 0; #endif // gracefully suspend FFT task (if running) disableSoundProcessing = true; @@ -1376,37 +2445,71 @@ class AudioReactive : public Usermod { memset(fftAvg, 0, sizeof(fftAvg)); memset(fftResult, 0, sizeof(fftResult)); for(int i=(init?0:1); i don't process audio + updateIsRunning = init; + } +#endif +#ifdef ARDUINO_ARCH_ESP32 /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. @@ -1423,7 +2526,7 @@ class AudioReactive : public Usermod { } return false; } - +#endif //////////////////////////// // Settings and Info Page // @@ -1436,7 +2539,9 @@ class AudioReactive : public Usermod { */ void addToJsonInfo(JsonObject& root) { - char myStringBuffer[16]; // buffer for snprintf() +#ifdef ARDUINO_ARCH_ESP32 + char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 +#endif JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); @@ -1454,10 +2559,25 @@ class AudioReactive : public Usermod { infoArr.add(uiDomString); if (enabled) { + bool audioSyncIDLE = false; // true if sound sync is not receiving + +#ifdef ARDUINO_ARCH_ESP32 + // audio sync status + if ((audioSyncEnabled & AUDIOSYNC_REC) && (!udpSyncConnected || (millis() - last_UDPTime > AUDIOSYNC_IDLE_MS))) // connected and nothing received in 2.5sec + audioSyncIDLE = true; + if ((audioSource == nullptr) || (!audioSource->isInitialized())) // local audio not configured + audioSyncIDLE = false; + // Input Level Slider if (disableSoundProcessing == false) { // only show slider when audio processing is running if (soundAgc > 0) { infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies + // show slider value as a number + float post_gain = (float)inputLevel/128.0f; + if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; + post_gain = roundf(post_gain * 100.0f); + snprintf_P(myStringBuffer, 15, PSTR("%3.0f %%"), post_gain); + infoArr.add(myStringBuffer); } else { infoArr = user.createNestedArray(F("Audio Input Level")); } @@ -1470,30 +2590,37 @@ class AudioReactive : public Usermod { uiDomString += F(" />
"); // infoArr.add(uiDomString); } - +#endif // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG - // current Audio input infoArr = user.createNestedArray(F("Audio Source")); - if (audioSyncEnabled & 0x02) { + if ((audioSyncEnabled == AUDIOSYNC_REC) || (!audioSyncIDLE && (audioSyncEnabled == AUDIOSYNC_REC_PLUS))){ // UDP sound sync - receive mode infoArr.add(F("UDP sound sync")); if (udpSyncConnected) { - if (millis() - last_UDPTime < 2500) + if (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS) infoArr.add(F(" - receiving")); else infoArr.add(F(" - idle")); } else { infoArr.add(F(" - no connection")); } +#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 + } else { + infoArr.add(F("sound sync Off")); + } +#else // ESP32 only } else { // Analog or I2S digital input if (audioSource && (audioSource->isInitialized())) { - // audio source sucessfully configured + // audio source successfully configured if (audioSource->getType() == AudioSource::Type_I2SAdc) { infoArr.add(F("ADC analog")); } else { - infoArr.add(F("I2S digital")); + if (dmType != 51) + infoArr.add(F("I2S digital")); + else + infoArr.add(F("legacy I2S PDM")); } // input level or "silence" if (maxSample5sec > 1.0) { @@ -1506,7 +2633,7 @@ class AudioReactive : public Usermod { } else { // error during audio source setup infoArr.add(F("not initialized")); - infoArr.add(F(" - check GPIO config")); + if (dmType < 254) infoArr.add(F(" - check pin settings")); } } @@ -1519,51 +2646,75 @@ class AudioReactive : public Usermod { } // AGC or manual Gain - if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + if ((soundAgc == 0) && (disableSoundProcessing == false) && !(audioSyncEnabled == AUDIOSYNC_REC)) { infoArr = user.createNestedArray(F("Manual Gain")); float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets infoArr.add(roundf(myGain*100.0f) / 100.0f); infoArr.add("x"); } - if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + if ((soundAgc > 0) && (disableSoundProcessing == false) && !(audioSyncEnabled == AUDIOSYNC_REC)) { infoArr = user.createNestedArray(F("AGC Gain")); infoArr.add(roundf(multAgc*100.0f) / 100.0f); infoArr.add("x"); } - +#endif // UDP Sound Sync status infoArr = user.createNestedArray(F("UDP Sound Sync")); if (audioSyncEnabled) { - if (audioSyncEnabled & 0x01) { + if (audioSyncEnabled & AUDIOSYNC_SEND) { infoArr.add(F("send mode")); - if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); - } else if (audioSyncEnabled & 0x02) { + if ((udpSyncConnected) && (millis() - lastTime < AUDIOSYNC_IDLE_MS)) infoArr.add(F(" v2+")); + } else if (audioSyncEnabled == AUDIOSYNC_REC) { infoArr.add(F("receive mode")); + } else if (audioSyncEnabled == AUDIOSYNC_REC_PLUS) { + infoArr.add(F("receive+local mode")); } } else infoArr.add("off"); if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); - if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { + if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS)) { if (receivedFormat == 1) infoArr.add(F(" v1")); if (receivedFormat == 2) infoArr.add(F(" v2")); + if (receivedFormat == 3) { + if (audioSyncSequence) infoArr.add(F(" v2+")); // Sequence checking enabled + else infoArr.add(F(" v2")); + } } - #if defined(WLED_DEBUG) || defined(SR_DEBUG) + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) + #ifdef ARDUINO_ARCH_ESP32 + infoArr = user.createNestedArray(F("I2S cycle time")); + infoArr.add(roundf(fftTaskCycle)/100.0f); + infoArr.add(" ms"); + infoArr = user.createNestedArray(F("Sampling time")); - infoArr.add(float(sampleTime)/100.0f); + infoArr.add(roundf(sampleTime)/100.0f); + infoArr.add(" ms"); + + infoArr = user.createNestedArray(F("Filtering time")); + infoArr.add(roundf(filterTime)/100.0f); infoArr.add(" ms"); infoArr = user.createNestedArray(F("FFT time")); - infoArr.add(float(fftTime)/100.0f); - if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow + infoArr.add(roundf(fftTime)/100.0f); + +#ifdef FFT_USE_SLIDING_WINDOW + unsigned timeBudget = doSlidingFFT ? (FFT_MIN_CYCLE) : fftTaskCycle / 115; +#else + unsigned timeBudget = (FFT_MIN_CYCLE); +#endif + if ((fftTime/100) >= timeBudget) // FFT time over budget -> I2S buffer will overflow infoArr.add("! ms"); - else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability + else if ((fftTime/85 + filterTime/85 + sampleTime/85) >= timeBudget) // FFT time >75% of budget -> risk of instability infoArr.add(" ms!"); else infoArr.add(" ms"); - DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); - DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); + DEBUGSR_PRINTF("AR I2S cycle time: %5.2f ms\n", roundf(fftTaskCycle)/100.0f); + DEBUGSR_PRINTF("AR Sampling time : %5.2f ms\n", roundf(sampleTime)/100.0f); + DEBUGSR_PRINTF("AR filter time : %5.2f ms\n", roundf(filterTime)/100.0f); + DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", roundf(fftTime)/100.0f); + #endif #endif } } @@ -1598,9 +2749,11 @@ class AudioReactive : public Usermod { enabled = usermod[FPSTR(_enabled)].as(); if (prevEnabled != enabled) onUpdateBegin(!enabled); } +#ifdef ARDUINO_ARCH_ESP32 if (usermod[FPSTR(_inputLvl)].is()) { inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); } +#endif } } @@ -1644,7 +2797,7 @@ class AudioReactive : public Usermod { { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; - +#ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); amic["pin"] = audioPin; @@ -1652,6 +2805,11 @@ class AudioReactive : public Usermod { JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); dmic[F("type")] = dmType; + // WLEDMM: align with globals I2C pins + if ((dmType == 2) || (dmType == 6)) { // only for ES7243 and ES8388 + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; // -1 = use global + } JsonArray pinArray = dmic.createNestedArray("pin"); pinArray.add(i2ssdPin); pinArray.add(i2swsPin); @@ -1665,17 +2823,29 @@ class AudioReactive : public Usermod { cfg[F("gain")] = sampleGain; cfg[F("AGC")] = soundAgc; + //WLEDMM: experimental settings + JsonObject poweruser = top.createNestedObject("experiments"); + poweruser[F("micLev")] = micLevelMethod; + poweruser[F("Mic_Quality")] = micQuality; + poweruser[F("freqDist")] = freqDist; + //poweruser[F("freqRMS")] = averageByRMS; + poweruser[F("FFT_Window")] = fftWindow; +#ifdef FFT_USE_SLIDING_WINDOW + poweruser[F("I2S_FastPath")] = doSlidingFFT; +#endif + JsonObject freqScale = top.createNestedObject("frequency"); + freqScale[F("scale")] = FFTScalingMode; + freqScale[F("profile")] = pinkIndex; //WLEDMM +#endif JsonObject dynLim = top.createNestedObject("dynamics"); dynLim[F("limiter")] = limiterOn; dynLim[F("rise")] = attackTime; dynLim[F("fall")] = decayTime; - JsonObject freqScale = top.createNestedObject("frequency"); - freqScale[F("scale")] = FFTScalingMode; - JsonObject sync = top.createNestedObject("sync"); sync[F("port")] = audioSyncPort; sync[F("mode")] = audioSyncEnabled; + sync[F("check_sequence")] = audioSyncSequence; } @@ -1699,8 +2869,18 @@ class AudioReactive : public Usermod { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); +#ifdef ARDUINO_ARCH_ESP32 + // remember previous values + auto oldEnabled = enabled; + auto oldDMType = dmType; + auto oldI2SsdPin = i2ssdPin; + auto oldI2SwsPin = i2swsPin; + auto oldI2SckPin = i2sckPin; + auto oldI2SmclkPin = mclkPin; +#endif + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); +#ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); #else @@ -1712,7 +2892,11 @@ class AudioReactive : public Usermod { if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM + if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM #endif + #else + if (dmType == 5) useInputFilter = 1; // enable filter for PDM + if (dmType == 51) useInputFilter = 1; // switch on filter for legacy PDM #endif configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); @@ -1726,66 +2910,302 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top["config"][F("gain")], sampleGain); configComplete &= getJsonValue(top["config"][F("AGC")], soundAgc); + //WLEDMM: experimental settings + configComplete &= getJsonValue(top["experiments"][F("micLev")], micLevelMethod); + configComplete &= getJsonValue(top["experiments"][F("Mic_Quality")], micQuality); + configComplete &= getJsonValue(top["experiments"][F("freqDist")], freqDist); + //configComplete &= getJsonValue(top["experiments"][F("freqRMS")], averageByRMS); + configComplete &= getJsonValue(top["experiments"][F("FFT_Window")], fftWindow); +#ifdef FFT_USE_SLIDING_WINDOW + configComplete &= getJsonValue(top["experiments"][F("I2S_FastPath")], doSlidingFFT); +#endif + + configComplete &= getJsonValue(top["frequency"][F("scale")], FFTScalingMode); + configComplete &= getJsonValue(top["frequency"][F("profile")], pinkIndex); //WLEDMM +#endif configComplete &= getJsonValue(top["dynamics"][F("limiter")], limiterOn); configComplete &= getJsonValue(top["dynamics"][F("rise")], attackTime); configComplete &= getJsonValue(top["dynamics"][F("fall")], decayTime); - configComplete &= getJsonValue(top["frequency"][F("scale")], FFTScalingMode); - configComplete &= getJsonValue(top["sync"][F("port")], audioSyncPort); configComplete &= getJsonValue(top["sync"][F("mode")], audioSyncEnabled); - + configComplete &= getJsonValue(top["sync"][F("check_sequence")], audioSyncSequence); + + // WLEDMM notify user when a reboot is necessary + #ifdef ARDUINO_ARCH_ESP32 + if (initDone) { + if ((audioSource != nullptr) && (oldDMType != dmType)) errorFlag = ERR_REBOOT_NEEDED; // changing mic type requires reboot + if ( (audioSource != nullptr) && (enabled==true) + && ((oldI2SsdPin != i2ssdPin) || (oldI2SsdPin != i2ssdPin) || (oldI2SckPin != i2sckPin)) ) errorFlag = ERR_REBOOT_NEEDED; // changing mic pins requires reboot + if ((audioSource != nullptr) && (oldI2SmclkPin != mclkPin)) errorFlag = ERR_REBOOT_NEEDED; // changing MCLK pin requires reboot + if ((oldDMType != dmType) && (oldDMType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing from analog mic requires power cycle + if ((oldDMType != dmType) && (dmType == 0)) errorFlag = ERR_POWEROFF_NEEDED; // changing to analog mic requires power cycle + } + #endif return configComplete; } void appendConfigData() { - oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addOption(dd,'Generic Analog',0);")); - #endif - oappend(SET_F("addOption(dd,'Generic I2S',1);")); - oappend(SET_F("addOption(dd,'ES7243',2);")); - oappend(SET_F("addOption(dd,'SPH0654',3);")); - oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); - #endif - oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); + oappend(SET_F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style + oappend(SET_F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[] + oappend(SET_F("addInfo(ux+':help',0,'');")); +#ifdef ARDUINO_ARCH_ESP32 + //WLEDMM: add defaults + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // -S3/-S2/-C3 don't support analog audio + #ifdef AUDIOPIN + oappend(SET_F("xOpt(ux+':analogmic:pin',1,' āŽŒ',")); oappendi(AUDIOPIN); oappend(");"); + #endif + oappend(SET_F("aOpt(ux+':analogmic:pin',1);")); //only analog options + #endif + + oappend(SET_F("dd=addDropdown(ux,'digitalmic:type');")); + #if SR_DMTYPE==254 + oappend(SET_F("addOption(dd,'None - network receive only (āŽŒ)',254);")); + #else + oappend(SET_F("addOption(dd,'None - network receive only',254);")); + #endif + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + #if SR_DMTYPE==0 + oappend(SET_F("addOption(dd,'Generic Analog (āŽŒ)',0);")); + #else + oappend(SET_F("addOption(dd,'Generic Analog',0);")); + #endif + #endif + #if SR_DMTYPE==1 + oappend(SET_F("addOption(dd,'Generic I2S (āŽŒ)',1);")); + #else + oappend(SET_F("addOption(dd,'Generic I2S',1);")); + #endif + #if SR_DMTYPE==2 + oappend(SET_F("addOption(dd,'ES7243 (āŽŒ)',2);")); + #else + oappend(SET_F("addOption(dd,'ES7243',2);")); + #endif + #if SR_DMTYPE==3 + oappend(SET_F("addOption(dd,'SPH0654 (āŽŒ)',3);")); + #else + oappend(SET_F("addOption(dd,'SPH0654',3);")); + #endif + #if SR_DMTYPE==4 + oappend(SET_F("addOption(dd,'Generic I2S with Mclk (āŽŒ)',4);")); + #else + oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); + #endif + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if SR_DMTYPE==5 + oappend(SET_F("addOption(dd,'Generic I2S PDM (āŽŒ)',5);")); + #else + oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + #endif + #if SR_DMTYPE==51 + oappend(SET_F("addOption(dd,'.Legacy I2S PDM ☾ (āŽŒ)',51);")); + #else + oappend(SET_F("addOption(dd,'.Legacy I2S PDM ☾',51);")); + #endif + #endif + #if SR_DMTYPE==6 + oappend(SET_F("addOption(dd,'ES8388 ☾ (āŽŒ)',6);")); + #else + oappend(SET_F("addOption(dd,'ES8388 ☾',6);")); + #endif + #if SR_DMTYPE==7 + oappend(SET_F("addOption(dd,'WM8978 ☾ (āŽŒ)',7);")); + #else + oappend(SET_F("addOption(dd,'WM8978 ☾',7);")); + #endif + #if SR_DMTYPE==8 + oappend(SET_F("addOption(dd,'AC101 ☾ (āŽŒ)',8);")); + #else + oappend(SET_F("addOption(dd,'AC101 ☾',8);")); + #endif + #if SR_DMTYPE==9 + oappend(SET_F("addOption(dd,'ES8311 ☾ (āŽŒ)',9);")); + #else + oappend(SET_F("addOption(dd,'ES8311 ☾',9);")); + #endif + #ifdef SR_SQUELCH + oappend(SET_F("addInfo(ux+':config:squelch',1,'⎌ ")); oappendi(SR_SQUELCH); oappend("');"); // 0 is field type, 1 is actual field + #endif + #ifdef SR_GAIN + oappend(SET_F("addInfo(ux+':config:gain',1,'⎌ ")); oappendi(SR_GAIN); oappend("');"); // 0 is field type, 1 is actual field + #endif + + oappend(SET_F("dd=addDropdown(ux,'config:AGC');")); oappend(SET_F("addOption(dd,'Off',0);")); oappend(SET_F("addOption(dd,'Normal',1);")); oappend(SET_F("addOption(dd,'Vivid',2);")); oappend(SET_F("addOption(dd,'Lazy',3);")); - oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); + //WLEDMM: experimental settings + oappend(SET_F("xx='experiments';")); // shortcut + oappend(SET_F("dd=addDropdown(ux,xx+':micLev');")); + oappend(SET_F("addOption(dd,'Floating (āŽŒ)',0);")); + oappend(SET_F("addOption(dd,'Freeze',1);")); + oappend(SET_F("addOption(dd,'Fast Freeze',2);")); + oappend(SET_F("addInfo(ux+':'+xx+':micLev',1,'☾');")); + + oappend(SET_F("dd=addDropdown(ux,xx+':Mic_Quality');")); + oappend(SET_F("addOption(dd,'average (standard)',0);")); + oappend(SET_F("addOption(dd,'low noise',1);")); + oappend(SET_F("addOption(dd,'perfect',2);")); + + oappend(SET_F("dd=addDropdown(ux,xx+':freqDist');")); + oappend(SET_F("addOption(dd,'Normal (āŽŒ)',0);")); + oappend(SET_F("addOption(dd,'RightShift',1);")); + oappend(SET_F("addInfo(ux+':'+xx+':freqDist',1,'☾');")); + + //oappend(SET_F("dd=addDropdown(ux,xx+':freqRMS');")); + //oappend(SET_F("addOption(dd,'Off (āŽŒ)',0);")); + //oappend(SET_F("addOption(dd,'On',1);")); + //oappend(SET_F("addInfo(ux+':experiments:freqRMS',1,'☾');")); + + oappend(SET_F("dd=addDropdown(ux,xx+':FFT_Window');")); + oappend(SET_F("addOption(dd,'Blackman-Harris (MM standard)',0);")); + oappend(SET_F("addOption(dd,'Hann (balanced)',1);")); + oappend(SET_F("addOption(dd,'Nuttall (more accurate)',2);")); + oappend(SET_F("addOption(dd,'Blackman',5);")); + oappend(SET_F("addOption(dd,'Hamming',3);")); + oappend(SET_F("addOption(dd,'Flat-Top (AC WLED, inaccurate)',4);")); + +#ifdef FFT_USE_SLIDING_WINDOW + oappend(SET_F("dd=addDropdown(ux,xx+':I2S_FastPath');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On (āŽŒ)',1);")); + oappend(SET_F("addInfo(ux+':'+xx+':I2S_FastPath',1,'☾');")); +#endif + + oappend(SET_F("dd=addDropdown(ux,'dynamics:limiter');")); oappend(SET_F("addOption(dd,'Off',0);")); oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); - oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); + oappend(SET_F("addInfo(ux+':dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo(ux+':dynamics:rise',1,'ms (♪ effects only)');")); + oappend(SET_F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); - oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); + oappend(SET_F("dd=addDropdown(ux,'frequency:scale');")); oappend(SET_F("addOption(dd,'None',0);")); oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); - oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); + //WLEDMM add defaults + oappend(SET_F("dd=addDropdown(ux,'frequency:profile');")); + #if SR_FREQ_PROF==0 + oappend(SET_F("addOption(dd,'Generic Microphone (āŽŒ)',0);")); + #else + oappend(SET_F("addOption(dd,'Generic Microphone',0);")); + #endif + #if SR_FREQ_PROF==1 + oappend(SET_F("addOption(dd,'Generic Line-In (āŽŒ)',1);")); + #else + oappend(SET_F("addOption(dd,'Generic Line-In',1);")); + #endif + #if SR_FREQ_PROF==5 + oappend(SET_F("addOption(dd,'ICS-43434 (āŽŒ)',5);")); + #else + oappend(SET_F("addOption(dd,'ICS-43434',5);")); + #endif + #if SR_FREQ_PROF==6 + oappend(SET_F("addOption(dd,'ICS-43434 - big speakers (āŽŒ)',6);")); + #else + oappend(SET_F("addOption(dd,'ICS-43434 - big speakers',6);")); + #endif + #if SR_FREQ_PROF==7 + oappend(SET_F("addOption(dd,'SPM1423 (āŽŒ)',7);")); + #else + oappend(SET_F("addOption(dd,'SPM1423',7);")); + #endif + #if SR_FREQ_PROF==2 + oappend(SET_F("addOption(dd,'IMNP441 (āŽŒ)',2);")); + #else + oappend(SET_F("addOption(dd,'IMNP441',2);")); + #endif + #if SR_FREQ_PROF==3 + oappend(SET_F("addOption(dd,'IMNP441 - big speakers (āŽŒ)',3);")); + #else + oappend(SET_F("addOption(dd,'IMNP441 - big speakers',3);")); + #endif + #if SR_FREQ_PROF==4 + oappend(SET_F("addOption(dd,'IMNP441 - small speakers (āŽŒ)',4);")); + #else + oappend(SET_F("addOption(dd,'IMNP441 - small speakers',4);")); + #endif + #if SR_FREQ_PROF==10 + oappend(SET_F("addOption(dd,'flat - no adjustments (āŽŒ)',10);")); + #else + oappend(SET_F("addOption(dd,'flat - no adjustments',10);")); + #endif + #if SR_FREQ_PROF==8 + oappend(SET_F("addOption(dd,'userdefined #1 (āŽŒ)',8);")); + #else + oappend(SET_F("addOption(dd,'userdefined #1',8);")); + #endif + #if SR_FREQ_PROF==9 + oappend(SET_F("addOption(dd,'userdefined #2 (āŽŒ)',9);")); + #else + oappend(SET_F("addOption(dd,'userdefined #2',9);")); + #endif + oappend(SET_F("addInfo(ux+':frequency:profile',1,'☾');")); +#endif + oappend(SET_F("dd=addDropdown(ux,'sync:mode');")); + oappend(SET_F("addOption(dd,'Off',0);")); // AUDIOSYNC_NONE +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addOption(dd,'Send',1);")); // AUDIOSYNC_SEND +#endif + oappend(SET_F("addOption(dd,'Receive',2);")); // AUDIOSYNC_REC +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addOption(dd,'Receive or Local',6);")); // AUDIOSYNC_REC_PLUS +#endif + // check_sequence: Receiver skips out-of-sequence packets when enabled + oappend(SET_F("dd=addDropdown(ux,'sync:check_sequence');")); oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'Send',1);")); - oappend(SET_F("addOption(dd,'Receive',2);")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); + oappend(SET_F("addOption(dd,'On',1);")); + + oappend(SET_F("addInfo(ux+':sync:check_sequence',1,'when receiving ☾
Sync audio data with other WLEDs');")); // must append this to the last field of 'sync' + + oappend(SET_F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); + #ifdef I2S_SDPIN + oappend(SET_F("xOpt(uxp,0,' āŽŒ',")); oappendi(I2S_SDPIN); oappend(");"); + #endif + + oappend(SET_F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); + oappend(SET_F("dRO(uxp,1);")); // disable read only pins + #ifdef I2S_WSPIN + oappend(SET_F("xOpt(uxp,1,' āŽŒ',")); oappendi(I2S_WSPIN); oappend(");"); + #endif + + oappend(SET_F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); + oappend(SET_F("dRO(uxp,2);")); // disable read only pins + #ifdef I2S_CKPIN + oappend(SET_F("xOpt(uxp,2,' āŽŒ',")); oappendi(I2S_CKPIN); oappend(");"); + #endif + + oappend(SET_F("addInfo(uxp,3,'master clock','I2S MCLK');")); + oappend(SET_F("dRO(uxp,3);")); // disable read only pins #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); - #else - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); + oappend(SET_F("dOpt(uxp,3,2,2);")); //only use -1, 0, 1 or 3 + oappend(SET_F("dOpt(uxp,3,4,39);")); //only use -1, 0, 1 or 3 + #endif + #ifdef MCLK_PIN + oappend(SET_F("xOpt(uxp,3,' āŽŒ',")); oappendi(MCLK_PIN); oappend(");"); + #endif + + oappend(SET_F("addInfo(uxp,4,'','I2C SDA');")); + oappend(SET_F("rOpt(uxp,4,'use global (")); oappendi(i2c_sda); oappend(")',-1);"); + #ifdef ES7243_SDAPIN + oappend(SET_F("xOpt(uxp,4,' āŽŒ',")); oappendi(ES7243_SDAPIN); oappend(");"); #endif - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',4,'','I2C SDA');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',5,'','I2C SCL');")); + + oappend(SET_F("addInfo(uxp,5,'','I2C SCL');")); + oappend(SET_F("rOpt(uxp,5,'use global (")); oappendi(i2c_scl); oappend(")',-1);"); + #ifdef ES7243_SCLPIN + oappend(SET_F("xOpt(uxp,5,' āŽŒ',")); oappendi(ES7243_SCLPIN); oappend(");"); + #endif + oappend(SET_F("dRO(uxp,5);")); // disable read only pins +#endif } @@ -1814,7 +3234,9 @@ class AudioReactive : public Usermod { const char AudioReactive::_name[] PROGMEM = "AudioReactive"; const char AudioReactive::_enabled[] PROGMEM = "enabled"; const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel"; +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) const char AudioReactive::_analogmic[] PROGMEM = "analogmic"; +#endif const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic"; const char AudioReactive::UDP_SYNC_HEADER[] PROGMEM = "00002"; // new sync header version, as format no longer compatible with previous structure const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature diff --git a/usermods/audioreactive/audio_source.h b/usermods/audioreactive/audio_source.h index 16bbbb658a..5548ac5602 100644 --- a/usermods/audioreactive/audio_source.h +++ b/usermods/audioreactive/audio_source.h @@ -1,5 +1,17 @@ #pragma once +/* + @title MoonModules WLED - audioreactive usermod + @file audio_source.h + @repo https://github.com/MoonModules/WLED, submit changes to this file as PRs to MoonModules/WLED + @Authors https://github.com/MoonModules/WLED/commits/mdev/ + @Copyright Ā© 2024 Github MoonModules Commit Authors (contact moonmodules@icloud.com for details) + @license Licensed under the EUPL-1.2 or later + +*/ + + +#ifdef ARDUINO_ARCH_ESP32 #include #include "wled.h" #include @@ -16,6 +28,9 @@ #define SRate_t int #endif +constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not change! I2S_NUM_1 possible but this has + // strong limitations -> no MCLK routing, no ADC support, no PDM support + //#include //#include //#include @@ -23,14 +38,14 @@ // see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents // and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes -#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) +#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) // there are two things in these MCUs that could lead to problems with audio processing: // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x) // * single core, so FFT task might slow down other things like LED updates #if !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) - #error This audio reactive usermod does not support ESP32-C2, ESP32-C3 or ESP32-S2. + #error This audio reactive usermod does not support ESP32-C2 or ESP32-C3. #else - #warning This audio reactive usermod does not support ESP32-C2, ESP32-C3 or ESP32-S2. + #warning This audio reactive usermod does not support ESP32-C2 and ESP32-C3. #endif #endif @@ -45,11 +60,16 @@ // benefit: analog mic inputs will be sampled contiously -> better response times and less "glitches" // WARNING: this option WILL lock-up your device in case that any other analogRead() operation is performed; // for example if you want to read "analog buttons" -//#define I2S_GRAB_ADC1_COMPLETELY // (experimental) continously sample analog ADC microphone. WARNING will cause analogRead() lock-up +//#define I2S_GRAB_ADC1_COMPLETELY // (experimental) continuously sample analog ADC microphone. WARNING will cause analogRead() lock-up // data type requested from the I2S driver - currently we always use 32bit //#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible +#if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) + // this is bitter, but necessary to survive + #define I2S_USE_16BIT_SAMPLES +#endif + #ifdef I2S_USE_16BIT_SAMPLES #define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT #define I2S_datatype int16_t @@ -72,7 +92,7 @@ * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. */ -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 3)) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 8)) // should be fixed in IDF 4.4.5, however arduino-esp32 2.0.14 - 2.0.17 did an "I2S rollback" to 4.4.4 // espressif bug: only_left has no sound, left and right are swapped // https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) // https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) @@ -106,6 +126,9 @@ #endif +// max number of samples for a single i2s_read --> size of global buffer. +#define I2S_SAMPLES_MAX 512 // same as samplesFFT + /* Interface class AudioSource serves as base class for all microphone types This enables accessing all microphones with one single interface @@ -123,7 +146,7 @@ class AudioSource { This function needs to take care of anything that needs to be done before samples can be obtained from the microphone. */ - virtual void initialize(int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) = 0; + virtual void initialize(int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) = 0; /* Deinitialize Release all resources and deactivate any functionality that is used @@ -149,38 +172,61 @@ class AudioSource { virtual I2S_datatype postProcessSample(I2S_datatype sample_in) {return(sample_in);} // default method can be overriden by instances (ADC) that need sample postprocessing // Private constructor, to make sure it is not callable except from derived classes - AudioSource(SRate_t sampleRate, int blockSize, float sampleScale) : + AudioSource(SRate_t sampleRate, int blockSize, float sampleScale, bool i2sMaster) : _sampleRate(sampleRate), _blockSize(blockSize), _initialized(false), + _i2sMaster(i2sMaster), _sampleScale(sampleScale) {}; SRate_t _sampleRate; // Microphone sampling rate int _blockSize; // I2S block size bool _initialized; // Gets set to true if initialization is successful + bool _i2sMaster; // when false, ESP32 will be in I2S SLAVE mode (for devices that only operate in MASTER mode). Only works in newer IDF >= 4.4.x float _sampleScale; // pre-scaling factor for I2S samples + I2S_datatype newSampleBuffer[I2S_SAMPLES_MAX+4] = { 0 }; // global buffer for i2s_read }; /* Basic I2S microphone source All functions are marked virtual, so derived classes can replace them + WARNING: i2sMaster = false is experimental, and most likely will not work */ class I2SSource : public AudioSource { public: - I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - AudioSource(sampleRate, blockSize, sampleScale) { + I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + AudioSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config = { - .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), + .mode = i2sMaster ? i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX) : i2s_mode_t(I2S_MODE_SLAVE | I2S_MODE_RX), .sample_rate = _sampleRate, - .bits_per_sample = I2S_SAMPLE_RESOLUTION, + .bits_per_sample = I2S_SAMPLE_RESOLUTION, // slave mode: may help to set this to 96000, as the other side (master) controls sample rates .channel_format = I2S_MIC_CHANNEL, #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), //.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, +#ifdef WLEDMM_FASTPATH + #ifdef WLED_ENABLE_HUB75MATRIX + .intr_alloc_flags = ESP_INTR_FLAG_IRAM|ESP_INTR_FLAG_LEVEL1, // HUB75 seems to get into trouble if we allocate a higher priority interrupt + .dma_buf_count = 18, // 100ms buffer (128 * dma_buf_count / sampleRate) + #else + #if CONFIG_IDF_TARGET_ESP32 && !defined(BOARD_HAS_PSRAM) // still need to test on boards with PSRAM + .intr_alloc_flags = ESP_INTR_FLAG_IRAM|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3, // IRAM flag reduces missed samples + #else + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3, // seems to reduce noise + #endif + .dma_buf_count = 24, // 140ms buffer (128 * dma_buf_count / sampleRate) + #endif +#else + #ifdef WLED_ENABLE_HUB75MATRIX + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // HUB75 seems to get into trouble if we allocate a higher priority interrupt + #else .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, + #endif .dma_buf_count = 8, +#endif .dma_buf_len = _blockSize, .use_apll = 0, + //.fixed_mclk = 0, .bits_per_chan = I2S_data_size, #else .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), @@ -192,11 +238,12 @@ class I2SSource : public AudioSource { }; } - virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE) { + DEBUGSR_PRINTLN("I2SSource:: initialize()."); if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { if (!pinManager.allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || !pinManager.allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); + ERRORSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); return; } } @@ -204,7 +251,7 @@ class I2SSource : public AudioSource { // i2ssckPin needs special treatment, since it might be unused on PDM mics if (i2sckPin != I2S_PIN_NO_CHANGE) { if (!pinManager.allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); + ERRORSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); return; } } else { @@ -226,10 +273,17 @@ class I2SSource : public AudioSource { _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3 _config.channel_format =I2S_PDM_MIC_CHANNEL; // seems that PDM mono mode always uses left channel. _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality + //_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; // not needed #endif } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + if ((_i2sMaster == false) && (_config.mode & I2S_MODE_SLAVE)) { // I2S slave mode (experimental). + // Seems we need to drive clocks in slave mode + _config.use_apll = true; + _config.fixed_mclk = 512 * int(_config.sample_rate); + } + if (mclkPin != I2S_PIN_NO_CHANGE) { _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches. // //_config.fixed_mclk = 512 * _sampleRate; @@ -244,13 +298,28 @@ class I2SSource : public AudioSource { #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 #endif + #if defined(WLED_ENABLE_HUB75MATRIX) + _config.use_apll = false; // APLL needed for HUB75 DMA driver ? + #endif #endif + if (_i2sMaster == false) { + DEBUG_PRINTLN(F("AR: Warning - i2S SLAVE mode is experimental!")); + if (_config.mode & I2S_MODE_PDM) { + // APLL does not work in DAC or PDM "Slave Mode": https://github.com/espressif/esp-idf/issues/1244, https://github.com/espressif/esp-idf/issues/2634 + _config.use_apll = false; + _config.fixed_mclk = 0; + } + if ((_config.mode & I2S_MODE_MASTER) != 0) { + DEBUG_PRINTLN("AR: (oops) I2S SLAVE mode requested but not configured!"); + } + } + // Reserve the master clock pin if provided _mclkPin = mclkPin; if (mclkPin != I2S_PIN_NO_CHANGE) { if(!pinManager.allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); + ERRORSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); return; } else _routeMclk(mclkPin); @@ -268,32 +337,36 @@ class I2SSource : public AudioSource { //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); - esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); + esp_err_t err = i2s_driver_install(AR_I2S_PORT, &_config, 0, nullptr); if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); + ERRORSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); return; } DEBUGSR_PRINTF("AR: I2S#0 driver %s aPLL; fixed_mclk=%d.\n", _config.use_apll? "uses":"without", _config.fixed_mclk); DEBUGSR_PRINTF("AR: %d bits, Sample scaling factor = %6.4f\n", _config.bits_per_sample, _sampleScale); - if (_config.mode & I2S_MODE_PDM) { + if(_config.mode & I2S_MODE_MASTER) { + if (_config.mode & I2S_MODE_PDM) { DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in PDM MASTER mode.")); - } else { + } else { DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in MASTER mode.")); + } + } else { + DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in SLAVE mode.")); } - err = i2s_set_pin(I2S_NUM_0, &_pinConfig); + err = i2s_set_pin(AR_I2S_PORT, &_pinConfig); if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); - i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver + ERRORSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); + i2s_driver_uninstall(AR_I2S_PORT); // uninstall already-installed driver return; } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) - err = i2s_set_clk(I2S_NUM_0, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. + err = i2s_set_clk(AR_I2S_PORT, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. if (err != ESP_OK) { - DEBUGSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); - i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver + ERRORSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); + i2s_driver_uninstall(AR_I2S_PORT); // uninstall already-installed driver return; } #endif @@ -302,7 +375,7 @@ class I2SSource : public AudioSource { virtual void deinitialize() { _initialized = false; - esp_err_t err = i2s_driver_uninstall(I2S_NUM_0); + esp_err_t err = i2s_driver_uninstall(AR_I2S_PORT); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); return; @@ -318,17 +391,20 @@ class I2SSource : public AudioSource { if (_initialized) { esp_err_t err; size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */ - I2S_datatype newSamples[num_samples]; /* Intermediary sample storage */ - err = i2s_read(I2S_NUM_0, (void *)newSamples, sizeof(newSamples), &bytes_read, portMAX_DELAY); + memset(buffer, 0, sizeof(float) * num_samples); // clear output buffer + I2S_datatype *newSamples = newSampleBuffer; // use global input buffer + if (num_samples > I2S_SAMPLES_MAX) num_samples = I2S_SAMPLES_MAX; // protect the buffer from overflow + + err = i2s_read(AR_I2S_PORT, (void *)newSamples, num_samples * sizeof(I2S_datatype), &bytes_read, portMAX_DELAY); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to get samples: %d\n", err); return; } // For correct operation, we need to read exactly sizeof(samples) bytes from i2s - if (bytes_read != sizeof(newSamples)) { - DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", sizeof(newSamples), bytes_read); + if (bytes_read != (num_samples * sizeof(I2S_datatype))) { + DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", num_samples * sizeof(I2S_datatype), bytes_read); return; } @@ -378,26 +454,21 @@ class I2SSource : public AudioSource { }; /* ES7243 Microphone - This is an I2S microphone that requires ininitialization over + This is an I2S microphone that requires initialization over I2C before I2S data can be received */ class ES7243 : public I2SSource { private: // I2C initialization functions for ES7243 void _es7243I2cBegin() { - bool i2c_initialized = Wire.begin(pin_ES7243_SDA, pin_ES7243_SCL, 100000U); - if (i2c_initialized == false) { - DEBUGSR_PRINTLN(F("AR: ES7243 failed to initialize I2C bus driver.")); - } + Wire.setClock(100000); } void _es7243I2cWrite(uint8_t reg, uint8_t val) { -#ifndef ES7243_ADDR - Wire.beginTransmission(0x13); - #define ES7243_ADDR 0x13 // default address -#else + #ifndef ES7243_ADDR + #define ES7243_ADDR 0x13 // default address + #endif Wire.beginTransmission(ES7243_ADDR); -#endif Wire.write((uint8_t)reg); Wire.write((uint8_t)val); uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK @@ -417,51 +488,464 @@ class ES7243 : public I2SSource { } public: - ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) { + ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; }; - void initialize(int8_t sdaPin, int8_t sclPin, int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { - // check that pins are valid - if ((sdaPin < 0) || (sclPin < 0)) { - DEBUGSR_PRINTF("\nAR: invalid ES7243 I2C pins: SDA=%d, SCL=%d\n", sdaPin, sclPin); + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("ES7243:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid ES7243 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } - - if ((i2sckPin < 0) || (mclkPin < 0)) { - DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + if (!pinManager.joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } - // Reserve SDA and SCL pins of the I2C interface - PinManagerPinType es7243Pins[2] = { { sdaPin, true }, { sclPin, true } }; - if (!pinManager.allocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C)) { - pinManager.deallocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C); - DEBUGSR_PRINTF("\nAR: Failed to allocate ES7243 I2C pins: SDA=%d, SCL=%d\n", sdaPin, sclPin); + // First route mclk, then configure ADC over I2C, then configure I2S + _es7243InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + + void deinitialize() { + I2SSource::deinitialize(); + } +}; + +/* ES8388 Sound Module + This is an I2S sound processing unit that requires initialization over + I2C before I2S data can be received. +*/ +class ES8388Source : public I2SSource { + private: + // I2C initialization functions for ES8388 + void _es8388I2cBegin() { + Wire.setClock(100000); + } + + void _es8388I2cWrite(uint8_t reg, uint8_t val) { + #ifndef ES8388_ADDR + #define ES8388_ADDR 0x10 // default address + #endif + Wire.beginTransmission(ES8388_ADDR); + Wire.write((uint8_t)reg); + Wire.write((uint8_t)val); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { + DEBUGSR_PRINTF("AR: ES8388 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8388_ADDR, reg, val); + } + } + + void _es8388InitAdc() { + // https://dl.radxa.com/rock2/docs/hw/ds/ES8388%20user%20Guide.pdf Section 10.1 + // http://www.everest-semi.com/pdf/ES8388%20DS.pdf Better spec sheet, more clear. + // https://docs.google.com/spreadsheets/d/1CN3MvhkcPVESuxKyx1xRYqfUit5hOdsG45St9BCUm-g/edit#gid=0 generally + // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring. + // Registries are decimal, settings are binary as that's how everything is listed in the docs + // ...which makes it easier to reference the docs. + // + _es8388I2cBegin(); + _es8388I2cWrite( 8,0b00000000); // I2S to slave + _es8388I2cWrite( 2,0b11110011); // Power down DEM and STM + _es8388I2cWrite(43,0b10000000); // Set same LRCK + _es8388I2cWrite( 0,0b00000101); // Set chip to Play & Record Mode + _es8388I2cWrite(13,0b00000010); // Set MCLK/LRCK ratio to 256 + _es8388I2cWrite( 1,0b01000000); // Power up analog and lbias + _es8388I2cWrite( 3,0b00000000); // Power up ADC, Analog Input, and Mic Bias + _es8388I2cWrite( 4,0b11111100); // Power down DAC, Turn on LOUT1 and ROUT1 and LOUT2 and ROUT2 power + _es8388I2cWrite( 2,0b01000000); // Power up DEM and STM and undocumented bit for "turn on line-out amp" + + // #define use_es8388_mic + + #ifdef use_es8388_mic + // The mics *and* line-in are BOTH connected to LIN2/RIN2 on the AudioKit + // so there's no way to completely eliminate the mics. It's also hella noisy. + // Line-in works OK on the AudioKit, generally speaking, as the mics really need + // amplification to be noticeable in a quiet room. If you're in a very loud room, + // the mics on the AudioKit WILL pick up sound even in line-in mode. + // TL;DR: Don't use the AudioKit for anything, use the LyraT. + // + // The LyraT does a reasonable job with mic input as configured below. + + // Pick one of these. If you have to use the mics, use a LyraT over an AudioKit if you can: + _es8388I2cWrite(10,0b00000000); // Use Lin1/Rin1 for ADC input (mic on LyraT) + //_es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input (mic *and* line-in on AudioKit) + + _es8388I2cWrite( 9,0b10001000); // Select Analog Input PGA Gain for ADC to +24dB (L+R) + _es8388I2cWrite(16,0b00000000); // Set ADC digital volume attenuation to 0dB (left) + _es8388I2cWrite(17,0b00000000); // Set ADC digital volume attenuation to 0dB (right) + _es8388I2cWrite(38,0b00011011); // Mixer - route LIN1/RIN1 to output after mic gain + + _es8388I2cWrite(39,0b01000000); // Mixer - route LIN to mixL, +6dB gain + _es8388I2cWrite(42,0b01000000); // Mixer - route RIN to mixR, +6dB gain + _es8388I2cWrite(46,0b00100001); // LOUT1VOL - 0b00100001 = +4.5dB + _es8388I2cWrite(47,0b00100001); // ROUT1VOL - 0b00100001 = +4.5dB + _es8388I2cWrite(48,0b00100001); // LOUT2VOL - 0b00100001 = +4.5dB + _es8388I2cWrite(49,0b00100001); // ROUT2VOL - 0b00100001 = +4.5dB + + // Music ALC - the mics like Auto Level Control + // You can also use this for line-in, but it's not really needed. + // + _es8388I2cWrite(18,0b11111000); // ALC: stereo, max gain +35.5dB, min gain -12dB + _es8388I2cWrite(19,0b00110000); // ALC: target -1.5dB, 0ms hold time + _es8388I2cWrite(20,0b10100110); // ALC: gain ramp up = 420ms/93ms, gain ramp down = check manual for calc + _es8388I2cWrite(21,0b00000110); // ALC: use "ALC" mode, no zero-cross, window 96 samples + _es8388I2cWrite(22,0b01011001); // ALC: noise gate threshold, PGA gain constant, noise gate enabled + #else + _es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input ("line-in") + _es8388I2cWrite( 9,0b00000000); // Select Analog Input PGA Gain for ADC to 0dB (L+R) + _es8388I2cWrite(16,0b01000000); // Set ADC digital volume attenuation to -32dB (left) + _es8388I2cWrite(17,0b01000000); // Set ADC digital volume attenuation to -32dB (right) + _es8388I2cWrite(38,0b00001001); // Mixer - route LIN2/RIN2 to output + + _es8388I2cWrite(39,0b01010000); // Mixer - route LIN to mixL, 0dB gain + _es8388I2cWrite(42,0b01010000); // Mixer - route RIN to mixR, 0dB gain + _es8388I2cWrite(46,0b00011011); // LOUT1VOL - 0b00011110 = +0dB, 0b00011011 = LyraT balance fix + _es8388I2cWrite(47,0b00011110); // ROUT1VOL - 0b00011110 = +0dB + _es8388I2cWrite(48,0b00011110); // LOUT2VOL - 0b00011110 = +0dB + _es8388I2cWrite(49,0b00011110); // ROUT2VOL - 0b00011110 = +0dB + #endif + + } + + public: + ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { + _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; + }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("ES8388Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S ES8388 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid ES8388 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } + if (!pinManager.joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } - pin_ES7243_SDA = sdaPin; - pin_ES7243_SCL = sclPin; + // First route mclk, then configure ADC over I2C, then configure I2S + _es8388InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + + void deinitialize() { + I2SSource::deinitialize(); + } + +}; + +/* ES8311 Sound Module + This is an I2S sound processing unit that requires initialization over + I2C before I2S data can be received. +*/ +class ES8311Source : public I2SSource { + private: + // I2C initialization functions for es8311 + void _es8311I2cBegin() { + Wire.setClock(100000); + } + + void _es8311I2cWrite(uint8_t reg, uint8_t val) { + #ifndef ES8311_ADDR + #define ES8311_ADDR 0x18 // default address is... foggy + #endif + Wire.beginTransmission(ES8311_ADDR); + Wire.write((uint8_t)reg); + Wire.write((uint8_t)val); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { + DEBUGSR_PRINTF("AR: ES8311 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8311_ADDR, reg, val); + } + } + + void _es8311InitAdc() { + // + // Currently only tested with the ESP32-P4 boards with the onboard mic. + // Datasheet with I2C commands: https://dl.xkwy2018.com/downloads/RK3588/01_Official%20Release/04_Product%20Line%20Branch_NVR/02_Key%20Device%20Specifications/ES8311%20DS.pdf + // + _es8311I2cBegin(); + _es8311I2cWrite(0x00, 0b00011111); // RESET, default value + _es8311I2cWrite(0x45, 0b00000000); // GP, default value + _es8311I2cWrite(0x01, 0b00111010); // CLOCK MANAGER was 0b00110000 trying 0b00111010 (MCLK enable?) + + _es8311I2cWrite(0x02, 0b00000000); // 22050hz calculated + _es8311I2cWrite(0x05, 0b00000000); // 22050hz calculated + _es8311I2cWrite(0x03, 0b00010000); // 22050hz calculated + _es8311I2cWrite(0x04, 0b00010000); // 22050hz calculated + _es8311I2cWrite(0x07, 0b00000000); // 22050hz calculated + _es8311I2cWrite(0x08, 0b11111111); // 22050hz calculated + _es8311I2cWrite(0x06, 0b11100011); // 22050hz calculated + + _es8311I2cWrite(0x16, 0b00100100); // ADC was 0b00000011 trying 0b00100100 was good + _es8311I2cWrite(0x0B, 0b00000000); // SYSTEM at default + _es8311I2cWrite(0x0C, 0b00100000); // SYSTEM was 0b00001111 trying 0b00100000 + _es8311I2cWrite(0x10, 0b00010011); // SYSTEM was 0b00011111 trying 0b00010011 + _es8311I2cWrite(0x11, 0b01111100); // SYSTEM was 0b01111111 trying 0b01111100 + _es8311I2cWrite(0x00, 0b11000000); // *** RESET (again - seems important?) + _es8311I2cWrite(0x01, 0b00111010); // *** CLOCK MANAGER was 0b00111111 trying 0b00111010 (again?? seems important) + _es8311I2cWrite(0x14, 0b00010000); // *** SYSTEM was 0b00011010 trying 0b00010000 (PGA gain) + _es8311I2cWrite(0x0A, 0b00001000); // *** SDP OUT, was 0b00001100 trying 0b00001000 (I2S 32-bit) + _es8311I2cWrite(0x0E, 0b00000010); // *** SYSTEM was 0b00000010 trying 0b00000010 + _es8311I2cWrite(0x0F, 0b01000100); // SYSTEM was 0b01000100 + _es8311I2cWrite(0x15, 0b00010000); // ADC soft ramp (disabled 0000xxxx) + _es8311I2cWrite(0x1B, 0b00000101); // ADC soft-mute was 0b00000101 + _es8311I2cWrite(0x1C, 0b01100101); // ADC EQ and offset freeze at 0b01100101 (bad at 0b00101100) + _es8311I2cWrite(0x17, 0b10111111); // ADC volume was 0b11111111 trying ADC volume 0b10111111 = 0db (maxgain) + _es8311I2cWrite(0x18, 0b10000001); // ADC ALC enabled and AutoMute disabled. + // _es8311I2cWrite(0x19, 0b11110100); // ADC ALC max and min - not sure how best to use this, default seems fine + } + + public: + ES8311Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { + _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; + }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("es8311Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S es8311 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid es8311 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } + if (!pinManager.joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } // First route mclk, then configure ADC over I2C, then configure I2S - _es7243InitAdc(); + _es8311InitAdc(); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); } void deinitialize() { - // Release SDA and SCL pins of the I2C interface - PinManagerPinType es7243Pins[2] = { { pin_ES7243_SDA, true }, { pin_ES7243_SCL, true } }; - pinManager.deallocateMultiplePins(es7243Pins, 2, PinOwner::HW_I2C); I2SSource::deinitialize(); } +}; + +class WM8978Source : public I2SSource { private: - int8_t pin_ES7243_SDA; - int8_t pin_ES7243_SCL; + // I2C initialization functions for WM8978 + void _wm8978I2cBegin() { + Wire.setClock(400000); + } + + void _wm8978I2cWrite(uint8_t reg, uint16_t val) { + #ifndef WM8978_ADDR + #define WM8978_ADDR 0x1A + #endif + char buf[2]; + buf[0] = (reg << 1) | ((val >> 8) & 0X01); + buf[1] = val & 0XFF; + Wire.beginTransmission(WM8978_ADDR); + Wire.write((const uint8_t*)buf, 2); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { + DEBUGSR_PRINTF("AR: WM8978 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, WM8978_ADDR, reg, val); + } + } + + void _wm8978InitAdc() { + // https://www.mouser.com/datasheet/2/76/WM8978_v4.5-1141768.pdf + // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring. + // Registries are decimal, settings are 9-bit binary as that's how everything is listed in the docs + // ...which makes it easier to reference the docs. + // + _wm8978I2cBegin(); + + _wm8978I2cWrite( 0,0b000000000); // Reset all settings + _wm8978I2cWrite( 1,0b000111110); // Power Management 1 - power off most things, but enable mic bias and I/O tie-off to help mitigate mic leakage. + _wm8978I2cWrite( 2,0b110111111); // Power Management 2 - enable output and amp stages (amps may lift signal but it works better on the ADCs) + _wm8978I2cWrite( 3,0b000001100); // Power Management 3 - enable L&R output mixers + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + _wm8978I2cWrite( 4,0b001010000); // Audio Interface - standard I2S, 24-bit + #else + _wm8978I2cWrite( 4,0b001001000); // Audio Interface - left-justified I2S, 24-bit + #endif + + _wm8978I2cWrite( 6,0b000000000); // Clock generation control - use external mclk + _wm8978I2cWrite( 7,0b000000100); // Sets sample rate to ~24kHz (only used for internal calculations, not I2S) + _wm8978I2cWrite(14,0b010001000); // 128x ADC oversampling - high pass filter disabled as it kills the bass response + _wm8978I2cWrite(43,0b000110000); // Mute signal paths we don't use + _wm8978I2cWrite(44,0b100000000); // Disconnect microphones + _wm8978I2cWrite(45,0b111000000); // Mute signal paths we don't use + _wm8978I2cWrite(46,0b111000000); // Mute signal paths we don't use + _wm8978I2cWrite(47,0b001000000); // 0dB gain on left line-in + _wm8978I2cWrite(48,0b001000000); // 0dB gain on right line-in + _wm8978I2cWrite(49,0b000000011); // Mixer thermal shutdown enable and unused IOs to 30kĪ© + _wm8978I2cWrite(50,0b000010110); // Output mixer enable only left bypass at 0dB gain + _wm8978I2cWrite(51,0b000010110); // Output mixer enable only right bypass at 0dB gain + _wm8978I2cWrite(52,0b110111001); // Left line-out enabled at 0dB gain + _wm8978I2cWrite(53,0b110111001); // Right line-out enabled at 0db gain + _wm8978I2cWrite(54,0b111000000); // Mute left speaker output + _wm8978I2cWrite(55,0b111000000); // Mute right speaker output + + } + + public: + WM8978Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { + _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; + }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("WM8978Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S WM8978 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid WM8978 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } + if (!pinManager.joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } + + // First route mclk, then configure ADC over I2C, then configure I2S + _wm8978InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + + void deinitialize() { + I2SSource::deinitialize(); + } + }; +class AC101Source : public I2SSource { + private: + // I2C initialization functions for WM8978 + void _ac101I2cBegin() { + Wire.setClock(400000); + } + + void _ac101I2cWrite(uint8_t reg_addr, uint16_t val) { + #ifndef AC101_ADDR + #define AC101_ADDR 0x1A + #endif + char send_buff[3]; + send_buff[0] = reg_addr; + send_buff[1] = uint8_t((val >> 8) & 0xff); + send_buff[2] = uint8_t(val & 0xff); + Wire.beginTransmission(AC101_ADDR); + Wire.write((const uint8_t*)send_buff, 3); + uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK + if (i2cErr != 0) { + DEBUGSR_PRINTF("AR: AC101 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, AC101_ADDR, reg_addr, val); + } + } + + void _ac101InitAdc() { + // https://files.seeedstudio.com/wiki/ReSpeaker_6-Mics_Circular_Array_kit_for_Raspberry_Pi/reg/AC101_User_Manual_v1.1.pdf + // This supports mostly the older AI Thinkier AudioKit A1S that has an AC101 chip + // Newer versions use the ES3833 chip - which we also support. + + _ac101I2cBegin(); + + #define CHIP_AUDIO_RS 0x00 + #define SYSCLK_CTRL 0x03 + #define MOD_CLK_ENA 0x04 + #define MOD_RST_CTRL 0x05 + #define I2S_SR_CTRL 0x06 + #define I2S1LCK_CTRL 0x10 + #define I2S1_SDOUT_CTRL 0x11 + #define I2S1_MXR_SRC 0x13 + #define ADC_DIG_CTRL 0x40 + #define ADC_APC_CTRL 0x50 + #define ADC_SRC 0x51 + #define ADC_SRCBST_CTRL 0x52 + #define OMIXER_DACA_CTRL 0x53 + #define OMIXER_SR 0x54 + #define HPOUT_CTRL 0x56 + + _ac101I2cWrite(CHIP_AUDIO_RS, 0x123); // I think anything written here is a reset as 0x123 is kinda suss. + + delay(100); + + _ac101I2cWrite(SYSCLK_CTRL, 0b0000100000001000); // System Clock is I2S MCLK + _ac101I2cWrite(MOD_CLK_ENA, 0b1000000000001000); // I2S and ADC Clock Enable + _ac101I2cWrite(MOD_RST_CTRL, 0b1000000000001000); // I2S and ADC Clock Enable + _ac101I2cWrite(I2S_SR_CTRL, 0b0100000000000000); // set to 22050hz just in case + _ac101I2cWrite(I2S1LCK_CTRL, 0b1000000000110000); // set I2S slave mode, 24-bit word size + _ac101I2cWrite(I2S1_SDOUT_CTRL, 0b1100000000000000); // I2S enable Left/Right channels + _ac101I2cWrite(I2S1_MXR_SRC, 0b0010001000000000); // I2S digital Mixer, ADC L/R data + _ac101I2cWrite(ADC_SRCBST_CTRL, 0b0000000000000100); // mute all boosts. last 3 bits are reserved/default + _ac101I2cWrite(OMIXER_SR, 0b0000010000001000); // Line L/R to output mixer + _ac101I2cWrite(ADC_SRC, 0b0000010000001000); // Line L/R to ADC + _ac101I2cWrite(ADC_DIG_CTRL, 0b1000000000000000); // Enable ADC + _ac101I2cWrite(ADC_APC_CTRL, 0b1011100100000000); // ADC L/R enabled, 0dB gain + _ac101I2cWrite(OMIXER_DACA_CTRL, 0b0011111110000000); // L/R Analog Output Mixer enabled, headphone DC offset default + _ac101I2cWrite(HPOUT_CTRL, 0b1111101111110001); // Headphone out from Analog Mixer stage, no reduction in volume + + } + + public: + AC101Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) { + _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; + }; + + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { + DEBUGSR_PRINTLN("AC101Source:: initialize();"); + + // if ((i2sckPin < 0) || (mclkPin < 0)) { // WLEDMM not sure if this check is needed here, too + // ERRORSR_PRINTF("\nAR: invalid I2S WM8978 pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); + // return; + // } + // BUG: "use global I2C pins" are valid as -1, and -1 is seen as invalid here. + // Workaround: Set I2C pins here, which will also set them globally. + // Bug also exists in ES7243. + if ((i2c_sda < 0) || (i2c_scl < 0)) { // check that global I2C pins are not "undefined" + ERRORSR_PRINTF("\nAR: invalid AC101 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } + if (!pinManager.joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins + ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); + return; + } + + // First route mclk, then configure ADC over I2C, then configure I2S + _ac101InitAdc(); + I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + } + + void deinitialize() { + I2SSource::deinitialize(); + } + +}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) #if !defined(SOC_I2S_SUPPORTS_ADC) && !defined(SOC_I2S_SUPPORTS_ADC_DAC) @@ -470,7 +954,7 @@ class ES7243 : public I2SSource { #endif #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) -// ADC over I2S is only availeable in "classic" ESP32 +// ADC over I2S is only available in "classic" ESP32 /* ADC over I2S Microphone This microphone is an ADC pin sampled via the I2S interval @@ -480,7 +964,7 @@ class ES7243 : public I2SSource { class I2SAdcSource : public I2SSource { public: I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) { + I2SSource(sampleRate, blockSize, sampleScale, true) { _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), .sample_rate = _sampleRate, @@ -503,18 +987,19 @@ class I2SAdcSource : public I2SSource { /* identify Audiosource type - I2S-ADC*/ AudioSourceType getType(void) {return(Type_I2SAdc);} - void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + DEBUGSR_PRINTLN("I2SAdcSource:: initialize()."); _myADCchannel = 0x0F; if(!pinManager.allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { - DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); + ERRORSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); return; } _audioPin = audioPin; // Determine Analog channel. Only Channels on ADC1 are supported int8_t channel = digitalPinToAnalogChannel(_audioPin); - if (channel > 9) { - DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _audioPin); + if ((channel < 0) || (channel > 9)) { // channel == -1 means "not an ADC pin" + USER_PRINTF("AR: Incompatible GPIO used for analog audio input: %d\n", _audioPin); return; } else { adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel)); @@ -524,16 +1009,16 @@ class I2SAdcSource : public I2SSource { // Install Driver esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to install i2s driver: %d\n", err); + ERRORSR_PRINTF("Failed to install i2s driver: %d\n", err); return; } - adc1_config_width(ADC_WIDTH_BIT_12); // ensure that ADC runs with 12bit resolution + // adc1_config_width(ADC_WIDTH_BIT_12); // ensure that ADC runs with 12bit resolution - should not be needed, because i2s_set_adc_mode does that any way // Enable I2S mode of ADC err = i2s_set_adc_mode(ADC_UNIT_1, adc1_channel_t(channel)); if (err != ESP_OK) { - DEBUGSR_PRINTF("Failed to set i2s adc mode: %d\n", err); + USER_PRINTF("AR: Failed to set i2s adc mode: %d\n", err); return; } @@ -670,18 +1155,20 @@ class I2SAdcSource : public I2SSource { // a user recommended this: Try to set .communication_format to I2S_COMM_FORMAT_STAND_I2S and call i2s_set_clk() after i2s_set_pin(). class SPH0654 : public I2SSource { public: - SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : - I2SSource(sampleRate, blockSize, sampleScale) + SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : + I2SSource(sampleRate, blockSize, sampleScale, i2sMaster) {} - void initialize(uint8_t i2swsPin, uint8_t i2ssdPin, uint8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { + void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { + DEBUGSR_PRINTLN("SPH0654:: initialize();"); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // these registers are only existing in "classic" ESP32 - REG_SET_BIT(I2S_TIMING_REG(I2S_NUM_0), BIT(9)); - REG_SET_BIT(I2S_CONF_REG(I2S_NUM_0), I2S_RX_MSB_SHIFT); + REG_SET_BIT(I2S_TIMING_REG(AR_I2S_PORT), BIT(9)); + REG_SET_BIT(I2S_CONF_REG(AR_I2S_PORT), I2S_RX_MSB_SHIFT); #else #warning FIX ME! Please. #endif } }; +#endif diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index d9f9ea7833..a80fa68169 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -1,6 +1,6 @@ # Audioreactive usermod -Enabless controlling LEDs via audio input. Audio source can be a microphone or analog-in (AUX) using an appropriate adapter. +Enables controlling LEDs via audio input. Audio source can be a microphone or analog-in (AUX) using an appropriate adapter. Supported microphones range from analog (MAX4466, MAX9814, ...) to digital (INMP441, ICS-43434, ...). Does audio processing and provides data structure that specially written effects can use. @@ -19,7 +19,7 @@ This usermod is an evolution of [SR-WLED](https://github.com/atuline/WLED), and ## Supported MCUs This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. -It will compile succesfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. +It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. Analog audio is only possible on "classic" ESP32, but not on other MCUs like ESP32-S3. @@ -27,17 +27,9 @@ Currently ESP8266 is not supported, due to low speed and small RAM of this chip. There are however plans to create a lightweight audioreactive for the 8266, with reduced features. ## Installation -### using customised _arduinoFFT_ library for use with this usermod -Add `-D USERMOD_AUDIOREACTIVE` to your PlatformIO environment `build_flags`, as well as `https://github.com/blazoncek/arduinoFFT.git` to your `lib_deps`. -If you are not using PlatformIO (which you should) try adding `#define USERMOD_AUDIOREACTIVE` to *my_config.h* and make sure you have _arduinoFFT_ library downloaded and installed. +### using latest _arduinoFFT_ library -Customised _arduinoFFT_ library for use with this usermod can be found at https://github.com/blazoncek/arduinoFFT.git - -### using latest (develop) _arduinoFFT_ library -Alternatively, you can use the latest arduinoFFT development version. -ArduinoFFT `develop` library is slightly more accurate, and slighly faster than our customised library, however also needs additional 2kB RAM. - -* `build_flags` = `-D USERMOD_AUDIOREACTIVE` `-D UM_AUDIOREACTIVE_USE_NEW_FFT` +* `build_flags` = `-D USERMOD_AUDIOREACTIVE` * `lib_deps`= `https://github.com/kosme/arduinoFFT#develop @ 1.9.2` ## Configuration @@ -55,6 +47,11 @@ If you want to define default GPIOs during compile time, use the following (defa - `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) - `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) +Other options: + +- `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) +- `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default + **NOTE** I2S is used for analog audio sampling. Hence, the analog *buttons* (i.e. potentiometers) are disabled when running this usermod with an analog microphone. ### Advanced Compile-Time Options @@ -63,7 +60,7 @@ You can use the following additional flags in your `build_flags` * `-D SR_GAIN=x` : Default "gain" setting (60) * `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). * `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM ressources (not recommended unless you absolutely need this). -* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continously sample analog ADC microphone. Only effective on ESP32. WARNING this _will_ cause conflicts(lock-up) with any analogRead() call. +* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this _will_ cause conflicts(lock-up) with any analogRead() call. * `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) * `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. diff --git a/usermods/boblight/boblight.h b/usermods/boblight/boblight.h index a1e2577586..b36f301cce 100644 --- a/usermods/boblight/boblight.h +++ b/usermods/boblight/boblight.h @@ -305,6 +305,8 @@ class BobLightUsermod : public Usermod { } void appendConfigData() { + oappend(SET_F("addHB('BobLight');")); + //oappend(SET_F("dd=addDropdown('usermod','selectfield');")); //oappend(SET_F("addOption(dd,'1st value',0);")); //oappend(SET_F("addOption(dd,'2nd value',1);")); diff --git a/usermods/mcu_temp/mcuTemp.h b/usermods/mcu_temp/mcuTemp.h new file mode 100644 index 0000000000..73272cd1b7 --- /dev/null +++ b/usermods/mcu_temp/mcuTemp.h @@ -0,0 +1,133 @@ +#pragma once + +#include "wled.h" + +// constants +#define MCUT_READ_TIME_MS 7500 // read once in 7.5 seconds + +// class name. Use something descriptive and leave the ": public Usermod" part :) +class mcuTemp : public Usermod +{ + +private: + float mcutemp = 0; + + // any private methods should go here (non-inline method should be defined out of class) + void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message + +public: + mcuTemp(const char *name, bool enabled) : Usermod(name, enabled) {} // WLEDMM + + void setup() + { + } + + void connected() + { + } + + void loop() + { + static unsigned long lastMQQTTime = 0; + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || (strip.isUpdating() && (millis() - lastTime < MCUT_READ_TIME_MS))) + return; + + if (millis() - lastTime < MCUT_READ_TIME_MS) return; // reading each 8 seconds should be enough + +#ifdef ESP8266 // ESP8266 + // does not seem possible + mcutemp = -1; +#elif defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32S2 + mcutemp = -1; +#else // ESP32 ESP32S3 and ESP32C3 + float newmcutemp = roundf(temperatureRead() * 10) / 10; + if (abs(newmcutemp - 53.3f) > 0.05f) mcutemp = (mcutemp + 2.0f * newmcutemp) / 3.0f; // skip error value (128 => 53.3deg), apply some filtering +#endif + +#ifndef WLED_DISABLE_MQTT + if (millis() - lastMQQTTime > 15000) + { + char array[10]; + snprintf(array, sizeof(array), "%3.1f", mcutemp); + publishMqtt(array); + lastMQQTTime = millis(); + } +#endif + lastTime = millis(); + } + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject &root) + { + // if "u" object does not exist yet wee need to create it + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + // this code adds "u":{"ExampleUsermod":[20," lux"]} to the info object + // int reading = 20; + if (!enabled) return; + JsonArray lightArr = user.createNestedArray(FPSTR(_name)); // name + lightArr.add(roundf(10.0f * mcutemp)/10.0f); // value, rounded to 1 decimal + lightArr.add(F(" °C")); // unit + + // if you are implementing a sensor usermod, you may publish sensor data + // JsonObject sensor = root[F("sensor")]; + // if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); + // temp = sensor.createNestedArray(F("light")); + // temp.add(reading); + // temp.add(F("lux")); + } +/* + void addToJsonState(JsonObject &root) + { + } + void readFromJsonState(JsonObject &root) + { + } + + void addToConfig(JsonObject &root) + { + } + + bool readFromConfig(JsonObject &root) + { + return true; + } + + void appendConfigData() + { + } + + void handleOverlayDraw() + { + } +*/ + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_MCUTEMP; + } +}; + +void mcuTemp::publishMqtt(const char *state, bool retain) +{ +#ifndef WLED_DISABLE_MQTT + // Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED) + { + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/mcutemp")); + mqtt->publish(subuf, 0, retain, state); + } +#endif +} diff --git a/usermods/mcu_temp/readme.md b/usermods/mcu_temp/readme.md new file mode 100644 index 0000000000..c284613ed7 --- /dev/null +++ b/usermods/mcu_temp/readme.md @@ -0,0 +1,11 @@ +# MCU Temp Usermod +This usermod adds the temperature readout to the Info tab and also publishes that over the topic `mcutemp` topic. + + +A shown temp of 53,33°C might indicate that the internal temp is not supported. + +ESP8266 does not have a internal temp sensor + +ESP32S2 seems to crash on reading the sensor -> disabled + +Buildflag: `-D USERMOD_MCUTEMP` diff --git a/usermods/mpu6050_imu/readme.md b/usermods/mpu6050_imu/readme.md index 4120041517..f1f3d74fd1 100644 --- a/usermods/mpu6050_imu/readme.md +++ b/usermods/mpu6050_imu/readme.md @@ -20,9 +20,9 @@ react to the globes orientation. See the blog post on building it For example: @@ -36,7 +36,7 @@ lib_deps = AsyncTCP@1.0.3 Esp Async WebServer@1.2.0 IRremoteESP8266@2.7.3 - jrowberg/I2Cdevlib-MPU6050@^1.0.0 + ElectronicCats/MPU6050 @ 0.6.0 ``` ## Wiring @@ -77,7 +77,7 @@ to the info object ## Usermod installation -1. Copy the file `usermod_mpu6050_imu.h` to the `wled00` directory. + 2. Register the usermod by adding `#include "usermod_mpu6050_imu.h"` in the top and `registerUsermod(new MPU6050Driver());` in the bottom of `usermods_list.cpp`. Example **usermods_list.cpp**: @@ -85,10 +85,11 @@ Example **usermods_list.cpp**: ```cpp #include "wled.h" -#include "usermod_mpu6050_imu.h" +#ifdef USERMOD_MPU6050_IMU +#include "../usermods/mpu6050_imu/usermod_mpu6050_imu.h" +#endif -void registerUsermods() -{ +#ifdef USERMOD_MPU6050_IMU usermods.add(new MPU6050Driver()); -} +#endif ``` diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/usermod_mpu6050_imu.h index b4dc01a4df..1e784d9576 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/usermod_mpu6050_imu.h @@ -1,7 +1,23 @@ #pragma once +#include // WLEDMM: make sure that I2C drivers have the "right" Wire Object +#include + #include "wled.h" + // #define MPU6050_INT_GPIO 13 // WLEDMM - better choice on ESP32 + +#ifdef WLED_DEBUG + #define DEBUG_PRINT_IMU(x) DEBUG_PRINT(x) + #define DEBUG_PRINT_IMULN(x) DEBUG_PRINTLN(x) + #define DEBUG_PRINT_IMUF(x...) DEBUG_PRINTF(x) +#else + #define DEBUG_PRINT_IMU(x) + #define DEBUG_PRINT_IMULN(x) + #define DEBUG_PRINT_IMUF(x...) +#endif + + /* This driver reads quaternion data from the MPU6060 and adds it to the JSON This example is adapted from: https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050/examples/MPU6050_DMP6_ESPWiFi @@ -26,20 +42,42 @@ 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp 3. I2Cdev and MPU6050 must be installed as libraries, or else the .cpp/.h file for both classes must be in the include path of your project. To install the - libraries add I2Cdevlib-MPU6050@fbde122cc5 to lib_deps in the platformio.ini file. - 4. You also need to change lib_compat_mode from strict to soft in platformio.ini (This ignores that I2Cdevlib-MPU6050 doesn't list platform compatibility) + libraries add ElectronicCats/MPU6050 @ 0.6.0 to lib_deps in the platformio.ini file. + // 4. You also need to change lib_compat_mode from strict to soft in platformio.ini (This ignores that I2Cdevlib-MPU6050 doesn't list platform compatibility) 5. Wire up the MPU6050 as detailed above. */ -#include "I2Cdev.h" +// WLEDMM: make sure that the "standard" Wire object is used +#define I2CDEV_IMPLEMENTATION I2CDEV_ARDUINO_WIRE + +// WLEDMM avoid stupid warnings +#undef DEBUG_PRINT +#undef DEBUG_PRINTLN +#undef DEBUG_PRINTF + +#include + +#include + +// WLEDMM - need to re-define WLED DEBUG_PRINT maros, because the were overwritten by MPU6050_6Axis_MotionApps20.h +#undef DEBUG_PRINT +#undef DEBUG_PRINTLN +#undef DEBUG_PRINTF -#include "MPU6050_6Axis_MotionApps20.h" -//#include "MPU6050.h" // not necessary if using MotionApps include file +#ifdef WLED_DEBUG + #define DEBUG_PRINT(x) DEBUGOUT(x) + #define DEBUG_PRINTLN(x) DEBUGOUTLN(x) + #define DEBUG_PRINTF(x...) DEBUGOUTF(x) +#else + #define DEBUG_PRINT(x) + #define DEBUG_PRINTLN(x) + #define DEBUG_PRINTF(x...) +#endif // Arduino Wire library is required if I2Cdev I2CDEV_ARDUINO_WIRE implementation // is used in I2Cdev.h #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE - #include "Wire.h" + //#include "Wire.h" // WLEDMM not necessary #endif // ================================================================ @@ -55,57 +93,125 @@ void IRAM_ATTR dmpDataReady() { class MPU6050Driver : public Usermod { private: MPU6050 mpu; - bool enabled = true; + unsigned long lastUMRun = millis(); // MPU control/status vars - bool dmpReady = false; // set true if DMP init was successful uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU uint8_t devStatus; // return status after each device operation (0 = success, !0 = error) uint16_t packetSize; // expected DMP packet size (default is 42 bytes) uint16_t fifoCount; // count of all bytes currently in FIFO uint8_t fifoBuffer[64]; // FIFO storage buffer - //NOTE: some of these can be removed to save memory, processing time - // if the measurement isn't needed + // strings to reduce flash memory usage (used more than twice) + static const char _INT_pin[]; + + public: + MPU6050Driver(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM: this shouldn't be necessary (passthrough of constructor), maybe because Usermod is an abstract class + + bool dmpReady = false; // set true if DMP init was successful // WLEDMM expose this info in public interface + // orientation/motion vars Quaternion qat; // [w, x, y, z] quaternion container - float euler[3]; // [psi, theta, phi] Euler angle container - float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container VectorInt16 aa; // [x, y, z] accel sensor measurements VectorInt16 gy; // [x, y, z] gyro sensor measurements VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements VectorFloat gravity; // [x, y, z] gravity vector + float euler[3] = {0.0f};// [psi, theta, phi] Euler angle container + float ypr[3] = {0.0f}; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector - static const int INTERRUPT_PIN = 15; // use pin 15 on ESP8266 + #if !defined(ARDUINO_ARCH_ESP32) || !defined(MPU6050_INT_GPIO) + static const int INTERRUPT_PIN = -1; // WLEDMM: not use pin 15 (on ESP8266) as can and will cause conflict with other pins + #else + static const int INTERRUPT_PIN = MPU6050_INT_GPIO; // WLEDMM + #endif - public: - //Functions called by WLED - - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - */ void setup() { + // WLEDMM begin + if (!enabled) { + dmpReady = false; + return; + } + USER_PRINTLN(F("mpu setup")); PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { enabled = false; return; } + if ((i2c_scl < 0) || (i2c_sda < 0)) { + //enabled = false; + USER_PRINTF("mpu6050: warning - ivalid I2C pins: sda=%d scl=%d\n", i2c_sda, i2c_scl); + //return; + } + + if (pins[1].pin < 0 || pins[0].pin < 0) { enabled=false; dmpReady = false; return; } //WLEDMM bugfix - ensure that "final" GPIO are valid and no "-1" sneaks trough + //if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { + + // WLEDMM join I2C HW wire + if (!pinManager.joinWire()) { + enabled = false; + dmpReady = false; + USER_PRINTF("mpu6050: failed to allocate I2C sda=%d scl=%d\n", i2c_sda, i2c_scl); + return; + } + // WLEDMM end + // join I2C bus (I2Cdev library doesn't do this automatically) #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE - Wire.begin(); - Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties +#if defined(ARDUINO_ARCH_ESP32) + //Wire.begin(pins[1].pin, pins[0].pin); // WLEDMM fix - need to use proper pins, in case that Wire was not started yet. Call will silently fail if Wire is initialized already. +#else + //Wire.begin(); // WLEDMM - i2c pins on 8266 are fixed. +#endif + + Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE - Fastwire::setup(400, true); + Fastwire::setup(400, true); #endif + // // initialize serial communication + // // (115200 chosen because it is required for Teapot Demo output, but it's + // // really up to you depending on your project) + // Serial.begin(115200); + // while (!Serial); // wait for Leonardo enumeration, others continue immediately + + // NOTE: 8MHz or slower host processors, like the Teensy @ 3.3V or Arduino + // Pro Mini running at 3.3V, cannot handle this baud rate reliably due to + // the baud timing being too misaligned with processor ticks. You must use + // 38400 or slower in these cases, or use some kind of external separate + // crystal solution for the UART timer. + // initialize device - DEBUG_PRINTLN(F("Initializing I2C devices...")); + DEBUG_PRINT_IMULN(F("Initializing I2C devices...")); + // WLEDMM begin + if ((INTERRUPT_PIN < 0) || (!pinManager.isPinINT(INTERRUPT_PIN))) { + //enabled = false; + USER_PRINTF("mpu6050: warning - interrupt GPIO %d does not support interrupts.\n", INTERRUPT_PIN); + //INTERRUPT_PIN = -1; + //return; + } + if ((INTERRUPT_PIN >= 0) && (pinManager.getPinOwner(INTERRUPT_PIN) != PinOwner::UM_IMU) // only allocate pin if we don't own it already + && !pinManager.allocatePin(INTERRUPT_PIN, false, PinOwner::UM_IMU)) + { + //enabled = false; + USER_PRINTF("mpu6050: warning - failed to allocate interrupt GPIO %d\n", INTERRUPT_PIN); + //INTERRUPT_PIN = -1; + //return; + } + // WLEDMM end + mpu.initialize(); - pinMode(INTERRUPT_PIN, INPUT); + if (INTERRUPT_PIN >= 0) { // WLEDMM only if pin is valid + pinMode(INTERRUPT_PIN, INPUT); + } // verify connection - DEBUG_PRINTLN(F("Testing device connections...")); - DEBUG_PRINTLN(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed")); + DEBUG_PRINT_IMULN(F("Testing device connections...")); + USER_PRINTLN(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed")); + + // // wait for ready + // DEBUG_PRINT_IMULN(F("\nSend any character to begin DMP programming and demo: ")); + // while (Serial.available() && Serial.read()); // empty buffer + // while (!Serial.available()); // wait for data + // while (Serial.available() && Serial.read()); // empty buffer again // load and configure the DMP - DEBUG_PRINTLN(F("Initializing DMP...")); + DEBUG_PRINT_IMULN(F("Initializing DMP...")); devStatus = mpu.dmpInitialize(); // supply your own gyro offsets here, scaled for min sensitivity @@ -116,21 +222,31 @@ class MPU6050Driver : public Usermod { // make sure it worked (returns 0 if so) if (devStatus == 0) { - // turn on the DMP, now that it's ready - DEBUG_PRINTLN(F("Enabling DMP...")); - mpu.setDMPEnabled(true); - - // enable Arduino interrupt detection - DEBUG_PRINTLN(F("Enabling interrupt detection (Arduino external interrupt 0)...")); - attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING); - mpuIntStatus = mpu.getIntStatus(); - - // set our DMP Ready flag so the main loop() function knows it's okay to use it - DEBUG_PRINTLN(F("DMP ready! Waiting for first interrupt...")); - dmpReady = true; - - // get expected DMP packet size for later comparison - packetSize = mpu.dmpGetFIFOPacketSize(); + // Calibration Time: generate offsets and calibrate our MPU6050 + mpu.CalibrateAccel(6); + mpu.CalibrateGyro(6); + #ifdef WLED_DEBUG + mpu.PrintActiveOffsets(); + #endif + // turn on the DMP, now that it's ready + DEBUG_PRINT_IMULN(F("Enabling DMP...")); + mpu.setDMPEnabled(true); + + if (INTERRUPT_PIN >= 0) { + // enable Arduino interrupt detection + DEBUG_PRINT_IMU(F("Enabling interrupt detection (Arduino external interrupt ")); + DEBUG_PRINT_IMU(digitalPinToInterrupt(INTERRUPT_PIN)); + DEBUG_PRINT_IMULN(F(")...")); + attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING); + } + mpuIntStatus = mpu.getIntStatus(); + + // set our DMP Ready flag so the main loop() function knows it's okay to use it + DEBUG_PRINT_IMULN(F("DMP ready! Waiting for first interrupt...")); + dmpReady = true; + + // get expected DMP packet size for later comparison + packetSize = mpu.dmpGetFIFOPacketSize(); } else { // ERROR! // 1 = initial memory load failed @@ -139,54 +255,24 @@ class MPU6050Driver : public Usermod { DEBUG_PRINT(F("DMP Initialization failed (code ")); DEBUG_PRINT(devStatus); DEBUG_PRINTLN(")"); + dmpReady = false; } + initDone = true; } - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ void connected() { - //DEBUG_PRINTLN("Connected to WiFi!"); } - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - */ void loop() { // if programming failed, don't try to do anything - if (!enabled || !dmpReady || strip.isUpdating()) return; - - // wait for MPU interrupt or extra packet(s) available - if (!mpuInterrupt && fifoCount < packetSize) return; - - // reset interrupt flag and get INT_STATUS byte - mpuInterrupt = false; - mpuIntStatus = mpu.getIntStatus(); - - // get current FIFO count - fifoCount = mpu.getFIFOCount(); - - // check for overflow (this should never happen unless our code is too inefficient) - if ((mpuIntStatus & 0x10) || fifoCount == 1024) { - // reset so we can continue cleanly - mpu.resetFIFO(); - DEBUG_PRINTLN(F("FIFO overflow!")); - - // otherwise, check for DMP data ready interrupt (this should happen frequently) - } else if (mpuIntStatus & 0x02) { - // wait for correct available data length, should be a VERY short wait - while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount(); - - // read a packet from FIFO - mpu.getFIFOBytes(fifoBuffer, packetSize); - - // track FIFO count here in case there is > 1 packet available - // (this lets us immediately read more without waiting for an interrupt) - fifoCount -= packetSize; - + if (!initDone) return; + if (!enabled || (strip.isUpdating() && (millis() - lastUMRun < 2))) return; // be nice, but not too nice + lastUMRun = millis(); // update time keeping + if (!dmpReady) return; + // read a packet from FIFO + if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) { // Get the Latest packet //NOTE: some of these can be removed to save memory, processing time // if the measurement isn't needed mpu.dmpGetQuaternion(&qat, fifoBuffer); @@ -200,25 +286,27 @@ class MPU6050Driver : public Usermod { } } - - void addToJsonInfo(JsonObject& root) { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object + if (!initDone) return; + if (!enabled && !dmpReady) return; // WLEDMM no info when usermod disabled JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonObject imu_meas = user.createNestedObject("IMU"); + StaticJsonDocument<800> doc; //measured 528 // WLEDMM added some margin (was 600) + + JsonObject imu_meas = doc.createNestedObject("IMU"); + //JsonObject imu_meas = user.createNestedObject("IMU"); + #ifdef WLED_DEBUG JsonArray quat_json = imu_meas.createNestedArray("Quat"); quat_json.add(qat.w); quat_json.add(qat.x); quat_json.add(qat.y); quat_json.add(qat.z); JsonArray euler_json = imu_meas.createNestedArray("Euler"); - euler_json.add(euler[0]); - euler_json.add(euler[1]); - euler_json.add(euler[2]); + euler_json.add(euler[0] * 180/M_PI); + euler_json.add(euler[1] * 180/M_PI); + euler_json.add(euler[2] * 180/M_PI); JsonArray accel_json = imu_meas.createNestedArray("Accel"); accel_json.add(aa.x); accel_json.add(aa.y); @@ -227,10 +315,6 @@ class MPU6050Driver : public Usermod { gyro_json.add(gy.x); gyro_json.add(gy.y); gyro_json.add(gy.z); - JsonArray world_json = imu_meas.createNestedArray("WorldAccel"); - world_json.add(aaWorld.x); - world_json.add(aaWorld.y); - world_json.add(aaWorld.z); JsonArray real_json = imu_meas.createNestedArray("RealAccel"); real_json.add(aaReal.x); real_json.add(aaReal.y); @@ -239,52 +323,95 @@ class MPU6050Driver : public Usermod { grav_json.add(gravity.x); grav_json.add(gravity.y); grav_json.add(gravity.z); - JsonArray orient_json = imu_meas.createNestedArray("Orientation"); - orient_json.add(ypr[0]); - orient_json.add(ypr[1]); - orient_json.add(ypr[2]); + #endif + JsonArray world_json = imu_meas.createNestedArray("WorldAccel"); + world_json.add(aaWorld.x); + world_json.add(aaWorld.y); + world_json.add(aaWorld.z); + JsonArray orient_json = imu_meas.createNestedArray("YPR"); + orient_json.add(ypr[0] * 180/M_PI); + orient_json.add(ypr[1] * 180/M_PI); + orient_json.add(ypr[2] * 180/M_PI); + char stringBuffer[400]; // measured 266 // WLEDMM added some margin (was 300) + serializeJson(imu_meas, stringBuffer); + JsonArray mainObject = user.createNestedArray("IMU"); + if (!dmpReady || !enabled) { // WLEDMM + if (!dmpReady) mainObject.add(F("Sensor Not Found")); + else if (!enabled) mainObject.add(F("usermod disabled")); + } else { + mainObject.add(stringBuffer); + } + // Serial.printf("imu_meas %u (%u %u) stringBuffer %u\n", (unsigned int)imu_meas.memoryUsage(), (unsigned int)imu_meas.size(), (unsigned int)imu_meas.nesting(), strlen(stringBuffer)); + } - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ //void addToJsonState(JsonObject& root) //{ - //root["user0"] = userVar0; //} - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ //void readFromJsonState(JsonObject& root) //{ - //if (root["bri"] == 255) DEBUG_PRINTLN(F("Don't burn down your garage!")); //} + // void addToConfig(JsonObject& root) + // { + // Usermod::addToConfig(root); + // JsonObject top = root[FPSTR(_name)]; + // // //JsonObject interruptPin = top.createNestedObject(FPSTR(_INT_pin)); + // // //interruptPin["pin"] = INTERRUPT_PIN; + // // DEBUG_PRINTLN(F("MPU6050 IMU config saved.")); + // } + + //WLEDMM: add appendConfigData + void appendConfigData() + { + oappend(SET_F("addHB('mpu6050-IMU');")); + /* + #ifdef MPU6050_INT_GPIO + oappend(SET_F("xOpt('mpu6050-IMU:interrupt_pin',0,' āŽŒ',")); oappendi(MPU6050_INT_GPIO); oappend(");"); + #endif + //WLEDMM add errorMessage to um settings + if (strcmp(errorMessage, "") != 0) { + oappend(SET_F("addInfo('errorMessage', 0, 'error: ")); oappend(errorMessage); oappend("! Correct and reboot');"); + } + */ + } + + bool readFromConfig(JsonObject& root) + { + bool configComplete = Usermod::readFromConfig(root); + JsonObject top = root[FPSTR(_name)]; + + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + //configComplete &= getJsonValue(top[FPSTR(_INT_pin)]["pin"], INTERRUPT_PIN); + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + if (enabled || dmpReady) setup(); // re-run setup if user has checked "enabled" + if (!enabled) dmpReady = false; // not enabled inplies "no DMP data ready" + } + + return configComplete; + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + //return !top[FPSTR(_INT_pin)].isNull(); + } - /* - * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! - */ -// void addToConfig(JsonObject& root) -// { -// JsonObject top = root.createNestedObject("MPU6050_IMU"); -// JsonArray pins = top.createNestedArray("pin"); -// pins.add(HW_PIN_SCL); -// pins.add(HW_PIN_SDA); -// } - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - */ uint16_t getId() { return USERMOD_ID_IMU; } }; + +// strings to reduce flash memory usage (used more than twice) +const char MPU6050Driver::_INT_pin[] PROGMEM = "interrupt_pin"; diff --git a/usermods/mqtt_switch_v2/README.md b/usermods/mqtt_switch_v2/README.md index 148e4a5643..744d7fe3c3 100644 --- a/usermods/mqtt_switch_v2/README.md +++ b/usermods/mqtt_switch_v2/README.md @@ -50,5 +50,5 @@ This usermod listens on `[mqttDeviceTopic]/switch/0/set` (where 0 is replaced wi Feedback about the current state is provided at `[mqttDeviceTopic]/switch/0/state`. ### Home Assistant auto-discovery -Auto-discovery information is automatically published and you shoudn't have to do anything to register the switches in Home Assistant. +Auto-discovery information is automatically published and you shouldn't have to do anything to register the switches in Home Assistant. diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index 71a540701c..1f3bec7a63 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -2,7 +2,7 @@ This usermod-v2 modification allows the connection of multiple relays, each with individual delay and on/off mode. Usermod supports PCF8574 I2C port expander to reduce GPIO use. -PCF8574 supports 8 outputs and each output corresponds to a relay in WLED (relay 0 = port 0, etc). I you are using more than 8 relays with multiple PCF8574 make sure their addresses are set conscutively (e.g. 0x20 and 0x21). You can set address of first expander in settings. +PCF8574 supports 8 outputs and each output corresponds to a relay in WLED (relay 0 = port 0, etc). I you are using more than 8 relays with multiple PCF8574 make sure their addresses are set in sequence (e.g. 0x20 and 0x21). You can set address of first expander in settings. (**NOTE:** Will require Wire library and global I2C pins defined.) ## HTTP API diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index ce71f7893a..dd47e76779 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -113,7 +113,7 @@ class MultiRelay : public Usermod { /** * Get usermod enabled/disabled state */ - inline bool isEnabled() { return enabled; } + // inline bool isEnabled() { return enabled; } /** * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). @@ -196,7 +196,7 @@ class MultiRelay : public Usermod { }; -// class implementetion +// class implementation void MultiRelay::publishMqtt(int relay) { #ifndef WLED_DISABLE_MQTT @@ -496,10 +496,10 @@ void MultiRelay::setup() { * loop() is called continuously. Here you can check for events, read sensors, etc. */ void MultiRelay::loop() { + static unsigned long lastUpdate = 0; yield(); - if (!enabled || strip.isUpdating()) return; + if (!enabled || (strip.isUpdating() && millis() - lastUpdate < 100)) return; - static unsigned long lastUpdate = 0; if (millis() - lastUpdate < 100) return; // update only 10 times/s lastUpdate = millis(); @@ -626,7 +626,7 @@ void MultiRelay::addToJsonInfo(JsonObject &root) { for (int i=0; i(not hex!)','address');")); oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); diff --git a/usermods/quinled-an-penta/readme.md b/usermods/quinled-an-penta/readme.md index 2338747d6e..c1260d9134 100644 --- a/usermods/quinled-an-penta/readme.md +++ b/usermods/quinled-an-penta/readme.md @@ -2,7 +2,7 @@ The (un)official usermod to get the best out of the QuinLED-An-Penta (https://quinled.info/quinled-an-penta/), e.g. using the OLED and the SHT30 temperature/humidity sensor. ## Requirements -* "u8gs" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2 +* "u8g2" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2 * "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85 ## Usermod installation diff --git a/usermods/rgb-rotary-encoder/readme.md b/usermods/rgb-rotary-encoder/readme.md index ba5aad4df7..6531791799 100644 --- a/usermods/rgb-rotary-encoder/readme.md +++ b/usermods/rgb-rotary-encoder/readme.md @@ -9,7 +9,7 @@ The actual / original code that controls the LED modes is from Adam Zeloof. I ta It was quite a bit more work than I hoped, but I got there eventually :) ## Requirements -* "ESP Rotary" by Lennart Hennigs, v1.5.0 or higher: https://github.com/LennartHennigs/ESPRotary +* "ESP Rotary" by Lennart Hennigs, v2.1.1 or higher: https://github.com/LennartHennigs/ESPRotary ## Usermod installation Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one and add the buildflag `-D RGB_ROTARY_ENCODER`. @@ -20,7 +20,7 @@ ESP32: extends = env:esp32dev build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 -D RGB_ROTARY_ENCODER lib_deps = ${esp32.lib_deps} - lennarthennigs/ESP Rotary@^1.5.0 + lennarthennigs/ESP Rotary@^2.1.1 ``` ESP8266 / D1 Mini: @@ -29,7 +29,7 @@ ESP8266 / D1 Mini: extends = env:d1_mini build_flags = ${common.build_flags_esp8266} -D RGB_ROTARY_ENCODER lib_deps = ${esp8266.lib_deps} - lennarthennigs/ESP Rotary@^1.5.0 + lennarthennigs/ESP Rotary@^2.1.1 ``` ## How to connect the board to your ESP diff --git a/usermods/sd_card/readme.md b/usermods/sd_card/readme.md index 299b68ebc9..96390c05ac 100644 --- a/usermods/sd_card/readme.md +++ b/usermods/sd_card/readme.md @@ -20,7 +20,7 @@ | `pinSourceSelect` | GPIO that is connected to SD's `SS`(source select) / `CS`(chip select) | 16 | | `pinSourceClock` | GPIO that is connected to SD's `SCLK` (source clock) / `CLK`(clock) | 14 | | `pinPoci` | GPIO that is connected to SD's `POCI`☨ (Peripheral-Out-Ctrl-In) / `MISO` (deprecated) | 36 | - | `pinPico` | GPIO that is connected to SD's `PICO`☨ (Peripheral-In-Ctrl-Out) / `MOSI` (deprecated) | 14 | + | `pinPico` | GPIO that is connected to SD's `PICO`☨ (Peripheral-In-Ctrl-Out) / `MOSI` (deprecated) | 15 | | `sdEnable` | Enable to read data from the SD-card | true | ☨Following new naming convention of [OSHWA](https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/) @@ -31,4 +31,4 @@ - checks if the specified file is available on the SD card ```cpp bool file_onSD(const char *filepath) {...} - ``` \ No newline at end of file + ``` diff --git a/usermods/seven_segment_display/readme.md b/usermods/seven_segment_display/readme.md index a5294701c1..792393a831 100644 --- a/usermods/seven_segment_display/readme.md +++ b/usermods/seven_segment_display/readme.md @@ -17,7 +17,7 @@ The number of individual LEDs per segment. 7 segments per digit. #### perPeriod -- ssLEDPerPeriod The number of individual LEDs per period. A ':' (colon) has two periods. #### startIdx -- ssStartLED -Index of the LED the display starts at. Enabless a seven segment display to be in the middle of a string. +Index of the LED the display starts at. Enables a seven segment display to be in the middle of a string. #### timeEnable -- ssTimeEnabled When true, when displayMask is configured for a time output and no message is set, the time will be displayed. #### scrollSpd -- ssScrollSpeed diff --git a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h index e5b726e52f..20fef15df5 100644 --- a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h +++ b/usermods/seven_segment_display/usermod_v2_seven_segment_display.h @@ -409,7 +409,7 @@ class SevenSegmentDisplay : public Usermod if (mqttGroupTopic[0] != 0) { - //subcribe for sevenseg messages on the group topic + //subscribe for sevenseg messages on the group topic sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_sevenSeg); mqtt->subscribe(subBuffer, 2); } @@ -417,7 +417,7 @@ class SevenSegmentDisplay : public Usermod bool onMqttMessage(char *topic, char *payload) { - //If topic beings iwth sevenSeg cut it off, otherwise not our message. + //If topic beings with sevenSeg cut it off, otherwise not our message. size_t topicPrefixLen = strlen_P(PSTR("/sevenSeg/")); if (strncmp_P(topic, PSTR("/sevenSeg/"), topicPrefixLen) == 0) topic += topicPrefixLen; diff --git a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h index 2797740575..3afe775f15 100644 --- a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h +++ b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h @@ -470,14 +470,14 @@ class UsermodSSDR : public Usermod { if (mqttGroupTopic[0] != 0) { - //subcribe for sevenseg messages on the group topic + //subscribe for sevenseg messages on the group topic sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_name); mqtt->subscribe(subBuffer, 2); } } bool onMqttMessage(char *topic, char *payload) { - //If topic beings iwth sevenSeg cut it off, otherwise not our message. + //If topic begins with sevenSeg cut it off, otherwise not our message. size_t topicPrefixLen = strlen_P(PSTR("/wledSS/")); if (strncmp_P(topic, PSTR("/wledSS/"), topicPrefixLen) == 0) { topic += topicPrefixLen; diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/usermod_sht.h index 56f3dd41d0..7583a823b9 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/usermod_sht.h @@ -1,5 +1,5 @@ #ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." +#warning "This user mod expects MQTT to be enabled." #endif #pragma once @@ -14,7 +14,7 @@ class ShtUsermod : public Usermod { private: - bool enabled = false; // Is usermod enabled or not + //bool enabled = false; // Is usermod enabled or not //WLEDMM use public attribute of class UserMod bool firstRunDone = false; // Remembers if the first config load run had been done bool pinAllocDone = true; // Remembers if we have allocated pins bool initDone = false; // Remembers if the mod has been completely initialised @@ -44,9 +44,10 @@ class ShtUsermod : public Usermod void appendDeviceToMqttDiscoveryMessage(JsonDocument& root); public: + ShtUsermod(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM // Strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; + //static const char _name[]; //WLEDMM use public attribute of class UserMod + //static const char _enabled[]; //WLEDMM not needed static const char _shtType[]; static const char _unitOfTemp[]; static const char _haMqttDiscovery[]; @@ -71,8 +72,8 @@ class ShtUsermod : public Usermod }; // Strings to reduce flash memory usage (used more than twice) -const char ShtUsermod::_name[] PROGMEM = "SHT-Sensor"; -const char ShtUsermod::_enabled[] PROGMEM = "Enabled"; +//const char ShtUsermod::_name[] PROGMEM = "SHT-Sensor"; //WLEDMM use public attribute of class UserMod +//const char ShtUsermod::_enabled[] PROGMEM = "Enabled"; //WLEDMM not needed const char ShtUsermod::_shtType[] PROGMEM = "SHT-Type"; const char ShtUsermod::_unitOfTemp[] PROGMEM = "Unit"; const char ShtUsermod::_haMqttDiscovery[] PROGMEM = "Add-To-HA-MQTT-Discovery"; @@ -93,10 +94,13 @@ void ShtUsermod::initShtTempHumiditySensor() case USERMOD_SHT_TYPE_SHT35: shtTempHumidSensor = (SHT *) new SHT35(); break; case USERMOD_SHT_TYPE_SHT85: shtTempHumidSensor = (SHT *) new SHT85(); break; } - +#if 0 shtTempHumidSensor->begin(shtI2cAddress, i2c_sda, i2c_scl); +#else + shtTempHumidSensor->begin((uint8_t)shtI2cAddress); // WLEDMM this connects to an existing Wire (I2C) object, instead starting a new driver +#endif if (shtTempHumidSensor->readStatus() == 0xFFFF) { - DEBUG_PRINTF("[%s] SHT init failed!\n", _name); + USER_PRINTF("[%s] SHT init failed, Sensor not found!\n", _name); cleanup(); return; } @@ -134,12 +138,15 @@ void ShtUsermod::cleanup() cleanupShtTempHumiditySensor(); if (pinAllocDone) { +#if 0 // WLEDMM not needed PinManagerPinType pins[2] = { { i2c_sda, true }, { i2c_scl, true } }; pinManager.deallocateMultiplePins(pins, 2, PinOwner::HW_I2C); +#endif pinAllocDone = false; } enabled = false; + shtInitDone = false; // WLEDMM bugfix } /** @@ -151,6 +158,7 @@ void ShtUsermod::cleanup() * @return void */ void ShtUsermod::publishTemperatureAndHumidityViaMqtt() { +#ifdef WLED_ENABLE_MQTT if (!WLED_MQTT_CONNECTED) return; char buf[128]; @@ -158,6 +166,7 @@ void ShtUsermod::publishTemperatureAndHumidityViaMqtt() { mqtt->publish(buf, 0, false, String(getTemperature()).c_str()); snprintf_P(buf, 127, PSTR("%s/humidity"), mqttDeviceTopic); mqtt->publish(buf, 0, false, String(getHumidity()).c_str()); +#endif } /** @@ -170,6 +179,7 @@ void ShtUsermod::publishTemperatureAndHumidityViaMqtt() { * @return void */ void ShtUsermod::publishHomeAssistantAutodiscovery() { +#ifdef WLED_ENABLE_MQTT if (!WLED_MQTT_CONNECTED) return; char json_str[1024], buf[128]; @@ -207,6 +217,7 @@ void ShtUsermod::publishHomeAssistantAutodiscovery() { mqtt->publish(buf, 0, true, json_str, payload_size); haMqttDiscoveryDone = true; +#endif } /** @@ -214,6 +225,7 @@ void ShtUsermod::publishHomeAssistantAutodiscovery() { * * @return void */ +#ifdef WLED_ENABLE_MQTT void ShtUsermod::appendDeviceToMqttDiscoveryMessage(JsonDocument& root) { JsonObject device = root.createNestedObject(F("dev")); device[F("ids")] = escapedMac.c_str(); @@ -222,6 +234,7 @@ void ShtUsermod::appendDeviceToMqttDiscoveryMessage(JsonDocument& root) { device[F("mdl")] = ESP.getChipModel(); device[F("mf")] = F("espressif"); } +#endif /** * Setup the mod. @@ -239,19 +252,35 @@ void ShtUsermod::setup() if (enabled) { PinManagerPinType pins[2] = { { i2c_sda, true }, { i2c_scl, true } }; // GPIOs can be set to -1 and allocateMultiplePins() will return true, so check they're gt zero +#if 0 // WLEDMM done by pinManager.joinWire() if (i2c_sda < 0 || i2c_scl < 0 || !pinManager.allocateMultiplePins(pins, 2, PinOwner::HW_I2C)) { +#else + if (i2c_sda < 0 || i2c_scl < 0) { +#endif DEBUG_PRINTF("[%s] SHT pin allocation failed!\n", _name); cleanup(); return; } - pinAllocDone = true; + // WLEDMM join hardware I2C + if (pinManager.joinWire()) { // WLEDMM - this allocates global I2C pins, then starts Wire - if not started previously + pinAllocDone = true; - initShtTempHumiditySensor(); + initShtTempHumiditySensor(); - initDone = true; + initDone = true; + } else { + DEBUG_PRINTF("[%s] SHT I2C pin allocation failed!\n", _name); + return; + } } firstRunDone = true; + + if (enabled && initDone && pinAllocDone && isShtReady()) { + USER_PRINTF(PSTR("[%s] SHT sensor ready.\n"), _name); + } else { + USER_PRINTF(PSTR("[%s] SHT sensor not ready.\n"), _name); + } } /** @@ -269,7 +298,9 @@ void ShtUsermod::setup() */ void ShtUsermod::loop() { - if (!enabled || !initDone || strip.isUpdating()) return; + unsigned long last_runtime = 0; // WLEDMM ensure that strip.isUpdating() will not block longer that 1000ms + if (!enabled || !initDone || !pinAllocDone || (strip.isUpdating() && (millis()-last_runtime < 1000))) return; // WLEDMM be nice, but not too nice + last_runtime = millis(); if (isShtReady()) { if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) { @@ -300,7 +331,7 @@ void ShtUsermod::loop() /** * Whenever MQTT is connected, publish HA autodiscovery topics. * - * Is only donce once. + * Is only done once. * * @see Usermod::onMqttConnect() * @see UsermodManager::onMqttConnect() @@ -308,7 +339,9 @@ void ShtUsermod::loop() * @return void */ void ShtUsermod::onMqttConnect(bool sessionPresent) { +#ifdef WLED_ENABLE_MQTT if (haMqttDiscovery && !haMqttDiscoveryDone) publishHomeAssistantAutodiscovery(); +#endif } /** @@ -350,7 +383,7 @@ void ShtUsermod::addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; + top[F("enabled")] = enabled; top[FPSTR(_shtType)] = shtType; top[FPSTR(_unitOfTemp)] = unitOfTemp; top[FPSTR(_haMqttDiscovery)] = haMqttDiscovery; @@ -381,7 +414,7 @@ bool ShtUsermod::readFromConfig(JsonObject &root) byte oldUnitOfTemp = unitOfTemp; bool oldHaMqttDiscovery = haMqttDiscovery; - getJsonValue(top[FPSTR(_enabled)], enabled); + getJsonValue(top[F("enabled")], enabled); getJsonValue(top[FPSTR(_shtType)], shtType); getJsonValue(top[FPSTR(_unitOfTemp)], unitOfTemp); getJsonValue(top[FPSTR(_haMqttDiscovery)], haMqttDiscovery); diff --git a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h index 61b76ba194..85a9a16054 100644 --- a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h +++ b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h @@ -23,7 +23,7 @@ class RotaryEncoderBrightnessColor : public Usermod unsigned char Enc_B; unsigned char Enc_A_prev = 0; - // private class memebers configurable by Usermod Settings (defaults set inside readFromConfig()) + // private class members configurable by Usermod Settings (defaults set inside readFromConfig()) int8_t pins[3]; // pins[0] = DT from encoder, pins[1] = CLK from encoder, pins[2] = CLK from encoder (optional) int fadeAmount; // how many points to fade the Neopixel with each step @@ -162,7 +162,7 @@ class RotaryEncoderBrightnessColor : public Usermod * - configComplete is used to return false if any value is missing, not just if the main object is missing * - The defaults are loaded every time readFromConfig() is run, not just once after boot * - * This ensures that missing values are added to the config, with their default values, in the rare but plauible cases of: + * This ensures that missing values are added to the config, with their default values, in the rare but plausible cases of: * - a single value being missing at boot, e.g. if the Usermod was upgraded and a new setting was added * - a single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) * diff --git a/usermods/usermod_v2_animartrix/readme.md b/usermods/usermod_v2_animartrix/readme.md new file mode 100644 index 0000000000..d12dd1b08f --- /dev/null +++ b/usermods/usermod_v2_animartrix/readme.md @@ -0,0 +1,10 @@ +# Usermods API v2 example usermod + +In this usermod file you can find the documentation on how to take advantage of the new version 2 usermods! + +## Installation + +Copy `usermod_v2_animartrix.h` to the wled00 directory. +Uncomment the corresponding lines in `usermods_list.cpp` and compile! +_(You shouldn't need to actually install this, it does nothing useful)_ + diff --git a/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h new file mode 100644 index 0000000000..6c9de44d88 --- /dev/null +++ b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h @@ -0,0 +1,484 @@ +#pragma once + +#include "wled.h" + +// softhack007: workaround for ICE (internal compiler error) when compiling with new framework and "-O2": + +/* + wled00/../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h: In function 'uint16_t mode_Waves()': + wled00/../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h:367:1: error: insn does not satisfy its constraints: + } + ^ + (insn 811 738 824 24 (set (reg/v:SF 19 f0 [orig:69 result ] [69]) + (mem/u/c:SF (symbol_ref/u:SI ("*.LC1657") [flags 0x2]) [0 S4 A32])) ".pio/libdeps/my_esp32_16MB_V4_S/animartrix/ANIMartRIX.h":372 47 {movsf_internal} + (nil)) + during RTL pass: postreload + wled00/../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h:367:1: internal compiler error: in extract_constrain_insn, at recog.c:2210 + libbacktrace could not find executable to open + Please submit a full bug report, + with preprocessed source if appropriate. + See for instructions. +*/ + +#if defined(ARDUINO_ARCH_ESP32) && defined(ESP_IDF_VERSION) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + // this pragma temporarily raises gcc optimization level to "-O3", to avoid internal error conditions + #pragma GCC push_options + #pragma GCC optimize ("O3") +#endif +#endif + +#include + +#warning WLEDMM usermod: CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms! +//======================================================================================================================== + + +static const char _data_FX_mode_Module_Experiment10[] PROGMEM = "YšŸ’”Module_Experiment10 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment9[] PROGMEM = "YšŸ’”Module_Experiment9 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment8[] PROGMEM = "YšŸ’”Module_Experiment8 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment7[] PROGMEM = "YšŸ’”Module_Experiment7 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment6[] PROGMEM = "YšŸ’”Module_Experiment6 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment5[] PROGMEM = "YšŸ’”Module_Experiment5 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment4[] PROGMEM = "YšŸ’”Module_Experiment4 ☾@Speed;;1;2"; +static const char _data_FX_mode_Zoom2[] PROGMEM = "YšŸ’”Zoom2 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment3[] PROGMEM = "YšŸ’”Module_Experiment3 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment2[] PROGMEM = "YšŸ’”Module_Experiment2 ☾@Speed;;1;2"; +static const char _data_FX_mode_Module_Experiment1[] PROGMEM = "YšŸ’”Module_Experiment1 ☾@Speed;;1;2"; +static const char _data_FX_mode_Parametric_Water[] PROGMEM = "YšŸ’”Parametric_Water ☾@Speed;;1;2"; +static const char _data_FX_mode_Water[] PROGMEM = "YšŸ’”Water ☾@Speed;;1;2"; +static const char _data_FX_mode_Complex_Kaleido_6[] PROGMEM = "YšŸ’”Complex_Kaleido_6 ☾@Speed;;1;2"; +static const char _data_FX_mode_Complex_Kaleido_5[] PROGMEM = "YšŸ’”Complex_Kaleido_5 ☾@Speed;;1;2"; +static const char _data_FX_mode_Complex_Kaleido_4[] PROGMEM = "YšŸ’”Complex_Kaleido_4 ☾@Speed;;1;2"; +static const char _data_FX_mode_Complex_Kaleido_3[] PROGMEM = "YšŸ’”Complex_Kaleido_3 ☾@Speed;;1;2"; +static const char _data_FX_mode_Complex_Kaleido_2[] PROGMEM = "YšŸ’”Complex_Kaleido_2 ☾@Speed;;1;2"; +static const char _data_FX_mode_Complex_Kaleido[] PROGMEM = "YšŸ’”Complex_Kaleido ☾@Speed;;1;2"; +static const char _data_FX_mode_SM10[] PROGMEM = "YšŸ’”SM10 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM9[] PROGMEM = "YšŸ’”SM9 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM8[] PROGMEM = "YšŸ’”SM8 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM7[] PROGMEM = "YšŸ’”SM7 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM6[] PROGMEM = "YšŸ’”SM6 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM5[] PROGMEM = "YšŸ’”SM5 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM4[] PROGMEM = "YšŸ’”SM4 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM3[] PROGMEM = "YšŸ’”SM3 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM2[] PROGMEM = "YšŸ’”SM2 ☾@Speed;;1;2"; +static const char _data_FX_mode_SM1[] PROGMEM = "YšŸ’”SM1 ☾@Speed;;1;2"; +static const char _data_FX_mode_Big_Caleido[] PROGMEM = "YšŸ’”Big_Caleido ☾@Speed;;1;2"; +static const char _data_FX_mode_RGB_Blobs5[] PROGMEM = "YšŸ’”RGB_Blobs5 ☾@Speed;;1;2"; +static const char _data_FX_mode_RGB_Blobs4[] PROGMEM = "YšŸ’”RGB_Blobs4 ☾@Speed;;1;2"; +static const char _data_FX_mode_RGB_Blobs3[] PROGMEM = "YšŸ’”RGB_Blobs3 ☾@Speed;;1;2"; +static const char _data_FX_mode_RGB_Blobs2[] PROGMEM = "YšŸ’”RGB_Blobs2 ☾@Speed;;1;2"; +static const char _data_FX_mode_RGB_Blobs[] PROGMEM = "YšŸ’”RGB_Blobs ☾@Speed;;1;2"; +static const char _data_FX_mode_Polar_Waves[] PROGMEM = "YšŸ’”Polar_Waves ☾@Speed;;1;2"; +static const char _data_FX_mode_Slow_Fade[] PROGMEM = "YšŸ’”Slow_Fade ☾@Speed;;1;2"; +static const char _data_FX_mode_Zoom[] PROGMEM = "YšŸ’”Zoom ☾@Speed;;1;2"; +static const char _data_FX_mode_Hot_Blob[] PROGMEM = "YšŸ’”Hot_Blob ☾@Speed;;1;2"; +static const char _data_FX_mode_Spiralus2[] PROGMEM = "YšŸ’”Spiralus2 ☾@Speed;;1;2"; +static const char _data_FX_mode_Spiralus[] PROGMEM = "YšŸ’”Spiralus ☾@Speed;;1;2"; +static const char _data_FX_mode_Yves[] PROGMEM = "YšŸ’”Yves ☾@Speed;;1;2"; +static const char _data_FX_mode_Scaledemo1[] PROGMEM = "YšŸ’”Scaledemo1 ☾@Speed;;1;2"; +static const char _data_FX_mode_Lava1[] PROGMEM = "YšŸ’”Lava1 ☾@Speed;;1;2"; +static const char _data_FX_mode_Caleido3[] PROGMEM = "YšŸ’”Caleido3 ☾@Speed;;1;2"; +static const char _data_FX_mode_Caleido2[] PROGMEM = "YšŸ’”Caleido2 ☾@Speed;;1;2"; +static const char _data_FX_mode_Caleido1[] PROGMEM = "YšŸ’”Caleido1 ☾@Speed;;1;2"; +static const char _data_FX_mode_Distance_Experiment[] PROGMEM = "YšŸ’”Distance_Experiment ☾@Speed;;1;2"; +static const char _data_FX_mode_Center_Field[] PROGMEM = "YšŸ’”Center_Field ☾@Speed;;1;2"; +static const char _data_FX_mode_Waves[] PROGMEM = "YšŸ’”Waves ☾@Speed;;1;2"; +static const char _data_FX_mode_Chasing_Spirals[] PROGMEM = "YšŸ’”Chasing_Spirals ☾@Speed;;1;2"; +static const char _data_FX_mode_Rotating_Blob[] PROGMEM = "YšŸ’”Rotating_Blob ☾@Speed;;1;2"; + + +class ANIMartRIXMod:public ANIMartRIX { + public: + void initEffect() { + if (SEGENV.call == 0) { + init(SEGMENT.virtualWidth(), SEGMENT.virtualHeight(), false); + } + float speedFactor = 1.0; + if (SEGMENT.speed < 128) { + speedFactor = (float) map(SEGMENT.speed, 0, 127, 1, 100) / 100.0f; + } + else{ + speedFactor = (float) map(SEGMENT.speed, 128, 255, 10, 100) / 10.0f; + } + setSpeedFactor(speedFactor); + } + void setPixelColor(int x, int y, rgb pixel) { + SEGMENT.setPixelColorXY(x, y, CRGB(pixel.red, pixel.green, pixel.blue)); + } + void setPixelColor(int index, rgb pixel) { + SEGMENT.setPixelColor(index, CRGB(pixel.red, pixel.green, pixel.blue)); + } + + // Add any extra custom effects not part of the ANIMartRIX libary here +}; +ANIMartRIXMod anim; + +uint16_t mode_Module_Experiment10() { + anim.initEffect(); + anim.Module_Experiment10(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment9() { + anim.initEffect(); + anim.Module_Experiment9(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment8() { + anim.initEffect(); + anim.Module_Experiment8(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment7() { + anim.initEffect(); + anim.Module_Experiment7(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment6() { + anim.initEffect(); + anim.Module_Experiment6(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment5() { + anim.initEffect(); + anim.Module_Experiment5(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment4() { + anim.initEffect(); + anim.Module_Experiment4(); + return FRAMETIME; +} +uint16_t mode_Zoom2() { + anim.initEffect(); + anim.Zoom2(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment3() { + anim.initEffect(); + anim.Module_Experiment3(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment2() { + anim.initEffect(); + anim.Module_Experiment2(); + return FRAMETIME; +} +uint16_t mode_Module_Experiment1() { + anim.initEffect(); + anim.Module_Experiment1(); + return FRAMETIME; +} +uint16_t mode_Parametric_Water() { + anim.initEffect(); + anim.Parametric_Water(); + return FRAMETIME; +} +uint16_t mode_Water() { + anim.initEffect(); + anim.Water(); + return FRAMETIME; +} +uint16_t mode_Complex_Kaleido_6() { + anim.initEffect(); + anim.Complex_Kaleido_6(); + return FRAMETIME; +} +uint16_t mode_Complex_Kaleido_5() { + anim.initEffect(); + anim.Complex_Kaleido_5(); + return FRAMETIME; +} +uint16_t mode_Complex_Kaleido_4() { + anim.initEffect(); + anim.Complex_Kaleido_4(); + return FRAMETIME; +} +uint16_t mode_Complex_Kaleido_3() { + anim.initEffect(); + anim.Complex_Kaleido_3(); + return FRAMETIME; +} +uint16_t mode_Complex_Kaleido_2() { + anim.initEffect(); + anim.Complex_Kaleido_2(); + return FRAMETIME; +} +uint16_t mode_Complex_Kaleido() { + anim.initEffect(); + anim.Complex_Kaleido(); + return FRAMETIME; +} +uint16_t mode_SM10() { + anim.initEffect(); + anim.SM10(); + return FRAMETIME; +} +uint16_t mode_SM9() { + anim.initEffect(); + anim.SM9(); + return FRAMETIME; +} +uint16_t mode_SM8() { + anim.initEffect(); + anim.SM8(); + return FRAMETIME; +} +// uint16_t mode_SM7() { +// anim.initEffect(); +// anim.SM7(); +// +// return FRAMETIME; +// } +uint16_t mode_SM6() { + anim.initEffect(); + anim.SM6(); + return FRAMETIME; +} +uint16_t mode_SM5() { + anim.initEffect(); + anim.SM5(); + return FRAMETIME; +} +uint16_t mode_SM4() { + anim.initEffect(); + anim.SM4(); + return FRAMETIME; +} +uint16_t mode_SM3() { + anim.initEffect(); + anim.SM3(); + return FRAMETIME; +} +uint16_t mode_SM2() { + anim.initEffect(); + anim.SM2(); + return FRAMETIME; +} +uint16_t mode_SM1() { + anim.initEffect(); + anim.SM1(); + return FRAMETIME; +} +uint16_t mode_Big_Caleido() { + anim.initEffect(); + anim.Big_Caleido(); + return FRAMETIME; +} +uint16_t mode_RGB_Blobs5() { + anim.initEffect(); + anim.RGB_Blobs5(); + return FRAMETIME; +} +uint16_t mode_RGB_Blobs4() { + anim.initEffect(); + anim.RGB_Blobs4(); + return FRAMETIME; +} +uint16_t mode_RGB_Blobs3() { + anim.initEffect(); + anim.RGB_Blobs3(); + return FRAMETIME; +} +uint16_t mode_RGB_Blobs2() { + anim.initEffect(); + anim.RGB_Blobs2(); + return FRAMETIME; +} +uint16_t mode_RGB_Blobs() { + anim.initEffect(); + anim.RGB_Blobs(); + return FRAMETIME; +} +uint16_t mode_Polar_Waves() { + anim.initEffect(); + anim.Polar_Waves(); + return FRAMETIME; +} +uint16_t mode_Slow_Fade() { + anim.initEffect(); + anim.Slow_Fade(); + return FRAMETIME; +} +uint16_t mode_Zoom() { + anim.initEffect(); + anim.Zoom(); + return FRAMETIME; +} +uint16_t mode_Hot_Blob() { + anim.initEffect(); + anim.Hot_Blob(); + return FRAMETIME; +} +uint16_t mode_Spiralus2() { + anim.initEffect(); + anim.Spiralus2(); + return FRAMETIME; +} +uint16_t mode_Spiralus() { + anim.initEffect(); + anim.Spiralus(); + return FRAMETIME; +} +uint16_t mode_Yves() { + anim.initEffect(); + anim.Yves(); + return FRAMETIME; +} +uint16_t mode_Scaledemo1() { + anim.initEffect(); + anim.Scaledemo1(); + return FRAMETIME; +} +uint16_t mode_Lava1() { + anim.initEffect(); + anim.Lava1(); + return FRAMETIME; +} +uint16_t mode_Caleido3() { + anim.initEffect(); + anim.Caleido3(); + return FRAMETIME; +} +uint16_t mode_Caleido2() { + anim.initEffect(); + anim.Caleido2(); + return FRAMETIME; +} +uint16_t mode_Caleido1() { + anim.initEffect(); + anim.Caleido1(); + return FRAMETIME; +} +uint16_t mode_Distance_Experiment() { + anim.initEffect(); + anim.Distance_Experiment(); + return FRAMETIME; +} +uint16_t mode_Center_Field() { + anim.initEffect(); + anim.Center_Field(); + return FRAMETIME; +} +uint16_t mode_Waves() { + anim.initEffect(); + anim.Waves(); + return FRAMETIME; +} +uint16_t mode_Chasing_Spirals() { + anim.initEffect(); + anim.Chasing_Spirals(); + return FRAMETIME; +} +uint16_t mode_Rotating_Blob() { + anim.initEffect(); + anim.Rotating_Blob(); + return FRAMETIME; +} + + +class AnimartrixUsermod : public Usermod { + + public: + + AnimartrixUsermod(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM + + + void setup() { + + if(!enabled) return; + + strip.addEffect(203, &mode_Module_Experiment10, _data_FX_mode_Module_Experiment10); + strip.addEffect(204, &mode_Module_Experiment9, _data_FX_mode_Module_Experiment9); + strip.addEffect(205, &mode_Module_Experiment8, _data_FX_mode_Module_Experiment8); + strip.addEffect(206, &mode_Module_Experiment7, _data_FX_mode_Module_Experiment7); + strip.addEffect(207, &mode_Module_Experiment6, _data_FX_mode_Module_Experiment6); + strip.addEffect(208, &mode_Module_Experiment5, _data_FX_mode_Module_Experiment5); + strip.addEffect(209, &mode_Module_Experiment4, _data_FX_mode_Module_Experiment4); + strip.addEffect(210, &mode_Zoom2, _data_FX_mode_Zoom2); + strip.addEffect(211, &mode_Module_Experiment3, _data_FX_mode_Module_Experiment3); + strip.addEffect(212, &mode_Module_Experiment2, _data_FX_mode_Module_Experiment2); + strip.addEffect(213, &mode_Module_Experiment1, _data_FX_mode_Module_Experiment1); + strip.addEffect(214, &mode_Parametric_Water, _data_FX_mode_Parametric_Water); + strip.addEffect(215, &mode_Water, _data_FX_mode_Water); + strip.addEffect(216, &mode_Complex_Kaleido_6, _data_FX_mode_Complex_Kaleido_6); + strip.addEffect(217, &mode_Complex_Kaleido_5, _data_FX_mode_Complex_Kaleido_5); + strip.addEffect(218, &mode_Complex_Kaleido_4, _data_FX_mode_Complex_Kaleido_4); + strip.addEffect(219, &mode_Complex_Kaleido_3, _data_FX_mode_Complex_Kaleido_3); + strip.addEffect(220, &mode_Complex_Kaleido_2, _data_FX_mode_Complex_Kaleido_2); + strip.addEffect(221, &mode_Complex_Kaleido, _data_FX_mode_Complex_Kaleido); + strip.addEffect(222, &mode_SM10, _data_FX_mode_SM10); + strip.addEffect(223, &mode_SM9, _data_FX_mode_SM9); + strip.addEffect(224, &mode_SM8, _data_FX_mode_SM8); + // strip.addEffect(225, &mode_SM7, _data_FX_mode_SM7); + strip.addEffect(226, &mode_SM6, _data_FX_mode_SM6); + strip.addEffect(227, &mode_SM5, _data_FX_mode_SM5); + strip.addEffect(228, &mode_SM4, _data_FX_mode_SM4); + strip.addEffect(229, &mode_SM3, _data_FX_mode_SM3); + strip.addEffect(230, &mode_SM2, _data_FX_mode_SM2); + strip.addEffect(231, &mode_SM1, _data_FX_mode_SM1); + strip.addEffect(232, &mode_Big_Caleido, _data_FX_mode_Big_Caleido); + strip.addEffect(233, &mode_RGB_Blobs5, _data_FX_mode_RGB_Blobs5); + strip.addEffect(234, &mode_RGB_Blobs4, _data_FX_mode_RGB_Blobs4); + strip.addEffect(235, &mode_RGB_Blobs3, _data_FX_mode_RGB_Blobs3); + strip.addEffect(236, &mode_RGB_Blobs2, _data_FX_mode_RGB_Blobs2); + strip.addEffect(237, &mode_RGB_Blobs, _data_FX_mode_RGB_Blobs); + strip.addEffect(238, &mode_Polar_Waves, _data_FX_mode_Polar_Waves); + strip.addEffect(239, &mode_Slow_Fade, _data_FX_mode_Slow_Fade); + strip.addEffect(240, &mode_Zoom, _data_FX_mode_Zoom); + strip.addEffect(241, &mode_Hot_Blob, _data_FX_mode_Hot_Blob); + strip.addEffect(242, &mode_Spiralus2, _data_FX_mode_Spiralus2); + strip.addEffect(243, &mode_Spiralus, _data_FX_mode_Spiralus); + strip.addEffect(244, &mode_Yves, _data_FX_mode_Yves); + strip.addEffect(245, &mode_Scaledemo1, _data_FX_mode_Scaledemo1); + strip.addEffect(246, &mode_Lava1, _data_FX_mode_Lava1); + strip.addEffect(247, &mode_Caleido3, _data_FX_mode_Caleido3); + strip.addEffect(248, &mode_Caleido2, _data_FX_mode_Caleido2); + strip.addEffect(249, &mode_Caleido1, _data_FX_mode_Caleido1); + strip.addEffect(250, &mode_Distance_Experiment, _data_FX_mode_Distance_Experiment); + strip.addEffect(251, &mode_Center_Field, _data_FX_mode_Center_Field); + strip.addEffect(252, &mode_Waves, _data_FX_mode_Waves); + strip.addEffect(253, &mode_Chasing_Spirals, _data_FX_mode_Chasing_Spirals); + strip.addEffect(254, &mode_Rotating_Blob, _data_FX_mode_Rotating_Blob); + + initDone = true; + } + + void loop() { + if (!enabled || strip.isUpdating()) return; + + // do your magic here + if (millis() - lastTime > 1000) { + //USER_PRINTLN("I'm alive!"); + lastTime = millis(); + } + } + + void addToJsonInfo(JsonObject& root) + { + if(!enabled) return; + char myStringBuffer[16]; // buffer for snprintf() + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + + String uiDomString = F("Animartrix requires the Creative Commons Attribution License CC BY-NC 3.0"); + infoArr.add(uiDomString); + } + + uint16_t getId() + { + return USERMOD_ID_ANIMARTRIX; + } + +}; + + +#if defined(ARDUINO_ARCH_ESP32) && defined(ESP_IDF_VERSION) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + // restore original gcc optimization level + #pragma GCC pop_options +#endif +#endif + diff --git a/usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h b/usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h new file mode 100644 index 0000000000..a4c557dd21 --- /dev/null +++ b/usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h @@ -0,0 +1,525 @@ +#pragma once + +/* + @title MoonModules WLED - auto-playlist usermod + @file usermod_v2_auto_playlist.h + @repo https://github.com/MoonModules/WLED, submit changes to this file as PRs to MoonModules/WLED + @Authors https://github.com/MoonModules/WLED/commits/mdev/ + @Copyright Ā© 2024 Github MoonModules Commit Authors (contact moonmodules@icloud.com for details) + @license Licensed under the EUPL-1.2 or later + +*/ + + +#ifdef WLED_DEBUG + #ifndef USERMOD_AUTO_PLAYLIST_DEBUG + #define USERMOD_AUTO_PLAYLIST_DEBUG + #endif +#endif + +#include "wled.h" + +class AutoPlaylistUsermod : public Usermod { + + private: + + // experimental parameters by softhack007 - more balanced but need testing + const uint_fast32_t MAX_DISTANCE_TRACKER = 184; // maximum accepted distance_tracker + const uint_fast32_t ENERGY_SCALE = 1500; + const float FILTER_SLOW1 = 0.0075f; // for "slow" energy - was 0.01f + const float FILTER_SLOW2 = 0.005f; // for "slow" lfc / zcr - was 0.01f + const float FILTER_FAST1 = 0.2f; // for "fast" energy - was 0.10f + const float FILTER_FAST2 = 0.25f; // for "fast" lfc / zcr - was 0.10f + const float FILTER_VOLUME = 0.03f; // for volumeSmth averaging - takes 8-10sec until "silence" + + bool initDone = false; + bool functionality_enabled = false; + bool silenceDetected = true; + byte ambientPlaylist = 1; + byte musicPlaylist = 2; + int timeout = 60; + bool autoChange = false; + byte lastAutoPlaylist = 0; + unsigned long lastSoundTime = millis()-(timeout*1000)-100; + unsigned long change_timer = millis(); + unsigned long autochange_timer = millis(); + float avg_volumeSmth = 0; + + // fftesult de-scaling factors: 2.8f / fftResultPink[] + const float fftDeScaler[NUM_GEQ_CHANNELS] = {2.8/2.35, 2.8/1.32, 2.8/1.32, 2.8/1.40, 2.8/1.48, 2.8/1.57, 2.8/1.68, 2.8/1.80, 2.8/1.89, 2.8/1.95, 2.8/2.14, 2.8/2.26, 2.8/2.50, 2.8/2.90, 2.8/4.20, 2.8/6.50}; + + uint_fast32_t energy = 0; + + float avg_long_energy = 250; + float avg_long_lfc = 1000; + float avg_long_zcr = 500; + + float avg_short_energy = 250; + float avg_short_lfc = 1000; + float avg_short_zcr = 500; + + bool resetFilters = true; // to (re)initialize filters on first run + uint_fast32_t vector_energy = 0; + uint_fast32_t vector_lfc = 0; + uint_fast32_t vector_zcr = 0; + + uint_fast32_t distance = 0; + uint_fast32_t distance_tracker = UINT_FAST32_MAX; + + unsigned long lastchange = millis(); + + int_fast16_t change_threshold = 50; // arbitrary starting point. + uint_fast16_t change_threshold_change = 0; + + int change_lockout = 1000; // never change below this number of millis. Ideally 60000/your_average_bpm*beats_to_skip = change_lockout (1000 = skip 2 beats at 120bpm) + int ideal_change_min = 10000; // ideally change patterns no less than this number of millis + int ideal_change_max = 20000; // ideally change patterns no more than this number of millis + + std::vector autoChangeIds; + + static const char _name[]; + static const char _autoPlaylistEnabled[]; + static const char _ambientPlaylist[]; + static const char _musicPlaylist[]; + static const char _timeout[]; + static const char _autoChange[]; + static const char _change_lockout[]; + static const char _ideal_change_min[]; + static const char _ideal_change_max[]; + + public: + + AutoPlaylistUsermod(bool enabled):Usermod("AutoPlaylist", enabled) { + // noop + } + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { + USER_PRINT(F("AutoPlaylistUsermod startup; enabled = ")); + USER_PRINT(enabled ? F("true"):F("false")); USER_PRINTLN(F(".")); + initDone = true; + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() { + // noop + } + + void change(um_data_t *um_data) { + + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + energy = 0; + + for (int i=0; i < NUM_GEQ_CHANNELS; i++) { + + // make an attempt to undo some "trying to look better" FFT manglings in AudioReactive postProcessFFTResults() + + float amplitude = float(fftResult[i]) * fftDeScaler[i]; // undo "pink noise" scaling + amplitude /= 0.85f + (float(i)/4.5f); // undo extra up-scaling for high frequencies + energy += roundf(amplitude * amplitude); // calc energy from amplitude + + } + + energy /= ENERGY_SCALE; // scale down so we get 0 sometimes + + uint16_t lfc = float(fftResult[0]) * fftDeScaler[0] / 0.85f; // might as well undo pink noise here too. + uint16_t zcr = *(uint16_t*)um_data->u_data[11]; + + // WLED-MM/TroyHacks: Calculate the long- and short-running averages + // and the individual vector distances. + + if (volumeSmth > 1.0f) { + + // initialize filters on first run + if (resetFilters) { + avg_short_energy = avg_long_energy = energy; + avg_short_lfc = avg_long_lfc = lfc; + avg_short_zcr = avg_long_zcr = zcr; + resetFilters = false; + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTLN("AutoPlaylist: Filters reset."); + #endif + } + + avg_long_energy = avg_long_energy + FILTER_SLOW1 * (float(energy) - avg_long_energy); + avg_long_lfc = avg_long_lfc + FILTER_SLOW2 * (float(lfc) - avg_long_lfc); + avg_long_zcr = avg_long_zcr + FILTER_SLOW2 * (float(zcr) - avg_long_zcr); + + avg_short_energy = avg_short_energy + FILTER_FAST1 * (float(energy) - avg_short_energy); + avg_short_lfc = avg_short_lfc + FILTER_FAST2 * (float(lfc) - avg_short_lfc); + avg_short_zcr = avg_short_zcr + FILTER_FAST2 * (float(zcr) - avg_short_zcr); + + // allegedly this is faster than pow(whatever,2) + vector_lfc = (avg_short_lfc-avg_long_lfc)*(avg_short_lfc-avg_long_lfc); + vector_energy = (avg_short_energy-avg_long_energy)*(avg_short_energy-avg_long_energy); + vector_zcr = (avg_short_zcr-avg_long_zcr)*(avg_short_zcr-avg_long_zcr); + + } + + distance = vector_lfc + vector_energy + vector_zcr; + + long change_interval = millis()-lastchange; + + if (distance < distance_tracker && change_interval > change_lockout && volumeSmth > 1.0f) { + distance_tracker = distance; + } + + // Debug for adjusting formulas, etc: + // USER_PRINTF("Distance: %5lu - v_lfc: %5lu v_energy: %5lu v_zcr: %5lu\n",(unsigned long)distance,(unsigned long)vector_lfc,(unsigned long)vector_energy,(unsigned long)vector_zcr); + + if ((millis() - change_timer) > ideal_change_min) { // softhack007 same result as "millis() > change_timer + ideal_change_min", but more robust against unsigned overflow + + // Make the analysis less sensitive if we miss the window. + // Sometimes the analysis lowers the change_threshold too much for + // the current music, especially after track changes or during + // sparse intros and breakdowns. + + if (change_interval > ideal_change_min && distance_tracker <= MAX_DISTANCE_TRACKER) { + + if (distance_tracker >= change_threshold) { + change_threshold_change = distance_tracker-change_threshold; + } else { + change_threshold_change = change_threshold-distance_tracker; + } + + change_threshold = distance_tracker; + + if (change_threshold_change > 9999) change_threshold_change = 0; // cosmetic for debug + + if (functionality_enabled) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("--- lowest distance = %4lu - no changes done in %6ldms - next change_threshold is %4u (%4u diff approx)\n", (unsigned long)distance_tracker,change_interval,change_threshold,change_threshold_change); + #endif + } + + distance_tracker = UINT_FAST32_MAX; + + } + + change_timer = millis(); + + } + + if (distance <= change_threshold && change_interval > change_lockout && volumeSmth > 1.0f) { + + change_threshold_change = max(1.0f, roundf(change_threshold-(distance*0.9f))); // exclude negatives, ensure change_threshold_change is always >= 1 + + if (change_interval > ideal_change_max) { + change_threshold += change_threshold_change; // make changes more sensitive + } else if (change_interval < ideal_change_min) { + change_threshold -= change_threshold_change; // make changes less sensitive + } else { + change_threshold_change = 0; // change was within our window, no sensitivity change + } + + if (change_threshold < 1) change_threshold = 0; // we need change_threshold to be signed because otherwise this wraps to UINT_FAST16_MAX + + distance_tracker = UINT_FAST32_MAX; + + if (functionality_enabled) { + + if (autoChangeIds.size() == 0) { + if(currentPlaylist < 1) return; + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("Loading presets from playlist: %3d\n", currentPlaylist); + #endif + + JsonObject playtlistOjb = doc.to(); + serializePlaylist(playtlistOjb); + JsonArray playlistArray = playtlistOjb["playlist"]["ps"]; + + for(JsonVariant v : playlistArray) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("Adding %3u to autoChangeIds\n", v.as()); + #endif + autoChangeIds.push_back(v.as()); + } + + } + + uint8_t newpreset = 0; + + do { + newpreset = autoChangeIds.at(random(0, autoChangeIds.size())); // random() is *exclusive* of the last value, so it's OK to use the full size. + } while ((currentPreset == newpreset) && (autoChangeIds.size() > 1)); // make sure we get a different random preset. Unless there is only one. + + if (change_interval > change_lockout+3) { + + // Make sure we have a statistically significant change and we aren't + // just bouncing off change_lockout. That's valid for changing the + // thresholds, but might be a bit crazy for lighting changes. + // When the music changes quite a bit, the distance calculation can + // go into freefall - this logic stops that from triggering right + // after change_lockout. Better for smaller change_lockout values. + + suspendPlaylist(); // suspend the playlist engine before changing to another preset + applyPreset(newpreset); + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("*** CHANGE distance = %4lu - change_interval was %5ldms - next change_threshold is %4u (%4u diff aprox)\n",(unsigned long)distance,change_interval,change_threshold,change_threshold_change); + #endif + + } else { + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("^^^ SKIP!! distance = %4lu - change_interval was %5ldms - next change_threshold is %4u (%4u diff aprox)\n",(unsigned long)distance,change_interval,change_threshold,change_threshold_change); + #endif + + } + + } + + lastchange = millis(); + change_timer = millis(); + + } + + } + + /* + * Da loop. + */ + void loop() { + + if (!enabled) return; + + if (millis() < 10000) return; // Wait for device to settle + + if (lastAutoPlaylist > 0 && currentPlaylist != lastAutoPlaylist && currentPreset != 0) { + if (functionality_enabled) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: disable due to manual change of playlist from %u to %d, preset:%u\n", lastAutoPlaylist, currentPlaylist, currentPreset); + #endif + suspendPlaylist(); + functionality_enabled = false; + } else if (currentPlaylist == musicPlaylist) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: enabled due to manual change of playlist back to %u\n", currentPlaylist); + #endif + functionality_enabled = true; + lastAutoPlaylist = currentPlaylist; + } + } + + if (!functionality_enabled && currentPlaylist == musicPlaylist) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: enabled due selecting musicPlaylist(%u)\n", musicPlaylist); + #endif + functionality_enabled = true; + } + + if (bri == 0) return; + + if(!functionality_enabled) return; + + um_data_t *um_data; + + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // No Audio Reactive + silenceDetected = true; + return; + } + + float volumeSmth = *(float*)um_data->u_data[0]; + + avg_volumeSmth = avg_volumeSmth + FILTER_VOLUME * (volumeSmth - avg_volumeSmth); + + if (avg_volumeSmth >= 1.0f) { + lastSoundTime = millis(); + } + + if (millis() - lastSoundTime > (long(timeout) * 1000)) { + if (!silenceDetected) { + silenceDetected = true; + USER_PRINTLN("AutoPlaylist: Silence detected"); + changePlaylist(ambientPlaylist); + } + } else { + if (silenceDetected) { + silenceDetected = false; + USER_PRINTLN("AutoPlaylist: Sound detected"); + changePlaylist(musicPlaylist); + } + if (autoChange && millis() >= autochange_timer+22) { + change(um_data); + autochange_timer = millis(); + } + } + } + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonInfo(JsonObject& root) { + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); + } + + if (!enabled) return; // usermod disabled -> don't add to info page + + String uiNameString = FPSTR(_name); + if (enabled && functionality_enabled) { + uiNameString += F(" Running"); + } else if (!enabled) { + uiNameString += F(" Disabled"); + } else { + uiNameString += F(" Suspended"); + } + JsonArray infoArr = user.createNestedArray(uiNameString); // name + status + + String uiDomString = (currentPlaylist > 0) ? String("#") + String(currentPlaylist) + String(" ") : String(""); + + if (currentPlaylist == musicPlaylist && currentPlaylist > 0) { + uiDomString += F("Music Playlist"); + } else if (currentPlaylist == ambientPlaylist && currentPlaylist > 0) { + uiDomString += F("Ambient Playlist"); + } else { + uiDomString += F("Playlist Overridden"); + } + + uiDomString += F("
"); + + if (enabled && autoChange && currentPlaylist == musicPlaylist && functionality_enabled) { + uiDomString += F("AutoChange is Active"); + } else if (autoChange && (currentPlaylist != musicPlaylist || !functionality_enabled || !enabled)) { + uiDomString += F("AutoChange on Stand-by"); + } else if (!autoChange) { + uiDomString += F("AutoChange is Disabled"); + } + + // #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + // uiDomString += F("
"); + // uiDomString += F("Change Threshold: "); + // uiDomString += String(change_threshold); + // #endif + + infoArr.add(uiDomString); + + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) { + return; + } + + void appendConfigData() { + oappend(SET_F("addHB('AutoPlaylist');")); + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) { + + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + + top[FPSTR(_autoPlaylistEnabled)] = enabled; + top[FPSTR(_timeout)] = timeout; + top[FPSTR(_ambientPlaylist)] = ambientPlaylist; // usermodparam + top[FPSTR(_musicPlaylist)] = musicPlaylist; // usermodparam + top[FPSTR(_autoChange)] = autoChange; + top[FPSTR(_change_lockout)] = change_lockout; + top[FPSTR(_ideal_change_min)] = ideal_change_min; + top[FPSTR(_ideal_change_max)] = ideal_change_max; + + lastAutoPlaylist = 0; + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTLN(F("AutoPlaylist config saved.")); + #endif + + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject& root) { + + JsonObject top = root[FPSTR(_name)]; + + if (top.isNull()) { + USER_PRINT(FPSTR(_name)); + USER_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_autoPlaylistEnabled)] | enabled; + timeout = top[FPSTR(_timeout)] | timeout; + ambientPlaylist = top[FPSTR(_ambientPlaylist)] | ambientPlaylist; + musicPlaylist = top[FPSTR(_musicPlaylist)] | musicPlaylist; + autoChange = top[FPSTR(_autoChange)] | autoChange; + change_lockout = top[FPSTR(_change_lockout)] | change_lockout; + ideal_change_min = top[FPSTR(_ideal_change_min)] | ideal_change_min; + ideal_change_max = top[FPSTR(_ideal_change_max)] | ideal_change_max; + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINT(FPSTR(_name)); + USER_PRINTLN(F(" config (re)loaded.")); + #endif + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; + + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_AUTOPLAYLIST; + } + + private: + + void changePlaylist(byte id) { + String name = ""; + getPresetName(id, name); + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: Applying \"%s\"\n", name.c_str()); + #endif + // if (currentPlaylist != id) { // un-comment to only change on "real" changes + unloadPlaylist(); // applying a preset requires to unload previous playlist + applyPreset(id, CALL_MODE_NOTIFICATION); + // } + lastAutoPlaylist = id; + } + +}; + +const char AutoPlaylistUsermod::_name[] PROGMEM = "AutoPlaylist"; +const char AutoPlaylistUsermod::_autoPlaylistEnabled[] PROGMEM = "enabled"; +const char AutoPlaylistUsermod::_ambientPlaylist[] PROGMEM = "ambientPlaylist"; +const char AutoPlaylistUsermod::_musicPlaylist[] PROGMEM = "musicPlaylist"; +const char AutoPlaylistUsermod::_timeout[] PROGMEM = "timeout"; +const char AutoPlaylistUsermod::_autoChange[] PROGMEM = "autoChange"; +const char AutoPlaylistUsermod::_change_lockout[] PROGMEM = "change_lockout"; +const char AutoPlaylistUsermod::_ideal_change_min[] PROGMEM = "ideal_change_min"; +const char AutoPlaylistUsermod::_ideal_change_max[] PROGMEM = "ideal_change_max"; diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index 8283aeed1d..1dd0a69a54 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -79,6 +79,10 @@ class AutoSaveUsermod : public Usermod { month(localTime), day(localTime), hour(localTime), minute(localTime), second(localTime)); cacheInvalidate++; // force reload of presets + DEBUG_PRINT(F("UM autosave: saving preset ")); + DEBUG_PRINT(autoSavePreset); + DEBUG_PRINT(F(" => ")); + DEBUG_PRINTLN(presetNameBuffer); savePreset(autoSavePreset, presetNameBuffer); } @@ -86,7 +90,11 @@ class AutoSaveUsermod : public Usermod { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display != nullptr) { display->wakeDisplay(); + #if defined(USE_ALT_DISPLAY) || defined(USE_ALT_DISPlAY) + if (display->canDraw()) display->overlay("Settings", "Auto Saved", 1500); // WLEDMM bugfix + #else display->overlay("Settings", "Auto Saved", 1500); + #endif } #endif } @@ -101,7 +109,7 @@ class AutoSaveUsermod : public Usermod { // network here void setup() { #ifdef USERMOD_FOUR_LINE_DISPLAY - // This Usermod has enhanced funcionality if + // This Usermod has enhanced functionality if // FourLineDisplayUsermod is available. display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); #endif @@ -148,7 +156,7 @@ class AutoSaveUsermod : public Usermod { if (autoSaveAfter && now > autoSaveAfter) { autoSaveAfter = 0; - // Time to auto save. You may have some flickry? + // Time to auto save. You may have some flickery? saveSettings(); displayOverlay(); } @@ -204,6 +212,10 @@ class AutoSaveUsermod : public Usermod { } } + void appendConfigData() { + oappend(SET_F("addHB('Autosave');")); + } + /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) diff --git a/usermods/usermod_v2_four_line_display/readme.md b/usermods/usermod_v2_four_line_display/readme.md deleted file mode 100644 index 26250cb5c5..0000000000 --- a/usermods/usermod_v2_four_line_display/readme.md +++ /dev/null @@ -1,63 +0,0 @@ -# I2C 4 Line Display Usermod - -First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. - -Provides a four line display using either -128x32 or 128x64 OLED displays. -It can operate independently, but starts to provide -a relatively complete on-device UI when paired with the -Rotary Encoder UI usermod. I strongly encourage you to use -them together. - -[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) - -## Installation - -Copy and update the example `platformio_override.ini.sample` -from the Rotary Encoder UI usermode folder to the root directory of your particular build. -This file should be placed in the same directory as `platformio.ini`. - -### Define Your Options - -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, the display is available -* `FLD_PIN_SCL` - The display SCL pin, defaults to 5 -* `FLD_PIN_SDA` - The display SDA pin, defaults to 4 - -All of the parameters can be configured via the Usermods settings page, inluding GPIO pins. - -### PlatformIO requirements - -This usermod requires the `U8g2` and `Wire` libraries. See the -`platformio_override.ini.sample` found in the Rotary Encoder -UI usermod folder for how to include these using `platformio_override.ini`. - -## Configuration - -* `enabled` - enable/disable usermod -* `pin` - GPIO pins used for display; I2C displays use Clk & Data; SPI displays can use SCK, MOSI, CS, DC & RST -* `type` - display type in numeric format - * 1 = I2C SSD1306 128x32 - * 2 = I2C SH1106 128x32 - * 3 = I2C SSD1306 128x64 (4 double-height lines) - * 4 = I2C SSD1305 128x32 - * 5 = I2C SSD1305 128x64 (4 double-height lines) - * 6 = SPI SSD1306 128x32 - * 7 = SPI SSD1306 128x64 (4 double-height lines) -* `contrast` - set display contrast (higher contrast may reduce display lifetime) -* `refreshRateSec` - display refresh time in seconds -* `screenTimeOutSec` - screen saver time-out in seconds -* `flip` - flip/rotate display 180° -* `sleepMode` - enable/disable screen saver -* `clockMode` - enable/disable clock display in screen saver mode -* `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) - -## Change Log - -2021-02 -* First public release - -2021-04 -* Adaptation for runtime configuration. - -2021-11 -* Added configuration option description. diff --git a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h deleted file mode 100644 index 3fcf661289..0000000000 --- a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h +++ /dev/null @@ -1,742 +0,0 @@ -#pragma once - -#include "wled.h" -#include // from https://github.com/olikraus/u8g2/ - -// -// Insired by the v1 usermod: ssd1306_i2c_oled_u8g2 -// -// v2 usermod for using 128x32 or 128x64 i2c -// OLED displays to provide a four line display -// for WLED. -// -// Dependencies -// * This usermod REQURES the ModeSortUsermod -// * This Usermod works best, by far, when coupled -// with RotaryEncoderUIUsermod. -// -// Make sure to enable NTP and set your time zone in WLED Config | Time. -// -// REQUIREMENT: You must add the following requirements to -// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini -// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) -// REQUIREMENT: * Wire -// - -//The SCL and SDA pins are defined here. -#ifndef FLD_PIN_SCL - #define FLD_PIN_SCL i2c_scl -#endif -#ifndef FLD_PIN_SDA - #define FLD_PIN_SDA i2c_sda -#endif -#ifndef FLD_PIN_CLOCKSPI - #define FLD_PIN_CLOCKSPI spi_sclk -#endif - #ifndef FLD_PIN_DATASPI - #define FLD_PIN_DATASPI spi_mosi -#endif -#ifndef FLD_PIN_CS - #define FLD_PIN_CS spi_cs -#endif -#ifdef ARDUINO_ARCH_ESP32 - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 19 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 26 - #endif -#else - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 12 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 16 - #endif -#endif - -#ifndef FLD_TYPE - #ifndef FLD_SPI_DEFAULT - #define FLD_TYPE SSD1306 - #else - #define FLD_TYPE SSD1306_SPI - #endif -#endif - -// When to time out to the clock or blank the screen -// if SLEEP_MODE_ENABLED. -#define SCREEN_TIMEOUT_MS 60*1000 // 1 min - -#define TIME_INDENT 0 -#define DATE_INDENT 2 - -// Minimum time between redrawing screen in ms -#define USER_LOOP_REFRESH_RATE_MS 1000 - -// Extra char (+1) for null -#define LINE_BUFFER_SIZE 16+1 - -typedef enum { - FLD_LINE_BRIGHTNESS = 0, - FLD_LINE_EFFECT_SPEED, - FLD_LINE_EFFECT_INTENSITY, - FLD_LINE_MODE, - FLD_LINE_PALETTE, - FLD_LINE_TIME -} Line4Type; - -typedef enum { - NONE = 0, - SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C - SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C - SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C - SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C - SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C - SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI - SSD1306_SPI64 // U8X8_SSD1306_128X64_NONAME_HW_SPI -} DisplayType; - -class FourLineDisplayUsermod : public Usermod { - - private: - - bool initDone = false; - unsigned long lastTime = 0; - - // HW interface & configuration - U8X8 *u8x8 = nullptr; // pointer to U8X8 display object - #ifndef FLD_SPI_DEFAULT - int8_t ioPin[5] = {FLD_PIN_SCL, FLD_PIN_SDA, -1, -1, -1}; // I2C pins: SCL, SDA - uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) - #else - int8_t ioPin[5] = {FLD_PIN_CLOCKSPI, FLD_PIN_DATASPI, FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // SPI pins: CLK, MOSI, CS, DC, RST - uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) - #endif - DisplayType type = FLD_TYPE; // display type - bool flip = false; // flip display 180° - uint8_t contrast = 10; // screen contrast - uint8_t lineHeight = 1; // 1 row or 2 rows - uint32_t refreshRate = USER_LOOP_REFRESH_RATE_MS; // in ms - uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms - bool sleepMode = true; // allow screen sleep? - bool clockMode = false; // display clock - bool enabled = true; - - // Next variables hold the previous known values to determine if redraw is - // required. - String knownSsid = ""; - IPAddress knownIp; - uint8_t knownBrightness = 0; - uint8_t knownEffectSpeed = 0; - uint8_t knownEffectIntensity = 0; - uint8_t knownMode = 0; - uint8_t knownPalette = 0; - uint8_t knownMinute = 99; - uint8_t knownHour = 99; - - bool displayTurnedOff = false; - unsigned long lastUpdate = 0; - unsigned long lastRedraw = 0; - unsigned long overlayUntil = 0; - Line4Type lineType = FLD_LINE_BRIGHTNESS; - // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. - byte markLineNum = 0; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _contrast[]; - static const char _refreshRate[]; - static const char _screenTimeOut[]; - static const char _flip[]; - static const char _sleepMode[]; - static const char _clockMode[]; - static const char _busClkFrequency[]; - - // If display does not work or looks corrupted check the - // constructor reference: - // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp - // or check the gallery: - // https://github.com/olikraus/u8g2/wiki/gallery - - public: - - // gets called once at boot. Do all initialization that doesn't depend on - // network here - void setup() { - if (type == NONE || !enabled) return; - - bool isHW; - PinOwner po = PinOwner::UM_FourLineDisplay; - if (type == SSD1306_SPI || type == SSD1306_SPI64) { - isHW = (ioPin[0]==spi_sclk && ioPin[1]==spi_mosi); - if (isHW) po = PinOwner::HW_SPI; // allow multiple allocations of HW I2C bus pins - PinManagerPinType pins[5] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true }, { ioPin[3], true }, { ioPin[4], true }}; - if (!pinManager.allocateMultiplePins(pins, 5, po)) { type=NONE; return; } - } else { - isHW = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); - if (isHW) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; - if (!pinManager.allocateMultiplePins(pins, 2, po)) { type=NONE; return; } - } - - DEBUG_PRINTLN(F("Allocating display.")); - switch (type) { - case SSD1306: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - lineHeight = 1; - break; - case SH1106: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - lineHeight = 2; - break; - case SSD1306_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - lineHeight = 2; - break; - case SSD1305: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - lineHeight = 1; - break; - case SSD1305_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - lineHeight = 2; - break; - case SSD1306_SPI: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - lineHeight = 1; - break; - case SSD1306_SPI64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - lineHeight = 2; - break; - default: - u8x8 = nullptr; - } - - if (nullptr == u8x8) { - DEBUG_PRINTLN(F("Display init failed.")); - pinManager.deallocateMultiplePins((const uint8_t*)ioPin, (type == SSD1306_SPI || type == SSD1306_SPI64) ? 5 : 2, po); - type = NONE; - return; - } - - initDone = true; - DEBUG_PRINTLN(F("Starting display.")); - /*if (!(type == SSD1306_SPI || type == SSD1306_SPI64))*/ u8x8->setBusClock(ioFrequency); // can be used for SPI too - u8x8->begin(); - setFlipMode(flip); - setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 - setPowerSave(0); - drawString(0, 0, "Loading..."); - } - - // gets called every time WiFi is (re-)connected. Initialize own network - // interfaces here - void connected() {} - - /** - * Da loop. - */ - void loop() { - if (!enabled || millis() - lastUpdate < (clockMode?1000:refreshRate) || strip.isUpdating()) return; - lastUpdate = millis(); - - redraw(false); - } - - /** - * Wrappers for screen drawing - */ - void setFlipMode(uint8_t mode) { - if (type == NONE || !enabled) return; - u8x8->setFlipMode(mode); - } - void setContrast(uint8_t contrast) { - if (type == NONE || !enabled) return; - u8x8->setContrast(contrast); - } - void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - u8x8->setFont(u8x8_font_chroma48medium8_r); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); - else u8x8->drawString(col, row, string); - } - void draw2x2String(uint8_t col, uint8_t row, const char *string) { - if (type == NONE || !enabled) return; - u8x8->setFont(u8x8_font_chroma48medium8_r); - u8x8->draw2x2String(col, row, string); - } - void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - u8x8->setFont(font); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); - else u8x8->drawGlyph(col, row, glyph); - } - uint8_t getCols() { - if (type==NONE || !enabled) return 0; - return u8x8->getCols(); - } - void clear() { - if (type == NONE || !enabled) return; - u8x8->clear(); - } - void setPowerSave(uint8_t save) { - if (type == NONE || !enabled) return; - u8x8->setPowerSave(save); - } - - void center(String &line, uint8_t width) { - int len = line.length(); - if (len0; i--) line = ' ' + line; - for (byte i=line.length(); i 0) { - if (now >= overlayUntil) { - // Time to display the overlay has elapsed. - overlayUntil = 0; - forceRedraw = true; - } else { - // We are still displaying the overlay - // Don't redraw. - return; - } - } - - // Check if values which are shown on display changed from the last time. - if (forceRedraw || - (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) || - (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) || - (knownBrightness != bri) || - (knownEffectSpeed != effectSpeed) || - (knownEffectIntensity != effectIntensity) || - (knownMode != strip.getMainSegment().mode) || - (knownPalette != strip.getMainSegment().palette)) { - knownHour = 99; // force time update - lastRedraw = now; // update lastRedraw marker - } else if (sleepMode && !displayTurnedOff && ((now - lastRedraw)/1000)%5 == 0) { - // change line every 5s - showName = !showName; - switch (lineType) { - case FLD_LINE_BRIGHTNESS: - lineType = FLD_LINE_EFFECT_SPEED; - break; - case FLD_LINE_MODE: - lineType = FLD_LINE_BRIGHTNESS; - break; - case FLD_LINE_PALETTE: - lineType = clockMode ? FLD_LINE_MODE : FLD_LINE_BRIGHTNESS; - break; - case FLD_LINE_EFFECT_SPEED: - lineType = FLD_LINE_EFFECT_INTENSITY; - break; - case FLD_LINE_EFFECT_INTENSITY: - lineType = FLD_LINE_PALETTE; - break; - default: - lineType = FLD_LINE_MODE; - break; - } - knownHour = 99; // force time update - // do not update lastRedraw marker if just switching row contenet - } else { - // Nothing to change. - // Turn off display after 3 minutes with no change. - if(sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { - // We will still check if there is a change in redraw() - // and turn it back on if it changed. - sleepOrClock(true); - } else if (displayTurnedOff && clockMode) { - showTime(); - } - return; - } - - // Turn the display back on - if (displayTurnedOff) sleepOrClock(false); - - // Update last known values. - knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); - knownIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); - knownBrightness = bri; - knownMode = strip.getMainSegment().mode; - knownPalette = strip.getMainSegment().palette; - knownEffectSpeed = effectSpeed; - knownEffectIntensity = effectIntensity; - - // Do the actual drawing - String line; - // First row with Wifi name - drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // home icon - line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); - center(line, getCols()-2); - drawString(1, 0, line.c_str()); - // Print `~` char to indicate that SSID is longer, than our display - if (knownSsid.length() > (int)getCols()-1) { - drawString(getCols() - 1, 0, "~"); - } - - // Second row with IP or Psssword - drawGlyph(0, lineHeight, 68, u8x8_font_open_iconic_embedded_1x1); // wifi icon - // Print password in AP mode and if led is OFF. - if (apActive && bri == 0) { - drawString(1, lineHeight, apPass); - } else { - // alternate IP address and server name - line = knownIp.toString(); - if (showName && strcmp(serverDescription, "WLED") != 0) { - line = serverDescription; - } - center(line, getCols()-1); - drawString(1, lineHeight, line.c_str()); - } - - // draw third and fourth row - drawLine(2, clockMode ? lineType : FLD_LINE_MODE); - drawLine(3, clockMode ? FLD_LINE_TIME : lineType); - - drawGlyph(0, 2*lineHeight, 66 + (bri > 0 ? 3 : 0), u8x8_font_open_iconic_weather_2x2); // sun/moon icon - //if (markLineNum>1) drawGlyph(2, markLineNum*lineHeight, 66, u8x8_font_open_iconic_arrow_1x1); // arrow icon - } - - void drawLine(uint8_t line, Line4Type lineType) { - char lineBuffer[LINE_BUFFER_SIZE]; - uint8_t printedChars; - switch(lineType) { - case FLD_LINE_BRIGHTNESS: - sprintf_P(lineBuffer, PSTR("Brightness %3d"), bri); - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_EFFECT_SPEED: - sprintf_P(lineBuffer, PSTR("FX Speed %3d"), effectSpeed); - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_EFFECT_INTENSITY: - sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity); - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_MODE: - printedChars = extractModeName(knownMode, JSON_mode_names, lineBuffer, LINE_BUFFER_SIZE-1); - for (;printedChars < getCols()-2 && printedChars < LINE_BUFFER_SIZE-3; printedChars++) lineBuffer[printedChars]=' '; - lineBuffer[printedChars] = 0; - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_PALETTE: - printedChars = extractModeName(knownPalette, JSON_palette_names, lineBuffer, LINE_BUFFER_SIZE-1); - for (;printedChars < getCols()-2 && printedChars < LINE_BUFFER_SIZE-3; printedChars++) lineBuffer[printedChars]=' '; - lineBuffer[printedChars] = 0; - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_TIME: - default: - showTime(false); - break; - } - } - - /** - * If there screen is off or in clock is displayed, - * this will return true. This allows us to throw away - * the first input from the rotary encoder but - * to wake up the screen. - */ - bool wakeDisplay() { - if (type == NONE || !enabled) return false; - knownHour = 99; - if (displayTurnedOff) { - // Turn the display back on - sleepOrClock(false); - redraw(true); - return true; - } - return false; - } - - /** - * Allows you to show up to two lines as overlay for a - * period of time. - * Clears the screen and prints on the middle two lines. - */ - void overlay(const char* line1, const char *line2, long showHowLong) { - if (type == NONE || !enabled) return; - - if (displayTurnedOff) { - // Turn the display back on (includes clear()) - sleepOrClock(false); - } else { - clear(); - } - - // Print the overlay - if (line1) { - String buf = line1; - center(buf, getCols()); - drawString(0, 1*lineHeight, buf.c_str()); - } - if (line2) { - String buf = line2; - center(buf, getCols()); - drawString(0, 2*lineHeight, buf.c_str()); - } - overlayUntil = millis() + showHowLong; - } - - void setLineType(byte lT) { - lineType = (Line4Type) lT; - } - - /** - * Line 3 or 4 (last two lines) can be marked with an - * arrow in the first column. Pass 2 or 3 to this to - * specify which line to mark with an arrow. - * Any other values are ignored. - */ - void setMarkLine(byte newMarkLineNum) { - if (newMarkLineNum == 2 || newMarkLineNum == 3) { - markLineNum = newMarkLineNum; - } - else { - markLineNum = 0; - } - } - - /** - * Enable sleep (turn the display off) or clock mode. - */ - void sleepOrClock(bool enabled) { - clear(); - if (enabled) { - if (clockMode) showTime(); - else setPowerSave(1); - displayTurnedOff = true; - } else { - setPowerSave(0); - displayTurnedOff = false; - } - } - - /** - * Display the current date and time in large characters - * on the middle rows. Based 24 or 12 hour depending on - * the useAMPM configuration. - */ - void showTime(bool fullScreen = true) { - if (type == NONE || !enabled) return; - char lineBuffer[LINE_BUFFER_SIZE]; - - updateLocalTime(); - byte minuteCurrent = minute(localTime); - byte hourCurrent = hour(localTime); - byte secondCurrent = second(localTime); - if (knownMinute == minuteCurrent && knownHour == hourCurrent) { - // Time hasn't changed. - if (!fullScreen) return; - } - knownMinute = minuteCurrent; - knownHour = hourCurrent; - - byte currentMonth = month(localTime); - sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(currentMonth), day(localTime)); - if (fullScreen) - draw2x2String(DATE_INDENT, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays - else - drawString(2, lineHeight*3, lineBuffer); - - byte showHour = hourCurrent; - boolean isAM = false; - if (useAMPM) { - if (showHour == 0) { - showHour = 12; - isAM = true; - } - else if (showHour > 12) { - showHour -= 12; - isAM = false; - } - else { - isAM = true; - } - } - - sprintf_P(lineBuffer, (secondCurrent%2 || !fullScreen) ? PSTR("%2d:%02d") : PSTR("%2d %02d"), (useAMPM ? showHour : hourCurrent), minuteCurrent); - // For time, we always use LINE_HEIGHT of 2 since - // we are printing it big. - if (fullScreen) { - draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer); - sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); - if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*2, (isAM ? "AM" : "PM"), true); - else drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line - } else { - drawString(9+(useAMPM?0:2), lineHeight*3, lineBuffer); - if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*3, (isAM ? "AM" : "PM"), true); - } - } - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - //void addToJsonInfo(JsonObject& root) { - //JsonObject user = root["u"]; - //if (user.isNull()) user = root.createNestedObject("u"); - //JsonArray data = user.createNestedArray(F("4LineDisplay")); - //data.add(F("Loaded.")); - //} - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void addToJsonState(JsonObject& root) { - //} - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void readFromJsonState(JsonObject& root) { - // if (!initDone) return; // prevent crash on boot applyPreset() - //} - - /* - * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will also not yet add your setting to one of the settings pages automatically. - * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * - * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! - */ - void addToConfig(JsonObject& root) { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - JsonArray io_pin = top.createNestedArray("pin"); - for (byte i=0; i<5; i++) io_pin.add(ioPin[i]); - top["help4Pins"] = F("Clk,Data,CS,DC,RST"); // help for Settings page - top["type"] = type; - top["help4Type"] = F("1=SSD1306,2=SH1106,3=SSD1306_128x64,4=SSD1305,5=SSD1305_128x64,6=SSD1306_SPI,7=SSD1306_SPI_128x64"); // help for Settings page - top[FPSTR(_flip)] = (bool) flip; - top[FPSTR(_contrast)] = contrast; - top[FPSTR(_refreshRate)] = refreshRate/1000; - top[FPSTR(_screenTimeOut)] = screenTimeout/1000; - top[FPSTR(_sleepMode)] = (bool) sleepMode; - top[FPSTR(_clockMode)] = (bool) clockMode; - top[FPSTR(_busClkFrequency)] = ioFrequency/1000; - DEBUG_PRINTLN(F("4 Line Display config saved.")); - } - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) - */ - bool readFromConfig(JsonObject& root) { - bool needsRedraw = false; - DisplayType newType = type; - int8_t newPin[5]; for (byte i=0; i<5; i++) newPin[i] = ioPin[i]; - - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - enabled = top[FPSTR(_enabled)] | enabled; - newType = top["type"] | newType; - for (byte i=0; i<5; i++) newPin[i] = top["pin"][i] | ioPin[i]; - flip = top[FPSTR(_flip)] | flip; - contrast = top[FPSTR(_contrast)] | contrast; - refreshRate = (top[FPSTR(_refreshRate)] | refreshRate/1000) * 1000; - screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; - sleepMode = top[FPSTR(_sleepMode)] | sleepMode; - clockMode = top[FPSTR(_clockMode)] | clockMode; - if (newType == SSD1306_SPI || newType == SSD1306_SPI64) - ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - else - ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json - for (byte i=0; i<5; i++) ioPin[i] = newPin[i]; - type = newType; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - bool pinsChanged = false; - for (byte i=0; i<5; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } - if (pinsChanged || type!=newType) { - if (type != NONE) delete u8x8; - PinOwner po = PinOwner::UM_FourLineDisplay; - bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64); - if (isSPI) { - if (ioPin[0]==spi_sclk && ioPin[1]==spi_mosi) po = PinOwner::HW_SPI; // allow multiple allocations of HW SPI bus pins - pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 5, po); - } else { - if (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 2, po); - } - for (byte i=0; i<5; i++) ioPin[i] = newPin[i]; - if (ioPin[0]<0 || ioPin[1]<0) { // data & clock must be > -1 - type = NONE; - return true; - } else type = newType; - setup(); - needsRedraw |= true; - } - if (!(type == SSD1306_SPI || type == SSD1306_SPI64)) u8x8->setBusClock(ioFrequency); // can be used for SPI too - setContrast(contrast); - setFlipMode(flip); - if (needsRedraw && !wakeDisplay()) redraw(true); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_enabled)].isNull(); - } - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() { - return USERMOD_ID_FOUR_LINE_DISP; - } -}; - -// strings to reduce flash memory usage (used more than twice) -const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay"; -const char FourLineDisplayUsermod::_enabled[] PROGMEM = "enabled"; -const char FourLineDisplayUsermod::_contrast[] PROGMEM = "contrast"; -const char FourLineDisplayUsermod::_refreshRate[] PROGMEM = "refreshRateSec"; -const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec"; -const char FourLineDisplayUsermod::_flip[] PROGMEM = "flip"; -const char FourLineDisplayUsermod::_sleepMode[] PROGMEM = "sleepMode"; -const char FourLineDisplayUsermod::_clockMode[] PROGMEM = "clockMode"; -const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md index ea9f436109..00f70caff0 100644 --- a/usermods/usermod_v2_four_line_display_ALT/readme.md +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -31,8 +31,8 @@ Also shows if the timer is enabled ## Installation Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions -Then to activate this alternative usermod add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, - or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file +Then to activate this alternative usermod add `#define USE_ALT_DISPLAY` to the `usermods_list.cpp` file, + or add `-D USE_ALT_DISPLAY` to the original `platformio_override.ini.sample` file ### PlatformIO requirements diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index ab244ef280..898545716d 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -1,60 +1,62 @@ #pragma once +#include // WLEDMM: make sure that I2C drivers have the "right" Wire Object +#include +#include +#undef U8X8_NO_HW_I2C // WLEDMM: we do want I2C hardware drivers - if possible +//#define WIRE_INTERFACES_COUNT 2 // experimental - tell U8x8Lib that there is a second Wire unit + #include "wled.h" #include // from https://github.com/olikraus/u8g2/ #include "4LD_wled_fonts.c" +#ifndef FLD_ESP32_NO_THREADS + #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! +#endif + +//#define OLD_4LD_FONTS // comment out if you prefer the "classic" look with blocky fonts (saves 1K flash) + // -// Insired by the usermod_v2_four_line_display +// Inspired by the usermod_v2_four_line_display // // v2 usermod for using 128x32 or 128x64 i2c // OLED displays to provide a four line display // for WLED. // // Dependencies -// * This usermod REQURES the ModeSortUsermod -// * This Usermod works best, by far, when coupled -// with RotaryEncoderUIUsermod. +// * This usermod does not REQUIRE the ModeSortUsermod any more +// * This usermod works best, by far, when coupled +// with RotaryEncoderUI_ALT usermod. // // Make sure to enable NTP and set your time zone in WLED Config | Time. // // REQUIREMENT: You must add the following requirements to // REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini -// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) -// REQUIREMENT: * Wire +// REQUIREMENT: olikraus/U8g2@ ^2.34.15 (the version already in platformio.ini is fine) // //The SCL and SDA pins are defined here. #ifndef FLD_PIN_SCL - #define FLD_PIN_SCL i2c_scl + #define FLD_PIN_SCL -1 #endif #ifndef FLD_PIN_SDA - #define FLD_PIN_SDA i2c_sda + #define FLD_PIN_SDA -1 #endif #ifndef FLD_PIN_CLOCKSPI - #define FLD_PIN_CLOCKSPI spi_sclk + #define FLD_PIN_CLOCKSPI -1 #endif - #ifndef FLD_PIN_DATASPI - #define FLD_PIN_DATASPI spi_mosi +#ifndef FLD_PIN_MOSISPI //WLEDMM renamed from HW_PIN_DATASPI + #define FLD_PIN_MOSISPI -1 #endif #ifndef FLD_PIN_CS - #define FLD_PIN_CS spi_cs + #define FLD_PIN_CS -1 #endif -#ifdef ARDUINO_ARCH_ESP32 - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 19 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 26 - #endif -#else - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 12 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 16 - #endif +#ifndef FLD_PIN_DC + #define FLD_PIN_DC -1 +#endif +#ifndef FLD_PIN_RESET + #define FLD_PIN_RESET -1 #endif #ifndef FLD_TYPE @@ -91,22 +93,29 @@ typedef enum { SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C - SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI - SSD1306_SPI64 // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI + SSD1306_SPI64=7, // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1309_SPI64=8, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI + SSD1327_SPI128=9,// U8X8_SSD1327_WS_128X128_4W_SW_SPI + SSD1309_64=10 // U8X8_SSD1309_128X64_NONAME2_HW_I2C } DisplayType; class FourLineDisplayUsermod : public Usermod { public: +#ifdef ARDUINO_ARCH_ESP32 FourLineDisplayUsermod() { if (!instance) instance = this; } static FourLineDisplayUsermod* getInstance(void) { return instance; } +#endif private: static FourLineDisplayUsermod *instance; bool initDone = false; - volatile bool drawing = false; - volatile bool lockRedraw = false; + volatile bool drawing = false; // true of overlay drawing is active + volatile bool reDrawing = false; // true if redraw ongoing (on esp32, this happens in a separate task) + + char errorMessage[100] = ""; //WLEDMM: show error in um settings if occurred // HW interface & configuration U8X8 *u8x8 = nullptr; // pointer to U8X8 display object @@ -115,11 +124,12 @@ class FourLineDisplayUsermod : public Usermod { int8_t ioPin[5] = {FLD_PIN_SCL, FLD_PIN_SDA, -1, -1, -1}; // I2C pins: SCL, SDA uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) #else - int8_t ioPin[5] = {FLD_PIN_CLOCKSPI, FLD_PIN_DATASPI, FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // SPI pins: CLK, MOSI, CS, DC, RST + int8_t ioPin[5] = {FLD_PIN_CLOCKSPI, FLD_PIN_MOSISPI, FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // SPI pins: CLK, MOSI, CS, DC, RST uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) #endif DisplayType type = FLD_TYPE; // display type + bool typeOK = false; //WLEDMM: instead of type == NULL and type=NULL. Initially false, as display was not initialized yet bool flip = false; // flip display 180° uint8_t contrast = 10; // screen contrast uint8_t lineHeight = 1; // 1 row or 2 rows @@ -128,8 +138,18 @@ class FourLineDisplayUsermod : public Usermod { bool sleepMode = true; // allow screen sleep? bool clockMode = false; // display clock bool showSeconds = true; // display clock with seconds +#if defined(ARDUINO_ARCH_ESP32) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) + bool enabled = false; // WLEDMM workaround for I2C bugs in IDF v4.4.1 +#else + bool enabled = true; +#endif +#else bool enabled = true; +#endif bool contrastFix = false; + bool driverHW = false; + bool driverSPI = false; // Next variables hold the previous known values to determine if redraw is // required. @@ -177,562 +197,97 @@ class FourLineDisplayUsermod : public Usermod { // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery - // some displays need this to properly apply contrast - void setVcomh(bool highContrast) { - u8x8_t *u8x8_struct = u8x8->getU8x8(); - u8x8_cad_StartTransfer(u8x8_struct); - u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value - u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64 - u8x8_cad_EndTransfer(u8x8_struct); + // WLEDMM is this display using SPI? + bool displayIsSPI(DisplayType disp) { + switch(disp) { + case SSD1306_SPI: // falls thru + case SSD1306_SPI64: // falls thru + case SSD1309_SPI64: // falls thru + case SSD1327_SPI128: + return true; // yes its SPI + break; // makes compiler happy + default: + return false; // no anything else is I2C + } } + // some displays need this to properly apply contrast + void setVcomh(bool highContrast); + /** * Wrappers for screen drawing */ - void setFlipMode(uint8_t mode) { - if (type == NONE || !enabled) return; - u8x8->setFlipMode(mode); - } - void setContrast(uint8_t contrast) { - if (type == NONE || !enabled) return; - u8x8->setContrast(contrast); - } - void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(u8x8_font_chroma48medium8_r); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); - else u8x8->drawString(col, row, string); - drawing = false; - } - void draw2x2String(uint8_t col, uint8_t row, const char *string) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(u8x8_font_chroma48medium8_r); - u8x8->draw2x2String(col, row, string); - drawing = false; - } - void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(font); - if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); - else u8x8->drawGlyph(col, row, glyph); - drawing = false; - } - void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->setFont(font); - u8x8->draw2x2Glyph(col, row, glyph); - drawing = false; - } - uint8_t getCols() { - if (type==NONE || !enabled) return 0; - return u8x8->getCols(); - } - void clear() { - if (type == NONE || !enabled) return; - drawing = true; - u8x8->clear(); - drawing = false; - } - void setPowerSave(uint8_t save) { - if (type == NONE || !enabled) return; - u8x8->setPowerSave(save); - } + void setFlipMode(uint8_t mode); + void setContrast(uint8_t contrast); + void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false); + void draw2x2String(uint8_t col, uint8_t row, const char *string); + void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false); + void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font); + uint8_t getCols(); + void clear(); + void setPowerSave(uint8_t save); - void center(String &line, uint8_t width) { - int len = line.length(); - if (len0; i--) line = ' ' + line; - for (byte i=line.length(); i 11) { AmPmHour -= 12; isitAM = false; } - if (AmPmHour == 0) { AmPmHour = 12; } - } - if (knownHour != hourCurrent) { - // only update date when hour changes - sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); - draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day - } - sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); - draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds - if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time - - drawStatusIcons(); //icons power, wifi, timer, etc - - knownMinute = minuteCurrent; - knownHour = hourCurrent; - } - if (showSeconds && secondCurrent != lastSecond) { - lastSecond = secondCurrent; - draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":"); - sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); - drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line - } - } - - /** - * Enable sleep (turn the display off) or clock mode. - */ - void sleepOrClock(bool enabled) { - if (enabled) { - displayTurnedOff = true; - if (clockMode && ntpEnabled) { - knownMinute = knownHour = 99; - showTime(); - } else - setPowerSave(1); - } else { - displayTurnedOff = false; - setPowerSave(0); - } - } + void showTime(); public: // gets called once at boot. Do all initialization that doesn't depend on // network here - void setup() { - if (type == NONE || !enabled) return; - - bool isHW, isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64); - PinOwner po = PinOwner::UM_FourLineDisplay; - if (isSPI) { - uint8_t hw_sclk = spi_sclk<0 ? HW_PIN_CLOCKSPI : spi_sclk; - uint8_t hw_mosi = spi_mosi<0 ? HW_PIN_DATASPI : spi_mosi; - if (ioPin[0] < 0 || ioPin[1] < 0) { - ioPin[0] = hw_sclk; - ioPin[1] = hw_mosi; - } - isHW = (ioPin[0]==hw_sclk && ioPin[1]==hw_mosi); - PinManagerPinType cspins[3] = { { ioPin[2], true }, { ioPin[3], true }, { ioPin[4], true } }; - if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type=NONE; return; } - if (isHW) po = PinOwner::HW_SPI; // allow multiple allocations of HW I2C bus pins - PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; - if (!pinManager.allocateMultiplePins(pins, 2, po)) { - pinManager.deallocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay); - type = NONE; - return; - } - } else { - uint8_t hw_scl = i2c_scl<0 ? HW_PIN_SCL : i2c_scl; - uint8_t hw_sda = i2c_sda<0 ? HW_PIN_SDA : i2c_sda; - if (ioPin[0] < 0 || ioPin[1] < 0) { - ioPin[0] = hw_scl; - ioPin[1] = hw_sda; - } - isHW = (ioPin[0]==hw_scl && ioPin[1]==hw_sda); - if (isHW) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins - PinManagerPinType pins[2] = { {ioPin[0], true }, { ioPin[1], true } }; - if (!pinManager.allocateMultiplePins(pins, 2, po)) { type=NONE; return; } - } - - DEBUG_PRINTLN(F("Allocating display.")); -/* -// At some point it may be good to not new/delete U8X8 object but use this instead -// (does not currently work) -//------------------------------------------------------------------------------- - switch (type) { - case SSD1306: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SH1106: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1305: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1305_64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_SPI: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); - break; - case SSD1306_SPI64: - u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); - break; - default: - type = NONE; - return; - } - if (isSPI) { - if (!isHW) u8x8_SetPin_4Wire_SW_SPI(u8x8.getU8x8(), ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8_SetPin_4Wire_HW_SPI(u8x8.getU8x8(), ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - } else { - if (!isHW) u8x8_SetPin_SW_I2C(u8x8.getU8x8(), ioPin[0], ioPin[1], U8X8_PIN_NONE); // SCL, SDA, reset - else u8x8_SetPin_HW_I2C(u8x8.getU8x8(), U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - } -*/ - switch (type) { - case SSD1306: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SH1106: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1306_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1305: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1305_64: - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset - else u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA - break; - case SSD1306_SPI: - // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - break; - case SSD1306_SPI64: - // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 - if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); - else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset - break; - default: - u8x8 = nullptr; - } - - if (nullptr == u8x8) { - DEBUG_PRINTLN(F("Display init failed.")); - pinManager.deallocateMultiplePins((const uint8_t*)ioPin, isSPI ? 5 : 2, po); - type = NONE; - return; - } - - lineHeight = u8x8->getRows() > 4 ? 2 : 1; - DEBUG_PRINTLN(F("Starting display.")); - u8x8->setBusClock(ioFrequency); // can be used for SPI too - u8x8->begin(); - setFlipMode(flip); - setVcomh(contrastFix); - setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 - setPowerSave(0); - //drawString(0, 0, "Loading..."); - overlayLogo(3500); - onUpdateBegin(false); // create Display task - initDone = true; - } + void setup(); // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here - void connected() { - knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : - knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); - networkOverlay(PSTR("NETWORK INFO"),7000); - } + void connected(); /** * Da loop. */ - void loop() { - #ifndef ARDUINO_ARCH_ESP32 - if (!enabled || strip.isUpdating()) return; - unsigned long now = millis(); - if (now < nextUpdate) return; - nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate); - redraw(false); - #endif - } + void loop(); //function to update lastredraw - void updateRedrawTime() { - lastRedraw = millis(); - } + inline void updateRedrawTime() {lastRedraw = millis(); } + + //function to to check if a redraw or overlay draw is active. Needed for UM Rotary, to avoid random/concurrent drawing + bool canDraw(void); /** * Redraw the screen (but only if things have changed * or if forceRedraw). */ - void redraw(bool forceRedraw) { - bool needRedraw = false; - unsigned long now = millis(); - - if (type == NONE || !enabled) return; - if (overlayUntil > 0) { - if (now >= overlayUntil) { - // Time to display the overlay has elapsed. - overlayUntil = 0; - forceRedraw = true; - } else { - // We are still displaying the overlay - // Don't redraw. - return; - } - } + void redraw(bool forceRedraw); - while (drawing && millis()-now < 25) delay(1); // wait if someone else is drawing - if (drawing || lockRedraw) return; + void redraw_core(bool forceRedraw); - if (apActive && WLED_WIFI_CONFIGURED && now<15000) { - knownSsid = apSSID; - networkOverlay(PSTR("NETWORK INFO"),30000); - return; - } - - // Check if values which are shown on display changed from the last time. - if (forceRedraw) { - needRedraw = true; - clear(); - } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon - powerON = !powerON; - drawStatusIcons(); - return; - } else if (knownnightlight != nightlightActive) { //trigger moon icon - knownnightlight = nightlightActive; - drawStatusIcons(); - if (knownnightlight) { - String timer = PSTR("Timer On"); - center(timer,LINE_BUFFER_SIZE-1); - overlay(timer.c_str(), 2500, 6); - } - return; - } else if (wificonnected != interfacesInited) { //trigger wifi icon - wificonnected = interfacesInited; - drawStatusIcons(); - return; - } else if (knownMode != effectCurrent || knownPalette != effectPalette) { - if (displayTurnedOff) needRedraw = true; - else { - if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } - if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } - lastRedraw = now; - return; - } - } else if (knownBrightness != bri) { - if (displayTurnedOff && nightlightActive) { knownBrightness = bri; } - else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; } - } else if (knownEffectSpeed != effectSpeed) { - if (displayTurnedOff) needRedraw = true; - else { updateSpeed(); lastRedraw = now; return; } - } else if (knownEffectIntensity != effectIntensity) { - if (displayTurnedOff) needRedraw = true; - else { updateIntensity(); lastRedraw = now; return; } - } - - if (!needRedraw) { - // Nothing to change. - // Turn off display after 1 minutes with no change. - if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { - // We will still check if there is a change in redraw() - // and turn it back on if it changed. - clear(); - sleepOrClock(true); - } else if (displayTurnedOff && ntpEnabled) { - showTime(); - } - return; - } + void updateBrightness(); - lastRedraw = now; - - // Turn the display back on - wakeDisplay(); - - // Update last known values. - knownBrightness = bri; - knownMode = effectCurrent; - knownPalette = effectPalette; - knownEffectSpeed = effectSpeed; - knownEffectIntensity = effectIntensity; - knownnightlight = nightlightActive; - wificonnected = interfacesInited; - - // Do the actual drawing - // First row: Icons - draw2x2GlyphIcons(); - drawArrow(); - drawStatusIcons(); - - // Second row - updateBrightness(); - updateSpeed(); - updateIntensity(); - - // Third row - showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info - - // Fourth row - showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info - } - - void updateBrightness() { - knownBrightness = bri; - if (overlayUntil == 0) { - lockRedraw = true; - brightness100 = ((uint16_t)bri*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), brightness100); - drawString(1, lineHeight, lineBuffer); - lockRedraw = false; - } - } - - void updateSpeed() { - knownEffectSpeed = effectSpeed; - if (overlayUntil == 0) { - lockRedraw = true; - fxspeed100 = ((uint16_t)effectSpeed*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), fxspeed100); - drawString(5, lineHeight, lineBuffer); - lockRedraw = false; - } - } + void updateSpeed(); - void updateIntensity() { - knownEffectIntensity = effectIntensity; - if (overlayUntil == 0) { - lockRedraw = true; - fxintensity100 = ((uint16_t)effectIntensity*100)/255; - char lineBuffer[4]; - sprintf_P(lineBuffer, PSTR("%-3d"), fxintensity100); - drawString(9, lineHeight, lineBuffer); - lockRedraw = false; - } - } + void updateIntensity(); - void drawStatusIcons() { - uint8_t col = 15; - uint8_t row = 0; - lockRedraw = true; - drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon - if (lineHeight==2) { col--; } else { row++; } - drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon - if (lineHeight==2) { col--; } else { col = row = 0; } - drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode - lockRedraw = false; - } + void drawStatusIcons(); /** * marks the position of the arrow showing * the current setting being changed * pass line and colum info */ - void setMarkLine(byte newMarkLineNum, byte newMarkColNum) { - markLineNum = newMarkLineNum; - markColNum = newMarkColNum; - } + void setMarkLine(byte newMarkLineNum, byte newMarkColNum); - //Draw the arrow for the current setting beiong changed - void drawArrow() { - lockRedraw = true; - if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1); - lockRedraw = false; - } + //Draw the arrow for the current setting being changed + void drawArrow(); //Display the current effect or palette (desiredEntry) // on the appropriate line (row). - void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { - char lineBuffer[MAX_JSON_CHARS]; - if (overlayUntil == 0) { - lockRedraw = true; - // Find the mode name in JSON - uint8_t printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1); - if (lineBuffer[0]=='*' && lineBuffer[1]==' ') { - // remove "* " from dynamic palettes - for (byte i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\0' - printedChars -= 2; - } else if ((lineBuffer[0]==' ' && lineBuffer[1]>127)) { - // remove note symbol from effect names - for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' - printedChars -= 5; - } - if (lineHeight == 2) { // use this code for 8 line display - char smallBuffer1[MAX_MODE_LINE_SPACE]; - char smallBuffer2[MAX_MODE_LINE_SPACE]; - uint8_t smallChars1 = 0; - uint8_t smallChars2 = 0; - if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits - while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; - lineBuffer[printedChars] = 0; - drawString(1, row*lineHeight, lineBuffer); - } else { // for long names divide the text into 2 lines and print them small - bool spaceHit = false; - for (uint8_t i = 0; i < printedChars; i++) { - switch (lineBuffer[i]) { - case ' ': - if (i > 4 && !spaceHit) { - spaceHit = true; - break; - } - if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; - else smallBuffer1[smallChars1++] = lineBuffer[i]; - break; - default: - if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; - else smallBuffer1[smallChars1++] = lineBuffer[i]; - break; - } - } - while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; - smallBuffer1[smallChars1] = 0; - drawString(1, row*lineHeight, smallBuffer1, true); - while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; - smallBuffer2[smallChars2] = 0; - drawString(1, row*lineHeight+1, smallBuffer2, true); - } - } else { // use this code for 4 ling displays - char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette - uint8_t smallChars3 = 0; - for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; - smallBuffer3[smallChars3] = 0; - drawString(1, row*lineHeight, smallBuffer3, true); - } - lockRedraw = false; - } - } + void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); /** * If there screen is off or in clock is displayed, @@ -740,286 +295,44 @@ class FourLineDisplayUsermod : public Usermod { * the first input from the rotary encoder but * to wake up the screen. */ - bool wakeDisplay() { - if (type == NONE || !enabled) return false; - if (displayTurnedOff) { - unsigned long now = millis(); - while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing - if (drawing) return false; - lockRedraw = true; - clear(); - // Turn the display back on - sleepOrClock(false); - lockRedraw = false; - return true; - } - return false; - } + bool wakeDisplay(); /** * Allows you to show one line and a glyph as overlay for a period of time. * Clears the screen and prints. * Used in Rotary Encoder usermod. */ - void overlay(const char* line1, long showHowLong, byte glyphType) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (glyphType>0 && glyphType<255) { - if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font - else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true); - } - if (line1) { - String buf = line1; - center(buf, getCols()); - drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str()); - } - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } + void overlay(const char* line1, long showHowLong, byte glyphType); /** * Allows you to show Akemi WLED logo overlay for a period of time. * Clears the screen and prints. */ - void overlayLogo(long showHowLong) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (lineHeight == 2) { - //add a bit of randomness - switch (millis()%3) { - case 0: - //WLED - draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2); - draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2); - draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2); - draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2); - break; - case 1: - //WLED Akemi - drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true); - drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true); - drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true); - break; - case 2: - //Akemi - //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font - drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true); - drawString(6, 6, "WLED"); - break; - } - } else { - switch (millis()%3) { - case 0: - //WLED - draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2); - draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2); - draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2); - draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2); - break; - case 1: - //WLED Akemi - drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4); - drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4); - drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4); - break; - case 2: - //Akemi - //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash - draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2); - break; - } - } - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } + void overlayLogo(long showHowLong); /** * Allows you to show two lines as overlay for a period of time. * Clears the screen and prints. * Used in Auto Save usermod */ - void overlay(const char* line1, const char* line2, long showHowLong) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (line1) { - String buf = line1; - center(buf, getCols()); - drawString(0, 1*lineHeight, buf.c_str()); - } - if (line2) { - String buf = line2; - center(buf, getCols()); - drawString(0, 2*lineHeight, buf.c_str()); - } - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } + void overlay(const char* line1, const char* line2, long showHowLong); - void networkOverlay(const char* line1, long showHowLong) { - unsigned long now = millis(); - while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing - if (drawing) return; - lockRedraw = true; - - String line; - // Turn the display back on - if (!wakeDisplay()) clear(); - // Print the overlay - if (line1) { - line = line1; - center(line, getCols()); - drawString(0, 0, line.c_str()); - } - // Second row with Wifi name - line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); - if (line.length() < getCols()) center(line, getCols()); - drawString(0, lineHeight, line.c_str()); - // Print `~` char to indicate that SSID is longer, than our display - if (knownSsid.length() > getCols()) { - drawString(getCols() - 1, 0, "~"); - } - // Third row with IP and Password in AP Mode - line = knownIp.toString(); - center(line, getCols()); - drawString(0, lineHeight*2, line.c_str()); - line = ""; - if (apActive) { - line = apPass; - } else if (strcmp(serverDescription, "WLED") != 0) { - line = serverDescription; - } - center(line, getCols()); - drawString(0, lineHeight*3, line.c_str()); - overlayUntil = millis() + showHowLong; - lockRedraw = false; - } + void networkOverlay(const char* line1, long showHowLong); + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool sleepEnable); + /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ - bool handleButton(uint8_t b) { - yield(); - if (!enabled - || b // butto 0 only - || buttonType[b] == BTN_TYPE_SWITCH - || buttonType[b] == BTN_TYPE_NONE - || buttonType[b] == BTN_TYPE_RESERVED - || buttonType[b] == BTN_TYPE_PIR_SENSOR - || buttonType[b] == BTN_TYPE_ANALOG - || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { - return false; - } - - unsigned long now = millis(); - static bool buttonPressedBefore = false; - static bool buttonLongPressed = false; - static unsigned long buttonPressedTime = 0; - static unsigned long buttonWaitTime = 0; - bool handled = true; - - //momentary button logic - if (isButtonPressed(b)) { //pressed - - if (!buttonPressedBefore) buttonPressedTime = now; - buttonPressedBefore = true; - - if (now - buttonPressedTime > 600) { //long press - buttonLongPressed = true; - //TODO: handleButton() handles button 0 without preset in a different way for double click - //so we need to override with same behaviour - longPressAction(0); - //handled = false; - } - - } else if (!isButtonPressed(b) && buttonPressedBefore) { //released - - long dur = now - buttonPressedTime; - if (dur < 50) { - buttonPressedBefore = false; - return true; - } //too short "press", debounce - - bool doublePress = buttonWaitTime; //did we have short press before? - buttonWaitTime = 0; - - if (!buttonLongPressed) { //short press - // if this is second release within 350ms it is a double press (buttonWaitTime!=0) - //TODO: handleButton() handles button 0 without preset in a different way for double click - if (doublePress) { - networkOverlay(PSTR("NETWORK INFO"),7000); - handled = true; - } else { - buttonWaitTime = now; - } - } - buttonPressedBefore = false; - buttonLongPressed = false; - } - // if 350ms elapsed since last press/release it is a short press - if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) { - buttonWaitTime = 0; - //TODO: handleButton() handles button 0 without preset in a different way for double click - //so we need to override with same behaviour - shortPressAction(0); - //handled = false; - } - return handled; - } + bool handleButton(uint8_t b); - #if CONFIG_FREERTOS_UNICORE - #define ARDUINO_RUNNING_CORE 0 - #else - #define ARDUINO_RUNNING_CORE 1 - #endif - void onUpdateBegin(bool init) { - #ifdef ARDUINO_ARCH_ESP32 - if (init && Display_Task) { - vTaskSuspend(Display_Task); // update is about to begin, disable task to prevent crash - } else { - // update has failed or create task requested - if (Display_Task) - vTaskResume(Display_Task); - else - xTaskCreatePinnedToCore( - [](void * par) { // Function to implement the task - // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; - TickType_t xLastWakeTime = xTaskGetTickCount(); - for(;;) { - delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. - // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. - vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, by doing nothing for REFRESH_RATE_MS millis - FourLineDisplayUsermod::getInstance()->redraw(false); - } - }, - "4LD", // Name of the task - 3072, // Stack size in words - NULL, // Task input parameter - 1, // Priority of the task (not idle) - &Display_Task, // Task handle - ARDUINO_RUNNING_CORE - ); - } - #endif - } + void onUpdateBegin(bool init); /* * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. @@ -1048,22 +361,7 @@ class FourLineDisplayUsermod : public Usermod { // if (!initDone) return; // prevent crash on boot applyPreset() //} - void appendConfigData() { - oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'SSD1306',1);")); - oappend(SET_F("addOption(dd,'SH1106',2);")); - oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); - oappend(SET_F("addOption(dd,'SSD1305',4);")); - oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'-1 use global','I2C/SPI CLK');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'-1 use global','I2C/SPI DTA');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI CS');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',3,'','SPI DC');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',4,'','SPI RST');")); - } + void appendConfigData(); /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -1079,40 +377,7 @@ class FourLineDisplayUsermod : public Usermod { * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ - void addToConfig(JsonObject& root) { - // determine if we are using global HW pins (data & clock) - int8_t hw_dta, hw_clk; - if ((type == SSD1306_SPI || type == SSD1306_SPI64)) { - hw_clk = spi_sclk<0 ? HW_PIN_CLOCKSPI : spi_sclk; - hw_dta = spi_mosi<0 ? HW_PIN_DATASPI : spi_mosi; - } else { - hw_clk = i2c_scl<0 ? HW_PIN_SCL : i2c_scl; - hw_dta = i2c_sda<0 ? HW_PIN_SDA : i2c_sda; - } - - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - - JsonArray io_pin = top.createNestedArray("pin"); - for (int i=0; i<5; i++) { - if (i==0 && ioPin[i]==hw_clk) io_pin.add(-1); // do not store global HW pin - else if (i==1 && ioPin[i]==hw_dta) io_pin.add(-1); // do not store global HW pin - else io_pin.add(ioPin[i]); - } - top["type"] = type; - top[FPSTR(_flip)] = (bool) flip; - top[FPSTR(_contrast)] = contrast; - top[FPSTR(_contrastFix)] = (bool) contrastFix; - #ifndef ARDUINO_ARCH_ESP32 - top[FPSTR(_refreshRate)] = refreshRate; - #endif - top[FPSTR(_screenTimeOut)] = screenTimeout/1000; - top[FPSTR(_sleepMode)] = (bool) sleepMode; - top[FPSTR(_clockMode)] = (bool) clockMode; - top[FPSTR(_showSeconds)] = (bool) showSeconds; - top[FPSTR(_busClkFrequency)] = ioFrequency/1000; - DEBUG_PRINTLN(F("4 Line Display config saved.")); - } + void addToConfig(JsonObject& root); /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). @@ -1122,80 +387,7 @@ class FourLineDisplayUsermod : public Usermod { * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) */ - bool readFromConfig(JsonObject& root) { - bool needsRedraw = false; - DisplayType newType = type; - int8_t oldPin[5]; for (byte i=0; i<5; i++) oldPin[i] = ioPin[i]; - - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - enabled = top[FPSTR(_enabled)] | enabled; - newType = top["type"] | newType; - for (byte i=0; i<5; i++) ioPin[i] = top["pin"][i] | ioPin[i]; - flip = top[FPSTR(_flip)] | flip; - contrast = top[FPSTR(_contrast)] | contrast; - #ifndef ARDUINO_ARCH_ESP32 - refreshRate = top[FPSTR(_refreshRate)] | refreshRate; - refreshRate = min(5000, max(250, (int)refreshRate)); - #endif - screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; - sleepMode = top[FPSTR(_sleepMode)] | sleepMode; - clockMode = top[FPSTR(_clockMode)] | clockMode; - showSeconds = top[FPSTR(_showSeconds)] | showSeconds; - contrastFix = top[FPSTR(_contrastFix)] | contrastFix; - if (newType == SSD1306_SPI || newType == SSD1306_SPI64) - ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - else - ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency - - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json - type = newType; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - bool pinsChanged = false; - for (byte i=0; i<5; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } - if (pinsChanged || type!=newType) { - if (type != NONE) delete u8x8; - PinOwner po = PinOwner::UM_FourLineDisplay; - bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64); - if (isSPI) { - pinManager.deallocateMultiplePins((const uint8_t *)(&oldPin[2]), 3, po); - uint8_t hw_sclk = spi_sclk<0 ? HW_PIN_CLOCKSPI : spi_sclk; - uint8_t hw_mosi = spi_mosi<0 ? HW_PIN_DATASPI : spi_mosi; - bool isHW = (oldPin[0]==hw_sclk && oldPin[1]==hw_mosi); - if (isHW) po = PinOwner::HW_SPI; - } else { - uint8_t hw_scl = i2c_scl<0 ? HW_PIN_SCL : i2c_scl; - uint8_t hw_sda = i2c_sda<0 ? HW_PIN_SDA : i2c_sda; - bool isHW = (oldPin[0]==hw_scl && oldPin[1]==hw_sda); - if (isHW) po = PinOwner::HW_I2C; - } - pinManager.deallocateMultiplePins((const uint8_t *)oldPin, 2, po); - type = newType; - setup(); - needsRedraw |= true; - } else { - u8x8->setBusClock(ioFrequency); // can be used for SPI too - setVcomh(contrastFix); - setContrast(contrast); - setFlipMode(flip); - } - knownHour = 99; - if (needsRedraw && !wakeDisplay()) redraw(true); - else overlayLogo(3500); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_contrastFix)].isNull(); - } + bool readFromConfig(JsonObject& root); /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). @@ -1219,4 +411,1275 @@ const char FourLineDisplayUsermod::_showSeconds[] PROGMEM = "showSeconds"; const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; const char FourLineDisplayUsermod::_contrastFix[] PROGMEM = "contrastFix"; +#ifdef ARDUINO_ARCH_ESP32 FourLineDisplayUsermod *FourLineDisplayUsermod::instance = nullptr; +#endif + +//WLEDMM +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + // semaphores - needed on ESP32 only, as we use a separate task to update the display + SemaphoreHandle_t drawMux = xSemaphoreCreateBinary(); // for drawstring and drawglyph functions (to prevent concurrent access to HW) + SemaphoreHandle_t drawMuxBig = xSemaphoreCreateBinary(); // for draw2x2GlyphIcons() and showCurrentEffectOrPalette() - more complex and not thread-safe + const TickType_t maxWait = 300 * portTICK_PERIOD_MS; // wait max. 300ms (drawstring semaphore) + const TickType_t maxWaitLong = 800 * portTICK_PERIOD_MS; // wait max. 800ms (big drawing semaphore) + #define FLD_SemaphoreTake(x,t) xSemaphoreTake((x),(t)) + #define FLD_SemaphoreGive(x) xSemaphoreGive(x) +#else + // 8266 or no tasks - no semaphores + #define FLD_SemaphoreTake(x,t) pdTRUE + #define FLD_SemaphoreGive(x) + #if !defined(ARDUINO_ARCH_ESP32) && !defined(pdTRUE) + #define pdTRUE true + #endif +#endif + + + +// some displays need this to properly apply contrast +void FourLineDisplayUsermod::setVcomh(bool highContrast) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (u8x8 == nullptr) return; + + u8x8_t *u8x8_struct = u8x8->getU8x8(); + u8x8_cad_StartTransfer(u8x8_struct); + u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value + u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64 + u8x8_cad_EndTransfer(u8x8_struct); +} + +/** + * Wrappers for screen drawing + */ +void FourLineDisplayUsermod::setFlipMode(uint8_t mode) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (u8x8 == nullptr) return; + u8x8->setFlipMode(mode); +} +void FourLineDisplayUsermod::setContrast(uint8_t contrast) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (u8x8 == nullptr) return; + u8x8->setContrast(contrast); +} +void FourLineDisplayUsermod::drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (u8x8 == nullptr) return; + if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex + +#if defined(ARDUINO_ARCH_ESP32) && !defined(OLD_4LD_FONTS) // WLEDMM use nicer 2x2 font on ESP32 + if (!ignoreLH && lineHeight>1) { + if(strlen(string) > 3) // WLEDMM little hack - less than 3 chars -> show in bold + u8x8->setFont(u8x8_font_7x14_1x2_r); // normal + else + u8x8->setFont(u8x8_font_8x13B_1x2_r); // bold + u8x8->drawString(col, row, string); + } + else { + u8x8->setFont(u8x8_font_chroma48medium8_r); + u8x8->drawString(col, row, string); + } +#else + u8x8->setFont(u8x8_font_chroma48medium8_r); + if (!ignoreLH && lineHeight>1) u8x8->draw1x2String(col, row, string); + else u8x8->drawString(col, row, string); +#endif + FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex +} +void FourLineDisplayUsermod::draw2x2String(uint8_t col, uint8_t row, const char *string) { + if (!typeOK || !enabled) return; + if (u8x8 == nullptr) return; + if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex +#if defined(ARDUINO_ARCH_ESP32) && !defined(OLD_4LD_FONTS) && !defined(WLEDMM_SAVE_FLASH) // WLEDMM use nicer 2x2 font on ESP32 + if (lineHeight>1) { // WLEDMM use 2x3 on 128x64 displays + //u8x8->setFont(u8x8_font_profont29_2x3_r); // sans serif 2x3 + u8x8->setFont(u8x8_font_courB18_2x3_r); // courier bold 2x3 + u8x8->drawString(col, row + (row >3? 1:0), string); + } else { + //u8x8->setFont(u8x8_font_lucasarts_scumm_subtitle_o_2x2_r); + //u8x8->setFont(u8x8_font_lucasarts_scumm_subtitle_r_2x2_r); + u8x8->setFont(u8x8_font_px437wyse700b_2x2_r); + u8x8->drawString(col, row, string); + } +#else + u8x8->setFont(u8x8_font_chroma48medium8_r); + if (lineHeight>1) { // WLEDMM use 2x3 on 128x64 displays + u8x8->draw2x2String(col, row + (row >3? 1:0), string); + } else { + u8x8->draw2x2String(col, row, string); + } +#endif + FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex +} +void FourLineDisplayUsermod::drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH) { + if (!typeOK || !enabled) return; + if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex + u8x8->setFont(font); + if (!ignoreLH && lineHeight>1) u8x8->draw1x2Glyph(col, row, glyph); + else u8x8->drawGlyph(col, row, glyph); + FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex +} +void FourLineDisplayUsermod::draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) { + if (!typeOK || !enabled) return; + if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex + u8x8->setFont(font); + u8x8->draw2x2Glyph(col, row, glyph); + FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex +} +uint8_t FourLineDisplayUsermod::getCols() { + if (!typeOK || !enabled) return 0; + return u8x8->getCols(); +} +void FourLineDisplayUsermod::clear() { + if (!typeOK || !enabled) return; + if (nullptr == u8x8) return; // prevents some crashes + if (FLD_SemaphoreTake(drawMux, maxWaitLong ) != pdTRUE) return; // WLEDMM acquire draw mutex - clear() can take very long in software I2C mode + u8x8->clear(); // crashes randomly on ESP32 + FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex +} +void FourLineDisplayUsermod::setPowerSave(uint8_t save) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (u8x8 == nullptr) return; + u8x8->setPowerSave(save); +} + +void FourLineDisplayUsermod::center(String &line, uint8_t width) { + int len = line.length(); + if (len0; i--) line = ' ' + line; + for (byte i=line.length(); i 1) { + drawGlyph( 1, 0, 1, u8x8_4LineDisplay_WLED_icons_2x2, true); //brightness icon + drawGlyph( 5, 0, 2, u8x8_4LineDisplay_WLED_icons_2x2, true); //speed icon + drawGlyph( 9, 0, 3, u8x8_4LineDisplay_WLED_icons_2x2, true); //intensity icon + drawGlyph(14, 2*lineHeight, 4, u8x8_4LineDisplay_WLED_icons_2x2, true); //palette icon + drawGlyph(14, 3*lineHeight, 5, u8x8_4LineDisplay_WLED_icons_2x2, true); //effect icon + } else { + drawGlyph( 1, 0, 1, u8x8_4LineDisplay_WLED_icons_2x1); //brightness icon + drawGlyph( 5, 0, 2, u8x8_4LineDisplay_WLED_icons_2x1); //speed icon + drawGlyph( 9, 0, 3, u8x8_4LineDisplay_WLED_icons_2x1); //intensity icon + drawGlyph(15, 2, 4, u8x8_4LineDisplay_WLED_icons_1x1); //palette icon + drawGlyph(15, 3, 5, u8x8_4LineDisplay_WLED_icons_1x1); //effect icon + } + FLD_SemaphoreGive(drawMuxBig); // WLEDMM release BIG draw mutex +} + +/** + * Display the current date and time in large characters + * on the middle rows. Based 24 or 12 hour depending on + * the useAMPM configuration. + */ +void FourLineDisplayUsermod::showTime() { + if (!typeOK || !enabled || !displayTurnedOff) return; + + unsigned long now = millis(); + while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing + drawing = true; + + char lineBuffer[LINE_BUFFER_SIZE] = { '\0' }; + static byte lastSecond; + byte secondCurrent = second(localTime); + byte minuteCurrent = minute(localTime); + byte hourCurrent = hour(localTime); + + if (knownMinute != minuteCurrent) { //only redraw clock if it has changed + //updateLocalTime(); + byte AmPmHour = hourCurrent; + boolean isitAM = true; + if (useAMPM) { + if (AmPmHour > 11) { AmPmHour -= 12; isitAM = false; } + if (AmPmHour == 0) { AmPmHour = 12; } + } + if (knownHour != hourCurrent) { + // only update date when hour changes + snprintf_P(lineBuffer, LINE_BUFFER_SIZE, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); + draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day + } + snprintf_P(lineBuffer,LINE_BUFFER_SIZE, PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); + draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds + if (useAMPM) drawString(12, lineHeight*2 + (lineHeight-1), (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time + + drawStatusIcons(); //icons power, wifi, timer, etc + + if (lineHeight > 1) { // WLEDMM use extra space for useful information + #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) + snprintf_P(lineBuffer, LINE_BUFFER_SIZE, PSTR(" %-3.3s %-2.2s "), driverSPI? "SPI" : "I2C", driverHW? "hw" : "sw"); // WLEDMM driver info + #else + strncpy_P(lineBuffer, PSTR(" "), LINE_BUFFER_SIZE); + #endif + if (apActive) strncpy_P(lineBuffer, PSTR(" AP mode "), LINE_BUFFER_SIZE); + else if (!WLED_CONNECTED) strncpy_P(lineBuffer, PSTR(" NO NET "), LINE_BUFFER_SIZE); + if (WLED_MQTT_CONNECTED) lineBuffer[9] = 'M'; // "MQTT" + if (realtimeMode && !realtimeOverride) lineBuffer[10] = 'X'; // "eXternal control" + //if (transitionActive) lineBuffer[11] = 'T'; + //if (stateChanged) lineBuffer[12] = 'C'; + drawString(1, 0, lineBuffer, false); + } + + knownMinute = minuteCurrent; + knownHour = hourCurrent; + } + if (showSeconds && secondCurrent != lastSecond) { + lastSecond = secondCurrent; + draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":"); + snprintf_P(lineBuffer, LINE_BUFFER_SIZE, PSTR("%02d"), secondCurrent); + if (useAMPM) + drawString(12, lineHeight*2+1 + (lineHeight-1), lineBuffer, true); // even with double sized rows print seconds in 1 line // WLEDMM move it a bit lower + else + drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line + } + drawing = false; +} + +// public: + +// gets called once at boot. Do all initialization that doesn't depend on +// network here +void FourLineDisplayUsermod::setup() { + if (!enabled) return; // typeOK = true will be set after successful setup + + bool isHW = false; + bool isSPI = displayIsSPI(type); + PinOwner po = PinOwner::UM_FourLineDisplay; + if (isSPI) { + if (ioPin[0] < 0 || ioPin[1] < 0) { + ioPin[0] = spi_sclk; + ioPin[1] = spi_mosi; + } else { + if ((spi_sclk < 0) && (spi_mosi < 0)) { // WLEDMM UM pins are valid, but global = -1 --> copy pins to "global" + spi_sclk = ioPin[0]; + spi_mosi = ioPin[1]; + } + } + if ((ioPin[0] < 0 || ioPin[1] < 0) && (spi_sclk < 0 || spi_mosi < 0)) { // invalid pins, or "use global" and global pins not defined + typeOK=false; strcpy(errorMessage, PSTR("SPI No Pins defined")); return; } //WLEDMM bugfix - ensure that "final" GPIO are valid + + isHW = (ioPin[0]==spi_sclk && ioPin[1]==spi_mosi); + if ((ioPin[0] == -1) || (ioPin[1] == -1)) isHW = true; // WLEDMM "use global" = hardware + if ((spi_sclk <0) || (spi_mosi < 0)) isHW = false; // no global pins - use software emulation + PinManagerPinType cspins[3] = { { ioPin[2], true }, { ioPin[3], true }, { ioPin[4], true } }; + if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { typeOK=false; strcpy(errorMessage, PSTR("SPI3 alloc pins failed")); return; } + if (isHW) po = PinOwner::HW_SPI; // allow multiple allocations of HW I2C bus pins + PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } }; + if (!pinManager.allocateMultiplePins(pins, 2, po)) { + pinManager.deallocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay); + typeOK=false; + strcpy(errorMessage, PSTR("SPI2 alloc pins failed")); + return; + } + // start SPI now! +#ifdef ARDUINO_ARCH_ESP32 + if (isHW) SPI.begin(spi_sclk, spi_miso, spi_mosi); // ESP32 - will silently fail if SPI already active. +#else + if (isHW) SPI.begin(); // ESP8266 - SPI pins are fixed +#endif + + } else { + //if (ioPin[0] < 0 || ioPin[1] < 0) { //WLEDMM do _not_ copy global pins !! + // ioPin[0] = i2c_scl; + // ioPin[1] = i2c_sda; + //} + isHW = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); + if ((ioPin[0] == -1) || (ioPin[1] == -1)) isHW = true; // WLEDMM "use global" = hardware + // isHW = true; + if (isHW) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins + PinManagerPinType pins[2] = { {ioPin[0], true }, { ioPin[1], true } }; + + if ((ioPin[0] < 0 || ioPin[1] < 0) && (i2c_scl < 0 || i2c_sda < 0)) { // invalid pins, or "use global" and global pins not defined + typeOK=false; strcpy(errorMessage, PSTR("I2C No Pins defined")); return; } //WLEDMM bugfix - ensure that "final" GPIO are valid + + if (isHW) { + if (!pinManager.joinWire(i2c_sda, i2c_scl)) { typeOK=false; strcpy(errorMessage, PSTR("I2C HW init failed")); return; } // WLEDMM join the HW bus + } else { + if (!pinManager.allocateMultiplePins(pins, 2, po)) { typeOK=false; strcpy(errorMessage, PSTR("I2C Alloc pins failed")); return; } // WLEDMM use software bus + } + } + + driverHW = isHW; + driverSPI= isSPI; + DEBUG_PRINTLN(F("Allocating display.")); +/* +// At some point it may be good to not new/delete U8X8 object but use this instead +// (does not currently work) +//------------------------------------------------------------------------------- + switch (type) { + case SSD1306: + u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); + break; + case SH1106: + u8x8_Setup(u8x8.getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); + break; + case SSD1306_64: + u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); + break; + case SSD1305: + u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + break; + case SSD1305_64: + u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_i2c, u8x8_byte_arduino_sw_i2c, u8x8_gpio_and_delay_arduino); + break; + case SSD1306_SPI: + u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); + break; + case SSD1306_SPI64: + u8x8_Setup(u8x8.getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_4wire_sw_spi, u8x8_gpio_and_delay_arduino); + break; + default: + typeOK=false; + strcpy(errorMessage, PSTR("No valid type")); + return; + } + if (isSPI) { + if (!isHW) u8x8_SetPin_4Wire_SW_SPI(u8x8.getU8x8(), ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); + else u8x8_SetPin_4Wire_HW_SPI(u8x8.getU8x8(), ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset + } else { + if (!isHW) u8x8_SetPin_SW_I2C(u8x8.getU8x8(), ioPin[0], ioPin[1], U8X8_PIN_NONE); // SCL, SDA, reset + else u8x8_SetPin_HW_I2C(u8x8.getU8x8(), U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + } +*/ + switch (type) { + case SSD1306: + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + break; + case SH1106: + if (!isHW) u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + break; + case SSD1306_64: + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + break; + case SSD1309_64: + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME2_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME2_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + break; + case SSD1305: + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + break; + case SSD1305_64: + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(ioPin[0], ioPin[1]); // SCL, SDA, reset + else u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, ioPin[0], ioPin[1]); // Pins are Reset, SCL, SDA + break; + case SSD1306_SPI: + // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); + else u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset + break; + case SSD1306_SPI64: + // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); + else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset + break; + case SSD1309_SPI64: + // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); + else u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset + break; + case SSD1327_SPI128: + // u8x8 uses global SPI variable that is attached to VSPI bus on ESP32 + if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1327_WS_128X128_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]); + else u8x8 = (U8X8 *) new U8X8_SSD1327_WS_128X128_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset + break; + default: + u8x8 = nullptr; + } + + if (nullptr == u8x8) { + USER_PRINTLN(F("Display init failed.")); + if (!isHW || !isSPI) pinManager.deallocateMultiplePins((const uint8_t*)ioPin, isSPI ? 5 : 2, po); // WLEDMM do not de-alloc global pins + typeOK=false; + strcpy(errorMessage, PSTR("Display init failed")); + return; + } + + lineHeight = u8x8->getRows() > 4 ? 2 : 1; + if (u8x8->getRows() > 8) lineHeight =3; + //if (u8x8->getRows() > 12) lineHeight =4; + if (isSPI) { + USER_PRINTLN(isHW ? F("Starting display (SPI HW).") : F("Starting display (SPI Soft).")); + } else { + USER_PRINTLN(isHW ? F("Starting display (I2C HW).") : F("Starting display (I2C Soft).")); + } + u8x8->setBusClock(ioFrequency); // can be used for SPI too + u8x8->begin(); + typeOK = true; + + reDrawing = false; + drawing = true; + setFlipMode(flip); + setVcomh(contrastFix); + setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 + setPowerSave(0); + drawing = false; + + // init semaphores to allow drawing + FLD_SemaphoreGive(drawMux); + FLD_SemaphoreGive(drawMuxBig); + + onUpdateBegin(false); // create Display task // WLEDMM bugfix: before drawing anything + delay(200); + + //drawString(0, 0, "Loading..."); + overlayLogo(3500); + initDone = true; +} + +// gets called every time WiFi is (re-)connected. Initialize own network +// interfaces here +void FourLineDisplayUsermod::connected() { + knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : + knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); + networkOverlay(PSTR("NETWORK INFO"),7000); +} + +/** + * Da loop. + */ +void FourLineDisplayUsermod::loop() { +#if !defined(ARDUINO_ARCH_ESP32) || !defined(FLD_ESP32_USE_THREADS) + static unsigned long lastRunTime = 0; + unsigned long now = millis(); + if (!enabled || !typeOK || (strip.isUpdating() && (now - lastRunTime < 50))) return; + lastRunTime = now; + + if (now < nextUpdate) return; + nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate); + reDrawing = true; + redraw(false); + reDrawing = false; +#endif +} + +//function to to check if a redraw or overlay draw is active. Needed for UM Rotary, to avoid random/concurrent drawing +bool FourLineDisplayUsermod::canDraw(void) { + if (!typeOK || !enabled || !initDone) return(false); // WLEDMM make sure the display is initialized before we try to draw on it +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) // only necessary on ESP32 + if (drawing) return(false); // overlay draws someting + if (reDrawing) return(false); // redraw task draws something +#endif + return(true); +} + +/** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ +void FourLineDisplayUsermod::redraw(bool forceRedraw) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + // use a wrapper onESP32, to ensure the functions is not running several times in parallel ! + static bool doForceRedraw = false; // for delaying "force redraw" + + if ((overlayUntil > 0) && (millis() >= overlayUntil)) { + forceRedraw = true; // Time to display the overlay has elapsed, force redraw needed + } + + if (forceRedraw) doForceRedraw = true; + if (reDrawing) return; // redraw already active + if (drawing) return; // overlay draw active + + reDrawing = true; // set redraw lock + if (doForceRedraw) forceRedraw = true; + redraw_core(forceRedraw); + if (overlayUntil == 0) doForceRedraw = false; // redraw was skipped if overlay is still visible + reDrawing = false; // reset activity flag, as redraw has too many early returns that don't take care of this +} + +void FourLineDisplayUsermod::redraw_core(bool forceRedraw) { +#endif + bool needRedraw = false; + unsigned long now = millis(); + + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (nullptr == u8x8) return; // prevent crash in case u8x8 is re-initialized (du to user changing setings) + + if (overlayUntil > 0) { + if (now >= overlayUntil) { + // Time to display the overlay has elapsed. + overlayUntil = 0; + forceRedraw = true; + } else { + // We are still displaying the overlay + // Don't redraw. + return; + } + } + + while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing + + if (apActive && WLED_WIFI_CONFIGURED && now<15000) { + knownSsid = apSSID; + networkOverlay(PSTR("NETWORK INFO"),30000); + return; + } + + // Check if values which are shown on display changed from the last time. + if (forceRedraw) { + needRedraw = true; + clear(); + } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon + powerON = !powerON; + drawStatusIcons(); + return; + } else if (knownnightlight != nightlightActive) { //trigger moon icon + knownnightlight = nightlightActive; + drawStatusIcons(); + if (knownnightlight) { + String timer = PSTR("Timer On"); + center(timer,LINE_BUFFER_SIZE-1); + overlay(timer.c_str(), 2500, 6); + } + return; + } else if (wificonnected != interfacesInited) { //trigger wifi icon + wificonnected = interfacesInited; + drawStatusIcons(); + return; + } else if (knownMode != effectCurrent || knownPalette != effectPalette) { + if (displayTurnedOff) needRedraw = true; + else { + if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } + if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } + lastRedraw = now; + return; + } + } else if (knownBrightness != bri) { + if (displayTurnedOff && nightlightActive) { knownBrightness = bri; } + else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; } + } else if (knownEffectSpeed != effectSpeed) { + if (displayTurnedOff) needRedraw = true; + else { updateSpeed(); lastRedraw = now; return; } + } else if (knownEffectIntensity != effectIntensity) { + if (displayTurnedOff) needRedraw = true; + else { updateIntensity(); lastRedraw = now; return; } + } + + if (!needRedraw) { + // Nothing to change. + // Turn off display after 1 minutes with no change. + if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { + // We will still check if there is a change in redraw() + // and turn it back on if it changed. + clear(); + sleepOrClock(true); + } else if (displayTurnedOff) { // WLEDMM removed "&& ntpEnabled" + showTime(); + } + return; + } + + lastRedraw = now; + + // Turn the display back on + wakeDisplay(); + + // Update last known values. + knownBrightness = bri; + knownMode = effectCurrent; + knownPalette = effectPalette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownnightlight = nightlightActive; + wificonnected = interfacesInited; + + while (drawing && millis()-now < 150) delay(8); // wait if someone else is drawing + + // Do the actual drawing + // First row: Icons + draw2x2GlyphIcons(); + drawArrow(); + drawStatusIcons(); + + // Second row + updateBrightness(); + updateSpeed(); + updateIntensity(); + + // Third row + showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info + + // Fourth row + showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info +} + +void FourLineDisplayUsermod::updateBrightness() { + knownBrightness = bri; + if (overlayUntil == 0) { + brightness100 = ((uint16_t)bri*100)/255; + char lineBuffer[4]; + snprintf_P(lineBuffer, 4, PSTR("%-3d"), brightness100); + drawString(1, lineHeight, lineBuffer); + //lastRedraw = millis(); + } +} + +void FourLineDisplayUsermod::updateSpeed() { + knownEffectSpeed = effectSpeed; + if (overlayUntil == 0) { + fxspeed100 = ((uint16_t)effectSpeed*100)/255; + char lineBuffer[4]; + snprintf_P(lineBuffer, 4, PSTR("%-3d"), fxspeed100); + drawString(5, lineHeight, lineBuffer); + //lastRedraw = millis(); + } +} + +void FourLineDisplayUsermod::updateIntensity() { + knownEffectIntensity = effectIntensity; + if (overlayUntil == 0) { + fxintensity100 = ((uint16_t)effectIntensity*100)/255; + char lineBuffer[4]; + snprintf_P(lineBuffer, 4, PSTR("%-3d"), fxintensity100); + drawString(9, lineHeight, lineBuffer); + //lastRedraw = millis(); + } +} + +void FourLineDisplayUsermod::drawStatusIcons() { + uint8_t col = 15; + uint8_t row = 0; + drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon + if (lineHeight>1) { col--; } else { row++; } + drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon + if (lineHeight>1) { col--; } else { col = row = 0; } + drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nightlight mode +} + +/** + * marks the position of the arrow showing + * the current setting being changed + * pass line and colum info + */ +void FourLineDisplayUsermod::setMarkLine(byte newMarkLineNum, byte newMarkColNum) { + markLineNum = newMarkLineNum; + markColNum = newMarkColNum; +} + +//Draw the arrow for the current setting being changed +void FourLineDisplayUsermod::drawArrow() { + if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1); +} + +//Display the current effect or palette (desiredEntry) +// on the appropriate line (row). +void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { + char lineBuffer[MAX_JSON_CHARS] = { '\0' }; + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (FLD_SemaphoreTake(drawMuxBig, maxWaitLong) != pdTRUE) return; // WLEDMM acquire BIG draw mutex + + if (overlayUntil == 0) { + // Find the mode name in JSON + uint8_t printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1); + if (printedChars < 2) strcpy(lineBuffer, "invalid"); // catch possible error + if (lineBuffer[0]=='*' && lineBuffer[1]==' ') { + // remove "* " from dynamic palettes + for (byte i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\0' + printedChars -= 2; + } else if ((lineBuffer[0]==' ' && lineBuffer[1]>127)) { + // remove note symbol from effect names + for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' + printedChars -= 5; + } + if (lineHeight > 1) { // use this code for 8 line display + char smallBuffer1[MAX_MODE_LINE_SPACE+1] = { '\0' }; + char smallBuffer2[MAX_MODE_LINE_SPACE+1] = { '\0' }; + uint8_t smallChars1 = 0; + uint8_t smallChars2 = 0; + if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits + while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; + lineBuffer[printedChars] = 0; + drawString(1, row*lineHeight, lineBuffer); + } else { // for long names divide the text into 2 lines and print them small + bool spaceHit = false; + for (uint8_t i = 0; i < printedChars; i++) { + switch (lineBuffer[i]) { + case ' ': + if (i > 4 && !spaceHit) { + spaceHit = true; + break; + } + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + else smallBuffer1[smallChars1++] = lineBuffer[i]; + break; + default: + if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; + else smallBuffer1[smallChars1++] = lineBuffer[i]; + break; + } + } + while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; + smallBuffer1[smallChars1] = 0; + smallBuffer1[MAX_MODE_LINE_SPACE -1] = '\0'; // ensure the string ends where it should (while loop above can overshoot by 1) + drawString(1, row*lineHeight, smallBuffer1, true); + while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; + smallBuffer2[smallChars2] = 0; + smallBuffer2[MAX_MODE_LINE_SPACE -1] = '\0'; // ensure the string ends where it should (while loop above can overshoot by 1) + drawString(1, row*lineHeight+1, smallBuffer2, true); + } + } else { // use this code for 4 ling displays + char smallBuffer3[MAX_MODE_LINE_SPACE+1] = {'\0'}; // uses 1x1 icon for mode/palette + uint8_t smallChars3 = 0; + for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; + smallBuffer3[smallChars3] = 0; + drawString(1, row*lineHeight, smallBuffer3, true); + } + } + + FLD_SemaphoreGive(drawMuxBig); // WLEDMM release BIG draw mutex +} + +/** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ +bool FourLineDisplayUsermod::wakeDisplay() { + if (!typeOK || !enabled) return false; + if (!initDone) return false; + if (displayTurnedOff) { + unsigned long now = millis(); + while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing + drawing = true; + clear(); + // Turn the display back on + sleepOrClock(false); + //lastRedraw = millis(); + drawing = false; + return true; + } + return false; +} + +/** + * Allows you to show one line and a glyph as overlay for a period of time. + * Clears the screen and prints. + * Used in Rotary Encoder usermod. + */ +void FourLineDisplayUsermod::overlay(const char* line1, long showHowLong, byte glyphType) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (!initDone) return; // WLEDMM bugfix + unsigned long now = millis(); + while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing + while ((reDrawing && overlayUntil == 0) && (millis()-now < 250)) delay(10); // wait if someone else is re-drawing + drawing = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (glyphType>0 && glyphType<255) { + if (lineHeight > 1) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font + else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true); + } + if (line1) { + String buf = line1; + center(buf, getCols()); + drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str()); + } + overlayUntil = millis() + showHowLong; + drawing = false; +} + +/** + * Allows you to show Akemi WLED logo overlay for a period of time. + * Clears the screen and prints. + */ +void FourLineDisplayUsermod::overlayLogo(long showHowLong) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + unsigned long now = millis(); + while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing + drawing = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (lineHeight > 1) { + //add a bit of randomness + switch (millis()%3) { + case 0: + //WLED + draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2); + draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2); + draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2); + draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2); + break; + case 1: + //WLED Akemi + drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true); + drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true); + drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true); + break; + case 2: + //Akemi + //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font + drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true); + drawString(6, 6, "WLED"); + break; + } + } else { + switch (millis()%3) { + case 0: + //WLED + draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2); + draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2); + draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2); + draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2); + break; + case 1: + //WLED Akemi + drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4); + drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4); + drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4); + break; + case 2: + //Akemi + //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash + draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2); + break; + } + } + overlayUntil = millis() + showHowLong; + drawing = false; +} + +/** + * Allows you to show two lines as overlay for a period of time. + * Clears the screen and prints. + * Used in Auto Save usermod + */ +void FourLineDisplayUsermod::overlay(const char* line1, const char* line2, long showHowLong) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (!initDone) return; // WLEDMM bugfix + unsigned long now = millis(); + while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing + while ((reDrawing && overlayUntil == 0) && (millis()-now < 250)) delay(10); // wait if someone else is re-drawing + drawing = true; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (line1) { + String buf = line1; + center(buf, getCols()); + drawString(0, 1*lineHeight, buf.c_str()); + } + if (line2) { + String buf = line2; + center(buf, getCols()); + drawString(0, 2*lineHeight, buf.c_str()); + } + overlayUntil = millis() + showHowLong; + drawing = false; +} + +void FourLineDisplayUsermod::networkOverlay(const char* line1, long showHowLong) { + if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it + if (!initDone) return; // WLEDMM bugfix + unsigned long now = millis(); + while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing + drawing = true; + + String line; + // Turn the display back on + if (!wakeDisplay()) clear(); + // Print the overlay + if (line1) { + line = line1; + center(line, getCols()); + drawString(0, 0, line.c_str()); + } + // Second row with Wifi name + line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); + if (line.length() < getCols()) center(line, getCols()); + drawString(0, lineHeight, line.c_str()); + // Print `~` char to indicate that SSID is longer, than our display + if (knownSsid.length() > getCols()) { + drawString(getCols() - 1, 0, "~"); + } + // Third row with IP and Password in AP Mode + line = knownIp.toString(); + center(line, getCols()); + drawString(0, lineHeight*2, line.c_str()); + line = ""; + if (apActive) { + line = apPass; + } else if (strcmp(serverDescription, "WLED") != 0) { + line = serverDescription; + } + center(line, getCols()); + drawString(0, lineHeight*3, line.c_str()); + overlayUntil = millis() + showHowLong; + drawing = false; +} + + +/** + * Enable sleep (turn the display off) or clock mode. + */ +void FourLineDisplayUsermod::sleepOrClock(bool sleepEnable) { + if (sleepEnable) { + displayTurnedOff = true; + //setContrast(contrastFix? 2+ contrast/4 : 0); // un-comment to dim display in "clock mode" + if (clockMode) { // WLEDMM removed " && ntpEnabled" + knownMinute = knownHour = 99; + showTime(); + } else + setPowerSave(1); + } else { + displayTurnedOff = false; + setPowerSave(0); + //setContrast(contrast); // un-comment to restore display brightness on wakeup + } +} + +/** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ +bool FourLineDisplayUsermod::handleButton(uint8_t b) { + yield(); + if (!enabled + || b // button 0 only + || buttonType[b] == BTN_TYPE_SWITCH + || buttonType[b] == BTN_TYPE_NONE + || buttonType[b] == BTN_TYPE_RESERVED + || buttonType[b] == BTN_TYPE_PIR_SENSOR + || buttonType[b] == BTN_TYPE_ANALOG + || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + return false; + } + + unsigned long now = millis(); + static bool buttonPressedBefore = false; + static bool buttonLongPressed = false; + static unsigned long buttonPressedTime = 0; + static unsigned long buttonWaitTime = 0; + bool handled = false; + + //momentary button logic + if (isButtonPressed(b)) { //pressed + + if (!buttonPressedBefore) buttonPressedTime = now; + buttonPressedBefore = true; + + if (now - buttonPressedTime > 600) { //long press + //TODO: handleButton() handles button 0 without preset in a different way for double click + //so we need to override with same behaviour + //DEBUG_PRINTLN(F("4LD action.")); + //if (!buttonLongPressed) longPressAction(0); + buttonLongPressed = true; + return false; + } + + } else if (!isButtonPressed(b) && buttonPressedBefore) { //released + + long dur = now - buttonPressedTime; + if (dur < 50) { + buttonPressedBefore = false; + return true; + } //too short "press", debounce + + bool doublePress = buttonWaitTime; //did we have short press before? + buttonWaitTime = 0; + + if (!buttonLongPressed) { //short press + // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + //TODO: handleButton() handles button 0 without preset in a different way for double click + if (doublePress) { + networkOverlay(PSTR("NETWORK INFO"),7000); + handled = true; + } else { + buttonWaitTime = now; + } + } + buttonPressedBefore = false; + buttonLongPressed = false; + } + // if 350ms elapsed since last press/release it is a short press + if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) { + buttonWaitTime = 0; + //TODO: handleButton() handles button 0 without preset in a different way for double click + //so we need to override with same behaviour + //shortPressAction(0); + //handled = false; + } + return handled; +} + +#if CONFIG_FREERTOS_UNICORE +#define ARDUINO_RUNNING_CORE 0 +#else +#ifndef ARDUINO_RUNNING_CORE +#define ARDUINO_RUNNING_CORE 1 +#endif +#endif +void FourLineDisplayUsermod::onUpdateBegin(bool init) { +#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + if (init && Display_Task) { + vTaskSuspend(Display_Task); // update is about to begin, disable task to prevent crash + } else { + // update has failed or create task requested + if (Display_Task) + vTaskResume(Display_Task); + else + xTaskCreateUniversal( // this is guaranteed to work on any ESP32 (single or dual core) + [](void * par) { // Function to implement the task + // see https://www.freertos.org/vtaskdelayuntil.html + const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; + TickType_t xLastWakeTime = xTaskGetTickCount(); + for(;;) { + delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. + // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. + xLastWakeTime = xTaskGetTickCount(); // workaround for vTaskDelayUntil bug: it does not always keep the last time so we refresh it explicitly + FourLineDisplayUsermod::getInstance()->redraw(false); + vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, by doing nothing until next REFRESH_RATE_MS millis + } + }, + "4LD", // Name of the task +// 3072, // Stack size in words + 4096, // bigger Stack size in words + NULL, // Task input parameter + 1, // Priority of the task (not idle) + &Display_Task, // Task handle + ARDUINO_RUNNING_CORE + ); + } +#endif +} + +/* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ +//void addToJsonInfo(JsonObject& root) { + //JsonObject user = root["u"]; + //if (user.isNull()) user = root.createNestedObject("u"); + //JsonArray data = user.createNestedArray(F("4LineDisplay")); + //data.add(F("Loaded.")); +//} + +/* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ +//void addToJsonState(JsonObject& root) { +//} + +/* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ +//void readFromJsonState(JsonObject& root) { +// if (!initDone) return; // prevent crash on boot applyPreset() +//} + +void FourLineDisplayUsermod::appendConfigData() { + oappend(SET_F("addHB('4LineDisplay');")); + + oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'SSD1306',1);")); + oappend(SET_F("addOption(dd,'SH1106',2);")); + oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); + oappend(SET_F("addOption(dd,'SSD1309 128x64',10);")); + oappend(SET_F("addOption(dd,'SSD1305',4);")); + oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); + oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); + oappend(SET_F("addOption(dd,'SSD1327 SPI 128x128',9);")); + bool isSPI = displayIsSPI(type); + // WLEDMM add defaults + oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','I2C/SPI CLK');")); + oappend(SET_F("dRO('4LineDisplay:pin[]',0);")); // disable read only pins + if (isSPI) { + oappend(SET_F("rOpt('4LineDisplay:pin[]',0,'use global (")); oappendi(spi_sclk); oappend(")',-1);"); + #ifdef FLD_PIN_CLOCKSPI + oappend(SET_F("xOpt('4LineDisplay:pin[]',0,' āŽŒ',")); oappendi(FLD_PIN_CLOCKSPI); oappend(");"); + #endif + } else { + oappend(SET_F("rOpt('4LineDisplay:pin[]',0,'use global (")); oappendi(i2c_scl); oappend(")',-1);"); + #ifdef FLD_PIN_SCL + oappend(SET_F("xOpt('4LineDisplay:pin[]',0,' āŽŒ',")); oappendi(FLD_PIN_SCL); oappend(");"); + #endif + } + oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'','I2C/SPI DTA');")); + if (isSPI) { + oappend(SET_F("rOpt('4LineDisplay:pin[]',1,'use global (")); oappendi(spi_mosi); oappend(")',-1);"); + #ifdef FLD_PIN_MOSISPI + oappend(SET_F("xOpt('4LineDisplay:pin[]',1,' āŽŒ',")); oappendi(FLD_PIN_MOSISPI); oappend(");"); + #endif + } else { + oappend(SET_F("rOpt('4LineDisplay:pin[]',1,'use global (")); oappendi(i2c_sda); oappend(")',-1);"); + #ifdef FLD_PIN_SDA + oappend(SET_F("xOpt('4LineDisplay:pin[]',1,' āŽŒ',")); oappendi(FLD_PIN_SDA); oappend(");"); + #endif + } + oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI CS');")); + #ifdef FLD_PIN_CS + oappend(SET_F("xOpt('4LineDisplay:pin[]',2,' āŽŒ',")); oappendi(FLD_PIN_CS); oappend(");"); + #endif + oappend(SET_F("addInfo('4LineDisplay:pin[]',3,'','SPI DC');")); + #ifdef FLD_PIN_DC + oappend(SET_F("xOpt('4LineDisplay:pin[]',3,' āŽŒ',")); oappendi(FLD_PIN_DC); oappend(");"); + #endif + oappend(SET_F("addInfo('4LineDisplay:pin[]',4,'','SPI RST');")); + #ifdef FLD_PIN_RESET + oappend(SET_F("xOpt('4LineDisplay:pin[]',4,' āŽŒ',")); oappendi(FLD_PIN_RESET); oappend(");"); + #endif + + //WLEDMM add errorMessage to um settings + if (strcmp(errorMessage, "") != 0) { + oappend(SET_F("addInfo('errorMessage', 0, 'error: ")); oappend(errorMessage); oappend("! Correct and reboot');"); + } +} + +/* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ +void FourLineDisplayUsermod::addToConfig(JsonObject& root) { + // determine if we are using global HW pins (data & clock) + int8_t hw_dta, hw_clk; + if (displayIsSPI(type)) { + hw_clk = spi_sclk; + hw_dta = spi_mosi; + } else { + hw_clk = i2c_scl; + hw_dta = i2c_sda; + } + + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + + JsonArray io_pin = top.createNestedArray("pin"); + for (int i=0; i<5; i++) { + if (i==0 && ioPin[i]==hw_clk) io_pin.add(-1); // do not store global HW pin + else if (i==1 && ioPin[i]==hw_dta) io_pin.add(-1); // do not store global HW pin + else io_pin.add(ioPin[i]); + } + top["type"] = type; + top[FPSTR(_flip)] = (bool) flip; + top[FPSTR(_contrast)] = contrast; + top[FPSTR(_contrastFix)] = (bool) contrastFix; + #if !defined(ARDUINO_ARCH_ESP32) || !defined(FLD_ESP32_USE_THREADS) + top[FPSTR(_refreshRate)] = refreshRate; + #endif + top[FPSTR(_screenTimeOut)] = screenTimeout/1000; + top[FPSTR(_sleepMode)] = (bool) sleepMode; + top[FPSTR(_clockMode)] = (bool) clockMode; + top[FPSTR(_showSeconds)] = (bool) showSeconds; + top[FPSTR(_busClkFrequency)] = ioFrequency/1000; + DEBUG_PRINTLN(F("4 Line Display config saved.")); +} + +/* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ +bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { + bool needsRedraw = false; + DisplayType newType = type; + int8_t oldPin[5]; for (byte i=0; i<5; i++) oldPin[i] = ioPin[i]; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + newType = top["type"] | newType; + for (byte i=0; i<5; i++) ioPin[i] = top["pin"][i] | ioPin[i]; + flip = top[FPSTR(_flip)] | flip; + contrast = top[FPSTR(_contrast)] | contrast; + #if !defined(ARDUINO_ARCH_ESP32) || !defined(FLD_ESP32_USE_THREADS) + refreshRate = top[FPSTR(_refreshRate)] | refreshRate; + refreshRate = min(5000, max(250, (int)refreshRate)); + #endif + screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; + sleepMode = top[FPSTR(_sleepMode)] | sleepMode; + clockMode = top[FPSTR(_clockMode)] | clockMode; + showSeconds = top[FPSTR(_showSeconds)] | showSeconds; + contrastFix = top[FPSTR(_contrastFix)] | contrastFix; + if (displayIsSPI(newType)) + ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency + else + ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + type = newType; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + bool pinsChanged = false; + for (byte i=0; i<5; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } + #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + unsigned long now = millis(); + while ((drawing || reDrawing) && millis()-now < 300) delay(10); // wait if someone else is drawing + #endif + drawing = false; + reDrawing = false; + + if (pinsChanged || type!=newType) { + if (typeOK) { + typeOK = false; + if (u8x8 != nullptr) delete u8x8; //WLEDMM warning: deleting object of polymorphic class type 'U8X8' which has non-virtual destructor might cause undefined behaviour [-Wdelete-non-virtual-dtor] + u8x8 = nullptr; + USER_PRINTLN(F("Display terminated.")); + } + PinOwner po = PinOwner::UM_FourLineDisplay; + bool isSPI = displayIsSPI(type); + if (isSPI) { + pinManager.deallocateMultiplePins((const uint8_t *)(&oldPin[2]), 3, po); + bool isHW = (oldPin[0]==spi_sclk && oldPin[1]==spi_mosi); + if (oldPin[0]==-1 && oldPin[1]==-1) isHW = true; // WLEDMM "use global" means hardware driver + if (spi_sclk==-1 && spi_mosi==-1) isHW = false; // WLEDMM global pins not set -> software driver + if (isHW) po = PinOwner::HW_SPI; + } else { + //bool isHW = (oldPin[0]==i2c_scl && oldPin[1]==i2c_sda); + //if ((ioPin[0] == -1) || (ioPin[1] == -1)) isHW = true; // WLEDMM "use global" = hardware + //if (isHW) po = PinOwner::HW_I2C; // WLEDMM don't try to de-alloc HW pins. + } + pinManager.deallocateMultiplePins((const uint8_t *)oldPin, 2, po); + type = newType; + setup(); + needsRedraw |= true; + } else { + if (enabled && typeOK && (nullptr != u8x8)) { // WLEDMM ensure we have a valid, active driver + u8x8->setBusClock(ioFrequency); // can be used for SPI too + setVcomh(contrastFix); + setContrast(contrast); + setFlipMode(flip); + } + } + knownHour = 99; + if (needsRedraw && !wakeDisplay()) redraw(true); + else overlayLogo(3500); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_contrastFix)].isNull(); +} + +// WLEDMM clean up some macros, so they don't cause problems in other usermods +#undef FLD_SemaphoreTake +#undef FLD_SemaphoreGive diff --git a/usermods/usermod_v2_games/readme.md b/usermods/usermod_v2_games/readme.md new file mode 100644 index 0000000000..8917a1fba3 --- /dev/null +++ b/usermods/usermod_v2_games/readme.md @@ -0,0 +1,10 @@ +# Usermods API v2 example usermod + +In this usermod file you can find the documentation on how to take advantage of the new version 2 usermods! + +## Installation + +Copy `usermod_v2_example.h` to the wled00 directory. +Uncomment the corresponding lines in `usermods_list.cpp` and compile! +_(You shouldn't need to actually install this, it does nothing useful)_ + diff --git a/usermods/usermod_v2_games/usermod_v2_games.h b/usermods/usermod_v2_games/usermod_v2_games.h new file mode 100644 index 0000000000..1b1f904700 --- /dev/null +++ b/usermods/usermod_v2_games/usermod_v2_games.h @@ -0,0 +1,379 @@ +/* + Games usermod by ewowi, september 2022 + + Contains: + - mode_pongGame + - Depending on USERMOD_MPU6050_IMU + - mode_IMUTest (shows IMU values only if WLED_DEBUG) + - class Frame3D and struct Voxel + - mode_3DIMUCube (uses class Frame3D to show a rotating cube, if USERMOD_MPU6050_IMU then IMU used for rotation) + - class GamesUsermod (Add the modes/effects and initiates IMU) +*/ + +#pragma once + +#include "wled.h" + +//inspired by https://noobtuts.com/cpp/2d-pong-game +typedef struct PongBall { + float x;// = SEGMENT.virtualWidth() / 2; + float y;// = SEGMENT.virtualHeight() / 2; + float dir_x;// = -1; + float dir_y;// = 0; + uint8_t width;// = 8; + uint8_t height;// = 8; + float speed;// = 1; + uint8_t scoreLeft, scoreRight; + uint32_t color; + void move() { + x += dir_x * speed; + y += dir_y * speed; + } + void vec2_norm() { + // sets a vectors length to 1 (which means that x + y == 1) + float length = sqrt((dir_x * dir_x) + (dir_y * dir_y)); + if (length != 0.0f) { + length = 1.0f / length; + dir_x *= length; + dir_x *= length; + } + } + void hit() { + // hit left wall? + if (x <= 0) { + dir_x = fabs(dir_x); // force it to be positive + // scoreLeft++; + // if (scoreLeft>9) scoreLeft = 0; + } + // hit right wall? + if (x + width-1 >= SEGMENT.virtualWidth()-1) { + dir_x = -fabs(dir_x); // force it to be negative + // scoreRight++; + // if (scoreRight>9) scoreRight = 0; + } + // hit top wall? + if (y <= 0) { + dir_y = fabs(dir_y); // force it to be positive + } + // hit bottom wall? + if (y + height-1 >= SEGMENT.virtualHeight()-1) { + dir_y = -fabs(dir_y); // force it to be negative + } + } + bool hit(PongBall *other) { + if (x < other->x + other->width && + x >= other->x && + y < other->y + other->height && + y >= other->y) { + // set fly direction depending on where it hit the racket + // (t is 0.5 if hit at top, 0 at center, -0.5 at bottom) + float t = ((y - other->y) / other->height) - 0.5f; + dir_x = fabs(dir_x); // force it to be positive + dir_y = t; + return true; + } + else + return false; + } +} pongBall; + + +//effect functions +uint16_t mode_pongGame(void) { + + uint16_t dataSize = 3 * sizeof(pongBall); + if (!SEGENV.allocateData(dataSize)) {SEGMENT.fill(SEGCOLOR(0)); return 350;} //mode_static(); //allocation failed + + PongBall* ball = reinterpret_cast(SEGENV.data); + PongBall* racket_left = reinterpret_cast(SEGENV.data + sizeof(pongBall)); + PongBall* racket_right = reinterpret_cast(SEGENV.data + 2* sizeof(pongBall)); + + // static uint16_t previousX, previousY; + + uint16_t vW = SEGMENT.virtualWidth(); + uint16_t vH = SEGMENT.virtualHeight(); + + if (SEGENV.call == 0) { + ball->width = 1; + ball->height = 1; + ball->x = vW/2; + ball->y = vH/2; + ball->dir_x = -0.1; + ball->dir_y = 0.18; + ball->color = BLUE; + // ball->speed = 1;//SEGMENT.speed/30.0; + + racket_left->width = 1; + racket_left->height = vH/4; + racket_left->x = 0; + racket_left->y = vH/2 - racket_left->height/2; + racket_left->dir_y = 0.18; + racket_left->speed = 1; + racket_left->color = BLUE; + + racket_right->width = 1; + racket_right->height = vH/4; + racket_right->x = vW - 1; + racket_right->y = vH/2 - racket_right->height/2; + racket_right->dir_y = -0.18; + racket_right->speed = 1; + racket_right->color = BLUE; + } + + ball->speed = SEGMENT.speed/30.0; + + SEGMENT.fill(BLACK); + + ball->move(); + racket_left->move(); + racket_right->move(); + + + if (ball->hit(racket_left)) { + ball->scoreLeft++; + if (ball->scoreLeft>9) ball->scoreLeft = 0; + + racket_left->color = RED; + } else { + racket_left->color = BLUE; + } + + if (ball->hit(racket_right)) { + ball->scoreRight++; + if (ball->scoreRight>9) ball->scoreRight = 0; + + racket_right->color = RED; + } else { + racket_right->color = BLUE; + } + + ball->hit(); + racket_left->hit(); + racket_right->hit(); + + ball->vec2_norm(); + racket_left->vec2_norm(); + racket_right->vec2_norm(); + + SEGMENT.setPixelColorXY((uint16_t)ball->x, (uint16_t)ball->y, ball->color); + + SEGMENT.drawLine(0, racket_left->y, 0, racket_left->y + racket_left->height-1, racket_left->color); + SEGMENT.drawLine(vW-1, racket_right->y, vW-1, racket_right->y + racket_right->height-1, racket_right->color); + + for (int i=0; iscoreRight, ball->scoreLeft); + SEGMENT.drawCharacter(tempString[0], vW/2-5, -2, 5, 8, BLUE); + SEGMENT.drawCharacter(tempString[1], vW/2+2, -2, 5, 8, BLUE); + + return FRAMETIME; +} + +static const char _data_FX_MODE_PONGGAME[] PROGMEM = "šŸŽ® Pong ☾@!;!;!;2"; + +//https://howtomechatronics.com/tutorials/arduino/arduino-and-mpu6050-accelerometer-and-gyroscope-tutorial/ + +#ifdef USERMOD_MPU6050_IMU +MPU6050Driver *IMU = nullptr; +uint16_t mode_IMUTest(void) { + SEGMENT.fill(BLACK); + + uint8_t y = 0; + + if ((IMU != nullptr) && (IMU->dmpReady)) { + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aa.x+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aa.y+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aa.z+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aaReal.x+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aaReal.y+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aaReal.z+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->gy.x+1024)/(2*1024), y+=1, RED); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->gy.y+1024)/(2*1024), y+=1, RED); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->gy.z+1024)/(2*1024), y+=1, RED); + + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aaWorld.x+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aaWorld.y+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aaWorld.z+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE); + + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->ypr[0]* 180/M_PI+180)/(2*180), y+=1, RED); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->ypr[1]* 180/M_PI+180)/(2*180), y+=1, RED); + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->ypr[2]* 180/M_PI+180)/(2*180), y+=1, RED); + } + + return FRAMETIME; +} +static const char _data_FX_MODE_IMUTest[] PROGMEM = "šŸŽ® IMUTest ☾@;;;2d"; + +#endif + +//WLEDMM 3D to 2D mapping +struct Voxel { + float x; + float y; + float z; + uint32_t col; +}; + +//https://xem.github.io/articles/projection.html +class Frame3D { + private: + std::vector points; + void rotate(float a, float b, float angle, float &x, float &y) { + x = cosf(angle) * a - sinf(angle) * b; + y = sinf(angle) * a + cosf(angle) * b; + } + float yaw; + float pitch; + float roll; + public: + Frame3D(float yaw1, float pitch1, float roll1) { + points.clear(); + yaw = yaw1; + pitch = pitch1; + roll = roll1; + } + ~Frame3D() { + std::sort(points.begin(), points.end(), [](const Voxel &lhs, const Voxel &rhs) {return lhs.z > rhs.z;}); + for(size_t i = 0; i < points.size(); ++i) { + float w = 0.5;//SEGMENT.virtualWidth()/2; + float h = 0.5;//SEGMENT.virtualHeight()/2; + float perspective = SEGMENT.intensity / 64.0; + if(points[i].z > 0) { + float projX, projY; + projX = w+points[i].x/points[i].z*perspective; + projY = h+points[i].y/points[i].z*perspective; + SEGMENT.setPixelColorXY(projX, projY, points[i].col, false); //no aa + } + } + } + void setPixelColorXYZ(Voxel voxel) { + float camx = 0; + float camy = 0; + float camz = -6; + rotate(voxel.x,voxel.z,yaw, voxel.x, voxel.z); // Camera yaw + rotate(voxel.y,voxel.z,pitch, voxel.y, voxel.z); // Camera pitch + rotate(voxel.x,voxel.y,roll, voxel.x, voxel.y); // Camera roll + voxel.x -= camx; + voxel.y -= camy; + voxel.z -= camz; + points.push_back(voxel); + } + void drawLineXYZ(Voxel from, Voxel to, uint32_t col) { + //causes crash on ESP8266: StoreProhibited: A store referenced a page mapped with an attribute that does not permit stores, maybe not enough free heap + for (float x=MIN(from.x, to.x); x<=MAX(from.x, to.x); x+=.05) + for (float y=MIN(from.y, to.y); y<=MAX(from.y, to.y); y+=.05) + for (float z=MIN(from.z, to.z); z<=MAX(from.z, to.z); z+=.05) + setPixelColorXYZ({x, y, z, col}); + } +}; + +uint16_t mode_3DIMUCube(void) { + SEGMENT.fill(BLACK); + + float yaw = 0; + float pitch = 0; + float roll = 0; + + #ifdef USERMOD_MPU6050_IMU + if ((IMU != nullptr) && (IMU->dmpReady)) { + yaw = -IMU->ypr[0]; + pitch = IMU->ypr[1]; + roll = IMU->ypr[2]; + } + #else + //simulate rotation + yaw = (fmod(SEGENV.call, 360)-180) / (180/M_PI); //-180 .. 180 + pitch = yaw; + roll = yaw; + #endif + + Frame3D frame3D = Frame3D(yaw, pitch, roll); + + Voxel leftbottomback = {-1,-1,-1}; + Voxel rightbottomback = {1,-1,-1}; + Voxel lefttopback = {-1,1,-1}; + Voxel righttopback = {1,1,-1}; + Voxel leftbottomfront = {-1,-1,1}; + Voxel rightbottomfront = {1,-1,1}; + Voxel lefttopfront = {-1,1,1}; + Voxel righttopfront = {1,1,1}; + frame3D.drawLineXYZ(leftbottomback, rightbottomback, SEGMENT.color_from_palette(255/12, false, true, 0)); + frame3D.drawLineXYZ(leftbottomback, lefttopback, SEGMENT.color_from_palette(255/12*2, false, true, 0)); + frame3D.drawLineXYZ(rightbottomback, righttopback, SEGMENT.color_from_palette(255/12*3, false, true, 0)); + frame3D.drawLineXYZ(lefttopback, righttopback, SEGMENT.color_from_palette(255/12*4, false, true, 0)); + + frame3D.drawLineXYZ(leftbottomfront, leftbottomback, SEGMENT.color_from_palette(255/12*9, false, true, 0)); + frame3D.drawLineXYZ(rightbottomfront, rightbottomback, SEGMENT.color_from_palette(255/12*10, false, true, 0)); + frame3D.drawLineXYZ(lefttopfront, lefttopback, SEGMENT.color_from_palette(255/12*11, false, true, 0)); + frame3D.drawLineXYZ(righttopfront, righttopback, SEGMENT.color_from_palette(255/12*12, false, true, 0)); + + frame3D.drawLineXYZ(leftbottomfront, rightbottomfront, SEGMENT.color_from_palette(255/12*5, false, true, 0)); + frame3D.drawLineXYZ(leftbottomfront, lefttopfront, SEGMENT.color_from_palette(255/12*6, false, true, 0)); + frame3D.drawLineXYZ(rightbottomfront, righttopfront, SEGMENT.color_from_palette(255/12*7, false, true, 0)); + frame3D.drawLineXYZ(lefttopfront, righttopfront, SEGMENT.color_from_palette(255/12*8, false, true, 0)); + + return FRAMETIME; +} +static const char _data_FX_MODE_3DIMUCube[] PROGMEM = "šŸŽ® 3DIMUCube ☾@,Perspective;!;!;2;pal=1"; //WLEDMM random smooth + +class GamesUsermod : public Usermod { + private: + + public: + + void setup() { + strip.addEffect(255, &mode_pongGame, _data_FX_MODE_PONGGAME); + #ifdef USERMOD_MPU6050_IMU + IMU = (MPU6050Driver *)usermods.lookup(USERMOD_ID_IMU); + #ifdef WLED_DEBUG + strip.addEffect(255, &mode_IMUTest, _data_FX_MODE_IMUTest); + #endif + #endif + strip.addEffect(255, &mode_3DIMUCube, _data_FX_MODE_3DIMUCube); //works also without IMU + } + + void connected() { + } + + void loop() { + } + + void addToJsonState(JsonObject& root) + { + //root["user0"] = userVar0; + } + + void readFromJsonState(JsonObject& root) + { + userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + } + + void addToConfig(JsonObject& root) + { + // JsonObject top = root.createNestedObject("gamesUsermod"); + } + + bool readFromConfig(JsonObject& root) + { + + JsonObject top = root["gamesUsermod"]; + + bool configComplete = !top.isNull(); + + return configComplete; + } + + void handleOverlayDraw() + { + } + + uint16_t getId() + { + return USERMOD_ID_GAMES; + } +}; \ No newline at end of file diff --git a/usermods/usermod_v2_klipper_percentage/readme.md b/usermods/usermod_v2_klipper_percentage/readme.md index 0619bf8572..e967d6b217 100644 --- a/usermods/usermod_v2_klipper_percentage/readme.md +++ b/usermods/usermod_v2_klipper_percentage/readme.md @@ -10,7 +10,7 @@ curl --location --request GET 'http://[]/printer/objects/query?virtual_sdcard=pr ## Usage Compile the source with the buildflag `-D USERMOD_KLIPPER_PERCENTAGE` added. -You can also use the WLBD bot in the Discord by simply extending an exsisting build enviroment: +You can also use the WLBD bot in the Discord by simply extending an existing build environment: ``` [env:esp32klipper] extends = env:esp32dev @@ -23,7 +23,7 @@ build_flags = ${common.build_flags_esp32} -D USERMOD_KLIPPER_PERCENTAGE Checkbox to enable or disable the overlay ### Klipper IP: -IP adress of your Klipper instance you want to poll. ESP has to be restarted after change +IP address of your Klipper instance you want to poll. ESP has to be restarted after change ### Direction : 0 = normal diff --git a/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h b/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h index 0e19cc80ff..2f591b1547 100644 --- a/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h +++ b/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h @@ -79,7 +79,7 @@ class klipper_percentage : public Usermod httpGet(wifiClient, errorMessage); if (strcmp(errorMessage, "") == 0) { - PSRAMDynamicJsonDocument klipperDoc(4096); // in practive about 2673 + PSRAMDynamicJsonDocument klipperDoc(4096); // in practice about 2673 DeserializationError error = deserializeJson(klipperDoc, wifiClient); if (error) { diff --git a/usermods/usermod_v2_mode_sort/readme.md b/usermods/usermod_v2_mode_sort/readme.md deleted file mode 100644 index c24322f32e..0000000000 --- a/usermods/usermod_v2_mode_sort/readme.md +++ /dev/null @@ -1,33 +0,0 @@ -# Mode Sort - -v2 usermod that provides data about modes and -palettes to other usermods. Notably it provides: -* A direct method for a mode or palette name -* Ability to retrieve mode and palette names in - alphabetical order - -```char **getModesQStrings()``` - -Provides a char* array (pointers) to the names of the -palettes contained in JSON_mode_names, in the same order as -JSON_mode_names. These strings end in double quote (") -(or \0 if there is a problem). - -```byte *getModesAlphaIndexes()``` - -A byte array designating the indexes of names of the -modes in alphabetical order. "Solid" will always remain -at the top of the list. - -```char **getPalettesQStrings()``` - -Provides a char* array (pointers) to the names of the -palettes contained in JSON_palette_names, in the same order as -JSON_palette_names. These strings end in double quote (") -(or \0 if there is a problem). - -```byte *getPalettesAlphaIndexes()``` - -A byte array designating the indexes of names of the -palettes in alphabetical order. "Default" and those -starting with "(" will always remain at the top of the list. diff --git a/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h b/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h deleted file mode 100644 index 092206bb6d..0000000000 --- a/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h +++ /dev/null @@ -1,244 +0,0 @@ -#pragma once - -#include "wled.h" - -// -// v2 usermod that provides data about modes and -// palettes to other usermods. Notably it provides: -// * A direct method for a mode or palette name -// * Ability to retrieve mode and palette names in -// alphabetical order -// -// char **getModesQStrings() -// Provides an array of char* (pointers) to the names of the -// palettes within JSON_mode_names, in the same order as -// JSON_mode_names. These strings end in double quote (") -// (or \0 if there is a problem). -// -// byte *getModesAlphaIndexes() -// An array of byte designating the indexes of names of the -// modes in alphabetical order. "Solid" will always remain -// at the front of the list. -// -// char **getPalettesQStrings() -// Provides an array of char* (pointers) to the names of the -// palettes within JSON_palette_names, in the same order as -// JSON_palette_names. These strings end in double quote (") -// (or \0 if there is a problem). -// -// byte *getPalettesAlphaIndexes() -// An array of byte designating the indexes of names of the -// palettes in alphabetical order. "Default" and those -// starting with "(" will always remain at the front of the list. -// - -// Number of modes at the start of the list to not sort -#define MODE_SORT_SKIP_COUNT 1 - -// Which list is being sorted -char **listBeingSorted = nullptr; - -/** - * Modes and palettes are stored as strings that - * end in a quote character. Compare two of them. - * We are comparing directly within either - * JSON_mode_names or JSON_palette_names. - */ -int re_qstringCmp(const void *ap, const void *bp) { - char *a = listBeingSorted[*((byte *)ap)]; - char *b = listBeingSorted[*((byte *)bp)]; - int i = 0; - do { - char aVal = pgm_read_byte_near(a + i); - if (aVal >= 97 && aVal <= 122) { - // Lowercase - aVal -= 32; - } - char bVal = pgm_read_byte_near(b + i); - if (bVal >= 97 && bVal <= 122) { - // Lowercase - bVal -= 32; - } - // Relly we shouldn't ever get to '\0' - if (aVal == '"' || bVal == '"' || aVal == '\0' || bVal == '\0') { - // We're done. one is a substring of the other - // or something happenend and the quote didn't stop us. - if (aVal == bVal) { - // Same value, probably shouldn't happen - // with this dataset - return 0; - } - else if (aVal == '"' || aVal == '\0') { - return -1; - } - else { - return 1; - } - } - if (aVal == bVal) { - // Same characters. Move to the next. - i++; - continue; - } - // We're done - if (aVal < bVal) { - return -1; - } - else { - return 1; - } - } while (true); - // We shouldn't get here. - return 0; -} - -class ModeSortUsermod : public Usermod { -private: - - // Pointers the start of the mode names within JSON_mode_names - char **modes_qstrings = nullptr; - - // Array of mode indexes in alphabetical order. - byte *modes_alpha_indexes = nullptr; - - // Pointers the start of the palette names within JSON_palette_names - char **palettes_qstrings = nullptr; - - // Array of palette indexes in alphabetical order. - byte *palettes_alpha_indexes = nullptr; - -public: - /** - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - */ - void setup() { - // Sort the modes and palettes on startup - // as they are guarantted to change. - sortModesAndPalettes(); - } - - char **getModesQStrings() { - return modes_qstrings; - } - - byte *getModesAlphaIndexes() { - return modes_alpha_indexes; - } - - char **getPalettesQStrings() { - return palettes_qstrings; - } - - byte *getPalettesAlphaIndexes() { - return palettes_alpha_indexes; - } - - /** - * This Usermod doesn't have anything for loop. - */ - void loop() {} - - /** - * Sort the modes and palettes to the index arrays - * modes_alpha_indexes and palettes_alpha_indexes. - */ - void sortModesAndPalettes() { - modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); - modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); - re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); - - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); - - int skipPaletteCount = 1; - while (true) { - // How many palette names start with '*' and should not be sorted? - // (Also skipping the first one, 'Default'). - if (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') { - skipPaletteCount++; - } - else { - break; - } - } - re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); - } - - byte *re_initIndexArray(int numModes) { - byte *indexes = (byte *)malloc(sizeof(byte) * numModes); - for (byte i = 0; i < numModes; i++) { - indexes[i] = i; - } - return indexes; - } - - /** - * Return an array of mode or palette names from the JSON string. - * They don't end in '\0', they end in '"'. - */ - char **re_findModeStrings(const char json[], int numModes) { - char **modeStrings = (char **)malloc(sizeof(char *) * numModes); - uint8_t modeIndex = 0; - bool insideQuotes = false; - // advance past the mark for markLineNum that may exist. - char singleJsonSymbol; - - // Find the mode name in JSON - bool complete = false; - for (size_t i = 0; i < strlen_P(json); i++) { - singleJsonSymbol = pgm_read_byte_near(json + i); - if (singleJsonSymbol == '\0') break; - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - if (insideQuotes) { - // We have a new mode or palette - modeStrings[modeIndex] = (char *)(json + i + 1); - } - break; - case '[': - break; - case ']': - if (!insideQuotes) complete = true; - break; - case ',': - if (!insideQuotes) modeIndex++; - default: - if (!insideQuotes) break; - } - if (complete) break; - } - return modeStrings; - } - - /** - * Sort either the modes or the palettes using quicksort. - */ - void re_sortModes(char **modeNames, byte *indexes, int count, int numSkip) { - listBeingSorted = modeNames; - qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp); - listBeingSorted = nullptr; - } - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void addToJsonState(JsonObject &root) {} - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void readFromJsonState(JsonObject &root) {} - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_MODE_SORT; - } -}; diff --git a/usermods/usermod_v2_ping_pong_clock/readme.md b/usermods/usermod_v2_ping_pong_clock/readme.md index 9f01b3ebf8..f8219489d1 100644 --- a/usermods/usermod_v2_ping_pong_clock/readme.md +++ b/usermods/usermod_v2_ping_pong_clock/readme.md @@ -7,4 +7,4 @@ Contains a modification to use WLED in combination with the Ping Pong Ball LED C To install this Usermod, you instruct PlatformIO to compile the Project with the USERMOD_PING_PONG_CLOCK flag. WLED then automatically provides you with various settings on the Usermod Page. -Note: Depending on the size of your clock, you may have to update the led indices for the indivdual numbers and the base indices. +Note: Depending on the size of your clock, you may have to update the led indices for the individual numbers and the base indices. diff --git a/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h b/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h index a690c1b1e4..40ff675c08 100644 --- a/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h +++ b/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h @@ -18,15 +18,15 @@ class PingPongClockUsermod : public Usermod // ---- Variables for correct LED numbering below, edit only if your clock is built different ---- - int baseH = 43; // Adress for the one place of the hours - int baseHH = 7; // Adress for the tens place of the hours - int baseM = 133; // Adress for the one place of the minutes - int baseMM = 97; // Adress for the tens place of the minutes - int colon1 = 79; // Adress for the first colon led - int colon2 = 80; // Adress for the second colon led + int baseH = 43; // Address for the one place of the hours + int baseHH = 7; // Address for the tens place of the hours + int baseM = 133; // Address for the one place of the minutes + int baseMM = 97; // Address for the tens place of the minutes + int colon1 = 79; // Address for the first colon led + int colon2 = 80; // Address for the second colon led // Matrix for the illumination of the numbers - // Note: These only define the increments of the base adress. e.g. to define the second Minute you have to add the baseMM to every led position + // Note: These only define the increments of the base address. e.g. to define the second Minute you have to add the baseMM to every led position const int numbers[10][10] = { { 0, 1, 4, 6, 13, 15, 18, 19, -1, -1 }, // 0: null diff --git a/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample deleted file mode 100644 index 4b537a8f7c..0000000000 --- a/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample +++ /dev/null @@ -1,48 +0,0 @@ -[platformio] -default_envs = d1_mini -; default_envs = esp32dev - -[env:esp32dev] -board = esp32dev -platform = espressif32@3.2 -build_unflags = ${common.build_unflags} -build_flags = - ${common.build_flags_esp32} - -D USERMOD_MODE_SORT - -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 - -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19 - -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 - -D LEDPIN=16 -D BTNPIN=13 -upload_speed = 460800 -lib_ignore = - ESPAsyncTCP - ESPAsyncUDP - -[env:d1_mini] -board = d1_mini -platform = ${common.platform_wled_default} -platform_packages = ${common.platform_packages} -upload_speed = 460800 -board_build.ldscript = ${common.ldscript_4m1m} -build_unflags = ${common.build_unflags} -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_MODE_SORT - -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 - -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=12 -D ENCODER_CLK_PIN=14 -D ENCODER_SW_PIN=13 - -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 - -D LEDPIN=3 -D BTNPIN=0 -monitor_filters = esp8266_exception_decoder - -[env] -lib_deps = - fastled/FastLED @ 3.3.2 - NeoPixelBus @ 2.6.0 - ESPAsyncTCP @ 1.2.0 - ESPAsyncUDP - AsyncTCP @ 1.0.3 - IRremoteESP8266 @ 2.7.3 - https://github.com/lorol/LITTLEFS.git - https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.0 - U8g2@~2.27.2 - Wire diff --git a/usermods/usermod_v2_rotary_encoder_ui/readme.md b/usermods/usermod_v2_rotary_encoder_ui/readme.md deleted file mode 100644 index 5e4f3cff63..0000000000 --- a/usermods/usermod_v2_rotary_encoder_ui/readme.md +++ /dev/null @@ -1,39 +0,0 @@ -# Rotary Encoder UI Usermod - -First, thanks to the authors of other Rotary Encoder usermods. - -This usermod starts to provide a relatively complete on-device -UI when paired with the Four Line Display usermod. I strongly -encourage you to try them together. - -[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) - -## Installation - -Copy and update the example `platformio_override.ini.sample` to the root directory of your particular build. -This file should be placed in the same directory as `platformio.ini`. - -### Define Your Options - -* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_ROTARY_ENCODER_GPIO` - define the GPIO function (INPUT, INPUT_PULLUP, etc...) -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available - (see the Four Line Display usermod `readme.md` for more details) -* `ENCODER_DT_PIN`   - defaults to 12 -* `ENCODER_CLK_PIN` - defaults to 14 -* `ENCODER_SW_PIN`   - defaults to 13 -* `USERMOD_ROTARY_ENCODER_GPIO` - GPIO functionality: - `INPUT_PULLUP` to use internal pull-up - `INPUT` to use pull-up on the PCB - -### PlatformIO requirements - -No special requirements. - -Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. - -## Change Log - -2021-02 -* First public release diff --git a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h deleted file mode 100644 index 02bc0ccdaa..0000000000 --- a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h +++ /dev/null @@ -1,496 +0,0 @@ -#pragma once - -#include "wled.h" - -// -// Inspired by the v1 usermods -// * rotary_encoder_change_brightness -// * rotary_encoder_change_effect -// -// v2 usermod that provides a rotary encoder-based UI. -// -// This usermod allows you to control: -// -// * Brightness -// * Selected Effect -// * Effect Speed -// * Effect Intensity -// * Palette -// -// Change between modes by pressing a button. -// -// Dependencies -// * This usermod REQURES the ModeSortUsermod -// * This Usermod works best coupled with -// FourLineDisplayUsermod. -// - -#ifndef ENCODER_DT_PIN -#define ENCODER_DT_PIN 12 -#endif - -#ifndef ENCODER_CLK_PIN -#define ENCODER_CLK_PIN 14 -#endif - -#ifndef ENCODER_SW_PIN -#define ENCODER_SW_PIN 13 -#endif - -#ifndef USERMOD_FOUR_LINE_DISPLAY -// These constants won't be defined if we aren't using FourLineDisplay. -#define FLD_LINE_BRIGHTNESS 0 -#define FLD_LINE_MODE 0 -#define FLD_LINE_EFFECT_SPEED 0 -#define FLD_LINE_EFFECT_INTENSITY 0 -#define FLD_LINE_PALETTE 0 -#endif - - -// The last UI state -#define LAST_UI_STATE 4 - - -class RotaryEncoderUIUsermod : public Usermod { -private: - int fadeAmount = 10; // Amount to change every step (brightness) - unsigned long currentTime; - unsigned long loopTime; - int8_t pinA = ENCODER_DT_PIN; // DT from encoder - int8_t pinB = ENCODER_CLK_PIN; // CLK from encoder - int8_t pinC = ENCODER_SW_PIN; // SW from encoder - unsigned char select_state = 0; // 0: brightness, 1: effect, 2: effect speed - unsigned char button_state = HIGH; - unsigned char prev_button_state = HIGH; - -#ifdef USERMOD_FOUR_LINE_DISPLAY - FourLineDisplayUsermod *display; -#else - void* display = nullptr; -#endif - - byte *modes_alpha_indexes = nullptr; - byte *palettes_alpha_indexes = nullptr; - - unsigned char Enc_A; - unsigned char Enc_B; - unsigned char Enc_A_prev = 0; - - bool currentEffectAndPaletteInitialized = false; - uint8_t effectCurrentIndex = 0; - uint8_t effectPaletteIndex = 0; - - bool initDone = false; - bool enabled = true; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _DT_pin[]; - static const char _CLK_pin[]; - static const char _SW_pin[]; - -public: - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - */ - void setup() - { - DEBUG_PRINTLN(F("Usermod Rotary Encoder init.")); - PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; - if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { - // BUG: configuring this usermod with conflicting pins - // will cause it to de-allocate pins it does not own - // (at second config) - // This is the exact type of bug solved by pinManager - // tracking the owner tags.... - pinA = pinB = pinC = -1; - enabled = false; - return; - } - - #ifndef USERMOD_ROTARY_ENCODER_GPIO - #define USERMOD_ROTARY_ENCODER_GPIO INPUT_PULLUP - #endif - pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO); - pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO); - pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO); - - currentTime = millis(); - loopTime = currentTime; - - ModeSortUsermod *modeSortUsermod = (ModeSortUsermod*) usermods.lookup(USERMOD_ID_MODE_SORT); - modes_alpha_indexes = modeSortUsermod->getModesAlphaIndexes(); - palettes_alpha_indexes = modeSortUsermod->getPalettesAlphaIndexes(); - -#ifdef USERMOD_FOUR_LINE_DISPLAY - // This Usermod uses FourLineDisplayUsermod for the best experience. - // But it's optional. But you want it. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); - if (display != nullptr) { - display->setLineType(FLD_LINE_BRIGHTNESS); - display->setMarkLine(3); - } -#endif - - initDone = true; - } - - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() - { - //Serial.println("Connected to WiFi!"); - } - - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - * - * Tips: - * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. - * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. - * - * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. - * Instead, use a timer check as shown here. - */ - void loop() - { - if (!enabled) return; - - currentTime = millis(); // get the current elapsed time - - // Initialize effectCurrentIndex and effectPaletteIndex to - // current state. We do it here as (at least) effectCurrent - // is not yet initialized when setup is called. - if (!currentEffectAndPaletteInitialized) { - findCurrentEffectAndPalette(); - } - - if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz - { - button_state = digitalRead(pinC); - if (prev_button_state != button_state) - { - if (button_state == LOW) - { - prev_button_state = button_state; - - char newState = select_state + 1; - if (newState > LAST_UI_STATE) newState = 0; - - bool changedState = true; - if (display != nullptr) { - switch(newState) { - case 0: - changedState = changeState("Brightness", FLD_LINE_BRIGHTNESS, 3); - break; - case 1: - changedState = changeState("Select FX", FLD_LINE_MODE, 2); - break; - case 2: - changedState = changeState("FX Speed", FLD_LINE_EFFECT_SPEED, 3); - break; - case 3: - changedState = changeState("FX Intensity", FLD_LINE_EFFECT_INTENSITY, 3); - break; - case 4: - changedState = changeState("Palette", FLD_LINE_PALETTE, 3); - break; - } - } - if (changedState) { - select_state = newState; - } - } - else - { - prev_button_state = button_state; - } - } - int Enc_A = digitalRead(pinA); // Read encoder pins - int Enc_B = digitalRead(pinB); - if ((!Enc_A) && (Enc_A_prev)) - { // A has gone from high to low - if (Enc_B == HIGH) - { // B is high so clockwise - switch(select_state) { - case 0: - changeBrightness(true); - break; - case 1: - changeEffect(true); - break; - case 2: - changeEffectSpeed(true); - break; - case 3: - changeEffectIntensity(true); - break; - case 4: - changePalette(true); - break; - } - } - else if (Enc_B == LOW) - { // B is low so counter-clockwise - switch(select_state) { - case 0: - changeBrightness(false); - break; - case 1: - changeEffect(false); - break; - case 2: - changeEffectSpeed(false); - break; - case 3: - changeEffectIntensity(false); - break; - case 4: - changePalette(false); - break; - } - } - } - Enc_A_prev = Enc_A; // Store value of A for next time - loopTime = currentTime; // Updates loopTime - } - } - - void findCurrentEffectAndPalette() { - currentEffectAndPaletteInitialized = true; - for (uint8_t i = 0; i < strip.getModeCount(); i++) { - //byte value = modes_alpha_indexes[i]; - if (modes_alpha_indexes[i] == effectCurrent) { - effectCurrentIndex = i; - break; - } - } - - for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { - //byte value = palettes_alpha_indexes[i]; - if (palettes_alpha_indexes[i] == strip.getSegment(0).palette) { - effectPaletteIndex = i; - break; - } - } - } - - boolean changeState(const char *stateName, byte lineThreeMode, byte markedLine) { -#ifdef USERMOD_FOUR_LINE_DISPLAY - if (display != nullptr) { - if (display->wakeDisplay()) { - // Throw away wake up input - return false; - } - display->overlay("Mode change", stateName, 1500); - display->setLineType(lineThreeMode); - display->setMarkLine(markedLine); - } - #endif - return true; - } - - void lampUdated() { - colorUpdated(CALL_MODE_BUTTON); - updateInterfaces(CALL_MODE_BUTTON); - } - - void changeBrightness(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - // Throw away wake up input - return; - } -#endif - if (increase) { - bri = (bri + fadeAmount <= 255) ? (bri + fadeAmount) : 255; - } - else { - bri = (bri - fadeAmount >= 0) ? (bri - fadeAmount) : 0; - } - lampUdated(); - } - - void changeEffect(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - // Throw away wake up input - return; - } -#endif - if (increase) { - effectCurrentIndex = (effectCurrentIndex + 1 >= strip.getModeCount()) ? 0 : (effectCurrentIndex + 1); - } - else { - effectCurrentIndex = (effectCurrentIndex - 1 < 0) ? (strip.getModeCount() - 1) : (effectCurrentIndex - 1); - } - effectCurrent = modes_alpha_indexes[effectCurrentIndex]; - lampUdated(); - } - - void changeEffectSpeed(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - // Throw away wake up input - return; - } -#endif - if (increase) { - effectSpeed = (effectSpeed + fadeAmount <= 255) ? (effectSpeed + fadeAmount) : 255; - } - else { - effectSpeed = (effectSpeed - fadeAmount >= 0) ? (effectSpeed - fadeAmount) : 0; - } - lampUdated(); - } - - void changeEffectIntensity(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - // Throw away wake up input - return; - } -#endif - if (increase) { - effectIntensity = (effectIntensity + fadeAmount <= 255) ? (effectIntensity + fadeAmount) : 255; - } - else { - effectIntensity = (effectIntensity - fadeAmount >= 0) ? (effectIntensity - fadeAmount) : 0; - } - lampUdated(); - } - - void changePalette(bool increase) { -#ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - // Throw away wake up input - return; - } -#endif - if (increase) { - effectPaletteIndex = (effectPaletteIndex + 1 >= strip.getPaletteCount()) ? 0 : (effectPaletteIndex + 1); - } - else { - effectPaletteIndex = (effectPaletteIndex - 1 < 0) ? (strip.getPaletteCount() - 1) : (effectPaletteIndex - 1); - } - effectPalette = palettes_alpha_indexes[effectPaletteIndex]; - lampUdated(); - } - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - /* - void addToJsonInfo(JsonObject& root) - { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - JsonArray lightArr = user.createNestedArray("Light"); //name - lightArr.add(reading); //value - lightArr.add(" lux"); //unit - } - */ - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void addToJsonState(JsonObject &root) - { - //root["user0"] = userVar0; - } - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void readFromJsonState(JsonObject &root) - { - //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value - //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); - } - - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ - void addToConfig(JsonObject &root) { - // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_DT_pin)] = pinA; - top[FPSTR(_CLK_pin)] = pinB; - top[FPSTR(_SW_pin)] = pinC; - DEBUG_PRINTLN(F("Rotary Encoder config saved.")); - } - - /** - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - * - * The function should return true if configuration was successfully loaded or false if there was no configuration. - */ - bool readFromConfig(JsonObject &root) { - // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; - int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; - int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; - - enabled = top[FPSTR(_enabled)] | enabled; - - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json - pinA = newDTpin; - pinB = newCLKpin; - pinC = newSWpin; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin) { - pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); - pinA = newDTpin; - pinB = newCLKpin; - pinC = newSWpin; - if (pinA<0 || pinB<0 || pinC<0) { - enabled = false; - return true; - } - setup(); - } - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_enabled)].isNull(); - } - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_ROTARY_ENC_UI; - } -}; - -// strings to reduce flash memory usage (used more than twice) -const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; -const char RotaryEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; -const char RotaryEncoderUIUsermod::_DT_pin[] PROGMEM = "DT-pin"; -const char RotaryEncoderUIUsermod::_CLK_pin[] PROGMEM = "CLK-pin"; -const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index 516362380d..811c50d09e 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -30,9 +30,9 @@ Also shows if the timer is enabled. ## Installation -Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions.
-To activate this alternative usermod, add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, -or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file. +Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions +Then to activate this alternative usermod add `#define USE_ALT_DISPLAY` to the `usermods_list.cpp` file, + or add `-D USE_ALT_DISPLAY` to the original `platformio_override.ini.sample` file ### PlatformIO requirements diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 35d3e6f320..66a1401a1d 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -4,7 +4,7 @@ // // Inspired by the original v2 usermods -// * usermod_v2_rotaty_encoder_ui +// * usermod_v2_rotary_encoder_ui // // v2 usermod that provides a rotary encoder-based UI. // @@ -29,10 +29,6 @@ // * display network (long press buttion) // -#ifdef USERMOD_MODE_SORT - #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" -#endif - #ifndef ENCODER_DT_PIN #define ENCODER_DT_PIN 18 #endif @@ -79,7 +75,7 @@ static int re_qstringCmp(const void *ap, const void *bp) { // Lowercase bVal -= 32; } - // Relly we shouldn't ever get to '\0' + // Really we shouldn't ever get to '\0' if (aVal == '"' || bVal == '"' || aVal == '\0' || bVal == '\0') { // We're done. one is a substring of the other // or something happenend and the quote didn't stop us. @@ -116,7 +112,7 @@ static int re_qstringCmp(const void *ap, const void *bp) { class RotaryEncoderUIUsermod : public Usermod { private: int8_t fadeAmount = 5; // Amount to change every step (brightness) - unsigned long loopTime; + unsigned long loopTime = 0; unsigned long buttonPressedTime = 0; unsigned long buttonWaitTime = 0; @@ -184,78 +180,20 @@ class RotaryEncoderUIUsermod : public Usermod { * Sort the modes and palettes to the index arrays * modes_alpha_indexes and palettes_alpha_indexes. */ - void sortModesAndPalettes() { - //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); - modes_qstrings = strip.getModeDataSrc(); - modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); - re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); - - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); // only use internal palettes - - // How many palette names start with '*' and should not be sorted? - // (Also skipping the first one, 'Default'). - int skipPaletteCount = 1; - while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount++]) == '*') ; - re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); - } - - byte *re_initIndexArray(int numModes) { - byte *indexes = (byte *)malloc(sizeof(byte) * numModes); - for (byte i = 0; i < numModes; i++) { - indexes[i] = i; - } - return indexes; - } + void sortModesAndPalettes(); + + byte *re_initIndexArray(int numModes); /** * Return an array of mode or palette names from the JSON string. * They don't end in '\0', they end in '"'. */ - const char **re_findModeStrings(const char json[], int numModes) { - const char **modeStrings = (const char **)malloc(sizeof(const char *) * numModes); - uint8_t modeIndex = 0; - bool insideQuotes = false; - // advance past the mark for markLineNum that may exist. - char singleJsonSymbol; - - // Find the mode name in JSON - bool complete = false; - for (size_t i = 0; i < strlen_P(json); i++) { - singleJsonSymbol = pgm_read_byte_near(json + i); - if (singleJsonSymbol == '\0') break; - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - if (insideQuotes) { - // We have a new mode or palette - modeStrings[modeIndex] = (char *)(json + i + 1); - } - break; - case '[': - break; - case ']': - if (!insideQuotes) complete = true; - break; - case ',': - if (!insideQuotes) modeIndex++; - default: - if (!insideQuotes) break; - } - if (complete) break; - } - return modeStrings; - } + const char **re_findModeStrings(const char json[], int numModes); /** * Sort either the modes or the palettes using quicksort. */ - void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip) { - if (!modeNames) return; - listBeingSorted = modeNames; - qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp); - listBeingSorted = nullptr; - } + void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip); public: @@ -263,57 +201,13 @@ class RotaryEncoderUIUsermod : public Usermod { * setup() is called once at boot. WiFi is not yet connected at this point. * You can use it to initialize variables, sensors or similar. */ - void setup() - { - DEBUG_PRINTLN(F("Usermod Rotary Encoder init.")); - PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; - if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { - // BUG: configuring this usermod with conflicting pins - // will cause it to de-allocate pins it does not own - // (at second config) - // This is the exact type of bug solved by pinManager - // tracking the owner tags.... - pinA = pinB = pinC = -1; - enabled = false; - return; - } - - #ifndef USERMOD_ROTARY_ENCODER_GPIO - #define USERMOD_ROTARY_ENCODER_GPIO INPUT_PULLUP - #endif - pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO); - pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO); - pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO); - - loopTime = millis(); - - currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; - - if (!initDone) sortModesAndPalettes(); - -#ifdef USERMOD_FOUR_LINE_DISPLAY - // This Usermod uses FourLineDisplayUsermod for the best experience. - // But it's optional. But you want it. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); - if (display != nullptr) { - display->setMarkLine(1, 0); - } -#endif - - initDone = true; - Enc_A = digitalRead(pinA); // Read encoder pins - Enc_B = digitalRead(pinB); - Enc_A_prev = Enc_A; - } + void setup(); /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ - void connected() - { - //Serial.println("Connected to WiFi!"); - } + void connected(); /* * loop() is called continuously. Here you can check for events, read sensors, etc. @@ -325,604 +219,885 @@ class RotaryEncoderUIUsermod : public Usermod { * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. * Instead, use a timer check as shown here. */ - void loop() + void loop(); + + void displayNetworkInfo(); + + void findCurrentEffectAndPalette(); + + boolean changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph); + + void lampUdated(); + + void changeBrightness(bool increase); + + + void changeEffect(bool increase); + + + void changeEffectSpeed(bool increase); + + + void changeEffectIntensity(bool increase); + + + void changeCustom(uint8_t par, bool increase); + + + void changePalette(bool increase); + + + void changeHue(bool increase); + + void changeSat(bool increase); + + void changePreset(bool increase) ; + + void changeCCT(bool increase); + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + /* + void addToJsonInfo(JsonObject& root) { - if (!enabled || strip.isUpdating()) return; - unsigned long currentTime = millis(); // get the current elapsed time - - // Initialize effectCurrentIndex and effectPaletteIndex to - // current state. We do it here as (at least) effectCurrent - // is not yet initialized when setup is called. - - if (!currentEffectAndPaletteInitialized) { - findCurrentEffectAndPalette(); + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit + } + */ + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + /* + void addToJsonState(JsonObject &root) + { + //root["user0"] = userVar0; + } + */ + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + /* + void readFromJsonState(JsonObject &root) + { + //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); + } + */ + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root); + + //WLEDMM: add appendConfigData + void appendConfigData(); + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject &root); + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_ROTARY_ENC_UI; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; +const char RotaryEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; +const char RotaryEncoderUIUsermod::_DT_pin[] PROGMEM = "DT-pin"; +const char RotaryEncoderUIUsermod::_CLK_pin[] PROGMEM = "CLK-pin"; +const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; +const char RotaryEncoderUIUsermod::_presetHigh[] PROGMEM = "preset-high"; +const char RotaryEncoderUIUsermod::_presetLow[] PROGMEM = "preset-low"; +const char RotaryEncoderUIUsermod::_applyToAll[] PROGMEM = "apply-2-all-seg"; + +/** + * Sort the modes and palettes to the index arrays + * modes_alpha_indexes and palettes_alpha_indexes. + */ +void RotaryEncoderUIUsermod::sortModesAndPalettes() { + //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); + modes_qstrings = strip.getModeDataSrc(); + modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); + re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); + + palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); + palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); // only use internal palettes + + // How many palette names start with '*' and should not be sorted? + // (Also skipping the first one, 'Default'). + int skipPaletteCount = 1; + while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount++]) == '*') ; + re_sortModes(palettes_qstrings, palettes_alpha_indexes, strip.getPaletteCount(), skipPaletteCount); +} + +byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) { + byte *indexes = (byte *)malloc(sizeof(byte) * numModes); + for (byte i = 0; i < numModes; i++) { + indexes[i] = i; + } + return indexes; +} + +/** + * Return an array of mode or palette names from the JSON string. + * They don't end in '\0', they end in '"'. + */ +const char **RotaryEncoderUIUsermod::re_findModeStrings(const char json[], int numModes) { + const char **modeStrings = (const char **)malloc(sizeof(const char *) * numModes); + uint8_t modeIndex = 0; + bool insideQuotes = false; + // advance past the mark for markLineNum that may exist. + char singleJsonSymbol; + + // Find the mode name in JSON + bool complete = false; + for (size_t i = 0; i < strlen_P(json); i++) { + singleJsonSymbol = pgm_read_byte_near(json + i); + if (singleJsonSymbol == '\0') break; + switch (singleJsonSymbol) { + case '"': + insideQuotes = !insideQuotes; + if (insideQuotes) { + // We have a new mode or palette + modeStrings[modeIndex] = (char *)(json + i + 1); + } + break; + case '[': + break; + case ']': + if (!insideQuotes) complete = true; + break; + case ',': + if (!insideQuotes) modeIndex++; + default: + if (!insideQuotes) break; } + if (complete) break; + } + return modeStrings; +} + +/** + * Sort either the modes or the palettes using quicksort. + */ +void RotaryEncoderUIUsermod::re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip) { + if (!modeNames) return; + listBeingSorted = modeNames; + qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp); + listBeingSorted = nullptr; +} + + +// public: +/* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ +void RotaryEncoderUIUsermod::setup() +{ + DEBUG_PRINTLN(F("Usermod Rotary Encoder init.")); + PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; + if ((pinA < 0) || (pinB < 0)) { //WLEDMM catch error: [ 1839][E][esp32-hal-gpio.c:102] __pinMode(): Invalid pin selected + enabled = false; + DEBUG_PRINTLN(F("Invalid GPIO pins for Usermod Rotary Encoder (ALT).")); //WLEDMM add debug info + return; + } + if (!enabled) return; // WLEDMM don't allocated PINS if disabled + if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { + // BUG: configuring this usermod with conflicting pins + // will cause it to de-allocate pins it does not own + // (at second config) + // This is the exact type of bug solved by pinManager + // tracking the owner tags.... + pinA = pinB = pinC = -1; + enabled = false; + DEBUG_PRINTLN(F("Failed to allocate GPIO pins for Usermod Rotary Encoder (ALT).")); //WLEDMM add debug info + return; + } + #ifndef USERMOD_ROTARY_ENCODER_GPIO + #define USERMOD_ROTARY_ENCODER_GPIO INPUT_PULLUP + #endif + pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO); + pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO); + if (pinC >= 0) pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO); // WLEDMM catch error + + loopTime = millis(); + + currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; + + if (!initDone) sortModesAndPalettes(); + +#ifdef USERMOD_FOUR_LINE_DISPLAY + // This Usermod uses FourLineDisplayUsermod for the best experience. + // But it's optional. But you want it. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + if (display != nullptr) { + display->setMarkLine(1, 0); + } +#endif + + initDone = true; + Enc_A = digitalRead(pinA); // Read encoder pins + Enc_B = digitalRead(pinB); + Enc_A_prev = Enc_A; + USER_PRINTLN(F("Rotary encoder (ALT) setup completed.")); // WLEDMM inform user +} + +/* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ +void RotaryEncoderUIUsermod::connected() +{ + //Serial.println("Connected to WiFi!"); +} + +/* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ +void RotaryEncoderUIUsermod::loop() +{ + if (!enabled) return; + unsigned long currentTime = millis(); // get the current elapsed time + + if (strip.isUpdating() && (currentTime - loopTime < 4)) return; // WLEDMM: be nice, but not too nice + + // Initialize effectCurrentIndex and effectPaletteIndex to + // current state. We do it here as (at least) effectCurrent + // is not yet initialized when setup is called. + + if (!currentEffectAndPaletteInitialized) { + findCurrentEffectAndPalette(); + } + + if (modes_alpha_indexes != nullptr) { // WLEDMM bugfix if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) { currentEffectAndPaletteInitialized = false; } + } - if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz - { - loopTime = currentTime; // Updates loopTime - - bool buttonPressed = !digitalRead(pinC); //0=pressed, 1=released - if (buttonPressed) { - if (!buttonPressedBefore) buttonPressedTime = currentTime; - buttonPressedBefore = true; - if (currentTime-buttonPressedTime > 3000) { - if (!buttonLongPressed) displayNetworkInfo(); //long press for network info - buttonLongPressed = true; - } - } else if (!buttonPressed && buttonPressedBefore) { - bool doublePress = buttonWaitTime; - buttonWaitTime = 0; - if (!buttonLongPressed) { - if (doublePress) { - toggleOnOff(); - lampUdated(); - } else { - buttonWaitTime = currentTime; - } + if (currentTime - loopTime >= 2) // 2ms since last check of encoder = 500Hz + { + loopTime = currentTime; // Updates loopTime + + bool buttonPressed = false; + if (pinC >= 0) buttonPressed = !digitalRead(pinC); //0=pressed, 1=released + if (buttonPressed) { + if (!buttonPressedBefore) buttonPressedTime = currentTime; + buttonPressedBefore = true; + if (currentTime-buttonPressedTime > 3000) { + if (!buttonLongPressed) displayNetworkInfo(); //long press for network info + buttonLongPressed = true; + } + } else if (!buttonPressed && buttonPressedBefore) { + bool doublePress = buttonWaitTime; + buttonWaitTime = 0; + if (!buttonLongPressed) { + if (doublePress) { + toggleOnOff(); + lampUdated(); + } else { + buttonWaitTime = currentTime; } - buttonLongPressed = false; - buttonPressedBefore = false; } - if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp - buttonWaitTime = 0; - char newState = select_state + 1; - bool changedState = false; - char lineBuffer[64]; - do { - // finde new state - switch (newState) { - case 0: strcpy_P(lineBuffer, PSTR("Brightness")); changedState = true; break; - case 1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; // speed - case 2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; // intensity - case 3: strcpy_P(lineBuffer, PSTR("Color Palette")); changedState = true; break; - case 4: strcpy_P(lineBuffer, PSTR("Effect")); changedState = true; break; - case 5: strcpy_P(lineBuffer, PSTR("Main Color")); changedState = true; break; - case 6: strcpy_P(lineBuffer, PSTR("Saturation")); changedState = true; break; - case 7: - if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++; - else { strcpy_P(lineBuffer, PSTR("CCT")); changedState = true; } - break; - case 8: if (presetHigh==0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR("Preset")); changedState = true; } break; - case 9: - case 10: - case 11: if (!extractModeSlider(effectCurrent, newState-7, lineBuffer, 63)) newState++; else changedState = true; break; // custom - } - if (newState > LAST_UI_STATE) newState = 0; - } while (!changedState); - if (display != nullptr) { - switch (newState) { - case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun - case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward - case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire - case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette - case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece - case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush - case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast - case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart - case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - } + buttonLongPressed = false; + buttonPressedBefore = false; + } + if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp + buttonWaitTime = 0; + char newState = select_state + 1; + bool changedState = false; + char lineBuffer[64] = { '\0' }; + do { + // find new state + switch (newState) { + case 0: strcpy_P(lineBuffer, PSTR("Brightness")); changedState = true; break; + case 1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; // speed + case 2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; // intensity + case 3: strcpy_P(lineBuffer, PSTR("Color Palette")); changedState = true; break; + case 4: strcpy_P(lineBuffer, PSTR("Effect")); changedState = true; break; + case 5: strcpy_P(lineBuffer, PSTR("Main Color")); changedState = true; break; + case 6: strcpy_P(lineBuffer, PSTR("Saturation")); changedState = true; break; + case 7: + if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++; + else { strcpy_P(lineBuffer, PSTR("CCT")); changedState = true; } + break; + case 8: if (presetHigh==0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR("Preset")); changedState = true; } break; + case 9: + case 10: + case 11: if (!extractModeSlider(effectCurrent, newState-7, lineBuffer, 63)) newState++; else changedState = true; break; // custom + } + if (newState > LAST_UI_STATE) newState = 0; + } while (!changedState); + if (display != nullptr) { + switch (newState) { + case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun + case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward + case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire + case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette + case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece + case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush + case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast + case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart + case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star } - if (changedState) select_state = newState; } + if (changedState) select_state = newState; + } - Enc_A = digitalRead(pinA); // Read encoder pins - Enc_B = digitalRead(pinB); - if ((Enc_A) && (!Enc_A_prev)) - { // A has gone from high to low - if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse - { // B is high so clockwise - switch(select_state) { - case 0: changeBrightness(true); break; - case 1: changeEffectSpeed(true); break; - case 2: changeEffectIntensity(true); break; - case 3: changePalette(true); break; - case 4: changeEffect(true); break; - case 5: changeHue(true); break; - case 6: changeSat(true); break; - case 7: changeCCT(true); break; - case 8: changePreset(true); break; - case 9: changeCustom(1,true); break; - case 10: changeCustom(2,true); break; - case 11: changeCustom(3,true); break; - } + Enc_A = digitalRead(pinA); // Read encoder pins + Enc_B = digitalRead(pinB); + if ((Enc_A) && (!Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse + { // B is high so clockwise + switch(select_state) { + case 0: changeBrightness(true); break; + case 1: changeEffectSpeed(true); break; + case 2: changeEffectIntensity(true); break; + case 3: changePalette(true); break; + case 4: changeEffect(true); break; + case 5: changeHue(true); break; + case 6: changeSat(true); break; + case 7: changeCCT(true); break; + case 8: changePreset(true); break; + case 9: changeCustom(1,true); break; + case 10: changeCustom(2,true); break; + case 11: changeCustom(3,true); break; } - else if (Enc_B == HIGH) - { // B is low so counter-clockwise - switch(select_state) { - case 0: changeBrightness(false); break; - case 1: changeEffectSpeed(false); break; - case 2: changeEffectIntensity(false); break; - case 3: changePalette(false); break; - case 4: changeEffect(false); break; - case 5: changeHue(false); break; - case 6: changeSat(false); break; - case 7: changeCCT(false); break; - case 8: changePreset(false); break; - case 9: changeCustom(1,false); break; - case 10: changeCustom(2,false); break; - case 11: changeCustom(3,false); break; - } + } + else if (Enc_B == HIGH) + { // B is low so counter-clockwise + switch(select_state) { + case 0: changeBrightness(false); break; + case 1: changeEffectSpeed(false); break; + case 2: changeEffectIntensity(false); break; + case 3: changePalette(false); break; + case 4: changeEffect(false); break; + case 5: changeHue(false); break; + case 6: changeSat(false); break; + case 7: changeCCT(false); break; + case 8: changePreset(false); break; + case 9: changeCustom(1,false); break; + case 10: changeCustom(2,false); break; + case 11: changeCustom(3,false); break; } } - Enc_A_prev = Enc_A; // Store value of A for next time } + Enc_A_prev = Enc_A; // Store value of A for next time } +} - void displayNetworkInfo() { - #ifdef USERMOD_FOUR_LINE_DISPLAY - display->networkOverlay(PSTR("NETWORK INFO"), 10000); - #endif - } +void RotaryEncoderUIUsermod::displayNetworkInfo() { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) + display->networkOverlay(PSTR("NETWORK INFO"), 10000); + #endif +} - void findCurrentEffectAndPalette() { - currentEffectAndPaletteInitialized = true; - for (uint8_t i = 0; i < strip.getModeCount(); i++) { - if (modes_alpha_indexes[i] == effectCurrent) { - effectCurrentIndex = i; - break; - } +void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { + if (modes_alpha_indexes == nullptr) return; // WLEDMM bugfix + currentEffectAndPaletteInitialized = true; + for (uint8_t i = 0; i < strip.getModeCount(); i++) { + if (modes_alpha_indexes[i] == effectCurrent) { + effectCurrentIndex = i; + break; } + } - for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { - if (palettes_alpha_indexes[i] == effectPalette) { - effectPaletteIndex = i; - break; - } + for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { + if (palettes_alpha_indexes[i] == effectPalette) { + effectPaletteIndex = i; + break; } } +} - boolean changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display != nullptr) { - if (display->wakeDisplay()) { - // Throw away wake up input - display->redraw(true); - return false; - } - display->overlay(stateName, 750, glyph); - display->setMarkLine(markedLine, markedCol); +boolean RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + if (display->wakeDisplay()) { + // Throw away wake up input + display->redraw(true); + return false; } - #endif - return true; + display->overlay(stateName, 750, glyph); + display->setMarkLine(markedLine, markedCol); } +#endif + return true; +} - void lampUdated() { - //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) - // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa - //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required) - stateUpdated(CALL_MODE_BUTTON); +void RotaryEncoderUIUsermod::lampUdated() { + //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) + // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa + //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required) + stateUpdated(CALL_MODE_BUTTON); + if ((millis() - lastInterfaceUpdate) > INTERFACE_UPDATE_COOLDOWN) // WLEDMM respect cooldown times, to avoid crash in AsyncWebSocketMessageBuffer updateInterfaces(CALL_MODE_BUTTON); - } +} - void changeBrightness(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); - lampUdated(); - #ifdef USERMOD_FOUR_LINE_DISPLAY - display->updateBrightness(); - #endif +void RotaryEncoderUIUsermod::changeBrightness(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; } + display->updateRedrawTime(); +#endif + byte lastBri = bri; + if (bri < 40) bri = max(min((increase ? bri+fadeAmount/2 : bri-fadeAmount/2), 255), 0); // WLEDMM slower steps when brightness < 16% + else bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); + if (lastBri != bri) stateChanged = true; // WLEDMM bugfix + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display->canDraw()) // only draw if nothing else is drawing + display->updateBrightness(); +#endif +} - void changeEffect(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); - effectCurrent = modes_alpha_indexes[effectCurrentIndex]; - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); + if (modes_alpha_indexes != nullptr) effectCurrent = modes_alpha_indexes[effectCurrentIndex]; + stateChanged = true; + if (applyToAll) { + for (byte i=0; ishowCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.setMode(effectCurrent); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display->canDraw()) // only draw if nothing else is drawing + display->showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); +#endif +} - void changeEffectSpeed(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateSpeed(); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.speed = effectSpeed; } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display->canDraw()) // only draw if nothing else is drawing + display->updateSpeed(); +#endif +} - void changeEffectIntensity(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (byte i=0; iupdateIntensity(); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.intensity = effectIntensity; } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display->canDraw()) // only draw if nothing else is drawing + display->updateIntensity(); +#endif +} - void changeCustom(uint8_t par, bool increase) { - uint8_t val = 0; - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; +void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + uint8_t val = 0; + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + stateChanged = true; + if (applyToAll) { + uint8_t id = strip.getFirstSelectedSegId(); + Segment& sid = strip.getSegment(id); +#ifdef USERMOD_FOUR_LINE_DISPLAY + switch (par) { + case 3: val = sid.custom3 = max(min((increase ? sid.custom3+fadeAmount : sid.custom3-fadeAmount), 255), 0); break; + case 2: val = sid.custom2 = max(min((increase ? sid.custom2+fadeAmount : sid.custom2-fadeAmount), 255), 0); break; + default: val = sid.custom1 = max(min((increase ? sid.custom1+fadeAmount : sid.custom1-fadeAmount), 255), 0); break; } - display->updateRedrawTime(); - #endif - stateChanged = true; - if (applyToAll) { - uint8_t id = strip.getFirstSelectedSegId(); - Segment& sid = strip.getSegment(id); - switch (par) { - case 3: val = sid.custom3 = max(min((increase ? sid.custom3+fadeAmount : sid.custom3-fadeAmount), 255), 0); break; - case 2: val = sid.custom2 = max(min((increase ? sid.custom2+fadeAmount : sid.custom2-fadeAmount), 255), 0); break; - default: val = sid.custom1 = max(min((increase ? sid.custom1+fadeAmount : sid.custom1-fadeAmount), 255), 0); break; - } - for (byte i=0; ioverlay(lineBuffer, 500, 10); // use star - #endif + } else { +#ifdef USERMOD_FOUR_LINE_DISPLAY + Segment& seg = strip.getMainSegment(); + switch (par) { + case 3: val = seg.custom3 = max(min((increase ? seg.custom3+fadeAmount : seg.custom3-fadeAmount), 255), 0); break; + case 2: val = seg.custom2 = max(min((increase ? seg.custom2+fadeAmount : seg.custom2-fadeAmount), 255), 0); break; + default: val = seg.custom1 = max(min((increase ? seg.custom1+fadeAmount : seg.custom1-fadeAmount), 255), 0); break; + } +#endif } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64] = { '\0' }; + snprintf(lineBuffer, 63, "%d", val); // WLEDMM: avoid string buffer overflow + display->overlay(lineBuffer, 500, 10); // use star +#endif +} - void changePalette(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - effectPaletteIndex = max(min((increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()-1), 0); - effectPalette = palettes_alpha_indexes[effectPaletteIndex]; - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + effectPaletteIndex = max(min((increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()-1), 0); + effectPalette = palettes_alpha_indexes[effectPaletteIndex]; + stateChanged = true; + if (applyToAll) { + for (byte i=0; ishowCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.setPalette(effectPalette); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY +if (display->canDraw()) // only draw if nothing else is drawing + display->showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); +#endif +} - void changeHue(bool increase){ - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); - stateChanged = true; - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); + colorHStoRGB(currentHue1*256, currentSat1, col); + stateChanged = true; + if (applyToAll) { + for (byte i=0; ioverlay(lineBuffer, 500, 7); // use brush - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.colors[0] = RGBW32(col[0], col[1], col[2], col[3]); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64] = { '\0' }; + snprintf(lineBuffer, 63, "%d", currentHue1); // WLEDMM: avoid string buffer overflow + display->overlay(lineBuffer, 500, 7); // use brush +#endif +} - void changeSat(bool increase){ - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); - if (applyToAll) { - for (byte i=0; iwakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); + colorHStoRGB(currentHue1*256, currentSat1, col); + if (applyToAll) { + for (byte i=0; ioverlay(lineBuffer, 500, 8); // use contrast - #endif + } else { + Segment& seg = strip.getSegment(strip.getMainSegmentId()); + seg.colors[0] = RGBW32(col[0], col[1], col[2], col[3]); } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64] = { '\0' }; + snprintf(lineBuffer, 63, "%d", currentSat1); // WLEDMM: avoid string buffer overflow + display->overlay(lineBuffer, 500, 8); // use contrast +#endif +} - void changePreset(bool increase) { - #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); - #endif - if (presetHigh && presetLow && presetHigh > presetLow) { - StaticJsonDocument<64> root; - char str[64]; - sprintf_P(str, PSTR("%d~%d~%s"), presetLow, presetHigh, increase?"":"-"); - root["ps"] = str; - deserializeState(root.as(), CALL_MODE_BUTTON_PRESET); +void RotaryEncoderUIUsermod::changePreset(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + if (presetHigh && presetLow && presetHigh > presetLow) { + StaticJsonDocument<64> root; + char str[64] = { '\0' }; + snprintf_P(str, 64, PSTR("%d~%d~%s"), presetLow, presetHigh, increase?"":"-"); + root["ps"] = str; + deserializeState(root.as(), CALL_MODE_BUTTON_PRESET); /* - String apireq = F("win&PL=~"); - if (!increase) apireq += '-'; - apireq += F("&P1="); - apireq += presetLow; - apireq += F("&P2="); - apireq += presetHigh; - handleSet(nullptr, apireq, false); + String apireq = F("win&PL=~"); + if (!increase) apireq += '-'; + apireq += F("&P1="); + apireq += presetLow; + apireq += F("&P2="); + apireq += presetHigh; + handleSet(nullptr, apireq, false); */ - lampUdated(); - #ifdef USERMOD_FOUR_LINE_DISPLAY - sprintf(str, "%d", currentPreset); - display->overlay(str, 500, 11); // use heart - #endif - } - } - - void changeCCT(bool increase){ + lampUdated(); #ifdef USERMOD_FOUR_LINE_DISPLAY - if (display && display->wakeDisplay()) { - display->redraw(true); - // Throw away wake up input - return; - } - display->updateRedrawTime(); + snprintf(str, 63, "%d", currentPreset); // WLEDMM: avoid string buffer overflow + display->overlay(str, 500, 11); // use heart #endif - currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); + } +} + +void RotaryEncoderUIUsermod::changeCCT(bool increase){ +#ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { + display->redraw(true); + // Throw away wake up input + return; + } + display->updateRedrawTime(); +#endif + currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); // if (applyToAll) { - for (byte i=0; ioverlay(lineBuffer, 500, 10); // use star - #endif - } + lampUdated(); +#ifdef USERMOD_FOUR_LINE_DISPLAY + char lineBuffer[64] = { '\0' }; + snprintf(lineBuffer, 63, "%d", currentCCT); // WLEDMM: avoid string buffer overflow + display->overlay(lineBuffer, 500, 10); // use star +#endif +} - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - /* - void addToJsonInfo(JsonObject& root) - { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - JsonArray lightArr = user.createNestedArray("Light"); //name - lightArr.add(reading); //value - lightArr.add(" lux"); //unit - } +/* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor */ +/* +void addToJsonInfo(JsonObject& root) +{ + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit +} +*/ - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - /* - void addToJsonState(JsonObject &root) - { - //root["user0"] = userVar0; - } +/* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients */ +/* +void addToJsonState(JsonObject &root) +{ + //root["user0"] = userVar0; +} +*/ - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - /* - void readFromJsonState(JsonObject &root) - { - //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value - //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); - } +/* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients */ +/* +void readFromJsonState(JsonObject &root) +{ + //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); +} +*/ - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ - void addToConfig(JsonObject &root) { - // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_DT_pin)] = pinA; - top[FPSTR(_CLK_pin)] = pinB; - top[FPSTR(_SW_pin)] = pinC; - top[FPSTR(_presetLow)] = presetLow; - top[FPSTR(_presetHigh)] = presetHigh; - top[FPSTR(_applyToAll)] = applyToAll; - DEBUG_PRINTLN(F("Rotary Encoder config saved.")); - } - - /** - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - * - * The function should return true if configuration was successfully loaded or false if there was no configuration. - */ - bool readFromConfig(JsonObject &root) { - // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; - int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; - int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; +/** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ +void RotaryEncoderUIUsermod::addToConfig(JsonObject &root) { + // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_DT_pin)] = pinA; + top[FPSTR(_CLK_pin)] = pinB; + top[FPSTR(_SW_pin)] = pinC; + top[FPSTR(_presetLow)] = presetLow; + top[FPSTR(_presetHigh)] = presetHigh; + top[FPSTR(_applyToAll)] = applyToAll; + DEBUG_PRINTLN(F("Rotary Encoder config saved.")); +} - presetHigh = top[FPSTR(_presetHigh)] | presetHigh; - presetLow = top[FPSTR(_presetLow)] | presetLow; - presetHigh = MIN(250,MAX(0,presetHigh)); - presetLow = MIN(250,MAX(0,presetLow)); +//WLEDMM: add appendConfigData +void RotaryEncoderUIUsermod::appendConfigData() +{ + oappend(SET_F("addHB('Rotary-Encoder');")); - enabled = top[FPSTR(_enabled)] | enabled; - applyToAll = top[FPSTR(_applyToAll)] | applyToAll; + #ifdef ENCODER_DT_PIN + oappend(SET_F("xOpt('Rotary-Encoder:DT-pin',1,' āŽŒ',")); oappendi(ENCODER_DT_PIN); oappend(");"); + #endif + #ifdef ENCODER_CLK_PIN + oappend(SET_F("xOpt('Rotary-Encoder:CLK-pin',1,' āŽŒ',")); oappendi(ENCODER_CLK_PIN); oappend(");"); + #endif + #ifdef ENCODER_SW_PIN + oappend(SET_F("xOpt('Rotary-Encoder:SW-pin',1,' āŽŒ',")); oappendi(ENCODER_SW_PIN); oappend(");"); + #endif +} +/** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ +bool RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) { + // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - // first run: reading from cfg.json + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; + int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; + int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; + + presetHigh = top[FPSTR(_presetHigh)] | presetHigh; + presetLow = top[FPSTR(_presetLow)] | presetLow; + presetHigh = MIN(250,MAX(0,presetHigh)); + presetLow = MIN(250,MAX(0,presetLow)); + + enabled = top[FPSTR(_enabled)] | enabled; + applyToAll = top[FPSTR(_applyToAll)] | applyToAll; + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + pinA = newDTpin; + pinB = newCLKpin; + pinC = newSWpin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin) { + pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); + pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); + pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); pinA = newDTpin; pinB = newCLKpin; pinC = newSWpin; - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - // changing parameters from settings page - if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin) { - pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); - pinA = newDTpin; - pinB = newCLKpin; - pinC = newSWpin; - if (pinA<0 || pinB<0 || pinC<0) { - enabled = false; - return true; - } - setup(); + if (pinA<0 || pinB<0) { // WLEDMM support for rotary without pushbutton + enabled = false; + return true; } + if (enabled) setup(); // WLEDMM no pin stealing! } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_applyToAll)].isNull(); - } - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_ROTARY_ENC_UI; } -}; - -// strings to reduce flash memory usage (used more than twice) -const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; -const char RotaryEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; -const char RotaryEncoderUIUsermod::_DT_pin[] PROGMEM = "DT-pin"; -const char RotaryEncoderUIUsermod::_CLK_pin[] PROGMEM = "CLK-pin"; -const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; -const char RotaryEncoderUIUsermod::_presetHigh[] PROGMEM = "preset-high"; -const char RotaryEncoderUIUsermod::_presetLow[] PROGMEM = "preset-low"; -const char RotaryEncoderUIUsermod::_applyToAll[] PROGMEM = "apply-2-all-seg"; + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_applyToAll)].isNull(); +} diff --git a/usermods/usermod_v2_weather/readme.md b/usermods/usermod_v2_weather/readme.md new file mode 100644 index 0000000000..aedfa98d85 --- /dev/null +++ b/usermods/usermod_v2_weather/readme.md @@ -0,0 +1,33 @@ +# Usermods API v2 example usermod + +In this usermod file you can find the documentation on how to take advantage of the new version 2 usermods! + +### adding usermod specific effects + +This usermod also provides an example on how to add your own effects in a usermod. +* the new effect code is in this function, similar to other effects fx.cpp: +```c++ +//effect function +uint16_t mode_2DWeather(void) { + +.... + + return FRAMETIME; +} +static const char _data_FX_MODE_2DWEATHER[] PROGMEM = "Weather@;!;!;2;pal=54"; //temperature palette +``` +* then activated in the usermod setup function +```c++ +class WeatherUsermod : public Usermod { + public: + void setup() { + strip.addEffect(255, &mode_2DWeather, _data_FX_MODE_2DWEATHER); + } +``` + +## Installation + +Copy `usermod_v2_example.h` to the wled00 directory. +Uncomment the corresponding lines in `usermods_list.cpp` and compile! +_(You shouldn't need to actually install this, it does nothing useful)_ + diff --git a/usermods/usermod_v2_weather/usermod_v2_weather.h b/usermods/usermod_v2_weather/usermod_v2_weather.h new file mode 100644 index 0000000000..10f7b03c65 --- /dev/null +++ b/usermods/usermod_v2_weather/usermod_v2_weather.h @@ -0,0 +1,392 @@ +// Weather usermod by ewowi. Next to the weather functionality, this usermod is also a proof of concept +// to work with data obtained from the internet. + +#pragma once + +#include "wled.h" + +// #define WEATHER_DEBUG + +//declare weathermod global variables (always precede with weather_ (pseudo class static variables) +static uint32_t usermods_pushLoop = 0; //effect pushes loop to execute. might be interesting for audioreactive too +static uint8_t weather_units = 1; //config var metric (celsius) is default. (Standard=Kelvin, Imperial is Fahrenheit) +static float weather_minTemp = 0; //config var +static float weather_maxTemp = 40; //config var +static float weather_temps[100]; //array of temperatures +static time_t weather_times[100]; //array of corresponding times + +//effect function +uint16_t mode_2DWeather(void) { + + usermods_pushLoop = millis(); //will be reset to 0 in usermod loop + + SEGMENT.fadeToBlackBy(10); + + float currentTemp = 0; + // time_t currentTime = 0; + + for (int x=0; x= localTime) { + color = RED; + currentTemp = map(localTime, weather_times[x%100], weather_times[(x+1)%100], weather_temps[x%100] * 1000, weather_temps[(x+1)%100] * 1000) / 1000.0; + // currentTime = localTime; + } + else + color = ColorFromPalette(SEGPALETTE, map((uint8_t)weather_temps[x%100], 0, 40, 1, 255), 255, LINEARBLEND); + + for (int y=0; y millis() - 1000 && (lastTime == 0 || millis() - lastTime > 3600 * 1000)) { + lastTime = millis(); + strcpy(errorMessage, ""); //clear possible previous errors + + WiFiClient client; + + char url[180]; + sprintf(url, "GET /data/2.5/forecast?lat=%f&lon=%f&appid=%s&units=%s HTTP/1.0", latitude, longitude, apiKey.c_str(), weather_units==0?"standard":weather_units==1?"metric":"imperial"); + #ifdef WEATHER_DEBUG + Serial.println(url); + #endif + + httpGet(client, url, errorMessage); + + if (strcmp(errorMessage, "") == 0) { + + // https://arduinojson.org/v6/how-to/deserialize-a-very-large-document/ + StaticJsonDocument<256> filter; //in practice about 128 + filter["list"][0]["dt"] = true; + filter["list"][0]["main"]["temp"] = true; + filter["city"]["name"] = true; + filter["city"]["country"] = true; + PSRAMDynamicJsonDocument weatherDoc(4096); //in practice about 2673 + + // Parse JSON object + DeserializationError error = deserializeJson(weatherDoc, client, DeserializationOption::Filter(filter)); + #ifdef WEATHER_DEBUG + Serial.printf("filter %u / %u%% (%u %u %u)\n", (unsigned int)filter.memoryUsage(), 100 * filter.memoryUsage() / filter.capacity(), (unsigned int)filter.size(), filter.overflowed(), (unsigned int)filter.nesting()); + Serial.printf("weatherDoc %u / %u%% (%u %u %u)\n", (unsigned int)weatherDoc.memoryUsage(), 100 * weatherDoc.memoryUsage() / weatherDoc.capacity(), (unsigned int)weatherDoc.size(), weatherDoc.overflowed(), (unsigned int)weatherDoc.nesting()); + serializeJson(filter, Serial); + Serial.println(); + serializeJson(weatherDoc, Serial); + Serial.println(); + #endif + if (error) { + strcat(errorMessage, PSTR("deserializeJson() failed: ")); + strcat(errorMessage, error.c_str()); + } + else { //everything successful!! + JsonObject weatherDocObject = weatherDoc.as(); + JsonArray list = weatherDocObject[F("list")]; + JsonObject city = weatherDocObject["city"]; + strcat(errorMessage, city["name"]); //api successful + strcat(errorMessage, city["country"]); + + uint8_t i = 0; + for (JsonObject listElement: list) { + weather_times[i%100] = listElement["dt"]; + + JsonObject main = listElement["main"]; + weather_temps[i%100] = main["temp"]; + + #ifdef WEATHER_DEBUG + char timeString[64]; + epochToString(listElement["dt"], timeString); + Serial.print(timeString); + + Serial.print(" temp "); + Serial.print(weather_temps[i%100]); + + Serial.print(" city "); + Serial.print(errorMessage); + + Serial.print(" sunrise "); + char sunriseString[64]; + epochToString(city["sunrise"], sunriseString); + Serial.print(sunriseString); + + Serial.print(" localtime "); + char localTimeString[64]; + epochToString(localTime, localTimeString); + Serial.print(localTimeString); + + Serial.println(); + #endif + + i++; + } + } + } + + // Disconnect + client.stop(); + + } + usermods_pushLoop = 0; + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + */ + void addToJsonInfo(JsonObject& root) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + infoArr.add(errorMessage); //value + // infoArr.add(""); //unit + } + + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) + { + //root["user0"] = userVar0; + } + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) + { + // userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); + } + + + void addToConfig(JsonObject& root) + { + Usermod::addToConfig(root); + JsonObject top = root[FPSTR(_name)]; + top[F("apiKey")] = apiKey; + top[F("units")] = weather_units; + top[F("minTemp")] = weather_minTemp; + top[F("maxTemp")] = weather_maxTemp; + } + + + bool readFromConfig(JsonObject& root) + { + bool configComplete = Usermod::readFromConfig(root); + JsonObject top = root[FPSTR(_name)]; + + configComplete &= getJsonValue(top[F("apiKey")], apiKey); + configComplete &= getJsonValue(top[F("units")], weather_units); + configComplete &= getJsonValue(top[F("minTemp")], weather_minTemp); + configComplete &= getJsonValue(top[F("maxTemp")], weather_maxTemp); + + // * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + return configComplete; + } + + void appendConfigData() + { + oappend(SET_F("addHB('Weather');")); // WLEDMM + + oappend(SET_F("dd=addDropdown('Weather','units');")); + oappend(SET_F("addOption(dd,'Kelvin',0);")); + oappend(SET_F("addOption(dd,'Celsius',1);")); + oappend(SET_F("addOption(dd,'Fahrenheit',2);")); + oappend(SET_F("addInfo('Weather:units',1,'Set time and location in time settings');")); + oappend(SET_F("addInfo('Weather:apiKey',1,'Create account on openweathermap.org and copy the key');")); + oappend(SET_F("addInfo('Weather:minTemp',1,'Changing values: Reboot to (re)load forecast');")); + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + } + + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_WEATHER; + } +}; + +// example openweathermap data +// {"cod":"200","message":0,"cnt":40,"list":[ +// {"dt":1663945200,"main":{"temp":18.05,"feels_like":17.79,"temp_min":17.64,"temp_max":18.05,"pressure":1014,"sea_level":1014,"grnd_level":1013,"humidity":72,"temp_kf":0.41},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":4.32,"deg":238,"gust":5.6},"visibility":10000,"pop":0.48,"rain":{"3h":0.15},"sys":{"pod":"d"},"dt_txt":"2022-09-23 15:00:00"}, +// {"dt":1663956000,"main":{"temp":16.86,"feels_like":16.59,"temp_min":16.16,"temp_max":16.86,"pressure":1014,"sea_level":1014,"grnd_level":1013,"humidity":76,"temp_kf":0.7},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":5.02,"deg":232,"gust":6.1},"visibility":10000,"pop":0.51,"rain":{"3h":0.33},"sys":{"pod":"n"},"dt_txt":"2022-09-23 18:00:00"}, +// {"dt":1663966800,"main":{"temp":15.66,"feels_like":15.47,"temp_min":15.66,"temp_max":15.66,"pressure":1013,"sea_level":1013,"grnd_level":1013,"humidity":84,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":5.51,"deg":243,"gust":7.66},"visibility":10000,"pop":0.92,"rain":{"3h":0.86},"sys":{"pod":"n"},"dt_txt":"2022-09-23 21:00:00"}, +// {"dt":1663977600,"main":{"temp":15.31,"feels_like":15.14,"temp_min":15.31,"temp_max":15.31,"pressure":1012,"sea_level":1012,"grnd_level":1012,"humidity":86,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":5.65,"deg":243,"gust":8.67},"visibility":10000,"pop":0.96,"rain":{"3h":2.48},"sys":{"pod":"n"},"dt_txt":"2022-09-24 00:00:00"}, +// {"dt":1663988400,"main":{"temp":14.86,"feels_like":14.7,"temp_min":14.86,"temp_max":14.86,"pressure":1011,"sea_level":1011,"grnd_level":1011,"humidity":88,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":6.3,"deg":238,"gust":8.78},"visibility":10000,"pop":0.94,"rain":{"3h":1.63},"sys":{"pod":"n"},"dt_txt":"2022-09-24 03:00:00"}, +// {"dt":1663999200,"main":{"temp":14.65,"feels_like":14.49,"temp_min":14.65,"temp_max":14.65,"pressure":1011,"sea_level":1011,"grnd_level":1011,"humidity":89,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":4.36,"deg":238,"gust":6.28},"visibility":10000,"pop":1,"rain":{"3h":2.17},"sys":{"pod":"d"},"dt_txt":"2022-09-24 06:00:00"}, +// {"dt":1664010000,"main":{"temp":14.41,"feels_like":14.31,"temp_min":14.41,"temp_max":14.41,"pressure":1012,"sea_level":1012,"grnd_level":1011,"humidity":92,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":3.03,"deg":198,"gust":5.32},"visibility":10000,"pop":0.88,"rain":{"3h":3.34},"sys":{"pod":"d"},"dt_txt":"2022-09-24 09:00:00"}, +// {"dt":1664020800,"main":{"temp":14.72,"feels_like":14.57,"temp_min":14.72,"temp_max":14.72,"pressure":1013,"sea_level":1013,"grnd_level":1012,"humidity":89,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":1.66,"deg":157,"gust":2.76},"visibility":10000,"pop":0.87,"rain":{"3h":1.4},"sys":{"pod":"d"},"dt_txt":"2022-09-24 12:00:00"}, +// {"dt":1664031600,"main":{"temp":14.61,"feels_like":14.45,"temp_min":14.61,"temp_max":14.61,"pressure":1013,"sea_level":1013,"grnd_level":1012,"humidity":89,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":4.64,"deg":30,"gust":6.18},"visibility":10000,"pop":0.76,"rain":{"3h":3.51},"sys":{"pod":"d"},"dt_txt":"2022-09-24 15:00:00"}, +// {"dt":1664042400,"main":{"temp":14.33,"feels_like":14.17,"temp_min":14.33,"temp_max":14.33,"pressure":1014,"sea_level":1014,"grnd_level":1013,"humidity":90,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":7.37,"deg":46,"gust":9.91},"visibility":10000,"pop":0.81,"rain":{"3h":2.46},"sys":{"pod":"n"},"dt_txt":"2022-09-24 18:00:00"}, +// {"dt":1664053200,"main":{"temp":13.81,"feels_like":13.47,"temp_min":13.81,"temp_max":13.81,"pressure":1015,"sea_level":1015,"grnd_level":1015,"humidity":85,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":5.62,"deg":53,"gust":9.48},"visibility":10000,"pop":0.53,"rain":{"3h":2.09},"sys":{"pod":"n"},"dt_txt":"2022-09-24 21:00:00"}, +// {"dt":1664064000,"main":{"temp":13.69,"feels_like":13.26,"temp_min":13.69,"temp_max":13.69,"pressure":1016,"sea_level":1016,"grnd_level":1015,"humidity":82,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":4.31,"deg":48,"gust":8.01},"visibility":10000,"pop":0.53,"rain":{"3h":0.23},"sys":{"pod":"n"},"dt_txt":"2022-09-25 00:00:00"}, +// {"dt":1664074800,"main":{"temp":13.68,"feels_like":13.14,"temp_min":13.68,"temp_max":13.68,"pressure":1016,"sea_level":1016,"grnd_level":1015,"humidity":78,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":4.7,"deg":45,"gust":7.58},"visibility":10000,"pop":0.69,"rain":{"3h":0.45},"sys":{"pod":"n"},"dt_txt":"2022-09-25 03:00:00"}, +// {"dt":1664085600,"main":{"temp":12.96,"feels_like":12.4,"temp_min":12.96,"temp_max":12.96,"pressure":1016,"sea_level":1016,"grnd_level":1015,"humidity":80,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":4.29,"deg":54,"gust":7.3},"visibility":10000,"pop":0.77,"rain":{"3h":2.4},"sys":{"pod":"d"},"dt_txt":"2022-09-25 06:00:00"}, +// {"dt":1664096400,"main":{"temp":14.14,"feels_like":13.44,"temp_min":14.14,"temp_max":14.14,"pressure":1017,"sea_level":1017,"grnd_level":1016,"humidity":70,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":77},"wind":{"speed":3.09,"deg":72,"gust":4.35},"visibility":10000,"pop":0.54,"rain":{"3h":0.34},"sys":{"pod":"d"},"dt_txt":"2022-09-25 09:00:00"}, +// {"dt":1664107200,"main":{"temp":16.15,"feels_like":15.15,"temp_min":16.15,"temp_max":16.15,"pressure":1016,"sea_level":1016,"grnd_level":1015,"humidity":51,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":47},"wind":{"speed":3.47,"deg":4,"gust":3.49},"visibility":10000,"pop":0.36,"sys":{"pod":"d"},"dt_txt":"2022-09-25 12:00:00"}, +// {"dt":1664118000,"main":{"temp":15.56,"feels_like":14.56,"temp_min":15.56,"temp_max":15.56,"pressure":1015,"sea_level":1015,"grnd_level":1014,"humidity":53,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":35},"wind":{"speed":3.39,"deg":357,"gust":2.98},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2022-09-25 15:00:00"}, +// {"dt":1664128800,"main":{"temp":13.95,"feels_like":12.94,"temp_min":13.95,"temp_max":13.95,"pressure":1014,"sea_level":1014,"grnd_level":1013,"humidity":59,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":21},"wind":{"speed":1.8,"deg":346,"gust":2.3},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2022-09-25 18:00:00"}, +// {"dt":1664139600,"main":{"temp":13.57,"feels_like":12.63,"temp_min":13.57,"temp_max":13.57,"pressure":1013,"sea_level":1013,"grnd_level":1012,"humidity":63,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"clouds":{"all":82},"wind":{"speed":2.6,"deg":267,"gust":3.43},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2022-09-25 21:00:00"}, +// {"dt":1664150400,"main":{"temp":13.93,"feels_like":13.02,"temp_min":13.93,"temp_max":13.93,"pressure":1011,"sea_level":1011,"grnd_level":1010,"humidity":63,"temp_kf":0},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"clouds":{"all":91},"wind":{"speed":4.77,"deg":236,"gust":7.03},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2022-09-26 00:00:00"}, +// {"dt":1664161200,"main":{"temp":14.53,"feels_like":13.79,"temp_min":14.53,"temp_max":14.53,"pressure":1007,"sea_level":1007,"grnd_level":1006,"humidity":67,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":7.51,"deg":234,"gust":10.73},"visibility":10000,"pop":0.2,"rain":{"3h":0.13},"sys":{"pod":"n"},"dt_txt":"2022-09-26 03:00:00"}, +// {"dt":1664172000,"main":{"temp":14.62,"feels_like":13.73,"temp_min":14.62,"temp_max":14.62,"pressure":1004,"sea_level":1004,"grnd_level":1003,"humidity":61,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":10.16,"deg":236,"gust":13.08},"visibility":10000,"pop":0.2,"rain":{"3h":0.15},"sys":{"pod":"d"},"dt_txt":"2022-09-26 06:00:00"}, +// {"dt":1664182800,"main":{"temp":14.24,"feels_like":13.68,"temp_min":14.24,"temp_max":14.24,"pressure":1002,"sea_level":1002,"grnd_level":1001,"humidity":75,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":10.43,"deg":232,"gust":13.98},"visibility":10000,"pop":0.41,"rain":{"3h":0.29},"sys":{"pod":"d"},"dt_txt":"2022-09-26 09:00:00"}, +// {"dt":1664193600,"main":{"temp":13.58,"feels_like":13.29,"temp_min":13.58,"temp_max":13.58,"pressure":999,"sea_level":999,"grnd_level":998,"humidity":88,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":7.48,"deg":252,"gust":10.89},"visibility":7736,"pop":0.9,"rain":{"3h":3.95},"sys":{"pod":"d"},"dt_txt":"2022-09-26 12:00:00"}, +// {"dt":1664204400,"main":{"temp":13.65,"feels_like":13.11,"temp_min":13.65,"temp_max":13.65,"pressure":998,"sea_level":998,"grnd_level":998,"humidity":78,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":4.63,"deg":288,"gust":6.33},"visibility":10000,"pop":0.93,"rain":{"3h":4.13},"sys":{"pod":"d"},"dt_txt":"2022-09-26 15:00:00"}, +// {"dt":1664215200,"main":{"temp":12.79,"feels_like":12.11,"temp_min":12.79,"temp_max":12.79,"pressure":999,"sea_level":999,"grnd_level":998,"humidity":76,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":84},"wind":{"speed":7.52,"deg":334,"gust":9.93},"visibility":10000,"pop":1,"rain":{"3h":1.18},"sys":{"pod":"n"},"dt_txt":"2022-09-26 18:00:00"}, +// {"dt":1664226000,"main":{"temp":12.75,"feels_like":11.88,"temp_min":12.75,"temp_max":12.75,"pressure":1000,"sea_level":1000,"grnd_level":999,"humidity":69,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":81},"wind":{"speed":8.6,"deg":330,"gust":10.65},"visibility":10000,"pop":0.95,"rain":{"3h":0.94},"sys":{"pod":"n"},"dt_txt":"2022-09-26 21:00:00"}, +// {"dt":1664236800,"main":{"temp":12.4,"feels_like":11.45,"temp_min":12.4,"temp_max":12.4,"pressure":1000,"sea_level":1000,"grnd_level":999,"humidity":67,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":90},"wind":{"speed":7.24,"deg":313,"gust":9.58},"visibility":10000,"pop":0.95,"rain":{"3h":0.66},"sys":{"pod":"n"},"dt_txt":"2022-09-27 00:00:00"}, +// {"dt":1664247600,"main":{"temp":12.05,"feels_like":11.19,"temp_min":12.05,"temp_max":12.05,"pressure":1000,"sea_level":1000,"grnd_level":999,"humidity":72,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":100},"wind":{"speed":7.56,"deg":310,"gust":9.89},"visibility":10000,"pop":1,"rain":{"3h":1.31},"sys":{"pod":"n"},"dt_txt":"2022-09-27 03:00:00"}, +// {"dt":1664258400,"main":{"temp":12.03,"feels_like":11.01,"temp_min":12.03,"temp_max":12.03,"pressure":999,"sea_level":999,"grnd_level":999,"humidity":66,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":9.24,"deg":284,"gust":11.35},"visibility":10000,"pop":1,"rain":{"3h":1.58},"sys":{"pod":"d"},"dt_txt":"2022-09-27 06:00:00"}, +// {"dt":1664269200,"main":{"temp":12.3,"feels_like":11.31,"temp_min":12.3,"temp_max":12.3,"pressure":999,"sea_level":999,"grnd_level":999,"humidity":66,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":70},"wind":{"speed":9.89,"deg":272,"gust":11.4},"visibility":10000,"pop":0.51,"rain":{"3h":0.38},"sys":{"pod":"d"},"dt_txt":"2022-09-27 09:00:00"}, +// {"dt":1664280000,"main":{"temp":12.97,"feels_like":12.12,"temp_min":12.97,"temp_max":12.97,"pressure":998,"sea_level":998,"grnd_level":997,"humidity":69,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":84},"wind":{"speed":9.12,"deg":263,"gust":12.4},"visibility":10000,"pop":0.95,"rain":{"3h":1.08},"sys":{"pod":"d"},"dt_txt":"2022-09-27 12:00:00"}, +// {"dt":1664290800,"main":{"temp":13.1,"feels_like":12.32,"temp_min":13.1,"temp_max":13.1,"pressure":996,"sea_level":996,"grnd_level":995,"humidity":71,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":10.39,"deg":274,"gust":12.96},"visibility":10000,"pop":1,"rain":{"3h":1.97},"sys":{"pod":"d"},"dt_txt":"2022-09-27 15:00:00"}, +// {"dt":1664301600,"main":{"temp":12.72,"feels_like":11.82,"temp_min":12.72,"temp_max":12.72,"pressure":996,"sea_level":996,"grnd_level":995,"humidity":68,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":99},"wind":{"speed":8.59,"deg":291,"gust":11.52},"visibility":10000,"pop":1,"rain":{"3h":1.59},"sys":{"pod":"n"},"dt_txt":"2022-09-27 18:00:00"}, +// {"dt":1664312400,"main":{"temp":12.27,"feels_like":11.41,"temp_min":12.27,"temp_max":12.27,"pressure":995,"sea_level":995,"grnd_level":995,"humidity":71,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":98},"wind":{"speed":8.22,"deg":279,"gust":10.74},"visibility":10000,"pop":0.89,"rain":{"3h":1.06},"sys":{"pod":"n"},"dt_txt":"2022-09-27 21:00:00"}, +// {"dt":1664323200,"main":{"temp":12.22,"feels_like":11.4,"temp_min":12.22,"temp_max":12.22,"pressure":994,"sea_level":994,"grnd_level":993,"humidity":73,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":92},"wind":{"speed":8.28,"deg":277,"gust":11.01},"visibility":10000,"pop":1,"rain":{"3h":1.02},"sys":{"pod":"n"},"dt_txt":"2022-09-28 00:00:00"}, +// {"dt":1664334000,"main":{"temp":12.36,"feels_like":11.48,"temp_min":12.36,"temp_max":12.36,"pressure":993,"sea_level":993,"grnd_level":992,"humidity":70,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":87},"wind":{"speed":8.64,"deg":276,"gust":11.11},"visibility":10000,"pop":0.92,"rain":{"3h":0.64},"sys":{"pod":"n"},"dt_txt":"2022-09-28 03:00:00"}, +// {"dt":1664344800,"main":{"temp":12.54,"feels_like":11.63,"temp_min":12.54,"temp_max":12.54,"pressure":993,"sea_level":993,"grnd_level":992,"humidity":68,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":93},"wind":{"speed":8.94,"deg":282,"gust":11.02},"visibility":10000,"pop":0.88,"rain":{"3h":0.62},"sys":{"pod":"d"},"dt_txt":"2022-09-28 06:00:00"}, +// {"dt":1664355600,"main":{"temp":12.76,"feels_like":11.95,"temp_min":12.76,"temp_max":12.76,"pressure":993,"sea_level":993,"grnd_level":992,"humidity":71,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":10.07,"deg":281,"gust":12.33},"visibility":10000,"pop":0.75,"rain":{"3h":1.12},"sys":{"pod":"d"},"dt_txt":"2022-09-28 09:00:00"}, +// {"dt":1664366400,"main":{"temp":13.41,"feels_like":12.66,"temp_min":13.41,"temp_max":13.41,"pressure":994,"sea_level":994,"grnd_level":993,"humidity":71,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":10.01,"deg":297,"gust":12.82},"visibility":9744,"pop":0.95,"rain":{"3h":2.77},"sys":{"pod":"d"},"dt_txt":"2022-09-28 12:00:00"} +// ],"city":{"id":2747373,"name":"The Hague","coord":{"lat":52.08,"lon":4.28},"country":"NL","population":474292,"timezone":7200,"sunrise":1663911010,"sunset":1663954830}} \ No newline at end of file diff --git a/usermods/usermod_v2_word_clock/readme.md b/usermods/usermod_v2_word_clock/readme.md index 1dde2223c2..c42ee0ee47 100644 --- a/usermods/usermod_v2_word_clock/readme.md +++ b/usermods/usermod_v2_word_clock/readme.md @@ -8,7 +8,7 @@ active: enable/disable usermod diplayItIs: enable/disable display of "Es ist" on the clock ledOffset: number of LEDs before the wordclock LEDs -### Update for alternatative wiring pattern +### Update for alternative wiring pattern Based on this fantastic work I added an alternative wiring pattern. The original used a long wire to connect DO to DI, from one line to the next line. diff --git a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h index 058b8318b3..b66be290a5 100644 --- a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h +++ b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h @@ -7,8 +7,8 @@ * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality * * This usermod can be used to drive a wordclock with a 11x10 pixel matrix with WLED. There are also 4 additional dots for the minutes. - * The visualisation is desribed in 4 mask with LED numbers (single dots for minutes, minutes, hours and "clock/Uhr"). - * There are 2 parameters to chnage the behaviour: + * The visualisation is described in 4 mask with LED numbers (single dots for minutes, minutes, hours and "clock/Uhr"). + * There are 2 parameters to change the behaviour: * * active: enable/disable usermod * diplayItIs: enable/disable display of "Es ist" on the clock. diff --git a/usermods/wizlights/readme.md b/usermods/wizlights/readme.md index a0e0a8b8f1..9e633043bf 100644 --- a/usermods/wizlights/readme.md +++ b/usermods/wizlights/readme.md @@ -1,6 +1,6 @@ # Controlling Wiz lights -Enabless controlling [WiZ](https://www.wizconnected.com/en/consumer/) lights that are part of the same network as the WLED controller. +Enables controlling [WiZ](https://www.wizconnected.com/en/consumer/) lights that are part of the same network as the WLED controller. The mod takes the colors from the first few pixels and sends them to the lights. @@ -8,7 +8,7 @@ The mod takes the colors from the first few pixels and sends them to the lights. - Interval (ms) - How frequently to update the WiZ lights, in milliseconds. - - Setting it too low may causse the ESP to become unresponsive. + - Setting it too low may cause the ESP to become unresponsive. - Send Delay (ms) - An optional millisecond delay after updating each WiZ light. - Can help smooth out effects when using a large number of WiZ lights diff --git a/wled00.sln b/wled00.sln deleted file mode 100644 index b2f12b8359..0000000000 --- a/wled00.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28010.2046 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wled00", "wled00\wled00.vcxproj", "{C5F80730-F44F-4478-BDAE-6634EFC2CA88}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Debug|x86.ActiveCfg = Debug|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Debug|x86.Build.0 = Debug|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Release|x86.ActiveCfg = Release|Win32 - {C5F80730-F44F-4478-BDAE-6634EFC2CA88}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9A679C2B-61D3-400B-B96F-06E604E9CED2} - EndGlobalSection -EndGlobal diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6428c8a0b6..04a77701f2 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1,26 +1,8 @@ +/* Some portions of this code have other licenses, like GEQ 3D. Please review fully. */ /* WS2812FX.cpp contains all effect methods Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) - Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - Modified heavily for WLED */ @@ -28,6 +10,13 @@ #include "FX.h" #include "fcn_declare.h" +#ifdef WLEDMM_FASTPATH +#undef SEGMENT +#undef SEGENV +#define SEGMENT (*strip._currentSeg) // saves us many calls to strip._segments[strip.getCurrSegmentId()] +#define SEGENV SEGMENT +#endif + #define IBN 5100 // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) @@ -36,25 +25,38 @@ #define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) +#if 0 // for benchmarking - change to "#if 1" to use less accurate, but 30% faster FastLed sin8 and cos8 functions +#define sin8_t sin8 +#define cos8_t cos8 +#define sin16_t sin16 +#define cos16_t cos16 +#define beatsin8_t beatsin8 +#define beatsin88_t beatsin88 +#define beatsin16_t beatsin16 +#endif + +// WLEDMM replace abs8 by abs, as abs8 does not work for numbers >127 +#define abs8(x) abs(x) + // effect utility functions -uint8_t sin_gap(uint16_t in) { +static uint8_t sin_gap(uint16_t in) { if (in & 0x100) return 0; - return sin8(in + 192); // correct phase shift of sine so that it starts and stops at 0 + return sin8_t(in + 192); // correct phase shift of sine so that it starts and stops at 0 } -uint16_t triwave16(uint16_t in) { +static uint16_t triwave16(uint16_t in) { if (in < 0x8000) return in *2; return 0xFFFF - (in - 0x8000)*2; } /* - * Generates a tristate square wave w/ attac & decay + * Generates a tristate square wave w/ attack & decay * @param x input value 0-255 * @param pulsewidth 0-127 - * @param attdec attac & decay, max. pulsewidth / 2 + * @param attdec attack & decay, max. pulsewidth / 2 * @returns signed waveform value */ -int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { +static int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { int8_t a = 127; if (x > 127) { a = -127; @@ -73,6 +75,38 @@ int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { return 0; } +// float version of map() // WLEDMM moved here so it is available for all effects +static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ + if (in_max == in_min) return (out_min); // WLEDMM avoid div/0 + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +// more accurate integer version of map() - based on map3() proposed in https://forum.arduino.cc/t/how-map-loses-precision-and-how-to-fix-it/371026/3 +// rounding instead of truncation, better handling of inverted ranges +static long map2(long x, long in_min, long in_max, long out_min, long out_max) +{ + long out_range = out_max - out_min; + if (out_range > 0) out_range ++; + else if (out_range < 0) out_range --; + else return out_min; // output range is 0 + + long in_range = in_max - in_min; + if (in_range > 0) in_range++; + else if (in_range < 0) in_range --; + else return out_min; // input range is 0 - Result is actually infinity but long has no such thing. The least negative long is another choice. + + return ((x - in_min) * out_range) / in_range + out_min; +} + + +static um_data_t* getAudioData() { + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + return um_data; +} // effect functions /* @@ -80,7 +114,7 @@ int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { */ uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); - return 350; + return strip.isOffRefreshRequired() ? FRAMETIME : FRAMETIME_FIXED_SLOW; // WLEDMM to ensure smooth color changes from DMX (PR #73) } static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; @@ -278,7 +312,7 @@ uint16_t mode_random_color(void) { SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), fade)); return FRAMETIME; } -static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!"; +static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!;01"; /* @@ -289,8 +323,8 @@ uint16_t mode_dynamic(void) { if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed if(SEGENV.call == 0) { - //SEGMENT.setUpLeds(); //lossless getPixelColor() - //SEGMENT.fill(BLACK); + SEGMENT.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); for (int i = 0; i < SEGLEN; i++) SEGENV.data[i] = random8(); } @@ -304,7 +338,7 @@ uint16_t mode_dynamic(void) { SEGENV.step = it; } - if (SEGMENT.check1) { + if ((SEGMENT.check1) && (SEGENV.call > 1)) { for (int i = 0; i < SEGLEN; i++) { SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16); } @@ -340,7 +374,7 @@ uint16_t mode_breath(void) { counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048 if (counter < 16384) { if (counter > 8192) counter = 8192 - (counter - 8192); - var = sin16(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 + var = sin16_t(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } uint8_t lum = 30 + var; @@ -433,7 +467,7 @@ uint16_t mode_rainbow(void) { return FRAMETIME; } -static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!"; +static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;01"; /* @@ -522,7 +556,7 @@ uint16_t running_base(bool saw, bool dual=false) { } a = 255 - a; } - uint8_t s = dual ? sin_gap(a) : sin8(a); + uint8_t s = dual ? sin_gap(a) : sin8_t(a); uint32_t ca = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s); if (dual) { uint16_t b = (SEGLEN-1-i)*x_scale - counter; @@ -715,7 +749,7 @@ uint16_t mode_hyper_sparkle(void) { if (strip.now - SEGENV.aux0 > SEGENV.step) { if (random8((255-SEGMENT.intensity) >> 4) == 0) { - for (int i = 0; i < MAX(1, SEGLEN/3); i++) { + for (int i = 0; i < max(1, SEGLEN/3); i++) { SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); } } @@ -761,7 +795,7 @@ static const char _data_FX_MODE_MULTI_STROBE[] PROGMEM = "Strobe Mega@!,!;!,!;!; * Android loading circle */ uint16_t mode_android(void) { - + if (SEGLEN <= 1) return mode_static(); // WLEDMM to prevent division by zero for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } @@ -1009,7 +1043,7 @@ static const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = "Traffic Light@!,US st */ #define FLASH_COUNT 4 uint16_t mode_chase_flash(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); uint8_t flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGLEN; i++) { @@ -1093,7 +1127,7 @@ uint16_t mode_running_random(void) { uint8_t z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (int i=SEGLEN-1; i > 0; i--) { + for (int i=SEGLEN-1; i >= 0; i--) { // WLEDMM bugfix if (nzone || z >= zoneSize) { uint8_t lastrand = PRNG16 >> 8; int16_t diff = 0; @@ -1114,12 +1148,13 @@ uint16_t mode_running_random(void) { SEGENV.aux1 = it; return FRAMETIME; } -static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;;!"; +static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream ☾@!,Zone size;;!"; uint16_t larson_scanner(bool dual) { uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t index = counter * SEGLEN >> 16; + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() SEGMENT.fade_out(SEGMENT.intensity); @@ -1176,7 +1211,11 @@ uint16_t mode_comet(void) { if (SEGLEN == 1) return mode_static(); uint16_t counter = strip.now * ((SEGMENT.speed >>2) +1); uint16_t index = (counter * SEGLEN) >> 16; - if (SEGENV.call == 0) SEGENV.aux0 = index; + if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + SEGENV.aux0 = index; + } SEGMENT.fade_out(SEGMENT.intensity); @@ -1200,7 +1239,7 @@ static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!" /* * Fireworks function. */ -uint16_t mode_fireworks() { +static uint16_t mode_fireworks_core(bool useaudio) { if (SEGLEN == 1) return mode_static(); const uint16_t width = SEGMENT.is2D() ? SEGMENT.virtualWidth() : SEGMENT.virtualLength(); const uint16_t height = SEGMENT.virtualHeight(); @@ -1216,33 +1255,73 @@ uint16_t mode_fireworks() { bool valid1 = (SEGENV.aux0 < width*height); bool valid2 = (SEGENV.aux1 < width*height); uint32_t sv1 = 0, sv2 = 0; + + // WLEDMM begin + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + useaudio = false; // no audio - fallback to standard behaviour (don't use soundSim) + } + bool addPixels = true; // false -> inhibit new pixels in silence + unsigned myIntensity = 129 - (SEGMENT.intensity >> 1); // make parameter explicit, so we can work with it + int soundColor = -1; // -1 = random color; 0..255 = use as palette index + + if (useaudio) { + float volumeSmth = *(float*) um_data->u_data[0]; + float FFT_MajorPeak = *(float*) um_data->u_data[4]; + uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; + if ((volumeSmth > 1.0f) && (FFT_MajorPeak > 60.0f)) { // we have sound - select color based on major frequency + float musicIndex = logf(FFT_MajorPeak); // log scaling of peak freq + soundColor = mapf(musicIndex, 4.6f, 9.06f, 0, 255); // pick color from frequency (4.6 = ln(100), 9.06 = ln(8600)) + soundColor = constrain(soundColor, 0, 255); // remove over-shoot + if (samplePeak > 0) myIntensity -= myIntensity / 2; // increase effect intensity at peaks + else if (volumeSmth > 96.0f) myIntensity -= myIntensity / 4; // increase effect intensity slightly when music plays + myIntensity = constrain(myIntensity, 0, 129); + } else { // silence -> fade away + valid1 = valid2 = false; // do not copy last pixels + addPixels = false; // don't add new pixels + } + } + // WLEDMM end + if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(SEGENV.aux0%width, SEGENV.aux0/width) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(SEGENV.aux1%width, SEGENV.aux1/width) : SEGMENT.getPixelColor(SEGENV.aux1); if (!SEGENV.step) SEGMENT.blur(16); if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(SEGENV.aux0%width, SEGENV.aux0/width, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(SEGENV.aux1%width, SEGENV.aux1/width, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur - for (int i=0; i> 1)) == 0) { + #if defined(ARDUINO_ARCH_ESP32) + random16_add_entropy(esp_random() & 0xFFFF); // improve randomness (esp32) + #endif + if (addPixels) // WLEDMM + for (int i=0; i 0) ? soundColor + random8(24) : random8(), false, false, 0); // WLEDMM if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(j, k, col); else SEGMENT.setPixelColor(index, col); SEGENV.aux1 = SEGENV.aux0; // old spark - SEGENV.aux0 = index; // remember where spark occured + SEGENV.aux0 = index; // remember where spark occurred } } return FRAMETIME; } + +uint16_t mode_fireworks(void) { return mode_fireworks_core(false); } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; +uint16_t mode_fireworks_audio(void) { return mode_fireworks_core(true); } +static const char _data_FX_MODE_FIREWORKS_AR[] PROGMEM = "Fireworks audio ☾@,Frequency;!,!;!;1v,12;ix=192,pal=11"; //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN == 1) return mode_static(); const uint16_t width = SEGMENT.virtualWidth(); const uint16_t height = SEGMENT.virtualHeight(); + if(SEGENV.call == 0) { + SEGMENT.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } SEGENV.step += FRAMETIME; if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { SEGENV.step = 1; @@ -1263,8 +1342,8 @@ uint16_t mode_rain() { SEGENV.aux0++; // increase spark index SEGENV.aux1++; } - if (SEGENV.aux0 == 0) SEGENV.aux0 = UINT16_MAX; // reset previous spark positiom - if (SEGENV.aux1 == 0) SEGENV.aux0 = UINT16_MAX; // reset previous spark positiom + if (SEGENV.aux0 == 0) SEGENV.aux0 = UINT16_MAX; // reset previous spark position + if (SEGENV.aux1 == 0) SEGENV.aux0 = UINT16_MAX; // reset previous spark position if (SEGENV.aux0 >= width*height) SEGENV.aux0 = 0; // ignore if (SEGENV.aux1 >= width*height) SEGENV.aux1 = 0; } @@ -1285,12 +1364,12 @@ uint16_t mode_fire_flicker(void) { byte r = (SEGCOLOR(0) >> 16); byte g = (SEGCOLOR(0) >> 8); byte b = (SEGCOLOR(0) ); - byte lum = (SEGMENT.palette == 0) ? MAX(w, MAX(r, MAX(g, b))) : 255; + byte lum = (SEGMENT.palette == 0) ? max(w, max(r, max(g, b))) : 255; lum /= (((256-SEGMENT.intensity)/16)+1); for (int i = 0; i < SEGLEN; i++) { byte flicker = random8(lum); if (SEGMENT.palette == 0) { - SEGMENT.setPixelColor(i, MAX(r - flicker, 0), MAX(g - flicker, 0), MAX(b - flicker, 0), MAX(w - flicker, 0)); + SEGMENT.setPixelColor(i, max(r - flicker, 0), max(g - flicker, 0), max(b - flicker, 0), max(w - flicker, 0)); } else { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, 255 - flicker)); } @@ -1299,7 +1378,7 @@ uint16_t mode_fire_flicker(void) { SEGENV.step = it; return FRAMETIME; } -static const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = "Fire Flicker@!,!;!;!;01"; +static const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = "Fire Flicker@!,!;!;!;01;pal=0"; //WLEDMM pal=0 /* @@ -1321,7 +1400,7 @@ uint16_t gradient_base(bool loading) { { val = abs(((i>pp) ? p2:pp) -i); } else { - val = MIN(abs(pp-i),MIN(abs(p1-i),abs(p2-i))); + val = min(abs(pp-i), min(abs(p1-i), abs(p2-i))); } val = (brd > val) ? val/brd * 255 : 255; SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), val)); @@ -1677,7 +1756,7 @@ uint16_t mode_tricolor_fade(void) { return FRAMETIME; } -static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; +static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!;01"; /* @@ -1690,6 +1769,7 @@ uint16_t mode_multi_comet(void) { if (SEGENV.step == it) return FRAMETIME; if (!SEGENV.allocateData(sizeof(uint16_t) * 8)) return mode_static(); //allocation failed + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() SEGMENT.fade_out(SEGMENT.intensity); uint16_t* comets = reinterpret_cast(SEGENV.data); @@ -1706,7 +1786,7 @@ uint16_t mode_multi_comet(void) { } comets[i]++; } else { - if(!random(SEGLEN)) { + if(!random16(SEGLEN)) { comets[i] = 0; } } @@ -1717,6 +1797,61 @@ uint16_t mode_multi_comet(void) { } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet"; +// audioreactive multi-comet by @softhack007 +uint16_t mode_multi_comet_ar(void) { + constexpr unsigned MAX_COMETS = 16; // was 8 + uint32_t cycleTime = max(1, int((255 - SEGMENT.speed)/4)); + uint32_t it = strip.now / cycleTime; + if (SEGENV.step == it) return FRAMETIME; // too early + + if (!SEGENV.allocateData(sizeof(uint16_t) * MAX_COMETS)) return mode_static(); //allocation failed + uint16_t* comets = reinterpret_cast(SEGENV.data); + if (SEGENV.call == 0) { // do some initializations + SEGMENT.setUpLeds(); SEGMENT.fill(BLACK); + for(uint8_t i=0; i < MAX_COMETS; i++) comets[i] = SEGLEN; // WLEDMM make sure comments are started individually + SEGENV.aux0 = 0; + } + SEGMENT.fade_out(254 - SEGMENT.intensity/2); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) um_data = simulateSound(SEGMENT.soundSim); + float volumeSmth = *(float*) um_data->u_data[0]; + int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; + uint8_t samplePeak = *(uint8_t*) um_data->u_data[3]; + + uint16_t armed = SEGENV.aux0; // allows to delay comet launch + + #if defined(ARDUINO_ARCH_ESP32) + random16_add_entropy(esp_random() & 0xFFFF); // improve randomness (esp32) + #endif + bool shotOne = false; // avoids starting several coments at the same time (invisible due to overlap) + for(unsigned i=0; i < MAX_COMETS; i++) { + if(comets[i] < SEGLEN) { + // draw comet + uint16_t index = comets[i]; + if (SEGCOLOR(2) != 0) + SEGMENT.setPixelColor(index, i % 2 ? SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(2)); + else + SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); + comets[i]++; // move + } else { + // randomly launch a new comet + if (random16(min(uint16_t(256), SEGLEN)) < 3) armed++; // new comet loaded and ready + if (armed > 2) armed = 2; // max three armed at once (avoid overlap) + // delay comet "launch" during silence, and wait until next beat + if ( (armed > 0) && (shotOne == false) + && (volumeSmth > 1.0f) && ((samplePeak > 0) || (volumeRaw > 104)) ) { // delayed lauch - wait until peak, don't launch in silence + comets[i] = 0; // start a new comet! + armed--; // un-arm one + shotOne = true; + } + } + } + SEGENV.aux0 = armed; + SEGENV.step = it; + return FRAMETIME; +} +static const char _data_FX_MODE_MULTI_COMET_AR[] PROGMEM = "Multi Comet audio ☾@Speed,Tail Length;!,!;!;1v;sx=160,ix=32,m12=7,si=1"; // Pinwheel, WeWillRockU /* * Running random pixels ("Stream 2") @@ -1733,7 +1868,7 @@ uint16_t mode_random_chase(void) { uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); - for (int i = SEGLEN -1; i > 0; i--) { + for (int i = SEGLEN -1; i >= 0; i--) { // WLEDMM bugfix uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8(); uint8_t g = random8(6) != 0 ? (color >> 8 & 0xFF) : random8(); uint8_t b = random8(6) != 0 ? (color & 0xFF) : random8(); @@ -1750,7 +1885,7 @@ uint16_t mode_random_chase(void) { random16_set_seed(prevSeed); // restore original seed so other effects can use "random" PRNG return FRAMETIME; } -static const char _data_FX_MODE_RANDOM_CHASE[] PROGMEM = "Stream 2@!;;"; +static const char _data_FX_MODE_RANDOM_CHASE[] PROGMEM = "Stream 2 ☾@!;;"; //7 bytes @@ -1840,10 +1975,10 @@ uint16_t mode_lightning(void) { } SEGENV.aux1--; - SEGENV.step = millis(); + SEGENV.step = strip.now; //return random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds } else { - if (millis() - SEGENV.step > SEGENV.aux0) { + if (strip.now - SEGENV.step > SEGENV.aux0) { SEGENV.aux1--; if (SEGENV.aux1 < 2) SEGENV.aux1 = 0; @@ -1851,7 +1986,7 @@ uint16_t mode_lightning(void) { if (SEGENV.aux1 == 2) { SEGENV.aux0 = (random8(255 - SEGMENT.speed) * 100); // delay between strikes } - SEGENV.step = millis(); + SEGENV.step = strip.now; } } return FRAMETIME; @@ -1867,24 +2002,28 @@ uint16_t mode_pride_2015(void) { uint16_t sPseudotime = SEGENV.step; uint16_t sHue16 = SEGENV.aux0; - uint8_t sat8 = beatsin88( 87, 220, 250); - uint8_t brightdepth = beatsin88( 341, 96, 224); - uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); - uint8_t msmultiplier = beatsin88(147, 23, 60); + uint8_t sat8 = beatsin88_t( 87, 220, 250); + uint8_t brightdepth = beatsin88_t( 341, 96, 224); + uint16_t brightnessthetainc16 = beatsin88_t( 203, (25 * 256), (40 * 256)); + uint8_t msmultiplier = beatsin88_t(147, 23, 60); uint16_t hue16 = sHue16;//gHue * 256; - uint16_t hueinc16 = beatsin88(113, 1, 3000); + uint16_t hueinc16 = beatsin88_t(113, 1, 3000); + + if(SEGENV.call == 0) { + SEGMENT.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } sPseudotime += duration * msmultiplier; - sHue16 += duration * beatsin88( 400, 5,9); + sHue16 += duration * beatsin88_t( 400, 5,9); uint16_t brightnesstheta16 = sPseudotime; - for (int i = 0 ; i < SEGLEN; i++) { hue16 += hueinc16; uint8_t hue8 = hue16 >> 8; brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16( brightnesstheta16 ) + 32768; + uint16_t b16 = sin16_t( brightnesstheta16 ) + 32768; uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; @@ -1901,6 +2040,67 @@ uint16_t mode_pride_2015(void) { static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; +////////////////////// +// PARTYJERK // +////////////////////// +// by @tonyxforce +// NB: This effects expects a palette that starts with black and then ramps up brightness. +// Currently works best with the "color gradient" and the "colors 1&2" palettes +uint16_t mode_partyjerk() { + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); // clear LEDs + SEGENV.aux0 = 0; + SEGENV.aux1 = 0; + SEGENV.step = 0; + } + /* + * use of persistent variables: + * aux0: hueDelay + * aux1: hue + * step: pos + */ + + um_data_t *um_data = getAudioData(); + float volumeSmth = *(float*) um_data->u_data[0]; + + SEGENV.aux0++; + if (SEGENV.aux1 > 254) { + SEGENV.aux1 = 0; + } + if (SEGENV.aux0 > map2(SEGMENT.custom1, 0, 255, 0, 14)) { + SEGENV.aux0 = 0; + SEGENV.aux1++; + } + + uint_fast32_t speed = 0; + uint16_t counter = 0; + + if (volumeSmth * 2 > (255 - SEGMENT.intensity)) { + speed = SEGMENT.speed * map2(SEGMENT.custom2, 0, 255, 0, 100); + } else { + speed = SEGMENT.speed; + } + + SEGENV.step += speed; + counter = SEGENV.step >> 8; + + for (unsigned i = 0; i < SEGLEN; i++) { + uint8_t colorIndex = ((i * 255) / SEGLEN) - counter; + uint32_t paletteColor = SEGMENT.color_from_palette(colorIndex, false, PALETTE_MOVING_WRAP, 255); + uint8_t r = R(paletteColor); + uint8_t g = G(paletteColor); + uint8_t b = B(paletteColor); + uint8_t activeColor = max(r, max(g, b)); + + CRGB rgb(CHSV(SEGENV.aux1, 255, activeColor)); + SEGMENT.setPixelColor((uint16_t)i, rgb.r, rgb.g, rgb.b); + } + + return FRAMETIME; +} // mode_partyjerk() +static const char _data_FX_MODE_PARTYJERK[] PROGMEM = "Party jerk@Effect speed,Sensitivity,Color change speed,Effect speed active multiplier;!,!;!;1v;c1=8,c2=48,m12=0,si=0"; + + //eight colored dots, weaving in and out of sync with each other uint16_t mode_juggle(void) { if (SEGLEN == 1) return mode_static(); @@ -1914,7 +2114,7 @@ uint16_t mode_juggle(void) { CRGB fastled_col; byte dothue = 0; for (int i = 0; i < 8; i++) { - uint16_t index = 0 + beatsin88((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); + uint16_t index = 0 + beatsin88_t((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); fastled_col = CRGB(SEGMENT.getPixelColor(index)); fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):ColorFromPalette(SEGPALETTE, dothue, 255); SEGMENT.setPixelColor(index, fastled_col); @@ -1977,6 +2177,7 @@ uint16_t mode_fire_2012() { const uint16_t strips = SEGMENT.nrOfVStrips(); if (!SEGENV.allocateData(strips * SEGLEN)) return mode_static(); //allocation failed byte* heat = SEGENV.data; + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() const uint32_t it = strip.now >> 5; //div 32 @@ -1985,12 +2186,16 @@ uint16_t mode_fire_2012() { const uint8_t ignition = max(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels + #if defined(ARDUINO_ARCH_ESP32) + random16_add_entropy(esp_random() & 0xFFFF); // improves randonmess + #endif + // Step 1. Cool down every cell a little for (int i = 0; i < SEGLEN; i++) { - uint8_t cool = (it != SEGENV.step) ? random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : random(4); + uint8_t cool = (it != SEGENV.step) ? random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : random8(4); uint8_t minTemp = (i> 2; + if (blurAmount > 48) blurAmount += blurAmount-48; // extra blur when slider > 192 (bush burn) + if (blurAmount < 16) SEGMENT.blurCols(SEGMENT.custom2 >> 1); // no side-burn when slider < 64 (faster) + else SEGMENT.blur(blurAmount); + } if (it != SEGENV.step) SEGENV.step = it; return FRAMETIME; } -static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,,Boost;;!;1;sx=64,ix=160,m12=1"; // bars +static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1.5d;sx=64,ix=160,c2=128,m12=1"; // bars WLEDMM 1.5d, // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb @@ -2035,17 +2245,22 @@ uint16_t mode_colorwaves() { uint16_t sPseudotime = SEGENV.step; uint16_t sHue16 = SEGENV.aux0; - uint8_t brightdepth = beatsin88(341, 96, 224); - uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); - uint8_t msmultiplier = beatsin88(147, 23, 60); + uint8_t brightdepth = beatsin88_t(341, 96, 224); + uint16_t brightnessthetainc16 = beatsin88_t( 203, (25 * 256), (40 * 256)); + uint8_t msmultiplier = beatsin88_t(147, 23, 60); uint16_t hue16 = sHue16;//gHue * 256; - uint16_t hueinc16 = beatsin88(113, 60, 300)*SEGMENT.intensity*10/255; // Use the Intensity Slider for the hues + uint16_t hueinc16 = beatsin88_t(113, 60, 300)*SEGMENT.intensity*10/255; // Use the Intensity Slider for the hues sPseudotime += duration * msmultiplier; - sHue16 += duration * beatsin88(400, 5, 9); + sHue16 += duration * beatsin88_t(400, 5, 9); uint16_t brightnesstheta16 = sPseudotime; + if(SEGENV.call == 0) { + SEGMENT.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } + for (int i = 0 ; i < SEGLEN; i++) { hue16 += hueinc16; uint8_t hue8 = hue16 >> 8; @@ -2057,7 +2272,7 @@ uint16_t mode_colorwaves() { } brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16(brightnesstheta16) + 32768; + uint16_t b16 = sin16_t(brightnesstheta16) + 32768; uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; @@ -2077,7 +2292,7 @@ static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!"; uint16_t mode_bpm() { //CRGB fastled_col; uint32_t stp = (strip.now / 20) & 0xFF; - uint8_t beat = beatsin8(SEGMENT.speed, 64, 255); + uint8_t beat = beatsin8_t(SEGMENT.speed, 64, 255); for (int i = 0; i < SEGLEN; i++) { //fastled_col = ColorFromPalette(SEGPALETTE, stp + (i * 2), beat - stp + (i * 10)); //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); @@ -2098,7 +2313,7 @@ uint16_t mode_fillnoise8() { //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } - SEGENV.step += beatsin8(SEGMENT.speed, 1, 6); //10,1,4 + SEGENV.step += beatsin8_t(SEGMENT.speed, 1, 6); //10,1,4 return FRAMETIME; } @@ -2111,13 +2326,13 @@ uint16_t mode_noise16_1() { SEGENV.step += (1 + SEGMENT.speed/16); for (int i = 0; i < SEGLEN; i++) { - uint16_t shift_x = beatsin8(11); // the x position of the noise field swings @ 17 bpm + uint16_t shift_x = beatsin8_t(11); // the x position of the noise field swings @ 17 bpm uint16_t shift_y = SEGENV.step/42; // the y position becomes slowly incremented uint16_t real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm uint16_t real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map LED color based on noise data + uint8_t index = sin8_t(noise * 3); // map LED color based on noise data //fastled_col = ColorFromPalette(SEGPALETTE, index, 255, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); @@ -2138,7 +2353,7 @@ uint16_t mode_noise16_2() { uint16_t shift_x = SEGENV.step >> 6; // x as a function of time uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint8_t noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map led color based on noise data + uint8_t index = sin8_t(noise * 3); // map led color based on noise data //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); @@ -2162,7 +2377,7 @@ uint16_t mode_noise16_3() { uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions uint32_t real_z = SEGENV.step*8; uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map led color based on noise data + uint8_t index = sin8_t(noise * 3); // map led color based on noise data //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); @@ -2193,6 +2408,10 @@ static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!"; uint16_t mode_colortwinkle() { uint16_t dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if(SEGENV.call == 0) { + SEGMENT.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } CRGB fastled_col, prev; fract8 fadeUpAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>2) : 68-strip.getBrightness(); @@ -2247,14 +2466,14 @@ static const char _data_FX_MODE_COLORTWINKLE[] PROGMEM = "Colortwinkles@Fade spe //Calm effect, like a lake at night uint16_t mode_lake() { uint8_t sp = SEGMENT.speed/10; - int wave1 = beatsin8(sp +2, -64,64); - int wave2 = beatsin8(sp +1, -64,64); - uint8_t wave3 = beatsin8(sp +2, 0,80); + int wave1 = beatsin8_t(sp +2, -64,64); + int wave2 = beatsin8_t(sp +1, -64,64); + uint8_t wave3 = beatsin8_t(sp +2, 0,80); //CRGB fastled_col; for (int i = 0; i < SEGLEN; i++) { - int index = cos8((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; + int index = cos8_t((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; uint8_t lum = (index > wave3) ? index - wave3 : 0; //fastled_col = ColorFromPalette(SEGPALETTE, map(index,0,255,0,240), lum, LINEARBLEND); //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); @@ -2275,33 +2494,35 @@ uint16_t mode_meteor() { byte* trail = SEGENV.data; - byte meteorSize= 1+ SEGLEN / 10; + const unsigned meteorSize= 1 + SEGLEN / 20; // 5% uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t in = counter * SEGLEN >> 16; + const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step for (int i = 0; i < SEGLEN; i++) { - if (random8() <= 255 - SEGMENT.intensity) - { - byte meteorTrailDecay = 128 + random8(127); + if (random8() <= 255 - SEGMENT.intensity) { + byte meteorTrailDecay = 162 + random8(92); trail[i] = scale8(trail[i], meteorTrailDecay); - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, false, 0, trail[i])); + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + SEGMENT.setPixelColor(i, col); } } // draw meteor - for (int j = 0; j < meteorSize; j++) { + for (unsigned j = 0; j < meteorSize; j++) { uint16_t index = in + j; if (index >= SEGLEN) { index -= SEGLEN; } - trail[index] = 240; - SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, false, 0, 255)); + trail[index] = max; + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + SEGMENT.setPixelColor(index, col); } return FRAMETIME; } -static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail length;!;!"; +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;;!;1"; // smooth meteor effect @@ -2313,35 +2534,35 @@ uint16_t mode_meteor_smooth() { byte* trail = SEGENV.data; - byte meteorSize= 1+ SEGLEN / 10; + const unsigned meteorSize= 1+ SEGLEN / 20; // 5% uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); + const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step for (int i = 0; i < SEGLEN; i++) { - if (trail[i] != 0 && random8() <= 255 - SEGMENT.intensity) - { - int change = 3 - random8(12); //change each time between -8 and +3 - trail[i] += change; - if (trail[i] > 245) trail[i] = 0; - if (trail[i] > 240) trail[i] = 240; - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, false, 0, trail[i])); + if (/*trail[i] != 0 &&*/ random8() <= 255 - SEGMENT.intensity) { + int change = trail[i] + 4 - random8(24); //change each time between -20 and +4 + trail[i] = constrain(change, 0, max); + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + SEGMENT.setPixelColor(i, col); } } // draw meteor - for (int j = 0; j < meteorSize; j++) { + for (unsigned j = 0; j < meteorSize; j++) { uint16_t index = in + j; if (index >= SEGLEN) { index -= SEGLEN; } - trail[index] = 240; - SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, false, 0, 255)); + trail[index] = max; + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + SEGMENT.setPixelColor(index, col); } SEGENV.step += SEGMENT.speed +1; return FRAMETIME; } -static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail length;!;!"; +static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail,,,,Gradient;;!;1"; //Railway Crossing / Christmas Fairy lights @@ -2397,7 +2618,15 @@ uint16_t ripple_base() uint16_t maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 uint16_t dataSize = sizeof(ripple) * maxRipples; + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int16_t maxDim = max(2, (cols + rows) / 4); // WLEDMM + #if defined(ARDUINO_ARCH_ESP32) + random16_add_entropy(esp_random() & 0xFFFF); // improve randomness (esp32) + #endif + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() Ripple* ripples = reinterpret_cast(SEGENV.data); @@ -2415,27 +2644,32 @@ uint16_t ripple_base() #ifndef WLED_DISABLE_2D if (SEGMENT.is2D()) { + propI /= 2; uint16_t cx = rippleorigin >> 8; uint16_t cy = rippleorigin & 0xFF; - uint8_t mag = scale8(cubicwave8((propF>>2)), amp); - if (propI > 0) SEGMENT.draw_circle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag)); + uint8_t mag = scale8(sin8_t((propF>>2)), amp); + propI = min(propI, maxDim); // WLEDMM make sure that circles are visible + if ((propI > 0) && (unsigned(cx + propI) < cols) && (unsigned(cy) < rows)) // WLEDMM + SEGMENT.drawCircle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag), true); } else #endif { - int16_t left = rippleorigin - propI -1; - for (int16_t v = left; v < left +4; v++) { + int left = rippleorigin - propI -1; + for (int v = left; v < left +4; v++) { uint8_t mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); - SEGMENT.setPixelColor(v, color_blend(SEGMENT.getPixelColor(v), col, mag)); // TODO - int16_t w = left + propI*2 + 3 -(v-left); - SEGMENT.setPixelColor(w, color_blend(SEGMENT.getPixelColor(w), col, mag)); // TODO + if ((v >= 0) && (v < SEGLEN)) // WLEDMM bugfix: v and w can be negative or out-of-range + SEGMENT.setPixelColor(v, color_blend(SEGMENT.getPixelColor(v), col, mag)); // TODO + int w = left + propI*2 + 3 -(v-left); + if ((w >= 0) && (w < SEGLEN)) // WLEDMM bugfix: v and w can be negative or out-of-range + SEGMENT.setPixelColor(w, color_blend(SEGMENT.getPixelColor(w), col, mag)); // TODO } } ripplestate += rippledecay; ripples[i].state = (ripplestate > 254) ? 0 : ripplestate; } else {//randomly create new wave - if (random16(IBN + 10000) <= SEGMENT.intensity) { + if (random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { ripples[i].state = 1; - ripples[i].pos = SEGMENT.is2D() ? ((random8(SEGENV.virtualWidth())<<8) | (random8(SEGENV.virtualHeight()))) : random16(SEGLEN); + ripples[i].pos = SEGMENT.is2D() ? ((random16(SEGENV.virtualWidth())<<8) | (random16(SEGENV.virtualHeight()))) : random16(SEGLEN); ripples[i].color = random8(); //color } } @@ -2449,6 +2683,7 @@ uint16_t ripple_base() uint16_t mode_ripple(void) { if (SEGLEN == 1) return mode_static(); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); + else SEGMENT.fade_out(250); return ripple_base(); } static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,,,,,Overlay;,!;!;12"; @@ -2489,7 +2724,7 @@ CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) uint16_t ticks = ms / SEGENV.aux0; uint8_t fastcycle8 = ticks; uint16_t slowcycle16 = (ticks >> 8) + salt; - slowcycle16 += sin8(slowcycle16); + slowcycle16 += sin8_t(slowcycle16); slowcycle16 = (slowcycle16 * 2053) + 1384; uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); @@ -2607,14 +2842,14 @@ uint16_t mode_twinklefox() { return twinklefox_base(false); } -static const char _data_FX_MODE_TWINKLEFOX[] PROGMEM = "Twinklefox@!,Twinkle rate;;!"; +static const char _data_FX_MODE_TWINKLEFOX[] PROGMEM = "Twinklefox@!,Twinkle rate;!,!;!"; uint16_t mode_twinklecat() { return twinklefox_base(true); } -static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate;;!"; +static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate;!,!;!"; //inspired by https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectBlinkingHalloweenEyes @@ -2789,7 +3024,7 @@ uint16_t mode_bouncing_balls(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data const uint16_t strips = SEGMENT.nrOfVStrips(); // adapt for 2D - const size_t maxNumBalls = 16; + constexpr size_t maxNumBalls = 16; uint16_t dataSize = sizeof(ball) * maxNumBalls; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed @@ -2807,7 +3042,7 @@ uint16_t mode_bouncing_balls(void) { uint16_t numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball const float gravity = -9.81f; // standard value of gravity const bool hasCol2 = SEGCOLOR(2); - const unsigned long time = millis(); + const unsigned long time = strip.now; if (SEGENV.call == 0) { for (size_t i = 0; i < maxNumBalls; i++) balls[i].lastBounceTime = time; @@ -2835,7 +3070,7 @@ uint16_t mode_bouncing_balls(void) { uint32_t color = SEGCOLOR(0); if (SEGMENT.palette) { - color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8))); + color = SEGMENT.color_wheel(i*(256/max(numBalls, uint16_t(8)))); } else if (hasCol2) { color = SEGCOLOR(i % NUM_COLORS); } @@ -2852,7 +3087,105 @@ uint16_t mode_bouncing_balls(void) { return FRAMETIME; } -static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar +static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1.5d;m12=1"; //bar WLEDMM 1.5d + + +/* + * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls + * Courtesy of pjhatch (https://github.com/pjhatch) + * https://github.com/Aircoookie/WLED/pull/1039 + */ +// modified for balltrack mode +typedef struct RollingBall { + unsigned long lastBounceUpdate; + float mass; // could fix this to be = 1. if memory is an issue + float velocity; + float height; +} rball_t; + +static uint16_t rolling_balls(void) { + //allocate segment data + const uint16_t maxNumBalls = 16; // 255/16 + 1 + uint16_t dataSize = sizeof(rball_t) * maxNumBalls; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + + rball_t *balls = reinterpret_cast(SEGENV.data); + + // number of balls based on intensity setting to max of 16 (cycles colors) + // non-chosen color is a random color + uint8_t numBalls = SEGMENT.intensity/16 + 1; + + if (SEGENV.call == 0) { + for (int i = 0; i < maxNumBalls; i++) { + balls[i].lastBounceUpdate = strip.now; + balls[i].velocity = 20.0f * float(random16(1000, 10000))/10000.0f; // number from 1 to 10 + if (random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction + balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. + balls[i].mass = (float(random16(1000, 10000)) / 10000.0f); // from .1 to 1. + } + } + + float cfac = float(scale8(8, 255-SEGMENT.speed) +1)*20000.0f; // this uses the Aircoookie conversion factor for scaling time using speed slider + + bool hasCol2 = SEGCOLOR(2); + //if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); + if (!SEGMENT.check2) SEGMENT.fade_out(253); // WLEDMM adding a bit of trail + + for (int i = 0; i < numBalls; i++) { + float timeSinceLastUpdate = float((strip.now - balls[i].lastBounceUpdate))/cfac; + float thisHeight = balls[i].height + balls[i].velocity * timeSinceLastUpdate; // this method keeps higher resolution + // test if intensity level was increased and some balls are way off the track then put them back + if (thisHeight < -0.5f || thisHeight > 1.5f){ + thisHeight = balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. + balls[i].lastBounceUpdate = strip.now; + } + // check if reached ends of the strip + if ((thisHeight <= 0.0f && balls[i].velocity < 0.0f) || (thisHeight >= 1.0f && balls[i].velocity > 0.0f)) { + balls[i].velocity = -balls[i].velocity; // reverse velocity + balls[i].lastBounceUpdate = strip.now; + balls[i].height = thisHeight; + } + // check for collisions + if (SEGMENT.check1) { + for (int j = i+1; j < numBalls; j++) { + if (balls[j].velocity != balls[i].velocity) { + // tcollided + balls[j].lastBounceUpdate is actual time of collision (this keeps precision with long to float conversions) + float tcollided = (cfac*(balls[i].height - balls[j].height) + + balls[i].velocity*float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/(balls[j].velocity - balls[i].velocity); + + if ((tcollided > 2.0f) && (tcollided < float(strip.now - balls[j].lastBounceUpdate))) { // 2ms minimum to avoid duplicate bounces + balls[i].height = balls[i].height + balls[i].velocity*(tcollided + float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/cfac; + balls[j].height = balls[i].height; + balls[i].lastBounceUpdate = (unsigned long)(tcollided + 0.5f) + balls[j].lastBounceUpdate; + balls[j].lastBounceUpdate = balls[i].lastBounceUpdate; + float vtmp = balls[i].velocity; + balls[i].velocity = ((balls[i].mass - balls[j].mass)*vtmp + 2.0f*balls[j].mass*balls[j].velocity)/(balls[i].mass + balls[j].mass); + balls[j].velocity = ((balls[j].mass - balls[i].mass)*balls[j].velocity + 2.0f*balls[i].mass*vtmp) /(balls[i].mass + balls[j].mass); + thisHeight = balls[i].height + balls[i].velocity*(strip.now - balls[i].lastBounceUpdate)/cfac; + } + } + } + } + + uint32_t color = SEGCOLOR(0); + if (SEGMENT.palette) { + //color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8))); + color = SEGMENT.color_from_palette(i*255/numBalls, false, PALETTE_SOLID_WRAP, 0); + } else if (hasCol2) { + color = SEGCOLOR(i % NUM_COLORS); + } + + if (thisHeight < 0.0f) thisHeight = 0.0f; + if (thisHeight > 1.0f) thisHeight = 1.0f; + uint16_t pos = roundf(thisHeight * (SEGLEN - 1)); + SEGMENT.setPixelColor(pos, color); + balls[i].lastBounceUpdate = strip.now; + balls[i].height = thisHeight; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay;!,!,!;!;1;m12=1"; //bar /* @@ -2860,8 +3193,9 @@ static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravit */ uint16_t sinelon_base(bool dual, bool rainbow=false) { if (SEGLEN == 1) return mode_static(); + if (SEGENV.call == 0) { SEGENV.setUpLeds(); SEGMENT.fill(BLACK); } // WLEDMM use lossless getPixelColor() SEGMENT.fade_out(SEGMENT.intensity); - uint16_t pos = beatsin16(SEGMENT.speed/10,0,SEGLEN-1); + uint16_t pos = beatsin16_t(SEGMENT.speed/10,0,SEGLEN-1); if (SEGENV.call == 0) SEGENV.aux0 = pos; uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0); uint32_t color2 = SEGCOLOR(2); @@ -2943,7 +3277,7 @@ static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,, //each needs 19 bytes -//Spark type is used for popcorn, 1D fireworks, and drip +//Spark type is used for popcorn, 1D fireworks typedef struct Spark { float pos, posX; float vel, velX; @@ -2956,37 +3290,72 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ -uint16_t mode_popcorn(void) { +static uint16_t mode_popcorn_core(bool useaudio) { if (SEGLEN == 1) return mode_static(); //allocate segment data uint16_t strips = SEGMENT.nrOfVStrips(); - uint16_t dataSize = sizeof(spark) * maxNumPopcorn; + size_t dataSize = sizeof(spark) * maxNumPopcorn; + uint8_t neededPopcorn = maxNumPopcorn; // WLEDMM + if (strips > 8) { // WLEDMM more than 8 virtual strips --> reduce memory requirements to minimum necessary + neededPopcorn = (SEGMENT.intensity*maxNumPopcorn)/255; + neededPopcorn = min(max(neededPopcorn, uint8_t(2)), uint8_t(maxNumPopcorn)); + dataSize = sizeof(spark) * neededPopcorn; + } if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* popcorn = reinterpret_cast(SEGENV.data); + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); // WLEDMM clear LEDs at startup + SEGENV.step = strip.now; // initial time + } + bool hasCol2 = SEGCOLOR(2); if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); + // WLEDMM init um_data + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // no audio - fallback to standard behaviour + useaudio = false; + um_data = simulateSound(SEGMENT.soundSim); // dummy + } + struct virtualStrip { - static void runStrip(uint16_t stripNr, Spark* popcorn) { - float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s - gravity *= SEGLEN; + static void runStrip(uint16_t stripNr, Spark* popcorn, bool useaudio, um_data_t *um_data, float deltaTime) { // WLEDMM added useaudio and um_data + float gravity = -0.0001f - (SEGMENT.speed/180000.0f); // m/s/s // WLEDMM original value was "-0.0001f - (SEGMENT.speed/200000.0f)" + gravity *= min(max(1, SEGLEN-1), 255); // WLEDMM speed limit 255 uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; if (numPopcorn == 0) numPopcorn = 1; + // WLEDMM audioreactive vars + float volumeSmth = *(float*) um_data->u_data[0]; + int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; + uint8_t samplePeak = *(uint8_t*) um_data->u_data[3]; for(int i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position - popcorn[i].pos += popcorn[i].vel; - popcorn[i].vel += gravity; + popcorn[i].pos += popcorn[i].vel * deltaTime; + popcorn[i].vel += gravity * deltaTime; } else { // if kernel is inactive, randomly pop it - if (random8() < 2) { // POP!!! + bool doPopCorn = false; // WLEDMM allows to inhibit new pops + // WLEDMM begin + if (useaudio) { + if ( (volumeSmth > 1.0f) // no pops in silence + &&((samplePeak > 0) || (volumeRaw > 128)) // try to pop at onsets (our peek detector still sucks) + &&(random8() < 4) ) // stay somewhat random + doPopCorn = true; + } else { + if (random8() < 2) doPopCorn = true; // default POP!!! + } + // WLEDMM end + + if (doPopCorn) { // POP!!! popcorn[i].pos = 0.01f; uint16_t peakHeight = 128 + random8(128); //0-255 peakHeight = (peakHeight * (SEGLEN -1)) >> 8; - popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); + popcorn[i].vel = sqrtf(-2.01f * gravity * peakHeight); if (SEGMENT.palette) { @@ -3003,18 +3372,34 @@ uint16_t mode_popcorn(void) { if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); uint16_t ledIndex = popcorn[i].pos; if (ledIndex < SEGLEN) SEGMENT.setPixelColor(indexToVStrip(ledIndex, stripNr), col); + // WLEDMM add small trail + for (int n=1; n<4; n++) { + float spdLimit = n; + unsigned fade = 128 - 32*n; + uint32_t trailColor = color_fade(col, fade, true); + if ((popcorn[i].vel < -spdLimit) && (ledIndex+n < SEGLEN)) SEGMENT.setPixelColor(indexToVStrip(ledIndex+n, stripNr), trailColor); + if ((popcorn[i].vel > spdLimit) && (ledIndex >= n)) SEGMENT.setPixelColor(indexToVStrip(ledIndex-n, stripNr), trailColor); + } } } } }; + // WLEDMM calculate time passed + uint32_t millisPassed = min(max(1U, unsigned(strip.now - SEGENV.step)), 200U); // constrain between 1 and 200 + SEGENV.step = strip.now; + float deltaTime = useaudio ? float(millisPassed) / 8.0f : float(millisPassed) / 16.0f; // base speed: 64 FPS (normal) / 120fps (audioreactive) for (int stripNr=0; stripNr(SEGENV.data); @@ -3152,18 +3537,44 @@ uint16_t mode_starburst(void) { float particleIgnition = 250.0f; // How long to "flash" float particleFadeTime = 1500.0f; // Fade out time + // WLEDMM init um_data + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // no audio - fallback to standard behaviour + useaudio = false; + um_data = simulateSound(SEGMENT.soundSim); // dummy + } + float volumeSmth = *(float*) um_data->u_data[0]; + int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; + uint8_t samplePeak = *(uint8_t*) um_data->u_data[3]; + for (int j = 0; j < numStars; j++) { // speed to adjust chance of a burst, max is nearly always. - if (random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) + bool doNewStar = random8((144-(SEGMENT.speed >> 1))) == 0; // WLEDMM original spawning trigger + // WLEDMM begin + if (useaudio) { + doNewStar = false; + int burstplus = (volumeSmth > 159)? 96:0; // high volume -> more stars + if (volumeRaw <= 56) burstplus = -64; // low volume -> fewer stars + int birthrate = (144-(SEGMENT.speed >> 1)) - burstplus; + birthrate = constrain(birthrate, 4, 144); + if ( (volumeSmth > 1.0f) // no bursts in silence + && ((samplePeak > 0) || (volumeRaw > 48)) // try to burst with sound + && (random8(birthrate) == 0) ) // original random rate + doNewStar = true; + } + // WLEDMM end + + if (doNewStar && stars[j].birth == 0) // WLEDMM { // Pick a random color and location. - uint16_t startPos = random16(SEGLEN-1); + uint16_t startPos = (SEGLEN > 1) ? random16(SEGLEN-1) : 0; float multiplier = (float)(random8())/255.0 * 1.0; stars[j].color = CRGB(SEGMENT.color_wheel(random8())); stars[j].pos = startPos; - stars[j].vel = maxSpeed * (float)(random8())/255.0 * multiplier; + stars[j].vel = maxSpeed * (float)(random8())/255.0f * multiplier; stars[j].birth = it; stars[j].last = it; // more fragments means larger burst effect @@ -3241,8 +3652,12 @@ uint16_t mode_starburst(void) { return FRAMETIME; } #undef STARBURST_MAX_FRAG + +uint16_t mode_starburst(void) { return mode_starburst_core(false); } static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; +uint16_t mode_starburst_audio(void) { return mode_starburst_core(true); } +static const char _data_FX_MODE_STARBURST_AR[] PROGMEM = "Fw Starburst audio ☾@Chance,Fragments,,,,,Overlay;,!;!;1v;pal=11,m12=0"; /* * Exploding fireworks effect @@ -3254,6 +3669,10 @@ uint16_t mode_exploding_fireworks(void) if (SEGLEN == 1) return mode_static(); const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } //allocate segment data uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 @@ -3262,8 +3681,8 @@ uint16_t mode_exploding_fireworks(void) if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs int maxSparks = maxData / sizeof(spark); //ESP8266: max. 21/42/85 sparks/seg, ESP32: max. 53/106/213 sparks/seg - uint16_t numSparks = min(2 + ((rows*cols) >> 1), maxSparks); - uint16_t dataSize = sizeof(spark) * numSparks; + unsigned numSparks = min(5 + ((rows*cols) >> 1), maxSparks); + unsigned dataSize = sizeof(spark) * numSparks; if (!SEGENV.allocateData(dataSize + sizeof(float))) return mode_static(); //allocation failed float *dying_gravity = reinterpret_cast(SEGENV.data + dataSize); @@ -3280,6 +3699,9 @@ uint16_t mode_exploding_fireworks(void) float gravity = -0.0004f - (SEGMENT.speed/800000.0f); // m/s/s gravity *= rows; +#if defined(ARDUINO_ARCH_ESP32) + random16_add_entropy(esp_random() & 0xFFFF); // improves randonmess +#endif if (SEGENV.aux0 < 2) { //FLARE if (SEGENV.aux0 == 0) { //init flare @@ -3301,7 +3723,7 @@ uint16_t mode_exploding_fireworks(void) flare->pos += flare->vel; flare->posX += flare->velX; flare->pos = constrain(flare->pos, 0, rows-1); - flare->posX = constrain(flare->posX, 0, cols-strip.isMatrix); + flare->posX = constrain(flare->posX, 0, cols-int(strip.isMatrix)); flare->vel += gravity; flare->col -= 2; } else { @@ -3314,8 +3736,9 @@ uint16_t mode_exploding_fireworks(void) * Explosion happens where the flare ended. * Size is proportional to the height. */ - int nSparks = flare->pos + random8(4); - nSparks = constrain(nSparks, 4, numSparks); + unsigned nSparks = flare->pos + random8(4); + nSparks = std::max(nSparks, 4U); // This is not a standard constrain; numSparks is not guaranteed to be at least 4 + nSparks = std::min(nSparks, numSparks); // initialize sparks if (SEGENV.aux0 == 2) { @@ -3330,10 +3753,10 @@ uint16_t mode_exploding_fireworks(void) sparks[i].colIndex = random8(); sparks[i].vel *= flare->pos/rows; // proportional to height sparks[i].velX *= strip.isMatrix ? flare->posX/cols : 0; // proportional to width - sparks[i].vel *= -gravity *50; + sparks[i].vel *= -gravity *50.0f; } //sparks[1].col = 345; // this will be our known spark - *dying_gravity = gravity/2; + *dying_gravity = gravity/2.0f; SEGENV.aux0 = 3; } @@ -3380,6 +3803,17 @@ uint16_t mode_exploding_fireworks(void) static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; + +//SparkDrop type is used for drip +typedef struct __attribute__ ((packed)) SparkDrop { + float pos; + float boost; // speed "kick" when dropping + float vel; + uint32_t aux; // aux variable (RGBW color) + uint16_t col; + uint8_t colIndex; +} sparkdrop; + /* * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k @@ -3389,21 +3823,26 @@ uint16_t mode_drip(void) if (SEGLEN == 1) return mode_static(); //allocate segment data uint16_t strips = SEGMENT.nrOfVStrips(); - const int maxNumDrops = 4; - uint16_t dataSize = sizeof(spark) * maxNumDrops; + constexpr int maxNumDrops = 4; + uint16_t dataSize = sizeof(sparkdrop) * maxNumDrops; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed - Spark* drops = reinterpret_cast(SEGENV.data); + SparkDrop* drops = reinterpret_cast(SEGENV.data); + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); // WLEDMM clear LEDs at startup + SEGENV.step = strip.now; // initial time + } if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); struct virtualStrip { - static void runStrip(uint16_t stripNr, Spark* drops) { + static void runStrip(uint16_t stripNr, SparkDrop* drops, float deltaTime) { // WLEDMM added deltaTime uint8_t numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 - float gravity = -0.0005 - (SEGMENT.speed/50000.0); - gravity *= SEGLEN-1; - int sourcedrop = 12; + float theSpeed = (SEGMENT.speed * SEGMENT.speed) / 255.0f; // WLEDMM + float gravity = -0.0002f - theSpeed/42000.0f; //gravity // WLEDMM adjusted + gravity *= min(max(1, SEGLEN-1), 255); // WLEDMM speed limit 255 + const int sourcedrop = 12; for (int j=0;j255) drops[j].col=255; - SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col)); + int intPos = max(0.0f, roundf(drops[j].pos)); // WLEDMM round it first + SEGMENT.setPixelColor(int(indexToVStrip(intPos, stripNr)), color_blend(BLACK,dropColor,drops[j].col)); - drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling + unsigned swell = map(SEGMENT.custom1, 0, 255, 1, 6); // swelling + drops[j].col += swell; + if (drops[j].boost < 4.0f) drops[j].boost += 0.012f * float(swell); // increase mass when swelling - if (random8() < drops[j].col/10) { // random drop + uint32_t fallrate = (drops[j].col * (1 + SEGMENT.custom1 * SEGMENT.custom1)) / 192; // WLEDMM specific + if (random16() <= (fallrate / 10)) { // random drop => 1% ... 20% probalibity drops[j].colIndex=2; //fall drops[j].col=255; + drops[j].vel = gravity * 2.0f * drops[j].boost; // WLEDMM initial kick } } if (drops[j].colIndex > 1) { // falling - if (drops[j].pos > 0) { // fall until end of segment - drops[j].pos += drops[j].vel; + if (drops[j].pos > 0.01f) { // fall until end of segment + drops[j].pos += drops[j].vel * deltaTime; if (drops[j].pos < 0) drops[j].pos = 0; - drops[j].vel += gravity; // gravity is negative - - for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets - uint16_t pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally - SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling + drops[j].vel += gravity * deltaTime; // gravity is negative + + int maxLen = 8 + SEGMENT.speed/64; + for (int i=1; i < maxLen-drops[j].colIndex; i++) { // some minor math so we don't expand bouncing droplets + int intPos = roundf(drops[j].pos + float(i)); // WLEDMM round it first + if ((intPos >= SEGLEN) || (intPos < 0)) break; // WLEDMM skip off-screen pixels + uint16_t pos = constrain(intPos, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally // WLEDMM bad cast to uint16_t removed + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,dropColor,drops[j].col/i)); //spread pixel with fade while falling } if (drops[j].colIndex > 2) { // during bounce, some water is on the floor - SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,drops[j].col)); + SEGMENT.addPixelColor(indexToVStrip(0, stripNr), color_blend(dropColor,BLACK, drops[j].col*4)); // WLEDMM darker } } else { // we hit bottom if (drops[j].colIndex > 2) { // already hit once, so back to forming drops[j].colIndex = 0; - drops[j].col = sourcedrop; + // drops[j].col = sourcedrop; } else { if (drops[j].colIndex==2) { // init bounce - drops[j].vel = -drops[j].vel/4;// reverse velocity with damping - drops[j].pos += drops[j].vel; + // reverse velocity with damping + if (SEGLEN > 16) drops[j].vel = -drops[j].vel/3.5f; + else drops[j].vel = -drops[j].vel/4.5f; + // do bounce + drops[j].pos += drops[j].vel * deltaTime * drops[j].boost*0.5f; } drops[j].col = sourcedrop*2; drops[j].colIndex = 5; // bouncing @@ -3459,12 +3912,16 @@ uint16_t mode_drip(void) } }; + // WLEDMM calculate time passed + uint32_t millisPassed = min(max(1U, unsigned(strip.now - SEGENV.step)), 180U); // constrain between 1 and 180 + SEGENV.step = strip.now; + float deltaTime = float(millisPassed) / 20.0f; // base speed 50 FPS for (int stripNr=0; stripNrstack = 0; // reset brick stack size - drop->step = millis() + 2000; // start by fading out strip + drop->step = strip.now + 2000; // start by fading out strip if (SEGMENT.check1) drop->col = 0;// use only one color from palette } if (drop->step == 0) { // init brick - // speed calcualtion: a single brick should reach bottom of strip in X seconds + // speed calculation: a single brick should reach bottom of strip in X seconds // if the speed is set to 1 this should take 5s and at 255 it should take 0.25s // as this is dependant on SEGLEN it should be taken into account and the fact that effect runs every FRAMETIME s int speed = SEGMENT.speed ? SEGMENT.speed : random8(1,255); @@ -3532,13 +3989,13 @@ uint16_t mode_tetrix(void) { } else { // we hit bottom drop->step = 0; // proceed with next brick, go back to init drop->stack += drop->brick; // increase the stack size - if (drop->stack >= SEGLEN) drop->step = millis() + 2000; // fade out stack + if (drop->stack >= SEGLEN) drop->step = strip.now + 2000; // fade out stack } } if (drop->step > 2) { // fade strip drop->brick = 0; // reset brick size (no more growing) - if (drop->step > millis()) { + if (drop->step > strip.now) { // allow fading of virtual strip for (int i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend } else { @@ -3555,7 +4012,7 @@ uint16_t mode_tetrix(void) { return FRAMETIME; } -static const char _data_FX_MODE_TETRIX[] PROGMEM = "Tetrix@!,Width,,,,One color;!,!;!;;sx=0,ix=0,pal=11,m12=1"; +static const char _data_FX_MODE_TETRIX[] PROGMEM = "Tetrix@!,Width,,,,One color;!,!;!;1.5d;sx=0,ix=0,pal=11,m12=1";//WLEDMM 1.5d /* @@ -3567,13 +4024,13 @@ uint16_t mode_plasma(void) { if (SEGENV.call == 0) { SEGENV.aux0 = random8(0,2); // add a bit of randomness } - uint8_t thisPhase = beatsin8(6+SEGENV.aux0,-64,64); - uint8_t thatPhase = beatsin8(7+SEGENV.aux0,-64,64); + uint8_t thisPhase = beatsin8_t(6+SEGENV.aux0,-64,64); + uint8_t thatPhase = beatsin8_t(7+SEGENV.aux0,-64,64); for (int i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set color & brightness based on a wave as follows: uint8_t colorIndex = cubicwave8((i*(2+ 3*(SEGMENT.speed >> 5))+thisPhase) & 0xFF)/2 // factor=23 // Create a wave and add a phase change and add another wave with its own phase change. - + cos8((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. - uint8_t thisBright = qsub8(colorIndex, beatsin8(7,0, (128 - (SEGMENT.intensity>>1)))); + + cos8_t((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. + uint8_t thisBright = qsub8(colorIndex, beatsin8_t(7,0, (128 - (SEGMENT.intensity>>1)))); //CRGB color = ColorFromPalette(SEGPALETTE, colorIndex, thisBright, LINEARBLEND); //SEGMENT.setPixelColor(i, color.red, color.green, color.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0, thisBright)); @@ -3586,14 +4043,14 @@ static const char _data_FX_MODE_PLASMA[] PROGMEM = "Plasma@Phase,!;!;!"; /* * Percentage display - * Intesity values from 0-100 turn on the leds. + * Intensity values from 0-100 turn on the leds. */ uint16_t mode_percent(void) { uint8_t percent = SEGMENT.intensity; percent = constrain(percent, 0, 200); - uint16_t active_leds = (percent < 100) ? SEGLEN * percent / 100.0 - : SEGLEN * (200 - percent) / 100.0; + uint16_t active_leds = (percent < 100) ? SEGLEN * percent / 100.0f + : SEGLEN * (200 - percent) / 100.0f; uint8_t size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); if (SEGMENT.speed == 255) size = 255; @@ -3639,7 +4096,7 @@ static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@,% of fill,,,,One c /* * Modulates the brightness similar to a heartbeat - * (unimplemented?) tries to draw an ECG aproximation on a 2D matrix + * (unimplemented?) tries to draw an ECG approximation on a 2D matrix */ uint16_t mode_heartbeat(void) { uint8_t bpm = 40 + (SEGMENT.speed >> 3); @@ -3667,7 +4124,7 @@ uint16_t mode_heartbeat(void) { return FRAMETIME; } -static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m12=1"; +static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m12=1"; //Bar // "Pacifica" @@ -3695,17 +4152,17 @@ static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m1 // Modified for WLED, based on https://github.com/FastLED/FastLED/blob/master/examples/Pacifica/Pacifica.ino // // Add one layer of waves into the led array -CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) +CRGB pacifica_one_layer(uint16_t i, const CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) { uint16_t ci = cistart; uint16_t waveangle = ioff; uint16_t wavescale_half = (wavescale >> 1) + 20; waveangle += ((120 + SEGMENT.intensity) * i); //original 250 * i - uint16_t s16 = sin16(waveangle) + 32768; + uint16_t s16 = sin16_t(waveangle) + 32768; uint16_t cs = scale16(s16, wavescale_half) + wavescale_half; ci += (cs * i); - uint16_t sindex16 = sin16(ci) + 32768; + uint16_t sindex16 = sin16_t(ci) + 32768; uint8_t sindex8 = scale16(sindex16, 240); return ColorFromPalette(p, sindex8, bri, LINEARBLEND); } @@ -3737,34 +4194,34 @@ uint16_t mode_pacifica() uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7); strip.now = deltat; - uint16_t speedfactor1 = beatsin16(3, 179, 269); - uint16_t speedfactor2 = beatsin16(4, 179, 269); + uint16_t speedfactor1 = beatsin16_t(3, 179, 269); + uint16_t speedfactor2 = beatsin16_t(4, 179, 269); uint32_t deltams1 = (deltams * speedfactor1) / 256; uint32_t deltams2 = (deltams * speedfactor2) / 256; uint32_t deltams21 = (deltams1 + deltams2) / 2; - sCIStart1 += (deltams1 * beatsin88(1011,10,13)); - sCIStart2 -= (deltams21 * beatsin88(777,8,11)); - sCIStart3 -= (deltams1 * beatsin88(501,5,7)); - sCIStart4 -= (deltams2 * beatsin88(257,4,6)); + sCIStart1 += (deltams1 * beatsin88_t(1011,10,13)); + sCIStart2 -= (deltams21 * beatsin88_t(777,8,11)); + sCIStart3 -= (deltams1 * beatsin88_t(501,5,7)); + sCIStart4 -= (deltams2 * beatsin88_t(257,4,6)); SEGENV.aux0 = sCIStart1; SEGENV.aux1 = sCIStart2; SEGENV.step = sCIStart4; SEGENV.step = (SEGENV.step << 16) + sCIStart3; // Clear out the LED array to a dim background blue-green //SEGMENT.fill(132618); - uint8_t basethreshold = beatsin8( 9, 55, 65); + uint8_t basethreshold = beatsin8_t( 9, 55, 65); uint8_t wave = beat8( 7 ); for (int i = 0; i < SEGLEN; i++) { CRGB c = CRGB(2, 6, 10); // Render each of four layers, with different scales and speeds, that vary over time - c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16(3, 11 * 256, 14 * 256), beatsin8(10, 70, 130), 0-beat16(301)); - c += pacifica_one_layer(i, pacifica_palette_2, sCIStart2, beatsin16(4, 6 * 256, 9 * 256), beatsin8(17, 40, 80), beat16(401)); - c += pacifica_one_layer(i, pacifica_palette_3, sCIStart3, 6 * 256 , beatsin8(9, 10,38) , 0-beat16(503)); - c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8(8, 10,28) , beat16(601)); + c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16_t(3, 11 * 256, 14 * 256), beatsin8_t(10, 70, 130), 0-beat16(301)); + c += pacifica_one_layer(i, pacifica_palette_2, sCIStart2, beatsin16_t(4, 6 * 256, 9 * 256), beatsin8_t(17, 40, 80), beat16(401)); + c += pacifica_one_layer(i, pacifica_palette_3, sCIStart3, 6 * 256 , beatsin8_t(9, 10,38) , 0-beat16(503)); + c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8_t(8, 10,28) , beat16(601)); // Add extra 'white' to areas where the four layers of light have lined up brightly - uint8_t threshold = scale8( sin8( wave), 20) + basethreshold; + uint8_t threshold = scale8( sin8_t( wave), 20) + basethreshold; wave += 7; uint8_t l = c.getAverageLight(); if (l > threshold) { @@ -3797,14 +4254,14 @@ uint16_t mode_sunrise() { //speed 60 - 120 : sunset time in minutes - 60; //speed above: "breathing" rise and set if (SEGENV.call == 0 || SEGMENT.speed != SEGENV.aux0) { - SEGENV.step = millis(); //save starting time, millis() because now can change from sync + SEGENV.step = strip.now; //save starting time, strip.now because now can change from sync SEGENV.aux0 = SEGMENT.speed; } SEGMENT.fill(BLACK); uint16_t stage = 0xFFFF; - uint32_t s10SinceStart = (millis() - SEGENV.step) /100; //tenths of seconds + uint32_t s10SinceStart = (strip.now - SEGENV.step) /100; //tenths of seconds if (SEGMENT.speed > 120) { //quick sunrise and sunset uint16_t counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1); @@ -3852,7 +4309,7 @@ uint16_t phased_base(uint8_t moder) { // We're making sine wave uint8_t modVal = 5;//SEGMENT.fft1/8+1; // You can change the modulus. AKA FFT1 (was 5). uint8_t index = strip.now/64; // Set color rotation speed - *phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) + *phase += SEGMENT.speed/32.0f; // You can change the speed of the wave. AKA SPEED (was .4) for (int i = 0; i < SEGLEN; i++) { if (moder == 1) modVal = (inoise8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. @@ -3883,15 +4340,17 @@ static const char _data_FX_MODE_PHASEDNOISE[] PROGMEM = "Phased Noise@!,!;!,!;!" uint16_t mode_twinkleup(void) { // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline. + uint16_t prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function random16_set_seed(535); // The randomizer needs to be re-set each time through the loop in order for the same 'random' numbers to be the same each time through. for (int i = 0; i < SEGLEN; i++) { uint8_t ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. - uint8_t pixBri = sin8(ranstart + 16 * strip.now/(256-SEGMENT.speed)); + uint8_t pixBri = sin8_t(ranstart + 16 * strip.now/(256-SEGMENT.speed)); if (random8() > SEGMENT.intensity) pixBri = 0; SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(random8()+strip.now/100, false, PALETTE_SOLID_WRAP, 0), pixBri)); } + random16_set_seed(prevSeed); // restore original seed so other effects can use "random" PRNG return FRAMETIME; } static const char _data_FX_MODE_TWINKLEUP[] PROGMEM = "Twinkleup@!,Intensity;!,!;!;;m12=0"; @@ -3908,9 +4367,9 @@ uint16_t mode_noisepal(void) { // Slow noise CRGBPalette16* palettes = reinterpret_cast(SEGENV.data); uint16_t changePaletteMs = 4000 + SEGMENT.speed *10; //between 4 - 6.5sec - if (millis() - SEGENV.step > changePaletteMs) + if (strip.now - SEGENV.step > changePaletteMs) { - SEGENV.step = millis(); + SEGENV.step = strip.now; uint8_t baseI = random8(); palettes[1] = CRGBPalette16(CHSV(baseI+random8(64), 255, random8(128,255)), CHSV(baseI+128, 255, random8(128,255)), CHSV(baseI+random8(92), 192, random8(128,255)), CHSV(baseI+random8(92), 255, random8(128,255))); @@ -3929,7 +4388,7 @@ uint16_t mode_noisepal(void) { // Slow noise SEGMENT.setPixelColor(i, color.red, color.green, color.blue); } - SEGENV.aux0 += beatsin8(10,1,4); // Moving along the distance. Vary it a bit with a sine wave. + SEGENV.aux0 += beatsin8_t(10,1,4); // Moving along the distance. Vary it a bit with a sine wave. return FRAMETIME; } @@ -4002,19 +4461,20 @@ static const char _data_FX_MODE_FLOW[] PROGMEM = "Flow@!,Zones;;!;;m12=1"; //ver */ uint16_t mode_chunchun(void) { - if (SEGLEN == 1) return mode_static(); - SEGMENT.fade_out(254); // add a bit of trail - uint16_t counter = strip.now * (6 + (SEGMENT.speed >> 4)); - uint16_t numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment - uint16_t span = (SEGMENT.intensity << 8) / numBirds; - - for (int i = 0; i < numBirds; i++) + if (SEGLEN <= 1) return mode_static(); + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() + SEGMENT.fade_out(253); // add a bit of trail // WLEDMM fade rate above 253 has no effect + uint32_t counter = ((strip.now * (96 + SEGMENT.speed)) >> 4); // WLEDMM same result, better resolution + uint16_t numBirds = min(32, 2 + (SEGLEN >> 3)); // 2 + 1/8 of a segment - WLEDMM max 32 + uint32_t span = (SEGMENT.intensity << 8) / numBirds; + + for (unsigned i = 0; i < numBirds; i++) { counter -= span; - uint16_t megumin = sin16(counter) + 0x8000; + uint16_t megumin = sin16_t(counter) + 0x8000; uint16_t bird = uint32_t(megumin * SEGLEN) >> 16; uint32_t c = SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0); // no palette wrapping - bird = constrain(bird, 0, SEGLEN-1); + bird = min(bird, uint16_t(SEGLEN-1)); // WLEDMM unsigned is always >= 0 SEGMENT.setPixelColor(bird, c); } return FRAMETIME; @@ -4062,17 +4522,18 @@ uint16_t mode_dancing_shadows(void) uint16_t dataSize = sizeof(spotlight) * numSpotlights; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Spotlight* spotlights = reinterpret_cast(SEGENV.data); + if (SEGENV.call == 0) SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() SEGMENT.fill(BLACK); - unsigned long time = millis(); + unsigned long time = strip.now; bool respawn = false; for (size_t i = 0; i < numSpotlights; i++) { if (!initialize) { // advance the position of the spotlight int16_t delta = (float)(time - spotlights[i].lastUpdateTime) * - (spotlights[i].speed * ((1.0 + SEGMENT.speed)/100.0)); + (spotlights[i].speed * ((1.0f + SEGMENT.speed)/100.0f)); if (abs(delta) >= 1) { spotlights[i].position += delta; @@ -4087,15 +4548,15 @@ uint16_t mode_dancing_shadows(void) spotlights[i].colorIdx = random8(); spotlights[i].width = random8(1, 10); - spotlights[i].speed = 1.0/random8(4, 50); + spotlights[i].speed = 1.0f/random8(4, 50); if (initialize) { spotlights[i].position = random16(SEGLEN); - spotlights[i].speed *= random8(2) ? 1.0 : -1.0; + spotlights[i].speed *= random8(2) > 0 ? 1.0 : -1.0; } else { if (random8(2)) { spotlights[i].position = SEGLEN + spotlights[i].width; - spotlights[i].speed *= -1.0; + spotlights[i].speed *= -1.0f; }else { spotlights[i].position = -spotlights[i].width; } @@ -4180,7 +4641,7 @@ uint16_t mode_washing_machine(void) { SEGENV.step += (speed * 2048) / (512 - SEGMENT.speed); for (int i = 0; i < SEGLEN; i++) { - uint8_t col = sin8(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); + uint8_t col = sin8_t(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(col, false, PALETTE_SOLID_WRAP, 3)); } @@ -4209,7 +4670,7 @@ uint16_t mode_blends(void) { uint16_t offset = 0; for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, pixels[offset++]); - if (offset > pixelLen) offset = 0; + if (offset >= pixelLen) offset = 0; } return FRAMETIME; @@ -4259,8 +4720,8 @@ uint16_t mode_tv_simulator(void) { } // create a new sceene - if (((millis() - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { - tvSimulator->sceeneStart = millis(); // remember the start of the new sceene + if (((strip.now - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { + tvSimulator->sceeneStart = strip.now; // remember the start of the new sceene tvSimulator->sceeneDuration = random16(60* 250* colorSpeed, 60* 750 * colorSpeed); // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) tvSimulator->sceeneColorHue = random16( 0, 768); // random start color-tone for the sceene tvSimulator->sceeneColorSat = random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene @@ -4311,13 +4772,13 @@ uint16_t mode_tv_simulator(void) { tvSimulator->fadeTime = random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time if (random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time - tvSimulator->startTime = millis(); + tvSimulator->startTime = strip.now; } // end of initialization // how much time is elapsed ? - tvSimulator->elapsed = millis() - tvSimulator->startTime; + tvSimulator->elapsed = strip.now - tvSimulator->startTime; - // fade from prev volor to next color + // fade from prev color to next color if (tvSimulator->elapsed < tvSimulator->fadeTime) { r = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pr, nr); g = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pg, ng); @@ -4343,7 +4804,7 @@ uint16_t mode_tv_simulator(void) { return FRAMETIME; } -static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;"; +static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01"; /* @@ -4374,19 +4835,19 @@ class AuroraWave { public: void init(uint32_t segment_length, CRGB color) { - ttl = random(500, 1501); + ttl = random16(500, 1501); basecolor = color; - basealpha = random(60, 101) / (float)100; + basealpha = random8(60, 101) / (float)100; age = 0; - width = random(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier + width = random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier if (!width) width = 1; - center = random(101) / (float)100 * segment_length; - goingleft = random(0, 2) == 0; - speed_factor = (random(10, 31) / (float)100 * W_MAX_SPEED / 255); + center = random8(101) / (float)100 * segment_length; + goingleft = random8(0, 2) == 0; + speed_factor = (random8(10, 31) / (float)100 * W_MAX_SPEED / 255); alive = true; } - CRGB getColorForLED(int ledIndex) { + CRGB getColorForLED(int ledIndex) const { if(ledIndex < center - width || ledIndex > center + width) return 0; //Position out of range of this wave CRGB rgb; @@ -4441,7 +4902,7 @@ class AuroraWave { } }; - bool stillAlive() { + bool stillAlive() const { return alive; }; }; @@ -4457,7 +4918,7 @@ uint16_t mode_aurora(void) { if(SEGENV.aux0 != SEGMENT.intensity || SEGENV.call == 0) { //Intensity slider changed or first call - SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); + SEGENV.aux1 = map2(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); SEGENV.aux0 = SEGMENT.intensity; if(!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) { // 26 on 32 segment ESP32, 9 on 16 segment ESP8266 @@ -4467,7 +4928,7 @@ uint16_t mode_aurora(void) { waves = reinterpret_cast(SEGENV.data); for (int i = 0; i < SEGENV.aux1; i++) { - waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random(0, 3)))); + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random8(0, 3)))); } } else { waves = reinterpret_cast(SEGENV.data); @@ -4479,7 +4940,7 @@ uint16_t mode_aurora(void) { if(!(waves[i].stillAlive())) { //If a wave dies, reinitialize it starts over. - waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random(0, 3)))); + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random8(0, 3)))); } } @@ -4517,9 +4978,10 @@ static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pa // Controls are speed, # of pixels, faderate. uint16_t mode_perlinmove(void) { if (SEGLEN == 1) return mode_static(); + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() SEGMENT.fade_out(255-SEGMENT.custom1); for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { - uint16_t locn = inoise16(millis()*128/(260-SEGMENT.speed)+i*15000, millis()*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + uint16_t locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. uint16_t pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0)); } @@ -4532,12 +4994,12 @@ static const char _data_FX_MODE_PERLINMOVE[] PROGMEM = "Perlin Move@!,# of pixel ///////////////////////// // Waveins // ///////////////////////// -// Uses beatsin8() + phase shifting. By: Andrew Tuline +// Uses beatsin8_t() + phase shifting. By: Andrew Tuline uint16_t mode_wavesins(void) { for (int i = 0; i < SEGLEN; i++) { - uint8_t bri = sin8(millis()/4 + i * SEGMENT.intensity); - uint8_t index = beatsin8(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider + uint8_t bri = sin8_t(strip.now/4 + i * SEGMENT.intensity); + uint8_t index = beatsin8_t(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider //SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, index, bri, LINEARBLEND)); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, bri)); } @@ -4554,14 +5016,14 @@ static const char _data_FX_MODE_WAVESINS[] PROGMEM = "Wavesins@!,Brightness vari uint16_t mode_FlowStripe(void) { const uint16_t hl = SEGLEN * 10 / 13; - uint8_t hue = millis() / (SEGMENT.speed+1); - uint32_t t = millis() / (SEGMENT.intensity/8+1); + uint8_t hue = strip.now / (SEGMENT.speed+1); + uint32_t t = strip.now / (SEGMENT.intensity/8+1); for (int i = 0; i < SEGLEN; i++) { int c = (abs(i - hl) / hl) * 127; - c = sin8(c); - c = sin8(c / 2 + t); - byte b = sin8(c + t/8); + c = sin8_t(c); + c = sin8_t(c / 2 + t); + byte b = sin8_t(c + t/8); SEGMENT.setPixelColor(i, CHSV(b + hue, 255, 255)); } @@ -4591,17 +5053,18 @@ uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulma } SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails - unsigned long t = millis()/128; // timebase + constexpr unsigned long ratio = 128; // rotation speed + unsigned long t = strip.now; // timebase // outer stars - for (size_t i = 0; i < 8; i++) { - x = beatsin8(SEGMENT.custom1>>3, 0, cols - 1, 0, ((i % 2) ? 128 : 0) + t * i); - y = beatsin8(SEGMENT.intensity>>3, 0, rows - 1, 0, ((i % 2) ? 192 : 64) + t * i); + for (unsigned i = 0; i < 8; i++) { + x = beatsin8_t(SEGMENT.custom1>>3, 0, cols - 1, 0, ((i % 2) ? 128 : 0) + (t * i)/ratio); + y = beatsin8_t(SEGMENT.intensity>>3, 0, rows - 1, 0, ((i % 2) ? 192 : 64) + (t * i)/ratio); SEGMENT.addPixelColorXY(x, y, CHSV(i*32, 255, 255)); } // inner stars for (size_t i = 0; i < 4; i++) { - x = beatsin8(SEGMENT.custom2>>3, cols/4, cols - 1 - cols/4, 0, ((i % 2) ? 128 : 0) + t * i); - y = beatsin8(SEGMENT.custom3 , rows/4, rows - 1 - rows/4, 0, ((i % 2) ? 192 : 64) + t * i); + x = beatsin8_t(SEGMENT.custom2>>3, cols/4, cols - 1 - cols/4, 0, ((i % 2) ? 128 : 0) + (t * i)/ratio); + y = beatsin8_t(SEGMENT.custom3 , rows/4, rows - 1 - rows/4, 0, ((i % 2) ? 192 : 64) + (t * i)/ratio); SEGMENT.addPixelColorXY(x, y, CHSV(i*32, 255, 255)); } // central white dot @@ -4637,10 +5100,10 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so SEGENV.aux0++; // hue SEGMENT.fadeToBlackBy(40); for (size_t i = 0; i < numLines; i++) { - byte x1 = beatsin8(2 + SEGMENT.speed/16, 0, (cols - 1)); - byte x2 = beatsin8(1 + SEGMENT.speed/16, 0, (cols - 1)); - byte y1 = beatsin8(5 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 24); - byte y2 = beatsin8(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); + byte x1 = beatsin8_t(2 + SEGMENT.speed/16, 0, (cols - 1)); + byte x2 = beatsin8_t(1 + SEGMENT.speed/16, 0, (rows - 1)); + byte y1 = beatsin8_t(5 + SEGMENT.speed/16, 0, (cols - 1), 0, i * 24); + byte y2 = beatsin8_t(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); CRGB color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); byte xsteps = abs8(x1 - y1) + 1; @@ -4653,7 +5116,7 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so byte dy = lerp8by8(x2, y2, rate); //SEGMENT.setPixelColorXY(dx, dy, grad ? color.nscale8_video(255-rate) : color); // use addPixelColorXY for different look SEGMENT.addPixelColorXY(dx, dy, color); // use setPixelColorXY for different look - if (grad) SEGMENT.fadePixelColorXY(dx, dy, rate); + if (grad) SEGMENT.fadePixelColorXY(dx, dy, gamma8(rate)); } if (dot) { //add white point at the ends of line @@ -4671,11 +5134,13 @@ static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Spee ///////////////////// // 2D DNA // ///////////////////// -uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. +uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. Phases added by @ewoudwijma if (!strip.isMatrix) return mode_static(); // not a 2D set-up const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); + unsigned phases = SEGMENT.custom1; + if (phases > 179) phases = 179 + 2.5f * (phases - 179); // boost for values > 179 if (SEGENV.call == 0) { SEGMENT.setUpLeds(); @@ -4684,21 +5149,37 @@ uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pa SEGMENT.fadeToBlackBy(64); + // WLEDMM optimized to prevent holes at height > 32 + int lastY1 = -1; + int lastY2 = -1; for (int i = 0; i < cols; i++) { - SEGMENT.setPixelColorXY(i, beatsin8(SEGMENT.speed/8, 0, rows-1, 0, i*4 ), ColorFromPalette(SEGPALETTE, i*5+millis()/17, beatsin8(5, 55, 255, 0, i*10), LINEARBLEND)); - SEGMENT.setPixelColorXY(i, beatsin8(SEGMENT.speed/8, 0, rows-1, 0, i*4+128), ColorFromPalette(SEGPALETTE, i*5+128+millis()/17, beatsin8(5, 55, 255, 0, i*10+128), LINEARBLEND)); + // 256 is a complete phase; half a phase of dna is 128 + // unsigned phase = i * 4 * phases / cols; // original formula; cols ==0 cannot happen due to the for loop + unsigned phase = i * 4 * phases / 128; // WLEDMM this reproduces the previous behaviour at phases=127 + int posY1 = beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, phase ); + int posY2 = beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, phase+128); + + if ((i==0) || ((abs(lastY1 - posY1) < 2) && (abs(lastY2 - posY2) < 2))) { // use original code when no holes + SEGMENT.setPixelColorXY(i, posY1, ColorFromPalette(SEGPALETTE, i*5+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10), LINEARBLEND)); + SEGMENT.setPixelColorXY(i, posY2, ColorFromPalette(SEGPALETTE, i*5+128+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10+128), LINEARBLEND)); + } else { // draw line to prevent holes + SEGMENT.drawLine(i-1, lastY1, i, posY1, ColorFromPalette(SEGPALETTE, i*5+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10), LINEARBLEND)); + SEGMENT.drawLine(i-1, lastY2, i, posY2, ColorFromPalette(SEGPALETTE, i*5+128+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10+128), LINEARBLEND)); + } + lastY1 = posY1; + lastY2 = posY2; } SEGMENT.blur(SEGMENT.intensity>>3); return FRAMETIME; } // mode_2Ddna() -static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; +static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur,Phases;;!;2"; ///////////////////////// // 2D DNA Spiral // ///////////////////////// -uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/810 , modified by: Andrew Tuline +uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline if (!strip.isMatrix) return mode_static(); // not a 2D set-up const uint16_t cols = SEGMENT.virtualWidth(); @@ -4709,24 +5190,26 @@ uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulma SEGMENT.fill(BLACK); } - uint8_t speeds = SEGMENT.speed/2 + 1; + uint8_t speeds = SEGMENT.speed/2 + 7; uint8_t freq = SEGMENT.intensity/8; - uint32_t ms = millis() / 20; + uint32_t ms = strip.now / 20; SEGMENT.fadeToBlackBy(135); for (int i = 0; i < rows; i++) { - uint16_t x = beatsin8(speeds, 0, cols - 1, 0, i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, i * freq + 128); - uint16_t x1 = beatsin8(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); + uint16_t x = beatsin8_t(speeds, 0, cols - 1, 0, i * freq) + beatsin8_t(speeds - 7, 0, cols - 1, 0, i * freq + 128); + uint16_t x1 = beatsin8_t(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8_t(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); uint8_t hue = (i * 128 / rows) + ms; // skip every 4th row every now and then (fade it more) if ((i + ms / 8) & 3) { // draw a gradient line between x and x1 x = x / 2; x1 = x1 / 2; - uint8_t steps = abs8(x - x1) + 1; + unsigned steps = abs8(x - x1) + 1; + bool positive = (x1 >= x); // direction of drawing for (size_t k = 1; k <= steps; k++) { - uint8_t rate = k * 255 / steps; - uint8_t dx = lerp8by8(x, x1, rate); + unsigned rate = k * 255 / steps; + //unsigned dx = lerp8by8(x, x1, rate); + unsigned dx = positive? (x + k-1) : (x - k+1); // behaves the same as "lerp8by8" but does not create holes //SEGMENT.setPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND).nscale8_video(rate)); SEGMENT.addPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND)); // use setPixelColorXY for different look SEGMENT.fadePixelColorXY(dx, i, rate); @@ -4745,6 +5228,7 @@ static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed // 2D Drift // ///////////////////////// uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline + // optimized for large panels by @softhack007 if (!strip.isMatrix) return mode_static(); // not a 2D set-up const uint16_t cols = SEGMENT.virtualWidth(); @@ -4755,21 +5239,44 @@ uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmateli SEGMENT.fill(BLACK); } - SEGMENT.fadeToBlackBy(128); + if (SEGMENT.intensity > 1) SEGMENT.fadeToBlackBy(128); + else SEGMENT.fill(BLACK); // WLEDMM fill is faster than fade + const float maxDim = max(cols, rows)/2.0f; - const uint16_t maxDim = MAX(cols, rows)/2; - unsigned long t = millis() / (32 - (SEGMENT.speed>>3)); - for (float i = 1; i < maxDim; i += 0.25) { - float angle = radians(t * (maxDim - i)); - uint16_t myX = (cols>>1) + (uint16_t)(sin_t(angle) * i) + (cols%2); - uint16_t myY = (rows>>1) + (uint16_t)(cos_t(angle) * i) + (rows%2); - SEGMENT.setPixelColorXY(myX, myY, ColorFromPalette(SEGPALETTE, (i * 20) + (t / 20), 255, LINEARBLEND)); - } - SEGMENT.blur(SEGMENT.intensity>>3); + // WLEDMM calculate timebase in float, so we don't need to worry about rounding + const float strip_now = strip.now & 0x003FFFFF; // float can exactly represent numbers up to 22bit + float t; + if (maxDim < 6.0f) t = strip_now / float(16U - (SEGMENT.speed>>4)); // up to 12 (faster) + else if (maxDim <= 16.0f) t = strip_now / float(32U - (SEGMENT.speed>>3)); // 12..32 (standard) + else if (maxDim <= 32.0f) t = strip_now / float(64U - (SEGMENT.speed>>2)); // 32..64 (slower) + else t = strip_now / float(256U - SEGMENT.speed); // above 64 (slowest) + + // WLEDMM pre-calculate some values to speed up the main loop + const int colsCenter = (cols >> 1) + (cols % 2); + const int rowsCenter = (rows >> 1) + (rows % 2); + unsigned t_20 = t/20.0f; // softhack007: pre-calculating this gives about 10% speedup + const float step = (maxDim < 6.0f) ? 0.52f : (maxDim > 24.0f) ? 0.16666666f : 0.25f; // WLEDMM more detail on larger panels + + for (float i = 1.0f; i <= maxDim; i += step) { + unsigned i_20 = i * 20.0f; + float t_maxdim = t * (maxDim - i); + float angle = float(DEG_TO_RAD) * t_maxdim; + // WLEDMM reduce angle to [0 ... 2*PI] - several times faster than letting sinf() do the job + float baseAngle = max(0.0f, floorf(angle * float(1.0 / M_TWOPI)) * float(M_TWOPI)); // multiple of 2_PI (360 deg) that's included in angle + angle -= baseAngle; + + int mySin = sinf(angle) * i; + int myCos = cosf(angle) * i; + if ((unsigned(colsCenter+mySin) < cols) && (unsigned(rowsCenter+myCos) < rows)) // don't draw invisible pixels + SEGMENT.setPixelColorXY(colsCenter+mySin, rowsCenter+myCos, ColorFromPalette(SEGPALETTE, i_20 + t_20, 255, LINEARBLEND)); + if ((SEGMENT.check1) && (unsigned(colsCenter+myCos) < cols) && (unsigned(rowsCenter+mySin) < rows)) + SEGMENT.setPixelColorXY(colsCenter+myCos, rowsCenter+mySin, ColorFromPalette(SEGPALETTE, i_20 + t_20, 255, LINEARBLEND)); // twin mode + } + SEGMENT.blur(SEGMENT.intensity>>((!SEGMENT.check2) * 3), SEGMENT.check2); // user-defined blur - thanks @dedehai return FRAMETIME; } // mode_2DDrift() -static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur amount;;!;2"; +static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur,,,,Twin,Smear;;!;2;ix=0"; ////////////////////////// @@ -4786,25 +5293,25 @@ uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline SEGMENT.fill(BLACK); } - uint16_t xscale = SEGMENT.intensity*4; - uint32_t yscale = SEGMENT.speed*8; - uint8_t indexx = 0; + unsigned xscale = SEGMENT.intensity*4; + unsigned yscale = SEGMENT.speed*8; + unsigned indexx = 0; - SEGPALETTE = CRGBPalette16( CRGB(0,0,0), CRGB(0,0,0), CRGB(0,0,0), CRGB(0,0,0), - CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, - CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, - CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); + CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black, + CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, + CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, + CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); for (int j=0; j < cols; j++) { for (int i=0; i < rows; i++) { - indexx = inoise8(j*yscale*rows/255, i*xscale+millis()/4); // We're moving along our Perlin map. - SEGMENT.setPixelColorXY(j, i, ColorFromPalette(SEGPALETTE, min(i*(indexx)>>4, 255), i*255/cols, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + indexx = inoise8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. + SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*(indexx)>>4, 255U), i*255/cols, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. } // for i } // for j return FRAMETIME; } // mode_2Dfirenoise() -static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale;;!;2"; +static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale,,,,Palette;;!;2;pal=0"; //WLEDMM pal=0 ////////////////////////////// @@ -4823,9 +5330,9 @@ uint16_t mode_2DFrizzles(void) { // By: Stepko https://editor.so SEGMENT.fadeToBlackBy(16); for (size_t i = 8; i > 0; i--) { - SEGMENT.addPixelColorXY(beatsin8(SEGMENT.speed/8 + i, 0, cols - 1), - beatsin8(SEGMENT.intensity/8 - i, 0, rows - 1), - ColorFromPalette(SEGPALETTE, beatsin8(12, 0, 255), 255, LINEARBLEND)); + SEGMENT.addPixelColorXY(beatsin8_t(SEGMENT.speed/8 + i, 0, cols - 1), + beatsin8_t(SEGMENT.intensity/8 - i, 0, rows - 1), + ColorFromPalette(SEGPALETTE, beatsin8_t(12, 0, 255), 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.custom1>>3); @@ -4835,117 +5342,401 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f /////////////////////////////////////////// -// 2D Cellular Automata Game of life // +// 2D Cellular Automata Game of Life // /////////////////////////////////////////// -typedef struct ColorCount { - CRGB color; - int8_t count; -} colorCount; +typedef struct Cell { + uint8_t alive : 1, neighbors : 3, toggleStatus : 1, superDead : 1, oscillatorCheck : 1, spaceshipCheck : 1; +} Cell; + +class GameOfLifeGrid { + private: + Cell* cells; + const int cols, rows, maxIndex; + const int nOffsets[8] = {-cols-1, -cols, -cols+1, -1, 1, cols-1, cols, cols+1}; // Neighbor offsets + const int8_t offsetX[8] = {-1, 0, 1, -1, 1, -1, 0, 1}; + const int8_t offsetY[8] = {-1, -1, -1, 0, 0, 1, 1, 1}; + public: + GameOfLifeGrid(Cell* data, int c, int r) : cells(data), cols(c), rows(r), maxIndex(r * c) {} + void getNeighborIndexes(unsigned neighbors[9], unsigned cIndex, unsigned x, unsigned y, bool wrap) const { + unsigned neighborCount = 0; + bool edgeCell = x == 0 || x == cols-1 || y == 0 || y == rows-1; + for (unsigned i = 0; i < 8; ++i) { + unsigned nIndex = cIndex + nOffsets[i]; + if (edgeCell) { + int nX = x + offsetX[i], nY = y + offsetY[i]; + if (wrap) { + if (nX < 0) nIndex += cols; + else if (nX >= cols) nIndex -= cols; + if (nY < 0) nIndex += maxIndex; + else if (nY >= rows) nIndex -= maxIndex; + } + else { // Wrap disabled, skip out of bound neighbors + if (nX < 0 || nX >= cols || nY < 0 || nY >= rows) continue; + } + } + neighbors[++neighborCount] = nIndex; + } + neighbors[0] = neighborCount; + } + void setCell(unsigned cIndex, unsigned x, unsigned y, bool alive, bool wrap) const { + Cell* cell = &cells[cIndex]; + if (alive == cell->alive) return; // No change + cell->alive = alive; + unsigned neighbors[9]; + getNeighborIndexes(neighbors, cIndex, x, y, wrap); + int val = alive ? 1 : -1; + for (unsigned i = 1; i <= neighbors[0]; ++i) cells[neighbors[i]].neighbors += val; + } + void recalculateEdgeNeighbors(bool wrap) const { + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) { + Cell* cell = &cells[cIndex]; + if (x == 0 || x == cols - 1 || y == 0 || y == rows - 1) { + cell->neighbors = 0; + cell->superDead = 0; + + unsigned neighbors[9]; + getNeighborIndexes(neighbors, cIndex, x, y, wrap); + + for (unsigned i = 1; i <= neighbors[0]; ++i) { + if (cells[neighbors[i]].alive) ++cell->neighbors; + } + } + } + } +}; -uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color +uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ + // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler if (!strip.isMatrix) return mode_static(); // not a 2D set-up const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); - const uint16_t dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled - const uint16_t crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) + const size_t dataSize = SEGMENT.length() * sizeof(Cell); // Cell = 2 bytes + const size_t totalSize = dataSize + 6; // 6 bytes for prevRows(2), prevCols(2), prevPalette, prevWrap + + if (!SEGENV.allocateData(totalSize)) return mode_static(); //allocation failed + uint16_t *prevRows = reinterpret_cast(SEGENV.data); + uint16_t *prevCols = reinterpret_cast(SEGENV.data + 2); + uint8_t *prevPalette = reinterpret_cast (SEGENV.data + 4); + bool *prevWrap = reinterpret_cast (SEGENV.data + 5); + Cell *cells = reinterpret_cast (SEGENV.data + 6); + + uint16_t& generation = SEGENV.aux0; //Rename SEGENV/SEGMENT variables for readability + uint16_t& gliderLength = SEGENV.aux1; + bool allColors = SEGMENT.check1; + bool overlayBG = SEGMENT.check2; + bool wrap = SEGMENT.check3; + bool bgBlendMode = SEGMENT.custom1 > 220 && !overlayBG; // if blur is high and not overlaying, use bg blend mode + byte blur = overlayBG ? 255 : bgBlendMode ? map2(SEGMENT.custom1 - 220, 0, 35, 255, 128) : map2(SEGMENT.custom1, 0, 220, 255, 10); + uint32_t bgColor = SEGCOLOR(1); + + GameOfLifeGrid grid(cells, cols, rows); + + // If rows or cols change due to mirror/transpose, neighbor counts need to be recalculated. Just reset the game. + bool setup = SEGENV.call == 0 || rows != *prevRows || cols != *prevCols; + + if (setup) { + SEGMENT.setUpLeds(); + SEGMENT.fill(bgColor); // to make sure that segment buffer and physical leds are aligned initially + SEGENV.step = 0; + *prevRows = rows; + *prevCols = cols; + + // Calculate glider length LCM(rows,cols)*4 once + uint8_t a = rows; + uint8_t b = cols; + while (b) { + uint8_t t = b; + b = a % b; + a = t; + } + gliderLength = cols * rows / a * 4; + } + + if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix + bool paused = SEGENV.step > strip.now; + + // Setup New Game of Life + if ((!paused && generation == 0) || setup) { + SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds + paused = true; + generation = 1; + *prevWrap = wrap; + *prevPalette = SEGMENT.palette; + + //Setup Grid + memset(cells, 0, dataSize); + random16_set_seed(strip.now>>2); //seed the random generator + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) { + #if defined(ARDUINO_ARCH_ESP32) + random16_add_entropy(esp_random() & 0xFFFF); + #endif + for (unsigned x = 0; x < cols; ++x, ++cIndex) { + if ((random16() & 0xFF) < 82) { // ~32% + grid.setCell(cIndex, x, y, true, wrap); + cells[cIndex].toggleStatus = 1; // Used to set initial color + } + else cells[cIndex].superDead = 1; + } + } + } - if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed - CRGB *prevLeds = reinterpret_cast(SEGENV.data); - uint16_t *crcBuffer = reinterpret_cast(SEGENV.data + dataSize); + bool palChanged = SEGMENT.palette != *prevPalette && !allColors; + if (palChanged) *prevPalette = SEGMENT.palette; + + // Enter redraw loop if not updating or palette changed. + if (palChanged || paused || (SEGMENT.speed != 255 && strip.now - SEGENV.step < 1000 / map2(SEGMENT.speed,0,254,1,60))) { //(1 - 60) updates/sec 255 is uncapped + // Redraw if paused (remove blur), palette changed, overlaying background if not max speed (avoid flicker) + // Generation 1 draws alive cells randomly and fades dead cells + bool newGame = generation == 1; + if (paused || palChanged || overlayBG) { + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) { + Cell& cell = cells[cIndex]; + if (!newGame && cell.superDead) continue; // Skip super dead cells unless new game + bool needsColor = (newGame && cell.toggleStatus); + if (cell.alive) { + if ((needsColor && !random(10)) || palChanged) { + cell.toggleStatus = 0; + uint32_t randomColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); + SEGMENT.setPixelColorXY(x,y, randomColor); // Palette changed or needs initial color + } + else if (overlayBG && !needsColor) SEGMENT.setPixelColorXY(x,y, SEGMENT.getPixelColorXY(x,y)); // Redraw alive cells for overlayBG + } // Dead + else if (paused && !overlayBG) { + uint32_t cellColor = SEGMENT.getPixelColorXY(x,y); + uint32_t blended = color_blend(cellColor, bgColor, bgBlendMode ? 16 : blur); + if (blended == cellColor) blended = bgColor; // color_blend fix + if ((bgBlendMode && newGame) || !bgBlendMode) SEGMENT.setPixelColorXY(x, y, blended); // Blur dead cells when paused + } + } + } + return FRAMETIME; + } + + // Repeat detection + unsigned aliveCount = 0; // Detects empty grids and solo gliders (for smaller grids) + bool updateOscillator = generation % 16 == 0; + bool updateSpaceship = gliderLength && generation % gliderLength == 0; + bool repeatingOscillator = true, repeatingSpaceship = true; + + // First Loop: Applies rules, sets toggleStatus, detects repeating patterns + // Does not update cells yet to prevent neighbor counts from changing mid-loop + int maxIndex = rows * cols; + for (unsigned i = 0; i < maxIndex; ++i) { + Cell& cell = cells[i]; + + if (cell.alive) ++aliveCount; + if (repeatingOscillator && cell.oscillatorCheck != cell.alive) repeatingOscillator = false; + if (repeatingSpaceship && cell.spaceshipCheck != cell.alive) repeatingSpaceship = false; + if (updateOscillator) cell.oscillatorCheck = cell.alive; + if (updateSpaceship) cell.spaceshipCheck = cell.alive; + + if (cell.alive && (cell.neighbors < 2 || cell.neighbors > 3)) cell.toggleStatus = 1; // Loneliness or Overpopulation + else if (!cell.alive && cell.neighbors == 3) { cell.toggleStatus = 1; cell.superDead = 0; } // Reproduction + else cell.toggleStatus = 0; // No change + } + + uint32_t color = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); // Backup color + if (generation <= 8 && !bgBlendMode) blur = 255 - (((generation-1) * (255 - blur)) >> 3); // Ramp up blur for first 8 generations + + bool disableWrap = !wrap || generation % 1500 == 0 || aliveCount == 5; // Disable wrap every 1500 generations to prevent undetected repeats + if (*prevWrap != !disableWrap) { grid.recalculateEdgeNeighbors(!disableWrap); *prevWrap = !disableWrap; } + + // Second Loop: Updates cells, sets colors, and detects super dead cells + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) { + Cell& cell = cells[cIndex]; + if (cell.superDead) continue; // Skip super dead cells (bgColor dead cells) + + uint32_t cellColor = SEGMENT.getPixelColorXY(x, y); + + if (cell.toggleStatus) { + if (cell.alive) { // Dies + grid.setCell(cIndex, x, y, false, !disableWrap); + if (cellColor != bgColor) color = cellColor; + if (!overlayBG) SEGMENT.setPixelColorXY(x,y, blur == 255 ? bgColor : color_blend(cellColor, bgColor, blur)); + if (blur == 255 || bgBlendMode) cell.superDead = 1; + } + else { // Reproduction + grid.setCell(cIndex, x, y, true, !disableWrap); + uint32_t birthColor = color; + if (random8() < SEGMENT.intensity) birthColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); + else { + // Get Colors + uint32_t nColors[8]; + unsigned colorCount = 0; + unsigned neighbors[9]; + grid.getNeighborIndexes(neighbors, cIndex, x, y, !disableWrap); + + for (unsigned i = 1; i <= neighbors[0]; ++i) { + unsigned nIndex = neighbors[i]; + if (cells[nIndex].alive) { + uint32_t nColor = SEGMENT.getPixelColorXY(nIndex % cols, nIndex / cols); + if (nColor == bgColor) continue; + nColors[colorCount++] = nColor; + } + } + if (colorCount) { birthColor = nColors[random8(colorCount)]; color = birthColor; } + } + SEGMENT.setPixelColorXY(x,y, birthColor); + } + } + else { // No change in status + if (cell.alive) { + if (cellColor == bgColor) cellColor = color; else color = cellColor; + SEGMENT.setPixelColorXY(x, y, cellColor); // Redraw alive cells + } + else { // Blur dead + if (blur != 255 && !overlayBG && !bgBlendMode) { + uint32_t blended = color_blend(cellColor, bgColor, blur); // color_blend doesn't always converge to bgColor (this fix needed for fast fps with custom bgColor) + if (blended == cellColor) { blended = bgColor; cell.superDead = 1; } + SEGMENT.setPixelColorXY(x, y, blended); + } + } + } + } - CRGB backgroundColor = SEGCOLOR(1); + if (repeatingOscillator || repeatingSpaceship || !aliveCount) { + generation = 0; // reset on next call + SEGENV.step += 1000; // pause final generation for 1 second + return FRAMETIME; + } - if (SEGENV.call == 0) SEGMENT.setUpLeds(); + ++generation; + SEGENV.step = strip.now; + return FRAMETIME; +} // mode_2Dgameoflife() +static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,Color Mutation ☾,Blur ☾,,,All Colors ☾,Overlay BG ☾,Wrap ☾;!,!;!;2;sx=56,ix=2,c1=128,o1=0,o2=0,o3=1"; - if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) { - SEGENV.step = strip.now; - SEGENV.aux0 = 0; - random16_set_seed(millis()>>2); //seed the random generator +///////////////////////// +// 2D SnowFall // +///////////////////////// +static bool getBitValue(const uint8_t* byteArray, size_t n) { + size_t byteIndex = n / 8; + size_t bitIndex = n % 8; + uint8_t byte = byteArray[byteIndex]; + return (byte >> bitIndex) & 1; +} +static void setBitValue(uint8_t* byteArray, size_t n, bool value) { + size_t byteIndex = n / 8; + size_t bitIndex = n % 8; + if (value) + byteArray[byteIndex] |= (1 << bitIndex); + else + byteArray[byteIndex] &= ~(1 << bitIndex); +} +uint16_t mode_2DSnowFall(void) { // By: Brandon Butler + // Uses bit array to track snow/particles + if (!strip.isMatrix) return mode_static(); // Not a 2D set-up + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + const size_t dataSize = (SEGMENT.length() + 7) / 8; // Round up to nearest byte + + if (!SEGENV.allocateData(dataSize)) return mode_static(); // Allocation failed + byte *grid = reinterpret_cast(SEGENV.data); + + bool overlay = SEGMENT.check2; // Overlay is inverted. Only draws non-snow. Layer 1 controls snow color + uint32_t bgColor = SEGCOLOR(1); + + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(bgColor); + SEGENV.aux0 = 0; // Overflow value + memset(grid, 0, dataSize); + } - //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) + // Draw non snow for inverted overlay + if (overlay) { for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - uint8_t state = random8()%2; - if (state == 0) - SEGMENT.setPixelColorXY(x,y, backgroundColor); - else - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); + if (!getBitValue(grid, y * cols + x)) SEGMENT.setPixelColorXY(x, y, bgColor); } + } + + // fix SEGENV.step in case that timebase jumps + if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black; - memset(crcBuffer, 0, sizeof(uint16_t)*crcBufferLen); - } else if (strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,4)) { - // update only when appropriate time passes (in 42 FPS slots) - return FRAMETIME; + uint8_t speed = map(SEGMENT.speed, 0, 255, 0, 60); // Updates per second + if (!speed || strip.now - SEGENV.step < 1000 / speed) return FRAMETIME; // Not enough time passed + + uint8_t blur = map(SEGMENT.custom2, 0, 255, 255, 0); + uint8_t sway = SEGMENT.custom3; + + // Despawn snow + bool overflow = SEGENV.aux0 && SEGMENT.check3; + int despawnChance = SEGMENT.custom1 == 255 ? 256 : map(SEGMENT.custom1, 0, 255, 0, 100); // 255 goes to 256, allows always despawn + int lastY = rows - 1; + for (int x = 0; x < cols; x++) { + if (overflow || random8() < despawnChance) setBitValue(grid, lastY * cols + x, 0); + if (overlay || getBitValue(grid, lastY * cols + x)) continue; // Skip drawing if inverted overlay or snow + SEGMENT.blendPixelColorXY(x, lastY, bgColor, blur); } + if (SEGENV.aux0) --SEGENV.aux0; // Decrease overflow - //copy previous leds (save previous generation) - //NOTE: using lossy getPixelColor() is a benefit as endlessly repeating patterns will eventually fade out causing a reset - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) prevLeds[XY(x,y)] = SEGMENT.getPixelColorXY(x,y); - - //calculate new leds - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - - colorCount colorsCount[9]; // count the different colors in the 3*3 matrix - for (int i=0; i<9; i++) colorsCount[i] = {backgroundColor, 0}; // init colorsCount - - // iterate through neighbors and count them and their different colors - int neighbors = 0; - for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix - if (i==0 && j==0) continue; // ignore itself - // wrap around segment - int16_t xx = x+i, yy = y+j; - if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0; - if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0; - - uint16_t xy = XY(xx, yy); // previous cell xy to check - // count different neighbours and colors - if (prevLeds[xy] != backgroundColor) { - neighbors++; - bool colorFound = false; - int k; - for (k=0; k<9 && colorsCount[i].count != 0; k++) - if (colorsCount[k].color == prevLeds[xy]) { - colorsCount[k].count++; - colorFound = true; - } - if (!colorFound) colorsCount[k] = {prevLeds[xy], 1}; //add new color found in the array + // Precompute shuffled indices, helps randomize snow movement + uint16_t shuffledIndices[cols]; + for (int i = 0; i < cols; i++) shuffledIndices[i] = i; + std::random_shuffle(shuffledIndices, shuffledIndices + cols); + + // Update snow, loop from 2nd bottom row to top with precomputed random order + for (int y = rows - 2; y >= 0; y--) { + for (int i = 0; i < cols; i++) { + int x = shuffledIndices[i]; + + int pos = XY(x, y); + + uint32_t xyColor = SEGMENT.getPixelColorXY(x, y); // Limit getPixelColorXY calls + if (!getBitValue(grid, pos)) { // No snow, fade if needed and skip + if (!overlay && blur) SEGMENT.setPixelColorXY(x, y, color_blend(xyColor, bgColor, blur)); + continue; } - } // i,j - - // Rules of Life - uint32_t col = prevLeds[XY(x,y)]; - uint32_t bgc = RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0); - if ((col != bgc) && (neighbors < 2)) SEGMENT.setPixelColorXY(x,y, bgc); // Loneliness - else if ((col != bgc) && (neighbors > 3)) SEGMENT.setPixelColorXY(x,y, bgc); // Overpopulation - else if ((col == bgc) && (neighbors == 3)) { // Reproduction - // find dominant color and assign it to a cell - colorCount dominantColorCount = {backgroundColor, 0}; - for (int i=0; i<9 && colorsCount[i].count != 0; i++) - if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i]; - // assign the dominant color w/ a bit of randomness to avoid "gliders" - if (dominantColorCount.count > 0 && random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); - } else if ((col == bgc) && (neighbors == 2) && !random8(128)) { // Mutation - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); - } - // else do nothing! - } //x,y - - // calculate CRC16 of leds - uint16_t crc = crc16((const unsigned char*)prevLeds, dataSize); - // check if we had same CRC and reset if needed - bool repetition = false; - for (int i=0; i 0 && !getBitValue(grid, newPos - 1); + bool downRight = x < cols - 1 && !getBitValue(grid, newPos + 1); + + if (!down) { + if (downLeft && downRight) newX = random8(2) ? x - 1 : x + 1; + else if (downLeft) newX = x - 1; + else if (downRight) newX = x + 1; + else newY = y; // Snow is stuck + } + else if (sway && random8(30) < sway) { // Sway falling snow if horizontal and diagonal directions are open + if (x % 2 == 1 && downLeft && !getBitValue(grid, pos - 1)) newX = x - 1; // Odd Columns Move Left + else if (x % 2 == 0 && downRight && !getBitValue(grid, pos + 1)) newX = x + 1; // Even Columns Move Right + } + + if (newY != y || newX != x) { // Snow moved + setBitValue(grid, pos, 0); // Clear old + setBitValue(grid, XY(newX, newY), 1); // Set new + if (!overlay) SEGMENT.setPixelColorXY(x, y, color_blend(xyColor, bgColor, blur)); // Fade old + } + if (!overlay) SEGMENT.setPixelColorXY(newX, newY, xyColor); // Draw new / redraw stuck + } + } + + // Spawn snow + int spawnChance = map(SEGMENT.intensity, 0, 255, 0, 100); + for (int x = 0; x < cols; x++) { // y = 0 + if (random8() >= spawnChance) continue; + if (getBitValue(grid, x)) {SEGENV.aux0 = rows; continue;} // Snow exists, overflowing + + setBitValue(grid, x, 1); // Spawn snow + if (overlay) continue; // Skip drawing if inverted overlay + if (SEGMENT.check1) SEGMENT.setPixelColorXY(x, 0, ColorFromPalette(SEGPALETTE, random8())); // Use palette + else {int c = random8(120,200); SEGMENT.setPixelColorXY(x, 0, c, c, c);} // Use snow color + } + + SEGENV.step = strip.now; + return FRAMETIME; +} // mode_2DSnowFall() +static const char _data_FX_MODE_2DSNOWFALL[] PROGMEM = "Snow Fall ☾@!,Spawn Rate,Despawn Rate,Blur,Sway Chance,Use Palette,Inverted Overlay,Prevent Overflow,;!,!;!;2;sx=128,ix=16,c1=17,c2=0,c3=0,o1=0,o2=0,o3=1"; ///////////////////////// // 2D Hiphotic // @@ -4959,7 +5750,7 @@ uint16_t mode_2DHiphotic() { // By: ldirko https://edit for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { - SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(sin8(cos8(x * SEGMENT.speed/16 + a / 3) + sin8(y * SEGMENT.intensity/16 + a / 4) + a), false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(sin8_t(cos8_t(x * SEGMENT.speed/16 + a / 3) + sin8_t(y * SEGMENT.intensity/16 + a / 4) + a), false, PALETTE_SOLID_WRAP, 0)); } } @@ -4995,6 +5786,7 @@ uint16_t mode_2DJulia(void) { // An animated Julia set float imAg; if (SEGENV.call == 0) { // Reset the center if we've just re-started this animation. + SEGMENT.setUpLeds(); SEGMENT.fill(BLACK); // WLEDMM avoids dimming when blur option is selected julias->xcen = 0.; julias->ycen = 0.; julias->xymag = 1.0; @@ -5005,8 +5797,11 @@ uint16_t mode_2DJulia(void) { // An animated Julia set SEGMENT.intensity = 24; } - julias->xcen = julias->xcen + (float)(SEGMENT.custom1 - 128)/100000.f; - julias->ycen = julias->ycen + (float)(SEGMENT.custom2 - 128)/100000.f; + // WLEDMM limit drift, so we don't move away into nothing + constexpr float maxCenter = 2.5f; // just an educated guess + if (fabsf(julias->xcen) < maxCenter) julias->xcen = julias->xcen + (float)(SEGMENT.custom1 - 128)/100000.f; + if (fabsf(julias->ycen) < maxCenter) julias->ycen = julias->ycen + (float)(SEGMENT.custom2 - 128)/100000.f; + julias->xymag = julias->xymag + (float)((SEGMENT.custom3 - 16)<<3)/100000.f; // reduced resolution slider if (julias->xymag < 0.01f) julias->xymag = 0.01f; if (julias->xymag > 1.0f) julias->xymag = 1.0f; @@ -5031,15 +5826,17 @@ uint16_t mode_2DJulia(void) { // An animated Julia set maxIterations = SEGMENT.intensity/2; - // Resize section on the fly for some animaton. + // Resize section on the fly for some animation. reAl = -0.94299f; // PixelBlaze example imAg = 0.3162f; - reAl += sin_t((float)millis()/305.f)/20.f; - imAg += sin_t((float)millis()/405.f)/20.f; + //reAl += sinf((float)strip.now/305.f)/20.f; + //imAg += sinf((float)strip.now/405.f)/20.f; + reAl += (float)sin16_t(strip.now * 34) / 655340.f; + imAg += (float)sin16_t(strip.now * 26) / 655340.f; - dx = (xmax - xmin) / (cols); // Scale the delta x and y values to our matrix size. - dy = (ymax - ymin) / (rows); + dx = (xmax - xmin) / cols; // Scale the delta x and y values to our matrix size. + dy = (ymax - ymin) / rows; // Start y float y = ymin; @@ -5078,11 +5875,23 @@ uint16_t mode_2DJulia(void) { // An animated Julia set } y += dy; } -// SEGMENT.blur(64); + + // WLEDMM + if(SEGMENT.check1) + SEGMENT.blurRows(48, false); // slight blurr + if(SEGMENT.check2) + SEGMENT.blur(64, true); // strong blurr + if(SEGMENT.check3) { // draw crosshair + int screenX = lroundf((0.5f / maxCenter) * (julias->xcen + maxCenter) * float(cols)); + int screenY = lroundf((0.5f / maxCenter) * (julias->ycen + maxCenter) * float(rows)); + int hair = min(min(cols-1, rows-1)/2, 3); + SEGMENT.drawLine(screenX, screenY-hair, screenX, screenY+hair, GREEN, true); + SEGMENT.drawLine(screenX-hair, screenY, screenX+hair, screenY, GREEN, true); + } return FRAMETIME; } // mode_2DJulia() -static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size;!;!;2;ix=24,c1=128,c2=128,c3=16"; +static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size,Soft Blur,Strong Blur,Show Center;!;!;2;ix=24,c1=128,c2=128,c3=16"; ////////////////////////////// @@ -5094,28 +5903,43 @@ uint16_t mode_2DLissajous(void) { // By: Andrew Tuline const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); + if (SEGENV.call == 0) { SEGMENT.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM fadeToBlackBy() needs previous setUpLeds() SEGMENT.fadeToBlackBy(SEGMENT.intensity); - - //for (int i=0; i < 4*(cols+rows); i ++) { + + uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed + + if (SEGENV.check3) { // WLEDMM: this is the original "float" code featuring anti-aliasing + int maxLoops = max(192, 4*(cols+rows)); + maxLoops = ((maxLoops / 128) +1) * 128; // make sure whe have half or full turns => multiples of 128 + for (int i=0; i < maxLoops; i ++) { + float xlocn = float(sin8_t(phase/2 + (i* SEGMENT.speed)/64)) / 255.0f; // WLEDMM align speed with original effect + float ylocn = float(cos8_t(phase/2 + i*2)) / 255.0f; + //SEGMENT.setPixelColorXY(xlocn, ylocn, SEGMENT.color_from_palette(strip.now/100+i, false, PALETTE_SOLID_WRAP, 0)); // draw pixel with anti-aliasing + unsigned palIndex = (256*ylocn) + phase/2 + (i* SEGMENT.speed)/64; + //SEGMENT.setPixelColorXY(xlocn, ylocn, SEGMENT.color_from_palette(palIndex, false, PALETTE_SOLID_WRAP, 0)); // draw pixel with anti-aliasing - color follows rotation + // WLEDMM wu_pixel is 50% faster, and still lokks better + SEGMENT.wu_pixel(uint32_t(xlocn * ((cols-1) <<8)), uint32_t(ylocn * ((rows-1) <<8)), + CRGB(SEGMENT.color_from_palette(palIndex, false, PALETTE_SOLID_WRAP, 0))); + } + } else for (int i=0; i < 256; i ++) { - //float xlocn = float(sin8(now/4+i*(SEGMENT.speed>>5))) / 255.0f; - //float ylocn = float(cos8(now/4+i*2)) / 255.0f; - uint8_t xlocn = sin8(millis()/4+i*(SEGMENT.speed>>5)); - uint8_t ylocn = cos8(millis()/4+i*2); - xlocn = map(xlocn,0,255,0,cols-1); - ylocn = map(ylocn,0,255,0,rows-1); - SEGMENT.setPixelColorXY(xlocn, ylocn, SEGMENT.color_from_palette(millis()/100+i, false, PALETTE_SOLID_WRAP, 0)); + //WLEDMM: stick to the original calculations of xlocn and ylocn + uint_fast8_t xlocn = sin8_t(phase/2 + (i*SEGMENT.speed)/64); + uint_fast8_t ylocn = cos8_t(phase/2 + i*2); + xlocn = (cols < 2) ? 1 : (map(2*xlocn, 0,511, 0,2*(cols-1)) +1) /2; // softhack007: "*2 +1" for proper rounding + ylocn = (rows < 2) ? 1 : (map(2*ylocn, 0,511, 0,2*(rows-1)) +1) /2; // "rows > 2" is needed to avoid div/0 in map() + SEGMENT.setPixelColorXY((uint8_t)xlocn, (uint8_t)ylocn, SEGMENT.color_from_palette(strip.now/100+i, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; } // mode_2DLissajous() -static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate;!;!;2"; +static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous ☾@X frequency,Fade rate,,,Speed,,,☾ Smooth Style;!;!;2;sx=64,c3=15"; /////////////////////// // 2D Matrix // /////////////////////// -uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi. +uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. if (!strip.isMatrix) return mode_static(); // not a 2D set-up const uint16_t cols = SEGMENT.virtualWidth(); @@ -5124,6 +5948,8 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. if (SEGENV.call == 0) { SEGMENT.setUpLeds(); SEGMENT.fill(BLACK); + SEGENV.aux0 = SEGENV.aux1 = UINT16_MAX; + SEGENV.step = 0; } uint8_t fade = map(SEGMENT.custom1, 0, 255, 50, 250); // equals trail size @@ -5141,32 +5967,43 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. if (strip.now - SEGENV.step >= speed) { SEGENV.step = strip.now; + // find out what color value is returned by gPC for a "falling code" example pixel + // the color values returned may differ from the previously set values, due to + // - auto brightness limiter (dimming) + // - lossy color buffer (when not using global buffer) + // - color balance correction + // - segment opacity + CRGB oldSpawnColor = spawnColor; + if ((SEGENV.aux0 < cols) && (SEGENV.aux1 < rows)) { // we have a hint from last run + oldSpawnColor = SEGMENT.getPixelColorXY(SEGENV.aux0, SEGENV.aux1); // find color of previous spawns + SEGENV.aux1 ++; // our sample pixel will be one row down the next time + } + if ((oldSpawnColor == CRGB::Black) || (oldSpawnColor == trailColor)) oldSpawnColor = spawnColor; // reject "black", as it would mean that ALL pixels create trails + + // move pixels one row down. Falling codes keep color and add trail pixels; all others pixels are faded for (int row=rows-1; row>=0; row--) { for (int col=0; col= rows); // empty screen means that the last falling code has moved out of screen area // spawn new falling code - if (random8() < SEGMENT.intensity || emptyScreen) { - uint8_t spawnX = random8(cols); + if (random8() <= SEGMENT.intensity || emptyScreen) { + uint16_t spawnX = random16(cols); SEGMENT.setPixelColorXY(spawnX, 0, spawnColor); + // update hint for next run + SEGENV.aux0 = spawnX; + SEGENV.aux1 = 0; } } // if millis @@ -5194,8 +6031,8 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have uint8_t y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); // and one Lissajou function - uint8_t x1 = beatsin8(23 * speed, 0, cols-1); - uint8_t y1 = beatsin8(28 * speed, 0, rows-1); + uint8_t x1 = beatsin8_t(23 * speed, 0, cols-1); + uint8_t y1 = beatsin8_t(28 * speed, 0, rows-1); for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { @@ -5217,7 +6054,7 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have byte color = dist ? 1000 / dist : 255; // map color between thresholds - if (color > 0 and color < 60) { + if (color > 0 && color < 60) { SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(map(color * 9, 9, 531, 0, 255), false, PALETTE_SOLID_WRAP, 0)); } else { SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(0, false, PALETTE_SOLID_WRAP, 0)); @@ -5247,7 +6084,7 @@ uint16_t mode_2Dnoise(void) { // By Andrew Tuline for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { - uint8_t pixelHue8 = inoise8(x * scale, y * scale, millis() / (16 - SEGMENT.speed/16)); + uint8_t pixelHue8 = inoise8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, pixelHue8)); } } @@ -5273,7 +6110,7 @@ uint16_t mode_2DPlasmaball(void) { // By: Stepko https://edito SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); - float t = millis() / (33 - SEGMENT.speed/8); + uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { uint16_t thisVal = inoise8(i * 30, t, t); uint16_t thisMax = map(thisVal, 0, 255, 0, cols-1); @@ -5293,7 +6130,7 @@ uint16_t mode_2DPlasmaball(void) { // By: Stepko https://edito (rows - 1 - cy == 0)) ? ColorFromPalette(SEGPALETTE, beat8(5), thisVal, LINEARBLEND) : CRGB::Black); } } - SEGMENT.blur(SEGMENT.custom2>>5); + SEGMENT.blur(SEGMENT.custom2>>5, (SEGMENT.custom2 > 132)); // WLEDMM return FRAMETIME; } // mode_2DPlasmaball() @@ -5311,17 +6148,23 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); + const float maxRows = (rows <= 32) ? 32.0f : (rows <= 64) ? 64.0f : 128.0f; // WLEDMM safe up to 128x128 + const float minScale = (rows <= 32) ? 12.0f : (rows <= 64) ? 7.2f : 4.6f; // WLEDMM + const float maxCols = (cols <= 32) ? 32.0f : (cols <= 64) ? 64.0f : 128.0f; // WLEDMM safe up to 128x128 - CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; + const CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; + const CRGBPalette16 &effectPalette = SEGENV.check1 ? SEGPALETTE : auroraPalette; if (SEGENV.call == 0) { SEGMENT.setUpLeds(); SEGMENT.fill(BLACK); - SEGENV.step = 0; } - float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ??? - uint16_t adjScale = map(cols, 8, 64, 310, 63); + float adjustHeight = mapf(rows, 8, maxRows, 28, minScale); // maybe use mapf() ??? // WLEDMM yes! + uint16_t adjScale = map2(cols, 8, maxCols, 310, 63); // WLEDMM + + adjustHeight = max(min(adjustHeight, 28.0f), minScale); // WLEDMM bugfix for larger fixtures + adjScale = max(min(adjScale, uint16_t(310)), uint16_t(63)); // WLEDMM /* if (SEGENV.aux1 != SEGMENT.custom1/12) { // Hacky palette rotation. We need that black. SEGENV.aux1 = SEGMENT.custom1/12; @@ -5337,22 +6180,40 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https } } */ - uint16_t _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); - byte _speed = map(SEGMENT.speed, 0, 255, 128, 16); - - for (int x = 0; x < cols; x++) { - for (int y = 0; y < rows; y++) { - SEGENV.step++; - SEGMENT.setPixelColorXY(x, y, ColorFromPalette(auroraPalette, - qsub8( - inoise8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), - fabsf((float)rows / 2.0f - (float)y) * adjustHeight))); + uint16_t _scale = map2(SEGMENT.intensity, 0, 255, 30, adjScale); + byte _speed = map2(SEGMENT.speed, 0, 255, 136, 20); + + //WLEDMM add SuperSync control + uint16_t xStart, xEnd, yStart, yEnd; + if (SEGMENT.check2) { //Master (sync on needs to show the whole effect, children only their first panel) + xStart = strip.panel[0].xOffset; + xEnd = strip.panel[0].xOffset + strip.panel[0].width; + yStart = strip.panel[0].yOffset; + yEnd = strip.panel[0].yOffset + strip.panel[0].height; + } + else { + xStart = 0; + xEnd = cols; + yStart = 0; + yEnd = rows; + } + + SEGENV.step = (strip.now * (cols * rows)) / 25; // baseline 40fps + const float rows_2 = (float)rows / 2.0f; // WLEDMM faster to pre-calculate this + for (int x = xStart; x < xEnd; x++) { + for (int y = yStart; y < yEnd; y++) { + SEGENV.step++; + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(effectPalette, + qsub8( + inoise8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), + fabsf(rows_2 - (float)y) * adjustHeight))); // WLEDMM } } + if (SEGENV.check3) SEGMENT.blurRows(192); return FRAMETIME; } // mode_2DPolarLights() -static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale;;;2"; +static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale,,,,Use Palette,SuperSync, Blur;;!;2"; ///////////////////////// @@ -5373,7 +6234,7 @@ uint16_t mode_2DPulser(void) { // By: ldirko https://edi uint32_t a = strip.now / (18 - SEGMENT.speed / 16); uint16_t x = (a / 14) % cols; - uint16_t y = map((sin8(a * 5) + sin8(a * 4) + sin8(a * 2)), 0, 765, rows-1, 0); + uint16_t y = map((sin8_t(a * 5) + sin8_t(a * 4) + sin8_t(a * 2)), 0, 765, rows-1, 0); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND)); SEGMENT.blur(1 + (SEGMENT.intensity>>4)); @@ -5399,11 +6260,11 @@ uint16_t mode_2DSindots(void) { // By: ldirko http SEGMENT.fadeToBlackBy(SEGMENT.custom1>>3); - byte t1 = millis() / (257 - SEGMENT.speed); // 20; - byte t2 = sin8(t1) / 4 * 2; + byte t1 = strip.now / (257 - SEGMENT.speed); // 20; + byte t2 = sin8_t(t1) / 4 * 2; for (int i = 0; i < 13; i++) { - byte x = sin8(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! - byte y = sin8(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! + byte x = sin8_t(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! + byte y = sin8_t(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.custom2>>3); @@ -5433,22 +6294,22 @@ uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://g SEGMENT.fadeToBlackBy(24); - uint8_t blurAmount = SEGMENT.custom3>>1; // reduced resolution slider + uint8_t blurAmount = SEGMENT.custom3<<3; // reduced resolution slider SEGMENT.blur(blurAmount); // Use two out-of-sync sine waves - uint8_t i = beatsin8(19, kBorderWidth, cols-kBorderWidth); - uint8_t j = beatsin8(22, kBorderWidth, cols-kBorderWidth); - uint8_t k = beatsin8(17, kBorderWidth, cols-kBorderWidth); - uint8_t m = beatsin8(18, kBorderWidth, rows-kBorderWidth); - uint8_t n = beatsin8(15, kBorderWidth, rows-kBorderWidth); - uint8_t p = beatsin8(20, kBorderWidth, rows-kBorderWidth); + uint8_t i = beatsin8_t(19, kBorderWidth, cols-kBorderWidth); + uint8_t j = beatsin8_t(22, kBorderWidth, cols-kBorderWidth); + uint8_t k = beatsin8_t(17, kBorderWidth, cols-kBorderWidth); + uint8_t m = beatsin8_t(18, kBorderWidth, rows-kBorderWidth); + uint8_t n = beatsin8_t(15, kBorderWidth, rows-kBorderWidth); + uint8_t p = beatsin8_t(20, kBorderWidth, rows-kBorderWidth); - uint16_t ms = millis(); + uint16_t ms = strip.now; - SEGMENT.addPixelColorXY(i, m, ColorFromPalette(SEGPALETTE, ms/29, 255, LINEARBLEND)); - SEGMENT.addPixelColorXY(j, n, ColorFromPalette(SEGPALETTE, ms/41, 255, LINEARBLEND)); - SEGMENT.addPixelColorXY(k, p, ColorFromPalette(SEGPALETTE, ms/73, 255, LINEARBLEND)); + if (i(SEGENV.data); @@ -5472,7 +6335,7 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi SEGMENT.fill(BLACK); } - unsigned long t = millis() / 4; + unsigned long t = strip.now / 4; int index = 0; uint8_t someVal = SEGMENT.speed/4; // Was 25. for (int j = 0; j < (rows + 2); j++) { @@ -5491,10 +6354,10 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi ++vlx; int8_t nx = bump[x + yindex + 1] - bump[x + yindex - 1]; int8_t ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)]; - byte difx = abs8(vlx * 7 - nx); - byte dify = abs8(vly * 7 - ny); + byte difx = min(abs(vlx * 7 - nx), 255); // WLEDMM replaced abs8 as it does not work for numbers >127 + byte dify = min(abs(vly * 7 - ny), 255); // WLEDMM int temp = difx * difx + dify * dify; - int col = 255 - temp / 8; //8 its a size of effect + int col = 255 - temp / magnify; //8 its a size of effect // WLEDMM size adjusts to matrix dimensions if (col < 0) col = 0; SEGMENT.setPixelColorXY(x, y, HeatColor(col / (3.0f-(float)(SEGMENT.intensity)/128.f))); } @@ -5522,19 +6385,19 @@ uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.so uint8_t hue, bri; size_t intensity; - int offsetX = beatsin16(3, -360, 360); - int offsetY = beatsin16(2, -360, 360); + int offsetX = beatsin16_t(3, -360, 360); + int offsetY = beatsin16_t(2, -360, 360); int sharpness = SEGMENT.custom3 / 8; // 0-3 for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { - hue = x * beatsin16(10, 1, 10) + offsetY; - intensity = bri = sin8(x * SEGMENT.speed/2 + offsetX); + hue = x * beatsin16_t(10, 1, 10) + offsetY; + intensity = bri = sin8_t(x * SEGMENT.speed/2 + offsetX); for (int i=0; i>= 8*sharpness; SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); hue = y * 3 + offsetX; - intensity = bri = sin8(y * SEGMENT.intensity/2 + offsetY); + intensity = bri = sin8_t(y * SEGMENT.intensity/2 + offsetY); for (int i=0; i>= 8*sharpness; SEGMENT.addPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); @@ -5574,9 +6437,9 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht SEGMENT.move(SEGENV.aux0, 1); for (size_t i = 0; i < 8; i++) { - byte x = beatsin8(12 + i, 2, cols - 3); - byte y = beatsin8(15 + i, 2, rows - 3); - CRGB color = ColorFromPalette(SEGPALETTE, beatsin8(12 + i, 0, 255), 255); + byte x = beatsin8_t(12 + i, 2, cols - 3); + byte y = beatsin8_t(15 + i, 2, rows - 3); + CRGB color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255); SEGMENT.addPixelColorXY(x, y, color); if (cols > 24 || rows > 24) { SEGMENT.addPixelColorXY(x+1, y, color); @@ -5596,22 +6459,22 @@ static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur;;!;2 // 2D Crazy Bees // ///////////////////////// //// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek) -#define MAX_BEES 5 +constexpr uint_fast16_t MAX_BEES = 5; uint16_t mode_2Dcrazybees(void) { if (!strip.isMatrix) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const uint_fast16_t cols = SEGMENT.virtualWidth(); + const uint_fast16_t rows = SEGMENT.virtualHeight(); - byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); + const byte n = min(MAX_BEES, (rows * cols) / 256 + 1); typedef struct Bee { uint8_t posX, posY, aimX, aimY, hue; - int8_t deltaX, deltaY, signX, signY, error; - void aimed(uint16_t w, uint16_t h) { - random16_set_seed(millis()); - aimX = random8(0, w); - aimY = random8(0, h); + int8_t signX, signY; + int16_t deltaX, deltaY, error; + void aimed(uint_fast16_t w, uint_fast16_t h) { + aimX = random8(0, min(UINT8_MAX, int(w))); + aimY = random8(0, min(UINT8_MAX, int(h))); hue = random8(); deltaX = abs(aimX - posX); deltaY = abs(aimY - posY); @@ -5625,17 +6488,18 @@ uint16_t mode_2Dcrazybees(void) { bee_t *bee = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { + random16_set_seed(strip.now); //WLEDMM SuperSync SEGMENT.setUpLeds(); SEGMENT.fill(BLACK); for (size_t i = 0; i < n; i++) { - bee[i].posX = random8(0, cols); - bee[i].posY = random8(0, rows); + bee[i].posX = random8(0, min(UINT8_MAX, int(cols))); + bee[i].posY = random8(0, min(UINT8_MAX, int(rows))); bee[i].aimed(cols, rows); } } - if (millis() > SEGENV.step) { - SEGENV.step = millis() + (FRAMETIME * 8 / ((SEGMENT.speed>>5)+1)); + if (strip.now > SEGENV.step) { + SEGENV.step = strip.now + (FRAMETIME * 8 / ((SEGMENT.speed>>5)+1)); SEGMENT.fadeToBlackBy(32); @@ -5646,7 +6510,7 @@ uint16_t mode_2Dcrazybees(void) { SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, CHSV(bee[i].hue, 255, 255)); if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) { SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255))); - int8_t error2 = bee[i].error * 2; + int_fast16_t error2 = bee[i].error * 2; if (error2 > -bee[i].deltaY) { bee[i].error -= bee[i].deltaY; bee[i].posX += bee[i].signX; @@ -5695,7 +6559,7 @@ uint16_t mode_2Dghostrider(void) { const size_t maxLighters = min(cols + rows, LIGHTERS_AM); - if (SEGENV.call == 0) SEGMENT.setUpLeds(); + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { SEGENV.aux0 = cols; SEGENV.aux1 = rows; @@ -5712,16 +6576,16 @@ uint16_t mode_2Dghostrider(void) { } } - if (millis() > SEGENV.step) { - SEGENV.step = millis() + 1024 / (cols+rows); + if (strip.now > SEGENV.step) { + SEGENV.step = strip.now + 1024 / (cols+rows); SEGMENT.fadeToBlackBy((SEGMENT.speed>>2)+64); CRGB color = CRGB::White; SEGMENT.wu_pixel(lighter->gPosX * 256 / 10, lighter->gPosY * 256 / 10, color); - lighter->gPosX += lighter->Vspeed * sin_t(radians(lighter->gAngle)); - lighter->gPosY += lighter->Vspeed * cos_t(radians(lighter->gAngle)); + lighter->gPosX += lighter->Vspeed * sinf(radians(lighter->gAngle)); + lighter->gPosY += lighter->Vspeed * cosf(radians(lighter->gAngle)); lighter->gAngle += lighter->angleSpeed; if (lighter->gPosX < 0) lighter->gPosX = (cols - 1) * 10; if (lighter->gPosX > (cols - 1) * 10) lighter->gPosX = 0; @@ -5739,12 +6603,12 @@ uint16_t mode_2Dghostrider(void) { if (lighter->reg[i]) { lighter->lightersPosY[i] = lighter->gPosY; lighter->lightersPosX[i] = lighter->gPosX; - lighter->Angle[i] = lighter->gAngle + random(-10, 10); + lighter->Angle[i] = lighter->gAngle + ((int)random8(20) - 10); lighter->time[i] = 0; lighter->reg[i] = false; } else { - lighter->lightersPosX[i] += -7 * sin_t(radians(lighter->Angle[i])); - lighter->lightersPosY[i] += -7 * cos_t(radians(lighter->Angle[i])); + lighter->lightersPosX[i] += -7 * sinf(radians(lighter->Angle[i])); + lighter->lightersPosY[i] += -7 * cosf(radians(lighter->Angle[i])); } SEGMENT.wu_pixel(lighter->lightersPosX[i] * 256 / 10, lighter->lightersPosY[i] * 256 / 10, ColorFromPalette(SEGPALETTE, (256 - lighter->time[i]))); } @@ -5780,7 +6644,10 @@ uint16_t mode_2Dfloatingblobs(void) { if (!SEGENV.allocateData(sizeof(blob_t))) return mode_static(); //allocation failed blob_t *blob = reinterpret_cast(SEGENV.data); - if (SEGENV.call == 0) SEGMENT.setUpLeds(); + // WLEDMM fix SEGENV.step in case that timebase jumps + if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; + + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { SEGENV.aux0 = cols; // re-initialise if virtual size changes SEGENV.aux1 = rows; @@ -5799,27 +6666,31 @@ uint16_t mode_2Dfloatingblobs(void) { } SEGMENT.fadeToBlackBy(20); + bool drawAA = (SEGMENT.custom1 > 0) && (SEGMENT.custom1 < 6); //WLEDMM + const uint16_t minDim = min(cols, rows); // WLEDMM use smaller dimension to find good blob size + float max_grow = min(minDim/4.f,2.f); + if (minDim>=24) max_grow =(minDim/8.0f); // WLEDMM allow bigger blobs // Bounce balls around for (size_t i = 0; i < Amount; i++) { - if (SEGENV.step < millis()) blob->color[i] = add8(blob->color[i], 4); // slowly change color + if (SEGENV.step < strip.now) blob->color[i] = add8(blob->color[i], 4); // slowly change color // change radius if needed if (blob->grow[i]) { // enlarge radius until it is >= 4 blob->r[i] += (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f; - if (blob->r[i] >= MIN(cols/4.f,2.f)) { + if (blob->r[i] >= max_grow) { blob->grow[i] = false; } } else { // reduce radius until it is < 1 blob->r[i] -= (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f; - if (blob->r[i] < 1.f) { + if (blob->r[i] < 0.8f) { blob->grow[i] = true; } } uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0); - if (blob->r[i] > 1.f) SEGMENT.fill_circle(blob->x[i], blob->y[i], roundf(blob->r[i]), c); - else SEGMENT.setPixelColorXY(blob->x[i], blob->y[i], c); + if (blob->r[i] > 1.f) SEGMENT.fillCircle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c, drawAA); + else SEGMENT.setPixelColorXY((int)roundf(blob->x[i]), (int)roundf(blob->y[i]), c); // move x if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f)); else if (blob->x[i] - blob->r[i] <= 0) blob->x[i] += (blob->sX[i] * (blob->x[i] / blob->r[i] + 0.005f)); @@ -5849,7 +6720,7 @@ uint16_t mode_2Dfloatingblobs(void) { } SEGMENT.blur(SEGMENT.custom1>>2); - if (SEGENV.step < millis()) SEGENV.step = millis() + 2000; // change colors every 2 seconds + if (SEGENV.step < strip.now) SEGENV.step = strip.now + 2000; // change colors every 2 seconds return FRAMETIME; } @@ -5865,6 +6736,10 @@ uint16_t mode_2Dscrollingtext(void) { const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } int letterWidth; int letterHeight; @@ -5878,10 +6753,31 @@ uint16_t mode_2Dscrollingtext(void) { } const int yoffset = map(SEGMENT.intensity, 0, 255, -rows/2, rows/2) + (rows-letterHeight)/2; char text[33] = {'\0'}; - if (SEGMENT.name) for (size_t i=0,j=0; i31 && SEGMENT.name[i]<128) text[j++] = SEGMENT.name[i]; - - if (!strlen(text) || !strncmp_P(text,PSTR("#DATE"),5) || !strncmp_P(text,PSTR("#DDMM"),5) || !strncmp_P(text,PSTR("#MMDD"),5) || !strncmp_P(text,PSTR("#TIME"),5) || !strncmp_P(text,PSTR("#HHMM"),5)) { // fallback if empty segment name: display date and time - char sec[5]; + unsigned maxLen = (SEGMENT.name) ? min(32, (int)strlen(SEGMENT.name)) : 0; // WLEDMM make it robust against too long segment names + if (SEGMENT.name) for (size_t i=0,j=0; i31 && SEGMENT.name[i]<128) text[j++] = SEGMENT.name[i]; + const bool zero = strchr(text, '0') != nullptr; + bool drawShadow = (SEGMENT.check2); // "shadow" is only needed for overlays to improve readability + + // #ERR = show last error code + if ((strlen(text) > 3) && (strncmp_P(text,PSTR("#ERR"),4) == 0)) { + // read wled error code, and keep it for 30sec max + static byte lastErr = ERR_NONE; // errorFlag cache - we can use a static (global) variable here because the error code is global, too + static unsigned long lastErrTime = 0; // time when lastErr was updated + if ((errorFlag != ERR_NONE) && (lastErr != errorFlag)) { // new error code arrived + lastErr = errorFlag; + lastErrTime = millis(); + } + bool haveError = (lastErr != ERR_NONE) && (millis() - lastErrTime < 30000); // true if we have an "active" error code + if (SEGENV.call < 512) haveError = true; // for testing - initially show "E00" + if ((!haveError) && (errorFlag == ERR_NONE)) lastErr = ERR_NONE; // reset error code + // print error number + if (haveError) sprintf_P(text, PSTR("E%-2.2d"), (int)lastErr); + else sprintf_P(text, PSTR(" ")); + } + + if (!strlen(text) || !strncmp_P(text,PSTR("#F"),2) || !strncmp_P(text,PSTR("#P"),2) || !strncmp_P(text,PSTR("#A"),2) || !strncmp_P(text,PSTR("#DATE"),5) || !strncmp_P(text,PSTR("#DDMM"),5) || !strncmp_P(text,PSTR("#MMDD"),5) || !strncmp_P(text,PSTR("#TIME"),5) || !strncmp_P(text,PSTR("#HH"),3) || !strncmp_P(text,PSTR("#MM"),3)) { // fallback if empty segment name: display date and time + if (!strncmp_P(text,PSTR("#D"),2) || !strncmp_P(text,PSTR("#MM"),3) || !strncmp_P(text,PSTR("#HH"),3)) drawShadow = false; // no seconds - no shadow needed + char sec[5]= {'\0'}; byte AmPmHour = hour(localTime); boolean isitAM = true; if (useAMPM) { @@ -5890,25 +6786,37 @@ uint16_t mode_2Dscrollingtext(void) { } if (useAMPM) sprintf_P(sec, PSTR(" %2s"), (isitAM ? "AM" : "PM")); else sprintf_P(sec, PSTR(":%02d"), second(localTime)); - if (!strncmp_P(text,PSTR("#DATE"),5)) sprintf_P(text, PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); - else if (!strncmp_P(text,PSTR("#DDMM"),5)) sprintf_P(text, PSTR("%d.%d"), day(localTime), month(localTime)); - else if (!strncmp_P(text,PSTR("#MMDD"),5)) sprintf_P(text, PSTR("%d/%d"), month(localTime), day(localTime)); - else if (!strncmp_P(text,PSTR("#TIME"),5)) sprintf_P(text, PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); - else if (!strncmp_P(text,PSTR("#HHMM"),5)) sprintf_P(text, PSTR("%2d:%02d"), AmPmHour, minute(localTime)); - else sprintf_P(text, PSTR("%s %d, %d %2d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); - } + if (!strncmp_P(text,PSTR("#DATE"),5)) sprintf_P(text, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); + else if (!strncmp_P(text,PSTR("#DDMM"),5)) sprintf_P(text, zero?PSTR("%02d.%02d") :PSTR("%d.%d"), day(localTime), month(localTime)); + else if (!strncmp_P(text,PSTR("#MMDD"),5)) sprintf_P(text, zero?PSTR("%02d/%02d") :PSTR("%d/%d"), month(localTime), day(localTime)); + else if (!strncmp_P(text,PSTR("#TIME"),5)) sprintf_P(text, zero?PSTR("%02d:%02d%s") :PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); + else if (!strncmp_P(text,PSTR("#HHMM"),5)) sprintf_P(text, zero?PSTR("%02d:%02d") :PSTR("%d:%02d"), AmPmHour, minute(localTime)); + else if (!strncmp_P(text,PSTR("#HH"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), AmPmHour); + else if (!strncmp_P(text,PSTR("#MM"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), minute(localTime)); + else if (!strncmp_P(text,PSTR("#FPS"),4)) sprintf_P(text, PSTR("%3d"), (int) strip.getFps()); // WLEDMM + else if ((!strncmp_P(text,PSTR("#AMP"),4)) || (!strncmp_P(text,PSTR("#POW"),4))) sprintf_P(text, PSTR("%3.1fA"), float(strip.currentMilliamps)/1000.0f); // WLEDMM + else sprintf_P(text, PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); + } else drawShadow = false; // static text does not require shadow const int numberOfLetters = strlen(text); - if (SEGENV.step < millis()) { + long delayTime = long(strip.now) - long(SEGENV.step); + if ((delayTime >= 0) || (abs(delayTime) > 1500)) { // WLEDMM keep on scrolling if timebase jumps (supersync, or brightness off, or wifi delay) if ((numberOfLetters * letterWidth) > cols) ++SEGENV.aux0 %= (numberOfLetters * letterWidth) + cols; // offset else SEGENV.aux0 = (cols + (numberOfLetters * letterWidth))/2; - ++SEGENV.aux1 &= 0xFF; // color shift - SEGENV.step = millis() + map(SEGMENT.speed, 0, 255, 10*FRAMETIME_FIXED, 2*FRAMETIME_FIXED); + SEGENV.aux1 = (SEGENV.aux1 + 1) & 0xFF; // color shift // WLEDMM changed to prevent overflow + SEGENV.step = strip.now + map2(SEGMENT.speed, 0, 255, 10*FRAMETIME_FIXED, 2*FRAMETIME_FIXED); if (!SEGMENT.check2) { for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++ ) SEGMENT.blendPixelColorXY(x, y, SEGCOLOR(1), 255 - (SEGMENT.custom1>>1)); } + } else { // WLEDMM "repaint" segment to prevent flickering + if (!SEGMENT.check2) { + for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) + SEGMENT.blendPixelColorXY(x, y, SEGCOLOR(1), 255 - SEGMENT.custom1); // slightly reduced "blending" to keep trails visible + } } + + if (SEGENV.check2 && ((numberOfLetters * letterWidth) > cols)) drawShadow = true; // scrolling overlay is easier to read with shadow for (int i = 0; i < numberOfLetters; i++) { if (int(cols) - int(SEGENV.aux0) + letterWidth*(i+1) < 0) continue; // don't draw characters off-screen uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); @@ -5917,7 +6825,7 @@ uint16_t mode_2Dscrollingtext(void) { col1 = SEGCOLOR(0); col2 = SEGCOLOR(2); } - SEGMENT.drawCharacter(text[i], int(cols) - int(SEGENV.aux0) + letterWidth*i, yoffset, letterWidth, letterHeight, col1, col2); + SEGMENT.drawCharacter(text[i], int(cols) - int(SEGENV.aux0) + letterWidth*i, yoffset, letterWidth, letterHeight, col1, col2, drawShadow); } return FRAMETIME; @@ -5937,7 +6845,13 @@ uint16_t mode_2Ddriftrose(void) { const float CX = (cols-cols%2)/2.f - .5f; const float CY = (rows-rows%2)/2.f - .5f; - const float L = min(cols, rows) / 2.f; + + unsigned L2 = SEGENV.check3 ? max(cols, rows) : min(cols, rows); // WLEDMM we use "max" to use the complete segment + if (SEGENV.check3 && (abs(int(cols) - int(rows)) < 4)) L2 = L2 * 1.4142f; // WLEDMM make "expand" look a bit bigger on square panels + const float L = L2 / 2.f; + // WLEDMM pre-calculate some values + const uint32_t wu_cols = SEGMENT.virtualWidth() * 256; + const uint32_t wu_rows = SEGMENT.virtualHeight() * 256; if (SEGENV.call == 0) { SEGMENT.setUpLeds(); @@ -5946,15 +6860,16 @@ uint16_t mode_2Ddriftrose(void) { SEGMENT.fadeToBlackBy(32+(SEGMENT.speed>>3)); for (size_t i = 1; i < 37; i++) { - uint32_t x = (CX + (sin_t(radians(i * 10)) * (beatsin8(i, 0, L*2)-L))) * 255.f; - uint32_t y = (CY + (cos_t(radians(i * 10)) * (beatsin8(i, 0, L*2)-L))) * 255.f; - SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + float angle = float(DEG_TO_RAD) * i * 10; + uint32_t x = int((CX + (sinf(angle) * (beatsin8_t(i, 0, L2)-L))) * 255.f); + uint32_t y = int((CY + (cosf(angle) * (beatsin8_t(i, 0, L2)-L))) * 255.f); + if ((x < wu_cols) && (y < wu_rows)) SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); } SEGMENT.blur((SEGMENT.intensity>>4)+1); return FRAMETIME; } -static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;;2"; +static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur,,,,,,Full Expand ☾;;;2"; #endif // WLED_DISABLE_2D @@ -5974,14 +6889,20 @@ static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;; um_data_t *um_data; if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { volumeSmth = *(float*) um_data->u_data[0]; - volumeRaw = *(float*) um_data->u_data[1]; + volumeRaw = *(int16_t*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; +or + uint8_t fftResult[NUM_GEQ_CHANNELS] = {0}; + if (um_data->u_data != nullptr) memcpy(fftResult, um_data->u_data[2], sizeof(fftResult)); // WLEDMM to buffer curent values + samplePeak = *(uint8_t*) um_data->u_data[3]; FFT_MajorPeak = *(float*) um_data->u_data[4]; my_magnitude = *(float*) um_data->u_data[5]; maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - fftBin = (float*) um_data->u_data[8]; + FFT_MajPeakSmth= *(float*) um_data->u_data[8]; // FFT Majorpeak smoothed + soundPressure = *(float*) um_data->u_data[9]; // sound pressure ( = logarithmic scale microphone input). Range 0...255 + agcSensitivity = *(float*) um_data->u_data[10]; // current AGC gain, scaled to 0...255. use "255.0f - agcSensitivity" to get MIC input level } else { // add support for no audio data um_data = simulateSound(SEGMENT.soundSim); @@ -5991,9 +6912,15 @@ static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;; // a few constants needed for AudioReactive effects +#define NUM_GEQ_CHANNELS 16 // number of audioreactive frequency channels. + // for 22Khz sampling -#define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) -#define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) +#define MIN_FREQUENCY 80 // 80 HZ - due to lower resolution +#define MIN_FREQ_LOG10 1.90309f // log10(MIN_FREQUENCY) +#define MIN_FREQ_LOG 4.38202663467f // log(MIN_FREQUENCY) +#define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) +#define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) +#define MAX_FREQ_LOG 9.30792070f // log(MAX_FREQUENCY) // for 20Khz sampling //#define MAX_FREQUENCY 10240 @@ -6016,11 +6943,7 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; #ifdef ESP32 float FFT_MajorPeak = *(float*) um_data->u_data[4]; @@ -6031,16 +6954,22 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli // printUmData(); if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); SEGENV.aux0 = 255; SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; } + if (SEGMENT.custom1 < 1) SEGMENT.custom1 = 1; // WLEDMM prevent stupid settings for bin + + if (SEGMENT.custom2 < 24) SEGMENT.custom2 = 24; // WLEDMM prevent stupid settings for maxVol (below 24 = noise) *binNum = SEGMENT.custom1; // Select a bin. *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. - SEGMENT.fade_out(240); // Lower frame rate means less effective fading than FastLED - SEGMENT.fade_out(240); + //SEGMENT.fade_out(240); // Lower frame rate means less effective fading than FastLED + //SEGMENT.fade_out(240); + SEGMENT.fade_out(224); // should be the same as 240 applied twice for (int i = 0; i < SEGMENT.intensity/16; i++) { // Limit the number of ripples. if (samplePeak) ripples[i].state = 255; @@ -6053,7 +6982,8 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli ripples[i].pos = random16(SEGLEN); #ifdef ESP32 if (FFT_MajorPeak > 1) // log10(0) is "forbidden" (throws exception) - ripples[i].color = (int)(log10f(FFT_MajorPeak)*128); + //ripples[i].color = (int)(log10f(FFT_MajorPeak)*128); // not to self: buggy !! + ripples[i].color = (int)(logf(FFT_MajorPeak)*32.0f); // works up to 10025 hz else ripples[i].color = 0; #else ripples[i].color = random8(); @@ -6080,7 +7010,7 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli return FRAMETIME; } // mode_ripplepeak() -static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Max # of ripples,Select bin,Volume (min);!,!;!;1v;c2=0,m12=0,si=0"; // Pixel, Beatsin +static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Max # of ripples,Select bin,Volume (min);!,!;!;1v;c1=8,c2=48,m12=0,si=0"; // Pixel, Beatsin #ifndef WLED_DISABLE_2D @@ -6099,32 +7029,28 @@ uint16_t mode_2DSwirl(void) { SEGMENT.fill(BLACK); } - const uint8_t borderWidth = 2; + constexpr uint8_t borderWidth = 2; SEGMENT.blur(SEGMENT.custom1); - uint8_t i = beatsin8( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); - uint8_t j = beatsin8( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); - uint8_t ni = (cols - 1) - i; - uint8_t nj = (cols - 1) - j; - uint16_t ms = millis(); + unsigned i = beatsin8_t( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); + unsigned j = beatsin8_t( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); + unsigned ni = (cols - 1) - i; + unsigned nj = (cols - 1) - j; + uint16_t ms = strip.now; - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; // printUmData(); - SEGMENT.addPixelColorXY( i, j, ColorFromPalette(SEGPALETTE, (ms / 11 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 11, 200, 255); - SEGMENT.addPixelColorXY( j, i, ColorFromPalette(SEGPALETTE, (ms / 13 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 13, 200, 255); - SEGMENT.addPixelColorXY(ni,nj, ColorFromPalette(SEGPALETTE, (ms / 17 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 17, 200, 255); - SEGMENT.addPixelColorXY(nj,ni, ColorFromPalette(SEGPALETTE, (ms / 29 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 29, 200, 255); - SEGMENT.addPixelColorXY( i,nj, ColorFromPalette(SEGPALETTE, (ms / 37 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 37, 200, 255); - SEGMENT.addPixelColorXY(ni, j, ColorFromPalette(SEGPALETTE, (ms / 41 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 41, 200, 255); + if (iu_data[0]; + float soundPressure = *(float*) um_data->u_data[9]; + float agcSensitivity= *(float*) um_data->u_data[10]; SEGMENT.fadeToBlackBy(SEGMENT.speed); + if (SEGENV.check3 && SEGENV.check2) SEGENV.check2 = false; // only one of the two at any time + if ((SEGENV.check2) && (volumeSmth > 0.5f)) volumeSmth = soundPressure; // show sound pressure instead of volume + if (SEGENV.check3) volumeSmth = 255.0 - agcSensitivity; // show AGC level instead of volume - long t = millis() / 2; + long t = strip.now / 2; for (int i = 0; i < cols; i++) { - uint16_t thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; - // use audio if available - if (um_data) { - thisVal /= 32; // reduce intensity of inoise8() - thisVal *= volumeSmth; - } - uint16_t thisMax = map(thisVal, 0, 512, 0, rows); + //uint16_t thisVal = volumeSmth*SEGMENT.intensity/64 * inoise8(i * 45 , t , t)/64; // WLEDMM back to SR code + unsigned thisVal = unsigned(volumeSmth*SEGMENT.intensity) * inoise8(i * 45 , t , t) / (64*64); // WLEDMM same result but more accurate + + //int thisMax = map(thisVal, 0, 512, 0, rows); + int thisMax = (thisVal * rows) / 512; // WLEDMM same result, just faster + int thisMax2 = min(int(rows), thisMax); // WLEDMM limit height to visible are - for (int j = 0; j < thisMax; j++) { - SEGMENT.addPixelColorXY(i, j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); - SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); + for (int j = 0; j < thisMax2; j++) { + //int jmap = map(j, 0, thisMax, 250, 0); + int jmap = 250 - ((j * 250) / thisMax); // WLEDMM same result, just faster + if (!SEGENV.check1) + SEGMENT.addPixelColorXY(i, j, ColorFromPalette(SEGPALETTE, jmap, 255, LINEARBLEND)); + SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, jmap, 255, LINEARBLEND)); } } SEGMENT.blur(16); return FRAMETIME; } // mode_2DWaverly() -static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity;;!;2v;ix=64,si=0"; // Beatsin +static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly ☾@Fade Rate,Amplification,,,,No Clouds,Sound Pressure,AGC debug;;!;2v;ix=64,si=0"; // Beatsin #endif // WLED_DISABLE_2D -// float version of map() -static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - // Gravity struct requited for GRAV* effects typedef struct Gravity { int topLED; @@ -6194,29 +7118,29 @@ typedef struct Gravity { /////////////////////// uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. - const uint16_t dataSize = sizeof(gravity); + constexpr uint16_t dataSize = sizeof(gravity); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); } + + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //SEGMENT.fade_out(240); SEGMENT.fade_out(251); // 30% float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivty" upscaling + segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivity" upscaling - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0); // map to pixels available in current segment + float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0, 32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment uint16_t tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; for (int i=0; itopLED--; if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0)); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); } gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; return FRAMETIME; } // mode_gravcenter() -static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Arc, Beatsin /////////////////////// @@ -6245,12 +7169,12 @@ uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew uint16_t dataSize = sizeof(gravity); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); } + + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; // printUmData(); @@ -6259,15 +7183,15 @@ uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew //SEGMENT.fade_out(240); // twice? really? SEGMENT.fade_out(253); // 50% - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0; - segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivty" upscaling + float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; + segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0); // map to pixels availeable in current segment + float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; for (int i=0; i(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; + // int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; //WLEDMM: this variable not used here + float soundPressure = *(float*) um_data->u_data[9]; + float agcSensitivity= *(float*) um_data->u_data[10]; + #ifdef SR_DEBUG + uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; + #endif - //SEGMENT.fade_out(240); - SEGMENT.fade_out(249); // 25% + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); + } + + float realVolume = volumeSmth; + if (SEGENV.check3 && SEGENV.check2) SEGENV.check2 = false; // only one option + if (SEGENV.check2) volumeSmth = soundPressure; + if (SEGENV.check3) volumeSmth = 255.0f - agcSensitivity; - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0; - segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivty" upscaling + SEGMENT.fade_out(253); + float sensGain = (float)(SEGMENT.intensity+2) / 257.0f; // min gain = 1/128 + if (sensGain > 0.5f) sensGain = ((sensGain -0.5f) * 3.0f) +0.5f; // extend upper range to 3x + float sensOffset = (SEGMENT.check2 && SEGMENT.intensity > 128) ? (float(SEGMENT.intensity - 128)*0.42f) : 0.0f; // slightly raise lower limit, to show more details (sound pressure only) - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment + float segmentSampleAvg = (volumeSmth * sensGain) - sensOffset; + if (segmentSampleAvg < 0) segmentSampleAvg = 0; // could be <0 due to sensOffset + segmentSampleAvg *= 0.25f; // divide by 4, to compensate for later "sensitivity" upscaling + float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment int tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; + int blendVal; + if (SEGENV.check1) blendVal = 255 - roundf((segmentSampleAvg)*6.5f); // reverse, min 48 + else blendVal = roundf(segmentSampleAvg*8.0f); + + //if ((realVolume > 1) && ((blendVal < 1) || (blendVal > 254))) blendVal = strip.now % 192; // provides flickering when overtuned + //else + blendVal = constrain(blendVal, 32, 255); // and saturation for all + if (realVolume > 0.85) // hide main "bar" in silence for (int i=0; i= gravcen->topLED) @@ -6324,51 +7270,58 @@ uint16_t mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--; - if (gravcen->topLED > 0) { - SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0)); + if ((gravcen->topLED > 0) && (SEGMENT.speed < 255)){ // hide top pixel if speed = 255 + if (SEGENV.check2 || SEGENV.check3) + SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(max(uint16_t(strip.now/16),(uint16_t)2), false, PALETTE_SOLID_WRAP, 0)); // flicker a bit slower + else + SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(max(uint16_t(strip.now/2),(uint16_t)2), false, PALETTE_SOLID_WRAP, 0)); // normal flickering } gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; +#ifdef SR_DEBUG + // WLEDMM: abuse last 2 pixels for debugging peak detection + SEGMENT.setPixelColor(SEGLEN-2, (samplePeak > 0) ? GREEN : BLACK); + if (samplePeak > 0) SEGMENT.setPixelColor(SEGLEN-1, GREEN); + // WLEDMM end +#endif + return FRAMETIME; } // mode_gravimeter() -static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter ☾@Rate of fall,Sensitivity,,,,Invert Palette,Sound Pressure,AGC debug;!,!;!;1v;ix=128,m12=2,si=0"; // Arc, Beatsin ////////////////////// // * JUGGLES // ////////////////////// uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() SEGMENT.fade_out(224); // 6.25% - uint16_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); + uint16_t my_sampleAgc = max(min(volumeSmth, 255.0f), 0.0f); for (size_t i=0; iu_data[1]; + float volumeSmth = *(float*) um_data->u_data[0]; + float soundPressure = *(float*) um_data->u_data[9]; + float FFT_MajorPeak = *(float*) um_data->u_data[8]; // 8 = smooth 4=normal if (SEGENV.call == 0) { SEGMENT.setUpLeds(); @@ -6376,17 +7329,38 @@ uint16_t mode_matripix(void) { // Matripix. By Andrew Tuline. } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; - if(SEGENV.aux0 != secondHand) { + if((SEGMENT.speed > 254) || (SEGENV.aux0 != secondHand)) { // WLEDMM allow run run at full speed SEGENV.aux0 = secondHand; - int pixBri = volumeRaw * SEGMENT.intensity / 64; + float rawPixel = (float)volumeRaw; + if (SEGENV.check2) rawPixel = soundPressure; // WLEDMM use Sound Pressure + if ((volumeSmth < 1.0f) || (rawPixel < 1)) rawPixel = 0; + rawPixel = rawPixel*rawPixel / 256.0f; // WLEDMM square scaling to emphasize peaks + + float pixOffset = 0; + if (SEGMENT.check2 && SEGMENT.intensity > 159) pixOffset = float(SEGMENT.intensity - 160) * 0.45f; // slightly raise lower limit, to show more details (sound pressure only) + rawPixel -= pixOffset; if (rawPixel < 0) rawPixel = 0; + + unsigned pixBri = (unsigned)rawPixel * (unsigned)(SEGMENT.intensity+1) / 160; for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left - SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0), pixBri)); + + if (!SEGENV.check1) { + // classic mode: Use Volume for colors + SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now/10, false, PALETTE_SOLID_WRAP, 0), pixBri)); + } else { + // Frequency Colors: select palette entry based on log(MajorPeak) + int16_t palLocn = (volumeSmth > 0.5f) ? 255 : 0; + if((volumeSmth > 0.5f) && (FFT_MajorPeak > MIN_FREQUENCY) && (FFT_MajorPeak < MAX_FREQUENCY)) { + palLocn = (logf(FFT_MajorPeak) - MIN_FREQ_LOG) * (255.0f/(MAX_FREQ_LOG - MIN_FREQ_LOG)) *1.1f; // Scale log frequency values to the 255 colour index. "* 1.1" for some overshoot + if (palLocn < 0) palLocn = 0; // just to be safe + } + SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint16_t)palLocn, false, PALETTE_SOLID_WRAP, 0), pixBri)); + } } return FRAMETIME; } // mode_matripix() -static const char _data_FX_MODE_MATRIPIX[] PROGMEM = "Matripix@!,Brightness;!,!;!;1v;ix=64,m12=2,si=1"; //,rev=1,mi=1,rY=1,mY=1 Circle, WeWillRockYou, reverseX +static const char _data_FX_MODE_MATRIPIX[] PROGMEM = "Matripix ☾@!,Brightness,,,,Frequency Colors,Sound Pressure;!,!;!;1v;ix=96,m12=2,si=1"; //,rev=1,mi=1,rY=1,mY=1 Arc, WeWillRockYou, reverseX ////////////////////// @@ -6395,18 +7369,18 @@ static const char _data_FX_MODE_MATRIPIX[] PROGMEM = "Matripix@!,Brightness;!,!; uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. // Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; - SEGMENT.fade_out(SEGMENT.speed); - SEGMENT.fade_out(SEGMENT.speed); + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); + } + SEGMENT.fadeToBlackBy(SEGMENT.speed/2); + //SEGMENT.fade_out(SEGMENT.speed); - float tmpSound2 = volumeSmth * (float)SEGMENT.intensity / 256.0; // Too sensitive. - tmpSound2 *= (float)SEGMENT.intensity / 128.0; // Reduce sensitity/length. + float tmpSound2 = volumeSmth * (float)SEGMENT.intensity / 256.0f; // Too sensitive. + tmpSound2 *= (float)SEGMENT.intensity / 128.0f; // Reduce sensitivity/length. int maxLen = mapf(tmpSound2, 0, 127, 0, SEGLEN/2); if (maxLen >SEGLEN/2) maxLen = SEGLEN/2; @@ -6416,12 +7390,12 @@ uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } - SEGENV.aux0=SEGENV.aux0+beatsin8(5,0,10); - SEGENV.aux1=SEGENV.aux1+beatsin8(4,0,10); + SEGENV.aux0=SEGENV.aux0+beatsin8_t(5,0,10); + SEGENV.aux1=SEGENV.aux1+beatsin8_t(4,0,10); return FRAMETIME; } // mode_midnoise() -static const char _data_FX_MODE_MIDNOISE[] PROGMEM = "Midnoise@Fade rate,Max. length;!,!;!;1v;ix=128,m12=1,si=0"; // Bar, Beatsin +static const char _data_FX_MODE_MIDNOISE[] PROGMEM = "Midnoise@Fade rate,Max. length;!,!;!;1v;sx=206,ix=128,m12=1,si=0"; // Bar, Beatsin ////////////////////// @@ -6434,17 +7408,13 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; if (SEGENV.call == 0) SEGMENT.fill(BLACK); for (int i = 0; i < SEGLEN; i++) { - uint16_t index = inoise8(i*SEGMENT.speed/64,millis()*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. + uint16_t index = inoise8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of strip.now. By Andrew Tuline. index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. @@ -6454,7 +7424,7 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. return FRAMETIME; } // mode_noisefire() -static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;1v;m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;01v;m12=2,si=0"; // Arc, Beatsin /////////////////////// @@ -6462,19 +7432,16 @@ static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;1v;m12=2, /////////////////////// uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; + if (SEGENV.call == 0) {SEGENV.setUpLeds(); SEGMENT.fill(BLACK);} // WLEDMM use lossless getPixelColor() //uint8_t fadeRate = map(SEGMENT.speed,0,255,224,255); - uint8_t fadeRate = map(SEGMENT.speed,0,255,200,254); + uint8_t fadeRate = map2(SEGMENT.speed,0,255,200,254); SEGMENT.fade_out(fadeRate); - float tmpSound2 = volumeRaw * 2.0 * (float)SEGMENT.intensity / 255.0; + float tmpSound2 = volumeRaw * 2.0f * (float)SEGMENT.intensity / 255.0f; int maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. if (maxLen <0) maxLen = 0; if (maxLen >SEGLEN) maxLen = SEGLEN; @@ -6484,12 +7451,12 @@ uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } - SEGENV.aux0+=beatsin8(5,0,10); - SEGENV.aux1+=beatsin8(4,0,10); + SEGENV.aux0+=beatsin8_t(5,0,10); + SEGENV.aux1+=beatsin8_t(4,0,10); return FRAMETIME; } // mode_noisemeter() -static const char _data_FX_MODE_NOISEMETER[] PROGMEM = "Noisemeter@Fade rate,Width;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_NOISEMETER[] PROGMEM = "Noisemeter@Fade rate,Width;!,!;!;1v;;sx=248,ix=128,m12=2,si=0"; // Arc, Beatsin ////////////////////// @@ -6503,27 +7470,25 @@ uint16_t mode_pixelwave(void) { // Pixelwave. By Andrew Tuline. SEGMENT.fill(BLACK); } - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16; - if (SEGENV.aux0 != secondHand) { + if((SEGMENT.speed > 254) || (SEGENV.aux0 != secondHand)) { // WLEDMM allow run run at full speed SEGENV.aux0 = secondHand; - int pixBri = volumeRaw * SEGMENT.intensity / 64; + float rawPixel = (float)volumeRaw; + rawPixel = rawPixel*rawPixel / 256.0f; // WLEDMM square scaling to emphasize peaks + int pixBri = rawPixel * (SEGMENT.intensity+1) / 96; - SEGMENT.setPixelColor(SEGLEN/2, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0), pixBri)); + SEGMENT.setPixelColor(SEGLEN/2, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now/5, false, PALETTE_SOLID_WRAP, 0), pixBri)); for (int i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left for (int i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; } // mode_pixelwave() -static const char _data_FX_MODE_PIXELWAVE[] PROGMEM = "Pixelwave@!,Sensitivity;!,!;!;1v;ix=64,m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_PIXELWAVE[] PROGMEM = "Pixelwave@!,Sensitivity;!,!;!;01v;ix=64,m12=2,si=0"; // Arc, Beatsin ////////////////////// @@ -6539,22 +7504,23 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. if (!SEGENV.allocateData(sizeof(plasphase))) return mode_static(); //allocation failed Plasphase* plasmoip = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; - SEGMENT.fadeToBlackBy(32); - - plasmoip->thisphase += beatsin8(6,-4,4); // You can change direction and speed individually. - plasmoip->thatphase += beatsin8(7,-4,4); // Two phase values to make a complex pattern. By Andrew Tuline. + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); + } + //SEGMENT.fadeToBlackBy(32); + SEGMENT.fadeToBlackBy(48); + + plasmoip->thisphase += beatsin8_t(6,-4,4); // You can change direction and speed individually. + plasmoip->thatphase += beatsin8_t(7,-4,4); // Two phase values to make a complex pattern. By Andrew Tuline. for (int i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows. // updated, similar to "plasma" effect - softhack007 uint8_t thisbright = cubicwave8(((i*(1 + (3*SEGMENT.speed/32)))+plasmoip->thisphase) & 0xFF)/2; - thisbright += cos8(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. + thisbright += cos8_t(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. uint8_t colorIndex=thisbright; if (volumeSmth * SEGMENT.intensity / 64 < thisbright) {thisbright = 0;} @@ -6564,7 +7530,7 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. return FRAMETIME; } // mode_plasmoid() -static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels;!,!;!;1v;sx=128,ix=128,m12=0,si=0"; // Pixels, Beatsin +static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels;!,!;!;01v;sx=128,ix=80,pal=8,m12=0,si=0"; // Pixels, Beatsin, Lava Palette /////////////////////// @@ -6574,23 +7540,23 @@ static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. uint16_t size = 0; - uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); - uint16_t pos = random(SEGLEN); // Set a random starting position. + uint8_t fadeVal = map2(SEGMENT.speed,0,255, 224, 254); + uint16_t pos = random16(SEGLEN); // Set a random starting position. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; float volumeSmth = *(float*) um_data->u_data[0]; if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; } + if (SEGMENT.custom1 < 1) SEGMENT.custom1 = 1; // WLEDMM prevent stupid settings for bin + if (SEGMENT.custom2 < 24) SEGMENT.custom2 = 24; // WLEDMM prevent stupid settings for maxVol (below 24 = noise) *binNum = SEGMENT.custom1; // Select a bin. *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. @@ -6603,12 +7569,12 @@ uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. } for (int i=0; iu_data[2]; if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() SEGMENT.fill(BLACK); SEGENV.aux0 = 0; } @@ -6699,7 +7661,7 @@ uint16_t mode_blurz(void) { // Blurz. By Andrew Tuline. SEGENV.step += FRAMETIME; if (SEGENV.step > SPEED_FORMULA_L) { uint16_t segLoc = random16(SEGLEN); - SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/(SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), 2*fftResult[SEGENV.aux0%16])); + SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), 2*fftResult[SEGENV.aux0%16])); ++(SEGENV.aux0) %= 16; // make sure it doesn't cross 16 SEGENV.step = 1; @@ -6708,21 +7670,84 @@ uint16_t mode_blurz(void) { // Blurz. By Andrew Tuline. return FRAMETIME; } // mode_blurz() -static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz@Fade rate,Blur;!,Color mix;!;1f;m12=0,si=0"; // Pixels, Beatsin +static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz@Fade rate,Blur;!,Color mix;!;01f;m12=0,si=0"; // Pixels, Beatsin + +#else // original version from SR 0.13, with some enhancements by @softhack007 +uint16_t mode_blurz(void) { // Blurz. By Andrew Tuline. + // Hint: Looks best with segment brightness set to max (use global brightness to reduce brightness) + // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + float volumeSmth = *(float*)um_data->u_data[0]; + float FFT_MajorPeak = *(float*)um_data->u_data[4]; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); // not sure if necessary + SEGMENT.fill(BLACK); + SEGENV.aux0 = 0; + SEGENV.aux1 = 65535; // last pixel postion. 65535 = none + SEGENV.step = 0; // last pixel color + } + + #if defined(ARDUINO_ARCH_ESP32) + random16_add_entropy(esp_random() & 0xFFFF); // improves randonmess + #endif + + int fadeoutDelay = (256 - SEGMENT.speed) / 24; + if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) + SEGMENT.fadeToBlackBy(max(SEGMENT.speed, (uint8_t)1)); + else { + SEGMENT.blur(8 + SEGMENT.intensity/8 + fadeoutDelay*4); + } + if ((SEGENV.aux1 < SEGLEN) && (volumeSmth > 1.0f)) SEGMENT.setPixelColor(SEGENV.aux1,SEGENV.step); // "repaint" last pixel after blur + + unsigned freqBand = SEGENV.aux0 % 16; + uint16_t segLoc = random16(SEGLEN); + + if (SEGENV.check1) { // FreqMap mode : blob location by major frequency + int freqLocn; + unsigned maxLen = (SEGENV.check2) ? max(1, SEGLEN-16): SEGLEN; // usable segment length - leave 16 pixels when embedding "GEQ scan" + freqLocn = roundf((log10f((float)FFT_MajorPeak) - 1.78f) * float(maxLen)/(MAX_FREQ_LOG10 - 1.78f)); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. // WLEDMM proper rounding + if (freqLocn < 1) freqLocn = 0; // avoid underflow + segLoc = (SEGENV.check2) ? freqLocn + freqBand : freqLocn; + } else if (SEGENV.check2) { // GEQ Scanner mode: blob location is defined by frequency band + random offset + float bandWidth = float(SEGLEN) / 16.0f; + int bandStart = roundf(bandWidth * freqBand); + segLoc = bandStart + random16(max(1, int(bandWidth))); + } + segLoc = max(uint16_t(0), min(uint16_t(SEGLEN-1), segLoc)); // fix overflows + + if (SEGLEN < 2) segLoc = 0; // WLEDMM just to be sure + unsigned pixColor = (2*fftResult[freqBand]*240)/max(1, SEGLEN-1); // WLEDMM avoid uint8 overflow, and preserve pixel parameters for redraw + unsigned pixIntensity = min((unsigned)(2.0f*fftResult[freqBand]), 255U); + + if (volumeSmth > 1.0f) { + SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint16_t)pixColor, false, PALETTE_SOLID_WRAP, 0),(uint8_t)pixIntensity)); + SEGENV.step = SEGMENT.getPixelColor(segLoc); // remember last color + SEGENV.aux1 = segLoc; // remember last position + + SEGMENT.blur(max(SEGMENT.intensity, (uint8_t)1)); + SEGENV.aux0 ++; + SEGENV.aux0 %= 16; // make sure it doesn't cross 16 + SEGMENT.addPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint16_t)pixColor, false, PALETTE_SOLID_WRAP, 0),(uint8_t)pixIntensity/2)); // repaint center pixel after blur + } else SEGMENT.blur(max(SEGMENT.intensity, (uint8_t)1)); // silence - just blur it again + + return SEGENV.check2 ? FRAMETIME : (3*FRAMETIME_FIXED/4); // faster updates in GEQ Scanner mode +} // mode_blurz() +static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz Plus ☾@Fade rate,Blur,,,,FreqMap ☾,GEQ Scanner ☾,;!,Color mix;!;01f;sx=48,ix=127,m12=7,si=0"; // Pinwheel, Beatsin +#endif ///////////////////////// // ** DJLight // ///////////////////////// -uint16_t mode_DJLight(void) { // Written by ??? Adapted by Will Tatam. +uint16_t mode_DJLight(void) { // Written by Stefan Petrick, Adapted by Will Tatam. + // No need to prevent from executing on single led strips, only mid will be set (mid = 0) const int mid = SEGLEN / 2; - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + float volumeSmth = *(float*)um_data->u_data[0]; if (SEGENV.call == 0) { SEGMENT.setUpLeds(); @@ -6730,19 +7755,53 @@ uint16_t mode_DJLight(void) { // Written by ??? Adapted by Wil } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64; - if (SEGENV.aux0 != secondHand) { // Triggered millis timing. + if((SEGMENT.speed > 254) || (SEGENV.aux0 != secondHand)) { // WLEDMM allow run run at full speed SEGENV.aux0 = secondHand; - CRGB color = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds - SEGMENT.setPixelColor(mid, color.fadeToBlackBy(map(fftResult[4], 0, 255, 255, 4))); // TODO - Update + CRGB color = CRGB(0,0,0); + // color = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // formula from 0.13.x (10Khz): R = 3880-5120, G=240-340, B=60-100 + if (!SEGENV.check1) { + color = CRGB(fftResult[12]/2, fftResult[3]/2, fftResult[1]/2); // formula for 0.14.x (22Khz): R = 3015-3704, G=216-301, B=86-129 + } else { + // candy factory: an attempt to get more colors + color = CRGB(fftResult[11]/2 + fftResult[12]/4 + fftResult[14]/4, // red : 2412-3704 + 4479-7106 + fftResult[4]/2 + fftResult[3]/4, // green: 216-430 + fftResult[0]/4 + fftResult[1]/4 + fftResult[2]/4); // blue: 46-216 + if ((color.getLuma() < 96) && (volumeSmth >= 1.5f)) { // enhance "almost dark" pixels with yellow, based on not-yet-used channels + unsigned yello_g = (fftResult[5] + fftResult[6] + fftResult[7]) / 3; + unsigned yello_r = (fftResult[7] + fftResult[8] + fftResult[9] + fftResult[10]) / 4; + color.green += (uint8_t) yello_g / 2; + color.red += (uint8_t) yello_r / 2; + } + } + + if (volumeSmth < 1.0f) color = CRGB(0,0,0); // silence = black + // make colors less "pastel", by turning up color saturation in HSV space + if (color.getLuma() > 32) { // don't change "dark" pixels + CHSV hsvColor = rgb2hsv_approximate(color); + hsvColor.v = min(max(hsvColor.v, (uint8_t)48), (uint8_t)204); // 48 < brightness < 204 + if (SEGENV.check1) + hsvColor.s = max(hsvColor.s, (uint8_t)204); // candy factory mode: strongly turn up color saturation (> 192) + else + hsvColor.s = max(hsvColor.s, (uint8_t)108); // normal mode: turn up color saturation to avoid pastels + color = hsvColor; + } + //if (color.getLuma() > 12) color.maximizeBrightness(); // for testing + + //SEGMENT.setPixelColor(mid, color.fadeToBlackBy(map(fftResult[4], 0, 255, 255, 4))); // 0.13.x fade -> 180hz-260hz + uint8_t fadeVal = map2(fftResult[3], 0, 255, 255, 4); // 0.14.x fade -> 216hz-301hz + if (SEGENV.check1) fadeVal = constrain(fadeVal, 0, 176); // "candy factory" mode - avoid complete fade-out + SEGMENT.setPixelColor(mid, color.fadeToBlackBy(fadeVal)); + + // if SEGLEN equals 1 these loops won't execute for (int i = SEGLEN - 1; i > mid; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); // move to the left for (int i = 0; i < mid; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; } // mode_DJLight() -static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed;;;1f;m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed,,,,,Candy Factory;;;01f;m12=2,si=0"; // Arc, Beatsin //////////////////// @@ -6752,44 +7811,48 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - float FFT_MajorPeak = *(float*)um_data->u_data[4]; + um_data_t *um_data = getAudioData(); + float FFT_MajorPeak = *(float*)um_data->u_data[SEGENV.check1 ? 8:4]; // WLEDMM may use FFT_MajorPeakSmth float my_magnitude = *(float*)um_data->u_data[5] / 4.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) - if (SEGENV.call == 0) SEGMENT.fill(BLACK); - int fadeoutDelay = (256 - SEGMENT.speed) / 32; + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); + } + int fadeoutDelay = (256 - SEGMENT.speed) / 96; // WLEDMM if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(SEGMENT.speed); - int locn = (log10f((float)FFT_MajorPeak) - 1.78f) * (float)SEGLEN/(MAX_FREQ_LOG10 - 1.78f); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. + int locn = roundf((log10f((float)FFT_MajorPeak) - 1.78f) * (float)SEGLEN/(MAX_FREQ_LOG10 - 1.78f)); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. // WLEDMM proper rounding if (locn < 1) locn = 0; // avoid underflow if (locn >=SEGLEN) locn = SEGLEN-1; uint16_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + uint16_t bright = (int) (sqrtf(my_magnitude)*16.0f); // WLEDMM sqrt scaling, to make peaks more prominent +#else uint16_t bright = (int)my_magnitude; +#endif SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); + if (SEGMENT.speed > 228) { // WLEDMM looks nice in 2D + SEGMENT.blur(5*(SEGMENT.speed - 224)); + SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); + } - return FRAMETIME; + return FRAMETIME_FIXED; } // mode_freqmap() -static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting color;!,!;!;1f;m12=0,si=0"; // Pixels, Beatsin +static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting color,,,,Smooth mover ☾;!,!;!;1f;sx=192,m12=0,si=0,o1=1"; // Pixels, Beatsin /////////////////////// // ** Freqmatrix // /////////////////////// uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -6798,42 +7861,42 @@ uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Plesch SEGMENT.fill(BLACK); } - uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; - if(SEGENV.aux0 != secondHand) { + uint8_t secondHand = (SEGMENT.speed < 255) ? (micros()/(256-SEGMENT.speed)/500 % 16) : 0; + if((SEGMENT.speed > 254) || (SEGENV.aux0 != secondHand)) { // WLEDMM allow run run at full speed SEGENV.aux0 = secondHand; - uint8_t sensitivity = map(SEGMENT.custom3, 0, 31, 1, 10); // reduced resolution slider - int pixVal = (volumeSmth * SEGMENT.intensity * sensitivity) / 256.0f; - if (pixVal > 255) pixVal = 255; - - float intensity = map(pixVal, 0, 255, 0, 100) / 100.0f; // make a brightness from the last avg + // Pixel brightness (value) based on volume * sensitivity * intensity + uint_fast8_t sensitivity10 = map(SEGMENT.custom3, 0, 31, 10, 100); // reduced resolution slider // WLEDMM sensitivity * 10, to avoid losing precision + int pixVal = volumeSmth * (float)SEGMENT.intensity * (float)sensitivity10 / 2560.0f; // WLEDMM 2560 due to sensitivity * 10 + if (pixVal > 255) pixVal = 255; // make a brightness from the last avg CRGB color = CRGB::Black; if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1; // MajorPeak holds the freq. value which is most abundant in the last sample. - // With our sampling rate of 10240Hz we have a usable freq range from roughtly 80Hz to 10240/2 Hz + // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz // we will treat everything with less than 65Hz as 0 - if (FFT_MajorPeak < 80) { - color = CRGB::Black; - } else { + if ((FFT_MajorPeak > 80.0f) && (volumeSmth > 0.25f)) { // WLEDMM + // Pixel color (hue) based on major frequency int upperLimit = 80 + 42 * SEGMENT.custom2; int lowerLimit = 80 + 3 * SEGMENT.custom1; - uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t - uint16_t b = 255 * intensity; - if (b > 255) b = 255; - color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED + //uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // (original formula) may under/overflow - so we enforce uint8_t + int freqMapped = lowerLimit!=upperLimit ? mapf(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // WLEDMM preserve overflows + uint8_t i = abs(freqMapped) & 0xFF; // WLEDMM we embrace overflow ;-) by "modulo 256" + + color = CHSV(i, 240, (uint8_t)pixVal); // implicit conversion to RGB supplied by FastLED } // shift the pixels one pixel up - SEGMENT.setPixelColor(0, color); + // if SEGLEN equals 1 this loop won't execute for (int i = SEGLEN - 1; i > 0; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + SEGMENT.setPixelColor(0, color); } return FRAMETIME; } // mode_freqmatrix() -static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Time delay,Sound effect,Low bin,High bin,Sensivity;;;1f;m12=3,si=0"; // Corner, Beatsin +static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensitivity;;;01f;c1=18,c2=48,c3=6,m12=3,si=0"; // Corner, Beatsin; notes range C3 to C7 ////////////////////// @@ -6844,18 +7907,17 @@ static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Time delay,So // SEGMENT.speed select faderate // SEGMENT.intensity select colour index uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 16.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255; // Get to 255 as quick as you can. - if (SEGENV.call == 0) SEGMENT.fill(BLACK); + if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } int fadeoutDelay = (256 - SEGMENT.speed) / 64; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(fadeRate); @@ -6868,7 +7930,7 @@ uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. return FRAMETIME; } // mode_freqpixels() -static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Starting color and # of pixels;;;1f;m12=0,si=0"; // Pixels, Beatsin +static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Starting color and # of pixels;;;1f;sx=204,m12=0,si=0"; // Pixels, Beatsin ////////////////////// @@ -6886,12 +7948,9 @@ static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Sta // // As a compromise between speed and accuracy we are currently sampling with 10240Hz, from which we can then determine with a 512bin FFT our max frequency is 5120Hz. // Depending on the music stream you have you might find it useful to change the frequency mapping. -uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschung. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } +uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschung. With some enhancements by @softhack007 + // As before, this effect can also work on single pixels, we just lose the shifting effect + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -6900,44 +7959,56 @@ uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschun SEGMENT.fill(BLACK); } - uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; - if(SEGENV.aux0 != secondHand) { + uint8_t secondHand = (SEGMENT.speed < 255) ? (micros()/(256-SEGMENT.speed)/500 % 16) : 0; + if((SEGMENT.speed > 254) || (SEGENV.aux0 != secondHand)) { // WLEDMM allow to run at full speed SEGENV.aux0 = secondHand; - float sensitivity = mapf(SEGMENT.custom3, 1, 31, 1, 10); // reduced resolution slider + float sensitivity = 0.5f * mapf(SEGMENT.custom3, 1, 31, 0.5, 10); // reduced resolution slider float pixVal = volumeSmth * (float)SEGMENT.intensity / 256.0f * sensitivity; if (pixVal > 255) pixVal = 255; - float intensity = mapf(pixVal, 0, 255, 0, 100) / 100.0f; // make a brightness from the last avg + float intensity = mapf(pixVal, 0, 255, 0, 100) / 100.0f; // make a brightness from the last avg - softhack007 note: completely over-complicated mappings. simplify! CRGB color = 0; if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1.0f; // MajorPeak holds the freq. value which is most abundant in the last sample. - // With our sampling rate of 10240Hz we have a usable freq range from roughtly 80Hz to 10240/2 Hz + // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz // we will treat everything with less than 65Hz as 0 - if (FFT_MajorPeak < 80) { + if ((FFT_MajorPeak < 80) || (volumeSmth < 1.0f) || (FFT_MajorPeak > 10800)) { // silence or out-of-range --> black color = CRGB::Black; } else { - int upperLimit = 80 + 42 * SEGMENT.custom2; - int lowerLimit = 80 + 3 * SEGMENT.custom1; - uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t - uint16_t b = 255.0 * intensity; + uint8_t i = 0; + if (!SEGENV.check1) { + // direct frequency scaling + int upperLimit = 80 + 42 * SEGMENT.custom2; // max 80hz-10Khz + int lowerLimit = 80 + 3 * SEGMENT.custom1; // min 80hz-850hz + i = lowerLimit!=upperLimit ? mapf(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t + } else { + // Musical Scale (logarithmic scaling) + float upperLimit = logf(80 + 42 * SEGMENT.custom2); // max 80hz-10Khz + float lowerLimit = logf(80 + 3 * SEGMENT.custom1); // min 80hz-850hz + float peakMapped = fabsf(lowerLimit - upperLimit)>0.05f ? mapf(logf(FFT_MajorPeak), lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow + if (peakMapped > 255) intensity = constrain((320-peakMapped), 0, intensity*100) / 100.0f; // too high: fade away + i = constrain(peakMapped, 0, 255); // fix over / underflow + } + uint16_t b = 255.0f * intensity; if (b > 255) b=255; - color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED + color = CHSV(i, 176+(uint8_t)b/4, (uint8_t)b); // implicit conversion to RGB supplied by FastLED } SEGMENT.setPixelColor(SEGLEN/2, color); // shift the pixels one pixel outwards + // if SEGLEN equals 1 these loops won't execute for (int i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left for (int i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; } // mode_freqwave() -static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Time delay,Sound effect,Low bin,High bin,Pre-amp;;;1f;m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp,Musical Scale ☾;;;01f;c1=18,c2=48,m12=2,si=0"; // notes range C3 to C7, Arc, Beatsin /////////////////////// @@ -6949,31 +8020,32 @@ uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) - SEGMENT.fade_out(250); + if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + SEGENV.aux0 = 0; // WLEDMM: last color index, to perform some color smoothing + } + SEGMENT.fadeToBlackBy(96); float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivty" upscaling + segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0,32, 0, (float)SEGLEN/2.0); // map to pixels availeable in current segment + float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0,32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment int tempsamp = constrain(mySampleAvg,0,SEGLEN/2); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; - for (int i=0; i= gravcen->topLED) @@ -6981,58 +8053,59 @@ uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--; - if (gravcen->topLED >= 0) { + if ((gravcen->topLED >= 0) && (SEGMENT.speed < 255)) { SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); } gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; + SEGENV.aux0 = indexNew; return FRAMETIME; } // mode_gravfreq() -static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin +static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq ☾@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin ////////////////////// // ** Noisemove // ////////////////////// uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuline - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + um_data_t *um_data = getAudioData(); + uint8_t fftResult[NUM_GEQ_CHANNELS] = {0}; + if (um_data->u_data != nullptr) memcpy(fftResult, um_data->u_data[2], sizeof(fftResult)); // WLEDMM buffer curent values - if (SEGENV.call == 0) SEGMENT.fill(BLACK); + if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } //SEGMENT.fade_out(224); // Just in case something doesn't get faded. int fadeoutDelay = (256 - SEGMENT.speed) / 96; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(4+ SEGMENT.speed/4); - uint8_t numBins = map(SEGMENT.intensity,0,255,0,16); // Map slider to fftResult bins. + uint8_t numBins = map2(SEGMENT.intensity,0,255,0,16); // Map slider to fftResult bins. for (int i=0; iu_data[4]; + um_data_t *um_data = getAudioData(); + float FFT_MajorPeak = *(float*) um_data->u_data[8]; // WLEDMM use FFT_MajorPeakSmth float my_magnitude = *(float*) um_data->u_data[5] / 16.0f; - if (SEGENV.call == 0) SEGMENT.fill(BLACK); + if (SEGENV.call == 0) { + SEGENV.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } SEGMENT.fadeToBlackBy(16); // Just in case something doesn't get faded. float frTemp = FFT_MajorPeak; @@ -7051,13 +8124,14 @@ uint16_t mode_rocktaves(void) { // Rocktaves. Same note from eac frTemp -=132; // This should give us a base musical note of C3 frTemp = fabsf(frTemp * 2.1f); // Fudge factors to compress octave range starting at 0 and going to 255; - uint16_t i = map(beatsin8(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); + uint16_t i = map(beatsin8_t(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); + // i will be always constrained between 0 and 0 if SEGLEN equals 1 i = constrain(i, 0, SEGLEN-1); SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp)); return FRAMETIME; } // mode_rocktaves() -static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;1f;m12=1,si=0"; // Bar, Beatsin +static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12=1,si=0"; // Bar, Beatsin /////////////////////// @@ -7065,13 +8139,10 @@ static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;1f;m12=1 /////////////////////// // Combines peak detection with FFT_MajorPeak and FFT_Magnitude. uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline - if (SEGENV.call == 0) SEGMENT.fill(BLACK); + // effect can work on single pixels, we just lose the shifting effect - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + if (SEGENV.call == 0) SEGMENT.fill(BLACK); + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; float FFT_MajorPeak = *(float*) um_data->u_data[4]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; @@ -7088,56 +8159,89 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin SEGMENT.custom2 = *maxVol * 2; } + if (SEGMENT.custom1 < 1) SEGMENT.custom1 = 1; // WLEDMM prevent stupid settings for bin + if (SEGMENT.custom2 < 24) SEGMENT.custom2 = 24; // WLEDMM prevent stupid settings for maxVol (below 24 = noise) + *binNum = SEGMENT.custom1; // Select a bin. *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. uint8_t secondHand = micros() / (256-SEGMENT.speed)/500 + 1 % 16; - if (SEGENV.aux0 != secondHand) { // Triggered millis timing. + if((SEGMENT.speed > 254) || (SEGENV.aux0 != secondHand)) { // WLEDMM allow run run at full speed SEGENV.aux0 = secondHand; //uint8_t pixCol = (log10f((float)FFT_MajorPeak) - 2.26f) * 177; // 10Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.7 (5012hz). Let's scale accordingly. uint8_t pixCol = (log10f(FFT_MajorPeak) - 2.26f) * 150; // 22Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.967 (9260hz). Let's scale accordingly. if (FFT_MajorPeak < 182.0f) pixCol = 0; // handle underflow + // loop will not execute if SEGLEN equals 1 + for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left + if (samplePeak) { SEGMENT.setPixelColor(SEGLEN-1, CHSV(92,92,92)); } else { SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); } - for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left } return FRAMETIME; } // mode_waterfall() -static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;1f;c2=0,m12=2,si=0"; // Circles, Beatsin +static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;01f;c1=8,c2=48,m12=2,si=0"; // Arc, Beatsin #ifndef WLED_DISABLE_2D ///////////////////////// -// ** 2D GEQ // +// 1D / 2D GEQ // ///////////////////////// -uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. - if (!strip.isMatrix) return mode_static(); // not a 2D set-up - const int NUM_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); +// GEQ helper: either draws 2D, or flattens pixels to 1D +static void setFlatPixelXY(bool flatMode, int x, int y, uint32_t color, unsigned cols, unsigned rows, unsigned offset) { + if ((unsigned(x) >= cols) || (unsigned(y) >= rows)) return; // skip invisible + + if (!flatMode) SEGMENT.setPixelColorXY(x, y, color); // normal 2D + else { + y = rows - y - 1; // reverse y + if (rows > 4) { // center y if we have more than 4 pixels per bar + if (y & 0x01) y = (rows + y) / 2; // center bars: odd pixels to the right + else y = (rows - 1 - y) / 2; // even pixels to the left + } + int pix = x*rows + y + offset; // flatten -> transpose x y so that bars stay bars + if (unsigned(pix) >= SEGLEN) return; // skip invisible + SEGMENT.setPixelColor(pix, color); + } +} + +uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. Flat Mode added by softhack007 + //if (!strip.isMatrix) return mode_static(); // not a 2D set-up, not a problem + bool flatMode = !SEGMENT.is2D() || (SEGMENT.width() < 3) || (SEGMENT.height() < 3); // also use flat mode when less than 3 colums or rows + + const int NUM_BANDS = map2(SEGMENT.custom1, 0, 255, 1, 16); + const int vLength = SEGLEN; // for flat mode + const uint16_t cols = flatMode ? min(max(2, NUM_BANDS), (vLength+1)/2) : SEGMENT.virtualWidth(); + const uint16_t rows = flatMode ? vLength / cols : SEGMENT.virtualHeight(); + const unsigned offset = flatMode ? max(0, (vLength - rows*cols +1) / 2) : 0; // flatmode: always center effect + + if ((cols <=1) || (rows <=1)) return mode_static(); // too small if (!SEGENV.allocateData(cols*sizeof(uint16_t))) return mode_static(); //allocation failed uint16_t *previousBarHeight = reinterpret_cast(SEGENV.data); //array of previous bar heights per frequency band - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + um_data_t *um_data = getAudioData(); + uint8_t fftResult[NUM_GEQ_CHANNELS] = {0}; + if (um_data->u_data != nullptr) memcpy(fftResult, um_data->u_data[2], sizeof(fftResult)); // WLEDMM buffer curent values - if (SEGENV.call == 0) for (int i=0; iu_data[3]; + #endif + + if (SEGENV.call == 0) { + for (int i=0; i= (256U - SEGMENT.intensity)) { - SEGENV.step = millis(); + if (strip.now - SEGENV.step >= (256U - SEGMENT.intensity)) { + SEGENV.step = strip.now; rippleTime = true; } @@ -7145,31 +8249,71 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. int fadeoutDelay = (256 - SEGMENT.speed) / 64; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(SEGMENT.speed); + uint16_t lastBandHeight = 0; // WLEDMM: for smoothing out bars + + //WLEDMM: evenly ditribute bands + float bandwidth = (float)cols / NUM_BANDS; + float remaining = bandwidth; + uint8_t band = 0; for (int x=0; x < cols; x++) { - uint8_t band = map(x, 0, cols-1, 0, NUM_BANDS - 1); - if (NUM_BANDS < 16) band = map(band, 0, NUM_BANDS - 1, 0, 15); // always use full range. comment out this line to get the previous behaviour. - band = constrain(band, 0, 15); - uint16_t colorIndex = band * 17; - uint16_t barHeight = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here + //WLEDMM if not enough remaining + if (remaining < 1) {band++; remaining+= bandwidth;} //increase remaining but keep the current remaining + remaining--; //consume remaining + + // Serial.printf("x %d b %d n %d w %f %f\n", x, band, NUM_BANDS, bandwidth, remaining); + uint8_t frBand = ((NUM_BANDS < 16) && (NUM_BANDS > 1)) ? map(band, 0, NUM_BANDS - 1, 0, 15):band; // always use full range. comment out this line to get the previous behaviour. + // frBand = constrain(frBand, 0, 15); //WLEDMM can never be out of bounds (I think...) + uint16_t colorIndex = frBand * 17; //WLEDMM 0.255 + uint16_t bandHeight = fftResult[frBand]; // WLEDMM we use the original ffResult, to preserve accuracy + + // WLEDMM begin - smooth out bars + if ((x > 0) && (x < (cols-1)) && (SEGMENT.check2)) { + // get height of next (right side) bar + uint8_t nextband = (remaining < 1)? band +1: band; + nextband = constrain(nextband, 0, 15); // just to be sure + frBand = ((NUM_BANDS < 16) && (NUM_BANDS > 1)) ? map(nextband, 0, NUM_BANDS - 1, 0, 15):nextband; // always use full range. comment out this line to get the previous behaviour. + uint16_t nextBandHeight = fftResult[frBand]; + // smooth Band height + bandHeight = (7*bandHeight + 3*lastBandHeight + 3*nextBandHeight) / 12; // yeees, its 12 not 13 (10% amplification) + bandHeight = constrain(bandHeight, 0, 255); // remove potential over/underflows + colorIndex = map(x, 0, cols-1, 0, 255); //WLEDMM + } + lastBandHeight = bandHeight; // remember BandHeight (left side) for next iteration + uint16_t barHeight = map2(bandHeight, 0, 255, 0, rows); // Now we map bandHeight to barHeight. do not subtract -1 from rows here + // WLEDMM end + + if (barHeight > rows) barHeight = rows; // WLEDMM map() can "overshoot" due to rounding errors if (barHeight > previousBarHeight[x]) previousBarHeight[x] = barHeight; //drive the peak up uint32_t ledColor = BLACK; + if ((! SEGMENT.check1) && !flatMode && (barHeight > 0)) { // use faster drawLine when single-color bars are needed + ledColor = SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0); + SEGMENT.drawLine(int(x), max(0,int(rows)-barHeight), int(x), int(rows-1), ledColor, false); // max(0, ...) to prevent negative Y + } else { for (int y=0; y < barHeight; y++) { if (SEGMENT.check1) //color_vertical / color bars toggle colorIndex = map(y, 0, rows-1, 0, 255); ledColor = SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0); - SEGMENT.setPixelColorXY(x, rows-1 - y, ledColor); - } - if (previousBarHeight[x] > 0) - SEGMENT.setPixelColorXY(x, rows - previousBarHeight[x], (SEGCOLOR(2) != BLACK) ? SEGCOLOR(2) : ledColor); + setFlatPixelXY(flatMode, x, rows-1 - y, ledColor, cols, rows, offset); + } } + if (!flatMode && (SEGMENT.intensity < 255) && (previousBarHeight[x] > 0) && (previousBarHeight[x] < rows)) // WLEDMM avoid "overshooting" into other segments - disable ripple pixels in 1D mode + setFlatPixelXY(flatMode, x, rows - previousBarHeight[x], (SEGCOLOR(2) != BLACK) ? SEGCOLOR(2) : ledColor, cols, rows, offset); if (rippleTime && previousBarHeight[x]>0) previousBarHeight[x]--; //delay/ripple effect } +#ifdef SR_DEBUG + if (!flatMode) { + // WLEDMM: abuse top left/right pixels for peak detection debugging + SEGMENT.setPixelColorXY(cols-1, 0, (samplePeak > 0) ? GREEN : BLACK); + if (samplePeak > 0) SEGMENT.setPixelColorXY(0, 0, GREEN); + // WLEDMM end + } +#endif return FRAMETIME; } // mode_2DGEQ() -static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# of bands,,,Color bars;!,,Peaks;!;2f;c1=255,c2=64,pal=11,si=0"; // Beatsin +static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ ☾@Fade speed,Ripple decay,# of bands,,,Color bars,Smooth bars ☾;!,,Peaks;!;12f;c1=255,c2=64,pal=11,si=0"; // Beatsin ///////////////////////// @@ -7181,7 +8325,7 @@ uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Wil const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); - int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); + int NUMB_BANDS = map2(SEGMENT.custom1, 0, 255, 1, 16); int barWidth = (cols / NUMB_BANDS); int bandInc = 1; if (barWidth == 0) { @@ -7190,12 +8334,9 @@ uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Wil bandInc = (NUMB_BANDS / cols); } - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + um_data_t *um_data = getAudioData(); + uint8_t fftResult[NUM_GEQ_CHANNELS] = {0}; + if (um_data->u_data != nullptr) memcpy(fftResult, um_data->u_data[2], sizeof(fftResult)); // WLEDMM buffer curent values if (SEGENV.call == 0) { SEGMENT.setUpLeds(); @@ -7203,14 +8344,14 @@ uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Wil } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64; - if (SEGENV.aux0 != secondHand) { // Triggered millis timing. + if((SEGMENT.speed > 254) || (SEGENV.aux0 != secondHand)) { // WLEDMM allow run run at full speed SEGENV.aux0 = secondHand; // display values of int b = 0; for (int band = 0; band < NUMB_BANDS; band += bandInc, b++) { int hue = fftResult[band % 16]; - int v = map(fftResult[band % 16], 0, 255, 10, 255); + int v = map2(fftResult[band % 16], 0, 255, 10, 255); for (int w = 0; w < barWidth; w++) { int xpos = (barWidth * b) + w; SEGMENT.setPixelColorXY(xpos, 0, CHSV(hue, 255, v)); @@ -7274,17 +8415,17 @@ uint16_t mode_2DAkemi(void) { const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); + if (SEGENV.call == 0) {SEGMENT.fill(BLACK);} + uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; const float lightFactor = 0.15f; const float normalFactor = 0.4f; - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - um_data = simulateSound(SEGMENT.soundSim); - } - uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + um_data_t *um_data = getAudioData(); + uint8_t fftResult[NUM_GEQ_CHANNELS] = {0}; + if (um_data->u_data != nullptr) memcpy(fftResult, um_data->u_data[2], sizeof(fftResult)); // WLEDMM buffer curent values float base = fftResult[0]/255.0f; //draw and color Akemi @@ -7306,7 +8447,7 @@ uint16_t mode_2DAkemi(void) { default: color = BLACK; break; } - if (SEGMENT.intensity > 128 && fftResult && fftResult[0] > 128) { //dance if base is high + if (SEGMENT.intensity > 128 && um_data && fftResult[0] > 128) { //dance if base is high SEGMENT.setPixelColorXY(x, 0, BLACK); SEGMENT.setPixelColorXY(x, y+1, color); } else @@ -7314,13 +8455,15 @@ uint16_t mode_2DAkemi(void) { } //add geq left and right - if (um_data && fftResult) { - for (int x=0; x < cols/8; x++) { - uint16_t band = x * cols/8; + if (um_data) { + int xMax = cols/8; + for (int x=0; x < xMax; x++) { + size_t band = map2(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands + uint32_t color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); band = constrain(band, 0, 15); uint16_t barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); - CRGB color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); + barHeight = constrain(barHeight, 0, (rows/2)+1); // map() may overshoot for (int y=0; y < barHeight; y++) { SEGMENT.setPixelColorXY(x, rows/2-y, color); SEGMENT.setPixelColorXY(cols-1-x, rows/2-y, color); @@ -7347,16 +8490,16 @@ uint16_t mode_2Ddistortionwaves() { uint8_t w = 2; - uint16_t a = millis()/32; + uint16_t a = strip.now/32; uint16_t a2 = a/2; uint16_t a3 = a/3; - uint16_t cx = beatsin8(10-speed,0,cols-1)*scale; - uint16_t cy = beatsin8(12-speed,0,rows-1)*scale; - uint16_t cx1 = beatsin8(13-speed,0,cols-1)*scale; - uint16_t cy1 = beatsin8(15-speed,0,rows-1)*scale; - uint16_t cx2 = beatsin8(17-speed,0,cols-1)*scale; - uint16_t cy2 = beatsin8(14-speed,0,rows-1)*scale; + uint16_t cx = beatsin8_t(10-speed,0,cols-1)*scale; + uint16_t cy = beatsin8_t(12-speed,0,rows-1)*scale; + uint16_t cx1 = beatsin8_t(13-speed,0,cols-1)*scale; + uint16_t cy1 = beatsin8_t(15-speed,0,rows-1)*scale; + uint16_t cx2 = beatsin8_t(17-speed,0,cols-1)*scale; + uint16_t cy2 = beatsin8_t(14-speed,0,rows-1)*scale; uint16_t xoffs = 0; for (int x = 0; x < cols; x++) { @@ -7366,17 +8509,17 @@ uint16_t mode_2Ddistortionwaves() { for (int y = 0; y < rows; y++) { yoffs += scale; - byte rdistort = cos8((cos8(((x<<3)+a )&255)+cos8(((y<<3)-a2)&255)+a3 )&255)>>1; - byte gdistort = cos8((cos8(((x<<3)-a2)&255)+cos8(((y<<3)+a3)&255)+a+32 )&255)>>1; - byte bdistort = cos8((cos8(((x<<3)+a3)&255)+cos8(((y<<3)-a) &255)+a2+64)&255)>>1; + byte rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3 )&255)>>1; + byte gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1; + byte bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((y<<3)-a) &255)+a2+64)&255)>>1; byte valueR = rdistort+ w* (a- ( ((xoffs - cx) * (xoffs - cx) + (yoffs - cy) * (yoffs - cy))>>7 )); byte valueG = gdistort+ w* (a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 )); byte valueB = bdistort+ w* (a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 )); - valueR = gamma8(cos8(valueR)); - valueG = gamma8(cos8(valueG)); - valueB = gamma8(cos8(valueB)); + valueR = gamma8(cos8_t(valueR)); + valueG = gamma8(cos8_t(valueG)); + valueB = gamma8(cos8_t(valueB)); SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); } @@ -7411,21 +8554,29 @@ uint16_t mode_2Dsoap() { // init if (SEGENV.call == 0) { - SEGMENT.setUpLeds(); + random16_set_seed(535); //WLEDMM SuperSync + SEGENV.setUpLeds(); + SEGMENT.fill(BLACK); *noise32_x = random16(); *noise32_y = random16(); *noise32_z = random16(); - } else { - *noise32_x += mov; - *noise32_y += mov; - *noise32_z += mov; } + //WLEDMM: changing noise calculation for SuperSync to make it deterministic using strip.now + uint32_t noise32_x_MM = *noise32_x; + uint32_t noise32_y_MM = *noise32_y; + uint32_t noise32_z_MM = *noise32_z; + + //WLEDMM SuperSync + noise32_x_MM = *noise32_x + mov * strip.now / 100; //10 fps (original 20-40 fps, depending on realized fps) + noise32_y_MM = *noise32_y + mov * strip.now / 100; + noise32_z_MM = *noise32_z + mov * strip.now / 100; + for (int i = 0; i < cols; i++) { int32_t ioffset = scale32_x * (i - cols / 2); for (int j = 0; j < rows; j++) { int32_t joffset = scale32_y * (j - rows / 2); - uint8_t data = inoise16(*noise32_x + ioffset, *noise32_y + joffset, *noise32_z) >> 8; + uint8_t data = inoise16(noise32_x_MM + ioffset, noise32_y_MM + joffset, noise32_z_MM) >> 8; //WLEDMM SuperSync noise3d[XY(i,j)] = scale8(noise3d[XY(i,j)], smoothness) + scale8(data, 255 - smoothness); } } @@ -7503,12 +8654,13 @@ static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; //Octopus (https://editor.soulmatelights.com/gallery/671-octopus) //Stepko and Sutaburosu // adapted for WLED by @blazoncek +// RadialWave mode added by @softhack007, based on https://editor.soulmatelights.com/gallery/1090-radialwave uint16_t mode_2Doctopus() { if (!strip.isMatrix) return mode_static(); // not a 2D set-up const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); - const uint8_t mapp = 180 / MAX(cols,rows); + const uint16_t mapp = max(1, 180 / MAX(cols,rows)); // WLEDMM make sure this value is not 0 typedef struct { uint8_t angle; @@ -7522,39 +8674,68 @@ uint16_t mode_2Doctopus() { uint8_t *offsX = reinterpret_cast(SEGENV.data + dataSize); uint8_t *offsY = reinterpret_cast(SEGENV.data + dataSize + 1); + //WLEDMM add SuperSync control + uint16_t xStart, xEnd, yStart, yEnd; + if (SEGMENT.check1) { //Master (sync on needs to show the whole effect, children only their first panel) + xStart = strip.panel[0].xOffset; + xEnd = strip.panel[0].xOffset + strip.panel[0].width; + yStart = strip.panel[0].yOffset; + yEnd = strip.panel[0].yOffset + strip.panel[0].height; + } + else { + xStart = 0; + xEnd = cols; + yStart = 0; + yEnd = rows; + } + // re-init if SEGMENT dimensions or offset changed if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows || SEGMENT.custom1 != *offsX || SEGMENT.custom2 != *offsY) { - SEGENV.step = 0; // t SEGENV.aux0 = cols; SEGENV.aux1 = rows; *offsX = SEGMENT.custom1; *offsY = SEGMENT.custom2; - const uint8_t C_X = cols / 2 + (SEGMENT.custom1 - 128)*cols/255; - const uint8_t C_Y = rows / 2 + (SEGMENT.custom2 - 128)*rows/255; - for (int x = 0; x < cols; x++) { - for (int y = 0; y < rows; y++) { - rMap[XY(x, y)].angle = 40.7436f * atan2f(y - C_Y, x - C_X); // avoid 128*atan2()/PI - rMap[XY(x, y)].radius = hypotf(x - C_X, y - C_Y) * mapp; //thanks Sutaburosu + const uint16_t C_X = cols / 2 + (SEGMENT.custom1 - 128)*cols/255; + const uint16_t C_Y = rows / 2 + (SEGMENT.custom2 - 128)*rows/255; + for (int x = xStart; x < xEnd; x++) { + for (int y = yStart; y < yEnd; y++) { + int dx = (x - C_X); + int dy = (y - C_Y); + rMap[XY(x, y)].angle = int(40.7436f * atan2f(dy, dx)); // avoid 128*atan2()/PI + //rMap[XY(x, y)].radius = hypotf(x - C_X, y - C_Y) * mapp; //thanks Sutaburosu + rMap[XY(x, y)].radius = sqrtf(dx * dx + dy * dy) * mapp; //thanks Sutaburosu } } } - SEGENV.step += SEGMENT.speed / 32 + 1; // 1-4 range - for (int x = 0; x < cols; x++) { - for (int y = 0; y < rows; y++) { + // WLEDMM SuperSync + SEGENV.step = (strip.now * (SEGMENT.speed+15)) / 33 / 25; // WLEDMM 40fps; speed range 0.4 ... 8 + + // speed of motion and color change + uint32_t colorSpeed = SEGENV.step / 2; + uint32_t octoSpeed; + if (SEGMENT.check3) octoSpeed = 4*SEGENV.step/5; // 4/5 = 0.8 for RadialWave mode + else octoSpeed = SEGENV.step/2; // 1/2 = 0.5 for Octopus mode + + for (int x = xStart; x < xEnd; x++) { + for (int y = yStart; y < yEnd; y++) { byte angle = rMap[XY(x,y)].angle; byte radius = rMap[XY(x,y)].radius; - //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); - uint16_t intensity = sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); - intensity = map(intensity*intensity, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display - CRGB c = ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity); + //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); + uint16_t intensity; + if (SEGMENT.check3) + intensity = sin8_t(octoSpeed + sin8_t(octoSpeed - radius) + angle * (SEGMENT.custom3/4+1)); // RadialWave + else + intensity = sin8_t(sin8_t((angle * 4 - radius) / 4 + octoSpeed) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); // Octopus + //intensity = map(intensity*intensity, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display + intensity = (intensity * intensity) / 255; // WLEDMM same as above, but faster and a bit more accurate + CRGB c = ColorFromPalette(SEGPALETTE, colorSpeed - radius, intensity); SEGMENT.setPixelColorXY(x, y, c); } } return FRAMETIME; } -static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offset Y,Legs;;!;2;"; - +static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offset Y,Legs, SuperSync,,RadialWave ☾;;!;2;"; //Waving Cell //@Stepko (https://editor.soulmatelights.com/gallery/1704-wavingcells) @@ -7565,27 +8746,251 @@ uint16_t mode_2Dwavingcell() { const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); - uint32_t t = millis()/(257-SEGMENT.speed); + uint32_t t = strip.now/(257-SEGMENT.speed); uint8_t aX = SEGMENT.custom1/16 + 9; uint8_t aY = SEGMENT.custom2/16 + 1; uint8_t aZ = SEGMENT.custom3 + 1; for (int x = 0; x < cols; x++) for (int y = 0; y (&(SEGENV.aux0)); // *projector is an alias for aux0 (uint16_t) + int16_t *projector_dir = reinterpret_cast(&(SEGENV.aux1)); // *projector_dir is an alias for aux1 (uint16_t) + + if (SEGENV.call == 0) { + *projector = 0; + *projector_dir = 1; + SEGMENT.setUpLeds(); // WLEDMM use lossless getPixelColor() + SEGMENT.fill(BLACK); + } else { + if (SEGENV.call % map(SEGMENT.speed,0,255,10,1) == 0) *projector += *projector_dir; + if (*projector >= cols) *projector_dir = -1; + if (*projector <= 0) *projector_dir = 1; + } + *projector = constrain(*projector, 0, cols-1); // make sure we don't walk out of range + + SEGMENT.fill(BLACK); + + uint32_t ledColorTemp; + const int NUM_BANDS = max(2, min(cols, int(map2(SEGMENT.custom3, 0, 31, 1, NUM_GEQ_CHANNELS)))); // custom3 is 0..31 - constrain NUM_BANDS between 2(for split) and cols (for small width segments) + uint_fast8_t split = map2(*projector,0,SEGMENT.virtualWidth(),0,(NUM_BANDS - 1)); + uint16_t horizon = map2(SEGMENT.custom1,0,255,rows-1,0); + uint8_t depth = SEGMENT.custom2; // depth of perspective. 255 = infinite ("laser") + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + uint8_t heights[NUM_GEQ_CHANNELS] = { 0 }; + const uint8_t maxHeight = roundf(float(rows) * ((rows<18) ? 0.75f : 0.85f)); // slightly reduce bar height on small panels + for (int i=0; i 1) { + ledColorTemp = color_fade(ledColor,32,true); + int pPos = max(0, linex+(cols/NUM_BANDS)-1); + for (int y = (i 0) SEGMENT.drawLine(pPos,rows-y-1,*projector,horizon,ledColorTemp,false,depth); // right side perspective + } + + ledColorTemp = color_fade(ledColor,128,true); + if (heights[i] < rows-horizon && (*projector <=linex || *projector >= pPos)) { // draw if above horizon AND not directly under projector (special case later) + if (rows-heights[i] > 1) { // sanity check - avoid negative Y + for (uint_fast8_t x=linex; x<=pPos;x++) { + bool doSoft = SEGMENT.check2 && ((x==linex) || (x==pPos)); // only first and last line need AA + SEGMENT.drawLine(x,rows-heights[i]-2,*projector,horizon,ledColorTemp,doSoft,depth); // top perspective + } + } + } + } + } + + + for (int i=(NUM_BANDS - 1); i>split; i--) { // paint left vertical faces and top - RIGHT to LEFT + uint16_t colorIndex = map(cols/NUM_BANDS*i, 0, cols-1, 0, 255); + uint32_t ledColor = SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0); + int linex = i*(cols/NUM_BANDS); + int pPos = max(0, linex+(cols/NUM_BANDS)-1); + + if (heights[i] > 1) { + ledColorTemp = color_fade(ledColor,32,true); + for (uint_fast8_t y = (i>0) ? heights[i-1] : 0; y <= heights[i]; y++) { // don't bother drawing what we'll hide anyway + if (rows-y > 0) SEGMENT.drawLine(linex,rows-y-1,*projector,horizon,ledColorTemp,false,depth); // left side perspective + } + + ledColorTemp = color_fade(ledColor,128,true); + if (heights[i] < rows-horizon && (*projector <=linex || *projector >= pPos)) { // draw if above horizon AND not directly under projector (special case later) + if (rows-heights[i] > 1) { // sanity check - avoid negative Y + for (uint_fast8_t x=linex; x<=pPos;x++) { + bool doSoft = SEGMENT.check2 && ((x==linex) || (x==pPos)); // only first and last line need AA + SEGMENT.drawLine(x,rows-heights[i]-2,*projector,horizon,ledColorTemp,doSoft,depth); // top perspective + } + } + } + } + } + + + for (int i=0; i=linex && *projector <= pPos) { // special case when top perspective is directly under the projector + if ((heights[i] > 1) && (heights[i] < rows-horizon) && (rows-heights[i] > 1)) { + ledColorTemp = color_fade(ledColor,128,true); + for (uint_fast8_t x=linex; x<=pPos;x++) { + bool doSoft = SEGMENT.check2 && ((x==linex) || (x==pPos)); // only first and last line need AA + SEGMENT.drawLine(x,rows-heights[i]-2,*projector,horizon,ledColorTemp,doSoft,depth); // top perspective + } + } + } + + if ((heights[i] > 1) && (rows-heights[i] > 0)) { + ledColorTemp = color_fade(ledColor,SEGMENT.intensity,true); + for (uint_fast8_t x=linex; x rows-horizon) { + if (SEGMENT.intensity == 0) ledColorTemp = color_fade(ledColor,32,true); // match side fill if we're in blackout mode + SEGMENT.drawLine(linex,rows-heights[i]-1,linex+(cols/NUM_BANDS)-1,rows-heights[i]-1,ledColorTemp); // top line to simulate hidden top fill + } + + if ((SEGMENT.check1) && (rows-heights[i] > 1)) { + SEGMENT.drawLine(linex, rows-1,linex,rows-heights[i]-1,ledColor); // left side line + SEGMENT.drawLine(linex+(cols/NUM_BANDS)-1,rows-1,linex+(cols/NUM_BANDS)-1,rows-heights[i]-1,ledColor); // right side line + SEGMENT.drawLine(linex, rows-heights[i]-2,linex+(cols/NUM_BANDS)-1,rows-heights[i]-2,ledColor); // top line + SEGMENT.drawLine(linex, rows-1,linex+(cols/NUM_BANDS)-1,rows-1,ledColor); // bottom line + } + } + } + return FRAMETIME; +} +static const char _data_FX_MODE_GEQLASER[] PROGMEM = "GEQ 3D ☾@Speed,Front Fill,Horizon,Depth,Num Bands,Borders,Soft,;!,,Peaks;!;2f;sx=255,ix=228,c1=255,c2=255,c3=15,pal=11"; #endif // WLED_DISABLE_2D +/* + @title MoonModules WLED - Painbrush Effect + @file included in FX.cpp + @repo https://github.com/MoonModules/WLED, submit changes to this file as PRs to MoonModules/WLED + @Authors https://github.com/MoonModules/WLED/commits/mdev/ + @Copyright Ā© 2024 Github MoonModules Commit Authors (contact moonmodules@icloud.com for details) + @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + + This function is part of the MoonModules WLED fork also known as "WLED-MM". + WLED-MM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License + as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + WLED-MM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with WLED-MM. If not, see . +*/ + +/////////////////////// +// 2D Paintbrush // +/////////////////////// +uint16_t mode_2DPaintbrush() { + + // Author: @TroyHacks + // @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + + if (!strip.isMatrix) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + if (!SEGENV.allocateData(4)) return mode_static(); //allocation failed + + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(BLACK); + SEGENV.aux0 = 0; + } + + bool phase_chaos = SEGMENT.check3; + bool soft = SEGMENT.check2; + bool color_chaos = SEGMENT.check1; + CRGB color; + + byte numLines = map8(SEGMENT.intensity,1,16); + + SEGENV.aux0++; // hue + SEGMENT.fadeToBlackBy(map8(SEGENV.custom1,10,128)); + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + SEGENV.aux1 = phase_chaos?random8():0; + + for (size_t i = 0; i < numLines; i++) { + byte bin = map(i,0,numLines,0,15); + + byte x1 = beatsin8_t(max(16,int(SEGMENT.speed))/16*1 + fftResult[0]/16, 0, (cols-1), fftResult[bin], SEGENV.aux1); + byte x2 = beatsin8_t(max(16,int(SEGMENT.speed))/16*2 + fftResult[0]/16, 0, (cols-1), fftResult[bin], SEGENV.aux1); + byte y1 = beatsin8_t(max(16,int(SEGMENT.speed))/16*3 + fftResult[0]/16, 0, (rows-1), fftResult[bin], SEGENV.aux1); + byte y2 = beatsin8_t(max(16,int(SEGMENT.speed))/16*4 + fftResult[0]/16, 0, (rows-1), fftResult[bin], SEGENV.aux1); + + int length = sqrtf((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)); + length = map8(fftResult[bin],0,length); + + if (length > max(1,int(SEGMENT.custom3))) { + if (color_chaos) { + color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); + } else { + uint16_t colorIndex = map(i,0,numLines,0,255); + color = SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0); + } + SEGMENT.drawLine(x1,y1,x2,y2,color,soft,length); + } + } + return FRAMETIME; +} // mode_2DPaintbrush() +static const char _data_FX_MODE_2DPAINTBRUSH[] PROGMEM = "Paintbrush ☾@Oscillator Offset,# of lines,Fade Rate,,Min Length,Color Chaos,Anti-aliasing,Phase Chaos;!,,Peaks;!;2f;sx=160,ix=255,c1=80,c2=255,c3=0,pal=72,o1=0,o2=1,o3=0"; ////////////////////////////////////////////////////////////////////////////////////////// // mode data static const char _data_RESERVED[] PROGMEM = "RSVD"; // add (or replace reserved) effect mode and data into vector -// use id==255 to find unallocatd gaps (with "Reserved" data string) +// use id==255 to find unallocated gaps (with "Reserved" data string) // if vector size() is smaller than id (single) data is appended at the end (regardless of id) void WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { if (id == 255) { // find empty slot @@ -7660,6 +9065,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); @@ -7672,6 +9078,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); + addEffect(FX_MODE_MULTI_COMET_AR, &mode_multi_comet_ar, _data_FX_MODE_MULTI_COMET_AR); addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); @@ -7734,6 +9141,7 @@ void WS2812FX::setupEffectData() { // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); + addEffect(FX_MODE_PARTYJERK, &mode_partyjerk, _data_FX_MODE_PARTYJERK); addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); @@ -7767,6 +9175,11 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + // --- WLEDSR experimental 1D audio enhanced + addEffect(FX_MODE_POPCORN_AR, &mode_popcorn_audio, _data_FX_MODE_POPCORN_AR); + addEffect(FX_MODE_STARBURST_AR, &mode_starburst_audio, _data_FX_MODE_STARBURST_AR); + addEffect(FX_MODE_FIREWORKS_AR, &mode_fireworks_audio, _data_FX_MODE_FIREWORKS_AR); + // --- 2D effects --- #ifndef WLED_DISABLE_2D addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); @@ -7813,8 +9226,14 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); + addEffect(FX_MODE_2DSNOWFALL, &mode_2DSnowFall, _data_FX_MODE_2DSNOWFALL); addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + + addEffect(FX_MODE_GEQLASER, &mode_GEQLASER, _data_FX_MODE_GEQLASER); // audio + + addEffect(FX_MODE_2DPAINTBRUSH, &mode_2DPaintbrush, _data_FX_MODE_2DPAINTBRUSH); // audio + #endif // WLED_DISABLE_2D } diff --git a/wled00/FX.h b/wled00/FX.h index 19b1fc4ac3..ddbd014610 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -2,24 +2,6 @@ WS2812FX.h - Library for WS2812 LED effects. Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) - Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. Modified for WLED */ @@ -31,6 +13,10 @@ #include "const.h" +bool canUseSerial(void); // WLEDMM implemented in wled_serial.cpp +void strip_wait_until_idle(String whoCalledMe); // WLEDMM implemented in FX_fcn.cpp +bool strip_uses_global_leds(void) __attribute__((pure)); // WLEDMM implemented in FX_fcn.cpp + #define FASTLED_INTERNAL //remove annoying pragma messages #define USE_GET_MILLISECOND_TIMER #include "FastLED.h" @@ -57,10 +43,25 @@ #endif /* Not used in all effects yet */ +#define FPS_UNLIMITED 250 +#define FPS_UNLIMITED_AC 0 // WLEDMM upstream uses "0 fps" for unlimited. We support both ways +#if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_FASTPATH) // WLEDMM go faster on ESP32 +#define FRAMETIME strip.getFrameTime() +#define MIN_SHOW_DELAY (max(2, (_frametime*5)/8)) // WLEDMM support higher framerates (up to 250fps) -- 5/8 = 62% +#define WLED_FPS 120 +#define WLED_FPS_SLOW 60 +#define FRAMETIME_FIXED 24 // used in Blurz, Freqmap, Scrolling text, Colortwinkles, Candle +//#define FRAMETIME_FIXED (strip.getFrameTime() < 10 ? 12 : 24) +#define FRAMETIME_FIXED_SLOW (15) // = 66 FPS => 1000/66 // used in Solid +#else #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) -//#define FRAMETIME _frametime +#define WLED_FPS_SLOW 42 +#define FRAMETIME_FIXED_SLOW (1000/WLED_FPS_SLOW) #define FRAMETIME strip.getFrameTime() +//#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) // Upstream legacy +#define MIN_SHOW_DELAY (_frametime < 16 ? (_frametime <8? (_frametime <7? (_frametime <6 ? 2 :3) :4) : 8) : 15) // WLEDMM support higher framerates (up to 250fps) +#endif /* each segment uses 52 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ @@ -72,28 +73,28 @@ #ifndef MAX_NUM_SEGMENTS #define MAX_NUM_SEGMENTS 32 #endif + #ifndef MAX_SEGMENT_DATA #if defined(ARDUINO_ARCH_ESP32S2) #define MAX_SEGMENT_DATA 24576 #else #define MAX_SEGMENT_DATA 32767 #endif + #endif #endif /* How much data bytes each segment should max allocate to leave enough space for other segments, assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ #define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / strip.getMaxSegments()) -#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) - #define NUM_COLORS 3 /* number of colors per segment */ #define SEGMENT strip._segments[strip.getCurrSegmentId()] #define SEGENV strip._segments[strip.getCurrSegmentId()] //#define SEGCOLOR(x) strip._segments[strip.getCurrSegmentId()].currentColor(x, strip._segments[strip.getCurrSegmentId()].colors[x]) //#define SEGLEN strip._segments[strip.getCurrSegmentId()].virtualLength() #define SEGCOLOR(x) strip.segColor(x) /* saves us a few kbytes of code */ -#define SEGPALETTE strip._currentPalette +#define SEGPALETTE Segment::getCurrentPalette() #define SEGLEN strip._virtualSegmentLength /* saves us a few kbytes of code */ -#define SPEED_FORMULA_L (5U + (50U*(255U - SEGMENT.speed))/SEGLEN) +#define SPEED_FORMULA_L (4U + (50U*(255U - SEGMENT.speed))/min(SEGLEN, uint16_t(512))) // WLEDMM limiting the formula to 512 virtual pixels // some common colors #define RED (uint32_t)0xFF0000 @@ -107,6 +108,10 @@ #define PURPLE (uint32_t)0x400080 #define ORANGE (uint32_t)0xFF3000 #define PINK (uint32_t)0xFF1493 +#define GREY (uint32_t)0x808080 +#define GRAY GREY +#define DARKGREY (uint32_t)0x333333 +#define DARKGRAY DARKGREY #define ULTRAWHITE (uint32_t)0xFFFFFFFF #define DARKSLATEGRAY (uint32_t)0x2F4F4F #define DARKSLATEGREY (uint32_t)0x2F4F4F @@ -176,7 +181,7 @@ #define FX_MODE_FIRE_FLICKER 45 #define FX_MODE_GRADIENT 46 #define FX_MODE_LOADING 47 -// #define FX_MODE_POLICE 48 // removed in 0.14! +#define FX_MODE_ROLLINGBALLS 48 //was Police before 0.14 #define FX_MODE_FAIRY 49 //was Police All prior to 0.13.0-b6 (use "Two Dots" with Red/Blue and full intensity) #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) @@ -319,14 +324,30 @@ #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#define FX_MODE_ARTIFX 187 //WLEDMM ARTIFX +#define FX_MODE_PARTYJERK 188 + +// Experimental Audioresponsive modes from WLED-SR +// #define FX_MODE_3DSphereMove 189 // experimental WLED-SR "cube" mode +#define FX_MODE_POPCORN_AR 190 // WLED-SR audioreactive popcorn +#define FX_MODE_MULTI_COMET_AR 191 // WLED-SR audioreactive multi-comet +#define FX_MODE_STARBURST_AR 192 // WLED-SR audioreactive fireworks starburst +// #define FX_MODE_PALETTE_AR 193 // WLED-SR audioreactive palette +#define FX_MODE_FIREWORKS_AR 194 // WLED-SR audioreactive fireworks +#define FX_MODE_GEQLASER 195 // WLED-MM GEQ Laser +#define FX_MODE_2DPAINTBRUSH 196 // WLED-MM Paintbrush +#define FX_MODE_2DSNOWFALL 197 // WLED-MM Snowfall +#define MODE_COUNT 198 typedef enum mapping1D2D { M12_Pixels = 0, M12_pBar = 1, M12_pArc = 2, - M12_pCorner = 3 + M12_pCorner = 3, + M12_jMap = 4, //WLEDMM jMap + M12_sCircle = 5, //WLEDMM Circle + M12_sBlock = 6, //WLEDMM Block + M12_sPinwheel = 7 //WLEDMM Pinwheel } mapping1D2D_t; // segment, 72 bytes @@ -359,6 +380,8 @@ typedef struct Segment { }; uint8_t grouping, spacing; uint8_t opacity; + uint8_t lastBri; // WLEDMM optimization for black-to-black "transitions" + bool needsBlank; // WLEDMM indicates that Segment needs to be blanked (due to change of mirror / reverse / transpose / spacing) uint32_t colors[NUM_COLORS]; uint8_t cct; //0==1900K, 255==10091K uint8_t custom1, custom2; // custom FX parameters/sliders @@ -368,9 +391,9 @@ typedef struct Segment { bool check2 : 1; // checkmark 2 bool check3 : 1; // checkmark 3 }; - uint8_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows - uint8_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows - char *name; + uint16_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows, but we cannot be sure. + uint16_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows, but we cannot be sure. + char *name = nullptr; // WLEDMM initialize to nullptr // runtime data unsigned long next_time; // millis() of next update @@ -378,10 +401,12 @@ typedef struct Segment { uint32_t call; // call counter uint16_t aux0; // custom var uint16_t aux1; // custom var - byte* data; // effect data pointer - CRGB* leds; // local leds[] array (may be a pointer to global) + byte* data = nullptr; // effect data pointer // WLEDMM initialize to nullptr + CRGB* ledsrgb = nullptr; // local leds[] array (may be a pointer to global) //WLEDMM rename to ledsrgb to search on them (temp?), and initialize to nullptr + size_t ledsrgbSize; //WLEDMM static CRGB *_globalLeds; // global leds[] array static uint16_t maxWidth, maxHeight; // these define matrix width & height (max. segment dimensions) + void *jMap = nullptr; //WLEDMM jMap private: union { @@ -394,8 +419,27 @@ typedef struct Segment { uint8_t _reserved : 4; }; }; - uint16_t _dataLen; - static uint16_t _usedSegmentData; + size_t _dataLen; // WLEDMM uint16_t is too small + static size_t _usedSegmentData; // WLEDMM uint16_t is too small + void setPixelColorXY_fast(int x, int y,uint32_t c, uint32_t scaled_col, int cols, int rows) const; // set relative pixel within segment with color - faster, but no error checking!!! + + bool _isSimpleSegment = false; // simple = no grouping or spacing - mirror, transpose or reverse allowed + bool _isSuperSimpleSegment = false; // superSimple = no grouping or spacing, no mirror - only transpose or reverse allowed +#ifdef WLEDMM_FASTPATH + // WLEDMM cache some values that won't change while drawing a frame + bool _isValid2D = false; + uint8_t _brightness = 255; // final pixel brightness - including transitions and segment opacity + uint16_t _2dWidth = 0; // virtualWidth + uint16_t _2dHeight = 0; // virtualHeight + uint16_t _virtuallength = 0; // virtualLength + + void setPixelColorXY_slow(int x, int y, uint32_t c); // set relative pixel within segment with color - full slow version +#else + void setPixelColorXY_slow(int x, int y, uint32_t c) { setPixelColorXY(x,y,c); } // not FASTPATH - slow is the normal +#endif + + // perhaps this should be per segment, not static + static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) // transition data, valid only if transitional==true, holds values during transition struct Transition { @@ -403,12 +447,12 @@ typedef struct Segment { uint8_t _briT; // temporary brightness uint8_t _cctT; // temporary CCT CRGBPalette16 _palT; // temporary palette - uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 belnds possible) + uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) uint8_t _modeP; // previous mode/effect //uint16_t _aux0, _aux1; // previous mode/effect runtime data //uint32_t _step, _call; // previous mode/effect runtime data //byte *_data; // previous mode/effect runtime data - uint32_t _start; + unsigned long _start; // must accommodate millis() uint16_t _dur; Transition(uint16_t dur=750) : _briT(255) @@ -446,6 +490,8 @@ typedef struct Segment { grouping(1), spacing(0), opacity(255), + lastBri(255), + needsBlank(false), colors{DEFAULT_COLOR,BLACK,BLACK}, cct(127), custom1(DEFAULT_C1), @@ -463,7 +509,8 @@ typedef struct Segment { aux0(0), aux1(0), data(nullptr), - leds(nullptr), + ledsrgb(nullptr), + ledsrgbSize(0), //WLEDMM _capabilities(0), _dataLen(0), _t(nullptr) @@ -480,16 +527,28 @@ typedef struct Segment { Segment(Segment &&orig) noexcept; // move constructor ~Segment() { - //#ifdef WLED_DEBUG - //Serial.print(F("Destroying segment:")); - //if (name) Serial.printf(" %s (%p)", name, name); - //if (data) Serial.printf(" %d (%p)", (int)_dataLen, data); - //if (leds) Serial.printf(" [%u]", length()*sizeof(CRGB)); - //Serial.println(); - //#endif - if (!Segment::_globalLeds && leds) free(leds); - if (name) delete[] name; - if (_t) delete _t; + #ifdef WLED_DEBUG + if(canUseSerial()) { + Serial.print(F("Destroying segment:")); + if (name) Serial.printf(" name=%s (%p)", name, name); + if (data) Serial.printf(" dataLen=%d (%p)", (int)_dataLen, data); + if (ledsrgb) Serial.printf(" [%sledsrgb %u bytes]", Segment::_globalLeds ? "global ":"",length()*sizeof(CRGB)); + if (strip_uses_global_leds() == true) Serial.println((Segment::_globalLeds != nullptr) ? F(" using global buffer.") : F(", using global buffer but Segment::_globalLeds is NULL!!")); + Serial.println(); + #ifdef ARDUINO_ARCH_ESP32 + Serial.flush(); + #endif + } + #endif + + // WLEDMM only delete segments when they are not in use + #ifdef ARDUINO_ARCH_ESP32 + strip_wait_until_idle("~Segment()"); + #endif + + if ((Segment::_globalLeds == nullptr) && !strip_uses_global_leds() && (ledsrgb != nullptr)) {free(ledsrgb); ledsrgb = nullptr;} // WLEDMM we need "!strip_uses_global_leds()" to avoid crashes (#104) + if (name) { delete[] name; name = nullptr; } + if (_t) { transitional = false; delete _t; _t = nullptr; } deallocateData(); } @@ -497,7 +556,7 @@ typedef struct Segment { Segment& operator= (Segment &&orig) noexcept; // move assignment #ifdef WLED_DEBUG - size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0) + (!Segment::_globalLeds && leds?sizeof(CRGB)*length():0); } + size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0) + (!Segment::_globalLeds && ledsrgb?sizeof(CRGB)*length():0); } #endif inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } @@ -507,30 +566,34 @@ typedef struct Segment { inline bool hasRGB(void) const { return _isRGB; } inline bool hasWhite(void) const { return _hasW; } inline bool isCCT(void) const { return _isCCT; } - inline uint16_t width(void) const { return stop - start; } // segment width in physical pixels (length if 1D) - inline uint16_t height(void) const { return stopY - startY; } // segment height (if 2D) in physical pixels - inline uint16_t length(void) const { return width() * height(); } // segment length (count) in physical pixels - inline uint16_t groupLength(void) const { return grouping + spacing; } + inline uint16_t width(void) const { return (stop > start) ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) + inline uint16_t height(void) const { return (stopY > startY) ? (stopY - startY) : 0; } // segment height (if 2D) in physical pixels // WLEDMM make sure its always > 0 + inline uint16_t length(void) const { return width() * height(); } // segment length (count) in physical pixels // WLEDMM fishy ... need to double-check if this is correct + inline uint16_t groupLength(void) const { return max(1, grouping + spacing); } // WLEDMM length = 0 could lead to div/0 in virtualWidth() and virtualHeight() inline uint8_t getLightCapabilities(void) const { return _capabilities; } - static uint16_t getUsedSegmentData(void) { return _usedSegmentData; } + static size_t getUsedSegmentData(void) { return _usedSegmentData; } // WLEDMM size_t static void addUsedSegmentData(int len) { _usedSegmentData += len; } + void allocLeds(); //WLEDMM + inline static const CRGBPalette16 &getCurrentPalette(void) { return Segment::_currentPalette; } + void setUp(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1); bool setColor(uint8_t slot, uint32_t c); //returns true if changed void setCCT(uint16_t k); void setOpacity(uint8_t o); void setOption(uint8_t n, bool val); - void setMode(uint8_t fx, bool loadDefaults = false); + void setMode(uint8_t fx, bool loadDefaults = false, bool sliderDefaultsOnly = false); void setPalette(uint8_t pal); uint8_t differs(Segment& b) const; void refreshLightCapabilities(void); // runtime data functions - inline uint16_t dataSize(void) const { return _dataLen; } + inline size_t dataSize(void) const { return _dataLen; } bool allocateData(size_t len); void deallocateData(void); void resetIfRequired(void); + void startFrame(void); // cache a few values that don't change while an effect is drawing /** * Flags that before the next effect is calculated, * the internal segment state should be reset. @@ -538,108 +601,226 @@ typedef struct Segment { * Safe to call from interrupts and network requests. */ inline void markForReset(void) { reset = true; } // setOption(SEG_OPTION_RESET, true) + inline void markForBlank(void) { needsBlank = true; } // WLEDMM serialize "blank" requests, avoid parallel drawing from different task void setUpLeds(void); // set up leds[] array for loseless getPixelColor() // transition functions void startTransition(uint16_t dur); // transition has to start before actual segment values change void handleTransition(void); - uint16_t progress(void); //transition progression between 0-65535 - uint8_t currentBri(uint8_t briNew, bool useCct = false); + // transition progression between 0-65535 + [[gnu::hot]] inline uint16_t progress() const { + if (!transitional || !_t) return 0xFFFFU; + unsigned long timeNow = millis(); + if (timeNow - _t->_start > _t->_dur || _t->_dur == 0) return 0xFFFFU; + return (timeNow - _t->_start) * 0xFFFFU / _t->_dur; + } + + // WLEDMM method inlined for speed (its called at each setPixelColor) + [[gnu::hot]] inline uint8_t currentBri(uint8_t briNew, bool useCct = false) const { + uint32_t prog = progress(); + if (prog < 0xFFFFU) { // progress() < 0xFFFFU implies that _t is valid (see progress() function) + if (useCct) return ((briNew * prog) + _t->_cctT * (0xFFFFU - prog)) >> 16; + else return ((briNew * prog) + _t->_briT * (0xFFFFU - prog)) >> 16; + } else { + return (useCct ? briNew : (on ? briNew : 0)); // WLEDMM aligned with upstream + } + } + uint8_t currentMode(uint8_t modeNew); uint32_t currentColor(uint8_t slot, uint32_t colorNew); - CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); - CRGBPalette16 ¤tPalette(CRGBPalette16 &tgt, uint8_t paletteID); + CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal) const; + void setCurrentPalette(void); // 1D strip - uint16_t virtualLength(void) const; + uint16_t calc_virtualLength(void) const; +#ifndef WLEDMM_FASTPATH + inline uint16_t virtualLength(void) const {return calc_virtualLength();} +#else + inline uint16_t virtualLength(void) const {return _virtuallength;} +#endif void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color - void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } // automatically inline - void setPixelColor(int n, CRGB c) { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } // automatically inline + inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } // automatically inline + inline void setPixelColor(int n, CRGB c) { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } // automatically inline void setPixelColor(float i, uint32_t c, bool aa = true); - void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); } - void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } - uint32_t getPixelColor(int i); + inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); } + inline void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } + uint32_t __attribute__((pure)) getPixelColor(int i) const; // WLEDMM attribute added // 1D support functions (some implement 2D as well) - void blur(uint8_t); + void blur(uint8_t, bool smear = false); void fill(uint32_t c); void fade_out(uint8_t r); void fadeToBlackBy(uint8_t fadeBy); void blendPixelColor(int n, uint32_t color, uint8_t blend); - void blendPixelColor(int n, CRGB c, uint8_t blend) { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } + inline void blendPixelColor(int n, CRGB c, uint8_t blend) { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } void addPixelColor(int n, uint32_t color, bool fast = false); - void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } // automatically inline - void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } // automatically inline + inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } // automatically inline + inline void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } // automatically inline void fadePixelColor(uint16_t n, uint8_t fade); - uint8_t get_random_wheel_index(uint8_t pos); - uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255); - uint32_t color_wheel(uint8_t pos); + uint8_t get_random_wheel_index(uint8_t pos) const; + uint32_t __attribute__((pure)) color_from_palette(uint_fast16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255); + uint32_t __attribute__((pure)) color_wheel(uint8_t pos); + + // 2D Blur: shortcuts for bluring columns or rows only (50% faster than full 2D blur) + inline void blurCols(fract8 blur_amount, bool smear = false) { // blur all columns + const unsigned cols = virtualWidth(); + for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); + } + inline void blurRows(fract8 blur_amount, bool smear = false) { // blur all rows + const unsigned rows = virtualHeight(); + for ( unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); + } // 2D matrix - uint16_t virtualWidth(void) const; - uint16_t virtualHeight(void) const; +#ifndef WLEDMM_FASTPATH + inline uint16_t virtualWidth() const { // WLEDMM use fast types, and make function inline + uint_fast16_t groupLen = groupLength(); + uint_fast16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; + if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vWidth; + } + inline uint16_t calc_virtualWidth() const { return virtualWidth();} + inline uint16_t virtualHeight() const { // WLEDMM use fast types, and make function inline + uint_fast16_t groupLen = groupLength(); + uint_fast16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; + if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vHeight; + } + inline uint16_t calc_virtualHeight() const { return virtualHeight();} +#else + inline uint16_t virtualWidth() const { return(_2dWidth);} // WLEDMM get pre-calculated virtualWidth + inline uint16_t virtualHeight() const { return(_2dHeight);} // WLEDMM get pre-calculated virtualHeight + + uint16_t calc_virtualWidth() const { + uint_fast16_t groupLen = groupLength(); + uint_fast16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; + if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vWidth; + } + uint16_t calc_virtualHeight() const { + uint_fast16_t groupLen = groupLength(); + uint_fast16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; + if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vHeight; + } +#endif + uint16_t nrOfVStrips(void) const; + void createjMap(); //WLEDMM jMap + void deletejMap(); //WLEDMM jMap + #ifndef WLED_DISABLE_2D - uint16_t XY(uint16_t x, uint16_t y); // support function to get relative index within segment (for leds[]) + [[gnu::hot]] inline uint16_t XY(uint_fast16_t x, uint_fast16_t y) const { // support function to get relative index within segment (for leds[]) // WLEDMM inline for speed + uint_fast16_t width = max(uint16_t(1), virtualWidth()); // segment width in logical pixels -- softhack007 avoid div/0 + uint_fast16_t height = max(uint16_t(1), virtualHeight()); // segment height in logical pixels -- softhack007 avoid div/0 + return (x%width) + (y%height) * width; + } + +#ifdef WLEDMM_FASTPATH + // WLEDMM this is a "gateway" function - we either call _fast or fall back to "slow" + [[gnu::hot]] inline void setPixelColorXY(int x, int y, uint32_t col) { + if (!_isSimpleSegment) { // slow path + setPixelColorXY_slow(x, y, col); + } else { // fast path + // some sanity checks + if (!_isValid2D) return; // not active + if ((unsigned(x) >= _2dWidth) || (unsigned(y) >= _2dHeight)) return; // check if (x,y) are out-of-range - due to 2's complement, this also catches negative values + if (!_brightness && !transitional) return; // black-out + + uint32_t scaled_col = (_brightness == 255) ? col : color_fade(col, _brightness); // calculate final color + setPixelColorXY_fast(x, y, col, scaled_col, int(_2dWidth), int(_2dHeight)); // call "fast" function + } + } + [[gnu::hot]] inline uint32_t getPixelColorXY(int x, int y) const { + // minimal sanity checks + if (!_isValid2D) return 0; // not active + if ((unsigned(x) >= _2dWidth) || (unsigned(y) >= _2dHeight)) return 0 ; // check if (x,y) are out-of-range - due to 2's complement, this also catches negative values + if (ledsrgb) { + int i = x + y*_2dWidth; // avoid error checking done by XY() - be optimistic about ranges of x and y + return RGBW32(ledsrgb[i].r, ledsrgb[i].g, ledsrgb[i].b, 0); + } + else return getPixelColorXY_part2(x, y, int(_2dWidth), int(_2dHeight)); // call "no ledsrgb" function to retrieve pixel from bus driver + } +#else void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color - void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } // automatically inline - void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } // automatically inline - void setPixelColorXY(float x, float y, uint32_t c, bool aa = true); - void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } - void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } - uint32_t getPixelColorXY(uint16_t x, uint16_t y); + uint32_t __attribute__((pure)) getPixelColorXY(int x, int y) const { return getPixelColorXY_slow(x,y);} +#endif + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } + //#ifdef WLED_USE_AA_PIXELS + void setPixelColorXY(float x, float y, uint32_t c, bool aa = true, bool fast=true); + inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } + inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } + //#endif + uint32_t __attribute__((pure)) getPixelColorXY_part2(int x, int y, int cols, int rows) const; + uint32_t __attribute__((pure)) getPixelColorXY_slow(int x, int y) const; // 2D support functions void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend); - void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } void addPixelColorXY(int x, int y, uint32_t color, bool fast = false); - void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColorXY(x, y, RGBW32(r,g,b,w), fast); } // automatically inline - void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColorXY(x, y, RGBW32(r,g,b,w), fast); } // automatically inline + inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade); void box_blur(uint16_t i, bool vertical, fract8 blur_amount); // 1D box blur (with weight) - void blurRow(uint16_t row, fract8 blur_amount); - void blurCol(uint16_t col, fract8 blur_amount); + void blurRow(uint32_t row, fract8 blur_amount, bool smear = false); + void blurCol(uint32_t col, fract8 blur_amount, bool smear = false); void moveX(int8_t delta, bool wrap = false); void moveY(int8_t delta, bool wrap = false); void move(uint8_t dir, uint8_t delta, bool wrap = false); - void draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); - void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c); - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0)); } // automatic inline - void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0); - void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0)); } // automatic inline - void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0)); } // automatic inline + void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void fillCircle(unsigned cx, unsigned cy, int radius, uint32_t col, bool soft); + inline void fillCircle(unsigned cx, unsigned cy, int radius, CRGB c, bool soft = false) { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false, uint8_t depth = UINT8_MAX); + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false, uint8_t depth = UINT8_MAX) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft, depth); } // automatic inline + void drawArc(unsigned x0, unsigned y0, int radius, uint32_t color, uint32_t fillColor = 0); + inline void drawArc(unsigned x0, unsigned y0, int radius, CRGB color, CRGB fillColor = BLACK) { drawArc(x0, y0, radius, RGBW32(color.r,color.g,color.b,0), RGBW32(fillColor.r,fillColor.g,fillColor.b,0)); } // automatic inline + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, bool drawShadow = false); + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0)); } // automatic inline void wu_pixel(uint32_t x, uint32_t y, CRGB c); - void blur1d(fract8 blur_amount); // blur all rows in 1 dimension + //void blur1d(fract8 blur_amount); // blur all rows in 1 dimension void blur2d(fract8 blur_amount) { blur(blur_amount); } - void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } + inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } void nscale8(uint8_t scale); + bool jsonToPixels(char *name, uint8_t fileNr); //WLEDMM for artifx #else - uint16_t XY(uint16_t x, uint16_t y) { return x; } - void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } - void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } - void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } - void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } - void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } - void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } - uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); } - void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } - void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } - void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { addPixelColor(x, color, fast); } - void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(x, RGBW32(r,g,b,w), fast); } - void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } - void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } - void box_blur(uint16_t i, bool vertical, fract8 blur_amount) {} - void blurRow(uint16_t row, fract8 blur_amount) {} - void blurCol(uint16_t col, fract8 blur_amount) {} - void moveX(int8_t delta, bool wrap = false) {} - void moveY(int8_t delta, bool wrap = false) {} - void move(uint8_t dir, uint8_t delta, bool wrap = false) {} - void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c) {} - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c) {} - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) {} - void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color) {} - void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} - void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} + inline uint16_t XY(uint16_t x, uint16_t y) { return x; } + inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } + //#ifdef WLED_USE_AA_PIXELS + inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } + inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } + inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } + //#endif + inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { addPixelColor(x, color, fast); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(x, RGBW32(r,g,b,w), fast); } + inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } + inline void box_blur(uint16_t i, bool vertical, fract8 blur_amount) {} + inline void blurRow(uint32_t row, fract8 blur_amount, bool smear = false) {} + inline void blurCol(uint32_t col, fract8 blur_amount, bool smear = false) {} + inline void moveX(int8_t delta, bool wrap = false) {} + inline void moveY(int8_t delta, bool wrap = false) {} + inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {} + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {} + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} + inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} #endif + uint8_t * getAudioPalette(int pal) const; //WLEDMM netmindz ar palette } segment; //static int segSize = sizeof(Segment); @@ -672,25 +853,30 @@ class WS2812FX { // 96 bytes panels(1), #endif // semi-private (just obscured) used in effect functions through macros - _currentPalette(CRGBPalette16(CRGB::Black)), _colors_t{0,0,0}, _virtualSegmentLength(0), // true private variables _length(DEFAULT_LED_COUNT), _brightness(DEFAULT_BRIGHTNESS), _transitionDur(750), - _targetFps(WLED_FPS), - _frametime(FRAMETIME_FIXED), + _targetFps(WLED_FPS_SLOW), // WLEDMM + _frametime(FRAMETIME_FIXED_SLOW), // WLEDMM _cumulativeFps(2), - _isServicing(false), +#ifdef ARDUINO_ARCH_ESP32 + _cumulativeFps500(2*500), // WLEDMM more accurate FPS measurement for ESP32 + _lastShow500(0), +#endif + _isServicing(true), // WLEDMM start with "true" - flag will be reset by strip.finalizeInit() _isOffRefreshRequired(false), _hasWhiteChannel(false), _triggered(false), _modeCount(MODE_COUNT), _callback(nullptr), customMappingTable(nullptr), + customMappingTableSize(0), //WLEDMM customMappingSize(0), _lastShow(0), + _lastServiceShow(0), _segment_index(0), _mainSegment(0) { @@ -702,6 +888,9 @@ class WS2812FX { // 96 bytes } ~WS2812FX() { + #ifdef WLED_DEBUG + if (Serial) Serial.println(F("~WS2812FX destroying strip.")); // WLEDMM can't use DEBUG_PRINTLN here + #endif if (customMappingTable) delete[] customMappingTable; _mode.clear(); _modeData.clear(); @@ -720,6 +909,7 @@ class WS2812FX { // 96 bytes printSize(), #endif finalizeInit(), + waitUntilIdle(void), // WLEDMM service(void), setMode(uint8_t segid, uint8_t m), setColor(uint8_t slot, uint32_t c), @@ -730,13 +920,14 @@ class WS2812FX { // 96 bytes purgeSegments(bool force = false), setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 1, uint8_t spacing = 0, uint16_t offset = UINT16_MAX, uint16_t startY=0, uint16_t stopY=1), setMainSegmentId(uint8_t n), - restartRuntime(), - resetSegments(), + restartRuntime(bool doReset=true), + resetSegments(bool boundsOnly = false), //WLEDMM add boundsOnly makeAutoSegments(bool forceReset = false), fixInvalidSegments(), setPixelColor(int n, uint32_t c), show(void), - setTargetFps(uint8_t fps); + setTargetFps(uint8_t fps), + enumerateLedmaps(); //WLEDMM (from fcn_declare) void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } void fill(uint32_t c) { for (int i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) @@ -753,64 +944,66 @@ class WS2812FX { // 96 bytes bool checkSegmentAlignment(void), - hasRGBWBus(void), - hasCCTBus(void), + hasRGBWBus(void) const, + hasCCTBus(void) const, // return true if the strip is being sent pixel updates - isUpdating(void), + isUpdating(void) const, deserializeMap(uint8_t n=0), useLedsArray = false; - inline bool isServicing(void) { return _isServicing; } - inline bool hasWhiteChannel(void) {return _hasWhiteChannel;} - inline bool isOffRefreshRequired(void) {return _isOffRefreshRequired;} + inline bool isServicing(void) const { return _isServicing; } + inline bool hasWhiteChannel(void) const {return _hasWhiteChannel;} + inline bool isOffRefreshRequired(void) const {return _isOffRefreshRequired;} uint8_t paletteFade, paletteBlend, milliampsPerLed, cctBlending, - getActiveSegmentsNum(void), - getFirstSelectedSegId(void), - getLastActiveSegmentId(void), - getActiveSegsLightCapabilities(bool selectedOnly = false), + getActiveSegmentsNum(void) const, + __attribute__((pure)) getFirstSelectedSegId(void), + getLastActiveSegmentId(void) const, + __attribute__((pure)) getActiveSegsLightCapabilities(bool selectedOnly = false), setPixelSegment(uint8_t n); - inline uint8_t getBrightness(void) { return _brightness; } - inline uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) - inline uint8_t getSegmentsNum(void) { return _segments.size(); } // returns currently present segments - inline uint8_t getCurrSegmentId(void) { return _segment_index; } - inline uint8_t getMainSegmentId(void) { return _mainSegment; } - inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT; } // will only return built-in palette count - inline uint8_t getTargetFps() { return _targetFps; } - inline uint8_t getModeCount() { return _modeCount; } + inline uint8_t getBrightness(void) const { return _brightness; } + inline uint8_t getSegmentsNum(void) const { return _segments.size(); } // returns currently present segments + inline uint8_t getCurrSegmentId(void) const { return _segment_index; } + inline uint8_t getMainSegmentId(void) const { return _mainSegment; } + inline uint8_t getTargetFps() const { return _targetFps; } + inline uint8_t getModeCount() const { return _modeCount; } + inline static constexpr uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline static constexpr uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT; } // will only return built-in palette count uint16_t ablMilliampsMax, currentMilliamps, - getLengthPhysical(void), - getLengthTotal(void), // will include virtual/nonexistent pixels in matrix - getFps(); + getLengthPhysical(void) const, + getLengthPhysical2(void) const, // WLEDMM total length including HUB75, network busses excluded + __attribute__((pure)) getLengthTotal(void) const, // will include virtual/nonexistent pixels in matrix //WLEDMM attribute added + getFps() const; - inline uint16_t getFrameTime(void) { return _frametime; } - inline uint16_t getMinShowDelay(void) { return MIN_SHOW_DELAY; } - inline uint16_t getLength(void) { return _length; } // 2D matrix may have less pixels than W*H - inline uint16_t getTransition(void) { return _transitionDur; } + inline uint16_t getFrameTime(void) const { return _frametime; } + inline uint16_t getMinShowDelay(void) const { return MIN_SHOW_DELAY; } + inline uint16_t getLength(void) const { return _length; } // 2D matrix may have less pixels than W*H + inline uint16_t getTransition(void) const { return _transitionDur; } uint32_t now, - timebase, - getPixelColor(uint16_t); + timebase; + uint32_t __attribute__((pure)) getPixelColor(uint_fast16_t) const; // WLEDMM attribute pure = does not have side-effects + uint32_t __attribute__((pure)) getPixelColorRestored(uint_fast16_t i) const;// WLEDMM gets the original color from the driver (without downscaling by _bri) - inline uint32_t getLastShow(void) { return _lastShow; } - inline uint32_t segColor(uint8_t i) { return _colors_t[i]; } + inline uint32_t getLastShow(void) const { return _lastShow; } + inline uint32_t segColor(uint8_t i) const { return _colors_t[i]; } const char * - getModeData(uint8_t id = 0) { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } + getModeData(uint8_t id = 0) const { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } const char ** getModeDataSrc(void) { return &(_modeData[0]); } // vectors use arrays for underlying data - Segment& getSegment(uint8_t id); + Segment& getSegment(uint8_t id) __attribute__((pure)); inline Segment& getFirstSelectedSeg(void) { return _segments[getFirstSelectedSegId()]; } inline Segment& getMainSegment(void) { return _segments[getMainSegmentId()]; } inline Segment* getSegments(void) { return &(_segments[0]); } @@ -822,13 +1015,31 @@ class WS2812FX { // 96 bytes #ifndef WLED_DISABLE_2D #define WLED_MAX_PANELS 64 uint8_t - panels; + panels, + panelsH, //WLEDMM needs to be stored as well + panelsV; //WLEDMM needs to be stored as well + + //WLEDMM: keep storing basic 2d setup + bool + bOrA = false; //WLEDMM basic or advanced, default basic + struct { + bool bottomStart : 1; + bool rightStart : 1; + bool vertical : 1; + bool serpentine : 1; + } matrix; + struct { + bool bottomStart : 1; + bool rightStart : 1; + bool vertical : 1; + bool serpentine : 1; + } panelO; //panelOrientation typedef struct panel_t { - uint16_t xOffset; // x offset relative to the top left of matrix in LEDs - uint16_t yOffset; // y offset relative to the top left of matrix in LEDs - uint8_t width; // width of the panel - uint8_t height; // height of the panel + uint16_t xOffset; // x offset relative to the top left of matrix in LEDs. + uint16_t yOffset; // y offset relative to the top left of matrix in LEDs. + uint16_t width; // width of the panel + uint16_t height; // height of the panel union { uint8_t options; struct { @@ -851,6 +1062,7 @@ class WS2812FX { // 96 bytes void setUpMatrix(), + setPixelColorXY_fast(int x, int y, uint32_t c), setPixelColorXY(int x, int y, uint32_t c); // outsmart the compiler :) by correctly overloading @@ -858,21 +1070,25 @@ class WS2812FX { // 96 bytes inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } uint32_t - getPixelColorXY(uint16_t, uint16_t); + getPixelColorXY(uint16_t, uint16_t) const; // end 2D support void loadCustomPalettes(void); // loads custom palettes from JSON - CRGBPalette16 _currentPalette; // palette used for current effect (includes transition) std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class // using public variables to reduce code size increase due to inline function getSegment() (with bounds checking) // and color transitions uint32_t _colors_t[3]; // color used for effect (includes transition) uint16_t _virtualSegmentLength; +#ifdef WLEDMM_FASTPATH + segment* _currentSeg = nullptr; // WLEDMM speed up SEGMENT access +#endif std::vector _segments; - friend class Segment; + friend struct Segment; + + uint32_t getPixelColorXYRestored(uint16_t x, uint16_t y) const; // WLEDMM gets the original color from the driver (without downscaling by _bri) private: uint16_t _length; @@ -882,6 +1098,10 @@ class WS2812FX { // 96 bytes uint8_t _targetFps; uint16_t _frametime; uint16_t _cumulativeFps; +#ifdef ARDUINO_ARCH_ESP32 + uint64_t _cumulativeFps500; // WLEDMM more accurate FPS measurement for ESP32 + uint64_t _lastShow500; +#endif // will require only 1 byte struct { @@ -898,9 +1118,11 @@ class WS2812FX { // 96 bytes show_callback _callback; uint16_t* customMappingTable; + uint16_t customMappingTableSize; //WLEDMM uint16_t customMappingSize; - uint32_t _lastShow; + /*uint32_t*/ unsigned long _lastShow; // WLEDMM avoid losing precision + unsigned long _lastServiceShow; // WLEDMM last call of strip.show (timestamp) uint8_t _segment_index; uint8_t _mainSegment; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 8b4c70ef0f..247f166e6c 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -1,26 +1,6 @@ /* FX_2Dfcn.cpp contains all 2D utility functions - - LICENSE - The MIT License (MIT) - Copyright (c) 2022 Blaz Kristan (https://blaz.at/home) - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - - Parts of the code adapted from WLED Sound Reactive + Parts of the code adapted from WLED Sound Reactive: Copyright (c) 2022 Andrew Tuline, Ewoud Wijma, Harm Aldick */ #include "wled.h" #include "FX.h" @@ -36,11 +16,6 @@ // so matrix should disable regular ledmap processing void WS2812FX::setUpMatrix() { #ifndef WLED_DISABLE_2D - // erase old ledmap, just in case. - if (customMappingTable != nullptr) delete[] customMappingTable; - customMappingTable = nullptr; - customMappingSize = 0; - // isMatrix is set in cfg.cpp or set.cpp if (isMatrix) { // calculate width dynamically because it will have gaps @@ -56,25 +31,66 @@ void WS2812FX::setUpMatrix() { } } - // safety check - if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { - DEBUG_PRINTLN(F("2D Bounds error.")); + // safety check + // WLEDMM no check on Segment::maxWidth * Segment::maxHeight > MAX_LEDS || + if (Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { + DEBUG_PRINTF("2D Bounds error. %d x %d\n", Segment::maxWidth, Segment::maxHeight); isMatrix = false; Segment::maxWidth = _length; Segment::maxHeight = 1; panels = 0; panel.clear(); // release memory allocated by panels - resetSegments(); + resetSegments(true); //WLEDMM bounds only return; } - customMappingTable = new uint16_t[Segment::maxWidth * Segment::maxHeight]; + USER_PRINTF("setUpMatrix %d x %d\n", Segment::maxWidth, Segment::maxHeight); + + // WLEDMM check if mapping table is necessary (avoiding heap fragmentation) +#if defined(WLED_ENABLE_HUB75MATRIX) + bool needLedMap = (loadedLedmap >0); // ledmap loaded + needLedMap |= WLED_FS.exists(F("/2d-gaps.json")); // gapFile found + needLedMap |= panel.size() > 1; // 2D config: more than one panel + if (panel.size() == 1) { + Panel &p = panel[0]; + needLedMap |= p.serpentine; // panel serpentine + needLedMap |= p.vertical; // panel not horizotal + needLedMap |= p.bottomStart | p.rightStart; // panel not top left, or not left->light + needLedMap |= (p.xOffset > 0) || (p.yOffset > 0); // panel does not start at (0,0) + } +#else + bool needLedMap = true; // always use ledMaps on non-HUB75 builds +#endif + + //WLEDMM recreate customMappingTable if more space needed + if (Segment::maxWidth * Segment::maxHeight > customMappingTableSize) { + size_t size = max(ledmapMaxSize, size_t(Segment::maxWidth * Segment::maxHeight)); // TroyHacks + if (!needLedMap) size = 0; // softhack007 + USER_PRINTF("setupmatrix customMappingTable alloc %d from %d\n", size, customMappingTableSize); + //if (customMappingTable != nullptr) delete[] customMappingTable; + //customMappingTable = new(std::nothrow) uint16_t[size]; + + // don't use new / delete + if ((size > 0) && (customMappingTable != nullptr)) { // resize + customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize + } + if ((size > 0) && (customMappingTable == nullptr)) { // second try + DEBUG_PRINTLN("setUpMatrix: trying to get fresh memory block."); + customMappingTable = (uint16_t*) calloc(size, sizeof(uint16_t)); + if (customMappingTable == nullptr) { + USER_PRINTLN("setUpMatrix: alloc failed"); + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + } + } + if (customMappingTable != nullptr) customMappingTableSize = size; + } - if (customMappingTable != nullptr) { + if ((customMappingTable != nullptr) || (!needLedMap)) { // softhack007 customMappingSize = Segment::maxWidth * Segment::maxHeight; + if (!needLedMap) customMappingSize = 0; // softhack007 // fill with empty in case we don't fill the entire matrix - for (size_t i = 0; i< customMappingSize; i++) { + for (size_t i = 0; i< customMappingTableSize; i++) { //WLEDMM use customMappingTableSize customMappingTable[i] = (uint16_t)-1; } @@ -91,8 +107,8 @@ void WS2812FX::setUpMatrix() { int8_t *gapTable = nullptr; if (isFile && requestJSONBufferLock(20)) { - DEBUG_PRINT(F("Reading LED gap from ")); - DEBUG_PRINTLN(fileName); + USER_PRINT(F("Reading LED gap from ")); + USER_PRINTLN(fileName); // read the array into global JSON buffer if (readObjectFromFile(fileName, nullptr, &doc)) { // the array is similar to ledmap, except it has only 3 values: @@ -101,8 +117,8 @@ void WS2812FX::setUpMatrix() { // 1 ... active pixel (it will count and will be mapped) JsonArray map = doc.as(); gapSize = map.size(); - if (!map.isNull() && gapSize >= customMappingSize) { // not an empty map - gapTable = new int8_t[gapSize]; + if (!map.isNull() && (gapSize > 0) && gapSize >= customMappingSize) { // not an empty map //softhack also check gapSize>0 + gapTable = new(std::nothrow) int8_t[gapSize]; if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } @@ -112,11 +128,12 @@ void WS2812FX::setUpMatrix() { releaseJSONBufferLock(); } - uint16_t x, y, pix=0; //pixel + if (needLedMap && customMappingTable != nullptr) { // softhack007 + uint_fast16_t x, y, pix=0; //pixel for (size_t pan = 0; pan < panel.size(); pan++) { Panel &p = panel[pan]; - uint16_t h = p.vertical ? p.height : p.width; - uint16_t v = p.vertical ? p.width : p.height; + uint_fast16_t h = p.vertical ? p.height : p.width; + uint_fast16_t v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ for(size_t i = 0; i < h; i++) { y = (p.vertical?p.rightStart:p.bottomStart) ? v-j-1 : j; @@ -128,39 +145,70 @@ void WS2812FX::setUpMatrix() { } } } + } // delete gap array as we no longer need it - if (gapTable) delete[] gapTable; + if (gapTable) {delete[] gapTable; gapTable=nullptr;} // softhack prevent dangling pointer - #ifdef WLED_DEBUG - DEBUG_PRINT(F("Matrix ledmap:")); + #ifdef WLED_DEBUG_MAPS + DEBUG_PRINTF("Matrix ledmap: \n"); for (uint16_t i=0; i 0) { + bool isIdentity = true; + for (size_t i = 0; (i< customMappingSize) && isIdentity; i++) { //WLEDMM use customMappingTableSize + if (customMappingTable[i] != (uint16_t)i ) isIdentity = false; + } + if (isIdentity) { + free(customMappingTable); customMappingTable = nullptr; + USER_PRINTF("!setupmatrix: customMappingTable is not needed. Dropping %d bytes.\n", customMappingTableSize * sizeof(uint16_t)); + customMappingTableSize = 0; + customMappingSize = 0; + loadedLedmap = 0; //WLEDMM } } +#endif + #else isMatrix = false; // no matter what config says #endif } +// absolute matrix version of setPixelColor(), without error checking +void IRAM_ATTR __attribute__((hot)) WS2812FX::setPixelColorXY_fast(int x, int y, uint32_t col) //WLEDMM: IRAM_ATTR conditionally +{ + uint_fast16_t index = y * Segment::maxWidth + x; + if (index < customMappingSize) index = customMappingTable[index]; + if (index >= _length) return; + busses.setPixelColor(index, col); +} + // absolute matrix version of setPixelColor() -void /*IRAM_ATTR*/ WS2812FX::setPixelColorXY(int x, int y, uint32_t col) +void IRAM_ATTR_YN WS2812FX::setPixelColorXY(int x, int y, uint32_t col) //WLEDMM: IRAM_ATTR conditionally { #ifndef WLED_DISABLE_2D if (!isMatrix) return; // not a matrix set-up - uint16_t index = y * Segment::maxWidth + x; + uint_fast16_t index = y * Segment::maxWidth + x; #else uint16_t index = x; #endif @@ -170,9 +218,9 @@ void /*IRAM_ATTR*/ WS2812FX::setPixelColorXY(int x, int y, uint32_t col) } // returns RGBW values of pixel -uint32_t WS2812FX::getPixelColorXY(uint16_t x, uint16_t y) { +uint32_t __attribute__((hot)) WS2812FX::getPixelColorXY(uint16_t x, uint16_t y) const { #ifndef WLED_DISABLE_2D - uint16_t index = (y * Segment::maxWidth + x); + uint_fast16_t index = (y * Segment::maxWidth + x); //WLEDMM: use fast types #else uint16_t index = x; #endif @@ -181,74 +229,179 @@ uint32_t WS2812FX::getPixelColorXY(uint16_t x, uint16_t y) { return busses.getPixelColor(index); } +uint32_t __attribute__((hot)) WS2812FX::getPixelColorXYRestored(uint16_t x, uint16_t y) const { // WLEDMM gets the original color from the driver (without downscaling by _bri) + #ifndef WLED_DISABLE_2D + uint_fast16_t index = (y * Segment::maxWidth + x); //WLEDMM: use fast types + #else + uint16_t index = x; + #endif + if (index < customMappingSize) index = customMappingTable[index]; + if (index >= _length) return 0; + return busses.getPixelColorRestored(index); +} + /////////////////////////////////////////////////////////// // Segment:: routines /////////////////////////////////////////////////////////// #ifndef WLED_DISABLE_2D -// XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) -uint16_t /*IRAM_ATTR*/ Segment::XY(uint16_t x, uint16_t y) { - uint16_t width = virtualWidth(); // segment width in logical pixels - uint16_t height = virtualHeight(); // segment height in logical pixels - return (x%width) + (y%height) * width; +// WLEDMM cache some values so we don't need to re-calc then for each pixel +void Segment::startFrame(void) { + _isSimpleSegment = (grouping == 1) && (spacing == 0); // we can handle pixels faster when no grouping or spacing is involved + _isSuperSimpleSegment = !mirror && !mirror_y && (grouping == 1) && (spacing == 0); // fastest - we only draw one pixel per call + +#ifdef WLEDMM_FASTPATH + //_isValid2D = isActive() && is2D(); + _isValid2D = isActive() && strip.isMatrix && length() > 1; + _brightness = currentBri(on ? opacity : 0); + // if (reverse_y) _isSimpleSegment = false; // for A/B testing + _2dHeight = calc_virtualHeight(); + _2dWidth = _isValid2D ? calc_virtualWidth() : calc_virtualLength(); + _virtuallength = calc_virtualLength(); +#endif } +// WLEDMM end + +// XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) +// WLEDMM Segment::XY()is declared inline, see FX.h + -void /*IRAM_ATTR*/ Segment::setPixelColorXY(int x, int y, uint32_t col) +// Simplified version of Segment::setPixelColorXY - without error checking. Does not support grouping or spacing +// * expects scaled color (final brightness) as additional input parameter, plus segment virtualWidth() and virtualHeight() +void IRAM_ATTR __attribute__((hot)) Segment::setPixelColorXY_fast(int x, int y, uint32_t col, uint32_t scaled_col, int cols, int rows) const //WLEDMM { - if (Segment::maxHeight==1) return; // not a matrix set-up - if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit + unsigned i = UINT_MAX; + bool sameColor = false; + if (ledsrgb) { // WLEDMM small optimization + i = x + y*cols; // avoid error checking done by XY() - be optimistic about ranges of x and y + CRGB fastled_col = CRGB(col); + if (ledsrgb[i] == fastled_col) sameColor = true; + else ledsrgb[i] = fastled_col; + } + +#if 0 // this is still a dangerous optimization + if ((i < UINT_MAX) && sameColor && (call > 0) && (!transitional) && (mode != FX_MODE_2DSCROLLTEXT) && (ledsrgb[i] == CRGB(scaled_col))) return; // WLEDMM looks like nothing to do +#endif + + // handle reverse and transpose + if (reverse ) x = cols - x - 1; + if (reverse_y) y = rows - y - 1; + if (transpose) std::swap(x,y); // swap X & Y if segment transposed + + // set the requested pixel + strip.setPixelColorXY_fast(start + x, startY + y, scaled_col); + #ifdef WLEDMM_FASTPATH + bool simpleSegment = _isSuperSimpleSegment; + #else + bool simpleSegment = !mirror && !mirror_y; + #endif + if (simpleSegment) return; // WLEDMM shortcut when no mirroring needed + + // handle mirroring - minimum width/height is 1 !!! + const int_fast16_t wid_ = max(1,stop - start); + const int_fast16_t hei_ = max(1, stopY - startY); + if (mirror) { //set the corresponding horizontally mirrored pixel + if (transpose) strip.setPixelColorXY_fast(start + x, startY + hei_ - y - 1, scaled_col); + else strip.setPixelColorXY_fast(start + wid_ - x - 1, startY + y, scaled_col); + } + if (mirror_y) { //set the corresponding vertically mirrored pixel + if (transpose) strip.setPixelColorXY_fast(start + wid_ - x - 1, startY + y, scaled_col); + else strip.setPixelColorXY_fast(start + x, startY + hei_ - y - 1, scaled_col); + } + if (mirror_y && mirror) { //set the corresponding vertically AND horizontally mirrored pixel + strip.setPixelColorXY_fast(start + wid_ - x - 1, startY + hei_ - y - 1, scaled_col); + } +} - if (leds) leds[XY(x,y)] = col; +// normal Segment::setPixelColorXY with error checking, and support for grouping / spacing +#ifdef WLEDMM_FASTPATH +void IRAM_ATTR_YN Segment::setPixelColorXY_slow(int x, int y, uint32_t col) //WLEDMM: IRAM_ATTR conditionally, renamed to "_slow" +#else +void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) //WLEDMM: IRAM_ATTR conditionally +#endif +{ + if ((Segment::maxHeight==1) || !isActive()) return; // not a matrix set-up + const int_fast16_t cols = virtualWidth(); // WLEDMM optimization + const int_fast16_t rows = virtualHeight(); + + if (x<0 || y<0 || x >= cols || y >= rows) return; // if pixel would fall out of virtual segment just exit + + unsigned i = UINT_MAX; + bool sameColor = false; + if (ledsrgb) { // WLEDMM small optimization + i = XY(x,y); + CRGB fastled_col = CRGB(col); + if (ledsrgb[i] == fastled_col) sameColor = true; + else ledsrgb[i] = fastled_col; + } uint8_t _bri_t = currentBri(on ? opacity : 0); if (!_bri_t && !transitional) return; if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_fade(col, _bri_t); } - if (reverse ) x = virtualWidth() - x - 1; - if (reverse_y) y = virtualHeight() - y - 1; +#if 0 // this is a dangerous optimization + if ((i < UINT_MAX) && sameColor && (call > 0) && (!transitional) && (mode != FX_MODE_2DSCROLLTEXT) && (ledsrgb[i] == CRGB(col))) return; // WLEDMM looks like nothing to do +#endif + + if (reverse ) x = cols - x - 1; + if (reverse_y) y = rows - y - 1; if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed - x *= groupLength(); // expand to physical pixels - y *= groupLength(); // expand to physical pixels - if (x >= width() || y >= height()) return; // if pixel would fall out of segment just exit + // WLEDMM shortcut when no grouping/spacing used + #ifdef WLEDMM_FASTPATH + bool simpleSegment = _isSuperSimpleSegment; + #else + bool simpleSegment = !mirror && !mirror_y && (grouping == 1) && (spacing == 0); + #endif + if (simpleSegment) { + strip.setPixelColorXY(start + x, startY + y, col); + return; + } + + const uint_fast16_t glen_ = groupLength(); // WLEDMM optimization + const uint_fast16_t wid_ = max(uint16_t(1), width()); + const uint_fast16_t hei_ = max(uint16_t(1), height()); + + x *= glen_; // expand to physical pixels + y *= glen_; // expand to physical pixels + if (x >= wid_ || y >= hei_) return; // if pixel would fall out of segment just exit - for (int j = 0; j < grouping; j++) { // groupping vertically - for (int g = 0; g < grouping; g++) { // groupping horizontally - uint16_t xX = (x+g), yY = (y+j); - if (xX >= width() || yY >= height()) continue; // we have reached one dimension's end + const int grp_ = grouping; // WLEDMM optimization + for (int j = 0; j < grp_; j++) { // groupping vertically + for (int g = 0; g < grp_; g++) { // groupping horizontally + uint_fast16_t xX = (x+g), yY = (y+j); //WLEDMM: use fast types + if (xX >= wid_ || yY >= hei_) continue; // we have reached one dimension's end strip.setPixelColorXY(start + xX, startY + yY, col); if (mirror) { //set the corresponding horizontally mirrored pixel - if (transpose) strip.setPixelColorXY(start + xX, startY + height() - yY - 1, col); - else strip.setPixelColorXY(start + width() - xX - 1, startY + yY, col); + if (transpose) strip.setPixelColorXY(start + xX, startY + hei_ - yY - 1, col); + else strip.setPixelColorXY(start + wid_ - xX - 1, startY + yY, col); } if (mirror_y) { //set the corresponding vertically mirrored pixel - if (transpose) strip.setPixelColorXY(start + width() - xX - 1, startY + yY, col); - else strip.setPixelColorXY(start + xX, startY + height() - yY - 1, col); + if (transpose) strip.setPixelColorXY(start + wid_ - xX - 1, startY + yY, col); + else strip.setPixelColorXY(start + xX, startY + hei_ - yY - 1, col); } if (mirror_y && mirror) { //set the corresponding vertically AND horizontally mirrored pixel - strip.setPixelColorXY(width() - xX - 1, height() - yY - 1, col); + strip.setPixelColorXY(start + wid_ - xX - 1, startY + hei_ - yY - 1, col); } } } } +// WLEDMM setPixelColorXY(float x, float y, uint32_t col, ..) is depricated. use wu_pixel(x,y,col) instead. // anti-aliased version of setPixelColorXY() -void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) +void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa, bool fast) // WLEDMM some speedups due to fast int and faster sqrt16 { if (Segment::maxHeight==1) return; // not a matrix set-up if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); +#if 0 // deprecated + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); float fX = x * (cols-1); float fY = y * (rows-1); @@ -267,10 +420,17 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) uint32_t cXRYB = getPixelColorXY(xR, yB); if (xL!=xR && yT!=yB) { - setPixelColorXY(xL, yT, color_blend(col, cXLYT, uint8_t(sqrtf(dL*dT)*255.0f))); // blend TL pixel - setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(sqrtf(dR*dT)*255.0f))); // blend TR pixel - setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(sqrtf(dL*dB)*255.0f))); // blend BL pixel - setPixelColorXY(xR, yB, color_blend(col, cXRYB, uint8_t(sqrtf(dR*dB)*255.0f))); // blend BR pixel + if (!fast) { + setPixelColorXY(xL, yT, color_blend(col, cXLYT, uint8_t(sqrtf(dL*dT)*255.0f))); // blend TL pixel + setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(sqrtf(dR*dT)*255.0f))); // blend TR pixel + setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(sqrtf(dL*dB)*255.0f))); // blend BL pixel + setPixelColorXY(xR, yB, color_blend(col, cXRYB, uint8_t(sqrtf(dR*dB)*255.0f))); // blend BR pixel + } else { + setPixelColorXY(xL, yT, color_blend(col, cXLYT, uint8_t(sqrt16(dL*dT*65025.0f)))); // blend TL pixel // WLEDMM: use faster sqrt16 for integer; perform multiplication by 255^2 before sqrt + setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(sqrt16(dR*dT*65025.0f)))); // blend TR pixel // this is possible because sqrt(a) * sqrt(b) = sqrt(a * b) + setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(sqrt16(dL*dB*65025.0f)))); // blend BL pixel + setPixelColorXY(xR, yB, color_blend(col, cXRYB, uint8_t(sqrt16(dR*dB*65025.0f)))); // blend BR pixel + } } else if (xR!=xL && yT==yB) { setPixelColorXY(xR, yT, color_blend(col, cXLYT, uint8_t(dL*255.0f))); // blend L pixel setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(dR*255.0f))); // blend R pixel @@ -283,154 +443,169 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) } else { setPixelColorXY(uint16_t(roundf(fX)), uint16_t(roundf(fY)), col); } + +#else // replacement using wu_pixel + unsigned px = x * ((virtualWidth()-1) <<8); + unsigned py = y * ((virtualHeight()-1) <<8); + wu_pixel(px, py, CRGB(col)); +#endif } -// returns RGBW values of pixel -uint32_t Segment::getPixelColorXY(uint16_t x, uint16_t y) { - int i = XY(x,y); - if (leds) return RGBW32(leds[i].r, leds[i].g, leds[i].b, 0); +// WLEDMM this function is only called by getPixelColorXY, in case we don't have the ledsrgb buffer! +uint32_t IRAM_ATTR_YN Segment::getPixelColorXY_part2(int x, int y, int cols, int rows) const { + if (reverse ) x = cols - x - 1; + if (reverse_y) y = rows - y - 1; + if (transpose) std::swap(x,y); // swap X & Y if segment transposed + const uint_fast16_t groupLength_ = groupLength(); // WLEDMM small optimization + x *= groupLength_; // expand to physical pixels + y *= groupLength_; // expand to physical pixels + return strip.getPixelColorXYRestored(start + x, startY + y); +} + +uint32_t IRAM_ATTR_YN Segment::getPixelColorXY_slow(int x, int y) const { // WLEDMM fallback for non-fastpath builds + if (x<0 || y<0 || !isActive()) return 0; // not active or out-of range + if (ledsrgb) { + int i = XY(x,y); + return RGBW32(ledsrgb[i].r, ledsrgb[i].g, ledsrgb[i].b, 0); + } if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed - x *= groupLength(); // expand to physical pixels - y *= groupLength(); // expand to physical pixels + const uint_fast16_t groupLength_ = groupLength(); // WLEDMM small optimization + x *= groupLength_; // expand to physical pixels + y *= groupLength_; // expand to physical pixels if (x >= width() || y >= height()) return 0; - return strip.getPixelColorXY(start + x, startY + y); + return strip.getPixelColorXYRestored(start + x, startY + y); } // Blends the specified color with the existing pixel color. void Segment::blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { - setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); + if (blend == UINT8_MAX) setPixelColorXY(x, y, color); + else setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } // Adds the specified color with the existing pixel color perserving color balance. -void Segment::addPixelColorXY(int x, int y, uint32_t color, bool fast) { - uint32_t col = getPixelColorXY(x,y); - uint8_t r = R(col); - uint8_t g = G(col); - uint8_t b = B(col); - uint8_t w = W(col); - if (fast) { - r = qadd8(r, R(color)); - g = qadd8(g, G(color)); - b = qadd8(b, B(color)); - w = qadd8(w, W(color)); - col = RGBW32(r,g,b,w); - } else { - col = color_add(col, color); - } - setPixelColorXY(x, y, col); +void IRAM_ATTR_YN Segment::addPixelColorXY(int x, int y, uint32_t color, bool fast) { + // if (!isActive()) return; // not active //WLEDMM sanity check is repeated in getPixelColorXY / setPixelColorXY + // if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit //WLEDMM + uint32_t oldCol = getPixelColorXY(x,y); + uint32_t col = color_add(oldCol, color, fast); + if (col != oldCol) setPixelColorXY(x, y, col); } void Segment::fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { - CRGB pix = CRGB(getPixelColorXY(x,y)).nscale8_video(fade); - setPixelColorXY(x, y, pix); + // if (!isActive()) return; // not active //WLEDMM sanity check is repeated in getPixelColorXY / setPixelColorXY + CRGB pix = CRGB(getPixelColorXY(x,y)); + CRGB oldPix = pix; + pix = pix.nscale8_video(fade); + if (pix != oldPix) setPixelColorXY(int(x), int(y), pix); } // blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint16_t row, fract8 blur_amount) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); +void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ + if (!isActive()) return; // not active + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); if (row >= rows) return; // blur one row - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; - for (uint16_t x = 0; x < cols; x++) { - CRGB cur = getPixelColorXY(x, row); - CRGB part = cur; - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (x) { - CRGB prev = CRGB(getPixelColorXY(x-1, row)) + part; - setPixelColorXY(x-1, row, prev); + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = 0; + for (unsigned x = 0; x < cols; x++) { + uint32_t cur = getPixelColorXY(x, row); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (x > 0) { + if (carryover) + curnew = color_add(curnew, carryover, !smear); // WLEDMM don't use "fast" when smear==true (better handling of bright colors) + uint32_t prev = color_add(lastnew, part, !smear);// WLEDMM + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(int(x - 1), int(row), prev); } - setPixelColorXY(x, row, cur); + else // first pixel + setPixelColorXY(int(x), int(row), curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColorXY(int(cols-1), int(row), curnew); // set last pixel } // blurCol: perform a blur on a column of a rectangular matrix -void Segment::blurCol(uint16_t col, fract8 blur_amount) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); +void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { + if (!isActive()) return; // not active + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); if (col >= cols) return; // blur one column - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; - for (uint16_t i = 0; i < rows; i++) { - CRGB cur = getPixelColorXY(col, i); - CRGB part = cur; - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (i) { - CRGB prev = CRGB(getPixelColorXY(col, i-1)) + part; - setPixelColorXY(col, i-1, prev); + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = 0; + for (unsigned y = 0; y < rows; y++) { + uint32_t cur = getPixelColorXY(col, y); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (y > 0) { + if (carryover) + curnew = color_add(curnew, carryover, !smear); // WLEDMM don't use "fast" when smear==true (better handling of bright colors) + uint32_t prev = color_add(lastnew, part, !smear); // WLEDMM + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(int(col), int(y - 1), prev); } - setPixelColorXY(col, i, cur); - carryover = part; + else // first pixel + setPixelColorXY(int(col), int(y), curnew); + lastnew = curnew; + last = cur; //save original value for comparison on next iteration + carryover = part; } + setPixelColorXY(int(col), int(rows - 1), curnew); } // 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - const uint16_t dim1 = vertical ? rows : cols; - const uint16_t dim2 = vertical ? cols : rows; + if (!isActive() || blur_amount == 0) return; // not active + const int cols = virtualWidth(); + const int rows = virtualHeight(); + const int dim1 = vertical ? rows : cols; + const int dim2 = vertical ? cols : rows; if (i >= dim2) return; const float seep = blur_amount/255.f; const float keep = 3.f - 2.f*seep; // 1D box blur - CRGB tmp[dim1]; - for (uint16_t j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; - uint16_t xp = vertical ? x : x-1; - uint16_t yp = vertical ? y-1 : y; - uint16_t xn = vertical ? x : x+1; - uint16_t yn = vertical ? y+1 : y; - CRGB curr = getPixelColorXY(x,y); - CRGB prev = (xp<0 || yp<0) ? CRGB::Black : getPixelColorXY(xp,yp); - CRGB next = ((vertical && yn>=dim1) || (!vertical && xn>=dim1)) ? CRGB::Black : getPixelColorXY(xn,yn); - uint16_t r, g, b; - r = (curr.r*keep + (prev.r + next.r)*seep) / 3; - g = (curr.g*keep + (prev.g + next.g)*seep) / 3; - b = (curr.b*keep + (prev.b + next.b)*seep) / 3; - tmp[j] = CRGB(r,g,b); - } - for (uint16_t j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; - setPixelColorXY(x, y, tmp[j]); + uint32_t out[dim1], in[dim1]; + for (int j = 0; j < dim1; j++) { + int x = vertical ? i : j; + int y = vertical ? j : i; + in[j] = getPixelColorXY(x, y); + } + for (int j = 0; j < dim1; j++) { + uint32_t curr = in[j]; + uint32_t prev = j > 0 ? in[j-1] : BLACK; + uint32_t next = j < dim1-1 ? in[j+1] : BLACK; + uint8_t r, g, b, w; + r = (R(curr)*keep + (R(prev) + R(next))*seep) / 3; + g = (G(curr)*keep + (G(prev) + G(next))*seep) / 3; + b = (B(curr)*keep + (B(prev) + B(next))*seep) / 3; + w = (W(curr)*keep + (W(prev) + W(next))*seep) / 3; + out[j] = RGBW32(r,g,b,w); + } + for (int j = 0; j < dim1; j++) { + int x = vertical ? i : j; + int y = vertical ? j : i; + if (in[j] != out[j]) setPixelColorXY(x, y, out[j]); } -} - -// blur1d: one-dimensional blur filter. Spreads light to 2 line neighbors. -// blur2d: two-dimensional blur filter. Spreads light to 8 XY neighbors. -// -// 0 = no spread at all -// 64 = moderate spreading -// 172 = maximum smooth, even spreading -// -// 173..255 = wider spreading, but increasing flicker -// -// Total light is NOT entirely conserved, so many repeated -// calls to 'blur' will also result in the light fading, -// eventually all the way to black; this is by design so that -// it can be used to (slowly) clear the LEDs to black. - -void Segment::blur1d(fract8 blur_amount) { - const uint16_t rows = virtualHeight(); - for (uint16_t y = 0; y < rows; y++) blurRow(y, blur_amount); } void Segment::moveX(int8_t delta, bool wrap) { + if (!isActive()) return; // not active const uint16_t cols = virtualWidth(); const uint16_t rows = virtualHeight(); if (!delta || abs(delta) >= cols) return; @@ -448,6 +623,7 @@ void Segment::moveX(int8_t delta, bool wrap) { } void Segment::moveY(int8_t delta, bool wrap) { + if (!isActive()) return; // not active const uint16_t cols = virtualWidth(); const uint16_t rows = virtualHeight(); if (!delta || abs(delta) >= rows) return; @@ -482,66 +658,255 @@ void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { } } -void Segment::draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { - // Bresenham’s Algorithm - int d = 3 - (2*radius); - int y = radius, x = 0; - while (y >= x) { - setPixelColorXY(cx+x, cy+y, col); - setPixelColorXY(cx-x, cy+y, col); - setPixelColorXY(cx+x, cy-y, col); - setPixelColorXY(cx-x, cy-y, col); - setPixelColorXY(cx+y, cy+x, col); - setPixelColorXY(cx-y, cy+x, col); - setPixelColorXY(cx+y, cy-x, col); - setPixelColorXY(cx-y, cy-x, col); - x++; - if (d > 0) { - y--; - d += 4 * (x - y) + 10; - } else { - d += 4 * x + 6; +void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { + if (!isActive() || radius == 0) return; // not active + if (soft) { + // Xiaolin Wu’s algorithm + int rsq = radius*radius; + int x = 0; + int y = radius; + unsigned oldFade = 0; + while (x < y) { + float yf = sqrtf(float(rsq - x*x)); // needs to be floating point + unsigned fade = float(0xFFFF) * (ceilf(yf) - yf); // how much color to keep + if (oldFade > fade) y--; + oldFade = fade; + setPixelColorXY(cx+x, cy+y, color_blend(col, getPixelColorXY(cx+x, cy+y), fade, true)); + setPixelColorXY(cx-x, cy+y, color_blend(col, getPixelColorXY(cx-x, cy+y), fade, true)); + setPixelColorXY(cx+x, cy-y, color_blend(col, getPixelColorXY(cx+x, cy-y), fade, true)); + setPixelColorXY(cx-x, cy-y, color_blend(col, getPixelColorXY(cx-x, cy-y), fade, true)); + setPixelColorXY(cx+y, cy+x, color_blend(col, getPixelColorXY(cx+y, cy+x), fade, true)); + setPixelColorXY(cx-y, cy+x, color_blend(col, getPixelColorXY(cx-y, cy+x), fade, true)); + setPixelColorXY(cx+y, cy-x, color_blend(col, getPixelColorXY(cx+y, cy-x), fade, true)); + setPixelColorXY(cx-y, cy-x, color_blend(col, getPixelColorXY(cx-y, cy-x), fade, true)); + setPixelColorXY(cx+x, cy+y-1, color_blend(getPixelColorXY(cx+x, cy+y-1), col, fade, true)); + setPixelColorXY(cx-x, cy+y-1, color_blend(getPixelColorXY(cx-x, cy+y-1), col, fade, true)); + setPixelColorXY(cx+x, cy-y+1, color_blend(getPixelColorXY(cx+x, cy-y+1), col, fade, true)); + setPixelColorXY(cx-x, cy-y+1, color_blend(getPixelColorXY(cx-x, cy-y+1), col, fade, true)); + setPixelColorXY(cx+y-1, cy+x, color_blend(getPixelColorXY(cx+y-1, cy+x), col, fade, true)); + setPixelColorXY(cx-y+1, cy+x, color_blend(getPixelColorXY(cx-y+1, cy+x), col, fade, true)); + setPixelColorXY(cx+y-1, cy-x, color_blend(getPixelColorXY(cx+y-1, cy-x), col, fade, true)); + setPixelColorXY(cx-y+1, cy-x, color_blend(getPixelColorXY(cx-y+1, cy-x), col, fade, true)); + x++; + } + } else { + // Bresenham’s Algorithm + int d = 3 - (2*radius); + int y = radius, x = 0; + while (y >= x) { + setPixelColorXY(cx+x, cy+y, col); + setPixelColorXY(cx-x, cy+y, col); + setPixelColorXY(cx+x, cy-y, col); + setPixelColorXY(cx-x, cy-y, col); + setPixelColorXY(cx+y, cy+x, col); + setPixelColorXY(cx-y, cy+x, col); + setPixelColorXY(cx+y, cy-x, col); + setPixelColorXY(cx-y, cy-x, col); + x++; + if (d > 0) { + y--; + d += 4 * (x - y) + 10; + } else { + d += 4 * x + 6; + } } } } // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs -void Segment::fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for (int16_t y = -radius; y <= radius; y++) { - for (int16_t x = -radius; x <= radius; x++) { - if (x * x + y * y <= radius * radius && - int16_t(cx)+x>=0 && int16_t(cy)+y>=0 && - int16_t(cx)+x 3) && !soft) ? 1:0); // WLEDMM pre-compute r^2; '-1' removes spikes from bigger blobs + // WLEDMM pre-compute boundaries + const int startx = max(-radius, -int(cx)); + const int endx = min(radius, cols-1-int(cx)); + const int starty = max(-radius, -int(cy)); + const int endy = min(radius, rows-1-int(cy)); + + // fill it - WLEDMM optimized + for (int y = starty; y <= endy; y++) { + for (int x = startx; x <= endx; x++) { + if ((x * x + y * y) <= maxRadius2) { setPixelColorXY(cx + x, cy + y, col); + } } } } -void Segment::nscale8(uint8_t scale) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for(uint16_t y = 0; y < rows; y++) for (uint16_t x = 0; x < cols; x++) { - setPixelColorXY(x, y, CRGB(getPixelColorXY(x, y)).nscale8(scale)); +void Segment::nscale8(uint8_t scale) { //WLEDMM: use fast types + if (!isActive()) return; // not active + const uint_fast16_t cols = virtualWidth(); + const uint_fast16_t rows = virtualHeight(); + for(uint_fast16_t y = 0; y < rows; y++) for (uint_fast16_t x = 0; x < cols; x++) { + setPixelColorXY((int)x, (int)y, CRGB(getPixelColorXY(x, y)).nscale8(scale)); } } //line function -void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c) { - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); +void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft, uint8_t depth) { + if (!isActive()) return; // not active + // if (Segment::maxHeight==1) return; // not a matrix set-up + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (x0 >= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; - const int16_t dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; - for (;;) { - setPixelColorXY(x0,y0,c); - if (x0==x1 && y0==y1) break; - e2 = err; - if (e2 >-dx) { err -= dy; x0 += sx; } - if (e2 < dy) { err += dx; y0 += sy; } + + // WLEDMM shortcut when no grouping/spacing used + bool simpleSegment = (grouping == 1) && (spacing == 0); + uint32_t scaled_col = c; + if (simpleSegment) { + // segment brightness must be pre-calculated for the "fast" setPixelColorXY variant! + #ifdef WLEDMM_FASTPATH + uint8_t _bri_t = _brightness; + #else + uint8_t _bri_t = currentBri(on ? opacity : 0); + #endif + if (!_bri_t && !transitional) return; + if (_bri_t < 255) scaled_col = color_fade(c, _bri_t); } + + // WLEDMM shorten line according to depth + if (depth < UINT8_MAX) { + if (depth == 0) return; // nothing to paint + if (depth<2) {x1 = x0; y1=y0; } // single pixel + else { // shorten line + x0 *=2; y0 *=2; // we do everything "*2" for better rounding + int dx1 = ((int(2*x1) - int(x0)) * int(depth)) / 255; // X distance, scaled down by depth + int dy1 = ((int(2*y1) - int(y0)) * int(depth)) / 255; // Y distance, scaled down by depth + x1 = (x0 + dx1 +1) / 2; + y1 = (y0 + dy1 +1) / 2; + x0 /=2; y0 /=2; + } + } + + const int dx = abs(x1-x0), sx = x0 dx; + if (steep) { + // we need to go along longest dimension + std::swap(x0,y0); + std::swap(x1,y1); + } + if (x0 > x1) { + // we need to go in increasing fashion + std::swap(x0,x1); + std::swap(y0,y1); + } + float gradient = x1-x0 == 0 ? 1.0f : float(y1-y0) / float(x1-x0); + float intersectY = y0; + for (int x = x0; x <= x1; x++) { + unsigned keep = float(0xFFFF) * (intersectY-int(intersectY)); // how much color to keep + unsigned seep = 0xFFFF - keep; // how much background to keep + int y = int(intersectY); + if (steep) std::swap(x,y); // temporarily swap if steep + // pixel coverage is determined by fractional part of y co-ordinate + + // WLEDMM added out-of-bounds check: "unsigned(x) < cols" catches negative numbers _and_ too large values + if ((unsigned(x) < unsigned(cols)) && (unsigned(y) < unsigned(rows))) setPixelColorXY(x, y, color_blend(c, getPixelColorXY(x, y), keep, true)); + int xx = x+int(steep); + int yy = y+int(!steep); + if ((unsigned(xx) < unsigned(cols)) && (unsigned(yy) < unsigned(rows))) setPixelColorXY(xx, yy, color_blend(c, getPixelColorXY(xx, yy), seep, true)); + + intersectY += gradient; + if (steep) std::swap(x,y); // restore if steep + } + } else { + // Bresenham's algorithm + int err = (dx>dy ? dx : -dy)/2; // error direction + for (;;) { + // if (x0 >= cols || y0 >= rows) break; // WLEDMM we hit the edge - should never happen + if (simpleSegment) setPixelColorXY_fast(x0, y0, c, scaled_col, cols, rows); + else setPixelColorXY_slow(x0, y0, c); + if (x0==x1 && y0==y1) break; + int e2 = err; + if (e2 >-dx) { err -= dy; x0 += sx; } + if (e2 < dy) { err += dx; y0 += sy; } + } + } +} + +void Segment::drawArc(unsigned x0, unsigned y0, int radius, uint32_t color, uint32_t fillColor) { + if (!isActive() || (radius <=0)) return; // not active + float minradius = float(radius) - .5f; + float maxradius = float(radius) + .5f; + // WLEDMM pre-calculate values to speed up the loop + const int minradius2 = roundf(minradius * minradius); + const int maxradius2 = roundf(maxradius * maxradius); + + // WLEDMM only loop over surrounding square (50% faster) + const int width = virtualWidth(); + const int height = virtualHeight(); + const int startx = max(0, int(x0)-radius-1); + const int endx = min(width, int(x0)+radius+1); + const int starty = max(0, int(y0)-radius-1); + const int endy = min(height, int(y0)+radius+1); + + for (int x=startx; x= minradius2) && (distance2 <= maxradius2)) { + setPixelColorXY(x, y, color); + } else { + if (fillColor != 0) + if (distance2 < minradius2) + setPixelColorXY(x, y, fillColor); + } + } + } +} + +//WLEDMM for artifx +bool Segment::jsonToPixels(char * name, uint8_t fileNr) { + if (!isActive()) return true; // segment not active, nothing to do + char fileName[32] = { '\0' }; + //WLEDMM: als support segment name ledmaps + bool isFile = false; + // strcpy_P(fileName, PSTR("/mario")); + snprintf(fileName, sizeof(fileName), "/%s%d.json", name, fileNr); //WLEDMM: trick to not include 0 in ledmap.json + // strcat(fileName, ".json"); + isFile = WLED_FS.exists(fileName); + + if (!isFile) { + return false; + } + + if (!requestJSONBufferLock(23)) return false; + + if (!readObjectFromFile(fileName, nullptr, &doc)) { + releaseJSONBufferLock(); + return false; //if file does not exist just exit + } + + JsonArray map = doc[F("seg")][F("i")]; + + if (!map.isNull() && map.size()) { // not an empty map + + for (uint16_t i=0; i 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries const uint16_t cols = virtualWidth(); @@ -560,27 +926,51 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, const int font = w*h; CRGB col = CRGB(color); - CRGBPalette16 grad = CRGBPalette16(col, col2 ? CRGB(col2) : col); + CRGBPalette16 grad = CRGBPalette16(col, (col2 != BLACK) ? CRGB(col2) : col); + uint32_t bgCol = SEGCOLOR(1); //if (w<5 || w>6 || h!=8) return; + if (drawShadow) w++; // one more column for shadow on right side for (int i = 0; i= rows) break; // drawing off-screen uint8_t bits = 0; + uint8_t bits_up = 0; // WLEDMM this is the previous line: font[(chr * h) + i -1] switch (font) { - case 24: bits = pgm_read_byte_near(&console_font_4x6[(chr * h) + i]); break; // 5x8 font - case 40: bits = pgm_read_byte_near(&console_font_5x8[(chr * h) + i]); break; // 5x8 font - case 48: bits = pgm_read_byte_near(&console_font_6x8[(chr * h) + i]); break; // 6x8 font - case 63: bits = pgm_read_byte_near(&console_font_7x9[(chr * h) + i]); break; // 7x9 font - case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); break; // 5x12 font + case 24: bits = pgm_read_byte_near(&console_font_4x6[(chr * h) + i]); + if ((i>0) && drawShadow) bits_up = pgm_read_byte_near(&console_font_4x6[(chr * h) + i -1]); + break; // 5x8 font + case 40: bits = pgm_read_byte_near(&console_font_5x8[(chr * h) + i]); + if ((i>0) && drawShadow) bits_up = pgm_read_byte_near(&console_font_5x8[(chr * h) + i -1]); + break; // 5x8 font + case 48: bits = pgm_read_byte_near(&console_font_6x8[(chr * h) + i]); + if ((i>0) && drawShadow) bits_up = pgm_read_byte_near(&console_font_6x8[(chr * h) + i -1]); + break; // 6x8 font + case 63: bits = pgm_read_byte_near(&console_font_7x9[(chr * h) + i]); + if ((i>0) && drawShadow) bits_up = pgm_read_byte_near(&console_font_7x9[(chr * h) + i -1]); + break; // 7x9 font + case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); + if ((i>0) && drawShadow) bits_up = pgm_read_byte_near(&console_font_5x12[(chr * h) + i -1]); + break; // 5x12 font default: return; } - col = ColorFromPalette(grad, (i+1)*255/h, 255, NOBLEND); + if (col2 != BLACK) col = ColorFromPalette(grad, (i+1)*255/h, 255, NOBLEND); + uint32_t fgCol = uint32_t(col) & 0x00FFFFFF; // WLEDMM cache color value + for (int j = 0; j= 0 || x0 < cols) && ((bits>>(j+(8-w))) & 0x01)) { // bit set & drawing on-screen - setPixelColorXY(x0, y0, col); + if ((x0 >= 0) || (x0 < cols)) { + if ((bits>>(j+(8-w))) & 0x01) { // bit set & drawing on-screen + setPixelColorXY(x0, y0, fgCol); + } else { + if (drawShadow) { + // WLEDMM + if ((j < (w-1)) && (bits>>(j+(8-w) +1)) & 0x01) setPixelColorXY(x0, y0, bgCol); // blank when pixel to the right is set + else if ((j > 0) && (bits>>(j+(8-w) -1)) & 0x01) setPixelColorXY(x0, y0, bgCol);// blank when pixel to the left is set + else if ((bits_up>>(j+(8-w))) & 0x01) setPixelColorXY(x0, y0, bgCol); // blank when pixel above is set + } + } } } } @@ -588,6 +978,7 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, #define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8)) void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu + if (!isActive()) return; // not active // extract the fractional parts and derive their inverses uint8_t xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; // calculate the intensities for each affected pixel @@ -595,11 +986,14 @@ void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)}; // multiply the intensities by the colour, and saturating-add them to the pixels for (int i = 0; i < 4; i++) { - CRGB led = getPixelColorXY((x >> 8) + (i & 1), (y >> 8) + ((i >> 1) & 1)); + int wu_x = (x >> 8) + (i & 1); // WLEDMM precalculate x + int wu_y = (y >> 8) + ((i >> 1) & 1); // WLEDMM precalculate y + CRGB led = getPixelColorXY(wu_x, wu_y); + CRGB oldLed = led; led.r = qadd8(led.r, c.r * wu[i] >> 8); led.g = qadd8(led.g, c.g * wu[i] >> 8); led.b = qadd8(led.b, c.b * wu[i] >> 8); - setPixelColorXY(int((x >> 8) + (i & 1)), int((y >> 8) + ((i >> 1) & 1)), led); + if (led != oldLed) setPixelColorXY(wu_x, wu_y, led); // WLEDMM don't repaint same color } } #undef WU_WEIGHT diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index c60ea5f23c..b09136f533 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -2,30 +2,14 @@ WS2812FX_fcn.cpp contains all utility functions Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) - Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - Modified heavily for WLED */ #include "wled.h" #include "FX.h" #include "palettes.h" +#ifdef ARDUINO_ARCH_ESP32 +#include // WLEDMM to get esp_timer_get_time() +#endif /* Custom per-LED mapping has moved! @@ -69,89 +53,185 @@ #error "Max segments must be at least max number of busses!" #endif +// WLEDMM experimental . this is a "C style" wrapper for strip.waitUntilIdle() +// This workaround is just needed for the segment class, that does't know about "strip" +void strip_wait_until_idle(String whoCalledMe) { +#if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_PROTECT_SERVICE) // WLEDMM experimental + if (strip.isServicing() && (strncmp(pcTaskGetTaskName(NULL), "loopTask", 8) != 0)) { // if we are in looptask (arduino loop), its safe to proceed without waiting + USER_PRINTLN(whoCalledMe + String(": strip is still drawing effects.")); + strip.waitUntilIdle(); + } +#endif +} +// WLEDMM another helper for segment class +bool strip_uses_global_leds(void) { + return strip.useLedsArray; +} /////////////////////////////////////////////////////////////////////////////// // Segment class implementation /////////////////////////////////////////////////////////////////////////////// -uint16_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] +size_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] CRGB *Segment::_globalLeds = nullptr; uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; uint16_t Segment::maxHeight = 1; -// copy constructor +CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); + +// copy constructor - creates a new segment by copy from orig, but does not copy buffers. Does not modify orig! Segment::Segment(const Segment &orig) { - //DEBUG_PRINTLN(F("-- Copy segment constructor --")); - memcpy((void*)this, (void*)&orig, sizeof(Segment)); + DEBUG_PRINTLN(F("-- Copy segment constructor --")); + memcpy((void*)this, (void*)&orig, sizeof(Segment)); //WLEDMM copy to this + transitional = false; // copied segment cannot be in transition + // WLEDMM temporarily prevent any fast draw calls to the new segment + // _isValid2D = false; + _isSimpleSegment = false; + _isSuperSimpleSegment = false; + name = nullptr; data = nullptr; _dataLen = 0; _t = nullptr; - if (leds && !Segment::_globalLeds) leds = nullptr; - if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + if (ledsrgb && !Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;} // WLEDMM + if (orig.name) { name = new(std::nothrow) char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } - if (orig._t) { _t = new Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } - if (orig.leds && !Segment::_globalLeds) { leds = (CRGB*)malloc(sizeof(CRGB)*length()); if (leds) memcpy(leds, orig.leds, sizeof(CRGB)*length()); } + //if (orig._t) { _t = new(std::nothrow) Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } + //else markForReset(); // WLEDMM + // if (orig.ledsrgb && !Segment::_globalLeds) { allocLeds(); if (ledsrgb) memcpy(ledsrgb, orig.ledsrgb, sizeof(CRGB)*length()); } // WLEDMM + jMap = nullptr; //WLEDMM jMap +} + +//WLEDMM: recreate ledsrgb if more space needed (will not free ledsrgb!) +void Segment::allocLeds() { + size_t size = sizeof(CRGB)*max((size_t) length(), ledmapMaxSize); // TroyHacks + if ((size < sizeof(CRGB)) || (size > 164000)) { //softhack too small (<3) or too large (>160Kb) + DEBUG_PRINTF("allocLeds warning: size == %u !!\n", size); + if (ledsrgb && (ledsrgbSize == 0)) { + USER_PRINTLN("allocLeds warning: ledsrgbSize == 0 but ledsrgb!=NULL"); + free(ledsrgb); ledsrgb=nullptr; + } // softhack007 clean up buffer + } + if ((size > 0) && (!ledsrgb || size > ledsrgbSize)) { //softhack dont allocate zero bytes + USER_PRINTF("allocLeds (%d,%d to %d,%d), %u from %u\n", start, startY, stop, stopY, size, ledsrgb?ledsrgbSize:0); + if (ledsrgb) free(ledsrgb); // we need a bigger buffer, so free the old one first + ledsrgb = (CRGB*)calloc(size, 1); + ledsrgbSize = ledsrgb?size:0; + if (ledsrgb == nullptr) { + USER_PRINTLN("allocLeds failed!!"); + errorFlag = ERR_LOW_BUF; // WLEDMM raise errorflag + } + } + else { + //USER_PRINTF("reuse Leds %u from %u\n", size, ledsrgb?ledsrgbSize:0); + } } -// move constructor +// move constructor --> moves everything (including buffer) from orig to this Segment::Segment(Segment &&orig) noexcept { - //DEBUG_PRINTLN(F("-- Move segment constructor --")); + DEBUG_PRINTLN(F("-- Move segment constructor --")); + + // WLEDMM temporarily prevent any fast draw calls to old and new segment + orig._isSimpleSegment = false; + orig._isSuperSimpleSegment = false; + memcpy((void*)this, (void*)&orig, sizeof(Segment)); + orig.transitional = false; // old segment cannot be in transition any more +#ifdef WLEDMM_FASTPATH + // WLEDMM prevent any draw calls to old segment + orig._isValid2D = false; +#endif orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; orig._t = nullptr; - orig.leds = nullptr; + orig.ledsrgb = nullptr; //WLEDMM + orig.ledsrgbSize = 0; // WLEDMM + orig.jMap = nullptr; //WLEDMM jMap } -// copy assignment +// copy assignment --> overwrite segment with orig - deletes old buffers in "this", but does not change orig! Segment& Segment::operator= (const Segment &orig) { - //DEBUG_PRINTLN(F("-- Copying segment --")); + DEBUG_PRINTLN(F("-- Copy-assignment segment --")); if (this != &orig) { // clean destination + transitional = false; // copied segment cannot be in transition if (name) delete[] name; if (_t) delete _t; - if (leds && !Segment::_globalLeds) free(leds); + CRGB* oldLeds = ledsrgb; + size_t oldLedsSize = ledsrgbSize; + if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); deallocateData(); // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); + transitional = false; + + // WLEDMM prevent any fast draw calls to this segment until the next frame starts + //_isValid2D = false; + _isSimpleSegment = false; + _isSuperSimpleSegment = false; + // erase pointers to allocated data name = nullptr; data = nullptr; _dataLen = 0; _t = nullptr; - if (!Segment::_globalLeds) leds = nullptr; + //if (!Segment::_globalLeds) {ledsrgb = oldLeds; ledsrgbSize = oldLedsSize;}; // WLEDMM reuse leds instead of ledsrgb = nullptr; + if (!Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;}; // WLEDMM copy has no buffers (yet) // copy source data - if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + if (orig.name) { name = new(std::nothrow) char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } - if (orig._t) { _t = new Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } - if (orig.leds && !Segment::_globalLeds) { leds = (CRGB*)malloc(sizeof(CRGB)*length()); if (leds) memcpy(leds, orig.leds, sizeof(CRGB)*length()); } + //if (orig._t) { _t = new(std::nothrow) Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } + //else markForReset(); // WLEDMM + //if (orig.ledsrgb && !Segment::_globalLeds) { allocLeds(); if (ledsrgb) memcpy(ledsrgb, orig.ledsrgb, sizeof(CRGB)*length()); } // WLEDMM don't copy old buffer + jMap = nullptr; //WLEDMM jMap } return *this; } -// move assignment +// move assignment --> moves everything (including buffers) from "orig" to "this" Segment& Segment::operator= (Segment &&orig) noexcept { - //DEBUG_PRINTLN(F("-- Moving segment --")); + DEBUG_PRINTLN(F("-- Move-assignment segment --")); if (this != &orig) { - if (name) delete[] name; // free old name + transitional = false; // just temporary + if (name) { delete[] name; name = nullptr; } // free old name deallocateData(); // free old runtime data - if (_t) delete _t; - if (leds && !Segment::_globalLeds) free(leds); + if (_t) { delete _t; _t = nullptr; } + if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); //WLEDMM: not needed anymore as we will use leds from copy. no need to nullify ledsrgb as it gets new value in memcpy + + // WLEDMM temporarily prevent any fast draw calls to old and new segment + orig._isSimpleSegment = false; + orig._isSuperSimpleSegment = false; + memcpy((void*)this, (void*)&orig, sizeof(Segment)); +#ifdef WLEDMM_FASTPATH + // WLEDMM temporarily prevent any draw calls to old segment + orig._isValid2D = false; +#endif orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; orig._t = nullptr; - orig.leds = nullptr; + orig.ledsrgb = nullptr; //WLEDMM: do not free as moved to here + orig.ledsrgbSize = 0; //WLEDMM + orig.jMap = nullptr; //WLEDMM jMap } return *this; } bool Segment::allocateData(size_t len) { - if (data && _dataLen == len) return true; //already allocated + // WLEDMM + if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) + if ((call == 0) && (len > 0)) memset(data, 0, len); // erase buffer if called during effect initialisation + return true; + } + //DEBUG_PRINTF("allocateData(%u) start %d, stop %d, vlen %d\n", len, start, stop, virtualLength()); deallocateData(); - if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) return false; //not enough memory + if (len == 0) return false; // nothing to do + if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { + //USER_PRINTF("Segment::allocateData: Segment data quota exceeded! used:%u request:%u max:%d\n", Segment::getUsedSegmentData(), len, MAX_SEGMENT_DATA); + if (len > 0) errorFlag = ERR_LOW_SEG_MEM; // WLEDMM raise errorflag + return false; //not enough memory + } // do not use SPI RAM on ESP32 since it is slow //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) //if (psramFound()) @@ -159,17 +239,25 @@ bool Segment::allocateData(size_t len) { //else //#endif data = (byte*) malloc(len); - if (!data) return false; //allocation failed + if (!data) { + _dataLen = 0; // WLEDMM reset dataLen + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + USER_PRINT(F("Segment::allocateData: FAILED to allocate ")); + USER_PRINT(len); USER_PRINTLN(F(" bytes.")); + return false; + } //allocation failed Segment::addUsedSegmentData(len); _dataLen = len; memset(data, 0, len); + if (errorFlag == ERR_LOW_SEG_MEM) errorFlag = ERR_NONE; // WLEDMM reset errorflag on success return true; } void Segment::deallocateData() { - if (!data) return; + if (!data) {_dataLen = 0; return;} // WLEDMM reset dataLen free(data); data = nullptr; + //USER_PRINTF("Segment::deallocateData: free'd %d bytes.\n", _dataLen); Segment::addUsedSegmentData(-_dataLen); _dataLen = 0; } @@ -183,39 +271,53 @@ void Segment::deallocateData() { */ void Segment::resetIfRequired() { if (reset) { - if (leds && !Segment::_globalLeds) { free(leds); leds = nullptr; } + if (ledsrgb && !Segment::_globalLeds) { free(ledsrgb); ledsrgb = nullptr; ledsrgbSize=0;} // WLEDMM segment has changed, so we need a fresh buffer. if (transitional && _t) { transitional = false; delete _t; _t = nullptr; } deallocateData(); next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; // setOption(SEG_OPTION_RESET, false); + startFrame(); // WLEDMM update cached propoerties + if (isActive() && !freeze) { fill(BLACK); needsBlank = false; } // WLEDMM start clean + DEBUG_PRINTLN("Segment reset"); + } else if (needsBlank) { + startFrame(); // WLEDMM update cached propoerties + if (isActive() && !freeze) { + fill(BLACK); // WLEDMM start clean + DEBUG_PRINTLN("Segment blanked"); + needsBlank = false; + } } } void Segment::setUpLeds() { // deallocation happens in resetIfRequired() as it is called when segment changes or in destructor - if (Segment::_globalLeds) + if (Segment::_globalLeds) { #ifndef WLED_DISABLE_2D - leds = &Segment::_globalLeds[start + startY*Segment::maxWidth]; + ledsrgb = &Segment::_globalLeds[start + startY*Segment::maxWidth]; + ledsrgbSize = length() * sizeof(CRGB); // also set this when using global leds. + //USER_PRINTF("\nsetUpLeds() Global LEDs: startX=%d stopx=%d startY=%d stopy=%d maxwidth=%d; length=%d, size=%d\n\n", start, stop, startY, stopY, Segment::maxWidth, length(), ledsrgbSize/3); #else - leds = &Segment::_globalLeds[start]; + ledsrgb = &Segment::_globalLeds[start]; + ledsrgbSize = length() * sizeof(CRGB); // also set this when using global leds. #endif - else if (leds == nullptr && length() > 0) { //softhack007 quickfix - avoid malloc(0) which is undefined behaviour (should not happen, but i've seen it) - //#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM) + } else if (length() > 0) { //WLEDMM we always want a new buffer //softhack007 quickfix - avoid malloc(0) which is undefined behaviour (should not happen, but i've seen it) + //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) //if (psramFound()) - // leds = (CRGB*)ps_malloc(sizeof(CRGB)*length()); // softhack007 disabled; putting leds into psram leads to horrible slowdown on WROVER boards + // ledsrgb = (CRGB*)ps_malloc(sizeof(CRGB)*length()); // softhack007 disabled; putting leds into psram leads to horrible slowdown on WROVER boards //else //#endif - leds = (CRGB*)malloc(sizeof(CRGB)*length()); + allocLeds(); //WLEDMM + //USER_PRINTF("\nsetUpLeds() local LEDs: startX=%d stopx=%d startY=%d stopy=%d maxwidth=%d; length=%d, size=%d\n\n", start, stop, startY, stopY, Segment::maxWidth, length(), ledsrgbSize/3); } } -CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { - static unsigned long _lastPaletteChange = 0; // perhaps it should be per segment +CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) const { + static unsigned long _lastPaletteChange = millis() - 990000; // perhaps it should be per segment //WLEDMM changed init value to avoid pure orange after startup static CRGBPalette16 randomPalette = CRGBPalette16(DEFAULT_COLOR); static CRGBPalette16 prevRandomPalette = CRGBPalette16(CRGB(BLACK)); - byte tcp[72]; + byte tcp[76] = { 255 }; //WLEDMM: prevent out-of-range access in loadDynamicGradientPalette() if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; + if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip //default palette. Differs depending on effect if (pal == 0) switch (mode) { case FX_MODE_FIRE_2012 : pal = 35; break; // heat palette @@ -233,7 +335,27 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { switch (pal) { case 0: //default palette. Exceptions for specific effects above targetPalette = PartyColors_p; break; - case 1: {//periodically replace palette with a random one. Transition palette change in 500ms + case 1: {//Random smooth: periodically replace palette with a random one. Transition palette change in 500ms + uint32_t timeSinceLastChange = millis() - _lastPaletteChange; + if (timeSinceLastChange > randomPaletteChangeTime * 1000U) { + prevRandomPalette = randomPalette; + randomPalette = CRGBPalette16( + CHSV(random8(), random8(160, 255), random8(128, 255)), + CHSV(random8(), random8(160, 255), random8(128, 255)), + CHSV(random8(), random8(160, 255), random8(128, 255)), + CHSV(random8(), random8(160, 255), random8(128, 255))); + _lastPaletteChange = millis(); + timeSinceLastChange = 0; + } + + //WLEDMM: smooth transitions of palettes instead of every 5 sec with short transition + for (int i=0; i< 16; i++) { + targetPalette[i].r = prevRandomPalette[i].r*(5000-timeSinceLastChange)/5000 + randomPalette[i].r*timeSinceLastChange/5000; + targetPalette[i].g = prevRandomPalette[i].g*(5000-timeSinceLastChange)/5000 + randomPalette[i].g*timeSinceLastChange/5000; + targetPalette[i].b = prevRandomPalette[i].b*(5000-timeSinceLastChange)/5000 + randomPalette[i].b*timeSinceLastChange/5000; + } + break;} + case 74: {//periodically replace palette with a random one. Transition palette change in 500ms uint32_t timeSinceLastChange = millis() - _lastPaletteChange; if (timeSinceLastChange > randomPaletteChangeTime * 1000U) { prevRandomPalette = randomPalette; @@ -268,7 +390,7 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { CRGB sec = gamma32(colors[1]); CRGB ter = gamma32(colors[2]); targetPalette = CRGBPalette16(ter,sec,prim); break;} - case 5: {//primary + secondary (+tert if not off), more distinct + case 5: {//primary + secondary (+tertiary if not off), more distinct CRGB prim = gamma32(colors[0]); CRGB sec = gamma32(colors[1]); if (colors[2]) { @@ -292,6 +414,10 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { targetPalette = RainbowColors_p; break; case 12: //Rainbow stripe colors targetPalette = RainbowStripeColors_p; break; + case 71: //WLEDMM netmindz ar palette +1 + case 72: //WLEDMM netmindz ar palette +2 + case 73: //WLEDMM netmindz ar palette +3 + targetPalette.loadDynamicGradientPalette(getAudioPalette(pal)); break; default: //progmem palettes if (pal>245) { targetPalette = strip.customPalettes[255-pal]; // we checked bounds above @@ -315,7 +441,7 @@ void Segment::startTransition(uint16_t dur) { uint32_t _colorT[NUM_COLORS]; for (size_t i=0; i_briT = _briT; _t->_cctT = _cctT; @@ -325,53 +451,58 @@ void Segment::startTransition(uint16_t dur) { transitional = true; // setOption(SEG_OPTION_TRANSITIONAL, true); } +// WLEDMM Segment::progress() is declared inline, see FX.h +#if 0 // transition progression between 0-65535 -uint16_t Segment::progress() { +uint16_t IRAM_ATTR_YN Segment::progress() const { if (!transitional || !_t) return 0xFFFFU; - uint32_t timeNow = millis(); + unsigned long timeNow = millis(); if (timeNow - _t->_start > _t->_dur || _t->_dur == 0) return 0xFFFFU; return (timeNow - _t->_start) * 0xFFFFU / _t->_dur; } +#endif -uint8_t Segment::currentBri(uint8_t briNew, bool useCct) { - if (transitional && _t) { - uint32_t prog = progress() + 1; - if (useCct) return ((briNew * prog) + _t->_cctT * (0x10000 - prog)) >> 16; - else return ((briNew * prog) + _t->_briT * (0x10000 - prog)) >> 16; +// WLEDMM Segment::currentBri() is declared inline, see FX.h +#if 0 +uint8_t IRAM_ATTR_YN Segment::currentBri(uint8_t briNew, bool useCct) const { + uint32_t prog = (transitional && _t) ? progress() : 0xFFFFU; + if (transitional && _t && prog < 0xFFFFU) { + if (useCct) return ((briNew * prog) + _t->_cctT * (0xFFFFU - prog)) >> 16; + else return ((briNew * prog) + _t->_briT * (0xFFFFU - prog)) >> 16; } else { - return briNew; + return (useCct ? briNew : (on ? briNew : 0)); // WLEDMM aligned with upstream } } +#endif uint8_t Segment::currentMode(uint8_t newMode) { - return (progress()>32767U) ? newMode : _t->_modeP; // change effect in the middle of transition + return (progress()>32767U) ? newMode : (_t ? _t->_modeP : newMode); // change effect in the middle of transition } uint32_t Segment::currentColor(uint8_t slot, uint32_t colorNew) { return transitional && _t ? color_blend(_t->_colorT[slot], colorNew, progress(), true) : colorNew; } -CRGBPalette16 &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal) { - loadPalette(targetPalette, pal); +void Segment::setCurrentPalette() { + loadPalette(_currentPalette, palette); if (transitional && _t && progress() < 0xFFFFU) { // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms - uint32_t timeMS = millis() - _t->_start; - uint16_t noOfBlends = (255U * timeMS / _t->_dur) - _t->_prevPaletteBlends; - for (int i=0; i_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, targetPalette, 48); - targetPalette = _t->_palT; // copy transitioning/temporary palette + unsigned long timeMS = millis() - _t->_start; + uint16_t noOfBlends = min(64UL, (255U * timeMS / _t->_dur) - _t->_prevPaletteBlends); // WLEDMM limit to 64 blends at once, prevent rollover + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); + _currentPalette = _t->_palT; // copy transitioning/temporary palette } - return targetPalette; } void Segment::handleTransition() { - if (!transitional) return; + if (!transitional || !_t) return; // Early exit if no transition active unsigned long maxWait = millis() + 20; if (mode == FX_MODE_STATIC && next_time > maxWait) next_time = maxWait; if (progress() == 0xFFFFU) { if (_t) { - if (_t->_modeP != mode) markForReset(); + //if (_t->_modeP != mode) markForReset(); // WLEDMM effect transition disabled as it does not work (flashes due to double effect restart) delete _t; _t = nullptr; } @@ -389,20 +520,22 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t && (!grp || (grouping == grp && spacing == spc)) && (ofs == UINT16_MAX || ofs == offset)) return; - if (stop) fill(BLACK); //turn old segment range off + stateChanged = true; // send UDP/WS broadcast + + if (stop>start) markForBlank(); //turn old segment range off // WLEDMM stop > start if (i2 <= i1) { //disable segment stop = 0; markForReset(); return; } if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D - stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : (i2 > Segment::maxWidth ? Segment::maxWidth : MAX(1,i2)); + stop = i2 > Segment::maxWidth*Segment::maxHeight ? min(i2,strip.getLengthTotal()) : (i2 > Segment::maxWidth ? Segment::maxWidth : max((uint16_t)1,i2)); // WLEDMM: use native min/max startY = 0; stopY = 1; #ifndef WLED_DISABLE_2D if (Segment::maxHeight>1) { // 2D if (i1Y < Segment::maxHeight) startY = i1Y; - stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : MAX(1,i2Y); + stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : max((uint16_t)1,i2Y); // WLEDMM: use native min/max } #endif if (grp) { @@ -421,7 +554,7 @@ bool Segment::setColor(uint8_t slot, uint32_t c) { //returns true if changed if (slot == 0 && c == BLACK) return false; // on/off segment cannot have primary color black if (slot == 1 && c != BLACK) return false; // on/off segment cannot have secondary color non black } - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change + if (fadeTransition && on) startTransition(strip.getTransition()); // start transition prior to change // WLEDMM only on real change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return true; @@ -434,7 +567,7 @@ void Segment::setCCT(uint16_t k) { k = (k - 1900) >> 5; } if (cct == k) return; - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change + if (fadeTransition && on) startTransition(strip.getTransition()); // start transition prior to change cct = k; stateChanged = true; // send UDP/WS broadcast } @@ -454,7 +587,11 @@ void Segment::setOption(uint8_t n, bool val) { if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET || n == SEG_OPTION_TRANSITIONAL)) stateChanged = true; // send UDP/WS broadcast } -void Segment::setMode(uint8_t fx, bool loadDefaults) { +void Segment::setMode(uint8_t fx, bool loadDefaults, bool sliderDefaultsOnly) { + //WLEDMM: return to old setting if not explicitly set + static int16_t oldMap = -1; + static int16_t oldSim = -1; + static int16_t oldPalette = -1; // if we have a valid mode & is not reserved if (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4)) { if (fx != mode) { @@ -473,14 +610,18 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false; sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; - sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); - sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 1); - sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; - sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business - sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; - sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business - sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0); + if (!sliderDefaultsOnly) { + //WLEDMM: return to old setting if not explicitly set + sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) {if (oldMap==-1) oldMap = map1D2D; map1D2D = constrain(sOpt, 0, 7);} else {if (oldMap!=-1) map1D2D = oldMap; oldMap = -1;} + sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) {if (oldSim==-1) oldSim = soundSim; soundSim = constrain(sOpt, 0, 1);} else {if (oldSim!=-1) soundSim = oldSim; oldSim = -1;} + sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; + sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; + sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) {if (oldPalette==-1) oldPalette = palette; setPalette(sOpt);} else {if (oldPalette!=-1) setPalette(oldPalette); oldPalette = -1;} + } } + /*if (!fadeTransition)*/ markForReset(); // WLEDMM quickfix for effect "double startup" bug. stateChanged = true; // send UDP/WS broadcast } } @@ -490,26 +631,17 @@ void Segment::setPalette(uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes if (pal != palette) { - if (strip.paletteFade) startTransition(strip.getTransition()); + if (strip.paletteFade && on) startTransition(strip.getTransition()); palette = pal; stateChanged = true; // send UDP/WS broadcast } } // 2D matrix -uint16_t Segment::virtualWidth() const { - uint16_t groupLen = groupLength(); - uint16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; - if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED - return vWidth; -} -uint16_t Segment::virtualHeight() const { - uint16_t groupLen = groupLength(); - uint16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; - if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED - return vHeight; -} +// +// WLEDMM Segment::virtualWidth() and Segment::virtualHeight() are declared inline, see FX.h +// uint16_t Segment::nrOfVStrips() const { uint16_t vLen = 1; @@ -517,7 +649,13 @@ uint16_t Segment::nrOfVStrips() const { if (is2D()) { switch (map1D2D) { case M12_pBar: - vLen = virtualWidth(); + vLen = calc_virtualWidth(); + break; + case M12_sCircle: //WLEDMM + vLen = (calc_virtualWidth() + calc_virtualHeight()) / 6; // take third of the average width + break; + case M12_sBlock: //WLEDMM + vLen = (calc_virtualWidth() + calc_virtualHeight()) / 8; // take half of the average width break; } } @@ -525,20 +663,241 @@ uint16_t Segment::nrOfVStrips() const { return vLen; } +//WLEDMM jMap +struct XandY { + uint8_t x; + uint8_t y; +}; +struct ArrayAndSize { + uint8_t size; + XandY *array; +}; +class JMapC { + public: + char previousSegmentName[50] = ""; + + ~JMapC() { + DEBUG_PRINTLN("~JMapC"); + deletejVectorMap(); + } + void deletejVectorMap() { + if (jVectorMap.size() > 0) { + DEBUG_PRINTLN("delete jVectorMap"); + for (size_t i=0; i 0) + return size; + else + return SEGMENT.calc_virtualWidth() * SEGMENT.calc_virtualHeight(); // calc pixel sizes + } + void setPixelColor(uint16_t i, uint32_t col) { + updatejMapDoc(); + if (jVectorMap.size() > i) { + if (i==0) { + SEGMENT.fadeToBlackBy(10); //as not all pixels used + } + for (int j=0; j 0) + return SEGMENT.getPixelColorXY(jVectorMap[i].array[0].x * scale, jVectorMap[i].array[0].y * scale); + else + return 0; + } + private: + std::vector jVectorMap; + StaticJsonDocument<4096> docChunk; //must fit forks with about 32 points each + uint8_t scale; + + void updatejMapDoc() { + if (SEGMENT.name == nullptr && jVectorMap.size() > 0) { + deletejVectorMap(); + } + else if (SEGMENT.name != nullptr && strcmp(SEGMENT.name, previousSegmentName) != 0) { + uint32_t dataSize = 0; + deletejVectorMap(); + DEBUG_PRINT("New "); DEBUG_PRINTLN(SEGMENT.name); + char jMapFileName[50]; + strcpy(jMapFileName, "/"); + strcat(jMapFileName, SEGMENT.name); + strcat(jMapFileName, ".json"); + File jMapFile; + jMapFile = WLED_FS.open(jMapFileName, "r"); + + uint_fast16_t maxWidth = 0; // WLEDMM fix uint8 overflow for large width/height + uint_fast16_t maxHeight = 0; // WLEDMM + + //https://arduinojson.org/v6/how-to/deserialize-a-very-large-document/ + jMapFile.find("["); + do { //for each element in the array + DeserializationError err = deserializeJson(docChunk, jMapFile); + // serializeJson(docChunk, Serial); USER_PRINTLN(); + // USER_PRINTF("docChunk %u / %u%% (%u %u %u) %u\n", (unsigned int)docChunk.memoryUsage(), 100 * docChunk.memoryUsage() / docChunk.capacity(), (unsigned int)docChunk.size(), docChunk.overflowed(), (unsigned int)docChunk.nesting(), jMapFile.size()); + if (err) + { + USER_PRINTF("deserializeJson() of parseTree failed with code %s\n", err.c_str()); + USER_FLUSH(); + if (SEGMENT.name) delete[] SEGMENT.name; SEGMENT.name = nullptr; //need to clear the name as otherwise continuously loaded // softhack007 avoid deleting nullptr + return; + } + + if (docChunk.is()) { //each item is or an array of arrays (fork) or an array of x,y (no fork) + //fill the vector with arrays and get the width and height of the jMap + + JsonArray arrayChunk = docChunk.as(); + ArrayAndSize arrayAndSize; + arrayAndSize.size = 0; + if (arrayChunk[0].is()) { //if array of arrays + arrayAndSize.array = new(std::nothrow) XandY[arrayChunk.size()]; + for (JsonVariant arrayElement: arrayChunk) { + maxWidth = max((uint16_t)maxWidth, arrayElement[0].as()); // WLEDMM use native min/max + maxHeight = max((uint16_t)maxHeight, arrayElement[1].as()); // WLEDMM + arrayAndSize.array[arrayAndSize.size].x = arrayElement[0].as(); + arrayAndSize.array[arrayAndSize.size].y = arrayElement[1].as(); + arrayAndSize.size++; + dataSize += sizeof(XandY); + } + } + else { // if array (of x and y) + arrayAndSize.array = new(std::nothrow) XandY[1]; + maxWidth = max((uint16_t)maxWidth, arrayChunk[0].as()); // WLEDMM use native min/max + maxHeight = max((uint16_t)maxHeight, arrayChunk[1].as()); // WLEDMM + arrayAndSize.array[arrayAndSize.size].x = arrayChunk[0].as(); + arrayAndSize.array[arrayAndSize.size].y = arrayChunk[1].as(); + arrayAndSize.size++; + dataSize += sizeof(XandY); + } + jVectorMap.push_back(arrayAndSize); + dataSize += sizeof(arrayAndSize); + } + + } while (jMapFile.findUntil(",", "]")); + + jMapFile.close(); + + maxWidth++; maxHeight++; + scale = min(SEGMENT.calc_virtualWidth() / maxWidth, SEGMENT.calc_virtualHeight() / maxHeight); // WLEDMM re-calc width/heiht from active settings + dataSize += sizeof(jVectorMap); + USER_PRINT("dataSize "); + USER_PRINT(dataSize); + USER_PRINT(" scale "); + USER_PRINTLN(scale); + strcpy(previousSegmentName, SEGMENT.name); + } + } //updatejMapDoc +}; //class JMapC + +//WLEDMM jMap +void Segment::createjMap() { + if (!jMap) { + DEBUG_PRINTLN("createjMap"); + jMap = new(std::nothrow) JMapC(); + } +} + +//WLEDMM jMap +void Segment::deletejMap() { + //Should be called from ~Segment but causes crash (and ~Segment is called quite often...) + if (jMap) { + DEBUG_PRINTLN("deletejMap"); + delete (JMapC *)jMap; jMap = nullptr; + } +} + + +// Constants for mapping mode "Pinwheel" +#ifndef WLED_DISABLE_2D +constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 +constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" +constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 +constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" +constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 +constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" +constexpr int Pinwheel_Steps_XL = 368; +constexpr int Pinwheel_Size_XL = 58; // larger than this -> use "XXL" +constexpr int Pinwheel_Steps_XXL = 456; +constexpr int Pinwheel_Size_XXL = 68; // larger than this -> use "LL" +constexpr int Pinwheel_Steps_LL = 592; // 128x64 no holes, 28fps +constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians +constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians +constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians +constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians +constexpr float Int_to_Rad_XXL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XXL; // conversion: from 0...456 to Radians +constexpr float Int_to_Rad_LL = (DEG_TO_RAD * 360) / Pinwheel_Steps_LL; // conversion: from 0...592 to Radians + +constexpr int Fixed_Scale = 32768; // fixpoint scaling factor (15bit for fraction) + +// Pinwheel helper function: pixel index to radians +static float getPinwheelAngle(int i, int vW, int vH) { + int maxXY = max(vW, vH); + if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; + if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; + if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; + if (maxXY <= Pinwheel_Size_XL) return float(i) * Int_to_Rad_XL; + if (maxXY <= Pinwheel_Size_XXL) return float(i) * Int_to_Rad_XXL; + // else + return float(i) * Int_to_Rad_LL; +} +// Pinwheel helper function: matrix dimensions to number of rays +static int getPinwheelLength(int vW, int vH) { + int maxXY = max(vW, vH); + if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; + if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; + if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; + if (maxXY <= Pinwheel_Size_XL) return Pinwheel_Steps_XL; + if (maxXY <= Pinwheel_Size_XXL) return Pinwheel_Steps_XXL; + // else + return Pinwheel_Steps_LL; +} +#endif + // 1D strip -uint16_t Segment::virtualLength() const { +uint16_t Segment::calc_virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vW = virtualWidth(); - uint16_t vH = virtualHeight(); + uint16_t vW = calc_virtualWidth(); + uint16_t vH = calc_virtualHeight(); uint16_t vLen = vW * vH; // use all pixels from segment switch (map1D2D) { case M12_pBar: vLen = vH; break; case M12_pCorner: + vLen = max(vW,vH); // get the longest dimension + break; case M12_pArc: + { unsigned vLen2 = vW * vW + vH * vH; // length ^2 + if (vLen2 < UINT16_MAX) vLen = sqrt16(vLen2); // use faster function for 16bit values + else vLen = sqrtf(vLen2); // fall-back to float if bigger + if (vW != vH) vLen++; // round up + } + break; + case M12_jMap: //WLEDMM jMap + if (jMap) + vLen = ((JMapC *)jMap)->length(); + break; + case M12_sCircle: //WLEDMM vLen = max(vW,vH); // get the longest dimension + // vLen = (virtualWidth() + virtualHeight()) * 3; + break; + case M12_sBlock: //WLEDMM + if (nrOfVStrips()>1) + vLen = max(vW,vH) * 4;//0.5; // get the longest dimension + else + vLen = max(vW,vH) * 0.5f; // get the longest dimension + break; + case M12_sPinwheel: + vLen = getPinwheelLength(vW, vH); break; } return vLen; @@ -546,12 +905,39 @@ uint16_t Segment::virtualLength() const { #endif uint16_t groupLen = groupLength(); uint16_t vLength = (length() + groupLen - 1) / groupLen; - if (mirror) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED + if (mirror && width() > 1) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED // WLEDMM bugfix for pseudo 2d strips return vLength; } -void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) +//WLEDMM used for M12_sBlock +static void xyFromBlock(uint16_t &x,uint16_t &y, uint16_t i, uint16_t vW, uint16_t vH, uint16_t vStrip) { + float i2; + if (i<=SEGLEN*0.25f) { //top, left to right + i2 = i/(SEGLEN*0.25f); + x = vW / 2 - vStrip - 1 + i2 * vStrip * 2; + y = vH / 2 - vStrip - 1; + } + else if (i <= SEGLEN * 0.5f) { //right, top to bottom + i2 = (i-SEGLEN*0.25f)/(SEGLEN*0.25f); + x = vW / 2 + vStrip; + y = vH / 2 - vStrip - 1 + i2 * vStrip * 2; + } + else if (i <= SEGLEN * 0.75f) { //bottom, right to left + i2 = (i-SEGLEN*0.5f)/(SEGLEN*0.25f); + x = vW / 2 + vStrip - i2 * vStrip * 2; + y = vH / 2 + vStrip; + } + else if (i <= SEGLEN) { //left, bottom to top + i2 = (i-SEGLEN*0.75f)/(SEGLEN*0.25f); + x = vW / 2 - vStrip - 1; + y = vH / 2 + vStrip - i2 * vStrip * 2; + } + +} + +void IRAM_ATTR_YN __attribute__((hot)) Segment::setPixelColor(int i, uint32_t col) //WLEDMM: IRAM_ATTR conditionally { + if (!isActive()) return; // not active #ifndef WLED_DISABLE_2D int vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) #endif @@ -571,40 +957,163 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) case M12_pBar: // expand 1D effect vertically or have it play on virtual strips if (vStrip>0) setPixelColorXY(vStrip - 1, vH - i - 1, col); - else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col); + else drawLine(0,vH-i-1, vW-1,vH-i-1, col, false); // WLEDMM draw line instead of plotting each pixel break; case M12_pArc: // expand in circular fashion from center if (i==0) setPixelColorXY(0, 0, col); else { - float step = HALF_PI / (2.85f*i); - for (float rad = 0.0f; rad <= HALF_PI+step/2; rad += step) { - // may want to try float version as well (with or without antialiasing) - int x = roundf(sin_t(rad) * i); - int y = roundf(cos_t(rad) * i); - setPixelColorXY(x, y, col); + if (i == virtualLength() - 1) setPixelColorXY(vW-1, vH-1, col); // Last i always fill corner + if (!_isSuperSimpleSegment) { + // WLEDMM: drawArc() is faster if it's NOT "super simple" as the regular M12_pArc + // can do "useSymmetry" to speed things along, but a more complicated segment likey + // uses mirroring which generates a symmetry speed-up, or other things which mean + // less pixels are calculated. + drawArc(0, 0, i, col); + } else { + //WLEDMM: some optimizations for the drawing loop + // pre-calculate loop limits, exploit symmetry at 45deg + float radius = float(i); + + // float step = HALF_PI / (2.85f * radius); // upstream uses this + float step = HALF_PI / (M_PI * radius); // WLEDMM we use the correct circumference + bool useSymmetry = (max(vH, vW) > 20); // for segments wider than 20 pixels, we exploit symmetry + unsigned numSteps; + if (useSymmetry) numSteps = 1 + ((HALF_PI/2.0f + step/2.0f) / step); // with symmetry + else numSteps = 1 + ((HALF_PI + step/2.0f) / step); // without symmetry + + float rad = 0.0f; + for (unsigned count = 0; count < numSteps; count++) { + // may want to try float version as well (with or without antialiasing) + // int x = max(0, min(vW-1, (int)roundf(sinf(rad) * radius))); + // int y = max(0, min(vH-1, (int)roundf(cosf(rad) * radius))); + int x = roundf(sinf(rad) * radius); + int y = roundf(cosf(rad) * radius); + setPixelColorXY(x, y, col); + if(useSymmetry) setPixelColorXY(y, x, col);// WLEDMM + rad += step; + } + + // // Bresenham’s Algorithm (may not fill every pixel) + // int d = 3 - (2*i); + // int y = i, x = 0; + // while (y >= x) { + // setPixelColorXY(x, y, col); + // setPixelColorXY(y, x, col); + // x++; + // if (d > 0) { + // y--; + // d += 4 * (x - y) + 10; + // } else { + // d += 4 * x + 6; + // } + // } } - // Bresenham’s Algorithm (may not fill every pixel) - //int d = 3 - (2*i); - //int y = i, x = 0; - //while (y >= x) { - // setPixelColorXY(x, y, col); - // setPixelColorXY(y, x, col); - // x++; - // if (d > 0) { - // y--; - // d += 4 * (x - y) + 10; - // } else { - // d += 4 * x + 6; - // } - //} } break; - case M12_pCorner: - for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); - for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); + case M12_pCorner: { + int x = min(i, vW-1); + int y = min(i, vH-1); + if (i <= y) drawLine(0,y, x,y, col, false); // botton line (if visible) + if (i <= x) drawLine(x,0, x,y, col, false); // right line (if visible) + } + break; + case M12_jMap: //WLEDMM jMap + if (jMap) + ((JMapC *)jMap)->setPixelColor(i, col); + break; + case M12_sCircle: //WLEDMM + if (vStrip > 0) + { + int x = roundf(sinf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); + int y = roundf(cosf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); + setPixelColorXY(x + vW/2, y + vH/2, col); + } + else // pArc -> circle + drawArc(vW/2, vH/2, i/2, col); + break; + case M12_sBlock: //WLEDMM + if (vStrip > 0) + { + //vStrip+1 is distance from centre, i is how much of the square is filled + uint16_t x=0,y=0; + xyFromBlock(x,y, i, vW, vH, (vStrip+1)*2); + setPixelColorXY(x, y, col); + } + else { // pCorner -> block + int centerX = (vW+1)/2 - 1; + int centerY = (vH+1)/2 - 1; + int xLeft = max(centerX-i, 0); + int yTop = max(centerY-i, 0); + int xRight = min(centerX+i+1, vW-1); + int yBottom = min(centerY+i+1, vH-1); + + if (yTop == centerY-i) drawLine(xLeft,yTop, xRight, yTop, col); // top and bottom horizontal lines, if visible + if (yBottom == centerY+i+1) drawLine(xLeft,yBottom, xRight, yBottom, col); + if (xLeft == centerX-i) drawLine(xLeft,yTop, xLeft, yBottom, col); // left and right vertical lines, if visible + if (xRight == centerX+i+1) drawLine(xRight,yTop, xRight, yBottom, col); + } + break; + case M12_sPinwheel: { + // WLEDMM shortcut when no grouping/spacing used + bool simpleSegment = (grouping == 1) && (spacing == 0); + uint32_t scaled_col = col; + if (simpleSegment) { + // segment brightness must be pre-calculated for the "fast" setPixelColorXY variant! + uint8_t _bri_t = currentBri(on ? opacity : 0); + if (!_bri_t && !transitional) return; + if (_bri_t < 255) scaled_col = color_fade(col, _bri_t); + } + + // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) + float centerX = roundf((vW-1) / 2.0f); + float centerY = roundf((vH-1) / 2.0f); + float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians + float cosVal = cosf(angleRad); + float sinVal = sinf(angleRad); + + // avoid re-painting the same pixel + int lastX = INT_MIN; // impossible position + int lastY = INT_MIN; // impossible position + // draw line at angle, starting at center and ending at the segment edge + // we use fixed point math for better speed. Starting distance is 0.5 for better rounding + // int_fast16_t and int_fast32_t types changed to int, minimum bits commented + int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit + int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit + int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit + int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit + + int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint + int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint + + // Odd rays start further from center if prevRay started at center. + static int prevRay = INT_MIN; // previous ray number + if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { + int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel + posx += inc_x * jump; + posy += inc_y * jump; + } + prevRay = i; + + // draw ray until we hit any edge + while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { + // scale down to integer (compiler will replace division with appropriate bitshift) + int x = posx / Fixed_Scale; + int y = posy / Fixed_Scale; + // set pixel + if (x != lastX || y != lastY) { // only paint if pixel position is different + if (simpleSegment) setPixelColorXY_fast(x, y, col, scaled_col, vW, vH); + else setPixelColorXY_slow(x, y, col); + } + lastX = x; + lastY = y; + // advance to next position + posx += inc_x; + posy += inc_y; + } break; + } } return; } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { @@ -612,24 +1121,20 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) // we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed) int x = 0, y = 0; if (virtualHeight()>1) y = i; - if (virtualWidth() >1) x = i; + else if (virtualWidth() >1) x = i; setPixelColorXY(x, y, col); return; } } #endif - if (leds) leds[i] = col; + if (ledsrgb) ledsrgb[i] = col; uint16_t len = length(); uint8_t _bri_t = currentBri(on ? opacity : 0); - if (!_bri_t && !transitional && fadeTransition) return; // if _bri_t == 0 && segment is not transitionig && transitions are enabled then save a few CPU cycles + if (!_bri_t && !transitional && fadeTransition) return; // if _bri_t == 0 && segment is not transitioning && transitions are enabled then save a few CPU cycles if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_fade(col, _bri_t); } // expand pixel (taking into account start, grouping, spacing [and offset]) @@ -643,6 +1148,17 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) } i += start; // starting pixel in a group +#if 0 // needs more testing + // WLEDMM shortcut when no grouping/spacing used + bool simpleSegment = !mirror && (grouping == 1) && (spacing == 0); + if (simpleSegment) { + int indexSet = i + offset; // offset/phase + if (indexSet >= stop) indexSet -= len; // wrap + strip.setPixelColor(indexSet, col); + return; + } +#endif + // set all the pixels in the group for (int j = 0; j < grouping; j++) { uint16_t indexSet = i + ((reverse) ? -j : j); @@ -663,6 +1179,7 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) // anti-aliased normalized version of setPixelColor() void Segment::setPixelColor(float i, uint32_t col, bool aa) { + if (!isActive()) return; // not active int vStrip = int(i/10.0f); // hack to allow running on virtual strips (2D segment columns/rows) i -= int(i); @@ -692,8 +1209,9 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) } } -uint32_t Segment::getPixelColor(int i) +uint32_t __attribute__((hot)) Segment::getPixelColor(int i) const { + if (!isActive()) return 0; // not active #ifndef WLED_DISABLE_2D int vStrip = i>>16; #endif @@ -711,25 +1229,96 @@ uint32_t Segment::getPixelColor(int i) if (vStrip>0) return getPixelColorXY(vStrip - 1, vH - i -1); else return getPixelColorXY(0, vH - i -1); break; - case M12_pArc: case M12_pCorner: - // use longest dimension - return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); + case M12_pArc: { + if (i < max(vW, vH)) { + return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); // Corner and Arc + break; + } + float minradius = float(i) - 0.1f; + const int minradius2 = roundf(minradius * minradius); + int startX, startY; + if (vW >= vH) {startX = vW - 1; startY = 1;} // Last Column + else {startX = 1; startY = vH - 1;} // Last Row + // Loop through only last row/column depending on orientation + for (int x = startX; x < vW; x++) { + int newX2 = x * x; + for (int y = startY; y < vH; y++) { + int newY2 = y * y; + if (newX2 + newY2 >= minradius2) return getPixelColorXY(x, y); + } + } + return getPixelColorXY(vW-1, vH-1); // Last pixel break; - } + } + case M12_jMap: //WLEDMM jMap + if (jMap) + return ((JMapC *)jMap)->getPixelColor(i); + break; + case M12_sCircle: //WLEDMM + if (vStrip > 0) + { + int x = roundf(sinf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); + int y = roundf(cosf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); + return getPixelColorXY(x + vW/2, y + vH/2); + } + else + return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); + break; + case M12_sBlock: //WLEDMM + if (vStrip > 0) + { + uint16_t x=0,y=0; + xyFromBlock(x,y, i, vW, vH, (vStrip+1)*2); + return getPixelColorXY(x, y); + } + else + return getPixelColorXY(vW / 2, vH / 2 - i - 1); + break; + case M12_sPinwheel: + // not 100% accurate, returns pixel at outer edge + // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) + float centerX = roundf((vW-1) / 2.0f); + float centerY = roundf((vH-1) / 2.0f); + float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians + float cosVal = cosf(angleRad); + float sinVal = sinf(angleRad); + + int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit + int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit + int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit + int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit + int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint + int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint + + // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor + int x = INT_MIN; + int y = INT_MIN; + while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { + // scale down to integer (compiler will replace division with appropriate bitshift) + x = posx / Fixed_Scale; + y = posy / Fixed_Scale; + // advance to next position + posx += inc_x; + posy += inc_y; + } + return getPixelColorXY(x, y); + break; + } return 0; } #endif - if (leds) return RGBW32(leds[i].r, leds[i].g, leds[i].b, 0); + if (ledsrgb) return RGBW32(ledsrgb[i].r, ledsrgb[i].g, ledsrgb[i].b, 0); if (reverse) i = virtualLength() - i - 1; i *= groupLength(); i += start; /* offset/phase */ - i += offset; - if (i >= stop) i -= length(); - return strip.getPixelColor(i); + if (offset < INT16_MAX) i += offset; // WLEDMM + if ((i >= stop) && (stop>0)) i -= length(); // WLEDMM avoid negative index (stop = 0 is a possible value) + if (i<0) i=0; // WLEDMM just to be 100% sure + return strip.getPixelColorRestored(i); } uint8_t Segment::differs(Segment& b) const { @@ -763,6 +1352,11 @@ void Segment::refreshLightCapabilities() { uint16_t segStartIdx = 0xFFFFU; uint16_t segStopIdx = 0; + if (!isActive()) { + _capabilities = 0; + return; + } + if (start < Segment::maxWidth * Segment::maxHeight) { // we are withing 2D matrix (includes 1D segments) for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { @@ -804,24 +1398,42 @@ void Segment::refreshLightCapabilities() { } /* - * Fills segment with color + * Fills segment with color - WLEDMM using faster sPC if possible */ -void Segment::fill(uint32_t c) { - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D - for(uint16_t y = 0; y < rows; y++) for (uint16_t x = 0; x < cols; x++) { - if (is2D()) setPixelColorXY(x, y, c); - else setPixelColor(x, c); +void __attribute__((hot)) Segment::fill(uint32_t c) { + if (!isActive()) return; // not active + + const uint_fast16_t cols = is2D() ? virtualWidth() : virtualLength(); // WLEDMM use fast int types + const uint_fast16_t rows = virtualHeight(); // will be 1 for 1D + + if (is2D()) { + // pre-calculate scaled color + uint32_t scaled_col = c; + bool simpleSegment = (grouping == 1) && (spacing == 0); + if (simpleSegment) { + uint8_t _bri_t = currentBri(on ? opacity : 0); + if (!_bri_t && !transitional) return; + if (_bri_t < 255) scaled_col = color_fade(c, _bri_t); + } + // fill 2D segment + for(unsigned y = 0; y < rows; y++) for (unsigned x = 0; x < cols; x++) { + if (simpleSegment) setPixelColorXY_fast(x, y, c, scaled_col, cols, rows); + else setPixelColorXY_slow(x, y, c); + } + } else { // fill 1D strip + for (unsigned x = 0; x < cols; x++) setPixelColor(int(x), c); } } // Blends the specified color with the existing pixel color. void Segment::blendPixelColor(int n, uint32_t color, uint8_t blend) { - setPixelColor(n, color_blend(getPixelColor(n), color, blend)); + if (blend == UINT8_MAX) setPixelColor(n, color); + else setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } // Adds the specified color with the existing pixel color perserving color balance. void Segment::addPixelColor(int n, uint32_t color, bool fast) { + if (!isActive()) return; // not active uint32_t col = getPixelColor(n); uint8_t r = R(col); uint8_t g = G(col); @@ -840,6 +1452,7 @@ void Segment::addPixelColor(int n, uint32_t color, bool fast) { } void Segment::fadePixelColor(uint16_t n, uint8_t fade) { + if (!isActive()) return; // not active CRGB pix = CRGB(getPixelColor(n)).nscale8_video(fade); setPixelColor(n, pix); } @@ -847,88 +1460,111 @@ void Segment::fadePixelColor(uint16_t n, uint8_t fade) { /* * fade out function, higher rate = quicker fade */ -void Segment::fade_out(uint8_t rate) { - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D - - rate = (255-rate) >> 1; - float mappedRate = float(rate) +1.1; - - uint32_t color = colors[1]; // SEGCOLOR(1); // target color - int w2 = W(color); - int r2 = R(color); - int g2 = G(color); - int b2 = B(color); - - for (uint16_t y = 0; y < rows; y++) for (uint16_t x = 0; x < cols; x++) { - color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); +void __attribute__((hot)) Segment::fade_out(uint8_t rate) { + if (!isActive()) return; // not active + const uint_fast16_t cols = is2D() ? virtualWidth() : virtualLength(); // WLEDMM use fast int types + const uint_fast16_t rows = virtualHeight(); // will be 1 for 1D + + uint_fast8_t fadeRate = (255-rate) >> 1; + float mappedRate_r = 1.0f / (float(fadeRate) +1.1f); // WLEDMM use reciprocal 1/mappedRate -> faster on non-FPU chips + + uint32_t color2 = colors[1]; // SEGCOLOR(1); // target color // WLEDMM minor optimization + int w2 = W(color2); + int r2 = R(color2); + int g2 = G(color2); + int b2 = B(color2); + + for (unsigned y = 0; y < rows; y++) for (unsigned x = 0; x < cols; x++) { + uint32_t color = is2D() ? getPixelColorXY(int(x), int(y)) : getPixelColor(int(x)); + if (color == color2) continue; // WLEDMM speedup - pixel color = target color, so nothing to do int w1 = W(color); int r1 = R(color); int g1 = G(color); int b1 = B(color); - int wdelta = (w2 - w1) / mappedRate; - int rdelta = (r2 - r1) / mappedRate; - int gdelta = (g2 - g1) / mappedRate; - int bdelta = (b2 - b1) / mappedRate; + int wdelta = mappedRate_r * (w2 - w1); // WLEDMM use reciprocal - its faster + int rdelta = mappedRate_r * (r2 - r1); + int gdelta = mappedRate_r * (g2 - g1); + int bdelta = mappedRate_r * (b2 - b1); // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; rdelta += (r2 == r1) ? 0 : (r2 > r1) ? 1 : -1; gdelta += (g2 == g1) ? 0 : (g2 > g1) ? 1 : -1; bdelta += (b2 == b1) ? 0 : (b2 > b1) ? 1 : -1; + uint32_t colorNew = RGBW32(r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); // WLEDMM - if (is2D()) setPixelColorXY(x, y, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); - else setPixelColor(x, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); + if (colorNew != color) { // WLEDMM speedup - do not repaint the same color + if (is2D()) setPixelColorXY(int(x), int(y), colorNew); + else setPixelColor(int(x), colorNew); + } } } // fades all pixels to black using nscale8() -void Segment::fadeToBlackBy(uint8_t fadeBy) { - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D - - for (uint16_t y = 0; y < rows; y++) for (uint16_t x = 0; x < cols; x++) { - if (is2D()) setPixelColorXY(x, y, CRGB(getPixelColorXY(x,y)).nscale8(255-fadeBy)); - else setPixelColor(x, CRGB(getPixelColor(x)).nscale8(255-fadeBy)); +void __attribute__((hot)) Segment::fadeToBlackBy(uint8_t fadeBy) { + if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply + const uint_fast16_t cols = is2D() ? virtualWidth() : virtualLength(); // WLEDMM use fast int types + const uint_fast16_t rows = virtualHeight(); // will be 1 for 1D + const uint_fast8_t scaledown = 255-fadeBy; // WLEDMM faster to pre-compute this + + // WLEDMM minor optimization + if(is2D()) { + for (unsigned y = 0; y < rows; y++) for (unsigned x = 0; x < cols; x++) { + uint32_t cc = getPixelColorXY(int(x),int(y)); // WLEDMM avoid RGBW32 -> CRGB -> RGBW32 conversion + uint32_t cc2 = color_fade(cc, scaledown); // fade +#ifdef WLEDMM_FASTPATH + if (cc2 != cc) // WLEDMM only re-paint if faded color is different - normally disabled - causes problem with text overlay +#endif + setPixelColorXY(int(x), int(y), cc2); + } + } else { + for (uint_fast16_t x = 0; x < cols; x++) { + setPixelColor((uint16_t)x, CRGB(getPixelColor((uint16_t)x)).nscale8(scaledown)); + } } } /* * blurs segment content, source: FastLED colorutils.cpp */ -void Segment::blur(uint8_t blur_amount) -{ +void __attribute__((hot)) Segment::blur(uint8_t blur_amount, bool smear) { + if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for (uint16_t i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows - for (uint16_t k = 0; k < cols; k++) blurCol(k, blur_amount); // blur all columns + const uint_fast32_t cols = virtualWidth(); + const uint_fast32_t rows = virtualHeight(); + for (uint_fast32_t i = 0; i < rows; i++) blurRow(i, blur_amount, smear); // blur all rows + for (uint_fast32_t k = 0; k < cols; k++) blurCol(k, blur_amount, smear); // blur all columns return; } #endif - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; - for(uint16_t i = 0; i < virtualLength(); i++) - { - CRGB cur = CRGB(getPixelColor(i)); - CRGB part = cur; - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if(i > 0) { - uint32_t c = getPixelColor(i-1); - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); - setPixelColor(i-1, qadd8(r, part.red), qadd8(g, part.green), qadd8(b, part.blue)); + unsigned vlength = virtualLength(); + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = 0; + for (unsigned i = 0; i < vlength; i++) { + uint32_t cur = getPixelColor(i); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (i > 0) { + if (carryover) + curnew = color_add(curnew, carryover, !smear); // WLEDMM + uint32_t prev = color_add(lastnew, part, !smear); // WLEDMM + if (last != prev) // optimization: only set pixel if color has changed + setPixelColor(int(i - 1), prev); } - setPixelColor(i,cur.red, cur.green, cur.blue); + else // first pixel + setPixelColor(int(i), curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColor(int(vlength - 1), curnew); } /* @@ -953,14 +1589,14 @@ uint32_t Segment::color_wheel(uint8_t pos) { /* * Returns a new, random wheel index with a minimum distance of 42 from pos. */ -uint8_t Segment::get_random_wheel_index(uint8_t pos) { - uint8_t r = 0, x = 0, y = 0, d = 0; +uint8_t Segment::get_random_wheel_index(uint8_t pos) const { // WLEDMM use fast int types, use native min/max + uint_fast8_t r = 0, x = 0, y = 0, d = 0; while(d < 42) { r = random8(); - x = abs(pos - r); + x = abs(int(pos - r)); y = 255 - x; - d = MIN(x, y); + d = min(x, y); } return r; } @@ -974,7 +1610,7 @@ uint8_t Segment::get_random_wheel_index(uint8_t pos) { * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) * @returns Single color from palette */ -uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) +uint32_t __attribute__((hot)) Segment::color_from_palette(uint_fast16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) // WLEDMM use fast int types { // default palette or no RGB support on segment if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { @@ -985,26 +1621,151 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_ } uint8_t paletteIndex = i; - if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); + uint_fast16_t vLen = mapping ? virtualLength() : 1; + if (mapping && vLen > 1) paletteIndex = (i*255)/(vLen -1); if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" - CRGB fastled_col; - CRGBPalette16 curPal; - if (transitional && _t) curPal = _t->_palT; - else loadPalette(curPal, palette); - fastled_col = ColorFromPalette(curPal, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + CRGB fastled_col = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, 0); } + //WLEDMM netmindz ar palette +uint8_t * Segment::getAudioPalette(int pal) const { + // https://forum.makerforums.info/t/hi-is-it-possible-to-define-a-gradient-palette-at-runtime-the-define-gradient-palette-uses-the/63339 + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + um_data = simulateSound(SEGMENT.soundSim); + } + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + static uint8_t xyz[16]; // Needs to be 4 times however many colors are being used. + // 3 colors = 12, 4 colors = 16, etc. + + xyz[0] = 0; // anchor of first color - must be zero + xyz[1] = 0; + xyz[2] = 0; + xyz[3] = 0; + + CRGB rgb = getCRGBForBand(1, fftResult, pal); + xyz[4] = 1; // anchor of first color + xyz[5] = rgb.r; + xyz[6] = rgb.g; + xyz[7] = rgb.b; + + rgb = getCRGBForBand(128, fftResult, pal); + xyz[8] = 128; + xyz[9] = rgb.r; + xyz[10] = rgb.g; + xyz[11] = rgb.b; + + rgb = getCRGBForBand(255, fftResult, pal); + xyz[12] = 255; // anchor of last color - must be 255 + xyz[13] = rgb.r; + xyz[14] = rgb.g; + xyz[15] = rgb.b; + + return xyz; +} + /////////////////////////////////////////////////////////////////////////////// // WS2812FX class implementation /////////////////////////////////////////////////////////////////////////////// +//WLEDMM from util.cpp +// enumerate all ledmapX.json files on FS and extract ledmap names if existing +void WS2812FX::enumerateLedmaps() { + ledmapMaxSize = 0; + ledMaps = 1; + for (int i=1; i<10; i++) { + char fileName[33] = {'\0'}; // WLEDMM ensure termination + snprintf_P(fileName, sizeof(fileName), PSTR("/ledmap%d.json"), i); + bool isFile = WLED_FS.exists(fileName); + + #ifndef ESP8266 + if (ledmapNames[i-1]) { //clear old name + delete[] ledmapNames[i-1]; + ledmapNames[i-1] = nullptr; + } + #endif + + if (isFile) { + ledMaps |= 1 << i; + + #ifndef ESP8266 + if (requestJSONBufferLock(21)) { + //WLEDMM: upstream code loops over all ledmap files, read them all, every byte (!!!!) and only get the name of the file!!! + File f; + f = WLED_FS.open(fileName, "r"); + if (f) { + f.find("\"n\":"); + char name[34] = { '\0' }; // ensure string termination + f.readBytesUntil('\n', name, sizeof(name)-1); + + size_t len = strlen(name); + if (len > 0 && len < 33) { + (void) cleanUpName(name); + len = strlen(name); + ledmapNames[i-1] = new(std::nothrow) char[len+1]; // +1 to include terminating \0 + if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], name, 33); + } + if (!ledmapNames[i-1]) { + char tmp[33]; + snprintf_P(tmp, 32, PSTR("ledmap%d.json"), i); + len = strlen(tmp); + ledmapNames[i-1] = new(std::nothrow) char[len+1]; + if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], tmp, 33); + } + + USER_PRINTF("enumerateLedmaps %s \"%s\"", fileName, name); + if (isMatrix) { + //WLEDMM calc ledmapMaxSize (TroyHacks) + char dim[34] = { '\0' }; + f.find("\"width\":"); + f.readBytesUntil('\n', dim, sizeof(dim)-1); //hack: use fileName as we have this allocated already + uint16_t maxWidth = atoi(cleanUpName(dim)); + f.find("\"height\":"); + memset(dim, 0, sizeof(dim)); // clear buffer before reading + f.readBytesUntil('\n', dim, sizeof(dim)-1); + uint16_t maxHeight = atoi(cleanUpName(dim)); + ledmapMaxSize = MAX(ledmapMaxSize, maxWidth * maxHeight); + + if (maxWidth*maxHeight>0) { + USER_PRINTF(" (%dx%d -> %d)\n", maxWidth, maxHeight, ledmapMaxSize); + } else { + USER_PRINTLN(); + } + } + else + USER_PRINTLN(); + } + f.close(); + USER_FLUSH(); + releaseJSONBufferLock(); + } + #endif + } + } + //WLEDMM add segment names to be used as ledmap names + uint8_t segment_index = 0; + for (segment &seg : _segments) { + if (seg.name != nullptr && strlen(seg.name) > 0) { + char fileName[33]; + snprintf_P(fileName, sizeof(fileName), PSTR("/lm%s.json"), seg.name); + bool isFile = WLED_FS.exists(fileName); + if (isFile) ledMaps |= 1 << (10+segment_index); + } + segment_index++; + } +} + + //do not call this method from system context (network callback) void WS2812FX::finalizeInit(void) { //reset segment runtimes + suspendStripService = true; // WLEDMM avoid running effects on an incomplete strip for (segment &seg : _segments) { seg.markForReset(); seg.resetIfRequired(); @@ -1039,7 +1800,10 @@ void WS2812FX::finalizeInit(void) for (uint8_t i=0; igetStart() + bus->getLength() > MAX_LEDS) break; + if (bus->getStart() + bus->getLength() > MAX_LEDS) { + USER_PRINT("\nError: too many LEDs, max number is "); USER_PRINTLN(MAX_LEDS); + break; + } //RGBW mode is enabled if at least one of the strips is RGBW _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. @@ -1063,20 +1827,21 @@ void WS2812FX::finalizeInit(void) //initialize leds array. TBD: realloc if nr of leds change if (Segment::_globalLeds) { - purgeSegments(true); free(Segment::_globalLeds); Segment::_globalLeds = nullptr; + purgeSegments(true); // WLEDMM moved here, because it seems to improve stability. } - if (useLedsArray) { + if (useLedsArray && getLengthTotal()>0) { // WLEDMM avoid malloc(0) size_t arrSize = sizeof(CRGB) * getLengthTotal(); // softhack007 disabled; putting leds into psram leads to horrible slowdown on WROVER boards (see setUpLeds()) - //#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM) + //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) //if (psramFound()) // Segment::_globalLeds = (CRGB*) ps_malloc(arrSize); //else //#endif - Segment::_globalLeds = (CRGB*) malloc(arrSize); - memset(Segment::_globalLeds, 0, arrSize); + if (arrSize > 0) Segment::_globalLeds = (CRGB*) malloc(arrSize); // WLEDMM avoid malloc(0) + if ((Segment::_globalLeds != nullptr) && (arrSize > 0)) memset(Segment::_globalLeds, 0, arrSize); // WLEDMM avoid dereferencing nullptr + if ((Segment::_globalLeds == nullptr) && (arrSize > 0)) errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag } //segments are created in makeAutoSegments(); @@ -1084,58 +1849,118 @@ void WS2812FX::finalizeInit(void) loadCustomPalettes(); // (re)load all custom palettes DEBUG_PRINTLN(F("Loading custom ledmaps")); deserializeMap(); // (re)load default ledmap + _isServicing = false; // WLEDMM + suspendStripService = false; // WLEDMM ready, run ! +} + +// WLEDMM wait until strip is idle (=not servicing). +// on 8266 this function does nothing, because we can only do "busy waiting" on ESP32 +#define MAX_IDLE_WAIT_MS 50 // seems to work in most cases +void WS2812FX::waitUntilIdle(void) { +#if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_PROTECT_SERVICE) + if (isServicing()) { + unsigned long waitStarted = millis(); + do { + delay(2); // Suspending for 1 tick (or more) gives other tasks a chance to run. + //yield(); // seems to be a no-op on esp32 + } while (isServicing() && (millis() - waitStarted < MAX_IDLE_WAIT_MS)); + USER_PRINTF("strip.waitUntilIdle(): strip %sidle after %d ms. (task %s with prio=%d)\n", isServicing()?"not ":"", int(millis() - waitStarted), pcTaskGetTaskName(NULL), uxTaskPriorityGet(NULL)); + } + return; +#else + return; +#endif } void WS2812FX::service() { - uint32_t nowUp = millis(); // Be aware, millis() rolls over every 49 days + unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days // WLEDMM avoid losing precision + if (OTAisRunning) return; // WLEDMM avoid flickering during OTA + now = nowUp + timebase; - if (nowUp - _lastShow < MIN_SHOW_DELAY) return; + unsigned long elapsed = nowUp - _lastServiceShow; + #if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_FASTPATH) // WLEDMM go faster on ESP32 + //if (_suspend) return; + if (elapsed < 2) return; // keep wifi alive + if ( !_triggered && (_targetFps != FPS_UNLIMITED) && (_targetFps != FPS_UNLIMITED_AC)) { +#if 0 + if (elapsed < MIN_SHOW_DELAY) return; // WLEDMM too early for service - delivers higher fps +#else + if ((elapsed+1) < _frametime) return; // code from upstream - stricter on FPS +#endif + } + #else // legacy + if (elapsed < _frametime) return; + #endif + bool doShow = false; + unsigned speedLimit = (_targetFps != FPS_UNLIMITED) && (_targetFps != FPS_UNLIMITED_AC) ? (0.85f * FRAMETIME) : 1; // WLEDMM minimum for effect frametime _isServicing = true; _segment_index = 0; for (segment &seg : _segments) { +#ifdef WLEDMM_FASTPATH + _currentSeg = &seg; +#endif // reset the segment runtime data if needed seg.resetIfRequired(); if (!seg.isActive()) continue; + if (!seg.on && !seg.transitional) continue; // WLEDMM skip disabled segments, unless a crossfade is ongoing // last condition ensures all solid segments are updated at the same time - if(nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) + if(nowUp >= seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) // WLEDMM ">=" instead of ">" { if (seg.grouping == 0) seg.grouping = 1; //sanity check - doShow = true; - uint16_t delay = FRAMETIME; + if (!seg.freeze) doShow = true; + uint16_t frameDelay = FRAMETIME; // WLEDMM avoid name clash with "delay" function if (!seg.freeze) { //only run effect function if not frozen - _virtualSegmentLength = seg.virtualLength(); + _virtualSegmentLength = seg.calc_virtualLength(); _colors_t[0] = seg.currentColor(0, seg.colors[0]); _colors_t[1] = seg.currentColor(1, seg.colors[1]); _colors_t[2] = seg.currentColor(2, seg.colors[2]); - seg.currentPalette(_currentPalette, seg.palette); + seg.setCurrentPalette(); // load actual palette if (!cctFromRgb || correctWB) busses.setSegmentCCT(seg.currentBri(seg.cct, true), correctWB); for (uint8_t c = 0; c < NUM_COLORS; c++) _colors_t[c] = gamma32(_colors_t[c]); - +#if 0 // WARNING this would kill _supersync_ + now = millis() + timebase; +#endif + seg.startFrame(); // WLEDMM + if (!_triggered && (seg.currentBri(seg.opacity) == 0) && (seg.lastBri == 0)) continue; // WLEDMM skip totally black segments // effect blending (execute previous effect) // actual code may be a bit more involved as effects have runtime data including allocated memory //if (seg.transitional && seg._modeP) (*_mode[seg._modeP])(progress()); - delay = (*_mode[seg.currentMode(seg.mode)])(); + frameDelay = (*_mode[seg.currentMode(seg.mode)])(); + + if (frameDelay < speedLimit) frameDelay = FRAMETIME; // WLEDMM limit effects that want to go faster than target FPS if (seg.mode != FX_MODE_HALLOWEEN_EYES) seg.call++; - if (seg.transitional && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition + if (seg.transitional && frameDelay > max(int(FRAMETIME), int(FRAMETIME_FIXED))) + frameDelay = max(int(FRAMETIME), int(FRAMETIME_FIXED)); // force faster updates during transition // WLEDMM only if effect requested very slow updates + + seg.lastBri = seg.currentBri(seg.on ? seg.opacity:0); // WLEDMM remember for next time seg.handleTransition(); } - seg.next_time = nowUp + delay; + seg.next_time = nowUp + frameDelay; } _segment_index++; } _virtualSegmentLength = 0; busses.setSegmentCCT(-1); if(doShow) { +#if 0 && defined(ARDUINO_ARCH_ESP32) // EXPERIMENTAL - enabled this to enforce stricter frametime limits + static unsigned long lastTimeShow = 0; + long tdelta = millis() - lastTimeShow; + if ((lastTimeShow > 0) && (tdelta > 1) && (tdelta < _frametime)) // too early - release CPU to slow down + vTaskDelay((tdelta-1) / portTICK_PERIOD_MS); // "-1" because vTaskDelay() may actually delay longer than requested + lastTimeShow = millis(); +#else yield(); +#endif show(); + _lastServiceShow = nowUp; // WLEDMM use correct timestamp } _triggered = false; _isServicing = false; @@ -1148,13 +1973,19 @@ void IRAM_ATTR WS2812FX::setPixelColor(int i, uint32_t col) busses.setPixelColor(i, col); } -uint32_t WS2812FX::getPixelColor(uint16_t i) +uint32_t WS2812FX::getPixelColor(uint_fast16_t i) const // WLEDMM fast int types { if (i < customMappingSize) i = customMappingTable[i]; if (i >= _length) return 0; return busses.getPixelColor(i); } +uint32_t WS2812FX::getPixelColorRestored(uint_fast16_t i) const // WLEDMM gets the original color from the driver (without downscaling by _bri) +{ + if (i < customMappingSize) i = customMappingTable[i]; + if (i >= _length) return 0; + return busses.getPixelColorRestored(i); +} //DISCLAIMER //The following function attemps to calculate the current LED power usage, @@ -1199,7 +2030,8 @@ void WS2812FX::estimateCurrentAndLimitBri() { for (uint_fast8_t bNum = 0; bNum < busses.getNumBusses(); bNum++) { Bus *bus = busses.getBus(bNum); - if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses + auto btype = bus->getType(); + if (EXCLUDE_FROM_ABL(btype)) continue; // WLEDMM exclude non-ABL and network busses uint16_t len = bus->getLength(); uint32_t busPowerSum = 0; for (uint_fast16_t i = 0; i < len; i++) { //sum up the usage of each LED @@ -1207,7 +2039,7 @@ void WS2812FX::estimateCurrentAndLimitBri() { byte r = R(c), g = G(c), b = B(c), w = W(c); if(useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation - busPowerSum += (MAX(MAX(r,g),b)) * 3; + busPowerSum += (max(max(r,g),b)) * 3; // WLEDMM use native min/max } else { busPowerSum += (r + g + b + w); } @@ -1221,7 +2053,8 @@ void WS2812FX::estimateCurrentAndLimitBri() { } uint32_t powerSum0 = powerSum; - powerSum *= _brightness; + //powerSum *= _brightness; // for NPBrightnessBus + powerSum *= 255; // no need to scale down powerSum - NPB-LG getPixelColor returns colors scaled down by brightness if (powerSum > powerBudget) //scale brightness down to stay in current limit { @@ -1229,56 +2062,83 @@ void WS2812FX::estimateCurrentAndLimitBri() { uint16_t scaleI = scale * 255; uint8_t scaleB = (scaleI > 255) ? 255 : scaleI; uint8_t newBri = scale8(_brightness, scaleB); - busses.setBrightness(newBri); //to keep brightness uniform, sets virtual busses too - currentMilliamps = (powerSum0 * newBri) / puPerMilliamp; + // to keep brightness uniform, sets virtual busses too - softhack007: apply reductions immediately + if (scaleB < 255) busses.setBrightness(scaleB, true); // NPB-LG has already applied brightness, so its sufficient to post-apply scaling ==> use scaleB instead of newBri + busses.setBrightness(newBri, false); // set new brightness for next frame + //currentMilliamps = (powerSum0 * newBri) / puPerMilliamp; // for NPBrightnessBus + currentMilliamps = (powerSum0 * scaleB) / puPerMilliamp; // for NPBus-LG } else { currentMilliamps = powerSum / puPerMilliamp; - busses.setBrightness(_brightness); + busses.setBrightness(_brightness, false); // set new brightness for next frame } currentMilliamps += MA_FOR_ESP; //add power of ESP back to estimate currentMilliamps += pLen; //add standby power back to estimate } void WS2812FX::show(void) { + if (OTAisRunning) return; // WLEDMM avoid flickering during OTA - // avoid race condition, caputre _callback value + // avoid race condition, capture _callback value show_callback callback = _callback; if (callback) callback(); estimateCurrentAndLimitBri(); + unsigned long showNow = millis(); // include time needed for busses.show() + #ifdef ARDUINO_ARCH_ESP32 // WLEDMM more accurate FPS measurement for ESP32 + uint64_t now500 = esp_timer_get_time() / 2; // native timer; micros /2 -> millis * 500 + #endif + // some buses send asynchronously and this method will return before // all of the data has been sent. // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods busses.show(); - unsigned long now = millis(); - unsigned long diff = now - _lastShow; + + unsigned long diff = showNow - _lastShow; uint16_t fpsCurr = 200; if (diff > 0) fpsCurr = 1000 / diff; - _cumulativeFps = (3 * _cumulativeFps + fpsCurr) >> 2; - _lastShow = now; + _cumulativeFps = (3 * _cumulativeFps + fpsCurr +2) >> 2; // "+2" for proper rounding (2/4 = 0.5) + _lastShow = showNow; + _lastServiceShow = showNow; + +#ifdef ARDUINO_ARCH_ESP32 // WLEDMM more accurate FPS measurement for ESP32 + int64_t diff500 = now500 - _lastShow500; + if ((diff500 > 1) && (diff500 < 800000)) { // exclude stupid values (timer rollover, major hickups) + float fpcCurr500 = 500000.0f / float(diff500); + if (fpcCurr500 > 2) + _cumulativeFps500 = (3 * _cumulativeFps500 + (500.0 * fpcCurr500)) / 4; // average for some smoothing + } + _lastShow500 = now500; +#endif } /** * Returns a true value if any of the strips are still being updated. * On some hardware (ESP32), strip updates are done asynchronously. */ -bool WS2812FX::isUpdating() { +bool WS2812FX::isUpdating() const { return !busses.canAllShow(); } /** * Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough. - * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accurary varies + * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies */ -uint16_t WS2812FX::getFps() { +uint16_t WS2812FX::getFps() const { if (millis() - _lastShow > 2000) return 0; +#ifdef ARDUINO_ARCH_ESP32 + return ((_cumulativeFps500 + 250) / 500); // +250 for proper rounding +#else return _cumulativeFps +1; +#endif } void WS2812FX::setTargetFps(uint8_t fps) { - if (fps > 0 && fps <= 120) _targetFps = fps; - _frametime = 1000 / _targetFps; + if (fps <= 251) _targetFps = fps; // WLEDMM allow higher framerates + //if (fps > 0) _frametime = ((2000 / _targetFps) +1) /2; // with rounding + if (fps > 0) _frametime = 1000 / _targetFps; + else _frametime = 2; // AC WLED compatibility + if (fps >= FPS_UNLIMITED) _frametime = 2; // WLEDMM unlimited mode } void WS2812FX::setMode(uint8_t segid, uint8_t m) { @@ -1357,14 +2217,14 @@ void WS2812FX::setMainSegmentId(uint8_t n) { return; } -uint8_t WS2812FX::getLastActiveSegmentId(void) { +uint8_t WS2812FX::getLastActiveSegmentId(void) const { for (size_t i = _segments.size() -1; i > 0; i--) { if (_segments[i].isActive()) return i; } return 0; } -uint8_t WS2812FX::getActiveSegmentsNum(void) { +uint8_t WS2812FX::getActiveSegmentsNum(void) const { uint8_t c = 0; for (size_t i = 0; i < _segments.size(); i++) { if (_segments[i].isActive()) c++; @@ -1372,17 +2232,30 @@ uint8_t WS2812FX::getActiveSegmentsNum(void) { return c; } -uint16_t WS2812FX::getLengthTotal(void) { - uint16_t len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D +uint16_t WS2812FX::getLengthTotal(void) const { // WLEDMM fast int types + uint_fast16_t len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip return len; } -uint16_t WS2812FX::getLengthPhysical(void) { - uint16_t len = 0; - for (size_t b = 0; b < busses.getNumBusses(); b++) { +uint16_t WS2812FX::getLengthPhysical(void) const { // WLEDMM fast int types + uint_fast16_t len = 0; + for (unsigned b = 0; b < busses.getNumBusses(); b++) { // WLEDMM use native (fast) types Bus *bus = busses.getBus(b); - if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses + auto btype = bus->getType(); + if (EXCLUDE_FROM_ABL(btype)) continue; //exclude HUB75, and non-physical network busses + len += bus->getLength(); + } + return len; +} + +//WLEDMM - getLengthPhysical plus plysical busses not supporting ABL (i.e. HUB75) +uint16_t WS2812FX::getLengthPhysical2(void) const { + uint_fast16_t len = 0; + for (unsigned b = 0; b < busses.getNumBusses(); b++) { + Bus *bus = busses.getBus(b); + auto btype = bus->getType(); + if (IS_VIRTUAL(btype)) continue; len += bus->getLength(); } return len; @@ -1391,8 +2264,8 @@ uint16_t WS2812FX::getLengthPhysical(void) { //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. //returns if there is an RGBW bus (supports RGB and White, not only white) //not influenced by auto-white mode, also true if white slider does not affect output white channel -bool WS2812FX::hasRGBWBus(void) { - for (size_t b = 0; b < busses.getNumBusses(); b++) { +bool WS2812FX::hasRGBWBus(void) const { + for (unsigned b = 0; b < busses.getNumBusses(); b++) { // WLEDMM use native (fast) types Bus *bus = busses.getBus(b); if (bus == nullptr || bus->getLength()==0) break; if (bus->hasRGB() && bus->hasWhite()) return true; @@ -1400,9 +2273,9 @@ bool WS2812FX::hasRGBWBus(void) { return false; } -bool WS2812FX::hasCCTBus(void) { +bool WS2812FX::hasCCTBus(void) const { if (cctFromRgb && !correctWB) return false; - for (size_t b = 0; b < busses.getNumBusses(); b++) { + for (unsigned b = 0; b < busses.getNumBusses(); b++) { // WLEDMM use native (fast) types Bus *bus = busses.getBus(b); if (bus == nullptr || bus->getLength()==0) break; switch (bus->getType()) { @@ -1425,12 +2298,14 @@ void WS2812FX::purgeSegments(bool force) { } if (deleted) { _segments.shrink_to_fit(); - if (_mainSegment >= _segments.size()) setMainSegmentId(0); + /*if (_mainSegment >= _segments.size())*/ setMainSegmentId(0); } } Segment& WS2812FX::getSegment(uint8_t id) { - return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors + uint8_t mainSegID = getMainSegmentId(); + if (mainSegID >= _segments.size()) mainSegID=0; // WLEDMM fallback in case that getMainSegmentId() is invalid + return _segments[id >= _segments.size() ? mainSegID : id]; // vectors } void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) { @@ -1438,19 +2313,42 @@ void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, _segments[n].setUp(i1, i2, grouping, spacing, offset, startY, stopY); } -void WS2812FX::restartRuntime() { - for (segment &seg : _segments) seg.markForReset(); +void WS2812FX::restartRuntime(bool doReset) { + for (segment &seg : _segments) { + if (doReset) { // WLEDMM we prefer not to perform a complete restart of all effects + seg.markForReset(); // seg.resetIfRequired(); // WLEDMM calling this function from webserver context will cause troubles + } else { + seg.next_time = 0; seg.step = 0; + seg.markForBlank(); + } + } } -void WS2812FX::resetSegments() { - _segments.clear(); // destructs all Segment as part of clearing - #ifndef WLED_DISABLE_2D - segment seg = isMatrix ? Segment(0, Segment::maxWidth, 0, Segment::maxHeight) : Segment(0, _length); - #else - segment seg = Segment(0, _length); - #endif - _segments.push_back(seg); - _mainSegment = 0; +void WS2812FX::resetSegments(bool boundsOnly) { //WLEDMM add boundsonly + DEBUG_PRINTF("resetSegments %d %dx%d\n", boundsOnly, Segment::maxWidth, Segment::maxHeight); + if (!boundsOnly) { + _segments.clear(); // destructs all Segment as part of clearing + #ifndef WLED_DISABLE_2D + segment seg = isMatrix ? Segment(0, Segment::maxWidth, 0, Segment::maxHeight) : Segment(0, _length); + #else + segment seg = Segment(0, _length); + #endif + _segments.push_back(seg); + _mainSegment = 0; + } else { //WLEDMM boundsonly + for (segment &seg : _segments) { + #ifndef WLED_DISABLE_2D + seg.start = 0; + seg.stop = Segment::maxWidth; + seg.startY = 0; + seg.stopY = Segment::maxHeight; + #else + seg.start = 0; + seg.stop = _length; + #endif + seg.allocLeds(); + } + } } void WS2812FX::makeAutoSegments(bool forceReset) { @@ -1475,7 +2373,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) { segStops[s] = segStarts[s] + b->getLength(); #ifndef WLED_DISABLE_2D - if (isMatrix && segStops[s] < Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix + if (isMatrix && segStops[s] <= Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix if (isMatrix && segStarts[s] < Segment::maxWidth*Segment::maxHeight) segStarts[s] = Segment::maxWidth*Segment::maxHeight; #endif @@ -1548,6 +2446,8 @@ void WS2812FX::fixInvalidSegments() { if (_segments[i].stop > _length) _segments[i].stop = _length; } } + // if any segments were deleted free memory + purgeSegments(); // this is always called as the last step after finalizeInit(), update covered bus types for (segment &seg : _segments) seg.refreshLightCapabilities(); @@ -1575,7 +2475,7 @@ uint8_t WS2812FX::setPixelSegment(uint8_t n) { uint8_t prevSegId = _segment_index; if (n < _segments.size()) { _segment_index = n; - _virtualSegmentLength = _segments[_segment_index].virtualLength(); + _virtualSegmentLength = _segments[_segment_index].calc_virtualLength(); } return prevSegId; } @@ -1583,10 +2483,10 @@ uint8_t WS2812FX::setPixelSegment(uint8_t n) { void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) { if (i2 >= i) { - for (uint16_t x = i; x <= i2; x++) setPixelColor(x, col); + for (uint_fast16_t x = i; x <= i2; x++) setPixelColor((uint16_t)x, col); // WLEDMM use fast int types } else { - for (uint16_t x = i2; x <= i; x++) setPixelColor(x, col); + for (uint_fast16_t x = i2; x <= i; x++) setPixelColor((uint16_t)x, col); } } @@ -1622,30 +2522,32 @@ void WS2812FX::loadCustomPalettes() { if (readObjectFromFile(fileName, nullptr, &pDoc)) { JsonArray pal = pDoc[F("palette")]; - if (!pal.isNull() && pal.size()>4) { // not an empty palette (at least 2 entries) + if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) if (pal[0].is() && pal[1].is()) { // we have an array of index & hex strings - size_t palSize = MIN(pal.size(), 36); + size_t palSize = min(pal.size(), (size_t)36); // WLEDMM use native min/max palSize -= palSize % 2; // make sure size is multiple of 2 - for (size_t i=0, j=0; i()<256; i+=2, j+=4) { + for (unsigned i=0, j=0; i()<256; i+=2, j+=4) { uint8_t rgbw[] = {0,0,0,0}; tcp[ j ] = (uint8_t) pal[ i ].as(); // index colorFromHexString(rgbw, pal[i+1].as()); // will catch non-string entires - for (size_t c=0; c<3; c++) tcp[j+1+c] = rgbw[c]; // only use RGB component + for (unsigned c=0; c<3; c++) tcp[j+1+c] = gamma8(rgbw[c]); // only use RGB component DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); } } else { - size_t palSize = MIN(pal.size(), 72); + size_t palSize = min(pal.size(), (size_t)72); // WLEDMM use native min/max palSize -= palSize % 4; // make sure size is multiple of 4 for (size_t i=0; i()<256; i+=4) { tcp[ i ] = (uint8_t) pal[ i ].as(); // index - tcp[i+1] = (uint8_t) pal[i+1].as(); // R - tcp[i+2] = (uint8_t) pal[i+2].as(); // G - tcp[i+3] = (uint8_t) pal[i+3].as(); // B + tcp[i+1] = gamma8((uint8_t) pal[i+1].as()); // R + tcp[i+2] = gamma8((uint8_t) pal[i+2].as()); // G + tcp[i+3] = gamma8((uint8_t) pal[i+3].as()); // B DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); } } customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); + } else { + DEBUG_PRINTLN(F("Wrong palette format.")); } } } else { @@ -1658,46 +2560,136 @@ void WS2812FX::loadCustomPalettes() { bool WS2812FX::deserializeMap(uint8_t n) { // 2D support creates its own ledmap (on the fly) if a ledmap.json exists it will overwrite built one. - char fileName[32]; - strcpy_P(fileName, PSTR("/ledmap")); - if (n) sprintf(fileName +7, "%d", n); - strcat(fileName, ".json"); - bool isFile = WLED_FS.exists(fileName); + char fileName[32] = {'\0'}; + //WLEDMM: als support segment name ledmaps + bool isFile = false;; + if (n<10) { + strcpy_P(fileName, PSTR("/ledmap")); + if (n) sprintf(fileName +7, "%d", n); //WLEDMM: trick to not include 0 in ledmap.json + strcat(fileName, ".json"); + isFile = WLED_FS.exists(fileName); + } else { //WLEDMM add segment name as ledmap.name + uint8_t segment_index = 0; + for (segment &seg : _segments) { + if (n == 10 + segment_index && !isFile && seg.name != nullptr) { + sprintf_P(fileName, PSTR("/%s.json"), seg.name); + isFile = WLED_FS.exists(fileName); + } + if (isFile) break; + segment_index++; + } + } if (!isFile) { // erase custom mapping if selecting nonexistent ledmap.json (n==0) - if (!isMatrix && !n && customMappingTable != nullptr) { + //WLEDMM: doubt this is necessary as return false causes setupMatrix to deal with this !!!! + if (!isMatrix && !n) { customMappingSize = 0; - delete[] customMappingTable; - customMappingTable = nullptr; + loadedLedmap = 0; //WLEDMM } return false; } if (!requestJSONBufferLock(7)) return false; - if (!readObjectFromFile(fileName, nullptr, &doc)) { + // WLEDMM: before changing maps, make sure our strip is _not_ servicing effects in parallel + if (strip.isServicing()) { + USER_PRINTLN(F("deserializeMap(): strip is still drawing effects, waiting ...")); + strip.waitUntilIdle(); + } + + //WLEDMM: change upstream code: do not load complete ledmaps in json as this blows up memory, use file read instead + //read the file + File f; + f = WLED_FS.open(fileName, "r"); + if (!f) { releaseJSONBufferLock(); return false; //if file does not exist just exit } - DEBUG_PRINT(F("Reading LED map from ")); - DEBUG_PRINTLN(fileName); - - // erase old custom ledmap - if (customMappingTable != nullptr) { - customMappingSize = 0; - delete[] customMappingTable; - customMappingTable = nullptr; + USER_PRINT(F("Reading LED map from ")); //WLEDMM use USER_PRINT + USER_PRINTLN(fileName); + + if (isMatrix) { + //WLEDMM: read width and height + memset(fileName, 0, sizeof(fileName)); // clear old buffer - readBytesUntil() does not terminate strings !!! + f.find("\"width\":"); + f.readBytesUntil('\n', fileName, sizeof(fileName)); //hack: use fileName as we have this allocated already + uint16_t maxWidth = atoi(cleanUpName(fileName)); + //DEBUG_PRINTF(" (\"width\": %s) ", fileName) + + memset(fileName, 0, sizeof(fileName)); // clear old buffer + f.find("\"height\":"); + f.readBytesUntil('\n', fileName, sizeof(fileName)); + uint16_t maxHeight = atoi(cleanUpName(fileName)); + //DEBUG_PRINTF(" (\"height\": %s) \n", fileName) + + #ifndef WLEDMM_NO_MAP_RESET + //WLEDMM: support ledmap file properties width and height: if found change segment + if (maxWidth * maxHeight > 0) { + Segment::maxWidth = maxWidth; + Segment::maxHeight = maxHeight; + resetSegments(true); //WLEDMM not makeAutoSegments() as we only want to change bounds + } + else + setUpMatrix(); //reset segment sizes to panels + #endif } - JsonArray map = doc[F("map")]; - if (!map.isNull() && map.size()) { // not an empty map - customMappingSize = map.size(); - customMappingTable = new uint16_t[customMappingSize]; - for (uint16_t i=0; i customMappingTableSize) { + size_t size = max(ledmapMaxSize, size_t(Segment::maxWidth * Segment::maxHeight)); // TroyHacks + USER_PRINTF("deserializemap customMappingTable alloc %u from %u\n", size, customMappingTableSize); + //if (customMappingTable != nullptr) delete[] customMappingTable; + //customMappingTable = new(std::nothrow) uint16_t[size]; + + // don't use new / delete + if ((size > 0) && (customMappingTable != nullptr)) { + customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize } + if ((size > 0) && (customMappingTable == nullptr)) { // second try + DEBUG_PRINTLN("deserializeMap: trying to get fresh memory block."); + customMappingTable = (uint16_t*) calloc(size, sizeof(uint16_t)); + if (customMappingTable == nullptr) { + DEBUG_PRINTLN("deserializeMap: alloc failed!"); + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + } + } + if (customMappingTable != nullptr) customMappingTableSize = size; + } + + if (customMappingTable != nullptr) { + customMappingSize = Segment::maxWidth * Segment::maxHeight; + // WLEDMM reset mapping table before loading + //memset(customMappingTable, 0xFF, customMappingTableSize * sizeof(uint16_t)); // FFFF = no pixel + for (unsigned i=0; i> 3; // same as "position/8" + unsigned bitIndex = position & 0x0007; // last 3 bits + uint8_t byteValue = byteArray[byteIndex]; + return (byteValue >> bitIndex) & 1; +} + +inline void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr + //if (byteArray == nullptr) return; + size_t byteIndex = position >> 3; + unsigned bitIndex = position & 0x0007; // last 3 bits + if (value) + byteArray[byteIndex] |= (1 << bitIndex); + else + byteArray[byteIndex] &= ~(1 << bitIndex); +} + +size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits + return (num_bits + 7) / 8; +} + +void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value + if (byteArray == nullptr) return; + size_t len = getBitArrayBytes(numBits); + if (value) memset(byteArray, 0xFF, len); + else memset(byteArray, 0x00, len); +} + +//WLEDMM: #define DEBUGOUT(x) netDebugEnabled?NetDebug.print(x):Serial.print(x) not supported in this file as netDebugEnabled not in scope +#if 0 //colors.cpp uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); uint16_t approximateKelvinFromRGB(uint32_t rgb); @@ -37,13 +69,15 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte #define DEBUG_PRINTLN(x) #define DEBUG_PRINTF(x...) #endif - -//color mangling macros -#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) -#define R(c) (byte((c) >> 16)) -#define G(c) (byte((c) >> 8)) -#define B(c) (byte(c)) -#define W(c) (byte((c) >> 24)) +#else + // un-define USER_PRINT macros from bus_wrapper.h + #undef USER_PRINT + #undef USER_PRINTF + #undef USER_PRINTLN + #undef USER_FLUSH + // WLEDMM use wled.h +#include "wled.h" +#endif void ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { @@ -64,7 +98,7 @@ void ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { if (_count == 0) return defaultColorOrder; - // upper nibble containd W swap information + // upper nibble contains W swap information uint8_t swapW = defaultColorOrder >> 4; for (uint8_t i = 0; i < _count; i++) { if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { @@ -75,9 +109,9 @@ uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaul } -uint32_t Bus::autoWhiteCalc(uint32_t c) { +uint32_t Bus::autoWhiteCalc(uint32_t c) const { uint8_t aWM = _autoWhiteMode; - if (_gAWM < 255) aWM = _gAWM; + if (_gAWM != AW_GLOBAL_DISABLED) aWM = _gAWM; if (aWM == RGBW_MODE_MANUAL_ONLY) return c; uint8_t w = W(c); //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) @@ -115,7 +149,13 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) : Bu _busPtr = PolyBus::create(_iType, _pins, lenToCreate, nr, _frequencykHz); _valid = (_busPtr != nullptr); _colorOrder = bc.colorOrder; - DEBUG_PRINTF("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u)\n", _valid?"S":"Uns", nr, _len, bc.type, _pins[0],_pins[1],_iType); + if (_pins[1] != 255) { // WLEDMM USER_PRINTF + USER_PRINTF("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u)", _valid?"S":"Uns", nr, _len, bc.type, _pins[0],_pins[1],_iType); + if (bc.frequency > 999) USER_PRINTF(", %d MHz", bc.frequency/1000); + USER_PRINTLN(); + } else { + USER_PRINTF("%successfully inited strip %u (len %u) with type %u and pin %u (itype %u)\n", _valid?"S":"Uns", nr, _len, bc.type, _pins[0],_iType); + } } void BusDigital::show() { @@ -126,15 +166,15 @@ bool BusDigital::canShow() { return PolyBus::canShow(_busPtr, _iType); } -void BusDigital::setBrightness(uint8_t b) { +void BusDigital::setBrightness(uint8_t b, bool immediate) { //Fix for turning off onboard LED breaking bus #ifdef LED_BUILTIN if (_bri == 0 && b > 0) { if (_pins[0] == LED_BUILTIN || _pins[1] == LED_BUILTIN) PolyBus::begin(_busPtr, _iType, _pins); } #endif - Bus::setBrightness(b); - PolyBus::setBrightness(_busPtr, _iType, b); + Bus::setBrightness(b, immediate); + PolyBus::setBrightness(_busPtr, _iType, b, immediate); } //If LEDs are skipped, it is possible to use the first as a status LED. @@ -165,7 +205,7 @@ void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); } -uint32_t BusDigital::getPixelColor(uint16_t pix) { +uint32_t IRAM_ATTR_YN BusDigital::getPixelColor(uint16_t pix) const { if (reversed) pix = _len - pix -1; else pix += _skip; uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); @@ -183,7 +223,7 @@ uint32_t BusDigital::getPixelColor(uint16_t pix) { return PolyBus::getPixelColor(_busPtr, _iType, pix, co); } -uint8_t BusDigital::getPins(uint8_t* pinArray) { +uint8_t BusDigital::getPins(uint8_t* pinArray) const { uint8_t numPins = IS_2PIN(_type) ? 2 : 1; for (uint8_t i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; @@ -226,6 +266,7 @@ BusPwm::BusPwm(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { } #endif + USER_PRINT("[PWM"); for (uint8_t i = 0; i < numPins; i++) { uint8_t currentPin = bc.pins[i]; if (!pinManager.allocatePin(currentPin, true, PinOwner::BusPwm)) { @@ -238,7 +279,9 @@ BusPwm::BusPwm(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { ledcSetup(_ledcStart + i, _frequency, 8); ledcAttachPin(_pins[i], _ledcStart + i); #endif + USER_PRINT(" "); USER_PRINT(currentPin); } + USER_PRINTLN("] "); reversed = bc.reversed; _valid = true; } @@ -297,9 +340,29 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { } //does no index check -uint32_t BusPwm::getPixelColor(uint16_t pix) { +uint32_t BusPwm::getPixelColor(uint16_t pix) const { if (!_valid) return 0; +#if 1 + // WLEDMM stick with the old code - we don't have cctICused return RGBW32(_data[0], _data[1], _data[2], _data[3]); +#else + // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) + switch (_type) { + case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation + return RGBW32(0, 0, 0, _data[0]); + case TYPE_ANALOG_2CH: //warm white + cold white + if (cctICused) return RGBW32(0, 0, 0, _data[0]); + else return RGBW32(0, 0, 0, _data[0] + _data[1]); + case TYPE_ANALOG_5CH: //RGB + warm white + cold white + if (cctICused) return RGBW32(_data[0], _data[1], _data[2], _data[3]); + else return RGBW32(_data[0], _data[1], _data[2], _data[3] + _data[4]); + case TYPE_ANALOG_4CH: //RGBW + return RGBW32(_data[0], _data[1], _data[2], _data[3]); + case TYPE_ANALOG_3CH: //standard dumb RGB + return RGBW32(_data[0], _data[1], _data[2], 0); + } + return RGBW32(_data[0], _data[0], _data[0], _data[0]); +#endif } void BusPwm::show() { @@ -316,7 +379,7 @@ void BusPwm::show() { } } -uint8_t BusPwm::getPins(uint8_t* pinArray) { +uint8_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; uint8_t numPins = NUM_PWM_PINS(_type); for (uint8_t i = 0; i < numPins; i++) { @@ -354,6 +417,7 @@ BusOnOff::BusOnOff(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { pinMode(_pin, OUTPUT); reversed = bc.reversed; _valid = true; + USER_PRINTF("[On-Off %d] \n", int(currentPin)); } void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { @@ -367,7 +431,7 @@ void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { _data = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; } -uint32_t BusOnOff::getPixelColor(uint16_t pix) { +uint32_t BusOnOff::getPixelColor(uint16_t pix) const { if (!_valid) return 0; return RGBW32(_data, _data, _data, _data); } @@ -377,64 +441,121 @@ void BusOnOff::show() { digitalWrite(_pin, reversed ? !(bool)_data : (bool)_data); } -uint8_t BusOnOff::getPins(uint8_t* pinArray) { +uint8_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; pinArray[0] = _pin; return 1; } -BusNetwork::BusNetwork(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { +BusNetwork::BusNetwork(BusConfig &bc, const ColorOrderMap &com) : Bus(bc.type, bc.start, bc.autoWhite), _colorOrderMap(com) { _valid = false; + USER_PRINT("["); switch (bc.type) { case TYPE_NET_ARTNET_RGB: _rgbw = false; _UDPtype = 2; + USER_PRINT("NET_ARTNET_RGB"); + break; + case TYPE_NET_ARTNET_RGBW: + _rgbw = true; + _UDPtype = 2; + USER_PRINT("NET_ARTNET_RGBW"); break; case TYPE_NET_E131_RGB: _rgbw = false; _UDPtype = 1; + USER_PRINT("NET_E131_RGB"); break; default: // TYPE_NET_DDP_RGB / TYPE_NET_DDP_RGBW _rgbw = bc.type == TYPE_NET_DDP_RGBW; _UDPtype = 0; + USER_PRINT(bc.type == TYPE_NET_DDP_RGBW ? "NET_DDP_RGBW" : "NET_DDP_RGB"); break; } _UDPchannels = _rgbw ? 4 : 3; - _data = (byte *)malloc(bc.count * _UDPchannels); + #ifdef ESP32 + _data = (byte*) heap_caps_calloc_prefer((bc.count * _UDPchannels)+15, sizeof(byte), 3, MALLOC_CAP_DEFAULT, MALLOC_CAP_SPIRAM); + #else + _data = (byte*) calloc((bc.count * _UDPchannels)+15, sizeof(byte)); + #endif if (_data == nullptr) return; - memset(_data, 0, bc.count * _UDPchannels); _len = bc.count; + _colorOrder = bc.colorOrder; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); _broadcastLock = false; _valid = true; + _artnet_outputs = bc.artnet_outputs; + _artnet_leds_per_output = bc.artnet_leds_per_output; + _artnet_fps_limit = max(uint8_t(1), bc.artnet_fps_limit); + USER_PRINTF(" %u.%u.%u.%u]\n", bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); } -void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { - if (!_valid || pix >= _len) return; - if (hasWhite()) c = autoWhiteCalc(c); - if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - uint16_t offset = pix * _UDPchannels; - _data[offset] = R(c); - _data[offset+1] = G(c); - _data[offset+2] = B(c); - if (_rgbw) _data[offset+3] = W(c); +void IRAM_ATTR_YN BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { + if (pix >= _len) return; + if (_rgbw) c = autoWhiteCalc(c); + if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); // color correction from CCT + + uint16_t offset = pix * _UDPchannels; + uint8_t co = _colorOrderMap.getPixelColorOrder(pix + _start, _colorOrder); + + if (_colorOrder != co || _colorOrder != COL_ORDER_RGB) { + switch (co) { + case COL_ORDER_GRB: + _data[offset] = G(c); _data[offset+1] = R(c); _data[offset+2] = B(c); + break; + case COL_ORDER_RGB: + _data[offset] = R(c); _data[offset+1] = G(c); _data[offset+2] = B(c); + break; + case COL_ORDER_BRG: + _data[offset] = B(c); _data[offset+1] = R(c); _data[offset+2] = G(c); + break; + case COL_ORDER_RBG: + _data[offset] = R(c); _data[offset+1] = B(c); _data[offset+2] = G(c); + break; + case COL_ORDER_GBR: + _data[offset] = G(c); _data[offset+1] = B(c); _data[offset+2] = R(c); + break; + case COL_ORDER_BGR: + _data[offset] = B(c); _data[offset+1] = G(c); _data[offset+2] = R(c); + break; + } + if (_rgbw) _data[offset+3] = W(c); + } else { + _data[offset] = R(c); _data[offset+1] = G(c); _data[offset+2] = B(c); + if (_rgbw) _data[offset+3] = W(c); + } } -uint32_t BusNetwork::getPixelColor(uint16_t pix) { - if (!_valid || pix >= _len) return 0; - uint16_t offset = pix * _UDPchannels; - return RGBW32(_data[offset], _data[offset+1], _data[offset+2], _rgbw ? (_data[offset+3] << 24) : 0); +uint32_t IRAM_ATTR_YN BusNetwork::getPixelColor(uint16_t pix) const { + if (pix >= _len) return 0; + uint16_t offset = pix * _UDPchannels; + uint8_t co = _colorOrderMap.getPixelColorOrder(pix + _start, _colorOrder); + + uint8_t r = _data[offset + 0]; + uint8_t g = _data[offset + 1]; + uint8_t b = _data[offset + 2]; + uint8_t w = _rgbw ? _data[offset + 3] : 0; + + switch (co) { + case COL_ORDER_GRB: return RGBW32(g, r, b, w); + case COL_ORDER_RGB: return RGBW32(r, g, b, w); + case COL_ORDER_BRG: return RGBW32(b, r, g, w); + case COL_ORDER_RBG: return RGBW32(r, b, g, w); + case COL_ORDER_GBR: return RGBW32(g, b, r, w); + case COL_ORDER_BGR: return RGBW32(b, g, r, w); + default: return RGBW32(r, g, b, w); // default to RGB order + } } void BusNetwork::show() { if (!_valid || !canShow()) return; _broadcastLock = true; - realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw); + realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw, _artnet_outputs, _artnet_leds_per_output, _artnet_fps_limit); _broadcastLock = false; } -uint8_t BusNetwork::getPins(uint8_t* pinArray) { +uint8_t BusNetwork::getPins(uint8_t* pinArray) const { for (uint8_t i = 0; i < 4; i++) { pinArray[i] = _client[i]; } @@ -446,8 +567,654 @@ void BusNetwork::cleanup() { _valid = false; if (_data != nullptr) free(_data); _data = nullptr; + _len = 0; } +// *************************************************************************** + +#ifdef WLED_ENABLE_HUB75MATRIX +#warning "HUB75 driver enabled (experimental)" + +// BusHub75Matrix "global" variables (static members) +MatrixPanel_I2S_DMA* BusHub75Matrix::activeDisplay = nullptr; +VirtualMatrixPanel* BusHub75Matrix::activeFourScanPanel = nullptr; +HUB75_I2S_CFG BusHub75Matrix::activeMXconfig = HUB75_I2S_CFG(); +uint8_t BusHub75Matrix::activeType = 0; +uint8_t BusHub75Matrix::instanceCount = 0; +uint8_t BusHub75Matrix::last_bri = 0; + + +// -------------------------- +// Bitdepth reduction based on panel size +// -------------------------- +#if defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_SPIRAM_MODE_OCT && defined(BOARD_HAS_PSRAM) && (defined(WLED_USE_PSRAM) || defined(WLED_USE_PSRAM_JSON)) + // esp32-S3 with octal PSRAM + #if defined(SPIRAM_FRAMEBUFFER) + // when PSRAM is used for pixel buffers + #define MAX_PIXELS_8BIT (192 * 64) + #define MAX_PIXELS_6BIT ( 64 * 64) // trick: skip this category, so we go directly from 8bit to 4bit + #define MAX_PIXELS_4BIT (256 * 128) + #else + // PSRAM not used for pixel buffers + #define MAX_PIXELS_8BIT (128 * 64) + #define MAX_PIXELS_6BIT (192 * 64) + #define MAX_PIXELS_4BIT (256 * 64) + #endif +#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM) + // standard esp32-S3 with quad PSRAM + #define MAX_PIXELS_8BIT ( 96 * 64) + #define MAX_PIXELS_6BIT (128 * 64) + #define MAX_PIXELS_4BIT (160 * 64) +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + // HD-WF2 is an esp32-S3 without PSRAM - use same limits as classic esp32 + #define MAX_PIXELS_8BIT ( 64 * 64) + #define MAX_PIXELS_6BIT ( 96 * 64) + #define MAX_PIXELS_4BIT (128 * 64) +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + // esp32-S2 only has 320KB RAM + #define MAX_PIXELS_8BIT ( 48 * 48) + #define MAX_PIXELS_6BIT ( 64 * 48) + #define MAX_PIXELS_4BIT ( 96 * 64) +#else + // classic esp32, and anything else + #define MAX_PIXELS_8BIT ( 64 * 64) + #define MAX_PIXELS_6BIT ( 96 * 64) + #define MAX_PIXELS_4BIT (128 * 64) +#endif +// -------------------------- + +BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { + MatrixPanel_I2S_DMA* display = nullptr; + VirtualMatrixPanel* fourScanPanel = nullptr; + HUB75_I2S_CFG mxconfig; + size_t lastHeap = ESP.getFreeHeap(); + + _valid = false; + _len = 0; + + // allow exactly one instance + if (instanceCount > 0) { + USER_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! already active - preventing attempt to create more than one driver instance."); + return; + } + + mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer + + // mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver + // mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel + + // mxconfig.latch_blanking = 1; // needed for some ICS panels + // mxconfig.latch_blanking = 3; // use in case you see gost images + // mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz + // mxconfig.min_refresh_rate = 90; + + mxconfig.clkphase = bc.reversed; + if (bc.refreshReq) mxconfig.latch_blanking = 1; // needed for some ICS panels (default = 2) + // fake bus flags + _needsRefresh = mxconfig.latch_blanking == 1; + reversed = mxconfig.clkphase; + + if (bc.type > 104) mxconfig.driver = HUB75_I2S_CFG::FM6124; // use FM6124 for "outdoor" panels - workaround until we can make the driver user-configurable + + // How many panels we have connected, cap at sane value, prevent bad data preventing boot due to low memory + #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM) // ESP32-S3: allow up to 6 panels + mxconfig.chain_length = max((uint8_t) 1, min(bc.pins[0], (uint8_t) 6)); + #elif defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32-S2: only 2 panels due to small RAM + mxconfig.chain_length = max((uint8_t) 1, min(bc.pins[0], (uint8_t) 2)); + #else // others: up to 4 panels + mxconfig.chain_length = max((uint8_t) 1, min(bc.pins[0], (uint8_t) 4)); + #endif + + #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM) + if(bc.pins[0] > 4) { + USER_PRINTLN("WARNING, chain limited to 4"); + } + # else + // Disable this check if you are want to try bigger setups and accept you + // might need to do full erase to recover from memory relayed boot-loop if you push too far + if(mxconfig.mx_height >= 64 && (bc.pins[0] > 1)) { + USER_PRINTLN("WARNING, only single panel can be used of 64 pixel boards due to memory"); + //mxconfig.chain_length = 1; + } + #endif + + switch(bc.type) { + case 101: + mxconfig.mx_width = 32; + mxconfig.mx_height = 32; + break; + case 102: + mxconfig.mx_width = 64; + mxconfig.mx_height = 32; + break; + case 103: + mxconfig.mx_width = 64; + mxconfig.mx_height = 64; + break; + case 104: + mxconfig.mx_width = 128; + mxconfig.mx_height = 64; + break; + case 105: + mxconfig.mx_width = 32 * 2; + mxconfig.mx_height = 32 / 2; + break; + case 106: + mxconfig.mx_width = 64 * 2; + mxconfig.mx_height = 32 / 2; + break; + case 107: + mxconfig.mx_width = 64 * 2; + mxconfig.mx_height = 64 / 2; + break; + case 108: // untested + mxconfig.mx_width = 128 * 2; + mxconfig.mx_height = 64 / 2; + break; + } + + // reduce bitdepth based on total pixels + unsigned numPixels = mxconfig.mx_height * mxconfig.mx_width * mxconfig.chain_length; + if (numPixels <= MAX_PIXELS_8BIT) mxconfig.setPixelColorDepthBits(8); // 24bit + else if (numPixels <= MAX_PIXELS_6BIT) mxconfig.setPixelColorDepthBits(6); // 18bit + else if (numPixels <= MAX_PIXELS_4BIT) mxconfig.setPixelColorDepthBits(4); // 12bit + else mxconfig.setPixelColorDepthBits(3); // 9bit + + +#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3 + + // https://www.adafruit.com/product/5778 + + USER_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config"); + + mxconfig.gpio.r1 = 42; + mxconfig.gpio.g1 = 41; + mxconfig.gpio.b1 = 40; + mxconfig.gpio.r2 = 38; + mxconfig.gpio.g2 = 39; + mxconfig.gpio.b2 = 37; + + mxconfig.gpio.lat = 47; + mxconfig.gpio.oe = 14; + mxconfig.gpio.clk = 2; + + mxconfig.gpio.a = 45; + mxconfig.gpio.b = 36; + mxconfig.gpio.c = 48; + mxconfig.gpio.d = 35; + mxconfig.gpio.e = 21; + +#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM + + #if defined(MOONHUB_S3_PINOUT) + USER_PRINTLN("MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout"); + + // HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; + mxconfig.gpio = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 }; + + #else + USER_PRINTLN("MatrixPanel_I2S_DMA - S3 with PSRAM"); + + mxconfig.gpio.r1 = 1; + mxconfig.gpio.g1 = 2; + mxconfig.gpio.b1 = 42; + // 4th pin is GND + mxconfig.gpio.r2 = 41; + mxconfig.gpio.g2 = 40; + mxconfig.gpio.b2 = 39; + mxconfig.gpio.e = 38; + mxconfig.gpio.a = 45; + mxconfig.gpio.b = 48; + mxconfig.gpio.c = 47; + mxconfig.gpio.d = 21; + mxconfig.gpio.clk = 18; + mxconfig.gpio.lat = 8; + mxconfig.gpio.oe = 3; + // 16th pin is GND + #endif + +#elif defined(CONFIG_IDF_TARGET_ESP32S3) // ESP32-S3 HD-WF2 + + // Huidu HD-WF2 ESP32-S3 + // https://www.aliexpress.com/item/1005002258734810.html + // https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA/issues/433 + + USER_PRINTLN("MatrixPanel_I2S_DMA - HD-WF2 S3 config"); + + mxconfig.gpio.r1 = 2; + mxconfig.gpio.g1 = 6; + mxconfig.gpio.b1 = 10; + mxconfig.gpio.r2 = 3; + mxconfig.gpio.g2 = 7; + mxconfig.gpio.b2 = 11; + + mxconfig.gpio.lat = 33; + mxconfig.gpio.oe = 35; + mxconfig.gpio.clk = 34; + + mxconfig.gpio.a = 39; + mxconfig.gpio.b = 38; + mxconfig.gpio.c = 37; + mxconfig.gpio.d = 36; + mxconfig.gpio.e = 21; + +#elif defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32-S2 + + // Huidu HD-WF1 ESP32-S2 + // https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA/issues/433 + + USER_PRINTLN("MatrixPanel_I2S_DMA - HD-WF1 S2 config"); + + mxconfig.gpio.r1 = 2; + mxconfig.gpio.g1 = 6; + mxconfig.gpio.b1 = 3; + mxconfig.gpio.r2 = 4; + mxconfig.gpio.g2 = 8; + mxconfig.gpio.b2 = 5; + + mxconfig.gpio.lat = 33; + mxconfig.gpio.oe = 35; + mxconfig.gpio.clk = 34; + + mxconfig.gpio.a = 39; + mxconfig.gpio.b = 38; + mxconfig.gpio.c = 37; + mxconfig.gpio.d = 36; + mxconfig.gpio.e = 12; + +#elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix + + USER_PRINTLN("MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT"); + +/* + ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT + + https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h + + Can use a board like https://github.com/rorosaurus/esp32-hub75-driver +*/ + + mxconfig.gpio.r1 = 2; + mxconfig.gpio.g1 = 15; + mxconfig.gpio.b1 = 4; + mxconfig.gpio.r2 = 16; + mxconfig.gpio.g2 = 27; + mxconfig.gpio.b2 = 17; + + mxconfig.gpio.lat = 26; + mxconfig.gpio.oe = 25; + mxconfig.gpio.clk = 22; + + mxconfig.gpio.a = 5; + mxconfig.gpio.b = 18; + mxconfig.gpio.c = 19; + mxconfig.gpio.d = 21; + mxconfig.gpio.e = 12; + +#else + USER_PRINTLN("MatrixPanel_I2S_DMA - Default pins"); + /* + https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file + + Boards + + https://esp32trinity.com/ + https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/ + + */ + mxconfig.gpio.r1 = 25; + mxconfig.gpio.g1 = 26; + mxconfig.gpio.b1 = 27; + mxconfig.gpio.r2 = 14; + mxconfig.gpio.g2 = 12; + mxconfig.gpio.b2 = 13; + + mxconfig.gpio.lat = 4; + mxconfig.gpio.oe = 15; + mxconfig.gpio.clk = 16; + + mxconfig.gpio.a = 23; + mxconfig.gpio.b = 19; + mxconfig.gpio.c = 5; + mxconfig.gpio.d = 17; + mxconfig.gpio.e = 18; + +#endif + + USER_PRINTF("MatrixPanel_I2S_DMA config - %ux%u (type %u) length: %u, %u bits/pixel.\n", mxconfig.mx_width, mxconfig.mx_height, bc.type, mxconfig.chain_length, mxconfig.getPixelColorDepthBits() * 3); + DEBUG_PRINT(F("Free heap: ")); DEBUG_PRINTLN(ESP.getFreeHeap()); lastHeap = ESP.getFreeHeap(); + + // check if we can re-use the existing display driver + if (activeDisplay) { + if ( (memcmp(&(activeMXconfig.gpio), &(mxconfig.gpio), sizeof(mxconfig.gpio)) != 0) // other pins? + || (activeMXconfig.chain_length != mxconfig.chain_length) // other chain length? + || (activeMXconfig.mx_width != mxconfig.mx_width) || (activeMXconfig.mx_height != mxconfig.mx_height) // other size? + || (bc.type != activeType) // different panel type ? + || (activeMXconfig.clkphase != mxconfig.clkphase) // different driver options ? + || (activeMXconfig.latch_blanking != mxconfig.latch_blanking) + || (activeMXconfig.i2sspeed != mxconfig.i2sspeed) + || (activeMXconfig.driver != mxconfig.driver) + || (activeMXconfig.min_refresh_rate != mxconfig.min_refresh_rate) + || (activeMXconfig.getPixelColorDepthBits() != mxconfig.getPixelColorDepthBits()) ) + { + // not the same as before - delete old driver + DEBUG_PRINTLN("MatrixPanel_I2S_DMA deleting old driver!"); + activeDisplay->stopDMAoutput(); + delay(28); + //#if !defined(CONFIG_IDF_TARGET_ESP32S3) // prevent crash + delete activeDisplay; + //#endif + activeDisplay = nullptr; + activeFourScanPanel = nullptr; + #if defined(CONFIG_IDF_TARGET_ESP32S3) // runtime reconfiguration is not working on -S3 + USER_PRINTLN("\n\n****** MatrixPanel_I2S_DMA !KABOOM WARNING! Reboot needed to change driver options ***********\n"); + errorFlag = ERR_REBOOT_NEEDED; + #endif + } + } + + // OK, now we can create our matrix object + bool newDisplay = false; // true when the previous display object wasn't re-used + if (!activeDisplay) { + display = new MatrixPanel_I2S_DMA(mxconfig); // create new matrix object + newDisplay = true; + } else { + display = activeDisplay; // continue with existing matrix object + fourScanPanel = activeFourScanPanel; + } + + if (display == nullptr) { + USER_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********"); + activeDisplay = nullptr; + activeFourScanPanel = nullptr; + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + return; + } + + this->_len = (display->width() * display->height()); + + pinManager.allocatePin(mxconfig.gpio.r1, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.g1, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.b1, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.r2, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.g2, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.b2, true, PinOwner::HUB75); + + pinManager.allocatePin(mxconfig.gpio.lat, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.oe, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.clk, true, PinOwner::HUB75); + + pinManager.allocatePin(mxconfig.gpio.a, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.b, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.c, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.d, true, PinOwner::HUB75); + pinManager.allocatePin(mxconfig.gpio.e, true, PinOwner::HUB75); + + // display->setLatBlanking(4); + + USER_PRINTLN("MatrixPanel_I2S_DMA created"); + // let's adjust default brightness + //display->setBrightness8(25); // range is 0-255, 0 - 0%, 255 - 100% // [setBrightness()] Tried to set output brightness before begin() + _bri = (last_bri > 0) ? last_bri : 25; // try to restore persistent brightness value + + delay(24); // experimental + DEBUG_PRINT(F("heap usage: ")); DEBUG_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + // Allocate memory and start DMA display + if (newDisplay && (display->begin() == false)) { + USER_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********"); + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + _valid = false; + return; + } + else { + if (newDisplay) { USER_PRINTLN("MatrixPanel_I2S_DMA begin, started ok"); } + else { USER_PRINTLN("MatrixPanel_I2S_DMA begin, using existing display."); } + + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle + _valid = true; + display->setBrightness8(_bri); // range is 0-255, 0 - 0%, 255 - 100% // [setBrightness()] Tried to set output brightness before begin() + display->clearScreen(); // initially clear the screen buffer + USER_PRINTLN("MatrixPanel_I2S_DMA clear ok"); + + if (_ledBuffer) free(_ledBuffer); // should not happen + if (_ledsDirty) free(_ledsDirty); // should not happen + + _ledsDirty = (byte*) malloc(getBitArrayBytes(_len)); // create LEDs dirty bits + if (_ledsDirty) setBitArray(_ledsDirty, _len, false); // reset dirty bits + + #if defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_SPIRAM_MODE_OCT && defined(BOARD_HAS_PSRAM) && (defined(WLED_USE_PSRAM) || defined(WLED_USE_PSRAM_JSON)) + if (psramFound()) { + _ledBuffer = (CRGB*) ps_calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK) + } else { + _ledBuffer = (CRGB*) calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK) + } + #else + _ledBuffer = (CRGB*) calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK) + #endif + } + + if ((_ledBuffer == nullptr) || (_ledsDirty == nullptr)) { + // fail is we cannot get memory for the buffer + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + USER_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for leds buffer!")); + cleanup(); // free buffers, and deallocate pins + _valid = false; + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + return; // fail + } + + switch(bc.type) { + case 105: + USER_PRINTLN("MatrixPanel_I2S_DMA FOUR_SCAN_32PX_HIGH - 32x32"); + if (!fourScanPanel) fourScanPanel = new VirtualMatrixPanel((*display), 1, mxconfig.chain_length, 32, 32); + fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH); + fourScanPanel->setRotation(0); + break; + case 106: + USER_PRINTLN("MatrixPanel_I2S_DMA FOUR_SCAN_32PX_HIGH - 64x32"); + if (!fourScanPanel) fourScanPanel = new VirtualMatrixPanel((*display), 1, mxconfig.chain_length, 64, 32); + fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH); + fourScanPanel->setRotation(0); + break; + case 107: + USER_PRINTLN("MatrixPanel_I2S_DMA FOUR_SCAN_64PX_HIGH"); + if (!fourScanPanel) fourScanPanel = new VirtualMatrixPanel((*display), 1, mxconfig.chain_length, 64, 64); + fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH); + fourScanPanel->setRotation(0); + break; + case 108: // untested + USER_PRINTLN("MatrixPanel_I2S_DMA 128x64 FOUR_SCAN_64PX_HIGH"); + if (!fourScanPanel) fourScanPanel = new VirtualMatrixPanel((*display), 1, mxconfig.chain_length, 128, 64); + fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH); + fourScanPanel->setRotation(0); + break; + } + + if (_valid) { + _panelWidth = fourScanPanel ? fourScanPanel->width() : display->width(); // cache width - it will never change + } + + USER_PRINT(F("MatrixPanel_I2S_DMA ")); + USER_PRINTF("%sstarted, width=%u, %u pixels.\n", _valid? "":"not ", _panelWidth, _len); + + if (_ledBuffer != nullptr) USER_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled.")); + if (_ledsDirty != nullptr) USER_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled.")); + if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) { + USER_PRINT(F("MatrixPanel_I2S_DMA LEDS buffer uses ")); + USER_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0)); + USER_PRINTLN(F(" bytes.")); + } + + if (_valid) { + // config is active, copy to global + activeType = bc.type; + activeDisplay = display; + activeFourScanPanel = fourScanPanel; + if (newDisplay) memcpy(&activeMXconfig, &mxconfig, sizeof(mxconfig)); + } + instanceCount++; + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); +} + +void __attribute__((hot)) IRAM_ATTR BusHub75Matrix::setPixelColor(uint16_t pix, uint32_t c) { + if ( pix >= _len) return; + // if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + + if (_ledBuffer) { + CRGB fastled_col = CRGB(c); + if (_ledBuffer[pix] != fastled_col) { + _ledBuffer[pix] = fastled_col; + setBitInArray(_ledsDirty, pix, true); // flag pixel as "dirty" + } + } + #if 0 + // !! this code is not used any more !! + // BusHub75Matrix::BusHub75Matrix will fail if allocating _ledBuffer fails. + // The fallback code below created lots of flickering so it does not make sense to keep it enabled. + else { + // no double buffer allocated --> directly draw pixel + MatrixPanel_I2S_DMA* display = BusHub75Matrix::activeDisplay; + VirtualMatrixPanel* fourScanPanel = BusHub75Matrix::activeFourScanPanel; + #ifndef NO_CIE1931 + c = unGamma24(c); // to use the driver linear brightness feature, we first need to undo WLED gamma correction + #endif + uint8_t r = R(c); + uint8_t g = G(c); + uint8_t b = B(c); + + if(fourScanPanel != nullptr) { + int width = _panelWidth; + int x = pix % width; + int y = pix / width; + fourScanPanel->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b); + } else { + int width = _panelWidth; + int x = pix % width; + int y = pix / width; + display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b); + } + } + #endif +} + +uint32_t IRAM_ATTR BusHub75Matrix::getPixelColor(uint16_t pix) const { + if (pix >= _len || !_ledBuffer) return BLACK; + return uint32_t(_ledBuffer[pix].scale8(_bri)) & 0x00FFFFFF; // scale8() is needed to mimic NeoPixelBus, which returns scaled-down colours +} + +uint32_t __attribute__((hot)) IRAM_ATTR BusHub75Matrix::getPixelColorRestored(uint16_t pix) const { + if (pix >= _len || !_ledBuffer) return BLACK; + return uint32_t(_ledBuffer[pix]) & 0x00FFFFFF; +} + +void BusHub75Matrix::setBrightness(uint8_t b, bool immediate) { + _bri = b; + if (!_valid) return; + MatrixPanel_I2S_DMA* display = BusHub75Matrix::activeDisplay; + // if (_bri > 238) _bri=238; // not strictly needed. Enable this line if you see glitches at highest brightness. + if ((_bri > 253) && (activeMXconfig.latch_blanking < 2)) _bri=253; // prevent glitches at highest brightness. + last_bri = _bri; + if (display) display->setBrightness(_bri); +} + +void __attribute__((hot)) IRAM_ATTR BusHub75Matrix::show(void) { + if (!_valid) return; + MatrixPanel_I2S_DMA* display = BusHub75Matrix::activeDisplay; + if (!display) return; + display->setBrightness(_bri); + + if (_ledBuffer) { + // write out buffered LEDs + VirtualMatrixPanel* fourScanPanel = BusHub75Matrix::activeFourScanPanel; + bool isFourScan = (fourScanPanel != nullptr); + //if (isFourScan) fourScanPanel->setRotation(0); + unsigned height = isFourScan ? fourScanPanel->height() : display->height(); + unsigned width = _panelWidth; + + // Cache pointers to LED array and bitmask array, to avoid repeated accesses + const byte* ledsDirty = _ledsDirty; + const CRGB* ledBuffer = _ledBuffer; + + //while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker. + + size_t pix = 0; // running pixel index + for (int y=0; ydrawPixelRGB888(int16_t(x), int16_t(y), r, g, b); + else display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b); + } + pix ++; + } + setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits + } +} + +void BusHub75Matrix::cleanup() { + MatrixPanel_I2S_DMA* display = BusHub75Matrix::activeDisplay; + VirtualMatrixPanel* fourScanPanel = BusHub75Matrix::activeFourScanPanel; + if (display) display->clearScreen(); + +#if !defined(CONFIG_IDF_TARGET_ESP32S3) // S3: don't stop, as we want to re-use the driver later + if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black) + _panelWidth = 0; + USER_PRINTLN("HUB75 output ended."); +#else + USER_PRINTLN("HUB75 output paused."); +#endif + + _valid = false; + deallocatePins(); + _len = 0; + //if (fourScanPanel != nullptr) delete fourScanPanel; // warning: deleting object of polymorphic class type 'VirtualMatrixPanel' which has non-virtual destructor might cause undefined behavior +#if !defined(CONFIG_IDF_TARGET_ESP32S3) // S3: don't delete, as we want to re-use the driver later + if (display) delete display; + activeDisplay = nullptr; + activeFourScanPanel = nullptr; + USER_PRINTLN("HUB75 deleted."); +#else + USER_PRINTLN("HUB75 cleanup done."); +#endif + + if (instanceCount > 0) instanceCount--; + if (_ledBuffer != nullptr) free(_ledBuffer); _ledBuffer = nullptr; + if (_ledsDirty != nullptr) free(_ledsDirty); _ledsDirty = nullptr; +} + +void BusHub75Matrix::deallocatePins() { + + pinManager.deallocatePin(activeMXconfig.gpio.r1, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.g1, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.b1, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.r2, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.g2, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.b2, PinOwner::HUB75); + + pinManager.deallocatePin(activeMXconfig.gpio.lat, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.oe, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.clk, PinOwner::HUB75); + + pinManager.deallocatePin(activeMXconfig.gpio.a, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.b, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.c, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.d, PinOwner::HUB75); + pinManager.deallocatePin(activeMXconfig.gpio.e, PinOwner::HUB75); + +} +#endif +// *************************************************************************** //utility to get the approx. memory usage of a given BusConfig uint32_t BusManager::memUsage(BusConfig &bc) { @@ -467,14 +1234,30 @@ uint32_t BusManager::memUsage(BusConfig &bc) { return len*6; #endif } - if (type > 31 && type < 48) return 5; + if (type > 31 && type < 48) return 5; return len*3; //RGB } int BusManager::add(BusConfig &bc) { if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; + // WLEDMM clear cached Bus info first + lastend = 0; + laststart = 0; + lastBus = nullptr; + slowMode = false; + + DEBUG_PRINTF("BusManager::add(bc.type=%u)\n", bc.type); if (bc.type >= TYPE_NET_DDP_RGB && bc.type < 96) { - busses[numBusses] = new BusNetwork(bc); + busses[numBusses] = new BusNetwork(bc, colorOrderMap); + } else if (bc.type >= TYPE_HUB75MATRIX && bc.type <= (TYPE_HUB75MATRIX + 10)) { +#ifdef WLED_ENABLE_HUB75MATRIX + DEBUG_PRINTLN("BusManager::add - Adding BusHub75Matrix"); + busses[numBusses] = new BusHub75Matrix(bc); + USER_PRINTLN("[BusHub75Matrix] "); +#else + USER_PRINTLN("[unsupported! BusHub75Matrix - add flag -D WLED_ENABLE_HUB75MATRIX] "); + return -1; +#endif } else if (IS_DIGITAL(bc.type)) { busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); } else if (bc.type == TYPE_ONOFF) { @@ -489,39 +1272,69 @@ int BusManager::add(BusConfig &bc) { void BusManager::removeAll() { DEBUG_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. +#if !defined(ARDUINO_ARCH_ESP32) while (!canAllShow()) yield(); +#else + while (!canAllShow()) delay(2); // WLEDMM on esp32, yield() doesn't work as you think it would +#endif for (uint8_t i = 0; i < numBusses; i++) delete busses[i]; numBusses = 0; + // WLEDMM clear cached Bus info + lastBus = nullptr; + laststart = 0; + lastend = 0; + slowMode = false; } -void BusManager::show() { - for (uint8_t i = 0; i < numBusses; i++) { +void __attribute__((hot)) BusManager::show() { + for (unsigned i = 0; i < numBusses; i++) { +#if 1 && defined(ARDUINO_ARCH_ESP32) + unsigned long t0 = millis(); + while ((busses[i]->canShow() == false) && (millis() - t0 < 80)) delay(1); // WLEDMM experimental: wait until bus driver is ready (max 80ms) - costs us 1-2 fps but reduces flickering +#endif busses[i]->show(); } } void BusManager::setStatusPixel(uint32_t c) { for (uint8_t i = 0; i < numBusses; i++) { + if (busses[i]->isOk() == false) continue; // WLEDMM ignore invalid (=not ready) busses busses[i]->setStatusPixel(c); } } -void IRAM_ATTR BusManager::setPixelColor(uint16_t pix, uint32_t c, int16_t cct) { - for (uint8_t i = 0; i < numBusses; i++) { +void IRAM_ATTR __attribute__((hot)) BusManager::setPixelColor(uint16_t pix, uint32_t c, int16_t cct) { + if (!slowMode && (pix >= laststart) && (pix < lastend ) && lastBus->isOk()) { + // WLEDMM same bus as last time - no need to search again + lastBus->setPixelColor(pix - laststart, c); + return; + } + + for (uint_fast8_t i = 0; i < numBusses; i++) { // WLEDMM use fast native types Bus* b = busses[i]; - uint16_t bstart = b->getStart(); + if (b->isOk() == false) continue; // WLEDMM ignore invalid (=not ready) busses + uint_fast16_t bstart = b->getStart(); if (pix < bstart || pix >= bstart + b->getLength()) continue; - busses[i]->setPixelColor(pix - bstart, c); + else { + if (!slowMode) { + // WLEDMM remember last Bus we took + lastBus = b; + laststart = bstart; + lastend = bstart + b->getLength(); + } + b->setPixelColor(pix - bstart, c); + if (!slowMode) break; // WLEDMM found the right Bus -> so we can stop searching - unless we have busses that overlap + } } } -void BusManager::setBrightness(uint8_t b) { +void BusManager::setBrightness(uint8_t b, bool immediate) { for (uint8_t i = 0; i < numBusses; i++) { - busses[i]->setBrightness(b); + busses[i]->setBrightness(b, immediate); } } -void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { +void __attribute__((cold)) BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { if (cct > 255) cct = 255; if (cct >= 0) { //if white balance correction allowed, save as kelvin value instead of 0-255 @@ -530,32 +1343,70 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { Bus::setCCT(cct); } -uint32_t BusManager::getPixelColor(uint16_t pix) { - for (uint8_t i = 0; i < numBusses; i++) { +uint32_t IRAM_ATTR __attribute__((hot)) BusManager::getPixelColor(uint_fast16_t pix) { // WLEDMM use fast native types, IRAM_ATTR + if ((pix >= laststart) && (pix < lastend ) && (lastBus != nullptr) && lastBus->isOk()) { + // WLEDMM same bus as last time - no need to search again + return lastBus->getPixelColor(pix - laststart); + } + + for (uint_fast8_t i = 0; i < numBusses; i++) { Bus* b = busses[i]; - uint16_t bstart = b->getStart(); + if (b->isOk() == false) continue; // WLEDMM ignore invalid (=not ready) busses + uint_fast16_t bstart = b->getStart(); if (pix < bstart || pix >= bstart + b->getLength()) continue; - return b->getPixelColor(pix - bstart); + else { + if (!slowMode) { + // WLEDMM remember last Bus we took + lastBus = b; + laststart = bstart; + lastend = bstart + b->getLength(); + } + return b->getPixelColor(pix - bstart); + } + } + return 0; +} + +uint32_t IRAM_ATTR __attribute__((hot)) BusManager::getPixelColorRestored(uint_fast16_t pix) { // WLEDMM uses bus::getPixelColorRestored() + if ((pix >= laststart) && (pix < lastend ) && (lastBus != nullptr) && lastBus->isOk()) { + // WLEDMM same bus as last time - no need to search again + return lastBus->getPixelColorRestored(pix - laststart); + } + + for (uint_fast8_t i = 0; i < numBusses; i++) { + Bus* b = busses[i]; + if (b->isOk() == false) continue; // WLEDMM ignore invalid (=not ready) busses + uint_fast16_t bstart = b->getStart(); + if (pix < bstart || pix >= bstart + b->getLength()) continue; + else { + if (!slowMode) { + // WLEDMM remember last Bus we took + lastBus = b; + laststart = bstart; + lastend = bstart + b->getLength(); + } + return b->getPixelColorRestored(pix - bstart); + } } return 0; } -bool BusManager::canAllShow() { +bool BusManager::canAllShow() const { for (uint8_t i = 0; i < numBusses; i++) { if (!busses[i]->canShow()) return false; } return true; } -Bus* BusManager::getBus(uint8_t busNr) { +Bus* BusManager::getBus(uint8_t busNr) const { if (busNr >= numBusses) return nullptr; return busses[busNr]; } //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) -uint16_t BusManager::getTotalLength() { - uint16_t len = 0; - for (uint8_t i=0; igetLength(); +uint16_t BusManager::getTotalLength() const { + uint_fast16_t len = 0; + for (uint_fast8_t i=0; igetLength(); // WLEDMM use fast native types return len; } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index b6d79d0776..04062215bf 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -1,12 +1,38 @@ #ifndef BusManager_h #define BusManager_h +#ifdef WLED_ENABLE_HUB75MATRIX +#include +#include +//extern volatile bool previousBufferFree; // experimental +#endif /* * Class for addressing various light types */ #include "const.h" +#if !defined(FASTLED_VERSION) // only pull in FastLED if we don't have it yet + #define FASTLED_INTERNAL + #include +#endif + +//color mangling macros +#if !defined(RGBW32) + #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) + #define R(c) (byte((c) >> 16)) + #define G(c) (byte((c) >> 8)) + #define B(c) (byte(c)) + #define W(c) (byte((c) >> 24)) +#endif + +// WLEDMM bitarray utilities +void setBitInArray(uint8_t* byteArray, size_t position, bool value); // set bit +bool getBitFromArray(const uint8_t* byteArray, size_t position) __attribute__((pure)); // get bit value +size_t getBitArrayBytes(size_t num_bits) __attribute__((const)); // number of bytes needed for an array with num_bits bits +void setBitArray(uint8_t* byteArray, size_t numBits, bool value); // set all bits to same value + + #define GET_BIT(var,bit) (((var)>>(bit))&0x01) #define SET_BIT(var,bit) ((var)|=(uint16_t)(0x0001<<(bit))) #define UNSET_BIT(var,bit) ((var)&=(~(uint16_t)(0x0001<<(bit)))) @@ -28,20 +54,25 @@ struct BusConfig { uint8_t skipAmount; bool refreshReq; uint8_t autoWhite; - uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; + uint8_t artnet_outputs, artnet_fps_limit; + uint16_t artnet_leds_per_output; + + uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; // WLEDMM warning: this means that BusConfig cannot handle nore than 5 pins per bus! uint16_t frequency; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U) { + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t art_o=1, uint16_t art_l=1, uint8_t art_f=30) { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) count = len; start = pstart; colorOrder = pcolorOrder; reversed = rev; skipAmount = skip; autoWhite = aw; frequency = clock_kHz; - uint8_t nPins = 1; - if (type >= TYPE_NET_DDP_RGB && type < 96) nPins = 4; //virtual network bus. 4 "pins" store IP address - else if (type > 47) nPins = 2; - else if (type > 40 && type < 46) nPins = NUM_PWM_PINS(type); - for (uint8_t i = 0; i < nPins; i++) pins[i] = ppins[i]; + artnet_outputs = art_o; artnet_leds_per_output = art_l; artnet_fps_limit = art_f; + uint8_t nPins = 1; // default = only one pin (clockless LEDs like WS281x) + if ((type >= TYPE_NET_DDP_RGB) && (type < (TYPE_NET_DDP_RGB + 16))) nPins = 4; // virtual network bus. 4 "pins" store IP address + else if ((type > 47) && (type < 63)) nPins = 2; // (data + clock / SPI) busses - two pins + else if (IS_PWM(type)) nPins = NUM_PWM_PINS(type); // PWM needs 1..5 pins + else if (type >= TYPE_HUB75MATRIX && type <= (TYPE_HUB75MATRIX + 10)) nPins = 1; // HUB75 does not use LED pins, but we need to preserve the "chain length" parameter + for (uint8_t i = 0; i < min(unsigned(nPins), sizeof(pins)/sizeof(pins[0])); i++) pins[i] = ppins[i]; //softhack007 fix for potential array out-of-bounds access } - //validates start and length and extends total if needed + //validates start and length and extends total if needed // WLEDMM this function is not used anywhere bool adjustBounds(uint16_t& total) { if (!count) count = 1; if (count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; @@ -98,7 +129,7 @@ class Bus { { _type = type; _start = start; - _autoWhiteMode = Bus::hasWhite(_type) ? aw : RGBW_MODE_MANUAL_ONLY; + _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; virtual ~Bus() {} //throw the bus under the bus @@ -107,34 +138,39 @@ class Bus { virtual bool canShow() { return true; } virtual void setStatusPixel(uint32_t c) {} virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; - virtual uint32_t getPixelColor(uint16_t pix) { return 0; } - virtual void setBrightness(uint8_t b) { _bri = b; }; + virtual uint32_t getPixelColor(uint16_t pix) const { return 0; } + virtual uint32_t getPixelColorRestored(uint16_t pix) const { return restore_Color_Lossy(getPixelColor(pix), _bri); } // override in case your bus has a lossless buffer (HUB75, FastLED, Art-Net) + virtual void setBrightness(uint8_t b, bool immediate=false) { _bri = b; } virtual void cleanup() = 0; - virtual uint8_t getPins(uint8_t* pinArray) { return 0; } - virtual uint16_t getLength() { return _len; } + virtual uint8_t getPins(uint8_t* pinArray) const { return 0; } + virtual inline uint16_t getLength() const { return _len; } virtual void setColorOrder() {} - virtual uint8_t getColorOrder() { return COL_ORDER_RGB; } - virtual uint8_t skippedLeds() { return 0; } - virtual uint16_t getFrequency() { return 0U; } - inline uint16_t getStart() { return _start; } + virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } + virtual uint8_t skippedLeds() const { return 0; } + virtual uint16_t getFrequency() const { return 0U; } + virtual uint8_t get_artnet_fps_limit() const { return 0; } + virtual uint8_t get_artnet_outputs() const { return 0; } + virtual uint16_t get_artnet_leds_per_output() const { return 0; } + inline uint16_t getStart() const { return _start; } inline void setStart(uint16_t start) { _start = start; } - inline uint8_t getType() { return _type; } - inline bool isOk() { return _valid; } - inline bool isOffRefreshRequired() { return _needsRefresh; } - bool containsPixel(uint16_t pix) { return pix >= _start && pix < _start+_len; } + inline uint8_t getType() const { return _type; } + inline bool isOk() const { return _valid; } + inline bool isOffRefreshRequired() const { return _needsRefresh; } + //inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start+_len; } // WLEDMM not used, plus wrong - it does not consider skipped pixels + virtual uint16_t getMaxPixels() const { return MAX_LEDS_PER_BUS; } - virtual bool hasRGB() { + virtual bool hasRGB() const { if ((_type >= TYPE_WS2812_1CH && _type <= TYPE_WS2812_WWA) || _type == TYPE_ANALOG_1CH || _type == TYPE_ANALOG_2CH || _type == TYPE_ONOFF) return false; return true; } - virtual bool hasWhite() { return Bus::hasWhite(_type); } + virtual bool hasWhite() const { return Bus::hasWhite(_type); } static bool hasWhite(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_SK6812_RGBW || type == TYPE_TM1814) return true; // digital types with white channel + if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904) return true; // digital types with white channel if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; // analog types with white channel if (type == TYPE_NET_DDP_RGBW) return true; // network types with white channel return false; } - virtual bool hasCCT() { + virtual bool hasCCT() const { if (_type == TYPE_WS2812_2CH_X3 || _type == TYPE_WS2812_WWA || _type == TYPE_ANALOG_2CH || _type == TYPE_ANALOG_5CH) return true; return false; @@ -151,10 +187,21 @@ class Bus { #endif } inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } - inline uint8_t getAutoWhiteMode() { return _autoWhiteMode; } + inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } inline static void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } inline static uint8_t getGlobalAWMode() { return _gAWM; } + inline static uint32_t restore_Color_Lossy(uint32_t c, uint8_t restoreBri) { // shamelessly grabbed from upstream, who grabbed from NPB, who .. + if (restoreBri < 255) { + uint8_t* chan = (uint8_t*) &c; + for (uint_fast8_t i=0; i<4; i++) { + uint_fast16_t val = chan[i]; + chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); //adding _bri slightly improves recovery / stops degradation on re-scale + } + } + return c; + } + bool reversed = false; protected: @@ -169,7 +216,7 @@ class Bus { static int16_t _cct; static uint8_t _cctBlend; - uint32_t autoWhiteCalc(uint32_t c); + uint32_t autoWhiteCalc(uint32_t c) const; }; @@ -179,33 +226,33 @@ class BusDigital : public Bus { inline void show(); - bool canShow(); + bool canShow() override; - void setBrightness(uint8_t b); + void setBrightness(uint8_t b, bool immediate); void setStatusPixel(uint32_t c); void setPixelColor(uint16_t pix, uint32_t c); - uint32_t getPixelColor(uint16_t pix); + uint32_t getPixelColor(uint16_t pix) const override; - uint8_t getColorOrder() { + uint8_t getColorOrder() const { return _colorOrder; } - uint16_t getLength() { + uint16_t getLength() const override { return _len - _skip; } - uint8_t getPins(uint8_t* pinArray); + uint8_t getPins(uint8_t* pinArray) const; void setColorOrder(uint8_t colorOrder); - uint8_t skippedLeds() { + uint8_t skippedLeds() const override { return _skip; } - uint16_t getFrequency() { return _frequencykHz; } + uint16_t getFrequency() const override { return _frequencykHz; } void reinit(); @@ -233,13 +280,13 @@ class BusPwm : public Bus { void setPixelColor(uint16_t pix, uint32_t c); //does no index check - uint32_t getPixelColor(uint16_t pix); + uint32_t getPixelColor(uint16_t pix) const; void show(); - uint8_t getPins(uint8_t* pinArray); + uint8_t getPins(uint8_t* pinArray) const; - uint16_t getFrequency() { return _frequency; } + uint16_t getFrequency() const override { return _frequency; } void cleanup() { deallocatePins(); @@ -267,11 +314,12 @@ class BusOnOff : public Bus { void setPixelColor(uint16_t pix, uint32_t c); - uint32_t getPixelColor(uint16_t pix); + uint32_t getPixelColor(uint16_t pix) const; + uint32_t getPixelColorRestored(uint16_t pix) const override { return getPixelColor(pix);} // WLEDMM BusOnOff ignores brightness void show(); - uint8_t getPins(uint8_t* pinArray); + uint8_t getPins(uint8_t* pinArray) const; void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); @@ -289,28 +337,48 @@ class BusOnOff : public Bus { class BusNetwork : public Bus { public: - BusNetwork(BusConfig &bc); + BusNetwork(BusConfig &bc, const ColorOrderMap &com); - bool hasRGB() { return true; } - bool hasWhite() { return _rgbw; } + uint16_t getMaxPixels() const override { return 4096; }; + bool hasRGB() const { return true; } + bool hasWhite() const { return _rgbw; } void setPixelColor(uint16_t pix, uint32_t c); - uint32_t getPixelColor(uint16_t pix); + uint32_t __attribute__((pure)) getPixelColor(uint16_t pix) const; // WLEDMM attribute added + uint32_t __attribute__((pure)) getPixelColorRestored(uint16_t pix) const override { return getPixelColor(pix);} // WLEDMM BusNetwork ignores brightness void show(); - bool canShow() { + bool canShow() override { // this should be a return value from UDP routine if it is still sending data out return !_broadcastLock; } - uint8_t getPins(uint8_t* pinArray); + uint8_t getPins(uint8_t* pinArray) const override; - uint16_t getLength() { + uint16_t getLength() const override { return _len; } + uint8_t get_artnet_fps_limit() const override { + return _artnet_fps_limit; + } + + uint8_t get_artnet_outputs() const override { + return _artnet_outputs; + } + + uint16_t get_artnet_leds_per_output() const override { + return _artnet_leds_per_output; + } + + void setColorOrder(uint8_t colorOrder); + + uint8_t getColorOrder() const override { + return _colorOrder; + } + void cleanup(); ~BusNetwork() { @@ -318,21 +386,71 @@ class BusNetwork : public Bus { } private: - IPAddress _client; - uint8_t _UDPtype; - uint8_t _UDPchannels; - bool _rgbw; - bool _broadcastLock; - byte *_data; + IPAddress _client; + uint8_t _UDPtype; + uint8_t _UDPchannels; + bool _rgbw; + bool _broadcastLock; + byte *_data; + uint8_t _colorOrder = COL_ORDER_RGB; + uint8_t _artnet_fps_limit; + uint8_t _artnet_outputs; + uint16_t _artnet_leds_per_output; + const ColorOrderMap &_colorOrderMap; }; +#ifdef WLED_ENABLE_HUB75MATRIX +class BusHub75Matrix : public Bus { + public: + BusHub75Matrix(BusConfig &bc); + + uint16_t getMaxPixels() const override { return MAX_LEDS; }; + + bool hasRGB() const override { return true; } + bool hasWhite() const override { return false; } + + void setPixelColor(uint16_t pix, uint32_t c) override; + uint32_t getPixelColor(uint16_t pix) const override; + uint32_t getPixelColorRestored(uint16_t pix) const override; // lossless getPixelColor supported + + void show(void) override; + + void setBrightness(uint8_t b, bool immediate) override; + + uint8_t getPins(uint8_t* pinArray) const override { + pinArray[0] = activeMXconfig.chain_length; + return 1; + } // Fake value due to keep finaliseInit happy + + void deallocatePins(); + + void cleanup(void) override; + + ~BusHub75Matrix() { + cleanup(); + } + + private: + unsigned _panelWidth = 0; + CRGB *_ledBuffer = nullptr; + byte *_ledsDirty = nullptr; + // C++ dirty trick: private static variables are actually _not_ part of the class (however only visibile to class instances). + // These variables persist when BusHub75Matrix gets deleted. + static MatrixPanel_I2S_DMA *activeDisplay; // active display object + static VirtualMatrixPanel *activeFourScanPanel; // active fourScan object + static HUB75_I2S_CFG activeMXconfig; // last used mxconfig + static uint8_t activeType; // last used type + static uint8_t instanceCount; // active instances - 0 or 1 + static uint8_t last_bri; // last used brightness value (persists on driver delete) +}; +#endif class BusManager { public: BusManager() {}; //utility to get the approx. memory usage of a given BusConfig - static uint32_t memUsage(BusConfig &bc); + static uint32_t memUsage(BusConfig &bc) __attribute__((pure)); int add(BusConfig &bc); @@ -341,22 +459,31 @@ class BusManager { void show(); + void invalidateCache(bool isRTMode) { + // WLEDMM clear cached Bus info + lastBus = nullptr; + laststart = 0; + lastend = 0; + slowMode = isRTMode; + } + void setStatusPixel(uint32_t c); void setPixelColor(uint16_t pix, uint32_t c, int16_t cct=-1); - void setBrightness(uint8_t b); + void setBrightness(uint8_t b, bool immediate=false); // immediate=true is for use in ABL, it applies brightness immediately (warning: inefficient) void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - uint32_t getPixelColor(uint16_t pix); + uint32_t __attribute__((pure)) getPixelColor(uint_fast16_t pix); // WLEDMM attribute added + uint32_t __attribute__((pure)) getPixelColorRestored(uint_fast16_t pix); // WLEDMM - bool canAllShow(); + bool canAllShow() const; - Bus* getBus(uint8_t busNr); + Bus* getBus(uint8_t busNr) const; //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - uint16_t getTotalLength(); + uint16_t getTotalLength() const; inline void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); @@ -366,19 +493,24 @@ class BusManager { return colorOrderMap; } - inline uint8_t getNumBusses() { + inline uint8_t getNumBusses() const { return numBusses; } private: uint8_t numBusses = 0; - Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; + Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES] = {nullptr}; // WLEDMM init array ColorOrderMap colorOrderMap; + // WLEDMM cache last used Bus -> 20% to 30% speedup when using many LED pins + Bus *lastBus = nullptr; + unsigned laststart = 0; + unsigned lastend = 0; + bool slowMode = false; // WLEDMM not sure why we need this. But its necessary. - inline uint8_t getNumVirtualBusses() { + inline uint8_t getNumVirtualBusses() const { int j = 0; for (int i=0; igetType() >= TYPE_NET_DDP_RGB && busses[i]->getType() < 96) j++; return j; } }; -#endif \ No newline at end of file +#endif diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 2437cfc67f..64dcd5b979 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -18,6 +18,28 @@ #endif // temporary end +// WLEDMM TroyHacks support - SLOWPATH has priority over TWOPATH +#ifdef WLEDMM_SLOWPATH +#undef WLEDMM_TWOPATH +#endif + +// WLEDMM repeat definition of USER_PRINT +bool canUseSerial(void); // WLEDMM (wled_serial.cpp) returns true if Serial can be used for debug output (i.e. not configured for other purpose) +#if defined(WLED_DEBUG_HOST) + #include "net_debug.h" + extern bool netDebugEnabled; + #define USER_PRINT(x) (netDebugEnabled || !canUseSerial())?NetDebug.print(x):Serial.print(x) + #define USER_PRINTLN(x) (netDebugEnabled || !canUseSerial())?NetDebug.println(x):Serial.println(x) + #define USER_PRINTF(x...) (netDebugEnabled || !canUseSerial())?NetDebug.printf(x):Serial.printf(x) + #define USER_FLUSH() (netDebugEnabled || !canUseSerial())?NetDebug.flush():Serial.flush() +#else + #define USER_PRINT(x) {if (canUseSerial()) Serial.print(x);} + #define USER_PRINTLN(x) {if (canUseSerial()) Serial.println(x);} + #define USER_PRINTF(x...) {if (canUseSerial()) Serial.printf(x);} + #define USER_FLUSH() {if (canUseSerial()) Serial.flush();} +#endif +// WLEDMM end + //Hardware SPI Pins #define P_8266_HS_MOSI 13 #define P_8266_HS_CLK 14 @@ -69,17 +91,17 @@ #define I_32_RN_NEO_3 21 #define I_32_I0_NEO_3 22 #define I_32_I1_NEO_3 23 -#define I_32_BB_NEO_3 24 // bitbangging on ESP32 not recommended +#define I_32_BB_NEO_3 24 // bitbanging on ESP32 not recommended //RGBW #define I_32_RN_NEO_4 25 #define I_32_I0_NEO_4 26 #define I_32_I1_NEO_4 27 -#define I_32_BB_NEO_4 28 // bitbangging on ESP32 not recommended +#define I_32_BB_NEO_4 28 // bitbanging on ESP32 not recommended //400Kbps #define I_32_RN_400_3 29 #define I_32_I0_400_3 30 #define I_32_I1_400_3 31 -#define I_32_BB_400_3 32 // bitbangging on ESP32 not recommended +#define I_32_BB_400_3 32 // bitbanging on ESP32 not recommended //TM1814 (RGBW) #define I_32_RN_TM1_4 33 #define I_32_I0_TM1_4 34 @@ -171,7 +193,7 @@ #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_NEO_3 NeoPixelBusLg #endif -//#define B_32_BB_NEO_3 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod +//#define B_32_BB_NEO_3 NeoPixelBrightnessBus // NeoEsp8266BitBang800KbpsMethod //RGBW #define B_32_RN_NEO_4 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS @@ -180,7 +202,7 @@ #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_NEO_4 NeoPixelBusLg #endif -//#define B_32_BB_NEO_4 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod +//#define B_32_BB_NEO_4 NeoPixelBrightnessBus // NeoEsp8266BitBang800KbpsMethod //400Kbps #define B_32_RN_400_3 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS @@ -189,7 +211,7 @@ #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_400_3 NeoPixelBusLg #endif -//#define B_32_BB_400_3 NeoPixelBusLg // NeoEsp8266BitBang400KbpsMethod +//#define B_32_BB_400_3 NeoPixelBrightnessBus // NeoEsp8266BitBang400KbpsMethod //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS @@ -374,7 +396,7 @@ class PolyBus { case I_32_I1_UCS_4: (static_cast(busPtr))->Begin(); break; #endif // case I_32_BB_UCS_4: (static_cast(busPtr))->Begin(); break; - // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() + // ESP32 can (and should, to avoid inadvertently driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPO_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -389,6 +411,17 @@ class PolyBus { } }; static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel, uint16_t clock_kHz = 0U) { + #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) + #if defined(WLEDMM_FASTPATH) && !defined(WLEDMM_SLOWPATH) // WLEDMM only for fastpath builds. + // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation + // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation + #if defined(WLEDMM_TWOPATH) + if (channel > 1) channel--; // accommodate I2S1 which is used as 2nd bus on classic ESP32 + #else + if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + #endif + #endif + #endif void* busPtr = nullptr; switch (busType) { case I_NONE: break; @@ -423,20 +456,20 @@ class PolyBus { case I_8266_BB_UCS_4: busPtr = new B_8266_BB_UCS_4(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); USER_PRINTF("(RMT #%u) ", channel); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; + case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); USER_PRINT("(I2S #0) "); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: busPtr = new B_32_I1_NEO_3(len, pins[0]); break; + case I_32_I1_NEO_3: busPtr = new B_32_I1_NEO_3(len, pins[0]); USER_PRINT("(I2S #1) "); break; #endif // case I_32_BB_NEO_3: busPtr = new B_32_BB_NEO_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); USER_PRINTF("(RGBW RMT #%u) ", channel); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; + case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); USER_PRINT("(RGBW I2S #0) "); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: busPtr = new B_32_I1_NEO_4(len, pins[0]); break; + case I_32_I1_NEO_4: busPtr = new B_32_I1_NEO_4(len, pins[0]); USER_PRINT("(RGBW I2S #1) "); break; #endif // case I_32_BB_NEO_4: busPtr = new B_32_BB_NEO_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break; @@ -747,7 +780,7 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; +// case I_32_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; @@ -763,7 +796,7 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif -// case I_32_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(colB)); break; +// case I_32_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col.R,col.G,col.B)); break; case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -777,7 +810,7 @@ class PolyBus { case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - #endif + #endif #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; #endif @@ -803,101 +836,101 @@ class PolyBus { case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; } }; - static void setBrightness(void* busPtr, uint8_t busType, uint8_t b) { + static void setBrightness(void* busPtr, uint8_t busType, uint8_t b, bool immediate) { // immediate=true is for use in ABL, it applies brightness immediately (warning: inefficient) switch (busType) { case I_NONE: break; #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U0_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_U1_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_DM_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_8266_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_400_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM1_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_I1_TM2_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; +// case I_32_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_LPO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_HS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_SS_P98_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_LPD_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_LPD_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_LPO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_LPO_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_WS1_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_WS1_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_HS_P98_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; + case I_SS_P98_3: (static_cast(busPtr))->SetLuminance(b); if (immediate) (static_cast(busPtr))->ApplyPostAdjustments(); break; } }; static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { @@ -1178,8 +1211,27 @@ class PolyBus { //if (num > 3) offset = num -4; // I2S not supported yet #else // standard ESP32 has 8 RMT and 2 I2S channels - if (num > 9) return I_NONE; - if (num > 7) offset = num -7; + #ifndef WLEDMM_FASTPATH + #ifdef WLEDMM_SLOWPATH // I2S flickers on large installs. Favor stability over framerate. + if (num > 7) return I_NONE; + #else + if (num == 8) offset = 2; // first use I2S#1 (so #0 stays available for audio) + if (num == 9) offset = 1; // use I2S#0 as the last driver + if (num > 9) return I_NONE; + #endif + #else + // ESP32 "audio_fastpath" - 8 RMT and 1 I2S channels. RMT 5-8 have sending delays, so use I2S#1 before going for RMT 5-8 + #ifdef WLEDMM_SLOWPATH // I2S flickers on large installs. Favor stability over framerate. + if (num > 7) return I_NONE; + #else + if (num > 8) return I_NONE; + #if defined(WLEDMM_TWOPATH) + if (num == 1) offset = 2; // use I2S#1 as 2nd bus - seems to be a good compromise for performance, and reduces flickering for some users + #else + if (num == 0) offset = 2; // use I2S#1 as 1st bus - sometimes helps, if you experience flickering during Wifi or filesystem activity. + #endif + #endif + #endif #endif switch (busType) { case TYPE_WS2812_1CH_X3: diff --git a/wled00/button.cpp b/wled00/button.cpp index d45274a622..7cfb126481 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -21,6 +21,7 @@ void shortPressAction(uint8_t b) case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; } } else { + unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); } @@ -42,6 +43,7 @@ void longPressAction(uint8_t b) case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action } } else { + unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); } @@ -63,6 +65,7 @@ void doublePressAction(uint8_t b) case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { + unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); } @@ -153,6 +156,7 @@ void handleAnalog(uint8_t b) #ifdef ESP8266 rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit #else + if ((btnPin[b] < 0) || (digitalPinToAnalogChannel(btnPin[b]) < 0)) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution #endif yield(); // keep WiFi task running - analog read may take several millis on ESP8266 @@ -167,13 +171,12 @@ void handleAnalog(uint8_t b) // remove noise & reduce frequency of UI updates if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading - // Unomment the next lines if you still see flickering related to potentiometer + // Un-comment the next lines if you still see flickering related to potentiometer // This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?) //unsigned long wait_started = millis(); //while(strip.isUpdating() && (millis() - wait_started < STRIP_WAIT_TIME)) { // delay(1); //} - //if (strip.isUpdating()) return; // give up oldRead[b] = aRead; @@ -185,7 +188,8 @@ void handleAnalog(uint8_t b) if (aRead == 0) { briLast = bri; bri = 0; - } else{ + } else { + if (bri == 0) strip.restartRuntime(false); bri = aRead; } } else if (macroDoublePress[b] == 249) { @@ -223,11 +227,11 @@ void handleAnalog(uint8_t b) void handleButton() { - static unsigned long lastRead = 0UL; + static unsigned long lastAnalogRead = 0UL; static unsigned long lastRun = 0UL; unsigned long now = millis(); - if (strip.isUpdating() && (now - lastRun < 400)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips) + if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips) lastRun = now; for (uint8_t b=0; b ANALOG_BTN_READ_CYCLE) { + if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { handleAnalog(b); - lastRead = now; } continue; } @@ -262,7 +265,7 @@ void handleButton() shortPressAction(b); buttonPressedBefore[b] = true; buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler) - return; + continue; } if (!buttonPressedBefore[b]) buttonPressedTime[b] = now; @@ -283,7 +286,7 @@ void handleButton() // released after rising-edge short press action if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) { if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released - return; + continue; } if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce @@ -322,6 +325,9 @@ void handleButton() shortPressAction(b); } } + if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { + lastAnalogRead = now; + } } // If enabled, RMT idle level is set to HIGH when off @@ -363,6 +369,7 @@ void handleIO() if (rlyPin>=0) { pinMode(rlyPin, OUTPUT); digitalWrite(rlyPin, rlyMde); + delay(50); // wait for relay to switch and power to stabilize } offMode = false; } @@ -371,7 +378,7 @@ void handleIO() if (!offMode) { #ifdef ESP8266 // turn off built-in LED if strip is turned off - // this will break digital bus so will need to be reinitialised on On + // this will break digital bus so will need to be re-initialised on On PinOwner ledPinOwner = pinManager.getPinOwner(LED_BUILTIN); if (!strip.isOffRefreshRequired() && (ledPinOwner == PinOwner::None || ledPinOwner == PinOwner::BusDigital)) { pinMode(LED_BUILTIN, OUTPUT); @@ -382,10 +389,11 @@ void handleIO() esp32RMTInvertIdle(); #endif if (rlyPin>=0) { + if (strip.isUpdating()) delay(FRAMETIME_FIXED); // WLEDMM avoids randomly colored pixles at power-on pinMode(rlyPin, OUTPUT); digitalWrite(rlyPin, !rlyMde); } } offMode = true; } -} \ No newline at end of file +} diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 16daac68d1..069b303aa0 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -1,3 +1,6 @@ +#include // WLEDMM: make sure that I2C drivers have the "right" Wire Object +#include + #include "wled.h" #include "wled_ethernet.h" @@ -14,6 +17,12 @@ void getStringFromJson(char* dest, const char* src, size_t len) { } bool deserializeConfig(JsonObject doc, bool fromFS) { + + //WLEDMM add USER_PRINT + // String temp; + // serializeJson(doc, temp); + DEBUG_PRINTF("deserializeConfig\n"); + bool needsSave = false; //int rev_major = doc["rev"][0]; // 1 //int rev_minor = doc["rev"][1]; // 0 @@ -74,17 +83,29 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted noWifiSleep = !noWifiSleep; - //int wifi_phy = doc[F("wifi")][F("phy")]; //force phy mode n? + force802_3g = doc[F("wifi")][F("phy")] | force802_3g; //force phy mode g? JsonObject hw = doc[F("hw")]; // initialize LED pins and lengths prior to other HW (except for ethernet) JsonObject hw_led = hw["led"]; - uint8_t autoWhiteMode = RGBW_MODE_MANUAL_ONLY; + // WLEDMM: before changing strip, make sure our strip is _not_ servicing effects in parallel + suspendStripService = true; // temporarily lock out strip updates +#ifdef ARDUINO_ARCH_ESP32 + if (strip.isServicing() && (strncmp(pcTaskGetTaskName(NULL), "loopTask", 8) != 0)) { // if we are in looptask (arduino loop), its safe to proceed without waiting + if (fromFS) { + USER_PRINTLN(F("deserializeConfig(fromFS): strip is still drawing effects.")); + } else { + USER_PRINTLN(F("deserializeConfig(): strip is still drawing effects.")); + } + strip.waitUntilIdle(); + } +#endif + CJSON(strip.ablMilliampsMax, hw_led[F("maxpwr")]); CJSON(strip.milliampsPerLed, hw_led[F("ledma")]); - Bus::setGlobalAWMode(hw_led[F("rgbwm")] | 255); + Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED); CJSON(correctWB, hw_led["cct"]); CJSON(cctFromRgb, hw_led[F("cr")]); CJSON(strip.cctBlending, hw_led[F("cb")]); @@ -97,7 +118,21 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject matrix = hw_led[F("matrix")]; if (!matrix.isNull()) { strip.isMatrix = true; - CJSON(strip.panels, matrix[F("mpc")]); + + //WLEDMM: keep storing basic 2d setup + CJSON(strip.panels, matrix[F("mpc")]); + CJSON(strip.bOrA, matrix["ba"]); //WLEDMM basic or advanced + CJSON(strip.panelsV, matrix[F("mpv")]); + CJSON(strip.panelsH, matrix[F("mph")]); + CJSON(strip.matrix.bottomStart, matrix[F("pb")]); + CJSON(strip.matrix.rightStart, matrix[F("pr")]); + CJSON(strip.matrix.vertical, matrix[F("pv")]); + CJSON(strip.matrix.serpentine, matrix["ps"]); + CJSON(strip.panelO.bottomStart, matrix[F("pbl")]); + CJSON(strip.panelO.rightStart, matrix[F("prl")]); + CJSON(strip.panelO.vertical, matrix[F("pvl")]); + CJSON(strip.panelO.serpentine, matrix["psl"]); + strip.panel.clear(); JsonArray panels = matrix[F("panels")]; uint8_t s = 0; @@ -149,7 +184,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } uint16_t length = elm["len"] | 1; - uint8_t colorOrder = (int)elm[F("order")]; + uint8_t colorOrder = (int)elm[F("order")]; // contains white channel swap option in upper nibble uint8_t skipFirst = elm[F("skip")]; uint16_t start = elm["start"] | 0; if (length==0 || start + length > MAX_LEDS) continue; // zero length or we reached max. number of LEDs, just stop @@ -158,14 +193,17 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { bool refresh = elm["ref"] | false; uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM (not yet implemented fully) ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - uint8_t AWmode = elm[F("rgbwm")] | autoWhiteMode; + uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; + uint8_t artnet_outputs = elm["artnet_outputs"] | 1; // sanity check + uint16_t artnet_leds_per_output = elm["artnet_leds_per_output"] | length; // sanity check + uint8_t artnet_fps_limit = elm["artnet_fps_limit"] | 24; // sanity check if (fromFS) { - BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz); + BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, artnet_outputs, artnet_leds_per_output, artnet_fps_limit); mem += BusManager::memUsage(bc); if (mem <= MAX_LED_MEMORY) if (busses.add(bc) == -1) break; // finalization will be done in WLED::beginStrip() } else { if (busConfigs[s] != nullptr) delete busConfigs[s]; - busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode); + busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, artnet_outputs, artnet_leds_per_output, artnet_fps_limit); busesChanged = true; } s++; @@ -197,6 +235,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj[F("ins")]; if (!hw_btn_ins.isNull()) { + for (uint8_t b = 0; b < WLED_MAX_BUTTONS; b++) { // deallocate existing button pins + pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + } uint8_t s = 0; for (JsonObject btn : hw_btn_ins) { CJSON(buttonType[s], btn["type"]); @@ -208,7 +249,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) && (digitalPinToAnalogChannel(btnPin[s]) < 0)) { // not an ADC analog pin - DEBUG_PRINTF("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n", btnPin[s], s); + USER_PRINTF("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n", btnPin[s], s); // WLEDMM btnPin[s] = -1; pinManager.deallocatePin(pin,PinOwner::Button); } @@ -264,6 +305,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 if (hw_ir_pin > -2) { + pinManager.deallocatePin(irPin, PinOwner::IR); if (pinManager.allocatePin(hw_ir_pin, false, PinOwner::IR)) { irPin = hw_ir_pin; } else { @@ -276,6 +318,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject relay = hw[F("relay")]; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { + pinManager.deallocatePin(rlyPin, PinOwner::Relay); if (pinManager.allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; pinMode(rlyPin, OUTPUT); @@ -297,12 +340,12 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } }; if (i2c_scl >= 0 && i2c_sda >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { #ifdef ESP32 - Wire.setPins(i2c_sda, i2c_scl); // this will fail if Wire is initilised (Wire.begin() called prior) + Wire.setPins(i2c_sda, i2c_scl); // this will fail if Wire is initialised (Wire.begin() called prior) #endif - Wire.begin(); + // Wire.begin(); // WLEDMM moved into pinManager + DEBUG_PRINTF("pinmgr success for global i2c %d %d\n", i2c_sda, i2c_scl); } else { - i2c_sda = -1; - i2c_scl = -1; + DEBUG_PRINTF("pinmgr not success for global i2c %d %d\n", i2c_sda, i2c_scl); } JsonArray hw_if_spi = hw[F("if")][F("spi-pin")]; CJSON(spi_mosi, hw_if_spi[0]); @@ -315,49 +358,56 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #else SPI.begin(); #endif + DEBUG_PRINTF("pinmgr success for global spi %d %d %d\n", spi_mosi, spi_miso, spi_sclk); } else { - spi_mosi = -1; - spi_miso = -1; - spi_sclk = -1; + DEBUG_PRINTF("pinmgr not success for global spi %d %d %d\n", spi_mosi, spi_miso, spi_sclk); } //int hw_status_pin = hw[F("status")]["pin"]; // -1 JsonObject light = doc[F("light")]; - CJSON(briMultiplier, light[F("scale-bri")]); - CJSON(strip.paletteBlend, light[F("pal-mode")]); - CJSON(autoSegments, light[F("aseg")]); - - CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.8 - float light_gc_bri = light["gc"]["bri"]; - float light_gc_col = light["gc"]["col"]; - if (light_gc_bri > 1.0f) gammaCorrectBri = true; - else gammaCorrectBri = false; - if (light_gc_col > 1.0f) gammaCorrectCol = true; - else gammaCorrectCol = false; - if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) { - if (gammaCorrectVal != 2.8f) calcGammaTable(gammaCorrectVal); - } else { - gammaCorrectVal = 1.0f; // no gamma correction - gammaCorrectBri = false; - gammaCorrectCol = false; - } - - JsonObject light_tr = light["tr"]; - CJSON(fadeTransition, light_tr["mode"]); - int tdd = light_tr["dur"] | -1; - if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100; - CJSON(strip.paletteFade, light_tr["pal"]); - CJSON(randomPaletteChangeTime, light_tr[F("rpc")]); - - JsonObject light_nl = light["nl"]; - CJSON(nightlightMode, light_nl["mode"]); - byte prev = nightlightDelayMinsDefault; - CJSON(nightlightDelayMinsDefault, light_nl["dur"]); - if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault; + byte prev; //WLEDMM + int tdd; //WLEDMM + if (!light.isNull()) { //WLEDMM: in case cfg string does not contain light! (solves issue that sometimes gamma correction dissappears) + CJSON(briMultiplier, light[F("scale-bri")]); + CJSON(strip.paletteBlend, light[F("pal-mode")]); + CJSON(autoSegments, light[F("aseg")]); + + CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.8 + float light_gc_bri = light["gc"]["bri"]; + float light_gc_col = light["gc"]["col"]; + float light_gc_prev = light["gc"]["prev"]; // WLEDMM + if (light_gc_bri > 1.0f) gammaCorrectBri = true; + else gammaCorrectBri = false; + if (light_gc_col > 1.0f) gammaCorrectCol = true; + else gammaCorrectCol = false; + if (light_gc_prev > 1.0f) gammaCorrectPreview = true; // WLEDMM + else gammaCorrectPreview = false; // WLEDMM + if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) { + if (gammaCorrectVal != 2.8f) calcGammaTable(gammaCorrectVal); + } else { + gammaCorrectVal = 1.0f; // no gamma correction + gammaCorrectBri = false; + gammaCorrectCol = false; + gammaCorrectPreview = false; // WLEDMM + } - CJSON(nightlightTargetBri, light_nl[F("tbri")]); - CJSON(macroNl, light_nl["macro"]); + JsonObject light_tr = light["tr"]; + CJSON(fadeTransition, light_tr["mode"]); + tdd = light_tr["dur"] | -1; + if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100; + CJSON(strip.paletteFade, light_tr["pal"]); + CJSON(randomPaletteChangeTime, light_tr[F("rpc")]); + + JsonObject light_nl = light["nl"]; + CJSON(nightlightMode, light_nl["mode"]); + prev = nightlightDelayMinsDefault; + CJSON(nightlightDelayMinsDefault, light_nl["dur"]); + if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault; + + CJSON(nightlightTargetBri, light_nl[F("tbri")]); + CJSON(macroNl, light_nl["macro"]); + } JsonObject def = doc["def"]; CJSON(bootPreset, def["ps"]); @@ -416,6 +466,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { tdd = if_live[F("timeout")] | -1; if (tdd >= 0) realtimeTimeoutMs = tdd * 100; + + #ifdef WLED_ENABLE_DMX_INPUT + CJSON(dmxInputTransmitPin, if_live_dmx[F("inputRxPin")]); + CJSON(dmxInputReceivePin, if_live_dmx[F("inputTxPin")]); + CJSON(dmxInputEnablePin, if_live_dmx[F("inputEnablePin")]); + CJSON(dmxInputPort, if_live_dmx[F("dmxInputPort")]); + #endif + CJSON(arlsForceMaxBri, if_live[F("maxbri")]); CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false CJSON(arlsOffset, if_live[F("offset")]); // 0 @@ -441,6 +499,13 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(retainMqttMsg, if_mqtt[F("rtn")]); #endif +#ifndef WLED_DISABLE_ESPNOW + JsonObject remote = doc["remote"]; + CJSON(enable_espnow_remote, remote[F("remote_enabled")]); + getStringFromJson(linked_remote, remote[F("linked_remote")], 13); +#endif + + #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces["hue"]; CJSON(huePollingEnabled, if_hue["en"]); @@ -459,6 +524,18 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(hueIP[i], if_hue_ip[i]); #endif +//WLEDMM: add netdebug variables +#ifdef WLED_DEBUG_HOST + JsonObject if_ndb = interfaces["ndb"]; + JsonArray if_ndb_ip = if_ndb["ip"]; + for (byte i = 0; i < 4; i++) + CJSON(netDebugPrintIP[i], if_ndb_ip[i]); + CJSON(netDebugPrintPort, if_ndb["port"]); + CJSON(netDebugEnabled, if_ndb["enabled"]); + // USER_PRINTF("deserializeConfig %d\n", netDebugEnabled); + pinManager.manageDebugTXPin(); +#endif + JsonObject if_ntp = interfaces[F("ntp")]; CJSON(ntpEnabled, if_ntp["en"]); getStringFromJson(ntpServerName, if_ntp[F("host")], 33); // "1.wled.pool.ntp.org" @@ -559,6 +636,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { needsSave = !usermods.readFromConfig(usermods_settings); } + suspendStripService = false; // WLEDMM release lock + if (fromFS) return needsSave; // if from /json/cfg doReboot = doc[F("rb")] | doReboot; @@ -660,7 +739,7 @@ void serializeConfig() { JsonObject wifi = doc.createNestedObject("wifi"); wifi[F("sleep")] = !noWifiSleep; - //wifi[F("phy")] = 1; + wifi[F("phy")] = force802_3g; #ifdef WLED_USE_ETHERNET JsonObject ethernet = doc.createNestedObject("eth"); @@ -704,8 +783,23 @@ void serializeConfig() { if (strip.isMatrix) { JsonObject matrix = hw_led.createNestedObject(F("matrix")); matrix[F("mpc")] = strip.panels; + + //WLEDMM: keep storing basic 2d setup + matrix[F("ba")] = strip.bOrA; //WLEDMM basic or advanced + matrix[F("mph")] = strip.panelsH; + matrix[F("mpv")] = strip.panelsV; + matrix[F("pb")] = strip.matrix.bottomStart; + matrix[F("pr")] = strip.matrix.rightStart; + matrix[F("pv")] = strip.matrix.vertical; + matrix["ps"] = strip.matrix.serpentine; + + matrix[F("pbl")] = strip.panelO.bottomStart; + matrix[F("prl")] = strip.panelO.rightStart; + matrix[F("pvl")] = strip.panelO.vertical; + matrix["psl"] = strip.panelO.serpentine; + JsonArray panels = matrix.createNestedArray(F("panels")); - for (uint8_t i=0; iisOffRefreshRequired(); ins[F("rgbwm")] = bus->getAutoWhiteMode(); ins[F("freq")] = bus->getFrequency(); + ins["artnet_outputs"] = bus->get_artnet_outputs(); + ins["artnet_fps_limit"] = bus->get_artnet_fps_limit(); + ins["artnet_leds_per_output"] = bus->get_artnet_leds_per_output(); } JsonArray hw_com = hw.createNestedArray(F("com")); @@ -804,6 +901,7 @@ void serializeConfig() { JsonObject light_gc = light.createNestedObject("gc"); light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f; // keep compatibility light_gc["col"] = (gammaCorrectCol) ? gammaCorrectVal : 1.0f; // keep compatibility + light_gc["prev"] = (gammaCorrectPreview) ? gammaCorrectVal : 1.0f; // WLEDMM light_gc["val"] = gammaCorrectVal; JsonObject light_tr = light.createNestedObject("tr"); @@ -863,6 +961,12 @@ void serializeConfig() { if_live_dmx[F("addr")] = DMXAddress; if_live_dmx[F("dss")] = DMXSegmentSpacing; if_live_dmx["mode"] = DMXMode; + #ifdef WLED_ENABLE_DMX_INPUT + if_live_dmx[F("inputRxPin")] = dmxInputTransmitPin; + if_live_dmx[F("inputTxPin")] = dmxInputReceivePin; + if_live_dmx[F("inputEnablePin")] = dmxInputEnablePin; + if_live_dmx[F("dmxInputPort")] = dmxInputPort; + #endif if_live[F("timeout")] = realtimeTimeoutMs / 100; if_live[F("maxbri")] = arlsForceMaxBri; @@ -893,6 +997,13 @@ void serializeConfig() { if_mqtt_topics[F("group")] = mqttGroupTopic; #endif +#ifndef WLED_DISABLE_ESPNOW + JsonObject remote = doc.createNestedObject(F("remote")); + remote[F("remote_enabled")] = enable_espnow_remote; + remote[F("linked_remote")] = linked_remote; +#endif + + #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces.createNestedObject("hue"); if_hue["en"] = huePollingEnabled; @@ -910,6 +1021,18 @@ void serializeConfig() { } #endif +//WLEDMM: add netdebug variables +#ifdef WLED_DEBUG_HOST + JsonObject if_ndb = interfaces.createNestedObject("ndb"); + JsonArray if_ndb_ip = if_ndb.createNestedArray("ip"); + for (byte i = 0; i < 4; i++) { + if_ndb_ip.add(netDebugPrintIP[i]); + } + if_ndb["port"] = netDebugPrintPort; + if_ndb["enabled"] = netDebugEnabled; + // USER_PRINTF("serializeConfig %d\n", netDebugEnabled); +#endif + JsonObject if_ntp = interfaces.createNestedObject("ntp"); if_ntp["en"] = ntpEnabled; if_ntp[F("host")] = ntpServerName; @@ -981,6 +1104,9 @@ void serializeConfig() { JsonObject usermods_settings = doc.createNestedObject("um"); usermods.addToConfig(usermods_settings); + //WLEDMM add USER_PRINT + DEBUG_PRINTF("serializeConfig\n"); + File f = WLED_FS.open("/cfg.json", "w"); if (f) serializeJson(doc, f); f.close(); @@ -1007,7 +1133,9 @@ bool deserializeConfigSec() { JsonObject ap = doc["ap"]; getStringFromJson(apPass, ap["psk"] , 65); +#if defined(WLED_ENABLE_MQTT) || !defined(WLED_DISABLE_HUESYNC) JsonObject interfaces = doc["if"]; +#endif #ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; @@ -1046,7 +1174,10 @@ void serializeConfigSec() { JsonObject ap = doc.createNestedObject("ap"); ap["psk"] = apPass; +#if defined(WLED_ENABLE_MQTT) || !defined(WLED_DISABLE_HUESYNC) JsonObject interfaces = doc.createNestedObject("if"); +#endif + #ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["psk"] = mqttPass; diff --git a/wled00/colorTools.hpp b/wled00/colorTools.hpp new file mode 100644 index 0000000000..c47f193468 --- /dev/null +++ b/wled00/colorTools.hpp @@ -0,0 +1,176 @@ +#pragma once +#if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_FASTPATH) && !defined(WLEDMM_SAVE_FLASH) + +#include "wled.h" +/* + * Color conversion & utility methods - moved here, so the compiler may inline these functions (up to 20% faster) + */ + +// WLEDMM make sure that color macros are always defined +#if !defined(RGBW32) +#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) +#endif +#if !defined(W) && !defined(R) +#define R(c) (byte((c) >> 16)) +#define G(c) (byte((c) >> 8)) +#define B(c) (byte(c)) +#define W(c) (byte((c) >> 24)) +#endif + +#if !defined(FASTLED_VERSION) // pull in FastLED if we don't have it yet (we need the CRGB type) + #define FASTLED_INTERNAL + #include +#endif + +/* + * color blend function (from colors.cpp) + */ +inline __attribute__((hot,const)) uint32_t color_blend(uint32_t color1, uint32_t color2, uint_fast16_t blend, bool b16=false) { + if ((color1 == color2) || (blend == 0)) return color1; // WLEDMM + const uint_fast16_t blendmax = b16 ? 0xFFFF : 0xFF; + if(blend >= blendmax) return color2; + const uint_fast8_t shift = b16 ? 16 : 8; + + uint16_t w1 = W(color1); // WLEDMM 16bit to make sure the compiler uses 32bit (not 64bit) for the math + uint16_t r1 = R(color1); + uint16_t g1 = G(color1); + uint16_t b1 = B(color1); + + uint16_t w2 = W(color2); + uint16_t r2 = R(color2); + uint16_t g2 = G(color2); + uint16_t b2 = B(color2); + + if (b16 == false) { + // WLEDMM based on fastled blend8() - better accuracy for 8bit + uint8_t w3 = (w1+w2 == 0) ? 0 : (((w1 << 8)|w2) + (w2 * blend) - (w1*blend) ) >> 8; + uint8_t r3 = (((r1 << 8)|r2) + (r2 * blend) - (r1*blend) ) >> 8; + uint8_t g3 = (((g1 << 8)|g2) + (g2 * blend) - (g1*blend) ) >> 8; + uint8_t b3 = (((b1 << 8)|b2) + (b2 * blend) - (b1*blend) ) >> 8; + return RGBW32(r3, g3, b3, w3); + } else { + // old code has lots of "jumps" due to rounding errors + const uint_fast16_t blend2 = blendmax - blend; // WLEDMM pre-calculate value + uint32_t w3 = ((w2 * blend) + (w1 * blend2)) >> shift; + uint32_t r3 = ((r2 * blend) + (r1 * blend2)) >> shift; + uint32_t g3 = ((g2 * blend) + (g1 * blend2)) >> shift; + uint32_t b3 = ((b2 * blend) + (b1 * blend2)) >> shift; + return RGBW32(r3, g3, b3, w3); + } +} + +/* + * color add function that preserves ratio (from colors.cpp) + * idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule + */ + +inline __attribute__((hot,const)) uint32_t color_add(uint32_t c1, uint32_t c2, bool fast=false) +{ + if (c2 == 0) return c1; // WLEDMM shortcut + if (c1 == 0) return c2; // WLEDMM shortcut + + if (fast) { + uint8_t r = R(c1); + uint8_t g = G(c1); + uint8_t b = B(c1); + uint8_t w = W(c1); + r = qadd8(r, R(c2)); + g = qadd8(g, G(c2)); + b = qadd8(b, B(c2)); + w = qadd8(w, W(c2)); + return RGBW32(r,g,b,w); + } else { + uint32_t r = R(c1) + R(c2); + uint32_t g = G(c1) + G(c2); + uint32_t b = B(c1) + B(c2); + uint32_t w = W(c1) + W(c2); + uint32_t max = r; + if (g > max) max = g; + if (b > max) max = b; + if (w > max) max = w; + if (max < 256) return RGBW32(r, g, b, w); + else return RGBW32(r * 255 / max, g * 255 / max, b * 255 / max, w * 255 / max); + } +} + +/* + * fades color toward black (from colors.cpp) + * if using "video" method the resulting color will never become black unless it is already black + */ + +inline __attribute__((hot,const)) uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false) +{ + if (amount == 255) return c1; // WLEDMM small optimization - plus it avoids over-fading in "video" mode + if (amount == 0) return 0; // WLEDMM shortcut + + uint32_t scaledcolor = 0; // color order is: W R G B from MSB to LSB + uint16_t w = W(c1); // WLEDMM 16bit to make sure the compiler uses 32bit (not 64bit) for the math + uint16_t r = R(c1); + uint16_t g = G(c1); + uint16_t b = B(c1); + if (video) { + uint16_t scale = amount; // 32bit for faster calculation + // bugfix: doing "+1" after shifting is obviously wrong + // optimization: ((r && scale) ? 1 : 0) can be simplified to "if (r > 0) +1" ; if we arive here, then scale != 0 and scale < 255 + if (w>0) scaledcolor |= (((w * scale) >> 8) +1) << 24; // WLEDMM small speedup when no white channel + if (r>0) scaledcolor |= (((r * scale) >> 8) +1) << 16; + if (g>0) scaledcolor |= (((g * scale) >> 8) +1) << 8; + if (b>0) scaledcolor |= ((b * scale) >> 8) +1; + return scaledcolor; + } + else { + uint16_t scale = 1 + amount; + if (w>0) scaledcolor |= ((w * scale) >> 8) << 24; // WLEDMM small speedup when no white channel + scaledcolor |= ((r * scale) >> 8) << 16; + scaledcolor |= (g * scale) & 0x0000FF00; // WLEDMM faster than right-left shift "" >>8 ) <<8" + scaledcolor |= (b * scale) >> 8; + return scaledcolor; + } +} + +//scales the brightness with the briMultiplier factor (from led.cpp) +extern uint_fast16_t briMultiplier; // defined in wled.h +inline __attribute__((hot,const)) byte scaledBri(byte in) // WLEDMM added IRAM_ATTR_YN +{ + if (briMultiplier == 100) return(in); // WLEDMM shortcut + uint_fast16_t val = ((uint_fast16_t)in*(uint_fast16_t)briMultiplier)/100; // WLEDMM + if (val > 255) val = 255; + return (byte)val; +} + +// +// overwrite FastLed colorFromPalette with an optimized version created by dedehai (https://github.com/Aircoookie/WLED/pull/4138) +// +// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes) +// WLEDMM: converted to inline +#undef ColorFromPalette // overwrite any existing override +inline __attribute__((hot)) CRGB ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness=255, TBlendType blendType=LINEARBLEND) +{ + if (blendType == LINEARBLEND_NOWRAP) { + index = (index*240) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping + } + unsigned hi4 = byte(index) >> 4; + const CRGB* entry = (CRGB*)((uint8_t*)(&(pal[0])) + (hi4 * sizeof(CRGB))); + unsigned red1 = entry->r; + unsigned green1 = entry->g; + unsigned blue1 = entry->b; + if (blendType != NOBLEND) { + if (hi4 == 15) entry = &(pal[0]); + else ++entry; + unsigned f2 = ((index & 0x0F) << 4) + 1; // +1 so we scale by 256 as a max value, then result can just be shifted by 8 + unsigned f1 = (257 - f2); // f2 is 1 minimum, so this is 256 max + red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; + green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8; + blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8; + } + if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted + uint32_t scale = brightness + 1; // adjust for rounding (bitshift) + red1 = (red1 * scale) >> 8; + green1 = (green1 * scale) >> 8; + blue1 = (blue1 * scale) >> 8; + } + return RGBW32(red1,green1,blue1,0); +} +#define ColorFromPalette ColorFromPaletteWLED // override fastled function + +#endif diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 9874c31454..37241959f3 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -1,5 +1,6 @@ #include "wled.h" +#if !defined(ARDUINO_ARCH_ESP32) || !defined(WLEDMM_FASTPATH) || defined(WLEDMM_SAVE_FLASH) // WLEDMM: color utils moved into colorTools.hpp, so comiler can inline calls (up to 12% faster) /* * Color conversion & utility methods */ @@ -7,48 +8,109 @@ /* * color blend function */ -uint32_t color_blend(uint32_t color1, uint32_t color2, uint16_t blend, bool b16) { - if(blend == 0) return color1; - uint16_t blendmax = b16 ? 0xFFFF : 0xFF; - if(blend == blendmax) return color2; - uint8_t shift = b16 ? 16 : 8; - - uint32_t w1 = W(color1); - uint32_t r1 = R(color1); - uint32_t g1 = G(color1); - uint32_t b1 = B(color1); - - uint32_t w2 = W(color2); - uint32_t r2 = R(color2); - uint32_t g2 = G(color2); - uint32_t b2 = B(color2); - - uint32_t w3 = ((w2 * blend) + (w1 * (blendmax - blend))) >> shift; - uint32_t r3 = ((r2 * blend) + (r1 * (blendmax - blend))) >> shift; - uint32_t g3 = ((g2 * blend) + (g1 * (blendmax - blend))) >> shift; - uint32_t b3 = ((b2 * blend) + (b1 * (blendmax - blend))) >> shift; - - return RGBW32(r3, g3, b3, w3); +IRAM_ATTR_YN __attribute__((hot)) uint32_t color_blend(uint32_t color1, uint32_t color2, uint_fast16_t blend, bool b16) { + if ((color1 == color2) || (blend == 0)) return color1; // WLEDMM + const uint_fast16_t blendmax = b16 ? 0xFFFF : 0xFF; + if(blend >= blendmax) return color2; + const uint_fast8_t shift = b16 ? 16 : 8; + + uint16_t w1 = W(color1); // WLEDMM 16bit to make sure the compiler uses 32bit (not 64bit) for the math + uint16_t r1 = R(color1); + uint16_t g1 = G(color1); + uint16_t b1 = B(color1); + + uint16_t w2 = W(color2); + uint16_t r2 = R(color2); + uint16_t g2 = G(color2); + uint16_t b2 = B(color2); + + if (b16 == false) { + // WLEDMM based on fastled blend8() - better accuracy for 8bit + uint8_t w3 = (w1+w2 == 0) ? 0 : (((w1 << 8)|w2) + (w2 * blend) - (w1*blend) ) >> 8; + uint8_t r3 = (((r1 << 8)|r2) + (r2 * blend) - (r1*blend) ) >> 8; + uint8_t g3 = (((g1 << 8)|g2) + (g2 * blend) - (g1*blend) ) >> 8; + uint8_t b3 = (((b1 << 8)|b2) + (b2 * blend) - (b1*blend) ) >> 8; + return RGBW32(r3, g3, b3, w3); + } else { + // old code has lots of "jumps" due to roundding errors + const uint_fast16_t blend2 = blendmax - blend; // WLEDMM pre-calculate value + uint32_t w3 = ((w2 * blend) + (w1 * blend2)) >> shift; + uint32_t r3 = ((r2 * blend) + (r1 * blend2)) >> shift; + uint32_t g3 = ((g2 * blend) + (g1 * blend2)) >> shift; + uint32_t b3 = ((b2 * blend) + (b1 * blend2)) >> shift; + return RGBW32(r3, g3, b3, w3); + } } /* * color add function that preserves ratio * idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule */ -uint32_t color_add(uint32_t c1, uint32_t c2) +IRAM_ATTR_YN uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) // WLEDMM added IRAM_ATTR_YN { - uint32_t r = R(c1) + R(c2); - uint32_t g = G(c1) + G(c2); - uint32_t b = B(c1) + B(c2); - uint32_t w = W(c1) + W(c2); - uint16_t max = r; - if (g > max) max = g; - if (b > max) max = b; - if (w > max) max = w; - if (max < 256) return RGBW32(r, g, b, w); - else return RGBW32(r * 255 / max, g * 255 / max, b * 255 / max, w * 255 / max); + if (c2 == 0) return c1; // WLEDMM shortcut + if (c1 == 0) return c2; // WLEDMM shortcut + + if (fast) { + uint8_t r = R(c1); + uint8_t g = G(c1); + uint8_t b = B(c1); + uint8_t w = W(c1); + r = qadd8(r, R(c2)); + g = qadd8(g, G(c2)); + b = qadd8(b, B(c2)); + w = qadd8(w, W(c2)); + return RGBW32(r,g,b,w); + } else { + uint32_t r = R(c1) + R(c2); + uint32_t g = G(c1) + G(c2); + uint32_t b = B(c1) + B(c2); + uint32_t w = W(c1) + W(c2); + uint32_t max = r; + if (g > max) max = g; + if (b > max) max = b; + if (w > max) max = w; + if (max < 256) return RGBW32(r, g, b, w); + else return RGBW32(r * 255 / max, g * 255 / max, b * 255 / max, w * 255 / max); + } } +/* + * fades color toward black + * if using "video" method the resulting color will never become black unless it is already black + */ + +IRAM_ATTR_YN __attribute__((hot)) uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) +{ + if (amount == 255) return c1; // WLEDMM small optimization - plus it avoids over-fading in "video" mode + if (amount == 0) return 0; // WLEDMM shortcut + + uint32_t scaledcolor = 0; // color order is: W R G B from MSB to LSB + uint16_t w = W(c1); // WLEDMM 16bit to make sure the compiler uses 32bit (not 64bit) for the math + uint16_t r = R(c1); + uint16_t g = G(c1); + uint16_t b = B(c1); + if (video) { + uint16_t scale = amount; // 32bit for faster calculation + // bugfix: doing "+1" after shifting is obviously wrong + // optimization: ((r && scale) ? 1 : 0) can be simplified to "if (r > 0) +1" ; if we arive here, then scale != 0 and scale < 255 + if (w>0) scaledcolor |= (((w * scale) >> 8) +1) << 24; // WLEDMM small speedup when no white channel + if (r>0) scaledcolor |= (((r * scale) >> 8) +1) << 16; + if (g>0) scaledcolor |= (((g * scale) >> 8) +1) << 8; + if (b>0) scaledcolor |= ((b * scale) >> 8) +1; + return scaledcolor; + } + else { + uint16_t scale = 1 + amount; + if (w>0) scaledcolor |= ((w * scale) >> 8) << 24; // WLEDMM small speedup when no white channel + scaledcolor |= ((r * scale) >> 8) << 16; + scaledcolor |= (g * scale) & 0x0000FF00; // WLEDMM faster than right-left shift "" >>8 ) <<8" + scaledcolor |= (b * scale) >> 8; + return scaledcolor; + } +} +#endif + void setRandomColor(byte* rgb) { lastRandomIndex = strip.getMainSegment().get_random_wheel_index(lastRandomIndex); @@ -232,22 +294,23 @@ bool colorFromHexString(byte* rgb, const char* in) { } return true; } - -float minf (float v, float w) +#if 0 // WLEDMM minf/maxf are defined in libm already +static float minf (float v, float w) // WLEDMM better use standard library fminf() { if (w > v) return v; return w; } -float maxf (float v, float w) +static float maxf (float v, float w) // WLEDMM better use standard library fmaxf() { if (w > v) return w; return v; } +#endif // adjust RGB values based on color temperature in K (range [2800-10200]) (https://en.wikipedia.org/wiki/Color_balance) // called from bus manager when color correction is enabled! -uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb) +uint32_t __attribute__((hot)) IRAM_ATTR_YN colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb) // WLEDMM: IRAM_ATTR_YN { //remember so that slow colorKtoRGB() doesn't have to run for every setPixelColor() static byte correctionRGB[4] = {0,0,0,0}; @@ -301,8 +364,9 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb) { } } +#if !defined(WLED_USE_CIE_BRIGHTNESS_TABLE) //gamma 2.8 lookup table used for color correction -static byte gammaT[] = { +static byte gammaT[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, @@ -319,6 +383,56 @@ static byte gammaT[] = { 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 }; +#else +// experimental +// CIE 1931 lookup table (8bit->8bit) that was proposed during discussion of issue #2767 +// https://github.com/Aircoookie/WLED/issues/2767#issuecomment-1310961308 +// unfortunately NeoPixelBus has its own internal table, that kills low brightness values similar to the original WLED table. +// see https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoGamma.h +static const byte gammaT[256] = { + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, + 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 10, 11, + 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 15, 16, 16, 17, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, + 25, 25, 26, 27, 27, 28, 28, 29, 30, 30, 31, 31, 32, 33, 33, 34, + 35, 35, 36, 37, 38, 38, 39, 40, 41, 41, 42, 43, 44, 44, 45, 46, + 47, 48, 49, 50, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, + 79, 81, 82, 83, 84, 85, 87, 88, 89, 91, 92, 93, 94, 96, 97, 99, + 100, 101, 103, 104, 106, 107, 109, 110, 111, 113, 115, 116, 118, 119, 121, + 122, 124, 126, 127, 129, 130, 132, 134, 135, 137, 139, 141, 142, 144, 146, + 148, 150, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 170, 172, 174, + 177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 198, 200, 202, 204, 207, + 209, 211, 213, 216, 218, 220, 223, 225, 227, 230, 232, 235, 237, 240, 242, + 245, 247, 250, 252, 255 }; +#endif + +// WLEDMM begin +static uint8_t gammaTinv[256] = { 0 }; +static void calcInvGammaTable(float gamma) +{ + float gammaInv = 1.0f / 2.4f; // surprise surprise: WLED palettes use a fixed gamma of 2.4 !!! + //float gammaInv = 1.0f / gamma; // if we go by the book, 1.0/gamma will revert gamma corrections + for (size_t i = 0; i < 256; i++) { + gammaTinv[i] = (int)(powf((float)i / 255.0f, gammaInv) * 255.0f + 0.5f); + } +} +uint8_t __attribute__((hot)) unGamma8(uint8_t value) { + //if (!gammaCorrectCol || (value == 0) || (value == 255)) return value; + if ((value == 0) || (value == 255)) return value; + if ((gammaCorrectVal < 0.999f) || (gammaCorrectVal > 3.0f)) return value; + if (gammaTinv[255] == 0) calcInvGammaTable(gammaCorrectVal); + return gammaTinv[value]; +} + +uint32_t __attribute__((hot)) unGamma24(uint32_t c) { + if ((gammaCorrectVal < 0.999f) || (gammaCorrectVal > 3.0f)) return c; + if (gammaTinv[255] == 0) calcInvGammaTable(gammaCorrectVal); + return RGBW32(gammaTinv[R(c)], gammaTinv[G(c)], gammaTinv[B(c)], W(c)); +} +// wleDMM end uint8_t gamma8_cal(uint8_t b, float gamma) { @@ -328,19 +442,22 @@ uint8_t gamma8_cal(uint8_t b, float gamma) // re-calculates & fills gamma table void calcGammaTable(float gamma) { +#if !defined(WLED_USE_CIE_BRIGHTNESS_TABLE) // WLEDMM not possible when using the CIE table for (uint16_t i = 0; i < 256; i++) { gammaT[i] = gamma8_cal(i, gamma); } +#endif + calcInvGammaTable(gamma); // WLEDMM } // used for individual channel or brightness gamma correction -uint8_t gamma8(uint8_t b) +IRAM_ATTR_YN __attribute__((hot)) uint8_t gamma8(uint8_t b) // WLEDMM added IRAM_ATTR_YN { return gammaT[b]; } // used for color gamma correction -uint32_t gamma32(uint32_t color) +uint32_t __attribute__((hot)) gamma32(uint32_t color) { if (!gammaCorrectCol) return color; uint8_t w = W(color); diff --git a/wled00/const.h b/wled00/const.h index cade2a0dc4..f224751212 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -5,7 +5,7 @@ * Readability defines and their associated numerical values + compile-time constants */ -#define GRADIENT_PALETTE_COUNT 58 +#define GRADIENT_PALETTE_COUNT 62 //WLEDMM netmindz ar palette +3, ewowi Random Smooth palette +1 //Defaults #define DEFAULT_CLIENT_SSID "Your_Network" @@ -44,8 +44,8 @@ #define WLED_MIN_VIRTUAL_BUSSES 4 #else #if defined(USERMOD_AUDIOREACTIVE) // requested by @softhack007 https://github.com/blazoncek/WLED/issues/33 - #define WLED_MAX_BUSSES 8 - #define WLED_MIN_VIRTUAL_BUSSES 2 + #define WLED_MAX_BUSSES 9 // WLEDMM I2S#1 is availeable for LEDs + #define WLED_MIN_VIRTUAL_BUSSES 1 #else #define WLED_MAX_BUSSES 10 #define WLED_MIN_VIRTUAL_BUSSES 0 @@ -103,7 +103,7 @@ #define USERMOD_ID_ROTARY_ENC_UI 8 //Usermod "usermod_v2_rotary_encoder_ui.h" #define USERMOD_ID_AUTO_SAVE 9 //Usermod "usermod_v2_auto_save.h" #define USERMOD_ID_DHT 10 //Usermod "usermod_dht.h" -#define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h" +// #define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h" #define USERMOD_ID_VL53L0X 12 //Usermod "usermod_vl53l0x_gestures.h" #define USERMOD_ID_MULTI_RELAY 13 //Usermod "usermod_multi_relay.h" #define USERMOD_ID_ANIMATED_STAIRCASE 14 //Usermod "Animated_Staircase.h" @@ -132,7 +132,17 @@ #define USERMOD_ID_SD_CARD 37 //Usermod "usermod_sd_card.h" #define USERMOD_ID_PWM_OUTPUTS 38 //Usermod "usermod_pwm_outputs.h #define USERMOD_ID_SHT 39 //Usermod "usermod_sht.h -#define USERMOD_ID_KLIPPER 40 // Usermod Klipper percentage +#define USERMOD_ID_KLIPPER 40 //Usermod Klipper percentage +#define USERMOD_ID_WIREGUARD 41 //Usermod "wireguard.h" +#define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h" +#define USERMOD_ID_LDR_DUSK_DAWN 43 //Usermod "usermod_LDR_Dusk_Dawn_v2.h" +//WLEDMM +#define USERMOD_ID_MCUTEMP 89 //Usermod "usermod_v2_artifx.h" +#define USERMOD_ID_ARTIFX 90 //Usermod "usermod_v2_artifx.h" +#define USERMOD_ID_WEATHER 91 //Usermod "usermod_v2_weather.h" +#define USERMOD_ID_GAMES 92 //Usermod "usermod_v2_games.h" +#define USERMOD_ID_ANIMARTRIX 93 //Usermod "usermod_v2_animartrix.h" +#define USERMOD_ID_AUTOPLAYLIST 94 // Usermod usermod_v2_auto_playlist.h //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -149,7 +159,7 @@ #define CALL_MODE_NO_NOTIFY 5 #define CALL_MODE_FX_CHANGED 6 //no longer used #define CALL_MODE_HUE 7 -#define CALL_MODE_PRESET_CYCLE 8 +#define CALL_MODE_PRESET_CYCLE 8 //no longer used #define CALL_MODE_BLYNK 9 //no longer used #define CALL_MODE_ALEXA 10 #define CALL_MODE_WS_SEND 11 //special call mode, not for notifier, updates websocket only @@ -174,6 +184,7 @@ #define REALTIME_MODE_ARTNET 6 #define REALTIME_MODE_TPM2NET 7 #define REALTIME_MODE_DDP 8 +#define REALTIME_MODE_DMX 9 //realtime override modes #define REALTIME_OVERRIDE_NONE 0 @@ -189,8 +200,8 @@ #define DMX_MODE_MULTIPLE_RGB 4 //every LED is addressed with its own RGB (ledCount * 3 channels) #define DMX_MODE_MULTIPLE_DRGB 5 //every LED is addressed with its own RGB and share a master dimmer (ledCount * 3 + 1 channels) #define DMX_MODE_MULTIPLE_RGBW 6 //every LED is addressed with its own RGBW (ledCount * 4 channels) -#define DMX_MODE_EFFECT_SEGMENT 8 //trigger standalone effects of WLED (15 channels per segement) -#define DMX_MODE_EFFECT_SEGMENT_W 9 //trigger standalone effects of WLED (18 channels per segement) +#define DMX_MODE_EFFECT_SEGMENT 8 //trigger standalone effects of WLED (15 channels per segment) +#define DMX_MODE_EFFECT_SEGMENT_W 9 //trigger standalone effects of WLED (18 channels per segment) #define DMX_MODE_PRESET 10 //apply presets (1 channel) //Light capability byte (unused) 0bRCCCTTTT @@ -233,16 +244,24 @@ #define TYPE_LPD8806 52 #define TYPE_P9813 53 #define TYPE_LPD6803 54 + +// WLEDMM additional types +#define TYPE_HUB75MATRIX 100 // 100 - 110 +// WLEDMM caution - do not use bus types > 127 + //Network types (master broadcast) (80-95) #define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus) #define TYPE_NET_E131_RGB 81 //network E131 RGB bus (master broadcast bus, unused) -#define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus, unused) +#define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus) +#define TYPE_NET_ARTNET_RGBW 83 //network ArtNet RGB bus (master broadcast bus) #define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus) -#define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63 +#define IS_DIGITAL(t) (((t) & 0x10) || ((t)==TYPE_HUB75MATRIX)) //digital are 16-31 and 48-63 // WLEDMM added HUB75 #define IS_PWM(t) ((t) > 40 && (t) < 46) #define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only #define IS_2PIN(t) ((t) > 47) +#define IS_VIRTUAL(t) ( ((t) <= TYPE_RESERVED) || (((t) >= TYPE_NET_DDP_RGB) && ((t) < (TYPE_NET_DDP_RGB + 16))) ) // WLEDMM 80..95 are network "virtual" busses +#define EXCLUDE_FROM_ABL(t) ( IS_VIRTUAL(t) || ( (t) >= (TYPE_HUB75MATRIX) && (t) < (TYPE_HUB75MATRIX + 10))) // WLEDMM do not apply auto-brightness-limiter on these bus types //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut @@ -266,7 +285,7 @@ #define BTN_TYPE_ANALOG_INVERTED 8 //Ethernet board types -#define WLED_NUM_ETH_TYPES 11 +#define WLED_NUM_ETH_TYPES 12 //WLEDMM +1 for Olimex ESP32-Gateway #define WLED_ETH_NONE 0 #define WLED_ETH_WT32_ETH01 1 @@ -279,6 +298,7 @@ #define WLED_ETH_QUINLED_OCTA 8 #define WLED_ETH_ABCWLEDV43ETH 9 #define WLED_ETH_SERG74 10 +#define WLED_ETH_OLIMEX_GTW 11 //Hue error codes #define HUE_ERROR_INACTIVE 0 @@ -306,7 +326,7 @@ #define SEG_DIFFERS_OPT 0x02 // all segment options except: selected, reset & transitional #define SEG_DIFFERS_COL 0x04 // colors #define SEG_DIFFERS_FX 0x08 // effect/mode parameters -#define SEG_DIFFERS_BOUNDS 0x10 // segment start/stop ounds +#define SEG_DIFFERS_BOUNDS 0x10 // segment start/stop bounds #define SEG_DIFFERS_GSO 0x20 // grouping, spacing & offset #define SEG_DIFFERS_SEL 0x80 // selected @@ -320,33 +340,67 @@ // WLED Error modes #define ERR_NONE 0 // All good :) -#define ERR_EEP_COMMIT 2 // Could not commit to EEPROM (wrong flash layout?) +#define ERR_DENIED 1 // Permission denied +#define ERR_EEP_COMMIT 2 // Could not commit to EEPROM (wrong flash layout?) OBSOLETE #define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) #define ERR_FS_QUOTA 11 // The FS is full or the maximum file size is reached #define ERR_FS_PLOAD 12 // It was attempted to load a preset that does not exist #define ERR_FS_IRLOAD 13 // It was attempted to load an IR JSON cmd, but the "ir.json" file does not exist -#define ERR_FS_GENERAL 19 // A general unspecified filesystem error occured +#define ERR_FS_RMLOAD 14 // It was attempted to load an remote JSON cmd, but the "remote.json" file does not exist +#define ERR_FS_GENERAL 19 // A general unspecified filesystem error occurred #define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented) #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) - -//Timer mode types +#define ERR_LOW_MEM 33 // WLEDMM: low memory (RAM) +#define ERR_LOW_SEG_MEM 34 // WLEDMM: low memory (segment data RAM) +#define ERR_LOW_WS_MEM 35 // WLEDMM: low memory (ws) +#define ERR_LOW_AJAX_MEM 36 // WLEDMM: low memory (oappend) +#define ERR_LOW_BUF 37 // WLEDMM: low memory (LED buffer from allocLEDs) +#define ERR_SYS_REBOOT 90 // WLEDMM: reboot after error +#define ERR_SYS_BROWNOUT 91 // WLEDMM: reboot after brownout alert +#define ERR_REBOOT_NEEDED 98 // WLEDMM: reboot needed after changing hardware setting +#define ERR_POWEROFF_NEEDED 99 // WLEDMM: power-cycle needed after changing hardware setting + +// Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness #define NL_MODE_FADE 1 //Fade to target brightness gradually #define NL_MODE_COLORFADE 2 //Fade to target brightness and secondary color gradually #define NL_MODE_SUN 3 //Sunrise/sunset. Target brightness is set immediately, then Sunrise effect is started. Max 60 min. - -#define NTP_PACKET_SIZE 48 +// Settings sub page IDs +#define SUBPAGE_MENU 0 +#define SUBPAGE_WIFI 1 +#define SUBPAGE_LEDS 2 +#define SUBPAGE_UI 3 +#define SUBPAGE_SYNC 4 +#define SUBPAGE_TIME 5 +#define SUBPAGE_SEC 6 +#define SUBPAGE_DMX 7 +#define SUBPAGE_UM 8 +#define SUBPAGE_UPDATE 9 +#define SUBPAGE_2D 10 +#define SUBPAGE_LOCK 251 +#define SUBPAGE_PINREQ 252 +#define SUBPAGE_CSS 253 +#define SUBPAGE_JS 254 +#define SUBPAGE_WELCOME 255 + +#define NTP_PACKET_SIZE 48 // size of NTP receive buffer +#define NTP_MIN_PACKET_SIZE 48 // min expected size - NTP v4 allows for "extended information" appended to the standard fields //maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses #ifndef MAX_LEDS #ifdef ESP8266 #define MAX_LEDS 1664 //can't rely on memory limit to limit this to 1600 LEDs #else -#define MAX_LEDS 8192 +//#define MAX_LEDS 8192 +#if !defined(CONFIG_IDF_TARGET_ESP32S3) + #define MAX_LEDS 8464 // WLEDMM 92x92 for esp32, esp32-S2 and esp32-c3 +#else + #define MAX_LEDS 18436 // WLEDMM 128x128 + 2048 + 4 for esp32-S3 +#endif #endif #endif @@ -363,28 +417,46 @@ #endif #ifndef MAX_LEDS_PER_BUS -#define MAX_LEDS_PER_BUS 2048 // may not be enough for fast LEDs (i.e. APA102) +#if !defined(ARDUINO_ARCH_ESP32) + #define MAX_LEDS_PER_BUS 2048 // may not be enough for fast LEDs (i.e. APA102) +#else + #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S3 + #define MAX_LEDS_PER_BUS MAX_LEDS // for fast LEDs and fast MCUs (i.e. APA102, HUB75, ART.Net) - allows to have all LEDs on one bus + #else + #define MAX_LEDS_PER_BUS 2048 // may not be enough for fast LEDs (i.e. APA102) + #endif +#endif #endif -// string temp buffer (now stored in stack locally) +// string temp buffer (now stored in stack locally) // WLEDMM ...which is actually not the greatest design choice on ESP32 #ifdef ESP8266 #define SETTINGS_STACK_BUF_SIZE 2048 #else -#define SETTINGS_STACK_BUF_SIZE 3096 + #if !defined(USERMOD_AUDIOREACTIVE) + #define SETTINGS_STACK_BUF_SIZE 3834 // WLEDMM added 696+32 bytes of margin (was 3096) + #else + #define SETTINGS_STACK_BUF_SIZE 4000 // WLEDMM more buffer for audioreactive UI (add '-D CONFIG_ASYNC_TCP_TASK_STACK_SIZE=9216' to your build_flags) + #endif #endif -#ifdef WLED_USE_ETHERNET - #define E131_MAX_UNIVERSE_COUNT 20 -#else - #ifdef ESP8266 - #define E131_MAX_UNIVERSE_COUNT 9 +#ifndef E131_MAX_UNIVERSE_COUNT + #ifdef WLED_USE_ETHERNET + #define E131_MAX_UNIVERSE_COUNT 20 #else - #define E131_MAX_UNIVERSE_COUNT 12 + #ifdef ESP8266 + #define E131_MAX_UNIVERSE_COUNT 9 + #else + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32C6) + #define E131_MAX_UNIVERSE_COUNT 112 // WLEDMM enough universes for 128 x 128 pixels, for boards that can handle it + #else + #define E131_MAX_UNIVERSE_COUNT 12 // WLEDMM use upstream default for S2 and C3 + #endif + #endif #endif #endif #ifndef ABL_MILLIAMPS_DEFAULT - #define ABL_MILLIAMPS_DEFAULT 850 // auto lower brightness to stay close to milliampere limit + #define ABL_MILLIAMPS_DEFAULT 1500 // auto lower brightness to stay close to milliampere limit WLEDMM: min 1500 for 1024leds #else #if ABL_MILLIAMPS_DEFAULT == 0 // disable ABL #elif ABL_MILLIAMPS_DEFAULT < 250 // make sure value is at least 250 @@ -405,14 +477,30 @@ #define TOUCH_THRESHOLD 32 // limit to recognize a touch, higher value means more sensitive // Size of buffer for API JSON object (increase for more segments) +#if !defined(JSON_BUFFER_SIZE) #ifdef ESP8266 #define JSON_BUFFER_SIZE 10240 #else + #if defined(BOARD_HAS_PSRAM) && (defined(WLED_USE_PSRAM) || defined(WLED_USE_PSRAM_JSON)) + #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) + #if defined(ARDUINO_ARCH_ESP32C3) + #define JSON_BUFFER_SIZE 44000 // WLEDMM - max 44KB on -C3 with PSRAM (chip has 400kb RAM) + #else + #define JSON_BUFFER_SIZE 28000 // WLEDMM - max 28KB on -S2 with PSRAM (chip has 320kb RAM) + #endif + #else + #define JSON_BUFFER_SIZE 54000 // WLEDMM (was 60000) slightly reduced to avoid build error "region dram0_0_seg overflowed" + #endif + #else #define JSON_BUFFER_SIZE 24576 + #endif +#endif #endif //#define MIN_HEAP_SIZE (8k for AsyncWebServer) +#if !defined(MIN_HEAP_SIZE) #define MIN_HEAP_SIZE 8192 +#endif // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 @@ -423,8 +511,9 @@ //this is merely a default now and can be changed at runtime #ifndef LEDPIN -#if defined(ESP8266) || (defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM)) || defined(CONFIG_IDF_TARGET_ESP32C3) - #define LEDPIN 2 // GPIO2 (D4) on Wemod D1 mini compatible boards + +#if defined(ESP8266) || (defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ARDUINO_ESP32_PICO) //WLEDMM + #define LEDPIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, and on boards where GPIO16 is not available #else #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards #endif @@ -442,61 +531,42 @@ #define DEFAULT_LED_COUNT 30 #endif -#define INTERFACE_UPDATE_COOLDOWN 2000 //time in ms to wait between websockets, alexa, and MQTT updates +#define INTERFACE_UPDATE_COOLDOWN 1200 // time in ms to wait between websockets, alexa, and MQTT updates + +#define PIN_RETRY_COOLDOWN 3000 // time in ms after an incorrect attempt PIN and OTA pass will be rejected even if correct +#define PIN_TIMEOUT 900000 // time in ms after which the PIN will be required again, 15 minutes // HW_PIN_SCL & HW_PIN_SDA are used for information in usermods settings page and usermods themselves -// which GPIO pins are actually used in a hardwarea layout (controller board) -#if defined(I2CSCLPIN) && !defined(HW_PIN_SCL) - #define HW_PIN_SCL I2CSCLPIN -#endif -#if defined(I2CSDAPIN) && !defined(HW_PIN_SDA) - #define HW_PIN_SDA I2CSDAPIN -#endif -// you cannot change HW I2C pins on 8266 -#if defined(ESP8266) && defined(HW_PIN_SCL) - #undef HW_PIN_SCL -#endif -#if defined(ESP8266) && defined(HW_PIN_SDA) - #undef HW_PIN_SDA -#endif +// which GPIO pins are actually used in a hardware layout (controller board) +//WLEDMM: unchangeable pins are not treated here by undef them, but elsewhere in the code // defaults for 1st I2C on ESP32 (Wire global) #ifndef HW_PIN_SCL - #define HW_PIN_SCL SCL + #define HW_PIN_SCL -1 //WLEDMM if not defined, -1 will be used (not SCL/22) (also for esp8266?) #endif #ifndef HW_PIN_SDA - #define HW_PIN_SDA SDA + #define HW_PIN_SDA -1 //WLEDMM if not defined, -1 will be used (not SDA/21) (also for esp8266?) #endif // HW_PIN_SCLKSPI & HW_PIN_MOSISPI & HW_PIN_MISOSPI are used for information in usermods settings page and usermods themselves -// which GPIO pins are actually used in a hardwarea layout (controller board) -#if defined(SPISCLKPIN) && !defined(HW_PIN_CLOCKSPI) - #define HW_PIN_CLOCKSPI SPISCLKPIN -#endif -#if defined(SPIMOSIPIN) && !defined(HW_PIN_MOSISPI) - #define HW_PIN_MOSISPI SPIMOSIPIN -#endif -#if defined(SPIMISOPIN) && !defined(HW_PIN_MISOSPI) - #define HW_PIN_MISOSPI SPIMISOPIN -#endif -// you cannot change HW SPI pins on 8266 -#if defined(ESP8266) && defined(HW_PIN_CLOCKSPI) - #undef HW_PIN_CLOCKSPI -#endif -#if defined(ESP8266) && defined(HW_PIN_DATASPI) - #undef HW_PIN_DATASPI -#endif -#if defined(ESP8266) && defined(HW_PIN_MISOSPI) - #undef HW_PIN_MISOSPI -#endif +// which GPIO pins are actually used in a hardware layout (controller board) +//WLEDMM: unchangeable pins are not treated here by undef them, but elsewhere in the code // defaults for VSPI on ESP32 (SPI global, SPI.cpp) as HSPI is used by WLED (bus_wrapper.h) #ifndef HW_PIN_CLOCKSPI - #define HW_PIN_CLOCKSPI SCK + #define HW_PIN_CLOCKSPI -1 //WLEDMM if not defined -1 will be used (not SCK/18) #endif -#ifndef HW_PIN_DATASPI - #define HW_PIN_DATASPI MOSI +#ifndef HW_PIN_MOSISPI //WLEDMM renamed from HW_PIN_DATASPI + #define HW_PIN_MOSISPI -1 //WLEDMM if not defined -1 will be used (not MOSI/23) #endif #ifndef HW_PIN_MISOSPI - #define HW_PIN_MISOSPI MISO + #define HW_PIN_MISOSPI -1 //WLEDMM if not defined -1 will be used (not MISO/19) +#endif + +// WLEDMM: IRAM_ATTR for 8266 causes error: section `.text1' will not fit in region `iram1_0_seg' +// error only in MM, not in upstream... tbd: find out why +#ifdef ARDUINO_ARCH_ESP32 + #define IRAM_ATTR_YN IRAM_ATTR +#else + #define IRAM_ATTR_YN #endif #endif diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index 5a8c801e5e..773c02d20d 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -1,6 +1,7 @@ + @@ -45,6 +46,7 @@ width: 7px; top: 50%; transform: translateY(-50%); + touch-action: none; } .color-picker-marker { height: 7px; @@ -94,9 +96,14 @@ line-height: 1; } .wrap { - width: 800px; + width: 100%; margin: 0 auto; } + @media (min-width: 800px) { + .wrap { + width: 800px; + } + } .palette { height: 20px; } @@ -136,6 +143,9 @@ .sendSpan, .editSpan{ cursor: pointer; } + h1 { + font-size: 1.6rem; + } @@ -191,6 +201,7 @@

var gradientLength = rect.width; var mOffs = Math.round((gradientLength / 256) / 2) - 5; var paletteArray = []; //Holds the palettes after load. + var paletteName = []; // Holds the names of the palettes after load. var svgSave = '' var svgEdit = '' @@ -349,24 +360,31 @@

var gradientLength = maxX - minX + 1; elmnt.onmousedown = dragMouseDown; + elmnt.ontouchstart = dragMouseDown; function dragMouseDown(e) { removeTrashcan(event) e = e || window.event; - e.preventDefault(); + var isTouch = e.type.startsWith('touch'); + if (!isTouch) e.preventDefault(); // get the mouse cursor position at startup: - mousePos = e.clientX; + mousePos = isTouch ? e.touches[0].clientX : e.clientX; d.onmouseup = closeDragElement; + d.ontouchcancel = closeDragElement; + d.ontouchend = closeDragElement; // call a function whenever the cursor moves: d.onmousemove = elementDrag; + d.ontouchmove = elementDrag; } function elementDrag(e) { e = e || window.event; - e.preventDefault(); + var isTouch = e.type.startsWith('touch'); + if (!isTouch) e.preventDefault(); // calculate the new cursor position: - posNew = mousePos - e.clientX; - mousePos = e.clientX; + var clientX = isTouch ? e.touches[0].clientX : e.clientX; + posNew = mousePos - clientX; + mousePos = clientX; mousePosInGradient = mousePos - (minX + 1) truePos = Math.round((mousePosInGradient/gradientLength)*256); @@ -393,7 +411,10 @@

function closeDragElement() { /* stop moving when mouse button is released:*/ d.onmouseup = null; + d.ontouchcancel = null; + d.ontouchend = null; d.onmousemove = null; + d.ontouchmove = null; } } @@ -485,7 +506,7 @@

console.log('Error: ', e); console.log(' Status: ', this.status); //Show some error notification for some time setTimeout(()=>{ - //Remove it when time has pased + //Remove it when time has passed }, 1000); }); req.open("POST", "/upload"); @@ -500,8 +521,10 @@

if (hst.length > 0 ) { try { var arr = []; - const response = await fetch('http://'+hst+'/json/info'); - const json = await response.json(); + const responseInfo = await fetch('http://'+hst+'/json/info'); + const responsePalettes = await fetch('http://'+hst+'/json/palettes'); + const json = await responseInfo.json(); + paletteName = await responsePalettes.json(); cpalc = json.cpalcount; fetchPalettes(cpalc-1); } catch (error) { @@ -530,7 +553,7 @@

paletteArray.push({"palette":[0,70,70,70,255,70,70,70]}); } - //Get static palettes from localStorage and do some magic to reformat them into the same format as the pallete JSONs + //Get static palettes from localStorage and do some magic to reformat them into the same format as the palette JSONs //This code excludes any objects with "non valid integer colors", i.e. r, c1, c2, c3 and such //This code also fixes potentially broken palettes which doesn't end on 255 //The code finally also removes any representations of the custom palettes, since we read them from file @@ -540,6 +563,7 @@

alert("The cache of palettes are missig from your browser. You should probably return to the main page and let it load properly for the palettes cache to regenerate before returning here.","Missing cached palettes!") } else { for (const key in wledPalx.p) { + wledPalx.p[key].name = paletteName[key]; if (key > 245) { delete wledPalx.p[key]; continue; @@ -571,8 +595,11 @@

} const pArray = Object.entries(wledPalx.p).map(([key, value]) => ({ - [key]: value.flat() + [key]: value.flat(), + name: value.name })); + // Sort pArray by name + pArray.sort((a, b) => a.name.localeCompare(b.name)); paletteArray.push( ...pArray); } @@ -614,6 +641,9 @@

editSpan.id = `editSpan${i}`; editSpan.onclick = function() {loadForEdit(i)}; editSpan.setAttribute('title', `Copy slot ${i} palette to editor`); + if (paletteArray[i].name) { + editSpan.setAttribute('title', `Copy ${paletteArray[i].name} palette to editor`); + } editSpan.innerHTML = svgEdit; editSpan.classList.add("editSpan") diff --git a/wled00/data/index.css b/wled00/data/index.css index 640839d558..deb0cc64e7 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -352,7 +352,7 @@ button { #putil, #segutil, #segutil2 { min-height: 42px; - margin: 13px auto 0; + margin: 0 auto; } #segutil .segin { @@ -412,6 +412,13 @@ button { bottom: 0; } +/* WLEDMM */ +#effectGFX, #segGFX { + position: -webkit-sticky; + position: sticky; + top: 0; +} + #sliders .labels { padding-top: 3px; font-size: small; @@ -556,7 +563,7 @@ button { transform: translateY(100%); transition: transform 0.4s; padding: 8px; - font-size: 20px; + font-size: 18px; /* WLEDMM: smaller is better (was 20px)*/ overflow: auto; } @@ -606,12 +613,23 @@ button { width: 200px; } -#info table, #nodes table { +#info table { table-layout: fixed; width: 100%; } -#info td, #nodes td { +/*WLEDMM nodes table auto*/ +#nodes table { + table-layout: auto; + width: 100%; +} + +/*WLEDMM split info and nodes table font attributes*/ +#info td { + padding-bottom: 8px; +} +#nodes td { + font-size: 12px; /*WLEDMM small table cells (nodes popup) */ padding-bottom: 8px; } @@ -621,11 +639,17 @@ button { #info table .btn, #nodes table .btn { margin: 0; } -#info div, #nodes div { +#info div { max-width: 490px; margin: 0 auto; } +/*WLEDMM nodes div width must be bigger*/ +#nodes div { + max-width: 980px; + margin: 0 auto; +} + #info #imgw { margin: 8px auto; } @@ -641,6 +665,12 @@ button { color: #f00; } +#heartMM { + transition: color 0.9s; + font-size: 16px; + color: #0f0; +} + img { max-width: 100%; max-height: 100%; @@ -1017,13 +1047,15 @@ textarea { .segname .flr, .pname .flr { transform: rotate(0deg); - right: -6px; + right: 4px; } /* segment power wrapper */ .sbs { padding: 1px 0 1px 20px; display: var(--sgp); + width: 100%; + position: relative; } .pname { @@ -1305,6 +1337,11 @@ TD .checkmark, TD .radiomark { box-shadow: 0px 0px 10px 4px var(--c-1); } +.lstI .flr:hover { + background: var(--c-6); + border-radius: 100%; +} + #pcont .selected:not([class*="expanded"]) { bottom: 52px; top: 42px; diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 530b238567..57fca4cf8e 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -6,6 +6,7 @@ + WLED + @@ -64,7 +66,7 @@ - + @@ -87,99 +89,102 @@
-
-
-
- -
-
- Hue -
-
-
- -
-
- Saturation -
-
-
- -
+
+
+
+
+
+ +
+
+ Hue
- Value/Brightness -
-
-
- -
+
+
+ +
+
+ Saturation
- Kelvin/Temperature -
-
- -
+
- +
- Red channel + Value/Brightness
-
+
- + +
+
+ Kelvin/Temperature +
+
+ +
+
+ +
+
+ Red channel +
+
+
+ +
+
+ Green channel +
+
+
+ +
+
+ Blue channel +
+
+
+ +
+
- Green channel + White channel
-
+
+
- +
- Blue channel + White balance
-
-
- -
- -
+
+
+
+
+
+
+

+
+
+
+
+
R
- White channel -
-
- -
- -
+
+ + +
- White balance -
-
-
-
-
-
-
-

-
-
-
-
-
R
-
-
- - - -
-

-
- - - -
+

+
+ + + +
+

Color palette

@@ -206,6 +211,12 @@
+

Effect mode

@@ -315,9 +326,16 @@
+ +

Segments

Loading...
+
@@ -351,7 +369,7 @@
-
+

- Made with ❤︎ by Aircoookie and the WLED community + WLED made with ❤︎ by Aircoookie and the WLED community
+ WLED MM made with ❤︎ by Softhack007 & Ewowi and the WLED 2D & Audio Dev community
+ + + @@ -394,5 +421,6 @@

+ diff --git a/wled00/data/index.js b/wled00/data/index.js index ccdbab968e..a89958f575 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -26,17 +26,24 @@ var ws, cpick, ranges; var cfg = { theme:{base:"dark", bg:{url:""}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, comp :{colors:{picker: true, rgb: false, quick: true, hex: false}, - labels:true, pcmbot:false, pid:true, seglen:false, segpwr:false, segexp:false, css:true, hdays:false} + labels:true, pcmbot:false, pid:true, seglen:false, segpwr:false, segexp:true, + css:true, hdays:false, fxdef:true, fxdef2:false} //WLEDMM segexp true as default, fxdef2 added }; var hol = [ [0,11,24,4,"https://aircoookie.github.io/xmas.png"], // christmas [0,2,17,1,"https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day [2025,3,20,2,"https://aircoookie.github.io/easter.png"], - [2023,3,9,2,"https://aircoookie.github.io/easter.png"], [2024,2,31,2,"https://aircoookie.github.io/easter.png"], - [0,6,4,1,"https://initiate.alphacoders.com/download/wallpaper/516792/images/jpg/510921363292536"], // 4th of July - [0,0,1,1,"https://initiate.alphacoders.com/download/wallpaper/1198800/images/jpg/2522807481585600"] // new year + [0,6,4,1,"https://images.alphacoders.com/516/516792.jpg"], // 4th of July + [0,0,1,1,"https://images.alphacoders.com/119/1198800.jpg"] // new year ]; +var ctx = null; // WLEDMM +var ledmapNr = -1; //WLEDMM +var ledmapFileNames = []; //WLEDMM +let nodesData = []; //WLEDMM +let ibtglChecked = true; //WLEDMM +let sbtglChecked = true; //WLEDMM +let sbchkChecked = false; //WLEDMM function handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3000) requestJson();} function sCol(na, col) {d.documentElement.style.setProperty(na, col);} @@ -240,6 +247,7 @@ function onLoad() selectSlot(0); updateTablinks(0); + handleLocationHash(); pmtLS = localStorage.getItem('wledPmt'); // Load initial data @@ -272,7 +280,6 @@ function updateTablinks(tabI) { var tablinks = gEBCN("tablinks"); for (var i of tablinks) i.classList.remove('active'); - if (pcMode) return; tablinks[tabI].classList.add('active'); } @@ -283,6 +290,21 @@ function openTab(tabI, force = false) _C.classList.toggle('smooth', false); _C.style.setProperty('--i', iSlide); updateTablinks(tabI); + switch (tabI) { + case 0: window.location.hash = "Colors"; break; + case 1: window.location.hash = "Effects"; break; + case 2: window.location.hash = "Segments"; break; + case 3: window.location.hash = "Presets"; break; + } +} + +function handleLocationHash() { + switch (window.location.hash) { + case "#Colors": openTab(0); break; + case "#Effects": openTab(1); break; + case "#Segments": openTab(2); break; + case "#Presets": openTab(3); break; + } } var timeout; @@ -537,7 +559,7 @@ function populateQL() { var cn = ""; if (pQL.length > 0) { - pQL.sort((a,b) => (a[0]>b[0])); + pQL.sort((a,b) => (a[1]>b[1])); //WLEDMM do not sort on preset id but on ql name cn += `

Quick load

`; for (var key of (pQL||[])) { cn += ``; @@ -568,7 +590,8 @@ function populatePresets(fromls) cn += `
`; if (cfg.comp.pid) cn += `
${i}
`; - cn += `
${isPlaylist(i)?"":""}${pName(i)} + //WLEDMM: show ql if defined + cn += `
${isPlaylist(i)?"":""}${(pJson[i].ql?pJson[i].ql+' ':'') + pName(i)}
@@ -643,8 +666,11 @@ function parseInfo(i) { function populateInfo(i) { var cn=""; - var heap = i.freeheap/1024; - heap = heap.toFixed(1); + var heap = i.freeheap/1000; + var heap = Math.round(i.freeheap/100)/10; // WLEDMM bugfix + var theap = (i.totalheap>0)?i.totalheap/1000:-1; //WLEDMM - total heap is not available on 8266 + var flashsize = i.getflash/1000; //WLEDMM and Athom + flashsize = flashsize.toFixed(1); //WLEDMM and Athom var pwr = i.leds.pwr; var pwru = "Not calculated"; if (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + " A";} @@ -658,25 +684,50 @@ function populateInfo(i) urows += inforow(k,val); } } - var vcn = "Kuuhaku"; - if (i.ver.startsWith("0.14.")) vcn = "Hoshi"; + var vcn = "Small Step"; // WLED-MM 14.5.0, release Dec 2024 +// if (i.ver.startsWith("0.14.")) vcn = "Hoshi"; // if (i.ver.includes("-bl")) vcn = "Supāku"; if (i.cn) vcn = i.cn; - cn += `v${i.ver} "${vcn}"

+ //WLEDMM: add total heap and total PSRAM, and build number, add bin name + //if (i.ver.includes("0.14.0")) vcn = "Lupo"; // check for MM versioning scheme + //if (i.ver.includes("0.14.0-b15")) vcn = "Sitting Ducks"; // late easter egg + //if (i.ver.includes("0.14.0-b2")) vcn = "This is the way"; // recently watched The Mandalorian? I have spoken ;-) + //if (i.ver.includes("0.14.0-b15.22")) vcn = "Lupo"; + //if (i.ver.includes("0.14.1-b")) vcn = "Fried Chicken"; // final line of "One Vision" by Queen + if (i.ver.includes("0.14.3-b")) vcn = "Fried Chicken"; + if (i.ver.includes("14.5.")) vcn = "Small Step"; + + cn += `v${i.ver}  "${vcn}"

(WLEDMM ${i.rel}.bin)

build ${i.vid}

${urows} -${urows===""?'':''} -${i.opt&0x100?inforow("Debug",""):''} +${urows===""?'':''} +${i.opt&0x100?inforow("Net Print ☾",""):''} +${i.serialOnline?inforow(i.serialOnline,"TX="+i.sTX,"; RX="+i.sRX):""} +${i.opt&0x100?'':''} ${inforow("Build",i.vid)} -${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} -${inforow("Uptime",getRuntimeStr(i.uptime))} -${inforow("Free heap",heap," kB")} -${i.psram?inforow("Free PSRAM",(i.psram/1024).toFixed(1)," kB"):""} ${inforow("Estimated current",pwru)} ${inforow("Average FPS",i.leds.fps)} +${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} ${inforow("MAC address",i.mac)} -${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} +${inforow("Uptime",getRuntimeStr(i.uptime))} + + +${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB, " +Math.round(i.fs.u*100/i.fs.t) + "%")} +${theap>0?inforow("Heap ☾",((i.totalheap-i.freeheap)/1000).toFixed(0)+"/"+theap.toFixed(0)+" kB",", "+Math.round((i.totalheap-i.freeheap)/(10*theap))+"%"):inforow("Free heap",heap," kB")} +${i.minfreeheap?inforow("Max used heap ☾",((i.totalheap-i.minfreeheap)/1000).toFixed(0)+" kB",", "+Math.round((i.totalheap-i.minfreeheap)/(10*theap))+"%"):""} +${i.psram?inforow("PSRAM ☾",((i.tpram-i.psram)/1024).toFixed(0)+"/"+(i.tpram/1024).toFixed(0)+" kB",", "+((i.tpram-i.psram)*100.0/i.tpram).toFixed(1)+"%"):""} +${i.psusedram?inforow("Max used PSRAM ☾",((i.tpram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpram-i.psusedram)*100.0/i.tpram).toFixed(1)+"%"):""} +${i.freestack?inforow("Free stack ☾",(i.freestack/1000).toFixed(3)," kB"):""} + +${i.tpram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")} +${i.e32flash?inforow("Flash mode "+i.e32flashmode+i.e32flashtext + " ☾",i.e32flash+" MB, "+i.e32flashspeed," Mhz"):""} +${i.e32model?inforow(i.e32model + " ☾",i.e32cores +" core(s),"," "+i.e32speed+" Mhz"):""} ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} + +${i.e32code?inforow("Last ESP Restart ☾",i.e32code+" "+i.e32text):""} +${i.e32core0code?inforow("Core0 rst reason ☾",i.e32core0code, " "+i.e32core0text):""} +${i.e32core1code?inforow("Core1 rst reason ☾",i.e32core1code, " "+i.e32core1text):""} +






`; gId('kv').innerHTML = cn; // update all sliders in Info @@ -692,6 +743,9 @@ function populateSegments(s) let li = lastinfo; segCount = 0; lowestUnused = 0; lSeg = 0; + ledmapNr = s.ledmap; //WLEDMM + ledmapFileNames = []; //WLEDMM + for (var inst of (s.seg||[])) { segCount++; @@ -702,6 +756,8 @@ function populateSegments(s) let sg = gId(`seg${i}`); let exp = sg ? (sg.classList.contains('expanded') || (i===0 && cfg.comp.segexp)) : false; + ledmapFileNames.push((inst.n?inst.n:"default") + ".json"); //WLEDMM + // segment set icon color let cG = "var(--c-b)"; switch (inst.set) { @@ -729,13 +785,18 @@ function populateSegments(s) rvYck = ``; miYck = ``; } + // WLEDMM: jMap let map2D = `
Expand 1D FX
`+ `
`+ + ``+ + ``+ + ``+ + ``+ + `
`+ `
`; let sndSim = `
Sound sim
`+ `
`+ `
`; + //WLEDMM ARTIFX + let fxName = eJson.find((o)=>{return o.id==selectedFx}).name; + let cusEff = `
`; cn += `
`+ `