Compare commits

..

20 Commits

Author SHA1 Message Date
bbc01f7cea animations: solid: increase pre-blob color headroom, make color transitions much smoother
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-02-20 07:09:01 +01:00
4a75e09792 sprites: blob: delete old commented out code 2023-02-20 07:08:05 +01:00
cb938d768a profiles: default: lower default bpm to 25 from 75 2023-02-20 07:07:46 +01:00
64666bbfb6 config: add new Configuration class to simplify handling json config update api 2023-02-20 07:07:32 +01:00
53d5775c6a platformio: update u8 lib, add new display esp32 variant 2023-02-19 18:47:48 +01:00
39190d6506 figments: input: fix as() signature to return pointers 2023-02-19 18:47:04 +01:00
ccb58082a2 main: tweak log levels, add crash info 2023-02-19 18:46:43 +01:00
ea058a33da main: drop pulse input in favor of existing BPM, add to default config 2023-02-19 18:46:28 +01:00
d824dbfa45 animations: power: make configurable 2023-02-19 18:45:28 +01:00
8223688d7b platform: fix crash, move some code from h to cpp 2023-02-19 18:44:26 +01:00
0700dfaf92 inputs: colorcycle: debug-- 2023-02-19 18:43:43 +01:00
d89630a340 platform: arduino: mqtt: allow configuration through json 2023-02-19 18:43:26 +01:00
f9add1f684 logservice: add support for Pointer event types and config event intents 2023-02-19 18:43:00 +01:00
2ee332914a sequencer: debug-- 2023-02-19 18:42:10 +01:00
bfbbeffcfd config: fix crashes, implement fallback profiles 2023-02-19 18:41:55 +01:00
de5d3e836a figments: mainloop: add debugging tool that records last task prior to reset 2023-02-19 18:41:25 +01:00
1426b1524b figments: figment: add F_LIKELY, F_UNLIKELY macros 2023-02-19 18:40:37 +01:00
160cbc5ea9 bootoptions: use NVRAM instead of wearing out flash for crash detection in esp32 2023-02-19 18:40:16 +01:00
ae3abc3aa3 inputs: bpm: add support for configuring startup/idle BPM 2023-02-19 18:39:26 +01:00
2a0d72f0a1 platform: arduino: u8display: delete task on stop 2023-02-19 18:38:48 +01:00
24 changed files with 386 additions and 183 deletions

View File

@ -1,15 +1,24 @@
{
"version": 1,
"tasks": [
"Power",
"Renderer",
"MQTT",
"U8Display",
"WiFi",
"MQTT",
"ArduinoOTA",
"UpdateStatusAnimation"
"BPM"
],
"scenes": {
"Idle": ["Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"],
"Idle": ["Solid", "MPU5060", "IdleColors", "CircadianRhythm"],
"Flashlight": ["Flashlight"]
},
"surfaceMap": "default"
"surfaceMap": "default",
"logLevel": 6,
"defaults": {
"mqtt.ip": "10.0.0.2",
"power.milliamps": 500,
"power.volts": 5,
"bpm.idle": 25
}
}

View File

@ -3,6 +3,9 @@
#include <functional>
#include <ArduinoLog.h>
#define F_LIKELY(x) __builtin_expect(!!(x), true)
#define F_UNLIKELY(x) __builtin_expect(!!(x), false)
class Display;
class InputEvent;
class InputSource;

View File

