wip commit

This commit is contained in:
Torrie Fischer 2023-12-26 11:29:49 +01:00
parent 6582bae0f2
commit ebbf433cdf
40 changed files with 904 additions and 170 deletions

View File

@ -1,6 +1,6 @@
{
"version": 1,
"rotation": 3,
"rotation": 2,
"strides": [
{"x": 0, "y": 0, "pixels": 17},
{"x": 0, "y": 1, "pixels": 17},

View File

@ -2,7 +2,6 @@
"version": 1,
"tasks": [
"Renderer",
"U8Display",
"WiFi",
"MQTT",
"ArduinoOTA",
@ -10,7 +9,7 @@
"Serial"
],
"scenes": {
"Idle": ["Solid", "MPU5060", "IdleColors", "CircadianRhythm"],
"Idle": ["Solid", "IdleColors", "CircadianRhythm"],
"Flashlight": ["Flashlight"]
},
"surfaceMap": "default",

View File

@ -5,20 +5,20 @@
"Renderer",
"Serial"
],
"scenes": {
"Rain": ["Rain", "Rainbow"],
"Test": ["Test"],
"Idle": ["Solid", "Pulse", "Rainbow", "CircadianRhythm"],
"Acid": ["Chimes", "Pulse", "IdleColors", "Rainbow"],
"Flashlight": ["Flashlight"]
},
"surfaceMap": "djstrip",
"defaults": {
"mqtt.ip": "10.0.0.2",
"power.volts": 0,
"power.milliamps": 0,
"power.useBPM": false,
"bpm.idle": 45
"bpm.idle": 45,
"scenes": {
"Rain": ["Rain", "Rainbow"],
"Test": ["Test"],
"Idle": ["Solid", "Pulse", "Rainbow", "CircadianRhythm"],
"Acid": ["Chimes", "Pulse", "IdleColors", "Rainbow"],
"Flashlight": ["Flashlight"]
}
}
}

View File

@ -7,11 +7,12 @@
"MQTT",
"ArduinoOTA",
"UpdateStatusAnimation",
"Serial"
"Serial",
"U8Display"
],
"scenes": {
"Idle": ["Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"],
"Acid": ["Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"],
"Idle": ["Solid", "Rainbow", "CircadianRhythm"],
"Acid": ["Chimes", "Rainbow"],
"Flashlight": ["Flashlight"]
},
"surfaceMap": "default",

View File

@ -8,24 +8,40 @@
"UpdateStatusAnimation",
"Serial",
"Weather",
"CircadianRhythm"
"Test",
"CircadianRhythm",
"IdleTimer"
],
"scenes": {
"Clear": ["Solid", "Rainbow"],
"Rain": ["Rain"],
"Drizzle": ["Rain"],
"Mist": ["Rain"],
"Snow": ["Solid"],
"UnknownWeather": ["Solid", "IdleColors"],
"Test": ["Test"],
"Idle": ["Solid", "Rainbow"],
"Acid": ["Chimes", "IdleColors", "Rainbow"],
"Flashlight": ["Flashlight"]
},
"surfaceMap": "ponder",
"defaults": {
"mqtt.ip": "10.0.0.2",
"power.volts": 0,
"power.milliamps": 0
"colors.sequences": {
"Rainbow": [[255, 0, 0], [255, 127, 0], [0, 255, 0], [0, 0, 255], [128, 0, 128]],
"Idle": [[0, 123, 167], [80, 200, 120], [207, 113, 175], [128, 0, 128], [255, 255, 255], [0, 255, 255]],
"Wet": [[3, 177, 252], [3, 78, 252], [126, 141, 242]],
"Snowy": [[215, 219, 245], [255, 255, 255], [192, 250, 244]],
"Sunny": [[213, 237, 95], [237, 180, 95], [245, 178, 10]]
},
"colors.scenes": {
"Clear": "Sunny",
"Idle": "Rainbow",
"Rain": "Wet",
"Drizzle": "Wet",
"Mist": "Wet",
"Snow": "Snowy"
},
"colors.secondsPerColor": 9,
"scenes": {
"Clear": ["Solid"],
"Cloudy": ["Cloudy", "Solid"],
"Rain": ["Rain", "Solid"],
"Drizzle": ["Rain", "Cloudy", "Solid"],
"Mist": ["Cloudy", "Solid"],
"Snow": ["Solid"],
"UnknownWeather": ["Thinking"],
"Start": ["Thinking"],
"Idle": ["Solid"],
"Test": ["Test"]
}
}
}

View File

@ -0,0 +1,7 @@
{
"version": 1,
"tasks": [
"Renderer",
"Serial"
]
}

View File

