diff --git a/README.md b/README.md index 45787b2..66f9a2d 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ report. It returns the enabled DALI channel ids so clients do not need to probe every possible gateway id one by one. For native C++ gateways, channel number is the fixed 1-based Kconfig slot (`1` to `16`) and channel id is the persisted, Lua-compatible gateway id used by normal `0x28 0x01 ...` command frames. +Physical gateway ids are reserved to `0..127`. Raw control transports reserve +`128..254` for gateway group targets and `255` for all-gateway fan-out. Gateway opcode `0x0B` is the serial-scoped channel command. The serial is the last three bytes of the ESP base MAC. Operation `0x00` reports serial plus @@ -74,6 +76,28 @@ Operation `0x03` wraps an existing gateway command and dispatches it by fixed channel number after the serial matches, so BLE/Wi-Fi configuration and DALI send/query commands can target a channel even when its variable channel id is unknown. +Operations `0x04` and `0x05` get and set a channel gateway group id. Group `0` +means no group membership; group ids `128..254` can be used as raw-control +targets. Gateway opcode `0x6A` exposes the same group setting by physical +gateway id: operation `0x00` reads the group and operation `0x01` writes group +`0` or `128..254`. + +## Ethernet control + +The network service accepts Lua-style raw gateway frames on UDP port `2020` and, +when enabled, TCP port `2020`. It also accepts JSON control frames on the same +ports: + +- `{"type":"discover"}` returns `discoverResp` with device identity, UDP/TCP + control ports, Ethernet status/config, and channel gateway/group data. +- `{"type":"dali","data":""}` enqueues a raw gateway command. +- `{"type":"setconfig","data":{"ip":"...","mask":"...","gw":"...","dns":"..."}}` + stores and applies static Ethernet IPv4 settings. `ip` empty or `0.0.0.0` + clears static config and returns Ethernet to DHCP. + +Raw UDP, raw TCP, and UART0 session control all expand gateway target `255` to +all physical gateway ids and targets `128..254` to channels whose configured +gateway group matches the target. ## Gateway operation protocol diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index f3c0e60..c2a261c 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -13,7 +13,7 @@ menu "Gateway Channel 1" config GATEWAY_CHANNEL1_GW_ID int "Gateway id" - range 0 255 + range 0 127 default 3 help Lua-compatible gateway id for the first DALI channel. @@ -121,7 +121,7 @@ menu "Gateway Channel 2" config GATEWAY_CHANNEL2_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 2 - range 0 255 + range 0 127 default 4 help Lua-compatible gateway id for the second DALI channel. @@ -230,7 +230,7 @@ menu "Gateway Channel 3" config GATEWAY_CHANNEL3_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 3 - range 0 255 + range 0 127 default 5 choice GATEWAY_CHANNEL3_PHY @@ -319,7 +319,7 @@ menu "Gateway Channel 4" config GATEWAY_CHANNEL4_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 4 - range 0 255 + range 0 127 default 6 choice GATEWAY_CHANNEL4_PHY @@ -408,7 +408,7 @@ menu "Gateway Channel 5" config GATEWAY_CHANNEL5_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 5 - range 0 255 + range 0 127 default 7 choice GATEWAY_CHANNEL5_PHY @@ -497,7 +497,7 @@ menu "Gateway Channel 6" config GATEWAY_CHANNEL6_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 6 - range 0 255 + range 0 127 default 8 choice GATEWAY_CHANNEL6_PHY @@ -586,7 +586,7 @@ menu "Gateway Channel 7" config GATEWAY_CHANNEL7_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 7 - range 0 255 + range 0 127 default 9 choice GATEWAY_CHANNEL7_PHY @@ -675,7 +675,7 @@ menu "Gateway Channel 8" config GATEWAY_CHANNEL8_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 8 - range 0 255 + range 0 127 default 10 choice GATEWAY_CHANNEL8_PHY @@ -764,7 +764,7 @@ menu "Gateway Channel 9" config GATEWAY_CHANNEL9_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 9 - range 0 255 + range 0 127 default 11 choice GATEWAY_CHANNEL9_PHY @@ -853,7 +853,7 @@ menu "Gateway Channel 10" config GATEWAY_CHANNEL10_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 10 - range 0 255 + range 0 127 default 12 choice GATEWAY_CHANNEL10_PHY @@ -942,7 +942,7 @@ menu "Gateway Channel 11" config GATEWAY_CHANNEL11_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 11 - range 0 255 + range 0 127 default 13 choice GATEWAY_CHANNEL11_PHY @@ -1031,7 +1031,7 @@ menu "Gateway Channel 12" config GATEWAY_CHANNEL12_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 12 - range 0 255 + range 0 127 default 14 choice GATEWAY_CHANNEL12_PHY @@ -1120,7 +1120,7 @@ menu "Gateway Channel 13" config GATEWAY_CHANNEL13_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 13 - range 0 255 + range 0 127 default 15 choice GATEWAY_CHANNEL13_PHY @@ -1209,7 +1209,7 @@ menu "Gateway Channel 14" config GATEWAY_CHANNEL14_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 14 - range 0 255 + range 0 127 default 16 choice GATEWAY_CHANNEL14_PHY @@ -1298,7 +1298,7 @@ menu "Gateway Channel 15" config GATEWAY_CHANNEL15_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 15 - range 0 255 + range 0 127 default 17 choice GATEWAY_CHANNEL15_PHY @@ -1387,7 +1387,7 @@ menu "Gateway Channel 16" config GATEWAY_CHANNEL16_GW_ID int "Gateway id" depends on GATEWAY_CHANNEL_COUNT >= 16 - range 0 255 + range 0 127 default 18 choice GATEWAY_CHANNEL16_PHY @@ -2502,6 +2502,32 @@ config GATEWAY_NETWORK_UDP_PORT range 1 65535 default 2020 +config GATEWAY_NETWORK_TCP_CONTROL_ENABLED + bool "Enable TCP raw gateway control" + default y + help + Enables a TCP listener for Lua-compatible raw gateway frames and JSON + Ethernet control frames. This mirrors the UDP control path and supports + gateway group and all-gateway target expansion. + +config GATEWAY_NETWORK_TCP_CONTROL_PORT + int "TCP raw gateway control port" + depends on GATEWAY_NETWORK_TCP_CONTROL_ENABLED + range 1 65535 + default 2020 + +config GATEWAY_NETWORK_TCP_CONTROL_TASK_STACK_SIZE + int "TCP control task stack bytes" + depends on GATEWAY_NETWORK_TCP_CONTROL_ENABLED + range 2048 16384 + default 4096 + +config GATEWAY_NETWORK_TCP_CONTROL_TASK_PRIORITY + int "TCP control task priority" + depends on GATEWAY_NETWORK_TCP_CONTROL_ENABLED + range 1 10 + default 4 + config GATEWAY_STATUS_LED_GPIO int "Status LED GPIO" range -1 48 @@ -2555,4 +2581,4 @@ config GATEWAY_BUTTON_TASK_STACK_SIZE endmenu -endmenu \ No newline at end of file +endmenu diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 33228a2..66abea5 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -30,6 +30,18 @@ #define CONFIG_GATEWAY_NETWORK_UDP_PORT 2020 #endif +#ifndef CONFIG_GATEWAY_NETWORK_TCP_CONTROL_PORT +#define CONFIG_GATEWAY_NETWORK_TCP_CONTROL_PORT 2020 +#endif + +#ifndef CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_STACK_SIZE +#define CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_STACK_SIZE 4096 +#endif + +#ifndef CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_PRIORITY +#define CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_PRIORITY 4 +#endif + #ifndef CONFIG_GATEWAY_STATUS_LED_GPIO #define CONFIG_GATEWAY_STATUS_LED_GPIO -1 #endif @@ -905,6 +917,11 @@ esp_err_t BindConfiguredChannels(gateway::DaliDomainService& dali_domain, } const uint8_t gateway_id = runtime.gatewayIdForChannel(channel.channel_index, channel.gateway_id); + if (gateway_id >= 128) { + ESP_LOGE(kTag, "gateway id %u for channel %u is reserved for gateway groups", + gateway_id, channel.channel_index + 1); + return ESP_ERR_INVALID_STATE; + } if (used_gateway_ids[gateway_id]) { ESP_LOGE(kTag, "duplicate runtime gateway id configured: %u", gateway_id); return ESP_ERR_INVALID_STATE; @@ -1255,9 +1272,20 @@ extern "C" void app_main(void) { network_config.udp_enabled = true; #else network_config.udp_enabled = false; + #endif + #ifdef CONFIG_GATEWAY_NETWORK_TCP_CONTROL_ENABLED + network_config.tcp_control_enabled = true; + #else + network_config.tcp_control_enabled = false; #endif network_config.http_port = static_cast(CONFIG_GATEWAY_NETWORK_HTTP_PORT); network_config.udp_port = static_cast(CONFIG_GATEWAY_NETWORK_UDP_PORT); + network_config.tcp_control_port = + static_cast(CONFIG_GATEWAY_NETWORK_TCP_CONTROL_PORT); + network_config.tcp_control_task_stack_size = + static_cast(CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_STACK_SIZE); + network_config.tcp_control_task_priority = + static_cast(CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_PRIORITY); network_config.ethernet_spi_host = CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST; network_config.ethernet_spi_sclk_gpio = CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO; network_config.ethernet_spi_mosi_gpio = CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO; diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index df9797c..13099fd 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -858,6 +858,10 @@ CONFIG_GATEWAY_NETWORK_HTTP_ENABLED=y CONFIG_GATEWAY_NETWORK_HTTP_PORT=80 CONFIG_GATEWAY_NETWORK_UDP_ROUTER_ENABLED=y CONFIG_GATEWAY_NETWORK_UDP_PORT=2020 +CONFIG_GATEWAY_NETWORK_TCP_CONTROL_ENABLED=y +CONFIG_GATEWAY_NETWORK_TCP_CONTROL_PORT=2020 +CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_STACK_SIZE=4096 +CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_PRIORITY=4 CONFIG_GATEWAY_STATUS_LED_GPIO=-1 CONFIG_GATEWAY_BOOT_BUTTON_GPIO=0 CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW=y diff --git a/components/gateway_485_control/include/gateway_485_control.hpp b/components/gateway_485_control/include/gateway_485_control.hpp index 5f99ff3..572768c 100644 --- a/components/gateway_485_control/include/gateway_485_control.hpp +++ b/components/gateway_485_control/include/gateway_485_control.hpp @@ -10,11 +10,10 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" +#include "gateway_controller.hpp" namespace gateway { -class GatewayController; - struct Gateway485ControlBridgeConfig { bool enabled{false}; int tx_pin{-1}; @@ -69,8 +68,10 @@ class Gateway485ControlBridge { uint8_t hash, SessionStatus status, const std::vector& result); + GatewayCommandTransactionResult transactRoutedCommand(const std::vector& command); bool shouldSuppressNotification(const std::vector& frame); bool responseMatchesActiveSession(const std::vector& frame) const; + bool activeSessionTargetMatches(uint8_t response_gateway) const; GatewayController& controller_; Gateway485ControlBridgeConfig config_; diff --git a/components/gateway_485_control/src/gateway_485_control.cpp b/components/gateway_485_control/src/gateway_485_control.cpp index ab31bd0..ee7be98 100644 --- a/components/gateway_485_control/src/gateway_485_control.cpp +++ b/components/gateway_485_control/src/gateway_485_control.cpp @@ -28,6 +28,7 @@ constexpr uint8_t kDali103NoResponseOpcode = 0x64; constexpr uint8_t kRawReportLeaseOpcode = 0x66; constexpr uint8_t kGatewayOperationOpcode = 0x67; constexpr uint8_t kGatewayOperationResultOpcode = 0x68; +constexpr uint8_t kGatewayGroupOpcode = 0x6A; constexpr uint8_t kGatewayCacheOpcode = 0x39; constexpr uint8_t kBridgeTransportRequestOpcode = 0xB0; constexpr uint8_t kBridgeTransportResponseOpcode = 0xB1; @@ -215,9 +216,7 @@ bool Gateway485ControlBridge::handleSessionBytes(const uint8_t* data, size_t len active_session_->status = SessionStatus::kInProcess; } - const auto result = controller_.transactCommandFrame( - command, kSessionTransactionTimeoutMs, kSessionTransactionIdleMs, - kSessionMaxResponseBytes, true); + const auto result = transactRoutedCommand(command); { LockGuard guard(session_lock_); @@ -325,11 +324,63 @@ void Gateway485ControlBridge::writeSessionStatus(uint8_t gateway_id, } } +GatewayCommandTransactionResult Gateway485ControlBridge::transactRoutedCommand( + const std::vector& command) { + GatewayCommandTransactionResult aggregate; + if (command.size() < kCommandFrameMinLen) { + aggregate.status = GatewayCommandTransactionStatus::kInvalidFrame; + return aggregate; + } + + const auto targets = controller_.gatewayIdsForControlTarget(command[2]); + if (targets.empty() || (targets.size() == 1 && targets.front() == command[2])) { + return controller_.transactCommandFrame(command, kSessionTransactionTimeoutMs, + kSessionTransactionIdleMs, + kSessionMaxResponseBytes, true); + } + + aggregate.status = GatewayCommandTransactionStatus::kNoResponse; + for (const auto gateway_id : targets) { + std::vector routed(command.begin(), command.end() - 1); + routed[2] = gateway_id; + const auto result = controller_.transactCommandFrame( + GatewayRuntime::checksum(std::move(routed)), kSessionTransactionTimeoutMs, + kSessionTransactionIdleMs, kSessionMaxResponseBytes, true); + if (result.status == GatewayCommandTransactionStatus::kOk) { + aggregate.status = GatewayCommandTransactionStatus::kOk; + } else if (aggregate.status != GatewayCommandTransactionStatus::kOk) { + aggregate.status = result.status; + } + if (!result.frames.empty()) { + const size_t remaining = + kSessionMaxResponseBytes > aggregate.frames.size() + ? kSessionMaxResponseBytes - aggregate.frames.size() + : 0; + aggregate.frames.insert(aggregate.frames.end(), result.frames.begin(), + result.frames.begin() + + std::min(remaining, result.frames.size())); + } + } + return aggregate; +} + bool Gateway485ControlBridge::shouldSuppressNotification(const std::vector& frame) { LockGuard guard(session_lock_); return responseMatchesActiveSession(frame); } +bool Gateway485ControlBridge::activeSessionTargetMatches(uint8_t response_gateway) const { + if (!active_session_.has_value()) { + return false; + } + const auto targets = controller_.gatewayIdsForControlTarget(active_session_->gateway_id); + if (targets.empty()) { + return response_gateway == active_session_->gateway_id || + active_session_->gateway_id == 0 || response_gateway == 0xff; + } + return std::find(targets.begin(), targets.end(), response_gateway) != targets.end(); +} + bool Gateway485ControlBridge::responseMatchesActiveSession( const std::vector& frame) const { if (!active_session_.has_value() || frame.size() < 3 || frame[0] != 0x22) { @@ -338,9 +389,7 @@ bool Gateway485ControlBridge::responseMatchesActiveSession( const auto& session = *active_session_; const uint8_t response_opcode = frame[1]; const uint8_t response_gateway = frame[2]; - const bool gateway_matches = response_gateway == session.gateway_id || - session.gateway_id == 0 || - response_gateway == 0xff; + const bool gateway_matches = activeSessionTargetMatches(response_gateway); switch (session.opcode) { case 0x06: return response_opcode == 0x03 && gateway_matches; @@ -365,6 +414,8 @@ bool Gateway485ControlBridge::responseMatchesActiveSession( return (response_opcode == kGatewayOperationOpcode || response_opcode == kGatewayOperationResultOpcode) && gateway_matches; + case kGatewayGroupOpcode: + return response_opcode == kGatewayGroupOpcode && gateway_matches; case kBridgeTransportRequestOpcode: return response_opcode == kBridgeTransportResponseOpcode && gateway_matches; default: diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index fe38b6c..ead8780 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -44,6 +44,7 @@ struct GatewayControllerConfig { struct GatewayChannelSnapshot { uint8_t channel_index{0}; uint8_t gateway_id{0}; + uint8_t gateway_group{0}; std::string name; std::string phy; uint8_t scene_mask_low{0}; @@ -119,6 +120,7 @@ class GatewayController { bool ipRouterEnabled() const; bool rawReportingEnabled(uint8_t gateway_id) const; GatewayControllerSnapshot snapshot(); + std::vector gatewayIdsForControlTarget(uint8_t target) const; struct ParsedTlv { uint8_t type{0}; @@ -212,6 +214,8 @@ class GatewayController { bool hasGateway(uint8_t gateway_id) const; std::vector gatewayIds() const; std::optional gatewayIdForChannelNumber(uint8_t channel_number) const; + std::optional channelNumberForGatewayId(uint8_t gateway_id) const; + uint8_t gatewayGroupForGatewayId(uint8_t gateway_id) const; std::string gatewayName(uint8_t gateway_id) const; void refreshRuntimeGatewayNames(); void publishPayload(uint8_t gateway_id, const std::vector& payload); @@ -266,6 +270,7 @@ class GatewayController { void handleGatewayNameCommand(uint8_t gateway_id, const std::vector& command); void handleGatewaySerialCommand(uint8_t channel_number, const std::vector& command); + void handleGatewayGroupCommand(uint8_t gateway_id, const std::vector& command); void publishGatewaySerialReport(); void publishGatewaySerialResponse(uint8_t status, uint8_t op, const std::vector& data); diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index d380cf2..7e73c2c 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -35,10 +35,21 @@ constexpr uint8_t kGatewaySerialOpReport = 0x00; constexpr uint8_t kGatewaySerialOpGetChannelId = 0x01; constexpr uint8_t kGatewaySerialOpSetChannelId = 0x02; constexpr uint8_t kGatewaySerialOpDispatchByChannelNumber = 0x03; +constexpr uint8_t kGatewaySerialOpGetGatewayGroup = 0x04; +constexpr uint8_t kGatewaySerialOpSetGatewayGroup = 0x05; constexpr uint8_t kGatewaySerialStatusOk = 0x00; constexpr uint8_t kGatewaySerialStatusInvalidArgument = 0x02; constexpr uint8_t kGatewaySerialStatusDuplicateChannelId = 0x03; constexpr uint8_t kGatewaySerialStatusStorageError = 0x04; +constexpr uint8_t kGatewayGroupOpcode = 0x6A; +constexpr uint8_t kGatewayGroupOpGet = 0x00; +constexpr uint8_t kGatewayGroupOpSet = 0x01; +constexpr uint8_t kGatewayGroupStatusOk = 0x00; +constexpr uint8_t kGatewayGroupStatusInvalidArgument = 0x02; +constexpr uint8_t kGatewayGroupStatusStorageError = 0x04; +constexpr uint8_t kGatewayGroupMin = 128; +constexpr uint8_t kGatewayGroupMax = 254; +constexpr uint8_t kGatewayTargetAll = 255; constexpr uint8_t kDali103SendOpcode = 0x60; constexpr uint8_t kDali103SendTwiceOpcode = 0x61; constexpr uint8_t kDali103QueryOpcode = 0x62; @@ -758,6 +769,7 @@ GatewayControllerSnapshot GatewayController::snapshot() { GatewayChannelSnapshot channel_snapshot; channel_snapshot.channel_index = channel.channel_index; channel_snapshot.gateway_id = channel.gateway_id; + channel_snapshot.gateway_group = runtime_.gatewayGroupForChannel(channel.channel_index); channel_snapshot.name = channel.name; channel_snapshot.phy = PhyKindToString(channel.phy_kind); channel_snapshot.scene_mask_low = scene_low; @@ -785,6 +797,33 @@ GatewayControllerSnapshot GatewayController::snapshot() { return out; } +std::vector GatewayController::gatewayIdsForControlTarget(uint8_t target) const { + const auto channels = dali_domain_.channelInfo(); + std::vector ids; + ids.reserve(channels.size()); + if (target == kGatewayTargetAll) { + for (const auto& channel : channels) { + ids.push_back(channel.gateway_id); + } + return ids; + } + if (target >= kGatewayGroupMin && target <= kGatewayGroupMax) { + for (const auto& channel : channels) { + if (runtime_.gatewayGroupForChannel(channel.channel_index) == target) { + ids.push_back(channel.gateway_id); + } + } + return ids; + } + if (target < kGatewayGroupMin && + std::any_of(channels.begin(), channels.end(), [target](const auto& channel) { + return channel.gateway_id == target; + })) { + ids.push_back(target); + } + return ids; +} + void GatewayController::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } @@ -1167,6 +1206,7 @@ void GatewayController::dispatchCommand(const std::vector& command) { } if (!chip_level_command && operationActive(gateway_id) && opcode != kRawReportLeaseOpcode && opcode != kGatewayOperationOpcode && + opcode != kGatewayGroupOpcode && !(opcode == 0x30 && (addr == 0 || addr == 3))) { ESP_LOGW(kTag, "gateway=%u busy with operation, opcode=0x%02x rejected", gateway_id, opcode); @@ -1395,6 +1435,9 @@ void GatewayController::dispatchCommand(const std::vector& command) { case kGatewayOperationOpcode: handleGatewayOperationCommand(gateway_id, command); break; + case kGatewayGroupOpcode: + handleGatewayGroupCommand(gateway_id, command); + break; case kGatewayCacheOpcode: handleGatewayCacheCommand(gateway_id, command); break; @@ -1447,6 +1490,30 @@ std::optional GatewayController::gatewayIdForChannelNumber( return it->gateway_id; } +std::optional GatewayController::channelNumberForGatewayId(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 std::nullopt; + } + return static_cast(it->channel_index + 1); +} + +uint8_t GatewayController::gatewayGroupForGatewayId(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 0; + } + return runtime_.gatewayGroupForChannel(it->channel_index); +} + 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) { @@ -2292,6 +2359,11 @@ void GatewayController::handleGatewaySerialCommand(uint8_t channel_number, return; } const uint8_t new_gateway_id = command[8]; + if (new_gateway_id >= kGatewayGroupMin) { + publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, + {channel_number, new_gateway_id}); + return; + } const uint8_t channel_index = static_cast(channel_number - 1); const uint8_t old_gateway_id = gateway_id.value(); if (!dali_domain_.updateChannelGatewayId(channel_index, new_gateway_id)) { @@ -2313,6 +2385,33 @@ void GatewayController::handleGatewaySerialCommand(uint8_t channel_number, {channel_number, new_gateway_id}); return; } + case kGatewaySerialOpGetGatewayGroup: + publishGatewaySerialResponse(kGatewaySerialStatusOk, op, + {channel_number, runtime_.gatewayGroupForChannel( + static_cast(channel_number - 1))}); + return; + case kGatewaySerialOpSetGatewayGroup: { + if (command.size() < 10) { + publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, {channel_number}); + return; + } + const uint8_t gateway_group = command[8]; + if (gateway_group != 0 && (gateway_group < kGatewayGroupMin || + gateway_group > kGatewayGroupMax)) { + publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, + {channel_number, gateway_group}); + return; + } + const uint8_t channel_index = static_cast(channel_number - 1); + if (!runtime_.setGatewayGroupForChannel(channel_index, gateway_group)) { + publishGatewaySerialResponse(kGatewaySerialStatusStorageError, op, + {channel_number, gateway_group}); + return; + } + publishGatewaySerialResponse(kGatewaySerialStatusOk, op, + {channel_number, gateway_group}); + return; + } case kGatewaySerialOpDispatchByChannelNumber: { if (command.size() < 12) { publishGatewaySerialResponse(kGatewaySerialStatusInvalidArgument, op, {channel_number}); @@ -2335,6 +2434,45 @@ void GatewayController::handleGatewaySerialCommand(uint8_t channel_number, } } +void GatewayController::handleGatewayGroupCommand(uint8_t gateway_id, + const std::vector& command) { + const uint8_t op = command.size() > 4 ? command[4] : kGatewayGroupOpGet; + const auto channel_number = channelNumberForGatewayId(gateway_id); + if (!channel_number.has_value()) { + publishPayload(gateway_id, {kGatewayGroupOpcode, gateway_id, + kGatewayGroupStatusInvalidArgument, op, 0x00}); + return; + } + const uint8_t channel_index = static_cast(channel_number.value() - 1); + + if (op == kGatewayGroupOpGet) { + publishPayload(gateway_id, {kGatewayGroupOpcode, gateway_id, kGatewayGroupStatusOk, op, + runtime_.gatewayGroupForChannel(channel_index)}); + return; + } + + if (op != kGatewayGroupOpSet || command.size() < 7) { + publishPayload(gateway_id, {kGatewayGroupOpcode, gateway_id, + kGatewayGroupStatusInvalidArgument, op, 0x00}); + return; + } + + const uint8_t gateway_group = command[5]; + if (gateway_group != 0 && + (gateway_group < kGatewayGroupMin || gateway_group > kGatewayGroupMax)) { + publishPayload(gateway_id, {kGatewayGroupOpcode, gateway_id, + kGatewayGroupStatusInvalidArgument, op, gateway_group}); + return; + } + if (!runtime_.setGatewayGroupForChannel(channel_index, gateway_group)) { + publishPayload(gateway_id, {kGatewayGroupOpcode, gateway_id, + kGatewayGroupStatusStorageError, op, gateway_group}); + return; + } + publishPayload(gateway_id, + {kGatewayGroupOpcode, gateway_id, kGatewayGroupStatusOk, op, gateway_group}); +} + void GatewayController::handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op) { std::string value; if (op == 0x00) { diff --git a/components/gateway_network/include/gateway_network.hpp b/components/gateway_network/include/gateway_network.hpp index 5755d26..9fe06c4 100644 --- a/components/gateway_network/include/gateway_network.hpp +++ b/components/gateway_network/include/gateway_network.hpp @@ -6,6 +6,7 @@ #include #include +#include "cJSON.h" #include "esp_err.h" #include "esp_event.h" #include "esp_eth.h" @@ -39,8 +40,10 @@ struct GatewayNetworkServiceConfig { uint8_t smartconfig_timeout_sec{60}; bool http_enabled{true}; bool udp_enabled{true}; + bool tcp_control_enabled{true}; uint16_t http_port{80}; uint16_t udp_port{2020}; + uint16_t tcp_control_port{2020}; int ethernet_spi_host{1}; int ethernet_spi_sclk_gpio{14}; int ethernet_spi_mosi_gpio{13}; @@ -63,6 +66,8 @@ struct GatewayNetworkServiceConfig { UBaseType_t boot_button_task_priority{2}; uint32_t udp_task_stack_size{4096}; UBaseType_t udp_task_priority{4}; + uint32_t tcp_control_task_stack_size{4096}; + UBaseType_t tcp_control_task_priority{4}; }; class GatewayNetworkService { @@ -76,6 +81,7 @@ class GatewayNetworkService { private: static void UdpTaskEntry(void* arg); + static void TcpControlTaskEntry(void* arg); static void BootButtonTaskEntry(void* arg); static esp_err_t HandleInfoGet(httpd_req_t* req); static esp_err_t HandleCommandGet(httpd_req_t* req); @@ -107,10 +113,18 @@ class GatewayNetworkService { esp_err_t configureStatusLed(); esp_err_t startHttpServer(); esp_err_t startUdpTask(); + esp_err_t startTcpControlTask(); esp_err_t configureBootButton(); esp_err_t startBootButtonTask(); void udpTaskLoop(); + void tcpControlTaskLoop(); void bootButtonTaskLoop(); + 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); + std::string discoveryResponseJson() const; + std::string setEthernetConfigFromJson(cJSON* data); + esp_err_t applyEthernetConfig(); void handleGatewayNotification(const std::vector& frame); void handleWifiControl(uint8_t mode); void handleEthernetEvent(esp_event_base_t event_base, int32_t event_id, void* event_data); @@ -151,11 +165,15 @@ class GatewayNetworkService { std::array espnow_peer_{}; TaskHandle_t boot_button_task_handle_{nullptr}; TaskHandle_t udp_task_handle_{nullptr}; + TaskHandle_t tcp_control_task_handle_{nullptr}; int udp_socket_{-1}; + int tcp_control_socket_{-1}; + int tcp_control_client_socket_{-1}; SemaphoreHandle_t udp_lock_{nullptr}; + SemaphoreHandle_t tcp_control_lock_{nullptr}; bool has_udp_remote_{false}; sockaddr_storage udp_remote_addr_{}; socklen_t udp_remote_addr_len_{0}; }; -} // namespace gateway \ No newline at end of file +} // namespace gateway diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp index bc8e1a5..f2ae4dc 100644 --- a/components/gateway_network/src/gateway_network.cpp +++ b/components/gateway_network/src/gateway_network.cpp @@ -21,6 +21,7 @@ #include "lwip/inet.h" #include +#include #include #include #include @@ -35,6 +36,7 @@ namespace { constexpr const char* kTag = "gateway_network"; constexpr const char* kSetupApSsid = "LAMMIN_Gateway"; constexpr size_t kUdpBufferSize = 256; +constexpr size_t kTcpControlBufferSize = 512; constexpr uint8_t kEspNowBroadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; GatewayNetworkService* s_espnow_service = nullptr; @@ -146,6 +148,17 @@ std::string BytesToHex(const std::vector& bytes) { return out; } +bool IsLikelyJson(const uint8_t* data, size_t len) { + if (data == nullptr || len == 0) { + return false; + } + size_t index = 0; + while (index < len && std::isspace(static_cast(data[index]))) { + ++index; + } + return index < len && data[index] == '{'; +} + std::string PrintJson(cJSON* node) { if (node == nullptr) { return {}; @@ -160,6 +173,32 @@ std::string PrintJson(cJSON* node) { return out; } +std::string JsonResponse(std::string_view type, std::string_view status) { + cJSON* root = cJSON_CreateObject(); + if (root == nullptr) { + return {}; + } + cJSON_AddStringToObject(root, "type", std::string(type).c_str()); + cJSON_AddStringToObject(root, "status", std::string(status).c_str()); + cJSON_AddStringToObject(root, "data", std::string(status).c_str()); + const std::string rendered = PrintJson(root); + cJSON_Delete(root); + return rendered; +} + +bool ParseIpv4(std::string_view value, esp_ip4_addr_t* out) { + if (out == nullptr || value.empty()) { + return false; + } + const std::string text(value); + const uint32_t address = esp_ip4addr_aton(text.c_str()); + if (address == 0 && text != "0.0.0.0") { + return false; + } + out->addr = address; + return true; +} + esp_err_t ReadRequestBody(httpd_req_t* req, std::string& body) { body.clear(); if (req == nullptr) { @@ -260,7 +299,8 @@ GatewayNetworkService::GatewayNetworkService(GatewayController& controller, GatewayNetworkServiceConfig config, GatewayBridgeService* bridge_service) : controller_(controller), runtime_(runtime), dali_domain_(dali_domain), config_(config), - bridge_service_(bridge_service), udp_lock_(xSemaphoreCreateMutex()) {} + bridge_service_(bridge_service), udp_lock_(xSemaphoreCreateMutex()), + tcp_control_lock_(xSemaphoreCreateMutex()) {} esp_err_t GatewayNetworkService::start() { if (started_) { @@ -331,15 +371,22 @@ esp_err_t GatewayNetworkService::start() { } } + if (config_.tcp_control_enabled) { + err = startTcpControlTask(); + if (err != ESP_OK) { + return err; + } + } + err = startBootButtonTask(); if (err != ESP_OK) { return err; } started_ = true; - ESP_LOGI(kTag, "network service started eth=%d wifi=%d http=%d udp=%d", + ESP_LOGI(kTag, "network service started eth=%d wifi=%d http=%d udp=%d tcp=%d", config_.ethernet_enabled, config_.wifi_enabled, config_.http_enabled, - config_.udp_enabled); + config_.udp_enabled, config_.tcp_control_enabled); return ESP_OK; } @@ -489,6 +536,13 @@ esp_err_t GatewayNetworkService::startEthernet() { return err; } + err = applyEthernetConfig(); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to apply Ethernet config: %s", esp_err_to_name(err)); + stopEthernet(); + return err; + } + err = esp_eth_start(eth_handle_); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to start Ethernet: %s", esp_err_to_name(err)); @@ -999,6 +1053,24 @@ esp_err_t GatewayNetworkService::startUdpTask() { return ESP_OK; } +esp_err_t GatewayNetworkService::startTcpControlTask() { + if (tcp_control_task_handle_ != nullptr) { + return ESP_OK; + } + + const BaseType_t created = + xTaskCreate(&GatewayNetworkService::TcpControlTaskEntry, "gateway_tcp_ctrl", + config_.tcp_control_task_stack_size, this, + config_.tcp_control_task_priority, &tcp_control_task_handle_); + if (created != pdPASS) { + tcp_control_task_handle_ = nullptr; + ESP_LOGE(kTag, "failed to create TCP control task"); + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + esp_err_t GatewayNetworkService::startBootButtonTask() { if ((config_.boot_button_gpio < 0 && config_.setup_ap_button_gpio < 0) || boot_button_task_handle_ != nullptr) { @@ -1021,6 +1093,10 @@ void GatewayNetworkService::UdpTaskEntry(void* arg) { static_cast(arg)->udpTaskLoop(); } +void GatewayNetworkService::TcpControlTaskEntry(void* arg) { + static_cast(arg)->tcpControlTaskLoop(); +} + void GatewayNetworkService::BootButtonTaskEntry(void* arg) { static_cast(arg)->bootButtonTaskLoop(); } @@ -1359,7 +1435,84 @@ void GatewayNetworkService::udpTaskLoop() { has_udp_remote_ = true; } - controller_.enqueueCommandFrame(std::vector(buffer, buffer + read_len)); + const auto response = + handleJsonControlFrame(buffer, static_cast(read_len)); + if (response.has_value()) { + sendto(udp_socket_, response->data(), response->size(), 0, + reinterpret_cast(&remote_addr), remote_addr_len); + continue; + } + + handleNetworkControlBytes(buffer, static_cast(read_len)); + } +} + +void GatewayNetworkService::tcpControlTaskLoop() { + tcp_control_socket_ = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); + if (tcp_control_socket_ < 0) { + ESP_LOGE(kTag, "failed to create TCP control socket"); + tcp_control_task_handle_ = nullptr; + vTaskDelete(nullptr); + return; + } + + int reuse = 1; + setsockopt(tcp_control_socket_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + + sockaddr_in local_addr = {}; + local_addr.sin_family = AF_INET; + local_addr.sin_port = htons(config_.tcp_control_port); + local_addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(tcp_control_socket_, reinterpret_cast(&local_addr), + sizeof(local_addr)) != 0 || + listen(tcp_control_socket_, 2) != 0) { + ESP_LOGE(kTag, "failed to listen on TCP control port %u", config_.tcp_control_port); + close(tcp_control_socket_); + tcp_control_socket_ = -1; + tcp_control_task_handle_ = nullptr; + vTaskDelete(nullptr); + return; + } + + ESP_LOGI(kTag, "TCP control listening on port %u", config_.tcp_control_port); + while (true) { + sockaddr_storage remote_addr = {}; + socklen_t remote_addr_len = sizeof(remote_addr); + const int client = + accept(tcp_control_socket_, reinterpret_cast(&remote_addr), &remote_addr_len); + if (client < 0) { + continue; + } + { + LockGuard guard(tcp_control_lock_); + if (tcp_control_client_socket_ >= 0) { + close(tcp_control_client_socket_); + } + tcp_control_client_socket_ = client; + } + + uint8_t buffer[kTcpControlBufferSize] = {0}; + while (true) { + const int read_len = recv(client, buffer, sizeof(buffer), 0); + if (read_len <= 0) { + break; + } + const auto response = + handleJsonControlFrame(buffer, static_cast(read_len)); + if (response.has_value()) { + send(client, response->data(), response->size(), 0); + continue; + } + handleNetworkControlBytes(buffer, static_cast(read_len)); + } + + { + LockGuard guard(tcp_control_lock_); + if (tcp_control_client_socket_ == client) { + tcp_control_client_socket_ = -1; + } + } + close(client); } } @@ -1454,23 +1607,110 @@ void GatewayNetworkService::bootButtonTaskLoop() { } void GatewayNetworkService::handleGatewayNotification(const std::vector& frame) { - if (!config_.udp_enabled || udp_socket_ < 0 || frame.empty()) { + if (frame.empty()) { return; } - sockaddr_storage remote_addr = {}; - socklen_t remote_addr_len = 0; - { - LockGuard guard(udp_lock_); - if (!has_udp_remote_) { - return; + if (config_.udp_enabled && udp_socket_ >= 0) { + sockaddr_storage remote_addr = {}; + socklen_t remote_addr_len = 0; + { + LockGuard guard(udp_lock_); + if (has_udp_remote_) { + remote_addr = udp_remote_addr_; + remote_addr_len = udp_remote_addr_len_; + } + } + if (remote_addr_len > 0) { + sendto(udp_socket_, frame.data(), frame.size(), 0, + reinterpret_cast(&remote_addr), remote_addr_len); } - remote_addr = udp_remote_addr_; - remote_addr_len = udp_remote_addr_len_; } - sendto(udp_socket_, frame.data(), frame.size(), 0, - reinterpret_cast(&remote_addr), remote_addr_len); + if (config_.tcp_control_enabled) { + int client = -1; + { + LockGuard guard(tcp_control_lock_); + client = tcp_control_client_socket_; + } + if (client >= 0) { + send(client, frame.data(), frame.size(), 0); + } + } +} + +void GatewayNetworkService::handleNetworkControlBytes(const uint8_t* data, size_t len) { + if (data == nullptr || len == 0) { + return; + } + enqueueControlFrameForTargets(std::vector(data, data + len)); +} + +bool GatewayNetworkService::enqueueControlFrameForTargets(const std::vector& frame) { + if (!GatewayRuntime::isGatewayCommandFrame(frame) || frame.size() < 7) { + return controller_.enqueueCommandFrame(frame); + } + const uint8_t target = frame[2]; + const auto gateways = controller_.gatewayIdsForControlTarget(target); + if (gateways.empty() || (gateways.size() == 1 && gateways.front() == target)) { + return controller_.enqueueCommandFrame(frame); + } + + bool any_enqueued = false; + for (const auto gateway_id : gateways) { + std::vector routed(frame.begin(), frame.end() - 1); + routed[2] = gateway_id; + any_enqueued = controller_.enqueueCommandFrame(GatewayRuntime::checksum(std::move(routed))) || + any_enqueued; + } + return any_enqueued; +} + +std::optional GatewayNetworkService::handleJsonControlFrame(const uint8_t* data, + size_t len) { + if (!IsLikelyJson(data, len)) { + return std::nullopt; + } + + cJSON* root = cJSON_ParseWithLength(reinterpret_cast(data), len); + if (root == nullptr) { + return JsonResponse("error", "json"); + } + const char* type = JsonString(root, "type"); + if (type == nullptr) { + cJSON_Delete(root); + return JsonResponse("error", "missing_type"); + } + + if (std::strcmp(type, "discover") == 0 || std::strcmp(type, "info") == 0) { + const std::string response = discoveryResponseJson(); + cJSON_Delete(root); + return response; + } + + if (std::strcmp(type, "dali") == 0) { + cJSON* payload = cJSON_GetObjectItemCaseSensitive(root, "data"); + std::vector frame; + if (cJSON_IsString(payload) && payload->valuestring != nullptr) { + frame = BytesFromJsonString(payload->valuestring); + } + cJSON_Delete(root); + if (frame.empty()) { + return JsonResponse("daliResp", "invalid"); + } + const bool accepted = enqueueControlFrameForTargets(frame); + return JsonResponse("daliResp", accepted ? "ok" : "rejected"); + } + + if (std::strcmp(type, "setconfig") == 0) { + cJSON* payload = cJSON_GetObjectItemCaseSensitive(root, "data"); + const std::string response = setEthernetConfigFromJson(payload); + cJSON_Delete(root); + return response; + } + + cJSON_Delete(root); + return JsonResponse("error", "unsupported"); } void GatewayNetworkService::handleWifiControl(uint8_t mode) { @@ -1550,6 +1790,12 @@ std::string GatewayNetworkService::deviceInfoJson() const { if (eth != nullptr) { cJSON_AddStringToObject(eth, "mac", info.eth->mac.c_str()); cJSON_AddStringToObject(eth, "IP", info.eth->ip.c_str()); + if (const auto eth_config = runtime_.ethernetConfig()) { + cJSON_AddStringToObject(eth, "configIp", eth_config->ip.c_str()); + cJSON_AddStringToObject(eth, "configMask", eth_config->mask.c_str()); + cJSON_AddStringToObject(eth, "configGw", eth_config->gateway.c_str()); + cJSON_AddStringToObject(eth, "configDns", eth_config->dns.c_str()); + } cJSON_AddItemToObject(root, "ethInfo", eth); } } @@ -1571,6 +1817,139 @@ std::string GatewayNetworkService::deviceInfoDoubleEncodedJson() const { return rendered; } +std::string GatewayNetworkService::discoveryResponseJson() const { + cJSON* root = cJSON_CreateObject(); + if (root == nullptr) { + return {}; + } + cJSON_AddStringToObject(root, "type", "discoverResp"); + cJSON_AddStringToObject(root, "status", "ok"); + cJSON_AddNumberToObject(root, "udpPort", config_.udp_port); + cJSON_AddNumberToObject(root, "tcpPort", config_.tcp_control_port); + cJSON_AddBoolToObject(root, "udpControl", config_.udp_enabled); + cJSON_AddBoolToObject(root, "tcpControl", config_.tcp_control_enabled); + + const auto info = runtime_.deviceInfo(); + cJSON_AddStringToObject(root, "serialId", info.serial_id.c_str()); + cJSON_AddStringToObject(root, "project", info.project.c_str()); + cJSON_AddStringToObject(root, "version", info.version.c_str()); + cJSON_AddStringToObject(root, "deviceName", runtime_.deviceName().c_str()); + if (info.eth.has_value()) { + cJSON_AddStringToObject(root, "ethMac", info.eth->mac.c_str()); + cJSON_AddStringToObject(root, "ethIp", info.eth->ip.c_str()); + } + if (const auto eth_config = runtime_.ethernetConfig()) { + cJSON* config = cJSON_CreateObject(); + if (config != nullptr) { + cJSON_AddStringToObject(config, "ip", eth_config->ip.c_str()); + cJSON_AddStringToObject(config, "mask", eth_config->mask.c_str()); + cJSON_AddStringToObject(config, "gw", eth_config->gateway.c_str()); + cJSON_AddStringToObject(config, "dns", eth_config->dns.c_str()); + cJSON_AddItemToObject(root, "ethConfig", config); + } + } + + cJSON* channels = cJSON_CreateArray(); + if (channels != nullptr) { + for (const auto& channel : controller_.snapshot().channels) { + cJSON* item = cJSON_CreateObject(); + if (item == nullptr) { + continue; + } + cJSON_AddNumberToObject(item, "channel", channel.channel_index + 1); + cJSON_AddNumberToObject(item, "gw", channel.gateway_id); + cJSON_AddNumberToObject(item, "gatewayGroup", channel.gateway_group); + cJSON_AddStringToObject(item, "name", channel.name.c_str()); + cJSON_AddStringToObject(item, "phy", channel.phy.c_str()); + cJSON_AddItemToArray(channels, item); + } + cJSON_AddItemToObject(root, "channels", channels); + } + + const std::string rendered = PrintJson(root); + cJSON_Delete(root); + return rendered; +} + +std::string GatewayNetworkService::setEthernetConfigFromJson(cJSON* data) { + if (!cJSON_IsObject(data)) { + return JsonResponse("setconfigResp", "invalid"); + } + + EthernetConfig config; + if (const char* value = JsonString(data, "ip")) { + config.ip = value; + } + if (const char* value = JsonString(data, "mask")) { + config.mask = value; + } + if (const char* value = JsonString(data, "gw")) { + config.gateway = value; + } + if (const char* value = JsonString(data, "gateway")) { + config.gateway = value; + } + if (const char* value = JsonString(data, "dns")) { + config.dns = value; + } + + const bool dhcp = config.ip.empty() || config.ip == "0.0.0.0"; + if (!dhcp) { + esp_ip4_addr_t parsed = {}; + if (!ParseIpv4(config.ip, &parsed) || !ParseIpv4(config.mask, &parsed) || + !ParseIpv4(config.gateway, &parsed) || + (!config.dns.empty() && !ParseIpv4(config.dns, &parsed))) { + return JsonResponse("setconfigResp", "invalid"); + } + } + + const bool stored = dhcp ? runtime_.clearEthernetConfig() : runtime_.setEthernetConfig(config); + if (!stored) { + return JsonResponse("setconfigResp", "storage_error"); + } + if (applyEthernetConfig() != ESP_OK) { + return JsonResponse("setconfigResp", "apply_error"); + } + return JsonResponse("setconfigResp", "ok"); +} + +esp_err_t GatewayNetworkService::applyEthernetConfig() { + if (eth_netif_ == nullptr) { + return ESP_OK; + } + + const auto config = runtime_.ethernetConfig(); + if (!config.has_value() || config->ip.empty() || config->ip == "0.0.0.0") { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcpc_start(eth_netif_)); + return ESP_OK; + } + + esp_netif_ip_info_t ip_info = {}; + if (!ParseIpv4(config->ip, &ip_info.ip) || !ParseIpv4(config->mask, &ip_info.netmask) || + !ParseIpv4(config->gateway, &ip_info.gw)) { + return ESP_ERR_INVALID_ARG; + } + + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcpc_stop(eth_netif_)); + esp_err_t err = esp_netif_set_ip_info(eth_netif_, &ip_info); + if (err != ESP_OK) { + return err; + } + if (!config->dns.empty()) { + esp_netif_dns_info_t dns = {}; + if (!ParseIpv4(config->dns, &dns.ip.u_addr.ip4)) { + return ESP_ERR_INVALID_ARG; + } + dns.ip.type = ESP_IPADDR_TYPE_V4; + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_set_dns_info(eth_netif_, + ESP_NETIF_DNS_MAIN, &dns)); + } + ESP_LOGI(kTag, "Ethernet static config applied ip=%s mask=%s gw=%s dns=%s", + config->ip.c_str(), config->mask.c_str(), config->gateway.c_str(), + config->dns.c_str()); + return ESP_OK; +} + std::string GatewayNetworkService::gatewaySnapshotJson() { const auto snapshot = controller_.snapshot(); cJSON* root = cJSON_CreateObject(); @@ -1601,6 +1980,7 @@ std::string GatewayNetworkService::gatewaySnapshotJson() { } cJSON_AddNumberToObject(item, "channel", channel.channel_index + 1); cJSON_AddNumberToObject(item, "gw", channel.gateway_id); + cJSON_AddNumberToObject(item, "gatewayGroup", channel.gateway_group); cJSON_AddStringToObject(item, "name", channel.name.c_str()); cJSON_AddStringToObject(item, "phy", channel.phy.c_str()); cJSON_AddNumberToObject(item, "sceneMaskLow", channel.scene_mask_low); diff --git a/components/gateway_runtime/include/gateway_runtime.hpp b/components/gateway_runtime/include/gateway_runtime.hpp index 1b5a225..770d2bd 100644 --- a/components/gateway_runtime/include/gateway_runtime.hpp +++ b/components/gateway_runtime/include/gateway_runtime.hpp @@ -35,6 +35,13 @@ struct EthernetInfo { std::string ip; }; +struct EthernetConfig { + std::string ip; + std::string mask; + std::string gateway; + std::string dns; +}; + struct GatewayRuntimeConfig { std::string_view project_name; std::string_view version; @@ -73,6 +80,9 @@ class GatewaySettingsStore { std::optional getWifiPassword() const; bool setWifiCredentials(std::string_view ssid, std::string_view password); bool clearWifiCredentials(); + std::optional getEthernetConfig() const; + bool setEthernetConfig(const EthernetConfig& config); + bool clearEthernetConfig(); std::string getDeviceName(std::string_view fallback) const; bool setDeviceName(std::string_view name); @@ -80,12 +90,15 @@ class GatewaySettingsStore { bool setGatewayName(uint8_t gateway_id, std::string_view name); uint8_t getChannelGatewayId(uint8_t channel_index, uint8_t fallback) const; 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); private: std::optional readString(std::string_view key) const; bool writeString(std::string_view key, std::string_view value); std::string makeGatewayNameKey(uint8_t gateway_id) const; std::string makeChannelGatewayIdKey(uint8_t channel_index) const; + std::string makeChannelGatewayGroupKey(uint8_t channel_index) const; mutable nvs_handle_t handle_{0}; }; @@ -133,6 +146,9 @@ class GatewayRuntime { void setEthernetInfo(EthernetInfo info); void clearEthernetInfo(); void clearEthernetIp(); + std::optional ethernetConfig() const; + bool setEthernetConfig(const EthernetConfig& config); + bool clearEthernetConfig(); void setCommandAddressResolver(std::function resolver); GatewayDeviceInfo deviceInfo() const; @@ -142,6 +158,8 @@ class GatewayRuntime { bool setCacheEnabled(bool enabled); uint8_t gatewayIdForChannel(uint8_t channel_index, uint8_t fallback) const; 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::string deviceName() const; bool setDeviceName(std::string_view name); std::string gatewayName(uint8_t gateway_id) const; @@ -175,6 +193,7 @@ class GatewayRuntime { mutable std::optional device_name_; mutable std::map gateway_names_; mutable std::map channel_gateway_ids_; + mutable std::map channel_gateway_groups_; 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 494da48..e3fc1db 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -22,6 +22,10 @@ constexpr const char* kCacheEnabledKey = "cache_enabled"; constexpr const char* kDeviceNameKey = "device_name"; constexpr const char* kWifiSsidKey = "wifi_ssid"; constexpr const char* kWifiPasswordKey = "wifi_passwd"; +constexpr const char* kEthIpKey = "eth_ip"; +constexpr const char* kEthMaskKey = "eth_mask"; +constexpr const char* kEthGatewayKey = "eth_gw"; +constexpr const char* kEthDnsKey = "eth_dns"; constexpr size_t kMaxGatewayNameBytes = 32; constexpr uint8_t kCommandFramePrefix0 = 0x28; constexpr uint8_t kCommandFramePrefix1 = 0x01; @@ -175,6 +179,54 @@ bool GatewaySettingsStore::clearWifiCredentials() { return ssid_err == ESP_OK && password_err == ESP_OK && nvs_commit(handle_) == ESP_OK; } +std::optional GatewaySettingsStore::getEthernetConfig() const { + EthernetConfig config; + if (const auto value = readString(kEthIpKey)) { + config.ip = *value; + } + if (const auto value = readString(kEthMaskKey)) { + config.mask = *value; + } + if (const auto value = readString(kEthGatewayKey)) { + config.gateway = *value; + } + if (const auto value = readString(kEthDnsKey)) { + config.dns = *value; + } + if (config.ip.empty() && config.mask.empty() && config.gateway.empty() && + config.dns.empty()) { + return std::nullopt; + } + return config; +} + +bool GatewaySettingsStore::setEthernetConfig(const EthernetConfig& config) { + if (handle_ == 0) { + return false; + } + + const esp_err_t ip_err = nvs_set_str(handle_, kEthIpKey, config.ip.c_str()); + const esp_err_t mask_err = nvs_set_str(handle_, kEthMaskKey, config.mask.c_str()); + const esp_err_t gw_err = nvs_set_str(handle_, kEthGatewayKey, config.gateway.c_str()); + const esp_err_t dns_err = nvs_set_str(handle_, kEthDnsKey, config.dns.c_str()); + return ip_err == ESP_OK && mask_err == ESP_OK && gw_err == ESP_OK && dns_err == ESP_OK && + nvs_commit(handle_) == ESP_OK; +} + +bool GatewaySettingsStore::clearEthernetConfig() { + if (handle_ == 0) { + return false; + } + + auto erase = [this](const char* key) { + const esp_err_t err = nvs_erase_key(handle_, key); + return err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND; + }; + const bool ok = erase(kEthIpKey) && erase(kEthMaskKey) && erase(kEthGatewayKey) && + erase(kEthDnsKey); + return ok && nvs_commit(handle_) == ESP_OK; +} + std::string GatewaySettingsStore::getDeviceName(std::string_view fallback) const { const auto value = readString(kDeviceNameKey); if (!value.has_value() || value->empty()) { @@ -228,6 +280,31 @@ bool GatewaySettingsStore::setChannelGatewayId(uint8_t channel_index, nvs_commit(handle_) == ESP_OK; } +uint8_t GatewaySettingsStore::getChannelGatewayGroup(uint8_t channel_index, + uint8_t fallback) const { + if (handle_ == 0) { + return fallback; + } + + uint8_t gateway_group = fallback; + if (nvs_get_u8(handle_, makeChannelGatewayGroupKey(channel_index).c_str(), + &gateway_group) != ESP_OK) { + return fallback; + } + return gateway_group; +} + +bool GatewaySettingsStore::setChannelGatewayGroup(uint8_t channel_index, + uint8_t gateway_group) { + if (handle_ == 0) { + return false; + } + + return nvs_set_u8(handle_, makeChannelGatewayGroupKey(channel_index).c_str(), + gateway_group) == ESP_OK && + nvs_commit(handle_) == ESP_OK; +} + std::optional GatewaySettingsStore::readString(std::string_view key) const { if (handle_ == 0) { return std::nullopt; @@ -268,6 +345,12 @@ std::string GatewaySettingsStore::makeChannelGatewayIdKey(uint8_t channel_index) return std::string(key); } +std::string GatewaySettingsStore::makeChannelGatewayGroupKey(uint8_t channel_index) const { + char key[24] = {0}; + std::snprintf(key, sizeof(key), "dali_ch_grp_%u", channel_index); + return std::string(key); +} + GatewayRuntime::GatewayRuntime(BootProfile profile, GatewayRuntimeConfig config, DaliDomainService* dali_domain) : profile_(profile), @@ -371,6 +454,7 @@ GatewayRuntime::CommandPriority GatewayRuntime::classifyCommandPriority( opcode == 0x08 || opcode == 0x10 || opcode == 0x11 || opcode == 0x12 || opcode == 0x13 || opcode == 0x0B || opcode == 0x17 || opcode == 0x18 || opcode == 0x37 || opcode == 0x38 || opcode == 0x60 || opcode == 0x61 || opcode == 0x62 || opcode == 0x66 || opcode == 0x67 || + opcode == 0x6A || (opcode == 0x30 && addr == 0)) { return CommandPriority::kControl; } @@ -514,6 +598,18 @@ void GatewayRuntime::clearEthernetIp() { } } +std::optional GatewayRuntime::ethernetConfig() const { + return settings_.getEthernetConfig(); +} + +bool GatewayRuntime::setEthernetConfig(const EthernetConfig& config) { + return settings_.setEthernetConfig(config); +} + +bool GatewayRuntime::clearEthernetConfig() { + return settings_.clearEthernetConfig(); +} + void GatewayRuntime::setCommandAddressResolver( std::function resolver) { LockGuard guard(command_lock_); @@ -599,6 +695,27 @@ bool GatewayRuntime::setGatewayIdForChannel(uint8_t channel_index, uint8_t gatew return true; } +uint8_t GatewayRuntime::gatewayGroupForChannel(uint8_t channel_index, uint8_t fallback) const { + LockGuard guard(command_lock_); + const auto cached = channel_gateway_groups_.find(channel_index); + if (cached != channel_gateway_groups_.end()) { + return cached->second; + } + const uint8_t gateway_group = settings_.getChannelGatewayGroup(channel_index, fallback); + channel_gateway_groups_[channel_index] = gateway_group; + return gateway_group; +} + +bool GatewayRuntime::setGatewayGroupForChannel(uint8_t channel_index, uint8_t gateway_group) { + if (!settings_.setChannelGatewayGroup(channel_index, gateway_group)) { + return false; + } + + LockGuard guard(command_lock_); + channel_gateway_groups_[channel_index] = gateway_group; + return true; +} + std::string GatewayRuntime::deviceName() const { LockGuard guard(command_lock_); if (device_name_.has_value()) {