If the code hasn't been touched in this long, its probably release-worthy.

This commit is contained in:
Torrie Fischer 2022-06-11 11:02:27 +02:00
parent 0c9eb831dd
commit d14fa7fde1
59 changed files with 1610 additions and 842 deletions

View File

@ -1,39 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

View File

@ -1,7 +1,6 @@
#pragma once
#include <FastLED.h>
#include "./Figment.h"
#include <vector>
class Display;
@ -24,6 +23,18 @@ struct AnimatedNumber {
}
}
void update(uint8_t speed) {
if (255 - speed >= m_idx) {
m_idx += speed;
} else {
m_idx = 255;
}
}
bool isFinished() const {
return m_idx == 255;
}
AnimatedNumber() {}
AnimatedNumber(uint8_t v) : m_end(v) {}

View File

@ -15,11 +15,11 @@ struct LinearCoordinateMapping: CoordinateMapping {
unsigned int startPixel = 0;
LinearCoordinateMapping() {}
LinearCoordinateMapping(unsigned int count, unsigned int start) : pixelCount(count), startPixel(start) {}
VirtualCoordinates physicalToVirtualCoords(const PhysicalCoordinates localCoords) const {
return VirtualCoordinates{(uint8_t)((localCoords.x) / pixelCount), 0};
VirtualCoordinates physicalToVirtualCoords(const PhysicalCoordinates localCoords) const override {
return VirtualCoordinates{map8(localCoords.x, 0, pixelCount), 0};
}
PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const {
PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const override {
return PhysicalCoordinates{scale8(pixelCount, virtualCoords.x), 0};
}
@ -27,7 +27,7 @@ struct LinearCoordinateMapping: CoordinateMapping {
return localCoords.x + startPixel;
}
unsigned int physicalPixelCount() const {
unsigned int physicalPixelCount() const override {
return pixelCount;
}
};

View File

@ -22,15 +22,15 @@ struct Task : public virtual Loopable {
};
Task() {}
Task(State initialState) : Task(0, initialState) {}
Task(const char* name) : Task(name, Running) {}
explicit Task(State initialState) : Task(0, initialState) {}
explicit Task(const char* name) : Task(name, Running) {}
Task(const char* name, State initialState) : name(name), state(initialState) {}
void start() { state = Running; onStart(); }
void stop() { onStop(); state = Stopped; }
virtual bool isFigment() const { return false; }
const char* name = 0;
const char* name = "";
State state = Running;
};
@ -42,8 +42,8 @@ struct TaskFunc: public Task {
struct Figment: public Task {
Figment() : Task() {}
Figment(State initialState) : Task(initialState) {}
Figment(const char* name) : Task(name) {}
explicit Figment(State initialState) : Task(initialState) {}
explicit Figment(const char* name) : Task(name) {}
Figment(const char* name, State initialState) : Task(name, initialState) {}
virtual void render(Display* dpy) const = 0;
bool isFigment() const override { return true; }

View File

@ -8,8 +8,8 @@ template<typename T> struct Coordinates {
T y;
};
struct VirtualCoordinates: Coordinates<uint16_t> {
VirtualCoordinates(uint16_t _x, uint16_t _y) : Coordinates(_x, _y) {}
struct VirtualCoordinates: Coordinates<uint8_t> {
VirtualCoordinates(uint8_t _x, uint8_t _y) : Coordinates(_x, _y) {}
};
struct PhysicalCoordinates: Coordinates<uint16_t> {

View File

@ -20,10 +20,63 @@ Variant::asInt() const
return m_value.asInt;
}
bool
Variant::asBool() const
{
return (bool)m_value.asInt;
}
void
InputSource::init()
{
#ifdef CONFIG_THREADED_INPUTS
m_queue = xQueueCreate(32, sizeof(InputEvent));
#endif
}
#ifdef CONFIG_THREADED_INPUTS
void
InputSource::readThread(void* data)
{
InputSource* self = static_cast<InputSource*>(data);
while(true) {
InputEvent evt = self->read();
if (evt.intent != InputEvent::None) {
xQueueSend(m_queue, &evt, 0)
}
taskYIELD();
}
}
#endif
void
InputSource::onStart()
{
#ifdef CONFIG_THREADED_INPUTS
m_threadLoop = MainLoop::instance();
xTaskCreate(
&InputSource::readThread,
name,
1000,
this,
1,
NULL
);
#endif
}
void
InputSource::loop()
{
#ifndef CONFIG_THREADED_INPUTS
MainLoop::instance()->dispatch(read());
#else
InputEvent evt;
xQueueReceive(m_queue, &evt, 0);
if (evt.intent != InputEvent::None) {
MainLoop::instance()->dispatch(evt);
}
#endif
}
InputEvent

View File

@ -32,6 +32,7 @@ struct Variant {
const char* asString() const;
CRGB asRGB() const;
int asInt() const;
bool asBool() const;
private:
union {
@ -74,6 +75,8 @@ struct InputEvent: public Variant {
// Timekeeping
ScheduleChange,
Beat,
BeatDetect,
// Task management
StartThing,
@ -87,6 +90,8 @@ struct InputEvent: public Variant {
// Firmware events
FirmwareUpdate,
ReadyToRoll,
};
template<typename Value>
@ -102,14 +107,26 @@ struct InputEvent: public Variant {
Intent intent;
};
struct MainLoop;
class InputSource: public Task {
public:
InputSource() : Task() {}
InputSource(const char* name) : Task(name) {}
InputSource(Task::State initialState) : Task(initialState) {}
InputSource(const char* name, Task::State initialState) : Task(name, initialState) {}
InputSource() : Task() {init();}
explicit InputSource(const char* name) : Task(name) {init();}
explicit InputSource(Task::State initialState) : Task(initialState) {init();}
InputSource(const char* name, Task::State initialState) : Task(name, initialState) {init();}
void loop() override;
void onStart() override;
virtual InputEvent read() = 0;
private:
void init();
#ifdef CONFIG_THREADED_INPUTS
static void readThread(void* data);
MainLoop* m_threadLoop;
static uint8_t m_threadBuf[sizeof(InputEvent) * 32];
QueueHandle_t m_queue;
StaticQueue_t m_threadQueue;
#endif
};
class InputFunc : public InputSource {
@ -128,7 +145,6 @@ private:
class BufferedInputSource: public InputSource {
public:
BufferedInputSource() : InputSource() {}
BufferedInputSource(const char* name) : InputSource(name) {}
InputEvent read() override;

View File

@ -16,6 +16,7 @@ MainLoop::dispatch(const InputEvent& evt)
void
MainLoop::loop()
{
s_instance = this;
InputEvent evt;
while (m_eventBuf.take(evt)) {
if (evt.intent == InputEvent::StartThing || evt.intent == InputEvent::StopThing) {
@ -23,10 +24,10 @@ MainLoop::loop()
for(auto figmentJob: scheduler.tasks) {
if (!strcmp(figmentJob->name, evt.asString())) {
if (jobState) {
//Log.notice("Starting %s", figmentJob->name);
Log.trace("Starting task %s", figmentJob->name);
figmentJob->start();
} else {
//Log.notice("Stopping %s", figmentJob->name);
Log.trace("Stopping task %s", figmentJob->name);
figmentJob->stop();
}
}
@ -34,6 +35,9 @@ MainLoop::loop()
}
for(Task* task : scheduler) {
if (evt.intent == InputEvent::SetPower) {
Log.notice("Event %s", task->name);
}
task->handleEvent(evt);
}
}
@ -44,10 +48,18 @@ MainLoop::loop()
Task* slowestTask = NULL;
for(Task* task : scheduler) {
//unsigned int start = millis();
#if defined(BOARD_ESP32) or defined(BOARD_ESP8266)
unsigned int start = ESP.getCycleCount();
#else
unsigned int start = millis();
#endif
Log.verbose("Running %s", task->name);
task->loop();
//unsigned int runtime = millis() - start;
unsigned int runtime = ESP.getCycleCount() - start;
#if defined(BOARD_ESP32) or defined(BOARD_ESP8266)
unsigned int runtime = (ESP.getCycleCount() - start) / 160000;
#else
unsigned int runtime = millis() - start;
#endif
frameSpeed += runtime;
taskCount++;
if (runtime > slowest) {
@ -57,19 +69,22 @@ MainLoop::loop()
}
frameSpeed = millis() - frameStart;
if (frameSpeed >= 23) {
Log.notice("Slow frame: %dms, %d tasks, longest task %s was %dms", frameSpeed, taskCount, slowestTask->name, slowest/160000);
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);
}
}
void
MainLoop::start()
{
s_instance = this;
Log.notice("*** Starting %d tasks...", scheduler.tasks.size());
Serial.flush();
for(auto task: scheduler) {
Log.notice("** Starting %s", task->name);
task->start();
}
dispatch(InputEvent::ReadyToRoll);
}
MainLoop* MainLoop::s_instance;

View File

@ -55,7 +55,7 @@ struct MainLoop {
Scheduler scheduler;
MainLoop(std::vector<Task*> &&tasks)
: scheduler(std::move(tasks)) {s_instance = this;}
: scheduler(std::move(tasks)) {}
void start();
void loop();
@ -63,7 +63,7 @@ struct MainLoop {
static MainLoop* instance() { return s_instance; }
private:
Ringbuf<InputEvent, 10> m_eventBuf;
Ringbuf<InputEvent, 32> m_eventBuf;
static MainLoop* s_instance;
};

View File

@ -0,0 +1,12 @@
#pragma once
#include <ArduinoLog.h>
struct PerfCounter {
PerfCounter(const char* name) {}
~PerfCounter() {}
/*PerfCounter(const char* name) : start(millis()), name(name) {}
~PerfCounter() {Log.notice("%s: %d", name, millis() - start);}*/
uint16_t start;
const char* name;
};

View File

@ -9,11 +9,18 @@ Renderer::loop()
for(Display* dpy : m_displays) {
for(Figment* figment : m_figments) {
if (figment->state == Task::Running) {
#if defined(BOARD_ESP32) or defined(BOARD_ESP8266)
unsigned int frameStart = ESP.getCycleCount();
#endif
Log.verbose("Render %s", figment->name);
figment->render(dpy);
#if defined(BOARD_ESP32) or defined(BOARD_ESP8266)
unsigned int runtime = (ESP.getCycleCount() - frameStart) / 160000;
#else
unsigned int runtime = 0;
#endif
if (runtime >= 8) {
Log.notice("SLOW RENDER: %s took %dms!", figment->name, runtime);
Log.warning("SLOW RENDER: %s took %dms!", figment->name, runtime);
}
}
};

View File

@ -5,7 +5,7 @@ class Display;
struct Renderer: public Task {
public:
Renderer(std::vector<Display*> displays, const std::vector<Figment*> &figments) : Task("Renderer"), m_figments(figments), m_displays(displays) {}
Renderer(std::vector<Display*>&& displays, const std::vector<Figment*> &figments) : Task("Renderer"), m_figments(figments), m_displays(std::move(displays)) {}
void loop() override;
void onStart() override;

View File

@ -10,6 +10,11 @@ struct Ringbuf {
m_tail = 0;
}
T peek(int offset) const {
const int nextHead = (m_head + offset) % Size;
return m_items[nextHead];
}
bool take(T& dest) {
if (m_head == m_tail) {
return false;
@ -41,6 +46,22 @@ struct Ringbuf {
}
return ret;
}
size_t write(Print& stream) {
T val;
size_t ret = 0;
while(take(val)) {
stream.write(val);
}
return ret;
}
size_t size() {
if (m_tail > m_head) {
return m_tail - m_head;
}
return m_tail + (Size - m_head);
}
private:
int m_head = 0;
int m_tail = 0;

View File

@ -1,12 +1,29 @@
#include "./Surface.h"
#include "./Display.h"
#include <ArduinoLog.h>
#include "Perfcounter.h"
Surface::Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end)
: start(dpy->coordinateMapping()->virtualToPhysicalCoords(start)),
end(dpy->coordinateMapping()->virtualToPhysicalCoords(end)),
virtStart(start),
virtEnd(end),
m_display(dpy)
{
//assert(start.x <= end.x);
//assert(start.y <= end.y);
}
Surface::Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end, uint8_t rotation)
: start(dpy->coordinateMapping()->virtualToPhysicalCoords(start)),
end(dpy->coordinateMapping()->virtualToPhysicalCoords(end)),
virtStart(start),
virtEnd(end),
m_display(dpy),
m_rotation(rotation)
{
//assert(start.x <= end.x);
//assert(start.y <= end.y);
}
Surface&
@ -30,11 +47,27 @@ Surface::operator+=(const CRGB& color)
void
Surface::paintWith(std::function<void(CRGB&)> func)
{
//Log.verbose("Painting startx=%d endx=%d starty=%d endy=%d", start.x, end.x, start.y, end.y);
for(auto x = start.x; x <= end.x; x++) {
for(auto y = start.y; y <= end.y; y++) {
//Log.verbose("x=%d y=%d", x, y);
func(m_display->pixelAt(PhysicalCoordinates{x, y}));
paintShader([=](CRGB& pixel, const VirtualCoordinates&, const PhysicalCoordinates&, const VirtualCoordinates&){ func(pixel); });
}
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;
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};
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
// 128 = 0.0
// 0 = 1.0
shader(m_display->pixelAt(coords), virtCoords, coords, surfaceCoords);
}
}
}

View File

@ -1,3 +1,5 @@
#pragma once
#include <FastLED.h>
#include "./Geometry.h"
#include <functional>
@ -7,15 +9,30 @@ class Display;
class Surface {
public:
Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end);
Surface(Display* dpy, const VirtualCoordinates& start, const VirtualCoordinates& end, uint8_t rotation);
Surface& operator=(const CRGB& color);
Surface& operator+=(const CRGB& color);
template<typename T>
Surface& operator|=(const T& val) {
paintWith([&](CRGB& pixel) {
pixel |= val;
});
return *this;
}
void paintWith(std::function<void(CRGB&)> func);
using Shader = std::function<void(CRGB&, const VirtualCoordinates& virtPos, const PhysicalCoordinates& realPos, const VirtualCoordinates& surfacePos)>;
using BrushFunc = std::function<void(CRGB&)>;
void paintWith(BrushFunc func);
void paintShader(Shader shader);
const PhysicalCoordinates start;
const PhysicalCoordinates end;
const VirtualCoordinates virtStart;
const VirtualCoordinates virtEnd;
private:
Display* m_display;
uint8_t m_rotation = 0;
};

View File

@ -0,0 +1,8 @@
{
"name": "Figments",
"version": "0.3.0",
"description": "An embedded graphics rendering engine",
"keywords": ["FastLED", "esp32", "esp8266"],
"frameworks": ["arduino"],
"platforms": ["espressif32", "espressif8266"]
}

View File

@ -9,47 +9,194 @@
; https://docs.platformio.org/page/projectconf.html
[common_env_data]
src_filter = "+<*> -<.git/> -<.svn/> -<platform/>"
src_filter = "+<*> -<.git/> -<.svn/> -<platform/> -<inputs/>"
lib_ldf_mode = chain+
src_build_flags =
-DRENDERBUG_VERSION=3
-DRENDERBUG_LED_PIN=14
-DRENDERBUG_LED_PACKING=RGB
-DDEFAULT_PATTERN_INDEX=0
lib_deps_external =
fastled/FastLED@^3.4.0
thijse/ArduinoLog@1.0.3
bblanchon/ArduinoJson@^6.17.3
[config_u8display]
src_build_flags =
-DCONFIG_U8DISPLAY
lib_deps =
olikraus/U8g2@2.28.8
src_filter = "+<platform/arduino/U8Display.cpp>"
[config_mqtt]
src_build_flags =
-DCONFIG_MQTT
lib_deps =
knolleary/PubSubClient@^2.8.0
src_filter = "+<platform/arduino/MQTTTelemetry.cpp>"
[config_wifi]
src_build_flags =
-DCONFIG_WIFI
src_filter = "+<platform/arduino/WiFiTask.cpp>"
[config_bluetooth]
src_build_flags =
-DCONFIG_BLUETOOTH
src_filter = "+<platform/arduino/BluetoothSerialTelemetry.cpp>"
lib_deps =
BluetoothSerial
[config_ota]
src_build_flags =
-DCONFIG_OTA
src_filter = "+<platform/arduino/OTA.cpp>"
lib_deps =
ArduinoOTA
ESP8266mDNS
[config_nocolor]
src_build_flags =
-DCONFIG_NO_COLORDATA
[config_buttons]
src_build_flags =
-DCONFIG_BUTTONS
src_filter = "+<inputs/Buttons.cpp>"
[config_mpu5060]
src_build_flags =
-DCONFIG_MPU5060
src_filter = "+<inputs/MPU6050.cpp>"
[env:teensy]
extends = config_nocolor
platform = teensy
board = teensy31
framework = arduino
src_build_flags =
${common_env_data.src_build_flags}
${config_nocolor.src_build_flags}
-DPLATFORM_ARDUINO
-DBOARD_TEENSY
lib_deps =
${common_env_data.lib_deps_external}
src_filter = "${common_env_data.src_filter}"
[env:bike_teensy]
extends = env:teensy
src_build_flags=
${env:teensy.src_build_flags}
-DRENDERBUG_LED_PIN=11
-DRENDERBUG_LED_PACKING=GRB
-DDEFAULT_PATTERN_INDEX=1
[env:bike]
extends = env:esp32, config_u8display
src_filter = "${env:esp32.src_filter} ${config_u8display.src_filter}"
lib_deps =
${env:esp32.lib_deps}
${config_u8display.lib_deps}
src_build_flags =
${env:esp32.src_build_flags}
${config_u8display.src_build_flags}
build_type = debug
[env:bike_ble]
extends = env:bike
lib_deps =
${env:bike.lib_deps}
nkolban/ESP32 BLE Arduino@1.0.1
src_build_flags =
${env:bike.src_build_flags}
[env:esp32]
extends = config_nocolor
platform = espressif32
board = featheresp32
framework = arduino
build_flags =
board_build.filesystem = littlefs
src_build_flags =
${common_env_data.src_build_flags}
${config_nocolor.src_build_flags}
-DPLATFORM_ARDUINO
-DBOARD_ESP32
-DCONFIG_NO_COLORDATA
; -DCORE_DEBUG_LEVEL=5
; -DCONFIG_THREADED_INPUTS
lib_deps =
fastled/FastLED@^3.4.0
thijse/ArduinoLog@^1.0.3
knolleary/PubSubClient@^2.8.0
bblanchon/ArduinoJson@^6.17.3
sstaub/NTP@^1.4.0
arduino-libraries/NTPClient@^3.1.0
src_filter = "${common_env_data.src_filter} +<platform/arduino/>"
${common_env_data.lib_deps_external}
src_filter = "${common_env_data.src_filter}"
board_build.partitions = no_ota.csv
;build_type = debug
[env:cyberplague]
extends = env:esp32
board_build.partitions = no_ota.csv
[env:esp8266-12f]
extends = env:esp8266
board = esp12e
[env:esp8266]
platform = espressif8266
board = huzzah
framework = arduino
build_flags =
booard_build.filesystem = littlefs
src_build_flags =
${common_env_data.src_build_flags}
-DPLATFORM_ARDUINO
-DBOARD_ESP8266
lib_deps =
fastled/FastLED@^3.4.0
thijse/ArduinoLog@^1.0.3
knolleary/PubSubClient@^2.8.0
bblanchon/ArduinoJson@^6.17.3
sstaub/NTP@^1.4.0
-DCORE_DEBUG_LEVEL=5
-fstack-protector
lib_deps =
${common_env_data.lib_deps_external}
arduino-libraries/NTPClient@^3.1.0
src_filter = "${common_env_data.src_filter} +<platform/arduino/>"
src_filter = "${common_env_data.src_filter}"
[env:cyberplague]
extends = env:esp32, config_bluetooth
src_filter = "${env:esp32.src_filter} ${config_bluetooth.src_filter}"
lib_deps =
${env:esp32.lib_deps}
${config_bluetooth.lib_deps}
src_build_flags =
${env:esp32.src_build_flags}
${config_bluetooth.src_build_flags}
-DRENDERBUG_LED_PIN=13
-DDEFAULT_PATTERN_INDEX=1
[env:cyberplague_wifi]
extends = env:esp32, config_wifi, config_mqtt
src_filter = "${env:esp32.src_filter} ${config_wifi.src_filter} ${config_mqtt.src_filter}"
lib_deps =
${env:esp32.lib_deps}
${config_mqtt.lib_deps}
src_build_flags =
${env:esp32.src_build_flags}
${config_mqtt.src_build_flags}
${config_wifi.src_build_flags}
[env:prototype]
extends = env:esp32, config_buttons, config_mpu5060
src_filter = "${env:esp32.src_filter} ${config_buttons.src_filter} ${config_mpu5060.src_filter}"
[env:home_lighting]
extends = env:esp8266, config_wifi, config_mqtt, config_ota
src_filter = "${env:esp32.src_filter} ${config_ota.src_filter} ${config_wifi.src_filter} ${config_mqtt.src_filter}"
src_build_flags =
${env:esp8266.src_build_flags}
${config_mqtt.src_build_flags}
${config_wifi.src_build_flags}
${config_ota.src_build_flags}
lib_deps =
${env:esp8266.lib_deps}
${config_mqtt.lib_deps}
ESP8266WiFi
${config_ota.lib_deps}
[env:home_lighting_grb]
extends = env:home_lighting
src_build_flags =
${env:home_lighting.src_build_flags}
-DRENDERBUG_LED_PACKING=GRB
[env:home_lighting-12f]
extends = env:home_lighting
board = esp12e
;[env:photon]
;platform = particlephoton

View File

@ -8,9 +8,13 @@ constexpr uint16_t HardwareConfig::MAX_LED_NUM;
HardwareConfig
HardwareConfig::load() {
HardwareConfig ret;
#ifndef BOARD_TEENSY
EEPROM.begin(sizeof(ret));
#endif
EEPROM.get(0, ret);
#ifndef BOARD_TEENSY
EEPROM.end();
#endif
Log.notice("Loaded config version %d, CRC %d", ret.version, ret.checksum);
return ret;
}
@ -19,10 +23,14 @@ void
HardwareConfig::save() {
HardwareConfig dataCopy{*this};
dataCopy.checksum = getCRC();
#ifndef BOARD_TEENSY
EEPROM.begin(sizeof(dataCopy));
#endif
EEPROM.put(0, dataCopy);
#ifndef BOARD_TEENSY
EEPROM.commit();
EEPROM.end();
#endif
}
LinearCoordinateMapping
@ -72,13 +80,6 @@ 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...");
for(int i = 0; i < 32; i++) {
auto svc = m_config.data.serviceStates[i];
if (strnlen(svc.name, 16) > 0) {
Log.notice("* %s: %s", svc.name, svc.isDisabled? "DISABLED" : "ENABLED");
}
}*/
}
void
@ -112,3 +113,4 @@ ConfigService::handleEvent(const InputEvent &evt)
}
STATIC_ALLOC(ConfigService);
STATIC_TASK(ConfigService);

View File

@ -1,17 +1,75 @@
#pragma once
#include <Figments.h>
//#define PLATFORM_PHOTON
//#define PLATFORM_ARDUINO
#define RENDERBUG_VERSION 2
struct MaskCoordinateMapping : CoordinateMapping {
struct Span {
int length = 0;
int x = 0;
int y = 0;
Span(int length, int x, int y) : length(length), x(x), y(y) {}
};
Span displayMap[13] = {
{6, 0, 6},
{6, 1, 6},
{7, 2, 6},
{9, 3, 4},
{14, 4, 4},
{17, 5, 0},
{12, 6, 2},
{18, 7, 0},
{14, 8, 4},
{9, 9, 5},
{7, 10, 4},
{6, 11, 5},
{6, 12, 5}
};
VirtualCoordinates physicalToVirtualCoords(const PhysicalCoordinates localCoords) const override {
int offset = localCoords.x;
for(int i = 0; i < 12; i++) {
if (offset > displayMap[i].length) {
offset -= displayMap[i].length;
} else {
return VirtualCoordinates{i, offset};
}
}
}
PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const override {
const uint8_t spanIdx = scale8(12, virtualCoords.x);
const uint8_t spanOffset = scale8(17, virtualCoords.y);
return PhysicalCoordinates{spanIdx, spanOffset};
}
int physicalCoordsToIndex(const PhysicalCoordinates localCoords) const override {
uint8_t idx = 0;
bool inverse = false;
for(int i = 0; i < localCoords.x; i++) {
idx += displayMap[i].length;
inverse = !inverse;
}
if (inverse) {
idx += std::max(0, displayMap[localCoords.x].length - 1 - std::max(0, (int)localCoords.y - displayMap[localCoords.x].y));
} else {
idx += std::min((int)displayMap[localCoords.x].length - 1, std::max(0, (int)localCoords.y - displayMap[localCoords.x].y));
}
return idx;
}
unsigned int physicalPixelCount() const override {
int total = 0;
for(int i = 0; i < 13; i++) {
total += displayMap[i].length;
}
return total;
}
};
struct HardwareConfig {
uint8_t version = 2;
uint8_t version = 3;
uint8_t checksum = 0;
struct TaskState {
char name[16] = {0};
bool isDisabled = false;
};
struct Data {
uint16_t pixelCount = 255;
uint16_t startPixel = 0;
@ -19,7 +77,6 @@ struct HardwareConfig {
uint8_t lastGreen = 255;
uint8_t lastBlue = 255;
char lastScene[16] = {0};
TaskState serviceStates[32];
};
Data data;
@ -28,13 +85,7 @@ struct HardwareConfig {
bool isValid() const;
LinearCoordinateMapping toCoordMap() const;
static constexpr uint16_t MAX_LED_NUM = 255;
#ifdef PLATFORM_PHOTON
static constexpr bool HAS_MPU_6050 = true;
#else
static constexpr bool HAS_MPU_6050 = false;
#endif
private:
uint8_t getCRC() const;
@ -50,9 +101,10 @@ struct ConfigService: public Task {
void onStart();
void loop() override;
void handleEvent(const InputEvent &evt) override;
const LinearCoordinateMapping* coordMap() const { return &m_coordMap; }
const CoordinateMapping* coordMap() const { return /*&m_maskMap;*/ &m_coordMap; }
private:
HardwareConfig m_config;
MaskCoordinateMapping m_maskMap;
LinearCoordinateMapping m_coordMap;
};

View File

@ -2,99 +2,96 @@
#include "Static.h"
#include <ArduinoLog.h>
static const char* eventValue(const InputEvent& evt);
const char*
LogService::intentName(InputEvent::Intent intent)
{
switch(intent) {
case InputEvent::BeatDetect:
return "beat-detection";
case InputEvent::Beat:
return "beat";
case InputEvent::ReadyToRoll:
return "ready-to-roll";
case InputEvent::PowerToggle:
return "power-toggle";
case InputEvent::SetPower:
return "set-power";
case InputEvent::PreviousPattern:
return "previous-pattern";
case InputEvent::NextPattern:
return "next-pattern";
case InputEvent::SetPattern:
return "set-pattern";
case InputEvent::SetColor:
return "set-color";
case InputEvent::Acceleration:
return "acceleration";
case InputEvent::UserInput:
return "user";
case InputEvent::SetBrightness:
return "set-brightness";
case InputEvent::FirmwareUpdate:
return "firmware-update";
case InputEvent::NetworkStatus:
return "network-status";
case InputEvent::NetworkActivity:
return "network-activity";
case InputEvent::StartThing:
return "start-thing";
case InputEvent::StopThing:
return "stop-thing";
case InputEvent::SetDisplayOffset:
return "set-display-offset";
case InputEvent::SetDisplayLength:
return "set-display-length";
case InputEvent::SaveConfigurationRequest:
return "save-configuration";
default:
return NULL;
}
}
char LogService::s_valueBuf[255];
const char*
LogService::eventValue(const InputEvent& evt)
{
switch(evt.type) {
case InputEvent::Null:
snprintf(s_valueBuf, sizeof(s_valueBuf), "null");break;
case InputEvent::Integer:
snprintf(s_valueBuf, sizeof(s_valueBuf), "%d %02x", evt.asInt(), evt.asInt());break;
case InputEvent::String:
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;
}
return s_valueBuf;
}
void
LogService::handleEvent(const InputEvent& evt) {
if (evt.intent != InputEvent::None) {
const char* sourceName;
switch(evt.intent) {
case InputEvent::PowerToggle:
sourceName = "power-toggle";
break;
case InputEvent::SetPower:
sourceName = "set-power";
break;
case InputEvent::PreviousPattern:
sourceName = "previous-pattern";
break;
case InputEvent::NextPattern:
sourceName = "next-pattern";
break;
case InputEvent::SetPattern:
sourceName = "set-pattern";
break;
case InputEvent::SetColor:
sourceName = "set-color";
break;
case InputEvent::Acceleration:
sourceName = "acceleration";
break;
case InputEvent::UserInput:
sourceName = "user";
break;
case InputEvent::SetBrightness:
sourceName = "set-brightness";
break;
case InputEvent::FirmwareUpdate:
sourceName = "firmware-update";
break;
case InputEvent::NetworkStatus:
sourceName = "network-status";
break;
case InputEvent::NetworkActivity:
sourceName = "network-activity";
break;
case InputEvent::StartThing:
sourceName = "start-thing";
break;
case InputEvent::StopThing:
sourceName = "stop-thing";
break;
case InputEvent::SetDisplayOffset:
sourceName = "set-display-offset";
break;
case InputEvent::SetDisplayLength:
sourceName = "set-display-length";
break;
case InputEvent::SaveConfigurationRequest:
sourceName = "save-configuration";
break;
default:
sourceName = 0;
break;
}
char valueBuf[255];
switch(evt.type) {
case InputEvent::Null:
snprintf(valueBuf, sizeof(valueBuf), "null");break;
case InputEvent::Integer:
snprintf(valueBuf, sizeof(valueBuf), "%d %02x", evt.asInt(), evt.asInt());break;
case InputEvent::String:
snprintf(valueBuf, sizeof(valueBuf), "\"%s\"", evt.asString());break;
case InputEvent::Color:
snprintf(valueBuf, sizeof(valueBuf), "[%d, %d, %d]", evt.asRGB().r, evt.asRGB().g, evt.asRGB().b);break;
}
char buf[255 * 2];
if (sourceName == 0) {
snprintf(buf, sizeof(buf), "{\"intent\": %d, \"value\": %s}", evt.intent, valueBuf);
} else {
snprintf(buf, sizeof(buf), "{\"intent\": \"%s\", \"value\": %s}", sourceName, valueBuf);
}
if (evt.intent != m_lastEvent.intent) {
if (m_duplicateEvents > 0) {
Log.notice("Suppressed reporting %d duplicate events.", m_duplicateEvents);
Log.trace("Suppressed reporting %d duplicate events.", m_duplicateEvents);
}
const char* sourceName = intentName(evt.intent);;
const char* valueBuf = eventValue(evt);
if (sourceName == 0) {
Log.trace("Event: intent: %d value: %s", evt.intent, valueBuf);
} else {
Log.trace("Event: intent: %s value: %s", sourceName, valueBuf);
}
Log.verbose("Event: %s", buf);
m_duplicateEvents = 0;
m_lastEvent = evt;
//Particle.publish("renderbug/event", buf, PRIVATE);
} else {
m_duplicateEvents++;
}
/*if (m_online) {
} else {
Log.info("[offline] Event: %s", buf);
}*/
}
}
STATIC_ALLOC(LogService);
STATIC_TASK(LogService);

View File

@ -6,7 +6,11 @@ class LogService : public Task {
void handleEvent(const InputEvent& event) override;
void loop() override {}
static const char* intentName(InputEvent::Intent intent);
static const char* eventValue(const InputEvent& evt);
private:
static char s_valueBuf[255];
uint16_t m_duplicateEvents = 0;
InputEvent m_lastEvent;
};

View File

@ -1,12 +1,15 @@
#include "Platform.h"
#include <ArduinoLog.h>
#include "Static.h"
#include <time.h>
#ifdef BOARD_ESP32
#ifdef CONFIG_WIFI
#include <WiFi.h>
#endif
#include <esp_task_wdt.h>
#include <time.h>
#elif defined(BOARD_ESP8266)
#ifdef CONFIG_WIFI
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
@ -16,11 +19,14 @@ WiFiUDP wifiUdp;
//NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7);
NTPClient timeClient(wifiUdp, "10.0.0.1", 3600 * -7);
#endif
#endif
#ifdef PLATFORM_PHOTON
STARTUP(BootOptions::initPins());
#else
#ifdef CONFIG_MQTT
#include "platform/arduino/MQTTTelemetry.h"
#endif
void printNewline(Print* logOutput)
{
logOutput->print("\r\n");
@ -33,6 +39,8 @@ int printEspLog(const char* fmt, va_list args)
#endif
int Platform::s_timezone = 0;
Platform::TaskRegistration* Platform::firstTask = NULL;
Platform::TaskRegistration* Platform::lastTask = NULL;
const char*
Platform::name()
@ -78,7 +86,11 @@ Platform::preSetup()
Log.notice("\xf0\x9f\x94\x8c Serial connected");
}
#else
Log.begin(LOG_LEVEL_VERBOSE, Static<MQTTTelemetry>::instance()->logPrinter());
#ifdef CONFIG_MQTT
Log.begin(LOG_LEVEL_TRACE, Static<MQTTTelemetry>::instance()->logPrinter());
#else
Log.begin(LOG_LEVEL_TRACE, &Serial);
#endif
Log.setSuffix(printNewline);
#endif
@ -101,8 +113,10 @@ Platform::setup()
constexpr int dst = 1;
configTime(s_timezone* 3600, 3600 * dst, "pool.ntp.org");
#elif defined(BOARD_ESP8266)
#ifdef CONFIG_WIFI
timeClient.begin();
#endif
#endif
}
void
@ -114,15 +128,24 @@ Platform::bootSplash()
Log.notice(u8" 3: Serial - %d", bootopts.isSerial);
Log.notice(u8" 4: Flash - %d", bootopts.isFlash);
#endif
Log.trace("Registered tasks:");
auto it = beginTasks();
while (it != endTasks()) {
Log.trace((*it)->name);
++it;
}
}
void
Platform::loop()
{
#ifdef BOARD_ESP8266
#ifdef CONFIG_WIFI
if (WiFi.status() == WL_CONNECTED) {
timeClient.update();
}
#endif
ESP.wdtFeed();
#elif defined(BOARD_ESP32)
esp_task_wdt_reset();
@ -141,13 +164,18 @@ Platform::getLocalTime(struct tm* timedata)
return false;
#elif defined(BOARD_ESP32)
time_t rawtime;
memset(&rawtime, 0, sizeof(rawtime));
time(&rawtime);
(*timedata) = (*localtime(&rawtime));
return (timedata->tm_year > (2016-1990));
//return getLocalTime(timedata);
#else
#ifdef CONFIG_WIFI
timedata->tm_hour = timeClient.getHours();
timedata->tm_min = timeClient.getMinutes();
#else
memset(timedata, sizeof(struct tm), 0);
return false;
#endif
return true;
#endif
}
@ -172,3 +200,4 @@ char
Platform::s_deviceID[15];
STATIC_ALLOC(Platform);
STATIC_TASK(Platform);

View File

@ -13,14 +13,7 @@ class Platform : public Task {
static int getTimezone() { return s_timezone; }
static void addLEDs(CRGB* leds, unsigned int ledCount) {
#ifdef PLATFORM_PHOTON
FastLED.addLeds<NEOPIXEL, 6>(leds, ledCount);
#elif defined(BOARD_ESP32)
FastLED.addLeds<WS2812B, 13, GRB>(leds, ledCount);
#else
//FastLED.addLeds<WS2812B, 14, GRB>(leds, ledCount);
FastLED.addLeds<WS2812B, 14, RGB>(leds, ledCount);
#endif
FastLED.addLeds<WS2812B, RENDERBUG_LED_PIN, RENDERBUG_LED_PACKING>(leds, ledCount);
}
static const char* name();
@ -39,5 +32,58 @@ class Platform : public Task {
void loop() override;
static bool getLocalTime(struct tm* timedata);
static const char* deviceID();
};
struct TaskRegistration {
Task* task = 0;
TaskRegistration* next = 0;
TaskRegistration(Task* task) : task(task) {}
};
static TaskRegistration* firstTask;
static TaskRegistration* lastTask;
static void registerTask(TaskRegistration* reg) {
if (firstTask == NULL) {
firstTask = reg;
lastTask = firstTask;
} else {
lastTask->next = reg;
lastTask = reg;
}
}
struct task_iterator: public std::iterator<std::input_iterator_tag, Task*> {
TaskRegistration* cur;
explicit task_iterator() : cur(NULL) {}
explicit task_iterator(TaskRegistration* head) : cur(head) {}
task_iterator& operator++() {
if (cur) {
cur = cur->next;
}
return *this;
}
task_iterator operator++(int) {task_iterator ret = *this; ++(*this); return ret;}
bool operator==(task_iterator other) const { return cur == other.cur; }
bool operator!=(task_iterator other) const { return !(*this == other); }
Task* operator*() const { return cur->task; }
};
static task_iterator beginTasks() {
return task_iterator(firstTask);
}
static task_iterator endTasks() {
return task_iterator(NULL);
}
static void restart() {
#ifdef BOARD_ESP8266
ESP.wdtDisable();
ESP.restart();
#elif defined(BOARD_ESP32)
ESP.restart();
#endif
}
};

View File

@ -3,6 +3,14 @@
Sequencer::Sequencer(std::vector<Sequencer::Scene> &&scenes) :
Task("SceneSequencer"),
m_idx(0),
m_scenes(std::move(scenes))
{
}
Sequencer::Sequencer(std::vector<Sequencer::Scene> &&scenes, int startIndex) :
Task("SceneSequencer"),
m_idx(startIndex),
m_scenes(std::move(scenes))
{
}
@ -22,16 +30,28 @@ Sequencer::scenes() const
return m_scenes;
}
void
Sequencer::onStart()
{
}
void
Sequencer::handleEvent(const InputEvent& evt)
{
if (evt.intent == InputEvent::ReadyToRoll) {
Log.notice("Starting pattern %s!", m_scenes[m_idx].name);
for(const char* pattern : m_scenes[m_idx].patterns) {
Log.verbose("Starting pattern task %s", pattern);
MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern});
}
}
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.verbose("Stopping pattern task %s", pattern);
MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern});
}
@ -57,7 +77,7 @@ Sequencer::handleEvent(const InputEvent& evt)
}
for(const char* pattern : m_scenes[m_idx].patterns) {
//Log.notice("Starting %s", pattern);
Log.verbose("Starting pattern task %s", pattern);
MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern});
}
}

