diff --git a/.atom-build.yml b/.atom-build.yml deleted file mode 100644 index 7946069..0000000 --- a/.atom-build.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -cmd: po -args: - - photon - - build -sh: false - -targets: - Build: - atomCommandName: po:Build Particle Firmware Locally - sh: false - args: - - photon - - build - cmd: po - keymap: ctrl-alt-1 - name: Build - Flash: - atomCommandName: po:Flash Particle Firmware Locally - sh: false - args: - - photon - - flash - cmd: po - keymap: ctrl-alt-2 - name: Flash - Clean: - atomCommandName: po:Clean Particle Firmware Locally - sh: false - args: - - photon - - clean - cmd: po - keymap: ctrl-alt-3 - name: Clean - DFU: - atomCommandName: po:Upload Particle Firmware Locally with DFU - sh: false - args: - - photon - - dfu - cmd: po - keymap: ctrl-alt-4 - name: DFU - OTA: - atomCommandName: po:Upload Particle Firmware Locally with OTA - sh: false - args: - - photon - - ota - - --multi - cmd: po - keymap: ctrl-alt-5 - name: OTA diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b847b87..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -dist: trusty -sudo: required -language: generic - -script: - - ci/travis.sh - -cache: - directories: - - $HOME/.po-util diff --git a/ci/travis.sh b/ci/travis.sh deleted file mode 100755 index 4d6fc50..0000000 --- a/ci/travis.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -bash <(curl -sL https://raw.githubusercontent.com/nrobinson2000/po/master/ci/ci-install) -po lib clean . -f &> /dev/null -yes "no" | po lib setup # change to "yes" to prefer libraries from GitHub -po photon build diff --git a/firmware/Config.cpp b/firmware/Config.cpp index b59c6b7..b9aee18 100644 --- a/firmware/Config.cpp +++ b/firmware/Config.cpp @@ -15,10 +15,18 @@ HardwareConfig::save() { EEPROM.put(0, dataCopy); } +LinearCoordinateMapping +HardwareConfig::toCoordMap() const +{ + auto pixelCount = min(HardwareConfig::MAX_LED_NUM, max(1, data.pixelCount)); + auto startPixel = min(pixelCount, max(1, data.startPixel)); + return LinearCoordinateMapping{pixelCount, startPixel}; +} + bool HardwareConfig::isValid() const { - return version == 1 && checksum == getCRC(); + return version == 1 && checksum == getCRC() && data.pixelCount <= MAX_LED_NUM; } uint8_t @@ -42,7 +50,7 @@ HardwareConfig::getCRC() const void ConfigService::onStart() { - Log.info("Starting configuration..."); + Log.info("Starting configuration service..."); m_config = HardwareConfig::load(); if (m_config.isValid()) { Log.info("Configuration found!"); @@ -51,8 +59,12 @@ ConfigService::onStart() m_config = HardwareConfig{}; m_config.save(); } - m_pixelCount = AnimatedNumber{m_config.data.pixelCount}; - m_startPixel = AnimatedNumber{m_config.data.startPixel}; + m_coordMap = m_config.toCoordMap(); + //m_coordMap.pixelCount = min(HardwareConfig::MAX_LED_NUM, max(1, m_config.data.pixelCount)); + //m_coordMap.startPixel = min(m_coordMap.pixelCount, max(0, m_config.data.startPixel)); + + m_pixelCount = AnimatedNumber{max(1, m_coordMap.pixelCount)}; + m_startPixel = AnimatedNumber{max(0, m_coordMap.startPixel)}; Log.info("Configured to use %d pixels, starting at %d", m_pixelCount.value(), m_startPixel.value()); Log.info("Loading task states..."); for(int i = 0; i < 32; i++) { @@ -80,17 +92,41 @@ ConfigService::onConnected() void ConfigService::loop() { - m_startPixel.update(); - m_pixelCount.update(); - m_coordMap.startPixel = m_startPixel; - m_coordMap.pixelCount = m_pixelCount; + //m_startPixel.update(); + //m_pixelCount.update(); + //m_coordMap.pixelCount = max(1, m_pixelCount.value()); + //m_coordMap.startPixel = max(0, m_startPixel.value()); } void ConfigService::handleEvent(const InputEvent &evt) { - if (evt.intent == InputEvent::NetworkStatus) { - onConnected(); + switch(evt.intent) { + case InputEvent::NetworkStatus: + onConnected(); + break; + case InputEvent::SetDisplayLength: + Log.info("Updating pixel count from %d to %d", m_coordMap.pixelCount, evt.asInt()); + m_config.data.pixelCount = evt.asInt(); + m_coordMap = m_config.toCoordMap(); + m_pixelCountInt = evt.asInt(); + //m_pixelCount = m_config.data.pixelCount; + //m_coordMap.pixelCount = evt.asInt(); + Log.info("Count is now %d", m_coordMap.pixelCount); + break; + case InputEvent::SetDisplayOffset: + Log.info("Updating pixel offset from %d to %d", m_coordMap.startPixel, evt.asInt()); + m_config.data.startPixel = evt.asInt(); + m_coordMap = m_config.toCoordMap(); + m_startPixelInt = evt.asInt(); + Log.info("Offset is now %d", m_coordMap.startPixel); + //m_coordMap.startPixel = evt.asInt(); + //m_startPixel = m_config.data.startPixel; + break; + case InputEvent::SaveConfigurationRequest: + Log.info("Saving configuration"); + m_config.save(); + break; } } diff --git a/firmware/Config.h b/firmware/Config.h index 925f759..875f200 100644 --- a/firmware/Config.h +++ b/firmware/Config.h @@ -20,6 +20,11 @@ struct HardwareConfig { void save(); bool isValid() const; + LinearCoordinateMapping toCoordMap() const; + + static constexpr uint16_t MAX_LED_NUM = 255; + static constexpr bool HAS_MPU_6050 = true; + private: uint8_t getCRC() const; diff --git a/firmware/Figments/Figment.h b/firmware/Figments/Figment.h index 65719ee..d4e5933 100644 --- a/firmware/Figments/Figment.h +++ b/firmware/Figments/Figment.h @@ -6,9 +6,12 @@ class Display; class InputEvent; class InputSource; -struct Task { +struct Loopable { virtual void handleEvent(const InputEvent& event) {} virtual void loop() = 0; +}; + +struct Task : public virtual Loopable { virtual void onStart() {}; virtual void onStop() {}; @@ -24,6 +27,7 @@ struct Task { void start() { Log.info("* Starting %s...", name); state = Running; onStart(); } void stop() { Log.info("* Stopping %s...", name); onStop(); state = Stopped; } + virtual bool isFigment() const { return false; } const char* name = 0; State state = Running; @@ -35,6 +39,7 @@ struct Figment: public Task { 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; } }; struct FigmentFunc: public Figment { diff --git a/firmware/Figments/Input.cpp b/firmware/Figments/Input.cpp index c98fed9..aac2cd4 100644 --- a/firmware/Figments/Input.cpp +++ b/firmware/Figments/Input.cpp @@ -39,3 +39,9 @@ BufferedInputSource::setEvent(InputEvent &&evt) { m_lastEvent = std::move(evt); } + +void +BufferedInputSource::setEvent(InputEvent::Intent intent, Variant &&v) +{ + m_lastEvent = InputEvent{intent, std::move(v)}; +} diff --git a/firmware/Figments/Input.h b/firmware/Figments/Input.h index 02c6bf8..4ae1972 100644 --- a/firmware/Figments/Input.h +++ b/firmware/Figments/Input.h @@ -42,20 +42,50 @@ private: struct InputEvent: public Variant { enum Intent { + // An empty non-event None, + + // An input from the user, for other tasks to translate into canonical + // types. Makes for easy button re-mapping on the fly. + UserInput, + + // + // The canonical types + // + // Hardware inputs + ButtonPress, + Acceleration, + NetworkStatus, + NetworkActivity, + + // Power management PowerToggle, SetPower, SetBrightness, + + // Animation sequencing PreviousPattern, NextPattern, SetPattern, - SetColor, - Acceleration, - FirmwareUpdate, - NetworkStatus, + PreviousScene, + NextScene, + SetScene, + + // Timekeeping + ScheduleChange, + + // Task management StartThing, StopThing, - UserInput, + + // Configuration + SetDisplayOffset, + SetDisplayLength, + SetColor, + SaveConfigurationRequest, + + // Firmware events + FirmwareUpdate, }; template @@ -103,7 +133,20 @@ public: protected: void setEvent(InputEvent &&evt); + void setEvent(InputEvent::Intent intent, Variant &&v); private: InputEvent m_lastEvent; }; + +class InputMapper: public BufferedInputSource { +public: + InputMapper(std::function f) : BufferedInputSource(), m_func(f) {} + + void handleEvent(const InputEvent& evt) override { + setEvent(m_func(evt)); + } + +private: + std::function m_func; +}; diff --git a/firmware/Figments/MainLoop.cpp b/firmware/Figments/MainLoop.cpp index 61d1cd8..3bbc8ba 100644 --- a/firmware/Figments/MainLoop.cpp +++ b/firmware/Figments/MainLoop.cpp @@ -34,7 +34,9 @@ MainLoop::loop() } } for(Task* task : scheduler) { + //Log.info("Running %s", task->name); task->loop(); + //Log.info("next"); } } diff --git a/firmware/Figments/MainLoop.h b/firmware/Figments/MainLoop.h index b6d50cd..354bb10 100644 --- a/firmware/Figments/MainLoop.h +++ b/firmware/Figments/MainLoop.h @@ -86,7 +86,6 @@ private: std::array m_items; }; - struct MainLoop { Scheduler scheduler; diff --git a/firmware/Figments/Renderer.cpp b/firmware/Figments/Renderer.cpp index 9e2dbf5..7b3c84b 100644 --- a/firmware/Figments/Renderer.cpp +++ b/firmware/Figments/Renderer.cpp @@ -7,7 +7,11 @@ Renderer::loop() for(Display* dpy : m_displays) { for(Figment* figment : m_figments) { if (figment->state == Task::Running) { + //Log.info("Rendering %s", figment->name); figment->render(dpy); + //Log.info("next"); + } else { + //Log.info("Not rendering %s", figment->name); } }; } diff --git a/firmware/MDNS/Buffer.cpp b/firmware/MDNS/Buffer.cpp new file mode 120000 index 0000000..73fccbf --- /dev/null +++ b/firmware/MDNS/Buffer.cpp @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/Buffer.cpp \ No newline at end of file diff --git a/firmware/MDNS/Buffer.h b/firmware/MDNS/Buffer.h new file mode 120000 index 0000000..1c103b5 --- /dev/null +++ b/firmware/MDNS/Buffer.h @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/Buffer.h \ No newline at end of file diff --git a/firmware/MDNS/Label.cpp b/firmware/MDNS/Label.cpp new file mode 120000 index 0000000..e3dd379 --- /dev/null +++ b/firmware/MDNS/Label.cpp @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/Label.cpp \ No newline at end of file diff --git a/firmware/MDNS/Label.h b/firmware/MDNS/Label.h new file mode 120000 index 0000000..d0cda62 --- /dev/null +++ b/firmware/MDNS/Label.h @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/Label.h \ No newline at end of file diff --git a/firmware/MDNS/MDNS b/firmware/MDNS/MDNS new file mode 120000 index 0000000..e22a966 --- /dev/null +++ b/firmware/MDNS/MDNS @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/MDNS \ No newline at end of file diff --git a/firmware/MDNS/MDNS.cpp b/firmware/MDNS/MDNS.cpp new file mode 120000 index 0000000..dd3f84d --- /dev/null +++ b/firmware/MDNS/MDNS.cpp @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/MDNS.cpp \ No newline at end of file diff --git a/firmware/MDNS/MDNS.h b/firmware/MDNS/MDNS.h new file mode 120000 index 0000000..7b792d6 --- /dev/null +++ b/firmware/MDNS/MDNS.h @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/MDNS.h \ No newline at end of file diff --git a/firmware/MDNS/Record.cpp b/firmware/MDNS/Record.cpp new file mode 120000 index 0000000..9c03fc6 --- /dev/null +++ b/firmware/MDNS/Record.cpp @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/Record.cpp \ No newline at end of file diff --git a/firmware/MDNS/Record.h b/firmware/MDNS/Record.h new file mode 120000 index 0000000..c0b0ace --- /dev/null +++ b/firmware/MDNS/Record.h @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MDNS/src/Record.h \ No newline at end of file diff --git a/firmware/MDNSService.cpp b/firmware/MDNSService.cpp new file mode 100644 index 0000000..1250f66 --- /dev/null +++ b/firmware/MDNSService.cpp @@ -0,0 +1,33 @@ +#include "MDNS/MDNS.h" +#include "Figments/Figments.h" +#include "Figments/MainLoop.h" +#include "Figments/Input.h" +#include "colors.h" + +class MDNSService : public Task { + private: + mdns::MDNS m_mdns; + bool m_online = false; + + public: + MDNSService() : Task("MDNS") { + } + + void handleEvent(const InputEvent &evt) { + if (evt.intent == InputEvent::NetworkStatus) { + m_mdns.setHostname("renderbug"); + m_mdns.addService("tcp", "http", 80, "Renderbug"); + m_mdns.begin(true); + m_online = true; + } + } + + void loop() override { + if (m_online) { + // Returns true when it reads at least one byte + if (m_mdns.processQueries()) { + MainLoop::instance()->dispatch(InputEvent::NetworkActivity); + } + } + } +}; diff --git a/firmware/MQTT/MQTT b/firmware/MQTT/MQTT new file mode 120000 index 0000000..6de4cb2 --- /dev/null +++ b/firmware/MQTT/MQTT @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MQTT/src/MQTT \ No newline at end of file diff --git a/firmware/MQTT/MQTT.cpp b/firmware/MQTT/MQTT.cpp new file mode 120000 index 0000000..8aaff5b --- /dev/null +++ b/firmware/MQTT/MQTT.cpp @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MQTT/src/MQTT.cpp \ No newline at end of file diff --git a/firmware/MQTT/MQTT.h b/firmware/MQTT/MQTT.h new file mode 120000 index 0000000..8d834cb --- /dev/null +++ b/firmware/MQTT/MQTT.h @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/MQTT/src/MQTT.h \ No newline at end of file diff --git a/firmware/Sequencer.cpp b/firmware/Sequencer.cpp new file mode 100644 index 0000000..3590c6a --- /dev/null +++ b/firmware/Sequencer.cpp @@ -0,0 +1,54 @@ +#include "Sequencer.h" +#include "Figments/MainLoop.h" + +Sequencer::Sequencer(std::vector &&scenes) : + Task("SceneSequencer"), + m_scenes(std::move(scenes)) +{ +} + +void +Sequencer::loop() {} + +const char* +Sequencer::currentSceneName() +{ + return m_scenes[m_idx].name; +} + +const std::vector +Sequencer::scenes() const +{ + return m_scenes; +} + +void +Sequencer::handleEvent(const InputEvent& evt) +{ + if (evt.intent == InputEvent::SetPattern || evt.intent == InputEvent::NextPattern || evt.intent == InputEvent::PreviousPattern) { + Log.info("Switching pattern!"); + for(const char* pattern : m_scenes[m_idx].patterns) { + MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern}); + } + + if (evt.intent == InputEvent::NextPattern) { + m_idx++; + } else if (evt.intent == InputEvent::PreviousPattern) { + m_idx--; + } else { + m_idx = evt.asInt(); + } + + if (m_idx < 0) { + m_idx = m_scenes.size() - 1; + } + + if (m_idx >= m_scenes.size()) { + m_idx = 0; + } + + for(const char* pattern : m_scenes[m_idx].patterns) { + MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern}); + } + } +} diff --git a/firmware/Sequencer.h b/firmware/Sequencer.h new file mode 100644 index 0000000..ad94b70 --- /dev/null +++ b/firmware/Sequencer.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Figments/Figment.h" +#include + +class Sequencer: public Task { +public: + + class Scene { + public: + const char* name; + std::vector patterns; + }; + + Sequencer(std::vector &&scenes); + + void loop() override; + void handleEvent(const InputEvent& evt) override; + + const char* currentSceneName(); + const std::vector scenes() const; + +private: + int m_idx = 0; + std::vector m_scenes; +}; diff --git a/firmware/WebDuino/WebDuino b/firmware/WebDuino/WebDuino new file mode 120000 index 0000000..d2f5bff --- /dev/null +++ b/firmware/WebDuino/WebDuino @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/WebDuino/src/WebDuino \ No newline at end of file diff --git a/firmware/WebDuino/WebDuino.cpp b/firmware/WebDuino/WebDuino.cpp new file mode 120000 index 0000000..416f3f5 --- /dev/null +++ b/firmware/WebDuino/WebDuino.cpp @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/WebDuino/src/WebDuino.cpp \ No newline at end of file diff --git a/firmware/WebDuino/WebDuino.h b/firmware/WebDuino/WebDuino.h new file mode 120000 index 0000000..965b593 --- /dev/null +++ b/firmware/WebDuino/WebDuino.h @@ -0,0 +1 @@ +/home/tdfischer/.po-util/lib/WebDuino/src/WebDuino.h \ No newline at end of file diff --git a/firmware/WebTelemetry.cpp b/firmware/WebTelemetry.cpp new file mode 100644 index 0000000..f5cb554 --- /dev/null +++ b/firmware/WebTelemetry.cpp @@ -0,0 +1,157 @@ +#include "WebDuino/WebDuino.h" +#include "Figments/Figments.h" +#include "Figments/Input.h" +#include "colors.h" +#include "Sequencer.h" + +class WebTelemetry : public Task { + private: + TCPServer m_server; + TCPClient m_client; + Sequencer& m_sequencer; + + void onConnected() { + m_server.begin(); + Log.info("HTTP server started on %s:80", WiFi.localIP().toString().c_str()); + } + + void redirectToRoot() { + m_server.write("HTTP/1.1 303 Redirect\n"); + m_server.write("Location: /\n"); + m_server.write("Connection: close\n\n"); + } + + public: + WebTelemetry(Sequencer& sequencer) : Task("WebTelemetry"), m_server(80), m_sequencer(sequencer) { + } + + void handleEvent(const InputEvent &evt) { + if (evt.intent == InputEvent::NetworkStatus) { + onConnected(); + } + } + + void loop() override { + static String taskName; + if (m_client.connected()) { + if (m_client.available()) { + MainLoop::instance()->dispatch(InputEvent::NetworkActivity); + String requestLine = m_client.readStringUntil('\n'); + Log.info("%s %s", m_client.remoteIP().toString().c_str(), requestLine.c_str()); + if (requestLine.startsWith("GET")) { + int httpVersionIdx = requestLine.lastIndexOf(" "); + String uri = requestLine.substring(4, httpVersionIdx); + if (uri.equals("/")) { + m_server.write("HTTP/1.1 200 Renderbug is here!\n"); + m_server.write("Connection: close\n\n"); + m_server.write(""); + m_server.write("

Scenes

"); + auto curScene = m_sequencer.currentSceneName(); + for(auto scene : m_sequencer.scenes()) { + bool isEnabled = strcmp(curScene, scene.name) == 0; + m_server.write(""); + } + m_server.write("
"); + if (isEnabled) { + m_server.write(""); + } + m_server.write(scene.name); + if (isEnabled) { + m_server.write(""); + } + m_server.write("
    "); + for(auto patternName : scene.patterns) { + m_server.write("
  • "); + m_server.write(patternName); + m_server.write("
  • "); + } + m_server.write("
"); + m_server.write("
"); + m_server.write("
"); + m_server.write(""); + m_server.write("
"); + m_server.write("

Tasks

"); + auto sched = MainLoop::instance()->scheduler; + for(auto task : sched.tasks) { + bool isFigment = task->isFigment(); + + m_server.write(""); + } + m_server.write("
"); + if (isFigment) { + m_server.write(""); + } + m_server.write(task->name); + if (isFigment) { + m_server.write(""); + } + m_server.write(""); + if (task->state == Task::Running) { + m_server.write("Running"); + } else { + m_server.write("Paused"); + } + m_server.write(""); + m_server.write("name); + m_server.write("\">Stop"); + m_server.write("name); + m_server.write("\">Start
"); + m_server.write("Reboot Renderbug"); + m_server.write("Save configuration"); + } else if (uri.startsWith("/save")) { + MainLoop::instance()->dispatch(InputEvent::SaveConfigurationRequest); + redirectToRoot(); + } else if (uri.startsWith("/?color=")) { + int varStart = uri.indexOf("="); + String colorName = uri.substring(varStart + 1); + colorName.replace('+', ' '); + colorName.replace("%20", " "); + ColorInfo nextColor = colorForName(colorName); + MainLoop::instance()->dispatch(InputEvent{InputEvent::SetColor, nextColor.rgb}); + redirectToRoot(); + } else if (uri.startsWith("/?start=")) { + int varStart = uri.indexOf("="); + taskName = uri.substring(varStart + 1); + MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, taskName.c_str()}); + redirectToRoot(); + } else if (uri.startsWith("/?stop=")) { + int varStart = uri.indexOf("="); + taskName = uri.substring(varStart + 1); + MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, taskName.c_str()}); + redirectToRoot(); + } else if (uri.equals("/?pattern=prev")) { + redirectToRoot(); + MainLoop::instance()->dispatch(InputEvent::PreviousPattern); + } else if (uri.equals("/?pattern=next")) { + redirectToRoot(); + MainLoop::instance()->dispatch(InputEvent::NextPattern); + } else if (uri.equals("/reboot")) { + m_server.write("HTTP/1.1 200 Ok\n"); + m_server.write("Connection: close\n\n"); + m_server.write("Rebooting!"); + } else { + m_server.write("HTTP/1.1 404 Not found\n"); + m_server.write("Connection: close\n\n"); + } + } else { + m_server.write("HTTP/1.1 501 Not Implemented\n"); + m_server.write("Connection: close\n\n"); + } + } + m_client.stop(); + } else { + m_client = m_server.available(); + } + } +}; + + diff --git a/firmware/animations/Drain.cpp b/firmware/animations/Drain.cpp new file mode 100644 index 0000000..09f223a --- /dev/null +++ b/firmware/animations/Drain.cpp @@ -0,0 +1,66 @@ +#include "../Figments/Figments.h" + +using namespace NSFastLED; +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; + } + } + + 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); + } + } + + uint16_t m_pos; + uint16_t m_burst; +}; + + diff --git a/firmware/animations/Drain.h b/firmware/animations/Drain.h new file mode 100644 index 0000000..5140704 --- /dev/null +++ b/firmware/animations/Drain.h @@ -0,0 +1,63 @@ +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; + } + } + + 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); + } + } + + uint16_t m_pos; + uint16_t m_burst; +}; + + diff --git a/firmware/colors.cpp b/firmware/colors.cpp index 6093ea2..270d7a8 100644 --- a/firmware/colors.cpp +++ b/firmware/colors.cpp @@ -807,3 +807,7 @@ ColorInfo colorForName(const char *name) { } return ColorInfo(); } + +const ColorInfo* allColors() { + return color_data; +} diff --git a/firmware/colors.h b/firmware/colors.h index 0798d50..68372a8 100644 --- a/firmware/colors.h +++ b/firmware/colors.h @@ -7,3 +7,4 @@ typedef struct ColorInfo { } ColorInfo; ColorInfo colorForName(const char *name); +const ColorInfo* allColors(); diff --git a/firmware/inputs/.FlaschenTaschen.cpp.swp b/firmware/inputs/.FlaschenTaschen.cpp.swp deleted file mode 100644 index 05274eb..0000000 Binary files a/firmware/inputs/.FlaschenTaschen.cpp.swp and /dev/null differ diff --git a/firmware/inputs/Buttons.cpp b/firmware/inputs/Buttons.cpp index 3632c55..5c19829 100644 --- a/firmware/inputs/Buttons.cpp +++ b/firmware/inputs/Buttons.cpp @@ -5,6 +5,7 @@ void Buttons::onStart() { for(int i = 0; i < 3; i++) { + Log.info("Bound pin %d to button %d", 2 + i, i); m_buttons[i].attach(2 + i, INPUT_PULLDOWN); m_buttons[i].interval(15); } @@ -15,8 +16,22 @@ Buttons::read() { for(int i = 0; i < 3; i++) { m_buttons[i].update(); - if (m_buttons[i].fell()) { - return InputEvent{m_buttonMap[i]}; + if (m_buttons[i].rose()) { + //Log.info("Read press on %d", i); + int buttonID = m_buttonMap[i]; + for(int j = 0; j < 3; j++ ) { + if (j != i && m_buttons[j].held()) { + buttonID |= m_buttonMap[j]; + Log.info("Chord with %d", j); + m_wasChord[j] = true; + } + } + if (m_wasChord[i]) { + //Log.info("Not emitting release from previous chord"); + m_wasChord[i] = false; + } else { + return InputEvent{InputEvent::UserInput, buttonID}; + } } } return InputEvent{}; diff --git a/firmware/inputs/Buttons.h b/firmware/inputs/Buttons.h index 399db2d..5c92f5a 100644 --- a/firmware/inputs/Buttons.h +++ b/firmware/inputs/Buttons.h @@ -15,24 +15,27 @@ public: if (readResult == HIGH) { m_state = Started; m_downStart = millis(); - Log.info("Button %d is started!", m_pin); + //Log.info("Button %d is started!", m_pin); } } else if (m_state == Started && millis() - m_downStart >= m_interval) { if (readResult == HIGH) { m_state = Confirmed; - Log.info("Button %d is CONFIRMED!", m_pin); + //Log.info("Button %d is CONFIRMED!", m_pin); } else { m_state = Ready; - Log.info("Button %d bounced back to ready!", m_pin); + //Log.info("Button %d bounced back to ready!", m_pin); } } else if (m_state == Confirmed || m_state == Held) { if (readResult == LOW) { - m_state = Ready; - Log.info("Button %d is released and back to ready!", m_pin); + //Log.info("Button %d is released", m_pin); + m_state = Released; } else if (m_state == Confirmed) { m_state = Held; - Log.info("Button %d is being held down!", m_pin); + //Log.info("Button %d is being held down!", m_pin); } + } else if (m_state == Released) { + //Log.info("Button %d is ready!", m_pin); + m_state = Ready; } } @@ -44,6 +47,10 @@ public: return m_state == Confirmed; } + bool rose() const { + return m_state == Released; + } + bool held() const { return m_state == Held; } @@ -53,7 +60,8 @@ private: Ready, Started, Confirmed, - Held + Held, + Released }; State m_state = Ready; @@ -64,10 +72,23 @@ private: class Buttons: public InputSource { public: + Buttons() : InputSource("Buttons") {} void onStart() override; InputEvent read() override; + enum Chord { + None = 0, + Circle = 1, + Triangle = 2, + Cross = 4, + CircleTriangle = Circle | Triangle, + CircleCross = Circle | Cross, + TriangleCross = Triangle | Cross, + CircleTriangleCross = Circle | Triangle | Cross + }; + private: Bounce m_buttons[3]; - InputEvent::Intent m_buttonMap[3] = {InputEvent::PowerToggle, InputEvent::NextPattern, InputEvent::UserInput}; + Chord m_buttonMap[3] = {Circle, Triangle, Cross}; + bool m_wasChord[3] = {false, false, false}; }; diff --git a/firmware/inputs/ColorCycle.h b/firmware/inputs/ColorCycle.h index 2577a42..44a8a24 100644 --- a/firmware/inputs/ColorCycle.h +++ b/firmware/inputs/ColorCycle.h @@ -7,14 +7,33 @@ public: : InputSource(name, initialState), m_colors(colors) {} InputEvent read() override { - EVERY_N_SECONDS(Period) { - m_idx %= m_colors.size(); - return InputEvent{InputEvent::SetColor, m_colors[m_idx++]}; + if (m_reset) { + m_reset = false; + m_override = false; + return InputEvent{InputEvent::SetColor, m_colors[m_idx]}; + } + if (!m_override) { + EVERY_N_SECONDS(Period) { + m_idx %= m_colors.size(); + return InputEvent{InputEvent::SetColor, m_colors[m_idx++]}; + } } return InputEvent{}; } + void handleEvent(const InputEvent& event) { + if (event.intent == InputEvent::SetColor) { + m_override = true; + } + } + + void onStart() override { + m_reset = true; + } + private: std::vector m_colors; int m_idx = 0; + bool m_reset = true; + bool m_override = false; }; diff --git a/firmware/inputs/MPU6050.h b/firmware/inputs/MPU6050.h index 85603c3..b05a26b 100644 --- a/firmware/inputs/MPU6050.h +++ b/firmware/inputs/MPU6050.h @@ -13,6 +13,7 @@ class MPU5060: public InputSource { const int ACCEL_CONFIG_REG = 0x1C; public: + MPU5060() : InputSource("MPU5060", HardwareConfig::HAS_MPU_6050 ? Task::Running : Task::Stopped) {} void onStart() override { Wire.begin(); diff --git a/firmware/main.cpp b/firmware/main.cpp index 677b3e0..c83b0cd 100644 --- a/firmware/main.cpp +++ b/firmware/main.cpp @@ -1,120 +1,153 @@ +#define RENDERBUG_VERSION 1 +#define PLATFORM_PHOTON +//#define PLATFORM_ESP2800 + #include "FastLED/FastLED.h" #include "Figments/Figments.h" -#include "PhotonTelemetry.h" #include "Static.h" #include "Config.h" #include "colors.h" +#include "WebTelemetry.cpp" +#include "MDNSService.cpp" +#include "Sequencer.h" + #include "animations/Power.cpp" #include "animations/SolidAnimation.cpp" #include "animations/Chimes.cpp" #include "animations/Flashlight.cpp" +#include "animations/Drain.cpp" #include "animations/UpdateStatus.h" -#include "inputs/Photon.h" #include "inputs/ColorCycle.h" -#include "inputs/CloudStatus.h" -#include "inputs/MPU6050.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" +#endif SerialLogHandler logHandler; using namespace NSFastLED; -#define LED_NUM 256 #define MAX_BRIGHTNESS 255 -#define PSU_MILLIAMPS 4800 +//#define PSU_MILLIAMPS 4800 +//#define PSU_MILLIAMPS 500 +#define PSU_MILLIAMPS 1000 // Enable system thread, so rendering happens while booting SYSTEM_THREAD(ENABLED); // Setup FastLED and the display -CRGB leds[LED_NUM]; -Display dpy(leds, LED_NUM, Static::instance()->coordMap()); +CRGB leds[HardwareConfig::MAX_LED_NUM]; +Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static::instance()->coordMap()); LinearCoordinateMapping neckMap{60, 0}; -Display neckDisplay(leds, LED_NUM, &neckMap); +Display neckDisplay(leds, HardwareConfig::MAX_LED_NUM, &neckMap); // Setup power management Power power; -class DrainAnimation: public Figment { -public: - - DrainAnimation(Task::State initialState) : Figment("Drain", initialState) {} - - void loop() override { - EVERY_N_MILLISECONDS(15) { - m_pos++; - m_fillColor.update(); - } - EVERY_N_MILLISECONDS(50) { - 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; - } - } - - 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); - } - } - - uint16_t m_pos; - uint16_t m_burst; -}; - // Clip the display at whatever is configured while still showing over-paints FigmentFunc displayClip([](Display* dpy) { auto coords = Static::instance()->coordMap(); - for(int i = 0; i < coords->startPixel; i++) { - dpy->pixelAt(i) %= 40; - } - for(int i = LED_NUM; i > coords->pixelCount + coords->startPixel; i--) { - dpy->pixelAt(i) %= 40; + for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) { + if (i < coords->startPixel || i > coords->pixelCount + coords->startPixel) { + dpy->pixelAt(i) %= 40; + } } }); +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++) { + if (i < coords->startPixel || i > coords->startPixel + coords->pixelCount) { + dpy->pixelAt(i) += CRGB(brightness, 0, 0); + } else { + dpy->pixelAt(i) += CRGB(255 - brightness, 255 - brightness, 255 - brightness); + } + } +}); + +class InputBlip: public Figment { +public: + InputBlip() : Figment("InputBlip") {} + + void handleEvent(const InputEvent& evt) override { + if (evt.intent != InputEvent::None) { + m_time = qadd8(m_time, 5); + } + } + void loop() override { + if (m_time > 0) { + m_time--; + } + } + void render(Display* dpy) const override { + if (m_time > 0) { + dpy->pixelAt(0) = CRGB(0, brighten8_video(ease8InOutApprox(m_time)), 0); + } + } +private: + uint8_t m_time = 0; +}; + +InputBlip inputBlip; + +InputFunc randomPulse([]() { + static unsigned int pulse = 0; + EVERY_N_MILLISECONDS(25) { + if (pulse == 0) { + pulse = random(25) + 25; + } + } + if (pulse > 0) { + if (random(255) >= 25) { + pulse--; + return InputEvent{InputEvent::Acceleration, beatsin16(60 * 8, 0, 4972)}; + } + } + return InputEvent{}; +}, "Pulse", Task::Running); + +InputMapper keyMap([](const InputEvent& evt) { + if (evt.intent == InputEvent::UserInput) { + Buttons::Chord chord = (Buttons::Chord)evt.asInt(); + switch(chord) { + case Buttons::Circle: + return InputEvent::PowerToggle; + break; + case Buttons::Triangle: + return InputEvent::NextPattern; + break; + case Buttons::Cross: + return InputEvent::UserInput; + break; + } + } + return InputEvent::None; +}); + ChimesAnimation chimes{Task::Stopped}; -SolidAnimation solid{Task::Stopped}; -DrainAnimation drain{Task::Running}; +SolidAnimation solid{Task::Running}; +DrainAnimation drain{Task::Stopped}; Flashlight flashlight{Task::Stopped}; +Sequencer sequencer{{ + {"Solid", {"Solid"}}, + {"Drain", {"Drain"}}, + {"Flashlight", {"Flashlight"}}, + {"Fiercewater", {"Solid", "Kieryn", "Hackerbots"}}, + {"Nightlight", {"Drain", "Noisebridge"}}, + {"Gay", {"Solid", "Rainbow", "Hackerbots", "Kieryn"}}, + {"Acid", {"Chimes", "Hackerbots", "Rainbow"}}, +}}; + // Render all layers to the displays Renderer renderer{ @@ -127,17 +160,226 @@ Renderer renderer{ &flashlight, Static::instance(), &displayClip, + &inputBlip, &power, } }; -// Photon telemetry needs a reference to the selected animation's name, so we -// set it up here -PhotonTelemetry telemetry; +Renderer configRenderer{ + {&dpy}, + {&drain, &configDisplay, &inputBlip, &power} +}; + +MDNSService mdnsService; + +class OnlineTaskMixin : public virtual Loopable { + public: + void handleEvent(const InputEvent &evt) override { + if (evt.intent == InputEvent::NetworkStatus) { + m_online = true; + } + if (m_online) { + handleEventOnline(evt); + } + } + + virtual void handleEventOnline(const InputEvent &evt) = 0; + + void loop() override { + if (m_online) { + loopOnline(); + } + } + + virtual void loopOnline() = 0; + + private: + bool m_online = false; +}; + +#include "MQTT/MQTT.h" + +class MQTTTelemetry : public Task, OnlineTaskMixin { + public: + MQTTTelemetry() : Task("MQTT"), m_client("relay.malloc.hackerbots.net", 1883, 512, 15, MQTTTelemetry::s_callback, true) { + s_instance = this; + strcpy(m_deviceName, System.deviceID().c_str()); + } + + void handleEventOnline(const InputEvent& event) override { + char response[255]; + if (event.intent == InputEvent::SetPower) { + JSONBufferWriter writer(response, sizeof(response)); + writer.beginObject(); + writer.name("state").value(event.asInt() == 0 ? "OFF" : "ON"); + writer.endObject(); + writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; + m_client.publish(m_statTopic, response, MQTT::QOS1); + } else if (event.intent == InputEvent::SetBrightness) { + JSONBufferWriter writer(response, sizeof(response)); + writer.beginObject(); + writer.name("brightness").value(event.asInt()); + writer.endObject(); + writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; + m_client.publish(m_statTopic, response, MQTT::QOS1); + } else if (event.intent == InputEvent::SetColor) { + JSONBufferWriter writer(response, sizeof(response)); + writer.beginObject(); + writer.name("color").beginObject(); + CRGB rgb = event.asRGB(); + writer.name("r").value(rgb.r); + writer.name("g").value(rgb.g); + writer.name("b").value(rgb.b); + writer.endObject(); + 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; + } + //root.printTo(response, sizeof(response)); + //m_client.publish("renderbug/event", response);*/ + } + } + + void loopOnline() override { + if (m_client.isConnected()) { + m_client.loop(); + EVERY_N_SECONDS(10) { + char heartbeatBuf[255]; + JSONBufferWriter writer(heartbeatBuf, sizeof(heartbeatBuf)); + writer.beginObject(); + writer.name("fps").value(NSFastLED::FastLED.getFPS()); + writer.name("os_version").value(System.version()); + writer.name("free_ram").value((unsigned int)System.freeMemory()); + //writer.name("uptime").value(System.uptime()); + /*WiFiSignal sig = WiFi.RSSI(); + writer.name("strength").value(sig.getStrength()); + writer.name("quality").value(sig.getQuality());*/ + writer.name("RSSI").value(WiFi.RSSI()); + writer.name("SSID").value(WiFi.SSID()); + //writer.name("MAC").value(WiFi.macAddress()); + writer.name("localip").value(WiFi.localIP().toString()); + writer.endObject(); + writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; + m_client.publish(m_attrTopic, heartbeatBuf); + } + } else { + m_client.connect("renderbug_" + String(m_deviceName) + "_" + String(Time.now())); + char response[512]; + JSONBufferWriter writer(response, sizeof(response)); + + String devTopic = String("homeassistant/light/renderbug/") + m_deviceName; + writer.beginObject(); + writer.name("~").value(devTopic.c_str()); + writer.name("name").value("Renderbug"); + writer.name("unique_id").value(m_deviceName); + writer.name("cmd_t").value("~/set"); + writer.name("stat_t").value("~/set"); + writer.name("json_attr_t").value("~/attributes"); + writer.name("schema").value("json"); + writer.name("brightness").value(true); + writer.name("rgb").value(true); + writer.name("ret").value(true); + + writer.name("dev").beginObject(); + writer.name("name").value("Renderbug"); + writer.name("mdl").value("Renderbug"); + writer.name("sw").value(RENDERBUG_VERSION); + writer.name("mf").value("Phong Robotics"); + + writer.name("ids").beginArray(); + writer.value(m_deviceName); + writer.endArray(); + + writer.endObject(); + + writer.name("fx_list").beginArray(); + for(const Sequencer::Scene& scene : sequencer.scenes()) { + writer.value(scene.name); + } + writer.endArray(); + + writer.endObject(); + writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; + + String configTopic = devTopic + "/config"; + m_client.publish(configTopic, response, true); + + String statTopic = devTopic + "/state"; + String cmdTopic = devTopic + "/set"; + String attrTopic = devTopic + "/attributes"; + strcpy(m_statTopic, statTopic.c_str()); + strcpy(m_attrTopic, attrTopic.c_str()); + strcpy(m_cmdTopic, cmdTopic.c_str()); + m_client.subscribe(m_cmdTopic, MQTT::QOS1); + } + } + + private: + 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); + JSONObjectIterator cmdIter(cmd); + while(cmdIter.next()) { + if (cmdIter.name() == "state" && cmdIter.value().toString() == "ON") { + MainLoop::instance()->dispatch({InputEvent::SetPower, 1}); + } else if (cmdIter.name() == "state" && cmdIter.value().toString() == "OFF") { + MainLoop::instance()->dispatch({InputEvent::SetPower, 0}); + } + + if (cmdIter.name() == "color") { + JSONObjectIterator colorIter(cmdIter.value()); + uint8_t r, g, b; + while(colorIter.next()) { + if (colorIter.name() == "r") { + r = colorIter.value().toInt(); + } else if (colorIter.name() == "g") { + g = colorIter.value().toInt(); + } else if (colorIter.name() == "b") { + b = colorIter.value().toInt(); + } + } + MainLoop::instance()->dispatch(InputEvent{InputEvent::SetColor, CRGB{r, g, b}}); + } + + if (cmdIter.name() == "brightness") { + uint8_t brightness = cmdIter.value().toInt(); + MainLoop::instance()->dispatch(InputEvent{InputEvent::SetBrightness, brightness}); + } + } + } + } + + static void s_callback(char* topic, byte* payload, unsigned int length) { + s_instance->callback(topic, payload, length); + }; + static MQTTTelemetry* s_instance; + MQTT m_client; + char m_deviceName[100]; + char m_statTopic[100]; + char m_attrTopic[100]; + char m_cmdTopic[100]; +}; + +MQTTTelemetry mqttTelemetry; + +MQTTTelemetry* MQTTTelemetry::s_instance = 0; + +WebTelemetry webTelemetry(sequencer); // Cycle some random colors ColorSequenceInput<7> noisebridgeCycle{{colorForName("Red").rgb}, "Noisebridge", Task::Stopped}; -ColorSequenceInput<7> hackerbotsCycle{{colorForName("Purple").rgb}, "Hackerbots", Task::Stopped}; ColorSequenceInput<13> kierynCycle{{ colorForName("Cerulean").rgb, @@ -154,28 +396,223 @@ ColorSequenceInput<7> rainbowCycle{{ colorForName("Blue").rgb, colorForName("Purple").rgb, colorForName("White").rgb, -}, "Rainbow", Task::Running}; +}, "Rainbow", Task::Stopped}; + +ColorSequenceInput<7> hackerbotsCycle{{ + colorForName("Purple").rgb, + colorForName("White").rgb, + colorForName("Cyan").rgb, +}, "Hackerbots", Task::Running}; + +struct ConfigInputTask: public BufferedInputSource { +public: + ConfigInputTask() : BufferedInputSource("ConfigInput") {} + + void handleEvent(const InputEvent& evt) override { + if (evt.intent == InputEvent::UserInput) { + Buttons::Chord chord = (Buttons::Chord) evt.asInt(); + switch (chord) { + case Buttons::Circle: + m_currentIntent = nextIntent(); + Log.info("Next setting... (%d)", m_currentIntent); + break; + case Buttons::CircleTriangle: + Log.info("Increment..."); + increment(); + break; + case Buttons::CircleCross: + Log.info("Decrement..."); + decrement(); + break; + case Buttons::Triangle: + Log.info("Save..."); + setEvent(InputEvent::SaveConfigurationRequest); + break; + } + } + } + +private: + InputEvent::Intent m_currentIntent = InputEvent::SetDisplayLength; + + void decrement() { + int current = 0; + switch (m_currentIntent) { + case InputEvent::SetDisplayLength: + current = Static::instance()->coordMap()->pixelCount; + break; + case InputEvent::SetDisplayOffset: + current = Static::instance()->coordMap()->startPixel; + break; + } + setEvent(InputEvent{m_currentIntent, current - 1}); + } + + void increment() { + int current = 0; + switch (m_currentIntent) { + case InputEvent::SetDisplayLength: + current = Static::instance()->coordMap()->pixelCount; + break; + case InputEvent::SetDisplayOffset: + current = Static::instance()->coordMap()->startPixel; + break; + } + setEvent(InputEvent{m_currentIntent, current + 1}); + } + + InputEvent::Intent nextIntent() { + switch (m_currentIntent) { + case InputEvent::SetDisplayLength: + return InputEvent::SetDisplayOffset; + case InputEvent::SetDisplayOffset: + return InputEvent::SetDisplayLength; + } + } +}; + +enum Phase { + Null, + AstronomicalDay, + NauticalDay, + CivilDay, + CivilNight, + NauticalNight, + AstronomicalNight, + Evening, + Bedtime, +}; + +struct ScheduleEntry { + uint8_t hour; + uint8_t brightness; +}; + +std::array schedule{{ + {0, 0}, + {5, 0}, + {6, 0}, + {7, 10}, + {8, 80}, + {11, 120}, + {18, 200}, + {19, 255}, + {22, 120}, + {23, 20} +}}; + +uint8_t brightnessForTime(uint8_t hour, uint8_t minute) { + ScheduleEntry start = schedule.back(); + ScheduleEntry end = schedule.front(); + for(ScheduleEntry cur : schedule) { + // Find the last hour that is prior to or equal to now + if (cur.hour <= hour) { + start = cur; + } else { + break; + } + } + for(ScheduleEntry cur : schedule) { + // Find the first hour that is after now + // If no such hour exists, we should automatically wrap back to hour 0 + if (cur.hour > hour) { + end = cur; + break; + } + } + + if (start.hour > end.hour) { + end.hour += 24; + } + + uint16_t startTime = start.hour * 60; + uint16_t endTime = end.hour * 60; + uint16_t nowTime = hour * 60 + minute; + + uint16_t duration = endTime - startTime; + uint16_t curDuration = nowTime - startTime; + + uint8_t frac = ((double)curDuration / (double)duration) * 255.0; + + return lerp8by8(start.brightness, end.brightness, frac); +} + +InputFunc circadianRhythm([]() { + static Phase lastPhase = Null; + static bool needsUpdate = true; + EVERY_N_SECONDS(60) { + if (Time.isValid()) { + return InputEvent{InputEvent::SetBrightness, brightnessForTime(Time.hour(), Time.minute())}; + } + } + return InputEvent{}; +}, "CircadianRhythm", Task::Running); + +// A special mainloop app for configuring hardware settings that reboots the +// device when the user is finished. +MainLoop configApp{{ + // Manage read/write of configuration data + Static::instance(), + +#ifdef PLATFORM_PHOTON + // Update photon telemetry + Static::instance(), +#endif + + // 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, + // Render it all + &configRenderer, +}}; // Turn on, -MainLoop runner{{ +MainLoop renderbugApp{{ // Load/update graphics configuration from EEPROM and Particle Static::instance(), + // Platform inputs +#ifdef PLATFORM_PHOTON // Particle cloud status Static::instance(), // Monitor network state and provide particle API events Static::instance(), +#endif + // Hardware drivers new MPU5060(), new Buttons(), + // Map buttons to events + &keyMap, + + // Pattern sequencer + &sequencer, + + // Daily rhythm activities + &circadianRhythm, + + // Periodic motion input + &randomPulse, + // Periodic color inputs &noisebridgeCycle, - &hackerbotsCycle, &kierynCycle, &rainbowCycle, + &hackerbotsCycle, // Animations &chimes, @@ -187,36 +624,110 @@ MainLoop runner{{ &power, &displayClip, Static::instance(), + &inputBlip, // Render everything &renderer, + // Platform telemetry +#ifdef PLATFORM_PHOTON // Update photon telemetry - &telemetry, + Static::instance(), + + // Web telemetry UI + &webTelemetry, + + // MQTT telemetry + &mqttTelemetry, + + // Network discovery + &mdnsService, +#endif }}; +MainLoop &runner = renderbugApp; + +struct BootOptions { + BootOptions() { + pinMode(2, INPUT_PULLDOWN); + pinMode(3, INPUT_PULLDOWN); + pinMode(4, INPUT_PULLDOWN); + isSetup = digitalRead(2) == HIGH; + isSerial = digitalRead(3) == HIGH; + isFlash = digitalRead(4) == HIGH; + + configStatus.setActive(isSetup); + serialStatus.setActive(isSerial); + } + + void waitForRelease() { + while(digitalRead(2) == HIGH || digitalRead(3) == HIGH) {}; + } + + bool isSetup = false; + bool isSerial = false; + bool isFlash = false; + + LEDStatus serialStatus = LEDStatus(RGB_COLOR_ORANGE, LED_PATTERN_FADE, LED_SPEED_FAST, LED_PRIORITY_BACKGROUND); + LEDStatus configStatus = LEDStatus(RGB_COLOR_YELLOW, LED_PATTERN_FADE, LED_SPEED_NORMAL, LED_PRIORITY_IMPORTANT); +}; + +BootOptions bootopts; + +ApplicationWatchdog *wd; + +void watchdogHandler() { + System.enterSafeMode(); +} + // Tune in, void setup() { + wd = new ApplicationWatchdog(5000, watchdogHandler, 1536); Serial.begin(115200); - //while(!Serial.isConnected()) { Particle.process(); } - //Serial.println("Hello, there!"); - Log.info("🐛 Booting Renderbug %s!", System.deviceID().c_str()); - Log.info("🐞 I am built for %d LEDs running on %dmA", LED_NUM, PSU_MILLIAMPS); - Log.info("📡 Particle version %s", System.version().c_str()); + if (bootopts.isFlash) { + System.dfu(); + } + if (bootopts.isSerial) { + bootopts.waitForRelease(); + while(!Serial.isConnected()) { +#ifdef PLATFORM_PHOTON + Particle.process(); +#endif + } + Log.info("\xf0\x9f\x94\x8c Serial connected"); + } + Log.info(u8"🐛 Booting Renderbug %s!", System.deviceID().c_str()); + Log.info(u8"🐞 I am built for %d LEDs running on %dmA", HardwareConfig::MAX_LED_NUM, PSU_MILLIAMPS); +#ifdef PLATFORM_PHOTON + Log.info(u8"📡 Particle version %s", System.version().c_str()); +#endif + Log.info(u8" Boot pin configuration:"); + Log.info(u8" 2: Setup - %d", bootopts.isSetup); + Log.info(u8" 3: Serial - %d", bootopts.isSerial); + Log.info(u8" 4: Flash - %d", bootopts.isFlash); - Log.info("💡 Starting FastLED..."); - FastLED.addLeds(leds, LED_NUM); + Log.info(u8" Setting timezone to UTC-7"); + Time.zone(-7); - Log.info("🌌 Starting Figment..."); + Log.info(u8"💡 Starting FastLED..."); + FastLED.addLeds(leds, HardwareConfig::MAX_LED_NUM); + + if (bootopts.isSetup) { + Log.info(u8"🌌 Starting Figment in configuration mode..."); + runner = configApp; + } else { + Log.info(u8"🌌 Starting Figment..."); + } Serial.flush(); runner.start(); - Log.info("💽 %lu bytes of free RAM", System.freeMemory()); - Log.info("🚀 Setup complete! Ready to rock and roll."); + Log.info(u8"💽 %lu bytes of free RAM", System.freeMemory()); + Log.info(u8"🚀 Setup complete! Ready to rock and roll."); Serial.flush(); } // Drop out. void loop() { runner.loop(); + wd->checkin(); } diff --git a/firmware/PhotonTelemetry.cpp b/firmware/platform/particle/PhotonTelemetry.cpp similarity index 77% rename from firmware/PhotonTelemetry.cpp rename to firmware/platform/particle/PhotonTelemetry.cpp index 46bb34e..2315cbe 100644 --- a/firmware/PhotonTelemetry.cpp +++ b/firmware/platform/particle/PhotonTelemetry.cpp @@ -1,4 +1,5 @@ #include "PhotonTelemetry.h" +#include "../../Static.h" using namespace NSFastLED; @@ -12,6 +13,7 @@ PhotonTelemetry::onConnected() Particle.variable("brightness", m_currentBrightness); Particle.variable("fps", m_fps); Particle.variable("services", m_serviceList); + Particle.variable("localip", m_localIP); m_online = true; } @@ -31,8 +33,9 @@ PhotonTelemetry::loop() if (m_online) { EVERY_N_SECONDS(30) { + m_localIP = WiFi.localIP().toString(); char valueBuf[255]; - snprintf(valueBuf, sizeof(valueBuf), "{\"fps\": %lu}", m_fps); + snprintf(valueBuf, sizeof(valueBuf), "{\"fps\": %lu, \"localip\": \"%s\"}", m_fps, m_localIP.c_str()); Log.info("Heartbeat: %s", valueBuf); Particle.publish("renderbug/heartbeat", valueBuf); auto sched = MainLoop::instance()->scheduler; @@ -54,6 +57,7 @@ PhotonTelemetry::loop() void PhotonTelemetry::handleEvent(const InputEvent &evt) { + Serial.flush(); if (evt.intent == InputEvent::NetworkStatus) { onConnected(); } @@ -93,14 +97,26 @@ PhotonTelemetry::handleEvent(const InputEvent &evt) 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 = "unknown"; + sourceName = 0; break; } char valueBuf[255]; @@ -108,18 +124,22 @@ PhotonTelemetry::handleEvent(const InputEvent &evt) case InputEvent::Null: snprintf(valueBuf, sizeof(valueBuf), "null");break; case InputEvent::Integer: - snprintf(valueBuf, sizeof(valueBuf), "%d", evt.asInt());break; + 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]; - snprintf(buf, sizeof(buf), "{\"intent\": \"%s\", \"value\": %s}", sourceName, valueBuf); + 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 (m_online) { if (evt.intent != m_lastEvent.intent) { if (m_duplicateEvents > 0) { - Log.info("Suppressed reporting %d duplicate events.", m_duplicateEvents); + Log.info("Suppressed reporting %ld duplicate events.", m_duplicateEvents); } Log.info("Event: %s", buf); m_duplicateEvents = 0; @@ -141,3 +161,5 @@ PhotonTelemetry::handleEvent(const InputEvent &evt) } } } + +STATIC_ALLOC(PhotonTelemetry); diff --git a/firmware/PhotonTelemetry.h b/firmware/platform/particle/PhotonTelemetry.h similarity index 96% rename from firmware/PhotonTelemetry.h rename to firmware/platform/particle/PhotonTelemetry.h index f209b45..cb3d7ab 100644 --- a/firmware/PhotonTelemetry.h +++ b/firmware/platform/particle/PhotonTelemetry.h @@ -12,6 +12,7 @@ private: int m_frameIdx; String m_serviceList; + String m_localIP; uint32_t m_currentBrightness; LEDStatus m_ledStatus = LEDStatus(0, LED_PATTERN_FADE, LED_SPEED_FAST); uint32_t m_rgbPulseFrame = 0; diff --git a/firmware/inputs/CloudStatus.cpp b/firmware/platform/particle/inputs/CloudStatus.cpp similarity index 72% rename from firmware/inputs/CloudStatus.cpp rename to firmware/platform/particle/inputs/CloudStatus.cpp index 7082c89..5f658a4 100644 --- a/firmware/inputs/CloudStatus.cpp +++ b/firmware/platform/particle/inputs/CloudStatus.cpp @@ -1,5 +1,5 @@ #include "CloudStatus.h" -#include "../Static.h" +#include "../../../Static.h" void CloudStatus::onStart() @@ -19,6 +19,9 @@ CloudStatus::initNetwork(system_event_t event, int param) { if (param == cloud_status_connected) { Log.info("Connected to T H E C L O U D"); MainLoop::instance()->dispatch(InputEvent{InputEvent::NetworkStatus, true}); + } else if (param == cloud_status_disconnected) { + Log.info("Lost cloud connection!!"); + MainLoop::instance()->dispatch(InputEvent{InputEvent::NetworkStatus, false}); } } diff --git a/firmware/inputs/CloudStatus.h b/firmware/platform/particle/inputs/CloudStatus.h similarity index 84% rename from firmware/inputs/CloudStatus.h rename to firmware/platform/particle/inputs/CloudStatus.h index ae361ad..d052d07 100644 --- a/firmware/inputs/CloudStatus.h +++ b/firmware/platform/particle/inputs/CloudStatus.h @@ -1,4 +1,4 @@ -#include "../Figments/Figments.h" +#include "../../../Figments/Figments.h" class CloudStatus: public Task { public: diff --git a/firmware/inputs/Photon.cpp b/firmware/platform/particle/inputs/Photon.cpp similarity index 92% rename from firmware/inputs/Photon.cpp rename to firmware/platform/particle/inputs/Photon.cpp index dc0972d..561bab0 100644 --- a/firmware/inputs/Photon.cpp +++ b/firmware/platform/particle/inputs/Photon.cpp @@ -1,13 +1,14 @@ #include "Particle.h" -#include "../Figments/Figments.h" -#include "../colors.h" -#include "../Static.h" +#include "../../../Figments/Figments.h" +#include "../../../colors.h" +#include "../../../Static.h" #include "./Photon.h" void PhotonInput::onConnected() { Log.info("Connecting photon input..."); + Particle.function("save", &PhotonInput::save, this); Particle.function("power", &PhotonInput::setPower, this); Particle.function("next", &PhotonInput::nextPattern, this); Particle.function("input", &PhotonInput::input, this); @@ -115,6 +116,11 @@ PhotonInput::previousPattern(String command) return 0; } +int +PhotonInput::save(String command) { + setEvent(InputEvent::SaveConfigurationRequest); +} + int PhotonInput::setPower(String command) { diff --git a/firmware/inputs/Photon.h b/firmware/platform/particle/inputs/Photon.h similarity index 92% rename from firmware/inputs/Photon.h rename to firmware/platform/particle/inputs/Photon.h index 958aa73..63124b4 100644 --- a/firmware/inputs/Photon.h +++ b/firmware/platform/particle/inputs/Photon.h @@ -1,5 +1,5 @@ #include "Particle.h" -#include "../Figments/Figments.h" +#include "../../../Figments/Figments.h" class PhotonInput: public BufferedInputSource { public: @@ -19,6 +19,7 @@ private: int previousPattern(String command); int setPower(String command); int setBrightness(String command); + int save(String command); int startThing(String command); int stopThing(String command); diff --git a/libs.txt b/libs.txt index 7c72db8..b985e21 100644 --- a/libs.txt +++ b/libs.txt @@ -1,2 +1,5 @@ https://github.com/focalintent/fastled-sparkcore.git FastLED https://github.com/geeksville/ParticleWebLog ParticleWebLog + MDNS +https://github.com/kasperkamperman/webduino WebDuino +https://github.com/hirotakaster/MQTT/ MQTT