Browse Source

Refactor presets to use JSON definitions

- New definitions can be delivered via MQTT
 - Includes extensible pattern class for new preset types
S3
jrhoffa 3 years ago
parent
commit
2fec06b9f8
12 changed files with 327 additions and 208 deletions
  1. +32
    -0
      main/colors.json
  2. +26
    -8
      main/device.cpp
  3. +1
    -0
      main/device.hpp
  4. +17
    -9
      main/leds.cpp
  5. +49
    -6
      main/leds.hpp
  6. +5
    -45
      main/main.cpp
  7. +1
    -0
      main/mqtt.cpp
  8. +124
    -102
      main/presets.cpp
  9. +5
    -38
      main/presets.hpp
  10. +17
    -0
      main/presets.json
  11. +44
    -0
      main/utils.cpp
  12. +6
    -0
      main/utils.hpp

+ 32
- 0
main/colors.json View File

@@ -0,0 +1,32 @@
{
"black": [ 0, 0, 0],
"white": [255, 255, 255],

"red": [255, 0, 0],
"orange": [ 78, 25, 0],
"yellow": [255, 160, 0],
"green": [ 0, 255, 0],
"blue": [ 0, 0, 255],
"purple": [ 38, 10, 42],

"dark_red": [ 32, 0, 0],
"dark_orange": [ 20, 6, 0],
"dark_yellow": [ 64, 40, 0],
"dark_green": [ 0, 32, 0],
"dark_blue": [ 0, 0, 32],
"dark_purple": [ 8, 3, 10],

"pale_red": [255, 64, 64],
"pale_orange": [ 78, 25, 10],
"pale_yellow": [255, 160, 10],
"pale_green": [ 64, 255, 25],
"pale_blue": [ 70, 70, 255],
"pale_purple": [ 42, 20, 42],

"pink": "pale_red",
"peach": "pale_orange",
"brown": [ 6, 2, 0],
"chartreuse": [ 35, 45, 5],
"teal": [ 0, 85, 35],
"magenta": [255, 0, 255]
}

+ 26
- 8
main/device.cpp View File

@@ -3,12 +3,14 @@ static const char *TAG = "device";

#include "device.hpp"
#include "presets.hpp"
#include "utils.hpp"


Device::Device(std::string _id) :
id(_id),
state_topic("light/" + id + "/state"),
cmd_topic("light/" + id + "/cmd"),
data_topic("light/" + id + "/data/+"),
ready(false), should_publish(false),
mutex(xSemaphoreCreateMutex()),
sem(xSemaphoreCreateBinary()),
@@ -45,7 +47,11 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) {
if (esp_mqtt_client_subscribe(client, cmd_topic.c_str(), 0) < 0) {
ESP_LOGE(TAG, "Failed to subscribe to %s", cmd_topic.c_str());
}
if (esp_mqtt_client_subscribe(client, data_topic.c_str(), 0) < 0) {
ESP_LOGE(TAG, "Failed to subscribe to %s", data_topic.c_str());
}

// TODO: Re-announce when presets change
cJSON *json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "unique_id", id.c_str());
cJSON *device = cJSON_AddObjectToObject(json, "device");
@@ -57,8 +63,8 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) {
cJSON_AddStringToObject(json, "schema", "json");
cJSON_AddBoolToObject(json, "effect", true);
cJSON *effects = cJSON_AddArrayToObject(json, "effect_list");
for (const auto &[k, _]: Presets::map)
cJSON_AddItemToArray(effects, cJSON_CreateString(k.c_str()));
for (auto iter = Presets::map_begin(); iter != Presets::map_end(); ++iter)
cJSON_AddItemToArray(effects, cJSON_CreateString(iter->first.c_str()));

char *config = cJSON_PrintUnformatted(json);

@@ -81,13 +87,25 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) {
}

