#define RENDERBUG_VERSION 1 #include "Arduino.h" #include #include #ifndef PLATFORM_PHOTON #include #include #endif #include "BootOptions.h" #include "Static.h" #include "Config.h" #include "colors.h" #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/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/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()); LinearCoordinateMapping neckMap{60, 0}; Display neckDisplay(leds, HardwareConfig::MAX_LED_NUM, &neckMap); // Setup power management Power power; // 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 < 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", 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).onProgress(&ArduinoOTAUpdater::s_onProgress); } void loop() override { ArduinoOTA.handle(); BufferedInputSource::loop(); } private: static void s_onStart() { Static::instance()->setEvent(InputEvent::FirmwareUpdate); } static void s_onProgress(unsigned int progress, unsigned int total) { Static::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress}); } }; 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; }, "Keymap"); ChimesAnimation chimes{Task::Stopped}; SolidAnimation solid{Task::Running}; DrainAnimation drain{Task::Stopped}; Flashlight flashlight{Task::Stopped}; Sequencer sequencer{{ {"Idle", {"Solid", "MPU5060", "Pulse", "Hackerbots", "Kieryn", "CircadianRhythm"}}, {"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}}, {"Interactive", {"Drain", "CircadianRhythm"}}, {"Flashlight", {"Flashlight"}}, {"Nightlight", {"Drain", "Pulse", "Noisebridge"}}, {"Gay", {"Solid", "Pulse", "Rainbow", "Hackerbots", "Kieryn"}}, {"Acid", {"Chimes", "Pulse", "MPU5060", "Hackerbots", "Rainbow"}}, }}; // Render all layers to the displays Renderer renderer{ //{&dpy, &neckDisplay}, {&dpy}, { &chimes, &drain, &solid, &flashlight, Static::instance(), &displayClip, &inputBlip, &power, } }; Renderer configRenderer{ {&dpy}, {&drain, &configDisplay, &inputBlip, &power} }; // Cycle some random colors ColorSequenceInput<7> noisebridgeCycle{{colorForName("Red").rgb}, "Noisebridge", Task::Stopped}; ColorSequenceInput<13> kierynCycle{{ CRGB(0, 123, 167), // Cerulean CRGB(80, 200, 120), // Emerald CRGB(207, 113, 175), // Sky Magenta }, "Kieryn", Task::Running}; ColorSequenceInput<7> rainbowCycle{{ colorForName("Red").rgb, colorForName("Orange").rgb, colorForName("Yellow").rgb, colorForName("Green").rgb, colorForName("Blue").rgb, colorForName("Purple").rgb, colorForName("White").rgb, }, "Rainbow", Task::Stopped}; ColorSequenceInput<7> hackerbotsCycle{{ CRGB(128, 0, 128), // Purple CRGB(255, 255, 255), // White CRGB(0, 255, 255), // Cyan }, "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); } WiFiUDP wifiUdp; NTP ntp(wifiUdp); InputFunc circadianRhythm([]() { static bool needsUpdate = true; EVERY_N_SECONDS(60) { needsUpdate = true; } #ifdef PLATFORM_PHOTON if (Time.isValid() && needsUpdate) { needsUpdate = false; return InputEvent{InputEvent::SetBrightness, brightnessForTime(Time.hour(), Time.minute())}; } #else ntp.update(); if (needsUpdate) { needsUpdate = false; return InputEvent{InputEvent::SetBrightness, brightnessForTime(ntp.hours(), ntp.minutes())}; } #endif 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(), // Map input buttons to configuration commands new ConfigInputTask(), // Fill the entire display with a color, to see size &configDisplay, // Render some basic input feedback &inputBlip, // Render it all &configRenderer, }}; // Turn on, 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(), #else // ESP Wifi 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, &kierynCycle, &rainbowCycle, &hackerbotsCycle, // Animations &chimes, &drain, &solid, &flashlight, // Update UI layer &power, &displayClip, Static::instance(), &inputBlip, // Render everything &renderer, // Platform telemetry #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(), #endif }}; MainLoop &runner = renderbugApp; #ifdef PLATFORM_PHOTON STARTUP(BootOptions::initPins()); retained BootOptions bootopts; #else BootOptions bootopts; void printNewline(Print* logOutput) { logOutput->print("\r\n"); } #endif // Tune in, void setup() { Serial.begin(115200); #ifdef PLATFORM_PHOTON System.enableFeature(FEATURE_RETAINED_MEMORY); if (bootopts.isFlash) { System.dfu(); } if (bootopts.isSerial) { bootopts.waitForRelease(); while(!Serial.isConnected()) { Particle.process(); } //Log.info("\xf0\x9f\x94\x8c Serial connected"); } #else Log.begin(LOG_LEVEL_VERBOSE, &Serial, true); Log.setSuffix(printNewline); Log.notice("Test log?"); #endif Log.notice(u8"🐛 Booting Renderbug!"); Log.notice(u8"🐞 I am built for %d LEDs running on %dmA", HardwareConfig::MAX_LED_NUM, PSU_MILLIAMPS); #ifdef PLATFORM_PHOTON Log.notice(u8"📡 Particle version %s", System.version().c_str()); #endif Log.notice(u8" Boot pin configuration:"); Log.notice(u8" 2: Setup - %d", bootopts.isSetup); Log.notice(u8" 3: Serial - %d", bootopts.isSerial); Log.notice(u8" 4: Flash - %d", bootopts.isFlash); //Log.info(u8" Setting timezone to UTC-7"); //Time.zone(-7); Log.notice(u8"💡 Starting FastLED..."); #ifdef PLATFORM_PHOTON FastLED.addLeds(leds, HardwareConfig::MAX_LED_NUM); #else FastLED.addLeds(leds, HardwareConfig::MAX_LED_NUM); #endif if (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(); ntp.begin(); ArduinoOTA.begin(); } // Drop out. void loop() { runner.loop(); }