build for esp32 mask project

This commit is contained in:
Torrie Fischer 2021-04-10 11:10:25 -07:00
parent 439a456d1a
commit 75bf48756b
24 changed files with 712 additions and 346 deletions

View File

@ -34,6 +34,12 @@ struct Task : public virtual Loopable {
State state = Running;
};
struct TaskFunc: public Task {
TaskFunc(std::function<void()> func) : Task("lambda"), func(func) {}
void loop() override {func();}
std::function<void()> func;
};
struct Figment: public Task {
Figment() : Task() {}
Figment(State initialState) : Task(initialState) {}

View File

@ -137,7 +137,7 @@ protected:
void setEvent(InputEvent::Intent intent, Variant &&v);
private:
Ringbuf<InputEvent, 5> m_eventQueue;
Ringbuf<InputEvent, 12> m_eventQueue;
};
class InputMapper: public BufferedInputSource {
@ -157,12 +157,20 @@ class OnlineTaskMixin : public virtual Loopable {
void handleEvent(const InputEvent &evt) override {
if (evt.intent == InputEvent::NetworkStatus) {
m_online = evt.asInt();
if (m_online) {
onOnline();
} else {
onOffline();
}
}
if (m_online) {
handleEventOnline(evt);
}
}
virtual void onOnline() {}
virtual void onOffline() {}
virtual void handleEventOnline(const InputEvent &evt) {}
void loop() override {

View File

@ -37,10 +37,27 @@ MainLoop::loop()
task->handleEvent(evt);
}
}
unsigned int slowest = 0;
unsigned int frameSpeed = 0;
unsigned int frameStart = millis();
unsigned int taskCount = 0;
Task* slowestTask = NULL;
for(Task* task : scheduler) {
//Log.notice("Running %s", task->name);
//unsigned int start = millis();
unsigned int start = ESP.getCycleCount();
task->loop();
//Log.notice("next");
//unsigned int runtime = millis() - start;
unsigned int runtime = ESP.getCycleCount() - start;
frameSpeed += runtime;
taskCount++;
if (runtime > slowest) {
slowest = runtime;
slowestTask = task;
}
}
frameSpeed = millis() - frameStart;
if (frameSpeed >= 23) {
Log.notice("Slow frame: %dms, %d tasks, longest task %s was %dms", frameSpeed, taskCount, slowestTask->name, slowest/160000);
}
}

View File

@ -9,11 +9,12 @@ Renderer::loop()
for(Display* dpy : m_displays) {
for(Figment* figment : m_figments) {
if (figment->state == Task::Running) {
//Log.notice("Rendering %s", figment->name);
unsigned int frameStart = ESP.getCycleCount();
figment->render(dpy);
//Log.notice("next");
} else {
//Log.notice("Not rendering %s", figment->name);
unsigned int runtime = (ESP.getCycleCount() - frameStart) / 160000;
if (runtime >= 8) {
Log.notice("SLOW RENDER: %s took %dms!", figment->name, runtime);
}
}
};
}

View File

@ -1,46 +0,0 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

4
no_ota.csv Normal file
View File

@ -0,0 +1,4 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x1000,
app, app, factory, 0x10000, 2M,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x1000
4 app app factory 0x10000 2M

33
out.log
View File

@ -1,33 +0,0 @@
ets Jun 8 2016 00:22:57
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:1044
load:0x40078000,len:10124
load:0x40080400,len:5828
entry 0x400806a8
N: 🐛 Booting Renderbug!
N: 🐞 I am built for 255 LEDs running on 2000mA
N: Boot pin configuration:
N: 2: Setup - 0
N: 3: Serial - 0
N: 4: Flash - 0
N: 💡 Starting FastLED...
N: 🌌 Starting Figment...
N: *** Starting 20 tasks...
N: * Starting Configuration...
N: * Starting MPU5060...
N: * Starting Buttons...
N: * Starting ...
N: * Starting SceneSequencer...
N: * Starting CircadianRhythm...
N: * Starting Pulse...
N: * Starting Solid...
N: * Starting Power...
N: * Starting lambda...
N: * Starting UpdateStatusAnimation...
N: * Starting Renderer...
N: 🚀 Setup complete! Ready to rock and roll.

View File

@ -11,13 +11,15 @@
[common_env_data]
src_filter = "+<*> -<.git/> -<.svn/> -<platform/>"
[env:featheresp32]
[env:esp32]
platform = espressif32
board = featheresp32
framework = arduino
build_flags =
-DPLATFORM_ARDUINO
-DBOARD_ESP32
-DCONFIG_NO_COLORDATA
; -DCORE_DEBUG_LEVEL=5
lib_deps =
fastled/FastLED@^3.4.0
thijse/ArduinoLog@^1.0.3
@ -26,8 +28,14 @@ lib_deps =
sstaub/NTP@^1.4.0
arduino-libraries/NTPClient@^3.1.0
src_filter = "${common_env_data.src_filter} +<platform/arduino/>"
board_build.partitions = no_ota.csv
;build_type = debug
[env:huzzah]
[env:cyberplague]
extends = env:esp32
board_build.partitions = no_ota.csv
[env:esp8266]
platform = espressif8266
board = huzzah
framework = arduino
@ -42,3 +50,8 @@ lib_deps =
sstaub/NTP@^1.4.0
arduino-libraries/NTPClient@^3.1.0
src_filter = "${common_env_data.src_filter} +<platform/arduino/>"
;[env:photon]
;platform = particlephoton
;board = photon
;framework = arduino

View File

