diff --git a/README.md b/README.md index b5f075d..868ddec 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway. - `gateway_core/`: boot profile and top-level role bootstrap. - `dali/`: vendored ESP-IDF DALI HAL/backend reused from LuatOS. - `dali_domain/`: native DALI domain facade over `dali_cpp`. + - `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`. + - `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out. - `gateway_runtime/`: persistent runtime state, command queueing, and device info services. ## Current status -The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port, and an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. \ No newline at end of file +The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app now also includes an initial `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications plus incoming `FFF1`/`FFF2`/`FFF3` writes into the native controller and DALI domain. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. \ No newline at end of file diff --git a/apps/gateway/main/CMakeLists.txt b/apps/gateway/main/CMakeLists.txt index c0992ea..d130ac1 100644 --- a/apps/gateway/main/CMakeLists.txt +++ b/apps/gateway/main/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( SRCS "app_main.cpp" - REQUIRES gateway_core dali_domain gateway_runtime log + REQUIRES gateway_core gateway_controller dali_domain gateway_runtime gateway_ble log ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 92fbd08..c9d3d39 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -1,10 +1,13 @@ #include "dali_domain.hpp" +#include "gateway_ble.hpp" +#include "gateway_controller.hpp" #include "gateway_core.hpp" #include "gateway_runtime.hpp" #include "sdkconfig.h" #include +#include #ifndef CONFIG_GATEWAY_CHANNEL_COUNT #define CONFIG_GATEWAY_CHANNEL_COUNT 2 @@ -14,6 +17,11 @@ namespace { constexpr const char* kProjectName = "DALI_485_Gateway"; constexpr const char* kProjectVersion = "0.1.0"; +std::unique_ptr s_dali_domain; +std::unique_ptr s_runtime; +std::unique_ptr s_controller; +std::unique_ptr s_ble_bridge; + [[maybe_unused]] void LogBindError(const char* channel_name, esp_err_t err) { if (err != ESP_OK) { std::printf("gateway_main: failed to bind %s err=%d\n", channel_name, err); @@ -99,24 +107,42 @@ extern "C" void app_main(void) { gateway::GatewayCore core(profile); core.start(); - gateway::DaliDomainService dali_domain; - gateway::GatewayRuntime runtime( + s_dali_domain = std::make_unique(); + s_runtime = std::make_unique( profile, gateway::GatewayRuntimeConfig{ kProjectName, kProjectVersion, gateway::ReadRuntimeSerialId(), }, - &dali_domain); - ESP_ERROR_CHECK(runtime.start()); - runtime.setGatewayCount(CONFIG_GATEWAY_CHANNEL_COUNT); - BindConfiguredChannels(dali_domain, runtime); + s_dali_domain.get()); + ESP_ERROR_CHECK(s_runtime->start()); + s_runtime->setGatewayCount(CONFIG_GATEWAY_CHANNEL_COUNT); + BindConfiguredChannels(*s_dali_domain, *s_runtime); - const auto device_info = runtime.deviceInfo(); + gateway::GatewayControllerConfig controller_config; + controller_config.setup_supported = true; + controller_config.ble_supported = profile.enable_ble; + controller_config.wifi_supported = profile.enable_wifi; + controller_config.ip_router_supported = profile.enable_wifi || profile.enable_eth; + controller_config.internal_scene_supported = true; + controller_config.internal_group_supported = true; + + s_controller = std::make_unique(*s_runtime, *s_dali_domain, + controller_config); + ESP_ERROR_CHECK(s_controller->start()); + + if (profile.enable_ble) { + s_ble_bridge = std::make_unique(*s_controller, *s_runtime, + *s_dali_domain); + ESP_ERROR_CHECK(s_ble_bridge->start()); + } + + const auto device_info = s_runtime->deviceInfo(); std::printf("gateway_main: dali domain implementation=%s bound=%d channels=%u\n", - dali_domain.implementationName(), dali_domain.isBound(), - static_cast(dali_domain.channelCount())); - for (const auto& channel : dali_domain.channelInfo()) { + s_dali_domain->implementationName(), s_dali_domain->isBound(), + static_cast(s_dali_domain->channelCount())); + for (const auto& channel : s_dali_domain->channelInfo()) { std::printf("gateway_main: channel=%u gateway=%u name=%s\n", channel.channel_index, channel.gateway_id, channel.name.c_str()); } diff --git a/components/dali_domain/include/dali_domain.hpp b/components/dali_domain/include/dali_domain.hpp index 37074e5..896c797 100644 --- a/components/dali_domain/include/dali_domain.hpp +++ b/components/dali_domain/include/dali_domain.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "esp_err.h" @@ -79,6 +80,9 @@ class DaliDomainService { std::vector channelInfo() const; bool resetBus(uint8_t gateway_id) const; + bool writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const; + std::vector transactBridgeFrame(uint8_t gateway_id, const uint8_t* data, + size_t len) const; bool sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const; bool sendExtRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const; std::optional queryRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const; @@ -90,6 +94,13 @@ class DaliDomainService { bool on(uint8_t gateway_id, int short_address) const; bool off(uint8_t gateway_id, int short_address) const; bool off(int short_address) const; + bool updateChannelName(uint8_t gateway_id, std::string_view name); + bool allocateAllAddr(uint8_t gateway_id, int start_address = 0) const; + void stopAllocAddr(uint8_t gateway_id) const; + bool resetAndAllocAddr(uint8_t gateway_id, int start_address = 0, + bool remove_addr_first = false, bool close_light = false) const; + bool isAllocAddr(uint8_t gateway_id) const; + int lastAllocAddr(uint8_t gateway_id) const; private: struct DaliChannel; diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index 4323c9c..c6dd05c 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -295,6 +295,21 @@ bool DaliDomainService::resetBus(uint8_t gateway_id) const { return channel != nullptr && channel->comm != nullptr && channel->comm->resetBus(); } +bool DaliDomainService::writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const { + const auto* channel = findChannelByGateway(gateway_id); + return channel != nullptr && channel->hooks.send && channel->hooks.send(data, len); +} + +std::vector DaliDomainService::transactBridgeFrame(uint8_t gateway_id, + const uint8_t* data, + size_t len) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || !channel->hooks.transact) { + return {}; + } + return channel->hooks.transact(data, len); +} + bool DaliDomainService::sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const { const auto* channel = findChannelByGateway(gateway_id); return channel != nullptr && channel->comm != nullptr && channel->comm->sendRawNew(raw_addr, command); @@ -363,6 +378,51 @@ bool DaliDomainService::off(int short_address) const { return off(channels_.front()->config.gateway_id, short_address); } +bool DaliDomainService::updateChannelName(uint8_t gateway_id, std::string_view name) { + auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr) { + return false; + } + channel->config.name = std::string(name); + if (channel->dali != nullptr) { + channel->dali->name = channel->config.name; + } + return true; +} + +bool DaliDomainService::allocateAllAddr(uint8_t gateway_id, int start_address) const { + const auto* channel = findChannelByGateway(gateway_id); + return channel != nullptr && channel->dali != nullptr && + channel->dali->addr.allocateAllAddr(start_address); +} + +void DaliDomainService::stopAllocAddr(uint8_t gateway_id) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel != nullptr && channel->dali != nullptr) { + channel->dali->addr.stopAllocAddr(); + } +} + +bool DaliDomainService::resetAndAllocAddr(uint8_t gateway_id, int start_address, + bool remove_addr_first, bool close_light) const { + const auto* channel = findChannelByGateway(gateway_id); + return channel != nullptr && channel->dali != nullptr && + channel->dali->addr.resetAndAllocAddr(start_address, remove_addr_first, close_light); +} + +bool DaliDomainService::isAllocAddr(uint8_t gateway_id) const { + const auto* channel = findChannelByGateway(gateway_id); + return channel != nullptr && channel->dali != nullptr && channel->dali->addr.isAllocAddr(); +} + +int DaliDomainService::lastAllocAddr(uint8_t gateway_id) const { + const auto* channel = findChannelByGateway(gateway_id); + if (channel == nullptr || channel->dali == nullptr) { + return 0; + } + return channel->dali->addr.lastAllocAddr(); +} + DaliDomainService::DaliChannel* DaliDomainService::findChannelByGateway(uint8_t gateway_id) { const auto it = std::find_if(channels_.begin(), channels_.end(), [gateway_id](const auto& channel) { return channel->config.gateway_id == gateway_id; diff --git a/components/gateway_ble/CMakeLists.txt b/components/gateway_ble/CMakeLists.txt new file mode 100644 index 0000000..eafee22 --- /dev/null +++ b/components/gateway_ble/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS "src/gateway_ble.cpp" + INCLUDE_DIRS "include" + REQUIRES gateway_controller gateway_runtime dali_domain bt esp_timer freertos log nvs_flash +) + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/gateway_ble/include/gateway_ble.hpp b/components/gateway_ble/include/gateway_ble.hpp new file mode 100644 index 0000000..4c8bf38 --- /dev/null +++ b/components/gateway_ble/include/gateway_ble.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "esp_err.h" + +struct ble_gap_event; +struct ble_gatt_access_ctxt; + +namespace gateway { + +class DaliDomainService; +class GatewayController; +class GatewayRuntime; + +class GatewayBleBridge { + public: + GatewayBleBridge(GatewayController& controller, GatewayRuntime& runtime, + DaliDomainService& dali_domain); + + esp_err_t start(); + void handleSync(); + int handleGapEvent(struct ble_gap_event* event); + int handleAccess(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt* ctxt); + + private: + static constexpr uint16_t kInvalidConnectionHandle = 0xffff; + + esp_err_t initNimble(); + void refreshDeviceName(); + void setEnabled(bool enabled); + void startAdvertising(); + void stopAdvertising(); + void notifyCharacteristic(size_t index, const std::vector& payload); + void handleGatewayNotification(const std::vector& frame); + void handleRawWrite(size_t channel_index, const std::vector& payload); + void handleGatewayWrite(const std::vector& payload); + std::string resolvedDeviceName() const; + int characteristicIndex(uint16_t attr_handle) const; + + GatewayController& controller_; + GatewayRuntime& runtime_; + DaliDomainService& dali_domain_; + bool started_{false}; + bool synced_{false}; + bool enabled_{false}; + uint8_t own_addr_type_{0}; + uint16_t conn_handle_{kInvalidConnectionHandle}; + std::string device_name_; + std::array notify_enabled_{}; + std::array, 3> characteristic_values_{}; + std::vector last_notify_payload_; + int64_t last_notify_at_us_{0}; +}; + +} // namespace gateway \ No newline at end of file diff --git a/components/gateway_ble/src/gateway_ble.cpp b/components/gateway_ble/src/gateway_ble.cpp new file mode 100644 index 0000000..59d9ca8 --- /dev/null +++ b/components/gateway_ble/src/gateway_ble.cpp @@ -0,0 +1,520 @@ +#include "gateway_ble.hpp" + +#include "dali_domain.hpp" +#include "gateway_controller.hpp" +#include "gateway_runtime.hpp" + +#include "esp_log.h" +#include "esp_timer.h" +#include "host/ble_gap.h" +#include "host/ble_gatt.h" +#include "host/ble_hs.h" +#include "host/ble_store.h" +#include "host/util/util.h" +#include "nimble/ble.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" +#include "store/config/ble_store_config.h" + +#include + +namespace { + +constexpr const char* kTag = "gateway_ble"; +constexpr uint16_t kServiceUuid = 0xFFF6; +constexpr uint16_t kChannel1Uuid = 0xFFF1; +constexpr uint16_t kChannel2Uuid = 0xFFF2; +constexpr uint16_t kGatewayUuid = 0xFFF3; +constexpr int64_t kGenericDedupeWindowUs = 120000; +constexpr size_t kGatewayCharacteristicIndex = 2; + +gateway::GatewayBleBridge* s_active_bridge = nullptr; +uint16_t s_value_handles[3] = {0, 0, 0}; + +int GapEvent(struct ble_gap_event* event, void* arg); +int AccessCharacteristic(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt* ctxt, void* arg); + +const ble_uuid16_t kServiceUuidDef = BLE_UUID16_INIT(kServiceUuid); +const ble_uuid16_t kChannel1UuidDef = BLE_UUID16_INIT(kChannel1Uuid); +const ble_uuid16_t kChannel2UuidDef = BLE_UUID16_INIT(kChannel2Uuid); +const ble_uuid16_t kGatewayUuidDef = BLE_UUID16_INIT(kGatewayUuid); + +const ble_gatt_chr_def kGattCharacteristics[] = { + { + .uuid = &kChannel1UuidDef.u, + .access_cb = AccessCharacteristic, + .arg = nullptr, + .descriptors = nullptr, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | + BLE_GATT_CHR_F_NOTIFY, + .min_key_size = 0, + .val_handle = &s_value_handles[0], + .cpfd = nullptr, + }, + { + .uuid = &kChannel2UuidDef.u, + .access_cb = AccessCharacteristic, + .arg = nullptr, + .descriptors = nullptr, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | + BLE_GATT_CHR_F_NOTIFY, + .min_key_size = 0, + .val_handle = &s_value_handles[1], + .cpfd = nullptr, + }, + { + .uuid = &kGatewayUuidDef.u, + .access_cb = AccessCharacteristic, + .arg = nullptr, + .descriptors = nullptr, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | + BLE_GATT_CHR_F_NOTIFY, + .min_key_size = 0, + .val_handle = &s_value_handles[2], + .cpfd = nullptr, + }, + { + .uuid = nullptr, + .access_cb = nullptr, + .arg = nullptr, + .descriptors = nullptr, + .flags = 0, + .min_key_size = 0, + .val_handle = nullptr, + .cpfd = nullptr, + }, +}; + +extern "C" void ble_store_config_init(void); + +int GapEvent(struct ble_gap_event* event, void* arg) { + auto* bridge = static_cast(arg != nullptr ? arg : s_active_bridge); + return bridge != nullptr ? bridge->handleGapEvent(event) : 0; +} + +int AccessCharacteristic(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt* ctxt, void* arg) { + auto* bridge = static_cast(arg != nullptr ? arg : s_active_bridge); + return bridge != nullptr ? bridge->handleAccess(conn_handle, attr_handle, ctxt) + : BLE_ATT_ERR_UNLIKELY; +} + +void OnReset(int reason) { + ESP_LOGW(kTag, "nimble reset reason=%d", reason); +} + +void OnSync(void) { + if (s_active_bridge == nullptr) { + return; + } + s_active_bridge->handleSync(); +} + +void HostTask(void* param) { + (void)param; + nimble_port_run(); + nimble_port_freertos_deinit(); +} + +void RegisterGatt(struct ble_gatt_register_ctxt* ctxt, void* arg) { + (void)arg; + char buffer[BLE_UUID_STR_LEN] = {0}; + switch (ctxt->op) { + case BLE_GATT_REGISTER_OP_SVC: + ESP_LOGD(kTag, "registered service %s handle=%u", + ble_uuid_to_str(ctxt->svc.svc_def->uuid, buffer), ctxt->svc.handle); + break; + case BLE_GATT_REGISTER_OP_CHR: + ESP_LOGD(kTag, "registered characteristic %s def=%u val=%u", + ble_uuid_to_str(ctxt->chr.chr_def->uuid, buffer), ctxt->chr.def_handle, + ctxt->chr.val_handle); + break; + case BLE_GATT_REGISTER_OP_DSC: + ESP_LOGD(kTag, "registered descriptor %s handle=%u", + ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buffer), ctxt->dsc.handle); + break; + default: + break; + } +} + +const struct ble_gatt_svc_def kGattServices[] = { + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &kServiceUuidDef.u, + .includes = nullptr, + .characteristics = kGattCharacteristics, + }, + { + .type = 0, + .uuid = nullptr, + .includes = nullptr, + .characteristics = nullptr, + }, +}; + +} // namespace + +namespace gateway { + +GatewayBleBridge::GatewayBleBridge(GatewayController& controller, GatewayRuntime& runtime, + DaliDomainService& dali_domain) + : controller_(controller), runtime_(runtime), dali_domain_(dali_domain) {} + +esp_err_t GatewayBleBridge::start() { + if (started_) { + return ESP_OK; + } + + s_active_bridge = this; + enabled_ = runtime_.bleEnabled(); + refreshDeviceName(); + + controller_.addNotificationSink( + [this](const std::vector& frame) { handleGatewayNotification(frame); }); + controller_.addBleStateSink([this](bool enabled) { setEnabled(enabled); }); + controller_.addGatewayNameSink([this](uint8_t) { refreshDeviceName(); }); + + const esp_err_t err = initNimble(); + if (err != ESP_OK) { + s_active_bridge = nullptr; + return err; + } + + started_ = true; + ESP_LOGI(kTag, "BLE bridge started enabled=%d name=%s", enabled_, device_name_.c_str()); + return ESP_OK; +} + +esp_err_t GatewayBleBridge::initNimble() { + const esp_err_t err = nimble_port_init(); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to init NimBLE err=%s", esp_err_to_name(err)); + return err; + } + + ble_hs_cfg.reset_cb = OnReset; + ble_hs_cfg.sync_cb = OnSync; + ble_hs_cfg.gatts_register_cb = RegisterGatt; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + ble_svc_gap_init(); + ble_svc_gatt_init(); + + int rc = ble_gatts_count_cfg(kGattServices); + if (rc != 0) { + ESP_LOGE(kTag, "failed to count GATT services rc=%d", rc); + return ESP_FAIL; + } + + rc = ble_gatts_add_svcs(kGattServices); + if (rc != 0) { + ESP_LOGE(kTag, "failed to add GATT services rc=%d", rc); + return ESP_FAIL; + } + +#if CONFIG_BT_NIMBLE_GAP_SERVICE + rc = ble_svc_gap_device_name_set(device_name_.c_str()); + if (rc != 0) { + ESP_LOGE(kTag, "failed to set BLE device name rc=%d", rc); + return ESP_FAIL; + } +#endif + + ble_store_config_init(); + nimble_port_freertos_init(HostTask); + return ESP_OK; +} + +void GatewayBleBridge::handleSync() { + int rc = ble_hs_util_ensure_addr(0); + if (rc != 0) { + ESP_LOGE(kTag, "failed to ensure BLE address rc=%d", rc); + return; + } + + rc = ble_hs_id_infer_auto(0, &own_addr_type_); + if (rc != 0) { + ESP_LOGE(kTag, "failed to infer BLE address type rc=%d", rc); + return; + } + + synced_ = true; + if (enabled_) { + startAdvertising(); + } +} + +void GatewayBleBridge::refreshDeviceName() { + device_name_ = resolvedDeviceName(); + + if (!started_) { + return; + } + +#if CONFIG_BT_NIMBLE_GAP_SERVICE + const int rc = ble_svc_gap_device_name_set(device_name_.c_str()); + if (rc != 0) { + ESP_LOGW(kTag, "failed to refresh BLE name rc=%d", rc); + } +#endif + + if (synced_ && enabled_ && conn_handle_ == kInvalidConnectionHandle) { + stopAdvertising(); + startAdvertising(); + } +} + +void GatewayBleBridge::setEnabled(bool enabled) { + enabled_ = enabled; + if (!synced_) { + return; + } + + if (enabled_) { + refreshDeviceName(); + if (conn_handle_ == kInvalidConnectionHandle) { + startAdvertising(); + } + return; + } + + stopAdvertising(); + if (conn_handle_ != kInvalidConnectionHandle) { + const int rc = ble_gap_terminate(conn_handle_, BLE_ERR_REM_USER_CONN_TERM); + if (rc != 0) { + ESP_LOGW(kTag, "failed to terminate BLE connection rc=%d", rc); + } + } +} + +void GatewayBleBridge::startAdvertising() { + if (!enabled_ || ble_gap_adv_active()) { + return; + } + + ble_uuid16_t advertised_service = BLE_UUID16_INIT(kServiceUuid); + struct ble_hs_adv_fields fields = {}; + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.tx_pwr_lvl_is_present = 1; + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + fields.name = reinterpret_cast(device_name_.data()); + fields.name_len = device_name_.size(); + fields.name_is_complete = 1; + fields.uuids16 = &advertised_service; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + + int rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + ESP_LOGE(kTag, "failed to set BLE advertising fields rc=%d", rc); + return; + } + + struct ble_gap_adv_params params = {}; + params.conn_mode = BLE_GAP_CONN_MODE_UND; + params.disc_mode = BLE_GAP_DISC_MODE_GEN; + rc = ble_gap_adv_start(own_addr_type_, nullptr, BLE_HS_FOREVER, ¶ms, GapEvent, this); + if (rc != 0) { + ESP_LOGE(kTag, "failed to start BLE advertising rc=%d", rc); + } +} + +void GatewayBleBridge::stopAdvertising() { + if (!ble_gap_adv_active()) { + return; + } + const int rc = ble_gap_adv_stop(); + if (rc != 0) { + ESP_LOGW(kTag, "failed to stop BLE advertising rc=%d", rc); + } +} + +void GatewayBleBridge::notifyCharacteristic(size_t index, const std::vector& payload) { + if (index >= std::size(s_value_handles) || conn_handle_ == kInvalidConnectionHandle || + !notify_enabled_[index]) { + return; + } + + characteristic_values_[index] = payload; + struct os_mbuf* buffer = ble_hs_mbuf_from_flat(payload.data(), payload.size()); + if (buffer == nullptr) { + ESP_LOGW(kTag, "failed to allocate notify mbuf idx=%u", static_cast(index)); + return; + } + const int rc = ble_gatts_notify_custom(conn_handle_, s_value_handles[index], buffer); + if (rc != 0) { + ESP_LOGW(kTag, "failed to send BLE notify idx=%u rc=%d", static_cast(index), rc); + } +} + +void GatewayBleBridge::handleGatewayNotification(const std::vector& frame) { + if (!enabled_ || conn_handle_ == kInvalidConnectionHandle || !notify_enabled_[kGatewayCharacteristicIndex] || + frame.empty()) { + return; + } + + const int64_t now = esp_timer_get_time(); + int64_t dedupe_window = kGenericDedupeWindowUs; + if (frame.size() > 1 && (frame[1] == 0x03 || frame[1] == 0x04)) { + dedupe_window = 0; + } + if (dedupe_window > 0 && frame == last_notify_payload_ && + (now - last_notify_at_us_) <= dedupe_window) { + return; + } + + notifyCharacteristic(kGatewayCharacteristicIndex, frame); + last_notify_payload_ = frame; + last_notify_at_us_ = now; +} + +void GatewayBleBridge::handleRawWrite(size_t channel_index, const std::vector& payload) { + const auto channels = dali_domain_.channelInfo(); + const auto channel_it = std::find_if(channels.begin(), channels.end(), + [channel_index](const DaliChannelInfo& channel) { + return channel.channel_index == channel_index; + }); + if (channel_it == channels.end()) { + ESP_LOGW(kTag, "raw BLE write for unavailable DALI channel=%u", + static_cast(channel_index)); + return; + } + + if (!payload.empty() && payload[0] == 0x12) { + const auto response = + dali_domain_.transactBridgeFrame(channel_it->gateway_id, payload.data(), payload.size()); + if (!response.empty()) { + notifyCharacteristic(channel_index, response); + } + return; + } + + if (!dali_domain_.writeBridgeFrame(channel_it->gateway_id, payload.data(), payload.size())) { + ESP_LOGW(kTag, "failed to forward raw BLE payload channel=%u len=%u", + static_cast(channel_index), static_cast(payload.size())); + } +} + +void GatewayBleBridge::handleGatewayWrite(const std::vector& payload) { + if (payload.empty()) { + return; + } + if (runtime_.hasPendingQueryCommand(payload)) { + return; + } + controller_.enqueueCommandFrame(payload); +} + +std::string GatewayBleBridge::resolvedDeviceName() const { + const auto channels = dali_domain_.channelInfo(); + if (channels.empty()) { + return runtime_.defaultBleGatewayName(); + } + + const auto channel_it = std::min_element(channels.begin(), channels.end(), + [](const DaliChannelInfo& lhs, + const DaliChannelInfo& rhs) { + return lhs.channel_index < rhs.channel_index; + }); + return runtime_.bleGatewayName(channel_it->gateway_id, channel_it->name); +} + +int GatewayBleBridge::characteristicIndex(uint16_t attr_handle) const { + for (size_t index = 0; index < 3; ++index) { + if (s_value_handles[index] == attr_handle) { + return static_cast(index); + } + } + return -1; +} + +int GatewayBleBridge::handleGapEvent(struct ble_gap_event* event) { + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + conn_handle_ = event->connect.conn_handle; + notify_enabled_.fill(false); + last_notify_payload_.clear(); + last_notify_at_us_ = 0; + ESP_LOGI(kTag, "BLE client connected handle=%u", conn_handle_); + } else if (enabled_) { + startAdvertising(); + } + return 0; + case BLE_GAP_EVENT_DISCONNECT: + conn_handle_ = kInvalidConnectionHandle; + notify_enabled_.fill(false); + last_notify_payload_.clear(); + last_notify_at_us_ = 0; + if (enabled_) { + startAdvertising(); + } + ESP_LOGI(kTag, "BLE client disconnected reason=%d", event->disconnect.reason); + return 0; + case BLE_GAP_EVENT_ADV_COMPLETE: + if (enabled_ && conn_handle_ == kInvalidConnectionHandle) { + startAdvertising(); + } + return 0; + case BLE_GAP_EVENT_SUBSCRIBE: { + const int index = characteristicIndex(event->subscribe.attr_handle); + if (index >= 0 && event->subscribe.conn_handle == conn_handle_) { + notify_enabled_[index] = event->subscribe.cur_notify != 0; + } + return 0; + } + default: + return 0; + } +} + +int GatewayBleBridge::handleAccess(uint16_t, uint16_t attr_handle, + struct ble_gatt_access_ctxt* ctxt) { + const int index = characteristicIndex(attr_handle); + if (index < 0) { + return BLE_ATT_ERR_UNLIKELY; + } + + switch (ctxt->op) { + case BLE_GATT_ACCESS_OP_READ_CHR: + if (!characteristic_values_[index].empty()) { + const int rc = os_mbuf_append(ctxt->om, characteristic_values_[index].data(), + characteristic_values_[index].size()); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + return 0; + + case BLE_GATT_ACCESS_OP_WRITE_CHR: { + const uint16_t len = OS_MBUF_PKTLEN(ctxt->om); + std::vector payload(len, 0); + if (len > 0) { + uint16_t copied = 0; + const int rc = ble_hs_mbuf_to_flat(ctxt->om, payload.data(), len, &copied); + if (rc != 0 || copied != len) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + } + + characteristic_values_[index] = payload; + if (!enabled_) { + return 0; + } + + if (index == static_cast(kGatewayCharacteristicIndex)) { + handleGatewayWrite(payload); + } else { + handleRawWrite(static_cast(index), payload); + } + return 0; + } + + default: + return BLE_ATT_ERR_UNLIKELY; + } +} + +} // namespace gateway \ No newline at end of file diff --git a/components/gateway_controller/CMakeLists.txt b/components/gateway_controller/CMakeLists.txt new file mode 100644 index 0000000..423d791 --- /dev/null +++ b/components/gateway_controller/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS "src/gateway_controller.cpp" + INCLUDE_DIRS "include" + REQUIRES dali_domain gateway_runtime freertos log nvs_flash +) + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp new file mode 100644 index 0000000..b0a4330 --- /dev/null +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "nvs.h" + +namespace gateway { + +class DaliDomainService; +class GatewayRuntime; + +struct GatewayControllerConfig { + uint32_t task_stack_size{6144}; + UBaseType_t task_priority{5}; + int color_temperature_min{2000}; + int color_temperature_max{6500}; + bool setup_supported{true}; + bool ble_supported{false}; + bool wifi_supported{true}; + bool ip_router_supported{true}; + bool internal_scene_supported{true}; + bool internal_group_supported{true}; +}; + +class GatewayController { + public: + using NotificationSink = std::function& frame)>; + using BleStateSink = std::function; + using GatewayNameSink = std::function; + + GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain, + GatewayControllerConfig config = {}); + ~GatewayController(); + + esp_err_t start(); + bool enqueueCommandFrame(const std::vector& frame); + void addNotificationSink(NotificationSink sink); + void addBleStateSink(BleStateSink sink); + void addGatewayNameSink(GatewayNameSink sink); + + bool setupMode() const; + bool bleEnabled() const; + bool wifiEnabled() const; + bool ipRouterEnabled() const; + + private: + struct InternalScene { + bool enabled{false}; + uint8_t brightness{254}; + uint8_t color_mode{2}; + uint8_t data1{0}; + uint8_t data2{0}; + uint8_t data3{0}; + std::string name; + }; + + struct InternalGroup { + bool enabled{false}; + uint8_t target_type{2}; + uint8_t target_value{0}; + std::string name; + }; + + using SceneStore = std::array; + using GroupStore = std::array; + + static void TaskEntry(void* arg); + void taskLoop(); + void dispatchCommand(const std::vector& command); + + bool hasGateway(uint8_t gateway_id) const; + std::vector gatewayIds() const; + std::string gatewayName(uint8_t gateway_id) const; + void refreshRuntimeGatewayNames(); + void publishPayload(uint8_t gateway_id, const std::vector& payload); + void publishFrame(const std::vector& frame); + + uint8_t resolveInternalGroupRawAddress(uint8_t gateway_id, uint8_t raw_addr); + static uint8_t normalizeGroupTargetType(uint8_t target_type); + static uint8_t normalizeGroupTargetValue(uint8_t target_type, uint8_t target_value); + static uint8_t internalGroupRawTargetAddress(uint8_t target_type, uint8_t target_value, + uint8_t raw_addr); + static int internalGroupDecTargetAddress(uint8_t target_type, uint8_t target_value); + static int shortAddressFromRaw(uint8_t raw_addr); + static int reverseInRange(int value, int min_value, int max_value); + + SceneStore& sceneStore(uint8_t gateway_id); + GroupStore& groupStore(uint8_t gateway_id); + InternalScene* scene(uint8_t gateway_id, uint8_t scene_id); + InternalGroup* group(uint8_t gateway_id, uint8_t group_id); + + bool setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled); + bool setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness, + uint8_t color_mode, uint8_t data1, uint8_t data2, uint8_t data3); + bool setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name); + bool deleteScene(uint8_t gateway_id, uint8_t scene_id); + std::pair sceneMask(uint8_t gateway_id); + bool executeScene(uint8_t gateway_id, int short_address, uint8_t scene_id); + + bool setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled); + bool setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type, + uint8_t target_value); + bool setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name); + bool deleteGroup(uint8_t gateway_id, uint8_t group_id); + std::pair groupMask(uint8_t gateway_id); + bool executeGroup(uint8_t gateway_id, uint8_t group_id); + + void handleGatewayNameCommand(uint8_t gateway_id, const std::vector& command); + void handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op); + void handleAllocationCommand(uint8_t gateway_id, const std::vector& command); + void handleInternalSceneCommand(uint8_t gateway_id, const std::vector& command); + void handleInternalGroupCommand(uint8_t gateway_id, const std::vector& command); + + bool openStorage(); + void closeStorage(); + void loadSceneStore(uint8_t gateway_id, SceneStore& scenes); + void loadGroupStore(uint8_t gateway_id, GroupStore& groups); + bool saveScene(uint8_t gateway_id, uint8_t scene_id); + bool deleteSceneStorage(uint8_t gateway_id, uint8_t scene_id); + bool saveSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name); + bool deleteSceneNameStorage(uint8_t gateway_id, uint8_t scene_id); + bool saveGroup(uint8_t gateway_id, uint8_t group_id); + bool deleteGroupStorage(uint8_t gateway_id, uint8_t group_id); + bool saveGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name); + bool deleteGroupNameStorage(uint8_t gateway_id, uint8_t group_id); + std::string readString(std::string_view key) const; + bool writeString(std::string_view key, std::string_view value); + bool eraseKey(std::string_view key); + + GatewayRuntime& runtime_; + DaliDomainService& dali_domain_; + GatewayControllerConfig config_; + TaskHandle_t task_handle_{nullptr}; + nvs_handle_t storage_{0}; + std::vector notification_sinks_; + std::vector ble_state_sinks_; + std::vector gateway_name_sinks_; + std::map scenes_; + std::map groups_; + bool setup_mode_{false}; + bool ble_enabled_{false}; + bool wifi_enabled_{false}; + bool ip_router_enabled_{true}; +}; + +} // namespace gateway diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp new file mode 100644 index 0000000..563f9d1 --- /dev/null +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -0,0 +1,1080 @@ +#include "gateway_controller.hpp" + +#include "dali_domain.hpp" +#include "esp_log.h" +#include "esp_system.h" +#include "gateway_runtime.hpp" + +#include +#include +#include +#include + +namespace gateway { + +namespace { + +constexpr const char* kTag = "gateway_controller"; +constexpr const char* kStorageNamespace = "gateway_rt"; +constexpr size_t kMaxNameBytes = 32; + +std::string ShortKey(const char* prefix, uint8_t gateway_id, uint8_t slot) { + char key[16] = {0}; + std::snprintf(key, sizeof(key), "%s%u_%u", prefix, gateway_id, slot); + return std::string(key); +} + +std::vector ParseCsv(std::string_view raw) { + std::vector values; + size_t start = 0; + while (start < raw.size()) { + const size_t comma = raw.find(',', start); + const size_t end = comma == std::string_view::npos ? raw.size() : comma; + if (end > start) { + values.push_back(static_cast(std::strtol(std::string(raw.substr(start, end - start)).c_str(), + nullptr, 10))); + } + if (comma == std::string_view::npos) { + break; + } + start = comma + 1; + } + return values; +} + +std::string NormalizeName(std::string_view name) { + std::string normalized(name); + if (normalized.size() > kMaxNameBytes) { + normalized.resize(kMaxNameBytes); + } + return normalized; +} + +std::string AppendGatewaySuffix(std::string_view name, uint8_t gateway_id) { + std::string normalized = NormalizeName(name); + const std::string suffix = "_" + std::to_string(gateway_id); + if (normalized.size() > suffix.size() && + normalized.compare(normalized.size() - suffix.size(), suffix.size(), suffix) == 0) { + return normalized; + } + const size_t max_base_len = kMaxNameBytes > suffix.size() ? kMaxNameBytes - suffix.size() : 0; + if (normalized.size() > max_base_len) { + normalized.resize(max_base_len); + } + normalized += suffix; + return normalized; +} + +void AppendStringBytes(std::vector& out, std::string_view value) { + for (const auto ch : value) { + out.push_back(static_cast(ch)); + } +} + +void AppendPaddedName(std::vector& out, std::string_view name) { + const auto normalized = NormalizeName(name); + out.push_back(static_cast(normalized.size())); + AppendStringBytes(out, normalized); + while (out.size() < 5 + kMaxNameBytes) { + out.push_back(0x00); + } +} + +} // namespace + +GatewayController::GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain, + GatewayControllerConfig config) + : runtime_(runtime), dali_domain_(dali_domain), config_(config) {} + +GatewayController::~GatewayController() { + closeStorage(); +} + +esp_err_t GatewayController::start() { + const esp_err_t err = openStorage() ? ESP_OK : ESP_FAIL; + if (err != ESP_OK) { + return err; + } + + const auto device_info = runtime_.deviceInfo(); + ble_enabled_ = device_info.ble_enabled; + refreshRuntimeGatewayNames(); + runtime_.setCommandAddressResolver([this](uint8_t gateway_id, uint8_t raw_addr) { + return resolveInternalGroupRawAddress(gateway_id, raw_addr); + }); + + for (const auto& channel : dali_domain_.channelInfo()) { + sceneStore(channel.gateway_id); + groupStore(channel.gateway_id); + dali_domain_.resetBus(channel.gateway_id); + publishPayload(channel.gateway_id, {0x02, channel.gateway_id, 0x88}); + } + + if (task_handle_ != nullptr) { + return ESP_OK; + } + + const BaseType_t created = xTaskCreate(&GatewayController::TaskEntry, "gateway_ctrl", + config_.task_stack_size, this, config_.task_priority, + &task_handle_); + if (created != pdPASS) { + task_handle_ = nullptr; + ESP_LOGE(kTag, "failed to create controller task"); + return ESP_ERR_NO_MEM; + } + + ESP_LOGI(kTag, "controller started channels=%u", static_cast(dali_domain_.channelCount())); + return ESP_OK; +} + +bool GatewayController::enqueueCommandFrame(const std::vector& frame) { + if (!GatewayRuntime::isGatewayCommandFrame(frame) || !GatewayRuntime::hasValidChecksum(frame)) { + ESP_LOGW(kTag, "dropped invalid command frame len=%u", static_cast(frame.size())); + return false; + } + if (!runtime_.enqueueCommand(frame)) { + if (runtime_.lastEnqueueDropReason() != GatewayRuntime::CommandDropReason::kDuplicate) { + ESP_LOGW(kTag, "dropped command frame reason=%d", + static_cast(runtime_.lastEnqueueDropReason())); + } + return false; + } + if (task_handle_ != nullptr) { + xTaskNotifyGive(task_handle_); + } + return true; +} + +void GatewayController::addNotificationSink(NotificationSink sink) { + if (sink) { + notification_sinks_.push_back(std::move(sink)); + } +} + +void GatewayController::addBleStateSink(BleStateSink sink) { + if (sink) { + ble_state_sinks_.push_back(std::move(sink)); + } +} + +void GatewayController::addGatewayNameSink(GatewayNameSink sink) { + if (sink) { + gateway_name_sinks_.push_back(std::move(sink)); + } +} + +bool GatewayController::setupMode() const { + return setup_mode_; +} + +bool GatewayController::bleEnabled() const { + return ble_enabled_; +} + +bool GatewayController::wifiEnabled() const { + return wifi_enabled_; +} + +bool GatewayController::ipRouterEnabled() const { + return ip_router_enabled_; +} + +void GatewayController::TaskEntry(void* arg) { + static_cast(arg)->taskLoop(); +} + +void GatewayController::taskLoop() { + while (true) { + bool drained = false; + while (auto command = runtime_.popNextCommand()) { + drained = true; + dispatchCommand(*command); + runtime_.completeCurrentCommand(); + } + if (!drained) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } + } +} + +void GatewayController::dispatchCommand(const std::vector& command) { + if (command.size() < 7) { + ESP_LOGW(kTag, "command too short len=%u", static_cast(command.size())); + return; + } + + const uint8_t gateway_id = command[2]; + const uint8_t opcode = command[3]; + const uint8_t addr = command[4]; + const uint8_t data = command[5]; + if (!hasGateway(gateway_id)) { + ESP_LOGW(kTag, "command for unknown gateway=%u opcode=0x%02x", gateway_id, opcode); + return; + } + + switch (opcode) { + case 0x00: + esp_restart(); + break; + case 0x01: + setup_mode_ = true; + break; + case 0x02: + ESP_LOGI(kTag, "legacy opcode 0x02 requested for gateway=%u", gateway_id); + break; + case 0x03: + if (!config_.ble_supported) { + publishPayload(gateway_id, {0x03, gateway_id, 0xff}); + } else if (addr == 0) { + if (runtime_.setBleEnabled(false)) { + ble_enabled_ = runtime_.bleEnabled(); + for (const auto& sink : ble_state_sinks_) { + sink(ble_enabled_); + } + publishPayload(gateway_id, {0x03, gateway_id, 0x00}); + } else { + publishPayload(gateway_id, {0x03, gateway_id, + static_cast(ble_enabled_ ? 1 : 0)}); + } + } else if (addr == 1) { + if (runtime_.setBleEnabled(true)) { + ble_enabled_ = runtime_.bleEnabled(); + for (const auto& sink : ble_state_sinks_) { + sink(ble_enabled_); + } + publishPayload(gateway_id, {0x03, gateway_id, 0x01}); + } else { + publishPayload(gateway_id, {0x03, gateway_id, + static_cast(ble_enabled_ ? 1 : 0)}); + } + } else if (addr == 2) { + publishPayload(gateway_id, {0x03, gateway_id, static_cast(ble_enabled_ ? 1 : 0)}); + } + break; + case 0x04: + if (addr == 0) { + wifi_enabled_ = false; + } else if (addr == 1 || addr == 101) { + wifi_enabled_ = true; + } + break; + case 0x05: + handleGatewayNameCommand(gateway_id, command); + break; + case 0x06: { + uint8_t feature = 0; + if (setup_mode_ && config_.setup_supported) { + feature |= 0x01; + } + if (config_.ble_supported) { + feature |= 0x02; + } + if (config_.ip_router_supported && ip_router_enabled_) { + feature |= 0x08; + } + if (config_.internal_scene_supported) { + feature |= 0x10; + } + if (config_.internal_group_supported) { + feature |= 0x20; + } + publishPayload(gateway_id, {0x03, gateway_id, feature}); + break; + } + case 0x07: + case 0x08: + dali_domain_.sendRaw(gateway_id, addr, data); + break; + case 0x09: { + const auto ids = gatewayIds(); + if (addr >= 1 && addr <= ids.size()) { + const auto selected_gateway = ids[addr - 1]; + publishPayload(gateway_id, {0x03, selected_gateway, selected_gateway}); + } else { + publishPayload(gateway_id, {0x04, 0xff, 0x00}); + } + break; + } + case 0x0A: + handleGatewayIdentityCommand(gateway_id, addr); + break; + case 0x10: + case 0x11: + dali_domain_.sendRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data); + break; + case 0x12: + if (addr == 0xff && data >= 0x10 && data <= 0x1f) { + const uint8_t scene_id = static_cast(data - 0x10); + if (!executeScene(gateway_id, shortAddressFromRaw(addr), scene_id)) { + dali_domain_.sendRaw(gateway_id, addr, data); + } + } else { + dali_domain_.sendRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data); + } + break; + case 0x13: + dali_domain_.sendExtRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data); + break; + case 0x14: { + const auto result = dali_domain_.queryRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data); + if (result.has_value()) { + publishPayload(gateway_id, {0x03, gateway_id, result.value()}); + } else { + publishPayload(gateway_id, {0x04, gateway_id, 0x00}); + } + break; + } + case 0x15: + dali_domain_.queryRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data); + break; + case 0x16: + dali_domain_.queryRaw(gateway_id, 0, 0); + break; + case 0x17: + if (command.size() >= 8) { + const int mirek = command[5] * 256 + command[6]; + const uint8_t target = resolveInternalGroupRawAddress(gateway_id, addr); + dali_domain_.setColTempRaw(gateway_id, shortAddressFromRaw(target), mirek); + } + break; + case 0x18: + if (command.size() >= 10) { + const int x = command[5] * 256 + command[6]; + const int y = command[7] * 256 + command[8]; + dali_domain_.setColourRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), x, y); + } + break; + case 0x30: + handleAllocationCommand(gateway_id, command); + break; + case 0x31: + break; + case 0x32: + dali_domain_.resetAndAllocAddr(gateway_id); + break; + case 0x37: + if (command.size() >= 8) { + int kelvin = command[5] * 256 + command[6]; + const uint8_t target = resolveInternalGroupRawAddress(gateway_id, addr); + if (config_.color_temperature_max < config_.color_temperature_min) { + kelvin = reverseInRange(kelvin, config_.color_temperature_min, + config_.color_temperature_max); + } + dali_domain_.setColTemp(gateway_id, shortAddressFromRaw(target), kelvin); + } + break; + case 0x38: + if (command.size() >= 9) { + const uint8_t r = command[5]; + const uint8_t g = command[6]; + const uint8_t b = command[7]; + const int target = shortAddressFromRaw(resolveInternalGroupRawAddress(gateway_id, addr)); + if (r == 0 && g == 0 && b == 0) { + dali_domain_.off(gateway_id, target); + } else { + dali_domain_.setColourRGB(gateway_id, target, r, g, b); + } + } + break; + case 0xA0: + handleInternalSceneCommand(gateway_id, command); + break; + case 0xA2: + handleInternalGroupCommand(gateway_id, command); + break; + default: + ESP_LOGW(kTag, "unhandled opcode=0x%02x gateway=%u", opcode, gateway_id); + break; + } +} + +bool GatewayController::hasGateway(uint8_t gateway_id) const { + const auto channels = dali_domain_.channelInfo(); + return std::any_of(channels.begin(), channels.end(), [gateway_id](const DaliChannelInfo& channel) { + return channel.gateway_id == gateway_id; + }); +} + +std::vector GatewayController::gatewayIds() const { + std::vector ids; + const auto channels = dali_domain_.channelInfo(); + ids.reserve(channels.size()); + for (const auto& channel : channels) { + ids.push_back(channel.gateway_id); + } + return ids; +} + +std::string GatewayController::gatewayName(uint8_t gateway_id) const { + const auto channels = dali_domain_.channelInfo(); + const auto it = std::find_if(channels.begin(), channels.end(), [gateway_id](const auto& channel) { + return channel.gateway_id == gateway_id; + }); + if (it != channels.end()) { + return it->name; + } + return runtime_.gatewayName(gateway_id); +} + +void GatewayController::refreshRuntimeGatewayNames() { + std::vector assigned_names; + assigned_names.reserve(dali_domain_.channelCount()); + + for (const auto& channel : dali_domain_.channelInfo()) { + std::string runtime_name = NormalizeName(runtime_.gatewayName(channel.gateway_id)); + if (std::any_of(assigned_names.begin(), assigned_names.end(), + [&runtime_name](const std::string& assigned_name) { + return assigned_name == runtime_name; + })) { + runtime_name = AppendGatewaySuffix(runtime_name, channel.gateway_id); + } + dali_domain_.updateChannelName(channel.gateway_id, runtime_name); + assigned_names.push_back(runtime_name); + } +} + +void GatewayController::publishPayload(uint8_t, const std::vector& payload) { + publishFrame(GatewayRuntime::buildNotificationFrame(payload)); +} + +void GatewayController::publishFrame(const std::vector& frame) { + for (const auto& sink : notification_sinks_) { + sink(frame); + } +} + +uint8_t GatewayController::resolveInternalGroupRawAddress(uint8_t gateway_id, uint8_t raw_addr) { + if (raw_addr < 0x80 || raw_addr > 0x9f) { + return raw_addr; + } + const uint8_t slot = static_cast((raw_addr - 0x80) / 2); + auto* group_data = group(gateway_id, slot); + if (group_data == nullptr || !group_data->enabled) { + return raw_addr; + } + return internalGroupRawTargetAddress(group_data->target_type, group_data->target_value, raw_addr); +} + +uint8_t GatewayController::normalizeGroupTargetType(uint8_t target_type) { + return target_type <= 2 ? target_type : 2; +} + +uint8_t GatewayController::normalizeGroupTargetValue(uint8_t target_type, uint8_t target_value) { + const uint8_t normalized_type = normalizeGroupTargetType(target_type); + if (normalized_type == 0) { + return std::min(target_value, 63); + } + if (normalized_type == 1) { + return std::min(target_value, 15); + } + return 0; +} + +uint8_t GatewayController::internalGroupRawTargetAddress(uint8_t target_type, uint8_t target_value, + uint8_t raw_addr) { + const uint8_t normalized_type = normalizeGroupTargetType(target_type); + const uint8_t normalized_value = normalizeGroupTargetValue(normalized_type, target_value); + const uint8_t lsb = raw_addr & 0x01; + if (normalized_type == 0) { + return static_cast(normalized_value * 2 + lsb); + } + if (normalized_type == 1) { + return static_cast(0x80 + normalized_value * 2 + lsb); + } + return lsb == 0 ? 0xfe : 0xff; +} + +int GatewayController::internalGroupDecTargetAddress(uint8_t target_type, uint8_t target_value) { + const uint8_t normalized_type = normalizeGroupTargetType(target_type); + const uint8_t normalized_value = normalizeGroupTargetValue(normalized_type, target_value); + if (normalized_type == 0) { + return normalized_value; + } + if (normalized_type == 1) { + return 64 + normalized_value; + } + return 127; +} + +int GatewayController::shortAddressFromRaw(uint8_t raw_addr) { + return raw_addr / 2; +} + +int GatewayController::reverseInRange(int value, int min_value, int max_value) { + return min_value + max_value - value; +} + +GatewayController::SceneStore& GatewayController::sceneStore(uint8_t gateway_id) { + auto [it, inserted] = scenes_.try_emplace(gateway_id); + if (inserted) { + loadSceneStore(gateway_id, it->second); + } + return it->second; +} + +GatewayController::GroupStore& GatewayController::groupStore(uint8_t gateway_id) { + auto [it, inserted] = groups_.try_emplace(gateway_id); + if (inserted) { + loadGroupStore(gateway_id, it->second); + } + return it->second; +} + +GatewayController::InternalScene* GatewayController::scene(uint8_t gateway_id, uint8_t scene_id) { + if (scene_id >= 16) { + return nullptr; + } + return &sceneStore(gateway_id)[scene_id]; +} + +GatewayController::InternalGroup* GatewayController::group(uint8_t gateway_id, uint8_t group_id) { + if (group_id >= 16) { + return nullptr; + } + return &groupStore(gateway_id)[group_id]; +} + +bool GatewayController::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled) { + auto* scene_data = scene(gateway_id, scene_id); + if (scene_data == nullptr) { + return false; + } + scene_data->enabled = enabled; + return saveScene(gateway_id, scene_id); +} + +bool GatewayController::setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness, + uint8_t color_mode, uint8_t data1, uint8_t data2, + uint8_t data3) { + auto* scene_data = scene(gateway_id, scene_id); + if (scene_data == nullptr) { + return false; + } + scene_data->brightness = std::min(brightness, 254); + scene_data->color_mode = std::min(color_mode, 2); + if (scene_data->color_mode == 0) { + scene_data->data1 = data1; + scene_data->data2 = data2; + scene_data->data3 = 0; + } else if (scene_data->color_mode == 1) { + scene_data->data1 = data1; + scene_data->data2 = data2; + scene_data->data3 = data3; + } else { + scene_data->data1 = 0; + scene_data->data2 = 0; + scene_data->data3 = 0; + } + return saveScene(gateway_id, scene_id); +} + +bool GatewayController::setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) { + auto* scene_data = scene(gateway_id, scene_id); + if (scene_data == nullptr) { + return false; + } + scene_data->name = NormalizeName(name); + return saveSceneName(gateway_id, scene_id, scene_data->name); +} + +bool GatewayController::deleteScene(uint8_t gateway_id, uint8_t scene_id) { + auto* scene_data = scene(gateway_id, scene_id); + if (scene_data == nullptr) { + return false; + } + *scene_data = InternalScene{}; + deleteSceneStorage(gateway_id, scene_id); + deleteSceneNameStorage(gateway_id, scene_id); + return true; +} + +std::pair GatewayController::sceneMask(uint8_t gateway_id) { + const auto& scenes = sceneStore(gateway_id); + uint16_t mask = 0; + for (size_t index = 0; index < scenes.size(); ++index) { + if (scenes[index].enabled) { + mask |= static_cast(1U << index); + } + } + return {static_cast(mask & 0xff), static_cast((mask >> 8) & 0xff)}; +} + +bool GatewayController::executeScene(uint8_t gateway_id, int short_address, uint8_t scene_id) { + auto* scene_data = scene(gateway_id, scene_id); + if (scene_data == nullptr || !scene_data->enabled) { + return false; + } + if (scene_data->brightness <= 0) { + dali_domain_.off(gateway_id, short_address); + } else { + dali_domain_.setBright(gateway_id, short_address, scene_data->brightness); + } + if (scene_data->color_mode == 0) { + int kelvin = scene_data->data1 * 256 + scene_data->data2; + if (kelvin > 0) { + if (config_.color_temperature_max < config_.color_temperature_min) { + kelvin = reverseInRange(kelvin, config_.color_temperature_min, + config_.color_temperature_max); + } + dali_domain_.setColTemp(gateway_id, short_address, kelvin); + } + } else if (scene_data->color_mode == 1) { + if (scene_data->data1 != 0 || scene_data->data2 != 0 || scene_data->data3 != 0) { + dali_domain_.setColourRGB(gateway_id, short_address, scene_data->data1, scene_data->data2, + scene_data->data3); + } else if (scene_data->brightness <= 0) { + dali_domain_.off(gateway_id, short_address); + } + } + return true; +} + +bool GatewayController::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled) { + auto* group_data = group(gateway_id, group_id); + if (group_data == nullptr) { + return false; + } + group_data->enabled = enabled; + return saveGroup(gateway_id, group_id); +} + +bool GatewayController::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type, + uint8_t target_value) { + auto* group_data = group(gateway_id, group_id); + if (group_data == nullptr) { + return false; + } + group_data->target_type = normalizeGroupTargetType(target_type); + group_data->target_value = normalizeGroupTargetValue(group_data->target_type, target_value); + return saveGroup(gateway_id, group_id); +} + +bool GatewayController::setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) { + auto* group_data = group(gateway_id, group_id); + if (group_data == nullptr) { + return false; + } + group_data->name = NormalizeName(name); + return saveGroupName(gateway_id, group_id, group_data->name); +} + +bool GatewayController::deleteGroup(uint8_t gateway_id, uint8_t group_id) { + auto* group_data = group(gateway_id, group_id); + if (group_data == nullptr) { + return false; + } + *group_data = InternalGroup{}; + deleteGroupStorage(gateway_id, group_id); + deleteGroupNameStorage(gateway_id, group_id); + return true; +} + +std::pair GatewayController::groupMask(uint8_t gateway_id) { + const auto& groups = groupStore(gateway_id); + uint16_t mask = 0; + for (size_t index = 0; index < groups.size(); ++index) { + if (groups[index].enabled) { + mask |= static_cast(1U << index); + } + } + return {static_cast(mask & 0xff), static_cast((mask >> 8) & 0xff)}; +} + +bool GatewayController::executeGroup(uint8_t gateway_id, uint8_t group_id) { + auto* group_data = group(gateway_id, group_id); + if (group_data == nullptr || !group_data->enabled) { + return false; + } + return dali_domain_.on(gateway_id, + internalGroupDecTargetAddress(group_data->target_type, + group_data->target_value)); +} + +void GatewayController::handleGatewayNameCommand(uint8_t gateway_id, + const std::vector& command) { + const uint8_t op = command[4]; + if (op == 0x00) { + const auto name = gatewayName(gateway_id); + std::vector payload{0x05, gateway_id, op, + static_cast(std::min(name.size(), kMaxNameBytes))}; + AppendStringBytes(payload, NormalizeName(name)); + publishPayload(gateway_id, payload); + return; + } + if (op != 0x01) { + return; + } + + if (command.size() < 7) { + publishPayload(gateway_id, {0x05, gateway_id, op, 0x00}); + return; + } + const size_t payload_end = command.size() - 1; + const size_t requested_len = std::min(command[5], kMaxNameBytes); + const size_t available = payload_end > 6 ? payload_end - 6 : 0; + const size_t actual_len = std::min(requested_len, available); + std::string name; + name.reserve(actual_len); + for (size_t index = 0; index < actual_len; ++index) { + name.push_back(static_cast(command[6 + index])); + } + if (runtime_.setGatewayName(gateway_id, name)) { + refreshRuntimeGatewayNames(); + for (const auto& sink : gateway_name_sinks_) { + sink(gateway_id); + } + publishPayload(gateway_id, {0x05, gateway_id, op, 0x01}); + } else { + publishPayload(gateway_id, {0x05, gateway_id, op, 0x00}); + } +} + +void GatewayController::handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op) { + std::string value; + if (op == 0x00) { + value = gatewayName(gateway_id); + } else if (op == 0x01) { + value = runtime_.gatewaySerialHex(gateway_id); + } else if (op == 0x02) { + value = runtime_.bleMacHex(); + } else { + publishPayload(gateway_id, {0x0A, gateway_id, op, 0x00, 0x00}); + return; + } + std::vector payload{0x0A, gateway_id, op, + static_cast(std::min(value.size(), kMaxNameBytes))}; + AppendStringBytes(payload, value); + publishPayload(gateway_id, payload); +} + +void GatewayController::handleAllocationCommand(uint8_t gateway_id, + const std::vector& command) { + const uint8_t mode = command[4]; + const uint8_t start_addr = command[5]; + if (mode == 0) { + dali_domain_.stopAllocAddr(gateway_id); + } else if (mode == 1) { + dali_domain_.allocateAllAddr(gateway_id, start_addr); + } else if (mode == 2) { + dali_domain_.resetAndAllocAddr(gateway_id); + } else if (mode == 3) { + publishPayload(gateway_id, {0x30, gateway_id, + static_cast(dali_domain_.isAllocAddr(gateway_id) ? 1 : 0), + static_cast(dali_domain_.lastAllocAddr(gateway_id) & 0xff)}); + } +} + +void GatewayController::handleInternalSceneCommand(uint8_t gateway_id, + const std::vector& command) { + if (command.size() < 7) { + publishPayload(gateway_id, {0xA1, gateway_id, 0xff, 0xff, 0x01}); + return; + } + const uint8_t op = command[4]; + const uint8_t scene_id = command[5]; + if (scene_id > 15) { + publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x01}); + return; + } + + auto* scene_data = scene(gateway_id, scene_id); + switch (op) { + case 0x00: + publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, true) + ? std::vector{0xA0, gateway_id, op, scene_id, 0x01} + : std::vector{0xA1, gateway_id, op, scene_id, 0x02}); + break; + case 0x01: + publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, false) + ? std::vector{0xA0, gateway_id, op, scene_id, 0x01} + : std::vector{0xA1, gateway_id, op, scene_id, 0x02}); + break; + case 0x02: + publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, + static_cast(scene_data->enabled ? 1 : 0)}); + break; + case 0x03: { + const auto [low, high] = sceneMask(gateway_id); + publishPayload(gateway_id, {0xA0, gateway_id, op, low, high}); + break; + } + case 0x04: + publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, scene_data->brightness, + scene_data->color_mode, scene_data->data1, scene_data->data2, + scene_data->data3}); + break; + case 0x05: + if (command.size() >= 11 && setSceneDetail(gateway_id, scene_id, command[6], command[7], + command[8], command[9], command[10])) { + publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, 0x01}); + } else { + publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x02}); + } + break; + case 0x06: + publishPayload(gateway_id, deleteScene(gateway_id, scene_id) + ? std::vector{0xA0, gateway_id, op, scene_id, 0x01} + : std::vector{0xA1, gateway_id, op, scene_id, 0x02}); + break; + case 0x07: { + std::vector payload{0xA0, gateway_id, op, scene_id}; + AppendPaddedName(payload, scene_data->name); + publishPayload(gateway_id, payload); + break; + } + case 0x08: { + if (command.size() < 8) { + publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x02}); + break; + } + const size_t payload_end = command.size() - 1; + const size_t requested_len = std::min(command[6], kMaxNameBytes); + const size_t available = payload_end > 7 ? payload_end - 7 : 0; + const size_t actual_len = std::min(requested_len, available); + std::string name; + for (size_t index = 0; index < actual_len; ++index) { + name.push_back(static_cast(command[7 + index])); + } + publishPayload(gateway_id, setSceneName(gateway_id, scene_id, name) + ? std::vector{0xA0, gateway_id, op, scene_id, 0x01} + : std::vector{0xA1, gateway_id, op, scene_id, 0x02}); + break; + } + default: + publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x03}); + break; + } +} + +void GatewayController::handleInternalGroupCommand(uint8_t gateway_id, + const std::vector& command) { + if (command.size() < 7) { + publishPayload(gateway_id, {0xA3, gateway_id, 0xff, 0xff, 0x01}); + return; + } + const uint8_t op = command[4]; + const uint8_t group_id = command[5]; + if (group_id > 15) { + publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x01}); + return; + } + + auto* group_data = group(gateway_id, group_id); + switch (op) { + case 0x00: + publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, true) + ? std::vector{0xA2, gateway_id, op, group_id, 0x01} + : std::vector{0xA3, gateway_id, op, group_id, 0x02}); + break; + case 0x01: + publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, false) + ? std::vector{0xA2, gateway_id, op, group_id, 0x01} + : std::vector{0xA3, gateway_id, op, group_id, 0x02}); + break; + case 0x02: + publishPayload(gateway_id, {0xA2, gateway_id, op, group_id, + static_cast(group_data->enabled ? 1 : 0)}); + break; + case 0x03: { + const auto [low, high] = groupMask(gateway_id); + publishPayload(gateway_id, {0xA2, gateway_id, op, low, high}); + break; + } + case 0x04: + publishPayload(gateway_id, {0xA2, gateway_id, op, group_id, + normalizeGroupTargetType(group_data->target_type), + normalizeGroupTargetValue(group_data->target_type, + group_data->target_value)}); + break; + case 0x05: + if (command.size() >= 9 && setGroupDetail(gateway_id, group_id, command[6], command[7])) { + publishPayload(gateway_id, {0xA2, gateway_id, op, group_id, 0x01}); + } else { + publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x02}); + } + break; + case 0x06: + publishPayload(gateway_id, deleteGroup(gateway_id, group_id) + ? std::vector{0xA2, gateway_id, op, group_id, 0x01} + : std::vector{0xA3, gateway_id, op, group_id, 0x02}); + break; + case 0x07: { + std::vector payload{0xA2, gateway_id, op, group_id}; + AppendPaddedName(payload, group_data->name); + publishPayload(gateway_id, payload); + break; + } + case 0x08: { + if (command.size() < 8) { + publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x02}); + break; + } + const size_t payload_end = command.size() - 1; + const size_t requested_len = std::min(command[6], kMaxNameBytes); + const size_t available = payload_end > 7 ? payload_end - 7 : 0; + const size_t actual_len = std::min(requested_len, available); + std::string name; + for (size_t index = 0; index < actual_len; ++index) { + name.push_back(static_cast(command[7 + index])); + } + publishPayload(gateway_id, setGroupName(gateway_id, group_id, name) + ? std::vector{0xA2, gateway_id, op, group_id, 0x01} + : std::vector{0xA3, gateway_id, op, group_id, 0x02}); + break; + } + case 0x09: + publishPayload(gateway_id, executeGroup(gateway_id, group_id) + ? std::vector{0xA2, gateway_id, op, group_id, 0x01} + : std::vector{0xA3, gateway_id, op, group_id, 0x02}); + break; + default: + publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x03}); + break; + } +} + +bool GatewayController::openStorage() { + if (storage_ != 0) { + return true; + } + const esp_err_t err = nvs_open(kStorageNamespace, NVS_READWRITE, &storage_); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to open controller storage: %s", esp_err_to_name(err)); + return false; + } + return true; +} + +void GatewayController::closeStorage() { + if (storage_ != 0) { + nvs_close(storage_); + storage_ = 0; + } +} + +void GatewayController::loadSceneStore(uint8_t gateway_id, SceneStore& scenes) { + for (uint8_t scene_id = 0; scene_id < scenes.size(); ++scene_id) { + const auto raw = readString(ShortKey("sc", gateway_id, scene_id)); + const auto values = ParseCsv(raw); + if (values.size() >= 6) { + scenes[scene_id].enabled = values[0] != 0; + scenes[scene_id].brightness = static_cast(std::clamp(values[1], 0, 254)); + scenes[scene_id].color_mode = static_cast(std::clamp(values[2], 0, 2)); + scenes[scene_id].data1 = static_cast(std::clamp(values[3], 0, 255)); + scenes[scene_id].data2 = static_cast(std::clamp(values[4], 0, 255)); + scenes[scene_id].data3 = static_cast(std::clamp(values[5], 0, 255)); + } + scenes[scene_id].name = NormalizeName(readString(ShortKey("sn", gateway_id, scene_id))); + } +} + +void GatewayController::loadGroupStore(uint8_t gateway_id, GroupStore& groups) { + for (uint8_t group_id = 0; group_id < groups.size(); ++group_id) { + const auto raw = readString(ShortKey("gr", gateway_id, group_id)); + const auto values = ParseCsv(raw); + if (values.size() >= 2) { + groups[group_id].enabled = values[0] != 0; + uint8_t target_type = static_cast(std::clamp(values[1], 0, 255)); + uint8_t target_value = values.size() >= 3 ? static_cast(std::clamp(values[2], 0, 255)) + : target_type; + if (values.size() < 3) { + target_type = 2; + } + groups[group_id].target_type = normalizeGroupTargetType(target_type); + groups[group_id].target_value = normalizeGroupTargetValue(groups[group_id].target_type, + target_value); + } + groups[group_id].name = NormalizeName(readString(ShortKey("gn", gateway_id, group_id))); + } +} + +bool GatewayController::saveScene(uint8_t gateway_id, uint8_t scene_id) { + const auto* scene_data = scene(gateway_id, scene_id); + if (scene_data == nullptr) { + return false; + } + char payload[32] = {0}; + std::snprintf(payload, sizeof(payload), "%u,%u,%u,%u,%u,%u", scene_data->enabled ? 1 : 0, + scene_data->brightness, scene_data->color_mode, scene_data->data1, + scene_data->data2, scene_data->data3); + return writeString(ShortKey("sc", gateway_id, scene_id), payload); +} + +bool GatewayController::deleteSceneStorage(uint8_t gateway_id, uint8_t scene_id) { + return eraseKey(ShortKey("sc", gateway_id, scene_id)); +} + +bool GatewayController::saveSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) { + if (name.empty()) { + return deleteSceneNameStorage(gateway_id, scene_id); + } + return writeString(ShortKey("sn", gateway_id, scene_id), NormalizeName(name)); +} + +bool GatewayController::deleteSceneNameStorage(uint8_t gateway_id, uint8_t scene_id) { + return eraseKey(ShortKey("sn", gateway_id, scene_id)); +} + +bool GatewayController::saveGroup(uint8_t gateway_id, uint8_t group_id) { + const auto* group_data = group(gateway_id, group_id); + if (group_data == nullptr) { + return false; + } + char payload[24] = {0}; + std::snprintf(payload, sizeof(payload), "%u,%u,%u", group_data->enabled ? 1 : 0, + normalizeGroupTargetType(group_data->target_type), + normalizeGroupTargetValue(group_data->target_type, group_data->target_value)); + return writeString(ShortKey("gr", gateway_id, group_id), payload); +} + +bool GatewayController::deleteGroupStorage(uint8_t gateway_id, uint8_t group_id) { + return eraseKey(ShortKey("gr", gateway_id, group_id)); +} + +bool GatewayController::saveGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) { + if (name.empty()) { + return deleteGroupNameStorage(gateway_id, group_id); + } + return writeString(ShortKey("gn", gateway_id, group_id), NormalizeName(name)); +} + +bool GatewayController::deleteGroupNameStorage(uint8_t gateway_id, uint8_t group_id) { + return eraseKey(ShortKey("gn", gateway_id, group_id)); +} + +std::string GatewayController::readString(std::string_view key) const { + if (storage_ == 0) { + return {}; + } + size_t required_size = 0; + if (nvs_get_str(storage_, std::string(key).c_str(), nullptr, &required_size) != ESP_OK || + required_size == 0) { + return {}; + } + std::string value(required_size - 1, '\0'); + if (nvs_get_str(storage_, std::string(key).c_str(), value.data(), &required_size) != ESP_OK) { + return {}; + } + return value; +} + +bool GatewayController::writeString(std::string_view key, std::string_view value) { + if (storage_ == 0) { + return false; + } + return nvs_set_str(storage_, std::string(key).c_str(), std::string(value).c_str()) == ESP_OK && + nvs_commit(storage_) == ESP_OK; +} + +bool GatewayController::eraseKey(std::string_view key) { + if (storage_ == 0) { + return false; + } + const esp_err_t err = nvs_erase_key(storage_, std::string(key).c_str()); + if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) { + return false; + } + return nvs_commit(storage_) == ESP_OK; +} + +} // namespace gateway diff --git a/components/gateway_runtime/include/gateway_runtime.hpp b/components/gateway_runtime/include/gateway_runtime.hpp index f197e71..d4577b8 100644 --- a/components/gateway_runtime/include/gateway_runtime.hpp +++ b/components/gateway_runtime/include/gateway_runtime.hpp @@ -99,10 +99,13 @@ class GatewayRuntime { void setCommandAddressResolver(std::function resolver); GatewayDeviceInfo deviceInfo() const; + bool bleEnabled() const; + bool setBleEnabled(bool enabled); std::string gatewayName(uint8_t gateway_id) const; bool setGatewayName(uint8_t gateway_id, std::string_view name); std::string gatewaySerialHex(uint8_t gateway_id) const; std::string bleMacHex() const; + std::string bleGatewayName(uint8_t gateway_id, std::string_view gateway_name) const; std::string defaultBleGatewayName() const; private: diff --git a/components/gateway_runtime/src/gateway_runtime.cpp b/components/gateway_runtime/src/gateway_runtime.cpp index 301391d..4fa5161 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -312,6 +312,18 @@ GatewayDeviceInfo GatewayRuntime::deviceInfo() const { return info; } +bool GatewayRuntime::bleEnabled() const { + return ble_enabled_; +} + +bool GatewayRuntime::setBleEnabled(bool enabled) { + if (!settings_.setBleEnabled(enabled)) { + return false; + } + ble_enabled_ = enabled; + return true; +} + std::string GatewayRuntime::gatewayName(uint8_t gateway_id) const { return settings_.getGatewayName(gateway_id, defaultGatewayName(gateway_id)); } @@ -342,6 +354,17 @@ std::string GatewayRuntime::bleMacHex() const { return toHex(serialBytes()); } +std::string GatewayRuntime::bleGatewayName(uint8_t gateway_id, std::string_view gateway_name) const { + std::string normalized(gateway_name); + if (normalized.size() > kMaxGatewayNameBytes) { + normalized.resize(kMaxGatewayNameBytes); + } + if (!normalized.empty() && normalized != defaultGatewayName(gateway_id)) { + return normalized; + } + return defaultBleGatewayName(); +} + std::string GatewayRuntime::defaultBleGatewayName() const { return "DALIGW_" + bleMacHex(); }