@ -37,8 +37,8 @@ struct Variant {
CRGB asRGB() const;
int asInt() const;
bool asBool() const;
template<typename T> const T& as() const {
return *static_cast<const T*>(m_value.asPointer);
template<typename T> const T* as() const {
return static_cast<const T*>(m_value.asPointer);
}
private:
@ -175,19 +175,6 @@ private:
std::function<InputEvent(const InputEvent)> m_func;
};
class ConfigTaskMixin : public virtual Loopable {
public:
void handleEvent(const InputEvent &evt) override {
if (evt.intent == InputEvent::ConfigurationChanged) {
handleConfigChange(evt);
}
}
void loop() override {}
virtual void handleConfigChange(const InputEvent& evt) {}
};
class OnlineTaskMixin : public virtual Loopable {
public:
void handleEvent(const InputEvent &evt) override {

View File

@ -13,12 +13,13 @@ MainLoop::dispatch(const InputEvent& evt)
m_eventBuf.insert(evt);
}
//#ifdef BOARD_ESP32
__NOINIT_ATTR const char* s_lastTaskName;
//#endif
void
MainLoop::loop()
MainLoop::dispatchSync(const InputEvent& evt)
{
s_instance = this;
InputEvent evt;
while (m_eventBuf.take(evt)) {
if (evt.intent == InputEvent::StartThing || evt.intent == InputEvent::StopThing) {
const bool jobState = (evt.intent == InputEvent::StartThing);
for(auto figmentJob: scheduler.tasks) {
@ -36,8 +37,24 @@ MainLoop::loop()
for(Task* task : scheduler) {
Log.verbose("** Eventing %s", task->name);
s_lastTaskName = task->name;
task->handleEvent(evt);
}
}
const char*
MainLoop::lastTaskName()
{
return s_lastTaskName;
}
void
MainLoop::loop()
{
s_instance = this;
InputEvent evt;
while (m_eventBuf.take(evt)) {
dispatchSync(evt);
}
unsigned int slowest = 0;
unsigned int frameSpeed = 0;
@ -52,6 +69,7 @@ MainLoop::loop()
unsigned int start = millis();
#endif
Log.verbose("Running %s", task->name);
s_lastTaskName = task->name;
task->loop();
#if defined(BOARD_ESP32) or defined(BOARD_ESP8266)
unsigned int runtime = (ESP.getCycleCount() - start) / 160000;
@ -66,7 +84,7 @@ MainLoop::loop()
}
}
frameSpeed = millis() - frameStart;
if (frameSpeed >= 23) {
if (frameSpeed >= 23) { // TODO: Configure max frame time at build
const char* slowestName = (slowestTask->name ? slowestTask->name : "(Unnamed)");
Log.warning("Slow frame: %dms, %d tasks, longest task %s was %dms", frameSpeed, taskCount, slowestTask->name, slowest);
}

View File

@ -60,7 +60,9 @@ struct MainLoop {
void start();
void loop();
void dispatch(const InputEvent& event);
void dispatchSync(const InputEvent& event);
static MainLoop* instance() { return s_instance; }
static const char* lastTaskName();
private:
Ringbuf<InputEvent, 32> m_eventBuf;

View File

@ -9,7 +9,7 @@
; https://docs.platformio.org/page/projectconf.html
[common_env_data]
src_filter = "+<*> -<.git/> -<.svn/> -<platform/> -<inputs/>"
src_filter = "+<*> -<.git/> -<.svn/> -<platform/> -<inputs/> +<inputs/BPM.cpp>"
lib_ldf_mode = chain+
extra_scripts = verify-configs.py
src_build_flags =
@ -27,7 +27,7 @@ lib_deps_external =
src_build_flags =
-DCONFIG_U8DISPLAY
lib_deps =
olikraus/U8g2@2.28.8
olikraus/U8g2@2.34.13
src_filter = "+<platform/arduino/U8Display.cpp>"
[config_mqtt]
@ -93,6 +93,7 @@ src_build_flags =
[env:esp32]
extends = config_nocolor
extra_scripts = verify-configs.py
board_build.filesystem = littlefs
platform = espressif32
board = featheresp32
framework = arduino
@ -101,12 +102,12 @@ src_build_flags =
${config_nocolor.src_build_flags}
-DPLATFORM_ARDUINO
-DBOARD_ESP32
; -DCONFIG_THREADED_INPUTS
-DCONFIG_THREADED_INPUTS
lib_deps =
${common_env_data.lib_deps_external}
src_filter = "${common_env_data.src_filter}"
board_build.partitions = no_ota.csv
monitor_filters = esp32_exception_decoder
monitor_speed = 115200
upload_speed = 115200
[env:esp8266-12f]
@ -140,11 +141,12 @@ src_build_flags =
${env:esp32.src_build_flags}
${config_bluetooth.src_build_flags}
${config_wifi.src_build_flags}
-DRENDERBUG_LED_PIN=13
-DRENDERBUG_LED_PIN=14
[env:esp32_wifi]
extends = env:esp32, config_wifi, config_mqtt
src_filter = "${env:esp32.src_filter} ${config_wifi.src_filter} ${config_mqtt.src_filter}"
buid_type = debug
lib_deps =
${env:esp32.lib_deps}
${config_mqtt.lib_deps}
@ -152,7 +154,34 @@ src_build_flags =
${env:esp32.src_build_flags}
${config_mqtt.src_build_flags}
${config_wifi.src_build_flags}
-DRENDERBUG_LED_PIN=13
-DRENDERBUG_LED_PIN=14
[env:esp32_display]
extends = env:esp32, config_u8display
src_filter = "${env:esp32.src_filter} ${config_u8display.src_filter}"
build_type = debug
lib_deps =
${env:esp32.lib_deps}
${config_u8display.lib_deps}
src_build_flags =
${env:esp32.src_build_flags}
${config_u8display.src_build_flags}
-DRENDERBUG_LED_PIN=14
[env:esp32_wifi_display]
extends = env:esp32, config_wifi, config_mqtt, config_u8display
src_filter = "${env:esp32.src_filter} ${config_wifi.src_filter} ${config_mqtt.src_filter} ${config_u8display.src_filter}"
build_type = debug
lib_deps =
${env:esp32.lib_deps}
${config_mqtt.lib_deps}
${config_u8display.lib_deps}
src_build_flags =
${env:esp32.src_build_flags}
${config_mqtt.src_build_flags}
${config_wifi.src_build_flags}
${config_u8display.src_build_flags}
-DRENDERBUG_LED_PIN=14
[env:prototype]
extends = env:esp32, config_buttons, config_mpu5060

View File

@ -1,9 +1,11 @@
#include "BootOptions.h"
#include "Config.h"
#include <EEPROM.h>
#ifdef BOARD_ESP8266
#include <ESP8266WiFi.h>
#endif
#include <EEPROM.h>
#include "Config.h"
#ifdef PLATFORM_PHOTON
LEDStatus serialStatus = LEDStatus(RGB_COLOR_ORANGE, LED_PATTERN_FADE, LED_SPEED_FAST, LED_PRIORITY_BACKGROUND);
@ -12,6 +14,10 @@ retained bool LAST_BOOT_WAS_FLASH;
retained bool LAST_BOOT_WAS_SERIAL;
#endif
#ifdef BOARD_ESP32
__NOINIT_ATTR uint8_t s_rebootCount = 0;
#endif
void
BootOptions::initPins()
{
@ -36,9 +42,24 @@ BootOptions::BootOptions()
configStatus.setActive(isSetup);
serialStatus.setActive(isSerial);
#endif
#ifdef BOARD_ESP32
resetReason = esp_reset_reason();
crashCount = s_rebootCount;
if (resetReason >= 4) { // TODO: These values are defined in
// esp32/rom/rtc.h, but not sure if that's included
// on platformio builds
if (crashCount++ >= 3) {
// Boot into safe mode if the watchdog reset us three times in a row.
isSafeMode = true;
}
} else {
crashCount = 0;
}
s_rebootCount = crashCount;
#endif
#ifdef BOARD_ESP8266
struct rst_info resetInfo = *ESP.getResetInfoPtr();
uint8_t crashCount;
resetReason = resetInfo.reason;
EEPROM.begin(sizeof(crashCount));
EEPROM.get(sizeof(HardwareConfig) + 32, crashCount);
EEPROM.end();

View File

@ -1,4 +1,5 @@
#pragma once
#include <Arduino.h>
struct BootOptions {
static void initPins();
@ -12,4 +13,6 @@ struct BootOptions {
bool isFlash = false;
bool lastBootWasFlash = false;
bool isSafeMode = false;
uint8_t crashCount = 0;
uint8_t resetReason = 0;
};

View File

@ -9,7 +9,51 @@
#include <LittleFS.h>
#include <vector>
StaticJsonDocument<256> jsonConfig;
void
ConfigTaskMixin::handleEvent(const InputEvent &evt)
{
if (evt.intent == InputEvent::ConfigurationChanged) {
const JsonObject& cfg = *evt.as<JsonObject>();
handleConfigChange(Configuration(cfg));
}
}
Configuration::Configuration(const JsonObject& data)
: m_json(data)
{
}
const char*
Configuration::get(const char* key, const char* defaultVal) const
{
if (m_json.containsKey(key)) {
return m_json[key];
} else {
return defaultVal;
}
}
int
Configuration::get(const char* key, int defaultVal) const
{
if (m_json.containsKey(key)) {
return m_json[key];
} else {
return defaultVal;
}
}
bool
Configuration::get(const char* key, bool defaultVal) const
{
if (m_json.containsKey(key)) {
return m_json[key];
} else {
return defaultVal;
}
}
StaticJsonDocument<1024> jsonConfig;
constexpr uint16_t HardwareConfig::MAX_LED_NUM;
@ -77,14 +121,20 @@ ConfigService::onStart()
m_config = HardwareConfig{};
m_config.save();
}
if (strlen(m_config.data.loadedProfile) == 0) {
strcpy(m_config.data.loadedProfile, "default");
}
bool loaded = false;
if (m_overrideProfile != nullptr) {
loadProfile(m_overrideProfile);
} else {
loadProfile(m_config.data.loadedProfile);
loaded = loadProfile(m_overrideProfile);
}
if (!loaded && strlen(m_config.data.loadedProfile) > 0) {
loaded = loadProfile(m_config.data.loadedProfile);
}
if (!loaded && !loadProfile("default")) {
Log.fatal("Could not load default fallback profile! No tasks will be started.");
m_jsonMap.loadDefault();
}
}
@ -94,24 +144,24 @@ ConfigService::overrideProfile(const char* profileName)
m_overrideProfile = profileName;
}
void
bool
ConfigService::loadMap(const String& mapName)
{
String fname = String("/maps/") + mapName + ".json";
if (LittleFS.exists(fname)) {
File configFile = LittleFS.open(fname, "r");
Log.notice("config: Loading coordinate map %s", mapName.c_str());
Log.notice("config: Loading coordinate map from %s", fname.c_str());
deserializeJson(jsonConfig, configFile);
configFile.close();
JsonArray strideList = jsonConfig["strides"];
m_jsonMap.load(strideList);
return true;
} else {
Log.warning("config: Couldn't load coordinate map %s!!! Defaulting to linear mapping.", mapName.c_str());
m_jsonMap.loadDefault();
return false;
}
}
void
bool
ConfigService::loadProfile(const char* profileName)
{
Log.notice("config: Loading profile %s...", profileName);
@ -119,11 +169,16 @@ ConfigService::loadProfile(const char* profileName)
LittleFS.begin();
if (LittleFS.exists(fname)) {
strncpy(m_config.data.loadedProfile, fname.c_str(), sizeof(m_config.data.loadedProfile));
File configFile = LittleFS.open(fname, "r");
jsonConfig.clear();
deserializeJson(jsonConfig, configFile);
configFile.close();
//int profileLogLevel = max(0, min(6, jsonConfig["logLevel"]));
//Log.setLevel(profileLogLevel);
//Log.trace("config: \t %d logging level");
JsonObject sceneList = jsonConfig["scenes"];
std::vector<Sequencer::Scene> scenes;
for(JsonPair pair : sceneList) {
@ -139,25 +194,37 @@ ConfigService::loadProfile(const char* profileName)
JsonArray taskList = jsonConfig["tasks"];
Log.notice("config: Starting %d tasks", taskList.size());
for(int i = 0; i < taskList.size();i++) {
MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, taskList[i].as<const char*>()});
MainLoop::instance()->dispatchSync(InputEvent{InputEvent::StartThing, taskList[i].as<const char*>()});
}
JsonObject defaults = jsonConfig["defaults"];
Log.notice("config: Loading %d app configurations", defaults.size());
MainLoop::instance()->dispatchSync(InputEvent{InputEvent::ConfigurationChanged, &defaults});
String mapName = jsonConfig["surfaceMap"];
jsonConfig.clear();
if (mapName.isEmpty()) {
Log.warning("config: No coordinate map defined! Defaulting to linear mapping.", mapName.c_str());
m_jsonMap.loadDefault();
} else if (!loadMap(mapName)) {
Log.warning("config: Couldn't load coordinate map %s!!! Defaulting to linear mapping.", mapName.c_str());
m_jsonMap.loadDefault();
}
Log.notice("config: Loaded!");
} else {
Log.warning("config: Could not load profile %s!", profileName);
return false;
}
JsonObject defaults = jsonConfig["defaults"];
MainLoop::instance()->dispatch(InputEvent{InputEvent::ConfigurationChanged, &defaults});
String configName = jsonConfig["surfaceMap"];
jsonConfig.clear();
loadMap(configName);
LittleFS.end();
Log.notice("config: Configured to use %d pixels", m_jsonMap.physicalPixelCount());
PhysicalCoordinates topLeft = m_jsonMap.virtualToPhysicalCoords({0, 0});
PhysicalCoordinates bottomRight = m_jsonMap.virtualToPhysicalCoords({255, 255});
Log.verbose(" (0,0) -> (%d, %d) -> %d", topLeft.x, topLeft.y, m_jsonMap.physicalCoordsToIndex(topLeft));
Log.verbose(" (255,255) -> (%d, %d) -> %d", bottomRight.x, bottomRight.y, m_jsonMap.physicalCoordsToIndex(bottomRight));
return true;
}
void
@ -177,7 +244,6 @@ ConfigService::handleEvent(const InputEvent &evt)
switch(evt.intent) {
case InputEvent::LoadConfigurationByName:
Log.notice("Reloading configuration %s", evt.asString());
strcpy(m_config.data.loadedProfile, evt.asString());
loadProfile(evt.asString());
case InputEvent::SaveConfigurationRequest:
Log.notice("Saving configuration");

View File

@ -1,6 +1,27 @@
#pragma once
#include <Figments.h>
#include "JsonCoordinateMapping.h"
#include <ArduinoJson.h>
class Configuration {
public:
Configuration(const JsonObject& data);
const char* get(const char* key, const char* defaultVal) const;
int get(const char* key, int defaultVal) const;
bool get(const char* key, bool defaultVal) const;
private:
const JsonObject& m_json;
};
class ConfigTaskMixin : public virtual Loopable {
public:
void handleEvent(const InputEvent &evt) override;
void loop() override {}
virtual void handleConfigChange(const Configuration& config) {}
};
struct HardwareConfig {
uint8_t version = 3;
@ -44,6 +65,6 @@ private:
JsonCoordinateMapping m_jsonMap;
const char* m_overrideProfile = nullptr;
void loadProfile(const char* name);
void loadMap(const String& mapName);
bool loadProfile(const char* name);
bool loadMap(const String& mapName);
};

View File

@ -48,6 +48,8 @@ LogService::intentName(InputEvent::Intent intent)
return "set-display-length";
case InputEvent::SaveConfigurationRequest:
return "save-configuration";
case InputEvent::ConfigurationChanged:
return "configuration-changed";
default:
return NULL;
}
@ -67,6 +69,10 @@ LogService::eventValue(const InputEvent& evt)
snprintf(s_valueBuf, sizeof(s_valueBuf), "\"%s\"", evt.asString());break;
case InputEvent::Color:
snprintf(s_valueBuf, sizeof(s_valueBuf), "[%d, %d, %d]", evt.asRGB().r, evt.asRGB().g, evt.asRGB().b);break;
case InputEvent::Pointer:
snprintf(s_valueBuf, sizeof(s_valueBuf), "*%p", evt.as<void*>());break;
default:
snprintf(s_valueBuf, sizeof(s_valueBuf), "CORRUPTED");break;
}
return s_valueBuf;
}

View File

@ -74,6 +74,9 @@ Platform::freeRam()
#ifdef BOARD_ESP8266
return ESP.getFreeHeap();
#endif
#ifdef BOARD_ESP32
return ESP.getFreeHeap();
#endif
}
void
@ -206,6 +209,28 @@ Platform::deviceID()
return s_deviceID;
}
void
Platform::addLEDs(CRGB* leds, unsigned int ledCount) {
FastLED.addLeds<WS2812B, RENDERBUG_LED_PIN, RENDERBUG_LED_PACKING>(leds, ledCount);
}
const String
Platform::model()
{
static String modelName = String("Renderbug " ) + Platform::name();
return modelName;
}
void
Platform::restart() {
#ifdef BOARD_ESP8266
ESP.wdtDisable();
ESP.restart();
#elif defined(BOARD_ESP32)
ESP.restart();
#endif
}
BootOptions
Platform::bootopts;

