renderbug/src/main.cpp

542 lines
14 KiB
C++
Raw Normal View History

2021-03-29 08:10:55 +00:00
#include "Arduino.h"
#include <FastLED.h>
#include <Figments.h>
#ifndef PLATFORM_PHOTON
#include <ArduinoLog.h>
#include <NTP.h>
#endif
#include "Platform.h"
2021-03-29 08:10:55 +00:00
#include "Static.h"
#include "Config.h"
#include "colors.h"
#include "Sequencer.h"
#include "LogService.h"
2021-03-29 08:10:55 +00:00
#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 <ArduinoOTA.h>
#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<ConfigService>::instance()->coordMap());
LinearCoordinateMapping neckMap{60, 0};
Display neckDisplay(leds, HardwareConfig::MAX_LED_NUM, &neckMap);
// Setup power management
Power<MAX_BRIGHTNESS, PSU_MILLIAMPS> power;
FigmentFunc configDisplay([](Display* dpy) {
uint8_t brightness = brighten8_video(beatsin8(60));
auto coords = Static<ConfigService>::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);
2021-03-29 08:10:55 +00:00
}
void loop() override {
if (m_online) {
ArduinoOTA.handle();
}
2021-03-29 08:10:55 +00:00
BufferedInputSource::loop();
}
void handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::NetworkStatus) {
Log.notice("Booting OTA");
m_online = true;
ArduinoOTA.begin();
}
}
2021-03-29 08:10:55 +00:00
private:
bool m_online = false;
2021-03-29 08:10:55 +00:00
static void s_onStart() {
Log.notice("OTA Start!");
2021-03-29 08:10:55 +00:00
Static<ArduinoOTAUpdater>::instance()->setEvent(InputEvent::FirmwareUpdate);
}
static void s_onProgress(unsigned int progress, unsigned int total) {
Log.notice("OTA Progress! %d / %d", progress, total);
2021-03-29 08:10:55 +00:00
Static<ArduinoOTAUpdater>::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress});
}
};
STATIC_ALLOC(ArduinoOTAUpdater);
2021-03-29 08:10:55 +00:00
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;
2021-03-29 08:10:55 +00:00
}
}
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<UpdateStatus>::instance(),
&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;
default:
break;
2021-03-29 08:10:55 +00:00
}
}
}
private:
InputEvent::Intent m_currentIntent = InputEvent::SetDisplayLength;
void decrement() {
int current = 0;
switch (m_currentIntent) {
case InputEvent::SetDisplayLength:
current = Static<ConfigService>::instance()->coordMap()->pixelCount;
break;
case InputEvent::SetDisplayOffset:
current = Static<ConfigService>::instance()->coordMap()->startPixel;
break;
default:
break;
2021-03-29 08:10:55 +00:00
}
setEvent(InputEvent{m_currentIntent, current - 1});
}
void increment() {
int current = 0;
switch (m_currentIntent) {
case InputEvent::SetDisplayLength:
current = Static<ConfigService>::instance()->coordMap()->pixelCount;
break;
case InputEvent::SetDisplayOffset:
current = Static<ConfigService>::instance()->coordMap()->startPixel;
break;
default:
break;
2021-03-29 08:10:55 +00:00
}
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;
2021-03-29 08:10:55 +00:00
}
}
};
struct ScheduleEntry {
uint8_t hour;
uint8_t brightness;
};
std::array<ScheduleEntry, 10> 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);
}
class CircadianRhythm : public InputSource {
private:
bool needsUpdate = true;
public:
CircadianRhythm() : InputSource("CircadianRhythm") {}
2021-03-29 08:10:55 +00:00
InputEvent read() {
2021-03-29 08:10:55 +00:00
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{};
2021-03-29 08:10:55 +00:00
}
};
STATIC_ALLOC(CircadianRhythm);
2021-03-29 08:10:55 +00:00
// 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<ConfigService>::instance(),
#ifdef PLATFORM_PHOTON
// Update photon telemetry
Static<PhotonTelemetry>::instance(),
#endif
// Read hardware inputs
Static<Buttons>::instance(),
2021-03-29 08:10:55 +00:00
// Map input buttons to configuration commands
new ConfigInputTask(),
// System logging
Static<LogService>::instance(),
2021-03-29 08:10:55 +00:00
// 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
2021-03-29 08:10:55 +00:00
Static<ConfigService>::instance(),
// Platform inputs
// TODO: Merge cloud and esp wifi tasks into a common networking base
2021-03-29 08:10:55 +00:00
#ifdef PLATFORM_PHOTON
// Particle cloud status
Static<CloudStatus>::instance(),
// Monitor network state and provide particle API events
Static<PhotonInput>::instance(),
#else
// ESP Wifi
Static<WiFiTask>::instance(),
#endif
// System logging
Static<LogService>::instance(),
2021-03-29 08:10:55 +00:00
// Hardware drivers
Static<MPU5060>::instance(),
Static<Buttons>::instance(),
2021-03-29 08:10:55 +00:00
// Map buttons to events
&keyMap,
// Pattern sequencer
&sequencer,
// Daily rhythm activities
Static<CircadianRhythm>::instance(),
2021-03-29 08:10:55 +00:00
// Periodic motion input
&randomPulse,
// Periodic color inputs
&noisebridgeCycle,
&kierynCycle,
&rainbowCycle,
&hackerbotsCycle,
// Animations
&chimes,
&drain,
&solid,
&flashlight,
// Update UI layer
&power,
Static<UpdateStatus>::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.
2021-03-29 08:10:55 +00:00
#ifdef PLATFORM_PHOTON
// Update photon telemetry
Static<PhotonTelemetry>::instance(),
// Web telemetry UI
Static<WebTelemetry>::instance(),
// MQTT telemetry
Static<MQTTTelemetry>::instance(),
// Network discovery
Static<MDNSService>::instance(),
//Watchdog
Static<Watchdog>::instance(),
#else
// MQTT
Static<MQTTTelemetry>::instance(),
// OTA Updates
Static<ArduinoOTAUpdater>::instance(),
2021-03-29 08:10:55 +00:00
#endif
}};
MainLoop &runner = renderbugApp;
// Tune in,
void setup() {
Platform::preSetup();
Static<MQTTTelemetry>::instance()->setSequencer(&sequencer);
2021-03-29 08:10:55 +00:00
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);
2021-03-29 08:10:55 +00:00
Log.notice(u8" Setting up platform...");
Platform::setup();
2021-03-29 08:10:55 +00:00
Log.notice(u8"💡 Starting FastLED...");
Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM);
2021-03-29 08:10:55 +00:00
if (Platform::bootopts.isSetup) {
2021-03-29 08:10:55 +00:00
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() {
runner.loop();
}