Compare commits
	
		
			329 Commits
		
	
	
		
			v0.12.0-b5
			...
			v0.13.0-b5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 4af1f62aab | ||
|   | bc403440ba | ||
|   | 38d8dfe5ab | ||
|   | 6df64d0d31 | ||
|   | 83753a5f81 | ||
|   | 3161f5fa47 | ||
|   | 5784092c1b | ||
|   | d6ad089c60 | ||
|   | 446b4b084c | ||
|   | adeb9bccb1 | ||
|   | b44ffffed8 | ||
|   | 2bdaf53ecf | ||
|   | 46e7db6d94 | ||
|   | 7e1920dc4b | ||
|   | a93f05c047 | ||
|   | 00238247cd | ||
|   | b33e28835d | ||
|   | 0acca2e313 | ||
|   | 00f1b483eb | ||
|   | c3d48acb4c | ||
|   | 392bda7d8c | ||
|   | 10cfcdab8c | ||
|   | 3f71d3b250 | ||
|   | 1b50fbab22 | ||
|   | 303fc65a6a | ||
|   | 445b6ee13f | ||
|   | 8afaac1e30 | ||
|   | 0327f9428e | ||
|   | a5de66bbd5 | ||
|   | d47157eec3 | ||
|   | f4b47ed399 | ||
|   | 8b2145bd88 | ||
|   | de454e8b78 | ||
|   | 6cd770b4c7 | ||
|   | 355525c248 | ||
|   | 47d4e7381f | ||
|   | 5dac6690d7 | ||
|   | b89f7180db | ||
|   | 2ebb837a15 | ||
|   | 849aa64678 | ||
|   | cbb12e1b7c | ||
|   | cc87ba4962 | ||
|   | fb2e556726 | ||
|   | 3f0eb0a046 | ||
|   | 7d6d9eddc4 | ||
|   | cf87da0ef3 | ||
|   | 0775acedc0 | ||
|   | 8f1cee2e61 | ||
|   | caa9cc32d7 | ||
|   | b750f827c5 | ||
|   | 5d147163e5 | ||
|   | 75fe1a19eb | ||
|   | 5c9405fffc | ||
|   | 6457314794 | ||
|   | 84f4e3eedc | ||
|   | b003ed3f03 | ||
|   | 330da137db | ||
|   | 9e5d45d0de | ||
|   | b5c15d97fa | ||
|   | 6ddcba8917 | ||
|   | 91598cbbbf | ||
|   | 772c80aa85 | ||
|   | a28345d858 | ||
|   | 05b532b9eb | ||
|   | 0b0d18f182 | ||
|   | c1b0877956 | ||
|   | 46b66c76ef | ||
|   | d00b4335b5 | ||
|   | 7a129e6de1 | ||
|   | 17c20276a9 | ||
|   | dc9dedf220 | ||
|   | 3ac772badc | ||
|   | 22fc58d93b | ||
|   | ccd3152b24 | ||
|   | 7d929dcde6 | ||
|   | 3a874bc8c7 | ||
|   | 8453cd82e9 | ||
|   | f62e56b7ec | ||
|   | 2ac90bbb96 | ||
|   | f85f2d5d22 | ||
|   | a94269ceb9 | ||
|   | 476ac263fb | ||
|   | 51a4f61a8f | ||
|   | 267f5159a3 | ||
|   | 96422de031 | ||
|   | f2043dc181 | ||
|   | e416ec9279 | ||
|   | 5eb4ffb1cc | ||
|   | 8fae964ee8 | ||
|   | baf49b88f4 | ||
|   | 3577da05ac | ||
|   | b8e8028eb9 | ||
|   | a899666e68 | ||
|   | 10a52f8cf9 | ||
|   | 72d04a0120 | ||
|   | 6dbed30008 | ||
|   | c5eac298e6 | ||
|   | bc18eda336 | ||
|   | 3cefb14297 | ||
|   | 4bc401278e | ||
|   | d7e3765efe | ||
|   | 3d51d1e345 | ||
|   | bd23942893 | ||
|   | c8610b8ad2 | ||
|   | d0440122b9 | ||
|   | 8d4636bbab | ||
|   | f59c6e7a7c | ||
|   | c24ab1b21d | ||
|   | 6a01658355 | ||
|   | f1e2439e66 | ||
|   | 4d89ed701d | ||
|   | e80594d61d | ||
|   | 83c6f72eb0 | ||
|   | e26299b998 | ||
|   | a839809eb8 | ||
|   | 88ceba59cf | ||
|   | f368bbec32 | ||
|   | 021c4ba68a | ||
|   | 54f4658dae | ||
|   | dbc67e077d | ||
|   | e968917dbc | ||
|   | d8240bb683 | ||
|   | b481c13829 | ||
|   | 77c0ba990d | ||
|   | 1d4487b6cd | ||
|   | ff8145b745 | ||
|   | 530e8b39e5 | ||
|   | 50aeee288b | ||
|   | 72e001b0d5 | ||
|   | f04c9d101e | ||
|   | 2ecc53ba56 | ||
|   | 3eb1fe0eb2 | ||
|   | aec998acc1 | ||
|   | 91e758f66f | ||
|   | 441416b241 | ||
|   | e541d8697e | ||
|   | 4b817208aa | ||
|   | 7fea0c3244 | ||
|   | bd13336256 | ||
|   | 815940913b | ||
|   | f7191c0381 | ||
|   | 07d11c845c | ||
|   | 2e9bd477d9 | ||
|   | b058fb8db4 | ||
|   | 9f0f6181a1 | ||
|   | f702e1a80d | ||
|   | e1527fcbb9 | ||
|   | 9ba7e5d567 | ||
|   | 02b6d53544 | ||
|   | 123bd0bb92 | ||
|   | 6a8ed1192f | ||
|   | 0862859f93 | ||
|   | 3ad336a1eb | ||
|   | a17f83cedd | ||
|   | 2c6850f6e4 | ||
|   | 5da47636cf | ||
|   | e04b965659 | ||
|   | 17d2fb80f2 | ||
|   | 14b7ec2a80 | ||
|   | f27b31b581 | ||
|   | 8c44147a45 | ||
|   | ec05215a5e | ||
|   | 5903e8256f | ||
|   | c879351063 | ||
|   | 1bb7e36a65 | ||
|   | 793a01f7ca | ||
|   | 40c8fdbf64 | ||
|   | dc01c907f1 | ||
|   | 801df94446 | ||
|   | 0197d89976 | ||
|   | e16a67242e | ||
|   | 4c678a5010 | ||
|   | 3754088a44 | ||
|   | c4f084a991 | ||
|   | 4c73df4ba6 | ||
|   | 4aa53aa5a5 | ||
|   | 7483d3b229 | ||
|   | 8b6cc708e7 | ||
|   | 200960899e | ||
|   | 599a456c81 | ||
|   | 4b46502d22 | ||
|   | 7233c55428 | ||
|   | a58c5cce78 | ||
|   | 0b23bf65b3 | ||
|   | bc0a3f8a47 | ||
|   | 9b2a0102be | ||
|   | 04b4ef6d85 | ||
|   | 9e8aadb750 | ||
|   | 0ae0f40628 | ||
|   | af9aa7d201 | ||
|   | 4cd3a614de | ||
|   | 1e5420e6a7 | ||
|   | 660de0b4e5 | ||
|   | b73aaecd22 | ||
|   | c831d62bc3 | ||
|   | 1539e703e9 | ||
|   | f43bf03768 | ||
|   | 495f7f190f | ||
|   | 16216b9eb9 | ||
|   | dfdb22f584 | ||
|   | 0b264176bc | ||
|   | bde70a27f0 | ||
|   | 7d2f5f0799 | ||
|   | 7610ab7a8d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 51db653b1a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dc4e4395a9 | ||
|   | 623694ab73 | ||
|   | 374457df70 | ||
|   | 7885dddef2 | ||
|   | 73d6cc1e54 | ||
|   | 8fdf84068d | ||
|   | 131625bb53 | ||
|   | 29c9e5cb17 | ||
|   | 52b60fd6a6 | ||
|   | d6337f7500 | ||
|   | 625e04d208 | ||
|   | 6da657d3e2 | ||
|   | 344c9e9238 | ||
|   | 89b2b066ef | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 894e084c7f | ||
|   | 2ba064b2a5 | ||
|   | dfe065ef82 | ||
|   | 7bd4b78470 | ||
|   | d7991a247d | ||
|   | 2178fd6ee9 | ||
|   | 7019ddb165 | ||
|   | 9f13763637 | ||
|   | beeba27f46 | ||
|   | 315d4f225a | ||
|   | 85489458d8 | ||
|   | bfc7f56c4d | ||
|   | 7685f9b73d | ||
|   | 664fad96fa | ||
|   | 669a610e36 | ||
|   | 7e0d9cb48c | ||
|   | 7cbc9d21b5 | ||
|   | 55b26751ae | ||
|   | c2892d7887 | ||
|   | 6c8bf090fe | ||
|   | 13bc378069 | ||
|   | 8431d0bd5c | ||
|   | 852f758be3 | ||
|   | b455f432d5 | ||
|   | 306cea60a1 | ||
|   | ba2e07c4b9 | ||
|   | 9b796531b2 | ||
|   | 08d7a1c123 | ||
|   | 1f70a735c7 | ||
|   | 6713fcfeb1 | ||
|   | c3107d213a | ||
|   | adf5c8c278 | ||
|   | 5f86a8a15b | ||
|   | 042c756be8 | ||
|   | 2d586406da | ||
|   | 371c4e0051 | ||
|   | 69099fcdd7 | ||
|   | 57e50d0c33 | ||
|   | 1617658bfe | ||
|   | 4bcfff780a | ||
|   | 12f9ad8f7f | ||
|   | 6f843fcb27 | ||
|   | e0f17e1778 | ||
|   | bfb27c49a2 | ||
|   | cb7b7f1dca | ||
|   | 5ca8bc3f2a | ||
|   | 1ccc8eec0a | ||
|   | 9c5afda83a | ||
|   | d94d3d4bc5 | ||
|   | 119826cb9b | ||
|   | 6ab95ed4ef | ||
|   | 4f1eb64ac6 | ||
|   | 3f8dc76f84 | ||
|   | f60579fd21 | ||
|   | 136a00a301 | ||
|   | fa075f6800 | ||
|   | 277f395595 | ||
|   | e2061464a5 | ||
|   | fcf5cd4655 | ||
|   | 3816f0b68b | ||
|   | 1a2543ddde | ||
|   | 7c9db7edeb | ||
|   | 8b759bc5d9 | ||
|   | 9a0aac4745 | ||
|   | ced0cc1bac | ||
|   | 3c49f22266 | ||
|   | 13ae99edec | ||
|   | 0f82730a78 | ||
|   | ff083daf31 | ||
|   | 7f6a554e1b | ||
|   | eb99271120 | ||
|   | 13e5c695c3 | ||
|   | 12de47c923 | ||
|   | afde7940d8 | ||
|   | 01dd41bdbf | ||
|   | f3b84f1365 | ||
|   | 5751d5c1b0 | ||
|   | 3d2336aac1 | ||
|   | afe5f19464 | ||
|   | 4091a3c238 | ||
|   | f411e07fb4 | ||
|   | 9bfe27dd5e | ||
|   | c4201d9a2a | ||
|   | 58e9817a6d | ||
|   | 48d5584491 | ||
|   | 5786f1d057 | ||
|   | 87c6f3c757 | ||
|   | 0e99c948d6 | ||
|   | 0f5e0f640b | ||
|   | 3d2c6388de | ||
|   | ad8e614ae8 | ||
|   | 48c0360877 | ||
|   | d230be3e1c | ||
|   | daa77d40a3 | ||
|   | 6ae743684f | ||
|   | 789c00dde1 | ||
|   | 90da471084 | ||
|   | 2d55056015 | ||
|   | ffab9bb893 | ||
|   | 1192d04391 | ||
|   | f18dced2f3 | ||
|   | ecdc0a3800 | ||
|   | 46d66dded8 | ||
|   | a69dcfc49d | ||
|   | 601005f837 | ||
|   | de6f32e486 | ||
|   | fbe866198d | ||
|   | d603a8a9be | ||
|   | 8f92077454 | ||
|   | cb0452964e | 
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| github: [Aircoookie] | ||||
| custom: ['https://paypal.me/Aircoookie'] | ||||
							
								
								
									
										27
									
								
								.github/ISSUE_TEMPLATE/bug.md
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,27 +0,0 @@ | ||||
| --- | ||||
| name: Bug | ||||
| about: Noticed an issue with your lights? | ||||
| title: '' | ||||
| labels: bug | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Describe the bug** | ||||
| A clear and concise description of what the bug is. Please quickly search existing issues first! | ||||
|  | ||||
| **To Reproduce** | ||||
| Steps to reproduce the behavior, if consistently possible | ||||
|  | ||||
| **Expected behavior** | ||||
| A clear and concise description of what you expected to happen. | ||||
|  | ||||
| **WLED version** | ||||
|  - Board: [e.g. Wemos D1, ESP32 dev] | ||||
|  - Version [e.g. 0.10.0, dev200603] | ||||
|  - Format [e.g. Binary, self-compiled] | ||||
|  | ||||
| **Additional context** | ||||
| Anything else you'd like to say about the problem? | ||||
|  | ||||
| Thank you for your help! | ||||
							
								
								
									
										83
									
								
								.github/ISSUE_TEMPLATE/bug.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,83 @@ | ||||
| name: Bug Report | ||||
| description: File a bug report | ||||
| labels: ["bug"] | ||||
| body: | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         Please quickly search existing issues first before submitting a bug. | ||||
|   - type: textarea | ||||
|     id: what-happened | ||||
|     attributes: | ||||
|       label: What happened? | ||||
|       description: A clear and concise description of what the bug is. | ||||
|       placeholder: Tell us what the problem is. | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: how-to-reproduce | ||||
|     attributes: | ||||
|       label: To Reproduce Bug | ||||
|       description: Steps to reproduce the behavior, if consistently possible. | ||||
|       placeholder: Tell us how to make the bug appear. | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: expected-behavior | ||||
|     attributes: | ||||
|       label: Expected Behavior | ||||
|       description: A clear and concise description of what you expected to happen. | ||||
|       placeholder: Tell us what you expected to happen. | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: dropdown | ||||
|     id: install_format | ||||
|     attributes: | ||||
|       label: Install Method | ||||
|       description: How did you install WLED? | ||||
|       options: | ||||
|         - Binary from WLED.me | ||||
|         - Self-Compiled | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: input | ||||
|     id: version | ||||
|     attributes: | ||||
|       label: What version of 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.0-b4 (build 2110110)" | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: dropdown | ||||
|     id: Board | ||||
|     attributes: | ||||
|       label: Which microcontroller/board are you seeing the problem on? | ||||
|       multiple: true | ||||
|       options: | ||||
|         - ESP8266 | ||||
|         - ESP32 | ||||
|         - Other | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: logs | ||||
|     attributes: | ||||
|       label: Relevant log/trace output | ||||
|       description: Please copy and paste any relevant log output if you have it. This will be automatically formatted into code, so no need for backticks. | ||||
|       render: shell | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: Anything else? | ||||
|       description: | | ||||
|         Links? References? Anything that will give us more context about the issue you are encountering! | ||||
|         Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. | ||||
|     validations: | ||||
|       required: false | ||||
|   - type: checkboxes | ||||
|     id: terms | ||||
|     attributes: | ||||
|       label: Code of Conduct | ||||
|       description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Aircoookie/WLED/blob/master/CODE_OF_CONDUCT.md) | ||||
|       options: | ||||
|         - label: I agree to follow this project's Code of Conduct | ||||
|           required: true | ||||
							
								
								
									
										67
									
								
								.github/workflows/wled-ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -3,10 +3,37 @@ name: PlatformIO CI | ||||
| on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|  | ||||
|   get_default_envs: | ||||
|     name: Gather Environments | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Cache pip | ||||
|       uses: actions/cache@v2 | ||||
|       with: | ||||
|         path: ~/.cache/pip | ||||
|         key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | ||||
|         restore-keys: | | ||||
|           ${{ runner.os }}-pip- | ||||
|     - uses: actions/setup-python@v2 | ||||
|     - name: Install PlatformIO | ||||
|       run: pip install -r requirements.txt | ||||
|     - name: Get default environments | ||||
|       id: envs | ||||
|       run: | | ||||
|         echo "::set-output name=environments::$(pio project config --json-output | jq -cr '.[0][1][0][1]')" | ||||
|     outputs: | ||||
|       environments: ${{ steps.envs.outputs.environments }} | ||||
|  | ||||
|  | ||||
|   build: | ||||
|     name: Build Enviornments | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: get_default_envs | ||||
|     strategy: | ||||
|       matrix: | ||||
|         environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Cache pip | ||||
| @@ -24,8 +51,36 @@ jobs: | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|     - name: Install PlatformIO | ||||
|       run: | | ||||
|         python -m pip install --upgrade pip | ||||
|         pip install --upgrade platformio | ||||
|     - name: Run PlatformIO | ||||
|       run: pio run | ||||
|       run: pip install -r requirements.txt | ||||
|     - name: Build firmware | ||||
|       env: | ||||
|         WLED_RELEASE: True | ||||
|       run: pio run -e ${{ matrix.environment }} | ||||
|     - uses: actions/upload-artifact@v2 | ||||
|       with: | ||||
|         name: firmware-${{ matrix.environment }} | ||||
|         path: | | ||||
|           build_output/firmware/*.bin | ||||
|           build_output/firmware/*.gz | ||||
|     - uses: actions/upload-artifact@v2 | ||||
|       if: startsWith(github.ref, 'refs/tags/') | ||||
|       with: | ||||
|         name: firmware-release | ||||
|         path: build_output/release/*.bin | ||||
|   release: | ||||
|     name: Create Release | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [get_default_envs, build] | ||||
|     if: startsWith(github.ref, 'refs/tags/') | ||||
|     steps: | ||||
|     - uses: actions/download-artifact@v2 | ||||
|       with: | ||||
|         name: firmware-release | ||||
|     - name: Create draft release | ||||
|       uses: softprops/action-gh-release@v1 | ||||
|       with: | ||||
|         draft: True | ||||
|         files: | | ||||
|           *.bin | ||||
|       env: | ||||
|         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -14,3 +14,4 @@ | ||||
| .clang-format | ||||
| node_modules | ||||
| .idea | ||||
| .direnv | ||||
|   | ||||
							
								
								
									
										310
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,7 +1,317 @@ | ||||
| ## WLED changelog | ||||
|  | ||||
| ### Builds after release 0.12.0 | ||||
|  | ||||
| #### Build 2111160 | ||||
|  | ||||
| -   Version bump to 0.13.0-b5 "Toki" | ||||
| -   Improv Serial support (PR #2334) | ||||
| -   Button improvements (PR #2284) | ||||
| -   Added two time zones (PR #2264, 2311) | ||||
| -   JSON in/decrementing support for brightness and presets | ||||
| -   Fixed no gamma correction for JSON individual LED control | ||||
| -   Preset cycle bugfix | ||||
| -   Removed ledCount | ||||
| -   LED settings buffer bugfix | ||||
| -   Network pin conflict bugfix | ||||
| -   Changed default ESP32 partition layout to 4M, 1M FS | ||||
|  | ||||
| #### Build 2110110 | ||||
|  | ||||
| -   Version bump to 0.13.0-b4 "Toki" | ||||
| -   Added option for bus refresh if off (PR #2259) | ||||
| -   New auto segment logic | ||||
| -   Fixed current calculations for virtual or non-linear configs (PR #2262) | ||||
|  | ||||
| #### Build 2110060 | ||||
|  | ||||
| -   Added virtual network DDP busses (PR #2245) | ||||
| -   Allow playlist as end preset in playlist | ||||
| -   Improved bus start field UX | ||||
| -   Pin reservations improvements (PR #2214) | ||||
|  | ||||
| #### Build 2109220 | ||||
|  | ||||
| -   Version bump to 0.13.0-b3 "Toki" | ||||
| -   Added segment names (PR #2184) | ||||
| -   Improved Police and other effects (PR #2184) | ||||
| -   Reverted PR #1902 (Live color correction - will be implemented as usermod) (PR #2175) | ||||
| -   Added transitions for segment on/off | ||||
| -   Improved number of sparks/stars in Fireworks effect with low number of segments | ||||
| -   Fixed segment name edit pencil disappearing with request | ||||
| -   Fixed color transition active even if the segment is off | ||||
| -   Disallowed file upload with OTA lock active | ||||
| -   Fixed analog invert option missing (PR #2219) | ||||
|  | ||||
| #### Build 2109100 | ||||
|  | ||||
| -   Added an auto create segments per bus setting | ||||
| -   Added 15 new palettes from SR branch (PR #2134) | ||||
| -   Fixed segment runtime not reset on FX change via HTTP API | ||||
| -   Changed AsyncTCP dependency to pbolduc fork v1.2.0 | ||||
|  | ||||
| #### Build 2108250 | ||||
|  | ||||
| -   Added Sync groups (PR #2150) | ||||
| -   Added JSON API over Serial support | ||||
| -   Live color correction (PR #1902) | ||||
|  | ||||
| #### Build 2108180 | ||||
|  | ||||
| -   Fixed JSON IR remote not working with codes greater than 0xFFFFFF (fixes #2135) | ||||
| -   Fixed transition 0 edge case | ||||
|  | ||||
| #### Build 2108170 | ||||
|  | ||||
| -   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 | ||||
|  | ||||
| #### Build 2108050 | ||||
|  | ||||
| -   Fixed undesirable color transition from Orange to boot preset color on first boot | ||||
| -   Removed misleading Delete button on new playlist with one entry | ||||
| -   Updated NeoPixelBus to 2.6.7 and AsyncTCP to 1.1.1 | ||||
|  | ||||
| #### Build 2107230 | ||||
|  | ||||
| -   Added skinning (extra custom CSS) (PR #2084) | ||||
| -   Added presets/config backup/restore (PR #2084) | ||||
| -   Added option for using length instead of Stop LED in UI (PR #2048) | ||||
| -   Added custom `holidays.json` holiday list (PR #2048) | ||||
|  | ||||
| #### Build 2107100 | ||||
|  | ||||
| -   Version bump to 0.13.0-b2 "Toki" | ||||
| -   Accept hex color strings in individual LED API | ||||
| -   Fixed transition property not applying unless power/bri/color changed next | ||||
| -   Moved transition field below segments (temporarily) | ||||
| -   Reduced unneeded websockets pushes | ||||
|  | ||||
| #### Build 2107091 | ||||
|  | ||||
| -   Fixed presets using wrong call mode (e.g. causing buttons to send UDP under direct change type) | ||||
| -   Increased hue buffer | ||||
| -   Renamed `NOTIFIER_CALL_MODE_` to `CALL_MODE_` | ||||
|  | ||||
| #### Build 2107090 | ||||
|  | ||||
| -   Busses extend total configured LEDs if required | ||||
| -   Fixed extra button pins defaulting to 0 on first boot | ||||
|  | ||||
| #### Build 2107080 | ||||
|  | ||||
| -   Made Peek use the main websocket connection instead of opening a second one | ||||
| -   Temperature usermod fix (from @blazoncek's dev branch) | ||||
|  | ||||
| #### Build 2107070 | ||||
|  | ||||
| -   More robust initial resource loading in UI | ||||
| -   Added `getJsonValue()` for usermod config parsing (PR #2061) | ||||
| -   Fixed preset saving over websocket | ||||
| -   Alpha ESP32 S2 support (filesystem does not work) (PR #2067) | ||||
|  | ||||
| #### Build 2107042 | ||||
|  | ||||
| -   Updated ArduinoJson to 6.18.1 | ||||
| -   Improved Twinkleup effect | ||||
| -   Fixed preset immediately deselecting when set via HTTP API `PL=` | ||||
|  | ||||
| #### Build 2107041 | ||||
|  | ||||
| -   Restored support for "PL=~" mistakenly removed in 2106300 | ||||
| -   JSON IR improvements | ||||
|  | ||||
| #### Build 2107040 | ||||
|  | ||||
| -   Playlist entries are now more compact | ||||
| -   Added the possibility to enter negative numbers for segment offset | ||||
|  | ||||
| #### Build 2107021 | ||||
|  | ||||
| -   Added WebSockets support to UI | ||||
|  | ||||
| #### Build 2107020 | ||||
|  | ||||
| -   Send websockets on every state change | ||||
| -   Improved Aurora effect | ||||
|  | ||||
| #### Build 2107011 | ||||
|  | ||||
| -   Added MQTT button feedback option (PR #2011) | ||||
|  | ||||
| #### Build 2107010 | ||||
|  | ||||
| -   Added JSON IR codes (PR #1941) | ||||
| -   Adjusted the width of WiFi and LED settings input fields | ||||
| -   Fixed a minor visual issue with slider trail not reaching thumb on low values | ||||
|  | ||||
| #### Build 2106302 | ||||
|  | ||||
| -   Fixed settings page broken by using "%" in input fields | ||||
|  | ||||
| #### Build 2106301 | ||||
|  | ||||
| -   Fixed a problem with disabled buttons reverting to pin 0 causing conflict | ||||
|  | ||||
| #### Build 2106300 | ||||
|  | ||||
| -   Version bump to 0.13.0-b0 "Toki" | ||||
| -   BREAKING: Removed preset cycle (use playlists) | ||||
| -   BREAKING: Removed `nl.fade`, `leds.pin` and `ccnf` from JSON API | ||||
| -   Added playlist editor UI | ||||
| -   Reordered segment UI and added offset field | ||||
| -   Raised maximum MQTT password length to 64 (closes #1373) | ||||
|  | ||||
| #### Build 2106290 | ||||
|  | ||||
| -   Added Offset to segments, allows shifting the LED considered first within a segment | ||||
| -   Added `of` property to seg object in JSON API to set offset | ||||
| -   Usermod settings improvements (PR #2043, PR #2045) | ||||
|  | ||||
| #### Build 2106250 | ||||
|  | ||||
| -   Fixed preset only disabling on second effect/color change | ||||
|  | ||||
| #### Build 2106241 | ||||
|  | ||||
| -   BREAKING: Added ability for usermods to force a config save if config incomplete. `readFromConfig()` needs to return a `bool` to indicate if the config is complete | ||||
| -   Updated usermods implementing `readFromConfig()` | ||||
| -   Auto-create segments based on configured busses | ||||
|  | ||||
| #### Build 2106200 | ||||
|  | ||||
| -   Added 2 Ethernet boards and split Ethernet configs into separate file | ||||
|  | ||||
| #### Build 2106180 | ||||
|  | ||||
| -   Fixed DOS on Chrome tab restore causing reboot | ||||
|  | ||||
| #### Build 2106170 | ||||
|  | ||||
| -   Optimized JSON buffer usage (pre-serialized color arrays) | ||||
|  | ||||
| #### Build 2106140 | ||||
|  | ||||
| -   Updated main logo | ||||
| -   Reduced flash usage by 0.8kB by using 8-bit instead of 32-bit PNGs for welcome and 404 pages | ||||
| -   Added a check to stop Alexa reporting an error if state set by macro differs from the expected state | ||||
|  | ||||
| #### Build 2106100 | ||||
|  | ||||
| -   Added support for multiple buttons with various types (PR #1977) | ||||
| -   Fixed infinite playlists (PR #2020) | ||||
| -   Added `r` to playlist object, allows for shuffle regardless of the `repeat` value | ||||
| -   Improved accuracy of NTP time sync | ||||
| -   Added possibility for WLED UDP sync to sync system time | ||||
| -   Improved UDP sync accuracy, if both sender and receiver are NTP synced | ||||
| -   Fixed a cache issue with restored tabs | ||||
| -   Cache CORS request | ||||
| -   Disable WiFi sleep by default on ESP32 | ||||
|  | ||||
| #### Build 2105230 | ||||
|  | ||||
| -   No longer retain MQTT `/v` topic to alleviate storage loads on MQTT broker | ||||
| -   Fixed Sunrise calculation (atan_t approx. used outside of value range) | ||||
|  | ||||
| #### Build 2105200 | ||||
|  | ||||
| -   Fixed WS281x output on ESP32 | ||||
| -   Fixed potential out-of-bounds write in MQTT | ||||
| -   Fixed IR pin not changeable if IR disabled | ||||
| -   Fixed XML API <wv> containing -1 on Manual only RGBW mode (see #888, #1783) | ||||
|  | ||||
| #### Build 2105171 | ||||
|  | ||||
| -   Always copy MQTT payloads to prevent non-0-terminated strings | ||||
| -   Updated ArduinoJson to 6.18.0 | ||||
| -   Added experimental support for `{"on":"t"}` to toggle on/off state via JSON | ||||
|  | ||||
| #### Build 2105120 | ||||
|  | ||||
| -   Fixed possibility of non-0-terminated MQTT payloads | ||||
| -   Fixed two warnings regarding integer comparison | ||||
|  | ||||
| #### Build 2105112 | ||||
|  | ||||
| -   Usermod settings page no usermods message | ||||
| -   Lowered min speed for Drip effect | ||||
|  | ||||
| #### Build 2105111 | ||||
|  | ||||
| -   Fixed various Codacy code style and logic issues | ||||
|  | ||||
| #### Build 2105110 | ||||
|  | ||||
| -   Added Usermod settings page and configurable usermods (PR #1951) | ||||
| -   Added experimental `/json/cfg` endpoint for changing settings from JSON (see #1944, not part of official API) | ||||
|  | ||||
| #### Build 2105070 | ||||
|  | ||||
| -   Fixed not turning on after pressing "Off" on IR remote twice (#1950) | ||||
| -   Fixed OTA update file selection from Android app (TODO: file type verification in JS, since android can't deal with accept='.bin' attribute) | ||||
|  | ||||
| #### Build 2104220 | ||||
|  | ||||
| -   Version bump to 0.12.1-b1 "Hikari" | ||||
| -   Release and build script improvements (PR #1844) | ||||
|  | ||||
| #### Build 2104211 | ||||
|  | ||||
| -   Replace default TV simulator effect with the version that saves 18k of flash and appears visually identical | ||||
|  | ||||
| #### Build 2104210 | ||||
|  | ||||
| -   Added `tb` to JSON state, allowing setting the timebase (set tb=0 to start e.g. wipe effect from the beginning). Receive only. | ||||
| -   Slightly raised Solid mode refresh rate to work with LEDs (TM1814) that require refresh rates of at least 2fps | ||||
| -   Added sunrise and sunset calculation to the backup JSON time source | ||||
|  | ||||
| #### Build 2104151 | ||||
|  | ||||
| -   `NUM_STRIPS` no longer required with compile-time strip defaults | ||||
| -   Further optimizations in wled_math.h | ||||
|  | ||||
| #### Build 2104150 | ||||
|  | ||||
| -   Added ability to add multiple busses as compile time defaults using the esp32_multistrip usermod define syntax | ||||
|  | ||||
| #### Build 2104141 | ||||
|  | ||||
| -   Reduced memory usage by 540b by switching to a different trigonometric approximation | ||||
|  | ||||
| #### Build 2104140 | ||||
|  | ||||
| -   Added dynamic location-based Sunrise/Sunset macros (PR #1889) | ||||
| -   Improved seasonal background handling (PR #1890) | ||||
| -   Fixed instance discovery not working if MQTT not compiled in | ||||
| -   Fixed Button, IR, Relay pin not assigned by default (resolves #1891) | ||||
|  | ||||
| #### Build 2104120 | ||||
|  | ||||
| -   Added switch support (button macro is switch closing action, long press macro switch opening) | ||||
| -   Replaced Circus effect with new Running Dual effect (Circus is Tricolor Chase with Red/White/Black) | ||||
| -   Fixed ledmap with multiple segments (PR #1864) | ||||
|  | ||||
| #### Build 2104030 | ||||
|  | ||||
| -   Fixed ESP32 crash on Drip effect with reversed segment (#1854) | ||||
| -   Added flag `WLED_DISABLE_BROWNOUT_DET` to disable ESP32 brownout detector (off by default) | ||||
|  | ||||
| ### WLED release 0.12.0 | ||||
|  | ||||
| #### Build 2104020 | ||||
|  | ||||
| -   Allow clearing button/IR/relay pin on platforms that don't support negative numbers | ||||
| -   Removed AUX pin | ||||
| -   Hid some easter eggs, only to be found at easter | ||||
|  | ||||
| ### Development versions between 0.11.1 and 0.12.0 releases | ||||
|  | ||||
| #### Build 2103310 | ||||
|  | ||||
| -   Version bump to 0.12.0 "Hikari" | ||||
| -   Fixed LED settings submission in iOS app | ||||
|  | ||||
| #### Build 2103300 | ||||
|  | ||||
| -   Version bump to 0.12.0-b5 "Hikari" | ||||
|   | ||||
| Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB | 
| Before Width: | Height: | Size: 602 B | 
| Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB | 
							
								
								
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "wled", | ||||
|   "version": "0.11.1", | ||||
|   "version": "0.13.0-b5", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
| @@ -761,9 +761,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "glob-parent": { | ||||
|       "version": "5.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", | ||||
|       "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", | ||||
|       "version": "5.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", | ||||
|       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", | ||||
|       "requires": { | ||||
|         "is-glob": "^4.0.1" | ||||
|       } | ||||
| @@ -1572,9 +1572,9 @@ | ||||
|       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" | ||||
|     }, | ||||
|     "normalize-url": { | ||||
|       "version": "4.5.0", | ||||
|       "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", | ||||
|       "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" | ||||
|       "version": "4.5.1", | ||||
|       "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", | ||||
|       "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" | ||||
|     }, | ||||
|     "nth-check": { | ||||
|       "version": "1.0.2", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "wled", | ||||
|   "version": "0.12.0-b5", | ||||
|   "version": "0.13.0-b5", | ||||
|   "description": "Tools for WLED project", | ||||
|   "main": "tools/cdata.js", | ||||
|   "directories": { | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| Import('env') | ||||
| import os | ||||
| import shutil | ||||
| import gzip | ||||
|  | ||||
| OUTPUT_DIR = "build_output{}".format(os.path.sep) | ||||
|  | ||||
| def bin_gzip(source, target, env): | ||||
|     variant = str(target[0]).split(os.path.sep)[2] | ||||
|      | ||||
|     # create string with location and file names based on variant | ||||
|     bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|     gzip_file = "{}firmware{}{}.bin.gz".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|  | ||||
|     # check if new target files exist and remove if necessary | ||||
|     if os.path.isfile(gzip_file): os.remove(gzip_file) | ||||
|  | ||||
|     # write gzip firmware file | ||||
|     with open(bin_file,"rb") as fp: | ||||
|         with gzip.open(gzip_file, "wb", compresslevel = 9) as f: | ||||
|             shutil.copyfileobj(fp, f) | ||||
|  | ||||
| env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_gzip]) | ||||
| @@ -1,34 +0,0 @@ | ||||
| Import('env') | ||||
| import os | ||||
| import shutil | ||||
|  | ||||
| OUTPUT_DIR = "build_output{}".format(os.path.sep) | ||||
|  | ||||
| def bin_rename_copy(source, target, env): | ||||
|     variant = str(target[0]).split(os.path.sep)[2] | ||||
|      | ||||
|     # check if output directories exist and create if necessary | ||||
|     if not os.path.isdir(OUTPUT_DIR): | ||||
|         os.mkdir(OUTPUT_DIR) | ||||
|  | ||||
|     for d in ['firmware', 'map']: | ||||
|         if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)): | ||||
|             os.mkdir("{}{}".format(OUTPUT_DIR, d)) | ||||
|  | ||||
|     # create string with location and file names based on variant | ||||
|     map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|     bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|  | ||||
|     # check if new target files exist and remove if necessary | ||||
|     for f in [map_file, bin_file]: | ||||
|         if os.path.isfile(f): | ||||
|             os.remove(f) | ||||
|  | ||||
|     # copy firmware.bin to firmware/<variant>.bin | ||||
|     shutil.copy(str(target[0]), bin_file) | ||||
|  | ||||
|     # copy firmware.map to map/<variant>.map | ||||
|     if os.path.isfile("firmware.map"): | ||||
|         shutil.move("firmware.map", map_file) | ||||
|  | ||||
| env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_rename_copy]) | ||||
							
								
								
									
										69
									
								
								pio-scripts/output_bins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,69 @@ | ||||
| Import('env') | ||||
| import os | ||||
| import shutil | ||||
| import gzip | ||||
|  | ||||
| OUTPUT_DIR = "build_output{}".format(os.path.sep) | ||||
|  | ||||
| def _get_cpp_define_value(env, define): | ||||
|     define_list = [item[-1] for item in env["CPPDEFINES"] if item[0] == define] | ||||
|  | ||||
|     if define_list: | ||||
|         return define_list[0] | ||||
|  | ||||
|     return None | ||||
|  | ||||
| def _create_dirs(dirs=["firmware", "map"]): | ||||
|     # check if output directories exist and create if necessary | ||||
|     if not os.path.isdir(OUTPUT_DIR): | ||||
|         os.mkdir(OUTPUT_DIR) | ||||
|  | ||||
|     for d in dirs: | ||||
|         if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)): | ||||
|             os.mkdir("{}{}".format(OUTPUT_DIR, d)) | ||||
|  | ||||
| def bin_rename_copy(source, target, env): | ||||
|     _create_dirs() | ||||
|     variant = env["PIOENV"] | ||||
|  | ||||
|     # create string with location and file names based on variant | ||||
|     map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|     bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|  | ||||
|     release_name = _get_cpp_define_value(env, "WLED_RELEASE_NAME") | ||||
|  | ||||
|     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) | ||||
|         shutil.copy(str(target[0]), release_file) | ||||
|  | ||||
|     # check if new target files exist and remove if necessary | ||||
|     for f in [map_file, bin_file]: | ||||
|         if os.path.isfile(f): | ||||
|             os.remove(f) | ||||
|  | ||||
|     # copy firmware.bin to firmware/<variant>.bin | ||||
|     shutil.copy(str(target[0]), bin_file) | ||||
|  | ||||
|     # copy firmware.map to map/<variant>.map | ||||
|     if os.path.isfile("firmware.map"): | ||||
|         shutil.move("firmware.map", map_file) | ||||
|  | ||||
| def bin_gzip(source, target, env): | ||||
|     _create_dirs() | ||||
|     variant = env["PIOENV"] | ||||
|  | ||||
|     # create string with location and file names based on variant | ||||
|     bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|     gzip_file = "{}firmware{}{}.bin.gz".format(OUTPUT_DIR, os.path.sep, variant) | ||||
|  | ||||
|     # check if new target files exist and remove if necessary | ||||
|     if os.path.isfile(gzip_file): os.remove(gzip_file) | ||||
|  | ||||
|     # write gzip firmware file | ||||
|     with open(bin_file,"rb") as fp: | ||||
|         with gzip.open(gzip_file, "wb", compresslevel = 9) as f: | ||||
|             shutil.copyfileobj(fp, f) | ||||
|  | ||||
| env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_rename_copy, bin_gzip]) | ||||
							
								
								
									
										8
									
								
								pio-scripts/set_version.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| Import('env') | ||||
| import json | ||||
|  | ||||
| PACKAGE_FILE = "package.json" | ||||
|  | ||||
| with open(PACKAGE_FILE, "r") as package: | ||||
|     version = json.load(package)["version"] | ||||
|     env.Append(BUILD_FLAGS=[f"-DWLED_VERSION={version}"]) | ||||
							
								
								
									
										333
									
								
								platformio.ini
									
									
									
									
									
								
							
							
						
						| @@ -8,13 +8,17 @@ | ||||
| # Please uncomment one of the lines below to select your board(s) | ||||
| # ------------------------------------------------------------------------------ | ||||
|  | ||||
| # Travis CI binaries (comment this out with a ';' when building for your own board) | ||||
| ;default_envs = travis_esp8266, travis_esp32 | ||||
| # Travis CI binaries (use `platformio_override.ini` when building for your own board; see `platformio_override.ini.sample` for an example) | ||||
| ; default_envs = travis_esp8266, travis_esp32 | ||||
|  | ||||
| # Release binaries | ||||
| default_envs = nodemcuv2, esp01_1m_full, esp32dev, esp32_eth | ||||
|  | ||||
| # Build everything | ||||
| ; default_envs = esp32dev, esp8285_4CH_MagicHome, esp8285_4CH_H801, codm-controller-0.6-rev2, codm-controller-0.6, esp32s2_saola, d1_mini_5CH_Shojo_PCB, d1_mini, sp501e, travis_esp8266, travis_esp32, nodemcuv2, esp32_eth, anavi_miracle_controller, esp07, esp01_1m_full, m5atom, h803wf, d1_mini_ota, heltec_wifi_kit_8, esp8285_5CH_H801, d1_mini_debug, wemos_shield_esp32, elekstube_ips | ||||
|  | ||||
| # Single binaries (uncomment your board) | ||||
| ; default_envs = elekstube_ips | ||||
| ; default_envs = nodemcuv2 | ||||
| ; default_envs = esp01_1m_full | ||||
| ; default_envs = esp07 | ||||
| @@ -48,6 +52,7 @@ extra_configs = | ||||
| # ------------------------------------------------------------------------------ | ||||
| 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 | ||||
|  | ||||
| # Development platforms | ||||
| arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop | ||||
| @@ -71,7 +76,6 @@ debug_flags = -D DEBUG=1 -D WLED_DEBUG -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # FLAGS: ldscript (available ldscripts at https://github.com/esp8266/Arduino/tree/master/tools/sdk/ld) | ||||
| #    ldscript_512k ( 512 KB) =  487 KB sketch, 4 KB eeprom,      no spiffs, 16 KB reserved | ||||
| #    ldscript_1m0m (1024 KB) =  999 KB sketch, 4 KB eeprom,      no spiffs, 16 KB reserved | ||||
| #    ldscript_2m1m (2048 KB) = 1019 KB sketch, 4 KB eeprom, 1004 KB spiffs, 16 KB reserved | ||||
| #    ldscript_4m1m (4096 KB) = 1019 KB sketch, 4 KB eeprom, 1002 KB spiffs, 16 KB reserved, 2048 KB empty/ota? | ||||
| @@ -114,9 +118,6 @@ build_unflags = | ||||
|  | ||||
| # enables all features for travis CI | ||||
| build_flags_all_features = | ||||
|   -D WLED_USE_ANALOG_LED | ||||
|   -D WLED_USE_H801 | ||||
|   -D WLED_ENABLE_5CH_LEDS | ||||
|   -D WLED_ENABLE_ADALIGHT | ||||
|   -D WLED_ENABLE_DMX | ||||
|   -D WLED_ENABLE_MQTT | ||||
| @@ -130,31 +131,12 @@ ldscript_2m512k = eagle.flash.2m512.ld | ||||
| ldscript_2m1m = eagle.flash.2m1m.ld | ||||
| ldscript_4m1m = eagle.flash.4m1m.ld | ||||
|  | ||||
| [esp8266] | ||||
| build_flags = | ||||
|   -DESP8266 | ||||
|   -DFP_IN_IROM | ||||
| ; NONOSDK22x_190703 = 2.2.2-dev(38a443e) | ||||
|   -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 | ||||
| ; lwIP 2 - Higher Bandwidth no Features | ||||
| ;  -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH | ||||
| ; lwIP 1.4 - Higher Bandwidth (Aircoookie has) | ||||
|    -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH | ||||
| ; VTABLES in Flash | ||||
|   -DVTABLES_IN_FLASH | ||||
| ; restrict to minimal mime-types | ||||
|   -DMIMETYPE_MINIMAL | ||||
|  | ||||
| [esp32] | ||||
| build_flags = -g | ||||
|   -DARDUINO_ARCH_ESP32 | ||||
|   -DCONFIG_LITTLEFS_FOR_IDF_3_2 | ||||
|  | ||||
| [scripts_defaults] | ||||
| extra_scripts             = pio-scripts/name-firmware.py | ||||
|                             pio-scripts/gzip-firmware.py | ||||
|                             pio-scripts/strip-floats.py | ||||
|                             pio-scripts/user_config_copy.py | ||||
| extra_scripts = | ||||
|   pre:pio-scripts/set_version.py | ||||
|   post:pio-scripts/output_bins.py | ||||
|   post:pio-scripts/strip-floats.py | ||||
|   pre:pio-scripts/user_config_copy.py | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # COMMON SETTINGS: | ||||
| @@ -177,12 +159,8 @@ upload_speed = 115200 | ||||
| # ------------------------------------------------------------------------------ | ||||
| lib_compat_mode = strict | ||||
| lib_deps = | ||||
|     fastled/FastLED @ 3.3.2 | ||||
|     NeoPixelBus @ 2.6.0 | ||||
|     ESPAsyncTCP @ 1.2.0 | ||||
|     ESPAsyncUDP | ||||
|     AsyncTCP @ 1.0.3 | ||||
|     IRremoteESP8266 @ 2.7.3 | ||||
|     fastled/FastLED @ 3.4.0 | ||||
|     IRremoteESP8266 @ 2.7.18 | ||||
|     https://github.com/lorol/LITTLEFS.git | ||||
|     https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.2 | ||||
|   #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line | ||||
| @@ -198,11 +176,58 @@ lib_deps = | ||||
|     ; adafruit/Adafruit CCS811 Library @ 1.0.4 | ||||
|     ; adafruit/Adafruit Si7021 Library @ 1.4.0 | ||||
|  | ||||
| lib_ignore = | ||||
|     AsyncTCP | ||||
|  | ||||
| extra_scripts             = ${scripts_defaults.extra_scripts} | ||||
|  | ||||
| [esp8266] | ||||
| build_flags = | ||||
|   -DESP8266 | ||||
|   -DFP_IN_IROM | ||||
|   ;-Wno-deprecated-declarations | ||||
|   ;-Wno-register | ||||
| ; NONOSDK22x_190703 = 2.2.2-dev(38a443e) | ||||
|   -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 | ||||
| ; lwIP 2 - Higher Bandwidth no Features | ||||
| ;  -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH | ||||
| ; lwIP 1.4 - Higher Bandwidth (Aircoookie has) | ||||
|    -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH | ||||
| ; VTABLES in Flash | ||||
|   -DVTABLES_IN_FLASH | ||||
| ; restrict to minimal mime-types | ||||
|   -DMIMETYPE_MINIMAL | ||||
|  | ||||
| lib_deps =  | ||||
|   ${env.lib_deps} | ||||
|   # ESPAsyncTCP @ 1.2.0 | ||||
|   ESPAsyncUDP | ||||
|   makuna/NeoPixelBus @ 2.6.7 # 2.6.5/2.6.6 and newer do not compile on ESP core < 3.0.0 | ||||
|  | ||||
| [esp32] | ||||
| build_flags = -g | ||||
|   -DARDUINO_ARCH_ESP32 | ||||
|   -DCONFIG_LITTLEFS_FOR_IDF_3_2 | ||||
|   -D CONFIG_ASYNC_TCP_USE_WDT=0 | ||||
|  | ||||
| default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv | ||||
|  | ||||
| lib_deps = | ||||
|   ${env.lib_deps} | ||||
|   makuna/NeoPixelBus @ 2.6.7 | ||||
|   https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 | ||||
|  | ||||
| [esp32s2] | ||||
| build_flags = -g | ||||
|   -DARDUINO_ARCH_ESP32 | ||||
|   -DCONFIG_LITTLEFS_FOR_IDF_3_2 | ||||
|   -DARDUINO_ARCH_ESP32S2 | ||||
|   -DCONFIG_IDF_TARGET_ESP32S2 | ||||
|   -D CONFIG_ASYNC_TCP_USE_WDT=0 | ||||
|   -DCO | ||||
|  | ||||
| lib_deps = | ||||
|   ${env.lib_deps} | ||||
|   makuna/NeoPixelBus @ 2.6.7 | ||||
|   https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # WLED BUILDS | ||||
| # ------------------------------------------------------------------------------ | ||||
| @@ -213,7 +238,8 @@ platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:esp01_1m_full] | ||||
| board = esp01_1m | ||||
| @@ -221,7 +247,8 @@ 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 | ||||
| build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:esp07] | ||||
| board = esp07 | ||||
| @@ -230,6 +257,7 @@ platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:d1_mini] | ||||
| board = d1_mini | ||||
| @@ -239,6 +267,7 @@ upload_speed = 921600 | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
| monitor_filters = esp8266_exception_decoder | ||||
|  | ||||
| [env:heltec_wifi_kit_8] | ||||
| @@ -248,6 +277,7 @@ platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:h803wf] | ||||
| board = d1_mini | ||||
| @@ -256,25 +286,37 @@ platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=1 -D WLED_DISABLE_INFRARED | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:esp32dev] | ||||
| board = esp32dev | ||||
| platform = espressif32@3.2 | ||||
| platform = espressif32@2.0 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
| build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET | ||||
| lib_deps = ${esp32.lib_deps} | ||||
| board_build.partitions = ${esp32.default_partitions} | ||||
|  | ||||
| [env:esp32_eth] | ||||
| board = esp32-poe | ||||
| platform = espressif32@3.2 | ||||
| platform = espressif32@2.0 | ||||
| upload_speed = 921600 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
| build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 | ||||
| lib_deps = ${esp32.lib_deps} | ||||
| board_build.partitions = ${esp32.default_partitions} | ||||
|  | ||||
| [env:esp32s2_saola] | ||||
| board = esp32dev | ||||
| board_build.mcu = esp32s2 | ||||
| platform = espressif32 | ||||
| platform_packages = | ||||
|     toolchain-xtensa32s2 | ||||
|     framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.0-alpha1 | ||||
| framework = arduino | ||||
| board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv | ||||
| upload_speed = 460800 | ||||
| build_unflags = ${common.build_unflags} | ||||
| lib_deps = ${esp32s2.lib_deps} | ||||
|  | ||||
| [env:esp8285_4CH_MagicHome] | ||||
| board = esp8285 | ||||
| @@ -282,7 +324,8 @@ 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_USE_ANALOG_LEDS | ||||
| build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:esp8285_4CH_H801] | ||||
| board = esp8285 | ||||
| @@ -290,7 +333,8 @@ 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_USE_ANALOG_LEDS -D WLED_USE_H801 | ||||
| build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:esp8285_5CH_H801] | ||||
| board = esp8285 | ||||
| @@ -298,7 +342,8 @@ 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_USE_ANALOG_LEDS -D WLED_USE_H801 -D WLED_ENABLE_5CH_LEDS | ||||
| build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:d1_mini_5CH_Shojo_PCB] | ||||
| board = d1_mini | ||||
| @@ -306,7 +351,8 @@ platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D WLED_USE_ANALOG_LEDS -D WLED_USE_SHOJO_PCB -D WLED_ENABLE_5CH_LEDS | ||||
| build_flags = ${common.build_flags_esp8266} -D WLED_USE_SHOJO_PCB | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # DEVELOPMENT BOARDS | ||||
| @@ -320,6 +366,7 @@ platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} ${common.debug_flags} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:d1_mini_ota] | ||||
| board = d1_mini | ||||
| @@ -331,6 +378,7 @@ platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:anavi_miracle_controller] | ||||
| board = d1_mini | ||||
| @@ -339,105 +387,52 @@ platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # custom board configurations | ||||
| # ------------------------------------------------------------------------------ | ||||
|  | ||||
| [env:custom_LEDPIN_4] | ||||
| board = d1_mini | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=4 -D IRPIN=5 | ||||
|  | ||||
| [env:custom_LEDPIN_16] | ||||
| board = d1_mini | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=16 | ||||
|  | ||||
|  | ||||
| [env:custom_LEDPIN_3] | ||||
| board = d1_mini | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 | ||||
|  | ||||
| [env:custom_APA102] | ||||
| board = d1_mini | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D USE_APA102 | ||||
|  | ||||
| [env:custom_WS2801] | ||||
| board = d1_mini | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D USE_WS2801 | ||||
|  | ||||
| [env:custom32_LEDPIN_16] | ||||
| board = esp32dev | ||||
| platform = espressif32@3.2 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} -D LEDPIN=16 -D RLYPIN=19 | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
|  | ||||
| [env:custom32_APA102] | ||||
| board = esp32dev | ||||
| platform = espressif32@3.2 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} -D USE_APA102 | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
|  | ||||
| [env:custom32_TOUCHPIN_T0] | ||||
| board = esp32dev | ||||
| platform = espressif32@3.2 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} -D TOUCHPIN=T0 | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
|  | ||||
| [env:wemos_shield_esp32] | ||||
| board = esp32dev | ||||
| platform = espressif32@3.2 | ||||
| upload_port = /dev/cu.SLAB_USBtoUART | ||||
| monitor_port = /dev/cu.SLAB_USBtoUART | ||||
| upload_speed = 460800 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} -D LEDPIN=16 -D RLYPIN=19 -D BTNPIN=17 | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
| 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 | ||||
| lib_deps = ${esp32.lib_deps} | ||||
|   OneWire@~2.3.5 | ||||
|   olikraus/U8g2 @ ^2.28.8 | ||||
| board_build.partitions = ${esp32.default_partitions} | ||||
|  | ||||
| [env:m5atom] | ||||
| board = esp32dev | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} -D LEDPIN=27 -D BTNPIN=39 | ||||
| lib_ignore = | ||||
| 	ESPAsyncTCP | ||||
| 	ESPAsyncUDP | ||||
| lib_deps = ${esp32.lib_deps} | ||||
| platform = espressif32@3.2 | ||||
| board_build.partitions = ${esp32.default_partitions} | ||||
|  | ||||
| [env:sp501e] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=1 | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:sp511e] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # travis test board configurations | ||||
| @@ -457,52 +452,56 @@ build_flags = ${common.build_flags_esp32} ${common.debug_flags} ${common.build_f | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # codm pixel controller board configurations | ||||
| # codm-controller-0.6 can also be used for the TYWE3S controller | ||||
| # ------------------------------------------------------------------------------ | ||||
|  | ||||
| [env:codm-controller-0.4] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 | ||||
|  | ||||
| [env:codm-controller-0.4-WS2801] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D USE_WS2801 -D CLKPIN=13 -D DATAPIN=3 | ||||
|  | ||||
| [env:codm-controller-0.4-APA102] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D USE_APA102 -D CLKPIN=13 -D DATAPIN=3 | ||||
|  | ||||
| [env:codm-controller-0.5] | ||||
| [env:codm-controller-0.6] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:codm-controller-0.5-WS2801] | ||||
| [env:codm-controller-0.6-rev2] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D USE_WS2801 #-D CLKPIN=0 -D DATAPIN=2 | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
|  | ||||
| [env:codm-controller-0.5-APA102] | ||||
| board = esp_wroom_02 | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D USE_APA102 #-D CLKPIN=0 -D DATAPIN=2 | ||||
| # ------------------------------------------------------------------------------ | ||||
| # EleksTube-IPS | ||||
| # ------------------------------------------------------------------------------ | ||||
| [env:elekstube_ips] | ||||
| board = esp32dev | ||||
| platform = espressif32@3.2 | ||||
| upload_speed = 921600 | ||||
| build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED | ||||
|   -D USERMOD_RTC | ||||
|   -D USERMOD_ELEKSTUBE_IPS | ||||
|   -D LEDPIN=12 | ||||
|   -D RLYPIN=27 | ||||
|   -D BTNPIN=34 | ||||
|   -D WLED_DISABLE_INFRARED | ||||
|   -D DEFAULT_LED_COUNT=6 | ||||
|   # Display config | ||||
|   -D ST7789_DRIVER | ||||
|   -D TFT_WIDTH=135 | ||||
|   -D TFT_HEIGHT=240 | ||||
|   -D CGRAM_OFFSET | ||||
|   -D TFT_SDA_READ | ||||
|   -D TFT_MOSI=23 | ||||
|   -D TFT_SCLK=18 | ||||
|   -D TFT_DC=25 | ||||
|   -D TFT_RST=26 | ||||
|   -D SPI_FREQUENCY=40000000 | ||||
|   -D USER_SETUP_LOADED | ||||
| monitor_filters = esp32_exception_decoder | ||||
| lib_deps = | ||||
|   ${esp32.lib_deps} | ||||
|   TFT_eSPI @ ^2.3.70 | ||||
| board_build.partitions = ${esp32.default_partitions} | ||||
|   | ||||
| @@ -12,6 +12,7 @@ board = esp01_1m | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_1m128k} | ||||
| lib_deps = ${esp8266.lib_deps} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
| ; ********************************************************************* | ||||
| @@ -39,12 +40,8 @@ build_flags = ${common.build_flags_esp8266} | ||||
| ; PIN defines for 2 wire LEDs | ||||
|    -D CLKPIN=0 | ||||
|    -D DATAPIN=2 | ||||
| ; to drive analog LED strips (aka 5050), uncomment the following | ||||
| ; PWM pins 5,12,13,15 are used with Magic Home LED Controller (default) | ||||
|    -D WLED_USE_ANALOG_LEDS | ||||
| ; for the H801 controller (PINs 15,13,12,14 (W2 = 04)) uncomment this | ||||
| ;   -D WLED_USE_H801 | ||||
| ; for the BW-LT11 controller (PINs 12,4,14,5 ) uncomment this | ||||
| ;   -D WLED_USE_BWLT11 | ||||
| ; and to enable channel 5 for RGBW-CT led strips this | ||||
| ;   -D WLED_USE_5CH_LEDS | ||||
| ; to drive analog LED strips (aka 5050) hardware configuration is no longer necessary | ||||
| ; configure the settings in the UI as follows (hard): | ||||
| ;   for the Magic Home LED Controller use PWM pins 5,12,13,15 | ||||
| ;   for the H801 controller use PINs 15,13,12,14 (W2 = 04) | ||||
| ;   for the BW-LT11 controller use PINs 12,4,14,5 | ||||
|   | ||||
| @@ -4,12 +4,12 @@ | ||||
|   <a href="https://raw.githubusercontent.com/Aircoookie/WLED/master/LICENSE"><img src="https://img.shields.io/github/license/Aircoookie/wled?color=blue&style=flat-square"></a> | ||||
|   <a href="https://wled.discourse.group"><img src="https://img.shields.io/discourse/topics?colorB=blue&label=forum&server=https%3A%2F%2Fwled.discourse.group%2F&style=flat-square"></a> | ||||
|   <a href="https://discord.gg/KuqP7NE"><img src="https://img.shields.io/discord/473448917040758787.svg?colorB=blue&label=discord&style=flat-square"></a> | ||||
|   <a href="https://github.com/Aircoookie/WLED/wiki"><img src="https://img.shields.io/badge/quick_start-wiki-blue.svg?style=flat-square"></a> | ||||
|   <a href="https://kno.wled.ge"><img src="https://img.shields.io/badge/quick_start-wiki-blue.svg?style=flat-square"></a> | ||||
|   <a href="https://github.com/Aircoookie/WLED-App"><img src="https://img.shields.io/badge/app-wled-blue.svg?style=flat-square"></a> | ||||
|   <a href="https://gitpod.io/#https://github.com/Aircoookie/WLED"><img src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?style=flat-square&logo=gitpod"></a> | ||||
|  | ||||
|   </p> | ||||
|    | ||||
|  | ||||
| # Welcome to my project WLED! ✨ | ||||
|  | ||||
| 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! | ||||
| @@ -21,6 +21,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control | ||||
| - Segments to set different effects and colors to parts of the LEDs   | ||||
| - Settings page - configuration over 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   | ||||
| @@ -36,6 +37,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control | ||||
| - MQTT   | ||||
| - Blynk IoT   | ||||
| - 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)   | ||||
| @@ -47,7 +49,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control | ||||
|  | ||||
| ## 📲 Quick start guide and documentation | ||||
|  | ||||
| See the [wiki](https://github.com/Aircoookie/WLED/wiki)! | ||||
| See the [documentation on our official site](https://kno.wled.ge)! | ||||
|  | ||||
| [On this page](https://github.com/Aircoookie/WLED/wiki/Learning-the-ropes) you can find excellent tutorials made by the community and helpful tools to help you get your new lamp up and running! | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								requirements.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| platformio | ||||
							
								
								
									
										54
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | ||||
| # | ||||
| # This file is autogenerated by pip-compile | ||||
| # To update, run: | ||||
| # | ||||
| #    pip-compile | ||||
| # | ||||
| aiofiles==0.6.0 | ||||
|     # via platformio | ||||
| ajsonrpc==1.1.0 | ||||
|     # via platformio | ||||
| bottle==0.12.19 | ||||
|     # via platformio | ||||
| certifi==2020.12.5 | ||||
|     # via requests | ||||
| chardet==4.0.0 | ||||
|     # via requests | ||||
| click==7.1.2 | ||||
|     # via | ||||
|     #   platformio | ||||
|     #   uvicorn | ||||
| colorama==0.4.4 | ||||
|     # via platformio | ||||
| h11==0.12.0 | ||||
|     # via | ||||
|     #   uvicorn | ||||
|     #   wsproto | ||||
| idna==2.10 | ||||
|     # via requests | ||||
| ifaddr==0.1.7 | ||||
|     # via zeroconf | ||||
| marshmallow==3.11.1 | ||||
|     # via platformio | ||||
| platformio==5.1.1 | ||||
|     # via -r requirements.in | ||||
| pyelftools==0.27 | ||||
|     # via platformio | ||||
| pyserial==3.5 | ||||
|     # via platformio | ||||
| requests==2.25.1 | ||||
|     # via platformio | ||||
| semantic-version==2.8.5 | ||||
|     # via platformio | ||||
| starlette==0.14.2 | ||||
|     # via platformio | ||||
| tabulate==0.8.9 | ||||
|     # via platformio | ||||
| urllib3==1.26.5 | ||||
|     # via requests | ||||
| uvicorn==0.13.4 | ||||
|     # via platformio | ||||
| wsproto==1.0.0 | ||||
|     # via platformio | ||||
| zeroconf==0.28.8 | ||||
|     # via platformio | ||||
							
								
								
									
										6
									
								
								tools/WLED_ESP32_16MB.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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, 0x200000, | ||||
| app1,     app,  ota_1,   0x210000,0x200000, | ||||
| spiffs,   data, spiffs,  0x410000,0xBE0000, | ||||
| 
 | 
							
								
								
									
										6
									
								
								tools/WLED_ESP32_4MB_1MB_FS.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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, 0x180000, | ||||
| app1,     app,  ota_1,   0x190000,0x180000, | ||||
| spiffs,   data, spiffs,  0x310000,0xF0000, | ||||
| 
 | 
							
								
								
									
										6
									
								
								tools/WLED_ESP32_8MB.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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, 0x200000, | ||||
| app1,     app,  ota_1,   0x210000,0x200000, | ||||
| spiffs,   data, spiffs,  0x410000,0x3F0000, | ||||
| 
 | 
| @@ -90,7 +90,7 @@ function writeHtmlGzipped(sourceFile, resultFile) { | ||||
|  * Binary array for the Web UI. | ||||
|  * gzip is used for smaller size and improved speeds. | ||||
|  *  | ||||
|  * Please see https://github.com/Aircoookie/WLED/wiki/Add-own-functionality#web-ui | ||||
|  * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui | ||||
|  * to find out how to easily modify the web UI source! | ||||
|  */ | ||||
|   | ||||
| @@ -175,7 +175,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://github.com/Aircoookie/WLED/wiki/Add-own-functionality#web-ui | ||||
|  * Instead, see https://kno.wled.ge/advanced/custom-features/#changing-web-ui | ||||
|  * to find out how to easily modify the web UI source! | ||||
|  */  | ||||
| `; | ||||
| @@ -333,6 +333,22 @@ const char PAGE_settings_dmx[] PROGMEM = R"=====()====="; | ||||
|             "function GetV() {var d=document;\n" | ||||
|           ), | ||||
|     }, | ||||
|     { | ||||
|       file: "settings_um.htm", | ||||
|       name: "PAGE_settings_um", | ||||
|       prepend: "=====(", | ||||
|       append: ")=====", | ||||
|       method: "plaintext", | ||||
|       filter: "html-minify", | ||||
|       mangle: (str) => | ||||
|         str | ||||
|           .replace(/\<link rel="stylesheet".*\>/gms, "") | ||||
|           .replace(/\<style\>.*\<\/style\>/gms, "%CSS%%SCSS%") | ||||
|           .replace( | ||||
|             /function GetV().*\<\/script\>/gms, | ||||
|             "function GetV() {var d=document;\n" | ||||
|           ), | ||||
|     } | ||||
|   ], | ||||
|   "wled00/html_settings.h" | ||||
| ); | ||||
|   | ||||
							
								
								
									
										232
									
								
								tools/fps_test.htm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,232 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <title>WLED frame rate test tool</title> | ||||
|     <style> | ||||
|         body { | ||||
|             background-color: #222; | ||||
|             color: #fff; | ||||
|             font-family: Helvetica, Verdana, sans-serif; | ||||
|         } | ||||
|         input { | ||||
|             background-color: #333; | ||||
|             color: #fff; | ||||
|         } | ||||
|         #ip { | ||||
|             width: 100px; | ||||
|         } | ||||
|         #secs { | ||||
|             width: 36px; | ||||
|         } | ||||
|         #csva { | ||||
|             position: absolute; | ||||
|             top: -100px; /*gtfo*/ | ||||
|         } | ||||
|         button { | ||||
|             background-color: #333; | ||||
|             color: #fff; | ||||
|         } | ||||
|         table, th, td { | ||||
|             border: 1px solid #aaa; | ||||
|             border-collapse: collapse; | ||||
|             text-align: center; | ||||
|         } | ||||
|         .red { | ||||
|             color: #d20; | ||||
|         } | ||||
|     </style> | ||||
|     <script> | ||||
|         var gotfx = false, running = false; | ||||
|         var pos = 0, prev = 0, min = 999, max = 0, fpslist = [], names = [], names_checked = []; | ||||
|         var to; | ||||
|         function S() { | ||||
|             document.getElementById('ip').value = localStorage.getItem('locIpFps'); | ||||
|             if (document.getElementById('ip').value) req(false); | ||||
|         } | ||||
|         function loadC() { | ||||
|             hide(false); | ||||
|             var list = localStorage.getItem('fpsFxSelection'); | ||||
|             if (!list) return; | ||||
|             list = JSON.parse(list); | ||||
|             var chks = document.querySelectorAll('.fxcheck'); | ||||
|             for (let i = 0; i < chks.length; i++) { | ||||
|                if (i < list.length) chks[i].checked = list[i]; | ||||
|             } | ||||
|         } | ||||
|         function saveC() { | ||||
|             var list = []; | ||||
|             var chks = document.querySelectorAll('.fxcheck'); | ||||
|             for (let i = 0; i < chks.length; i++) { | ||||
|                 list.push(chks[i].checked); | ||||
|             } | ||||
|             localStorage.setItem('fpsFxSelection', JSON.stringify(list)); | ||||
|         } | ||||
|         function setC(c) { | ||||
|             hide(false); | ||||
|             var chks = document.querySelectorAll('.fxcheck'); | ||||
|             for (let i = 0; i < chks.length; i++) { | ||||
|                 chks[i].checked = (c == 255); | ||||
|             } | ||||
|             if (c == 1 && chks.length > 100) { | ||||
|                 chks[1].checked = true;  //Blink | ||||
|                 chks[15].checked = true; //Running | ||||
|                 chks[16].checked = true; //Saw | ||||
|                 chks[37].checked = true; //Running 2 | ||||
|                 chks[44].checked = true; //Tetrix | ||||
|                 chks[63].checked = true; //Pride 2015 | ||||
|                 chks[74].checked = true; //Colortwinkles | ||||
|                 chks[101].checked = true;//Pacifica | ||||
|             } | ||||
|         } | ||||
|         function hide(h) { | ||||
|             var trs = document.querySelectorAll('.trs'); | ||||
|             var chks = document.querySelectorAll('.fxcheck'); | ||||
|             for (let i = 0; i < trs.length; i++) { | ||||
|                 trs[i].style.display = (h && !chks[i].checked) ? "none":"table-row"; | ||||
|             } | ||||
|         } | ||||
|         function run(init) { | ||||
|             if (init) { | ||||
|                 running = !running; | ||||
|                 document.getElementById('runbtn').innerText = running ? 'Stop':'Run'; | ||||
|                 if (running) {pos = 0; prev = -1; min = 999; max = 0; fpslist = []; names_checked = []; hide(true);} | ||||
|                 clearTimeout(to); | ||||
|                 if (!running) {req({seg:{fx:0},v:true,stop:true}); return;} | ||||
|             } | ||||
|             if (!gotfx) {req(false); return;} | ||||
|             var chks = document.querySelectorAll('.fxcheck'); | ||||
|             var fpsb = document.querySelectorAll('.fps'); | ||||
|             if (prev >= 0) {pos++}; | ||||
|             if (pos >= chks.length) {run(true); return;} //end | ||||
|             while (!chks[pos].checked) { | ||||
|                 fpsb[pos].innerText = "-"; | ||||
|                 pos++; | ||||
|                 if (pos >= chks.length) {run(true); return;} //end | ||||
|             } | ||||
|             names_checked.push(names[pos]); | ||||
|             var extra = {}; | ||||
|             try { | ||||
|                 extra = JSON.parse(document.getElementById('ej').value); | ||||
|             } catch (e) { | ||||
|  | ||||
|             } | ||||
|             var cmd = {seg:{fx:pos},v:true}; | ||||
|             Object.assign(cmd, extra); | ||||
|             req(cmd); | ||||
|         } | ||||
|         function req(command) { | ||||
|             var ip = document.getElementById('ip').value; | ||||
|             if (!ip) {alert("Please enter WLED IP"); return;} | ||||
|             if (ip != localStorage.getItem('locIpFps')) localStorage.setItem('locIpFps', document.getElementById('ip').value); | ||||
|             var url = command ? `http://${ip}/json/si` : `http://${ip}/json/effects`; | ||||
|             var type = command ? 'post':'get'; | ||||
|             var req = undefined; | ||||
|             if (command) | ||||
|             { | ||||
|                 req = JSON.stringify(command); | ||||
|             } | ||||
|             fetch | ||||
|             (url, { | ||||
|                 method: type, | ||||
|                 headers: { | ||||
|                     "Content-type": "application/json; charset=UTF-8" | ||||
|                 }, | ||||
|                 body: req | ||||
|             }) | ||||
|             .then(res => { | ||||
|                 if (!res.ok) { | ||||
|                     alert('Data malfunction'); | ||||
|                 } | ||||
|                 return res.json(); | ||||
|             }) | ||||
|             .then(json => { | ||||
|                 if (!json) { | ||||
|                     alert('Empty response'); return; | ||||
|                 } | ||||
|                 if (!command) { | ||||
|                     names = json; | ||||
|                     var tblc = ''; | ||||
|                     for (let i = 0; i < json.length; i++) { | ||||
| 		                tblc += `<tr class="trs"><td><input type="checkbox" class="fxcheck" /></td><td>${i}</td><td>${json[i]}</td><td class="fps"></td></tr>` | ||||
| 	                } | ||||
|                     var tbl = `<table> | ||||
|                         <tr> | ||||
|                             <th>Test?</th><th>ID</th><th>Effect Name</th><th>FPS</th> | ||||
|                         </tr> | ||||
|                         ${tblc} | ||||
|                     </table>`; | ||||
|                     document.getElementById('tablecon').innerHTML = tbl; | ||||
|                     setC(1); | ||||
|                     loadC(); | ||||
|                     gotfx = true; | ||||
|                     document.getElementById('runbtn').innerText = "Run"; | ||||
|                 } else { | ||||
|                     if (!json.info) return; | ||||
|                     document.getElementById('leds').innerText = json.info.leds.count; | ||||
|                     document.getElementById('seg').innerText = json.state.seg[0].len; | ||||
|                     document.getElementById('bri').innerText = json.state.bri; | ||||
|                     if (prev >= 0) { | ||||
|                         var lastfps = parseInt(json.info.leds.fps); //previous FX | ||||
|                         if (lastfps < min) min = lastfps; | ||||
|                         if (lastfps > max) max = lastfps; | ||||
|                         fpslist.push(lastfps); | ||||
|                         var sum = 0; | ||||
|                         for (let i = 0; i < fpslist.length; i++) { | ||||
|                             sum += fpslist[i]; | ||||
|                         } | ||||
|                         sum /= fpslist.length; | ||||
|                         document.getElementById('fps_min').innerText = min; | ||||
|                         document.getElementById('fps_max').innerText = max; | ||||
|                         document.getElementById('fps_avg').innerText = Math.round(sum*10)/10; | ||||
|                         var fpsb = document.querySelectorAll('.fps'); | ||||
|                         fpsb[prev].innerHTML = lastfps; | ||||
|                     } | ||||
|                     prev = pos; | ||||
|                     var delay = parseInt(document.getElementById('secs').value)*1000; | ||||
|                     delay = Math.min(Math.max(delay, 2000), 15000) | ||||
|                     if (!command.stop) to = setTimeout(run,delay); | ||||
|                 } | ||||
|             }) | ||||
|             .catch(function (error) { | ||||
| 		        alert('Comms malfunction'); | ||||
| 		        console.log(error); | ||||
| 	        }); | ||||
|         } | ||||
|         function csv(n) { | ||||
|             var txt = ""; | ||||
|             for (let i = 0; i < fpslist.length; i++) { | ||||
|                 if (!n) txt += names_checked[i] + ','; | ||||
|                 txt += fpslist[i]; txt += "\n"; | ||||
|             } | ||||
|             document.getElementById('csva').value = txt; | ||||
|             var copyText = document.getElementById('csva'); | ||||
|  | ||||
|             copyText.select(); | ||||
|             copyText.setSelectionRange(0, 999999); | ||||
|             document.execCommand("copy"); | ||||
|         } | ||||
|     </script> | ||||
| </head> | ||||
| <body onload="S()"> | ||||
|     <h2>Starship monitoring dashboard</h2> | ||||
|     (or rather just a WLED frame rate tester lol)<br><br> | ||||
|     IP: <input id="ip" /><br> | ||||
|     Time per effect: <input type=number id=secs value=5 max=15 min=2 />s<br> | ||||
|     Effects to test: | ||||
|     <button type="button" onclick="setC(255)">All</button> | ||||
|     <button type="button" onclick="setC(1)">Selection 1</button> | ||||
|     <button type="button" onclick="setC(0)">None</button> | ||||
|     <button type="button" onclick="loadC()">Get LS</button> | ||||
|     <button type="button" class="red" onclick="saveC()">Save to LS</button><br> | ||||
|     Extra JSON: <input id="ej" /><br> | ||||
|  | ||||
|     <button type="button" onclick="run(true)" id="runbtn">Fetch FX list</button><br> | ||||
|     LEDs: <span id="leds">-</span>, Seg: <span id="seg">-</span>, Bri: <span id="bri">-</span><br> | ||||
|     FPS min: <span id="fps_min">-</span>, max: <span id="fps_max">-</span>, avg: <span id="fps_avg">-</span><br><br> | ||||
|     <div id="tablecon"> | ||||
|     </div><br> | ||||
|     <button type="button" onclick="csv(false)">Copy csv to clipboard</button> | ||||
|     <button type="button" onclick="csv(true)">Copy csv (FPS only)</button> | ||||
|     <textarea id=csva></textarea> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										16
									
								
								tools/multi-update.cmd
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| @echo off | ||||
| SETLOCAL | ||||
| SET FWPATH=c:\path\to\your\WLED\build_output\firmware | ||||
| GOTO ESPS | ||||
|  | ||||
| :UPDATEONE | ||||
| IF NOT EXIST %FWPATH%\%2 GOTO SKIP | ||||
| 	ping -w 1000 -n 1 %1 | find "TTL=" || GOTO SKIP | ||||
| 	ECHO Updating %1 | ||||
| 	curl -s -F "update=@%FWPATH%/%2" %1/update >nul | ||||
| :SKIP | ||||
| GOTO:EOF | ||||
|  | ||||
| :ESPS | ||||
| call :UPDATEONE 192.168.x.x firmware.bin | ||||
| call :UPDATEONE .... | ||||
							
								
								
									
										19
									
								
								tools/multi-update.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| #!/bin/bash | ||||
| FWPATH=/path/to/your/WLED/build_output/firmware | ||||
|  | ||||
| update_one() { | ||||
| if [ -f $FWPATH/$2 ]; then | ||||
| 	ping -c 1 $1 >/dev/null | ||||
| 	PINGRESULT=$? | ||||
| 	if [ $PINGRESULT -eq 0 ]; then | ||||
| 		echo Updating $1 | ||||
| 		curl -s -F "update=@${FWPATH}/$2" $1/update >/dev/null | ||||
| 		return 0 | ||||
| 	fi | ||||
| 	return 1 | ||||
| fi | ||||
| } | ||||
|  | ||||
| update_one 192.168.x.x firmware.bin | ||||
| update_one 192.168.x.x firmware.bin | ||||
| # ... | ||||
| @@ -9,419 +9,526 @@ | ||||
|  */ | ||||
| #pragma once | ||||
| #include "wled.h" | ||||
| #include "Animated_Staircase_config.h" | ||||
| #define USERMOD_ID_ANIMATED_STAIRCASE 1011 | ||||
|  | ||||
| /* Initial configuration (available in API and stored in flash) */ | ||||
| bool enabled = true;                   // Enable this usermod | ||||
| unsigned long segment_delay_ms = 150;  // Time between switching each segment | ||||
| unsigned long on_time_ms = 5 * 1000;   // The time for the light to stay on | ||||
| #ifndef TOP_PIR_PIN | ||||
| unsigned int topMaxTimeUs = 1749;  // default echo timout, top | ||||
| #endif | ||||
| #ifndef BOTTOM_PIR_PIN | ||||
| unsigned int bottomMaxTimeUs = 1749;  // default echo timout, bottom | ||||
| #endif | ||||
|  | ||||
| // Time between checking of the sensors | ||||
| const int scanDelay = 50; | ||||
|  | ||||
| class Animated_Staircase : public Usermod { | ||||
|  private: | ||||
|   // Lights on or off. | ||||
|   // Flipping this will start a transition. | ||||
|   bool on = false; | ||||
|   private: | ||||
|  | ||||
|   // Swipe direction for current transition | ||||
| #define SWIPE_UP true | ||||
| #define SWIPE_DOWN false | ||||
|   bool swipe = SWIPE_UP; | ||||
|     /* 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 | ||||
|     int8_t topPIRorTriggerPin      = -1;    // disabled | ||||
|     int8_t bottomPIRorTriggerPin   = -1;    // disabled | ||||
|     int8_t topEchoPin              = -1;    // disabled | ||||
|     int8_t bottomEchoPin           = -1;    // disabled | ||||
|     bool useUSSensorTop            = false; // using PIR or UltraSound sensor? | ||||
|     bool useUSSensorBottom         = false; // using PIR or UltraSound sensor? | ||||
|     unsigned int topMaxDist        = 50;    // default maximum measured distance in cm, top | ||||
|     unsigned int bottomMaxDist     = 50;    // default maximum measured distance in cm, bottom | ||||
|  | ||||
|   // Indicates which Sensor was seen last (to determine | ||||
|   // the direction when swiping off) | ||||
| #define LOWER false | ||||
| #define UPPER true | ||||
|   bool lastSensor = LOWER; | ||||
|     /* runtime variables */ | ||||
|     bool initDone = false; | ||||
|  | ||||
|   // Time of the last transition action | ||||
|   unsigned long lastTime = 0; | ||||
|     // Time between checking of the sensors | ||||
|     const unsigned int scanDelay = 100; | ||||
|  | ||||
|   // Time of the last sensor check | ||||
|   unsigned long lastScanTime = 0; | ||||
|     // Lights on or off. | ||||
|     // Flipping this will start a transition. | ||||
|     bool on = false; | ||||
|  | ||||
|   // Last time the lights were switched on or off | ||||
|   unsigned long lastSwitchTime = 0; | ||||
|     // Swipe direction for current transition | ||||
|   #define SWIPE_UP true | ||||
|   #define SWIPE_DOWN false | ||||
|     bool swipe = SWIPE_UP; | ||||
|  | ||||
|   // segment id between onIndex and offIndex are on. | ||||
|   // controll the swipe by setting/moving these indices around. | ||||
|   // onIndex must be less than or equal to offIndex | ||||
|   byte onIndex = 0; | ||||
|   byte offIndex = 0; | ||||
|     // Indicates which Sensor was seen last (to determine | ||||
|     // the direction when swiping off) | ||||
|   #define LOWER false | ||||
|   #define UPPER true | ||||
|     bool lastSensor = LOWER; | ||||
|  | ||||
|   // The maximum number of configured segments. | ||||
|   // Dynamically updated based on user configuration. | ||||
|   byte maxSegmentId = 1; | ||||
|   byte mainSegmentId = 0; | ||||
|     // Time of the last transition action | ||||
|     unsigned long lastTime = 0; | ||||
|  | ||||
|   bool saveState = false; | ||||
|     // Time of the last sensor check | ||||
|     unsigned long lastScanTime = 0; | ||||
|  | ||||
|   // These values are used by the API to read the | ||||
|   // last sensor state, or trigger a sensor | ||||
|   // through the API | ||||
|   bool topSensorRead = false; | ||||
|   bool topSensorWrite = false; | ||||
|   bool bottomSensorRead = false; | ||||
|   bool bottomSensorWrite = false;   | ||||
|     // Last time the lights were switched on or off | ||||
|     unsigned long lastSwitchTime = 0; | ||||
|  | ||||
|   void updateSegments() { | ||||
|     mainSegmentId = strip.getMainSegmentId(); | ||||
|     WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); | ||||
|     WS2812FX::Segment* segments = strip.getSegments(); | ||||
|     for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { | ||||
|       if (!segments->isActive()) { | ||||
|         maxSegmentId = i - 1; | ||||
|         break; | ||||
|       } | ||||
|     // segment id between onIndex and offIndex are on. | ||||
|     // controll the swipe by setting/moving these indices around. | ||||
|     // onIndex must be less than or equal to offIndex | ||||
|     byte onIndex = 0; | ||||
|     byte offIndex = 0; | ||||
|  | ||||
|       if (i >= onIndex && i < offIndex) { | ||||
|         segments->setOption(SEG_OPTION_ON, 1, 1); | ||||
|     // The maximum number of configured segments. | ||||
|     // Dynamically updated based on user configuration. | ||||
|     byte maxSegmentId = 1; | ||||
|     byte mainSegmentId = 0; | ||||
|  | ||||
|         // We may need to copy mode and colors from segment 0 to make sure | ||||
|         // changes are propagated even when the config is changed during a wipe | ||||
|         // segments->mode = mainsegment.mode; | ||||
|         // segments->colors[0] = mainsegment.colors[0]; | ||||
|       } else { | ||||
|         segments->setOption(SEG_OPTION_ON, 0, 1); | ||||
|       } | ||||
|       // Always mark segments as "transitional", we are animating the staircase | ||||
|       segments->setOption(SEG_OPTION_TRANSITIONAL, 1, 1); | ||||
|     } | ||||
|     colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); | ||||
|   } | ||||
|     // These values are used by the API to read the | ||||
|     // last sensor state, or trigger a sensor | ||||
|     // through the API | ||||
|     bool topSensorRead     = false; | ||||
|     bool topSensorWrite    = false; | ||||
|     bool bottomSensorRead  = false; | ||||
|     bool bottomSensorWrite = false; | ||||
|     bool topSensorState    = false; | ||||
|     bool bottomSensorState = false; | ||||
|  | ||||
|   /* | ||||
|    * Detects if an object is within ultrasound range. | ||||
|    * signalPin: The pin where the pulse is sent | ||||
|    * echoPin:   The pin where the echo is received | ||||
|    * maxTimeUs: Detection timeout in microseconds. If an echo is | ||||
|    *            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. | ||||
|    * Since the sound has to travel back and forth, the detection | ||||
|    * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. | ||||
|    * | ||||
|    * For practical reasons, here are some useful distances: | ||||
|    * | ||||
|    * Distance =	maxtime | ||||
|    *     5 cm =  292 uS | ||||
|    *    10 cm =  583 uS | ||||
|    *    20 cm = 1166 uS | ||||
|    *    30 cm = 1749 uS | ||||
|    *    50 cm = 2915 uS | ||||
|    *   100 cm = 5831 uS | ||||
|    */ | ||||
|   bool ultrasoundRead(uint8_t signalPin, | ||||
|                       uint8_t echoPin, | ||||
|                       unsigned int maxTimeUs) { | ||||
|     digitalWrite(signalPin, HIGH); | ||||
|     delayMicroseconds(10); | ||||
|     digitalWrite(signalPin, LOW); | ||||
|     return pulseIn(echoPin, HIGH, maxTimeUs) > 0; | ||||
|   } | ||||
|  | ||||
|   void checkSensors() { | ||||
|     if ((millis() - lastScanTime) > scanDelay) { | ||||
|       lastScanTime = millis(); | ||||
|  | ||||
| #ifdef BOTTOM_PIR_PIN | ||||
|       bottomSensorRead = bottomSensorWrite || (digitalRead(BOTTOM_PIR_PIN) == HIGH); | ||||
| #else | ||||
|       bottomSensorRead = bottomSensorWrite || ultrasoundRead(BOTTOM_TRIGGER_PIN, BOTTOM_ECHO_PIN, bottomMaxTimeUs); | ||||
| #endif | ||||
|  | ||||
| #ifdef TOP_PIR_PIN | ||||
|       topSensorRead = topSensorWrite || (digitalRead(TOP_PIR_PIN) == HIGH); | ||||
| #else | ||||
|       topSensorRead = topSensorWrite || ultrasoundRead(TOP_TRIGGER_PIN, TOP_ECHO_PIN, topMaxTimeUs); | ||||
| #endif | ||||
|  | ||||
|       // Values read, reset the flags for next API call | ||||
|       topSensorWrite = false; | ||||
|       bottomSensorWrite = false; | ||||
|  | ||||
|       if (topSensorRead != bottomSensorRead) { | ||||
|         lastSwitchTime = millis(); | ||||
|  | ||||
|         if (on) { | ||||
|           lastSensor = topSensorRead; | ||||
|         } else { | ||||
|           // If the bottom sensor triggered, we need to swipe up, ON | ||||
|           swipe = bottomSensorRead; | ||||
|  | ||||
|           if (swipe) { | ||||
|             Serial.println("ON -> Swipe up."); | ||||
|           } else { | ||||
|             Serial.println("ON -> Swipe down."); | ||||
|           } | ||||
|  | ||||
|           if (onIndex == offIndex) { | ||||
|             // Position the indices for a correct on-swipe | ||||
|             if (swipe == SWIPE_UP) { | ||||
|               onIndex = mainSegmentId; | ||||
|             } else { | ||||
|               onIndex = maxSegmentId+1; | ||||
|             } | ||||
|             offIndex = onIndex; | ||||
|           } | ||||
|           on = true; | ||||
|         } | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _enabled[]; | ||||
|     static const char _segmentDelay[]; | ||||
|     static const char _onTime[]; | ||||
|     static const char _useTopUltrasoundSensor[]; | ||||
|     static const char _topPIRorTrigger_pin[]; | ||||
|     static const char _topEcho_pin[]; | ||||
|     static const char _useBottomUltrasoundSensor[]; | ||||
|     static const char _bottomPIRorTrigger_pin[]; | ||||
|     static const char _bottomEcho_pin[]; | ||||
|     static const char _topEchoCm[]; | ||||
|     static const char _bottomEchoCm[]; | ||||
|      | ||||
|     void publishMqtt(bool bottom, const char* state) | ||||
|     { | ||||
|       //Check if MQTT Connected, otherwise it will crash the 8266 | ||||
|       if (WLED_MQTT_CONNECTED){ | ||||
|         char subuf[64]; | ||||
|         sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); | ||||
|         mqtt->publish(subuf, 0, false, state); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void autoPowerOff() { | ||||
|     if (on && ((millis() - lastSwitchTime) > on_time_ms)) { | ||||
|       // Swipe OFF in the direction of the last sensor detection | ||||
|       swipe = lastSensor; | ||||
|       on = false; | ||||
|  | ||||
|       if (swipe) { | ||||
|         Serial.println("OFF -> Swipe up."); | ||||
|       } else { | ||||
|         Serial.println("OFF -> Swipe down."); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void updateSwipe() { | ||||
|     if ((millis() - lastTime) > segment_delay_ms) { | ||||
|       lastTime = millis(); | ||||
|  | ||||
|       byte oldOnIndex = onIndex; | ||||
|       byte oldOffIndex = offIndex; | ||||
|  | ||||
|       if (on) { | ||||
|         // Turn on all segments | ||||
|         onIndex = MAX(mainSegmentId, onIndex - 1); | ||||
|         offIndex = MIN(maxSegmentId + 1, offIndex + 1); | ||||
|       } else { | ||||
|         if (swipe == SWIPE_UP) { | ||||
|           onIndex = MIN(offIndex, onIndex + 1); | ||||
|         } else { | ||||
|           offIndex = MAX(onIndex, offIndex - 1); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       updateSegments(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void writeSettingsToJson(JsonObject& root) { | ||||
|     JsonObject staircase = root["staircase"]; | ||||
|     if (staircase.isNull()) { | ||||
|       staircase = root.createNestedObject("staircase"); | ||||
|     } | ||||
|     staircase["enabled"] = enabled; | ||||
|     staircase["segment-delay-ms"] = segment_delay_ms; | ||||
|     staircase["on-time-s"] = on_time_ms / 1000; | ||||
|  | ||||
| #ifdef TOP_TRIGGER_PIN | ||||
|     staircase["top-echo-us"] = topMaxTimeUs; | ||||
| #endif | ||||
| #ifdef BOTTOM_TRIGGER_PIN | ||||
|     staircase["bottom-echo-us"] = bottomMaxTimeUs; | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   void writeSensorsToJson(JsonObject& root) { | ||||
|     JsonObject staircase = root["staircase"]; | ||||
|     if (staircase.isNull()) { | ||||
|       staircase = root.createNestedObject("staircase"); | ||||
|     } | ||||
|     staircase["top-sensor"] = topSensorRead; | ||||
|     staircase["bottom-sensor"] = bottomSensorRead; | ||||
|   } | ||||
|  | ||||
|   bool readSettingsFromJson(JsonObject& root) { | ||||
|     JsonObject staircase = root["staircase"]; | ||||
|     bool changed = false; | ||||
|  | ||||
|     bool shouldEnable = staircase["enabled"] | enabled; | ||||
|     if (shouldEnable != enabled) { | ||||
|       enable(shouldEnable); | ||||
|       changed = true; | ||||
|     } | ||||
|  | ||||
|     unsigned long c_segment_delay_ms = staircase["segment-delay-ms"] | segment_delay_ms; | ||||
|     if (c_segment_delay_ms != segment_delay_ms) { | ||||
|       segment_delay_ms = c_segment_delay_ms; | ||||
|       changed = true; | ||||
|     } | ||||
|  | ||||
|     unsigned long c_on_time_ms = (staircase["on-time-s"] | (on_time_ms / 1000)) * 1000; | ||||
|     if (c_on_time_ms != on_time_ms) { | ||||
|       on_time_ms = c_on_time_ms; | ||||
|       changed = true; | ||||
|     } | ||||
|  | ||||
| #ifdef TOP_TRIGGER_PIN | ||||
|     unsigned int c_topMaxTimeUs = staircase["top-echo-us"] | topMaxTimeUs; | ||||
|     if (c_topMaxTimeUs != topMaxTimeUs) { | ||||
|       topMaxTimeUs = c_topMaxTimeUs; | ||||
|       changed = true; | ||||
|     } | ||||
| #endif | ||||
| #ifdef BOTTOM_TRIGGER_PIN | ||||
|     unsigned int c_bottomMaxTimeUs = staircase["bottom-echo-us"] | bottomMaxTimeUs; | ||||
|     if (c_bottomMaxTimeUs != bottomMaxTimeUs) { | ||||
|       bottomMaxTimeUs = c_bottomMaxTimeUs; | ||||
|       changed = true; | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     return changed; | ||||
|   } | ||||
|  | ||||
|   void readSensorsFromJson(JsonObject& root) { | ||||
|     JsonObject staircase = root["staircase"]; | ||||
|     bottomSensorWrite = bottomSensorRead || (staircase["bottom-sensor"].as<bool>()); | ||||
|     topSensorWrite = topSensorRead || (staircase["top-sensor"].as<bool>()); | ||||
|   } | ||||
|  | ||||
|   void enable(bool enable) { | ||||
|     if (enable) { | ||||
|       Serial.println("Animated Staircase enabled."); | ||||
|       Serial.print("Delay between steps: "); | ||||
|       Serial.print(segment_delay_ms, DEC); | ||||
|       Serial.print(" milliseconds.\nStairs switch off after: "); | ||||
|       Serial.print(on_time_ms / 1000, DEC); | ||||
|       Serial.println(" seconds."); | ||||
|  | ||||
| #ifdef BOTTOM_PIR_PIN | ||||
|       pinMode(BOTTOM_PIR_PIN, INPUT); | ||||
| #else | ||||
|       pinMode(BOTTOM_TRIGGER_PIN, OUTPUT); | ||||
|       pinMode(BOTTOM_ECHO_PIN, INPUT); | ||||
| #endif | ||||
|  | ||||
| #ifdef TOP_PIR_PIN | ||||
|       pinMode(TOP_PIR_PIN, INPUT); | ||||
| #else | ||||
|       pinMode(TOP_TRIGGER_PIN, OUTPUT); | ||||
|       pinMode(TOP_ECHO_PIN, INPUT); | ||||
| #endif | ||||
|     } else { | ||||
|       // Restore segment options | ||||
|       WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); | ||||
|     void updateSegments() { | ||||
|       mainSegmentId = strip.getMainSegmentId(); | ||||
|       WS2812FX::Segment* segments = strip.getSegments(); | ||||
|       for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { | ||||
|         if (!segments->isActive()) { | ||||
|           maxSegmentId = i - 1; | ||||
|           break; | ||||
|         } | ||||
|         segments->setOption(SEG_OPTION_ON, 1, 1); | ||||
|  | ||||
|         if (i >= onIndex && i < offIndex) { | ||||
|           segments->setOption(SEG_OPTION_ON, 1, 1); | ||||
|  | ||||
|           // We may need to copy mode and colors from segment 0 to make sure | ||||
|           // changes are propagated even when the config is changed during a wipe | ||||
|           // segments->mode = mainsegment.mode; | ||||
|           // segments->colors[0] = mainsegment.colors[0]; | ||||
|         } else { | ||||
|           segments->setOption(SEG_OPTION_ON, 0, 1); | ||||
|         } | ||||
|         // Always mark segments as "transitional", we are animating the staircase | ||||
|         segments->setOption(SEG_OPTION_TRANSITIONAL, 1, 1); | ||||
|       } | ||||
|       colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); | ||||
|       Serial.println("Animated Staircase disabled."); | ||||
|     } | ||||
|     enabled = enable; | ||||
|   } | ||||
|  | ||||
|  public: | ||||
|   void setup() { enable(enabled); } | ||||
|  | ||||
|   void loop() { | ||||
|     // Write changed settings from to flash (see readFromJsonState()) | ||||
|     if (saveState) { | ||||
|       serializeConfig(); | ||||
|       saveState = false; | ||||
|       colorUpdated(CALL_MODE_DIRECT_CHANGE); | ||||
|     } | ||||
|  | ||||
|     if (!enabled) { | ||||
|       return; | ||||
|     /* | ||||
|     * Detects if an object is within ultrasound range. | ||||
|     * signalPin: The pin where the pulse is sent | ||||
|     * echoPin:   The pin where the echo is received | ||||
|     * maxTimeUs: Detection timeout in microseconds. If an echo is | ||||
|     *            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. | ||||
|     * Since the sound has to travel back and forth, the detection | ||||
|     * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. | ||||
|     * | ||||
|     * For practical reasons, here are some useful distances: | ||||
|     * | ||||
|     * Distance =	maxtime | ||||
|     *     5 cm =  292 uS | ||||
|     *    10 cm =  583 uS | ||||
|     *    20 cm = 1166 uS | ||||
|     *    30 cm = 1749 uS | ||||
|     *    50 cm = 2915 uS | ||||
|     *   100 cm = 5831 uS | ||||
|     */ | ||||
|     bool ultrasoundRead(int8_t signalPin, int8_t echoPin, unsigned int maxTimeUs) { | ||||
|       if (signalPin<0 || echoPin<0) return false; | ||||
|       digitalWrite(signalPin, LOW); | ||||
|       delayMicroseconds(2); | ||||
|       digitalWrite(signalPin, HIGH); | ||||
|       delayMicroseconds(10); | ||||
|       digitalWrite(signalPin, LOW); | ||||
|       return pulseIn(echoPin, HIGH, maxTimeUs) > 0; | ||||
|     } | ||||
|  | ||||
|     checkSensors(); | ||||
|     autoPowerOff(); | ||||
|     updateSwipe(); | ||||
|     bool checkSensors() { | ||||
|       bool sensorChanged = false; | ||||
|  | ||||
|   } | ||||
|       if ((millis() - lastScanTime) > scanDelay) { | ||||
|         lastScanTime = millis(); | ||||
|  | ||||
|   uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } | ||||
|         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 | ||||
|           ); | ||||
|  | ||||
|   /* | ||||
|    * Shows configuration settings to the json API. This object looks like: | ||||
|    * | ||||
|    * "staircase" : { | ||||
|    *   "enabled" : true | ||||
|    *   "segment-delay-ms" : 150, | ||||
|    *   "on-time-s" : 5 | ||||
|    * } | ||||
|    * | ||||
|    */ | ||||
|   void addToJsonState(JsonObject& root) { | ||||
|     writeSettingsToJson(root); | ||||
|     writeSensorsToJson(root); | ||||
|     Serial.println("Staircase config exposed in API."); | ||||
|   } | ||||
|         if (bottomSensorRead != bottomSensorState) { | ||||
|           bottomSensorState = bottomSensorRead; // change previous state | ||||
|           sensorChanged = true; | ||||
|           publishMqtt(true, bottomSensorState ? "on" : "off"); | ||||
|           DEBUG_PRINTLN(F("Bottom sensor changed.")); | ||||
|         } | ||||
|  | ||||
|   /* | ||||
|    * Reads configuration settings from the json API. | ||||
|    * See void addToJsonState(JsonObject& root) | ||||
|    */ | ||||
|   void readFromJsonState(JsonObject& root) { | ||||
|     // The call to serializeConfig() must be done in the main loop, | ||||
|     // so we set a flag to signal the main loop to save state. | ||||
|     saveState = readSettingsFromJson(root); | ||||
|     readSensorsFromJson(root); | ||||
|     Serial.println("Staircase config read from API."); | ||||
|   } | ||||
|         if (topSensorRead != topSensorState) { | ||||
|           topSensorState = topSensorRead; // change previous state | ||||
|           sensorChanged = true; | ||||
|           publishMqtt(false, topSensorState ? "on" : "off"); | ||||
|           DEBUG_PRINTLN(F("Top sensor changed.")); | ||||
|         } | ||||
|  | ||||
|   /* | ||||
|    * Writes the configuration to internal flash memory. | ||||
|    */ | ||||
|   void addToConfig(JsonObject& root) { | ||||
|     writeSettingsToJson(root); | ||||
|     Serial.println("Staircase config saved."); | ||||
|   } | ||||
|         // Values read, reset the flags for next API call | ||||
|         topSensorWrite = false; | ||||
|         bottomSensorWrite = false; | ||||
|  | ||||
|   /* | ||||
|    * Reads the configuration to internal flash memory before setup() is called. | ||||
|    */ | ||||
|   void readFromConfig(JsonObject& root) { | ||||
|     readSettingsFromJson(root); | ||||
|     Serial.println("Staircase config loaded."); | ||||
|   } | ||||
|         if (topSensorRead != bottomSensorRead) { | ||||
|           lastSwitchTime = millis(); | ||||
|  | ||||
|   /* | ||||
|    * Shows the delay between steps and power-off time in the "info" | ||||
|    * tab of the web-UI. | ||||
|    */ | ||||
|   void addToJsonInfo(JsonObject& root) { | ||||
|     JsonObject staircase = root["u"]; | ||||
|     if (staircase.isNull()) { | ||||
|       staircase = root.createNestedObject("u"); | ||||
|           if (on) { | ||||
|             lastSensor = topSensorRead; | ||||
|           } else { | ||||
|             // If the bottom sensor triggered, we need to swipe up, ON | ||||
|             swipe = bottomSensorRead; | ||||
|  | ||||
|             DEBUG_PRINT(F("ON -> Swipe ")); | ||||
|             DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); | ||||
|  | ||||
|             if (onIndex == offIndex) { | ||||
|               // Position the indices for a correct on-swipe | ||||
|               if (swipe == SWIPE_UP) { | ||||
|                 onIndex = mainSegmentId; | ||||
|               } else { | ||||
|                 onIndex = maxSegmentId+1; | ||||
|               } | ||||
|               offIndex = onIndex; | ||||
|             } | ||||
|             on = true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return sensorChanged; | ||||
|     } | ||||
|  | ||||
|     if (enabled) { | ||||
|       JsonArray usermodEnabled = | ||||
|           staircase.createNestedArray("Staircase enabled");  // name | ||||
|       usermodEnabled.add("yes");                             // value | ||||
|     void autoPowerOff() { | ||||
|       if (on && ((millis() - lastSwitchTime) > on_time_ms)) { | ||||
|         // if sensors are still on, do nothing | ||||
|         if (bottomSensorState || topSensorState) return; | ||||
|  | ||||
|       JsonArray segmentDelay = | ||||
|           staircase.createNestedArray("Delay between stairs");  // name | ||||
|       segmentDelay.add(segment_delay_ms);                       // value | ||||
|       segmentDelay.add(" milliseconds");                        // unit | ||||
|         // Swipe OFF in the direction of the last sensor detection | ||||
|         swipe = lastSensor; | ||||
|         on = false; | ||||
|  | ||||
|       JsonArray onTime = | ||||
|           staircase.createNestedArray("Power-off stairs after");  // name | ||||
|       onTime.add(on_time_ms / 1000);                              // value | ||||
|       onTime.add(" seconds");                                     // unit | ||||
|     } else { | ||||
|       JsonArray usermodEnabled = | ||||
|           staircase.createNestedArray("Staircase enabled");  // name | ||||
|       usermodEnabled.add("no");                              // value | ||||
|         DEBUG_PRINT(F("OFF -> Swipe ")); | ||||
|         DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
|     void updateSwipe() { | ||||
|       if ((millis() - lastTime) > segment_delay_ms) { | ||||
|         lastTime = millis(); | ||||
|  | ||||
|         if (on) { | ||||
|           // Turn on all segments | ||||
|           onIndex = MAX(mainSegmentId, onIndex - 1); | ||||
|           offIndex = MIN(maxSegmentId + 1, offIndex + 1); | ||||
|         } else { | ||||
|           if (swipe == SWIPE_UP) { | ||||
|             onIndex = MIN(offIndex, onIndex + 1); | ||||
|           } else { | ||||
|             offIndex = MAX(onIndex, offIndex - 1); | ||||
|           } | ||||
|         } | ||||
|         updateSegments(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // send sesnor values to JSON API | ||||
|     void writeSensorsToJson(JsonObject& staircase) { | ||||
|       staircase[F("top-sensor")]    = topSensorRead; | ||||
|       staircase[F("bottom-sensor")] = bottomSensorRead; | ||||
|     } | ||||
|  | ||||
|     // allow overrides from JSON API | ||||
|     void readSensorsFromJson(JsonObject& staircase) { | ||||
|       bottomSensorWrite = bottomSensorState || (staircase[F("bottom-sensor")].as<bool>()); | ||||
|       topSensorWrite    = topSensorState    || (staircase[F("top-sensor")].as<bool>()); | ||||
|     } | ||||
|  | ||||
|     void enable(bool enable) { | ||||
|       if (enable) { | ||||
|         DEBUG_PRINTLN(F("Animated Staircase enabled.")); | ||||
|         DEBUG_PRINT(F("Delay between steps: ")); | ||||
|         DEBUG_PRINT(segment_delay_ms); | ||||
|         DEBUG_PRINT(F(" milliseconds.\nStairs switch off after: ")); | ||||
|         DEBUG_PRINT(on_time_ms / 1000); | ||||
|         DEBUG_PRINTLN(F(" seconds.")); | ||||
|  | ||||
|         if (!useUSSensorBottom) | ||||
|           pinMode(bottomPIRorTriggerPin, INPUT_PULLUP); | ||||
|         else { | ||||
|           pinMode(bottomPIRorTriggerPin, OUTPUT); | ||||
|           pinMode(bottomEchoPin, INPUT); | ||||
|         } | ||||
|  | ||||
|         if (!useUSSensorTop) | ||||
|           pinMode(topPIRorTriggerPin, INPUT_PULLUP); | ||||
|         else { | ||||
|           pinMode(topPIRorTriggerPin, OUTPUT); | ||||
|           pinMode(topEchoPin, INPUT); | ||||
|         } | ||||
|       } else { | ||||
|         // Restore segment options | ||||
|         WS2812FX::Segment* segments = strip.getSegments(); | ||||
|         for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { | ||||
|           if (!segments->isActive()) { | ||||
|             maxSegmentId = i - 1; | ||||
|             break; | ||||
|           } | ||||
|           segments->setOption(SEG_OPTION_ON, 1, 1); | ||||
|         } | ||||
|         colorUpdated(CALL_MODE_DIRECT_CHANGE); | ||||
|         DEBUG_PRINTLN(F("Animated Staircase disabled.")); | ||||
|       } | ||||
|       enabled = enable; | ||||
|     } | ||||
|  | ||||
|   public: | ||||
|     void setup() { | ||||
|       // standardize invalid pin numbers to -1 | ||||
|       if (topPIRorTriggerPin    < 0) topPIRorTriggerPin    = -1; | ||||
|       if (topEchoPin            < 0) topEchoPin            = -1; | ||||
|       if (bottomPIRorTriggerPin < 0) bottomPIRorTriggerPin = -1; | ||||
|       if (bottomEchoPin         < 0) bottomEchoPin         = -1; | ||||
|       // allocate pins | ||||
|       PinManagerPinType pins[4] = { | ||||
|         { topPIRorTriggerPin, useUSSensorTop }, | ||||
|         { topEchoPin, false }, | ||||
|         { bottomPIRorTriggerPin, useUSSensorBottom }, | ||||
|         { bottomEchoPin, false }, | ||||
|       }; | ||||
|       // NOTE: this *WILL* return TRUE if all the pins are set to -1. | ||||
|       //       this is *BY DESIGN*. | ||||
|       if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { | ||||
|         topPIRorTriggerPin = -1; | ||||
|         topEchoPin = -1; | ||||
|         bottomPIRorTriggerPin = -1; | ||||
|         bottomEchoPin = -1; | ||||
|         enabled = false; | ||||
|       } | ||||
|       enable(enabled); | ||||
|       initDone = true; | ||||
|     } | ||||
|  | ||||
|     void loop() { | ||||
|       if (!enabled || strip.isUpdating()) return; | ||||
|       checkSensors(); | ||||
|       autoPowerOff(); | ||||
|       updateSwipe(); | ||||
|     } | ||||
|  | ||||
|     uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } | ||||
|  | ||||
|     /** | ||||
|      * handling of MQTT message | ||||
|      * topic only contains stripped topic (part after /wled/MAC) | ||||
|      * topic should look like: /swipe with amessage of [up|down] | ||||
|      */ | ||||
|     bool onMqttMessage(char* topic, char* payload) { | ||||
|       if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/swipe"), 6) == 0) { | ||||
|         String action = payload; | ||||
|         if (action == "up") { | ||||
|           bottomSensorWrite = true; | ||||
|           return true; | ||||
|         } else if (action == "down") { | ||||
|           topSensorWrite = true; | ||||
|           return true; | ||||
|         } else if (action == "on") { | ||||
|           enable(true); | ||||
|           return true; | ||||
|         } else if (action == "off") { | ||||
|           enable(false); | ||||
|           return true; | ||||
|         } | ||||
|       } | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * subscribe to MQTT topic for controlling usermod | ||||
|      */ | ||||
|     void onMqttConnect(bool sessionPresent) { | ||||
|       //(re)subscribe to required topics | ||||
|       char subuf[64]; | ||||
|       if (mqttDeviceTopic[0] != 0) { | ||||
|         strcpy(subuf, mqttDeviceTopic); | ||||
|         strcat_P(subuf, PSTR("/swipe")); | ||||
|         mqtt->subscribe(subuf, 0); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void addToJsonState(JsonObject& root) { | ||||
|       JsonObject staircase = root[FPSTR(_name)]; | ||||
|       if (staircase.isNull()) { | ||||
|         staircase = root.createNestedObject(FPSTR(_name)); | ||||
|       } | ||||
|       writeSensorsToJson(staircase); | ||||
|       DEBUG_PRINTLN(F("Staircase sensor state exposed in API.")); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|     * Reads configuration settings from the json API. | ||||
|     * See void addToJsonState(JsonObject& root) | ||||
|     */ | ||||
|     void readFromJsonState(JsonObject& root) { | ||||
|       if (!initDone) return;  // prevent crash on boot applyPreset() | ||||
|       JsonObject staircase = root[FPSTR(_name)]; | ||||
|       if (!staircase.isNull()) { | ||||
|         if (staircase[FPSTR(_enabled)].is<bool>()) { | ||||
|           enabled   = staircase[FPSTR(_enabled)].as<bool>(); | ||||
|         } else { | ||||
|           String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on | ||||
|           enabled = (bool)(str!="off"); // off is guaranteed to be present | ||||
|         } | ||||
|         readSensorsFromJson(staircase); | ||||
|         DEBUG_PRINTLN(F("Staircase sensor state read from API.")); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|     * Writes the configuration to internal flash memory. | ||||
|     */ | ||||
|     void addToConfig(JsonObject& root) { | ||||
|       JsonObject staircase = root[FPSTR(_name)]; | ||||
|       if (staircase.isNull()) { | ||||
|         staircase = root.createNestedObject(FPSTR(_name)); | ||||
|       } | ||||
|       staircase[FPSTR(_enabled)]                   = enabled; | ||||
|       staircase[FPSTR(_segmentDelay)]              = segment_delay_ms; | ||||
|       staircase[FPSTR(_onTime)]                    = on_time_ms / 1000; | ||||
|       staircase[FPSTR(_useTopUltrasoundSensor)]    = useUSSensorTop; | ||||
|       staircase[FPSTR(_topPIRorTrigger_pin)]       = topPIRorTriggerPin; | ||||
|       staircase[FPSTR(_topEcho_pin)]               = useUSSensorTop ? topEchoPin : -1; | ||||
|       staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom; | ||||
|       staircase[FPSTR(_bottomPIRorTrigger_pin)]    = bottomPIRorTriggerPin; | ||||
|       staircase[FPSTR(_bottomEcho_pin)]            = useUSSensorBottom ? bottomEchoPin : -1; | ||||
|       staircase[FPSTR(_topEchoCm)]                 = topMaxDist; | ||||
|       staircase[FPSTR(_bottomEchoCm)]              = bottomMaxDist; | ||||
|       DEBUG_PRINTLN(F("Staircase config saved.")); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|     * Reads the configuration to internal flash memory before setup() is called. | ||||
|     *  | ||||
|     * The function should return true if configuration was successfully loaded or false if there was no configuration. | ||||
|     */ | ||||
|     bool readFromConfig(JsonObject& root) { | ||||
|       bool oldUseUSSensorTop = useUSSensorTop; | ||||
|       bool oldUseUSSensorBottom = useUSSensorBottom; | ||||
|       int8_t oldTopAPin = topPIRorTriggerPin; | ||||
|       int8_t oldTopBPin = topEchoPin; | ||||
|       int8_t oldBottomAPin = bottomPIRorTriggerPin; | ||||
|       int8_t oldBottomBPin = bottomEchoPin; | ||||
|  | ||||
|       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; | ||||
|  | ||||
|       segment_delay_ms = top[FPSTR(_segmentDelay)] | segment_delay_ms; | ||||
|       segment_delay_ms = (unsigned long) min((unsigned long)10000,max((unsigned long)10,(unsigned long)segment_delay_ms));  // max delay 10s | ||||
|  | ||||
|       on_time_ms = top[FPSTR(_onTime)] | on_time_ms/1000; | ||||
|       on_time_ms = min(900,max(10,(int)on_time_ms)) * 1000; // min 10s, max 15min | ||||
|  | ||||
|       useUSSensorTop     = top[FPSTR(_useTopUltrasoundSensor)] | useUSSensorTop; | ||||
|       topPIRorTriggerPin = top[FPSTR(_topPIRorTrigger_pin)] | topPIRorTriggerPin; | ||||
|       topEchoPin         = top[FPSTR(_topEcho_pin)] | topEchoPin; | ||||
|  | ||||
|       useUSSensorBottom     = top[FPSTR(_useBottomUltrasoundSensor)] | useUSSensorBottom; | ||||
|       bottomPIRorTriggerPin = top[FPSTR(_bottomPIRorTrigger_pin)] | bottomPIRorTriggerPin; | ||||
|       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) | ||||
|       bottomMaxDist = top[FPSTR(_bottomEchoCm)] | bottomMaxDist; | ||||
|       bottomMaxDist = min(150,max(30,(int)bottomMaxDist));  // max distance ~1.5m (a lag of 9ms may be expected) | ||||
|  | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       if (!initDone) { | ||||
|         // first run: reading from cfg.json | ||||
|         DEBUG_PRINTLN(F(" config loaded.")); | ||||
|       } else { | ||||
|         // changing parameters from settings page | ||||
|         DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|         bool changed = false; | ||||
|         if ((oldUseUSSensorTop != useUSSensorTop) || | ||||
|             (oldUseUSSensorBottom != useUSSensorBottom) || | ||||
|             (oldTopAPin != topPIRorTriggerPin) || | ||||
|             (oldTopBPin != topEchoPin) || | ||||
|             (oldBottomAPin != bottomPIRorTriggerPin) || | ||||
|             (oldBottomBPin != bottomEchoPin)) { | ||||
|           changed = true; | ||||
|           pinManager.deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); | ||||
|           pinManager.deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); | ||||
|           pinManager.deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); | ||||
|           pinManager.deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); | ||||
|         } | ||||
|         if (changed) setup(); | ||||
|       } | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|     * Shows the delay between steps and power-off time in the "info" | ||||
|     * tab of the web-UI. | ||||
|     */ | ||||
|     void addToJsonInfo(JsonObject& root) { | ||||
|       JsonObject staircase = root["u"]; | ||||
|       if (staircase.isNull()) { | ||||
|         staircase = root.createNestedObject("u"); | ||||
|       } | ||||
|  | ||||
|       JsonArray usermodEnabled = staircase.createNestedArray(F("Staircase"));  // name | ||||
|       String btn = F("<button class=\"btn infobtn\" onclick=\"requestJson({staircase:{enabled:"); | ||||
|       if (enabled) { | ||||
|         btn += F("false}},false,false);loadInfo();\">"); | ||||
|         btn += F("enabled"); | ||||
|       } else { | ||||
|         btn += F("true}},false,false);loadInfo();\">"); | ||||
|         btn += F("disabled"); | ||||
|       } | ||||
|       btn += F("</button>"); | ||||
|       usermodEnabled.add(btn);                             // value | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char Animated_Staircase::_name[]                      PROGMEM = "staircase"; | ||||
| const char Animated_Staircase::_enabled[]                   PROGMEM = "enabled"; | ||||
| const char Animated_Staircase::_segmentDelay[]              PROGMEM = "segment-delay-ms"; | ||||
| const char Animated_Staircase::_onTime[]                    PROGMEM = "on-time-s"; | ||||
| const char Animated_Staircase::_useTopUltrasoundSensor[]    PROGMEM = "useTopUltrasoundSensor"; | ||||
| const char Animated_Staircase::_topPIRorTrigger_pin[]       PROGMEM = "topPIRorTrigger_pin"; | ||||
| const char Animated_Staircase::_topEcho_pin[]               PROGMEM = "topEcho_pin"; | ||||
| const char Animated_Staircase::_useBottomUltrasoundSensor[] PROGMEM = "useBottomUltrasoundSensor"; | ||||
| const char Animated_Staircase::_bottomPIRorTrigger_pin[]    PROGMEM = "bottomPIRorTrigger_pin"; | ||||
| const char Animated_Staircase::_bottomEcho_pin[]            PROGMEM = "bottomEcho_pin"; | ||||
| const char Animated_Staircase::_topEchoCm[]                 PROGMEM = "top-dist-cm"; | ||||
| const char Animated_Staircase::_bottomEchoCm[]              PROGMEM = "bottom-dist-cm"; | ||||
|   | ||||
| @@ -1,21 +0,0 @@ | ||||
| /* | ||||
|  * Animated_Staircase compiletime confguration. | ||||
|  * | ||||
|  * Please see README.md on how to change this file. | ||||
|  */ | ||||
|  | ||||
| // Please change the pin numbering below to match your board. | ||||
| #define TOP_PIR_PIN D5 | ||||
| #define BOTTOM_PIR_PIN D6 | ||||
|  | ||||
| // Or uncumment and a pir and use an ultrasound HC-SR04 sensor, | ||||
| // see README.md for details | ||||
| #ifndef TOP_PIR_PIN | ||||
| #define TOP_TRIGGER_PIN D2 | ||||
| #define TOP_ECHO_PIN D3 | ||||
| #endif | ||||
|  | ||||
| #ifndef BOTTOM_PIR_PIN | ||||
| #define BOTTOM_TRIGGER_PIN D4 | ||||
| #define BOTTOM_ECHO_PIN D5 | ||||
| #endif | ||||
| @@ -20,44 +20,10 @@ Edit `usermods_list.cpp`: | ||||
| 2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file | ||||
| 3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. | ||||
|  | ||||
| Edit `Animated_Staircase_config.h`: | ||||
| 1. Open `usermods/Animated_Staircase/Animated_Staircase_config.h`  | ||||
| 2. To use PIR sensors, change these lines to match your setup: | ||||
|    Using D7 and D6 pin notation as used on several boards: | ||||
|    | ||||
|    ```cpp | ||||
|      #define TOP_PIR_PIN    D7 | ||||
|      #define BOTTOM_PIR_PIN D6 | ||||
|    ``` | ||||
|     | ||||
|    Or using GPIO numbering for pins 25 and 26: | ||||
|    ```cpp | ||||
|      #define TOP_PIR_PIN    26 | ||||
|      #define BOTTOM_PIR_PIN 25 | ||||
|    ``` | ||||
|  | ||||
|    To use Ultrasonic HC-SR04 sensors instead of (one of the) PIR sensors, | ||||
|    uncomment one of the PIR sensor lines and adjust the pin numbers for the | ||||
|    connected Ultrasonic sensor. In the example below we use an Ultrasonic | ||||
|    sensor at the bottom of the stairs: | ||||
|  | ||||
|    ```cpp | ||||
|    #define TOP_PIR_PIN 32 | ||||
|    //#define BOTTOM_PIR_PIN D6 /* This PIR sensor is disabled   */ | ||||
|  | ||||
|    #ifndef TOP_PIR_PIN | ||||
|    #define TOP_SIGNAL_PIN D2 | ||||
|    #define TOP_ECHO_PIN   D3 | ||||
|    #endif | ||||
|  | ||||
|    #ifndef BOTTOM_PIR_PIN      /* If the bottom PIR is disabled, */ | ||||
|    #define BOTTOM_SIGNAL_PIN 25 /* This Ultrasonic sensor is used */ | ||||
|    #define BOTTOM_ECHO_PIN   26 | ||||
|    #endif | ||||
|    ``` | ||||
|  | ||||
| After these modifications, compile and upload your WLED binary to your board | ||||
| and check the WLED info page to see if this usermod is enabled. | ||||
| You can configure usermod using Usermods settings page. | ||||
| Please enter GPIO pins for PIR sensors or ultrasonic sensor (trigger and echo). | ||||
| If you use PIR sensor enter -1 for echo pin. | ||||
| Maximum distance for ultrasonic sensor can be configured as a time needed for echo (see below). | ||||
|  | ||||
| ## Hardware installation | ||||
| 1. Stick the LED strip under each step of the stairs. | ||||
| @@ -90,11 +56,8 @@ or remove them and put everything on one line. | ||||
| | Setting          | Description                                                   | Default | | ||||
| |------------------|---------------------------------------------------------------|---------| | ||||
| | enabled          | Enable or disable the usermod                                 | true    | | ||||
| | segment-delay-ms | Delay (milliseconds) between switching on/off each step       | 150     | | ||||
| | on-time-s        | Time (seconds) the stairs stay lit after last detection       | 5       | | ||||
| | bottom-echo-us   | Detection range of ultrasonic sensor                          | 1749    | | ||||
| | bottomsensor     | Manually trigger a down to up animation via API               | false   |  | ||||
| | topsensor        | Manually trigger an up to down animation via API              | false   | | ||||
| | bottom-sensor    | Manually trigger a down to up animation via API               | false   |  | ||||
| | top-sensor       | Manually trigger an up to down animation via API              | false   | | ||||
|  | ||||
|  | ||||
| To read the current settings, open a browser to `http://xxx.xxx.xxx.xxx/json/state` (use your WLED  | ||||
| @@ -106,10 +69,8 @@ The staircase settings and sensor states are inside the WLED status element: | ||||
|     "state": { | ||||
|         "staircase": { | ||||
|             "enabled": true, | ||||
|             "segment-delay-ms": 150, | ||||
|             "on-time-s": 5, | ||||
|             "bottomsensor": false, | ||||
|             "topsensor": false | ||||
|             "bottom-sensor": false, | ||||
|             "tops-ensor": false | ||||
|         }, | ||||
| } | ||||
| ``` | ||||
| @@ -128,58 +89,16 @@ curl -X POST -H "Content-Type: application/json" \ | ||||
|  | ||||
| To enable the usermod again, use `"enabled":true`. | ||||
|  | ||||
| ### Changing animation parameters | ||||
| To change the delay between the steps to (for example) 100 milliseconds and the on-time to | ||||
| 10 seconds: | ||||
| Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. | ||||
|  | ||||
| ```bash | ||||
| curl -X POST -H "Content-Type: application/json" \ | ||||
|      -d '{"staircase":{"segment-delay-ms":100,"on-time-s":10}}' \ | ||||
|      xxx.xxx.xxx.xxx/json/state | ||||
| ``` | ||||
| ### 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 and so on. | ||||
|  | ||||
| ### Changing detection range of the ultrasonic HC-SR04 sensor | ||||
| When an ultrasonic sensor is enabled in `Animated_Staircase_config.h`, you'll see a  | ||||
| `bottom-echo-us` setting appear in the json api: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "state": { | ||||
|         "staircase": { | ||||
|             "enabled": true, | ||||
|             "segment-delay-ms": 150, | ||||
|             "on-time-s": 5, | ||||
|             "bottom-echo-us": 1749 | ||||
|         }, | ||||
| } | ||||
| ``` | ||||
|  | ||||
| If the HC-SR04 sensor detects an echo within 1749 microseconds (corresponding to ~30 cm  | ||||
| detection range from the sensor), it will trigger switching on the staircase. This setting  | ||||
| can be changed through the API with an HTTP POST: | ||||
|  | ||||
| ```bash | ||||
| curl -X POST -H "Content-Type: application/json" \ | ||||
|      -d '{"staircase":{"bottom-echo-us":1166}}' \ | ||||
|      xxx.xxx.xxx.xxx/json/state | ||||
| ``` | ||||
|  | ||||
| Calculating the detection range can be performed as follows: The speed of sound is 343m/s at 20  | ||||
| degrees Centigrade. Since the sound has to travel back and forth, the detection range for the | ||||
| sensor in cm is (0.0343 * maxTimeUs) / 2. To get you started, please find delays and distances below: | ||||
|  | ||||
| | Distance | Detection time  | | ||||
| |---------:|----------------:| | ||||
| |     5 cm |          292 uS | | ||||
| |    10 cm |          583 uS | | ||||
| |    20 cm |         1166 uS | | ||||
| |    30 cm |         1749 uS | | ||||
| |    50 cm |         2915 uS | | ||||
| |   100 cm |         5831 uS | | ||||
| When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. | ||||
|  | ||||
| **Please note:** that using an HC-SR04 sensor, particularly when detecting echos at longer | ||||
| distances creates delays in the WLED software, and _might_ introduce timing hickups in your animations or | ||||
| a less responsive web interface. It is therefore advised to keep the detection time as short as possible. | ||||
| a less responsive web interface. It is therefore advised to keep the detection distance as short as possible. | ||||
|  | ||||
| ### Animation triggering through the API | ||||
| Instead of stairs activation by one of the sensors, you can also trigger the animation through | ||||
| @@ -187,7 +106,7 @@ the API. To simulate triggering the bottom sensor, use: | ||||
|  | ||||
| ```bash | ||||
| curl -X POST -H "Content-Type: application/json" \ | ||||
|      -d '{"staircase":{"bottomsensor":true}}' \ | ||||
|      -d '{"staircase":{"bottom-sensor":true}}' \ | ||||
|      xxx.xxx.xxx.xxx/json/state | ||||
| ``` | ||||
|  | ||||
| @@ -195,9 +114,18 @@ Likewise, to trigger the top sensor, use: | ||||
|  | ||||
| ```bash | ||||
| curl -X POST -H "Content-Type: application/json" \ | ||||
|      -d '{"staircase":{"topsensor":true}}' \ | ||||
|      -d '{"staircase":{"top-sensor":true}}' \ | ||||
|      xxx.xxx.xxx.xxx/json/state | ||||
| ``` | ||||
| **MQTT** | ||||
| You can publish a message with either `up` or `down` on topic `/swipe` to trigger animation. | ||||
| You can also use `on` or `off` for enabling or disabling usermod. | ||||
|  | ||||
| Have fun with this usermod.<br/> | ||||
| www.rolfje.com | ||||
|  | ||||
| Modifications @blazoncek | ||||
|  | ||||
| ## Change log | ||||
| 2021-04 | ||||
| * Adaptation for runtime configuration. | ||||
| @@ -24,7 +24,7 @@ void RGBNET_readValues() { | ||||
|     int channel = UDP.read(); | ||||
|  | ||||
|     //channel data is not used we only supports one channel | ||||
|     int len = UDP.read(RGBNET_packet, ledCount*3); | ||||
|     int len = UDP.read(RGBNET_packet, strip.getLengthTotal()*3); | ||||
|     if(len==0){ | ||||
|       return; | ||||
|     } | ||||
| @@ -50,7 +50,7 @@ void handleConfig(AsyncWebServerRequest *request) | ||||
|   \"channels\": [\ | ||||
|     {\ | ||||
|       \"channel\": 1,\ | ||||
|       \"leds\": " + ledCount + "\ | ||||
|       \"leds\": " + strip.getLengthTotal() + "\ | ||||
|     },\ | ||||
|     {\ | ||||
|       \"channel\": 2,\ | ||||
|   | ||||
							
								
								
									
										16
									
								
								usermods/BH1750_v2/platformio_override.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| ; Options | ||||
| ; ------- | ||||
| ; USERMOD_BH1750                                - define this to have this user mod included wled00\usermods_list.cpp | ||||
| ; USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL       - the max number of milliseconds between measurements, defaults to 10000ms | ||||
| ; USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL       - the min number of milliseconds between measurements, defaults to 500ms | ||||
| ; USERMOD_BH1750_FIRST_MEASUREMENT_AT           - the number of milliseconds after boot to take first measurement, defaults to 10 seconds | ||||
| ; USERMOD_BH1750_OFFSET_VALUE                   - the offset value to report on, defaults to 1 | ||||
| ; | ||||
| [env:usermod_BH1750_d1_mini] | ||||
| extends = env:d1_mini | ||||
| build_flags = | ||||
|     ${common.build_flags_esp8266} | ||||
|     -D USERMOD_BH1750 | ||||
| lib_deps =  | ||||
|     ${env.lib_deps} | ||||
|     claws/BH1750 @ ^1.2.0 | ||||
							
								
								
									
										24
									
								
								usermods/BH1750_v2/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| # BH1750 usermod | ||||
|  | ||||
| This usermod will read from an ambient light sensor like the BH1750 sensor. | ||||
| The luminance is displayed both in the Info section of the web UI as well as published to the `/luminance` MQTT topic if enabled. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Copy the example `platformio_override.ini` to the root directory.  This file should be placed in the same directory as `platformio.ini`. | ||||
|  | ||||
| ### Define Your Options | ||||
|  | ||||
| *   `USERMOD_BH1750`                                - define this to have this user mod included wled00\usermods_list.cpp | ||||
| *   `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL`       - the max number of milliseconds between measurements, defaults to 10000ms | ||||
| *   `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL`       - the min number of milliseconds between measurements, defaults to 500ms | ||||
| *   `USERMOD_BH1750_FIRST_MEASUREMENT_AT`           - the number of milliseconds after boot to take first measurement, defaults to 10 seconds | ||||
| *   `USERMOD_BH1750_OFFSET_VALUE`                   - the offset value to report on, defaults to 1 | ||||
|  | ||||
| All parameters can be configured at runtime using Usermods settings page. | ||||
|  | ||||
| ### PlatformIO requirements | ||||
|  | ||||
| If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:usermod_BH1750_d1_mini`. | ||||
|  | ||||
| ## Change Log | ||||
							
								
								
									
										177
									
								
								usermods/BH1750_v2/usermod_bh1750.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,177 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
| #include <Wire.h> | ||||
| #include <BH1750.h> | ||||
|  | ||||
| // the max frequency to check photoresistor, 10 seconds | ||||
| #ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL | ||||
| #define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 | ||||
| #endif | ||||
|  | ||||
| // the min frequency to check photoresistor, 500 ms | ||||
| #ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL | ||||
| #define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 | ||||
| #endif | ||||
|  | ||||
| // how many seconds after boot to take first measurement, 10 seconds | ||||
| #ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT | ||||
| #define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 | ||||
| #endif | ||||
|  | ||||
| // only report if differance grater than offset value | ||||
| #ifndef USERMOD_BH1750_OFFSET_VALUE | ||||
| #define USERMOD_BH1750_OFFSET_VALUE 1 | ||||
| #endif | ||||
|  | ||||
| class Usermod_BH1750 : public Usermod | ||||
| { | ||||
| private: | ||||
|   int8_t offset = USERMOD_BH1750_OFFSET_VALUE; | ||||
|  | ||||
|   unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; | ||||
|   unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; | ||||
|   unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); | ||||
|   unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); | ||||
|   // flag to indicate we have finished the first readLightLevel call | ||||
|   // allows this library to report to the user how long until the first | ||||
|   // measurement | ||||
|   bool getLuminanceComplete = false; | ||||
|  | ||||
|   // flag set at startup | ||||
|   bool disabled = false; | ||||
|  | ||||
|   // strings to reduce flash memory usage (used more than twice) | ||||
|   static const char _name[]; | ||||
|   static const char _enabled[]; | ||||
|   static const char _maxReadInterval[]; | ||||
|   static const char _minReadInterval[]; | ||||
|   static const char _offset[]; | ||||
|  | ||||
|   BH1750 lightMeter; | ||||
|   float lastLux = -1000; | ||||
|  | ||||
|   bool checkBoundSensor(float newValue, float prevValue, float maxDiff) | ||||
|   { | ||||
|     return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); | ||||
|   } | ||||
|  | ||||
| public: | ||||
|   void setup() | ||||
|   { | ||||
|     Wire.begin(); | ||||
|     lightMeter.begin(); | ||||
|   } | ||||
|  | ||||
|   void loop() | ||||
|   { | ||||
|     if (disabled || strip.isUpdating()) | ||||
|       return; | ||||
|  | ||||
|     unsigned long now = millis(); | ||||
|  | ||||
|     // check to see if we are due for taking a measurement | ||||
|     // lastMeasurement will not be updated until the conversion | ||||
|     // is complete the the reading is finished | ||||
|     if (now - lastMeasurement < minReadingInterval) | ||||
|     { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     bool shouldUpdate = now - lastSend > maxReadingInterval; | ||||
|  | ||||
|     float lux = lightMeter.readLightLevel(); | ||||
|     lastMeasurement = millis(); | ||||
|     getLuminanceComplete = true; | ||||
|  | ||||
|     if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) | ||||
|     { | ||||
|       lastLux = lux; | ||||
|       lastSend = millis(); | ||||
|       if (WLED_MQTT_CONNECTED) | ||||
|       { | ||||
|         char subuf[45]; | ||||
|         strcpy(subuf, mqttDeviceTopic); | ||||
|         strcat_P(subuf, PSTR("/luminance")); | ||||
|         mqtt->publish(subuf, 0, true, String(lux).c_str()); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         DEBUG_PRINTLN("Missing MQTT connection. Not publishing data"); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void addToJsonInfo(JsonObject &root) | ||||
|   { | ||||
|     JsonObject user = root[F("u")]; | ||||
|     if (user.isNull()) | ||||
|       user = root.createNestedObject(F("u")); | ||||
|  | ||||
|     JsonArray lux_json = user.createNestedArray(F("Luminance")); | ||||
|  | ||||
|     if (!getLuminanceComplete) | ||||
|     { | ||||
|       // if we haven't read the sensor yet, let the user know | ||||
|       // that we are still waiting for the first measurement | ||||
|       lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); | ||||
|       lux_json.add(F(" sec until read")); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     lux_json.add(lastLux); | ||||
|     lux_json.add(F(" lx")); | ||||
|   } | ||||
|  | ||||
|   uint16_t getId() | ||||
|   { | ||||
|     return USERMOD_ID_BH1750; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|      * addToConfig() (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)] = !disabled; | ||||
|     top[FPSTR(_maxReadInterval)] = maxReadingInterval; | ||||
|     top[FPSTR(_minReadInterval)] = minReadingInterval; | ||||
|     top[FPSTR(_offset)] = offset; | ||||
|  | ||||
|     DEBUG_PRINTLN(F("Photoresistor config saved.")); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   * readFromConfig() is called before setup() to populate properties from values stored in cfg.json | ||||
|   */ | ||||
|   bool readFromConfig(JsonObject &root) | ||||
|   { | ||||
|     // we look for JSON object. | ||||
|     JsonObject top = root[FPSTR(_name)]; | ||||
|     if (top.isNull()) | ||||
|     { | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     disabled = !(top[FPSTR(_enabled)] | !disabled); | ||||
|     maxReadingInterval = (top[FPSTR(_maxReadInterval)] | maxReadingInterval); // ms | ||||
|     minReadingInterval = (top[FPSTR(_minReadInterval)] | minReadingInterval); // ms | ||||
|     offset = top[FPSTR(_offset)] | offset; | ||||
|     DEBUG_PRINT(FPSTR(_name)); | ||||
|     DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|  | ||||
|     // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|     return true; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // 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::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; | ||||
| const char Usermod_BH1750::_minReadInterval[] PROGMEM = "min-read-interval-ms"; | ||||
| const char Usermod_BH1750::_offset[] PROGMEM = "offset-lx"; | ||||
							
								
								
									
										14
									
								
								usermods/BH1750_v2/usermods_list.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| #include "wled.h" | ||||
| /* | ||||
|  * Register your v2 usermods here! | ||||
|  */ | ||||
| #ifdef USERMOD_BH1750 | ||||
| #include "../usermods/BH1750_v2/usermod_BH1750.h" | ||||
| #endif | ||||
|  | ||||
| void registerUsermods() | ||||
| { | ||||
| #ifdef USERMOD_BH1750 | ||||
|   usermods.add(new Usermod_BH1750()); | ||||
| #endif | ||||
| } | ||||
| @@ -12,10 +12,11 @@ private: | ||||
| // User-defined configuration | ||||
| #define Celsius               // Show temperature mesaurement in Celcius. Comment out for Fahrenheit | ||||
| #define TemperatureDecimals 1 // Number of decimal places in published temperaure values | ||||
| #define HumidityDecimals 0    // Number of decimal places in published humidity values | ||||
| #define HumidityDecimals 2    // Number of decimal places in published humidity values | ||||
| #define PressureDecimals 2    // Number of decimal places in published pressure values | ||||
| #define TemperatureInterval 5 // Interval to measure temperature (and humidity, dew point if available) in seconds | ||||
| #define PressureInterval 300  // Interval to measure pressure in seconds | ||||
| #define PublishAlways 0       // Publish values even when they have not changed | ||||
|  | ||||
| // Sanity checks | ||||
| #if !defined(TemperatureDecimals) || TemperatureDecimals < 0 | ||||
| @@ -33,6 +34,9 @@ private: | ||||
| #if !defined(PressureInterval) || PressureInterval < 0 | ||||
|   #define PressureInterval TemperatureInterval | ||||
| #endif | ||||
| #if !defined(PublishAlways) | ||||
|   #define PublishAlways 0 | ||||
| #endif | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP32 // ESP32 boards | ||||
|   uint8_t SCL_PIN = 22; | ||||
| @@ -58,7 +62,7 @@ private: | ||||
|  | ||||
|   BME280I2C bme{settings}; | ||||
|  | ||||
|   uint8_t SensorType; | ||||
|   uint8_t sensorType; | ||||
|  | ||||
|   // Measurement timers | ||||
|   long timer; | ||||
| @@ -66,11 +70,11 @@ private: | ||||
|   long lastPressureMeasure = 0; | ||||
|  | ||||
|   // Current sensor values | ||||
|   float SensorTemperature; | ||||
|   float SensorHumidity; | ||||
|   float SensorHeatIndex; | ||||
|   float SensorDewPoint; | ||||
|   float SensorPressure; | ||||
|   float sensorTemperature; | ||||
|   float sensorHumidity; | ||||
|   float sensorHeatIndex; | ||||
|   float sensorDewPoint; | ||||
|   float sensorPressure; | ||||
|   // Track previous sensor values | ||||
|   float lastTemperature; | ||||
|   float lastHumidity; | ||||
| @@ -96,13 +100,13 @@ private: | ||||
|  | ||||
|     bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); | ||||
|  | ||||
|     SensorTemperature = _temperature; | ||||
|     SensorHumidity = _humidity; | ||||
|     SensorPressure = _pressure; | ||||
|     if (SensorType == 1) | ||||
|     sensorTemperature = _temperature; | ||||
|     sensorHumidity = _humidity; | ||||
|     sensorPressure = _pressure; | ||||
|     if (sensorType == 1) | ||||
|     { | ||||
|       SensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); | ||||
|       SensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); | ||||
|       sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); | ||||
|       sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -113,7 +117,7 @@ public: | ||||
|  | ||||
|     if (!bme.begin()) | ||||
|     { | ||||
|       SensorType = 0; | ||||
|       sensorType = 0; | ||||
|       Serial.println("Could not find BME280I2C sensor!"); | ||||
|     } | ||||
|     else | ||||
| @@ -121,15 +125,15 @@ public: | ||||
|       switch (bme.chipModel()) | ||||
|       { | ||||
|       case BME280::ChipModel_BME280: | ||||
|         SensorType = 1; | ||||
|         sensorType = 1; | ||||
|         Serial.println("Found BME280 sensor! Success."); | ||||
|         break; | ||||
|       case BME280::ChipModel_BMP280: | ||||
|         SensorType = 2; | ||||
|         sensorType = 2; | ||||
|         Serial.println("Found BMP280 sensor! No Humidity available."); | ||||
|         break; | ||||
|       default: | ||||
|         SensorType = 0; | ||||
|         sensorType = 0; | ||||
|         Serial.println("Found UNKNOWN sensor! Error!"); | ||||
|       } | ||||
|     } | ||||
| @@ -139,7 +143,7 @@ public: | ||||
|   { | ||||
|     // BME280 sensor MQTT publishing | ||||
|     // Check if sensor present and MQTT Connected, otherwise it will crash the MCU | ||||
|     if (SensorType != 0 && mqtt != nullptr) | ||||
|     if (sensorType != 0 && mqtt != nullptr) | ||||
|     { | ||||
|       // Timer to fetch new temperature, humidity and pressure data at intervals | ||||
|       timer = millis(); | ||||
| @@ -148,48 +152,48 @@ public: | ||||
|       { | ||||
|         lastTemperatureMeasure = timer; | ||||
|  | ||||
|         UpdateBME280Data(SensorType); | ||||
|         UpdateBME280Data(sensorType); | ||||
|  | ||||
|         float Temperature = roundf(SensorTemperature * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); | ||||
|         float Humidity, HeatIndex, DewPoint; | ||||
|         float temperature = roundf(sensorTemperature * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); | ||||
|         float humidity, heatIndex, dewPoint; | ||||
|  | ||||
|         // If temperature has changed since last measure, create string populated with device topic | ||||
|         // from the UI and values read from sensor, then publish to broker | ||||
|         if (Temperature != lastTemperature) | ||||
|         if (temperature != lastTemperature || PublishAlways) | ||||
|         { | ||||
|           String topic = String(mqttDeviceTopic) + "/temperature"; | ||||
|           mqttTemperaturePub = mqtt->publish(topic.c_str(), 0, false, String(Temperature, TemperatureDecimals).c_str()); | ||||
|           mqttTemperaturePub = mqtt->publish(topic.c_str(), 0, false, String(temperature, TemperatureDecimals).c_str()); | ||||
|         } | ||||
|  | ||||
|         lastTemperature = Temperature; // Update last sensor temperature for next loop | ||||
|         lastTemperature = temperature; // Update last sensor temperature for next loop | ||||
|  | ||||
|         if (SensorType == 1) // Only if sensor is a BME280 | ||||
|         if (sensorType == 1) // Only if sensor is a BME280 | ||||
|         { | ||||
|           Humidity = roundf(SensorHumidity * pow(10, HumidityDecimals)) / pow(10, HumidityDecimals); | ||||
|           HeatIndex = roundf(SensorHeatIndex * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); | ||||
|           DewPoint = roundf(SensorDewPoint * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); | ||||
|           humidity = roundf(sensorHumidity * pow(10, HumidityDecimals)) / pow(10, HumidityDecimals); | ||||
|           heatIndex = roundf(sensorHeatIndex * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); | ||||
|           dewPoint = roundf(sensorDewPoint * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); | ||||
|  | ||||
|           if (Humidity != lastHumidity) | ||||
|           if (humidity != lastHumidity || PublishAlways) | ||||
|           { | ||||
|             String topic = String(mqttDeviceTopic) + "/humidity"; | ||||
|             mqtt->publish(topic.c_str(), 0, false, String(Humidity, HumidityDecimals).c_str()); | ||||
|             mqtt->publish(topic.c_str(), 0, false, String(humidity, HumidityDecimals).c_str()); | ||||
|           } | ||||
|  | ||||
|           if (HeatIndex != lastHeatIndex) | ||||
|           if (heatIndex != lastHeatIndex || PublishAlways) | ||||
|           { | ||||
|             String topic = String(mqttDeviceTopic) + "/heat_index"; | ||||
|             mqtt->publish(topic.c_str(), 0, false, String(HeatIndex, TemperatureDecimals).c_str()); | ||||
|             mqtt->publish(topic.c_str(), 0, false, String(heatIndex, TemperatureDecimals).c_str()); | ||||
|           } | ||||
|  | ||||
|           if (DewPoint != lastDewPoint) | ||||
|           if (dewPoint != lastDewPoint || PublishAlways) | ||||
|           { | ||||
|             String topic = String(mqttDeviceTopic) + "/dew_point"; | ||||
|             mqtt->publish(topic.c_str(), 0, false, String(DewPoint, TemperatureDecimals).c_str()); | ||||
|             mqtt->publish(topic.c_str(), 0, false, String(dewPoint, TemperatureDecimals).c_str()); | ||||
|           } | ||||
|  | ||||
|           lastHumidity = Humidity; | ||||
|           lastHeatIndex = HeatIndex; | ||||
|           lastDewPoint = DewPoint; | ||||
|           lastHumidity = humidity; | ||||
|           lastHeatIndex = heatIndex; | ||||
|           lastDewPoint = dewPoint; | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @@ -197,15 +201,15 @@ public: | ||||
|       { | ||||
|         lastPressureMeasure = timer; | ||||
|  | ||||
|         float Pressure = roundf(SensorPressure * pow(10, PressureDecimals)) / pow(10, PressureDecimals); | ||||
|         float pressure = roundf(sensorPressure * pow(10, PressureDecimals)) / pow(10, PressureDecimals); | ||||
|  | ||||
|         if (Pressure != lastPressure) | ||||
|         if (pressure != lastPressure || PublishAlways) | ||||
|         { | ||||
|           String topic = String(mqttDeviceTopic) + "/pressure"; | ||||
|           mqttPressurePub = mqtt->publish(topic.c_str(), 0, true, String(Pressure, PressureDecimals).c_str()); | ||||
|           mqttPressurePub = mqtt->publish(topic.c_str(), 0, true, String(pressure, PressureDecimals).c_str()); | ||||
|         } | ||||
|  | ||||
|         lastPressure = Pressure; | ||||
|         lastPressure = pressure; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -25,6 +25,18 @@ class MyExampleUsermod : public Usermod { | ||||
|   private: | ||||
|     //Private class members. You can declare variables and functions only accessible to your usermod here | ||||
|     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; | ||||
|     unsigned long testULong = 42424242; | ||||
|     float testFloat = 42.42; | ||||
|     String testString = "Forty-Two"; | ||||
|  | ||||
|     // These config variables have defaults set inside readFromConfig() | ||||
|     int testInt; | ||||
|     long testLong; | ||||
|     int8_t testPins[2]; | ||||
|  | ||||
|   public: | ||||
|     //Functions called by WLED | ||||
|  | ||||
| @@ -114,30 +126,85 @@ class MyExampleUsermod : public Usermod { | ||||
|      * 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. | ||||
|      * addToConfig() will make your settings editable through the Usermod Settings page automatically. | ||||
|      * | ||||
|      * Usermod Settings Overview: | ||||
|      * - Numeric values are treated as floats in the browser. | ||||
|      *   - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float | ||||
|      *     before being returned to the Usermod.  The float data type has only 6-7 decimal digits of precision, and | ||||
|      *     doubles are not supported, numbers will be rounded to the nearest float value when being parsed. | ||||
|      *     The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. | ||||
|      *   - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a | ||||
|      *     C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. | ||||
|      *     Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type | ||||
|      *     used in the Usermod when reading the value from ArduinoJson. | ||||
|      * - Pin values can be treated differently from an integer value by using the key name "pin" | ||||
|      *   - "pin" can contain a single or array of integer values | ||||
|      *   - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins | ||||
|      *     - Red color indicates a conflict.  Yellow color indicates a pin with a warning (e.g. an input-only pin) | ||||
|      *   - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used | ||||
|      * | ||||
|      * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings | ||||
|      *  | ||||
|      * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work.   | ||||
|      * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. | ||||
|      * See the WLED Soundreactive fork (code and wiki) for reference.  https://github.com/atuline/WLED | ||||
|      *  | ||||
|      * 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("exampleUsermod"); | ||||
|       top["great"] = userVar0; //save this var persistently whenever settings are saved | ||||
|       top["great"] = userVar0; //save these vars persistently whenever settings are saved | ||||
|       top["testBool"] = testBool; | ||||
|       top["testInt"] = testInt; | ||||
|       top["testLong"] = testLong; | ||||
|       top["testULong"] = testULong; | ||||
|       top["testFloat"] = testFloat; | ||||
|       top["testString"] = testString; | ||||
|       JsonArray pinArray = top.createNestedArray("pin"); | ||||
|       pinArray.add(testPins[0]); | ||||
|       pinArray.add(testPins[1]);  | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /* | ||||
|      * 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) | ||||
|      * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) | ||||
|      *  | ||||
|      * 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 :) | ||||
|      *  | ||||
|      * 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) | ||||
|      *  | ||||
|      * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present | ||||
|      * The configComplete variable is true only if the "exampleUsermod" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them | ||||
|      *  | ||||
|      * This function is guaranteed to be called on boot, but could also be called every time settings are updated | ||||
|      */ | ||||
|     void readFromConfig(JsonObject& root) | ||||
|     bool readFromConfig(JsonObject& root) | ||||
|     { | ||||
|       JsonObject top = root["top"]; | ||||
|       userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) | ||||
|       // 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["exampleUsermod"]; | ||||
|  | ||||
|       bool configComplete = !top.isNull(); | ||||
|  | ||||
|       configComplete &= getJsonValue(top["great"], userVar0); | ||||
|       configComplete &= getJsonValue(top["testBool"], testBool); | ||||
|       configComplete &= getJsonValue(top["testULong"], testULong); | ||||
|       configComplete &= getJsonValue(top["testFloat"], testFloat); | ||||
|       configComplete &= getJsonValue(top["testString"], testString); | ||||
|  | ||||
|       // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing | ||||
|       configComplete &= getJsonValue(top["testInt"], testInt, 42);   | ||||
|       configComplete &= getJsonValue(top["testLong"], testLong, -42424242); | ||||
|       configComplete &= getJsonValue(top["pin"][0], testPins[0], -1); | ||||
|       configComplete &= getJsonValue(top["pin"][1], testPins[1], -1); | ||||
|  | ||||
|       return configComplete; | ||||
|     } | ||||
|  | ||||
|     | ||||
|   | ||||
							
								
								
									
										70
									
								
								usermods/EleksTube_IPS/ChipSelect.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | ||||
| #ifndef CHIP_SELECT_H | ||||
| #define CHIP_SELECT_H | ||||
|  | ||||
| #include "Hardware.h" | ||||
|  | ||||
| /* | ||||
|  * `digit`s are as defined in Hardware.h, 0 == seconds ones, 5 == hours tens. | ||||
|  */ | ||||
|  | ||||
| class ChipSelect { | ||||
| private: | ||||
|   uint8_t digits_map; | ||||
|   const uint8_t all_on = 0x3F; | ||||
|   const uint8_t all_off = 0x00; | ||||
| public: | ||||
|   ChipSelect() : digits_map(all_off) {} | ||||
|    | ||||
|   void update() { | ||||
|     // Documented in README.md.  Q7 and Q6 are unused. Q5 is Seconds Ones, Q0 is Hours Tens. | ||||
|     // Q7 is the first bit written, Q0 is the last.  So we push two dummy bits, then start with | ||||
|     // Seconds Ones and end with Hours Tens. | ||||
|     // CS is Active Low, but digits_map is 1 for enable, 0 for disable.  So we bit-wise NOT first. | ||||
|  | ||||
|     uint8_t to_shift = (~digits_map) << 2; | ||||
|  | ||||
|     digitalWrite(CSSR_LATCH_PIN, LOW); | ||||
|     shiftOut(CSSR_DATA_PIN, CSSR_CLOCK_PIN, LSBFIRST, to_shift); | ||||
|     digitalWrite(CSSR_LATCH_PIN, HIGH); | ||||
|   } | ||||
|  | ||||
|     void begin()  | ||||
|   { | ||||
|     pinMode(CSSR_LATCH_PIN, OUTPUT); | ||||
|     pinMode(CSSR_DATA_PIN, OUTPUT); | ||||
|     pinMode(CSSR_CLOCK_PIN, OUTPUT); | ||||
|  | ||||
|     digitalWrite(CSSR_DATA_PIN, LOW); | ||||
|     digitalWrite(CSSR_CLOCK_PIN, LOW); | ||||
|     digitalWrite(CSSR_LATCH_PIN, LOW); | ||||
|     update(); | ||||
|   } | ||||
|  | ||||
|   // These speak the indexes defined in Hardware.h. | ||||
|   // So 0 is disabled, 1 is enabled (even though CS is active low, this gets mapped.) | ||||
|   // So bit 0 (LSB), is index 0, is SECONDS_ONES | ||||
|   // Translation to what the 74HC595 uses is done in update() | ||||
|   void setDigitMap(uint8_t map, bool update_=true)   { digits_map = map; if (update_) update(); } | ||||
|   uint8_t getDigitMap()                        { return digits_map; } | ||||
|  | ||||
|   // Helper functions | ||||
|   // Sets just the one digit by digit number | ||||
|   void setDigit(uint8_t digit, bool update_=true) { setDigitMap(0x01 << digit, update_); } | ||||
|   void setAll(bool update_=true)                  { setDigitMap(all_on,  update_); } | ||||
|   void clear(bool update_=true)                   { setDigitMap(all_off, update_); } | ||||
|   void setSecondsOnes()                           { setDigit(SECONDS_ONES); } | ||||
|   void setSecondsTens()                           { setDigit(SECONDS_TENS); } | ||||
|   void setMinutesOnes()                           { setDigit(MINUTES_ONES); } | ||||
|   void setMinutesTens()                           { setDigit(MINUTES_TENS); } | ||||
|   void setHoursOnes()                             { setDigit(HOURS_ONES); } | ||||
|   void setHoursTens()                             { setDigit(HOURS_TENS); } | ||||
|   bool isSecondsOnes()                            { return ((digits_map & SECONDS_ONES_MAP) > 0); } | ||||
|   bool isSecondsTens()                            { return ((digits_map & SECONDS_TENS_MAP) > 0); } | ||||
|   bool isMinutesOnes()                            { return ((digits_map & MINUTES_ONES_MAP) > 0); } | ||||
|   bool isMinutesTens()                            { return ((digits_map & MINUTES_TENS_MAP) > 0); } | ||||
|   bool isHoursOnes()                              { return ((digits_map & HOURS_ONES_MAP) > 0); } | ||||
|   bool isHoursTens()                              { return ((digits_map & HOURS_TENS_MAP) > 0); } | ||||
| }; | ||||
|  | ||||
|  | ||||
| #endif // CHIP_SELECT_H | ||||
							
								
								
									
										52
									
								
								usermods/EleksTube_IPS/Hardware.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,52 @@ | ||||
| /* | ||||
|  * Define the hardware for the EleksTube IPS clock.  Mostly pin definitions | ||||
|  */ | ||||
| #ifndef ELEKSTUBEHAX_HARDWARE_H | ||||
| #define ELEKSTUBEHAX_HARDWARE_H | ||||
|  | ||||
| #include <stdint.h>  | ||||
| #include <Arduino.h> // for HIGH and LOW | ||||
|  | ||||
| // Common indexing scheme, used to identify the digit | ||||
| #define SECONDS_ONES (0) | ||||
| #define SECONDS_TENS (1) | ||||
| #define MINUTES_ONES (2) | ||||
| #define MINUTES_TENS (3) | ||||
| #define HOURS_ONES   (4) | ||||
| #define HOURS_TENS   (5) | ||||
| #define NUM_DIGITS   (6) | ||||
|  | ||||
| #define SECONDS_ONES_MAP (0x01 << SECONDS_ONES) | ||||
| #define SECONDS_TENS_MAP (0x01 << SECONDS_TENS) | ||||
| #define MINUTES_ONES_MAP (0x01 << MINUTES_ONES) | ||||
| #define MINUTES_TENS_MAP (0x01 << MINUTES_TENS) | ||||
| #define HOURS_ONES_MAP   (0x01 << HOURS_ONES) | ||||
| #define HOURS_TENS_MAP   (0x01 << HOURS_TENS) | ||||
|  | ||||
| // WS2812 (or compatible) LEDs on the back of the display modules. | ||||
| #define BACKLIGHTS_PIN (12) | ||||
|  | ||||
| // Buttons, active low, externally pulled up (with actual resistors!) | ||||
| #define BUTTON_LEFT_PIN (33) | ||||
| #define BUTTON_MODE_PIN (32) | ||||
| #define BUTTON_RIGHT_PIN (35) | ||||
| #define BUTTON_POWER_PIN (34) | ||||
|  | ||||
| // I2C to DS3231 RTC. | ||||
| #define RTC_SCL_PIN (22) | ||||
| #define RTC_SDA_PIN (21) | ||||
|  | ||||
| // Chip Select shift register, to select the display | ||||
| #define CSSR_DATA_PIN (14) | ||||
| #define CSSR_CLOCK_PIN (16) | ||||
| #define CSSR_LATCH_PIN (17) | ||||
|  | ||||
| // SPI to displays | ||||
| // DEFINED IN User_Setup.h | ||||
| // Look for: TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, and TFT_RST | ||||
|  | ||||
| // Power for all TFT displays are grounded through a MOSFET so they can all be turned off. | ||||
| // Active HIGH. | ||||
| #define TFT_ENABLE_PIN (27) | ||||
|  | ||||
| #endif // ELEKSTUBEHAX_HARDWARE_H | ||||
							
								
								
									
										218
									
								
								usermods/EleksTube_IPS/TFTs.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,218 @@ | ||||
| #ifndef TFTS_H | ||||
| #define TFTS_H | ||||
|  | ||||
| #include "wled.h" | ||||
| #include <FS.h> | ||||
|  | ||||
| #include <TFT_eSPI.h> | ||||
| #include "Hardware.h" | ||||
| #include "ChipSelect.h" | ||||
|  | ||||
| class TFTs : public TFT_eSPI { | ||||
| private: | ||||
|   uint8_t digits[NUM_DIGITS]; | ||||
|  | ||||
|   // These read 16- and 32-bit types from the SD card file. | ||||
|   // BMP data is stored little-endian, Arduino is little-endian too. | ||||
|   // May need to reverse subscript order if porting elsewhere. | ||||
|  | ||||
|   uint16_t read16(fs::File &f) { | ||||
|     uint16_t result; | ||||
|     ((uint8_t *)&result)[0] = f.read(); // LSB | ||||
|     ((uint8_t *)&result)[1] = f.read(); // MSB | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   uint32_t read32(fs::File &f) { | ||||
|     uint32_t result; | ||||
|     ((uint8_t *)&result)[0] = f.read(); // LSB | ||||
|     ((uint8_t *)&result)[1] = f.read(); | ||||
|     ((uint8_t *)&result)[2] = f.read(); | ||||
|     ((uint8_t *)&result)[3] = f.read(); // MSB | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH]; | ||||
|    | ||||
|  | ||||
|   // These BMP functions are stolen directly from the TFT_SPIFFS_BMP example in the TFT_eSPI library. | ||||
|   // Unfortunately, they aren't part of the library itself, so I had to copy them. | ||||
|   // I've modified drawBmp to buffer the whole image at once instead of doing it line-by-line. | ||||
|  | ||||
|   //// BEGIN STOLEN CODE | ||||
|  | ||||
|   // Draw directly from file stored in RGB565 format | ||||
|   bool drawBin(const char *filename) { | ||||
|     fs::File bmpFS; | ||||
|  | ||||
|  | ||||
|     // Open requested file on SD card | ||||
|     bmpFS = WLED_FS.open(filename, "r"); | ||||
|  | ||||
|     if (!bmpFS) | ||||
|     { | ||||
|       Serial.print(F("File not found: ")); | ||||
|       Serial.println(filename); | ||||
|       return(false); | ||||
|     } | ||||
|  | ||||
|     size_t sz = bmpFS.size(); | ||||
|     if (sz <= 64800) | ||||
|     { | ||||
|       bool oldSwapBytes = getSwapBytes(); | ||||
|       setSwapBytes(true); | ||||
|  | ||||
|       int16_t h = sz / (135 * 2); | ||||
|  | ||||
|       //draw img that is shorter than 240pix into the center | ||||
|       int16_t y = (height() - h) /2; | ||||
|  | ||||
|       bmpFS.read((uint8_t *) output_buffer,sz); | ||||
|  | ||||
|       if (!realtimeMode || realtimeOverride) strip.service(); | ||||
|  | ||||
|       pushImage(0, y, 135, h, (uint16_t *)output_buffer); | ||||
|  | ||||
|       setSwapBytes(oldSwapBytes); | ||||
|     } | ||||
|  | ||||
|     bmpFS.close(); | ||||
|  | ||||
|     return(true); | ||||
|   } | ||||
|  | ||||
|   bool drawBmp(const char *filename) { | ||||
|     fs::File bmpFS; | ||||
|  | ||||
|     // Open requested file on SD card | ||||
|     bmpFS = WLED_FS.open(filename, "r"); | ||||
|  | ||||
|     if (!bmpFS) | ||||
|     { | ||||
|       Serial.print(F("File not found: ")); | ||||
|       Serial.println(filename); | ||||
|       return(false); | ||||
|     } | ||||
|  | ||||
|     uint32_t seekOffset; | ||||
|     int16_t w, h, row; | ||||
|     uint8_t  r, g, b; | ||||
|  | ||||
|     uint16_t magic = read16(bmpFS); | ||||
|     if (magic == 0xFFFF) { | ||||
|       Serial.println(F("BMP not found!")); | ||||
|       bmpFS.close(); | ||||
|       return(false); | ||||
|     } | ||||
|      | ||||
|     if (magic != 0x4D42) { | ||||
|       Serial.print(F("File not a BMP. Magic: ")); | ||||
|       Serial.println(magic); | ||||
|       bmpFS.close(); | ||||
|       return(false); | ||||
|     } | ||||
|  | ||||
|     read32(bmpFS); | ||||
|     read32(bmpFS); | ||||
|     seekOffset = read32(bmpFS); | ||||
|     read32(bmpFS); | ||||
|     w = read32(bmpFS); | ||||
|     h = read32(bmpFS); | ||||
|  | ||||
|     if ((read16(bmpFS) != 1) || (read16(bmpFS) != 24) || (read32(bmpFS) != 0)) { | ||||
|       Serial.println(F("BMP format not recognized.")); | ||||
|       bmpFS.close(); | ||||
|       return(false); | ||||
|     } | ||||
|  | ||||
|     //draw img that is shorter than 240pix into the center | ||||
|     int16_t y = (height() - h) /2; | ||||
|  | ||||
|     bool oldSwapBytes = getSwapBytes(); | ||||
|     setSwapBytes(true); | ||||
|     bmpFS.seek(seekOffset); | ||||
|  | ||||
|     uint16_t padding = (4 - ((w * 3) & 3)) & 3; | ||||
|     uint8_t lineBuffer[w * 3 + padding]; | ||||
|      | ||||
|     uint8_t serviceStrip = (!realtimeMode || realtimeOverride) ? 7 : 0; | ||||
|     // row is decremented as the BMP image is drawn bottom up | ||||
|     for (row = h-1; row >= 0; row--) { | ||||
|       if ((row & 0b00000111) == serviceStrip) strip.service(); //still refresh backlight to mitigate stutter every few rows | ||||
|       bmpFS.read(lineBuffer, sizeof(lineBuffer)); | ||||
|       uint8_t*  bptr = lineBuffer; | ||||
|        | ||||
|       // Convert 24 to 16 bit colours while copying to output buffer. | ||||
|       for (uint16_t col = 0; col < w; col++) | ||||
|       { | ||||
|         b = *bptr++; | ||||
|         g = *bptr++; | ||||
|         r = *bptr++; | ||||
|         output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     pushImage(0, y, w, h, (uint16_t *)output_buffer); | ||||
|     setSwapBytes(oldSwapBytes); | ||||
|  | ||||
|     bmpFS.close(); | ||||
|     return(true); | ||||
|   } | ||||
|  | ||||
| public: | ||||
|   TFTs() : TFT_eSPI(), chip_select() | ||||
|     { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; } | ||||
|  | ||||
|   // no == Do not send to TFT. yes == Send to TFT if changed. force == Send to TFT. | ||||
|   enum show_t { no, yes, force }; | ||||
|   // A digit of 0xFF means blank the screen. | ||||
|   const static uint8_t blanked = 255; | ||||
|    | ||||
|   void begin() { | ||||
|     pinMode(TFT_ENABLE_PIN, OUTPUT); | ||||
|     digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot | ||||
|  | ||||
|     // Start with all displays selected. | ||||
|     chip_select.begin(); | ||||
|     chip_select.setAll(); | ||||
|  | ||||
|     // Initialize the super class. | ||||
|     init(); | ||||
|   } | ||||
|  | ||||
|   void showDigit(uint8_t digit) { | ||||
|     chip_select.setDigit(digit); | ||||
|  | ||||
|     if (digits[digit] == blanked) { | ||||
|       fillScreen(TFT_BLACK); | ||||
|     } | ||||
|     else { | ||||
|       // Filenames are no bigger than "255.bmp\0" | ||||
|       char file_name[10]; | ||||
|       sprintf(file_name, "/%d.bmp", digits[digit]); | ||||
|       if (WLED_FS.exists(file_name)) { | ||||
|         drawBmp(file_name); | ||||
|       } else { | ||||
|         sprintf(file_name, "/%d.bin", digits[digit]); | ||||
|         drawBin(file_name); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void setDigit(uint8_t digit, uint8_t value, show_t show=yes) { | ||||
|     uint8_t old_value = digits[digit]; | ||||
|     digits[digit] = value; | ||||
|    | ||||
|     if (show != no && (old_value != value || show == force)) { | ||||
|       showDigit(digit); | ||||
|     } | ||||
|   } | ||||
|   uint8_t getDigit(uint8_t digit)                 { return digits[digit]; } | ||||
|  | ||||
|   void showAllDigits()               { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit); } | ||||
|  | ||||
|   // Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly. | ||||
|   ChipSelect chip_select; | ||||
| }; | ||||
|  | ||||
| #endif // TFTS_H | ||||
							
								
								
									
										47
									
								
								usermods/EleksTube_IPS/User_Setup.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
| /* | ||||
|  * This is intended to over-ride `User_Setup.h` that comes with the TFT_eSPI library. | ||||
|  * I hate having to modify the library code. | ||||
|  */ | ||||
|  | ||||
| // ST7789 135 x 240 display with no chip select line | ||||
|  | ||||
| #define ST7789_DRIVER     // Configure all registers | ||||
|  | ||||
| #define TFT_WIDTH  135 | ||||
| #define TFT_HEIGHT 240 | ||||
|  | ||||
| #define CGRAM_OFFSET      // Library will add offsets required | ||||
|  | ||||
| //#define TFT_RGB_ORDER TFT_RGB  // Colour order Red-Green-Blue | ||||
| //#define TFT_RGB_ORDER TFT_BGR  // Colour order Blue-Green-Red | ||||
|  | ||||
| //#define TFT_INVERSION_ON | ||||
| //#define TFT_INVERSION_OFF | ||||
|  | ||||
| // EleksTube IPS | ||||
| #define TFT_SDA_READ      // Read and write on the MOSI/SDA pin, no separate MISO pin | ||||
| #define TFT_MOSI 23 | ||||
| #define TFT_SCLK 18 | ||||
| //#define TFT_CS    -1 // Not connected | ||||
| #define TFT_DC   25  // Data Command, aka Register Select or RS | ||||
| #define TFT_RST  26  // Connect reset to ensure display initialises | ||||
|  | ||||
| #define LOAD_GLCD   // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH | ||||
| //#define LOAD_FONT2  // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters | ||||
| //#define LOAD_FONT4  // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters | ||||
| //#define LOAD_FONT6  // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm | ||||
| //#define LOAD_FONT7  // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. | ||||
| //#define LOAD_FONT8  // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. | ||||
| //#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT | ||||
| //#define LOAD_GFXFF  // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts | ||||
|  | ||||
| //#define SMOOTH_FONT | ||||
|  | ||||
|  | ||||
| //#define SPI_FREQUENCY  27000000 | ||||
| #define SPI_FREQUENCY  40000000 | ||||
|  | ||||
| /* | ||||
|  * To make the Library not over-write all this: | ||||
|  */ | ||||
| #define USER_SETUP_LOADED | ||||
							
								
								
									
										31
									
								
								usermods/EleksTube_IPS/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
| # EleksTube IPS Clock usermod | ||||
|  | ||||
| This usermod allows WLED to run on the EleksTube IPS clock. | ||||
| It enables running all WLED effects on the background SK6812 lighting, while displaying digit bitmaps on the 6 IPS screens. | ||||
| Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith! | ||||
|  | ||||
| Supported: | ||||
| - Display with custom bitmaps or raw RGB565 images (.bin) from filesystem | ||||
| - Background lighting | ||||
| - Power button | ||||
| - RTC (with RTC usermod) | ||||
| - Standard WLED time features (NTP, DST, timezones) | ||||
|  | ||||
| Not supported: | ||||
| - 3 navigation buttons, on-device setup | ||||
|  | ||||
| Your images must be exactly 135 pixels wide and 1-240 pixels high. | ||||
|  | ||||
| ## Installation  | ||||
|  | ||||
| Compile and upload to clock using the `elekstube_ips` PlatformIO environment | ||||
| Once uploaded (the clock can be flashed like any ESP32 module), go to `[WLED-IP]/edit` and upload the 0-9.bin files from [here](https://github.com/Aircoookie/NixieThemes/tree/master/themes/RealisticNixie/bin). | ||||
| You can find more clockfaces in the [NixieThemes](https://github.com/Aircoookie/NixieThemes/) repo. | ||||
| Use LED pin 12, relay pin 27 and button pin 34. | ||||
|  | ||||
| ## Use of RGB565 images | ||||
|  | ||||
| Binary 16-bit per pixel RGB565 format `.bin` images are now supported. This has the benefit of only using 2/3rds of the file size a `.bmp` has. | ||||
| The drawback is that this format cannot be handled by common image programs and that an extra conversion step is needed. | ||||
| You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`) | ||||
| Thank you to @RedNax67 for adding .bin support. | ||||
							
								
								
									
										60
									
								
								usermods/EleksTube_IPS/usermod_elekstube_ips.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,60 @@ | ||||
| #pragma once | ||||
| #include "TFTs.h" | ||||
| #include "wled.h" | ||||
|  | ||||
| //Large parts of the code are from https://github.com/SmittyHalibut/EleksTubeHAX | ||||
|  | ||||
| class ElekstubeIPSUsermod : public Usermod { | ||||
|   private: | ||||
|     TFTs tfts; | ||||
|     void updateClockDisplay(TFTs::show_t show=TFTs::yes) { | ||||
|       bool set[6] = {false};  | ||||
|       for (uint8_t i = 0; i<6; i++) { | ||||
|         char c = cronixieDisplay[i]; | ||||
|         if (c >= '0' && c <= '9') { | ||||
|           tfts.setDigit(5-i, c - '0', show); set[i] = true; | ||||
|         } else if (c >= 'A' && c <= 'G') { | ||||
|           tfts.setDigit(5-i, c - 'A' + 10, show); set[i] = true; //10.bmp to 16.bmp static display | ||||
|         } else if (c == '-' || c == '_' || c == ' ') { | ||||
|           tfts.setDigit(5-i, 255, show); set[i] = true; //blank | ||||
|         } else { | ||||
|           set[i] = false; //display HHMMSS time | ||||
|         } | ||||
|       } | ||||
|       uint8_t hr = hour(localTime); | ||||
|       uint8_t hrTens = hr/10; | ||||
|       uint8_t mi = minute(localTime); | ||||
|       uint8_t mittens = mi/10; | ||||
|       uint8_t s = second(localTime); | ||||
|       uint8_t sTens = s/10; | ||||
|       if (!set[0]) tfts.setDigit(HOURS_TENS, hrTens, show); | ||||
|       if (!set[1]) tfts.setDigit(HOURS_ONES, hr - hrTens*10, show); | ||||
|       if (!set[2]) tfts.setDigit(MINUTES_TENS, mittens, show); | ||||
|       if (!set[3]) tfts.setDigit(MINUTES_ONES, mi - mittens*10, show); | ||||
|       if (!set[4]) tfts.setDigit(SECONDS_TENS, sTens, show); | ||||
|       if (!set[5]) tfts.setDigit(SECONDS_ONES, s - sTens*10, show); | ||||
|     } | ||||
|     unsigned long lastTime = 0; | ||||
|   public: | ||||
|  | ||||
|     void setup() { | ||||
|       tfts.begin(); | ||||
|       tfts.fillScreen(TFT_BLACK); | ||||
|  | ||||
|       for (int8_t i = 5; i >= 0; i--) { | ||||
|         tfts.setDigit(i, 255, TFTs::force); //turn all off | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void loop() { | ||||
|       if (toki.isTick()) { | ||||
|         updateLocalTime(); | ||||
|         updateClockDisplay(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     uint16_t getId() | ||||
|     { | ||||
|       return USERMOD_ID_ELEKSTUBE_IPS; | ||||
|     } | ||||
| }; | ||||
| @@ -149,11 +149,14 @@ Delay <input type=\"number\" min=\"5\" max=\"300\" value=\""; | ||||
|   /** | ||||
|    * restore the changeable values | ||||
|    */ | ||||
|   void readFromConfig(JsonObject &root) | ||||
|   bool readFromConfig(JsonObject &root) | ||||
|   { | ||||
|     JsonObject top = root["FixUnreachableNetServices"]; | ||||
|     if (top.isNull()) return false; | ||||
|     m_pingDelayMs = top["PingDelayMs"] | m_pingDelayMs; | ||||
|     m_pingDelayMs = max(5000UL, min(18000000UL, m_pingDelayMs)); | ||||
|     // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   | ||||
							
								
								
									
										119
									
								
								usermods/JSON_IR_remote/21-key_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,119 @@ | ||||
| { | ||||
|   "desc": "21-key", | ||||
|   "0xFFA25D": { | ||||
|     "label": "On", | ||||
|     "pos": "1x1", | ||||
|     "cmd": "T=1" | ||||
|   }, | ||||
|   "0xFF629D": { | ||||
|     "label": "Off", | ||||
|     "pos": "1x2", | ||||
|     "cmd": "T=0" | ||||
|   }, | ||||
|   "0xFFE21D": { | ||||
|     "label": "Flash", | ||||
|     "pos": "1x3", | ||||
|     "cmnt": "Cycle Effects", | ||||
|     "cmd": "CY=0&FX=~" | ||||
|   }, | ||||
|   "0xFF22DD": { | ||||
|     "label": "Strobe", | ||||
|     "pos": "2x1", | ||||
|     "cmnt": "Sinelon Dual", | ||||
|     "cmd": "CY=0&FX=93" | ||||
|   }, | ||||
|   "0xFF02FD": { | ||||
|     "label": "Fade", | ||||
|     "pos": "2x2", | ||||
|     "cmnt": "Rain", | ||||
|     "cmd": "CY=0&FX=43" | ||||
|   }, | ||||
|   "0xFFC23D": { | ||||
|     "label": "Smooth", | ||||
|     "pos": "2x3", | ||||
|     "cmnt": "Aurora", | ||||
|     "cmd": "CY=0&FX=38" | ||||
|   }, | ||||
|   "0xFFE01F": { | ||||
|     "label": "Bright +", | ||||
|     "pos": "3x1", | ||||
|     "cmd": "A=~16" | ||||
|   }, | ||||
|   "0xFFA857": { | ||||
|     "label": "Bright -", | ||||
|     "pos": "3x2", | ||||
|     "cmd": "A=~-16" | ||||
|   }, | ||||
|   "0xFF906F": { | ||||
|     "label": "White", | ||||
|     "pos": "3x3", | ||||
|     "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" | ||||
|   }, | ||||
|   "0xFF6897": { | ||||
|     "label": "Red", | ||||
|     "pos": "4x1", | ||||
|     "cmnt": "Lava", | ||||
|     "cmd": "FP=8" | ||||
|   }, | ||||
|   "0xFF9867": { | ||||
|     "label": "Green", | ||||
|     "pos": "4x2", | ||||
|     "cmnt": "Forest", | ||||
|     "cmd": "FP=10" | ||||
|   }, | ||||
|   "0xFFB04F": { | ||||
|     "label": "Blue", | ||||
|     "pos": "4x3", | ||||
|     "cmnt": "Breeze", | ||||
|     "cmd": "FP=15" | ||||
|   }, | ||||
|   "0xFF30CF": { | ||||
|     "label": "Tomato", | ||||
|     "pos": "5x1", | ||||
|     "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" | ||||
|   }, | ||||
|   "0xFF18E7": { | ||||
|     "label": "LightGreen", | ||||
|     "pos": "5x2", | ||||
|     "cmnt": "Rivendale", | ||||
|     "cmd": "FP=14" | ||||
|   }, | ||||
|   "0xFF7A85": { | ||||
|     "label": "SkyBlue", | ||||
|     "pos": "5x3", | ||||
|     "cmnt": "Ocean", | ||||
|     "cmd": "FP=9" | ||||
|   }, | ||||
|   "0xFF10EF": { | ||||
|     "label": "Orange", | ||||
|     "pos": "6x1", | ||||
|     "cmnt": "Orangery", | ||||
|     "cmd": "FP=47" | ||||
|   }, | ||||
|   "0xFF38C7": { | ||||
|     "label": "Aqua", | ||||
|     "pos": "6x2", | ||||
|     "cmd": "FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895" | ||||
|   }, | ||||
|   "0xFF5AA5": { | ||||
|     "label": "Purple", | ||||
|     "pos": "6x3", | ||||
|     "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" | ||||
|   }, | ||||
|   "0xFF42BD": { | ||||
|     "label": "Yellow", | ||||
|     "pos": "7x1", | ||||
|     "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" | ||||
|   }, | ||||
|   "0xFF4AB5": { | ||||
|     "label": "Cyan", | ||||
|     "pos": "7x2", | ||||
|     "cmnt": "Beech", | ||||
|     "cmd": "FP=22" | ||||
|   }, | ||||
|   "0xFF52AD": { | ||||
|     "label": "Pink", | ||||
|     "pos": "7x3", | ||||
|     "cmd": "FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										147
									
								
								usermods/JSON_IR_remote/24-key_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,147 @@ | ||||
| { | ||||
|   "desc": "24-key", | ||||
|   "0xF700FF": { | ||||
|     "label": "+", | ||||
|     "pos": "1x1", | ||||
|     "cmnt": "Speed +", | ||||
|     "cmd": "SX=~16" | ||||
|   }, | ||||
|   "0xF7807F": { | ||||
|     "label": "-", | ||||
|     "pos": "1x2", | ||||
|     "cmnt": "Speed -", | ||||
|     "cmd": "SX=~-16" | ||||
|   }, | ||||
|   "0xF740BF": { | ||||
|     "label": "On/Off", | ||||
|     "pos": "1x3", | ||||
|     "cmnt": "Toggle On/Off", | ||||
|     "cmd": "T=2" | ||||
|   }, | ||||
|   "0xF7C03F": { | ||||
|     "label": "W", | ||||
|     "pos": "1x4", | ||||
|     "cmnt": "Cycle color palette", | ||||
|     "cmd": "FP=~" | ||||
|   }, | ||||
|   "0xF720DF": { | ||||
|     "label": "R", | ||||
|     "pos": "2x1", | ||||
|     "cmnt": "Lava", | ||||
|     "cmd": "FP=8" | ||||
|   }, | ||||
|   "0xF7A05F": { | ||||
|     "label": "G", | ||||
|     "pos": "2x2", | ||||
|     "cmnt": "Forest", | ||||
|     "cmd": "FP=10" | ||||
|   }, | ||||
|   "0xF7609F": { | ||||
|     "label": "B", | ||||
|     "pos": "2x3", | ||||
|     "cmnt": "Breeze", | ||||
|     "cmd": "FP=15" | ||||
|   }, | ||||
|   "0xF7E01F": { | ||||
|     "label": "Bright -", | ||||
|     "pos": "2x4", | ||||
|     "cmnt": "Bright -", | ||||
|     "cmd": "A=~-16" | ||||
|   }, | ||||
|   "0xF710EF": { | ||||
|     "label": "Timer1H", | ||||
|     "pos": "3x1", | ||||
|     "cmnt": "Timer 60 min", | ||||
|     "cmd": "NL=60&NT=0" | ||||
|   }, | ||||
|   "0xF7906F": { | ||||
|     "label": "Timer4H", | ||||
|     "pos": "3x2", | ||||
|     "cmnt": "Timer 30 min", | ||||
|     "cmd": "NL=30&NT=0" | ||||
|   }, | ||||
|   "0xF750AF": { | ||||
|     "label": "Timer8H", | ||||
|     "pos": "3x3", | ||||
|     "cmnt": "Timer 15 min", | ||||
|     "cmd": "NL=15&NT=0" | ||||
|   }, | ||||
|   "0xF7D02F": { | ||||
|     "label": "Bright128", | ||||
|     "pos": "3x4", | ||||
|     "cmnt": "Bright 128", | ||||
|     "cmd": "A=128" | ||||
|   }, | ||||
|   "0xF730CF": { | ||||
|     "label": "Music1", | ||||
|     "pos": "4x1", | ||||
|     "cmnt": "Cycle FX +", | ||||
|     "cmd": "FX=~" | ||||
|   }, | ||||
|   "0xF7B04F": { | ||||
|     "label": "Music2", | ||||
|     "pos": "4x2", | ||||
|     "cmnt": "Cycle FX -", | ||||
|     "cmd": "FX=~-1" | ||||
|   }, | ||||
|   "0xF7708F": { | ||||
|     "label": "Music3", | ||||
|     "pos": "4x3", | ||||
|     "cmnt": "Reset FX and FP", | ||||
|     "cmd": "FX=1&PF=6" | ||||
|   }, | ||||
|   "0xF7F00F": { | ||||
|     "label": "Bright +", | ||||
|     "pos": "4x4", | ||||
|     "cmnt": "Bright +", | ||||
|     "cmd": "A=~16" | ||||
|   }, | ||||
|   "0xF708F7": { | ||||
|     "label": "Mode1", | ||||
|     "pos": "5x1", | ||||
|     "cmnt": "Preset 1", | ||||
|     "cmd": "PL=1" | ||||
|   }, | ||||
|   "0xF78877": { | ||||
|     "label": "Mode2", | ||||
|     "pos": "5x2", | ||||
|     "cmnt": "Preset 2", | ||||
|     "cmd": "PL=2" | ||||
|   }, | ||||
|   "0xF748B7": { | ||||
|     "label": "Mode3", | ||||
|     "pos": "5x3", | ||||
|     "cmnt": "Preset 3", | ||||
|     "cmd": "PL=3" | ||||
|   }, | ||||
|   "0xF7C837": { | ||||
|     "label": "Up", | ||||
|     "pos": "5x4", | ||||
|     "cmnt": "Intensity +", | ||||
|     "cmd": "IX=~16" | ||||
|   }, | ||||
|   "0xF728D7": { | ||||
|     "label": "Mode4", | ||||
|     "pos": "6x1", | ||||
|     "cmnt": "Preset 4", | ||||
|     "cmd": "PL=4" | ||||
|   }, | ||||
|   "0xF7A857": { | ||||
|     "label": "Mode5", | ||||
|     "pos": "6x2", | ||||
|     "cmnt": "Preset 5", | ||||
|     "cmd": "PL=5" | ||||
|   }, | ||||
|   "0xF76897": { | ||||
|     "label": "Cycle", | ||||
|     "pos": "6x3", | ||||
|     "cmnt": "Toggle preset cycle", | ||||
|     "cmd": "CY=1&PT=60000" | ||||
|   }, | ||||
|   "0xF7E817": { | ||||
|     "label": "Down", | ||||
|     "pos": "6x4", | ||||
|     "cmnt": "Intensity -", | ||||
|     "cmd": "IX=~-16" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										185
									
								
								usermods/JSON_IR_remote/32-key_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,185 @@ | ||||
| { | ||||
|   "desc": "32-key", | ||||
|   "0xFF08F7": { | ||||
|     "label": "On", | ||||
|     "pos": "1x1", | ||||
|     "cmd": "T=1" | ||||
|   }, | ||||
|   "0xFFC03F": { | ||||
|     "label": "Off", | ||||
|     "pos": "1x2", | ||||
|     "cmd": "T=0" | ||||
|   }, | ||||
|   "0xFF807F": { | ||||
|     "label": "Auto", | ||||
|     "pos": "1x3", | ||||
|     "cmnt": "Toggle preset cycle", | ||||
|     "cmd": "CY=2" | ||||
|   }, | ||||
|   "0xFF609F": { | ||||
|     "label": "Mode", | ||||
|     "pos": "1x4", | ||||
|     "cmnt": "Cycle effects", | ||||
|     "cmd": "FX=~&CY=0" | ||||
|   }, | ||||
|   "0xFF906F": { | ||||
|     "label": "4H", | ||||
|     "pos": "2x1", | ||||
|     "cmnt": "Timer 60min", | ||||
|     "cmd": "NL=60&NT=0" | ||||
|   }, | ||||
|   "0xFFB847": { | ||||
|     "label": "6H", | ||||
|     "pos": "2x2", | ||||
|     "cmnt": "Timer 90min", | ||||
|     "cmd": "NL=90&NT=0" | ||||
|   }, | ||||
|   "0xFFF807": { | ||||
|     "label": "8H", | ||||
|     "pos": "2x3", | ||||
|     "cmnt": "Timer 120min", | ||||
|     "cmd": "NL=120&NT=0" | ||||
|   }, | ||||
|   "0xFFB04F": { | ||||
|     "label": "Timer Off", | ||||
|     "pos": "2x4", | ||||
|     "cmd": "NL=0" | ||||
|   }, | ||||
|   "0xFF9867": { | ||||
|     "label": "Red", | ||||
|     "pos": "3x1", | ||||
|     "cmnt": "Lava", | ||||
|     "cmd": "FP=8" | ||||
|   }, | ||||
|   "0xFFD827": { | ||||
|     "label": "Green", | ||||
|     "pos": "3x2", | ||||
|     "cmnt": "Forest", | ||||
|     "cmd": "FP=10" | ||||
|   }, | ||||
|   "0xFF8877": { | ||||
|     "label": "Blue", | ||||
|     "pos": "3x3", | ||||
|     "cmnt": "Breeze", | ||||
|     "cmd": "FP=15" | ||||
|   }, | ||||
|   "0xFFA857": { | ||||
|     "label": "White", | ||||
|     "pos": "3x4", | ||||
|     "cmd": "FP=5&CL=hFFFFFF&C2=hFFE4CD&C3=hE4E4FF" | ||||
|   }, | ||||
|   "0xFFE817": { | ||||
|     "label": "OrangeRed", | ||||
|     "pos": "4x1", | ||||
|     "cmnt": "Sakura", | ||||
|     "cmd": "FP=49" | ||||
|   }, | ||||
|   "0xFF48B7": { | ||||
|     "label": "SeaGreen", | ||||
|     "pos": "4x2", | ||||
|     "cmnt": "Rivendale", | ||||
|     "cmd": "FP=14" | ||||
|   }, | ||||
|   "0xFF6897": { | ||||
|     "label": "RoyalBlue", | ||||
|     "pos": "4x3", | ||||
|     "cmnt": "Ocean", | ||||
|     "cmd": "FP=9" | ||||
|   }, | ||||
|   "0xFFB24D": { | ||||
|     "label": "DarkBlue", | ||||
|     "pos": "4x4", | ||||
|     "cmnt": "Breeze", | ||||
|     "cmd": "FP=15" | ||||
|   }, | ||||
|   "0xFF02FD": { | ||||
|     "label": "Orange", | ||||
|     "pos": "5x1", | ||||
|     "cmnt": "Orangery", | ||||
|     "cmd": "FP=47" | ||||
|   }, | ||||
|   "0xFF32CD": { | ||||
|     "label": "YellowGreen", | ||||
|     "pos": "5x2", | ||||
|     "cmnt": "Aurora", | ||||
|     "cmd": "FP=37" | ||||
|   }, | ||||
|   "0xFF20DF": { | ||||
|     "label": "SkyBlue", | ||||
|     "pos": "5x3", | ||||
|     "cmnt": "Beech", | ||||
|     "cmd": "FP=22" | ||||
|   }, | ||||
|   "0xFF00FF": { | ||||
|     "label": "Orchid", | ||||
|     "pos": "5x4", | ||||
|     "cmd": "FP=5&CL=hDA70D6&C2=hDA70A0&C3=h89618F" | ||||
|   }, | ||||
|   "0xFF50AF": { | ||||
|     "label": "Yellow", | ||||
|     "pos": "6x1", | ||||
|     "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" | ||||
|   }, | ||||
|   "0xFF7887": { | ||||
|     "label": "DarkGreen", | ||||
|     "pos": "6x2", | ||||
|     "cmnt": "Orange and Teal", | ||||
|     "cmd": "FP=44" | ||||
|   }, | ||||
|   "0xFF708F": { | ||||
|     "label": "RebeccaPurple", | ||||
|     "pos": "6x3", | ||||
|     "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" | ||||
|   }, | ||||
|   "0xFF58A7": { | ||||
|     "label": "Plum", | ||||
|     "pos": "6x4", | ||||
|     "cmd": "FP=5&CL=hDDA0DD&C2=hDDA0BE&C3=h8D7791" | ||||
|   }, | ||||
|   "0xFF38C7": { | ||||
|     "label": "Strobe", | ||||
|     "pos": "7x1", | ||||
|     "cmnt": "Dancing Shadows", | ||||
|     "cmd": "FX=112&CY=0" | ||||
|   }, | ||||
|   "0xFF28D7": { | ||||
|     "label": "In Waves", | ||||
|     "pos": "7x2", | ||||
|     "cmnt": "Noise 1", | ||||
|     "cmd": "FX=70&CY=0" | ||||
|   }, | ||||
|   "0xFFF00F": { | ||||
|     "label": "Speed +", | ||||
|     "pos": "7x3", | ||||
|     "cmd": "SX=~16" | ||||
|   }, | ||||
|   "0xFF30CF": { | ||||
|     "label": "Speed -", | ||||
|     "pos": "7x4", | ||||
|     "cmd": "SX=~-16" | ||||
|   }, | ||||
|   "0xFF40BF": { | ||||
|     "label": "Jump", | ||||
|     "pos": "8x1", | ||||
|     "cmnt": "Colortwinkles", | ||||
|     "cmd": "FX=74&CY=0" | ||||
|   }, | ||||
|   "0xFF12ED": { | ||||
|     "label": "Fade", | ||||
|     "pos": "8x2", | ||||
|     "cmnt": "Sunrise", | ||||
|     "cmd": "FX=104&CY=0" | ||||
|   }, | ||||
|   "0xFF2AD5": { | ||||
|     "label": "Flash", | ||||
|     "pos": "8x3", | ||||
|     "cmnt": "Railway", | ||||
|     "cmd": "FX=78&CY=0" | ||||
|   }, | ||||
|   "0xFFA05F": { | ||||
|     "label": "Chase Flash", | ||||
|     "pos": "8x4", | ||||
|     "cmnt": "Washing Machine", | ||||
|     "cmd": "FX=113&CY=0" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										233
									
								
								usermods/JSON_IR_remote/40-key-black_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,233 @@ | ||||
| { | ||||
|   "desc": "40-key-black", | ||||
|   "0xFF3AC5": { | ||||
|     "label": "Bright +", | ||||
|     "pos": "1x1", | ||||
|     "cmd": "A=~16" | ||||
|   }, | ||||
|   "0xFFBA45": { | ||||
|     "label": "Bright -", | ||||
|     "pos": "1x2", | ||||
|     "cmd": "A=~-16" | ||||
|   }, | ||||
|   "0xFF827D": { | ||||
|     "label": "Off", | ||||
|     "pos": "1x3", | ||||
|     "cmd": "T=0" | ||||
|   }, | ||||
|   "0xFF02FD": { | ||||
|     "label": "On", | ||||
|     "pos": "1x4", | ||||
|     "cmd": "T=1" | ||||
|   }, | ||||
|   "0xFF1AE5": { | ||||
|     "label": "Red", | ||||
|     "pos": "2x1", | ||||
|     "cmnt": "Lava", | ||||
|     "cmd": "FP=8" | ||||
|   }, | ||||
|   "0xFF9A65": { | ||||
|     "label": "Green", | ||||
|     "pos": "2x2", | ||||
|     "cmnt": "Forest", | ||||
|     "cmd": "FP=10" | ||||
|   }, | ||||
|   "0xFFA25D": { | ||||
|     "label": "Blue", | ||||
|     "pos": "2x3", | ||||
|     "cmnt": "Breeze", | ||||
|     "cmd": "FP=15" | ||||
|   }, | ||||
|   "0xFF22DD": { | ||||
|     "label": "White", | ||||
|     "pos": "2x4", | ||||
|     "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" | ||||
|   }, | ||||
|   "0xFF2AD5": { | ||||
|     "label": "Tomato", | ||||
|     "pos": "3x1", | ||||
|     "cmnt": "Yelmag", | ||||
|     "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" | ||||
|   }, | ||||
|   "0xFFAA55": { | ||||
|     "label": "LightGreen", | ||||
|     "pos": "3x2", | ||||
|     "cmnt": "Rivendale", | ||||
|     "cmd": "FP=14" | ||||
|   }, | ||||
|   "0xFF926D": { | ||||
|     "label": "SkyBlue", | ||||
|     "pos": "3x3", | ||||
|     "cmnt": "Ocean", | ||||
|     "cmd": "FP=9" | ||||
|   }, | ||||
|   "0xFF12ED": { | ||||
|     "label": "WarmWhite", | ||||
|     "pos": "3x4", | ||||
|     "cmnt": "Warm White", | ||||
|     "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" | ||||
|   }, | ||||
|   "0xFF0AF5": { | ||||
|     "label": "OrangeRed", | ||||
|     "pos": "4x1", | ||||
|     "cmnt": "Sakura", | ||||
|     "cmd": "FP=49" | ||||
|   }, | ||||
|   "0xFF8A75": { | ||||
|     "label": "Cyan", | ||||
|     "pos": "4x2", | ||||
|     "cmnt": "Beech", | ||||
|     "cmd": "FP=22" | ||||
|   }, | ||||
|   "0xFFB24D": { | ||||
|     "label": "RebeccaPurple", | ||||
|     "pos": "4x3", | ||||
|     "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" | ||||
|   }, | ||||
|   "0xFF32CD": { | ||||
|     "label": "CoolWhite", | ||||
|     "pos": "4x4", | ||||
|     "cmnt": "Cool White", | ||||
|     "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" | ||||
|   }, | ||||
|   "0xFF38C7": { | ||||
|     "label": "Orange", | ||||
|     "pos": "5x1", | ||||
|     "cmnt": "Orangery", | ||||
|     "cmd": "FP=47" | ||||
|   }, | ||||
|   "0xFFB847": { | ||||
|     "label": "Turquoise", | ||||
|     "pos": "5x2", | ||||
|     "cmd": "FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381" | ||||
|   }, | ||||
|   "0xFF7887": { | ||||
|     "label": "Purple", | ||||
|     "pos": "5x3", | ||||
|     "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" | ||||
|   }, | ||||
|   "0xFFF807": { | ||||
|     "label": "MedGray", | ||||
|     "pos": "5x4", | ||||
|     "cmnt": "Cycle palette +", | ||||
|     "cmd": "FP=~" | ||||
|   }, | ||||
|   "0xFF18E7": { | ||||
|     "label": "Yellow", | ||||
|     "pos": "6x1", | ||||
|     "cmd": "FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539" | ||||
|   }, | ||||
|   "0xFF9867": { | ||||
|     "label": "DarkCyan", | ||||
|     "pos": "6x2", | ||||
|     "cmd": "FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51" | ||||
|   }, | ||||
|   "0xFF58A7": { | ||||
|     "label": "Plum", | ||||
|     "pos": "6x3", | ||||
|     "cmnt": "Magenta", | ||||
|     "cmd": "FP=40" | ||||
|   }, | ||||
|   "0xFFD827": { | ||||
|     "label": "DarkGray", | ||||
|     "pos": "6x4", | ||||
|     "cmnt": "Cycle palette -", | ||||
|     "cmd": "FP=~-" | ||||
|   }, | ||||
|   "0xFF28D7": { | ||||
|     "label": "Jump3", | ||||
|     "pos": "7x1", | ||||
|     "cmnt": "Colortwinkles", | ||||
|     "cmd": "CY=0&FX=74" | ||||
|   }, | ||||
|   "0xFFA857": { | ||||
|     "label": "Fade3", | ||||
|     "pos": "7x2", | ||||
|     "cmnt": "Rain", | ||||
|     "cmd": "CY=0&FX=43" | ||||
|   }, | ||||
|   "0xFF6897": { | ||||
|     "label": "Flash", | ||||
|     "pos": "7x3", | ||||
|     "cmnt": "Cycle Effects", | ||||
|     "cmd": "CY=0&FX=~" | ||||
|   }, | ||||
|   "0xFFE817": { | ||||
|     "label": "Quick", | ||||
|     "pos": "7x4", | ||||
|     "cmnt": "Fx speed +16", | ||||
|     "cmd": "SX=~16" | ||||
|   }, | ||||
|   "0xFF08F7": { | ||||
|     "label": "Jump7", | ||||
|     "pos": "8x1", | ||||
|     "cmnt": "Sinelon Dual", | ||||
|     "cmd": "CY=0&FX=93" | ||||
|   }, | ||||
|   "0xFF8877": { | ||||
|     "label": "Fade7", | ||||
|     "pos": "8x2", | ||||
|     "cmnt": "Lighthouse", | ||||
|     "cmd": "CY=0&FX=41" | ||||
|   }, | ||||
|   "0xFF48B7": { | ||||
|     "label": "Auto", | ||||
|     "pos": "8x3", | ||||
|     "cmnt": "Toggle preset cycle", | ||||
|     "cmd": "CY=2" | ||||
|   }, | ||||
|   "0xFFC837": { | ||||
|     "label": "Slow", | ||||
|     "pos": "8x4", | ||||
|     "cmnt": "FX speed -16", | ||||
|     "cmd": "SX=~-16" | ||||
|   }, | ||||
|   "0xFF30CF": { | ||||
|     "label": "Custom1", | ||||
|     "pos": "9x1", | ||||
|     "cmnt": "Noise 1", | ||||
|     "cmd": "CY=0&FX=70" | ||||
|   }, | ||||
|   "0xFFB04F": { | ||||
|     "label": "Custom2", | ||||
|     "pos": "9x2", | ||||
|     "cmnt": "Dancing Shadows", | ||||
|     "cmd": "CY=0&FX=112" | ||||
|   }, | ||||
|   "0xFF708F": { | ||||
|     "label": "Music +", | ||||
|     "pos": "9x3", | ||||
|     "cmnt": "FX Intensity +16", | ||||
|     "cmd": "IX=~16" | ||||
|   }, | ||||
|   "0xFFF00F": { | ||||
|     "label": "Timer60", | ||||
|     "pos": "9x4", | ||||
|     "cmnt": "Timer 60 min", | ||||
|     "cmd": "NL=60&NT=0" | ||||
|   }, | ||||
|   "0xFF10EF": { | ||||
|     "label": "Custom3", | ||||
|     "pos": "10x1", | ||||
|     "cmnt": "Twinklefox", | ||||
|     "cmd": "CY=0&FX=80" | ||||
|   }, | ||||
|   "0xFF906F": { | ||||
|     "label": "Custom4", | ||||
|     "pos": "10x2", | ||||
|     "cmnt": "Twinklecat", | ||||
|     "cmd": "CY=0&FX=81" | ||||
|   }, | ||||
|   "0xFF50AF": { | ||||
|     "label": "Music -", | ||||
|     "pos": "10x3", | ||||
|     "cmnt": "FX Intesity -16", | ||||
|     "cmd": "IX=~-16" | ||||
|   }, | ||||
|   "0xFFD02F": { | ||||
|     "label": "Timer120", | ||||
|     "pos": "10x4", | ||||
|     "cmnt": "Timer 120 min", | ||||
|     "cmd": "NL=120&NT=0" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										217
									
								
								usermods/JSON_IR_remote/40-key-blue_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,217 @@ | ||||
| { | ||||
|   "desc": "40-key-blue", | ||||
|   "0xFF3AC5": { | ||||
|     "label": "Bright +", | ||||
|     "pos": "1x1", | ||||
|     "cmd": "A=~16" | ||||
|   }, | ||||
|   "0xFFBA45": { | ||||
|     "label": "Bright -", | ||||
|     "pos": "1x2", | ||||
|     "cmd": "A=~-16" | ||||
|   }, | ||||
|   "0xFF827D": { | ||||
|     "label": "Off", | ||||
|     "pos": "1x3", | ||||
|     "cmd": "T=0" | ||||
|   }, | ||||
|   "0xFF02FD": { | ||||
|     "label": "On", | ||||
|     "pos": "1x4", | ||||
|     "cmd": "T=1" | ||||
|   }, | ||||
|   "0xFF1AE5": { | ||||
|     "label": "Red", | ||||
|     "pos": "2x1", | ||||
|     "cmnt": "Lava", | ||||
|     "cmd": "FP=8" | ||||
|   }, | ||||
|   "0xFF9A65": { | ||||
|     "label": "Green", | ||||
|     "pos": "2x2", | ||||
|     "cmnt": "Forest", | ||||
|     "cmd": "FP=10" | ||||
|   }, | ||||
|   "0xFFA25D": { | ||||
|     "label": "Blue", | ||||
|     "pos": "2x3", | ||||
|     "cmnt": "Breeze", | ||||
|     "cmd": "FP=15" | ||||
|   }, | ||||
|   "0xFF22DD": { | ||||
|     "label": "White", | ||||
|     "pos": "2x4", | ||||
|     "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" | ||||
|   }, | ||||
|   "0xFF2AD5": { | ||||
|     "label": "Tomato", | ||||
|     "pos": "3x1", | ||||
|     "cmnt": "Yelmag", | ||||
|     "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" | ||||
|   }, | ||||
|   "0xFFAA55": { | ||||
|     "label": "LightGreen", | ||||
|     "pos": "3x2", | ||||
|     "cmnt": "Rivendale", | ||||
|     "cmd": "FP=14" | ||||
|   }, | ||||
|   "0xFF926D": { | ||||
|     "label": "SkyBlue", | ||||
|     "pos": "3x3", | ||||
|     "cmnt": "Ocean", | ||||
|     "cmd": "FP=9" | ||||
|   }, | ||||
|   "0xFF12ED": { | ||||
|     "label": "WarmWhite", | ||||
|     "pos": "3x4", | ||||
|     "cmnt": "Warm White", | ||||
|     "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" | ||||
|   }, | ||||
|   "0xFF0AF5": { | ||||
|     "label": "OrangeRed", | ||||
|     "pos": "4x1", | ||||
|     "cmnt": "Sakura", | ||||
|     "cmd": "FP=49" | ||||
|   }, | ||||
|   "0xFF8A75": { | ||||
|     "label": "Cyan", | ||||
|     "pos": "4x2", | ||||
|     "cmnt": "Beech", | ||||
|     "cmd": "FP=22" | ||||
|   }, | ||||
|   "0xFFB24D": { | ||||
|     "label": "RebeccaPurple", | ||||
|     "pos": "4x3", | ||||
|     "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" | ||||
|   }, | ||||
|   "0xFF32CD": { | ||||
|     "label": "CoolWhite", | ||||
|     "pos": "4x4", | ||||
|     "cmnt": "Cool White", | ||||
|     "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" | ||||
|   }, | ||||
|   "0xFF38C7": { | ||||
|     "label": "Orange", | ||||
|     "pos": "5x1", | ||||
|     "cmnt": "Orangery", | ||||
|     "cmd": "FP=47" | ||||
|   }, | ||||
|   "0xFFB847": { | ||||
|     "label": "Turquoise", | ||||
|     "pos": "5x2", | ||||
|     "cmd": "FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381" | ||||
|   }, | ||||
|   "0xFF7887": { | ||||
|     "label": "Purple", | ||||
|     "pos": "5x3", | ||||
|     "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" | ||||
|   }, | ||||
|   "0xFFF807": { | ||||
|     "label": "MedGray", | ||||
|     "pos": "5x4", | ||||
|     "cmnt": "Cycle palette +", | ||||
|     "cmd": "FP=~" | ||||
|   }, | ||||
|   "0xFF18E7": { | ||||
|     "label": "Yellow", | ||||
|     "pos": "6x1", | ||||
|     "cmd": "FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539" | ||||
|   }, | ||||
|   "0xFF9867": { | ||||
|     "label": "DarkCyan", | ||||
|     "pos": "6x2", | ||||
|     "cmd": "FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51" | ||||
|   }, | ||||
|   "0xFF58A7": { | ||||
|     "label": "Plum", | ||||
|     "pos": "6x3", | ||||
|     "cmnt": "Magenta", | ||||
|     "cmd": "FP=40" | ||||
|   }, | ||||
|   "0xFFD827": { | ||||
|     "label": "DarkGray", | ||||
|     "pos": "6x4", | ||||
|     "cmnt": "Cycle palette -", | ||||
|     "cmd": "FP=~-" | ||||
|   }, | ||||
|   "0xFF28D7": { | ||||
|     "label": "W +", | ||||
|     "pos": "7x1" | ||||
|   }, | ||||
|   "0xFFA857": { | ||||
|     "label": "W -", | ||||
|     "pos": "7x2" | ||||
|   }, | ||||
|   "0xFF6897": { | ||||
|     "label": "W On", | ||||
|     "pos": "7x3" | ||||
|   }, | ||||
|   "0xFFE817": { | ||||
|     "label": "W Off", | ||||
|     "pos": "7x4" | ||||
|   }, | ||||
|   "0xFF08F7": { | ||||
|     "label": "W25", | ||||
|     "pos": "8x1" | ||||
|   }, | ||||
|   "0xFF8877": { | ||||
|     "label": "W50", | ||||
|     "pos": "8x2" | ||||
|   }, | ||||
|   "0xFF48B7": { | ||||
|     "label": "W75", | ||||
|     "pos": "8x3" | ||||
|   }, | ||||
|   "0xFFC837": { | ||||
|     "label": "W100", | ||||
|     "pos": "8x4" | ||||
|   }, | ||||
|   "0xFF30CF": { | ||||
|     "label": "Jump3", | ||||
|     "pos": "9x1", | ||||
|     "cmnt": "Colortwinkles", | ||||
|     "cmd": "CY=0&FX=74" | ||||
|   }, | ||||
|   "0xFFB04F": { | ||||
|     "label": "Fade3", | ||||
|     "pos": "9x2", | ||||
|     "cmnt": "Rain", | ||||
|     "cmd": "CY=0&FX=43" | ||||
|   }, | ||||
|   "0xFF708F": { | ||||
|     "label": "Jump7", | ||||
|     "pos": "9x3", | ||||
|     "cmnt": "Sinelon Dual", | ||||
|     "cmd": "CY=0&FX=93" | ||||
|   }, | ||||
|   "0xFFF00F": { | ||||
|     "label": "Quick", | ||||
|     "pos": "9x4", | ||||
|     "cmnt": "Fx speed +16", | ||||
|     "cmd": "SX=~16" | ||||
|   }, | ||||
|   "0xFF10EF": { | ||||
|     "label": "Fade", | ||||
|     "pos": "10x1", | ||||
|     "cmnt": "Lighthouse", | ||||
|     "cmd": "CY=0&FX=41" | ||||
|   }, | ||||
|   "0xFF906F": { | ||||
|     "label": "Flash", | ||||
|     "pos": "10x2", | ||||
|     "cmnt": "Cycle Effects", | ||||
|     "cmd": "CY=0&FX=~" | ||||
|   }, | ||||
|   "0xFF50AF": { | ||||
|     "label": "Auto", | ||||
|     "pos": "10x3", | ||||
|     "cmnt": "Toggle preset cycle", | ||||
|     "cmd": "CY=2" | ||||
|   }, | ||||
|   "0xFFD02F": { | ||||
|     "label": "Slow", | ||||
|     "pos": "10x4", | ||||
|     "cmnt": "Sinelon Dual", | ||||
|     "cmd": "CY=0&FX=93" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										241
									
								
								usermods/JSON_IR_remote/44-key_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,241 @@ | ||||
| { | ||||
|   "desc": "44-key", | ||||
|   "0xFF3AC5": { | ||||
|     "label": "Bright +", | ||||
|     "pos": "1x1", | ||||
|     "cmd": "A=~16" | ||||
|   }, | ||||
|   "0xFFBA45": { | ||||
|     "label": "Bright -", | ||||
|     "pos": "1x2", | ||||
|     "cmd": "A=~-16" | ||||
|   }, | ||||
|   "0xFF827D": { | ||||
|     "label": "Off", | ||||
|     "pos": "1x3", | ||||
|     "cmd": "T=0" | ||||
|   }, | ||||
|   "0xFF02FD": { | ||||
|     "label": "On", | ||||
|     "pos": "1x4", | ||||
|     "cmd": "T=1" | ||||
|   }, | ||||
|   "0xFF1AE5": { | ||||
|     "label": "Red", | ||||
|     "pos": "2x1", | ||||
|     "cmnt": "Lava", | ||||
|     "cmd": "FP=8" | ||||
|   }, | ||||
|   "0xFF9A65": { | ||||
|     "label": "Green", | ||||
|     "pos": "2x2", | ||||
|     "cmnt": "Forest", | ||||
|     "cmd": "FP=10" | ||||
|   }, | ||||
|   "0xFFA25D": { | ||||
|     "label": "Blue", | ||||
|     "pos": "2x3", | ||||
|     "cmnt": "Breeze", | ||||
|     "cmd": "FP=15" | ||||
|   }, | ||||
|   "0xFF22DD": { | ||||
|     "label": "White", | ||||
|     "pos": "2x4", | ||||
|     "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" | ||||
|   }, | ||||
|   "0xFF2AD5": { | ||||
|     "label": "Tomato", | ||||
|     "pos": "3x1", | ||||
|     "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" | ||||
|   }, | ||||
|   "0xFFAA55": { | ||||
|     "label": "LightGreen", | ||||
|     "pos": "3x2", | ||||
|     "cmnt": "Rivendale", | ||||
|     "cmd": "FP=14" | ||||
|   }, | ||||
|   "0xFF926D": { | ||||
|     "label": "DeepBlue", | ||||
|     "pos": "3x3", | ||||
|     "cmnt": "Ocean", | ||||
|     "cmd": "FP=9" | ||||
|   }, | ||||
|   "0xFF12ED": { | ||||
|     "label": "Warmwhite2", | ||||
|     "pos": "3x4", | ||||
|     "cmnt": "Warm White", | ||||
|     "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" | ||||
|   }, | ||||
|   "0xFF0AF5": { | ||||
|     "label": "Orange", | ||||
|     "pos": "4x1", | ||||
|     "cmnt": "Sakura", | ||||
|     "cmd": "FP=49" | ||||
|   }, | ||||
|   "0xFF8A75": { | ||||
|     "label": "Turquoise", | ||||
|     "pos": "4x2", | ||||
|     "cmnt": "Beech", | ||||
|     "cmd": "FP=22" | ||||
|   }, | ||||
|   "0xFFB24D": { | ||||
|     "label": "Purple", | ||||
|     "pos": "4x3", | ||||
|     "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" | ||||
|   }, | ||||
|   "0xFF32CD": { | ||||
|     "label": "WarmWhite", | ||||
|     "pos": "4x4", | ||||
|     "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" | ||||
|   }, | ||||
|   "0xFF38C7": { | ||||
|     "label": "Yellowish", | ||||
|     "pos": "5x1", | ||||
|     "cmnt": "Orangery", | ||||
|     "cmd": "FP=47" | ||||
|   }, | ||||
|   "0xFFB847": { | ||||
|     "label": "Cyan", | ||||
|     "pos": "5x2", | ||||
|     "cmnt": "Beech", | ||||
|     "cmd": "FP=22" | ||||
|   }, | ||||
|   "0xFF7887": { | ||||
|     "label": "Magenta", | ||||
|     "pos": "5x3", | ||||
|     "cmd": "FP=5&CL=hFF00FF&C2=hFF007F&C3=h9539A8" | ||||
|   }, | ||||
|   "0xFFF807": { | ||||
|     "label": "ColdWhite", | ||||
|     "pos": "5x4", | ||||
|     "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" | ||||
|   }, | ||||
|   "0xFF18E7": { | ||||
|     "label": "Yellow", | ||||
|     "pos": "6x1", | ||||
|     "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" | ||||
|   }, | ||||
|   "0xFF9867": { | ||||
|     "label": "Aqua", | ||||
|     "pos": "6x2", | ||||
|     "cmd": "FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895" | ||||
|   }, | ||||
|   "0xFF58A7": { | ||||
|     "label": "Pink", | ||||
|     "pos": "6x3", | ||||
|     "cmd": "FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96" | ||||
|   }, | ||||
|   "0xFFD827": { | ||||
|     "label": "ColdWhite2", | ||||
|     "pos": "6x4", | ||||
|     "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" | ||||
|   }, | ||||
|   "0xFF28D7": { | ||||
|     "label": "Red +", | ||||
|     "pos": "7x1", | ||||
|     "cmd": "FP=5&R=~16" | ||||
|   }, | ||||
|   "0xFFA857": { | ||||
|     "label": "Green +", | ||||
|     "pos": "7x2", | ||||
|     "cmd": "FP=5&G=~16" | ||||
|   }, | ||||
|   "0xFF6897": { | ||||
|     "label": "Blue +", | ||||
|     "pos": "7x3", | ||||
|     "cmd": "FP=5&B=~16" | ||||
|   }, | ||||
|   "0xFFE817": { | ||||
|     "label": "Quick", | ||||
|     "pos": "7x4", | ||||
|     "cmnt": "Fx speed +16", | ||||
|     "cmd": "SX=~16" | ||||
|   }, | ||||
|   "0xFF08F7": { | ||||
|     "label": "Red -", | ||||
|     "pos": "8x1", | ||||
|     "cmd": "FP=5&R=~-16" | ||||
|   }, | ||||
|   "0xFF8877": { | ||||
|     "label": "Green -", | ||||
|     "pos": "8x2", | ||||
|     "cmd": "FP=5&G=~-16" | ||||
|   }, | ||||
|   "0xFF48B7": { | ||||
|     "label": "Blue -", | ||||
|     "pos": "8x3", | ||||
|     "cmd": "FP=5&B=~-16" | ||||
|   }, | ||||
|   "0xFFC837": { | ||||
|     "label": "Slow", | ||||
|     "pos": "8x4", | ||||
|     "cmnt": "FX speed -16", | ||||
|     "cmd": "SX=~-16" | ||||
|   }, | ||||
|   "0xFF30CF": { | ||||
|     "label": "Diy1", | ||||
|     "pos": "9x1", | ||||
|     "cmd": "CY=0&PL=1" | ||||
|   }, | ||||
|   "0xFFB04F": { | ||||
|     "label": "Diy2", | ||||
|     "pos": "9x2", | ||||
|     "cmd": "CY=0&PL=2" | ||||
|   }, | ||||
|   "0xFF708F": { | ||||
|     "label": "Diy3", | ||||
|     "pos": "9x3", | ||||
|     "cmd": "CY=0&PL=3" | ||||
|   }, | ||||
|   "0xFFF00F": { | ||||
|     "label": "Auto", | ||||
|     "pos": "9x4", | ||||
|     "cmnt": "Toggle preset cycle", | ||||
|     "cmd": "CY=2" | ||||
|   }, | ||||
|   "0xFF10EF": { | ||||
|     "label": "Diy4", | ||||
|     "pos": "10x1", | ||||
|     "cmd": "CY=0&PL=4" | ||||
|   }, | ||||
|   "0xFF906F": { | ||||
|     "label": "Diy5", | ||||
|     "pos": "10x2", | ||||
|     "cmd": "CY=0&PL=5" | ||||
|   }, | ||||
|   "0xFF50AF": { | ||||
|     "label": "Diy6", | ||||
|     "pos": "10x3", | ||||
|     "cmd": "CY=0&PL=6" | ||||
|   }, | ||||
|   "0xFFD02F": { | ||||
|     "label": "Flash", | ||||
|     "pos": "10x4", | ||||
|     "cmnt": "Cycle Effects", | ||||
|     "cmd": "CY=0&FX=~" | ||||
|   }, | ||||
|   "0xFF20DF": { | ||||
|     "label": "Jump3", | ||||
|     "pos": "11x1", | ||||
|     "cmnt": "Colortwinkles", | ||||
|     "cmd": "CY=0&FX=74" | ||||
|   }, | ||||
|   "0xFFA05F": { | ||||
|     "label": "Jump7", | ||||
|     "pos": "11x2", | ||||
|     "cmnt": "Sinelon Dual", | ||||
|     "cmd": "CY=0&FX=93" | ||||
|   }, | ||||
|   "0xFF609F": { | ||||
|     "label": "Fade3", | ||||
|     "pos": "11x3", | ||||
|     "cmnt": "Rain", | ||||
|     "cmd": "CY=0&FX=43" | ||||
|   }, | ||||
|   "0xFFE01F": { | ||||
|     "label": "Fade7", | ||||
|     "pos": "11x4", | ||||
|     "cmnt": "Lighthouse", | ||||
|     "cmd": "CY=0&FX=41" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										38
									
								
								usermods/JSON_IR_remote/6-key_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,38 @@ | ||||
| { | ||||
|   "desc": "6-key", | ||||
|   "0xFF0FF0": { | ||||
|     "label": "Power", | ||||
|     "pos": "1x1", | ||||
|     "cmd": "T=2" | ||||
|   }, | ||||
|   "0xFF8F70": { | ||||
|     "label": "Channel +", | ||||
|     "pos": "2x1", | ||||
|     "cmnt": "Cycle palette up", | ||||
|     "cmd": "FP=~" | ||||
|   }, | ||||
|   "0xFF4FB0": { | ||||
|     "label": "Channel -", | ||||
|     "pos": "3x1", | ||||
|     "cmnt": "Cycle palette down", | ||||
|     "cmd": "FP=~-" | ||||
|   }, | ||||
|   "0xFFCF30": { | ||||
|     "label": "Volume +", | ||||
|     "pos": "4x1", | ||||
|     "cmnt": "Brighten", | ||||
|     "cmd": "A=~16" | ||||
|   }, | ||||
|   "0xFF2FD0": { | ||||
|     "label": "Volume -", | ||||
|     "pos": "5x1", | ||||
|     "cmnt": "Dim", | ||||
|     "cmd": "A=~-16" | ||||
|   }, | ||||
|   "0xFFAF50": { | ||||
|     "label": "Mute", | ||||
|     "pos": "6x1", | ||||
|     "cmnt": "Cycle effects", | ||||
|     "cmd": "CY=0&FX=~" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										47
									
								
								usermods/JSON_IR_remote/9-key_ir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
| { | ||||
|   "desc": "9-key", | ||||
|   "0xFF629D": { | ||||
|     "label": "Power", | ||||
|     "cmd": "T=2" | ||||
|   }, | ||||
|   "0xFF22DD": { | ||||
|     "label": "A", | ||||
|     "cmnt": "Preset 1", | ||||
|     "cmd": "PL=1" | ||||
|   }, | ||||
|   "0xFF02FD": { | ||||
|     "label": "B", | ||||
|     "cmnt": "Preset 2", | ||||
|     "cmd": "PL=2" | ||||
|   }, | ||||
|   "0xFFC23D": { | ||||
|     "label": "C", | ||||
|     "cmnt": "Preset 3", | ||||
|     "cmd": "PL=3" | ||||
|   }, | ||||
|   "0xFF30CF": { | ||||
|     "label": "Left", | ||||
|     "cmnt": "Speed -", | ||||
|     "cmd": "SI=~-16" | ||||
|   }, | ||||
|   "0xFF7A85": { | ||||
|     "label": "Right", | ||||
|     "cmnt": "Speed +", | ||||
|     "cmd": "SI=~16" | ||||
|   }, | ||||
|   "0xFF9867": { | ||||
|     "label": "Up", | ||||
|     "cmnt": "Bright +", | ||||
|     "cmd": "A=~16" | ||||
|   }, | ||||
|   "0xFF38C7": { | ||||
|     "label": "Down", | ||||
|     "cmnt": "Bright -", | ||||
|     "cmd": "A=~-16" | ||||
|   }, | ||||
|   "0xFF18E7": { | ||||
|     "label": "Select", | ||||
|     "cmnt": "Cycle effects", | ||||
|     "cmd": "CY=0&FX=~" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								usermods/JSON_IR_remote/IR_Remote_Codes.xlsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										108
									
								
								usermods/JSON_IR_remote/ir_json_maker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,108 @@ | ||||
| import colorsys | ||||
| import json | ||||
| import openpyxl | ||||
|  | ||||
| named_colors = {'AliceBlue': '0xF0F8FF', 'AntiqueWhite': '0xFAEBD7', 'Aqua': '0x00FFFF', | ||||
|                 'Aquamarine': '0x7FFFD4', 'Azure': '0xF0FFFF', 'Beige': '0xF5F5DC', 'Bisque': '0xFFE4C4', | ||||
|                 'Black': '0x000000', 'BlanchedAlmond': '0xFFEBCD', 'Blue': '0x0000FF', | ||||
|                 'BlueViolet': '0x8A2BE2', 'Brown': '0xA52A2A', 'BurlyWood': '0xDEB887', | ||||
|                 'CadetBlue': '0x5F9EA0', 'Chartreuse': '0x7FFF00', 'Chocolate': '0xD2691E', | ||||
|                 'Coral': '0xFF7F50', 'CornflowerBlue': '0x6495ED', 'Cornsilk': '0xFFF8DC', | ||||
|                 'Crimson': '0xDC143C', 'Cyan': '0x00FFFF', 'DarkBlue': '0x00008B', 'DarkCyan': '0x008B8B', | ||||
|                 'DarkGoldenRod': '0xB8860B', 'DarkGray': '0xA9A9A9', 'DarkGrey': '0xA9A9A9', | ||||
|                 'DarkGreen': '0x006400', 'DarkKhaki': '0xBDB76B', 'DarkMagenta': '0x8B008B', | ||||
|                 'DarkOliveGreen': '0x556B2F', 'DarkOrange': '0xFF8C00', 'DarkOrchid': '0x9932CC', | ||||
|                 'DarkRed': '0x8B0000', 'DarkSalmon': '0xE9967A', 'DarkSeaGreen': '0x8FBC8F', | ||||
|                 'DarkSlateBlue': '0x483D8B', 'DarkSlateGray': '0x2F4F4F', 'DarkSlateGrey': '0x2F4F4F', | ||||
|                 'DarkTurquoise': '0x00CED1', 'DarkViolet': '0x9400D3', 'DeepPink': '0xFF1493', | ||||
|                 'DeepSkyBlue': '0x00BFFF', 'DimGray': '0x696969', 'DimGrey': '0x696969', | ||||
|                 'DodgerBlue': '0x1E90FF', 'FireBrick': '0xB22222', 'FloralWhite': '0xFFFAF0', | ||||
|                 'ForestGreen': '0x228B22', 'Fuchsia': '0xFF00FF', 'Gainsboro': '0xDCDCDC', | ||||
|                 'GhostWhite': '0xF8F8FF', 'Gold': '0xFFD700', 'GoldenRod': '0xDAA520', 'Gray': '0x808080', | ||||
|                 'Grey': '0x808080', 'Green': '0x008000', 'GreenYellow': '0xADFF2F', 'HoneyDew': '0xF0FFF0', | ||||
|                 'HotPink': '0xFF69B4', 'IndianRed': '0xCD5C5C', 'Indigo': '0x4B0082', 'Ivory': '0xFFFFF0', | ||||
|                 'Khaki': '0xF0E68C', 'Lavender': '0xE6E6FA', 'LavenderBlush': '0xFFF0F5', | ||||
|                 'LawnGreen': '0x7CFC00', 'LemonChiffon': '0xFFFACD', 'LightBlue': '0xADD8E6', | ||||
|                 'LightCoral': '0xF08080', 'LightCyan': '0xE0FFFF', 'LightGoldenRodYellow': '0xFAFAD2', | ||||
|                 'LightGray': '0xD3D3D3', 'LightGrey': '0xD3D3D3', 'LightGreen': '0x90EE90', | ||||
|                 'LightPink': '0xFFB6C1', 'LightSalmon': '0xFFA07A', 'LightSeaGreen': '0x20B2AA', | ||||
|                 'LightSkyBlue': '0x87CEFA', 'LightSlateGray': '0x778899', 'LightSlateGrey': '0x778899', | ||||
|                 'LightSteelBlue': '0xB0C4DE', 'LightYellow': '0xFFFFE0', 'Lime': '0x00FF00', | ||||
|                 'LimeGreen': '0x32CD32', 'Linen': '0xFAF0E6', 'Magenta': '0xFF00FF', 'Maroon': '0x800000', | ||||
|                 'MediumAquaMarine': '0x66CDAA', 'MediumBlue': '0x0000CD', 'MediumOrchid': '0xBA55D3', | ||||
|                 'MediumPurple': '0x9370DB', 'MediumSeaGreen': '0x3CB371', 'MediumSlateBlue': '0x7B68EE', | ||||
|                 'MediumSpringGreen': '0x00FA9A', 'MediumTurquoise': '0x48D1CC', 'MediumVioletRed': '0xC71585', | ||||
|                 'MidnightBlue': '0x191970', 'MintCream': '0xF5FFFA', 'MistyRose': '0xFFE4E1', | ||||
|                 'Moccasin': '0xFFE4B5', 'NavajoWhite': '0xFFDEAD', 'Navy': '0x000080', 'OldLace': '0xFDF5E6', | ||||
|                 'Olive': '0x808000', 'OliveDrab': '0x6B8E23', 'Orange': '0xFFA500', 'OrangeRed': '0xFF4500', | ||||
|                 'Orchid': '0xDA70D6', 'PaleGoldenRod': '0xEEE8AA', 'PaleGreen': '0x98FB98', | ||||
|                 'PaleTurquoise': '0xAFEEEE', 'PaleVioletRed': '0xDB7093', 'PapayaWhip': '0xFFEFD5', | ||||
|                 'PeachPuff': '0xFFDAB9', 'Peru': '0xCD853F', 'Pink': '0xFFC0CB', 'Plum': '0xDDA0DD', | ||||
|                 'PowderBlue': '0xB0E0E6', 'Purple': '0x800080', 'RebeccaPurple': '0x663399', 'Red': '0xFF0000', | ||||
|                 'RosyBrown': '0xBC8F8F', 'RoyalBlue': '0x4169E1', 'SaddleBrown': '0x8B4513', 'Salmon': '0xFA8072', | ||||
|                 'SandyBrown': '0xF4A460', 'SeaGreen': '0x2E8B57', 'SeaShell': '0xFFF5EE', 'Sienna': '0xA0522D', | ||||
|                 'Silver': '0xC0C0C0', 'SkyBlue': '0x87CEEB', 'SlateBlue': '0x6A5ACD', 'SlateGray': '0x708090', | ||||
|                 'SlateGrey': '0x708090', 'Snow': '0xFFFAFA', 'SpringGreen': '0x00FF7F', 'SteelBlue': '0x4682B4', | ||||
|                 'Tan': '0xD2B48C', 'Teal': '0x008080', 'Thistle': '0xD8BFD8', 'Tomato': '0xFF6347', | ||||
|                 'Turquoise': '0x40E0D0', 'Violet': '0xEE82EE', 'Wheat': '0xF5DEB3', 'White': '0xFFFFFF', | ||||
|                 'WhiteSmoke': '0xF5F5F5', 'Yellow': '0xFFFF00', 'YellowGreen': '0x9ACD32'} | ||||
|  | ||||
| def shift_color(col, shift=30, sat=1.0, val=1.0): | ||||
|     r = (col & (255 << 16)) >> 16 | ||||
|     g = (col & (255 << 8)) >> 8 | ||||
|     b = col & 255 | ||||
|     hsv = colorsys.rgb_to_hsv(r, g, b) | ||||
|     h = (((hsv[0] * 360) + shift) % 360) / 360 | ||||
|     rgb = colorsys.hsv_to_rgb(h, hsv[1] * sat, hsv[2] * val) | ||||
|     return (int(rgb[0]) << 16) + (int(rgb[1]) << 8) + int(rgb[2]) | ||||
|  | ||||
| def parse_sheet(ws): | ||||
|     print(f'Parsing worksheet {ws.title}') | ||||
|     ir = {"desc": ws.title} | ||||
|     rows = ws.rows | ||||
|     keys = [col.value.lower() for col in next(rows)] | ||||
|     for row in rows: | ||||
|         rec = dict(zip(keys, [col.value for col in row])) | ||||
|         if rec.get('code') is None: | ||||
|             continue | ||||
|         cd = {"label": rec.get('label')} | ||||
|         if rec.get('row'): | ||||
|             cd['pos'] = f'{rec["row"]}x{rec["col"]}' | ||||
|         if rec.get('comment'): | ||||
|             cd['cmnt'] = rec.get('comment') | ||||
|         if rec.get('rpt'): | ||||
|             cd['rpt'] = bool(rec['rpt']) | ||||
|                         | ||||
|         if rec.get('cmd'): | ||||
|             cd['cmd'] = rec['cmd'] | ||||
|         elif all((rec.get('primary'), rec.get('secondary'), rec.get('tertiary'))): | ||||
|             c1 = int(rec.get('primary'), 16) | ||||
|             c2 = int(rec.get('secondary'), 16) | ||||
|             c3 = int(rec.get('tertiary'), 16) | ||||
|             cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' | ||||
|         elif all((rec.get('primary'), rec.get('secondary'))): | ||||
|             c1 = int(rec.get('primary'), 16) | ||||
|             c2 = int(rec.get('secondary'), 16) | ||||
|             c3 = shift_color(c1, -1, sat=0.66, val=0.66) | ||||
|             cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' | ||||
|         elif rec.get('primary'): | ||||
|             c1 = int(rec.get('primary'), 16) | ||||
|             c2 = shift_color(c1, 30) | ||||
|             c3 = shift_color(c1, -10, sat=0.66, val=0.66) | ||||
|             cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' | ||||
|         elif rec.get('label') in named_colors: | ||||
|             c1 = int(named_colors[rec.get('label')], 16) | ||||
|             c2 = shift_color(c1, 30) | ||||
|             c3 = shift_color(c1, -10, sat=0.66, val=0.66) | ||||
|             cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' | ||||
|         else: | ||||
|             print(f'Did not find a command or color for {rec["label"]}. Hint use named CSS colors as labels') | ||||
|         ir[rec['code']] = cd | ||||
|              | ||||
|     with open(f'{ws.title}_ir.json', 'w') as fp: | ||||
|         json.dump(ir, fp, indent=2)         | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     wb = openpyxl.load_workbook('IR_Remote_Codes.xlsx') | ||||
|     for ws in wb.worksheets: | ||||
|         parse_sheet(ws) | ||||
							
								
								
									
										33
									
								
								usermods/JSON_IR_remote/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| # JSON IR remote | ||||
|  | ||||
| ## Purpose  | ||||
|  | ||||
| The JSON IR remote allows users to customize IR remote behavior without writing custom code and compiling.  | ||||
| It also enables using any remote that is compatible with your IR receiver. Using the JSON IR remote, you can | ||||
| map buttons from any remote to any HTTP request API or JSON API command.   | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| * Upload the IR config file, named _ir.json_ to your board using the [ip address]/edit url. Pick from one of the included files or create your own. | ||||
| * On the config > LED settings page, set the correct IR pin. | ||||
| * On the config > Sync Interfaces page, select "JSON Remote" as the Infrared remote. | ||||
|  | ||||
| ## Modification | ||||
|  | ||||
| * See if there is a json file with the same number of buttons as your remote. Many remotes will have the same internals and emit the same codes but have different labels. | ||||
| * In the ir.json file, each key will be the hex encoded IR code. | ||||
| * The "cmd" property will be the HTTP Request API or JSON API to execute when that button is pressed. | ||||
| * A limited number of c functions are supported (!incBrightness, !decBrightness, !presetFallback) | ||||
| * When using !presetFallback, include properties PL (preset to load), FX (effect to fall back to) and FP (palette to fall back to) | ||||
| * If the command is _repeatable_ and does not contain the "~" character, add a "rpt": true property. | ||||
| * Other properties are ignored, but having a label property may help when editing. | ||||
|  | ||||
|  | ||||
| Sample: | ||||
| { | ||||
|   "0xFF629D": {"cmd": "T=2", "rpt": true, "label": "Toggle on/off"},  // HTTP command | ||||
|   "0xFF9867": {"cmd": "A=~16", "label": "Inc brightness"},            // HTTP command with incrementing           | ||||
|   "0xFF38C7": {"cmd": {"bri": 10}, "label": "Dim to 10"},             // JSON command  | ||||
|   "0xFF22DD": {"cmd": "!presetFallback", "PL": 1, "FX": 16, "FP": 6,   | ||||
|                "label": "Preset 1 or fallback to Saw - Party"},       // c function | ||||
| } | ||||
| @@ -62,7 +62,7 @@ class PIRsensorSwitch : public Usermod { | ||||
|     // PIR sensor pin | ||||
|     const uint8_t PIRsensorPin = 13; // D7 on D1 mini | ||||
|     // notification mode for colorUpdated() | ||||
|     const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE | ||||
|     const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE | ||||
|     // 1 min delay before switch off after the sensor state goes LOW | ||||
|     uint32_t m_switchOffDelay = 60000; | ||||
|     // off timer start time | ||||
|   | ||||
| @@ -9,28 +9,13 @@ The LED strip is switched [using a relay](https://github.com/Aircoookie/WLED/wik | ||||
|  | ||||
| ## Webinterface | ||||
|  | ||||
| The info page in the web interface shows the items below | ||||
|  | ||||
| - the state of the sensor. By clicking on the state the sensor can be deactivated/activated. Changes persist after a reboot. | ||||
| **I recommend to deactivate the sensor before an OTA update and activate it again afterwards**. | ||||
| - the remaining time of the off timer.  | ||||
|  | ||||
| ## JSON API | ||||
|  | ||||
| The usermod supports the following state changes: | ||||
|  | ||||
| | JSON key   | Value range | Description                     | | ||||
| |------------|-------------|---------------------------------| | ||||
| | PIRenabled | bool        | Deactivdate/activate the sensor | | ||||
| | PIRoffSec  | 60 to 43200 | Off timer seconds               | | ||||
|  | ||||
|  Changes also persist after a reboot. | ||||
| The info page in the web interface shows the remaining time of the off timer.  | ||||
|  | ||||
| ## Sensor connection | ||||
|  | ||||
| My setup uses an HC-SR501 sensor, a HC-SR505 should also work. | ||||
|  | ||||
| The usermod uses GPIO13 (D1 mini pin D7) for the sensor signal.  | ||||
| The usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal but can be changed in the Usermod settings page. | ||||
| [This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor. | ||||
|  | ||||
| Use the potentiometers on the sensor to set the time-delay to the minimum and the sensitivity to about half, or slightly above. | ||||
| @@ -76,9 +61,10 @@ void registerUsermods() | ||||
|  | ||||
| ## API to enable/disable the PIR sensor from outside. For example from another usermod. | ||||
|  | ||||
| The class provides the static method `PIRsensorSwitch* PIRsensorSwitch::GetInstance()` to get a pointer to the usermod object. | ||||
| To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. | ||||
|  | ||||
| To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available.  | ||||
| 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 just send MQTT message and not change WLED state using settings page as well as responding to motion only during nighttime (assuming NTP and lattitude/longitude are set to determine sunrise/sunset times). | ||||
|  | ||||
| ### There are two options to get access to the usermod instance: | ||||
|  | ||||
| @@ -98,12 +84,19 @@ class MyUsermod : public Usermod { | ||||
|   //... | ||||
|  | ||||
|   void togglePIRSensor() { | ||||
|     if (PIRsensorSwitch::GetInstance() != nullptr) { | ||||
|       PIRsensorSwitch::GetInstance()->EnablePIRsensor(!PIRsensorSwitch::GetInstance()->PIRsensorEnabled()); | ||||
|     #ifdef USERMOD_PIR_SENSOR_SWITCH | ||||
|     PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH); | ||||
|     if (PIRsensor != nullptr) { | ||||
|       PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); | ||||
|     } | ||||
|     #endif | ||||
|   } | ||||
|   //... | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| Have fun - @gegu | ||||
|  | ||||
| ## Change log | ||||
| 2021-04 | ||||
| * Adaptation for runtime configuration. | ||||
| @@ -2,6 +2,15 @@ | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| #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 | ||||
| #endif | ||||
|  | ||||
| /* | ||||
|  * This usermod handles PIR sensor states. | ||||
|  * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH.  | ||||
| @@ -30,104 +39,134 @@ public: | ||||
|   /** | ||||
|    * constructor | ||||
|    */ | ||||
|   PIRsensorSwitch() | ||||
|   { | ||||
|     // set static instance pointer | ||||
|     PIRsensorSwitchInstance(this); | ||||
|   } | ||||
|   PIRsensorSwitch() {} | ||||
|   /** | ||||
|    * desctructor | ||||
|    */ | ||||
|   ~PIRsensorSwitch() | ||||
|   { | ||||
|     PIRsensorSwitchInstance(nullptr, true); | ||||
|     ; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * return the instance pointer of the class | ||||
|    */ | ||||
|   static PIRsensorSwitch *GetInstance() { return PIRsensorSwitchInstance(); } | ||||
|   ~PIRsensorSwitch() {} | ||||
|  | ||||
|   /** | ||||
|    * Enable/Disable the PIR sensor | ||||
|    */ | ||||
|   void EnablePIRsensor(bool enable) { m_PIRenabled = enable; } | ||||
|   void EnablePIRsensor(bool en) { enabled = en; } | ||||
|   /** | ||||
|    * Get PIR sensor enabled/disabled state | ||||
|    */ | ||||
|   bool PIRsensorEnabled() { return m_PIRenabled; } | ||||
|   bool PIRsensorEnabled() { return enabled; } | ||||
|  | ||||
| private: | ||||
|   // PIR sensor pin | ||||
|   const uint8_t PIRsensorPin = 13; // D7 on D1 mini | ||||
|   int8_t PIRsensorPin = PIR_SENSOR_PIN; | ||||
|   // notification mode for colorUpdated() | ||||
|   const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE | ||||
|   const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE | ||||
|   // delay before switch off after the sensor state goes LOW | ||||
|   uint32_t m_switchOffDelay = 600000; | ||||
|   uint32_t m_switchOffDelay = 600000; // 10min | ||||
|   // off timer start time | ||||
|   uint32_t m_offTimerStart = 0; | ||||
|   // current PIR sensor pin state | ||||
|   byte m_PIRsensorPinState = LOW; | ||||
|   // PIR sensor enabled - ISR attached | ||||
|   bool m_PIRenabled = true; | ||||
|   // state if serializeConfig() should be called | ||||
|   bool m_updateConfig = false; | ||||
|   byte sensorPinState = LOW; | ||||
|   // PIR sensor enabled | ||||
|   bool enabled = true; | ||||
|   // status of initialisation | ||||
|   bool initDone = false; | ||||
|   // on and off presets | ||||
|   uint8_t m_onPreset = 0; | ||||
|   uint8_t m_offPreset = 0; | ||||
|   // flag to indicate that PIR sensor should activate WLED during nighttime only | ||||
|   bool m_nightTimeOnly = false; | ||||
|   // flag to send MQTT message only (assuming it is enabled) | ||||
|   bool m_mqttOnly = false; | ||||
|   // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR) | ||||
|   bool m_offOnly = false; | ||||
|   bool PIRtriggered = false; | ||||
|  | ||||
|   unsigned long lastLoop = 0; | ||||
|  | ||||
|   // strings to reduce flash memory usage (used more than twice) | ||||
|   static const char _name[]; | ||||
|   static const char _switchOffDelay[]; | ||||
|   static const char _enabled[]; | ||||
|   static const char _onPreset[]; | ||||
|   static const char _offPreset[]; | ||||
|   static const char _nightTime[]; | ||||
|   static const char _mqttOnly[]; | ||||
|   static const char _offOnly[]; | ||||
|  | ||||
|   /** | ||||
|    * return or change if new PIR sensor state is available | ||||
|    * check if it is daytime | ||||
|    * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime | ||||
|    */ | ||||
|   static volatile bool newPIRsensorState(bool changeState = false, bool newState = false); | ||||
|   bool isDayTime() { | ||||
|     bool isDayTime = false; | ||||
|     updateLocalTime(); | ||||
|     uint8_t hr = hour(localTime); | ||||
|     uint8_t mi = minute(localTime); | ||||
|  | ||||
|   /** | ||||
|    * PIR sensor state has changed | ||||
|    */ | ||||
|   static void IRAM_ATTR ISR_PIRstateChange(); | ||||
|  | ||||
|   /** | ||||
|    * Set/get instance pointer | ||||
|    */ | ||||
|   static PIRsensorSwitch *PIRsensorSwitchInstance(PIRsensorSwitch *pInstance = nullptr, bool bRemoveInstance = false); | ||||
|     if (sunrise && sunset) { | ||||
|       if (hour(sunrise)<hr && hour(sunset)>hr) { | ||||
|         isDayTime = true; | ||||
|       } else { | ||||
|         if (hour(sunrise)==hr && minute(sunrise)<mi) { | ||||
|           isDayTime = true; | ||||
|         } | ||||
|         if (hour(sunset)==hr && minute(sunset)>mi) { | ||||
|           isDayTime = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return isDayTime; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * switch strip on/off | ||||
|    */ | ||||
|   void switchStrip(bool switchOn) | ||||
|   { | ||||
|     if (switchOn && bri == 0) | ||||
|     { | ||||
|     if (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; | ||||
|     PIRtriggered = switchOn; | ||||
|     if (switchOn && m_onPreset) { | ||||
|       applyPreset(m_onPreset); | ||||
|     } else if (!switchOn && m_offPreset) { | ||||
|       applyPreset(m_offPreset); | ||||
|     } else if (switchOn && bri == 0) { | ||||
|       bri = briLast; | ||||
|       colorUpdated(NotifyUpdateMode); | ||||
|     } | ||||
|     else if (!switchOn && bri != 0) | ||||
|     { | ||||
|     } else if (!switchOn && bri != 0) { | ||||
|       briLast = bri; | ||||
|       bri = 0; | ||||
|       colorUpdated(NotifyUpdateMode); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void publishMqtt(const char* state) | ||||
|   { | ||||
|     //Check if MQTT Connected, otherwise it will crash the 8266 | ||||
|     if (WLED_MQTT_CONNECTED){ | ||||
|       char subuf[64]; | ||||
|       strcpy(subuf, mqttDeviceTopic); | ||||
|       strcat_P(subuf, PSTR("/motion")); | ||||
|       mqtt->publish(subuf, 0, false, state); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Read and update PIR sensor state. | ||||
|    * Initilize/reset switch off timer | ||||
|    */ | ||||
|   bool updatePIRsensorState() | ||||
|   { | ||||
|     if (newPIRsensorState()) | ||||
|     { | ||||
|       m_PIRsensorPinState = digitalRead(PIRsensorPin); | ||||
|     bool pinState = digitalRead(PIRsensorPin); | ||||
|     if (pinState != sensorPinState) { | ||||
|       sensorPinState = pinState; // change previous state | ||||
|  | ||||
|       if (m_PIRsensorPinState == HIGH) | ||||
|       { | ||||
|       if (sensorPinState == HIGH) { | ||||
|         m_offTimerStart = 0; | ||||
|         switchStrip(true); | ||||
|       } | ||||
|       else if (bri != 0) | ||||
|       { | ||||
|         if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); | ||||
|         publishMqtt("on"); | ||||
|       } else /*if (bri != 0)*/ { | ||||
|         // start switch off timer | ||||
|         m_offTimerStart = millis(); | ||||
|       } | ||||
|       newPIRsensorState(true, false); | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
| @@ -140,9 +179,10 @@ private: | ||||
|   { | ||||
|     if (m_offTimerStart > 0 && millis() - m_offTimerStart > m_switchOffDelay) | ||||
|     { | ||||
|       if (m_PIRenabled == true) | ||||
|       if (enabled == true) | ||||
|       { | ||||
|         switchStrip(false); | ||||
|         if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(false); | ||||
|         publishMqtt("off"); | ||||
|       } | ||||
|       m_offTimerStart = 0; | ||||
|       return true; | ||||
| @@ -159,13 +199,21 @@ public: | ||||
|    */ | ||||
|   void setup() | ||||
|   { | ||||
|     // PIR Sensor mode INPUT_PULLUP | ||||
|     pinMode(PIRsensorPin, INPUT_PULLUP); | ||||
|     if (m_PIRenabled) | ||||
|     { | ||||
|       // assign interrupt function and set CHANGE mode | ||||
|       attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); | ||||
|     if (enabled) { | ||||
|       // pin retrieved from cfg.json (readFromConfig()) prior to running setup() | ||||
|       if (PIRsensorPin >= 0 && pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { | ||||
|         // PIR Sensor mode INPUT_PULLUP | ||||
|         pinMode(PIRsensorPin, INPUT_PULLUP); | ||||
|         sensorPinState = digitalRead(PIRsensorPin); | ||||
|       } else { | ||||
|         if (PIRsensorPin >= 0) { | ||||
|           DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); | ||||
|         } | ||||
|         PIRsensorPin = -1;  // allocation failed | ||||
|         enabled = false; | ||||
|       } | ||||
|     } | ||||
|     initDone = true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -181,14 +229,12 @@ public: | ||||
|    */ | ||||
|   void loop() | ||||
|   { | ||||
|     if (!updatePIRsensorState()) | ||||
|     { | ||||
|     // only check sensors 4x/s | ||||
|     if (!enabled || millis() - lastLoop < 250 || strip.isUpdating()) return; | ||||
|     lastLoop = millis(); | ||||
|  | ||||
|     if (!updatePIRsensorState()) { | ||||
|       handleOffTimer(); | ||||
|       if (m_updateConfig) | ||||
|       { | ||||
|         serializeConfig(); | ||||
|         m_updateConfig = false; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -199,132 +245,147 @@ public: | ||||
|    */ | ||||
|   void addToJsonInfo(JsonObject &root) | ||||
|   { | ||||
|     //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object | ||||
|     // the value contains a button to toggle the sensor enabled/disabled | ||||
|     JsonObject user = root["u"]; | ||||
|     if (user.isNull()) | ||||
|       user = root.createNestedObject("u"); | ||||
|     if (user.isNull()) user = root.createNestedObject("u"); | ||||
|  | ||||
|     JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name | ||||
|     String uiDomString = "<button class=\"btn infobtn\" onclick=\"requestJson({PIRenabled:"; | ||||
|     String sensorStateInfo; | ||||
|  | ||||
|     // PIR sensor state | ||||
|     if (m_PIRenabled) | ||||
|     if (enabled) | ||||
|     { | ||||
|       uiDomString += "false"; | ||||
|       sensorStateInfo = (m_PIRsensorPinState != LOW ? "active" : "inactive"); //value | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|       uiDomString += "true"; | ||||
|       sensorStateInfo = "Disabled !"; | ||||
|     } | ||||
|     uiDomString += "});return false;\">"; | ||||
|     uiDomString += sensorStateInfo; | ||||
|     uiDomString += "</button>"; | ||||
|     infoArr.add(uiDomString); //value | ||||
|  | ||||
|     //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object | ||||
|     uiDomString = "⏲ switch off timer<span style=\"display:block;padding-left:25px;\">\ | ||||
| after <input type=\"number\" min=\"1\" max=\"720\" value=\""; | ||||
|     uiDomString += (m_switchOffDelay / 60000); | ||||
|     uiDomString += "\" onchange=\"requestJson({PIRoffSec:parseInt(this.value)*60});\">min</span>"; | ||||
|     infoArr = user.createNestedArray(uiDomString); //name | ||||
|  | ||||
|     // off timer | ||||
|     if (m_offTimerStart > 0) | ||||
|     { | ||||
|       uiDomString = ""; | ||||
|       unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; | ||||
|       if (offSeconds >= 3600) | ||||
|       // off timer | ||||
|       String uiDomString = F("PIR <i class=\"icons\"></i>"); | ||||
|       JsonArray infoArr = user.createNestedArray(uiDomString); // timer value | ||||
|       if (m_offTimerStart > 0) | ||||
|       { | ||||
|         uiDomString += (offSeconds / 3600); | ||||
|         uiDomString += " hours "; | ||||
|         offSeconds %= 3600; | ||||
|         uiDomString = ""; | ||||
|         unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; | ||||
|         if (offSeconds >= 3600) | ||||
|         { | ||||
|           uiDomString += (offSeconds / 3600); | ||||
|           uiDomString += F("h "); | ||||
|           offSeconds %= 3600; | ||||
|         } | ||||
|         if (offSeconds >= 60) | ||||
|         { | ||||
|           uiDomString += (offSeconds / 60); | ||||
|           offSeconds %= 60; | ||||
|         } | ||||
|         else if (uiDomString.length() > 0) | ||||
|         { | ||||
|           uiDomString += 0; | ||||
|         } | ||||
|         if (uiDomString.length() > 0) | ||||
|         { | ||||
|           uiDomString += F("min "); | ||||
|         } | ||||
|         uiDomString += (offSeconds); | ||||
|         infoArr.add(uiDomString + F("s")); | ||||
|       } else { | ||||
|         infoArr.add(sensorPinState ? F("sensor on") : F("inactive")); | ||||
|       } | ||||
|       if (offSeconds >= 60) | ||||
|       { | ||||
|         uiDomString += (offSeconds / 60); | ||||
|         offSeconds %= 60; | ||||
|       } | ||||
|       else if (uiDomString.length() > 0) | ||||
|       { | ||||
|         uiDomString += 0; | ||||
|       } | ||||
|       if (uiDomString.length() > 0) | ||||
|       { | ||||
|         uiDomString += " min "; | ||||
|       } | ||||
|       uiDomString += (offSeconds); | ||||
|       infoArr.add(uiDomString + " sec"); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|       infoArr.add("inactive"); | ||||
|     } else { | ||||
|       String uiDomString = F("PIR sensor"); | ||||
|       JsonArray infoArr = user.createNestedArray(uiDomString); | ||||
|       infoArr.add(F("disabled")); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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 | ||||
|    * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. | ||||
|    * Add "PIRoffSec" to json state. This can be used to adjust <m_switchOffDelay> milliseconds. | ||||
|    */ | ||||
| /* | ||||
|   void addToJsonState(JsonObject &root) | ||||
|   { | ||||
|     root["PIRenabled"] = m_PIRenabled; | ||||
|     root["PIRoffSec"] = (m_switchOffDelay / 1000); | ||||
|   } | ||||
| */ | ||||
|  | ||||
|   /** | ||||
|    * 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 | ||||
|    * Read "PIRenabled" from json state and switch enable/disable the PIR sensor. | ||||
|    * Read "PIRoffSec" from json state and adjust <m_switchOffDelay> milliseconds. | ||||
|    */ | ||||
| /* | ||||
|   void readFromJsonState(JsonObject &root) | ||||
|   { | ||||
|     if (root["PIRoffSec"] != nullptr) | ||||
|     { | ||||
|       m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as<unsigned long>()))); | ||||
|       m_updateConfig = true; | ||||
|     } | ||||
|  | ||||
|     if (root["PIRenabled"] != nullptr) | ||||
|     { | ||||
|       if (root["PIRenabled"] && !m_PIRenabled) | ||||
|       { | ||||
|         attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); | ||||
|         newPIRsensorState(true, true); | ||||
|       } | ||||
|       else if (m_PIRenabled) | ||||
|       { | ||||
|         detachInterrupt(PIRsensorPin); | ||||
|       } | ||||
|       m_PIRenabled = root["PIRenabled"]; | ||||
|       m_updateConfig = true; | ||||
|     } | ||||
|   } | ||||
| */ | ||||
|  | ||||
|   /** | ||||
|    * provide the changeable values | ||||
|    */ | ||||
|   void addToConfig(JsonObject &root) | ||||
|   { | ||||
|     JsonObject top = root.createNestedObject("PIRsensorSwitch"); | ||||
|     top["PIRenabled"] = m_PIRenabled; | ||||
|     top["PIRoffSec"] = m_switchOffDelay; | ||||
|     JsonObject top = root.createNestedObject(FPSTR(_name)); | ||||
|     top[FPSTR(_enabled)]   = enabled; | ||||
|     top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; | ||||
|     top["pin"]             = PIRsensorPin; | ||||
|     top[FPSTR(_onPreset)]  = m_onPreset; | ||||
|     top[FPSTR(_offPreset)] = m_offPreset; | ||||
|     top[FPSTR(_nightTime)] = m_nightTimeOnly; | ||||
|     top[FPSTR(_mqttOnly)]  = m_mqttOnly; | ||||
|     top[FPSTR(_offOnly)]   = m_offOnly; | ||||
|     DEBUG_PRINTLN(F("PIR config saved.")); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * restore the changeable values | ||||
|    * 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. | ||||
|    */ | ||||
|   void readFromConfig(JsonObject &root) | ||||
|   bool readFromConfig(JsonObject &root) | ||||
|   { | ||||
|     JsonObject top = root["PIRsensorSwitch"]; | ||||
|     m_PIRenabled = (top["PIRenabled"] != nullptr ? top["PIRenabled"] : true); | ||||
|     m_switchOffDelay = top["PIRoffSec"] | m_switchOffDelay; | ||||
|     bool oldEnabled = enabled; | ||||
|     int8_t oldPin = PIRsensorPin; | ||||
|  | ||||
|     JsonObject top = root[FPSTR(_name)]; | ||||
|     if (top.isNull()) { | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     PIRsensorPin = top["pin"] | PIRsensorPin; | ||||
|  | ||||
|     enabled = top[FPSTR(_enabled)] | enabled; | ||||
|  | ||||
|     m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; | ||||
|  | ||||
|     m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; | ||||
|     m_onPreset = max(0,min(250,(int)m_onPreset)); | ||||
|  | ||||
|     m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; | ||||
|     m_offPreset = max(0,min(250,(int)m_offPreset)); | ||||
|  | ||||
|     m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; | ||||
|     m_mqttOnly      = top[FPSTR(_mqttOnly)] | m_mqttOnly; | ||||
|     m_offOnly       = top[FPSTR(_offOnly)] | m_offOnly; | ||||
|  | ||||
|     DEBUG_PRINT(FPSTR(_name)); | ||||
|     if (!initDone) { | ||||
|       // reading config prior to setup() | ||||
|       DEBUG_PRINTLN(F(" config loaded.")); | ||||
|     } else { | ||||
|       if (oldPin != PIRsensorPin || oldEnabled != enabled) { | ||||
|         // check if pin is OK | ||||
|         if (oldPin != PIRsensorPin && oldPin >= 0) { | ||||
|           // if we are changing pin in settings page | ||||
|           // deallocate old pin | ||||
|           pinManager.deallocatePin(oldPin, PinOwner::UM_PIR); | ||||
|           if (pinManager.allocatePin(PIRsensorPin, false, PinOwner::UM_PIR)) { | ||||
|             pinMode(PIRsensorPin, INPUT_PULLUP); | ||||
|           } else { | ||||
|             // allocation failed | ||||
|             PIRsensorPin = -1; | ||||
|             enabled = false; | ||||
|           } | ||||
|         } | ||||
|         if (enabled) { | ||||
|           sensorPinState = digitalRead(PIRsensorPin); | ||||
|         } | ||||
|       } | ||||
|       DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|     } | ||||
|     // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|     return !top[FPSTR(_offOnly)].isNull(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -337,30 +398,12 @@ after <input type=\"number\" min=\"1\" max=\"720\" value=\""; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| ////////////////////////////////////////////////////// | ||||
| // PIRsensorSwitch static method implementations | ||||
|  | ||||
| volatile bool PIRsensorSwitch::newPIRsensorState(bool changeState, bool newState) | ||||
| { | ||||
|   static volatile bool s_PIRsensorState = false; | ||||
|   if (changeState) | ||||
|   { | ||||
|     s_PIRsensorState = newState; | ||||
|   } | ||||
|   return s_PIRsensorState; | ||||
| } | ||||
|  | ||||
| void IRAM_ATTR PIRsensorSwitch::ISR_PIRstateChange() | ||||
| { | ||||
|   newPIRsensorState(true, true); | ||||
| } | ||||
|  | ||||
| PIRsensorSwitch *PIRsensorSwitch::PIRsensorSwitchInstance(PIRsensorSwitch *pInstance, bool bRemoveInstance) | ||||
| { | ||||
|   static PIRsensorSwitch *s_pPIRsensorSwitch = nullptr; | ||||
|   if (pInstance != nullptr || bRemoveInstance) | ||||
|   { | ||||
|     s_pPIRsensorSwitch = pInstance; | ||||
|   } | ||||
|   return s_pPIRsensorSwitch; | ||||
| } | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char PIRsensorSwitch::_name[]           PROGMEM = "PIRsensorSwitch"; | ||||
| const char PIRsensorSwitch::_enabled[]        PROGMEM = "PIRenabled"; | ||||
| const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec"; | ||||
| const char PIRsensorSwitch::_onPreset[]       PROGMEM = "on-preset"; | ||||
| const char PIRsensorSwitch::_offPreset[]      PROGMEM = "off-preset"; | ||||
| const char PIRsensorSwitch::_nightTime[]      PROGMEM = "nighttime-only"; | ||||
| const char PIRsensorSwitch::_mqttOnly[]       PROGMEM = "mqtt-only"; | ||||
| const char PIRsensorSwitch::_offOnly[]        PROGMEM = "off-only"; | ||||
|   | ||||
							
								
								
									
										36
									
								
								usermods/PWM_fan/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| # PWM fan | ||||
|  | ||||
| v2 Usermod to to control PWM fan with RPM feedback and temperature control | ||||
|  | ||||
| This usermod requires Dallas Temperature usermod to obtain temperature information. If this is not available the fan will always run at 100% speed. | ||||
| If the fan does not have _tacho_ (RPM) output you can set the _tacho-pin_ to -1 to not use that feature. | ||||
|  | ||||
| You can also set the thershold temperature at which fan runs at lowest speed. If the actual temperature measured will be 3°C greater than threshold temperature the fan will run at 100%. | ||||
|  | ||||
| If the _tacho_ is supported the current speed (in RPM) will be repored in WLED Info page. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Add the compile-time option `-D USERMOD_PWM_FAN` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_PWM_FAN` in `myconfig.h`. | ||||
| You will also need `-D USERMOD_DALLASTEMPERATURE`. | ||||
|  | ||||
| ### Define Your Options | ||||
|  | ||||
| All of the parameters are configured during run-time using Usermods settings page. | ||||
| This includes: | ||||
|  | ||||
| * PWM output pin | ||||
| * tacho input pin | ||||
| * sampling frequency in seconds | ||||
| * threshold temperature in degees C | ||||
|  | ||||
| _NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency. | ||||
|  | ||||
| ### PlatformIO requirements | ||||
|  | ||||
| No special requirements. | ||||
|  | ||||
| ## Change Log | ||||
|  | ||||
| 2021-10 | ||||
| * First public release | ||||
							
								
								
									
										332
									
								
								usermods/PWM_fan/usermod_PWM_fan.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,332 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifndef USERMOD_DALLASTEMPERATURE | ||||
| #error The "PWM fan" usermod requires "Dallas Temeprature" usermod to function properly. | ||||
| #endif | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| // PWM & tacho code curtesy of @KlausMu | ||||
| // https://github.com/KlausMu/esp32-fan-controller/tree/main/src | ||||
| // adapted for WLED usermod by @blazoncek | ||||
|  | ||||
|  | ||||
| // tacho counter | ||||
| static volatile unsigned long counter_rpm = 0; | ||||
| // Interrupt counting every rotation of the fan | ||||
| // https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/ | ||||
| static void IRAM_ATTR rpm_fan() { | ||||
|   counter_rpm++; | ||||
| } | ||||
|  | ||||
|  | ||||
| class PWMFanUsermod : public Usermod { | ||||
|  | ||||
|   private: | ||||
|  | ||||
|     bool initDone = false; | ||||
|     bool enabled = true; | ||||
|     unsigned long msLastTachoMeasurement = 0; | ||||
|     uint16_t last_rpm = 0; | ||||
|     #ifdef ARDUINO_ARCH_ESP32 | ||||
|     uint8_t pwmChannel = 255; | ||||
|     #endif | ||||
|  | ||||
|     #ifdef USERMOD_DALLASTEMPERATURE | ||||
|     UsermodTemperature* tempUM; | ||||
|     #endif | ||||
|  | ||||
|     // configurable parameters | ||||
|     int8_t  tachoPin          = -1; | ||||
|     int8_t  pwmPin            = -1; | ||||
|     uint8_t tachoUpdateSec    = 30; | ||||
|     float   targetTemperature = 25.0; | ||||
|     uint8_t minPWMValuePct    = 50; | ||||
|     uint8_t numberOfInterrupsInOneSingleRotation = 2;     // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups. | ||||
|  | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _enabled[]; | ||||
|     static const char _tachoPin[]; | ||||
|     static const char _pwmPin[]; | ||||
|     static const char _temperature[]; | ||||
|     static const char _tachoUpdateSec[]; | ||||
|     static const char _minPWMValuePct[]; | ||||
|     static const char _IRQperRotation[]; | ||||
|  | ||||
|     void initTacho(void) { | ||||
|       if (tachoPin < 0 || !pinManager.allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ | ||||
|         tachoPin = -1; | ||||
|         return; | ||||
|       } | ||||
|       pinMode(tachoPin, INPUT); | ||||
|       digitalWrite(tachoPin, HIGH); | ||||
|       attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); | ||||
|       DEBUG_PRINTLN(F("Tacho sucessfully initialized.")); | ||||
|     } | ||||
|  | ||||
|     void deinitTacho(void) { | ||||
|       if (tachoPin < 0) return; | ||||
|       detachInterrupt(digitalPinToInterrupt(tachoPin)); | ||||
|       pinManager.deallocatePin(tachoPin, PinOwner::UM_Unspecified); | ||||
|       tachoPin = -1; | ||||
|     } | ||||
|  | ||||
|     void updateTacho(void) { | ||||
|       if (tachoPin < 0) return; | ||||
|  | ||||
|       // start of tacho measurement | ||||
|       // detach interrupt while calculating rpm | ||||
|       detachInterrupt(digitalPinToInterrupt(tachoPin));  | ||||
|       // calculate rpm | ||||
|       last_rpm = (counter_rpm * 60) / numberOfInterrupsInOneSingleRotation; | ||||
|       last_rpm /= tachoUpdateSec; | ||||
|       // reset counter | ||||
|       counter_rpm = 0;  | ||||
|       // store milliseconds when tacho was measured the last time | ||||
|       msLastTachoMeasurement = millis(); | ||||
|       // attach interrupt again | ||||
|       attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); | ||||
|     } | ||||
|  | ||||
|     // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/ | ||||
|     void initPWMfan(void) { | ||||
|       if (pwmPin < 0 || !pinManager.allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { | ||||
|         pwmPin = -1; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       #ifdef ESP8266 | ||||
|       analogWriteRange(255); | ||||
|       analogWriteFreq(WLED_PWM_FREQ); | ||||
|       #else | ||||
|       pwmChannel = pinManager.allocateLedc(1); | ||||
|       if (pwmChannel == 255) { //no more free LEDC channels | ||||
|         deinitPWMfan(); return; | ||||
|       } | ||||
|       // configure LED PWM functionalitites | ||||
|       ledcSetup(pwmChannel, 25000, 8); | ||||
|       // attach the channel to the GPIO to be controlled | ||||
|       ledcAttachPin(pwmPin, pwmChannel); | ||||
|       #endif | ||||
|       DEBUG_PRINTLN(F("Fan PWM sucessfully initialized.")); | ||||
|     } | ||||
|  | ||||
|     void deinitPWMfan(void) { | ||||
|       if (pwmPin < 0) return; | ||||
|  | ||||
|       pinManager.deallocatePin(pwmPin, PinOwner::UM_Unspecified); | ||||
|       #ifdef ARDUINO_ARCH_ESP32 | ||||
|       pinManager.deallocateLedc(pwmChannel, 1); | ||||
|       #endif | ||||
|       pwmPin = -1; | ||||
|     } | ||||
|  | ||||
|     void updateFanSpeed(uint8_t pwmValue){ | ||||
|       if (pwmPin < 0) return; | ||||
|  | ||||
|       #ifdef ESP8266 | ||||
|       analogWrite(pwmPin, pwmValue); | ||||
|       #else | ||||
|       ledcWrite(pwmChannel, pwmValue); | ||||
|       #endif | ||||
|     } | ||||
|  | ||||
|     float getActualTemperature(void) { | ||||
|       #ifdef USERMOD_DALLASTEMPERATURE | ||||
|       if (tempUM != nullptr) | ||||
|         return tempUM->getTemperatureC(); | ||||
|       #endif | ||||
|       return -127.0f; | ||||
|     } | ||||
|  | ||||
|     void setFanPWMbasedOnTemperature(void) { | ||||
|       float temp = getActualTemperature(); | ||||
|       float difftemp = temp - targetTemperature; | ||||
|       // Default to run fan at full speed. | ||||
|       int newPWMvalue = 255; | ||||
|       int pwmStep = ((100 - minPWMValuePct) * newPWMvalue) / (7*100); | ||||
|       int pwmMinimumValue = (minPWMValuePct * newPWMvalue) / 100; | ||||
|  | ||||
|       if ((temp == NAN) || (temp <= 0.0)) { | ||||
|         DEBUG_PRINTLN(F("WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255.")); | ||||
|       } else if (difftemp <= 0.0) { | ||||
|         // Temperature is below target temperature. Run fan at minimum speed. | ||||
|         newPWMvalue = pwmMinimumValue; | ||||
|       } else if (difftemp <= 0.5) { | ||||
|         newPWMvalue = pwmMinimumValue + pwmStep; | ||||
|       } else if (difftemp <= 1.0) { | ||||
|         newPWMvalue = pwmMinimumValue + 2*pwmStep; | ||||
|       } else if (difftemp <= 1.5) { | ||||
|         newPWMvalue = pwmMinimumValue + 3*pwmStep; | ||||
|       } else if (difftemp <= 2.0) { | ||||
|         newPWMvalue = pwmMinimumValue + 4*pwmStep; | ||||
|       } else if (difftemp <= 2.5) { | ||||
|         newPWMvalue = pwmMinimumValue + 5*pwmStep; | ||||
|       } else if (difftemp <= 3.0) { | ||||
|         newPWMvalue = pwmMinimumValue + 6*pwmStep; | ||||
|       } | ||||
|       updateFanSpeed(newPWMvalue); | ||||
|     } | ||||
|  | ||||
|   public: | ||||
|  | ||||
|     // gets called once at boot. Do all initialization that doesn't depend on | ||||
|     // network here | ||||
|     void setup() { | ||||
|       #ifdef USERMOD_DALLASTEMPERATURE    | ||||
|       // This Usermod requires Temperature usermod | ||||
|       tempUM = (UsermodTemperature*) usermods.lookup(USERMOD_ID_TEMPERATURE); | ||||
|       #endif | ||||
|       initTacho(); | ||||
|       initPWMfan(); | ||||
|       updateFanSpeed((minPWMValuePct * 255) / 100); // inital fan speed | ||||
|       initDone = true; | ||||
|     } | ||||
|  | ||||
|     // gets called every time WiFi is (re-)connected. Initialize own network | ||||
|     // interfaces here | ||||
|     void connected() {} | ||||
|  | ||||
|     /* | ||||
|      * Da loop. | ||||
|      */ | ||||
|     void loop() { | ||||
|       if (!enabled || strip.isUpdating()) return; | ||||
|  | ||||
|       unsigned long now = millis(); | ||||
|       if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return; | ||||
|  | ||||
|       updateTacho(); | ||||
|       setFanPWMbasedOnTemperature(); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * 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 (tachoPin < 0) return; | ||||
|       JsonObject user = root["u"]; | ||||
|       if (user.isNull()) user = root.createNestedObject("u"); | ||||
|       JsonArray data = user.createNestedArray(FPSTR(_name)); | ||||
|       data.add(last_rpm); | ||||
|       data.add(F("rpm")); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * 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)); // usermodname | ||||
|       top[FPSTR(_enabled)]        = enabled; | ||||
|       top[FPSTR(_pwmPin)]         = pwmPin; | ||||
|       top[FPSTR(_tachoPin)]       = tachoPin; | ||||
|       top[FPSTR(_tachoUpdateSec)] = tachoUpdateSec; | ||||
|       top[FPSTR(_temperature)]    = targetTemperature; | ||||
|       top[FPSTR(_minPWMValuePct)] = minPWMValuePct; | ||||
|       top[FPSTR(_IRQperRotation)] = numberOfInterrupsInOneSingleRotation; | ||||
|       DEBUG_PRINTLN(F("Autosave 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 :) | ||||
|      *  | ||||
|      * The function should return true if configuration was successfully loaded or false if there was no configuration. | ||||
|      */ | ||||
|     bool readFromConfig(JsonObject& root) { | ||||
|       int8_t newTachoPin = tachoPin; | ||||
|       int8_t newPwmPin   = pwmPin; | ||||
|  | ||||
|       JsonObject top = root[FPSTR(_name)]; | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       if (top.isNull()) { | ||||
|         DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       enabled           = top[FPSTR(_enabled)] | enabled; | ||||
|       newTachoPin       = top[FPSTR(_tachoPin)] | newTachoPin; | ||||
|       newPwmPin         = top[FPSTR(_pwmPin)] | newPwmPin; | ||||
|       tachoUpdateSec    = top[FPSTR(_tachoUpdateSec)] | tachoUpdateSec; | ||||
|       tachoUpdateSec    = (uint8_t) max(1,(int)tachoUpdateSec); // bounds checking | ||||
|       targetTemperature = top[FPSTR(_temperature)] | targetTemperature; | ||||
|       minPWMValuePct    = top[FPSTR(_minPWMValuePct)] | minPWMValuePct; | ||||
|       minPWMValuePct    = (uint8_t) min(100,max(0,(int)minPWMValuePct)); // bounds checking | ||||
|       numberOfInterrupsInOneSingleRotation = top[FPSTR(_IRQperRotation)] | numberOfInterrupsInOneSingleRotation; | ||||
|       numberOfInterrupsInOneSingleRotation = (uint8_t) max(1,(int)numberOfInterrupsInOneSingleRotation); // bounds checking | ||||
|  | ||||
|       if (!initDone) { | ||||
|         // first run: reading from cfg.json | ||||
|         tachoPin = newTachoPin; | ||||
|         pwmPin   = newPwmPin; | ||||
|         DEBUG_PRINTLN(F(" config loaded.")); | ||||
|       } else { | ||||
|         DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|         // changing paramters from settings page | ||||
|         if (tachoPin != newTachoPin || pwmPin != newPwmPin) { | ||||
|           DEBUG_PRINTLN(F("Re-init pins.")); | ||||
|           // deallocate pin and release interrupts | ||||
|           deinitTacho(); | ||||
|           deinitPWMfan(); | ||||
|           tachoPin = newTachoPin; | ||||
|           pwmPin   = newPwmPin; | ||||
|           // initialise | ||||
|           setup(); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return !top[FPSTR(_IRQperRotation)].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_PWM_FAN; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char PWMFanUsermod::_name[]           PROGMEM = "PWM-fan"; | ||||
| const char PWMFanUsermod::_enabled[]        PROGMEM = "enabled"; | ||||
| const char PWMFanUsermod::_tachoPin[]       PROGMEM = "tacho-pin"; | ||||
| const char PWMFanUsermod::_pwmPin[]         PROGMEM = "PWM-pin"; | ||||
| const char PWMFanUsermod::_temperature[]    PROGMEM = "target-temp-C"; | ||||
| const char PWMFanUsermod::_tachoUpdateSec[] PROGMEM = "tacho-update-s"; | ||||
| const char PWMFanUsermod::_minPWMValuePct[] PROGMEM = "min-PWM-percent"; | ||||
| const char PWMFanUsermod::_IRQperRotation[] PROGMEM = "IRQs-per-rotation"; | ||||
							
								
								
									
										8
									
								
								usermods/RTC/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| # DS1307/DS3231 Real time clock | ||||
|  | ||||
| Gets the time from I2C RTC module on boot. This allows clocks to operate e.g. if temporarily no WiFi is available. | ||||
| The stored time is updated each time NTP is synced.  | ||||
|  | ||||
| ## Installation  | ||||
|  | ||||
| Add the build flag `-D USERMOD_RTC` to your platformio environment. | ||||
							
								
								
									
										35
									
								
								usermods/RTC/usermod_rtc.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "src/dependencies/time/DS1307RTC.h" | ||||
| #include "wled.h" | ||||
|  | ||||
| //Connect DS1307 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL)) | ||||
|  | ||||
| class RTCUsermod : public Usermod { | ||||
|   private: | ||||
|     unsigned long lastTime = 0; | ||||
|     bool disabled = false; | ||||
|   public: | ||||
|  | ||||
|     void setup() { | ||||
|       time_t rtcTime = RTC.get(); | ||||
|       if (rtcTime) { | ||||
|         toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC); | ||||
|         updateLocalTime(); | ||||
|       } else { | ||||
|         if (!RTC.chipPresent()) disabled = true; //don't waste time if H/W error | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void loop() { | ||||
|       if (!disabled && toki.isTick()) { | ||||
|         time_t t = toki.second(); | ||||
|         if (t != RTC.get()) RTC.set(t); //set RTC to NTP/UI-provided value | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     uint16_t getId() | ||||
|     { | ||||
|       return USERMOD_ID_RTC; | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										16
									
								
								usermods/SN_Photoresistor/platformio_override.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| ; Options | ||||
| ; ------- | ||||
| ; USERMOD_SN_PHOTORESISTOR                      - define this to have this user mod included wled00\usermods_list.cpp | ||||
| ; USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds | ||||
| ; USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 20 seconds | ||||
| ; USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE    - the voltage supplied to the sensor, defaults to 5v | ||||
| ; USERMOD_SN_PHOTORESISTOR_ADC_PRECISION        - the ADC precision is the number of distinguishable ADC inputs, defaults to 1024.0 (10 bits) | ||||
| ; USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE       - the resistor size, defaults to 10000.0 (10K hms) | ||||
| ; USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE         - the offset value to report on, defaults to 25 | ||||
| ; | ||||
| [env:usermod_sn_photoresistor_d1_mini] | ||||
| extends = env:d1_mini | ||||
| build_flags = | ||||
|     ${common.build_flags_esp8266} | ||||
|     -D USERMOD_SN_PHOTORESISTOR | ||||
| lib_deps = ${env.lib_deps} | ||||
							
								
								
									
										30
									
								
								usermods/SN_Photoresistor/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| # SN_Photoresistor usermod | ||||
|  | ||||
| This usermod will read from an attached photoresistor sensor like the KY-018 sensor. | ||||
| The luminance is displayed both in the Info section of the web UI as well as published to the `/luminance` MQTT topic if enabled. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Copy the example `platformio_override.ini` to the root directory.  This file should be placed in the same directory as `platformio.ini`. | ||||
|  | ||||
| ### Define Your Options | ||||
|  | ||||
| * `USERMOD_SN_PHOTORESISTOR`                      - define this to have this user mod included wled00\usermods_list.cpp | ||||
| * `USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60 seconds | ||||
| * `USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 20 seconds | ||||
| * `USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE`    - the voltage supplied to the sensor, defaults to 5v | ||||
| * `USERMOD_SN_PHOTORESISTOR_ADC_PRECISION`        - the ADC precision is the number of distinguishable ADC inputs, defaults to 1024.0 (10 bits) | ||||
| * `USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE`       - the resistor size, defaults to 10000.0 (10K hms) | ||||
| * `USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE`         - the offset value to report on, defaults to 25 | ||||
|  | ||||
| All parameters can be configured at runtime using Usermods settings page. | ||||
|  | ||||
| ## Project link | ||||
|  | ||||
| * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link | ||||
|  | ||||
| ### PlatformIO requirements | ||||
|  | ||||
| If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:usermod_sn_photoresistor_d1_mini`. | ||||
|  | ||||
| ## Change Log | ||||
							
								
								
									
										203
									
								
								usermods/SN_Photoresistor/usermod_sn_photoresistor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,203 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| //Pin defaults for QuinLed Dig-Uno (A0) | ||||
| #define PHOTORESISTOR_PIN A0 | ||||
|  | ||||
| // the frequency to check photoresistor, 10 seconds | ||||
| #ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL | ||||
| #define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000 | ||||
| #endif | ||||
|  | ||||
| // how many seconds after boot to take first measurement, 10 seconds | ||||
| #ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT | ||||
| #define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000 | ||||
| #endif | ||||
|  | ||||
| // supplied voltage | ||||
| #ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE | ||||
| #define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5 | ||||
| #endif | ||||
|  | ||||
| // 10 bits | ||||
| #ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION | ||||
| #define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0 | ||||
| #endif | ||||
|  | ||||
| // resistor size 10K hms | ||||
| #ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE | ||||
| #define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0 | ||||
| #endif | ||||
|  | ||||
| // only report if differance grater than offset value | ||||
| #ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE | ||||
| #define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 | ||||
| #endif | ||||
|  | ||||
| class Usermod_SN_Photoresistor : public Usermod | ||||
| { | ||||
| private: | ||||
|   float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE; | ||||
|   float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE; | ||||
|   float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION; | ||||
|   int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE; | ||||
|  | ||||
|   unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL; | ||||
|   // set last reading as "40 sec before boot", so first reading is taken after 20 sec | ||||
|   unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT); | ||||
|   // flag to indicate we have finished the first getTemperature call | ||||
|   // allows this library to report to the user how long until the first | ||||
|   // measurement | ||||
|   bool getLuminanceComplete = false; | ||||
|   uint16_t lastLDRValue = -1000; | ||||
|  | ||||
|   // flag set at startup | ||||
|   bool disabled = 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 _referenceVoltage[]; | ||||
|   static const char _resistorValue[]; | ||||
|   static const char _adcPrecision[]; | ||||
|   static const char _offset[]; | ||||
|  | ||||
|   bool checkBoundSensor(float newValue, float prevValue, float maxDiff) | ||||
|   { | ||||
|     return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; | ||||
|   } | ||||
|  | ||||
|   uint16_t getLuminance() | ||||
|   { | ||||
|     // http://forum.arduino.cc/index.php?topic=37555.0 | ||||
|     // https://forum.arduino.cc/index.php?topic=185158.0 | ||||
|     float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); | ||||
|     float amps = volts / resistorValue; | ||||
|     float lux = amps * 1000000 * 2.0; | ||||
|  | ||||
|     lastMeasurement = millis(); | ||||
|     getLuminanceComplete = true; | ||||
|     return uint16_t(lux); | ||||
|   } | ||||
|  | ||||
| public: | ||||
|   void setup() | ||||
|   { | ||||
|     // set pinmode | ||||
|     pinMode(PHOTORESISTOR_PIN, INPUT); | ||||
|   } | ||||
|  | ||||
|   void loop() | ||||
|   { | ||||
|     if (disabled || strip.isUpdating()) | ||||
|       return; | ||||
|  | ||||
|     unsigned long now = millis(); | ||||
|  | ||||
|     // check to see if we are due for taking a measurement | ||||
|     // lastMeasurement will not be updated until the conversion | ||||
|     // is complete the the reading is finished | ||||
|     if (now - lastMeasurement < readingInterval) | ||||
|     { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     uint16_t currentLDRValue = getLuminance(); | ||||
|     if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) | ||||
|     { | ||||
|       lastLDRValue = currentLDRValue; | ||||
|  | ||||
|       if (WLED_MQTT_CONNECTED) | ||||
|       { | ||||
|         char subuf[45]; | ||||
|         strcpy(subuf, mqttDeviceTopic); | ||||
|         strcat_P(subuf, PSTR("/luminance")); | ||||
|         mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str()); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         DEBUG_PRINTLN("Missing MQTT connection. Not publishing data"); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void addToJsonInfo(JsonObject &root) | ||||
|   { | ||||
|     JsonObject user = root[F("u")]; | ||||
|     if (user.isNull()) | ||||
|       user = root.createNestedObject(F("u")); | ||||
|  | ||||
|     JsonArray lux = user.createNestedArray(F("Luminance")); | ||||
|  | ||||
|     if (!getLuminanceComplete) | ||||
|     { | ||||
|       // if we haven't read the sensor yet, let the user know | ||||
|       // that we are still waiting for the first measurement | ||||
|       lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); | ||||
|       lux.add(F(" sec until read")); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     lux.add(lastLDRValue); | ||||
|     lux.add(F(" lux")); | ||||
|   } | ||||
|  | ||||
|   uint16_t getId() | ||||
|   { | ||||
|     return USERMOD_ID_SN_PHOTORESISTOR; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|      * addToConfig() (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)] = !disabled; | ||||
|     top[FPSTR(_readInterval)] = readingInterval / 1000; | ||||
|     top[FPSTR(_referenceVoltage)] = referenceVoltage; | ||||
|     top[FPSTR(_resistorValue)] = resistorValue; | ||||
|     top[FPSTR(_adcPrecision)] = adcPrecision; | ||||
|     top[FPSTR(_offset)] = offset; | ||||
|  | ||||
|     DEBUG_PRINTLN(F("Photoresistor config saved.")); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   * readFromConfig() is called before setup() to populate properties from values stored in cfg.json | ||||
|   */ | ||||
|   bool readFromConfig(JsonObject &root) | ||||
|   { | ||||
|     // we look for JSON object. | ||||
|     JsonObject top = root[FPSTR(_name)]; | ||||
|     if (top.isNull()) { | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     disabled         = !(top[FPSTR(_enabled)] | !disabled); | ||||
|     readingInterval  = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms | ||||
|     referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage; | ||||
|     resistorValue    = top[FPSTR(_resistorValue)] | resistorValue; | ||||
|     adcPrecision     = top[FPSTR(_adcPrecision)] | adcPrecision; | ||||
|     offset           = top[FPSTR(_offset)] | offset; | ||||
|     DEBUG_PRINT(FPSTR(_name)); | ||||
|     DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|  | ||||
|     // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|     return true; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char Usermod_SN_Photoresistor::_name[] PROGMEM = "Photoresistor"; | ||||
| const char Usermod_SN_Photoresistor::_enabled[] PROGMEM = "enabled"; | ||||
| const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s"; | ||||
| const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; | ||||
| const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; | ||||
| const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; | ||||
| const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; | ||||
							
								
								
									
										14
									
								
								usermods/SN_Photoresistor/usermods_list.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| #include "wled.h" | ||||
| /* | ||||
|  * Register your v2 usermods here! | ||||
|  */ | ||||
| #ifdef USERMOD_SN_PHOTORESISTOR | ||||
| #include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" | ||||
| #endif | ||||
|  | ||||
| void registerUsermods() | ||||
| { | ||||
| #ifdef USERMOD_SN_PHOTORESISTOR | ||||
|   usermods.add(new Usermod_SN_Photoresistor()); | ||||
| #endif | ||||
| } | ||||
							
								
								
									
										72
									
								
								usermods/ST7789_display/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,72 @@ | ||||
| # ST7789 TFT IPS Color display 240x240pxwith ESP32 boards | ||||
|  | ||||
| This usermod allow to use 240x240 display to display following: | ||||
|  | ||||
| * Network SSID; | ||||
| * IP address; | ||||
| * Brightness; | ||||
| * Chosen effect; | ||||
| * Chosen palette; | ||||
| * Estimated current in mA; | ||||
|  | ||||
| ## Hardware | ||||
|  | ||||
| *** | ||||
|  | ||||
|  | ||||
| ## Library used | ||||
|  | ||||
| [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) | ||||
|  | ||||
| ## Setup | ||||
|  | ||||
| *** | ||||
|  | ||||
| ### Platformio.ini changes | ||||
|  | ||||
| In the `platformio.ini` file, uncomment the `TFT_eSPI` line within the [common] section, under `lib_deps`: | ||||
|  | ||||
| ```ini | ||||
| # platformio.ini | ||||
| ... | ||||
| [common] | ||||
| ... | ||||
| lib_deps = | ||||
|     ... | ||||
|   #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line   | ||||
|     #TFT_eSPI | ||||
| ... | ||||
| ``` | ||||
|  | ||||
| Also, while in the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: | ||||
|  | ||||
| Add lines to section: | ||||
|  | ||||
| ```ini | ||||
| default_envs = esp32dev | ||||
| build_flags = ${common.build_flags_esp32} | ||||
|   -D USERMOD_ST7789_DISPLAY | ||||
|  | ||||
| ``` | ||||
|  | ||||
| Save the `platformio.ini` file.  Once this is saved, the required library files should be automatically downloaded for modifications in a later step. | ||||
|  | ||||
| ### TFT_eSPI Library Adjustments | ||||
|  | ||||
| We need to modify a file in the `TFT_eSPI` library. If you followed the directions to modify and save the `platformio.ini` file above, the `User_Setup_Select.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI` folder. | ||||
|  | ||||
| Modify the  `User_Setup_Select.h` file as follows: | ||||
|  | ||||
| * Comment out the following line (which is the 'default' setup file): | ||||
|  | ||||
| ```ini | ||||
| //#include <User_Setup.h>           // Default setup is root library folder | ||||
| ``` | ||||
|  | ||||
| * Add following line: | ||||
|  | ||||
| ```ini | ||||
| #include <User_Setups/Setup_ST7789_Display.h>    // Setup file for ESP32 ST7789V SPI bus TFT | ||||
| ``` | ||||
|  | ||||
| * Copy file `"Setup_ST7789_Display.h"` from usermod folder to `/.pio/libdeps/esp32dev/TFT_eSPI/User_Setups` | ||||
							
								
								
									
										350
									
								
								usermods/ST7789_display/ST7789_display.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,350 @@ | ||||
| // Credits to @mrVanboy, @gwaland and my dearest friend @westward | ||||
| // Also for @spiff72 for usermod TTGO-T-Display | ||||
| // 210217 | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
| #include <TFT_eSPI.h> | ||||
| #include <SPI.h> | ||||
|  | ||||
| #define USERMOD_ST7789_DISPLAY 97 | ||||
|  | ||||
| #ifndef TFT_DISPOFF | ||||
| #define TFT_DISPOFF 0x28 | ||||
| #endif | ||||
|  | ||||
| #ifndef TFT_SLPIN | ||||
| #define TFT_SLPIN   0x10 | ||||
| #endif | ||||
|  | ||||
| #define TFT_MOSI            21 | ||||
| #define TFT_SCLK            22 | ||||
| #define TFT_DC              18 | ||||
| #define TFT_RST             5 | ||||
| #define TFT_BL              26  // Display backlight control pin | ||||
|  | ||||
| TFT_eSPI tft = TFT_eSPI(240, 240); // Invoke custom library | ||||
|  | ||||
| // How often we are redrawing screen | ||||
| #define USER_LOOP_REFRESH_RATE_MS 1000 | ||||
|  | ||||
|  | ||||
| //class name. Use something descriptive and leave the ": public Usermod" part :) | ||||
| class St7789DisplayUsermod : public Usermod { | ||||
|   private: | ||||
|     //Private class members. You can declare variables and functions only accessible to your usermod here | ||||
|     unsigned long lastTime = 0; | ||||
|  | ||||
|     bool displayTurnedOff = false; | ||||
|     long lastRedraw = 0; | ||||
|     // needRedraw marks if redraw is required to prevent often redrawing. | ||||
|     bool needRedraw = true; | ||||
|     // Next variables hold the previous known values to determine if redraw is required. | ||||
|     String knownSsid = ""; | ||||
|     IPAddress knownIp; | ||||
|     uint8_t knownBrightness = 0; | ||||
|     uint8_t knownMode = 0; | ||||
|     uint8_t knownPalette = 0; | ||||
|     uint8_t tftcharwidth = 19;  // Number of chars that fit on screen with text size set to 2 | ||||
|     long lastUpdate = 0; | ||||
|  | ||||
|   public: | ||||
|     //Functions called by WLED | ||||
|  | ||||
|     /* | ||||
|      * 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() | ||||
|     { | ||||
|         tft.init(); | ||||
|         tft.setRotation(0);  //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip. | ||||
|         tft.fillScreen(TFT_BLACK); | ||||
|         tft.setTextColor(TFT_RED); | ||||
|         tft.setCursor(60, 100); | ||||
|         tft.setTextDatum(MC_DATUM); | ||||
|         tft.setTextSize(2); | ||||
|         tft.print("Loading..."); | ||||
|         if (TFT_BL > 0)  | ||||
|         { // TFT_BL has been set in the TFT_eSPI library | ||||
|          pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode | ||||
|          digitalWrite(TFT_BL, HIGH); // Turn backlight on. | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * 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() { | ||||
| // Check if we time interval for redrawing passes. | ||||
|     if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|     lastUpdate = millis(); | ||||
|    | ||||
| // Turn off display after 5 minutes with no change. | ||||
|     if(!displayTurnedOff && millis() - lastRedraw > 5*60*1000) | ||||
|         { | ||||
|             digitalWrite(TFT_BL, LOW); // Turn backlight off.  | ||||
|             displayTurnedOff = true; | ||||
|         }  | ||||
|  | ||||
| // Check if values which are shown on display changed from the last time. | ||||
|     if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) | ||||
|     { | ||||
|     needRedraw = true; | ||||
|     } | ||||
|     else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) | ||||
|     { | ||||
|     needRedraw = true; | ||||
|     } | ||||
|     else if (knownBrightness != bri) | ||||
|     { | ||||
|     needRedraw = true; | ||||
|     } | ||||
|     else if (knownMode != strip.getMode()) | ||||
|     { | ||||
|     needRedraw = true; | ||||
|     } | ||||
|     else if (knownPalette != strip.getSegment(0).palette) | ||||
|     { | ||||
|     needRedraw = true; | ||||
|     } | ||||
|  | ||||
|     if (!needRedraw) | ||||
|     { | ||||
|     return; | ||||
|     } | ||||
|     needRedraw = false; | ||||
|    | ||||
|     if (displayTurnedOff) | ||||
|     { | ||||
|         digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); // Turn backlight on. | ||||
|         displayTurnedOff = false; | ||||
|     } | ||||
|     lastRedraw = millis(); | ||||
|  | ||||
| // Update last known values. | ||||
|     #if defined(ESP8266) | ||||
|         knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); | ||||
|     #else | ||||
|         knownSsid = WiFi.SSID(); | ||||
|     #endif | ||||
|     knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); | ||||
|     knownBrightness = bri; | ||||
|     knownMode = strip.getMode(); | ||||
|     knownPalette = strip.getSegment(0).palette; | ||||
|  | ||||
|     tft.fillScreen(TFT_BLACK); | ||||
|     tft.setTextSize(2); | ||||
| // First row with Wifi name | ||||
|     tft.setTextColor(TFT_SILVER); | ||||
|     tft.setCursor(3, 40); | ||||
|     tft.print(knownSsid.substring(0, tftcharwidth > 1 ? tftcharwidth - 1 : 0)); | ||||
| // Print `~` char to indicate that SSID is longer, than our dicplay | ||||
|     if (knownSsid.length() > tftcharwidth) | ||||
|         tft.print("~"); | ||||
|  | ||||
| // Second row with AP IP and Password or IP | ||||
|     tft.setTextColor(TFT_GREEN); | ||||
|     tft.setTextSize(2); | ||||
|     tft.setCursor(3, 64); | ||||
| // Print AP IP and password in AP mode or knownIP if AP not active. | ||||
|  | ||||
|     if (apActive) | ||||
|     { | ||||
|     tft.setTextColor(TFT_YELLOW); | ||||
|     tft.print("AP IP: "); | ||||
|     tft.print(knownIp); | ||||
|     tft.setCursor(3,86); | ||||
|     tft.setTextColor(TFT_YELLOW); | ||||
|     tft.print("AP Pass:"); | ||||
|     tft.print(apPass); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|     tft.setTextColor(TFT_GREEN); | ||||
|     tft.print("IP: "); | ||||
|     tft.print(knownIp); | ||||
|     tft.setCursor(3,86); | ||||
|     //tft.print("Signal Strength: "); | ||||
|     //tft.print(i.wifi.signal); | ||||
|     tft.setTextColor(TFT_WHITE); | ||||
|     tft.print("Bri: "); | ||||
|     tft.print(((float(bri)/255)*100),0); | ||||
|     tft.print("%"); | ||||
|     } | ||||
|  | ||||
| // Third row with mode name | ||||
|     tft.setCursor(3, 108); | ||||
|     uint8_t qComma = 0; | ||||
|     bool insideQuotes = false; | ||||
|     uint8_t printedChars = 0; | ||||
|     char singleJsonSymbol; | ||||
| // Find the mode name in JSON | ||||
|     for (size_t i = 0; i < strlen_P(JSON_mode_names); i++) | ||||
|     { | ||||
|         singleJsonSymbol = pgm_read_byte_near(JSON_mode_names + i); | ||||
|         switch (singleJsonSymbol) | ||||
|         { | ||||
|             case '"': | ||||
|             insideQuotes = !insideQuotes; | ||||
|         break; | ||||
|         case '[': | ||||
|         case ']': | ||||
|         break; | ||||
|         case ',': | ||||
|         qComma++; | ||||
|         default: | ||||
|             if (!insideQuotes || (qComma != knownMode)) | ||||
|             break; | ||||
|         tft.setTextColor(TFT_MAGENTA); | ||||
|         tft.print(singleJsonSymbol); | ||||
|         printedChars++; | ||||
|         } | ||||
|     if ((qComma > knownMode) || (printedChars > tftcharwidth - 1)) | ||||
|       break; | ||||
|     } | ||||
| // Fourth row with palette name | ||||
|     tft.setTextColor(TFT_YELLOW); | ||||
|     tft.setCursor(3, 130); | ||||
|     qComma = 0; | ||||
|     insideQuotes = false; | ||||
|     printedChars = 0; | ||||
| // Looking for palette name in JSON. | ||||
|     for (size_t i = 0; i < strlen_P(JSON_palette_names); i++) | ||||
|     { | ||||
|     singleJsonSymbol = pgm_read_byte_near(JSON_palette_names + i); | ||||
|         switch (singleJsonSymbol) | ||||
|         { | ||||
|         case '"': | ||||
|         insideQuotes = !insideQuotes; | ||||
|         break; | ||||
|         case '[': | ||||
|         case ']': | ||||
|         break; | ||||
|         case ',': | ||||
|         qComma++; | ||||
|         default: | ||||
|             if (!insideQuotes || (qComma != knownPalette)) | ||||
|             break; | ||||
|         tft.print(singleJsonSymbol); | ||||
|         printedChars++; | ||||
|         } | ||||
| // The following is modified from the code from the u8g2/u8g8 based code (knownPalette was knownMode) | ||||
|     if ((qComma > knownPalette) || (printedChars > tftcharwidth - 1)) | ||||
|       break; | ||||
|     } | ||||
| // Fifth row with estimated mA usage | ||||
|     tft.setTextColor(TFT_SILVER); | ||||
|     tft.setCursor(3, 152); | ||||
| // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate). | ||||
|     tft.print("Current: "); | ||||
|     tft.print(strip.currentMilliamps); | ||||
|     tft.print("mA"); | ||||
|     } | ||||
|     /* | ||||
|      * 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() 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("exampleUsermod"); | ||||
|       top["great"] = userVar0; //save this var persistently whenever settings are 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 :) | ||||
|      */ | ||||
|     void readFromConfig(JsonObject& root) | ||||
|     { | ||||
|       JsonObject top = root["top"]; | ||||
|       userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /* | ||||
|      * 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_ST7789_DISPLAY; | ||||
|     } | ||||
|  | ||||
|    //More methods can be added in the future, this example will then be extended. | ||||
|    //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! | ||||
| }; | ||||
							
								
								
									
										39
									
								
								usermods/ST7789_display/Setup_ST7789_Display.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | ||||
| // Setup for the ESP32 board with 1.5" 240x240 display | ||||
|  | ||||
| // See SetupX_Template.h for all options available | ||||
|  | ||||
| #define ST7789_DRIVER | ||||
| #define TFT_SDA_READ   // Display has a bidirectionsl SDA pin | ||||
|  | ||||
| #define TFT_WIDTH  240 | ||||
| #define TFT_HEIGHT 240 | ||||
|  | ||||
| #define CGRAM_OFFSET      // Library will add offsets required | ||||
|  | ||||
| //#define TFT_MISO -1 | ||||
|  | ||||
| #define TFT_MOSI            21 | ||||
| #define TFT_SCLK            22 | ||||
| //#define TFT_CS              5 | ||||
| #define TFT_DC              18 | ||||
| #define TFT_RST             5 | ||||
|  | ||||
| #define TFT_BL          26  // Display backlight control pin | ||||
|  | ||||
| #define TFT_BACKLIGHT_ON HIGH  // HIGH or LOW are options | ||||
|  | ||||
| #define LOAD_GLCD | ||||
| #define LOAD_FONT2 | ||||
| #define LOAD_FONT4 | ||||
| #define LOAD_FONT6 | ||||
| #define LOAD_FONT7 | ||||
| #define LOAD_FONT8 | ||||
| #define LOAD_GFXFF | ||||
|  | ||||
| //#define SMOOTH_FONT | ||||
|  | ||||
| //#define SPI_FREQUENCY  27000000 | ||||
|   #define SPI_FREQUENCY  40000000   // Maximum for ILI9341 | ||||
|  | ||||
|  | ||||
| #define SPI_READ_FREQUENCY  6000000 // 6 MHz is the maximum SPI read speed for the ST7789V | ||||
							
								
								
									
										
											BIN
										
									
								
								usermods/ST7789_display/images/ST7789_Guide.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 175 KiB | 
| @@ -3,7 +3,7 @@ | ||||
| Based on the excellent `QuinLED_Dig_Uno_Temp_MQTT` by srg74 and 400killer!   | ||||
| This usermod will read from an attached DS18B20 temperature sensor (as available on the QuinLED Dig-Uno)   | ||||
| The temperature is displayed both in the Info section of the web UI as well as published to the `/temperature` MQTT topic if enabled.   | ||||
| This usermod will be expanded with support for different sensor types in the future. | ||||
| This usermod may be expanded with support for different sensor types in the future. | ||||
|  | ||||
| If temperature sensor is not detected during boot, this usermod will be disabled. | ||||
|  | ||||
| @@ -14,20 +14,21 @@ Copy the example `platformio_override.ini` to the root directory.  This file sho | ||||
| ### Define Your Options | ||||
|  | ||||
| * `USERMOD_DALLASTEMPERATURE`                      - define this to have this user mod included wled00\usermods_list.cpp | ||||
| * `USERMOD_DALLASTEMPERATURE_CELSIUS`              - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported | ||||
| * `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60 seconds | ||||
| * `USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 20 seconds | ||||
|  | ||||
| All parameters can be configured at runtime using Usermods settings page, including pin, selection to display temerature in degrees Celsius or Farenheit mand measurement interval. | ||||
|  | ||||
| ## Project link | ||||
|  | ||||
| * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link | ||||
| * [Srg74-WLED-Wemos-shield](https://github.com/srg74/WLED-wemos-shield) - another great DIY WLED board | ||||
|  | ||||
| ### PlatformIO requirements | ||||
|  | ||||
| If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:d1_mini_usermod_dallas_temperature_C`. | ||||
|  | ||||
|  | ||||
| If you are not using `platformio_override.ini`, you might have to uncomment `DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: | ||||
| If you are not using `platformio_override.ini`, you might have to uncomment `OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: | ||||
|  | ||||
| ```ini | ||||
| # platformio.ini | ||||
| @@ -39,12 +40,9 @@ default_envs = d1_mini | ||||
| ... | ||||
| [common] | ||||
| ... | ||||
| lib_deps_external = | ||||
| lib_deps = | ||||
|   ... | ||||
|   #For use SSD1306 OLED display uncomment following | ||||
|   U8g2@~2.27.3 | ||||
|   #For Dallas sensor uncomment following 2 lines | ||||
|   DallasTemperature@~3.8.0 | ||||
|   #For Dallas sensor uncomment following line | ||||
|   OneWire@~2.3.5 | ||||
| ... | ||||
| ``` | ||||
| @@ -55,4 +53,6 @@ lib_deps_external = | ||||
| * Changed to use async, non-blocking implementation | ||||
| * Do not report low temperatures that indicate an error to mqtt | ||||
| * Disable plugin if temperature sensor not detected | ||||
| * Report the number of seconds until the first read in the info screen instead of sensor error | ||||
| * Report the number of seconds until the first read in the info screen instead of sensor error | ||||
| 2021-04 | ||||
| * Adaptation for runtime configuration. | ||||
| @@ -1,16 +1,15 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
| #include "OneWire.h" | ||||
|  | ||||
| #include <DallasTemperature.h> //DS18B20 | ||||
|  | ||||
| //Pin defaults for QuinLed Dig-Uno | ||||
| //Pin defaults for QuinLed Dig-Uno if not overriden | ||||
| #ifndef TEMPERATURE_PIN | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
| #define TEMPERATURE_PIN 18 | ||||
| #else //ESP8266 boards | ||||
| #define TEMPERATURE_PIN 14 | ||||
| #endif | ||||
|   #ifdef ARDUINO_ARCH_ESP32 | ||||
|     #define TEMPERATURE_PIN 18 | ||||
|   #else //ESP8266 boards | ||||
|     #define TEMPERATURE_PIN 14 | ||||
|   #endif | ||||
| #endif | ||||
|  | ||||
| // the frequency to check temperature, 1 minute | ||||
| @@ -18,23 +17,22 @@ | ||||
| #define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 | ||||
| #endif | ||||
|  | ||||
| // how many seconds after boot to take first measurement, 20 seconds | ||||
| #ifndef USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT | ||||
| #define USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT 20000  | ||||
| #endif | ||||
|  | ||||
| OneWire oneWire(TEMPERATURE_PIN); | ||||
| DallasTemperature sensor(&oneWire); | ||||
|  | ||||
| class UsermodTemperature : public Usermod { | ||||
|  | ||||
|   private: | ||||
|     // The device's unique 64-bit serial code stored in on-board ROM. | ||||
|     // Reading directly from the sensor device address is faster than | ||||
|     // reading from index. When reading by index, DallasTemperature | ||||
|     // must first look up the device address at the specified index. | ||||
|     DeviceAddress sensorDeviceAddress; | ||||
|  | ||||
|     bool initDone = false; | ||||
|     OneWire *oneWire; | ||||
|     // GPIO pin used for sensor (with a default compile-time fallback) | ||||
|     int8_t temperaturePin = TEMPERATURE_PIN; | ||||
|     // measurement unit (true==°C, false==°F) | ||||
|     bool degC = true; | ||||
|     // using parasite power on the sensor | ||||
|     bool parasite = false; | ||||
|     // how often do we read from sensor? | ||||
|     unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; | ||||
|     // set last reading as "40 sec before boot", so first reading is taken after 20 sec | ||||
|     unsigned long lastMeasurement = UINT32_MAX - (USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT); | ||||
|     unsigned long lastMeasurement = UINT32_MAX - USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; | ||||
|     // last time requestTemperatures was called | ||||
|     // used to determine when we can read the sensors temperature | ||||
|     // we have to wait at least 93.75 ms after requestTemperatures() is called | ||||
| @@ -42,92 +40,136 @@ class UsermodTemperature : public Usermod { | ||||
|     float temperature = -100; // default to -100, DS18B20 only goes down to -50C | ||||
|     // indicates requestTemperatures has been called but the sensor measurement is not complete | ||||
|     bool waitingForConversion = false; | ||||
|     // flag to indicate we have finished the first getTemperature call | ||||
|     // allows this library to report to the user how long until the first | ||||
|     // measurement | ||||
|     bool getTemperatureComplete = false; | ||||
|     // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting | ||||
|     // temperature if flashed to a board without a sensor attached | ||||
|     bool disabled = false; | ||||
|     bool sensorFound = false; | ||||
|  | ||||
|     void requestTemperatures() { | ||||
|         // there is requestTemperaturesByAddress however it  | ||||
|         // appears to do more work,  | ||||
|         // TODO: measure exection time difference | ||||
|         sensor.requestTemperatures();  | ||||
|         lastTemperaturesRequest = millis(); | ||||
|         waitingForConversion = true; | ||||
|     bool enabled = true; | ||||
|  | ||||
|     // 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[]; | ||||
|  | ||||
|     //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 | ||||
|     float readDallas() { | ||||
|       byte i; | ||||
|       byte data[2]; | ||||
|       int16_t result;                         // raw data from sensor | ||||
|       if (!oneWire->reset()) return -127.0f;  // send reset command and fail fast | ||||
|       oneWire->skip();                        // skip ROM | ||||
|       oneWire->write(0xBE);                   // read (temperature) from EEPROM | ||||
|       for (i=0; i < 2; i++) data[i] = oneWire->read();  // first 2 bytes contain temperature | ||||
|       for (i=2; i < 8; i++) oneWire->read();  // read unused bytes   | ||||
|       result = (data[1]<<4) | (data[0]>>4);   // we only need whole part, we will add fraction when returning | ||||
|       if (data[1]&0x80) result |= 0xFF00;     // fix negative value | ||||
|       oneWire->reset(); | ||||
|       oneWire->skip();                        // skip ROM | ||||
|       oneWire->write(0x44,parasite);          // request new temperature reading (without parasite power) | ||||
|       return (float)result + ((data[0]&0x0008) ? 0.5f : 0.0f); | ||||
|     } | ||||
|  | ||||
|     void getTemperature() { | ||||
|       if (strip.isUpdating()) return; | ||||
|       #ifdef USERMOD_DALLASTEMPERATURE_CELSIUS | ||||
|       temperature = sensor.getTempC(sensorDeviceAddress); | ||||
|       #else | ||||
|       temperature = sensor.getTempF(sensorDeviceAddress); | ||||
|       #endif | ||||
|     void requestTemperatures() { | ||||
|       readDallas(); | ||||
|       lastTemperaturesRequest = millis(); | ||||
|       waitingForConversion = true; | ||||
|       DEBUG_PRINTLN(F("Requested temperature.")); | ||||
|     } | ||||
|  | ||||
|     void readTemperature() { | ||||
|       temperature = readDallas(); | ||||
|       lastMeasurement = millis(); | ||||
|       waitingForConversion = false; | ||||
|       getTemperatureComplete = true; | ||||
|       //DEBUG_PRINTF("Read temperature %2.1f.\n", temperature); // does not work properly on 8266 | ||||
|       DEBUG_PRINT(F("Read temperature ")); | ||||
|       DEBUG_PRINTLN(temperature); | ||||
|     } | ||||
|  | ||||
|     bool findSensor() { | ||||
|       DEBUG_PRINTLN(F("Searching for sensor...")); | ||||
|       uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0}; | ||||
|       // find out if we have DS18xxx sensor attached | ||||
|       oneWire->reset_search(); | ||||
|       delay(10); | ||||
|       while (oneWire->search(deviceAddress)) { | ||||
|         DEBUG_PRINTLN(F("Found something...")); | ||||
|         if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) { | ||||
|           switch (deviceAddress[0]) { | ||||
|             case 0x10:  // DS18S20 | ||||
|             case 0x22:  // DS18B20 | ||||
|             case 0x28:  // DS1822 | ||||
|             case 0x3B:  // DS1825 | ||||
|             case 0x42:  // DS28EA00 | ||||
|               DEBUG_PRINTLN(F("Sensor found.")); | ||||
|               return true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|   public: | ||||
|  | ||||
|  | ||||
|     void setup() { | ||||
|       sensor.begin(); | ||||
|  | ||||
|       // get the unique 64-bit serial code stored in on-board ROM | ||||
|       // if getAddress returns false, the sensor was not found | ||||
|       disabled = !sensor.getAddress(sensorDeviceAddress, 0); | ||||
|  | ||||
|       if (!disabled) { | ||||
|         DEBUG_PRINTLN(F("Dallas Temperature found")); | ||||
|         // set the resolution for this specific device | ||||
|         sensor.setResolution(sensorDeviceAddress, 9, true); | ||||
|         // do not block waiting for reading | ||||
|         sensor.setWaitForConversion(false); | ||||
|         // allocate pin & prevent other use | ||||
|         if (!pinManager.allocatePin(TEMPERATURE_PIN,false)) | ||||
|           disabled = true; | ||||
|       } else { | ||||
|         DEBUG_PRINTLN(F("Dallas Temperature not found")); | ||||
|       int retries = 10; | ||||
|       if (enabled) { | ||||
|         // config says we are enabled | ||||
|         DEBUG_PRINTLN(F("Allocating temperature pin...")); | ||||
|         // pin retrieved from cfg.json (readFromConfig()) prior to running setup() | ||||
|         if (temperaturePin >= 0 && pinManager.allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { | ||||
|           oneWire = new OneWire(temperaturePin); | ||||
|           if (!oneWire->reset()) { | ||||
|             sensorFound = false;   // resetting 1-Wire bus yielded an error | ||||
|           } else { | ||||
|             while ((sensorFound=findSensor()) && retries--) { | ||||
|               delay(25); // try to find sensor | ||||
|             } | ||||
|           } | ||||
|         } else { | ||||
|           if (temperaturePin >= 0) { | ||||
|             DEBUG_PRINTLN(F("Temperature pin allocation failed.")); | ||||
|           } | ||||
|           temperaturePin = -1;  // allocation failed | ||||
|           sensorFound = false; | ||||
|         } | ||||
|       } | ||||
|       lastMeasurement = millis() - readingInterval + 10000; | ||||
|       initDone = true; | ||||
|     } | ||||
|  | ||||
|     void loop() { | ||||
|       if (disabled || strip.isUpdating()) return; | ||||
|        | ||||
|       if (!enabled || strip.isUpdating()) return; | ||||
|  | ||||
|       unsigned long now = millis(); | ||||
|  | ||||
|       // check to see if we are due for taking a measurement | ||||
|       // lastMeasurement will not be updated until the conversion | ||||
|       // is complete the the reading is finished | ||||
|       if (now - lastMeasurement < USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL) return; | ||||
|       if (now - lastMeasurement < readingInterval) return; | ||||
|  | ||||
|       // we are due for a measurement, if we are not already waiting  | ||||
|       // we are due for a measurement, if we are not already waiting | ||||
|       // for a conversion to complete, then make a new request for temps | ||||
|       if (!waitingForConversion) | ||||
|       { | ||||
|       if (!waitingForConversion) { | ||||
|         requestTemperatures(); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // we were waiting for a conversion to complete, have we waited log enough? | ||||
|       if (now - lastTemperaturesRequest >= 94 /* 93.75ms per the datasheet */) | ||||
|       { | ||||
|         getTemperature(); | ||||
|   | ||||
|       if (now - lastTemperaturesRequest >= 100 /* 93.75ms per the datasheet but can be up to 750ms */) { | ||||
|         readTemperature(); | ||||
|  | ||||
|         if (WLED_MQTT_CONNECTED) { | ||||
|           char subuf[38]; | ||||
|           char subuf[64]; | ||||
|           strcpy(subuf, mqttDeviceTopic); | ||||
|           if (-100 <= temperature) { | ||||
|             // dont publish super low temperature as the graph will get messed up | ||||
|             // the DallasTemperature library returns -127C or -196.6F when problem | ||||
|             // reading the sensor | ||||
|             strcat_P(subuf, PSTR("/temperature")); | ||||
|             mqtt->publish(subuf, 0, true, String(temperature).c_str()); | ||||
|             mqtt->publish(subuf, 0, false, String(temperature).c_str()); | ||||
|             strcat_P(subuf, PSTR("_f")); | ||||
|             mqtt->publish(subuf, 0, false, String((float)temperature * 1.8f + 32).c_str()); | ||||
|           } else { | ||||
|             // publish something else to indicate status? | ||||
|           } | ||||
| @@ -135,35 +177,117 @@ class UsermodTemperature : public Usermod { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * API calls te enable data exchange between WLED modules | ||||
|      */ | ||||
|     inline float getTemperatureC() { | ||||
|       return (float)temperature; | ||||
|     } | ||||
|     inline float getTemperatureF() { | ||||
|       return (float)temperature * 1.8f + 32; | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * 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) { | ||||
|       // dont add temperature to info if we are disabled | ||||
|       if (disabled) return; | ||||
|       if (!enabled) return; | ||||
|  | ||||
|       JsonObject user = root[F("u")]; | ||||
|       if (user.isNull()) user = root.createNestedObject(F("u")); | ||||
|       JsonObject user = root["u"]; | ||||
|       if (user.isNull()) user = root.createNestedObject("u"); | ||||
|  | ||||
|       JsonArray temp = user.createNestedArray(F("Temperature")); | ||||
|       JsonArray temp = user.createNestedArray(FPSTR(_name)); | ||||
|       //temp.add(F("Loaded.")); | ||||
|  | ||||
|       if (!getTemperatureComplete) { | ||||
|         // if we haven't read the sensor yet, let the user know | ||||
|         // that we are still waiting for the first measurement | ||||
|         temp.add((USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT - millis()) / 1000); | ||||
|         temp.add(F(" sec until read")); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (temperature <= -100) { | ||||
|       if (temperature <= -100.0 || (!sensorFound && temperature == -1.0)) { | ||||
|         temp.add(0); | ||||
|         temp.add(F(" Sensor Error!")); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       temp.add(temperature); | ||||
|       #ifdef USERMOD_DALLASTEMPERATURE_CELSIUS | ||||
|       temp.add(F("°C")); | ||||
|       #else | ||||
|       temp.add(F("°F")); | ||||
|       #endif | ||||
|       temp.add(degC ? temperature : (float)temperature * 1.8f + 32); | ||||
|       if (degC) temp.add(F("°C")); | ||||
|       else      temp.add(F("°F")); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 | ||||
|      * Read "<usermodname>_<usermodparam>" from json state and and change settings (i.e. GPIO pin) used. | ||||
|      */ | ||||
|     //void readFromJsonState(JsonObject &root) { | ||||
|     //  if (!initDone) return;  // prevent crash on boot applyPreset() | ||||
|     //} | ||||
|  | ||||
|     /** | ||||
|      * addToConfig() (called from set.cpp) stores persistent properties to cfg.json | ||||
|      */ | ||||
|     void addToConfig(JsonObject &root) { | ||||
|       // we add JSON object: {"Temperature": {"pin": 0, "degC": true}} | ||||
|       JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname | ||||
|       top[FPSTR(_enabled)] = enabled; | ||||
|       top["pin"]  = temperaturePin;     // usermodparam | ||||
|       top["degC"] = degC;  // usermodparam | ||||
|       top[FPSTR(_readInterval)] = readingInterval / 1000; | ||||
|       top[FPSTR(_parasite)] = parasite; | ||||
|       DEBUG_PRINTLN(F("Temperature 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: {"Temperature": {"pin": 0, "degC": true}} | ||||
|       int8_t newTemperaturePin = temperaturePin; | ||||
|  | ||||
|       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; | ||||
|       newTemperaturePin = top["pin"] | newTemperaturePin; | ||||
| //      newTemperaturePin = min(33,max(-1,(int)newTemperaturePin)); // bounds check | ||||
|       degC              = top["degC"] | degC; | ||||
|       readingInterval   = top[FPSTR(_readInterval)] | readingInterval/1000; | ||||
|       readingInterval   = min(120,max(10,(int)readingInterval)) * 1000;  // convert to ms | ||||
|       parasite          = top[FPSTR(_parasite)] | parasite; | ||||
|  | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       if (!initDone) { | ||||
|         // first run: reading from cfg.json | ||||
|         temperaturePin = newTemperaturePin; | ||||
|         DEBUG_PRINTLN(F(" config loaded.")); | ||||
|       } else { | ||||
|         DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|         // changing paramters from settings page | ||||
|         if (newTemperaturePin != temperaturePin) { | ||||
|           DEBUG_PRINTLN(F("Re-init temperature.")); | ||||
|           // deallocate pin and release memory | ||||
|           delete oneWire; | ||||
|           pinManager.deallocatePin(temperaturePin, PinOwner::UM_Temperature); | ||||
|           temperaturePin = newTemperaturePin; | ||||
|           // initialise | ||||
|           setup(); | ||||
|         } | ||||
|       } | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return !top[FPSTR(_parasite)].isNull(); | ||||
|     } | ||||
|  | ||||
|     uint16_t getId() | ||||
| @@ -171,3 +295,9 @@ class UsermodTemperature : public Usermod { | ||||
|       return USERMOD_ID_TEMPERATURE; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // 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"; | ||||
|   | ||||
| @@ -85,7 +85,7 @@ public: | ||||
|         if (m_pD2D && (999000000L != ntpLastSyncTime)) | ||||
|         { | ||||
|             // to prevent needing to import all the timezone stuff from other modules, work completely in UTC | ||||
|             time_t timeUTC = now(); | ||||
|             time_t timeUTC = toki.second(); | ||||
|             tmElements_t tmNow; | ||||
|             breakTime(timeUTC, tmNow); | ||||
|             int nCurMinute = tmNow.Minute; | ||||
|   | ||||
							
								
								
									
										35
									
								
								usermods/VL53L0X_gestures/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | ||||
| # Description | ||||
|  | ||||
| That usermod implements support of simple hand gestures with VL53L0X sensor: on/off and brightness correction. | ||||
| It can be useful for kitchen strips to avoid any touches. | ||||
|  - on/off - just swipe a hand below your sensor ("shortPressAction" is called and can be customized through WLED macros) | ||||
|  - brightness correction - keep your hand below sensor for 1 second to switch to "brightness" mode. | ||||
|                            Configure brightness by changing distance to the sensor (see parameters below for customization). | ||||
|                            "macroLongPress" is also called here. | ||||
|  | ||||
| ## Installation  | ||||
|  | ||||
| 1. Attach VL53L0X sensor to i2c pins according to default pins for your board. | ||||
| 2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment. | ||||
| In my case, for example: `build_flags = ${common.build_flags_esp8266} -D RLYPIN=12 -D USERMOD_VL53L0X_GESTURES` | ||||
| 3. Add "pololu/VL53L0X" dependency below to `lib_deps` like this: | ||||
| ```ini | ||||
| lib_deps = ${env.lib_deps} | ||||
|       pololu/VL53L0X @ ^1.3.0 | ||||
| ``` | ||||
|  | ||||
| My entire `platformio_override.ini` for example (for nodemcu board): | ||||
| ```ini | ||||
| [platformio] | ||||
| default_envs = nodemcu | ||||
|  | ||||
| [env:nodemcu] | ||||
| board = nodemcu | ||||
| platform = ${common.platform_wled_default} | ||||
| platform_packages = ${common.platform_packages} | ||||
| board_build.ldscript = ${common.ldscript_4m1m} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} -D RLYPIN=12 -D USERMOD_VL53L0X_GESTURES | ||||
| lib_deps = ${env.lib_deps} | ||||
|   pololu/VL53L0X @ ^1.3.0 | ||||
| ``` | ||||
							
								
								
									
										121
									
								
								usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,121 @@ | ||||
| /* | ||||
|  * That usermod implements support of simple hand gestures with VL53L0X sensor: on/off and brightness correction. | ||||
|  * It can be useful for kitchen strips to avoid any touches. | ||||
|  * - on/off - just swipe a hand below your sensor ("shortPressAction" is called and can be customized through WLED macros) | ||||
|  * - brightness correction - keep your hand below sensor for 1 second to switch to "brightness" mode. | ||||
|  *                           Configure brightness by changing distance to the sensor (see parameters below for customization). | ||||
|  *                           "macroLongPress" is also called here. | ||||
|  * | ||||
|  * Enabling this mod usermod: | ||||
|  * 1. Attach VL53L0X sensor to i2c pins according to default pins for your board. | ||||
|  * 2. Add "-D USERMOD_VL53L0X_GESTURES" to your build flags at platformio.ini (plaformio_override.ini) for needed environment. | ||||
|  * In my case, for example: build_flags = ${common.build_flags_esp8266} -D RLYPIN=12 -D USERMOD_VL53L0X_GESTURES | ||||
|  * 3. Add "pololu/VL53L0X" dependency to lib_deps like this: | ||||
|  * lib_deps = ${env.lib_deps} | ||||
|  *     pololu/VL53L0X @ ^1.3.0 | ||||
|  */ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| #include <Wire.h> | ||||
| #include <VL53L0X.h> | ||||
|  | ||||
| #ifndef VL53L0X_MAX_RANGE_MM | ||||
| #define VL53L0X_MAX_RANGE_MM 230 // max height in millimiters to react for motions | ||||
| #endif | ||||
|  | ||||
| #ifndef VL53L0X_MIN_RANGE_OFFSET | ||||
| #define VL53L0X_MIN_RANGE_OFFSET 60 // minimal range in millimiters that sensor can detect. Used in long motions to correct brightnes calculation. | ||||
| #endif | ||||
|  | ||||
| #ifndef VL53L0X_DELAY_MS | ||||
| #define VL53L0X_DELAY_MS 100 // how often to get data from sensor  | ||||
| #endif | ||||
|  | ||||
| #ifndef VL53L0X_LONG_MOTION_DELAY_MS | ||||
| #define VL53L0X_LONG_MOTION_DELAY_MS 1000 // how often to get data from sensor  | ||||
| #endif | ||||
|  | ||||
| class UsermodVL53L0XGestures : public Usermod { | ||||
|   private: | ||||
|     //Private class members. You can declare variables and functions only accessible to your usermod here | ||||
|     unsigned long lastTime = 0; | ||||
|     VL53L0X sensor; | ||||
|  | ||||
|     bool wasMotionBefore = false; | ||||
|     bool isLongMotion = false; | ||||
|     unsigned long motionStartTime = 0; | ||||
|      | ||||
|   public: | ||||
|  | ||||
|     void setup() { | ||||
|       Wire.begin(); | ||||
|  | ||||
|       sensor.setTimeout(150); | ||||
|       if (!sensor.init()) | ||||
|       { | ||||
|         DEBUG_PRINTLN(F("Failed to detect and initialize VL53L0X sensor!")); | ||||
|       } else { | ||||
|         sensor.setMeasurementTimingBudget(20000); // set high speed mode | ||||
|       } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     void loop() { | ||||
|       if (millis() - lastTime > VL53L0X_DELAY_MS) | ||||
|       { | ||||
|         lastTime = millis(); | ||||
|  | ||||
|         int range = sensor.readRangeSingleMillimeters(); | ||||
|         DEBUG_PRINTF(F("range: %d, brightness: %d"), range, bri); | ||||
|  | ||||
|         if (range < VL53L0X_MAX_RANGE_MM) | ||||
|         { | ||||
|           if (!wasMotionBefore) | ||||
|           { | ||||
|             motionStartTime = millis(); | ||||
|             DEBUG_PRINTF(F("motionStartTime: %d"), motionStartTime); | ||||
|           } | ||||
|           wasMotionBefore = true; | ||||
|  | ||||
|           if (millis() - motionStartTime > VL53L0X_LONG_MOTION_DELAY_MS) //long motion | ||||
|           { | ||||
|             DEBUG_PRINTF(F("long motion: %d"), motionStartTime); | ||||
|             if (!isLongMotion) | ||||
|             { | ||||
|               if (macroLongPress) | ||||
|               { | ||||
|                 applyMacro(macroLongPress); | ||||
|               } | ||||
|               isLongMotion = true; | ||||
|             } | ||||
|  | ||||
|             // set brightness according to range | ||||
|             bri = (VL53L0X_MAX_RANGE_MM - max(range, VL53L0X_MIN_RANGE_OFFSET)) * 255 / (VL53L0X_MAX_RANGE_MM - VL53L0X_MIN_RANGE_OFFSET); | ||||
|             DEBUG_PRINTF(F("new brightness: %d"), bri); | ||||
|             colorUpdated(1); | ||||
|           } | ||||
|         } else if (wasMotionBefore) { //released | ||||
|           long dur = millis() - motionStartTime; | ||||
|  | ||||
|           if (!isLongMotion) | ||||
|           { //short press | ||||
|             DEBUG_PRINTF(F("shortPressAction...")); | ||||
|             shortPressAction(); | ||||
|           } | ||||
|           wasMotionBefore = false; | ||||
|           isLongMotion = false; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * 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_VL53L0X; | ||||
|     } | ||||
| }; | ||||
| @@ -54,46 +54,46 @@ void userLoop() | ||||
|             switch (myKey) { | ||||
|                 case '1': | ||||
|                     applyPreset(1); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '2': | ||||
|                     applyPreset(2); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '3': | ||||
|                     applyPreset(3); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '4': | ||||
|                     applyPreset(4); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '5': | ||||
|                     applyPreset(5); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '6': | ||||
|                     applyPreset(6); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case 'A': | ||||
|                     applyPreset(7); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case 'B': | ||||
|                     applyPreset(8); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|  | ||||
|                 case '7': | ||||
|                     effectCurrent += 1; | ||||
|                     if (effectCurrent >= MODE_COUNT) effectCurrent = 0; | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '*': | ||||
|                     effectCurrent -= 1; | ||||
|                     if (effectCurrent < 0) effectCurrent = (MODE_COUNT-1); | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|  | ||||
|                 case '8': | ||||
| @@ -102,7 +102,7 @@ void userLoop() | ||||
|                     } else if (effectSpeed < 255) { | ||||
|                         effectSpeed += 1; | ||||
|                     } | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '0': | ||||
|                     if (effectSpeed > 15) { | ||||
| @@ -110,7 +110,7 @@ void userLoop() | ||||
|                     } else if (effectSpeed > 0) { | ||||
|                         effectSpeed -= 1; | ||||
|                     } | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|  | ||||
|                 case '9': | ||||
| @@ -119,7 +119,7 @@ void userLoop() | ||||
|                     } else if (effectIntensity < 255) { | ||||
|                         effectIntensity += 1; | ||||
|                     } | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case '#': | ||||
|                     if (effectIntensity > 15) { | ||||
| @@ -127,18 +127,18 @@ void userLoop() | ||||
|                     } else if (effectIntensity > 0) { | ||||
|                         effectIntensity -= 1; | ||||
|                     } | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|  | ||||
|                 case 'C': | ||||
|                     effectPalette += 1; | ||||
|                     if (effectPalette >= 50) effectPalette = 0; | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|                 case 'D': | ||||
|                     effectPalette -= 1; | ||||
|                     if (effectPalette <= 0) effectPalette = 50; | ||||
|                     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|                     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|                     break; | ||||
|  | ||||
|             } | ||||
|   | ||||
| After Width: | Height: | Size: 48 KiB | 
| After Width: | Height: | Size: 46 KiB | 
							
								
								
									
										
											BIN
										
									
								
								usermods/battery_status_basic/assets/battery_info_screen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 68 KiB | 
							
								
								
									
										69
									
								
								usermods/battery_status_basic/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,69 @@ | ||||
| # :battery: Battery status/level Usermod :battery: | ||||
|  | ||||
| This Usermod allows you to monitor the battery level of your battery powered project. | ||||
|  | ||||
| You can see the battery level and voltage in the `info modal`.  | ||||
|  | ||||
| For this to work the positive side of the (18650) battery must be connected to pin `A0` of the d1mini/esp8266 with a 100k ohm resistor (see [Useful Links](#useful-links)). | ||||
|  | ||||
| If you have a esp32 board it is best to connect the positive side of the battery to ADC1 (GPIO32 - GPIO39) | ||||
|  | ||||
| <p align="center"> | ||||
|   <img width="300" src="assets/battery_info_screen.png"> | ||||
| </p> | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| define `USERMOD_BATTERY_STATUS_BASIC` in `my_config.h` | ||||
|  | ||||
| ### Basic wiring diagram | ||||
| <p align="center"> | ||||
|   <img width="300" src="assets/battery_connection_schematic_01.png"> | ||||
| </p> | ||||
|  | ||||
| ### Define Your Options | ||||
|  | ||||
| * `USERMOD_BATTERY_STATUS_BASIC`                   - define this (in `my_config.h`) to have this user mod included wled00\usermods_list.cpp | ||||
| * `USERMOD_BATTERY_MEASUREMENT_PIN`                - defaults to A0 on esp8266 and GPIO32 on esp32 | ||||
| * `USERMOD_BATTERY_MEASUREMENT_INTERVAL`           - the frequency to check the battery, defaults to 30 seconds | ||||
| * `USERMOD_BATTERY_MIN_VOLTAGE`                    - minimum voltage of the Battery used, default is 2.6 (18650 battery standard) | ||||
| * `USERMOD_BATTERY_MAX_VOLTAGE`                    - maximum voltage of the Battery used, default is 4.2 (18650 battery standard) | ||||
|  | ||||
| All parameters can be configured at runtime using Usermods settings page. | ||||
|  | ||||
| ## Important :warning: | ||||
| * Make sure you know your battery specification ! not every battery is the same ! | ||||
| * Example: | ||||
|  | ||||
| | Your battery specification table  |                 | Options you can define        |  | ||||
| | :-------------------------------- |:--------------- | :---------------------------- | | ||||
| | Capacity                          | 3500mAh 12,5 Wh |                               | | ||||
| | Minimum capacity                  | 3350mAh 11,9 Wh |                               | | ||||
| | Rated voltage                     | 3.6V - 3.7V     |                               | | ||||
| | **Charging end voltage**          | **4,2V ± 0,05** | `USERMOD_BATTERY_MAX_VOLTAGE` | | ||||
| | **Discharge voltage**             | **2,5V**        | `USERMOD_BATTERY_MIN_VOLTAGE` | | ||||
| | Max. discharge current (constant) | 10A (10000mA)   |                               | | ||||
| | max. charging current             | 1.7A (1700mA)   |                               | | ||||
| | ...                               | ...             | ...                           | | ||||
| | ..                                | ..              | ..                            | | ||||
|  | ||||
| Specification from:  [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833) | ||||
|  | ||||
| ## Useful Links | ||||
| * https://lazyzero.de/elektronik/esp8266/wemos_d1_mini_a0/start | ||||
| * https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/ | ||||
|  | ||||
| ## Change Log | ||||
| 2021-09-02 | ||||
| * added "Battery voltage" to info | ||||
| * added circuit diagram to readme | ||||
| * added MQTT support, sending battery voltage | ||||
| * minor fixes | ||||
|  | ||||
| 2021-08-15 | ||||
| * changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries | ||||
| * Updated readme, added specification table | ||||
|  | ||||
| 2021-08-10 | ||||
| * Created | ||||
|  | ||||
							
								
								
									
										398
									
								
								usermods/battery_status_basic/usermod_v2_battery_status_basic.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,398 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| // pin defaults | ||||
| // for the esp32 it is best to use the ADC1: GPIO32 - GPIO39 | ||||
| // 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 32 | ||||
|   #else //ESP8266 boards | ||||
|     #define USERMOD_BATTERY_MEASUREMENT_PIN A0 | ||||
|   #endif | ||||
| #endif | ||||
|  | ||||
| // esp32 has a 12bit adc resolution | ||||
| // esp8266 only 10bit | ||||
| #ifndef USERMOD_BATTERY_ADC_PRECISION | ||||
|   #ifdef ARDUINO_ARCH_ESP32 | ||||
|     // 12 bits | ||||
|     #define USERMOD_BATTERY_ADC_PRECISION 4095.0 | ||||
|   #else | ||||
|     // 10 bits | ||||
|     #define USERMOD_BATTERY_ADC_PRECISION 1024.0 | ||||
|   #endif | ||||
| #endif | ||||
|  | ||||
|  | ||||
| // the frequency to check the battery, 30 sec | ||||
| #ifndef USERMOD_BATTERY_MEASUREMENT_INTERVAL | ||||
|   #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000 | ||||
| #endif | ||||
|  | ||||
|  | ||||
| // default for 18650 battery | ||||
| // https://batterybro.com/blogs/18650-wholesale-battery-reviews/18852515-when-to-recycle-18650-batteries-and-how-to-start-a-collection-center-in-your-vape-shop | ||||
| // Discharge voltage: 2.5 volt + .1 for personal safety | ||||
| #ifndef USERMOD_BATTERY_MIN_VOLTAGE | ||||
|   #define USERMOD_BATTERY_MIN_VOLTAGE 2.6 | ||||
| #endif | ||||
|  | ||||
| #ifndef USERMOD_BATTERY_MAX_VOLTAGE | ||||
|   #define USERMOD_BATTERY_MAX_VOLTAGE 4.2 | ||||
| #endif | ||||
|  | ||||
| class UsermodBatteryBasic : public Usermod  | ||||
| { | ||||
|   private: | ||||
|     // battery pin can be defined in my_config.h | ||||
|     int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; | ||||
|     // how often to read the battery voltage | ||||
|     unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; | ||||
|     unsigned long nextReadTime = 0; | ||||
|     unsigned long lastReadTime = 0; | ||||
|     // battery min. voltage | ||||
|     float minBatteryVoltage = USERMOD_BATTERY_MIN_VOLTAGE; | ||||
|     // battery max. voltage | ||||
|     float maxBatteryVoltage = USERMOD_BATTERY_MAX_VOLTAGE; | ||||
|     // 0 - 1024 for esp8266 (10-bit resolution) | ||||
|     // 0 - 4095 for esp32 (Default is 12-bit resolution) | ||||
|     float adcPrecision = USERMOD_BATTERY_ADC_PRECISION; | ||||
|     // raw analog reading  | ||||
|     float rawValue = 0.0; | ||||
|     // calculated voltage             | ||||
|     float voltage = 0.0; | ||||
|     // mapped battery level based on voltage | ||||
|     long batteryLevel = 0; | ||||
|     bool initDone = false; | ||||
|     bool initializing = true; | ||||
|  | ||||
|  | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _readInterval[]; | ||||
|      | ||||
|  | ||||
|     // custom map function | ||||
|     // https://forum.arduino.cc/t/floating-point-using-map-function/348113/2 | ||||
|     double mapf(double x, double in_min, double in_max, double out_min, double out_max)  | ||||
|     { | ||||
|       return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; | ||||
|     } | ||||
|  | ||||
|     float truncate(float val, byte dec)  | ||||
|     { | ||||
|       float x = val * pow(10, dec); | ||||
|       float y = round(x); | ||||
|       float z = x - y; | ||||
|       if ((int)z == 5) | ||||
|       { | ||||
|           y++; | ||||
|       } | ||||
|       x = y / pow(10, dec); | ||||
|       return x; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|   public: | ||||
|     //Functions called by WLED | ||||
|  | ||||
|     /* | ||||
|      * 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()  | ||||
|     { | ||||
|       #ifdef ARDUINO_ARCH_ESP32 | ||||
|         DEBUG_PRINTLN(F("Allocating battery pin...")); | ||||
|         if (batteryPin >= 0 && pinManager.allocatePin(batteryPin, false))  | ||||
|         { | ||||
|           DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); | ||||
|         } else { | ||||
|           if (batteryPin >= 0) DEBUG_PRINTLN(F("Battery pin allocation failed.")); | ||||
|           batteryPin = -1;  // allocation failed | ||||
|         } | ||||
|       #else //ESP8266 boards have only one analog input pin A0 | ||||
|  | ||||
|         pinMode(batteryPin, INPUT); | ||||
|       #endif | ||||
|  | ||||
|       nextReadTime = millis() + readingInterval; | ||||
|       lastReadTime = millis(); | ||||
|  | ||||
|       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. | ||||
|      *  | ||||
|      */ | ||||
|     void loop()  | ||||
|     { | ||||
|       if(strip.isUpdating()) return; | ||||
|  | ||||
|       // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms) | ||||
|       if (millis() < nextReadTime) return; | ||||
|  | ||||
|  | ||||
|       nextReadTime = millis() + readingInterval; | ||||
|       lastReadTime = millis(); | ||||
|       initializing = false; | ||||
|  | ||||
|       // read battery raw input | ||||
|       rawValue = analogRead(batteryPin); | ||||
|  | ||||
|       // calculate the voltage      | ||||
|       voltage = (rawValue / adcPrecision) * maxBatteryVoltage ; | ||||
|       // check if voltage is within specified voltage range | ||||
|       voltage = voltage<minBatteryVoltage||voltage>maxBatteryVoltage?-1.0f:voltage; | ||||
|  | ||||
|       // translate battery voltage into percentage | ||||
|       /* | ||||
|         the standard "map" function doesn't work | ||||
|         https://www.arduino.cc/reference/en/language/functions/math/map/  notes and warnings at the bottom | ||||
|       */ | ||||
|       batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); | ||||
|  | ||||
|  | ||||
|       // SmartHome stuff | ||||
|       if (WLED_MQTT_CONNECTED) { | ||||
|         char subuf[64]; | ||||
|         strcpy(subuf, mqttDeviceTopic); | ||||
|         strcat_P(subuf, PSTR("/voltage")); | ||||
|         mqtt->publish(subuf, 0, false, String(voltage).c_str()); | ||||
|       } | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /* | ||||
|      * 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"); | ||||
|  | ||||
|       // info modal display names | ||||
|       JsonArray batteryPercentage = user.createNestedArray("Battery level"); | ||||
|       JsonArray batteryVoltage = user.createNestedArray("Battery voltage"); | ||||
|  | ||||
|       if (initializing) { | ||||
|         batteryPercentage.add((nextReadTime - millis()) / 1000); | ||||
|         batteryPercentage.add(" sec"); | ||||
|         batteryVoltage.add((nextReadTime - millis()) / 1000); | ||||
|         batteryVoltage.add(" sec"); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if(batteryLevel < 0) { | ||||
|         batteryPercentage.add(F("invalid")); | ||||
|       } else { | ||||
|         batteryPercentage.add(batteryLevel); | ||||
|       } | ||||
|       batteryPercentage.add(F(" %")); | ||||
|  | ||||
|       if(voltage < 0) { | ||||
|         batteryVoltage.add(F("invalid")); | ||||
|       } else { | ||||
|         batteryVoltage.add(truncate(voltage, 2)); | ||||
|       } | ||||
|       batteryVoltage.add(F(" V")); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /* | ||||
|      * 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) | ||||
|     { | ||||
|     } | ||||
|     */ | ||||
|  | ||||
|  | ||||
|     /* | ||||
|      * 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 make your settings editable through the Usermod Settings page automatically. | ||||
|      * | ||||
|      * Usermod Settings Overview: | ||||
|      * - Numeric values are treated as floats in the browser. | ||||
|      *   - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float | ||||
|      *     before being returned to the Usermod.  The float data type has only 6-7 decimal digits of precision, and | ||||
|      *     doubles are not supported, numbers will be rounded to the nearest float value when being parsed. | ||||
|      *     The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. | ||||
|      *   - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a | ||||
|      *     C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. | ||||
|      *     Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type | ||||
|      *     used in the Usermod when reading the value from ArduinoJson. | ||||
|      * - Pin values can be treated differently from an integer value by using the key name "pin" | ||||
|      *   - "pin" can contain a single or array of integer values | ||||
|      *   - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins | ||||
|      *     - Red color indicates a conflict.  Yellow color indicates a pin with a warning (e.g. an input-only pin) | ||||
|      *   - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used | ||||
|      * | ||||
|      * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings | ||||
|      *  | ||||
|      * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work.   | ||||
|      * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. | ||||
|      * See the WLED Soundreactive fork (code and wiki) for reference.  https://github.com/atuline/WLED | ||||
|      *  | ||||
|      * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! | ||||
|      */ | ||||
|     void addToConfig(JsonObject& root) | ||||
|     { | ||||
|       // created JSON object:  | ||||
|       /* | ||||
|       { | ||||
|         "Battery-Level": { | ||||
|           "pin": "A0",              <--- only when using esp32 boards | ||||
|           "minBatteryVoltage": 2.6,  | ||||
|           "maxBatteryVoltage": 4.2, | ||||
|           "read-interval-ms": 30000   | ||||
|         } | ||||
|       } | ||||
|       */  | ||||
|       JsonObject battery = root.createNestedObject(FPSTR(_name)); // usermodname | ||||
|       #ifdef ARDUINO_ARCH_ESP32 | ||||
|         battery["pin"] = batteryPin;                              // usermodparam | ||||
|       #endif | ||||
|       battery["minBatteryVoltage"] = minBatteryVoltage;           // usermodparam | ||||
|       battery["maxBatteryVoltage"] = maxBatteryVoltage;           // usermodparam | ||||
|       battery[FPSTR(_readInterval)] = readingInterval; | ||||
|  | ||||
|       DEBUG_PRINTLN(F("Battery 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 immediately after boot, or after saving on the Usermod Settings page) | ||||
|      *  | ||||
|      * 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 :) | ||||
|      *  | ||||
|      * 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) | ||||
|      *  | ||||
|      * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present | ||||
|      * The configComplete variable is true only if the "exampleUsermod" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them | ||||
|      *  | ||||
|      * This function is guaranteed to be called on boot, but could also be called every time settings are updated | ||||
|      */ | ||||
|     bool readFromConfig(JsonObject& root) | ||||
|     { | ||||
|       // looking for JSON object:  | ||||
|       /* | ||||
|       { | ||||
|         "BatteryLevel": { | ||||
|           "pin": "A0",              <--- only when using esp32 boards | ||||
|           "minBatteryVoltage": 2.6,  | ||||
|           "maxBatteryVoltage": 4.2, | ||||
|           "read-interval-ms": 30000   | ||||
|         } | ||||
|       } | ||||
|       */ | ||||
|       #ifdef ARDUINO_ARCH_ESP32 | ||||
|         int8_t newBatteryPin = batteryPin; | ||||
|       #endif | ||||
|  | ||||
|       JsonObject battery = root[FPSTR(_name)]; | ||||
|       if (battery.isNull())  | ||||
|       { | ||||
|         DEBUG_PRINT(FPSTR(_name)); | ||||
|         DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       #ifdef ARDUINO_ARCH_ESP32 | ||||
|         newBatteryPin     = battery["pin"] | newBatteryPin; | ||||
|       #endif | ||||
|       minBatteryVoltage   = battery["minBatteryVoltage"] | minBatteryVoltage; | ||||
|       //minBatteryVoltage = min(12.0f, (int)readingInterval); | ||||
|       maxBatteryVoltage   = battery["maxBatteryVoltage"] | maxBatteryVoltage; | ||||
|       //maxBatteryVoltage = min(14.4f, max(3.3f,(int)readingInterval)); | ||||
|       readingInterval     = battery["read-interval-ms"] | readingInterval; | ||||
|       readingInterval     = max(3000, (int)readingInterval); // minimum repetition is >5000ms (5s) | ||||
|  | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|  | ||||
|       #ifdef ARDUINO_ARCH_ESP32 | ||||
|         if (!initDone)  | ||||
|         { | ||||
|           // first run: reading from cfg.json | ||||
|           newBatteryPin = batteryPin; | ||||
|           DEBUG_PRINTLN(F(" config loaded.")); | ||||
|         }  | ||||
|         else  | ||||
|         { | ||||
|           DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|  | ||||
|           // changing paramters from settings page | ||||
|           if (newBatteryPin != batteryPin)  | ||||
|           { | ||||
|             // deallocate pin | ||||
|             pinManager.deallocatePin(batteryPin); | ||||
|             batteryPin = newBatteryPin; | ||||
|             // initialise | ||||
|             setup(); | ||||
|           } | ||||
|         } | ||||
|       #endif | ||||
|  | ||||
|       return !battery[FPSTR(_readInterval)].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_BATTERY_STATUS_BASIC; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char UsermodBatteryBasic::_name[]         PROGMEM = "Battery-level"; | ||||
| const char UsermodBatteryBasic::_readInterval[] PROGMEM = "read-interval-ms"; | ||||
							
								
								
									
										85
									
								
								usermods/multi_relay/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,85 @@ | ||||
| # Multi Relay | ||||
|  | ||||
| This usermod-v2 modification allows the connection of multiple relays each with individual delay and on/off mode. | ||||
|  | ||||
| ## HTTP API | ||||
| All responses are returned as JSON.  | ||||
|  | ||||
| Status Request: `http://[device-ip]/relays` | ||||
| Switch Command: `http://[device-ip]/relays?switch=1,0,1,1`    | ||||
| The number of numbers behind the switch parameter must correspond to the number of relays. The number 1 switches the relay on. The number 0 switches the relay off.  | ||||
|  | ||||
| Toggle Command: `http://[device-ip]/relays?toggle=1,0,1,1` | ||||
| The number of numbers behind the parameter switch must correspond to the number of relays. The number 1 causes a toggling of the relay. The number 0 leaves the state of the device. | ||||
|  | ||||
| Examples | ||||
| 1. 4 relays at all, relay 2 will be toggled: `http://[device-ip]/relays?toggle=0,1,0,0` | ||||
| 2. 3 relays at all, relay 1&3 will be switched on: `http://[device-ip]/relays?switch=1,0,1` | ||||
|  | ||||
| ## JSON API | ||||
| You can switch relay state using the following JSON object transmitted to: `http://[device-ip]/json` | ||||
|  | ||||
| Switch relay 0 on: `{"MultiRelay":{"relay":0,"on":true}}` | ||||
| Switch relay4 3 & 4 off: `{"MultiRelay":[{"relay":2,"on":false},{"relay":3,"on":false}]}` | ||||
|  | ||||
| ## MQTT API | ||||
|  | ||||
| wled/deviceMAC/relay/0/command on|off|toggle | ||||
| wled/deviceMAC/relay/1/command on|off|toggle | ||||
|  | ||||
| When relay is switched it will publish a message: | ||||
|  | ||||
| wled/deviceMAC/relay/0 on|off | ||||
|  | ||||
|  | ||||
| ## Usermod installation | ||||
|  | ||||
| 1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `usermods.add(new MultiRelay());` at the bottom of `usermods_list.cpp`. | ||||
| or | ||||
| 2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY`in your platformio.ini | ||||
|  | ||||
| You can override the default maximum number (4) of relays by defining MULTI_RELAY_MAX_RELAYS. | ||||
|  | ||||
| Example **usermods_list.cpp**: | ||||
|  | ||||
| ```cpp | ||||
| #include "wled.h" | ||||
| /* | ||||
|  * Register your v2 usermods here! | ||||
|  *   (for v1 usermods using just usermod.cpp, you can ignore this file) | ||||
|  */ | ||||
|  | ||||
| /* | ||||
|  * Add/uncomment your usermod filename here (and once more below) | ||||
|  * || || || | ||||
|  * \/ \/ \/ | ||||
|  */ | ||||
| //#include "usermod_v2_example.h" | ||||
| //#include "usermod_temperature.h" | ||||
| #include "../usermods/usermod_multi_relay.h" | ||||
|  | ||||
| void registerUsermods() | ||||
| { | ||||
|   /* | ||||
|    * Add your usermod class name here | ||||
|    * || || || | ||||
|    * \/ \/ \/ | ||||
|    */ | ||||
|   //usermods.add(new MyExampleUsermod()); | ||||
|   //usermods.add(new UsermodTemperature()); | ||||
|   usermods.add(new MultiRelay()); | ||||
|  | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Configuration | ||||
|  | ||||
| Usermod can be configured in Usermods settings page. | ||||
|  | ||||
| If there is no MultiRelay section, just save current configuration and re-open Usermods settings page.  | ||||
|  | ||||
| Have fun - @blazoncek | ||||
|  | ||||
| ## Change log | ||||
| 2021-04 | ||||
| * First implementation. | ||||
							
								
								
									
										572
									
								
								usermods/multi_relay/usermod_multi_relay.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,572 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| #ifndef MULTI_RELAY_MAX_RELAYS | ||||
|   #define MULTI_RELAY_MAX_RELAYS 4 | ||||
| #endif | ||||
|  | ||||
| #define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) | ||||
|  | ||||
| #define ON  true | ||||
| #define OFF false | ||||
|  | ||||
| /* | ||||
|  * This usermod handles multiple relay outputs. | ||||
|  * These outputs complement built-in relay output in a way that the activation can be delayed. | ||||
|  * They can also activate/deactivate in reverse logic independently. | ||||
|  */ | ||||
|  | ||||
|  | ||||
| typedef struct relay_t { | ||||
|   int8_t pin; | ||||
|   bool active; | ||||
|   bool mode; | ||||
|   bool state; | ||||
|   bool external; | ||||
|   uint16_t delay; | ||||
|   int8_t button; | ||||
| } Relay; | ||||
|  | ||||
|  | ||||
| class MultiRelay : public Usermod { | ||||
|  | ||||
|   private: | ||||
|     // array of relays | ||||
|     Relay _relay[MULTI_RELAY_MAX_RELAYS]; | ||||
|  | ||||
|     // switch timer start time | ||||
|     uint32_t _switchTimerStart = 0; | ||||
|     // old brightness | ||||
|     bool _oldMode; | ||||
|  | ||||
|     // usermod enabled | ||||
|     bool enabled = false;  // needs to be configured (no default config) | ||||
|     // status of initialisation | ||||
|     bool initDone = false; | ||||
|  | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _enabled[]; | ||||
|     static const char _relay_str[]; | ||||
|     static const char _delay_str[]; | ||||
|     static const char _activeHigh[]; | ||||
|     static const char _external[]; | ||||
|     static const char _button[]; | ||||
|  | ||||
|  | ||||
|     void publishMqtt(const char* state, int relay) { | ||||
|       //Check if MQTT Connected, otherwise it will crash the 8266 | ||||
|       if (WLED_MQTT_CONNECTED){ | ||||
|         char subuf[64]; | ||||
|         sprintf_P(subuf, PSTR("%s/relay/%d"), mqttDeviceTopic, relay); | ||||
|         mqtt->publish(subuf, 0, false, state); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * switch off the strip if the delay has elapsed  | ||||
|      */ | ||||
|     void handleOffTimer() { | ||||
|       bool activeRelays = false; | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|         if (_relay[i].active && _switchTimerStart > 0 && millis() - _switchTimerStart > (_relay[i].delay*1000)) { | ||||
|           if (!_relay[i].external) toggleRelay(i); | ||||
|           _relay[i].active = false; | ||||
|         } | ||||
|         activeRelays = activeRelays || _relay[i].active; | ||||
|       } | ||||
|       if (!activeRelays) _switchTimerStart = 0; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * HTTP API handler | ||||
|      * borrowed from: | ||||
|      * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h | ||||
|      */ | ||||
|     #define GEOGABVERSION "0.1.3" | ||||
|     void InitHtmlAPIHandle() {  // https://github.com/me-no-dev/ESPAsyncWebServer | ||||
|       DEBUG_PRINTLN(F("Relays: Initialize HTML API")); | ||||
|  | ||||
|       server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) { | ||||
|         DEBUG_PRINTLN("Relays: HTML API"); | ||||
|         String janswer; | ||||
|         String error = ""; | ||||
|         //int params = request->params(); | ||||
|         janswer = F("{\"NoOfRelays\":"); | ||||
|         janswer += String(MULTI_RELAY_MAX_RELAYS) + ","; | ||||
|  | ||||
|         if (getActiveRelayCount()) { | ||||
|           // Commands | ||||
|           if(request->hasParam("switch")) { | ||||
|             /**** Switch ****/ | ||||
|             AsyncWebParameter* p = request->getParam("switch"); | ||||
|             // Get Values | ||||
|             for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|               int value = getValue(p->value(), ',', i); | ||||
|               if (value==-1) { | ||||
|                 error = F("There must be as much arugments as relays"); | ||||
|               } else { | ||||
|                 // Switch | ||||
|                 if (_relay[i].external) switchRelay(i, (bool)value); | ||||
|               } | ||||
|             } | ||||
|           } else if(request->hasParam("toggle")) { | ||||
|             /**** Toggle ****/ | ||||
|             AsyncWebParameter* p = request->getParam("toggle"); | ||||
|             // Get Values | ||||
|             for (int i=0;i<MULTI_RELAY_MAX_RELAYS;i++) { | ||||
|               int value = getValue(p->value(), ',', i); | ||||
|               if (value==-1) { | ||||
|                 error = F("There must be as mutch arugments as relays"); | ||||
|               } else { | ||||
|                 // Toggle | ||||
|                 if (value && _relay[i].external) toggleRelay(i); | ||||
|               } | ||||
|             } | ||||
|           } else { | ||||
|             error = F("No valid command found"); | ||||
|           } | ||||
|         } else { | ||||
|           error = F("No active relays"); | ||||
|         } | ||||
|  | ||||
|         // Status response | ||||
|         char sbuf[16]; | ||||
|         for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|           sprintf_P(sbuf, PSTR("\"%d\":%d,"), i, (_relay[i].pin<0 ? -1 : (int)_relay[i].state)); | ||||
|           janswer += sbuf; | ||||
|         } | ||||
|         janswer += F("\"error\":\""); | ||||
|         janswer += error; | ||||
|         janswer += F("\","); | ||||
|         janswer += F("\"SW Version\":\""); | ||||
|         janswer += String(GEOGABVERSION); | ||||
|         janswer += F("\"}"); | ||||
|         request->send(200, "application/json", janswer); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     int getValue(String data, char separator, int index) { | ||||
|       int found = 0; | ||||
|       int strIndex[] = {0, -1}; | ||||
|       int maxIndex = data.length()-1; | ||||
|  | ||||
|       for(int i=0; i<=maxIndex && found<=index; i++){ | ||||
|         if(data.charAt(i)==separator || i==maxIndex){ | ||||
|             found++; | ||||
|             strIndex[0] = strIndex[1]+1; | ||||
|             strIndex[1] = (i == maxIndex) ? i+1 : i; | ||||
|         } | ||||
|       } | ||||
|       return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1; | ||||
|     } | ||||
|  | ||||
|   public: | ||||
|     /** | ||||
|      * constructor | ||||
|      */ | ||||
|     MultiRelay() { | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|         _relay[i].pin      = -1; | ||||
|         _relay[i].delay    = 0; | ||||
|         _relay[i].mode     = false; | ||||
|         _relay[i].active   = false; | ||||
|         _relay[i].state    = false; | ||||
|         _relay[i].external = false; | ||||
|         _relay[i].button   = -1; | ||||
|       } | ||||
|     } | ||||
|     /** | ||||
|      * desctructor | ||||
|      */ | ||||
|     ~MultiRelay() {} | ||||
|  | ||||
|     /** | ||||
|      * Enable/Disable the usermod | ||||
|      */ | ||||
|     inline void enable(bool enable) { enabled = enable; } | ||||
|     /** | ||||
|      * Get usermod enabled/disabled state | ||||
|      */ | ||||
|     inline bool isEnabled() { return enabled; } | ||||
|  | ||||
|     /** | ||||
|      * switch relay on/off | ||||
|      */ | ||||
|     void switchRelay(uint8_t relay, bool mode) { | ||||
|       if (relay>=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return; | ||||
|       _relay[relay].state = mode; | ||||
|       pinMode(_relay[relay].pin, OUTPUT); | ||||
|       digitalWrite(_relay[relay].pin, mode ? !_relay[relay].mode : _relay[relay].mode); | ||||
|       publishMqtt(mode ? "on" : "off", relay); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * toggle relay | ||||
|      */ | ||||
|     inline void toggleRelay(uint8_t relay) { | ||||
|       switchRelay(relay, !_relay[relay].state); | ||||
|     } | ||||
|  | ||||
|     uint8_t getActiveRelayCount() { | ||||
|       uint8_t count = 0; | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) if (_relay[i].pin>=0) count++; | ||||
|       return count; | ||||
|     } | ||||
|  | ||||
|     //Functions called by WLED | ||||
|  | ||||
|     /** | ||||
|      * handling of MQTT message | ||||
|      * topic only contains stripped topic (part after /wled/MAC) | ||||
|      * topic should look like: /relay/X/command; where X is relay number, 0 based | ||||
|      */ | ||||
|     bool onMqttMessage(char* topic, char* payload) { | ||||
|       if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) { | ||||
|         uint8_t relay = strtoul(topic+7, NULL, 10); | ||||
|         if (relay<MULTI_RELAY_MAX_RELAYS) { | ||||
|           String action = payload; | ||||
|           if (action == "on") { | ||||
|             if (_relay[relay].external) switchRelay(relay, true); | ||||
|             return true; | ||||
|           } else if (action == "off") { | ||||
|             if (_relay[relay].external) switchRelay(relay, false); | ||||
|             return true; | ||||
|           } else if (action == "toggle") { | ||||
|             if (_relay[relay].external) toggleRelay(relay); | ||||
|             return true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * subscribe to MQTT topic for controlling relays | ||||
|      */ | ||||
|     void onMqttConnect(bool sessionPresent) { | ||||
|       //(re)subscribe to required topics | ||||
|       char subuf[64]; | ||||
|       if (mqttDeviceTopic[0] != 0) { | ||||
|         strcpy(subuf, mqttDeviceTopic); | ||||
|         strcat_P(subuf, PSTR("/relay/#")); | ||||
|         mqtt->subscribe(subuf, 0); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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() { | ||||
|       // pins retrieved from cfg.json (readFromConfig()) prior to running setup() | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|         if (_relay[i].pin<0) continue; | ||||
|         if (!pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { | ||||
|           _relay[i].pin = -1;  // allocation failed | ||||
|         } else { | ||||
|           if (!_relay[i].external) _relay[i].state = offMode; | ||||
|           switchRelay(i, _relay[i].state); | ||||
|           _relay[i].active = false; | ||||
|         } | ||||
|       } | ||||
|       _oldMode = offMode; | ||||
|       initDone = true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * connected() is called every time the WiFi is (re)connected | ||||
|      * Use it to initialize network interfaces | ||||
|      */ | ||||
|     void connected() { | ||||
|       InitHtmlAPIHandle(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * loop() is called continuously. Here you can check for events, read sensors, etc. | ||||
|      */ | ||||
|     void loop() { | ||||
|       yield(); | ||||
|       if (!enabled || strip.isUpdating()) return; | ||||
|  | ||||
|       static unsigned long lastUpdate = 0; | ||||
|       if (millis() - lastUpdate < 100) return;  // update only 10 times/s | ||||
|       lastUpdate = millis(); | ||||
|  | ||||
|       //set relay when LEDs turn on | ||||
|       if (_oldMode != offMode) { | ||||
|         _oldMode = offMode; | ||||
|         _switchTimerStart = millis(); | ||||
|         for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|           if (_relay[i].pin>=0 && !_relay[i].external) _relay[i].active = true; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       handleOffTimer(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 (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; | ||||
|       } | ||||
|  | ||||
|       bool handled = false; | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|         if (_relay[i].button == b) { | ||||
|           handled = true; | ||||
|         } | ||||
|       } | ||||
|       if (!handled) return false; | ||||
|  | ||||
|       unsigned long now = millis(); | ||||
|  | ||||
|       //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) | ||||
|       if (buttonType[b] == BTN_TYPE_SWITCH) { | ||||
|         //handleSwitch(b); | ||||
|         if (buttonPressedBefore[b] != isButtonPressed(b)) { | ||||
|           buttonPressedTime[b] = now; | ||||
|           buttonPressedBefore[b] = !buttonPressedBefore[b]; | ||||
|         } | ||||
|  | ||||
|         if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled; | ||||
|            | ||||
|         if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) | ||||
|           for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|             if (_relay[i].pin>=0 && _relay[i].button == b) { | ||||
|               switchRelay(i, buttonPressedBefore[b]); | ||||
|               buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         return handled; | ||||
|       } | ||||
|  | ||||
|       //momentary button logic | ||||
|       if (isButtonPressed(b)) { //pressed | ||||
|  | ||||
|         if (!buttonPressedBefore[b]) buttonPressedTime[b] = now; | ||||
|         buttonPressedBefore[b] = true; | ||||
|  | ||||
|         if (now - buttonPressedTime[b] > 600) { //long press | ||||
|           buttonLongPressed[b] = true; | ||||
|         } | ||||
|  | ||||
|       } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released | ||||
|  | ||||
|         long dur = now - buttonPressedTime[b]; | ||||
|         if (dur < WLED_DEBOUNCE_THRESHOLD) { | ||||
|           buttonPressedBefore[b] = false; | ||||
|           return handled; | ||||
|         } //too short "press", debounce | ||||
|         bool doublePress = buttonWaitTime[b]; //did we have short press before? | ||||
|         buttonWaitTime[b] = 0; | ||||
|  | ||||
|         if (!buttonLongPressed[b]) { //short press | ||||
|           // if this is second release within 350ms it is a double press (buttonWaitTime!=0) | ||||
|           if (doublePress) { | ||||
|             //doublePressAction(b); | ||||
|           } else  { | ||||
|             buttonWaitTime[b] = now; | ||||
|           } | ||||
|         } | ||||
|         buttonPressedBefore[b] = false; | ||||
|         buttonLongPressed[b] = false; | ||||
|       } | ||||
|       // if 450ms elapsed since last press/release it is a short press | ||||
|       if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) { | ||||
|         buttonWaitTime[b] = 0; | ||||
|         for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|           if (_relay[i].pin>=0 && _relay[i].button == b) { | ||||
|             toggleRelay(i); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return handled; | ||||
|     } | ||||
|    | ||||
|     /** | ||||
|      * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. | ||||
|      */ | ||||
|     void addToJsonInfo(JsonObject &root) { | ||||
|       if (enabled) { | ||||
|         JsonObject user = root["u"]; | ||||
|         if (user.isNull()) | ||||
|           user = root.createNestedObject("u"); | ||||
|  | ||||
|         JsonArray infoArr = user.createNestedArray(F("Number of relays")); //name | ||||
|         infoArr.add(String(getActiveRelayCount())); | ||||
|  | ||||
|         String uiDomString; | ||||
|         for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|           if (_relay[i].pin<0 || !_relay[i].external) continue; | ||||
|           uiDomString = F("<button class=\"btn\" onclick=\"requestJson({"); | ||||
|           uiDomString += FPSTR(_name); | ||||
|           uiDomString += F(":{"); | ||||
|           uiDomString += FPSTR(_relay_str); | ||||
|           uiDomString += F(":"); | ||||
|           uiDomString += i; | ||||
|           uiDomString += F(",on:"); | ||||
|           uiDomString += _relay[i].state ? "false" : "true"; | ||||
|           uiDomString += F("}});\">"); | ||||
|           uiDomString += F("Relay "); | ||||
|           uiDomString += i; | ||||
|           uiDomString += F(" <i class=\"icons\"></i></button>"); | ||||
|           JsonArray infoArr = user.createNestedArray(uiDomString); // timer value | ||||
|  | ||||
|           infoArr.add(_relay[i].state ? "on" : "off"); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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) { | ||||
|       if (!initDone || !enabled) return;  // prevent crash on boot applyPreset() | ||||
|       JsonObject multiRelay = root[FPSTR(_name)]; | ||||
|       if (multiRelay.isNull()) { | ||||
|         multiRelay = root.createNestedObject(FPSTR(_name)); | ||||
|       } | ||||
|       #if MULTI_RELAY_MAX_RELAYS > 1 | ||||
|       JsonArray rel_arr = multiRelay.createNestedArray(F("relays")); | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|         if (_relay[i].pin < 0) continue; | ||||
|         JsonObject relay = rel_arr.createNestedObject(); | ||||
|         relay[FPSTR(_relay_str)] = i; | ||||
|         relay[F("state")] = _relay[i].state; | ||||
|       } | ||||
|       #else | ||||
|       multiRelay[FPSTR(_relay_str)] = 0; | ||||
|       multiRelay[F("state")] = _relay[0].state; | ||||
|       #endif | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 || !enabled) return;  // prevent crash on boot applyPreset() | ||||
|       JsonObject usermod = root[FPSTR(_name)]; | ||||
|       if (!usermod.isNull()) { | ||||
|         if (usermod["on"].is<bool>() && usermod[FPSTR(_relay_str)].is<int>() && usermod[FPSTR(_relay_str)].as<int>()>=0) { | ||||
|           switchRelay(usermod[FPSTR(_relay_str)].as<int>(), usermod["on"].as<bool>()); | ||||
|         } | ||||
|       } else if (root[FPSTR(_name)].is<JsonArray>()) { | ||||
|         JsonArray relays = root[FPSTR(_name)].as<JsonArray>(); | ||||
|         for (JsonVariant r : relays) { | ||||
|           if (r["on"].is<bool>() && r[FPSTR(_relay_str)].is<int>() && r[FPSTR(_relay_str)].as<int>()>=0) { | ||||
|             switchRelay(r[FPSTR(_relay_str)].as<int>(), r["on"].as<bool>()); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * provide the changeable values | ||||
|      */ | ||||
|     void addToConfig(JsonObject &root) { | ||||
|       JsonObject top = root.createNestedObject(FPSTR(_name)); | ||||
|  | ||||
|       top[FPSTR(_enabled)] = enabled; | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|         String parName = FPSTR(_relay_str); parName += '-'; parName += i; | ||||
|         JsonObject relay = top.createNestedObject(parName); | ||||
|         relay["pin"]              = _relay[i].pin; | ||||
|         relay[FPSTR(_activeHigh)] = _relay[i].mode; | ||||
|         relay[FPSTR(_delay_str)]  = _relay[i].delay; | ||||
|         relay[FPSTR(_external)]   = _relay[i].external; | ||||
|         relay[FPSTR(_button)]     = _relay[i].button; | ||||
|       } | ||||
|       DEBUG_PRINTLN(F("MultiRelay config saved.")); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * restore the changeable values | ||||
|      * 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) { | ||||
|       int8_t oldPin[MULTI_RELAY_MAX_RELAYS]; | ||||
|  | ||||
|       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; | ||||
|  | ||||
|       for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|         String parName = FPSTR(_relay_str); parName += '-'; parName += i; | ||||
|         oldPin[i]          = _relay[i].pin; | ||||
|         _relay[i].pin      = top[parName]["pin"] | _relay[i].pin; | ||||
|         _relay[i].mode     = top[parName][FPSTR(_activeHigh)] | _relay[i].mode; | ||||
|         _relay[i].external = top[parName][FPSTR(_external)]   | _relay[i].external; | ||||
|         _relay[i].delay    = top[parName][FPSTR(_delay_str)]  | _relay[i].delay; | ||||
|         _relay[i].button   = top[parName][FPSTR(_button)]     | _relay[i].button; | ||||
|         // begin backwards compatibility (beta) remove when 0.13 is released | ||||
|         parName += '-'; | ||||
|         _relay[i].pin      = top[parName+"pin"] | _relay[i].pin; | ||||
|         _relay[i].mode     = top[parName+FPSTR(_activeHigh)] | _relay[i].mode; | ||||
|         _relay[i].external = top[parName+FPSTR(_external)]   | _relay[i].external; | ||||
|         _relay[i].delay    = top[parName+FPSTR(_delay_str)]  | _relay[i].delay; | ||||
|         // end compatibility | ||||
|         _relay[i].delay    = min(600,max(0,abs((int)_relay[i].delay))); // bounds checking max 10min | ||||
|       } | ||||
|  | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       if (!initDone) { | ||||
|         // reading config prior to setup() | ||||
|         DEBUG_PRINTLN(F(" config loaded.")); | ||||
|       } else { | ||||
|         // deallocate all pins 1st | ||||
|         for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) | ||||
|           if (oldPin[i]>=0) { | ||||
|             pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); | ||||
|           } | ||||
|         // allocate new pins | ||||
|         for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|           if (_relay[i].pin>=0 && pinManager.allocatePin(_relay[i].pin, true, PinOwner::UM_MultiRelay)) { | ||||
|             if (!_relay[i].external) { | ||||
|               switchRelay(i, offMode); | ||||
|             } | ||||
|           } else { | ||||
|             _relay[i].pin = -1; | ||||
|           } | ||||
|           _relay[i].active = false; | ||||
|         } | ||||
|         DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|       } | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return !top[F("relay-0")][FPSTR(_button)].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_MULTI_RELAY; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char MultiRelay::_name[]       PROGMEM = "MultiRelay"; | ||||
| const char MultiRelay::_enabled[]    PROGMEM = "enabled"; | ||||
| const char MultiRelay::_relay_str[]  PROGMEM = "relay"; | ||||
| const char MultiRelay::_delay_str[]  PROGMEM = "delay-s"; | ||||
| const char MultiRelay::_activeHigh[] PROGMEM = "active-high"; | ||||
| const char MultiRelay::_external[]   PROGMEM = "external"; | ||||
| const char MultiRelay::_button[]     PROGMEM = "button"; | ||||
| @@ -5,6 +5,8 @@ | ||||
|  * I've had good results with settings around 5 (20 fps). | ||||
|  *  | ||||
|  */ | ||||
| #include "wled.h" | ||||
|  | ||||
| const uint8_t PCARS_dimcolor = 20; | ||||
| WiFiUDP UDP; | ||||
| const unsigned int PCARS_localUdpPort = 5606; // local port to listen on | ||||
| @@ -49,11 +51,12 @@ void PCARS_readValues() { | ||||
| void PCARS_buildcolorbars() { | ||||
|   boolean activated = false; | ||||
|   float ledratio = 0; | ||||
|   uint16_t totalLen = strip.getLengthTotal(); | ||||
|  | ||||
|   for (uint16_t i = 0; i < ledCount; i++) { | ||||
|   for (uint16_t i = 0; i < totalLen; i++) { | ||||
|     if (PCARS_rpmRatio < .95 || (millis() % 100 > 70 )) { | ||||
|  | ||||
|       ledratio = (float)i / (float)ledCount; | ||||
|       ledratio = (float)i / (float)totalLen; | ||||
|       if (ledratio < PCARS_rpmRatio) { | ||||
|         activated = true; | ||||
|       } else { | ||||
|   | ||||
							
								
								
									
										755
									
								
								usermods/quinled-an-penta/quinled-an-penta.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,755 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "U8g2lib.h" | ||||
| #include "SHT85.h" | ||||
| #include "Wire.h" | ||||
| #include "wled.h" | ||||
|  | ||||
| class QuinLEDAnPentaUsermod : public Usermod | ||||
| { | ||||
|   private: | ||||
|     bool enabled = false; | ||||
|     bool firstRunDone = false; | ||||
|     bool initDone = false; | ||||
|     U8G2 *oledDisplay = nullptr; | ||||
|     SHT *sht30TempHumidSensor; | ||||
|  | ||||
|     // Network info vars | ||||
|     bool networkHasChanged = false; | ||||
|     bool lastKnownNetworkConnected; | ||||
|     IPAddress lastKnownIp; | ||||
|     bool lastKnownWiFiConnected; | ||||
|     String lastKnownSsid; | ||||
|     bool lastKnownApActive; | ||||
|     char *lastKnownApSsid; | ||||
|     char *lastKnownApPass; | ||||
|     byte lastKnownApChannel; | ||||
|     int lastKnownEthType; | ||||
|     bool lastKnownEthLinkUp; | ||||
|  | ||||
|     // Brightness / LEDC vars | ||||
|     byte lastKnownBri = 0; | ||||
|     int8_t currentBussesNumPins[5] = {0, 0, 0, 0, 0}; | ||||
|     int8_t currentLedPins[5] = {0, 0, 0, 0, 0}; | ||||
|     uint8_t currentLedcReads[5] = {0, 0, 0, 0, 0}; | ||||
|     uint8_t lastKnownLedcReads[5] = {0, 0, 0, 0, 0}; | ||||
|  | ||||
|     // OLED vars | ||||
|     bool oledEnabled = false; | ||||
|     bool oledInitDone = false; | ||||
|     bool oledUseProgressBars = false; | ||||
|     bool oledFlipScreen = false; | ||||
|     bool oledFixBuggedScreen = false; | ||||
|     byte oledMaxPage = 3; | ||||
|     byte oledCurrentPage = 3; // Start with the network page to help identifying the IP | ||||
|     byte oledSecondsPerPage = 10; | ||||
|     unsigned long oledLogoDrawn = 0; | ||||
|     unsigned long oledLastTimeUpdated = 0; | ||||
|     unsigned long oledLastTimePageChange = 0; | ||||
|     unsigned long oledLastTimeFixBuggedScreen = 0; | ||||
|  | ||||
|     // SHT30 vars | ||||
|     bool shtEnabled = false; | ||||
|     bool shtInitDone = false; | ||||
|     bool shtReadDataSuccess = false; | ||||
|     byte shtI2cAddress = 0x44; | ||||
|     unsigned long shtLastTimeUpdated = 0; | ||||
|     bool shtDataRequested = false; | ||||
|     float shtCurrentTemp = 0; | ||||
|     float shtLastKnownTemp = 0; | ||||
|     float shtCurrentHumidity = 0; | ||||
|     float shtLastKnownHumidity = 0; | ||||
|  | ||||
|     // Pin/IO vars | ||||
|     const int8_t anPentaPins[5] = {14, 13, 12, 4, 2}; | ||||
|     int8_t oledSpiClk = 15; | ||||
|     int8_t oledSpiData = 16; | ||||
|     int8_t oledSpiCs = 0; | ||||
|     int8_t oledSpiDc = 32; | ||||
|     int8_t oledSpiRst = 33; | ||||
|     int8_t shtSda = 1; | ||||
|     int8_t shtScl = 3; | ||||
|  | ||||
|  | ||||
|     bool isAnPentaLedPin(int8_t pin) | ||||
|     { | ||||
|       for(int8_t i = 0; i <= 4; i++) | ||||
|       { | ||||
|         if(anPentaPins[i] == pin) | ||||
|           return true; | ||||
|       } | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     void getCurrentUsedLedPins() | ||||
|     { | ||||
|       for (int8_t lp = 0; lp <= 4; lp++) currentLedPins[lp] = 0; | ||||
|       byte numBusses = busses.getNumBusses(); | ||||
|       byte numUsedPins = 0; | ||||
|  | ||||
|       for (int8_t b = 0; b < numBusses; b++) { | ||||
|         Bus* curBus = busses.getBus(b); | ||||
|         if (curBus != nullptr) { | ||||
|           uint8_t pins[5] = {0, 0, 0, 0, 0}; | ||||
|           currentBussesNumPins[b] = curBus->getPins(pins); | ||||
|           for (int8_t p = 0; p < currentBussesNumPins[b]; p++) { | ||||
|             if (isAnPentaLedPin(pins[p])) { | ||||
|               currentLedPins[numUsedPins] = pins[p]; | ||||
|               numUsedPins++; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void getCurrentLedcValues() | ||||
|     { | ||||
|       byte numBusses = busses.getNumBusses(); | ||||
|       byte numLedc = 0; | ||||
|  | ||||
|       for (int8_t b = 0; b < numBusses; b++) { | ||||
|         Bus* curBus = busses.getBus(b); | ||||
|         if (curBus != nullptr) { | ||||
|           uint32_t curPixColor = curBus->getPixelColor(0); | ||||
|           uint8_t _data[5] = {255, 255, 255, 255, 255}; | ||||
|           _data[3] = curPixColor >> 24; | ||||
|           _data[0] = curPixColor >> 16; | ||||
|           _data[1] = curPixColor >> 8; | ||||
|           _data[2] = curPixColor; | ||||
|  | ||||
|           for (uint8_t i = 0; i < currentBussesNumPins[b]; i++) { | ||||
|             currentLedcReads[numLedc] = (_data[i] * bri) / 255; | ||||
|             numLedc++; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     void initOledDisplay() | ||||
|     { | ||||
|       PinManagerPinType pins[5] = { { oledSpiClk, true }, { oledSpiData, true }, { oledSpiCs, true }, { oledSpiDc, true }, { oledSpiRst, true } }; | ||||
|       if (!pinManager.allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { | ||||
|         DEBUG_PRINTF("[%s] OLED pin allocation failed!\n", _name); | ||||
|         oledEnabled = oledInitDone = false; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       oledDisplay = (U8G2 *) new U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI(U8G2_R0, oledSpiClk, oledSpiData, oledSpiCs, oledSpiDc, oledSpiRst); | ||||
|       if (oledDisplay == nullptr) { | ||||
|         DEBUG_PRINTF("[%s] OLED init failed!\n", _name); | ||||
|         oledEnabled = oledInitDone = false; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       oledDisplay->begin(); | ||||
|       oledDisplay->setBusClock(40 * 1000 * 1000); | ||||
|       oledDisplay->setContrast(10); | ||||
|       oledDisplay->setPowerSave(0); | ||||
|       oledDisplay->setFont(u8g2_font_6x10_tf); | ||||
|       oledDisplay->setFlipMode(oledFlipScreen); | ||||
|  | ||||
|       oledDisplay->firstPage(); | ||||
|       do { | ||||
|         oledDisplay->drawXBMP(0, 16, 128, 36, quinLedLogo); | ||||
|       } while (oledDisplay->nextPage()); | ||||
|       oledLogoDrawn = millis(); | ||||
|  | ||||
|       oledInitDone = true; | ||||
|     } | ||||
|  | ||||
|     void cleanupOledDisplay() | ||||
|     { | ||||
|       if (oledInitDone) { | ||||
|         oledDisplay->clear(); | ||||
|       } | ||||
|  | ||||
|       pinManager.deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); | ||||
|       pinManager.deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); | ||||
|       pinManager.deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); | ||||
|       pinManager.deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); | ||||
|       pinManager.deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); | ||||
|  | ||||
|       delete oledDisplay; | ||||
|  | ||||
|       oledEnabled = false; | ||||
|       oledInitDone = false; | ||||
|     } | ||||
|  | ||||
|     bool isOledReady() | ||||
|     { | ||||
|       return oledEnabled && oledInitDone; | ||||
|     } | ||||
|  | ||||
|     void initSht30TempHumiditySensor() | ||||
|     { | ||||
|       PinManagerPinType pins[2] = { { shtSda, true }, { shtScl, true } }; | ||||
|       if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { | ||||
|         DEBUG_PRINTF("[%s] SHT30 pin allocation failed!\n", _name); | ||||
|         shtEnabled = shtInitDone = false; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       TwoWire *wire = new TwoWire(1); | ||||
|       wire->setClock(400000); | ||||
|  | ||||
|       sht30TempHumidSensor = (SHT *) new SHT30(); | ||||
|       sht30TempHumidSensor->begin(shtI2cAddress, wire); | ||||
|       // The SHT lib calls wire.begin() again without the SDA and SCL pins... So call it again here... | ||||
|       wire->begin(shtSda, shtScl); | ||||
|       if (sht30TempHumidSensor->readStatus() == 0xFFFF) { | ||||
|         DEBUG_PRINTF("[%s] SHT30 init failed!\n", _name); | ||||
|         shtEnabled = shtInitDone = false; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       shtInitDone = true; | ||||
|     } | ||||
|  | ||||
|     void cleanupSht30TempHumiditySensor() | ||||
|     { | ||||
|       if (shtInitDone) { | ||||
|         sht30TempHumidSensor->reset(); | ||||
|       } | ||||
|  | ||||
|       pinManager.deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); | ||||
|       pinManager.deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); | ||||
|  | ||||
|       delete sht30TempHumidSensor; | ||||
|  | ||||
|       shtEnabled = false; | ||||
|       shtInitDone = false; | ||||
|     } | ||||
|  | ||||
|     void cleanup() | ||||
|     { | ||||
|       if (isOledReady()) { | ||||
|         cleanupOledDisplay(); | ||||
|       } | ||||
|  | ||||
|       if (isShtReady()) { | ||||
|         cleanupSht30TempHumiditySensor(); | ||||
|       } | ||||
|  | ||||
|       enabled = false; | ||||
|     } | ||||
|  | ||||
|     bool oledCheckForNetworkChanges() | ||||
|     { | ||||
|       if (lastKnownNetworkConnected != Network.isConnected() || lastKnownIp != Network.localIP() | ||||
|           || lastKnownWiFiConnected != WiFi.isConnected() || lastKnownSsid != WiFi.SSID() | ||||
|           || lastKnownApActive != apActive || lastKnownApSsid != apSSID || lastKnownApPass != apPass || lastKnownApChannel != apChannel) { | ||||
|         lastKnownNetworkConnected = Network.isConnected(); | ||||
|         lastKnownIp = Network.localIP(); | ||||
|         lastKnownWiFiConnected = WiFi.isConnected(); | ||||
|         lastKnownSsid = WiFi.SSID(); | ||||
|         lastKnownApActive = apActive; | ||||
|         lastKnownApSsid = apSSID; | ||||
|         lastKnownApPass = apPass; | ||||
|         lastKnownApChannel = apChannel; | ||||
|  | ||||
|         return networkHasChanged = true; | ||||
|       } | ||||
|       #ifdef WLED_USE_ETHERNET | ||||
|       if (lastKnownEthType != ethernetType || lastKnownEthLinkUp != ETH.linkUp()) { | ||||
|         lastKnownEthType = ethernetType; | ||||
|         lastKnownEthLinkUp = ETH.linkUp(); | ||||
|  | ||||
|         return networkHasChanged = true; | ||||
|       } | ||||
|       #endif | ||||
|  | ||||
|       return networkHasChanged = false; | ||||
|     } | ||||
|  | ||||
|     byte oledGetNextPage() | ||||
|     { | ||||
|       return oledCurrentPage + 1 <= oledMaxPage ? oledCurrentPage + 1 : 1; | ||||
|     } | ||||
|  | ||||
|     void oledShowPage(byte page, bool updateLastTimePageChange = false) | ||||
|     { | ||||
|       oledCurrentPage = page; | ||||
|       updateOledDisplay(); | ||||
|       oledLastTimeUpdated = millis(); | ||||
|       if (updateLastTimePageChange) oledLastTimePageChange = oledLastTimeUpdated; | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * Page 1: Overall brightness and LED outputs | ||||
|      * Page 2: General info like temp, humidity and others | ||||
|      * Page 3: Network info | ||||
|      */ | ||||
|     void updateOledDisplay() | ||||
|     { | ||||
|       if (!isOledReady()) return; | ||||
|  | ||||
|       oledDisplay->firstPage(); | ||||
|       do { | ||||
|         oledDisplay->setFont(u8g2_font_chroma48medium8_8r); | ||||
|         oledDisplay->drawStr(0, 8, serverDescription); | ||||
|         oledDisplay->drawHLine(0, 13, 127); | ||||
|         oledDisplay->setFont(u8g2_font_6x10_tf); | ||||
|  | ||||
|         byte charPerRow = 21; | ||||
|         byte oledRow = 23; | ||||
|         switch (oledCurrentPage) { | ||||
|           // LED Outputs | ||||
|           case 1: | ||||
|           { | ||||
|             char charCurrentBrightness[charPerRow+1] = "Brightness:"; | ||||
|             if (oledUseProgressBars) { | ||||
|               oledDisplay->drawStr(0, oledRow, charCurrentBrightness); | ||||
|               // There is no method to draw a filled box with rounded corners. So draw the rounded frame first, then fill that frame accordingly to LED percentage | ||||
|               oledDisplay->drawRFrame(68, oledRow - 6, 60, 7, 2); | ||||
|               oledDisplay->drawBox(69, oledRow - 5, int(round(58*getPercentageForBrightness(bri)) / 100), 5); | ||||
|             } | ||||
|             else { | ||||
|               sprintf(charCurrentBrightness, "%s %d%%", charCurrentBrightness, getPercentageForBrightness(bri)); | ||||
|               oledDisplay->drawStr(0, oledRow, charCurrentBrightness); | ||||
|             } | ||||
|             oledRow += 8; | ||||
|  | ||||
|             byte drawnLines = 0; | ||||
|             for (int8_t app = 0; app <= 4; app++) { | ||||
|               for (int8_t clp = 0; clp <= 4; clp++) { | ||||
|                 if (anPentaPins[app] == currentLedPins[clp]) { | ||||
|                   char charCurrentLedcReads[17]; | ||||
|                   sprintf(charCurrentLedcReads, "LED %d:", app+1); | ||||
|                   if (oledUseProgressBars) { | ||||
|                     oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads); | ||||
|                     oledDisplay->drawRFrame(38, oledRow - 6 + (drawnLines * 8), 90, 7, 2); | ||||
|                     oledDisplay->drawBox(39, oledRow - 5 + (drawnLines * 8), int(round(88*getPercentageForBrightness(currentLedcReads[clp])) / 100), 5); | ||||
|                   } | ||||
|                   else { | ||||
|                     sprintf(charCurrentLedcReads, "%s %d%%", charCurrentLedcReads, getPercentageForBrightness(currentLedcReads[clp])); | ||||
|                     oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads); | ||||
|                   } | ||||
|  | ||||
|                   drawnLines++; | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|             break; | ||||
|           } | ||||
|  | ||||
|           // Various info | ||||
|           case 2: | ||||
|           { | ||||
|             if (isShtReady() && shtReadDataSuccess) { | ||||
|               char charShtCurrentTemp[charPerRow+4]; // Reserve 3 more bytes than usual as we gonna have one UTF8 char which can be up to 4 bytes. | ||||
|               sprintf(charShtCurrentTemp, "Temperature: %.02f°C", shtCurrentTemp); | ||||
|               char charShtCurrentHumidity[charPerRow+1]; | ||||
|               sprintf(charShtCurrentHumidity, "Humidity: %.02f RH", shtCurrentHumidity); | ||||
|  | ||||
|               oledDisplay->drawUTF8(0, oledRow, charShtCurrentTemp); | ||||
|               oledDisplay->drawStr(0, oledRow + 10, charShtCurrentHumidity); | ||||
|               oledRow += 20; | ||||
|             } | ||||
|  | ||||
|             if (mqttEnabled && mqttServer[0] != 0) { | ||||
|               char charMqttStatus[charPerRow+1]; | ||||
|               sprintf(charMqttStatus, "MQTT: %s", (WLED_MQTT_CONNECTED ? "Connected" : "Disconnected")); | ||||
|               oledDisplay->drawStr(0, oledRow, charMqttStatus); | ||||
|               oledRow += 10; | ||||
|             } | ||||
|  | ||||
|             // Always draw these two on the bottom | ||||
|             char charUptime[charPerRow+1]; | ||||
|             sprintf(charUptime, "Uptime: %ds", int(millis()/1000 + rolloverMillis*4294967)); // From json.cpp | ||||
|             oledDisplay->drawStr(0, 53, charUptime); | ||||
|  | ||||
|             char charWledVersion[charPerRow+1]; | ||||
|             sprintf(charWledVersion, "WLED v%s", versionString); | ||||
|             oledDisplay->drawStr(0, 63, charWledVersion); | ||||
|             break; | ||||
|           } | ||||
|  | ||||
|           // Network Info | ||||
|           case 3: | ||||
|             #ifdef WLED_USE_ETHERNET | ||||
|               if (lastKnownEthType == WLED_ETH_NONE) { | ||||
|                 oledDisplay->drawStr(0, oledRow, "Ethernet: No board selected"); | ||||
|                 oledRow += 10; | ||||
|               } | ||||
|               else if (!lastKnownEthLinkUp) { | ||||
|                 oledDisplay->drawStr(0, oledRow, "Ethernet: Link Down"); | ||||
|                 oledRow += 10; | ||||
|               } | ||||
|             #endif | ||||
|  | ||||
|             if (lastKnownNetworkConnected) { | ||||
|               #ifdef WLED_USE_ETHERNET | ||||
|                 if (lastKnownEthLinkUp) { | ||||
|                   oledDisplay->drawStr(0, oledRow, "Ethernet: Link Up"); | ||||
|                   oledRow += 10; | ||||
|                 } | ||||
|                 else | ||||
|               #endif | ||||
|               // Wi-Fi can be active with ETH being connected, but we don't mind... | ||||
|               if (lastKnownWiFiConnected) { | ||||
|                 #ifdef WLED_USE_ETHERNET | ||||
|                   if (!lastKnownEthLinkUp) { | ||||
|                 #endif | ||||
|  | ||||
|                 oledDisplay->drawStr(0, oledRow, "Wi-Fi: Connected"); | ||||
|                 char currentSsidChar[lastKnownSsid.length() + 1]; | ||||
|                 lastKnownSsid.toCharArray(currentSsidChar, lastKnownSsid.length() + 1); | ||||
|                 char charCurrentSsid[50]; | ||||
|                 sprintf(charCurrentSsid, "SSID: %s", currentSsidChar); | ||||
|                 oledDisplay->drawStr(0, oledRow + 10, charCurrentSsid); | ||||
|                 oledRow += 20; | ||||
|  | ||||
|                 #ifdef WLED_USE_ETHERNET | ||||
|                   } | ||||
|                 #endif | ||||
|               } | ||||
|  | ||||
|               String currentIpStr = lastKnownIp.toString(); | ||||
|               char currentIpChar[currentIpStr.length() + 1]; | ||||
|               currentIpStr.toCharArray(currentIpChar, currentIpStr.length() + 1); | ||||
|               char charCurrentIp[30]; | ||||
|               sprintf(charCurrentIp, "IP: %s", currentIpChar); | ||||
|               oledDisplay->drawStr(0, oledRow, charCurrentIp); | ||||
|             } | ||||
|             // If WLED AP is active. Theoretically, it can even be active with ETH being connected, but we don't mind... | ||||
|             else if (lastKnownApActive) { | ||||
|               char charCurrentApStatus[charPerRow+1]; | ||||
|               sprintf(charCurrentApStatus, "WLED AP: %s (Ch: %d)", (lastKnownApActive ? "On" : "Off"), lastKnownApChannel); | ||||
|               oledDisplay->drawStr(0, oledRow, charCurrentApStatus); | ||||
|  | ||||
|               char charCurrentApSsid[charPerRow+1]; | ||||
|               sprintf(charCurrentApSsid, "SSID: %s", lastKnownApSsid); | ||||
|               oledDisplay->drawStr(0, oledRow + 10, charCurrentApSsid); | ||||
|  | ||||
|               char charCurrentApPass[charPerRow+1]; | ||||
|               sprintf(charCurrentApPass, "PW: %s", lastKnownApPass); | ||||
|               oledDisplay->drawStr(0, oledRow + 20, charCurrentApPass); | ||||
|  | ||||
|               // IP is hardcoded / no var exists in WLED at the time this mod was coded, so also hardcode it here | ||||
|               oledDisplay->drawStr(0, oledRow + 30, "IP: 4.3.2.1"); | ||||
|             } | ||||
|  | ||||
|             break; | ||||
|         } | ||||
|       } while (oledDisplay->nextPage()); | ||||
|     } | ||||
|  | ||||
|     bool isShtReady() | ||||
|     { | ||||
|       return shtEnabled && shtInitDone; | ||||
|     } | ||||
|  | ||||
|  | ||||
|   public: | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _enabled[]; | ||||
|     static const char _oledEnabled[]; | ||||
|     static const char _oledUseProgressBars[]; | ||||
|     static const char _oledFlipScreen[]; | ||||
|     static const char _oledSecondsPerPage[]; | ||||
|     static const char _oledFixBuggedScreen[]; | ||||
|     static const char _shtEnabled[]; | ||||
|     static const unsigned char quinLedLogo[]; | ||||
|  | ||||
|  | ||||
|     static int8_t getPercentageForBrightness(byte brightness) | ||||
|     { | ||||
|       return int(((float)brightness / (float)255) * 100); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /* | ||||
|       * 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() | ||||
|     { | ||||
|       if (enabled) { | ||||
|         lastKnownBri = bri; | ||||
|  | ||||
|         if (oledEnabled) { | ||||
|           initOledDisplay(); | ||||
|         } | ||||
|  | ||||
|         if (shtEnabled) { | ||||
|           initSht30TempHumiditySensor(); | ||||
|         } | ||||
|  | ||||
|         getCurrentUsedLedPins(); | ||||
|  | ||||
|         initDone = true; | ||||
|       } | ||||
|  | ||||
|       firstRunDone = true; | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|       * 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 || !initDone || strip.isUpdating()) return; | ||||
|  | ||||
|       if (isShtReady()) { | ||||
|         if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) { | ||||
|           sht30TempHumidSensor->requestData(); | ||||
|           shtDataRequested = true; | ||||
|  | ||||
|           shtLastTimeUpdated = millis(); | ||||
|         } | ||||
|  | ||||
|         if (shtDataRequested) { | ||||
|           if (sht30TempHumidSensor->dataReady()) { | ||||
|             if (sht30TempHumidSensor->readData()) { | ||||
|               shtCurrentTemp = sht30TempHumidSensor->getTemperature(); | ||||
|               shtCurrentHumidity = sht30TempHumidSensor->getHumidity(); | ||||
|               shtReadDataSuccess = true; | ||||
|             } | ||||
|             else { | ||||
|               shtReadDataSuccess = false; | ||||
|             } | ||||
|  | ||||
|             shtDataRequested = false; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (isOledReady() && millis() - oledLogoDrawn > 3000) { | ||||
|         // Check for changes on the current page and update the OLED if a change is detected | ||||
|         if (millis() - oledLastTimeUpdated > 150) { | ||||
|           // If there was a network change, force page 3 (network page) | ||||
|           if (oledCheckForNetworkChanges()) { | ||||
|             oledCurrentPage = 3; | ||||
|           } | ||||
|           // Only redraw a page if there was a change for that page | ||||
|           switch (oledCurrentPage) { | ||||
|             case 1: | ||||
|               lastKnownBri = bri; | ||||
|               // Probably causes lag to always do ledcRead(), so rather re-do the math, 'cause we can't easily get it... | ||||
|               getCurrentLedcValues(); | ||||
|  | ||||
|               if (bri != lastKnownBri || lastKnownLedcReads[0] != currentLedcReads[0] || lastKnownLedcReads[1] != currentLedcReads[1] || lastKnownLedcReads[2] != currentLedcReads[2] | ||||
|                   || lastKnownLedcReads[3] != currentLedcReads[3] || lastKnownLedcReads[4] != currentLedcReads[4]) { | ||||
|                 lastKnownLedcReads[0] = currentLedcReads[0]; lastKnownLedcReads[1] = currentLedcReads[1]; lastKnownLedcReads[2] = currentLedcReads[2]; lastKnownLedcReads[3] = currentLedcReads[3]; lastKnownLedcReads[4] = currentLedcReads[4]; | ||||
|  | ||||
|                 oledShowPage(1); | ||||
|               } | ||||
|               break; | ||||
|  | ||||
|             case 2: | ||||
|               if (shtLastKnownTemp != shtCurrentTemp || shtLastKnownHumidity != shtCurrentHumidity) { | ||||
|                 shtLastKnownTemp = shtCurrentTemp; | ||||
|                 shtLastKnownHumidity = shtCurrentHumidity; | ||||
|  | ||||
|                 oledShowPage(2); | ||||
|               } | ||||
|               break; | ||||
|  | ||||
|             case 3: | ||||
|               if (networkHasChanged) { | ||||
|                 networkHasChanged = false; | ||||
|  | ||||
|                 oledShowPage(3, true); | ||||
|               } | ||||
|               break; | ||||
|           } | ||||
|         } | ||||
|         // Cycle through OLED pages | ||||
|         if (millis() - oledLastTimePageChange > oledSecondsPerPage * 1000) { | ||||
|           // Periodically fixing a "bugged out" OLED. More details in the ReadMe | ||||
|           if (oledFixBuggedScreen && millis() - oledLastTimeFixBuggedScreen > 60000) { | ||||
|             oledDisplay->begin(); | ||||
|             oledLastTimeFixBuggedScreen = millis(); | ||||
|           } | ||||
|           oledShowPage(oledGetNextPage(), true); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void addToConfig(JsonObject &root) | ||||
|     { | ||||
|       JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname | ||||
|  | ||||
|       top[FPSTR(_enabled)] = enabled; | ||||
|       top[FPSTR(_oledEnabled)] = oledEnabled; | ||||
|       top[FPSTR(_oledUseProgressBars)] = oledUseProgressBars; | ||||
|       top[FPSTR(_oledFlipScreen)] = oledFlipScreen; | ||||
|       top[FPSTR(_oledSecondsPerPage)] = oledSecondsPerPage; | ||||
|       top[FPSTR(_oledFixBuggedScreen)] = oledFixBuggedScreen; | ||||
|       top[FPSTR(_shtEnabled)] = shtEnabled; | ||||
|  | ||||
|       // Update LED pins on config save | ||||
|       getCurrentUsedLedPins(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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) | ||||
|     { | ||||
|       JsonObject top = root[FPSTR(_name)]; | ||||
|       if (top.isNull()) { | ||||
|         DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       bool oldEnabled = enabled; | ||||
|       bool oldOledEnabled = oledEnabled; | ||||
|       bool oldOledFlipScreen = oledFlipScreen; | ||||
|       bool oldShtEnabled = shtEnabled; | ||||
|  | ||||
|       getJsonValue(top[FPSTR(_enabled)], enabled); | ||||
|       getJsonValue(top[FPSTR(_oledEnabled)], oledEnabled); | ||||
|       getJsonValue(top[FPSTR(_oledUseProgressBars)], oledUseProgressBars); | ||||
|       getJsonValue(top[FPSTR(_oledFlipScreen)], oledFlipScreen); | ||||
|       getJsonValue(top[FPSTR(_oledSecondsPerPage)], oledSecondsPerPage); | ||||
|       getJsonValue(top[FPSTR(_oledFixBuggedScreen)], oledFixBuggedScreen); | ||||
|       getJsonValue(top[FPSTR(_shtEnabled)], shtEnabled); | ||||
|  | ||||
|       // First run: reading from cfg.json, nothing to do here, will be all done in setup() | ||||
|       if (!firstRunDone) { | ||||
|         DEBUG_PRINTF("[%s] First run, nothing to do\n", _name); | ||||
|       } | ||||
|       // Check if mod has been en-/disabled | ||||
|       else if (enabled != oldEnabled) { | ||||
|         enabled ? setup() : cleanup(); | ||||
|         DEBUG_PRINTF("[%s] Usermod has been en-/disabled\n", _name); | ||||
|       } | ||||
|       // Config has been changed, so adopt to changes | ||||
|       else if (enabled) { | ||||
|         if (oldOledEnabled != oledEnabled) { | ||||
|           oledEnabled ? initOledDisplay() : cleanupOledDisplay(); | ||||
|         } | ||||
|         else if (oledEnabled && oldOledFlipScreen != oledFlipScreen) { | ||||
|           oledDisplay->clear(); | ||||
|           oledDisplay->setFlipMode(oledFlipScreen); | ||||
|           oledShowPage(oledCurrentPage); | ||||
|         } | ||||
|  | ||||
|         if (oldShtEnabled != shtEnabled) { | ||||
|           shtEnabled ? initSht30TempHumiditySensor() : cleanupSht30TempHumiditySensor(); | ||||
|         } | ||||
|  | ||||
|         DEBUG_PRINTF("[%s] Config (re)loaded\n", _name); | ||||
|       } | ||||
|  | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     void addToJsonInfo(JsonObject& root) | ||||
|     { | ||||
|       if (!enabled && !isShtReady()) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       JsonObject user = root["u"]; | ||||
|       if (user.isNull()) user = root.createNestedObject("u"); | ||||
|  | ||||
|       JsonArray jsonTemp = user.createNestedArray("Temperature"); | ||||
|       JsonArray jsonHumidity = user.createNestedArray("Humidity"); | ||||
|  | ||||
|       if (shtLastTimeUpdated == 0 || !shtReadDataSuccess) { | ||||
|         jsonTemp.add(0); | ||||
|         jsonHumidity.add(0); | ||||
|         if (shtLastTimeUpdated == 0) { | ||||
|           jsonTemp.add(" Not read yet"); | ||||
|           jsonHumidity.add(" Not read yet"); | ||||
|         } | ||||
|         else { | ||||
|           jsonTemp.add(" Error"); | ||||
|           jsonHumidity.add(" Error"); | ||||
|         } | ||||
|  | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       jsonHumidity.add(shtCurrentHumidity); | ||||
|       jsonHumidity.add(" RH"); | ||||
|  | ||||
|       jsonTemp.add(shtCurrentTemp); | ||||
|       jsonTemp.add(" °C"); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|       * 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_QUINLED_AN_PENTA; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| // Config settings | ||||
| const char QuinLEDAnPentaUsermod::_name[]                PROGMEM = "QuinLED-An-Penta"; | ||||
| const char QuinLEDAnPentaUsermod::_enabled[]             PROGMEM = "Enabled"; | ||||
| const char QuinLEDAnPentaUsermod::_oledEnabled[]         PROGMEM = "Enable-OLED"; | ||||
| const char QuinLEDAnPentaUsermod::_oledUseProgressBars[] PROGMEM = "OLED-Use-Progress-Bars"; | ||||
| const char QuinLEDAnPentaUsermod::_oledFlipScreen[]      PROGMEM = "OLED-Flip-Screen-180"; | ||||
| const char QuinLEDAnPentaUsermod::_oledSecondsPerPage[]  PROGMEM = "OLED-Seconds-Per-Page"; | ||||
| const char QuinLEDAnPentaUsermod::_oledFixBuggedScreen[] PROGMEM = "OLED-Fix-Bugged-Screen"; | ||||
| const char QuinLEDAnPentaUsermod::_shtEnabled[]          PROGMEM = "Enable-SHT30-Temp-Humidity-Sensor"; | ||||
| // Other strings | ||||
|  | ||||
| const unsigned char QuinLEDAnPentaUsermod::quinLedLogo[] PROGMEM = { | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFD, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x80, 0xFF, | ||||
|   0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0x3F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x07, 0xFE, 0xFF, 0xFF, 0x0F, 0xFC, | ||||
|   0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFC, 0x0F, 0xFE, | ||||
|   0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xE3, 0xFF, 0xA5, 0xFF, 0xFF, 0xFF, | ||||
|   0x0F, 0xFC, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xE1, 0xFF, | ||||
|   0x00, 0xF0, 0xE3, 0xFF, 0x0F, 0xFE, 0x1F, 0xFE, 0xFF, 0xFF, 0x3F, 0xFF, | ||||
|   0xFF, 0xFF, 0xE3, 0xFF, 0x00, 0xF0, 0x00, 0xFF, 0x07, 0xFE, 0x1F, 0xFC, | ||||
|   0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0xFF, 0x00, 0xF0, 0x00, 0xFE, | ||||
|   0x07, 0xFF, 0x1F, 0xFC, 0xF0, 0xC7, 0x3F, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, | ||||
|   0xF1, 0xFF, 0x00, 0xFC, 0x07, 0xFF, 0x1F, 0xFE, 0xF0, 0xC3, 0x1F, 0xFE, | ||||
|   0x00, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0x30, 0xF8, 0x07, 0xFF, 0x1F, 0xFE, | ||||
|   0xF0, 0xC3, 0x1F, 0xFE, 0x00, 0xFC, 0xC3, 0xFF, 0xE1, 0xFF, 0xF0, 0xF0, | ||||
|   0x03, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E, 0x00, 0xF8, 0xE3, 0xFF, | ||||
|   0xE1, 0xFF, 0xF1, 0xF1, 0x83, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E, | ||||
|   0x00, 0xF0, 0xC3, 0xFF, 0xE1, 0xFF, 0xF1, 0xE1, 0x83, 0xFF, 0x0F, 0xFE, | ||||
|   0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0xA1, 0xFF, 0xF1, 0xE3, | ||||
|   0x81, 0xFF, 0x0F, 0x7E, 0xF0, 0xC1, 0x1F, 0x7E, 0xF0, 0xF0, 0xC3, 0xFF, | ||||
|   0x01, 0xF8, 0xE1, 0xC3, 0x83, 0xFF, 0x0F, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E, | ||||
|   0xF8, 0xF0, 0xC3, 0xFF, 0x03, 0xF8, 0xE1, 0xC7, 0x81, 0xE4, 0x0F, 0x7F, | ||||
|   0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0x01, 0xF8, 0xE3, 0xC7, | ||||
|   0x01, 0xC0, 0x07, 0x7F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xE1, 0xC3, 0xFF, | ||||
|   0xC3, 0xFD, 0xE1, 0x87, 0x01, 0x00, 0x07, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E, | ||||
|   0xF8, 0xF0, 0xC3, 0xFF, 0xE3, 0xFF, 0xE3, 0x87, 0x01, 0x00, 0x82, 0x3F, | ||||
|   0xF8, 0xE1, 0x1F, 0xFE, 0xF8, 0xE1, 0xC3, 0xFF, 0xC3, 0xFF, 0xC3, 0x87, | ||||
|   0x01, 0x00, 0x80, 0x3F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xF1, 0xC3, 0xFF, | ||||
|   0xC3, 0xFF, 0xC3, 0x87, 0x03, 0x0F, 0x80, 0x3F, 0xF8, 0xE1, 0x0F, 0x7E, | ||||
|   0xF8, 0xE1, 0x87, 0xFF, 0xC3, 0xFF, 0xC7, 0x87, 0x03, 0x04, 0xC0, 0x7F, | ||||
|   0xF0, 0xE1, 0x0F, 0xFF, 0xF8, 0xF1, 0x87, 0xFF, 0xC3, 0xFF, 0xC3, 0x87, | ||||
|   0x07, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE0, 0xC3, 0xFF, | ||||
|   0xC7, 0xFF, 0x87, 0x87, 0x0F, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x0F, 0x7F, | ||||
|   0xF8, 0xE1, 0x07, 0x80, 0x07, 0xEA, 0x87, 0xC1, 0x0F, 0x00, 0x80, 0xFF, | ||||
|   0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE1, 0x07, 0x00, 0x03, 0x80, 0x07, 0xC0, | ||||
|   0x7F, 0x00, 0x00, 0xFF, 0x01, 0xE0, 0x1F, 0xFF, 0xF8, 0xE1, 0x07, 0x00, | ||||
|   0x07, 0x00, 0x07, 0xE0, 0xFF, 0xF7, 0x01, 0xFF, 0x57, 0xF7, 0x9F, 0xFF, | ||||
|   0xFC, 0xF1, 0x0F, 0x00, 0x07, 0x80, 0x0F, 0xE0, 0xFF, 0xFF, 0x03, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xBF, 0xFE, | ||||
|   0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
|   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||||
| }; | ||||
							
								
								
									
										69
									
								
								usermods/quinled-an-penta/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,69 @@ | ||||
| # QuinLED-An-Penta | ||||
| The (un)official usermod to get the best out of the QuinLED-An-Penta, like using the OLED and the SHT30 temperature/humidity sensor. | ||||
|  | ||||
| ## Requirements | ||||
| * "u8gs" 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 | ||||
| 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 QUINLED_AN_PENTA`. | ||||
|  | ||||
| ESP32 (**without** ethernet): | ||||
| ``` | ||||
| [env:custom_esp32dev_usermod_quinled_an_penta] | ||||
| extends = env:esp32dev | ||||
| build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 -D QUINLED_AN_PENTA | ||||
| lib_deps = ${esp32.lib_deps} | ||||
|     olikraus/U8g2@~2.28.8 | ||||
|     robtillaart/SHT85@~0.2.0 | ||||
| ``` | ||||
|  | ||||
| ESP32 (**with** ethernet): | ||||
| ``` | ||||
| [env:custom_esp32dev_usermod_quinled_an_penta] | ||||
| extends = env:esp32dev | ||||
| build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D WLED_USE_ETHERNET -D QUINLED_AN_PENTA | ||||
| lib_deps = ${esp32.lib_deps} | ||||
|     olikraus/U8g2@~2.28.8 | ||||
|     robtillaart/SHT85@~0.2.0 | ||||
| ``` | ||||
|  | ||||
| ## Some words about the (optional) OLED | ||||
| This mod has been optimized for an SSD1306 driven 128x64 OLED. Using a smaller OLED or an OLED using a different driver will result in unexpected results. | ||||
| I highly recommend using these "two color monochromatic OLEDs", which have the first 16 pixels in a different color than the other 48, e.g. a yellow/blue OLED. | ||||
| Also note, you need to have an **SPI** driven OLED, **not i2c**! | ||||
|  | ||||
| ### My OLED flickers after some time, what should I do? | ||||
| That's a tricky one: During development I saw that the OLED sometimes starts to "bug out" / flicker and won't work anymore. This seems to be caused by the high PWM interference the board produces. It seems to loose it's settings and then doesn't know how to draw anymore. Turns out the only way to fix this is to call the libraries `begin()` method again which will re-initialize the display. | ||||
| If you're facing this issue, you can enable a setting I've added which will call the `begin()` roughly every 60 seconds between a page change. This will make the page change take ~500ms, but will fix the display. | ||||
|  | ||||
| ## Configuration | ||||
| Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D QUINLED_AN_PENTA`, you will see the config for it there: | ||||
| * Enable-OLED: | ||||
|   * What it does: Enabled the optional SPI driven OLED that can be mounted to the 7-pin female header | ||||
|   * Possible values: Enabled/Disabled | ||||
|   * Default: Disabled | ||||
| * OLED-Use-Progress-Bars: | ||||
|   * What it does: Toggle between showing percentage numbers or a progress-bar-like visualization for overall brightness and each LED channels brightness level | ||||
|   * Possible values: Enabled/Disabled | ||||
|   * Default: Disabled | ||||
| * OLED-Flip-Screen-180: | ||||
|   * What it does: Flips the screen 180° / upside-down | ||||
|   * Possible values: Enabled/Disabled | ||||
|   * Default: Disabled | ||||
| * OLED-Seconds-Per-Page: | ||||
|   * What it does: Defines how long the OLED should stay on one page in seconds before changing to the next | ||||
|   * Possible values: Enabled/Disabled | ||||
|   * Default: 10 | ||||
| * OLED-Fix-Bugged-Screen: | ||||
|   * What it does: Enable this if your OLED flickers after some time. For more info read above under ["My OLED flickers after some time, what should I do?"](#My-OLED-flickers-after-some-time-what-should-I-do) | ||||
|   * Possible values: Enabled/Disabled | ||||
|   * Default: Disabled | ||||
| * Enable-SHT30-Temp-Humidity-Sensor: | ||||
|   * What it does: Enabled the onboard SHT30 temperature and humidity sensor | ||||
|   * Possible values: Enabled/Disabled | ||||
|   * Default: Disabled | ||||
|  | ||||
| ## Change log | ||||
| 2021-10 | ||||
| * First implementation. | ||||
							
								
								
									
										86
									
								
								usermods/rgb-rotary-encoder/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,86 @@ | ||||
| # RGB Encoder Board | ||||
|  | ||||
| This usermod-v2 adds support for the awesome RGB Rotary Encoder Board by Adam Zeloof / "Isotope Engineering" to control the overall brightness of your WLED instance: https://github.com/isotope-engineering/RGB-Encoder-Board. A great DIY rotary encoder with 20 tiny SK6805 / "NeoPixel Nano" LEDs. | ||||
|  | ||||
| https://user-images.githubusercontent.com/3090131/124680599-0180ab80-dec7-11eb-9065-a6d08ebe0287.mp4 | ||||
|  | ||||
| ## Credits | ||||
| The actual / original code that does the different LED modes is from Adam Zeloof. So I don't take credit for these. But I ported it to WLED, which involved replacing the LED library he used (because, guess what, WLED already has one; so no need to add another one, but use whatever WLED uses), plus the rotary encoder library, because that one was not compatible with ESP, only Arduino. | ||||
| So it was quite 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 | ||||
|  | ||||
| ## 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`. | ||||
|  | ||||
| ESP32: | ||||
| ``` | ||||
| [env:custom_esp32dev_usermod_rgb_encoder_board] | ||||
| 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 | ||||
| ``` | ||||
|  | ||||
| ESP8266 / D1 Mini: | ||||
| ``` | ||||
| [env:custom_d1_mini_usermod_rgb_encoder_board] | ||||
| 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 | ||||
| ``` | ||||
|  | ||||
| ## How to connect the board to your ESP | ||||
| We gonna need (minimum) three or (maximum) four GPIOs for the board: | ||||
| * "ea": Basically tells if the encoder goes into one or the other direction | ||||
| * "eb": Same thing, but the other direction | ||||
| * "di": LED data in. To actually control the LEDs | ||||
| * *(optional)* "sw": The integrated switch in the rotary encoder. Can be omitted for the bare functionality of just controlling the brightness | ||||
|  | ||||
| We also gonna need some power, so: | ||||
|  | ||||
| * "vdd": Needs to be connected to **+5V**. | ||||
| * "gnd": Well, it's GND. | ||||
|  | ||||
| You can freely pick the GPIOs, it doesn't matter. Those will be configured in the "Usermods" section in the WLED web panel: | ||||
|  | ||||
| ## Configuration | ||||
| Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D RGB_ROTARY_ENCODER`, you will see the config for it there. The settings there are the GPIOs we mentioned before (*Note: The switch pin is not there, as this can just be configured the "normal" button on the "LED Preferences" page*), plus a few more: | ||||
| * LED pin: | ||||
|   * Possible values: Any valid and available GPIO | ||||
|   * Default: 3 | ||||
|   * What it does: Pin to control the LED ring | ||||
| * ea pin: | ||||
|   * Possible values: Any valid and available GPIO | ||||
|   * Default: 15 | ||||
|   * What it does: First of the two rotary encoder pins | ||||
| * eb pin: | ||||
|   * Possible values: Any valid and available GPIO | ||||
|   * Default: 32 | ||||
|   * What it does: Second of the two rotary encoder pins | ||||
| * LED Mode: | ||||
|   * Possible values: 1-3 | ||||
|   * Default: 3 | ||||
|   * What it does: The usermod provides three different modes of how the LEDs can look like. Here's an example: https://github.com/isotope-engineering/RGB-Encoder-Board/blob/master/images/rgb-encoder-animations.gif | ||||
|     * Up left is "1" | ||||
|     * Up right is not supported / doesn't make sense for brightness control | ||||
|     * Bottom left is "2" | ||||
|     * Bottom right is "3" | ||||
| * LED Brightness: | ||||
|   * Possible values: 1-255 | ||||
|   * Default: 64 | ||||
|   * What it does: Brightness of the LED ring | ||||
| * Steps per click: | ||||
|   * Possible values: Any positive number | ||||
|   * Default: 4 | ||||
|   * What it does: With each "click", a rotary encoder actually increments it's "steps". Most rotary encoder do four "steps" per "click". I know this sounds super weird, so just leave this the default value, unless your rotary encoder behaves weirdly, like with one click, it makes two LEDs light up, or you sometimes need two click for one LED. Then you should play around with this value or write a small sketch using the same "ESP Rotary" library and read out the steps it does. | ||||
| * Increment per click: | ||||
|   * Possible values: Any positive number | ||||
|   * Default: 5 | ||||
|   * What it does: Most rotary encoder have 20 "clicks", so basically 20 positions. This value should be set to 100 / `number of clicks` | ||||
|  | ||||
| ## Change log | ||||
| 2021-07 | ||||
| * First implementation. | ||||
							
								
								
									
										343
									
								
								usermods/rgb-rotary-encoder/rgb-rotary-encoder.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,343 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "ESPRotary.h" | ||||
| #include <math.h> | ||||
| #include "wled.h" | ||||
|  | ||||
| class RgbRotaryEncoderUsermod : public Usermod | ||||
| { | ||||
|   private: | ||||
|     bool enabled = false; | ||||
|     bool initDone = false; | ||||
|     bool isDirty = false; | ||||
|     BusDigital *ledBus; | ||||
|     /* | ||||
|     * Green - eb - Q4 - 32 | ||||
|     * Red   - ea - Q1 - 15 | ||||
|     * Black - sw - Q2 - 12 | ||||
|     */ | ||||
|     ESPRotary *rotaryEncoder; | ||||
|     int8_t ledIo = 3; // GPIO to control the LEDs | ||||
|     int8_t eaIo = 15; // "ea" from RGB Encoder Board | ||||
|     int8_t ebIo = 32; // "eb" from RGB Encoder Board | ||||
|     byte stepsPerClick = 4; // How many "steps" your rotary encoder does per click. This varies per rotary encoder | ||||
|     /* This could vary per rotary encoder: Usually rotary encoders have 20 "clicks". | ||||
|       If yours has less/more, adjust this to: 100% = 20 LEDs * incrementPerClick */ | ||||
|     byte incrementPerClick = 5; | ||||
|     byte ledMode = 3; | ||||
|     byte ledBrightness = 64; | ||||
|  | ||||
|     // This is all needed to calculate the brightness, rotary position, etc. | ||||
|     const byte minPos = 5; // minPos is not zero, because if we want to turn the LEDs off, we use the built-in button ;) | ||||
|     const byte maxPos = 100; // maxPos=100, like 100% | ||||
|     const byte numLeds = 20; | ||||
|     byte lastKnownPos = 0; | ||||
|  | ||||
|     byte currentColors[3]; | ||||
|     byte lastKnownBri = 0; | ||||
|  | ||||
|  | ||||
|     void initRotaryEncoder() | ||||
|     { | ||||
|       PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } }; | ||||
|       if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { | ||||
|         eaIo = -1; | ||||
|         ebIo = -1; | ||||
|         cleanup(); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // I don't know why, but setting the upper bound here does not work. It results into 1717922932 O_o | ||||
|       rotaryEncoder = new ESPRotary(eaIo, ebIo, stepsPerClick, incrementPerClick, maxPos, currentPos, incrementPerClick); | ||||
|       rotaryEncoder->setUpperBound(maxPos); // I have to again set it here and then it works / is actually 100... | ||||
|  | ||||
|       rotaryEncoder->setChangedHandler(RgbRotaryEncoderUsermod::cbRotate); | ||||
|     } | ||||
|  | ||||
|     void initLedBus() | ||||
|     { | ||||
|       byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255}; | ||||
|       BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0); | ||||
|  | ||||
|       ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1); | ||||
|       if (!ledBus->isOk()) { | ||||
|         cleanup(); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       ledBus->setBrightness(ledBrightness); | ||||
|     } | ||||
|  | ||||
|     void updateLeds() | ||||
|     { | ||||
|       switch (ledMode) { | ||||
|         case 2: | ||||
|           { | ||||
|             currentColors[0] = 255; currentColors[1] = 0; currentColors[2] = 0; | ||||
|             for (int i = 0; i < currentPos / incrementPerClick - 1; i++) { | ||||
|               ledBus->setPixelColor(i, 0); | ||||
|             } | ||||
|             ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors)); | ||||
|             for (int i = currentPos / incrementPerClick; i < numLeds; i++) { | ||||
|               ledBus->setPixelColor(i, 0); | ||||
|             } | ||||
|           } | ||||
|           break; | ||||
|  | ||||
|         default: | ||||
|         case 1: | ||||
|         case 3: | ||||
|           // WLED orange (of course), which we will use in mode 1 | ||||
|           currentColors[0] = 255; currentColors[1] = 160; currentColors[2] = 0; | ||||
|           for (int i = 0; i < currentPos / incrementPerClick; i++) { | ||||
|             if (ledMode == 3) { | ||||
|               hsv2rgb((i) / float(numLeds), 1, .25); | ||||
|             } | ||||
|             ledBus->setPixelColor(i, colorFromRgbw(currentColors)); | ||||
|           } | ||||
|           for (int i = currentPos / incrementPerClick; i < numLeds; i++) { | ||||
|             ledBus->setPixelColor(i, 0); | ||||
|           } | ||||
|           break; | ||||
|       } | ||||
|  | ||||
|       isDirty = true; | ||||
|     } | ||||
|  | ||||
|     void cleanup() | ||||
|     { | ||||
|       // Only deallocate pins if we allocated them ;) | ||||
|       if (eaIo != -1) { | ||||
|         pinManager.deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); | ||||
|         eaIo = -1; | ||||
|       } | ||||
|       if (ebIo != -1) { | ||||
|         pinManager.deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); | ||||
|         ebIo = -1; | ||||
|       } | ||||
|  | ||||
|       delete rotaryEncoder; | ||||
|       delete ledBus; | ||||
|  | ||||
|       enabled = false; | ||||
|     } | ||||
|  | ||||
|     int getPositionForBrightness() | ||||
|     { | ||||
|       return int(((float)bri / (float)255) * 100); | ||||
|     } | ||||
|  | ||||
|     float fract(float x) { return x - int(x); } | ||||
|  | ||||
|     float mix(float a, float b, float t) { return a + (b - a) * t; } | ||||
|  | ||||
|     void hsv2rgb(float h, float s, float v) { | ||||
|       currentColors[0] = int((v * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); | ||||
|       currentColors[1] = int((v * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); | ||||
|       currentColors[2] = int((v * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); | ||||
|     } | ||||
|  | ||||
|   public: | ||||
|     static byte currentPos; | ||||
|  | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _enabled[]; | ||||
|     static const char _ledIo[]; | ||||
|     static const char _eaIo[]; | ||||
|     static const char _ebIo[]; | ||||
|     static const char _ledMode[]; | ||||
|     static const char _ledBrightness[]; | ||||
|     static const char _stepsPerClick[]; | ||||
|     static const char _incrementPerClick[]; | ||||
|  | ||||
|  | ||||
|     static void cbRotate(ESPRotary& r) { | ||||
|       currentPos = r.getPosition(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Enable/Disable the usermod | ||||
|      */ | ||||
|     // inline void enable(bool enable) { enabled = enable; } | ||||
|     /** | ||||
|      * Get usermod enabled/disabled state | ||||
|      */ | ||||
|     // inline bool isEnabled() { return enabled; } | ||||
|  | ||||
|     /* | ||||
|       * 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() | ||||
|     { | ||||
|       if (enabled) { | ||||
|         currentPos = getPositionForBrightness(); | ||||
|         lastKnownBri = bri; | ||||
|  | ||||
|         initRotaryEncoder(); | ||||
|         initLedBus(); | ||||
|  | ||||
|         // No updating of LEDs here, as that's sometimes not working; loop() will take care of that | ||||
|  | ||||
|         initDone = true; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|       * 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 || strip.isUpdating()) return; | ||||
|  | ||||
|       rotaryEncoder->loop(); | ||||
|  | ||||
|       // If the rotary was changed | ||||
|       if(lastKnownPos != currentPos) { | ||||
|         lastKnownPos = currentPos; | ||||
|  | ||||
|         bri = min(int(round((2.55 * currentPos))), 255); | ||||
|         lastKnownBri = bri; | ||||
|  | ||||
|         updateLeds(); | ||||
|         colorUpdated(CALL_MODE_DIRECT_CHANGE); | ||||
|       } | ||||
|  | ||||
|       // If the brightness is changed not with the rotary, update the rotary | ||||
|       if (bri != lastKnownBri) { | ||||
|         currentPos = lastKnownPos = getPositionForBrightness(); | ||||
|         lastKnownBri = bri; | ||||
|         rotaryEncoder->resetPosition(currentPos); | ||||
|         updateLeds(); | ||||
|       } | ||||
|  | ||||
|       // Update LEDs here in loop to also validate that we can update/show | ||||
|       if (isDirty && ledBus->canShow()) { | ||||
|         isDirty = false; | ||||
|         ledBus->show(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void addToConfig(JsonObject &root) | ||||
|     { | ||||
|       JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname | ||||
|  | ||||
|       top[FPSTR(_enabled)] = enabled; | ||||
|       top[FPSTR(_ledIo)] = ledIo; | ||||
|       top[FPSTR(_eaIo)] = eaIo; | ||||
|       top[FPSTR(_ebIo)] = ebIo; | ||||
|       top[FPSTR(_ledMode)] = ledMode; | ||||
|       top[FPSTR(_ledBrightness)] = ledBrightness; | ||||
|       top[FPSTR(_stepsPerClick)] = stepsPerClick; | ||||
|       top[FPSTR(_incrementPerClick)] = incrementPerClick; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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) | ||||
|     { | ||||
|       JsonObject top = root[FPSTR(_name)]; | ||||
|       if (top.isNull()) { | ||||
|         DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       bool oldEnabled = enabled; | ||||
|       int8_t oldLedIo = ledIo; | ||||
|       int8_t oldEaIo =  eaIo; | ||||
|       int8_t oldEbIo =  ebIo; | ||||
|       byte oldLedMode = ledMode; | ||||
|       byte oldStepsPerClick = stepsPerClick; | ||||
|       byte oldIncrementPerClick = incrementPerClick; | ||||
|       byte oldLedBrightness = ledBrightness; | ||||
|  | ||||
|       getJsonValue(top[FPSTR(_enabled)], enabled); | ||||
|       getJsonValue(top[FPSTR(_ledIo)], ledIo); | ||||
|       getJsonValue(top[FPSTR(_eaIo)], eaIo); | ||||
|       getJsonValue(top[FPSTR(_ebIo)], ebIo); | ||||
|       getJsonValue(top[FPSTR(_stepsPerClick)], stepsPerClick); | ||||
|       getJsonValue(top[FPSTR(_incrementPerClick)], incrementPerClick); | ||||
|       ledMode = top[FPSTR(_ledMode)] > 0 && top[FPSTR(_ledMode)] < 4 ? top[FPSTR(_ledMode)] : ledMode; | ||||
|       ledBrightness = top[FPSTR(_ledBrightness)] > 0 && top[FPSTR(_ledBrightness)] <= 255 ? top[FPSTR(_ledBrightness)] : ledBrightness; | ||||
|  | ||||
|       if (!initDone) { | ||||
|         // First run: reading from cfg.json | ||||
|         // Nothing to do here, will be all done in setup()  | ||||
|       } | ||||
|       // Mod was disabled, so run setup() | ||||
|       else if (enabled && enabled != oldEnabled) { | ||||
|         DEBUG_PRINTF("[%s] Usermod has been re-enabled\n", _name); | ||||
|         setup(); | ||||
|       } | ||||
|       // Config has been changed, so adopt to changes | ||||
|       else { | ||||
|         if (!enabled) { | ||||
|           DEBUG_PRINTF("[%s] Usermod has been disabled\n", _name); | ||||
|           cleanup(); | ||||
|         } | ||||
|         else { | ||||
|           DEBUG_PRINTF("[%s] Usermod is enabled\n", _name); | ||||
|           if (ledIo != oldLedIo) { | ||||
|             delete ledBus; | ||||
|             initLedBus(); | ||||
|           } | ||||
|  | ||||
|           if (ledBrightness != oldLedBrightness) { | ||||
|             ledBus->setBrightness(ledBrightness); | ||||
|             isDirty = true; | ||||
|           } | ||||
|  | ||||
|           if (ledMode != oldLedMode) { | ||||
|             updateLeds(); | ||||
|           } | ||||
|  | ||||
|           if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) { | ||||
|             pinManager.deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); | ||||
|             pinManager.deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); | ||||
|              | ||||
|             delete rotaryEncoder; | ||||
|             initRotaryEncoder(); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         DEBUG_PRINTF("[%s] Config (re)loaded\n", _name); | ||||
|       } | ||||
|        | ||||
|       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_RGB_ROTARY_ENCODER; | ||||
|     } | ||||
|  | ||||
|     //More methods can be added in the future, this example will then be extended. | ||||
|     //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! | ||||
| }; | ||||
|  | ||||
| byte RgbRotaryEncoderUsermod::currentPos = 5; | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char RgbRotaryEncoderUsermod::_name[]              PROGMEM = "RGB-Rotary-Encoder"; | ||||
| const char RgbRotaryEncoderUsermod::_enabled[]           PROGMEM = "Enabled"; | ||||
| const char RgbRotaryEncoderUsermod::_ledIo[]             PROGMEM = "LED-pin"; | ||||
| const char RgbRotaryEncoderUsermod::_eaIo[]              PROGMEM = "ea-pin"; | ||||
| const char RgbRotaryEncoderUsermod::_ebIo[]              PROGMEM = "eb-pin"; | ||||
| const char RgbRotaryEncoderUsermod::_ledMode[]           PROGMEM = "LED-Mode"; | ||||
| const char RgbRotaryEncoderUsermod::_ledBrightness[]     PROGMEM = "LED-Brightness"; | ||||
| const char RgbRotaryEncoderUsermod::_stepsPerClick[]     PROGMEM = "Steps-per-Click"; | ||||
| const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click"; | ||||
| @@ -1,62 +0,0 @@ | ||||
| #include "wled.h" | ||||
| /* | ||||
|  * 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 | ||||
|  */ | ||||
|  | ||||
| //Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) | ||||
|  | ||||
| /* | ||||
| ** Rotary Encoder Example | ||||
| ** Use the Sparkfun Rotary Encoder to vary brightness of LED | ||||
| ** | ||||
| ** Sample the encoder at 500Hz using the millis() function | ||||
| */ | ||||
|  | ||||
| int fadeAmount = 5;  // how many points to fade the Neopixel with each step | ||||
| unsigned long currentTime; | ||||
| unsigned long loopTime; | ||||
| const int pinA = D6;  // DT from encoder | ||||
| const int pinB = D7;  // CLK from encoder | ||||
|  | ||||
| unsigned char Enc_A; | ||||
| unsigned char Enc_B; | ||||
| unsigned char Enc_A_prev = 0; | ||||
|  | ||||
| //gets called once at boot. Do all initialization that doesn't depend on network here | ||||
| void userSetup() { | ||||
|   pinMode(pinA, INPUT_PULLUP); | ||||
|   pinMode(pinB, INPUT_PULLUP); | ||||
|   currentTime = millis(); | ||||
|   loopTime = currentTime; | ||||
| } | ||||
|  | ||||
| //gets called every time WiFi is (re-)connected. Initialize own network interfaces here | ||||
| void userConnected() { | ||||
| } | ||||
|  | ||||
| //loop. You can use "if (WLED_CONNECTED)" to check for successful connection | ||||
| void userLoop() { | ||||
|   currentTime = millis();  // get the current elapsed time | ||||
|   if(currentTime >= (loopTime + 2))  // 2ms since last check of encoder = 500Hz  | ||||
|   { | ||||
|     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 | ||||
|         if(bri + fadeAmount <= 255) bri += fadeAmount;   // increase the brightness, dont go over 255 | ||||
|        | ||||
|       } else if (Enc_B == LOW) { // B is low so counter-clockwise | ||||
|         if(bri - fadeAmount >= 0) bri -= fadeAmount;   // decrease the brightness, dont go below 0           | ||||
|       }    | ||||
|     }    | ||||
|       Enc_A_prev = Enc_A;     // Store value of A for next time     | ||||
|       loopTime = currentTime;  // Updates loopTime | ||||
|      | ||||
|     //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 | ||||
|     colorUpdated(6); | ||||
|   } | ||||
| } | ||||
| @@ -1,211 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| //v2 usermod that allows to change brightness and color using a rotary encoder,  | ||||
| //change between modes by pressing a button (many encoder have one included) | ||||
| class RotaryEncoderSet : public Usermod | ||||
| { | ||||
| private: | ||||
|   //Private class members. You can declare variables and functions only accessible to your usermod here | ||||
|   unsigned long lastTime = 0; | ||||
|   /* | ||||
| ** Rotary Encoder Example | ||||
| ** Use the Sparkfun Rotary Encoder to vary brightness of LED | ||||
| ** | ||||
| ** Sample the encoder at 500Hz using the millis() function | ||||
| */ | ||||
|  | ||||
|   int fadeAmount = 5; // how many points to fade the Neopixel with each step | ||||
|   unsigned long currentTime; | ||||
|   unsigned long loopTime; | ||||
|   const int pinA = 5;             // DT from encoder | ||||
|   const int pinB = 18;            // CLK from encoder | ||||
|   const int pinC = 23;            // SW from encoder | ||||
|   unsigned char select_state = 0; // 0 = brightness 1 = color | ||||
|   unsigned char button_state = HIGH; | ||||
|   unsigned char prev_button_state = HIGH; | ||||
|   CRGB fastled_col; | ||||
|   CHSV prim_hsv; | ||||
|   int16_t new_val; | ||||
|  | ||||
|   unsigned char Enc_A; | ||||
|   unsigned char Enc_B; | ||||
|   unsigned char Enc_A_prev = 0; | ||||
|  | ||||
| public: | ||||
|   //Functions called by WLED | ||||
|  | ||||
|   /* | ||||
|      * 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() | ||||
|   { | ||||
|     //Serial.println("Hello from my usermod!"); | ||||
|     pinMode(pinA, INPUT_PULLUP); | ||||
|     pinMode(pinB, INPUT_PULLUP); | ||||
|     pinMode(pinC, INPUT_PULLUP); | ||||
|     currentTime = millis(); | ||||
|     loopTime = currentTime; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|      * 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() | ||||
|   { | ||||
|     currentTime = millis(); // get the current elapsed time | ||||
|  | ||||
|     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) | ||||
|         { | ||||
|           if (select_state == 1) | ||||
|           { | ||||
|             select_state = 0; | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             select_state = 1; | ||||
|           } | ||||
|           prev_button_state = button_state; | ||||
|         } | ||||
|         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 | ||||
|           if (select_state == 0) | ||||
|           { | ||||
|             if (bri + fadeAmount <= 255) | ||||
|               bri += fadeAmount; // increase the brightness, dont go over 255 | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             fastled_col.red = col[0]; | ||||
|             fastled_col.green = col[1]; | ||||
|             fastled_col.blue = col[2]; | ||||
|             prim_hsv = rgb2hsv_approximate(fastled_col); | ||||
|             new_val = (int16_t)prim_hsv.h + fadeAmount; | ||||
|             if (new_val > 255) | ||||
|               new_val -= 255; // roll-over if  bigger than 255 | ||||
|             if (new_val < 0) | ||||
|               new_val += 255; // roll-over if smaller than 0 | ||||
|             prim_hsv.h = (byte)new_val; | ||||
|             hsv2rgb_rainbow(prim_hsv, fastled_col); | ||||
|             col[0] = fastled_col.red; | ||||
|             col[1] = fastled_col.green; | ||||
|             col[2] = fastled_col.blue; | ||||
|           } | ||||
|         } | ||||
|         else if (Enc_B == LOW) | ||||
|         { // B is low so counter-clockwise | ||||
|           if (select_state == 0) | ||||
|           { | ||||
|             if (bri - fadeAmount >= 0) | ||||
|               bri -= fadeAmount; // decrease the brightness, dont go below 0 | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             fastled_col.red = col[0]; | ||||
|             fastled_col.green = col[1]; | ||||
|             fastled_col.blue = col[2]; | ||||
|             prim_hsv = rgb2hsv_approximate(fastled_col); | ||||
|             new_val = (int16_t)prim_hsv.h - fadeAmount; | ||||
|             if (new_val > 255) | ||||
|               new_val -= 255; // roll-over if  bigger than 255 | ||||
|             if (new_val < 0) | ||||
|               new_val += 255; // roll-over if smaller than 0 | ||||
|             prim_hsv.h = (byte)new_val; | ||||
|             hsv2rgb_rainbow(prim_hsv, fastled_col); | ||||
|             col[0] = fastled_col.red; | ||||
|             col[1] = fastled_col.green; | ||||
|             col[2] = fastled_col.blue; | ||||
|           } | ||||
|         } | ||||
|         //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 | ||||
|         colorUpdated(NOTIFIER_CALL_MODE_BUTTON); | ||||
|         updateInterfaces() | ||||
|       } | ||||
|       Enc_A_prev = Enc_A;     // Store value of A for next time | ||||
|       loopTime = currentTime; // Updates loopTime | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|      * 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!")); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|      * 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 0xABCD; | ||||
|   } | ||||
|  | ||||
|   //More methods can be added in the future, this example will then be extended. | ||||
|   //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! | ||||
| }; | ||||
| @@ -39,7 +39,7 @@ void userLoop() { | ||||
|  | ||||
|     //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 | ||||
|     colorUpdated(NOTIFIER_CALL_MODE_FX_CHANGED); | ||||
|     colorUpdated(CALL_MODE_FX_CHANGED); | ||||
|     lastTime = millis(); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										55
									
								
								usermods/seven_segment_display/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | ||||
| # Seven Segment Display | ||||
|  | ||||
| Usermod that uses the overlay feature to create a configurable seven segment display.   | ||||
| This has only been tested on a single configuration. Colon support is entirely  untested.  | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Add the compile-time option `-D USERMOD_SEVEN_SEGMENT` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SEVEN_SEGMENT` in `my_config.h`. | ||||
|  | ||||
| ## Settings | ||||
| Settings can be controlled through both the usermod setting page and through MQTT with a raw payload. | ||||
| ##### Example | ||||
|  Topic ```<mqttDeviceTopic||mqttGroupTopic>/sevenSeg/perSegment/set```   | ||||
|  Payload ```3``` | ||||
| #### perSegment -- ssLEDPerSegment | ||||
| The number of individual LEDs per segment. There are 7 segments per digit.   | ||||
| #### perPeriod -- ssLEDPerPeriod | ||||
| The number of individual LEDs per period. A ':' has 2x periods. | ||||
| #### startIdx -- ssStartLED | ||||
| Index of the LED that the display starts at. Allows 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 | ||||
| Time, in milliseconds, between message shifts when the length of displayMsg exceeds the length of the displayMask. | ||||
| #### displayMask -- ssDisplayMask | ||||
| This should represent the configuration of the physical display.  | ||||
| <pre> | ||||
| HH - 0-23. hh - 1-12, kk - 1-24 hours   | ||||
| MM or mm - 0-59 minutes   | ||||
| SS or ss = 0-59 seconds   | ||||
| : for a colon   | ||||
| All others for alpha numeric, (will be blank when displaying time) | ||||
| </pre> | ||||
| ##### Example | ||||
| ```HHMMSS ```   | ||||
| ```hh:MM:SS ``` | ||||
| #### displayMsg -- ssDisplayMessage | ||||
| Message to be displayed across the display. If the length exceeds the length of the displayMask the message will scroll at scrollSpd. To 'remove' a message or revert  back to time, if timeEnabled is true, set the message to '~'. | ||||
| #### displayCfg -- ssDisplayConfig | ||||
| The order that your LEDs are configured. All seven segments in the display need to be wired the same way. | ||||
| <pre> | ||||
|            ------- | ||||
|          /   A   /          0 - EDCGFAB | ||||
|         / F     / B         1 - EDCBAFG | ||||
|        /       /            2 - GCDEFAB | ||||
|        -------              3 - GBAFEDC | ||||
|      /   G   /              4 - FABGEDC | ||||
|     / E     / C             5 - FABCDEG | ||||
|    /       / | ||||
|    ------- | ||||
|       D | ||||
| </pre> | ||||
|  | ||||
| ## Version | ||||
| 20211009 - Initial release | ||||
| @@ -0,0 +1,497 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| class SevenSegmentDisplay : public Usermod | ||||
| { | ||||
|  | ||||
| #define WLED_SS_BUFFLEN 6 | ||||
| #define REFRESHTIME 497 | ||||
| private: | ||||
|   //Runtime variables. | ||||
|   unsigned long lastRefresh = 0; | ||||
|   unsigned long lastCharacterStep = 0; | ||||
|   String ssDisplayBuffer = ""; | ||||
|   char ssCharacterMask[36] = {0x77, 0x11, 0x6B, 0x3B, 0x1D, 0x3E, 0x7E, 0x13, 0x7F, 0x1F, 0x5F, 0x7C, 0x66, 0x79, 0x6E, 0x4E, 0x76, 0x5D, 0x44, 0x71, 0x5E, 0x64, 0x27, 0x58, 0x77, 0x4F, 0x1F, 0x48, 0x3E, 0x6C, 0x75, 0x25, 0x7D, 0x2A, 0x3D, 0x6B}; | ||||
|   int ssDisplayMessageIdx = 0; //Position of the start of the message to be physically displayed. | ||||
|   bool ssDoDisplayTime = true; | ||||
|   int ssVirtualDisplayMessageIdxStart = 0; | ||||
|   int ssVirtualDisplayMessageIdxEnd = 0; | ||||
|   unsigned long resfreshTime = 497; | ||||
|  | ||||
|   // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) | ||||
|   int ssLEDPerSegment = 1; //The number of LEDs in each segment of the 7 seg (total per digit is 7 * ssLedPerSegment) | ||||
|   int ssLEDPerPeriod = 1;  //A Period will have 1x and a Colon will have 2x | ||||
|   int ssStartLED = 0;      //The pixel that the display starts at. | ||||
|   /*  HH - 0-23. hh - 1-12, kk - 1-24 hours | ||||
|     //  MM or mm - 0-59 minutes | ||||
|     //  SS or ss = 0-59 seconds | ||||
|     //  : for a colon | ||||
|     //  All others for alpha numeric, (will be blank when displaying time) | ||||
|     */ | ||||
|   String ssDisplayMask = "HHMMSS"; //Physical Display Mask, this should reflect physical equipment. | ||||
|   /* ssDisplayConfig | ||||
|     //           ------- | ||||
|     //         /   A   /          0 - EDCGFAB | ||||
|     //        / F     / B         1 - EDCBAFG | ||||
|     //       /       /            2 - GCDEFAB | ||||
|     //       -------              3 - GBAFEDC | ||||
|     //     /   G   /              4 - FABGEDC | ||||
|     //    / E     / C             5 - FABCDEG | ||||
|     //   /       / | ||||
|     //   ------- | ||||
|     //      D | ||||
|     */ | ||||
|   int ssDisplayConfig = 5; //Physical configuration of the Seven segment display | ||||
|   String ssDisplayMessage = "~"; | ||||
|   bool ssTimeEnabled = true;         //If not, display message. | ||||
|   unsigned int ssScrollSpeed = 1000; //Time between advancement of extended message scrolling, in milliseconds. | ||||
|  | ||||
|   //String to reduce flash memory usage | ||||
|   static const char _str_perSegment[]; | ||||
|   static const char _str_perPeriod[]; | ||||
|   static const char _str_startIdx[]; | ||||
|   static const char _str_displayCfg[]; | ||||
|   static const char _str_timeEnabled[]; | ||||
|   static const char _str_scrollSpd[]; | ||||
|   static const char _str_displayMask[]; | ||||
|   static const char _str_displayMsg[]; | ||||
|   static const char _str_sevenSeg[]; | ||||
|   static const char _str_subFormat[]; | ||||
|   static const char _str_topicFormat[]; | ||||
|  | ||||
|   unsigned long _overlaySevenSegmentProcess() | ||||
|   { | ||||
|     //Do time for now. | ||||
|     if (ssDoDisplayTime) | ||||
|     { | ||||
|       //Format the ssDisplayBuffer based on ssDisplayMask | ||||
|       int displayMaskLen = static_cast<int>(ssDisplayMask.length()); | ||||
|       for (int index = 0; index < displayMaskLen; index++) | ||||
|       { | ||||
|         //Only look for time formatting if there are at least 2 characters left in the buffer. | ||||
|         if ((index < displayMaskLen - 1) && (ssDisplayMask[index] == ssDisplayMask[index + 1])) | ||||
|         { | ||||
|           int timeVar = 0; | ||||
|           switch (ssDisplayMask[index]) | ||||
|           { | ||||
|           case 'h': | ||||
|             timeVar = hourFormat12(localTime); | ||||
|             break; | ||||
|           case 'H': | ||||
|             timeVar = hour(localTime); | ||||
|             break; | ||||
|           case 'k': | ||||
|             timeVar = hour(localTime) + 1; | ||||
|             break; | ||||
|           case 'M': | ||||
|           case 'm': | ||||
|             timeVar = minute(localTime); | ||||
|             break; | ||||
|           case 'S': | ||||
|           case 's': | ||||
|             timeVar = second(localTime); | ||||
|             break; | ||||
|           } | ||||
|  | ||||
|           //Only want to leave a blank in the hour formatting. | ||||
|           if ((ssDisplayMask[index] == 'h' || ssDisplayMask[index] == 'H' || ssDisplayMask[index] == 'k') && timeVar < 10) | ||||
|             ssDisplayBuffer[index] = ' '; | ||||
|           else | ||||
|             ssDisplayBuffer[index] = 0x30 + (timeVar / 10); | ||||
|           ssDisplayBuffer[index + 1] = 0x30 + (timeVar % 10); | ||||
|  | ||||
|           //Need to increment the index because of the second digit. | ||||
|           index++; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|           ssDisplayBuffer[index] = (ssDisplayMask[index] == ':' ? ':' : ' '); | ||||
|         } | ||||
|       } | ||||
|       return REFRESHTIME; | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|       /* This will handle displaying a message and the scrolling of the message if its longer than the buffer length */ | ||||
|  | ||||
|       //Check to see if the message has scrolled completely | ||||
|       int len = static_cast<int>(ssDisplayMessage.length()); | ||||
|       if (ssDisplayMessageIdx > len) | ||||
|       { | ||||
|         //If it has scrolled the whole message, reset it. | ||||
|         setSevenSegmentMessage(ssDisplayMessage); | ||||
|         return REFRESHTIME; | ||||
|       } | ||||
|       //Display message | ||||
|       int displayMaskLen = static_cast<int>(ssDisplayMask.length()); | ||||
|       for (int index = 0; index < displayMaskLen; index++) | ||||
|       { | ||||
|         if (ssDisplayMessageIdx + index < len && ssDisplayMessageIdx + index >= 0) | ||||
|           ssDisplayBuffer[index] = ssDisplayMessage[ssDisplayMessageIdx + index]; | ||||
|         else | ||||
|           ssDisplayBuffer[index] = ' '; | ||||
|       } | ||||
|  | ||||
|       //Increase the displayed message index to progress it one character if the length exceeds the display length. | ||||
|       if (len > displayMaskLen) | ||||
|         ssDisplayMessageIdx++; | ||||
|  | ||||
|       return ssScrollSpeed; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _overlaySevenSegmentDraw() | ||||
|   { | ||||
|  | ||||
|     //Start pixels at ssStartLED, Use ssLEDPerSegment, ssLEDPerPeriod, ssDisplayBuffer | ||||
|     int indexLED = ssStartLED; | ||||
|     int displayMaskLen = static_cast<int>(ssDisplayMask.length()); | ||||
|     for (int indexBuffer = 0; indexBuffer < displayMaskLen; indexBuffer++) | ||||
|     { | ||||
|       if (ssDisplayBuffer[indexBuffer] == 0) | ||||
|         break; | ||||
|       else if (ssDisplayBuffer[indexBuffer] == '.') | ||||
|       { | ||||
|         //Won't ever turn off LED lights for a period. (or will we?) | ||||
|         indexLED += ssLEDPerPeriod; | ||||
|         continue; | ||||
|       } | ||||
|       else if (ssDisplayBuffer[indexBuffer] == ':') | ||||
|       { | ||||
|         //Turn off colon if odd second? | ||||
|         indexLED += ssLEDPerPeriod * 2; | ||||
|       } | ||||
|       else if (ssDisplayBuffer[indexBuffer] == ' ') | ||||
|       { | ||||
|         //Turn off all 7 segments. | ||||
|         _overlaySevenSegmentLEDOutput(0, indexLED); | ||||
|         indexLED += ssLEDPerSegment * 7; | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         //Turn off correct segments. | ||||
|         _overlaySevenSegmentLEDOutput(_overlaySevenSegmentGetCharMask(ssDisplayBuffer[indexBuffer]), indexLED); | ||||
|         indexLED += ssLEDPerSegment * 7; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _overlaySevenSegmentLEDOutput(char mask, int indexLED) | ||||
|   { | ||||
|     for (char index = 0; index < 7; index++) | ||||
|     { | ||||
|       if ((mask & (0x40 >> index)) != (0x40 >> index)) | ||||
|       { | ||||
|         for (int numPerSeg = 0; numPerSeg < ssLEDPerSegment; numPerSeg++) | ||||
|         { | ||||
|           strip.setPixelColor(indexLED + numPerSeg, 0x000000); | ||||
|         } | ||||
|       } | ||||
|       indexLED += ssLEDPerSegment; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   char _overlaySevenSegmentGetCharMask(char var) | ||||
|   { | ||||
|     if (var >= 0x30 && var <= 0x39) | ||||
|     { /*If its a number, shift to index 0.*/ | ||||
|       var -= 0x30; | ||||
|     } | ||||
|     else if (var >= 0x41 && var <= 0x5a) | ||||
|     { /*If its an Upper case, shift to index 0xA.*/ | ||||
|       var -= 0x37; | ||||
|     } | ||||
|     else if (var >= 0x61 && var <= 0x7A) | ||||
|     { /*If its a lower case, shift to index 0xA.*/ | ||||
|       var -= 0x57; | ||||
|     } | ||||
|     else | ||||
|     { /* Else unsupported, return 0; */ | ||||
|       return 0; | ||||
|     } | ||||
|     char mask = ssCharacterMask[static_cast<int>(var)]; | ||||
|     /* | ||||
|       0 - EDCGFAB | ||||
|       1 - EDCBAFG | ||||
|       2 - GCDEFAB | ||||
|       3 - GBAFEDC | ||||
|       4 - FABGEDC | ||||
|       5 - FABCDEG | ||||
|       */ | ||||
|     switch (ssDisplayConfig) | ||||
|     { | ||||
|     case 1: | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); | ||||
|       break; | ||||
|     case 2: | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); | ||||
|       break; | ||||
|     case 3: | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); | ||||
|       break; | ||||
|     case 4: | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); | ||||
|       break; | ||||
|     case 5: | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); | ||||
|       mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); | ||||
|       break; | ||||
|     } | ||||
|     return mask; | ||||
|   } | ||||
|  | ||||
|   char _overlaySevenSegmentSwapBits(char x, char p1, char p2, char n) | ||||
|   { | ||||
|     /* Move all bits of first set to rightmost side */ | ||||
|     char set1 = (x >> p1) & ((1U << n) - 1); | ||||
|  | ||||
|     /* Move all bits of second set to rightmost side */ | ||||
|     char set2 = (x >> p2) & ((1U << n) - 1); | ||||
|  | ||||
|     /* Xor the two sets */ | ||||
|     char Xor = (set1 ^ set2); | ||||
|  | ||||
|     /* Put the Xor bits back to their original positions */ | ||||
|     Xor = (Xor << p1) | (Xor << p2); | ||||
|  | ||||
|     /* Xor the 'Xor' with the original number so that the  | ||||
|         two sets are swapped */ | ||||
|     char result = x ^ Xor; | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   void _publishMQTTint_P(const char *subTopic, int value) | ||||
|   { | ||||
|     if(mqtt == NULL) return; | ||||
|        | ||||
|     char buffer[64]; | ||||
|     char valBuffer[12]; | ||||
|     sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); | ||||
|     sprintf_P(valBuffer, PSTR("%d"), value); | ||||
|     mqtt->publish(buffer, 2, true, valBuffer); | ||||
|   } | ||||
|  | ||||
|   void _publishMQTTstr_P(const char *subTopic, String Value) | ||||
|   { | ||||
|     if(mqtt == NULL) return; | ||||
|     char buffer[64]; | ||||
|     sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); | ||||
|     mqtt->publish(buffer, 2, true, Value.c_str(), Value.length()); | ||||
|   } | ||||
|  | ||||
|   void _updateMQTT() | ||||
|   { | ||||
|     _publishMQTTint_P(_str_perSegment, ssLEDPerSegment); | ||||
|     _publishMQTTint_P(_str_perPeriod, ssLEDPerPeriod); | ||||
|     _publishMQTTint_P(_str_startIdx, ssStartLED); | ||||
|     _publishMQTTint_P(_str_displayCfg, ssDisplayConfig); | ||||
|     _publishMQTTint_P(_str_timeEnabled, ssTimeEnabled); | ||||
|     _publishMQTTint_P(_str_scrollSpd, ssScrollSpeed); | ||||
|  | ||||
|     _publishMQTTstr_P(_str_displayMask, ssDisplayMask); | ||||
|     _publishMQTTstr_P(_str_displayMsg, ssDisplayMessage); | ||||
|   } | ||||
|  | ||||
|   bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value) | ||||
|   { | ||||
|     if (strcmp_P(topic, setting) == 0) | ||||
|     { | ||||
|       *((int *)value) = strtol(payload, NULL, 10); | ||||
|       _publishMQTTint_P(setting, *((int *)value)); | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   bool _handleSetting(char *topic, char *payload) | ||||
|   { | ||||
|     if (_cmpIntSetting_P(topic, payload, _str_perSegment, &ssLEDPerSegment)) | ||||
|       return true; | ||||
|     if (_cmpIntSetting_P(topic, payload, _str_perPeriod, &ssLEDPerPeriod)) | ||||
|       return true; | ||||
|     if (_cmpIntSetting_P(topic, payload, _str_startIdx, &ssStartLED)) | ||||
|       return true; | ||||
|     if (_cmpIntSetting_P(topic, payload, _str_displayCfg, &ssDisplayConfig)) | ||||
|       return true; | ||||
|     if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &ssTimeEnabled)) | ||||
|       return true; | ||||
|     if (_cmpIntSetting_P(topic, payload, _str_scrollSpd, &ssScrollSpeed)) | ||||
|       return true; | ||||
|     if (strcmp_P(topic, _str_displayMask) == 0) | ||||
|     { | ||||
|       ssDisplayMask = String(payload); | ||||
|       ssDisplayBuffer = ssDisplayMask; | ||||
|       _publishMQTTstr_P(_str_displayMask, ssDisplayMask); | ||||
|       return true; | ||||
|     } | ||||
|     if (strcmp_P(topic, _str_displayMsg) == 0) | ||||
|     { | ||||
|       setSevenSegmentMessage(String(payload)); | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
| public: | ||||
|   void setSevenSegmentMessage(String message) | ||||
|   { | ||||
|     //If the message isn't blank display it otherwise show time, if enabled. | ||||
|     if (message.length() < 1 || message == "~") | ||||
|       ssDoDisplayTime = ssTimeEnabled; | ||||
|     else | ||||
|       ssDoDisplayTime = false; | ||||
|  | ||||
|     //Determine is the message is longer than the display, if it is configure it to scroll the message. | ||||
|     if (message.length() > ssDisplayMask.length()) | ||||
|       ssDisplayMessageIdx = -ssDisplayMask.length(); | ||||
|     else | ||||
|       ssDisplayMessageIdx = 0; | ||||
|  | ||||
|     //If the message isn't the same, update runtime/mqtt (most calls will be resetting message scroll) | ||||
|     if (!ssDisplayMessage.equals(message)) | ||||
|     { | ||||
|       _publishMQTTstr_P(_str_displayMsg, message); | ||||
|       ssDisplayMessage = message; | ||||
|     } | ||||
|   } | ||||
|   //Functions called by WLED | ||||
|  | ||||
|   /* | ||||
|      * 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() | ||||
|   { | ||||
|     ssDisplayBuffer = ssDisplayMask; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|      * loop() is called continuously. Here you can check for events, read sensors, etc. | ||||
|      */ | ||||
|   void loop() | ||||
|   { | ||||
|     if (millis() - lastRefresh > resfreshTime) | ||||
|     { | ||||
|       //In theory overlaySevenSegmentProcess should return the amount of time until it changes next. | ||||
|       //So we should be okay to trigger the stripi on every process loop. | ||||
|       resfreshTime = _overlaySevenSegmentProcess(); | ||||
|       lastRefresh = millis(); | ||||
|       strip.trigger(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void handleOverlayDraw() | ||||
|   { | ||||
|     _overlaySevenSegmentDraw(); | ||||
|   } | ||||
|  | ||||
|   void onMqttConnect(bool sessionPresent) | ||||
|   { | ||||
|     char subBuffer[48]; | ||||
|     if (mqttDeviceTopic[0] != 0) | ||||
|     { | ||||
|       _updateMQTT(); | ||||
|       //subscribe for sevenseg messages on the device topic | ||||
|       sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttDeviceTopic, _str_sevenSeg); | ||||
|       mqtt->subscribe(subBuffer, 2); | ||||
|     } | ||||
|  | ||||
|     if (mqttGroupTopic[0] != 0) | ||||
|     { | ||||
|       //subcribe for sevenseg messages on the group topic | ||||
|       sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_sevenSeg); | ||||
|       mqtt->subscribe(subBuffer, 2); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool onMqttMessage(char *topic, char *payload) | ||||
|   { | ||||
|     //If topic beings iwth 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; | ||||
|     else | ||||
|       return false; | ||||
|     //We only care if the topic ends with /set | ||||
|     size_t topicLen = strlen(topic); | ||||
|     if (topicLen > 4 && | ||||
|         topic[topicLen - 4] == '/' && | ||||
|         topic[topicLen - 3] == 's' && | ||||
|         topic[topicLen - 2] == 'e' && | ||||
|         topic[topicLen - 1] == 't') | ||||
|     { | ||||
|       //Trim /set and handle it | ||||
|       topic[topicLen - 4] = '\0'; | ||||
|       _handleSetting(topic, payload); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   void addToConfig(JsonObject &root) | ||||
|   { | ||||
|     JsonObject top = root[FPSTR(_str_sevenSeg)]; | ||||
|     if (top.isNull()) | ||||
|     { | ||||
|       top = root.createNestedObject(FPSTR(_str_sevenSeg)); | ||||
|     } | ||||
|     top[FPSTR(_str_perSegment)] = ssLEDPerSegment; | ||||
|     top[FPSTR(_str_perPeriod)] = ssLEDPerPeriod; | ||||
|     top[FPSTR(_str_startIdx)] = ssStartLED; | ||||
|     top[FPSTR(_str_displayMask)] = ssDisplayMask; | ||||
|     top[FPSTR(_str_displayCfg)] = ssDisplayConfig; | ||||
|     top[FPSTR(_str_displayMsg)] = ssDisplayMessage; | ||||
|     top[FPSTR(_str_timeEnabled)] = ssTimeEnabled; | ||||
|     top[FPSTR(_str_scrollSpd)] = ssScrollSpeed; | ||||
|   } | ||||
|  | ||||
|   bool readFromConfig(JsonObject &root) | ||||
|   { | ||||
|     JsonObject top = root[FPSTR(_str_sevenSeg)]; | ||||
|  | ||||
|     bool configComplete = !top.isNull(); | ||||
|  | ||||
|     //if sevenseg section doesn't exist return | ||||
|     if (!configComplete) | ||||
|       return configComplete; | ||||
|  | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_perSegment)], ssLEDPerSegment); | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_perPeriod)], ssLEDPerPeriod); | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_startIdx)], ssStartLED); | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_displayMask)], ssDisplayMask); | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_displayCfg)], ssDisplayConfig); | ||||
|  | ||||
|     String newDisplayMessage; | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_displayMsg)], newDisplayMessage); | ||||
|     setSevenSegmentMessage(newDisplayMessage); | ||||
|  | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_timeEnabled)], ssTimeEnabled); | ||||
|     configComplete &= getJsonValue(top[FPSTR(_str_scrollSpd)], ssScrollSpeed); | ||||
|     return configComplete; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|      * 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_SEVEN_SEGMENT_DISPLAY; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const char SevenSegmentDisplay::_str_perSegment[] PROGMEM = "perSegment"; | ||||
| const char SevenSegmentDisplay::_str_perPeriod[] PROGMEM = "perPeriod"; | ||||
| const char SevenSegmentDisplay::_str_startIdx[] PROGMEM = "startIdx"; | ||||
| const char SevenSegmentDisplay::_str_displayCfg[] PROGMEM = "displayCfg"; | ||||
| const char SevenSegmentDisplay::_str_timeEnabled[] PROGMEM = "timeEnabled"; | ||||
| const char SevenSegmentDisplay::_str_scrollSpd[] PROGMEM = "scrollSpd"; | ||||
| const char SevenSegmentDisplay::_str_displayMask[] PROGMEM = "displayMask"; | ||||
| const char SevenSegmentDisplay::_str_displayMsg[] PROGMEM = "displayMsg"; | ||||
| const char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = "sevenSeg"; | ||||
| @@ -42,7 +42,7 @@ class StairwayWipeUsermod : public Usermod { | ||||
|       if (millis() + strip.timebase > (cycleTime - 25)) { //wipe complete | ||||
|         effectCurrent = FX_MODE_STATIC; | ||||
|         timeStaticStart = millis(); | ||||
|         colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|         colorUpdated(CALL_MODE_NOTIFICATION); | ||||
|         wipeState = 2; | ||||
|       } | ||||
|     } else if (wipeState == 2) { //static | ||||
| @@ -54,7 +54,7 @@ class StairwayWipeUsermod : public Usermod { | ||||
|       #ifdef STAIRCASE_WIPE_OFF | ||||
|       effectCurrent = FX_MODE_COLOR_WIPE; | ||||
|       strip.timebase = 360 + (255 - effectSpeed)*75 - millis(); //make sure wipe starts fully lit | ||||
|       colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|       colorUpdated(CALL_MODE_NOTIFICATION); | ||||
|       wipeState = 4; | ||||
|       #else | ||||
|       turnOff(); | ||||
| @@ -82,18 +82,6 @@ class StairwayWipeUsermod : public Usermod { | ||||
|       //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); | ||||
|     } | ||||
|  | ||||
|     void addToConfig(JsonObject& root) | ||||
|     { | ||||
|       JsonObject top = root.createNestedObject("exampleUsermod"); | ||||
|       top["great"] = userVar0; //save this var persistently whenever settings are saved | ||||
|     } | ||||
|  | ||||
|     void readFromConfig(JsonObject& root) | ||||
|     { | ||||
|       JsonObject top = root["top"]; | ||||
|       userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) | ||||
|     } | ||||
|  | ||||
|     uint16_t getId() | ||||
|     { | ||||
|       return USERMOD_ID_EXAMPLE; | ||||
| @@ -112,7 +100,7 @@ class StairwayWipeUsermod : public Usermod { | ||||
|     bool doReverse = (userVar0 == 2); | ||||
|     seg.setOption(1, doReverse); | ||||
|  | ||||
|     colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|     colorUpdated(CALL_MODE_NOTIFICATION); | ||||
|     } | ||||
|  | ||||
|     void turnOff() | ||||
| @@ -123,7 +111,7 @@ class StairwayWipeUsermod : public Usermod { | ||||
|     transitionDelayTemp = 4000; //fade out slowly | ||||
|     #endif | ||||
|     bri = 0; | ||||
|     colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|     colorUpdated(CALL_MODE_NOTIFICATION); | ||||
|     wipeState = 0; | ||||
|     userVar0 = 0; | ||||
|     previousUserVar0 = 0; | ||||
|   | ||||
| @@ -47,7 +47,7 @@ void userLoop() | ||||
|       if (millis() + strip.timebase > (cycleTime - 25)) { //wipe complete | ||||
|         effectCurrent = FX_MODE_STATIC; | ||||
|         timeStaticStart = millis(); | ||||
|         colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|         colorUpdated(CALL_MODE_NOTIFICATION); | ||||
|         wipeState = 2; | ||||
|       } | ||||
|     } else if (wipeState == 2) { //static | ||||
| @@ -59,7 +59,7 @@ void userLoop() | ||||
|       #ifdef STAIRCASE_WIPE_OFF | ||||
|       effectCurrent = FX_MODE_COLOR_WIPE; | ||||
|       strip.timebase = 360 + (255 - effectSpeed)*75 - millis(); //make sure wipe starts fully lit | ||||
|       colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|       colorUpdated(CALL_MODE_NOTIFICATION); | ||||
|       wipeState = 4; | ||||
|       #else | ||||
|       turnOff(); | ||||
| @@ -93,7 +93,7 @@ void startWipe() | ||||
|   bool doReverse = (userVar0 == 2); | ||||
|   seg.setOption(1, doReverse); | ||||
|  | ||||
|   colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|   colorUpdated(CALL_MODE_NOTIFICATION); | ||||
| } | ||||
|  | ||||
| void turnOff() | ||||
| @@ -104,7 +104,7 @@ void turnOff() | ||||
|   transitionDelayTemp = 4000; //fade out slowly | ||||
|   #endif | ||||
|   bri = 0; | ||||
|   colorUpdated(NOTIFIER_CALL_MODE_NOTIFICATION); | ||||
|   colorUpdated(CALL_MODE_NOTIFICATION); | ||||
|   wipeState = 0; | ||||
|   userVar0 = 0; | ||||
|   previousUserVar0 = 0; | ||||
|   | ||||
							
								
								
									
										42
									
								
								usermods/usermod_rotary_brightness_color/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| # Rotary Encoder (Brightness and Color) | ||||
|  | ||||
| V2 usermod that allows changing brightness and color using a rotary encoder,  | ||||
| change between modes by pressing a button (many encoders have one included) | ||||
|  | ||||
| but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle"  | ||||
| period in case there are other changes (any change will  | ||||
| extend the "settle" window). | ||||
|  | ||||
| It will additionally load preset AUTOSAVE_PRESET_NUM at startup. | ||||
| during the first `loop()`.  Reasoning below. | ||||
|  | ||||
| AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. | ||||
|  | ||||
| Note: I don't love that WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| define `USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` e.g. | ||||
|  | ||||
| `#define USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` in my_config.h | ||||
|  | ||||
| or add `-D USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` to `build_flags` in platformio_override.ini | ||||
|  | ||||
| ### Define Your Options | ||||
|  | ||||
| Open Usermod Settings in WLED to change settings: | ||||
|  | ||||
| `fadeAmount` - how many points to fade the Neopixel with each step of the rotary encoder (default 5) | ||||
| `pin[3]` - pins to connect to the rotary encoder: | ||||
| - `pin[0]` is pin A on your rotary encoder | ||||
| - `pin[1]` is pin B on your rotary encoder | ||||
| - `pin[2]` is the button on your rotary encoder (optional, set to -1 to disable the button and the rotary encoder will control brightness only) | ||||
|  | ||||
| ### PlatformIO requirements | ||||
|  | ||||
| No special requirements. | ||||
|  | ||||
| ## Change Log | ||||
|  | ||||
| 2021-07 | ||||
| * Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings | ||||
| @@ -0,0 +1,189 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| //v2 usermod that allows to change brightness and color using a rotary encoder,  | ||||
| //change between modes by pressing a button (many encoders have one included) | ||||
| class RotaryEncoderBrightnessColor : public Usermod | ||||
| { | ||||
| private: | ||||
|   //Private class members. You can declare variables and functions only accessible to your usermod here | ||||
|   unsigned long lastTime = 0; | ||||
|   unsigned long currentTime; | ||||
|   unsigned long loopTime; | ||||
|  | ||||
|   unsigned char select_state = 0; // 0 = brightness 1 = color | ||||
|   unsigned char button_state = HIGH; | ||||
|   unsigned char prev_button_state = HIGH; | ||||
|   CRGB fastled_col; | ||||
|   CHSV prim_hsv; | ||||
|   int16_t new_val; | ||||
|  | ||||
|   unsigned char Enc_A; | ||||
|   unsigned char Enc_B; | ||||
|   unsigned char Enc_A_prev = 0; | ||||
|  | ||||
|   // private class memebers 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 | ||||
|  | ||||
| public: | ||||
|   //Functions called by WLED | ||||
|  | ||||
|   /* | ||||
|    * 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() | ||||
|   { | ||||
|     //Serial.println("Hello from my usermod!"); | ||||
|     pinMode(pins[0], INPUT_PULLUP); | ||||
|     pinMode(pins[1], INPUT_PULLUP); | ||||
|     if(pins[2] >= 0) pinMode(pins[2], INPUT_PULLUP); | ||||
|     currentTime = millis(); | ||||
|     loopTime = currentTime; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * 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() | ||||
|   { | ||||
|     currentTime = millis(); // get the current elapsed time | ||||
|  | ||||
|     if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz | ||||
|     { | ||||
|       if(pins[2] >= 0) { | ||||
|         button_state = digitalRead(pins[2]); | ||||
|         if (prev_button_state != button_state) | ||||
|         { | ||||
|           if (button_state == LOW) | ||||
|           { | ||||
|             if (select_state == 1) | ||||
|             { | ||||
|               select_state = 0; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|               select_state = 1; | ||||
|             } | ||||
|             prev_button_state = button_state; | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             prev_button_state = button_state; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       int Enc_A = digitalRead(pins[0]); // Read encoder pins | ||||
|       int Enc_B = digitalRead(pins[1]); | ||||
|       if ((!Enc_A) && (Enc_A_prev)) | ||||
|       { // A has gone from high to low | ||||
|         if (Enc_B == HIGH) | ||||
|         { // B is high so clockwise | ||||
|           if (select_state == 0) | ||||
|           { | ||||
|             if (bri + fadeAmount <= 255) | ||||
|               bri += fadeAmount; // increase the brightness, dont go over 255 | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             fastled_col.red = col[0]; | ||||
|             fastled_col.green = col[1]; | ||||
|             fastled_col.blue = col[2]; | ||||
|             prim_hsv = rgb2hsv_approximate(fastled_col); | ||||
|             new_val = (int16_t)prim_hsv.h + fadeAmount; | ||||
|             if (new_val > 255) | ||||
|               new_val -= 255; // roll-over if  bigger than 255 | ||||
|             if (new_val < 0) | ||||
|               new_val += 255; // roll-over if smaller than 0 | ||||
|             prim_hsv.h = (byte)new_val; | ||||
|             hsv2rgb_rainbow(prim_hsv, fastled_col); | ||||
|             col[0] = fastled_col.red; | ||||
|             col[1] = fastled_col.green; | ||||
|             col[2] = fastled_col.blue; | ||||
|           } | ||||
|         } | ||||
|         else if (Enc_B == LOW) | ||||
|         { // B is low so counter-clockwise | ||||
|           if (select_state == 0) | ||||
|           { | ||||
|             if (bri - fadeAmount >= 0) | ||||
|               bri -= fadeAmount; // decrease the brightness, dont go below 0 | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             fastled_col.red = col[0]; | ||||
|             fastled_col.green = col[1]; | ||||
|             fastled_col.blue = col[2]; | ||||
|             prim_hsv = rgb2hsv_approximate(fastled_col); | ||||
|             new_val = (int16_t)prim_hsv.h - fadeAmount; | ||||
|             if (new_val > 255) | ||||
|               new_val -= 255; // roll-over if  bigger than 255 | ||||
|             if (new_val < 0) | ||||
|               new_val += 255; // roll-over if smaller than 0 | ||||
|             prim_hsv.h = (byte)new_val; | ||||
|             hsv2rgb_rainbow(prim_hsv, fastled_col); | ||||
|             col[0] = fastled_col.red; | ||||
|             col[1] = fastled_col.green; | ||||
|             col[2] = fastled_col.blue; | ||||
|           } | ||||
|         } | ||||
|         //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 | ||||
|         colorUpdated(CALL_MODE_BUTTON); | ||||
|         updateInterfaces(CALL_MODE_BUTTON); | ||||
|       } | ||||
|       Enc_A_prev = Enc_A;     // Store value of A for next time | ||||
|       loopTime = currentTime; // Updates loopTime | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void addToConfig(JsonObject& root) | ||||
|   { | ||||
|     JsonObject top = root.createNestedObject("rotEncBrightness"); | ||||
|     top["fadeAmount"] = fadeAmount; | ||||
|     JsonArray pinArray = top.createNestedArray("pin"); | ||||
|     pinArray.add(pins[0]); | ||||
|     pinArray.add(pins[1]);  | ||||
|     pinArray.add(pins[2]);  | ||||
|   } | ||||
|  | ||||
|   /*  | ||||
|    * This example uses a more robust method of checking for missing values in the config, and setting back to defaults: | ||||
|    * - The getJsonValue() function copies the value to the variable only if the key requested is present, returning false with no copy if the value isn't present | ||||
|    * - 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: | ||||
|    * - 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) | ||||
|    *  | ||||
|    * If configComplete is false, the default values are already set, and by returning false, WLED now knows it needs to save the defaults by calling addToConfig() | ||||
|    */ | ||||
|   bool readFromConfig(JsonObject& root) | ||||
|   { | ||||
|     // set defaults here, they will be set before setup() is called, and if any values parsed from ArduinoJson below are missing, the default will be used instead | ||||
|     fadeAmount = 5; | ||||
|     pins[0] = -1; | ||||
|     pins[1] = -1; | ||||
|     pins[2] = -1; | ||||
|  | ||||
|     JsonObject top = root["rotEncBrightness"]; | ||||
|  | ||||
|     bool configComplete = !top.isNull(); | ||||
|     configComplete &= getJsonValue(top["fadeAmount"], fadeAmount); | ||||
|     configComplete &= getJsonValue(top["pin"][0], pins[0]); | ||||
|     configComplete &= getJsonValue(top["pin"][1], pins[1]); | ||||
|     configComplete &= getJsonValue(top["pin"][2], pins[2]); | ||||
|  | ||||
|     return configComplete; | ||||
|   } | ||||
| }; | ||||
| @@ -29,9 +29,9 @@ This file should be placed in the same directory as `platformio.ini`. | ||||
| ### Define Your Options | ||||
|  | ||||
| * `USERMOD_AUTO_SAVE`   - define this to have this the Auto Save usermod included wled00\usermods_list.cpp | ||||
| * `USERMOD_FOUR_LINE_DISLAY`   - 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) | ||||
| * `AUTOSAVE_SETTLE_MS`         - Minimum time to wave before auto saving, defaults to 10000  (10s) | ||||
| * `AUTOSAVE_PRESET_NUM`        - Preset number to auto-save to, auto-load at startup from, defaults to 99 | ||||
| * `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) | ||||
|  | ||||
| You can configure auto-save parameters using Usermods settings page. | ||||
|  | ||||
| ### PlatformIO requirements | ||||
|  | ||||
| @@ -43,3 +43,5 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. | ||||
|  | ||||
| 2021-02 | ||||
| * First public release | ||||
| 2021-04 | ||||
| * Adaptation for runtime configuration. | ||||