View File

@ -11,17 +11,11 @@ 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) {
FastLED.addLeds<WS2812B, RENDERBUG_LED_PIN, RENDERBUG_LED_PACKING>(leds, ledCount);
}
static void addLEDs(CRGB* leds, unsigned int ledCount);
static const char* name();
static const char* version();
static const String model() {
static String modelName = String("Renderbug " ) + Platform::name();
return modelName;
}
static const String model();
static const String deviceName() {
static String devName = model() + " " + Platform::deviceID();
return devName;
@ -110,12 +104,5 @@ class Platform : public Task {
return task_iterator(NULL);
}
static void restart() {
#ifdef BOARD_ESP8266
ESP.wdtDisable();
ESP.restart();
#elif defined(BOARD_ESP32)
ESP.restart();
#endif
}
static void restart();
};

View File

@ -50,7 +50,6 @@ Sequencer::onStart()
void
Sequencer::setScenes(std::vector<Scene> &&scenes)
{
Log.notice("Updated scenes");
m_idx = 0;
m_scenes = scenes;
}

View File

@ -0,0 +1,24 @@
#include "./Power.h"
#include "../Static.h"
#include <ArduinoJson.h>
void
Power::handleConfigChange(const Configuration& config)
{
m_milliamps = config.get("power.milliamps", m_milliamps);
m_voltage = config.get("power.volts", m_voltage);
m_useBPM = config.get("power.useBPM", m_useBPM);
if (m_voltage == 0 || m_milliamps == 0) {
Log.notice("power: Impossible power config: %dma @ %dv", m_milliamps, m_voltage);
m_valid = false;
} else {
Log.notice("power: Configured to use %dma @ %dv", m_milliamps, m_voltage);
m_valid = true;
FastLED.setMaxPowerInVoltsAndMilliamps(m_voltage, m_milliamps);
}
}
STATIC_ALLOC(Power);
STATIC_TASK(Power);

