diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3a8e84..156e8d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,7 @@ jobs: path: ${{ matrix.path }} command: idf.py build && idf.py merge-bin -o merged.bin + # TODO: Overwrites NVS. Supply individual bin files and build web flasher. - name: Package firmware run: | VERSION="${{ github.event.release.tag_name || env.TARGET_BRANCH }}" diff --git a/README.md b/README.md index 399f6d0..d7c5a89 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ Firmware written for DIY devices in the Beacon ecosystem # TODO - - [ ] Implement new LIGHT state! + - [x] Implement new LIGHT state! + - [ ] Better startup state. + - [ ] Initial long and short names + - [ ] Add some setup instructions to the displays? - [ ] Section grouping - [ ] Multi alert text indeces - [ ] Make textAlert an array of tasks in the group, one per index of alert text. diff --git a/components/beacon_device/CMakeLists.txt b/components/beacon_device/CMakeLists.txt index 54f8621..6d916c5 100644 --- a/components/beacon_device/CMakeLists.txt +++ b/components/beacon_device/CMakeLists.txt @@ -26,8 +26,13 @@ idf_component_register( "consumer/display/Hub75LvglDisplayConsumer.cpp" ${HELVATICA_FONT_SRCS} "platform/Platform.cpp" - "httpServer/EspHttpServer.cpp" - "httpServer/HttpHandlers.cpp" + "http/EspHttpDaemon.cpp" + "http/BeaconHttpServer.cpp" + "http/api/StaticAssetApi.cpp" + "http/api/ConfigApi.cpp" + "http/api/HardwareConfigApi.cpp" + "config/NvsHardwareStore.cpp" + "hardware/HardwareConfig.cpp" "networkConnection/StaWifiConnection.cpp" "orchestrator/IOrchestrator.cpp" "orchestrator/SateliteOrchestrator.cpp" @@ -47,8 +52,16 @@ idf_component_register( "consumer/display/Ili9341LvglDisplayConsumer.hpp" "consumer/display/Hub75LvglDisplayConsumer.hpp" "platform/Platform.hpp" - "httpServer/EspHttpServer.hpp" - "httpServer/HttpHandlers.hpp" + "http/IHttpDaemon.hpp" + "http/IHttpApi.hpp" + "http/EspHttpDaemon.hpp" + "http/BeaconHttpServer.hpp" + "http/api/StaticAssetApi.hpp" + "http/api/ConfigApi.hpp" + "http/api/HardwareConfigApi.hpp" + "config/IHardwareStore.hpp" + "config/NvsHardwareStore.hpp" + "hardware/HardwareConfig.hpp" "networkConnection/INetworkConnection.hpp" "networkConnection/IEthernetConnection.hpp" "networkConnection/IWifiConnection.hpp" diff --git a/components/beacon_device/config/IHardwareStore.hpp b/components/beacon_device/config/IHardwareStore.hpp new file mode 100644 index 0000000..854df9d --- /dev/null +++ b/components/beacon_device/config/IHardwareStore.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +class IHardwareStore { +public: + virtual ~IHardwareStore() = default; + virtual bool getInt(const char* key, int32_t& out) const = 0; + virtual bool setInt(const char* key, int32_t val) = 0; + virtual bool commit() = 0; +}; diff --git a/components/beacon_device/config/NvsHardwareStore.cpp b/components/beacon_device/config/NvsHardwareStore.cpp new file mode 100644 index 0000000..a82016a --- /dev/null +++ b/components/beacon_device/config/NvsHardwareStore.cpp @@ -0,0 +1,47 @@ +#include "config/NvsHardwareStore.hpp" +#include "esp_log.h" + +static constexpr char TAG[] = "NvsHardwareStore"; + +NvsHardwareStore::~NvsHardwareStore() +{ + if (_writeHandle) nvs_close(_writeHandle); +} + +bool NvsHardwareStore::getInt(const char* key, int32_t& out) const +{ + nvs_handle_t h; + if (nvs_open(NS, NVS_READONLY, &h) != ESP_OK) return false; + esp_err_t err = nvs_get_i32(h, key, &out); + nvs_close(h); + return err == ESP_OK; +} + +bool NvsHardwareStore::setInt(const char* key, int32_t val) +{ + if (!openWrite()) return false; + esp_err_t err = nvs_set_i32(_writeHandle, key, val); + if (err != ESP_OK) ESP_LOGW(TAG, "nvs_set_i32('%s') failed: %s", key, esp_err_to_name(err)); + return err == ESP_OK; +} + +bool NvsHardwareStore::commit() +{ + if (!_writeHandle) return true; + esp_err_t err = nvs_commit(_writeHandle); + nvs_close(_writeHandle); + _writeHandle = 0; + if (err != ESP_OK) ESP_LOGW(TAG, "nvs_commit failed: %s", esp_err_to_name(err)); + return err == ESP_OK; +} + +bool NvsHardwareStore::openWrite() +{ + if (_writeHandle) return true; + esp_err_t err = nvs_open(NS, NVS_READWRITE, &_writeHandle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_open for write failed: %s", esp_err_to_name(err)); + _writeHandle = 0; + } + return err == ESP_OK; +} diff --git a/components/beacon_device/config/NvsHardwareStore.hpp b/components/beacon_device/config/NvsHardwareStore.hpp new file mode 100644 index 0000000..ce008fd --- /dev/null +++ b/components/beacon_device/config/NvsHardwareStore.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "config/IHardwareStore.hpp" +#include "nvs.h" + +class NvsHardwareStore : public IHardwareStore { +public: + ~NvsHardwareStore(); + bool getInt(const char* key, int32_t& out) const override; + bool setInt(const char* key, int32_t val) override; + bool commit() override; + +private: + static constexpr char NS[] = "hw_cfg"; + nvs_handle_t _writeHandle = 0; + + bool openWrite(); +}; diff --git a/components/beacon_device/hardware/HardwareConfig.cpp b/components/beacon_device/hardware/HardwareConfig.cpp new file mode 100644 index 0000000..b89174c --- /dev/null +++ b/components/beacon_device/hardware/HardwareConfig.cpp @@ -0,0 +1,190 @@ +#include "hardware/HardwareConfig.hpp" +#include "esp_log.h" +#include +#include + +static constexpr char TAG[] = "HardwareConfig"; + +// ── IntField ────────────────────────────────────────────────────────────────── + +void IntField::addToJson(cJSON* arr) const +{ + cJSON* f = cJSON_CreateObject(); + cJSON_AddStringToObject(f, "key", _key); + cJSON_AddStringToObject(f, "label", _label); + cJSON_AddStringToObject(f, "type", "int"); + cJSON_AddNumberToObject(f, "value", static_cast(_raw)); + cJSON_AddNumberToObject(f, "default", static_cast(_default)); + cJSON_AddNumberToObject(f, "min", static_cast(_min)); + cJSON_AddNumberToObject(f, "max", static_cast(_max)); + cJSON_AddItemToArray(arr, f); +} + +// ── BoolField ───────────────────────────────────────────────────────────────── + +void BoolField::addToJson(cJSON* arr) const +{ + cJSON* f = cJSON_CreateObject(); + cJSON_AddStringToObject(f, "key", _key); + cJSON_AddStringToObject(f, "label", _label); + cJSON_AddStringToObject(f, "type", "bool"); + cJSON_AddBoolToObject (f, "value", _raw != 0); + cJSON_AddBoolToObject (f, "default", _default != 0); + cJSON_AddItemToArray(arr, f); +} + +// ── SelectField ─────────────────────────────────────────────────────────────── + +bool SelectField::validate(int32_t v) const +{ + for (size_t i = 0; i < _optionCount; i++) { + if (_options[i].value == v) return true; + } + return false; +} + +void SelectField::addToJson(cJSON* arr) const +{ + cJSON* f = cJSON_CreateObject(); + cJSON_AddStringToObject(f, "key", _key); + cJSON_AddStringToObject(f, "label", _label); + cJSON_AddStringToObject(f, "type", "select"); + cJSON_AddNumberToObject(f, "value", static_cast(_raw)); + cJSON_AddNumberToObject(f, "default", static_cast(_default)); + cJSON* opts = cJSON_AddArrayToObject(f, "options"); + for (size_t i = 0; i < _optionCount; i++) { + cJSON* o = cJSON_CreateObject(); + cJSON_AddStringToObject(o, "label", _options[i].label); + cJSON_AddNumberToObject(o, "value", static_cast(_options[i].value)); + cJSON_AddItemToArray(opts, o); + } + cJSON_AddItemToArray(arr, f); +} + +// ── GpioField ───────────────────────────────────────────────────────────────── + +GpioField::GpioField(const char* key, const char* label, GpioFieldConfig cfg) + : HardwareFieldBase(key, label, static_cast(cfg.defaultValue)) +{ + if (cfg.allow && cfg.allowCount > 0) { + for (size_t i = 0; i < cfg.allowCount; i++) { + if (GPIO_IS_VALID_OUTPUT_GPIO(static_cast(cfg.allow[i]))) + _available.push_back(cfg.allow[i]); + } + } else { + for (int i = 0; i < GPIO_NUM_MAX; i++) { + if (!GPIO_IS_VALID_OUTPUT_GPIO(i)) continue; + gpio_num_t pin = static_cast(i); + bool excluded = false; + for (size_t j = 0; j < cfg.excludeCount; j++) { + if (cfg.exclude[j] == pin) { excluded = true; break; } + } + if (!excluded) _available.push_back(pin); + } + } +} + +bool GpioField::validate(int32_t v) const +{ + gpio_num_t pin = static_cast(v); + for (gpio_num_t a : _available) { + if (a == pin) return true; + } + return false; +} + +void GpioField::addToJson(cJSON* arr) const +{ + cJSON* f = cJSON_CreateObject(); + cJSON_AddStringToObject(f, "key", _key); + cJSON_AddStringToObject(f, "label", _label); + cJSON_AddStringToObject(f, "type", "gpio"); + cJSON_AddNumberToObject(f, "value", static_cast(_raw)); + cJSON_AddNumberToObject(f, "default", static_cast(_default)); + cJSON* avail = cJSON_AddArrayToObject(f, "available"); + for (gpio_num_t pin : _available) { + cJSON_AddItemToArray(avail, cJSON_CreateNumber(static_cast(pin))); + } + cJSON_AddItemToArray(arr, f); +} + +// ── HardwareSection ─────────────────────────────────────────────────────────── + +IntField& HardwareSection::addInt(const char* key, const char* label, IntFieldConfig cfg) +{ + assert(strlen(key) <= 15 && "NVS key must be <= 15 chars"); + _intFields.emplace_back(std::make_unique(key, label, cfg)); + _allFields.push_back(_intFields.back().get()); + return *_intFields.back(); +} + +BoolField& HardwareSection::addBool(const char* key, const char* label, BoolFieldConfig cfg) +{ + assert(strlen(key) <= 15 && "NVS key must be <= 15 chars"); + _boolFields.emplace_back(std::make_unique(key, label, cfg)); + _allFields.push_back(_boolFields.back().get()); + return *_boolFields.back(); +} + +SelectField& HardwareSection::addSelect(const char* key, const char* label, SelectFieldConfig cfg) +{ + assert(strlen(key) <= 15 && "NVS key must be <= 15 chars"); + _selectFields.emplace_back(std::make_unique(key, label, cfg)); + _allFields.push_back(_selectFields.back().get()); + return *_selectFields.back(); +} + +GpioField& HardwareSection::addGpio(const char* key, const char* label, GpioFieldConfig cfg) +{ + assert(strlen(key) <= 15 && "NVS key must be <= 15 chars"); + _gpioFields.emplace_back(std::make_unique(key, label, cfg)); + _allFields.push_back(_gpioFields.back().get()); + return *_gpioFields.back(); +} + +// ── HardwareConfig ──────────────────────────────────────────────────────────── + +HardwareConfig::HardwareConfig(IHardwareStore& store) + : _store(store) {} + +HardwareSection& HardwareConfig::addSection(const char* title) +{ + _sections.emplace_back(std::make_unique(title)); + return *_sections.back(); +} + +void HardwareConfig::load() +{ + for (auto& section : _sections) { + for (auto* field : section->allFields()) { + int32_t val; + if (_store.getInt(field->key(), val)) { + field->setRaw(val); + } + } + } + ESP_LOGI(TAG, "Hardware config loaded"); +} + +bool HardwareConfig::save() +{ + for (auto& section : _sections) { + for (auto* field : section->allFields()) { + if (!_store.setInt(field->key(), field->rawValue())) { + ESP_LOGE(TAG, "Failed to set '%s'", field->key()); + return false; + } + } + } + return _store.commit(); +} + +HardwareFieldBase* HardwareConfig::findField(const char* key) +{ + for (auto& section : _sections) { + for (auto* field : section->allFields()) { + if (strcmp(field->key(), key) == 0) return field; + } + } + return nullptr; +} diff --git a/components/beacon_device/hardware/HardwareConfig.hpp b/components/beacon_device/hardware/HardwareConfig.hpp new file mode 100644 index 0000000..4a8dd55 --- /dev/null +++ b/components/beacon_device/hardware/HardwareConfig.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include +#include "driver/gpio.h" +#include "cJSON.h" +#include "config/IHardwareStore.hpp" + +// ── Field config structs ────────────────────────────────────────────────────── + +struct IntFieldConfig { + int32_t defaultValue; + int32_t min; + int32_t max; +}; + +struct BoolFieldConfig { + bool defaultValue; +}; + +struct SelectOption { + const char* label; + int32_t value; +}; + +struct SelectFieldConfig { + const SelectOption* options; + size_t optionCount; + int32_t defaultValue; +}; + +struct GpioFieldConfig { + gpio_num_t defaultValue; + const gpio_num_t* exclude = nullptr; // blacklist — remove from all valid output GPIOs + size_t excludeCount = 0; + const gpio_num_t* allow = nullptr; // whitelist — use only these (still filtered by GPIO_IS_VALID_OUTPUT_GPIO) + size_t allowCount = 0; +}; + +// ── Base field ──────────────────────────────────────────────────────────────── + +class HardwareFieldBase { +public: + virtual ~HardwareFieldBase() = default; + + const char* key() const { return _key; } + const char* label() const { return _label; } + int32_t rawValue() const { return _raw; } + void setRaw(int32_t v) { _raw = v; } + + virtual const char* typeName() const = 0; + virtual bool validate(int32_t v) const = 0; + virtual void addToJson(cJSON* fieldsArray) const = 0; + +protected: + HardwareFieldBase(const char* key, const char* label, int32_t defaultVal) + : _key(key), _label(label), _raw(defaultVal), _default(defaultVal) {} + + const char* _key; + const char* _label; + int32_t _raw; + int32_t _default; +}; + +// ── Typed fields ────────────────────────────────────────────────────────────── + +class IntField : public HardwareFieldBase { +public: + IntField(const char* key, const char* label, IntFieldConfig cfg) + : HardwareFieldBase(key, label, cfg.defaultValue) + , _min(cfg.min), _max(cfg.max) {} + + int32_t value() const { return _raw; } + const char* typeName() const override { return "int"; } + bool validate(int32_t v) const override { return v >= _min && v <= _max; } + void addToJson(cJSON* arr) const override; + +private: + int32_t _min; + int32_t _max; +}; + +class BoolField : public HardwareFieldBase { +public: + BoolField(const char* key, const char* label, BoolFieldConfig cfg) + : HardwareFieldBase(key, label, cfg.defaultValue ? 1 : 0) {} + + bool value() const { return _raw != 0; } + const char* typeName() const override { return "bool"; } + bool validate(int32_t v) const override { return v == 0 || v == 1; } + void addToJson(cJSON* arr) const override; +}; + +class SelectField : public HardwareFieldBase { +public: + SelectField(const char* key, const char* label, SelectFieldConfig cfg) + : HardwareFieldBase(key, label, cfg.defaultValue) + , _options(cfg.options), _optionCount(cfg.optionCount) {} + + int32_t value() const { return _raw; } + const char* typeName() const override { return "select"; } + bool validate(int32_t v) const override; + void addToJson(cJSON* arr) const override; + +private: + const SelectOption* _options; + size_t _optionCount; +}; + +class GpioField : public HardwareFieldBase { +public: + GpioField(const char* key, const char* label, GpioFieldConfig cfg); + + gpio_num_t value() const { return static_cast(_raw); } + const char* typeName() const override { return "gpio"; } + bool validate(int32_t v) const override; + void addToJson(cJSON* arr) const override; + +private: + std::vector _available; +}; + +// ── Section ─────────────────────────────────────────────────────────────────── + +class HardwareSection { +public: + explicit HardwareSection(const char* title) : _title(title) {} + + const char* title() const { return _title; } + + IntField& addInt (const char* key, const char* label, IntFieldConfig cfg); + BoolField& addBool (const char* key, const char* label, BoolFieldConfig cfg); + SelectField& addSelect(const char* key, const char* label, SelectFieldConfig cfg); + GpioField& addGpio (const char* key, const char* label, GpioFieldConfig cfg); + + const std::vector& allFields() const { return _allFields; } + +private: + const char* _title; + std::vector> _intFields; + std::vector> _boolFields; + std::vector> _selectFields; + std::vector> _gpioFields; + std::vector _allFields; +}; + +// ── HardwareConfig ──────────────────────────────────────────────────────────── + +class HardwareConfig { +public: + explicit HardwareConfig(IHardwareStore& store); + + HardwareSection& addSection(const char* title); + void load(); + bool save(); + HardwareFieldBase* findField(const char* key); + + const std::vector>& sections() const { return _sections; } + +private: + IHardwareStore& _store; + std::vector> _sections; +}; diff --git a/components/beacon_device/http/BeaconHttpServer.cpp b/components/beacon_device/http/BeaconHttpServer.cpp new file mode 100644 index 0000000..a5856e7 --- /dev/null +++ b/components/beacon_device/http/BeaconHttpServer.cpp @@ -0,0 +1,33 @@ +#include "http/BeaconHttpServer.hpp" +#include "esp_log.h" + +static constexpr char TAG[] = "BeaconHttpServer"; + +BeaconHttpServer::BeaconHttpServer(Config& config, DeviceInfo& deviceInfo, + INetworkConnection& network, IBeaconConnection& beacon) + : _configApi(config, deviceInfo, network, beacon) +{ + _apis[_apiCount++] = &_staticApi; + _apis[_apiCount++] = &_configApi; +} + +void BeaconHttpServer::addApi(IHttpApi& api) +{ + if (_apiCount >= MAX_APIS) { + ESP_LOGW(TAG, "Max APIs reached, cannot add more"); + return; + } + _apis[_apiCount++] = &api; +} + +void BeaconHttpServer::start(uint16_t port) +{ + _server.start(port); + for (uint8_t i = 0; i < _apiCount; i++) + _apis[i]->registerOn(_server); +} + +void BeaconHttpServer::stop() +{ + _server.stop(); +} diff --git a/components/beacon_device/http/BeaconHttpServer.hpp b/components/beacon_device/http/BeaconHttpServer.hpp new file mode 100644 index 0000000..37ff137 --- /dev/null +++ b/components/beacon_device/http/BeaconHttpServer.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "http/EspHttpDaemon.hpp" +#include "http/IHttpApi.hpp" +#include "http/api/StaticAssetApi.hpp" +#include "http/api/ConfigApi.hpp" + +class Config; +class DeviceInfo; +class INetworkConnection; +class IBeaconConnection; + +class BeaconHttpServer { +public: + BeaconHttpServer(Config& config, DeviceInfo& deviceInfo, + INetworkConnection& network, IBeaconConnection& beacon); + + void addApi(IHttpApi& api); + void start(uint16_t port = 80); + void stop(); + +private: + static constexpr uint8_t MAX_APIS = 8; + + EspHttpDaemon _server; + StaticAssetApi _staticApi; + ConfigApi _configApi; + IHttpApi* _apis[MAX_APIS] = {}; + uint8_t _apiCount = 0; +}; diff --git a/components/beacon_device/httpServer/EspHttpServer.cpp b/components/beacon_device/http/EspHttpDaemon.cpp similarity index 78% rename from components/beacon_device/httpServer/EspHttpServer.cpp rename to components/beacon_device/http/EspHttpDaemon.cpp index d7a63b8..d8b3bdc 100644 --- a/components/beacon_device/httpServer/EspHttpServer.cpp +++ b/components/beacon_device/http/EspHttpDaemon.cpp @@ -1,7 +1,7 @@ -#include "httpServer/EspHttpServer.hpp" +#include "http/EspHttpDaemon.hpp" #include "esp_log.h" -void EspHttpServer::start(uint16_t port) +void EspHttpDaemon::start(uint16_t port) { if (_server) return; @@ -17,7 +17,7 @@ void EspHttpServer::start(uint16_t port) ESP_LOGI(TAG, "Started on port %d", port); } -void EspHttpServer::stop() +void EspHttpDaemon::stop() { if (!_server) return; httpd_stop(_server); @@ -25,9 +25,9 @@ void EspHttpServer::stop() ESP_LOGI(TAG, "Stopped"); } -bool EspHttpServer::isRunning() const { return _server != nullptr; } +bool EspHttpDaemon::isRunning() const { return _server != nullptr; } -void EspHttpServer::registerHandler(const char* uri, httpd_method_t method, +void EspHttpDaemon::registerHandler(const char* uri, httpd_method_t method, esp_err_t (*handle)(httpd_req_t*), void* ctx) { if (!_server) { diff --git a/components/beacon_device/http/EspHttpDaemon.hpp b/components/beacon_device/http/EspHttpDaemon.hpp new file mode 100644 index 0000000..4a1df5a --- /dev/null +++ b/components/beacon_device/http/EspHttpDaemon.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "http/IHttpDaemon.hpp" + +class EspHttpDaemon : public IHttpDaemon { +public: + EspHttpDaemon() = default; + ~EspHttpDaemon() { stop(); } + + void start(uint16_t port = 80) override; + void stop() override; + bool isRunning() const override; + + void registerHandler(const char* uri, + httpd_method_t method, + esp_err_t (*handler)(httpd_req_t*), + void* ctx = nullptr) override; + +private: + static constexpr char TAG[] = "EspHttpDaemon"; + static constexpr int MAX_URI_HANDLERS = 12; + + httpd_handle_t _server = nullptr; +}; diff --git a/components/beacon_device/http/IHttpApi.hpp b/components/beacon_device/http/IHttpApi.hpp new file mode 100644 index 0000000..f7ccc5b --- /dev/null +++ b/components/beacon_device/http/IHttpApi.hpp @@ -0,0 +1,9 @@ +#pragma once + +class IHttpDaemon; + +class IHttpApi { +public: + virtual ~IHttpApi() = default; + virtual void registerOn(IHttpDaemon& server) = 0; +}; diff --git a/components/beacon_device/http/IHttpDaemon.hpp b/components/beacon_device/http/IHttpDaemon.hpp new file mode 100644 index 0000000..8f87223 --- /dev/null +++ b/components/beacon_device/http/IHttpDaemon.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "esp_http_server.h" +#include + +class IHttpDaemon { +public: + virtual ~IHttpDaemon() = default; + + virtual void start(uint16_t port = 80) = 0; + virtual void stop() = 0; + virtual bool isRunning() const = 0; + + virtual void registerHandler(const char* uri, + httpd_method_t method, + esp_err_t (*handler)(httpd_req_t*), + void* ctx = nullptr) = 0; +}; diff --git a/components/beacon_device/httpServer/HttpHandlers.cpp b/components/beacon_device/http/api/ConfigApi.cpp similarity index 80% rename from components/beacon_device/httpServer/HttpHandlers.cpp rename to components/beacon_device/http/api/ConfigApi.cpp index 55be8d5..7c699b0 100644 --- a/components/beacon_device/httpServer/HttpHandlers.cpp +++ b/components/beacon_device/http/api/ConfigApi.cpp @@ -1,4 +1,5 @@ -#include "httpServer/HttpHandlers.hpp" +#include "http/api/ConfigApi.hpp" +#include "http/IHttpDaemon.hpp" #include "networkConnection/IWifiConnection.hpp" #include "esp_log.h" #include "esp_system.h" @@ -9,46 +10,7 @@ #include #include -static constexpr char TAG[] = "HttpHandlers"; - -// ── Embedded UI assets ──────────────────────────────────────────────────────── - -extern const uint8_t ui_index_html_start[] asm("_binary_index_html_start"); -extern const uint8_t ui_index_html_end[] asm("_binary_index_html_end"); -extern const uint8_t ui_style_css_start[] asm("_binary_style_css_start"); -extern const uint8_t ui_style_css_end[] asm("_binary_style_css_end"); -extern const uint8_t ui_app_js_start[] asm("_binary_app_js_start"); -extern const uint8_t ui_app_js_end[] asm("_binary_app_js_end"); - -static esp_err_t sendAsset(httpd_req_t* req, - const uint8_t* start, const uint8_t* end, - const char* contentType) -{ - httpd_resp_set_type(req, contentType); - size_t size = end - start; - if (size > 0 && start[size - 1] == '\0') size--; - return httpd_resp_send(req, reinterpret_cast(start), size); -} - -// ── Static asset handlers ───────────────────────────────────────────────────── - -esp_err_t HttpHandlers::handleRoot(httpd_req_t* req) -{ - return sendAsset(req, ui_index_html_start, ui_index_html_end, - "text/html; charset=utf-8"); -} - -esp_err_t HttpHandlers::handleCss(httpd_req_t* req) -{ - return sendAsset(req, ui_style_css_start, ui_style_css_end, - "text/css; charset=utf-8"); -} - -esp_err_t HttpHandlers::handleJs(httpd_req_t* req) -{ - return sendAsset(req, ui_app_js_start, ui_app_js_end, - "application/javascript; charset=utf-8"); -} +static constexpr char TAG[] = "ConfigApi"; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -73,18 +35,34 @@ static int readBody(httpd_req_t* req, char* buf, size_t bufLen) return received; } +// ── Construction + registration ─────────────────────────────────────────────── + +ConfigApi::ConfigApi(Config& config, DeviceInfo& deviceInfo, + INetworkConnection& network, IBeaconConnection& beacon) + : _ctx{config, deviceInfo, network, beacon} +{} + +void ConfigApi::registerOn(IHttpDaemon& server) +{ + server.registerHandler("/api/config", HTTP_GET, handleGetConfig, &_ctx); + server.registerHandler("/api/config", HTTP_POST, handleSetConfig, &_ctx); + server.registerHandler("/api/status", HTTP_GET, handleGetStatus, &_ctx); + server.registerHandler("/api/scan/start", HTTP_POST, handleStartScan, &_ctx); + server.registerHandler("/api/scan", HTTP_GET, handleGetScan, &_ctx); + server.registerHandler("/api/reboot", HTTP_POST, handleReboot, nullptr); +} + // ── GET /api/config ─────────────────────────────────────────────────────────── -esp_err_t HttpHandlers::handleGetConfig(httpd_req_t* req) +esp_err_t ConfigApi::handleGetConfig(httpd_req_t* req) { - auto* ctx = static_cast(req->user_ctx); + auto* ctx = static_cast(req->user_ctx); const Settings& s = ctx->config.get(); const int cn = ctx->deviceInfo.consumerCount; const int bn = ctx->deviceInfo.deviceType == DeviceType::SINGLE_TOPIC ? 1 : cn; cJSON* root = cJSON_CreateObject(); - // device — includes static device info alongside writable name cJSON* device = cJSON_AddObjectToObject(root, "device"); cJSON_AddStringToObject(device, "name", s.deviceName); cJSON_AddStringToObject(device, "model", ctx->deviceInfo.model); @@ -94,12 +72,10 @@ esp_err_t HttpHandlers::handleGetConfig(httpd_req_t* req) for (int i = 0; i < cn; i++) cJSON_AddItemToArray(namesArr, cJSON_CreateString(ctx->deviceInfo.consumerNames[i])); - // network cJSON* network = cJSON_AddObjectToObject(root, "network"); cJSON_AddStringToObject(network, "ssid", s.network.ssid); - cJSON_AddStringToObject(network, "apSsid", ""); // TODO: auto-generate from deviceName + cJSON_AddStringToObject(network, "apSsid", ""); - // beacon cJSON* beacon = cJSON_AddObjectToObject(root, "beacon"); cJSON_AddStringToObject(beacon, "mqttUrl", s.beacon.mqttUrl); cJSON_AddBoolToObject (beacon, "discoveryEnabled", s.beacon.discoveryEnabled); @@ -111,7 +87,6 @@ esp_err_t HttpHandlers::handleGetConfig(httpd_req_t* req) cJSON_AddItemToArray(consumers, c); } - // display — map int8_t [-127,127] → UI percentage [-50,50] cJSON* display = cJSON_AddObjectToObject(root, "display"); cJSON* trims = cJSON_AddArrayToObject(display, "brightnessTrims"); for (int i = 0; i < cn; i++) { @@ -119,7 +94,6 @@ esp_err_t HttpHandlers::handleGetConfig(httpd_req_t* req) cJSON_AddItemToArray(trims, cJSON_CreateNumber(pct)); } - // runtime — all readonly from the Beacon Base perspective cJSON* runtime = cJSON_AddObjectToObject(root, "runtime"); cJSON_AddNumberToObject(runtime, "masterBrightness", static_cast(roundf(s.runtime.brightness / 255.0f * 100.0f))); @@ -139,9 +113,9 @@ esp_err_t HttpHandlers::handleGetConfig(httpd_req_t* req) // ── POST /api/config ────────────────────────────────────────────────────────── -esp_err_t HttpHandlers::handleSetConfig(httpd_req_t* req) +esp_err_t ConfigApi::handleSetConfig(httpd_req_t* req) { - auto* ctx = static_cast(req->user_ctx); + auto* ctx = static_cast(req->user_ctx); if (req->content_len <= 0 || req->content_len > 4096) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid body size"); @@ -228,7 +202,6 @@ esp_err_t HttpHandlers::handleSetConfig(httpd_req_t* req) cJSON_ArrayForEach(val, trims) { if (i >= cn) break; if (cJSON_IsNumber(val)) { - // Map UI percentage [-50,50] → int8_t [-127,127] float hw = val->valuedouble * 127.0f / 50.0f; hw = hw < -127.0f ? -127.0f : hw > 127.0f ? 127.0f : hw; updated.display.brightness_trim[i] = static_cast(roundf(hw)); @@ -250,9 +223,9 @@ esp_err_t HttpHandlers::handleSetConfig(httpd_req_t* req) // ── GET /api/status ─────────────────────────────────────────────────────────── -esp_err_t HttpHandlers::handleGetStatus(httpd_req_t* req) +esp_err_t ConfigApi::handleGetStatus(httpd_req_t* req) { - auto* ctx = static_cast(req->user_ctx); + auto* ctx = static_cast(req->user_ctx); char ipStr[16] = "0.0.0.0"; if (ctx->network.isConnected()) { @@ -270,9 +243,9 @@ esp_err_t HttpHandlers::handleGetStatus(httpd_req_t* req) // ── POST /api/scan/start ────────────────────────────────────────────────────── -esp_err_t HttpHandlers::handleStartScan(httpd_req_t* req) +esp_err_t ConfigApi::handleStartScan(httpd_req_t* req) { - auto* ctx = static_cast(req->user_ctx); + auto* ctx = static_cast(req->user_ctx); auto* wifi = ctx->network.asWifi(); cJSON* root = cJSON_CreateObject(); @@ -284,9 +257,9 @@ esp_err_t HttpHandlers::handleStartScan(httpd_req_t* req) // ── GET /api/scan ───────────────────────────────────────────────────────────── -esp_err_t HttpHandlers::handleGetScan(httpd_req_t* req) +esp_err_t ConfigApi::handleGetScan(httpd_req_t* req) { - auto* ctx = static_cast(req->user_ctx); + auto* ctx = static_cast(req->user_ctx); auto* wifi = ctx->network.asWifi(); cJSON* root = cJSON_CreateObject(); @@ -315,7 +288,7 @@ esp_err_t HttpHandlers::handleGetScan(httpd_req_t* req) // ── POST /api/reboot ────────────────────────────────────────────────────────── -esp_err_t HttpHandlers::handleReboot(httpd_req_t* req) +esp_err_t ConfigApi::handleReboot(httpd_req_t* req) { cJSON* root = cJSON_CreateObject(); cJSON_AddBoolToObject(root, "ok", true); diff --git a/components/beacon_device/http/api/ConfigApi.hpp b/components/beacon_device/http/api/ConfigApi.hpp new file mode 100644 index 0000000..ee162f0 --- /dev/null +++ b/components/beacon_device/http/api/ConfigApi.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "http/IHttpApi.hpp" +#include "esp_http_server.h" +#include "config/Config.hpp" +#include "types/DeviceTypes.hpp" +#include "networkConnection/INetworkConnection.hpp" +#include "beaconConnection/IBeaconConnection.hpp" + +class ConfigApi : public IHttpApi { +public: + ConfigApi(Config& config, DeviceInfo& deviceInfo, + INetworkConnection& network, IBeaconConnection& beacon); + + void registerOn(IHttpDaemon& server) override; + +private: + struct Ctx { + Config& config; + DeviceInfo& deviceInfo; + INetworkConnection& network; + IBeaconConnection& beacon; + }; + + Ctx _ctx; + + static esp_err_t handleGetConfig (httpd_req_t* req); + static esp_err_t handleSetConfig (httpd_req_t* req); + static esp_err_t handleGetStatus (httpd_req_t* req); + static esp_err_t handleStartScan (httpd_req_t* req); + static esp_err_t handleGetScan (httpd_req_t* req); + static esp_err_t handleReboot (httpd_req_t* req); +}; diff --git a/components/beacon_device/http/api/HardwareConfigApi.cpp b/components/beacon_device/http/api/HardwareConfigApi.cpp new file mode 100644 index 0000000..f1a0af6 --- /dev/null +++ b/components/beacon_device/http/api/HardwareConfigApi.cpp @@ -0,0 +1,143 @@ +#include "http/api/HardwareConfigApi.hpp" +#include "http/IHttpDaemon.hpp" +#include "cJSON.h" +#include "esp_log.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include + +static constexpr char TAG[] = "HardwareConfigApi"; + +static esp_err_t sendJson(httpd_req_t* req, cJSON* root) +{ + char* body = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (!body) return httpd_resp_send_500(req); + httpd_resp_set_type(req, "application/json"); + esp_err_t err = httpd_resp_sendstr(req, body); + free(body); + return err; +} + +// ── Construction + registration ─────────────────────────────────────────────── + +HardwareConfigApi::HardwareConfigApi(HardwareConfig& config) + : _ctx{config} {} + +void HardwareConfigApi::registerOn(IHttpDaemon& server) +{ + server.registerHandler("/api/hardware", HTTP_GET, handleGet, &_ctx); + server.registerHandler("/api/hardware", HTTP_POST, handlePost, &_ctx); +} + +// ── GET /api/hardware ───────────────────────────────────────────────────────── + +esp_err_t HardwareConfigApi::handleGet(httpd_req_t* req) +{ + auto* ctx = static_cast(req->user_ctx); + + cJSON* root = cJSON_CreateObject(); + cJSON* sectionsArr = cJSON_AddArrayToObject(root, "sections"); + + for (const auto& section : ctx->config.sections()) { + cJSON* sObj = cJSON_CreateObject(); + cJSON* fieldsArr = cJSON_AddArrayToObject(sObj, "fields"); + cJSON_AddStringToObject(sObj, "title", section->title()); + for (const auto* field : section->allFields()) { + field->addToJson(fieldsArr); + } + cJSON_AddItemToArray(sectionsArr, sObj); + } + + return sendJson(req, root); +} + +// ── POST /api/hardware ──────────────────────────────────────────────────────── + +esp_err_t HardwareConfigApi::handlePost(httpd_req_t* req) +{ + auto* ctx = static_cast(req->user_ctx); + + if (req->content_len <= 0 || req->content_len > 2048) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid body size"); + return ESP_FAIL; + } + + char* body = static_cast(malloc(req->content_len + 1)); + if (!body) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); + return ESP_FAIL; + } + + int received = httpd_req_recv(req, body, req->content_len); + if (received <= 0) { + free(body); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); + return ESP_FAIL; + } + body[received] = '\0'; + + cJSON* root = cJSON_Parse(body); + free(body); + if (!root) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + return ESP_FAIL; + } + + // Pass 1: validate all recognised fields before applying any + bool valid = true; + cJSON* item; + cJSON_ArrayForEach(item, root) { + if (!item->string) continue; + HardwareFieldBase* field = ctx->config.findField(item->string); + if (!field) continue; // unknown keys are silently ignored + if (!cJSON_IsNumber(item) && !cJSON_IsBool(item)) { + ESP_LOGW(TAG, "Field '%s': expected number or bool", item->string); + valid = false; + break; + } + int32_t val = cJSON_IsBool(item) + ? (cJSON_IsTrue(item) ? 1 : 0) + : static_cast(item->valuedouble); + if (!field->validate(val)) { + ESP_LOGW(TAG, "Field '%s': value %d failed validation", item->string, (int)val); + valid = false; + break; + } + } + + if (!valid) { + cJSON_Delete(root); + cJSON* resp = cJSON_CreateObject(); + cJSON_AddBoolToObject(resp, "ok", false); + return sendJson(req, resp); + } + + // Pass 2: apply + cJSON_ArrayForEach(item, root) { + if (!item->string) continue; + HardwareFieldBase* field = ctx->config.findField(item->string); + if (!field) continue; + int32_t val = cJSON_IsBool(item) + ? (cJSON_IsTrue(item) ? 1 : 0) + : static_cast(item->valuedouble); + field->setRaw(val); + } + + cJSON_Delete(root); + + bool saved = ctx->config.save(); + if (!saved) ESP_LOGW(TAG, "Failed to save hardware config to NVS"); + + cJSON* resp = cJSON_CreateObject(); + cJSON_AddBoolToObject(resp, "ok", saved); + sendJson(req, resp); + + if (saved) { + vTaskDelay(pdMS_TO_TICKS(200)); + esp_restart(); + } + + return ESP_OK; +} diff --git a/components/beacon_device/http/api/HardwareConfigApi.hpp b/components/beacon_device/http/api/HardwareConfigApi.hpp new file mode 100644 index 0000000..9397af5 --- /dev/null +++ b/components/beacon_device/http/api/HardwareConfigApi.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "http/IHttpApi.hpp" +#include "hardware/HardwareConfig.hpp" +#include "esp_http_server.h" + +class HardwareConfigApi : public IHttpApi { +public: + explicit HardwareConfigApi(HardwareConfig& config); + void registerOn(IHttpDaemon& server) override; + +private: + struct Ctx { + HardwareConfig& config; + }; + + Ctx _ctx; + + static esp_err_t handleGet (httpd_req_t* req); + static esp_err_t handlePost(httpd_req_t* req); +}; diff --git a/components/beacon_device/http/api/StaticAssetApi.cpp b/components/beacon_device/http/api/StaticAssetApi.cpp new file mode 100644 index 0000000..ee374a9 --- /dev/null +++ b/components/beacon_device/http/api/StaticAssetApi.cpp @@ -0,0 +1,44 @@ +#include "http/api/StaticAssetApi.hpp" +#include "http/IHttpDaemon.hpp" + +extern const uint8_t ui_index_html_start[] asm("_binary_index_html_start"); +extern const uint8_t ui_index_html_end[] asm("_binary_index_html_end"); +extern const uint8_t ui_style_css_start[] asm("_binary_style_css_start"); +extern const uint8_t ui_style_css_end[] asm("_binary_style_css_end"); +extern const uint8_t ui_app_js_start[] asm("_binary_app_js_start"); +extern const uint8_t ui_app_js_end[] asm("_binary_app_js_end"); + +static esp_err_t sendAsset(httpd_req_t* req, + const uint8_t* start, const uint8_t* end, + const char* contentType) +{ + httpd_resp_set_type(req, contentType); + size_t size = end - start; + if (size > 0 && start[size - 1] == '\0') size--; + return httpd_resp_send(req, reinterpret_cast(start), size); +} + +void StaticAssetApi::registerOn(IHttpDaemon& server) +{ + server.registerHandler("/", HTTP_GET, handleRoot, nullptr); + server.registerHandler("/style.css", HTTP_GET, handleCss, nullptr); + server.registerHandler("/app.js", HTTP_GET, handleJs, nullptr); +} + +esp_err_t StaticAssetApi::handleRoot(httpd_req_t* req) +{ + return sendAsset(req, ui_index_html_start, ui_index_html_end, + "text/html; charset=utf-8"); +} + +esp_err_t StaticAssetApi::handleCss(httpd_req_t* req) +{ + return sendAsset(req, ui_style_css_start, ui_style_css_end, + "text/css; charset=utf-8"); +} + +esp_err_t StaticAssetApi::handleJs(httpd_req_t* req) +{ + return sendAsset(req, ui_app_js_start, ui_app_js_end, + "application/javascript; charset=utf-8"); +} diff --git a/components/beacon_device/http/api/StaticAssetApi.hpp b/components/beacon_device/http/api/StaticAssetApi.hpp new file mode 100644 index 0000000..33168ae --- /dev/null +++ b/components/beacon_device/http/api/StaticAssetApi.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "http/IHttpApi.hpp" +#include "esp_http_server.h" + +class StaticAssetApi : public IHttpApi { +public: + void registerOn(IHttpDaemon& server) override; + +private: + static esp_err_t handleRoot(httpd_req_t* req); + static esp_err_t handleCss (httpd_req_t* req); + static esp_err_t handleJs (httpd_req_t* req); +}; diff --git a/components/beacon_device/httpServer/EspHttpServer.hpp b/components/beacon_device/httpServer/EspHttpServer.hpp deleted file mode 100644 index cb34c6c..0000000 --- a/components/beacon_device/httpServer/EspHttpServer.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "esp_http_server.h" -#include - -class EspHttpServer { -public: - EspHttpServer() = default; - ~EspHttpServer() { stop(); } - - void start(uint16_t port = 80); - void stop(); - bool isRunning() const; - - void registerHandler(const char* uri, - httpd_method_t method, - esp_err_t (*handler)(httpd_req_t*), - void* ctx = nullptr); - -private: - static constexpr char TAG[] = "EspHttpServer"; - static constexpr int MAX_URI_HANDLERS = 12; - - httpd_handle_t _server = nullptr; -}; diff --git a/components/beacon_device/httpServer/HttpHandlers.hpp b/components/beacon_device/httpServer/HttpHandlers.hpp deleted file mode 100644 index e08c048..0000000 --- a/components/beacon_device/httpServer/HttpHandlers.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "esp_http_server.h" -#include "config/Config.hpp" -#include "types/DeviceTypes.hpp" -#include "networkConnection/INetworkConnection.hpp" -#include "beaconConnection/IBeaconConnection.hpp" - -struct HttpCtx { - Config& config; - const DeviceInfo& deviceInfo; - INetworkConnection& network; - IBeaconConnection& beacon; -}; - -namespace HttpHandlers { - - // Static assets - esp_err_t handleRoot(httpd_req_t* req); - esp_err_t handleCss (httpd_req_t* req); - esp_err_t handleJs (httpd_req_t* req); - - // API - esp_err_t handleGetConfig (httpd_req_t* req); // GET /api/config - esp_err_t handleSetConfig (httpd_req_t* req); // POST /api/config - esp_err_t handleGetStatus (httpd_req_t* req); // GET /api/status - esp_err_t handleStartScan (httpd_req_t* req); // POST /api/scan/start - esp_err_t handleGetScan (httpd_req_t* req); // GET /api/scan - esp_err_t handleReboot (httpd_req_t* req); // POST /api/reboot - -} diff --git a/components/beacon_device/orchestrator/IOrchestrator.hpp b/components/beacon_device/orchestrator/IOrchestrator.hpp index 8913f9e..8869bee 100644 --- a/components/beacon_device/orchestrator/IOrchestrator.hpp +++ b/components/beacon_device/orchestrator/IOrchestrator.hpp @@ -1,13 +1,10 @@ #pragma once #include "config/Config.hpp" -#include "config/ISettingsStore.hpp" #include "types/DeviceTypes.hpp" #include "networkConnection/INetworkConnection.hpp" #include "beaconConnection/IBeaconConnection.hpp" #include "consumer/IConsumer.hpp" -#include "httpServer/EspHttpServer.hpp" -#include "httpServer/HttpHandlers.hpp" #include "orchestrator/handlers/TallyHandler.hpp" #include @@ -16,21 +13,16 @@ class IOrchestrator { public: static constexpr uint8_t MAX_CONSUMERS = 8; - IOrchestrator( ISettingsStore& store, - const DeviceConfig& config, - INetworkConnection& network, - IBeaconConnection& beacon, - TallyHandler& tallyHandler, - EspHttpServer& http, - const Settings& defaults = Settings{} - ) : - _config(store, defaults) - , _deviceInfo(config) + IOrchestrator(Config& config, + const DeviceConfig& deviceConfig, + INetworkConnection& network, + IBeaconConnection& beacon, + TallyHandler& tallyHandler) + : _config(config) + , _deviceInfo(deviceConfig) , _network(network) , _beacon(beacon) , _tallyHandler(tallyHandler) - , _http(http) - , _httpCtx{_config, _deviceInfo, _network, _beacon} { } @@ -52,6 +44,8 @@ class IOrchestrator { _tallyHandler.addConsumer(consumer); } + DeviceInfo& getDeviceInfo() { return _deviceInfo; } + void start(); virtual void stop() = 0; @@ -60,18 +54,14 @@ class IOrchestrator { static constexpr uint32_t STARTUP_STACK_SIZE = 8192; virtual void doStart() = 0; - - Config _config; + + Config& _config; DeviceInfo _deviceInfo; INetworkConnection& _network; IBeaconConnection& _beacon; IConsumer* _consumers[MAX_CONSUMERS] = {}; uint8_t _consumerCount = 0; TallyHandler& _tallyHandler; - EspHttpServer& _http; - - // TODO Move to UI class. - HttpCtx _httpCtx; // TODO Check if needed, or should be stored in the INetworkConnection implementation. Some devices may not have an IP. NetworkStatus _networkStatus = NetworkStatus::DISCONNECTED; diff --git a/components/beacon_device/orchestrator/NodeOrchestrator.hpp b/components/beacon_device/orchestrator/NodeOrchestrator.hpp index d1ebf42..faffdb5 100644 --- a/components/beacon_device/orchestrator/NodeOrchestrator.hpp +++ b/components/beacon_device/orchestrator/NodeOrchestrator.hpp @@ -6,6 +6,8 @@ // Each consumer has its own MQTT subscription and displays an independent tally state. class NodeOrchestrator : public IOrchestrator { public: + using IOrchestrator::IOrchestrator; + void stop() override {} protected: diff --git a/components/beacon_device/orchestrator/SateliteOrchestrator.cpp b/components/beacon_device/orchestrator/SateliteOrchestrator.cpp index 7a320bb..a40b074 100644 --- a/components/beacon_device/orchestrator/SateliteOrchestrator.cpp +++ b/components/beacon_device/orchestrator/SateliteOrchestrator.cpp @@ -1,6 +1,7 @@ #include "orchestrator/SateliteOrchestrator.hpp" #include "networkConnection/IWifiConnection.hpp" #include "consumer/IDisplayConsumer.hpp" +#include "config/Config.hpp" #include "esp_log.h" #include "esp_mac.h" @@ -51,16 +52,12 @@ void SateliteOrchestrator::doStart() _config.load(); - _http.start(); - registerHttpHandlers(); // TODO Build context. - ESP_LOGI(TAG, "Started"); ESP_LOGI("main", "Stack HWM: %lu bytes free", uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t)); } void SateliteOrchestrator::stop() { - _http.stop(); _beacon.stop(); _network.stop(); ESP_LOGI(TAG, "Stopped"); @@ -262,18 +259,3 @@ void SateliteOrchestrator::discoveryTimerCallback(void* arg) self->_beacon.publishDiscovery(self->buildDiscoveryPacket()); } -// ? HTTP -// TODO Handled here? - -void SateliteOrchestrator::registerHttpHandlers() -{ - _http.registerHandler("/", HTTP_GET, HttpHandlers::handleRoot, nullptr); - _http.registerHandler("/style.css", HTTP_GET, HttpHandlers::handleCss, nullptr); - _http.registerHandler("/app.js", HTTP_GET, HttpHandlers::handleJs, nullptr); - _http.registerHandler("/api/config", HTTP_GET, HttpHandlers::handleGetConfig, &_httpCtx); - _http.registerHandler("/api/config", HTTP_POST, HttpHandlers::handleSetConfig, &_httpCtx); - _http.registerHandler("/api/status", HTTP_GET, HttpHandlers::handleGetStatus, &_httpCtx); - _http.registerHandler("/api/scan/start", HTTP_POST, HttpHandlers::handleStartScan, &_httpCtx); - _http.registerHandler("/api/scan", HTTP_GET, HttpHandlers::handleGetScan, &_httpCtx); - _http.registerHandler("/api/reboot", HTTP_POST, HttpHandlers::handleReboot, nullptr); -} diff --git a/components/beacon_device/orchestrator/SateliteOrchestrator.hpp b/components/beacon_device/orchestrator/SateliteOrchestrator.hpp index 805e827..a10d4bc 100644 --- a/components/beacon_device/orchestrator/SateliteOrchestrator.hpp +++ b/components/beacon_device/orchestrator/SateliteOrchestrator.hpp @@ -5,16 +5,12 @@ class SateliteOrchestrator : public IOrchestrator { public: - SateliteOrchestrator( - ISettingsStore& store, - const DeviceConfig& config, - INetworkConnection& network, - IBeaconConnection& beacon, - TallyHandler& tallyHandler, - EspHttpServer& http, - const Settings& defaults = Settings{} - ) - : IOrchestrator(store, config, network, beacon, tallyHandler, http, defaults) + SateliteOrchestrator(Config& config, + const DeviceConfig& deviceConfig, + INetworkConnection& network, + IBeaconConnection& beacon, + TallyHandler& tallyHandler) + : IOrchestrator(config, deviceConfig, network, beacon, tallyHandler) { _deviceInfo.deviceType = DeviceType::SINGLE_TOPIC; } @@ -25,8 +21,7 @@ class SateliteOrchestrator : public IOrchestrator { void doStart() override; private: - - BeaconStatus _beaconStatus = BeaconStatus::DISCONNECTED; // TODO Add UI and check if it needs to be stored here at all. + BeaconStatus _beaconStatus = BeaconStatus::DISCONNECTED; esp_timer_handle_t _discoveryTimer = nullptr; bool _isRegistered = false; @@ -46,6 +41,4 @@ class SateliteOrchestrator : public IOrchestrator { void stopDiscoveryTimer(); static void discoveryTimerCallback(void* arg); DiscoveryPacket buildDiscoveryPacket() const; - - void registerHttpHandlers(); }; diff --git a/components/beacon_device/ui/app.js b/components/beacon_device/ui/app.js index b503257..577ea5b 100644 --- a/components/beacon_device/ui/app.js +++ b/components/beacon_device/ui/app.js @@ -96,22 +96,52 @@ async function saveChanges() { setSaving(true) elSaveBtn.disabled = elDiscardBtn.disabled = true + const hwIds = [...changed].filter(isHwField) + const configIds = [...changed].filter(id => !isHwField(id)) + try { - const res = await fetchWithTimeout('/api/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(buildPayload()), - }) - if (!res.ok) throw new Error() + if (configIds.length > 0) { + const res = await fetchWithTimeout('/api/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(buildPayload()), + }) + if (!res.ok) throw new Error() + + configIds.forEach(id => { + const row = document.querySelector(`.s-row[data-field="${id}"]`) + if (!row) return + originals[id] = getCurrentValue(row) + row.classList.remove('changed') + getVisualInput(row)?.classList.remove('changed') + row.querySelector('.radio-group')?.classList.remove('changed') + changed.delete(id) + }) + updateSavebar() + } - document.querySelectorAll('.s-row[data-field]').forEach(row => { - originals[row.dataset.field] = getCurrentValue(row) - row.classList.remove('changed') - getVisualInput(row)?.classList.remove('changed') - row.querySelector('.radio-group')?.classList.remove('changed') - }) - changed.clear() - updateSavebar() + if (hwIds.length > 0) { + let rebooting = false + try { + const res = await fetchWithTimeout('/api/hardware', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(buildHardwarePayload(hwIds)), + }, 8000) + if (res.ok) { + const data = await res.json().catch(() => null) + rebooting = data?.ok === true + } + } catch { + // Network/timeout error: device likely received the request and is rebooting + rebooting = true + } + if (!rebooting) throw new Error() + elSaveBtn.innerHTML = 'Rebooting…' + elSaveBtn.disabled = elDiscardBtn.disabled = true + pollUntilBack() + return + } } catch { showSaveError() } @@ -468,6 +498,98 @@ function applyConfig(cfg) { setRuntimeFields(cfg.runtime) } +// ── Hardware config ──────────────────────────────────────────────────────────── + +function isHwField(id) { + return document.querySelector(`.s-row[data-field="${id}"][data-hw]`) !== null +} + +function renderHwField(field) { + const row = document.createElement('div') + row.className = 's-row' + row.dataset.field = field.key + row.dataset.hw = 'true' + row.dataset.reboot = 'true' + row.dataset.hwType = field.type + + let control = '' + if (field.type === 'int') { + control = `` + } else if (field.type === 'bool') { + control = `` + } else if (field.type === 'select') { + const opts = field.options.map(o => + `` + ).join('') + control = `` + } else if (field.type === 'gpio') { + const opts = field.available.map(pin => + `` + ).join('') + control = `` + } + + row.innerHTML = ` +
${field.label}
+
+ Reboot + ${control} +
` + + return row +} + +function renderHwSections(data) { + const container = document.getElementById('hw-sections') + if (!container || !Array.isArray(data.sections)) return + + data.sections.forEach(section => { + const lbl = document.createElement('div') + lbl.className = 'sec-lbl' + lbl.textContent = section.title + container.appendChild(lbl) + + const card = document.createElement('div') + card.className = 's-card' + section.fields.forEach(field => card.appendChild(renderHwField(field))) + container.appendChild(card) + }) +} + +async function loadHardwareConfig() { + try { + const res = await fetchWithTimeout('/api/hardware') + if (res.ok) renderHwSections(await res.json()) + } catch {} +} + +function pollUntilBack() { + const attempt = async () => { + try { + const res = await fetchWithTimeout('/api/status', {}, 2000) + if (res.ok) { location.reload(); return } + } catch {} + setTimeout(attempt, 2000) + } + setTimeout(attempt, 3000) +} + +function buildHardwarePayload(hwIds) { + const payload = {} + hwIds.forEach(id => { + const row = document.querySelector(`.s-row[data-field="${id}"]`) + if (!row) return + const val = getCurrentValue(row) + payload[id] = row.dataset.hwType === 'bool' ? Boolean(val) : Number(val) + }) + return payload +} + // ── Init ─────────────────────────────────────────────────────────────────────── function init() { @@ -546,6 +668,7 @@ function init() { document.addEventListener('DOMContentLoaded', async () => { await loadConfig() + await loadHardwareConfig() init() pollStatus() }) diff --git a/components/beacon_device/ui/index.html b/components/beacon_device/ui/index.html index 51dd19f..a809e4e 100644 --- a/components/beacon_device/ui/index.html +++ b/components/beacon_device/ui/index.html @@ -278,6 +278,9 @@

Beacon Satellite

+ +
+ diff --git a/satelites/CYD/CYD-Satelite/main/main.cpp b/satelites/CYD/CYD-Satelite/main/main.cpp index aee8561..dfb7709 100644 --- a/satelites/CYD/CYD-Satelite/main/main.cpp +++ b/satelites/CYD/CYD-Satelite/main/main.cpp @@ -8,6 +8,10 @@ #include "platform/Platform.hpp" #include "config/NvsSettingsStore.hpp" +#include "config/NvsHardwareStore.hpp" +#include "config/Config.hpp" +#include "hardware/HardwareConfig.hpp" +#include "http/api/HardwareConfigApi.hpp" #include "types/DeviceTypes.hpp" #include "networkConnection/StaWifiConnection.hpp" #include "beaconConnection/TcpMqttBeaconConnection.hpp" @@ -15,7 +19,7 @@ #include "consumer/WS2812Consumer.hpp" #include "consumer/display/Ili9341LvglDisplayConsumer.hpp" #include "consumer/IDisplayConsumer.hpp" -#include "httpServer/EspHttpServer.hpp" +#include "http/BeaconHttpServer.hpp" #include "mapper/FixedTallyColorMapper.hpp" #include "orchestrator/handlers/TallyHandler.hpp" @@ -29,13 +33,12 @@ #define FIX_LED_G_GPIO GPIO_NUM_16 #define FIX_LED_B_GPIO GPIO_NUM_17 #define ADD_LED_STRIP_GPIO 22 -#define ADD_LED_STRIP_LED_NUMBER 64 -static led_strip_handle_t createLedStrip() +static led_strip_handle_t createLedStrip(gpio_num_t pin, uint8_t ledCount = 64) { led_strip_config_t stripCfg = { - ADD_LED_STRIP_GPIO, - ADD_LED_STRIP_LED_NUMBER, + pin, + ledCount, LED_MODEL_WS2812, LED_STRIP_COLOR_COMPONENT_FMT_GRB, { false }, @@ -59,39 +62,48 @@ static led_strip_handle_t createLedStrip() extern "C" void app_main() { vTaskDelay(pdMS_TO_TICKS(100)); - + Platform::init(); - + + //? Hardware confg + auto* hwStore = new NvsHardwareStore(); + auto* hwConfig = new HardwareConfig(*hwStore); + + auto& addressableLed = hwConfig->addSection("WS2812 Strip"); + auto& ledEnabled = addressableLed.addBool("addr_enabled", "Enabled", {true}); + auto& ledCount = addressableLed.addInt ("addr_led_count", "LED Count", {64, 1, 255}); + static const gpio_num_t allowed[] = { GPIO_NUM_22, GPIO_NUM_27 }; + auto& stripPin = addressableLed.addGpio("addr_strip_pin", "Strip Pin", {GPIO_NUM_22, nullptr, 0, allowed, 2}); + + hwConfig->load(); + + + //? Config + Settings defaults; + defaults.display.brightness_trim[0] = 127; // ILI9341 display + defaults.display.brightness_trim[1] = 0; // ws2812 strip + defaults.display.brightness_trim[2] = 0; // RGB LED ISettingsStore* settingsStore = new NvsSettingsStore(); + Config* config = new Config(*settingsStore, defaults); + //? Network Config DeviceConfig* deviceConfig = new DeviceConfig{ .model = "CYD Satellite", }; INetworkConnection* network = new StaWifiConnection(*deviceConfig); - IBeaconConnection* beacon = new TcpMqttBeaconConnection(); - EspHttpServer* httpServer = new EspHttpServer(); + //? Orchestration IColorAlertPatternMap* colorAlertPatternMap = new DefaultColorAlertPatternMap(); - TallyHandler* tallyHandler = new TallyHandler(*colorAlertPatternMap); - Settings defaults; - defaults.display.brightness_trim[0] = 127; // ILI9341 display - defaults.display.brightness_trim[1] = 0; // ws2812 strip - defaults.display.brightness_trim[2] = 0; // RGB LED - - SateliteOrchestrator* orchestrator = new SateliteOrchestrator(*settingsStore, *deviceConfig, *network, *beacon, *tallyHandler, *httpServer, defaults); + SateliteOrchestrator* orchestrator = new SateliteOrchestrator(*config, *deviceConfig, *network, *beacon, *tallyHandler); - - //? Consumers - ITallyColorMapper* colorMapper = new FixedTallyColorMapper(); - static const IDisplayConsumer::Zone dispZones[] = { { 0, 0, 320, 240, 0, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // background (always visible) @@ -102,27 +114,34 @@ extern "C" void app_main() { 160, 0, 120, 10, 2, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // right bottom { 160, 230, 120, 10, 2, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // left side }; - - LV_FONT_DECLARE(helvatica_140); - - static const ILvglDisplayConsumer::FixedTextConfig text0 { &helvatica_140, 255, LV_ALIGN_CENTER, 0, 0 }; - static const ILvglDisplayConsumer::FixedTextConfig text1 { &lv_font_montserrat_28, 80, LV_ALIGN_CENTER, 0, 70 }; + // LV_FONT_DECLARE(helvatica_140); + static const ILvglDisplayConsumer::AutoTextConfig text0 { 255, LV_ALIGN_CENTER, 0, 0, 2, 60, 0, 300, 190, true }; + static const ILvglDisplayConsumer::FixedTextConfig text1 { &lv_font_montserrat_28, 80, LV_ALIGN_CENTER, 0, 80 }; static const ILvglDisplayConsumer::TextConfig* const textConf[] = { &text0, &text1 }; + IConsumer* consumer1 = new Ili9341LvglDisplayConsumer(*colorMapper, dispZones, 7, textConf, 2); orchestrator->addConsumer(consumer1, "Front Display"); + if (ledEnabled.value() && stripPin.value() != GPIO_NUM_NC) { - static StripZone ws2812Sections[] = { - { 0, 1, DeviceAlertTarget::OPERATOR }, - { 24, 0, DeviceAlertTarget::OPERATOR }, - { 40, 2, DeviceAlertTarget::OPERATOR }, - }; - IConsumer* consumer2 = new WS2812Consumer(*colorMapper, createLedStrip(), ADD_LED_STRIP_LED_NUMBER, ws2812Sections, 3); - orchestrator->addConsumer(consumer2, "Back Panel"); + static StripZone ws2812Sections[] = { + { 0, 1, DeviceAlertTarget::OPERATOR }, + { 24, 0, DeviceAlertTarget::OPERATOR }, + { 40, 2, DeviceAlertTarget::OPERATOR }, + }; + IConsumer* consumer2 = new WS2812Consumer(*colorMapper, createLedStrip(stripPin.value(), ledCount.value()), ledCount.value(), ws2812Sections, 3); + orchestrator->addConsumer(consumer2, "Back Panel"); + + } IConsumer* consumer3 = new SimpleRGBConsumer(*colorMapper, FIX_LED_R_GPIO, FIX_LED_G_GPIO, FIX_LED_B_GPIO, DeviceAlertTarget::OPERATOR); orchestrator->addConsumer(consumer3, "Side Strip"); + BeaconHttpServer* http = new BeaconHttpServer(*config, orchestrator->getDeviceInfo(), *network, *beacon); + auto* hwApi = new HardwareConfigApi(*hwConfig); + http->addApi(*hwApi); + http->start(); + orchestrator->start(); } diff --git a/satelites/Huidu/HD-WF2/main/main.cpp b/satelites/Huidu/HD-WF2/main/main.cpp index 683102d..a99fcfb 100644 --- a/satelites/Huidu/HD-WF2/main/main.cpp +++ b/satelites/Huidu/HD-WF2/main/main.cpp @@ -8,13 +8,17 @@ #include "platform/Platform.hpp" #include "config/NvsSettingsStore.hpp" +#include "config/NvsHardwareStore.hpp" +#include "hardware/HardwareConfig.hpp" +#include "http/api/HardwareConfigApi.hpp" #include "types/DeviceTypes.hpp" #include "networkConnection/StaWifiConnection.hpp" #include "beaconConnection/TcpMqttBeaconConnection.hpp" #include "consumer/display/Hub75LvglDisplayConsumer.hpp" #include "consumer/WS2812Consumer.hpp" #include "consumer/IDisplayConsumer.hpp" -#include "httpServer/EspHttpServer.hpp" +#include "config/Config.hpp" +#include "http/BeaconHttpServer.hpp" #include "mapper/FixedTallyColorMapper.hpp" @@ -25,14 +29,13 @@ static const char *const TAG = "HD-WF2 Satelite"; -#define ADD_LED_STRIP_GPIO 8 -#define ADD_LED_STRIP_LED_NUMBER 6 +#define LED_COUNT 6 -static led_strip_handle_t createLedStrip() +static led_strip_handle_t createLedStrip(gpio_num_t gpio, uint32_t count) { led_strip_config_t stripCfg = { - ADD_LED_STRIP_GPIO, - ADD_LED_STRIP_LED_NUMBER, + gpio, + count, LED_MODEL_WS2812, LED_STRIP_COLOR_COMPONENT_FMT_GRB, { false }, @@ -105,40 +108,48 @@ static inline Hub75Config getHub75Config() { extern "C" void app_main() { - ESP_LOGI(TAG, "HUB75 LVGL Simple Demo Starting..."); vTaskDelay(pdMS_TO_TICKS(100)); Platform::init(); - - Hub75Config hub75Config = getHub75Config(); + //? Hardware confg + auto* hwStore = new NvsHardwareStore(); + auto* hwConfig = new HardwareConfig(*hwStore); + + auto& addressableLed = hwConfig->addSection("WS2812 Strip"); + auto& ledEnabled = addressableLed.addBool("addr_enabled", "Enabled", {true}); + static const gpio_num_t allowed[] = { GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_8, GPIO_NUM_9, GPIO_NUM_12, GPIO_NUM_13 }; + auto& stripPin = addressableLed.addGpio("addr_strip_pin", "Strip Pin", {GPIO_NUM_8, nullptr, 0, allowed, 6}); + + hwConfig->load(); + + + //? Config + Settings defaults; + defaults.display.brightness_trim[0] = 0; // hub75 display + defaults.display.brightness_trim[1] = 25; // ws2812 strip ISettingsStore* settingsStore = new NvsSettingsStore(); + Config* config = new Config(*settingsStore, defaults); + + //? Network Config DeviceConfig* deviceConfig = new DeviceConfig{ .model = "HD-WF2 Satellite", }; INetworkConnection* network = new StaWifiConnection(*deviceConfig); - IBeaconConnection* beacon = new TcpMqttBeaconConnection(); - EspHttpServer* httpServer = new EspHttpServer(); - + //? Orchestration IColorAlertPatternMap* colorAlertPatternMap = new DefaultColorAlertPatternMap(); - TallyHandler* tallyHandler = new TallyHandler(*colorAlertPatternMap); - Settings defaults; - defaults.display.brightness_trim[0] = 0; // hub75 display - defaults.display.brightness_trim[1] = 25; // ws2812 strip - - SateliteOrchestrator* orchestrator = new SateliteOrchestrator(*settingsStore, *deviceConfig, *network, *beacon, *tallyHandler, *httpServer, defaults); + SateliteOrchestrator* orchestrator = new SateliteOrchestrator(*config, *deviceConfig, *network, *beacon, *tallyHandler); - //? Consumers - + //? Consumer 1 ITallyColorMapper* colorMapper = new FixedTallyColorMapper(); static const IDisplayConsumer::Zone dispZones[] = { @@ -152,36 +163,38 @@ extern "C" void app_main() { { 54, 3, 10, 26, 2, DeviceAlertTarget::TALENT, TallyState::PROGRAM, true }, // Right Bar { 10, 3, 44, 26, 0, DeviceAlertTarget::TALENT, TallyState::PROGRAM, true }, // Center }; - // static const IDisplayConsumer::Zone hub75Zones[] = { - // { 0, 0, 10, 3, 1, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // Top Left - // { 10, 0, 44, 3, 0, DeviceAlertTarget::TALENT, TallyState::PROGRAM, true }, // Top Bar - // { 54, 0, 10, 3, 2, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // Top Right - // { 0, 29, 10, 3, 1, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // Bottom Left - // { 10, 29, 44, 3, 0, DeviceAlertTarget::TALENT, TallyState::PROGRAM, true }, // Bottom Bar - // { 54, 29, 10, 3, 2, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // Bottom Right - // { 0, 3, 10, 26, 1, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // Left Bar - // { 54, 3, 10, 26, 2, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // Right Bar - // { 10, 3, 44, 26, 0, DeviceAlertTarget::TALENT, TallyState::PROGRAM, true }, // Center - // }; - // static const ILvglDisplayConsumer::FixedTextConfig text0 { &lv_font_montserrat_32, 255, LV_ALIGN_CENTER, 0, 0, 1 }; static const ILvglDisplayConsumer::AutoTextConfig text0 { 255, LV_ALIGN_CENTER, 0, -1, 1, 12, 0, 62, 30, false }; static const ILvglDisplayConsumer::TextConfig* const textConf[] = { &text0 }; + + Hub75Config hub75Config = getHub75Config(); IConsumer* consumer1 = new Hub75LvglDisplayConsumer(*colorMapper, hub75Config, dispZones, 9, textConf, 1); orchestrator->addConsumer(consumer1, "Front Display"); - static StripZone ws2812Sections[] = { - { 0, 0, DeviceAlertTarget::OPERATOR }, - { 1, 2, DeviceAlertTarget::OPERATOR }, - { 2, 0, DeviceAlertTarget::OPERATOR }, - { 3, 0, DeviceAlertTarget::OPERATOR }, - { 4, 1, DeviceAlertTarget::OPERATOR }, - { 5, 0, DeviceAlertTarget::OPERATOR }, - }; - IConsumer* consumer2 = new WS2812Consumer(*colorMapper, createLedStrip(), ADD_LED_STRIP_LED_NUMBER, ws2812Sections, 6); + + //? Consumer 2 + if (ledEnabled.value() && stripPin.value() != GPIO_NUM_NC) + { + static StripZone ws2812Sections[] = { + { 0, 0, DeviceAlertTarget::OPERATOR }, + { 1, 2, DeviceAlertTarget::OPERATOR }, + { 2, 0, DeviceAlertTarget::OPERATOR }, + { 3, 0, DeviceAlertTarget::OPERATOR }, + { 4, 1, DeviceAlertTarget::OPERATOR }, + { 5, 0, DeviceAlertTarget::OPERATOR }, + }; + IConsumer* consumer2 = new WS2812Consumer(*colorMapper, createLedStrip(stripPin.value(), LED_COUNT), LED_COUNT, ws2812Sections, 6); + + orchestrator->addConsumer(consumer2, "Back Lights"); + } - orchestrator->addConsumer(consumer2, "Back Lights"); + // Http Server + BeaconHttpServer* http = new BeaconHttpServer(*config, orchestrator->getDeviceInfo(), *network, *beacon); + auto* hwApi = new HardwareConfigApi(*hwConfig); + http->addApi(*hwApi); + http->start(); + // Start Orchestrator orchestrator->start(); } \ No newline at end of file diff --git a/satelites/Synapt/Satelite-Pro/main/main.cpp b/satelites/Synapt/Satelite-Pro/main/main.cpp index 6f9f26d..622d3d7 100644 --- a/satelites/Synapt/Satelite-Pro/main/main.cpp +++ b/satelites/Synapt/Satelite-Pro/main/main.cpp @@ -6,10 +6,14 @@ #include "platform/Platform.hpp" #include "config/NvsSettingsStore.hpp" +#include "config/NvsHardwareStore.hpp" +#include "config/Config.hpp" +#include "hardware/HardwareConfig.hpp" +#include "http/api/HardwareConfigApi.hpp" #include "types/DeviceTypes.hpp" #include "networkConnection/StaWifiConnection.hpp" #include "beaconConnection/TcpMqttBeaconConnection.hpp" -#include "httpServer/EspHttpServer.hpp" +#include "http/BeaconHttpServer.hpp" #include "consumer/display/Hub75LvglDisplayConsumer.hpp" #include "orchestrator/SateliteOrchestrator.hpp" @@ -260,9 +264,21 @@ extern "C" void app_main() Platform::init(); + // ── Hardware config ─────────────────────────────────────────────────────── + // HUB75 signal pins are wired to a fixed PCB connector — not GPIO-configurable. + // Panel dimensions vary between panel models so are exposed here. + auto* hwStore = new NvsHardwareStore(); + auto* hwConfig = new HardwareConfig(*hwStore); + + auto& displaySection = hwConfig->addSection("HUB75 Display"); + auto& panelWidth = displaySection.addInt("panel_width", "Panel Width", {64, 1, 256}); + auto& panelHeight = displaySection.addInt("panel_height", "Panel Height", {32, 1, 256}); + + hwConfig->load(); + Hub75Config config = {}; - config.panel_width = 64; - config.panel_height = 32; + config.panel_width = static_cast(panelWidth.value()); + config.panel_height = static_cast(panelHeight.value()); config.scan_wiring = Hub75ScanWiring::STANDARD_TWO_SCAN; config.shift_driver = Hub75ShiftDriver::MBI5124; config.clk_phase_inverted = true; @@ -305,7 +321,6 @@ extern "C" void app_main() INetworkConnection* network = new StaWifiConnection(*deviceConfig); IBeaconConnection* beacon = new TcpMqttBeaconConnection(); - EspHttpServer* httpServer = new EspHttpServer(); static const IDisplayConsumer::Zone hub75Zones[] = { { 0, 0, 64, 32, 1, DeviceAlertTarget::TALENT, TallyState::NONE, true }, // background (always visible) @@ -314,12 +329,19 @@ extern "C" void app_main() static const ILvglDisplayConsumer::TextConfig* const hub75Text[] = { &hub75Text0 }; IConsumer* consumer1 = new Hub75LvglDisplayConsumer(config, hub75Zones, 1, hub75Text, 1); - IConsumer* consumers[] = { consumer1 }; IColorAlertPatternMap* colorAlertPatternMap = new DefaultColorAlertPatternMap(); TallyHandler* tallyHandler = new TallyHandler(*colorAlertPatternMap); - SateliteOrchestrator* orchestrator = new SateliteOrchestrator(*settingsStore, *deviceConfig, *network, *beacon, *tallyHandler, *httpServer); + Config* appConfig = new Config(*settingsStore); + + SateliteOrchestrator* orchestrator = new SateliteOrchestrator(*appConfig, *deviceConfig, *network, *beacon, *tallyHandler); orchestrator->addConsumer(consumer1, "Front Display"); + + BeaconHttpServer* http = new BeaconHttpServer(*appConfig, orchestrator->getDeviceInfo(), *network, *beacon); + auto* hwApi = new HardwareConfigApi(*hwConfig); + http->addApi(*hwApi); + http->start(); + orchestrator->start(); }