View File

@ -13,14 +13,16 @@ public:
};
Sequencer(std::vector<Scene> &&scenes);
Sequencer(std::vector<Scene> &&scenes, int startingIndex);
void loop() override;
void onStart() override;
void handleEvent(const InputEvent& evt) override;
const char* currentSceneName();
const std::vector<Scene> scenes() const;
private:
int m_idx = 0;
int m_idx;
std::vector<Scene> m_scenes;
};

View File

@ -1,4 +1,5 @@
#pragma once
#include "Platform.h"
// Utility class mostly for when certain inputs need singleton callback handlers
template<typename T> class Static {
@ -10,7 +11,24 @@ private:
static T* s_instance;
};
template<typename T> struct StaticTaskRegistration : public Platform::TaskRegistration {
StaticTaskRegistration() : Platform::TaskRegistration(Static<T>::instance()) {
Platform::registerTask(this);
}
};
struct AutoTaskRegistration : public Platform::TaskRegistration {
explicit AutoTaskRegistration(Task* task) : Platform::TaskRegistration(task) {
Platform::registerTask(this);
}
};
#define NAMED_STATIC_ALLOC(Cls, StaticName) static Cls _staticAlloc__ ## StaticName;\
template<> Cls* Static<Cls>::s_instance=&_staticAlloc__ ## StaticName;
#define STATIC_ALLOC(Cls) NAMED_STATIC_ALLOC(Cls, Cls)
#define NAMED_STATIC_TASK(Cls, StaticName) static StaticTaskRegistration<Cls> _staticTask_ ## StaticName;
#define STATIC_TASK(Cls) NAMED_STATIC_TASK(Cls, Cls)
#define REGISTER_TASK(TaskName) static AutoTaskRegistration _autoTask__ ## TaskName(&TaskName);