@ -1,4 +1,9 @@
#include "BootOptions.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);
@ -31,6 +36,28 @@ BootOptions::BootOptions()
configStatus.setActive(isSetup);
serialStatus.setActive(isSerial);
#endif
#ifdef BOARD_ESP8266
struct rst_info resetInfo = *ESP.getResetInfoPtr();
uint8_t crashCount;
EEPROM.begin(sizeof(crashCount));
EEPROM.get(sizeof(HardwareConfig) + 32, crashCount);
EEPROM.end();
if (resetInfo.reason == REASON_WDT_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) {
crashCount = 0;
EEPROM.begin(sizeof(crashCount));
EEPROM.put(sizeof(HardwareConfig) + 32, crashCount);
EEPROM.end();
}
#endif
}
void

View File

@ -11,4 +11,5 @@ struct BootOptions {
bool isSerial = false;
bool isFlash = false;
bool lastBootWasFlash = false;
bool isSafeMode = false;
};

View File

@ -8,7 +8,10 @@ constexpr uint16_t HardwareConfig::MAX_LED_NUM;
HardwareConfig
HardwareConfig::load() {
HardwareConfig ret;
EEPROM.begin(sizeof(ret));
EEPROM.get(0, ret);
EEPROM.end();
Log.notice("Loaded config version %d, CRC %d", ret.version, ret.checksum);
return ret;
}
@ -16,7 +19,10 @@ void
HardwareConfig::save() {
HardwareConfig dataCopy{*this};
dataCopy.checksum = getCRC();
EEPROM.begin(sizeof(dataCopy));
EEPROM.put(0, dataCopy);
EEPROM.commit();
EEPROM.end();
}
LinearCoordinateMapping
@ -66,13 +72,13 @@ ConfigService::onStart()
m_coordMap = m_config.toCoordMap();
Log.notice("Configured to use %d pixels, starting at %d", m_config.data.pixelCount, m_config.data.startPixel);
Log.notice("Loading task states...");
/*Log.notice("Loading task states...");
for(int i = 0; i < 32; i++) {
auto svc = m_config.data.serviceStates[i];
if (strlen(svc.name) > 0) {
if (strnlen(svc.name, 16) > 0) {
Log.notice("* %s: %s", svc.name, svc.isDisabled? "DISABLED" : "ENABLED");
}
}
}*/
}
void

View File

@ -81,7 +81,7 @@ LogService::handleEvent(const InputEvent& evt) {
}
if (evt.intent != m_lastEvent.intent) {
if (m_duplicateEvents > 0) {
Log.notice("Suppressed reporting %u duplicate events.", m_duplicateEvents);
Log.notice("Suppressed reporting %d duplicate events.", m_duplicateEvents);
}
Log.verbose("Event: %s", buf);
m_duplicateEvents = 0;

View File

@ -4,6 +4,8 @@
#ifdef BOARD_ESP32
#include <WiFi.h>
#include <esp_task_wdt.h>
#include <time.h>
#elif defined(BOARD_ESP8266)
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
@ -11,16 +13,23 @@
#include <ctime>
WiFiUDP wifiUdp;
NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7);
//NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7);
NTPClient timeClient(wifiUdp, "10.0.0.1", 3600 * -7);
#endif
#ifdef PLATFORM_PHOTON
STARTUP(BootOptions::initPins());
#else
#include "platform/arduino/MQTTTelemetry.h"
void printNewline(Print* logOutput) {
void printNewline(Print* logOutput)
{
logOutput->print("\r\n");
}
int printEspLog(const char* fmt, va_list args)
{
Log.notice(fmt, args);
return 1;
}
#endif
int Platform::s_timezone = 0;
@ -30,6 +39,10 @@ Platform::name()
{
#ifdef PLATFORM_PHOTON
return "Photon";
#elif defined(BOARD_ESP8266)
return "ESP8266";
#elif defined(BOARD_ESP32)
return "ESP32";
#else
return "Unknown!";
#endif
@ -40,6 +53,8 @@ Platform::version()
{
#ifdef PLATFORM_PHOTON
return System.version().c_str();
#elif defined(BOARD_ESP32)
return ESP.getSdkVersion();
#else
return "Unknown!";
#endif
@ -49,6 +64,7 @@ void
Platform::preSetup()
{
Serial.begin(115200);
delay(5000);
#ifdef PLATFORM_PHOTON
System.enableFeature(FEATURE_RETAINED_MEMORY);
if (bootopts.isFlash) {
@ -65,6 +81,15 @@ Platform::preSetup()
Log.begin(LOG_LEVEL_VERBOSE, Static<MQTTTelemetry>::instance()->logPrinter());
Log.setSuffix(printNewline);
#endif
#ifdef BOARD_ESP32
esp_task_wdt_init(10, true);
esp_task_wdt_add(NULL);
esp_log_set_vprintf(printEspLog);
#endif
#ifdef BOARD_ESP8266
ESP.wdtEnable(0);
#endif
}
void
@ -95,7 +120,12 @@ void
Platform::loop()
{
#ifdef BOARD_ESP8266
if (WiFi.status() == WL_CONNECTED) {
timeClient.update();
}
ESP.wdtFeed();
#elif defined(BOARD_ESP32)
esp_task_wdt_reset();
#endif
}
@ -110,9 +140,12 @@ Platform::getLocalTime(struct tm* timedata)
}
return false;
#elif defined(BOARD_ESP32)
return getLocalTime(timedata);
time_t rawtime;
time(&rawtime);
(*timedata) = (*localtime(&rawtime));
return (timedata->tm_year > (2016-1990));
//return getLocalTime(timedata);
#else
timeClient.update();
timedata->tm_hour = timeClient.getHours();
timedata->tm_min = timeClient.getMinutes();
return true;
@ -137,3 +170,5 @@ Platform::bootopts;
char
Platform::s_deviceID[15];
STATIC_ALLOC(Platform);

View File

@ -1,11 +1,13 @@
#pragma once
#include <FastLED.h>
#include <Figments.h>
#include "BootOptions.h"
class Platform {
class Platform : public Task {
static int s_timezone;
static char s_deviceID[15];
public:
Platform() : Task("Platform") {}
static BootOptions bootopts;
static void setTimezone(int tz) { s_timezone = tz; }
static int getTimezone() { return s_timezone; }
@ -14,18 +16,28 @@ class Platform {
#ifdef PLATFORM_PHOTON
FastLED.addLeds<NEOPIXEL, 6>(leds, ledCount);
#elif defined(BOARD_ESP32)
FastLED.addLeds<WS2812B, 13, RGB>(leds, ledCount);
FastLED.addLeds<WS2812B, 13, GRB>(leds, ledCount);
#else
FastLED.addLeds<WS2812B, 14, GRB>(leds, ledCount);
//FastLED.addLeds<WS2812B, 14, GRB>(leds, ledCount);
FastLED.addLeds<WS2812B, 14, RGB>(leds, ledCount);
#endif
}
static const char* name();
static const char* version();
static const String model() {
static String modelName = String("Renderbug " ) + Platform::name();
return modelName;
}
static const String deviceName() {
static String devName = model() + " " + Platform::deviceID();
return devName;
}
static void preSetup();
static void bootSplash();
static void setup();
static void loop();
void loop() override;
static bool getLocalTime(struct tm* timedata);
static const char* deviceID();
};

View File

@ -25,10 +25,13 @@ Sequencer::scenes() const
void
Sequencer::handleEvent(const InputEvent& evt)
{
if (evt.intent == InputEvent::SetPattern && evt.asString() == m_scenes[m_idx].name) {
return;
}
if (evt.intent == InputEvent::SetPattern || evt.intent == InputEvent::NextPattern || evt.intent == InputEvent::PreviousPattern) {
Log.notice("Switching pattern!");
for(const char* pattern : m_scenes[m_idx].patterns) {
Log.notice("Stopping %s", pattern);
//Log.notice("Stopping %s", pattern);
MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern});
}
@ -54,7 +57,7 @@ Sequencer::handleEvent(const InputEvent& evt)
}
for(const char* pattern : m_scenes[m_idx].patterns) {
Log.notice("Starting %s", pattern);
//Log.notice("Starting %s", pattern);
MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern});
}
}

