From ebbf433cdf29faeb65855786526ec02b706b410c Mon Sep 17 00:00:00 2001 From: Torrie Fischer Date: Tue, 26 Dec 2023 11:29:49 +0100 Subject: [PATCH] wip commit --- data/maps/ponder.json | 2 +- data/profiles/default.json | 3 +- data/profiles/djstrip.json | 16 +-- data/profiles/home-lighting.json | 7 +- data/profiles/ponderjar.json | 46 ++++--- data/profiles/pondertest.json | 7 + lib/Figments/Command.h | 3 + lib/Figments/Input.h | 2 +- lib/Figments/Renderer.cpp | 1 + lib/Figments/Scheduler.h | 0 lib/Figments/Surface.cpp | 8 ++ lib/Figments/Surface.h | 2 + platformio.ini | 5 +- src/Colors.cpp | 153 ++++++++++++++++++++++ src/Colors.h | 36 +++++ src/Config.cpp | 60 +++++---- src/Config.h | 2 + src/JsonCoordinateMapping.cpp | 43 +++--- src/JsonCoordinateMapping.h | 10 +- src/LogService.cpp | 2 +- src/Platform.cpp | 2 +- src/Scripts.cpp | 33 +++++ src/Scripts.h | 12 ++ src/Sequencer.cpp | 124 ++++++++++++------ src/Sequencer.h | 13 +- src/animations/Cloudy.cpp | 46 +++++++ src/animations/Cloudy.h | 13 ++ src/animations/Rain.cpp | 10 +- src/animations/Rain.h | 2 +- src/animations/SolidAnimation.cpp | 24 ++-- src/animations/SolidAnimation.h | 1 + src/animations/TestAnimation.cpp | 105 +++++++++++++-- src/animations/TestAnimation.h | 23 ++++ src/animations/Thinking.cpp | 26 ++++ src/inputs/Serial.cpp | 13 +- src/inputs/Weather.cpp | 147 +++++++++++++++++++++ src/inputs/Weather.h | 51 ++++++++ src/main.cpp | 4 +- src/platform/arduino/esp8266/Platform.cpp | 13 +- src/sprites/Blob.h | 4 +- 40 files changed, 904 insertions(+), 170 deletions(-) create mode 100644 data/profiles/pondertest.json create mode 100644 lib/Figments/Scheduler.h create mode 100644 src/Colors.cpp create mode 100644 src/Colors.h create mode 100644 src/Scripts.cpp create mode 100644 src/Scripts.h create mode 100644 src/animations/Cloudy.cpp create mode 100644 src/animations/Cloudy.h create mode 100644 src/animations/Thinking.cpp create mode 100644 src/inputs/Weather.cpp create mode 100644 src/inputs/Weather.h diff --git a/data/maps/ponder.json b/data/maps/ponder.json index 9e7f20c..40571c0 100644 --- a/data/maps/ponder.json +++ b/data/maps/ponder.json @@ -1,6 +1,6 @@ { "version": 1, - "rotation": 3, + "rotation": 2, "strides": [ {"x": 0, "y": 0, "pixels": 17}, {"x": 0, "y": 1, "pixels": 17}, diff --git a/data/profiles/default.json b/data/profiles/default.json index 727d423..bb5d90a 100644 --- a/data/profiles/default.json +++ b/data/profiles/default.json @@ -2,7 +2,6 @@ "version": 1, "tasks": [ "Renderer", - "U8Display", "WiFi", "MQTT", "ArduinoOTA", @@ -10,7 +9,7 @@ "Serial" ], "scenes": { - "Idle": ["Solid", "MPU5060", "IdleColors", "CircadianRhythm"], + "Idle": ["Solid", "IdleColors", "CircadianRhythm"], "Flashlight": ["Flashlight"] }, "surfaceMap": "default", diff --git a/data/profiles/djstrip.json b/data/profiles/djstrip.json index 05f9ffb..28dc841 100644 --- a/data/profiles/djstrip.json +++ b/data/profiles/djstrip.json @@ -5,20 +5,20 @@ "Renderer", "Serial" ], - "scenes": { - "Rain": ["Rain", "Rainbow"], - "Test": ["Test"], - "Idle": ["Solid", "Pulse", "Rainbow", "CircadianRhythm"], - "Acid": ["Chimes", "Pulse", "IdleColors", "Rainbow"], - "Flashlight": ["Flashlight"] - }, "surfaceMap": "djstrip", "defaults": { "mqtt.ip": "10.0.0.2", "power.volts": 0, "power.milliamps": 0, "power.useBPM": false, - "bpm.idle": 45 + "bpm.idle": 45, + "scenes": { + "Rain": ["Rain", "Rainbow"], + "Test": ["Test"], + "Idle": ["Solid", "Pulse", "Rainbow", "CircadianRhythm"], + "Acid": ["Chimes", "Pulse", "IdleColors", "Rainbow"], + "Flashlight": ["Flashlight"] + } } } diff --git a/data/profiles/home-lighting.json b/data/profiles/home-lighting.json index b4404f1..dcc0e0f 100644 --- a/data/profiles/home-lighting.json +++ b/data/profiles/home-lighting.json @@ -7,11 +7,12 @@ "MQTT", "ArduinoOTA", "UpdateStatusAnimation", - "Serial" + "Serial", + "U8Display" ], "scenes": { - "Idle": ["Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"], - "Acid": ["Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"], + "Idle": ["Solid", "Rainbow", "CircadianRhythm"], + "Acid": ["Chimes", "Rainbow"], "Flashlight": ["Flashlight"] }, "surfaceMap": "default", diff --git a/data/profiles/ponderjar.json b/data/profiles/ponderjar.json index 3472c86..dfdcb6e 100644 --- a/data/profiles/ponderjar.json +++ b/data/profiles/ponderjar.json @@ -8,24 +8,40 @@ "UpdateStatusAnimation", "Serial", "Weather", - "CircadianRhythm" + "Test", + "CircadianRhythm", + "IdleTimer" ], - "scenes": { - "Clear": ["Solid", "Rainbow"], - "Rain": ["Rain"], - "Drizzle": ["Rain"], - "Mist": ["Rain"], - "Snow": ["Solid"], - "UnknownWeather": ["Solid", "IdleColors"], - "Test": ["Test"], - "Idle": ["Solid", "Rainbow"], - "Acid": ["Chimes", "IdleColors", "Rainbow"], - "Flashlight": ["Flashlight"] - }, "surfaceMap": "ponder", "defaults": { "mqtt.ip": "10.0.0.2", - "power.volts": 0, - "power.milliamps": 0 + "colors.sequences": { + "Rainbow": [[255, 0, 0], [255, 127, 0], [0, 255, 0], [0, 0, 255], [128, 0, 128]], + "Idle": [[0, 123, 167], [80, 200, 120], [207, 113, 175], [128, 0, 128], [255, 255, 255], [0, 255, 255]], + "Wet": [[3, 177, 252], [3, 78, 252], [126, 141, 242]], + "Snowy": [[215, 219, 245], [255, 255, 255], [192, 250, 244]], + "Sunny": [[213, 237, 95], [237, 180, 95], [245, 178, 10]] + }, + "colors.scenes": { + "Clear": "Sunny", + "Idle": "Rainbow", + "Rain": "Wet", + "Drizzle": "Wet", + "Mist": "Wet", + "Snow": "Snowy" + }, + "colors.secondsPerColor": 9, + "scenes": { + "Clear": ["Solid"], + "Cloudy": ["Cloudy", "Solid"], + "Rain": ["Rain", "Solid"], + "Drizzle": ["Rain", "Cloudy", "Solid"], + "Mist": ["Cloudy", "Solid"], + "Snow": ["Solid"], + "UnknownWeather": ["Thinking"], + "Start": ["Thinking"], + "Idle": ["Solid"], + "Test": ["Test"] + } } } diff --git a/data/profiles/pondertest.json b/data/profiles/pondertest.json new file mode 100644 index 0000000..6b494c4 --- /dev/null +++ b/data/profiles/pondertest.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "tasks": [ + "Renderer", + "Serial" + ] +} diff --git a/lib/Figments/Command.h b/lib/Figments/Command.h index 893e045..19fc371 100644 --- a/lib/Figments/Command.h +++ b/lib/Figments/Command.h @@ -6,6 +6,9 @@ class Args { String *str; public: Args(String *str) : str(str) {} + operator const char*() const { + return str->c_str(); + } String operator[](int pos) { char buf[64]; strncpy(buf, str->c_str(), sizeof(buf)); diff --git a/lib/Figments/Input.h b/lib/Figments/Input.h index 1b73b8b..a3a7d52 100644 --- a/lib/Figments/Input.h +++ b/lib/Figments/Input.h @@ -108,7 +108,7 @@ struct InputEvent: public Variant { InputEvent() : Variant(), intent(None) {} - bool operator!=(const InputEvent::Intent& otherIntent) { + bool operator!=(const InputEvent::Intent& otherIntent) const { return intent != otherIntent; } diff --git a/lib/Figments/Renderer.cpp b/lib/Figments/Renderer.cpp index 112dc25..49b99b7 100644 --- a/lib/Figments/Renderer.cpp +++ b/lib/Figments/Renderer.cpp @@ -21,6 +21,7 @@ Renderer::loop() { uint16_t totalPower = 0; for(Display* dpy : m_displays) { + dpy->clear(); totalPower += calculate_unscaled_power_mW(dpy->pixelBacking(), dpy->pixelCount()); for(Figment* figment : m_figments) { if (figment->state == Task::Running) { diff --git a/lib/Figments/Scheduler.h b/lib/Figments/Scheduler.h new file mode 100644 index 0000000..e69de29 diff --git a/lib/Figments/Surface.cpp b/lib/Figments/Surface.cpp index a92ada9..1bdac11 100644 --- a/lib/Figments/Surface.cpp +++ b/lib/Figments/Surface.cpp @@ -43,6 +43,14 @@ Surface::operator+=(const CRGB& color) return *this; } +void +Surface::blend(const CRGB& color, uint8_t pct) +{ + paintWith([&](CRGB& pixel) { + nblend(pixel, color, pct); + }); +} + void Surface::paintWith(std::function func) { diff --git a/lib/Figments/Surface.h b/lib/Figments/Surface.h index 62351d7..ebe9a1f 100644 --- a/lib/Figments/Surface.h +++ b/lib/Figments/Surface.h @@ -31,6 +31,8 @@ public: */ Surface& operator+=(const CRGB& color); + void blend(const CRGB& color, uint8_t pct); + /** * OR operation that applies the given color to every pixel on the surface */ diff --git a/platformio.ini b/platformio.ini index fd157f6..4bd4e3d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,21 +11,18 @@ [env] src_filter = +<*>, -<.git/>, -<.svn/>, - lib_ldf_mode = chain+ -extra_scripts = pre:verify-configs.py, pre:build-hal.py +extra_scripts = post:verify-configs.py, pre:build-hal.py check_flags = cppcheck: --inline-suppr --suppress=*:*/.pio/* board_build.filesystem = littlefs upload_speed = 115200 monitor_speed = 115200 -build_type = debug build_flags = -DFASTLED_ALL_PINS_HARDWARE_SPI src_build_flags = -DFASTLED_ALL_PINS_HARDWARE_SPI -DRENDERBUG_VERSION=3 -DRENDERBUG_LED_PIN=14 - -DRENDERBUG_LED_PACKING=RGB - -DDEFAULT_PATTERN_INDEX=0 -fstack-protector -Wall lib_deps = diff --git a/src/Colors.cpp b/src/Colors.cpp new file mode 100644 index 0000000..5999ce5 --- /dev/null +++ b/src/Colors.cpp @@ -0,0 +1,153 @@ +#include "Colors.h" +#include "Static.h" + +Colors::Colors() + : BufferedInputSource("Colors"), + m_colorTimer(0), + m_secondsPerColor(9) +{ + state = Task::Running; +} + +void +Colors::loop() +{ + EVERY_N_SECONDS(1) { + if (m_colorTimer > 0) { + Log.verbose("colors: tick"); + m_colorTimer -= 1; + + if (m_colorTimer == 0) { + rotate(); + } + } + } + + ConfigTaskMixin::loop(); +} + +void +Colors::handleEvent(const InputEvent& evt) +{ + if (evt.intent == InputEvent::SetScene) { + setScene(evt.asString()); + } + ConfigTaskMixin::handleEvent(evt); +} + +void +Colors::setSequence(const char* seqName) +{ + bool found = false; + int idx = 0; + for(Sequence seq : m_sequences) { + if (strcmp(seq.name.c_str(), seqName) == 0) { + found = true; + break; + } + idx++; + } + if (found) { + Log.info("colors: Setting sequence to %s", seqName); + m_idx = idx; + rotate(); + } else { + Log.warning("colors: Unknown sequence %s", seqName); + } +} + +void +Colors::rotate() +{ + if (!m_sequences.empty()) { + Log.info("colors: rotate!") ; + m_colorIdx += 1; + m_colorIdx %= m_sequences[m_idx].colors.size(); + m_colorTimer = m_secondsPerColor; + MainLoop::instance()->dispatch(InputEvent{InputEvent::SetColor, m_sequences[m_idx].colors[m_colorIdx]}); + //setEvent(InputEvent{InputEvent::SetColor, m_sequences[m_idx].colors[m_colorIdx]}); + } else { + Log.info("colors: No colors to rotate"); + } +} + +void +Colors::setScene(const char* sceneName) +{ + auto found = m_sceneMap.find(sceneName); + if (found != m_sceneMap.end()) { + Log.info("colors: Running scene %s", sceneName); + setSequence(found->second); + } else { + Log.warning("colors: Unknown scene %s", sceneName); + m_colorTimer = 0; + } +} + +void +Colors::handleConfigChange(const Configuration& cfg) +{ + Log.info("colors: reloading config"); + m_sequences.clear(); + m_colorIdx = 0; + m_idx = 0; + m_secondsPerColor = cfg.get("colors.secondsPerColor", 9); + m_colorTimer = 0; + JsonObject seqJson = cfg.get("colors.sequences"); + for(const auto seq : seqJson) { + Log.info("colors: Found color sequence %s", seq.key().c_str()); + std::vector colors; + for(JsonArray rgb : seq.value().as()) { + colors.emplace_back(CRGB(rgb[0], rgb[1], rgb[2])); + } + m_sequences.emplace_back(Sequence{ + seq.key().c_str(), + std::move(colors) + }); + } + + m_sceneMap.clear(); + JsonObject sceneJson = cfg.get("colors.scenes"); + for(const auto scene : sceneJson) { + Log.info("colors: Found scene %s", scene.key().c_str()); + m_sceneMap[scene.key().c_str()] = scene.value().as().c_str(); + } + + rotate(); +} + +void +Colors::doSequences(Args& args, Print& out) +{ + out.println("Color sequences:"); + for(const Sequence& seq : m_sequences) { + out.print("\t"); + out.println(seq.name.c_str()); + } +} + +void +Colors::doSequence(Args& args, Print& out) +{ + setSequence(args[1].c_str()); +} + +void +Colors::doRotate(Args& args, Print& out) +{ + rotate(); +} + +const std::vector& +Colors::commands() const +{ + static const std::vector _commands = { + {"sequences", &Colors::doSequences}, + {"sequence", &Colors::doSequence}, + {"rotate", &Colors::doRotate} + }; + return _commands; +} + +STATIC_TASK(Colors); +STATIC_ALLOC(Colors); diff --git a/src/Colors.h b/src/Colors.h new file mode 100644 index 0000000..53319ce --- /dev/null +++ b/src/Colors.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include "Config.h" +#include + +class Colors: public BufferedInputSource, ConfigTaskMixin { + public: + struct Sequence { + String name; + const std::vector colors; + }; + + Colors(); + void loop() override; + void handleEvent(const InputEvent& evt) override; + void handleConfigChange(const Configuration& cfg) override; + const std::vector& commands() const override; + + private: + uint16_t m_idx; + uint16_t m_colorIdx; + uint16_t m_colorTimer; + uint16_t m_secondsPerColor; + std::vector m_sequences; + std::map m_sceneMap; + + void setScene(const char* sceneName); + void setSequence(const char* seqName); + + void doSequences(Args& args, Print& out); + void doSequence(Args& args, Print& out); + void doRotate(Args& args, Print& out); + + void rotate(); +}; diff --git a/src/Config.cpp b/src/Config.cpp index e98b095..29586b1 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -1,6 +1,5 @@ #include "./Config.h" #include "./Static.h" -#include "./Sequencer.h" #include #include @@ -23,6 +22,12 @@ Configuration::Configuration(const JsonObject& data) { } +JsonVariant +Configuration::get(const char* key) const +{ + return m_json[key]; +} + const char* Configuration::get(const char* key, const char* defaultVal) const { @@ -53,8 +58,6 @@ Configuration::get(const char* key, bool defaultVal) const } } -StaticJsonDocument<2048> jsonConfig; - constexpr uint16_t HardwareConfig::MAX_LED_NUM; HardwareConfig @@ -149,12 +152,15 @@ ConfigService::loadMap(const String& mapName) { String fname = String("/maps/") + mapName + ".json"; if (LittleFS.exists(fname)) { + DynamicJsonDocument jsonConfig(2048); File configFile = LittleFS.open(fname, "r"); Log.notice("config: Loading coordinate map from %s", fname.c_str()); deserializeJson(jsonConfig, configFile); configFile.close(); JsonArray strideList = jsonConfig["strides"]; + uint8_t rotation = jsonConfig["rotation"]; m_jsonMap.load(strideList); + m_jsonMap.rotation = rotation; return true; } else { return false; @@ -171,25 +177,13 @@ ConfigService::loadProfile(const char* profileName) if (LittleFS.exists(fname)) { strncpy(m_config.data.loadedProfile, fname.c_str(), sizeof(m_config.data.loadedProfile)); File configFile = LittleFS.open(fname, "r"); - jsonConfig.clear(); - deserializeJson(jsonConfig, configFile); + DynamicJsonDocument jsonConfig(2048*2); + auto err = deserializeJson(jsonConfig, configFile); configFile.close(); - //int profileLogLevel = max(0, min(6, jsonConfig["logLevel"])); - //Log.setLevel(profileLogLevel); - //Log.trace("config: \t %d logging level"); - - JsonObject sceneList = jsonConfig["scenes"]; - std::vector scenes; - for(JsonPair pair : sceneList) { - Log.notice("config: \tFound scene %s", pair.key().c_str()); - std::vector patterns; - for(const char* taskName : pair.value().as()) { - patterns.push_back(taskName); - } - scenes.push_back(Sequencer::Scene{pair.key().c_str(), patterns}); + if (err) { + Log.error("JSON failure: %s", err.c_str()); } - Static::instance()->setScenes(std::move(scenes)); JsonArray taskList = jsonConfig["tasks"]; Log.notice("config: Starting %d tasks", taskList.size()); @@ -197,14 +191,8 @@ ConfigService::loadProfile(const char* profileName) MainLoop::instance()->dispatchSync(InputEvent{InputEvent::StartThing, taskList[i].as()}); } - JsonObject defaults = jsonConfig["defaults"]; - Log.notice("config: Loading %d app configurations", defaults.size()); - MainLoop::instance()->dispatchSync(InputEvent{InputEvent::ConfigurationChanged, &defaults}); - String mapName = jsonConfig["surfaceMap"]; - jsonConfig.clear(); - if (mapName.isEmpty()) { Log.warning("config: No coordinate map defined! Defaulting to linear mapping.", mapName.c_str()); m_jsonMap.loadDefault(); @@ -212,6 +200,11 @@ ConfigService::loadProfile(const char* profileName) Log.warning("config: Couldn't load coordinate map %s!!! Defaulting to linear mapping.", mapName.c_str()); m_jsonMap.loadDefault(); } + + JsonObject defaults = jsonConfig["defaults"]; + Log.notice("config: Loading %d app configurations", defaults.size()); + MainLoop::instance()->dispatchSync(InputEvent{InputEvent::ConfigurationChanged, &defaults}); + Log.notice("config: Loaded!"); strcpy(m_config.data.loadedProfile, profileName); } else { @@ -223,8 +216,8 @@ ConfigService::loadProfile(const char* profileName) Log.notice("config: Configured to use %d pixels", m_jsonMap.physicalPixelCount()); PhysicalCoordinates topLeft = m_jsonMap.virtualToPhysicalCoords({0, 0}); PhysicalCoordinates bottomRight = m_jsonMap.virtualToPhysicalCoords({255, 255}); - Log.verbose(" (0,0) -> (%d, %d) -> %d", topLeft.x, topLeft.y, m_jsonMap.physicalCoordsToIndex(topLeft)); - Log.verbose(" (255,255) -> (%d, %d) -> %d", bottomRight.x, bottomRight.y, m_jsonMap.physicalCoordsToIndex(bottomRight)); + Log.info(" (0,0) -> (%d, %d) -> %d", topLeft.x, topLeft.y, m_jsonMap.physicalCoordsToIndex(topLeft)); + Log.info(" (255,255) -> (%d, %d) -> %d", bottomRight.x, bottomRight.y, m_jsonMap.physicalCoordsToIndex(bottomRight)); return true; } @@ -293,6 +286,16 @@ ConfigService::doCoordMap(Args& args, Print& out) out.println(buf); } +void +ConfigService::doRotate(Args& args, Print& out) +{ + out.print("Rotation: "); + out.print(m_jsonMap.rotation); + out.print(" "); + m_jsonMap.rotation = atoi(args[1].c_str()); + out.println(m_jsonMap.rotation); +} + const std::vector& ConfigService::commands() const { @@ -300,7 +303,8 @@ ConfigService::commands() const {"save", &ConfigService::doSave}, {"profile", &ConfigService::doSetProfile}, {"maps", &ConfigService::doMapList}, - {"coordmap", &ConfigService::doCoordMap} + {"coordmap", &ConfigService::doCoordMap}, + {"rotate", &ConfigService::doRotate} }; return _commands; } diff --git a/src/Config.h b/src/Config.h index 6633e63..8c7cc4a 100644 --- a/src/Config.h +++ b/src/Config.h @@ -10,6 +10,7 @@ class Configuration { const char* get(const char* key, const char* defaultVal) const; int get(const char* key, int defaultVal) const; bool get(const char* key, bool defaultVal) const; + JsonVariant get(const char* name) const; private: const JsonObject& m_json; @@ -79,4 +80,5 @@ private: void doSetProfile(Args& args, Print& print); void doCoordMap(Args& args, Print& print); void doMapList(Args& args, Print& print); + void doRotate(Args& args, Print& print); }; diff --git a/src/JsonCoordinateMapping.cpp b/src/JsonCoordinateMapping.cpp index 050f12f..742cc6a 100644 --- a/src/JsonCoordinateMapping.cpp +++ b/src/JsonCoordinateMapping.cpp @@ -11,24 +11,33 @@ JsonCoordinateMapping::physicalPixelCount() const { int JsonCoordinateMapping::physicalCoordsToIndex(const PhysicalCoordinates localCoords) const { - 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)); + return displayMap[localCoords.x].physicalIdx + localCoords.y; +} + +VirtualCoordinates +JsonCoordinateMapping::rotate(const VirtualCoordinates coords, uint8_t rot) const +{ + if (rot == 0) { + return coords; + } else if (rot == 1) { + return VirtualCoordinates{coords.y, coords.x}; + } else if (rot == 2) { + return VirtualCoordinates{255 - coords.y, 255 - coords.x}; + } else if (rot == 3) { + return VirtualCoordinates{255 - coords.x, 255 - coords.y}; } else { - idx += std::min((int)displayMap[localCoords.x].length - 1, std::max(0, (int)localCoords.y - displayMap[localCoords.x].y)); + return coords; } - return idx; } PhysicalCoordinates JsonCoordinateMapping::virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const { - const uint8_t spanIdx = scale8(strideCount-1, virtualCoords.x); - const uint8_t spanOffset = scale8(maxStrideSize - 1, virtualCoords.y); + // 0, 0 -> first pixel, first strand + // 0, 255 -> first pixel, last strand + // 255, 255 -> last pixel, last strand + const VirtualCoordinates rotated = rotate(virtualCoords, rotation); + const uint8_t spanIdx = scale8(strideCount-1, rotated.x); + const uint8_t spanOffset = scale8(displayMap[spanIdx].length, rotated.y); return PhysicalCoordinates{spanIdx, spanOffset}; } @@ -48,23 +57,23 @@ JsonCoordinateMapping::physicalToVirtualCoords(const PhysicalCoordinates localCo void JsonCoordinateMapping::load(const JsonArray& strideList) { - maxStrideSize = 0; strideCount = strideList.size(); + uint16_t idx = 0; for(int i = 0; i < strideList.size();i++) { JsonObject strideObj = strideList[i]; int length = strideObj["pixels"]; int x = strideObj["x"]; int y = strideObj["y"]; - displayMap[i] = Span{length, x, y}; + bool reverse = strideObj["reverse"]; + displayMap[i] = Span{length, x, y, reverse, idx}; + idx += length; Log.info("config: Found stride (%d, %d): %d", x, y, length); - maxStrideSize = length > maxStrideSize ? length : maxStrideSize; } } void JsonCoordinateMapping::loadDefault() { - displayMap[0] = Span{255, 0, 0}; - maxStrideSize = 255; + displayMap[0] = Span{255, 0, 0, false, 0}; strideCount = 1; } diff --git a/src/JsonCoordinateMapping.h b/src/JsonCoordinateMapping.h index db7beb4..b4d66a8 100644 --- a/src/JsonCoordinateMapping.h +++ b/src/JsonCoordinateMapping.h @@ -7,14 +7,18 @@ struct JsonCoordinateMapping : CoordinateMapping { int length = 0; int x = 0; int y = 0; + bool reverse = false; + + uint16_t physicalIdx = 0; Span() {} - Span(int length, int x, int y) : length(length), x(x), y(y) {} + Span(int length, int x, int y, bool rev, uint16_t idx) : length(length), x(x), y(y), reverse(rev), physicalIdx(idx) {} }; + uint8_t rotation = 0; + Span displayMap[32]; int strideCount = 0; - int maxStrideSize = 0; void load(const JsonArray& strides); void loadDefault(); @@ -23,4 +27,6 @@ struct JsonCoordinateMapping : CoordinateMapping { PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const override; int physicalCoordsToIndex(const PhysicalCoordinates localCoords) const override; unsigned int physicalPixelCount() const override; + + VirtualCoordinates rotate(const VirtualCoordinates coords, uint8_t rotation) const; }; diff --git a/src/LogService.cpp b/src/LogService.cpp index 4750c8b..099c2ea 100644 --- a/src/LogService.cpp +++ b/src/LogService.cpp @@ -29,7 +29,7 @@ LogService::eventValue(const InputEvent& evt) void LogService::handleEvent(const InputEvent& evt) { if (evt.intent != InputEvent::None) { - if (evt.intent != m_lastEvent.intent) { + if (true || evt.intent != m_lastEvent.intent) { if (m_duplicateEvents > 0) { Log.trace("Suppressed reporting %d duplicate events.", m_duplicateEvents); } diff --git a/src/Platform.cpp b/src/Platform.cpp index 18e54c5..5658aa0 100644 --- a/src/Platform.cpp +++ b/src/Platform.cpp @@ -15,7 +15,7 @@ void printNewline(Print* logOutput, int logLevel) } #endif -int Platform::s_timezone = 0; +int Platform::s_timezone = 1; Platform::TaskRegistration* Platform::firstTask = NULL; Platform::TaskRegistration* Platform::lastTask = NULL; diff --git a/src/Scripts.cpp b/src/Scripts.cpp new file mode 100644 index 0000000..fd8c0f5 --- /dev/null +++ b/src/Scripts.cpp @@ -0,0 +1,33 @@ +#include "Scripts.h" +#include "Static.h" + +Scripts::Scripts() + : Task("Scripts") +{ + state = Running; +} + +void +Scripts::loop() +{ +} + +void +Scripts::doEval(Args& args, Print& out) +{ + String fullCmd((const char*)args); + String fullScript(fullCmd.substring(fullCmd.indexOf(" ") + 1)); +} + +const std::vector& +Scripts::commands() const +{ + static const std::vector _commands{ + {"eval", &Scripts::doEval} + }; + + return _commands; +} + +STATIC_ALLOC(Scripts); +STATIC_TASK(Scripts); diff --git a/src/Scripts.h b/src/Scripts.h new file mode 100644 index 0000000..5c1748a --- /dev/null +++ b/src/Scripts.h @@ -0,0 +1,12 @@ +#pragma once +#include + +class Scripts : public Task { + public: + Scripts(); + void loop() override; + const std::vector& commands() const; + + private: + void doEval(Args& args, Print& out); +}; diff --git a/src/Sequencer.cpp b/src/Sequencer.cpp index 8fb051b..b572813 100644 --- a/src/Sequencer.cpp +++ b/src/Sequencer.cpp @@ -3,8 +3,9 @@ #include "Static.h" Sequencer::Sequencer() : - Task("SceneSequencer"), - m_idx(0) + Task("Sequencer"), + m_idx(0), + m_sceneBeforeIdle(-1) { state = Task::Running; } @@ -12,28 +13,32 @@ Sequencer::Sequencer() : void Sequencer::Scene::start() { - for(const char* pattern : patterns) { - Log.verbose("Starting pattern task %s", pattern); - MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern}); + for(const auto& pattern : patterns) { + //pattern.getBytes(namebuf, sizeof(namebuf)); + Log.info("sequencer: Starting pattern task %s", pattern.c_str()); + MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern.c_str()}); } } void Sequencer::Scene::stop() { - for(const char* pattern : patterns) { - Log.verbose("Stopping pattern task %s", pattern); - MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern}); + for(const auto& pattern : patterns) { + Log.info("sequencer: Stopping pattern task %s", pattern.c_str()); + MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern.c_str()}); } } void -Sequencer::loop() {} +Sequencer::loop() +{ + ConfigTaskMixin::loop(); +} const char* Sequencer::currentSceneName() { - return m_scenes[m_idx].name; + return m_scenes[m_idx].name.c_str(); } const std::vector @@ -48,42 +53,77 @@ Sequencer::onStart() } void -Sequencer::setScenes(std::vector &&scenes) +Sequencer::handleEvent(const InputEvent& evt) { - m_idx = 0; - m_scenes = scenes; + if (!m_scenes.empty()) { + if (evt.intent == InputEvent::ReadyToRoll) { + Log.info("sequencer: Activating startup scene"); + if (!changeToScene("Start")) { + Log.info("sequencer: Start scene not found, falling back to first scene"); + changeToScene(m_scenes[0].name.c_str()); + } + } else if (evt.intent == InputEvent::SetScene && m_scenes[m_idx].name == evt.asString()) { + return; + } else if (evt.intent == InputEvent::SetScene) { + changeToScene(evt.asString()); + } else if (evt.intent == InputEvent::IdleStart) { + Log.info("sequencer: Activating idle scene"); + m_sceneBeforeIdle = m_idx; + MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, "Idle"}); + } else if (evt.intent == InputEvent::IdleStop && m_sceneBeforeIdle >= 0) { + Log.info("sequencer: Resuming pre-idle scene"); + //changeToScene(m_scenes[m_sceneBeforeIdle].name.c_str()); + String sceneName = m_scenes[m_sceneBeforeIdle].name; + m_sceneBeforeIdle = -1; + MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, sceneName.c_str()}); + } + } + ConfigTaskMixin::handleEvent(evt); } void -Sequencer::handleEvent(const InputEvent& evt) +Sequencer::handleConfigChange(const Configuration& cfg) { - if (evt.intent == InputEvent::ReadyToRoll && !m_scenes.empty()) { - 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}); + Log.notice("sequencer: Loading scenes"); + + JsonObject sceneList = cfg.get("scenes"); + m_idx = 0; + m_scenes.clear(); + for(JsonPair pair : sceneList) { + Log.notice("sequencer: Found scene %s", pair.key().c_str()); + std::vector patterns; + for(const char* taskName : pair.value().as()) { + patterns.push_back(taskName); + } + m_scenes.push_back(Scene{pair.key().c_str(), patterns}); + } + if (m_scenes.empty()) { + Log.warning("sequencer: Zero scenes loaded!"); + } +} + +bool +Sequencer::changeToScene(const char* name) +{ + Log.notice("sequencer: Switching scene to %s!", name); + + int newIdx = 0; + bool found = false; + for(newIdx = 0; newIdx < m_scenes.size(); newIdx++) { + if (!strcmp(name, m_scenes[newIdx].name.c_str())) { + found = true; + break; } - } else if (evt.intent == InputEvent::SetScene && evt.asString() == m_scenes[m_idx].name) { - return; - } else if (evt.intent == InputEvent::SetScene) { - Log.notice("Switching scene to %s!", evt.asString()); + } - int newIdx = 0; - bool found = false; - for(newIdx = 0; newIdx < m_scenes.size(); newIdx++) { - if (!strcmp(evt.asString(), m_scenes[newIdx].name)) { - found = true; - break; - } - } - - if (found) { - m_scenes[m_idx].stop(); - m_idx = newIdx; - m_scenes[m_idx].start(); - } else { - Log.notice("Scene not found!"); - } + if (found) { + m_scenes[m_idx].stop(); + m_idx = newIdx; + m_scenes[m_idx].start(); + return true; + } else { + Log.notice("sequencer: Scene not found!"); + return false; } } @@ -92,7 +132,7 @@ Sequencer::doScenes(Args& args, Print& out) { out.println("Available scenes: "); for (auto scene : scenes()) { - out.println(scene.name); + out.println(scene.name.c_str()); } } @@ -101,8 +141,8 @@ static String s; void Sequencer::doScene(Args& args, Print& out) { - s = args[1]; - MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, s.c_str()}); + String sceneName = args[1]; + MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, sceneName.c_str()}); } const std::vector& diff --git a/src/Sequencer.h b/src/Sequencer.h index 0c8ed29..7f5a847 100644 --- a/src/Sequencer.h +++ b/src/Sequencer.h @@ -2,14 +2,15 @@ #include #include +#include "Config.h" -class Sequencer: public Task { +class Sequencer: public Task, ConfigTaskMixin { public: class Scene { public: - const char* name; - std::vector patterns; + String name; + std::vector patterns; void start(); void stop(); }; @@ -19,7 +20,7 @@ public: void loop() override; void onStart() override; void handleEvent(const InputEvent& evt) override; - void setScenes(std::vector &&scenes); + void handleConfigChange(const Configuration& cfg) override; const char* currentSceneName(); const std::vector scenes() const; @@ -27,8 +28,12 @@ public: private: int m_idx; + int m_sceneBeforeIdle; + bool m_hasStartupScene; std::vector m_scenes; + bool changeToScene(const char* name); + void doScene(Args& args, Print& out); void doScenes(Args& args, Print& out); }; diff --git a/src/animations/Cloudy.cpp b/src/animations/Cloudy.cpp new file mode 100644 index 0000000..00997cd --- /dev/null +++ b/src/animations/Cloudy.cpp @@ -0,0 +1,46 @@ +#include "Cloudy.h" +#include "../Static.h" + +Cloudy::Cloudy() + : Figment("Cloudy") +{ +} + +void +Cloudy::loop() +{ + m_noiseOffset += 1; + EVERY_N_MILLISECONDS(60) { + m_hue.update(1); + } +} + +void +Cloudy::render(Display* dpy) const +{ + Surface sfc = Surface(dpy, {0, 0}, {255, 128}); + uint8_t noiseY = sin8(m_noiseOffset % 255); + uint8_t noiseX = cos8(m_noiseOffset % 255); + sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) { + uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y); + uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x); + nblend(pixel, CHSV(m_hue, scale8(saturation, 128), brightness), 255 - surfaceCoords.y); + }); + Surface horizon = Surface(dpy, {0, 64}, {255, 128}); + horizon.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) { + uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y); + uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x); + nblend(pixel, CHSV(m_hue + (saturation % 45), std::max((uint8_t)64, (uint8_t)saturation), brightness), surfaceCoords.y); + }); +} + +void +Cloudy::handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::SetColor) { + CHSV next = rgb2hsv_approximate(evt.asRGB()); + m_hue.set(next.h); + } +} + +STATIC_ALLOC(Cloudy); +STATIC_TASK(Cloudy); diff --git a/src/animations/Cloudy.h b/src/animations/Cloudy.h new file mode 100644 index 0000000..aa89685 --- /dev/null +++ b/src/animations/Cloudy.h @@ -0,0 +1,13 @@ +#pragma once +#include + +class Cloudy: public Figment { + private: + uint16_t m_noiseOffset; + AnimatedNumber m_hue; + public: + Cloudy(); + void render(Display* dpy) const override; + void loop() override; + void handleEvent(const InputEvent& evt) override; +}; diff --git a/src/animations/Rain.cpp b/src/animations/Rain.cpp index 42a4f2a..becd61e 100644 --- a/src/animations/Rain.cpp +++ b/src/animations/Rain.cpp @@ -8,11 +8,13 @@ RainAnimation::RainAnimation() : Figment("Rain") void RainAnimation::render(Display* dpy) const { - Surface sfc = Surface(dpy, {0, 0}, {255, 255}); + Surface ground = Surface(dpy, {0, 64}, {255, 255}); uint8_t noiseY = sin8(m_noiseOffset % 255); uint8_t noiseX = cos8(m_noiseOffset % 255); - sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) { - pixel = CHSV(m_hue, inoise8(noiseX + coords.x, coords.y), inoise8(m_noiseOffset + coords.x, noiseY + coords.y)); + ground.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) { + uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y); + uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x); + nblend(pixel, CHSV(m_hue, saturation, brightness), 255 - surfaceCoords.y); }); m_drops.render(dpy); } @@ -20,7 +22,7 @@ RainAnimation::render(Display* dpy) const void RainAnimation::loop() { - EVERY_N_MILLISECONDS(250) { + EVERY_N_MILLISECONDS(120) { m_drops.update(); } EVERY_N_MILLISECONDS(60) { diff --git a/src/animations/Rain.h b/src/animations/Rain.h index 7debb58..09a2c68 100644 --- a/src/animations/Rain.h +++ b/src/animations/Rain.h @@ -4,7 +4,7 @@ class RainAnimation: public Figment { private: struct Raindrop { - int size = 20; + int size = 10; int x = random(255); int y = random(255); CHSV fg{180, 255, 255}; diff --git a/src/animations/SolidAnimation.cpp b/src/animations/SolidAnimation.cpp index c55850f..05967ec 100644 --- a/src/animations/SolidAnimation.cpp +++ b/src/animations/SolidAnimation.cpp @@ -47,22 +47,20 @@ void SolidAnimation::loop() { }); m_blobs.update(); } + m_noiseOffset += 1; } void SolidAnimation::render(Display* dpy) const { - CRGB color(m_red.value(), m_green.value(), m_blue.value()); - CRGB scaledPrev = m_prevColor; - scaledPrev = color.nscale8(30); - uint8_t frame = ease8InOutApprox(m_changePct); - if (F_LIKELY(frame == 255)) { - Surface(dpy, {0, 0}, {255, 255}) = color.nscale8(10); - } else { - uint8_t cutoff = (frame / 2); - uint8_t rotation = m_horizontal ? 0 : 128; - Surface(dpy, {0, 0}, {128 - cutoff, 255}, rotation) = scaledPrev; - Surface(dpy, {128 - cutoff, 0}, {128 + cutoff, 255}, rotation) = scaledPrev; - Surface(dpy, {128 + cutoff, 0}, {255, 255}, rotation) = scaledPrev; - } + Surface sfc = Surface(dpy, {0, 0}, {255, 255}); + uint8_t noiseY = sin8(m_noiseOffset % 255); + uint8_t noiseX = cos8(m_noiseOffset % 255); + CHSV color(rgb2hsv_approximate(CRGB(m_red, m_green, m_blue))); + + sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) { + uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y); + uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x); + nblend(pixel, CHSV(color.h, std::max((uint8_t)128, saturation), scale8(color.v, brightness)), 128); + }); m_blobs.render(dpy); } STATIC_ALLOC(SolidAnimation); diff --git a/src/animations/SolidAnimation.h b/src/animations/SolidAnimation.h index 24a9c2f..cf63046 100644 --- a/src/animations/SolidAnimation.h +++ b/src/animations/SolidAnimation.h @@ -5,6 +5,7 @@ class SolidAnimation: public Figment { private: AnimatedNumber m_red, m_green, m_blue, m_changePct; + uint16_t m_noiseOffset; CRGB m_curColor; CRGB m_prevColor; static constexpr int blobCount = 20; diff --git a/src/animations/TestAnimation.cpp b/src/animations/TestAnimation.cpp index 64d3192..7e6a38b 100644 --- a/src/animations/TestAnimation.cpp +++ b/src/animations/TestAnimation.cpp @@ -5,27 +5,106 @@ void TestAnimation::loop() { - m_x += 1; - m_y += 1; + m_x += 1; + m_y += 1; + m_hue += 1; +} + +void +TestAnimation::handleEvent(const InputEvent& evt) +{ + if (evt.intent == InputEvent::SetColor) { + m_color = evt.asRGB(); + } } void TestAnimation::render(Display* dpy) const { - for(unsigned int i = 0; i < dpy->pixelCount(); i++) { - dpy->pixelAt(i) = CRGB{255, 255, 255}; + CRGB drawColor = blend(m_color, CHSV(m_hue, 255, 255), 128); + if (m_mode == Raw) { + for(unsigned int i = 0; i < dpy->pixelCount(); i++) { + dpy->pixelAt(i) = drawColor; + } + } else if (m_mode == Top) { + Surface{dpy, {0, 0}, {255, 0}} = drawColor; + } else if (m_mode == Left) { + Surface{dpy, {0, 0}, {0, 255}} = drawColor; + } else if (m_mode == Right) { + Surface{dpy, {255, 0}, {255, 255}} = drawColor; + } else if (m_mode == Bottom) { + Surface{dpy, {0, 255}, {255, 255}} = drawColor; + } else if (m_mode == Outline) { + // Draw red line on top row + Surface{dpy, {0, 0}, {255, 0}} = CRGB{255, 0, 0}; + // Green line on first column + Surface{dpy, {0, 0}, {0, 255}} = CRGB{0, 255, 0}; + } else if (m_mode == White) { + Surface{dpy, {0, 0}, {255, 255}} = CRGB{255, 255, 255}; + } else if (m_mode == Red) { + Surface{dpy, {0, 0}, {255, 255}} = CRGB{255, 0, 0}; + } else if (m_mode == Green) { + Surface{dpy, {0, 0}, {255, 255}} = CRGB{0, 255, 0}; + } else if (m_mode == Blue) { + Surface{dpy, {0, 0}, {255, 255}} = CRGB{0, 0, 255}; + } else if (m_mode == HSV) { + Surface{dpy, {0, 0}, {255, 255}}.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates&, const VirtualCoordinates& surfaceCoords) { + pixel = CHSV(coords.x, coords.y, 255); + }); + } else if (m_mode == RGB) { + Surface{dpy, {0, 0}, {255, 255}}.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates&, const VirtualCoordinates& surfaceCoords) { + pixel = CRGB(coords.x, coords.y, (coords.x/2) + (coords.y/2)); + }); } return; - // Blank the canvas to white - Surface{dpy, {0, 0}, {255, 255}} = CRGB{255, 255, 255}; - // Draw red line on top row - Surface{dpy, {0, 0}, {255, 0}} = CRGB{255, 0, 0}; - // Green line on first column - Surface{dpy, {0, 0}, {0, 255}} = CRGB{0, 255, 0}; +} - //Surface{dpy, {m_x, 0}, {m_x, 255}} = CRGB{255, 0, 0}; - ///Surface{dpy, {0, m_y}, {255, m_y}} = CRGB{255, 0, 0}; - //dpy->pixelAt(VirtualCoordinates{m_x, m_y}) = CRGB{255, 0, 255}; +void +TestAnimation::doMode(Args& args, Print& out) +{ + String s = args[1]; + if (s == "") { + out.println("Available modes: raw outline white red green blue hsv rgb none top left right bottom"); + out.print("Current mode: "); + out.println((int)m_mode); + } else if (s == "raw") { + m_mode = Raw; + } else if (s == "outline") { + m_mode = Outline; + } else if (s == "white") { + m_mode = White; + } else if (s == "red") { + m_mode = Red; + } else if (s == "green") { + m_mode = Green; + } else if (s == "blue") { + m_mode = Blue; + } else if (s == "hsv") { + m_mode = HSV; + } else if (s == "rgb") { + m_mode = RGB; + } else if (s == "top") { + m_mode = Top; + } else if (s == "left") { + m_mode = Left; + } else if (s == "right") { + m_mode = Right; + } else if (s == "bottom") { + m_mode = Bottom; + } else if (s == "none") { + m_mode = None; + } else { + out.println("Unknown test mode"); + } +} + +const std::vector& +TestAnimation::commands() const +{ + static const std::vector _commands{ + {"test", &TestAnimation::doMode} + }; + return _commands; } STATIC_ALLOC(TestAnimation); diff --git a/src/animations/TestAnimation.h b/src/animations/TestAnimation.h index 1bc47eb..ea192fb 100644 --- a/src/animations/TestAnimation.h +++ b/src/animations/TestAnimation.h @@ -5,8 +5,31 @@ public: TestAnimation() : Figment("Test") {} void loop() override; void render(Display* dpy) const override; + void handleEvent(const InputEvent& evt) override; + const std::vector& commands() const override; private: uint8_t m_x; uint8_t m_y; + CRGB m_color; + uint8_t m_hue; + + enum Mode { + None, + Raw, + Outline, + White, + Red, + Green, + Blue, + HSV, + RGB, + Top, + Left, + Right, + Bottom + }; + + Mode m_mode = None; + void doMode(Args&, Print&); }; diff --git a/src/animations/Thinking.cpp b/src/animations/Thinking.cpp new file mode 100644 index 0000000..69fd345 --- /dev/null +++ b/src/animations/Thinking.cpp @@ -0,0 +1,26 @@ +#include +#include "../Static.h" + +class Thinking: public Figment { + public: + Thinking() : Figment("Thinking") {} + void render(Display* dpy) const override { + Surface sfc = Surface(dpy, {0, 0}, {255, 255}); + uint8_t noiseY = sin8(m_noiseOffset % 255); + uint8_t noiseX = cos8(m_noiseOffset % 255); + sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) { + pixel = CHSV(inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y), 255, inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x)); + }); + } + + void loop() override { + EVERY_N_MILLISECONDS(15) { + m_noiseOffset += 1; + } + } + private: + int m_noiseOffset; +}; + +STATIC_ALLOC(Thinking); +STATIC_TASK(Thinking); diff --git a/src/inputs/Serial.cpp b/src/inputs/Serial.cpp index c8ee2c1..519e946 100644 --- a/src/inputs/Serial.cpp +++ b/src/inputs/Serial.cpp @@ -131,9 +131,16 @@ SerialInput::doHelp(Args& args, Print& out) out.println("Available commands:"); auto sched = MainLoop::instance()->scheduler; for(auto task : sched.tasks) { - for(auto &command : task->commands()) { - out.print(command.name); - out.print(" "); + const auto taskCommands = task->commands(); + if (!taskCommands.empty()) { + out.print(task->name); + out.println(":"); + out.print("\t"); + for(auto &command : task->commands()) { + out.print(command.name); + out.print(" "); + } + out.println(); } } out.println(); diff --git a/src/inputs/Weather.cpp b/src/inputs/Weather.cpp new file mode 100644 index 0000000..eb12ae8 --- /dev/null +++ b/src/inputs/Weather.cpp @@ -0,0 +1,147 @@ +#include "Weather.h" +#include "../Static.h" + +Weather::Weather() + : BufferedInputSource("Weather") +{ + m_parser.setListener(this); +} + +void +Weather::handleEvent(const InputEvent& evt) +{ + BufferedInputSource::handleEvent(evt); + OnlineTaskMixin::handleEvent(evt); + if (evt.intent == InputEvent::IdleStart) { + doWeatherEvent(); + } +} + +void +Weather::loop() +{ + BufferedInputSource::loop(); + OnlineTaskMixin::loop(); + EVERY_N_SECONDS(3600) { + update(); + } + if (m_fetching) { + parse(); + } +} + +void +Weather::update() +{ + if (!m_fetching) { + Log.info("Connecting to weather service"); + m_parser.reset(); + const String host = "api.openweathermap.org"; + const uint8_t port = 80; +#ifdef ESP32 + m_fetching = m_client.connect(host.c_str(), port); +#else + m_fetching = m_client.connect(host, port); +#endif + if (m_fetching) { + Log.info("Ok!"); + m_foundBody = false; + m_client.print( + "GET /data/2.5/weather?id=2950159&appid=f7aea43b54bcd27f542195a809c1463f&units=metric&lang=en HTTP/1.1\r\n" + "Host: api.openweathermap.org\r\n" + "Connection: close\r\n\r\n" + ); + } else { + Log.info("No :("); + } + } +} + +void +Weather::parse() +{ + if (m_client.available()) { + char c = m_client.read(); + if (c == '{') { + m_foundBody = true; + } + if (m_foundBody) { + m_parser.parse(c); + } + } else if (!m_client.connected()) { + Log.info("Disconnected"); + m_client.stop(); + m_fetching = false; + doWeatherEvent(); + } +} + +void +Weather::key(String key) +{ + m_curKey = key; +} + +void +Weather::doWeatherEvent() +{ + Log.info("Dispatching weather event for %d", m_weatherId); + if (m_weatherId >= 200 && m_weatherId < 300 ) { + setEvent(InputEvent::SetScene, "Thunderstorm"); + setEvent(InputEvent::SetColor, CRGB(255, 187, 0)); + } else if (m_weatherId >= 300 && m_weatherId < 400) { + setEvent(InputEvent::SetScene, "Drizzle"); + setEvent(InputEvent::SetColor, CRGB(93, 183, 201)); + } else if (m_weatherId >= 500 && m_weatherId < 600) { + setEvent(InputEvent::SetScene, "Rain"); + setEvent(InputEvent::SetColor, CRGB(64, 64, 255)); + } else if (m_weatherId >= 600 && m_weatherId < 700) { + setEvent(InputEvent::SetScene, "Snow"); + setEvent(InputEvent::SetColor, CRGB(255, 255, 255)); + } else if (m_weatherId == 701) { + setEvent(InputEvent::SetScene, "Mist"); + setEvent(InputEvent::SetColor, CRGB(77, 255, 124)); + } else if (m_weatherId == 800) { + setEvent(InputEvent::SetScene, "Clear"); + setEvent(InputEvent::SetColor, CRGB(255, 249, 79)); + } else if (m_weatherId >= 801 && m_weatherId <= 804) { + setEvent(InputEvent::SetScene, "Cloudy"); + setEvent(InputEvent::SetColor, CRGB(102, 110, 110)); + } else { + setEvent(InputEvent::SetScene, "UnknownWeather"); + setEvent(InputEvent::SetColor, CRGB(255, 0, 0)); + } +} + +void +Weather::value(String value) +{ + if (m_curField == "weather") { + if (m_curKey == "id") { + m_weatherId = atoi(value.c_str()); + } else if (m_curKey == "main") { + Log.info("WEATHER ENCOUNTERED: %s", value.c_str()); + } + } +} + +void +Weather::startObject() +{ + m_curField = m_curKey; +} + +void +Weather::endObject() +{ + m_curField = ""; +} + +void Weather::endArray() {} +void Weather::startArray() {} +void Weather::endDocument() {} +void Weather::whitespace(char c) {} +void Weather::startDocument() {} + +STATIC_ALLOC(Weather); +STATIC_TASK(Weather); diff --git a/src/inputs/Weather.h b/src/inputs/Weather.h new file mode 100644 index 0000000..c221bf1 --- /dev/null +++ b/src/inputs/Weather.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + +class Weather : public BufferedInputSource, OnlineTaskMixin, JsonListener { + public: + Weather(); + + void handleEvent(const InputEvent& evt) override; + void loop() override; + + void onOnline() override { + update(); + } + + void onStart() override { + update(); + } + + void loopOnline() override { + } + + void update(); + void parse(); + + virtual void whitespace(char c); + virtual void startDocument(); + virtual void key(String key); + virtual void value(String value); + virtual void endArray(); + virtual void endObject(); + virtual void endDocument(); + virtual void startArray(); + virtual void startObject(); + + private: + JsonStreamingParser m_parser; + WiFiClient m_client; + bool m_fetching = false; + String m_curKey; + String m_curField; + bool m_foundBody = false; + + int m_weatherId = 0; + + void doWeatherEvent(); +}; + diff --git a/src/main.cpp b/src/main.cpp index 60c981b..56ca61b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,7 +44,7 @@ ColorSequenceInput<9> idleCycle{{ CRGB(0, 255, 255), // Cyan }, "IdleColors"}; -REGISTER_TASK(idleCycle); +//REGISTER_TASK(idleCycle); ColorSequenceInput<7> rainbowCycle{{ CRGB(255, 0, 0), // Red @@ -54,7 +54,7 @@ ColorSequenceInput<7> rainbowCycle{{ CRGB(128, 0, 128), // Purple }, "Rainbow"}; -REGISTER_TASK(rainbowCycle); +//REGISTER_TASK(rainbowCycle); MainLoop* runner = &SafeMode::safeModeApp; diff --git a/src/platform/arduino/esp8266/Platform.cpp b/src/platform/arduino/esp8266/Platform.cpp index 72a437d..35b7743 100644 --- a/src/platform/arduino/esp8266/Platform.cpp +++ b/src/platform/arduino/esp8266/Platform.cpp @@ -9,6 +9,8 @@ __NOINIT_ATTR static uint8_t s_rebootCount; __NOINIT_ATTR static uint16_t s_forceSafeMode; #define SAFE_MODE_MAGIC 6942 +__NOINIT_ATTR static struct tm s_lastGoodTime; + WiFiUDP wifiUdp; NTPClient timeClient(wifiUdp, "pool.ntp.org", 0); @@ -44,8 +46,12 @@ template<> bool PlatformImpl::getLocalTime(struct tm* timedata, int timezone) { - timedata->tm_hour = (timeClient.getHours() + timezone) % 23; - timedata->tm_min = timeClient.getMinutes(); + if (timeClient.isTimeSet()) { + s_lastGoodTime.tm_min = (timeClient.getMinutes() + timezone) % 23; + s_lastGoodTime.tm_hour = timeClient.getHours(); + s_lastGoodTime.tm_sec = timeClient.getSeconds(); + } + memcpy(timedata, &s_lastGoodTime, sizeof(s_lastGoodTime)); return true; } @@ -54,6 +60,9 @@ void PlatformImpl::startNTP() { timeClient.begin(); + if (s_lastGoodTime.tm_hour >= 24 || s_lastGoodTime.tm_min >= 60 || s_lastGoodTime.tm_sec >= 60) { + memset(&s_lastGoodTime, 0, sizeof(s_lastGoodTime)); + } } template<> diff --git a/src/sprites/Blob.h b/src/sprites/Blob.h index 2c7eb7b..f545317 100644 --- a/src/sprites/Blob.h +++ b/src/sprites/Blob.h @@ -64,9 +64,7 @@ public: 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); + nblend(pixel, CRGB(blobColor), 80); }); } };