diff --git a/firmware/Figments/Input.cpp b/firmware/Figments/Input.cpp index aac2cd4..b4f6d51 100644 --- a/firmware/Figments/Input.cpp +++ b/firmware/Figments/Input.cpp @@ -29,19 +29,19 @@ InputSource::loop() InputEvent BufferedInputSource::read() { - InputEvent ret = m_lastEvent; - m_lastEvent = InputEvent{}; + InputEvent ret; + m_eventQueue.take(ret); return ret; } void BufferedInputSource::setEvent(InputEvent &&evt) { - m_lastEvent = std::move(evt); + m_eventQueue.insert(std::move(evt)); } void BufferedInputSource::setEvent(InputEvent::Intent intent, Variant &&v) { - m_lastEvent = InputEvent{intent, std::move(v)}; + m_eventQueue.insert(InputEvent{intent, std::move(v)}); } diff --git a/firmware/Figments/Input.h b/firmware/Figments/Input.h index 4ae1972..b1ec111 100644 --- a/firmware/Figments/Input.h +++ b/firmware/Figments/Input.h @@ -2,6 +2,7 @@ #include "application.h" #include "./Geometry.h" #include "./Figment.h" +#include "./Ringbuf.h" #include "FastLED/FastLED.h" typedef Vector3d MotionVec; @@ -136,7 +137,7 @@ protected: void setEvent(InputEvent::Intent intent, Variant &&v); private: - InputEvent m_lastEvent; + Ringbuf m_eventQueue; }; class InputMapper: public BufferedInputSource { diff --git a/firmware/Figments/MainLoop.h b/firmware/Figments/MainLoop.h index 354bb10..208664b 100644 --- a/firmware/Figments/MainLoop.h +++ b/firmware/Figments/MainLoop.h @@ -3,6 +3,7 @@ #include #include #include "./Input.h" +#include "./Ringbuf.h" class Task; class InputSource; @@ -50,42 +51,6 @@ struct Scheduler { iterator end() { return iterator(*this, tasks.size()); } }; -template -struct Ringbuf { - Ringbuf() : m_head(0), m_tail(0) {} - - void clear() { - m_head = 0; - m_tail = 0; - } - - bool take(T& dest) { - if (m_head == m_tail) { - return false; - } - const int cur = m_head; - const int nextHead = (m_head + 1) % Size; - m_head = nextHead; - dest = m_items[cur]; - return true; - } - - void insert(const T& src) { - const int cur = m_tail; - const int nextTail = (m_tail + 1) % Size; - if (nextTail == m_head) { - return; - } else { - m_tail = nextTail; - } - m_items[cur] = src; - } -private: - int m_head = 0; - int m_tail = 0; - std::array m_items; -}; - struct MainLoop { Scheduler scheduler; diff --git a/firmware/Figments/Ringbuf.h b/firmware/Figments/Ringbuf.h new file mode 100644 index 0000000..5807ca5 --- /dev/null +++ b/firmware/Figments/Ringbuf.h @@ -0,0 +1,39 @@ +#pragma once +#include + +template +struct Ringbuf { + Ringbuf() : m_head(0), m_tail(0) {} + + void clear() { + m_head = 0; + m_tail = 0; + } + + bool take(T& dest) { + if (m_head == m_tail) { + return false; + } + const int cur = m_head; + const int nextHead = (m_head + 1) % Size; + m_head = nextHead; + dest = m_items[cur]; + return true; + } + + void insert(const T& src) { + const int cur = m_tail; + const int nextTail = (m_tail + 1) % Size; + if (nextTail == m_head) { + return; + } else { + m_tail = nextTail; + } + m_items[cur] = src; + } +private: + int m_head = 0; + int m_tail = 0; + std::array m_items; +}; + diff --git a/firmware/Sequencer.cpp b/firmware/Sequencer.cpp index 3590c6a..a42171c 100644 --- a/firmware/Sequencer.cpp +++ b/firmware/Sequencer.cpp @@ -36,7 +36,12 @@ Sequencer::handleEvent(const InputEvent& evt) } else if (evt.intent == InputEvent::PreviousPattern) { m_idx--; } else { - m_idx = evt.asInt(); + //m_idx = evt.asInt(); + for(m_idx = 0; m_idx < m_scenes.size(); m_idx++) { + if (!strcmp(evt.asString(), m_scenes[m_idx].name)) { + break; + } + } } if (m_idx < 0) { diff --git a/firmware/inputs/ColorCycle.h b/firmware/inputs/ColorCycle.h index 44a8a24..baea357 100644 --- a/firmware/inputs/ColorCycle.h +++ b/firmware/inputs/ColorCycle.h @@ -1,9 +1,11 @@ #include "../Figments/Figments.h" +using CRGB = NSFastLED::CRGB; + template class ColorSequenceInput: public InputSource { public: - ColorSequenceInput(const std::vector &colors, const char* name, Task::State initialState) + ColorSequenceInput(const std::vector &colors, const char* name, Task::State initialState) : InputSource(name, initialState), m_colors(colors) {} InputEvent read() override { @@ -32,7 +34,7 @@ public: } private: - std::vector m_colors; + std::vector m_colors; int m_idx = 0; bool m_reset = true; bool m_override = false; diff --git a/firmware/main.cpp b/firmware/main.cpp index c83b0cd..4bddb56 100644 --- a/firmware/main.cpp +++ b/firmware/main.cpp @@ -76,7 +76,7 @@ FigmentFunc configDisplay([](Display* dpy) { class InputBlip: public Figment { public: - InputBlip() : Figment("InputBlip") {} + InputBlip() : Figment("InputBlip", Task::Stopped) {} void handleEvent(const InputEvent& evt) override { if (evt.intent != InputEvent::None) { @@ -139,13 +139,13 @@ DrainAnimation drain{Task::Stopped}; Flashlight flashlight{Task::Stopped}; Sequencer sequencer{{ - {"Solid", {"Solid"}}, - {"Drain", {"Drain"}}, + {"Idle", {"Solid", "MPU5060", "Pulse", "Hackerbots", "Kieryn", "CircadianRhythm"}}, + {"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}}, + {"Interactive", {"Drain", "CircadianRhythm"}}, {"Flashlight", {"Flashlight"}}, - {"Fiercewater", {"Solid", "Kieryn", "Hackerbots"}}, - {"Nightlight", {"Drain", "Noisebridge"}}, - {"Gay", {"Solid", "Rainbow", "Hackerbots", "Kieryn"}}, - {"Acid", {"Chimes", "Hackerbots", "Rainbow"}}, + {"Nightlight", {"Drain", "Pulse", "Noisebridge"}}, + {"Gay", {"Solid", "Pulse", "Rainbow", "Hackerbots", "Kieryn"}}, + {"Acid", {"Chimes", "Pulse", "MPU5060", "Hackerbots", "Rainbow"}}, }}; @@ -176,7 +176,7 @@ class OnlineTaskMixin : public virtual Loopable { public: void handleEvent(const InputEvent &evt) override { if (evt.intent == InputEvent::NetworkStatus) { - m_online = true; + m_online = evt.asInt(); } if (m_online) { handleEventOnline(evt); @@ -199,13 +199,17 @@ class OnlineTaskMixin : public virtual Loopable { #include "MQTT/MQTT.h" -class MQTTTelemetry : public Task, OnlineTaskMixin { +class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { public: - MQTTTelemetry() : Task("MQTT"), m_client("relay.malloc.hackerbots.net", 1883, 512, 15, MQTTTelemetry::s_callback, true) { - s_instance = this; + MQTTTelemetry() : BufferedInputSource("MQTT"), m_client("relay.malloc.hackerbots.net", 1883, 512, 15, MQTTTelemetry::s_callback, true) { strcpy(m_deviceName, System.deviceID().c_str()); } + void loop() override { + BufferedInputSource::loop(); + OnlineTaskMixin::loop(); + } + void handleEventOnline(const InputEvent& event) override { char response[255]; if (event.intent == InputEvent::SetPower) { @@ -234,20 +238,23 @@ class MQTTTelemetry : public Task, OnlineTaskMixin { writer.endObject(); writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; m_client.publish(m_statTopic, response, MQTT::QOS1); - } else { - /*root["intent"] = event.intent; - switch(event.type) { - case InputEvent::Null: - root["value"] = 0;break; - case InputEvent::Integer: - root["value"] = event.asInt();break; - case InputEvent::String: - root["value"] = event.asString();break; - case InputEvent::Color: - root["value"] = "RGB";break; + } else if (event.intent == InputEvent::SetPattern) { + JSONBufferWriter writer(response, sizeof(response)); + writer.beginObject(); + writer.name("effect").value(event.asString()); + writer.endObject(); + writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; + m_client.publish(m_statTopic, response, MQTT::QOS1); + } else { + if (m_lastIntent != event.intent) { + m_lastIntent = event.intent; + JSONBufferWriter writer(response, sizeof(response)); + writer.beginObject(); + writer.name("intent").value(event.intent); + writer.endObject(); + writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; + m_client.publish("renderbug/events", response, MQTT::QOS1); } - //root.printTo(response, sizeof(response)); - //m_client.publish("renderbug/event", response);*/ } } @@ -258,7 +265,7 @@ class MQTTTelemetry : public Task, OnlineTaskMixin { char heartbeatBuf[255]; JSONBufferWriter writer(heartbeatBuf, sizeof(heartbeatBuf)); writer.beginObject(); - writer.name("fps").value(NSFastLED::FastLED.getFPS()); + writer.name("fps").value(FastLED.getFPS()); writer.name("os_version").value(System.version()); writer.name("free_ram").value((unsigned int)System.freeMemory()); //writer.name("uptime").value(System.uptime()); @@ -329,13 +336,13 @@ class MQTTTelemetry : public Task, OnlineTaskMixin { void callback(char* topic, byte* payload, unsigned int length) { MainLoop::instance()->dispatch(InputEvent::NetworkActivity); if (!strcmp(topic, m_cmdTopic)) { - JSONValue cmd = JSONValue::parse((char*)payload, length); + JSONValue cmd = JSONValue::parseCopy((char*)payload, length); JSONObjectIterator cmdIter(cmd); while(cmdIter.next()) { if (cmdIter.name() == "state" && cmdIter.value().toString() == "ON") { - MainLoop::instance()->dispatch({InputEvent::SetPower, 1}); + setEvent({InputEvent::SetPower, 1}); } else if (cmdIter.name() == "state" && cmdIter.value().toString() == "OFF") { - MainLoop::instance()->dispatch({InputEvent::SetPower, 0}); + setEvent({InputEvent::SetPower, 0}); } if (cmdIter.name() == "color") { @@ -350,31 +357,36 @@ class MQTTTelemetry : public Task, OnlineTaskMixin { b = colorIter.value().toInt(); } } - MainLoop::instance()->dispatch(InputEvent{InputEvent::SetColor, CRGB{r, g, b}}); + setEvent(InputEvent{InputEvent::SetColor, CRGB{r, g, b}}); } if (cmdIter.name() == "brightness") { uint8_t brightness = cmdIter.value().toInt(); - MainLoop::instance()->dispatch(InputEvent{InputEvent::SetBrightness, brightness}); + setEvent(InputEvent{InputEvent::SetBrightness, brightness}); + } + + if (cmdIter.name() == "effect") { + strcpy(m_patternBuf, (const char*) cmdIter.value().toString()); + setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf}); } } } } static void s_callback(char* topic, byte* payload, unsigned int length) { - s_instance->callback(topic, payload, length); + Static::instance()->callback(topic, payload, length); }; - static MQTTTelemetry* s_instance; + MQTT m_client; + InputEvent::Intent m_lastIntent; char m_deviceName[100]; char m_statTopic[100]; char m_attrTopic[100]; char m_cmdTopic[100]; + char m_patternBuf[48]; }; -MQTTTelemetry mqttTelemetry; - -MQTTTelemetry* MQTTTelemetry::s_instance = 0; +STATIC_ALLOC(MQTTTelemetry); WebTelemetry webTelemetry(sequencer); @@ -386,7 +398,7 @@ ColorSequenceInput<13> kierynCycle{{ colorForName("Electric Purple").rgb, colorForName("Emerald").rgb, colorForName("Sky Magenta").rgb -}, "Kieryn", Task::Running}; +}, "Kieryn", Task::Stopped}; ColorSequenceInput<7> rainbowCycle{{ colorForName("Red").rgb, @@ -402,7 +414,7 @@ ColorSequenceInput<7> hackerbotsCycle{{ colorForName("Purple").rgb, colorForName("White").rgb, colorForName("Cyan").rgb, -}, "Hackerbots", Task::Running}; +}, "Hackerbots", Task::Stopped}; struct ConfigInputTask: public BufferedInputSource { public: @@ -540,8 +552,11 @@ uint8_t brightnessForTime(uint8_t hour, uint8_t minute) { InputFunc circadianRhythm([]() { static Phase lastPhase = Null; static bool needsUpdate = true; - EVERY_N_SECONDS(60) { - if (Time.isValid()) { + if (Time.isValid()) { + EVERY_N_SECONDS(60) { + needsUpdate = true; + } + if (needsUpdate) { return InputEvent{InputEvent::SetBrightness, brightnessForTime(Time.hour(), Time.minute())}; } } @@ -561,15 +576,11 @@ MainLoop configApp{{ // Read hardware inputs new Buttons(), - &randomPulse, - //new MPU5060(), // Map input buttons to configuration commands new ConfigInputTask(), // Fill the entire display with a color, to see size - &rainbowCycle, - &drain, &configDisplay, // Render some basic input feedback &inputBlip, @@ -638,7 +649,7 @@ MainLoop renderbugApp{{ &webTelemetry, // MQTT telemetry - &mqttTelemetry, + Static::instance(), // Network discovery &mdnsService, @@ -647,15 +658,24 @@ MainLoop renderbugApp{{ MainLoop &runner = renderbugApp; +retained bool LAST_BOOT_WAS_FLASH; +retained bool LAST_BOOT_WAS_SERIAL; + struct BootOptions { - BootOptions() { + static void initPins() { pinMode(2, INPUT_PULLDOWN); pinMode(3, INPUT_PULLDOWN); pinMode(4, INPUT_PULLDOWN); + } + + BootOptions() { isSetup = digitalRead(2) == HIGH; - isSerial = digitalRead(3) == HIGH; + isSerial = digitalRead(3) == HIGH || LAST_BOOT_WAS_SERIAL; isFlash = digitalRead(4) == HIGH; + LAST_BOOT_WAS_FLASH = isFlash; + LAST_BOOT_WAS_SERIAL |= isSerial; + configStatus.setActive(isSetup); serialStatus.setActive(isSerial); } @@ -672,16 +692,27 @@ struct BootOptions { LEDStatus configStatus = LEDStatus(RGB_COLOR_YELLOW, LED_PATTERN_FADE, LED_SPEED_NORMAL, LED_PRIORITY_IMPORTANT); }; -BootOptions bootopts; +STARTUP(BootOptions::initPins()); + +retained BootOptions bootopts; ApplicationWatchdog *wd; void watchdogHandler() { - System.enterSafeMode(); + for(int i = 0; i < 8; i++) { + leds[i] = CRGB(i % 3 ? 35 : 255, 0, 0); + } + FastLED.show(); + if (LAST_BOOT_WAS_FLASH) { + System.dfu(); + } else { + System.enterSafeMode(); + } } // Tune in, void setup() { + System.enableFeature(FEATURE_RETAINED_MEMORY); wd = new ApplicationWatchdog(5000, watchdogHandler, 1536); Serial.begin(115200); if (bootopts.isFlash) { diff --git a/firmware/sprites/Blob.h b/firmware/sprites/Blob.h index 7e124af..ab425a5 100644 --- a/firmware/sprites/Blob.h +++ b/firmware/sprites/Blob.h @@ -1,5 +1,7 @@ #pragma once +using namespace NSFastLED; + class Blob { uint16_t m_pos; int8_t m_velocity; @@ -56,12 +58,12 @@ public: for(uint8_t i = 0;i < scaledWidth; i++) { // Blobs desaturate towards their tail - NSFastLED::CHSV blobColor(m_hue, m_saturation, NSFastLED::quadwave8((i / (double)scaledWidth) * m_brightness)); + CHSV blobColor(m_hue, m_saturation, quadwave8((i / (double)scaledWidth) * m_brightness)); PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), 0}; - NSFastLED::CRGB src(display->pixelAt(pos)); - display->pixelAt(pos) = NSFastLED::blend(NSFastLED::CRGB(blobColor), src, 200); + CRGB src(display->pixelAt(pos)); + display->pixelAt(pos) = blend(CRGB(blobColor), src, 200); } } }; diff --git a/firmware/sprites/Chime.h b/firmware/sprites/Chime.h index 031b6f9..b9b4dc5 100644 --- a/firmware/sprites/Chime.h +++ b/firmware/sprites/Chime.h @@ -67,7 +67,7 @@ public: dpy->pixelAt(i + m_offset) = CHSV(0, 0, 0); } else { uint8_t distance = m_pos - i; - uint8_t brightness = NSFastLED::scale8(NSFastLED::quadwave8((ChimeLength / (double)distance) * 255), m_brightness); + uint8_t brightness = scale8(quadwave8((ChimeLength / (double)distance) * 255), m_brightness); if (brightness <= 0.2) brightness = 0; dpy->pixelAt(VirtualCoordinates{i + m_offset, 0}) = CHSV(m_hue, min(m_saturation, brightness), brightness);