#include "Arduino.h" #include #include #ifndef PLATFORM_PHOTON #include #endif // !PLATFORM_PHOTON #include "Platform.h" #include "Static.h" #include "Config.h" #include "LogService.h" #include #include "animations/Power.h" #include "animations/Drain.h" #include "animations/InputBlip.h" #include "inputs/ColorCycle.h" #include "inputs/Buttons.h" #ifdef PLATFORM_PHOTON #include "platform/particle/inputs/Photon.h" #include "platform/particle/inputs/CloudStatus.h" #endif // PLATFORM_PHOTON //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; REGISTER_TASK(power); /*FigmentFunc configDisplay([](Display* dpy) { uint8_t brightness = brighten8_video(beatsin8(60)); auto coords = Static::instance()->coordMap(); for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) { 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); } } });*/ 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::Stopped); REGISTER_TASK(randomPulse); 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"); REGISTER_TASK(keyMap); class BPM : public InputSource { public: BPM() : InputSource("BPM") {} void handleEvent(const InputEvent& evt) override { if (evt.intent == InputEvent::BeatDetect) { m_nextBpm = millis(); m_timings.insert(millis()); Log.notice("%d timings", m_timings.size()); if (m_timings.size() >= 5) { updateBPM(); } } } InputEvent read() override { if (m_bpm > 0) { uint16_t now = millis(); if (now >= m_nextBpm) { m_nextBpm += m_bpm; return InputEvent{InputEvent::Beat, m_bpm}; } if (now >= m_nextLearn && m_nextLearn != 0) { m_timings.clear(); m_nextLearn = 0; } } return InputEvent{}; } private: uint16_t m_bpm = 0; uint16_t m_nextBpm = 0; uint16_t m_nextLearn = 0; Ringbuf m_timings; void updateBPM() { uint16_t avgDelta = 0; for(uint8_t i = 0; i < m_timings.size() - 1; i++) { uint16_t delta = m_timings.peek(i+1) - m_timings.peek(i); Log.notice("Timing %d Delta %d", m_timings.peek(i), delta); avgDelta += delta; } m_bpm = avgDelta / 4; m_nextLearn = m_bpm * 5 + millis(); Log.notice("BPM is now %d", m_bpm); uint16_t trash; m_timings.take(trash); } }; STATIC_ALLOC(BPM); STATIC_TASK(BPM); // Render all layers to the displays Renderer renderer{ {&dpy}, { Static::instance(), Static::instance(), Static::instance(), Static::instance(), Static::instance(), &inputBlip, &power, } }; REGISTER_TASK(renderer); Renderer configRenderer{ {&dpy}, {Static::instance(), /*&configDisplay,*/ Static::instance(), &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::Stopped}; REGISTER_TASK(idleCycle); 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}; REGISTER_TASK(rainbowCycle); /*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; if (Platform::getLocalTime(&timeinfo)) { hour = timeinfo.tm_hour; minute = timeinfo.tm_min; } else { hour = 0; minute = 0; } Log.notice("Current time: %d:%d", hour, minute); return InputEvent{InputEvent::SetBrightness, brightnessForTime(hour, minute)}; } return InputEvent{}; } }; STATIC_ALLOC(CircadianRhythm); STATIC_TASK(CircadianRhythm); // A special mainloop app for configuring hardware settings that reboots the // device when the user is finished. /*MainLoop configApp{{ Static::instance(), // Manage read/write of configuration data Static::instance(), // 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, }};*/ MainLoop configApp{std::vector()}; TaskFunc safeModeNag([]{ static uint8_t frame = 0; EVERY_N_SECONDS(30) { Log.fatal("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(); } }); #ifdef CONFIG_WIFI #include "platform/arduino/WiFiTask.h" #endif // CONFIG_WIFI #ifdef CONFIG_OTA #include "platform/arduino/OTA.h" #endif // CONFIG_OTA #ifdef CONFIG_MQTT #include "platform/arduino/MQTTTelemetry.h" #endif // CONFIG_MQTT MainLoop safeModeApp{{ Static::instance(), // System logging Static::instance(), &safeModeNag, #ifdef CONFIG_WIFI // ESP Wifi Static::instance(), #endif // CONFIG_WIFI #ifdef CONFIG_MQTT // MQTT Static::instance(), #endif // CONFIG_MQTT #ifdef CONFIG_OTA // OTA Updates Static::instance(), #endif // CONFIG_OTA }}; MainLoop* runner = &safeModeApp; void setup() { // Turn on, Platform::preSetup(); #ifdef CONFIG_MQTT Static::instance()->setSequencer(&sequencer); #endif // CONFIG_MQTT Log.notice(u8"🐛 Booting Renderbug!"); Log.notice(u8"🐞 I am built for %d LEDs running on %dmA", HardwareConfig::MAX_LED_NUM, PSU_MILLIAMPS); Log.notice(u8"📡 Platform %s version %s", Platform::name(), Platform::version()); Log.notice(u8"Setting timezone to -7 (PST)"); Platform::setTimezone(-7); Log.notice(u8" Setting up platform..."); Platform::setup(); Platform::bootSplash(); Log.notice(u8"💡 Starting FastLED..."); Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM); runner = new MainLoop{std::vector{Platform::beginTasks(), Platform::endTasks()}}; // Tune in, if (Platform::bootopts.isSafeMode) { Log.notice(u8"⚠️ Starting Figment in safe mode!!!"); runner = &safeModeApp; 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.notice(u8"💽 %lu bytes of free RAM", Platform::freeRam()); Log.notice(u8"🚀 Setup complete! Ready to rock and roll."); Serial.flush(); } // Drop out. void loop() { EVERY_N_SECONDS(5) { Log.notice("FPS: %d\tRAM: %d", FastLED.getFPS(), Platform::freeRam()); } runner->loop(); }