View File

@ -1,75 +1,74 @@
#include "../Figments/Figments.h"
#include "../sprites/Chime.h"
#include "../sprites/Blob.h"
#include "Chimes.h"
#include "../Static.h"
#define CHIME_LENGTH 23
#define CHIME_COUNT 4
#define BLOB_COUNT 10
ChimesAnimation::ChimesAnimation() : Figment("Chimes", Task::Stopped) {
}
class ChimesAnimation: public Figment {
public:
ChimesAnimation(Task::State initialState) : Figment("Chimes", initialState) {
m_chimes.forEach([](Chime<CHIME_LENGTH> &chime) {
chime.setPos(random(Chime<CHIME_LENGTH>::Length * 5));
chime.setHue(random(255));
chime.setSpeed(random(90) + 138);
chime.setBrightness(200);
chime.setOffset(random(1024));
});
m_blobs.forEach([](Blob &blob) {
blob.setPos(random(255));
blob.setHue(random(255));
blob.setBrightness(random(255));
if (random(255) % 2) {
blob.setVelocity(-1);
} else {
blob.setVelocity(1);
void ChimesAnimation::randomize() {
m_isRandom = true;
m_chimes.forEach([](Chime<CHIME_LENGTH> &chime) {
chime.setPos(random(Chime<CHIME_LENGTH>::Length * 5));
chime.setHue(random(255));
chime.setSpeed(random(90) + 138);
chime.setBrightness(200);
chime.setOffset(random(1024));
});
m_blobs.forEach([](Blob &blob) {
blob.setPos(random(255));
blob.setHue(random(255));
blob.setBrightness(random(255));
if (random(255) % 2) {
blob.setVelocity(-1);
} else {
blob.setVelocity(1);
}
});
}
void ChimesAnimation::handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::UserInput) {
if (strcmp(evt.asString(), "blobs") == 0) {
m_blobs.toggle();
} else if (strcmp(evt.asString(), "chimes") == 0) {
m_chimes.toggle();
}
});
} else if (evt.intent == InputEvent::SetColor) {
m_flashBrightness.set(255, 0);
m_flashColor = evt.asRGB();
uint8_t flashHue = rgb2hsv_approximate(m_flashColor).hue;
m_blobs.forEach([&](Blob& blob) {
blob.setHue(flashHue);
});
m_chimes.forEach([&](Chime<CHIME_LENGTH>& chime) {
chime.setHue(flashHue);
});
} else if (evt.intent == InputEvent::Beat) {
m_isRandom = false;
}
}
void handleEvent(const InputEvent& evt) override {
if (evt.intent == InputEvent::UserInput) {
if (strcmp(evt.asString(), "blobs") == 0) {
m_blobs.toggle();
} else if (strcmp(evt.asString(), "chimes") == 0) {
m_chimes.toggle();
}
} else if (evt.intent == InputEvent::SetColor) {
m_flashBrightness.set(255, 0);
m_flashColor = evt.asRGB();
uint8_t flashHue = rgb2hsv_approximate(m_flashColor).hue;
m_blobs.forEach([&](Blob& blob) {
blob.setHue(flashHue);
});
m_chimes.forEach([&](Chime<CHIME_LENGTH>& chime) {
chime.setHue(flashHue);
});
}
}
void ChimesAnimation::loop() {
if (!m_isRandom) {
randomize();
}
m_chimes.update();
m_blobs.update();
m_flashColor.update();
EVERY_N_MILLISECONDS(5) {
m_flashBrightness.update();
}
}
void loop() override {
m_chimes.update();
m_blobs.update();
m_flashColor.update();
EVERY_N_MILLISECONDS(5) {
m_flashBrightness.update();
}
}
void ChimesAnimation::render(Display* dpy) const {
m_chimes.render(dpy);
m_blobs.render(dpy);
Surface fullSurface(dpy, {0, 0}, {255, 0});
CRGB scaledColor = CRGB(m_flashColor).nscale8_video(std::max((uint8_t)10, ease8InOutCubic(m_flashBrightness)));
fullSurface.paintWith([&](CRGB& pixel) {
pixel = blend(scaledColor, pixel, 200);
//pixel = scaledColor;
});
}
void render(Display* dpy) const override {
m_chimes.render(dpy);
m_blobs.render(dpy);
Surface fullSurface(dpy, {0, 0}, {255, 0});
CRGB scaledColor = CRGB(m_flashColor).nscale8_video(std::max((uint8_t)10, ease8InOutCubic(m_flashBrightness)));
fullSurface.paintWith([&](CRGB& pixel) {
pixel = blend(scaledColor, pixel, 200);
//pixel = scaledColor;
});
}
SpriteList<Chime<CHIME_LENGTH>, CHIME_COUNT> m_chimes;
SpriteList<Blob, BLOB_COUNT> m_blobs;
AnimatedRGB m_flashColor;
AnimatedNumber m_flashBrightness;
};
STATIC_ALLOC(ChimesAnimation);
STATIC_TASK(ChimesAnimation);

