Compare commits

...

33 Commits

Author SHA1 Message Date
Torrie Fischer
9a3186edbd animations: implement a rain animation for weather 2023-12-11 08:10:52 +01:00
Torrie Fischer
dc1dfd9f4a data: add mapping and profile for ponderjar 2023-12-11 08:10:00 +01:00
Torrie Fischer
195b405a3b profiles: add Serial to all profiles 2023-12-11 08:09:46 +01:00
Torrie Fischer
5668266a23 animations: testanimation: rewrite into something that can help debug coord mappings 2023-12-11 08:09:10 +01:00
Torrie Fischer
81bebad459 animations: updatestatus: avoid writing past end of LED array 2023-12-11 08:08:27 +01:00
Torrie Fischer
d36de899fd platform: implement commands for task management 2023-12-11 08:07:52 +01:00
Torrie Fischer
ef74dc2178 platform: use uint16_t type instead of unsigned int, for readability 2023-12-11 08:07:38 +01:00
Torrie Fischer
4412fd8f1a platform: remove useless 5s startup delay 2023-12-11 08:07:09 +01:00
Torrie Fischer
4114a8b2b5 platform: unify more esp32/esp8266 api 2023-12-11 08:06:57 +01:00
Torrie Fischer
7418172f79 platform: use Static<> init for serial printer, remove carriage return 2023-12-11 08:06:22 +01:00
Torrie Fischer
63a705ddd4 platform: use pool.ntp.org for ntp server 2023-12-11 08:05:33 +01:00
Torrie Fischer
e9f63e718c platform: move some startup state reporting from main to platform 2023-12-11 08:04:55 +01:00
Torrie Fischer
d592810b3b config: implement commands to change profiles and save settings 2023-12-11 07:59:47 +01:00
Torrie Fischer
57f1ca837c config: bump max LEDs to 512 2023-12-11 07:59:26 +01:00
Torrie Fischer
5ea43bc908 inputs: bpm: add commands for setting BPM 2023-12-11 07:58:33 +01:00
Torrie Fischer
9c53d05ab1 animations: power: implement commands for brightness+power on/off 2023-12-11 07:58:17 +01:00
Torrie Fischer
58df15702d sequencer: implement commands to change scenes 2023-12-11 07:57:49 +01:00
Torrie Fischer
4e56134dd9 inputs: serial: implement a CLI 2023-12-11 07:57:27 +01:00
Torrie Fischer
ac94c4be0c platformio: add serial, circadianrhythm to default app set 2023-12-11 07:56:27 +01:00
Torrie Fischer
63397dc39a inputs: circadianrhythm: improve logging, fix build error 2023-12-11 07:56:00 +01:00
Torrie Fischer
7b0434e3df safemode: add serial handler to default safemode apps 2023-12-11 07:55:05 +01:00
Torrie Fischer
87ac61b061 bootoptions: unify esp32+esp8266 crash detection, add api to force reboot to safemode 2023-12-11 07:54:08 +01:00
Torrie Fischer
b1ec20982b figments: start building generic command-execution framework 2023-12-11 07:52:44 +01:00
Torrie Fischer
50c98bc5b5 platform: arduino: mqtt: print where logs get sent to 2023-12-11 07:52:01 +01:00
Torrie Fischer
5a62b30019 main: start all safemode tasks, unconditionally 2023-12-11 07:51:31 +01:00
Torrie Fischer
5a6809a723 main: clean up startup logging 2023-12-11 07:51:10 +01:00
Torrie Fischer
ad9d6649c9 figments: renderer: store last figment name for crash reporting purposes 2023-12-11 07:50:28 +01:00
Torrie Fischer
ddc3804ae0 figments: mainloop: 30fps should be the slowest frame, actually 2023-12-11 07:49:43 +01:00
Torrie Fischer
7970192c1a figments: mainloop: print warning if task couldnt be found 2023-12-11 07:49:23 +01:00
Torrie Fischer
1e2f60201d figments: ringbuf: prevent infinite loop if ringbuf is empty 2023-12-11 07:48:59 +01:00
Torrie Fischer
3e5cead5ff figments: input: implement InputEvent::operator!= 2023-12-11 07:48:33 +01:00
Torrie Fischer
c91757308d figments: surface: handle swapped start/end positions 2023-12-11 07:47:54 +01:00
Torrie Fischer
e5d4eea02b figments: display: assert that we dont exceed pixel array bounds 2023-12-11 07:47:11 +01:00
42 changed files with 852 additions and 137 deletions

24
data/maps/ponder.json Normal file
View File

@ -0,0 +1,24 @@
{
"version": 1,
"rotation": 3,
"strides": [
{"x": 0, "y": 0, "pixels": 17},
{"x": 0, "y": 1, "pixels": 17},
{"x": 0, "y": 2, "pixels": 17},
{"x": 0, "y": 3, "pixels": 17},
{"x": 0, "y": 4, "pixels": 16},
{"x": 0, "y": 5, "pixels": 17},
{"x": 0, "y": 6, "pixels": 17},
{"x": 0, "y": 7, "pixels": 17},
{"x": 0, "y": 8, "pixels": 17},
{"x": 0, "y": 9, "pixels": 17},
{"x": 0, "y": 10, "pixels": 17},
{"x": 0, "y": 11, "pixels": 17},
{"x": 0, "y": 12, "pixels": 18},
{"x": 0, "y": 13, "pixels": 17},
{"x": 0, "y": 14, "pixels": 18},
{"x": 0, "y": 15, "pixels": 17},
{"x": 0, "y": 16, "pixels": 17},
{"x": 0, "y": 17, "pixels": 17}
]
}

View File

