diff --git a/include/README b/include/README deleted file mode 100644 index 194dcd4..0000000 --- a/include/README +++ /dev/null @@ -1,39 +0,0 @@ - -This directory is intended for project header files. - -A header file is a file containing C declarations and macro definitions -to be shared between several project source files. You request the use of a -header file in your project source file (C, C++, etc) located in `src` folder -by including it, with the C preprocessing directive `#include'. - -```src/main.c - -#include "header.h" - -int main (void) -{ - ... -} -``` - -Including a header file produces the same results as copying the header file -into each source file that needs it. Such copying would be time-consuming -and error-prone. With a header file, the related declarations appear -in only one place. If they need to be changed, they can be changed in one -place, and programs that include the header file will automatically use the -new version when next recompiled. The header file eliminates the labor of -finding and changing all the copies as well as the risk that a failure to -find one copy will result in inconsistencies within a program. - -In C, the usual convention is to give header files names that end with `.h'. -It is most portable to use only letters, digits, dashes, and underscores in -header file names, and at most one dot. - -Read more about using header files in official GCC documentation: - -* Include Syntax -* Include Operation -* Once-Only Headers -* Computed Includes - -https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/Figments/Animation.h b/lib/Figments/Animation.h index cc04dd4..de40d8b 100644 --- a/lib/Figments/Animation.h +++ b/lib/Figments/Animation.h @@ -1,7 +1,6 @@ #pragma once #include #include "./Figment.h" -#include class Display; @@ -24,6 +23,18 @@ struct AnimatedNumber { } } + void update(uint8_t speed) { + if (255 - speed >= m_idx) { + m_idx += speed; + } else { + m_idx = 255; + } + } + + bool isFinished() const { + return m_idx == 255; + } + AnimatedNumber() {} AnimatedNumber(uint8_t v) : m_end(v) {} diff --git a/lib/Figments/Display.h b/lib/Figments/Display.h index f8c8b04..c96a201 100644 --- a/lib/Figments/Display.h +++ b/lib/Figments/Display.h @@ -15,11 +15,11 @@ struct LinearCoordinateMapping: CoordinateMapping { unsigned int startPixel = 0; LinearCoordinateMapping() {} LinearCoordinateMapping(unsigned int count, unsigned int start) : pixelCount(count), startPixel(start) {} - VirtualCoordinates physicalToVirtualCoords(const PhysicalCoordinates localCoords) const { - return VirtualCoordinates{(uint8_t)((localCoords.x) / pixelCount), 0}; + VirtualCoordinates physicalToVirtualCoords(const PhysicalCoordinates localCoords) const override { + return VirtualCoordinates{map8(localCoords.x, 0, pixelCount), 0}; } - PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const { + PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const override { return PhysicalCoordinates{scale8(pixelCount, virtualCoords.x), 0}; } @@ -27,7 +27,7 @@ struct LinearCoordinateMapping: CoordinateMapping { return localCoords.x + startPixel; } - unsigned int physicalPixelCount() const { + unsigned int physicalPixelCount() const override { return pixelCount; } }; diff --git a/lib/Figments/Figment.h b/lib/Figments/Figment.h index fbce8e3..4da9659 100644 --- a/lib/Figments/Figment.h +++ b/lib/Figments/Figment.h @@ -22,15 +22,15 @@ struct Task : public virtual Loopable { }; Task() {} - Task(State initialState) : Task(0, initialState) {} - Task(const char* name) : Task(name, Running) {} + explicit Task(State initialState) : Task(0, initialState) {} + explicit Task(const char* name) : Task(name, Running) {} Task(const char* name, State initialState) : name(name), state(initialState) {} void start() { state = Running; onStart(); } void stop() { onStop(); state = Stopped; } virtual bool isFigment() const { return false; } - const char* name = 0; + const char* name = ""; State state = Running; }; @@ -42,8 +42,8 @@ struct TaskFunc: public Task { struct Figment: public Task { Figment() : Task() {} - Figment(State initialState) : Task(initialState) {} - Figment(const char* name) : Task(name) {} + explicit Figment(State initialState) : Task(initialState) {} + explicit Figment(const char* name) : Task(name) {} Figment(const char* name, State initialState) : Task(name, initialState) {} virtual void render(Display* dpy) const = 0; bool isFigment() const override { return true; } diff --git a/lib/Figments/Geometry.h b/lib/Figments/Geometry.h index 7d9d8a7..ebcd5b8 100644 --- a/lib/Figments/Geometry.h +++ b/lib/Figments/Geometry.h @@ -8,8 +8,8 @@ template struct Coordinates { T y; }; -struct VirtualCoordinates: Coordinates { - VirtualCoordinates(uint16_t _x, uint16_t _y) : Coordinates(_x, _y) {} +struct VirtualCoordinates: Coordinates { + VirtualCoordinates(uint8_t _x, uint8_t _y) : Coordinates(_x, _y) {} }; struct PhysicalCoordinates: Coordinates { diff --git a/lib/Figments/Input.cpp b/lib/Figments/Input.cpp index 1a52d2e..d9990d9 100644 --- a/lib/Figments/Input.cpp +++ b/lib/Figments/Input.cpp @@ -20,10 +20,63 @@ Variant::asInt() const return m_value.asInt; } +bool +Variant::asBool() const +{ + return (bool)m_value.asInt; +} + +void +InputSource::init() +{ +#ifdef CONFIG_THREADED_INPUTS + m_queue = xQueueCreate(32, sizeof(InputEvent)); +#endif +} + +#ifdef CONFIG_THREADED_INPUTS +void +InputSource::readThread(void* data) +{ + InputSource* self = static_cast(data); + while(true) { + InputEvent evt = self->read(); + if (evt.intent != InputEvent::None) { + xQueueSend(m_queue, &evt, 0) + } + taskYIELD(); + } +} +#endif + +void +InputSource::onStart() +{ +#ifdef CONFIG_THREADED_INPUTS + m_threadLoop = MainLoop::instance(); + xTaskCreate( + &InputSource::readThread, + name, + 1000, + this, + 1, + NULL + ); +#endif +} + void InputSource::loop() { +#ifndef CONFIG_THREADED_INPUTS MainLoop::instance()->dispatch(read()); +#else + InputEvent evt; + xQueueReceive(m_queue, &evt, 0); + if (evt.intent != InputEvent::None) { + MainLoop::instance()->dispatch(evt); + } +#endif } InputEvent diff --git a/lib/Figments/Input.h b/lib/Figments/Input.h index a3fd2c0..f74fb9b 100644 --- a/lib/Figments/Input.h +++ b/lib/Figments/Input.h @@ -32,6 +32,7 @@ struct Variant { const char* asString() const; CRGB asRGB() const; int asInt() const; + bool asBool() const; private: union { @@ -74,6 +75,8 @@ struct InputEvent: public Variant { // Timekeeping ScheduleChange, + Beat, + BeatDetect, // Task management StartThing, @@ -87,6 +90,8 @@ struct InputEvent: public Variant { // Firmware events FirmwareUpdate, + + ReadyToRoll, }; template @@ -102,14 +107,26 @@ struct InputEvent: public Variant { Intent intent; }; +struct MainLoop; + class InputSource: public Task { public: - InputSource() : Task() {} - InputSource(const char* name) : Task(name) {} - InputSource(Task::State initialState) : Task(initialState) {} - InputSource(const char* name, Task::State initialState) : Task(name, initialState) {} + InputSource() : Task() {init();} + explicit InputSource(const char* name) : Task(name) {init();} + explicit InputSource(Task::State initialState) : Task(initialState) {init();} + InputSource(const char* name, Task::State initialState) : Task(name, initialState) {init();} void loop() override; + void onStart() override; virtual InputEvent read() = 0; +private: + void init(); +#ifdef CONFIG_THREADED_INPUTS + static void readThread(void* data); + MainLoop* m_threadLoop; + static uint8_t m_threadBuf[sizeof(InputEvent) * 32]; + QueueHandle_t m_queue; + StaticQueue_t m_threadQueue; +#endif }; class InputFunc : public InputSource { @@ -128,7 +145,6 @@ private: class BufferedInputSource: public InputSource { public: - BufferedInputSource() : InputSource() {} BufferedInputSource(const char* name) : InputSource(name) {} InputEvent read() override; diff --git a/lib/Figments/MainLoop.cpp b/lib/Figments/MainLoop.cpp index bdb20f6..4296314 100644 --- a/lib/Figments/MainLoop.cpp +++ b/lib/Figments/MainLoop.cpp @@ -16,6 +16,7 @@ MainLoop::dispatch(const InputEvent& evt) void MainLoop::loop() { + s_instance = this; InputEvent evt; while (m_eventBuf.take(evt)) { if (evt.intent == InputEvent::StartThing || evt.intent == InputEvent::StopThing) { @@ -23,10 +24,10 @@ MainLoop::loop() for(auto figmentJob: scheduler.tasks) { if (!strcmp(figmentJob->name, evt.asString())) { if (jobState) { - //Log.notice("Starting %s", figmentJob->name); + Log.trace("Starting task %s", figmentJob->name); figmentJob->start(); } else { - //Log.notice("Stopping %s", figmentJob->name); + Log.trace("Stopping task %s", figmentJob->name); figmentJob->stop(); } } @@ -34,6 +35,9 @@ MainLoop::loop() } for(Task* task : scheduler) { + if (evt.intent == InputEvent::SetPower) { + Log.notice("Event %s", task->name); + } task->handleEvent(evt); } } @@ -44,10 +48,18 @@ MainLoop::loop() Task* slowestTask = NULL; for(Task* task : scheduler) { //unsigned int start = millis(); +#if defined(BOARD_ESP32) or defined(BOARD_ESP8266) unsigned int start = ESP.getCycleCount(); +#else + unsigned int start = millis(); +#endif + Log.verbose("Running %s", task->name); task->loop(); - //unsigned int runtime = millis() - start; - unsigned int runtime = ESP.getCycleCount() - start; +#if defined(BOARD_ESP32) or defined(BOARD_ESP8266) + unsigned int runtime = (ESP.getCycleCount() - start) / 160000; +#else + unsigned int runtime = millis() - start; +#endif frameSpeed += runtime; taskCount++; if (runtime > slowest) { @@ -57,19 +69,22 @@ MainLoop::loop() } frameSpeed = millis() - frameStart; if (frameSpeed >= 23) { - Log.notice("Slow frame: %dms, %d tasks, longest task %s was %dms", frameSpeed, taskCount, slowestTask->name, slowest/160000); + const char* slowestName = (slowestTask->name ? slowestTask->name : "(Unnamed)"); + Log.warning("Slow frame: %dms, %d tasks, longest task %s was %dms", frameSpeed, taskCount, slowestTask->name, slowest); } } void MainLoop::start() { + s_instance = this; Log.notice("*** Starting %d tasks...", scheduler.tasks.size()); Serial.flush(); for(auto task: scheduler) { Log.notice("** Starting %s", task->name); task->start(); } + dispatch(InputEvent::ReadyToRoll); } MainLoop* MainLoop::s_instance; diff --git a/lib/Figments/MainLoop.h b/lib/Figments/MainLoop.h index 208664b..1e6fc8f 100644 --- a/lib/Figments/MainLoop.h +++ b/lib/Figments/MainLoop.h @@ -55,7 +55,7 @@ struct MainLoop { Scheduler scheduler; MainLoop(std::vector &&tasks) - : scheduler(std::move(tasks)) {s_instance = this;} + : scheduler(std::move(tasks)) {} void start(); void loop(); @@ -63,7 +63,7 @@ struct MainLoop { static MainLoop* instance() { return s_instance; } private: - Ringbuf m_eventBuf; + Ringbuf m_eventBuf; static MainLoop* s_instance; }; diff --git a/lib/Figments/Perfcounter.h b/lib/Figments/Perfcounter.h new file mode 100644 index 0000000..5dff3fd --- /dev/null +++ b/lib/Figments/Perfcounter.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +struct PerfCounter { + PerfCounter(const char* name) {} + ~PerfCounter() {} + /*PerfCounter(const char* name) : start(millis()), name(name) {} + ~PerfCounter() {Log.notice("%s: %d", name, millis() - start);}*/ + uint16_t start; + const char* name; +}; diff --git a/lib/Figments/Renderer.cpp b/lib/Figments/Renderer.cpp index acdafb2..3b93603 100644 --- a/lib/Figments/Renderer.cpp +++ b/lib/Figments/Renderer.cpp @@ -9,11 +9,18 @@ Renderer::loop() for(Display* dpy : m_displays) { for(Figment* figment : m_figments) { if (figment->state == Task::Running) { +#if defined(BOARD_ESP32) or defined(BOARD_ESP8266) unsigned int frameStart = ESP.getCycleCount(); +#endif + Log.verbose("Render %s", figment->name); figment->render(dpy); +#if defined(BOARD_ESP32) or defined(BOARD_ESP8266) unsigned int runtime = (ESP.getCycleCount() - frameStart) / 160000; +#else + unsigned int runtime = 0; +#endif if (runtime >= 8) { - Log.notice("SLOW RENDER: %s took %dms!", figment->name, runtime); + Log.warning("SLOW RENDER: %s took %dms!", figment->name, runtime); } } }; diff --git a/lib/Figments/Renderer.h b/lib/Figments/Renderer.h index 8f008df..489a9b3 100644 --- a/lib/Figments/Renderer.h +++ b/lib/Figments/Renderer.h @@ -5,7 +5,7 @@ class Display; struct Renderer: public Task { public: - Renderer(std::vector displays, const std::vector &figments) : Task("Renderer"), m_figments(figments), m_displays(displays) {} + Renderer(std::vector&& displays, const std::vector &figments) : Task("Renderer"), m_figments(figments), m_displays(std::move(displays)) {} void loop() override; void onStart() override; diff --git a/lib/Figments/Ringbuf.h b/lib/Figments/Ringbuf.h index 023be93..f3f3e07 100644 --- a/lib/Figments/Ringbuf.h +++ b/lib/Figments/Ringbuf.h @@ -10,6 +10,11 @@ struct Ringbuf { m_tail = 0; } + T peek(int offset) const { + const int nextHead = (m_head + offset) % Size; + return m_items[nextHead]; + } + bool take(T& dest) { if (m_head == m_tail) { return false; @@ -41,6 +46,22 @@ struct Ringbuf { } return ret; } + + size_t write(Print& stream) { + T val; + size_t ret = 0; + while(take(val)) { + stream.write(val); + } + return ret; + } + + size_t size() { + if (m_tail > m_head) { + return m_tail - m_head; + } + return m_tail + (Size - m_head); + } private: int m_head = 0; int m_tail = 0; diff --git a/lib/Figments/Surface.cpp b/lib/Figments/Surface.cpp index fd34204..5676f1d 100644 --- a/lib/Figments/Surface.cpp +++ b/lib/Figments/Surface.cpp @@ -1,12 +1,29 @@ #include "./Surface.h" #include "./Display.h" #include +#include "Perfcounter.h" Surface::Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end) : start(dpy->coordinateMapping()->virtualToPhysicalCoords(start)), end(dpy->coordinateMapping()->virtualToPhysicalCoords(end)), + virtStart(start), + virtEnd(end), m_display(dpy) { + //assert(start.x <= end.x); + //assert(start.y <= end.y); +} + +Surface::Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end, uint8_t rotation) + : start(dpy->coordinateMapping()->virtualToPhysicalCoords(start)), + end(dpy->coordinateMapping()->virtualToPhysicalCoords(end)), + virtStart(start), + virtEnd(end), + m_display(dpy), + m_rotation(rotation) +{ + //assert(start.x <= end.x); + //assert(start.y <= end.y); } Surface& @@ -30,11 +47,27 @@ Surface::operator+=(const CRGB& color) void Surface::paintWith(std::function func) { - //Log.verbose("Painting startx=%d endx=%d starty=%d endy=%d", start.x, end.x, start.y, end.y); - for(auto x = start.x; x <= end.x; x++) { - for(auto y = start.y; y <= end.y; y++) { - //Log.verbose("x=%d y=%d", x, y); - func(m_display->pixelAt(PhysicalCoordinates{x, y})); + paintShader([=](CRGB& pixel, const VirtualCoordinates&, const PhysicalCoordinates&, const VirtualCoordinates&){ func(pixel); }); +} + +void +Surface::paintShader(Surface::Shader shader) +{ + PerfCounter _("paintShader"); + const uint16_t width = end.x - start.x + 1; + const uint16_t height = end.y - start.y + 1; + const uint8_t xMod = 255 / width; + const uint8_t yMod = 255 / height; + for(auto x = 0; x < width; x++) { + for(auto y = 0; y < height; y++) { + PhysicalCoordinates coords{x + start.x, y + start.y}; + VirtualCoordinates virtCoords{m_display->coordinateMapping()->physicalToVirtualCoords(coords)}; + VirtualCoordinates surfaceCoords{xMod * x, yMod * y}; + //Log.notice("width=%d height=%d vx=%d vy=%d sx=%d sy=%d x=%d y=%d px=%d py=%d", width, height, start.x, start.y, x, y, coords.x, coords.y); + // 256 = 1.0 + // 128 = 0.0 + // 0 = 1.0 + shader(m_display->pixelAt(coords), virtCoords, coords, surfaceCoords); } } } diff --git a/lib/Figments/Surface.h b/lib/Figments/Surface.h index 2e221f0..28400c5 100644 --- a/lib/Figments/Surface.h +++ b/lib/Figments/Surface.h @@ -1,3 +1,5 @@ +#pragma once + #include #include "./Geometry.h" #include @@ -7,15 +9,30 @@ class Display; class Surface { public: Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end); + Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end, uint8_t rotation); Surface& operator=(const CRGB& color); Surface& operator+=(const CRGB& color); + template + Surface& operator|=(const T& val) { + paintWith([&](CRGB& pixel) { + pixel |= val; + }); + return *this; + } - void paintWith(std::function func); + using Shader = std::function; + using BrushFunc = std::function; + + void paintWith(BrushFunc func); + void paintShader(Shader shader); const PhysicalCoordinates start; const PhysicalCoordinates end; + const VirtualCoordinates virtStart; + const VirtualCoordinates virtEnd; private: Display* m_display; + uint8_t m_rotation = 0; }; diff --git a/lib/Figments/library.json b/lib/Figments/library.json new file mode 100644 index 0000000..5c47a44 --- /dev/null +++ b/lib/Figments/library.json @@ -0,0 +1,8 @@ +{ + "name": "Figments", + "version": "0.3.0", + "description": "An embedded graphics rendering engine", + "keywords": ["FastLED", "esp32", "esp8266"], + "frameworks": ["arduino"], + "platforms": ["espressif32", "espressif8266"] +} diff --git a/platformio.ini b/platformio.ini index 23664ca..080a69c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,47 +9,194 @@ ; https://docs.platformio.org/page/projectconf.html [common_env_data] -src_filter = "+<*> -<.git/> -<.svn/> -" +src_filter = "+<*> -<.git/> -<.svn/> - -" +lib_ldf_mode = chain+ +src_build_flags = + -DRENDERBUG_VERSION=3 + -DRENDERBUG_LED_PIN=14 + -DRENDERBUG_LED_PACKING=RGB + -DDEFAULT_PATTERN_INDEX=0 +lib_deps_external = + fastled/FastLED@^3.4.0 + thijse/ArduinoLog@1.0.3 + bblanchon/ArduinoJson@^6.17.3 + +[config_u8display] +src_build_flags = + -DCONFIG_U8DISPLAY +lib_deps = + olikraus/U8g2@2.28.8 +src_filter = "+" + +[config_mqtt] +src_build_flags = + -DCONFIG_MQTT +lib_deps = + knolleary/PubSubClient@^2.8.0 +src_filter = "+" + +[config_wifi] +src_build_flags = + -DCONFIG_WIFI +src_filter = "+" + +[config_bluetooth] +src_build_flags = + -DCONFIG_BLUETOOTH +src_filter = "+" +lib_deps = + BluetoothSerial + +[config_ota] +src_build_flags = + -DCONFIG_OTA +src_filter = "+" +lib_deps = + ArduinoOTA + ESP8266mDNS + +[config_nocolor] +src_build_flags = + -DCONFIG_NO_COLORDATA + +[config_buttons] +src_build_flags = + -DCONFIG_BUTTONS +src_filter = "+" + +[config_mpu5060] +src_build_flags = + -DCONFIG_MPU5060 +src_filter = "+" + +[env:teensy] +extends = config_nocolor +platform = teensy +board = teensy31 +framework = arduino +src_build_flags = + ${common_env_data.src_build_flags} + ${config_nocolor.src_build_flags} + -DPLATFORM_ARDUINO + -DBOARD_TEENSY +lib_deps = + ${common_env_data.lib_deps_external} +src_filter = "${common_env_data.src_filter}" + +[env:bike_teensy] +extends = env:teensy +src_build_flags= + ${env:teensy.src_build_flags} + -DRENDERBUG_LED_PIN=11 + -DRENDERBUG_LED_PACKING=GRB + -DDEFAULT_PATTERN_INDEX=1 + +[env:bike] +extends = env:esp32, config_u8display +src_filter = "${env:esp32.src_filter} ${config_u8display.src_filter}" +lib_deps = + ${env:esp32.lib_deps} + ${config_u8display.lib_deps} +src_build_flags = + ${env:esp32.src_build_flags} + ${config_u8display.src_build_flags} +build_type = debug + +[env:bike_ble] +extends = env:bike +lib_deps = + ${env:bike.lib_deps} + nkolban/ESP32 BLE Arduino@1.0.1 +src_build_flags = + ${env:bike.src_build_flags} [env:esp32] +extends = config_nocolor platform = espressif32 board = featheresp32 framework = arduino -build_flags = +board_build.filesystem = littlefs +src_build_flags = + ${common_env_data.src_build_flags} + ${config_nocolor.src_build_flags} -DPLATFORM_ARDUINO -DBOARD_ESP32 - -DCONFIG_NO_COLORDATA -; -DCORE_DEBUG_LEVEL=5 +; -DCONFIG_THREADED_INPUTS lib_deps = - fastled/FastLED@^3.4.0 - thijse/ArduinoLog@^1.0.3 - knolleary/PubSubClient@^2.8.0 - bblanchon/ArduinoJson@^6.17.3 - sstaub/NTP@^1.4.0 - arduino-libraries/NTPClient@^3.1.0 -src_filter = "${common_env_data.src_filter} +" + ${common_env_data.lib_deps_external} +src_filter = "${common_env_data.src_filter}" board_build.partitions = no_ota.csv -;build_type = debug -[env:cyberplague] -extends = env:esp32 -board_build.partitions = no_ota.csv +[env:esp8266-12f] +extends = env:esp8266 +board = esp12e [env:esp8266] platform = espressif8266 board = huzzah framework = arduino -build_flags = +booard_build.filesystem = littlefs +src_build_flags = + ${common_env_data.src_build_flags} -DPLATFORM_ARDUINO -DBOARD_ESP8266 -lib_deps = - fastled/FastLED@^3.4.0 - thijse/ArduinoLog@^1.0.3 - knolleary/PubSubClient@^2.8.0 - bblanchon/ArduinoJson@^6.17.3 - sstaub/NTP@^1.4.0 + -DCORE_DEBUG_LEVEL=5 + -fstack-protector +lib_deps = + ${common_env_data.lib_deps_external} arduino-libraries/NTPClient@^3.1.0 -src_filter = "${common_env_data.src_filter} +" +src_filter = "${common_env_data.src_filter}" + +[env:cyberplague] +extends = env:esp32, config_bluetooth +src_filter = "${env:esp32.src_filter} ${config_bluetooth.src_filter}" +lib_deps = + ${env:esp32.lib_deps} + ${config_bluetooth.lib_deps} +src_build_flags = + ${env:esp32.src_build_flags} + ${config_bluetooth.src_build_flags} + -DRENDERBUG_LED_PIN=13 + -DDEFAULT_PATTERN_INDEX=1 + +[env:cyberplague_wifi] +extends = env:esp32, config_wifi, config_mqtt +src_filter = "${env:esp32.src_filter} ${config_wifi.src_filter} ${config_mqtt.src_filter}" +lib_deps = + ${env:esp32.lib_deps} + ${config_mqtt.lib_deps} +src_build_flags = + ${env:esp32.src_build_flags} + ${config_mqtt.src_build_flags} + ${config_wifi.src_build_flags} + +[env:prototype] +extends = env:esp32, config_buttons, config_mpu5060 +src_filter = "${env:esp32.src_filter} ${config_buttons.src_filter} ${config_mpu5060.src_filter}" + +[env:home_lighting] +extends = env:esp8266, config_wifi, config_mqtt, config_ota +src_filter = "${env:esp32.src_filter} ${config_ota.src_filter} ${config_wifi.src_filter} ${config_mqtt.src_filter}" +src_build_flags = + ${env:esp8266.src_build_flags} + ${config_mqtt.src_build_flags} + ${config_wifi.src_build_flags} + ${config_ota.src_build_flags} +lib_deps = + ${env:esp8266.lib_deps} + ${config_mqtt.lib_deps} + ESP8266WiFi + ${config_ota.lib_deps} + +[env:home_lighting_grb] +extends = env:home_lighting +src_build_flags = + ${env:home_lighting.src_build_flags} + -DRENDERBUG_LED_PACKING=GRB + +[env:home_lighting-12f] +extends = env:home_lighting +board = esp12e ;[env:photon] ;platform = particlephoton diff --git a/src/Config.cpp b/src/Config.cpp index 2637176..83293a0 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -8,9 +8,13 @@ constexpr uint16_t HardwareConfig::MAX_LED_NUM; HardwareConfig HardwareConfig::load() { HardwareConfig ret; +#ifndef BOARD_TEENSY EEPROM.begin(sizeof(ret)); +#endif EEPROM.get(0, ret); +#ifndef BOARD_TEENSY EEPROM.end(); +#endif Log.notice("Loaded config version %d, CRC %d", ret.version, ret.checksum); return ret; } @@ -19,10 +23,14 @@ void HardwareConfig::save() { HardwareConfig dataCopy{*this}; dataCopy.checksum = getCRC(); +#ifndef BOARD_TEENSY EEPROM.begin(sizeof(dataCopy)); +#endif EEPROM.put(0, dataCopy); +#ifndef BOARD_TEENSY EEPROM.commit(); EEPROM.end(); +#endif } LinearCoordinateMapping @@ -72,13 +80,6 @@ ConfigService::onStart() m_coordMap = m_config.toCoordMap(); Log.notice("Configured to use %d pixels, starting at %d", m_config.data.pixelCount, m_config.data.startPixel); - /*Log.notice("Loading task states..."); - for(int i = 0; i < 32; i++) { - auto svc = m_config.data.serviceStates[i]; - if (strnlen(svc.name, 16) > 0) { - Log.notice("* %s: %s", svc.name, svc.isDisabled? "DISABLED" : "ENABLED"); - } - }*/ } void @@ -112,3 +113,4 @@ ConfigService::handleEvent(const InputEvent &evt) } STATIC_ALLOC(ConfigService); +STATIC_TASK(ConfigService); diff --git a/src/Config.h b/src/Config.h index a0eda84..c5bf96c 100644 --- a/src/Config.h +++ b/src/Config.h @@ -1,17 +1,75 @@ #pragma once #include -//#define PLATFORM_PHOTON -//#define PLATFORM_ARDUINO -#define RENDERBUG_VERSION 2 +struct MaskCoordinateMapping : CoordinateMapping { + struct Span { + int length = 0; + int x = 0; + int y = 0; + + Span(int length, int x, int y) : length(length), x(x), y(y) {} + }; + + Span displayMap[13] = { + {6, 0, 6}, + {6, 1, 6}, + {7, 2, 6}, + {9, 3, 4}, + {14, 4, 4}, + {17, 5, 0}, + {12, 6, 2}, + {18, 7, 0}, + {14, 8, 4}, + {9, 9, 5}, + {7, 10, 4}, + {6, 11, 5}, + {6, 12, 5} + }; + + VirtualCoordinates physicalToVirtualCoords(const PhysicalCoordinates localCoords) const override { + int offset = localCoords.x; + for(int i = 0; i < 12; i++) { + if (offset > displayMap[i].length) { + offset -= displayMap[i].length; + } else { + return VirtualCoordinates{i, offset}; + } + } + } + + PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const override { + const uint8_t spanIdx = scale8(12, virtualCoords.x); + const uint8_t spanOffset = scale8(17, virtualCoords.y); + return PhysicalCoordinates{spanIdx, spanOffset}; + } + + int physicalCoordsToIndex(const PhysicalCoordinates localCoords) const override { + uint8_t idx = 0; + bool inverse = false; + for(int i = 0; i < localCoords.x; i++) { + idx += displayMap[i].length; + inverse = !inverse; + } + if (inverse) { + idx += std::max(0, displayMap[localCoords.x].length - 1 - std::max(0, (int)localCoords.y - displayMap[localCoords.x].y)); + } else { + idx += std::min((int)displayMap[localCoords.x].length - 1, std::max(0, (int)localCoords.y - displayMap[localCoords.x].y)); + } + return idx; + } + + unsigned int physicalPixelCount() const override { + int total = 0; + for(int i = 0; i < 13; i++) { + total += displayMap[i].length; + } + return total; + } +}; struct HardwareConfig { - uint8_t version = 2; + uint8_t version = 3; uint8_t checksum = 0; - struct TaskState { - char name[16] = {0}; - bool isDisabled = false; - }; struct Data { uint16_t pixelCount = 255; uint16_t startPixel = 0; @@ -19,7 +77,6 @@ struct HardwareConfig { uint8_t lastGreen = 255; uint8_t lastBlue = 255; char lastScene[16] = {0}; - TaskState serviceStates[32]; }; Data data; @@ -28,13 +85,7 @@ struct HardwareConfig { bool isValid() const; LinearCoordinateMapping toCoordMap() const; - static constexpr uint16_t MAX_LED_NUM = 255; -#ifdef PLATFORM_PHOTON - static constexpr bool HAS_MPU_6050 = true; -#else - static constexpr bool HAS_MPU_6050 = false; -#endif private: uint8_t getCRC() const; @@ -50,9 +101,10 @@ struct ConfigService: public Task { void onStart(); void loop() override; void handleEvent(const InputEvent &evt) override; - const LinearCoordinateMapping* coordMap() const { return &m_coordMap; } + const CoordinateMapping* coordMap() const { return /*&m_maskMap;*/ &m_coordMap; } private: HardwareConfig m_config; + MaskCoordinateMapping m_maskMap; LinearCoordinateMapping m_coordMap; }; diff --git a/src/LogService.cpp b/src/LogService.cpp index 7bc33e3..749aaf0 100644 --- a/src/LogService.cpp +++ b/src/LogService.cpp @@ -2,99 +2,96 @@ #include "Static.h" #include +static const char* eventValue(const InputEvent& evt); + +const char* +LogService::intentName(InputEvent::Intent intent) +{ + switch(intent) { + case InputEvent::BeatDetect: + return "beat-detection"; + case InputEvent::Beat: + return "beat"; + case InputEvent::ReadyToRoll: + return "ready-to-roll"; + case InputEvent::PowerToggle: + return "power-toggle"; + case InputEvent::SetPower: + return "set-power"; + case InputEvent::PreviousPattern: + return "previous-pattern"; + case InputEvent::NextPattern: + return "next-pattern"; + case InputEvent::SetPattern: + return "set-pattern"; + case InputEvent::SetColor: + return "set-color"; + case InputEvent::Acceleration: + return "acceleration"; + case InputEvent::UserInput: + return "user"; + case InputEvent::SetBrightness: + return "set-brightness"; + case InputEvent::FirmwareUpdate: + return "firmware-update"; + case InputEvent::NetworkStatus: + return "network-status"; + case InputEvent::NetworkActivity: + return "network-activity"; + case InputEvent::StartThing: + return "start-thing"; + case InputEvent::StopThing: + return "stop-thing"; + case InputEvent::SetDisplayOffset: + return "set-display-offset"; + case InputEvent::SetDisplayLength: + return "set-display-length"; + case InputEvent::SaveConfigurationRequest: + return "save-configuration"; + default: + return NULL; + } +} + +char LogService::s_valueBuf[255]; + +const char* +LogService::eventValue(const InputEvent& evt) +{ + switch(evt.type) { + case InputEvent::Null: + snprintf(s_valueBuf, sizeof(s_valueBuf), "null");break; + case InputEvent::Integer: + snprintf(s_valueBuf, sizeof(s_valueBuf), "%d %02x", evt.asInt(), evt.asInt());break; + case InputEvent::String: + snprintf(s_valueBuf, sizeof(s_valueBuf), "\"%s\"", evt.asString());break; + case InputEvent::Color: + snprintf(s_valueBuf, sizeof(s_valueBuf), "[%d, %d, %d]", evt.asRGB().r, evt.asRGB().g, evt.asRGB().b);break; + } + return s_valueBuf; +} + void LogService::handleEvent(const InputEvent& evt) { if (evt.intent != InputEvent::None) { - const char* sourceName; - switch(evt.intent) { - case InputEvent::PowerToggle: - sourceName = "power-toggle"; - break; - case InputEvent::SetPower: - sourceName = "set-power"; - break; - case InputEvent::PreviousPattern: - sourceName = "previous-pattern"; - break; - case InputEvent::NextPattern: - sourceName = "next-pattern"; - break; - case InputEvent::SetPattern: - sourceName = "set-pattern"; - break; - case InputEvent::SetColor: - sourceName = "set-color"; - break; - case InputEvent::Acceleration: - sourceName = "acceleration"; - break; - case InputEvent::UserInput: - sourceName = "user"; - break; - case InputEvent::SetBrightness: - sourceName = "set-brightness"; - break; - case InputEvent::FirmwareUpdate: - sourceName = "firmware-update"; - break; - case InputEvent::NetworkStatus: - sourceName = "network-status"; - break; - case InputEvent::NetworkActivity: - sourceName = "network-activity"; - break; - case InputEvent::StartThing: - sourceName = "start-thing"; - break; - case InputEvent::StopThing: - sourceName = "stop-thing"; - break; - case InputEvent::SetDisplayOffset: - sourceName = "set-display-offset"; - break; - case InputEvent::SetDisplayLength: - sourceName = "set-display-length"; - break; - case InputEvent::SaveConfigurationRequest: - sourceName = "save-configuration"; - break; - default: - sourceName = 0; - break; - } - char valueBuf[255]; - switch(evt.type) { - case InputEvent::Null: - snprintf(valueBuf, sizeof(valueBuf), "null");break; - case InputEvent::Integer: - snprintf(valueBuf, sizeof(valueBuf), "%d %02x", evt.asInt(), evt.asInt());break; - case InputEvent::String: - snprintf(valueBuf, sizeof(valueBuf), "\"%s\"", evt.asString());break; - case InputEvent::Color: - snprintf(valueBuf, sizeof(valueBuf), "[%d, %d, %d]", evt.asRGB().r, evt.asRGB().g, evt.asRGB().b);break; - } - char buf[255 * 2]; - if (sourceName == 0) { - snprintf(buf, sizeof(buf), "{\"intent\": %d, \"value\": %s}", evt.intent, valueBuf); - } else { - snprintf(buf, sizeof(buf), "{\"intent\": \"%s\", \"value\": %s}", sourceName, valueBuf); - } if (evt.intent != m_lastEvent.intent) { if (m_duplicateEvents > 0) { - Log.notice("Suppressed reporting %d duplicate events.", m_duplicateEvents); + Log.trace("Suppressed reporting %d duplicate events.", m_duplicateEvents); + } + const char* sourceName = intentName(evt.intent);; + const char* valueBuf = eventValue(evt); + if (sourceName == 0) { + Log.trace("Event: intent: %d value: %s", evt.intent, valueBuf); + } else { + Log.trace("Event: intent: %s value: %s", sourceName, valueBuf); } - Log.verbose("Event: %s", buf); m_duplicateEvents = 0; m_lastEvent = evt; - //Particle.publish("renderbug/event", buf, PRIVATE); } else { m_duplicateEvents++; } - /*if (m_online) { - } else { - Log.info("[offline] Event: %s", buf); - }*/ } } STATIC_ALLOC(LogService); +STATIC_TASK(LogService); diff --git a/src/LogService.h b/src/LogService.h index 32f3e49..129c1bc 100644 --- a/src/LogService.h +++ b/src/LogService.h @@ -6,7 +6,11 @@ class LogService : public Task { void handleEvent(const InputEvent& event) override; void loop() override {} + static const char* intentName(InputEvent::Intent intent); + static const char* eventValue(const InputEvent& evt); + private: + static char s_valueBuf[255]; uint16_t m_duplicateEvents = 0; InputEvent m_lastEvent; }; diff --git a/src/Platform.cpp b/src/Platform.cpp index 4998c61..5af4b42 100644 --- a/src/Platform.cpp +++ b/src/Platform.cpp @@ -1,12 +1,15 @@ #include "Platform.h" #include #include "Static.h" +#include #ifdef BOARD_ESP32 +#ifdef CONFIG_WIFI #include +#endif #include -#include #elif defined(BOARD_ESP8266) +#ifdef CONFIG_WIFI #include #include #include @@ -16,11 +19,14 @@ WiFiUDP wifiUdp; //NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7); NTPClient timeClient(wifiUdp, "10.0.0.1", 3600 * -7); #endif +#endif #ifdef PLATFORM_PHOTON STARTUP(BootOptions::initPins()); #else +#ifdef CONFIG_MQTT #include "platform/arduino/MQTTTelemetry.h" +#endif void printNewline(Print* logOutput) { logOutput->print("\r\n"); @@ -33,6 +39,8 @@ int printEspLog(const char* fmt, va_list args) #endif int Platform::s_timezone = 0; +Platform::TaskRegistration* Platform::firstTask = NULL; +Platform::TaskRegistration* Platform::lastTask = NULL; const char* Platform::name() @@ -78,7 +86,11 @@ Platform::preSetup() Log.notice("\xf0\x9f\x94\x8c Serial connected"); } #else - Log.begin(LOG_LEVEL_VERBOSE, Static::instance()->logPrinter()); +#ifdef CONFIG_MQTT + Log.begin(LOG_LEVEL_TRACE, Static::instance()->logPrinter()); +#else + Log.begin(LOG_LEVEL_TRACE, &Serial); +#endif Log.setSuffix(printNewline); #endif @@ -101,8 +113,10 @@ Platform::setup() constexpr int dst = 1; configTime(s_timezone* 3600, 3600 * dst, "pool.ntp.org"); #elif defined(BOARD_ESP8266) +#ifdef CONFIG_WIFI timeClient.begin(); #endif +#endif } void @@ -114,15 +128,24 @@ Platform::bootSplash() Log.notice(u8" 3: Serial - %d", bootopts.isSerial); Log.notice(u8" 4: Flash - %d", bootopts.isFlash); #endif + + Log.trace("Registered tasks:"); + auto it = beginTasks(); + while (it != endTasks()) { + Log.trace((*it)->name); + ++it; + } } void Platform::loop() { #ifdef BOARD_ESP8266 +#ifdef CONFIG_WIFI if (WiFi.status() == WL_CONNECTED) { timeClient.update(); } +#endif ESP.wdtFeed(); #elif defined(BOARD_ESP32) esp_task_wdt_reset(); @@ -141,13 +164,18 @@ Platform::getLocalTime(struct tm* timedata) return false; #elif defined(BOARD_ESP32) time_t rawtime; + memset(&rawtime, 0, sizeof(rawtime)); time(&rawtime); (*timedata) = (*localtime(&rawtime)); return (timedata->tm_year > (2016-1990)); - //return getLocalTime(timedata); #else +#ifdef CONFIG_WIFI timedata->tm_hour = timeClient.getHours(); timedata->tm_min = timeClient.getMinutes(); +#else + memset(timedata, sizeof(struct tm), 0); + return false; +#endif return true; #endif } @@ -172,3 +200,4 @@ char Platform::s_deviceID[15]; STATIC_ALLOC(Platform); +STATIC_TASK(Platform); diff --git a/src/Platform.h b/src/Platform.h index ecaeced..c07ee15 100644 --- a/src/Platform.h +++ b/src/Platform.h @@ -13,14 +13,7 @@ class Platform : public Task { static int getTimezone() { return s_timezone; } static void addLEDs(CRGB* leds, unsigned int ledCount) { -#ifdef PLATFORM_PHOTON - FastLED.addLeds(leds, ledCount); -#elif defined(BOARD_ESP32) - FastLED.addLeds(leds, ledCount); -#else - //FastLED.addLeds(leds, ledCount); - FastLED.addLeds(leds, ledCount); -#endif + FastLED.addLeds(leds, ledCount); } static const char* name(); @@ -39,5 +32,58 @@ class Platform : public Task { void loop() override; static bool getLocalTime(struct tm* timedata); static const char* deviceID(); -}; + struct TaskRegistration { + Task* task = 0; + TaskRegistration* next = 0; + + TaskRegistration(Task* task) : task(task) {} + }; + + static TaskRegistration* firstTask; + static TaskRegistration* lastTask; + + static void registerTask(TaskRegistration* reg) { + if (firstTask == NULL) { + firstTask = reg; + lastTask = firstTask; + } else { + lastTask->next = reg; + lastTask = reg; + } + } + + struct task_iterator: public std::iterator { + TaskRegistration* cur; + explicit task_iterator() : cur(NULL) {} + explicit task_iterator(TaskRegistration* head) : cur(head) {} + task_iterator& operator++() { + if (cur) { + cur = cur->next; + } + return *this; + } + + task_iterator operator++(int) {task_iterator ret = *this; ++(*this); return ret;} + bool operator==(task_iterator other) const { return cur == other.cur; } + bool operator!=(task_iterator other) const { return !(*this == other); } + Task* operator*() const { return cur->task; } + }; + + static task_iterator beginTasks() { + return task_iterator(firstTask); + } + + static task_iterator endTasks() { + return task_iterator(NULL); + } + + static void restart() { +#ifdef BOARD_ESP8266 + ESP.wdtDisable(); + ESP.restart(); +#elif defined(BOARD_ESP32) + ESP.restart(); +#endif + } +}; diff --git a/src/Sequencer.cpp b/src/Sequencer.cpp index 1ccbe04..c07d2cc 100644 --- a/src/Sequencer.cpp +++ b/src/Sequencer.cpp @@ -3,6 +3,14 @@ Sequencer::Sequencer(std::vector &&scenes) : Task("SceneSequencer"), + m_idx(0), + m_scenes(std::move(scenes)) +{ +} + +Sequencer::Sequencer(std::vector &&scenes, int startIndex) : + Task("SceneSequencer"), + m_idx(startIndex), m_scenes(std::move(scenes)) { } @@ -22,16 +30,28 @@ Sequencer::scenes() const return m_scenes; } +void +Sequencer::onStart() +{ +} + void Sequencer::handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::ReadyToRoll) { + Log.notice("Starting pattern %s!", m_scenes[m_idx].name); + for(const char* pattern : m_scenes[m_idx].patterns) { + Log.verbose("Starting pattern task %s", pattern); + MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern}); + } + } if (evt.intent == InputEvent::SetPattern && evt.asString() == m_scenes[m_idx].name) { return; } if (evt.intent == InputEvent::SetPattern || evt.intent == InputEvent::NextPattern || evt.intent == InputEvent::PreviousPattern) { Log.notice("Switching pattern!"); for(const char* pattern : m_scenes[m_idx].patterns) { - //Log.notice("Stopping %s", pattern); + Log.verbose("Stopping pattern task %s", pattern); MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern}); } @@ -57,7 +77,7 @@ Sequencer::handleEvent(const InputEvent& evt) } for(const char* pattern : m_scenes[m_idx].patterns) { - //Log.notice("Starting %s", pattern); + Log.verbose("Starting pattern task %s", pattern); MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern}); } } diff --git a/src/Sequencer.h b/src/Sequencer.h index 7bda5e8..8f5cdd3 100644 --- a/src/Sequencer.h +++ b/src/Sequencer.h @@ -13,14 +13,16 @@ public: }; Sequencer(std::vector &&scenes); + Sequencer(std::vector &&scenes, int startingIndex); void loop() override; + void onStart() override; void handleEvent(const InputEvent& evt) override; const char* currentSceneName(); const std::vector scenes() const; private: - int m_idx = 0; + int m_idx; std::vector m_scenes; }; diff --git a/src/Static.h b/src/Static.h index 54737f6..ec457c9 100644 --- a/src/Static.h +++ b/src/Static.h @@ -1,4 +1,5 @@ #pragma once +#include "Platform.h" // Utility class mostly for when certain inputs need singleton callback handlers template class Static { @@ -10,7 +11,24 @@ private: static T* s_instance; }; +template struct StaticTaskRegistration : public Platform::TaskRegistration { + StaticTaskRegistration() : Platform::TaskRegistration(Static::instance()) { + Platform::registerTask(this); + } +}; + +struct AutoTaskRegistration : public Platform::TaskRegistration { + explicit AutoTaskRegistration(Task* task) : Platform::TaskRegistration(task) { + Platform::registerTask(this); + } +}; + #define NAMED_STATIC_ALLOC(Cls, StaticName) static Cls _staticAlloc__ ## StaticName;\ template<> Cls* Static::s_instance=&_staticAlloc__ ## StaticName; #define STATIC_ALLOC(Cls) NAMED_STATIC_ALLOC(Cls, Cls) + +#define NAMED_STATIC_TASK(Cls, StaticName) static StaticTaskRegistration _staticTask_ ## StaticName; +#define STATIC_TASK(Cls) NAMED_STATIC_TASK(Cls, Cls) + +#define REGISTER_TASK(TaskName) static AutoTaskRegistration _autoTask__ ## TaskName(&TaskName); diff --git a/src/animations/Chimes.cpp b/src/animations/Chimes.cpp index 31efa44..1babca9 100644 --- a/src/animations/Chimes.cpp +++ b/src/animations/Chimes.cpp @@ -1,75 +1,74 @@ -#include "../Figments/Figments.h" -#include "../sprites/Chime.h" -#include "../sprites/Blob.h" +#include "Chimes.h" +#include "../Static.h" -#define CHIME_LENGTH 23 -#define CHIME_COUNT 4 -#define BLOB_COUNT 10 +ChimesAnimation::ChimesAnimation() : Figment("Chimes", Task::Stopped) { +} -class ChimesAnimation: public Figment { -public: - ChimesAnimation(Task::State initialState) : Figment("Chimes", initialState) { - m_chimes.forEach([](Chime &chime) { - chime.setPos(random(Chime::Length * 5)); - chime.setHue(random(255)); - chime.setSpeed(random(90) + 138); - chime.setBrightness(200); - chime.setOffset(random(1024)); - }); - m_blobs.forEach([](Blob &blob) { - blob.setPos(random(255)); - blob.setHue(random(255)); - blob.setBrightness(random(255)); - if (random(255) % 2) { - blob.setVelocity(-1); - } else { - blob.setVelocity(1); +void ChimesAnimation::randomize() { + m_isRandom = true; + m_chimes.forEach([](Chime &chime) { + chime.setPos(random(Chime::Length * 5)); + chime.setHue(random(255)); + chime.setSpeed(random(90) + 138); + chime.setBrightness(200); + chime.setOffset(random(1024)); + }); + m_blobs.forEach([](Blob &blob) { + blob.setPos(random(255)); + blob.setHue(random(255)); + blob.setBrightness(random(255)); + if (random(255) % 2) { + blob.setVelocity(-1); + } else { + blob.setVelocity(1); + } + }); +} + +void ChimesAnimation::handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::UserInput) { + if (strcmp(evt.asString(), "blobs") == 0) { + m_blobs.toggle(); + } else if (strcmp(evt.asString(), "chimes") == 0) { + m_chimes.toggle(); } - }); + } else if (evt.intent == InputEvent::SetColor) { + m_flashBrightness.set(255, 0); + m_flashColor = evt.asRGB(); + uint8_t flashHue = rgb2hsv_approximate(m_flashColor).hue; + m_blobs.forEach([&](Blob& blob) { + blob.setHue(flashHue); + }); + m_chimes.forEach([&](Chime& chime) { + chime.setHue(flashHue); + }); + } else if (evt.intent == InputEvent::Beat) { + m_isRandom = false; } +} - void handleEvent(const InputEvent& evt) override { - if (evt.intent == InputEvent::UserInput) { - if (strcmp(evt.asString(), "blobs") == 0) { - m_blobs.toggle(); - } else if (strcmp(evt.asString(), "chimes") == 0) { - m_chimes.toggle(); - } - } else if (evt.intent == InputEvent::SetColor) { - m_flashBrightness.set(255, 0); - m_flashColor = evt.asRGB(); - uint8_t flashHue = rgb2hsv_approximate(m_flashColor).hue; - m_blobs.forEach([&](Blob& blob) { - blob.setHue(flashHue); - }); - m_chimes.forEach([&](Chime& chime) { - chime.setHue(flashHue); - }); - } - } +void ChimesAnimation::loop() { + if (!m_isRandom) { + randomize(); + } + m_chimes.update(); + m_blobs.update(); + m_flashColor.update(); + EVERY_N_MILLISECONDS(5) { + m_flashBrightness.update(); + } +} - void loop() override { - m_chimes.update(); - m_blobs.update(); - m_flashColor.update(); - EVERY_N_MILLISECONDS(5) { - m_flashBrightness.update(); - } - } +void ChimesAnimation::render(Display* dpy) const { + m_chimes.render(dpy); + m_blobs.render(dpy); + Surface fullSurface(dpy, {0, 0}, {255, 0}); + CRGB scaledColor = CRGB(m_flashColor).nscale8_video(std::max((uint8_t)10, ease8InOutCubic(m_flashBrightness))); + fullSurface.paintWith([&](CRGB& pixel) { + pixel = blend(scaledColor, pixel, 200); + //pixel = scaledColor; + }); +} - void render(Display* dpy) const override { - m_chimes.render(dpy); - m_blobs.render(dpy); - Surface fullSurface(dpy, {0, 0}, {255, 0}); - CRGB scaledColor = CRGB(m_flashColor).nscale8_video(std::max((uint8_t)10, ease8InOutCubic(m_flashBrightness))); - fullSurface.paintWith([&](CRGB& pixel) { - pixel = blend(scaledColor, pixel, 200); - //pixel = scaledColor; - }); - } - - SpriteList, CHIME_COUNT> m_chimes; - SpriteList m_blobs; - AnimatedRGB m_flashColor; - AnimatedNumber m_flashBrightness; -}; +STATIC_ALLOC(ChimesAnimation); +STATIC_TASK(ChimesAnimation); diff --git a/src/animations/Chimes.h b/src/animations/Chimes.h new file mode 100644 index 0000000..108a372 --- /dev/null +++ b/src/animations/Chimes.h @@ -0,0 +1,24 @@ +#include "../Figments/Figments.h" +#include "../sprites/Chime.h" +#include "../sprites/Blob.h" + +#define CHIME_LENGTH 23 +#define CHIME_COUNT 4 +#define BLOB_COUNT 10 + +class ChimesAnimation: public Figment { +public: + ChimesAnimation(); + void handleEvent(const InputEvent& evt) override; + void loop() override; + void render(Display* dpy) const override; + +private: + SpriteList, CHIME_COUNT> m_chimes; + SpriteList m_blobs; + AnimatedRGB m_flashColor; + AnimatedNumber m_flashBrightness; + + void randomize(); + bool m_isRandom = false; +}; diff --git a/src/animations/Drain.cpp b/src/animations/Drain.cpp index 67de550..a9ab2fa 100644 --- a/src/animations/Drain.cpp +++ b/src/animations/Drain.cpp @@ -1,64 +1,58 @@ -#include -#include +#include "Drain.h" +#include "../Static.h" -class DrainAnimation: public Figment { -public: +DrainAnimation::DrainAnimation() : Figment("Drain", Task::Stopped) {} - DrainAnimation(Task::State initialState) : Figment("Drain", initialState) {} - - void loop() override { - EVERY_N_MILLISECONDS(8) { - m_pos++; - m_fillColor.update(); - } - EVERY_N_MILLISECONDS(50) { - if (random(255) >= 10) { - m_burst -= m_burst / 10; - } +void DrainAnimation::loop() { + EVERY_N_MILLISECONDS(8) { + m_pos++; + m_fillColor.update(); + } + EVERY_N_MILLISECONDS(50) { + if (random(255) >= 10) { + m_burst -= m_burst / 10; } } +} - void handleEvent(const InputEvent& event) override { - if (event.intent == InputEvent::SetColor) { - m_fillColor = event.asRGB(); - } else if (event.intent == InputEvent::Acceleration) { - m_pos += log10(event.asInt()); - uint16_t burstInc = event.asInt() / 6; - m_burst = (m_burst > 0xFFFF - burstInc) ? 0xFFFF : m_burst + burstInc; - } +void DrainAnimation::handleEvent(const InputEvent& event) { + if (event.intent == InputEvent::SetColor) { + m_fillColor = event.asRGB(); + } else if (event.intent == InputEvent::Acceleration) { + m_pos += log10(event.asInt()); + uint16_t burstInc = event.asInt() / 6; + m_burst = (m_burst > 0xFFFF - burstInc) ? 0xFFFF : m_burst + burstInc; + } +} + +void DrainAnimation::render(Display* dpy) const { + dpy->clear(); + Surface leftPanel{dpy, {0, 0}, {128, 0}}; + Surface rightPanel{dpy, {128, 0}, {255, 0}}; + fillRange(dpy, leftPanel.start, leftPanel.end, rgb2hsv_approximate(m_fillColor)); + fillRange(dpy, rightPanel.end, rightPanel.start, rgb2hsv_approximate(m_fillColor)); +} + +void DrainAnimation::fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const { + int length = end.x - start.x; + int direction = 1; + if (length < 0) { + direction = -1; } - AnimatedRGB m_fillColor; + uint8_t frac = length == 0 ? 0 : 255 / std::abs(length); + for(int i = 0; i < std::abs(length); i++) { + auto coords = PhysicalCoordinates((start.x + (i * direction)), 0); - void render(Display* dpy) const override { - dpy->clear(); - Surface leftPanel{dpy, {0, 0}, {128, 0}}; - Surface rightPanel{dpy, {128, 0}, {255, 0}}; - fillRange(dpy, leftPanel.start, leftPanel.end, rgb2hsv_approximate(m_fillColor)); - fillRange(dpy, rightPanel.end, rightPanel.start, rgb2hsv_approximate(m_fillColor)); + const uint8_t localScale = inoise8(i * 80, m_pos * 3); + const uint8_t dimPosition = lerp8by8(50, 190, scale8(sin8((frac * i) / 2), localScale)); + const uint8_t withBurst = ease8InOutCubic(lerp16by16(dimPosition, 255, m_burst)); + auto scaledColor = CHSV(baseColor.hue, lerp8by8(100, 255, localScale), withBurst); + + CRGB src(dpy->pixelAt(coords)); + dpy->pixelAt(coords) = blend(scaledColor, src, 200); } +} - void fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const { - int length = end.x - start.x; - int direction = 1; - if (length < 0) { - direction = -1; - } - - uint8_t frac = 255 / std::abs(length); - for(int i = 0; i < std::abs(length); i++) { - auto coords = PhysicalCoordinates((start.x + (i * direction)), 0); - - const uint8_t localScale = inoise8(i * 80, m_pos * 3); - const uint8_t dimPosition = lerp8by8(50, 190, scale8(sin8((frac * i) / 2), localScale)); - const uint8_t withBurst = ease8InOutCubic(lerp16by16(dimPosition, 255, m_burst)); - auto scaledColor = CHSV(baseColor.hue, lerp8by8(100, 255, localScale), withBurst); - - CRGB src(dpy->pixelAt(coords)); - dpy->pixelAt(coords) = blend(scaledColor, src, 200); - } - } - - uint16_t m_pos; - uint16_t m_burst; -}; +STATIC_ALLOC(DrainAnimation); +STATIC_TASK(DrainAnimation); diff --git a/src/animations/Drain.h b/src/animations/Drain.h index 5140704..7313b2f 100644 --- a/src/animations/Drain.h +++ b/src/animations/Drain.h @@ -1,63 +1,16 @@ +#pragma once +#include +#include + class DrainAnimation: public Figment { public: - - DrainAnimation(Task::State initialState) : Figment("Drain", initialState) {} - - void loop() override { - EVERY_N_MILLISECONDS(8) { - m_pos++; - m_fillColor.update(); - } - EVERY_N_MILLISECONDS(50) { - if (random(255) >= 10) { - m_burst -= m_burst / 10; - } - } - } - - void handleEvent(const InputEvent& event) override { - if (event.intent == InputEvent::SetColor) { - m_fillColor = event.asRGB(); - } else if (event.intent == InputEvent::Acceleration) { - m_pos += log10(event.asInt()); - uint16_t burstInc = event.asInt() / 6; - m_burst = (m_burst > 0xFFFF - burstInc) ? 0xFFFF : m_burst + burstInc; - } - } - + DrainAnimation(); + void loop() override; + void handleEvent(const InputEvent& event) override; + void render(Display* dpy) const override; + private: AnimatedRGB m_fillColor; - - void render(Display* dpy) const override { - dpy->clear(); - Surface leftPanel{dpy, {0, 0}, {128, 0}}; - Surface rightPanel{dpy, {128, 0}, {255, 0}}; - fillRange(dpy, leftPanel.start, leftPanel.end, rgb2hsv_approximate(m_fillColor)); - fillRange(dpy, rightPanel.end, rightPanel.start, rgb2hsv_approximate(m_fillColor)); - } - - void fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const { - int length = end.x - start.x; - int direction = 1; - if (length < 0) { - direction = -1; - } - - uint8_t frac = 255 / std::abs(length); - for(int i = 0; i < std::abs(length); i++) { - auto coords = PhysicalCoordinates((start.x + (i * direction)), 0); - - const uint8_t localScale = inoise8(i * 80, m_pos * 3); - const uint8_t dimPosition = lerp8by8(50, 190, scale8(sin8((frac * i) / 2), localScale)); - const uint8_t withBurst = ease8InOutCubic(lerp16by16(dimPosition, 255, m_burst)); - auto scaledColor = CHSV(baseColor.hue, lerp8by8(100, 255, localScale), withBurst); - - CRGB src(dpy->pixelAt(coords)); - dpy->pixelAt(coords) = blend(scaledColor, src, 200); - } - } - + void fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const; uint16_t m_pos; uint16_t m_burst; }; - - diff --git a/src/animations/Flashlight.cpp b/src/animations/Flashlight.cpp index cc9426c..c7d458a 100644 --- a/src/animations/Flashlight.cpp +++ b/src/animations/Flashlight.cpp @@ -1,47 +1,33 @@ -#pragma once +#include "Flashlight.h" +#include "../Static.h" -#include -#include "../sprites/Blob.h" - -class Flashlight: public Figment { -public: - Flashlight(Task::State initialState) : Figment("Flashlight", initialState) { - m_blobs.forEach([](Blob &blob) { - blob.setHue(random(255)); - blob.setSaturation(10); - blob.setPos(random(255)); - }); - } - - void handleEvent(const InputEvent& evt) override { - if (evt.intent == InputEvent::Acceleration) { - if (evt.asInt() > 10) { - m_blobs.forEach([](Blob& blob) {blob.update();}); - } +Flashlight::Flashlight() : Figment("Flashlight", Task::Stopped) { + m_blobs.forEach([](Blob &blob) { + blob.setHue(random(255)); + blob.setSaturation(10); + blob.setPos(random(255)); + if (random(255) >= 128) { + blob.setVelocity(-1); } + }); +} - /*if (evt.intent() == InputEvent::UserInput) { - if (evt.asInt() == 1) { - m_blobs.forEach([](Blob& blob) {blob.setPos(random(255));}); - } - if (evt.asInt() == 2) { - m_blobs.forEach([](Blob& chime) {blob.setPos(0);}); - } - }*/ - } - - void loop() override { - m_blobs.update(); - } - - void render(Display* dpy) const override { - m_blobs.render(dpy); - for(int i = 0; i < dpy->pixelCount();i++) { - dpy->pixelAt(i) |= 100; +void Flashlight::handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::Acceleration) { + if (evt.asInt() > 10) { + m_blobs.forEach([](Blob& blob) {blob.update();}); } - } + } +} -private: - static constexpr int blobCount = 30; - SpriteList m_blobs; -}; +void Flashlight::loop() { + m_blobs.update(); +} + +void Flashlight::render(Display* dpy) const { + m_blobs.render(dpy); + Surface(dpy, {0, 0}, {255, 0}) |= 100; +} + +STATIC_ALLOC(Flashlight); +STATIC_TASK(Flashlight); diff --git a/src/animations/Flashlight.h b/src/animations/Flashlight.h new file mode 100644 index 0000000..3536708 --- /dev/null +++ b/src/animations/Flashlight.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include "../sprites/Blob.h" + +class Flashlight: public Figment { +public: + Flashlight(); + void handleEvent(const InputEvent& evt) override; + void loop() override; + void render(Display* dpy) const override; + +private: + static constexpr int blobCount = 30; + SpriteList m_blobs; +}; diff --git a/src/animations/Power.cpp b/src/animations/Power.cpp index e0e09ad..e69de29 100644 --- a/src/animations/Power.cpp +++ b/src/animations/Power.cpp @@ -1,42 +0,0 @@ -#include - -template -class Power: public Figment { -public: - Power() : Figment("Power") {} - - void handleEvent(const InputEvent& evt) override { - switch (evt.intent) { - case InputEvent::PowerToggle: - m_powerState = m_powerState.value() <= 128 ? 255 : 0; - //Log.info("POWER TOGGLE %d", m_powerState.value()); - break; - case InputEvent::SetPower: - m_powerState = evt.asInt() == 0 ? 0 : 255; - break; - case InputEvent::SetBrightness: - m_brightness = evt.asInt(); - default: - return; - } - } - - void loop() override { - m_powerState.update(); - m_brightness.update(); - } - - void render(Display* dpy) const override { - const uint8_t clippedBrightness = min((uint8_t)m_brightness, MaxBrightness); - const uint8_t scaledBrightness = scale8(m_powerState, clippedBrightness); - const uint8_t videoBrightness = brighten8_video(scaledBrightness); - const uint8_t powerBrightness = calculate_max_brightness_for_power_mW(videoBrightness, Watts); - FastLED.setBrightness(powerBrightness); - } - - static constexpr uint32_t Watts = Voltage * MaxMilliAmps; - -private: - AnimatedNumber m_powerState = 255; - AnimatedNumber m_brightness = MaxBrightness; -}; diff --git a/src/animations/Power.h b/src/animations/Power.h new file mode 100644 index 0000000..5422ba9 --- /dev/null +++ b/src/animations/Power.h @@ -0,0 +1,54 @@ +#pragma once +#include + +template +class Power: public Figment { +public: + Power() : Figment("Power") {} + + void handleEvent(const InputEvent& evt) override { + switch (evt.intent) { + case InputEvent::PowerToggle: + m_powerState = m_powerState.value() <= 128 ? 255 : 0; + //Log.info("POWER TOGGLE %d", m_powerState.value()); + break; + case InputEvent::SetPower: + m_powerState = evt.asInt() == 0 ? 0 : 255; + Log.notice("Power is now %d", m_powerState); + break; + case InputEvent::SetBrightness: + m_brightness = evt.asInt(); + m_brightness = 255; + break; + case InputEvent::Beat: + m_beatDecay.set(0, 255); + break; + default: + return; + } + } + + void loop() override { + m_powerState.update(); + m_brightness.update(); + EVERY_N_MILLISECONDS(20) { + m_beatDecay.update(13); + } + } + + void render(Display* dpy) const override { + const uint8_t decayedBrightness = scale8((uint8_t)m_brightness, ease8InOutCubic((uint8_t)m_beatDecay)); + const uint8_t clippedBrightness = std::min(decayedBrightness, MaxBrightness); + const uint8_t scaledBrightness = scale8(m_powerState, clippedBrightness); + const uint8_t videoBrightness = brighten8_video(scaledBrightness); + const uint8_t powerBrightness = calculate_max_brightness_for_power_mW(videoBrightness, Watts); + FastLED.setBrightness(powerBrightness); + } + + static constexpr uint32_t Watts = Voltage * MaxMilliAmps; + +private: + AnimatedNumber m_powerState = 255; + AnimatedNumber m_brightness = MaxBrightness; + AnimatedNumber m_beatDecay = 255; +}; diff --git a/src/animations/SolidAnimation.cpp b/src/animations/SolidAnimation.cpp index 2eb32d2..7964053 100644 --- a/src/animations/SolidAnimation.cpp +++ b/src/animations/SolidAnimation.cpp @@ -1,50 +1,72 @@ -#include -#include "../sprites/Blob.h" +#include "SolidAnimation.h" +#include "../Static.h" -class SolidAnimation: public Figment { -private: - AnimatedNumber m_red, m_green, m_blue; - static constexpr int blobCount = 20; - SpriteList m_blobs; +SolidAnimation::SolidAnimation() : Figment("Solid", Task::Stopped) { +} -public: - SolidAnimation(Task::State initialState) : Figment("Solid", initialState) { - m_blobs.forEach([](Blob& blob) { - blob.setPos(random(140)); - blob.setBrightness(random(255)); - if (random(255) % 2) { - blob.setVelocity(-1); - } +void SolidAnimation::randomize() { + m_isRandom = true; + m_blobs.forEach([](Blob& blob) { + blob.setPos(random(140)); + blob.setBrightness(random(255)); + if (random(255) % 2) { + blob.setVelocity(-1); + } + }); +} + +void SolidAnimation::handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::SetColor) { + CRGB nextColor = evt.asRGB(); + m_red.set(nextColor.red); + m_green.set(nextColor.green); + m_blue.set(nextColor.blue); + m_changePct.set(0, 255); + m_prevColor = m_curColor; + m_curColor = nextColor; + m_horizontal = !m_horizontal; + } else if (evt.intent == InputEvent::Beat) { + m_isRandom = false; + } +} + +void SolidAnimation::loop() { + if (!m_isRandom) { + randomize(); + } + m_red.update(15); + m_green.update(15); + m_blue.update(15); + EVERY_N_MILLIS(16) { + m_changePct.update(12); + } + EVERY_N_MILLIS(6) { + CRGB rgb{m_red, m_green, m_blue}; + CHSV hsv = rgb2hsv_approximate(rgb); + m_blobs.forEach([=](Blob& blob) { + blob.setHue(hsv.hue); + blob.setSaturation(hsv.saturation); }); + m_blobs.update(); } +} - void handleEvent(const InputEvent& evt) override { - if (evt.intent == InputEvent::SetColor) { - CRGB nextColor = evt.asRGB(); - m_red.set(nextColor.red); - m_green.set(nextColor.green); - m_blue.set(nextColor.blue); - } - } +#include - void loop() override { - m_red.update(); - m_green.update(); - m_blue.update(); - EVERY_N_MILLIS(6) { - CRGB rgb{m_red, m_green, m_blue}; - CHSV hsv = rgb2hsv_approximate(rgb); - m_blobs.forEach([=](Blob& blob) { - blob.setHue(hsv.hue); - blob.setSaturation(hsv.saturation); - }); - m_blobs.update(); - } +void SolidAnimation::render(Display* dpy) const { + PerfCounter _("solidRender"); + CRGB color(m_red.value(), m_green.value(), m_blue.value()); + uint8_t frame = ease8InOutApprox(m_changePct); + if (frame == 255) { + Surface(dpy, {0, 0}, {255, 255}) = color; + } else { + uint8_t cutoff = (frame / 2); + uint8_t rotation = m_horizontal ? 0 : 128; + Surface(dpy, {0, 0}, {128 - cutoff, 255}, rotation) = m_prevColor; + Surface(dpy, {128 - cutoff, 0}, {128 + cutoff, 255}, rotation) = color; + Surface(dpy, {128 + cutoff, 0}, {255, 255}, rotation) = m_prevColor; } - - void render(Display* dpy) const override { - CRGB color(m_red.value(), m_green.value(), m_blue.value()); - Surface(dpy, {0, 0}, {255, 0}) = color; - m_blobs.render(dpy); - } -}; + m_blobs.render(dpy); +} +STATIC_ALLOC(SolidAnimation); +STATIC_TASK(SolidAnimation); diff --git a/src/animations/SolidAnimation.h b/src/animations/SolidAnimation.h new file mode 100644 index 0000000..24a9c2f --- /dev/null +++ b/src/animations/SolidAnimation.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include "../sprites/Blob.h" + +class SolidAnimation: public Figment { +private: + AnimatedNumber m_red, m_green, m_blue, m_changePct; + CRGB m_curColor; + CRGB m_prevColor; + static constexpr int blobCount = 20; + SpriteList m_blobs; + void randomize(); + bool m_isRandom = false; + bool m_horizontal = false; + +public: + SolidAnimation(); + void handleEvent(const InputEvent& evt) override; + void loop() override; + void render(Display* dpy) const override; +}; diff --git a/src/animations/UpdateStatus.cpp b/src/animations/UpdateStatus.cpp index 72c7981..389b777 100644 --- a/src/animations/UpdateStatus.cpp +++ b/src/animations/UpdateStatus.cpp @@ -32,3 +32,4 @@ UpdateStatus::render(Display* dpy) const } STATIC_ALLOC(UpdateStatus); +STATIC_TASK(UpdateStatus); diff --git a/src/animations/UpdateStatus.h b/src/animations/UpdateStatus.h index 5b8eaca..d52e0e2 100644 --- a/src/animations/UpdateStatus.h +++ b/src/animations/UpdateStatus.h @@ -1,3 +1,4 @@ +#pragma once #include class UpdateStatus: public Figment { diff --git a/src/inputs/Buttons.cpp b/src/inputs/Buttons.cpp index 5692f71..6cc9fa8 100644 --- a/src/inputs/Buttons.cpp +++ b/src/inputs/Buttons.cpp @@ -39,3 +39,4 @@ Buttons::read() } STATIC_ALLOC(Buttons); +STATIC_TASK(Buttons); diff --git a/src/inputs/ColorCycle.h b/src/inputs/ColorCycle.h index 6117f1a..c220e8b 100644 --- a/src/inputs/ColorCycle.h +++ b/src/inputs/ColorCycle.h @@ -21,6 +21,14 @@ public: return InputEvent{}; } + void handleEvent(const InputEvent& evt) override { + if (evt.intent == InputEvent::Beat) { + m_idx %= m_colors.size(); + m_reset = true; + m_idx++; + } + } + void onStart() override { m_reset = true; } diff --git a/src/inputs/MPU6050.cpp b/src/inputs/MPU6050.cpp index 488dd2c..536635b 100644 --- a/src/inputs/MPU6050.cpp +++ b/src/inputs/MPU6050.cpp @@ -63,3 +63,4 @@ MPU5060::read() } STATIC_ALLOC(MPU5060); +STATIC_TASK(MPU5060); diff --git a/src/main.cpp b/src/main.cpp index b4844fa..17cabd1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,8 +5,7 @@ #ifndef PLATFORM_PHOTON #include -#include -#endif +#endif // !PLATFORM_PHOTON #include "Platform.h" @@ -16,29 +15,22 @@ #include "Sequencer.h" #include "LogService.h" -#include "animations/Power.cpp" -#include "animations/SolidAnimation.cpp" -#include "animations/Chimes.cpp" -#include "animations/Flashlight.cpp" -#include "animations/Drain.cpp" +#include + +#include "animations/Power.h" +#include "animations/SolidAnimation.h" +#include "animations/Chimes.h" +#include "animations/Flashlight.h" +#include "animations/Drain.h" #include "animations/UpdateStatus.h" #include "inputs/ColorCycle.h" #include "inputs/Buttons.h" -#include "inputs/MPU6050.h" #ifdef PLATFORM_PHOTON #include "platform/particle/inputs/Photon.h" #include "platform/particle/inputs/CloudStatus.h" -#include "platform/particle/PhotonTelemetry.h" -#include "platform/particle/WebTelemetry.cpp" -#include "platform/particle/MDNSService.cpp" -#else -#include "WiFiTask.h" -#include "platform/arduino/BluetoothSerialTelemetry.h" -#include "platform/arduino/MQTTTelemetry.h" -#include -#endif +#endif // PLATFORM_PHOTON //SerialLogHandler logHandler; @@ -51,6 +43,7 @@ // Enable system thread, so rendering happens while booting //SYSTEM_THREAD(ENABLED); + // Setup FastLED and the display CRGB leds[HardwareConfig::MAX_LED_NUM]; Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static::instance()->coordMap()); @@ -58,7 +51,9 @@ Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static::instance() // Setup power management Power power; -FigmentFunc configDisplay([](Display* dpy) { +REGISTER_TASK(power); + +/*FigmentFunc configDisplay([](Display* dpy) { uint8_t brightness = brighten8_video(beatsin8(60)); auto coords = Static::instance()->coordMap(); for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) { @@ -68,7 +63,7 @@ FigmentFunc configDisplay([](Display* dpy) { dpy->pixelAt(i) += CRGB(255 - brightness, 255 - brightness, 255 - brightness); } } -}); +});*/ class InputBlip: public Figment { public: @@ -94,42 +89,7 @@ private: }; InputBlip inputBlip; - -class ArduinoOTAUpdater : public BufferedInputSource { - public: - ArduinoOTAUpdater() : BufferedInputSource("ArduinoOTA") { - ArduinoOTA.onStart(&ArduinoOTAUpdater::s_onStart); - ArduinoOTA.onProgress(&ArduinoOTAUpdater::s_onProgress); - } - - void loop() override { - if (m_online) { - ArduinoOTA.handle(); - } - BufferedInputSource::loop(); - } - - void handleEvent(const InputEvent& evt) { - if (evt.intent == InputEvent::NetworkStatus && evt.asInt()) { - Log.notice("Booting OTA"); - m_online = true; - ArduinoOTA.begin(); - } - } - private: - bool m_online = false; - static void s_onStart() { - Log.notice("OTA Start!"); - Static::instance()->setEvent(InputEvent::FirmwareUpdate); - } - - static void s_onProgress(unsigned int progress, unsigned int total) { - Log.notice("OTA Progress! %d / %d", progress, total); - Static::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress}); - } -}; - -STATIC_ALLOC(ArduinoOTAUpdater); +REGISTER_TASK(inputBlip); InputFunc randomPulse([]() { static unsigned int pulse = 0; @@ -145,7 +105,9 @@ InputFunc randomPulse([]() { } } return InputEvent{}; -}, "Pulse", Task::Running); +}, "Pulse", Task::Stopped); + +REGISTER_TASK(randomPulse); InputMapper keyMap([](const InputEvent& evt) { if (evt.intent == InputEvent::UserInput) { @@ -167,38 +129,96 @@ InputMapper keyMap([](const InputEvent& evt) { return InputEvent::None; }, "Keymap"); -ChimesAnimation chimes{Task::Stopped}; -SolidAnimation solid{Task::Running}; -DrainAnimation drain{Task::Stopped}; -Flashlight flashlight{Task::Stopped}; +REGISTER_TASK(keyMap); + + +#ifndef DEFAULT_PATTERN_INDEX +#define DEFAULT_PATTERN_INDEX 0 +#endif Sequencer sequencer{{ {"Idle", {"Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"}}, + {"Acid", {"Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"}}, {"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}}, {"Interactive", {"Drain", "MPU5060", "CircadianRhythm"}}, {"Flashlight", {"Flashlight"}}, - {"Gay", {"Solid", "Pulse", "Rainbow", "Rainbow"}}, - {"Acid", {"Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"}}, -}}; + {"Gay", {"Solid", "Pulse", "Rainbow"}}, +}, DEFAULT_PATTERN_INDEX}; +REGISTER_TASK(sequencer); + +class BPM : public InputSource { +public: + BPM() : InputSource("BPM") {} + void handleEvent(const InputEvent& evt) override { + if (evt.intent == InputEvent::BeatDetect) { + m_nextBpm = millis(); + m_timings.insert(millis()); + Log.notice("%d timings", m_timings.size()); + if (m_timings.size() >= 5) { + updateBPM(); + } + } + } + + InputEvent read() override { + if (m_bpm > 0) { + uint16_t now = millis(); + if (now >= m_nextBpm) { + m_nextBpm += m_bpm; + return InputEvent{InputEvent::Beat, m_bpm}; + } + if (now >= m_nextLearn && m_nextLearn != 0) { + m_timings.clear(); + m_nextLearn = 0; + } + } + return InputEvent{}; + } + +private: + uint16_t m_bpm = 0; + uint16_t m_nextBpm = 0; + uint16_t m_nextLearn = 0; + Ringbuf m_timings; + + void updateBPM() { + uint16_t avgDelta = 0; + for(uint8_t i = 0; i < m_timings.size() - 1; i++) { + uint16_t delta = m_timings.peek(i+1) - m_timings.peek(i); + Log.notice("Timing %d Delta %d", m_timings.peek(i), delta); + avgDelta += delta; + } + m_bpm = avgDelta / 4; + m_nextLearn = m_bpm * 5 + millis(); + Log.notice("BPM is now %d", m_bpm); + uint16_t trash; + m_timings.take(trash); + } +}; + +STATIC_ALLOC(BPM); +STATIC_TASK(BPM); // Render all layers to the displays Renderer renderer{ {&dpy}, { - &chimes, - &drain, - &solid, - &flashlight, + Static::instance(), + Static::instance(), + Static::instance(), + Static::instance(), Static::instance(), &inputBlip, &power, } }; +REGISTER_TASK(renderer); + Renderer configRenderer{ {&dpy}, - {&drain, &configDisplay, &inputBlip, &power} + {Static::instance(), /*&configDisplay,*/ &inputBlip, &power} }; // Cycle some random colors @@ -209,7 +229,9 @@ ColorSequenceInput<9> idleCycle{{ CRGB(128, 0, 128), // Purple CRGB(255, 255, 255), // White CRGB(0, 255, 255), // Cyan -}, "IdleColors", Task::Running}; +}, "IdleColors", Task::Stopped}; + +REGISTER_TASK(idleCycle); ColorSequenceInput<7> rainbowCycle{{ CRGB(255, 0, 0), // Red @@ -219,7 +241,9 @@ ColorSequenceInput<7> rainbowCycle{{ CRGB(128, 0, 128), // Purple }, "Rainbow", Task::Stopped}; -struct ConfigInputTask: public BufferedInputSource { +REGISTER_TASK(rainbowCycle); + +/*struct ConfigInputTask: public BufferedInputSource { public: ConfigInputTask() : BufferedInputSource("ConfigInput") {} @@ -292,7 +316,7 @@ private: return InputEvent::None; } } -}; +};*/ struct ScheduleEntry { uint8_t hour; @@ -368,9 +392,13 @@ class CircadianRhythm : public InputSource { uint8_t minute = 0; needsUpdate = false; struct tm timeinfo; - Platform::getLocalTime(&timeinfo); - hour = timeinfo.tm_hour; - minute = timeinfo.tm_min; + if (Platform::getLocalTime(&timeinfo)) { + hour = timeinfo.tm_hour; + minute = timeinfo.tm_min; + } else { + hour = 0; + minute = 0; + } Log.notice("Current time: %d:%d", hour, minute); return InputEvent{InputEvent::SetBrightness, brightnessForTime(hour, minute)}; } @@ -379,20 +407,16 @@ class CircadianRhythm : public InputSource { }; STATIC_ALLOC(CircadianRhythm); +STATIC_TASK(CircadianRhythm); // A special mainloop app for configuring hardware settings that reboots the // device when the user is finished. -MainLoop configApp{{ +/*MainLoop configApp{{ Static::instance(), // Manage read/write of configuration data Static::instance(), -#ifdef PLATFORM_PHOTON - // Update photon telemetry - Static::instance(), -#endif - // Read hardware inputs Static::instance(), @@ -408,12 +432,14 @@ MainLoop configApp{{ &inputBlip, // Render it all &configRenderer, -}}; +}};*/ + +MainLoop configApp{std::vector()}; TaskFunc safeModeNag([]{ static uint8_t frame = 0; EVERY_N_SECONDS(30) { - Log.notice("I am running in safe mode!"); + Log.fatal("I am running in safe mode!"); } EVERY_N_MILLISECONDS(16) { frame++; @@ -430,149 +456,72 @@ TaskFunc safeModeNag([]{ } }); -MainLoop safeModeApp({ +#ifdef CONFIG_WIFI +#include "platform/arduino/WiFiTask.h" +#endif // CONFIG_WIFI +#ifdef CONFIG_OTA +#include "platform/arduino/OTA.h" +#endif // CONFIG_OTA +#ifdef CONFIG_MQTT +#include "platform/arduino/MQTTTelemetry.h" +#endif // CONFIG_MQTT + +MainLoop safeModeApp{{ Static::instance(), + // System logging + Static::instance(), + &safeModeNag, +#ifdef CONFIG_WIFI // ESP Wifi Static::instance(), - // System logging - Static::instance(), +#endif // CONFIG_WIFI +#ifdef CONFIG_MQTT // MQTT Static::instance(), +#endif // CONFIG_MQTT +#ifdef CONFIG_OTA // OTA Updates Static::instance(), - - &safeModeNag, -}); - -// Turn on, -MainLoop renderbugApp{{ - - Static::instance(), - - // Load/update graphics configuration from EEPROM - Static::instance(), - - // Platform inputs - // TODO: Merge cloud and esp wifi tasks into a common networking base -#ifdef PLATFORM_PHOTON - // Particle cloud status - Static::instance(), - - // Monitor network state and provide particle API events - Static::instance(), -#else - // ESP Wifi - //Static::instance(), -#endif - -#ifdef BOARD_ESP32 - // ESP32 Bluetooth - Static::instance(), -#endif - - // System logging - Static::instance(), - -#ifdef CONFIG_MPU5060 - // Hardware drivers - Static::instance(), -#endif - -#ifdef CONFIG_BUTTONS - Static::instance(), - - // Map buttons to events - &keyMap, -#endif - - // Pattern sequencer - &sequencer, - - // Daily rhythm activities - Static::instance(), - - // Periodic motion input - //&randomPulse, - - // Periodic color inputs - &idleCycle, - &rainbowCycle, - - // Animations - &chimes, - &drain, - &solid, - &flashlight, - - // Update UI layer - &power, - Static::instance(), - &inputBlip, - - // Render everything - &renderer, - - // Platform telemetry - // TODO: Combine some of these services into a unified telemetry API with - // platform-specific backends? - // Or at least, just the MQTT and watchdog ones. -#ifdef PLATFORM_PHOTON - // Update photon telemetry - Static::instance(), - - // Web telemetry UI - Static::instance(), - - // MQTT telemetry - Static::instance(), - - // Network discovery - Static::instance(), - - //Watchdog - Static::instance(), -#else - // MQTT - Static::instance(), - - // OTA Updates - Static::instance(), -#endif +#endif // CONFIG_OTA }}; -MainLoop &runner = renderbugApp; +MainLoop* runner = &safeModeApp; -// Tune in, void setup() { + // Turn on, Platform::preSetup(); +#ifdef CONFIG_MQTT Static::instance()->setSequencer(&sequencer); +#endif // CONFIG_MQTT Log.notice(u8"🐛 Booting Renderbug!"); Log.notice(u8"🐞 I am built for %d LEDs running on %dmA", HardwareConfig::MAX_LED_NUM, PSU_MILLIAMPS); Log.notice(u8"📡 Platform %s version %s", Platform::name(), Platform::version()); - Platform::bootSplash(); Log.notice(u8"Setting timezone to -7 (PST)"); Platform::setTimezone(-7); Log.notice(u8" Setting up platform..."); Platform::setup(); + Platform::bootSplash(); Log.notice(u8"💡 Starting FastLED..."); Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM); + runner = new MainLoop{std::vector{Platform::beginTasks(), Platform::endTasks()}}; + // Tune in, if (Platform::bootopts.isSafeMode) { Log.notice(u8"⚠️ Starting Figment in safe mode!!!"); - runner = safeModeApp; + runner = &safeModeApp; FastLED.showColor(CRGB(5, 0, 0)); FastLED.show(); } else if (Platform::bootopts.isSetup) { Log.notice(u8"🔧 Starting Figment in configuration mode..."); - runner = configApp; + //runner = &configApp; } else { Log.notice(u8"🌌 Starting Figment..."); } Serial.flush(); - runner.start(); + runner->start(); //Log.info(u8"💽 %lu bytes of free RAM", System.freeMemory()); Log.notice(u8"🚀 Setup complete! Ready to rock and roll."); @@ -581,6 +530,8 @@ void setup() { // Drop out. void loop() { - //Platform::loop(); - runner.loop(); + EVERY_N_SECONDS(5) { + Log.notice("FPS: %d", FastLED.getFPS()); + } + runner->loop(); } diff --git a/src/platform/arduino/BluetoothSerialTelemetry.cpp b/src/platform/arduino/BluetoothSerialTelemetry.cpp index 0aadb59..b92a16b 100644 --- a/src/platform/arduino/BluetoothSerialTelemetry.cpp +++ b/src/platform/arduino/BluetoothSerialTelemetry.cpp @@ -26,7 +26,7 @@ BluetoothSerialTelemetry::read() // Overwrite the '*' character, to leave us with a complete command commandBuf[cmdSize-1] = 0; - //Log.notice("Bluetooth read %s", commandBuf); + Log.verbose("Bluetooth read %s", commandBuf); if (commandBuf[0] == 'R') { m_color = CRGB(std::atoi(&commandBuf[1]), m_color.g, m_color.b); @@ -49,6 +49,12 @@ BluetoothSerialTelemetry::read() return InputEvent{InputEvent::SetPower, 0}; } else if (commandBuf[0] == 'p') { return InputEvent{InputEvent::SetPattern, &commandBuf[1]}; + } else if (commandBuf[0] == 'b') { + return InputEvent::BeatDetect; + } else if (commandBuf[0] == 'c') { + return InputEvent{InputEvent::SetDisplayLength, std::atoi(&commandBuf[1])}; + } else if (commandBuf[0] == 'Y') { + return InputEvent::SaveConfigurationRequest; } else if (commandBuf[0] == 'A') { char* axisVal = strtok(&commandBuf[1], ","); const uint8_t accelX = std::atof(axisVal) * 10; @@ -75,12 +81,13 @@ BluetoothSerialTelemetry::read() void BluetoothSerialTelemetry::onStart() { - Log.notice("Starting up Bluetooth..."); + Log.trace("Starting up Bluetooth..."); if (m_serial.begin(Platform::deviceName())) { - Log.notice("Bluetooth started!"); + Log.notice("Bluetooth started! Device name is %s", Platform::deviceName()); } else { Log.warning("Bluetooth could not be started!"); } } STATIC_ALLOC(BluetoothSerialTelemetry); +STATIC_TASK(BluetoothSerialTelemetry); diff --git a/src/platform/arduino/MQTTTelemetry.cpp b/src/platform/arduino/MQTTTelemetry.cpp index ca27810..f0d3098 100644 --- a/src/platform/arduino/MQTTTelemetry.cpp +++ b/src/platform/arduino/MQTTTelemetry.cpp @@ -117,39 +117,37 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt) Log.notice("Connected to MQTT"); m_needHeartbeat = true; - StaticJsonDocument<1024> configJson; - - Lightswitch.toJson(configJson); + m_json.clear(); + Lightswitch.toJson(m_json); int i = 0; for(const Sequencer::Scene& scene : m_sequencer->scenes()) { - configJson["fx_list"][i++] = scene.name; + m_json["fx_list"][i++] = scene.name; } - configJson["brightness"] = true; - configJson["rgb"] = true; + m_json["brightness"] = true; + m_json["rgb"] = true; - char buf[1024]; - serializeJson(configJson, buf, sizeof(buf)); + publishDoc(Lightswitch.configTopic().c_str(), true); - Log.verbose("Publish %s %s", Lightswitch.configTopic().c_str(), buf); - m_mqtt.publish(Lightswitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); + //Log.verbose("Publish %s %s", Lightswitch.configTopic().c_str(), buf); + //m_mqtt.publish(Lightswitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); m_mqtt.subscribe(Lightswitch.commandTopic().c_str()); - configJson.clear(); - flashlightSwitch.toJson(configJson, false); - configJson["cmd_t"] = "~/set"; - configJson["ret"] = true; - serializeJson(configJson, buf, sizeof(buf)); - m_mqtt.publish(flashlightSwitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); + m_json.clear(); + flashlightSwitch.toJson(m_json, false); + m_json["cmd_t"] = "~/set"; + m_json["ret"] = true; + publishDoc(flashlightSwitch.configTopic().c_str(), true); + //m_mqtt.publish(flashlightSwitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); m_mqtt.subscribe(flashlightSwitch.commandTopic().c_str()); - configJson.clear(); - FPSSensor.toJson(configJson, false); - configJson["unit_of_meas"] = "Frames/s"; - serializeJson(configJson, buf, sizeof(buf)); + m_json.clear(); + FPSSensor.toJson(m_json, false); + m_json["unit_of_meas"] = "Frames/s"; + publishDoc(FPSSensor.configTopic().c_str(), true); - Log.verbose("Publish %s %s", FPSSensor.configTopic().c_str(), buf); - m_mqtt.publish(FPSSensor.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); + //Log.verbose("Publish %s %s", FPSSensor.configTopic().c_str(), buf); + //m_mqtt.publish(FPSSensor.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); m_mqtt.subscribe(FPSSensor.commandTopic().c_str()); #ifdef BOARD_ESP8266 @@ -172,39 +170,28 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt) String flashlightStatTopic = flashlightSwitch.statTopic(); m_mqtt.publish(flashlightStatTopic.c_str(), "ON"); } else if (evt.intent == InputEvent::SetPower) { - StaticJsonDocument<256> doc; - char buf[256]; + m_json.clear(); m_isOn = evt.asInt() ? true : false; - doc["state"] = m_isOn ? "ON" : "OFF"; - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(statTopic.c_str(), buf); + m_json["state"] = m_isOn ? "ON" : "OFF"; + publishDoc(statTopic.c_str()); } else if (evt.intent == InputEvent::SetBrightness) { - StaticJsonDocument<256> doc; - char buf[256]; - doc["brightness"] = evt.asInt(); - doc["state"] = m_isOn ? "ON" : "OFF"; - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(statTopic.c_str(), buf); + m_json.clear(); + m_json["brightness"] = evt.asInt(); + m_json["state"] = m_isOn ? "ON" : "OFF"; + publishDoc(statTopic.c_str()); } else if (evt.intent == InputEvent::SetColor) { - StaticJsonDocument<256> doc; - char buf[256]; CRGB color = evt.asRGB(); - doc["color"]["r"] = color.r; - doc["color"]["g"] = color.g; - doc["color"]["b"] = color.b; - doc["state"] = m_isOn ? "ON" : "OFF"; - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(statTopic.c_str(), buf); + m_json.clear(); + m_json["color"]["r"] = color.r; + m_json["color"]["g"] = color.g; + m_json["color"]["b"] = color.b; + m_json["state"] = m_isOn ? "ON" : "OFF"; + publishDoc(statTopic.c_str()); } else if (evt.intent == InputEvent::SetPattern) { - StaticJsonDocument<256> doc; - char buf[256]; - doc["effect"] = evt.asString(); - doc["state"] = m_isOn ? "ON" : "OFF"; - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(statTopic.c_str(), buf); - } else if (evt.intent == InputEvent::FirmwareUpdate) { - String updateTopic = m_debugTopic + "/firmware"; - m_mqtt.publish(updateTopic.c_str(), "firmware update!"); + m_json.clear(); + m_json["effect"] = evt.asString(); + m_json["state"] = m_isOn ? "ON" : "OFF"; + publishDoc(statTopic.c_str()); } } } @@ -241,22 +228,20 @@ MQTTTelemetry::loopOnline() m_needHeartbeat = true; } if (m_needHeartbeat) { - char buf[512]; - StaticJsonDocument<512> response; - response["device_id"] = Platform::deviceID(); - response["sketch_version"] = ESP.getSketchMD5(); - response["os_version"] = ESP.getSdkVersion(); - response["localip"] = WiFi.localIP().toString(); - response["pixelCount"] = Static::instance()->coordMap()->pixelCount; - response["startPixel"] = Static::instance()->coordMap()->startPixel; - response["RSSI"] = WiFi.RSSI(); - response["free_ram"] = ESP.getFreeHeap(); - response["fps"] = FastLED.getFPS(); - serializeJson(response, buf, sizeof(buf)); + m_json.clear(); + m_json["device_id"] = Platform::deviceID(); + m_json["sketch_version"] = ESP.getSketchMD5(); + m_json["os_version"] = ESP.getSdkVersion(); + m_json["localip"] = WiFi.localIP().toString(); + m_json["pixelCount"] = Static::instance()->coordMap()->physicalPixelCount(); + //m_json["startPixel"] = Static::instance()->coordMap()->startPixel; + m_json["RSSI"] = WiFi.RSSI(); + m_json["free_ram"] = ESP.getFreeHeap(); + m_json["fps"] = FastLED.getFPS(); String availTopic = m_rootTopic + "/available"; - m_mqtt.publish(Lightswitch.heartbeatTopic().c_str(), buf); + publishDoc(Lightswitch.heartbeatTopic().c_str()); m_mqtt.publish(Device.availabilityTopic.c_str(), "online"); - //Log.notice("Heartbeat: %s", buf); + //Log.trace("Heartbeat: %s", buf); String fpsCounter = String(FastLED.getFPS()); m_mqtt.publish(FPSSensor.statTopic().c_str(), fpsCounter.c_str()); @@ -280,89 +265,106 @@ MQTTTelemetry::callback(char* topic, const char* payload) setEvent(InputEvent{InputEvent::SetPattern, "Idle"}); } } else if (Lightswitch.isCommandTopic(topic)) { - StaticJsonDocument<512> doc; - deserializeJson(doc, payload); + deserializeJson(m_json, payload); - if (doc.containsKey("state")) { - if (doc["state"] == "ON") { + if (m_json.containsKey("state")) { + if (m_json["state"] == "ON") { Log.notice("Turning on power"); setEvent(InputEvent{InputEvent::SetPower, true}); - } else if (doc["state"] == "OFF") { + } else if (m_json["state"] == "OFF") { Log.notice("Turning off power"); setEvent(InputEvent{InputEvent::SetPattern, "Idle"}); setEvent(InputEvent{InputEvent::SetPower, false}); } } - if (doc.containsKey("start")) { - strcpy(m_patternBuf, doc["start"].as()); + if (m_json.containsKey("start")) { + strcpy(m_patternBuf, m_json["start"].as()); setEvent(InputEvent{InputEvent::StartThing, m_patternBuf}); } - if (doc.containsKey("stop")) { - if (doc["stop"] == name) { - Log.notice("You can't kill an idea, or stop the MQTT Task via MQTT."); + if (m_json.containsKey("stop")) { + if (m_json["stop"] == name) { + Log.warning("You can't kill an idea, or stop the MQTT Task via MQTT."); } else { - strcpy(m_patternBuf, doc["stop"].as()); + strcpy(m_patternBuf, m_json["stop"].as()); setEvent(InputEvent{InputEvent::StopThing, m_patternBuf}); } } - if (doc.containsKey("pixelCount")) { - setEvent(InputEvent{InputEvent::SetDisplayLength, (int)doc["pixelCount"]}); + if (m_json.containsKey("pixelCount")) { + Log.notice("Pixel count changed"); + setEvent(InputEvent{InputEvent::SetDisplayLength, m_json["pixelCount"].as()}); } - if (doc.containsKey("startPixel")) { - setEvent(InputEvent{InputEvent::SetDisplayOffset, (int)doc["startPixel"]}); + if (m_json.containsKey("startPixel")) { + Log.notice("Start pixel changed"); + setEvent(InputEvent{InputEvent::SetDisplayOffset, m_json["startPixel"].as()}); } - if (doc.containsKey("save")) { + if (m_json.containsKey("save")) { setEvent(InputEvent{InputEvent::SaveConfigurationRequest}); } - if (doc.containsKey("restart")) { -#ifdef BOARD_ESP8266 - ESP.wdtDisable(); - ESP.restart(); -#endif + if (m_json.containsKey("restart")) { + Platform::restart(); } - if (doc.containsKey("reconnect")) { + if (m_json.containsKey("reconnect")) { m_mqtt.disconnect(); } - if (doc.containsKey("ping")) { + if (m_json.containsKey("ping")) { m_needHeartbeat = true; Log.notice("Queuing up heartbeat"); } - if (doc.containsKey("effect")) { - strcpy(m_patternBuf, doc["effect"].as()); + if (m_json.containsKey("effect")) { + strcpy(m_patternBuf, m_json["effect"].as()); setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf}); } - if (doc.containsKey("color")) { - uint8_t r = doc["color"]["r"]; - uint8_t g = doc["color"]["g"]; - uint8_t b = doc["color"]["b"]; + if (m_json.containsKey("color")) { + uint8_t r = m_json["color"]["r"]; + uint8_t g = m_json["color"]["g"]; + uint8_t b = m_json["color"]["b"]; setEvent(InputEvent{InputEvent::SetColor, CRGB(r, g, b)}); } - if (doc.containsKey("brightness")) { - setEvent(InputEvent{InputEvent::SetBrightness, (int)doc["brightness"]}); + if (m_json.containsKey("brightness")) { + setEvent(InputEvent{InputEvent::SetBrightness, m_json["brightness"].as()}); } + + Log.notice("Event done."); } } void MQTTTelemetry::s_callback(char* topic, byte* payload, unsigned int length) { - char topicBuf[128]; - char payloadBuf[512]; - strcpy(topicBuf, topic); - memcpy(payloadBuf, payload, length); - payloadBuf[std::min(sizeof(payloadBuf) - 1, length)] = 0; - Static::instance()->callback(topicBuf, payloadBuf); + strcpy(s_topicBuf, topic); + memcpy(s_payloadBuf, payload, length); + s_payloadBuf[std::min(sizeof(s_payloadBuf) - 1, length)] = 0; + Static::instance()->callback(s_topicBuf, s_payloadBuf); } +char MQTTTelemetry::s_topicBuf[128] = {0}; +char MQTTTelemetry::s_payloadBuf[512] = {0}; + +void +MQTTTelemetry::publishDoc(const char* topic, bool retain) +{ + m_mqtt.beginPublish(topic, measureJson(m_json), retain); + serializeJson(m_json, m_mqtt); + m_mqtt.endPublish(); +} + +void +MQTTTelemetry::publishDoc(const char* topic) +{ + publishDoc(topic, false); +} + + STATIC_ALLOC(MQTTTelemetry); +STATIC_TASK(MQTTTelemetry); diff --git a/src/platform/arduino/MQTTTelemetry.h b/src/platform/arduino/MQTTTelemetry.h index 14fc929..4830dcc 100644 --- a/src/platform/arduino/MQTTTelemetry.h +++ b/src/platform/arduino/MQTTTelemetry.h @@ -12,6 +12,7 @@ #include #endif +#include class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { public: @@ -25,13 +26,16 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { public: LogPrinter(MQTTTelemetry* telemetry) : telemetry(telemetry) {}; size_t write(uint8_t byte) { - char outBuf[512]; if (byte == '\n') { - size_t bufSize = buf.write(outBuf); - outBuf[std::min(sizeof(outBuf), bufSize)] = 0; - Serial.println(outBuf); + char c; String logTopic = telemetry->m_debugTopic + "/log"; - telemetry->m_mqtt.publish(logTopic.c_str(), outBuf); + telemetry->m_mqtt.beginPublish(logTopic.c_str(), buf.size(), false); + while (buf.take(c)) { + Serial.write(c); + telemetry->m_mqtt.write(c); + } + Serial.println(); + telemetry->m_mqtt.endPublish(); } else { buf.insert(byte); } @@ -61,9 +65,15 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { char m_patternBuf[48]; bool m_needHeartbeat = false; bool m_isOn = true; + static char s_topicBuf[128]; + static char s_payloadBuf[512]; + + void publishDoc(const char* topic); + void publishDoc(const char* topic, bool retain); Sequencer *m_sequencer = 0; WiFiClient m_wifi; PubSubClient m_mqtt; LogPrinter m_logPrinter; + StaticJsonDocument<1024> m_json; }; diff --git a/src/platform/arduino/OTA.cpp b/src/platform/arduino/OTA.cpp new file mode 100644 index 0000000..7f205a0 --- /dev/null +++ b/src/platform/arduino/OTA.cpp @@ -0,0 +1,34 @@ +#include "OTA.h" +#include "../../Static.h" + +ArduinoOTAUpdater::ArduinoOTAUpdater() : BufferedInputSource("ArduinoOTA") { + ArduinoOTA.onStart(&ArduinoOTAUpdater::s_onStart); + ArduinoOTA.onProgress(&ArduinoOTAUpdater::s_onProgress); +} + +void ArduinoOTAUpdater::loop() { + if (m_online) { + ArduinoOTA.handle(); + } + BufferedInputSource::loop(); +} + +void ArduinoOTAUpdater::handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::NetworkStatus && evt.asInt()) { + Log.notice("Booting OTA"); + m_online = true; + ArduinoOTA.begin(); + } +} +void ArduinoOTAUpdater::s_onStart() { + Log.notice("OTA Start!"); + Static::instance()->setEvent(InputEvent::FirmwareUpdate); +} + +void ArduinoOTAUpdater::s_onProgress(unsigned int progress, unsigned int total) { + Log.notice("OTA Progress! %d / %d", progress, total); + Static::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress}); +} + +STATIC_ALLOC(ArduinoOTAUpdater); +STATIC_TASK(ArduinoOTAUpdater); diff --git a/src/platform/arduino/OTA.h b/src/platform/arduino/OTA.h new file mode 100644 index 0000000..f840666 --- /dev/null +++ b/src/platform/arduino/OTA.h @@ -0,0 +1,14 @@ +#include +#include + +class ArduinoOTAUpdater : public BufferedInputSource { + public: + ArduinoOTAUpdater(); + void loop() override; + void handleEvent(const InputEvent& evt) override; + + private: + bool m_online = false; + static void s_onStart(); + static void s_onProgress(unsigned int progress, unsigned int total); +}; diff --git a/src/platform/arduino/U8Display.cpp b/src/platform/arduino/U8Display.cpp new file mode 100644 index 0000000..b29d465 --- /dev/null +++ b/src/platform/arduino/U8Display.cpp @@ -0,0 +1,232 @@ +#include +#include +#include "../../Static.h" +#include +#include "../../LogService.h" + +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, 16, 15, 4); + +class U8Display : public Task { + public: + U8Display() : Task("U8Display") {} + + enum ScreenState { + BootSplash, + Running, + Message, + Idle = Running + }; + + void onStart() { + xTaskCreatePinnedToCore( + &U8Display::redrawTask, + name, + 2000, + this, + 1, + &m_renderTask, 0 + ); + } + + void handleEvent(const InputEvent& evt) { + m_lastEvent = evt; + if (m_state == Idle) { + switch(evt.intent) { + case InputEvent::NetworkStatus: + m_nextState = Message; + m_message = evt.asBool() ? "Online!" : "Offline :["; + break; + case InputEvent::SetPattern: + m_nextState = Message; + m_message = "Pattern: " + String(evt.asString()); + break; + case InputEvent::SetPower: + m_nextState = Message; + m_message = evt.asBool() ? "Power On" : "Power Off"; + break; + case InputEvent::SaveConfigurationRequest: + m_nextState = Message; + m_message = "Settings Saved!"; + break; + case InputEvent::FirmwareUpdate: + m_nextState = Message; + m_message = "Firmware update"; + break; + case InputEvent::PreviousPattern: + m_nextState = Message; + m_message = "Previous Pattern"; + break; + case InputEvent::NextPattern: + m_nextState = Message; + m_message = "Next Pattern"; + break; + } + } + } + + void drawSplash() { + //u8g2.setFont(u8g2_font_VCR_OSD_mr); + u8g2.setFont(u8g2_font_HelvetiPixelOutline_tr); + u8g2.setDrawColor(1); + uint8_t splashWidth = u8g2.getStrWidth("Renderbug!"); + u8g2.drawStr(64 - splashWidth / 2, 32 - 15, "Renderbug!"); + u8g2.setFont(u8g2_font_7x13B_mr); + u8g2.setCursor(15, 64 - 7); + u8g2.print("Version "); + u8g2.print(RENDERBUG_VERSION); + + for(int i = 0; i < 3; i++) { + uint8_t sparkleX = (64 - splashWidth/2) + scale8(7-i, beatsin8(40)) * (splashWidth/7) + 5; + uint8_t sparkleY = scale8(3+i, beatsin8(40)) * 3 + 7; + u8g2.setDrawColor(2); + if (beatsin8(60*4) + i * 3 >= 170) { + u8g2.drawLine(sparkleX - 3, sparkleY - 3, sparkleX + 3, sparkleY + 3); + u8g2.drawLine(sparkleX + 3, sparkleY - 3, sparkleX - 3, sparkleY + 3); + } else if (beatsin8(60*4) + i * 2 >= 82) { + u8g2.drawLine(sparkleX - 4, sparkleY - 4, sparkleX + 2, sparkleY + 2); + u8g2.drawLine(sparkleX - 2, sparkleY - 2, sparkleX + 4, sparkleY + 4); + u8g2.drawLine(sparkleX + 4, sparkleY - 4, sparkleX - 2, sparkleY + 2); + u8g2.drawLine(sparkleX + 2, sparkleY - 2, sparkleX - 4, sparkleY + 4); + } else { + u8g2.drawLine(sparkleX - 2, sparkleY, sparkleX + 2, sparkleY); + u8g2.drawLine(sparkleX, sparkleY - 2, sparkleX, sparkleY + 2); + } + } + } + + void drawMessage() { + //u8g2.setFont(u8g2_font_VCR_OSD_mr); + u8g2.setFont(u8g2_font_HelvetiPixelOutline_tr); + uint8_t splashWidth = u8g2.getStrWidth(m_message.c_str()); + if (splashWidth >= 128) { + u8g2.setFont(u8g2_font_7x13B_mr); + splashWidth = u8g2.getStrWidth(m_message.c_str()); + } + u8g2.drawStr(64 - splashWidth / 2, 32 - 15, m_message.c_str()); + } + + void drawTime() { + u8g2.setFont(u8g2_font_7x13O_tn); + u8g2.setCursor(0, 64); + struct tm timeinfo; + Platform::getLocalTime(&timeinfo); + uint8_t hour = timeinfo.tm_hour; + uint8_t minute = timeinfo.tm_min; + u8g2.print(hour); + u8g2.print(":"); + u8g2.print(minute); + } + + void drawState(ScreenState state) { + switch(state) { + case BootSplash: + drawSplash(); + break; + case Message: + drawMessage(); + break; + case Running: + uint8_t y = 11; + u8g2.setFont(u8g2_font_7x13B_mr); + u8g2.setCursor(0, y); + u8g2.print("FPS: "); + u8g2.setFont(u8g2_font_7x13O_tn); + u8g2.print(FastLED.getFPS()); + y += 12; + u8g2.setCursor(0, y); + u8g2.setFont(u8g2_font_7x13B_mr); + u8g2.print("Last event: "); + y += 7; + u8g2.setCursor(10, y); + u8g2.setFont(u8g2_font_4x6_tr); + const char* intentName = LogService::intentName(m_lastEvent.intent); + if (intentName) { + u8g2.print(intentName); + } else { + u8g2.print("<"); + u8g2.print(m_lastEvent.intent); + u8g2.print(">"); + } + y += 12; + u8g2.setCursor(15, y); + u8g2.setFont(u8g2_font_7x13O_tf); + u8g2.print(LogService::eventValue(m_lastEvent)); + drawTime(); + break; + } + } + + void loop() { + EVERY_N_MILLISECONDS(8) { + xTaskNotifyGive(m_renderTask); + } + } + + private: + ScreenState m_state = BootSplash; + ScreenState m_nextState = BootSplash; + uint8_t m_transitionFrame = 0; + uint8_t m_screenStartTime = 0; + InputEvent m_lastEvent; + String m_message; + + TaskHandle_t m_renderTask; + void redraw() { + if (m_state != m_nextState) { + EVERY_N_MILLISECONDS(8) { + constexpr uint8_t speed = 11; + if (m_transitionFrame <= 255 - speed) { + m_transitionFrame += speed; + } else { + m_transitionFrame = 255; + } + uint8_t offset = ease8InOutApprox(m_transitionFrame); + u8g2.clearBuffer(); + if (m_transitionFrame <= 128) { + uint8_t width = scale8(128, offset * 2); + u8g2.setDrawColor(1); + drawState(m_state); + u8g2.drawBox(0, 0, width, 64); + u8g2.setDrawColor(2); + u8g2.drawBox(width, 0, 8, 64); + } else { + uint8_t width = scale8(128, offset/2)*2; + u8g2.setDrawColor(1); + drawState(m_nextState); + u8g2.setDrawColor(2); + u8g2.drawBox(std::max(0, width - 8), 0, 8, 64); + u8g2.setDrawColor(1); + u8g2.drawBox(width, 0, 128, 64); + } + u8g2.sendBuffer(); + + if (m_transitionFrame == 255) { + m_state = m_nextState; + m_screenStartTime = millis(); + m_transitionFrame = 0; + } + } + } else { + u8g2.clearBuffer(); + drawState(m_state); + u8g2.sendBuffer(); + uint16_t screenTime = millis() - m_screenStartTime; + if (screenTime >= 7000 && m_state != Idle) { + m_nextState = Idle; + } + } + } + + static void redrawTask(void* data) { + u8g2.begin(); + U8Display* self = static_cast(data); + self->m_screenStartTime = millis(); + while (true) { + self->redraw(); + ulTaskNotifyTake(0, portMAX_DELAY); + } + } +}; + +STATIC_ALLOC(U8Display); +STATIC_TASK(U8Display); diff --git a/src/WiFiTask.cpp b/src/platform/arduino/WiFiTask.cpp similarity index 97% rename from src/WiFiTask.cpp rename to src/platform/arduino/WiFiTask.cpp index 2f819e6..398d1c3 100644 --- a/src/WiFiTask.cpp +++ b/src/platform/arduino/WiFiTask.cpp @@ -39,3 +39,4 @@ WiFiTask::read() } STATIC_ALLOC(WiFiTask); +STATIC_TASK(WiFiTask); diff --git a/src/WiFiTask.h b/src/platform/arduino/WiFiTask.h similarity index 100% rename from src/WiFiTask.h rename to src/platform/arduino/WiFiTask.h diff --git a/src/platform/particle/MDNSService.cpp b/src/platform/particle/MDNSService.cpp index 8d5eb6a..cb87021 100644 --- a/src/platform/particle/MDNSService.cpp +++ b/src/platform/particle/MDNSService.cpp @@ -33,3 +33,4 @@ class MDNSService : public Task { }; STATIC_ALLOC(MDNSService); +STATIC_TASK(MDNSService); diff --git a/src/platform/particle/MQTTTelemetry.cpp b/src/platform/particle/MQTTTelemetry.cpp index 426f214..70b2ee1 100644 --- a/src/platform/particle/MQTTTelemetry.cpp +++ b/src/platform/particle/MQTTTelemetry.cpp @@ -202,4 +202,4 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { }; STATIC_ALLOC(MQTTTelemetry); - +STATIC_TASK(MQTTTelemetry); diff --git a/src/platform/particle/PhotonTelemetry.cpp b/src/platform/particle/PhotonTelemetry.cpp index 2ef2d89..5a503d4 100644 --- a/src/platform/particle/PhotonTelemetry.cpp +++ b/src/platform/particle/PhotonTelemetry.cpp @@ -67,3 +67,4 @@ PhotonTelemetry::handleEvent(const InputEvent &evt) } STATIC_ALLOC(PhotonTelemetry); +STATIC_TASK(PhotonTelemetry); diff --git a/src/platform/particle/Watchdog.cpp b/src/platform/particle/Watchdog.cpp index a12d43e..8d0fb9d 100644 --- a/src/platform/particle/Watchdog.cpp +++ b/src/platform/particle/Watchdog.cpp @@ -28,3 +28,4 @@ class Watchdog : public Task { }; STATIC_ALLOC(Watchdog); +STATIC_TASK(Watchdog); diff --git a/src/platform/particle/WebTelemetry.cpp b/src/platform/particle/WebTelemetry.cpp index f5cb554..ad12a85 100644 --- a/src/platform/particle/WebTelemetry.cpp +++ b/src/platform/particle/WebTelemetry.cpp @@ -154,4 +154,5 @@ class WebTelemetry : public Task { } }; - +STATIC_ALLOC(WebTelemetry); +STATIC_TASK(WebTelemetry); diff --git a/src/platform/particle/inputs/CloudStatus.cpp b/src/platform/particle/inputs/CloudStatus.cpp index 5f658a4..a272e58 100644 --- a/src/platform/particle/inputs/CloudStatus.cpp +++ b/src/platform/particle/inputs/CloudStatus.cpp @@ -26,3 +26,4 @@ CloudStatus::initNetwork(system_event_t event, int param) { } STATIC_ALLOC(CloudStatus); +STATIC_TASK(CloudStatus); diff --git a/src/platform/particle/inputs/Photon.cpp b/src/platform/particle/inputs/Photon.cpp index fd38e42..9ddba84 100644 --- a/src/platform/particle/inputs/Photon.cpp +++ b/src/platform/particle/inputs/Photon.cpp @@ -182,6 +182,5 @@ PhotonInput::setStartPixel(String command) return 0; } - - STATIC_ALLOC(PhotonInput); +STATIC_TASK(PhotonInput); diff --git a/src/sprites/Blob.h b/src/sprites/Blob.h index 2937ec6..ea44fc5 100644 --- a/src/sprites/Blob.h +++ b/src/sprites/Blob.h @@ -1,9 +1,11 @@ #pragma once +#include #include +#include class Blob { - uint16_t m_pos; + uint8_t m_pos; int8_t m_velocity; uint8_t m_hue; int16_t m_brightness; @@ -49,13 +51,26 @@ public: } void render(Display* display) const { + PerfCounter _("blobRender"); const uint8_t width = 25; - //Log.notice("get coords"); auto map = display->coordinateMapping(); // Grab the physical pixel we'll start with - PhysicalCoordinates startPos = map->virtualToPhysicalCoords({m_pos, 0}); - PhysicalCoordinates endPos = map->virtualToPhysicalCoords({m_pos + width, 0}); - uint8_t scaledWidth = std::abs(endPos.x - startPos.x); + //PhysicalCoordinates startPos = map->virtualToPhysicalCoords({m_pos, m_pos}); + //PhysicalCoordinates endPos = map->virtualToPhysicalCoords({m_pos + width, m_pos}); + Surface sfc{display, {m_pos, m_pos}, {qadd8(m_pos, width), qadd8(m_pos, width)}}; + sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) { + uint8_t pixelMod = std::min(surfaceCoords.y, surfaceCoords.x); + pixelMod = surfaceCoords.x; + // Blobs desaturate and dim towards their edges + uint8_t saturation = lerp8by8(0, m_brightness, pixelMod); + uint8_t val = lerp8by8(0, m_brightness, pixelMod); + CHSV blobColor(m_hue, m_saturation, val); + + //PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), startPos.y}; + + pixel += blend(CRGB(blobColor), pixel, 80); + }); + /*uint8_t scaledWidth = std::abs(endPos.x - startPos.x); //Log.notice("blob w=%d x=%d", scaledWidth, startPos.x); for(uint8_t i = 0;i < scaledWidth; i++) { @@ -66,10 +81,10 @@ public: //CHSV blobColor(m_hue, m_saturation, quadwave8((i / (double)scaledWidth) * m_brightness)); CHSV blobColor(m_hue, m_saturation, quadwave8(val)); - PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), 0}; + PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), startPos.y}; CRGB src(display->pixelAt(pos)); - display->pixelAt(pos) = blend(CRGB(blobColor), src, 200); - } + display->pixelAt(pos) = blend(CRGB(blobColor), src, 140); + }*/ } }; diff --git a/test/README b/test/README deleted file mode 100644 index b94d089..0000000 --- a/test/README +++ /dev/null @@ -1,11 +0,0 @@ - -This directory is intended for PlatformIO Unit Testing and project tests. - -Unit Testing is a software testing method by which individual units of -source code, sets of one or more MCU program modules together with associated -control data, usage procedures, and operating procedures, are tested to -determine whether they are fit for use. Unit testing finds problems early -in the development cycle. - -More information about PlatformIO Unit Testing: -- https://docs.platformio.org/page/plus/unit-testing.html