@ -6,6 +6,9 @@ class Args {
String *str;
public:
Args(String *str) : str(str) {}
operator const char*() const {
return str->c_str();
}
String operator[](int pos) {
char buf[64];
strncpy(buf, str->c_str(), sizeof(buf));

View File

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

View File

@ -21,6 +21,7 @@ Renderer::loop()
{
uint16_t totalPower = 0;
for(Display* dpy : m_displays) {
dpy->clear();
totalPower += calculate_unscaled_power_mW(dpy->pixelBacking(), dpy->pixelCount());
for(Figment* figment : m_figments) {
if (figment->state == Task::Running) {

0
lib/Figments/Scheduler.h Normal file
View File

View File

@ -43,6 +43,14 @@ Surface::operator+=(const CRGB& color)
return *this;
}
void
Surface::blend(const CRGB& color, uint8_t pct)
{
paintWith([&](CRGB& pixel) {
nblend(pixel, color, pct);
});
}
void
Surface::paintWith(std::function<void(CRGB&)> func)
{

View File

@ -31,6 +31,8 @@ public:
*/
Surface& operator+=(const CRGB& color);
void blend(const CRGB& color, uint8_t pct);
/**
* OR operation that applies the given color to every pixel on the surface
*/

View File

@ -11,21 +11,18 @@
[env]
src_filter = +<*>, -<.git/>, -<.svn/>, -<platform/>
lib_ldf_mode = chain+
extra_scripts = pre:verify-configs.py, pre:build-hal.py
extra_scripts = post:verify-configs.py, pre:build-hal.py
check_flags =
cppcheck: --inline-suppr --suppress=*:*/.pio/*
board_build.filesystem = littlefs
upload_speed = 115200
monitor_speed = 115200
build_type = debug
build_flags =
-DFASTLED_ALL_PINS_HARDWARE_SPI
src_build_flags =
-DFASTLED_ALL_PINS_HARDWARE_SPI
-DRENDERBUG_VERSION=3
-DRENDERBUG_LED_PIN=14
-DRENDERBUG_LED_PACKING=RGB
-DDEFAULT_PATTERN_INDEX=0
-fstack-protector
-Wall
lib_deps =

153
src/Colors.cpp Normal file
View File

@ -0,0 +1,153 @@
#include "Colors.h"
#include "Static.h"
Colors::Colors()
: BufferedInputSource("Colors"),
m_colorTimer(0),
m_secondsPerColor(9)
{
state = Task::Running;
}
void
Colors::loop()
{
EVERY_N_SECONDS(1) {
if (m_colorTimer > 0) {
Log.verbose("colors: tick");
m_colorTimer -= 1;
if (m_colorTimer == 0) {
rotate();
}
}
}
ConfigTaskMixin::loop();
}
void
Colors::handleEvent(const InputEvent& evt)
{
if (evt.intent == InputEvent::SetScene) {
setScene(evt.asString());
}
ConfigTaskMixin::handleEvent(evt);
}
void
Colors::setSequence(const char* seqName)
{
bool found = false;
int idx = 0;
for(Sequence seq : m_sequences) {
if (strcmp(seq.name.c_str(), seqName) == 0) {
found = true;
break;
}
idx++;
}
if (found) {
Log.info("colors: Setting sequence to %s", seqName);
m_idx = idx;
rotate();
} else {
Log.warning("colors: Unknown sequence %s", seqName);
}
}
void
Colors::rotate()
{
if (!m_sequences.empty()) {
Log.info("colors: rotate!") ;
m_colorIdx += 1;
m_colorIdx %= m_sequences[m_idx].colors.size();
m_colorTimer = m_secondsPerColor;
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetColor, m_sequences[m_idx].colors[m_colorIdx]});
//setEvent(InputEvent{InputEvent::SetColor, m_sequences[m_idx].colors[m_colorIdx]});
} else {
Log.info("colors: No colors to rotate");
}
}
void
Colors::setScene(const char* sceneName)
{
auto found = m_sceneMap.find(sceneName);
if (found != m_sceneMap.end()) {
Log.info("colors: Running scene %s", sceneName);
setSequence(found->second);
} else {
Log.warning("colors: Unknown scene %s", sceneName);
m_colorTimer = 0;
}
}
void
Colors::handleConfigChange(const Configuration& cfg)
{
Log.info("colors: reloading config");
m_sequences.clear();
m_colorIdx = 0;
m_idx = 0;
m_secondsPerColor = cfg.get("colors.secondsPerColor", 9);
m_colorTimer = 0;
JsonObject seqJson = cfg.get("colors.sequences");
for(const auto seq : seqJson) {
Log.info("colors: Found color sequence %s", seq.key().c_str());
std::vector<CRGB> colors;
for(JsonArray rgb : seq.value().as<JsonArray>()) {
colors.emplace_back(CRGB(rgb[0], rgb[1], rgb[2]));
}
m_sequences.emplace_back(Sequence{
seq.key().c_str(),
std::move(colors)
});
}
m_sceneMap.clear();
JsonObject sceneJson = cfg.get("colors.scenes");
for(const auto scene : sceneJson) {
Log.info("colors: Found scene %s", scene.key().c_str());
m_sceneMap[scene.key().c_str()] = scene.value().as<JsonString>().c_str();
}
rotate();
}
void
Colors::doSequences(Args& args, Print& out)
{
out.println("Color sequences:");
for(const Sequence& seq : m_sequences) {
out.print("\t");
out.println(seq.name.c_str());
}
}
void
Colors::doSequence(Args& args, Print& out)
{
setSequence(args[1].c_str());
}
void
Colors::doRotate(Args& args, Print& out)
{
rotate();
}
const std::vector<Command>&
Colors::commands() const
{
static const std::vector<Command> _commands = {
{"sequences", &Colors::doSequences},
{"sequence", &Colors::doSequence},
{"rotate", &Colors::doRotate}
};
return _commands;
}
STATIC_TASK(Colors);
STATIC_ALLOC(Colors);

36
src/Colors.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <Figments.h>
#include "Config.h"
#include <map>
class Colors: public BufferedInputSource, ConfigTaskMixin {
public:
struct Sequence {
String name;
const std::vector<CRGB> colors;
};
Colors();
void loop() override;
void handleEvent(const InputEvent& evt) override;
void handleConfigChange(const Configuration& cfg) override;
const std::vector<Command>& commands() const override;
private:
uint16_t m_idx;
uint16_t m_colorIdx;
uint16_t m_colorTimer;
uint16_t m_secondsPerColor;
std::vector<Sequence> m_sequences;
std::map<const char*, const char*> m_sceneMap;
void setScene(const char* sceneName);
void setSequence(const char* seqName);
void doSequences(Args& args, Print& out);
void doSequence(Args& args, Print& out);
void doRotate(Args& args, Print& out);
void rotate();
};

View File

@ -1,6 +1,5 @@
#include "./Config.h"
#include "./Static.h"
#include "./Sequencer.h"
#include <ArduinoLog.h>
#include <ArduinoJson.h>
@ -23,6 +22,12 @@ Configuration::Configuration(const JsonObject& data)
{
}
JsonVariant
Configuration::get(const char* key) const
{
return m_json[key];
}
const char*
Configuration::get(const char* key, const char* defaultVal) const
{
@ -53,8 +58,6 @@ Configuration::get(const char* key, bool defaultVal) const
}
}
StaticJsonDocument<2048> jsonConfig;
constexpr uint16_t HardwareConfig::MAX_LED_NUM;
HardwareConfig
@ -149,12 +152,15 @@ ConfigService::loadMap(const String& mapName)
{
String fname = String("/maps/") + mapName + ".json";
if (LittleFS.exists(fname)) {
DynamicJsonDocument jsonConfig(2048);
File configFile = LittleFS.open(fname, "r");
Log.notice("config: Loading coordinate map from %s", fname.c_str());
deserializeJson(jsonConfig, configFile);
configFile.close();
JsonArray strideList = jsonConfig["strides"];
uint8_t rotation = jsonConfig["rotation"];
m_jsonMap.load(strideList);
m_jsonMap.rotation = rotation;
return true;
} else {
return false;
@ -171,25 +177,13 @@ ConfigService::loadProfile(const char* profileName)
if (LittleFS.exists(fname)) {
strncpy(m_config.data.loadedProfile, fname.c_str(), sizeof(m_config.data.loadedProfile));
File configFile = LittleFS.open(fname, "r");
jsonConfig.clear();
deserializeJson(jsonConfig, configFile);
DynamicJsonDocument jsonConfig(2048*2);
auto err = deserializeJson(jsonConfig, configFile);
configFile.close();
//int profileLogLevel = max(0, min(6, jsonConfig["logLevel"]));
//Log.setLevel(profileLogLevel);
//Log.trace("config: \t %d logging level");
JsonObject sceneList = jsonConfig["scenes"];
std::vector<Sequencer::Scene> scenes;
for(JsonPair pair : sceneList) {
Log.notice("config: \tFound scene %s", pair.key().c_str());
std::vector<const char*> patterns;
for(const char* taskName : pair.value().as<JsonArray>()) {
patterns.push_back(taskName);
}
scenes.push_back(Sequencer::Scene{pair.key().c_str(), patterns});
if (err) {
Log.error("JSON failure: %s", err.c_str());
}
Static<Sequencer>::instance()->setScenes(std::move(scenes));
JsonArray taskList = jsonConfig["tasks"];
Log.notice("config: Starting %d tasks", taskList.size());
@ -197,14 +191,8 @@ ConfigService::loadProfile(const char* profileName)
MainLoop::instance()->dispatchSync(InputEvent{InputEvent::StartThing, taskList[i].as<const char*>()});
}
JsonObject defaults = jsonConfig["defaults"];
Log.notice("config: Loading %d app configurations", defaults.size());
MainLoop::instance()->dispatchSync(InputEvent{InputEvent::ConfigurationChanged, &defaults});
String mapName = jsonConfig["surfaceMap"];
jsonConfig.clear();
if (mapName.isEmpty()) {
Log.warning("config: No coordinate map defined! Defaulting to linear mapping.", mapName.c_str());
m_jsonMap.loadDefault();
@ -212,6 +200,11 @@ ConfigService::loadProfile(const char* profileName)
Log.warning("config: Couldn't load coordinate map %s!!! Defaulting to linear mapping.", mapName.c_str());
m_jsonMap.loadDefault();
}
JsonObject defaults = jsonConfig["defaults"];
Log.notice("config: Loading %d app configurations", defaults.size());
MainLoop::instance()->dispatchSync(InputEvent{InputEvent::ConfigurationChanged, &defaults});
Log.notice("config: Loaded!");
strcpy(m_config.data.loadedProfile, profileName);
} else {
@ -223,8 +216,8 @@ ConfigService::loadProfile(const char* profileName)
Log.notice("config: Configured to use %d pixels", m_jsonMap.physicalPixelCount());
PhysicalCoordinates topLeft = m_jsonMap.virtualToPhysicalCoords({0, 0});
PhysicalCoordinates bottomRight = m_jsonMap.virtualToPhysicalCoords({255, 255});
Log.verbose(" (0,0) -> (%d, %d) -> %d", topLeft.x, topLeft.y, m_jsonMap.physicalCoordsToIndex(topLeft));
Log.verbose(" (255,255) -> (%d, %d) -> %d", bottomRight.x, bottomRight.y, m_jsonMap.physicalCoordsToIndex(bottomRight));
Log.info(" (0,0) -> (%d, %d) -> %d", topLeft.x, topLeft.y, m_jsonMap.physicalCoordsToIndex(topLeft));
Log.info(" (255,255) -> (%d, %d) -> %d", bottomRight.x, bottomRight.y, m_jsonMap.physicalCoordsToIndex(bottomRight));
return true;
}
@ -293,6 +286,16 @@ ConfigService::doCoordMap(Args& args, Print& out)
out.println(buf);
}
void
ConfigService::doRotate(Args& args, Print& out)
{
out.print("Rotation: ");
out.print(m_jsonMap.rotation);
out.print(" ");
m_jsonMap.rotation = atoi(args[1].c_str());
out.println(m_jsonMap.rotation);
}
const std::vector<Command>&
ConfigService::commands() const
{
@ -300,7 +303,8 @@ ConfigService::commands() const
{"save", &ConfigService::doSave},
{"profile", &ConfigService::doSetProfile},
{"maps", &ConfigService::doMapList},
{"coordmap", &ConfigService::doCoordMap}
{"coordmap", &ConfigService::doCoordMap},
{"rotate", &ConfigService::doRotate}
};
return _commands;
}

View File

@ -10,6 +10,7 @@ class Configuration {
const char* get(const char* key, const char* defaultVal) const;
int get(const char* key, int defaultVal) const;
bool get(const char* key, bool defaultVal) const;
JsonVariant get(const char* name) const;
private:
const JsonObject& m_json;
@ -79,4 +80,5 @@ private:
void doSetProfile(Args& args, Print& print);
void doCoordMap(Args& args, Print& print);
void doMapList(Args& args, Print& print);
void doRotate(Args& args, Print& print);
};

View File

@ -11,24 +11,33 @@ JsonCoordinateMapping::physicalPixelCount() const {
int
JsonCoordinateMapping::physicalCoordsToIndex(const PhysicalCoordinates localCoords) const {
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));
return displayMap[localCoords.x].physicalIdx + localCoords.y;
}
VirtualCoordinates
JsonCoordinateMapping::rotate(const VirtualCoordinates coords, uint8_t rot) const
{
if (rot == 0) {
return coords;
} else if (rot == 1) {
return VirtualCoordinates{coords.y, coords.x};
} else if (rot == 2) {
return VirtualCoordinates{255 - coords.y, 255 - coords.x};
} else if (rot == 3) {
return VirtualCoordinates{255 - coords.x, 255 - coords.y};
} else {
idx += std::min((int)displayMap[localCoords.x].length - 1, std::max(0, (int)localCoords.y - displayMap[localCoords.x].y));
return coords;
}
return idx;
}
PhysicalCoordinates
JsonCoordinateMapping::virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const {
const uint8_t spanIdx = scale8(strideCount-1, virtualCoords.x);
const uint8_t spanOffset = scale8(maxStrideSize - 1, virtualCoords.y);
// 0, 0 -> first pixel, first strand
// 0, 255 -> first pixel, last strand
// 255, 255 -> last pixel, last strand
const VirtualCoordinates rotated = rotate(virtualCoords, rotation);
const uint8_t spanIdx = scale8(strideCount-1, rotated.x);
const uint8_t spanOffset = scale8(displayMap[spanIdx].length, rotated.y);
return PhysicalCoordinates{spanIdx, spanOffset};
}
@ -48,23 +57,23 @@ JsonCoordinateMapping::physicalToVirtualCoords(const PhysicalCoordinates localCo
void
JsonCoordinateMapping::load(const JsonArray& strideList)
{
maxStrideSize = 0;
strideCount = strideList.size();
uint16_t idx = 0;
for(int i = 0; i < strideList.size();i++) {
JsonObject strideObj = strideList[i];
int length = strideObj["pixels"];
int x = strideObj["x"];
int y = strideObj["y"];
displayMap[i] = Span{length, x, y};
bool reverse = strideObj["reverse"];
displayMap[i] = Span{length, x, y, reverse, idx};
idx += length;
Log.info("config: Found stride (%d, %d): %d", x, y, length);
maxStrideSize = length > maxStrideSize ? length : maxStrideSize;
}
}
void
JsonCoordinateMapping::loadDefault()
{
displayMap[0] = Span{255, 0, 0};
maxStrideSize = 255;
displayMap[0] = Span{255, 0, 0, false, 0};
strideCount = 1;
}

View File

@ -7,14 +7,18 @@ struct JsonCoordinateMapping : CoordinateMapping {
int length = 0;
int x = 0;
int y = 0;
bool reverse = false;
uint16_t physicalIdx = 0;
Span() {}
Span(int length, int x, int y) : length(length), x(x), y(y) {}
Span(int length, int x, int y, bool rev, uint16_t idx) : length(length), x(x), y(y), reverse(rev), physicalIdx(idx) {}
};
uint8_t rotation = 0;
Span displayMap[32];
int strideCount = 0;
int maxStrideSize = 0;
void load(const JsonArray& strides);
void loadDefault();
@ -23,4 +27,6 @@ struct JsonCoordinateMapping : CoordinateMapping {
PhysicalCoordinates virtualToPhysicalCoords(const VirtualCoordinates virtualCoords) const override;
int physicalCoordsToIndex(const PhysicalCoordinates localCoords) const override;
unsigned int physicalPixelCount() const override;
VirtualCoordinates rotate(const VirtualCoordinates coords, uint8_t rotation) const;
};

View File

@ -29,7 +29,7 @@ LogService::eventValue(const InputEvent& evt)
void
LogService::handleEvent(const InputEvent& evt) {
if (evt.intent != InputEvent::None) {
if (evt.intent != m_lastEvent.intent) {
if (true || evt.intent != m_lastEvent.intent) {
if (m_duplicateEvents > 0) {
Log.trace("Suppressed reporting %d duplicate events.", m_duplicateEvents);
}

View File

@ -15,7 +15,7 @@ void printNewline(Print* logOutput, int logLevel)
}
#endif
int Platform::s_timezone = 0;
int Platform::s_timezone = 1;
Platform::TaskRegistration* Platform::firstTask = NULL;
Platform::TaskRegistration* Platform::lastTask = NULL;

33
src/Scripts.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "Scripts.h"
#include "Static.h"
Scripts::Scripts()
: Task("Scripts")
{
state = Running;
}
void
Scripts::loop()
{
}
void
Scripts::doEval(Args& args, Print& out)
{
String fullCmd((const char*)args);
String fullScript(fullCmd.substring(fullCmd.indexOf(" ") + 1));
}
const std::vector<Command>&
Scripts::commands() const
{
static const std::vector<Command> _commands{
{"eval", &Scripts::doEval}
};
return _commands;
}
STATIC_ALLOC(Scripts);
STATIC_TASK(Scripts);

12
src/Scripts.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#include <Figments.h>
class Scripts : public Task {
public:
Scripts();
void loop() override;
const std::vector<Command>& commands() const;
private:
void doEval(Args& args, Print& out);
};

View File

@ -3,8 +3,9 @@
#include "Static.h"
Sequencer::Sequencer() :
Task("SceneSequencer"),
m_idx(0)
Task("Sequencer"),
m_idx(0),
m_sceneBeforeIdle(-1)
{
state = Task::Running;
}
@ -12,28 +13,32 @@ Sequencer::Sequencer() :
void
Sequencer::Scene::start()
{
for(const char* pattern : patterns) {
Log.verbose("Starting pattern task %s", pattern);
MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern});
for(const auto& pattern : patterns) {
//pattern.getBytes(namebuf, sizeof(namebuf));
Log.info("sequencer: Starting pattern task %s", pattern.c_str());
MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern.c_str()});
}
}
void
Sequencer::Scene::stop()
{
for(const char* pattern : patterns) {
Log.verbose("Stopping pattern task %s", pattern);
MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern});
for(const auto& pattern : patterns) {
Log.info("sequencer: Stopping pattern task %s", pattern.c_str());
MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern.c_str()});
}
}
void
Sequencer::loop() {}
Sequencer::loop()
{
ConfigTaskMixin::loop();
}
const char*
Sequencer::currentSceneName()
{
return m_scenes[m_idx].name;
return m_scenes[m_idx].name.c_str();
}
const std::vector<Sequencer::Scene>
@ -48,42 +53,77 @@ Sequencer::onStart()
}
void
Sequencer::setScenes(std::vector<Scene> &&scenes)
Sequencer::handleEvent(const InputEvent& evt)
{
m_idx = 0;
m_scenes = scenes;
if (!m_scenes.empty()) {
if (evt.intent == InputEvent::ReadyToRoll) {
Log.info("sequencer: Activating startup scene");
if (!changeToScene("Start")) {
Log.info("sequencer: Start scene not found, falling back to first scene");
changeToScene(m_scenes[0].name.c_str());
}
} else if (evt.intent == InputEvent::SetScene && m_scenes[m_idx].name == evt.asString()) {
return;
} else if (evt.intent == InputEvent::SetScene) {
changeToScene(evt.asString());
} else if (evt.intent == InputEvent::IdleStart) {
Log.info("sequencer: Activating idle scene");
m_sceneBeforeIdle = m_idx;
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, "Idle"});
} else if (evt.intent == InputEvent::IdleStop && m_sceneBeforeIdle >= 0) {
Log.info("sequencer: Resuming pre-idle scene");
//changeToScene(m_scenes[m_sceneBeforeIdle].name.c_str());
String sceneName = m_scenes[m_sceneBeforeIdle].name;
m_sceneBeforeIdle = -1;
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, sceneName.c_str()});
}
}
ConfigTaskMixin::handleEvent(evt);
}
void
Sequencer::handleEvent(const InputEvent& evt)
Sequencer::handleConfigChange(const Configuration& cfg)
{
if (evt.intent == InputEvent::ReadyToRoll && !m_scenes.empty()) {
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});
Log.notice("sequencer: Loading scenes");
JsonObject sceneList = cfg.get("scenes");
m_idx = 0;
m_scenes.clear();
for(JsonPair pair : sceneList) {
Log.notice("sequencer: Found scene %s", pair.key().c_str());
std::vector<String> patterns;
for(const char* taskName : pair.value().as<JsonArray>()) {
patterns.push_back(taskName);
}
m_scenes.push_back(Scene{pair.key().c_str(), patterns});
}
if (m_scenes.empty()) {
Log.warning("sequencer: Zero scenes loaded!");
}
}
bool
Sequencer::changeToScene(const char* name)
{
Log.notice("sequencer: Switching scene to %s!", name);
int newIdx = 0;
bool found = false;
for(newIdx = 0; newIdx < m_scenes.size(); newIdx++) {
if (!strcmp(name, m_scenes[newIdx].name.c_str())) {
found = true;
break;
}
} else if (evt.intent == InputEvent::SetScene && evt.asString() == m_scenes[m_idx].name) {
return;
} else if (evt.intent == InputEvent::SetScene) {
Log.notice("Switching scene to %s!", evt.asString());
}
int newIdx = 0;
bool found = false;
for(newIdx = 0; newIdx < m_scenes.size(); newIdx++) {
if (!strcmp(evt.asString(), m_scenes[newIdx].name)) {
found = true;
break;
}
}
if (found) {
m_scenes[m_idx].stop();
m_idx = newIdx;
m_scenes[m_idx].start();
} else {
Log.notice("Scene not found!");
}
if (found) {
m_scenes[m_idx].stop();
m_idx = newIdx;
m_scenes[m_idx].start();
return true;
} else {
Log.notice("sequencer: Scene not found!");
return false;
}
}
@ -92,7 +132,7 @@ Sequencer::doScenes(Args& args, Print& out)
{
out.println("Available scenes: ");
for (auto scene : scenes()) {
out.println(scene.name);
out.println(scene.name.c_str());
}
}
@ -101,8 +141,8 @@ static String s;
void
Sequencer::doScene(Args& args, Print& out)
{
s = args[1];
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, s.c_str()});
String sceneName = args[1];
MainLoop::instance()->dispatch(InputEvent{InputEvent::SetScene, sceneName.c_str()});
}
const std::vector<Command>&

View File

@ -2,14 +2,15 @@
#include <Figment.h>
#include <vector>
#include "Config.h"
class Sequencer: public Task {
class Sequencer: public Task, ConfigTaskMixin {
public:
class Scene {
public:
const char* name;
std::vector<const char*> patterns;
String name;
std::vector<String> patterns;
void start();
void stop();
};
@ -19,7 +20,7 @@ public:
void loop() override;
void onStart() override;
void handleEvent(const InputEvent& evt) override;
void setScenes(std::vector<Scene> &&scenes);
void handleConfigChange(const Configuration& cfg) override;
const char* currentSceneName();
const std::vector<Scene> scenes() const;
@ -27,8 +28,12 @@ public:
private:
int m_idx;
int m_sceneBeforeIdle;
bool m_hasStartupScene;
std::vector<Scene> m_scenes;
bool changeToScene(const char* name);
void doScene(Args& args, Print& out);
void doScenes(Args& args, Print& out);
};

46
src/animations/Cloudy.cpp Normal file
View File

@ -0,0 +1,46 @@
#include "Cloudy.h"
#include "../Static.h"
Cloudy::Cloudy()
: Figment("Cloudy")
{
}
void
Cloudy::loop()
{
m_noiseOffset += 1;
EVERY_N_MILLISECONDS(60) {
m_hue.update(1);
}
}
void
Cloudy::render(Display* dpy) const
{
Surface sfc = Surface(dpy, {0, 0}, {255, 128});
uint8_t noiseY = sin8(m_noiseOffset % 255);
uint8_t noiseX = cos8(m_noiseOffset % 255);
sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y);
uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x);
nblend(pixel, CHSV(m_hue, scale8(saturation, 128), brightness), 255 - surfaceCoords.y);
});
Surface horizon = Surface(dpy, {0, 64}, {255, 128});
horizon.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y);
uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x);
nblend(pixel, CHSV(m_hue + (saturation % 45), std::max((uint8_t)64, (uint8_t)saturation), brightness), surfaceCoords.y);
});
}
void
Cloudy::handleEvent(const InputEvent& evt) {
if (evt.intent == InputEvent::SetColor) {
CHSV next = rgb2hsv_approximate(evt.asRGB());
m_hue.set(next.h);
}
}
STATIC_ALLOC(Cloudy);
STATIC_TASK(Cloudy);

13
src/animations/Cloudy.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include <Figments.h>
class Cloudy: public Figment {
private:
uint16_t m_noiseOffset;
AnimatedNumber m_hue;
public:
Cloudy();
void render(Display* dpy) const override;
void loop() override;
void handleEvent(const InputEvent& evt) override;
};

View File

@ -8,11 +8,13 @@ RainAnimation::RainAnimation() : Figment("Rain")
void
RainAnimation::render(Display* dpy) const
{
Surface sfc = Surface(dpy, {0, 0}, {255, 255});
Surface ground = Surface(dpy, {0, 64}, {255, 255});
uint8_t noiseY = sin8(m_noiseOffset % 255);
uint8_t noiseX = cos8(m_noiseOffset % 255);
sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
pixel = CHSV(m_hue, inoise8(noiseX + coords.x, coords.y), inoise8(m_noiseOffset + coords.x, noiseY + coords.y));
ground.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y);
uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x);
nblend(pixel, CHSV(m_hue, saturation, brightness), 255 - surfaceCoords.y);
});
m_drops.render(dpy);
}
@ -20,7 +22,7 @@ RainAnimation::render(Display* dpy) const
void
RainAnimation::loop()
{
EVERY_N_MILLISECONDS(250) {
EVERY_N_MILLISECONDS(120) {
m_drops.update();
}
EVERY_N_MILLISECONDS(60) {

View File

@ -4,7 +4,7 @@
class RainAnimation: public Figment {
private:
struct Raindrop {
int size = 20;
int size = 10;
int x = random(255);
int y = random(255);
CHSV fg{180, 255, 255};

View File

@ -47,22 +47,20 @@ void SolidAnimation::loop() {
});
m_blobs.update();
}
m_noiseOffset += 1;
}
void SolidAnimation::render(Display* dpy) const {
CRGB color(m_red.value(), m_green.value(), m_blue.value());
CRGB scaledPrev = m_prevColor;
scaledPrev = color.nscale8(30);
uint8_t frame = ease8InOutApprox(m_changePct);
if (F_LIKELY(frame == 255)) {
Surface(dpy, {0, 0}, {255, 255}) = color.nscale8(10);
} else {
uint8_t cutoff = (frame / 2);
uint8_t rotation = m_horizontal ? 0 : 128;
Surface(dpy, {0, 0}, {128 - cutoff, 255}, rotation) = scaledPrev;
Surface(dpy, {128 - cutoff, 0}, {128 + cutoff, 255}, rotation) = scaledPrev;
Surface(dpy, {128 + cutoff, 0}, {255, 255}, rotation) = scaledPrev;
}
Surface sfc = Surface(dpy, {0, 0}, {255, 255});
uint8_t noiseY = sin8(m_noiseOffset % 255);
uint8_t noiseX = cos8(m_noiseOffset % 255);
CHSV color(rgb2hsv_approximate(CRGB(m_red, m_green, m_blue)));
sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
uint8_t brightness = inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y);
uint8_t saturation = inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x);
nblend(pixel, CHSV(color.h, std::max((uint8_t)128, saturation), scale8(color.v, brightness)), 128);
});
m_blobs.render(dpy);
}
STATIC_ALLOC(SolidAnimation);

View File

@ -5,6 +5,7 @@
class SolidAnimation: public Figment {
private:
AnimatedNumber m_red, m_green, m_blue, m_changePct;
uint16_t m_noiseOffset;
CRGB m_curColor;
CRGB m_prevColor;
static constexpr int blobCount = 20;

View File

@ -5,27 +5,106 @@
void
TestAnimation::loop()
{
m_x += 1;
m_y += 1;
m_x += 1;
m_y += 1;
m_hue += 1;
}
void
TestAnimation::handleEvent(const InputEvent& evt)
{
if (evt.intent == InputEvent::SetColor) {
m_color = evt.asRGB();
}
}
void
TestAnimation::render(Display* dpy) const
{
for(unsigned int i = 0; i < dpy->pixelCount(); i++) {
dpy->pixelAt(i) = CRGB{255, 255, 255};
CRGB drawColor = blend(m_color, CHSV(m_hue, 255, 255), 128);
if (m_mode == Raw) {
for(unsigned int i = 0; i < dpy->pixelCount(); i++) {
dpy->pixelAt(i) = drawColor;
}
} else if (m_mode == Top) {
Surface{dpy, {0, 0}, {255, 0}} = drawColor;
} else if (m_mode == Left) {
Surface{dpy, {0, 0}, {0, 255}} = drawColor;
} else if (m_mode == Right) {
Surface{dpy, {255, 0}, {255, 255}} = drawColor;
} else if (m_mode == Bottom) {
Surface{dpy, {0, 255}, {255, 255}} = drawColor;
} else if (m_mode == Outline) {
// Draw red line on top row
Surface{dpy, {0, 0}, {255, 0}} = CRGB{255, 0, 0};
// Green line on first column
Surface{dpy, {0, 0}, {0, 255}} = CRGB{0, 255, 0};
} else if (m_mode == White) {
Surface{dpy, {0, 0}, {255, 255}} = CRGB{255, 255, 255};
} else if (m_mode == Red) {
Surface{dpy, {0, 0}, {255, 255}} = CRGB{255, 0, 0};
} else if (m_mode == Green) {
Surface{dpy, {0, 0}, {255, 255}} = CRGB{0, 255, 0};
} else if (m_mode == Blue) {
Surface{dpy, {0, 0}, {255, 255}} = CRGB{0, 0, 255};
} else if (m_mode == HSV) {
Surface{dpy, {0, 0}, {255, 255}}.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates&, const VirtualCoordinates& surfaceCoords) {
pixel = CHSV(coords.x, coords.y, 255);
});
} else if (m_mode == RGB) {
Surface{dpy, {0, 0}, {255, 255}}.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates&, const VirtualCoordinates& surfaceCoords) {
pixel = CRGB(coords.x, coords.y, (coords.x/2) + (coords.y/2));
});
}
return;
// Blank the canvas to white
Surface{dpy, {0, 0}, {255, 255}} = CRGB{255, 255, 255};
// Draw red line on top row
Surface{dpy, {0, 0}, {255, 0}} = CRGB{255, 0, 0};
// Green line on first column
Surface{dpy, {0, 0}, {0, 255}} = CRGB{0, 255, 0};
}
//Surface{dpy, {m_x, 0}, {m_x, 255}} = CRGB{255, 0, 0};
///Surface{dpy, {0, m_y}, {255, m_y}} = CRGB{255, 0, 0};
//dpy->pixelAt(VirtualCoordinates{m_x, m_y}) = CRGB{255, 0, 255};
void
TestAnimation::doMode(Args& args, Print& out)
{
String s = args[1];
if (s == "") {
out.println("Available modes: raw outline white red green blue hsv rgb none top left right bottom");
out.print("Current mode: ");
out.println((int)m_mode);
} else if (s == "raw") {
m_mode = Raw;
} else if (s == "outline") {
m_mode = Outline;
} else if (s == "white") {
m_mode = White;
} else if (s == "red") {
m_mode = Red;
} else if (s == "green") {
m_mode = Green;
} else if (s == "blue") {
m_mode = Blue;
} else if (s == "hsv") {
m_mode = HSV;
} else if (s == "rgb") {
m_mode = RGB;
} else if (s == "top") {
m_mode = Top;
} else if (s == "left") {
m_mode = Left;
} else if (s == "right") {
m_mode = Right;
} else if (s == "bottom") {
m_mode = Bottom;
} else if (s == "none") {
m_mode = None;
} else {
out.println("Unknown test mode");
}
}
const std::vector<Command>&
TestAnimation::commands() const
{
static const std::vector<Command> _commands{
{"test", &TestAnimation::doMode}
};
return _commands;
}
STATIC_ALLOC(TestAnimation);

View File

@ -5,8 +5,31 @@ public:
TestAnimation() : Figment("Test") {}
void loop() override;
void render(Display* dpy) const override;
void handleEvent(const InputEvent& evt) override;
const std::vector<Command>& commands() const override;
private:
uint8_t m_x;
uint8_t m_y;
CRGB m_color;
uint8_t m_hue;
enum Mode {
None,
Raw,
Outline,
White,
Red,
Green,
Blue,
HSV,
RGB,
Top,
Left,
Right,
Bottom
};
Mode m_mode = None;
void doMode(Args&, Print&);
};

View File

@ -0,0 +1,26 @@
#include <Figments.h>
#include "../Static.h"
class Thinking: public Figment {
public:
Thinking() : Figment("Thinking") {}
void render(Display* dpy) const override {
Surface sfc = Surface(dpy, {0, 0}, {255, 255});
uint8_t noiseY = sin8(m_noiseOffset % 255);
uint8_t noiseX = cos8(m_noiseOffset % 255);
sfc.paintShader([=](CRGB& pixel, const VirtualCoordinates& coords, const PhysicalCoordinates, const VirtualCoordinates& surfaceCoords) {
pixel = CHSV(inoise8(noiseX + surfaceCoords.x, noiseY + surfaceCoords.y), 255, inoise8(noiseY + surfaceCoords.y, noiseX + surfaceCoords.x));
});
}
void loop() override {
EVERY_N_MILLISECONDS(15) {
m_noiseOffset += 1;
}
}
private:
int m_noiseOffset;
};
STATIC_ALLOC(Thinking);
STATIC_TASK(Thinking);

View File

@ -131,9 +131,16 @@ SerialInput::doHelp(Args& args, Print& out)
out.println("Available commands:");
auto sched = MainLoop::instance()->scheduler;
for(auto task : sched.tasks) {
for(auto &command : task->commands()) {
out.print(command.name);
out.print(" ");
const auto taskCommands = task->commands();
if (!taskCommands.empty()) {
out.print(task->name);
out.println(":");
out.print("\t");
for(auto &command : task->commands()) {
out.print(command.name);
out.print(" ");
}
out.println();
}
}
out.println();

147
src/inputs/Weather.cpp Normal file
View File

@ -0,0 +1,147 @@
#include "Weather.h"
#include "../Static.h"
Weather::Weather()
: BufferedInputSource("Weather")
{
m_parser.setListener(this);
}
void
Weather::handleEvent(const InputEvent& evt)
{
BufferedInputSource::handleEvent(evt);
OnlineTaskMixin::handleEvent(evt);
if (evt.intent == InputEvent::IdleStart) {
doWeatherEvent();
}
}
void
Weather::loop()
{
BufferedInputSource::loop();
OnlineTaskMixin::loop();
EVERY_N_SECONDS(3600) {
update();
}
if (m_fetching) {
parse();
}
}
void
Weather::update()
{
if (!m_fetching) {
Log.info("Connecting to weather service");
m_parser.reset();
const String host = "api.openweathermap.org";
const uint8_t port = 80;
#ifdef ESP32
m_fetching = m_client.connect(host.c_str(), port);
#else
m_fetching = m_client.connect(host, port);
#endif
if (m_fetching) {
Log.info("Ok!");
m_foundBody = false;
m_client.print(
"GET /data/2.5/weather?id=2950159&appid=f7aea43b54bcd27f542195a809c1463f&units=metric&lang=en HTTP/1.1\r\n"
"Host: api.openweathermap.org\r\n"
"Connection: close\r\n\r\n"
);
} else {
Log.info("No :(");
}
}
}
void
Weather::parse()
{
if (m_client.available()) {
char c = m_client.read();
if (c == '{') {
m_foundBody = true;
}
if (m_foundBody) {
m_parser.parse(c);
}
} else if (!m_client.connected()) {
Log.info("Disconnected");
m_client.stop();
m_fetching = false;
doWeatherEvent();
}
}
void
Weather::key(String key)
{
m_curKey = key;
}
void
Weather::doWeatherEvent()
{
Log.info("Dispatching weather event for %d", m_weatherId);
if (m_weatherId >= 200 && m_weatherId < 300 ) {
setEvent(InputEvent::SetScene, "Thunderstorm");
setEvent(InputEvent::SetColor, CRGB(255, 187, 0));
} else if (m_weatherId >= 300 && m_weatherId < 400) {
setEvent(InputEvent::SetScene, "Drizzle");
setEvent(InputEvent::SetColor, CRGB(93, 183, 201));
} else if (m_weatherId >= 500 && m_weatherId < 600) {
setEvent(InputEvent::SetScene, "Rain");
setEvent(InputEvent::SetColor, CRGB(64, 64, 255));
} else if (m_weatherId >= 600 && m_weatherId < 700) {
setEvent(InputEvent::SetScene, "Snow");
setEvent(InputEvent::SetColor, CRGB(255, 255, 255));
} else if (m_weatherId == 701) {
setEvent(InputEvent::SetScene, "Mist");
setEvent(InputEvent::SetColor, CRGB(77, 255, 124));
} else if (m_weatherId == 800) {
setEvent(InputEvent::SetScene, "Clear");
setEvent(InputEvent::SetColor, CRGB(255, 249, 79));
} else if (m_weatherId >= 801 && m_weatherId <= 804) {
setEvent(InputEvent::SetScene, "Cloudy");
setEvent(InputEvent::SetColor, CRGB(102, 110, 110));
} else {
setEvent(InputEvent::SetScene, "UnknownWeather");
setEvent(InputEvent::SetColor, CRGB(255, 0, 0));
}
}
void
Weather::value(String value)
{
if (m_curField == "weather") {
if (m_curKey == "id") {
m_weatherId = atoi(value.c_str());
} else if (m_curKey == "main") {
Log.info("WEATHER ENCOUNTERED: %s", value.c_str());
}
}
}
void
Weather::startObject()
{
m_curField = m_curKey;
}
void
Weather::endObject()
{
m_curField = "";
}
void Weather::endArray() {}
void Weather::startArray() {}
void Weather::endDocument() {}
void Weather::whitespace(char c) {}
void Weather::startDocument() {}
STATIC_ALLOC(Weather);
STATIC_TASK(Weather);

51
src/inputs/Weather.h Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include <Figments.h>
#include <JsonListener.h>
#include <JsonStreamingParser.h>
#include <WiFiClient.h>
class Weather : public BufferedInputSource, OnlineTaskMixin, JsonListener {
public:
Weather();
void handleEvent(const InputEvent& evt) override;
void loop() override;
void onOnline() override {
update();
}
void onStart() override {
update();
}
void loopOnline() override {
}
void update();
void parse();
virtual void whitespace(char c);
virtual void startDocument();
virtual void key(String key);
virtual void value(String value);
virtual void endArray();
virtual void endObject();
virtual void endDocument();
virtual void startArray();
virtual void startObject();
private:
JsonStreamingParser m_parser;
WiFiClient m_client;
bool m_fetching = false;
String m_curKey;
String m_curField;
bool m_foundBody = false;
int m_weatherId = 0;
void doWeatherEvent();
};

View File

@ -44,7 +44,7 @@ ColorSequenceInput<9> idleCycle{{
CRGB(0, 255, 255), // Cyan
}, "IdleColors"};
REGISTER_TASK(idleCycle);
//REGISTER_TASK(idleCycle);
ColorSequenceInput<7> rainbowCycle{{
CRGB(255, 0, 0), // Red
@ -54,7 +54,7 @@ ColorSequenceInput<7> rainbowCycle{{
CRGB(128, 0, 128), // Purple
}, "Rainbow"};
REGISTER_TASK(rainbowCycle);
//REGISTER_TASK(rainbowCycle);
MainLoop* runner = &SafeMode::safeModeApp;

View File

@ -9,6 +9,8 @@ __NOINIT_ATTR static uint8_t s_rebootCount;
__NOINIT_ATTR static uint16_t s_forceSafeMode;
#define SAFE_MODE_MAGIC 6942
__NOINIT_ATTR static struct tm s_lastGoodTime;
WiFiUDP wifiUdp;
NTPClient timeClient(wifiUdp, "pool.ntp.org", 0);
@ -44,8 +46,12 @@ template<>
bool
PlatformImpl<HAL_ESP8266>::getLocalTime(struct tm* timedata, int timezone)
{
timedata->tm_hour = (timeClient.getHours() + timezone) % 23;
timedata->tm_min = timeClient.getMinutes();
if (timeClient.isTimeSet()) {
s_lastGoodTime.tm_min = (timeClient.getMinutes() + timezone) % 23;
s_lastGoodTime.tm_hour = timeClient.getHours();
s_lastGoodTime.tm_sec = timeClient.getSeconds();
}
memcpy(timedata, &s_lastGoodTime, sizeof(s_lastGoodTime));
return true;
}
@ -54,6 +60,9 @@ void
PlatformImpl<HAL_ESP8266>::startNTP()
{
timeClient.begin();
if (s_lastGoodTime.tm_hour >= 24 || s_lastGoodTime.tm_min >= 60 || s_lastGoodTime.tm_sec >= 60) {
memset(&s_lastGoodTime, 0, sizeof(s_lastGoodTime));
}
}
template<>

View File

@ -64,9 +64,7 @@ public:
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);
nblend(pixel, CRGB(blobColor), 80);
});
}
};