| @@ -11,14 +11,26 @@ Device::Device(std::string _id) : | |||||
| ready(false), should_publish(false), | ready(false), should_publish(false), | ||||
| mutex(xSemaphoreCreateMutex()), | mutex(xSemaphoreCreateMutex()), | ||||
| sem(xSemaphoreCreateBinary()), | sem(xSemaphoreCreateBinary()), | ||||
| power_on(true), | |||||
| power_on(false), strip_length(0), | |||||
| client(start_mqtt_client(on_mqtt_connect, on_mqtt_message, this)) | client(start_mqtt_client(on_mqtt_connect, on_mqtt_message, this)) | ||||
| {} | {} | ||||
| void Device::set_mqtt_config(cJSON*) { | |||||
| void Device::set_json_config(const cJSON *json) { | |||||
| xSemaphoreTake(mutex, portMAX_DELAY); | xSemaphoreTake(mutex, portMAX_DELAY); | ||||
| // TODO: update power_on and effect from JSON | |||||
| const cJSON *state = cJSON_GetObjectItem(json, "state"); | |||||
| if (state && cJSON_IsString(state)) { | |||||
| power_on = strcmp(state->valuestring, "ON") == 0; | |||||
| } | |||||
| const cJSON *fx = cJSON_GetObjectItem(json, "effect"); | |||||
| if (fx && cJSON_IsString(fx)) { | |||||
| effect = fx->valuestring; | |||||
| } | |||||
| const cJSON *length = cJSON_GetObjectItem(json, "length"); | |||||
| if (length && cJSON_IsNumber(length)) { | |||||
| strip_length = length->valuedouble; | |||||
| } | |||||
| publish_state_locked(); | publish_state_locked(); | ||||
| xSemaphoreGive(mutex); | xSemaphoreGive(mutex); | ||||
| @@ -55,22 +67,34 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) { | |||||
| xSemaphoreGive(mutex); | xSemaphoreGive(mutex); | ||||
| } | } | ||||
| void Device::on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t) { | |||||
| void Device::on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t event) { | |||||
| ESP_LOGI(TAG, "Received command via MQTT"); | ESP_LOGI(TAG, "Received command via MQTT"); | ||||
| // TODO: extract JSON, call set_mqtt_config | |||||
| 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"); | |||||
| } | |||||
| xSemaphoreGive(sem); | xSemaphoreGive(sem); | ||||
| } | } | ||||
| cJSON* Device::make_json_config_locked() const { | |||||
| cJSON *json = cJSON_CreateObject(); | |||||
| cJSON_AddItemToObject(json, "state", cJSON_CreateString(power_on ? "ON" : "OFF")); | |||||
| cJSON_AddItemToObject(json, "effect", cJSON_CreateString(effect.c_str())); | |||||
| cJSON_AddItemToObject(json, "length", cJSON_CreateNumber(strip_length)); | |||||
| return json; | |||||
| } | |||||
| void Device::publish_state_locked() { | void Device::publish_state_locked() { | ||||
| if (!(should_publish = !ready)) { | if (!(should_publish = !ready)) { | ||||
| cJSON *json = make_json_config_locked(); | |||||
| char *config = cJSON_PrintUnformatted(json); | |||||
| ESP_ERROR_CHECK(esp_mqtt_client_publish( | ESP_ERROR_CHECK(esp_mqtt_client_publish( | ||||
| client, | |||||
| state_topic.c_str(), | |||||
| ("{" | |||||
| "\"state\": \"" + std::string(power_on ? "ON" : "OFF") + "\", " | |||||
| "\"effect\": \"" + effect + "\"" | |||||
| "}").c_str(), 0, | |||||
| 0, 0 | |||||
| client, state_topic.c_str(), config, 0, 0, 0 | |||||
| )); | )); | ||||
| cJSON_Delete(json); | |||||
| cJSON_free(config); | |||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -11,7 +11,7 @@ class Device { | |||||
| public: | public: | ||||
| Device(std::string id); | Device(std::string id); | ||||
| void set_mqtt_config(cJSON*); | |||||
| void set_json_config(const cJSON*); | |||||
| void lock() const { xSemaphoreTake(mutex, portMAX_DELAY); } | void lock() const { xSemaphoreTake(mutex, portMAX_DELAY); } | ||||
| void unlock() const { xSemaphoreGive(mutex); } | void unlock() const { xSemaphoreGive(mutex); } | ||||
| @@ -26,6 +26,9 @@ public: | |||||
| // Only call while locked | // Only call while locked | ||||
| const std::string& get_effect() const { return effect; } | const std::string& get_effect() const { return effect; } | ||||
| bool is_on() const { return power_on; } | bool is_on() const { return power_on; } | ||||
| int get_strip_length() const { return strip_length; } | |||||
| cJSON* make_json_config_locked() const; | |||||
| private: | private: | ||||
| std::string id; | std::string id; | ||||
| @@ -40,6 +43,7 @@ private: | |||||
| bool power_on; | bool power_on; | ||||
| std::string effect; | std::string effect; | ||||
| int strip_length; | |||||
| esp_mqtt_client_handle_t client; | esp_mqtt_client_handle_t client; | ||||
| @@ -54,4 +58,4 @@ private: | |||||
| void on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t); | void on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t); | ||||
| void publish_state_locked(); | void publish_state_locked(); | ||||
| }; | |||||
| }; | |||||
| @@ -55,6 +55,13 @@ LEDStrip::LEDStrip(int _length) : | |||||
| last_us(0), offset(0) | last_us(0), offset(0) | ||||
| {} | {} | ||||
| void LEDStrip::setLength(int _length) { | |||||
| length_changing(_length); | |||||
| delete[] pixels; | |||||
| length = _length; | |||||
| pixels = new Color[_length]; | |||||
| } | |||||
| void LEDStrip::step() { | void LEDStrip::step() { | ||||
| if (pattern) pattern->step(pixels, length, last_us, offset); | if (pattern) pattern->step(pixels, length, last_us, offset); | ||||
| } | } | ||||
| @@ -74,3 +81,7 @@ void TerminalLEDs::show() const { | |||||
| line += "\x1b[0m"; | line += "\x1b[0m"; | ||||
| ESP_LOGI(TAG, "%s", line.c_str()); | ESP_LOGI(TAG, "%s", line.c_str()); | ||||
| } | } | ||||
| void TerminalLEDs::length_changing(int new_len) { | |||||
| ESP_LOGI(TAG, "Length changing: %d->%d", length, new_len); | |||||
| } | |||||
| @@ -32,19 +32,21 @@ struct Pattern { | |||||
| class LEDStrip { | class LEDStrip { | ||||
| public: | public: | ||||
| LEDStrip(int length); | |||||
| LEDStrip(int length = 0); | |||||
| void step(); | void step(); | ||||
| virtual void show() const = 0; | virtual void show() const = 0; | ||||
| virtual void length_changing(int) {}; | |||||
| void setPattern(const Pattern* _pattern) { pattern = _pattern; } | |||||
| void setPattern(const Pattern *_pattern) { pattern = _pattern; } | |||||
| const Pattern* getPattern() const { return pattern; } | const Pattern* getPattern() const { return pattern; } | ||||
| void setLength(int length); | |||||
| int getLength() const { return length; } | int getLength() const { return length; } | ||||
| protected: | protected: | ||||
| const int length; | |||||
| int length; | |||||
| const Pattern *pattern; | const Pattern *pattern; | ||||
| Color *pixels; | Color *pixels; | ||||
| @@ -55,6 +57,7 @@ private: | |||||
| class TerminalLEDs : public LEDStrip { | class TerminalLEDs : public LEDStrip { | ||||
| public: | public: | ||||
| TerminalLEDs(int length) : LEDStrip(length) {} | |||||
| TerminalLEDs(int length = 0) : LEDStrip(length) {} | |||||
| void show() const; | void show() const; | ||||
| void length_changing(int); | |||||
| }; | }; | ||||
| @@ -84,6 +84,52 @@ static void log_dir(const std::string &path) { | |||||
| } | } | ||||
| 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"; | |||||
| // Entry Point | // Entry Point | ||||
| extern "C" void app_main(void) { | extern "C" void app_main(void) { | ||||
| @@ -109,38 +155,52 @@ extern "C" void app_main(void) { | |||||
| // Dummy Filesystem | // Dummy Filesystem | ||||
| start_filesystem(); | start_filesystem(); | ||||
| //log_dir("/spiffs"); | |||||
| // TODO: Scrape this out | |||||
| log_dir("/spiffs"); | |||||
| Device device("blinky-jr"); | Device device("blinky-jr"); | ||||
| // TODO: Load config from SPIFFS | |||||
| cJSON *json = cJSON_Parse(read_file(config_path).c_str()); | |||||
| if (json) { | |||||
| ESP_LOGI(TAG, "Setting config"); | |||||
| device.set_json_config(json); | |||||
| cJSON_Delete(json); | |||||
| } | |||||
| auto LEDs = TerminalLEDs(20); | |||||
| /*** *** ***/ | |||||
| LEDs.setPattern(Presets::find("rainbow")); | |||||
| /*** *** ***/ | |||||
| LEDStrip *LEDs = new TerminalLEDs(); | |||||
| while (true) { | while (true) { | ||||
| ESP_LOGI(TAG, "Configuring LEDs"); | ESP_LOGI(TAG, "Configuring LEDs"); | ||||
| device.lock(); | device.lock(); | ||||
| const std::string &effect = device.get_effect(); | |||||
| bool is_on = device.is_on(); | |||||
| int length = device.get_strip_length(); | |||||
| cJSON *json = device.make_json_config_locked(); | |||||
| device.unlock(); | |||||
| if (length != LEDs->getLength()) { | |||||
| LEDs->setLength(length); | |||||
| } | |||||
| const Pattern *pattern = Presets::find(device.get_effect()); | |||||
| const Pattern *pattern = Presets::find(effect); | |||||
| if (pattern) { | if (pattern) { | ||||
| LEDs.setPattern(pattern); | |||||
| LEDs->setPattern(pattern); | |||||
| } else { | } else { | ||||
| ESP_LOGW(TAG, "Could not find pattern '%s'", device.get_effect()); | |||||
| pattern = LEDs.getPattern(); | |||||
| ESP_LOGW(TAG, "Could not find pattern '%s'", effect.c_str()); | |||||
| pattern = LEDs->getPattern(); | |||||
| } | } | ||||
| // TODO: Save Config to SPIFFS | |||||
| char *config = cJSON_PrintUnformatted(json); | |||||
| write_file(config_path, config); | |||||
| ESP_LOGI(TAG, "Saved config"); | |||||
| bool is_on = device.is_on(); | |||||
| device.unlock(); | |||||
| cJSON_free(config); | |||||
| cJSON_Delete(json); | |||||
| if (!pattern || !is_on) { | |||||
| if (length <= 0 || !pattern || !is_on) { | |||||
| if (!is_on) ESP_LOGI(TAG, "Device off"); | if (!is_on) ESP_LOGI(TAG, "Device off"); | ||||
| if (length <= 0) ESP_LOGW(TAG, "No LEDs configured"); | |||||
| if (!pattern) ESP_LOGW(TAG, "No LED pattern set"); | if (!pattern) ESP_LOGW(TAG, "No LED pattern set"); | ||||
| ESP_LOGI(TAG, "Waiting for new config"); | ESP_LOGI(TAG, "Waiting for new config"); | ||||
| @@ -159,8 +219,8 @@ extern "C" void app_main(void) { | |||||
| break; | break; | ||||
| } | } | ||||
| LEDs.step(); | |||||
| LEDs.show(); | |||||
| LEDs->step(); | |||||
| LEDs->show(); | |||||
| target_us += period_us; | target_us += period_us; | ||||
| } | } | ||||