port to platformio
This commit is contained in:
558
src/main.cpp
Normal file
558
src/main.cpp
Normal file
@@ -0,0 +1,558 @@
|
||||
#define RENDERBUG_VERSION 1
|
||||
|
||||
#include "Arduino.h"
|
||||
|
||||
#include <FastLED.h>
|
||||
#include <Figments.h>
|
||||
|
||||
#ifndef PLATFORM_PHOTON
|
||||
#include <ArduinoLog.h>
|
||||
#include <NTP.h>
|
||||
#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 <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;
|
||||
|
||||
// Clip the display at whatever is configured while still showing over-paints
|
||||
FigmentFunc displayClip([](Display* dpy) {
|
||||
auto coords = Static<ConfigService>::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<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).onProgress(&ArduinoOTAUpdater::s_onProgress);
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
ArduinoOTA.handle();
|
||||
BufferedInputSource::loop();
|
||||
}
|
||||
private:
|
||||
static void s_onStart() {
|
||||
Static<ArduinoOTAUpdater>::instance()->setEvent(InputEvent::FirmwareUpdate);
|
||||
}
|
||||
|
||||
static void s_onProgress(unsigned int progress, unsigned int total) {
|
||||
Static<ArduinoOTAUpdater>::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<UpdateStatus>::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<ConfigService>::instance()->coordMap()->pixelCount;
|
||||
break;
|
||||
case InputEvent::SetDisplayOffset:
|
||||
current = Static<ConfigService>::instance()->coordMap()->startPixel;
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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<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);
|
||||
}
|
||||
|
||||
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<ConfigService>::instance(),
|
||||
|
||||
#ifdef PLATFORM_PHOTON
|
||||
// Update photon telemetry
|
||||
Static<PhotonTelemetry>::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<ConfigService>::instance(),
|
||||
|
||||
// Platform inputs
|
||||
#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
|
||||
|
||||
// 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<UpdateStatus>::instance(),
|
||||
&inputBlip,
|
||||
|
||||
// Render everything
|
||||
&renderer,
|
||||
|
||||
// Platform telemetry
|
||||
#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(),
|
||||
#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<NEOPIXEL, 6>(leds, HardwareConfig::MAX_LED_NUM);
|
||||
#else
|
||||
FastLED.addLeds<WS2812B, 13, RGB>(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();
|
||||
}
|
Reference in New Issue
Block a user