#include "Arduino.h" #include #include #ifndef PLATFORM_PHOTON #include #include #endif #include "Platform.h" #include "Static.h" #include "Config.h" #include "Sequencer.h" #include "LogService.h" #include "animations/Power.cpp" #include "animations/SolidAnimation.cpp" #include "animations/Chimes.cpp" #include "animations/Flashlight.cpp" #include "animations/Drain.cpp" #include "animations/UpdateStatus.h" #include "inputs/ColorCycle.h" #include "inputs/Buttons.h" #include "inputs/MPU6050.h" #ifdef PLATFORM_PHOTON #include "platform/particle/inputs/Photon.h" #include "platform/particle/inputs/CloudStatus.h" #include "platform/particle/PhotonTelemetry.h" #include "platform/particle/WebTelemetry.cpp" #include "platform/particle/MDNSService.cpp" #else #include "WiFiTask.h" #include "platform/arduino/BluetoothSerialTelemetry.h" #include "platform/arduino/MQTTTelemetry.h" #include #endif //SerialLogHandler logHandler; #define MAX_BRIGHTNESS 255 //#define PSU_MILLIAMPS 4800 //#define PSU_MILLIAMPS 500 //#define PSU_MILLIAMPS 1000 #define PSU_MILLIAMPS 1000 // Enable system thread, so rendering happens while booting //SYSTEM_THREAD(ENABLED); // Setup FastLED and the display CRGB leds[HardwareConfig::MAX_LED_NUM]; Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static::instance()->coordMap()); // Setup power management Power power; FigmentFunc configDisplay([](Display* dpy) { uint8_t brightness = brighten8_video(beatsin8(60)); auto coords = Static::instance()->coordMap(); for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) { 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", Task::Stopped) {} 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; class ArduinoOTAUpdater : public BufferedInputSource { public: ArduinoOTAUpdater() : BufferedInputSource("ArduinoOTA") { ArduinoOTA.onStart(&ArduinoOTAUpdater::s_onStart); ArduinoOTA.onProgress(&ArduinoOTAUpdater::s_onProgress); } void loop() override { if (m_online) { ArduinoOTA.handle(); } BufferedInputSource::loop(); } void handleEvent(const InputEvent& evt) { if (evt.intent == InputEvent::NetworkStatus && evt.asInt()) { Log.notice("Booting OTA"); m_online = true; ArduinoOTA.begin(); } } private: bool m_online = false; static void s_onStart() { Log.notice("OTA Start!"); Static::instance()->setEvent(InputEvent::FirmwareUpdate); } static void s_onProgress(unsigned int progress, unsigned int total) { Log.notice("OTA Progress! %d / %d", progress, total); Static::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress}); } }; STATIC_ALLOC(ArduinoOTAUpdater); 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; default: break; } } return InputEvent::None; }, "Keymap"); ChimesAnimation chimes{Task::Stopped}; SolidAnimation solid{Task::Running}; DrainAnimation drain{Task::Stopped}; Flashlight flashlight{Task::Stopped}; Sequencer sequencer{{ {"Idle", {"Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"}}, {"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}}, {"Interactive", {"Drain", "MPU5060", "CircadianRhythm"}}, {"Flashlight", {"Flashlight"}}, {"Gay", {"Solid", "Pulse", "Rainbow", "Rainbow"}}, {"Acid", {"Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"}}, }}; // Render all layers to the displays Renderer renderer{ {&dpy}, { &chimes, &drain, &solid, &flashlight, Static::instance(), &inputBlip, &power, } }; Renderer configRenderer{ {&dpy}, {&drain, &configDisplay, &inputBlip, &power} }; // Cycle some random colors ColorSequenceInput<9> idleCycle{{ CRGB(0, 123, 167), // Cerulean CRGB(80, 200, 120), // Emerald CRGB(207, 113, 175), // Sky Magenta CRGB(128, 0, 128), // Purple CRGB(255, 255, 255), // White CRGB(0, 255, 255), // Cyan }, "IdleColors", Task::Running}; ColorSequenceInput<7> rainbowCycle{{ CRGB(255, 0, 0), // Red CRGB(255, 127, 0), // Yellow CRGB(0, 255, 0), // Green CRGB(0, 0, 255), // Blue CRGB(128, 0, 128), // Purple }, "Rainbow", Task::Stopped}; 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; default: 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; default: 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; default: 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; default: return InputEvent::None; } } }; 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} }}; class CircadianRhythm : public InputSource { private: bool needsUpdate = true; public: CircadianRhythm() : InputSource("CircadianRhythm") {} void onStart() { needsUpdate = true; } uint8_t brightnessForTime(uint8_t hour, uint8_t minute) const { 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 = map8(curDuration, 0, duration); return lerp8by8(start.brightness, end.brightness, frac); } InputEvent read() { EVERY_N_SECONDS(60) { needsUpdate = true; } if (needsUpdate) { uint8_t hour = 0; uint8_t minute = 0; needsUpdate = false; struct tm timeinfo; Platform::getLocalTime(&timeinfo); hour = timeinfo.tm_hour; minute = timeinfo.tm_min; Log.notice("Current time: %d:%d", hour, minute); return InputEvent{InputEvent::SetBrightness, brightnessForTime(hour, minute)}; } return InputEvent{}; } }; STATIC_ALLOC(CircadianRhythm); // A special mainloop app for configuring hardware settings that reboots the // device when the user is finished. MainLoop configApp{{ Static::instance(), // Manage read/write of configuration data Static::instance(), #ifdef PLATFORM_PHOTON // Update photon telemetry Static::instance(), #endif // Read hardware inputs Static::instance(), // Map input buttons to configuration commands new ConfigInputTask(), // System logging Static::instance(), // Fill the entire display with a color, to see size &configDisplay, // Render some basic input feedback &inputBlip, // Render it all &configRenderer, }}; TaskFunc safeModeNag([]{ static uint8_t frame = 0; EVERY_N_SECONDS(30) { Log.notice("I am running in safe mode!"); } EVERY_N_MILLISECONDS(16) { frame++; for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) { leds[i] = CRGB(0, 0, 0); } for(int idx = 0; idx < 3; idx++) { uint8_t length = beatsin8(5, 3, HardwareConfig::MAX_LED_NUM, 0, idx * 5); for(int i = 0; i < length; i++) { leds[i] += CRGB(scale8(5, beatsin8(5 + i * 7, 0, 255, 0, i*3)), 0, 0); } } FastLED.show(); } }); MainLoop safeModeApp({ Static::instance(), // ESP Wifi Static::instance(), // System logging Static::instance(), // MQTT Static::instance(), // OTA Updates Static::instance(), &safeModeNag, }); // Turn on, MainLoop renderbugApp{{ Static::instance(), // Load/update graphics configuration from EEPROM Static::instance(), // Platform inputs // TODO: Merge cloud and esp wifi tasks into a common networking base #ifdef PLATFORM_PHOTON // Particle cloud status Static::instance(), // Monitor network state and provide particle API events Static::instance(), #else // ESP Wifi //Static::instance(), #endif #ifdef BOARD_ESP32 // ESP32 Bluetooth Static::instance(), #endif // System logging Static::instance(), #ifdef CONFIG_MPU5060 // Hardware drivers Static::instance(), #endif #ifdef CONFIG_BUTTONS Static::instance(), // Map buttons to events &keyMap, #endif // Pattern sequencer &sequencer, // Daily rhythm activities Static::instance(), // Periodic motion input //&randomPulse, // Periodic color inputs &idleCycle, &rainbowCycle, // Animations &chimes, &drain, &solid, &flashlight, // Update UI layer &power, Static::instance(), &inputBlip, // Render everything &renderer, // Platform telemetry // TODO: Combine some of these services into a unified telemetry API with // platform-specific backends? // Or at least, just the MQTT and watchdog ones. #ifdef PLATFORM_PHOTON // Update photon telemetry Static::instance(), // Web telemetry UI Static::instance(), // MQTT telemetry Static::instance(), // Network discovery Static::instance(), //Watchdog Static::instance(), #else // MQTT Static::instance(), // OTA Updates Static::instance(), #endif }}; MainLoop &runner = renderbugApp; // Tune in, void setup() { Platform::preSetup(); Static::instance()->setSequencer(&sequencer); Log.notice(u8"🐛 Booting Renderbug!"); Log.notice(u8"🐞 I am built for %d LEDs running on %dmA", HardwareConfig::MAX_LED_NUM, PSU_MILLIAMPS); Log.notice(u8"📡 Platform %s version %s", Platform::name(), Platform::version()); Platform::bootSplash(); Log.notice(u8"Setting timezone to -7 (PST)"); Platform::setTimezone(-7); Log.notice(u8" Setting up platform..."); Platform::setup(); Log.notice(u8"💡 Starting FastLED..."); Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM); if (Platform::bootopts.isSafeMode) { Log.notice(u8"⚠️ Starting Figment in safe mode!!!"); runner = safeModeApp; FastLED.showColor(CRGB(5, 0, 0)); FastLED.show(); } else if (Platform::bootopts.isSetup) { Log.notice(u8"🔧 Starting Figment in configuration mode..."); runner = configApp; } else { Log.notice(u8"🌌 Starting Figment..."); } Serial.flush(); runner.start(); //Log.info(u8"💽 %lu bytes of free RAM", System.freeMemory()); Log.notice(u8"🚀 Setup complete! Ready to rock and roll."); Serial.flush(); } // Drop out. void loop() { //Platform::loop(); runner.loop(); }