View File

@ -1,8 +1,8 @@
#pragma once
#include <Figments.h>
#include "../Config.h"
template<uint8_t MaxBrightness = 255, uint32_t MaxMilliAmps = 500, uint32_t Voltage = 5>
class Power: public Figment {
class Power: public Figment, ConfigTaskMixin {
public:
Power() : Figment("Power") {state = Task::Running;}
@ -24,11 +24,14 @@ public:
m_beatDecay.set(0, 255);
break;
default:
return;
ConfigTaskMixin::handleEvent(evt);
}
}
void handleConfigChange(const Configuration& config) override;
void loop() override {
ConfigTaskMixin::loop();
m_powerState.update();
m_brightness.update();
EVERY_N_MILLISECONDS(20) {
@ -37,18 +40,23 @@ public:
}
void render(Display* dpy) const override {
const uint8_t decayedBrightness = scale8((uint8_t)m_brightness, ease8InOutCubic((uint8_t)m_beatDecay));
const uint8_t clippedBrightness = std::min(decayedBrightness, MaxBrightness);
if (F_LIKELY(m_valid)) {
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);
const uint8_t videoBrightness = brighten8_video(scaledBrightness);
const uint8_t powerBrightness = calculate_max_brightness_for_power_mW(videoBrightness, Watts);
const uint8_t powerBrightness = calculate_max_brightness_for_power_mW(videoBrightness, m_voltage * m_milliamps);
FastLED.setBrightness(powerBrightness);
}
}
static constexpr uint32_t Watts = Voltage * MaxMilliAmps;
private:
AnimatedNumber m_powerState = 255;
AnimatedNumber m_brightness = MaxBrightness;
AnimatedNumber m_brightness = 255;
AnimatedNumber m_beatDecay = 255;
uint8_t m_voltage = 5;
uint16_t m_milliamps = 500;
bool m_valid = true;
bool m_useBPM = false;
};