@ -4,7 +4,8 @@
"Renderer",
"SerialInput",
"BPM",
"Bluetooth"
"Bluetooth",
"Serial"
],
"scenes": {
"Idle": ["Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"],

View File

@ -7,7 +7,8 @@
"WiFi",
"MQTT",
"ArduinoOTA",
"BPM"
"BPM",
"Serial"
],
"scenes": {
"Idle": ["Solid", "MPU5060", "IdleColors", "CircadianRhythm"],

View File

@ -6,7 +6,8 @@
"WiFi",
"MQTT",
"ArduinoOTA",
"UpdateStatusAnimation"
"UpdateStatusAnimation",
"Serial"
],
"scenes": {
"Idle": ["Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"],

View File

@ -0,0 +1,25 @@
{
"version": 1,
"tasks": [
"Bluetooth",
"WiFi",
"Renderer",
"Power",
"BPM",
"MQTT",
"ArduinoOTA",
"UpdateStatusAnimation",
"Serial"
],
"scenes": {
"Rain": ["Rain", "Rainbow"],
"Test": ["Test"],
"Idle": ["Solid", "Pulse", "Rainbow", "CircadianRhythm"],
"Acid": ["Chimes", "Pulse", "IdleColors", "Rainbow"],
"Flashlight": ["Flashlight"]
},
"surfaceMap": "ponder",
"defaults": {
"mqtt.ip": "10.0.0.2"
}
}

View File

@ -4,7 +4,8 @@
"Renderer",
"ConfigInput",
"ConfigDisplay",
"InputBlip"
"InputBlip",
"Serial"
],
"scenes": [],
"surfaceMap": "default"

7
lib/Figments/Command.cpp Normal file
View File

@ -0,0 +1,7 @@
#include "./Command.h"
void
doNothing(Args& args, Print& printer)
{}
Command::Command() : func(doNothing) {}

33
lib/Figments/Command.h Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#include <Arduino.h>
class Args {
private:
String *str;
public:
Args(String *str) : str(str) {}
String operator[](int pos) {
char buf[64];
strncpy(buf, str->c_str(), sizeof(buf));
char *args = strtok(buf, " ");
while (pos > 0 && args != NULL) {
args = strtok(NULL, " ");
pos--;
}
if (args == NULL) {
return String();
}
return String(args);
}
};
struct CommandList;
struct Command {
using Executor = std::function<void(Args&, Print& output)>;
Executor func;
const char* name = NULL;
Command();
Command(const char* name, Executor func) : name(name), func(func) {}
};

View File