View File

@ -17,21 +17,7 @@ WiFiTask::onStart()
{
Log.notice("Starting wifi...");
WiFi.mode(WIFI_STA);
int n = WiFi.scanNetworks();
if (n == 0) {
Log.notice("No wifi found");
} else {
for(int i = 0; i < n; ++i) {
Serial.print("WiFi: ");
Serial.println(WiFi.SSID(i));
}
}
WiFi.mode(WIFI_STA);
WiFi.begin("The Frequency", "thepasswordkenneth");
while(WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
}
InputEvent
@ -42,7 +28,7 @@ WiFiTask::read()
m_lastStatus = curStatus;
Log.verbose("WiFi Status: %d", curStatus);
if (curStatus == WL_CONNECTED) {
Log.notice("Connected!");
Log.notice("Connected! IP address is %s", WiFi.localIP().toString().c_str());
return InputEvent{InputEvent::NetworkStatus, true};
} else if (curStatus == WL_CONNECTION_LOST || curStatus == WL_DISCONNECTED) {
Log.notice("Lost wifi connection!");

View File

@ -1,6 +1,8 @@
#include "colors.h"
const ColorInfo color_data[] = {
#ifdef CONFIG_NO_COLORDATA
#else
{ "Air Superiority Blue", { 114, 160, 193 } },
{ "Alabama Crimson", { 163, 38, 56 } },
{ "Alice Blue", { 240, 248, 255 } },
@ -792,6 +794,7 @@ const ColorInfo color_data[] = {
{ "Yellow Orange", { 255, 174, 66 } },
{ "Zaffre", { 0, 20, 168 } },
{ "Zinnwaldite Brown", { 44, 22, 8 } },
#endif
{0, {0, 0, 0}},
};

View File

@ -1 +0,0 @@
firmware

View File

@ -12,7 +12,6 @@
#include "Static.h"
#include "Config.h"
#include "colors.h"
#include "Sequencer.h"
#include "LogService.h"
@ -36,6 +35,7 @@
#include "platform/particle/MDNSService.cpp"
#else
#include "WiFiTask.h"
#include "platform/arduino/BluetoothSerialTelemetry.h"
#include "platform/arduino/MQTTTelemetry.h"
#include <ArduinoOTA.h>
#endif
@ -55,9 +55,6 @@
CRGB leds[HardwareConfig::MAX_LED_NUM];
Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static<ConfigService>::instance()->coordMap());
LinearCoordinateMapping neckMap{60, 0};
Display neckDisplay(leds, HardwareConfig::MAX_LED_NUM, &neckMap);
// Setup power management
Power<MAX_BRIGHTNESS, PSU_MILLIAMPS> power;
@ -113,7 +110,7 @@ class ArduinoOTAUpdater : public BufferedInputSource {
}
void handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::NetworkStatus) {
if (evt.intent == InputEvent::NetworkStatus && evt.asInt()) {
Log.notice("Booting OTA");
m_online = true;
ArduinoOTA.begin();
@ -176,19 +173,17 @@ DrainAnimation drain{Task::Stopped};
Flashlight flashlight{Task::Stopped};
Sequencer sequencer{{
{"Idle", {"Solid", "MPU5060", "Pulse", "Hackerbots", "Kieryn", "CircadianRhythm"}},
{"Idle", {"Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"}},
{"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}},
{"Interactive", {"Drain", "CircadianRhythm"}},
{"Interactive", {"Drain", "MPU5060", "CircadianRhythm"}},
{"Flashlight", {"Flashlight"}},
{"Nightlight", {"Drain", "Pulse", "Noisebridge"}},
{"Gay", {"Solid", "Pulse", "Rainbow", "Hackerbots", "Kieryn"}},
{"Acid", {"Chimes", "Pulse", "MPU5060", "Hackerbots", "Rainbow"}},
{"Gay", {"Solid", "Pulse", "Rainbow", "Rainbow"}},
{"Acid", {"Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"}},
}};
// Render all layers to the displays
Renderer renderer{
//{&dpy, &neckDisplay},
{&dpy},
{
&chimes,
@ -207,29 +202,22 @@ Renderer configRenderer{
};
// Cycle some random colors
ColorSequenceInput<7> noisebridgeCycle{{colorForName("Red").rgb}, "Noisebridge", Task::Stopped};
ColorSequenceInput<13> kierynCycle{{
ColorSequenceInput<9> idleCycle{{
CRGB(0, 123, 167), // Cerulean
CRGB(80, 200, 120), // Emerald
CRGB(207, 113, 175), // Sky Magenta
}, "Kieryn", Task::Running};
ColorSequenceInput<7> rainbowCycle{{
colorForName("Red").rgb,
colorForName("Orange").rgb,
colorForName("Yellow").rgb,
colorForName("Green").rgb,
colorForName("Blue").rgb,
colorForName("Purple").rgb,
colorForName("White").rgb,
}, "Rainbow", Task::Stopped};
ColorSequenceInput<7> hackerbotsCycle{{
CRGB(128, 0, 128), // Purple
CRGB(255, 255, 255), // White
CRGB(0, 255, 255), // Cyan
}, "Hackerbots", Task::Running};
}, "IdleColors", Task::Running};
ColorSequenceInput<7> rainbowCycle{{
CRGB(255, 0, 0), // Red
CRGB(255, 127, 0), // Yellow
CRGB(0, 255, 0), // Green
CRGB(0, 0, 255), // Blue
CRGB(128, 0, 128), // Purple
}, "Rainbow", Task::Stopped};
struct ConfigInputTask: public BufferedInputSource {
public:
@ -324,7 +312,17 @@ std::array<ScheduleEntry, 10> schedule{{
{23, 20}
}};
uint8_t brightnessForTime(uint8_t hour, uint8_t minute) {
class CircadianRhythm : public InputSource {
private:
bool needsUpdate = true;
public:
CircadianRhythm() : InputSource("CircadianRhythm") {}
void onStart() {
needsUpdate = true;
}
uint8_t brightnessForTime(uint8_t hour, uint8_t minute) const {
ScheduleEntry start = schedule.back();
ScheduleEntry end = schedule.front();
for(ScheduleEntry cur : schedule) {
@ -355,16 +353,11 @@ uint8_t brightnessForTime(uint8_t hour, uint8_t minute) {
uint16_t duration = endTime - startTime;
uint16_t curDuration = nowTime - startTime;
uint8_t frac = ((double)curDuration / (double)duration) * 255.0;
uint8_t frac = map8(curDuration, 0, duration);
return lerp8by8(start.brightness, end.brightness, frac);
}
class CircadianRhythm : public InputSource {
private:
bool needsUpdate = true;
public:
CircadianRhythm() : InputSource("CircadianRhythm") {}
InputEvent read() {
EVERY_N_SECONDS(60) {
@ -390,6 +383,8 @@ STATIC_ALLOC(CircadianRhythm);
// A special mainloop app for configuring hardware settings that reboots the
// device when the user is finished.
MainLoop configApp{{
Static<Platform>::instance(),
// Manage read/write of configuration data
Static<ConfigService>::instance(),
@ -415,9 +410,45 @@ MainLoop configApp{{
&configRenderer,
}};
TaskFunc safeModeNag([]{
static uint8_t frame = 0;
EVERY_N_SECONDS(30) {
Log.notice("I am running in safe mode!");
}
EVERY_N_MILLISECONDS(16) {
frame++;
for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) {
leds[i] = CRGB(0, 0, 0);
}
for(int idx = 0; idx < 3; idx++) {
uint8_t length = beatsin8(5, 3, HardwareConfig::MAX_LED_NUM, 0, idx * 5);
for(int i = 0; i < length; i++) {
leds[i] += CRGB(scale8(5, beatsin8(5 + i * 7, 0, 255, 0, i*3)), 0, 0);
}
}
FastLED.show();
}
});
MainLoop safeModeApp({
Static<Platform>::instance(),
// ESP Wifi
Static<WiFiTask>::instance(),
// System logging
Static<LogService>::instance(),
// MQTT
Static<MQTTTelemetry>::instance(),
// OTA Updates
Static<ArduinoOTAUpdater>::instance(),
&safeModeNag,
});
// Turn on,
MainLoop renderbugApp{{
Static<Platform>::instance(),
// Load/update graphics configuration from EEPROM
Static<ConfigService>::instance(),
@ -431,18 +462,28 @@ MainLoop renderbugApp{{
Static<PhotonInput>::instance(),
#else
// ESP Wifi
Static<WiFiTask>::instance(),
//Static<WiFiTask>::instance(),
#endif
#ifdef BOARD_ESP32
// ESP32 Bluetooth
Static<BluetoothSerialTelemetry>::instance(),
#endif
// System logging
Static<LogService>::instance(),
#ifdef CONFIG_MPU5060
// Hardware drivers
Static<MPU5060>::instance(),
#endif
#ifdef CONFIG_BUTTONS
Static<Buttons>::instance(),
// Map buttons to events
&keyMap,
#endif
// Pattern sequencer
&sequencer,
@ -451,13 +492,11 @@ MainLoop renderbugApp{{
Static<CircadianRhythm>::instance(),
// Periodic motion input
&randomPulse,
//&randomPulse,
// Periodic color inputs
&noisebridgeCycle,
&kierynCycle,
&idleCycle,
&rainbowCycle,
&hackerbotsCycle,
// Animations
&chimes,
@ -521,8 +560,13 @@ void setup() {
Log.notice(u8"💡 Starting FastLED...");
Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM);
if (Platform::bootopts.isSetup) {
Log.notice(u8"🌌 Starting Figment in configuration mode...");
if (Platform::bootopts.isSafeMode) {
Log.notice(u8"⚠️ Starting Figment in safe mode!!!");
runner = safeModeApp;
FastLED.showColor(CRGB(5, 0, 0));
FastLED.show();
} else if (Platform::bootopts.isSetup) {
Log.notice(u8"🔧 Starting Figment in configuration mode...");
runner = configApp;
} else {
Log.notice(u8"🌌 Starting Figment...");
@ -537,5 +581,6 @@ void setup() {
// Drop out.
void loop() {
//Platform::loop();
runner.loop();
}

View File

@ -0,0 +1,86 @@
#include "BluetoothSerialTelemetry.h"
#include "../../Static.h"
#include "../../Platform.h"
#include <ArduinoLog.h>
#include "../../inputs/Buttons.h"
#include <cstdlib>
BluetoothSerialTelemetry::BluetoothSerialTelemetry() : InputSource("Bluetooth")
{
//m_serial.setPin("0000");
m_serial.enableSSP();
}
InputEvent
BluetoothSerialTelemetry::read()
{
bool didRead = false;
while (m_serial.available()) {
didRead = true;
char charRead = m_serial.read();
m_ringbuf.insert(charRead);
if (charRead == '*') {
static char commandBuf[32];
size_t cmdSize = m_ringbuf.write(commandBuf);
// Overwrite the '*' character, to leave us with a complete command
commandBuf[cmdSize-1] = 0;
//Log.notice("Bluetooth read %s", commandBuf);
if (commandBuf[0] == 'R') {
m_color = CRGB(std::atoi(&commandBuf[1]), m_color.g, m_color.b);
return InputEvent{InputEvent::SetColor, m_color};
} else if (commandBuf[0] == 'G') {
m_color = CRGB(m_color.r, std::atoi(&commandBuf[1]), m_color.b);
return InputEvent{InputEvent::SetColor, m_color};
} else if (commandBuf[0] == 'B') {
m_color = CRGB(m_color.r, m_color.g, std::atoi(&commandBuf[1]));
return InputEvent{InputEvent::SetColor, m_color};
} else if (commandBuf[0] == 'O') {
return InputEvent{InputEvent::UserInput, Buttons::Circle};
} else if (commandBuf[0] == 'S') {
return InputEvent{InputEvent::UserInput, Buttons::Triangle};
} else if (commandBuf[0] == 'X') {
return InputEvent{InputEvent::UserInput, Buttons::Cross};
} else if (commandBuf[0] == '+') {
return InputEvent{InputEvent::SetPower, 1};
} else if (commandBuf[0] == '-') {
return InputEvent{InputEvent::SetPower, 0};
} else if (commandBuf[0] == 'p') {
return InputEvent{InputEvent::SetPattern, &commandBuf[1]};
} else if (commandBuf[0] == 'A') {
char* axisVal = strtok(&commandBuf[1], ",");
const uint8_t accelX = std::atof(axisVal) * 10;
axisVal = strtok(NULL, ",");
const uint8_t accelY = std::atof(axisVal) * 10;
axisVal = strtok(NULL, ",");
const uint8_t accelZ = std::atof(axisVal) * 10;
const uint16_t accelSum = abs(accelX) + abs(accelY) + abs(accelZ);
const uint16_t delta = abs(m_value.value() - accelSum);
m_value.add(accelSum);
if (delta > 32) {
return InputEvent{InputEvent::Acceleration, delta};
}
}
}
}
if (didRead) {
return InputEvent::NetworkActivity;
} else {
return InputEvent{};
}
}
void
BluetoothSerialTelemetry::onStart()
{
Log.notice("Starting up Bluetooth...");
if (m_serial.begin(Platform::deviceName())) {
Log.notice("Bluetooth started!");
} else {
Log.warning("Bluetooth could not be started!");
}
}
STATIC_ALLOC(BluetoothSerialTelemetry);

View File

@ -0,0 +1,41 @@
#include <Figments.h>
#include <BluetoothSerial.h>
#include <Ringbuf.h>
class BluetoothSerialTelemetry : public InputSource {
public:
BluetoothSerialTelemetry();
void onStart() override;
InputEvent read() override;
template<typename T, uint8_t Size = 8>
struct Averager {
std::array<T, Size> buf;
unsigned int idx = 0;
unsigned int count = 0;
void add(const T &value) {
buf[idx] = value;
idx = (idx + 1) % Size;
if (count < Size) {
count += 1;
}
}
T value() const {
if (count == 0) {
return T{};
}
long long int sum = 0;
for(unsigned int i = 0; i < count; i++) {
sum += buf[i];
}
return sum / count;
}
};
private:
BluetoothSerial m_serial;
Ringbuf<char, 32> m_ringbuf;
CRGB m_color;
Averager<int16_t, 32> m_value;
};

View File

@ -1,107 +1,190 @@
#include "MQTTTelemetry.h"
#ifdef BOARD_ESP8266
#include <ESP8266WiFi.h>
#elif defined(BOARD_ESP32)
#include <WiFi.h>
#endif
#include <ArduinoJson.h>
#include "../../Static.h"
#include "../../Config.h"
#include "../../Platform.h"
WiFiClient wifiClient;
struct MQTTDevice {
const String id;
const String name;
const String model;
const String softwareVersion;
const String manufacturer;
const String availabilityTopic;
void toJson(const JsonObject& json) const {
json["name"] = name;
json["mdl"] = model;
json["sw"] = softwareVersion;
json["mf"] = manufacturer;
json["ids"][0] = id;
}
};
const String availTopic = String("renderbug/") + Platform::deviceID() + "/availability";
const MQTTDevice Device{
Platform::deviceID(),
Platform::deviceName(),
Platform::model(),
#ifdef BOARD_ESP8266
ESP.getSketchMD5(),
#else
"",
#endif
"Phong Robotics",
availTopic
};
struct MQTTEntity {
const MQTTDevice& device;
String name;
String entityId;
String rootTopic;
MQTTEntity(const String& domain, const MQTTDevice& device, const String& name) : device(device), name(Platform::deviceName() + " " + name) {
entityId = String(device.id) + "-" + name;
rootTopic = String("homeassistant/") + domain + String("/renderbug/") + entityId;
}
String configTopic() const {
return rootTopic + "/config";
}
String commandTopic() const {
return rootTopic + "/set";
}
String heartbeatTopic() const {
return String("renderbug/") + Device.id + "/heartbeat";
}
String statTopic() const {
return rootTopic + "/state";
}
bool isCommandTopic(const char* topic) const {
if (strncmp(topic, rootTopic.c_str(), rootTopic.length()) == 0) {
return strncmp(&topic[rootTopic.length()], "/set", sizeof("/set")) == 0;
}
return false;
}
void toJson(JsonDocument& jsonBuf, bool isInteractive = true) const {
jsonBuf["~"] = rootTopic.c_str();
jsonBuf["name"] = name;
jsonBuf["unique_id"] = entityId;
if (isInteractive) {
jsonBuf["cmd_t"] = "~/set";
jsonBuf["ret"] = true;
jsonBuf["schema"] = "json";
} else {
}
jsonBuf["stat_t"] = "~/state";
jsonBuf["json_attr_t"] = heartbeatTopic();
jsonBuf["avty_t"] = device.availabilityTopic;
device.toJson(jsonBuf.createNestedObject("dev"));
}
};
const MQTTEntity Lightswitch {
"light", Device, "lightswitch"
};
const MQTTEntity flashlightSwitch {
"switch", Device, "flashlight"
};
const MQTTEntity FPSSensor {
"sensor", Device, "fps"
};
MQTTTelemetry::MQTTTelemetry() : BufferedInputSource("MQTT"),
m_mqtt(PubSubClient(wifiClient)),
m_mqtt(m_wifi),
m_logPrinter(this)
{}
{
m_debugTopic = String("renderbug/") + Platform::deviceID();
}
void
MQTTTelemetry::handleEventOnline(const InputEvent& evt)
{
if (!m_mqtt.connected()) {
Log.notice("Connecting to MQTT...");
const IPAddress server(10, 0, 0, 2);
const char* deviceID = Platform::deviceID();
Log.verbose("Device ID %s", deviceID);
m_mqtt.setServer(server, 1883);
m_mqtt.setBufferSize(512);
m_mqtt.setCallback(&MQTTTelemetry::s_callback);
if (m_mqtt.connect(deviceID)) {
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");
m_needHeartbeat = true;
const String deviceName = String("Renderbug ESP8266") + (char*)deviceID;
const String rootTopic = String("homeassistant/light/renderbug/") + (char*)deviceID;
const String configTopic = rootTopic + "/config";
Log.verbose("root topic %s", rootTopic.c_str());
Log.verbose("config topic %s", configTopic.c_str());
const String statTopic = rootTopic + "/state";
const String cmdTopic = rootTopic + "/set";
const String attrTopic = rootTopic + "/attributes";
const String logTopic = rootTopic + "/log";
const String heartbeatTopic = rootTopic + "/heartbeat";
strcpy(m_statTopic, statTopic.c_str());
strcpy(m_attrTopic, attrTopic.c_str());
strcpy(m_cmdTopic, cmdTopic.c_str());
strcpy(m_logTopic, logTopic.c_str());
strcpy(m_heartbeatTopic, heartbeatTopic.c_str());
StaticJsonDocument<1024> configJson;
configJson["~"] = rootTopic;
configJson["name"] = deviceName;
configJson["ret"] = true;
configJson["unique_id"] = (char*) deviceID;
configJson["cmd_t"] = "~/set";
configJson["stat_t"] = "~/state";
configJson["json_attr_t"] = "~/attributes";
configJson["schema"] = "json";
configJson["brightness"] = true;
configJson["rgb"] = true;
Lightswitch.toJson(configJson);
int i = 0;
for(const Sequencer::Scene& scene : m_sequencer->scenes()) {
configJson["fx_list"][i++] = scene.name;
}
configJson["brightness"] = true;
configJson["rgb"] = true;
configJson["dev"]["name"] = "Renderbug";
#ifdef PLATFORM_PHOTON
configJson["dev"]["mdl"] = "Photon";
#elif defined(BOARD_ESP32)
configJson["dev"]["mdl"] = "ESP32";
#elif defined(BOARD_ESP8266)
configJson["dev"]["mdl"] = "ESP8266";
#else
configJson["dev"]["mdl"] = "Unknown";
#endif
configJson["dev"]["sw"] = RENDERBUG_VERSION;
configJson["dev"]["mf"] = "Phong Robotics";
configJson["dev"]["ids"][0] = (char*)deviceID;
char buf[1024];
serializeJson(configJson, buf, sizeof(buf));
Log.verbose("Publish %s %s", configTopic.c_str(), buf);
m_mqtt.publish(configTopic.c_str(), buf, true);
m_mqtt.subscribe(m_cmdTopic);
Log.verbose("Publish %s %s", Lightswitch.configTopic().c_str(), buf);
m_mqtt.publish(Lightswitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true);
m_mqtt.subscribe(Lightswitch.commandTopic().c_str());
configJson.clear();
flashlightSwitch.toJson(configJson, false);
configJson["cmd_t"] = "~/set";
configJson["ret"] = true;
serializeJson(configJson, buf, sizeof(buf));
m_mqtt.publish(flashlightSwitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true);
m_mqtt.subscribe(flashlightSwitch.commandTopic().c_str());
configJson.clear();
FPSSensor.toJson(configJson, false);
configJson["unit_of_meas"] = "Frames/s";
serializeJson(configJson, buf, sizeof(buf));
Log.verbose("Publish %s %s", FPSSensor.configTopic().c_str(), buf);
m_mqtt.publish(FPSSensor.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true);
m_mqtt.subscribe(FPSSensor.commandTopic().c_str());
#ifdef BOARD_ESP8266
struct rst_info resetInfo = *ESP.getResetInfoPtr();
if (resetInfo.reason != 0) {
char buff[200];
sprintf(&buff[0], "Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x", resetInfo.exccause, resetInfo.reason, (resetInfo.reason == 0 ? "DEFAULT" : resetInfo.reason == 1 ? "WDT" : resetInfo.reason == 2 ? "EXCEPTION" : resetInfo.reason == 3 ? "SOFT_WDT" : resetInfo.reason == 4 ? "SOFT_RESTART" : resetInfo.reason == 5 ? "DEEP_SLEEP_AWAKE" : resetInfo.reason == 6 ? "EXT_SYS_RST" : "???"), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3, resetInfo.excvaddr, resetInfo.depc);
Log.warning("Previous crash detected! %s", buff);
}
#endif
} else {
Log.warning("Could not connect to MQTT");
}
} else {
if (evt.intent == InputEvent::SetPower) {
String statTopic = Lightswitch.statTopic();
if (evt.intent == InputEvent::StopThing && String(evt.asString()) == "Flashlight") {
String flashlightStatTopic = flashlightSwitch.statTopic();
m_mqtt.publish(flashlightStatTopic.c_str(), "OFF");
} else if (evt.intent == InputEvent::StartThing && String(evt.asString()) == "Flashlight") {
String flashlightStatTopic = flashlightSwitch.statTopic();
m_mqtt.publish(flashlightStatTopic.c_str(), "ON");
} else if (evt.intent == InputEvent::SetPower) {
StaticJsonDocument<256> doc;
char buf[256];
doc["state"] = evt.asInt() ? "ON" : "OFF";
m_isOn = evt.asInt() ? true : false;
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(m_statTopic, buf);
m_mqtt.publish(statTopic.c_str(), buf);
} else if (evt.intent == InputEvent::SetBrightness) {
StaticJsonDocument<256> doc;
char buf[256];
doc["brightness"] = evt.asInt();
doc["state"] = "ON";
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(m_statTopic, buf);
m_mqtt.publish(statTopic.c_str(), buf);
} else if (evt.intent == InputEvent::SetColor) {
StaticJsonDocument<256> doc;
char buf[256];
@ -109,18 +192,19 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt)
doc["color"]["r"] = color.r;
doc["color"]["g"] = color.g;
doc["color"]["b"] = color.b;
doc["state"] = "ON";
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(m_statTopic, buf);
m_mqtt.publish(statTopic.c_str(), buf);
} else if (evt.intent == InputEvent::SetPattern) {
StaticJsonDocument<256> doc;
char buf[256];
doc["effect"] = evt.asString();
doc["state"] = "ON";
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(m_statTopic, buf);
m_mqtt.publish(statTopic.c_str(), buf);
} else if (evt.intent == InputEvent::FirmwareUpdate) {
m_mqtt.publish("renderbug/debug/firmware", "firmware update!");
String updateTopic = m_debugTopic + "/firmware";
m_mqtt.publish(updateTopic.c_str(), "firmware update!");
}
}
}
@ -132,6 +216,23 @@ MQTTTelemetry::loop()
OnlineTaskMixin::loop();
}
void
MQTTTelemetry::onOnline()
{
const IPAddress server(10, 0, 0, 2);
m_needHeartbeat = true;
m_mqtt.setServer(server, 1883);
m_mqtt.setBufferSize(1024);
m_mqtt.setCallback(&MQTTTelemetry::s_callback);
}
void
MQTTTelemetry::onOffline()
{
m_mqtt.disconnect();
}
void
MQTTTelemetry::loopOnline()
{
@ -142,38 +243,53 @@ MQTTTelemetry::loopOnline()
if (m_needHeartbeat) {
char buf[512];
StaticJsonDocument<512> response;
response["fps"] = FastLED.getFPS();
response["RSSI"] = WiFi.RSSI();
response["localip"] = WiFi.localIP().toString();
response["free_ram"] = ESP.getFreeHeap();
response["os_version"] = ESP.getSdkVersion();
response["device_id"] = Platform::deviceID();
response["sketch_version"] = ESP.getSketchMD5();
response["os_version"] = ESP.getSdkVersion();
response["localip"] = WiFi.localIP().toString();
response["pixelCount"] = Static<ConfigService>::instance()->coordMap()->pixelCount;
response["startPixel"] = Static<ConfigService>::instance()->coordMap()->startPixel;
response["RSSI"] = WiFi.RSSI();
response["free_ram"] = ESP.getFreeHeap();
response["fps"] = FastLED.getFPS();
serializeJson(response, buf, sizeof(buf));
m_mqtt.publish(m_attrTopic, buf);
m_mqtt.publish(m_heartbeatTopic, buf);
Log.notice("Heartbeat: %s", buf);
String availTopic = m_rootTopic + "/available";
m_mqtt.publish(Lightswitch.heartbeatTopic().c_str(), buf);
m_mqtt.publish(Device.availabilityTopic.c_str(), "online");
//Log.notice("Heartbeat: %s", buf);
String fpsCounter = String(FastLED.getFPS());
m_mqtt.publish(FPSSensor.statTopic().c_str(), fpsCounter.c_str());
response.clear();
auto sched = MainLoop::instance()->scheduler;
for(auto task : sched.tasks) {
response[task->name] = task->state == Task::Running;
}
serializeJson(response, buf, sizeof(buf));
m_mqtt.publish(m_heartbeatTopic, buf);
m_needHeartbeat = false;
}
}
void
MQTTTelemetry::callback(char* topic, byte* payload, unsigned int length)
MQTTTelemetry::callback(char* topic, const char* payload)
{
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload, length);
setEvent(InputEvent::NetworkActivity);
if (flashlightSwitch.isCommandTopic(topic)) {
if (!strncmp((char*)payload, "ON", sizeof("ON"))) {
Log.notice("Turning on flashlight");
setEvent(InputEvent{InputEvent::SetPower, true});
setEvent(InputEvent{InputEvent::SetPattern, "Flashlight"});
setEvent(InputEvent{InputEvent::SetBrightness, 255});
} else if (!strncmp((char*)payload, "OFF", sizeof("OFF"))) {
Log.notice("Turning off flashlight");
setEvent(InputEvent{InputEvent::SetPattern, "Idle"});
}
} else if (Lightswitch.isCommandTopic(topic)) {
StaticJsonDocument<512> doc;
deserializeJson(doc, payload);
if (doc.containsKey("state")) {
if (doc["state"] == "ON") {
Log.notice("Turning on power");
setEvent(InputEvent{InputEvent::SetPower, true});
} else if (doc["state"] == "OFF") {
Log.notice("Turning off power");
setEvent(InputEvent{InputEvent::SetPattern, "Idle"});
setEvent(InputEvent{InputEvent::SetPower, false});
}
}
@ -204,6 +320,22 @@ MQTTTelemetry::callback(char* topic, byte* payload, unsigned int length)
setEvent(InputEvent{InputEvent::SaveConfigurationRequest});
}
if (doc.containsKey("restart")) {
#ifdef BOARD_ESP8266
ESP.wdtDisable();
ESP.restart();
#endif
}
if (doc.containsKey("reconnect")) {
m_mqtt.disconnect();
}
if (doc.containsKey("ping")) {
m_needHeartbeat = true;
Log.notice("Queuing up heartbeat");
}
if (doc.containsKey("effect")) {
strcpy(m_patternBuf, doc["effect"].as<const char*>());
setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf});
@ -220,11 +352,17 @@ MQTTTelemetry::callback(char* topic, byte* payload, unsigned int length)
setEvent(InputEvent{InputEvent::SetBrightness, (int)doc["brightness"]});
}
}
}
void
MQTTTelemetry::s_callback(char* topic, byte* payload, unsigned int length)
{
Static<MQTTTelemetry>::instance()->callback(topic, payload, length);
char topicBuf[128];
char payloadBuf[512];
strcpy(topicBuf, topic);
memcpy(payloadBuf, payload, length);
payloadBuf[std::min(sizeof(payloadBuf) - 1, length)] = 0;
Static<MQTTTelemetry>::instance()->callback(topicBuf, payloadBuf);
}
STATIC_ALLOC(MQTTTelemetry);

View File

@ -6,6 +6,13 @@
#include "../../Sequencer.h"
#ifdef BOARD_ESP8266
#include <ESP8266WiFi.h>
#elif defined(BOARD_ESP32)
#include <WiFi.h>
#endif
class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
public:
MQTTTelemetry();
@ -22,7 +29,9 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
if (byte == '\n') {
size_t bufSize = buf.write(outBuf);
outBuf[std::min(sizeof(outBuf), bufSize)] = 0;
telemetry->m_mqtt.publish(telemetry->m_logTopic, outBuf);
Serial.println(outBuf);
String logTopic = telemetry->m_debugTopic + "/log";
telemetry->m_mqtt.publish(logTopic.c_str(), outBuf);
} else {
buf.insert(byte);
}
@ -39,20 +48,22 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
void loopOnline() override;
private:
char m_statTopic[100];
char m_attrTopic[100];
char m_cmdTopic[100];
char m_logTopic[100];
char m_heartbeatTopic[100];
void onOnline() override;
void onOffline() override;
void callback(char* topic, byte* payload, unsigned int length);
private:
String m_rootTopic;
String m_debugTopic;
void callback(char* topic, const char* payload);
static void s_callback(char* topic, byte* payload, unsigned int length);
char m_patternBuf[48];
bool m_needHeartbeat = false;
bool m_isOn = true;
Sequencer *m_sequencer = 0;
WiFiClient m_wifi;
PubSubClient m_mqtt;
LogPrinter m_logPrinter;
};

View File

@ -55,13 +55,16 @@ public:
// Grab the physical pixel we'll start with
PhysicalCoordinates startPos = map->virtualToPhysicalCoords({m_pos, 0});
PhysicalCoordinates endPos = map->virtualToPhysicalCoords({m_pos + width, 0});
int scaledWidth = std::abs(endPos.x - startPos.x);
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);
CHSV blobColor(m_hue, m_saturation, quadwave8((i / (double)scaledWidth) * m_brightness));
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), 0};