24
src/animations/Chimes.h Normal file
View File

@ -0,0 +1,24 @@
#include "../Figments/Figments.h"
#include "../sprites/Chime.h"
#include "../sprites/Blob.h"
#define CHIME_LENGTH 23
#define CHIME_COUNT 4
#define BLOB_COUNT 10
class ChimesAnimation: public Figment {
public:
ChimesAnimation();
void handleEvent(const InputEvent& evt) override;
void loop() override;
void render(Display* dpy) const override;
private:
SpriteList<Chime<CHIME_LENGTH>, CHIME_COUNT> m_chimes;
SpriteList<Blob, BLOB_COUNT> m_blobs;
AnimatedRGB m_flashColor;
AnimatedNumber m_flashBrightness;
void randomize();
bool m_isRandom = false;
};

View File

@ -1,64 +1,58 @@
#include <Figments.h>
#include <ArduinoLog.h>
#include "Drain.h"
#include "../Static.h"
class DrainAnimation: public Figment {
public:
DrainAnimation::DrainAnimation() : Figment("Drain", Task::Stopped) {}
DrainAnimation(Task::State initialState) : Figment("Drain", initialState) {}
void loop() override {
EVERY_N_MILLISECONDS(8) {
m_pos++;
m_fillColor.update();
}
EVERY_N_MILLISECONDS(50) {
if (random(255) >= 10) {
m_burst -= m_burst / 10;
}
void DrainAnimation::loop() {
EVERY_N_MILLISECONDS(8) {
m_pos++;
m_fillColor.update();
}
EVERY_N_MILLISECONDS(50) {
if (random(255) >= 10) {
m_burst -= m_burst / 10;
}
}
}
void handleEvent(const InputEvent& event) override {
if (event.intent == InputEvent::SetColor) {
m_fillColor = event.asRGB();
} else if (event.intent == InputEvent::Acceleration) {
m_pos += log10(event.asInt());
uint16_t burstInc = event.asInt() / 6;
m_burst = (m_burst > 0xFFFF - burstInc) ? 0xFFFF : m_burst + burstInc;
}
void DrainAnimation::handleEvent(const InputEvent& event) {
if (event.intent == InputEvent::SetColor) {
m_fillColor = event.asRGB();
} else if (event.intent == InputEvent::Acceleration) {
m_pos += log10(event.asInt());
uint16_t burstInc = event.asInt() / 6;
m_burst = (m_burst > 0xFFFF - burstInc) ? 0xFFFF : m_burst + burstInc;
}
}
void DrainAnimation::render(Display* dpy) const {
dpy->clear();
Surface leftPanel{dpy, {0, 0}, {128, 0}};
Surface rightPanel{dpy, {128, 0}, {255, 0}};
fillRange(dpy, leftPanel.start, leftPanel.end, rgb2hsv_approximate(m_fillColor));
fillRange(dpy, rightPanel.end, rightPanel.start, rgb2hsv_approximate(m_fillColor));
}
void DrainAnimation::fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const {
int length = end.x - start.x;
int direction = 1;
if (length < 0) {
direction = -1;
}
AnimatedRGB m_fillColor;
uint8_t frac = length == 0 ? 0 : 255 / std::abs(length);
for(int i = 0; i < std::abs(length); i++) {
auto coords = PhysicalCoordinates((start.x + (i * direction)), 0);
void render(Display* dpy) const override {
dpy->clear();
Surface leftPanel{dpy, {0, 0}, {128, 0}};
Surface rightPanel{dpy, {128, 0}, {255, 0}};
fillRange(dpy, leftPanel.start, leftPanel.end, rgb2hsv_approximate(m_fillColor));
fillRange(dpy, rightPanel.end, rightPanel.start, rgb2hsv_approximate(m_fillColor));
const uint8_t localScale = inoise8(i * 80, m_pos * 3);
const uint8_t dimPosition = lerp8by8(50, 190, scale8(sin8((frac * i) / 2), localScale));
const uint8_t withBurst = ease8InOutCubic(lerp16by16(dimPosition, 255, m_burst));
auto scaledColor = CHSV(baseColor.hue, lerp8by8(100, 255, localScale), withBurst);
CRGB src(dpy->pixelAt(coords));
dpy->pixelAt(coords) = blend(scaledColor, src, 200);
}
}
void fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const {
int length = end.x - start.x;
int direction = 1;
if (length < 0) {
direction = -1;
}
uint8_t frac = 255 / std::abs(length);
for(int i = 0; i < std::abs(length); i++) {
auto coords = PhysicalCoordinates((start.x + (i * direction)), 0);
const uint8_t localScale = inoise8(i * 80, m_pos * 3);
const uint8_t dimPosition = lerp8by8(50, 190, scale8(sin8((frac * i) / 2), localScale));
const uint8_t withBurst = ease8InOutCubic(lerp16by16(dimPosition, 255, m_burst));
auto scaledColor = CHSV(baseColor.hue, lerp8by8(100, 255, localScale), withBurst);
CRGB src(dpy->pixelAt(coords));
dpy->pixelAt(coords) = blend(scaledColor, src, 200);
}
}
uint16_t m_pos;
uint16_t m_burst;
};
STATIC_ALLOC(DrainAnimation);
STATIC_TASK(DrainAnimation);

View File

@ -1,63 +1,16 @@
#pragma once
#include <Figments.h>
#include <ArduinoLog.h>
class DrainAnimation: public Figment {
public:
DrainAnimation(Task::State initialState) : Figment("Drain", initialState) {}
void loop() override {
EVERY_N_MILLISECONDS(8) {
m_pos++;
m_fillColor.update();
}
EVERY_N_MILLISECONDS(50) {
if (random(255) >= 10) {
m_burst -= m_burst / 10;
}
}
}
void handleEvent(const InputEvent& event) override {
if (event.intent == InputEvent::SetColor) {
m_fillColor = event.asRGB();
} else if (event.intent == InputEvent::Acceleration) {
m_pos += log10(event.asInt());
uint16_t burstInc = event.asInt() / 6;
m_burst = (m_burst > 0xFFFF - burstInc) ? 0xFFFF : m_burst + burstInc;
}
}
DrainAnimation();
void loop() override;
void handleEvent(const InputEvent& event) override;
void render(Display* dpy) const override;
private:
AnimatedRGB m_fillColor;
void render(Display* dpy) const override {
dpy->clear();
Surface leftPanel{dpy, {0, 0}, {128, 0}};
Surface rightPanel{dpy, {128, 0}, {255, 0}};
fillRange(dpy, leftPanel.start, leftPanel.end, rgb2hsv_approximate(m_fillColor));
fillRange(dpy, rightPanel.end, rightPanel.start, rgb2hsv_approximate(m_fillColor));
}
void fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const {
int length = end.x - start.x;
int direction = 1;
if (length < 0) {
direction = -1;
}
uint8_t frac = 255 / std::abs(length);
for(int i = 0; i < std::abs(length); i++) {
auto coords = PhysicalCoordinates((start.x + (i * direction)), 0);
const uint8_t localScale = inoise8(i * 80, m_pos * 3);
const uint8_t dimPosition = lerp8by8(50, 190, scale8(sin8((frac * i) / 2), localScale));
const uint8_t withBurst = ease8InOutCubic(lerp16by16(dimPosition, 255, m_burst));
auto scaledColor = CHSV(baseColor.hue, lerp8by8(100, 255, localScale), withBurst);
CRGB src(dpy->pixelAt(coords));
dpy->pixelAt(coords) = blend(scaledColor, src, 200);
}
}
void fillRange(Display* dpy, const PhysicalCoordinates &start, const PhysicalCoordinates& end, const CHSV &baseColor) const;
uint16_t m_pos;
uint16_t m_burst;
};

View File

@ -1,47 +1,33 @@
#pragma once
#include "Flashlight.h"
#include "../Static.h"
#include <Figments.h>
#include "../sprites/Blob.h"
class Flashlight: public Figment {
public:
Flashlight(Task::State initialState) : Figment("Flashlight", initialState) {
m_blobs.forEach([](Blob &blob) {
blob.setHue(random(255));
blob.setSaturation(10);
blob.setPos(random(255));
});
}
void handleEvent(const InputEvent& evt) override {
if (evt.intent == InputEvent::Acceleration) {
if (evt.asInt() > 10) {
m_blobs.forEach([](Blob& blob) {blob.update();});
}
Flashlight::Flashlight() : Figment("Flashlight", Task::Stopped) {
m_blobs.forEach([](Blob &blob) {
blob.setHue(random(255));
blob.setSaturation(10);
blob.setPos(random(255));
if (random(255) >= 128) {
blob.setVelocity(-1);
}
});
}
/*if (evt.intent() == InputEvent::UserInput) {
if (evt.asInt() == 1) {
m_blobs.forEach([](Blob& blob) {blob.setPos(random(255));});
}
if (evt.asInt() == 2) {
m_blobs.forEach([](Blob& chime) {blob.setPos(0);});
}
}*/
}
void loop() override {
m_blobs.update();
}
void render(Display* dpy) const override {
m_blobs.render(dpy);
for(int i = 0; i < dpy->pixelCount();i++) {
dpy->pixelAt(i) |= 100;
void Flashlight::handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::Acceleration) {
if (evt.asInt() > 10) {
m_blobs.forEach([](Blob& blob) {blob.update();});
}
}
}
}
private:
static constexpr int blobCount = 30;
SpriteList<Blob, blobCount> m_blobs;
};
void Flashlight::loop() {
m_blobs.update();
}
void Flashlight::render(Display* dpy) const {
m_blobs.render(dpy);
Surface(dpy, {0, 0}, {255, 0}) |= 100;
}
STATIC_ALLOC(Flashlight);
STATIC_TASK(Flashlight);

View File

@ -0,0 +1,15 @@
#pragma once
#include <Figments.h>
#include "../sprites/Blob.h"
class Flashlight: public Figment {
public:
Flashlight();
void handleEvent(const InputEvent& evt) override;
void loop() override;
void render(Display* dpy) const override;
private:
static constexpr int blobCount = 30;
SpriteList<Blob, blobCount> m_blobs;
};

View File

@ -1,42 +0,0 @@
#include <Figments.h>
template<uint8_t MaxBrightness = 255, uint32_t MaxMilliAmps = 500, uint32_t Voltage = 5>
class Power: public Figment {
public:
Power() : Figment("Power") {}
void handleEvent(const InputEvent& evt) override {
switch (evt.intent) {
case InputEvent::PowerToggle:
m_powerState = m_powerState.value() <= 128 ? 255 : 0;
//Log.info("POWER TOGGLE %d", m_powerState.value());
break;
case InputEvent::SetPower:
m_powerState = evt.asInt() == 0 ? 0 : 255;
break;
case InputEvent::SetBrightness:
m_brightness = evt.asInt();
default:
return;
}
}
void loop() override {
m_powerState.update();
m_brightness.update();
}
void render(Display* dpy) const override {
const uint8_t clippedBrightness = min((uint8_t)m_brightness, MaxBrightness);
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);
FastLED.setBrightness(powerBrightness);
}
static constexpr uint32_t Watts = Voltage * MaxMilliAmps;
private:
AnimatedNumber m_powerState = 255;
AnimatedNumber m_brightness = MaxBrightness;
};

54
src/animations/Power.h Normal file
View File

@ -0,0 +1,54 @@
#pragma once
#include <Figments.h>
template<uint8_t MaxBrightness = 255, uint32_t MaxMilliAmps = 500, uint32_t Voltage = 5>
class Power: public Figment {
public:
Power() : Figment("Power") {}
void handleEvent(const InputEvent& evt) override {
switch (evt.intent) {
case InputEvent::PowerToggle:
m_powerState = m_powerState.value() <= 128 ? 255 : 0;
//Log.info("POWER TOGGLE %d", m_powerState.value());
break;
case InputEvent::SetPower:
m_powerState = evt.asInt() == 0 ? 0 : 255;
Log.notice("Power is now %d", m_powerState);
break;
case InputEvent::SetBrightness:
m_brightness = evt.asInt();
m_brightness = 255;
break;
case InputEvent::Beat:
m_beatDecay.set(0, 255);
break;
default:
return;
}
}
void loop() override {
m_powerState.update();
m_brightness.update();
EVERY_N_MILLISECONDS(20) {
m_beatDecay.update(13);
}
}
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);
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);
FastLED.setBrightness(powerBrightness);
}
static constexpr uint32_t Watts = Voltage * MaxMilliAmps;
private:
AnimatedNumber m_powerState = 255;
AnimatedNumber m_brightness = MaxBrightness;
AnimatedNumber m_beatDecay = 255;
};