@ -43,6 +43,7 @@ CRGB&
Display::pixelAt(int idx)
{
const int kx = idx % pixelCount();
assert(abs(kx) < pixelCount());
if (kx < 0) {
return m_pixels[pixelCount() + 1 + kx];
} else {

9
lib/Figments/Figment.cpp Normal file
View File

@ -0,0 +1,9 @@
#include "./Figment.h"
const std::vector<Command> emptyCommands;
const std::vector<Command>&
Task::commands() const
{
return emptyCommands;
}

View File

@ -2,6 +2,7 @@
#include <Arduino.h>
#include <functional>
#include <ArduinoLog.h>
#include "./Command.h"
#define F_LIKELY(x) __builtin_expect(!!(x), true)
#define F_UNLIKELY(x) __builtin_expect(!!(x), false)
@ -68,6 +69,8 @@ struct Task : public virtual Loopable {
const char* name = "";
State state = Stopped;
virtual const std::vector<Command> &commands() const;
};
/**

View File

@ -114,6 +114,10 @@ struct InputEvent: public Variant {
InputEvent()
: Variant(), intent(None) {}
bool operator!=(const InputEvent::Intent& otherIntent) {
return intent != otherIntent;
}
Intent intent;
};

View File

@ -24,17 +24,23 @@ MainLoop::dispatchSync(const InputEvent& evt)
{
if (evt.intent == InputEvent::StartThing || evt.intent == InputEvent::StopThing) {
const bool jobState = (evt.intent == InputEvent::StartThing);
bool wasFound = false;
for(auto figmentJob: scheduler.tasks) {
if (!strcmp(figmentJob->name, evt.asString())) {
if (jobState) {
Log.trace("** Starting %s", figmentJob->name);
figmentJob->start();
wasFound = true;
} else {
Log.trace("** Stopping %s", figmentJob->name);
figmentJob->stop();
wasFound = true;
}
}
}
if (!wasFound) {
Log.warning("** Unable to find task %s", evt.asString());
}
}
for(Task* task : scheduler) {
@ -86,7 +92,8 @@ MainLoop::loop()
}
}
frameSpeed = millis() - frameStart;
if (frameSpeed >= 23) { // TODO: Configure max frame time at build
if (frameSpeed >= 34) { // TODO: Configure max frame time at build. Default
// to 30FPS
const char* slowestName = (slowestTask->name ? slowestTask->name : "(Unnamed)");
Log.warning("Slow frame: %dms, %d tasks, longest task %s was %dms", frameSpeed, taskCount, slowestName, slowest);
}

View File

@ -3,6 +3,18 @@
#include <ArduinoLog.h>
#ifndef __NOINIT_ATTR // Pre-defined on esp32
#define __NOINIT_ATTR __attribute__ ((section (".noinit")))
#endif
__NOINIT_ATTR const char* s_lastFigmentName;
const char*
Renderer::lastFigmentName()
{
return s_lastFigmentName;
}
void
Renderer::loop()
{
@ -13,6 +25,7 @@ Renderer::loop()
unsigned int frameStart = ESP.getCycleCount();
#endif
Log.verbose("Render %s", figment->name);
s_lastFigmentName = figment->name;
figment->render(dpy);
#if defined(BOARD_ESP32) or defined(BOARD_ESP8266)
unsigned int runtime = (ESP.getCycleCount() - frameStart) / 160000;

View File

@ -10,6 +10,8 @@ public:
void loop() override;
void onStart() override;
static const char* lastFigmentName();
private:
const std::vector<Figment*> m_figments;
const std::vector<Display*> m_displays;

View File

@ -86,6 +86,8 @@ struct Ringbuf {
size_t size() {
if (m_tail > m_head) {
return m_tail - m_head;
} else if (m_tail == m_head) {
return 0;
}
return m_tail + (Size - m_head);
}

View File

@ -54,17 +54,20 @@ void
Surface::paintShader(Surface::Shader shader)
{
PerfCounter _("paintShader");
const uint16_t width = end.x - start.x + 1;
const uint16_t height = end.y - start.y + 1;
uint8_t startX = min(start.x, end.x);
uint8_t startY = min(start.y, end.y);
uint8_t endX = max(start.x, end.x);
uint8_t endY = max(start.y, end.y);
const uint16_t width = endX - startX + 1;
const uint16_t height = endY - startY + 1;
const uint8_t xMod = 255 / width;
const uint8_t yMod = 255 / height;
for(auto x = 0; x < width; x++) {
for(auto y = 0; y < height; y++) {
PhysicalCoordinates coords{x + start.x, y + start.y};
PhysicalCoordinates coords{x + startX, y + startY};
VirtualCoordinates virtCoords{m_display->coordinateMapping()->physicalToVirtualCoords(coords)};
VirtualCoordinates surfaceCoords{xMod * x, yMod * y};
//Log.notice("width=%d height=%d vx=%d vy=%d sx=%d sy=%d x=%d y=%d px=%d py=%d", width, height, start.x, start.y, x, y, coords.x, coords.y);
// 256 = 1.0
//Log.notice("width=%d height=%d vx=%d vy=%d sx=%d sy=%d x=%d y=%d px=%d py=%d", width, height, startX, startY, x, y, coords.x, coords.y); // 256 = 1.0
// 128 = 0.0
// 0 = 1.0
shader(m_display->pixelAt(coords), virtCoords, coords, surfaceCoords);

View File

@ -9,7 +9,7 @@
; https://docs.platformio.org/page/projectconf.html
[common_env_data]
src_filter = "+<*> -<.git/> -<.svn/> -<platform/> -<inputs/> +<inputs/BPM.cpp>"
src_filter = "+<*> -<.git/> -<.svn/> -<platform/> -<inputs/> +<inputs/BPM.cpp> +<inputs/Serial.cpp> +<inputs/CircadianRhythm.cpp>"
lib_ldf_mode = chain+
extra_scripts = verify-configs.py
src_build_flags =

View File

@ -14,10 +14,21 @@ retained bool LAST_BOOT_WAS_FLASH;
retained bool LAST_BOOT_WAS_SERIAL;
#endif
#ifdef BOARD_ESP32
__NOINIT_ATTR uint8_t s_rebootCount = 0;
#ifndef __NOINIT_ATTR // Pre-defined on esp32
#define __NOINIT_ATTR __attribute__ ((section (".noinit")))
#endif
#define SAFE_MODE_MAGIC 6942
__NOINIT_ATTR uint8_t s_rebootCount;
__NOINIT_ATTR uint16_t s_forceSafeMode;
void
BootOptions::forceSafeMode()
{
s_forceSafeMode = SAFE_MODE_MAGIC;
}
void
BootOptions::initPins()
{
@ -60,23 +71,20 @@ BootOptions::BootOptions()
#ifdef BOARD_ESP8266
struct rst_info resetInfo = *ESP.getResetInfoPtr();
resetReason = resetInfo.reason;
EEPROM.begin(sizeof(crashCount));
EEPROM.get(sizeof(HardwareConfig) + 32, crashCount);
EEPROM.end();
if (resetInfo.reason == REASON_WDT_RST || resetInfo.reason == REASON_EXCEPTION_RST) {
crashCount = s_rebootCount;
if (resetInfo.reason == REASON_SOFT_WDT_RST || resetInfo.reason == REASON_WDT_RST || resetInfo.reason == REASON_EXCEPTION_RST) {
if (crashCount++ >= 3) {
// Boot into safe mode if the watchdog reset us three times in a row.
isSafeMode = true;
} else {
EEPROM.begin(sizeof(crashCount));
EEPROM.put(sizeof(HardwareConfig) + 32, crashCount);
EEPROM.end();
}
} else if (crashCount != 0) {
} else {
crashCount = 0;
EEPROM.begin(sizeof(crashCount));
EEPROM.put(sizeof(HardwareConfig) + 32, crashCount);
EEPROM.end();
}
s_rebootCount = crashCount;
if (resetInfo.reason > 0 && s_forceSafeMode == SAFE_MODE_MAGIC) {
isSafeMode = true;
s_forceSafeMode = 0;
}
#endif
}

View File

@ -7,6 +7,7 @@ struct BootOptions {
BootOptions();
void waitForRelease();
void forceSafeMode();
bool isSetup = false;
bool isSerial = false;

View File

@ -53,7 +53,7 @@ Configuration::get(const char* key, bool defaultVal) const
}
}
StaticJsonDocument<1024> jsonConfig;
StaticJsonDocument<2048> jsonConfig;
constexpr uint16_t HardwareConfig::MAX_LED_NUM;
@ -154,6 +154,11 @@ ConfigService::loadMap(const String& mapName)
deserializeJson(jsonConfig, configFile);
configFile.close();
JsonArray strideList = jsonConfig["strides"];
if (jsonConfig.containsKey("rotation")) {
m_jsonMap.rotation = jsonConfig["rotation"];
} else {
m_jsonMap.rotation = 0;
}
m_jsonMap.load(strideList);
return true;
} else {
@ -213,8 +218,9 @@ ConfigService::loadProfile(const char* profileName)
m_jsonMap.loadDefault();
}
Log.notice("config: Loaded!");
strcpy(m_config.data.loadedProfile, profileName);
} else {
Log.warning("config: Could not load profile %s!", profileName);
Log.warning("config: Could not find profile json %s!", fname.c_str());
return false;
}
@ -254,5 +260,56 @@ ConfigService::handleEvent(const InputEvent &evt)
}
}
void
doMapList(Args& args, Print& out)
{
static const auto conf = Static<ConfigService>::instance();
out.println("Available maps:");
LittleFS.begin();
for(auto it = conf->mapsBegin();it != conf->mapsEnd(); it++) {
out.println(*it);
}
LittleFS.end();
}
void
doSave(Args& args, Print& print)
{
MainLoop::instance()->dispatch(InputEvent::SaveConfigurationRequest);
}
static String s;
void
doSetProfile(Args& args, Print& out)
{
s = args[1];
MainLoop::instance()->dispatch(InputEvent{InputEvent::LoadConfigurationByName, s.c_str()});
}
void
doCoordMap(Args& args, Print& out)
{
VirtualCoordinates coords{atoi(args[1].c_str()), atoi(args[2].c_str())};
auto map = Static<ConfigService>::instance()->coordMap();
auto pPos = map->virtualToPhysicalCoords(coords);
auto idx = map->physicalCoordsToIndex(pPos);
char buf[32];
sprintf(buf, "(%d, %d) -> (%d, %d) -> %d", coords.x, coords.y, pPos.x, pPos.y, idx);
out.println(buf);
}
const std::vector<Command>&
ConfigService::commands() const
{
static const std::vector<Command> _commands = {
{"save", doSave},
{"profile", doSetProfile},
{"maps", doMapList},
{"coordmap", doCoordMap}
};
return _commands;
}
STATIC_ALLOC(ConfigService);
STATIC_TASK(ConfigService);

View File

@ -2,6 +2,7 @@
#include <Figments.h>
#include "JsonCoordinateMapping.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
class Configuration {
public:
@ -39,7 +40,7 @@ struct HardwareConfig {
void save();
bool isValid() const;
static constexpr uint16_t MAX_LED_NUM = 255;
static constexpr uint16_t MAX_LED_NUM = 512;
private:
uint8_t getCRC() const;
@ -59,6 +60,59 @@ struct ConfigService: public Task {
const char* loadedProfile() const;
void overrideProfile(const char* profileName);
const char* getConfigValue(const char* key) const;
const std::vector<Command>& commands() const override;
struct filename_iterator: public std::iterator<std::input_iterator_tag, const char*> {
Dir dir;
String ret;
bool valid;
const char* suffix;
explicit filename_iterator() : suffix(NULL), valid(false) {}
explicit filename_iterator(const char* path, const char* suffix) : dir(LittleFS.openDir(path)), valid(true), suffix(suffix) {
next();
}
void next() {
if (!valid) {
return;
}
int extPos = -1;
do {
valid = dir.next();
Log.info("valid %F", valid);
if (valid) {
String fname = dir.fileName();
extPos = fname.lastIndexOf(suffix);
Log.info("compare %s %d", fname.c_str(), extPos);
if (extPos != -1) {
ret = fname.substring(0, extPos);
Log.info("found %s", ret.c_str());
}
}
} while (valid && extPos == -1);
}
filename_iterator& operator++() {
next();
return *this;
}
filename_iterator& operator++(int) {filename_iterator ret = *this; ++(*this); return ret;}
bool operator==(const filename_iterator &other) const { return valid == other.valid;}
bool operator!=(const filename_iterator &other) const { return !(*this == other); }
const char* operator*() const {
if (!valid) {
return NULL;
}
return ret.c_str();
}
};
filename_iterator mapsBegin() const { return filename_iterator("/maps/", ".json"); }
filename_iterator mapsEnd() const { return filename_iterator(); }
filename_iterator profilesBegin() const { return filename_iterator("/profiles/", ".json"); }
filename_iterator profilesEnd() const { return filename_iterator(); }
private:
HardwareConfig m_config;

View File

@ -16,21 +16,21 @@
#include <ctime>
WiFiUDP wifiUdp;
//NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7);
NTPClient timeClient(wifiUdp, "10.0.0.1", 3600 * -7);
NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7);
#endif
#endif
#ifdef PLATFORM_PHOTON
STARTUP(BootOptions::initPins());
#else
#include "inputs/Serial.h"
#ifdef CONFIG_MQTT
#include "platform/arduino/MQTTTelemetry.h"
#endif
void printNewline(Print* logOutput, int logLevel)
{
(void)logLevel; // unused
logOutput->print("\r\n");
logOutput->print("\n");
}
int printEspLog(const char* fmt, va_list args)
{
@ -62,7 +62,7 @@ Platform::version()
{
#ifdef PLATFORM_PHOTON
return System.version().c_str();
#elif defined(BOARD_ESP32)
#elif defined(BOARD_ESP32) || defined(BOARD_ESP8266)
return ESP.getSdkVersion();
#else
return "Unknown!";
@ -72,10 +72,7 @@ Platform::version()
int
Platform::freeRam()
{
#ifdef BOARD_ESP8266
return ESP.getFreeHeap();
#endif
#ifdef BOARD_ESP32
#if defined(BOARD_ESP8266) || defined(BOARD_ESP32)
return ESP.getFreeHeap();
#endif
}
@ -84,7 +81,6 @@ void
Platform::preSetup()
{
Serial.begin(115200);
delay(5000);
#ifdef PLATFORM_PHOTON
System.enableFeature(FEATURE_RETAINED_MEMORY);
if (bootopts.isFlash) {
@ -102,7 +98,7 @@ Platform::preSetup()
Log.begin(LOG_LEVEL_TRACE, Static<MQTTTelemetry>::instance()->logPrinter());
Static<MQTTTelemetry>::instance()->setSequencer(Static<Sequencer>::instance());
#else
Log.begin(LOG_LEVEL_TRACE, &Serial);
Log.begin(LOG_LEVEL_TRACE, Static<SerialInput>::instance()->logPrinter());
#endif
Log.setSuffix(printNewline);
#endif
@ -114,6 +110,9 @@ Platform::preSetup()
#endif
#ifdef BOARD_ESP8266
ESP.wdtEnable(0);
if (!ESP.checkFlashCRC()) {
Log.fatal("Firmware failed CRC check!!!");
}
#endif
}
@ -142,6 +141,30 @@ Platform::bootSplash()
Log.notice(u8" 4: Flash - %d", bootopts.isFlash);
#endif
if (bootopts.crashCount > 0) {
Log.warning(u8"Previous crash detected!!!! We're on attempt %d", bootopts.crashCount);
char lastTaskBuf[16];
strncpy(lastTaskBuf, MainLoop::lastTaskName(), sizeof(lastTaskBuf));
lastTaskBuf[15] = 0;
Log.error(u8"Crash occurred in task %s", lastTaskBuf);
#ifdef BOARD_ESP8266
auto rInfo = ESP.getResetInfoPtr();
if (Platform::bootopts.resetReason == REASON_EXCEPTION_RST) {
Log.error("Fatal exception (%d):", rInfo->exccause);
}
Log.error("epc1=%X, epc2=%X, epc3=%X, excvaddr=%X, depc=%X",
rInfo->epc1, rInfo->epc2, rInfo->epc3, rInfo->excvaddr, rInfo->depc);
#endif
strncpy(lastTaskBuf, Renderer::lastFigmentName(), sizeof(lastTaskBuf));
lastTaskBuf[15] = 0;
Log.error(u8"Last Figment was %s", lastTaskBuf);
}
Log.trace("Startup reason: %d", bootopts.resetReason);
Log.trace("Registered tasks:");
auto it = beginTasks();
while (it != endTasks()) {
@ -211,8 +234,8 @@ Platform::deviceID()
}
void
Platform::addLEDs(CRGB* leds, unsigned int ledCount) {
FastLED.addLeds<WS2812B, RENDERBUG_LED_PIN, RENDERBUG_LED_PACKING>(leds, ledCount);
Platform::addLEDs(CRGB* leds, uint16_t ledCount) {
FastLED.addLeds<WS2812, RENDERBUG_LED_PIN, RENDERBUG_LED_PACKING>(leds, ledCount);
}
const String
@ -232,6 +255,78 @@ Platform::restart() {
#endif
}
__attribute__((noreturn))
void
doReboot(Args& args, Print& out)
{
out.println("Rebooting");
Platform::restart();
}
__attribute__((noreturn))
void
doSafeMode(Args& args, Print& out)
{
out.println("Rebooting into safe mode");
Platform::bootopts.forceSafeMode();
Platform::restart();
}
String s;
void
doTaskStart(Args& args, Print& out)
{
s = args[1];
MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, s.c_str()});
}
void
doTaskStop(Args& args, Print& out)
{
s = args[1];
MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, s.c_str()});
}
void
doTaskList(Args& args, Print& out)
{
auto sched = MainLoop::instance()->scheduler;
auto printer = Static<SerialInput>::instance()->printer();
out.println("Tasks:");
for(auto task : sched.tasks) {
bool isFigment = task->isFigment();
if (task->state == Task::Running) {
out.print("+");
} else {
out.print("-");
}
if (isFigment) {
out.print("F ");
} else {
out.print("T ");
}
out.println(task->name);
}
}
const std::vector<Command> _commands = {
{"tasks", doTaskList},
{"safe-mode", doSafeMode},
{"reboot", doReboot},
{"stop", doTaskStop},
{"start", doTaskStart}
};
const std::vector<Command>&
Platform::commands() const
{
return _commands;
}
BootOptions
Platform::bootopts;

View File

@ -11,7 +11,7 @@ class Platform : public Task {
static BootOptions bootopts;
static void setTimezone(int tz) { s_timezone = tz; }
static int getTimezone() { return s_timezone; }
static void addLEDs(CRGB* leds, unsigned int ledCount);
static void addLEDs(CRGB* leds, uint16_t ledCount);
static const char* name();
static const char* version();
@ -105,4 +105,6 @@ class Platform : public Task {
}
static void restart();
const std::vector<Command>& commands() const override;
};

View File

@ -3,6 +3,7 @@
#include "./Platform.h"
#include "./Static.h"
#include "./Config.h"
#include "./inputs/Serial.h"
TaskFunc safeModeNag([]{
static uint8_t frame = 0;
@ -41,6 +42,7 @@ SafeMode::safeModeApp{{
// System logging
Static<LogService>::instance(),
&safeModeNag,
Static<SerialInput>::instance(),
#ifdef CONFIG_WIFI
// ESP Wifi
Static<WiFiTask>::instance(),

View File

@ -94,5 +94,34 @@ Sequencer::handleEvent(const InputEvent& evt)
}
}
void
doScenes(Args& args, Print& out)
{
out.println("Available scenes: ");
for (auto scene : Static<Sequencer>::instance()->scenes()) {
out.println(scene.name);
}
}
static String s;
void
doScene(Args& args, Print& out)
{
s = args[1];
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetPattern, s.c_str()});
}
const std::vector<Command> _commands = {
{"scene", doScene},
{"scenes", doScenes}
};
const std::vector<Command>&
Sequencer::commands() const
{
return _commands;
}
STATIC_ALLOC(Sequencer);
STATIC_TASK(Sequencer);

View File

@ -23,6 +23,7 @@ public:
const char* currentSceneName();
const std::vector<Scene> scenes() const;
const std::vector<Command>& commands() const override;
private:
int m_idx;

View File

@ -2,6 +2,54 @@
#include "../Static.h"
#include <ArduinoJson.h>
void
doBrightness(Args& args, Print& out)
{
String nextVal = args[1];
uint8_t newBrightness = (uint8_t)atoi(nextVal.c_str());
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetBrightness, newBrightness});
}
void
doOn(Args& args, Print& out)
{
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetPower, 255});
}
void
doOff(Args& args, Print& out)
{
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetPower, 0});
}
void
doForceBrightness(Args& args, Print& out)
{
String nextVal = args[1];
uint8_t newBrightness = (uint8_t)atoi(nextVal.c_str());
Static<Power>::instance()->forceBrightness(newBrightness);
}
void
Power::forceBrightness(uint8_t v)
{
m_forced = true;
FastLED.setBrightness(v);
}
const std::vector<Command> _commands = {
{"brightness", doBrightness},
{"brightness-force", doForceBrightness},
{"on", doOn},
{"off", doOff}
};
const std::vector<Command>&
Power::commands() const
{
return _commands;
}
void
Power::handleConfigChange(const Configuration& config)
{

View File

@ -10,15 +10,17 @@ public:
switch (evt.intent) {
case InputEvent::PowerToggle:
m_powerState = m_powerState.value() <= 128 ? 255 : 0;
//Log.info("POWER TOGGLE %d", m_powerState.value());
m_forced = false;
Log.notice("Power toggled to %t", m_powerState);
break;
case InputEvent::SetPower:
m_powerState = evt.asInt() == 0 ? 0 : 255;
Log.notice("Power is now %d", m_powerState);
m_forced = false;
Log.notice("Power state is now %t", m_powerState);
break;
case InputEvent::SetBrightness:
m_brightness = evt.asInt();
m_brightness = 255;
m_forced = false;
break;
case InputEvent::Beat:
m_beatDecay.set(0, 255);
@ -40,7 +42,7 @@ public:
}
void render(Display* dpy) const override {
if (F_LIKELY(m_valid)) {
if (F_LIKELY(m_valid && !m_forced)) {
const uint8_t decayedBrightness = scale8((uint8_t)m_brightness, m_useBPM ? ease8InOutCubic((uint8_t)m_beatDecay) : 255);
const uint8_t clippedBrightness = std::min(decayedBrightness, (uint8_t)255);
const uint8_t scaledBrightness = scale8(m_powerState, clippedBrightness);
@ -50,6 +52,9 @@ public:
}
}
void forceBrightness(uint8_t v);
const std::vector<Command>& commands() const override;
private:
AnimatedNumber m_powerState = 255;
@ -59,4 +64,5 @@ private:
uint16_t m_milliamps = 500;
bool m_valid = true;
bool m_useBPM = false;
bool m_forced = false;
};

45
src/animations/Rain.cpp Normal file
View File

@ -0,0 +1,45 @@
#include "Rain.h"
#include "../Static.h"
RainAnimation::RainAnimation() : Figment("Rain")
{
}
void
RainAnimation::render(Display* dpy) const
{
Surface sfc = Surface(dpy, {0, 0}, {255, 255});
uint8_t noiseY = sin8(m_noiseOffset % 255);
uint8_t noiseX = cos8(m_noiseOffset % 255);
sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
pixel = CHSV(m_hue, inoise8(noiseX + coords.x, coords.y), inoise8(m_noiseOffset + coords.x, noiseY + coords.y));
});
m_drops.render(dpy);
}
void
RainAnimation::loop()
{
EVERY_N_MILLISECONDS(250) {
m_drops.update();
}
EVERY_N_MILLISECONDS(60) {
m_hue.update(1);
m_curColor.h = m_hue;
}
m_noiseOffset += 1;
}
void
RainAnimation::handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::SetColor) {
CHSV next = rgb2hsv_approximate(evt.asRGB());
m_hue.set(next.h);
m_drops.forEach([=](Raindrop& drop) {
drop.nextColor = next;
});
}
}
STATIC_ALLOC(RainAnimation);
STATIC_TASK(RainAnimation);

48
src/animations/Rain.h Normal file
View File

@ -0,0 +1,48 @@
#pragma once
#include <Figments.h>
class RainAnimation: public Figment {
private:
struct Raindrop {
int size = 20;
int x = random(255);
int y = random(255);
CHSV fg{180, 255, 255};
CHSV nextColor{180, 255, 255};
void render(Display* dpy) const {
Surface sfc{dpy, {x - size, y - size}, {x + size, y + size}};
paint(sfc);
}
void paint(Surface& sfc) const {
sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
int distance = 255 - (min(128, abs(128 - surfaceCoords.x)) + min(128, abs(128 - surfaceCoords.y)));
pixel += CHSV{fg.h, fg.s, scale8_video(fg.v, distance)};
});
}
void update() {
if (random(255) >= 100) {
y++;
if (y >= 255) {
y = 0;
x += 13;
x %= 255;
fg = nextColor;
}
}
}
};
SpriteList<Raindrop, 10> m_drops;
uint16_t m_noiseOffset;
CHSV m_curColor{180, 255, 255};
AnimatedNumber m_hue;
public:
RainAnimation();
//void handleEvent(const InputEvent& evt) override;
void loop() override;
void render(Display* dpy) const override;
void handleEvent(const InputEvent& evt) override;
};

View File

@ -1,53 +1,32 @@
#include "./TestAnimation.h"
#include "../Static.h"
#include <FastLED.h>
const char*
TestAnimation::name() const
{
return "Test";
}
void
TestAnimation::handleEvent(const InputEvent& evt)
{
if (evt.intent == InputEvent::Acceleration) {
if (evt.asInt() > 5) {
m_brightness += 15;
}
m_hue += scale8(evt.asInt(), 128);
}
if (evt.intent == InputEvent::UserInput) {
switch(evt.asInt()) {
case 1:
m_brightness.set(255, 0);break;
case 2:
m_saturation.set(255, 128);break;
default:
m_brightness.set(255, 0);
m_saturation.set(255, 128);
}
}
}
void
TestAnimation::loop()
{
m_x += 4;
if (m_x % 12 == 0) {
m_y += 28;
}
m_hue.update();
m_saturation.update();
m_brightness.update();
m_x += 1;
m_y += 1;
}
void
TestAnimation::render(Display* dpy) const
{
for(uint8_t col = 0; col < 3; col++) {
for (uint8_t i = 0; i < 254; i+=10) {
dpy->pixelAt(VirtualCoordinates{(uint8_t)(m_x + (col * (254 / 3))), (uint8_t)(i + m_y)}) = CHSV(m_hue, m_saturation + 100, scale8(i, m_brightness));
}
}
for(unsigned int i = 0; i < dpy->pixelCount(); i++) {
dpy->pixelAt(i) = CRGB{255, 255, 255};
}
return;
// Blank the canvas to white
Surface{dpy, {0, 0}, {255, 255}} = CRGB{255, 255, 255};
// Draw red line on top row
Surface{dpy, {0, 0}, {255, 0}} = CRGB{255, 0, 0};
// Green line on first column
Surface{dpy, {0, 0}, {0, 255}} = CRGB{0, 255, 0};
//Surface{dpy, {m_x, 0}, {m_x, 255}} = CRGB{255, 0, 0};
///Surface{dpy, {0, m_y}, {255, m_y}} = CRGB{255, 0, 0};
//dpy->pixelAt(VirtualCoordinates{m_x, m_y}) = CRGB{255, 0, 255};
}
STATIC_ALLOC(TestAnimation);
STATIC_TASK(TestAnimation);

View File

@ -2,15 +2,11 @@
class TestAnimation: public Figment {
public:
const char* name() const;
void handleEvent(const InputEvent& evt) override;
TestAnimation() : Figment("Test") {}
void loop() override;
void render(Display* dpy) const override;
private:
AnimatedNumber m_hue;
AnimatedNumber m_saturation;
AnimatedNumber m_brightness;
uint8_t m_x;
uint8_t m_y;
};

View File

@ -8,7 +8,7 @@ UpdateStatus::handleEvent(const InputEvent& evt)
if (evt.intent == InputEvent::FirmwareUpdate) {
static int updateCount = 0;
updateCount++;
//Log.info("Update count %d", updateCount);
Log.info("Update count %d", updateCount);
m_updateReady = true;
}
}
@ -22,11 +22,12 @@ UpdateStatus::loop()
void
UpdateStatus::render(Display* dpy) const
{
int pos = m_pos % dpy->pixelCount();
if (m_updateReady) {
for(int i = 0; i < 12; i+=3) {
dpy->pixelAt(m_pos + i) = CRGB(255, 0, 0);
dpy->pixelAt(m_pos + i + 1) = CRGB(0, 255, 0);
dpy->pixelAt(m_pos + i + 2) = CRGB(0, 0, 255);
dpy->pixelAt(pos + i) = CRGB(255, 0, 0);
dpy->pixelAt(pos + i + 1) = CRGB(0, 255, 0);
dpy->pixelAt(pos + i + 2) = CRGB(0, 0, 255);
}
}
}

View File

@ -10,5 +10,5 @@ public:
private:
bool m_updateReady = false;
uint8_t m_pos = 0;
int m_pos = 0;
};

View File

@ -1,5 +1,22 @@
#include "./BPM.h"
#include "../Static.h"
void
doBPM(Args& args, Print& out)
{
uint8_t newBPM(atoi(args[1].c_str()));
Static<BPM>::instance()->setBPM(newBPM);
}
const std::vector<Command> _commands = {
{"bpm", doBPM}
};
const std::vector<Command>&
BPM::commands() const
{
return _commands;
}
STATIC_ALLOC(BPM);
STATIC_TASK(BPM);

View File

@ -18,6 +18,8 @@ public:
ConfigTaskMixin::handleEvent(evt);
}
const std::vector<Command>& commands() const override;
void loop() {
InputSource::loop();
ConfigTaskMixin::loop();
@ -29,6 +31,11 @@ public:
Log.notice("bpm: idle BPM set to %d (requested %d)", (int)msToBPM(m_msPerBeat), (int)requestedBPM);
}
void setBPM(double bpm) {
m_msPerBeat = 60000.0 / (double)bpm;
Log.notice("bpm: Command changed to %d (requested %d)", (int)msToBPM(m_msPerBeat), (int)bpm);
}
InputEvent read() override {
if (m_msPerBeat > 0) {
uint16_t now = millis();

View File

@ -1,5 +1,6 @@
#pragma once
#include <Figments.h>
#include "../Platform.h"
struct ScheduleEntry {
uint8_t hour;
@ -83,7 +84,9 @@ class CircadianRhythm : public InputSource {
minute = 0;
}
Log.notice("Current time: %d:%d", hour, minute);
return InputEvent{InputEvent::SetBrightness, brightnessForTime(hour, minute)};
auto brightness = brightnessForTime(hour, minute);
Log.notice("Adjusting brightness to %d", brightness);
return InputEvent{InputEvent::SetBrightness, brightness};
}
return InputEvent{};
}

View File

@ -1,33 +1,171 @@
#include "Serial.h"
#include "../Static.h"
#include <LittleFS.h>
#include "../Config.h"
#include "../Sequencer.h"
InputEvent
Serial::read()
SerialInput::SerialInput() : InputSource("Serial"),
m_state(ParseState::Normal),
m_logPrinter(this)
{
while (Serial.available() > 0) {
char nextChar = Serial.read();
if (nextChar == '\n') {
doCommand();
m_buf = "";
} else {
m_buf += nextChar;
}
}
}
void
Serial::doCommand() {
if (command == "tasks") {
Serial.println("Tasks:");
auto sched = MainLoop::instance()->scheduler;
for(auto task : sched.tasks) {
bool isFigment = task->isFigment();
if (isFigment) {
Serial.println("F " + task->name);
} else {
Serial.println("T " + task->name);
}
SerialInput::redrawPrompt()
{
if (m_canRedraw) {
Serial.print((char)8);
Serial.print((char)27);
Serial.print("[2K");
Serial.print((char)8);
Serial.print((char)27);
Serial.print("[G");
Serial.print('\r');
Serial.print("> ");
Serial.print(m_buf);
}
}
InputEvent
SerialInput::parseNormal(char nextChar)
{
if (nextChar == 27) {
m_state = ParseState::EscapeSequence;
return InputEvent::None;
}
if (nextChar == 13) {
redrawPrompt();
Serial.println();
if (m_buf.length() > 0) {
m_canRedraw = false;
doCommand();
m_canRedraw = true;
m_history.insert(m_buf);
m_buf = "";
}
m_historyOffset = 0;
redrawPrompt();
return InputEvent{};
}
if (nextChar == 8) {
if (m_buf.length() > 0) {
m_buf.remove(m_buf.length() - 1, 1);
}
Serial.print((char)8);
Serial.print((char)27);
Serial.print("[K");
redrawPrompt();
return InputEvent::None;
}
if (nextChar >= 32 && nextChar <= 126) {
m_buf += nextChar;
Serial.print(nextChar);
}
return InputEvent::None;
}
InputEvent
SerialInput::parseEscape(char nextChar)
{
if (nextChar == '[') {
m_state = ParseState::CSI;
} else {
m_state = ParseState::Normal;
}
return InputEvent::None;
}
InputEvent
SerialInput::parseCSI(char nextChar)
{
if (nextChar == 'A') {
if (m_historyOffset < m_history.size()) {
m_historyOffset += 1;
m_buf = m_history.peek(m_historyOffset);
redrawPrompt();
} else {
Serial.print((char)7);
}
} else if (nextChar == 'B') {
if (m_historyOffset > 0) {
m_historyOffset -= 1;
m_buf = m_history.peek(m_historyOffset);
redrawPrompt();
} else {
Serial.print((char)7);
}
} else {
Serial.print((char)7);
}
m_state = ParseState::Normal;
return InputEvent::None;
}
InputEvent
SerialInput::read()
{
while (Serial.available() > 0) {
char nextChar = Serial.read();
InputEvent ret = InputEvent::None;
switch (m_state) {
case ParseState::Normal:
ret = parseNormal(nextChar);break;
case ParseState::EscapeSequence:
ret = parseEscape(nextChar);break;
case ParseState::CSI:
ret = parseCSI(nextChar);break;
}
if (ret != InputEvent::None) {
return ret;
}
}
return InputEvent::None;
}
void
doHelp(Args& args, Print& out)
{
out.println("Available commands:");
auto sched = MainLoop::instance()->scheduler;
for(auto task : sched.tasks) {
for(auto &command : task->commands()) {
out.print(command.name);
out.print(" ");
}
}
out.println();
}
const std::vector<Command> serialCommands = {
{"help", doHelp}
};
const std::vector<Command>&
SerialInput::commands() const
{
return serialCommands;
}
void
SerialInput::doCommand() {
auto sched = MainLoop::instance()->scheduler;
Args args = Args(&m_buf);
const auto cmdName = args[0];
for(auto task : sched.tasks) {
for(auto &command : task->commands()) {
if (cmdName == command.name) {
command.func(args, m_logPrinter);
return;
}
}
}
m_logPrinter.println("Unknown command");
doHelp(args, m_logPrinter);
}
STATIC_ALLOC(SerialInput);

View File

@ -3,14 +3,60 @@
class SerialInput: public InputSource {
public:
void onStart() override {
//Serial.begin();
SerialInput();
InputEvent read() override;
class LogPrinter : public Print {
private:
SerialInput* serial;
Ringbuf<char, 512> buf;
public:
LogPrinter(SerialInput* serial) : serial(serial) {};
size_t write(uint8_t byte) {
if (byte == '\n') {
char c;
Serial.print('\r');
while (buf.take(c)) {
Serial.write(c);
}
Serial.println();
serial->redrawPrompt();
} else {
buf.insert(byte);
}
return sizeof(byte);
}
};
void redrawPrompt();
Print* logPrinter() {
return &m_logPrinter;
}
InputEvent read();
//static SerialInput::Command *s_root;
LogPrinter* printer() {
return &m_logPrinter;
}
const std::vector<Command> &commands() const override;
private:
enum ParseState {
Normal,
EscapeSequence,
CSI
};
String m_buf;
ParseState m_state;
char m_escapeSeq[3];
void doCommand();
LogPrinter m_logPrinter;
bool m_canRedraw = true;
Ringbuf<String, 5> m_history;
int m_historyOffset = 0;
}
InputEvent parseNormal(char nextChar);
InputEvent parseEscape(char nextChar);
InputEvent parseCSI(char nextChar);
};

View File

@ -87,16 +87,6 @@ void setup() {
Log.notice(u8"🐞 I am built for %d LEDs on pin %d", HardwareConfig::MAX_LED_NUM, RENDERBUG_LED_PIN);
Log.notice(u8"📡 Platform %s version %s", Platform::name(), Platform::version());
if (Platform::bootopts.crashCount > 0) {
Log.warning(u8"Previous crash detected!!!! We're on attempt %d", Platform::bootopts.crashCount);
char lastTaskBuf[16];
strncpy(lastTaskBuf, MainLoop::lastTaskName(), sizeof(lastTaskBuf));
lastTaskBuf[15] = 0;
Log.error(u8"Crash occurred in task %s", lastTaskBuf);
}
Log.trace("Startup reason: %d", Platform::bootopts.resetReason);
Log.notice(u8"Setting timezone to +2 (CEST)");
Platform::setTimezone(+2);
@ -105,14 +95,17 @@ void setup() {
Platform::setup();
Platform::bootSplash();
Log.notice(u8"💡 Starting FastLED...");
Log.notice(u8"💡 Starting FastLED on %d LEDs...", HardwareConfig::MAX_LED_NUM);
Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM);
// Tune in,
if (Platform::bootopts.isSafeMode) {
Log.error(u8"⚠️ Starting Figment in safe mode!!!");
Log.warning(u8"⚠️ Starting Figment in safe mode!!!");
runner = &SafeMode::safeModeApp;
FastLED.showColor(CRGB(5, 0, 0));
for(auto task : runner->scheduler.tasks) {
task->state = Task::Running;
}
FastLED.showColor(CRGB(255, 0, 0));
FastLED.show();
} else {
Log.notice(u8"🌌 Starting Figment...");
@ -131,7 +124,7 @@ void setup() {
Serial.flush();
runner->start();
Log.notice(u8"💽 %lu bytes of free RAM", Platform::freeRam());
Log.notice(u8"💽 %l bytes of free RAM", Platform::freeRam());
Log.notice(u8"🚀 Setup complete! Ready to rock and roll.");
Serial.flush();
}

View File

@ -118,6 +118,8 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt)
Log.notice("Connecting to MQTT as %s on %s...", Platform::deviceID(), Device.availabilityTopic.c_str());
if (m_mqtt.connect(Platform::deviceID(), NULL, NULL, Device.availabilityTopic.c_str(), 0, true, "offline")) {
Log.notice("Connected to MQTT");
String logTopic = m_debugTopic + "/log";
Log.info("MQTT logs are available at %s", logTopic.c_str());
m_needHeartbeat = true;
m_json.clear();