2021-03-28 01:19:55 +00:00
|
|
|
#define RENDERBUG_VERSION 1
|
|
|
|
#define PLATFORM_PHOTON
|
|
|
|
//#define PLATFORM_ESP2800
|
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
#include "FastLED/FastLED.h"
|
|
|
|
#include "Figments/Figments.h"
|
2018-12-27 04:35:58 +00:00
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
#include "Static.h"
|
|
|
|
#include "Config.h"
|
|
|
|
#include "colors.h"
|
2018-12-27 04:35:58 +00:00
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
#include "WebTelemetry.cpp"
|
|
|
|
#include "MDNSService.cpp"
|
|
|
|
#include "Sequencer.h"
|
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
#include "animations/Power.cpp"
|
|
|
|
#include "animations/SolidAnimation.cpp"
|
|
|
|
#include "animations/Chimes.cpp"
|
|
|
|
#include "animations/Flashlight.cpp"
|
2021-03-28 01:19:55 +00:00
|
|
|
#include "animations/Drain.cpp"
|
2019-05-10 05:17:29 +00:00
|
|
|
#include "animations/UpdateStatus.h"
|
|
|
|
|
|
|
|
#include "inputs/ColorCycle.h"
|
|
|
|
#include "inputs/Buttons.h"
|
2021-03-28 01:19:55 +00:00
|
|
|
#include "inputs/MPU6050.h"
|
|
|
|
|
|
|
|
#ifdef PLATFORM_PHOTON
|
|
|
|
#include "platform/particle/inputs/Photon.h"
|
|
|
|
#include "platform/particle/inputs/CloudStatus.h"
|
|
|
|
#include "platform/particle/PhotonTelemetry.h"
|
|
|
|
#endif
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
SerialLogHandler logHandler;
|
|
|
|
|
|
|
|
using namespace NSFastLED;
|
|
|
|
|
|
|
|
#define MAX_BRIGHTNESS 255
|
2021-03-28 01:19:55 +00:00
|
|
|
//#define PSU_MILLIAMPS 4800
|
|
|
|
//#define PSU_MILLIAMPS 500
|
2021-03-29 00:18:03 +00:00
|
|
|
//#define PSU_MILLIAMPS 1000
|
|
|
|
#define PSU_MILLIAMPS 2000
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Enable system thread, so rendering happens while booting
|
|
|
|
SYSTEM_THREAD(ENABLED);
|
|
|
|
|
|
|
|
// Setup FastLED and the display
|
2021-03-28 01:19:55 +00:00
|
|
|
CRGB leds[HardwareConfig::MAX_LED_NUM];
|
|
|
|
Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static<ConfigService>::instance()->coordMap());
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
LinearCoordinateMapping neckMap{60, 0};
|
2021-03-28 01:19:55 +00:00
|
|
|
Display neckDisplay(leds, HardwareConfig::MAX_LED_NUM, &neckMap);
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Setup power management
|
|
|
|
Power<MAX_BRIGHTNESS, PSU_MILLIAMPS> power;
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
// 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;
|
2019-05-10 05:17:29 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
});
|
2019-05-10 05:17:29 +00:00
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
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);
|
2019-05-10 05:17:29 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
});
|
2019-05-10 05:17:29 +00:00
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
class InputBlip: public Figment {
|
|
|
|
public:
|
2021-03-28 21:50:31 +00:00
|
|
|
InputBlip() : Figment("InputBlip", Task::Stopped) {}
|
2019-05-10 05:17:29 +00:00
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
void handleEvent(const InputEvent& evt) override {
|
|
|
|
if (evt.intent != InputEvent::None) {
|
|
|
|
m_time = qadd8(m_time, 5);
|
|
|
|
}
|
2019-05-10 05:17:29 +00:00
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
void loop() override {
|
|
|
|
if (m_time > 0) {
|
|
|
|
m_time--;
|
2019-05-10 05:17:29 +00:00
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
|
|
|
void render(Display* dpy) const override {
|
|
|
|
if (m_time > 0) {
|
|
|
|
dpy->pixelAt(0) = CRGB(0, brighten8_video(ease8InOutApprox(m_time)), 0);
|
2019-05-10 05:17:29 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
private:
|
|
|
|
uint8_t m_time = 0;
|
2019-05-10 05:17:29 +00:00
|
|
|
};
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
InputBlip inputBlip;
|
|
|
|
|
|
|
|
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)};
|
|
|
|
}
|
2019-05-10 05:17:29 +00:00
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
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;
|
|
|
|
}
|
2019-05-10 05:17:29 +00:00
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
return InputEvent::None;
|
2019-05-10 05:17:29 +00:00
|
|
|
});
|
2018-12-27 04:35:58 +00:00
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
ChimesAnimation chimes{Task::Stopped};
|
2021-03-28 01:19:55 +00:00
|
|
|
SolidAnimation solid{Task::Running};
|
|
|
|
DrainAnimation drain{Task::Stopped};
|
2019-05-10 05:17:29 +00:00
|
|
|
Flashlight flashlight{Task::Stopped};
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
Sequencer sequencer{{
|
2021-03-28 21:50:31 +00:00
|
|
|
{"Idle", {"Solid", "MPU5060", "Pulse", "Hackerbots", "Kieryn", "CircadianRhythm"}},
|
|
|
|
{"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}},
|
|
|
|
{"Interactive", {"Drain", "CircadianRhythm"}},
|
2021-03-28 01:19:55 +00:00
|
|
|
{"Flashlight", {"Flashlight"}},
|
2021-03-28 21:50:31 +00:00
|
|
|
{"Nightlight", {"Drain", "Pulse", "Noisebridge"}},
|
|
|
|
{"Gay", {"Solid", "Pulse", "Rainbow", "Hackerbots", "Kieryn"}},
|
|
|
|
{"Acid", {"Chimes", "Pulse", "MPU5060", "Hackerbots", "Rainbow"}},
|
2021-03-28 01:19:55 +00:00
|
|
|
}};
|
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Render all layers to the displays
|
|
|
|
Renderer renderer{
|
|
|
|
//{&dpy, &neckDisplay},
|
|
|
|
{&dpy},
|
|
|
|
{
|
|
|
|
&chimes,
|
|
|
|
&drain,
|
|
|
|
&solid,
|
|
|
|
&flashlight,
|
|
|
|
Static<UpdateStatus>::instance(),
|
|
|
|
&displayClip,
|
2021-03-28 01:19:55 +00:00
|
|
|
&inputBlip,
|
2019-05-10 05:17:29 +00:00
|
|
|
&power,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
Renderer configRenderer{
|
|
|
|
{&dpy},
|
|
|
|
{&drain, &configDisplay, &inputBlip, &power}
|
|
|
|
};
|
|
|
|
|
|
|
|
MDNSService mdnsService;
|
|
|
|
|
|
|
|
class OnlineTaskMixin : public virtual Loopable {
|
|
|
|
public:
|
|
|
|
void handleEvent(const InputEvent &evt) override {
|
|
|
|
if (evt.intent == InputEvent::NetworkStatus) {
|
2021-03-28 21:50:31 +00:00
|
|
|
m_online = evt.asInt();
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
|
|
|
if (m_online) {
|
|
|
|
handleEventOnline(evt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void handleEventOnline(const InputEvent &evt) = 0;
|
|
|
|
|
|
|
|
void loop() override {
|
|
|
|
if (m_online) {
|
|
|
|
loopOnline();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void loopOnline() = 0;
|
|
|
|
|
|
|
|
private:
|
|
|
|
bool m_online = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
#include "MQTT/MQTT.h"
|
|
|
|
|
2021-03-28 21:50:31 +00:00
|
|
|
class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
|
2021-03-28 01:19:55 +00:00
|
|
|
public:
|
2021-03-28 21:50:31 +00:00
|
|
|
MQTTTelemetry() : BufferedInputSource("MQTT"), m_client("relay.malloc.hackerbots.net", 1883, 512, 15, MQTTTelemetry::s_callback, true) {
|
2021-03-28 01:19:55 +00:00
|
|
|
strcpy(m_deviceName, System.deviceID().c_str());
|
|
|
|
}
|
|
|
|
|
2021-03-28 21:50:31 +00:00
|
|
|
void loop() override {
|
|
|
|
BufferedInputSource::loop();
|
|
|
|
OnlineTaskMixin::loop();
|
|
|
|
}
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
void handleEventOnline(const InputEvent& event) override {
|
|
|
|
char response[255];
|
|
|
|
if (event.intent == InputEvent::SetPower) {
|
|
|
|
JSONBufferWriter writer(response, sizeof(response));
|
|
|
|
writer.beginObject();
|
|
|
|
writer.name("state").value(event.asInt() == 0 ? "OFF" : "ON");
|
|
|
|
writer.endObject();
|
|
|
|
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0;
|
|
|
|
m_client.publish(m_statTopic, response, MQTT::QOS1);
|
|
|
|
} else if (event.intent == InputEvent::SetBrightness) {
|
|
|
|
JSONBufferWriter writer(response, sizeof(response));
|
|
|
|
writer.beginObject();
|
|
|
|
writer.name("brightness").value(event.asInt());
|
|
|
|
writer.endObject();
|
|
|
|
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0;
|
|
|
|
m_client.publish(m_statTopic, response, MQTT::QOS1);
|
|
|
|
} else if (event.intent == InputEvent::SetColor) {
|
|
|
|
JSONBufferWriter writer(response, sizeof(response));
|
|
|
|
writer.beginObject();
|
|
|
|
writer.name("color").beginObject();
|
|
|
|
CRGB rgb = event.asRGB();
|
|
|
|
writer.name("r").value(rgb.r);
|
|
|
|
writer.name("g").value(rgb.g);
|
|
|
|
writer.name("b").value(rgb.b);
|
|
|
|
writer.endObject();
|
|
|
|
writer.endObject();
|
|
|
|
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0;
|
|
|
|
m_client.publish(m_statTopic, response, MQTT::QOS1);
|
2021-03-28 21:50:31 +00:00
|
|
|
} else if (event.intent == InputEvent::SetPattern) {
|
|
|
|
JSONBufferWriter writer(response, sizeof(response));
|
|
|
|
writer.beginObject();
|
|
|
|
writer.name("effect").value(event.asString());
|
|
|
|
writer.endObject();
|
|
|
|
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0;
|
|
|
|
m_client.publish(m_statTopic, response, MQTT::QOS1);
|
|
|
|
} else {
|
|
|
|
if (m_lastIntent != event.intent) {
|
|
|
|
m_lastIntent = event.intent;
|
|
|
|
JSONBufferWriter writer(response, sizeof(response));
|
|
|
|
writer.beginObject();
|
|
|
|
writer.name("intent").value(event.intent);
|
|
|
|
writer.endObject();
|
|
|
|
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0;
|
|
|
|
m_client.publish("renderbug/events", response, MQTT::QOS1);
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void loopOnline() override {
|
|
|
|
if (m_client.isConnected()) {
|
|
|
|
m_client.loop();
|
|
|
|
EVERY_N_SECONDS(10) {
|
|
|
|
char heartbeatBuf[255];
|
|
|
|
JSONBufferWriter writer(heartbeatBuf, sizeof(heartbeatBuf));
|
|
|
|
writer.beginObject();
|
2021-03-28 21:50:31 +00:00
|
|
|
writer.name("fps").value(FastLED.getFPS());
|
2021-03-28 01:19:55 +00:00
|
|
|
writer.name("os_version").value(System.version());
|
|
|
|
writer.name("free_ram").value((unsigned int)System.freeMemory());
|
|
|
|
//writer.name("uptime").value(System.uptime());
|
|
|
|
/*WiFiSignal sig = WiFi.RSSI();
|
|
|
|
writer.name("strength").value(sig.getStrength());
|
|
|
|
writer.name("quality").value(sig.getQuality());*/
|
|
|
|
writer.name("RSSI").value(WiFi.RSSI());
|
|
|
|
writer.name("SSID").value(WiFi.SSID());
|
|
|
|
//writer.name("MAC").value(WiFi.macAddress());
|
|
|
|
writer.name("localip").value(WiFi.localIP().toString());
|
2021-03-29 00:18:03 +00:00
|
|
|
writer.name("startPixel").value(Static<ConfigService>::instance()->coordMap()->startPixel);
|
|
|
|
writer.name("pixelCount").value(Static<ConfigService>::instance()->coordMap()->pixelCount);
|
2021-03-28 01:19:55 +00:00
|
|
|
writer.endObject();
|
|
|
|
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0;
|
|
|
|
m_client.publish(m_attrTopic, heartbeatBuf);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
m_client.connect("renderbug_" + String(m_deviceName) + "_" + String(Time.now()));
|
|
|
|
char response[512];
|
|
|
|
JSONBufferWriter writer(response, sizeof(response));
|
|
|
|
|
|
|
|
String devTopic = String("homeassistant/light/renderbug/") + m_deviceName;
|
|
|
|
writer.beginObject();
|
|
|
|
writer.name("~").value(devTopic.c_str());
|
|
|
|
writer.name("name").value("Renderbug");
|
|
|
|
writer.name("unique_id").value(m_deviceName);
|
|
|
|
writer.name("cmd_t").value("~/set");
|
|
|
|
writer.name("stat_t").value("~/set");
|
|
|
|
writer.name("json_attr_t").value("~/attributes");
|
|
|
|
writer.name("schema").value("json");
|
|
|
|
writer.name("brightness").value(true);
|
|
|
|
writer.name("rgb").value(true);
|
|
|
|
writer.name("ret").value(true);
|
|
|
|
|
|
|
|
writer.name("dev").beginObject();
|
|
|
|
writer.name("name").value("Renderbug");
|
|
|
|
writer.name("mdl").value("Renderbug");
|
|
|
|
writer.name("sw").value(RENDERBUG_VERSION);
|
|
|
|
writer.name("mf").value("Phong Robotics");
|
|
|
|
|
|
|
|
writer.name("ids").beginArray();
|
|
|
|
writer.value(m_deviceName);
|
|
|
|
writer.endArray();
|
|
|
|
|
|
|
|
writer.endObject();
|
|
|
|
|
|
|
|
writer.name("fx_list").beginArray();
|
|
|
|
for(const Sequencer::Scene& scene : sequencer.scenes()) {
|
|
|
|
writer.value(scene.name);
|
|
|
|
}
|
|
|
|
writer.endArray();
|
|
|
|
|
|
|
|
writer.endObject();
|
|
|
|
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0;
|
|
|
|
|
|
|
|
String configTopic = devTopic + "/config";
|
|
|
|
m_client.publish(configTopic, response, true);
|
|
|
|
|
|
|
|
String statTopic = devTopic + "/state";
|
|
|
|
String cmdTopic = devTopic + "/set";
|
|
|
|
String attrTopic = devTopic + "/attributes";
|
|
|
|
strcpy(m_statTopic, statTopic.c_str());
|
|
|
|
strcpy(m_attrTopic, attrTopic.c_str());
|
|
|
|
strcpy(m_cmdTopic, cmdTopic.c_str());
|
|
|
|
m_client.subscribe(m_cmdTopic, MQTT::QOS1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
void callback(char* topic, byte* payload, unsigned int length) {
|
|
|
|
MainLoop::instance()->dispatch(InputEvent::NetworkActivity);
|
|
|
|
if (!strcmp(topic, m_cmdTopic)) {
|
2021-03-28 21:50:31 +00:00
|
|
|
JSONValue cmd = JSONValue::parseCopy((char*)payload, length);
|
2021-03-28 01:19:55 +00:00
|
|
|
JSONObjectIterator cmdIter(cmd);
|
|
|
|
while(cmdIter.next()) {
|
|
|
|
if (cmdIter.name() == "state" && cmdIter.value().toString() == "ON") {
|
2021-03-28 21:50:31 +00:00
|
|
|
setEvent({InputEvent::SetPower, 1});
|
2021-03-28 01:19:55 +00:00
|
|
|
} else if (cmdIter.name() == "state" && cmdIter.value().toString() == "OFF") {
|
2021-03-28 21:50:31 +00:00
|
|
|
setEvent({InputEvent::SetPower, 0});
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (cmdIter.name() == "color") {
|
|
|
|
JSONObjectIterator colorIter(cmdIter.value());
|
|
|
|
uint8_t r, g, b;
|
|
|
|
while(colorIter.next()) {
|
|
|
|
if (colorIter.name() == "r") {
|
|
|
|
r = colorIter.value().toInt();
|
|
|
|
} else if (colorIter.name() == "g") {
|
|
|
|
g = colorIter.value().toInt();
|
|
|
|
} else if (colorIter.name() == "b") {
|
|
|
|
b = colorIter.value().toInt();
|
|
|
|
}
|
|
|
|
}
|
2021-03-28 21:50:31 +00:00
|
|
|
setEvent(InputEvent{InputEvent::SetColor, CRGB{r, g, b}});
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (cmdIter.name() == "brightness") {
|
|
|
|
uint8_t brightness = cmdIter.value().toInt();
|
2021-03-28 21:50:31 +00:00
|
|
|
setEvent(InputEvent{InputEvent::SetBrightness, brightness});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cmdIter.name() == "effect") {
|
|
|
|
strcpy(m_patternBuf, (const char*) cmdIter.value().toString());
|
|
|
|
setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf});
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
2021-03-29 00:18:03 +00:00
|
|
|
|
|
|
|
if (cmdIter.name() == "pixelCount") {
|
|
|
|
setEvent(InputEvent{InputEvent::SetDisplayLength, cmdIter.value().toInt()});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cmdIter.name() == "startPixel") {
|
|
|
|
setEvent(InputEvent{InputEvent::SetDisplayOffset, cmdIter.value().toInt()});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cmdIter.name() == "save") {
|
|
|
|
setEvent(InputEvent{InputEvent::SaveConfigurationRequest});
|
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void s_callback(char* topic, byte* payload, unsigned int length) {
|
2021-03-28 21:50:31 +00:00
|
|
|
Static<MQTTTelemetry>::instance()->callback(topic, payload, length);
|
2021-03-28 01:19:55 +00:00
|
|
|
};
|
2021-03-28 21:50:31 +00:00
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
MQTT m_client;
|
2021-03-28 21:50:31 +00:00
|
|
|
InputEvent::Intent m_lastIntent;
|
2021-03-28 01:19:55 +00:00
|
|
|
char m_deviceName[100];
|
|
|
|
char m_statTopic[100];
|
|
|
|
char m_attrTopic[100];
|
|
|
|
char m_cmdTopic[100];
|
2021-03-28 21:50:31 +00:00
|
|
|
char m_patternBuf[48];
|
2021-03-28 01:19:55 +00:00
|
|
|
};
|
|
|
|
|
2021-03-28 21:50:31 +00:00
|
|
|
STATIC_ALLOC(MQTTTelemetry);
|
2021-03-28 01:19:55 +00:00
|
|
|
|
|
|
|
WebTelemetry webTelemetry(sequencer);
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Cycle some random colors
|
|
|
|
ColorSequenceInput<7> noisebridgeCycle{{colorForName("Red").rgb}, "Noisebridge", Task::Stopped};
|
|
|
|
|
|
|
|
ColorSequenceInput<13> kierynCycle{{
|
|
|
|
colorForName("Cerulean").rgb,
|
|
|
|
colorForName("Electric Purple").rgb,
|
|
|
|
colorForName("Emerald").rgb,
|
|
|
|
colorForName("Sky Magenta").rgb
|
2021-03-28 21:50:31 +00:00
|
|
|
}, "Kieryn", Task::Stopped};
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
ColorSequenceInput<7> rainbowCycle{{
|
|
|
|
colorForName("Red").rgb,
|
|
|
|
colorForName("Orange").rgb,
|
|
|
|
colorForName("Yellow").rgb,
|
|
|
|
colorForName("Green").rgb,
|
|
|
|
colorForName("Blue").rgb,
|
|
|
|
colorForName("Purple").rgb,
|
|
|
|
colorForName("White").rgb,
|
2021-03-28 01:19:55 +00:00
|
|
|
}, "Rainbow", Task::Stopped};
|
|
|
|
|
|
|
|
ColorSequenceInput<7> hackerbotsCycle{{
|
|
|
|
colorForName("Purple").rgb,
|
|
|
|
colorForName("White").rgb,
|
|
|
|
colorForName("Cyan").rgb,
|
2021-03-28 21:50:31 +00:00
|
|
|
}, "Hackerbots", Task::Stopped};
|
2021-03-28 01:19:55 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
InputFunc circadianRhythm([]() {
|
|
|
|
static Phase lastPhase = Null;
|
|
|
|
static bool needsUpdate = true;
|
2021-03-28 21:50:31 +00:00
|
|
|
if (Time.isValid()) {
|
|
|
|
EVERY_N_SECONDS(60) {
|
|
|
|
needsUpdate = true;
|
|
|
|
}
|
|
|
|
if (needsUpdate) {
|
2021-03-29 00:18:03 +00:00
|
|
|
needsUpdate = false;
|
2021-03-28 01:19:55 +00:00
|
|
|
return InputEvent{InputEvent::SetBrightness, brightnessForTime(Time.hour(), Time.minute())};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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,
|
|
|
|
}};
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Turn on,
|
2021-03-28 01:19:55 +00:00
|
|
|
MainLoop renderbugApp{{
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Load/update graphics configuration from EEPROM and Particle
|
|
|
|
Static<ConfigService>::instance(),
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
// Platform inputs
|
|
|
|
#ifdef PLATFORM_PHOTON
|
2019-05-10 05:17:29 +00:00
|
|
|
// Particle cloud status
|
|
|
|
Static<CloudStatus>::instance(),
|
|
|
|
|
|
|
|
// Monitor network state and provide particle API events
|
|
|
|
Static<PhotonInput>::instance(),
|
2021-03-28 01:19:55 +00:00
|
|
|
#endif
|
2019-05-10 05:17:29 +00:00
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
// Hardware drivers
|
2019-05-10 05:17:29 +00:00
|
|
|
new MPU5060(),
|
|
|
|
new Buttons(),
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
// Map buttons to events
|
|
|
|
&keyMap,
|
|
|
|
|
|
|
|
// Pattern sequencer
|
|
|
|
&sequencer,
|
|
|
|
|
|
|
|
// Daily rhythm activities
|
|
|
|
&circadianRhythm,
|
|
|
|
|
|
|
|
// Periodic motion input
|
|
|
|
&randomPulse,
|
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
// Periodic color inputs
|
|
|
|
&noisebridgeCycle,
|
|
|
|
&kierynCycle,
|
|
|
|
&rainbowCycle,
|
2021-03-28 01:19:55 +00:00
|
|
|
&hackerbotsCycle,
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Animations
|
|
|
|
&chimes,
|
|
|
|
&drain,
|
|
|
|
&solid,
|
|
|
|
&flashlight,
|
|
|
|
|
|
|
|
// Update UI layer
|
|
|
|
&power,
|
|
|
|
&displayClip,
|
|
|
|
Static<UpdateStatus>::instance(),
|
2021-03-28 01:19:55 +00:00
|
|
|
&inputBlip,
|
2019-05-10 05:17:29 +00:00
|
|
|
|
|
|
|
// Render everything
|
|
|
|
&renderer,
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
// Platform telemetry
|
|
|
|
#ifdef PLATFORM_PHOTON
|
2019-05-10 05:17:29 +00:00
|
|
|
// Update photon telemetry
|
2021-03-28 01:19:55 +00:00
|
|
|
Static<PhotonTelemetry>::instance(),
|
|
|
|
|
|
|
|
// Web telemetry UI
|
|
|
|
&webTelemetry,
|
|
|
|
|
|
|
|
// MQTT telemetry
|
2021-03-28 21:50:31 +00:00
|
|
|
Static<MQTTTelemetry>::instance(),
|
2021-03-28 01:19:55 +00:00
|
|
|
|
|
|
|
// Network discovery
|
|
|
|
&mdnsService,
|
|
|
|
#endif
|
2019-05-10 05:17:29 +00:00
|
|
|
}};
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
MainLoop &runner = renderbugApp;
|
|
|
|
|
2021-03-28 21:50:31 +00:00
|
|
|
retained bool LAST_BOOT_WAS_FLASH;
|
|
|
|
retained bool LAST_BOOT_WAS_SERIAL;
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
struct BootOptions {
|
2021-03-28 21:50:31 +00:00
|
|
|
static void initPins() {
|
2021-03-28 01:19:55 +00:00
|
|
|
pinMode(2, INPUT_PULLDOWN);
|
|
|
|
pinMode(3, INPUT_PULLDOWN);
|
|
|
|
pinMode(4, INPUT_PULLDOWN);
|
2021-03-28 21:50:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
BootOptions() {
|
2021-03-28 01:19:55 +00:00
|
|
|
isSetup = digitalRead(2) == HIGH;
|
2021-03-28 21:50:31 +00:00
|
|
|
isSerial = digitalRead(3) == HIGH || LAST_BOOT_WAS_SERIAL;
|
2021-03-28 01:19:55 +00:00
|
|
|
isFlash = digitalRead(4) == HIGH;
|
|
|
|
|
2021-03-28 21:50:31 +00:00
|
|
|
LAST_BOOT_WAS_FLASH = isFlash;
|
|
|
|
LAST_BOOT_WAS_SERIAL |= isSerial;
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
configStatus.setActive(isSetup);
|
|
|
|
serialStatus.setActive(isSerial);
|
|
|
|
}
|
|
|
|
|
|
|
|
void waitForRelease() {
|
|
|
|
while(digitalRead(2) == HIGH || digitalRead(3) == HIGH) {};
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isSetup = false;
|
|
|
|
bool isSerial = false;
|
|
|
|
bool isFlash = false;
|
|
|
|
|
|
|
|
LEDStatus serialStatus = LEDStatus(RGB_COLOR_ORANGE, LED_PATTERN_FADE, LED_SPEED_FAST, LED_PRIORITY_BACKGROUND);
|
|
|
|
LEDStatus configStatus = LEDStatus(RGB_COLOR_YELLOW, LED_PATTERN_FADE, LED_SPEED_NORMAL, LED_PRIORITY_IMPORTANT);
|
|
|
|
};
|
|
|
|
|
2021-03-28 21:50:31 +00:00
|
|
|
STARTUP(BootOptions::initPins());
|
|
|
|
|
|
|
|
retained BootOptions bootopts;
|
2021-03-28 01:19:55 +00:00
|
|
|
|
|
|
|
ApplicationWatchdog *wd;
|
|
|
|
|
|
|
|
void watchdogHandler() {
|
2021-03-28 21:50:31 +00:00
|
|
|
for(int i = 0; i < 8; i++) {
|
|
|
|
leds[i] = CRGB(i % 3 ? 35 : 255, 0, 0);
|
|
|
|
}
|
|
|
|
FastLED.show();
|
|
|
|
if (LAST_BOOT_WAS_FLASH) {
|
|
|
|
System.dfu();
|
|
|
|
} else {
|
|
|
|
System.enterSafeMode();
|
|
|
|
}
|
2021-03-28 01:19:55 +00:00
|
|
|
}
|
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
// Tune in,
|
|
|
|
void setup() {
|
2021-03-28 21:50:31 +00:00
|
|
|
System.enableFeature(FEATURE_RETAINED_MEMORY);
|
2021-03-28 01:19:55 +00:00
|
|
|
wd = new ApplicationWatchdog(5000, watchdogHandler, 1536);
|
2019-05-10 05:17:29 +00:00
|
|
|
Serial.begin(115200);
|
2021-03-28 01:19:55 +00:00
|
|
|
if (bootopts.isFlash) {
|
|
|
|
System.dfu();
|
|
|
|
}
|
|
|
|
if (bootopts.isSerial) {
|
|
|
|
bootopts.waitForRelease();
|
|
|
|
while(!Serial.isConnected()) {
|
|
|
|
#ifdef PLATFORM_PHOTON
|
|
|
|
Particle.process();
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
Log.info("\xf0\x9f\x94\x8c Serial connected");
|
|
|
|
}
|
|
|
|
Log.info(u8"🐛 Booting Renderbug %s!", System.deviceID().c_str());
|
|
|
|
Log.info(u8"🐞 I am built for %d LEDs running on %dmA", HardwareConfig::MAX_LED_NUM, PSU_MILLIAMPS);
|
|
|
|
#ifdef PLATFORM_PHOTON
|
|
|
|
Log.info(u8"📡 Particle version %s", System.version().c_str());
|
|
|
|
#endif
|
|
|
|
Log.info(u8" Boot pin configuration:");
|
|
|
|
Log.info(u8" 2: Setup - %d", bootopts.isSetup);
|
|
|
|
Log.info(u8" 3: Serial - %d", bootopts.isSerial);
|
|
|
|
Log.info(u8" 4: Flash - %d", bootopts.isFlash);
|
|
|
|
|
|
|
|
Log.info(u8" Setting timezone to UTC-7");
|
|
|
|
Time.zone(-7);
|
|
|
|
|
|
|
|
Log.info(u8"💡 Starting FastLED...");
|
|
|
|
FastLED.addLeds<NEOPIXEL, 6>(leds, HardwareConfig::MAX_LED_NUM);
|
|
|
|
|
|
|
|
if (bootopts.isSetup) {
|
|
|
|
Log.info(u8"🌌 Starting Figment in configuration mode...");
|
|
|
|
runner = configApp;
|
|
|
|
} else {
|
|
|
|
Log.info(u8"🌌 Starting Figment...");
|
|
|
|
}
|
2019-05-10 05:17:29 +00:00
|
|
|
Serial.flush();
|
|
|
|
runner.start();
|
|
|
|
|
2021-03-28 01:19:55 +00:00
|
|
|
Log.info(u8"💽 %lu bytes of free RAM", System.freeMemory());
|
|
|
|
Log.info(u8"🚀 Setup complete! Ready to rock and roll.");
|
2019-05-10 05:17:29 +00:00
|
|
|
Serial.flush();
|
|
|
|
}
|
2018-12-27 04:35:58 +00:00
|
|
|
|
2019-05-10 05:17:29 +00:00
|
|
|
// Drop out.
|
|
|
|
void loop() {
|
|
|
|
runner.loop();
|
2021-03-28 01:19:55 +00:00
|
|
|
wd->checkin();
|
2018-12-27 04:35:58 +00:00
|
|
|
}
|