View File

@ -9,7 +9,7 @@ void SolidAnimation::randomize() {
m_blobs.forEach([](Blob& blob) {
blob.setPos(random(140));
blob.setBrightness(random(255));
if (random(255) % 2) {
if (random(254) % 2) {
blob.setVelocity(-1);
}
});
@ -26,7 +26,7 @@ void SolidAnimation::handleEvent(const InputEvent& evt) {
m_curColor = nextColor;
m_horizontal = !m_horizontal;
} else if (evt.intent == InputEvent::Beat) {
m_isRandom = false;
//m_isRandom = false;
}
}
@ -34,13 +34,11 @@ void SolidAnimation::loop() {
if (!m_isRandom) {
randomize();
}
m_red.update(15);
m_green.update(15);
m_blue.update(15);
EVERY_N_MILLIS(16) {
m_changePct.update(12);
}
EVERY_N_MILLIS(6) {
EVERY_N_MILLIS(20) {
m_changePct.update(1);
m_red.update(1);
m_green.update(1);
m_blue.update(1);
CRGB rgb{m_red, m_green, m_blue};
CHSV hsv = rgb2hsv_approximate(rgb);
m_blobs.forEach([=](Blob& blob) {
@ -56,15 +54,17 @@ void SolidAnimation::loop() {
void SolidAnimation::render(Display* dpy) const {
PerfCounter _("solidRender");
CRGB color(m_red.value(), m_green.value(), m_blue.value());
CRGB scaledPrev = m_prevColor;
scaledPrev = color.nscale8(30);
uint8_t frame = ease8InOutApprox(m_changePct);
if (frame == 255) {
Surface(dpy, {0, 0}, {255, 255}) = color;
if (F_LIKELY(frame == 255)) {
Surface(dpy, {0, 0}, {255, 255}) = color.nscale8(10);
} else {
uint8_t cutoff = (frame / 2);
uint8_t rotation = m_horizontal ? 0 : 128;
Surface(dpy, {0, 0}, {128 - cutoff, 255}, rotation) = m_prevColor;
Surface(dpy, {128 - cutoff, 0}, {128 + cutoff, 255}, rotation) = color;
Surface(dpy, {128 + cutoff, 0}, {255, 255}, rotation) = m_prevColor;
Surface(dpy, {0, 0}, {128 - cutoff, 255}, rotation) = scaledPrev;
Surface(dpy, {128 - cutoff, 0}, {128 + cutoff, 255}, rotation) = scaledPrev;
Surface(dpy, {128 + cutoff, 0}, {255, 255}, rotation) = scaledPrev;
}
m_blobs.render(dpy);
}

View File

@ -1,26 +1,40 @@
#pragma once
#include <Figments.h>
#include <ArduinoJson.h>
#include "../Config.h"
class BPM : public InputSource {
class BPM : public InputSource, ConfigTaskMixin {
public:
BPM() : InputSource("BPM") {}
void handleEvent(const InputEvent& evt) override {
if (evt.intent == InputEvent::BeatDetect) {
m_nextBpm = millis();
m_timings.insert(millis());
Log.notice("%d timings", m_timings.size());
Log.trace("bpm: %d timings", m_timings.size());
if (m_timings.size() >= 5) {
updateBPM();
}
}
ConfigTaskMixin::handleEvent(evt);
}
void loop() {
InputSource::loop();
ConfigTaskMixin::loop();
}
void handleConfigChange(const Configuration& cfg) override {
double requestedBPM = cfg.get("bpm.idle", msToBPM(m_msPerBeat));
m_msPerBeat = 60000.0 / (double)requestedBPM;
Log.notice("bpm: idle BPM set to %d (requested %d)", (int)msToBPM(m_msPerBeat), (int)requestedBPM);
}
InputEvent read() override {
if (m_bpm > 0) {
if (m_msPerBeat > 0) {
uint16_t now = millis();
if (now >= m_nextBpm) {
m_nextBpm += m_bpm;
return InputEvent{InputEvent::Beat, m_bpm};
m_nextBpm += m_msPerBeat;
return InputEvent{InputEvent::Beat, msToBPM(m_msPerBeat)};
}
if (now >= m_nextLearn && m_nextLearn != 0) {
m_timings.clear();
@ -31,21 +45,28 @@ public:
}
private:
uint16_t m_bpm = 0;
uint16_t m_msPerBeat = 60000;
uint16_t m_nextBpm = 0;
uint16_t m_nextLearn = 0;
Ringbuf<uint16_t, 7> m_timings;
constexpr uint16_t msToBPM(float msPerBeat) const {
if (msPerBeat == 0) {
return 0;
}
return 60000.0 / msPerBeat;
}
void updateBPM() {
uint16_t avgDelta = 0;
for(uint8_t i = 0; i < m_timings.size() - 1; i++) {
uint16_t delta = m_timings.peek(i+1) - m_timings.peek(i);
Log.notice("Timing %d Delta %d", m_timings.peek(i), delta);
Log.trace("bpm: Timing %d Delta %d", m_timings.peek(i), delta);
avgDelta += delta;
}
m_bpm = avgDelta / 4;
m_nextLearn = m_bpm * 5 + millis();
Log.notice("BPM is now %d", m_bpm);
m_msPerBeat = avgDelta / 4;
m_nextLearn = m_msPerBeat * 5 + millis();
Log.notice("bpm: BPM is now %d", msToBPM(m_msPerBeat));
uint16_t trash;
m_timings.take(trash);
}

View File

@ -15,7 +15,6 @@ public:
}
if (m_reset) {
m_reset = false;
Log.notice("Cycling %s color to %d [%d, %d, %d]", name, m_idx, m_colors[m_idx].r, m_colors[m_idx].g, m_colors[m_idx].b);
return InputEvent{InputEvent::SetColor, m_colors[m_idx]};
}
return InputEvent{};

View File

@ -13,30 +13,14 @@
#include <time.h>
#include "animations/Power.h"
#include "inputs/ColorCycle.h"
#include "inputs/Buttons.h"
#include "SafeMode.h"
#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());
// Setup power management
Power<MAX_BRIGHTNESS, PSU_MILLIAMPS> power;
REGISTER_TASK(power);
// FIXME: rewrite as static task
/*FigmentFunc configDisplay([](Display* dpy) {
uint8_t brightness = brighten8_video(beatsin8(60));
@ -50,24 +34,6 @@ REGISTER_TASK(power);
}
});*/
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");
REGISTER_TASK(randomPulse);
InputMapper keyMap([](const InputEvent& evt) {
if (evt.intent == InputEvent::UserInput) {
Buttons::Chord chord = (Buttons::Chord)evt.asInt();
@ -90,7 +56,6 @@ InputMapper keyMap([](const InputEvent& evt) {
REGISTER_TASK(keyMap);
// Cycle some random colors
ColorSequenceInput<9> idleCycle{{
CRGB(0, 123, 167), // Cerulean
@ -119,13 +84,23 @@ void setup() {
// Turn on,
Platform::preSetup();
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"🐞 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[15];
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);
Log.notice(u8"Setting up platform...");
Log.trace(u8"Setting up platform...");
Platform::setup();
Platform::bootSplash();
@ -135,7 +110,7 @@ void setup() {
// Tune in,
if (Platform::bootopts.isSafeMode) {
Log.notice(u8"⚠️ Starting Figment in safe mode!!!");
Log.error(u8"⚠️ Starting Figment in safe mode!!!");
runner = &SafeMode::safeModeApp;
FastLED.showColor(CRGB(5, 0, 0));
FastLED.show();
@ -143,7 +118,7 @@ void setup() {
Log.notice(u8"🌌 Starting Figment...");
if (Platform::bootopts.isSetup) {
Log.notice(u8"🔧 Booting up into setup profile!!!");
Log.warning(u8"🔧 Booting up into setup profile!!!");
Static<ConfigService>::instance()->overrideProfile("setup");
}

View File

@ -108,6 +108,7 @@ MQTTTelemetry::MQTTTelemetry() : BufferedInputSource("MQTT"),
m_logPrinter(this)
{
m_debugTopic = String("renderbug/") + Platform::deviceID();
memset(m_hostBuf, 0, sizeof(m_hostBuf));
}
void
@ -199,10 +200,13 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt)
}
void
MQTTTelemetry::handleConfigChange(const InputEvent& event)
MQTTTelemetry::handleConfigChange(const Configuration& cfg)
{
const JsonObject& obj = event.as<JsonObject>();
strncpy(m_hostBuf, obj["mqtt.ip"].as<JsonString>().c_str(), sizeof(m_hostBuf));
Log.notice("Config change in mqtt");
const char* newIP = cfg.get("mqtt.ip", m_hostBuf);
Log.notice("ip: %s", newIP);
strncpy(m_hostBuf, newIP, sizeof(m_hostBuf));
m_hostBuf[sizeof(m_hostBuf)-1] = 0;
m_mqtt.disconnect();
}
@ -217,6 +221,7 @@ MQTTTelemetry::loop()
void
MQTTTelemetry::handleEvent(const InputEvent& evt)
{
BufferedInputSource::handleEvent(evt);
OnlineTaskMixin::handleEvent(evt);
ConfigTaskMixin::handleEvent(evt);
}
@ -225,7 +230,11 @@ void
MQTTTelemetry::onOnline()
{
m_needHeartbeat = true;
if (strlen(m_hostBuf) > 0) {
m_mqtt.setServer(m_hostBuf, 1883);
} else {
Log.warning("mqtt.ip is not configured, no connection possible");
}
m_mqtt.setBufferSize(1024);
m_mqtt.setCallback(&MQTTTelemetry::s_callback);
}

View File

@ -3,6 +3,7 @@
#include <PubSubClient.h>
#include <ArduinoLog.h>
#include "../../Config.h"
#include "../../Sequencer.h"
@ -47,7 +48,7 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin, ConfigTaskMix
void handleEvent(const InputEvent& evt) override;
void handleEventOnline(const InputEvent& evt) override;
void handleConfigChange(const InputEvent& evt) override;
void handleConfigChange(const Configuration& cfg) override;
void loop() override;
void loopOnline() override;

View File

@ -18,6 +18,7 @@ class U8Display : public Task {
};
void onStart() {
Log.trace("display: starting redraw thread");
xTaskCreatePinnedToCore(
&U8Display::redrawTask,
name,
@ -28,6 +29,11 @@ class U8Display : public Task {
);
}
void onStop() {
Log.trace("display: stopping redraw thread");
vTaskDelete(m_renderTask);
}
void handleEvent(const InputEvent& evt) {
m_lastEvent = evt;
if (m_state == Idle) {

View File

@ -70,21 +70,5 @@ public:
pixel += blend(CRGB(blobColor), pixel, 80);
});
/*uint8_t scaledWidth = std::abs(endPos.x - startPos.x);
//Log.notice("blob w=%d x=%d", scaledWidth, startPos.x);
for(uint8_t i = 0;i < scaledWidth; i++) {
// Blobs desaturate towards their tail
//Log.notice("blob i=%d w=%d x=%d", i, scaledWidth, startPos.x);
uint8_t scalePct = map8(i, 0, scaledWidth);
uint8_t val = lerp8by8(0, m_brightness, scalePct);
//CHSV blobColor(m_hue, m_saturation, quadwave8((i / (double)scaledWidth) * m_brightness));
CHSV blobColor(m_hue, m_saturation, quadwave8(val));
PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), startPos.y};
CRGB src(display->pixelAt(pos));
display->pixelAt(pos) = blend(CRGB(blobColor), src, 140);
}*/
}
};