Compare commits
	
		
			152 Commits
		
	
	
		
			v0.12.0-b5
			...
			v0.13.0-b0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 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 | ||
|   | 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
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| github: [Aircoookie] | ||||
| custom: ['https://paypal.me/Aircoookie'] | ||||
							
								
								
									
										67
									
								
								.github/workflows/wled-ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										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
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -14,3 +14,4 @@ | ||||
| .clang-format | ||||
| node_modules | ||||
| .idea | ||||
| .direnv | ||||
|   | ||||
							
								
								
									
										166
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										166
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,7 +1,173 @@ | ||||
| ## WLED changelog | ||||
|  | ||||
| ### Builds after release 0.12.0 | ||||
|  | ||||
| #### 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" | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 602 B | 
| Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB | 
							
								
								
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "wled", | ||||
|   "version": "0.11.1", | ||||
|   "version": "0.13.0-b0", | ||||
|   "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-b0", | ||||
|   "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
									
								
							
							
						
						
									
										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 and os.getenv("WLED_RELEASE"): | ||||
|         _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
									
								
							
							
						
						
									
										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}"]) | ||||
							
								
								
									
										109
									
								
								platformio.ini
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								platformio.ini
									
									
									
									
									
								
							| @@ -15,6 +15,7 @@ | ||||
| default_envs = nodemcuv2, esp01_1m_full, esp32dev, esp32_eth | ||||
|  | ||||
| # Single binaries (uncomment your board) | ||||
| ; default_envs = elekstube_ips | ||||
| ; default_envs = nodemcuv2 | ||||
| ; default_envs = esp01_1m_full | ||||
| ; default_envs = esp07 | ||||
| @@ -48,6 +49,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 | ||||
| @@ -114,9 +116,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 | ||||
| @@ -134,6 +133,8 @@ ldscript_4m1m = eagle.flash.4m1m.ld | ||||
| 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 | ||||
| @@ -151,10 +152,11 @@ build_flags = -g | ||||
|   -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: | ||||
| @@ -190,8 +192,8 @@ lib_deps = | ||||
|   #For use SSD1306 OLED display uncomment following | ||||
|     #U8g2@~2.27.2 | ||||
|   #For Dallas sensor uncomment following 2 lines | ||||
|     #OneWire@~2.3.5 | ||||
|     #milesburton/DallasTemperature@^3.9.0 | ||||
|     OneWire@~2.3.5 | ||||
|     milesburton/DallasTemperature@^3.9.0 | ||||
|   #For BME280 sensor uncomment following | ||||
|     #BME280@~3.0.0 | ||||
|     ; adafruit/Adafruit BMP280 Library @ 2.1.0 | ||||
| @@ -213,7 +215,7 @@ 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 | ||||
|  | ||||
| [env:esp01_1m_full] | ||||
| board = esp01_1m | ||||
| @@ -221,7 +223,7 @@ 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 | ||||
|  | ||||
| [env:esp07] | ||||
| board = esp07 | ||||
| @@ -261,7 +263,7 @@ build_flags = ${common.build_flags_esp8266} -D LEDPIN=1 -D WLED_DISABLE_INFRARED | ||||
| board = esp32dev | ||||
| platform = espressif32@3.2 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} | ||||
| build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
| @@ -271,7 +273,7 @@ board = esp32-poe | ||||
| platform = espressif32@3.2 | ||||
| upload_speed = 921600 | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp32} -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 | ||||
| build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 | ||||
| lib_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
| @@ -282,7 +284,7 @@ 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 | ||||
|  | ||||
| [env:esp8285_4CH_H801] | ||||
| board = esp8285 | ||||
| @@ -290,7 +292,7 @@ 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 | ||||
|  | ||||
| [env:esp8285_5CH_H801] | ||||
| board = esp8285 | ||||
| @@ -298,7 +300,7 @@ 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 | ||||
|  | ||||
| [env:d1_mini_5CH_Shojo_PCB] | ||||
| board = d1_mini | ||||
| @@ -306,7 +308,7 @@ 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 | ||||
|  | ||||
| # ------------------------------------------------------------------------------ | ||||
| # DEVELOPMENT BOARDS | ||||
| @@ -457,33 +459,10 @@ 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} | ||||
| @@ -491,18 +470,44 @@ board_build.ldscript = ${common.ldscript_2m512k} | ||||
| build_unflags = ${common.build_unflags} | ||||
| build_flags = ${common.build_flags_esp8266} | ||||
|  | ||||
| [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} | ||||
|  | ||||
| [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 | ||||
| lib_deps = ${env.lib_deps} | ||||
|   TFT_eSPI | ||||
| 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_ignore = | ||||
|   ESPAsyncTCP | ||||
|   ESPAsyncUDP | ||||
|   | ||||
| @@ -39,12 +39,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  | ||||
|   | ||||
| @@ -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)   | ||||
|   | ||||
							
								
								
									
										1
									
								
								requirements.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								requirements.in
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| platformio | ||||
							
								
								
									
										54
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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, | ||||
| 
 | 
| @@ -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
									
								
							
							
						
						
									
										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> | ||||
| @@ -9,419 +9,522 @@ | ||||
|  */ | ||||
| #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; | ||||
|     } | ||||
|  | ||||
|     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(NOTIFIER_CALL_MODE_DIRECT_CHANGE); | ||||
|         DEBUG_PRINTLN(F("Animated Staircase disabled.")); | ||||
|       } | ||||
|       enabled = enable; | ||||
|     } | ||||
|  | ||||
|   public: | ||||
|     void setup() { | ||||
|       // allocate pins | ||||
|       if (topPIRorTriggerPin >= 0) { | ||||
|         if (!pinManager.allocatePin(topPIRorTriggerPin,useUSSensorTop)) | ||||
|           topPIRorTriggerPin = -1; | ||||
|       } | ||||
|       if (topEchoPin >= 0) { | ||||
|         if (!pinManager.allocatePin(topEchoPin,false)) | ||||
|           topEchoPin = -1; | ||||
|       } | ||||
|       if (bottomPIRorTriggerPin >= 0) { | ||||
|         if (!pinManager.allocatePin(bottomPIRorTriggerPin,useUSSensorBottom)) | ||||
|           bottomPIRorTriggerPin = -1; | ||||
|       } | ||||
|       if (bottomEchoPin >= 0) { | ||||
|         if (!pinManager.allocatePin(bottomEchoPin,false)) | ||||
|           bottomEchoPin = -1; | ||||
|       } | ||||
|       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); | ||||
|           pinManager.deallocatePin(oldTopBPin); | ||||
|           pinManager.deallocatePin(oldBottomAPin); | ||||
|           pinManager.deallocatePin(oldBottomBPin); | ||||
|         } | ||||
|         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. | ||||
| @@ -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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -23,8 +23,12 @@ | ||||
| //class name. Use something descriptive and leave the ": public Usermod" part :) | ||||
| class MyExampleUsermod : public Usermod { | ||||
|   private: | ||||
|     // sample usermod default value for variable (you can also use constructor) | ||||
|     int userVar0 = 42; | ||||
|  | ||||
|     //Private class members. You can declare variables and functions only accessible to your usermod here | ||||
|     unsigned long lastTime = 0; | ||||
|  | ||||
|   public: | ||||
|     //Functions called by WLED | ||||
|  | ||||
| @@ -133,11 +137,21 @@ class MyExampleUsermod : public Usermod { | ||||
|      * 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 your config was complete, or false if you'd like WLED to save your defaults to disk | ||||
|      *  | ||||
|      * 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) | ||||
|       //set defaults for variables when declaring the variable (class definition or constructor) | ||||
|       JsonObject top = root["exampleUsermod"]; | ||||
|       if (!top.isNull()) return false; | ||||
|  | ||||
|       userVar0 = top["great"] | userVar0;  | ||||
|  | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     | ||||
|   | ||||
							
								
								
									
										70
									
								
								usermods/EleksTube_IPS/ChipSelect.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -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,128 @@ 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 | ||||
|   // 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; | ||||
|  | ||||
|   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[]; | ||||
|  | ||||
|   /** | ||||
|    * 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 (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 +173,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 +193,19 @@ 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); | ||||
|     // pin retrieved from cfg.json (readFromConfig()) prior to running setup() | ||||
|     if (!pinManager.allocatePin(PIRsensorPin,false)) { | ||||
|       PIRsensorPin = -1;  // allocation failed | ||||
|       enabled = false; | ||||
|       DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed.")); | ||||
|     } else { | ||||
|       // PIR Sensor mode INPUT_PULLUP | ||||
|       pinMode(PIRsensorPin, INPUT_PULLUP); | ||||
|       if (enabled) { | ||||
|         sensorPinState = digitalRead(PIRsensorPin); | ||||
|       } | ||||
|     } | ||||
|     initDone = true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -181,14 +221,12 @@ public: | ||||
|    */ | ||||
|   void loop() | ||||
|   { | ||||
|     if (!updatePIRsensorState()) | ||||
|     { | ||||
|     // only check sensors 10x/s | ||||
|     if (millis() - lastLoop < 100 || strip.isUpdating()) return; | ||||
|     lastLoop = millis(); | ||||
|  | ||||
|     if (!updatePIRsensorState()) { | ||||
|       handleOffTimer(); | ||||
|       if (m_updateConfig) | ||||
|       { | ||||
|         serializeConfig(); | ||||
|         m_updateConfig = false; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -199,132 +237,145 @@ 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; | ||||
|     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; | ||||
|  | ||||
|     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); | ||||
|           if (pinManager.allocatePin(PIRsensorPin,false)) { | ||||
|             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 true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -337,30 +388,11 @@ 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"; | ||||
|   | ||||
							
								
								
									
										8
									
								
								usermods/RTC/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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 | ||||
| } | ||||
| @@ -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,123 @@ 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 enabled = true; | ||||
|  | ||||
|     void requestTemperatures() { | ||||
|         // there is requestTemperaturesByAddress however it  | ||||
|         // appears to do more work,  | ||||
|         // TODO: measure exection time difference | ||||
|         sensor.requestTemperatures();  | ||||
|         lastTemperaturesRequest = millis(); | ||||
|         waitingForConversion = 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); | ||||
|     } | ||||
|  | ||||
|     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(); | ||||
|       while (oneWire->search(deviceAddress)) { | ||||
|         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; | ||||
|       int retries = 10; | ||||
|       // pin retrieved from cfg.json (readFromConfig()) prior to running setup() | ||||
|       if (!pinManager.allocatePin(temperaturePin,false)) { | ||||
|         temperaturePin = -1;  // allocation failed | ||||
|         enabled = false; | ||||
|         DEBUG_PRINTLN(F("Temperature pin allocation failed.")); | ||||
|       } else { | ||||
|         DEBUG_PRINTLN(F("Dallas Temperature not found")); | ||||
|         if (enabled) { | ||||
|           // config says we are enabled | ||||
|           oneWire = new OneWire(temperaturePin); | ||||
|           if (!oneWire->reset()) | ||||
|             enabled = false;   // resetting 1-Wire bus yielded an error | ||||
|           else | ||||
|             while ((enabled=findSensor()) && retries--) delay(25); // try to find sensor | ||||
|         } | ||||
|       } | ||||
|       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,22 +164,30 @@ 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")); | ||||
|  | ||||
|       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; | ||||
|       } | ||||
|       JsonArray temp = user.createNestedArray(FPSTR(_name)); | ||||
|       //temp.add(F("Loaded.")); | ||||
|  | ||||
|       if (temperature <= -100) { | ||||
|         temp.add(0); | ||||
| @@ -158,12 +195,84 @@ class UsermodTemperature : public Usermod { | ||||
|         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; | ||||
|       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 { | ||||
|         // changing parameters from settings page | ||||
|         if (newTemperaturePin != temperaturePin) { | ||||
|           // deallocate pin and release memory | ||||
|           delete oneWire; | ||||
|           pinManager.deallocatePin(temperaturePin); | ||||
|           temperaturePin = newTemperaturePin; | ||||
|           // initialise | ||||
|           setup(); | ||||
|         } | ||||
|         DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|       } | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return !top[FPSTR(_parasite)].isNull(); | ||||
|     } | ||||
|  | ||||
|     uint16_t getId() | ||||
| @@ -171,3 +280,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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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; | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										79
									
								
								usermods/multi_relay/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								usermods/multi_relay/readme.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| # 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` | ||||
|  | ||||
| ## 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. | ||||
							
								
								
									
										416
									
								
								usermods/multi_relay/usermod_multi_relay.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								usermods/multi_relay/usermod_multi_relay.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,416 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| #ifndef MULTI_RELAY_MAX_RELAYS | ||||
|   #define MULTI_RELAY_MAX_RELAYS 4 | ||||
| #endif | ||||
|  | ||||
| #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; | ||||
| } 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 _oldBrightness = 0; | ||||
|  | ||||
|     // 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[]; | ||||
|  | ||||
|  | ||||
|     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; | ||||
|       } | ||||
|     } | ||||
|     /** | ||||
|      * 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)) { | ||||
|           _relay[i].pin = -1;  // allocation failed | ||||
|         } else { | ||||
|           switchRelay(i, _relay[i].state = (bool)bri); | ||||
|           _relay[i].active = false; | ||||
|         } | ||||
|       } | ||||
|       _oldBrightness = (bool)bri; | ||||
|       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() { | ||||
|       if (!enabled || strip.isUpdating()) return; | ||||
|  | ||||
|       static unsigned long lastUpdate = 0; | ||||
|       if (millis() - lastUpdate < 200) return;  // update only 5 times/s | ||||
|       lastUpdate = millis(); | ||||
|  | ||||
|       //set relay when LEDs turn on | ||||
|       if (_oldBrightness != (bool)bri) { | ||||
|         _oldBrightness = (bool)bri; | ||||
|         _switchTimerStart = millis(); | ||||
|         for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) { | ||||
|           if (_relay[i].pin>=0) _relay[i].active = true; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       handleOffTimer(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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())); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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; parName += "-"; | ||||
|         top[parName+"pin"]              = _relay[i].pin; | ||||
|         top[parName+FPSTR(_activeHigh)] = _relay[i].mode; | ||||
|         top[parName+FPSTR(_delay_str)]  = _relay[i].delay; | ||||
|         top[parName+FPSTR(_external)]   = _relay[i].external; | ||||
|       } | ||||
|       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; parName += "-"; | ||||
|         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].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]); | ||||
|           } | ||||
|         // 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)) { | ||||
|             if (!_relay[i].external) switchRelay(i, _relay[i].state = (bool)bri); | ||||
|           } 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 true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). | ||||
|      * This could be used in the future for the system to determine whether your usermod is installed. | ||||
|      */ | ||||
|     uint16_t getId() | ||||
|     { | ||||
|       return USERMOD_ID_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"; | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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. | ||||
| @@ -2,9 +2,8 @@ | ||||
|  | ||||
| #include "wled.h" | ||||
|  | ||||
| // | ||||
| // v2 Usermod to automatically save settings  | ||||
| // to preset number AUTOSAVE_PRESET_NUM after a change to any of | ||||
| // to configurable preset after a change to any of | ||||
| // | ||||
| // * brightness | ||||
| // * effect speed | ||||
| @@ -12,45 +11,34 @@ | ||||
| // * mode (effect) | ||||
| // * palette | ||||
| // | ||||
| // but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle"  | ||||
| // but it will wait for configurable number of seconds, 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. | ||||
| // It can be configured to load auto saved preset at startup, | ||||
| // during the first `loop()`. | ||||
| // | ||||
| // 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. | ||||
|  | ||||
| //How long to wait after settings change to auto-save | ||||
| #ifndef AUTOSAVE_SETTLE_MS | ||||
| #define AUTOSAVE_SETTLE_MS 10*1000 | ||||
| #endif | ||||
|  | ||||
| //Preset number to save to | ||||
| #ifndef AUTOSAVE_PRESET_NUM | ||||
| #define AUTOSAVE_PRESET_NUM 99 | ||||
| #endif | ||||
|  | ||||
| //  "Auto save MM-DD HH:MM:SS" | ||||
| // format: "~ MM-DD HH:MM:SS ~" | ||||
| #define PRESET_NAME_BUFFER_SIZE 25 | ||||
|  | ||||
| class AutoSaveUsermod : public Usermod { | ||||
|   private: | ||||
|     // If we've detected the need to auto save, this will | ||||
|     // be non zero. | ||||
|     unsigned long autoSaveAfter = 0; | ||||
|  | ||||
|     char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; | ||||
|   private: | ||||
|  | ||||
|     bool firstLoop = true; | ||||
|     bool initDone = false; | ||||
|     bool enabled = true; | ||||
|  | ||||
|     // configurable parameters | ||||
|     uint16_t autoSaveAfterSec = 15;       // 15s by default | ||||
|     uint8_t autoSavePreset = 250;         // last possible preset | ||||
|     bool applyAutoSaveOnBoot = false;     // do we load auto-saved preset on boot? | ||||
|  | ||||
|     // If we've detected the need to auto save, this will be non zero. | ||||
|     uint16_t autoSaveAfter = 0; | ||||
|  | ||||
|     uint8_t knownBrightness = 0; | ||||
|     uint8_t knownEffectSpeed = 0; | ||||
| @@ -58,35 +46,65 @@ class AutoSaveUsermod : public Usermod { | ||||
|     uint8_t knownMode = 0; | ||||
|     uint8_t knownPalette = 0; | ||||
|  | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
|     #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|     FourLineDisplayUsermod* display; | ||||
| #endif | ||||
|     #endif | ||||
|  | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _autoSaveEnabled[]; | ||||
|     static const char _autoSaveAfterSec[]; | ||||
|     static const char _autoSavePreset[]; | ||||
|     static const char _autoSaveApplyOnBoot[]; | ||||
|  | ||||
|     void inline saveSettings() { | ||||
|       char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; | ||||
|       updateLocalTime(); | ||||
|       sprintf_P(presetNameBuffer,  | ||||
|         PSTR("~ %02d-%02d %02d:%02d:%02d ~"), | ||||
|         month(localTime), day(localTime), | ||||
|         hour(localTime), minute(localTime), second(localTime)); | ||||
|       savePreset(autoSavePreset, true, presetNameBuffer); | ||||
|     } | ||||
|  | ||||
|     void inline displayOverlay() { | ||||
|       #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|       if (display != nullptr) { | ||||
|         display->wakeDisplay(); | ||||
|         display->overlay("Settings", "Auto Saved", 1500); | ||||
|       } | ||||
|       #endif | ||||
|     } | ||||
|  | ||||
|   public: | ||||
|  | ||||
|     // gets called once at boot. Do all initialization that doesn't depend on | ||||
|     // network here | ||||
|     void setup() { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY     | ||||
|     // This Usermod has enhanced funcionality if | ||||
|     // FourLineDisplayUsermod is available. | ||||
|     display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); | ||||
| #endif | ||||
|       #ifdef USERMOD_FOUR_LINE_DISPLAY     | ||||
|       // This Usermod has enhanced funcionality if | ||||
|       // FourLineDisplayUsermod is available. | ||||
|       display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); | ||||
|       #endif | ||||
|       initDone = true; | ||||
|     } | ||||
|  | ||||
|     // gets called every time WiFi is (re-)connected. Initialize own network | ||||
|     // interfaces here | ||||
|     void connected() {} | ||||
|  | ||||
|     /** | ||||
|     /* | ||||
|      * Da loop. | ||||
|      */ | ||||
|     void loop() { | ||||
|       if (!autoSaveAfterSec || !enabled || strip.isUpdating()) return;  // setting 0 as autosave seconds disables autosave | ||||
|  | ||||
|       unsigned long now = millis(); | ||||
|       uint8_t currentMode = strip.getMode(); | ||||
|       uint8_t currentPalette = strip.getSegment(0).palette; | ||||
|       if (firstLoop) { | ||||
|         firstLoop = false; | ||||
|         applyPreset(AUTOSAVE_PRESET_NUM); | ||||
|         if (applyAutoSaveOnBoot) applyPreset(autoSavePreset); | ||||
|         knownBrightness = bri; | ||||
|         knownEffectSpeed = effectSpeed; | ||||
|         knownEffectIntensity = effectIntensity; | ||||
| @@ -95,7 +113,7 @@ class AutoSaveUsermod : public Usermod { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       unsigned long wouldAutoSaveAfter = now + AUTOSAVE_SETTLE_MS; | ||||
|       unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000; | ||||
|       if (knownBrightness != bri) { | ||||
|         knownBrightness = bri; | ||||
|         autoSaveAfter = wouldAutoSaveAfter; | ||||
| @@ -121,37 +139,32 @@ class AutoSaveUsermod : public Usermod { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     void saveSettings() { | ||||
|       updateLocalTime(); | ||||
|       sprintf(presetNameBuffer,  | ||||
|         "Auto save %02d-%02d %02d:%02d:%02d", | ||||
|         month(localTime), day(localTime), | ||||
|         hour(localTime), minute(localTime), second(localTime)); | ||||
|       savePreset(AUTOSAVE_PRESET_NUM, true, presetNameBuffer); | ||||
|     } | ||||
|  | ||||
|     void displayOverlay() { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
|       if (display != nullptr) { | ||||
|         display->wakeDisplay(); | ||||
|         display->overlay("Settings", "Auto Saved", 1500); | ||||
|       } | ||||
| #endif | ||||
|     } | ||||
|     /* | ||||
|      * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. | ||||
|      * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. | ||||
|      * Below it is shown how this could be used for e.g. a light sensor | ||||
|      */ | ||||
|     //void addToJsonInfo(JsonObject& root) { | ||||
|       //JsonObject user = root["u"]; | ||||
|       //if (user.isNull()) user = root.createNestedObject("u"); | ||||
|       //JsonArray data = user.createNestedArray(F("Autosave")); | ||||
|       //data.add(F("Loaded.")); | ||||
|     //} | ||||
|  | ||||
|     /* | ||||
|      * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). | ||||
|      * Values in the state object may be modified by connected clients | ||||
|      */ | ||||
|     void addToJsonState(JsonObject& root) { | ||||
|     } | ||||
|     //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) { | ||||
|     } | ||||
|     //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. | ||||
| @@ -168,6 +181,13 @@ class AutoSaveUsermod : public Usermod { | ||||
|      * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! | ||||
|      */ | ||||
|     void addToConfig(JsonObject& root) { | ||||
|       // we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}} | ||||
|       JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname | ||||
|       top[FPSTR(_autoSaveEnabled)]     = enabled; | ||||
|       top[FPSTR(_autoSaveAfterSec)]    = autoSaveAfterSec;  // usermodparam | ||||
|       top[FPSTR(_autoSavePreset)]      = autoSavePreset;    // usermodparam | ||||
|       top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot; | ||||
|       DEBUG_PRINTLN(F("Autosave config saved.")); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
| @@ -177,9 +197,30 @@ class AutoSaveUsermod : public Usermod { | ||||
|      * 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. | ||||
|      */ | ||||
|     void readFromConfig(JsonObject& root) { | ||||
|     } | ||||
|     bool readFromConfig(JsonObject& root) { | ||||
|       // we look for JSON object: {"Autosave": {"enabled": true, "autoSaveAfterSec": 10, "autoSavePreset": 250, ...}} | ||||
|       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(_autoSaveEnabled)] | enabled; | ||||
|       autoSaveAfterSec    = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec; | ||||
|       autoSaveAfterSec    = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking | ||||
|       autoSavePreset      = top[FPSTR(_autoSavePreset)] | autoSavePreset; | ||||
|       autoSavePreset      = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking | ||||
|       applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot; | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|  | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return true; | ||||
|   } | ||||
|  | ||||
|     /* | ||||
|      * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). | ||||
| @@ -188,5 +229,11 @@ class AutoSaveUsermod : public Usermod { | ||||
|     uint16_t getId() { | ||||
|       return USERMOD_ID_AUTO_SAVE; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| }; | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char AutoSaveUsermod::_name[]                PROGMEM = "Autosave"; | ||||
| const char AutoSaveUsermod::_autoSaveEnabled[]     PROGMEM = "enabled"; | ||||
| const char AutoSaveUsermod::_autoSaveAfterSec[]    PROGMEM = "autoSaveAfterSec"; | ||||
| const char AutoSaveUsermod::_autoSavePreset[]      PROGMEM = "autoSavePreset"; | ||||
| const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot"; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Rotary Encoder UI Usermod | ||||
| # I2C 4 Line Display Usermod | ||||
|  | ||||
| First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. | ||||
|  | ||||
| @@ -19,13 +19,11 @@ This file should be placed in the same directory as `platformio.ini`. | ||||
|  | ||||
| ### Define Your Options | ||||
|  | ||||
| * `USERMOD_FOUR_LINE_DISLAY`   - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available | ||||
| * `USERMOD_FOUR_LINE_DISPLAY`  - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available | ||||
| * `FLD_PIN_SCL`                - The display SCL pin, defaults to 5 | ||||
| * `FLD_PIN_SDA`                - The display SDA pin, defaults to 4 | ||||
| * `FLIP_MODE`                  - Set to 0 or 1 | ||||
| * `LINE_HEIGHT`                - Set to 1 or 2 | ||||
|  | ||||
| There are other `#define` values in the Usermod that might be of interest. | ||||
| All of the parameters can be configured using Usermods settings page, inluding GPIO pins. | ||||
|  | ||||
| ### PlatformIO requirements | ||||
|  | ||||
| @@ -37,3 +35,5 @@ UI usermod folder for how to include these using `platformio_override.ini`. | ||||
|  | ||||
| 2021-02 | ||||
| * First public release | ||||
| 2021-04 | ||||
| * Adaptation for runtime configuration. | ||||
| @@ -24,50 +24,25 @@ | ||||
| // | ||||
|  | ||||
| //The SCL and SDA pins are defined here.  | ||||
| #ifndef FLD_PIN_SCL | ||||
| #define FLD_PIN_SCL 5 | ||||
| #endif | ||||
|  | ||||
| #ifndef FLD_PIN_SDA | ||||
| #define FLD_PIN_SDA 4 | ||||
| #endif | ||||
|  | ||||
| // U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8( | ||||
| //   U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA);  | ||||
| U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8( | ||||
|   U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA);  | ||||
|  | ||||
| // Screen upside down? Change to 0 or 1 | ||||
| #ifndef FLIP_MODE | ||||
| #define FLIP_MODE 0 | ||||
| #endif | ||||
|  | ||||
| // LINE_HEIGHT 1 is single height, for 128x32 displays. | ||||
| // LINE_HEIGHT 2 makes the 128x64 screen display at double height. | ||||
| #ifndef LINE_HEIGHT | ||||
| #define LINE_HEIGHT 2 | ||||
| #endif | ||||
|  | ||||
| // If you aren't also including RotaryEncoderUIUsermod | ||||
| // you probably want to set both | ||||
| //     SLEEP_MODE_ENABLED false | ||||
| //     CLOCK_MODE_ENABLED false | ||||
| // as you will never be able wake the display / disable the clock. | ||||
| #ifdef USERMOD_ROTARY_ENCODER_UI | ||||
| #ifndef SLEEP_MODE_ENABLED | ||||
| #define SLEEP_MODE_ENABLED true | ||||
| #endif | ||||
| #ifndef CLOCK_MODE_ENABLED | ||||
| #define CLOCK_MODE_ENABLED true | ||||
| #endif | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
|   #ifndef FLD_PIN_SCL | ||||
|     #define FLD_PIN_SCL 22 | ||||
|   #endif | ||||
|   #ifndef FLD_PIN_SDA | ||||
|     #define FLD_PIN_SDA 21 | ||||
|   #endif | ||||
| #else | ||||
| #define SLEEP_MODE_ENABLED false | ||||
| #define CLOCK_MODE_ENABLED false | ||||
|   #ifndef FLD_PIN_SCL | ||||
|     #define FLD_PIN_SCL 5 | ||||
|   #endif | ||||
|   #ifndef FLD_PIN_SDA | ||||
|     #define FLD_PIN_SDA 4 | ||||
|   #endif | ||||
| #endif | ||||
|  | ||||
| // When to time out to the clock or blank the screen | ||||
| // if SLEEP_MODE_ENABLED. | ||||
| #define SCREEN_TIMEOUT_MS  15*1000 | ||||
| #define SCREEN_TIMEOUT_MS  60*1000    // 1 min | ||||
|  | ||||
| #define TIME_INDENT        0 | ||||
| #define DATE_INDENT        2 | ||||
| @@ -75,35 +50,45 @@ U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8( | ||||
| // Minimum time between redrawing screen in ms | ||||
| #define USER_LOOP_REFRESH_RATE_MS 1000 | ||||
|  | ||||
| #if LINE_HEIGHT == 2 | ||||
| #define DRAW_STRING draw1x2String | ||||
| #define DRAW_GLYPH draw1x2Glyph | ||||
| #define DRAW_BIG_STRING draw2x2String | ||||
| #else | ||||
| #define DRAW_STRING drawString | ||||
| #define DRAW_GLYPH drawGlyph | ||||
| #define DRAW_BIG_STRING draw2x2String | ||||
| #endif | ||||
|  | ||||
| // Extra char (+1) for null | ||||
| #define LINE_BUFFER_SIZE            16+1 | ||||
| #define FLD_LINE_3_BRIGHTNESS       0 | ||||
| #define FLD_LINE_3_EFFECT_SPEED     1 | ||||
| #define FLD_LINE_3_EFFECT_INTENSITY 2 | ||||
| #define FLD_LINE_3_PALETTE          3 | ||||
|  | ||||
| #if LINE_HEIGHT == 2 | ||||
| #define TIME_LINE  1 | ||||
| #else | ||||
| #define TIME_LINE  0 | ||||
| #endif | ||||
| typedef enum { | ||||
|   FLD_LINE_BRIGHTNESS = 0, | ||||
|   FLD_LINE_EFFECT_SPEED, | ||||
|   FLD_LINE_EFFECT_INTENSITY, | ||||
|   FLD_LINE_MODE, | ||||
|   FLD_LINE_PALETTE, | ||||
|   FLD_LINE_TIME | ||||
| } Line4Type; | ||||
|  | ||||
| typedef enum { | ||||
|   NONE = 0, | ||||
|   SSD1306,    // U8X8_SSD1306_128X32_UNIVISION_HW_I2C | ||||
|   SH1106,     // U8X8_SH1106_128X64_WINSTAR_HW_I2C | ||||
|   SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C | ||||
|   SSD1305,    // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C | ||||
|   SSD1305_64  // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C | ||||
| } DisplayType; | ||||
|  | ||||
| class FourLineDisplayUsermod : public Usermod { | ||||
|  | ||||
|   private: | ||||
|  | ||||
|     bool initDone = false; | ||||
|     unsigned long lastTime = 0; | ||||
|  | ||||
|     // needRedraw marks if redraw is required to prevent often redrawing. | ||||
|     bool needRedraw = true; | ||||
|     // HW interface & configuration | ||||
|     U8X8 *u8x8 = nullptr;           // pointer to U8X8 display object | ||||
|     int8_t sclPin=FLD_PIN_SCL, sdaPin=FLD_PIN_SDA;    // I2C pins for interfacing, get initialised in readFromConfig() | ||||
|     DisplayType type = SSD1306;     // display type | ||||
|     bool flip = false;              // flip display 180° | ||||
|     uint8_t contrast = 10;          // screen contrast | ||||
|     uint8_t lineHeight = 1;         // 1 row or 2 rows | ||||
|     uint32_t refreshRate = USER_LOOP_REFRESH_RATE_MS; // in ms | ||||
|     uint32_t screenTimeout = SCREEN_TIMEOUT_MS;       // in ms | ||||
|     bool sleepMode = true;          // allow screen sleep? | ||||
|     bool clockMode = false;         // display clock | ||||
|  | ||||
|     // Next variables hold the previous known values to determine if redraw is | ||||
|     // required. | ||||
| @@ -118,38 +103,93 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|     uint8_t knownHour = 99; | ||||
|  | ||||
|     bool displayTurnedOff = false; | ||||
|     long lastUpdate = 0; | ||||
|     long lastRedraw = 0; | ||||
|     long overlayUntil = 0; | ||||
|     byte lineThreeType = FLD_LINE_3_BRIGHTNESS; | ||||
|     unsigned long lastUpdate = 0; | ||||
|     unsigned long lastRedraw = 0; | ||||
|     unsigned long overlayUntil = 0; | ||||
|     Line4Type lineType = FLD_LINE_BRIGHTNESS; | ||||
|     // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. | ||||
|     byte markLineNum = 0; | ||||
|  | ||||
|     char lineBuffer[LINE_BUFFER_SIZE]; | ||||
|  | ||||
|     char **modes_qstrings = nullptr; | ||||
|     char **palettes_qstrings = nullptr; | ||||
|     // strings to reduce flash memory usage (used more than twice) | ||||
|     static const char _name[]; | ||||
|     static const char _contrast[]; | ||||
|     static const char _refreshRate[]; | ||||
|     static const char _screenTimeOut[]; | ||||
|     static const char _flip[]; | ||||
|     static const char _sleepMode[]; | ||||
|     static const char _clockMode[]; | ||||
|  | ||||
|     // If display does not work or looks corrupted check the | ||||
|     // constructor reference: | ||||
|     // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp | ||||
|     // or check the gallery: | ||||
|     // https://github.com/olikraus/u8g2/wiki/gallery | ||||
|  | ||||
|   public: | ||||
|  | ||||
|     // gets called once at boot. Do all initialization that doesn't depend on | ||||
|     // network here | ||||
|     void setup() { | ||||
|       u8x8.begin(); | ||||
|       u8x8.setFlipMode(FLIP_MODE); | ||||
|       u8x8.setPowerSave(0); | ||||
|       u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 | ||||
|       u8x8.setFont(u8x8_font_chroma48medium8_r); | ||||
|       u8x8.DRAW_STRING(0, 0*LINE_HEIGHT, "Loading..."); | ||||
|  | ||||
|       ModeSortUsermod *modeSortUsermod = (ModeSortUsermod*) usermods.lookup(USERMOD_ID_MODE_SORT); | ||||
|       modes_qstrings = modeSortUsermod->getModesQStrings(); | ||||
|       palettes_qstrings = modeSortUsermod->getPalettesQStrings(); | ||||
|       if (type == NONE) return; | ||||
|       if (!pinManager.allocatePin(sclPin)) { sclPin = -1; type = NONE; return;} | ||||
|       if (!pinManager.allocatePin(sdaPin)) { pinManager.deallocatePin(sclPin); sclPin = sdaPin = -1; type = NONE; return; } | ||||
|       switch (type) { | ||||
|         case SSD1306: | ||||
|           #ifdef ESP8266 | ||||
|           if (!(sclPin==5 && sdaPin==4)) | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset | ||||
|           else | ||||
|           #endif | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA | ||||
|           lineHeight = 1; | ||||
|           break; | ||||
|         case SH1106: | ||||
|           #ifdef ESP8266 | ||||
|           if (!(sclPin==5 && sdaPin==4)) | ||||
|             u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset | ||||
|           else | ||||
|           #endif | ||||
|             u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA | ||||
|           lineHeight = 2; | ||||
|           break; | ||||
|         case SSD1306_64: | ||||
|           #ifdef ESP8266 | ||||
|           if (!(sclPin==5 && sdaPin==4)) | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset | ||||
|           else | ||||
|           #endif | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA | ||||
|           lineHeight = 2; | ||||
|           break; | ||||
|         case SSD1305: | ||||
|           #ifdef ESP8266 | ||||
|           if (!(sclPin==5 && sdaPin==4)) | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset | ||||
|           else | ||||
|           #endif | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA | ||||
|           lineHeight = 1; | ||||
|           break; | ||||
|         case SSD1305_64: | ||||
|           #ifdef ESP8266 | ||||
|           if (!(sclPin==5 && sdaPin==4)) | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset | ||||
|           else | ||||
|           #endif | ||||
|             u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA | ||||
|           lineHeight = 2; | ||||
|           break; | ||||
|         default: | ||||
|           u8x8 = nullptr; | ||||
|           type = NONE; | ||||
|           return; | ||||
|       } | ||||
|       (static_cast<U8X8*>(u8x8))->begin(); | ||||
|       setFlipMode(flip); | ||||
|       setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 | ||||
|       setPowerSave(0); | ||||
|       drawString(0, 0, "Loading..."); | ||||
|       initDone = true; | ||||
|     } | ||||
|  | ||||
|     // gets called every time WiFi is (re-)connected. Initialize own network | ||||
| @@ -160,26 +200,68 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|      * Da loop. | ||||
|      */ | ||||
|     void loop() { | ||||
|       if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { | ||||
|         return; | ||||
|       } | ||||
|       if (millis() - lastUpdate < (clockMode?1000:refreshRate) || strip.isUpdating()) return; | ||||
|       lastUpdate = millis(); | ||||
|  | ||||
|       redraw(false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Wrappers for screen drawing | ||||
|      */ | ||||
|     void setFlipMode(uint8_t mode) { | ||||
|       if (type==NONE) return; | ||||
|       (static_cast<U8X8*>(u8x8))->setFlipMode(mode); | ||||
|     } | ||||
|     void setContrast(uint8_t contrast) { | ||||
|       if (type==NONE) return; | ||||
|       (static_cast<U8X8*>(u8x8))->setContrast(contrast); | ||||
|     } | ||||
|     void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { | ||||
|       if (type==NONE) return; | ||||
|       (static_cast<U8X8*>(u8x8))->setFont(u8x8_font_chroma48medium8_r); | ||||
|       if (!ignoreLH && lineHeight==2) (static_cast<U8X8*>(u8x8))->draw1x2String(col, row, string); | ||||
|       else                            (static_cast<U8X8*>(u8x8))->drawString(col, row, string); | ||||
|     } | ||||
|     void draw2x2String(uint8_t col, uint8_t row, const char *string) { | ||||
|       if (type==NONE) return; | ||||
|       (static_cast<U8X8*>(u8x8))->setFont(u8x8_font_chroma48medium8_r); | ||||
|       (static_cast<U8X8*>(u8x8))->draw2x2String(col, row, string); | ||||
|     } | ||||
|     void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) { | ||||
|       if (type==NONE) return; | ||||
|       (static_cast<U8X8*>(u8x8))->setFont(font); | ||||
|       if (!ignoreLH && lineHeight==2) (static_cast<U8X8*>(u8x8))->draw1x2Glyph(col, row, glyph); | ||||
|       else                            (static_cast<U8X8*>(u8x8))->drawGlyph(col, row, glyph); | ||||
|     } | ||||
|     uint8_t getCols() { | ||||
|       if (type==NONE) return 0; | ||||
|       return (static_cast<U8X8*>(u8x8))->getCols(); | ||||
|     } | ||||
|     void clear() { | ||||
|       if (type==NONE) return; | ||||
|       (static_cast<U8X8*>(u8x8))->clear(); | ||||
|     } | ||||
|     void setPowerSave(uint8_t save) { | ||||
|       if (type==NONE) return; | ||||
|       (static_cast<U8X8*>(u8x8))->setPowerSave(save); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Redraw the screen (but only if things have changed | ||||
|      * or if forceRedraw). | ||||
|      */ | ||||
|     void redraw(bool forceRedraw) { | ||||
|       static bool showName = false; | ||||
|       unsigned long now = millis(); | ||||
|  | ||||
|       if (type==NONE) return; | ||||
|       if (overlayUntil > 0) { | ||||
|         if (millis() >= overlayUntil) { | ||||
|         if (now >= overlayUntil) { | ||||
|           // Time to display the overlay has elapsed. | ||||
|           overlayUntil = 0; | ||||
|           forceRedraw = true; | ||||
|         } | ||||
|         else { | ||||
|         } else { | ||||
|           // We are still displaying the overlay | ||||
|           // Don't redraw. | ||||
|           return; | ||||
| @@ -187,54 +269,63 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|       } | ||||
|  | ||||
|       // Check if values which are shown on display changed from the last time. | ||||
|       if (forceRedraw) { | ||||
|         needRedraw = true; | ||||
|       } else 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 (knownEffectSpeed != effectSpeed) { | ||||
|         needRedraw = true; | ||||
|       } else if (knownEffectIntensity != effectIntensity) { | ||||
|         needRedraw = true; | ||||
|       } else if (knownMode != strip.getMode()) { | ||||
|         needRedraw = true; | ||||
|       } else if (knownPalette != strip.getSegment(0).palette) { | ||||
|         needRedraw = true; | ||||
|       } | ||||
|  | ||||
|       if (!needRedraw) { | ||||
|       if (forceRedraw || | ||||
|           (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) || | ||||
|           (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) || | ||||
|           (knownBrightness != bri) || | ||||
|           (knownEffectSpeed != effectSpeed) || | ||||
|           (knownEffectIntensity != effectIntensity) || | ||||
|           (knownMode != strip.getMode()) || | ||||
|           (knownPalette != strip.getSegment(0).palette)) { | ||||
|         knownHour = 99; // force time update | ||||
|         clear(); | ||||
|       } else if (sleepMode && !displayTurnedOff && ((now - lastRedraw)/1000)%5 == 0) { | ||||
|         // change line every 5s | ||||
|         showName = !showName; | ||||
|         switch (lineType) { | ||||
|           case FLD_LINE_BRIGHTNESS: | ||||
|             lineType = FLD_LINE_EFFECT_SPEED; | ||||
|             break; | ||||
|           case FLD_LINE_MODE: | ||||
|             lineType = FLD_LINE_BRIGHTNESS; | ||||
|             break; | ||||
|           case FLD_LINE_PALETTE: | ||||
|             lineType = clockMode ? FLD_LINE_MODE : FLD_LINE_BRIGHTNESS; | ||||
|             break; | ||||
|           case FLD_LINE_EFFECT_SPEED: | ||||
|             lineType = FLD_LINE_EFFECT_INTENSITY; | ||||
|             break; | ||||
|           case FLD_LINE_EFFECT_INTENSITY: | ||||
|             lineType = FLD_LINE_PALETTE; | ||||
|             break; | ||||
|           default: | ||||
|             lineType = FLD_LINE_MODE; | ||||
|             break; | ||||
|         } | ||||
|         knownHour = 99; // force time update | ||||
|       } else { | ||||
|         // Nothing to change. | ||||
|         // Turn off display after 3 minutes with no change. | ||||
|         if(SLEEP_MODE_ENABLED && !displayTurnedOff && | ||||
|             (millis() - lastRedraw > SCREEN_TIMEOUT_MS)) { | ||||
|         if(sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { | ||||
|           // We will still check if there is a change in redraw() | ||||
|           // and turn it back on if it changed. | ||||
|           clear(); // force screen clear | ||||
|           sleepOrClock(true); | ||||
|         } | ||||
|         else if (displayTurnedOff && CLOCK_MODE_ENABLED) { | ||||
|         } else if (displayTurnedOff && clockMode) { | ||||
|           showTime(); | ||||
|         } | ||||
|         return; | ||||
|       } | ||||
|       needRedraw = false; | ||||
|       lastRedraw = millis(); | ||||
|  | ||||
|       // do not update lastRedraw marker if just switching row contenet | ||||
|       if (((now - lastRedraw)/1000)%5 != 0) lastRedraw = now; | ||||
|        | ||||
|       if (displayTurnedOff) | ||||
|       { | ||||
|         // Turn the display back on | ||||
|         sleepOrClock(false); | ||||
|       } | ||||
|       // Turn the display back on | ||||
|       if (displayTurnedOff) sleepOrClock(false); | ||||
|  | ||||
|       // 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(); | ||||
|       knownIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); | ||||
|       knownBrightness = bri; | ||||
|       knownMode = strip.getMode(); | ||||
|       knownPalette = strip.getSegment(0).palette; | ||||
| @@ -242,79 +333,102 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|       knownEffectIntensity = effectIntensity; | ||||
|  | ||||
|       // Do the actual drawing | ||||
|       u8x8.clear(); | ||||
|       u8x8.setFont(u8x8_font_chroma48medium8_r); | ||||
|  | ||||
|       // First row with Wifi name | ||||
|       String ssidString = knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0); | ||||
|       u8x8.DRAW_STRING(1, 0*LINE_HEIGHT, ssidString.c_str()); | ||||
|       // Print `~` char to indicate that SSID is longer, than owr dicplay | ||||
|       if (knownSsid.length() > u8x8.getCols()) { | ||||
|         u8x8.DRAW_STRING(u8x8.getCols() - 1, 0*LINE_HEIGHT, "~"); | ||||
|       drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // home icon | ||||
|       String ssidString = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); | ||||
|       drawString(1, 0, ssidString.c_str()); | ||||
|       // Print `~` char to indicate that SSID is longer, than our display | ||||
|       if (knownSsid.length() > getCols()) { | ||||
|         drawString(getCols() - 1, 0, "~"); | ||||
|       } | ||||
|  | ||||
|       // Second row with IP or Psssword | ||||
|       drawGlyph(0, lineHeight, 68, u8x8_font_open_iconic_embedded_1x1); // wifi icon | ||||
|       // Print password in AP mode and if led is OFF. | ||||
|       if (apActive && bri == 0) { | ||||
|         u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, apPass); | ||||
|       } | ||||
|       else { | ||||
|         String ipString = knownIp.toString(); | ||||
|         u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, ipString.c_str()); | ||||
|         drawString(1, lineHeight, apPass); | ||||
|       } else { | ||||
|         // alternate IP address and server name | ||||
|         String secondLine = knownIp.toString(); | ||||
|         if (showName && strcmp(serverDescription, "WLED") != 0) { | ||||
|           secondLine = serverDescription; | ||||
|         } | ||||
|         for (uint8_t i=secondLine.length(); i<getCols()-1; i++) secondLine += ' '; | ||||
|         drawString(1, lineHeight, secondLine.c_str()); | ||||
|       } | ||||
|  | ||||
|       // Third row with mode name | ||||
|       showCurrentEffectOrPalette(modes_qstrings[knownMode], 2); | ||||
|       // draw third and fourth row | ||||
|       drawLine(2, clockMode ? lineType : FLD_LINE_MODE); | ||||
|       drawLine(3, clockMode ? FLD_LINE_TIME : lineType); | ||||
|  | ||||
|       switch(lineThreeType) { | ||||
|         case FLD_LINE_3_BRIGHTNESS: | ||||
|           sprintf(lineBuffer, "Brightness %d", bri); | ||||
|           u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); | ||||
|       drawGlyph(0, 2*lineHeight, 66 + (bri > 0 ? 3 : 0), u8x8_font_open_iconic_weather_2x2); // sun/moon icon | ||||
|       //if (markLineNum>1) drawGlyph(2, markLineNum*lineHeight, 66, u8x8_font_open_iconic_arrow_1x1); // arrow icon | ||||
|     } | ||||
|  | ||||
|     void drawLine(uint8_t line, Line4Type lineType) { | ||||
|       char lineBuffer[LINE_BUFFER_SIZE]; | ||||
|       switch(lineType) { | ||||
|         case FLD_LINE_BRIGHTNESS: | ||||
|           sprintf_P(lineBuffer, PSTR("Brightness %3d"), bri); | ||||
|           drawString(2, line*lineHeight, lineBuffer); | ||||
|           break; | ||||
|         case FLD_LINE_3_EFFECT_SPEED: | ||||
|           sprintf(lineBuffer, "FX Speed %d", effectSpeed); | ||||
|           u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); | ||||
|         case FLD_LINE_EFFECT_SPEED: | ||||
|           sprintf_P(lineBuffer, PSTR("FX Speed   %3d"), effectSpeed); | ||||
|           drawString(2, line*lineHeight, lineBuffer); | ||||
|           break; | ||||
|         case FLD_LINE_3_EFFECT_INTENSITY: | ||||
|           sprintf(lineBuffer, "FX Intense %d", effectIntensity); | ||||
|           u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); | ||||
|         case FLD_LINE_EFFECT_INTENSITY: | ||||
|           sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity); | ||||
|           drawString(2, line*lineHeight, lineBuffer); | ||||
|           break; | ||||
|         case FLD_LINE_3_PALETTE: | ||||
|           showCurrentEffectOrPalette(palettes_qstrings[knownPalette], 3); | ||||
|         case FLD_LINE_MODE: | ||||
|           showCurrentEffectOrPalette(knownMode, JSON_mode_names, line); | ||||
|           break; | ||||
|         case FLD_LINE_PALETTE: | ||||
|           showCurrentEffectOrPalette(knownPalette, JSON_palette_names, line); | ||||
|           break; | ||||
|         case FLD_LINE_TIME: | ||||
|           showTime(false); | ||||
|           break; | ||||
|         default: | ||||
|           // unknown type, do nothing | ||||
|           break; | ||||
|       } | ||||
|  | ||||
|       u8x8.setFont(u8x8_font_open_iconic_arrow_1x1); | ||||
|       u8x8.DRAW_GLYPH(0, markLineNum*LINE_HEIGHT, 66); // arrow icon | ||||
|  | ||||
|       u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); | ||||
|       u8x8.DRAW_GLYPH(0, 0*LINE_HEIGHT, 80); // wifi icon | ||||
|       u8x8.DRAW_GLYPH(0, 1*LINE_HEIGHT, 68); // home icon | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display the current effect or palette (desiredEntry)  | ||||
|      * on the appropriate line (row). | ||||
|      *  | ||||
|      * TODO: Should we cache the current effect and  | ||||
|      * TODO: palette name? This seems expensive. | ||||
|      */ | ||||
|     void showCurrentEffectOrPalette(char *qstring, uint8_t row) { | ||||
|       uint8_t printedChars = 1; | ||||
|     void showCurrentEffectOrPalette(int knownMode, const char *qstring, uint8_t row) { | ||||
|       char lineBuffer[LINE_BUFFER_SIZE]; | ||||
|       uint8_t qComma = 0; | ||||
|       bool insideQuotes = false; | ||||
|       uint8_t printedChars = 0; | ||||
|       char singleJsonSymbol; | ||||
|       int i = 0; | ||||
|       while (true) { | ||||
|  | ||||
|       // Find the mode name in JSON | ||||
|       for (size_t i = 0; i < strlen_P(qstring); i++) { | ||||
|         singleJsonSymbol = pgm_read_byte_near(qstring + i); | ||||
|         if (singleJsonSymbol == '"' || singleJsonSymbol == '\0' ) { | ||||
|           break; | ||||
|         if (singleJsonSymbol == '\0') break; | ||||
|         switch (singleJsonSymbol) { | ||||
|           case '"': | ||||
|             insideQuotes = !insideQuotes; | ||||
|             break; | ||||
|           case '[': | ||||
|           case ']': | ||||
|             break; | ||||
|           case ',': | ||||
|             qComma++; | ||||
|           default: | ||||
|             if (!insideQuotes || (qComma != knownMode)) break; | ||||
|             lineBuffer[printedChars++] = singleJsonSymbol; | ||||
|         } | ||||
|         u8x8.DRAW_GLYPH(printedChars, row * LINE_HEIGHT, singleJsonSymbol); | ||||
|         printedChars++; | ||||
|         if ( (printedChars > u8x8.getCols() - 2)) { | ||||
|           break; | ||||
|         } | ||||
|         i++; | ||||
|         if ((qComma > knownMode) || (printedChars >= getCols()-2) || printedChars >= sizeof(lineBuffer)-2) break; | ||||
|       } | ||||
|       for (;printedChars < getCols()-2 && printedChars < sizeof(lineBuffer)-2; printedChars++) lineBuffer[printedChars]=' '; | ||||
|       lineBuffer[printedChars] = 0; | ||||
|       drawString(2, row*lineHeight, lineBuffer); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -324,6 +438,7 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|      * to wake up the screen. | ||||
|      */ | ||||
|     bool wakeDisplay() { | ||||
|       knownHour = 99; | ||||
|       if (displayTurnedOff) { | ||||
|         // Turn the display back on | ||||
|         sleepOrClock(false); | ||||
| @@ -345,36 +460,14 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|       } | ||||
|  | ||||
|       // Print the overlay | ||||
|       u8x8.clear(); | ||||
|       u8x8.setFont(u8x8_font_chroma48medium8_r); | ||||
|       if (line1) { | ||||
|         u8x8.DRAW_STRING(0, 1*LINE_HEIGHT, line1); | ||||
|       } | ||||
|       if (line2) { | ||||
|         u8x8.DRAW_STRING(0, 2*LINE_HEIGHT, line2); | ||||
|       } | ||||
|       clear(); | ||||
|       if (line1) drawString(0, 1*lineHeight, line1); | ||||
|       if (line2) drawString(0, 2*lineHeight, line2); | ||||
|       overlayUntil = millis() + showHowLong; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Specify what data should be defined on line 3 | ||||
|      * (the last line). | ||||
|      */ | ||||
|     void setLineThreeType(byte newLineThreeType) { | ||||
|       if (newLineThreeType == FLD_LINE_3_BRIGHTNESS ||  | ||||
|           newLineThreeType == FLD_LINE_3_EFFECT_SPEED ||  | ||||
|           newLineThreeType == FLD_LINE_3_EFFECT_INTENSITY ||  | ||||
|           newLineThreeType == FLD_LINE_3_PALETTE) { | ||||
|         lineThreeType = newLineThreeType; | ||||
|       } | ||||
|       else { | ||||
|         // Unknown value. | ||||
|         lineThreeType = FLD_LINE_3_BRIGHTNESS;  | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Line 2 or 3 (last two lines) can be marked with an | ||||
|      * Line 3 or 4 (last two lines) can be marked with an | ||||
|      * arrow in the first column. Pass 2 or 3 to this to | ||||
|      * specify which line to mark with an arrow. | ||||
|      * Any other values are ignored. | ||||
| @@ -388,42 +481,16 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * 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 | ||||
|     } | ||||
|     */ | ||||
|  | ||||
|     /** | ||||
|      * Enable sleep (turn the display off) or clock mode. | ||||
|      */ | ||||
|     void sleepOrClock(bool enabled) { | ||||
|       if (enabled) { | ||||
|         if (CLOCK_MODE_ENABLED) { | ||||
|           showTime(); | ||||
|         } | ||||
|         else { | ||||
|           u8x8.setPowerSave(1); | ||||
|         } | ||||
|         if (clockMode) showTime(); | ||||
|         else           setPowerSave(1); | ||||
|         displayTurnedOff = true; | ||||
|       } | ||||
|       else { | ||||
|         if (!CLOCK_MODE_ENABLED) { | ||||
|           u8x8.setPowerSave(0); | ||||
|         } | ||||
|       } else { | ||||
|         setPowerSave(0); | ||||
|         displayTurnedOff = false; | ||||
|       } | ||||
|     } | ||||
| @@ -433,23 +500,28 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|      * on the middle rows. Based 24 or 12 hour depending on | ||||
|      * the useAMPM configuration. | ||||
|      */ | ||||
|     void showTime() { | ||||
|     void showTime(bool fullScreen = true) { | ||||
|       char lineBuffer[LINE_BUFFER_SIZE]; | ||||
|  | ||||
|       updateLocalTime(); | ||||
|       byte minuteCurrent = minute(localTime); | ||||
|       byte hourCurrent = hour(localTime); | ||||
|       byte hourCurrent   = hour(localTime); | ||||
|       byte secondCurrent = second(localTime); | ||||
|       if (knownMinute == minuteCurrent && knownHour == hourCurrent) { | ||||
|         // Time hasn't changed. | ||||
|         return; | ||||
|         if (!fullScreen) return; | ||||
|       } else { | ||||
|         //if (fullScreen) clear(); | ||||
|       } | ||||
|       knownMinute = minuteCurrent; | ||||
|       knownHour = hourCurrent; | ||||
|  | ||||
|       u8x8.clear(); | ||||
|       u8x8.setFont(u8x8_font_chroma48medium8_r); | ||||
|  | ||||
|       int currentMonth = month(localTime); | ||||
|       sprintf(lineBuffer, "%s %d", monthShortStr(currentMonth), day(localTime)); | ||||
|       u8x8.DRAW_BIG_STRING(DATE_INDENT, TIME_LINE*LINE_HEIGHT, lineBuffer); | ||||
|       byte currentMonth = month(localTime); | ||||
|       sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(currentMonth), day(localTime)); | ||||
|       if (fullScreen) | ||||
|         draw2x2String(DATE_INDENT, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays | ||||
|       else | ||||
|         drawString(2, lineHeight*3, lineBuffer); | ||||
|  | ||||
|       byte showHour = hourCurrent; | ||||
|       boolean isAM = false; | ||||
| @@ -467,25 +539,46 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       sprintf(lineBuffer, "%02d:%02d %s", showHour, minuteCurrent, useAMPM ? (isAM ? "AM" : "PM") : ""); | ||||
|       sprintf_P(lineBuffer, (secondCurrent%2 || !fullScreen) ? PSTR("%2d:%02d") : PSTR("%2d %02d"), (useAMPM ? showHour : hourCurrent), minuteCurrent); | ||||
|       // For time, we always use LINE_HEIGHT of 2 since | ||||
|       // we are printing it big. | ||||
|       u8x8.DRAW_BIG_STRING(TIME_INDENT + (useAMPM ? 0 : 2), (TIME_LINE + 1) * 2, lineBuffer); | ||||
|       if (fullScreen) { | ||||
|         draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer); | ||||
|         sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); | ||||
|         if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*2, (isAM ? "AM" : "PM"), true); | ||||
|         else         drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line | ||||
|       } else { | ||||
|         drawString(9+(useAMPM?0:2), lineHeight*3, lineBuffer); | ||||
|         if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*3, (isAM ? "AM" : "PM"), true); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. | ||||
|      * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. | ||||
|      * Below it is shown how this could be used for e.g. a light sensor | ||||
|      */ | ||||
|     //void addToJsonInfo(JsonObject& root) { | ||||
|       //JsonObject user = root["u"]; | ||||
|       //if (user.isNull()) user = root.createNestedObject("u"); | ||||
|       //JsonArray data = user.createNestedArray(F("4LineDisplay")); | ||||
|       //data.add(F("Loaded.")); | ||||
|     //} | ||||
|  | ||||
|     /* | ||||
|      * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). | ||||
|      * Values in the state object may be modified by connected clients | ||||
|      */ | ||||
|     void addToJsonState(JsonObject& root) { | ||||
|     } | ||||
|     //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) { | ||||
|     } | ||||
|     //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. | ||||
| @@ -502,6 +595,18 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|      * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! | ||||
|      */ | ||||
|     void addToConfig(JsonObject& root) { | ||||
|       JsonObject top    = root.createNestedObject(FPSTR(_name)); | ||||
|       JsonArray i2c_pin = top.createNestedArray("pin"); | ||||
|       i2c_pin.add(sclPin); | ||||
|       i2c_pin.add(sdaPin); | ||||
|       top["type"]                = type; | ||||
|       top[FPSTR(_flip)]          = (bool) flip; | ||||
|       top[FPSTR(_contrast)]      = contrast; | ||||
|       top[FPSTR(_refreshRate)]   = refreshRate/1000; | ||||
|       top[FPSTR(_screenTimeOut)] = screenTimeout/1000; | ||||
|       top[FPSTR(_sleepMode)]     = (bool) sleepMode; | ||||
|       top[FPSTR(_clockMode)]     = (bool) clockMode; | ||||
|       DEBUG_PRINTLN(F("4 Line Display config saved.")); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
| @@ -512,7 +617,58 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|      * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. | ||||
|      * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) | ||||
|      */ | ||||
|     void readFromConfig(JsonObject& root) { | ||||
|     bool readFromConfig(JsonObject& root) { | ||||
|       bool needsRedraw    = false; | ||||
|       DisplayType newType = type; | ||||
|       int8_t newScl       = sclPin; | ||||
|       int8_t newSda       = sdaPin; | ||||
|  | ||||
|       JsonObject top = root[FPSTR(_name)]; | ||||
|       if (top.isNull()) { | ||||
|         DEBUG_PRINT(FPSTR(_name)); | ||||
|         DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       newScl        = top["pin"][0] | newScl; | ||||
|       newSda        = top["pin"][1] | newSda; | ||||
|       newType       = top["type"] | newType; | ||||
|       flip          = top[FPSTR(_flip)] | flip; | ||||
|       contrast      = top[FPSTR(_contrast)] | contrast; | ||||
|       refreshRate   = (top[FPSTR(_refreshRate)] | refreshRate/1000) * 1000; | ||||
|       screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; | ||||
|       sleepMode     = top[FPSTR(_sleepMode)] | sleepMode; | ||||
|       clockMode     = top[FPSTR(_clockMode)] | clockMode; | ||||
|  | ||||
|       DEBUG_PRINT(FPSTR(_name)); | ||||
|       if (!initDone) { | ||||
|         // first run: reading from cfg.json | ||||
|         sclPin = newScl; | ||||
|         sdaPin = newSda; | ||||
|         type = newType; | ||||
|         DEBUG_PRINTLN(F(" config loaded.")); | ||||
|       } else { | ||||
|         // changing parameters from settings page | ||||
|         if (sclPin!=newScl || sdaPin!=newSda || type!=newType) { | ||||
|           if (type != NONE) delete (static_cast<U8X8*>(u8x8)); | ||||
|           pinManager.deallocatePin(sclPin); | ||||
|           pinManager.deallocatePin(sdaPin); | ||||
|           sclPin = newScl; | ||||
|           sdaPin = newSda; | ||||
|           if (newScl<0 || newSda<0) { | ||||
|             type = NONE; | ||||
|             return true; | ||||
|           } else type = newType; | ||||
|           setup(); | ||||
|           needsRedraw |= true; | ||||
|         } | ||||
|         setContrast(contrast); | ||||
|         setFlipMode(flip); | ||||
|         if (needsRedraw && !wakeDisplay()) redraw(true); | ||||
|         DEBUG_PRINTLN(F(" config (re)loaded.")); | ||||
|       } | ||||
|       // use "return !top["newestParameter"].isNull();" when updating Usermod with new features | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     /* | ||||
| @@ -522,5 +678,13 @@ class FourLineDisplayUsermod : public Usermod { | ||||
|     uint16_t getId() { | ||||
|       return USERMOD_ID_FOUR_LINE_DISP; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| }; | ||||
| // strings to reduce flash memory usage (used more than twice) | ||||
| const char FourLineDisplayUsermod::_name[]          PROGMEM = "4LineDisplay"; | ||||
| const char FourLineDisplayUsermod::_contrast[]      PROGMEM = "contrast"; | ||||
| const char FourLineDisplayUsermod::_refreshRate[]   PROGMEM = "refreshRateSec"; | ||||
| const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec"; | ||||
| const char FourLineDisplayUsermod::_flip[]          PROGMEM = "flip"; | ||||
| const char FourLineDisplayUsermod::_sleepMode[]     PROGMEM = "sleepMode"; | ||||
| const char FourLineDisplayUsermod::_clockMode[]     PROGMEM = "clockMode"; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ build_unflags = ${common.build_unflags} | ||||
| build_flags = | ||||
|     ${common.build_flags_esp32}  | ||||
|     -D USERMOD_MODE_SORT | ||||
|     -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 | ||||
|     -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 | ||||
|     -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19   | ||||
|     -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 | ||||
|     -D LEDPIN=16 -D BTNPIN=13 | ||||
| @@ -28,7 +28,7 @@ build_unflags = ${common.build_unflags} | ||||
| build_flags = | ||||
|     ${common.build_flags_esp8266}  | ||||
|     -D USERMOD_MODE_SORT | ||||
|     -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 | ||||
|     -D USERMOD_FOUR_LINE_DISPLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 | ||||
|     -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=12 -D ENCODER_CLK_PIN=14 -D ENCODER_SW_PIN=13 | ||||
|     -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 | ||||
|     -D LEDPIN=3 -D BTNPIN=0 | ||||
|   | ||||
| @@ -16,7 +16,7 @@ This file should be placed in the same directory as `platformio.ini`. | ||||
| ### Define Your Options | ||||
|  | ||||
| * `USERMOD_ROTARY_ENCODER_UI`  - define this to have this user mod included wled00\usermods_list.cpp | ||||
| * `USERMOD_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) | ||||
| * `USERMOD_FOUR_LINE_DISPLAY`  - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) | ||||
| * `ENCODER_DT_PIN`             - The encoders DT pin, defaults to 12 | ||||
| * `ENCODER_CLK_PIN`            - The encoders CLK pin, defaults to 14 | ||||
| * `ENCODER_SW_PIN`             - The encoders SW pin, defaults to 13 | ||||
|   | ||||
| @@ -37,7 +37,7 @@ | ||||
| #define ENCODER_SW_PIN 13 | ||||
| #endif | ||||
|  | ||||
| #ifndef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifndef USERMOD_FOUR_LINE_DISPLAY | ||||
| // These constants won't be defined if we aren't using FourLineDisplay. | ||||
| #define FLD_LINE_3_BRIGHTNESS       0 | ||||
| #define FLD_LINE_3_EFFECT_SPEED     0 | ||||
| @@ -62,7 +62,7 @@ private: | ||||
|   unsigned char button_state = HIGH; | ||||
|   unsigned char prev_button_state = HIGH; | ||||
|    | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|   FourLineDisplayUsermod *display; | ||||
| #else | ||||
|   void* display = nullptr; | ||||
| @@ -96,7 +96,7 @@ public: | ||||
|     modes_alpha_indexes = modeSortUsermod->getModesAlphaIndexes(); | ||||
|     palettes_alpha_indexes = modeSortUsermod->getPalettesAlphaIndexes(); | ||||
|  | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY     | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY     | ||||
|     // This Usermod uses FourLineDisplayUsermod for the best experience. | ||||
|     // But it's optional. But you want it. | ||||
|     display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); | ||||
| @@ -248,7 +248,7 @@ public: | ||||
|   } | ||||
|  | ||||
|   boolean changeState(const char *stateName, byte lineThreeMode, byte markedLine) { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|     if (display != nullptr) { | ||||
|       if (display->wakeDisplay()) { | ||||
|         // Throw away wake up input | ||||
| @@ -272,7 +272,7 @@ public: | ||||
|   } | ||||
|  | ||||
|   void changeBrightness(bool increase) { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|     if (display && display->wakeDisplay()) { | ||||
|       // Throw away wake up input | ||||
|       return; | ||||
| @@ -288,7 +288,7 @@ public: | ||||
|   } | ||||
|  | ||||
|   void changeEffect(bool increase) { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|     if (display && display->wakeDisplay()) { | ||||
|       // Throw away wake up input | ||||
|       return; | ||||
| @@ -305,7 +305,7 @@ public: | ||||
|   } | ||||
|  | ||||
|   void changeEffectSpeed(bool increase) { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|     if (display && display->wakeDisplay()) { | ||||
|       // Throw away wake up input | ||||
|       return; | ||||
| @@ -321,7 +321,7 @@ public: | ||||
|   } | ||||
|  | ||||
|   void changeEffectIntensity(bool increase) { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|     if (display && display->wakeDisplay()) { | ||||
|       // Throw away wake up input | ||||
|       return; | ||||
| @@ -337,7 +337,7 @@ public: | ||||
|   } | ||||
|  | ||||
|   void changePalette(bool increase) { | ||||
| #ifdef USERMOD_FOUR_LINE_DISLAY | ||||
| #ifdef USERMOD_FOUR_LINE_DISPLAY | ||||
|     if (display && display->wakeDisplay()) { | ||||
|       // Throw away wake up input | ||||
|       return; | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										176
									
								
								wled00/FX.cpp
									
									
									
									
									
								
							
							
						
						
									
										176
									
								
								wled00/FX.cpp
									
									
									
									
									
								
							| @@ -25,7 +25,6 @@ | ||||
| */ | ||||
|  | ||||
| #include "FX.h" | ||||
| #include "tv_colors.h" | ||||
|  | ||||
| #define IBN 5100 | ||||
| #define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) | ||||
| @@ -35,7 +34,7 @@ | ||||
|  */ | ||||
| uint16_t WS2812FX::mode_static(void) { | ||||
|   fill(SEGCOLOR(0)); | ||||
|   return (SEGMENT.getOption(SEG_OPTION_TRANSITIONAL)) ? FRAMETIME : 500; //update faster if in transition | ||||
|   return (SEGMENT.getOption(SEG_OPTION_TRANSITIONAL)) ? FRAMETIME : 350; //update faster if in transition | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -43,30 +42,24 @@ uint16_t WS2812FX::mode_static(void) { | ||||
|  * Blink/strobe function | ||||
|  * Alternate between color1 and color2 | ||||
|  * if(strobe == true) then create a strobe effect | ||||
|  * NOTE: Maybe re-rework without timer | ||||
|  */ | ||||
| uint16_t WS2812FX::blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { | ||||
|   uint16_t stateTime = SEGENV.aux1; | ||||
|   uint32_t cycleTime = (255 - SEGMENT.speed)*20; | ||||
|   uint32_t onTime = 0; | ||||
|   uint32_t offTime = cycleTime; | ||||
|  | ||||
|   if (!strobe) { | ||||
|     onTime = (cycleTime * SEGMENT.intensity) >> 8; | ||||
|     offTime = cycleTime - onTime; | ||||
|   uint32_t onTime = FRAMETIME; | ||||
|   if (!strobe) onTime += ((cycleTime * SEGMENT.intensity) >> 8); | ||||
|   cycleTime += FRAMETIME*2; | ||||
|   uint32_t it = now / cycleTime; | ||||
|   uint32_t rem = now % cycleTime; | ||||
|    | ||||
|   bool on = false; | ||||
|   if (it != SEGENV.step //new iteration, force on state for one frame, even if set time is too brief | ||||
|       || rem <= onTime) {  | ||||
|     on = true; | ||||
|   } | ||||
|    | ||||
|   stateTime = ((SEGENV.aux0 & 1) == 0) ? onTime : offTime; | ||||
|   stateTime += 20; | ||||
|      | ||||
|   if (now - SEGENV.step > stateTime) | ||||
|   { | ||||
|     SEGENV.aux0++; | ||||
|     SEGENV.aux1 = stateTime; | ||||
|     SEGENV.step = now; | ||||
|   } | ||||
|   SEGENV.step = it; //save previous iteration | ||||
|  | ||||
|   uint32_t color = ((SEGENV.aux0 & 1) == 0) ? color1 : color2; | ||||
|   uint32_t color = on ? color1 : color2; | ||||
|   if (color == color1 && do_palette) | ||||
|   { | ||||
|     for(uint16_t i = 0; i < SEGLEN; i++) { | ||||
| @@ -445,28 +438,45 @@ uint16_t WS2812FX::mode_theater_chase_rainbow(void) { | ||||
| /* | ||||
|  * Running lights effect with smooth sine transition base. | ||||
|  */ | ||||
| uint16_t WS2812FX::running_base(bool saw) { | ||||
| uint16_t WS2812FX::running_base(bool saw, bool dual=false) { | ||||
|   uint8_t x_scale = SEGMENT.intensity >> 2; | ||||
|   uint32_t counter = (now * SEGMENT.speed) >> 9; | ||||
|  | ||||
|   for(uint16_t i = 0; i < SEGLEN; i++) { | ||||
|     uint8_t s = 0; | ||||
|     uint8_t a = i*x_scale - counter; | ||||
|     uint16_t a = i*x_scale - counter; | ||||
|     if (saw) { | ||||
|       a &= 0xFF; | ||||
|       if (a < 16) | ||||
|       { | ||||
|         a = 192 + a*8; | ||||
|       } else { | ||||
|         a = map(a,16,255,64,192); | ||||
|       } | ||||
|       a = 255 - a; | ||||
|     } | ||||
|     s = sin8(a); | ||||
|     setPixelColor(i, color_blend(color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), s)); | ||||
|     uint8_t s = dual ? sin_gap(a) : sin8(a); | ||||
|     uint32_t ca = color_blend(SEGCOLOR(1), color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s); | ||||
|     if (dual) { | ||||
|       uint16_t b = (SEGLEN-1-i)*x_scale - counter; | ||||
|       uint8_t t = sin_gap(b); | ||||
|       uint32_t cb = color_blend(SEGCOLOR(1), color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); | ||||
|       ca = color_blend(ca, cb, 127); | ||||
|     } | ||||
|     setPixelColor(i, ca); | ||||
|   } | ||||
|   return FRAMETIME; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Running lights in opposite directions. | ||||
|  * Idea: Make the gap width controllable with a third slider in the future | ||||
|  */ | ||||
| uint16_t WS2812FX::mode_running_dual(void) { | ||||
|   return running_base(false, true); | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Running lights effect with smooth sine transition. | ||||
|  */ | ||||
| @@ -1335,14 +1345,6 @@ uint16_t WS2812FX::tricolor_chase(uint32_t color1, uint32_t color2) { | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Alternating white/red/black pixels running. PLACEHOLDER | ||||
|  */ | ||||
| uint16_t WS2812FX::mode_circus_combustus(void) { | ||||
|   return tricolor_chase(RED, WHITE); | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Tricolor chase mode | ||||
|  */ | ||||
| @@ -1459,8 +1461,8 @@ uint16_t WS2812FX::mode_tricolor_fade(void) | ||||
|   } | ||||
|  | ||||
|   byte stp = prog; // % 256 | ||||
|   uint32_t color = 0; | ||||
|   for(uint16_t i = 0; i < SEGLEN; i++) { | ||||
|     uint32_t color; | ||||
|     if (stage == 2) { | ||||
|       color = color_blend(color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp); | ||||
|     } else if (stage == 1) { | ||||
| @@ -3077,11 +3079,11 @@ uint16_t WS2812FX::mode_drip(void) | ||||
|  | ||||
|   numDrops = 1 + (SEGMENT.intensity >> 6); | ||||
|  | ||||
|   float gravity = -0.001 - (SEGMENT.speed/50000.0); | ||||
|   float gravity = -0.0005 - (SEGMENT.speed/50000.0); | ||||
|   gravity *= SEGLEN; | ||||
|   int sourcedrop = 12; | ||||
|  | ||||
|   for (int j=0;j<numDrops;j++) { | ||||
|   for (uint8_t j=0;j<numDrops;j++) { | ||||
|     if (drops[j].colIndex == 0) { //init | ||||
|       drops[j].pos = SEGLEN-1;    // start at end | ||||
|       drops[j].vel = 0;           // speed | ||||
| @@ -3092,7 +3094,7 @@ uint16_t WS2812FX::mode_drip(void) | ||||
|     setPixelColor(SEGLEN-1,color_blend(BLACK,SEGCOLOR(0), sourcedrop));// water source | ||||
|     if (drops[j].colIndex==1) { | ||||
|       if (drops[j].col>255) drops[j].col=255; | ||||
|       setPixelColor(int(drops[j].pos),color_blend(BLACK,SEGCOLOR(0),drops[j].col)); | ||||
|       setPixelColor(uint16_t(drops[j].pos),color_blend(BLACK,SEGCOLOR(0),drops[j].col)); | ||||
|        | ||||
|       drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling | ||||
|        | ||||
| @@ -3107,8 +3109,9 @@ uint16_t WS2812FX::mode_drip(void) | ||||
|         if (drops[j].pos < 0) drops[j].pos = 0; | ||||
|         drops[j].vel += gravity; | ||||
|  | ||||
|         for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets | ||||
|           setPixelColor(int(drops[j].pos)+i,color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling | ||||
|         for (uint16_t i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets | ||||
|           uint16_t pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally | ||||
|           setPixelColor(pos,color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling | ||||
|         } | ||||
|          | ||||
|         if (drops[j].colIndex > 2) {       // during bounce, some water is on the floor | ||||
| @@ -3822,65 +3825,99 @@ uint16_t WS2812FX::mode_blends(void) { | ||||
|   return FRAMETIME; | ||||
| } | ||||
|  | ||||
| #ifndef WLED_DISABLE_FX_HIGH_FLASH_USE | ||||
| typedef struct TvSim { | ||||
|   uint32_t totalTime = 0; | ||||
|   uint32_t fadeTime  = 0; | ||||
|   uint32_t startTime = 0; | ||||
|   uint32_t elapsed   = 0; | ||||
|   uint32_t pixelNum  = 0; | ||||
|   uint16_t sliderValues = 0; | ||||
|   uint32_t sceeneStart    = 0; | ||||
|   uint32_t sceeneDuration = 0; | ||||
|   uint16_t sceeneColorHue = 0; | ||||
|   uint8_t  sceeneColorSat = 0; | ||||
|   uint8_t  sceeneColorBri = 0; | ||||
|   uint8_t  actualColorR = 0; | ||||
|   uint8_t  actualColorG = 0; | ||||
|   uint8_t  actualColorB = 0; | ||||
|   uint16_t pr = 0; // Prev R, G, B | ||||
|   uint16_t pg = 0; | ||||
|   uint16_t pb = 0; | ||||
| } tvSim; | ||||
|  | ||||
| #define  numTVPixels (sizeof(tv_colors) / 2)  // 2 bytes per Pixel (5/6/5) | ||||
| #endif | ||||
|  | ||||
| /* | ||||
|   TV Simulator | ||||
|   Modified and adapted to WLED by Def3nder, based on "Fake TV Light for Engineers" by Phillip Burgess https://learn.adafruit.com/fake-tv-light-for-engineers/arduino-sketch | ||||
| */ | ||||
| uint16_t WS2812FX::mode_tv_simulator(void) { | ||||
|   #ifdef WLED_DISABLE_FX_HIGH_FLASH_USE | ||||
|   return mode_static(); | ||||
|   #else | ||||
|   uint16_t nr, ng, nb, r, g, b, i; | ||||
|   uint8_t  hi, lo, r8, g8, b8; | ||||
|   uint16_t nr, ng, nb, r, g, b, i, hue; | ||||
|   uint8_t  sat, bri, j; | ||||
|  | ||||
|   if (!SEGENV.allocateData(sizeof(tvSim))) return mode_static(); //allocation failed | ||||
|   TvSim* tvSimulator = reinterpret_cast<TvSim*>(SEGENV.data); | ||||
|  | ||||
|   // initialize start of the TV-Colors | ||||
|   if (SEGENV.call == 0) {  | ||||
|     tvSimulator->pixelNum = ((uint8_t)random(18)) * numTVPixels / 18; // Begin at random movie (18 in total) | ||||
|   uint8_t colorSpeed     = map(SEGMENT.speed,     0, UINT8_MAX,  1, 20); | ||||
|   uint8_t colorIntensity = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 30); | ||||
|  | ||||
|   i = SEGMENT.speed << 8 | SEGMENT.intensity; | ||||
|   if (i != tvSimulator->sliderValues) { | ||||
|     tvSimulator->sliderValues = i; | ||||
|     SEGENV.aux1 = 0; | ||||
|   } | ||||
|  | ||||
|   // Read next 16-bit (5/6/5) color | ||||
|   hi = pgm_read_byte(&tv_colors[tvSimulator->pixelNum * 2    ]); | ||||
|   lo = pgm_read_byte(&tv_colors[tvSimulator->pixelNum * 2 + 1]); | ||||
|     // create a new sceene | ||||
|     if (((millis() - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { | ||||
|       tvSimulator->sceeneStart    = millis();                                               // remember the start of the new sceene | ||||
|       tvSimulator->sceeneDuration = random16(60* 250* colorSpeed, 60* 750 * colorSpeed);    // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) | ||||
|       tvSimulator->sceeneColorHue = random16(   0, 768);                                    // random start color-tone for the sceene | ||||
|       tvSimulator->sceeneColorSat = random8 ( 100, 130 + colorIntensity);                   // random start color-saturation for the sceene | ||||
|       tvSimulator->sceeneColorBri = random8 ( 200, 240);                                    // random start color-brightness for the sceene | ||||
|       SEGENV.aux1 = 1; | ||||
|       SEGENV.aux0 = 0; | ||||
|     }  | ||||
|      | ||||
|     // slightly change the color-tone in this sceene | ||||
|     if ( SEGENV.aux0 == 0) { | ||||
|       // hue change in both directions | ||||
|       j = random8(4 * colorIntensity); | ||||
|       hue = (random8() < 128) ? ((j < tvSimulator->sceeneColorHue)       ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) :  // negative | ||||
|                                 ((j + tvSimulator->sceeneColorHue) < 767 ? tvSimulator->sceeneColorHue + j : tvSimulator->sceeneColorHue + j - 767) ;  // positive | ||||
|        | ||||
|       // saturation | ||||
|       j = random8(2 * colorIntensity); | ||||
|       sat = (tvSimulator->sceeneColorSat - j) < 0 ? 0 : tvSimulator->sceeneColorSat - j; | ||||
|        | ||||
|       // brightness | ||||
|       j = random8(100); | ||||
|       bri = (tvSimulator->sceeneColorBri - j) < 0 ? 0 : tvSimulator->sceeneColorBri - j; | ||||
|  | ||||
|   // Expand to 24-bit (8/8/8) | ||||
|   r8 = (hi & 0xF8) | (hi >> 5); | ||||
|   g8 = ((hi << 5) & 0xff) | ((lo & 0xE0) >> 3) | ((hi & 0x06) >> 1); | ||||
|   b8 = ((lo << 3) & 0xff) | ((lo & 0x1F) >> 2); | ||||
|  | ||||
|   // Apply gamma correction, further expand to 16/16/16 | ||||
|   nr = (uint8_t)gamma8(r8) * 257; // New R/G/B | ||||
|   ng = (uint8_t)gamma8(g8) * 257; | ||||
|   nb = (uint8_t)gamma8(b8) * 257; | ||||
|       // calculate R,G,B from HSV | ||||
|       // Source: https://blog.adafruit.com/2012/03/14/constant-brightness-hsb-to-rgb-algorithm/ | ||||
|       { // just to create a local scope for  the variables | ||||
|         uint8_t temp[5], n = (hue >> 8) % 3; | ||||
|         uint8_t x = ((((hue & 255) * sat) >> 8) * bri) >> 8; | ||||
|         uint8_t s = (  (256 - sat) * bri) >> 8; | ||||
|         temp[0] = temp[3] =       s; | ||||
|         temp[1] = temp[4] =   x + s; | ||||
|         temp[2] =           bri - x; | ||||
|         tvSimulator->actualColorR = temp[n + 2]; | ||||
|         tvSimulator->actualColorG = temp[n + 1]; | ||||
|         tvSimulator->actualColorB = temp[n    ]; | ||||
|       } | ||||
|     } | ||||
|     // Apply gamma correction, further expand to 16/16/16 | ||||
|     nr = (uint8_t)gamma8(tvSimulator->actualColorR) * 257; // New R/G/B | ||||
|     ng = (uint8_t)gamma8(tvSimulator->actualColorG) * 257; | ||||
|     nb = (uint8_t)gamma8(tvSimulator->actualColorB) * 257; | ||||
|  | ||||
|   if (SEGENV.aux0 == 0) {  // initialize next iteration  | ||||
|     SEGENV.aux0 = 1; | ||||
|      | ||||
|     // increase color-index for next loop | ||||
|     tvSimulator->pixelNum++; | ||||
|     if (tvSimulator->pixelNum >= numTVPixels) tvSimulator->pixelNum = 0; | ||||
|  | ||||
|     // randomize total duration and fade duration for the actual color | ||||
|     tvSimulator->totalTime = random(250, 2500);                   // Semi-random pixel-to-pixel time | ||||
|     tvSimulator->fadeTime  = random(0, tvSimulator->totalTime);   // Pixel-to-pixel transition time | ||||
|     if (random(10) < 3) tvSimulator->fadeTime = 0;                // Force scene cut 30% of time | ||||
|     tvSimulator->totalTime = random16(250, 2500);                   // Semi-random pixel-to-pixel time | ||||
|     tvSimulator->fadeTime  = random16(0, tvSimulator->totalTime);   // Pixel-to-pixel transition time | ||||
|     if (random8(10) < 3) tvSimulator->fadeTime = 0;                 // Force scene cut 30% of time | ||||
|  | ||||
|     tvSimulator->startTime = millis(); | ||||
|   } // end of initialization | ||||
| @@ -3913,7 +3950,6 @@ uint16_t WS2812FX::mode_tv_simulator(void) { | ||||
|   } | ||||
|    | ||||
|   return FRAMETIME; | ||||
|   #endif | ||||
| } | ||||
|  | ||||
| /* | ||||
|   | ||||
							
								
								
									
										31
									
								
								wled00/FX.h
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								wled00/FX.h
									
									
									
									
									
								
							| @@ -48,9 +48,6 @@ | ||||
| #define MAX(a,b) ((a)>(b)?(a):(b)) | ||||
| #endif | ||||
|  | ||||
| /* Disable effects with high flash memory usage (currently TV simulator) - saves 18.5kB */ | ||||
| //#define WLED_DISABLE_FX_HIGH_FLASH_USE | ||||
|  | ||||
| /* Not used in all effects yet */ | ||||
| #define WLED_FPS         42 | ||||
| #define FRAMETIME        (1000/WLED_FPS) | ||||
| @@ -64,7 +61,9 @@ | ||||
|   /* How much data bytes all segments combined may allocate */ | ||||
|   #define MAX_SEGMENT_DATA  2048 | ||||
| #else | ||||
| #ifndef MAX_NUM_SEGMENTS | ||||
|   #define MAX_NUM_SEGMENTS    16 | ||||
| #endif | ||||
|   #define MAX_NUM_TRANSITIONS 16 | ||||
|   #define MAX_SEGMENT_DATA  8192 | ||||
| #endif | ||||
| @@ -78,7 +77,7 @@ | ||||
| #define SEGENV           _segment_runtimes[_segment_index] | ||||
| #define SEGLEN           _virtualSegmentLength | ||||
| #define SEGACT           SEGMENT.stop | ||||
| #define SPEED_FORMULA_L  5 + (50*(255 - SEGMENT.speed))/SEGLEN | ||||
| #define SPEED_FORMULA_L  5U + (50U*(255U - SEGMENT.speed))/SEGLEN | ||||
| #define RESET_RUNTIME    memset(_segment_runtimes, 0, sizeof(_segment_runtimes)) | ||||
|  | ||||
| // some common colors | ||||
| @@ -168,7 +167,7 @@ | ||||
| #define FX_MODE_POLICE_ALL              49 | ||||
| #define FX_MODE_TWO_DOTS                50 | ||||
| #define FX_MODE_TWO_AREAS               51 | ||||
| #define FX_MODE_CIRCUS_COMBUSTUS        52 | ||||
| #define FX_MODE_RUNNING_DUAL            52 | ||||
| #define FX_MODE_HALLOWEEN               53 | ||||
| #define FX_MODE_TRICOLOR_CHASE          54 | ||||
| #define FX_MODE_TRICOLOR_WIPE           55 | ||||
| @@ -246,9 +245,10 @@ class WS2812FX { | ||||
|    | ||||
|   // segment parameters | ||||
|   public: | ||||
|     typedef struct Segment { // 24 bytes | ||||
|     typedef struct Segment { // 25 (28 in memory?) bytes | ||||
|       uint16_t start; | ||||
|       uint16_t stop; //segment invalid if stop == 0 | ||||
|       uint16_t offset; | ||||
|       uint8_t speed; | ||||
|       uint8_t intensity; | ||||
|       uint8_t palette; | ||||
| @@ -505,7 +505,7 @@ class WS2812FX { | ||||
|       _mode[FX_MODE_POLICE_ALL]              = &WS2812FX::mode_police_all; | ||||
|       _mode[FX_MODE_TWO_DOTS]                = &WS2812FX::mode_two_dots; | ||||
|       _mode[FX_MODE_TWO_AREAS]               = &WS2812FX::mode_two_areas; | ||||
|       _mode[FX_MODE_CIRCUS_COMBUSTUS]        = &WS2812FX::mode_circus_combustus; | ||||
|       _mode[FX_MODE_RUNNING_DUAL]            = &WS2812FX::mode_running_dual; | ||||
|       _mode[FX_MODE_HALLOWEEN]               = &WS2812FX::mode_halloween; | ||||
|       _mode[FX_MODE_TRICOLOR_CHASE]          = &WS2812FX::mode_tricolor_chase; | ||||
|       _mode[FX_MODE_TRICOLOR_WIPE]           = &WS2812FX::mode_tricolor_wipe; | ||||
| @@ -584,7 +584,7 @@ class WS2812FX { | ||||
|     } | ||||
|  | ||||
|     void | ||||
|       finalizeInit(uint16_t countPixels, bool skipFirst), | ||||
|       finalizeInit(uint16_t countPixels), | ||||
|       service(void), | ||||
|       blur(uint8_t), | ||||
|       fill(uint32_t), | ||||
| @@ -609,6 +609,7 @@ class WS2812FX { | ||||
|  | ||||
|     bool | ||||
|       isRgbw = false, | ||||
|       isOffRefreshRequred = false, //periodic refresh is required for the strip to remain off. | ||||
|       gammaCorrectBri = false, | ||||
|       gammaCorrectCol = true, | ||||
|       applyToAllSelected = true, | ||||
| @@ -636,6 +637,7 @@ class WS2812FX { | ||||
|       getColorOrder(void), | ||||
|       gamma8(uint8_t), | ||||
|       gamma8_cal(uint8_t, float), | ||||
|       sin_gap(uint16_t), | ||||
|       get_random_wheel_index(uint8_t); | ||||
|  | ||||
|     int8_t | ||||
| @@ -729,7 +731,7 @@ class WS2812FX { | ||||
|       mode_police_all(void), | ||||
|       mode_two_dots(void), | ||||
|       mode_two_areas(void), | ||||
|       mode_circus_combustus(void), | ||||
|       mode_running_dual(void), | ||||
|       mode_bicolor_chase(void), | ||||
|       mode_tricolor_chase(void), | ||||
|       mode_tricolor_wipe(void), | ||||
| @@ -802,7 +804,7 @@ class WS2812FX { | ||||
|     CRGBPalette16 currentPalette; | ||||
|     CRGBPalette16 targetPalette; | ||||
|  | ||||
|     uint16_t _length, _lengthRaw, _virtualSegmentLength; | ||||
|     uint16_t _length, _virtualSegmentLength; | ||||
|     uint16_t _rand16seed; | ||||
|     uint8_t _brightness; | ||||
|     uint16_t _usedSegmentData = 0; | ||||
| @@ -814,7 +816,6 @@ class WS2812FX { | ||||
|     void handle_palette(void); | ||||
|  | ||||
|     bool | ||||
|       _skipFirstMode, | ||||
|       _triggered; | ||||
|  | ||||
|     mode_ptr _mode[MODE_COUNT]; // SRAM footprint: 4 bytes per element | ||||
| @@ -829,7 +830,7 @@ class WS2812FX { | ||||
|       dynamic(bool), | ||||
|       scan(bool), | ||||
|       theater_chase(uint32_t, uint32_t, bool), | ||||
|       running_base(bool), | ||||
|       running_base(bool,bool), | ||||
|       larson_scanner(bool), | ||||
|       sinelon_base(bool,bool), | ||||
|       dissolve(uint32_t), | ||||
| @@ -863,8 +864,8 @@ class WS2812FX { | ||||
|     uint8_t _segment_index = 0; | ||||
|     uint8_t _segment_index_palette_last = 99; | ||||
|     segment _segments[MAX_NUM_SEGMENTS] = { // SRAM footprint: 24 bytes per element | ||||
|       // start, stop, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[] | ||||
|       { 0, 7, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}} | ||||
|       // start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[] | ||||
|       {0, 7, 0, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}} | ||||
|     }; | ||||
|     segment_runtime _segment_runtimes[MAX_NUM_SEGMENTS]; // SRAM footprint: 28 bytes per element | ||||
|     friend class Segment_runtime; | ||||
| @@ -884,7 +885,7 @@ const char JSON_mode_names[] PROGMEM = R"=====([ | ||||
| "Sparkle","Sparkle Dark","Sparkle+","Strobe","Strobe Rainbow","Strobe Mega","Blink Rainbow","Android","Chase","Chase Random", | ||||
| "Chase Rainbow","Chase Flash","Chase Flash Rnd","Rainbow Runner","Colorful","Traffic Light","Sweep Random","Running 2","Aurora","Stream", | ||||
| "Scanner","Lighthouse","Fireworks","Rain","Tetrix","Fire Flicker","Gradient","Loading","Police","Police All", | ||||
| "Two Dots","Two Areas","Circus","Halloween","Tri Chase","Tri Wipe","Tri Fade","Lightning","ICU","Multi Comet", | ||||
| "Two Dots","Two Areas","Running Dual","Halloween","Tri Chase","Tri Wipe","Tri Fade","Lightning","ICU","Multi Comet", | ||||
| "Scanner Dual","Stream 2","Oscillate","Pride 2015","Juggle","Palette","Fire 2012","Colorwaves","Bpm","Fill Noise", | ||||
| "Noise 1","Noise 2","Noise 3","Noise 4","Colortwinkles","Lake","Meteor","Meteor Smooth","Railway","Ripple", | ||||
| "Twinklefox","Twinklecat","Halloween Eyes","Solid Pattern","Solid Pattern Tri","Spots","Spots Fade","Glitter","Candle","Fireworks Starburst", | ||||
|   | ||||
| @@ -43,43 +43,93 @@ | ||||
|   19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25] | ||||
| */ | ||||
|  | ||||
| //factory defaults LED setup | ||||
| //#define PIXEL_COUNTS 30, 30, 30, 30 | ||||
| //#define DATA_PINS 16, 1, 3, 4 | ||||
| //#define DEFAULT_LED_TYPE TYPE_WS2812_RGB | ||||
|  | ||||
| #ifndef PIXEL_COUNTS | ||||
|   #define PIXEL_COUNTS DEFAULT_LED_COUNT | ||||
| #endif | ||||
|  | ||||
| #ifndef DATA_PINS | ||||
|   #define DATA_PINS LEDPIN | ||||
| #endif | ||||
|  | ||||
| #ifndef DEFAULT_LED_TYPE | ||||
|   #define DEFAULT_LED_TYPE TYPE_WS2812_RGB | ||||
| #endif | ||||
|  | ||||
| #if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES | ||||
|   #error "Max segments must be at least max number of busses!" | ||||
| #endif | ||||
|  | ||||
| //do not call this method from system context (network callback) | ||||
| void WS2812FX::finalizeInit(uint16_t countPixels, bool skipFirst) | ||||
| void WS2812FX::finalizeInit(uint16_t countPixels) | ||||
| { | ||||
|   RESET_RUNTIME; | ||||
|   _length = countPixels; | ||||
|   _skipFirstMode = skipFirst; | ||||
|  | ||||
|   _lengthRaw = _length; | ||||
|   if (_skipFirstMode) { | ||||
|     _lengthRaw += LED_SKIP_AMOUNT; | ||||
|   } | ||||
|  | ||||
|   //if busses failed to load, add default (FS issue...) | ||||
|   if (busses.getNumBusses() == 0) { | ||||
|     uint8_t defPin[] = {LEDPIN}; | ||||
|     BusConfig defCfg = BusConfig(TYPE_WS2812_RGB, defPin, 0, _lengthRaw, COL_ORDER_GRB); | ||||
|     busses.add(defCfg); | ||||
|     const uint8_t defDataPins[] = {DATA_PINS}; | ||||
|     const uint16_t defCounts[] = {PIXEL_COUNTS}; | ||||
|     const uint8_t defNumBusses = ((sizeof defDataPins) / (sizeof defDataPins[0])); | ||||
|     const uint8_t defNumCounts = ((sizeof defCounts)   / (sizeof defCounts[0])); | ||||
|     uint16_t prevLen = 0; | ||||
|     for (uint8_t i = 0; i < defNumBusses; i++) { | ||||
|       uint8_t defPin[] = {defDataPins[i]}; | ||||
|       uint16_t start = prevLen; | ||||
|       uint16_t count = _length; | ||||
|       if (defNumBusses > 1 && defNumCounts) { | ||||
|         count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; | ||||
|       } | ||||
|       prevLen += count; | ||||
|       BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, COL_ORDER_GRB); | ||||
|       busses.add(defCfg); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   deserializeMap(); | ||||
|  | ||||
|   //make segment 0 cover the entire strip | ||||
|   _segments[0].start = 0; | ||||
|   _segments[0].stop = _length; | ||||
|   uint16_t segStarts[MAX_NUM_SEGMENTS] = {0}; | ||||
|   uint16_t segStops [MAX_NUM_SEGMENTS] = {0}; | ||||
|  | ||||
|   setBrightness(_brightness); | ||||
|  | ||||
|   #ifdef ESP8266 | ||||
|   //TODO make sure segments are only refreshed when bus config actually changed (new settings page) | ||||
|   //make one segment per bus | ||||
|   uint8_t s = 0; | ||||
|   for (uint8_t i = 0; i < busses.getNumBusses(); i++) { | ||||
|     Bus* b = busses.getBus(i); | ||||
|  | ||||
|     segStarts[s] = b->getStart(); | ||||
|     segStops[s] = segStarts[s] + b->getLength(); | ||||
|  | ||||
|     //check for overlap with previous segments | ||||
|     for (uint8_t j = 0; j < s; j++) { | ||||
|       if (segStops[j] > segStarts[s] && segStarts[j] < segStops[s]) { | ||||
|         //segments overlap, merge | ||||
|         segStarts[j] = min(segStarts[s],segStarts[j]); | ||||
|         segStops [j] = max(segStops [s],segStops [j]); segStops[s] = 0; | ||||
|         s--; | ||||
|       } | ||||
|     } | ||||
|     s++; | ||||
|  | ||||
|     #ifdef ESP8266 | ||||
|     if ((!IS_DIGITAL(b->getType()) || IS_2PIN(b->getType()))) continue; | ||||
|     uint8_t pins[5]; | ||||
|     b->getPins(pins); | ||||
|     BusDigital* bd = static_cast<BusDigital*>(b); | ||||
|     if (pins[0] == 3) bd->reinit(); | ||||
|     #endif | ||||
|   } | ||||
|  | ||||
|   for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) { | ||||
|     _segments[i].start = segStarts[i]; | ||||
|     _segments[i].stop  = segStops [i]; | ||||
|   } | ||||
|   #endif | ||||
| } | ||||
|  | ||||
| void WS2812FX::service() { | ||||
| @@ -174,7 +224,6 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   uint16_t skip = _skipFirstMode ? LED_SKIP_AMOUNT : 0; | ||||
|   if (SEGLEN) {//from segment | ||||
|  | ||||
|     //color_blend(getpixel, col, _bri_t); (pseudocode for future blending of segments) | ||||
| @@ -186,32 +235,35 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) | ||||
|     } | ||||
|     uint32_t col = ((w << 24) | (r << 16) | (g << 8) | (b)); | ||||
|  | ||||
|     /* Set all the pixels in the group, ensuring _skipFirstMode is honored */ | ||||
|     bool reversed = IS_REVERSE; | ||||
|     uint16_t realIndex = realPixelIndex(i); | ||||
|     uint16_t len = SEGMENT.length(); | ||||
|  | ||||
|     for (uint16_t j = 0; j < SEGMENT.grouping; j++) { | ||||
|       int16_t indexSet = realIndex + (reversed ? -j : j); | ||||
|       if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; | ||||
|       int indexSet = realIndex + (reversed ? -j : j); | ||||
|       if (indexSet >= SEGMENT.start && indexSet < SEGMENT.stop) { | ||||
|         busses.setPixelColor(indexSet + skip, col); | ||||
|         if (IS_MIRROR) { //set the corresponding mirrored pixel | ||||
|           uint16_t indexMir = SEGMENT.stop - indexSet + SEGMENT.start - 1; | ||||
|           /* offset/phase */ | ||||
|           indexMir += SEGMENT.offset; | ||||
|           if (indexMir >= SEGMENT.stop) indexMir -= len; | ||||
|  | ||||
|           if (indexMir < customMappingSize) indexMir = customMappingTable[indexMir]; | ||||
|           busses.setPixelColor(indexMir + skip, col); | ||||
|           busses.setPixelColor(indexMir, col); | ||||
|         } | ||||
|         /* offset/phase */ | ||||
|           indexSet += SEGMENT.offset; | ||||
|           if (indexSet >= SEGMENT.stop) indexSet -= len; | ||||
|  | ||||
|         if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; | ||||
|         busses.setPixelColor(indexSet, col); | ||||
|       } | ||||
|     } | ||||
|   } else { //live data, etc. | ||||
|     if (i < customMappingSize) i = customMappingTable[i]; | ||||
|      | ||||
|     uint32_t col = ((w << 24) | (r << 16) | (g << 8) | (b)); | ||||
|     busses.setPixelColor(i + skip, col); | ||||
|   } | ||||
|   if (skip && i == 0) { | ||||
|     for (uint16_t j = 0; j < skip; j++) { | ||||
|       busses.setPixelColor(j, BLACK); | ||||
|     } | ||||
|     busses.setPixelColor(i, col); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -482,12 +534,14 @@ uint32_t WS2812FX::getColor(void) { | ||||
| uint32_t WS2812FX::getPixelColor(uint16_t i) | ||||
| { | ||||
|   i = realPixelIndex(i); | ||||
|  | ||||
|   if (SEGLEN) { | ||||
|     /* offset/phase */ | ||||
|     i += SEGMENT.offset; | ||||
|     if (i >= SEGMENT.stop) i -= SEGMENT.length(); | ||||
|   } | ||||
|    | ||||
|   if (i < customMappingSize) i = customMappingTable[i]; | ||||
|  | ||||
|   if (_skipFirstMode) i += LED_SKIP_AMOUNT; | ||||
|    | ||||
|   if (i >= _lengthRaw) return 0; | ||||
|    | ||||
|   return busses.getPixelColor(i); | ||||
| } | ||||
| @@ -738,6 +792,12 @@ uint16_t WS2812FX::triwave16(uint16_t in) | ||||
|   return 0xFFFF - (in - 0x8000)*2; | ||||
| } | ||||
|  | ||||
| uint8_t WS2812FX::sin_gap(uint16_t in) { | ||||
|   if (in & 0x100) return 0; | ||||
|   //if (in > 255) return 0; | ||||
|   return sin8(in + 192); //correct phase shift of sine so that it starts and stops at 0 | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Generates a tristate square wave w/ attac & decay  | ||||
|  * @param x input value 0-255 | ||||
| @@ -942,7 +1002,7 @@ uint32_t WS2812FX::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8 | ||||
|   } | ||||
|  | ||||
|   uint8_t paletteIndex = i; | ||||
|   if (mapping) paletteIndex = (i*255)/(SEGLEN -1); | ||||
|   if (mapping && SEGLEN > 1) paletteIndex = (i*255)/(SEGLEN -1); | ||||
|   if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" | ||||
|   CRGB fastled_col; | ||||
|   fastled_col = ColorFromPalette( currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND); | ||||
|   | ||||
| @@ -1,124 +0,0 @@ | ||||
| /*  | ||||
| 	Editor: https://www.visualmicro.com/ | ||||
| 			This file is for intellisense purpose only. | ||||
| 			Visual micro (and the arduino ide) ignore this code during compilation. This code is automatically maintained by visualmicro, manual changes to this file will be overwritten | ||||
| 			The contents of the _vm sub folder can be deleted prior to publishing a project | ||||
| 			All non-arduino files created by visual micro and all visual studio project or solution files can be freely deleted and are not required to compile a sketch (do not delete your own code!). | ||||
| 			Note: debugger breakpoints are stored in '.sln' or '.asln' files, knowledge of last uploaded breakpoints is stored in the upload.vmps.xml file. Both files are required to continue a previous debug session without needing to compile and upload again | ||||
| 	 | ||||
| 	Hardware: ESP32 Dev Module, Platform=esp32, Package=esp32 | ||||
| */ | ||||
|  | ||||
| #if defined(_VMICRO_INTELLISENSE) | ||||
|  | ||||
| #ifndef _VSARDUINO_H_ | ||||
| #define _VSARDUINO_H_ | ||||
| #define __ESP32_esp32__ | ||||
| #define __ESP32_ESP32__ | ||||
| #define ESP_PLATFORM | ||||
| #define HAVE_CONFIG_H | ||||
| #define GCC_NOT_5_2_0 0 | ||||
| #define WITH_POSIX | ||||
| #define F_CPU 240000000L | ||||
| #define ARDUINO 108011 | ||||
| #define ARDUINO_ESP32_DEV | ||||
| #define ARDUINO_ARCH_ESP32 | ||||
| #define ESP32 | ||||
| #define CORE_DEBUG_LEVEL 0 | ||||
| #define __cplusplus 201103L | ||||
|  | ||||
| #define _Pragma(x) | ||||
| #undef __cplusplus | ||||
| #define __cplusplus 201103L | ||||
|  | ||||
| #define __STDC__ | ||||
| #define __ARM__ | ||||
| #define __arm__ | ||||
| #define __inline__ | ||||
| #define __asm__(...) | ||||
| #define __extension__ | ||||
| #define __ATTR_PURE__ | ||||
| #define __ATTR_CONST__ | ||||
| #define __volatile__ | ||||
|  | ||||
| #define __ASM | ||||
| #define __INLINE | ||||
| #define __attribute__(noinline) | ||||
|  | ||||
| //#define _STD_BEGIN | ||||
| //#define EMIT | ||||
| #define WARNING | ||||
| #define _Lockit | ||||
| #define __CLR_OR_THIS_CALL | ||||
| #define C4005 | ||||
| #define _NEW | ||||
|  | ||||
| typedef bool _Bool; | ||||
| typedef int _read; | ||||
| typedef int _seek; | ||||
| typedef int _write; | ||||
| typedef int _close; | ||||
| typedef int __cleanup; | ||||
|  | ||||
| //#define inline  | ||||
|  | ||||
| #define __builtin_clz | ||||
| #define __builtin_clzl | ||||
| #define __builtin_clzll | ||||
| #define __builtin_labs | ||||
| #define __builtin_va_list | ||||
| typedef int __gnuc_va_list; | ||||
|  | ||||
| #define __ATOMIC_ACQ_REL | ||||
|  | ||||
| #define __CHAR_BIT__ | ||||
| #define _EXFUN() | ||||
|  | ||||
| typedef unsigned char byte; | ||||
| extern "C" void __cxa_pure_virtual() {;} | ||||
|  | ||||
| typedef long __INTPTR_TYPE__ ; | ||||
| typedef long __UINTPTR_TYPE__ ; | ||||
| typedef long __SIZE_TYPE__ 	; | ||||
| typedef long __PTRDIFF_TYPE__; | ||||
|  | ||||
| typedef long pthread_t; | ||||
| typedef long pthread_key_t; | ||||
| typedef long pthread_once_t; | ||||
| typedef long pthread_mutex_t; | ||||
| typedef long pthread_mutex_t; | ||||
| typedef long pthread_cond_t; | ||||
|  | ||||
|  | ||||
|  | ||||
| #include "arduino.h" | ||||
| #include <pins_arduino.h>  | ||||
|  | ||||
| #define interrupts() sei() | ||||
| #define noInterrupts() cli() | ||||
|  | ||||
| #define ESP_LOGI(tag, ...) | ||||
|  | ||||
| #include "wled00.ino" | ||||
| #include "wled01_eeprom.ino" | ||||
| #include "wled02_xml.ino" | ||||
| #include "wled03_set.ino" | ||||
| #include "wled04_file.ino" | ||||
| #include "wled05_init.ino" | ||||
| #include "wled06_usermod.ino" | ||||
| #include "wled07_notify.ino" | ||||
| #include "wled08_led.ino" | ||||
| #include "wled09_button.ino" | ||||
| #include "wled10_ntp.ino" | ||||
| #include "wled11_ol.ino" | ||||
| #include "wled12_alexa.ino" | ||||
| #include "wled13_cronixie.ino" | ||||
| #include "wled14_colors.ino" | ||||
| #include "wled15_hue.ino" | ||||
| #include "wled16_blynk.ino" | ||||
| #include "wled17_mqtt.ino" | ||||
| #include "wled18_server.ino" | ||||
| #include "wled19_json.ino" | ||||
| #include "wled20_ir.ino" | ||||
| #endif | ||||
| #endif | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -46,7 +46,10 @@ void onAlexaChange(EspalexaDevice* dev) | ||||
|         bri = briLast; | ||||
|         colorUpdated(NOTIFIER_CALL_MODE_ALEXA); | ||||
|       } | ||||
|     } else applyPreset(macroAlexaOn); | ||||
|     } else { | ||||
|       applyPreset(macroAlexaOn); | ||||
|       if (bri == 0) espalexaDevice->setValue(briLast); //stop Alexa from complaining if macroAlexaOn does not actually turn on | ||||
|     } | ||||
|   } else if (m == EspalexaDeviceProperty::off) | ||||
|   { | ||||
|     if (!macroAlexaOff) | ||||
| @@ -57,7 +60,10 @@ void onAlexaChange(EspalexaDevice* dev) | ||||
|         bri = 0; | ||||
|         colorUpdated(NOTIFIER_CALL_MODE_ALEXA); | ||||
|       } | ||||
|     } else applyPreset(macroAlexaOff); | ||||
|     } else { | ||||
|       applyPreset(macroAlexaOff); | ||||
|       if (bri != 0) espalexaDevice->setValue(0); //stop Alexa from complaining if macroAlexaOff does not actually turn off | ||||
|     } | ||||
|   } else if (m == EspalexaDeviceProperty::bri) | ||||
|   { | ||||
|     bri = espalexaDevice->getValue(); | ||||
|   | ||||
| @@ -17,12 +17,14 @@ struct BusConfig { | ||||
|   uint16_t start = 0; | ||||
|   uint8_t colorOrder = COL_ORDER_GRB; | ||||
|   bool reversed = false; | ||||
|   uint8_t skipAmount; | ||||
|   uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; | ||||
|   BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false) { | ||||
|     type = busType; count = len; start = pstart; colorOrder = pcolorOrder; reversed = rev; | ||||
|   BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip=0) { | ||||
|     type = busType; count = len; start = pstart; | ||||
|     colorOrder = pcolorOrder; reversed = rev; skipAmount = skip; | ||||
|     uint8_t nPins = 1; | ||||
|     if (type > 47) nPins = 2; | ||||
|     else if (type > 41 && type < 46) nPins = NUM_PWM_PINS(type); | ||||
|     else if (type > 40 && type < 46) nPins = NUM_PWM_PINS(type); | ||||
|     for (uint8_t i = 0; i < nPins; i++) pins[i] = ppins[i]; | ||||
|   } | ||||
| }; | ||||
| @@ -51,11 +53,11 @@ class Bus { | ||||
|  | ||||
|   virtual uint8_t getPins(uint8_t* pinArray) { return 0; } | ||||
|  | ||||
|   uint16_t getStart() { | ||||
|   inline uint16_t getStart() { | ||||
|     return _start; | ||||
|   } | ||||
|  | ||||
|   void setStart(uint16_t start) { | ||||
|   inline void setStart(uint16_t start) { | ||||
|     _start = start; | ||||
|   } | ||||
|  | ||||
| @@ -69,14 +71,28 @@ class Bus { | ||||
|     return COL_ORDER_RGB; | ||||
|   } | ||||
|  | ||||
|   uint8_t getType() { | ||||
|   virtual bool isRgbw() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   virtual uint8_t skippedLeds() { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   inline uint8_t getType() { | ||||
|     return _type; | ||||
|   } | ||||
|  | ||||
|   bool isOk() { | ||||
|   inline bool isOk() { | ||||
|     return _valid; | ||||
|   } | ||||
|  | ||||
|   static bool isRgbw(uint8_t type) { | ||||
|     if (type == TYPE_SK6812_RGBW || type == TYPE_TM1814) return true; | ||||
|     if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   bool reversed = false; | ||||
|  | ||||
|   protected: | ||||
| @@ -99,8 +115,9 @@ class BusDigital : public Bus { | ||||
|         cleanup(); return; | ||||
|       } | ||||
|     } | ||||
|     _len = bc.count; | ||||
|     reversed = bc.reversed; | ||||
|     _skip = bc.skipAmount;    //sacrificial pixels | ||||
|     _len = bc.count + _skip; | ||||
|     _iType = PolyBus::getI(bc.type, _pins, nr); | ||||
|     if (_iType == I_NONE) return; | ||||
|     _busPtr = PolyBus::create(_iType, _pins, _len); | ||||
| @@ -109,11 +126,11 @@ class BusDigital : public Bus { | ||||
|     //Serial.printf("Successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u)\n",nr, len, type, pins[0],pins[1],_iType); | ||||
|   }; | ||||
|  | ||||
|   void show() { | ||||
|   inline void show() { | ||||
|     PolyBus::show(_busPtr, _iType); | ||||
|   } | ||||
|  | ||||
|   bool canShow() { | ||||
|   inline bool canShow() { | ||||
|     return PolyBus::canShow(_busPtr, _iType); | ||||
|   } | ||||
|  | ||||
| @@ -130,20 +147,22 @@ class BusDigital : public Bus { | ||||
|  | ||||
|   void setPixelColor(uint16_t pix, uint32_t c) { | ||||
|     if (reversed) pix = _len - pix -1; | ||||
|     else pix += _skip; | ||||
|     PolyBus::setPixelColor(_busPtr, _iType, pix, c, _colorOrder); | ||||
|   } | ||||
|  | ||||
|   uint32_t getPixelColor(uint16_t pix) { | ||||
|     if (reversed) pix = _len - pix -1; | ||||
|     else pix += _skip; | ||||
|     return PolyBus::getPixelColor(_busPtr, _iType, pix, _colorOrder); | ||||
|   } | ||||
|  | ||||
|   uint8_t getColorOrder() { | ||||
|   inline uint8_t getColorOrder() { | ||||
|     return _colorOrder; | ||||
|   } | ||||
|  | ||||
|   uint16_t getLength() { | ||||
|     return _len; | ||||
|   inline uint16_t getLength() { | ||||
|     return _len - _skip; | ||||
|   } | ||||
|  | ||||
|   uint8_t getPins(uint8_t* pinArray) { | ||||
| @@ -157,7 +176,15 @@ class BusDigital : public Bus { | ||||
|     _colorOrder = colorOrder; | ||||
|   } | ||||
|  | ||||
|   void reinit() { | ||||
|   inline bool isRgbw() { | ||||
|     return (_type == TYPE_SK6812_RGBW || _type == TYPE_TM1814); | ||||
|   } | ||||
|  | ||||
|   inline uint8_t skippedLeds() { | ||||
|     return _skip; | ||||
|   } | ||||
|  | ||||
|   inline void reinit() { | ||||
|     PolyBus::begin(_busPtr, _iType, _pins); | ||||
|   } | ||||
|  | ||||
| @@ -180,6 +207,7 @@ class BusDigital : public Bus { | ||||
|   uint8_t _pins[2] = {255, 255}; | ||||
|   uint8_t _iType = I_NONE; | ||||
|   uint16_t _len = 0; | ||||
|   uint8_t _skip = 0; | ||||
|   void * _busPtr = nullptr; | ||||
| }; | ||||
|  | ||||
| @@ -255,7 +283,7 @@ class BusPwm : public Bus { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void setBrightness(uint8_t b) { | ||||
|   inline void setBrightness(uint8_t b) { | ||||
|     _bri = b; | ||||
|   } | ||||
|  | ||||
| @@ -265,7 +293,11 @@ class BusPwm : public Bus { | ||||
|     return numPins; | ||||
|   } | ||||
|  | ||||
|   void cleanup() { | ||||
|   bool isRgbw() { | ||||
|     return (_type > TYPE_ONOFF && _type <= TYPE_ANALOG_5CH && _type != TYPE_ANALOG_3CH); | ||||
|   } | ||||
|  | ||||
|   inline void cleanup() { | ||||
|     deallocatePins(); | ||||
|   } | ||||
|  | ||||
| @@ -304,7 +336,7 @@ class BusManager { | ||||
|   }; | ||||
|  | ||||
|   //utility to get the approx. memory usage of a given BusConfig | ||||
|   uint32_t memUsage(BusConfig &bc) { | ||||
|   static uint32_t memUsage(BusConfig &bc) { | ||||
|     uint8_t type = bc.type; | ||||
|     uint16_t len = bc.count; | ||||
|     if (type < 32) { | ||||
| @@ -333,8 +365,7 @@ class BusManager { | ||||
|     } else { | ||||
|       busses[numBusses] = new BusPwm(bc); | ||||
|     } | ||||
|     numBusses++; | ||||
|     return numBusses -1; | ||||
|     return numBusses++; | ||||
|   } | ||||
|  | ||||
|   //do not call this method from system context (network callback) | ||||
| @@ -358,6 +389,7 @@ class BusManager { | ||||
|       uint16_t bstart = b->getStart(); | ||||
|       if (pix < bstart || pix >= bstart + b->getLength()) continue; | ||||
|       busses[i]->setPixelColor(pix - bstart, c); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -389,14 +421,23 @@ class BusManager { | ||||
|     return busses[busNr]; | ||||
|   } | ||||
|  | ||||
|   uint8_t getNumBusses() { | ||||
|   inline uint8_t getNumBusses() { | ||||
|     return numBusses; | ||||
|   } | ||||
|  | ||||
|   static bool isRgbw(uint8_t type) { | ||||
|     if (type == TYPE_SK6812_RGBW || type == TYPE_TM1814) return true; | ||||
|     if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; | ||||
|     return false; | ||||
|   uint16_t getTotalLength() { | ||||
|     uint16_t len = 0; | ||||
|     for (uint8_t i=0; i<numBusses; i++ ) len += busses[i]->getLength(); | ||||
|     return len; | ||||
|   } | ||||
|  | ||||
|   static inline bool isRgbw(uint8_t type) { | ||||
|     return Bus::isRgbw(type); | ||||
|   } | ||||
|  | ||||
|   //Return true if the strip requires a refresh to stay off. | ||||
|   static bool isOffRefreshRequred(uint8_t type) { | ||||
|     return type == TYPE_TM1814; | ||||
|   } | ||||
|  | ||||
|   private: | ||||
|   | ||||
| @@ -28,7 +28,7 @@ | ||||
| #define I_8266_U1_400_3 10 | ||||
| #define I_8266_DM_400_3 11 | ||||
| #define I_8266_BB_400_3 12 | ||||
| //TM1418 (RGBW) | ||||
| //TM1814 (RGBW) | ||||
| #define I_8266_U0_TM1_4 13 | ||||
| #define I_8266_U1_TM1_4 14 | ||||
| #define I_8266_DM_TM1_4 15 | ||||
| @@ -68,7 +68,7 @@ | ||||
| #define I_32_R7_400_3 44 | ||||
| #define I_32_I0_400_3 45 | ||||
| #define I_32_I1_400_3 46 | ||||
| //TM1418 (RGBW) | ||||
| //TM1814 (RGBW) | ||||
| #define I_32_R0_TM1_4 47 | ||||
| #define I_32_R1_TM1_4 48 | ||||
| #define I_32_R2_TM1_4 49 | ||||
| @@ -115,7 +115,7 @@ | ||||
| #define B_8266_U1_400_3 NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp8266Uart1400KbpsMethod>   //3 chan, esp8266, gpio2 | ||||
| #define B_8266_DM_400_3 NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp8266Dma400KbpsMethod>     //3 chan, esp8266, gpio3 | ||||
| #define B_8266_BB_400_3 NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp8266BitBang400KbpsMethod> //3 chan, esp8266, bb (any pin) | ||||
| //TM1418 (RGBW) | ||||
| //TM1814 (RGBW) | ||||
| #define B_8266_U0_TM1_4 NeoPixelBrightnessBus<NeoWrgbTm1814Feature, NeoEsp8266Uart0Tm1814Method> | ||||
| #define B_8266_U1_TM1_4 NeoPixelBrightnessBus<NeoWrgbTm1814Feature, NeoEsp8266Uart1Tm1814Method> | ||||
| #define B_8266_DM_TM1_4 NeoPixelBrightnessBus<NeoWrgbTm1814Feature, NeoEsp8266DmaTm1814Method> | ||||
| @@ -157,7 +157,7 @@ | ||||
| #define B_32_R7_400_3 NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp32Rmt7400KbpsMethod> | ||||
| #define B_32_I0_400_3 NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp32I2s0400KbpsMethod> | ||||
| #define B_32_I1_400_3 NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp32I2s1400KbpsMethod> | ||||
| //TM1418 (RGBW) | ||||
| //TM1814 (RGBW) | ||||
| #define B_32_R0_TM1_4 NeoPixelBrightnessBus<NeoWrgbTm1814Feature, NeoEsp32Rmt0Tm1814Method> | ||||
| #define B_32_R1_TM1_4 NeoPixelBrightnessBus<NeoWrgbTm1814Feature, NeoEsp32Rmt1Tm1814Method> | ||||
| #define B_32_R2_TM1_4 NeoPixelBrightnessBus<NeoWrgbTm1814Feature, NeoEsp32Rmt2Tm1814Method> | ||||
| @@ -191,6 +191,14 @@ | ||||
| //handles pointer type conversion for all possible bus types | ||||
| class PolyBus { | ||||
|   public: | ||||
|   // Begin & initialize the PixelSettings for TM1814 strips. | ||||
|   template <class T> | ||||
|   static void beginTM1814(void* busPtr) { | ||||
|     T tm1814_strip = static_cast<T>(busPtr); | ||||
|     tm1814_strip->Begin(); | ||||
|     // Max current for each LED (22.5 mA). | ||||
|     tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225)); | ||||
|   } | ||||
|   static void begin(void* busPtr, uint8_t busType, uint8_t* pins) { | ||||
|     switch (busType) { | ||||
|       case I_NONE: break; | ||||
| @@ -207,10 +215,10 @@ class PolyBus { | ||||
|       case I_8266_U1_400_3: (static_cast<B_8266_U1_400_3*>(busPtr))->Begin(); break; | ||||
|       case I_8266_DM_400_3: (static_cast<B_8266_DM_400_3*>(busPtr))->Begin(); break; | ||||
|       case I_8266_BB_400_3: (static_cast<B_8266_BB_400_3*>(busPtr))->Begin(); break; | ||||
|       case I_8266_U0_TM1_4: (static_cast<B_8266_U0_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_8266_U1_TM1_4: (static_cast<B_8266_U1_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_8266_DM_TM1_4: (static_cast<B_8266_DM_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_8266_BB_TM1_4: (static_cast<B_8266_BB_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_8266_U0_TM1_4: beginTM1814<B_8266_U0_TM1_4*>(busPtr); break; | ||||
|       case I_8266_U1_TM1_4: beginTM1814<B_8266_U1_TM1_4*>(busPtr); break; | ||||
|       case I_8266_DM_TM1_4: beginTM1814<B_8266_DM_TM1_4*>(busPtr); break; | ||||
|       case I_8266_BB_TM1_4: beginTM1814<B_8266_BB_TM1_4*>(busPtr); break; | ||||
|       case I_HS_DOT_3: (static_cast<B_HS_DOT_3*>(busPtr))->Begin(); break; | ||||
|       case I_HS_LPD_3: (static_cast<B_HS_LPD_3*>(busPtr))->Begin(); break; | ||||
|       case I_HS_WS1_3: (static_cast<B_HS_WS1_3*>(busPtr))->Begin(); break; | ||||
| @@ -247,16 +255,16 @@ class PolyBus { | ||||
|       case I_32_R7_400_3: (static_cast<B_32_R7_400_3*>(busPtr))->Begin(); break; | ||||
|       case I_32_I0_400_3: (static_cast<B_32_I0_400_3*>(busPtr))->Begin(); break; | ||||
|       case I_32_I1_400_3: (static_cast<B_32_I1_400_3*>(busPtr))->Begin(); break; | ||||
|       case I_32_R0_TM1_4: (static_cast<B_32_R0_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R1_TM1_4: (static_cast<B_32_R1_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R2_TM1_4: (static_cast<B_32_R2_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R3_TM1_4: (static_cast<B_32_R3_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R4_TM1_4: (static_cast<B_32_R4_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R5_TM1_4: (static_cast<B_32_R5_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R6_TM1_4: (static_cast<B_32_R6_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R7_TM1_4: (static_cast<B_32_R7_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_I0_TM1_4: (static_cast<B_32_I0_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_I1_TM1_4: (static_cast<B_32_I1_TM1_4*>(busPtr))->Begin(); break; | ||||
|       case I_32_R0_TM1_4: beginTM1814<B_32_R0_TM1_4*>(busPtr); break; | ||||
|       case I_32_R1_TM1_4: beginTM1814<B_32_R1_TM1_4*>(busPtr); break; | ||||
|       case I_32_R2_TM1_4: beginTM1814<B_32_R2_TM1_4*>(busPtr); break; | ||||
|       case I_32_R3_TM1_4: beginTM1814<B_32_R3_TM1_4*>(busPtr); break; | ||||
|       case I_32_R4_TM1_4: beginTM1814<B_32_R4_TM1_4*>(busPtr); break; | ||||
|       case I_32_R5_TM1_4: beginTM1814<B_32_R5_TM1_4*>(busPtr); break; | ||||
|       case I_32_R6_TM1_4: beginTM1814<B_32_R6_TM1_4*>(busPtr); break; | ||||
|       case I_32_R7_TM1_4: beginTM1814<B_32_R7_TM1_4*>(busPtr); break; | ||||
|       case I_32_I0_TM1_4: beginTM1814<B_32_I0_TM1_4*>(busPtr); break; | ||||
|       case I_32_I1_TM1_4: beginTM1814<B_32_I1_TM1_4*>(busPtr); break; | ||||
|       // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() | ||||
|       case I_HS_DOT_3: (static_cast<B_HS_DOT_3*>(busPtr))->Begin(pins[1], -1, pins[0], -1); break; | ||||
|       case I_HS_LPD_3: (static_cast<B_HS_LPD_3*>(busPtr))->Begin(pins[1], -1, pins[0], -1); break; | ||||
| @@ -860,6 +868,8 @@ class PolyBus { | ||||
|           return I_8266_U0_NEO_4 + offset; | ||||
|         case TYPE_WS2811_400KHZ: | ||||
|           return I_8266_U0_400_3 + offset; | ||||
|         case TYPE_TM1814: | ||||
|           return I_8266_U0_TM1_4 + offset; | ||||
|       } | ||||
|       #else //ESP32 | ||||
|       uint8_t offset = num; //RMT bus # == bus index in BusManager | ||||
| @@ -872,6 +882,8 @@ class PolyBus { | ||||
|           return I_32_R0_NEO_4 + offset; | ||||
|         case TYPE_WS2811_400KHZ: | ||||
|           return I_32_R0_400_3 + offset; | ||||
|         case TYPE_TM1814: | ||||
|           return I_32_R0_TM1_4 + offset; | ||||
|       } | ||||
|       #endif | ||||
|     } | ||||
|   | ||||
| @@ -4,73 +4,212 @@ | ||||
|  * Physical IO | ||||
|  */ | ||||
|  | ||||
| void shortPressAction() | ||||
| #define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) | ||||
|  | ||||
| void shortPressAction(uint8_t b) | ||||
| { | ||||
|   if (!macroButton) | ||||
|   if (!macroButton[b]) | ||||
|   { | ||||
|     toggleOnOff(); | ||||
|     colorUpdated(NOTIFIER_CALL_MODE_BUTTON); | ||||
|   } else { | ||||
|     applyPreset(macroButton); | ||||
|     applyPreset(macroButton[b]); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool isButtonPressed() | ||||
| bool isButtonPressed(uint8_t i) | ||||
| { | ||||
|   if (btnPin>=0 && digitalRead(btnPin) == LOW) return true; | ||||
|   #ifdef TOUCHPIN | ||||
|     if (touchRead(TOUCHPIN) <= TOUCH_THRESHOLD) return true; | ||||
|   #endif | ||||
|   if (btnPin[i]<0) return false; | ||||
|   switch (buttonType[i]) { | ||||
|     case BTN_TYPE_NONE: | ||||
|     case BTN_TYPE_RESERVED: | ||||
|       break; | ||||
|     case BTN_TYPE_PUSH: | ||||
|     case BTN_TYPE_SWITCH: | ||||
|       if (digitalRead(btnPin[i]) == LOW) return true; | ||||
|       break; | ||||
|     case BTN_TYPE_PUSH_ACT_HIGH: | ||||
|     case BTN_TYPE_SWITCH_ACT_HIGH: | ||||
|       if (digitalRead(btnPin[i]) == HIGH) return true; | ||||
|       break; | ||||
|     case BTN_TYPE_TOUCH: | ||||
|       #ifdef ARDUINO_ARCH_ESP32 | ||||
|       if (touchRead(btnPin[i]) <= touchThreshold) return true; | ||||
|       #endif | ||||
|       break; | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| void handleSwitch(uint8_t b) | ||||
| { | ||||
|   if (buttonPressedBefore[b] != isButtonPressed(b)) { | ||||
|     buttonPressedTime[b] = millis(); | ||||
|     buttonPressedBefore[b] = !buttonPressedBefore[b]; | ||||
|   } | ||||
|  | ||||
|   if (buttonLongPressed[b] == buttonPressedBefore[b]) return; | ||||
|      | ||||
|   if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) | ||||
|     if (buttonPressedBefore[b]) { //LOW, falling edge, switch closed | ||||
|       if (macroButton[b]) applyPreset(macroButton[b]); | ||||
|       else { //turn on | ||||
|         if (!bri) {toggleOnOff(); colorUpdated(NOTIFIER_CALL_MODE_BUTTON);} | ||||
|       }  | ||||
|     } else { //HIGH, rising edge, switch opened | ||||
|       if (macroLongPress[b]) applyPreset(macroLongPress[b]); | ||||
|       else { //turn off | ||||
|         if (bri) {toggleOnOff(); colorUpdated(NOTIFIER_CALL_MODE_BUTTON);} | ||||
|       }  | ||||
|     } | ||||
|     buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state | ||||
|   } | ||||
| } | ||||
|  | ||||
| void handleAnalog(uint8_t b) | ||||
| { | ||||
|   static uint8_t oldRead[WLED_MAX_BUTTONS]; | ||||
|   #ifdef ESP8266 | ||||
|   uint16_t aRead = analogRead(A0) >> 2; // convert 10bit read to 8bit | ||||
|   #else | ||||
|   uint16_t aRead = analogRead(btnPin[b]) >> 4; // convert 12bit read to 8bit | ||||
|   #endif | ||||
|   // remove noise & reduce frequency of UI updates | ||||
|   aRead &= 0xFC; | ||||
|  | ||||
|   if (oldRead[b] == aRead) return;  // no change in reading | ||||
|   oldRead[b] = aRead; | ||||
|  | ||||
|   // if no macro for "short press" and "long press" is defined use brightness control | ||||
|   if (!macroButton[b] && !macroLongPress[b]) { | ||||
|     // if "double press" macro defines which option to change | ||||
|     if (macroDoublePress[b] >= 250) { | ||||
|       // global brightness | ||||
|       if (aRead == 0) { | ||||
|         briLast = bri; | ||||
|         bri = 0; | ||||
|       } else{ | ||||
|         bri = aRead; | ||||
|       } | ||||
|     } else if (macroDoublePress[b] == 249) { | ||||
|       // effect speed | ||||
|       effectSpeed = aRead; | ||||
|       effectChanged = true; | ||||
|       for (uint8_t i = 0; i < strip.getMaxSegments(); i++) { | ||||
|         WS2812FX::Segment& seg = strip.getSegment(i); | ||||
|         if (!seg.isSelected()) continue; | ||||
|         seg.speed = effectSpeed; | ||||
|       } | ||||
|     } else if (macroDoublePress[b] == 248) { | ||||
|       // effect intensity | ||||
|       effectIntensity = aRead; | ||||
|       effectChanged = true; | ||||
|       for (uint8_t i = 0; i < strip.getMaxSegments(); i++) { | ||||
|         WS2812FX::Segment& seg = strip.getSegment(i); | ||||
|         if (!seg.isSelected()) continue; | ||||
|         seg.intensity = effectIntensity; | ||||
|       } | ||||
|     } else if (macroDoublePress[b] == 247) { | ||||
|       // selected palette | ||||
|       effectPalette = map(aRead, 0, 252, 0, strip.getPaletteCount()-1); | ||||
|       effectChanged = true; | ||||
|       for (uint8_t i = 0; i < strip.getMaxSegments(); i++) { | ||||
|         WS2812FX::Segment& seg = strip.getSegment(i); | ||||
|         if (!seg.isSelected()) continue; | ||||
|         seg.palette = effectPalette; | ||||
|       } | ||||
|     } else if (macroDoublePress[b] == 200) { | ||||
|       // primary color, hue, full saturation | ||||
|       colorHStoRGB(aRead*256,255,col); | ||||
|     } else { | ||||
|       // otherwise use "double press" for segment selection | ||||
|       //uint8_t mainSeg = strip.getMainSegmentId(); | ||||
|       WS2812FX::Segment& seg = strip.getSegment(macroDoublePress[b]); | ||||
|       if (aRead == 0) { | ||||
|         seg.setOption(SEG_OPTION_ON, 0); // off | ||||
|       } else { | ||||
|         seg.setOpacity(aRead, macroDoublePress[b]); | ||||
|         seg.setOption(SEG_OPTION_ON, 1); | ||||
|       } | ||||
|       // this will notify clients of update (websockets,mqtt,etc) | ||||
|       //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 | ||||
|       updateInterfaces(NOTIFIER_CALL_MODE_BUTTON); | ||||
|     } | ||||
|   } else { | ||||
|     //TODO: | ||||
|     // we can either trigger a preset depending on the level (between short and long entries) | ||||
|     // or use it for RGBW direct control | ||||
|   } | ||||
|   //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); | ||||
| } | ||||
|  | ||||
| void handleButton() | ||||
| { | ||||
|   if (btnPin<0 || !buttonEnabled) return; | ||||
|   static unsigned long lastRead = 0UL; | ||||
|  | ||||
|   if (isButtonPressed()) //pressed | ||||
|   { | ||||
|     if (!buttonPressedBefore) buttonPressedTime = millis(); | ||||
|     buttonPressedBefore = true; | ||||
|   for (uint8_t b=0; b<WLED_MAX_BUTTONS; b++) { | ||||
|     #ifdef ESP8266 | ||||
|     if ((btnPin[b]<0 && buttonType[b] != BTN_TYPE_ANALOG) || buttonType[b] == BTN_TYPE_NONE) continue; | ||||
|     #else | ||||
|     if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue; | ||||
|     #endif | ||||
|  | ||||
|     if (millis() - buttonPressedTime > 600) //long press | ||||
|     if (buttonType[b] == BTN_TYPE_ANALOG && millis() - lastRead > 250) {   // button is not a button but a potentiometer | ||||
|       if (b+1 == WLED_MAX_BUTTONS) lastRead = millis(); | ||||
|       handleAnalog(b); continue; | ||||
|     } | ||||
|  | ||||
|     if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_SWITCH_ACT_HIGH) { //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) | ||||
|       handleSwitch(b); continue; | ||||
|     } | ||||
|  | ||||
|     //momentary button logic | ||||
|     if (isButtonPressed(b)) //pressed | ||||
|     { | ||||
|       if (!buttonLongPressed)  | ||||
|       { | ||||
|         if (macroLongPress) {applyPreset(macroLongPress);} | ||||
|         else _setRandomColor(false,true); | ||||
|       if (!buttonPressedBefore[b]) buttonPressedTime[b] = millis(); | ||||
|       buttonPressedBefore[b] = true; | ||||
|  | ||||
|         buttonLongPressed = true; | ||||
|       if (millis() - buttonPressedTime[b] > 600) //long press | ||||
|       { | ||||
|         if (!buttonLongPressed[b])  | ||||
|         { | ||||
|           if (macroLongPress[b]) {applyPreset(macroLongPress[b]);} | ||||
|           else _setRandomColor(false,true); | ||||
|  | ||||
|           buttonLongPressed[b] = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   else if (!isButtonPressed() && buttonPressedBefore) //released | ||||
|   { | ||||
|     long dur = millis() - buttonPressedTime; | ||||
|     if (dur < 50) {buttonPressedBefore = false; return;} //too short "press", debounce | ||||
|     bool doublePress = buttonWaitTime; | ||||
|     buttonWaitTime = 0; | ||||
|  | ||||
|     if (dur > 6000) //long press | ||||
|     else if (!isButtonPressed(b) && buttonPressedBefore[b]) //released | ||||
|     { | ||||
|       WLED::instance().initAP(true); | ||||
|     } | ||||
|     else if (!buttonLongPressed) { //short press | ||||
|       if (macroDoublePress) | ||||
|       { | ||||
|         if (doublePress) applyPreset(macroDoublePress); | ||||
|         else buttonWaitTime = millis(); | ||||
|       } else shortPressAction(); | ||||
|     } | ||||
|     buttonPressedBefore = false; | ||||
|     buttonLongPressed = false; | ||||
|   } | ||||
|       long dur = millis() - buttonPressedTime[b]; | ||||
|       if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} //too short "press", debounce | ||||
|       bool doublePress = buttonWaitTime[b]; | ||||
|       buttonWaitTime[b] = 0; | ||||
|  | ||||
|   if (buttonWaitTime && millis() - buttonWaitTime > 450 && !buttonPressedBefore) | ||||
|   { | ||||
|     buttonWaitTime = 0; | ||||
|     shortPressAction(); | ||||
|       if (dur > 6000 && b==0) //long press on button 0 | ||||
|       { | ||||
|         WLED::instance().initAP(true); | ||||
|       } | ||||
|       else if (!buttonLongPressed[b]) { //short press | ||||
|         if (macroDoublePress[b]) | ||||
|         { | ||||
|           if (doublePress) applyPreset(macroDoublePress[b]); | ||||
|           else buttonWaitTime[b] = millis(); | ||||
|         } else shortPressAction(b); | ||||
|       } | ||||
|       buttonPressedBefore[b] = false; | ||||
|       buttonLongPressed[b] = false; | ||||
|     } | ||||
|  | ||||
|     if (buttonWaitTime[b] && millis() - buttonWaitTime[b] > 450 && !buttonPressedBefore[b]) | ||||
|     { | ||||
|       buttonWaitTime[b] = 0; | ||||
|       shortPressAction(b); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -94,7 +233,8 @@ void handleIO() | ||||
|   { | ||||
|     if (!offMode) { | ||||
|       #ifdef ESP8266 | ||||
|       //turn off built-in LED if strip is turned off | ||||
|       // turn off built-in LED if strip is turned off | ||||
|       // this will break digital bus so will need to be reinitialised on On | ||||
|       pinMode(LED_BUILTIN, OUTPUT); | ||||
|       digitalWrite(LED_BUILTIN, HIGH); | ||||
|       #endif | ||||
| @@ -105,31 +245,4 @@ void handleIO() | ||||
|     } | ||||
|     offMode = true; | ||||
|   } | ||||
|  | ||||
|   //output | ||||
|   if (auxPin>=1 && (auxActive || auxActiveBefore)) | ||||
|   { | ||||
|     if (!auxActiveBefore) | ||||
|     { | ||||
|       auxActiveBefore = true; | ||||
|       switch (auxTriggeredState) | ||||
|       { | ||||
|         case 0: pinMode(auxPin, INPUT); break; | ||||
|         case 1: pinMode(auxPin, OUTPUT); digitalWrite(auxPin, HIGH); break; | ||||
|         case 2: pinMode(auxPin, OUTPUT); digitalWrite(auxPin, LOW); break; | ||||
|       } | ||||
|       auxStartTime = millis(); | ||||
|     } | ||||
|     if ((millis() - auxStartTime > auxTime*1000 && auxTime != 255) || !auxActive) | ||||
|     { | ||||
|       auxActive = false; | ||||
|       auxActiveBefore = false; | ||||
|       switch (auxDefaultState) | ||||
|       { | ||||
|         case 0: pinMode(auxPin, INPUT); break; | ||||
|         case 1: pinMode(auxPin, OUTPUT); digitalWrite(auxPin, HIGH); break; | ||||
|         case 2: pinMode(auxPin, OUTPUT); digitalWrite(auxPin, LOW); break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										339
									
								
								wled00/cfg.cpp
									
									
									
									
									
								
							
							
						
						
									
										339
									
								
								wled00/cfg.cpp
									
									
									
									
									
								
							| @@ -12,24 +12,7 @@ void getStringFromJson(char* dest, const char* src, size_t len) { | ||||
|   if (src != nullptr) strlcpy(dest, src, len); | ||||
| } | ||||
|  | ||||
| void deserializeConfig() { | ||||
|   bool fromeep = false; | ||||
|   bool success = deserializeConfigSec(); | ||||
|   if (!success) { //if file does not exist, try reading from EEPROM | ||||
|     deEEPSettings(); | ||||
|     fromeep = true; | ||||
|   } | ||||
|  | ||||
|   DynamicJsonDocument doc(JSON_BUFFER_SIZE); | ||||
|  | ||||
|   DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); | ||||
|  | ||||
|   success = readObjectFromFile("/cfg.json", nullptr, &doc); | ||||
|   if (!success) { //if file does not exist, try reading from EEPROM | ||||
|     if (!fromeep) deEEPSettings(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| bool deserializeConfig(JsonObject doc, bool fromFS) { | ||||
|   //int rev_major = doc["rev"][0]; // 1 | ||||
|   //int rev_minor = doc["rev"][1]; // 0 | ||||
|  | ||||
| @@ -40,7 +23,7 @@ void deserializeConfig() { | ||||
|   getStringFromJson(serverDescription, id[F("name")], 33); | ||||
|   getStringFromJson(alexaInvocationName, id[F("inv")], 33); | ||||
|  | ||||
|   JsonObject nw_ins_0 = doc["nw"][F("ins")][0]; | ||||
|   JsonObject nw_ins_0 = doc["nw"]["ins"][0]; | ||||
|   getStringFromJson(clientSSID, nw_ins_0[F("ssid")], 33); | ||||
|   //int nw_ins_0_pskl = nw_ins_0[F("pskl")]; | ||||
|   //The WiFi PSK is normally not contained in the regular file for security reasons. | ||||
| @@ -79,7 +62,8 @@ void deserializeConfig() { | ||||
|   JsonArray ap_ip = ap["ip"]; | ||||
|   for (byte i = 0; i < 4; i++) { | ||||
|     apIP[i] = ap_ip; | ||||
|   }*/ | ||||
|   } | ||||
|   */ | ||||
|  | ||||
|   noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted | ||||
|   noWifiSleep = !noWifiSleep; | ||||
| @@ -98,85 +82,112 @@ void deserializeConfig() { | ||||
|   CJSON(strip.rgbwMode, hw_led[F("rgbwm")]); | ||||
|  | ||||
|   JsonArray ins = hw_led["ins"]; | ||||
|   uint8_t s = 0; //bus iterator | ||||
|   strip.isRgbw = false; | ||||
|   busses.removeAll(); | ||||
|   uint32_t mem = 0; | ||||
|   for (JsonObject elm : ins) { | ||||
|     if (s >= WLED_MAX_BUSSES) break; | ||||
|     uint8_t pins[5] = {255, 255, 255, 255, 255}; | ||||
|     JsonArray pinArr = elm[F("pin")]; | ||||
|     if (pinArr.size() == 0) continue; | ||||
|     pins[0] = pinArr[0]; | ||||
|     uint8_t i = 0; | ||||
|     for (int p : pinArr) { | ||||
|       pins[i] = p; | ||||
|       i++; | ||||
|       if (i>4) break; | ||||
|     } | ||||
|   if (fromFS || !ins.isNull()) { | ||||
|     uint8_t s = 0; //bus iterator | ||||
|     strip.isRgbw = false; | ||||
|     busses.removeAll(); | ||||
|     uint32_t mem = 0; | ||||
|     for (JsonObject elm : ins) { | ||||
|       if (s >= WLED_MAX_BUSSES) break; | ||||
|       uint8_t pins[5] = {255, 255, 255, 255, 255}; | ||||
|       JsonArray pinArr = elm["pin"]; | ||||
|       if (pinArr.size() == 0) continue; | ||||
|       pins[0] = pinArr[0]; | ||||
|       uint8_t i = 0; | ||||
|       for (int p : pinArr) { | ||||
|         pins[i++] = p; | ||||
|         if (i>4) break; | ||||
|       } | ||||
|  | ||||
|     uint16_t length = elm[F("len")]; | ||||
|     if (length==0) continue; | ||||
|     uint8_t colorOrder = (int)elm[F("order")]; | ||||
|     //only use skip from the first strip (this shouldn't have been in ins obj. but remains here for compatibility) | ||||
|     if (s==0) skipFirstLed = elm[F("skip")]; | ||||
|     uint16_t start = elm[F("start")] | 0; | ||||
|     if (start >= ledCount) continue; | ||||
|     //limit length of strip if it would exceed total configured LEDs | ||||
|     if (start + length > ledCount) length = ledCount - start; | ||||
|     uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; | ||||
|     bool reversed = elm["rev"]; | ||||
|     //RGBW mode is enabled if at least one of the strips is RGBW | ||||
|     strip.isRgbw = (strip.isRgbw || BusManager::isRgbw(ledType)); | ||||
|     s++; | ||||
|     BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed); | ||||
|     mem += busses.memUsage(bc); | ||||
|     if (mem <= MAX_LED_MEMORY) busses.add(bc); | ||||
|       uint16_t length = elm[F("len")]; | ||||
|       if (length==0) continue; | ||||
|       uint8_t colorOrder = (int)elm[F("order")]; | ||||
|       //only use skip from the first strip (this shouldn't have been in ins obj. but remains here for compatibility) | ||||
|       uint8_t skipFirst = elm[F("skip")]; | ||||
|       uint16_t start = elm[F("start")] | 0; | ||||
|       if (start >= ledCount) continue; | ||||
|       //limit length of strip if it would exceed total configured LEDs | ||||
|       if (start + length > ledCount) length = ledCount - start; | ||||
|       uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; | ||||
|       bool reversed = elm["rev"]; | ||||
|       //RGBW mode is enabled if at least one of the strips is RGBW | ||||
|       strip.isRgbw = (strip.isRgbw || BusManager::isRgbw(ledType)); | ||||
|       //refresh is required to remain off if at least one of the strips requires the refresh. | ||||
|       strip.isOffRefreshRequred |= BusManager::isOffRefreshRequred(ledType); | ||||
|       s++; | ||||
|       BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); | ||||
|       mem += busses.memUsage(bc); | ||||
|       if (mem <= MAX_LED_MEMORY) busses.add(bc); | ||||
|     } | ||||
|     strip.finalizeInit(ledCount); | ||||
|   } | ||||
|   strip.finalizeInit(ledCount, skipFirstLed); | ||||
|   if (hw_led["rev"]) busses.getBus(0)->reversed = true; //set 0.11 global reversed setting for first bus | ||||
|  | ||||
|   JsonObject hw_btn_ins_0 = hw[F("btn")][F("ins")][0]; | ||||
|   CJSON(buttonEnabled, hw_btn_ins_0["type"]); | ||||
|   int hw_btn_pin = hw_btn_ins_0[F("pin")][0]; | ||||
|   if (pinManager.allocatePin(hw_btn_pin,false)) { | ||||
|     btnPin = hw_btn_pin; | ||||
|     pinMode(btnPin, INPUT_PULLUP); | ||||
|   // read multiple button configuration | ||||
|   JsonArray hw_btn_ins = hw[F("btn")][F("ins")]; | ||||
|   if (!hw_btn_ins.isNull()) { | ||||
|     uint8_t s = 0; | ||||
|     for (JsonObject btn : hw_btn_ins) { | ||||
|       CJSON(buttonType[s], btn["type"]); | ||||
|       int8_t pin = btn["pin"][0] | -1; | ||||
|       if (pin > -1 && pinManager.allocatePin(pin,false)) { | ||||
|         btnPin[s] = pin; | ||||
|         pinMode(btnPin[s], INPUT_PULLUP); | ||||
|       } else { | ||||
|         btnPin[s] = -1; | ||||
|       } | ||||
|       JsonArray hw_btn_ins_0_macros = btn[F("macros")]; | ||||
|       CJSON(macroButton[s], hw_btn_ins_0_macros[0]); | ||||
|       CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]); | ||||
|       CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]); | ||||
|       if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached | ||||
|     } | ||||
|     // clear remaining buttons | ||||
|     for (; s<WLED_MAX_BUTTONS; s++) { | ||||
|       btnPin[s]           = -1; | ||||
|       buttonType[s]       = BTN_TYPE_NONE; | ||||
|       macroButton[s]      = 0; | ||||
|       macroLongPress[s]   = 0; | ||||
|       macroDoublePress[s] = 0; | ||||
|     } | ||||
|   } else { | ||||
|     btnPin = -1; | ||||
|     // new install/missing configuration (button 0 has defaults) | ||||
|     if (fromFS) | ||||
|       for (uint8_t s=1; s<WLED_MAX_BUTTONS; s++) { | ||||
|         btnPin[s]           = -1; | ||||
|         buttonType[s]       = BTN_TYPE_NONE; | ||||
|         macroButton[s]      = 0; | ||||
|         macroLongPress[s]   = 0; | ||||
|         macroDoublePress[s] = 0; | ||||
|       } | ||||
|   } | ||||
|   CJSON(touchThreshold,hw[F("btn")][F("tt")]); | ||||
|  | ||||
|   JsonArray hw_btn_ins_0_macros = hw_btn_ins_0[F("macros")]; | ||||
|   CJSON(macroButton, hw_btn_ins_0_macros[0]); | ||||
|   CJSON(macroLongPress,hw_btn_ins_0_macros[1]); | ||||
|   CJSON(macroDoublePress, hw_btn_ins_0_macros[2]); | ||||
|  | ||||
|   //int hw_btn_ins_0_type = hw_btn_ins_0["type"]; // 0 | ||||
|  | ||||
|   #ifndef WLED_DISABLE_INFRARED | ||||
|   int hw_ir_pin = hw["ir"]["pin"] | -1; // 4 | ||||
|   if (pinManager.allocatePin(hw_ir_pin,false)) { | ||||
|     irPin = hw_ir_pin; | ||||
|   } else { | ||||
|     irPin = -1; | ||||
|   int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 | ||||
|   if (hw_ir_pin > -2) { | ||||
|     if (pinManager.allocatePin(hw_ir_pin,false)) { | ||||
|       irPin = hw_ir_pin; | ||||
|     } else { | ||||
|       irPin = -1; | ||||
|     } | ||||
|   } | ||||
|   #endif | ||||
|   CJSON(irEnabled, hw["ir"]["type"]); | ||||
|  | ||||
|   JsonObject relay = hw[F("relay")]; | ||||
|  | ||||
|   int hw_relay_pin = relay["pin"]; | ||||
|   if (pinManager.allocatePin(hw_relay_pin,true)) { | ||||
|     rlyPin = hw_relay_pin; | ||||
|     pinMode(rlyPin, OUTPUT); | ||||
|   } else { | ||||
|     rlyPin = -1; | ||||
|   int hw_relay_pin = relay["pin"] | -2; | ||||
|   if (hw_relay_pin > -2) { | ||||
|     if (pinManager.allocatePin(hw_relay_pin,true)) { | ||||
|       rlyPin = hw_relay_pin; | ||||
|       pinMode(rlyPin, OUTPUT); | ||||
|     } else { | ||||
|       rlyPin = -1; | ||||
|     } | ||||
|   } | ||||
|   if (relay.containsKey("rev")) { | ||||
|     rlyMde = !relay["rev"]; | ||||
|   } | ||||
|  | ||||
|   //int hw_status_pin = hw[F("status")][F("pin")]; // -1 | ||||
|   //int hw_status_pin = hw[F("status")]["pin"]; // -1 | ||||
|  | ||||
|   JsonObject light = doc[F("light")]; | ||||
|   CJSON(briMultiplier, light[F("scale-bri")]); | ||||
| @@ -191,14 +202,15 @@ void deserializeConfig() { | ||||
|  | ||||
|   JsonObject light_tr = light[F("tr")]; | ||||
|   CJSON(fadeTransition, light_tr[F("mode")]); | ||||
|   int tdd = light_tr[F("dur")] | -1; | ||||
|   int tdd = light_tr["dur"] | -1; | ||||
|   if (tdd >= 0) transitionDelayDefault = tdd * 100; | ||||
|   CJSON(strip.paletteFade, light_tr[F("pal")]); | ||||
|   CJSON(strip.paletteFade, light_tr["pal"]); | ||||
|  | ||||
|   JsonObject light_nl = light["nl"]; | ||||
|   CJSON(nightlightMode, light_nl[F("mode")]); | ||||
|   byte prev = nightlightDelayMinsDefault; | ||||
|   CJSON(nightlightDelayMinsDefault, light_nl[F("dur")]); | ||||
|   nightlightDelayMins = nightlightDelayMinsDefault; | ||||
|   if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault; | ||||
|  | ||||
|   CJSON(nightlightTargetBri, light_nl[F("tbri")]); | ||||
|   CJSON(macroNl, light_nl[F("macro")]); | ||||
| @@ -208,15 +220,6 @@ void deserializeConfig() { | ||||
|   CJSON(turnOnAtBoot, def["on"]); // true | ||||
|   CJSON(briS, def["bri"]); // 128 | ||||
|  | ||||
|   JsonObject def_cy = def[F("cy")]; | ||||
|   CJSON(presetCyclingEnabled, def_cy["on"]); | ||||
|  | ||||
|   CJSON(presetCycleMin, def_cy[F("range")][0]); | ||||
|   CJSON(presetCycleMax, def_cy[F("range")][1]); | ||||
|  | ||||
|   tdd = def_cy[F("dur")] | -1; | ||||
|   if (tdd > 0) presetCycleTime = tdd; | ||||
|  | ||||
|   JsonObject interfaces = doc["if"]; | ||||
|  | ||||
|   JsonObject if_sync = interfaces[F("sync")]; | ||||
| @@ -227,11 +230,13 @@ void deserializeConfig() { | ||||
|   CJSON(receiveNotificationBrightness, if_sync_recv["bri"]); | ||||
|   CJSON(receiveNotificationColor, if_sync_recv["col"]); | ||||
|   CJSON(receiveNotificationEffects, if_sync_recv[F("fx")]); | ||||
|   //! following line might be a problem if called after boot | ||||
|   receiveNotifications = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects); | ||||
|  | ||||
|   JsonObject if_sync_send = if_sync["send"]; | ||||
|   prev = notifyDirectDefault; | ||||
|   CJSON(notifyDirectDefault, if_sync_send[F("dir")]); | ||||
|   notifyDirect = notifyDirectDefault; | ||||
|   if (notifyDirectDefault != prev) notifyDirect = notifyDirectDefault; | ||||
|   CJSON(notifyButton, if_sync_send[F("btn")]); | ||||
|   CJSON(notifyAlexa, if_sync_send[F("va")]); | ||||
|   CJSON(notifyHue, if_sync_send[F("hue")]); | ||||
| @@ -264,6 +269,7 @@ void deserializeConfig() { | ||||
|   CJSON(macroAlexaOn, interfaces[F("va")][F("macros")][0]); | ||||
|   CJSON(macroAlexaOff, interfaces[F("va")][F("macros")][1]); | ||||
|  | ||||
| #ifndef WLED_DISABLE_BLYNK | ||||
|   const char* apikey = interfaces["blynk"][F("token")] | "Hidden"; | ||||
|   tdd = strnlen(apikey, 36); | ||||
|   if (tdd > 20 || tdd == 0) | ||||
| @@ -272,18 +278,22 @@ void deserializeConfig() { | ||||
|   JsonObject if_blynk = interfaces["blynk"]; | ||||
|   getStringFromJson(blynkHost, if_blynk[F("host")], 33); | ||||
|   CJSON(blynkPort, if_blynk["port"]); | ||||
| #endif | ||||
|  | ||||
| #ifdef WLED_ENABLE_MQTT | ||||
|   JsonObject if_mqtt = interfaces["mqtt"]; | ||||
|   CJSON(mqttEnabled, if_mqtt["en"]); | ||||
|   getStringFromJson(mqttServer, if_mqtt[F("broker")], 33); | ||||
|   CJSON(mqttPort, if_mqtt["port"]); // 1883 | ||||
|   getStringFromJson(mqttUser, if_mqtt[F("user")], 41); | ||||
|   getStringFromJson(mqttPass, if_mqtt["psk"], 41); //normally not present due to security | ||||
|   getStringFromJson(mqttPass, if_mqtt["psk"], 65); //normally not present due to security | ||||
|   getStringFromJson(mqttClientID, if_mqtt[F("cid")], 41); | ||||
|  | ||||
|   getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], 33); // "wled/test" | ||||
|   getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], 33); // "" | ||||
| #endif | ||||
|  | ||||
| #ifndef WLED_DISABLE_HUESYNC | ||||
|   JsonObject if_hue = interfaces[F("hue")]; | ||||
|   CJSON(huePollingEnabled, if_hue["en"]); | ||||
|   CJSON(huePollLightId, if_hue["id"]); | ||||
| @@ -299,6 +309,7 @@ void deserializeConfig() { | ||||
|  | ||||
|   for (byte i = 0; i < 4; i++) | ||||
|     CJSON(hueIP[i], if_hue_ip[i]); | ||||
| #endif | ||||
|  | ||||
|   JsonObject if_ntp = interfaces[F("ntp")]; | ||||
|   CJSON(ntpEnabled, if_ntp["en"]); | ||||
| @@ -306,13 +317,16 @@ void deserializeConfig() { | ||||
|   CJSON(currentTimezone, if_ntp[F("tz")]); | ||||
|   CJSON(utcOffsetSecs, if_ntp[F("offset")]); | ||||
|   CJSON(useAMPM, if_ntp[F("ampm")]); | ||||
|   CJSON(longitude, if_ntp[F("ln")]); | ||||
|   CJSON(latitude, if_ntp[F("lt")]); | ||||
|  | ||||
|   JsonObject ol = doc[F("ol")]; | ||||
|   prev = overlayDefault; | ||||
|   CJSON(overlayDefault ,ol[F("clock")]); // 0 | ||||
|   CJSON(countdownMode, ol[F("cntdwn")]); | ||||
|   overlayCurrent = overlayDefault; | ||||
|   if (prev != overlayDefault) overlayCurrent = overlayDefault; | ||||
|  | ||||
|   CJSON(overlayMin, ol[F("min")]); | ||||
|   CJSON(overlayMin, ol["min"]); | ||||
|   CJSON(overlayMax, ol[F("max")]); | ||||
|   CJSON(analogClock12pixel, ol[F("o12pix")]); | ||||
|   CJSON(analogClock5MinuteMarks, ol[F("o5m")]); | ||||
| @@ -334,9 +348,10 @@ void deserializeConfig() { | ||||
|   JsonArray timers = tm[F("ins")]; | ||||
|   uint8_t it = 0; | ||||
|   for (JsonObject timer : timers) { | ||||
|     if (it > 7) break; | ||||
|     if (it > 9) break; | ||||
|     if (it<8 && timer[F("hour")]==255) it=8;  // hour==255 -> sunrise/sunset  | ||||
|     CJSON(timerHours[it], timer[F("hour")]); | ||||
|     CJSON(timerMinutes[it], timer[F("min")]); | ||||
|     CJSON(timerMinutes[it], timer["min"]); | ||||
|     CJSON(timerMacro[it], timer[F("macro")]); | ||||
|  | ||||
|     byte dowPrev =  timerWeekday[it]; | ||||
| @@ -382,8 +397,36 @@ void deserializeConfig() { | ||||
|   } | ||||
|   #endif | ||||
|  | ||||
|   DEBUG_PRINTLN(F("Starting usermod config.")); | ||||
|   JsonObject usermods_settings = doc["um"]; | ||||
|   usermods.readFromConfig(usermods_settings); | ||||
|   if (!usermods_settings.isNull()) { | ||||
|     bool allComplete = usermods.readFromConfig(usermods_settings); | ||||
|     if (!allComplete && fromFS) serializeConfig(); | ||||
|   } | ||||
|  | ||||
|   if (fromFS) return false; | ||||
|   doReboot = doc[F("rb")] | doReboot; | ||||
|   return (doc["sv"] | true); | ||||
| } | ||||
|  | ||||
| void deserializeConfigFromFS() { | ||||
|   bool success = deserializeConfigSec(); | ||||
|   if (!success) { //if file does not exist, try reading from EEPROM | ||||
|     deEEPSettings(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   DynamicJsonDocument doc(JSON_BUFFER_SIZE); | ||||
|  | ||||
|   DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); | ||||
|  | ||||
|   success = readObjectFromFile("/cfg.json", nullptr, &doc); | ||||
|   if (!success) { //if file does not exist, try reading from EEPROM | ||||
|     deEEPSettings(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   deserializeConfig(doc.as<JsonObject>(), true);   | ||||
| } | ||||
|  | ||||
| void serializeConfig() { | ||||
| @@ -458,7 +501,6 @@ void serializeConfig() { | ||||
|     Bus *bus = busses.getBus(s); | ||||
|     if (!bus || bus->getLength()==0) break; | ||||
|     JsonObject ins = hw_led_ins.createNestedObject(); | ||||
|     ins["en"] = true; | ||||
|     ins[F("start")] = bus->getStart(); | ||||
|     ins[F("len")] = bus->getLength(); | ||||
|     JsonArray ins_pin = ins.createNestedArray("pin"); | ||||
| @@ -467,31 +509,42 @@ void serializeConfig() { | ||||
|     for (uint8_t i = 0; i < nPins; i++) ins_pin.add(pins[i]); | ||||
|     ins[F("order")] = bus->getColorOrder(); | ||||
|     ins["rev"] = bus->reversed; | ||||
|     ins[F("skip")] = (skipFirstLed && s == 0) ? 1 : 0; | ||||
|     ins[F("skip")] = bus->skippedLeds(); | ||||
|     ins["type"] = bus->getType(); | ||||
|   } | ||||
|  | ||||
|   // button(s) | ||||
|   JsonObject hw_btn = hw.createNestedObject("btn"); | ||||
|  | ||||
|   hw_btn["max"] = WLED_MAX_BUTTONS; // just information about max number of buttons (not actually used) | ||||
|   JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); | ||||
|  | ||||
|   // button BTNPIN | ||||
|   // there is always at least one button | ||||
|   JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject(); | ||||
|   hw_btn_ins_0["type"] = (buttonEnabled) ? BTN_TYPE_PUSH : BTN_TYPE_NONE; | ||||
|  | ||||
|   hw_btn_ins_0["type"] = buttonType[0]; | ||||
|   JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin"); | ||||
|   hw_btn_ins_0_pin.add(btnPin); | ||||
|  | ||||
|   hw_btn_ins_0_pin.add(btnPin[0]); | ||||
|   JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros"); | ||||
|   hw_btn_ins_0_macros.add(macroButton); | ||||
|   hw_btn_ins_0_macros.add(macroLongPress); | ||||
|   hw_btn_ins_0_macros.add(macroDoublePress); | ||||
|   hw_btn_ins_0_macros.add(macroButton[0]); | ||||
|   hw_btn_ins_0_macros.add(macroLongPress[0]); | ||||
|   hw_btn_ins_0_macros.add(macroDoublePress[0]); | ||||
|  | ||||
|   // additional buttons | ||||
|   for (uint8_t i=1; i<WLED_MAX_BUTTONS; i++) { | ||||
|     //if (btnPin[i]<0) continue; | ||||
|     hw_btn_ins_0 = hw_btn_ins.createNestedObject(); | ||||
|     hw_btn_ins_0["type"] = buttonType[i]; | ||||
|     hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin"); | ||||
|     hw_btn_ins_0_pin.add(btnPin[i]); | ||||
|     hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros"); | ||||
|     hw_btn_ins_0_macros.add(macroButton[i]); | ||||
|     hw_btn_ins_0_macros.add(macroLongPress[i]); | ||||
|     hw_btn_ins_0_macros.add(macroDoublePress[i]); | ||||
|   } | ||||
|   hw_btn[F("tt")] = touchThreshold; | ||||
|  | ||||
|   #ifndef WLED_DISABLE_INFRARED | ||||
|   JsonObject hw_ir = hw.createNestedObject("ir"); | ||||
|   hw_ir["pin"] = irPin; | ||||
|   hw_ir[F("type")] = irEnabled;              // the byte 'irEnabled' does contain the IR-Remote Type ( 0=disabled ) | ||||
|   #endif | ||||
|  | ||||
|   JsonObject hw_relay = hw.createNestedObject(F("relay")); | ||||
|   hw_relay["pin"] = rlyPin; | ||||
| @@ -500,9 +553,6 @@ void serializeConfig() { | ||||
|   //JsonObject hw_status = hw.createNestedObject("status"); | ||||
|   //hw_status["pin"] = -1; | ||||
|  | ||||
|   JsonObject hw_aux = hw.createNestedObject("aux"); | ||||
|   hw_aux["pin"] = auxPin; | ||||
|  | ||||
|   JsonObject light = doc.createNestedObject(F("light")); | ||||
|   light[F("scale-bri")] = briMultiplier; | ||||
|   light[F("pal-mode")] = strip.paletteBlend; | ||||
| @@ -513,12 +563,12 @@ void serializeConfig() { | ||||
|  | ||||
|   JsonObject light_tr = light.createNestedObject("tr"); | ||||
|   light_tr[F("mode")] = fadeTransition; | ||||
|   light_tr[F("dur")] = transitionDelayDefault / 100; | ||||
|   light_tr[F("pal")] = strip.paletteFade; | ||||
|   light_tr["dur"] = transitionDelayDefault / 100; | ||||
|   light_tr["pal"] = strip.paletteFade; | ||||
|  | ||||
|   JsonObject light_nl = light.createNestedObject("nl"); | ||||
|   light_nl[F("mode")] = nightlightMode; | ||||
|   light_nl[F("dur")] = nightlightDelayMinsDefault; | ||||
|   light_nl["dur"] = nightlightDelayMinsDefault; | ||||
|   light_nl[F("tbri")] = nightlightTargetBri; | ||||
|   light_nl[F("macro")] = macroNl; | ||||
|  | ||||
| @@ -527,17 +577,6 @@ void serializeConfig() { | ||||
|   def["on"] = turnOnAtBoot; | ||||
|   def["bri"] = briS; | ||||
|  | ||||
|   //to be removed once preset cycles are presets | ||||
|   if (saveCurrPresetCycConf) { | ||||
|     JsonObject def_cy = def.createNestedObject("cy"); | ||||
|     def_cy["on"] = presetCyclingEnabled; | ||||
|  | ||||
|     JsonArray def_cy_range = def_cy.createNestedArray(F("range")); | ||||
|     def_cy_range.add(presetCycleMin); | ||||
|     def_cy_range.add(presetCycleMax); | ||||
|     def_cy[F("dur")] = presetCycleTime; | ||||
|   } | ||||
|  | ||||
|   JsonObject interfaces = doc.createNestedObject("if"); | ||||
|  | ||||
|   JsonObject if_sync = interfaces.createNestedObject("sync"); | ||||
| @@ -547,7 +586,7 @@ void serializeConfig() { | ||||
|   JsonObject if_sync_recv = if_sync.createNestedObject("recv"); | ||||
|   if_sync_recv["bri"] = receiveNotificationBrightness; | ||||
|   if_sync_recv["col"] = receiveNotificationColor; | ||||
|   if_sync_recv[F("fx")] = receiveNotificationEffects; | ||||
|   if_sync_recv["fx"] = receiveNotificationEffects; | ||||
|  | ||||
|   JsonObject if_sync_send = if_sync.createNestedObject("send"); | ||||
|   if_sync_send[F("dir")] = notifyDirect; | ||||
| @@ -582,11 +621,15 @@ void serializeConfig() { | ||||
|   JsonArray if_va_macros = if_va.createNestedArray("macros"); | ||||
|   if_va_macros.add(macroAlexaOn); | ||||
|   if_va_macros.add(macroAlexaOff); | ||||
|  | ||||
| #ifndef WLED_DISABLE_BLYNK | ||||
|   JsonObject if_blynk = interfaces.createNestedObject("blynk"); | ||||
|   if_blynk[F("token")] = strlen(blynkApiKey) ? "Hidden":""; | ||||
|   if_blynk[F("host")] = blynkHost; | ||||
|   if_blynk["port"] = blynkPort; | ||||
| #endif | ||||
|  | ||||
| #ifdef WLED_ENABLE_MQTT | ||||
|   JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); | ||||
|   if_mqtt["en"] = mqttEnabled; | ||||
|   if_mqtt[F("broker")] = mqttServer; | ||||
| @@ -598,7 +641,9 @@ void serializeConfig() { | ||||
|   JsonObject if_mqtt_topics = if_mqtt.createNestedObject(F("topics")); | ||||
|   if_mqtt_topics[F("device")] = mqttDeviceTopic; | ||||
|   if_mqtt_topics[F("group")] = mqttGroupTopic; | ||||
| #endif | ||||
|  | ||||
| #ifndef WLED_DISABLE_HUESYNC | ||||
|   JsonObject if_hue = interfaces.createNestedObject("hue"); | ||||
|   if_hue["en"] = huePollingEnabled; | ||||
|   if_hue["id"] = huePollLightId; | ||||
| @@ -613,6 +658,7 @@ void serializeConfig() { | ||||
|   for (byte i = 0; i < 4; i++) { | ||||
|     if_hue_ip.add(hueIP[i]); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   JsonObject if_ntp = interfaces.createNestedObject("ntp"); | ||||
|   if_ntp["en"] = ntpEnabled; | ||||
| @@ -620,12 +666,14 @@ void serializeConfig() { | ||||
|   if_ntp[F("tz")] = currentTimezone; | ||||
|   if_ntp[F("offset")] = utcOffsetSecs; | ||||
|   if_ntp[F("ampm")] = useAMPM; | ||||
|   if_ntp[F("ln")] = longitude; | ||||
|   if_ntp[F("lt")] = latitude; | ||||
|  | ||||
|   JsonObject ol = doc.createNestedObject("ol"); | ||||
|   ol[F("clock")] = overlayDefault; | ||||
|   ol[F("cntdwn")] = countdownMode; | ||||
|  | ||||
|   ol[F("min")] = overlayMin; | ||||
|   ol["min"] = overlayMin; | ||||
|   ol[F("max")] = overlayMax; | ||||
|   ol[F("o12pix")] = analogClock12pixel; | ||||
|   ol[F("o5m")] = analogClock5MinuteMarks; | ||||
| @@ -641,12 +689,12 @@ void serializeConfig() { | ||||
|  | ||||
|   JsonArray timers_ins = timers.createNestedArray("ins"); | ||||
|  | ||||
|   for (byte i = 0; i < 8; i++) { | ||||
|     if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; | ||||
|   for (byte i = 0; i < 10; i++) { | ||||
|     if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255) | ||||
|     JsonObject timers_ins0 = timers_ins.createNestedObject(); | ||||
|     timers_ins0["en"] = (timerWeekday[i] & 0x01); | ||||
|     timers_ins0[F("hour")] = timerHours[i]; | ||||
|     timers_ins0[F("min")] = timerMinutes[i]; | ||||
|     timers_ins0["min"] = timerMinutes[i]; | ||||
|     timers_ins0[F("macro")] = timerMacro[i]; | ||||
|     timers_ins0[F("dow")] = timerWeekday[i] >> 1; | ||||
|   } | ||||
| @@ -668,7 +716,6 @@ void serializeConfig() { | ||||
|   for (byte i = 0; i < 15; i++) | ||||
|     dmx_fixmap.add(DMXFixtureMap[i]); | ||||
|   #endif | ||||
|   //} | ||||
|  | ||||
|   JsonObject usermods_settings = doc.createNestedObject("um"); | ||||
|   usermods.addToConfig(usermods_settings); | ||||
| @@ -687,7 +734,7 @@ bool deserializeConfigSec() { | ||||
|   bool success = readObjectFromFile("/wsec.json", nullptr, &doc); | ||||
|   if (!success) return false; | ||||
|  | ||||
|   JsonObject nw_ins_0 = doc["nw"][F("ins")][0]; | ||||
|   JsonObject nw_ins_0 = doc["nw"]["ins"][0]; | ||||
|   getStringFromJson(clientPass, nw_ins_0["psk"], 65); | ||||
|  | ||||
|   JsonObject ap = doc["ap"]; | ||||
| @@ -695,15 +742,21 @@ bool deserializeConfigSec() { | ||||
|  | ||||
|   JsonObject interfaces = doc["if"]; | ||||
|  | ||||
| #ifndef WLED_DISABLE_BLYNK | ||||
|   const char* apikey = interfaces["blynk"][F("token")] | "Hidden"; | ||||
|   int tdd = strnlen(apikey, 36); | ||||
|   if (tdd > 20 || tdd == 0) | ||||
|     getStringFromJson(blynkApiKey, apikey, 36); | ||||
| #endif | ||||
|  | ||||
| #ifdef WLED_ENABLE_MQTT | ||||
|   JsonObject if_mqtt = interfaces["mqtt"]; | ||||
|   getStringFromJson(mqttPass, if_mqtt["psk"], 41); | ||||
|   getStringFromJson(mqttPass, if_mqtt["psk"], 65); | ||||
| #endif | ||||
|  | ||||
| #ifndef WLED_DISABLE_HUESYNC | ||||
|   getStringFromJson(hueApiKey, interfaces[F("hue")][F("key")], 47); | ||||
| #endif | ||||
|  | ||||
|   JsonObject ota = doc["ota"]; | ||||
|   getStringFromJson(otaPass, ota[F("pwd")], 33); | ||||
| @@ -730,12 +783,18 @@ void serializeConfigSec() { | ||||
|   ap["psk"] = apPass; | ||||
|  | ||||
|   JsonObject interfaces = doc.createNestedObject("if"); | ||||
| #ifndef WLED_DISABLE_BLYNK | ||||
|   JsonObject if_blynk = interfaces.createNestedObject("blynk"); | ||||
|   if_blynk[F("token")] = blynkApiKey; | ||||
| #endif | ||||
| #ifdef WLED_ENABLE_MQTT | ||||
|   JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); | ||||
|   if_mqtt["psk"] = mqttPass; | ||||
| #endif | ||||
| #ifndef WLED_DISABLE_HUESYNC | ||||
|   JsonObject if_hue = interfaces.createNestedObject("hue"); | ||||
|   if_hue[F("key")] = hueApiKey; | ||||
| #endif | ||||
|  | ||||
|   JsonObject ota = doc.createNestedObject("ota"); | ||||
|   ota[F("pwd")] = otaPass; | ||||
|   | ||||
							
								
								
									
										119
									
								
								wled00/const.h
									
									
									
									
									
								
							
							
						
						
									
										119
									
								
								wled00/const.h
									
									
									
									
									
								
							| @@ -11,42 +11,68 @@ | ||||
| #define DEFAULT_OTA_PASS    "wledota" | ||||
|  | ||||
| //increase if you need more | ||||
| #define WLED_MAX_USERMODS 4 | ||||
| #ifndef WLED_MAX_USERMODS | ||||
|   #ifdef ESP8266 | ||||
|     #define WLED_MAX_USERMODS 4 | ||||
|   #else | ||||
|     #define WLED_MAX_USERMODS 6 | ||||
|   #endif | ||||
| #endif | ||||
|  | ||||
| #ifdef ESP8266 | ||||
| #define WLED_MAX_BUSSES 3 | ||||
| #else | ||||
| #define WLED_MAX_BUSSES 10 | ||||
| #ifndef WLED_MAX_BUSSES | ||||
|   #ifdef ESP8266 | ||||
|     #define WLED_MAX_BUSSES 3 | ||||
|   #else | ||||
|     #ifdef CONFIG_IDF_TARGET_ESP32S2 | ||||
|       #define WLED_MAX_BUSSES 5 | ||||
|     #else | ||||
|       #define WLED_MAX_BUSSES 10 | ||||
|     #endif | ||||
|   #endif | ||||
| #endif | ||||
|  | ||||
| #ifndef WLED_MAX_BUTTONS | ||||
|   #ifdef ESP8266 | ||||
|     #define WLED_MAX_BUTTONS 2 | ||||
|   #else | ||||
|     #define WLED_MAX_BUTTONS 4 | ||||
|   #endif | ||||
| #endif | ||||
|  | ||||
| //Usermod IDs | ||||
| #define USERMOD_ID_RESERVED       0            //Unused. Might indicate no usermod present | ||||
| #define USERMOD_ID_UNSPECIFIED    1            //Default value for a general user mod that does not specify a custom ID | ||||
| #define USERMOD_ID_EXAMPLE        2            //Usermod "usermod_v2_example.h" | ||||
| #define USERMOD_ID_TEMPERATURE    3            //Usermod "usermod_temperature.h" | ||||
| #define USERMOD_ID_FIXNETSERVICES 4            //Usermod "usermod_Fix_unreachable_netservices.h" | ||||
| #define USERMOD_ID_PIRSWITCH      5            //Usermod "usermod_PIR_sensor_switch.h" | ||||
| #define USERMOD_ID_IMU            6            //Usermod "usermod_mpu6050_imu.h" | ||||
| #define USERMOD_ID_FOUR_LINE_DISP 7            //Usermod "usermod_v2_four_line_display.h  | ||||
| #define USERMOD_ID_ROTARY_ENC_UI  8            //Usermod "usermod_v2_rotary_encoder_ui.h" | ||||
| #define USERMOD_ID_AUTO_SAVE      9            //Usermod "usermod_v2_auto_save.h" | ||||
| #define USERMOD_ID_DHT           10            //Usermod "usermod_dht.h" | ||||
| #define USERMOD_ID_MODE_SORT     11            //Usermod "usermod_v2_mode_sort.h" | ||||
| #define USERMOD_ID_RESERVED               0     //Unused. Might indicate no usermod present | ||||
| #define USERMOD_ID_UNSPECIFIED            1     //Default value for a general user mod that does not specify a custom ID | ||||
| #define USERMOD_ID_EXAMPLE                2     //Usermod "usermod_v2_example.h" | ||||
| #define USERMOD_ID_TEMPERATURE            3     //Usermod "usermod_temperature.h" | ||||
| #define USERMOD_ID_FIXNETSERVICES         4     //Usermod "usermod_Fix_unreachable_netservices.h" | ||||
| #define USERMOD_ID_PIRSWITCH              5     //Usermod "usermod_PIR_sensor_switch.h" | ||||
| #define USERMOD_ID_IMU                    6     //Usermod "usermod_mpu6050_imu.h" | ||||
| #define USERMOD_ID_FOUR_LINE_DISP         7     //Usermod "usermod_v2_four_line_display.h | ||||
| #define USERMOD_ID_ROTARY_ENC_UI          8     //Usermod "usermod_v2_rotary_encoder_ui.h" | ||||
| #define USERMOD_ID_AUTO_SAVE              9     //Usermod "usermod_v2_auto_save.h" | ||||
| #define USERMOD_ID_DHT                   10     //Usermod "usermod_dht.h" | ||||
| #define USERMOD_ID_MODE_SORT             11     //Usermod "usermod_v2_mode_sort.h" | ||||
| #define USERMOD_ID_VL53L0X               12     //Usermod "usermod_vl53l0x_gestures.h" | ||||
| #define USERMOD_ID_MULTI_RELAY           13     //Usermod "usermod_multi_relay.h" | ||||
| #define USERMOD_ID_ANIMATED_STAIRCASE    14     //Usermod "Animated_Staircase.h" | ||||
| #define USERMOD_ID_RTC                   15     //Usermod "usermod_rtc.h" | ||||
| #define USERMOD_ID_ELEKSTUBE_IPS         16     //Usermod "usermod_elekstube_ips.h" | ||||
| #define USERMOD_ID_SN_PHOTORESISTOR      17     //Usermod "usermod_sn_photoresistor.h" | ||||
|  | ||||
| //Access point behavior | ||||
| #define AP_BEHAVIOR_BOOT_NO_CONN  0            //Open AP when no connection after boot | ||||
| #define AP_BEHAVIOR_NO_CONN       1            //Open when no connection (either after boot or if connection is lost) | ||||
| #define AP_BEHAVIOR_ALWAYS        2            //Always open | ||||
| #define AP_BEHAVIOR_BUTTON_ONLY   3            //Only when button pressed for 6 sec | ||||
| #define AP_BEHAVIOR_BOOT_NO_CONN          0     //Open AP when no connection after boot | ||||
| #define AP_BEHAVIOR_NO_CONN               1     //Open when no connection (either after boot or if connection is lost) | ||||
| #define AP_BEHAVIOR_ALWAYS                2     //Always open | ||||
| #define AP_BEHAVIOR_BUTTON_ONLY           3     //Only when button pressed for 6 sec | ||||
|  | ||||
| //Notifier callMode  | ||||
| #define NOTIFIER_CALL_MODE_INIT           0    //no updates on init, can be used to disable updates | ||||
| //Notifier callMode | ||||
| #define NOTIFIER_CALL_MODE_INIT           0     //no updates on init, can be used to disable updates | ||||
| #define NOTIFIER_CALL_MODE_DIRECT_CHANGE  1 | ||||
| #define NOTIFIER_CALL_MODE_BUTTON         2 | ||||
| #define NOTIFIER_CALL_MODE_NOTIFICATION   3 | ||||
| #define NOTIFIER_CALL_MODE_NIGHTLIGHT     4 | ||||
| #define NOTIFIER_CALL_MODE_NO_NOTIFY      5 | ||||
| #define NOTIFIER_CALL_MODE_FX_CHANGED     6    //no longer used | ||||
| #define NOTIFIER_CALL_MODE_FX_CHANGED     6     //no longer used | ||||
| #define NOTIFIER_CALL_MODE_HUE            7 | ||||
| #define NOTIFIER_CALL_MODE_PRESET_CYCLE   8 | ||||
| #define NOTIFIER_CALL_MODE_BLYNK          9 | ||||
| @@ -56,7 +82,7 @@ | ||||
| #define RGBW_MODE_MANUAL_ONLY     0            //No automatic white channel calculation. Manual white channel slider | ||||
| #define RGBW_MODE_AUTO_BRIGHTER   1            //New algorithm. Adds as much white as the darkest RGBW channel | ||||
| #define RGBW_MODE_AUTO_ACCURATE   2            //New algorithm. Adds as much white as the darkest RGBW channel and subtracts this amount from each RGB channel | ||||
| #define RGBW_MODE_DUAL            3            //Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0)   | ||||
| #define RGBW_MODE_DUAL            3            //Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0) | ||||
| #define RGBW_MODE_LEGACY          4            //Old floating algorithm. Too slow for realtime and palette support | ||||
|  | ||||
| //realtime modes | ||||
| @@ -115,10 +141,10 @@ | ||||
| #define TYPE_LPD8806             52 | ||||
| #define TYPE_P9813               53 | ||||
|  | ||||
| #define IS_DIGITAL(t) (t & 0x10) //digital are 16-31 and 48-63 | ||||
| #define IS_PWM(t)     (t > 40 && t < 46) | ||||
| #define NUM_PWM_PINS(t) (t - 40) //for analog PWM 41-45 only | ||||
| #define IS_2PIN(t)      (t > 47) | ||||
| #define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63 | ||||
| #define IS_PWM(t)     ((t) > 40 && (t) < 46) | ||||
| #define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only | ||||
| #define IS_2PIN(t)      ((t) > 47) | ||||
|  | ||||
| //Color orders | ||||
| #define COL_ORDER_GRB             0           //GRB(w),defaut | ||||
| @@ -133,18 +159,22 @@ | ||||
| #define BTN_TYPE_NONE             0 | ||||
| #define BTN_TYPE_RESERVED         1 | ||||
| #define BTN_TYPE_PUSH             2 | ||||
| #define BTN_TYPE_PUSH_ACT_HIGH    3 //not implemented | ||||
| #define BTN_TYPE_SWITCH           4 //not implemented | ||||
| #define BTN_TYPE_SWITCH_ACT_HIGH  5 //not implemented | ||||
| #define BTN_TYPE_PUSH_ACT_HIGH    3 | ||||
| #define BTN_TYPE_SWITCH           4 | ||||
| #define BTN_TYPE_SWITCH_ACT_HIGH  5 | ||||
| #define BTN_TYPE_TOUCH            6 | ||||
| #define BTN_TYPE_ANALOG           7 | ||||
|  | ||||
| //Ethernet board types | ||||
| #define WLED_NUM_ETH_TYPES        5 | ||||
| #define WLED_NUM_ETH_TYPES        7 | ||||
|  | ||||
| #define WLED_ETH_NONE             0 | ||||
| #define WLED_ETH_WT32_ETH01       1 | ||||
| #define WLED_ETH_ESP32_POE        2 | ||||
| #define WLED_ETH_WESP32           3 | ||||
| #define WLED_ETH_QUINLED          4 | ||||
| #define WLED_ETH_TWILIGHTLORD     5 | ||||
| #define WLED_ETH_ESP32DEUX        6 | ||||
|  | ||||
| //Hue error codes | ||||
| #define HUE_ERROR_INACTIVE        0 | ||||
| @@ -164,6 +194,9 @@ | ||||
| #define SEG_OPTION_FREEZE         5            //Segment contents will not be refreshed | ||||
| #define SEG_OPTION_TRANSITIONAL   7 | ||||
|  | ||||
| //Playlist option byte | ||||
| #define PL_OPTION_SHUFFLE      0x01 | ||||
|  | ||||
| // WLED Error modes | ||||
| #define ERR_NONE         0  // All good :) | ||||
| #define ERR_EEP_COMMIT   2  // Could not commit to EEPROM (wrong flash layout?) | ||||
| @@ -188,7 +221,7 @@ | ||||
| // maximum number of LEDs - more than 1500 LEDs (or 500 DMA "LEDPIN 3" driven ones) will cause a low memory condition on ESP8266 | ||||
| #ifndef MAX_LEDS | ||||
| #ifdef ESP8266 | ||||
| #define MAX_LEDS 8192 //rely on memory limit to limit this to 1600 LEDs | ||||
| #define MAX_LEDS 1664 // can't rely on memory limit to limit this to 1600 LEDs | ||||
| #else | ||||
| #define MAX_LEDS 8192 | ||||
| #endif | ||||
| @@ -209,7 +242,11 @@ | ||||
| // string temp buffer (now stored in stack locally) | ||||
| #define OMAX 2048 | ||||
|  | ||||
| #define E131_MAX_UNIVERSE_COUNT 9 | ||||
| #ifdef WLED_USE_ETHERNET | ||||
| #define E131_MAX_UNIVERSE_COUNT 20 | ||||
| #else | ||||
| #define E131_MAX_UNIVERSE_COUNT 10 | ||||
| #endif | ||||
|  | ||||
| #define ABL_MILLIAMPS_DEFAULT 850  // auto lower brightness to stay close to milliampere limit | ||||
|  | ||||
| @@ -228,7 +265,7 @@ | ||||
| #ifdef ESP8266 | ||||
|   #define JSON_BUFFER_SIZE 9216 | ||||
| #else | ||||
|   #define JSON_BUFFER_SIZE 16384 | ||||
|   #define JSON_BUFFER_SIZE 20480 | ||||
| #endif | ||||
|  | ||||
| // Maximum size of node map (list of other WLED instances) | ||||
| @@ -240,7 +277,11 @@ | ||||
|  | ||||
| //this is merely a default now and can be changed at runtime | ||||
| #ifndef LEDPIN | ||||
| #define LEDPIN 2 | ||||
| #ifdef ESP8266 | ||||
|   #define LEDPIN 2    // GPIO2 (D4) on Wemod D1 mini compatible boards | ||||
| #else | ||||
|   #define LEDPIN 16   // alligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| #ifdef WLED_ENABLE_DMX | ||||
| @@ -251,4 +292,8 @@ | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| #ifndef DEFAULT_LED_COUNT | ||||
|   #define DEFAULT_LED_COUNT 30 | ||||
| #endif | ||||
|  | ||||
| #endif | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
| 	</style> | ||||
| </head> | ||||
| <body> | ||||
| <img alt="" src=" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsEAAA7BAbiRa+0AAAFMSURBVFhH7ZTfbYNADMaPKDNUrJA85CEjdIjOgNQV+sASlZgmI/AIK6AuQfngnDrmjtpHItQ/P+l0juHsz2cH9+fJ/G7nreldfnDnp+ln/ZIlxbIfQmIwJOekCrEJ8FUvASEWEXoBiuSERcTO75uhuwFWff86bi57n3ZC+rW3YLqB5rn11ldCEPNr2LwFJgHHy8G1bTsu3oKYX4N5BrQ8ZAYewSoBGDjr0ElWCUC/rT2X7MqynL7tG4Dc45BwEYM9H5w7DqHMdfNCURR9nue3Iobk55MtOYeLoOQ8vmoG6o+0FaLrOm9FwC3wayLgx5I2WHpGIGYorulfgPYQ3AZLz4hQ9TMBVVVleJGrRUWz2YgQOg8bPjzzrit7vwcRQb5NTiARRPPzMYItoCpoWZITMkao+mRkddpqQ6z6FN+DfwFJrOm55GfewC/CuU/E4tQYg7BPYQAAAABJRU5ErkJggg=="> | ||||
| <img alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAbUExURQAAAAB81gCU/zKq/////9bW1oCAgGhoaAAAAGPLX8AAAAAJdFJOU///////////AFNPeBIAAAAJcEhZcwAADsAAAA7AAWrWiQkAAACdSURBVDhPxc9bDoUgEANQebP/FUuHMjBGY/B+3EYR7RH0qC/ZBc6HwCljgHO+xZIVSI2sYgHaG7EBWh8jWoxTrCBFdDJ+BD4lbIHxAcz8APAVLTsrZE4eQD5qzt3cAFTYokC4YCN9Gybgu4yAQtBFLQXHuHABA7JMeOEC/E0W5uy9gv4vo5QHK2i7yq2C8UABM4HmL+CSTXCTF1DrCX6+Gp9zB5dsAAAAAElFTkSuQmCC"> | ||||
| <h1>404 Not Found</h1> | ||||
| <b>Akemi does not know where you are headed...</b><br><br> | ||||
| <button onclick="window.location.href='/sliders'">Back to controls</button> | ||||
|   | ||||
| @@ -402,9 +402,8 @@ button { | ||||
| } | ||||
|  | ||||
| #imgw { | ||||
| 	width: 200px; | ||||
| 	height: 55px; | ||||
| 	display: inline-block; | ||||
| 	margin: 8px; | ||||
| } | ||||
|  | ||||
| #kv, #kn { | ||||
| @@ -432,6 +431,12 @@ img { | ||||
| 	max-height: 100%; | ||||
| } | ||||
|  | ||||
| .wi { | ||||
| 	image-rendering: pixelated; | ||||
| 	image-rendering: crisp-edges; | ||||
| 	width: 210px; | ||||
| } | ||||
|  | ||||
| @keyframes fadein { | ||||
| 	from {bottom: 0; opacity: 0;} | ||||
| 	to {bottom: calc(var(--bh) + 22px); opacity: 1;} | ||||
| @@ -519,12 +524,16 @@ input[type=range]:active + .sliderbubble { | ||||
| 	position: relative; | ||||
| } | ||||
|  | ||||
| .sbs { | ||||
| 	margin: 0px -20px 5px -6px; | ||||
| } | ||||
|  | ||||
| .sws { | ||||
| 	width: 212px; | ||||
| 	width: 230px; | ||||
| } | ||||
|  | ||||
| .sis { | ||||
| 	width: 192px !important; | ||||
| 	width: 214px !important; | ||||
| } | ||||
|  | ||||
| .hd { | ||||
| @@ -561,12 +570,11 @@ input[type=range]:active + .sliderbubble { | ||||
| } | ||||
|  | ||||
| .btn-s { | ||||
| 	padding: 9px; | ||||
| 	width: 276px; | ||||
| 	background-color: var(--c-2); | ||||
| } | ||||
| .btn-i { | ||||
| 	padding-bottom: 3px; | ||||
| 	padding-bottom: 4px; | ||||
| } | ||||
| .btn-icon { | ||||
| 	margin: 0px 8px 4px 0; | ||||
| @@ -578,6 +586,19 @@ input[type=range]:active + .sliderbubble { | ||||
| .btn-p { | ||||
| 	width: 216px; | ||||
| } | ||||
| .btn-xs { | ||||
|   width: 39px; | ||||
| } | ||||
| .btn-pl-add { | ||||
|   position: absolute; | ||||
|   bottom: -29px; | ||||
|   left: 94px; | ||||
| } | ||||
| .btn-pl-del { | ||||
| 	position: absolute; | ||||
| 	right: 2px; | ||||
| 	bottom: -3px; | ||||
| } | ||||
|  | ||||
| #qcs-w { | ||||
| 	margin-top: 10px; | ||||
| @@ -601,6 +622,16 @@ input[type=range]:active + .sliderbubble { | ||||
| 	width: 42px; | ||||
| } | ||||
|  | ||||
| .sel-pl { | ||||
|   width: 200px; | ||||
|   background-position: 176px 16px; | ||||
| } | ||||
|  | ||||
| .sel-ple { | ||||
| 	width: 216px; | ||||
|   background-position: 192px 16px; | ||||
| } | ||||
|  | ||||
| select { | ||||
| 	-webkit-appearance: none; | ||||
| 	-moz-appearance: none; | ||||
| @@ -665,6 +696,11 @@ input[type=text] { | ||||
| 	margin: 26px 0 6px 12px !important; | ||||
| } | ||||
|  | ||||
| .plentry { | ||||
|   margin-top: 16px; | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| .stxt { | ||||
| 	width: 50px !important; | ||||
| } | ||||
| @@ -741,9 +777,9 @@ input[type=number]::-webkit-outer-spin-button { | ||||
|  | ||||
| .cnf-s { | ||||
| 	position: absolute; | ||||
| 	top: 66px; | ||||
| 	right: 28px; | ||||
| 	padding: 43px 6px; | ||||
| 	top: 173px; | ||||
| 	right: 23px; | ||||
| 	padding: 7px 22px; | ||||
| } | ||||
|  | ||||
| .pwr { | ||||
| @@ -1016,6 +1052,16 @@ input[type="text"].search:not(:placeholder-shown) { | ||||
| 	color: red; | ||||
| } | ||||
|  | ||||
| .hrz { | ||||
|   width: auto; | ||||
|   height: 2px; | ||||
|   background-color: var(--c-e); | ||||
| } | ||||
| .hrz-pl { | ||||
|   margin: 20px 0; | ||||
| 	position: relative; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar { | ||||
| 	width: 6px; | ||||
| } | ||||
|   | ||||
| @@ -165,15 +165,7 @@ | ||||
| 		<div id="pcont"> | ||||
| 			Loading... | ||||
| 		</div><br> | ||||
| 		 <label class="check psvl"> | ||||
| 			Preset cycle | ||||
| 			<input type="checkbox" id="cyToggle" onchange="toggleCY()"> | ||||
| 			<span class="checkmark psv"></span> | ||||
| 		</label><br> | ||||
| 		First preset: <input id="cycs" class="noslide" type="number" min="1" max="249" value="1"><br> | ||||
| 		Last preset: <input id="cyce" class="noslide" type="number" min="2" max="250" value="3"><br> | ||||
| 		Time per preset: <input id="cyct" class="noslide" type="number" min="0.2" max="6553.5" step="0.1" value="1.2">s<br> | ||||
| 		Transition: <input id="cyctt" class="noslide" type="number" min="0" max="65.5" step="0.1" value="0.7">s | ||||
| 		Transition <input id="tt" class="noslide" type="number" min="0" max="65.5" step="0.1" value="0.7">s | ||||
| 	</div> | ||||
| </div> | ||||
|  | ||||
| @@ -189,7 +181,7 @@ | ||||
| <div id="namelabel" onclick="toggleNodes()"></div> | ||||
| <div id="info" class="modal"> | ||||
| 	<div id="imgw"> | ||||
| 		<img alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAggAAACMCAYAAAAZQlGEAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKLSURBVHhe7dgxjtwwEADBpf//5zUDwklnpzFAnKoSTigNFTT0AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGDcOieX+G5nvNLaznil6f1Nv+/tz3c7+3tmen/Tpu/jbe877c85AQD+EQgAQAgEACAEAgAQAgEACIEAAIRAAABCIAAAIRAAgBAIAEAIBAAgBAIAEAIBAAiBAACEQAAAQiAAACEQAIAQCABACAQAINY5+aHvdsYRazvjK9jfM7fvz/0+Y3+/2+336w8CABACAQAIgQAAhEAAAEIgAAAhEACAEAgAQAgEACAEAgAQAgEACIEAAIRAAABCIAAAIRAAgBAIAEAIBAAgBAIAEAIBAAiBAADEOudrfLczXmltZ3yF6fuwv2em9+d+n5ne3zT3cZfp+/AHAQAIgQAAhEAAAEIgAAAhEACAEAgAQAgEACAEAgAQAgEACIEAAIRAAABCIAAAIRAAgBAIAEAIBAAgBAIAEAIBAAiBAACEQAAAYp3zNb7bGUes7Yz8wO334fmeuf35bmd/z9y+v9ufzx8EACAEAgAQAgEACIEAAIRAAABCIAAAIRAAgBAIAEAIBAAgBAIAEAIBAAiBAACEQAAAQiAAACEQAIAQCABACAQAIAQCABACAQCIdc4x3+2MV1rbGfmFpr+/6e/l9ue73fT+pt1+H2/bn+/lGX8QAIAQCABACAQAIAQCABACAQAIgQAAhEAAAEIgAAAhEACAEAgAQAgEACAEAgAQAgEACIEAAIRAAABCIAAAIRAAgBAIAEAIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP/u8/kLYCqAxINTyZkAAAAASUVORK5CYII="> | ||||
| 		<img class="wi" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAFCAYAAAC5Fuf5AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABbSURBVChTlY9bDoAwDMNW7n9nwCipytQN4Z8tbrTHmDmF4oPzyldwRqp1SSdnV/NuZuzqerAByxXznBw3igkeFEfXyUuhK/yFM0CxJfyqXZEOc6/Sr9/bf7uIC5Nwd7orMvAPAAAAAElFTkSuQmCC" /> | ||||
| 	</div><br> | ||||
| <div id="kv">Loading...</div><br> | ||||
| <button class="btn infobtn" onclick="requestJson(null)">Refresh</button> | ||||
|   | ||||
| @@ -14,11 +14,12 @@ var currentPreset = -1; | ||||
| var lastUpdate = 0; | ||||
| var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0; | ||||
| var pcMode = false, pcModeA = false, lastw = 0; | ||||
| var tr = 7; | ||||
| var d = document; | ||||
| const ranges = RangeTouch.setup('input[type="range"]', {}); | ||||
| var palettesData; | ||||
| var pJson = {}; | ||||
| var pN = "", pI = 0; | ||||
| var pN = "", pI = 0, pNum = 0; | ||||
| var pmt = 1, pmtLS = 0, pmtLast = 0; | ||||
| var lastinfo = {}; | ||||
| var cfg = { | ||||
| @@ -151,12 +152,25 @@ function cTheme(light) { | ||||
|  | ||||
| function loadBg(iUrl) { | ||||
| 	let bg = d.getElementById('bg'); | ||||
|   let img = d.createElement("img"); | ||||
|   img.src = iUrl; | ||||
|   if (iUrl == "") { | ||||
|     var today = new Date(); | ||||
|     if (today.getMonth() == 11 && (today.getDate() > 23 && today.getDate() < 28)) img.src = "https://aircoookie.github.io/xmas.png"; | ||||
|   } | ||||
| 	let img = d.createElement("img"); | ||||
| 	img.src = iUrl; | ||||
| 	if (iUrl == "") { | ||||
| 		var today = new Date(); | ||||
| 		var hol = [ | ||||
| 			[0,11,24,4,"https://aircoookie.github.io/xmas.png"], // christmas | ||||
| 			[0,2,17,1,"https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day | ||||
| 			[2022,3,17,2,"https://aircoookie.github.io/easter.png"], | ||||
| 			[2023,3,9,2,"https://aircoookie.github.io/easter.png"], | ||||
| 			[2024,2,31,2,"https://aircoookie.github.io/easter.png"] | ||||
| 		]; | ||||
| 		for (var i=0; i<hol.length; i++) { | ||||
| 			var yr = hol[i][0]==0 ? today.getFullYear() : hol[i][0]; | ||||
| 			var hs = new Date(yr,hol[i][1],hol[i][2]); | ||||
| 			var he = new Date(hs); | ||||
| 			he.setDate(he.getDate() + hol[i][3]); | ||||
| 			if (today>=hs && today<=he)	img.src = hol[i][4]; | ||||
| 		} | ||||
| 	} | ||||
| 	img.addEventListener('load', (event) => { | ||||
| 		var a = parseFloat(cfg.theme.alpha.bg); | ||||
| 		if (isNaN(a)) a = 0.6; | ||||
| @@ -291,6 +305,10 @@ function pName(i) { | ||||
| 	return n; | ||||
| } | ||||
|  | ||||
| function isPlaylist(i) { | ||||
| 	return pJson[i].playlist && pJson[i].playlist.ps; | ||||
| } | ||||
|  | ||||
| function papiVal(i) { | ||||
| 	if (!pJson[i]) return ""; | ||||
| 	var o = Object.assign({},pJson[i]); | ||||
| @@ -405,9 +423,9 @@ function populatePresets(fromls) | ||||
| 	var cn = ""; | ||||
| 	var arr = Object.entries(pJson); | ||||
| 	arr.sort(cmpP); | ||||
| 	var added = false; | ||||
|   pQL = []; | ||||
|   var is = []; | ||||
|   pNum = 0; | ||||
|  | ||||
| 	for (var key of (arr||[])) | ||||
| 	{ | ||||
| @@ -419,15 +437,15 @@ function populatePresets(fromls) | ||||
|  | ||||
|     cn += `<div class="seg pres" id="p${i}o">`; | ||||
|     if (cfg.comp.pid) cn += `<div class="pid">${i}</div>`; | ||||
|     cn += `<div class="segname pname" onclick="setPreset(${i})">${pName(i)}</div> | ||||
|     cn += `<div class="segname pname" onclick="setPreset(${i})">${isPlaylist(i)?"<i class='icons btn-icon'></i>":""}${pName(i)}</div> | ||||
| 			<i class="icons e-icon flr ${expanded[i+100] ? "exp":""}" id="sege${i+100}" onclick="expand(${i+100})"></i> | ||||
| 			<div class="segin" id="seg${i+100}"></div> | ||||
| 		</div><br>`; | ||||
| 		added = true; | ||||
|     pNum++; | ||||
| 	} | ||||
|  | ||||
| 	d.getElementById('pcont').innerHTML = cn; | ||||
| 	if (added) { | ||||
| 	if (pNum > 0) { | ||||
| 		if (pmtLS != pmt && pmt != 0) { | ||||
| 			localStorage.setItem("wledPmt", pmt); | ||||
| 			pJson["0"] = {}; | ||||
| @@ -438,6 +456,7 @@ function populatePresets(fromls) | ||||
|       let i = is[a]; | ||||
|       if (expanded[i+100]) expand(i+100, true); | ||||
|     } | ||||
| 		makePlSel(arr); | ||||
| 	} else { presetError(true); } | ||||
| 	updatePA(); | ||||
| 	populateQL(); | ||||
| @@ -465,7 +484,7 @@ function populateInfo(i) | ||||
|   } | ||||
|  | ||||
| 	var vcn = "Kuuhaku"; | ||||
| 	if (i.ver.startsWith("0.12.")) vcn = "Hikari"; | ||||
| 	if (i.ver.startsWith("0.13.")) vcn = "Toki"; | ||||
| 	if (i.cn) vcn = i.cn; | ||||
|  | ||||
| 	cn += `v${i.ver} "${vcn}"<br><br><table class="infot"> | ||||
| @@ -474,8 +493,8 @@ function populateInfo(i) | ||||
| 	${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} | ||||
| 	${inforow("Uptime",getRuntimeStr(i.uptime))} | ||||
| 	${inforow("Free heap",heap," kB")} | ||||
|   ${inforow("Estimated current",pwru)} | ||||
|   ${inforow("Frames / second",i.leds.fps)} | ||||
|   	${inforow("Estimated current",pwru)} | ||||
|   	${inforow("Frames / second",i.leds.fps)} | ||||
| 	${inforow("MAC address",i.mac)} | ||||
| 	${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} | ||||
| 	${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} | ||||
| @@ -509,34 +528,39 @@ function populateSegments(s) | ||||
| 			</div> | ||||
| 			<i class="icons e-icon flr ${expanded[i] ? "exp":""}" id="sege${i}" onclick="expand(${i})"></i> | ||||
| 			<div class="segin ${expanded[i] ? "expanded":""}" id="seg${i}"> | ||||
| 			<table class="segt"> | ||||
| 				<tr> | ||||
| 					<td class="segtd">Start LED</td> | ||||
| 					<td class="segtd">Stop LED</td> | ||||
| 				</tr> | ||||
| 				<tr> | ||||
| 					<td class="segtd"><input class="noslide segn" id="seg${i}s" type="number" min="0" max="${ledCount-1}" value="${inst.start}" oninput="updateLen(${i})"></td> | ||||
| 					<td class="segtd"><input class="noslide segn" id="seg${i}e" type="number" min="0" max="${ledCount}" value="${inst.stop}" oninput="updateLen(${i})"></td> | ||||
| 				</tr> | ||||
| 			</table> | ||||
| 			<table class="segt"> | ||||
| 				<tr> | ||||
| 					<td class="segtd">Grouping</td> | ||||
| 					<td class="segtd">Spacing</td> | ||||
| 				</tr> | ||||
| 				<tr> | ||||
| 					<td class="segtd"><input class="noslide segn" id="seg${i}grp" type="number" min="1" max="255" value="${inst.grp}" oninput="updateLen(${i})"></td> | ||||
| 					<td class="segtd"><input class="noslide segn" id="seg${i}spc" type="number" min="0" max="255" value="${inst.spc}" oninput="updateLen(${i})"></td> | ||||
| 				</tr> | ||||
| 			</table> | ||||
| 			<div class="h bp" id="seg${i}len"></div> | ||||
| 			<i class="icons e-icon pwr ${powered[i] ? "act":""}" id="seg${i}pwr" onclick="setSegPwr(${i})"></i> | ||||
| 			<div class="sliderwrap il sws"> | ||||
| 				<input id="seg${i}bri" class="noslide sis" onchange="setSegBri(${i})" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" /> | ||||
| 				<div class="sliderdisplay"></div> | ||||
| 			</div> | ||||
| 				<i class="icons e-icon cnf cnf-s" id="segc${i}" onclick="setSeg(${i})"></i> | ||||
| 				<i class="icons e-icon del" id="segd${i}" onclick="delSeg(${i})"></i> | ||||
| 				<div class="sbs"> | ||||
| 				<i class="icons e-icon pwr ${powered[i] ? "act":""}" id="seg${i}pwr" onclick="setSegPwr(${i})"></i> | ||||
| 				<div class="sliderwrap il sws"> | ||||
| 					<input id="seg${i}bri" class="noslide sis" onchange="setSegBri(${i})" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" /> | ||||
| 					<div class="sliderdisplay"></div> | ||||
| 				</div> | ||||
| 				</div> | ||||
| 				<table class="infot"> | ||||
| 					<tr> | ||||
| 						<td class="segtd">Start LED</td> | ||||
| 						<td class="segtd">Stop LED</td> | ||||
| 						<td class="segtd">Offset</td> | ||||
| 					</tr> | ||||
| 					<tr> | ||||
| 						<td class="segtd"><input class="noslide segn" id="seg${i}s" type="number" min="0" max="${ledCount-1}" value="${inst.start}" oninput="updateLen(${i})"></td> | ||||
| 						<td class="segtd"><input class="noslide segn" id="seg${i}e" type="number" min="0" max="${ledCount}" value="${inst.stop}" oninput="updateLen(${i})"></td> | ||||
| 						<td class="segtd"><input class="noslide segn" id="seg${i}of" type="number" min="0" max="${ledCount-1}" value="${inst.of}" oninput="updateLen(${i})"></td> | ||||
| 					</tr> | ||||
| 				</table> | ||||
| 				<table class="infot"> | ||||
| 					<tr> | ||||
| 						<td class="segtd">Grouping</td> | ||||
| 						<td class="segtd">Spacing</td> | ||||
| 						<td class="segtd">Apply</td> | ||||
| 					</tr> | ||||
| 					<tr> | ||||
| 						<td class="segtd"><input class="noslide segn" id="seg${i}grp" type="number" min="1" max="255" value="${inst.grp}" oninput="updateLen(${i})"></td> | ||||
| 						<td class="segtd"><input class="noslide segn" id="seg${i}spc" type="number" min="0" max="255" value="${inst.spc}" oninput="updateLen(${i})"></td> | ||||
| 						<td class="segtd"><i class="icons e-icon cnf cnf-s" id="segc${i}" onclick="setSeg(${i})"></i></td> | ||||
| 					</tr> | ||||
| 				</table> | ||||
| 				<div class="h" id="seg${i}len"></div> | ||||
| 				<button class="btn btn-i btn-xs del" id="segd${i}" onclick="delSeg(${i})"><i class="icons btn-icon"></i></button> | ||||
| 				<label class="check revchkl"> | ||||
| 					Reverse direction | ||||
| 					<input type="checkbox" id="seg${i}rev" onchange="setRev(${i})" ${inst.rev ? "checked":""}> | ||||
| @@ -885,8 +909,11 @@ function cmpP(a, b) { | ||||
| } | ||||
|  | ||||
| var jsonTimeout; | ||||
| var reqsLegal = false; | ||||
|  | ||||
| function requestJson(command, rinfo = true, verbose = true) { | ||||
| 	d.getElementById('connind').style.backgroundColor = "#a90"; | ||||
|   if (command && !reqsLegal) return; //stop post requests from chrome onchange event on page restore | ||||
| 	lastUpdate = new Date(); | ||||
| 	if (!jsonTimeout) jsonTimeout = setTimeout(showErrorToast, 3000); | ||||
| 	var req = null; | ||||
| @@ -946,6 +973,7 @@ function requestJson(command, rinfo = true, verbose = true) { | ||||
|  | ||||
| 				populateEffects(json.effects); | ||||
| 				populatePalettes(json.palettes); | ||||
|         reqsLegal = true; | ||||
| 			} | ||||
|  | ||||
| 			var info = json.info; | ||||
| @@ -990,11 +1018,8 @@ function requestJson(command, rinfo = true, verbose = true) { | ||||
| 		nlFade = s.nl.fade; | ||||
| 		syncSend = s.udpn.send; | ||||
| 		currentPreset = s.ps; | ||||
| 		d.getElementById('cyToggle').checked = (s.pl >= 0); | ||||
| 		d.getElementById('cycs').value = s.ccnf.min; | ||||
| 		d.getElementById('cyce').value = s.ccnf.max; | ||||
| 		d.getElementById('cyct').value = s.ccnf.time /10; | ||||
| 		d.getElementById('cyctt').value = s.transition /10; | ||||
| 		tr = s.transition; | ||||
| 		d.getElementById('tt').value = tr/10; | ||||
|  | ||||
| 		var selc=0; var ind=0; | ||||
| 		populateSegments(s); | ||||
| @@ -1023,7 +1048,10 @@ function requestJson(command, rinfo = true, verbose = true) { | ||||
| 		d.getElementById('sliderIntensity').value = i.ix; | ||||
|  | ||||
| 		// Effects | ||||
| 		e1.querySelector(`input[name="fx"][value="${i.fx}"]`).checked = true; | ||||
|     var selFx = e1.querySelector(`input[name="fx"][value="${i.fx}"]`); | ||||
|     if (selFx) selFx.checked = true; | ||||
|     else location.reload(); //effect list is gone (e.g. if restoring tab). Reload. | ||||
|  | ||||
| 		var selElement = e1.querySelector('.selected'); | ||||
| 		if (selElement) { | ||||
| 			selElement.classList.remove('selected') | ||||
| @@ -1057,7 +1085,7 @@ function requestJson(command, rinfo = true, verbose = true) { | ||||
| 					errstr = "Not enough space to save preset!"; | ||||
| 					break; | ||||
| 				case 12: | ||||
| 					errstr = "The requested preset does not exist."; | ||||
| 					errstr = "Preset not found."; | ||||
| 					break; | ||||
| 				case 19: | ||||
| 					errstr = "A filesystem error has occured."; | ||||
| @@ -1076,7 +1104,7 @@ function requestJson(command, rinfo = true, verbose = true) { | ||||
| function togglePower() { | ||||
| 	isOn = !isOn; | ||||
| 	var obj = {"on": isOn}; | ||||
| 	obj.transition = parseInt(d.getElementById('cyctt').value*10); | ||||
| 	obj.transition = parseInt(d.getElementById('tt').value*10); | ||||
| 	requestJson(obj); | ||||
| } | ||||
|  | ||||
| @@ -1152,7 +1180,7 @@ function makeSeg() { | ||||
| 						<td class="segtd"><input class="noslide segn" id="seg${lowestUnused}e" type="number" min="0" max="${ledCount}" value="${ledCount}" oninput="updateLen(${lowestUnused})"></td> | ||||
| 					</tr> | ||||
| 				</table> | ||||
| 				<div class="h" id="seg${lowestUnused}len">${ledCount - ns} LEDs</div> | ||||
| 				<div class="h" id="seg${lowestUnused}len">${ledCount - ns} LED${ledCount - ns >1 ? "s":""}</div> | ||||
| 				<i class="icons e-icon cnf cnf-s half" id="segc${lowestUnused}" onclick="setSeg(${lowestUnused}); resetUtil();"></i> | ||||
| 			</div> | ||||
| 		</div>`; | ||||
| @@ -1164,37 +1192,148 @@ function resetUtil() { | ||||
| 	d.getElementById('segutil').innerHTML = cn; | ||||
| } | ||||
|  | ||||
| function makeP(i) { | ||||
| var plJson = {"0":{ | ||||
| 	"ps": [0],	 | ||||
| 	"dur": [100],	 | ||||
| 	"transition": [-1],	//to be inited to default transition dur | ||||
| 	"repeat": 0, | ||||
| 	"r": false, | ||||
| 	"end": 0	 | ||||
| }}; | ||||
|  | ||||
| var plSelContent = ""; | ||||
| function makePlSel(arr) { | ||||
| 	plSelContent = ""; | ||||
| 	for (var i = 0; i < arr.length; i++) { | ||||
| 		var n = arr[i][1].n ? arr[i][1].n : "Preset " + arr[i][0]; | ||||
| 		if (arr[i][1].playlist && arr[i][1].playlist.ps) continue; //remove playlists, sub-playlists not yet supported | ||||
| 		plSelContent += `<option value=${arr[i][0]}>${n}</option>` | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function refreshPlE(p) { | ||||
| 	var plEDiv = d.getElementById(`ple${p}`); | ||||
| 	if (!plEDiv) return; | ||||
| 	var content = ""; | ||||
| 	for (var i = 0; i < plJson[p].ps.length; i++) { | ||||
| 		content += makePlEntry(p,i); | ||||
| 	} | ||||
| 	plEDiv.innerHTML = content; | ||||
| 	var dels = plEDiv.getElementsByClassName("btn-pl-del"); | ||||
| 	if (dels.length < 2 && p > 0) dels[0].style.display = "none"; | ||||
|  | ||||
| 	var sels = d.getElementById(`seg${p+100}`).getElementsByClassName("sel"); | ||||
| 	for (var i of sels) { | ||||
| 		if (i.dataset.val) { | ||||
| 			if (parseInt(i.dataset.val) > 0) i.value = i.dataset.val; | ||||
| 			else plJson[p].ps[i.dataset.index] = parseInt(i.value); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| //p: preset ID, i: ps index | ||||
| function addPl(p,i) { | ||||
| 	plJson[p].ps.splice(i+1,0,0); | ||||
| 	plJson[p].dur.splice(i+1,0,plJson[p].dur[i]); | ||||
| 	plJson[p].transition.splice(i+1,0,plJson[p].transition[i]); | ||||
| 	refreshPlE(p); | ||||
| } | ||||
|  | ||||
| function delPl(p,i) { | ||||
| 	if (plJson[p].ps.length < 2) {if (p == 0) resetPUtil(); return;} | ||||
| 	plJson[p].ps.splice(i,1); | ||||
| 	plJson[p].dur.splice(i,1); | ||||
| 	plJson[p].transition.splice(i,1); | ||||
| 	refreshPlE(p); | ||||
| } | ||||
|  | ||||
| function plePs(p,i,field) { | ||||
| 	plJson[p].ps[i] = parseInt(field.value); | ||||
| } | ||||
|  | ||||
| function pleDur(p,i,field) { | ||||
| 	if (field.validity.valid) | ||||
| 		plJson[p].dur[i] = Math.floor(field.value*10); | ||||
| } | ||||
|  | ||||
| function pleTr(p,i,field) { | ||||
| 	if (field.validity.valid) | ||||
| 		plJson[p].transition[i] = Math.floor(field.value*10); | ||||
| } | ||||
|  | ||||
| function plR(p) { | ||||
| 	var pl = plJson[p]; | ||||
| 	pl.r = d.getElementById(`pl${p}rtgl`).checked; | ||||
| 	if (d.getElementById(`pl${p}rptgl`).checked) { //infinite | ||||
| 		pl.repeat = 0; | ||||
| 		delete pl.end; | ||||
| 		d.getElementById(`pl${p}o1`).style.display = "none"; | ||||
| 	} else { | ||||
| 		pl.repeat = parseInt(d.getElementById(`pl${p}rp`).value); | ||||
| 		pl.end = parseInt(d.getElementById(`pl${p}selEnd`).value); | ||||
| 		d.getElementById(`pl${p}o1`).style.display = "block"; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function makeP(i,pl) { | ||||
|   var content = ""; | ||||
|   if (pl) { | ||||
| 		var rep = plJson[i].repeat ? plJson[i].repeat : 0; | ||||
| 		content = ` | ||||
|   <div id="ple${i}"></div><label class="check revchkl"> | ||||
|     Shuffle | ||||
|     <input type="checkbox" id="pl${i}rtgl" onchange="plR(${i})" ${plJson[i].r?"checked":""}> | ||||
|     <span class="checkmark schk"></span> | ||||
|   </label> | ||||
|   <label class="check revchkl"> | ||||
|     Repeat indefinitely | ||||
|     <input type="checkbox" id="pl${i}rptgl" onchange="plR(${i})" ${rep?"":"checked"}> | ||||
|     <span class="checkmark schk"></span> | ||||
|   </label> | ||||
| 	<div id="pl${i}o1" style="display:${rep?"block":"none"}"> | ||||
|   <div class="c">Repeat <input class="noslide" type="number" id="pl${i}rp" oninput="plR(${i})" max=127 min=0 value=${rep>0?rep:1}> times</div> | ||||
|   End preset:<br> | ||||
|   <select class="btn sel sel-ple" id="pl${i}selEnd" onchange="plR(${i})" data-val=${plJson[i].end?plJson[i].end:0}> | ||||
| 		<option value=0>None</option> | ||||
|     ${plSelContent} | ||||
|   </select> | ||||
| 	</div> | ||||
|   <button class="btn btn-i btn-p" onclick="testPl(${i}, this)"><i class='icons btn-icon'></i>Test</button>`; | ||||
| 	} | ||||
|   else content = `<label class="check revchkl"> | ||||
| 		Include brightness | ||||
| 		<input type="checkbox" id="p${i}ibtgl" checked> | ||||
| 		<span class="checkmark schk"></span> | ||||
| 	</label> | ||||
| 	<label class="check revchkl"> | ||||
| 		Save segment bounds | ||||
| 		<input type="checkbox" id="p${i}sbtgl" checked> | ||||
| 		<span class="checkmark schk"></span> | ||||
| 	</label>`; | ||||
|  | ||||
| 	return ` | ||||
| 	<input type="text" class="ptxt noslide" id="p${i}txt" autocomplete="off" maxlength=32 value="${(i>0)?pName(i):""}" placeholder="Enter name..."/><br> | ||||
| 	<div class="c">Quick load label: <input type="text" class="stxt noslide" maxlength=2 value="${qlName(i)}" id="p${i}ql" autocomplete="off"/></div> | ||||
| 	<div class="h">(leave empty for no Quick load button)</div> | ||||
| 	<div ${pl&&i==0?"style='display:none'":""}> | ||||
| 	<label class="check revchkl"> | ||||
| 		${(i>0)?"Overwrite with state":"Use current state"} | ||||
| 		<input type="checkbox" id="p${i}cstgl" onchange="tglCs(${i})" ${(i>0)?"":"checked"}> | ||||
| 		<span class="checkmark schk"></span> | ||||
| 	</label><br> | ||||
| 	<div class="po2" id="p${i}o2"> | ||||
| 		API command<br> | ||||
| 		<textarea class="noslide" id="p${i}api"></textarea> | ||||
| 	</div> | ||||
| 	<div class="po1" id="p${i}o1"> | ||||
| 		<label class="check revchkl"> | ||||
| 			Include brightness | ||||
| 			<input type="checkbox" id="p${i}ibtgl" checked> | ||||
| 			<span class="checkmark schk"></span> | ||||
| 		</label> | ||||
| 		<label class="check revchkl"> | ||||
| 			Save segment bounds | ||||
| 			<input type="checkbox" id="p${i}sbtgl" checked> | ||||
| 			<span class="checkmark schk"></span> | ||||
| 		</label> | ||||
|     ${pl?"Show playlist editor":(i>0)?"Overwrite with state":"Use current state"} | ||||
|     <input type="checkbox" id="p${i}cstgl" onchange="tglCs(${i})" ${(i==0||pl)?"checked":""}> | ||||
|     <span class="checkmark schk"></span> | ||||
|   </label><br> | ||||
| 	</div> | ||||
|   <div class="po2" id="p${i}o2"> | ||||
|     API command<br> | ||||
|     <textarea class="noslide" id="p${i}api"></textarea> | ||||
|   </div> | ||||
|   <div class="po1" id="p${i}o1"> | ||||
| 		${content} | ||||
|   </div> | ||||
| 	<div class="c">Save to ID <input class="noslide" id="p${i}id" type="number" oninput="checkUsed(${i})" max=250 min=1 value=${(i>0)?i:getLowestUnusedP()}></div> | ||||
| 	<div class="c"> | ||||
| 		<button class="btn btn-i btn-p" onclick="saveP(${i})"><i class="icons btn-icon"></i>${(i>0)?"Save changes":"Save preset"}</button> | ||||
| 		${(i>0)?'<button class="btn btn-i btn-p" onclick="delP('+i+')"><i class="icons btn-icon"></i>Delete preset</button>': | ||||
| 						'<button class="btn btn-p" onclick="resetPUtil()">Cancel</button>'} | ||||
| 		<button class="btn btn-i btn-p" onclick="saveP(${i},${pl})"><i class="icons btn-icon"></i>Save ${(pl)?"playlist":(i>0)?"changes":"preset"}</button> | ||||
| 		${(i>0)?'<button class="btn btn-i btn-p" id="p'+i+'del" onclick="delP('+i+')"><i class="icons btn-icon"></i>Delete '+(pl?"playlist":"preset"): | ||||
| 						'<button class="btn btn-p" onclick="resetPUtil()">Cancel'}</button> | ||||
| 	</div> | ||||
| 	<div class="pwarn ${(i>0)?"bp":""} c" id="p${i}warn"> | ||||
|  | ||||
| @@ -1208,11 +1347,48 @@ function makePUtil() { | ||||
| 			New preset</div> | ||||
| 		<div class="segin expanded"> | ||||
| 		${makeP(0)}</div></div>`; | ||||
| 	updateTrail(d.getElementById('p0p')); | ||||
| } | ||||
|  | ||||
| function makePlEntry(p,i) { | ||||
|   return ` | ||||
|   <div class="plentry"> | ||||
|     ${i+1}: | ||||
|     <select class="btn sel sel-pl" onchange="plePs(${p},${i},this)" data-val=${plJson[p].ps[i]} data-index=${i}> | ||||
| 			${plSelContent} | ||||
|     </select> | ||||
| 		<table class="segt"> | ||||
| 			<tr> | ||||
| 				<td class="segtd">Duration</td> | ||||
| 				<td class="segtd">Transition</td> | ||||
| 			</tr> | ||||
| 			<tr> | ||||
| 				<td class="segtd"><input class="noslide segn" type="number" max=6553.0 min=0.2 step=0.1 oninput="pleDur(${p},${i},this)" value=${plJson[p].dur[i]/10.0}></td> | ||||
| 				<td class="segtd"><input class="noslide segn" type="number" max=65.0 min=0.0 step=0.1 oninput="pleTr(${p},${i},this)" value=${plJson[p].transition[i]/10.0}> s</td> | ||||
| 			</tr> | ||||
| 		</table> | ||||
|     <button class="btn btn-i btn-xs btn-pl-del" onclick="delPl(${p},${i})"><i class="icons btn-icon"></i></button></div> | ||||
|     <div class="hrz hrz-pl" /> | ||||
|     <button class="btn btn-i btn-xs btn-pl-add" onclick="addPl(${p},${i})"><i class="icons btn-icon"></i></button></div> | ||||
|   </div>`; | ||||
| } | ||||
|  | ||||
| function makePlUtil() { | ||||
|   if (pNum < 2) { | ||||
|     showToast("You need at least 2 presets to make a playlist!"); return; | ||||
|   } | ||||
| 	if (plJson[0].transition[0] < 0) plJson[0].transition[0] = tr; | ||||
|   d.getElementById('putil').innerHTML = `<div class="seg pres"> | ||||
|   <div class="segname newseg"> | ||||
|     New playlist</div> | ||||
|   <div class="segin expanded" id="seg100"> | ||||
|   ${makeP(0,true)}</div></div>`; | ||||
| 	 | ||||
| 	refreshPlE(0); | ||||
| } | ||||
|  | ||||
| function resetPUtil() { | ||||
| 	var cn = `<button class="btn btn-s btn-i" onclick="makePUtil()"><i class="icons btn-icon"></i>Create preset</button><br>`; | ||||
| 	var cn = `<button class="btn btn-s btn-i" onclick="makePUtil()"><i class="icons btn-icon"></i>Create preset</button><br> | ||||
|             <button class="btn btn-s btn-i" onclick="makePlUtil()"><i class='icons btn-icon'></i>Create playlist</button><br>`; | ||||
| 	d.getElementById('putil').innerHTML = cn; | ||||
| } | ||||
|  | ||||
| @@ -1246,8 +1422,10 @@ function setSeg(s){ | ||||
| 	{ | ||||
| 		var grp = parseInt(d.getElementById(`seg${s}grp`).value); | ||||
| 		var spc = parseInt(d.getElementById(`seg${s}spc`).value); | ||||
| 		var ofs = parseInt(d.getElementById(`seg${s}of` ).value); | ||||
| 		obj.seg.grp = grp; | ||||
| 		obj.seg.spc = spc; | ||||
| 		obj.seg.of  = ofs; | ||||
| 	} | ||||
| 	requestJson(obj); | ||||
| } | ||||
| @@ -1319,7 +1497,7 @@ function setPalette(paletteId = null) | ||||
|  | ||||
| function setBri() { | ||||
| 	var obj = {"bri": parseInt(d.getElementById('sliderBri').value)}; | ||||
| 	obj.transition = parseInt(d.getElementById('cyctt').value*10); | ||||
| 	obj.transition = parseInt(d.getElementById('tt').value*10); | ||||
| 	requestJson(obj); | ||||
| } | ||||
|  | ||||
| @@ -1338,17 +1516,6 @@ function setLor(i) { | ||||
| 	requestJson(obj); | ||||
| } | ||||
|  | ||||
| function toggleCY() { | ||||
| 	var obj = {"pl" : -1}; | ||||
| 	if (d.getElementById('cyToggle').checked) | ||||
| 	{ | ||||
| 		obj = {"pl": 0, "ccnf": {"min": parseInt(d.getElementById('cycs').value), "max": parseInt(d.getElementById('cyce').value), "time": parseInt(d.getElementById('cyct').value*10)}}; | ||||
| 		obj.transition = parseInt(d.getElementById('cyctt').value*10); | ||||
| 	} | ||||
|  | ||||
| 	requestJson(obj); | ||||
| } | ||||
|  | ||||
| function setPreset(i) { | ||||
| 	var obj = {"ps": i}; | ||||
|  | ||||
| @@ -1357,12 +1524,14 @@ function setPreset(i) { | ||||
| 	requestJson(obj); | ||||
| } | ||||
|  | ||||
| function saveP(i) { | ||||
| function saveP(i,pl) { | ||||
| 	pI = parseInt(d.getElementById(`p${i}id`).value); | ||||
| 	if (!pI || pI < 1) pI = (i>0) ? i : getLowestUnusedP(); | ||||
| 	pN = d.getElementById(`p${i}txt`).value; | ||||
| 	if (pN == "") pN = "Preset " + pI; | ||||
|  | ||||
| 	if (pN == "") pN = (pl?"Playlist ":"Preset ") + pI; | ||||
| 	var obj = {}; | ||||
|  | ||||
| 	if (!d.getElementById(`p${i}cstgl`).checked) { | ||||
| 		var raw = d.getElementById(`p${i}api`).value; | ||||
| 		try { | ||||
| @@ -1376,15 +1545,21 @@ function saveP(i) { | ||||
| 				d.getElementById(`p${i}warn`).innerHTML = "⚠ Syntax error in custom JSON API command"; | ||||
| 				return; | ||||
| 			} else if (raw.indexOf("Please") == 0) { | ||||
|         d.getElementById(`p${i}warn`).innerHTML = "⚠ Please refresh the page before modifying this preset"; | ||||
| 				d.getElementById(`p${i}warn`).innerHTML = "⚠ Please refresh the page before modifying this preset"; | ||||
| 				return; | ||||
|       } | ||||
| 			} | ||||
| 		} | ||||
| 		obj.o = true; | ||||
| 	} else { | ||||
| 		obj.ib = d.getElementById(`p${i}ibtgl`).checked; | ||||
| 		obj.sb = d.getElementById(`p${i}sbtgl`).checked; | ||||
| 		if (pl) { | ||||
| 			obj.playlist = plJson[i]; | ||||
| 			obj.o = true; | ||||
| 		} else { | ||||
| 			obj.ib = d.getElementById(`p${i}ibtgl`).checked; | ||||
| 			obj.sb = d.getElementById(`p${i}sbtgl`).checked; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	obj.psave = pI; obj.n = pN; | ||||
| 	var pQN = d.getElementById(`p${i}ql`).value; | ||||
| 	if (pQN.length > 0) obj.ql = pQN; | ||||
| @@ -1406,11 +1581,36 @@ function saveP(i) { | ||||
| 	resetPUtil(); | ||||
| } | ||||
|  | ||||
| function delP(i) { | ||||
| 	var obj = {"pdel": i}; | ||||
| function testPl(i,bt) { | ||||
| 	if (bt.dataset.test == 1) { | ||||
| 		bt.dataset.test = 0; | ||||
| 		bt.innerHTML = "<i class='icons btn-icon'></i>Test"; | ||||
| 		stopPl(); | ||||
| 		return; | ||||
| 	} | ||||
| 	bt.dataset.test = 1; | ||||
| 	bt.innerHTML = "<i class='icons btn-icon'></i>Stop"; | ||||
| 	var obj = {}; | ||||
| 	obj.playlist = plJson[i]; | ||||
| 	requestJson(obj); | ||||
| 	delete pJson[i]; | ||||
| 	populatePresets(); | ||||
| } | ||||
|  | ||||
| function stopPl() { | ||||
| 	requestJson({playlist:{}}) | ||||
| } | ||||
|  | ||||
| function delP(i) { | ||||
| 	var bt = d.getElementById(`p${i}del`); | ||||
| 	if (bt.dataset.cnf == 1) { | ||||
| 		var obj = {"pdel": i}; | ||||
| 		requestJson(obj); | ||||
| 		delete pJson[i]; | ||||
| 		populatePresets(); | ||||
| 	} else { | ||||
| 		bt.style.color = "#f00"; | ||||
| 		bt.innerHTML = "<i class='icons btn-icon'></i>Confirm delete"; | ||||
| 		bt.dataset.cnf = 1; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function selectSlot(b) { | ||||
| @@ -1510,7 +1710,7 @@ function setColor(sr) { | ||||
| 	} | ||||
| 	updateHex(); | ||||
| 	updateRgb(); | ||||
| 	obj.transition = parseInt(d.getElementById('cyctt').value*10); | ||||
| 	obj.transition = parseInt(d.getElementById('tt').value*10); | ||||
| 	requestJson(obj); | ||||
| } | ||||
|  | ||||
| @@ -1638,22 +1838,61 @@ function cancelSearch(ic) { | ||||
|   searchField.focus(); | ||||
| } | ||||
|  | ||||
| //make sure "dur" and "transition" are arrays with at least the length of "ps" | ||||
| function formatArr(pl) { | ||||
| 	var l = pl.ps.length; | ||||
| 	if (!Array.isArray(pl.dur)) { | ||||
| 		var v = pl.dur; | ||||
| 		if (isNaN(v)) v = 100; | ||||
| 		pl.dur = [v]; | ||||
| 	} | ||||
| 	var l2 = pl.dur.length; | ||||
| 	if (l2 < l) | ||||
| 	{ | ||||
| 		for (var i = 0; i < l - l2; i++) | ||||
| 			pl.dur.push(pl.dur[l2-1]); | ||||
| 	} | ||||
|  | ||||
| 	if (!Array.isArray(pl.transition)) { | ||||
| 		var v = pl.transition; | ||||
| 		if (isNaN(v)) v = tr; | ||||
| 		pl.transition = [v]; | ||||
| 	} | ||||
| 	var l2 = pl.transition.length; | ||||
| 	if (l2 < l) | ||||
| 	{ | ||||
| 		for (var i = 0; i < l - l2; i++) | ||||
| 			pl.transition.push(pl.transition[l2-1]); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function expand(i,a) | ||||
| { | ||||
| 	if (!a) expanded[i] = !expanded[i]; | ||||
| 	d.getElementById('seg' +i).style.display = (expanded[i]) ? "block":"none"; | ||||
| 	d.getElementById('sege' +i).style.transform = (expanded[i]) ? "rotate(180deg)":"rotate(0deg)"; | ||||
| 	if (i > 100) { //presets | ||||
| 		var p = i-100; | ||||
| 		d.getElementById(`p${p}o`).style.background = (expanded[i] || p != currentPreset)?"var(--c-2)":"var(--c-6)"; | ||||
| 		if (d.getElementById('seg' +i).innerHTML == "") { | ||||
|       d.getElementById('seg' +i).innerHTML = makeP(p); | ||||
|       var papi = papiVal(p); | ||||
|       d.getElementById(`p${p}api`).value = papi; | ||||
|       if (papi.indexOf("Please") == 0) d.getElementById(`p${p}cstgl`).checked = true; | ||||
|       tglCs(p); | ||||
| 		} | ||||
| 	if (i < 100) return; //no preset, we are done | ||||
|  | ||||
| 	var p = i-100; | ||||
| 	d.getElementById(`p${p}o`).style.background = (expanded[i] || p != currentPreset)?"var(--c-2)":"var(--c-6)"; | ||||
| 	if (d.getElementById('seg' +i).innerHTML != "") return; | ||||
| 	if (isPlaylist(p)) { | ||||
| 		plJson[p] = pJson[p].playlist; | ||||
| 		//make sure all keys are present in plJson[p] | ||||
| 		formatArr(plJson[p]); | ||||
| 		if (isNaN(plJson[p].repeat)) plJson[p].repeat = 0; | ||||
| 		if (!plJson[p].r) plJson[p].r = false; | ||||
| 		if (isNaN(plJson[p].end)) plJson[p].end = 0; | ||||
|  | ||||
| 		d.getElementById('seg' +i).innerHTML = makeP(p,true); | ||||
| 		refreshPlE(p); | ||||
| 	} else { | ||||
| 		d.getElementById('seg' +i).innerHTML = makeP(p); | ||||
| 	} | ||||
| 	var papi = papiVal(p); | ||||
| 	d.getElementById(`p${p}api`).value = papi; | ||||
| 	if (papi.indexOf("Please") == 0) d.getElementById(`p${p}cstgl`).checked = true; | ||||
| 	tglCs(p); | ||||
| } | ||||
|  | ||||
| function unfocusSliders() { | ||||
|   | ||||
| @@ -10,18 +10,17 @@ | ||||
| 			margin: 0; | ||||
| 		} | ||||
| 		html { | ||||
| 			--h: 11.55vh; | ||||
| 			--h: 10.2vh; | ||||
| 		} | ||||
| 		button { | ||||
| 			background: #333; | ||||
| 			color: #fff; | ||||
| 			font-family: Verdana, Helvetica, sans-serif; | ||||
| 			border: 0.3ch solid #333; | ||||
| 			display: inline-block; | ||||
| 			font-size: 8vmin; | ||||
| 			border: 1px solid #333; | ||||
| 			font-size: 6vmin; | ||||
| 			height: var(--h); | ||||
| 			width: 95%; | ||||
| 			margin-top: 2.4vh; | ||||
| 			margin-top: 2vh; | ||||
| 		} | ||||
| 	</style> | ||||
| 	<script> | ||||
| @@ -41,6 +40,7 @@ | ||||
| <form action="/settings/ui"><button type="submit">User Interface</button></form> | ||||
| <form action="/settings/sync"><button type="submit">Sync Interfaces</button></form> | ||||
| <form action="/settings/time"><button type="submit">Time & Macros</button></form> | ||||
| <form action="/settings/um"><button type="submit">Usermods</button></form> | ||||
| <form action="/settings/sec"><button type="submit">Security & Updates</button></form> | ||||
| </body> | ||||
| </html> | ||||
| @@ -1,350 +1,408 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="utf-8"> | ||||
| 	<meta name="viewport" content="width=500"> | ||||
| 	<title>LED Settings</title> | ||||
| 	<script> | ||||
|     var d=document,laprev=55,maxB=1,maxM=5000,maxPB=4096,bquot=0; //maximum bytes for LED allocation: 5kB for 8266, 32kB for 32 | ||||
| 		function H() | ||||
| 		{ | ||||
| 			window.open("https://github.com/Aircoookie/WLED/wiki/Settings#led-settings"); | ||||
| 		} | ||||
| 		function B() | ||||
| 		{ | ||||
| 			window.open("/settings","_self"); | ||||
|     } | ||||
|     function bLimits(b,p,m) { | ||||
|       maxB = b; maxM = m; maxPB = p; | ||||
|     } | ||||
|     function trySubmit() { | ||||
|       var LCs = d.getElementsByTagName("input"); | ||||
|       for (i=0; i<LCs.length; i++) { | ||||
|         var nm = LCs[i].name.substring(0,2); | ||||
|  | ||||
|         //check for pin conflicts | ||||
|         if (nm=="L0" || nm=="L1" || nm=="RL" || nm=="BT" || nm=="IR" || nm=="AX") | ||||
|           if (LCs[i].value!="" && LCs[i].value!="-1") { | ||||
|             if (LCs[i].value > 5 && LCs[i].value < 12) {alert("Sorry, pins 6-11 can not be used.");LCs[i].focus();return;} | ||||
|             if (d.um_p && d.um_p.some((e)=>e==parseInt(LCs[i].value,10))) {alert("Usermod pin conflict!");LCs[i].focus();return;} | ||||
|             for (j=i+1; j<LCs.length; j++) | ||||
|             { | ||||
|               var n2 = LCs[j].name.substring(0,2); | ||||
|               if (n2=="L0" || n2=="L1" || n2=="RL" || n2=="BT" || n2=="IR" || n2=="AX") | ||||
|                 if (LCs[j].value!="" && LCs[i].value==LCs[j].value) {alert("Pin conflict!");LCs[i].focus();return;} | ||||
|             } | ||||
|           } | ||||
|       } | ||||
|       if (bquot > 100) {var msg = "Too many LEDs for me to handle!"; if (maxM < 10000) msg += " Consider using an ESP32."; alert(msg); return;} | ||||
|       if (d.Sf.reportValidity()) d.Sf.submit(); | ||||
|     } | ||||
| 		function S(){GetV();setABL();} | ||||
|     function enABL() | ||||
|     { | ||||
|       var en = d.getElementById('able').checked; | ||||
|       d.Sf.LA.value = (en) ? laprev:0; | ||||
|       d.getElementById('abl').style.display = (en) ? 'inline':'none'; | ||||
|       d.getElementById('psu2').style.display = (en) ? 'inline':'none'; | ||||
|       if (d.Sf.LA.value > 0) setABL(); | ||||
|     } | ||||
|     function enLA() | ||||
|     { | ||||
|       var val = d.Sf.LAsel.value; | ||||
|       d.Sf.LA.value = val; | ||||
|       d.getElementById('LAdis').style.display = (val == 50) ? 'inline':'none'; | ||||
|       UI(); | ||||
|     } | ||||
|     function setABL() | ||||
|     { | ||||
|       d.getElementById('able').checked = true; | ||||
|       d.Sf.LAsel.value = 50; | ||||
|       switch (parseInt(d.Sf.LA.value)) { | ||||
|         case 0: d.getElementById('able').checked = false; enABL(); break; | ||||
|         case 30: d.Sf.LAsel.value = 30; break; | ||||
|         case 35: d.Sf.LAsel.value = 35; break; | ||||
|         case 55: d.Sf.LAsel.value = 55; break; | ||||
|         case 255: d.Sf.LAsel.value = 255; break; | ||||
|         default: d.getElementById('LAdis').style.display = 'inline'; | ||||
|       } | ||||
|       d.getElementById('m1').innerHTML = maxM; | ||||
|       UI(); | ||||
|     } | ||||
|     //returns mem usage | ||||
|     function getMem(type, len, p0) { | ||||
|       //len = parseInt(len); | ||||
|       if (type < 32) { | ||||
|         if (maxM < 10000 && p0 ==3) { //8266 DMA uses 5x the mem | ||||
|           if (type > 29) return len*20; //RGBW | ||||
|           return len*15; | ||||
|         } else if (maxM >= 10000) //ESP32 RMT uses double buffer? | ||||
|         { | ||||
|           if (type > 29) return len*8; //RGBW | ||||
|           return len*6; | ||||
|         } | ||||
|         if (type > 29) return len*4; //RGBW | ||||
|         return len*3; | ||||
|       } | ||||
|       if (type > 31 && type < 48) return 5; | ||||
|       if (type == 44 || type == 45) return len*4; //RGBW | ||||
|       return len*3; | ||||
|     } | ||||
| 		function UI() | ||||
| 		{ | ||||
|       var isRGBW = false, memu = 0; | ||||
|        | ||||
|       d.getElementById('ampwarning').style.display = (d.Sf.MA.value > 7200) ? 'inline':'none'; | ||||
| 	   | ||||
| 	    if (d.Sf.LA.value == 255) laprev = 12; | ||||
| 	    else if (d.Sf.LA.value > 0) laprev = d.Sf.LA.value; | ||||
|        | ||||
|       var s = d.getElementsByTagName("select"); | ||||
|       for (i=0; i<s.length; i++) { | ||||
|         if (s[i].name.substring(0,2)=="LT") { | ||||
|           n=s[i].name.substring(2); | ||||
|           var type = s[i].value; | ||||
|           d.getElementById("p0d"+n).innerHTML = (type > 49) ? "Data pin:" : (type >41) ? "Pins:" : "Pin:"; | ||||
|           d.getElementById("p1d"+n).innerHTML = (type > 49) ? "Clk:" : ""; | ||||
|           var LK = d.getElementsByName("L1"+n)[0]; | ||||
|  | ||||
|           memu += getMem(type, d.getElementsByName("LC"+n)[0].value, d.getElementsByName("L0"+n)[0].value); | ||||
|  | ||||
|           for (p=1; p<5; p++) { | ||||
|             var LK = d.getElementsByName("L"+p+n)[0]; | ||||
|             if (!LK) continue; | ||||
|             if ((type>49 && p==1) || (type>41 && type < 50 && (p+40 < type))) // TYPE_xxxx values from const.h | ||||
|             { | ||||
|               LK.style.display = "inline"; | ||||
|               LK.required = true; | ||||
|             } else { | ||||
|               LK.style.display = "none"; | ||||
|               LK.required = false; | ||||
|               LK.value=""; | ||||
|             } | ||||
|           } | ||||
|           if (type == 30 || type == 31 || (type > 40 && type < 46 && type != 43)) isRGBW = true; | ||||
|           d.getElementById("dig"+n).style.display = (type > 31 && type < 48) ? "none":"inline"; | ||||
|           d.getElementById("psd"+n).innerHTML = (type > 31 && type < 48) ? "Index:":"Start:"; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       var myC = d.querySelectorAll('.wc'), | ||||
| 			l = myC.length; | ||||
| 			for (i = 0; i < l; i++) { | ||||
| 				myC[i].style.display = (isRGBW) ? 'inline':'none'; | ||||
| 			} | ||||
|  | ||||
|       if (d.activeElement == d.getElementsByName("LC")[0]) { | ||||
|         var o = d.getElementsByClassName("iST"); | ||||
|         var i = o.length; | ||||
|         if (i == 1) d.getElementsByName("LC0")[0].value = d.getElementsByName("LC")[0].value; | ||||
|       } | ||||
|  | ||||
|       var LCs = d.getElementsByTagName("input"); | ||||
|       var sLC = 0, maxLC = 0; | ||||
|       for (i=0; i<LCs.length; i++) { | ||||
|         var nm = LCs[i].name.substring(0,2); | ||||
|         if (nm=="LC" && LCs[i].name != "LC") {var c = parseInt(LCs[i].value,10); if (c) {sLC+=c; if (c>maxLC) maxLC = c;} continue;} | ||||
|       } | ||||
|  | ||||
|       d.getElementById('m0').innerHTML = memu; | ||||
|       bquot = memu / maxM * 100; | ||||
|       d.getElementById('dbar').style.background = `linear-gradient(90deg, ${bquot > 60 ? bquot > 90 ? "red":"orange":"#ccc"} 0 ${bquot}%%, #444 ${bquot}%% 100%%)`; | ||||
|       d.getElementById('ledwarning').style.display = (maxLC > 800 || bquot > 80) ? 'inline':'none'; | ||||
|       //TODO add warning "Recommended pins on ESP8266 are 1 and 2 (3 only with low LED count)" | ||||
|       //TODO add overmemory warning | ||||
|       //TODO block disallowed pins 6-11 | ||||
|       d.getElementById('wreason').innerHTML = (bquot > 80) ? "than 60%% of max. LED memory" : "800 LEDs per pin"; | ||||
|  | ||||
|       //var val = Math.ceil((100 + d.Sf.LC.value * laprev)/500)/2; | ||||
|       var val = Math.ceil((100 + sLC * laprev)/500)/2; | ||||
| 			val = (val > 5) ? Math.ceil(val) : val; | ||||
| 			var s = ""; | ||||
|       var is12V = (d.Sf.LAsel.value == 30); | ||||
|       var isWS2815 = (d.Sf.LAsel.value == 255); | ||||
| 			if (val < 1.02 && !is12V && !isWS2815) | ||||
| 			{ | ||||
| 				s = "ESP 5V pin with 1A USB supply"; | ||||
| 			} else | ||||
| 			{ | ||||
|         		s += is12V ? "12V ": isWS2815 ? "WS2815 12V " : "5V "; | ||||
| 				s += val; | ||||
| 				s += "A supply connected to LEDs"; | ||||
| 			} | ||||
|       var val2 = Math.ceil((100 + sLC * laprev)/1500)/2; | ||||
|       val2 = (val2 > 5) ? Math.ceil(val2) : val2; | ||||
|       var s2 = "(for most effects, ~"; | ||||
|       s2 += val2; | ||||
|       s2 += "A is enough)<br>"; | ||||
| 			d.getElementById('psu').innerHTML = s; | ||||
|       d.getElementById('psu2').innerHTML = isWS2815 ? "" : s2; | ||||
|     } | ||||
|     function lastEnd(i) { | ||||
|       if (i<1) return 0; | ||||
|       v = parseInt(d.getElementsByName("LS"+(i-1))[0].value) + parseInt(d.getElementsByName("LC"+(i-1))[0].value); | ||||
|       if (isNaN(v)) return 0; | ||||
|       return v; | ||||
|     } | ||||
|     function addLEDs(n) | ||||
|     { | ||||
|       if (n>1) {maxB=n; d.getElementById("+").style.display="inline"; return;} | ||||
|  | ||||
|       var o = d.getElementsByClassName("iST"); | ||||
|       var i = o.length; | ||||
|  | ||||
|       if ((n==1 && i>=maxB) || (n==-1 && i==0)) return; | ||||
|  | ||||
|       var f = d.getElementById("mLC"); | ||||
|       if (n==1) { | ||||
|         var cn = `<div class="iST"> | ||||
|           ${i>0?'<hr style="width:260px">':''} | ||||
|           ${i+1}: | ||||
|           <select name="LT${i}" onchange="UI()"> | ||||
|             <option value="22">WS281x</option> | ||||
|             <option value="30">SK6812 RGBW</option> | ||||
|             <option value="31">TM1814</option> | ||||
|             <option value="24">400kHz</option> | ||||
|             <option value="50">WS2801</option> | ||||
|             <option value="51">APA102</option> | ||||
|             <option value="52">LPD8806</option> | ||||
|             <option value="53">P9813</option> | ||||
|             <option value="41">PWM White</option> | ||||
|             <option value="42">PWM WWCW</option> | ||||
|             <option value="43">PWM RGB</option> | ||||
|             <option value="44">PWM RGBW</option> | ||||
|             <option value="45">PWM RGBWC</option> | ||||
|           </select>  | ||||
|           Color Order: | ||||
|           <select name="CO${i}"> | ||||
|             <option value="0">GRB</option> | ||||
|             <option value="1">RGB</option> | ||||
|             <option value="2">BRG</option> | ||||
|             <option value="3">RBG</option> | ||||
|             <option value="4">BGR</option> | ||||
|             <option value="5">GBR</option> | ||||
|           </select><br> | ||||
|           <span id="p0d${i}">Pin:</span> <input type="number" name="L0${i}" min="0" max="40" required style="width:35px" oninput="UI()"/> | ||||
|           <span id="p1d${i}">Clock:</span> <input type="number" name="L1${i}" min="0" max="40" style="width:35px"/> | ||||
|           <span id="p2d${i}"></span><input type="number" name="L2${i}" min="0" max="40" style="width:35px"/> | ||||
|           <span id="p3d${i}"></span><input type="number" name="L3${i}" min="0" max="40" style="width:35px"/> | ||||
|           <span id="p4d${i}"></span><input type="number" name="L4${i}" min="0" max="40" style="width:35px"/> | ||||
|           <br> | ||||
|           <span id="psd${i}">Start:</span> <input type="number" name="LS${i}" min="0" max="8191" value="${lastEnd(i)}" required />  | ||||
|           <div id="dig${i}" style="display:inline"> | ||||
|           Count: <input type="number" name="LC${i}" min="0" max="${maxPB}" value="1" required oninput="UI()" /><br></div> | ||||
|           Reverse: <input type="checkbox" name="CV${i}"><br> | ||||
|         </div>`; | ||||
|         f.insertAdjacentHTML("beforeend", cn); | ||||
|       } | ||||
|       if (n==-1) { | ||||
|         o[--i].remove();--i; | ||||
|       } | ||||
|  | ||||
|       d.getElementById("+").style.display = (i<maxB-1) ? "inline":"none"; | ||||
|       d.getElementById("-").style.display = (i>0) ? "inline":"none"; | ||||
|  | ||||
|       UI(); | ||||
|     } | ||||
| 		function GetV() | ||||
| 		{ | ||||
|       //values injected by server while sending HTML | ||||
|       //d.um_p=[];addLEDs(3);d.Sf.LC.value=250;addLEDs(1);d.Sf.L00.value=2;d.Sf.L10.value=0;d.Sf.LC0.value=250;d.Sf.LT0.value=22;d.Sf.CO0.value=0;d.Sf.LS0.value=0;d.Sf.LS0.checked=0;d.Sf.MA.value=5400;d.Sf.LA.value=55;d.getElementsByClassName("pow")[0].innerHTML="350mA";d.Sf.CA.value=40;d.Sf.AW.value=3;d.Sf.BO.checked=0;d.Sf.BP.value=3;d.Sf.GB.checked=0;d.Sf.GC.checked=1;d.Sf.TF.checked=1;d.Sf.TD.value=700;d.Sf.PF.checked=0;d.Sf.BF.value=64;d.Sf.TB.value=0;d.Sf.TL.value=60;d.Sf.TW.value=1;d.Sf.PB.selectedIndex=0;d.Sf.RV.checked=0;d.Sf.SL.checked=0;d.Sf.RL.value=12;d.Sf.RM.checked=0;d.Sf.BT.value=-1;d.Sf.IR.value=-1;d.Sf.AX.value=-1; | ||||
|     } | ||||
| 	</script> | ||||
| 	<style> | ||||
| 		@import url("style.css"); | ||||
| 	</style> | ||||
| </head> | ||||
| <body onload="S()"> | ||||
| 	<form id="form_s" name="Sf" method="post"> | ||||
| 		<div class="helpB"><button type="button" onclick="H()">?</button></div> | ||||
| 		<button type="button" onclick="B()">Back</button><button type="button" onclick="trySubmit()">Save</button><hr> | ||||
|     <h2>LED & Hardware setup</h2> | ||||
|     Total LED count: <input name="LC" type="number" min="1" max="8192" oninput="UI()" required><br> | ||||
|   <i>Recommended power supply for brightest white:</i><br> | ||||
|   <b><span id="psu">?</span></b><br> | ||||
|   <span id="psu2"><br></span> | ||||
|   <br> | ||||
|   Enable automatic brightness limiter: <input type="checkbox" name="ABen" onchange="enABL()" id="able"><br> | ||||
|   <div id="abl"> | ||||
|     Maximum Current: <input name="MA" type="number" min="250" max="65000" oninput="UI()" required> mA<br> | ||||
|     <div id="ampwarning" style="color: orange; display: none;"> | ||||
|       ⚠ Your power supply provides high current.<br> | ||||
|       To improve the safety of your setup,<br> | ||||
|       please use thick cables,<br> | ||||
|       multiple power injection points and a fuse!<br> | ||||
|     </div> | ||||
|     <i>Automatically limits brightness to stay close to the limit.<br> | ||||
|     Keep at <1A if powering LEDs directly from the ESP 5V pin!<br> | ||||
|     If you are using an external power supply, enter its rating.<br> | ||||
|     (Current estimated usage: <span class="pow">unknown</span>)</i><br><br> | ||||
|     LED voltage (Max. current for a single LED):<br> | ||||
|     <select name="LAsel" onchange="enLA()"> | ||||
|       <option value="55" selected>5V default (55mA)</option> | ||||
|       <option value="35">5V efficient (35mA)</option> | ||||
|       <option value="30">12V (30mA)</option> | ||||
|       <option value="255">WS2815 (12mA)</option> | ||||
|       <option value="50">Custom</option> | ||||
|     </select><br> | ||||
|     <span id="LAdis" style="display: none;">Custom max. current per LED: <input name="LA" type="number" min="0" max="255" id="la" oninput="UI()" required> mA<br></span> | ||||
|     <i>Keep at default if you are unsure about your type of LEDs.</i><br> | ||||
|   </div> | ||||
|     <h3>Hardware setup</h3> | ||||
|     <div id="mLC">LED outputs:</div> | ||||
|     <button type="button" id="+" onclick="addLEDs(1)" style="display:none;border-radius:20px;height:36px;">+</button> | ||||
|     <button type="button" id="-" onclick="addLEDs(-1)" style="display:none;border-radius:20px;width:36px;height:36px;">-</button><br> | ||||
|     LED Memory Usage: <span id="m0">0</span> / <span id="m1">?</span> B<br> | ||||
|     <div id="dbar" style="display:inline-block; width: 100px; height: 10px; border-radius: 20px;"></div><br> | ||||
|     <div id="ledwarning" style="color: orange; display: none;"> | ||||
|       ⚠ You might run into stability or lag issues.<br> | ||||
|       Use less than <span id="wreason">800 LEDs per pin</span> for the best experience!<br> | ||||
|     </div><br> | ||||
|     Relay pin: <input type="number" min="-1" max="40" name="RL" onchange="UI()"> Active high <input type="checkbox" name="RM"><br> | ||||
|     Button pin: <input type="number" min="-1" max="40" name="BT" onchange="UI()"><br> | ||||
|     IR pin: <input type="number" min="-1" max="40" name="IR" onchange="UI()"><br> | ||||
|     AUX pin: <input type="number" min="-1" max="40" name="AX" onchange="UI()"> | ||||
| 		<h3>Defaults</h3> | ||||
| 		Turn LEDs on after power up/reset: <input type="checkbox" name="BO"><br> | ||||
|     Default brightness: <input name="CA" type="number" min="0" max="255" required> (0-255)<br><br> | ||||
|     Apply preset <input name="BP" type="number" min="0" max="250" required> at boot (0 uses defaults) | ||||
|     <br>- <i>or</i> -<br> | ||||
|     Set current preset cycle setting as boot default: <input type="checkbox" name="PC"><br><br> | ||||
| 		Use Gamma correction for color: <input type="checkbox" name="GC"> (strongly recommended)<br> | ||||
| 		Use Gamma correction for brightness: <input type="checkbox" name="GB"> (not recommended)<br><br> | ||||
| 		Brightness factor: <input name="BF" type="number" min="1" max="255" required> % | ||||
| 		<h3>Transitions</h3> | ||||
| 		Crossfade: <input type="checkbox" name="TF"><br> | ||||
| 		Transition Time: <input name="TD" maxlength="5" size="2"> ms<br> | ||||
| 		Enable Palette transitions: <input type="checkbox" name="PF"> | ||||
| 		<h3>Timed light</h3> | ||||
| 		Default Duration: <input name="TL" type="number" min="1" max="255" required> min<br> | ||||
| 		Default Target brightness: <input name="TB" type="number" min="0" max="255" required><br> | ||||
| 		Mode: | ||||
|     <select name="TW"> | ||||
| 			<option value="0">Wait and set</option> | ||||
| 			<option value="1">Fade</option> | ||||
| 			<option value="2">Fade Color</option> | ||||
| 			<option value="3">Sunrise</option> | ||||
| 		</select> | ||||
|     <h3>Advanced</h3> | ||||
| 		Palette blending: | ||||
| 		<select name="PB"> | ||||
| 			<option value="0">Linear (wrap if moving)</option> | ||||
| 			<option value="1">Linear (always wrap)</option> | ||||
| 			<option value="2">Linear (never wrap)</option> | ||||
| 			<option value="3">None (not recommended)</option> | ||||
| 		</select><br> | ||||
|     Skip first LED: <input type="checkbox" name="SL"><br> | ||||
|     <span class="wc"> | ||||
|       Auto-calculate white channel from RGB:<br> | ||||
|       <select name="AW"> | ||||
|         <option value=0>None</option> | ||||
|         <option value=1>Brighter</option> | ||||
|         <option value=2>Accurate</option> | ||||
|         <option value=3>Dual</option> | ||||
|         <option value=4>Legacy</option> | ||||
|       </select> | ||||
|     <br></span><hr> | ||||
| 		<button type="button" onclick="B()">Back</button><button type="button" onclick="trySubmit()">Save</button> | ||||
| 	</form> | ||||
| </body> | ||||
| </html> | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="utf-8"> | ||||
| 	<meta name="viewport" content="width=500"> | ||||
| 	<title>LED Settings</title> | ||||
| 	<script> | ||||
|     var d=document,laprev=55,maxB=1,maxM=5000,maxPB=4096,bquot=0; //maximum bytes for LED allocation: 5kB for 8266, 32kB for 32 | ||||
| 		function H() | ||||
| 		{ | ||||
| 			window.open("https://github.com/Aircoookie/WLED/wiki/Settings#led-settings"); | ||||
| 		} | ||||
| 		function B() | ||||
| 		{ | ||||
| 			window.open("/settings","_self"); | ||||
|     } | ||||
|     function gId(n){return d.getElementById(n);} | ||||
|     function off(n){ | ||||
|       d.getElementsByName(n)[0].value = -1; | ||||
|     } | ||||
|     function bLimits(b,p,m) { | ||||
|       maxB = b; maxM = m; maxPB = p; | ||||
|     } | ||||
|     function pinsOK() { | ||||
|       var LCs = d.getElementsByTagName("input"); | ||||
|       for (i=0; i<LCs.length; i++) { | ||||
|         var nm = LCs[i].name.substring(0,2); | ||||
|         //check for pin conflicts | ||||
|         if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4" || nm=="RL" || nm=="BT" || nm=="IR") | ||||
|           if (LCs[i].value!="" && LCs[i].value!="-1") { | ||||
|             if (d.um_p && d.um_p.some((e)=>e==parseInt(LCs[i].value,10))) {alert(`Sorry, pins ${JSON.stringify(d.um_p)} can't be used.`);LCs[i].value="";LCs[i].focus();return false;} | ||||
|             else if (LCs[i].value > 5 && LCs[i].value < 12) {alert("Sorry, pins 6-11 can not be used.");LCs[i].value="";LCs[i].focus();return false;} | ||||
|             for (j=i+1; j<LCs.length; j++) | ||||
|             { | ||||
|               var n2 = LCs[j].name.substring(0,2); | ||||
|               if (n2=="L0" || n2=="L1" || n2=="L2" || n2=="L3" || n2=="L4" || n2=="RL" || n2=="BT" || n2=="IR") | ||||
|                 if (LCs[j].value!="" && LCs[i].value==LCs[j].value) {alert(`Pin conflict between ${nm}/${n2}!`);LCs[j].value="";LCs[j].focus();return false;} | ||||
|             } | ||||
|           } | ||||
|       } | ||||
|       return true; | ||||
|     } | ||||
|     function trySubmit(e) { | ||||
|       e.preventDefault(); | ||||
|       if (!pinsOK()) {e.stopPropagation();return false;} // Prevent form submission and contact with server | ||||
|       if (bquot > 100) {var msg = "Too many LEDs for me to handle!"; if (maxM < 10000) msg += "\n\rConsider using an ESP32."; alert(msg);} | ||||
|       if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914 | ||||
|     } | ||||
| 		function S(){GetV();setABL();} | ||||
|     function enABL() | ||||
|     { | ||||
|       var en = gId('able').checked; | ||||
|       d.Sf.LA.value = (en) ? laprev:0; | ||||
|       gId('abl').style.display = (en) ? 'inline':'none'; | ||||
|       gId('psu2').style.display = (en) ? 'inline':'none'; | ||||
|       if (d.Sf.LA.value > 0) setABL(); | ||||
|     } | ||||
|     function enLA() | ||||
|     { | ||||
|       var val = d.Sf.LAsel.value; | ||||
|       d.Sf.LA.value = val; | ||||
|       gId('LAdis').style.display = (val == 50) ? 'inline':'none'; | ||||
|       UI(); | ||||
|     } | ||||
|     function setABL() | ||||
|     { | ||||
|       gId('able').checked = true; | ||||
|       d.Sf.LAsel.value = 50; | ||||
|       switch (parseInt(d.Sf.LA.value)) { | ||||
|         case 0: gId('able').checked = false; enABL(); break; | ||||
|         case 30: d.Sf.LAsel.value = 30; break; | ||||
|         case 35: d.Sf.LAsel.value = 35; break; | ||||
|         case 55: d.Sf.LAsel.value = 55; break; | ||||
|         case 255: d.Sf.LAsel.value = 255; break; | ||||
|         default: gId('LAdis').style.display = 'inline'; | ||||
|       } | ||||
|       gId('m1').innerHTML = maxM; | ||||
|       d.getElementsByName("Sf")[0].addEventListener("submit", trySubmit); | ||||
|       UI(); | ||||
|     } | ||||
|     //returns mem usage | ||||
|     function getMem(type, len, p0) { | ||||
|       if (type < 32) { | ||||
|         if (maxM < 10000 && p0==3) {    //8266 DMA uses 5x the mem | ||||
|           if (type > 29) return len*20; //RGBW | ||||
|           return len*15; | ||||
|         } else if (maxM >= 10000) //ESP32 RMT uses double buffer? | ||||
|         { | ||||
|           if (type > 29) return len*8; //RGBW | ||||
|           return len*6; | ||||
|         } | ||||
|         if (type > 29) return len*4; //RGBW | ||||
|         return len*3; | ||||
|       } | ||||
|       if (type > 31 && type < 48) return 5; | ||||
|       if (type == 44 || type == 45) return len*4; //RGBW | ||||
|       return len*3; | ||||
|     } | ||||
| 		function UI(change=false) | ||||
| 		{ | ||||
|       var isRGBW = false, memu = 0; | ||||
|  | ||||
|       gId('ampwarning').style.display = (d.Sf.MA.value > 7200) ? 'inline':'none'; | ||||
|  | ||||
| 	    if (d.Sf.LA.value == 255) laprev = 12; | ||||
| 	    else if (d.Sf.LA.value > 0) laprev = d.Sf.LA.value; | ||||
|  | ||||
|       var s = d.getElementsByTagName("select"); | ||||
|       for (i=0; i<s.length; i++) { | ||||
|         if (s[i].name.substring(0,2)=="LT") { | ||||
|           n=s[i].name.substring(2); | ||||
|           var type = parseInt(s[i].value,10); | ||||
|           gId("p0d"+n).innerHTML = (type > 49) ? "Data:" : (type >41) ? "Pins:" : "Pin:"; | ||||
|           gId("p1d"+n).innerHTML = (type > 49) ? "Clk:" : ""; | ||||
|           var LK = d.getElementsByName("L1"+n)[0]; | ||||
|  | ||||
|           memu += getMem(type, d.getElementsByName("LC"+n)[0].value, d.getElementsByName("L0"+n)[0].value); | ||||
|  | ||||
|           for (p=1; p<5; p++) { | ||||
|             var LK = d.getElementsByName("L"+p+n)[0]; | ||||
|             if (!LK) continue; | ||||
|             if ((type>49 && p==1) || (type>41 && type < 50 && (p+40 < type))) // TYPE_xxxx values from const.h | ||||
|             { | ||||
|               LK.style.display = "inline"; | ||||
|               LK.required = true; | ||||
|             } else { | ||||
|               LK.style.display = "none"; | ||||
|               LK.required = false; | ||||
|               LK.value=""; | ||||
|             } | ||||
|           } | ||||
|           if (type == 30 || type == 31 || (type > 40 && type < 46 && type != 43)) isRGBW = true; | ||||
|           gId("dig"+n).style.display = (type > 31 && type < 48) ? "none":"inline"; | ||||
|           gId("psd"+n).innerHTML = (type > 31 && type < 48) ? "Index:":"Start:"; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       var myC = d.querySelectorAll('.wc'), | ||||
| 			l = myC.length; | ||||
| 			for (i = 0; i < l; i++) { | ||||
| 				myC[i].style.display = (isRGBW) ? 'inline':'none'; | ||||
| 			} | ||||
|  | ||||
|       if (d.activeElement == d.getElementsByName("LC")[0]) { | ||||
|         var o = d.getElementsByClassName("iST"); | ||||
|         var i = o.length; | ||||
|         if (i == 1) d.getElementsByName("LC0")[0].value = d.getElementsByName("LC")[0].value; | ||||
|       } | ||||
|  | ||||
|       var LCs = d.getElementsByTagName("input"); | ||||
|       var sLC = 0, maxLC = 0; | ||||
|       for (i=0; i<LCs.length; i++) { | ||||
|         var nm = LCs[i].name.substring(0,2); | ||||
|         if (nm=="LC" && LCs[i].name !== "LC") { | ||||
|           var n=LCs[i].name.substring(2); | ||||
|           var c=parseInt(LCs[i].value,10); | ||||
|           if(gId("ls"+n).readOnly) gId("ls"+n).value=sLC; | ||||
|           if(c){sLC+=c;if(c>maxLC)maxLC=c;} | ||||
|           continue; | ||||
|         } | ||||
|         if (nm=="L0" || nm=="L1") { | ||||
|           var lc=d.getElementsByName("LC"+LCs[i].name.substring(2))[0]; | ||||
|           lc.max=maxPB; | ||||
|         } | ||||
|         if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4" || nm=="RL" || nm=="BT" || nm=="IR") | ||||
|           if (LCs[i].value!="" && LCs[i].value!="-1") { | ||||
|             var p = []; | ||||
|             if (d.um_p && Array.isArray(d.um_p)) for (k=0;k<d.um_p.length;k++) p.push(d.um_p[k]); | ||||
|             for (j=0; j<LCs.length; j++) { | ||||
|               if (i==j) continue; | ||||
|               var n2 = LCs[j].name.substring(0,2); | ||||
|               if (n2=="L0" || n2=="L1" || n2=="L2" || n2=="L3" || n2=="L4" || n2=="RL" || n2=="BT" || n2=="IR") | ||||
|                 if (LCs[j].value!="" && LCs[j].value!="-1") p.push(parseInt(LCs[j].value,10)); | ||||
|             } | ||||
|             if (p.some((e)=>e==parseInt(LCs[i].value,10))) LCs[i].style.color="red"; else LCs[i].style.color="#fff"; | ||||
|           } | ||||
|       } | ||||
|  | ||||
|       gId('m0').innerHTML = memu; | ||||
|       bquot = memu / maxM * 100; | ||||
|       gId('dbar').style.background = `linear-gradient(90deg, ${bquot > 60 ? (bquot > 90 ? "red":"orange"):"#ccc"} 0 ${bquot}%%, #444 ${bquot}%% 100%%)`; | ||||
|       gId('ledwarning').style.display = (sLC > maxPB || maxLC > 800 || bquot > 80) ? 'inline':'none'; | ||||
|       gId('ledwarning').style.color = (sLC > maxPB || maxLC > maxPB || bquot > 100) ? 'red':'orange'; | ||||
|       gId('wreason').innerHTML = (bquot > 80) ? "80% of max. LED memory" +(bquot>100 ? ` (<b>WARNING: Using over ${maxM}B!</b>)` : "") : "800 LEDs per pin"; | ||||
|  | ||||
|       var val = Math.ceil((100 + sLC * laprev)/500)/2; | ||||
| 			val = (val > 5) ? Math.ceil(val) : val; | ||||
| 			var s = ""; | ||||
|       var is12V = (d.Sf.LAsel.value == 30); | ||||
|       var isWS2815 = (d.Sf.LAsel.value == 255); | ||||
| 			if (val < 1.02 && !is12V && !isWS2815) | ||||
| 			{ | ||||
| 				s = "ESP 5V pin with 1A USB supply"; | ||||
| 			} else | ||||
| 			{ | ||||
|         		s += is12V ? "12V ": isWS2815 ? "WS2815 12V " : "5V "; | ||||
| 				s += val; | ||||
| 				s += "A supply connected to LEDs"; | ||||
| 			} | ||||
|       var val2 = Math.ceil((100 + sLC * laprev)/1500)/2; | ||||
|       val2 = (val2 > 5) ? Math.ceil(val2) : val2; | ||||
|       var s2 = "(for most effects, ~"; | ||||
|       s2 += val2; | ||||
|       s2 += "A is enough)<br>"; | ||||
| 			gId('psu').innerHTML = s; | ||||
|       gId('psu2').innerHTML = isWS2815 ? "" : s2; | ||||
|     } | ||||
|     function lastEnd(i) { | ||||
|       if (i<1) return 0; | ||||
|       v = parseInt(d.getElementsByName("LS"+(i-1))[0].value) + parseInt(d.getElementsByName("LC"+(i-1))[0].value); | ||||
|       var type = parseInt(d.getElementsByName("LT"+(i-1))[0].value); | ||||
|       if (type > 31 && type < 48) v = 1; //PWM busses | ||||
|       if (isNaN(v)) return 0; | ||||
|       return v; | ||||
|     } | ||||
|     function addLEDs(n) | ||||
|     { | ||||
|       if (n>1) {maxB=n; gId("+").style.display="inline"; return;} | ||||
|  | ||||
|       var o = d.getElementsByClassName("iST"); | ||||
|       var i = o.length; | ||||
|  | ||||
|       if ((n==1 && i>=maxB) || (n==-1 && i==0)) return; | ||||
|  | ||||
|       var f = gId("mLC"); | ||||
|       if (n==1) { | ||||
| // npm run build has trouble minimizing spaces inside string | ||||
|         var cn = `<div class="iST"> | ||||
| ${i>0?'<hr style="width:260px">':''} | ||||
| ${i+1}: | ||||
| <select name="LT${i}" onchange="UI()"> | ||||
| <option value="22">WS281x</option> | ||||
| <option value="30">SK6812 RGBW</option> | ||||
| <option value="31">TM1814</option> | ||||
| <option value="24">400kHz</option> | ||||
| <option value="50">WS2801</option> | ||||
| <option value="51">APA102</option> | ||||
| <option value="52">LPD8806</option> | ||||
| <option value="53">P9813</option> | ||||
| <option value="41">PWM White</option> | ||||
| <option value="42">PWM WWCW</option> | ||||
| <option value="43">PWM RGB</option> | ||||
| <option value="44">PWM RGBW</option> | ||||
| <option value="45">PWM RGBWC</option> | ||||
| </select>  | ||||
| Color Order: | ||||
| <select name="CO${i}"> | ||||
| <option value="0">GRB</option> | ||||
| <option value="1">RGB</option> | ||||
| <option value="2">BRG</option> | ||||
| <option value="3">RBG</option> | ||||
| <option value="4">BGR</option> | ||||
| <option value="5">GBR</option> | ||||
| </select><br> | ||||
| <span id="p0d${i}">Pin:</span> <input type="number" name="L0${i}" min="0" max="40" required style="width:35px" onchange="UI()"/> | ||||
| <span id="p1d${i}">Clock:</span> <input type="number" name="L1${i}" min="0" max="40" style="width:35px" onchange="UI()"/> | ||||
| <span id="p2d${i}"></span><input type="number" name="L2${i}" min="0" max="40" style="width:35px" onchange="UI()"/> | ||||
| <span id="p3d${i}"></span><input type="number" name="L3${i}" min="0" max="40" style="width:35px" onchange="UI()"/> | ||||
| <span id="p4d${i}"></span><input type="number" name="L4${i}" min="0" max="40" style="width:35px" onchange="UI()"/> | ||||
| <br> | ||||
| <span id="psd${i}">Start:</span> <input type="number" name="LS${i}" id="ls${i}" min="0" max="8191" value="${lastEnd(i)}" required />  | ||||
| <div id="dig${i}" style="display:inline"> | ||||
| Count: <input type="number" name="LC${i}" min="0" max="${maxPB}" value="1" required oninput="UI()" /><br> | ||||
| Reverse (rotated 180°): <input type="checkbox" name="CV${i}"> | ||||
|  Skip 1<sup>st</sup> LED: <input id="sl${i}" type="checkbox" name="SL${i}"><br> | ||||
| </div> | ||||
| </div>`; | ||||
|         f.insertAdjacentHTML("beforeend", cn); | ||||
|       } | ||||
|       if (n==-1) { | ||||
|         o[--i].remove();--i; | ||||
|       } | ||||
|  | ||||
|       gId("+").style.display = (i<maxB-1) ? "inline":"none"; | ||||
|       gId("-").style.display = (i>0) ? "inline":"none"; | ||||
|  | ||||
|       UI(); | ||||
|     } | ||||
|     function addBtn(i,p,t) { | ||||
|       var c = gId("btns").innerHTML; | ||||
|       var bt = "BT" + i; | ||||
|       var be = "BE" + i; | ||||
|       c += `Button ${i} pin: <input type="number" min="-1" max="40" name="${bt}" onchange="UI()" style="width:35px" value="${p}"> `; | ||||
|       c += `<select name="${be}">` | ||||
|       c += `<option value="0" ${t==0?"selected":""}>Disabled</option>`; | ||||
|       c += `<option value="2" ${t==2?"selected":""}>Pushbutton</option>`; | ||||
|       c += `<option value="3" ${t==3?"selected":""}>Push inverted</option>`; | ||||
|       c += `<option value="4" ${t==4?"selected":""}>Switch</option>`; | ||||
|       c += `<option value="5" ${t==4?"selected":""}>Switch inverted</option>`; | ||||
|       c += `<option value="6" ${t==6?"selected":""}>Touch</option>`; | ||||
|       c += `<option value="7" ${t==7?"selected":""}>Analog</option>`; | ||||
|       c += `</select>`; | ||||
|       c += `<span style="cursor: pointer;" onclick="off('${bt}')"> ×</span><br>`; | ||||
|       gId("btns").innerHTML = c; | ||||
|     } | ||||
| 		function GetV() | ||||
| 		{ | ||||
|       //values injected by server while sending HTML | ||||
|       //maxM=5000;maxPB=1536;d.um_p=[1,6,7,8,9,10,11];addLEDs(3);d.Sf.LC.value=250;addLEDs(1);d.Sf.L00.value=2;d.Sf.L10.value=0;d.Sf.LC0.value=250;d.Sf.LT0.value=22;d.Sf.CO0.value=0;d.Sf.LS0.value=0;d.Sf.LS0.checked=0;d.Sf.MA.value=5400;d.Sf.LA.value=55;d.getElementsByClassName("pow")[0].innerHTML="350mA";d.Sf.CA.value=40;d.Sf.AW.value=3;d.Sf.BO.checked=0;d.Sf.BP.value=3;d.Sf.GB.checked=0;d.Sf.GC.checked=1;d.Sf.TF.checked=1;d.Sf.TD.value=700;d.Sf.PF.checked=0;d.Sf.BF.value=64;d.Sf.TB.value=0;d.Sf.TL.value=60;d.Sf.TW.value=1;d.Sf.PB.selectedIndex=0;d.Sf.RL.value=12;d.Sf.RM.checked=0;addBtn(0,0,2);addBtn(1,3,4);addBtn(2,-1,0);d.Sf.IR.value=-1; | ||||
|     } | ||||
| 	</script> | ||||
| 	<style> | ||||
| 		@import url("style.css"); | ||||
| 	</style> | ||||
| </head> | ||||
| <body onload="S()"> | ||||
| 	<form id="form_s" name="Sf" method="post"> | ||||
| 		<div class="helpB"><button type="button" onclick="H()">?</button></div> | ||||
| 		<button type="button" onclick="B()">Back</button><button type="submit">Save</button><hr> | ||||
|     <h2>LED & Hardware setup</h2> | ||||
|     Total LED count: <input name="LC" id="LC" type="number" min="1" max="8192" oninput="UI()" required><br> | ||||
|     <i>Recommended power supply for brightest white:</i><br> | ||||
|     <b><span id="psu">?</span></b><br> | ||||
|     <span id="psu2"><br></span> | ||||
|     <br> | ||||
|     Enable automatic brightness limiter: <input type="checkbox" name="ABen" onchange="enABL()" id="able"><br> | ||||
|     <div id="abl"> | ||||
|       Maximum Current: <input name="MA" type="number" min="250" max="65000" oninput="UI()" required> mA<br> | ||||
|       <div id="ampwarning" style="color: orange; display: none;"> | ||||
|         ⚠ Your power supply provides high current.<br> | ||||
|         To improve the safety of your setup,<br> | ||||
|         please use thick cables,<br> | ||||
|         multiple power injection points and a fuse!<br> | ||||
|       </div> | ||||
|       <i>Automatically limits brightness to stay close to the limit.<br> | ||||
|       Keep at <1A if powering LEDs directly from the ESP 5V pin!<br> | ||||
|       If you are using an external power supply, enter its rating.<br> | ||||
|       (Current estimated usage: <span class="pow">unknown</span>)</i><br><br> | ||||
|       LED voltage (Max. current for a single LED):<br> | ||||
|       <select name="LAsel" onchange="enLA()"> | ||||
|         <option value="55" selected>5V default (55mA)</option> | ||||
|         <option value="35">5V efficient (35mA)</option> | ||||
|         <option value="30">12V (30mA)</option> | ||||
|         <option value="255">WS2815 (12mA)</option> | ||||
|         <option value="50">Custom</option> | ||||
|       </select><br> | ||||
|       <span id="LAdis" style="display: none;">Custom max. current per LED: <input name="LA" type="number" min="0" max="255" id="la" oninput="UI()" required> mA<br></span> | ||||
|       <i>Keep at default if you are unsure about your type of LEDs.</i><br> | ||||
|     </div> | ||||
|     <h3>Hardware setup</h3> | ||||
|     <div id="mLC">LED outputs:</div> | ||||
|     <button type="button" id="+" onclick="addLEDs(1)" style="display:none;border-radius:20px;height:36px;">+</button> | ||||
|     <button type="button" id="-" onclick="addLEDs(-1)" style="display:none;border-radius:20px;width:36px;height:36px;">-</button><br> | ||||
|     LED Memory Usage: <span id="m0">0</span> / <span id="m1">?</span> B<br> | ||||
|     <div id="dbar" style="display:inline-block; width: 100px; height: 10px; border-radius: 20px;"></div><br> | ||||
|     <div id="ledwarning" style="color: orange; display: none;"> | ||||
|       ⚠ You might run into stability or lag issues.<br> | ||||
|       Use less than <span id="wreason">800 LEDs per pin</span> for the best experience!<br> | ||||
|     </div><hr style="width:260px"> | ||||
|     <div id="btns"></div> | ||||
|     Touch threshold: <input type="number" min="0" max="100" name="TT" required><br> | ||||
|     IR pin: <input type="number" min="-1" max="40" name="IR" onchange="UI()" style="width:35px"> <select name="IT"> | ||||
|     <option value="0">Remote disabled</option> | ||||
|     <option value="1">24-key RGB</option> | ||||
|     <option value="2">24-key with CT</option> | ||||
|     <option value="3">40-key blue</option> | ||||
|     <option value="4">44-key RGB</option> | ||||
|     <option value="5">21-key RGB</option> | ||||
|     <option value="6">6-key black</option> | ||||
|     <option value="7">9-key red</option> | ||||
|     </select><span style="cursor: pointer;" onclick="off('IR')"> ×</span><br> | ||||
|     <a href="https://github.com/Aircoookie/WLED/wiki/Infrared-Control" target="_blank">IR info</a><br> | ||||
|     Relay pin: <input type="number" min="-1" max="40" name="RL" onchange="UI()" style="width:35px"> Invert <input type="checkbox" name="RM"><span style="cursor: pointer;" onclick="off('RL')"> ×</span><br> | ||||
|     <hr style="width:260px"> | ||||
| 		<h3>Defaults</h3> | ||||
| 		Turn LEDs on after power up/reset: <input type="checkbox" name="BO"><br> | ||||
|     Default brightness: <input name="CA" type="number" min="0" max="255" required> (0-255)<br><br> | ||||
|     Apply preset <input name="BP" type="number" min="0" max="250" required> at boot (0 uses defaults) | ||||
|     <br><br> | ||||
| 		Use Gamma correction for color: <input type="checkbox" name="GC"> (strongly recommended)<br> | ||||
| 		Use Gamma correction for brightness: <input type="checkbox" name="GB"> (not recommended)<br><br> | ||||
| 		Brightness factor: <input name="BF" type="number" min="1" max="255" required> % | ||||
| 		<h3>Transitions</h3> | ||||
| 		Crossfade: <input type="checkbox" name="TF"><br> | ||||
| 		Transition Time: <input name="TD" maxlength="5" size="2"> ms<br> | ||||
| 		Enable Palette transitions: <input type="checkbox" name="PF"> | ||||
| 		<h3>Timed light</h3> | ||||
| 		Default Duration: <input name="TL" type="number" min="1" max="255" required> min<br> | ||||
| 		Default Target brightness: <input name="TB" type="number" min="0" max="255" required><br> | ||||
| 		Mode: | ||||
|     <select name="TW"> | ||||
| 			<option value="0">Wait and set</option> | ||||
| 			<option value="1">Fade</option> | ||||
| 			<option value="2">Fade Color</option> | ||||
| 			<option value="3">Sunrise</option> | ||||
| 		</select> | ||||
|     <h3>Advanced</h3> | ||||
| 		Palette blending: | ||||
| 		<select name="PB"> | ||||
| 			<option value="0">Linear (wrap if moving)</option> | ||||
| 			<option value="1">Linear (always wrap)</option> | ||||
| 			<option value="2">Linear (never wrap)</option> | ||||
| 			<option value="3">None (not recommended)</option> | ||||
| 		</select><br> | ||||
|     <span class="wc"> | ||||
|       Auto-calculate white channel from RGB:<br> | ||||
|       <select name="AW"> | ||||
|         <option value=0>None</option> | ||||
|         <option value=1>Brighter</option> | ||||
|         <option value=2>Accurate</option> | ||||
|         <option value=3>Dual</option> | ||||
|         <option value=4>Legacy</option> | ||||
|       </select> | ||||
|     <br></span><hr> | ||||
| 		<button type="button" onclick="B()">Back</button><button type="submit">Save</button> | ||||
| 	</form> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
| 		<i>Settings on this page are only changable if OTA lock is disabled!</i><br> | ||||
| 		Deny access to WiFi settings if locked: <input type="checkbox" name="OW"><br><br> | ||||
| 		Factory reset: <input type="checkbox" name="RS"><br> | ||||
| 		All EEPROM content (settings) will be erased.<br><br> | ||||
| 		All settings and presets will be erased.<br><br> | ||||
| 		HTTP traffic is unencrypted. An attacker in the same network can intercept form data! | ||||
| 		<h3>Software Update</h3> | ||||
| 		<button type="button" onclick="U()">Manual OTA Update</button><br> | ||||
|   | ||||
| @@ -15,24 +15,10 @@ function GetV(){var d=document;} | ||||
| <div class="helpB"><button type="button" onclick="H()">?</button></div> | ||||
| <button type="button" onclick="B()">Back</button><button type="submit">Save</button><hr> | ||||
| <h2>Sync setup</h2> | ||||
| <h3>Button setup</h3> | ||||
| On/Off button enabled: <input type="checkbox" name="BT"><br> | ||||
| Infrared remote: | ||||
| <select name=IR> | ||||
| <option value=0>Disabled</option> | ||||
| <option value=1>24-key RGB</option> | ||||
| <option value=2>24-key with CT</option> | ||||
| <option value=3>40-key blue</option> | ||||
| <option value=4>44-key RGB</option> | ||||
| <option value=5>21-key RGB</option> | ||||
| <option value=6>6-key black</option> | ||||
| <option value=7>9-key red</option> | ||||
| </select><br> | ||||
| <a href="https://github.com/Aircoookie/WLED/wiki/Infrared-Control" target="_blank">IR info</a> | ||||
| <h3>WLED Broadcast</h3> | ||||
| UDP Port: <input name="UP" type="number" min="1" max="65535" class="d5" required><br> | ||||
| 2nd Port: <input name="U2" type="number" min="1" max="65535" class="d5" required><br> | ||||
| Receive <input type="checkbox" name="RB">Brightness, <input type="checkbox" name="RC">Color, and <input type="checkbox" name="RX">Effects<br> | ||||
| Receive: <input type="checkbox" name="RB">Brightness, <input type="checkbox" name="RC">Color, and <input type="checkbox" name="RX">Effects<br> | ||||
| Send notifications on direct change: <input type="checkbox" name="SD"><br> | ||||
| Send notifications on button press or IR: <input type="checkbox" name="SB"><br> | ||||
| Send Alexa notifications: <input type="checkbox" name="SA"><br> | ||||
| @@ -93,7 +79,7 @@ Port: <input name="MQPORT" type="number" min="1" max="65535" class="d5"><br> | ||||
| <b>The MQTT credentials are sent over an unsecured connection.<br> | ||||
| Never use the MQTT password for another service!</b><br> | ||||
| Username: <input name="MQUSER" maxlength="40"><br> | ||||
| Password: <input type="password" name="MQPASS" maxlength="40"><br> | ||||
| Password: <input type="password" name="MQPASS" maxlength="64"><br> | ||||
| Client ID: <input name="MQCID" maxlength="40"><br> | ||||
| Device Topic: <input name="MD" maxlength="32"><br> | ||||
| Group Topic: <input name="MG" maxlength="32"><br> | ||||
|   | ||||
| @@ -6,41 +6,41 @@ | ||||
| 	<title>Time Settings</title> | ||||
| 	<script> | ||||
|     var d=document; | ||||
| 		function H() | ||||
| 	function H() | ||||
| 	{ | ||||
| 		window.open("https://github.com/Aircoookie/WLED/wiki/Settings#time-settings"); | ||||
| 	} | ||||
| 	function B() | ||||
| 	{ | ||||
| 		window.open("/settings","_self"); | ||||
| 	} | ||||
| 	function S() | ||||
| 	{ | ||||
| 		BTa();GetV();Cs();FC(); | ||||
| 	} | ||||
| 	function gId(s) | ||||
| 	{ | ||||
| 		return d.getElementById(s); | ||||
| 	} | ||||
| 	function Cs() | ||||
| 	{ | ||||
| 		gId("cac").style.display="none"; | ||||
| 		gId("coc").style.display="block"; | ||||
| 		gId("ccc").style.display="none"; | ||||
| 		if (gId("ca").selected) | ||||
| 		{ | ||||
| 			window.open("https://github.com/Aircoookie/WLED/wiki/Settings#time-settings"); | ||||
| 			gId("cac").style.display="block"; | ||||
| 		} | ||||
| 		function B() | ||||
| 		if (gId("cc").selected) | ||||
| 		{ | ||||
| 			window.open("/settings","_self"); | ||||
| 			gId("coc").style.display="none"; | ||||
| 			gId("ccc").style.display="block"; | ||||
| 		} | ||||
| 		function S() | ||||
| 		if (gId("cn").selected) | ||||
| 		{ | ||||
| 			BTa();GetV();Cs();FC(); | ||||
| 		} | ||||
| 		function gId(s) | ||||
| 		{ | ||||
| 			return d.getElementById(s); | ||||
| 		} | ||||
| 		function Cs() | ||||
| 		{ | ||||
| 			gId("cac").style.display="none"; | ||||
| 			gId("coc").style.display="block"; | ||||
| 			gId("ccc").style.display="none"; | ||||
| 			if (gId("ca").selected) | ||||
| 			{ | ||||
| 				gId("cac").style.display="block"; | ||||
| 			} | ||||
| 			if (gId("cc").selected) | ||||
| 			{ | ||||
| 				gId("coc").style.display="none"; | ||||
| 				gId("ccc").style.display="block"; | ||||
| 			} | ||||
| 			if (gId("cn").selected) | ||||
| 			{ | ||||
| 				gId("coc").style.display="none"; | ||||
| 			} | ||||
| 			gId("coc").style.display="none"; | ||||
| 		} | ||||
| 	} | ||||
|     function BTa() | ||||
|     { | ||||
|       var ih="<tr><th>Active</th><th>Hour</th><th>Minute</th><th>Preset</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr>"; | ||||
| @@ -49,19 +49,23 @@ | ||||
|         ih+="<tr><td><input name=\"W"+i+"\" id=\"W"+i+"\" type=\"number\" style=\"display:none\"><input id=\"W"+i+"0\" type=\"checkbox\"></td><td><input name=\"H"+i+"\" type=\"number\" min=\"0\" max=\"24\"></td><td><input name=\"N"+i+"\" type=\"number\" min=\"0\" max=\"59\"></td><td><input name=\"T"+i+"\" type=\"number\" min=\"0\" max=\"250\"></td>"; | ||||
|         for (j=1;j<8;j++) ih+="<td><input id=\"W"+i+j+"\" type=\"checkbox\"></td>"; | ||||
|       } | ||||
| 	  ih+="<tr><td><input name=\"W8\" id=\"W8\" type=\"number\" style=\"display:none\"><input id=\"W80\" type=\"checkbox\"></td><td>Sunrise<input name=\"H8\" value=\"255\" type=\"hidden\"></td><td><input name=\"N8\" type=\"number\" min=\"-59\" max=\"59\"></td><td><input name=\"T8\" type=\"number\" min=\"0\" max=\"250\"></td>"; | ||||
|       for (j=1;j<8;j++) ih+="<td><input id=\"W8"+j+"\" type=\"checkbox\"></td>"; | ||||
|       ih+="<tr><td><input name=\"W9\" id=\"W9\" type=\"number\" style=\"display:none\"><input id=\"W90\" type=\"checkbox\"></td><td>Sunset<input name=\"H9\" value=\"255\" type=\"hidden\"></td><td><input name=\"N9\" type=\"number\" min=\"-59\" max=\"59\"><td><input name=\"T9\" type=\"number\" min=\"0\" max=\"250\"></td>"; | ||||
|       for (j=1;j<8;j++) ih+="<td><input id=\"W9"+j+"\" type=\"checkbox\"></td>"; | ||||
|       gId("TMT").innerHTML=ih; | ||||
|     } | ||||
|     function FC() | ||||
|     { | ||||
|       for(j=0;j<8;j++) | ||||
|       { | ||||
|         for(i=0;i<8;i++) gId("W"+i+j).checked=gId("W"+i).value>>j&1; | ||||
|         for(i=0;i<10;i++) gId("W"+i+j).checked=gId("W"+i).value>>j&1; | ||||
|       } | ||||
|     } | ||||
|     function Wd() | ||||
|     { | ||||
|       a=[0,0,0,0,0,0,0,0]; | ||||
|       for(i=0;i<8;i++) | ||||
|       a=[0,0,0,0,0,0,0,0,0,0]; | ||||
|       for(i=0;i<10;i++) | ||||
|       { | ||||
|         m=1; | ||||
|         for(j=0;j<8;j++) | ||||
| @@ -71,10 +75,25 @@ | ||||
|         gId("W"+i).value=a[i]; | ||||
|       } | ||||
|     } | ||||
| 		function GetV() | ||||
| 		{ | ||||
| 			//values injected by server while sending HTML | ||||
| 		} | ||||
| 	function addRow(i,p,l,d) { | ||||
| 		var t = gId("macros");	// table | ||||
|         var rCnt = t.rows.length;   // get the number of rows. | ||||
|         var tr = t.insertRow(rCnt); // table row. | ||||
|  | ||||
| 		var td = document.createElement('td');          // TABLE DEFINITION. | ||||
| 		td = tr.insertCell(0); | ||||
| 		td.innerHTML = `Button ${i}:`; | ||||
| 		td = tr.insertCell(1); | ||||
| 		td.innerHTML = `<input name="MP${i}" type="number" min="0" max="250" value="${p}" required>`; | ||||
| 		td = tr.insertCell(2); | ||||
| 		td.innerHTML = `<input name="ML${i}" type="number" min="0" max="250" value="${l}" required>`; | ||||
| 		td = tr.insertCell(3); | ||||
| 		td.innerHTML = `<input name="MD${i}" type="number" min="0" max="250" value="${d}" required>`; | ||||
| 	} | ||||
| 	function GetV() | ||||
| 	{ | ||||
| 		//values injected by server while sending HTML | ||||
| 	} | ||||
| 	</script> | ||||
| 	<style> | ||||
| 		@import url("style.css"); | ||||
| @@ -103,15 +122,18 @@ | ||||
| 			<option value="10">JST(KST)</option> | ||||
| 			<option value="11">AEST/AEDT</option> | ||||
| 			<option value="12">NZST/NZDT</option> | ||||
|       <option value="13">North Korea</option> | ||||
|       <option value="14">IST (India)</option> | ||||
|       <option value="15">CA-Saskatchewan</option> | ||||
|       <option value="16">ACST</option> | ||||
|       <option value="17">ACST/ACDT</option> | ||||
|       <option value="18">HST (Hawaii)</option> | ||||
| 			<option value="13">North Korea</option> | ||||
| 			<option value="14">IST (India)</option> | ||||
| 			<option value="15">CA-Saskatchewan</option> | ||||
| 			<option value="16">ACST</option> | ||||
| 			<option value="17">ACST/ACDT</option> | ||||
| 			<option value="18">HST (Hawaii)</option> | ||||
| 		</select><br> | ||||
| 		UTC offset: <input name="UO" type="number" min="-65500" max="65500" required> seconds (max. 18 hours)<br> | ||||
| 		Current local time is <span class="times">unknown</span>. | ||||
| 		Current local time is <span class="times">unknown</span>.<br> | ||||
| 		Latitude (N): <input name="LT" type="number" min="-66.6" max="66.6" step="0.01"> | ||||
| 		Longitude (E): <input name="LN" type="number" min="-180" max="180" step="0.01"> | ||||
| 		<div id="sun" class="times"></div> | ||||
| 		<h3>Clock</h3> | ||||
| 		Clock Overlay: | ||||
| 		<select name="OL" onchange="Cs()"> | ||||
| @@ -136,15 +158,27 @@ | ||||
| 		Year: 20 <input name="CY" type="number" min="0" max="99" required> Month: <input name="CI" type="number" min="1" max="12" required> Day: <input name="CD" type="number" min="1" max="31" required><br> | ||||
| 		Hour: <input name="CH" type="number" min="0" max="23" required> Minute: <input name="CM" type="number" min="0" max="59" required> Second: <input name="CS" type="number" min="0" max="59" required><br> | ||||
| 		<h3>Macro presets</h3> | ||||
|     <b>Macros have moved!</b><br> | ||||
|     <i>Presets now also can be used as macros to save both JSON and HTTP API commands.<br> | ||||
|     Just enter the preset id below!</i> | ||||
|     	<b>Macros have moved!</b><br> | ||||
|     	<i>Presets now also can be used as macros to save both JSON and HTTP API commands.<br> | ||||
|     	Just enter the preset id below!</i> | ||||
| 		<i>Use 0 for the default action instead of a preset</i><br> | ||||
| 		Alexa On/Off Preset: <input name="A0" type="number" min="0" max="250" required> <input name="A1" type="number" min="0" max="250" required><br> | ||||
| 		Button short press Preset: <input name="MP" type="number" min="0" max="250" required><br> | ||||
|     Long Press: <input name="ML" type="number" min="0" max="250" required> Double press: <input name="MD" type="number" min="0" max="250" required><br>	 | ||||
| 		Countdown-Over Preset: <input name="MC" type="number" min="0" max="250" required><br> | ||||
| 		Timed-Light-Over Presets: <input name="MN" type="number" min="0" max="250" required><br> | ||||
| 		<h3>Button actions</h3> | ||||
| 		<table style="margin: 0 auto;" id="macros"> | ||||
| 			<thead> | ||||
| 				<tr> | ||||
| 					<td>push<br>switch</td> | ||||
| 					<td>short<br>on->off</td> | ||||
| 					<td>long<br>off->on</td> | ||||
| 					<td>double<br>N/A</td> | ||||
| 				</tr> | ||||
| 			</thead> | ||||
| 			<tbody> | ||||
| 			</tbody> | ||||
| 		</table> | ||||
| 		<a href="https://github.com/Aircoookie/WLED/wiki/Macros#analog-button" target="_blank">Analog Button setup</a> | ||||
| 		<h3>Time-controlled presets</h3> | ||||
|     <div style="display: inline-block"> | ||||
|     <table id="TMT"> | ||||
|   | ||||
| @@ -182,9 +182,7 @@ | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		function GetV() | ||||
| 		{ | ||||
| 		} | ||||
| 		function GetV(){var d=document;} | ||||
| 	</script> | ||||
| 	<style> | ||||
| 		@import url("style.css"); | ||||
|   | ||||
							
								
								
									
										155
									
								
								wled00/data/settings_um.htm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								wled00/data/settings_um.htm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head lang="en"> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=500"> | ||||
|     <title>Usermod Settings</title> | ||||
|     <script> | ||||
|     var d = document; | ||||
|     var umCfg = {}; | ||||
|     var pins = [6,7,8,9,10,11]; | ||||
|     var pinO = ["rsvd","rsvd","rsvd","rsvd","rsvd","rsvd"], owner; | ||||
|     var loc = false, locip; | ||||
|     var urows; | ||||
|     var numM = 0; | ||||
|     function gId(s) { return d.getElementById(s); } | ||||
|     function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); } | ||||
|     function H() { window.open("https://github.com/Aircoookie/WLED/wiki/Settings#usermod-settings"); } | ||||
|     function B() { window.open("/settings","_self"); } | ||||
|     function S() { | ||||
|         if (window.location.protocol == "file:") { | ||||
|             loc = true; | ||||
|             locip = localStorage.getItem('locIp'); | ||||
|             if (!locip) { | ||||
|                 locip = prompt("File Mode. Please enter WLED IP!"); | ||||
|                 localStorage.setItem('locIp', locip); | ||||
|             } | ||||
|         } | ||||
|         GetV(); | ||||
|         if (numM > 0 || locip) ldS(); | ||||
|         else gId("um").innerHTML = "No Usermods installed."; | ||||
|     } | ||||
|     // https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer | ||||
|     function isF(n) { return n === +n && n !== (n|0); } | ||||
|     function isI(n) { return n === +n && n === (n|0); } | ||||
|     function check(o,k) {   // input object, pin owner key | ||||
|         var n = o.name.replace("[]","").substr(-3); | ||||
|         if (o.type=="number" && n.substr(0,3)=="pin") { | ||||
|             for (var i=0; i<pins.length; i++) { | ||||
|                 if (k==pinO[i]) continue; | ||||
|                 if (o.value==pins[i] || o.value<-1 || o.value>39) { o.style.color="red"; break; } else o.style.color=o.value>33?"orange":"#fff"; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     function getPins(o) { | ||||
|         if (isO(o)) { | ||||
|             for (const [k,v] of Object.entries(o)) { | ||||
|                 if (isO(v)) { | ||||
|                     owner = k; | ||||
|                     getPins(v); | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (k.replace("[]","").substr(-3)=="pin") { | ||||
|                     if (Array.isArray(v)) { | ||||
|                         for (var i=0; i<v.length; i++) if (v[i]>=0) { pins.push(v[i]); pinO.push(owner); } | ||||
|                     } else { | ||||
|                         if (v>=0) { pins.push(v); pinO.push(owner); } | ||||
|                     } | ||||
|                 } else if (Array.isArray(v)) { | ||||
|                     for (var i=0; i<v.length; i++) getPins(v[i]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     function addField(k,f,o,a=false) {  //key, field, (sub)object, isArray | ||||
|         if (isO(o)) { | ||||
|             for (const [s,v] of Object.entries(o)) { | ||||
|                 // possibility to nest objects (only 1 level) | ||||
|                 if (f!=='unknown' && !k.includes(":")) addField(k+":"+f,s,v); | ||||
|                 else addField(k,s,v); | ||||
|             } | ||||
|         } else if (Array.isArray(o)) { | ||||
|             for (var j=0; j<o.length; j++) { | ||||
|                 addField(k,f,o[j],true); | ||||
|             } | ||||
|         } else { | ||||
|             var c, t = typeof o; | ||||
|             switch (t) { | ||||
|                 case "boolean": | ||||
|                     t = "checkbox"; c = 'value="true"' + (o ? ' checked' : ''); | ||||
|                     break; | ||||
|                 case "number": | ||||
|                     c = `value="${o}"`; | ||||
|                     if (f.substr(-3)==="pin") { | ||||
|                         c += ' max="39" min="-1" style="width:40px;"'; | ||||
|                         t = "int"; | ||||
|                     } else { | ||||
|                         c += ' step="0.00001" style="width:80px;"'; | ||||
|                     } | ||||
|                     break; | ||||
|                 default: | ||||
|                     t = "text"; c = `value="${o}" style="width:250px;"`; | ||||
|                     break; | ||||
|             } | ||||
|             if (k.includes(":")) urows += k.substr(k.indexOf(":")+1); | ||||
|             urows += ` ${f}: `; | ||||
|             // https://stackoverflow.com/questions/11657123/posting-both-checked-and-unchecked-checkboxes | ||||
|             if (t=="checkbox") urows += `<input type="hidden" name="${k}:${f}${a?"[]":""}" value="false">`; | ||||
|             else if (!a)       urows += `<input type="hidden" name="${k}:${f}${a?"[]":""}" value="${t}">`; | ||||
|             urows += `<input type="${t==="int"?"number":t}" name="${k}:${f}${a?"[]":""}" ${c} oninput="check(this,'${k.substr(k.indexOf(":")+1)}')"><br>`; | ||||
|         } | ||||
|     } | ||||
|     function ldS() { | ||||
|         var url = (loc?`http://${locip}`:'') + '/cfg.json'; | ||||
|         fetch(url, { | ||||
|             method: 'get' | ||||
|         }) | ||||
|         .then(res => { | ||||
|             if (!res.ok) gId('lserr').style.display = "inline"; | ||||
|             return res.json(); | ||||
|         }) | ||||
|         .then(json => { | ||||
|             umCfg = json.um; | ||||
|             getPins(json); | ||||
|             urows=""; | ||||
|             if (isO(umCfg)) { | ||||
|                 for (const [k,o] of Object.entries(umCfg)) { | ||||
|                     urows += `<hr><h3>${k}</h3>`; | ||||
|                     addField(k,'unknown',o); | ||||
|                 } | ||||
|             } | ||||
|             if (urows==="") urows = "Usermods configuration not found.<br>Press <i>Save</i> to initialize defaults."; | ||||
|             gId("um").innerHTML = urows; | ||||
|         }) | ||||
|         .catch(function (error) { | ||||
|             gId('lserr').style.display = "inline" | ||||
|             console.log(error); | ||||
|         }); | ||||
|     } | ||||
|     function svS(e) { | ||||
|         e.preventDefault(); | ||||
|         console.log(d.Sf); | ||||
|         if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914 | ||||
|     } | ||||
|     function GetV() {} | ||||
|     </script> | ||||
|     <style> | ||||
|         @import url("style.css"); | ||||
|     </style> | ||||
| </head> | ||||
|  | ||||
| <body onload="S()"> | ||||
| 	<form id="form_s" name="Sf" method="post" onsubmit="svS(event)"> | ||||
| 		<div class="toprow"> | ||||
| 		<div class="helpB"><button type="button" onclick="H()">?</button></div> | ||||
| 		<button type="button" onclick="B()">Back</button><button type="submit">Save</button><br> | ||||
| 		<span id="lssuc" style="color:green; display:none">✔ Configuration saved!</span> | ||||
| 		<span id="lserr" style="color:red; display:none">⚠ Could not load configuration.</span><hr> | ||||
| 		</div> | ||||
| 		<h2>Usermod Setup</h2> | ||||
|         <div id="um">Loading settings...</div> | ||||
| 		<hr><button type="button" onclick="B()">Back</button><button type="submit">Save</button> | ||||
| 	</form> | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
| @@ -69,7 +69,9 @@ | ||||
| 		<select name="ETH"> | ||||
| 		<option value="0">None</option> | ||||
|     <option value="2">ESP32-POE</option> | ||||
|     <option value="6">ESP32Deux</option> | ||||
|     <option value="4">QuinLED-ESP32</option> | ||||
|     <option value="5">TwilightLord-ESP32</option> | ||||
|     <option value="3">WESP32</option> | ||||
| 		<option value="1">WT32-ETH01</option> | ||||
|     </select><br><br></div> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ body { | ||||
|   text-align: center; | ||||
|   background: #222; | ||||
|   color: #fff; | ||||
|   line-height: 200%; | ||||
|   line-height: 200%%; /* %% because of AsyncWebServer */ | ||||
|   margin: 0; | ||||
| } | ||||
| hr { | ||||
| @@ -18,6 +18,7 @@ button { | ||||
|   font-size: 20px; | ||||
|   margin: 8px; | ||||
|   margin-top: 12px; | ||||
|   cursor: pointer; | ||||
| } | ||||
| .helpB { | ||||
|   text-align: left; | ||||
| @@ -32,6 +33,23 @@ input { | ||||
| } | ||||
| input[type="number"] { | ||||
|   width: 4em; | ||||
|   margin: 2px; | ||||
| } | ||||
| input[type="number"].xxl { | ||||
|   width: 100px; | ||||
| } | ||||
| input[type="number"].big { | ||||
|   width: 85px; | ||||
| } | ||||
| input[type="number"].med { | ||||
|   width: 55px; | ||||
| } | ||||
| input[type="number"].small { | ||||
|   width: 40px; | ||||
| } | ||||
| input[type="checkbox"] { | ||||
|   transform: scale(1.5); | ||||
|   margin-right: 10px; | ||||
| } | ||||
| select { | ||||
|   background: #333; | ||||
|   | ||||
| @@ -43,7 +43,7 @@ | ||||
|       Installed version: ##VERSION##<br> | ||||
|       Download the latest binary: <a href="https://github.com/Aircoookie/WLED/releases" target="_blank"> | ||||
|       <img src="https://img.shields.io/github/release/Aircoookie/WLED.svg?style=flat-square"></a><br> | ||||
|       <input type='file' class="bt" name='update' accept=".bin" required><br> | ||||
|       <input type='file' class="bt" name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app--> | ||||
|       <input type='submit' class="bt" value='Update!' ><br> | ||||
|       <button type="button" class="bt" onclick="B()">Back</button></form> | ||||
|     <div id="msg"><b>Updating...</b><br>Please do not close or refresh the page :)</div> | ||||
|   | ||||
| @@ -27,15 +27,14 @@ | ||||
|       color: white; | ||||
|       border: 0px solid white; | ||||
|       border-radius: 25px; | ||||
|       filter: drop-shadow(0px 0px 1px #000); | ||||
|     } | ||||
|  | ||||
|     img { | ||||
|       width: 999px; | ||||
|       max-width: 85%; | ||||
|       width: 950px; | ||||
|       max-width: 82%; | ||||
|       image-rendering: pixelated; | ||||
|       image-rendering: crisp-edges; | ||||
|       margin: 25px 0 -10px 0; | ||||
|       margin: 4vh 0 0 0; | ||||
|       animation: fi 1s; | ||||
|     } | ||||
|      | ||||
| @@ -50,7 +49,7 @@ | ||||
| 	</style> | ||||
| </head> | ||||
| <body> | ||||
| <img alt="" src=" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAAjCAYAAACjOOUsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsAAAA7AAWrWiQkAAATfSURBVGhD7ZlPaBVHHMdnS2Ox5iKYGIyYJwQlUTHG6kUwtSAIBsEiBopgKR5i68mbIPgEwZun0uYgouIlobQIKQheIsSLaBSaRFoEc3gtMa9HLaiH7X4n81tn583u/HkTaZ/7gR+zMzvv93bfZ3/777GSkpL/AJFo3Tg/H7OuPtGxY7CTsZnjkd/3lRTi/qMKgfGXt8TAO6KfT2TG1f7u6ROlyBXgI9HaQRW4+JR3ox92Z1p1XO3PLCUVORHHvFMSDDeJBuJvH4mlZdS+C3c7B+Ln3fvivw59w0MMOzP/FYvj75K4PMhDDDszdbM7rk1V4hcPD/MQw858MbEnPvJgf3x29hQPMdwUbqc2qRIhCBVGrQk+Lzm9mq6NkIe2t20N7xOf7NjK2w13rlltM+Sh7VvLu+/YOMib6NyMVR7IQ9u7qY33ibb2bbxd/9mvVnkgD217ZTXvE72fbuHtle1X3VxIuH1QOZ2mmG5yaH4yr0giBKryVCDTJBICG+SpJDJNIiFQlacCmSaREKjKU4FMX5F+p1NIk8OExTwbgeD1b7+zotOrlUBQm2FFp1cbgeDtyzlWdHq1EQie/fMH8z29Br0myiwdr7PkWpSGC2se/8IqtWnRa+y7Ur+Q3FF97/X7ZFjV/4B1Dz1Pw5WrlZ/Y7T330gjFikkE9Xo9jSJ0Vah+Ru7nVWNRFWq3Iacai6qQ9kfOl1eNuirUfZ7wrcbgEqkCOyc6MmEDVdyrXUd5ENR3rUiqwI6LnTx8oQp8M783E66cWjiWCarMZlnRSix5PwSTSBXY0WFXdf9HfK+FKlSBoX6rjEQ8XMutD3nn+1Yg5L755Mrzk0qkh2xMwLOYr0iXa6CJZu9KQ+N7LSSoAuma6EKRn1TiwaUn0bO3r/gy7v42/3nf+cEzxFGq5jD1TYTYJhAqj4pt3iI/mdMpTbQRqD4HhqhA9S40r28L3ZWmz4kUjlAFNvucSNBzomtF5vlJJVarVb53mMgHLMBRRBEaNWez3xFqW5vN0+znyQ/5Ahlho6OjcVdXl+jxiVmhHn8Gq+jendq+cpPRvUO1fuUmo3mHavvKTUb3DtX2lZtM3jtUWRqQ3TRMBrLMjMgVkAiB2waX/1nwgURygTv885BICOzrH+BjPpBICBzo38nHfJBFksDFxUU2NjaWjhMNAwSJ1Ems/n2RDQ0NsQNzn4sVjF1vvyGWGOvp6cmsk+frJLpWISFXo1cVElI1+lQhIVejTxUSajVCYp5AoB0k6AhIRQqJo7OnGRe87gIfxl9N1Y8nlpcTKpUK+/rlSdFLDghpft5fUTho0GJD5WW0RetUWi1PgwMNmbtTFdhXaRBowdj2H3kuVGQeyIkg5GVQtE6mFfPoPMgUSqSjgo4Gdqk/whfxpGeSaqJIxjFGsbCwkF2fBHJh3ZH5RpE2RxtBedJtkmi1PLSMdXwgh0KJAIkBEiLQV5MODw9ndiBvp/I2VIwZd5T4EPJQLvRNGCVSYgr0xSotIyMjYkkPfV7eUFNOHa2cx+X3BkaJAIkoxJAWCBwfHxc9PdhBbFzSRgixzHfahVbNA2x/b8JKoi0QaKpE7KC8cVjGmOha06p5fAgq0aYSS8ITROLk5GRUq9UyAt/XUVgSGNyl0kNrMyBHchCUeSwJejotKSkp+VBh7F9fIy7OdlOyMwAAAABJRU5ErkJggg=="> | ||||
| <img alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG0AAAAfCAMAAADazLOuAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABLUExURQAAAAB81gCU/zKq///mo7sWMN8bO+ZIYtZaAP9rAP+HMsCiG+TAIOnMS0KqNU7KPnLUZOrq6v///4CAgGhoaL+/v6CgoExMTAAAAAlm4O8AAAAZdFJOU////////////////////////////////wABNAq3AAAACXBIWXMAAA7DAAAOwwHHb6hkAAACN0lEQVRIS73VjVLCMBAEYIr8CYKkrdj3f1J37zaXFCpTO+piaDgbPq9px9VQ0qyrvKj4q6m0Zr1h+M7xF1zRmnWzqV9/0d2jttGotO1uv9dUObwej5oqp7fzWVPl8n69aprzoOUUbbvdIbV3OLwitXc6vSG1d7m8I3feSEN0j2CeNbOY4MxigjOLCc4sZsTV2l1cCyy4wIILLLjAxtykltq2rbTU+qi01N5rXNO2leaFORoija2l5MM5a02ac9Ya16Sk5tgaPrUpjZub0BL6YqSxKwbH77XUUmSkJXSl8QtaMuyJhq5maL5nTKVpZC13VmtMpTFT2g4vJjTuGfMzzXftiUZnhdtgb1xofvypRon5TjNnxYN9zJo6K5ruSIzQtGuVZn0x91rKvdHBvm39E7SyZ4y06Gz8BDBFKzsXmhcwyfsGZ9VpbhoiCinaxPNmGWmWWrNU2jB0q6HvOhN1JUtCixQtp2g51ZVUXIPS2RMAD++T2nY/DrDjOMDO4wC7jmNYj3d73nrXug8Yt9uNB8xNU1cKNXWlUFNXCjV1pZhGTE83m2vWfYf/NGj4Bg1zu5JD3/MnH5ZWfLOksbmGWGjgXMN5/C2GXYGFFW9Nmtle6Xut0Gm+JsayCj8z0nhjGvYJzVf4aSzmNYsr+u7Q2JIdoX3YOQjOslmsW1jJ3120nE9gfo79hTaNdcsqVR610lvO47pllae9ReZ805zKo2a3iaY5c75pTmVCA6dJ5H7N0sr/asPwBehb7ifEhusRAAAAAElFTkSuQmCC"> | ||||
| <div class="main"> | ||||
| <h1>Welcome to WLED!</h1> | ||||
| <h3>Thank you for installing my application!</h3> | ||||
|   | ||||
| @@ -20,13 +20,14 @@ void handleBlynk(); | ||||
| void updateBlynk(); | ||||
|  | ||||
| //button.cpp | ||||
| void shortPressAction(); | ||||
| bool isButtonPressed(); | ||||
| void shortPressAction(uint8_t b=0); | ||||
| bool isButtonPressed(uint8_t b=0); | ||||
| void handleButton(); | ||||
| void handleIO(); | ||||
|  | ||||
| //cfg.cpp | ||||
| void deserializeConfig(); | ||||
| bool deserializeConfig(JsonObject doc, bool fromFS = false); | ||||
| void deserializeConfigFromFS(); | ||||
| bool deserializeConfigSec(); | ||||
| void serializeConfig(); | ||||
| void serializeConfigSec(); | ||||
| @@ -96,8 +97,8 @@ void handleIR(); | ||||
| #include "src/dependencies/json/AsyncJson-v6.h" | ||||
| #include "FX.h" | ||||
|  | ||||
| void deserializeSegment(JsonObject elem, byte it); | ||||
| bool deserializeState(JsonObject root); | ||||
| void deserializeSegment(JsonObject elem, byte it, byte presetId = 0); | ||||
| bool deserializeState(JsonObject root, byte presetId = 0); | ||||
| void serializeSegment(JsonObject& root, WS2812FX::Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); | ||||
| void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true); | ||||
| void serializeInfo(JsonObject root); | ||||
| @@ -126,6 +127,7 @@ bool initMqtt(); | ||||
| void publishMqtt(); | ||||
|  | ||||
| //ntp.cpp | ||||
| void handleTime(); | ||||
| void handleNetworkTime(); | ||||
| void sendNTPPacket(); | ||||
| bool checkNTPResponse();     | ||||
| @@ -135,6 +137,8 @@ bool checkCountdown(); | ||||
| void setCountdown(); | ||||
| byte weekdayMondayFirst(); | ||||
| void checkTimers(); | ||||
| void calculateSunriseAndSunset(); | ||||
| void setTimeFromAPI(uint32_t timein); | ||||
|  | ||||
| //overlay.cpp | ||||
| void initCronixie(); | ||||
| @@ -149,8 +153,9 @@ void _overlayCronixie(); | ||||
| void _drawOverlayCronixie(); | ||||
|  | ||||
| //playlist.cpp | ||||
| void shufflePlaylist(); | ||||
| void unloadPlaylist(); | ||||
| void loadPlaylist(JsonObject playlistObject); | ||||
| void loadPlaylist(JsonObject playlistObject, byte presetId = 0); | ||||
| void handlePlaylist(); | ||||
|  | ||||
| //presets.cpp | ||||
| @@ -184,7 +189,9 @@ class Usermod { | ||||
|     virtual void addToJsonInfo(JsonObject& obj) {} | ||||
|     virtual void readFromJsonState(JsonObject& obj) {} | ||||
|     virtual void addToConfig(JsonObject& obj) {} | ||||
|     virtual void readFromConfig(JsonObject& obj) {} | ||||
|     virtual bool readFromConfig(JsonObject& obj) { return true; } //Heads up! readFromConfig() now needs to return a bool | ||||
|     virtual void onMqttConnect(bool sessionPresent) {} | ||||
|     virtual bool onMqttMessage(char* topic, char* payload) { return false; } | ||||
|     virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} | ||||
| }; | ||||
|  | ||||
| @@ -204,8 +211,9 @@ class UsermodManager { | ||||
|     void readFromJsonState(JsonObject& obj); | ||||
|  | ||||
|     void addToConfig(JsonObject& obj); | ||||
|     void readFromConfig(JsonObject& obj); | ||||
|  | ||||
|     bool readFromConfig(JsonObject& obj); | ||||
|     void onMqttConnect(bool sessionPresent); | ||||
|     bool onMqttMessage(char* topic, char* payload); | ||||
|     bool add(Usermod* um); | ||||
|     Usermod* lookup(uint16_t mod_id); | ||||
|     byte getModCount(); | ||||
|   | ||||
| @@ -55,13 +55,12 @@ bool bufferedFind(const char *target, bool fromStart = true) { | ||||
|   size_t targetLen = strlen(target); | ||||
|  | ||||
|   size_t index = 0; | ||||
|   uint16_t bufsize = 0, count = 0; | ||||
|   byte buf[FS_BUFSIZE]; | ||||
|   if (fromStart) f.seek(0); | ||||
|  | ||||
|   while (f.position() < f.size() -1) { | ||||
|     bufsize = f.read(buf, FS_BUFSIZE); | ||||
|     count = 0; | ||||
|     uint16_t bufsize = f.read(buf, FS_BUFSIZE); | ||||
|     uint16_t count = 0; | ||||
|     while (count < bufsize) { | ||||
|       if(buf[count] != target[index]) | ||||
|       index = 0; // reset index if any char does not match | ||||
| @@ -97,13 +96,12 @@ bool bufferedFindSpace(uint16_t targetLen, bool fromStart = true) { | ||||
|   if (!f || !f.size()) return false; | ||||
|  | ||||
|   uint16_t index = 0; | ||||
|   uint16_t bufsize = 0, count = 0; | ||||
|   byte buf[FS_BUFSIZE]; | ||||
|   if (fromStart) f.seek(0); | ||||
|  | ||||
|   while (f.position() < f.size() -1) { | ||||
|     bufsize = f.read(buf, FS_BUFSIZE); | ||||
|     count = 0; | ||||
|     uint16_t bufsize = f.read(buf, FS_BUFSIZE); | ||||
|     uint16_t count = 0; | ||||
|      | ||||
|     while (count < bufsize) { | ||||
|       if(buf[count] == ' ') { | ||||
| @@ -140,13 +138,12 @@ bool bufferedFindObjectEnd() { | ||||
|   if (!f || !f.size()) return false; | ||||
|  | ||||
|   uint16_t objDepth = 0; //num of '{' minus num of '}'. return once 0 | ||||
|   uint16_t bufsize = 0, count = 0; | ||||
|   //size_t start = f.position(); | ||||
|   byte buf[FS_BUFSIZE]; | ||||
|  | ||||
|   while (f.position() < f.size() -1) { | ||||
|     bufsize = f.read(buf, FS_BUFSIZE); | ||||
|     count = 0; | ||||
|     uint16_t bufsize = f.read(buf, FS_BUFSIZE); | ||||
|     uint16_t count = 0; | ||||
|      | ||||
|     while (count < bufsize) { | ||||
|       if (buf[count] == '{') objDepth++; | ||||
|   | ||||
| @@ -42,22 +42,22 @@ function B(){window.history.back()}function U(){document.getElementById("uf").st | ||||
| .bt{background:#333;color:#fff;font-family:Verdana,sans-serif;border:.3ch solid #333;display:inline-block;font-size:20px;margin:8px;margin-top:12px}input[type=file]{font-size:16px}body{font-family:Verdana,sans-serif;text-align:center;background:#222;color:#fff;line-height:200%}#msg{display:none} | ||||
| </style></head><body><h2>WLED Software Update</h2><form method="POST"  | ||||
| action="/update" id="uf" enctype="multipart/form-data" onsubmit="U()"> | ||||
| Installed version: 0.12.0-b5<br>Download the latest binary: <a  | ||||
| Installed version: 0.13.0-b0<br>Download the latest binary: <a  | ||||
| href="https://github.com/Aircoookie/WLED/releases" target="_blank"><img  | ||||
| src="https://img.shields.io/github/release/Aircoookie/WLED.svg?style=flat-square"> | ||||
| </a><br><input type="file" class="bt" name="update" accept=".bin" required><br> | ||||
| <input type="submit" class="bt" value="Update!"><br><button type="button"  | ||||
| class="bt" onclick="B()">Back</button></form><div id="msg"><b>Updating...</b> | ||||
| <br>Please do not close or refresh the page :)</div></body></html>)====="; | ||||
| </a><br><input type="file" class="bt" name="update" required><br><input  | ||||
| type="submit" class="bt" value="Update!"><br><button type="button" class="bt"  | ||||
| onclick="B()">Back</button></form><div id="msg"><b>Updating...</b><br> | ||||
| Please do not close or refresh the page :)</div></body></html>)====="; | ||||
|  | ||||
|  | ||||
| // Autogenerated from wled00/data/welcome.htm, do not edit!! | ||||
| const char PAGE_welcome[] PROGMEM = R"=====(<!DOCTYPE html><html><head><meta charset="utf-8"><meta  | ||||
| content="width=device-width" name="viewport"><meta name="theme-color"  | ||||
| content="#222222"><title>Welcome!</title><style> | ||||
| body{font-family:Verdana,Helvetica,sans-serif;text-align:center;background-color:#222;margin:0;color:#fff}button{outline:0;cursor:pointer;padding:8px;margin:10px;width:230px;text-transform:uppercase;font-family:helvetica;font-size:19px;background-color:#333;color:#fff;border:0 solid #fff;border-radius:25px;filter:drop-shadow(0 0 1px #000)}img{width:999px;max-width:85%;image-rendering:pixelated;image-rendering:crisp-edges;margin:25px 0 -10px 0;animation:fi 1s}@keyframes fi{from{opacity:0}to{opacity:1}}.main{animation:fi 1.5s .7s both} | ||||
| body{font-family:Verdana,Helvetica,sans-serif;text-align:center;background-color:#222;margin:0;color:#fff}button{outline:0;cursor:pointer;padding:8px;margin:10px;width:230px;text-transform:uppercase;font-family:helvetica;font-size:19px;background-color:#333;color:#fff;border:0 solid #fff;border-radius:25px}img{width:950px;max-width:82%;image-rendering:pixelated;image-rendering:crisp-edges;margin:4vh 0 0 0;animation:fi 1s}@keyframes fi{from{opacity:0}to{opacity:1}}.main{animation:fi 1.5s .7s both} | ||||
| </style></head><body><img alt=""  | ||||
| src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAAjCAYAAACjOOUsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsAAAA7AAWrWiQkAAATfSURBVGhD7ZlPaBVHHMdnS2Ox5iKYGIyYJwQlUTHG6kUwtSAIBsEiBopgKR5i68mbIPgEwZun0uYgouIlobQIKQheIsSLaBSaRFoEc3gtMa9HLaiH7X4n81tn583u/HkTaZ/7gR+zMzvv93bfZ3/777GSkpL/AJFo3Tg/H7OuPtGxY7CTsZnjkd/3lRTi/qMKgfGXt8TAO6KfT2TG1f7u6ROlyBXgI9HaQRW4+JR3ox92Z1p1XO3PLCUVORHHvFMSDDeJBuJvH4mlZdS+C3c7B+Ln3fvivw59w0MMOzP/FYvj75K4PMhDDDszdbM7rk1V4hcPD/MQw858MbEnPvJgf3x29hQPMdwUbqc2qRIhCBVGrQk+Lzm9mq6NkIe2t20N7xOf7NjK2w13rlltM+Sh7VvLu+/YOMib6NyMVR7IQ9u7qY33ibb2bbxd/9mvVnkgD217ZTXvE72fbuHtle1X3VxIuH1QOZ2mmG5yaH4yr0giBKryVCDTJBICG+SpJDJNIiFQlacCmSaREKjKU4FMX5F+p1NIk8OExTwbgeD1b7+zotOrlUBQm2FFp1cbgeDtyzlWdHq1EQie/fMH8z29Br0myiwdr7PkWpSGC2se/8IqtWnRa+y7Ur+Q3FF97/X7ZFjV/4B1Dz1Pw5WrlZ/Y7T330gjFikkE9Xo9jSJ0Vah+Ru7nVWNRFWq3Iacai6qQ9kfOl1eNuirUfZ7wrcbgEqkCOyc6MmEDVdyrXUd5ENR3rUiqwI6LnTx8oQp8M783E66cWjiWCarMZlnRSix5PwSTSBXY0WFXdf9HfK+FKlSBoX6rjEQ8XMutD3nn+1Yg5L755Mrzk0qkh2xMwLOYr0iXa6CJZu9KQ+N7LSSoAuma6EKRn1TiwaUn0bO3r/gy7v42/3nf+cEzxFGq5jD1TYTYJhAqj4pt3iI/mdMpTbQRqD4HhqhA9S40r28L3ZWmz4kUjlAFNvucSNBzomtF5vlJJVarVb53mMgHLMBRRBEaNWez3xFqW5vN0+znyQ/5Ahlho6OjcVdXl+jxiVmhHn8Gq+jendq+cpPRvUO1fuUmo3mHavvKTUb3DtX2lZtM3jtUWRqQ3TRMBrLMjMgVkAiB2waX/1nwgURygTv885BICOzrH+BjPpBICBzo38nHfJBFksDFxUU2NjaWjhMNAwSJ1Ems/n2RDQ0NsQNzn4sVjF1vvyGWGOvp6cmsk+frJLpWISFXo1cVElI1+lQhIVejTxUSajVCYp5AoB0k6AhIRQqJo7OnGRe87gIfxl9N1Y8nlpcTKpUK+/rlSdFLDghpft5fUTho0GJD5WW0RetUWi1PgwMNmbtTFdhXaRBowdj2H3kuVGQeyIkg5GVQtE6mFfPoPMgUSqSjgo4Gdqk/whfxpGeSaqJIxjFGsbCwkF2fBHJh3ZH5RpE2RxtBedJtkmi1PLSMdXwgh0KJAIkBEiLQV5MODw9ndiBvp/I2VIwZd5T4EPJQLvRNGCVSYgr0xSotIyMjYkkPfV7eUFNOHa2cx+X3BkaJAIkoxJAWCBwfHxc9PdhBbFzSRgixzHfahVbNA2x/b8JKoi0QaKpE7KC8cVjGmOha06p5fAgq0aYSS8ITROLk5GRUq9UyAt/XUVgSGNyl0kNrMyBHchCUeSwJejotKSkp+VBh7F9fIy7OdlOyMwAAAABJRU5ErkJggg=="> | ||||
| src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG0AAAAfCAMAAADazLOuAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABLUExURQAAAAB81gCU/zKq///mo7sWMN8bO+ZIYtZaAP9rAP+HMsCiG+TAIOnMS0KqNU7KPnLUZOrq6v///4CAgGhoaL+/v6CgoExMTAAAAAlm4O8AAAAZdFJOU////////////////////////////////wABNAq3AAAACXBIWXMAAA7DAAAOwwHHb6hkAAACN0lEQVRIS73VjVLCMBAEYIr8CYKkrdj3f1J37zaXFCpTO+piaDgbPq9px9VQ0qyrvKj4q6m0Zr1h+M7xF1zRmnWzqV9/0d2jttGotO1uv9dUObwej5oqp7fzWVPl8n69aprzoOUUbbvdIbV3OLwitXc6vSG1d7m8I3feSEN0j2CeNbOY4MxigjOLCc4sZsTV2l1cCyy4wIILLLjAxtykltq2rbTU+qi01N5rXNO2leaFORoija2l5MM5a02ac9Ya16Sk5tgaPrUpjZub0BL6YqSxKwbH77XUUmSkJXSl8QtaMuyJhq5maL5nTKVpZC13VmtMpTFT2g4vJjTuGfMzzXftiUZnhdtgb1xofvypRon5TjNnxYN9zJo6K5ruSIzQtGuVZn0x91rKvdHBvm39E7SyZ4y06Gz8BDBFKzsXmhcwyfsGZ9VpbhoiCinaxPNmGWmWWrNU2jB0q6HvOhN1JUtCixQtp2g51ZVUXIPS2RMAD++T2nY/DrDjOMDO4wC7jmNYj3d73nrXug8Yt9uNB8xNU1cKNXWlUFNXCjV1pZhGTE83m2vWfYf/NGj4Bg1zu5JD3/MnH5ZWfLOksbmGWGjgXMN5/C2GXYGFFW9Nmtle6Xut0Gm+JsayCj8z0nhjGvYJzVf4aSzmNYsr+u7Q2JIdoX3YOQjOslmsW1jJ3120nE9gfo79hTaNdcsqVR610lvO47pllae9ReZ805zKo2a3iaY5c75pTmVCA6dJ5H7N0sr/asPwBehb7ifEhusRAAAAAElFTkSuQmCC"> | ||||
| <div class="main"><h1>Welcome to WLED!</h1><h3> | ||||
| Thank you for installing my application!</h3><b>Next steps:</b><br><br> | ||||
| Connect the module to your local WiFi here!<br><button  | ||||
| @@ -95,7 +95,7 @@ content="width=device-width" name="viewport"><meta name="theme-color" | ||||
| content="#222222"><title>Not found</title><style> | ||||
| body{font-family:Verdana,Helvetica,sans-serif;text-align:center;background-color:#222;margin:0;color:#fff}img{width:400px;max-width:50%;image-rendering:pixelated;image-rendering:crisp-edges;margin:25px 0 -10px 0}button{outline:0;cursor:pointer;padding:8px;margin:10px;width:230px;text-transform:uppercase;font-family:helvetica;font-size:19px;background-color:#333;color:#fff;border:0 solid #fff;border-radius:25px} | ||||
| </style></head><body><img alt=""  | ||||
| src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsEAAA7BAbiRa+0AAAFMSURBVFhH7ZTfbYNADMaPKDNUrJA85CEjdIjOgNQV+sASlZgmI/AIK6AuQfngnDrmjtpHItQ/P+l0juHsz2cH9+fJ/G7nreldfnDnp+ln/ZIlxbIfQmIwJOekCrEJ8FUvASEWEXoBiuSERcTO75uhuwFWff86bi57n3ZC+rW3YLqB5rn11ldCEPNr2LwFJgHHy8G1bTsu3oKYX4N5BrQ8ZAYewSoBGDjr0ElWCUC/rT2X7MqynL7tG4Dc45BwEYM9H5w7DqHMdfNCURR9nue3Iobk55MtOYeLoOQ8vmoG6o+0FaLrOm9FwC3wayLgx5I2WHpGIGYorulfgPYQ3AZLz4hQ9TMBVVVleJGrRUWz2YgQOg8bPjzzrit7vwcRQb5NTiARRPPzMYItoCpoWZITMkao+mRkddpqQ6z6FN+DfwFJrOm55GfewC/CuU/E4tQYg7BPYQAAAABJRU5ErkJggg=="> | ||||
| src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAbUExURQAAAAB81gCU/zKq/////9bW1oCAgGhoaAAAAGPLX8AAAAAJdFJOU///////////AFNPeBIAAAAJcEhZcwAADsAAAA7AAWrWiQkAAACdSURBVDhPxc9bDoUgEANQebP/FUuHMjBGY/B+3EYR7RH0qC/ZBc6HwCljgHO+xZIVSI2sYgHaG7EBWh8jWoxTrCBFdDJ+BD4lbIHxAcz8APAVLTsrZE4eQD5qzt3cAFTYokC4YCN9Gybgu4yAQtBFLQXHuHABA7JMeOEC/E0W5uy9gv4vo5QHK2i7yq2C8UABM4HmL+CSTXCTF1DrCX6+Gp9zB5dsAAAAAElFTkSuQmCC"> | ||||
| <h1>404 Not Found</h1><b>Akemi does not know where you are headed...</b><br><br> | ||||
| <button onclick='window.location.href="/sliders"'>Back to controls</button> | ||||
| </body></html>)====="; | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										4242
									
								
								wled00/html_ui.h
									
									
									
									
									
								
							
							
						
						
									
										4242
									
								
								wled00/html_ui.h
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -42,7 +42,7 @@ const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t); | ||||
| void incBrightness() | ||||
| { | ||||
|   // dumb incremental search is efficient enough for so few items | ||||
|   for (int index = 0; index < numBrightnessSteps; ++index) | ||||
|   for (uint8_t index = 0; index < numBrightnessSteps; ++index) | ||||
|   { | ||||
|     if (brightnessSteps[index] > bri) | ||||
|     { | ||||
| @@ -227,7 +227,7 @@ void decodeIR24(uint32_t code) | ||||
|   switch (code) { | ||||
|     case IR24_BRIGHTER  : incBrightness();                  break; | ||||
|     case IR24_DARKER    : decBrightness();                  break; | ||||
|     case IR24_OFF       : briLast = bri; bri = 0;           break; | ||||
|     case IR24_OFF    : if (bri > 0) briLast = bri; bri = 0; break; | ||||
|     case IR24_ON        : bri = briLast;                    break; | ||||
|     case IR24_RED       : colorFromUint32(COLOR_RED);       break; | ||||
|     case IR24_REDDISH   : colorFromUint32(COLOR_REDDISH);   break; | ||||
| @@ -259,7 +259,7 @@ void decodeIR24OLD(uint32_t code) | ||||
|   switch (code) { | ||||
|     case IR24_OLD_BRIGHTER  : incBrightness();                     break; | ||||
|     case IR24_OLD_DARKER    : decBrightness();                     break; | ||||
|     case IR24_OLD_OFF       : briLast = bri; bri = 0;              break; | ||||
|     case IR24_OLD_OFF       : if (bri > 0) briLast = bri; bri = 0; break; | ||||
|     case IR24_OLD_ON        : bri = briLast;                       break; | ||||
|     case IR24_OLD_RED       : colorFromUint32(COLOR_RED);          break; | ||||
|     case IR24_OLD_REDDISH   : colorFromUint32(COLOR_REDDISH);      break; | ||||
| @@ -292,7 +292,7 @@ void decodeIR24CT(uint32_t code) | ||||
|   switch (code) { | ||||
|     case IR24_CT_BRIGHTER   : incBrightness();                     break; | ||||
|     case IR24_CT_DARKER     : decBrightness();                     break; | ||||
|     case IR24_CT_OFF        : briLast = bri; bri = 0;              break; | ||||
|     case IR24_CT_OFF        : if (bri > 0) briLast = bri; bri = 0; break; | ||||
|     case IR24_CT_ON         : bri = briLast;                       break; | ||||
|     case IR24_CT_RED        : colorFromUint32(COLOR_RED);          break; | ||||
|     case IR24_CT_REDDISH    : colorFromUint32(COLOR_REDDISH);      break; | ||||
| @@ -327,7 +327,7 @@ void decodeIR40(uint32_t code) | ||||
|   switch (code) { | ||||
|     case IR40_BPLUS        : incBrightness();                                            break; | ||||
|     case IR40_BMINUS       : decBrightness();                                            break; | ||||
|     case IR40_OFF          : briLast = bri; bri = 0;                                     break; | ||||
|     case IR40_OFF          : if (bri > 0) briLast = bri; bri = 0;                        break; | ||||
|     case IR40_ON           : bri = briLast;                                              break; | ||||
|     case IR40_RED          : colorFromUint24(COLOR_RED);                                 break; | ||||
|     case IR40_REDDISH      : colorFromUint24(COLOR_REDDISH);                             break; | ||||
| @@ -384,7 +384,7 @@ void decodeIR44(uint32_t code) | ||||
|   switch (code) { | ||||
|     case IR44_BPLUS       : incBrightness();                                            break; | ||||
|     case IR44_BMINUS      : decBrightness();                                            break; | ||||
|     case IR44_OFF         : briLast = bri; bri = 0;                                     break; | ||||
|     case IR44_OFF         : if (bri > 0) briLast = bri; bri = 0;                        break; | ||||
|     case IR44_ON          : bri = briLast;                                              break; | ||||
|     case IR44_RED         : colorFromUint24(COLOR_RED);                                 break; | ||||
|     case IR44_REDDISH     : colorFromUint24(COLOR_REDDISH);                             break; | ||||
| @@ -447,7 +447,7 @@ void decodeIR21(uint32_t code) | ||||
|     switch (code) { | ||||
|     case IR21_BRIGHTER:  incBrightness();                  break; | ||||
|     case IR21_DARKER:    decBrightness();                  break; | ||||
|     case IR21_OFF:       briLast = bri; bri = 0;           break; | ||||
|     case IR21_OFF:    if (bri > 0) briLast = bri; bri = 0; break; | ||||
|     case IR21_ON:        bri = briLast;                    break; | ||||
|     case IR21_RED:       colorFromUint32(COLOR_RED);       break; | ||||
|     case IR21_REDDISH:   colorFromUint32(COLOR_REDDISH);   break; | ||||
|   | ||||
							
								
								
									
										138
									
								
								wled00/json.cpp
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								wled00/json.cpp
									
									
									
									
									
								
							| @@ -6,7 +6,7 @@ | ||||
|  * JSON API (De)serialization | ||||
|  */ | ||||
|  | ||||
| void deserializeSegment(JsonObject elem, byte it) | ||||
| void deserializeSegment(JsonObject elem, byte it, byte presetId) | ||||
| { | ||||
|   byte id = elem["id"] | it; | ||||
|   if (id < strip.getMaxSegments()) | ||||
| @@ -22,6 +22,8 @@ void deserializeSegment(JsonObject elem, byte it) | ||||
|     uint16_t grp = elem[F("grp")] | seg.grouping; | ||||
|     uint16_t spc = elem[F("spc")] | seg.spacing; | ||||
|     strip.setSegment(id, start, stop, grp, spc); | ||||
|     seg.offset = elem[F("of")] | seg.offset; | ||||
|     if (stop > start && seg.offset > stop - start -1) seg.offset = stop - start -1; | ||||
|  | ||||
|     int segbri = elem["bri"] | -1; | ||||
|     if (segbri == 0) { | ||||
| @@ -95,16 +97,21 @@ void deserializeSegment(JsonObject elem, byte it) | ||||
|  | ||||
|     //temporary, strip object gets updated via colorUpdated() | ||||
|     if (id == strip.getMainSegmentId()) { | ||||
|       byte effectPrev = effectCurrent; | ||||
|       effectCurrent = elem[F("fx")] | effectCurrent; | ||||
|       if (!presetId && effectCurrent != effectPrev) unloadPlaylist(); //stop playlist if active and FX changed manually | ||||
|       effectSpeed = elem[F("sx")] | effectSpeed; | ||||
|       effectIntensity = elem[F("ix")] | effectIntensity; | ||||
|       effectPalette = elem[F("pal")] | effectPalette; | ||||
|       effectPalette = elem["pal"] | effectPalette; | ||||
|     } else { //permanent | ||||
|       byte fx = elem[F("fx")] | seg.mode; | ||||
|       if (fx != seg.mode && fx < strip.getModeCount()) strip.setMode(id, fx); | ||||
|       if (fx != seg.mode && fx < strip.getModeCount()) { | ||||
|         strip.setMode(id, fx); | ||||
|         if (!presetId) unloadPlaylist(); //stop playlist if active and FX changed manually | ||||
|       } | ||||
|       seg.speed = elem[F("sx")] | seg.speed; | ||||
|       seg.intensity = elem[F("ix")] | seg.intensity; | ||||
|       seg.palette = elem[F("pal")] | seg.palette; | ||||
|       seg.palette = elem["pal"] | seg.palette; | ||||
|     } | ||||
|  | ||||
|     JsonArray iarr = elem[F("i")]; //set individual LEDs | ||||
| @@ -134,7 +141,7 @@ void deserializeSegment(JsonObject elem, byte it) | ||||
|           if (icol.isNull()) break; | ||||
|  | ||||
|           byte sz = icol.size(); | ||||
|           if (sz == 0 && sz > 4) break; | ||||
|           if (sz == 0 || sz > 4) break; | ||||
|  | ||||
|           int rgbw[] = {0,0,0,0}; | ||||
|           copyArray(icol, rgbw); | ||||
| @@ -156,7 +163,7 @@ void deserializeSegment(JsonObject elem, byte it) | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool deserializeState(JsonObject root) | ||||
| bool deserializeState(JsonObject root, byte presetId) | ||||
| { | ||||
|   strip.applyToAllSelected = false; | ||||
|   bool stateResponse = root[F("v")] | false; | ||||
| @@ -166,12 +173,17 @@ bool deserializeState(JsonObject root) | ||||
|   bool on = root["on"] | (bri > 0); | ||||
|   if (!on != !bri) toggleOnOff(); | ||||
|  | ||||
|   int tr = root[F("transition")] | -1; | ||||
|   if (tr >= 0) | ||||
|   { | ||||
|     transitionDelay = tr; | ||||
|     transitionDelay *= 100; | ||||
|     transitionDelayTemp = transitionDelay; | ||||
|   if (root["on"].is<const char*>() && root["on"].as<const char*>()[0] == 't') toggleOnOff(); | ||||
|  | ||||
|   int tr = -1; | ||||
|   if (!presetId || currentPlaylist < 0) { //do not apply transition time from preset if playlist active, as it would override playlist transition times | ||||
|     tr = root[F("transition")] | -1; | ||||
|     if (tr >= 0) | ||||
|     { | ||||
|       transitionDelay = tr; | ||||
|       transitionDelay *= 100; | ||||
|       transitionDelayTemp = transitionDelay; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   tr = root[F("tt")] | -1; | ||||
| @@ -182,19 +194,13 @@ bool deserializeState(JsonObject root) | ||||
|     jsonTransitionOnce = true; | ||||
|   } | ||||
|   strip.setTransition(transitionDelayTemp); | ||||
|    | ||||
|   int cy = root[F("pl")] | -2; | ||||
|   if (cy > -2) presetCyclingEnabled = (cy >= 0); | ||||
|   JsonObject ccnf = root["ccnf"]; | ||||
|   presetCycleMin = ccnf[F("min")] | presetCycleMin; | ||||
|   presetCycleMax = ccnf[F("max")] | presetCycleMax; | ||||
|   tr = ccnf[F("time")] | -1; | ||||
|   if (tr >= 2) presetCycleTime = tr; | ||||
|  | ||||
|   tr = root[F("tb")] | -1; | ||||
|   if (tr >= 0) strip.timebase = ((uint32_t)tr) - millis(); | ||||
|  | ||||
|   JsonObject nl = root["nl"]; | ||||
|   nightlightActive    = nl["on"]      | nightlightActive; | ||||
|   nightlightDelayMins = nl[F("dur")]  | nightlightDelayMins; | ||||
|   nightlightMode      = nl[F("fade")] | nightlightMode; //deprecated, remove for v0.13.0 | ||||
|   nightlightMode      = nl[F("mode")] | nightlightMode; | ||||
|   nightlightTargetBri = nl[F("tbri")] | nightlightTargetBri; | ||||
|  | ||||
| @@ -203,9 +209,9 @@ bool deserializeState(JsonObject root) | ||||
|   receiveNotifications = udpn["recv"] | receiveNotifications; | ||||
|   bool noNotification  = udpn[F("nn")]; //send no notification just for this request | ||||
|  | ||||
|   unsigned long timein = root[F("time")] | UINT32_MAX; | ||||
|   unsigned long timein = root[F("time")] | UINT32_MAX; //backup time source if NTP not synced | ||||
|   if (timein != UINT32_MAX) { | ||||
|     if (millis() - ntpLastSyncTime > 50000000L) setTime(timein); | ||||
|     setTimeFromAPI(timein); | ||||
|     if (presetsModifiedTime == 0) presetsModifiedTime = timein; | ||||
|   } | ||||
|  | ||||
| @@ -240,24 +246,30 @@ bool deserializeState(JsonObject root) | ||||
|         { | ||||
|           if (lowestActive == 99) lowestActive = s; | ||||
|           if (sg.isSelected()) { | ||||
|             deserializeSegment(segVar, s); | ||||
|             deserializeSegment(segVar, s, presetId); | ||||
|             didSet = true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       if (!didSet && lowestActive < strip.getMaxSegments()) deserializeSegment(segVar, lowestActive); | ||||
|       if (!didSet && lowestActive < strip.getMaxSegments()) deserializeSegment(segVar, lowestActive, presetId); | ||||
|     } else { //set only the segment with the specified ID | ||||
|       deserializeSegment(segVar, it); | ||||
|       deserializeSegment(segVar, it, presetId); | ||||
|     } | ||||
|   } else { | ||||
|     JsonArray segs = segVar.as<JsonArray>(); | ||||
|     for (JsonObject elem : segs) | ||||
|     { | ||||
|       deserializeSegment(elem, it); | ||||
|       deserializeSegment(elem, it, presetId); | ||||
|       it++; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   #ifndef WLED_DISABLE_CRONIXIE | ||||
|     if (root["nx"].is<const char*>()) { | ||||
|       strncpy(cronixieDisplay, root["nx"], 6); | ||||
|     } | ||||
|   #endif | ||||
|  | ||||
|   usermods.readFromJsonState(root); | ||||
|  | ||||
|   int ps = root[F("psave")] | -1; | ||||
| @@ -269,7 +281,11 @@ bool deserializeState(JsonObject root) | ||||
|       deletePreset(ps); | ||||
|     } | ||||
|     ps = root["ps"] | -1; //load preset (clears state request!) | ||||
|     if (ps >= 0) {applyPreset(ps); return stateResponse;} | ||||
|     if (ps >= 0) { | ||||
|       if (!presetId) unloadPlaylist(); //stop playlist if preset changed manually | ||||
|       applyPreset(ps); | ||||
|       return stateResponse; | ||||
|     } | ||||
|  | ||||
|     //HTTP API commands | ||||
|     const char* httpwin = root["win"]; | ||||
| @@ -282,7 +298,7 @@ bool deserializeState(JsonObject root) | ||||
|  | ||||
|   JsonObject playlist = root[F("playlist")]; | ||||
|   if (!playlist.isNull()) { | ||||
|     loadPlaylist(playlist); | ||||
|     loadPlaylist(playlist, presetId); | ||||
|     noNotification = true; //do not notify both for this request and the first playlist entry | ||||
|   } | ||||
|  | ||||
| @@ -298,38 +314,41 @@ void serializeSegment(JsonObject& root, WS2812FX::Segment& seg, byte id, bool fo | ||||
|     root[F("start")] = seg.start; | ||||
|     root["stop"] = seg.stop; | ||||
|   } | ||||
| 	if (!forPreset)  root[F("len")] = seg.stop - seg.start; | ||||
| 	if (!forPreset) root[F("len")] = seg.stop - seg.start; | ||||
|   root[F("grp")] = seg.grouping; | ||||
|   root[F("spc")] = seg.spacing; | ||||
|   root[F("of")] = seg.offset; | ||||
|   root["on"] = seg.getOption(SEG_OPTION_ON); | ||||
|   byte segbri = seg.opacity; | ||||
|   root["bri"] = (segbri) ? segbri : 255; | ||||
|  | ||||
| 	JsonArray colarr = root.createNestedArray("col"); | ||||
|   char colstr[70]; colstr[0] = '['; colstr[1] = '\0'; //max len 68 (5 chan, all 255) | ||||
|  | ||||
| 	for (uint8_t i = 0; i < 3; i++) | ||||
| 	{ | ||||
| 		JsonArray colX = colarr.createNestedArray(); | ||||
|     byte segcol[4]; byte* c = segcol; | ||||
|  | ||||
|     if (id == strip.getMainSegmentId() && i < 2) //temporary, to make transition work on main segment | ||||
|     { | ||||
|       if (i == 0) { | ||||
|         colX.add(col[0]); colX.add(col[1]); colX.add(col[2]); if (strip.isRgbw) colX.add(col[3]); | ||||
|       } else { | ||||
|          colX.add(colSec[0]); colX.add(colSec[1]); colX.add(colSec[2]); if (strip.isRgbw) colX.add(colSec[3]); | ||||
|       } | ||||
|       c = (i == 0)? col:colSec; | ||||
|     } else { | ||||
|   		colX.add((seg.colors[i] >> 16) & 0xFF); | ||||
|   		colX.add((seg.colors[i] >> 8) & 0xFF); | ||||
|   		colX.add((seg.colors[i]) & 0xFF); | ||||
|   		if (strip.isRgbw) | ||||
|   			colX.add((seg.colors[i] >> 24) & 0xFF); | ||||
|       segcol[0] = (byte)(seg.colors[i] >> 16); segcol[1] = (byte)(seg.colors[i] >> 8); | ||||
|       segcol[2] = (byte)(seg.colors[i]);       segcol[3] = (byte)(seg.colors[i] >> 24); | ||||
|     } | ||||
|  | ||||
|     char tmpcol[22]; | ||||
|     if (strip.isRgbw) sprintf_P(tmpcol, PSTR("[%u,%u,%u,%u]"), c[0], c[1], c[2], c[3]); | ||||
|     else              sprintf_P(tmpcol, PSTR("[%u,%u,%u]"),   c[0], c[1], c[2]); | ||||
|  | ||||
|     strcat(colstr, i<2 ? strcat(tmpcol,",") : tmpcol); | ||||
| 	} | ||||
|   strcat(colstr,"]"); | ||||
|   root["col"] = serialized(colstr); | ||||
|  | ||||
| 	root[F("fx")]  = seg.mode; | ||||
| 	root[F("sx")]  = seg.speed; | ||||
| 	root[F("ix")]  = seg.intensity; | ||||
| 	root[F("pal")] = seg.palette; | ||||
| 	root["pal"] = seg.palette; | ||||
| 	root[F("sel")] = seg.isSelected(); | ||||
| 	root["rev"] = seg.getOption(SEG_OPTION_REVERSED); | ||||
|   root[F("mi")]  = seg.getOption(SEG_OPTION_MIRROR); | ||||
| @@ -347,20 +366,13 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme | ||||
|     if (errorFlag) root[F("error")] = errorFlag; | ||||
|  | ||||
|     root[F("ps")] = currentPreset; | ||||
|     root[F("pl")] = (presetCyclingEnabled) ? 0: -1; | ||||
|     root[F("pl")] = currentPlaylist; | ||||
|  | ||||
|     usermods.addToJsonState(root); | ||||
|  | ||||
|     //temporary for preset cycle | ||||
|     JsonObject ccnf = root.createNestedObject("ccnf"); | ||||
|     ccnf[F("min")] = presetCycleMin; | ||||
|     ccnf[F("max")] = presetCycleMax; | ||||
|     ccnf[F("time")] = presetCycleTime; | ||||
|  | ||||
|     JsonObject nl = root.createNestedObject("nl"); | ||||
|     nl["on"] = nightlightActive; | ||||
|     nl[F("dur")] = nightlightDelayMins; | ||||
|     nl[F("fade")] = (nightlightMode > NL_MODE_SET); //deprecated | ||||
|     nl[F("mode")] = nightlightMode; | ||||
|     nl[F("tbri")] = nightlightTargetBri; | ||||
|     if (nightlightActive) { | ||||
| @@ -423,9 +435,6 @@ void serializeInfo(JsonObject root) | ||||
|   leds[F("count")] = ledCount; | ||||
|   leds[F("rgbw")] = strip.isRgbw; | ||||
|   leds[F("wv")] = strip.isRgbw && (strip.rgbwMode == RGBW_MODE_MANUAL_ONLY || strip.rgbwMode == RGBW_MODE_DUAL); //should a white channel slider be displayed? | ||||
|   JsonArray leds_pin = leds.createNestedArray("pin"); | ||||
|   leds_pin.add(LEDPIN); | ||||
|  | ||||
|   leds[F("pwr")] = strip.currentMilliamps; | ||||
|   leds[F("fps")] = strip.getFps(); | ||||
|   leds[F("maxpwr")] = (strip.currentMilliamps)? strip.ablMilliampsMax : 0; | ||||
| @@ -539,6 +548,13 @@ void serializeInfo(JsonObject root) | ||||
|   root[F("brand")] = "WLED"; | ||||
|   root[F("product")] = F("FOSS"); | ||||
|   root["mac"] = escapedMac; | ||||
|   char s[16] = ""; | ||||
|   if (Network.isConnected()) | ||||
|   { | ||||
|     IPAddress localIP = Network.localIP(); | ||||
|     sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); | ||||
|   } | ||||
|   root["ip"] = s; | ||||
| } | ||||
|  | ||||
| void setPaletteColors(JsonArray json, CRGBPalette16 palette) | ||||
| @@ -712,21 +728,24 @@ void serveJson(AsyncWebServerRequest* request) | ||||
|   const String& url = request->url(); | ||||
|   if      (url.indexOf("state") > 0) subJson = 1; | ||||
|   else if (url.indexOf("info")  > 0) subJson = 2; | ||||
|   else if (url.indexOf("si") > 0) subJson = 3; | ||||
|   else if (url.indexOf("si")    > 0) subJson = 3; | ||||
|   else if (url.indexOf("nodes") > 0) subJson = 4; | ||||
|   else if (url.indexOf("palx") > 0) subJson = 5; | ||||
|   else if (url.indexOf("palx")  > 0) subJson = 5; | ||||
|   else if (url.indexOf("live")  > 0) { | ||||
|     serveLiveLeds(request); | ||||
|     return; | ||||
|   } | ||||
|   else if (url.indexOf(F("eff"))   > 0) { | ||||
|   else if (url.indexOf(F("eff")) > 0) { | ||||
|     request->send_P(200, "application/json", JSON_mode_names); | ||||
|     return; | ||||
|   } | ||||
|   else if (url.indexOf(F("pal"))   > 0) { | ||||
|   else if (url.indexOf("pal") > 0) { | ||||
|     request->send_P(200, "application/json", JSON_palette_names); | ||||
|     return; | ||||
|   } | ||||
|   else if (url.indexOf("cfg") > 0 && handleFileRead(request, "/cfg.json")) { | ||||
|     return; | ||||
|   } | ||||
|   else if (url.length() > 6) { //not just /json | ||||
|     request->send(  501, "application/json", F("{\"error\":\"Not implemented\"}")); | ||||
|     return; | ||||
| @@ -757,6 +776,9 @@ void serveJson(AsyncWebServerRequest* request) | ||||
|       } | ||||
|   } | ||||
|  | ||||
|   DEBUG_PRINT("JSON buffer size: "); | ||||
|   DEBUG_PRINTLN(doc.memoryUsage()); | ||||
|  | ||||
|   response->setLength(); | ||||
|   request->send(response); | ||||
| } | ||||
|   | ||||
| @@ -111,8 +111,7 @@ void colorUpdated(int callMode) | ||||
|   { | ||||
|     effectChanged = false; | ||||
|     if (realtimeTimeout == UINT32_MAX) realtimeTimeout = 0; | ||||
|     if (isPreset) {isPreset = false;} | ||||
|         else {currentPreset = -1;} | ||||
|     currentPreset = -1; //something changed, so we are no longer in the preset | ||||
|          | ||||
|     notify(callMode); | ||||
|      | ||||
| @@ -297,19 +296,6 @@ void handleNightlight() | ||||
|     } | ||||
|     nightlightActiveOld = false; | ||||
|   } | ||||
|  | ||||
|   //also handle preset cycle here | ||||
|   if (presetCyclingEnabled && (millis() - presetCycledTime > (100*presetCycleTime))) | ||||
|   { | ||||
|     presetCycledTime = millis(); | ||||
|     if (bri == 0 || nightlightActive) return; | ||||
|  | ||||
|     if (presetCycCurr < presetCycleMin || presetCycCurr > presetCycleMax) presetCycCurr = presetCycleMin; | ||||
|     applyPreset(presetCycCurr); //this handles colorUpdated() for us | ||||
|     presetCycCurr++; | ||||
|     if (presetCycCurr > 250) presetCycCurr = 1; | ||||
|     interfaceUpdateCallMode = 0; //disable updates to MQTT and Blynk | ||||
|   } | ||||
| } | ||||
|  | ||||
| //utility for FastLED to use our custom timer | ||||
|   | ||||
| @@ -25,28 +25,28 @@ void onMqttConnect(bool sessionPresent) | ||||
|   //(re)subscribe to required topics | ||||
|   char subuf[38]; | ||||
|  | ||||
|   if (mqttDeviceTopic[0] != 0) | ||||
|   { | ||||
|   if (mqttDeviceTopic[0] != 0) { | ||||
|     strcpy(subuf, mqttDeviceTopic); | ||||
|     mqtt->subscribe(subuf, 0); | ||||
|     strcat(subuf, "/col"); | ||||
|     strcat_P(subuf, PSTR("/col")); | ||||
|     mqtt->subscribe(subuf, 0); | ||||
|     strcpy(subuf, mqttDeviceTopic); | ||||
|     strcat(subuf, "/api"); | ||||
|     strcat_P(subuf, PSTR("/api")); | ||||
|     mqtt->subscribe(subuf, 0); | ||||
|   } | ||||
|  | ||||
|   if (mqttGroupTopic[0] != 0) | ||||
|   { | ||||
|   if (mqttGroupTopic[0] != 0) { | ||||
|     strcpy(subuf, mqttGroupTopic); | ||||
|     mqtt->subscribe(subuf, 0); | ||||
|     strcat(subuf, "/col"); | ||||
|     strcat_P(subuf, PSTR("/col")); | ||||
|     mqtt->subscribe(subuf, 0); | ||||
|     strcpy(subuf, mqttGroupTopic); | ||||
|     strcat(subuf, "/api"); | ||||
|     strcat_P(subuf, PSTR("/api")); | ||||
|     mqtt->subscribe(subuf, 0); | ||||
|   } | ||||
|  | ||||
|   usermods.onMqttConnect(sessionPresent); | ||||
|  | ||||
|   doPublishMqtt = true; | ||||
|   DEBUG_PRINTLN(F("MQTT ready")); | ||||
| } | ||||
| @@ -62,42 +62,51 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties | ||||
|     DEBUG_PRINTLN(F("no payload -> leave")); | ||||
|     return; | ||||
|   } | ||||
|   DEBUG_PRINTLN(payload); | ||||
|   //make a copy of the payload to 0-terminate it | ||||
|   char* payloadStr = new char[len+1]; | ||||
|   if (payloadStr == nullptr) return; //no mem | ||||
|   strncpy(payloadStr, payload, len); | ||||
|   payloadStr[len] = '\0'; | ||||
|   DEBUG_PRINTLN(payloadStr); | ||||
|  | ||||
|   size_t topicPrefixLen = strlen(mqttDeviceTopic); | ||||
|   if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) { | ||||
|       topic += topicPrefixLen; | ||||
|     topic += topicPrefixLen; | ||||
|   } else { | ||||
|       topicPrefixLen = strlen(mqttGroupTopic); | ||||
|       if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { | ||||
|           topic += topicPrefixLen; | ||||
|       } else { | ||||
|           // Topic not used here. Probably a usermod subscribed to this topic. | ||||
|           return; | ||||
|       } | ||||
|     topicPrefixLen = strlen(mqttGroupTopic); | ||||
|     if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { | ||||
|       topic += topicPrefixLen; | ||||
|     } else { | ||||
|       // Non-Wled Topic used here. Probably a usermod subscribed to this topic. | ||||
|       usermods.onMqttMessage(topic, payloadStr); | ||||
|       delete[] payloadStr; | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   //Prefix is stripped from the topic at this point | ||||
|  | ||||
|   if (strcmp(topic, "/col") == 0) | ||||
|   { | ||||
|     colorFromDecOrHexString(col, (char*)payload); | ||||
|   if (strcmp_P(topic, PSTR("/col")) == 0) { | ||||
|     colorFromDecOrHexString(col, (char*)payloadStr); | ||||
|     colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); | ||||
|   } else if (strcmp(topic, "/api") == 0) | ||||
|   { | ||||
|   } else if (strcmp_P(topic, PSTR("/api")) == 0) { | ||||
|     if (payload[0] == '{') { //JSON API | ||||
|       DynamicJsonDocument doc(JSON_BUFFER_SIZE); | ||||
|       deserializeJson(doc, payload); | ||||
|       deserializeJson(doc, payloadStr); | ||||
|       deserializeState(doc.as<JsonObject>()); | ||||
|     } else { //HTTP API | ||||
|       String apireq = "win&"; | ||||
|       apireq += (char*)payload; | ||||
|       apireq += (char*)payloadStr; | ||||
|       handleSet(nullptr, apireq); | ||||
|     } | ||||
|   } else if (strcmp(topic, "") == 0) | ||||
|   { | ||||
|     parseMQTTBriPayload(payload); | ||||
|   } else if (strlen(topic) != 0) { | ||||
|     // non standard topic, check with usermods | ||||
|     usermods.onMqttMessage(topic, payloadStr); | ||||
|   } else { | ||||
|     // topmost topic (just wled/MAC) | ||||
|     parseMQTTBriPayload(payloadStr); | ||||
|   } | ||||
|   delete[] payloadStr; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -110,25 +119,25 @@ void publishMqtt() | ||||
|   char s[10]; | ||||
|   char subuf[38]; | ||||
|  | ||||
|   sprintf(s, "%u", bri); | ||||
|   sprintf_P(s, PSTR("%u"), bri); | ||||
|   strcpy(subuf, mqttDeviceTopic); | ||||
|   strcat(subuf, "/g"); | ||||
|   strcat_P(subuf, PSTR("/g")); | ||||
|   mqtt->publish(subuf, 0, true, s); | ||||
|  | ||||
|   sprintf(s, "#%06X", (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); | ||||
|   sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); | ||||
|   strcpy(subuf, mqttDeviceTopic); | ||||
|   strcat(subuf, "/c"); | ||||
|   strcat_P(subuf, PSTR("/c")); | ||||
|   mqtt->publish(subuf, 0, true, s); | ||||
|  | ||||
|   strcpy(subuf, mqttDeviceTopic); | ||||
|   strcat(subuf, "/status"); | ||||
|   strcat_P(subuf, PSTR("/status")); | ||||
|   mqtt->publish(subuf, 0, true, "online"); | ||||
|  | ||||
|   char apires[1024]; | ||||
|   XML_response(nullptr, apires); | ||||
|   strcpy(subuf, mqttDeviceTopic); | ||||
|   strcat(subuf, "/v"); | ||||
|   mqtt->publish(subuf, 0, true, apires); | ||||
|   strcat_P(subuf, PSTR("/v")); | ||||
|   mqtt->publish(subuf, 0, false, apires); | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -136,7 +145,6 @@ void publishMqtt() | ||||
|  | ||||
| bool initMqtt() | ||||
| { | ||||
|   lastMqttReconnectAttempt = millis(); | ||||
|   if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; | ||||
|  | ||||
|   if (mqtt == nullptr) { | ||||
| @@ -158,7 +166,7 @@ bool initMqtt() | ||||
|   if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); | ||||
|  | ||||
|   strcpy(mqttStatusTopic, mqttDeviceTopic); | ||||
|   strcat(mqttStatusTopic, "/status"); | ||||
|   strcat_P(mqttStatusTopic, PSTR("/status")); | ||||
|   mqtt->setWill(mqttStatusTopic, 0, true, "offline"); | ||||
|   mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); | ||||
|   mqtt->connect(); | ||||
|   | ||||
							
								
								
									
										233
									
								
								wled00/ntp.cpp
									
									
									
									
									
								
							
							
						
						
									
										233
									
								
								wled00/ntp.cpp
									
									
									
									
									
								
							| @@ -1,9 +1,13 @@ | ||||
| #include "src/dependencies/timezone/Timezone.h" | ||||
| #include "wled.h" | ||||
| #include "wled_math.h" | ||||
|  | ||||
| /* | ||||
|  * Acquires time from NTP server | ||||
|  */ | ||||
| //#define WLED_DEBUG_NTP | ||||
| #define NTP_SYNC_INTERVAL 42000UL //Get fresh NTP time about twice per day | ||||
|  | ||||
| Timezone* tz; | ||||
|  | ||||
| #define TZ_UTC                  0 | ||||
| @@ -111,7 +115,7 @@ void updateTimezone() { | ||||
|       break; | ||||
|     } | ||||
|     case TZ_AUSTRALIA_NORTHERN : { | ||||
|       tcrStandard = {First, Sun, Apr, 3, 570};   //ACST = UTC + 9.5 hours | ||||
|       tcrDaylight = {First, Sun, Apr, 3, 570};   //ACST = UTC + 9.5 hours | ||||
|       tcrStandard = tcrDaylight; | ||||
|       break; | ||||
|     } | ||||
| @@ -132,9 +136,28 @@ void updateTimezone() { | ||||
|   tz = new Timezone(tcrDaylight, tcrStandard); | ||||
| } | ||||
|  | ||||
| void handleTime() { | ||||
|   handleNetworkTime(); | ||||
|    | ||||
|   toki.millisecond(); | ||||
|   toki.setTick(); | ||||
|  | ||||
|   if (toki.isTick()) //true only in the first loop after a new second started | ||||
|   { | ||||
|     #ifdef WLED_DEBUG_NTP | ||||
|     Serial.print(F("TICK! ")); | ||||
|     toki.printTime(toki.getTime()); | ||||
|     #endif | ||||
|     updateLocalTime(); | ||||
|     checkTimers(); | ||||
|     checkCountdown(); | ||||
|     handleOverlays(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void handleNetworkTime() | ||||
| { | ||||
|   if (ntpEnabled && ntpConnected && millis() - ntpLastSyncTime > 50000000L && WLED_CONNECTED) | ||||
|   if (ntpEnabled && ntpConnected && millis() - ntpLastSyncTime > (1000*NTP_SYNC_INTERVAL) && WLED_CONNECTED) | ||||
|   { | ||||
|     if (millis() - ntpPacketSentTime > 10000) | ||||
|     { | ||||
| @@ -181,32 +204,52 @@ void sendNTPPacket() | ||||
| bool checkNTPResponse() | ||||
| { | ||||
|   int cb = ntpUdp.parsePacket(); | ||||
|   if (cb) { | ||||
|     DEBUG_PRINT(F("NTP recv, l=")); | ||||
|     DEBUG_PRINTLN(cb); | ||||
|     byte pbuf[NTP_PACKET_SIZE]; | ||||
|     ntpUdp.read(pbuf, NTP_PACKET_SIZE); // read the packet into the buffer | ||||
|   if (!cb) return false; | ||||
|  | ||||
|     unsigned long highWord = word(pbuf[40], pbuf[41]); | ||||
|     unsigned long lowWord = word(pbuf[42], pbuf[43]); | ||||
|     if (highWord == 0 && lowWord == 0) return false; | ||||
|      | ||||
|     unsigned long secsSince1900 = highWord << 16 | lowWord; | ||||
|   | ||||
|     DEBUG_PRINT(F("Unix time = ")); | ||||
|     unsigned long epoch = secsSince1900 - 2208988799UL; //subtract 70 years -1sec (on avg. more precision) | ||||
|     setTime(epoch); | ||||
|     DEBUG_PRINTLN(epoch); | ||||
|     if (countdownTime - now() > 0) countdownOverTriggered = false; | ||||
|     return true; | ||||
|   } | ||||
|   return false; | ||||
|   uint32_t ntpPacketReceivedTime = millis(); | ||||
|   DEBUG_PRINT(F("NTP recv, l=")); | ||||
|   DEBUG_PRINTLN(cb); | ||||
|   byte pbuf[NTP_PACKET_SIZE]; | ||||
|   ntpUdp.read(pbuf, NTP_PACKET_SIZE); // read the packet into the buffer | ||||
|  | ||||
|   Toki::Time arrived  = toki.fromNTP(pbuf + 32); | ||||
|   Toki::Time departed = toki.fromNTP(pbuf + 40); | ||||
|   if (departed.sec == 0) return false; | ||||
|   //basic half roundtrip estimation | ||||
|   uint32_t serverDelay = toki.msDifference(arrived, departed); | ||||
|   uint32_t offset = (ntpPacketReceivedTime - ntpPacketSentTime - serverDelay) >> 1; | ||||
|   #ifdef WLED_DEBUG_NTP | ||||
|   //the time the packet departed the NTP server | ||||
|   toki.printTime(departed); | ||||
|   #endif | ||||
|  | ||||
|   toki.adjust(departed, offset); | ||||
|   toki.setTime(departed, TOKI_TS_NTP); | ||||
|  | ||||
|   #ifdef WLED_DEBUG_NTP | ||||
|   Serial.print("Arrived: "); | ||||
|   toki.printTime(arrived); | ||||
|   Serial.print("Time: "); | ||||
|   toki.printTime(departed); | ||||
|   Serial.print("Roundtrip: "); | ||||
|   Serial.println(ntpPacketReceivedTime - ntpPacketSentTime); | ||||
|   Serial.print("Offset: "); | ||||
|   Serial.println(offset); | ||||
|   Serial.print("Serverdelay: "); | ||||
|   Serial.println(serverDelay); | ||||
|   #endif | ||||
|  | ||||
|   if (countdownTime - toki.second() > 0) countdownOverTriggered = false; | ||||
|   // if time changed re-calculate sunrise/sunset | ||||
|   updateLocalTime(); | ||||
|   calculateSunriseAndSunset(); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void updateLocalTime() | ||||
| { | ||||
|   if (currentTimezone != tzCurrent) updateTimezone(); | ||||
|   unsigned long tmc = now()+ utcOffsetSecs; | ||||
|   unsigned long tmc = toki.second()+ utcOffsetSecs; | ||||
|   localTime = tz->toLocal(tmc); | ||||
| } | ||||
|  | ||||
| @@ -219,9 +262,7 @@ void getTimeString(char* out) | ||||
|     if (hr > 11) hr -= 12; | ||||
|     if (hr == 0) hr  = 12; | ||||
|   } | ||||
|   sprintf(out,"%i-%i-%i, %i:%s%i:%s%i",year(localTime), month(localTime), day(localTime),  | ||||
|                                        hr,(minute(localTime)<10)?"0":"",minute(localTime), | ||||
|                                        (second(localTime)<10)?"0":"",second(localTime)); | ||||
|   sprintf_P(out,PSTR("%i-%i-%i, %02d:%02d:%02d"),year(localTime), month(localTime), day(localTime), hr, minute(localTime), second(localTime)); | ||||
|   if (useAMPM) | ||||
|   { | ||||
|     strcat(out,(hour(localTime) > 11)? " PM":" AM"); | ||||
| @@ -232,13 +273,13 @@ void setCountdown() | ||||
| { | ||||
|   if (currentTimezone != tzCurrent) updateTimezone(); | ||||
|   countdownTime = tz->toUTC(getUnixTime(countdownHour, countdownMin, countdownSec, countdownDay, countdownMonth, countdownYear)); | ||||
|   if (countdownTime - now() > 0) countdownOverTriggered = false; | ||||
|   if (countdownTime - toki.second() > 0) countdownOverTriggered = false; | ||||
| } | ||||
|  | ||||
| //returns true if countdown just over | ||||
| bool checkCountdown() | ||||
| { | ||||
|   unsigned long n = now(); | ||||
|   unsigned long n = toki.second(); | ||||
|   if (countdownMode) localTime = countdownTime - n + utcOffsetSecs; | ||||
|   if (n > countdownTime) { | ||||
|     if (countdownMode) localTime = n - countdownTime + utcOffsetSecs; | ||||
| @@ -264,16 +305,152 @@ void checkTimers() | ||||
|   if (lastTimerMinute != minute(localTime)) //only check once a new minute begins | ||||
|   { | ||||
|     lastTimerMinute = minute(localTime); | ||||
|  | ||||
|     // re-calculate sunrise and sunset just after midnight | ||||
|     if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset(); | ||||
|  | ||||
|     DEBUG_PRINTF("Local time: %02d:%02d\n", hour(localTime), minute(localTime)); | ||||
|     for (uint8_t i = 0; i < 8; i++) | ||||
|     { | ||||
|       if (timerMacro[i] != 0 | ||||
|           && (timerHours[i] == hour(localTime) || timerHours[i] == 24) //if hour is set to 24, activate every hour  | ||||
|           && timerMinutes[i] == minute(localTime) | ||||
|           && (timerWeekday[i] & 0x01) //timer is enabled | ||||
|           && timerWeekday[i] >> weekdayMondayFirst() & 0x01) //timer should activate at current day of week | ||||
|           && ((timerWeekday[i] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week | ||||
|       { | ||||
|         applyPreset(timerMacro[i]); | ||||
|       } | ||||
|     } | ||||
|     // sunrise macro | ||||
|     if (sunrise) { | ||||
|       time_t tmp = sunrise + timerMinutes[8]*60;  // NOTE: may not be ok | ||||
|       DEBUG_PRINTF("Trigger time: %02d:%02d\n", hour(tmp), minute(tmp)); | ||||
|       if (timerMacro[8] != 0 | ||||
|           && hour(tmp) == hour(localTime) | ||||
|           && minute(tmp) == minute(localTime) | ||||
|           && (timerWeekday[8] & 0x01) //timer is enabled | ||||
|           && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week | ||||
|       { | ||||
|         applyPreset(timerMacro[8]); | ||||
|         DEBUG_PRINTF("Sunrise macro %d triggered.",timerMacro[8]); | ||||
|       } | ||||
|     } | ||||
|     // sunset macro | ||||
|     if (sunset) { | ||||
|       time_t tmp = sunset + timerMinutes[9]*60;  // NOTE: may not be ok | ||||
|       DEBUG_PRINTF("Trigger time: %02d:%02d\n", hour(tmp), minute(tmp)); | ||||
|       if (timerMacro[9] != 0 | ||||
|           && hour(tmp) == hour(localTime) | ||||
|           && minute(tmp) == minute(localTime) | ||||
|           && (timerWeekday[9] & 0x01) //timer is enabled | ||||
|           && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week | ||||
|       { | ||||
|         applyPreset(timerMacro[9]); | ||||
|         DEBUG_PRINTF("Sunset macro %d triggered.",timerMacro[9]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| #define ZENITH -0.83 | ||||
| // get sunrise (or sunset) time (in minutes) for a given day at a given geo location | ||||
| int getSunriseUTC(int year, int month, int day, float lat, float lon, bool sunset=false) { | ||||
|   //1. first calculate the day of the year | ||||
|   float N1 = 275 * month / 9; | ||||
|   float N2 = (month + 9) / 12; | ||||
|   float N3 = (1 + floor_t((year - 4 * floor_t(year / 4) + 2) / 3)); | ||||
|   float N = N1 - (N2 * N3) + day - 30; | ||||
|  | ||||
|   //2. convert the longitude to hour value and calculate an approximate time | ||||
|   float lngHour = lon / 15.0f;       | ||||
|   float t = N + (((sunset ? 18 : 6) - lngHour) / 24); | ||||
|    | ||||
|   //3. calculate the Sun's mean anomaly    | ||||
|   float M = (0.9856f * t) - 3.289f; | ||||
|  | ||||
|   //4. calculate the Sun's true longitude | ||||
|   float L = fmod_t(M + (1.916f * sin_t(DEG_TO_RAD*M)) + (0.02f * sin_t(2*DEG_TO_RAD*M)) + 282.634f, 360.0f); | ||||
|  | ||||
|   //5a. calculate the Sun's right ascension       | ||||
|   float RA = fmod_t(RAD_TO_DEG*atan_t(0.91764f * tan_t(DEG_TO_RAD*L)), 360.0f); | ||||
|  | ||||
|   //5b. right ascension value needs to be in the same quadrant as L    | ||||
|   float Lquadrant  = floor_t( L/90) * 90; | ||||
|   float RAquadrant = floor_t(RA/90) * 90; | ||||
|   RA = RA + (Lquadrant - RAquadrant); | ||||
|  | ||||
|   //5c. right ascension value needs to be converted into hours    | ||||
|   RA /= 15.0f; | ||||
|  | ||||
|   //6. calculate the Sun's declination | ||||
|   float sinDec = 0.39782f * sin_t(DEG_TO_RAD*L); | ||||
|   float cosDec = cos_t(asin_t(sinDec)); | ||||
|  | ||||
|   //7a. calculate the Sun's local hour angle | ||||
|   float cosH = (sin_t(DEG_TO_RAD*ZENITH) - (sinDec * sin_t(DEG_TO_RAD*lat))) / (cosDec * cos_t(DEG_TO_RAD*lat)); | ||||
|   if (cosH > 1 && !sunset) return 0;  // the sun never rises on this location (on the specified date) | ||||
|   if (cosH < -1 && sunset) return 0;  // the sun never sets on this location (on the specified date) | ||||
|  | ||||
|   //7b. finish calculating H and convert into hours | ||||
|   float H = sunset ? RAD_TO_DEG*acos_t(cosH) : 360 - RAD_TO_DEG*acos_t(cosH); | ||||
|   H /= 15.0f; | ||||
|  | ||||
|   //8. calculate local mean time of rising/setting       | ||||
|   float T = H + RA - (0.06571f * t) - 6.622f; | ||||
|  | ||||
|   //9. adjust back to UTC | ||||
|   float UT = fmod_t(T - lngHour, 24.0f); | ||||
|  | ||||
|   // return in minutes from midnight | ||||
| 	return UT*60; | ||||
| } | ||||
|  | ||||
| // calculate sunrise and sunset (if longitude and latitude are set) | ||||
| void calculateSunriseAndSunset() { | ||||
|   if ((int)(longitude*10.) || (int)(latitude*10.)) { | ||||
|     struct tm tim_0; | ||||
|     tim_0.tm_year = year(localTime)-1900; | ||||
|     tim_0.tm_mon = month(localTime)-1; | ||||
|     tim_0.tm_mday = day(localTime); | ||||
|     tim_0.tm_sec = 0; | ||||
|     tim_0.tm_isdst = 0; | ||||
|  | ||||
|     int minUTC = getSunriseUTC(year(localTime), month(localTime), day(localTime), latitude, longitude); | ||||
|     if (minUTC) { | ||||
|       // there is a sunrise | ||||
|       tim_0.tm_hour = minUTC / 60; | ||||
|       tim_0.tm_min = minUTC % 60; | ||||
|       sunrise = tz->toLocal(mktime(&tim_0) - utcOffsetSecs); | ||||
|       DEBUG_PRINTF("Sunrise: %02d:%02d\n", hour(sunrise), minute(sunrise)); | ||||
|     } else { | ||||
|       sunrise = 0; | ||||
|     } | ||||
|  | ||||
|     minUTC = getSunriseUTC(year(localTime), month(localTime), day(localTime), latitude, longitude, true); | ||||
|     if (minUTC) { | ||||
|       // there is a sunset | ||||
|       tim_0.tm_hour = minUTC / 60; | ||||
|       tim_0.tm_min = minUTC % 60; | ||||
|       sunset = tz->toLocal(mktime(&tim_0) - utcOffsetSecs); | ||||
|       DEBUG_PRINTF("Sunset: %02d:%02d\n", hour(sunset), minute(sunset)); | ||||
|     } else { | ||||
|       sunset = 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| //time from JSON and HTTP API | ||||
| void setTimeFromAPI(uint32_t timein) { | ||||
|   if (timein == 0 || timein == UINT32_MAX) return; | ||||
|   uint32_t prev = toki.second(); | ||||
|   //only apply if more accurate or there is a significant difference to the "more accurate" time source | ||||
|   uint32_t diff = (timein > prev) ? timein - prev : prev - timein; | ||||
|   if (toki.getTimeSource() > TOKI_TS_JSON && diff < 60U) return; | ||||
|  | ||||
|   toki.setTime(timein, TOKI_NO_MS_ACCURACY, TOKI_TS_JSON); | ||||
|   if (diff >= 60U) { | ||||
|     updateLocalTime(); | ||||
|     calculateSunriseAndSunset(); | ||||
|   } | ||||
|   if (presetsModifiedTime == 0) presetsModifiedTime = timein; | ||||
| } | ||||
| @@ -6,29 +6,24 @@ | ||||
|   | ||||
| void initCronixie() | ||||
| { | ||||
|   if (overlayCurrent == 3 && !cronixieInit) | ||||
|   if (overlayCurrent == 3 && dP[0] == 255) //if dP[0] is 255, cronixie is not yet init'ed | ||||
|   { | ||||
|     setCronixie(); | ||||
|     strip.getSegment(0).grouping = 10; //10 LEDs per digit | ||||
|     cronixieInit = true; | ||||
|   } else if (cronixieInit && overlayCurrent != 3) | ||||
|   } else if (dP[0] < 255 && overlayCurrent != 3) | ||||
|   { | ||||
|     strip.getSegment(0).grouping = 1; | ||||
|     cronixieInit = false;  | ||||
|     dP[0] = 255;  | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| void handleOverlays() | ||||
| { | ||||
|   if (millis() - overlayRefreshedTime > overlayRefreshMs) | ||||
|   { | ||||
|     initCronixie(); | ||||
|     updateLocalTime(); | ||||
|     checkTimers(); | ||||
|     checkCountdown(); | ||||
|     if (overlayCurrent == 3) _overlayCronixie();//Diamex cronixie clock kit | ||||
|     overlayRefreshedTime = millis(); | ||||
|   initCronixie(); | ||||
|   if (overlayCurrent == 3) { | ||||
|     _overlayCronixie();//Diamex cronixie clock kit | ||||
|     strip.trigger(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -63,10 +58,9 @@ void _overlayAnalogClock() | ||||
|   } | ||||
|   if (analogClock5MinuteMarks) | ||||
|   { | ||||
|     int pix; | ||||
|     for (int i = 0; i <= 12; i++) | ||||
|     for (byte i = 0; i <= 12; i++) | ||||
|     { | ||||
|       pix = analogClock12pixel + round((overlaySize / 12.0) *i); | ||||
|       int pix = analogClock12pixel + round((overlaySize / 12.0) *i); | ||||
|       if (pix > overlayMax) pix -= overlaySize; | ||||
|       strip.setPixelColor(pix, 0x00FFAA); | ||||
|     } | ||||
| @@ -74,15 +68,14 @@ void _overlayAnalogClock() | ||||
|   if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, 0xFF0000); | ||||
|   strip.setPixelColor(minutePixel, 0x00FF00); | ||||
|   strip.setPixelColor(hourPixel, 0x0000FF); | ||||
|   overlayRefreshMs = 998; | ||||
| } | ||||
|  | ||||
|  | ||||
| void _overlayAnalogCountdown() | ||||
| { | ||||
|   if (now() < countdownTime) | ||||
|   if ((unsigned long)toki.second() < countdownTime) | ||||
|   { | ||||
|     long diff = countdownTime - now(); | ||||
|     long diff = countdownTime - toki.second(); | ||||
|     double pval = 60; | ||||
|     if (diff > 31557600L) //display in years if more than 365 days | ||||
|     { | ||||
| @@ -116,7 +109,6 @@ void _overlayAnalogCountdown() | ||||
|       strip.setRange(analogClock12pixel, analogClock12pixel + pixelCnt, ((uint32_t)colSec[3] << 24)| ((uint32_t)colSec[0] << 16) | ((uint32_t)colSec[1] << 8) | colSec[2]); | ||||
|     } | ||||
|   } | ||||
|   overlayRefreshMs = 998; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -220,8 +212,6 @@ void setCronixie() | ||||
|  | ||||
|   DEBUG_PRINT("cset "); | ||||
|   DEBUG_PRINTLN(cronixieDisplay); | ||||
|  | ||||
|   overlayRefreshMs = 1997; //Only refresh every 2secs if no seconds are displayed | ||||
|    | ||||
|   for (int i = 0; i < 6; i++) | ||||
|   { | ||||
| @@ -242,8 +232,8 @@ void setCronixie() | ||||
|       case 'a': dP[i] = 58; i++; break; | ||||
|       case 'm': dP[i] = 74 + getSameCodeLength('m',i,cronixieDisplay); i = i+dP[i]-74; break; | ||||
|       case 'M': dP[i] = 24 + getSameCodeLength('M',i,cronixieDisplay); i = i+dP[i]-24; break; | ||||
|       case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; overlayRefreshMs = 497; break; //refresh more often bc. of secs | ||||
|       case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; overlayRefreshMs = 497; break; | ||||
|       case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; break; //refresh more often bc. of secs | ||||
|       case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; break; | ||||
|       case 'Y': dP[i] = 36 + getSameCodeLength('Y',i,cronixieDisplay); i = i+dP[i]-36; break;  | ||||
|       case 'y': dP[i] = 86 + getSameCodeLength('y',i,cronixieDisplay); i = i+dP[i]-86; break;  | ||||
|       case 'I': dP[i] = 39 + getSameCodeLength('I',i,cronixieDisplay); i = i+dP[i]-39; break;  //Month. Don't ask me why month and minute both start with M. | ||||
|   | ||||
| @@ -5,127 +5,141 @@ | ||||
|  */ | ||||
|  | ||||
| typedef struct PlaylistEntry { | ||||
|   uint8_t preset; | ||||
|   uint16_t dur; | ||||
|   uint16_t tr; | ||||
|   uint8_t preset; //ID of the preset to apply | ||||
|   uint16_t dur;   //Duration of the entry (in tenths of seconds) | ||||
|   uint16_t tr;    //Duration of the transition TO this entry (in tenths of seconds) | ||||
| } ple; | ||||
|  | ||||
| int8_t   playlistRepeat = 1; | ||||
| byte     playlistEndPreset = 0; | ||||
| byte    *playlistEntries = nullptr; | ||||
| byte     playlistLen; | ||||
| int8_t   playlistIndex = -1; | ||||
| uint16_t playlistEntryDur = 0; | ||||
| byte           playlistRepeat = 1;        //how many times to repeat the playlist (0 = infinitely) | ||||
| byte           playlistEndPreset = 0;     //what preset to apply after playlist end (0 = stay on last preset) | ||||
| byte           playlistOptions = 0;       //bit 0: shuffle playlist after each iteration. bits 1-7 TBD | ||||
|  | ||||
| PlaylistEntry *playlistEntries = nullptr; | ||||
| byte           playlistLen;               //number of playlist entries | ||||
| int8_t         playlistIndex = -1; | ||||
| uint16_t       playlistEntryDur = 0;      //duration of the current entry in tenths of seconds | ||||
|  | ||||
| //values we need to keep about the parent playlist while inside sub-playlist | ||||
| //int8_t         parentPlaylistIndex = -1; | ||||
| //byte           parentPlaylistRepeat = 0; | ||||
| //byte           parentPlaylistPresetId = 0; //for re-loading | ||||
|  | ||||
|  | ||||
| void shufflePlaylist() { | ||||
|   int currentIndex = playlistLen, randomIndex; | ||||
|  | ||||
|   PlaylistEntry temporaryValue, *entries = reinterpret_cast<PlaylistEntry*>(playlistEntries); | ||||
|   int currentIndex = playlistLen; | ||||
|   PlaylistEntry temporaryValue; | ||||
|  | ||||
|   // While there remain elements to shuffle... | ||||
|   while (currentIndex--) { | ||||
|     // Pick a random element... | ||||
|     randomIndex = random(0, currentIndex); | ||||
|     int randomIndex = random(0, currentIndex); | ||||
|     // And swap it with the current element. | ||||
|     temporaryValue = entries[currentIndex]; | ||||
|     entries[currentIndex] = entries[randomIndex]; | ||||
|     entries[randomIndex] = temporaryValue; | ||||
|     temporaryValue = playlistEntries[currentIndex]; | ||||
|     playlistEntries[currentIndex] = playlistEntries[randomIndex]; | ||||
|     playlistEntries[randomIndex] = temporaryValue; | ||||
|   } | ||||
|   DEBUG_PRINTLN(F("Playlist shuffle.")); | ||||
| } | ||||
|  | ||||
|  | ||||
| void unloadPlaylist() { | ||||
|   if (playlistEntries != nullptr) { | ||||
|     delete[] playlistEntries; | ||||
|     playlistEntries = nullptr; | ||||
|   } | ||||
|   currentPlaylist = playlistIndex = -1; | ||||
|   playlistLen = playlistEntryDur = 0; | ||||
|   playlistLen = playlistEntryDur = playlistOptions = 0; | ||||
|   DEBUG_PRINTLN(F("Playlist unloaded.")); | ||||
| } | ||||
|  | ||||
| void loadPlaylist(JsonObject playlistObj) { | ||||
|  | ||||
| void loadPlaylist(JsonObject playlistObj, byte presetId) { | ||||
|   unloadPlaylist(); | ||||
|    | ||||
|   JsonArray presets = playlistObj["ps"]; | ||||
|   playlistLen = presets.size(); | ||||
|   if (playlistLen == 0) return; | ||||
|   if (playlistLen > 100) playlistLen = 100; | ||||
|   uint16_t dataSize = sizeof(ple) * playlistLen; | ||||
|   playlistEntries = new byte[dataSize]; | ||||
|   PlaylistEntry* entries = reinterpret_cast<PlaylistEntry*>(playlistEntries); | ||||
|  | ||||
|   playlistEntries = new PlaylistEntry[playlistLen]; | ||||
|   if (playlistEntries == nullptr) return; | ||||
|  | ||||
|   byte it = 0; | ||||
|   for (int ps : presets) { | ||||
|     if (it >= playlistLen) break; | ||||
|     entries[it].preset = ps; | ||||
|     playlistEntries[it].preset = ps; | ||||
|     it++; | ||||
|   } | ||||
|  | ||||
|   it = 0; | ||||
|   JsonArray durations = playlistObj["dur"]; | ||||
|   if (durations.isNull()) { | ||||
|     entries[0].dur = playlistObj["dur"] | 100; | ||||
|     playlistEntries[0].dur = playlistObj["dur"] | 100; //10 seconds as fallback | ||||
|     it = 1; | ||||
|   } else { | ||||
|     for (int dur : durations) { | ||||
|       if (it >= playlistLen) break; | ||||
|       entries[it].dur = dur; | ||||
|       playlistEntries[it].dur = (dur > 1) ? dur : 100; | ||||
|       it++; | ||||
|     } | ||||
|   } | ||||
|   for (int i = it; i < playlistLen; i++) entries[i].dur = entries[it -1].dur; | ||||
|   for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur; | ||||
|  | ||||
|   it = 0; | ||||
|   JsonArray tr = playlistObj["transition"]; | ||||
|   JsonArray tr = playlistObj[F("transition")]; | ||||
|   if (tr.isNull()) { | ||||
|     entries[0].tr = playlistObj["transition"] | (transitionDelay / 100); | ||||
|     playlistEntries[0].tr = playlistObj[F("transition")] | (transitionDelay / 100); | ||||
|     it = 1; | ||||
|   } else { | ||||
|     for (int transition : tr) { | ||||
|       if (it >= playlistLen) break; | ||||
|       entries[it].tr = transition; | ||||
|       playlistEntries[it].tr = transition; | ||||
|       it++; | ||||
|     } | ||||
|   } | ||||
|   for (int i = it; i < playlistLen; i++) entries[i].tr = entries[it -1].tr; | ||||
|   for (int i = it; i < playlistLen; i++) playlistEntries[i].tr = playlistEntries[it -1].tr; | ||||
|  | ||||
|   playlistRepeat = playlistObj[F("repeat")] | 0; | ||||
|   int rep = playlistObj[F("repeat")]; | ||||
|   bool shuffle = false; | ||||
|   if (rep < 0) { //support negative values as infinite + shuffle | ||||
|     rep = 0; shuffle = true; | ||||
|   } | ||||
|  | ||||
|   playlistRepeat = rep; | ||||
|   if (playlistRepeat > 0) playlistRepeat++; //add one extra repetition immediately since it will be deducted on first start | ||||
|   playlistEndPreset = playlistObj[F("end")] | 0; | ||||
|   shuffle = shuffle || playlistObj["r"]; | ||||
|   if (shuffle) playlistOptions += PL_OPTION_SHUFFLE; | ||||
|  | ||||
|   currentPlaylist = 0; //TODO here we need the preset ID where the playlist is saved | ||||
|   currentPlaylist = presetId; | ||||
|   DEBUG_PRINTLN(F("Playlist loaded.")); | ||||
| } | ||||
|  | ||||
|  | ||||
| void handlePlaylist() { | ||||
|   if (currentPlaylist < 0 || playlistEntries == nullptr || presetCyclingEnabled) return; | ||||
|    | ||||
|   if (currentPlaylist < 0 || playlistEntries == nullptr) return; | ||||
|  | ||||
|   if (millis() - presetCycledTime > (100*playlistEntryDur)) { | ||||
|     presetCycledTime = millis(); | ||||
|     if (bri == 0 || nightlightActive) return; | ||||
|  | ||||
|     ++playlistIndex %= playlistLen; // -1 at 1st run (limit to playlistLen) | ||||
|  | ||||
|     if (!playlistRepeat && !playlistIndex) { //stop if repeat == 0 and restart of playlist | ||||
|       unloadPlaylist(); | ||||
|       if (playlistEndPreset) applyPreset(playlistEndPreset); | ||||
|       return; | ||||
|     } | ||||
|     // playlist roll-over | ||||
|     if (!playlistIndex) { | ||||
|       if (playlistRepeat > 0) {// playlistRepeat < 0 => endless loop with shuffling presets | ||||
|         playlistRepeat--; // decrease repeat count on each index reset | ||||
|       } else { | ||||
|         shufflePlaylist();  // shuffle playlist and start over | ||||
|       if (playlistRepeat == 1) { //stop if all repetitions are done | ||||
|         unloadPlaylist(); | ||||
|         if (playlistEndPreset) applyPreset(playlistEndPreset); | ||||
|         return; | ||||
|       } | ||||
|       if (playlistRepeat > 1) playlistRepeat--; // decrease repeat count on each index reset if not an endless playlist | ||||
|       // playlistRepeat == 0: endless loop | ||||
|       if (playlistOptions & PL_OPTION_SHUFFLE) shufflePlaylist(); // shuffle playlist and start over | ||||
|     } | ||||
|  | ||||
|     PlaylistEntry* entries = reinterpret_cast<PlaylistEntry*>(playlistEntries); | ||||
|  | ||||
|     jsonTransitionOnce = true; | ||||
|     transitionDelayTemp = entries[playlistIndex].tr * 100; | ||||
|  | ||||
|     applyPreset(entries[playlistIndex].preset); | ||||
|     playlistEntryDur = entries[playlistIndex].dur; | ||||
|     if (playlistEntryDur == 0) playlistEntryDur = 10; | ||||
|     transitionDelayTemp = playlistEntries[playlistIndex].tr * 100; | ||||
|     playlistEntryDur = playlistEntries[playlistIndex].dur; | ||||
|     applyPreset(playlistEntries[playlistIndex].preset); | ||||
|   } | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user