diff --git a/.gitignore b/.gitignore
index c85fae0c22..b687696b95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,6 @@
.vscode
esp01-update.sh
-platformio_override.ini
replace_fs.py
wled-update.sh
diff --git a/platformio.ini b/platformio.ini
index cbe13c0dd8..5da7162652 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -433,6 +433,16 @@ board_build.partitions = ${esp32.default_partitions}
; board_build.f_flash = 80000000L
; board_build.flash_mode = dio
+[env:esp32_quinled_diguno]
+extends = env:esp32dev
+build_flags = ${env:esp32dev.build_flags} -D RLYPIN=12 -D BTNPIN=0 -D DATA_PINS=16 -D DMTYPE=1 -D I2S_SDPIN=19 -D I2S_WSPIN=4 -D I2S_CKPIN=18
+ -D USERMOD_AUDIOREACTIVE
+lib_deps = ${env:esp32dev.lib_deps}
+ https://github.com/blazoncek/arduinoFFT.git
+upload_speed = 690000
+board_build.f_flash = 80000000L
+board_build.flash_mode = qio
+
[env:esp32dev_qio80]
board = esp32dev
platform = ${esp32.platform}
diff --git a/platformio_override.ini b/platformio_override.ini
new file mode 100644
index 0000000000..0ff991f78f
--- /dev/null
+++ b/platformio_override.ini
@@ -0,0 +1,17 @@
+[platformio]
+default_envs = esp32dev
+extra_configs =
+ platformio_tubes.ini
+
+[env:golden]
+extends = env:esp32_quinled_dig2go_tubes
+build_flags = ${env:tubes.build_flags} -D GOLDEN
+
+[env:christmas]
+extends = env:esp32_quinled_dig2go_tubes
+build_flags = ${env:tubes.build_flags} -D CHRISTMAS
+
+[env:ruby]
+extends = env:esp32_quinled_dig2go_tubes
+build_flags = ${env:tubes.build_flags} -D RUBY
+
diff --git a/platformio_tubes.ini b/platformio_tubes.ini
new file mode 100644
index 0000000000..39b499cff4
--- /dev/null
+++ b/platformio_tubes.ini
@@ -0,0 +1,202 @@
+[tubes_no_mic]
+; these settings create a basic template for tubes build from
+; from which all other tube envs should build
+; the base build does not support
+; -- mic (audio reactive)
+; -- ir remotes
+; For devices with those inputs use the [tubes] settings
+; which adds those back in
+platform = espressif32@5.3.0
+platform_packages =
+;platform_packages = ;framework-arduinoespressif32 @ 3.20017.0
+build_unflags =
+ -D LOROL_LITTLEFS
+ -D CONFIG_ASYNC_TCP_USE_WDT
+build_flags =
+ -g
+ -O2
+ -D FASTLED_ALL_PINS_HARDWARE_SPI
+ -D ARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
+ -D WLED_WATCHDOG_TIMEOUT=0
+ -D USERMOD_TUBES
+ -D CONFIG_ASYNC_TCP_RUNNING_CORE=-1
+ ; Disable a bunch of unnecessary integrations
+ -D WLED_DISABLE_BLYNK
+ -D WLED_DISABLE_MQTT
+ -D WLED_DISABLE_LOXONE
+ -D WLED_DISABLE_ALEXA
+ -D WLED_DISABLE_INFRARED
+ -D WLED_DISABLE_CRONIXIE
+ -D WLED_DISABLE_HUESYNC
+ -D WLED_DISABLE_WEBSOCKETS
+ -D WLED_DISABLE_ADALIGHT
+ -D WLED_DISABLE_ESPNOW
+ -D WLED_DISABLE_SERIAL
+ -D IRTYPE=0
+lib_ignore =
+ ESPAsyncTCP
+ ESPAsyncUDP
+ IRremoteESP8266
+lib_deps =
+ fastled/FastLED @ ^3.7.3
+ makuna/NeoPixelBus @ ^2.8.0
+ https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1
+
+[tubes]
+extends = tubes_no_mic
+build_flags =
+ ${tubes_no_mic.build_flags}
+ ${esp32.AR_build_flags}
+lib_deps = ${tubes_no_mic.lib_deps}
+ IRremoteESP8266 @ ^2.8.6
+ https://github.com/kosme/arduinoFFT#v2.0.2
+
+
+[env:esp32_quinled_dig2go]
+; basis quinled dig2go without any tubes support
+extends = env:esp32_quinled_diguno
+build_unflags = ${env:esp32_quinled_diguno.build_unflags}
+ -D WLED_DISABLE_INFRARED
+ -D IRTYPE
+lib_ignore = ${env:esp32_quinled_diguno.lib_ignore}
+lib_deps = ${env:esp32_quinled_diguno.lib_deps}
+ IRremoteESP8266 @ 2.8.6
+
+
+[env:esp32_quinled_dig2go_tubes]
+extends = env:esp32_quinled_dig2go
+platform = ${tubes.platform}
+platform_packages = ${tubes.platform_packages}
+build_unflags =
+ ${tubes.build_unflags}
+ ${env:esp32_quinled_dig2go}
+ -D WLED_RELEASE_NAME
+build_flags = -D WLED_RELEASE_NAME=DIG2GO_TUBES
+ ${tubes.build_flags}
+ ${env:esp32_quinled_dig2go.build_flags}
+ -D FASTLED_ESP32_SPI_BUS=HSPI
+ -D NUM_STRIPS=1 -D DEFAULT_LED_COUNT=150
+ -D CHRISTMAS # REMOVE
+lib_ignore =
+ ESPAsyncTCP
+ ESPAsyncUDP
+ ${env:esp32_quinled_dig2go.lib_ignore}
+lib_deps =
+ ${tubes.lib_deps}
+
+
+
+# ------------------------------------------------------------------------------
+# ESP32 S3 Matrix M1
+# ------------------------------------------------------------------------------
+[env:esp32-s3-matrix-m1]
+; builds using the default WLED settings
+extends = env:esp32s3dev_8MB_PSRAM_opi
+board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB
+board_upload.flash_size = 4MB
+board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv
+build_unflags = ${env:esp32s3dev_8MB_PSRAM_opi.build_unflags}
+ -D ARDUINO_USB_CDC_ON_BOOT=1
+ -D ARDUINO_USB_MSC_ON_BOOT=0
+ -D ARDUINO_DFU_ON_BOOT=0
+build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_MATRIX_M1
+ -D WLED_WATCHDOG_TIMEOUT=0
+ -D ARDUINO_USB_CDC_ON_BOOT=1
+ -D ARDUINO_USB_MODE=1
+ -D BOARD_HAS_PSRAM ; tells WLED that PSRAM shall be used
+ -D WLED_USE_PSRAM
+ -D ABL_MILLIAMPS_DEFAULT=250
+ -D NUM_STRIPS=1 -D PIXEL_COUNTS=64 -D DEFAULT_LED_COUNT=64
+ -D DEFAULT_LED_COLOR_ORDER=1 -D LEDPIN=14
+lib_deps = ${esp32s3.lib_deps}
+lib_ignore = ${env:esp32s3dev_8MB_PSRAM_opi.lib_ignore}
+ IRremoteESP8266
+
+[env:esp32-s3-matrix-m1_tubes]
+extends = env:esp32-s3-matrix-m1
+platform = espressif32@6.8.1
+platform_packages =
+;platform = ${tubes.platform}
+;platform_packages = ${tubes.platform_packages}
+board_build.partitions = tools/WLED_ESP32_4MB_noOTA.csv
+build_unflags = ${env:esp32-s3-matrix-m1.build_unflags}
+ ${tubes_no_mic.build_unflags}
+build_flags =
+ ${tubes_no_mic.build_flags}
+ ${env:esp32-s3-matrix-m1.build_flags}
+ -D IRTYPE=0
+ -D FASTLED_ALL_PINS_HARDWARE_SPI
+lib_ignore =
+ ${tubes_no_mic.lib_ignore}
+ ${env:esp32-s3-matrix-m1.lib_ignore}
+lib_deps =
+ ${tubes_no_mic.lib_deps}
+
+# ------------------------------------------------------------------------------
+# ESP32 C3 Athom
+# ------------------------------------------------------------------------------
+[env:esp32-c3-athom]
+extends = env:esp32c3dev
+lib_ignore = IRremoteESP8266
+ ${env:esp32c3dev.lib_ignore}
+build_flags = ${env:esp32c3dev.build_flags}
+ -D LEDPIN=10
+ -D BTNPIN=9
+ -D WLED_DISABLE_INFRARED
+ -D IRTYPE=0
+
+[env:esp32-c3-athom_tubes]
+extends = env:esp32-c3-athom
+platform = espressif32@6.8.1
+upload_speed = 115200
+platform_packages =
+;platform = ${tubes_no_mic.platform}
+;platform_packages = ${tubes_no_mic.platform_packages}
+build_unflags = ${env:esp32-c3-athom.build_unflags}
+ ${tubes_no_mic.build_unflags}
+build_flags = ${env:esp32-c3-athom.build_flags} -D WLED_RELEASE_NAME=ESP32-C3_ATHOM_TUBES
+ ${tubes_no_mic.build_flags}
+ -D FASTLED_ESP32_SPI_BUS=HSPI
+ -D NUM_STRIPS=1 -D DEFAULT_LED_COUNT=150
+ -D WLED_WIFI_POWER_SETTING=WIFI_POWER_8_5dBm
+ -D LOLIN_WIFI_FIX
+lib_ignore = ${env:esp32-c3-athom.lib_ignore}
+ ${tubes_no_mic.lib_ignore}
+lib_deps =
+ ${tubes_no_mic.lib_deps}
+
+
+# ------------------------------------------------------------------------------
+# ESP32 GLEDOPTO
+#
+# LEDs: GRB @ pin 16
+# Button: GPIO 0
+# SR: i2S SD 26, WS 5, SCK 21, MCLK unused
+# ------------------------------------------------------------------------------
+[env:esp32-gledopto]
+extends = env:esp32dev
+lib_ignore = IRremoteESP8266
+ ${env:esp32c3dev.lib_ignore} ; ok to keep this line if it exists, harmless
+build_flags =
+ -D LEDPIN=16
+ -D BTNPIN=0
+ -D WLED_DISABLE_INFRARED
+ -D IRTYPE=0
+
+[env:esp32-gledopto_tubes]
+extends = env:esp32-gledopto
+platform = platformio/espressif32 @ 6.7.0
+platform_packages =
+ framework-arduinoespressif32@3.20016.0 ; Arduino-ESP32 2.0.16
+build_unflags = ${env:esp32-gledopto.build_unflags}
+ ${tubes_no_mic.build_unflags}
+build_flags = ${env:esp32-gledopto.build_flags}
+ ${tubes_no_mic.build_flags}
+ -D FASTLED_ESP32_SPI_BUS=HSPI
+ -D NUM_STRIPS=1 -D DEFAULT_LED_COUNT=150
+ -D WLED_WIFI_POWER_SETTING=WIFI_POWER_8_5dBm
+ -D LOLIN_WIFI_FIX
+lib_ignore = ${env:esp32-gledopto.lib_ignore}
+ ${tubes_no_mic.lib_ignore}
+lib_deps = ${env:esp32-gledopto.lib_deps}
+ ${tubes_no_mic.lib_deps}
diff --git a/readme.md b/readme.md
index 11c1733f87..fef03b9c6a 100644
--- a/readme.md
+++ b/readme.md
@@ -1,19 +1,89 @@
-
-
-
-
-
-
-
-
-
+# WLED-based LED Sticks
+These portable LED light poles make pretty lights for a dance party - and better yet:
-
+They're **portable**! Convenient tripod bases keep them standing at attention, and a 10Ah USB battery (like the one you charge your phone with) will power them for about 8 hours.
-# Welcome to my project WLED! ✨
+They're **versatile**! You can deploy them in a [wide open space at Burning Man](https://youtu.be/UtXL0ScNUoE), in a [private event space](https://youtu.be/B6OBde7zRN4), or right next to each other [in your living room](https://youtu.be/UmiRBCOAMBg). (Click into these links for videos of them in action.)
+
+They're **coordinated**! If you put them next to each other, they sync using near-field radio. And they'll even daisy-chain, meaning the furthest ones can be really far apart as long as there are some in the middle to relay the signal.
+
+
+
+They're **sturdy**! I built over 100 of them and have deployed them at parties in all kinds of weather, from hot & dusty windstorms to torrential rain. They've been tossed in a truck, carried around as totems, knocked or blown over, and almost all of 'em are still working. Just don't let people use them like light sabers (lesson learned.)
+
+They're also **in progress** with lots of things that could be improved. But for now:
+
+* they play patterns from an expanding set of "genetically-driven" combinations
+* they stay in sync or deliberately drift from each other in pleasing ways
+* they operate internally at a certain BPM - allowing them to sync to music
+
+## How do they work?
+
+Each light tube is running custom software on an ESP32 microcontroller. The software is running a generative light program, based on the popular software WLED, that has a simple "DNA" specifying its pattern, color, effects overlay, offset, and so on.
+
+The patterns run on a clock that's synced to a specific BPM and counts out the 4/4 rhythm of most dance music, so they morph and change on individual beats, measures, and phrases. After several beat phrases pass, a pole's DNA is mutated a little, which can cause it to change color, or adopt a new pattern, etc.
+
+On-board radio lets a light pole broadcast its DNA and clock timer; when others hear the signal, they can choose to follow along by updating their own pattern DNA to match it. The method of coordination is pretty simple right now: each pole has an 8-bit ID, and lower-ID poles obey higher-ID poles.
+
+If they're all close enough, they'll soon start doing the same thing as a group and keeping their clock timers in sync. In case they're not close enough, poles also re-broadcast all the signals they receive and obey. This lets them pass along a signal through the group until all of them have found it.
+
+Some randomness and chaos is intentional. Radio isn't 100% reliable, so they sometimes fall out of contact and then re-connect. And in some cases, the poles will deliberately offset their own clock a bit so that they are clearly doing the same thing but not exactly at the same time. Each tube is actually running several copies of the software and smoothly "cross-fading" between them, to avoid any jarring transitions.
+
+## Credits
+
+The form factor was inspired by [Mark Lottor's Hexatron](http://www.3waylabs.com/projects/hex/). The color schemes were inspired by [the work of Christopher Schardt](https://americanart.si.edu/exhibitions/burning-man/online/christopher-schardt) and many were found on [cpt-city](http://soliton.vm.bytemark.co.uk/pub/cpt-city/). My most recent versions use the excellent [WLED](https://github.com/Aircoookie/WLED) codebase and run as a usermod. My older code was written using [FastLED](http://fastled.io/) and I still use some original patterns that are evolved versions of FastLED examples. The mesh networking is based on [Chuck Sommerville's led-swarm](https://github.com/chucks13/LED-swarm), and still follows its [theory](https://github.com/chucks13/LED-swarm/blob/master/theory.txt) for radio connection. When I kicked off this project, I used [Paul Stoffregen's WS2812Serial](https://github.com/PaulStoffregen/WS2812Serial) for smooth animation, and of course [his Teensy CPU](https://www.pjrc.com/teensy/teensyLC.html) made the whole thing possible. Standing on the shoulders of these giants let me create these in about a month, in time to debut at Burning Man 2019 and appear at many parties since!
+
+## WLEDtubes branch changes
+
+This is a WLED-based update to my [2019 light tube project](https://github.com/SteveEisner/tubes), which ran on Teensy + FastLED + nRF24L01 radios.
+
+Most of the changes are in the usermod `usermods/Tubes`:
+* Tubes is installed as an overlay function, which allows it to run WLED "underneath" but completely control the output.
+* Its final output is a composition of multiple layers: it starts with WLED's output, then optionally overwrites with its own patterns, then runs a particle effects library on top of that.
+* It stores a curated playlist of composite effects. Some are WLED stock FX, others are custom-built. It moves randomly through that playlist.
+* At the time of writing, WLED could not correctly fade transitions between 2 FX. Tubes makes up for that by being able to fade between 1 WLED FX and 1 custom overlay pattern, or between 2 custom overlays. As it moves through the playlist, it ensures that it never plays two WLED FX in a row.
+* The particle library runs on top of everything (including WLED FX) and introduces a variety of patterns & blit effects.
+* If the user changes effects or palettes in the WLED web UI, Tubes will honor that & stop overlaying for a little while. It will eventually time out and revert to full overlay mode.
+* Everything runs on a custom clock that is synced to a specific BPM. The BPM can be changed (manually now, automatic eventually) so that the effects perfectly sync to nearby music. WLED FX don't sync yet but could be speed-adjusted.
+* An ESP-Now based mesh network is created, so all devices running this usermod stay in sync with each other, without Wi-Fi.
+
+Mesh networking is based on a unidirectional broadcast protocol:
+* Every device (node) is assigned a random 12-bit ID. This ID can change at any time, although in practice it only changes upon reboot.
+* A node begins with the assumption that it is the only node, and therefore it is the leader, which means it's the one controlling patterns, palettes, and effects.
+* As a node operates, it regularly broadcasts its status via ESP-Now, in case other device nodes are nearby. Nodes also continuously listen for ESP-Now broadcasts with node status.
+* Status messages are identified by the node's ID. If a node receives a broadcast from a lower ID, it ignores it.
+* When a node receives a broadcast from a higher ID, it assumes that other node must be the leader. It syncs its status to the leader's status & stops broadcasting its own status
+* When a node receives a broadcast from the same ID, it assumes there's been an ID assignment collision and randomizes its own ID. (This happens sometimes even in a 12-bit space.)
+* Nodes are assumed to be unstable; they can move or be turned off (or crash.) Status packets include both a current status and 30+ seconds of future states. All nodes can continue to run in sync even if they don't hear from the leader during this time.
+* If a node hasn't heard from the leader in a long time (20 sec or more), it assumes the leader has permanently left. It reverts to being its own leader again until it hears from a new leader with higher ID.
+* To help boost the leader's effective range, a following node will occasionally relay the leader's commands using the leader's ID. This helps sync devices that are out of range of the leader, but within range of a follower. The effective range of a single ESP32 device has been measured at hundreds feet; relays allow for an even larger mesh range.
+* There's a protocol for explicit control, with commands that can be sent to specific nodes or all nodes. This allows a single master remote to directly control the entire mesh.
+* This has been tested on 75+ devices in proximity, but theoretically can expand until it saturates ESPNow bandwidth (hundreds of devices? thousands? not sure)
+
+The Tubes usermod uses several sub-libraries and helper functions:
+* beats.h: an 8-bit bpm library that helps the Tubes run patterns at a specific bpm
+* node.h: the ESP-Now based mesh network
+* particle.h: a particle effects overlay library
+* firmware.sh: successful firmware+config mass-autoupdater
+* master.h: a remote that overrides & controls all ESP-Now nodes (run from a separate device)
+* timer.h: a tiny library to help with timed events
+
+There are several left-over modules that aren't used any more.
+* bluetooth.h: a failed initial attempt to sync over BLE (now unused)
+* updater.h + update_server.h: a failed attempt to write a peer-to-peer firmware auto-updater (unreliable)
+* sound.h: an initial attempt to create some sound-reactive effect overlays
+
+Also, there a few changes to core library files:
+* New brighter, more vivid color palettes (palettes.h + wled00/FX_fcn.cpp)
+* New button-press code to allow a single button to handle Wi-Fi protection (it's only turned on by explicit button press) and "Power-save" for battery operation (wled00/button.cpp)
+* Fleet provisioning for flashing dozens of WLED controllers (wled00/wled_serial.cpp disabled to allow it)
+
+# Original README: Welcome to WLED! ✨
A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102!
+Now with new magical sync powers!
+
## ⚙️ Features
- WS2812FX library with more than 100 special effects
- FastLED noise effects and 50 palettes
diff --git a/tools/WLED_ESP32_4MB_noOTA.csv b/tools/WLED_ESP32_4MB_noOTA.csv
new file mode 100644
index 0000000000..518b0b1e04
--- /dev/null
+++ b/tools/WLED_ESP32_4MB_noOTA.csv
@@ -0,0 +1,6 @@
+# Name, Type, SubType, Offset, Size, Flags
+nvs, data, nvs, 0x9000, 0x5000,
+otadata, data, ota, 0xe000, 0x2000,
+app0, app, ota_0, 0x10000, 0x2F0000,
+spiffs, data, spiffs, 0x300000,0xF0000,
+coredump, data, coredump,,64K
\ No newline at end of file
diff --git a/usermods/Tubes/Tubes.h b/usermods/Tubes/Tubes.h
new file mode 100644
index 0000000000..ebd34b6c4e
--- /dev/null
+++ b/usermods/Tubes/Tubes.h
@@ -0,0 +1,137 @@
+#pragma once
+
+#include "wled.h"
+
+#include "util.h"
+#include "options.h"
+
+// #define USERADIO
+
+#include "FX.h"
+
+#include "virtual_strip.h"
+#include "led_strip.h"
+#include "master.h"
+
+#include "controller.h"
+#include "debug.h"
+
+
+#define MASTER_PIN 25
+#define LEGACY_PIN 32 // DigUno Q4
+
+#ifndef NUM_STRIPS
+#define NUM_STRIPS 1
+#endif
+
+#ifndef MAX_REAL_LEDS
+#define MAX_REAL_LEDS (DEFAULT_LED_COUNT * NUM_STRIPS)
+#endif
+
+class TubesUsermod : public Usermod {
+ private:
+ PatternController controller = PatternController(MAX_REAL_LEDS);
+ DebugController debug = DebugController(controller);
+ Master master = Master(controller);
+ bool isLegacy = false;
+
+ void randomize() {
+ randomSeed(esp_random());
+ random16_set_seed(random(0, 65535));
+ random16_add_entropy(esp_random());
+ }
+
+ public:
+ void setup() {
+
+ if (pinManager.isPinOk(MASTER_PIN)) {
+ pinMode(MASTER_PIN, INPUT_PULLUP);
+ if(pinManager.isPinOk(LEGACY_PIN)) {
+ pinMode(LEGACY_PIN, INPUT_PULLUP);
+ }
+ if (digitalRead(MASTER_PIN) == LOW) {
+ }
+ isLegacy = (digitalRead(MASTER_PIN) == LOW);
+ }
+ randomize();
+
+ // Override some behaviors on all Tubes
+ bootPreset = 0; // Try to prevent initial playlists from starting
+ fadeTransition = true; // Fade palette transitions
+ transitionDelay = 8000; // Fade them for a long time
+ strip.setTargetFps(60);
+ strip.setCCT(100);
+
+ // Start timing
+ globalTimer.setup();
+ controller.setup();
+ if (controller.isMasterRole()) {
+ master.setup();
+ }
+ debug.setup();
+ }
+
+ void loop()
+ {
+ EVERY_N_MILLISECONDS(10000) {
+ randomize();
+ }
+
+ globalTimer.update();
+
+ if (controller.isMasterRole()) {
+ master.update();
+ }
+ controller.update();
+ debug.update();
+
+ // Draw after everything else is done
+ controller.led_strip.update();
+ }
+
+ void handleOverlayDraw() {
+ // Draw effects layers over whatever WLED is doing.
+ controller.handleOverlayDraw();
+ debug.handleOverlayDraw();
+ if (controller.isMasterRole()) {
+ master.handleOverlayDraw();
+ }
+
+ // When AP mode is on, make sure it's obvious
+ // Blink when there's a connected client
+ if (apActive) {
+ strip.setPixelColor(0, CRGB::Purple);
+ if (millis() % 4000 > 1000 && WiFi.softAPgetStationNum()) {
+ strip.setPixelColor(0, CRGB::Black);
+ }
+ strip.setPixelColor(1, CRGB::Black);
+ }
+ }
+
+ bool handleButton(uint8_t b) {
+ // Special code for handling the "power save" button
+ if (b == 100) { // Press button 0 for WLED_LONG_POWER_SAVE ms
+ controller.togglePowerSave();
+ return true;
+ }
+ if (b == 101) { // Short press button 0 (piggybacks with default)
+ controller.cancelOverrides();
+ return true;
+ }
+ if (b == 102) { // Double-click button 0
+ controller.acknowledge();
+ if (controller.isSelecting()) {
+ if (controller.isSelected())
+ controller.deselect();
+ else
+ controller.select();
+ } else {
+ controller.request_new_bpm();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+};
diff --git a/usermods/Tubes/beats.h b/usermods/Tubes/beats.h
new file mode 100644
index 0000000000..617872e910
--- /dev/null
+++ b/usermods/Tubes/beats.h
@@ -0,0 +1,73 @@
+#pragma once
+#include "wled.h"
+#include "timer.h"
+
+#define DEFAULT_BPM 120
+
+typedef uint32_t BeatFrame_24_8; // 24:8 bitwise float
+
+// Regulates the beat counter, running patterns at 256 "fracs" per beat
+class BeatController {
+ public:
+ accum88 bpm = 0;
+ BeatFrame_24_8 frac;
+ uint32_t accum = 0;
+ uint32_t micros_per_frac;
+
+ void setup()
+ {
+ // Starts in phrase 1
+ sync(DEFAULT_BPM << 8, 0);
+ }
+
+ void update()
+ {
+ // Maintains an accumulator with 14 bits of precision
+ accum += globalTimer.delta_micros << 8;
+ while (accum > micros_per_frac) {
+ frac++;
+ accum -= micros_per_frac;
+ }
+ }
+
+ void sync(accum88 b, BeatFrame_24_8 f) {
+ if (b < 40<<8) {
+ // Reject BPMs that are too low.
+ return;
+ }
+
+ accum88 last_bpm = bpm;
+ bpm = b;
+ frac = f;
+ accum = 0;
+
+ micros_per_frac = (uint32_t)(15360000000.0 / (float)bpm);
+
+ if (last_bpm != bpm)
+ print_bpm();
+ }
+
+ void set_bpm(accum88 b) {
+ sync(b, frac);
+ }
+
+ void adjust_bpm(saccum78 b) {
+ sync(bpm + b, frac);
+ }
+
+ void start_phrase() {
+ frac &= -0xFFF;
+ accum = 0;
+ }
+
+ void print_bpm() const {
+ Serial.print(bpm >> 8);
+ uint8_t frac = scale8(100, bpm & 0xFF);
+ Serial.print(F("."));
+ if (frac < 10)
+ Serial.print(F("0"));
+ Serial.print(frac);
+ Serial.println(F("bpm"));
+ }
+
+};
diff --git a/usermods/Tubes/bluetooth.h b/usermods/Tubes/bluetooth.h
new file mode 100644
index 0000000000..3a48a51861
--- /dev/null
+++ b/usermods/Tubes/bluetooth.h
@@ -0,0 +1,497 @@
+#if 0
+#pragma once
+
+// THIS FILE ISN'T USED ANY MORE
+
+#include
+#include
+#include "global_state.h"
+
+
+#include "node.h"
+
+#define MAX_CONNECTED_CLIENTS 3
+
+#define DATA_UPDATE_SERVICE "D00B"
+
+typedef struct {
+ MeshNodeHeader header;
+ TubeState current;
+ TubeState next;
+} MeshStorage;
+
+// Asynchronous queue handling
+typedef struct {
+ MeshId id;
+ NimBLEAddress address;
+} MeshUpdateRequest;
+
+static TaskHandle_t xUpdaterTaskHandle;
+QueueHandle_t UpdaterQueue = xQueueCreate(5, sizeof(MeshUpdateRequest));
+void procUpdaterTask(void* pvParameters);
+
+/** None of these are required as they will be handled by the library with defaults. **
+ ** Remove as you see fit for your needs */
+class ServerCallbacks: public NimBLEServerCallbacks {
+ void onConnect(NimBLEServer* pServer) {
+ Serial.println("Client connected");
+ if (pServer->getConnectedCount() < MAX_CONNECTED_CLIENTS)
+ NimBLEDevice::startAdvertising();
+ };
+
+ /** Alternative onConnect() method to extract details of the connection.
+ * See: src/ble_gap.h for the details of the ble_gap_conn_desc struct.
+ */
+ void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) {
+ Serial.print("Client address: ");
+ Serial.println(NimBLEAddress(desc->peer_ota_addr).toString().c_str());
+ /** We can use the connection handle here to ask for different connection parameters.
+ * Args: connection handle, min connection interval, max connection interval
+ * latency, supervision timeout.
+ * Units; Min/Max Intervals: 1.25 millisecond increments.
+ * Latency: number of intervals allowed to skip.
+ * Timeout: 10 millisecond increments, try for 5x interval time for best results.
+ */
+ pServer->updateConnParams(desc->conn_handle, 24, 48, 0, 60);
+ };
+
+ void onMTUChange(uint16_t MTU, ble_gap_conn_desc* desc) {
+ Serial.printf("MTU updated: %u for connection ID: %u\n", MTU, desc->conn_handle);
+ };
+
+/********************* Security handled here **********************
+****** Note: these are the same return values as defaults ********/
+ uint32_t onPassKeyRequest(){
+ Serial.println("Server Passkey Request");
+ /** This should return a random 6 digit number for security
+ * or make your own static passkey as done here.
+ */
+ return 123456;
+ };
+
+ bool onConfirmPIN(uint32_t pass_key){
+ Serial.print("The passkey YES/NO number: ");Serial.println(pass_key);
+ /** Return false if passkeys don't match. */
+ return true;
+ };
+
+ void onAuthenticationComplete(ble_gap_conn_desc* desc){
+ /** Check that encryption was successful, if not we disconnect the client */
+ if(!desc->sec_state.encrypted) {
+ NimBLEDevice::getServer()->disconnect(desc->conn_handle);
+ Serial.println("Encrypt connection failed - disconnecting client");
+ return;
+ }
+ Serial.println("Starting BLE work!");
+ };
+};
+
+/** Handler class for characteristic actions */
+class CharacteristicCallbacks: public NimBLECharacteristicCallbacks {
+ void onRead(NimBLECharacteristic* pCharacteristic){
+ Serial.print(pCharacteristic->getUUID().toString().c_str());
+ Serial.print(": onRead(), value: ");
+ Serial.println(pCharacteristic->getValue().c_str());
+ };
+
+ void onWrite(NimBLECharacteristic* pCharacteristic) {
+ Serial.print(pCharacteristic->getUUID().toString().c_str());
+ Serial.print(": onWrite(), value: ");
+ Serial.println(pCharacteristic->getValue().c_str());
+
+ ESP.restart();
+ };
+
+ /** Called before notification or indication is sent,
+ * the value can be changed here before sending if desired.
+ */
+ void onNotify(NimBLECharacteristic* pCharacteristic) {
+ Serial.println("Sending notification to clients");
+ };
+
+
+ /** The status returned in status is defined in NimBLECharacteristic.h.
+ * The value returned in code is the NimBLE host return code.
+ */
+ void onStatus(NimBLECharacteristic* pCharacteristic, Status status, int code) {
+ String str = ("Notification/Indication status code: ");
+ str += status;
+ str += ", return code: ";
+ str += code;
+ str += ", ";
+ str += NimBLEUtils::returnCodeToString(code);
+ Serial.println(str);
+ };
+
+ void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) {
+ String str = "Client ID: ";
+ str += desc->conn_handle;
+ str += " Address: ";
+ str += std::string(NimBLEAddress(desc->peer_ota_addr)).c_str();
+ if(subValue == 0) {
+ str += " Unsubscribed to ";
+ }else if(subValue == 1) {
+ str += " Subscribed to notfications for ";
+ } else if(subValue == 2) {
+ str += " Subscribed to indications for ";
+ } else if(subValue == 3) {
+ str += " Subscribed to notifications and indications for ";
+ }
+ str += std::string(pCharacteristic->getUUID()).c_str();
+
+ Serial.println(str);
+ };
+};
+
+/** Handler class for descriptor actions */
+class DescriptorCallbacks : public NimBLEDescriptorCallbacks {
+ void onWrite(NimBLEDescriptor* pDescriptor) {
+ std::string dscVal = pDescriptor->getValue();
+ Serial.print("Descriptor witten value:");
+ Serial.println(dscVal.c_str());
+ };
+
+ void onRead(NimBLEDescriptor* pDescriptor) {
+ Serial.print(pDescriptor->getUUID().toString().c_str());
+ Serial.println(" Descriptor read");
+ };
+};
+
+/** Define callback instances globally to use for multiple Charateristics \ Descriptors */
+static DescriptorCallbacks dscCallbacks;
+static CharacteristicCallbacks chrCallbacks;
+
+
+NimBLEClient* connectToServer(NimBLEAddress &peer_address) {
+ NimBLEClient* pClient = nullptr;
+ bool known_server = false;
+
+ // Check if we have a client we should reuse
+ if (NimBLEDevice::getClientListSize()) {
+ // If we already know this peer, send false as the second argument in connect()
+ // to prevent refreshing the service database. This saves considerable time and power.
+ pClient = NimBLEDevice::getClientByPeerAddress(peer_address);
+ if (pClient)
+ known_server = true;
+ else
+ pClient = NimBLEDevice::getDisconnectedClient();
+
+ if (pClient) {
+ if (pClient->connect(peer_address, known_server))
+ return pClient;
+
+ // Failed to connect. Just drop the client rather than deleting it.
+ return NULL;
+ }
+ }
+
+ // No client to reuse; create a new one - if we can.
+ if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
+ Serial.println("Max clients reached - no more connections available");
+ return NULL;
+ }
+ pClient = NimBLEDevice::createClient();
+
+ // Set up this client and attempt to connect.
+ pClient->setConnectionParams(12,12,0,51);
+ pClient->setConnectTimeout(5);
+ if (!pClient->connect(peer_address)) {
+ // Created a client but failed to connect, don't need to keep it as it has no data.
+ NimBLEDevice::deleteClient(pClient);
+ return NULL;
+ }
+
+ return pClient;
+}
+
+
+class BLEMeshNode: public NimBLEAdvertisedDeviceCallbacks {
+ public:
+ bool alive = false; // true if radio booted up
+ bool changed = false;
+
+ MeshNodeHeader ids;
+
+ uint16_t serviceUUID = 0xD00F;
+
+ NimBLEServer* pServer = nullptr;
+ NimBLEService* pService = nullptr;
+ NimBLEScan* pScanner = nullptr;
+ NimBLEAddress uplink_address;
+
+ MessageReceiver *receiver = nullptr;
+
+ BLEMeshNode(MessageReceiver *receiver) {
+ this->receiver = receiver;
+ }
+
+ void advertise() {
+ auto service_data = std::string((char *)&ids, sizeof(ids));
+
+#ifdef BLE_MESH
+ if (!pService)
+ return;
+
+ // Add the services to the advertisement data
+ auto pAdvertising = NimBLEDevice::getAdvertising();
+ if (!pAdvertising)
+ return;
+
+
+ // Reset the device name:
+ // NimBLEDevice::deinit(false);
+ // NimBLEDevice::init(node_name);
+
+ // Set advertisement
+ pAdvertising->stop();
+ pAdvertising->setServiceData(NimBLEUUID(serviceUUID), service_data);
+ pAdvertising->start();
+#endif
+
+ Serial.printf("Advertising %s\n", node_name);
+ }
+
+ void init_service() {
+ /** Optional: set the transmit power, default is 3db */
+ #ifdef ESP_PLATFORM
+ NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
+ #else
+ NimBLEDevice::setPower(9); /** +9db */
+ #endif
+
+ /** Set the IO capabilities of the device, each option will trigger a different pairing method.
+ * BLE_HS_IO_DISPLAY_ONLY - Passkey pairing
+ * BLE_HS_IO_DISPLAY_YESNO - Numeric comparison pairing
+ * BLE_HS_IO_NO_INPUT_OUTPUT - DEFAULT setting - just works pairing
+ */
+ NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);
+ NimBLEDevice::setSecurityAuth(false, false, true);
+
+ pServer = NimBLEDevice::createServer();
+ pServer->setCallbacks(new ServerCallbacks());
+ pServer->advertiseOnDisconnect(true);
+
+ pService = pServer->createService(DATA_UPDATE_SERVICE);
+ NimBLECharacteristic* pCharacteristic = pService->createCharacteristic(
+ "FEED",
+ NIMBLE_PROPERTY::READ
+ );
+ pCharacteristic->setValue("");
+ pCharacteristic->setCallbacks(&chrCallbacks);
+
+ pCharacteristic = pService->createCharacteristic(
+ "F00D",
+ NIMBLE_PROPERTY::WRITE
+ );
+ pCharacteristic->setCallbacks(&chrCallbacks);
+
+ /** Start the services when finished creating all Characteristics and Descriptors */
+ pService->start();
+
+ /** Add the services to the advertisement data **/
+ NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
+ pAdvertising->addServiceUUID(pService->getUUID());
+ pAdvertising->setScanResponse(false);
+ pAdvertising->setAppearance(0x07C6); // Multi-color LED array
+ pAdvertising->setAdvertisementType(BLE_GAP_CONN_MODE_UND);
+
+ changed = true;
+ }
+
+ void init_scanner() {
+ NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE);
+ NimBLEDevice::setScanDuplicateCacheSize(20);
+
+ pScanner = NimBLEDevice::getScan(); //create new scan
+ // Set the callback for when devices are discovered, no duplicates.
+ pScanner->setAdvertisedDeviceCallbacks(this, false);
+ pScanner->setActiveScan(false); // Don't request data (it uses more energy)
+ pScanner->setInterval(97); // How often the scan occurs / switches channels; in milliseconds,
+ pScanner->setWindow(37); // How long to scan during the interval; in milliseconds.
+ pScanner->setMaxResults(0); // do not store the scan results, use callback only.
+ }
+
+ void init_updater() {
+ xTaskCreate(
+ procUpdaterTask, /* Function to implement the task */
+ "UpdaterTask", /* Name of the task */
+ 3840, /* Stack size in bytes */
+ this, /* Task input parameter */
+ tskIDLE_PRIORITY+1, /* Priority of the task */
+ &xUpdaterTaskHandle /* Task handle. */
+ );
+ }
+
+ void init() {
+ WiFi.mode(WIFI_AP_STA);
+
+ esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
+
+#ifdef BLE_MESH
+ esp_coex_preference_set(ESP_COEX_PREFER_BT);
+ NimBLEDevice::init(std::string("Tube"));
+ init_scanner();
+ init_service();
+ init_updater();
+#endif
+ this->alive = true;
+ }
+
+ void onPeerPing(MeshNodeHeader* pRemoteNode, NimBLEAdvertisedDevice* pAdvertisedDevice) {
+ Serial.printf("Found %03X/%03X at %s\n",
+ pRemoteNode->id,
+ pRemoteNode->uplinkId,
+ std::string(pAdvertisedDevice->getAddress()).c_str()
+ );
+
+ if (pRemoteNode->id == ids.id) {
+ Serial.println("Detected an ID conflict.");
+ this->reset();
+ }
+
+ if (pRemoteNode->id > ids.id && pRemoteNode->id > ids.uplinkId) {
+ follow(pRemoteNode->id, pAdvertisedDevice);
+ }
+
+ if (pRemoteNode->id == ids.uplinkId) {
+ this->onUplinkAlive();
+ }
+ }
+
+ void setup() {
+ Serial.println("Mesh: ok");
+ }
+
+ void update() {
+ // Don't do anything for the first second to avoid crashing WiFi
+ if (millis() < 1000)
+ return;
+
+ if (!this->alive) {
+ this->init();
+ }
+
+#ifdef BLE_MESH
+ MeshUpdateRequest request = {
+ .id = this->ids.uplinkId,
+ .address = this->uplink_address
+ };
+ if (xQueueSend(UpdaterQueue, &request, 0) != pdTRUE) {
+ Serial.println("Update queue is full!");
+ }
+#endif
+ }
+
+ // If any actions caused the service to change, re-advertise with new values
+ if (changed) {
+ advertise();
+ changed = false;
+ }
+
+ if (this->pScanner && !this->pScanner->isScanning()) {
+ // Start scan with: duration = 0 seconds(forever), no scan end callback, not a continuation of a previous scan.
+ this->pScanner->start(0, nullptr, false);
+ }
+ }
+
+ void update_node_storage(TubeState ¤t, TubeState &next) {
+#ifdef BLE_MESH
+ // Broadcast the current effect state to every connected client
+
+ if (!pServer || pServer->getConnectedCount() == 0)
+ return;
+
+ if (!pService)
+ return;
+
+ NimBLECharacteristic* pCharacteristic = pService->getCharacteristic("FEED");
+ if(!pCharacteristic)
+ return;
+
+ // Store this data in the characteristic
+ MeshStorage storage = {
+ .header = this->ids,
+ .current = current,
+ .next = next
+ };
+ pCharacteristic->setValue(storage);
+#endif
+ }
+
+ // ====== CALLBACKS =======
+ void onResult(NimBLEAdvertisedDevice* pAdvertisedDevice) {
+ // Discovered a peer via scanning.
+
+ if (!pAdvertisedDevice->isAdvertisingService(NimBLEUUID("D00B")))
+ return;
+
+ // Make sure it's booted up and advertising Mesh IDs
+ auto data = pAdvertisedDevice->getServiceData(NimBLEUUID(serviceUUID));
+ if (data.length() != sizeof(MeshNodeHeader))
+ return;
+ MeshNodeHeader* pRemoteNode = (MeshNodeHeader *)data.c_str();
+ if (pRemoteNode->version != this->ids.version)
+ return;
+
+ this->onPeerPing(pRemoteNode, pAdvertisedDevice);
+ }
+
+ void onUpdateData(MeshUpdateRequest &request, MeshStorage &storage) {
+ if (request.id != storage.header.id) {
+ Serial.println("Uplink is invalid!");
+ // The remote server has changed its ID. We need to adapt.
+ follow(0);
+ changed = true;
+ return;
+ }
+ this->onUplinkAlive();
+
+ // Process the command
+ this->receiver->onCommand(
+ storage.header.id,
+ COMMAND_STATE,
+ &storage.current
+ );
+ this->receiver->onCommand(
+ storage.header.id,
+ COMMAND_NEXT,
+ &storage.next
+ );
+ }
+
+ void onUplinkAlive() {
+ // Track the last time we received a message from our uplink
+ this->uplinkTimer.start(UPLINK_TIMEOUT);
+ }
+
+};
+
+
+// UPDATER
+// This is an async task handler that awaits requests to update data,
+// then connects to the requested server and fetches the data.
+void procUpdaterTask(void* pvParameters) {
+ BLEMeshNode *pNode = (BLEMeshNode*)pvParameters;
+ MeshUpdateRequest request;
+
+ for (;;) {
+ // Wait to be told to update (the queue blocks)
+ xQueueReceive(UpdaterQueue, &request, portMAX_DELAY);
+
+ // Got a request to update, so try to connect and pull down data.
+ auto uplink_address = request.address;
+ auto pClient = connectToServer(uplink_address);
+ if (!pClient)
+ continue;
+
+ auto pService = pClient->getService(DATA_UPDATE_SERVICE);
+ if (pService) {
+ auto pCharacteristic = pService->getCharacteristic("FEED");
+ MeshStorage storage = pCharacteristic->readValue();
+ pNode->onUpdateData(request, storage);
+ }
+ pClient->disconnect();
+ }
+
+}
+#endif
\ No newline at end of file
diff --git a/usermods/Tubes/controller.h b/usermods/Tubes/controller.h
new file mode 100644
index 0000000000..aca749f21a
--- /dev/null
+++ b/usermods/Tubes/controller.h
@@ -0,0 +1,1504 @@
+#pragma once
+
+#include
+#include "wled.h"
+#include "FX.h"
+#include "updater.h"
+#include "sound.h"
+
+#include "beats.h"
+
+#include "pattern.h"
+#include "palettes.h"
+#include "effects.h"
+#include "led_strip.h"
+#include "global_state.h"
+#include "node.h"
+
+#define EEPSIZE 2560
+
+const static uint8_t DEFAULT_MASTER_BRIGHTNESS = 200;
+const static uint8_t DEFAULT_TUBE_BRIGHTNESS = 120;
+const static uint8_t DEFAULT_TANK_BRIGHTNESS = 240;
+#define DEFAULT_WLED_FX FX_MODE_RAINBOW_CYCLE
+
+#define STATUS_UPDATE_PERIOD 2000
+
+#define MIN_COLOR_CHANGE_PHRASES 4
+#define MAX_COLOR_CHANGE_PHRASES 10
+
+#define ROLE_EEPROM_LOCATION 2559
+#define BOOT_OPTIONS_EEPROM_LOCATION 2551
+
+// #define IDENTIFY_STUCK_PATTERNS
+// #define IDENTIFY_STUCK_PALETTES
+
+typedef struct {
+ bool debugging;
+ uint8_t brightness;
+
+ uint8_t reserved[12];
+} ControllerOptions;
+
+typedef struct {
+ TubeState current;
+ TubeState next;
+} TubeStates;
+
+typedef enum ControllerRole : uint8_t {
+ UnknownRole = 0,
+ DefaultRole = 10, // Turn on in power saving mode
+ CampRole = 50, // Turn on in non-power-saving mode
+ InstallationRole = 100, // Disable power-saving mode completely
+ SmallArtRole = 120, // < 1/2 the pixels, scale the art
+ LegacyRole = 190, // LEGACY: 1/2 the pixels, no "power saving" necessary, no scaling
+ MasterRole = 200 // Controls all the others
+} ControllerRole;
+
+typedef struct BootOptions {
+ unsigned int default_power_save:2;
+} BootOptions;
+
+#define BOOT_OPTION_POWER_SAVE_DEFAULT 0
+#define BOOT_OPTION_POWER_SAVE_OFF 1
+#define BOOT_OPTION_POWER_SAVE_ON 2
+
+typedef struct {
+ char key;
+ uint8_t arg;
+} Action;
+
+#define NUM_VSTRIPS 3
+
+#define DEBOUNCE_TIME 40
+
+class Button {
+ public:
+ Timer debounceTimer;
+ uint8_t pin;
+ bool lastPressed = false;
+
+ void setup(uint8_t p) {
+ pin = p;
+ pinMode(pin, INPUT_PULLUP);
+ debounceTimer.start(0);
+ }
+
+ bool pressed() {
+ if (digitalRead(pin) == HIGH) {
+ return !debounceTimer.ended();
+ }
+
+ debounceTimer.start(DEBOUNCE_TIME);
+ return true;
+ }
+
+ bool triggered() {
+ // Triggers BOTH low->high AND high->low
+ bool p = pressed();
+ bool lp = lastPressed;
+ lastPressed = p;
+ return p != lp;
+ }
+};
+
+class PatternController : public MessageReceiver {
+ public:
+ const static int FRAMES_PER_SECOND = 60; // how often we animate, in frames per second
+ const static int REFRESH_PERIOD = 1000 / FRAMES_PER_SECOND; // how often we animate, in milliseconds
+
+ VirtualStrip *vstrips[NUM_VSTRIPS];
+ uint8_t next_vstrip = 0;
+ bool canOverride = false;
+ uint8_t paletteOverride = 0;
+ uint8_t patternOverride = 0;
+ uint16_t wled_fader = 0;
+ ControllerRole role;
+ bool power_save = false; // Default to power save mode OFF but 3 sec press turns it on
+ uint8_t flashColor = 0;
+
+ AutoUpdater updater = AutoUpdater();
+ Sounder sound = Sounder();
+
+ Timer graphicsTimer;
+ Timer updateTimer;
+ Timer paletteOverrideTimer;
+ Timer patternOverrideTimer;
+ Timer flashTimer;
+ Timer selectTimer;
+
+#ifdef USELCD
+ Lcd *lcd;
+#endif
+ LEDs led_strip;
+ BeatController beats;
+ Effects effects;
+ LightNode node;
+
+ ControllerOptions options;
+ char key_buffer[20] = {0};
+
+ Energy energy=Chill;
+ TubeState current_state;
+ TubeState next_state;
+
+ // When a pattern is boring, spice it up a bit with more effects
+ bool isBoring = false;
+
+ PatternController() : node(this) {
+#ifdef USELCD
+ lcd = new Lcd();
+#endif
+
+ for (auto i=0; i < NUM_VSTRIPS; i++) {
+ vstrips[i] = new VirtualStrip();
+ }
+ }
+
+ // Compatibility ctor for older call sites that pass a real LED count.
+ PatternController(uint16_t real_led_count) : PatternController() {
+ (void)real_led_count;
+ }
+
+ bool isMasterRole() const {
+#if defined(GOLDEN) || defined(CHRISTMAS) || defined(RUBY)
+ return true;
+#endif
+ return role >= MasterRole;
+ }
+
+ void setup()
+ {
+ EEPROM.begin(EEPSIZE);
+ role = (ControllerRole)EEPROM.read(ROLE_EEPROM_LOCATION);
+ if (role == 255) {
+ role = UnknownRole;
+ }
+ Serial.printf("Role = %d\n", role);
+
+ auto b = EEPROM.read(BOOT_OPTIONS_EEPROM_LOCATION);
+ Serial.printf("EEPROM read: %d\n", b);
+ EEPROM.end();
+
+ BootOptions* boot = (BootOptions*)&b;
+ switch (boot->default_power_save) {
+ case BOOT_OPTION_POWER_SAVE_OFF:
+ power_save = 0;
+ break;
+ case BOOT_OPTION_POWER_SAVE_ON:
+ power_save = 1;
+ break;
+ default:
+ power_save = (role < CampRole);
+ break;
+ }
+
+ if (role <= CampRole)
+ strip.ablMilliampsMax = min(ABL_MILLIAMPS_DEFAULT,700); // Really limit for batteries
+ else if (role <= InstallationRole)
+ strip.ablMilliampsMax = 1000;
+ else
+ strip.ablMilliampsMax = 1400;
+
+
+ beats.setup();
+ node.setup();
+
+ if (role >= MasterRole) {
+ node.reset(3850 + role); // MASTER ID
+ options.brightness = DEFAULT_MASTER_BRIGHTNESS;
+ } else if (role >= LegacyRole) {
+ options.brightness = DEFAULT_TUBE_BRIGHTNESS;
+ } else if (role == InstallationRole) {
+ options.brightness = DEFAULT_TANK_BRIGHTNESS;
+ } else {
+ options.brightness = DEFAULT_TUBE_BRIGHTNESS;
+ }
+#if defined(GOLDEN) || defined(CHRISTMAS) || defined(RUBY)
+ node.reset(0xFFF);
+#endif
+ options.debugging = false;
+ load_options(options, true);
+
+#ifdef USELCD
+ lcd->setup();
+#endif
+ set_next_pattern(0);
+ set_next_palette(0);
+ set_next_effect(0);
+ next_state.pattern_phrase = 0;
+ next_state.palette_phrase = 0;
+ next_state.effect_phrase = 0;
+ set_wled_palette(0); // Default palette
+ set_wled_pattern(0, 128, 128); // Default pattern
+
+ sound.setup();
+
+ updateTimer.start(STATUS_UPDATE_PERIOD); // Ready to send an update as soon as we're able to
+ Serial.println("Controller: ok");
+ }
+
+ void do_pattern_changes() {
+ uint16_t phrase = current_state.beat_frame >> 12;
+ bool changed = false;
+
+ if (phrase >= next_state.pattern_phrase) {
+#ifdef IDENTIFY_STUCK_PATTERNS
+ Serial.println("Time to change pattern");
+#endif
+ load_pattern(next_state);
+ next_state.pattern_phrase = phrase + set_next_pattern(phrase);
+
+ // Don't change pattern and others at the same time
+ while (next_state.pattern_phrase == next_state.palette_phrase || next_state.pattern_phrase == next_state.effect_phrase) {
+ next_state.pattern_phrase += random8(1,3);
+ }
+ changed = true;
+ }
+ if (phrase >= next_state.palette_phrase) {
+#ifdef IDENTIFY_STUCK_PATTERNS
+ Serial.println("Time to change palette");
+#endif
+ load_palette(next_state);
+ next_state.palette_phrase = phrase + set_next_palette(phrase);
+
+ // Don't change palette and others at the same time
+ while (next_state.palette_phrase == next_state.pattern_phrase || next_state.palette_phrase == next_state.effect_phrase) {
+ next_state.palette_phrase += random8(1,3);
+ }
+ changed = true;
+ }
+ if (phrase >= next_state.effect_phrase) {
+#ifdef IDENTIFY_STUCK_PATTERNS
+ Serial.println("Time to change effect");
+#endif
+ load_effect(next_state);
+ next_state.effect_phrase = phrase + set_next_effect(phrase);
+
+ // Don't change palette and others at the same time
+ while (next_state.effect_phrase == next_state.pattern_phrase || next_state.effect_phrase == next_state.palette_phrase) {
+ next_state.effect_phrase += random8(1,3);
+ }
+ changed = true;
+ }
+
+ if (changed) {
+ next_state.print();
+ Serial.println();
+ }
+ }
+
+ void cancelOverrides() {
+ // Release the WLED overrides and take over control of the strip again.
+ paletteOverrideTimer.stop();
+ patternOverrideTimer.stop();
+ }
+
+ void enterSelectMode() {
+ selectTimer.start(20000);
+ }
+
+ bool isSelecting() const {
+ return !selectTimer.ended();
+ }
+
+ bool isSelected() const {
+ return updater.status == Ready;
+ }
+
+ void select(bool selected = true) {
+ if (selected)
+ updater.ready();
+ else {
+ updater.stop();
+ WiFi.softAPdisconnect(true);
+ }
+ }
+
+ void deselect() {
+ select(false);
+ }
+
+ void set_palette_override(uint8_t value) {
+ if (!canOverride)
+ return;
+ if (value == paletteOverride)
+ return;
+
+ paletteOverride = value;
+ if (value) {
+ Serial.println("WLED has control of palette.");
+ paletteOverrideTimer.start(300000); // 5 minutes of manual control
+ } else {
+ Serial.println("Turning off WLED control of palette.");
+ paletteOverrideTimer.stop();
+ set_wled_palette(current_state.palette_id);
+ }
+ }
+
+ void set_pattern_override(uint8_t value, uint8_t auto_mode) {
+ if (!canOverride)
+ return;
+ if (value == DEFAULT_WLED_FX && !patternOverride)
+ return;
+ if (value == patternOverride)
+ return;
+
+ patternOverride = value;
+ if (value) {
+ Serial.println("WLED has control of patterns.");
+ patternOverrideTimer.start(300000); // 5 minutes of manual control
+ transitionDelay = 500; // Short transitions
+ } else {
+ Serial.println("Turning off WLED control of patterns.");
+ patternOverrideTimer.stop();
+ transitionDelay = 8000; // Back to long transitions
+
+ uint8_t param = modeParameter(auto_mode);
+ set_wled_pattern(auto_mode, param, param);
+ }
+ }
+
+ void update()
+ {
+ read_keys();
+
+ beats.update();
+
+ // Update the mesh
+ node.update();
+
+ // Update sound meter
+ sound.update();
+
+ // Update patterns to the beat
+ update_beat();
+
+ Segment& segment = strip.getMainSegment();
+
+ // You can only go into manual control after enabling the wifi
+ if (apActive && updater.status != Ready)
+ canOverride = true;
+
+ // Detect manual overrides & update the current state to match.
+ if (canOverride) {
+ if (paletteOverride && (paletteOverrideTimer.ended() || !apActive)) {
+ set_palette_override(0);
+ } else if (segment.palette != current_state.palette_id) {
+ set_palette_override(segment.palette);
+ }
+
+ uint8_t wled_mode = gPatterns[current_state.pattern_id].wled_fx_id;
+ if (wled_mode < 10)
+ wled_mode = DEFAULT_WLED_FX;
+ if (patternOverride && (patternOverrideTimer.ended() || !apActive)) {
+ set_pattern_override(0, wled_mode);
+ } else if (segment.mode != wled_mode) {
+ set_pattern_override(segment.mode, wled_mode);
+ }
+ }
+
+ do_pattern_changes();
+
+ if (graphicsTimer.every(REFRESH_PERIOD)) {
+ updateGraphics();
+ }
+
+ // Update current status
+ if (updateTimer.every(STATUS_UPDATE_PERIOD)) {
+ // Transmit less often when following
+ if (!node.isFollowing() || random(0, 4) == 0) {
+ send_update();
+ }
+ }
+
+ updater.update();
+
+#ifdef USELCD
+ if (lcd->active) {
+ lcd->size(1);
+ lcd->write(0,56, current_state.beat_frame);
+ lcd->write(80,56, x_axis);
+ lcd->write(100,56, y_axis);
+ lcd->show();
+
+ lcd->update();
+ }
+#endif
+ }
+
+ void handleOverlayDraw() {
+ // In manual mode WLED is always active
+ if (patternOverride) {
+ wled_fader = 0xFFFF;
+ }
+
+ uint16_t length = strip.getLengthTotal();
+
+ // Crossfade between the custom pattern engine and WLED
+ uint8_t fader = wled_fader >> 8;
+ if (fader < 255) {
+ // Perform a cross-fade between current WLED mode and the external buffer
+ for (int i = 0; i < length; i++) {
+ CRGB c = getBlendedPixelColor(i);
+ if (fader > 0) {
+ CRGB color2 = strip.getPixelColor(i);
+ uint8_t r = blend8(c.r, color2.r, fader);
+ uint8_t g = blend8(c.g, color2.g, fader);
+ uint8_t b = blend8(c.b, color2.b, fader);
+#ifdef RUBY
+ // Simple average brightness for a "luminosity" measure
+ uint8_t brightness = (uint16_t)(r + g + b) / 3;
+
+ // Check if it's near white (all channels fairly similar and somewhat bright)
+ // You can tweak thresholds to taste.
+ bool isNearWhite = (abs(r - g) < 20 && abs(g - b) < 20 && (r + g + b) > 200);
+
+ // Force everything into a shade of red:
+ uint8_t redLevel = brightness;
+ uint8_t greenLevel = 0;
+ uint8_t blueLevel = 0;
+
+ // If it’s near white, add a little G/B so it’s not pure red.
+ if(isNearWhite) {
+ greenLevel = brightness / 2;
+ blueLevel = brightness / 2;
+ }
+
+ c = CRGB(redLevel, greenLevel, blueLevel);
+#else
+ c = CRGB(r,g,b);
+#endif
+ }
+ strip.setPixelColor(i, c);
+ }
+ }
+
+ // Power Save mode: reduce number of displayed pixels
+ // Only affects non-powered poles
+ if (power_save && role < InstallationRole) {
+ // Screen door effect to save power
+ for (int i = 0; i < length; i++) {
+ if (i % 2) {
+ strip.setPixelColor(i, CRGB::Black);
+ }
+ }
+ }
+
+ sound.handleOverlayDraw();
+
+ // Draw effects layers over whatever WLED is doing.
+ // But not in manual (WLED) mode
+ if (!patternOverride) {
+ effects.draw(&strip);
+ }
+
+ // Make the art half-size if it has a small number of pixels
+ if (role >= MasterRole || role == SmallArtRole) {
+ int p = 0;
+ for (int i = 0; i < length; i++) {
+ CRGB c = strip.getPixelColor(i++); // i advances by 2
+ CRGB c2 = strip.getPixelColor(i);
+ nblend(c, c2, 128);
+ if (role >= MasterRole) {
+ nblend(c, CRGB::Black, 128);
+ }
+ strip.setPixelColor(p++, c);
+ }
+ }
+
+ if (flashColor) {
+ if (flashTimer.ended())
+ flashColor = 0;
+ else {
+ if (millis() % 4000 < 2000) {
+ auto chsv = CHSV(flashColor, 255, 255);
+ for (int i = 0; i < length; i++) {
+ strip.setPixelColor(i, CRGB(chsv));
+ }
+ }
+ }
+ }
+
+ updater.handleOverlayDraw();
+ }
+
+ void restart_phrase() {
+ beats.start_phrase();
+ update_beat();
+ send_update();
+ }
+
+ void set_phrase_position(uint8_t pos) {
+ beats.sync(beats.bpm, (beats.frac & -0xFFF) + (pos<<8));
+ update_beat();
+ send_update();
+ }
+
+ void set_tapped_bpm(accum88 bpm, uint8_t pos=15) {
+ // By default, restarts at 15th beat - because this is the end of a tap
+ beats.sync(bpm, (beats.frac & -0xFFF) + (pos<<8));
+ update_beat();
+ send_update();
+ }
+
+ void request_new_bpm(accum88 new_bpm = 0) {
+ // 0 = toggle 120 to 125
+ if (new_bpm == 0)
+ new_bpm = current_state.bpm>>8 >= 123 ? 120<<8 : 125<<8;
+
+ if (node.isFollowing()) {
+ // Send a request up to ROOT
+ broadcast_bpm(new_bpm);
+ } else {
+ set_tapped_bpm(new_bpm, 0);
+ }
+ }
+
+ void update_beat() {
+ current_state.bpm = next_state.bpm = beats.bpm;
+ current_state.beat_frame = particle_beat_frame = beats.frac; // (particle_beat_frame is a hack)
+ if (current_state.bpm>>8 <= 118) // Hip hop / ghettofunk
+ energy = MediumEnergy;
+ else if (current_state.bpm>>8 >= 125) // House & breaks
+ energy = HighEnergy;
+ else if (current_state.bpm>>8 > 120) // Tech house
+ energy = MediumEnergy;
+ else
+ energy = Chill; // Deep house
+ }
+
+ void send_update() {
+ Serial.print(" ");
+ current_state.print();
+ Serial.print(F(" "));
+
+ uint16_t phrase = current_state.beat_frame >> 12;
+ Serial.print(F(" "));
+ Serial.print(next_state.pattern_phrase - phrase);
+ Serial.print(F("P "));
+ Serial.print(next_state.palette_phrase - phrase);
+ Serial.print(F("C "));
+ Serial.print(next_state.effect_phrase - phrase);
+ Serial.print(F("E: "));
+ next_state.print();
+ Serial.print(F(" "));
+ Serial.println();
+
+ broadcast_state();
+ }
+
+ void background_changed() {
+ update_background();
+ current_state.print();
+ Serial.println();
+ }
+
+ void load_options(ControllerOptions &options, bool init=false) {
+ // Power-saving devices retain their WLED brightness
+ if (init || !power_save)
+ strip.setBrightness(options.brightness);
+ }
+
+ void load_pattern(TubeState &tube_state) {
+ if (current_state.pattern_id == tube_state.pattern_id
+ && current_state.pattern_sync_id == tube_state.pattern_sync_id)
+ return;
+
+ current_state.pattern_phrase = tube_state.pattern_phrase;
+ current_state.pattern_id = tube_state.pattern_id % gPatternCount;
+ current_state.pattern_sync_id = tube_state.pattern_sync_id;
+ isBoring = gPatterns[current_state.pattern_id].control.energy == Boring;
+
+ Serial.print(F("Change pattern "));
+ background_changed();
+ }
+
+ bool isShowingWled() const {
+ return current_state.pattern_id >= numInternalPatterns;
+ }
+
+ uint8_t modeParameter(uint8_t mode) {
+ switch (energy) {
+ case Boring:
+ // Spice things up a bit
+ return 128;
+
+ case Chill:
+ return 90;
+
+ case HighEnergy:
+ return 140;
+
+ default:
+ case MediumEnergy:
+ return 128;
+ }
+ }
+
+ // For now, can't crossfade between internal and WLED patterns
+ // If currently running an WLED pattern, only select from internal patterns.
+ uint8_t get_valid_next_pattern() {
+ if (isShowingWled())
+ return random8(0, numInternalPatterns);
+ return random8(0, gPatternCount);
+ }
+
+ // Choose the pattern to display at the next pattern cycle
+ // Return the number of phrases until the next pattern cycle
+ uint16_t set_next_pattern(uint16_t phrase) {
+ uint8_t pattern_id;
+ PatternDef def;
+
+#ifdef IDENTIFY_STUCK_PATTERNS
+ Serial.println("Changing next pattern");
+#endif
+ // Try 10 times to find a pattern that fits the current "energy"
+ for (int i = 0; i < 10; i++) {
+ pattern_id = get_valid_next_pattern();
+ def = gPatterns[pattern_id];
+ if (def.control.energy <= energy)
+ break;
+ }
+#ifdef IDENTIFY_STUCK_PATTERNS
+ Serial.printf("Next pattern will be %d\n", pattern_id);
+#endif
+
+ next_state.pattern_id = pattern_id;
+ next_state.pattern_sync_id = randomSyncMode();
+
+ switch (def.control.duration) {
+ case ExtraShortDuration: return random8(2, 6);
+ case ShortDuration: return random8(5,15);
+ case MediumDuration: return random8(15,25);
+ case LongDuration: return random8(20,40);
+ case ExtraLongDuration: return random8(25, 60);
+ }
+ return 5;
+ }
+
+ void load_palette(TubeState &tube_state) {
+ if (current_state.palette_id == tube_state.palette_id)
+ return;
+
+ current_state.palette_phrase = tube_state.palette_phrase;
+ current_state.palette_id = tube_state.palette_id % gGradientPaletteCount;
+ set_wled_palette(current_state.palette_id);
+ }
+
+ // Choose the palette to display at the next palette cycle
+ // Return the number of phrases until the next palette cycle
+ uint16_t set_next_palette(uint16_t phrase) {
+#if defined(GOLDEN)
+ uint r = random8(0, 4);
+ uint colors[4] = {18, 58, 71, 111};
+ next_state.palette_id = colors[r];
+#elif defined(CHRISTMAS) // 81, 107 are too bright
+ uint r = random8(0, 26);
+ uint colors[26] = {/*gold:*/18, 58, 71, 111,
+ /*yes:*/25, 34, 61, 63, 81, 112,
+ /*yesx2:*/25, 34, 61, 63, 81, 112,
+ /*best yes:*/25, 34, 34, 61, 63, 81, 112,
+ /*maybe:*/81, 28, 107};
+ next_state.palette_id = colors[r];
+#elif defined(RUBY) // 81, 107 are too bright
+ uint r = random8(0, 20);
+ uint colors[20] = {/*gold:*/,
+ /*yes:*/21,
+ /*best yes:*/,
+ /*maybe:*/33, 35, 44, 81, 93, 107;
+ next_state.palette_id = colors[r];
+#else
+ // Don't select the built-in palettes
+ next_state.palette_id = random8(6, gGradientPaletteCount);
+#endif
+
+ auto phrases = random8(MIN_COLOR_CHANGE_PHRASES, MAX_COLOR_CHANGE_PHRASES);
+
+ // Change color more often in boring patterns
+ if (isBoring) {
+ phrases /= 2;
+ }
+ return phrases;
+ }
+
+ void load_effect(TubeState &tube_state) {
+ if (current_state.effect_params.effect == tube_state.effect_params.effect &&
+ current_state.effect_params.pen == tube_state.effect_params.pen &&
+ current_state.effect_params.chance == tube_state.effect_params.chance)
+ return;
+
+ _load_effect(tube_state.effect_params);
+ }
+
+ void _load_effect(EffectParameters params) {
+ current_state.effect_params = params;
+
+ Serial.print(F("Change effect "));
+ current_state.print();
+ Serial.println();
+
+ effects.load(current_state.effect_params);
+ }
+
+ // Choose the effect to display at the next effect cycle
+ // Return the number of phrases until the next effect cycle
+ uint16_t set_next_effect(uint16_t phrase) {
+ uint8_t effect_num = random8(gEffectCount);
+
+ // Pick a random effect to add; boring patterns get better chance at having an effect.
+ EffectDef def = gEffects[effect_num];
+ if (def.control.energy > energy) {
+ def = gEffects[0];
+ }
+
+ next_state.effect_params = def.params;
+
+ switch (def.control.duration) {
+ case ExtraShortDuration: return random(1,3);
+ case ShortDuration: return random(2,4);
+ case MediumDuration: return random(4,7);
+ case LongDuration: return random(8, 11);
+ case ExtraLongDuration: return random(10,15);
+ }
+ return 1;
+ }
+
+ void update_background() {
+ Background background;
+ background.animate = gPatterns[current_state.pattern_id].backgroundFn;
+ background.wled_fx_id = gPatterns[current_state.pattern_id].wled_fx_id;
+ background.palette_id = current_state.palette_id;
+ background.sync = (SyncMode)current_state.pattern_sync_id;
+
+ // Use one of the virtual strips to render the patterns.
+ // A WLED-based pattern exists on the virtual strip, but causes
+ // it to do nothing since WLED merging happens in handleOverlayDraw.
+ // Reuse virtual strips to prevent heap fragmentation
+ for (uint8_t i = 0; i < NUM_VSTRIPS; i++) {
+ vstrips[i]->fadeOut();
+ }
+ vstrips[next_vstrip]->load(background);
+ next_vstrip = (next_vstrip + 1) % NUM_VSTRIPS;
+
+ uint8_t param = modeParameter(background.wled_fx_id);
+ set_wled_pattern(background.wled_fx_id, param, param);
+ set_wled_palette(background.palette_id);
+ }
+
+ bool isUnderWledControl() const {
+ return paletteOverride || patternOverride;
+ }
+
+ void set_wled_palette(uint8_t palette_id) {
+ if (paletteOverride)
+ palette_id = paletteOverride;
+
+ Segment& seg = strip.getMainSegment();
+ seg.setPalette(palette_id);
+
+ stateChanged = true;
+ stateUpdated(CALL_MODE_DIRECT_CHANGE);
+ }
+
+ void set_wled_pattern(uint8_t pattern_id, uint8_t speed, uint8_t intensity) {
+ if (patternOverride)
+ pattern_id = patternOverride;
+ else if (pattern_id == 0)
+ pattern_id = DEFAULT_WLED_FX; // Never set it to solid
+
+ Segment& seg = strip.getMainSegment();
+ seg.speed = speed;
+ seg.intensity = intensity;
+ seg.setMode(pattern_id);
+
+ stateChanged = true;
+ stateUpdated(CALL_MODE_DIRECT_CHANGE);
+ }
+
+ void setBrightness(uint8_t brightness) {
+ Serial.printf("brightness: %d\n", brightness);
+
+ options.brightness = brightness;
+ load_options(options);
+
+ // The master controls all followers
+ if (!node.isFollowing())
+ broadcast_options();
+ }
+
+ void setDebugging(bool debugging) {
+ Serial.printf("debugging: %d\n", debugging);
+
+ options.debugging = debugging;
+ load_options(options);
+
+ // The master controls all followers
+ if (!node.isFollowing())
+ broadcast_options();
+ }
+
+ void togglePowerSave() {
+ setPowerSave(!power_save);
+ }
+
+ void setPowerSave(bool ps) {
+ power_save = ps;
+ Serial.printf("power_save: %d\n", power_save);
+
+ // Remember this setting on the next boot
+ EEPROM.begin(2560);
+ auto b = EEPROM.read(BOOT_OPTIONS_EEPROM_LOCATION);
+ BootOptions* boot = (BootOptions*)&b;
+ if (power_save)
+ boot->default_power_save = BOOT_OPTION_POWER_SAVE_ON;
+ else
+ boot->default_power_save = BOOT_OPTION_POWER_SAVE_OFF;
+ EEPROM.write(BOOT_OPTIONS_EEPROM_LOCATION, b); // Reset all boot options
+ Serial.printf("wrote: %d\n", b);
+ EEPROM.end();
+ }
+
+ void setRole(ControllerRole r) {
+ role = r;
+ Serial.printf("Role = %d", role);
+ EEPROM.begin(EEPSIZE);
+ EEPROM.write(ROLE_EEPROM_LOCATION, role);
+ EEPROM.write(BOOT_OPTIONS_EEPROM_LOCATION, 0); // Reset all boot options
+ EEPROM.end();
+ delay(10);
+ doReboot = true;
+ }
+
+ SyncMode randomSyncMode() {
+ uint8_t r = random8(128);
+
+ // For boring patterns, up the chance of a sync mode
+ if (isBoring)
+ r -= 20;
+
+ if (r < 30)
+ return SinDrift;
+ if (r < 50)
+ return Pulse;
+ if (r < 70)
+ return Swing;
+ if (r < 80)
+ return SwingDrift;
+ return All;
+ }
+
+ void updateGraphics() {
+ static BeatFrame_24_8 lastFrame = 0;
+ BeatFrame_24_8 beat_frame = current_state.beat_frame;
+
+ uint8_t beat_pulse = 0;
+ for (int i = 0; i < 8; i++) {
+ if ( (beat_frame >> (5+i)) != (lastFrame >> (5+i)))
+ beat_pulse |= 1<fade == Dead)
+ continue;
+
+ // Remember the first strip
+ if (first_strip == NULL)
+ first_strip = vstrip;
+
+ // Remember the strip that's actually WLED
+ if (vstrip->isWled())
+ wled_fader = vstrip->fader;
+
+ vstrip->update(beat_frame, beat_pulse);
+ }
+
+ effects.update(first_strip, beat_frame, (BeatPulse)beat_pulse);
+ }
+
+ CRGB getBlendedPixelColor(int32_t pos) const {
+ // Calculate the color of the pixel at position i by blending the colors of the virtual strips
+ CRGB color = CRGB::Black;
+
+ bool first_strip = true;
+ for (uint8_t i=0; i < NUM_VSTRIPS; i++) {
+ VirtualStrip *vstrip = vstrips[i];
+
+ // Don't bother blending a fully faded strip, or the WLED strip itself
+ if (vstrip->fade == Dead || vstrip->isWled())
+ continue;
+
+ auto br = vstrip->brightness;
+ // TODO: code intended to use scale8(options.brightness, vstrip->brightness);
+ // but that was never implemented - should review later to see if we want
+ // options.brightness to be a factor in the brightness of the strip
+
+ // Fetch the color from the strip and dim it according to the brightness
+ CRGB c = vstrip->getPixelColor(pos);
+ nscale8x3(c.r, c.g, c.b, br);
+ nscale8x3(c.r, c.g, c.b, vstrip->fader>>8);
+
+ if (first_strip) {
+ color = c;
+ first_strip = false;
+ } else {
+ color |= c;
+ }
+ }
+
+ return color;
+ }
+
+ virtual void acknowledge() {
+ addFlash(CRGB::Green);
+ }
+
+ void read_keys() {
+ if (!Serial.available())
+ return;
+
+ char c = Serial.read();
+ char *k = key_buffer;
+ uint8_t max = sizeof(key_buffer);
+ for (uint8_t i=0; *k && (i < max-1); i++) {
+ k++;
+ }
+ if (c == 10) {
+ keyboard_command(key_buffer);
+ key_buffer[0] = 0;
+ } else {
+ *k++ = c;
+ *k = 0;
+ }
+ }
+
+ accum88 parse_number(char *s) const {
+ uint16_t n=0, d=0;
+
+ while (*s == ' ')
+ s++;
+ while (*s) {
+ if (*s < '0' || *s > '9')
+ break;
+ n = n*10 + (*s++ - '0');
+ }
+ n = n << 8;
+
+ if (*s == '.') {
+ uint16_t div = 1;
+ s++;
+ while (*s) {
+ if (*s < '0' || *s > '9')
+ break;
+ d = d*10 + (*s++ - '0');
+ div *= 10;
+ }
+ d = (d << 8) / div;
+ }
+ return n+d;
+ }
+
+ void keyboard_command(char *command) {
+ // If not the lead, send it to the lead.
+ uint8_t b;
+ accum88 arg = parse_number(command+1);
+ Serial.printf("[command=%c arg=%04x]\n", command[0], arg);
+
+ switch (command[0]) {
+ case 'd':
+ setDebugging(!options.debugging);
+ break;
+ case '~':
+ doReboot = true;
+ break;
+ case '_':
+ togglePowerSave();
+ break;
+
+ case '-':
+ b = options.brightness;
+ while (*command++ == '-')
+ b -= 5;
+ setBrightness(b - 5);
+ break;
+ case '+':
+ b = options.brightness;
+ while (*command++ == '+')
+ b += 5;
+ setBrightness(b + 5);
+ return;
+ case 'l':
+ if (arg < 5*256) {
+ Serial.println(F("nope"));
+ return;
+ }
+ setBrightness(arg >> 8);
+ return;
+ case 'a':
+ Serial.println("Turning on WiFi access point.");
+ WLED::instance().initAP(true);
+ return;
+ case 'q':
+ Serial.println("Turning off WiFi access point.");
+ WiFi.disconnect(true);
+ return;
+ case 'b':
+ if (arg < 60*256) {
+ Serial.println(F("nope"));
+ return;
+ }
+ request_new_bpm(arg);
+ return;
+
+ case 's':
+ beats.start_phrase();
+ update_beat();
+ send_update();
+ return;
+
+ case 'n':
+ force_next();
+ return;
+
+ case 'p':
+ next_state.pattern_phrase = 0;
+ next_state.pattern_id = arg >> 8;
+ next_state.pattern_sync_id = All;
+ broadcast_state();
+ return;
+
+ case 'm':
+ next_state.pattern_phrase = 0;
+ next_state.pattern_id = current_state.pattern_id;
+ next_state.pattern_sync_id = arg >> 8;
+ broadcast_state();
+ return;
+
+ case 'c':
+ next_state.palette_phrase = 0;
+ next_state.palette_id = arg >> 8;
+ broadcast_state();
+ return;
+
+ case 'e':
+ next_state.effect_phrase = 0;
+ next_state.effect_params = gEffects[(arg >> 8) % gEffectCount].params;
+ broadcast_state();
+ return;
+
+ case '%':
+ next_state.effect_phrase = 0;
+ next_state.effect_params = current_state.effect_params;
+ next_state.effect_params.chance = arg;
+ broadcast_state();
+ return;
+
+ case 'i':
+ Serial.printf("Reset! ID -> %03X\n", arg >> 4);
+ node.reset(arg >> 4);
+ return;
+
+ case 'U':
+ case 'V':
+ case '*':
+ case '(':
+ case ')':
+ case '@':
+ case 'G':
+ case 'A':
+ case 'W':
+ case 'X':
+ case 'F':
+ case 'R':
+ case 'M': {
+ Action action = {
+ .key = command[0],
+ .arg = (uint8_t)(arg >> 8)
+ };
+ broadcast_action(action);
+ return;
+ }
+
+ case 'P': {
+ // Toggle power save
+ Action action = {
+ .key = command[0],
+ .arg = !power_save,
+ };
+ broadcast_action(action);
+ break;
+ }
+
+ case 'O': {
+ // Toggle sound overlay
+ Action action = {
+ .key = command[0],
+ .arg = !sound.overlay
+ };
+ broadcast_action(action);
+ break;
+ }
+
+ case 'r':
+ setRole((ControllerRole)(arg >> 8));
+ return;
+
+ case '?':
+ Serial.println(F("b###.# - set bpm"));
+ Serial.println(F("s - start phrase"));
+ Serial.println();
+ Serial.println(F("p### - patterns"));
+ Serial.println(F("m### - sync mode"));
+ Serial.println(F("c### - colors"));
+ Serial.println(F("e### - effects"));
+ Serial.println(F("n - force next"));
+ Serial.println();
+ Serial.println(F("i### - set ID"));
+ Serial.println(F("d - toggle debugging"));
+ Serial.println(F("l### - brightness"));
+ Serial.println("@ - toggle power saving mode");
+ Serial.println("U - begin auto-update");
+ Serial.println("P - toggle all power saves");
+ Serial.println("O - toggle all sound overlays");
+ Serial.println("==== wifi ====");
+ Serial.println("a - turn on access point");
+ Serial.println("q - turn off access point");
+ Serial.println("==== global actions ====");
+ Serial.println("* - enter select mode (double-click to Ready)");
+ Serial.println("A - turn on access point (Ready to update)");
+ Serial.println("W - forget WiFi client");
+ Serial.println("X - restart");
+ Serial.println("V### - auto-upgrade to version");
+ Serial.println("M - cancel manual pattern override");
+ return;
+
+ case 'u':
+ updater.start();
+ return;
+
+ case 0:
+ // Empty command
+ return;
+
+ default:
+ Serial.println("dunno?");
+ return;
+ }
+ }
+
+ void force_next() {
+ uint16_t phrase = current_state.beat_frame >> 12;
+ uint16_t next_phrase = min(next_state.pattern_phrase, min(next_state.palette_phrase, next_state.effect_phrase)) - phrase;
+ next_state.pattern_phrase -= next_phrase;
+ next_state.palette_phrase -= next_phrase;
+ next_state.effect_phrase -= next_phrase;
+ broadcast_state();
+ }
+
+ void broadcast_action(Action& action) {
+ if (!node.isFollowing()) {
+ onAction(&action);
+ }
+ node.sendCommand(COMMAND_ACTION, &action, sizeof(Action));
+ }
+
+ void broadcast_info(NodeInfo *info) {
+ node.sendCommand(COMMAND_INFO, &info, sizeof(NodeInfo));
+ }
+
+ void broadcast_state() {
+ node.sendCommand(COMMAND_STATE, ¤t_state, sizeof(TubeStates));
+ }
+
+ void broadcast_options() {
+ node.sendCommand(COMMAND_OPTIONS, &options, sizeof(options));
+ }
+
+ void broadcast_autoupdate() {
+ node.sendCommand(COMMAND_UPGRADE, &updater.current_version, sizeof(updater.current_version));
+ }
+
+ void broadcast_bpm(accum88 bpm) {
+ // Hacked in feature: request a new BPM
+ node.sendCommand(COMMAND_BEATS, &bpm, sizeof(bpm));
+ }
+
+ virtual bool onCommand(CommandId command, void *data) override {
+ switch (command) {
+ case COMMAND_INFO:
+ Serial.printf(" \"%s\"\n",
+ ((NodeInfo*)data)->message
+ );
+ return true;
+
+ case COMMAND_OPTIONS:
+ memcpy(&options, data, sizeof(options));
+ load_options(options);
+ Serial.printf("[debug=%d bri=%d]",
+ options.debugging,
+ options.brightness
+ );
+ return true;
+
+ case COMMAND_STATE: {
+ auto update_data = (TubeStates*)data;
+
+ TubeState state;
+ memcpy(&state, &update_data->current, sizeof(TubeState));
+ memcpy(&next_state, &update_data->next, sizeof(TubeState));
+ state.print();
+ next_state.print();
+
+ // Catch up to this state
+ load_pattern(state);
+ load_palette(state);
+ load_effect(state);
+ beats.sync(state.bpm, state.beat_frame);
+ return true;
+ }
+
+ case COMMAND_UPGRADE:
+ updater.start((AutoUpdateOffer*)data);
+ return true;
+
+ case COMMAND_ACTION:
+ onAction((Action*)data);
+ return true;
+
+ case COMMAND_BEATS:
+ // the master control ignores this request, it has its own
+ // beat measuring.
+ if (isMasterRole())
+ return false;
+ set_tapped_bpm(*(accum88*)data, 0);
+ return true;
+ }
+
+ Serial.printf("UNKNOWN COMMAND %02X", command);
+ return false;
+ }
+
+ void onAction(Action* action) {
+ switch (action->key) {
+ case 'A':
+ Serial.println("Turning on WiFi access point.");
+ WLED::instance().initAP(true);
+ return;
+
+ case 'O':
+ sound.overlay = (action->arg != 0);
+ return;
+
+ case 'X':
+ if (!isSelected())
+ return;
+ doReboot = true;
+ return;
+
+ case 'R':
+ if (!isSelected())
+ return;
+ setRole((ControllerRole)(action->arg));
+ return;
+
+ case '@':
+ Serial.print("Setting power save to %d\n");
+ setPowerSave(action->arg);
+ return;
+
+ case 'W':
+ Serial.println("Clearing WiFi connection.");
+ strcpy(clientSSID, "");
+ strcpy(clientPass, "");
+ WiFi.disconnect(false, true);
+ return;
+
+ case 'G':
+ Serial.println("glitter!");
+ for (int i=0; i< 10; i++)
+ addGlitter();
+ return;
+
+ case 'F':
+ Serial.println("flash!");
+ flashTimer.start(20000);
+ flashColor = action->arg;
+ return;
+
+ case 'M':
+ Serial.println("cancel manual mode");
+ cancelOverrides();
+ break;
+
+ case '*':
+ case '(':
+ Serial.println("enter select mode");
+ enterSelectMode();
+ break;
+
+ case ')':
+ Serial.println("exit select mode");
+ deselect();
+ break;
+
+ case 'V':
+ // Version check: prepare for update
+ if (updater.current_version.version >= action->arg)
+ break;
+
+ select();
+ break;
+
+ case 'U':
+ if (!isSelected())
+ return;
+ updater.start();
+ break;
+
+ }
+ }
+
+#define WIZMOTE_BUTTON_ON 1
+#define WIZMOTE_BUTTON_OFF 2
+#define WIZMOTE_BUTTON_NIGHT 3
+#define WIZMOTE_BUTTON_ONE 16
+#define WIZMOTE_BUTTON_TWO 17
+#define WIZMOTE_BUTTON_THREE 18
+#define WIZMOTE_BUTTON_FOUR 19
+#define WIZMOTE_BUTTON_BRIGHT_UP 9
+#define WIZMOTE_BUTTON_BRIGHT_DOWN 8
+
+ void force_next_pattern() {
+ next_state.pattern_phrase = current_state.beat_frame >> 12;
+ if (next_state.palette_phrase == next_state.pattern_phrase)
+ next_state.palette_phrase += random8(0, 5);
+ force_next();
+ }
+
+ void force_next_effect() {
+ next_state.effect_phrase = current_state.beat_frame >> 12;
+ force_next();
+ }
+
+ virtual bool onButton(uint8_t button_id) override {
+ bool isMaster = !this->node.isFollowing();
+
+ switch (button_id) {
+ case WIZMOTE_BUTTON_ON:
+ WLED::instance().initAP(true);
+ setDebugging(true);
+ acknowledge();
+ return true;
+
+ case WIZMOTE_BUTTON_OFF:
+ WiFi.softAPdisconnect(true);
+ apActive = false;
+ WiFi.disconnect(false, true);
+ WLED::instance().enableWatchdog();
+ apBehavior = AP_BEHAVIOR_BUTTON_ONLY;
+ setDebugging(false);
+ acknowledge();
+ return true;
+
+ case WIZMOTE_BUTTON_ONE:
+ // Make it interesting - switch to a good pattern and sync mode
+ // Only the master will respond to this
+ if (!isMaster)
+ return false;
+
+ Serial.println("WizMote preset 1: de-sync");
+
+ set_next_pattern(0);
+ while (next_state.pattern_sync_id == All)
+ set_next_pattern(0);
+
+ this->force_next_pattern();
+ return true;
+
+ case WIZMOTE_BUTTON_TWO:
+ // Apply an interesting effect & sync layer
+ // Only the master will respond to this
+ if (!isMaster)
+ return false;
+
+ Serial.println("WizMote preset 2: add an effect");
+
+ set_next_effect(0);
+ while (next_state.effect_params.effect == None)
+ set_next_effect(0);
+
+ this->force_next_effect();
+ return true;
+
+ case WIZMOTE_BUTTON_THREE:
+ // Turn on flames. Also up the tempo to 125
+ // Only the master will respond to this
+ if (!isMaster)
+ return false;
+
+ // Switch to house mode
+ set_tapped_bpm(125<<8);
+
+ Serial.println("WizMote preset 3: flames!");
+ next_state.pattern_id = 63; // Fire
+ next_state.pattern_sync_id = SyncMode::All;
+ this->force_next_pattern();
+ return true;
+
+ case WIZMOTE_BUTTON_FOUR:
+ // Make it an interesting combo
+ // Only the master will respond to this
+ if (!isMaster)
+ return false;
+
+ // 38: Noise 3
+ Serial.println("WizMote preset 4: interesting pattern");
+
+ set_next_pattern(0);
+ next_state.pattern_id = 38; // overwrite with: Noise 3
+
+ this->force_next_pattern();
+ return true;
+
+ case WIZMOTE_BUTTON_BRIGHT_UP:
+ // Brighten (ignored if in power save mode)
+ Serial.println("WizMote: brightness up");
+ if (options.brightness <= 230)
+ setBrightness(options.brightness + 25);
+ return true;
+
+ case WIZMOTE_BUTTON_BRIGHT_DOWN:
+ // Dim (ignored if in power save mode)
+ Serial.println("WizMote: brightness down");
+
+ if (options.brightness >= 25)
+ setBrightness(options.brightness - 25);
+ return true;
+
+ case WIZMOTE_BUTTON_NIGHT:
+ // Chill mode
+ // Only the master will respond to this
+ if (!isMaster)
+ return false;
+
+ Serial.println("WizMote: chill");
+
+ // Switch to deep house mode
+ set_tapped_bpm(120<<8);
+
+ this->force_next();
+ return true;
+
+ default:
+ Serial.printf("Button %d master=%d\n", button_id, isMaster);
+ return false;
+ }
+ }
+
+
+};
diff --git a/usermods/Tubes/debug.h b/usermods/Tubes/debug.h
new file mode 100644
index 0000000000..b4af3920f3
--- /dev/null
+++ b/usermods/Tubes/debug.h
@@ -0,0 +1,144 @@
+#pragma once
+
+#include "controller.h"
+#include "node.h"
+#include "wled.h"
+
+std::string formatted_time(long ms) {
+ long secs = ms / 1000; // set the seconds remaining
+ long mins = secs / 60; //convert seconds to minutes
+ long hours = mins / 60; //convert minutes to hours
+ long days = hours / 24; //convert hours to days
+
+ secs = secs % 60;
+ mins = mins % 60;
+ hours = hours % 24;
+
+ char buffer[100];
+ if (days > 0)
+ sprintf(buffer, "%ld %02ld:%02ld:%02ld", days, hours, mins, secs);
+ else
+ sprintf(buffer, "%02ld:%02ld:%02ld", hours, mins, secs);
+ return std::string(buffer);
+}
+
+class DebugController {
+ public:
+ const PatternController& controller;
+ uint32_t lastPhraseTime;
+ uint32_t lastFrame;
+
+ DebugController(const PatternController& c) : controller(c) {}
+
+ void setup()
+ {
+ lastPhraseTime = globalTimer.now_micros;
+ lastFrame = (uint32_t)-1;
+ }
+
+ void update()
+ {
+ EVERY_N_MILLISECONDS( 10000 ) {
+ // Dump internal status
+ auto knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID();
+ auto knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();
+ Serial.printf("\n=== %s%s WiFi[ch%d] %s IP: %u.%u.%u.%u Free memory: %d space: %u/%u Uptime: %s\n",
+ controller.node.node_name,
+ controller.node.status_code(),
+ WiFi.channel(),
+ knownSsid.c_str(),
+ knownIp[0],
+ knownIp[1],
+ knownIp[2],
+ knownIp[3],
+ freeMemory(),
+ WLED_FS.usedBytes(),
+ WLED_FS.totalBytes(),
+ formatted_time(millis()).c_str()
+ );
+
+ Serial.printf("=== Controller: ");
+ if (controller.isMasterRole()) {
+ Serial.print("PRIMARY ");
+ }
+ if (controller.sound.active) {
+ Serial.print("SOUND ");
+ }
+ if (controller.node.isLeading()) {
+ Serial.print("LEADING ");
+ }
+ if (controller.node.isFollowing()) {
+ Serial.print("FOLLLOWING ");
+ }
+ Serial.printf("role=%d power_save=%d\n",
+ controller.role,
+ controller.power_save
+ );
+
+ // Dump WLED status
+ char mode_name[50];
+ char palette_name[50];
+ auto seg = strip.getMainSegment();
+ extractModeName(seg.mode, JSON_mode_names, mode_name, 50);
+ extractModeName(seg.palette, JSON_palette_names, palette_name, 50);
+ Serial.printf("=== WLED: %d LEDs, %s(%u) %s(%u) speed:%u intensity:%u",
+ strip.getLengthTotal(),
+ mode_name,
+ seg.mode,
+ palette_name,
+ seg.palette,
+ seg.speed,
+ seg.intensity
+ );
+ if (controller.patternOverride) {
+ Serial.printf(" (PATTERN %d)", controller.patternOverride);
+ } else {
+ Serial.printf(" at %d", controller.wled_fader);
+ }
+ if (controller.paletteOverride) {
+ Serial.printf(" (PALETTE %d)", controller.paletteOverride);
+ }
+ Serial.println();
+
+ Serial.printf("=== firmware: v%d "
+#ifdef TUBES_AUTOUPDATER
+ "from SSID %s %u.%u.%u.%u "
+#endif
+ "OTA=%d\n\n",
+ controller.updater.current_version.version,
+#ifdef TUBES_AUTOUPDATER
+ controller.updater.current_version.ssid,
+ controller.updater.current_version.host[0],
+ controller.updater.current_version.ssid[1],
+ controller.updater.current_version.ssid[2],
+ controller.updater.current_version.ssid[3],
+#endif
+ controller.updater.status
+ );
+
+ }
+ }
+
+ void handleOverlayDraw()
+ {
+ // Show the beat on the master OR if debugging
+ if (controller.options.debugging) {
+ uint16_t num_leds = strip.getLengthTotal();
+
+ uint8_t p1 = (controller.current_state.beat_frame >> 8) % 16;
+ strip.setPixelColor(p1, CRGB::White);
+
+ uint8_t p2 = scale8(controller.node.header.id>>4, num_leds-1);
+ strip.setPixelColor(p2, CRGB::Yellow);
+
+ uint8_t p3 = scale8(controller.node.header.uplinkId>>4, num_leds-1);
+ if (p3 == p2) {
+ strip.setPixelColor(p3, CRGB::Green);
+ } else {
+ strip.setPixelColor(p3, CRGB::Blue);
+ }
+ }
+
+ }
+};
+
diff --git a/usermods/Tubes/default_config.json b/usermods/Tubes/default_config.json
new file mode 100644
index 0000000000..d32eb86079
--- /dev/null
+++ b/usermods/Tubes/default_config.json
@@ -0,0 +1 @@
+{"rev":[1,0],"vid":2308110,"id":{"mdns":"wled-bfdc58","name":"Light Tube","inv":"Light"},"nw":{"ins":[{"ssid":"","pskl":0,"ip":[0,0,0,0],"gw":[0,0,0,0],"sn":[255,255,255,0]}]},"ap":{"ssid":"WLED-AP","pskl":8,"chan":1,"hide":0,"behav":3,"ip":[4,3,2,1]},"wifi":{"sleep":false},"hw":{"led":{"total":112,"maxpwr":700,"ledma":55,"cct":true,"cr":false,"cb":0,"fps":60,"rgbwm":3,"ld":false,"ins":[{"start":0,"len":112,"pin":[16],"order":0,"rev":false,"skip":0,"type":22,"ref":false,"rgbwm":0,"freq":0}]},"com":[],"btn":{"max":4,"pull":true,"ins":[{"type":2,"pin":[0],"macros":[0,0,0]},{"type":0,"pin":[-1],"macros":[0,0,0]},{"type":0,"pin":[-1],"macros":[0,0,0]},{"type":0,"pin":[-1],"macros":[0,0,0]}],"tt":32,"mqtt":false},"ir":{"pin":5,"type":0,"sel":true},"relay":{"pin":12,"rev":false},"baud":1152,"if":{"i2c-pin":[-1,-1],"spi-pin":[-1,-1,-1]}},"light":{"scale-bri":100,"pal-mode":0,"aseg":false,"gc":{"bri":1,"col":2.8,"val":2.8},"tr":{"mode":true,"dur":80,"pal":1,"rpc":5},"nl":{"mode":1,"dur":60,"tbri":0,"macro":0}},"def":{"ps":0,"on":true,"bri":128},"if":{"sync":{"port0":21324,"port1":65506,"recv":{"bri":true,"col":true,"fx":true,"grp":1,"seg":false,"sb":false},"send":{"dir":false,"btn":false,"va":false,"hue":false,"macro":false,"grp":1,"ret":0}},"nodes":{"list":false,"bcast":false},"live":{"en":true,"mso":false,"port":5568,"mc":false,"dmx":{"uni":1,"seqskip":false,"e131prio":0,"addr":1,"dss":0,"mode":4},"timeout":25,"maxbri":false,"no-gc":true,"offset":0},"va":{"alexa":false,"macros":[0,0],"p":0},"ntp":{"en":false,"host":"0.wled.pool.ntp.org","tz":0,"offset":0,"ampm":false,"ln":0,"lt":0}},"remote":{"remote_enabled":false,"linked_remote":""},"ol":{"clock":0,"cntdwn":false,"min":0,"max":43,"o12pix":0,"o5m":false,"osec":false},"timers":{"cntdwn":{"goal":[20,1,1,0,0,0],"macro":0},"ins":[]},"ota":{"lock":false,"lock-wifi":false,"pskl":7,"aota":true},"um":{"AudioReactive":{"enabled":true,"analogmic":{"pin":36},"digitalmic":{"type":1,"pin":[19,4,18,-1]},"config":{"squelch":10,"gain":60,"AGC":1},"dynamics":{"limiter":true,"rise":80,"fall":1400},"frequency":{"scale":3},"sync":{"port":11988,"mode":0}}}}
\ No newline at end of file
diff --git a/usermods/Tubes/effects.h b/usermods/Tubes/effects.h
new file mode 100644
index 0000000000..a23254850b
--- /dev/null
+++ b/usermods/Tubes/effects.h
@@ -0,0 +1,157 @@
+#pragma once
+
+#include "util.h"
+#include "particle.h"
+#include "virtual_strip.h"
+
+void addGlitter(CRGB color=CRGB::White, PenMode pen=Draw)
+{
+ addParticle(Particle(random16(), color, pen, 128));
+}
+
+void addSpark(CRGB color=CRGB::White, PenMode pen=Draw)
+{
+ Particle particle {random16(), color, pen, 64};
+ uint8_t r = random8();
+ if (r > 128)
+ particle.velocity = r;
+ else
+ particle.velocity = -(128 + r);
+ addParticle(std::move(particle));
+}
+
+void addBeatbox(CRGB color=CRGB::White, PenMode pen=Draw)
+{
+ Particle particle {random16(), color, pen, 256, drawBeatbox};
+ addParticle(std::move(particle));
+}
+
+void addBubble(CRGB color=CRGB::White, PenMode pen=Draw)
+{
+ Particle particle {random16(), color, pen, 1024, drawPop};
+ particle.velocity = random16(0, 40) - 20;
+ addParticle(std::move(particle));
+}
+
+void addFlash(CRGB color=CRGB::Blue, PenMode pen=Draw)
+{
+ addParticle(Particle(random16(), color, pen, 256, drawFlash));
+}
+
+void addDrop(CRGB color, PenMode pen=Draw)
+{
+ Particle particle {65535, color, pen, 360};
+ particle.velocity = -500;
+ particle.gravity = -10;
+ addParticle(std::move(particle));
+}
+
+class Effects {
+ public:
+ EffectMode effect=None;
+ PenMode pen=Draw;
+ BeatPulse beat;
+ uint8_t chance;
+
+ void load(EffectParameters ¶ms) {
+ effect = params.effect;
+ pen = params.pen;
+ beat = params.beat;
+ chance = params.chance;
+ }
+
+ void update(VirtualStrip *strip, BeatFrame_24_8 beat_frame, BeatPulse beat_pulse) {
+ if (!beat || beat_pulse & beat) {
+
+ if (random8() <= chance) {
+ CRGB color = strip->palette_color(random8());
+
+ switch (effect) {
+ case None:
+ break;
+
+ case Glitter:
+ addGlitter(color, pen);
+ break;
+
+ case Beatbox1:
+ case Beatbox2:
+ addBeatbox(color, pen);
+ if (effect == Beatbox2)
+ addBeatbox(color, pen);
+ break;
+
+ case Bubble:
+ addBubble(color, pen);
+ break;
+
+ case Spark:
+ addSpark(color, pen);
+ break;
+
+ case Flash:
+ addFlash(CRGB::White, pen);
+ break;
+ }
+ }
+ }
+
+ animate(beat_frame, beat_pulse);
+ }
+
+ void animate(BeatFrame_24_8 frame, uint8_t beat_pulse) {
+ unsigned int len = numParticles;
+ for (unsigned i=len; i > 0; --i) {
+ Particle& particle = particles[i];
+ particle.update(frame);
+ if (particle.age > particle.lifetime) {
+ removeParticle(i);
+ }
+ }
+ }
+
+ void draw(WS2812FX* leds) {
+ uint8_t len = numParticles;
+ for (uint8_t i=0; i/dev/null
+}
+
+update_firmware() {
+ echo "Getting info"
+ json=$( curl -s http://$1/json/si )
+
+ arch=$(echo "$json" | jq -r '.["info"].arch')
+ name=$(echo "$json" | jq -r '.["info"].name')
+ echo "arch: $arch name: $name"
+
+ firmware=
+
+ if [ ! -z "$name" ]; then
+ if [ "dig2go" == "$name" ]; then
+ firmware="esp32_quinled_dig2go_tubes.bin"
+ fi
+ fi
+
+ if [ -z "$firmware" ] && [ ! -z "$arch" ]; then
+ if [ "ESP32-C3" == "$arch" ]; then
+ firmware="esp32-c3-athom_tubes.bin"
+ elif [ "esp32" == "$arch" ]; then
+ firmware="esp32_quinled_dig2go_tubes.bin"
+ fi
+ fi
+
+ if [ -z "$firmware" ]; then
+ echo "firmware not set - not updating OTA"
+ curl -s http://$1/reset -H "Connection: close" >/dev/null
+ else
+ echo "Updating $firmware firmware via OTA"
+ curl -s -F "update=@../../build_output/firmware/$firmware" -H "Connection: close" --no-keepalive $1/update >/dev/null
+ echo "Updated; wait..."
+ sleep 5
+ update_config $1
+ fi
+}
+
+connect() {
+ if ! networksetup -getairportnetwork en0 | grep "$1"
+ then
+ echo "Connecting to $1"
+ networksetup -setairportnetwork en0 "$1" "$2"
+ echo "Connected; wait..."
+ sleep 5
+ fi
+}
+
+update_one() {
+ connect "$2" "$3"
+
+ ping -c 1 -t 2 $1 >/dev/null
+ PINGRESULT=$?
+ if [ $PINGRESULT -eq 0 ]; then
+ update_firmware $1
+ else
+ echo Missing $1
+ fi
+ return 1
+}
+
+update_batch() {
+ airport -s | grep WLED | cut -c10-32 | while read line
+ do
+ if [ "$line" == "WLED-AP" ]; then
+ update_one 4.3.2.1 "$line" "wled1234"
+ elif [ "$line" == "WLED-UPDATE" ]; then
+ update_one 4.3.2.1 "$line" "update1234"
+ else
+ update_one 4.3.2.1 "$line" "WledWled"
+ fi
+ done
+}
+
+process() {
+ if [ "$1" == "upload" ]; then
+ upload_firmware
+ elif [ "$1" == "batch" ]; then
+ update_batch
+ else
+ while :
+ do
+ update_one 4.3.2.1 "WLED-AP" "wled1234"
+ done
+ fi
+}
+
+# WiFi is spotty. Let's do it a couple times :)
+process "$@"
+process "$@"
+process "$@"
+process "$@"
+process "$@"
+process "$@"
+process "$@"
\ No newline at end of file
diff --git a/usermods/Tubes/firmware_test.sh b/usermods/Tubes/firmware_test.sh
new file mode 100644
index 0000000000..ed284f8dde
--- /dev/null
+++ b/usermods/Tubes/firmware_test.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+# Updates new boards (which start as broadcasting on WLED-AP) to custom firmware
+# Will update as many boards as are plugged in, one at a time.
+
+
+# Two ways to use it:
+# 1) ./firmware.sh
+# This will update all boards with open APs named WLED-AP
+# 2) ./firmware.sh batch
+# This will update all boards with open APs named WLED-AP or WLED-UPDATE
+# This is useful for updating a batch of boards at once
+# To put a board in this mode, send it a V## command with a version higher than the current one
+# There's also:
+# 3) ./firmware.sh upload
+# This will upload the firmware to the internet server, but not update any boards
+# Internet upload is not working 100% yet, so this is not recommended
+
+
+WLEDPATH=../../build_output/firmware
+ESPPATH=~/.platformio/packages/framework-arduinoespressif32/tools
+
+update_config() {
+ # No longer update configs? comment this
+ # return;
+
+ echo "Updating configuration via OTA"
+# curl -s http://$1/upload -F "data=@default_config.json;filename=/cfg.json" -H "Connection: close" --no-keepalive
+ echo "Configured; wait..."
+# curl -s http://$1/reset -H "Connection: close" >/dev/null
+}
+
+
+update_firmware() {
+
+ echo "Getting info"
+ json=$( curl -s http://$1/json/si )
+
+ arch=$(echo "$json" | jq -r '.["info"].arch')
+ name=$(echo "$json" | jq -r '.["info"].name')
+ echo "$arch $name"
+
+ firmware=
+
+ if [ ! -z "$name" ]; then
+ if [ "dig2go" == "$name" ]; then
+ firmware="esp32_quinled_dig2go_tubes.bin"
+ fi
+ fi
+
+ if [ -z "$firmware" ] && [ ! -z "$arch" ]; then
+ if [ "ESP32-C3" == "$arch" ]; then
+ firmware="esp32-c3-athom_tubes.bin"
+ elif [ "esp32" == "$arch" ]; then
+ firmware="esp32_quinled_dig2go_tubes.bin"
+ fi
+ fi
+
+ if [ -z "$firmware" ]; then
+ echo "firmware not set - not updating OTA"
+ curl -s http://$1/reset -H "Connection: close" >/dev/null
+ else
+ echo "Updating $firmware firmware via OTA"
+ curl -s -F "update=@../../build_output/firmware/$firmware" -H "Connection: close" --no-keepalive $1/update >/dev/null
+ curl -s http://$1/reset -H "Connection: close" >/dev/null
+ fi
+}
+
+update_firmware $1
diff --git a/usermods/Tubes/global_state.h b/usermods/Tubes/global_state.h
new file mode 100644
index 0000000000..5042d41d62
--- /dev/null
+++ b/usermods/Tubes/global_state.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "wled.h"
+#include "beats.h"
+#include "effects.h"
+
+
+class TubeState {
+ public:
+ // Global clock: frames are defined as 1/64th of a beat
+ accum88 bpm = 0; // BPM in high 8 bits, fraction in low 8 bits
+ BeatFrame_24_8 beat_frame = 0; // current beat (24 bits) and fractional beat (8bits)
+
+ uint16_t pattern_phrase;
+ uint8_t pattern_id;
+ uint8_t pattern_sync_id;
+
+ uint16_t palette_phrase;
+ uint8_t palette_id;
+
+ uint16_t effect_phrase;
+ EffectParameters effect_params;
+
+ void print() {
+ uint16_t phrase = beat_frame >> 12;
+ uint8_t frac = scale8(100, bpm & 0xFF);
+ char buf[128];
+ sprintf(buf, "[%d.%d P%d,%d C%d E%d,%d,%d,%d %d.%02dbpm]",
+ phrase,
+ (beat_frame >> 8) % 16,
+ pattern_id,
+ pattern_sync_id,
+ palette_id,
+ effect_params.effect,
+ effect_params.pen,
+ effect_params.beat,
+ effect_params.chance,
+ bpm >> 8,
+ frac
+ );
+ Serial.print(buf);
+ }
+
+};
+
+typedef uint8_t CommandId;
+
+const static CommandId COMMAND_OPTIONS = 0x10;
+const static CommandId COMMAND_STATE = 0x20;
+const static CommandId COMMAND_ACTION = 0x30;
+const static CommandId COMMAND_INFO = 0x40;
+const static CommandId COMMAND_BEATS = 0x50;
+const static CommandId COMMAND_UPGRADE = 0xE0;
diff --git a/usermods/Tubes/led_strip.h b/usermods/Tubes/led_strip.h
new file mode 100644
index 0000000000..55c58591f4
--- /dev/null
+++ b/usermods/Tubes/led_strip.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#define USE_WLED
+#include "wled.h"
+
+
+class LEDs {
+ public:
+ const static int TARGET_FRAMES_PER_SECOND = 150;
+ const static int TARGET_REFRESH = 1000 / TARGET_FRAMES_PER_SECOND;
+ uint16_t fps = 0;
+
+ LEDs() { };
+
+ void setup() {
+ Serial.println((char *)F("LEDs: ok"));
+ }
+
+ void update() {
+ EVERY_N_MILLISECONDS( TARGET_REFRESH ) {
+ fps++;
+ }
+ EVERY_N_MILLISECONDS( 1000 ) {
+ if (fps < (TARGET_FRAMES_PER_SECOND - 30)) {
+ Serial.print(fps);
+ Serial.println((char *)F(" fps!"));
+ }
+ fps = 0;
+ }
+ }
+};
diff --git a/usermods/Tubes/master.h b/usermods/Tubes/master.h
new file mode 100644
index 0000000000..4efcb33a6e
--- /dev/null
+++ b/usermods/Tubes/master.h
@@ -0,0 +1,191 @@
+#pragma once
+
+#include "timer.h"
+#include "controller.h"
+#include "led_strip.h"
+
+#define X_AXIS_PIN 20
+#define Y_AXIS_PIN 21
+
+#define BUTTON_PIN_1 70 // SELECT?
+#define BUTTON_PIN_2 71 // SKIP
+#define BUTTON_PIN_3 72 // SET COLOR
+#define BUTTON_PIN_4 23 // TAP
+#define BUTTON_PIN_5 25 // "NEXT!"
+
+
+class Master {
+ public:
+ uint8_t taps=0;
+ Timer tapTimer;
+ Timer perTapTimer;
+ uint16_t tapTime[16];
+
+ Background background;
+ uint8_t palette_mode = false;
+ uint8_t palette_id = 0;
+
+ PatternController& controller;
+ Button button[5];
+
+ Master(PatternController& c) : controller(c) { };
+
+ void setup() {
+ button[0].setup(BUTTON_PIN_1);
+ button[1].setup(BUTTON_PIN_2);
+ button[2].setup(BUTTON_PIN_3);
+ button[3].setup(BUTTON_PIN_4);
+ button[4].setup(BUTTON_PIN_5);
+ Serial.println((char *)F("Master: ok"));
+ }
+
+ void update() {
+ for (uint8_t i=0; i < 5; i++) {
+ if (button[i].triggered()) {
+ if (button[i].pressed())
+ onButtonPress(i);
+ else
+ onButtonRelease(i);
+ }
+ }
+
+ if (taps && perTapTimer.ended()) {
+ if (taps == 2) {
+ ok();
+ } else {
+ fail();
+ }
+ taps = 0;
+ }
+ }
+
+ void handleOverlayDraw() {
+ updateStatus(controller);
+ }
+
+ void ok() {
+ addFlash(CRGB::Green);
+ }
+
+ void fail() {
+ addFlash(CRGB::Red);
+ }
+
+ void onButtonPress(uint8_t b) {
+ if (b == 0)
+ return;
+
+ if (b == 4) {
+ Serial.println((char *)F("Skip >>"));
+ controller.force_next();
+ ok();
+ return;
+ }
+
+ if (b == 3) {
+ tap();
+ return;
+ }
+
+ Serial.print((char *)F("Pressed "));
+ Serial.println(b);
+ }
+
+ void onButtonRelease(uint8_t b) {
+#ifdef EXTRA_STUFF
+ if (b == 2) {
+ if (palette_mode)
+ controller._load_palette(palette_id);
+ palette_mode = false;
+ }
+#endif
+
+ if (b == 3) {
+ if (taps == 0)
+ return;
+ tap();
+ return;
+ }
+
+ Serial.print((char *)F("Released "));
+ Serial.println(b);
+ }
+
+ void tap() {
+ if (!taps) {
+ tapTimer.start(0);
+ }
+ perTapTimer.start(1500);
+
+ uint32_t time = tapTimer.since_mark();
+ tapTime[taps++] = time;
+
+ uint32_t bpm = 0;
+ if (taps > 4) {
+ // Can study this later to make BPM detection better
+
+ // Should be 60000; fudge a bit to adjust to real-world timings
+ bpm = 60220*256*(taps-1) / time; // 120 beats per min = 500ms per beat
+ if (bpm < 70*256)
+ bpm *= 2;
+ else if (bpm > 140*256)
+ bpm /= 2;
+ }
+
+ Serial.printf("tap %d: ", taps);
+ Serial.print(bpm >> 8);
+ uint8_t f = scale8(100, bpm & 0xFF);
+ Serial.print(".");
+ if (f < 10)
+ Serial.print("0");
+ Serial.println(f);
+
+ if (taps == 16) {
+ Serial.println("OK! taps");
+ taps = 0;
+ auto frac = bpm % 256;
+
+ // Slight snap to beat
+ if (frac < 128)
+ bpm -= frac / 2;
+ else if (frac > 128)
+ bpm += (256-frac) / 2;
+
+ controller.set_tapped_bpm(bpm);
+ ok();
+ } else if (taps >= 2) {
+ controller.set_tapped_bpm(controller.current_state.bpm, taps-1);
+ }
+ }
+
+ void updateStatus(const PatternController& controller) {
+ if (taps) {
+ displayProgress(taps);
+ } else if (palette_mode) {
+ displayPalette(background);
+ } else {
+ uint8_t beat_pos = (controller.current_state.beat_frame >> 8) % 16;
+ strip.setPixelColor(15 - beat_pos, CRGB::White);
+ }
+ }
+
+ void displayProgress(uint8_t progress) {
+ for (int i = 0; i < 16; i++) {
+ if (i < progress % 16) {
+ strip.setPixelColor(15 - i, CRGB(128,128,128));
+ } else {
+ strip.setPixelColor(15 - i, CRGB::Black);
+ }
+ }
+ }
+
+ void displayPalette(Background &background) {
+ for (int i = 0; i < 16; i++) {
+ Segment& segment = strip.getMainSegment();
+ auto color = CRGB(segment.color_from_palette(i * 16, false, true, 255));
+ strip.setPixelColor(i, color);
+ }
+ }
+
+};
+
diff --git a/usermods/Tubes/node.h b/usermods/Tubes/node.h
new file mode 100644
index 0000000000..690b75ec69
--- /dev/null
+++ b/usermods/Tubes/node.h
@@ -0,0 +1,482 @@
+#pragma once
+
+#include
+#include "global_state.h"
+#include "espnow_broadcast.h"
+
+// #define NODE_DEBUGGING
+// #define RELAY_DEBUGGING
+#define TESTING_NODE_ID 0
+
+#define CURRENT_NODE_VERSION 2
+
+#pragma pack(push,4) // set packing for consist transport across network
+// ideally this would have been pack 1, so we're actually wasting a
+// number of bytes across the network, but we've already shipped...
+
+typedef enum{
+ RECIPIENTS_ALL=0, // Send to all neighbors; non-followers will ignore
+ RECIPIENTS_ROOT=1, // Send to root for rebroadcasting downward, all will see
+ RECIPIENTS_INFO=2, // Send to all neighbors "FYI"; none will ignore
+} MessageRecipients;
+
+typedef uint16_t MeshId;
+
+typedef struct {
+ MeshId id = 0;
+ MeshId uplinkId = 0;
+ uint8_t version = CURRENT_NODE_VERSION;
+} MeshNodeHeader;
+
+#define MESSAGE_DATA_SIZE 64
+typedef struct {
+ MeshNodeHeader header;
+ MessageRecipients recipients;
+ uint32_t timebase;
+ CommandId command;
+ byte data[MESSAGE_DATA_SIZE] = {0};
+} NodeMessage;
+
+#pragma pack(pop)
+
+typedef struct {
+ uint8_t status;
+ char message[40];
+} NodeInfo;
+
+
+const char *command_name(CommandId command) {
+ switch (command) {
+ case COMMAND_STATE:
+ return "UPDATE";
+ case COMMAND_OPTIONS:
+ return "OPTIONS";
+ case COMMAND_ACTION:
+ return "ACTION";
+ case COMMAND_INFO:
+ return "INFO";
+ case COMMAND_BEATS:
+ return "BEATS";
+ default:
+ return "?COMMAND?";
+ }
+}
+
+class MessageReceiver {
+ public:
+ virtual bool onCommand(CommandId command, void *data) = 0;
+ virtual bool onButton(uint8_t button_id) = 0;
+};
+
+class LightNode {
+ public:
+ static LightNode* instance;
+
+ MessageReceiver *receiver;
+ MeshNodeHeader header;
+
+ typedef enum{
+ NODE_STATUS_QUIET=0,
+ NODE_STATUS_RECEIVING,
+ NODE_STATUS_STARTED,
+ NODE_STATUS_MAX,
+ } NodeStatus;
+ NodeStatus status = NODE_STATUS_QUIET;
+
+ PGM_P status_code() const {
+ switch (status) {
+ case NODE_STATUS_QUIET:
+ return PSTR(" (quiet)");
+ case NODE_STATUS_RECEIVING:
+ return PSTR(" (receiving)");
+ case NODE_STATUS_STARTED:
+ return PSTR(" (started)");
+ default:
+ return PSTR("??");
+ }
+ }
+
+ char node_name[20];
+
+ LightNode(MessageReceiver *r) : receiver(r) {
+ instance = this;
+ }
+
+ protected:
+
+ const uint32_t STATUS_TIMEOUT_BASE = 3000; // Base time to wait to send broadcasts
+ const uint32_t UPLINK_TIMEOUT = 20000; // Time at which uplink is presumed lost
+ const uint32_t REBROADCAST_TIME = 30000; // Time at which followers are presumed re-uplinked
+
+ Timer statusTimer; // Use this timer to initialize and check wifi status
+ Timer uplinkTimer; // When this timer ends, assume uplink is lost.
+ Timer rebroadcastTimer; // Until this timer ends, re-broadcast messages from uplink
+
+ void onMeshChange() {
+ sprintf(node_name,
+ "Tube %03X:%03X",
+ header.id,
+ header.uplinkId
+ );
+
+ configureAP();
+ }
+
+ void configureAP() {
+#ifdef DEFAULT_WIFI
+ strcpy(clientSSID, DEFAULT_WIFI);
+ strcpy(clientPass, DEFAULT_WIFI_PASSWORD);
+#else
+ // Don't connect to any networks.
+ strcpy(clientSSID, "");
+ strcpy(clientPass, "");
+#endif
+
+ // By default, we don't want these visible.
+ apBehavior = AP_BEHAVIOR_BUTTON_ONLY; // Must press button for 6 seconds to get AP
+ }
+
+ void onPeerPing(const MeshNodeHeader& node) {
+ // When receiving a message, if the IDs match, it's a conflict
+ // Reset to create a new ID.
+ if (node.id == header.id) {
+ Serial.println("Detected an ID conflict.");
+ reset();
+ }
+
+ // If the message arrives from a higher ID, switch into follower mode
+ if (node.id > header.uplinkId && node.id > header.id) {
+#ifdef RELAY_DEBUGGING
+ // When debugging relay, pretend not to see any nodes above 0x800
+ if (node->id < 0x800)
+#endif
+ follow(&node);
+ }
+
+ // If the message arrived from our uplink, track that we're still linked.
+ if (node.id == header.uplinkId) {
+ uplinkTimer.start(UPLINK_TIMEOUT);
+ }
+
+ // If a message indicates that another node is following this one, or
+ // should be (it's not following anything, but this node's ID is higher)
+ // enter or continue re-broadcasting mode.
+ if (node.uplinkId == header.id || (node.uplinkId == 0 && node.id < header.id)) {
+ if (!isLeading()) {
+ Serial.printf(" LEADING because %03X/%03X is following me\n", node.id, node.uplinkId);
+ }
+ rebroadcastTimer.start(REBROADCAST_TIME);
+ }
+ }
+
+ void printMessage(const NodeMessage* message, signed int rssi) const {
+ Serial.printf("%03X/%03X %s",
+ message->header.id,
+ message->header.uplinkId,
+ command_name(message->command)
+ );
+ if (message->recipients == RECIPIENTS_ROOT)
+ Serial.printf(":ROOT");
+ if (rssi)
+ Serial.printf(" %ddB ", rssi);
+ }
+
+ void onPeerData(const uint8_t* address, const NodeMessage* message, uint8_t len, signed int rssi, bool broadcast) {
+ // Track that another node exists, updating this node's understanding of the mesh.
+ onPeerPing(message->header);
+
+ bool ignore = false;
+ switch (message->recipients) {
+ case RECIPIENTS_ALL:
+ // Ignore this message if not from the uplink
+ ignore = (message->header.id != header.uplinkId);
+ break;
+
+ case RECIPIENTS_ROOT:
+ // Ignore this message if not from one of this node's downlinks
+ ignore = (message->header.uplinkId != header.id);
+ break;
+
+ case RECIPIENTS_INFO:
+ ignore = false;
+ break;
+
+ default:
+ // ignore this!
+ ignore = true;
+ break;
+ }
+
+ if (ignore) {
+#ifdef NODE_DEBUGGING
+ Serial.print(" -- ignored ");
+ printMessage(message, rssi);
+ Serial.println();
+#endif
+ return;
+ }
+
+ // Execute the received command
+ if (message->recipients != RECIPIENTS_ROOT || !isFollowing()) {
+ Serial.print(" >> ");
+ printMessage(message, rssi);
+ Serial.print(" ");
+
+ // Adjust the timebase to match uplink
+ // But only if it's drifting, else animations get jittery
+ uint32_t new_timebase = message->timebase - millis() + 3; // Factor for network delay
+ int32_t diff = new_timebase - strip.timebase;
+ if (diff < -10 || diff > 10)
+ strip.timebase = new_timebase;
+
+ // Execute the command
+ auto valid = receiver->onCommand(
+ message->command,
+ const_cast(message->data)
+ );
+ Serial.println();
+
+ if (!valid)
+ return;
+ }
+
+ // Re-broadcast the message if appropriate
+ if (isLeading() && message->recipients != RECIPIENTS_INFO) {
+ static NodeMessage msg;
+ memcpy(&msg, &message, len);
+ msg.header = header;
+ if (!isFollowing()) {
+ msg.recipients = RECIPIENTS_ALL;
+ }
+#ifdef NODE_DEBUGGING
+ Serial.println("rebroadcast");
+#endif
+ broadcastMessage(&msg, true);
+ }
+ }
+
+ void broadcastMessage(NodeMessage *message, bool is_rebroadcast=false) {
+ // Don't broadcast anything if this node isn't active.
+ if (status != NODE_STATUS_STARTED) {
+ if (status == NODE_STATUS_RECEIVING && statusTimer.ended()) {
+ status = NODE_STATUS_STARTED;
+ statusTimer.stop();
+ Serial.printf("LightNode %s\n", status_code());
+ } else {
+ Serial.printf("broadcastMessage() - not started - %s\n", status_code());
+ return;
+ }
+ }
+ message->timebase = strip.timebase + millis();
+
+#ifdef NODE_DEBUGGING
+ Serial.print(" <<< ");
+ printMessage(message, 0);
+ Serial.println();
+#endif
+
+ __attribute__((unused)) auto success = espnowBroadcast.send((const uint8_t*)message, sizeof(*message));
+#ifdef NODE_DEBUGGING
+ if (!success) {
+ Serial.println("espnowBroadcast.send() failed!");
+ } else {
+ Serial.println("successful broadcast");
+ }
+#endif
+
+ }
+
+ public:
+
+ void sendCommand(CommandId command, void *data, uint8_t len) {
+ // if (!ESP_NOW.isStarted()) {
+ // Serial.println("SendCommand ESP Not Started!");
+ // return;
+ // }
+ if (len > MESSAGE_DATA_SIZE) {
+ Serial.printf("Message is too big: %d vs %d\n",
+ len, MESSAGE_DATA_SIZE);
+ return;
+ }
+
+ NodeMessage message;
+ message.header = header;
+ if (command == COMMAND_INFO) {
+ message.recipients = RECIPIENTS_INFO;
+ } else if (command == COMMAND_STATE) {
+ message.recipients = RECIPIENTS_ALL;
+ } else if (isFollowing()) {
+ // Follower nodes must request that the root re-sends this message
+ message.recipients = RECIPIENTS_ROOT;
+ } else {
+ message.recipients = RECIPIENTS_ALL;
+ }
+ message.command = command;
+ memcpy(&message.data, data, len);
+#ifdef NODE_DEBUGGING
+ Serial.println("sendCommand");
+#endif
+ broadcastMessage(&message);
+ }
+
+ void setup() {
+#ifdef NODE_DEBUGGING
+ reset(TESTING_NODE_ID);
+#else
+ reset();
+#endif
+
+
+#ifdef NODE_DEBUGGING
+ delay(2000);
+#endif
+
+ espnowBroadcast.registerFilter(onEspNowFilter);
+ espnowBroadcast.registerCallback(onEspNowMessage);
+
+ Serial.println("setup: ok");
+ }
+
+ void update() {
+
+ //process any wifi events to turn on/off ESPNode
+ updateESPNowState();
+
+ // Check the last time we heard from the uplink node
+ if (isFollowing() && uplinkTimer.ended()) {
+ follow(NULL);
+ }
+ }
+
+ void reset(MeshId id = 0) {
+ if (id == 0) {
+#if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3))
+ id = random(10, 255); // Leave room at bottom and top of 12 bits
+#else
+ id = random(256, 4000); // Leave room at bottom and top of 12 bits
+#endif
+ }
+ header.id = id;
+ follow(NULL);
+ }
+
+ void follow(const MeshNodeHeader* node) {
+ if (node == NULL) {
+ if (header.uplinkId != 0) {
+ Serial.println("Uplink lost");
+ }
+
+ // Unfollow: following zero means you have no uplink
+ header.uplinkId = 0;
+ onMeshChange();
+ return;
+ }
+
+ // Already following? ignore
+ if (header.uplinkId == node->id)
+ return;
+
+ // Follow
+ Serial.printf("Following %03X:%03X\n",
+ node->id,
+ node->uplinkId
+ );
+ header.uplinkId = node->id;
+ onMeshChange();
+ }
+
+ bool isFollowing() const {
+ return header.uplinkId != 0;
+ }
+ bool isLeading() const {
+ // For now, leading mode is defined as being in re-broadcast mode for any reason.
+ // Any node that thinks it has the highest ID is leading, but so are any nodes that
+ // have seen another node that should be following the leader it is following.
+ return !rebroadcastTimer.ended();
+ }
+
+protected:
+
+ void updateESPNowState() {
+ auto state = espnowBroadcast.getState();
+ static auto prev = espnowBroadcast.STOPPED;
+ switch(state) {
+ case ESPNOWBroadcast::STOPPED:
+ if (NODE_STATUS_QUIET != status) {
+ Serial.printf("updateESPNowState() - %d node_status:%s\n", state, status_code());
+ status = NODE_STATUS_QUIET;
+ rebroadcastTimer.stop();
+ Serial.printf("LightNode %s\n", status_code());
+ }
+ break;
+ case ESPNOWBroadcast::STARTING: {}
+ if ( state != prev ) {
+ Serial.printf("updateESPNowState() - %d node_status:%s\n", state, status_code());
+ }
+ break;
+ case ESPNOWBroadcast::STARTED:
+ if (NODE_STATUS_QUIET == status) {
+ Serial.printf("updateESPNowState() - %d node_status:%s\n", state, status_code());
+ status = NODE_STATUS_RECEIVING;
+ statusTimer.start(STATUS_TIMEOUT_BASE - header.id / 2);
+ Serial.printf("LightNode %s\n", status_code());
+ }
+ break;
+ default:
+ break;
+ }
+ prev = state;
+ }
+
+ typedef struct wizmote_message {
+ uint8_t program; // 0x91 for ON button, 0x81 for all others
+ uint8_t seq[4]; // Incremetal sequence number 32 bit unsigned integer LSB first
+ uint8_t byte5 = 32; // Unknown
+ uint8_t button; // Identifies which button is being pressed
+ uint8_t byte8 = 1; // Unknown, but always 0x01
+ uint8_t byte9 = 100; // Unnkown, but always 0x64
+
+ uint8_t byte10; // Unknown, maybe checksum
+ uint8_t byte11; // Unknown, maybe checksum
+ uint8_t byte12; // Unknown, maybe checksum
+ uint8_t byte13; // Unknown, maybe checksum
+ } wizmote_message;
+
+ void onWizmote(const uint8_t* address, const wizmote_message* data, uint8_t len) {
+ static uint32_t last_seq = 0;
+ uint32_t cur_seq = data->seq[0] | (data->seq[1] << 8) | (data->seq[2] << 16) | (data->seq[3] << 24);
+ if (cur_seq == last_seq)
+ return;
+ last_seq = cur_seq;
+
+ receiver->onButton(data->button);
+ }
+
+ static void onEspNowMessage(const uint8_t *address, const uint8_t *msg, uint8_t len, int8_t rssi) {
+ // basic length and field checking has been done in onEspNowFilter
+ if (msg) {
+ if(len == sizeof(NodeMessage)) {
+ instance->onPeerData(address, (const NodeMessage*)msg, len, rssi, true);
+ } else if(len == sizeof(wizmote_message)) {
+ instance->onWizmote(address, (const wizmote_message*)msg, len);
+ } else {
+#ifdef NODE_DEBUGGING
+ Serial.printf("wrong size EspNowMessage received %d\n", len);
+#endif
+ }
+ }
+ }
+
+ static bool onEspNowFilter(const uint8_t *address, const uint8_t *msg, uint8_t len, int8_t rssi) {
+ if (len == sizeof(NodeMessage)) {
+ return ((const NodeMessage*)msg)->header.version == instance->header.version;
+ } else if (len == sizeof(wizmote_message)) {
+ auto wizmote = (const wizmote_message*)msg;
+ return !( wizmote->byte8 != 1 || wizmote->byte9 != 100 || wizmote->byte5 != 32);
+ }
+ return false;
+ }
+};
+
+LightNode* LightNode::instance = nullptr;
+
diff --git a/usermods/Tubes/options.h b/usermods/Tubes/options.h
new file mode 100644
index 0000000000..0c4b28e7ea
--- /dev/null
+++ b/usermods/Tubes/options.h
@@ -0,0 +1,82 @@
+#pragma once
+
+typedef enum SyncMode: uint8_t {
+ All=0,
+ SinDrift=1,
+ Pulse=2,
+ Swing=3,
+ SwingDrift=4,
+} SyncMode;
+
+typedef enum Duration: uint8_t {
+ ExtraShortDuration=0,
+ ShortDuration=10,
+ MediumDuration=20,
+ LongDuration=30,
+ ExtraLongDuration=40,
+} Duration;
+
+typedef enum Energy: uint8_t {
+ Boring=0, // a "boring" pattern is slow or whatever but -needs- effects to be interesting
+ Chill=10, // A "chill" pattern is only slow fades, no flashes
+ MediumEnergy=20,
+ HighEnergy=230
+} Energy;
+
+
+typedef struct ControlParameters {
+ ControlParameters(Duration d=MediumDuration, Energy e=Chill) : duration(d), energy(e) {};
+
+ public:
+ Duration duration;
+ Energy energy;
+} ControlParams;
+
+typedef enum PenMode: uint8_t {
+ Draw=0,
+ Erase=1,
+ Blend=2,
+ Invert=3,
+ White=4,
+ Black=5,
+ Brighten=6,
+ Darken=7,
+ Flicker=8,
+} PenMode;
+
+typedef enum EffectMode: uint8_t {
+ None=0,
+ Glitter=1,
+ Bubble=2,
+ Beatbox1=3,
+ Beatbox2=4,
+ Spark=5,
+ Flash=6,
+} EffectMode;
+
+typedef enum BeatPulse: uint8_t {
+ Continuous=0,
+ Eighth=1,
+ Quarter=2,
+ Half=4,
+ Beat=8,
+ TwoBeats=16,
+ Measure=32,
+ TwoMeasures=64,
+ Phrase=128,
+} BeatPulse;
+
+class EffectParameters {
+ public:
+ EffectParameters(EffectMode e=None, PenMode p=Draw, BeatPulse b=Beat, uint8_t c=255) :
+ effect(e),
+ pen(p),
+ beat(b),
+ chance(c)
+ { };
+
+ EffectMode effect;
+ PenMode pen=Draw;
+ BeatPulse beat=Beat;
+ uint8_t chance=255;
+};
diff --git a/usermods/Tubes/particle.h b/usermods/Tubes/particle.h
new file mode 100644
index 0000000000..01e1399a04
--- /dev/null
+++ b/usermods/Tubes/particle.h
@@ -0,0 +1,252 @@
+#pragma once
+
+#include "wled.h"
+#include "beats.h"
+#include "options.h"
+
+#define MAX_PARTICLES 80
+#undef PARTICLE_PALETTES
+
+#define DEFAULT_PARTICLE_VOLUME 64 // Default mic average is around 64 during music
+
+class Particle;
+
+typedef void (*ParticleFn)(Particle& particle, WS2812FX* leds);
+uint8_t particleVolume = DEFAULT_PARTICLE_VOLUME;
+
+extern void drawPoint(Particle& particle, WS2812FX* leds);
+
+
+class Particle {
+ public:
+ Particle(uint16_t pos = 0, CRGB c=CRGB::White, PenMode p=Draw, uint32_t life=20000, ParticleFn fn=drawPoint) :
+ born(0),
+ lifetime(life),
+ age(0),
+ color(c),
+ pen(p),
+ brightness(192<<8),
+ drawFn(fn),
+ position(pos),
+ velocity(0),
+ gravity(0)
+ {};
+
+ BeatFrame_24_8 born;
+ BeatFrame_24_8 lifetime;
+ BeatFrame_24_8 age;
+
+ CRGB color;
+ PenMode pen;
+ uint16_t brightness;
+ ParticleFn drawFn;
+
+ uint16_t position;
+ int16_t velocity;
+ int16_t gravity;
+// void (*die_fn)(Particle *particle) = NULL;
+
+#ifdef PARTICLE_PALETTES
+ CRGBPalette16 palette; // 48 bytes per particle!?
+#endif
+
+ void update(BeatFrame_24_8 frame)
+ {
+ // Particles get brighter with the beat
+ brightness = (scale8(particleVolume, 80) + 170) << 8;
+
+ age = frame - born;
+ position = udelta16(position, velocity);
+ velocity = delta16(velocity, gravity);
+ }
+
+ uint16_t age_frac16(BeatFrame_24_8 age) const
+ {
+ if (age >= lifetime)
+ return 65535;
+ uint32_t a = age * 65536;
+ return a / lifetime;
+ }
+
+ uint16_t udelta16(uint16_t x, int16_t dx) const
+ {
+ if (dx > 0 && 65535-x < dx)
+ return 65335;
+ if (dx < 0 && x < -dx)
+ return 0;
+ return x + dx;
+ }
+
+ int16_t delta16(int16_t x, int16_t dx) const
+ {
+ if (dx > 0 && 32767-x < dx)
+ return 32767;
+ if (dx < 0 && x < -32767 - dx)
+ return -32767;
+ return x + dx;
+ }
+
+ CRGB color_at(uint16_t age_frac) {
+ // Particles get dimmer with age
+ uint8_t a = age_frac >> 8;
+ brightness = scale8((uint8_t)(brightness>>8), 255-a);
+
+#ifdef PARTICLE_PALETTES
+ // a black pattern actually means to use the current palette
+ if (color == CRGB(0,0,0))
+ return ColorFromPalette(palette, a, brightness);
+#endif
+
+ uint8_t r = scale8(color.r, brightness);
+ uint8_t g = scale8(color.g, brightness);
+ uint8_t b = scale8(color.b, brightness);
+ return CRGB(r,g,b);
+ }
+
+ void draw(WS2812FX* leds) {
+ drawFn(*this, leds);
+ }
+
+ void draw_with_pen(WS2812FX* leds, int pos, CRGB color) {
+ CRGB c = CRGB(strip.getPixelColor(pos));
+ CRGB new_color;
+
+ switch (pen) {
+ case Draw:
+ new_color = color;
+ break;
+
+ case Blend:
+ new_color = c | color;
+ break;
+
+ case Erase:
+ new_color = c & color;
+ break;
+
+ case Invert:
+ new_color = -c;
+ break;
+
+ case Brighten: {
+ uint8_t t = color.getAverageLight();
+ new_color = c + CRGB(t,t,t);
+ break;
+ }
+
+ case Darken: {
+ uint8_t t = color.getAverageLight();
+ new_color = c - CRGB(t,t,t);
+ break;
+ }
+
+ case Flicker: {
+ uint8_t t = color.getAverageLight();
+ if (millis() % 2) {
+ new_color = c - CRGB(t,t,t);
+ } else {
+ new_color = c + CRGB(t,t,t);
+ }
+ break;
+ }
+
+ case White:
+ new_color = CRGB::White;
+ break;
+
+ case Black:
+ new_color = CRGB::Black;
+ break;
+
+ default:
+ // Unknown pen
+ return;
+ }
+
+ strip.setPixelColor(pos, new_color);
+ }
+
+};
+
+Particle particles[MAX_PARTICLES];
+BeatFrame_24_8 particle_beat_frame;
+uint8_t numParticles = 0;
+
+void removeParticle(uint8_t i) {
+ if (i >= numParticles)
+ return;
+
+ // coalesce current particle list
+ int rest = numParticles - i;
+ if (rest > 0) {
+ memmove(&particles[i], &particles[i+1], sizeof(particles[0]) * rest);
+ }
+
+ numParticles -= 1;
+}
+
+void addParticle(Particle&& particle) {
+ particle.born = particle_beat_frame;
+ if (numParticles >= MAX_PARTICLES) {
+ removeParticle(0);
+ }
+ particles[numParticles++] = particle;
+}
+
+void drawFlash(Particle& particle, WS2812FX* leds) {
+ auto num_leds = leds->getLengthTotal();
+ uint16_t age_frac = particle.age_frac16(particle.age);
+ CRGB c = particle.color_at(age_frac);
+ for (int pos = 0; pos < num_leds; pos++) {
+ particle.draw_with_pen(leds, pos, c);
+ }
+}
+
+void drawPoint(Particle& particle, WS2812FX* leds) {
+ uint16_t age_frac = particle.age_frac16(particle.age);
+ CRGB c = particle.color_at(age_frac);
+
+ uint16_t pos = scale16(particle.position, leds->getLengthTotal() - 1);
+ particle.draw_with_pen(leds, pos, c);
+}
+
+void drawRadius(Particle& particle, WS2812FX* leds, uint16_t pos, uint8_t radius, CRGB c, bool dim=true) {
+ auto num_leds = leds->getLengthTotal();
+ for (int i = 0; i < radius; i++) {
+ uint8_t bright = dim ? ((radius-i) * 255) / radius : 255;
+ nscale8(&c, 1, bright);
+
+ uint8_t y = pos - i;
+ if (y >= 0 && y < num_leds)
+ particle.draw_with_pen(leds, y, c);
+
+ if (i == 0)
+ continue;
+
+ y = pos + i;
+ if (y >= 0 && y < num_leds)
+ particle.draw_with_pen(leds, y, c);
+ }
+}
+
+void drawPop(Particle& particle, WS2812FX* leds) {
+ uint16_t age_frac = particle.age_frac16(particle.age);
+ CRGB c = particle.color_at(age_frac);
+ uint16_t pos = scale16(particle.position, leds->getLengthTotal() - 1);
+ uint8_t radius = scale16((sin16(age_frac/2) - 32768) * 2, 8);
+
+ drawRadius(particle, leds, pos, radius, c);
+}
+
+void drawBeatbox(Particle& particle, WS2812FX* leds) {
+ uint16_t age_frac = particle.age_frac16(particle.age);
+ CRGB c = particle.color_at(age_frac);
+ uint16_t pos = scale16(particle.position, leds->getLengthTotal() - 1);
+ uint8_t radius = 3;
+
+ // Bump up the radius with any beats
+ radius += scale8(particleVolume, 8);
+
+ drawRadius(particle, leds, pos, radius, c, false);
+}
+
diff --git a/usermods/Tubes/pattern.h b/usermods/Tubes/pattern.h
new file mode 100644
index 0000000000..0bc32f35a8
--- /dev/null
+++ b/usermods/Tubes/pattern.h
@@ -0,0 +1,327 @@
+#pragma once
+
+#include "virtual_strip.h"
+#include "FX.h"
+
+void rainbow(VirtualStrip *strip)
+{
+ // FastLED's built-in rainbow generator
+ fill_rainbow( strip->leds, strip->num_leds, strip->hue, 3);
+}
+
+void palette_wave(VirtualStrip *strip)
+{
+ // FastLED's built-in rainbow generator
+ uint8_t hue = strip->hue;
+ for (auto i=0; i < strip->num_leds; i++) {
+ CRGB c = strip->palette_color(i, hue);
+ nscale8x3(c.r, c.g, c.b, sin8(hue*8));
+ strip->leds[i] = c;
+ hue++;
+ }
+}
+
+void particleTest(VirtualStrip *strip)
+{
+ fill_solid( strip->leds, strip->num_leds, CRGB::Black);
+ fill_solid( strip->leds, 2, strip->palette_color(0, strip->hue));
+}
+
+void solidBlack(VirtualStrip *strip)
+{
+ strip->fill(CRGB::Black);
+}
+
+void solidWhite(VirtualStrip *strip)
+{
+ strip->fill(CRGB::White);
+}
+
+void solidRed(VirtualStrip *strip)
+{
+ strip->fill(CRGB::Red);
+}
+
+void solidBlue(VirtualStrip *strip)
+{
+ strip->fill(CRGB::Blue);
+}
+
+void confetti(VirtualStrip *strip)
+{
+ strip->darken(2);
+
+ int pos = random16(strip->num_leds);
+ strip->leds[pos] += strip->palette_color(random8(64), strip->hue);
+}
+
+uint16_t random_offset = random16();
+
+void biwave(VirtualStrip *strip)
+{
+ uint16_t l = strip->frame * 16;
+ l = sin16( l + random_offset ) + 32768;
+
+ uint16_t r = strip->frame * 32;
+ r = cos16( r + random_offset ) + 32768;
+
+ uint8_t p1 = scaled16to8(l, 0, strip->num_leds-1);
+ uint8_t p2 = scaled16to8(r, 0, strip->num_leds-1);
+
+ if (p2 < p1) {
+ uint16_t t = p1;
+ p1 = p2;
+ p2 = t;
+ }
+
+ strip->fill(CRGB::Black);
+ for (uint16_t p = p1; p <= p2; p++) {
+ strip->leds[p] = strip->palette_color(p*2, strip->hue*3);
+ }
+}
+
+void tick(VirtualStrip *strip) {
+ strip->fill(CRGB::Black);
+ strip->leds[strip->beat % 16] = CRGB::White;
+}
+
+void sinelon(VirtualStrip *strip)
+{
+ // a colored dot sweeping back and forth, with fading trails
+ strip->darken(30);
+
+ int pos = scale16(sin16( strip->frame << 5 ) + 32768, strip->num_leds-1); // beatsin16 re-implemented
+ strip->leds[pos] += strip->hue_color();
+}
+
+void bpm_palette(VirtualStrip *strip)
+{
+ uint8_t beat = strip->bpm_sin16(64, 255);
+ for (auto i = 0; i < strip->num_leds; i++) {
+ CRGB c = strip->palette_color(i*2, strip->hue, beat-strip->hue+(i*10));
+ strip->leds[i] = c;
+ }
+}
+
+void bpm(VirtualStrip *strip)
+{
+ // colored stripes pulsing at a defined Beats-Per-Minute (BPM)
+ CRGBPalette16 palette = PartyColors_p;
+
+ uint8_t beat = strip->bpm_sin16(64, 255);
+ for (auto i = 0; i < strip->num_leds; i++) {
+ strip->leds[i] = ColorFromPalette(palette, strip->hue+(i*2), beat-strip->hue+(i*10));
+ }
+}
+
+void juggle(VirtualStrip *strip)
+{
+ // eight colored dots, weaving in and out of sync with each other
+ strip->darken(5);
+
+ byte dothue = 0;
+ for (auto i = 0; i < 8; i++) {
+ CRGB c = strip->palette_color(dothue + strip->hue);
+ // c = CHSV(dothue, 200, 255);
+ strip->leds[beatsin16( i+7, 0, strip->num_leds-1 )] |= c;
+ dothue += 32;
+ }
+}
+
+uint8_t noise[MAX_VIRTUAL_LEDS] = {0};
+
+void fillnoise8(uint32_t frame, int32_t num_leds) {
+ uint16_t scale = 17;
+ uint8_t dataSmoothing = 240;
+
+ auto max_leds = sizeof(noise)/sizeof(noise[0]);
+ if (num_leds > max_leds) {
+ num_leds = max_leds;
+ }
+
+ for (auto i = 0; i < num_leds; i++) {
+ uint8_t data = inoise8(i * scale, frame>>2);
+
+ // The range of the inoise8 function is roughly 16-238.
+ // These two operations expand those values out to roughly 0..255
+ data = qsub8(data,16);
+ data = qadd8(data,scale8(data,39));
+
+ uint8_t olddata = noise[i];
+ uint8_t newdata = scale8( olddata, dataSmoothing) + scale8( data, 256 - dataSmoothing);
+ noise[i] = newdata;
+ }
+}
+
+void drawNoise(VirtualStrip *strip)
+{
+ // generate noise data
+ fillnoise8(strip->frame >> 2, strip->num_leds);
+
+ for (auto i = 0; i < strip->num_leds; i++) {
+ CRGB color = strip->palette_color(noise[i], strip->hue);
+ strip->leds[i] = color;
+ }
+}
+
+void draw_wled_fx(VirtualStrip *strip) {
+}
+
+typedef struct {
+ uint8_t wled_fx_id;
+ BackgroundFn backgroundFn;
+ ControlParameters control;
+} PatternDef;
+
+
+// List of patterns to cycle through. Each is defined as a separate function below.
+static const uint8_t numInternalPatterns = 24;
+PatternDef gPatterns[] = {
+ {0, drawNoise, {ShortDuration}},
+ {0, drawNoise, {ShortDuration}},
+ {0, drawNoise, {MediumDuration}},
+ {0, drawNoise, {MediumDuration}},
+ {0, drawNoise, {MediumDuration}},
+ {0, drawNoise, {LongDuration}},
+ {0, drawNoise, {LongDuration}},
+ {0, confetti, {ShortDuration}},
+ {0, confetti, {MediumDuration}},
+ {0, juggle, {ShortDuration}},
+
+ {0, drawNoise, {ShortDuration}},
+ {0, drawNoise, {ShortDuration}},
+ {0, drawNoise, {MediumDuration}},
+ {0, drawNoise, {MediumDuration}},
+ {0, drawNoise, {MediumDuration}},
+ {0, drawNoise, {LongDuration}},
+ {0, drawNoise, {LongDuration}},
+ {0, confetti, {ShortDuration}},
+ {0, confetti, {MediumDuration}},
+ {0, juggle, {ShortDuration}},
+
+ {0, palette_wave, {ShortDuration, Boring}},
+ {0, palette_wave, {MediumDuration, Boring}},
+ {0, bpm_palette, {ShortDuration}},
+ {0, bpm_palette, {MediumDuration, HighEnergy}},
+ {FX_MODE_FADE, draw_wled_fx, {ShortDuration, Boring}}, // 12
+ {FX_MODE_CHASE_RAINBOW, draw_wled_fx, {MediumDuration, HighEnergy}}, // 30
+ // Make it HighEnergy? or find out why it's sometimes flashy
+ {FX_MODE_AURORA, draw_wled_fx, {MediumDuration, Boring}}, // 38
+ // TODO: Aurora is too dark?
+ {FX_MODE_GRADIENT, draw_wled_fx,{ShortDuration}}, // 46
+ {FX_MODE_FAIRYTWINKLE, draw_wled_fx, {LongDuration}}, // 51
+ {FX_MODE_RUNNING_DUAL, draw_wled_fx, {ExtraShortDuration, Boring}},// 52
+
+ {FX_MODE_DUAL_LARSON_SCANNER, draw_wled_fx, {MediumDuration}}, // 60
+ {FX_MODE_JUGGLE, draw_wled_fx, {MediumDuration}}, // 64
+ {FX_MODE_PALETTE, draw_wled_fx, {ShortDuration}},// 65
+ {FX_MODE_FIRE_2012, draw_wled_fx, {MediumDuration}}, // 66
+ {FX_MODE_BPM, draw_wled_fx, {MediumDuration}}, // 68
+ // TODO: needs to be slowed down
+ {FX_MODE_FILLNOISE8, draw_wled_fx, {LongDuration}}, // 69
+ {FX_MODE_NOISE16_2, draw_wled_fx, {MediumDuration}}, // 71
+ {FX_MODE_NOISE16_3, draw_wled_fx, {ShortDuration}}, // 72
+ {FX_MODE_NOISE16_3, draw_wled_fx, {LongDuration, MediumEnergy}}, // 72
+ // TODO: Noise3 needs to be slowed down, it's a bit spastic
+ {FX_MODE_COLORTWINKLE, draw_wled_fx, {MediumDuration}}, // 74
+
+ {FX_MODE_LAKE, draw_wled_fx, {ShortDuration}}, // 75
+ {FX_MODE_LAKE, draw_wled_fx, {MediumDuration}}, // 75
+ {FX_MODE_LAKE, draw_wled_fx, {LongDuration}}, // 75
+ {FX_MODE_METEOR_SMOOTH, draw_wled_fx, {MediumDuration}}, // 77
+ {FX_MODE_STARBURST, draw_wled_fx, {ExtraShortDuration, HighEnergy}}, // 89
+ {FX_MODE_EXPLODING_FIREWORKS, draw_wled_fx, {ExtraShortDuration}},// 90
+ // TODO: Must be set to only fire from one side
+ {FX_MODE_SINELON_DUAL, draw_wled_fx, {MediumDuration}}, // 93
+ {FX_MODE_POPCORN, draw_wled_fx, {ShortDuration, MediumEnergy}}, // 95
+ {FX_MODE_PLASMA, draw_wled_fx, {ShortDuration}}, // 97
+ {FX_MODE_PLASMA, draw_wled_fx, {LongDuration}}, // 97
+
+ {FX_MODE_PACIFICA, draw_wled_fx, {ShortDuration}}, // 101
+ {FX_MODE_PACIFICA, draw_wled_fx, {LongDuration}}, // 101
+ {FX_MODE_TWINKLEUP, draw_wled_fx, {LongDuration}}, // 106
+ {FX_MODE_NOISEPAL, draw_wled_fx, {LongDuration}}, // 107
+ {FX_MODE_PHASEDNOISE, draw_wled_fx, {MediumDuration}}, // 109
+ {FX_MODE_FLOW, draw_wled_fx, {ShortDuration}}, // 110
+ {FX_MODE_FLOW, draw_wled_fx, {ShortDuration}}, // 110
+ {FX_MODE_FLOW, draw_wled_fx, {ShortDuration}}, // 110
+ {FX_MODE_FLOW, draw_wled_fx, {MediumDuration}}, // 110
+ {FX_MODE_FLOW, draw_wled_fx, {MediumDuration}}, // 110
+
+ {FX_MODE_FLOW, draw_wled_fx, {LongDuration}}, // 110
+ {FX_MODE_FLOW, draw_wled_fx, {ExtraLongDuration}}, // 110
+ {FX_MODE_FIRE_2012, draw_wled_fx, {ShortDuration}}, // 66
+ {FX_MODE_FIRE_2012, draw_wled_fx, {MediumDuration}}, // 66
+ {FX_MODE_PHASEDNOISE, draw_wled_fx, {ShortDuration}}, // 109
+
+};
+
+/*
+*/
+const uint8_t gPatternCount = ARRAY_SIZE(gPatterns);
+
+/*
+
+WLED OK not great:
+4 - Wipe random
+8 - Colorloop
+15 - Running
+18 - Dissolve
+27 - Android
+36 - Sweep Random
+41 - Lighthouse
+44 - Tetrix
+50 - Two Dots
+56 - Tri Fade
+67 - Color Waves --- maybe too spastic
+70 - Noise 1
+73 - Noise 4
+80 - Twinklefox
+104 - Sunrise
+108 - Sine Wave
+115 - Blends
+154 - Plasmoid
+157 - Noisemeter
+
+LIST OF GOOD PATTERNS
+
+Aurora
+Dynamic Smooth
+Blends
+Colortwinkles
+Fireworks
+Fireworks Starburst
+Flow
+Lake
+Noise 2
+Noise 4
+Pacifica
+Plasma
+Ripple
+Running Dual
+Twinklecat
+Twinkleup
+
+AUDIOREACTIVE
+Midnoise
+GravCenter
+
+MAYBE GOOD PATTERNS
+Fillnoise
+Gradient
+Juggle
+Meteor Smooth
+Palette
+Phased
+Saw
+Sinelon Dual
+Tetrix
+
+
+Colors to fix:
+72 - replace with something else
+35 - drops framerate
+62 - drops framerate
+61 - drops framerate
+
+*/
\ No newline at end of file
diff --git a/usermods/Tubes/sound.h b/usermods/Tubes/sound.h
new file mode 100644
index 0000000000..facd751577
--- /dev/null
+++ b/usermods/Tubes/sound.h
@@ -0,0 +1,54 @@
+
+#include "wled.h"
+#include "fcn_declare.h"
+#include "particle.h"
+
+
+class Sounder {
+ public:
+ bool active = true;
+ bool overlay = false;
+ uint8_t volume;
+
+ void setup() {
+ }
+
+ void update() {
+ if (!active) {
+ particleVolume = DEFAULT_PARTICLE_VOLUME; // Average volume
+ return;
+ }
+
+ um_data_t *um_data;
+ if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) {
+ active = false;
+ overlay = false;
+ return;
+ }
+
+ float volumeSmth = *(float*)um_data->u_data[0];
+ volume = constrain(volumeSmth, 0, 255); // Keep the sample from overflowing.
+ particleVolume = volume;
+ }
+
+ void handleOverlayDraw() {
+ if (!active)
+ return;
+ if (!overlay)
+ return;
+
+ int len = scale8(volume, 32);
+
+ Segment& segment = strip.getMainSegment();
+
+ for (int i = 0; i < len; i++) {
+ uint32_t color = segment.color_from_palette(i, true, true, 255, 192);
+ strip.setPixelColor(i, color);
+ }
+ }
+
+ static float mapf(float x, float in_min, float in_max, float out_min, float out_max){
+ return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
+ }
+
+};
diff --git a/usermods/Tubes/timer.h b/usermods/Tubes/timer.h
new file mode 100644
index 0000000000..f888a12316
--- /dev/null
+++ b/usermods/Tubes/timer.h
@@ -0,0 +1,67 @@
+#pragma once
+
+class GlobalTimer {
+ public:
+ uint32_t now_millis;
+ uint32_t now_micros;
+ uint32_t last_micros;
+ uint32_t last_millis;
+ uint32_t delta_micros;
+ uint32_t delta_millis;
+
+ void setup()
+ {
+ last_millis = now_millis = millis();
+ last_micros = now_micros = micros();
+ }
+
+ void update()
+ {
+ last_millis = now_millis;
+ now_millis = millis();
+ delta_millis = now_millis - last_millis;
+
+ last_micros = now_micros;
+ now_micros = micros();
+ delta_micros = now_micros - last_micros;
+ }
+};
+
+GlobalTimer globalTimer;
+
+
+
+class Timer {
+ public:
+ uint32_t markTime;
+
+ void start(uint32_t duration_ms) {
+ markTime = globalTimer.now_millis + duration_ms;
+ }
+
+ void stop() {
+ start(0);
+ }
+
+ uint32_t since_mark() const {
+ if (globalTimer.now_millis < markTime)
+ return 0;
+ return globalTimer.now_millis - markTime;
+ }
+
+ void snooze(uint32_t duration_ms) {
+ while (markTime < globalTimer.now_millis)
+ markTime += duration_ms;
+ }
+
+ bool ended() const {
+ return globalTimer.now_millis > markTime;
+ }
+
+ bool every(uint32_t duration_ms) {
+ if (!ended())
+ return 0;
+ snooze(duration_ms);
+ return 1;
+ }
+};
\ No newline at end of file
diff --git a/usermods/Tubes/update_server.py b/usermods/Tubes/update_server.py
new file mode 100644
index 0000000000..2cea859c69
--- /dev/null
+++ b/usermods/Tubes/update_server.py
@@ -0,0 +1,275 @@
+#!/usr/bin/python
+"""Simple HTTP Server.
+
+This module builds on BaseHTTPServer by implementing the standard GET
+and HEAD requests in a fairly straightforward manner.
+
+"""
+__version__ = "0.6"
+
+__all__ = ["SimpleHTTPRequestHandler"]
+
+import os
+import posixpath
+import BaseHTTPServer
+import urllib
+import cgi
+import sys
+import mimetypes
+import zlib
+from optparse import OptionParser
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+SERVER_PORT = 8000
+encoding_type = 'gzip'
+
+def parse_options():
+ # Option parsing logic.
+ parser = OptionParser()
+ parser.add_option("-e", "--encoding", dest="encoding_type",
+ help="Encoding type for server to utilize",
+ metavar="ENCODING", default='gzip')
+ global SERVER_PORT
+ parser.add_option("-p", "--port", dest="port", default=SERVER_PORT,
+ help="The port to serve the files on",
+ metavar="ENCODING")
+ (options, args) = parser.parse_args()
+ global encoding_type
+ encoding_type = options.encoding_type
+ SERVER_PORT = int(options.port)
+
+ if encoding_type not in ['zlib', 'deflate', 'gzip']:
+ sys.stderr.write("Please provide a valid encoding_type for the server to utilize.\n")
+ sys.stderr.write("Possible values are 'zlib', 'gzip', and 'deflate'\n")
+ sys.stderr.write("Usage: python GzipSimpleHTTPServer.py --encoding=\n")
+ sys.exit()
+
+
+def zlib_encode(content):
+ zlib_compress = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS)
+ data = zlib_compress.compress(content) + zlib_compress.flush()
+ return data
+
+
+def deflate_encode(content):
+ deflate_compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS)
+ data = deflate_compress.compress(content) + deflate_compress.flush()
+ return data
+
+
+def gzip_encode(content):
+ gzip_compress = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)
+ data = gzip_compress.compress(content) + gzip_compress.flush()
+ return data
+
+
+class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ """Simple HTTP request handler with GET and HEAD commands.
+
+ This serves files from the current directory and any of its
+ subdirectories. The MIME type for files is determined by
+ calling the .guess_type() method.
+
+ The GET and HEAD requests are identical except that the HEAD
+ request omits the actual contents of the file.
+
+ """
+
+ server_version = "SimpleHTTP/" + __version__
+
+ def do_GET(self):
+ """Serve a GET request."""
+ content = self.send_head()
+ if content:
+ self.wfile.write(content)
+
+ def do_HEAD(self):
+ """Serve a HEAD request."""
+ content = self.send_head()
+
+ def send_head(self):
+ """Common code for GET and HEAD commands.
+
+ This sends the response code and MIME headers.
+
+ Return value is either a file object (which has to be copied
+ to the outputfile by the caller unless the command was HEAD,
+ and must be closed by the caller under all circumstances), or
+ None, in which case the caller has nothing further to do.
+
+ """
+ path = self.translate_path(self.path)
+ print("Serving path '%s'" % path)
+ f = None
+ if os.path.isdir(path):
+ if not self.path.endswith('/'):
+ # redirect browser - doing basically what apache does
+ self.send_response(301)
+ self.send_header("Location", self.path + "/")
+ self.end_headers()
+ return None
+ for index in "index.html", "index.htm":
+ index = os.path.join(path, index)
+ if os.path.exists(index):
+ path = index
+ break
+ else:
+ return self.list_directory(path).read()
+ ctype = self.guess_type(path)
+ try:
+ # Always read in binary mode. Opening files in text mode may cause
+ # newline translations, making the actual size of the content
+ # transmitted *less* than the content-length!
+ f = open(path, 'rb')
+ except IOError:
+ self.send_error(404, "File not found")
+ return None
+ self.send_response(200)
+ self.send_header("Content-type", ctype)
+ self.send_header("Content-Encoding", encoding_type)
+ fs = os.fstat(f.fileno())
+ raw_content_length = fs[6]
+ content = f.read()
+
+ # # Encode content based on runtime arg
+ # if encoding_type == "gzip":
+ # content = gzip_encode(content)
+ # elif encoding_type == "deflate":
+ # content = deflate_encode(content)
+ # elif encoding_type == "zlib":
+ # content = zlib_encode(content)
+
+ compressed_content_length = len(content)
+ f.close()
+ self.send_header("Content-Length", max(raw_content_length, compressed_content_length))
+ self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
+ self.end_headers()
+ return content
+
+ def list_directory(self, path):
+ """Helper to produce a directory listing (absent index.html).
+
+ Return value is either a file object, or None (indicating an
+ error). In either case, the headers are sent, making the
+ interface the same as for send_head().
+
+ """
+ try:
+ list = os.listdir(path)
+ except os.error:
+ self.send_error(404, "No permission to list directory")
+ return None
+ list.sort(key=lambda a: a.lower())
+ f = StringIO()
+ displaypath = cgi.escape(urllib.unquote(self.path))
+ f.write('')
+ f.write("\nDirectory listing for %s\n" % displaypath)
+ f.write("\nDirectory listing for %s
\n" % displaypath)
+ f.write("
\n\n")
+ for name in list:
+ fullname = os.path.join(path, name)
+ displayname = linkname = name
+ # Append / for directories or @ for symbolic links
+ if os.path.isdir(fullname):
+ displayname = name + "/"
+ linkname = name + "/"
+ if os.path.islink(fullname):
+ displayname = name + "@"
+ # Note: a link to a directory displays with @ and links with /
+ f.write('- %s\n'
+ % (urllib.quote(linkname), cgi.escape(displayname)))
+ f.write("
\n
\n\n\n")
+ length = f.tell()
+ f.seek(0)
+ self.send_response(200)
+ encoding = sys.getfilesystemencoding()
+ self.send_header("Content-type", "text/html; charset=%s" % encoding)
+ self.send_header("Content-Length", str(length))
+ self.end_headers()
+ return f
+
+ def translate_path(self, path):
+ """Translate a /-separated PATH to the local filename syntax.
+
+ Components that mean special things to the local file system
+ (e.g. drive or directory names) are ignored. (XXX They should
+ probably be diagnosed.)
+
+ """
+ # abandon query parameters
+ path = path.split('?',1)[0]
+ path = path.split('#',1)[0]
+ path = posixpath.normpath(urllib.unquote(path))
+ words = path.split('/')
+ words = filter(None, words)
+ path = os.getcwd() + "/../../.pio/build/esp32_quinled_uno"
+ for word in words:
+ drive, word = os.path.splitdrive(word)
+ head, word = os.path.split(word)
+ if word in (os.curdir, os.pardir): continue
+ path = os.path.join(path, word)
+ return path
+
+ def guess_type(self, path):
+ """Guess the type of a file.
+
+ Argument is a PATH (a filename).
+
+ Return value is a string of the form type/subtype,
+ usable for a MIME Content-type header.
+
+ The default implementation looks the file's extension
+ up in the table self.extensions_map, using application/octet-stream
+ as a default; however it would be permissible (if
+ slow) to look inside the data to make a better guess.
+
+ """
+
+ base, ext = posixpath.splitext(path)
+ if ext in self.extensions_map:
+ return self.extensions_map[ext]
+ ext = ext.lower()
+ if ext in self.extensions_map:
+ return self.extensions_map[ext]
+ else:
+ return self.extensions_map['']
+
+ if not mimetypes.inited:
+ mimetypes.init() # try to read system mime.types
+ extensions_map = mimetypes.types_map.copy()
+ extensions_map.update({
+ '': 'application/octet-stream', # Default
+ '.py': 'text/plain',
+ '.c': 'text/plain',
+ '.h': 'text/plain',
+ })
+
+
+def test(HandlerClass = SimpleHTTPRequestHandler,
+ ServerClass = BaseHTTPServer.HTTPServer):
+ """Run the HTTP request handler class.
+
+ This runs an HTTP server on port 8000 (or the first command line
+ argument).
+
+ """
+
+ parse_options()
+
+ server_address = ('0.0.0.0', SERVER_PORT)
+
+ SimpleHTTPRequestHandler.protocol_version = "HTTP/1.0"
+ httpd = BaseHTTPServer.HTTPServer(server_address, SimpleHTTPRequestHandler)
+
+ sa = httpd.socket.getsockname()
+ print("Serving HTTP on", sa[0], "port", sa[1], "...")
+ httpd.serve_forever()
+ BaseHTTPServer.test(HandlerClass, ServerClass)
+
+
+if __name__ == '__main__':
+ test()
\ No newline at end of file
diff --git a/usermods/Tubes/updater.h b/usermods/Tubes/updater.h
new file mode 100644
index 0000000000..e7fee8efbc
--- /dev/null
+++ b/usermods/Tubes/updater.h
@@ -0,0 +1,346 @@
+#pragma once
+
+#include "wled.h"
+#include
+#include
+#include
+#include "timer.h"
+
+#define RELEASE_VERSION 13
+
+// Utility to extract header value from headers
+String getHeaderValue(String header, String headerName) {
+ return header.substring(strlen(headerName.c_str()));
+}
+
+typedef enum UpdateWorkflowStatus: uint8_t {
+ Idle=0,
+ Ready=10,
+ Started=50,
+ Connected=60,
+ Received=80,
+ Complete=100,
+ Failed=101,
+} UpdateWorkflowStatus;
+
+typedef struct AutoUpdateOffer {
+ int version = RELEASE_VERSION;
+ char ssid[25] = "Fish Tank";
+ char password[25] = "Fish Tank";
+ IPAddress host = IPAddress(192,168,0,146);
+} AutoUpdateOffer;
+
+class AutoUpdater {
+ public:
+ AutoUpdateOffer current_version;
+
+ // For now, hardcode it all
+ String host_name = "brcac.com";
+ String path = "/firmware.bin";
+ int port = 80;
+
+ long fileSize = 0;
+
+ String _storedSSID = "";
+ String _storedPass = "";
+ String _storedAPSSID = "";
+ String _storedAPPass = "";
+
+ int progress = 0;
+
+ WiFiClient _client;
+ UpdateWorkflowStatus status = Idle;
+ Timer timeoutTimer;
+ Timer displayStatusTimer;
+
+ void update() {
+ switch (this->status) {
+ case Complete:
+ if (this->displayStatusTimer.ended()) {
+ doReboot = true;
+ this->status = Idle;
+ }
+ return;
+
+ case Failed:
+ if (this->displayStatusTimer.ended())
+ this->status = Idle;
+ return;
+
+ case Ready:
+ case Idle:
+ case Received:
+ return;
+
+ case Started:
+ this->do_connect();
+ return;
+
+ case Connected:
+ this->do_request();
+ return;
+ }
+ }
+
+ void start(AutoUpdateOffer *new_version = nullptr) {
+ if (this->status != Idle) {
+ log("update already in progress.");
+ return;
+ }
+
+ if (new_version && new_version->version <= current_version.version) {
+ log("don't need to update to that version.");
+ return;
+ }
+
+ // The auto-update process might break the current connection
+ _storedSSID = String(clientSSID);
+ _storedPass = String(clientPass);
+ _storedAPSSID = String(apSSID);
+ _storedAPPass = String(apPass);
+ otaLock = false;
+
+ if (new_version) {
+ memcpy((byte*)&this->current_version, new_version, sizeof(this->current_version));
+ }
+
+ log("starting autoupdate");
+ this->status = Started;
+ this->displayStatusTimer.stop();
+ }
+
+ void ready() {
+ log("ready for update - turning on updater AP");
+ strcpy(apSSID, "WLED-UPDATE");
+ strcpy(apPass, "update1234");
+ auto tmp = apBehavior;
+ apBehavior = AP_BEHAVIOR_ALWAYS;
+ WLED::instance().initAP();
+ apBehavior = tmp;
+ this->status = Ready;
+ }
+
+ void stop() {
+ this->_client.stop();
+ strcpy(clientSSID, _storedSSID.c_str());
+ strcpy(clientPass, _storedPass.c_str());
+ strcpy(apSSID, _storedAPSSID.c_str());
+ strcpy(apPass, _storedAPPass.c_str());
+ WiFi.softAPdisconnect(true);
+ apActive = false;
+ WiFi.disconnect(false, true);
+ WLED::instance().enableWatchdog();
+ this->status = Idle;
+ }
+
+ void handleOverlayDraw() {
+ CRGB c;
+ switch (this->status) {
+ case Ready:
+ // Once the updater connects, no need to show ready
+ if (WiFi.softAPgetStationNum())
+ return;
+ c = CRGB::Purple;
+ break;
+
+ case Started:
+ case Connected:
+ case Received:
+ c = CRGB::Yellow;
+ if (millis() % 1000 < 500) {
+ c = CRGB::Black;
+ }
+ break;
+
+ case Failed:
+ c = CRGB::Red;
+ break;
+
+ case Complete:
+ c = CRGB::Green;
+ break;
+
+ case Idle:
+ default:
+ return;
+ }
+ for (int i = 0; i < 10; i++) {
+ strip.setPixelColor(i, c);
+ }
+ }
+
+ private:
+ void log(const char *message) {
+ Serial.printf("OTA: %s\n", message);
+ }
+
+ void log(String message) {
+ log(message.c_str());
+ }
+
+ void abort(const char *message) {
+ log(message);
+ this->stop();
+ this->status = Failed;
+ this->displayStatusTimer.start(30000);
+ }
+
+ void do_connect() {
+ auto s = WiFi.status();
+ switch (s) {
+ case WL_DISCONNECTED:
+ if (!strlen(clientSSID)) {
+ log("connecting to autoupdate server");
+ strcpy(clientSSID, this->current_version.ssid);
+ strcpy(clientPass, this->current_version.password);
+ }
+ return;
+
+ case WL_NO_SSID_AVAIL:
+ abort("Invalid auto-update SSID");
+ return;
+
+ case WL_CONNECT_FAILED:
+ abort("Invalid auto-update password");
+ return;
+
+ case WL_CONNECTED:
+ if (WiFi.SSID() != String(this->current_version.ssid)) {
+ log("disconnecting from WiFi");
+ WiFi.softAPdisconnect(true);
+ WiFi.disconnect(false, true);
+ apBehavior = AP_BEHAVIOR_BUTTON_ONLY;
+ return;
+ }
+
+ log("WiFi connected");
+ this->status = Connected;
+ return;
+
+ case WL_IDLE_STATUS:
+ EVERY_N_MILLIS(300) {
+ Serial.print("...");
+ }
+ break;
+
+ default:
+ Serial.printf("OTA: wifi %d", (int)s);
+ break;
+ }
+ }
+
+ void do_request() {
+ WLED::instance().disableWatchdog();
+ log("connecting");
+ if (!this->_client.connect(this->host_name.c_str(), this->port)) { // this->current_version.host
+ abort("connect failed");
+ return;
+ }
+
+ // Get the contents of the bin file
+ log("requesting update package");
+ auto request = String("GET ") + this->path + " HTTP/1.1\r\n" +
+ "Host: " + this->host_name + "\r\n" +
+ "Cache-Control: no-cache\r\n\r\n";
+ Serial.println(request);
+ this->_client.print(request);
+
+ log("awaiting response");
+ timeoutTimer.start(5000);
+ while (!this->_client.available()) {
+ vTaskDelay( 400 );
+ if (timeoutTimer.ended()) {
+ abort("timed out waiting for response");
+ return;
+ }
+ log("waiting...");
+ }
+
+ String contentType = "";
+
+ log("reading response");
+ while (this->_client.available()) {
+ // read line till /n - if the line is empty, it's the end of the headers.
+ String line = this->_client.readStringUntil('\n');
+ line.trim();
+ if (!line.length()) break;
+
+ // Check if the HTTP Response is 200
+ if (line.startsWith("HTTP/")) {
+ if (line.indexOf("200") < 0) {
+ abort(line.c_str());
+ return;
+ }
+ }
+
+ // Read the file size from Content-Length
+ if (line.startsWith("Content-Length: ")) {
+ fileSize = atol((getHeaderValue(line, "Content-Length: ")).c_str());
+ log(line.c_str());
+ }
+
+ // Read the content type from Content-Type
+ if (line.startsWith("Content-Type: ")) {
+ contentType = getHeaderValue(line, "Content-Type: ");
+ log(line.c_str());
+ }
+ if (line.startsWith("Content-type: ")) {
+ contentType = getHeaderValue(line, "Content-type: ");
+ log(line.c_str());
+ }
+
+ }
+
+ if (fileSize == 0 || contentType != "application/octet-stream") {
+ abort("Must get a valid Content-Type and Content-Length header.");
+ return;
+ }
+
+ log("found a valid OTA BIN");
+
+ this->status = Received;
+ this->do_update(this->_client);
+ }
+
+ void do_update(WiFiClient client) {
+ if (!Update.begin(fileSize)) {
+ abort("no room for the update");
+ return;
+ };
+
+ WLED::instance().disableWatchdog();
+ this->progress = 0;
+ vTaskDelay(500);
+ uint8_t buf[4096];
+ int lr;
+ while ((lr = client.read(buf, sizeof(buf))) > 0) {
+ size_t written = Update.write(buf, lr);
+ if (!written)
+ break;
+
+ this->progress += written;
+ Serial.printf(" %d of %ld\n", this->progress, fileSize);
+
+ // Give the server time to send some more data
+ if (!client.available())
+ vTaskDelay(100);
+ }
+
+ if (!Update.end()) {
+ Serial.println("Error #: " + String(Update.getError()));
+ abort("Error during streaming");
+ return;
+ }
+
+ if (!Update.isFinished()) {
+ abort("Error during finishing");
+ return;
+ }
+
+ log("update successfully completed. Rebooting.");
+ this->stop();
+ this->status = Complete;
+ this->displayStatusTimer.start(10000);
+ }
+
+};
diff --git a/usermods/Tubes/util.h b/usermods/Tubes/util.h
new file mode 100644
index 0000000000..f0fc596f98
--- /dev/null
+++ b/usermods/Tubes/util.h
@@ -0,0 +1,18 @@
+#pragma once
+
+uint8_t scaled16to8( uint16_t v, uint16_t lowest=0, uint16_t highest=65535) {
+ uint16_t rangewidth = highest - lowest;
+ uint16_t scaledbeat = scale16( v, rangewidth );
+ uint16_t result = lowest + scaledbeat;
+ return result;
+}
+
+#define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0]))
+
+uint32_t freeMemory() {
+ return ESP.getFreeHeap();
+}
+
+// #define __ESP32__
+// #define USTD_OPTION_FS_FORCE_NO_FS
+// #include
\ No newline at end of file
diff --git a/usermods/Tubes/virtual_strip.h b/usermods/Tubes/virtual_strip.h
new file mode 100644
index 0000000000..4e4232d18d
--- /dev/null
+++ b/usermods/Tubes/virtual_strip.h
@@ -0,0 +1,197 @@
+#pragma once
+
+#include "util.h"
+#include "options.h"
+#include "beats.h"
+#include "palettes.h"
+#include "wled.h"
+
+#define DEFAULT_FADE_SPEED 100
+#define MAX_VIRTUAL_LEDS 500
+
+#define DEFAULT_WLED_FX FX_MODE_RAINBOW_CYCLE
+
+class VirtualStrip;
+typedef void (*BackgroundFn)(VirtualStrip *strip);
+
+class Background {
+ public:
+ BackgroundFn animate {nullptr};
+ uint8_t wled_fx_id {0};
+ uint8_t palette_id {0};
+ SyncMode sync {All};
+};
+
+typedef enum VirtualStripFade {
+ Steady=0,
+ FadeIn=1,
+ FadeOut=2,
+ Dead=99,
+} VirtualStripFade;
+
+BeatFrame_24_8 swing(BeatFrame_24_8 frame) {
+ uint16_t fr = (frame & 0x3FF); // grab 4 beats
+ if (fr < 256)
+ fr = ease8InOutApprox(fr) << 2;
+ else
+ fr = 0x3FF;
+
+ return (frame & 0xFC00) + fr; // recompose it
+}
+
+class VirtualStrip {
+ // Let WLED do the dimming
+ const static uint16_t DEF_BRIGHT = 255;
+
+ public:
+ CRGB leds[MAX_VIRTUAL_LEDS];
+ uint16_t num_leds = 1; // only temporary until the first loop
+ uint8_t brightness;
+
+ // Fade in/out
+ VirtualStripFade fade;
+ uint16_t fader;
+ uint8_t fade_speed;
+
+ // Pattern parameters
+ Background background;
+ uint32_t frame;
+ uint8_t beat;
+ uint16_t beat16; // 8 bits of beat and 8 bits of fractional
+ uint8_t hue;
+ bool beat_pulse;
+ int bps = 0;
+
+ VirtualStrip()
+ {
+ fade = Dead;
+ }
+
+
+ void load(Background &b, uint8_t fs=DEFAULT_FADE_SPEED)
+ {
+ background = b;
+ fade = FadeIn;
+ fader = 0;
+ fade_speed = fs;
+ brightness = DEF_BRIGHT;
+ }
+
+ bool isWled() const {
+ return background.wled_fx_id != 0;
+ }
+
+ void fadeOut(uint8_t fs=DEFAULT_FADE_SPEED)
+ {
+ if (fade == Dead)
+ return;
+ fade = FadeOut;
+ fade_speed = fs;
+ }
+
+ void darken(uint8_t amount=10)
+ {
+ fadeToBlackBy( leds, num_leds, amount);
+ }
+
+ void fill(CRGB crgb)
+ {
+ fill_solid( leds, num_leds, crgb);
+ }
+
+ void update(BeatFrame_24_8 fr, uint8_t bp)
+ {
+ if (fade == Dead)
+ return;
+
+ frame = fr;
+
+ // Try to keep our number of LEDs as the same as the main segment,
+ // but not if it's too big for the buffer array.
+ auto len = strip.getMainSegment().length();
+ if (len > MAX_VIRTUAL_LEDS)
+ len = MAX_VIRTUAL_LEDS;
+ num_leds = len;
+
+ switch (this->background.sync) {
+ case All:
+ break;
+
+ case SinDrift:
+ // Drift slightly
+ frame = frame + (beatsin16( 5 ) >> 6);
+ break;
+
+ case Swing:
+ // Swing the beat
+ frame = swing(frame);
+ break;
+
+ case SwingDrift:
+ // Swing the beat AND drift slightly
+ frame = swing(frame) + (beatsin16( 5 ) >> 6);
+ break;
+
+ case Pulse:
+ // Pulsing from 30 - 210 brightness
+ brightness = scale8(beatsin8( 10 ), 180) + 30;
+ break;
+ }
+ hue = (frame >> 4) % 256;
+ beat = (frame >> 8) % 16;
+ beat_pulse = bp;
+
+ // Animate this virtual strip
+ background.animate(this);
+
+ switch (fade) {
+ case Steady:
+ case Dead:
+ break;
+
+ case FadeIn:
+ if (65535 - fader < fade_speed) {
+ fader = 65535;
+ fade = Steady;
+ } else {
+ fader += fade_speed;
+ }
+ break;
+
+ case FadeOut:
+ if (fader < fade_speed) {
+ fader = 0;
+ fade = Dead;
+ return;
+ } else {
+ fader -= fade_speed;
+ }
+ break;
+ }
+ }
+
+ CRGB palette_color(uint8_t c, uint8_t offset=0, uint8_t brightness=255) const {
+ Segment& segment = strip.getMainSegment();
+ uint32_t color = segment.color_from_palette(c + offset, false, true, 255, brightness);
+ return CRGB(color);
+ }
+
+ CRGB hue_color(uint8_t offset=0, uint8_t saturation=255, uint8_t value=192) const {
+ return CHSV(hue + offset, saturation, value);
+ }
+
+ uint8_t bpm_sin16( uint16_t lowest=0, uint16_t highest=65535 ) const
+ {
+ return scaled16to8(sin16( frame << 7 ) + 32768, lowest, highest);
+ }
+
+ uint8_t bpm_cos16( uint16_t lowest=0, uint16_t highest=65535 ) const
+ {
+ return scaled16to8(cos16( frame << 7 ) + 32768, lowest, highest);
+ }
+
+ CRGB getPixelColor(int32_t pos) const {
+ return leds[pos % num_leds];
+ }
+
+};
diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h
index fec0525ec7..8041879a0d 100644
--- a/usermods/audioreactive/audio_reactive.h
+++ b/usermods/audioreactive/audio_reactive.h
@@ -300,7 +300,7 @@ void FFTcode(void * parameter)
#endif
#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT
- FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant
+ FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant
#else
FFT.MajorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant
#endif
diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp
index f9cf3e1e73..a96ba11411 100644
--- a/wled00/FX_fcn.cpp
+++ b/wled00/FX_fcn.cpp
@@ -1831,11 +1831,17 @@ WS2812FX* WS2812FX::instance = nullptr;
const char JSON_mode_names[] PROGMEM = R"=====(["FX names moved"])=====";
const char JSON_palette_names[] PROGMEM = R"=====([
"Default","* Random Cycle","* Color 1","* Colors 1&2","* Color Gradient","* Colors Only","Party","Cloud","Lava","Ocean",
-"Forest","Rainbow","Rainbow Bands","Sunset","Rivendell","Breeze","Red & Blue","Yellowout","Analogous","Splash",
-"Pastel","Sunset 2","Beach","Vintage","Departure","Landscape","Beech","Sherbet","Hult","Hult 64",
-"Drywet","Jul","Grintage","Rewhi","Tertiary","Fire","Icefire","Cyane","Light Pink","Autumn",
-"Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura",
-"Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf",
-"Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide",
-"Candy2"
+"Forest","Rainbow","Rainbow Bands","July","Vintage 57","Vintage 01","Rivendell","RGI 15","Retro","Analogous",
+"Pink Splash 08","Pink Splash 07","Coral Reef","Ocean Breeze 68","Ocean Breeze 36","Departure","Landscape 64","Landscape 33","Sherbet","Hult 65",
+"Hult 64","Drywet","IB15","Fuschia","Emerald Dragon 08","Hot Lava","Fire","Hiyane","Colorfull","Magenta Evening",
+"Pink Purple","Sunset","Autumn","Blue/Magenta/White","Blue/Magenta/Red","Blue/Red/Yellow","Blue/Cyan/Yellow","Sunset Yellow","Cloud","Fire & Ice",
+"BHW2","Rainfall","Angel","Butterfly","250K Meters","Night Midnight","Afterdusk","Blue Sky","Gold Orange", "Frizell 10",
+"Frizell 12","Fib 18","Fib 13","Fib 17","Fib 05","Analogous 02","Analogous 04a","Cyan Orange","C/W/G","Wild Orange",
+"Ikat Radial","Citrus","Teal Blue","Ldby Orange","Purple/Orange","Blue/Tan","Green/Purple","Knoza 00","Knoza 18","Calpan",
+"Calbayo","Fib53","Purple/Orange","PMH","Konjo 08","Konikyo","McCahon","Pills","Pink/Yellow/Orange","Autumn 04",
+"Autumn 02","Candide","Chic","Coffee","Emerald Dragon 01","Landscape 57","Landscape 22","Landscape 47","Landscape 10","Landscape 76",
+"Landscape 61","Landscape 60","Landscape 51","Landscape 06","Ocean Breeze 49","Ocean Breeze 57","Ocean Breeze 74","Pink Splash 05","Pink Splash 10","Vintage 56",
+"Vintage 10","Gold/Yellow","Radioactive Slime","Pastel Rainbow","Purple Sunset","Adrift in Dreams","Set3","Pastel1","Es Rosa","Daybreak",
+"Melancholiy","Xanidu","Air","Revolution","Sky05","Sky33","Sky45","Carousel","NRWC"
])=====";
+
diff --git a/wled00/button.cpp b/wled00/button.cpp
index c1d0337667..efc2af8b66 100644
--- a/wled00/button.cpp
+++ b/wled00/button.cpp
@@ -8,8 +8,10 @@
#define WLED_LONG_PRESS 600 // long press if button is released after held for at least 600ms
#define WLED_DOUBLE_PRESS 350 // double press if another press within 350ms after a short press
#define WLED_LONG_REPEATED_ACTION 300 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0
+#define WLED_LONG_POWER_SAVE 1000 // how long button 0 needs to be held to DEactivate power saving mode (toggle it)
#define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP
-#define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset
+// SteveE: geez you can't put factory reset so close to "just turn on the AP"
+#define WLED_LONG_FACTORY_RESET 30000 // how long button 0 needs to be held to trigger a factory reset
static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage
@@ -17,7 +19,7 @@ void shortPressAction(uint8_t b)
{
if (!macroButton[b]) {
switch (b) {
- case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
+ case 0: toggleOnOff(); usermods.handleButton(101); stateUpdated(CALL_MODE_BUTTON); break;
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
@@ -292,19 +294,24 @@ void handleButton()
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
buttonWaitTime[b] = 0;
- if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
+ if (b == 0 && dur > WLED_LONG_POWER_SAVE) { // long press on button 0 (when released)
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
WLED_FS.format();
#ifdef WLED_ADD_EEPROM_SUPPORT
clearEEPROM();
#endif
doReboot = true;
- } else {
+ } else if (dur > WLED_LONG_AP) {
WLED::instance().initAP(true);
+ } else {
+ usermods.handleButton(100);
}
} else if (!buttonLongPressed[b]) { //short press
+ if (b == 0 && doublePress) {
+ usermods.handleButton(102);
+ }
//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 && !macroDoublePress[b]) { //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) {
diff --git a/wled00/const.h b/wled00/const.h
index 388b64c820..b493782e90 100644
--- a/wled00/const.h
+++ b/wled00/const.h
@@ -5,7 +5,7 @@
* Readability defines and their associated numerical values + compile-time constants
*/
-#define GRADIENT_PALETTE_COUNT 58
+#define GRADIENT_PALETTE_COUNT 116 // custom palette.h
//Defaults
#define DEFAULT_CLIENT_SSID "Your_Network"
@@ -152,6 +152,7 @@
#define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h"
#define USERMOD_ID_LDR_DUSK_DAWN 43 //Usermod "usermod_LDR_Dusk_Dawn_v2.h"
#define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h"
+#define USERMOD_ID_TUBES 45 //Usermod "usermod_tubes.h"
//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
diff --git a/wled00/espnow_broadcast.cpp b/wled00/espnow_broadcast.cpp
new file mode 100644
index 0000000000..c0c789f85e
--- /dev/null
+++ b/wled00/espnow_broadcast.cpp
@@ -0,0 +1,520 @@
+
+#ifndef WLED_DISABLE_ESPNOW_NEW
+#include
+#include
+#include
+#if defined ESP32
+#include
+#include
+#include
+
+#elif defined ESP8266
+#include
+#define WIFI_MODE_STA WIFI_STA
+#else
+#error "Unsupported platform"
+#endif //ESP32
+
+#include "espnow_broadcast.h"
+
+#ifdef ESP32
+
+#include
+#include
+#include
+
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 0, 0)
+// Legacy Event Loop
+ESP_EVENT_DEFINE_BASE(SYSTEM_EVENT);
+#define WIFI_EVENT SYSTEM_EVENT
+#define WIFI_EVENT_STA_START SYSTEM_EVENT_STA_START
+#define WIFI_EVENT_STA_STOP SYSTEM_EVENT_STA_STOP
+#define WIFI_EVENT_AP_START SYSTEM_EVENT_AP_START
+#endif
+
+//#define ESPNOW_DEBUGGING
+//#define ESPNOW_CALLBACK_DEBUGGING // Serial is called from multiple threads
+//#define ESPNOW_EVENT_DEBUGGING
+//#define ESPNOW_DEBUG_COUNTERS
+
+#define BROADCAST_ADDR_ARRAY_INITIALIZER {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
+#define WLED_ESPNOW_WIFI_CHANNEL 1
+
+#ifndef WLED_WIFI_POWER_SETTING
+#define WLED_WIFI_POWER_SETTING WIFI_POWER_18_5dBm
+#endif
+
+typedef struct {
+ uint8_t mac[6];
+ uint8_t len;
+ int8_t rssi;
+ uint8_t data[WLED_ESPNOW_MAX_MESSAGE_LENGTH];
+} QueuedNetworkMessage;
+static_assert(sizeof(QueuedNetworkMessage) == WLED_ESPNOW_MAX_MESSAGE_LENGTH+8, "QueuedNetworkMessage larger than needed");
+static_assert(WLED_ESPNOW_MAX_MESSAGE_LENGTH <= ESP_NOW_MAX_DATA_LEN, "WLED_ESPNOW_MAX_MESSAGE_LENGTH must be <= 250 bytes");
+
+
+class ESPNOWBroadcastImpl : public ESPNOWBroadcast {
+
+ friend ESPNOWBroadcast;
+
+ std::atomic _state {STOPPED};
+ STATE getState() const {
+ return _state.load();
+ }
+
+ bool setupWiFi();
+
+ void start();
+
+ static esp_err_t onSystemEvent(void *ctx, system_event_t *event);
+
+ static void onWiFiEvent(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
+
+ static void onESPNowRxCallback(const uint8_t *mac_addr, const uint8_t *data, int len);
+
+ receive_filter_t _rxFilter = nullptr;
+
+#ifdef ESPNOW_DEBUG_COUNTERS
+ std::atomic _received {0};
+ std::atomic _processed {0};
+ std::atomic _loop {0};
+ std::atomic _exit {0};
+#endif
+
+ class QueuedNetworkRingBuffer {
+ protected:
+ //QueuedNetworkMessage messages[WLED_ESPNOW_MAX_QUEUED_MESSAGES];
+ RingbufHandle_t buf = nullptr;
+
+ public:
+ QueuedNetworkRingBuffer() {
+ buf = xRingbufferCreateNoSplit(sizeof(QueuedNetworkMessage), WLED_ESPNOW_MAX_QUEUED_MESSAGES);
+ }
+
+ bool push(const uint8_t* mac, const uint8_t* data, uint8_t len, int8_t rssi);
+
+ QueuedNetworkMessage* pop() {
+ size_t size = 0;
+ return (QueuedNetworkMessage*)xRingbufferReceive(buf, &size, 0);
+ }
+
+ void popComplete(QueuedNetworkMessage* msg) {
+ vRingbufferReturnItem(buf, (void *)msg);
+ }
+ };
+
+ QueuedNetworkRingBuffer queuedNetworkRingBuffer {};
+
+};
+
+ESPNOWBroadcastImpl espnowBroadcastImpl {};
+#endif // ESP32
+
+ESPNOWBroadcast espnowBroadcast {};
+
+
+ESPNOWBroadcast::STATE ESPNOWBroadcast::getState() const {
+#ifdef ESP32
+ return espnowBroadcastImpl.getState();
+#else
+ return ESPNOWBroadcast::STOPPED;
+#endif
+}
+
+bool ESPNOWBroadcast::setup() {
+
+ static bool setup = false;
+#ifdef ESP32
+ if (setup) {
+ return true;
+ }
+
+ #ifdef ESPNOW_DEBUGGING
+ delay(2000);
+ #endif
+
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 0, 0)
+ tcpip_adapter_init();
+ esp_event_loop_init(ESPNOWBroadcastImpl::onSystemEvent, nullptr);
+#else
+
+ auto err = esp_event_loop_create_default();
+ if ( ESP_OK != err && ESP_ERR_INVALID_STATE != err ) {
+ Serial.printf("esp_event_loop_create_default() err %d\n", err);
+ return false;
+ }
+
+ #ifdef ESPNOW_EVENT_DEBUGGING
+ err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, ESPNOWBroadcastImpl::onWiFiEvent, nullptr, nullptr);
+ if ( ESP_OK != err ) {
+ Serial.printf("esp_event_handler_instance_register(ESP_EVENT_ANY_ID) err %d\n", err);
+ return false;
+ }
+ #else
+ err = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, ESPNOWBroadcastImpl::onWiFiEvent, nullptr, nullptr);
+ if ( ESP_OK != err ) {
+ Serial.printf("esp_event_handler_instance_register(WIFI_EVENT_STA_START) err %d\n", err);
+ return false;
+ }
+ err = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_STOP, ESPNOWBroadcastImpl::onWiFiEvent, nullptr, nullptr);
+ if ( ESP_OK != err ) {
+ Serial.printf("esp_event_handler_instance_register(WIFI_EVENT_STA_STOP) err %d\n", err);
+ return false;
+ }
+ err = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_AP_START, ESPNOWBroadcastImpl::onWiFiEvent, nullptr, nullptr);
+ if ( ESP_OK != err ) {
+ Serial.printf("esp_event_handler_instance_register(WIFI_EVENT_AP_START) err %d\n", err);
+ return false;
+ }
+ #endif // ESPNOW_EVENT_DEBUGGING
+#endif // ESP_IDF_VERSION
+
+ setup = espnowBroadcastImpl.setupWiFi();
+#endif //ESP32
+ return setup;
+}
+
+#ifdef ESP32
+bool ESPNOWBroadcastImpl::setupWiFi() {
+ Serial.println("ESPNOWBroadcast::setupWiFi()");
+
+ _state.exchange(STOPPED);
+
+ // To enable ESPNow, we need to be in WIFI_STA mode
+ if ( !WiFi.mode(WIFI_STA) ) {
+ Serial.println("WiFi.mode() failed");
+ return false;
+ }
+ // and not have the WiFi connect
+ // Calling discount with tigger an async Wifi Event
+ if ( !WiFi.disconnect(false, true) ) {
+ Serial.println("WiFi.disconnect() failed");
+ return false;
+ }
+
+ yield();
+ return true;
+}
+#endif //ESP32
+
+
+void ESPNOWBroadcast::loop(size_t maxMessagesToProcess /*= 1*/) {
+#ifdef ESP32
+#ifdef ESPNOW_DEBUG_COUNTERS
+ espnowBroadcastImpl._loop++;
+#endif
+ switch (espnowBroadcastImpl._state.load()) {
+ case ESPNOWBroadcast::STARTING:
+ // if WiFI is in starting state, actually stat ESPNow from our main task thread.
+ espnowBroadcastImpl.start();
+ break;
+ case ESPNOWBroadcast::STARTED: {
+ while(maxMessagesToProcess-- > 0) {
+ auto *msg = espnowBroadcastImpl.queuedNetworkRingBuffer.pop();
+ if (msg) {
+#ifdef ESPNOW_DEBUG_COUNTERS
+ espnowBroadcastImpl._processed++;
+#endif
+ for( auto ndx = 0; ndx < (sizeof(_rxCallbacks)/sizeof(_rxCallbacks[0]))-1; ndx++) {
+ if(_rxCallbacks[ndx]) {
+ _rxCallbacks[ndx](msg->mac, msg->data, msg->len, msg->rssi);
+ }
+ }
+ espnowBroadcastImpl.queuedNetworkRingBuffer.popComplete(msg);
+ } else {
+ break;
+ }
+ yield();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+#ifdef ESPNOW_DEBUG_COUNTERS
+ espnowBroadcastImpl._exit++;
+#endif
+#endif // ESP32
+}
+
+bool ESPNOWBroadcast::send(const uint8_t* msg, size_t len) {
+#ifdef ESP32
+ static const uint8_t broadcast[] = BROADCAST_ADDR_ARRAY_INITIALIZER;
+ auto err = esp_now_send(broadcast, msg, len);
+#ifdef ESPNOW_DEBUGGING
+ if (ESP_OK != err) {
+ Serial.printf( "esp_now_send() failed %d\n", err);
+ }
+#endif
+ return ESP_OK == err;
+#else
+ return false;
+#endif
+}
+
+bool ESPNOWBroadcast::registerCallback( ESPNOWBroadcast::receive_callback_t callback ) {
+ // last element is always null
+ size_t ndx;
+ for (ndx = 0; ndx < _rxCallbacksSize-1; ndx++) {
+ // already registered
+ if (callback == _rxCallbacks[ndx]) {
+ break;
+ }
+ if (nullptr == _rxCallbacks[ndx]) {
+ _rxCallbacks[ndx] = callback;
+ break;
+ }
+ }
+ return ndx < _rxCallbacksSize;
+}
+
+bool ESPNOWBroadcast::removeCallback( ESPNOWBroadcast::receive_callback_t callback ) {
+ size_t ndx;
+ for (ndx = 0; ndx < _rxCallbacksSize-1; ndx++) {
+ if (_rxCallbacks[ndx] == callback ) {
+ break;
+ }
+ }
+
+ for (; ndx < _rxCallbacksSize-1; ndx++) {
+ _rxCallbacks[ndx] = _rxCallbacks[ndx+1];
+ }
+
+ return ndx < _rxCallbacksSize;
+
+}
+
+ESPNOWBroadcast::receive_filter_t ESPNOWBroadcast::registerFilter( ESPNOWBroadcast::receive_filter_t filter ) {
+#ifdef ESP32
+ auto old = espnowBroadcastImpl._rxFilter;
+ espnowBroadcastImpl._rxFilter = filter;
+ return old;
+#else
+ return nullptr;
+#endif
+}
+
+
+#ifdef ESP32
+
+void ESPNOWBroadcastImpl::start() {
+
+ Serial.println("starting ESPNow");
+
+ if ( WiFi.mode(WIFI_STA) ) {
+ auto status = WiFi.status();
+ if ( status >= WL_DISCONNECTED ) {
+ if (esp_wifi_start() == ESP_OK) {
+ yield();
+ if (!WiFi.setTxPower(WLED_WIFI_POWER_SETTING)) {
+ auto power = WiFi.getTxPower();
+ Serial.printf("setTxPower(%d) failed. getTX: %d\n", WLED_WIFI_POWER_SETTING, power);
+ }
+ if (esp_now_init() == ESP_OK) {
+ yield();
+ if (esp_now_register_recv_cb(ESPNOWBroadcastImpl::onESPNowRxCallback) == ESP_OK) {
+ static esp_now_peer_info_t peer = {
+ BROADCAST_ADDR_ARRAY_INITIALIZER,
+ {0},
+ WLED_ESPNOW_WIFI_CHANNEL,
+ WIFI_IF_STA,
+ false,
+ NULL
+ };
+
+ if (esp_now_add_peer(&peer) == ESP_OK) {
+ ESPNOWBroadcast::STATE starting {ESPNOWBroadcast::STARTING};
+ if (_state.compare_exchange_strong(starting, ESPNOWBroadcast::STARTED)) {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("ESPNOWBroadcast started :)");
+#endif
+ return;
+ } else {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("atomic state out of sync");
+#endif
+ }
+ } else {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("esp_now_add_peer failed");
+#endif
+ }
+ } else {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("esp_now_register_recv_cb failed");
+#endif
+ }
+ } else {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("esp_now_init_init failed");
+#endif
+ }
+ } else {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("esp_wifi_start failed");
+#endif
+ }
+ } else {
+#ifdef ESPNOW_DEBUGGING
+ Serial.printf("WiFi.status not disconnected - %d\n", status);
+#endif
+ }
+ } else {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("WiFi.mode failed");
+#endif
+ }
+ Serial.println("restarting ESPNow");
+ setupWiFi();
+}
+
+esp_err_t ESPNOWBroadcastImpl::onSystemEvent(void *ctx, system_event_t *event) {
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 0, 0)
+ onWiFiEvent(ctx, SYSTEM_EVENT, event->event_id, nullptr );
+#endif
+ return ESP_OK;
+}
+
+
+void ESPNOWBroadcastImpl::onWiFiEvent(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
+ if ( event_base == WIFI_EVENT ) {
+
+#ifdef ESPNOW_DEBUGGING
+ #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 4)
+ Serial.printf("WiFiEvent( %d )\n", event_id );
+ #else
+ Serial.printf("WiFiEvent %d ( %s )\n", event_id, WiFi.eventName((arduino_event_id_t)event_id) );
+ #endif
+#endif
+ switch (event_id) {
+ case WIFI_EVENT_STA_START: {
+ ESPNOWBroadcast::STATE stopped {ESPNOWBroadcast::STOPPED};
+ espnowBroadcastImpl._state.compare_exchange_strong(stopped, ESPNOWBroadcast::STARTING);
+ break;
+ }
+
+ case WIFI_EVENT_STA_STOP:
+ lastReconnectAttempt = 0;
+ // fall thru
+ case WIFI_EVENT_AP_START: {
+ ESPNOWBroadcast::STATE started {ESPNOWBroadcast::STARTED};
+ ESPNOWBroadcast::STATE starting {ESPNOWBroadcast::STARTING};
+ if (espnowBroadcastImpl._state.compare_exchange_strong(started, ESPNOWBroadcast::STOPPED) ||
+ espnowBroadcastImpl._state.compare_exchange_strong(starting, ESPNOWBroadcast::STOPPED)) {
+#ifdef ESPNOW_DEBUGGING
+ Serial.println("WiFi connected: stop broadcasting");
+#endif
+ esp_now_unregister_recv_cb();
+ esp_now_deinit();
+ }
+ break;
+ }
+ }
+ }
+}
+
+typedef struct {
+ uint16_t frame_head;
+ uint16_t duration;
+ uint8_t destination_address[6];
+ uint8_t source_address[6];
+ uint8_t broadcast_address[6];
+ uint16_t sequence_control;
+
+ uint8_t category_code;
+ uint8_t organization_identifier[3]; // 0x18fe34
+ uint8_t random_values[4];
+ struct {
+ uint8_t element_id; // 0xdd
+ uint8_t lenght; //
+ uint8_t organization_identifier[3]; // 0x18fe34
+ uint8_t type; // 4
+ uint8_t version;
+ uint8_t body[0];
+ } vendor_specific_content;
+} __attribute__ ((packed)) espnow_frame_format_t;
+
+void ESPNOWBroadcastImpl::onESPNowRxCallback(const uint8_t *mac, const uint8_t *data, int len) {
+ //const espnow_frame_format_t* espnow_data = (espnow_frame_format_t*)(data - sizeof (espnow_frame_format_t));
+ const wifi_promiscuous_pkt_t* promiscuous_pkt = (wifi_promiscuous_pkt_t*)(data - sizeof (wifi_pkt_rx_ctrl_t) - sizeof (espnow_frame_format_t));
+ int8_t rssi = 0;
+ try {
+ auto rssi32 = promiscuous_pkt->rx_ctrl.rssi;
+ rssi = rssi32 <= -128 ? -127 : rssi32 > 0 ? 0 : rssi32;
+ } catch(...) {
+ // be safe about accessing memory that isn't directly exposed to the callback
+ rssi = 0;
+ }
+
+ if (espnowBroadcastImpl._rxFilter) {
+ if (!espnowBroadcastImpl._rxFilter(mac, data, len, rssi)) {
+#ifdef ESPNOW_CALLBACK_DEBUGGING
+ Serial.printf("Filtering message of len %d\n", len);
+#endif
+ return;
+ }
+ }
+
+#ifdef ESPNOW_DEBUG_COUNTERS
+ espnowBroadcastImpl._received++;
+#endif
+ if(!espnowBroadcastImpl.queuedNetworkRingBuffer.push(mac, data, len, rssi)) {
+ Serial.printf("Failed to queue message (%d bytes) to ring buffer. Dropping message\n"
+#ifdef ESPNOW_DEBUG_COUNTERS
+ "\tState: %d\t loop:0x%x\t exit:0x%x recv:0x%x\t processed:0x%x\n"
+#endif
+ , len
+#ifdef ESPNOW_DEBUG_COUNTERS
+ , espnowBroadcastImpl.getState(),
+ espnowBroadcastImpl._loop.load(),
+ espnowBroadcastImpl._exit.load(),
+ espnowBroadcastImpl._received.load(),
+ espnowBroadcastImpl._processed.load()
+#endif
+ );
+ } else {
+#ifdef ESPNOW_CALLBACK_DEBUGGING
+ char buf[128];
+ sprintf(buf, "Received %d bytes from %x:%x:%x:%x:%x:%x RSSI %d", len,
+ mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
+ rssi
+ );
+ Serial.println(buf);
+#endif
+ }
+}
+
+bool ESPNOWBroadcastImpl::QueuedNetworkRingBuffer::push(const uint8_t* mac, const uint8_t* data, uint8_t len, int8_t rssi) {
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 0, 0)
+ QueuedNetworkMessage msg[1];
+ if (len <= sizeof(msg->data)) {
+ memcpy(msg->mac, mac, sizeof(msg->mac));
+ memcpy(&(msg->data), data, len);
+ msg->len = len;
+ msg->rssi = rssi;
+
+ if (pdTRUE == xRingbufferSend(buf, (void**)&msg, sizeof(*msg), 0)) {
+ return true;
+ }
+ }
+ return false;
+#else
+ QueuedNetworkMessage* msg = nullptr;
+ if (len <= sizeof(msg->data)) {
+ if (pdTRUE == xRingbufferSendAcquire(buf, (void**)&msg, sizeof(*msg), 0)) {
+ memcpy(msg->mac, mac, sizeof(msg->mac));
+ memcpy(&(msg->data), data, len);
+ msg->len = len;
+ msg->rssi = rssi;
+ xRingbufferSendComplete(buf, msg);
+ return true;
+ }
+ }
+ return false;
+#endif
+}
+
+#endif // ESP32
+
+#endif
\ No newline at end of file
diff --git a/wled00/espnow_broadcast.h b/wled00/espnow_broadcast.h
new file mode 100644
index 0000000000..d14786065f
--- /dev/null
+++ b/wled00/espnow_broadcast.h
@@ -0,0 +1,65 @@
+
+#pragma once
+
+#ifndef WLED_DISABLE_ESPNOW_NEW
+
+//#include "const.h"
+
+#ifndef WLED_ESPNOW_MAX_QUEUED_MESSAGES
+#define WLED_ESPNOW_MAX_QUEUED_MESSAGES 6
+#endif
+
+#ifndef WLED_ESPNOW_MAX_MESSAGE_LENGTH
+#define WLED_ESPNOW_MAX_MESSAGE_LENGTH 250
+#endif
+
+#ifndef WLED_ESPNOW_MAX_REGISTERED_CALLBACKS
+ #ifndef WLED_MAX_USERMODS
+ #define WLED_MAX_USERMODS 1
+ #endif
+#define WLED_ESPNOW_MAX_REGISTERED_CALLBACKS WLED_MAX_USERMODS+1
+#endif
+
+class ESPNOWBroadcast {
+
+ public:
+
+ bool setup();
+
+ void loop(size_t maxMessagesToProcess = WLED_ESPNOW_MAX_QUEUED_MESSAGES);
+
+ bool send(const uint8_t* msg, size_t len);
+
+ typedef void (*receive_callback_t)(const uint8_t *sender, const uint8_t *data, uint8_t len, int8_t rssi);
+ bool registerCallback( receive_callback_t callback );
+ bool removeCallback( receive_callback_t callback );
+
+ // the receive filter happens from the WiFi task and this should not do any lengthly processing
+ // additionally thread synchronization may need to be done with the main application task thread
+ // only a single filter may be register at a given time
+ // The filter should return false if the received network message should be ignored and not
+ // processed by future callbacks
+ typedef bool (*receive_filter_t)(const uint8_t *sender, const uint8_t *data, uint8_t len, int8_t rssi);
+ receive_filter_t registerFilter( receive_filter_t filter = nullptr );
+
+
+ enum STATE {
+ STOPPED = 0,
+ STARTING,
+ STARTED,
+ MAX
+ };
+
+ STATE getState() const;
+
+ bool isStarted() const {
+ return STATE::STARTED == getState();
+ }
+
+ protected:
+ receive_callback_t _rxCallbacks[WLED_ESPNOW_MAX_REGISTERED_CALLBACKS] = {0};
+ static constexpr size_t _rxCallbacksSize = sizeof(_rxCallbacks)/sizeof(_rxCallbacks[0]);
+};
+
+extern ESPNOWBroadcast espnowBroadcast;
+#endif
\ No newline at end of file
diff --git a/wled00/palettes.h b/wled00/palettes.h
index 41dfbbc163..16f26b87dd 100644
--- a/wled00/palettes.h
+++ b/wled00/palettes.h
@@ -13,7 +13,17 @@
#ifndef PalettesWLED_h
#define PalettesWLED_h
-const byte ib_jul01_gp[] PROGMEM = {
+// Redefine FastLED's gradient palette declaration:
+#define DEFINE_PALETTE(X) const byte X[] PROGMEM =
+
+#define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0]))
+
+// Gradient palette "ib_jul01_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/xmas/tn/ib_jul01.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 16 bytes of program space.
+
+DEFINE_PALETTE(ib_jul01_gp) {
0, 194, 1, 1,
94, 1, 29, 18,
132, 57,131, 28,
@@ -24,20 +34,19 @@ const byte ib_jul01_gp[] PROGMEM = {
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 20 bytes of program space.
-const byte es_vintage_57_gp[] PROGMEM = {
+DEFINE_PALETTE( es_vintage_57_gp ) {
0, 2, 1, 1,
53, 18, 1, 0,
104, 69, 29, 1,
153, 167,135, 10,
255, 46, 56, 4};
-
// Gradient palette "es_vintage_01_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_01.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 32 bytes of program space.
-const byte es_vintage_01_gp[] PROGMEM = {
+DEFINE_PALETTE( es_vintage_01_gp ) {
0, 4, 1, 1,
51, 16, 0, 1,
76, 97,104, 3,
@@ -47,92 +56,124 @@ const byte es_vintage_01_gp[] PROGMEM = {
229, 4, 1, 1,
255, 4, 1, 1};
-
// Gradient palette "es_rivendell_15_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/rivendell/tn/es_rivendell_15.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 20 bytes of program space.
-const byte es_rivendell_15_gp[] PROGMEM = {
+DEFINE_PALETTE( es_rivendell_15_gp ) {
0, 1, 14, 5,
101, 16, 36, 14,
165, 56, 68, 30,
242, 150,156, 99,
255, 150,156, 99};
-
// Gradient palette "rgi_15_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ds/rgi/tn/rgi_15.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 36 bytes of program space.
-// Edited to be brighter
-
-const byte rgi_15_gp[] PROGMEM = {
- 0, 4, 1, 70,
- 31, 55, 1, 30,
- 63, 255, 4, 7,
- 95, 59, 2, 29,
- 127, 11, 3, 50,
- 159, 39, 8, 60,
- 191, 112, 19, 40,
- 223, 78, 11, 39,
- 255, 29, 8, 59};
+DEFINE_PALETTE( rgi_15_gp ) {
+ 0, 4, 1, 31,
+ 31, 55, 1, 16,
+ 63, 197, 3, 7,
+ 95, 59, 2, 17,
+ 127, 6, 2, 34,
+ 159, 39, 6, 33,
+ 191, 112, 13, 32,
+ 223, 56, 9, 35,
+ 255, 22, 6, 38};
// Gradient palette "retro2_16_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/retro2/tn/retro2_16.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 8 bytes of program space.
-const byte retro2_16_gp[] PROGMEM = {
+DEFINE_PALETTE( retro2_16_gp ) {
0, 188,135, 1,
255, 46, 7, 1};
-
// Gradient palette "Analogous_1_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/red/tn/Analogous_1.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 20 bytes of program space.
-const byte Analogous_1_gp[] PROGMEM = {
+DEFINE_PALETTE( Analogous_1_gp ) {
0, 3, 0,255,
63, 23, 0,255,
127, 67, 0,255,
191, 142, 0, 45,
255, 255, 0, 0};
-
// Gradient palette "es_pinksplash_08_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/pink_splash/tn/es_pinksplash_08.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 20 bytes of program space.
-const byte es_pinksplash_08_gp[] PROGMEM = {
+DEFINE_PALETTE( es_pinksplash_08_gp ) {
0, 126, 11,255,
127, 197, 1, 22,
175, 210,157,172,
221, 157, 3,112,
255, 157, 3,112};
+// Gradient palette "es_pinksplash_07_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/pink_splash/tn/es_pinksplash_07.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 28 bytes of program space.
+
+DEFINE_PALETTE( es_pinksplash_07_gp ) {
+ 0, 229, 1, 1,
+ 61, 242, 4, 63,
+ 101, 255, 12,255,
+ 127, 249, 81,252,
+ 153, 255, 11,235,
+ 193, 244, 5, 68,
+ 255, 232, 1, 5};
+
+// Gradient palette "Coral_reef_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/other/tn/Coral_reef.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( Coral_reef_gp ) {
+ 0, 40,199,197,
+ 50, 10,152,155,
+ 96, 1,111,120,
+ 96, 43,127,162,
+ 139, 10, 73,111,
+ 255, 1, 34, 71};
+
+// Gradient palette "es_ocean_breeze_068_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_068.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( es_ocean_breeze_068_gp ) {
+ 0, 100,156,153,
+ 51, 1, 99,137,
+ 101, 1, 68, 84,
+ 104, 35,142,168,
+ 178, 0, 63,117,
+ 255, 1, 10, 10};
// Gradient palette "es_ocean_breeze_036_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_036.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 16 bytes of program space.
-const byte es_ocean_breeze_036_gp[] PROGMEM = {
+DEFINE_PALETTE( es_ocean_breeze_036_gp ) {
0, 1, 6, 7,
89, 1, 99,111,
153, 144,209,255,
255, 0, 73, 82};
-
// Gradient palette "departure_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/mjf/tn/departure.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 88 bytes of program space.
-const byte departure_gp[] PROGMEM = {
+DEFINE_PALETTE( departure_gp ) {
0, 8, 3, 0,
42, 23, 7, 0,
63, 75, 38, 6,
@@ -146,13 +187,12 @@ const byte departure_gp[] PROGMEM = {
212, 0, 55, 0,
255, 0, 55, 0};
-
// Gradient palette "es_landscape_64_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_64.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 36 bytes of program space.
-const byte es_landscape_64_gp[] PROGMEM = {
+DEFINE_PALETTE( es_landscape_64_gp ) {
0, 0, 0, 0,
37, 2, 25, 1,
76, 15,115, 5,
@@ -163,13 +203,12 @@ const byte es_landscape_64_gp[] PROGMEM = {
204, 59,117,250,
255, 1, 37,192};
-
// Gradient palette "es_landscape_33_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_33.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 24 bytes of program space.
-const byte es_landscape_33_gp[] PROGMEM = {
+DEFINE_PALETTE( es_landscape_33_gp ) {
0, 1, 5, 0,
19, 32, 23, 1,
38, 161, 55, 1,
@@ -177,13 +216,12 @@ const byte es_landscape_33_gp[] PROGMEM = {
66, 39,142, 74,
255, 1, 4, 1};
-
// Gradient palette "rainbowsherbet_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/icecream/tn/rainbowsherbet.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 28 bytes of program space.
-const byte rainbowsherbet_gp[] PROGMEM = {
+DEFINE_PALETTE( rainbowsherbet_gp ) {
0, 255, 33, 4,
43, 255, 68, 25,
86, 255, 7, 25,
@@ -192,13 +230,12 @@ const byte rainbowsherbet_gp[] PROGMEM = {
209, 42,255, 22,
255, 87,255, 65};
-
// Gradient palette "gr65_hult_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr65_hult.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 24 bytes of program space.
-const byte gr65_hult_gp[] PROGMEM = {
+DEFINE_PALETTE( gr65_hult_gp ) {
0, 247,176,247,
48, 255,136,255,
89, 220, 29,226,
@@ -206,13 +243,12 @@ const byte gr65_hult_gp[] PROGMEM = {
216, 1,124,109,
255, 1,124,109};
-
// Gradient palette "gr64_hult_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr64_hult.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 32 bytes of program space.
-const byte gr64_hult_gp[] PROGMEM = {
+DEFINE_PALETTE( gr64_hult_gp ) {
0, 1,124,109,
66, 1, 93, 79,
104, 52, 65, 1,
@@ -222,13 +258,12 @@ const byte gr64_hult_gp[] PROGMEM = {
239, 0, 55, 45,
255, 0, 55, 45};
-
// Gradient palette "GMT_drywet_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gmt/tn/GMT_drywet.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 28 bytes of program space.
-const byte GMT_drywet_gp[] PROGMEM = {
+DEFINE_PALETTE( GMT_drywet_gp ) {
0, 47, 30, 2,
42, 213,147, 24,
84, 103,219, 52,
@@ -237,13 +272,12 @@ const byte GMT_drywet_gp[] PROGMEM = {
212, 1, 1,111,
255, 1, 7, 33};
-
// Gradient palette "ib15_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/general/tn/ib15.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 24 bytes of program space.
-const byte ib15_gp[] PROGMEM = {
+DEFINE_PALETTE( ib15_gp ) {
0, 113, 91,147,
72, 157, 88, 78,
89, 208, 85, 33,
@@ -251,26 +285,35 @@ const byte ib15_gp[] PROGMEM = {
141, 137, 31, 39,
255, 59, 33, 89};
-
-// Gradient palette "Tertiary_01_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/vermillion/tn/Tertiary_01.png.index.html
+// Gradient palette "Fuschia_7_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ds/fuschia/tn/Fuschia-7.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 20 bytes of program space.
-const byte Tertiary_01_gp[] PROGMEM = {
- 0, 0, 1,255,
- 63, 3, 68, 45,
- 127, 23,255, 0,
- 191, 100, 68, 1,
- 255, 255, 1, 4};
+DEFINE_PALETTE( Fuschia_7_gp ) {
+ 0, 43, 3,153,
+ 63, 100, 4,103,
+ 127, 188, 5, 66,
+ 191, 161, 11,115,
+ 255, 135, 20,182};
+
+// Gradient palette "es_emerald_dragon_08_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/emerald_dragon/tn/es_emerald_dragon_08.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 16 bytes of program space.
+DEFINE_PALETTE( es_emerald_dragon_08_gp ) {
+ 0, 97,255, 1,
+ 101, 47,133, 1,
+ 178, 13, 43, 1,
+ 255, 2, 10, 1};
// Gradient palette "lava_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/lava.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 52 bytes of program space.
-const byte lava_gp[] PROGMEM = {
+DEFINE_PALETTE( lava_gp ) {
0, 0, 0, 0,
46, 18, 0, 0,
96, 113, 0, 0,
@@ -285,28 +328,26 @@ const byte lava_gp[] PROGMEM = {
244, 255,255, 71,
255, 255,255,255};
-
-// Gradient palette "fierce_ice_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/fierce-ice.png.index.html
+// Gradient palette "fire_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/fire.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 28 bytes of program space.
-const byte fierce_ice_gp[] PROGMEM = {
- 0, 0, 0, 0,
- 59, 0, 9, 45,
- 119, 0, 38,255,
- 149, 3,100,255,
- 180, 23,199,255,
- 217, 100,235,255,
+DEFINE_PALETTE( fire_gp ) {
+ 0, 1, 1, 0,
+ 76, 32, 5, 0,
+ 146, 192, 24, 0,
+ 197, 220,105, 5,
+ 240, 252,255, 31,
+ 250, 252,255,111,
255, 255,255,255};
-
// Gradient palette "Colorfull_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Colorfull.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 44 bytes of program space.
-const byte Colorfull_gp[] PROGMEM = {
+DEFINE_PALETTE( Colorfull_gp ) {
0, 10, 85, 5,
25, 29,109, 18,
60, 59,138, 42,
@@ -319,13 +360,26 @@ const byte Colorfull_gp[] PROGMEM = {
168, 100,180,155,
255, 22,121,174};
+// Gradient palette "Magenta_Evening_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Magenta_Evening.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 28 bytes of program space.
+
+DEFINE_PALETTE( Magenta_Evening_gp ) {
+ 0, 71, 27, 39,
+ 31, 130, 11, 51,
+ 63, 213, 2, 64,
+ 70, 232, 1, 66,
+ 76, 252, 1, 69,
+ 108, 123, 2, 51,
+ 255, 46, 9, 35};
// Gradient palette "Pink_Purple_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Pink_Purple.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 44 bytes of program space.
-const byte Pink_Purple_gp[] PROGMEM = {
+DEFINE_PALETTE( Pink_Purple_gp ) {
0, 19, 2, 39,
25, 26, 4, 45,
51, 33, 6, 52,
@@ -338,13 +392,12 @@ const byte Pink_Purple_gp[] PROGMEM = {
183, 128, 57,155,
255, 146, 40,123};
-
// Gradient palette "Sunset_Real_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Real.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 28 bytes of program space.
-const byte Sunset_Real_gp[] PROGMEM = {
+DEFINE_PALETTE( Sunset_Real_gp ) {
0, 120, 0, 0,
22, 179, 22, 0,
51, 255,104, 0,
@@ -353,74 +406,12 @@ const byte Sunset_Real_gp[] PROGMEM = {
198, 16, 0,130,
255, 0, 0,160};
-
-// Gradient palette "Sunset_Yellow_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Yellow.png.index.html
-// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 44 bytes of program space.
-
-const byte Sunset_Yellow_gp[] PROGMEM = {
- 0, 10, 62,123,
- 36, 56,130,103,
- 87, 153,225, 85,
- 100, 199,217, 68,
- 107, 255,207, 54,
- 115, 247,152, 57,
- 120, 239,107, 61,
- 128, 247,152, 57,
- 180, 255,207, 54,
- 223, 255,227, 48,
- 255, 255,248, 42};
-
-
-// Gradient palette "Beech_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Beech.png.index.html
-// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 60 bytes of program space.
-
-const byte Beech_gp[] PROGMEM = {
- 0, 255,252,214,
- 12, 255,252,214,
- 22, 255,252,214,
- 26, 190,191,115,
- 28, 137,141, 52,
- 28, 112,255,205,
- 50, 51,246,214,
- 71, 17,235,226,
- 93, 2,193,199,
- 120, 0,156,174,
- 133, 1,101,115,
- 136, 1, 59, 71,
- 136, 7,131,170,
- 208, 1, 90,151,
- 255, 0, 56,133};
-
-
-// Gradient palette "Another_Sunset_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Another_Sunset.png.index.html
-// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 32 bytes of program space.
-
-const byte Another_Sunset_gp[] PROGMEM = {
- 0, 110, 49, 11,
- 29, 55, 34, 10,
- 68, 22, 22, 9,
- 68, 239,124, 8,
- 97, 220,156, 27,
- 124, 203,193, 61,
- 178, 33, 53, 56,
- 255, 0, 1, 52};
-
-
-
-
-
// Gradient palette "es_autumn_19_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/autumn/tn/es_autumn_19.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 52 bytes of program space.
-const byte es_autumn_19_gp[] PROGMEM = {
+DEFINE_PALETTE( es_autumn_19_gp ) {
0, 26, 1, 1,
51, 67, 4, 1,
84, 118, 14, 1,
@@ -435,13 +426,12 @@ const byte es_autumn_19_gp[] PROGMEM = {
249, 17, 1, 1,
255, 17, 1, 1};
-
// Gradient palette "BlacK_Blue_Magenta_White_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Blue_Magenta_White.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 28 bytes of program space.
-const byte BlacK_Blue_Magenta_White_gp[] PROGMEM = {
+DEFINE_PALETTE( BlacK_Blue_Magenta_White_gp ) {
0, 0, 0, 0,
42, 0, 0, 45,
84, 0, 0,255,
@@ -450,26 +440,24 @@ const byte BlacK_Blue_Magenta_White_gp[] PROGMEM = {
212, 255, 55,255,
255, 255,255,255};
-
// Gradient palette "BlacK_Magenta_Red_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Magenta_Red.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 20 bytes of program space.
-const byte BlacK_Magenta_Red_gp[] PROGMEM = {
+DEFINE_PALETTE( BlacK_Magenta_Red_gp ) {
0, 0, 0, 0,
63, 42, 0, 45,
127, 255, 0,255,
191, 255, 0, 45,
255, 255, 0, 0};
-
// Gradient palette "BlacK_Red_Magenta_Yellow_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Red_Magenta_Yellow.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 28 bytes of program space.
-const byte BlacK_Red_Magenta_Yellow_gp[] PROGMEM = {
+DEFINE_PALETTE( BlacK_Red_Magenta_Yellow_gp ) {
0, 0, 0, 0,
42, 42, 0, 0,
84, 255, 0, 0,
@@ -478,377 +466,1887 @@ const byte BlacK_Red_Magenta_Yellow_gp[] PROGMEM = {
212, 255, 55, 45,
255, 255,255, 0};
-
// Gradient palette "Blue_Cyan_Yellow_gp", originally from
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/Blue_Cyan_Yellow.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 20 bytes of program space.
-const byte Blue_Cyan_Yellow_gp[] PROGMEM = {
+DEFINE_PALETTE( Blue_Cyan_Yellow_gp ) {
0, 0, 0,255,
63, 0, 55,255,
127, 0,255,255,
191, 42,255, 45,
255, 255,255, 0};
+// Gradient palette "Sunset_Yellow_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Yellow.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 44 bytes of program space.
+
+DEFINE_PALETTE( Sunset_Yellow_gp ) {
+ 0, 10, 62,123,
+ 36, 56,130,103,
+ 87, 153,225, 85,
+ 100, 199,217, 68,
+ 107, 255,207, 54,
+ 115, 247,152, 57,
+ 120, 239,107, 61,
+ 128, 247,152, 57,
+ 180, 255,207, 54,
+ 223, 255,227, 48,
+ 255, 255,248, 42};
+
-//Custom palette by Aircoookie
-
-const byte Orange_Teal_gp[] PROGMEM = {
- 0, 0,150, 92,
- 55, 0,150, 92,
- 200, 255, 72, 0,
- 255, 255, 72, 0};
-
-//Custom palette by Aircoookie
-
-const byte Tiamat_gp[] PROGMEM = {
- 0, 1, 2, 14, //gc
- 33, 2, 5, 35, //gc from 47, 61,126
- 100, 13,135, 92, //gc from 88,242,247
- 120, 43,255,193, //gc from 135,255,253
- 140, 247, 7,249, //gc from 252, 69,253
- 160, 193, 17,208, //gc from 231, 96,237
- 180, 39,255,154, //gc from 130, 77,213
- 200, 4,213,236, //gc from 57,122,248
- 220, 39,252,135, //gc from 177,254,255
- 240, 193,213,253, //gc from 203,239,253
- 255, 255,249,255};
-
-//Custom palette by Aircoookie
-
-const byte April_Night_gp[] PROGMEM = {
- 0, 1, 5, 45, //deep blue
- 10, 1, 5, 45,
- 25, 5,169,175, //light blue
- 40, 1, 5, 45,
- 61, 1, 5, 45,
- 76, 45,175, 31, //green
- 91, 1, 5, 45,
- 112, 1, 5, 45,
- 127, 249,150, 5, //yellow
- 143, 1, 5, 45,
- 162, 1, 5, 45,
- 178, 255, 92, 0, //pastel orange
- 193, 1, 5, 45,
- 214, 1, 5, 45,
- 229, 223, 45, 72, //pink
- 244, 1, 5, 45,
- 255, 1, 5, 45};
-
-const byte Orangery_gp[] PROGMEM = {
- 0, 255, 95, 23,
- 30, 255, 82, 0,
- 60, 223, 13, 8,
- 90, 144, 44, 2,
- 120, 255,110, 17,
- 150, 255, 69, 0,
- 180, 158, 13, 11,
- 210, 241, 82, 17,
- 255, 213, 37, 4};
-
-//inspired by Mark Kriegsman https://gist.github.com/kriegsman/756ea6dcae8e30845b5a
-const byte C9_gp[] PROGMEM = {
- 0, 184, 4, 0, //red
- 60, 184, 4, 0,
- 65, 144, 44, 2, //amber
- 125, 144, 44, 2,
- 130, 4, 96, 2, //green
- 190, 4, 96, 2,
- 195, 7, 7, 88, //blue
- 255, 7, 7, 88};
-
-const byte Sakura_gp[] PROGMEM = {
- 0, 196, 19, 10,
- 65, 255, 69, 45,
- 130, 223, 45, 72,
- 195, 255, 82,103,
- 255, 223, 13, 17};
-
-const byte Aurora_gp[] PROGMEM = {
- 0, 1, 5, 45, //deep blue
- 64, 0,200, 23,
- 128, 0,255, 0, //green
- 170, 0,243, 45,
- 200, 0,135, 7,
- 255, 1, 5, 45};//deep blue
-
-const byte Atlantica_gp[] PROGMEM = {
- 0, 0, 28,112, //#001C70
- 50, 32, 96,255, //#2060FF
- 100, 0,243, 45,
- 150, 12, 95, 82, //#0C5F52
- 200, 25,190, 95, //#19BE5F
- 255, 40,170, 80};//#28AA50
-
- const byte C9_2_gp[] PROGMEM = {
- 0, 6, 126, 2, //green
- 45, 6, 126, 2,
- 45, 4, 30, 114, //blue
- 90, 4, 30, 114,
- 90, 255, 5, 0, //red
- 135, 255, 5, 0,
- 135, 196, 57, 2, //amber
- 180, 196, 57, 2,
- 180, 137, 85, 2, //yellow
- 255, 137, 85, 2};
-
- //C9, but brighter and with a less purple blue
- const byte C9_new_gp[] PROGMEM = {
- 0, 255, 5, 0, //red
- 60, 255, 5, 0,
- 60, 196, 57, 2, //amber (start 61?)
- 120, 196, 57, 2,
- 120, 6, 126, 2, //green (start 126?)
- 180, 6, 126, 2,
- 180, 4, 30, 114, //blue (start 191?)
- 255, 4, 30, 114};
-
-// Gradient palette "temperature_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/arendal/tn/temperature.png.index.html
-// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 144 bytes of program space.
-
-const byte temperature_gp[] PROGMEM = {
- 0, 1, 27,105,
- 14, 1, 40,127,
- 28, 1, 70,168,
- 42, 1, 92,197,
- 56, 1,119,221,
- 70, 3,130,151,
- 84, 23,156,149,
- 99, 67,182,112,
- 113, 121,201, 52,
- 127, 142,203, 11,
- 141, 224,223, 1,
- 155, 252,187, 2,
- 170, 247,147, 1,
- 184, 237, 87, 1,
- 198, 229, 43, 1,
- 226, 171, 2, 2,
- 240, 80, 3, 3,
- 255, 80, 3, 3};
-
- const byte Aurora2_gp[] PROGMEM = {
- 0, 17, 177, 13, //Greenish
- 64, 121, 242, 5, //Greenish
- 128, 25, 173, 121, //Turquoise
- 192, 250, 77, 127, //Pink
- 255, 171, 101, 221 //Purple
- };
-
- // Gradient palette "bhw1_01_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_01.png.index.html
+// Gradient palette "cloud_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rc/tn/cloud.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 12 bytes of program space.
-const byte retro_clown_gp[] PROGMEM = {
- 0, 227,101, 3,
- 117, 194, 18, 19,
- 255, 92, 8,192};
+DEFINE_PALETTE( cloud_gp ) {
+ 0, 247,149, 91,
+ 127, 208, 32, 71,
+ 255, 42, 79,188};
-// Gradient palette "bhw1_04_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_04.png.index.html
+
+// Gradient palette "fireandice_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rc/tn/fireandice.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 20 bytes of program space.
+// Size: 24 bytes of program space.
-const byte candy_gp[] PROGMEM = {
- 0, 229,227, 1,
- 15, 227,101, 3,
- 142, 40, 1, 80,
- 198, 17, 1, 79,
- 255, 0, 0, 45};
+DEFINE_PALETTE( fireandice_gp ) {
+ 0, 80, 2, 1,
+ 51, 206, 15, 1,
+ 101, 242, 34, 1,
+ 153, 16, 67,128,
+ 204, 2, 21, 69,
+ 255, 1, 2, 4};
-// Gradient palette "bhw1_05_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_05.png.index.html
+// Gradient palette "bhw2_39_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_39.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 8 bytes of program space.
+// Size: 28 bytes of program space.
-const byte toxy_reaf_gp[] PROGMEM = {
- 0, 1,221, 53,
- 255, 73, 3,178};
+DEFINE_PALETTE( bhw2_39_gp ) {
+ 0, 2,184,188,
+ 33, 56, 27,162,
+ 66, 56, 27,162,
+ 122, 255,255, 45,
+ 150, 227, 65, 6,
+ 201, 67, 13, 27,
+ 255, 16, 1, 53};
+
+// Gradient palette "tashangel_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rc/tn/tashangel.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( tashangel_gp ) {
+ 0, 133, 68,197,
+ 51, 2, 1, 33,
+ 101, 50, 35,130,
+ 153, 199,225,237,
+ 204, 41,187,228,
+ 255, 133, 68,197};
-// Gradient palette "bhw1_06_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_06.png.index.html
+// Gradient palette "butterflytalker_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rc/tn/butterflytalker.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 16 bytes of program space.
+// Size: 28 bytes of program space.
-const byte fairy_reaf_gp[] PROGMEM = {
- 0, 184, 1,128,
- 160, 1,193,182,
- 219, 153,227,190,
- 255, 255,255,255};
+DEFINE_PALETTE( butterflytalker_gp ) {
+ 0, 1, 1, 6,
+ 51, 6, 11, 52,
+ 89, 107,107,192,
+ 127, 101,161,192,
+ 165, 107,107,192,
+ 204, 6, 11, 52,
+ 255, 0, 0, 0};
-// Gradient palette "bhw1_14_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_14.png.index.html
+// Gradient palette "os250k_metres_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/os/tn/os250k-metres.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( os250k_metres_gp ) {
+ 0, 255,255,255,
+ 11, 255,255,255,
+ 11, 255,252,214,
+ 34, 255,252,214,
+ 34, 255,248,178,
+ 57, 255,248,178,
+ 57, 255,211,130,
+ 81, 255,211,130,
+ 81, 255,176, 89,
+ 115, 255,176, 89,
+ 115, 255,147, 63,
+ 173, 255,147, 63,
+ 173, 255,127, 55,
+ 255, 255,127, 55};
+
+// Gradient palette "Night_Midnight_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Night_Midnight.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 36 bytes of program space.
-const byte semi_blue_gp[] PROGMEM = {
+DEFINE_PALETTE( Night_Midnight_gp ) {
+ 0, 15, 25, 27,
+ 36, 22, 48, 91,
+ 59, 32, 80,203,
+ 74, 110,154,228,
+ 77, 255,255,255,
+ 82, 110,154,228,
+ 96, 32, 80,203,
+ 189, 5, 18, 73,
+ 255, 0, 1, 12};
+
+// Gradient palette "Afterdusk_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Afterdusk.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 44 bytes of program space.
+
+DEFINE_PALETTE( Afterdusk_gp ) {
0, 0, 0, 0,
- 12, 1, 1, 3,
- 53, 8, 1, 22,
- 80, 4, 6, 89,
- 119, 2, 25,216,
- 145, 7, 10, 99,
- 186, 15, 2, 31,
- 233, 2, 1, 5,
+ 25, 1, 1, 1,
+ 48, 1, 1, 1,
+ 67, 41, 49, 52,
+ 70, 210,219,216,
+ 73, 155,115,137,
+ 81, 109, 46, 78,
+ 86, 109, 46, 78,
+ 97, 109, 46, 78,
+ 165, 50, 15, 79,
+ 255, 16, 1, 80};
+
+// Gradient palette "BlueSky_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/BlueSky.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 36 bytes of program space.
+
+DEFINE_PALETTE( BlueSky_gp ) {
+ 0, 1, 7, 39,
+ 25, 2, 25, 88,
+ 61, 9, 53,160,
+ 88, 46,115,201,
+ 102, 120,203,245,
+ 108, 88,169,230,
+ 124, 63,139,216,
+ 216, 21, 96,203,
+ 255, 2, 60,188};
+
+// Gradient palette "Gold_Orange_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Gold_Orange.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 52 bytes of program space.
+
+DEFINE_PALETTE( Gold_Orange_gp ) {
+ 0, 244, 88, 11,
+ 21, 247,118, 26,
+ 40, 249,152, 50,
+ 62, 252,201, 82,
+ 72, 255,255,125,
+ 79, 255,211,119,
+ 83, 255,169,112,
+ 87, 255,211,119,
+ 94, 255,255,125,
+ 103, 244,207, 54,
+ 118, 237,164, 16,
+ 202, 242,124, 13,
+ 255, 244, 88, 11};
+
+// Gradient palette "Analogous_02_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/vermillion/tn/Analogous_02.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( Analogous_02_gp ) {
+ 0, 32, 0,123,
+ 63, 110, 5, 79,
+ 127, 255, 23, 45,
+ 191, 255, 21, 30,
+ 255, 255, 18, 18};
+
+// Gradient palette "Analogous_04a_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/pink/tn/Analogous_04a.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 36 bytes of program space.
+
+DEFINE_PALETTE( Analogous_04a_gp ) {
+ 0, 67, 55,255,
+ 42, 67, 55,255,
+ 84, 67, 55,255,
+ 84, 120, 33,255,
+ 127, 120, 33,255,
+ 170, 120, 33,255,
+ 170, 255, 23, 45,
+ 212, 255, 23, 45,
+ 255, 255, 23, 45};
+
+// Gradient palette "Cyan_Orange_Stripped_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/other/tn/Cyan_Orange_Stripped.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 48 bytes of program space.
+
+DEFINE_PALETTE( Cyan_Orange_Stripped_gp ) {
+ 0, 1,108,212,
+ 60, 1,108,212,
+ 121, 1,108,212,
+ 121, 0, 0, 0,
+ 124, 0, 0, 0,
+ 127, 0, 0, 0,
+ 127, 229,127, 15,
+ 188, 242,186, 92,
+ 248, 255,255,255,
+ 248, 0, 0, 0,
+ 251, 0, 0, 0,
255, 0, 0, 0};
-// Gradient palette "bhw1_three_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_three.png.index.html
+// Gradient palette "Cyan_White_Green_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/Cyan_White_Green.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( Cyan_White_Green_gp ) {
+ 0, 0,255,255,
+ 63, 42,255,255,
+ 127, 255,255,255,
+ 191, 42,255, 45,
+ 255, 0,255, 0};
+
+// Gradient palette "Wild_Orange_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/lb/mp/tn/Wild_Orange.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( Wild_Orange_gp ) {
+ 0, 0, 0, 0,
+ 0, 144, 11, 1,
+ 0, 144, 11, 1,
+ 5, 144, 11, 1,
+ 10, 194, 36, 1,
+ 30, 252, 79, 1,
+ 86, 249,175,100,
+ 106, 244,122, 25,
+ 124, 237, 79, 1,
+ 157, 244,154, 2,
+ 196, 252,255, 5,
+ 209, 252,223, 3,
+ 239, 255,108, 1,
+ 255, 255, 36, 1};
+
+// Gradient palette "IKat_Radial_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/lb/mp/tn/IKat_Radial.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( IKat_Radial_gp ) {
+ 0, 3, 7, 4,
+ 56, 255,255,255,
+ 127, 3, 7, 4,
+ 196, 255,255,255,
+ 255, 3, 7, 4};
+
+// Gradient palette "Citrus_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/lb/misc/tn/Citrus.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( Citrus_gp ) {
+ 0, 252,164, 5,
+ 63, 149,25, 3,
+ 135, 255,166, 9,
+ 201, 147,39, 3,
+ 255, 237,119, 4};
+
+// Gradient palette "Teal_Blue_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/lb/misc/tn/Teal_Blue.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( Teal_Blue_gp ) {
+ 0, 1, 73, 88,
+ 63, 1, 43, 52,
+ 127, 1, 77, 95,
+ 196, 1, 58, 67,
+ 255, 1, 45, 50};
+
+// Gradient palette "Ldby_Orange_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/lb/misc/tn/Ldby_Orange.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( Ldby_Orange_gp ) {
+ 0, 217, 45, 17,
+ 61, 179, 21, 8,
+ 130, 222, 49, 21,
+ 193, 203, 32, 7,
+ 255, 173, 22, 6};
+
+// Gradient palette "purple_orange_d07_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/km/tn/purple-orange-d07.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( purple_orange_d07_gp ) {
+ 0, 53, 27, 91,
+ 36, 53, 27, 91,
+ 36, 121, 55,111,
+ 72, 121, 55,111,
+ 72, 179,107,137,
+ 109, 179,107,137,
+ 109, 179,189,182,
+ 145, 179,189,182,
+ 145, 234,152, 59,
+ 182, 234,152, 59,
+ 182, 227, 92, 11,
+ 218, 227, 92, 11,
+ 218, 165, 40, 1,
+ 255, 165, 40, 1};
+
+// Gradient palette "blue_tan_d08_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/km/tn/blue-tan-d08.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 64 bytes of program space.
+
+DEFINE_PALETTE( blue_tan_d08_gp ) {
+ 0, 7, 77,210,
+ 31, 7, 77,210,
+ 31, 21,112,216,
+ 63, 21,112,216,
+ 63, 53,149,207,
+ 95, 53,149,207,
+ 95, 123,180,192,
+ 127, 123,180,192,
+ 127, 186,186,127,
+ 159, 186,186,127,
+ 159, 182,159, 50,
+ 191, 182,159, 50,
+ 191, 155,117, 14,
+ 223, 155,117, 14,
+ 223, 115, 72, 2,
+ 255, 115, 72, 2};
+
+// Gradient palette "green_purple_d07_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/km/tn/green-purple-d07.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( green_purple_d07_gp ) {
+ 0, 1, 90, 12,
+ 36, 1, 90, 12,
+ 36, 12,147, 51,
+ 72, 12,147, 51,
+ 72, 56,189,120,
+ 109, 56,189,120,
+ 109, 179,189,182,
+ 145, 179,189,182,
+ 145, 179,107,137,
+ 182, 179,107,137,
+ 182, 121, 55,111,
+ 218, 121, 55,111,
+ 218, 53, 27, 91,
+ 255, 53, 27, 91};
+
+// Gradient palette "knoza_00_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/knoza/tn/knoza-00.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( knoza_00_gp ) {
+ 0, 56, 56,237,
+ 1, 115, 1, 1,
+ 24, 115, 1, 1,
+ 25, 237,130, 46,
+ 101, 237,186, 1,
+ 113, 237,186, 1,
+ 115, 2, 1, 1,
+ 138, 2, 1, 1,
+ 139, 237,186, 1,
+ 153, 237,186, 1,
+ 228, 237,130, 46,
+ 229, 115, 1, 1,
+ 253, 115, 1, 1,
+ 255, 56, 56,237};
+// Gradient palette "knoza_18_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/knoza/tn/knoza-18.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 64 bytes of program space.
+
+DEFINE_PALETTE( knoza_18_gp ) {
+ 0, 8, 1, 1,
+ 2, 1,239, 1,
+ 51, 1,239, 1,
+ 52, 175,130, 1,
+ 100, 175,130, 1,
+ 101, 1, 1, 1,
+ 115, 1, 1, 1,
+ 117, 237,239,237,
+ 138, 237,239,237,
+ 139, 1, 1, 1,
+ 153, 1, 1, 1,
+ 153, 175,130, 1,
+ 203, 175,130, 1,
+ 203, 1,239, 1,
+ 252, 1,239, 1,
+ 255, 8, 1, 1};
+
+// Gradient palette "calpan_18_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/calpan/tn/calpan-18.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 40 bytes of program space.
+
+DEFINE_PALETTE( calpan_18_gp ) {
+ 0, 133, 31,137,
+ 1, 117, 2, 88,
+ 24, 117, 2, 88,
+ 25, 239,241,245,
+ 32, 239,241,245,
+ 51, 239,241,245,
+ 53, 117, 2, 88,
+ 76, 117, 2, 88,
+ 77, 133, 31,137,
+ 255, 239,241,245};
+
+// Gradient palette "calbayo_18_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/calbayo/tn/calbayo-18.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 64 bytes of program space.
+
+DEFINE_PALETTE( calbayo_18_gp ) {
+ 0, 210,131, 1,
+ 60, 210,131, 1,
+ 62, 41, 2, 3,
+ 99, 41, 2, 3,
+ 100, 106, 40, 1,
+ 101, 210,131, 1,
+ 126, 210,131, 1,
+ 127, 210, 31, 6,
+ 165, 210, 31, 6,
+ 166, 210,131, 1,
+ 188, 210,131, 1,
+ 191, 3, 6, 6,
+ 226, 3, 6, 6,
+ 228, 210,131, 1,
+ 253, 210,131, 1,
+ 255, 1, 58, 29};
+
+// Gradient palette "fib53_15_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/fib53/tn/fib53-15.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 60 bytes of program space.
+
+DEFINE_PALETTE( fib53_15_gp ) {
+ 0, 239, 11, 31,
+ 101, 239, 11, 31,
+ 101, 239,241,240,
+ 127, 239,241,240,
+ 128, 1, 1, 1,
+ 152, 1, 1, 1,
+ 153, 239,241,240,
+ 178, 239,241,240,
+ 179, 239, 11, 31,
+ 202, 239, 11, 31,
+ 203, 239,241,240,
+ 229, 239,241,240,
+ 230, 1, 1, 1,
+ 253, 1, 1, 1,
+ 255, 239,241,240};
+
+// Gradient palette "purple_orange_d08_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/km/tn/purple-orange-d08.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 64 bytes of program space.
+
+DEFINE_PALETTE( purple_orange_d08_gp ) {
+ 0, 49, 26, 89,
+ 31, 49, 26, 89,
+ 31, 107, 49,106,
+ 63, 107, 49,106,
+ 63, 165, 88,127,
+ 95, 165, 88,127,
+ 95, 188,151,158,
+ 127, 188,151,158,
+ 127, 210,178,117,
+ 159, 210,178,117,
+ 159, 239,135, 37,
+ 191, 239,135, 37,
+ 191, 220, 81, 7,
+ 223, 220, 81, 7,
+ 223, 159, 37, 1,
+ 255, 159, 37, 1};
+
+// Gradient palette "pmh_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/esdb/tn/pmh.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 48 bytes of program space.
+
+DEFINE_PALETTE( pmh_gp ) {
+ 0, 255, 55,145,
+ 42, 255, 55,145,
+ 42, 255,182,145,
+ 84, 255,182,145,
+ 84, 255,255,105,
+ 127, 255,255,105,
+ 127, 171,255,174,
+ 170, 171,255,174,
+ 170, 101,255,212,
+ 212, 101,255,212,
+ 212, 171, 82,212,
+ 255, 171, 82,212};
+
+// Gradient palette "konjo_08_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/konjo/tn/konjo-08.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( konjo_08_gp ) {
+ 0, 213,229,240,
+ 127, 213,229,240,
+ 128, 133,168,188,
+ 150, 133,168,188,
+ 150, 21, 57, 91,
+ 152, 0, 6, 33,
+ 177, 0, 6, 33,
+ 179, 0, 2, 9,
+ 200, 0, 2, 9,
+ 203, 0, 6, 33,
+ 227, 0, 6, 33,
+ 229, 30, 0, 2,
+ 252, 30, 0, 2,
+ 255, 0, 6, 33};
+
+// Gradient palette "konjo_18_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/konjo/tn/konjo-18.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 88 bytes of program space.
+
+DEFINE_PALETTE( konjo_18_gp ) {
+ 0, 109, 5, 1,
+ 13, 109, 5, 1,
+ 14, 133,168,188,
+ 37, 133,168,188,
+ 39, 0, 6, 33,
+ 63, 0, 6, 33,
+ 63, 77,130,184,
+ 87, 77,130,184,
+ 89, 0, 2, 9,
+ 114, 0, 2, 9,
+ 115, 213,229,240,
+ 140, 213,229,240,
+ 142, 0, 2, 9,
+ 165, 0, 2, 9,
+ 166, 77,130,184,
+ 191, 77,130,184,
+ 193, 0, 6, 33,
+ 216, 0, 6, 33,
+ 217, 133,168,188,
+ 240, 133,168,188,
+ 241, 109, 5, 1,
+ 255, 109, 5, 1};
+
+// Gradient palette "konjo_19_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/konjo/tn/konjo-19.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 88 bytes of program space.
+
+DEFINE_PALETTE( konjo_19_gp ) {
+ 0, 109, 5, 1,
+ 13, 109, 5, 1,
+ 14, 133,168,188,
+ 37, 133,168,188,
+ 39, 0, 6, 33,
+ 63, 0, 6, 33,
+ 65, 109, 5, 1,
+ 87, 109, 5, 1,
+ 89, 0, 2, 9,
+ 114, 0, 2, 9,
+ 115, 213,229,240,
+ 140, 213,229,240,
+ 142, 0, 2, 9,
+ 165, 0, 2, 9,
+ 166, 109, 5, 1,
+ 192, 109, 5, 1,
+ 193, 0, 6, 33,
+ 216, 0, 6, 33,
+ 217, 133,168,188,
+ 240, 133,168,188,
+ 241, 109, 5, 1,
+ 255, 109, 5, 1};
+
+// Gradient palette "konkikyo_19_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/konkikyo/tn/konkikyo-19.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 60 bytes of program space.
+
+DEFINE_PALETTE( konkikyo_19_gp ) {
+ 0, 1, 2, 9,
+ 101, 1, 2, 9,
+ 102, 199,213,252,
+ 122, 199,213,252,
+ 126, 1, 2, 9,
+ 151, 1, 2, 9,
+ 151, 24,128,245,
+ 177, 24,128,245,
+ 178, 1, 2, 9,
+ 203, 1, 2, 9,
+ 203, 177,133, 1,
+ 229, 177,133, 1,
+ 229, 1, 2, 9,
+ 252, 1, 2, 9,
+ 255, 1, 2, 9};
+
+// Gradient palette "sulz_22_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/sulz/tn/sulz-22.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 96 bytes of program space.
+
+DEFINE_PALETTE( sulz_22_gp ) {
+ 0, 247, 54, 7,
+ 1, 247, 54, 7,
+ 24, 247, 54, 7,
+ 25, 1, 1, 1,
+ 51, 1, 1, 1,
+ 51, 247,248,247,
+ 75, 247,248,247,
+ 75, 247, 54, 7,
+ 101, 247, 54, 7,
+ 102, 1, 1, 1,
+ 115, 1, 1, 1,
+ 115, 247, 54, 7,
+ 138, 247, 54, 7,
+ 139, 1, 1, 1,
+ 153, 1, 1, 1,
+ 153, 247, 54, 7,
+ 179, 247, 54, 7,
+ 181, 247,248,247,
+ 202, 247,248,247,
+ 203, 1, 1, 1,
+ 228, 1, 1, 1,
+ 228, 247, 54, 7,
+ 249, 247, 54, 7,
+ 255, 247, 54, 7};
+
+// Gradient palette "Pills_2_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ds/icons/tn/Pills-2.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 12 bytes of program space.
+
+DEFINE_PALETTE( Pills_2_gp ) {
+ 0, 192,147, 11,
+ 127, 148,104, 59,
+ 255, 109, 69,155};
+
+// Gradient palette "Pink_Yellow_Orange_1_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ds/icons/tn/Pink-Yellow-Orange-1.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( Pink_Yellow_Orange_1_gp ) {
+ 0, 255,199, 0,
+ 34, 255,121, 0,
+ 106, 255, 63, 0,
+ 168, 194, 13, 6,
+ 255, 146, 1, 37};
+// Gradient palette "es_autumn_04_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/autumn/tn/es_autumn_04.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( es_autumn_04_gp ) {
+ 0, 2, 1, 1,
+ 101, 27, 1, 0,
+ 165, 210, 22, 1,
+ 234, 255,166, 42,
+ 255, 255,166, 42};
+
+// Gradient palette "es_autumn_02_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/autumn/tn/es_autumn_02.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( es_autumn_02_gp ) {
+ 0, 86, 6, 1,
+ 127, 255,255,125,
+ 153, 255,255,125,
+ 242, 194, 96, 1,
+ 255, 194, 96, 1};
+
+// Gradient palette "es_candide_30_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/candide/tn/es_candide_30.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( es_candide_30_gp ) {
+ 0, 242,244,242,
+ 63, 133,255,137,
+ 127, 242,146,194,
+ 191, 104,187,245,
+ 252, 232,239,237,
+ 255, 232,239,237};
+
+// Gradient palette "es_chic_16_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/chic/tn/es_chic_16.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 52 bytes of program space.
+
+DEFINE_PALETTE( es_chic_16_gp ) {
+ 0, 4, 1, 1,
+ 51, 135, 99, 3,
+ 63, 222,248,160,
+ 76, 110,118, 50,
+ 89, 72, 55, 6,
+ 127, 4, 1, 1,
+ 165, 72, 55, 6,
+ 172, 90, 84, 22,
+ 178, 110,118, 50,
+ 191, 222,248,160,
+ 204, 135, 99, 3,
+ 247, 4, 1, 1,
+ 255, 4, 1, 1};
+
+// Gradient palette "es_coffee_01_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/coffee/tn/es_coffee_01.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 44 bytes of program space.
+
+DEFINE_PALETTE( es_coffee_01_gp ) {
+ 0, 152,173,123,
+ 13, 152,154,106,
+ 25, 150,136, 91,
+ 63, 133, 78, 35,
+ 86, 112, 46, 15,
+ 114, 86, 15, 1,
+ 153, 68, 6, 1,
+ 178, 46, 1, 1,
+ 191, 31, 1, 1,
+ 216, 14, 1, 0,
+ 255, 6, 1, 0};
+
+// Gradient palette "es_emerald_dragon_01_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/emerald_dragon/tn/es_emerald_dragon_01.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( es_emerald_dragon_01_gp ) {
+ 0, 1, 1, 1,
+ 79, 1, 19, 7,
+ 130, 1, 59, 25,
+ 229, 28,255,255,
+ 255, 28,255,255};
+
+// Gradient palette "es_landscape_57_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_57.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( es_landscape_57_gp ) {
+ 0, 27, 91, 0,
+ 89, 126,171,106,
+ 91, 157,199,255,
+ 143, 45,142,245,
+ 191, 3, 96,235,
+ 255, 1, 15, 22};
+// Gradient palette "es_landscape_22_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_22.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( es_landscape_22_gp ) {
+ 0, 1, 6, 1,
+ 38, 7, 49, 1,
+ 63, 21,124, 1,
+ 68, 173,244,252,
+ 127, 10,164,156,
+ 255, 5, 68, 66};
+// Gradient palette "es_landscape_47_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_47.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( es_landscape_47_gp ) {
+ 0, 175,125, 44,
+ 38, 88, 45, 3,
+ 58, 46, 27, 1,
+ 76, 20, 14, 0,
+ 79, 249,193,140,
+ 255, 121, 27, 1};
+
+// Gradient palette "es_landscape_10_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_10.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 32 bytes of program space.
-const byte pink_candy_gp[] PROGMEM = {
- 0, 255,255,255,
- 45, 7, 12,255,
- 112, 227, 1,127,
- 112, 227, 1,127,
- 140, 255,255,255,
- 155, 227, 1,127,
- 196, 45, 1, 99,
- 255, 255,255,255};
+DEFINE_PALETTE( es_landscape_10_gp ) {
+ 0, 244,213, 55,
+ 24, 242,209, 53,
+ 51, 237,203, 51,
+ 63, 210,252,252,
+ 89, 171,225,230,
+ 127, 123,221,203,
+ 204, 25,122,144,
+ 255, 10, 93,115};
+
+// Gradient palette "es_landscape_76_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_76.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
-// Gradient palette "bhw1_w00t_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_w00t.png.index.html
+DEFINE_PALETTE( es_landscape_76_gp ) {
+ 0, 252,178, 82,
+ 127, 208, 91, 7,
+ 132, 153,173,188,
+ 191, 163,187,221,
+ 255, 130,191,250};
+
+// Gradient palette "es_landscape_61_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_61.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 16 bytes of program space.
+// Size: 28 bytes of program space.
+
+DEFINE_PALETTE( es_landscape_61_gp ) {
+ 0, 90,199, 1,
+ 89, 73,219, 6,
+ 127, 34,189, 6,
+ 128, 113,221, 75,
+ 130, 255,252,255,
+ 178, 64,189,255,
+ 255, 1,122,255};
+
+// Gradient palette "es_landscape_60_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_60.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 32 bytes of program space.
+
+DEFINE_PALETTE( es_landscape_60_gp ) {
+ 0, 161,112, 18,
+ 51, 130, 78, 1,
+ 89, 95, 59, 1,
+ 91, 133,151,140,
+ 136, 22, 92, 91,
+ 178, 1, 49, 52,
+ 242, 0, 1, 1,
+ 255, 0, 1, 1};
+
+// Gradient palette "es_landscape_51_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_51.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( es_landscape_51_gp ) {
+ 0, 128,128,103,
+ 39, 165,161,144,
+ 76, 206,195,190,
+ 114, 15, 71,247,
+ 178, 1, 9, 71,
+ 255, 1, 1, 10};
+
+// Gradient palette "es_landscape_06_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_06.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 12 bytes of program space.
-const byte red_reaf_gp[] PROGMEM = {
- 0, 3, 13, 43,
- 104, 78,141,240,
- 188, 255, 0, 0,
- 255, 28, 1, 1};
+DEFINE_PALETTE( es_landscape_06_gp ) {
+ 0, 90,199, 1,
+ 89, 173,244,252,
+ 255, 57,175,207};
+// Gradient palette "es_ocean_breeze_049_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_049.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( es_ocean_breeze_049_gp ) {
+ 0, 184,231,250,
+ 76, 0,112,203,
+ 77, 29,168,228,
+ 79, 179,235,255,
+ 153, 64,189,255,
+ 255, 0,124,199};
+
+// Gradient palette "es_ocean_breeze_057_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_057.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 32 bytes of program space.
-// Gradient palette "bhw2_23_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_23.png.index.html
+DEFINE_PALETTE( es_ocean_breeze_057_gp ) {
+ 0, 115, 82, 49,
+ 76, 87, 51, 22,
+ 79, 249, 71, 9,
+ 101, 249,122, 17,
+ 140, 247,121, 38,
+ 178, 175,125, 71,
+ 229, 123,108, 83,
+ 255, 83, 97, 83};
+
+// Gradient palette "es_ocean_breeze_074_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_074.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Red & Flash in SR
// Size: 28 bytes of program space.
-const byte aqua_flash_gp[] PROGMEM = {
- 0, 0, 0, 0,
- 66, 57,227,233,
- 96, 255,255, 8,
- 124, 255,255,255,
- 153, 255,255, 8,
- 188, 57,227,233,
- 255, 0, 0, 0};
+DEFINE_PALETTE( es_ocean_breeze_074_gp ) {
+ 0, 1, 1, 1,
+ 101, 34, 23, 3,
+ 127, 53, 26, 2,
+ 130, 203, 65, 7,
+ 153, 78, 56, 8,
+ 191, 22, 37, 11,
+ 255, 1, 4, 1};
-// Gradient palette "bhw2_xc_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_xc.png.index.html
+// Gradient palette "es_pinksplash_05_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/pink_splash/tn/es_pinksplash_05.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 36 bytes of program space.
+
+DEFINE_PALETTE( es_pinksplash_05_gp ) {
+ 0, 206, 1, 25,
+ 20, 192, 45, 82,
+ 38, 179,182,182,
+ 76, 206, 1, 25,
+ 127, 255,135,252,
+ 178, 206, 1, 25,
+ 216, 179,182,182,
+ 231, 192, 45, 82,
+ 255, 206, 1, 25};
+
+// Gradient palette "es_pinksplash_10_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/pink_splash/tn/es_pinksplash_10.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// YBlue in SR
// Size: 28 bytes of program space.
-const byte yelblu_hot_gp[] PROGMEM = {
- 0, 4, 2, 9,
- 58, 16, 0, 47,
- 122, 24, 0, 16,
- 158, 144, 9, 1,
- 183, 179, 45, 1,
- 219, 220,114, 2,
- 255, 234,237, 1};
+DEFINE_PALETTE( es_pinksplash_10_gp ) {
+ 0, 26, 17, 27,
+ 63, 184, 1, 37,
+ 76, 234,141,174,
+ 89, 148, 2, 35,
+ 127, 26, 17, 27,
+ 252, 90, 65, 89,
+ 255, 90, 65, 89};
+
+// Gradient palette "es_vintage_56_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_56.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 36 bytes of program space.
- // Gradient palette "bhw2_45_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_45.png.index.html
+DEFINE_PALETTE( es_vintage_56_gp ) {
+ 0, 220,225,221,
+ 51, 83, 79, 7,
+ 109, 25, 0, 1,
+ 119, 255,131, 19,
+ 127, 217,221,184,
+ 135, 255,131, 19,
+ 145, 25, 0, 1,
+ 204, 60, 46, 1,
+ 255, 220,225,221};
+
+// Gradient palette "es_vintage_10_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_10.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Size: 24 bytes of program space.
+// Size: 16 bytes of program space.
-const byte lite_light_gp[] PROGMEM = {
+DEFINE_PALETTE( es_vintage_10_gp ) {
+ 0, 1, 3, 1,
+ 51, 7, 1, 1,
+ 127, 112, 18, 0,
+ 255, 206,207,182};
+
+// Gradient palette "gold_yellow_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/clth/tn/gold-yellow.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 28 bytes of program space.
+
+DEFINE_PALETTE( gold_yellow_gp ) {
0, 0, 0, 0,
- 9, 1, 1, 1,
- 40, 5, 5, 6,
- 66, 5, 5, 6,
- 101, 10, 1, 12,
- 255, 0, 0, 0};
+ 94, 42, 29, 0,
+ 189, 255,135, 0,
+ 213, 255,189, 4,
+ 238, 255,255, 25,
+ 246, 255,255,103,
+ 255, 255,255,255};
-// Gradient palette "bhw2_22_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_22.png.index.html
+// Gradient palette "radioactive_slime_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/faun/tn/radioactive-slime.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Pink Plasma in SR
-// Size: 20 bytes of program space.
+// Size: 52 bytes of program space.
-const byte red_flash_gp[] PROGMEM = {
+DEFINE_PALETTE( radioactive_slime_gp ) {
0, 0, 0, 0,
- 99, 227, 1, 1,
- 130, 249,199, 95,
- 155, 227, 1, 1,
- 255, 0, 0, 0};
+ 25, 1, 4, 1,
+ 58, 1, 19, 1,
+ 76, 4, 30, 4,
+ 101, 17, 43, 13,
+ 118, 12, 69, 13,
+ 135, 8,100, 13,
+ 150, 27,146, 36,
+ 174, 59,199, 75,
+ 195, 135,195, 79,
+ 222, 255,189, 84,
+ 239, 255,221, 96,
+ 255, 255,255,111};
+
+// Gradient palette "pastel_rainbow_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/othr/tn/pastel-rainbow.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 52 bytes of program space.
+
+DEFINE_PALETTE( pastel_rainbow_gp ) {
+ 0, 0, 0, 0,
+ 33, 1, 2, 8,
+ 67, 7, 12, 45,
+ 88, 27, 18, 31,
+ 110, 67, 27, 19,
+ 129, 83, 38, 52,
+ 147, 100, 53,103,
+ 168, 90, 96, 93,
+ 189, 79,156, 83,
+ 206, 110,178,132,
+ 222, 148,203,197,
+ 238, 197,227,223,
+ 255, 255,255,255};
+
+// Gradient palette "purple_sunset_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/othr/tn/purple-sunset.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 60 bytes of program space.
+
+DEFINE_PALETTE( purple_sunset_gp ) {
+ 0, 0, 0, 0,
+ 31, 1, 1, 1,
+ 62, 1, 1, 7,
+ 62, 3, 2, 6,
+ 63, 6, 4, 5,
+ 88, 16, 8, 9,
+ 114, 31, 14, 15,
+ 131, 45, 22, 22,
+ 148, 61, 31, 31,
+ 152, 65, 39, 37,
+ 155, 69, 48, 45,
+ 192, 118, 86, 46,
+ 225, 184,135, 47,
+ 238, 197,161, 72,
+ 255, 213,187,103};
+
+// Gradient palette "rainfall_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/jjg/misc/tn/rainfall.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( rainfall_gp ) {
+ 0, 192,118, 3,
+ 36, 192,118, 3,
+ 36, 222,118, 24,
+ 72, 222,118, 24,
+ 72, 224,209, 37,
+ 109, 224,209, 37,
+ 109, 58,159, 43,
+ 145, 58,159, 43,
+ 145, 7,133, 52,
+ 182, 7,133, 52,
+ 182, 4,118, 50,
+ 218, 4,118, 50,
+ 218, 1, 85, 8,
+ 255, 1, 85, 8};
+
+// Gradient palette "sulz_12_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/sulz/tn/sulz-12.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 96 bytes of program space.
+
+DEFINE_PALETTE( sulz_12_gp ) {
+ 0, 247,229,247,
+ 2, 1, 3, 10,
+ 23, 1, 3, 10,
+ 23, 17, 38, 14,
+ 45, 17, 38, 14,
+ 46, 1, 3, 10,
+ 69, 1, 3, 10,
+ 70, 17, 38, 14,
+ 91, 17, 38, 14,
+ 92, 1, 3, 10,
+ 113, 1, 3, 10,
+ 115, 247,229,247,
+ 137, 247,229,247,
+ 140, 1, 3, 10,
+ 160, 1, 3, 10,
+ 160, 17, 38, 14,
+ 182, 17, 38, 14,
+ 183, 1, 3, 10,
+ 206, 1, 3, 10,
+ 206, 17, 38, 14,
+ 228, 17, 38, 14,
+ 229, 1, 3, 10,
+ 253, 1, 3, 10,
+ 255, 247,229,247};
+
+// Gradient palette "sulz_10_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/sulz/tn/sulz-10.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 96 bytes of program space.
+
+DEFINE_PALETTE( sulz_10_gp ) {
+ 0, 247,229,247,
+ 2, 1, 3, 10,
+ 23, 1, 3, 10,
+ 23, 117, 1,168,
+ 45, 117, 1,168,
+ 46, 1, 3, 10,
+ 69, 1, 3, 10,
+ 69, 117, 1,168,
+ 91, 117, 1,168,
+ 92, 1, 3, 10,
+ 113, 1, 3, 10,
+ 115, 247,229,247,
+ 137, 247,229,247,
+ 140, 1, 3, 10,
+ 160, 1, 3, 10,
+ 160, 117, 1,168,
+ 182, 117, 1,168,
+ 183, 1, 3, 10,
+ 206, 1, 3, 10,
+ 206, 117, 1,168,
+ 229, 117, 1,168,
+ 229, 1, 3, 10,
+ 253, 1, 3, 10,
+ 255, 247,229,247};
+
+// Gradient palette "sulz_15_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/sulz/tn/sulz-15.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 96 bytes of program space.
+
+DEFINE_PALETTE( sulz_15_gp ) {
+ 0, 247,229,247,
+ 2, 1, 3, 10,
+ 23, 1, 3, 10,
+ 23, 57, 1, 1,
+ 45, 57, 1, 1,
+ 46, 1, 3, 10,
+ 69, 1, 3, 10,
+ 69, 57, 1, 1,
+ 90, 57, 1, 1,
+ 92, 1, 3, 10,
+ 113, 1, 3, 10,
+ 115, 247,229,247,
+ 137, 247,229,247,
+ 140, 1, 3, 10,
+ 160, 1, 3, 10,
+ 160, 57, 1, 1,
+ 181, 57, 1, 1,
+ 183, 1, 3, 10,
+ 206, 1, 3, 10,
+ 207, 57, 1, 1,
+ 229, 57, 1, 1,
+ 229, 1, 3, 10,
+ 253, 1, 3, 10,
+ 255, 247,229,247};
+
+// Gradient palette "sulz_21_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/sulz/tn/sulz-21.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 96 bytes of program space.
+
+DEFINE_PALETTE( sulz_21_gp ) {
+ 0, 247,248, 7,
+ 1, 247,248, 7,
+ 23, 247,248, 7,
+ 25, 1, 1, 1,
+ 51, 1, 1, 1,
+ 51, 247,248,247,
+ 75, 247,248,247,
+ 75, 247,248, 7,
+ 100, 247,248, 7,
+ 102, 1, 1, 1,
+ 115, 1, 1, 1,
+ 115, 247,248, 7,
+ 138, 247,248, 7,
+ 139, 1, 1, 1,
+ 153, 1, 1, 1,
+ 153, 247,248, 7,
+ 179, 247,248, 7,
+ 181, 247,248,247,
+ 202, 247,248,247,
+ 203, 1, 1, 1,
+ 228, 1, 1, 1,
+ 229, 247,248, 7,
+ 249, 247,248, 7,
+ 255, 247,248, 7};
+
+// Gradient palette "fib53_07_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/fib53/tn/fib53-07.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 96 bytes of program space.
+
+DEFINE_PALETTE( fib53_07_gp ) {
+ 0, 98,114,102,
+ 2, 98,114,240,
+ 24, 98,114,240,
+ 25, 239,114,240,
+ 50, 239,114,240,
+ 50, 98,241,240,
+ 75, 98,241,240,
+ 76, 239,114,102,
+ 101, 239,114,102,
+ 102, 239,241,240,
+ 118, 239,241,240,
+ 120, 1, 1, 1,
+ 134, 1, 1, 1,
+ 135, 239,241,240,
+ 151, 239,241,240,
+ 153, 239,114,102,
+ 177, 239,114,102,
+ 179, 98,241,240,
+ 203, 98,241,240,
+ 204, 239,114,240,
+ 228, 239,114,240,
+ 229, 98,114,240,
+ 252, 98,114,240,
+ 255, 98,114,102};
+
+// Gradient palette "fib53_13_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/fib53/tn/fib53-13.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 60 bytes of program space.
+
+DEFINE_PALETTE( fib53_13_gp ) {
+ 0, 6, 61,240,
+ 101, 6, 61,240,
+ 101, 239,241,240,
+ 127, 239,241,240,
+ 128, 1, 1, 1,
+ 152, 1, 1, 1,
+ 153, 239,241,240,
+ 178, 239,241,240,
+ 178, 6, 61,240,
+ 202, 6, 61,240,
+ 203, 239,241,240,
+ 229, 239,241,240,
+ 230, 1, 1, 1,
+ 253, 1, 1, 1,
+ 255, 239,241,240};
+
+// Gradient palette "fib53_17_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/fib53/tn/fib53-17.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 64 bytes of program space.
+
+DEFINE_PALETTE( fib53_17_gp ) {
+ 0, 227,231,140,
+ 2, 1, 1, 1,
+ 12, 1, 1, 1,
+ 13, 73,184, 31,
+ 76, 73,184, 31,
+ 77, 1, 1, 1,
+ 89, 1, 1, 1,
+ 89, 1,121, 1,
+ 166, 1,121, 1,
+ 166, 1, 1, 1,
+ 179, 1, 1, 1,
+ 179, 1, 56, 1,
+ 241, 1, 56, 1,
+ 241, 1, 1, 1,
+ 252, 1, 1, 1,
+ 255, 227,231,140};
+
+// Gradient palette "fib53_05_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/fib53/tn/fib53-05.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 72 bytes of program space.
+
+DEFINE_PALETTE( fib53_05_gp ) {
+ 0, 239,241,240,
+ 23, 239,241,240,
+ 25, 239,114,102,
+ 51, 239,114,102,
+ 51, 98,114,240,
+ 75, 98,114,240,
+ 77, 239,114,240,
+ 99, 239,114,240,
+ 101, 98,114,102,
+ 125, 98,114,102,
+ 127, 98,241,240,
+ 152, 98,241,240,
+ 153, 1, 1, 1,
+ 178, 1, 1, 1,
+ 179, 98,241,240,
+ 204, 98,241,240,
+ 205, 239,241,240,
+ 255, 239,241,240};
+
+// Gradient palette "fib53_18_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/fib53/tn/fib53-18.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( fib53_18_gp ) {
+ 0, 73,184, 31,
+ 179, 73,184, 31,
+ 179, 1, 1, 1,
+ 192, 1, 1, 1,
+ 193, 1, 56, 1,
+ 205, 1, 56, 1,
+ 205, 239,241,240,
+ 216, 239,241,240,
+ 217, 1,121, 1,
+ 229, 1,121, 1,
+ 230, 1, 1, 1,
+ 243, 1, 1, 1,
+ 243, 227,231,140,
+ 255, 227,231,140};
+
+// Gradient palette "fib53_01_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/fib53/tn/fib53-01.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 80 bytes of program space.
+
+DEFINE_PALETTE( fib53_01_gp ) {
+ 0, 239,241,240,
+ 2, 6, 88, 77,
+ 30, 6, 88, 77,
+ 30, 239,241,240,
+ 45, 239,241,240,
+ 46, 73, 88, 77,
+ 61, 73, 88, 77,
+ 62, 239,241,240,
+ 77, 239,241,240,
+ 79, 6, 88, 77,
+ 173, 6, 88, 77,
+ 174, 239,241,240,
+ 191, 239,241,240,
+ 192, 73, 88, 77,
+ 209, 73, 88, 77,
+ 210, 239,241,240,
+ 224, 239,241,240,
+ 225, 6, 88, 77,
+ 252, 6, 88, 77,
+ 255, 239,241,240};
+
+// Gradient palette "mccahon_16_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/mccahon/tn/mccahon-16.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 32 bytes of program space.
+
+DEFINE_PALETTE( mccahon_16_gp ) {
+ 0, 237, 95, 29,
+ 61, 247,233,190,
+ 63, 109, 73, 1,
+ 125, 247,233,190,
+ 127, 186, 20, 5,
+ 190, 247,233,190,
+ 191, 3, 1, 1,
+ 255, 237, 95, 29};
+
+// Gradient palette "frizzell_09_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/frizzell/tn/frizzell-09.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 96 bytes of program space.
+
+DEFINE_PALETTE( frizzell_09_gp ) {
+ 0, 242,142, 10,
+ 1, 137, 19, 0,
+ 63, 137, 19, 0,
+ 65, 210,189,119,
+ 76, 210,189,119,
+ 78, 79, 25, 1,
+ 88, 79, 25, 1,
+ 89, 210,189,119,
+ 102, 210,189,119,
+ 103, 1, 5, 6,
+ 115, 1, 5, 6,
+ 115, 45, 68, 64,
+ 137, 45, 68, 64,
+ 139, 1, 5, 6,
+ 152, 1, 5, 6,
+ 153, 210,189,119,
+ 163, 210,189,119,
+ 165, 79, 25, 1,
+ 175, 79, 25, 1,
+ 178, 210,189,119,
+ 188, 210,189,119,
+ 191, 137, 19, 0,
+ 252, 137, 19, 0,
+ 255, 242,142, 10};
+
+// Gradient palette "frizzell_10_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/frizzell/tn/frizzell-10.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 56 bytes of program space.
+
+DEFINE_PALETTE( frizzell_10_gp ) {
+ 0, 45, 68, 64,
+ 11, 45, 68, 64,
+ 11, 242,142, 10,
+ 25, 242,142, 10,
+ 25, 1, 5, 6,
+ 38, 1, 5, 6,
+ 39, 210,189,119,
+ 49, 210,189,119,
+ 49, 79, 25, 1,
+ 63, 79, 25, 1,
+ 65, 210,189,119,
+ 76, 210,189,119,
+ 77, 137, 19, 0,
+ 255, 137, 19, 0};
+
+// Gradient palette "frizzell_12_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/frizzell/tn/frizzell-12.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 40 bytes of program space.
+
+DEFINE_PALETTE( frizzell_12_gp ) {
+ 0, 45, 68, 64,
+ 2, 210,189,119,
+ 24, 210,189,119,
+ 25, 1, 5, 6,
+ 126, 1, 5, 6,
+ 126, 137, 19, 0,
+ 228, 137, 19, 0,
+ 230, 79, 25, 1,
+ 253, 79, 25, 1,
+ 255, 242,142, 10};
+
+// Gradient palette "frizzell_05_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/frizzell/tn/frizzell-05.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 80 bytes of program space.
+
+DEFINE_PALETTE( frizzell_05_gp ) {
+ 0, 222,133, 47,
+ 1, 27, 17, 4,
+ 28, 27, 17, 4,
+ 28, 247,146,178,
+ 53, 247,146,178,
+ 55, 5, 1, 1,
+ 84, 5, 1, 1,
+ 84, 247,146,178,
+ 112, 247,146,178,
+ 113, 5, 1, 1,
+ 139, 5, 1, 1,
+ 140, 247,146,178,
+ 166, 247,146,178,
+ 168, 5, 1, 1,
+ 195, 5, 1, 1,
+ 196, 247,146,178,
+ 223, 247,146,178,
+ 224, 27, 17, 4,
+ 253, 27, 17, 4,
+ 255, 222,133, 47};
+
+// Gradient palette "haiyan_23_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/haiyan/tn/haiyan-23.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 52 bytes of program space.
+
+DEFINE_PALETTE( haiyan_23_gp ) {
+ 0, 36,197,164,
+ 122, 36,197,164,
+ 124, 1, 1, 1,
+ 135, 1, 1, 1,
+ 136, 239,241,240,
+ 177, 239,241,240,
+ 178, 1, 1, 1,
+ 204, 1, 1, 1,
+ 205, 84,100, 88,
+ 229, 84,100, 88,
+ 230, 1, 1, 1,
+ 253, 1, 1, 1,
+ 255, 239,241,240};
+
+// Gradient palette "janico_22_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gacruxa/janico/tn/janico-22.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 108 bytes of program space.
+
+DEFINE_PALETTE( janico_22_gp ) {
+ 0, 112,109, 87,
+ 2, 126,109,115,
+ 37, 126,109,115,
+ 38, 83, 99,115,
+ 76, 83, 99,115,
+ 77, 148,127,115,
+ 89, 148,127,115,
+ 90, 112, 65, 73,
+ 127, 112, 65, 73,
+ 128, 148,127,115,
+ 141, 148,127,115,
+ 141, 4, 1, 1,
+ 151, 4, 1, 1,
+ 152, 112, 65, 53,
+ 165, 112, 65, 53,
+ 166, 4, 1, 1,
+ 178, 4, 1, 1,
+ 179, 69, 65, 53,
+ 202, 69, 65, 53,
+ 203, 4, 1, 1,
+ 214, 4, 1, 1,
+ 216, 194,225,255,
+ 230, 194,225,255,
+ 231, 4, 1, 1,
+ 252, 4, 1, 1,
+ 253, 4, 1, 1,
+ 255, 194,225,255};
+
+
+// Gradient palette "Adrift_in_Dreams_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/colo/Skyblue2u/tn/Adrift_in_Dreams.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 40 bytes of program space.
+
+DEFINE_PALETTE( Adrift_in_Dreams_gp ) {
+ 0, 148,223, 77,
+ 51, 148,223, 77,
+ 51, 86,182, 89,
+ 102, 86,182, 89,
+ 102, 36,131, 72,
+ 153, 36,131, 72,
+ 153, 5, 61, 51,
+ 204, 5, 61, 51,
+ 204, 1, 15, 29,
+ 255, 1, 15, 29};
+
+
+// Gradient palette "Set3_03_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/cb/qual/tn/Set3_03.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 24 bytes of program space.
+
+DEFINE_PALETTE( Set3_03_gp ) {
+ 0, 54,168,137,
+ 84, 54,168,137,
+ 84, 255,255,105,
+ 170, 255,255,105,
+ 170, 118,127,172,
+ 255, 118,127,172};
+
+// Gradient palette "Pastel1_06_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/cb/qual/tn/Pastel1_06.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 48 bytes of program space.
+
+DEFINE_PALETTE( Pastel1_06_gp ) {
+ 0, 244,118, 98,
+ 42, 244,118, 98,
+ 42, 101,157,190,
+ 84, 101,157,190,
+ 84, 142,213,133,
+ 127, 142,213,133,
+ 127, 177,154,192,
+ 170, 177,154,192,
+ 170, 252,178, 87,
+ 212, 252,178, 87,
+ 212, 255,255,145,
+ 255, 255,255,145};
+
+
+// Gradient palette "es_rosa_55_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/rosa/tn/es_rosa_55.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( es_rosa_55_gp ) {
+ 0, 6, 1, 2,
+ 101, 54, 1, 10,
+ 170, 15, 29, 4,
+ 216, 95,124, 54,
+ 255, 213,233,158};
-// Gradient palette "bhw3_40_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_40.png.index.html
+// Gradient palette "daybreak_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/pj/1/tn/daybreak.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 32 bytes of program space.
-const byte blink_red_gp[] PROGMEM = {
+DEFINE_PALETTE( daybreak_gp ) {
0, 1, 1, 1,
- 43, 4, 1, 11,
- 76, 10, 1, 3,
- 109, 161, 4, 29,
- 127, 255, 86,123,
- 165, 125, 16,160,
- 204, 35, 13,223,
- 255, 18, 2, 18};
-
-// Gradient palette "bhw3_52_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_52.png.index.html
-// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Yellow2Blue in SR
-// Size: 28 bytes of program space.
+ 91, 4, 11, 21,
+ 140, 11, 31,135,
+ 150, 255,255,125,
+ 165, 132, 18,123,
+ 198, 58, 92,221,
+ 232, 57,168,223,
+ 255, 255,241,242};
-const byte red_shift_gp[] PROGMEM = {
- 0, 31, 1, 27,
- 45, 34, 1, 16,
- 99, 137, 5, 9,
- 132, 213,128, 10,
- 175, 199, 22, 1,
- 201, 199, 9, 6,
- 255, 1, 0, 1};
-// Gradient palette "bhw4_097_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_097.png.index.html
+// Gradient palette "melancholiy_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/pj/3/tn/melancholiy.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
-// Yellow2Red in SR
-// Size: 44 bytes of program space.
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( melancholiy_gp ) {
+ 0, 255,171,242,
+ 76, 1, 2,105,
+ 140, 121,136,125,
+ 211, 255,171,242,
+ 255, 1, 2,105};
+
+
+// Gradient palette "xanidu_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/pj/3/tn/xanidu.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 36 bytes of program space.
-const byte red_tide_gp[] PROGMEM = {
- 0, 247, 5, 0,
- 28, 255, 67, 1,
- 43, 234, 88, 11,
- 58, 234,176, 51,
- 84, 229, 28, 1,
- 114, 113, 12, 1,
- 140, 255,225, 44,
- 168, 113, 12, 1,
- 196, 244,209, 88,
- 216, 255, 28, 1,
- 255, 53, 1, 1};
-
-// Gradient palette "bhw4_017_gp", originally from
-// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_017.png.index.html
+DEFINE_PALETTE( xanidu_gp ) {
+ 0, 118,161,226,
+ 5, 255,255, 45,
+ 15, 252,203,156,
+ 53, 79, 1,162,
+ 94, 67, 1, 7,
+ 132, 1, 55,156,
+ 173, 1,127, 61,
+ 211, 39, 45, 72,
+ 255, 118,161,226};
+
+
+// Gradient palette "air_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/pj/3/tn/air.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 36 bytes of program space.
+
+DEFINE_PALETTE( air_gp ) {
+ 0, 252,246,103,
+ 84, 252,246,103,
+ 140, 14, 1, 91,
+ 155, 165,176,156,
+ 163, 252,246,103,
+ 170, 14, 1, 91,
+ 181, 165,176,156,
+ 193, 252,246,103,
+ 255, 252,246,103};
+
+
+// Gradient palette "revolution2_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/pj/6/tn/revolution2.png.index.html
// converted for FastLED with gammas (2.6, 2.2, 2.5)
// Size: 40 bytes of program space.
-const byte candy2_gp[] PROGMEM = {
- 0, 39, 33, 34,
- 25, 4, 6, 15,
- 48, 49, 29, 22,
- 73, 224,173, 1,
- 89, 177, 35, 5,
- 130, 4, 6, 15,
- 163, 255,114, 6,
- 186, 224,173, 1,
- 211, 39, 33, 34,
- 255, 1, 1, 1};
+DEFINE_PALETTE( revolution2_gp ) {
+ 0, 112, 46, 21,
+ 33, 101, 69, 14,
+ 61, 194, 74, 29,
+ 91, 242,115, 52,
+ 119, 215,211,102,
+ 145, 2, 2, 1,
+ 163, 8, 28, 46,
+ 186, 17, 9, 1,
+ 229, 215,211,102,
+ 255, 242,115, 52};
+
+
+// Gradient palette "sky_33_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rafi/tn/sky-33.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( sky_33_gp ) {
+ 0, 237,229,140,
+ 51, 227,107, 79,
+ 87, 155, 55, 54,
+ 178, 22, 28, 36,
+ 255, 5, 19, 31};
+
+// Gradient palette "sky_45_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rafi/tn/sky-45.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 20 bytes of program space.
+
+DEFINE_PALETTE( sky_45_gp ) {
+ 0, 249,205, 4,
+ 51, 255,239,123,
+ 87, 5,141, 85,
+ 178, 1, 26, 43,
+ 255, 0, 2, 23};
+
+// Gradient palette "sky_05_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rafi/tn/sky-05.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 28 bytes of program space.
+
+DEFINE_PALETTE( sky_05_gp ) {
+ 0, 252, 61, 2,
+ 25, 255,146, 4,
+ 63, 224,255,255,
+ 101, 46,114,226,
+ 127, 6, 40,127,
+ 191, 1, 3, 17,
+ 255, 1, 1, 4};
+
+
+// Gradient palette "carousel_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/rc/tn/carousel.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 28 bytes of program space.
+
+DEFINE_PALETTE( carousel_gp ) {
+ 0, 2, 6, 37,
+ 101, 2, 6, 37,
+ 122, 177,121, 9,
+ 127, 217,149, 2,
+ 132, 177,121, 9,
+ 153, 84, 13, 36,
+ 255, 84, 13, 36};
+
+// Gradient palette "nrwc_gp", originally from
+// http://soliton.vm.bytemark.co.uk/pub/cpt-city/wkp/tubs/tn/nrwc.png.index.html
+// converted for FastLED with gammas (2.6, 2.2, 2.5)
+// Size: 44 bytes of program space.
+
+DEFINE_PALETTE( nrwc_gp ) {
+ 0, 1, 1, 1,
+ 25, 4, 8, 1,
+ 51, 1, 11, 2,
+ 76, 4, 36, 9,
+ 102, 6, 66, 18,
+ 127, 27, 95, 23,
+ 153, 82,127, 31,
+ 178, 197,171, 40,
+ 204, 133,100, 19,
+ 229, 97, 48, 6,
+ 255, 163, 55, 7};
+
+
// Single array of defined cpt-city color palettes.
// This will let us programmatically choose one based on
// a number, rather than having to activate each explicitly
// by name every time.
const byte* const gGradientPalettes[] PROGMEM = {
+ // starts at #13:
+ ib_jul01_gp,
+ es_vintage_57_gp,
+ es_vintage_01_gp,
+ es_rivendell_15_gp,
+ rgi_15_gp,
+ retro2_16_gp,
+ Analogous_1_gp,
+
+ // 20
+ es_pinksplash_08_gp,
+ es_pinksplash_07_gp,
+ Coral_reef_gp,
+ es_ocean_breeze_068_gp,
+ es_ocean_breeze_036_gp,
+ departure_gp,
+ es_landscape_64_gp,
+ es_landscape_33_gp,
+ rainbowsherbet_gp,
+ gr65_hult_gp,
+
+ // 30
+ gr64_hult_gp,
+ GMT_drywet_gp,
+ ib15_gp,
+ Fuschia_7_gp,
+ es_emerald_dragon_08_gp,
+ lava_gp,
+ fire_gp,
+ haiyan_23_gp,
+ Colorfull_gp,
+ Magenta_Evening_gp,
+
+ // 40
+ Pink_Purple_gp,
+ Sunset_Real_gp,
+ es_autumn_19_gp,
+ BlacK_Blue_Magenta_White_gp,
+ BlacK_Magenta_Red_gp,
+ BlacK_Red_Magenta_Yellow_gp,
+ Blue_Cyan_Yellow_gp,
+ Sunset_Yellow_gp,
+ cloud_gp,
+ fireandice_gp,
+
+ // 50
+ bhw2_39_gp,
+ rainfall_gp,
+ tashangel_gp,
+ butterflytalker_gp,
+ os250k_metres_gp,
+ Night_Midnight_gp,
+ Afterdusk_gp,
+ BlueSky_gp,
+ Gold_Orange_gp,
+ frizzell_10_gp,
+
+ // 60
+ frizzell_12_gp,
+ fib53_18_gp,
+ fib53_13_gp,
+ fib53_17_gp,
+ fib53_05_gp,
+ Analogous_02_gp,
+ Analogous_04a_gp,
+ Cyan_Orange_Stripped_gp,
+ Cyan_White_Green_gp,
+ Wild_Orange_gp,
+
+ // 70
+ IKat_Radial_gp,
+ Citrus_gp,
+ Teal_Blue_gp,
+ Ldby_Orange_gp,
+ purple_orange_d07_gp,
+ blue_tan_d08_gp,
+ green_purple_d07_gp,
+ knoza_00_gp,
+ knoza_18_gp,
+ calpan_18_gp,
+
+ // 80
+ calbayo_18_gp,
+ fib53_15_gp,
+ purple_orange_d08_gp,
+ pmh_gp,
+ konjo_08_gp,
+ konkikyo_19_gp,
+ mccahon_16_gp,
+ Pills_2_gp,
+ Pink_Yellow_Orange_1_gp,
+ es_autumn_04_gp,
+
+ // 90
+ es_autumn_02_gp,
+ es_candide_30_gp,
+ es_chic_16_gp,
+ es_coffee_01_gp,
+ es_emerald_dragon_01_gp,
+ es_landscape_57_gp,
+ es_landscape_22_gp,
+ es_landscape_47_gp,
+ es_landscape_10_gp,
+ es_landscape_76_gp,
+
+ // 100
+ es_landscape_61_gp,
+ es_landscape_60_gp,
+ es_landscape_51_gp,
+ es_landscape_06_gp,
+ es_ocean_breeze_049_gp,
+ es_ocean_breeze_057_gp,
+ es_ocean_breeze_074_gp,
+ es_pinksplash_05_gp,
+ es_pinksplash_10_gp,
+ es_vintage_56_gp,
+
+ // 110
+ es_vintage_10_gp,
+ gold_yellow_gp,
+ radioactive_slime_gp,
+ pastel_rainbow_gp,
+ purple_sunset_gp,
+ Adrift_in_Dreams_gp,
+ Set3_03_gp,
+ Pastel1_06_gp,
+ es_rosa_55_gp,
+ daybreak_gp,
+
+ // 120
+ melancholiy_gp,
+ xanidu_gp,
+ air_gp,
+ revolution2_gp,
+ sky_05_gp,
+ sky_33_gp,
+ sky_45_gp,
+ carousel_gp,
+ nrwc_gp,
+
+/*
Sunset_Real_gp, //13-00 Sunset
es_rivendell_15_gp, //14-01 Rivendell
es_ocean_breeze_036_gp, //15-02 Breeze
@@ -907,6 +2405,9 @@ const byte* const gGradientPalettes[] PROGMEM = {
red_shift_gp, //68-55 Red Shift
red_tide_gp, //69-56 Red Tide
candy2_gp //70-57 Candy2
+*/
};
+const uint8_t gGradientPaletteCount = ARRAY_SIZE(gGradientPalettes);
+
#endif
diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp
index db016f5508..3b7cc62930 100644
--- a/wled00/usermods_list.cpp
+++ b/wled00/usermods_list.cpp
@@ -200,6 +200,9 @@
#ifdef USERMOD_LDR_DUSK_DAWN
#include "../usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h"
#endif
+#ifdef USERMOD_TUBES
+#include "../usermods/Tubes/Tubes.h"
+#endif
#ifdef USERMOD_STAIRCASE_WIPE
#include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h"
@@ -387,4 +390,8 @@ void registerUsermods()
#ifdef USERMOD_STAIRCASE_WIPE
usermods.add(new StairwayWipeUsermod());
#endif
+
+ #ifdef USERMOD_TUBES
+ usermods.add(new TubesUsermod());
+ #endif
}
diff --git a/wled00/wled.cpp b/wled00/wled.cpp
index 8ba6b1a565..8e35f00af7 100644
--- a/wled00/wled.cpp
+++ b/wled00/wled.cpp
@@ -2,6 +2,7 @@
#include "wled.h"
#include "wled_ethernet.h"
#include
+#include "espnow_broadcast.h"
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)
#include "soc/soc.h"
@@ -29,6 +30,13 @@ void WLED::reset()
}
applyBri();
DEBUG_PRINTLN(F("WLED RESET"));
+
+ WiFi.softAPdisconnect(true);
+ WiFi.disconnect(true);
+ yield();
+
+ lastReconnectAttempt = 0;
+ apActive = false;
ESP.restart();
}
@@ -57,7 +65,9 @@ void WLED::loop()
#ifndef WLED_DISABLE_ESPNOW
handleRemote();
#endif
+ #ifndef WLED_DISABLE_SERIAL
handleSerial();
+ #endif
handleImprovWifiScan();
handleNotifications();
handleTransitions();
@@ -499,6 +509,10 @@ pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), Pin
initServer();
DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap());
+#ifndef WLED_DISABLE_ESPNOW_NEW
+ espnowBroadcast.setup();
+#endif
+
enableWatchdog();
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)
@@ -535,8 +549,9 @@ void WLED::beginStrip()
void WLED::initAP(bool resetAP)
{
- if (apBehavior == AP_BEHAVIOR_BUTTON_ONLY && !resetAP)
+ if (apBehavior == AP_BEHAVIOR_BUTTON_ONLY && !resetAP) {
return;
+ }
if (resetAP) {
WLED_SET_AP_SSID();
@@ -552,8 +567,11 @@ void WLED::initAP(bool resetAP)
if (!apActive) // start captive portal if AP active
{
+
DEBUG_PRINTLN(F("Init AP interfaces"));
server.begin();
+ yield();
+
if (udpPort > 0 && udpPort != ntpLocalPort) {
udpConnected = notifierUdp.begin(udpPort);
}
@@ -563,9 +581,13 @@ void WLED::initAP(bool resetAP)
if (udpPort2 > 0 && udpPort2 != ntpLocalPort && udpPort2 != udpPort && udpPort2 != udpRgbPort) {
udp2Connected = notifier2Udp.begin(udpPort2);
}
+ yield();
+
e131.begin(false, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT);
ddp.begin(false, DDP_DEFAULT_PORT);
+ yield();
+
dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
dnsServer.start(53, "*", WiFi.softAPIP());
}
@@ -808,6 +830,10 @@ void WLED::handleConnection()
return;
}
+#ifndef WLED_DISABLE_ESPNOW_NEW
+ espnowBroadcast.loop();
+#endif
+
// reconnect WiFi to clear stale allocations if heap gets too low
if (now - heapTime > 5000) {
uint32_t heap = ESP.getFreeHeap();