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. + +![IMG_1521](https://github.com/SteveEisner/tubes/blob/master/assembly/poles_in_use.jpg) + +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("\n

Directory 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();