View File

@ -1,50 +1,72 @@
#include <Figments.h>
#include "../sprites/Blob.h"
#include "SolidAnimation.h"
#include "../Static.h"
class SolidAnimation: public Figment {
private:
AnimatedNumber m_red, m_green, m_blue;
static constexpr int blobCount = 20;
SpriteList<Blob, blobCount> m_blobs;
SolidAnimation::SolidAnimation() : Figment("Solid", Task::Stopped) {
}
public:
SolidAnimation(Task::State initialState) : Figment("Solid", initialState) {
m_blobs.forEach([](Blob& blob) {
blob.setPos(random(140));
blob.setBrightness(random(255));
if (random(255) % 2) {
blob.setVelocity(-1);
}
void SolidAnimation::randomize() {
m_isRandom = true;
m_blobs.forEach([](Blob& blob) {
blob.setPos(random(140));
blob.setBrightness(random(255));
if (random(255) % 2) {
blob.setVelocity(-1);
}
});
}
void SolidAnimation::handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::SetColor) {
CRGB nextColor = evt.asRGB();
m_red.set(nextColor.red);
m_green.set(nextColor.green);
m_blue.set(nextColor.blue);
m_changePct.set(0, 255);
m_prevColor = m_curColor;
m_curColor = nextColor;
m_horizontal = !m_horizontal;
} else if (evt.intent == InputEvent::Beat) {
m_isRandom = false;
}
}
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) {
CRGB rgb{m_red, m_green, m_blue};
CHSV hsv = rgb2hsv_approximate(rgb);
m_blobs.forEach([=](Blob& blob) {
blob.setHue(hsv.hue);
blob.setSaturation(hsv.saturation);
});
m_blobs.update();
}
}
void handleEvent(const InputEvent& evt) override {
if (evt.intent == InputEvent::SetColor) {
CRGB nextColor = evt.asRGB();
m_red.set(nextColor.red);
m_green.set(nextColor.green);
m_blue.set(nextColor.blue);
}
}
#include <Perfcounter.h>
void loop() override {
m_red.update();
m_green.update();
m_blue.update();
EVERY_N_MILLIS(6) {
CRGB rgb{m_red, m_green, m_blue};
CHSV hsv = rgb2hsv_approximate(rgb);
m_blobs.forEach([=](Blob& blob) {
blob.setHue(hsv.hue);
blob.setSaturation(hsv.saturation);
});
m_blobs.update();
}
void SolidAnimation::render(Display* dpy) const {
PerfCounter _("solidRender");
CRGB color(m_red.value(), m_green.value(), m_blue.value());
uint8_t frame = ease8InOutApprox(m_changePct);
if (frame == 255) {
Surface(dpy, {0, 0}, {255, 255}) = color;
} 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;
}
void render(Display* dpy) const override {
CRGB color(m_red.value(), m_green.value(), m_blue.value());
Surface(dpy, {0, 0}, {255, 0}) = color;
m_blobs.render(dpy);
}
};
m_blobs.render(dpy);
}
STATIC_ALLOC(SolidAnimation);
STATIC_TASK(SolidAnimation);

View File

@ -0,0 +1,21 @@
#pragma once
#include <Figments.h>
#include "../sprites/Blob.h"
class SolidAnimation: public Figment {
private:
AnimatedNumber m_red, m_green, m_blue, m_changePct;
CRGB m_curColor;
CRGB m_prevColor;
static constexpr int blobCount = 20;
SpriteList<Blob, blobCount> m_blobs;
void randomize();
bool m_isRandom = false;
bool m_horizontal = false;
public:
SolidAnimation();
void handleEvent(const InputEvent& evt) override;
void loop() override;
void render(Display* dpy) const override;
};

View File

@ -32,3 +32,4 @@ UpdateStatus::render(Display* dpy) const
}
STATIC_ALLOC(UpdateStatus);
STATIC_TASK(UpdateStatus);

View File

@ -1,3 +1,4 @@
#pragma once
#include <Figments.h>
class UpdateStatus: public Figment {

View File

@ -39,3 +39,4 @@ Buttons::read()
}
STATIC_ALLOC(Buttons);
STATIC_TASK(Buttons);

View File

@ -21,6 +21,14 @@ public:
return InputEvent{};
}
void handleEvent(const InputEvent& evt) override {
if (evt.intent == InputEvent::Beat) {
m_idx %= m_colors.size();
m_reset = true;
m_idx++;
}
}
void onStart() override {
m_reset = true;
}

View File

@ -63,3 +63,4 @@ MPU5060::read()
}
STATIC_ALLOC(MPU5060);
STATIC_TASK(MPU5060);

View File

