feat: Add gateway group management and TCP control support

- Introduced gateway group functionality in GatewayController, allowing for grouping of gateways.
- Added methods to retrieve and set gateway groups for channels in GatewayRuntime and GatewaySettingsStore.
- Implemented TCP control task in GatewayNetworkService to handle control commands over TCP.
- Enhanced JSON handling for control frames, including discovery and configuration commands.
- Updated Ethernet configuration management to support new settings for gateway groups.
- Improved snapshot and reporting mechanisms to include gateway group information.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-06-27 02:16:57 +08:00
parent c04fc6a856
commit 2c96d6b097
12 changed files with 852 additions and 41 deletions
+24
View File
@@ -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":"<hex gateway frame>"}` 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
+43 -17
View File
@@ -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
endmenu
+28
View File
@@ -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<uint16_t>(CONFIG_GATEWAY_NETWORK_HTTP_PORT);
network_config.udp_port = static_cast<uint16_t>(CONFIG_GATEWAY_NETWORK_UDP_PORT);
network_config.tcp_control_port =
static_cast<uint16_t>(CONFIG_GATEWAY_NETWORK_TCP_CONTROL_PORT);
network_config.tcp_control_task_stack_size =
static_cast<uint32_t>(CONFIG_GATEWAY_NETWORK_TCP_CONTROL_TASK_STACK_SIZE);
network_config.tcp_control_task_priority =
static_cast<UBaseType_t>(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;
+4
View File
@@ -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
@@ -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<uint8_t>& result);
GatewayCommandTransactionResult transactRoutedCommand(const std::vector<uint8_t>& command);
bool shouldSuppressNotification(const std::vector<uint8_t>& frame);
bool responseMatchesActiveSession(const std::vector<uint8_t>& frame) const;
bool activeSessionTargetMatches(uint8_t response_gateway) const;
GatewayController& controller_;
Gateway485ControlBridgeConfig config_;
@@ -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<uint8_t>& 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<uint8_t> 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<uint8_t>& 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<uint8_t>& 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:
@@ -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<uint8_t> 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<uint8_t> gatewayIds() const;
std::optional<uint8_t> gatewayIdForChannelNumber(uint8_t channel_number) const;
std::optional<uint8_t> 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<uint8_t>& payload);
@@ -266,6 +270,7 @@ class GatewayController {
void handleGatewayNameCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleGatewaySerialCommand(uint8_t channel_number, const std::vector<uint8_t>& command);
void handleGatewayGroupCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void publishGatewaySerialReport();
void publishGatewaySerialResponse(uint8_t status, uint8_t op,
const std::vector<uint8_t>& data);
@@ -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<uint8_t> GatewayController::gatewayIdsForControlTarget(uint8_t target) const {
const auto channels = dali_domain_.channelInfo();
std::vector<uint8_t> 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<GatewayController*>(arg)->taskLoop();
}
@@ -1167,6 +1206,7 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t> GatewayController::gatewayIdForChannelNumber(
return it->gateway_id;
}
std::optional<uint8_t> 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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>& 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<uint8_t>(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) {
@@ -6,6 +6,7 @@
#include <string>
#include <vector>
#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<std::string> handleJsonControlFrame(const uint8_t* data, size_t len);
bool enqueueControlFrameForTargets(const std::vector<uint8_t>& frame);
std::string discoveryResponseJson() const;
std::string setEthernetConfigFromJson(cJSON* data);
esp_err_t applyEthernetConfig();
void handleGatewayNotification(const std::vector<uint8_t>& 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<uint8_t, 6> 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
} // namespace gateway
@@ -21,6 +21,7 @@
#include "lwip/inet.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <cstdlib>
@@ -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<uint8_t>& 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<unsigned char>(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<GatewayNetworkService*>(arg)->udpTaskLoop();
}
void GatewayNetworkService::TcpControlTaskEntry(void* arg) {
static_cast<GatewayNetworkService*>(arg)->tcpControlTaskLoop();
}
void GatewayNetworkService::BootButtonTaskEntry(void* arg) {
static_cast<GatewayNetworkService*>(arg)->bootButtonTaskLoop();
}
@@ -1359,7 +1435,84 @@ void GatewayNetworkService::udpTaskLoop() {
has_udp_remote_ = true;
}
controller_.enqueueCommandFrame(std::vector<uint8_t>(buffer, buffer + read_len));
const auto response =
handleJsonControlFrame(buffer, static_cast<size_t>(read_len));
if (response.has_value()) {
sendto(udp_socket_, response->data(), response->size(), 0,
reinterpret_cast<const sockaddr*>(&remote_addr), remote_addr_len);
continue;
}
handleNetworkControlBytes(buffer, static_cast<size_t>(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<sockaddr*>(&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<sockaddr*>(&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<size_t>(read_len));
if (response.has_value()) {
send(client, response->data(), response->size(), 0);
continue;
}
handleNetworkControlBytes(buffer, static_cast<size_t>(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<uint8_t>& 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<const sockaddr*>(&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<const sockaddr*>(&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<uint8_t>(data, data + len));
}
bool GatewayNetworkService::enqueueControlFrameForTargets(const std::vector<uint8_t>& 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<uint8_t> 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<std::string> GatewayNetworkService::handleJsonControlFrame(const uint8_t* data,
size_t len) {
if (!IsLikelyJson(data, len)) {
return std::nullopt;
}
cJSON* root = cJSON_ParseWithLength(reinterpret_cast<const char*>(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<uint8_t> 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);
@@ -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<std::string> getWifiPassword() const;
bool setWifiCredentials(std::string_view ssid, std::string_view password);
bool clearWifiCredentials();
std::optional<EthernetConfig> 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<std::string> 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> ethernetConfig() const;
bool setEthernetConfig(const EthernetConfig& config);
bool clearEthernetConfig();
void setCommandAddressResolver(std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> 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<std::string> device_name_;
mutable std::map<uint8_t, std::string> gateway_names_;
mutable std::map<uint8_t, uint8_t> channel_gateway_ids_;
mutable std::map<uint8_t, uint8_t> channel_gateway_groups_;
size_t gateway_count_{0};
bool ble_enabled_{false};
bool cache_enabled_{true};
@@ -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<EthernetConfig> 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<std::string> 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<EthernetConfig> 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<uint8_t(uint8_t gw, uint8_t raw_addr)> 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()) {