void Device::on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t event) {
std::string topic(event->topic, event->topic_len);
ESP_LOGI(TAG, "Received command via MQTT");
cJSON *json = cJSON_ParseWithLength(event->data, event->data_len);
if (json) {
set_json_config(json);
cJSON_Delete(json);
} else {
ESP_LOGE(TAG, "Invalid JSON data");
if (topic == cmd_topic) {
cJSON *json = cJSON_ParseWithLength(event->data, event->data_len);
if (json) {
set_json_config(json);
cJSON_Delete(json);
} else {
ESP_LOGE(TAG, "Invalid JSON data");
}

} else /* data topic */ {
size_t slash = topic.rfind('/');
if (slash != std::string::npos) {
write_file(
("/spiffs/" + topic.substr(slash + 1) + ".json").c_str(),
event->data, event->data_len
);
}
}
xSemaphoreGive(sem);
}


+ 1
- 0
main/device.hpp View File

@@ -35,6 +35,7 @@ private:

std::string state_topic;
std::string cmd_topic;
std::string data_topic;

bool ready, should_publish;



+ 17
- 9
main/leds.cpp View File

@@ -30,11 +30,12 @@ Color Gradient::at(Fixed x) const {
return color;
}

void GradientPattern::step(Color pixels[], int len, Pattern::State *_state) const {
GradientPattern::State *state = (GradientPattern::State*)_state;

void Pattern::step(Color pixels[], int len, int64_t &last_us, Fixed &offset) const {
int64_t now_us = time_us();
int64_t duration_us = last_us == 0 ? 0 : now_us - last_us;
last_us = now_us;
int64_t duration_us = state->last_us == 0 ? 0 : now_us - state->last_us;
state->last_us = now_us;
//ESP_LOGI(TAG, "duration %d", duration_us);

/*
@@ -49,13 +50,13 @@ void Pattern::step(Color pixels[], int len, int64_t &last_us, Fixed &offset) con


int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000);
if (reverse) offset += offset_delta; else offset -= offset_delta;
if (reverse) state->offset += offset_delta; else state->offset -= offset_delta;
Fixed off = march ?
(((int)offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length :
offset;
(((int)state->offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length :
state->offset;
//ESP_LOGI(TAG, "cycle time %d, delta %d, offset %d, off %d", cycle_time_ms, offset_delta, offset, off);
for (int i = 0; i < len; ++i) {
pixels[i] = gradient.at(off + ((i << shift) / cycle_length));
pixels[i] = gradient->at(off + ((i << shift) / cycle_length));
}
}

@@ -63,18 +64,25 @@ void Pattern::step(Color pixels[], int len, int64_t &last_us, Fixed &offset) con
LEDStrip::LEDStrip(int _length) :
length(_length), pattern(NULL),
pixels(new Color[_length]),
last_us(0), offset(0)
state(NULL)
{}

void LEDStrip::update_state() {
if (state) delete state;
if (pattern) state = pattern->start(length);
else state = NULL;
}

void LEDStrip::setLength(int _length) {
length_changing(_length);
delete[] pixels;
length = _length;
pixels = new Color[_length];
update_state();
}

void LEDStrip::step() {
if (pattern) pattern->step(pixels, length, last_us, offset);
if (pattern) pattern->step(pixels, length, state);
}

static std::string termcolor(Color c) {


+ 49
- 6
main/leds.hpp View File

@@ -16,18 +16,60 @@ constexpr int shift = 16;
struct Gradient {
Color at(Fixed x) const;

static Gradient* make(int n) {
Gradient *gradient = (Gradient*)malloc(
sizeof(Gradient) + n * sizeof(*colors)
);
gradient->n_colors = n;
return gradient;
}

unsigned int n_colors;
Color colors[];
};

struct Pattern {
void step(Color[], int, int64_t &last_us, Fixed &offset) const;
class Pattern {
public:
virtual ~Pattern() {};

struct State {};

virtual State* start(int) const = 0;

virtual void step(Color[], int, State*) const = 0;
};

class GradientPattern : public Pattern {
public:
GradientPattern(
Gradient *_gradient,
int _cycle_length=20, int _cycle_time_ms=-1,
bool _reverse=false, bool _march=false
) : cycle_length(_cycle_length),
cycle_time_ms( _cycle_time_ms < 0 ?
_cycle_length * 500 : _cycle_time_ms
),
reverse(_reverse), march(_march), gradient(_gradient)
{}

~GradientPattern() { free(gradient); }

State* start(int) const { return new State(); }

void step(Color[], int, State*) const;

private:
struct State : Pattern::State {
State() : last_us(0), offset(0) {}
int64_t last_us;
Fixed offset;
};

int cycle_length;
int cycle_time_ms;
bool reverse;
bool march;
Gradient gradient;
Gradient *gradient;
};

class LEDStrip {
@@ -39,7 +81,7 @@ public:
virtual void show() = 0;
virtual void length_changing(int) {};

void setPattern(const Pattern *_pattern) { pattern = _pattern; }
void setPattern(const Pattern *_pattern) { pattern = _pattern; update_state(); }
const Pattern* getPattern() const { return pattern; }

void setLength(int length);
@@ -51,8 +93,9 @@ protected:
Color *pixels;

private:
int64_t last_us;
Fixed offset;
void update_state();

Pattern::State *state;
};

class TerminalLEDs : public LEDStrip {


+ 5
- 45
main/main.cpp View File

@@ -56,7 +56,7 @@ static void start_filesystem() {
ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf));
}

/*
#include <string>
#include <dirent.h>
#include <sys/stat.h>
@@ -84,49 +84,6 @@ static void log_dir(const std::string &path) {

closedir(dir);
}
*/

static std::string read_file(const char *path) {
std::string data;
FILE *f = fopen(path, "r");
if (!f) {
ESP_LOGE(TAG, "Failed to read %s", path);
return data;
}
constexpr size_t szbuf = 4096;
char *buf = (char*)malloc(szbuf);
size_t nread;
while ((nread = fread(buf, 1, szbuf - 1, f)) > 0) {
buf[nread] = '\0';
data += buf;
}
fclose(f);
free(buf);
return data;
}

static void write_file(const char *path, const std::string &data) {
FILE *f = fopen(path, "w");
if (!f) {
ESP_LOGE(TAG, "Failed to open %s", path);
return;
}
const char *ptr = data.c_str();
size_t remain = data.length();
size_t nwritten = fwrite(ptr, 1, remain, f);
if (nwritten != remain) {
ESP_LOGE(TAG, "Failed to write to %s: %d/%d", path, nwritten, remain);
}
fclose(f);
}


std::string gen_config(bool is_on, const std::string &effect) {
return std::string() + "{ "
"\"state\": \"" + (is_on ? "ON" : "OFF") + "\", "
"\"effect\": \"" + effect + "\""
" }";
}


constexpr char config_path[] = "/spiffs/blinky.json";
@@ -159,7 +116,7 @@ extern "C" void app_main(void) {
// Dummy Filesystem
start_filesystem();
// TODO: Scrape this out
//log_dir("/spiffs");
log_dir("/spiffs");

Device device("blinky-jr");

@@ -173,6 +130,8 @@ extern "C" void app_main(void) {
LEDStrip *LEDs = new SPI_LEDs(39); //new TerminalLEDs();

while (true) {
Presets::reload();

ESP_LOGI(TAG, "Configuring LEDs");

device.lock();
@@ -188,6 +147,7 @@ extern "C" void app_main(void) {

const Pattern *pattern = Presets::find(effect);
if (pattern) {
ESP_LOGI(TAG, "Setting pattern '%s'", effect.c_str());
LEDs->setPattern(pattern);
} else {
ESP_LOGW(TAG, "Could not find pattern '%s'", effect.c_str());


+ 1
- 0
main/mqtt.cpp View File

@@ -88,6 +88,7 @@ esp_mqtt_client_handle_t start_mqtt_client(
.cert_pem = (const char *)broker_pem_start,
.username = CONFIG_BROKER_USER,
.password = CONFIG_BROKER_PASSWORD,
.buffer_size = 4096,
};

esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);


+ 124
- 102
main/presets.cpp View File

@@ -1,118 +1,140 @@
static const char *TAG = "presets";
#include <esp_log.h>

#include <cJSON.h>

#include "presets.hpp"
#include "utils.hpp"


namespace Gradients {
namespace Presets {

#define GRADIENT(name, len, ...) \
constexpr Gradient name = { \
.n_colors = len, \
.colors = __VA_ARGS__, \
}
/* Preset Map */

GRADIENT(rainbow, 6, {
Colors::red,
Colors::orange,
Colors::yellow,
Colors::green,
Colors::blue,
Colors::purple,
});

GRADIENT(dark_rainbow, 6, {
Colors::dark_red,
Colors::dark_orange,
Colors::dark_yellow,
Colors::dark_green,
Colors::dark_blue,
Colors::dark_purple,
});

GRADIENT(pale_rainbow, 6, {
Colors::pale_red,
Colors::pale_orange,
Colors::pale_yellow,
Colors::pale_green,
Colors::pale_blue,
Colors::pale_purple,
});

GRADIENT(peacock, 4, {
Colors::teal,
Colors::black,
Colors::purple,
Colors::blue,
});

// Guaranteed to be random. https://xkcd.com/221/
GRADIENT(random, 20, {
{223, 241, 107},
Colors::black,
{34, 170, 82},
Colors::black,
{98, 222, 224},
Colors::black,
{230, 114, 8},
Colors::black,
{226, 215, 213},
Colors::black,
{94, 187, 179},
Colors::black,
{76, 185, 214},
Colors::black,
{40, 115, 111},
Colors::black,
{230, 234, 120},
Colors::black,
{157, 128, 68},
Colors::black,
});


} // Gradients


namespace Patterns {

#define PATTERN(x) \
static const Pattern x = { \
.cycle_length = 20, \
.cycle_time_ms = 10000, \
.reverse = false, \
.march = false, \
.gradient = Gradients::x, \
}
std::map <std::string, const Pattern*> map;

const Pattern* find(const std::string &name) {
auto e = map.find(name);
return e == map.end() ? NULL : e->second;
}

std::map<std::string, const Pattern*>::const_iterator map_begin() {
return map.begin();
}

PATTERN(rainbow);
PATTERN(dark_rainbow);
PATTERN(pale_rainbow);
PATTERN(peacock);
std::map<std::string, const Pattern*>::const_iterator map_end() {
return map.end();
}

static const Pattern random = {
.cycle_length = 20,
.cycle_time_ms = 10000,
.reverse = false,
.march = true,
.gradient = Gradients::random
};
static inline cJSON* load_json(const char *path) {
return cJSON_Parse(read_file(path).c_str());
}

} // Patterns

/* Preset Loader */

namespace Presets {
static Color json_raw_color(cJSON *json) {
if (json->type != cJSON_Array) {
ESP_LOGE(TAG, "Not an array: %s", json->string);
return {0, 0, 0};
}
int count = cJSON_GetArraySize(json);
if (count != 3) {
ESP_LOGE(TAG, "Wrong size for %s: %d", json->string, count);
return {0, 0, 0};
}
// TODO: Number checks for array elements?
return Color{
(uint8_t)cJSON_GetArrayItem(json, 0)->valueint,
(uint8_t)cJSON_GetArrayItem(json, 1)->valueint,
(uint8_t)cJSON_GetArrayItem(json, 2)->valueint
};
}

#define PRESET(x) {#x, &Patterns::x}
static Color json_color(cJSON *json, const std::map <std::string, Color> &colors) {
if (json->type == cJSON_Array) return json_raw_color(json);
if (json->type != cJSON_String) {
ESP_LOGE(TAG, "Not a string: %s", json->string);
return {0, 0, 0};
}
auto iter = colors.find(json->valuestring);
if (iter == colors.end()) {
ESP_LOGE(TAG, "Not a color: %s", json->valuestring);
return {0, 0, 0};
}
return iter->second;
}

const std::map <std::string, const Pattern*> map = {
PRESET(rainbow),
PRESET(dark_rainbow),
PRESET(pale_rainbow),
PRESET(peacock),
PRESET(random),
};
static Pattern* json_pattern(cJSON *json, const std::map <std::string, Color> &colors) {
if (json->type != cJSON_Object) {
ESP_LOGE(TAG, "Not an object: %s", json->string);
return NULL;
}

const Pattern* find(const std::string &name) {
auto e = map.find(name);
return e == map.end() ? NULL : e->second;
cJSON *pattern = json->child;
std::string type(pattern->string);

if (type == "gradient") {
if (pattern->type != cJSON_Object) {
ESP_LOGE(TAG, "Not an object: %s", pattern->type);
return NULL;
}
cJSON *jcolors = cJSON_GetObjectItem(pattern, "colors");
if (!jcolors) {
ESP_LOGE(TAG, "No colors for %s", json->string);
return NULL;
}
if (jcolors->type != cJSON_Array) {
ESP_LOGE(TAG, "Not an array: %s", jcolors->string);
return NULL;
}
Gradient *gradient = Gradient::make(cJSON_GetArraySize(jcolors));
int i = 0;
cJSON *jcolor;
cJSON_ArrayForEach(jcolor, jcolors) {
Color color = json_color(jcolor, colors);
gradient->colors[i++] = color;
}
return new GradientPattern(gradient);

} else {
ESP_LOGE(TAG, "Unknown pattern type: %s", type.c_str());
}

return NULL;
}

void reload() {
std::map <std::string, Color> colors;
cJSON *jcolors = load_json("/spiffs/colors.json");
if (!jcolors) {
ESP_LOGW(TAG, "No preset colors!");
} else {
cJSON *jcolor;
cJSON_ArrayForEach(jcolor, jcolors) {
Color color = json_color(jcolor, colors);
colors[jcolor->string] = color;
}
cJSON_Delete(jcolors);
}

for (const auto &k_v : map) delete k_v.second;
map.clear();

cJSON *jpresets = load_json("/spiffs/presets.json");
if (!jpresets) {
ESP_LOGE(TAG, "No preset patterns!");
} else {
cJSON *jpreset;
cJSON_ArrayForEach(jpreset, jpresets) {
Pattern *pattern = json_pattern(jpreset, colors);
if (pattern) {
ESP_LOGI(TAG, "Creating preset: %s", jpreset->string);
map[jpreset->string] = pattern;
}
}
cJSON_Delete(jpresets);
}
}

} // Presets

+ 5
- 38
main/presets.hpp View File

@@ -7,44 +7,11 @@

namespace Presets {

const struct Pattern* find(const std::string&);
const Pattern* find(const std::string&);

extern const std::map <std::string, const Pattern*> map;
std::map<std::string, const Pattern*>::const_iterator map_begin();
std::map<std::string, const Pattern*>::const_iterator map_end();

} // Presets


namespace Colors {

constexpr Color red = {255, 0, 0};
constexpr Color orange = {78, 25, 0};
constexpr Color yellow = {255, 160, 0};
constexpr Color green = {0, 255, 0};
constexpr Color blue = {0, 0, 255};
constexpr Color purple = {38, 10, 42};

constexpr Color dark_red = {32, 0, 0};
constexpr Color dark_orange = {20, 6, 0};
constexpr Color dark_yellow = {64, 40, 0};
constexpr Color dark_green = {0, 32, 0};
constexpr Color dark_blue = {0, 0, 32};
constexpr Color dark_purple = {8, 3, 10};
void reload();

constexpr Color pale_red = {255, 64, 64};
constexpr Color pale_orange = {78, 25, 10};
constexpr Color pale_yellow = {255, 160, 10};
constexpr Color pale_green = {64, 255, 25};
constexpr Color pale_blue = {70, 70, 255};
constexpr Color pale_purple = {42, 20, 42};

constexpr Color black = {0, 0, 0};
constexpr Color white = {255, 255, 255};

constexpr Color pink = pale_red;
constexpr Color peach = pale_orange;
// TODO: chartreuse
constexpr Color brown = {6, 2, 0};
constexpr Color teal = {0, 85, 35};
constexpr Color magenta = {255, 0, 255};

} // Colors
} // Presets

+ 17
- 0
main/presets.json View File

@@ -0,0 +1,17 @@
{
"rainbow": {"gradient": {"colors": ["red", "orange", "yellow", "green", "blue", "purple"]}},
"dark_rainbow": {"gradient": {"colors": ["dark_red", "dark_orange", "dark_yellow", "dark_green", "dark_blue", "dark_purple"]}},
"pale_rainbow": {"gradient": {"colors": ["pale_red", "pale_orange", "pale_yellow", "pale_green", "pale_blue", "pale_purple"]}},

"peacock": {"gradient": {"colors": ["purple", "blue", "teal", "black"]}},
"pusheen": {"gradient": {"colors": ["teal", "pink"]}},

"love": {"gradient": {"colors": ["pink", "black", "red", "black"]}},
"irish": {"gradient": {"colors": ["dark_green", "black", "green", "black"]}},
"bunny": {"gradient": {"colors": ["pink", "pale_purple", "black", "pale_yellow", "pale_green", "black"]}},
"patriot": {"gradient": {"colors": ["red", "black", "black", "white", "black", "black", "blue", "black", "black"]}},
"pumpkin": {"gradient": {"colors": ["orange", "black", "purple", "black"]}},
"turkey": {"gradient": {"colors": ["orange", "black", "dark_green", "black", "brown", "black", "dark_yellow", "black"]}},
"mazel": {"gradient": {"colors": ["blue", "black", "white", "black"]}},
"santa": {"gradient": {"colors": ["red", "white", "green", "black"]}}
}

+ 44
- 0
main/utils.cpp View File

@@ -1,4 +1,8 @@
static const char *TAG = "utils";
#include <esp_log.h>

#include <sys/time.h>
#include <cstring>

#include "utils.hpp"

@@ -11,3 +15,43 @@ int64_t time_us() {
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000000L + (int64_t)ts.tv_nsec / 1000L;
}



std::string read_file(const char *path) {
std::string data;
FILE *f = fopen(path, "r");
if (!f) {
ESP_LOGE(TAG, "Failed to read %s", path);
return data;
}
constexpr size_t szbuf = 4096;
char *buf = (char*)malloc(szbuf);
size_t nread;
while ((nread = fread(buf, 1, szbuf - 1, f)) > 0) {
buf[nread] = '\0';
data += buf;
}
fclose(f);
free(buf);
return data;
}

void write_file(const char *path, const std::string &data) {
return write_file(path, data.c_str(), data.length());
}

void write_file(const char *path, const char *data, int len) {
FILE *f = fopen(path, "w");
if (!f) {
ESP_LOGE(TAG, "Failed to open %s", path);
return;
}
const char *ptr = data;
size_t remain = (len < 0 ? strlen(data) : len);
size_t nwritten = fwrite(ptr, 1, remain, f);
if (nwritten != remain) {
ESP_LOGE(TAG, "Failed to write to %s: %d/%d", path, nwritten, remain);
}
fclose(f);
}

+ 6
- 0
main/utils.hpp View File

@@ -1,6 +1,12 @@
#pragma once

#include <cstdint>
#include <string>
#include <cstdio>


int64_t time_us();

std::string read_file(const char *path);
void write_file(const char *path, const std::string &data);
void write_file(const char *path, const char *data, int len = -1);

Loading…
Cancel
Save