@ -5,8 +5,7 @@
#ifndef PLATFORM_PHOTON
#include <ArduinoLog.h>
#include <NTP.h>
#endif
#endif // !PLATFORM_PHOTON
#include "Platform.h"
@ -16,29 +15,22 @@
#include "Sequencer.h"
#include "LogService.h"
#include "animations/Power.cpp"
#include "animations/SolidAnimation.cpp"
#include "animations/Chimes.cpp"
#include "animations/Flashlight.cpp"
#include "animations/Drain.cpp"
#include <time.h>
#include "animations/Power.h"
#include "animations/SolidAnimation.h"
#include "animations/Chimes.h"
#include "animations/Flashlight.h"
#include "animations/Drain.h"
#include "animations/UpdateStatus.h"
#include "inputs/ColorCycle.h"
#include "inputs/Buttons.h"
#include "inputs/MPU6050.h"
#ifdef PLATFORM_PHOTON
#include "platform/particle/inputs/Photon.h"
#include "platform/particle/inputs/CloudStatus.h"
#include "platform/particle/PhotonTelemetry.h"
#include "platform/particle/WebTelemetry.cpp"
#include "platform/particle/MDNSService.cpp"
#else
#include "WiFiTask.h"
#include "platform/arduino/BluetoothSerialTelemetry.h"
#include "platform/arduino/MQTTTelemetry.h"
#include <ArduinoOTA.h>
#endif
#endif // PLATFORM_PHOTON
//SerialLogHandler logHandler;
@ -51,6 +43,7 @@
// 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());
@ -58,7 +51,9 @@ Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static<ConfigService>::instance()
// Setup power management
Power<MAX_BRIGHTNESS, PSU_MILLIAMPS> power;
FigmentFunc configDisplay([](Display* dpy) {
REGISTER_TASK(power);
/*FigmentFunc configDisplay([](Display* dpy) {
uint8_t brightness = brighten8_video(beatsin8(60));
auto coords = Static<ConfigService>::instance()->coordMap();
for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) {
@ -68,7 +63,7 @@ FigmentFunc configDisplay([](Display* dpy) {
dpy->pixelAt(i) += CRGB(255 - brightness, 255 - brightness, 255 - brightness);
}
}
});
});*/
class InputBlip: public Figment {
public:
@ -94,42 +89,7 @@ private:
};
InputBlip inputBlip;
class ArduinoOTAUpdater : public BufferedInputSource {
public:
ArduinoOTAUpdater() : BufferedInputSource("ArduinoOTA") {
ArduinoOTA.onStart(&ArduinoOTAUpdater::s_onStart);
ArduinoOTA.onProgress(&ArduinoOTAUpdater::s_onProgress);
}
void loop() override {
if (m_online) {
ArduinoOTA.handle();
}
BufferedInputSource::loop();
}
void handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::NetworkStatus && evt.asInt()) {
Log.notice("Booting OTA");
m_online = true;
ArduinoOTA.begin();
}
}
private:
bool m_online = false;
static void s_onStart() {
Log.notice("OTA Start!");
Static<ArduinoOTAUpdater>::instance()->setEvent(InputEvent::FirmwareUpdate);
}
static void s_onProgress(unsigned int progress, unsigned int total) {
Log.notice("OTA Progress! %d / %d", progress, total);
Static<ArduinoOTAUpdater>::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress});
}
};
STATIC_ALLOC(ArduinoOTAUpdater);
REGISTER_TASK(inputBlip);
InputFunc randomPulse([]() {
static unsigned int pulse = 0;
@ -145,7 +105,9 @@ InputFunc randomPulse([]() {
}
}
return InputEvent{};
}, "Pulse", Task::Running);
}, "Pulse", Task::Stopped);
REGISTER_TASK(randomPulse);
InputMapper keyMap([](const InputEvent& evt) {
if (evt.intent == InputEvent::UserInput) {
@ -167,38 +129,96 @@ InputMapper keyMap([](const InputEvent& evt) {
return InputEvent::None;
}, "Keymap");
ChimesAnimation chimes{Task::Stopped};
SolidAnimation solid{Task::Running};
DrainAnimation drain{Task::Stopped};
Flashlight flashlight{Task::Stopped};
REGISTER_TASK(keyMap);
#ifndef DEFAULT_PATTERN_INDEX
#define DEFAULT_PATTERN_INDEX 0
#endif
Sequencer sequencer{{
{"Idle", {"Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"}},
{"Acid", {"Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"}},
{"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}},
{"Interactive", {"Drain", "MPU5060", "CircadianRhythm"}},
{"Flashlight", {"Flashlight"}},
{"Gay", {"Solid", "Pulse", "Rainbow", "Rainbow"}},
{"Acid", {"Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"}},
}};
{"Gay", {"Solid", "Pulse", "Rainbow"}},
}, DEFAULT_PATTERN_INDEX};
REGISTER_TASK(sequencer);
class BPM : public InputSource {
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());
if (m_timings.size() >= 5) {
updateBPM();
}
}
}
InputEvent read() override {
if (m_bpm > 0) {
uint16_t now = millis();
if (now >= m_nextBpm) {
m_nextBpm += m_bpm;
return InputEvent{InputEvent::Beat, m_bpm};
}
if (now >= m_nextLearn && m_nextLearn != 0) {
m_timings.clear();
m_nextLearn = 0;
}
}
return InputEvent{};
}
private:
uint16_t m_bpm = 0;
uint16_t m_nextBpm = 0;
uint16_t m_nextLearn = 0;
Ringbuf<uint16_t, 7> m_timings;
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);
avgDelta += delta;
}
m_bpm = avgDelta / 4;
m_nextLearn = m_bpm * 5 + millis();
Log.notice("BPM is now %d", m_bpm);
uint16_t trash;
m_timings.take(trash);
}
};
STATIC_ALLOC(BPM);
STATIC_TASK(BPM);
// Render all layers to the displays
Renderer renderer{
{&dpy},
{
&chimes,
&drain,
&solid,
&flashlight,
Static<ChimesAnimation>::instance(),
Static<DrainAnimation>::instance(),
Static<SolidAnimation>::instance(),
Static<Flashlight>::instance(),
Static<UpdateStatus>::instance(),
&inputBlip,
&power,
}
};
REGISTER_TASK(renderer);
Renderer configRenderer{
{&dpy},
{&drain, &configDisplay, &inputBlip, &power}
{Static<DrainAnimation>::instance(), /*&configDisplay,*/ &inputBlip, &power}
};
// Cycle some random colors
@ -209,7 +229,9 @@ ColorSequenceInput<9> idleCycle{{
CRGB(128, 0, 128), // Purple
CRGB(255, 255, 255), // White
CRGB(0, 255, 255), // Cyan
}, "IdleColors", Task::Running};
}, "IdleColors", Task::Stopped};
REGISTER_TASK(idleCycle);
ColorSequenceInput<7> rainbowCycle{{
CRGB(255, 0, 0), // Red
@ -219,7 +241,9 @@ ColorSequenceInput<7> rainbowCycle{{
CRGB(128, 0, 128), // Purple
}, "Rainbow", Task::Stopped};
struct ConfigInputTask: public BufferedInputSource {
REGISTER_TASK(rainbowCycle);
/*struct ConfigInputTask: public BufferedInputSource {
public:
ConfigInputTask() : BufferedInputSource("ConfigInput") {}
@ -292,7 +316,7 @@ private:
return InputEvent::None;
}
}
};
};*/
struct ScheduleEntry {
uint8_t hour;
@ -368,9 +392,13 @@ class CircadianRhythm : public InputSource {
uint8_t minute = 0;
needsUpdate = false;
struct tm timeinfo;
Platform::getLocalTime(&timeinfo);
hour = timeinfo.tm_hour;
minute = timeinfo.tm_min;
if (Platform::getLocalTime(&timeinfo)) {
hour = timeinfo.tm_hour;
minute = timeinfo.tm_min;
} else {
hour = 0;
minute = 0;
}
Log.notice("Current time: %d:%d", hour, minute);
return InputEvent{InputEvent::SetBrightness, brightnessForTime(hour, minute)};
}
@ -379,20 +407,16 @@ class CircadianRhythm : public InputSource {
};
STATIC_ALLOC(CircadianRhythm);
STATIC_TASK(CircadianRhythm);
// A special mainloop app for configuring hardware settings that reboots the
// device when the user is finished.
MainLoop configApp{{
/*MainLoop configApp{{
Static<Platform>::instance(),
// Manage read/write of configuration data
Static<ConfigService>::instance(),
#ifdef PLATFORM_PHOTON
// Update photon telemetry
Static<PhotonTelemetry>::instance(),
#endif
// Read hardware inputs
Static<Buttons>::instance(),
@ -408,12 +432,14 @@ MainLoop configApp{{
&inputBlip,
// Render it all
&configRenderer,
}};
}};*/
MainLoop configApp{std::vector<Task*>()};
TaskFunc safeModeNag([]{
static uint8_t frame = 0;
EVERY_N_SECONDS(30) {
Log.notice("I am running in safe mode!");
Log.fatal("I am running in safe mode!");
}
EVERY_N_MILLISECONDS(16) {
frame++;
@ -430,149 +456,72 @@ TaskFunc safeModeNag([]{
}
});
MainLoop safeModeApp({
#ifdef CONFIG_WIFI
#include "platform/arduino/WiFiTask.h"
#endif // CONFIG_WIFI
#ifdef CONFIG_OTA
#include "platform/arduino/OTA.h"
#endif // CONFIG_OTA
#ifdef CONFIG_MQTT
#include "platform/arduino/MQTTTelemetry.h"
#endif // CONFIG_MQTT
MainLoop safeModeApp{{
Static<Platform>::instance(),
// System logging
Static<LogService>::instance(),
&safeModeNag,
#ifdef CONFIG_WIFI
// ESP Wifi
Static<WiFiTask>::instance(),
// System logging
Static<LogService>::instance(),
#endif // CONFIG_WIFI
#ifdef CONFIG_MQTT
// MQTT
Static<MQTTTelemetry>::instance(),
#endif // CONFIG_MQTT
#ifdef CONFIG_OTA
// OTA Updates
Static<ArduinoOTAUpdater>::instance(),
&safeModeNag,
});
// Turn on,
MainLoop renderbugApp{{
Static<Platform>::instance(),
// Load/update graphics configuration from EEPROM
Static<ConfigService>::instance(),
// Platform inputs
// TODO: Merge cloud and esp wifi tasks into a common networking base
#ifdef PLATFORM_PHOTON
// Particle cloud status
Static<CloudStatus>::instance(),
// Monitor network state and provide particle API events
Static<PhotonInput>::instance(),
#else
// ESP Wifi
//Static<WiFiTask>::instance(),
#endif
#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,
// Daily rhythm activities
Static<CircadianRhythm>::instance(),
// Periodic motion input
//&randomPulse,
// Periodic color inputs
&idleCycle,
&rainbowCycle,
// Animations
&chimes,
&drain,
&solid,
&flashlight,
// Update UI layer
&power,
Static<UpdateStatus>::instance(),
&inputBlip,
// Render everything
&renderer,
// Platform telemetry
// TODO: Combine some of these services into a unified telemetry API with
// platform-specific backends?
// Or at least, just the MQTT and watchdog ones.
#ifdef PLATFORM_PHOTON
// Update photon telemetry
Static<PhotonTelemetry>::instance(),
// Web telemetry UI
Static<WebTelemetry>::instance(),
// MQTT telemetry
Static<MQTTTelemetry>::instance(),
// Network discovery
Static<MDNSService>::instance(),
//Watchdog
Static<Watchdog>::instance(),
#else
// MQTT
Static<MQTTTelemetry>::instance(),
// OTA Updates
Static<ArduinoOTAUpdater>::instance(),
#endif
#endif // CONFIG_OTA
}};
MainLoop &runner = renderbugApp;
MainLoop* runner = &safeModeApp;
// Tune in,
void setup() {
// Turn on,
Platform::preSetup();
#ifdef CONFIG_MQTT
Static<MQTTTelemetry>::instance()->setSequencer(&sequencer);
#endif // CONFIG_MQTT
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"📡 Platform %s version %s", Platform::name(), Platform::version());
Platform::bootSplash();
Log.notice(u8"Setting timezone to -7 (PST)");
Platform::setTimezone(-7);
Log.notice(u8" Setting up platform...");
Platform::setup();
Platform::bootSplash();
Log.notice(u8"💡 Starting FastLED...");
Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM);
runner = new MainLoop{std::vector<Task*>{Platform::beginTasks(), Platform::endTasks()}};
// Tune in,
if (Platform::bootopts.isSafeMode) {
Log.notice(u8"⚠️ Starting Figment in safe mode!!!");
runner = safeModeApp;
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;
//runner = &configApp;
} else {
Log.notice(u8"🌌 Starting Figment...");
}
Serial.flush();
runner.start();
runner->start();
//Log.info(u8"💽 %lu bytes of free RAM", System.freeMemory());
Log.notice(u8"🚀 Setup complete! Ready to rock and roll.");
@ -581,6 +530,8 @@ void setup() {
// Drop out.
void loop() {
//Platform::loop();
runner.loop();
EVERY_N_SECONDS(5) {
Log.notice("FPS: %d", FastLED.getFPS());
}
runner->loop();
}

View File

@ -26,7 +26,7 @@ BluetoothSerialTelemetry::read()
// Overwrite the '*' character, to leave us with a complete command
commandBuf[cmdSize-1] = 0;
//Log.notice("Bluetooth read %s", commandBuf);
Log.verbose("Bluetooth read %s", commandBuf);
if (commandBuf[0] == 'R') {
m_color = CRGB(std::atoi(&commandBuf[1]), m_color.g, m_color.b);
@ -49,6 +49,12 @@ BluetoothSerialTelemetry::read()
return InputEvent{InputEvent::SetPower, 0};
} else if (commandBuf[0] == 'p') {
return InputEvent{InputEvent::SetPattern, &commandBuf[1]};
} else if (commandBuf[0] == 'b') {
return InputEvent::BeatDetect;
} else if (commandBuf[0] == 'c') {
return InputEvent{InputEvent::SetDisplayLength, std::atoi(&commandBuf[1])};
} else if (commandBuf[0] == 'Y') {
return InputEvent::SaveConfigurationRequest;
} else if (commandBuf[0] == 'A') {
char* axisVal = strtok(&commandBuf[1], ",");
const uint8_t accelX = std::atof(axisVal) * 10;
@ -75,12 +81,13 @@ BluetoothSerialTelemetry::read()
void
BluetoothSerialTelemetry::onStart()
{
Log.notice("Starting up Bluetooth...");
Log.trace("Starting up Bluetooth...");
if (m_serial.begin(Platform::deviceName())) {
Log.notice("Bluetooth started!");
Log.notice("Bluetooth started! Device name is %s", Platform::deviceName());
} else {
Log.warning("Bluetooth could not be started!");
}
}
STATIC_ALLOC(BluetoothSerialTelemetry);
STATIC_TASK(BluetoothSerialTelemetry);

View File

@ -117,39 +117,37 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt)
Log.notice("Connected to MQTT");
m_needHeartbeat = true;
StaticJsonDocument<1024> configJson;
Lightswitch.toJson(configJson);
m_json.clear();
Lightswitch.toJson(m_json);
int i = 0;
for(const Sequencer::Scene& scene : m_sequencer->scenes()) {
configJson["fx_list"][i++] = scene.name;
m_json["fx_list"][i++] = scene.name;
}
configJson["brightness"] = true;
configJson["rgb"] = true;
m_json["brightness"] = true;
m_json["rgb"] = true;
char buf[1024];
serializeJson(configJson, buf, sizeof(buf));
publishDoc(Lightswitch.configTopic().c_str(), true);
Log.verbose("Publish %s %s", Lightswitch.configTopic().c_str(), buf);
m_mqtt.publish(Lightswitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true);
//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_json.clear();
flashlightSwitch.toJson(m_json, false);
m_json["cmd_t"] = "~/set";
m_json["ret"] = true;
publishDoc(flashlightSwitch.configTopic().c_str(), true);
//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));
m_json.clear();
FPSSensor.toJson(m_json, false);
m_json["unit_of_meas"] = "Frames/s";
publishDoc(FPSSensor.configTopic().c_str(), true);
Log.verbose("Publish %s %s", FPSSensor.configTopic().c_str(), buf);
m_mqtt.publish(FPSSensor.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true);
//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
@ -172,39 +170,28 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt)
String flashlightStatTopic = flashlightSwitch.statTopic();
m_mqtt.publish(flashlightStatTopic.c_str(), "ON");
} else if (evt.intent == InputEvent::SetPower) {
StaticJsonDocument<256> doc;
char buf[256];
m_json.clear();
m_isOn = evt.asInt() ? true : false;
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(statTopic.c_str(), buf);
m_json["state"] = m_isOn ? "ON" : "OFF";
publishDoc(statTopic.c_str());
} else if (evt.intent == InputEvent::SetBrightness) {
StaticJsonDocument<256> doc;
char buf[256];
doc["brightness"] = evt.asInt();
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(statTopic.c_str(), buf);
m_json.clear();
m_json["brightness"] = evt.asInt();
m_json["state"] = m_isOn ? "ON" : "OFF";
publishDoc(statTopic.c_str());
} else if (evt.intent == InputEvent::SetColor) {
StaticJsonDocument<256> doc;
char buf[256];
CRGB color = evt.asRGB();
doc["color"]["r"] = color.r;
doc["color"]["g"] = color.g;
doc["color"]["b"] = color.b;
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(statTopic.c_str(), buf);
m_json.clear();
m_json["color"]["r"] = color.r;
m_json["color"]["g"] = color.g;
m_json["color"]["b"] = color.b;
m_json["state"] = m_isOn ? "ON" : "OFF";
publishDoc(statTopic.c_str());
} else if (evt.intent == InputEvent::SetPattern) {
StaticJsonDocument<256> doc;
char buf[256];
doc["effect"] = evt.asString();
doc["state"] = m_isOn ? "ON" : "OFF";
serializeJson(doc, buf, sizeof(buf));
m_mqtt.publish(statTopic.c_str(), buf);
} else if (evt.intent == InputEvent::FirmwareUpdate) {
String updateTopic = m_debugTopic + "/firmware";
m_mqtt.publish(updateTopic.c_str(), "firmware update!");
m_json.clear();
m_json["effect"] = evt.asString();
m_json["state"] = m_isOn ? "ON" : "OFF";
publishDoc(statTopic.c_str());
}
}
}
@ -241,22 +228,20 @@ MQTTTelemetry::loopOnline()
m_needHeartbeat = true;
}
if (m_needHeartbeat) {
char buf[512];
StaticJsonDocument<512> response;
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_json.clear();
m_json["device_id"] = Platform::deviceID();
m_json["sketch_version"] = ESP.getSketchMD5();
m_json["os_version"] = ESP.getSdkVersion();
m_json["localip"] = WiFi.localIP().toString();
m_json["pixelCount"] = Static<ConfigService>::instance()->coordMap()->physicalPixelCount();
//m_json["startPixel"] = Static<ConfigService>::instance()->coordMap()->startPixel;
m_json["RSSI"] = WiFi.RSSI();
m_json["free_ram"] = ESP.getFreeHeap();
m_json["fps"] = FastLED.getFPS();
String availTopic = m_rootTopic + "/available";
m_mqtt.publish(Lightswitch.heartbeatTopic().c_str(), buf);
publishDoc(Lightswitch.heartbeatTopic().c_str());
m_mqtt.publish(Device.availabilityTopic.c_str(), "online");
//Log.notice("Heartbeat: %s", buf);
//Log.trace("Heartbeat: %s", buf);
String fpsCounter = String(FastLED.getFPS());
m_mqtt.publish(FPSSensor.statTopic().c_str(), fpsCounter.c_str());
@ -280,89 +265,106 @@ MQTTTelemetry::callback(char* topic, const char* payload)
setEvent(InputEvent{InputEvent::SetPattern, "Idle"});
}
} else if (Lightswitch.isCommandTopic(topic)) {
StaticJsonDocument<512> doc;
deserializeJson(doc, payload);
deserializeJson(m_json, payload);
if (doc.containsKey("state")) {
if (doc["state"] == "ON") {
if (m_json.containsKey("state")) {
if (m_json["state"] == "ON") {
Log.notice("Turning on power");
setEvent(InputEvent{InputEvent::SetPower, true});
} else if (doc["state"] == "OFF") {
} else if (m_json["state"] == "OFF") {
Log.notice("Turning off power");
setEvent(InputEvent{InputEvent::SetPattern, "Idle"});
setEvent(InputEvent{InputEvent::SetPower, false});
}
}
if (doc.containsKey("start")) {
strcpy(m_patternBuf, doc["start"].as<const char*>());
if (m_json.containsKey("start")) {
strcpy(m_patternBuf, m_json["start"].as<const char*>());
setEvent(InputEvent{InputEvent::StartThing, m_patternBuf});
}
if (doc.containsKey("stop")) {
if (doc["stop"] == name) {
Log.notice("You can't kill an idea, or stop the MQTT Task via MQTT.");
if (m_json.containsKey("stop")) {
if (m_json["stop"] == name) {
Log.warning("You can't kill an idea, or stop the MQTT Task via MQTT.");
} else {
strcpy(m_patternBuf, doc["stop"].as<const char*>());
strcpy(m_patternBuf, m_json["stop"].as<const char*>());
setEvent(InputEvent{InputEvent::StopThing, m_patternBuf});
}
}
if (doc.containsKey("pixelCount")) {
setEvent(InputEvent{InputEvent::SetDisplayLength, (int)doc["pixelCount"]});
if (m_json.containsKey("pixelCount")) {
Log.notice("Pixel count changed");
setEvent(InputEvent{InputEvent::SetDisplayLength, m_json["pixelCount"].as<int>()});
}
if (doc.containsKey("startPixel")) {
setEvent(InputEvent{InputEvent::SetDisplayOffset, (int)doc["startPixel"]});
if (m_json.containsKey("startPixel")) {
Log.notice("Start pixel changed");
setEvent(InputEvent{InputEvent::SetDisplayOffset, m_json["startPixel"].as<int>()});
}
if (doc.containsKey("save")) {
if (m_json.containsKey("save")) {
setEvent(InputEvent{InputEvent::SaveConfigurationRequest});
}
if (doc.containsKey("restart")) {
#ifdef BOARD_ESP8266
ESP.wdtDisable();
ESP.restart();
#endif
if (m_json.containsKey("restart")) {
Platform::restart();
}
if (doc.containsKey("reconnect")) {
if (m_json.containsKey("reconnect")) {
m_mqtt.disconnect();
}
if (doc.containsKey("ping")) {
if (m_json.containsKey("ping")) {
m_needHeartbeat = true;
Log.notice("Queuing up heartbeat");
}
if (doc.containsKey("effect")) {
strcpy(m_patternBuf, doc["effect"].as<const char*>());
if (m_json.containsKey("effect")) {
strcpy(m_patternBuf, m_json["effect"].as<const char*>());
setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf});
}
if (doc.containsKey("color")) {
uint8_t r = doc["color"]["r"];
uint8_t g = doc["color"]["g"];
uint8_t b = doc["color"]["b"];
if (m_json.containsKey("color")) {
uint8_t r = m_json["color"]["r"];
uint8_t g = m_json["color"]["g"];
uint8_t b = m_json["color"]["b"];
setEvent(InputEvent{InputEvent::SetColor, CRGB(r, g, b)});
}
if (doc.containsKey("brightness")) {
setEvent(InputEvent{InputEvent::SetBrightness, (int)doc["brightness"]});
if (m_json.containsKey("brightness")) {
setEvent(InputEvent{InputEvent::SetBrightness, m_json["brightness"].as<int>()});
}
Log.notice("Event done.");
}
}
void
MQTTTelemetry::s_callback(char* topic, byte* payload, unsigned int 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);
strcpy(s_topicBuf, topic);
memcpy(s_payloadBuf, payload, length);
s_payloadBuf[std::min(sizeof(s_payloadBuf) - 1, length)] = 0;
Static<MQTTTelemetry>::instance()->callback(s_topicBuf, s_payloadBuf);
}
char MQTTTelemetry::s_topicBuf[128] = {0};
char MQTTTelemetry::s_payloadBuf[512] = {0};
void
MQTTTelemetry::publishDoc(const char* topic, bool retain)
{
m_mqtt.beginPublish(topic, measureJson(m_json), retain);
serializeJson(m_json, m_mqtt);
m_mqtt.endPublish();
}
void
MQTTTelemetry::publishDoc(const char* topic)
{
publishDoc(topic, false);
}
STATIC_ALLOC(MQTTTelemetry);
STATIC_TASK(MQTTTelemetry);

View File

@ -12,6 +12,7 @@
#include <WiFi.h>
#endif
#include <ArduinoJson.h>
class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
public:
@ -25,13 +26,16 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
public:
LogPrinter(MQTTTelemetry* telemetry) : telemetry(telemetry) {};
size_t write(uint8_t byte) {
char outBuf[512];
if (byte == '\n') {
size_t bufSize = buf.write(outBuf);
outBuf[std::min(sizeof(outBuf), bufSize)] = 0;
Serial.println(outBuf);
char c;
String logTopic = telemetry->m_debugTopic + "/log";
telemetry->m_mqtt.publish(logTopic.c_str(), outBuf);
telemetry->m_mqtt.beginPublish(logTopic.c_str(), buf.size(), false);
while (buf.take(c)) {
Serial.write(c);
telemetry->m_mqtt.write(c);
}
Serial.println();
telemetry->m_mqtt.endPublish();
} else {
buf.insert(byte);
}
@ -61,9 +65,15 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
char m_patternBuf[48];
bool m_needHeartbeat = false;
bool m_isOn = true;
static char s_topicBuf[128];
static char s_payloadBuf[512];
void publishDoc(const char* topic);
void publishDoc(const char* topic, bool retain);
Sequencer *m_sequencer = 0;
WiFiClient m_wifi;
PubSubClient m_mqtt;
LogPrinter m_logPrinter;
StaticJsonDocument<1024> m_json;
};

View File

@ -0,0 +1,34 @@
#include "OTA.h"
#include "../../Static.h"
ArduinoOTAUpdater::ArduinoOTAUpdater() : BufferedInputSource("ArduinoOTA") {
ArduinoOTA.onStart(&ArduinoOTAUpdater::s_onStart);
ArduinoOTA.onProgress(&ArduinoOTAUpdater::s_onProgress);
}
void ArduinoOTAUpdater::loop() {
if (m_online) {
ArduinoOTA.handle();
}
BufferedInputSource::loop();
}
void ArduinoOTAUpdater::handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::NetworkStatus && evt.asInt()) {
Log.notice("Booting OTA");
m_online = true;
ArduinoOTA.begin();
}
}
void ArduinoOTAUpdater::s_onStart() {
Log.notice("OTA Start!");
Static<ArduinoOTAUpdater>::instance()->setEvent(InputEvent::FirmwareUpdate);
}
void ArduinoOTAUpdater::s_onProgress(unsigned int progress, unsigned int total) {
Log.notice("OTA Progress! %d / %d", progress, total);
Static<ArduinoOTAUpdater>::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress});
}
STATIC_ALLOC(ArduinoOTAUpdater);
STATIC_TASK(ArduinoOTAUpdater);

View File

@ -0,0 +1,14 @@
#include <ArduinoOTA.h>
#include <Figments.h>
class ArduinoOTAUpdater : public BufferedInputSource {
public:
ArduinoOTAUpdater();
void loop() override;
void handleEvent(const InputEvent& evt) override;
private:
bool m_online = false;
static void s_onStart();
static void s_onProgress(unsigned int progress, unsigned int total);
};

View File

@ -0,0 +1,232 @@
#include <Figments.h>
#include <U8g2lib.h>
#include "../../Static.h"
#include <ArduinoLog.h>
#include "../../LogService.h"
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, 16, 15, 4);
class U8Display : public Task {
public:
U8Display() : Task("U8Display") {}
enum ScreenState {
BootSplash,
Running,
Message,
Idle = Running
};
void onStart() {
xTaskCreatePinnedToCore(
&U8Display::redrawTask,
name,
2000,
this,
1,
&m_renderTask, 0
);
}
void handleEvent(const InputEvent& evt) {
m_lastEvent = evt;
if (m_state == Idle) {
switch(evt.intent) {
case InputEvent::NetworkStatus:
m_nextState = Message;
m_message = evt.asBool() ? "Online!" : "Offline :[";
break;
case InputEvent::SetPattern:
m_nextState = Message;
m_message = "Pattern: " + String(evt.asString());
break;
case InputEvent::SetPower:
m_nextState = Message;
m_message = evt.asBool() ? "Power On" : "Power Off";
break;
case InputEvent::SaveConfigurationRequest:
m_nextState = Message;
m_message = "Settings Saved!";
break;
case InputEvent::FirmwareUpdate:
m_nextState = Message;
m_message = "Firmware update";
break;
case InputEvent::PreviousPattern:
m_nextState = Message;
m_message = "Previous Pattern";
break;
case InputEvent::NextPattern:
m_nextState = Message;
m_message = "Next Pattern";
break;
}
}
}
void drawSplash() {
//u8g2.setFont(u8g2_font_VCR_OSD_mr);
u8g2.setFont(u8g2_font_HelvetiPixelOutline_tr);
u8g2.setDrawColor(1);
uint8_t splashWidth = u8g2.getStrWidth("Renderbug!");
u8g2.drawStr(64 - splashWidth / 2, 32 - 15, "Renderbug!");
u8g2.setFont(u8g2_font_7x13B_mr);
u8g2.setCursor(15, 64 - 7);
u8g2.print("Version ");
u8g2.print(RENDERBUG_VERSION);
for(int i = 0; i < 3; i++) {
uint8_t sparkleX = (64 - splashWidth/2) + scale8(7-i, beatsin8(40)) * (splashWidth/7) + 5;
uint8_t sparkleY = scale8(3+i, beatsin8(40)) * 3 + 7;
u8g2.setDrawColor(2);
if (beatsin8(60*4) + i * 3 >= 170) {
u8g2.drawLine(sparkleX - 3, sparkleY - 3, sparkleX + 3, sparkleY + 3);
u8g2.drawLine(sparkleX + 3, sparkleY - 3, sparkleX - 3, sparkleY + 3);
} else if (beatsin8(60*4) + i * 2 >= 82) {
u8g2.drawLine(sparkleX - 4, sparkleY - 4, sparkleX + 2, sparkleY + 2);
u8g2.drawLine(sparkleX - 2, sparkleY - 2, sparkleX + 4, sparkleY + 4);
u8g2.drawLine(sparkleX + 4, sparkleY - 4, sparkleX - 2, sparkleY + 2);
u8g2.drawLine(sparkleX + 2, sparkleY - 2, sparkleX - 4, sparkleY + 4);
} else {
u8g2.drawLine(sparkleX - 2, sparkleY, sparkleX + 2, sparkleY);
u8g2.drawLine(sparkleX, sparkleY - 2, sparkleX, sparkleY + 2);
}
}
}
void drawMessage() {
//u8g2.setFont(u8g2_font_VCR_OSD_mr);
u8g2.setFont(u8g2_font_HelvetiPixelOutline_tr);
uint8_t splashWidth = u8g2.getStrWidth(m_message.c_str());
if (splashWidth >= 128) {
u8g2.setFont(u8g2_font_7x13B_mr);
splashWidth = u8g2.getStrWidth(m_message.c_str());
}
u8g2.drawStr(64 - splashWidth / 2, 32 - 15, m_message.c_str());
}
void drawTime() {
u8g2.setFont(u8g2_font_7x13O_tn);
u8g2.setCursor(0, 64);
struct tm timeinfo;
Platform::getLocalTime(&timeinfo);
uint8_t hour = timeinfo.tm_hour;
uint8_t minute = timeinfo.tm_min;
u8g2.print(hour);
u8g2.print(":");
u8g2.print(minute);
}
void drawState(ScreenState state) {
switch(state) {
case BootSplash:
drawSplash();
break;
case Message:
drawMessage();
break;
case Running:
uint8_t y = 11;
u8g2.setFont(u8g2_font_7x13B_mr);
u8g2.setCursor(0, y);
u8g2.print("FPS: ");
u8g2.setFont(u8g2_font_7x13O_tn);
u8g2.print(FastLED.getFPS());
y += 12;
u8g2.setCursor(0, y);
u8g2.setFont(u8g2_font_7x13B_mr);
u8g2.print("Last event: ");
y += 7;
u8g2.setCursor(10, y);
u8g2.setFont(u8g2_font_4x6_tr);
const char* intentName = LogService::intentName(m_lastEvent.intent);
if (intentName) {
u8g2.print(intentName);
} else {
u8g2.print("<");
u8g2.print(m_lastEvent.intent);
u8g2.print(">");
}
y += 12;
u8g2.setCursor(15, y);
u8g2.setFont(u8g2_font_7x13O_tf);
u8g2.print(LogService::eventValue(m_lastEvent));
drawTime();
break;
}
}
void loop() {
EVERY_N_MILLISECONDS(8) {
xTaskNotifyGive(m_renderTask);
}
}
private:
ScreenState m_state = BootSplash;
ScreenState m_nextState = BootSplash;
uint8_t m_transitionFrame = 0;
uint8_t m_screenStartTime = 0;
InputEvent m_lastEvent;
String m_message;
TaskHandle_t m_renderTask;
void redraw() {
if (m_state != m_nextState) {
EVERY_N_MILLISECONDS(8) {
constexpr uint8_t speed = 11;
if (m_transitionFrame <= 255 - speed) {
m_transitionFrame += speed;
} else {
m_transitionFrame = 255;
}
uint8_t offset = ease8InOutApprox(m_transitionFrame);
u8g2.clearBuffer();
if (m_transitionFrame <= 128) {
uint8_t width = scale8(128, offset * 2);
u8g2.setDrawColor(1);
drawState(m_state);
u8g2.drawBox(0, 0, width, 64);
u8g2.setDrawColor(2);
u8g2.drawBox(width, 0, 8, 64);
} else {
uint8_t width = scale8(128, offset/2)*2;
u8g2.setDrawColor(1);
drawState(m_nextState);
u8g2.setDrawColor(2);
u8g2.drawBox(std::max(0, width - 8), 0, 8, 64);
u8g2.setDrawColor(1);
u8g2.drawBox(width, 0, 128, 64);
}
u8g2.sendBuffer();
if (m_transitionFrame == 255) {
m_state = m_nextState;
m_screenStartTime = millis();
m_transitionFrame = 0;
}
}
} else {
u8g2.clearBuffer();
drawState(m_state);
u8g2.sendBuffer();
uint16_t screenTime = millis() - m_screenStartTime;
if (screenTime >= 7000 && m_state != Idle) {
m_nextState = Idle;
}
}
}
static void redrawTask(void* data) {
u8g2.begin();
U8Display* self = static_cast<U8Display*>(data);
self->m_screenStartTime = millis();
while (true) {
self->redraw();
ulTaskNotifyTake(0, portMAX_DELAY);
}
}
};
STATIC_ALLOC(U8Display);
STATIC_TASK(U8Display);

View File

@ -39,3 +39,4 @@ WiFiTask::read()
}
STATIC_ALLOC(WiFiTask);
STATIC_TASK(WiFiTask);

View File

@ -33,3 +33,4 @@ class MDNSService : public Task {
};
STATIC_ALLOC(MDNSService);
STATIC_TASK(MDNSService);

View File

@ -202,4 +202,4 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin {
};
STATIC_ALLOC(MQTTTelemetry);
STATIC_TASK(MQTTTelemetry);

View File

@ -67,3 +67,4 @@ PhotonTelemetry::handleEvent(const InputEvent &evt)
}
STATIC_ALLOC(PhotonTelemetry);
STATIC_TASK(PhotonTelemetry);

View File

@ -28,3 +28,4 @@ class Watchdog : public Task {
};
STATIC_ALLOC(Watchdog);
STATIC_TASK(Watchdog);

View File

@ -154,4 +154,5 @@ class WebTelemetry : public Task {
}
};
STATIC_ALLOC(WebTelemetry);
STATIC_TASK(WebTelemetry);

View File

@ -26,3 +26,4 @@ CloudStatus::initNetwork(system_event_t event, int param) {
}
STATIC_ALLOC(CloudStatus);
STATIC_TASK(CloudStatus);

View File

@ -182,6 +182,5 @@ PhotonInput::setStartPixel(String command)
return 0;
}
STATIC_ALLOC(PhotonInput);
STATIC_TASK(PhotonInput);

View File

@ -1,9 +1,11 @@
#pragma once
#include <Figments.h>
#include <ArduinoLog.h>
#include <Perfcounter.h>
class Blob {
uint16_t m_pos;
uint8_t m_pos;
int8_t m_velocity;
uint8_t m_hue;
int16_t m_brightness;
@ -49,13 +51,26 @@ public:
}
void render(Display* display) const {
PerfCounter _("blobRender");
const uint8_t width = 25;
//Log.notice("get coords");
auto map = display->coordinateMapping();
// Grab the physical pixel we'll start with
PhysicalCoordinates startPos = map->virtualToPhysicalCoords({m_pos, 0});
PhysicalCoordinates endPos = map->virtualToPhysicalCoords({m_pos + width, 0});
uint8_t scaledWidth = std::abs(endPos.x - startPos.x);
//PhysicalCoordinates startPos = map->virtualToPhysicalCoords({m_pos, m_pos});
//PhysicalCoordinates endPos = map->virtualToPhysicalCoords({m_pos + width, m_pos});
Surface sfc{display, {m_pos, m_pos}, {qadd8(m_pos, width), qadd8(m_pos, width)}};
sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
uint8_t pixelMod = std::min(surfaceCoords.y, surfaceCoords.x);
pixelMod = surfaceCoords.x;
// Blobs desaturate and dim towards their edges
uint8_t saturation = lerp8by8(0, m_brightness, pixelMod);
uint8_t val = lerp8by8(0, m_brightness, pixelMod);
CHSV blobColor(m_hue, m_saturation, val);
//PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), startPos.y};
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++) {
@ -66,10 +81,10 @@ public:
//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};
PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), startPos.y};
CRGB src(display->pixelAt(pos));
display->pixelAt(pos) = blend(CRGB(blobColor), src, 200);
}
display->pixelAt(pos) = blend(CRGB(blobColor), src, 140);
}*/
}
};

View File

@ -1,11 +0,0 @@
This directory is intended for PlatformIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html