mirror of
https://github.com/wled/WLED.git
synced 2025-12-18 05:57:56 +00:00
Compare commits
82 Commits
coderabbit
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55d4d6c044 | ||
|
|
d16dc5dfaa | ||
|
|
0605c3b1ef | ||
|
|
76616c02fd | ||
|
|
bb6114e8aa | ||
|
|
c097cb1f27 | ||
|
|
9094b3130d | ||
|
|
32b104e1a9 | ||
|
|
d1260ccf8b | ||
|
|
c35140e763 | ||
|
|
6632a35339 | ||
|
|
6388b8f4bb | ||
|
|
f9a8b3021f | ||
|
|
6a8c6c1f58 | ||
|
|
19bc3c513a | ||
|
|
fa3a94e33e | ||
|
|
5c2177e8d5 | ||
|
|
6e39969cdc | ||
|
|
4b0cf874c9 | ||
|
|
2ff4ee0e1b | ||
|
|
f2a3502445 | ||
|
|
1fee9d4c29 | ||
|
|
b4d3a279e3 | ||
|
|
4684e092a8 | ||
|
|
2a53f415ea | ||
|
|
e074d19593 | ||
|
|
14a728084c | ||
|
|
ead1d6b5f8 | ||
|
|
a421cfeabe | ||
|
|
6f6ac066c9 | ||
|
|
a55a32cc7e | ||
|
|
474c84c9e6 | ||
|
|
a2b64ad332 | ||
|
|
cc5b504771 | ||
|
|
fe33709eb0 | ||
|
|
41b51edbdd | ||
|
|
e6b5429873 | ||
|
|
8cbc76540f | ||
|
|
de01c5e61f | ||
|
|
c114ea6b30 | ||
|
|
bdea3d4959 | ||
|
|
e403f4e0d0 | ||
|
|
3bac2ddae2 | ||
|
|
b98b1b4c78 | ||
|
|
5885a9cc63 | ||
|
|
e306e14b22 | ||
|
|
7b9d643dcd | ||
|
|
f70b359631 | ||
|
|
ae37f4268c | ||
|
|
7c6a1d717d | ||
|
|
a2c1ad01da | ||
|
|
a947e8f35e | ||
|
|
653e03921e | ||
|
|
a0eec81c8a | ||
|
|
9eda32b93a | ||
|
|
e1f5bbf895 | ||
|
|
5d4fdb171e | ||
|
|
4fa4bc8d4b | ||
|
|
b5f13e4331 | ||
|
|
a897271a03 | ||
|
|
75a7ed132a | ||
|
|
fc25eb2c90 | ||
|
|
dc5732a5f5 | ||
|
|
a9811c2020 | ||
|
|
33411f0300 | ||
|
|
8bc434b614 | ||
|
|
ce6577ee35 | ||
|
|
579021f5fc | ||
|
|
17e91a7d2a | ||
|
|
61f5737df2 | ||
|
|
49a25af1f2 | ||
|
|
b6f3cb6394 | ||
|
|
571ab674c3 | ||
|
|
6b607fb545 | ||
|
|
e761418531 | ||
|
|
fca921ee82 | ||
|
|
fc7993f4a7 | ||
|
|
f12e3e03ac | ||
|
|
eb87fbf8e4 | ||
|
|
c534328cc5 | ||
|
|
28d8a1c25c | ||
|
|
bc5d4fed3c |
8
.github/workflows/usermods.yml
vendored
8
.github/workflows/usermods.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: Usermod CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- usermods/**
|
||||
- .github/workflows/usermods.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- usermods/**
|
||||
@@ -12,6 +8,8 @@ on:
|
||||
jobs:
|
||||
|
||||
get_usermod_envs:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
name: Gather Usermods
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -31,6 +29,8 @@ jobs:
|
||||
|
||||
|
||||
build:
|
||||
# Only run for pull requests from forks (not from branches within wled/WLED)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
name: Build Enviornments
|
||||
runs-on: ubuntu-latest
|
||||
needs: get_usermod_envs
|
||||
|
||||
@@ -26,9 +26,28 @@ Github will pick up the changes so your PR stays up-to-date.
|
||||
> It has many subtle and unexpected consequences on our github reposistory.
|
||||
> For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push.
|
||||
|
||||
> [!TIP]
|
||||
> use [cherry-picking](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop) to copy commits from one branch to another.
|
||||
|
||||
You can find a collection of very useful tips and tricks here: https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR
|
||||
|
||||
### Source Code from an AI agent or bot
|
||||
> [!IMPORTANT]
|
||||
> Its OK if you took help from an AI for writing your source code.
|
||||
>
|
||||
> However, we expect a few things from you as the person making a contribution to WLED:
|
||||
* Make sure you really understand the code suggested by the AI, and don't just accept it because it "seems to work".
|
||||
* Don't let the AI change existing code without double-checking by you as the contributor. Often, the result will not be complete. For example, previous source code comments may be lost.
|
||||
* Remember that AI are still "Often-Wrong" ;-)
|
||||
* If you don't feel very confident using English, you can use AI for translating code comments and descriptions into English. AI bots are very good at understanding language. However, always check if the results is correct. The translation might still have wrong technical terms, or errors in some details.
|
||||
|
||||
#### best practice with AI:
|
||||
* As the person who contributes source code to WLED, make sure you understand exactly what the AI generated code does
|
||||
* best practice: add a comment like ``'// below section of my code was generated by an AI``, when larger parts of your source code were not written by you personally.
|
||||
* always review translations and code comments for correctness
|
||||
* always review AI generated source code
|
||||
* If the AI has rewritten existing code, check that the change is necessary and that nothing has been lost or broken. Also check that previous code comments are still intact.
|
||||
|
||||
|
||||
### Code style
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "partitions-8MB-tinyuf2.csv"
|
||||
"partitions": "default_8MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
@@ -43,16 +43,8 @@
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Adafruit MatrixPortal ESP32-S3",
|
||||
"name": "Adafruit MatrixPortal ESP32-S3 for WLED",
|
||||
"upload": {
|
||||
"arduino": {
|
||||
"flash_extra_images": [
|
||||
[
|
||||
"0x410000",
|
||||
"variants/adafruit_matrixportal_esp32s3/tinyuf2.bin"
|
||||
]
|
||||
]
|
||||
},
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
@@ -14,14 +14,14 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wled-dev/WLED.git"
|
||||
"url": "git+https://github.com/wled/WLED.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wled-dev/WLED/issues"
|
||||
"url": "https://github.com/wled/WLED/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wled-dev/WLED#readme",
|
||||
"homepage": "https://github.com/wled/WLED#readme",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
@@ -31,4 +31,4 @@
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ lib_compat_mode = strict
|
||||
lib_deps =
|
||||
fastled/FastLED @ 3.6.0
|
||||
IRremoteESP8266 @ 2.8.2
|
||||
makuna/NeoPixelBus @ 2.8.3
|
||||
https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce
|
||||
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
|
||||
marvinroger/AsyncMqttClient @ 0.9.0
|
||||
# for I2C interface
|
||||
@@ -256,7 +256,7 @@ lib_deps_compat =
|
||||
lib_deps =
|
||||
esp32async/AsyncTCP @ 3.4.7
|
||||
bitbank2/AnimatedGIF@^1.4.7
|
||||
https://github.com/Aircoookie/GifDecoder#bc3af18
|
||||
https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e
|
||||
build_flags =
|
||||
-D CONFIG_ASYNC_TCP_USE_WDT=0
|
||||
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
|
||||
@@ -300,7 +300,7 @@ build_flags = -g
|
||||
-D WLED_ENABLE_DMX_INPUT
|
||||
lib_deps =
|
||||
${esp32_all_variants.lib_deps}
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d
|
||||
https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0
|
||||
${env.lib_deps}
|
||||
|
||||
[esp32s2]
|
||||
|
||||
@@ -580,7 +580,7 @@ build_flags = ${common.build_flags}
|
||||
|
||||
[env:adafruit_matrixportal_esp32s3]
|
||||
; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75
|
||||
board = adafruit_matrixportal_esp32s3
|
||||
board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload
|
||||
;; adafruit recommends to use arduino-esp32 2.0.14
|
||||
;;platform = espressif32@ ~6.5.0
|
||||
;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14
|
||||
|
||||
@@ -26,7 +26,7 @@ const packageJson = require("../package.json");
|
||||
// Export functions for testing
|
||||
module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };
|
||||
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||
|
||||
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
|
||||
const wledBanner = `
|
||||
@@ -38,6 +38,11 @@ const wledBanner = `
|
||||
\t\t\x1b[36m build script for web UI
|
||||
\x1b[0m`;
|
||||
|
||||
// Generate build timestamp as UNIX timestamp (seconds since epoch)
|
||||
function generateBuildTime() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const singleHeader = `/*
|
||||
* Binary array for the Web UI.
|
||||
* gzip is used for smaller size and improved speeds.
|
||||
@@ -45,6 +50,9 @@ const singleHeader = `/*
|
||||
* Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui
|
||||
* to find out how to easily modify the web UI source!
|
||||
*/
|
||||
|
||||
// Automatically generated build time for cache busting (UNIX timestamp)
|
||||
#define WEB_BUILD_TIME ${generateBuildTime()}
|
||||
|
||||
`;
|
||||
|
||||
@@ -126,12 +134,13 @@ async function minify(str, type = "plain") {
|
||||
throw new Error("Unknown filter: " + type);
|
||||
}
|
||||
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page) {
|
||||
async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {
|
||||
console.info("Reading " + sourceFile);
|
||||
inline.html({
|
||||
fileContent: fs.readFileSync(sourceFile, "utf8"),
|
||||
relativeTo: path.dirname(sourceFile),
|
||||
strict: true,
|
||||
strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)
|
||||
stylesheets: inlineCss // when true (default), css is inlined
|
||||
},
|
||||
async function (error, html) {
|
||||
if (error) throw error;
|
||||
@@ -244,8 +253,8 @@ if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.ar
|
||||
|
||||
writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
|
||||
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
||||
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||
writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css
|
||||
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');
|
||||
|
||||
|
||||
|
||||
@@ -313,11 +313,11 @@ class MyExampleUsermod : public Usermod {
|
||||
yield();
|
||||
// ignore certain button types as they may have other consequences
|
||||
if (!enabled
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1335,7 +1335,7 @@ class AudioReactive : public Usermod {
|
||||
disableSoundProcessing = true;
|
||||
} else {
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled"
|
||||
DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed."));
|
||||
DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride));
|
||||
}
|
||||
@@ -1347,7 +1347,7 @@ class AudioReactive : public Usermod {
|
||||
if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode
|
||||
if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source
|
||||
|
||||
|
||||
// Only run the sampling code IF we're not in Receive mode or realtime mode
|
||||
@@ -1544,7 +1544,7 @@ class AudioReactive : public Usermod {
|
||||
// better would be for AudioSource to implement getType()
|
||||
if (enabled
|
||||
&& dmType == 0 && audioPin>=0
|
||||
&& (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)
|
||||
&& (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -562,11 +562,11 @@ void MultiRelay::loop() {
|
||||
bool MultiRelay::handleButton(uint8_t b) {
|
||||
yield();
|
||||
if (!enabled
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -581,20 +581,20 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
unsigned long now = millis();
|
||||
|
||||
//button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
|
||||
if (buttonType[b] == BTN_TYPE_SWITCH) {
|
||||
if (buttons[b].type == BTN_TYPE_SWITCH) {
|
||||
//handleSwitch(b);
|
||||
if (buttonPressedBefore[b] != isButtonPressed(b)) {
|
||||
buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = !buttonPressedBefore[b];
|
||||
if (buttons[b].pressedBefore != isButtonPressed(b)) {
|
||||
buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = !buttons[b].pressedBefore;
|
||||
}
|
||||
|
||||
if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled;
|
||||
if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
switchRelay(i, buttonPressedBefore[b]);
|
||||
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
|
||||
switchRelay(i, buttons[b].pressedBefore);
|
||||
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,40 +604,40 @@ bool MultiRelay::handleButton(uint8_t b) {
|
||||
//momentary button logic
|
||||
if (isButtonPressed(b)) { //pressed
|
||||
|
||||
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = true;
|
||||
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > 600) { //long press
|
||||
if (now - buttons[b].pressedTime > 600) { //long press
|
||||
//longPressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
}
|
||||
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
|
||||
long dur = now - buttonPressedTime[b];
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttonPressedBefore[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
return handled;
|
||||
} //too short "press", debounce
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
//doublePressAction(b); //not exposed
|
||||
//handled = false; //use if you want to pass to default behaviour
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
//shortPressAction(b); //not exposed
|
||||
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
|
||||
if (_relay[i].button == b) {
|
||||
|
||||
@@ -461,11 +461,11 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
#if USING_TFT_DISPLAY
|
||||
bool handleButton(uint8_t b) override {
|
||||
if (!enabled || b > 1 // buttons 0,1 only
|
||||
|| buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE ||
|
||||
buttonType[b] == BTN_TYPE_RESERVED ||
|
||||
buttonType[b] == BTN_TYPE_PIR_SENSOR ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG ||
|
||||
buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE ||
|
||||
buttons[b].type == BTN_TYPE_RESERVED ||
|
||||
buttons[b].type == BTN_TYPE_PIR_SENSOR ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG ||
|
||||
buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -476,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod {
|
||||
static unsigned long buttonWaitTime[2] = {0};
|
||||
|
||||
//momentary button logic
|
||||
if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed
|
||||
if (!buttonPressedBefore[b]) {
|
||||
buttonPressedTime[b] = now;
|
||||
if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed
|
||||
if (!buttons[b].pressedBefore) {
|
||||
buttons[b].pressedTime = now;
|
||||
}
|
||||
buttonPressedBefore[b] = true;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
|
||||
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
|
||||
menu_ctrl.HandleButton(ButtonType::LONG, b);
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
return true;
|
||||
}
|
||||
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
|
||||
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
|
||||
|
||||
long dur = now - buttonPressedTime[b];
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {
|
||||
buttonPressedBefore[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
return true;
|
||||
} //too short "press", debounce
|
||||
|
||||
bool doublePress = buttonWaitTime[b]; //did we have short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
bool doublePress = buttons[b].waitTime; //did we have short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (!buttonLongPressed[b]) { //short press
|
||||
if (!buttons[b].longPressed) { //short press
|
||||
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
menu_ctrl.HandleButton(ButtonType::DOUBLE, b);
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
// if 350ms elapsed since last press/release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS &&
|
||||
!buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&
|
||||
!buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
menu_ctrl.HandleButton(ButtonType::SINGLE, b);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,51 +25,11 @@ class StairwayWipeUsermod : public Usermod {
|
||||
public:
|
||||
void setup() {
|
||||
}
|
||||
/**
|
||||
* @brief Drives the stairway wipe state machine and reacts to user variables.
|
||||
*
|
||||
* @details
|
||||
* Reads userVar0 (U0) and userVar1 (U1) to control a directional stairway color wipe:
|
||||
* - U0 = 0: off.
|
||||
* - U0 = 1: start/keep wipe from local side.
|
||||
* - U0 = 2: start/keep wipe from opposite side.
|
||||
* - U0 = 3: toggle mode for direction 1 (becomes 1 when off, 0 when on).
|
||||
* - U0 = 4: toggle mode for direction 2 (becomes 2 when off, 0 when on).
|
||||
*
|
||||
* Manages a small state machine:
|
||||
* - State 0: idle, will start a wipe.
|
||||
* - State 1: wiping; transitions to static when wipe completes.
|
||||
* - State 2: static/hold; transitions to off after U1 seconds if U1 > 0.
|
||||
* - State 3: prepare to wipe off (or immediately off if off-wipe is disabled).
|
||||
* - State 4: wiping off; turns fully off when wipe-off completes.
|
||||
*
|
||||
* The wipe duration and wipe-off timing are derived from the current effectSpeed. A change
|
||||
* in trigger side (previousUserVar0 differing from userVar0) forces the usermod to begin
|
||||
* turning off. When turning on/off the code invokes startWipe() or turnOff() and issues
|
||||
* color/state update notifications as appropriate.
|
||||
*
|
||||
* @note Defining STAIRCASE_WIPE_OFF enables a reverse color-wipe transition when turning off;
|
||||
* without it the lights fade off immediately.
|
||||
*/
|
||||
void loop() {
|
||||
void loop() {
|
||||
//userVar0 (U0 in HTTP API):
|
||||
//has to be set to 1 if movement is detected on the PIR that is the same side of the staircase as the ESP8266
|
||||
//has to be set to 2 if movement is detected on the PIR that is the opposite side
|
||||
//can be set to 0 if no movement is detected. Otherwise LEDs will turn off after a configurable timeout (userVar1 seconds)
|
||||
//U0 = 3: Toggle mode for direction 1 (if off, turn on with U0=1; if on, turn off with U0=0)
|
||||
//U0 = 4: Toggle mode for direction 2 (if off, turn on with U0=2; if on, turn off with U0=0)
|
||||
|
||||
// Handle toggle modes U0=3 and U0=4
|
||||
if (userVar0 == 3 || userVar0 == 4) {
|
||||
if (wipeState == 0 || wipeState == 3 || wipeState == 4) {
|
||||
// Lights are off or turning off, so turn them on
|
||||
wipeState = 0; // Reset state so the state machine starts fresh
|
||||
userVar0 = (userVar0 == 3) ? 1 : 2;
|
||||
} else {
|
||||
// Lights are on or turning on, so turn them off
|
||||
userVar0 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (userVar0 > 0)
|
||||
{
|
||||
|
||||
@@ -749,12 +749,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) {
|
||||
yield();
|
||||
if (!enabled
|
||||
|| b // button 0 only
|
||||
|| buttonType[b] == BTN_TYPE_SWITCH
|
||||
|| buttonType[b] == BTN_TYPE_NONE
|
||||
|| buttonType[b] == BTN_TYPE_RESERVED
|
||||
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG
|
||||
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
|
||||
|| buttons[b].type == BTN_TYPE_SWITCH
|
||||
|| buttons[b].type == BTN_TYPE_NONE
|
||||
|| buttons[b].type == BTN_TYPE_RESERVED
|
||||
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG
|
||||
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
479
wled00/FX.cpp
479
wled00/FX.cpp
@@ -15,14 +15,25 @@
|
||||
#include "fcn_declare.h"
|
||||
|
||||
#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))
|
||||
#include "FXparticleSystem.h"
|
||||
#include "FXparticleSystem.h" // include particle system code only if at least one system is enabled
|
||||
#ifdef WLED_DISABLE_PARTICLESYSTEM2D
|
||||
#define WLED_PS_DONT_REPLACE_2D_FX
|
||||
#endif
|
||||
#ifdef WLED_DISABLE_PARTICLESYSTEM1D
|
||||
#define WLED_PS_DONT_REPLACE_1D_FX
|
||||
#endif
|
||||
#ifdef ESP8266
|
||||
#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D)
|
||||
#error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.
|
||||
#error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
#define WLED_PS_DONT_REPLACE_FX
|
||||
#define WLED_PS_DONT_REPLACE_1D_FX
|
||||
#define WLED_PS_DONT_REPLACE_2D_FX
|
||||
#endif
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#define WLED_PS_DONT_REPLACE_1D_FX
|
||||
#define WLED_PS_DONT_REPLACE_2D_FX
|
||||
#endif
|
||||
|
||||
//////////////
|
||||
@@ -161,7 +172,7 @@ uint16_t mode_copy_segment(void) {
|
||||
} else { // 1D source, source can be expanded into 2D
|
||||
for (unsigned i = 0; i < SEGMENT.vLength(); i++) {
|
||||
if(SEGMENT.check2) {
|
||||
sourcecolor = strip.getPixelColor(i + sourcesegment.start); // read from global buffer (reads the last rendered frame)
|
||||
sourcecolor = strip.getPixelColorNoMap(i + sourcesegment.start); // read from global buffer (reads the last rendered frame)
|
||||
}
|
||||
else {
|
||||
sourcesegment.setDrawDimensions(); // set to source segment dimensions
|
||||
@@ -713,7 +724,7 @@ uint16_t dissolve(uint32_t color) {
|
||||
if (SEGENV.aux0) { //dissolve to primary/palette
|
||||
if (pixels[i] == SEGCOLOR(1)) {
|
||||
pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color;
|
||||
break; //only spawn 1 new pixel per frame per 50 LEDs
|
||||
break; //only spawn 1 new pixel per frame
|
||||
}
|
||||
} else { //dissolve to secondary
|
||||
if (pixels[i] != SEGCOLOR(1)) {
|
||||
@@ -724,14 +735,27 @@ uint16_t dissolve(uint32_t color) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// fix for #4401
|
||||
for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.setPixelColor(i, pixels[i]);
|
||||
unsigned incompletePixels = 0;
|
||||
for (unsigned i = 0; i < SEGLEN; i++) {
|
||||
SEGMENT.setPixelColor(i, pixels[i]); // fix for #4401
|
||||
if (SEGMENT.check2) {
|
||||
if (SEGENV.aux0) {
|
||||
if (pixels[i] == SEGCOLOR(1)) incompletePixels++;
|
||||
} else {
|
||||
if (pixels[i] != SEGCOLOR(1)) incompletePixels++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (SEGENV.step > (255 - SEGMENT.speed) + 15U) {
|
||||
SEGENV.aux0 = !SEGENV.aux0;
|
||||
SEGENV.step = 0;
|
||||
} else {
|
||||
SEGENV.step++;
|
||||
if (SEGMENT.check2) {
|
||||
if (incompletePixels == 0)
|
||||
SEGENV.step++; // only advance step once all pixels have changed
|
||||
} else
|
||||
SEGENV.step++;
|
||||
}
|
||||
|
||||
return FRAMETIME;
|
||||
@@ -744,7 +768,7 @@ uint16_t dissolve(uint32_t color) {
|
||||
uint16_t mode_dissolve(void) {
|
||||
return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0));
|
||||
}
|
||||
static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random;!,!;!";
|
||||
static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random,Complete;!,!;!";
|
||||
|
||||
|
||||
/*
|
||||
@@ -755,7 +779,6 @@ uint16_t mode_dissolve_random(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat speed,Dissolve speed;,!;!";
|
||||
|
||||
|
||||
/*
|
||||
* Blinks one LED at a time.
|
||||
* Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
|
||||
@@ -777,7 +800,6 @@ uint16_t mode_sparkle(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_SPARKLE[] PROGMEM = "Sparkle@!,,,,,,Overlay;!,!;!;;m12=0";
|
||||
|
||||
|
||||
/*
|
||||
* Lights all LEDs in the color. Flashes single col 1 pixels randomly. (List name: Sparkle Dark)
|
||||
* Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
|
||||
@@ -1752,7 +1774,6 @@ uint16_t mode_tricolor_fade(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!";
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
/*
|
||||
* Creates random comets
|
||||
* Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h
|
||||
@@ -1791,7 +1812,6 @@ uint16_t mode_multi_comet(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1";
|
||||
#undef MAX_COMETS
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
|
||||
/*
|
||||
* Running random pixels ("Stream 2")
|
||||
@@ -2118,7 +2138,7 @@ uint16_t mode_palette() {
|
||||
}
|
||||
static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1";
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
|
||||
// WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active
|
||||
// Fire2012 by Mark Kriegsman, July 2012
|
||||
// as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY
|
||||
@@ -2205,7 +2225,7 @@ uint16_t mode_fire_2012() {
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_x_FX
|
||||
|
||||
// colored stripes pulsing at a defined Beats-Per-Minute (BPM)
|
||||
uint16_t mode_bpm() {
|
||||
@@ -3056,7 +3076,7 @@ uint16_t mode_bouncing_balls(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
/*
|
||||
* bouncing balls on a track track Effect modified from Aircoookie's bouncing balls
|
||||
* Courtesy of pjhatch (https://github.com/pjhatch)
|
||||
@@ -3156,7 +3176,203 @@ static uint16_t rolling_balls(void) {
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_1D_FX
|
||||
|
||||
|
||||
/*
|
||||
/ Pac-Man by Bob Loeffler with help from @dedehai and @blazoncek
|
||||
* speed slider is for speed.
|
||||
* intensity slider is for selecting the number of power dots.
|
||||
* custom1 slider is for selecting the LED where the ghosts will start blinking blue.
|
||||
* custom2 slider is for blurring the LEDs in the segment.
|
||||
* custom3 slider is for selecting the # of ghosts (between 2 and 8).
|
||||
* check1 is for displaying White Dots that PacMan eats. Enabled will show white dots. Disabled will not show any white dots (all leds will be black).
|
||||
* check2 is for Smear mode (enabled will smear/persist the LED colors, disabled will not).
|
||||
* check3 is for the Compact Dots mode of displaying white dots. Enabled will show white dots in every LED. Disabled will show black LEDs between the white dots.
|
||||
* aux0 is used to keep track of the previous number of power dots in case the user selects a different number with the intensity slider.
|
||||
* aux1 is the main counter for timing.
|
||||
*/
|
||||
typedef struct PacManChars {
|
||||
signed pos;
|
||||
signed topPos; // LED position of farthest PacMan has moved
|
||||
uint32_t color;
|
||||
bool direction; // true = moving away from first LED
|
||||
bool blue; // used for ghosts only
|
||||
bool eaten; // used for power dots only
|
||||
} pacmancharacters_t;
|
||||
|
||||
static uint16_t mode_pacman(void) {
|
||||
constexpr unsigned ORANGEYELLOW = 0xFFCC00;
|
||||
constexpr unsigned PURPLEISH = 0xB000B0;
|
||||
constexpr unsigned ORANGEISH = 0xFF8800;
|
||||
constexpr unsigned WHITEISH = 0x999999;
|
||||
constexpr unsigned PACMAN = 0; // PacMan is character[0]
|
||||
constexpr uint32_t ghostColors[] = {RED, PURPLEISH, CYAN, ORANGEISH};
|
||||
|
||||
unsigned maxPowerDots = min(SEGLEN / 10U, 255U); // cap the max so packed state fits in 8 bits
|
||||
unsigned numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots);
|
||||
unsigned numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8);
|
||||
bool smearMode = SEGMENT.check2;
|
||||
|
||||
// Pack two 8-bit values into one 16-bit field (stored in SEGENV.aux0)
|
||||
uint16_t combined_value = uint16_t(((numPowerDots & 0xFF) << 8) | (numGhosts & 0xFF));
|
||||
if (combined_value != SEGENV.aux0) SEGENV.call = 0; // Reinitialize on setting change
|
||||
SEGENV.aux0 = combined_value;
|
||||
|
||||
// Allocate segment data
|
||||
unsigned dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1); // +1 is the PacMan character
|
||||
if (SEGLEN <= 16 + (2*numGhosts) || !SEGENV.allocateData(dataSize)) return mode_static();
|
||||
pacmancharacters_t *character = reinterpret_cast<pacmancharacters_t *>(SEGENV.data);
|
||||
|
||||
// Calculate when blue ghosts start blinking.
|
||||
// On first call (or after settings change), `topPos` is not known yet, so fall back to the full segment length in that case.
|
||||
int maxBlinkPos = (SEGENV.call == 0) ? (int)SEGLEN - 1 : character[PACMAN].topPos;
|
||||
if (maxBlinkPos < 20) maxBlinkPos = 20;
|
||||
int startBlinkingGhostsLED = (SEGLEN < 64)
|
||||
? (int)SEGLEN / 3
|
||||
: map(SEGMENT.custom1, 0, 255, 20, maxBlinkPos);
|
||||
|
||||
// Initialize characters on first call
|
||||
if (SEGENV.call == 0) {
|
||||
// Initialize PacMan
|
||||
character[PACMAN].color = YELLOW;
|
||||
character[PACMAN].pos = 0;
|
||||
character[PACMAN].topPos = 0;
|
||||
character[PACMAN].direction = true;
|
||||
character[PACMAN].blue = false;
|
||||
|
||||
// Initialize ghosts with alternating colors
|
||||
for (int i = 1; i <= numGhosts; i++) {
|
||||
character[i].color = ghostColors[(i-1) % 4];
|
||||
character[i].pos = -2 * (i + 1);
|
||||
character[i].direction = true;
|
||||
character[i].blue = false;
|
||||
}
|
||||
|
||||
// Initialize power dots
|
||||
for (int i = 0; i < numPowerDots; i++) {
|
||||
character[i + numGhosts + 1].color = ORANGEYELLOW;
|
||||
character[i + numGhosts + 1].eaten = false;
|
||||
}
|
||||
character[numGhosts + 1].pos = SEGLEN - 1; // Last power dot at end
|
||||
}
|
||||
|
||||
if (strip.now > SEGENV.step) {
|
||||
SEGENV.step = strip.now;
|
||||
SEGENV.aux1++;
|
||||
}
|
||||
|
||||
// Clear background if not in smear mode
|
||||
if (!smearMode) SEGMENT.fill(BLACK);
|
||||
|
||||
// Draw white dots in front of PacMan if option selected
|
||||
if (SEGMENT.check1) {
|
||||
int step = SEGMENT.check3 ? 1 : 2; // Compact or spaced dots
|
||||
for (int i = SEGLEN - 1; i > character[PACMAN].topPos; i -= step) {
|
||||
SEGMENT.setPixelColor(i, WHITEISH);
|
||||
}
|
||||
}
|
||||
|
||||
// Update power dot positions dynamically
|
||||
uint32_t everyXLeds = (((uint32_t)SEGLEN - 10U) << 8) / numPowerDots; // Fixed-point spacing for power dots: use 32-bit math to avoid overflow on long segments.
|
||||
for (int i = 1; i < numPowerDots; i++) {
|
||||
character[i + numGhosts + 1].pos = 10 + ((i * everyXLeds) >> 8);
|
||||
}
|
||||
|
||||
// Blink power dots every 10 ticks
|
||||
if (SEGENV.aux1 % 10 == 0) {
|
||||
uint32_t dotColor = (character[numGhosts + 1].color == ORANGEYELLOW) ? BLACK : ORANGEYELLOW;
|
||||
for (int i = 0; i < numPowerDots; i++) {
|
||||
character[i + numGhosts + 1].color = dotColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Blink blue ghosts when nearing start
|
||||
if (SEGENV.aux1 % 15 == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) {
|
||||
uint32_t ghostColor = (character[1].color == BLUE) ? WHITEISH : BLUE;
|
||||
for (int i = 1; i <= numGhosts; i++) {
|
||||
character[i].color = ghostColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw uneaten power dots
|
||||
for (int i = 0; i < numPowerDots; i++) {
|
||||
if (!character[i + numGhosts + 1].eaten && (unsigned)character[i + numGhosts + 1].pos < SEGLEN) {
|
||||
SEGMENT.setPixelColor(character[i + numGhosts + 1].pos, character[i + numGhosts + 1].color);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if PacMan ate a power dot
|
||||
for (int j = 0; j < numPowerDots; j++) {
|
||||
auto &dot = character[j + numGhosts + 1];
|
||||
if (character[PACMAN].pos == dot.pos && !dot.eaten) {
|
||||
// Reverse all characters - PacMan now chases ghosts
|
||||
for (int i = 0; i <= numGhosts; i++) {
|
||||
character[i].direction = false;
|
||||
}
|
||||
// Turn ghosts blue
|
||||
for (int i = 1; i <= numGhosts; i++) {
|
||||
character[i].color = BLUE;
|
||||
character[i].blue = true;
|
||||
}
|
||||
dot.eaten = true;
|
||||
break; // only one power dot per frame
|
||||
}
|
||||
}
|
||||
|
||||
// Reset when PacMan reaches start with blue ghosts
|
||||
if (character[1].blue && character[PACMAN].pos <= 0) {
|
||||
// Reverse direction back
|
||||
for (int i = 0; i <= numGhosts; i++) {
|
||||
character[i].direction = true;
|
||||
}
|
||||
// Reset ghost colors
|
||||
for (int i = 1; i <= numGhosts; i++) {
|
||||
character[i].color = ghostColors[(i-1) % 4];
|
||||
character[i].blue = false;
|
||||
}
|
||||
// Reset power dots if last one was eaten
|
||||
if (character[numGhosts + 1].eaten) {
|
||||
for (int i = 0; i < numPowerDots; i++) {
|
||||
character[i + numGhosts + 1].eaten = false;
|
||||
}
|
||||
character[PACMAN].topPos = 0; // set the top position of PacMan to LED 0 (beginning of the segment)
|
||||
}
|
||||
}
|
||||
|
||||
// Update and draw characters based on speed setting
|
||||
bool updatePositions = (SEGENV.aux1 % map(SEGMENT.speed, 0, 255, 15, 1) == 0);
|
||||
|
||||
// update positions of characters if it's time to do so
|
||||
if (updatePositions) {
|
||||
character[PACMAN].pos += character[PACMAN].direction ? 1 : -1;
|
||||
for (int i = 1; i <= numGhosts; i++) {
|
||||
character[i].pos += character[i].direction ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw PacMan
|
||||
if ((unsigned)character[PACMAN].pos < SEGLEN) {
|
||||
SEGMENT.setPixelColor(character[PACMAN].pos, character[PACMAN].color);
|
||||
}
|
||||
|
||||
// Draw ghosts
|
||||
for (int i = 1; i <= numGhosts; i++) {
|
||||
if ((unsigned)character[i].pos < SEGLEN) {
|
||||
SEGMENT.setPixelColor(character[i].pos, character[i].color);
|
||||
}
|
||||
}
|
||||
|
||||
// Track farthest position of PacMan
|
||||
if (character[PACMAN].topPos < character[PACMAN].pos) {
|
||||
character[PACMAN].topPos = character[PACMAN].pos;
|
||||
}
|
||||
|
||||
SEGMENT.blur(SEGMENT.custom2>>1);
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_PACMAN[] PROGMEM = "PacMan@Speed,# of PowerDots,Blink distance,Blur,# of Ghosts,Dots,Smear,Compact;;!;1;m12=0,sx=192,ix=64,c1=64,c2=0,c3=12,o1=1,o2=0";
|
||||
|
||||
|
||||
/*
|
||||
* Sinelon stolen from FASTLED examples
|
||||
@@ -3213,7 +3429,6 @@ uint16_t mode_sinelon_rainbow(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!";
|
||||
|
||||
|
||||
// utility function that will add random glitter to SEGMENT
|
||||
void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) {
|
||||
if (intensity > hw_random8()) SEGMENT.setPixelColor(hw_random16(SEGLEN), col);
|
||||
@@ -3418,7 +3633,7 @@ uint16_t mode_candle_multi()
|
||||
}
|
||||
static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0";
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
/*
|
||||
/ Fireworks in starburst effect
|
||||
/ based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/
|
||||
@@ -3550,9 +3765,9 @@ uint16_t mode_starburst(void) {
|
||||
}
|
||||
#undef STARBURST_MAX_FRAG
|
||||
static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0";
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_1DFX
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
|
||||
/*
|
||||
* Exploding fireworks effect
|
||||
* adapted from: http://www.anirama.com/1000leds/1d-fireworks/
|
||||
@@ -3690,7 +3905,7 @@ uint16_t mode_exploding_fireworks(void)
|
||||
}
|
||||
#undef MAX_SPARKS
|
||||
static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128";
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_x_FX
|
||||
|
||||
/*
|
||||
* Drip Effect
|
||||
@@ -4338,7 +4553,7 @@ static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"
|
||||
#define SPOT_MAX_COUNT 49 //Number of simultaneous waves
|
||||
#endif
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
//13 bytes
|
||||
typedef struct Spotlight {
|
||||
float speed;
|
||||
@@ -4472,7 +4687,7 @@ uint16_t mode_dancing_shadows(void)
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!";
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_1D_FX
|
||||
|
||||
/*
|
||||
Imitates a washing machine, rotating same waves forward, then pause, then backward.
|
||||
@@ -6033,7 +6248,7 @@ uint16_t mode_2Dcrazybees(void) {
|
||||
static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0";
|
||||
#undef MAX_BEES
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_2D_FX
|
||||
/////////////////////////
|
||||
// 2D Ghost Rider //
|
||||
/////////////////////////
|
||||
@@ -6221,7 +6436,7 @@ uint16_t mode_2Dfloatingblobs(void) {
|
||||
}
|
||||
static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8";
|
||||
#undef MAX_BLOBS
|
||||
#endif // WLED_PS_DONT_REPLACE_FX
|
||||
#endif // WLED_PS_DONT_REPLACE_2D_FX
|
||||
|
||||
////////////////////////////
|
||||
// 2D Scrolling text //
|
||||
@@ -8224,7 +8439,7 @@ uint16_t mode_particlefire(void) {
|
||||
uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results
|
||||
|
||||
if (SEGMENT.call == 0) { // initialization
|
||||
if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall)
|
||||
if (!initParticleSystem2D(PartSys, SEGMENT.vWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall)
|
||||
return mode_static(); // allocation failed or not 2D
|
||||
SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise
|
||||
}
|
||||
@@ -8264,10 +8479,10 @@ uint16_t mode_particlefire(void) {
|
||||
PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width
|
||||
PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame
|
||||
PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed
|
||||
PartSys->sources[i].maxLife = hw_random16(SEGMENT.virtualHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height
|
||||
PartSys->sources[i].maxLife = hw_random16(SEGMENT.vHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height
|
||||
PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1;
|
||||
PartSys->sources[i].vx = hw_random16(5) - 2; // emitting speed (sideways)
|
||||
PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards)
|
||||
PartSys->sources[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards)
|
||||
PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var)
|
||||
}
|
||||
}
|
||||
@@ -8297,11 +8512,11 @@ uint16_t mode_particlefire(void) {
|
||||
if(hw_random8() < 10 + (SEGMENT.intensity >> 2)) {
|
||||
for (i = 0; i < PartSys->usedParticles; i++) {
|
||||
if (PartSys->particles[i].ttl == 0) { // find a dead particle
|
||||
PartSys->particles[i].ttl = hw_random16(SEGMENT.virtualHeight()) + 30;
|
||||
PartSys->particles[i].ttl = hw_random16(SEGMENT.vHeight()) + 30;
|
||||
PartSys->particles[i].x = PartSys->sources[0].source.x;
|
||||
PartSys->particles[i].y = PartSys->sources[0].source.y;
|
||||
PartSys->particles[i].vx = PartSys->sources[0].source.vx;
|
||||
PartSys->particles[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards)
|
||||
PartSys->particles[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards)
|
||||
break; // emit only one particle
|
||||
}
|
||||
}
|
||||
@@ -8367,11 +8582,12 @@ uint16_t mode_particlepit(void) {
|
||||
PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7;
|
||||
// set particle size
|
||||
if (SEGMENT.custom1 == 255) {
|
||||
PartSys->setParticleSize(1); // set global size to 1 for advanced rendering (no single pixel particles)
|
||||
PartSys->perParticleSize = true;
|
||||
PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size
|
||||
} else {
|
||||
PartSys->perParticleSize = false;
|
||||
PartSys->setParticleSize(SEGMENT.custom1); // set global size
|
||||
PartSys->advPartProps[i].size = 0; // use global size
|
||||
PartSys->advPartProps[i].size = SEGMENT.custom1; // also set individual size for consistency
|
||||
}
|
||||
break; // emit only one particle per round
|
||||
}
|
||||
@@ -8389,7 +8605,7 @@ uint16_t mode_particlepit(void) {
|
||||
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o3=1";
|
||||
static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=70,c2=180,c3=31,o3=1";
|
||||
|
||||
/*
|
||||
Particle Waterfall
|
||||
@@ -8473,7 +8689,7 @@ uint16_t mode_particlebox(void) {
|
||||
uint32_t i;
|
||||
|
||||
if (SEGMENT.call == 0) { // initialization
|
||||
if (!initParticleSystem2D(PartSys, 1)) // init
|
||||
if (!initParticleSystem2D(PartSys, 1, 0, true)) // init
|
||||
return mode_static(); // allocation failed or not 2D
|
||||
PartSys->setBounceX(true);
|
||||
PartSys->setBounceY(true);
|
||||
@@ -8486,19 +8702,26 @@ uint16_t mode_particlebox(void) {
|
||||
return mode_static(); // something went wrong, no data!
|
||||
|
||||
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
|
||||
PartSys->setParticleSize(SEGMENT.custom3<<3);
|
||||
PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more
|
||||
PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness
|
||||
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153)); // 1% - 60%
|
||||
int maxParticleSize = min(((SEGMENT.vWidth() * SEGMENT.vHeight()) >> 2), 255U); // max particle size based on matrix size
|
||||
unsigned currentParticleSize = map(SEGMENT.custom3, 0, 31, 0, maxParticleSize);
|
||||
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153) / (1 + (currentParticleSize >> 4))); // 1% - 60%, reduce if using larger size
|
||||
if (SEGMENT.custom3 < 31)
|
||||
PartSys->setParticleSize(currentParticleSize); // set global size if not max (resets perParticleSize)
|
||||
else
|
||||
PartSys->perParticleSize = true; // per particle size, uses advPartProps.size (randomized below)
|
||||
|
||||
// add in new particles if amount has changed
|
||||
for (i = 0; i < PartSys->usedParticles; i++) {
|
||||
if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles
|
||||
if (PartSys->particles[i].ttl < 260) { // initialize dead particles
|
||||
PartSys->particles[i].ttl = 260; // full brigthness
|
||||
PartSys->particles[i].x = hw_random16(PartSys->maxX);
|
||||
PartSys->particles[i].y = hw_random16(PartSys->maxY);
|
||||
PartSys->particles[i].hue = hw_random8(); // make it colorful
|
||||
PartSys->particleFlags[i].perpetual = true; // never die
|
||||
PartSys->particleFlags[i].collide = true; // all particles colllide
|
||||
PartSys->advPartProps[i].size = hw_random8(maxParticleSize); // random size, used only if size is set to max (SEGMENT.custom3=31)
|
||||
break; // only spawn one particle per frame for less chaotic transitions
|
||||
}
|
||||
}
|
||||
@@ -8754,22 +8977,10 @@ uint16_t mode_particleattractor(void) {
|
||||
|
||||
// Particle System settings
|
||||
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
|
||||
attractor = reinterpret_cast<PSparticle *>(PartSys->PSdataEnd);
|
||||
|
||||
PartSys->setColorByAge(SEGMENT.check1);
|
||||
PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally
|
||||
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190));
|
||||
|
||||
if (SEGMENT.custom2 > 0) // collisions enabled
|
||||
PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness
|
||||
else
|
||||
PartSys->enableParticleCollisions(false);
|
||||
|
||||
if (SEGMENT.call == 0) {
|
||||
attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y
|
||||
attractor->vy = PartSys->sources[0].source.vx;
|
||||
}
|
||||
|
||||
attractor = reinterpret_cast<PSparticle *>(PartSys->PSdataEnd);
|
||||
// set attractor properties
|
||||
attractor->ttl = 100; // never dies
|
||||
if (SEGMENT.check2) {
|
||||
@@ -8780,6 +8991,15 @@ uint16_t mode_particleattractor(void) {
|
||||
attractor->x = PartSys->maxX >> 1; // set to center
|
||||
attractor->y = PartSys->maxY >> 1;
|
||||
}
|
||||
if (SEGMENT.call == 0) {
|
||||
attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y
|
||||
attractor->vy = PartSys->sources[0].source.vx;
|
||||
}
|
||||
|
||||
if (SEGMENT.custom2 > 0) // collisions enabled
|
||||
PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness
|
||||
else
|
||||
PartSys->enableParticleCollisions(false);
|
||||
|
||||
if (SEGMENT.call % 5 == 0)
|
||||
PartSys->sources[0].source.hue++;
|
||||
@@ -8791,13 +9011,11 @@ uint16_t mode_particleattractor(void) {
|
||||
PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well
|
||||
// apply force
|
||||
uint32_t strength = SEGMENT.speed;
|
||||
#ifdef USERMOD_AUDIOREACTIVE
|
||||
um_data_t *um_data;
|
||||
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data
|
||||
uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255
|
||||
strength = (SEGMENT.speed * volumeSmth) >> 8;
|
||||
}
|
||||
#endif
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3);
|
||||
}
|
||||
@@ -8809,6 +9027,7 @@ uint16_t mode_particleattractor(void) {
|
||||
PartSys->update(); // update and render
|
||||
return FRAMETIME;
|
||||
}
|
||||
//static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=1,c2=0";
|
||||
static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=2,c2=0";
|
||||
|
||||
/*
|
||||
@@ -8855,7 +9074,6 @@ uint16_t mode_particlespray(void) {
|
||||
PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY);
|
||||
uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8;
|
||||
|
||||
#ifdef USERMOD_AUDIOREACTIVE
|
||||
um_data_t *um_data;
|
||||
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data
|
||||
uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255
|
||||
@@ -8879,17 +9097,6 @@ uint16_t mode_particlespray(void) {
|
||||
PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// change source properties
|
||||
if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles
|
||||
PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic
|
||||
PartSys->sources[0].minLife = 100;
|
||||
PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source
|
||||
// PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32)
|
||||
// spray[j].source.hue = hw_random16(); //set random color for each particle (using palette)
|
||||
PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2);
|
||||
}
|
||||
#endif
|
||||
|
||||
PartSys->update(); // update and render
|
||||
return FRAMETIME;
|
||||
@@ -9138,6 +9345,7 @@ uint16_t mode_particleblobs(void) {
|
||||
PartSys->setWallHardness(255);
|
||||
PartSys->setWallRoughness(255);
|
||||
PartSys->setCollisionHardness(255);
|
||||
PartSys->perParticleSize = true; // enable per particle size control
|
||||
}
|
||||
else
|
||||
PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS
|
||||
@@ -9180,16 +9388,14 @@ uint16_t mode_particleblobs(void) {
|
||||
SEGENV.aux0 = SEGMENT.speed; //write state back
|
||||
SEGENV.aux1 = SEGMENT.custom1;
|
||||
|
||||
#ifdef USERMOD_AUDIOREACTIVE
|
||||
um_data_t *um_data;
|
||||
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data
|
||||
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data if available, do not use simulated data
|
||||
uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]);
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles
|
||||
if (SEGMENT.check3) //pulsate selected
|
||||
PartSys->advPartProps[i].size = volumeSmth;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7);
|
||||
PartSys->update(); // update and render
|
||||
@@ -9228,8 +9434,6 @@ uint16_t mode_particlegalaxy(void) {
|
||||
// Particle System settings
|
||||
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
|
||||
uint8_t particlesize = SEGMENT.custom1;
|
||||
if(SEGMENT.check3)
|
||||
particlesize = SEGMENT.custom1 ? 1 : 0; // set size to 0 (single pixel) or 1 (quad pixel) so motion blur works and adds streaks
|
||||
PartSys->setParticleSize(particlesize); // set size globally
|
||||
PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1
|
||||
|
||||
@@ -9297,7 +9501,7 @@ uint16_t mode_particlegalaxy(void) {
|
||||
PartSys->update(); // update and render
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=2,c3=4";
|
||||
static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=1,c3=4";
|
||||
|
||||
#endif //WLED_DISABLE_PARTICLESYSTEM2D
|
||||
#endif // WLED_DISABLE_2D
|
||||
@@ -9400,7 +9604,7 @@ uint16_t mode_particleDrip(void) {
|
||||
if (PartSys->particles[i].hue < 245)
|
||||
PartSys->particles[i].hue += 8;
|
||||
}
|
||||
//increase speed on high settings by calling the move function twice
|
||||
//increase speed on high settings by calling the move function twice note: this can lead to missed collisions
|
||||
if (SEGMENT.speed > 200)
|
||||
PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]);
|
||||
}
|
||||
@@ -9412,8 +9616,8 @@ static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Sp
|
||||
|
||||
|
||||
/*
|
||||
Particle Replacement for "Bbouncing Balls by Aircoookie"
|
||||
Also replaces rolling balls and juggle (and maybe popcorn)
|
||||
Particle Version of "Bouncing Balls by Aircoookie"
|
||||
Also does rolling balls and juggle (and popcorn)
|
||||
Uses palette for particle color
|
||||
by DedeHai (Damian Schneider)
|
||||
*/
|
||||
@@ -9424,10 +9628,10 @@ uint16_t mode_particlePinball(void) {
|
||||
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init
|
||||
return mode_static(); // allocation failed or is single pixel
|
||||
PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled)
|
||||
PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom
|
||||
PartSys->setKillOutOfBounds(true); // out of bounds particles dont return
|
||||
PartSys->sources[0].source.x = -1000; // shoot up from below
|
||||
//PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)
|
||||
SEGENV.aux0 = 1;
|
||||
SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call
|
||||
SEGENV.aux1 = 5000; // set settings out of range to ensure uptate on first call
|
||||
}
|
||||
else
|
||||
PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS
|
||||
@@ -9438,62 +9642,71 @@ uint16_t mode_particlePinball(void) {
|
||||
// Particle System settings
|
||||
//uint32_t hardness = 240 + (SEGMENT.custom1>>4);
|
||||
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
|
||||
PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength)
|
||||
PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 8)); // set gravity (8 is default strength)
|
||||
PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used
|
||||
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur
|
||||
PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness
|
||||
PartSys->setUsedParticles(SEGMENT.intensity);
|
||||
PartSys->setColorByPosition(SEGMENT.check3);
|
||||
uint32_t maxParticles = max(20, SEGMENT.intensity / (1 + (SEGMENT.check2 * (SEGMENT.custom1 >> 5)))); // max particles depends on intensity and rolling balls mode + size
|
||||
if (SEGMENT.custom1 < 255)
|
||||
PartSys->setParticleSize(SEGMENT.custom1); // set size globally
|
||||
else {
|
||||
PartSys->perParticleSize = true; // use random individual particle size (see below)
|
||||
maxParticles *= 2; // use more particles if individual s ize is used as there is more space
|
||||
}
|
||||
PartSys->setUsedParticles(maxParticles); // reduce if using larger size and rolling balls mode
|
||||
|
||||
bool updateballs = false;
|
||||
if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available
|
||||
SEGENV.step = SEGMENT.call; // reset delay
|
||||
updateballs = true;
|
||||
PartSys->sources[0].maxLife = SEGMENT.custom3 ? 5000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed)
|
||||
PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed)
|
||||
PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1;
|
||||
}
|
||||
|
||||
if (SEGMENT.check2) { //rolling balls
|
||||
if (SEGMENT.check2) { // rolling balls
|
||||
PartSys->setGravity(0);
|
||||
PartSys->setWallHardness(255);
|
||||
int speedsum = 0;
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
PartSys->particles[i].ttl = 260; // keep particles alive
|
||||
if (updateballs) { //speed changed or particle is dead, set particle properties
|
||||
PartSys->particles[i].ttl = 500; // keep particles alive
|
||||
if (updateballs) { // speed changed or particle is dead, set particle properties
|
||||
PartSys->particleFlags[i].collide = true;
|
||||
if (PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS)
|
||||
if (PartSys->particles[i].x == 0) { // still at initial position
|
||||
PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles
|
||||
PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction
|
||||
}
|
||||
PartSys->particles[i].hue = hw_random8(); //set ball colors to random
|
||||
PartSys->advPartProps[i].sat = 255;
|
||||
PartSys->advPartProps[i].size = SEGMENT.custom1;
|
||||
PartSys->advPartProps[i].size = hw_random8(); // set ball size for individual size mode
|
||||
}
|
||||
speedsum += abs(PartSys->particles[i].vx);
|
||||
}
|
||||
int32_t avgSpeed = speedsum / PartSys->usedParticles;
|
||||
int32_t setSpeed = 2 + (SEGMENT.speed >> 3);
|
||||
int32_t setSpeed = 2 + (SEGMENT.speed >> 2);
|
||||
if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going
|
||||
for (int i = 0; i < setSpeed - avgSpeed; i++) {
|
||||
int idx = hw_random16(PartSys->usedParticles);
|
||||
PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction
|
||||
if (abs(PartSys->particles[idx].vx) < PS_P_MAXSPEED)
|
||||
PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction
|
||||
}
|
||||
}
|
||||
else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down
|
||||
PartSys->applyFriction(1);
|
||||
}
|
||||
else { //bouncing balls
|
||||
else { // bouncing balls
|
||||
PartSys->setWallHardness(220);
|
||||
PartSys->sources[0].var = SEGMENT.speed >> 3;
|
||||
int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3);
|
||||
PartSys->sources[0].v = newspeed;
|
||||
//check for balls that are 'laying on the ground' and remove them
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1))
|
||||
PartSys->particles[i].ttl = 0;
|
||||
if (PartSys->particles[i].ttl < 50) PartSys->particles[i].ttl = 0; // no dark particles
|
||||
else if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1))
|
||||
PartSys->particles[i].ttl -= 50; // age fast
|
||||
|
||||
if (updateballs) {
|
||||
PartSys->advPartProps[i].size = SEGMENT.custom1;
|
||||
if (SEGMENT.custom3 == 0) //gravity off, update speed
|
||||
if (SEGMENT.custom3 == 0) // gravity off, update speed
|
||||
PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction
|
||||
}
|
||||
}
|
||||
@@ -9504,14 +9717,14 @@ uint16_t mode_particlePinball(void) {
|
||||
SEGENV.step += interval + hw_random16(interval);
|
||||
PartSys->sources[0].source.hue = hw_random16(); //set ball color
|
||||
PartSys->sources[0].sat = 255;
|
||||
PartSys->sources[0].size = SEGMENT.custom1;
|
||||
PartSys->sources[0].size = hw_random8(); //set ball size
|
||||
PartSys->sprayEmit(PartSys->sources[0]);
|
||||
}
|
||||
}
|
||||
SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles;
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed
|
||||
}
|
||||
//for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
// PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed note: this leads to bad collisions, also need to run collision detection before
|
||||
//}
|
||||
|
||||
PartSys->update(); // update and render
|
||||
return FRAMETIME;
|
||||
@@ -9859,7 +10072,7 @@ uint16_t mode_particleHourglass(void) {
|
||||
PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8));
|
||||
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur
|
||||
PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30));
|
||||
PartSys->enableParticleCollisions(true, 32); // hardness value found by experimentation on different settings
|
||||
PartSys->enableParticleCollisions(true, 64); // hardness value (found by experimentation on different settings)
|
||||
|
||||
uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7
|
||||
|
||||
@@ -9873,6 +10086,12 @@ uint16_t mode_particleHourglass(void) {
|
||||
SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle
|
||||
}
|
||||
|
||||
// re-order particles in case heavy collisions flipped particles (highest number index particle is on the "bottom")
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) {
|
||||
if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) {
|
||||
std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);
|
||||
}
|
||||
}
|
||||
// calculate target position depending on direction
|
||||
auto calcTargetPos = [&](size_t i) {
|
||||
return PartSys->particleFlags[i].reversegrav ?
|
||||
@@ -9880,12 +10099,12 @@ uint16_t mode_particleHourglass(void) {
|
||||
: (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset;
|
||||
};
|
||||
|
||||
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling
|
||||
if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) {
|
||||
int32_t targetposition = calcTargetPos(i);
|
||||
bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D;
|
||||
if (closeToTarget) { // close to target and slow speed
|
||||
bool belowtarget = PartSys->particleFlags[i].reversegrav ? (PartSys->particles[i].x > targetposition) : (PartSys->particles[i].x < targetposition);
|
||||
bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < PS_P_RADIUS_1D;
|
||||
if (belowtarget || closeToTarget) { // overshot target or close to target and slow speed
|
||||
PartSys->particles[i].x = targetposition; // set exact position
|
||||
PartSys->particleFlags[i].fixed = true; // pin particle
|
||||
}
|
||||
@@ -9899,25 +10118,17 @@ uint16_t mode_particleHourglass(void) {
|
||||
case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34)
|
||||
case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color
|
||||
case 2: // 2 colors inverleaved (same code as 3)
|
||||
case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % colormode)*74; break; // interleved colors (every 2 or 3 particles)
|
||||
case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % 3)*74; break; // 3 interleved colors
|
||||
case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors
|
||||
case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors
|
||||
case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient
|
||||
default: break;
|
||||
default: break; // use color by position
|
||||
}
|
||||
}
|
||||
if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen
|
||||
PartSys->particles[i].hue += 120;
|
||||
}
|
||||
|
||||
// re-order particles in case collisions flipped particles (highest number index particle is on the "bottom")
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) {
|
||||
if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) {
|
||||
std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles
|
||||
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
|
||||
PartSys->particleFlags[i].collide = true;
|
||||
@@ -9929,19 +10140,19 @@ uint16_t mode_particleHourglass(void) {
|
||||
}
|
||||
|
||||
if (SEGENV.aux1 == 0) { // countdown passed, run
|
||||
if (strip.now >= SEGENV.step) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly
|
||||
if (strip.now >= SEGENV.step) { // drop a particle
|
||||
// set next drop time
|
||||
if (SEGMENT.check3 && *direction) // fast reset
|
||||
SEGENV.step = strip.now + 100; // drop one particle every 100ms
|
||||
else // normal interval
|
||||
SEGENV.step = strip.now + max(20, SEGMENT.speed * 20); // map speed slider from 0.1s to 5s
|
||||
SEGENV.step = strip.now + max(100, SEGMENT.speed * 100); // map speed slider from 0.1s to 25.5s
|
||||
if (SEGENV.aux0 < PartSys->usedParticles) {
|
||||
PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise
|
||||
PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin
|
||||
}
|
||||
else { // overflow
|
||||
*direction = !(*direction); // flip direction
|
||||
SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown
|
||||
SEGENV.aux1 = (SEGMENT.check2) * SEGMENT.vLength() + 100; // set restart countdown, make it short if auto start is unchecked
|
||||
}
|
||||
if (*direction == 0) // down, start dropping the highest number particle
|
||||
SEGENV.aux0--; // next particle
|
||||
@@ -9949,14 +10160,14 @@ uint16_t mode_particleHourglass(void) {
|
||||
SEGENV.aux0++;
|
||||
}
|
||||
}
|
||||
else if (SEGMENT.check2) // auto reset
|
||||
else if (SEGMENT.check2) // auto start/reset
|
||||
SEGENV.aux1--; // countdown
|
||||
|
||||
PartSys->update(); // update and render
|
||||
|
||||
return FRAMETIME;
|
||||
}
|
||||
static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=50,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1";
|
||||
static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=5,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1";
|
||||
|
||||
/*
|
||||
Particle based Spray effect (like a volcano, possible replacement for popcorn)
|
||||
@@ -10073,7 +10284,7 @@ uint16_t mode_particleBalance(void) {
|
||||
}
|
||||
|
||||
uint32_t randomindex = hw_random16(PartSys->usedParticles);
|
||||
PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions)
|
||||
PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping
|
||||
|
||||
//if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out
|
||||
if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt)
|
||||
@@ -10099,7 +10310,7 @@ by DedeHai (Damian Schneider)
|
||||
uint16_t mode_particleChase(void) {
|
||||
ParticleSystem1D *PartSys = nullptr;
|
||||
if (SEGMENT.call == 0) { // initialization
|
||||
if (!initParticleSystem1D(PartSys, 1, 255, 2, true)) // init
|
||||
if (!initParticleSystem1D(PartSys, 1, 191, 2, true)) // init
|
||||
return mode_static(); // allocation failed or is single pixel
|
||||
SEGENV.aux0 = 0xFFFF; // invalidate
|
||||
*PartSys->PSdataEnd = 1; // huedir
|
||||
@@ -10113,15 +10324,17 @@ uint16_t mode_particleChase(void) {
|
||||
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
|
||||
PartSys->setColorByPosition(SEGMENT.check3);
|
||||
PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur
|
||||
uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1), minimum 1
|
||||
uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 0, PartSys->usedParticles / (1 + (SEGMENT.custom1 >> 5))); // depends on intensity and particle size (custom1), minimum 1
|
||||
numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles
|
||||
int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment
|
||||
uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3;
|
||||
if (SEGENV.aux0 != settingssum) { // settings changed changed, update
|
||||
if (SEGMENT.check1)
|
||||
SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles);
|
||||
else
|
||||
SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / numParticles; // spacing between particles
|
||||
else {
|
||||
SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 6)) / numParticles; // spacing between particles
|
||||
SEGENV.step = (SEGENV.step / PS_P_RADIUS_1D) * PS_P_RADIUS_1D; // round down to nearest multiple of particle subpixel unit to align to pixel grid (makes them move in union)
|
||||
}
|
||||
for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {
|
||||
PartSys->advPartProps[i].sat = 255;
|
||||
PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0)
|
||||
@@ -10587,7 +10800,7 @@ by DedeHai (Damian Schneider)
|
||||
uint16_t mode_particleSpringy(void) {
|
||||
ParticleSystem1D *PartSys = nullptr;
|
||||
if (SEGMENT.call == 0) { // initialization
|
||||
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init
|
||||
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init with advanced properties (used for spring forces)
|
||||
return mode_static(); // allocation failed or is single pixel
|
||||
SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings
|
||||
}
|
||||
@@ -10600,18 +10813,20 @@ uint16_t mode_particleSpringy(void) {
|
||||
PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur
|
||||
PartSys->setSmearBlur(50); // smear a little
|
||||
PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255 >> (SEGMENT.check2*2))); // depends on density and particle size
|
||||
// PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft.
|
||||
//PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft.
|
||||
int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles)
|
||||
int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness)
|
||||
|
||||
uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2;
|
||||
PartSys->setParticleSize(SEGMENT.check2 ? 120 : 1); // large or small particles
|
||||
|
||||
if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution
|
||||
for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {
|
||||
PartSys->advPartProps[i].sat = 255; // full saturation
|
||||
//PartSys->particleFlags[i].collide = true; // enable collision for particles
|
||||
//PartSys->particleFlags[i].collide = true; // enable collision for particles -> results in chaos, removed for now
|
||||
PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute
|
||||
//PartSys->particles[i].vx = 0; //reset speed
|
||||
PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big
|
||||
//PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big -> use global size
|
||||
}
|
||||
SEGENV.aux0 = settingssum;
|
||||
}
|
||||
@@ -10703,7 +10918,6 @@ uint16_t mode_particleSpringy(void) {
|
||||
int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10
|
||||
int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed);
|
||||
if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles
|
||||
//PartSys->applyForce(PartSys->particles[index], (sin16_t(phase) * amplitude) >> 15, PartSys->advPartProps[index].forcecounter); // apply acceleration
|
||||
PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position
|
||||
}
|
||||
else {
|
||||
@@ -10873,16 +11087,18 @@ void WS2812FX::setupEffectData() {
|
||||
addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS);
|
||||
addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE);
|
||||
addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET);
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET);
|
||||
addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS);
|
||||
#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)
|
||||
addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012);
|
||||
addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS);
|
||||
#endif
|
||||
addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE);
|
||||
addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER);
|
||||
addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER);
|
||||
addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET);
|
||||
#ifdef WLED_PS_DONT_REPLACE_1D_FX
|
||||
addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS);
|
||||
addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST);
|
||||
addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS);
|
||||
addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012);
|
||||
addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS);
|
||||
#endif
|
||||
addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE);
|
||||
addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS);
|
||||
@@ -10909,6 +11125,7 @@ void WS2812FX::setupEffectData() {
|
||||
addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS);
|
||||
addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR);
|
||||
addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH);
|
||||
addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN);
|
||||
|
||||
// --- 1D audio effects ---
|
||||
addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS);
|
||||
@@ -10946,7 +11163,7 @@ void WS2812FX::setupEffectData() {
|
||||
addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS);
|
||||
addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES);
|
||||
|
||||
#ifdef WLED_PS_DONT_REPLACE_FX
|
||||
#ifdef WLED_PS_DONT_REPLACE_2D_FX
|
||||
addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER);
|
||||
addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS);
|
||||
#endif
|
||||
|
||||
@@ -310,6 +310,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
|
||||
#define FX_MODE_2DFIRENOISE 149
|
||||
#define FX_MODE_2DSQUAREDSWIRL 150
|
||||
// #define FX_MODE_2DFIRE2012 151
|
||||
#define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable.
|
||||
#define FX_MODE_2DDNA 152
|
||||
#define FX_MODE_2DMATRIX 153
|
||||
#define FX_MODE_2DMETABALLS 154
|
||||
@@ -965,7 +966,8 @@ class WS2812FX {
|
||||
};
|
||||
|
||||
unsigned long now, timebase;
|
||||
inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n
|
||||
inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds
|
||||
inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table
|
||||
inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call
|
||||
|
||||
const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); }
|
||||
|
||||
@@ -458,7 +458,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
return;
|
||||
}
|
||||
if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D
|
||||
stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth);
|
||||
stop = i2 > Segment::maxWidth*Segment::maxHeight && i1 >= Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); // check for 2D trailing strip
|
||||
startY = 0;
|
||||
stopY = 1;
|
||||
#ifndef WLED_DISABLE_2D
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
#include <stdint.h>
|
||||
#include "wled.h"
|
||||
|
||||
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8)
|
||||
#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8), limiting below 127 to avoid overflows in collisions due to rounding errors
|
||||
#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!)
|
||||
|
||||
//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash
|
||||
@@ -103,7 +103,7 @@ typedef union {
|
||||
|
||||
// struct for additional particle settings (option)
|
||||
typedef struct { // 2 bytes
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering)
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable
|
||||
uint8_t forcecounter; // counter for applying forces to individual particles
|
||||
} PSadvancedParticle;
|
||||
|
||||
@@ -190,16 +190,18 @@ public:
|
||||
int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1
|
||||
uint32_t numSources; // number of sources
|
||||
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
|
||||
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
|
||||
//note: some variables are 32bit for speed and code size at the cost of ram
|
||||
|
||||
private:
|
||||
//rendering functions
|
||||
void render();
|
||||
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
|
||||
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
|
||||
//paricle physics applied by system if flags are set
|
||||
void applyGravity(); // applies gravity to all particles
|
||||
void handleCollisions();
|
||||
[[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq);
|
||||
void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2);
|
||||
void fireParticleupdate();
|
||||
//utility functions
|
||||
void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space
|
||||
@@ -226,12 +228,26 @@ private:
|
||||
uint8_t smearBlur; // 2D smeared blurring of full frame
|
||||
};
|
||||
|
||||
void blur2D(uint32_t *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false);
|
||||
// initialization functions (not part of class)
|
||||
bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false);
|
||||
uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol);
|
||||
uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources);
|
||||
bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes);
|
||||
|
||||
// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center
|
||||
inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) {
|
||||
// square the distances
|
||||
uint32_t dx_sq = dx * dx;
|
||||
uint32_t dy_sq = dy * dy;
|
||||
|
||||
uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256
|
||||
|
||||
if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0
|
||||
//if (dist_sq <= 96) return maxBrightness; // core at full brightness
|
||||
int32_t falloff = 256 - dist_sq;
|
||||
return (maxBrightness * falloff) >> 8; // linear falloff
|
||||
//return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges
|
||||
}
|
||||
#endif // WLED_DISABLE_PARTICLESYSTEM2D
|
||||
|
||||
////////////////////////
|
||||
@@ -301,9 +317,9 @@ typedef union {
|
||||
|
||||
// struct for additional particle settings (optional)
|
||||
typedef struct {
|
||||
uint8_t sat; //color saturation
|
||||
uint8_t sat; // color saturation
|
||||
uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting
|
||||
uint8_t forcecounter;
|
||||
uint8_t forcecounter; // counter for applying forces to individual particles
|
||||
} PSadvancedParticle1D;
|
||||
|
||||
//struct for a particle source (20 bytes)
|
||||
@@ -346,10 +362,11 @@ public:
|
||||
void setColorByPosition(const bool enable);
|
||||
void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero
|
||||
void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame
|
||||
void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled if advanced particle is used
|
||||
void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called
|
||||
void setGravity(int8_t force = 8);
|
||||
void enableParticleCollisions(bool enable, const uint8_t hardness = 255);
|
||||
|
||||
|
||||
PSparticle1D *particles; // pointer to particle array
|
||||
PSparticleFlags1D *particleFlags; // pointer to particle flags array
|
||||
PSsource1D *sources; // pointer to sources
|
||||
@@ -360,16 +377,18 @@ public:
|
||||
int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1
|
||||
uint32_t numSources; // number of sources
|
||||
uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'
|
||||
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
|
||||
|
||||
private:
|
||||
//rendering functions
|
||||
void render(void);
|
||||
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
|
||||
void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
|
||||
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap);
|
||||
|
||||
//paricle physics applied by system if flags are set
|
||||
void applyGravity(); // applies gravity to all particles
|
||||
void handleCollisions();
|
||||
[[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance);
|
||||
void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance);
|
||||
|
||||
//utility functions
|
||||
void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space
|
||||
@@ -397,5 +416,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso
|
||||
uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced);
|
||||
uint32_t calculateNumberOfSources1D(const uint32_t requestedsources);
|
||||
bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes);
|
||||
void blur1D(uint32_t *colorbuffer, uint32_t size, uint32_t blur, uint32_t start);
|
||||
|
||||
#endif // WLED_DISABLE_PARTICLESYSTEM1D
|
||||
|
||||
@@ -17,13 +17,13 @@ static bool buttonBriDirection = false; // true: increase brightness, false: dec
|
||||
|
||||
void shortPressAction(uint8_t b)
|
||||
{
|
||||
if (!macroButton[b]) {
|
||||
if (!buttons[b].macroButton) {
|
||||
switch (b) {
|
||||
case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
|
||||
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -38,7 +38,7 @@ void shortPressAction(uint8_t b)
|
||||
|
||||
void longPressAction(uint8_t b)
|
||||
{
|
||||
if (!macroLongPress[b]) {
|
||||
if (!buttons[b].macroLongPress) {
|
||||
switch (b) {
|
||||
case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;
|
||||
case 1:
|
||||
@@ -52,11 +52,11 @@ void longPressAction(uint8_t b)
|
||||
else bri -= WLED_LONG_BRI_STEPS;
|
||||
}
|
||||
stateUpdated(CALL_MODE_BUTTON);
|
||||
buttonPressedTime[b] = millis();
|
||||
buttons[b].pressedTime = millis();
|
||||
break; // repeatable action
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -71,13 +71,13 @@ void longPressAction(uint8_t b)
|
||||
|
||||
void doublePressAction(uint8_t b)
|
||||
{
|
||||
if (!macroDoublePress[b]) {
|
||||
if (!buttons[b].macroDoublePress) {
|
||||
switch (b) {
|
||||
//case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
|
||||
case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
|
||||
}
|
||||
} else {
|
||||
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
|
||||
applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);
|
||||
}
|
||||
|
||||
#ifndef WLED_DISABLE_MQTT
|
||||
@@ -92,10 +92,10 @@ void doublePressAction(uint8_t b)
|
||||
|
||||
bool isButtonPressed(uint8_t b)
|
||||
{
|
||||
if (btnPin[b]<0) return false;
|
||||
unsigned pin = btnPin[b];
|
||||
if (buttons[b].pin < 0) return false;
|
||||
unsigned pin = buttons[b].pin;
|
||||
|
||||
switch (buttonType[b]) {
|
||||
switch (buttons[b].type) {
|
||||
case BTN_TYPE_NONE:
|
||||
case BTN_TYPE_RESERVED:
|
||||
break;
|
||||
@@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t b)
|
||||
#ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)
|
||||
if (touchInterruptGetLastStatus(pin)) return true;
|
||||
#else
|
||||
if (digitalPinToTouchChannel(btnPin[b]) >= 0 && touchRead(pin) <= touchThreshold) return true;
|
||||
if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
@@ -124,25 +124,25 @@ bool isButtonPressed(uint8_t b)
|
||||
void handleSwitch(uint8_t b)
|
||||
{
|
||||
// isButtonPressed() handles inverted/noninverted logic
|
||||
if (buttonPressedBefore[b] != isButtonPressed(b)) {
|
||||
if (buttons[b].pressedBefore != isButtonPressed(b)) {
|
||||
DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b);
|
||||
buttonPressedTime[b] = millis();
|
||||
buttonPressedBefore[b] = !buttonPressedBefore[b];
|
||||
buttons[b].pressedTime = millis();
|
||||
buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state
|
||||
}
|
||||
|
||||
if (buttonLongPressed[b] == buttonPressedBefore[b]) return;
|
||||
if (buttons[b].longPressed == buttons[b].pressedBefore) return;
|
||||
|
||||
if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
|
||||
DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b);
|
||||
if (!buttonPressedBefore[b]) { // on -> off
|
||||
if (!buttons[b].pressedBefore) { // on -> off
|
||||
DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b);
|
||||
if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
|
||||
if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
|
||||
else { //turn on
|
||||
if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
|
||||
}
|
||||
} else { // off -> on
|
||||
DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b);
|
||||
if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
|
||||
if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
|
||||
else { //turn off
|
||||
if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
|
||||
}
|
||||
@@ -152,13 +152,13 @@ void handleSwitch(uint8_t b)
|
||||
// publish MQTT message
|
||||
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
|
||||
char subuf[MQTT_MAX_TOPIC_LEN + 32];
|
||||
if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
|
||||
if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
|
||||
else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
|
||||
mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on");
|
||||
mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on");
|
||||
}
|
||||
#endif
|
||||
|
||||
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
|
||||
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,17 +178,17 @@ void handleAnalog(uint8_t b)
|
||||
#ifdef ESP8266
|
||||
rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit
|
||||
#else
|
||||
if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
|
||||
rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution
|
||||
if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
|
||||
rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution
|
||||
#endif
|
||||
yield(); // keep WiFi task running - analog read may take several millis on ESP8266
|
||||
|
||||
filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
|
||||
unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit
|
||||
if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
|
||||
if(aRead >= 255-POT_SENSITIVITY) aRead = 255;
|
||||
if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
|
||||
if (aRead >= 255-POT_SENSITIVITY) aRead = 255;
|
||||
|
||||
if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
|
||||
if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
|
||||
|
||||
// remove noise & reduce frequency of UI updates
|
||||
if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading
|
||||
@@ -206,10 +206,10 @@ void handleAnalog(uint8_t b)
|
||||
oldRead[b] = aRead;
|
||||
|
||||
// if no macro for "short press" and "long press" is defined use brightness control
|
||||
if (!macroButton[b] && !macroLongPress[b]) {
|
||||
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]);
|
||||
if (!buttons[b].macroButton && !buttons[b].macroLongPress) {
|
||||
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress);
|
||||
// if "double press" macro defines which option to change
|
||||
if (macroDoublePress[b] >= 250) {
|
||||
if (buttons[b].macroDoublePress >= 250) {
|
||||
// global brightness
|
||||
if (aRead == 0) {
|
||||
briLast = bri;
|
||||
@@ -218,27 +218,30 @@ void handleAnalog(uint8_t b)
|
||||
if (bri == 0) strip.restartRuntime();
|
||||
bri = aRead;
|
||||
}
|
||||
} else if (macroDoublePress[b] == 249) {
|
||||
} else if (buttons[b].macroDoublePress == 249) {
|
||||
// effect speed
|
||||
effectSpeed = aRead;
|
||||
} else if (macroDoublePress[b] == 248) {
|
||||
} else if (buttons[b].macroDoublePress == 248) {
|
||||
// effect intensity
|
||||
effectIntensity = aRead;
|
||||
} else if (macroDoublePress[b] == 247) {
|
||||
} else if (buttons[b].macroDoublePress == 247) {
|
||||
// selected palette
|
||||
effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);
|
||||
effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result
|
||||
} else if (macroDoublePress[b] == 200) {
|
||||
} else if (buttons[b].macroDoublePress == 200) {
|
||||
// primary color, hue, full saturation
|
||||
colorHStoRGB(aRead*256,255,colPri);
|
||||
colorHStoRGB(aRead*256, 255, colPri);
|
||||
} else {
|
||||
// otherwise use "double press" for segment selection
|
||||
Segment& seg = strip.getSegment(macroDoublePress[b]);
|
||||
Segment& seg = strip.getSegment(buttons[b].macroDoublePress);
|
||||
if (aRead == 0) {
|
||||
seg.setOption(SEG_OPTION_ON, false); // off (use transition)
|
||||
seg.on = false; // do not use transition
|
||||
//seg.setOption(SEG_OPTION_ON, false); // off (use transition)
|
||||
} else {
|
||||
seg.setOpacity(aRead);
|
||||
seg.setOption(SEG_OPTION_ON, true); // on (use transition)
|
||||
seg.opacity = aRead; // set brightness (opacity) of segment
|
||||
seg.on = true;
|
||||
//seg.setOpacity(aRead);
|
||||
//seg.setOption(SEG_OPTION_ON, true); // on (use transition)
|
||||
}
|
||||
// this will notify clients of update (websockets,mqtt,etc)
|
||||
updateInterfaces(CALL_MODE_BUTTON);
|
||||
@@ -261,16 +264,16 @@ void handleButton()
|
||||
if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
|
||||
lastRun = now;
|
||||
|
||||
for (unsigned b=0; b<WLED_MAX_BUTTONS; b++) {
|
||||
for (unsigned b = 0; b < buttons.size(); b++) {
|
||||
#ifdef ESP8266
|
||||
if ((btnPin[b]<0 && !(buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)) || buttonType[b] == BTN_TYPE_NONE) continue;
|
||||
if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;
|
||||
#else
|
||||
if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue;
|
||||
if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;
|
||||
#endif
|
||||
|
||||
if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons
|
||||
|
||||
if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
|
||||
if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
|
||||
if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
|
||||
handleAnalog(b);
|
||||
}
|
||||
@@ -278,7 +281,7 @@ void handleButton()
|
||||
}
|
||||
|
||||
// button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
|
||||
if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_TOUCH_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) {
|
||||
if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {
|
||||
handleSwitch(b);
|
||||
continue;
|
||||
}
|
||||
@@ -287,40 +290,39 @@ void handleButton()
|
||||
if (isButtonPressed(b)) { // pressed
|
||||
|
||||
// if all macros are the same, fire action immediately on rising edge
|
||||
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
|
||||
if (!buttonPressedBefore[b])
|
||||
shortPressAction(b);
|
||||
buttonPressedBefore[b] = true;
|
||||
buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler)
|
||||
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
|
||||
if (!buttons[b].pressedBefore) shortPressAction(b);
|
||||
buttons[b].pressedBefore = true;
|
||||
buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
|
||||
buttonPressedBefore[b] = true;
|
||||
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
|
||||
buttons[b].pressedBefore = true;
|
||||
|
||||
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
|
||||
if (!buttonLongPressed[b]) {
|
||||
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
|
||||
if (!buttons[b].longPressed) {
|
||||
buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press
|
||||
longPressAction(b);
|
||||
} else if (b) { //repeatable action (~5 times per s) on button > 0
|
||||
longPressAction(b);
|
||||
buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms
|
||||
buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms
|
||||
}
|
||||
buttonLongPressed[b] = true;
|
||||
buttons[b].longPressed = true;
|
||||
}
|
||||
|
||||
} else if (buttonPressedBefore[b]) { //released
|
||||
long dur = now - buttonPressedTime[b];
|
||||
} else if (buttons[b].pressedBefore) { //released
|
||||
long dur = now - buttons[b].pressedTime;
|
||||
|
||||
// released after rising-edge short press action
|
||||
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
|
||||
if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released
|
||||
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
|
||||
if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce
|
||||
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
|
||||
buttonWaitTime[b] = 0;
|
||||
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce
|
||||
bool doublePress = buttons[b].waitTime; //did we have a short press before?
|
||||
buttons[b].waitTime = 0;
|
||||
|
||||
if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
|
||||
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
|
||||
@@ -332,25 +334,25 @@ void handleButton()
|
||||
} else {
|
||||
WLED::instance().initAP(true);
|
||||
}
|
||||
} else if (!buttonLongPressed[b]) { //short press
|
||||
} else if (!buttons[b].longPressed) { //short press
|
||||
//NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
|
||||
if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set
|
||||
if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set
|
||||
shortPressAction(b);
|
||||
} else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
|
||||
if (doublePress) {
|
||||
doublePressAction(b);
|
||||
} else {
|
||||
buttonWaitTime[b] = now;
|
||||
buttons[b].waitTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
buttonPressedBefore[b] = false;
|
||||
buttonLongPressed[b] = false;
|
||||
buttons[b].pressedBefore = false;
|
||||
buttons[b].longPressed = false;
|
||||
}
|
||||
|
||||
//if 350ms elapsed since last short press release it is a short press
|
||||
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) {
|
||||
buttonWaitTime[b] = 0;
|
||||
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {
|
||||
buttons[b].waitTime = 0;
|
||||
shortPressAction(b);
|
||||
}
|
||||
}
|
||||
|
||||
126
wled00/cfg.cpp
126
wled00/cfg.cpp
@@ -345,97 +345,91 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
JsonArray hw_btn_ins = btn_obj["ins"];
|
||||
if (!hw_btn_ins.isNull()) {
|
||||
// deallocate existing button pins
|
||||
for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
|
||||
for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
|
||||
buttons.clear(); // clear existing buttons
|
||||
unsigned s = 0;
|
||||
for (JsonObject btn : hw_btn_ins) {
|
||||
CJSON(buttonType[s], btn["type"]);
|
||||
int8_t pin = btn["pin"][0] | -1;
|
||||
uint8_t type = btn["type"] | BTN_TYPE_NONE;
|
||||
int8_t pin = btn["pin"][0] | -1;
|
||||
if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {
|
||||
btnPin[s] = pin;
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// ESP32 only: check that analog button pin is a valid ADC gpio
|
||||
if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(btnPin[s]) < 0) {
|
||||
if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(pin) < 0) {
|
||||
// not an ADC analog pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s);
|
||||
btnPin[s] = -1;
|
||||
PinManager::deallocatePin(pin,PinOwner::Button);
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s);
|
||||
PinManager::deallocatePin(pin, PinOwner::Button);
|
||||
pin = -1;
|
||||
continue;
|
||||
} else {
|
||||
analogReadResolution(12); // see #4040
|
||||
}
|
||||
}
|
||||
else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH))
|
||||
{
|
||||
if (digitalPinToTouchChannel(btnPin[s]) < 0) {
|
||||
} else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {
|
||||
if (digitalPinToTouchChannel(pin) < 0) {
|
||||
// not a touch pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s);
|
||||
btnPin[s] = -1;
|
||||
PinManager::deallocatePin(pin,PinOwner::Button);
|
||||
}
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s);
|
||||
PinManager::deallocatePin(pin, PinOwner::Button);
|
||||
pin = -1;
|
||||
continue;
|
||||
}
|
||||
//if touch pin, enable the touch interrupt on ESP32 S2 & S3
|
||||
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so
|
||||
else
|
||||
{
|
||||
touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
}
|
||||
else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
#endif
|
||||
}
|
||||
else
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// regular buttons and switches
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[s], INPUT);
|
||||
pinMode(pin, INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[s], INPUT_PULLUP);
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
btnPin[s] = -1;
|
||||
JsonArray hw_btn_ins_0_macros = btn["macros"];
|
||||
uint8_t press = hw_btn_ins_0_macros[0] | 0;
|
||||
uint8_t longPress = hw_btn_ins_0_macros[1] | 0;
|
||||
uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;
|
||||
buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector
|
||||
}
|
||||
JsonArray hw_btn_ins_0_macros = btn["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 if (fromFS) {
|
||||
// new install/missing configuration (button 0 has defaults)
|
||||
// relies upon only being called once with fromFS == true, which is currently true.
|
||||
for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) {
|
||||
if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) {
|
||||
btnPin[s] = -1;
|
||||
buttonType[s] = BTN_TYPE_NONE;
|
||||
constexpr uint8_t defTypes[] = {BTNTYPE};
|
||||
constexpr int8_t defPins[] = {BTNPIN};
|
||||
constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0]));
|
||||
constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0]));
|
||||
// check if the number of pins and types are valid; count of pins must be greater than or equal to types
|
||||
static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE");
|
||||
|
||||
uint8_t type = BTN_TYPE_NONE;
|
||||
buttons.clear(); // clear existing buttons (just in case)
|
||||
for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {
|
||||
type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins
|
||||
if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {
|
||||
if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)
|
||||
continue; // pin not available or invalid, skip configuring this GPIO
|
||||
}
|
||||
if (btnPin[s] >= 0) {
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[s], INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[s], INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
if (disablePullUp) {
|
||||
pinMode(defPins[s], INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(defPins[s], INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
macroButton[s] = 0;
|
||||
macroLongPress[s] = 0;
|
||||
macroDoublePress[s] = 0;
|
||||
buttons.emplace_back(defPins[s], type); // add button to vector
|
||||
}
|
||||
}
|
||||
|
||||
CJSON(buttonPublishMqtt,btn_obj["mqtt"]);
|
||||
CJSON(buttonPublishMqtt, btn_obj["mqtt"]);
|
||||
|
||||
#ifndef WLED_DISABLE_INFRARED
|
||||
int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
|
||||
@@ -1016,15 +1010,15 @@ void serializeConfig(JsonObject root) {
|
||||
JsonArray hw_btn_ins = hw_btn.createNestedArray("ins");
|
||||
|
||||
// configuration for all buttons
|
||||
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
|
||||
for (const auto &button : buttons) {
|
||||
JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();
|
||||
hw_btn_ins_0["type"] = buttonType[i];
|
||||
hw_btn_ins_0["type"] = button.type;
|
||||
JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin");
|
||||
hw_btn_ins_0_pin.add(btnPin[i]);
|
||||
hw_btn_ins_0_pin.add(button.pin);
|
||||
JsonArray 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_ins_0_macros.add(button.macroButton);
|
||||
hw_btn_ins_0_macros.add(button.macroLongPress);
|
||||
hw_btn_ins_0_macros.add(button.macroDoublePress);
|
||||
}
|
||||
|
||||
hw_btn[F("tt")] = touchThreshold;
|
||||
|
||||
@@ -102,9 +102,9 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
|
||||
#ifndef WLED_MAX_BUTTONS
|
||||
#ifdef ESP8266
|
||||
#define WLED_MAX_BUTTONS 2
|
||||
#define WLED_MAX_BUTTONS 10
|
||||
#else
|
||||
#define WLED_MAX_BUTTONS 4
|
||||
#define WLED_MAX_BUTTONS 32
|
||||
#endif
|
||||
#else
|
||||
#if WLED_MAX_BUTTONS < 2
|
||||
@@ -366,7 +366,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define BTN_TYPE_TOUCH_SWITCH 9
|
||||
|
||||
//Ethernet board types
|
||||
#define WLED_NUM_ETH_TYPES 13
|
||||
#define WLED_NUM_ETH_TYPES 14
|
||||
|
||||
|
||||
#define WLED_ETH_NONE 0
|
||||
#define WLED_ETH_WT32_ETH01 1
|
||||
@@ -381,6 +382,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define WLED_ETH_SERG74 10
|
||||
#define WLED_ETH_ESP32_POE_WROVER 11
|
||||
#define WLED_ETH_LILYGO_T_POE_PRO 12
|
||||
#define WLED_ETH_GLEDOPTO 13
|
||||
|
||||
//Hue error codes
|
||||
#define HUE_ERROR_INACTIVE 0
|
||||
|
||||
@@ -51,6 +51,38 @@ function tooltip(cont=null) {
|
||||
});
|
||||
});
|
||||
};
|
||||
// sequential loading of external resources (JS or CSS) with retry, calls init() when done
|
||||
function loadResources(files, init) {
|
||||
let i = 0;
|
||||
const loadNext = () => {
|
||||
if (i >= files.length) {
|
||||
if (init) {
|
||||
d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display)
|
||||
d.readyState === 'complete' ? init() : window.addEventListener('load', init);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const file = files[i++];
|
||||
const isCSS = file.endsWith('.css');
|
||||
const el = d.createElement(isCSS ? 'link' : 'script');
|
||||
if (isCSS) {
|
||||
el.rel = 'stylesheet';
|
||||
el.href = file;
|
||||
const st = d.head.querySelector('style');
|
||||
if (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides
|
||||
else d.head.appendChild(el);
|
||||
} else {
|
||||
el.src = file;
|
||||
d.head.appendChild(el);
|
||||
}
|
||||
el.onload = () => { loadNext(); };
|
||||
el.onerror = () => {
|
||||
i--; // load this file again
|
||||
setTimeout(loadNext, 100);
|
||||
};
|
||||
};
|
||||
loadNext();
|
||||
}
|
||||
// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript
|
||||
function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) {
|
||||
let scE = d.createElement("script");
|
||||
@@ -116,21 +148,22 @@ function uploadFile(fileObj, name) {
|
||||
fileObj.value = '';
|
||||
return false;
|
||||
}
|
||||
// connect to WebSocket, use parent WS or open new
|
||||
// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object
|
||||
function connectWs(onOpen) {
|
||||
try {
|
||||
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
|
||||
if (onOpen) onOpen();
|
||||
return top.window.ws;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
getLoc(); // ensure globals (loc, locip, locproto) are up to date
|
||||
let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
|
||||
let ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
if (onOpen) { ws.onopen = onOpen; }
|
||||
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
|
||||
let ws;
|
||||
try { ws = top.window.ws;} catch (e) {}
|
||||
// reuse if open
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
if (onOpen) onOpen(ws);
|
||||
} else {
|
||||
// create new ws connection
|
||||
getLoc(); // ensure globals are up to date
|
||||
let url = loc ? getURL('/ws').replace("http", "ws")
|
||||
: "ws://" + window.location.hostname + "/ws";
|
||||
ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
if (onOpen) ws.onopen = () => onOpen(ws);
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
|
||||
@@ -794,7 +794,7 @@ input[type=range]::-moz-range-thumb {
|
||||
/* buttons */
|
||||
.btn {
|
||||
padding: 8px;
|
||||
/*margin: 10px 4px;*/
|
||||
margin: 10px 4px;
|
||||
width: 230px;
|
||||
font-size: 19px;
|
||||
color: var(--c-d);
|
||||
@@ -1476,7 +1476,7 @@ dialog {
|
||||
.expanded {
|
||||
display: inline-block !important;
|
||||
}
|
||||
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon {
|
||||
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
<div style="padding: 8px 0;" id="btns">
|
||||
<button class="btn btn-xs" title="File editor" type="button" id="edit" onclick="window.location.href=getURL('/edit')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="Pixel Magic Tool" type="button" id="pxmb" onclick="window.location.href=getURL('/pxmagic.htm')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="PixelForge" type="button" onclick="window.location.href=getURL('/pixelforge.htm')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="Add custom palette" type="button" id="adPal" onclick="window.location.href=getURL('/cpal.htm')"><i class="icons btn-icon"></i></button>
|
||||
<button class="btn btn-xs" title="Remove last custom palette" type="button" id="rmPal" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon"></i></button>
|
||||
</div>
|
||||
|
||||
@@ -16,13 +16,12 @@ var simplifiedUI = false;
|
||||
var tr = 7;
|
||||
var d = document;
|
||||
const ranges = RangeTouch.setup('input[type="range"]', {});
|
||||
var retry = false;
|
||||
var palettesData;
|
||||
var fxdata = [];
|
||||
var pJson = {}, eJson = {}, lJson = {};
|
||||
var plJson = {}; // array of playlists
|
||||
var pN = "", pI = 0, pNum = 0;
|
||||
var pmt = 1, pmtLS = 0, pmtLast = 0;
|
||||
var pmt = 1, pmtLS = 0;
|
||||
var lastinfo = {};
|
||||
var isM = false, mw = 0, mh=0;
|
||||
var ws, wsRpt=0;
|
||||
@@ -200,19 +199,17 @@ function loadBg() {
|
||||
});
|
||||
}
|
||||
|
||||
function loadSkinCSS(cId)
|
||||
{
|
||||
if (!gId(cId)) // check if element exists
|
||||
{
|
||||
var h = d.getElementsByTagName('head')[0];
|
||||
var l = d.createElement('link');
|
||||
l.id = cId;
|
||||
l.rel = 'stylesheet';
|
||||
l.type = 'text/css';
|
||||
function loadSkinCSS(cId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (gId(cId)) return resolve();
|
||||
const l = d.createElement('link');
|
||||
l.id = cId;
|
||||
l.rel = 'stylesheet';
|
||||
l.href = getURL('/skin.css');
|
||||
l.media = 'all';
|
||||
h.appendChild(l);
|
||||
}
|
||||
l.onload = resolve;
|
||||
l.onerror = reject;
|
||||
d.head.appendChild(l);
|
||||
});
|
||||
}
|
||||
|
||||
function getURL(path) {
|
||||
@@ -278,19 +275,23 @@ function onLoad()
|
||||
cpick.on("color:change", () => {updatePSliders()});
|
||||
pmtLS = localStorage.getItem('wledPmt');
|
||||
|
||||
// Load initial data
|
||||
loadPalettes(()=>{
|
||||
// fill effect extra data array
|
||||
loadFXData(()=>{
|
||||
// load and populate effects
|
||||
setTimeout(()=>{loadFX(()=>{
|
||||
loadPalettesData(()=>{
|
||||
requestJson();// will load presets and create WS
|
||||
if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50);
|
||||
});
|
||||
})},50);
|
||||
});
|
||||
});
|
||||
// Load initial data sequentially, no parallel requests to avoid "503" errors when heap is low (slower but much more reliable)
|
||||
(async ()=>{
|
||||
try {
|
||||
await loadPalettes(); // loads base palettes and builds #pallist (safe first)
|
||||
await loadFXData(); // loads fx data
|
||||
await loadFX(); // populates effect list
|
||||
await requestJson(); // updates info variables
|
||||
await loadPalettesData(); // fills palettesData[] for previews
|
||||
populatePalettes(); // repopulate with custom palettes now that cpalcount is known
|
||||
if(pmt == pmtLS) populatePresets(true); // load presets from localStorage if signature matches (i.e. no device reboot)
|
||||
else await loadPresets(); // load and populate presets
|
||||
if (cfg.comp.css) await loadSkinCSS('skinCss');
|
||||
if (!ws) makeWS();
|
||||
} catch(e) {
|
||||
showToast("Init failed: " + e, true);
|
||||
}
|
||||
})();
|
||||
resetUtil();
|
||||
|
||||
d.addEventListener("visibilitychange", handleVisibilityChange, false);
|
||||
@@ -448,7 +449,7 @@ function presetError(empty)
|
||||
if (bckstr.length > 10) hasBackup = true;
|
||||
} catch (e) {}
|
||||
|
||||
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="pmtLast=0;loadPresets();"'}>`;
|
||||
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="loadPresets();"'}>`;
|
||||
if (empty)
|
||||
cn += `You have no presets yet!`;
|
||||
else
|
||||
@@ -481,123 +482,81 @@ function restore(txt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function loadPresets(callback = null)
|
||||
{
|
||||
// 1st boot (because there is a callback)
|
||||
if (callback && pmt == pmtLS && pmt > 0) {
|
||||
// we have a copy of the presets in local storage and don't need to fetch another one
|
||||
populatePresets(true);
|
||||
pmtLast = pmt;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// afterwards
|
||||
if (!callback && pmt == pmtLast) return;
|
||||
|
||||
fetch(getURL('/presets.json'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status=="404") return {"0":{}};
|
||||
//if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then(json => {
|
||||
pJson = json;
|
||||
pmtLast = pmt;
|
||||
populatePresets();
|
||||
})
|
||||
.catch((e)=>{
|
||||
//showToast(e, true);
|
||||
presetError(false);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) setTimeout(callback,99);
|
||||
async function loadPresets() {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/presets.json'), {method: 'get'})
|
||||
.then(res => res.status=="404" ? {"0":{}} : res.json())
|
||||
.then(json => {
|
||||
pJson = json;
|
||||
populatePresets();
|
||||
resolve();
|
||||
})
|
||||
.catch(() => {
|
||||
presetError(false);
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function loadPalettes(callback = null)
|
||||
{
|
||||
fetch(getURL('/json/palettes'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then((res)=>{
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then((json)=>{
|
||||
lJson = Object.entries(json);
|
||||
populatePalettes();
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(loadPalettes, 500); // retry
|
||||
}
|
||||
showToast(e, true);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) callback();
|
||||
updateUI();
|
||||
async function loadPalettes(retry=0) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/json/palettes'), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
lJson = Object.entries(json);
|
||||
populatePalettes();
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (retry<5) {
|
||||
setTimeout(() => loadPalettes(retry+1).then(resolve), 100);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadFX(callback = null)
|
||||
{
|
||||
fetch(getURL('/json/effects'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then((res)=>{
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then((json)=>{
|
||||
eJson = Object.entries(json);
|
||||
populateEffects();
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(loadFX, 500); // retry
|
||||
}
|
||||
showToast(e, true);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) callback();
|
||||
updateUI();
|
||||
async function loadFX(retry=0) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/json/effects'), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
eJson = Object.entries(json);
|
||||
populateEffects();
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (retry<5) {
|
||||
setTimeout(() => loadFX(retry+1).then(resolve), 100);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadFXData(callback = null)
|
||||
{
|
||||
fetch(getURL('/json/fxdata'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then((res)=>{
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then((json)=>{
|
||||
fxdata = json||[];
|
||||
// add default value for Solid
|
||||
fxdata.shift()
|
||||
fxdata.unshift(";!;");
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
fxdata = [];
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(()=>{loadFXData(loadFX);}, 500); // retry
|
||||
}
|
||||
showToast(e, true);
|
||||
})
|
||||
.finally(()=>{
|
||||
if (callback) callback();
|
||||
updateUI();
|
||||
async function loadFXData(retry=0) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(getURL('/json/fxdata'), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
fxdata = json||[];
|
||||
fxdata.shift();
|
||||
fxdata.unshift(";!;");
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
fxdata = [];
|
||||
if (retry<5) {
|
||||
setTimeout(() => loadFXData(retry+1).then(resolve), 100);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -619,7 +578,7 @@ function populateQL()
|
||||
function populatePresets(fromls)
|
||||
{
|
||||
if (fromls) pJson = JSON.parse(localStorage.getItem("wledP"));
|
||||
if (!pJson) {setTimeout(loadPresets,250); return;}
|
||||
if (!pJson) {loadPresets(); return;} // note: no await as this is a fallback that should not be needed as init function fetches pJson
|
||||
delete pJson["0"];
|
||||
var cn = "";
|
||||
var arr = Object.entries(pJson).sort(cmpP);
|
||||
@@ -693,16 +652,18 @@ function parseInfo(i) {
|
||||
// gId("filterVol").classList.add("hide"); hideModes(" ♪"); // hide volume reactive effects
|
||||
// gId("filterFreq").classList.add("hide"); hideModes(" ♫"); // hide frequency reactive effects
|
||||
// }
|
||||
// Check for version upgrades on page load
|
||||
checkVersionUpgrade(i);
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml
|
||||
//var setInnerHTML = function(elm, html) {
|
||||
// elm.innerHTML = html;
|
||||
// Array.from(elm.querySelectorAll("script")).forEach( oldScript => {
|
||||
// const newScript = document.createElement("script");
|
||||
// const newScript = d.createElement("script");
|
||||
// Array.from(oldScript.attributes)
|
||||
// .forEach( attr => newScript.setAttribute(attr.name, attr.value) );
|
||||
// newScript.appendChild(document.createTextNode(oldScript.innerHTML));
|
||||
// newScript.appendChild(d.createTextNode(oldScript.innerHTML));
|
||||
// oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
// });
|
||||
//}
|
||||
@@ -904,6 +865,7 @@ function populateSegments(s)
|
||||
gId("segcont").classList.remove("hide");
|
||||
let noNewSegs = (lowestUnused >= maxSeg);
|
||||
resetUtil(noNewSegs);
|
||||
if (segCount === 0) return; // no segments to populate
|
||||
for (var i = 0; i <= lSeg; i++) {
|
||||
if (!gId(`seg${i}`)) continue;
|
||||
updateLen(i);
|
||||
@@ -1274,7 +1236,6 @@ function updateUI()
|
||||
gId('buttonPower').className = (isOn) ? 'active':'';
|
||||
gId('buttonNl').className = (nlA) ? 'active':'';
|
||||
gId('buttonSync').className = (syncSend) ? 'active':'';
|
||||
gId('pxmb').style.display = (isM) ? "inline-block" : "none";
|
||||
|
||||
updateSelectedFx();
|
||||
updateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes
|
||||
@@ -1436,7 +1397,7 @@ function makeWS() {
|
||||
};
|
||||
ws.onclose = (e)=>{
|
||||
gId('connind').style.backgroundColor = "var(--c-r)";
|
||||
if (wsRpt++ < 5) setTimeout(makeWS,1500); // retry WS connection
|
||||
if (wsRpt++ < 10) setTimeout(makeWS,wsRpt * 200); // retry WS connection
|
||||
ws = null;
|
||||
}
|
||||
ws.onopen = (e)=>{
|
||||
@@ -1469,6 +1430,7 @@ function readState(s,command=false)
|
||||
|
||||
populateSegments(s);
|
||||
hasRGB = hasWhite = hasCCT = has2D = false;
|
||||
segLmax = 0; // reset max selected segment length
|
||||
let i = {};
|
||||
// determine light capabilities from selected segments
|
||||
for (let seg of (s.seg||[])) {
|
||||
@@ -1708,77 +1670,68 @@ function setEffectParameters(idx)
|
||||
|
||||
var jsonTimeout;
|
||||
var reqsLegal = false;
|
||||
async function requestJson(command=null, retry=0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
gId('connind').style.backgroundColor = "var(--c-y)";
|
||||
if (command && !reqsLegal) {resolve(); return;}
|
||||
if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);
|
||||
|
||||
function requestJson(command=null)
|
||||
{
|
||||
gId('connind').style.backgroundColor = "var(--c-y)";
|
||||
if (command && !reqsLegal) return; // stop post requests from chrome onchange event on page restore
|
||||
if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);
|
||||
var req = null;
|
||||
var useWs = (ws && ws.readyState === WebSocket.OPEN);
|
||||
var type = command ? 'post':'get';
|
||||
if (command) {
|
||||
command.v = true; // force complete /json/si API response
|
||||
command.time = Math.floor(Date.now() / 1000);
|
||||
var t = gId('tt');
|
||||
if (t.validity.valid && command.transition==null) {
|
||||
var tn = parseInt(t.value*10);
|
||||
if (tn != tr) command.transition = tn;
|
||||
var useWs = (ws && ws.readyState === WebSocket.OPEN);
|
||||
var req = null;
|
||||
if (command) {
|
||||
command.v = true;
|
||||
command.time = Math.floor(Date.now() / 1000);
|
||||
var t = gId('tt');
|
||||
if (t && t.validity.valid && command.transition==null) {
|
||||
var tn = parseInt(t.value*10);
|
||||
if (tn != tr) command.transition = tn;
|
||||
}
|
||||
req = JSON.stringify(command);
|
||||
if (req.length > 1340) useWs = false;
|
||||
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false;
|
||||
}
|
||||
//command.bs = parseInt(gId('bs').value);
|
||||
req = JSON.stringify(command);
|
||||
if (req.length > 1340) useWs = false; // do not send very long requests over websocket
|
||||
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes
|
||||
};
|
||||
|
||||
if (useWs) {
|
||||
ws.send(req?req:'{"v":true}');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(getURL('/json/si'), {
|
||||
method: type,
|
||||
headers: {"Content-Type": "application/json; charset=UTF-8"},
|
||||
body: req
|
||||
})
|
||||
.then(res => {
|
||||
clearTimeout(jsonTimeout);
|
||||
jsonTimeout = null;
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
.then(json => {
|
||||
lastUpdate = new Date();
|
||||
clearErrorToast(3000);
|
||||
gId('connind').style.backgroundColor = "var(--c-g)";
|
||||
if (!json) { showToast('Empty response', true); return; }
|
||||
if (json.success) return;
|
||||
if (json.info) {
|
||||
let i = json.info;
|
||||
parseInfo(i);
|
||||
populatePalettes(i);
|
||||
if (isInfo) populateInfo(i);
|
||||
if (simplifiedUI) simplifyUI();
|
||||
if (useWs) {
|
||||
ws.send(req?req:'{"v":true}');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
var s = json.state ? json.state : json;
|
||||
readState(s);
|
||||
|
||||
//load presets and open websocket sequentially
|
||||
if (!pJson || isEmpty(pJson)) setTimeout(()=>{
|
||||
loadPresets(()=>{
|
||||
wsRpt = 0;
|
||||
if (!(ws && ws.readyState === WebSocket.OPEN)) makeWS();
|
||||
});
|
||||
},25);
|
||||
reqsLegal = true;
|
||||
retry = false;
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(requestJson,500);
|
||||
}
|
||||
showToast(e, true);
|
||||
fetch(getURL('/json/si'), {
|
||||
method: command ? 'post' : 'get',
|
||||
headers: {"Content-Type": "application/json; charset=UTF-8"},
|
||||
body: req
|
||||
})
|
||||
.then(res => {
|
||||
clearTimeout(jsonTimeout);
|
||||
jsonTimeout = null;
|
||||
return res.ok ? res.json() : Promise.reject();
|
||||
})
|
||||
.then(json => {
|
||||
lastUpdate = new Date();
|
||||
clearErrorToast(3000);
|
||||
gId('connind').style.backgroundColor = "var(--c-g)";
|
||||
if (!json) { showToast('Empty response', true); resolve(); return; }
|
||||
if (json.success) {resolve(); return;}
|
||||
if (json.info) {
|
||||
parseInfo(json.info);
|
||||
if (isInfo) populateInfo(json.info);
|
||||
if (simplifiedUI) simplifyUI();
|
||||
}
|
||||
var s = json.state ? json.state : json;
|
||||
readState(s);
|
||||
|
||||
reqsLegal = true;
|
||||
resolve();
|
||||
})
|
||||
.catch((e)=>{
|
||||
if (retry<10) {
|
||||
setTimeout(() => requestJson(command,retry+1).then(resolve).catch(reject), retry*50);
|
||||
} else {
|
||||
showToast(e, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2553,7 +2506,7 @@ function saveP(i,pl)
|
||||
}
|
||||
populatePresets();
|
||||
resetPUtil();
|
||||
setTimeout(()=>{pmtLast=0; loadPresets();}, 750); // force reloading of presets
|
||||
setTimeout(()=>{loadPresets();}, 750); // force reloading of presets
|
||||
}
|
||||
|
||||
function testPl(i,bt) {
|
||||
@@ -2819,56 +2772,51 @@ function rSegs()
|
||||
requestJson(obj);
|
||||
}
|
||||
|
||||
function loadPalettesData(callback = null)
|
||||
{
|
||||
if (palettesData) return;
|
||||
const lsKey = "wledPalx";
|
||||
var lsPalData = localStorage.getItem(lsKey);
|
||||
if (lsPalData) {
|
||||
try {
|
||||
var d = JSON.parse(lsPalData);
|
||||
if (d && d.vid == d.vid) {
|
||||
palettesData = d.p;
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
function loadPalettesData() {
|
||||
return new Promise((resolve) => {
|
||||
if (palettesData) return resolve(); // already loaded
|
||||
var lsPalData = localStorage.getItem("wledPalx");
|
||||
if (lsPalData) {
|
||||
try {
|
||||
var d = JSON.parse(lsPalData);
|
||||
if (d && d.vid == lastinfo.vid) {
|
||||
palettesData = d.p;
|
||||
redrawPalPrev();
|
||||
return resolve();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
palettesData = {};
|
||||
getPalettesData(0, ()=>{
|
||||
localStorage.setItem(lsKey, JSON.stringify({
|
||||
p: palettesData,
|
||||
vid: lastinfo.vid
|
||||
}));
|
||||
redrawPalPrev();
|
||||
if (callback) setTimeout(callback, 99);
|
||||
palettesData = {};
|
||||
getPalettesData(0, () => {
|
||||
localStorage.setItem("wledPalx", JSON.stringify({
|
||||
p: palettesData,
|
||||
vid: lastinfo.vid
|
||||
}));
|
||||
redrawPalPrev();
|
||||
setTimeout(resolve, 99); // delay optional
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getPalettesData(page, callback)
|
||||
{
|
||||
fetch(getURL(`/json/palx?page=${page}`), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) showErrorToast();
|
||||
return res.json();
|
||||
})
|
||||
function getPalettesData(page, callback, retry=0) {
|
||||
fetch(getURL(`/json/palx?page=${page}`), {method: 'get'})
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(json => {
|
||||
retry = false;
|
||||
palettesData = Object.assign({}, palettesData, json.p);
|
||||
if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75);
|
||||
else callback();
|
||||
})
|
||||
.catch((error)=>{
|
||||
if (!retry) {
|
||||
retry = true;
|
||||
setTimeout(()=>{getPalettesData(page,callback);}, 500); // retry
|
||||
if (retry<5) {
|
||||
setTimeout(()=>{getPalettesData(page,callback,retry+1);}, 100);
|
||||
} else {
|
||||
showToast(error, true);
|
||||
callback();
|
||||
}
|
||||
showToast(error, true);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
function hideModes(txt)
|
||||
{
|
||||
@@ -2970,7 +2918,7 @@ function filterFocus(e) {
|
||||
}
|
||||
if (e.type === "blur") {
|
||||
setTimeout(() => {
|
||||
if (e.target === document.activeElement && document.hasFocus()) return;
|
||||
if (e.target === d.activeElement && d.hasFocus()) return;
|
||||
// do not hide if filter is active
|
||||
if (!c) {
|
||||
// compute sticky top
|
||||
@@ -3217,7 +3165,7 @@ function simplifyUI() {
|
||||
// Create dropdown dialog
|
||||
function createDropdown(id, buttonText, dialogElements = null) {
|
||||
// Create dropdown dialog
|
||||
const dialog = document.createElement("dialog");
|
||||
const dialog = d.createElement("dialog");
|
||||
// Move every dialogElement to the dropdown dialog or if none are given, move all children of the element with the given id
|
||||
if (dialogElements) {
|
||||
dialogElements.forEach((e) => {
|
||||
@@ -3230,7 +3178,7 @@ function simplifyUI() {
|
||||
}
|
||||
|
||||
// Create button for the dropdown
|
||||
const btn = document.createElement("button");
|
||||
const btn = d.createElement("button");
|
||||
btn.id = id + "btn";
|
||||
btn.classList.add("btn");
|
||||
btn.innerText = buttonText;
|
||||
@@ -3278,7 +3226,7 @@ function simplifyUI() {
|
||||
|
||||
// Hide palette label
|
||||
gId("pall").style.display = "none";
|
||||
gId("Colors").insertBefore(document.createElement("br"), gId("pall"));
|
||||
gId("Colors").insertBefore(d.createElement("br"), gId("pall"));
|
||||
// Hide effect label
|
||||
gId("modeLabel").style.display = "none";
|
||||
|
||||
@@ -3290,7 +3238,7 @@ function simplifyUI() {
|
||||
|
||||
// Hide bottom bar
|
||||
gId("bot").style.display = "none";
|
||||
document.documentElement.style.setProperty('--bh', '0px');
|
||||
d.documentElement.style.setProperty('--bh', '0px');
|
||||
|
||||
// Hide other tabs
|
||||
gId("Effects").style.display = "none";
|
||||
@@ -3304,6 +3252,197 @@ function simplifyUI() {
|
||||
gId("btns").style.display = "none";
|
||||
}
|
||||
|
||||
// Version reporting feature
|
||||
var versionCheckDone = false;
|
||||
|
||||
function checkVersionUpgrade(info) {
|
||||
// Only check once per page load
|
||||
if (versionCheckDone) return;
|
||||
versionCheckDone = true;
|
||||
|
||||
// Suppress feature if in AP mode (no internet connection available)
|
||||
if (info.wifi && info.wifi.ap) return;
|
||||
|
||||
// Fetch version-info.json using existing /edit endpoint
|
||||
fetch(getURL('/edit?func=edit&path=/version-info.json'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 404) {
|
||||
// File doesn't exist - first install, show install prompt
|
||||
showVersionUpgradePrompt(info, null, info.ver);
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch version-info.json');
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(versionInfo => {
|
||||
if (!versionInfo) return; // 404 case already handled
|
||||
|
||||
// Check if user opted out
|
||||
if (versionInfo.neverAsk) return;
|
||||
|
||||
// Check if version has changed
|
||||
const currentVersion = info.ver;
|
||||
const storedVersion = versionInfo.version || '';
|
||||
|
||||
if (storedVersion && storedVersion !== currentVersion) {
|
||||
// Version has changed, show upgrade prompt
|
||||
showVersionUpgradePrompt(info, storedVersion, currentVersion);
|
||||
} else if (!storedVersion) {
|
||||
// Empty version in file, show install prompt
|
||||
showVersionUpgradePrompt(info, null, currentVersion);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to load version-info.json', e);
|
||||
// On error, save current version for next time
|
||||
if (info && info.ver) {
|
||||
updateVersionInfo(info.ver, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showVersionUpgradePrompt(info, oldVersion, newVersion) {
|
||||
// Determine if this is an install or upgrade
|
||||
const isInstall = !oldVersion;
|
||||
|
||||
// Create overlay and dialog
|
||||
const overlay = d.createElement('div');
|
||||
overlay.id = 'versionUpgradeOverlay';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';
|
||||
|
||||
const dialog = d.createElement('div');
|
||||
dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);';
|
||||
|
||||
// Build contextual message based on install vs upgrade
|
||||
const title = isInstall
|
||||
? '🎉 Thank you for installing WLED!'
|
||||
: '🎉 WLED Upgrade Detected!';
|
||||
|
||||
const description = isInstall
|
||||
? `You are now running WLED <strong style="text-wrap: nowrap">${newVersion}</strong>.`
|
||||
: `Your WLED has been upgraded from <strong style="text-wrap: nowrap">${oldVersion}</strong> to <strong style="text-wrap: nowrap">${newVersion}</strong>.`;
|
||||
|
||||
const question = 'Help make WLED better with a one-time hardware report? It includes only device details like chip type, LED count, etc. — never personal data or your activities.'
|
||||
|
||||
dialog.innerHTML = `
|
||||
<h2 style="margin-top:0;color:var(--c-f);">${title}</h2>
|
||||
<p style="color:var(--c-f);">${description}</p>
|
||||
<p style="color:var(--c-f);">${question}</p>
|
||||
<p style="color:var(--c-f);font-size:0.9em;">
|
||||
<a href="https://kno.wled.ge/about/privacy-policy/" target="_blank" style="color:var(--c-6);">Learn more about what data is collected and why</a>
|
||||
</p>
|
||||
<div style="margin-top:20px;">
|
||||
<button id="versionReportYes" class="btn">Yes</button>
|
||||
<button id="versionReportNo" class="btn">Not Now</button>
|
||||
<button id="versionReportNever" class="btn">Never Ask</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(dialog);
|
||||
d.body.appendChild(overlay);
|
||||
|
||||
// Add event listeners
|
||||
gId('versionReportYes').addEventListener('click', () => {
|
||||
reportUpgradeEvent(info, oldVersion);
|
||||
d.body.removeChild(overlay);
|
||||
});
|
||||
|
||||
gId('versionReportNo').addEventListener('click', () => {
|
||||
// Don't update version, will ask again on next load
|
||||
d.body.removeChild(overlay);
|
||||
});
|
||||
|
||||
gId('versionReportNever').addEventListener('click', () => {
|
||||
updateVersionInfo(newVersion, true);
|
||||
d.body.removeChild(overlay);
|
||||
showToast('You will not be asked again.');
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpgradeEvent(info, oldVersion) {
|
||||
showToast('Reporting upgrade...');
|
||||
|
||||
// Fetch fresh data from /json/info endpoint as requested
|
||||
fetch(getURL('/json/info'), {
|
||||
method: 'get'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(infoData => {
|
||||
// Map to UpgradeEventRequest structure per OpenAPI spec
|
||||
// Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256
|
||||
const upgradeData = {
|
||||
deviceId: infoData.deviceId, // Use anonymous unique device ID
|
||||
version: infoData.ver || '', // Current version string
|
||||
previousVersion: oldVersion || '', // Previous version from version-info.json
|
||||
releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0")
|
||||
chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc)
|
||||
ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs
|
||||
isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup
|
||||
bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash
|
||||
brand: infoData.brand, // Device brand (always present)
|
||||
product: infoData.product, // Product name (always present)
|
||||
flashSize: infoData.flash, // Flash size (always present)
|
||||
repo: infoData.repo // GitHub repository (always present)
|
||||
};
|
||||
|
||||
// Add optional fields if available
|
||||
if (infoData.psramPresent !== undefined) upgradeData.psramPresent = infoData.psramPresent; // Whether device has PSRAM
|
||||
if (infoData.psramSize !== undefined) upgradeData.psramSize = infoData.psramSize; // Total PSRAM size in MB
|
||||
// Note: partitionSizes not currently available in /json/info endpoint
|
||||
|
||||
// Make AJAX call to postUpgradeEvent API
|
||||
return fetch('https://usage.wled.me/api/usage/upgrade', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(upgradeData)
|
||||
});
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast('Thank you for reporting!');
|
||||
updateVersionInfo(info.ver, false);
|
||||
} else {
|
||||
showToast('Report failed. Please try again later.', true);
|
||||
// Do NOT update version info on failure - user will be prompted again
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to report upgrade', e);
|
||||
showToast('Report failed. Please try again later.', true);
|
||||
// Do NOT update version info on error - user will be prompted again
|
||||
});
|
||||
}
|
||||
|
||||
function updateVersionInfo(version, neverAsk) {
|
||||
const versionInfo = {
|
||||
version: version,
|
||||
neverAsk: neverAsk
|
||||
};
|
||||
|
||||
// Create a Blob with JSON content and use /upload endpoint
|
||||
const blob = new Blob([JSON.stringify(versionInfo)], {type: 'application/json'});
|
||||
const formData = new FormData();
|
||||
formData.append('data', blob, 'version-info.json');
|
||||
|
||||
fetch(getURL('/upload'), {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(data => {
|
||||
console.log('Version info updated', data);
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed to update version-info.json', e);
|
||||
});
|
||||
}
|
||||
|
||||
size();
|
||||
_C.style.setProperty('--n', N);
|
||||
|
||||
|
||||
@@ -17,8 +17,15 @@
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function common() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => S();
|
||||
l.onerror = () => setTimeout(common, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
var ws;
|
||||
var tmout = null;
|
||||
var c;
|
||||
@@ -31,7 +38,7 @@
|
||||
ctx.fillRect(Math.round((i - start) * w / skip), 0, Math.ceil(w), c.height);
|
||||
}
|
||||
}
|
||||
function update() { // via HTTP (/json/live)
|
||||
function update(retry=0) { // via HTTP (/json/live)
|
||||
if (d.hidden) {
|
||||
clearTimeout(tmout);
|
||||
tmout = setTimeout(update, 250);
|
||||
@@ -53,7 +60,7 @@
|
||||
.catch((error)=>{
|
||||
//console.error("Peek HTTP error:",error);
|
||||
clearTimeout(tmout);
|
||||
tmout = setTimeout(update, 2500);
|
||||
if (retry<5) tmout = setTimeout(() => update(retry+1), 2500); // stop endlessly bugging the ESP if resource is not available
|
||||
})
|
||||
}
|
||||
function S() { // Startup function (onload)
|
||||
@@ -62,10 +69,7 @@
|
||||
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
|
||||
|
||||
// Initialize WebSocket connection
|
||||
ws = connectWs(function () {
|
||||
//console.info("Peek WS open");
|
||||
ws.send('{"lv":true}');
|
||||
});
|
||||
ws = connectWs(ws => ws.send('{"lv":true}'));
|
||||
ws.addEventListener('message', (e) => {
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
@@ -81,7 +85,7 @@
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<canvas id="canv"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,11 +10,18 @@
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script src="common.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canv"></canvas>
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function common() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => S();
|
||||
l.onerror = () => setTimeout(common, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
var c = document.getElementById('canv');
|
||||
var leds = "";
|
||||
var throttled = false;
|
||||
@@ -22,36 +29,35 @@
|
||||
c.width = window.innerWidth * 0.98; //remove scroll bars
|
||||
c.height = window.innerHeight * 0.98; //remove scroll bars
|
||||
}
|
||||
setCanvas();
|
||||
// Check for canvas support
|
||||
var ctx = c.getContext('2d');
|
||||
if (ctx) { // Access the rendering context
|
||||
// use parent WS or open new
|
||||
var ws = connectWs(()=>{
|
||||
ws.send('{"lv":true}');
|
||||
});
|
||||
ws.addEventListener('message',(e)=>{
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
let leds = new Uint8Array(e.data);
|
||||
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
|
||||
let mW = leds[2]; // matrix width
|
||||
let mH = leds[3]; // matrix height
|
||||
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
|
||||
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
|
||||
var i = 4;
|
||||
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
|
||||
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
i+=3;
|
||||
function S() { // Startup function (onload)
|
||||
setCanvas();
|
||||
// Check for canvas support
|
||||
var ctx = c.getContext('2d');
|
||||
if (ctx) { // Access the rendering context
|
||||
ws = connectWs(ws => ws.send('{"lv":true}')); // use parent WS or open new
|
||||
ws.addEventListener('message',(e)=>{
|
||||
try {
|
||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
||||
let leds = new Uint8Array(e.data);
|
||||
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
|
||||
let mW = leds[2]; // matrix width
|
||||
let mH = leds[3]; // matrix height
|
||||
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
|
||||
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
|
||||
var i = 4;
|
||||
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
|
||||
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
i+=3;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Peek WS error:",err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Peek WS error:",err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// window.resize event listener
|
||||
window.addEventListener('resize', (e)=>{
|
||||
|
||||
809
wled00/data/pixelforge/omggif.js
Normal file
809
wled00/data/pixelforge/omggif.js
Normal file
@@ -0,0 +1,809 @@
|
||||
// (c) Dean McNamee <dean@gmail.com>, 2013.
|
||||
//
|
||||
// https://github.com/deanm/omggif
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//
|
||||
// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,
|
||||
// including animation and compression. It does not rely on any specific
|
||||
// underlying system, so should run in the browser, Node, or Plask.
|
||||
|
||||
"use strict";
|
||||
|
||||
function GifWriter(buf, width, height, gopts) {
|
||||
var p = 0;
|
||||
|
||||
var gopts = gopts === undefined ? { } : gopts;
|
||||
var loop_count = gopts.loop === undefined ? null : gopts.loop;
|
||||
var global_palette = gopts.palette === undefined ? null : gopts.palette;
|
||||
|
||||
if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
|
||||
throw new Error("Width/Height invalid.");
|
||||
|
||||
function check_palette_and_num_colors(palette) {
|
||||
var num_colors = palette.length;
|
||||
if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) {
|
||||
throw new Error(
|
||||
"Invalid code/color length, must be power of 2 and 2 .. 256.");
|
||||
}
|
||||
return num_colors;
|
||||
}
|
||||
|
||||
// - Header.
|
||||
buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF
|
||||
buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a
|
||||
|
||||
// Handling of Global Color Table (palette) and background index.
|
||||
var gp_num_colors_pow2 = 0;
|
||||
var background = 0;
|
||||
if (global_palette !== null) {
|
||||
var gp_num_colors = check_palette_and_num_colors(global_palette);
|
||||
while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
|
||||
gp_num_colors = 1 << gp_num_colors_pow2;
|
||||
--gp_num_colors_pow2;
|
||||
if (gopts.background !== undefined) {
|
||||
background = gopts.background;
|
||||
if (background >= gp_num_colors)
|
||||
throw new Error("Background index out of range.");
|
||||
// The GIF spec states that a background index of 0 should be ignored, so
|
||||
// this is probably a mistake and you really want to set it to another
|
||||
// slot in the palette. But actually in the end most browsers, etc end
|
||||
// up ignoring this almost completely (including for dispose background).
|
||||
if (background === 0)
|
||||
throw new Error("Background index explicitly passed as 0.");
|
||||
}
|
||||
}
|
||||
|
||||
// - Logical Screen Descriptor.
|
||||
// NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
|
||||
buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
|
||||
buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
|
||||
// NOTE: Indicates 0-bpp original color resolution (unused?).
|
||||
buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag.
|
||||
gp_num_colors_pow2; // NOTE: No sort flag (unused?).
|
||||
buf[p++] = background; // Background Color Index.
|
||||
buf[p++] = 0; // Pixel aspect ratio (unused?).
|
||||
|
||||
// - Global Color Table
|
||||
if (global_palette !== null) {
|
||||
for (var i = 0, il = global_palette.length; i < il; ++i) {
|
||||
var rgb = global_palette[i];
|
||||
buf[p++] = rgb >> 16 & 0xff;
|
||||
buf[p++] = rgb >> 8 & 0xff;
|
||||
buf[p++] = rgb & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
if (loop_count !== null) { // Netscape block for looping.
|
||||
if (loop_count < 0 || loop_count > 65535)
|
||||
throw new Error("Loop count invalid.")
|
||||
// Extension code, label, and length.
|
||||
buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
|
||||
// NETSCAPE2.0
|
||||
buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
|
||||
buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
|
||||
buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
|
||||
// Sub-block
|
||||
buf[p++] = 0x03; buf[p++] = 0x01;
|
||||
buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
|
||||
buf[p++] = 0x00; // Terminator.
|
||||
}
|
||||
|
||||
|
||||
var ended = false;
|
||||
|
||||
this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
|
||||
if (ended === true) { --p; ended = false; } // Un-end.
|
||||
|
||||
opts = opts === undefined ? { } : opts;
|
||||
|
||||
// TODO(deanm): Bounds check x, y. Do they need to be within the virtual
|
||||
// canvas width/height, I imagine?
|
||||
if (x < 0 || y < 0 || x > 65535 || y > 65535)
|
||||
throw new Error("x/y invalid.")
|
||||
|
||||
if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
|
||||
throw new Error("Width/Height invalid.")
|
||||
|
||||
if (indexed_pixels.length < w * h)
|
||||
throw new Error("Not enough pixels for the frame size.");
|
||||
|
||||
var using_local_palette = true;
|
||||
var palette = opts.palette;
|
||||
if (palette === undefined || palette === null) {
|
||||
using_local_palette = false;
|
||||
palette = global_palette;
|
||||
}
|
||||
|
||||
if (palette === undefined || palette === null)
|
||||
throw new Error("Must supply either a local or global palette.");
|
||||
|
||||
var num_colors = check_palette_and_num_colors(palette);
|
||||
|
||||
// Compute the min_code_size (power of 2), destroying num_colors.
|
||||
var min_code_size = 0;
|
||||
while (num_colors >>= 1) ++min_code_size;
|
||||
num_colors = 1 << min_code_size; // Now we can easily get it back.
|
||||
|
||||
var delay = opts.delay === undefined ? 0 : opts.delay;
|
||||
|
||||
// From the spec:
|
||||
// 0 - No disposal specified. The decoder is
|
||||
// not required to take any action.
|
||||
// 1 - Do not dispose. The graphic is to be left
|
||||
// in place.
|
||||
// 2 - Restore to background color. The area used by the
|
||||
// graphic must be restored to the background color.
|
||||
// 3 - Restore to previous. The decoder is required to
|
||||
// restore the area overwritten by the graphic with
|
||||
// what was there prior to rendering the graphic.
|
||||
// 4-7 - To be defined.
|
||||
// NOTE(deanm): Dispose background doesn't really work, apparently most
|
||||
// browsers ignore the background palette index and clear to transparency.
|
||||
var disposal = opts.disposal === undefined ? 0 : opts.disposal;
|
||||
if (disposal < 0 || disposal > 3) // 4-7 is reserved.
|
||||
throw new Error("Disposal out of range.");
|
||||
|
||||
var use_transparency = false;
|
||||
var transparent_index = 0;
|
||||
if (opts.transparent !== undefined && opts.transparent !== null) {
|
||||
use_transparency = true;
|
||||
transparent_index = opts.transparent;
|
||||
if (transparent_index < 0 || transparent_index >= num_colors)
|
||||
throw new Error("Transparent color index.");
|
||||
}
|
||||
|
||||
if (disposal !== 0 || use_transparency || delay !== 0) {
|
||||
// - Graphics Control Extension
|
||||
buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label.
|
||||
buf[p++] = 4; // Byte size.
|
||||
|
||||
buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
|
||||
buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
|
||||
buf[p++] = transparent_index; // Transparent color index.
|
||||
buf[p++] = 0; // Block Terminator.
|
||||
}
|
||||
|
||||
// - Image Descriptor
|
||||
buf[p++] = 0x2c; // Image Seperator.
|
||||
buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left.
|
||||
buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top.
|
||||
buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
|
||||
buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
|
||||
// NOTE: No sort flag (unused?).
|
||||
// TODO(deanm): Support interlace.
|
||||
buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;
|
||||
|
||||
// - Local Color Table
|
||||
if (using_local_palette === true) {
|
||||
for (var i = 0, il = palette.length; i < il; ++i) {
|
||||
var rgb = palette[i];
|
||||
buf[p++] = rgb >> 16 & 0xff;
|
||||
buf[p++] = rgb >> 8 & 0xff;
|
||||
buf[p++] = rgb & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
p = GifWriterOutputLZWCodeStream(
|
||||
buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
this.end = function() {
|
||||
if (ended === false) {
|
||||
buf[p++] = 0x3b; // Trailer.
|
||||
ended = true;
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
this.getOutputBuffer = function() { return buf; };
|
||||
this.setOutputBuffer = function(v) { buf = v; };
|
||||
this.getOutputBufferPosition = function() { return p; };
|
||||
this.setOutputBufferPosition = function(v) { p = v; };
|
||||
}
|
||||
|
||||
// Main compression routine, palette indexes -> LZW code stream.
|
||||
// |index_stream| must have at least one entry.
|
||||
function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
|
||||
buf[p++] = min_code_size;
|
||||
var cur_subblock = p++; // Pointing at the length field.
|
||||
|
||||
var clear_code = 1 << min_code_size;
|
||||
var code_mask = clear_code - 1;
|
||||
var eoi_code = clear_code + 1;
|
||||
var next_code = eoi_code + 1;
|
||||
|
||||
var cur_code_size = min_code_size + 1; // Number of bits per code.
|
||||
var cur_shift = 0;
|
||||
// We have at most 12-bit codes, so we should have to hold a max of 19
|
||||
// bits here (and then we would write out).
|
||||
var cur = 0;
|
||||
|
||||
function emit_bytes_to_buffer(bit_block_size) {
|
||||
while (cur_shift >= bit_block_size) {
|
||||
buf[p++] = cur & 0xff;
|
||||
cur >>= 8; cur_shift -= 8;
|
||||
if (p === cur_subblock + 256) { // Finished a subblock.
|
||||
buf[cur_subblock] = 255;
|
||||
cur_subblock = p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emit_code(c) {
|
||||
cur |= c << cur_shift;
|
||||
cur_shift += cur_code_size;
|
||||
emit_bytes_to_buffer(8);
|
||||
}
|
||||
|
||||
// I am not an expert on the topic, and I don't want to write a thesis.
|
||||
// However, it is good to outline here the basic algorithm and the few data
|
||||
// structures and optimizations here that make this implementation fast.
|
||||
// The basic idea behind LZW is to build a table of previously seen runs
|
||||
// addressed by a short id (herein called output code). All data is
|
||||
// referenced by a code, which represents one or more values from the
|
||||
// original input stream. All input bytes can be referenced as the same
|
||||
// value as an output code. So if you didn't want any compression, you
|
||||
// could more or less just output the original bytes as codes (there are
|
||||
// some details to this, but it is the idea). In order to achieve
|
||||
// compression, values greater then the input range (codes can be up to
|
||||
// 12-bit while input only 8-bit) represent a sequence of previously seen
|
||||
// inputs. The decompressor is able to build the same mapping while
|
||||
// decoding, so there is always a shared common knowledge between the
|
||||
// encoding and decoder, which is also important for "timing" aspects like
|
||||
// how to handle variable bit width code encoding.
|
||||
//
|
||||
// One obvious but very important consequence of the table system is there
|
||||
// is always a unique id (at most 12-bits) to map the runs. 'A' might be
|
||||
// 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship
|
||||
// can be used for an effecient lookup strategy for the code mapping. We
|
||||
// need to know if a run has been seen before, and be able to map that run
|
||||
// to the output code. Since we start with known unique ids (input bytes),
|
||||
// and then from those build more unique ids (table entries), we can
|
||||
// continue this chain (almost like a linked list) to always have small
|
||||
// integer values that represent the current byte chains in the encoder.
|
||||
// This means instead of tracking the input bytes (AAAABCD) to know our
|
||||
// current state, we can track the table entry for AAAABC (it is guaranteed
|
||||
// to exist by the nature of the algorithm) and the next character D.
|
||||
// Therefor the tuple of (table_entry, byte) is guaranteed to also be
|
||||
// unique. This allows us to create a simple lookup key for mapping input
|
||||
// sequences to codes (table indices) without having to store or search
|
||||
// any of the code sequences. So if 'AAAA' has a table entry of 12, the
|
||||
// tuple of ('AAAA', K) for any input byte K will be unique, and can be our
|
||||
// key. This leads to a integer value at most 20-bits, which can always
|
||||
// fit in an SMI value and be used as a fast sparse array / object key.
|
||||
|
||||
// Output code for the current contents of the index buffer.
|
||||
var ib_code = index_stream[0] & code_mask; // Load first input index.
|
||||
var code_table = { }; // Key'd on our 20-bit "tuple".
|
||||
|
||||
emit_code(clear_code); // Spec says first code should be a clear code.
|
||||
|
||||
// First index already loaded, process the rest of the stream.
|
||||
for (var i = 1, il = index_stream.length; i < il; ++i) {
|
||||
var k = index_stream[i] & code_mask;
|
||||
var cur_key = ib_code << 8 | k; // (prev, k) unique tuple.
|
||||
var cur_code = code_table[cur_key]; // buffer + k.
|
||||
|
||||
// Check if we have to create a new code table entry.
|
||||
if (cur_code === undefined) { // We don't have buffer + k.
|
||||
// Emit index buffer (without k).
|
||||
// This is an inline version of emit_code, because this is the core
|
||||
// writing routine of the compressor (and V8 cannot inline emit_code
|
||||
// because it is a closure here in a different context). Additionally
|
||||
// we can call emit_byte_to_buffer less often, because we can have
|
||||
// 30-bits (from our 31-bit signed SMI), and we know our codes will only
|
||||
// be 12-bits, so can safely have 18-bits there without overflow.
|
||||
// emit_code(ib_code);
|
||||
cur |= ib_code << cur_shift;
|
||||
cur_shift += cur_code_size;
|
||||
while (cur_shift >= 8) {
|
||||
buf[p++] = cur & 0xff;
|
||||
cur >>= 8; cur_shift -= 8;
|
||||
if (p === cur_subblock + 256) { // Finished a subblock.
|
||||
buf[cur_subblock] = 255;
|
||||
cur_subblock = p++;
|
||||
}
|
||||
}
|
||||
|
||||
if (next_code === 4096) { // Table full, need a clear.
|
||||
emit_code(clear_code);
|
||||
next_code = eoi_code + 1;
|
||||
cur_code_size = min_code_size + 1;
|
||||
code_table = { };
|
||||
} else { // Table not full, insert a new entry.
|
||||
// Increase our variable bit code sizes if necessary. This is a bit
|
||||
// tricky as it is based on "timing" between the encoding and
|
||||
// decoder. From the encoders perspective this should happen after
|
||||
// we've already emitted the index buffer and are about to create the
|
||||
// first table entry that would overflow our current code bit size.
|
||||
if (next_code >= (1 << cur_code_size)) ++cur_code_size;
|
||||
code_table[cur_key] = next_code++; // Insert into code table.
|
||||
}
|
||||
|
||||
ib_code = k; // Index buffer to single input k.
|
||||
} else {
|
||||
ib_code = cur_code; // Index buffer to sequence in code table.
|
||||
}
|
||||
}
|
||||
|
||||
emit_code(ib_code); // There will still be something in the index buffer.
|
||||
emit_code(eoi_code); // End Of Information.
|
||||
|
||||
// Flush / finalize the sub-blocks stream to the buffer.
|
||||
emit_bytes_to_buffer(1);
|
||||
|
||||
// Finish the sub-blocks, writing out any unfinished lengths and
|
||||
// terminating with a sub-block of length 0. If we have already started
|
||||
// but not yet used a sub-block it can just become the terminator.
|
||||
if (cur_subblock + 1 === p) { // Started but unused.
|
||||
buf[cur_subblock] = 0;
|
||||
} else { // Started and used, write length and additional terminator block.
|
||||
buf[cur_subblock] = p - cur_subblock - 1;
|
||||
buf[p++] = 0;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function GifReader(buf) {
|
||||
var p = 0;
|
||||
|
||||
// - Header (GIF87a or GIF89a).
|
||||
if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
|
||||
buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
|
||||
throw new Error("Invalid GIF 87a/89a header.");
|
||||
}
|
||||
|
||||
// - Logical Screen Descriptor.
|
||||
var width = buf[p++] | buf[p++] << 8;
|
||||
var height = buf[p++] | buf[p++] << 8;
|
||||
var pf0 = buf[p++]; // <Packed Fields>.
|
||||
var global_palette_flag = pf0 >> 7;
|
||||
var num_global_colors_pow2 = pf0 & 0x7;
|
||||
var num_global_colors = 1 << (num_global_colors_pow2 + 1);
|
||||
var background = buf[p++];
|
||||
buf[p++]; // Pixel aspect ratio (unused?).
|
||||
|
||||
var global_palette_offset = null;
|
||||
var global_palette_size = null;
|
||||
|
||||
if (global_palette_flag) {
|
||||
global_palette_offset = p;
|
||||
global_palette_size = num_global_colors;
|
||||
p += num_global_colors * 3; // Seek past palette.
|
||||
}
|
||||
|
||||
var no_eof = true;
|
||||
|
||||
var frames = [ ];
|
||||
|
||||
var delay = 0;
|
||||
var transparent_index = null;
|
||||
var disposal = 0; // 0 - No disposal specified.
|
||||
var loop_count = null;
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
while (no_eof && p < buf.length) {
|
||||
switch (buf[p++]) {
|
||||
case 0x21: // Graphics Control Extension Block
|
||||
switch (buf[p++]) {
|
||||
case 0xff: // Application specific block
|
||||
// Try if it's a Netscape block (with animation loop counter).
|
||||
if (buf[p ] !== 0x0b || // 21 FF already read, check block size.
|
||||
// NETSCAPE2.0
|
||||
buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&
|
||||
buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&
|
||||
buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&
|
||||
buf[p+10] == 0x2e && buf[p+11] == 0x30 &&
|
||||
// Sub-block
|
||||
buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {
|
||||
p += 14;
|
||||
loop_count = buf[p++] | buf[p++] << 8;
|
||||
p++; // Skip terminator.
|
||||
} else { // We don't know what it is, just try to get past it.
|
||||
p += 12;
|
||||
while (true) { // Seek through subblocks.
|
||||
var block_size = buf[p++];
|
||||
// Bad block size (ex: undefined from an out of bounds read).
|
||||
if (!(block_size >= 0)) throw Error("Invalid block size");
|
||||
if (block_size === 0) break; // 0 size is terminator
|
||||
p += block_size;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf9: // Graphics Control Extension
|
||||
if (buf[p++] !== 0x4 || buf[p+4] !== 0)
|
||||
throw new Error("Invalid graphics extension block.");
|
||||
var pf1 = buf[p++];
|
||||
delay = buf[p++] | buf[p++] << 8;
|
||||
transparent_index = buf[p++];
|
||||
if ((pf1 & 1) === 0) transparent_index = null;
|
||||
disposal = pf1 >> 2 & 0x7;
|
||||
p++; // Skip terminator.
|
||||
break;
|
||||
|
||||
case 0xfe: // Comment Extension.
|
||||
while (true) { // Seek through subblocks.
|
||||
var block_size = buf[p++];
|
||||
// Bad block size (ex: undefined from an out of bounds read).
|
||||
if (!(block_size >= 0)) throw Error("Invalid block size");
|
||||
if (block_size === 0) break; // 0 size is terminator
|
||||
// console.log(buf.slice(p, p+block_size).toString('ascii'));
|
||||
p += block_size;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
"Unknown graphic control label: 0x" + buf[p-1].toString(16));
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x2c: // Image Descriptor.
|
||||
var x = buf[p++] | buf[p++] << 8;
|
||||
var y = buf[p++] | buf[p++] << 8;
|
||||
var w = buf[p++] | buf[p++] << 8;
|
||||
var h = buf[p++] | buf[p++] << 8;
|
||||
var pf2 = buf[p++];
|
||||
var local_palette_flag = pf2 >> 7;
|
||||
var interlace_flag = pf2 >> 6 & 1;
|
||||
var num_local_colors_pow2 = pf2 & 0x7;
|
||||
var num_local_colors = 1 << (num_local_colors_pow2 + 1);
|
||||
var palette_offset = global_palette_offset;
|
||||
var palette_size = global_palette_size;
|
||||
var has_local_palette = false;
|
||||
if (local_palette_flag) {
|
||||
var has_local_palette = true;
|
||||
palette_offset = p; // Override with local palette.
|
||||
palette_size = num_local_colors;
|
||||
p += num_local_colors * 3; // Seek past palette.
|
||||
}
|
||||
|
||||
var data_offset = p;
|
||||
|
||||
p++; // codesize
|
||||
while (true) {
|
||||
var block_size = buf[p++];
|
||||
// Bad block size (ex: undefined from an out of bounds read).
|
||||
if (!(block_size >= 0)) throw Error("Invalid block size");
|
||||
if (block_size === 0) break; // 0 size is terminator
|
||||
p += block_size;
|
||||
}
|
||||
|
||||
frames.push({x: x, y: y, width: w, height: h,
|
||||
has_local_palette: has_local_palette,
|
||||
palette_offset: palette_offset,
|
||||
palette_size: palette_size,
|
||||
data_offset: data_offset,
|
||||
data_length: p - data_offset,
|
||||
transparent_index: transparent_index,
|
||||
interlaced: !!interlace_flag,
|
||||
delay: delay,
|
||||
disposal: disposal});
|
||||
break;
|
||||
|
||||
case 0x3b: // Trailer Marker (end of file).
|
||||
no_eof = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.numFrames = function() {
|
||||
return frames.length;
|
||||
};
|
||||
|
||||
this.loopCount = function() {
|
||||
return loop_count;
|
||||
};
|
||||
|
||||
this.frameInfo = function(frame_num) {
|
||||
if (frame_num < 0 || frame_num >= frames.length)
|
||||
throw new Error("Frame index out of range.");
|
||||
return frames[frame_num];
|
||||
}
|
||||
|
||||
this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {
|
||||
var frame = this.frameInfo(frame_num);
|
||||
var num_pixels = frame.width * frame.height;
|
||||
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
|
||||
GifReaderLZWOutputIndexStream(
|
||||
buf, frame.data_offset, index_stream, num_pixels);
|
||||
var palette_offset = frame.palette_offset;
|
||||
|
||||
// NOTE(deanm): It seems to be much faster to compare index to 256 than
|
||||
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
|
||||
// the profile, not sure if it's related to using a Uint8Array.
|
||||
var trans = frame.transparent_index;
|
||||
if (trans === null) trans = 256;
|
||||
|
||||
// We are possibly just blitting to a portion of the entire frame.
|
||||
// That is a subrect within the framerect, so the additional pixels
|
||||
// must be skipped over after we finished a scanline.
|
||||
var framewidth = frame.width;
|
||||
var framestride = width - framewidth;
|
||||
var xleft = framewidth; // Number of subrect pixels left in scanline.
|
||||
|
||||
// Output indicies of the top left and bottom right corners of the subrect.
|
||||
var opbeg = ((frame.y * width) + frame.x) * 4;
|
||||
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
|
||||
var op = opbeg;
|
||||
|
||||
var scanstride = framestride * 4;
|
||||
|
||||
// Use scanstride to skip past the rows when interlacing. This is skipping
|
||||
// 7 rows for the first two passes, then 3 then 1.
|
||||
if (frame.interlaced === true) {
|
||||
scanstride += width * 4 * 7; // Pass 1.
|
||||
}
|
||||
|
||||
var interlaceskip = 8; // Tracking the row interval in the current pass.
|
||||
|
||||
for (var i = 0, il = index_stream.length; i < il; ++i) {
|
||||
var index = index_stream[i];
|
||||
|
||||
if (xleft === 0) { // Beginning of new scan line
|
||||
op += scanstride;
|
||||
xleft = framewidth;
|
||||
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
|
||||
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
|
||||
// interlaceskip / 2 * 4 is interlaceskip << 1.
|
||||
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
|
||||
interlaceskip >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (index === trans) {
|
||||
op += 4;
|
||||
} else {
|
||||
var r = buf[palette_offset + index * 3];
|
||||
var g = buf[palette_offset + index * 3 + 1];
|
||||
var b = buf[palette_offset + index * 3 + 2];
|
||||
pixels[op++] = b;
|
||||
pixels[op++] = g;
|
||||
pixels[op++] = r;
|
||||
pixels[op++] = 255;
|
||||
}
|
||||
--xleft;
|
||||
}
|
||||
};
|
||||
|
||||
// I will go to copy and paste hell one day...
|
||||
this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {
|
||||
var frame = this.frameInfo(frame_num);
|
||||
var num_pixels = frame.width * frame.height;
|
||||
var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
|
||||
GifReaderLZWOutputIndexStream(
|
||||
buf, frame.data_offset, index_stream, num_pixels);
|
||||
var palette_offset = frame.palette_offset;
|
||||
|
||||
// NOTE(deanm): It seems to be much faster to compare index to 256 than
|
||||
// to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
|
||||
// the profile, not sure if it's related to using a Uint8Array.
|
||||
var trans = frame.transparent_index;
|
||||
if (trans === null) trans = 256;
|
||||
|
||||
// We are possibly just blitting to a portion of the entire frame.
|
||||
// That is a subrect within the framerect, so the additional pixels
|
||||
// must be skipped over after we finished a scanline.
|
||||
var framewidth = frame.width;
|
||||
var framestride = width - framewidth;
|
||||
var xleft = framewidth; // Number of subrect pixels left in scanline.
|
||||
|
||||
// Output indicies of the top left and bottom right corners of the subrect.
|
||||
var opbeg = ((frame.y * width) + frame.x) * 4;
|
||||
var opend = ((frame.y + frame.height) * width + frame.x) * 4;
|
||||
var op = opbeg;
|
||||
|
||||
var scanstride = framestride * 4;
|
||||
|
||||
// Use scanstride to skip past the rows when interlacing. This is skipping
|
||||
// 7 rows for the first two passes, then 3 then 1.
|
||||
if (frame.interlaced === true) {
|
||||
scanstride += width * 4 * 7; // Pass 1.
|
||||
}
|
||||
|
||||
var interlaceskip = 8; // Tracking the row interval in the current pass.
|
||||
|
||||
for (var i = 0, il = index_stream.length; i < il; ++i) {
|
||||
var index = index_stream[i];
|
||||
|
||||
if (xleft === 0) { // Beginning of new scan line
|
||||
op += scanstride;
|
||||
xleft = framewidth;
|
||||
if (op >= opend) { // Catch the wrap to switch passes when interlacing.
|
||||
scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
|
||||
// interlaceskip / 2 * 4 is interlaceskip << 1.
|
||||
op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
|
||||
interlaceskip >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (index === trans) {
|
||||
op += 4;
|
||||
} else {
|
||||
var r = buf[palette_offset + index * 3];
|
||||
var g = buf[palette_offset + index * 3 + 1];
|
||||
var b = buf[palette_offset + index * 3 + 2];
|
||||
pixels[op++] = r;
|
||||
pixels[op++] = g;
|
||||
pixels[op++] = b;
|
||||
pixels[op++] = 255;
|
||||
}
|
||||
--xleft;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {
|
||||
var min_code_size = code_stream[p++];
|
||||
|
||||
var clear_code = 1 << min_code_size;
|
||||
var eoi_code = clear_code + 1;
|
||||
var next_code = eoi_code + 1;
|
||||
|
||||
var cur_code_size = min_code_size + 1; // Number of bits per code.
|
||||
// NOTE: This shares the same name as the encoder, but has a different
|
||||
// meaning here. Here this masks each code coming from the code stream.
|
||||
var code_mask = (1 << cur_code_size) - 1;
|
||||
var cur_shift = 0;
|
||||
var cur = 0;
|
||||
|
||||
var op = 0; // Output pointer.
|
||||
|
||||
var subblock_size = code_stream[p++];
|
||||
|
||||
// TODO(deanm): Would using a TypedArray be any faster? At least it would
|
||||
// solve the fast mode / backing store uncertainty.
|
||||
// var code_table = Array(4096);
|
||||
var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits.
|
||||
|
||||
var prev_code = null; // Track code-1.
|
||||
|
||||
while (true) {
|
||||
// Read up to two bytes, making sure we always 12-bits for max sized code.
|
||||
while (cur_shift < 16) {
|
||||
if (subblock_size === 0) break; // No more data to be read.
|
||||
|
||||
cur |= code_stream[p++] << cur_shift;
|
||||
cur_shift += 8;
|
||||
|
||||
if (subblock_size === 1) { // Never let it get to 0 to hold logic above.
|
||||
subblock_size = code_stream[p++]; // Next subblock.
|
||||
} else {
|
||||
--subblock_size;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(deanm): We should never really get here, we should have received
|
||||
// and EOI.
|
||||
if (cur_shift < cur_code_size)
|
||||
break;
|
||||
|
||||
var code = cur & code_mask;
|
||||
cur >>= cur_code_size;
|
||||
cur_shift -= cur_code_size;
|
||||
|
||||
// TODO(deanm): Maybe should check that the first code was a clear code,
|
||||
// at least this is what you're supposed to do. But actually our encoder
|
||||
// now doesn't emit a clear code first anyway.
|
||||
if (code === clear_code) {
|
||||
// We don't actually have to clear the table. This could be a good idea
|
||||
// for greater error checking, but we don't really do any anyway. We
|
||||
// will just track it with next_code and overwrite old entries.
|
||||
|
||||
next_code = eoi_code + 1;
|
||||
cur_code_size = min_code_size + 1;
|
||||
code_mask = (1 << cur_code_size) - 1;
|
||||
|
||||
// Don't update prev_code ?
|
||||
prev_code = null;
|
||||
continue;
|
||||
} else if (code === eoi_code) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We have a similar situation as the decoder, where we want to store
|
||||
// variable length entries (code table entries), but we want to do in a
|
||||
// faster manner than an array of arrays. The code below stores sort of a
|
||||
// linked list within the code table, and then "chases" through it to
|
||||
// construct the dictionary entries. When a new entry is created, just the
|
||||
// last byte is stored, and the rest (prefix) of the entry is only
|
||||
// referenced by its table entry. Then the code chases through the
|
||||
// prefixes until it reaches a single byte code. We have to chase twice,
|
||||
// first to compute the length, and then to actually copy the data to the
|
||||
// output (backwards, since we know the length). The alternative would be
|
||||
// storing something in an intermediate stack, but that doesn't make any
|
||||
// more sense. I implemented an approach where it also stored the length
|
||||
// in the code table, although it's a bit tricky because you run out of
|
||||
// bits (12 + 12 + 8), but I didn't measure much improvements (the table
|
||||
// entries are generally not the long). Even when I created benchmarks for
|
||||
// very long table entries the complexity did not seem worth it.
|
||||
// The code table stores the prefix entry in 12 bits and then the suffix
|
||||
// byte in 8 bits, so each entry is 20 bits.
|
||||
|
||||
var chase_code = code < next_code ? code : prev_code;
|
||||
|
||||
// Chase what we will output, either {CODE} or {CODE-1}.
|
||||
var chase_length = 0;
|
||||
var chase = chase_code;
|
||||
while (chase > clear_code) {
|
||||
chase = code_table[chase] >> 8;
|
||||
++chase_length;
|
||||
}
|
||||
|
||||
var k = chase;
|
||||
|
||||
var op_end = op + chase_length + (chase_code !== code ? 1 : 0);
|
||||
if (op_end > output_length) {
|
||||
console.log("Warning, gif stream longer than expected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Already have the first byte from the chase, might as well write it fast.
|
||||
output[op++] = k;
|
||||
|
||||
op += chase_length;
|
||||
var b = op; // Track pointer, writing backwards.
|
||||
|
||||
if (chase_code !== code) // The case of emitting {CODE-1} + k.
|
||||
output[op++] = k;
|
||||
|
||||
chase = chase_code;
|
||||
while (chase_length--) {
|
||||
chase = code_table[chase];
|
||||
output[--b] = chase & 0xff; // Write backwards.
|
||||
chase >>= 8; // Pull down to the prefix code.
|
||||
}
|
||||
|
||||
if (prev_code !== null && next_code < 4096) {
|
||||
code_table[next_code++] = prev_code << 8 | k;
|
||||
// TODO(deanm): Figure out this clearing vs code growth logic better. I
|
||||
// have an feeling that it should just happen somewhere else, for now it
|
||||
// is awkward between when we grow past the max and then hit a clear code.
|
||||
// For now just check if we hit the max 12-bits (then a clear code should
|
||||
// follow, also of course encoded in 12-bits).
|
||||
if (next_code >= code_mask+1 && cur_code_size < 12) {
|
||||
++cur_code_size;
|
||||
code_mask = code_mask << 1 | 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev_code = code;
|
||||
}
|
||||
|
||||
if (op !== output_length) {
|
||||
console.log("Warning, gif stream shorter than expected.");
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
// CommonJS.
|
||||
//try { exports.GifWriter = GifWriter; exports.GifReader = GifReader } catch(e) {}
|
||||
try { exports.GifWriter = GifWriter; } catch(e) {}
|
||||
1181
wled00/data/pixelforge/pixelforge.htm
Normal file
1181
wled00/data/pixelforge/pixelforge.htm
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1977,4 +1977,4 @@
|
||||
return isValid;
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
</html>
|
||||
@@ -4,17 +4,23 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>WLED Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<script>
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=0'), false); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
}
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
// load style.css then initialize
|
||||
l.onload = () => loadResources(['style.css'], () => {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=0'), false);
|
||||
});
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
@import url("style.css");
|
||||
body {
|
||||
text-align: center;
|
||||
background: #222;
|
||||
height: 100px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -22,21 +28,17 @@
|
||||
--h: 9vh;
|
||||
}
|
||||
button {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-family: Verdana, Helvetica, sans-serif;
|
||||
display: block;
|
||||
border: 1px solid #333;
|
||||
border-radius: var(--h);
|
||||
font-size: 6vmin;
|
||||
height: var(--h);
|
||||
width: calc(100% - 40px);
|
||||
margin: 2vh auto 0;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<button type=submit id="b" onclick="window.location=getURL('/')">Back</button>
|
||||
<button type="submit" onclick="window.location=getURL('/settings/wifi')">WiFi Setup</button>
|
||||
<button type="submit" onclick="window.location=getURL('/settings/leds')">LED Preferences</button>
|
||||
|
||||
@@ -4,11 +4,20 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>2D Set-up</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var maxPanels=64;
|
||||
var ctx = null;
|
||||
function fS(){d.Sf.submit();} // <button type=submit> sometimes didn't work
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=10'), false, undefined, ()=>{
|
||||
@@ -238,9 +247,8 @@ Y: <input name="P${i}Y" type="number" min="0" max="255" value="0" oninput="UI()"
|
||||
gId("MD").innerHTML = "Matrix Dimensions (W*H=LC): " + maxWidth + " x " + maxHeight + " = " + maxWidth * maxHeight;
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/2D')">?</button></div>
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>DMX Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
function HW(){window.open("https://kno.wled.ge/interfaces/dmx-output/");}
|
||||
function GCH(num) {
|
||||
gId('dmxchannels').innerHTML += "";
|
||||
@@ -38,9 +46,8 @@
|
||||
if (loc) d.Sf.action = getURL('/settings/dmx');
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="HW()">?</button></div>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>LED Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
|
||||
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxBT=4; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
|
||||
var customStarts=false,startsDirty=[];
|
||||
function off(n) { gN(n).value = -1;}
|
||||
// these functions correspond to C macros found in const.h
|
||||
@@ -26,6 +26,15 @@
|
||||
function numPins(t){ return Math.max(gT(t).t.length, 1); } // type length determines number of GPIO pins
|
||||
function chrID(x) { return String.fromCharCode((x<10?48:55)+x); }
|
||||
function toNum(c) { let n=c.charCodeAt(0); return (n>=48 && n<=57)?n-48:(n>=65 && n<=90)?n-55:0; } // convert char (0-9A-Z) to number (0-35)
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=2'), false, ()=>{
|
||||
@@ -43,7 +52,7 @@
|
||||
}); // If we set async false, file is loaded and executed, then next statement is processed
|
||||
if (loc) d.Sf.action = getURL('/settings/leds');
|
||||
}
|
||||
function bLimits(b,v,p,m,l,o=5,d=2,a=6) {
|
||||
function bLimits(b,v,p,m,l,o=5,d=2,a=6,n=4) {
|
||||
maxB = b; // maxB - max physical (analog + digital) buses: 32 - ESP32, 14 - S3/S2, 6 - C3, 4 - 8266
|
||||
maxV = v; // maxV - min virtual buses: 6 - ESP32/S3, 4 - S2/C3, 3 - ESP8266 (only used to distinguish S2/S3)
|
||||
maxPB = p; // maxPB - max LEDs per bus
|
||||
@@ -52,6 +61,7 @@
|
||||
maxCO = o; // maxCO - max Color Order mappings
|
||||
maxD = d; // maxD - max digital channels (can be changed if using ESP32 parallel I2S): 16 - ESP32, 12 - S3/S2, 2 - C3, 3 - 8266
|
||||
maxA = a; // maxA - max analog channels: 16 - ESP32, 8 - S3/S2, 6 - C3, 5 - 8266
|
||||
maxBT = n; // maxBT - max buttons
|
||||
}
|
||||
function is8266() { return maxA == 5 && maxD == 3; } // NOTE: see const.h
|
||||
function is32() { return maxA == 16 && maxD == 16; } // NOTE: see const.h
|
||||
@@ -267,10 +277,10 @@
|
||||
}
|
||||
|
||||
// enable/disable LED fields
|
||||
updateTypeDropdowns(); // restrict bus types in dropdowns to max allowed digital/analog buses
|
||||
let dC = 0; // count of digital buses (for parallel I2S)
|
||||
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
|
||||
LTs.forEach((s,i)=>{
|
||||
if (i < LTs.length-1) s.disabled = true; // prevent changing type (as we can't update options)
|
||||
// is the field a LED type?
|
||||
var n = s.name.substring(2,3); // bus number (0-Z)
|
||||
var t = parseInt(s.value);
|
||||
@@ -447,17 +457,8 @@
|
||||
{
|
||||
var o = gEBCN("iST");
|
||||
var i = o.length;
|
||||
let disable = (sel,opt) => { sel.querySelectorAll(opt).forEach((o)=>{o.disabled=true;}); }
|
||||
|
||||
var f = gId("mLC");
|
||||
let digitalB = 0, analogB = 0, twopinB = 0, virtB = 0;
|
||||
f.querySelectorAll("select[name^=LT]").forEach((s)=>{
|
||||
let t = s.value;
|
||||
if (isDig(t) && !isD2P(t)) digitalB++;
|
||||
if (isD2P(t)) twopinB++;
|
||||
if (isPWM(t)) analogB += numPins(t); // each GPIO is assigned to a channel
|
||||
if (isVir(t)) virtB++;
|
||||
});
|
||||
|
||||
if ((n==1 && i>=36) || (n==-1 && i==0)) return; // used to be i>=maxB+maxV when virtual buses were limited (now :"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
var s = chrID(i);
|
||||
@@ -467,7 +468,7 @@
|
||||
var cn = `<div class="iST">
|
||||
<hr class="sml">
|
||||
${i+1}:
|
||||
<select name="LT${s}" onchange="UI(true)"></select><br>
|
||||
<select name="LT${s}" onchange="updateTypeDropdowns();UI(true)"></select><br>
|
||||
<div id="abl${s}">
|
||||
mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
|
||||
<option value="55" selected>55mA (typ. 5V WS281x)</option>
|
||||
@@ -522,18 +523,15 @@ mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
|
||||
}
|
||||
});
|
||||
enLA(d.Sf["LAsel"+s],s); // update LED mA
|
||||
// disable inappropriate LED types
|
||||
// temporarily set to virtual (network) type to avoid "same type" exception during dropdown update
|
||||
let sel = d.getElementsByName("LT"+s)[0];
|
||||
// 32 & S2 supports mono I2S as well as parallel so we need to take that into account; S3 only supports parallel
|
||||
let maxDB = maxD - (is32() || isS2() || isS3() ? (!d.Sf["PR"].checked)*8 - (!isS3()) : 0); // adjust max digital buses if parallel I2S is not used
|
||||
if (digitalB >= maxDB) disable(sel,'option[data-type="D"]'); // NOTE: see isDig()
|
||||
if (twopinB >= 2) disable(sel,'option[data-type="2P"]'); // NOTE: see isD2P() (we will only allow 2 2pin buses)
|
||||
disable(sel,`option[data-type^="${'A'.repeat(maxA-analogB+1)}"]`); // NOTE: see isPWM()
|
||||
sel.value = sel.querySelector('option[data-type="N"]').value;
|
||||
updateTypeDropdowns(); // update valid bus options including this new one
|
||||
sel.selectedIndex = sel.querySelector('option:not(:disabled)').index;
|
||||
updateTypeDropdowns(); // update again for the newly selected type
|
||||
}
|
||||
if (n==-1) {
|
||||
o[--i].remove();--i;
|
||||
o[i].querySelector("[name^=LT]").disabled = false;
|
||||
}
|
||||
|
||||
gId("+").style.display = (i<35) ? "inline":"none"; // was maxB+maxV-1 when virtual buses were limited (now :"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
@@ -600,9 +598,9 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
}
|
||||
|
||||
function addBtn(i,p,t) {
|
||||
var c = gId("btns").innerHTML;
|
||||
var b = gId("btns");
|
||||
var s = chrID(i);
|
||||
c += `Button ${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" class="xs" value="${p}">`;
|
||||
var c = `<div id="btn${i}">#${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" min="-1" max="${d.max_gpio}" class="xs" value="${p}">`;
|
||||
c += ` <select name="BE${s}">`
|
||||
c += `<option value="0" ${t==0?"selected":""}>Disabled</option>`;
|
||||
c += `<option value="2" ${t==2?"selected":""}>Pushbutton</option>`;
|
||||
@@ -614,8 +612,24 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
c += `<option value="8" ${t==8?"selected":""}>Analog inverted</option>`;
|
||||
c += `<option value="9" ${t==9?"selected":""}>Touch (switch)</option>`;
|
||||
c += `</select>`;
|
||||
c += `<span style="cursor: pointer;" onclick="off('BT${s}')"> ✕</span><br>`;
|
||||
gId("btns").innerHTML = c;
|
||||
c += `<span style="cursor: pointer;" onclick="off('BT${s}')"> ✕</span><br></div>`;
|
||||
b.insertAdjacentHTML("beforeend", c);
|
||||
btnBtn();
|
||||
pinDropdowns();
|
||||
UI();
|
||||
}
|
||||
function remBtn() {
|
||||
var b = gId("btns");
|
||||
if (b.children.length <= 1) return;
|
||||
b.lastElementChild.remove();
|
||||
btnBtn();
|
||||
pinDropdowns();
|
||||
UI();
|
||||
}
|
||||
function btnBtn() {
|
||||
var b = gId("btns");
|
||||
gId("btn_rem").style.display = (b.children.length > 1) ? "inline" : "none";
|
||||
gId("btn_add").style.display = (b.children.length < maxBT) ? "inline" : "none";
|
||||
}
|
||||
function tglSi(cs) {
|
||||
customStarts = cs;
|
||||
@@ -812,10 +826,37 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
// dynamically enforce bus type availability based on current usage
|
||||
function updateTypeDropdowns() {
|
||||
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
|
||||
let digitalB = 0, analogB = 0, twopinB = 0, virtB = 0;
|
||||
// count currently used buses
|
||||
LTs.forEach(sel => {
|
||||
let t = parseInt(sel.value);
|
||||
if (isDig(t) && !isD2P(t)) digitalB++;
|
||||
if (isPWM(t)) analogB += numPins(t);
|
||||
if (isD2P(t)) twopinB++;
|
||||
if (isVir(t)) virtB++;
|
||||
});
|
||||
// enable/disable type options according to limits in dropdowns
|
||||
LTs.forEach(sel => {
|
||||
const curType = parseInt(sel.value);
|
||||
const disable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = true);
|
||||
const enable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = false);
|
||||
enable('option'); // reset all first
|
||||
// max digital buses: ESP32 & S2 support mono I2S as well as parallel so we need to take that into account; S3 only supports parallel
|
||||
// supported outputs using parallel I2S/mono I2S: S2: 12/5, S3: 12/4, ESP32: 16/9
|
||||
let maxDB = maxD - ((is32() || isS2() || isS3()) ? (!d.Sf["PR"].checked) * 8 - (!isS3()) : 0); // adjust max digital buses if parallel I2S is not used
|
||||
// disallow adding more of a type that has reached its limit but allow changing the current type
|
||||
if (digitalB >= maxDB && !(isDig(curType) && !isD2P(curType))) disable('option[data-type="D"]');
|
||||
if (twopinB >= 2 && !isD2P(curType)) disable('option[data-type="2P"]');
|
||||
// Disable PWM types that need more pins than available (accounting for current type's pins if PWM)
|
||||
disable(`option[data-type^="${'A'.repeat(maxA - analogB + (isPWM(curType)?numPins(curType):0) + 1)}"]`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#led-settings')">?</button></div>
|
||||
@@ -867,10 +908,16 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
<div id="com_entries"></div>
|
||||
<hr class="sml">
|
||||
<button type="button" id="com_add" onclick="addCOM()">+</button>
|
||||
<button type="button" id="com_rem" onclick="remCOM()">-</button><br>
|
||||
<button type="button" id="com_rem" onclick="remCOM()">-</button>
|
||||
</div>
|
||||
<hr class="sml">
|
||||
<div id="btns"></div>
|
||||
<div id="btn_wrap">
|
||||
Buttons:
|
||||
<div id="btns"></div>
|
||||
<hr class="sml">
|
||||
<button type="button" id="btn_add" onclick="addBtn(gId('btns').children.length,-1,0)">+</button>
|
||||
<button type="button" id="btn_rem" onclick="remBtn()">-</button>
|
||||
</div>
|
||||
Disable internal pull-up/down: <input type="checkbox" name="IP"><br>
|
||||
Touch threshold: <input type="number" class="s" min="0" max="100" name="TT" required><br>
|
||||
<hr class="sml">
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>Misc Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
function U() { window.open(getURL("/update"),"_self"); }
|
||||
function checkNum(o) {
|
||||
const specialkeys = ["Backspace", "Tab", "Enter", "Shift", "Control", "Alt", "Pause", "CapsLock", "Escape", "Space", "PageUp", "PageDown", "End", "Home", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", "Insert", "Delete"];
|
||||
@@ -30,11 +38,8 @@
|
||||
if (loc) d.Sf.action = getURL('/settings/sec');
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url("style.css");
|
||||
</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#security-settings')">?</button></div>
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>Sync Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
function adj(){if (d.Sf.DI.value == 6454) {if (d.Sf.EU.value == 1) d.Sf.EU.value = 0;}
|
||||
else if (d.Sf.DI.value == 5568) {if (d.Sf.DA.value == 0) d.Sf.DA.value = 1; if (d.Sf.EU.value == 0) d.Sf.EU.value = 1;} }
|
||||
function FC()
|
||||
@@ -43,9 +51,8 @@
|
||||
function hideDMXInput(){gId("dmxInput").style.display="none";}
|
||||
function hideNoDMXInput(){gId("dmxInputOff").style.display="none";}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post" onsubmit="GC()">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('interfaces/udp-notifier/')">?</button></div>
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<meta charset="utf-8">
|
||||
<title>Time Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var el=false;
|
||||
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
|
||||
@@ -119,9 +128,8 @@
|
||||
if (parseFloat(d.Sf.LN.value)<0) { d.Sf.LNR.value = "W"; d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); } else d.Sf.LNR.value = "E";
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post" onsubmit="Wd()">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#time-settings')">?</button></div>
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>UI Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var initial_ds, initial_st, initial_su, oldUrl;
|
||||
var sett = null;
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
var l = {
|
||||
"comp":{
|
||||
"labels":"Show button labels",
|
||||
@@ -208,9 +217,8 @@
|
||||
gId("theme_bg_rnd").checked = false;
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#user-interface-settings')">?</button></div>
|
||||
|
||||
@@ -4,12 +4,22 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>Usermod Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
var umCfg = {};
|
||||
var pins = [], pinO = [], owner;
|
||||
var urows;
|
||||
var numM = 0;
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
|
||||
function S() {
|
||||
getLoc();
|
||||
// load settings and insert values into DOM
|
||||
@@ -269,10 +279,9 @@
|
||||
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
|
||||
}
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H()">?</button></div>
|
||||
|
||||
@@ -4,8 +4,17 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<title>WiFi Settings</title>
|
||||
<script src="common.js" type="text/javascript"></script>
|
||||
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
|
||||
<script>
|
||||
// load common.js with retry on error
|
||||
(function loadFiles() {
|
||||
const l = document.createElement('script');
|
||||
l.src = 'common.js';
|
||||
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
|
||||
l.onerror = () => setTimeout(loadFiles, 100);
|
||||
document.head.appendChild(l);
|
||||
})();
|
||||
|
||||
var scanLoops = 0, preScanSSID = "";
|
||||
var maxNetworks = 3;
|
||||
function N() {
|
||||
@@ -178,9 +187,8 @@ Static subnet mask:<br>
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>@import url("style.css");</style>
|
||||
</head>
|
||||
<body onload="S()">
|
||||
<body>
|
||||
<form id="form_s" name="Sf" method="post">
|
||||
<div class="toprow">
|
||||
<div class="helpB"><button type="button" onclick="H('features/settings/#wifi-settings')">?</button></div>
|
||||
@@ -270,6 +278,7 @@ Static subnet mask:<br>
|
||||
<option value="5">TwilightLord-ESP32</option>
|
||||
<option value="3">WESP32</option>
|
||||
<option value="1">WT32-ETH01</option>
|
||||
<option value="13">Gledopto</option>
|
||||
</select><br><br>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
@@ -36,6 +36,7 @@ button, .btn {
|
||||
min-width: 48px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
button.sml {
|
||||
padding: 8px;
|
||||
@@ -44,6 +45,11 @@ button.sml {
|
||||
min-width: 40px;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
button:hover, .btn:hover{
|
||||
background:#555;
|
||||
border-color:#555;
|
||||
}
|
||||
|
||||
#scan {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
@@ -839,8 +839,16 @@ void serializeInfo(JsonObject root)
|
||||
#endif
|
||||
|
||||
root[F("freeheap")] = getFreeHeapSize();
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
root[F("psram")] = ESP.getFreePsram();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// Report PSRAM information
|
||||
bool hasPsram = psramFound();
|
||||
root[F("psramPresent")] = hasPsram;
|
||||
if (hasPsram) {
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
root[F("psram")] = ESP.getFreePsram(); // Free PSRAM in bytes (backward compatibility)
|
||||
#endif
|
||||
root[F("psramSize")] = ESP.getPsramSize() / (1024UL * 1024UL); // Total PSRAM size in MB
|
||||
}
|
||||
#endif
|
||||
root[F("uptime")] = millis()/1000 + rolloverMillis*4294967;
|
||||
|
||||
|
||||
@@ -144,7 +144,17 @@ const ethernet_settings ethernetBoards[] = {
|
||||
18, // eth_mdio,
|
||||
ETH_PHY_LAN8720, // eth_type,
|
||||
ETH_CLOCK_GPIO0_OUT // eth_clk_mode
|
||||
}
|
||||
},
|
||||
|
||||
// Gledopto Series With Ethernet
|
||||
{
|
||||
1, // eth_address,
|
||||
5, // eth_power,
|
||||
23, // eth_mdc,
|
||||
33, // eth_mdio,
|
||||
ETH_PHY_LAN8720, // eth_type,
|
||||
ETH_CLOCK_GPIO0_IN // eth_clk_mode
|
||||
},
|
||||
};
|
||||
|
||||
bool initEthernet()
|
||||
|
||||
@@ -12,7 +12,20 @@
|
||||
#ifdef ESP32
|
||||
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
|
||||
#define UPDATE_ERROR errorString
|
||||
const size_t BOOTLOADER_OFFSET = 0x1000;
|
||||
|
||||
// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB
|
||||
// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#else
|
||||
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
|
||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||
#endif
|
||||
|
||||
#elif defined(ESP8266)
|
||||
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
||||
#define UPDATE_ERROR getErrorString
|
||||
@@ -280,9 +293,6 @@ static String bootloaderSHA256HexCache = "";
|
||||
void calculateBootloaderSHA256() {
|
||||
if (!bootloaderSHA256HexCache.isEmpty()) return;
|
||||
|
||||
// Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB
|
||||
const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size
|
||||
|
||||
// Calculate SHA256
|
||||
uint8_t sha256[32];
|
||||
mbedtls_sha256_context ctx;
|
||||
@@ -292,8 +302,8 @@ void calculateBootloaderSHA256() {
|
||||
const size_t chunkSize = 256;
|
||||
uint8_t buffer[chunkSize];
|
||||
|
||||
for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) {
|
||||
size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize);
|
||||
for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) {
|
||||
size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize);
|
||||
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {
|
||||
mbedtls_sha256_update(&ctx, buffer, readSize);
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
PinManager::deallocatePin(irPin, PinOwner::IR);
|
||||
}
|
||||
#endif
|
||||
for (unsigned s=0; s<WLED_MAX_BUTTONS; s++) {
|
||||
if (btnPin[s]>=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) {
|
||||
PinManager::deallocatePin(btnPin[s], PinOwner::Button);
|
||||
for (const auto &button : buttons) {
|
||||
if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) {
|
||||
PinManager::deallocatePin(button.pin, PinOwner::Button);
|
||||
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt
|
||||
if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin
|
||||
touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing
|
||||
if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin
|
||||
touchDetachInterrupt(button.pin); // if not assigned previously, this will do nothing
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -280,54 +280,56 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10)
|
||||
char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10)
|
||||
int hw_btn_pin = request->arg(bt).toInt();
|
||||
if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) {
|
||||
btnPin[i] = hw_btn_pin;
|
||||
buttonType[i] = request->arg(be).toInt();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector
|
||||
else {
|
||||
buttons[i].pin = hw_btn_pin;
|
||||
buttons[i].type = request->arg(be).toInt();
|
||||
}
|
||||
if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) {
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// ESP32 only: check that button pin is a valid gpio
|
||||
if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED))
|
||||
{
|
||||
if (digitalPinToAnalogChannel(btnPin[i]) < 0) {
|
||||
if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) {
|
||||
if (digitalPinToAnalogChannel(buttons[i].pin) < 0) {
|
||||
// not an ADC analog pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i);
|
||||
btnPin[i] = -1;
|
||||
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), buttons[i].pin, i);
|
||||
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
|
||||
buttons[i].type = BTN_TYPE_NONE;
|
||||
} else {
|
||||
analogReadResolution(12); // see #4040
|
||||
}
|
||||
}
|
||||
else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH))
|
||||
{
|
||||
if (digitalPinToTouchChannel(btnPin[i]) < 0)
|
||||
{
|
||||
} else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) {
|
||||
if (digitalPinToTouchChannel(buttons[i].pin) < 0) {
|
||||
// not a touch pin
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i);
|
||||
btnPin[i] = -1;
|
||||
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
|
||||
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), buttons[i].pin, i);
|
||||
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
|
||||
buttons[i].type = BTN_TYPE_NONE;
|
||||
}
|
||||
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so
|
||||
else
|
||||
{
|
||||
touchAttachInterrupt(btnPin[i], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
#endif
|
||||
else touchAttachInterrupt(buttons[i].pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// regular buttons and switches
|
||||
if (disablePullUp) {
|
||||
pinMode(btnPin[i], INPUT);
|
||||
pinMode(buttons[i].pin, INPUT);
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
pinMode(btnPin[i], buttonType[i]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(btnPin[i], INPUT_PULLUP);
|
||||
pinMode(buttons[i].pin, INPUT_PULLUP);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
btnPin[i] = -1;
|
||||
buttonType[i] = BTN_TYPE_NONE;
|
||||
buttons[i].pin = -1;
|
||||
buttons[i].type = BTN_TYPE_NONE;
|
||||
}
|
||||
}
|
||||
// we should remove all unused buttons from the vector
|
||||
for (int i = buttons.size()-1; i > 0; i--) {
|
||||
if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) {
|
||||
buttons.erase(buttons.begin() + i); // remove button from vector
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,14 +533,16 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
macroAlexaOff = request->arg(F("A1")).toInt();
|
||||
macroCountdown = request->arg(F("MC")).toInt();
|
||||
macroNl = request->arg(F("MN")).toInt();
|
||||
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
|
||||
char mp[4] = "MP"; mp[2] = (i<10?48:55)+i; mp[3] = 0; // short
|
||||
char ml[4] = "ML"; ml[2] = (i<10?48:55)+i; ml[3] = 0; // long
|
||||
char md[4] = "MD"; md[2] = (i<10?48:55)+i; md[3] = 0; // double
|
||||
int i = 0;
|
||||
for (auto &button : buttons) {
|
||||
char mp[4] = "MP"; mp[2] = (i<10?'0':'A'-10)+i; mp[3] = 0; // short
|
||||
char ml[4] = "ML"; ml[2] = (i<10?'0':'A'-10)+i; ml[3] = 0; // long
|
||||
char md[4] = "MD"; md[2] = (i<10?'0':'A'-10)+i; md[3] = 0; // double
|
||||
//if (!request->hasArg(mp)) break;
|
||||
macroButton[i] = request->arg(mp).toInt(); // these will default to 0 if not present
|
||||
macroLongPress[i] = request->arg(ml).toInt();
|
||||
macroDoublePress[i] = request->arg(md).toInt();
|
||||
button.macroButton = request->arg(mp).toInt(); // these will default to 0 if not present
|
||||
button.macroLongPress = request->arg(ml).toInt();
|
||||
button.macroDoublePress = request->arg(md).toInt();
|
||||
i++;
|
||||
}
|
||||
|
||||
char k[3]; k[2] = 0;
|
||||
|
||||
@@ -1159,60 +1159,62 @@ String computeSHA1(const String& input) {
|
||||
}
|
||||
|
||||
#ifdef ESP32
|
||||
static String dump_raw_block(esp_efuse_block_t block)
|
||||
{
|
||||
const int WORDS = 8; // ESP32: 8×32-bit words per block i.e. 256bits
|
||||
uint32_t buf[WORDS] = {0};
|
||||
|
||||
const esp_efuse_desc_t d = {
|
||||
.efuse_block = block,
|
||||
.bit_start = 0,
|
||||
.bit_count = WORDS * 32
|
||||
};
|
||||
const esp_efuse_desc_t* field[2] = { &d, NULL };
|
||||
|
||||
esp_err_t err = esp_efuse_read_field_blob(field, buf, WORDS * 32);
|
||||
if (err != ESP_OK) {
|
||||
return "";
|
||||
#include "esp_adc_cal.h"
|
||||
String generateDeviceFingerprint() {
|
||||
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
esp_efuse_mac_get_default((uint8_t*)fp);
|
||||
fp[1] ^= ESP.getFlashChipSize();
|
||||
fp[0] ^= chip_info.full_revision | (chip_info.model << 16);
|
||||
// mix in ADC calibration data:
|
||||
esp_adc_cal_characteristics_t ch;
|
||||
#if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC
|
||||
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_13;
|
||||
#else
|
||||
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_12;
|
||||
#endif
|
||||
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, myBIT_WIDTH, 1100, &ch);
|
||||
fp[0] ^= ch.coeff_a;
|
||||
fp[1] ^= ch.coeff_b;
|
||||
if (ch.low_curve) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
fp[0] ^= ch.low_curve[i];
|
||||
}
|
||||
}
|
||||
|
||||
String result = "";
|
||||
for (const unsigned int i : buf) {
|
||||
char line[32];
|
||||
sprintf(line, "0x%08X", i);
|
||||
result += line;
|
||||
if (ch.high_curve) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
fp[1] ^= ch.high_curve[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
char fp_string[17]; // 16 hex chars + null terminator
|
||||
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
|
||||
return String(fp_string);
|
||||
}
|
||||
#else // ESP8266
|
||||
String generateDeviceFingerprint() {
|
||||
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
|
||||
WiFi.macAddress((uint8_t*)&fp); // use MAC address as fingerprint base
|
||||
fp[0] ^= ESP.getFlashChipId();
|
||||
fp[1] ^= ESP.getFlashChipSize() | ESP.getFlashChipVendorId() << 16;
|
||||
char fp_string[17]; // 16 hex chars + null terminator
|
||||
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
|
||||
return String(fp_string);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Generate a device ID based on SHA1 hash of MAC address salted with "WLED"
|
||||
// Generate a device ID based on SHA1 hash of MAC address salted with other unique device info
|
||||
// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total)
|
||||
String getDeviceId() {
|
||||
static String cachedDeviceId = "";
|
||||
if (cachedDeviceId.length() > 0) return cachedDeviceId;
|
||||
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
char macStr[18];
|
||||
sprintf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
|
||||
// The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase
|
||||
// MAC is salted with other consistent device info to avoid rainbow table attacks.
|
||||
// If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices,
|
||||
// but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable.
|
||||
// If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1
|
||||
#ifdef ESP8266
|
||||
String deviceString = String(macStr) + "WLED" + ESP.getFlashChipId();
|
||||
#else
|
||||
String deviceString = String(macStr) + "WLED" + ESP.getChipModel() + ESP.getChipRevision();
|
||||
deviceString += dump_raw_block(EFUSE_BLK0);
|
||||
deviceString += dump_raw_block(EFUSE_BLK1);
|
||||
deviceString += dump_raw_block(EFUSE_BLK2);
|
||||
deviceString += dump_raw_block(EFUSE_BLK3);
|
||||
#endif
|
||||
String firstHash = computeSHA1(deviceString);
|
||||
|
||||
String firstHash = computeSHA1(generateDeviceFingerprint());
|
||||
|
||||
// Second hash: SHA1 of the first hash
|
||||
String secondHash = computeSHA1(firstHash);
|
||||
|
||||
@@ -670,7 +670,6 @@ void WLED::initConnection()
|
||||
if (!WLED_WIFI_CONFIGURED) {
|
||||
DEBUG_PRINTLN(F("No connection configured."));
|
||||
if (!apActive) initAP(); // instantly go to ap mode
|
||||
return;
|
||||
} else if (!apActive) {
|
||||
if (apBehavior == AP_BEHAVIOR_ALWAYS) {
|
||||
DEBUG_PRINTLN(F("Access point ALWAYS enabled."));
|
||||
|
||||
@@ -286,10 +286,10 @@ WLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS);
|
||||
|
||||
// Hardware and pin config
|
||||
#ifndef BTNPIN
|
||||
#define BTNPIN 0,-1
|
||||
#define BTNPIN 0
|
||||
#endif
|
||||
#ifndef BTNTYPE
|
||||
#define BTNTYPE BTN_TYPE_PUSH,BTN_TYPE_NONE
|
||||
#define BTNTYPE BTN_TYPE_PUSH
|
||||
#endif
|
||||
#ifndef RLYPIN
|
||||
WLED_GLOBAL int8_t rlyPin _INIT(-1);
|
||||
@@ -365,7 +365,7 @@ WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CO
|
||||
#define force802_3g wifiOpt.force802_3g
|
||||
#else
|
||||
WLED_GLOBAL int8_t selectedWiFi _INIT(0);
|
||||
WLED_GLOBAL byte apChannel _INIT(1); // 2.4GHz WiFi AP channel (1-13)
|
||||
WLED_GLOBAL byte apChannel _INIT(6); // 2.4GHz WiFi AP channel (1-13)
|
||||
WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID
|
||||
WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
@@ -571,9 +571,6 @@ WLED_GLOBAL byte countdownMin _INIT(0) , countdownSec _INIT(0);
|
||||
WLED_GLOBAL byte macroNl _INIT(0); // after nightlight delay over
|
||||
WLED_GLOBAL byte macroCountdown _INIT(0);
|
||||
WLED_GLOBAL byte macroAlexaOn _INIT(0), macroAlexaOff _INIT(0);
|
||||
WLED_GLOBAL byte macroButton[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL byte macroLongPress[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL byte macroDoublePress[WLED_MAX_BUTTONS] _INIT({0});
|
||||
|
||||
// Security CONFIG
|
||||
#ifdef WLED_OTA_PASS
|
||||
@@ -639,13 +636,32 @@ WLED_GLOBAL byte briLast _INIT(128); // brightness before
|
||||
WLED_GLOBAL byte whiteLast _INIT(128); // white channel before turned off. Used for toggle function in ir.cpp
|
||||
|
||||
// button
|
||||
WLED_GLOBAL int8_t btnPin[WLED_MAX_BUTTONS] _INIT({BTNPIN});
|
||||
WLED_GLOBAL byte buttonType[WLED_MAX_BUTTONS] _INIT({BTNTYPE});
|
||||
struct Button {
|
||||
unsigned long pressedTime; // time button was pressed
|
||||
unsigned long waitTime; // time to wait for next button press
|
||||
int8_t pin; // pin number
|
||||
struct {
|
||||
uint8_t type : 6; // button type (push, long, double, etc.)
|
||||
bool pressedBefore : 1; // button was pressed before
|
||||
bool longPressed : 1; // button was long pressed
|
||||
};
|
||||
uint8_t macroButton; // macro/preset to call on button press
|
||||
uint8_t macroLongPress; // macro/preset to call on long press
|
||||
uint8_t macroDoublePress; // macro/preset to call on double press
|
||||
|
||||
Button(int8_t p, uint8_t t, uint8_t mB = 0, uint8_t mLP = 0, uint8_t mDP = 0)
|
||||
: pressedTime(0)
|
||||
, waitTime(0)
|
||||
, pin(p)
|
||||
, type(t)
|
||||
, pressedBefore(false)
|
||||
, longPressed(false)
|
||||
, macroButton(mB)
|
||||
, macroLongPress(mLP)
|
||||
, macroDoublePress(mDP) {}
|
||||
};
|
||||
WLED_GLOBAL std::vector<Button> buttons; // vector of button structs
|
||||
WLED_GLOBAL bool buttonPublishMqtt _INIT(false);
|
||||
WLED_GLOBAL bool buttonPressedBefore[WLED_MAX_BUTTONS] _INIT({false});
|
||||
WLED_GLOBAL bool buttonLongPressed[WLED_MAX_BUTTONS] _INIT({false});
|
||||
WLED_GLOBAL unsigned long buttonPressedTime[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL unsigned long buttonWaitTime[WLED_MAX_BUTTONS] _INIT({0});
|
||||
WLED_GLOBAL bool disablePullUp _INIT(false);
|
||||
WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESHOLD);
|
||||
|
||||
@@ -806,7 +822,7 @@ WLED_GLOBAL byte timerHours[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
|
||||
WLED_GLOBAL int8_t timerMinutes[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
|
||||
WLED_GLOBAL byte timerMacro[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));
|
||||
//weekdays to activate on, bit pattern of arr elem: 0b11111111: sun,sat,fri,thu,wed,tue,mon,validity
|
||||
WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }));
|
||||
WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 254, 254, 254, 254, 254, 254, 254, 254, 254, 254 }));
|
||||
//upper 4 bits start, lower 4 bits end month (default 28: start month 1 and end month 12)
|
||||
WLED_GLOBAL byte timerMonth[] _INIT_N(({28,28,28,28,28,28,28,28}));
|
||||
WLED_GLOBAL byte timerDay[] _INIT_N(({1,1,1,1,1,1,1,1}));
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
#ifdef WLED_ENABLE_PIXART
|
||||
#include "html_pixart.h"
|
||||
#endif
|
||||
#ifndef WLED_DISABLE_PXMAGIC
|
||||
#ifdef WLED_ENABLE_PXMAGIC
|
||||
#include "html_pxmagic.h"
|
||||
#endif
|
||||
#ifndef WLED_DISABLE_PIXELFORGE
|
||||
#include "html_pixelforge.h"
|
||||
#endif
|
||||
#include "html_cpal.h"
|
||||
#include "html_edit.h"
|
||||
|
||||
@@ -27,6 +30,7 @@ static const char s_accessdenied[] PROGMEM = "Access Denied";
|
||||
static const char s_not_found[] PROGMEM = "Not found";
|
||||
static const char s_wsec[] PROGMEM = "wsec.json";
|
||||
static const char s_func[] PROGMEM = "func";
|
||||
static const char s_list[] PROGMEM = "list";
|
||||
static const char s_path[] PROGMEM = "path";
|
||||
static const char s_cache_control[] PROGMEM = "Cache-Control";
|
||||
static const char s_no_store[] PROGMEM = "no-store";
|
||||
@@ -66,7 +70,7 @@ static bool inLocalSubnet(const IPAddress &client) {
|
||||
*/
|
||||
|
||||
static void generateEtag(char *etag, uint16_t eTagSuffix) {
|
||||
sprintf_P(etag, PSTR("%7d-%02x-%04x"), VERSION, cacheInvalidate, eTagSuffix);
|
||||
sprintf_P(etag, PSTR("%u-%02x-%04x"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix);
|
||||
}
|
||||
|
||||
static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, uint16_t eTagSuffix = 0) {
|
||||
@@ -226,14 +230,18 @@ void createEditHandler() {
|
||||
return;
|
||||
}
|
||||
const String& func = request->arg(FPSTR(s_func));
|
||||
bool legacyList = false;
|
||||
if (request->hasArg(FPSTR(s_list))) {
|
||||
legacyList = true; // support for '?list=/'
|
||||
}
|
||||
|
||||
if(func.length() == 0) {
|
||||
if(func.length() == 0 && !legacyList) {
|
||||
// default: serve the editor page
|
||||
handleStaticContent(request, FPSTR(_edit_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_edit, PAGE_edit_length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (func == "list") {
|
||||
if (func == FPSTR(s_list) || legacyList) {
|
||||
bool first = true;
|
||||
AsyncResponseStream* response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JSON));
|
||||
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
|
||||
@@ -243,15 +251,15 @@ void createEditHandler() {
|
||||
File rootdir = WLED_FS.open("/", "r");
|
||||
File rootfile = rootdir.openNextFile();
|
||||
while (rootfile) {
|
||||
String name = rootfile.name();
|
||||
if (name.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||
rootfile = rootdir.openNextFile(); // skip wsec.json
|
||||
continue;
|
||||
}
|
||||
if (!first) response->write(',');
|
||||
first = false;
|
||||
response->printf_P(PSTR("{\"name\":\"%s\",\"type\":\"file\",\"size\":%u}"), name.c_str(), rootfile.size());
|
||||
rootfile = rootdir.openNextFile();
|
||||
String name = rootfile.name();
|
||||
if (name.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||
rootfile = rootdir.openNextFile(); // skip wsec.json
|
||||
continue;
|
||||
}
|
||||
if (!first) response->write(',');
|
||||
first = false;
|
||||
response->printf_P(PSTR("{\"name\":\"%s\",\"type\":\"file\",\"size\":%u}"), name.c_str(), rootfile.size());
|
||||
rootfile = rootdir.openNextFile();
|
||||
}
|
||||
rootfile.close();
|
||||
rootdir.close();
|
||||
@@ -600,12 +608,19 @@ void initServer()
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_PXMAGIC
|
||||
#ifdef WLED_ENABLE_PXMAGIC
|
||||
static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm";
|
||||
server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_length);
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifndef WLED_DISABLE_PIXELFORGE
|
||||
static const char _pixelforge_htm[] PROGMEM = "/pixelforge.htm";
|
||||
server.on(_pixelforge_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
handleStaticContent(request, FPSTR(_pixelforge_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixelforge, PAGE_pixelforge_length);
|
||||
});
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static const char _cpal_htm[] PROGMEM = "/cpal.htm";
|
||||
|
||||
@@ -157,12 +157,6 @@ void sendDataWs(AsyncWebSocketClient * client)
|
||||
// the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS
|
||||
size_t heap1 = getFreeHeapSize();
|
||||
DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize());
|
||||
#ifdef ESP8266
|
||||
if (len>heap1) {
|
||||
DEBUG_PRINTLN(F("Out of memory (WS)!"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
AsyncWebSocketBuffer buffer(len);
|
||||
#ifdef ESP8266
|
||||
size_t heap2 = getFreeHeapSize();
|
||||
@@ -236,7 +230,7 @@ bool sendLiveLedsWs(uint32_t wsClient)
|
||||
#ifndef WLED_DISABLE_2D
|
||||
if (strip.isMatrix && n>1 && (i/Segment::maxWidth)%n) i += Segment::maxWidth * (n-1);
|
||||
#endif
|
||||
uint32_t c = strip.getPixelColor(i);
|
||||
uint32_t c = strip.getPixelColor(i); // note: LEDs mapped outside of valid range are set to black
|
||||
uint8_t r = R(c);
|
||||
uint8_t g = G(c);
|
||||
uint8_t b = B(c);
|
||||
|
||||
@@ -291,7 +291,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str());
|
||||
|
||||
// set limits
|
||||
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"),
|
||||
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d);"),
|
||||
WLED_MAX_BUSSES,
|
||||
WLED_MIN_VIRTUAL_BUSSES, // irrelevant, but kept to distinguish S2/S3 in UI
|
||||
MAX_LEDS_PER_BUS,
|
||||
@@ -299,7 +299,8 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
MAX_LEDS,
|
||||
WLED_MAX_COLOR_ORDER_MAPPINGS,
|
||||
WLED_MAX_DIGITAL_CHANNELS,
|
||||
WLED_MAX_ANALOG_CHANNELS
|
||||
WLED_MAX_ANALOG_CHANNELS,
|
||||
WLED_MAX_BUTTONS
|
||||
);
|
||||
|
||||
printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments);
|
||||
@@ -403,8 +404,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
printSetFormValue(settingsScript,PSTR("RL"),rlyPin);
|
||||
printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde);
|
||||
printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain);
|
||||
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
|
||||
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]);
|
||||
int i = 0;
|
||||
for (const auto &button : buttons) {
|
||||
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i++, button.pin, button.type);
|
||||
}
|
||||
printSetFormCheckbox(settingsScript,PSTR("IP"),disablePullUp);
|
||||
printSetFormValue(settingsScript,PSTR("TT"),touchThreshold);
|
||||
@@ -578,8 +580,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
||||
printSetFormValue(settingsScript,PSTR("A1"),macroAlexaOff);
|
||||
printSetFormValue(settingsScript,PSTR("MC"),macroCountdown);
|
||||
printSetFormValue(settingsScript,PSTR("MN"),macroNl);
|
||||
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
|
||||
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i, macroButton[i], macroLongPress[i], macroDoublePress[i]);
|
||||
int i = 0;
|
||||
for (const auto &button : buttons) {
|
||||
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i++, button.macroButton, button.macroLongPress, button.macroDoublePress);
|
||||
}
|
||||
|
||||
char k[4];
|
||||
|
||||
Reference in New Issue
Block a user