diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index 2bce6c0..a6ec653 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -1471,6 +1471,20 @@ config GATEWAY_CHANNEL16_SERIAL_QUERY_TIMEOUT_MS endmenu +menu "DALI Part 103 Application Controller" + +config GATEWAY_DALI_103_APPLICATION_CONTROLLER_ENABLED + bool "Enable the gateway as a DALI Part 103 application controller" + default n + help + Makes each configured DALI channel expose the gateway as a logical + Part 103 control device. The dali_cpp application layer answers the + standard device queries, supports short-address commissioning, and + emits an identity action for the gateway adapter. The ESP-IDF domain + owns repeat-command timing and backward-frame transmission. + +endmenu + menu "Gateway Cache" config GATEWAY_CACHE_SUPPORTED diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 01c1717..6ea588c 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -517,6 +518,12 @@ constexpr gateway::GatewayCachePriorityMode kCachePriorityMode = gateway::GatewayCachePriorityMode::kOutsideBusFirst; #endif +#ifdef CONFIG_GATEWAY_DALI_103_APPLICATION_CONTROLLER_ENABLED +constexpr bool kDali103ApplicationControllerEnabled = true; +#else +constexpr bool kDali103ApplicationControllerEnabled = false; +#endif + #if defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU) || \ defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII) constexpr bool kModbusDefaultSerialTransport = true; @@ -1016,6 +1023,17 @@ extern "C" void app_main(void) { ESP_ERROR_CHECK(ValidateChannelBindings() ? ESP_OK : ESP_ERR_INVALID_STATE); s_dali_domain = std::make_unique(); + s_dali_domain->setApplicationControllerEnabled(kDali103ApplicationControllerEnabled); + s_dali_domain->setApplicationControllerIdentitySink([](uint8_t gateway_id) { + // dali_cpp only emits a semantic identity action. This gateway adapter + // turns it into a physical status-LED indication when the network service + // is running; another host can bind the same action to its own hardware. + if (s_network != nullptr) { + s_network->identify(); + } else { + ESP_LOGI(kTag, "Part 103 identity command executed for gateway channel=%u", gateway_id); + } + }); s_runtime = std::make_unique( profile, gateway::GatewayRuntimeConfig{ @@ -1026,9 +1044,25 @@ extern "C" void app_main(void) { kCacheSupported && kCacheStartupEnabled, kUsbStartupDefaultMode, kUsbStartupDefaultChannelIndex, - }, + }, s_dali_domain.get()); ESP_ERROR_CHECK(s_runtime->start()); + s_dali_domain->setApplicationControllerShortAddressProvider( + [](uint8_t channel_index, uint8_t gateway_id) { + return s_runtime == nullptr + ? std::optional(static_cast(gateway_id & 0x3fU)) + : s_runtime->applicationControllerShortAddressForChannel(channel_index, + gateway_id & 0x3fU); + }); + s_dali_domain->setApplicationControllerShortAddressSink( + [](uint8_t channel_index, std::optional short_address) { + if (s_runtime == nullptr || + !s_runtime->setApplicationControllerShortAddressForChannel(channel_index, + short_address)) { + ESP_LOGE(kTag, "failed to persist Part 103 short address for channel=%u", + channel_index); + } + }); ESP_ERROR_CHECK(BindConfiguredChannels(*s_dali_domain, *s_runtime)); s_runtime->setGatewayCount(s_dali_domain->channelCount()); diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 3c09e6b..8d3ba0b 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -691,6 +691,12 @@ CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE=1200 # # end of Gateway Channel 16 +# +# DALI Part 103 Application Controller +# +# CONFIG_GATEWAY_DALI_103_APPLICATION_CONTROLLER_ENABLED is not set +# end of DALI Part 103 Application Controller + # # Gateway Cache # diff --git a/components/dali_domain/include/dali_domain.hpp b/components/dali_domain/include/dali_domain.hpp index 3fd738b..2ba9d23 100644 --- a/components/dali_domain/include/dali_domain.hpp +++ b/components/dali_domain/include/dali_domain.hpp @@ -77,6 +77,7 @@ struct DaliRawFrame { uint8_t gateway_id{0}; DaliPhyKind phy_kind{DaliPhyKind::kCustom}; std::vector data; + bool double_send_confirmed{false}; }; enum class DaliDt8SceneColorMode { @@ -115,6 +116,11 @@ struct DaliAddressSettingsSnapshot { class DaliDomainService { public: + using ApplicationControllerIdentitySink = std::function; + using ApplicationControllerShortAddressProvider = + std::function(uint8_t channel_index, uint8_t gateway_id)>; + using ApplicationControllerShortAddressSink = + std::function short_address)>; DaliDomainService(); ~DaliDomainService(); @@ -127,6 +133,10 @@ class DaliDomainService { size_t channelCount() const; std::vector channelInfo() const; void addRawFrameSink(std::function sink); + void setApplicationControllerEnabled(bool enabled); + void setApplicationControllerIdentitySink(ApplicationControllerIdentitySink sink); + void setApplicationControllerShortAddressProvider(ApplicationControllerShortAddressProvider provider); + void setApplicationControllerShortAddressSink(ApplicationControllerShortAddressSink sink); bool resetBus(uint8_t gateway_id) const; bool pulseBusLow(uint8_t gateway_id, uint32_t duration_ms) const; @@ -215,6 +225,11 @@ class DaliDomainService { TickType_t tick{0}; bool valid{false}; }; + struct RecentForwardFrame { + std::array data{}; + TickType_t tick{0}; + bool valid{false}; + }; DaliChannel* findChannelByGateway(uint8_t gateway_id); const DaliChannel* findChannelByGateway(uint8_t gateway_id) const; @@ -243,6 +258,11 @@ class DaliDomainService { mutable SemaphoreHandle_t host_activity_lock_{nullptr}; mutable std::map last_host_activity_ticks_; mutable std::map recent_host_command_frames_; + std::map recent_forward_frames_; + bool application_controller_enabled_{false}; + ApplicationControllerIdentitySink application_controller_identity_sink_; + ApplicationControllerShortAddressProvider application_controller_short_address_provider_; + ApplicationControllerShortAddressSink application_controller_short_address_sink_; QueueHandle_t raw_frame_dispatch_queue_{nullptr}; TaskHandle_t raw_frame_dispatch_task_handle_{nullptr}; TaskHandle_t raw_frame_task_handle_{nullptr}; diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index 63eeace..4e4e067 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -6,6 +6,7 @@ #include "dali.h" #include "dali_hal.h" #include "dali.hpp" +#include "dali_gateway.hpp" #include "driver/uart.h" #include "freertos/queue.h" @@ -42,6 +43,7 @@ constexpr uint32_t kRawFrameRxTaskStackSize = CONFIG_DALI_DOMAIN_RAW_RX_TASK_STA constexpr uint32_t kRawFrameDispatchTaskStackSize = CONFIG_DALI_DOMAIN_RAW_DISPATCH_TASK_STACK_SIZE; constexpr uint32_t kHardwareQueryRawPostSuppressMs = 10; +constexpr uint32_t kDaliDoubleSendWindowMs = 100; constexpr uint8_t kControlDeviceSendOpcode = 0x60; constexpr uint8_t kControlDeviceSendTwiceOpcode = 0x61; constexpr uint8_t kControlDeviceQueryOpcode = 0x62; @@ -439,8 +441,24 @@ struct DaliDomainService::DaliChannel { std::optional serial_bus; QueueHandle_t serial_rx_queue{nullptr}; TaskHandle_t serial_rx_task_handle{nullptr}; + std::unique_ptr application_controller; }; +std::optional ApplicationControllerShortAddress( + const DaliDomainService::ApplicationControllerShortAddressProvider& provider, + const DaliChannelConfig& channel) { + if (provider) { + const auto short_address = provider(channel.channel_index, channel.gateway_id); + if (short_address.has_value() && short_address.value() <= 63) { + return short_address; + } + if (!short_address.has_value()) { + return std::nullopt; + } + } + return static_cast(channel.gateway_id & 0x3fU); +} + DaliDomainService::DaliDomainService() : raw_frame_sink_lock_(xSemaphoreCreateMutex()), bus_activity_lock_(xSemaphoreCreateMutex()), @@ -475,6 +493,13 @@ bool DaliDomainService::bindTransport(const DaliChannelConfig& config, DaliTrans channel->comm = std::make_unique(channel->hooks.send, channel->hooks.read, channel->hooks.transact, channel->hooks.delay); channel->dali = std::make_unique(*channel->comm, config.gateway_id, config.name); + if (application_controller_enabled_) { + DaliApplicationControllerConfig application_config; + application_config.shortAddress = + ApplicationControllerShortAddress(application_controller_short_address_provider_, config); + channel->application_controller = + std::make_unique(application_config); + } auto* existing = findChannelByIndex(config.channel_index); if (existing != nullptr) { @@ -702,6 +727,37 @@ void DaliDomainService::addRawFrameSink(std::functionapplication_controller.reset(); + continue; + } + if (channel->application_controller != nullptr) { + continue; + } + DaliApplicationControllerConfig config; + config.shortAddress = ApplicationControllerShortAddress( + application_controller_short_address_provider_, channel->config); + channel->application_controller = std::make_unique(config); + } +} + +void DaliDomainService::setApplicationControllerIdentitySink(ApplicationControllerIdentitySink sink) { + application_controller_identity_sink_ = std::move(sink); +} + +void DaliDomainService::setApplicationControllerShortAddressProvider( + ApplicationControllerShortAddressProvider provider) { + application_controller_short_address_provider_ = std::move(provider); +} + +void DaliDomainService::setApplicationControllerShortAddressSink( + ApplicationControllerShortAddressSink sink) { + application_controller_short_address_sink_ = std::move(sink); +} + bool DaliDomainService::resetBus(uint8_t gateway_id) const { const auto* channel = findChannelByGateway(gateway_id); if (channel == nullptr || channel->comm == nullptr) { @@ -1873,6 +1929,37 @@ void DaliDomainService::rawFrameDispatchTaskLoop() { frame.gateway_id = event.gateway_id; frame.phy_kind = event.phy_kind; frame.data.assign(event.data, event.data + event.len); + if (event.len == 3) { + const TickType_t now = xTaskGetTickCount(); + auto& recent = recent_forward_frames_[event.gateway_id]; + const bool same = recent.valid && recent.data[0] == event.data[0] && + recent.data[1] == event.data[1] && recent.data[2] == event.data[2]; + frame.double_send_confirmed = + same && (now - recent.tick) <= pdMS_TO_TICKS(kDaliDoubleSendWindowMs); + recent.data = {event.data[0], event.data[1], event.data[2]}; + recent.tick = now; + recent.valid = true; + } + if (auto* channel = findChannelByGateway(frame.gateway_id); + channel != nullptr && channel->application_controller != nullptr && frame.data.size() == 3) { + const std::array forward{frame.data[0], frame.data[1], frame.data[2]}; + const auto result = channel->application_controller->handleForwardFrame( + forward, frame.double_send_confirmed); + if (result.backwardFrame.has_value()) { + sendBackwardFrame(frame.gateway_id, result.backwardFrame.value()); + } + if (result.identifyRequested && application_controller_identity_sink_) { + application_controller_identity_sink_(frame.gateway_id); + } + if (result.shortAddressChanged) { + ESP_LOGI(TAG, "Part 103 application controller gateway=%u short-address=%d", + frame.gateway_id, result.shortAddress.has_value() ? result.shortAddress.value() : -1); + if (application_controller_short_address_sink_) { + application_controller_short_address_sink_(channel->config.channel_index, + result.shortAddress); + } + } + } notifyRawFrameSinks(frame); } } diff --git a/components/gateway_cache/CMakeLists.txt b/components/gateway_cache/CMakeLists.txt index c363795..d9e6f13 100644 --- a/components/gateway_cache/CMakeLists.txt +++ b/components/gateway_cache/CMakeLists.txt @@ -1,7 +1,7 @@ idf_component_register( SRCS "src/gateway_cache.cpp" INCLUDE_DIRS "include" - REQUIRES freertos log nvs_flash + REQUIRES dali_cpp freertos log nvs_flash ) -set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/components/gateway_cache/include/gateway_cache.hpp b/components/gateway_cache/include/gateway_cache.hpp index 621de8e..bc1913e 100644 --- a/components/gateway_cache/include/gateway_cache.hpp +++ b/components/gateway_cache/include/gateway_cache.hpp @@ -12,6 +12,7 @@ #include "freertos/semphr.h" #include "freertos/task.h" #include "nvs.h" +#include "dali_gateway.hpp" namespace gateway { @@ -232,9 +233,11 @@ class GatewayCache { std::string readStringLocked(std::string_view key); bool writeStringLocked(std::string_view key, std::string_view value); bool eraseKeyLocked(std::string_view key); + void syncDaliStatesFromCoreLocked(uint8_t gateway_id); GatewayCacheConfig config_; GatewayCachePriorityMode priority_mode_; + DaliGatewayCache dali_gateway_cache_; TaskHandle_t task_handle_{nullptr}; SemaphoreHandle_t lock_{nullptr}; nvs_handle_t storage_{0}; diff --git a/components/gateway_cache/src/gateway_cache.cpp b/components/gateway_cache/src/gateway_cache.cpp index c20f03a..9e574ce 100644 --- a/components/gateway_cache/src/gateway_cache.cpp +++ b/components/gateway_cache/src/gateway_cache.cpp @@ -115,14 +115,102 @@ bool IsDefaultGroup(const GatewayCache::GroupEntry& group) { return !group.enabled && group.target_type == 2 && group.target_value == 0; } -bool SameFlags(const GatewayCacheChannelFlags& lhs, const GatewayCacheChannelFlags& rhs) { - return lhs.need_update_group == rhs.need_update_group && - lhs.need_update_scene == rhs.need_update_scene && - lhs.need_update_settings == rhs.need_update_settings; +DaliGatewayCachePriorityMode ToDaliCppPriorityMode(GatewayCachePriorityMode mode) { + return mode == GatewayCachePriorityMode::kLocalGatewayFirst + ? DaliGatewayCachePriorityMode::localGatewayFirst + : DaliGatewayCachePriorityMode::outsideBusFirst; } -bool AnyFlagSet(const GatewayCacheChannelFlags& flags) { - return flags.need_update_group || flags.need_update_scene || flags.need_update_settings; +GatewayCacheDaliPresence FromDaliCppPresence(DaliGatewayPresence presence) { + switch (presence) { + case DaliGatewayPresence::online: + return GatewayCacheDaliPresence::kOnline; + case DaliGatewayPresence::offline: + return GatewayCacheDaliPresence::kOffline; + case DaliGatewayPresence::unknown: + default: + return GatewayCacheDaliPresence::kUnknown; + } +} + +DaliGatewayPresence ToDaliCppPresence(GatewayCacheDaliPresence presence) { + switch (presence) { + case GatewayCacheDaliPresence::kOnline: + return DaliGatewayPresence::online; + case GatewayCacheDaliPresence::kOffline: + return DaliGatewayPresence::offline; + case GatewayCacheDaliPresence::kUnknown: + default: + return DaliGatewayPresence::unknown; + } +} + +GatewayCacheDaliRuntimeStatus FromDaliCppStatus(const DaliGatewayRuntimeStatus& value) { + return {value.actualLevel, value.sceneID, value.useMinLevel, value.stale, value.revision}; +} + +DaliGatewayRuntimeStatus ToDaliCppStatus(const GatewayCacheDaliRuntimeStatus& value) { + return {value.actual_level, value.scene_id, value.use_min_level, value.stale, value.revision}; +} + +GatewayCacheDaliSettingsSnapshot FromDaliCppSettings(const DaliGatewaySettingsSnapshot& value) { + return {value.powerOnLevel, value.systemFailureLevel, value.minLevel, value.maxLevel, + value.fadeTime, value.fadeRate}; +} + +DaliGatewaySettingsSnapshot ToDaliCppSettings(const GatewayCacheDaliSettingsSnapshot& value) { + return {value.power_on_level, value.system_failure_level, value.min_level, value.max_level, + value.fade_time, value.fade_rate}; +} + +GatewayCacheDaliAddressState FromDaliCppAddressState(const DaliGatewayAddressState& value) { + GatewayCacheDaliAddressState result; + result.group_mask_known = value.groupMaskKnown; + result.group_mask = value.groupMask; + result.scene_levels = value.sceneLevels; + result.settings = FromDaliCppSettings(value.settings); + result.status = FromDaliCppStatus(value.status); + return result; +} + +DaliGatewayAddressState ToDaliCppAddressState(const GatewayCacheDaliAddressState& value) { + DaliGatewayAddressState result; + result.groupMaskKnown = value.group_mask_known; + result.groupMask = value.group_mask; + result.sceneLevels = value.scene_levels; + result.settings = ToDaliCppSettings(value.settings); + result.status = ToDaliCppStatus(value.status); + return result; +} + +GatewayCacheChannelFlags FromDaliCppFlags(const DaliGatewayChannelFlags& value) { + return {value.needUpdateGroup, value.needUpdateScene, value.needUpdateSettings}; +} + +DaliGatewayChannelFlags ToDaliCppFlags(const GatewayCacheChannelFlags& value) { + return {value.need_update_group, value.need_update_scene, value.need_update_settings}; +} + +std::optional ToDaliCppTarget(std::optional target) { + if (!target.has_value()) return std::nullopt; + DaliGatewayTargetKind kind = DaliGatewayTargetKind::shortAddress; + if (target->kind == GatewayCacheDaliTargetKind::kGroup) { + kind = DaliGatewayTargetKind::group; + } else if (target->kind == GatewayCacheDaliTargetKind::kBroadcast) { + kind = DaliGatewayTargetKind::broadcast; + } + return DaliGatewayTarget{kind, target->value}; +} + +std::optional FromDaliCppTarget(std::optional target) { + if (!target.has_value()) return std::nullopt; + GatewayCacheDaliTargetKind kind = GatewayCacheDaliTargetKind::kShortAddress; + if (target->kind == DaliGatewayTargetKind::group) { + kind = GatewayCacheDaliTargetKind::kGroup; + } else if (target->kind == DaliGatewayTargetKind::broadcast) { + kind = GatewayCacheDaliTargetKind::kBroadcast; + } + return GatewayCacheDaliTarget{kind, target->value}; } std::optional DecodeDaliTarget(uint8_t raw_addr) { @@ -140,24 +228,6 @@ std::optional DecodeDaliTarget(uint8_t raw_addr) { return std::nullopt; } -bool ShouldMirrorObservedMutation(GatewayCacheRawFrameOrigin origin, - GatewayCachePriorityMode priority_mode) { - return origin == GatewayCacheRawFrameOrigin::kLocalGateway || - priority_mode == GatewayCachePriorityMode::kOutsideBusFirst; -} - -bool ShouldAlwaysMirrorObservedStatus(uint8_t raw_addr, uint8_t command) { - if (!DecodeDaliTarget(raw_addr).has_value()) { - return false; - } - if ((raw_addr & 0x01) == 0) { - return command <= 254; - } - return command == kDaliCmdOff || command == kDaliCmdRecallMax || - command == kDaliCmdRecallMin || - (command >= kDaliCmdGoToSceneMin && command <= kDaliCmdGoToSceneMax); -} - void ClearDaliState(GatewayCacheDaliAddressState& state) { state.group_mask_known = false; state.group_mask = 0; @@ -192,39 +262,6 @@ void ApplyObservedSettingsValue(GatewayCacheDaliSettingsSnapshot& settings, uint } } -GatewayCacheChannelFlags ClassifyDaliMutation(uint8_t raw_addr, uint8_t command) { - GatewayCacheChannelFlags flags; - if (raw_addr == kDaliCmdSpecialProgramShortAddress) { - flags.need_update_settings = true; - return flags; - } - - const bool special_command = raw_addr >= 0xA1 && raw_addr <= 0xC5 && (raw_addr & 0x01) != 0; - if (special_command || (raw_addr & 0x01) == 0) { - return flags; - } - - if (command == kDaliCmdReset) { - flags.need_update_group = true; - flags.need_update_scene = true; - flags.need_update_settings = true; - } else if (command >= kDaliCmdStoreDtrAsMaxLevel && command <= kDaliCmdStoreDtrAsFadeRate) { - flags.need_update_settings = true; - } else if (command >= kDaliCmdSetSceneMin && command <= kDaliCmdRemoveSceneMax) { - flags.need_update_scene = true; - } else if (command >= kDaliCmdAddToGroupMin && command <= kDaliCmdRemoveFromGroupMax) { - flags.need_update_group = true; - } else if (command == kDaliCmdStoreDtrAsShortAddress) { - flags.need_update_settings = true; - } else if (command == kDaliCmdDt8StoreDtrAsColorX || command == kDaliCmdDt8StoreDtrAsColorY || - (command >= kDaliCmdDt8StorePrimaryMin && - command <= kDaliCmdDt8StartAutoCalibration)) { - flags.need_update_settings = true; - } - - return flags; -} - std::string BuildScenePayload(const GatewayCache::SceneEntry& scene) { char payload[32] = {0}; std::snprintf(payload, sizeof(payload), "%u,%u,%u,%u,%u,%u", scene.enabled ? 1 : 0, @@ -377,6 +414,10 @@ void ApplyDaliStatePayload(std::string_view raw, GatewayCacheDaliAddressState& s GatewayCache::GatewayCache(GatewayCacheConfig config) : config_(std::move(config)), priority_mode_(config_.default_priority_mode), + dali_gateway_cache_(DaliGatewayCacheConfig{config_.cache_enabled, + config_.reconciliation_enabled, + config_.full_state_mirror_enabled, + ToDaliCppPriorityMode(config_.default_priority_mode)}), lock_(xSemaphoreCreateRecursiveMutex()) {} GatewayCache::~GatewayCache() { @@ -446,6 +487,11 @@ void GatewayCache::preloadChannel(uint8_t gateway_id) { if (inserted) { loadDaliStateStoreLocked(gateway_id, it->second); } + std::array states; + for (size_t index = 0; index < states.size(); ++index) { + states[index] = ToDaliCppAddressState(it->second[index]); + } + dali_gateway_cache_.restoreAddressStates(gateway_id, states); } GatewayCache::SceneStore GatewayCache::scenes(uint8_t gateway_id) { @@ -739,225 +785,115 @@ std::pair GatewayCache::groupMask(uint8_t gateway_id) { GatewayCacheChannelFlags GatewayCache::channelFlags(uint8_t gateway_id) { LockGuard guard(lock_); - if (!shouldTrackUpdateFlagsLocked()) { - return {}; - } - return channel_flags_[gateway_id]; + return FromDaliCppFlags(dali_gateway_cache_.channelFlags(gateway_id)); } GatewayCacheChannelFlags GatewayCache::pendingChannelFlags(uint8_t gateway_id) { LockGuard guard(lock_); - return shouldTrackUpdateFlagsLocked() ? channel_flags_[gateway_id] : GatewayCacheChannelFlags{}; + return FromDaliCppFlags(dali_gateway_cache_.pendingChannelFlags(gateway_id)); } GatewayCacheDaliAddressState GatewayCache::daliAddressState(uint8_t gateway_id, uint8_t short_address) { LockGuard guard(lock_); - if (short_address >= 64) { - return {}; - } - return ensureDaliAddressStateLocked(gateway_id, short_address); + if (short_address >= 64) return {}; + return FromDaliCppAddressState(dali_gateway_cache_.addressState(gateway_id, short_address)); } GatewayCacheDaliPresence GatewayCache::daliAddressPresence(uint8_t gateway_id, uint8_t short_address) { LockGuard guard(lock_); - if (short_address >= 64) { - return GatewayCacheDaliPresence::kUnknown; - } - if (const auto it = dali_presence_.find(gateway_id); it != dali_presence_.end()) { - return it->second[short_address]; - } - return GatewayCacheDaliPresence::kUnknown; + return short_address >= 64 ? GatewayCacheDaliPresence::kUnknown + : FromDaliCppPresence(dali_gateway_cache_.addressPresence(gateway_id, + short_address)); } void GatewayCache::markDaliAddressPresence(uint8_t gateway_id, uint8_t short_address, GatewayCacheDaliPresence presence) { LockGuard guard(lock_); - markDaliAddressPresenceLocked(gateway_id, short_address, presence); + dali_gateway_cache_.markAddressPresence(gateway_id, short_address, ToDaliCppPresence(presence)); } std::optional GatewayCache::decodeDaliTarget(uint8_t raw_addr) { - return DecodeDaliTarget(raw_addr); + return FromDaliCppTarget(DaliGatewayCache::decodeTarget(raw_addr)); } std::vector GatewayCache::reconciliationAddresses( uint8_t gateway_id, std::optional target) { LockGuard guard(lock_); - std::vector addresses; - - auto presence = [&](uint8_t short_address) { - if (const auto it = dali_presence_.find(gateway_id); it != dali_presence_.end()) { - return it->second[short_address]; - } - return GatewayCacheDaliPresence::kUnknown; - }; - auto add_if_known_online = [&](uint8_t short_address) { - if (short_address < 64 && presence(short_address) == GatewayCacheDaliPresence::kOnline) { - addresses.push_back(short_address); - } - }; - - if (!target.has_value()) { - for (uint8_t short_address = 0; short_address < 64; ++short_address) { - add_if_known_online(short_address); - } - return addresses; - } - - switch (target->kind) { - case GatewayCacheDaliTargetKind::kShortAddress: - if (target->value < 64 && presence(target->value) != GatewayCacheDaliPresence::kOffline) { - addresses.push_back(target->value); - } - break; - case GatewayCacheDaliTargetKind::kGroup: { - if (target->value >= 16) { - break; - } - const uint16_t bit = static_cast(1U << target->value); - auto [states_it, inserted] = dali_states_.try_emplace(gateway_id); - if (inserted) { - loadDaliStateStoreLocked(gateway_id, states_it->second); - } - for (uint8_t short_address = 0; short_address < states_it->second.size(); ++short_address) { - const auto& state = states_it->second[short_address]; - if (state.group_mask_known && (state.group_mask & bit) != 0) { - add_if_known_online(short_address); - } - } - break; - } - case GatewayCacheDaliTargetKind::kBroadcast: - for (uint8_t short_address = 0; short_address < 64; ++short_address) { - add_if_known_online(short_address); - } - break; - default: - break; - } - return addresses; + return dali_gateway_cache_.reconciliationAddresses(gateway_id, ToDaliCppTarget(target)); } GatewayCacheDaliRuntimeStatus GatewayCache::daliGroupStatus(uint8_t gateway_id, uint8_t group_id) { LockGuard guard(lock_); - if (group_id >= 16) { - return {}; - } - return ensureDaliGroupStatusLocked(gateway_id, group_id); + return group_id >= 16 ? GatewayCacheDaliRuntimeStatus{} + : FromDaliCppStatus(dali_gateway_cache_.groupStatus(gateway_id, group_id)); } GatewayCacheDaliRuntimeStatus GatewayCache::daliBroadcastStatus(uint8_t gateway_id) { LockGuard guard(lock_); - return ensureDaliBroadcastStatusLocked(gateway_id); + return FromDaliCppStatus(dali_gateway_cache_.broadcastStatus(gateway_id)); } bool GatewayCache::setDaliGroupMask(uint8_t gateway_id, uint8_t short_address, std::optional group_mask) { LockGuard guard(lock_); - if (short_address >= 64) { - return false; - } - - auto& state = ensureDaliAddressStateLocked(gateway_id, short_address); - state.group_mask_known = group_mask.has_value(); - state.group_mask = group_mask.value_or(0); - refreshDaliAddressAggregateStatusLocked(gateway_id, state); - dirty_ = true; - return true; + const bool changed = dali_gateway_cache_.setGroupMask(gateway_id, short_address, group_mask); + if (changed) { syncDaliStatesFromCoreLocked(gateway_id); dirty_ = true; } + return changed; } bool GatewayCache::setDaliSceneLevel(uint8_t gateway_id, uint8_t short_address, uint8_t scene_id, std::optional level) { LockGuard guard(lock_); - if (short_address >= 64 || scene_id >= 16) { - return false; - } - - auto& state = ensureDaliAddressStateLocked(gateway_id, short_address); - state.scene_levels[scene_id] = level; - dirty_ = true; - return true; + const bool changed = dali_gateway_cache_.setSceneLevel(gateway_id, short_address, scene_id, level); + if (changed) { syncDaliStatesFromCoreLocked(gateway_id); dirty_ = true; } + return changed; } bool GatewayCache::setDaliSettings(uint8_t gateway_id, uint8_t short_address, std::optional settings) { LockGuard guard(lock_); - if (short_address >= 64) { - return false; - } - - auto& state = ensureDaliAddressStateLocked(gateway_id, short_address); - state.settings = settings.value_or(GatewayCacheDaliSettingsSnapshot{}); - dirty_ = true; - return true; + const auto core_settings = settings.has_value() + ? std::optional(ToDaliCppSettings(*settings)) + : std::nullopt; + const bool changed = dali_gateway_cache_.setSettings(gateway_id, short_address, core_settings); + if (changed) { syncDaliStatesFromCoreLocked(gateway_id); dirty_ = true; } + return changed; } bool GatewayCache::setDaliActualLevel(uint8_t gateway_id, uint8_t short_address, std::optional level) { LockGuard guard(lock_); - if (short_address >= 64) { - return false; - } - - GatewayCacheDaliRuntimeStatus status; - status.actual_level = level; - status.revision = nextDaliRuntimeRevisionLocked(); - status.stale = false; - auto& state = ensureDaliAddressStateLocked(gateway_id, short_address); - state.status.scene_id.reset(); - state.status.use_min_level = false; - applyDaliRuntimeStatusToAddressLocked(state, status); - if (!level.has_value()) { - state.status.actual_level.reset(); - state.status.revision = status.revision; - state.status.stale = false; - } - dirty_ = true; - return true; + const bool changed = dali_gateway_cache_.setActualLevel(gateway_id, short_address, level); + if (changed) { syncDaliStatesFromCoreLocked(gateway_id); dirty_ = true; } + return changed; } bool GatewayCache::clearChannelFlagsIfMatched(uint8_t gateway_id, const GatewayCacheChannelFlags& flags) { LockGuard guard(lock_); - if (!shouldTrackUpdateFlagsLocked()) { - return true; - } - auto& current = channel_flags_[gateway_id]; - if (!SameFlags(current, flags)) { - return false; - } - current = {}; - return true; + return dali_gateway_cache_.clearChannelFlagsIfMatched(gateway_id, ToDaliCppFlags(flags)); } void GatewayCache::markGroupUpdateNeeded(uint8_t gateway_id, bool needed) { LockGuard guard(lock_); - if (!shouldTrackUpdateFlagsLocked()) { - return; - } - channel_flags_[gateway_id].need_update_group = needed; + dali_gateway_cache_.markGroupUpdateNeeded(gateway_id, needed); } void GatewayCache::markSceneUpdateNeeded(uint8_t gateway_id, bool needed) { LockGuard guard(lock_); - if (!shouldTrackUpdateFlagsLocked()) { - return; - } - channel_flags_[gateway_id].need_update_scene = needed; + dali_gateway_cache_.markSceneUpdateNeeded(gateway_id, needed); } void GatewayCache::markSettingsUpdateNeeded(uint8_t gateway_id, bool needed) { LockGuard guard(lock_); - if (!shouldTrackUpdateFlagsLocked()) { - return; - } - channel_flags_[gateway_id].need_update_settings = needed; + dali_gateway_cache_.markSettingsUpdateNeeded(gateway_id, needed); } bool GatewayCache::cacheEnabled() const { - return config_.cache_enabled; + return dali_gateway_cache_.enabled(); } bool GatewayCache::setCacheEnabled(bool enabled) { @@ -975,80 +911,73 @@ bool GatewayCache::setCacheEnabled(bool enabled) { } LockGuard guard(lock_); config_.cache_enabled = false; + dali_gateway_cache_.setEnabled(false); return true; } { LockGuard guard(lock_); config_.cache_enabled = true; + dali_gateway_cache_.setEnabled(true); } return start() == ESP_OK; } bool GatewayCache::reconciliationEnabled() const { - return config_.cache_enabled && config_.reconciliation_enabled; + return dali_gateway_cache_.reconciliationEnabled(); } bool GatewayCache::fullStateMirrorEnabled() const { - return reconciliationEnabled() && config_.full_state_mirror_enabled; + return dali_gateway_cache_.fullStateMirrorEnabled(); } bool GatewayCache::mirrorDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) { LockGuard guard(lock_); - if (!config_.cache_enabled) { - return false; + const bool changed = dali_gateway_cache_.mirrorForwardFrame(gateway_id, raw_addr, command); + if (changed) { + syncDaliStatesFromCoreLocked(gateway_id); + dirty_ = true; } - return mirrorDaliCommandLocked(gateway_id, raw_addr, command); + return changed; } bool GatewayCache::observeDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint8_t command, GatewayCacheRawFrameOrigin origin) { LockGuard guard(lock_); - if (!config_.cache_enabled) { - return false; + bool changed = false; + if (origin == GatewayCacheRawFrameOrigin::kOutsideBus) { + changed = dali_gateway_cache_.observeForwardFrame(gateway_id, raw_addr, command, + DaliGatewayFrameOrigin::outsideBus); + } else { + dali_gateway_cache_.mirrorForwardFrame(gateway_id, raw_addr, command); } - - if (ShouldAlwaysMirrorObservedStatus(raw_addr, command) || - ShouldMirrorObservedMutation(origin, priority_mode_)) { - mirrorDaliCommandLocked(gateway_id, raw_addr, command); - } - - if (!shouldTrackUpdateFlagsLocked()) { - return false; - } - const auto detected = ClassifyDaliMutation(raw_addr, command); - if (!AnyFlagSet(detected)) { - return false; - } - - if (origin != GatewayCacheRawFrameOrigin::kOutsideBus) { - return false; - } - - auto& current = channel_flags_[gateway_id]; - const bool changed = (!current.need_update_group && detected.need_update_group) || - (!current.need_update_scene && detected.need_update_scene) || - (!current.need_update_settings && detected.need_update_settings); - current.need_update_group = current.need_update_group || detected.need_update_group; - current.need_update_scene = current.need_update_scene || detected.need_update_scene; - current.need_update_settings = current.need_update_settings || detected.need_update_settings; - - if (changed) { - ESP_LOGI(kTag, "outside DALI mutation gateway=%u addr=0x%02x cmd=0x%02x flags g=%d s=%d cfg=%d", - gateway_id, raw_addr, command, current.need_update_group, current.need_update_scene, - current.need_update_settings); + if (dali_gateway_cache_.enabled()) { + syncDaliStatesFromCoreLocked(gateway_id); + dirty_ = true; } return changed; } GatewayCachePriorityMode GatewayCache::priorityMode() { LockGuard guard(lock_); - return priority_mode_; + return dali_gateway_cache_.priorityMode() == DaliGatewayCachePriorityMode::localGatewayFirst + ? GatewayCachePriorityMode::kLocalGatewayFirst + : GatewayCachePriorityMode::kOutsideBusFirst; } void GatewayCache::setPriorityMode(GatewayCachePriorityMode mode) { LockGuard guard(lock_); priority_mode_ = mode; + dali_gateway_cache_.setPriorityMode(ToDaliCppPriorityMode(mode)); +} + +void GatewayCache::syncDaliStatesFromCoreLocked(uint8_t gateway_id) { + const auto states = dali_gateway_cache_.addressStates(gateway_id); + auto& legacy = dali_states_[gateway_id]; + for (size_t index = 0; index < states.size(); ++index) { + legacy[index] = FromDaliCppAddressState(states[index]); + dali_runtime_revision_ = std::max(dali_runtime_revision_, legacy[index].status.revision); + } } uint32_t GatewayCache::nextDaliRuntimeRevisionLocked() { diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index bebb18f..15da8d8 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -242,9 +242,6 @@ class GatewayController { void captureTransactionFrame(const std::vector& frame); void captureTransactionCompletion(const std::vector& command); void handleDaliRawFrame(const DaliRawFrame& frame); - bool handleApplicationControllerFrame(const DaliRawFrame& frame); - std::optional applicationControllerResponse(uint8_t gateway_id, uint8_t first, - uint8_t instance, uint8_t opcode) const; bool sendRawAndMirror(uint8_t gateway_id, uint8_t raw_addr, uint8_t command); bool sendExtRawAndMirror(uint8_t gateway_id, uint8_t raw_addr, uint8_t command); @@ -329,14 +326,6 @@ class GatewayController { bool ble_enabled_{false}; bool wifi_enabled_{false}; bool ip_router_enabled_{true}; - bool application_controller_enabled_{true}; - bool application_controller_power_cycle_notification_{true}; - bool application_controller_power_cycle_seen_{true}; - bool application_controller_reset_state_{false}; - uint8_t application_controller_operating_mode_{0}; - uint8_t application_controller_dtr0_{0}; - uint8_t application_controller_dtr1_{0}; - uint8_t application_controller_dtr2_{0}; }; } // namespace gateway diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index 7d63825..d70ed13 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -1815,160 +1815,6 @@ void GatewayController::captureTransactionCompletion(const std::vector& } } -std::optional GatewayController::applicationControllerResponse( - uint8_t gateway_id, uint8_t first, uint8_t instance, uint8_t opcode) const { - const uint8_t gateway_short = - static_cast(((gateway_id & 0x3F) << 1) | 0x01); - const bool addressed_to_gateway = first == 0xFF || first == gateway_short; - if (!addressed_to_gateway || instance != 0xFE) { - return std::nullopt; - } - - switch (opcode) { - case 0x30: { - uint8_t status = 0; - if (application_controller_enabled_) { - status |= 0x08; - } - if (application_controller_power_cycle_seen_) { - status |= 0x20; - } - if (application_controller_reset_state_) { - status |= 0x40; - } - return status; - } - case 0x31: - case 0x32: - case 0x33: - return 0x00; - case 0x34: - return 0x02; - case 0x35: - return 0x00; - case 0x36: - return application_controller_dtr0_; - case 0x37: - return application_controller_dtr1_; - case 0x38: - return application_controller_dtr2_; - case 0x39: - case 0x3A: - case 0x3B: - return 0x00; - case 0x3C: - return 0xFF; - case 0x3D: - return static_cast(application_controller_enabled_ ? 1 : 0); - case 0x3E: - return application_controller_operating_mode_; - case 0x3F: - case 0x40: - case 0x41: - case 0x42: - case 0x43: - case 0x44: - return 0x00; - case 0x45: - return static_cast(application_controller_power_cycle_notification_ ? 1 : 0); - case 0x46: - return 0x01; - case 0x47: - return 0x01; - case 0x48: - return static_cast(application_controller_reset_state_ ? 1 : 0); - default: - return std::nullopt; - } -} - -bool GatewayController::handleApplicationControllerFrame(const DaliRawFrame& frame) { - if (frame.data.size() != 3) { - return false; - } - - const uint8_t first = frame.data[0]; - const uint8_t instance = frame.data[1]; - const uint8_t opcode = frame.data[2]; - - if (first == 0xC1) { - switch (instance) { - case 0x30: - application_controller_dtr0_ = opcode; - break; - case 0x31: - application_controller_dtr1_ = opcode; - break; - case 0x32: - application_controller_dtr2_ = opcode; - break; - default: - break; - } - return false; - } - - if ((first & 0x01) == 0) { - return false; - } - - const uint8_t gateway_short = - static_cast(((frame.gateway_id & 0x3F) << 1) | 0x01); - if (first != 0xFF && first != gateway_short) { - return false; - } - if (instance != 0xFE) { - return false; - } - - switch (opcode) { - case 0x01: - application_controller_power_cycle_seen_ = false; - application_controller_reset_state_ = false; - break; - case 0x10: - application_controller_enabled_ = true; - application_controller_power_cycle_notification_ = true; - application_controller_reset_state_ = true; - application_controller_operating_mode_ = 0; - application_controller_dtr0_ = 0; - application_controller_dtr1_ = 0; - application_controller_dtr2_ = 0; - break; - case 0x16: - application_controller_enabled_ = true; - application_controller_reset_state_ = false; - break; - case 0x17: - application_controller_enabled_ = false; - application_controller_reset_state_ = false; - break; - case 0x18: - application_controller_operating_mode_ = application_controller_dtr0_; - application_controller_reset_state_ = false; - break; - case 0x1F: - application_controller_power_cycle_notification_ = true; - application_controller_reset_state_ = false; - break; - case 0x20: - application_controller_power_cycle_notification_ = false; - application_controller_reset_state_ = false; - break; - case 0x21: - application_controller_reset_state_ = false; - break; - default: - break; - } - - const auto response = applicationControllerResponse(frame.gateway_id, first, instance, opcode); - if (!response.has_value()) { - return false; - } - return dali_domain_.sendBackwardFrame(frame.gateway_id, response.value()); -} - void GatewayController::handleBridgeTransportCommand(uint8_t gateway_id, const std::vector& command) { const uint8_t version = command.size() > 4 ? command[4] : kBridgeTransportVersion; @@ -2054,7 +1900,6 @@ void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) { if (frame.data.size() == 3 && (((frame.data[0] & 0x01) != 0) || frame.data[0] == 0xC1)) { - handleApplicationControllerFrame(frame); const bool maintenance_activity = maintenance_activity_gateway_.load() == frame.gateway_id; if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || maintenance_activity || runtime_.hasActiveQueryCommand(frame.gateway_id) || diff --git a/components/gateway_network/include/gateway_network.hpp b/components/gateway_network/include/gateway_network.hpp index 1a2af32..c11c62c 100644 --- a/components/gateway_network/include/gateway_network.hpp +++ b/components/gateway_network/include/gateway_network.hpp @@ -89,6 +89,9 @@ class GatewayNetworkService { GatewayBridgeService* bridge_service = nullptr); esp_err_t start(); + // Physical adapter for the Part 103 IDENTIFY DEVICE action. The DALI core + // remains hardware-independent and invokes this only through the app wiring. + void identify(); /// Stable runtime settings surface shared by local HTTP, the bridge API, and /// cloud provisioning. The payload is a schema-versioned JSON document. @@ -100,6 +103,7 @@ class GatewayNetworkService { static void UdpTaskEntry(void* arg); static void TcpControlTaskEntry(void* arg); static void BootButtonTaskEntry(void* arg); + static void IdentifyTaskEntry(void* arg); static esp_err_t HandleInfoGet(httpd_req_t* req); static esp_err_t HandleCommandGet(httpd_req_t* req); static esp_err_t HandleCommandPost(httpd_req_t* req); @@ -136,6 +140,7 @@ class GatewayNetworkService { void udpTaskLoop(); void tcpControlTaskLoop(); void bootButtonTaskLoop(); + void identifyTaskLoop(); void handleNetworkControlBytes(const uint8_t* data, size_t len); std::optional handleJsonControlFrame(const uint8_t* data, size_t len); bool enqueueControlFrameForTargets(const std::vector& frame); @@ -184,6 +189,7 @@ class GatewayNetworkService { bool espnow_connected_{false}; std::array espnow_peer_{}; TaskHandle_t boot_button_task_handle_{nullptr}; + TaskHandle_t identify_task_handle_{nullptr}; TaskHandle_t udp_task_handle_{nullptr}; TaskHandle_t tcp_control_task_handle_{nullptr}; int udp_socket_{-1}; diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp index f0dc50b..72146f2 100644 --- a/components/gateway_network/src/gateway_network.cpp +++ b/components/gateway_network/src/gateway_network.cpp @@ -433,6 +433,23 @@ esp_err_t GatewayNetworkService::start() { return ESP_OK; } +void GatewayNetworkService::identify() { + if (config_.status_led_gpio < 0) { + ESP_LOGI(kTag, "Part 103 identity requested; no status LED is configured"); + return; + } + if (identify_task_handle_ != nullptr) { + return; + } + const BaseType_t created = xTaskCreate(&GatewayNetworkService::IdentifyTaskEntry, + "gateway_identify", 2048, this, 2, + &identify_task_handle_); + if (created != pdPASS) { + identify_task_handle_ = nullptr; + ESP_LOGW(kTag, "failed to start Part 103 identity indicator task"); + } +} + esp_err_t GatewayNetworkService::ensureNetworkStack() { esp_err_t err = esp_netif_init(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { @@ -1144,6 +1161,23 @@ void GatewayNetworkService::BootButtonTaskEntry(void* arg) { static_cast(arg)->bootButtonTaskLoop(); } +void GatewayNetworkService::IdentifyTaskEntry(void* arg) { + static_cast(arg)->identifyTaskLoop(); +} + +void GatewayNetworkService::identifyTaskLoop() { + // Five short flashes provide the physical IDENTIFY DEVICE indication without + // tying the generic DALI layer to an ESP timer or a GPIO implementation. + for (int flash = 0; flash < 5; ++flash) { + setStatusLed(true); + vTaskDelay(pdMS_TO_TICKS(200)); + setStatusLed(false); + vTaskDelay(pdMS_TO_TICKS(200)); + } + identify_task_handle_ = nullptr; + vTaskDelete(nullptr); +} + void GatewayNetworkService::HandleWifiEvent(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { auto* service = static_cast(arg); diff --git a/components/gateway_runtime/include/gateway_runtime.hpp b/components/gateway_runtime/include/gateway_runtime.hpp index 4fa0e11..4f6849b 100644 --- a/components/gateway_runtime/include/gateway_runtime.hpp +++ b/components/gateway_runtime/include/gateway_runtime.hpp @@ -111,6 +111,10 @@ class GatewaySettingsStore { bool setChannelGatewayId(uint8_t channel_index, uint8_t gateway_id); uint8_t getChannelGatewayGroup(uint8_t channel_index, uint8_t fallback) const; bool setChannelGatewayGroup(uint8_t channel_index, uint8_t gateway_group); + std::optional getApplicationControllerShortAddress(uint8_t channel_index, + uint8_t fallback) const; + bool setApplicationControllerShortAddress(uint8_t channel_index, + std::optional short_address); private: std::optional readString(std::string_view key) const; @@ -118,6 +122,7 @@ class GatewaySettingsStore { std::string makeGatewayNameKey(uint8_t gateway_id) const; std::string makeChannelGatewayIdKey(uint8_t channel_index) const; std::string makeChannelGatewayGroupKey(uint8_t channel_index) const; + std::string makeApplicationControllerShortAddressKey(uint8_t channel_index) const; mutable nvs_handle_t handle_{0}; }; @@ -183,6 +188,10 @@ class GatewayRuntime { bool setGatewayIdForChannel(uint8_t channel_index, uint8_t gateway_id); uint8_t gatewayGroupForChannel(uint8_t channel_index, uint8_t fallback = 0) const; bool setGatewayGroupForChannel(uint8_t channel_index, uint8_t gateway_group); + std::optional applicationControllerShortAddressForChannel(uint8_t channel_index, + uint8_t fallback) const; + bool setApplicationControllerShortAddressForChannel( + uint8_t channel_index, std::optional short_address); std::string deviceName() const; bool setDeviceName(std::string_view name); std::string gatewayName(uint8_t gateway_id) const; @@ -217,6 +226,7 @@ class GatewayRuntime { mutable std::map gateway_names_; mutable std::map channel_gateway_ids_; mutable std::map channel_gateway_groups_; + mutable std::map> application_controller_short_addresses_; size_t gateway_count_{0}; bool ble_enabled_{false}; bool cache_enabled_{true}; diff --git a/components/gateway_runtime/src/gateway_runtime.cpp b/components/gateway_runtime/src/gateway_runtime.cpp index 861ba39..29b8223 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -372,6 +372,31 @@ bool GatewaySettingsStore::setChannelGatewayGroup(uint8_t channel_index, nvs_commit(handle_) == ESP_OK; } +std::optional GatewaySettingsStore::getApplicationControllerShortAddress( + uint8_t channel_index, uint8_t fallback) const { + if (handle_ == 0) { + return fallback & 0x3fU; + } + + uint8_t short_address = fallback & 0x3fU; + if (nvs_get_u8(handle_, makeApplicationControllerShortAddressKey(channel_index).c_str(), + &short_address) != ESP_OK) { + return fallback & 0x3fU; + } + return short_address <= 63 ? std::optional(short_address) : std::nullopt; +} + +bool GatewaySettingsStore::setApplicationControllerShortAddress( + uint8_t channel_index, std::optional short_address) { + if (handle_ == 0 || (short_address.has_value() && short_address.value() > 63)) { + return false; + } + const uint8_t stored = short_address.value_or(0xffU); + return nvs_set_u8(handle_, makeApplicationControllerShortAddressKey(channel_index).c_str(), + stored) == ESP_OK && + nvs_commit(handle_) == ESP_OK; +} + std::optional GatewaySettingsStore::readString(std::string_view key) const { if (handle_ == 0) { return std::nullopt; @@ -418,6 +443,13 @@ std::string GatewaySettingsStore::makeChannelGatewayGroupKey(uint8_t channel_ind return std::string(key); } +std::string GatewaySettingsStore::makeApplicationControllerShortAddressKey( + uint8_t channel_index) const { + char key[24] = {0}; + std::snprintf(key, sizeof(key), "dali_cd_sa_%u", channel_index); + return std::string(key); +} + GatewayRuntime::GatewayRuntime(BootProfile profile, GatewayRuntimeConfig config, DaliDomainService* dali_domain) : profile_(profile), @@ -847,6 +879,33 @@ bool GatewayRuntime::setGatewayGroupForChannel(uint8_t channel_index, uint8_t ga return true; } +std::optional GatewayRuntime::applicationControllerShortAddressForChannel( + uint8_t channel_index, uint8_t fallback) const { + LockGuard guard(command_lock_); + const auto cached = application_controller_short_addresses_.find(channel_index); + if (cached != application_controller_short_addresses_.end()) { + return cached->second; + } + const auto short_address = + settings_.getApplicationControllerShortAddress(channel_index, fallback); + application_controller_short_addresses_[channel_index] = short_address; + return short_address; +} + +bool GatewayRuntime::setApplicationControllerShortAddressForChannel( + uint8_t channel_index, std::optional short_address) { + if (short_address.has_value() && short_address.value() > 63) { + return false; + } + if (!settings_.setApplicationControllerShortAddress(channel_index, short_address)) { + return false; + } + + LockGuard guard(command_lock_); + application_controller_short_addresses_[channel_index] = short_address; + return true; +} + std::string GatewayRuntime::deviceName() const { LockGuard guard(command_lock_); if (device_name_.has_value()) {