feat: Add cache management and USB configuration support

- Implemented `setCacheEnabled` method in `GatewayCache` to enable/disable caching with state preservation.
- Added `setCacheEnabled` and `cacheEnabled` methods in `GatewayController` to manage cache settings.
- Enhanced `GatewayNetworkService` to include new JSON endpoints for getting and setting gateway settings, including cache and USB configurations.
- Introduced `GatewayUsbMode` enum and related methods for managing USB modes in `GatewayRuntime` and `GatewaySettingsStore`.
- Updated runtime configuration to persist USB mode and channel index.
- Added validation for configured channels in USB setup and Tridonic HID components to ensure proper channel usage.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-07-28 04:02:58 +08:00
parent 83733802fc
commit 1e8bda56e0
13 changed files with 1041 additions and 9 deletions
+66 -1
View File
@@ -155,6 +155,71 @@ and gateway notification opcodes `0x01` and `0x65` are suppressed unless the
lease is active. Direct command/query responses still work without a raw-report
lease.
## Gateway runtime settings
`GET /bridge?action=gateway_settings` returns the versioned runtime-settings
document. `POST /bridge?action=gateway_settings` accepts a partial patch inside
`settings`. The mutable fields are the device name, BLE/cache switches, Wi-Fi
credentials, Ethernet DHCP/static IPv4 configuration, the boot USB personality,
and channel gateway ids/groups/names. Wi-Fi passwords are write-only: reads
contain `wifi.passwordSet`, never the password itself. Device and channel names
must contain at least one non-whitespace character and be at most 32 bytes.
```json
{
"schemaVersion": 1,
"settings": {
"deviceName": "DALI Gateway",
"bleEnabled": true,
"cacheEnabled": true,
"wifi": {"configured": true, "ssid": "example", "passwordSet": true},
"ethernet": {"dhcp": true, "ip": "", "mask": "", "gateway": "", "dns": ""},
"usb": {"mode": "debug", "channelIndex": 0, "restartRequired": false},
"channels": [{"channelIndex": 0, "gatewayId": 3, "gatewayGroup": 0,
"name": "Gateway_3", "phy": "native"}]
}
}
```
USB mode is one of `debug`, `setup`, or `tridonic`. New devices default to
`debug`; Tridonic HID is never the runtime default. The desired USB mode and
channel are stored in NVS, then applied only after restart because USB
descriptors cannot be changed while attached. A response sets
`settings.usb.restartRequired` and top-level `restartRequired` when the desired
value differs from the personality running at boot. Channel indexes are
zero-based. `capabilities.usb.channelIndices` is the authoritative list of
configured DALI channel indexes, for example:
```json
{
"capabilities": {
"usb": {
"supported": true,
"modes": ["debug", "setup", "tridonic"],
"channelIndices": [0, 2]
}
}
}
```
For `setup` and `tridonic`, `settings.usb.channelIndex` must be one of those
values. An unavailable channel is rejected before it is persisted with an
error that points clients to `capabilities.usb.channelIndices`; it is not a
generic `0` through `15` selector. At boot, legacy invalid persisted USB
channels are normalized to the first configured channel when NVS is writable;
the current boot runs in safe `debug` mode rather than starting a USB
personality on an unintended bus. Hardware PHY/pin mappings, task sizing, and
other Kconfig build settings are exposed only as build capabilities/defaults;
they are not mutable runtime settings.
When the MQTT cloud bridge is provisioned, the same document is available on
the existing `<topicPrefix>/<deviceID>/down` topic. Send
`{"type":"gatewaySettingsGet","requestId":"..."}` or
`{"type":"gatewaySettingsSet","requestId":"...","settings":{...}}`.
The gateway publishes `type:"gatewaySettings"`, the request id, a status of
`ok` on success or `error`, and the sanitized current
settings/capabilities/build document on the normal `up` topic.
## C++ 485 session frames
The optional raw 485 control bridge requires a one-byte random session hash for
@@ -179,7 +244,7 @@ LAMMIN legacy/new 485 paths continue to use their existing framing.
## Current status
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, setup AP GPIO entry, and optional Wi-Fi credential reset GPIO handling, and an optional `gateway_485_control` bridge that claims UART0 for C++ session-scoped command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, setup AP GPIO entry, and optional Wi-Fi credential reset GPIO handling, and an optional `gateway_485_control` bridge that claims UART0 for C++ session-scoped command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the persisted USB personality starts in debug mode unless it was explicitly changed to setup or Tridonic before reboot, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
## KNX Security
@@ -7,6 +7,7 @@
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
#include "esp_err.h"
@@ -21,6 +22,11 @@ class DaliDomainService;
struct DaliRawFrame;
class GatewayCache;
struct GatewayBridgeHttpResponse {
esp_err_t err{ESP_OK};
std::string body;
};
struct GatewayBridgeServiceConfig {
bool bridge_enabled{true};
bool modbus_enabled{true};
@@ -43,15 +49,13 @@ struct GatewayBridgeServiceConfig {
UBaseType_t knx_task_priority{5};
std::optional<GatewayKnxConfig> default_knx_config;
std::function<std::string()> gateway_device_name_provider;
std::function<GatewayBridgeHttpResponse()> gateway_settings_getter;
std::function<GatewayBridgeHttpResponse(std::string_view)> gateway_settings_setter;
std::function<std::string(std::string_view)> gateway_settings_cloud_handler;
GatewayKnxGatewaySnapshotProvider knx_gateway_snapshot_provider;
GatewayKnxGatewayCommandTransactor knx_gateway_command_transactor;
};
struct GatewayBridgeHttpResponse {
esp_err_t err{ESP_OK};
std::string body;
};
class GatewayBridgeService {
public:
GatewayBridgeService(DaliDomainService& dali_domain,
@@ -1921,6 +1921,7 @@ struct GatewayBridgeService::ChannelRuntime {
for (const auto& model : bridge_config.models) {
cloud->bridge().upsertModel(model);
}
cloud->setGatewaySettingsHandler(service_config.gateway_settings_cloud_handler);
wireCloudCemiProxyLocked();
}
@@ -4682,6 +4683,13 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
action = "status";
}
if (action == "gateway_settings") {
if (!config_.gateway_settings_getter) {
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "gateway settings are not available");
}
return config_.gateway_settings_getter();
}
if (action == "status" && !gateway_id.has_value()) {
cJSON* root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "enabled", true);
@@ -4853,6 +4861,12 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
if (action.empty()) {
action = "execute";
}
if (action == "gateway_settings") {
if (!config_.gateway_settings_setter) {
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "gateway settings are not available");
}
return config_.gateway_settings_setter(body);
}
if (!gateway_id.has_value()) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
}
@@ -167,6 +167,7 @@ class GatewayCache {
void markSettingsUpdateNeeded(uint8_t gateway_id, bool needed = true);
bool cacheEnabled() const;
bool setCacheEnabled(bool enabled);
bool reconciliationEnabled() const;
bool fullStateMirrorEnabled() const;
bool mirrorDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint8_t command);
@@ -249,4 +250,4 @@ class GatewayCache {
bool dirty_{false};
};
} // namespace gateway
} // namespace gateway
+26 -1
View File
@@ -960,6 +960,31 @@ bool GatewayCache::cacheEnabled() const {
return config_.cache_enabled;
}
bool GatewayCache::setCacheEnabled(bool enabled) {
{
LockGuard guard(lock_);
if (config_.cache_enabled == enabled) {
return true;
}
}
if (!enabled) {
// Preserve pending state before disabling the background cache worker.
if (!flushDirty()) {
return false;
}
LockGuard guard(lock_);
config_.cache_enabled = false;
return true;
}
{
LockGuard guard(lock_);
config_.cache_enabled = true;
}
return start() == ESP_OK;
}
bool GatewayCache::reconciliationEnabled() const {
return config_.cache_enabled && config_.reconciliation_enabled;
}
@@ -1766,4 +1791,4 @@ bool GatewayCache::eraseKeyLocked(std::string_view key) {
return true;
}
} // namespace gateway
} // namespace gateway
@@ -116,12 +116,22 @@ class GatewayController {
bool wirelessSetupMode() const;
void setWirelessSetupMode(bool enabled);
bool bleEnabled() const;
bool setBleEnabled(bool enabled);
bool cacheEnabled() const;
bool setCacheEnabled(bool enabled);
bool wifiEnabled() const;
bool ipRouterEnabled() const;
bool rawReportingEnabled(uint8_t gateway_id) const;
bool setDeviceName(std::string_view name);
GatewayControllerSnapshot snapshot();
std::vector<uint8_t> gatewayIdsForControlTarget(uint8_t target) const;
/// Runtime configuration helpers used by the local/cloud gateway-settings
/// surface. They persist the same values as the compatible raw commands.
bool setChannelGatewayId(uint8_t channel_index, uint8_t gateway_id);
bool setChannelGatewayGroup(uint8_t channel_index, uint8_t gateway_group);
bool setChannelName(uint8_t channel_index, std::string_view name);
struct ParsedTlv {
uint8_t type{0};
std::vector<uint8_t> value;
@@ -734,6 +734,44 @@ bool GatewayController::bleEnabled() const {
return ble_enabled_;
}
bool GatewayController::setBleEnabled(bool enabled) {
if (!config_.ble_supported || !runtime_.setBleEnabled(enabled)) {
return false;
}
ble_enabled_ = runtime_.bleEnabled();
for (const auto& sink : ble_state_sinks_) {
sink(ble_enabled_);
}
return true;
}
bool GatewayController::cacheEnabled() const {
return cache_.cacheEnabled();
}
bool GatewayController::setCacheEnabled(bool enabled) {
if (!config_.cache_supported || !runtime_.setCacheEnabled(enabled)) {
return false;
}
if (cache_.setCacheEnabled(enabled)) {
return true;
}
// Keep the persisted setting aligned with the live cache when the worker
// cannot be started or stopped.
runtime_.setCacheEnabled(cache_.cacheEnabled());
return false;
}
bool GatewayController::setDeviceName(std::string_view name) {
if (!runtime_.setDeviceName(name)) {
return false;
}
for (const auto& sink : gateway_name_sinks_) {
sink(0xff);
}
return true;
}
bool GatewayController::wifiEnabled() const {
return wifi_enabled_;
}
@@ -824,6 +862,57 @@ std::vector<uint8_t> GatewayController::gatewayIdsForControlTarget(uint8_t targe
return ids;
}
bool GatewayController::setChannelGatewayId(uint8_t channel_index, uint8_t gateway_id) {
if (gateway_id >= kGatewayGroupMin) {
return false;
}
const auto current_gateway_id = dali_domain_.gatewayIdForChannelIndex(channel_index);
if (!current_gateway_id.has_value()) {
return false;
}
if (current_gateway_id.value() == gateway_id) {
return true;
}
if (!dali_domain_.updateChannelGatewayId(channel_index, gateway_id)) {
return false;
}
if (!runtime_.setGatewayIdForChannel(channel_index, gateway_id)) {
dali_domain_.updateChannelGatewayId(channel_index, current_gateway_id.value());
return false;
}
cache_.preloadChannel(gateway_id);
{
LockGuard guard(maintenance_lock_);
reconciliation_jobs_.erase(current_gateway_id.value());
cache_refresh_jobs_.erase(current_gateway_id.value());
}
refreshRuntimeGatewayNames();
return true;
}
bool GatewayController::setChannelGatewayGroup(uint8_t channel_index, uint8_t gateway_group) {
if (gateway_group != 0 &&
(gateway_group < kGatewayGroupMin || gateway_group > kGatewayGroupMax)) {
return false;
}
if (!dali_domain_.gatewayIdForChannelIndex(channel_index).has_value()) {
return false;
}
return runtime_.setGatewayGroupForChannel(channel_index, gateway_group);
}
bool GatewayController::setChannelName(uint8_t channel_index, std::string_view name) {
const auto gateway_id = dali_domain_.gatewayIdForChannelIndex(channel_index);
if (!gateway_id.has_value() || !runtime_.setGatewayName(gateway_id.value(), name)) {
return false;
}
refreshRuntimeGatewayNames();
for (const auto& sink : gateway_name_sinks_) {
sink(gateway_id.value());
}
return true;
}
void GatewayController::TaskEntry(void* arg) {
static_cast<GatewayController*>(arg)->taskLoop();
}
@@ -4,6 +4,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "cJSON.h"
@@ -27,9 +28,19 @@ namespace gateway {
class GatewayController;
class DaliDomainService;
class GatewayBridgeService;
struct GatewayBridgeHttpResponse;
struct DaliRawFrame;
struct GatewayNetworkServiceConfig {
bool wifi_supported{false};
bool ethernet_supported{false};
bool ble_supported{false};
bool cache_supported{false};
bool usb_supported{false};
GatewayUsbMode usb_default_mode{GatewayUsbMode::kDebug};
uint8_t usb_default_channel_index{0};
GatewayUsbMode usb_active_mode{GatewayUsbMode::kDebug};
uint8_t usb_active_channel_index{0};
bool wifi_enabled{true};
bool ethernet_enabled{false};
bool ethernet_ignore_init_failure{false};
@@ -79,6 +90,12 @@ class GatewayNetworkService {
esp_err_t start();
/// Stable runtime settings surface shared by local HTTP, the bridge API, and
/// cloud provisioning. The payload is a schema-versioned JSON document.
GatewayBridgeHttpResponse gatewaySettingsGet() const;
GatewayBridgeHttpResponse gatewaySettingsSet(std::string_view body);
std::string gatewaySettingsCloudResponse(std::string_view request);
private:
static void UdpTaskEntry(void* arg);
static void TcpControlTaskEntry(void* arg);
@@ -124,6 +141,9 @@ class GatewayNetworkService {
bool enqueueControlFrameForTargets(const std::vector<uint8_t>& frame);
std::string discoveryResponseJson() const;
std::string setEthernetConfigFromJson(cJSON* data);
std::string gatewaySettingsJson() const;
GatewayBridgeHttpResponse gatewaySettingsError(std::string_view message) const;
bool applyGatewaySettings(cJSON* settings, std::string* error);
esp_err_t applyEthernetConfig();
void handleGatewayNotification(const std::vector<uint8_t>& frame);
void handleWifiControl(uint8_t mode);
@@ -25,6 +25,8 @@
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <optional>
#include <set>
#include <string_view>
#include <utility>
#include <vector>
@@ -120,10 +122,51 @@ std::string LocalMacHex(wifi_interface_t interface) {
}
const char* JsonString(cJSON* parent, const char* name) {
if (parent == nullptr || name == nullptr) {
return nullptr;
}
cJSON* item = cJSON_GetObjectItem(parent, name);
return cJSON_IsString(item) ? item->valuestring : nullptr;
}
bool JsonUint8(cJSON* parent, const char* name, uint8_t* value) {
if (parent == nullptr || name == nullptr || value == nullptr) {
return false;
}
cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
if (!cJSON_IsNumber(item) || item->valuedouble < 0 || item->valuedouble > 255 ||
item->valuedouble != static_cast<double>(item->valueint)) {
return false;
}
*value = static_cast<uint8_t>(item->valueint);
return true;
}
const char* PhyKindToString(DaliPhyKind phy_kind) {
switch (phy_kind) {
case DaliPhyKind::kNativeHardware:
return "native";
case DaliPhyKind::kSerialUart:
return "serial";
case DaliPhyKind::kCustom:
default:
return "custom";
}
}
bool HasConfiguredDaliChannel(const std::vector<DaliChannelInfo>& channels,
uint8_t channel_index) {
return std::any_of(channels.begin(), channels.end(), [channel_index](const auto& channel) {
return channel.channel_index == channel_index;
});
}
bool IsBlankName(std::string_view value) {
return std::all_of(value.begin(), value.end(), [](unsigned char character) {
return std::isspace(character) != 0;
});
}
std::vector<uint8_t> BytesFromJsonString(const char* value) {
if (value == nullptr) {
return {};
@@ -1913,6 +1956,590 @@ std::string GatewayNetworkService::setEthernetConfigFromJson(cJSON* data) {
return JsonResponse("setconfigResp", "ok");
}
GatewayBridgeHttpResponse GatewayNetworkService::gatewaySettingsError(
std::string_view message) const {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return {ESP_ERR_NO_MEM, {}};
}
cJSON_AddStringToObject(root, "status", "error");
cJSON_AddStringToObject(root, "error", std::string(message).c_str());
const std::string body = PrintJson(root);
cJSON_Delete(root);
return {ESP_ERR_INVALID_ARG, body};
}
std::string GatewayNetworkService::gatewaySettingsJson() const {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return {};
}
cJSON_AddNumberToObject(root, "schemaVersion", 1);
cJSON* settings = cJSON_CreateObject();
if (settings == nullptr) {
cJSON_Delete(root);
return {};
}
cJSON_AddStringToObject(settings, "deviceName", runtime_.deviceName().c_str());
cJSON_AddBoolToObject(settings, "bleEnabled", controller_.bleEnabled());
cJSON_AddBoolToObject(settings, "cacheEnabled", controller_.cacheEnabled());
const auto device_info = runtime_.deviceInfo();
cJSON* wifi = cJSON_CreateObject();
if (wifi != nullptr) {
const bool configured =
device_info.wlan.has_value() &&
(!device_info.wlan->ssid.empty() || !device_info.wlan->password.empty());
cJSON_AddBoolToObject(wifi, "configured", configured);
cJSON_AddStringToObject(wifi, "ssid",
device_info.wlan.has_value() ? device_info.wlan->ssid.c_str() : "");
cJSON_AddBoolToObject(wifi, "passwordSet",
device_info.wlan.has_value() && !device_info.wlan->password.empty());
cJSON_AddItemToObject(settings, "wifi", wifi);
}
cJSON* ethernet = cJSON_CreateObject();
if (ethernet != nullptr) {
const auto ethernet_config = runtime_.ethernetConfig();
const bool dhcp = !ethernet_config.has_value() || ethernet_config->ip.empty() ||
ethernet_config->ip == "0.0.0.0";
cJSON_AddBoolToObject(ethernet, "dhcp", dhcp);
cJSON_AddStringToObject(ethernet, "ip",
ethernet_config.has_value() ? ethernet_config->ip.c_str() : "");
cJSON_AddStringToObject(ethernet, "mask",
ethernet_config.has_value() ? ethernet_config->mask.c_str() : "");
cJSON_AddStringToObject(
ethernet, "gateway",
ethernet_config.has_value() ? ethernet_config->gateway.c_str() : "");
cJSON_AddStringToObject(ethernet, "dns",
ethernet_config.has_value() ? ethernet_config->dns.c_str() : "");
cJSON_AddItemToObject(settings, "ethernet", ethernet);
}
cJSON* usb = cJSON_CreateObject();
if (usb != nullptr) {
const auto desired_mode = runtime_.usbMode();
const uint8_t desired_channel = runtime_.usbChannelIndex();
const bool restart_required =
config_.usb_supported &&
(desired_mode != config_.usb_active_mode ||
desired_channel != config_.usb_active_channel_index);
cJSON_AddStringToObject(usb, "mode", GatewayUsbModeToString(desired_mode));
cJSON_AddNumberToObject(usb, "channelIndex", desired_channel);
cJSON_AddBoolToObject(usb, "restartRequired", restart_required);
cJSON_AddItemToObject(settings, "usb", usb);
cJSON_AddBoolToObject(root, "restartRequired", restart_required);
}
cJSON* channels = cJSON_CreateArray();
if (channels != nullptr) {
for (const auto& channel : dali_domain_.channelInfo()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
}
cJSON_AddNumberToObject(item, "channelIndex", channel.channel_index);
cJSON_AddNumberToObject(item, "gatewayId", channel.gateway_id);
cJSON_AddNumberToObject(item, "gatewayGroup",
runtime_.gatewayGroupForChannel(channel.channel_index));
cJSON_AddStringToObject(item, "name", channel.name.c_str());
cJSON_AddStringToObject(item, "phy", PhyKindToString(channel.phy_kind));
cJSON_AddItemToArray(channels, item);
}
cJSON_AddItemToObject(settings, "channels", channels);
}
cJSON_AddItemToObject(root, "settings", settings);
cJSON* capabilities = cJSON_CreateObject();
if (capabilities != nullptr) {
cJSON_AddBoolToObject(capabilities, "wifi", config_.wifi_supported);
cJSON_AddBoolToObject(capabilities, "ethernet", config_.ethernet_supported);
cJSON_AddBoolToObject(capabilities, "ble", config_.ble_supported);
cJSON_AddBoolToObject(capabilities, "cache", config_.cache_supported);
cJSON* usb_capabilities = cJSON_CreateObject();
if (usb_capabilities != nullptr) {
cJSON_AddBoolToObject(usb_capabilities, "supported", config_.usb_supported);
cJSON* modes = cJSON_CreateArray();
if (modes != nullptr) {
cJSON_AddItemToArray(modes, cJSON_CreateString("debug"));
cJSON_AddItemToArray(modes, cJSON_CreateString("setup"));
cJSON_AddItemToArray(modes, cJSON_CreateString("tridonic"));
cJSON_AddItemToObject(usb_capabilities, "modes", modes);
}
cJSON* channel_indices = cJSON_CreateArray();
if (channel_indices != nullptr) {
for (const auto& channel : dali_domain_.channelInfo()) {
cJSON_AddItemToArray(channel_indices, cJSON_CreateNumber(channel.channel_index));
}
cJSON_AddItemToObject(usb_capabilities, "channelIndices", channel_indices);
}
cJSON_AddItemToObject(capabilities, "usb", usb_capabilities);
}
cJSON_AddItemToObject(root, "capabilities", capabilities);
}
cJSON* build = cJSON_CreateObject();
if (build != nullptr) {
cJSON_AddStringToObject(build, "usbDefaultMode",
GatewayUsbModeToString(config_.usb_default_mode));
cJSON_AddNumberToObject(build, "usbDefaultChannelIndex",
config_.usb_default_channel_index);
cJSON_AddItemToObject(root, "build", build);
}
const std::string rendered = PrintJson(root);
cJSON_Delete(root);
return rendered;
}
GatewayBridgeHttpResponse GatewayNetworkService::gatewaySettingsGet() const {
const std::string body = gatewaySettingsJson();
if (body.empty()) {
return {ESP_ERR_NO_MEM, {}};
}
return {ESP_OK, body};
}
bool GatewayNetworkService::applyGatewaySettings(cJSON* settings, std::string* error) {
const auto fail = [error](std::string_view message) {
if (error != nullptr) {
*error = std::string(message);
}
return false;
};
if (!cJSON_IsObject(settings)) {
return fail("settings must be a JSON object");
}
cJSON* device_name_item = cJSON_GetObjectItemCaseSensitive(settings, "deviceName");
cJSON* ble_item = cJSON_GetObjectItemCaseSensitive(settings, "bleEnabled");
cJSON* cache_item = cJSON_GetObjectItemCaseSensitive(settings, "cacheEnabled");
if (device_name_item != nullptr && !cJSON_IsString(device_name_item)) {
return fail("deviceName must be a string");
}
if (device_name_item != nullptr) {
const std::string_view device_name = device_name_item->valuestring == nullptr
? std::string_view()
: std::string_view(device_name_item->valuestring);
if (IsBlankName(device_name)) {
return fail("deviceName must contain at least one non-whitespace character");
}
if (device_name.size() > 32) {
return fail("deviceName must be at most 32 bytes");
}
}
if (ble_item != nullptr && !cJSON_IsBool(ble_item)) {
return fail("bleEnabled must be a boolean");
}
if (cache_item != nullptr && !cJSON_IsBool(cache_item)) {
return fail("cacheEnabled must be a boolean");
}
if (ble_item != nullptr && !config_.ble_supported) {
return fail("BLE is not supported by this gateway build");
}
if (cache_item != nullptr && !config_.cache_supported) {
return fail("cache is not supported by this gateway build");
}
struct WifiPatch {
bool present{false};
std::optional<bool> configured;
std::optional<std::string> ssid;
std::optional<std::string> password;
} wifi_patch;
cJSON* wifi = cJSON_GetObjectItemCaseSensitive(settings, "wifi");
if (wifi != nullptr) {
if (!config_.wifi_supported) {
return fail("Wi-Fi is not supported by this gateway build");
}
if (!cJSON_IsObject(wifi)) {
return fail("wifi must be an object");
}
wifi_patch.present = true;
cJSON* configured = cJSON_GetObjectItemCaseSensitive(wifi, "configured");
cJSON* ssid = cJSON_GetObjectItemCaseSensitive(wifi, "ssid");
cJSON* password = cJSON_GetObjectItemCaseSensitive(wifi, "password");
if (configured != nullptr) {
if (!cJSON_IsBool(configured)) {
return fail("wifi.configured must be a boolean");
}
wifi_patch.configured = cJSON_IsTrue(configured);
}
if (ssid != nullptr) {
if (!cJSON_IsString(ssid)) {
return fail("wifi.ssid must be a string");
}
wifi_patch.ssid = ssid->valuestring == nullptr ? "" : ssid->valuestring;
}
if (password != nullptr) {
if (!cJSON_IsString(password)) {
return fail("wifi.password must be a string");
}
wifi_patch.password = password->valuestring == nullptr ? "" : password->valuestring;
}
}
struct EthernetPatch {
bool present{false};
std::optional<bool> dhcp;
std::optional<std::string> ip;
std::optional<std::string> mask;
std::optional<std::string> gateway;
std::optional<std::string> dns;
} ethernet_patch;
cJSON* ethernet = cJSON_GetObjectItemCaseSensitive(settings, "ethernet");
if (ethernet != nullptr) {
if (!config_.ethernet_supported) {
return fail("Ethernet is not supported by this gateway build");
}
if (!cJSON_IsObject(ethernet)) {
return fail("ethernet must be an object");
}
ethernet_patch.present = true;
cJSON* dhcp = cJSON_GetObjectItemCaseSensitive(ethernet, "dhcp");
if (dhcp != nullptr) {
if (!cJSON_IsBool(dhcp)) {
return fail("ethernet.dhcp must be a boolean");
}
ethernet_patch.dhcp = cJSON_IsTrue(dhcp);
}
const auto parse_ethernet_string = [&fail, ethernet](const char* key,
std::optional<std::string>* value) {
cJSON* item = cJSON_GetObjectItemCaseSensitive(ethernet, key);
if (item == nullptr) {
return true;
}
if (!cJSON_IsString(item)) {
return fail(std::string("ethernet.") + key + " must be a string");
}
*value = item->valuestring == nullptr ? "" : item->valuestring;
return true;
};
if (!parse_ethernet_string("ip", &ethernet_patch.ip) ||
!parse_ethernet_string("mask", &ethernet_patch.mask) ||
!parse_ethernet_string("gateway", &ethernet_patch.gateway) ||
!parse_ethernet_string("dns", &ethernet_patch.dns)) {
return false;
}
}
struct UsbPatch {
bool present{false};
std::optional<GatewayUsbMode> mode;
std::optional<uint8_t> channel_index;
} usb_patch;
cJSON* usb = cJSON_GetObjectItemCaseSensitive(settings, "usb");
if (usb != nullptr) {
if (!config_.usb_supported) {
return fail("USB is not supported by this gateway build");
}
if (!cJSON_IsObject(usb)) {
return fail("usb must be an object");
}
usb_patch.present = true;
cJSON* mode = cJSON_GetObjectItemCaseSensitive(usb, "mode");
if (mode != nullptr) {
if (!cJSON_IsString(mode) || mode->valuestring == nullptr) {
return fail("usb.mode must be debug, setup, or tridonic");
}
usb_patch.mode = GatewayUsbModeFromString(mode->valuestring);
if (!usb_patch.mode.has_value()) {
return fail("usb.mode must be debug, setup, or tridonic");
}
}
cJSON* channel_index = cJSON_GetObjectItemCaseSensitive(usb, "channelIndex");
if (channel_index != nullptr) {
uint8_t parsed = 0;
if (!JsonUint8(usb, "channelIndex", &parsed)) {
return fail("usb.channelIndex must be an integer");
}
usb_patch.channel_index = parsed;
}
const auto configured_channels = dali_domain_.channelInfo();
const auto has_channel = [&configured_channels](uint8_t channel_index) {
return HasConfiguredDaliChannel(configured_channels, channel_index);
};
if (usb_patch.channel_index.has_value() && !has_channel(usb_patch.channel_index.value())) {
return fail("usb.channelIndex is not configured on this gateway; choose a value from "
"capabilities.usb.channelIndices");
}
const GatewayUsbMode effective_mode = usb_patch.mode.value_or(runtime_.usbMode());
const uint8_t effective_channel =
usb_patch.channel_index.value_or(runtime_.usbChannelIndex());
if (effective_mode != GatewayUsbMode::kDebug && !has_channel(effective_channel)) {
return fail("usb.mode requires usb.channelIndex to be a configured DALI channel; choose "
"a value from capabilities.usb.channelIndices");
}
}
struct ChannelPatch {
uint8_t channel_index{0};
std::optional<uint8_t> gateway_id;
std::optional<uint8_t> gateway_group;
std::optional<std::string> name;
};
std::vector<ChannelPatch> channel_patches;
cJSON* channels = cJSON_GetObjectItemCaseSensitive(settings, "channels");
if (channels != nullptr) {
if (!cJSON_IsArray(channels)) {
return fail("channels must be an array");
}
const auto available_channels = dali_domain_.channelInfo();
const int channel_count = cJSON_GetArraySize(channels);
for (int index = 0; index < channel_count; ++index) {
cJSON* item = cJSON_GetArrayItem(channels, index);
uint8_t channel_index = 0;
if (!cJSON_IsObject(item) || !JsonUint8(item, "channelIndex", &channel_index)) {
return fail("each channel patch requires an integer channelIndex");
}
const auto channel = std::find_if(
available_channels.begin(), available_channels.end(),
[channel_index](const DaliChannelInfo& value) {
return value.channel_index == channel_index;
});
if (channel == available_channels.end()) {
return fail("channelIndex does not exist on this gateway");
}
if (std::any_of(channel_patches.begin(), channel_patches.end(),
[channel_index](const ChannelPatch& value) {
return value.channel_index == channel_index;
})) {
return fail("each channel may be patched only once");
}
ChannelPatch patch;
patch.channel_index = channel_index;
cJSON* gateway_id = cJSON_GetObjectItemCaseSensitive(item, "gatewayId");
if (gateway_id != nullptr) {
uint8_t parsed = 0;
if (!JsonUint8(item, "gatewayId", &parsed) || parsed >= 128) {
return fail("channel gatewayId must be an integer from 0 to 127");
}
patch.gateway_id = parsed;
}
cJSON* gateway_group = cJSON_GetObjectItemCaseSensitive(item, "gatewayGroup");
if (gateway_group != nullptr) {
uint8_t parsed = 0;
if (!JsonUint8(item, "gatewayGroup", &parsed) ||
(parsed != 0 && (parsed < 128 || parsed > 254))) {
return fail("channel gatewayGroup must be 0 or an integer from 128 to 254");
}
patch.gateway_group = parsed;
}
cJSON* name = cJSON_GetObjectItemCaseSensitive(item, "name");
if (name != nullptr) {
if (!cJSON_IsString(name) || name->valuestring == nullptr) {
return fail("channel name must be a string");
}
patch.name = name->valuestring;
if (IsBlankName(patch.name.value())) {
return fail("channel name must contain at least one non-whitespace character");
}
if (patch.name->size() > 32) {
return fail("channel name must be at most 32 bytes");
}
}
channel_patches.push_back(std::move(patch));
}
// Reject duplicate final identifiers before changing anything. Renumbering
// swaps remains a two-step operation so a live DALI channel is never left
// briefly ambiguous.
std::vector<uint8_t> final_ids;
final_ids.reserve(available_channels.size());
for (const auto& channel : available_channels) {
auto patch = std::find_if(channel_patches.begin(), channel_patches.end(),
[&channel](const ChannelPatch& value) {
return value.channel_index == channel.channel_index;
});
final_ids.push_back(patch != channel_patches.end() && patch->gateway_id.has_value()
? patch->gateway_id.value()
: channel.gateway_id);
}
std::set<uint8_t> unique_ids(final_ids.begin(), final_ids.end());
if (unique_ids.size() != final_ids.size()) {
return fail("channel gatewayId values must be unique");
}
}
if (device_name_item != nullptr &&
!controller_.setDeviceName(device_name_item->valuestring == nullptr
? std::string_view()
: std::string_view(device_name_item->valuestring))) {
return fail("failed to save deviceName");
}
if (ble_item != nullptr && !controller_.setBleEnabled(cJSON_IsTrue(ble_item))) {
return fail("failed to apply bleEnabled");
}
if (cache_item != nullptr && !controller_.setCacheEnabled(cJSON_IsTrue(cache_item))) {
return fail("failed to apply cacheEnabled");
}
if (wifi_patch.present) {
if (wifi_patch.configured.has_value() && !wifi_patch.configured.value()) {
if (!runtime_.clearWirelessInfo()) {
return fail("failed to clear Wi-Fi credentials");
}
handleWifiControl(0);
} else {
auto info = runtime_.deviceInfo().wlan.value_or(WirelessInfo{});
if (wifi_patch.ssid.has_value()) {
info.ssid = wifi_patch.ssid.value();
}
if (wifi_patch.password.has_value()) {
info.password = wifi_patch.password.value();
}
if (info.ssid.empty()) {
return fail("wifi.ssid is required when Wi-Fi is configured");
}
runtime_.setWirelessInfo(std::move(info));
handleWifiControl(1);
}
}
if (ethernet_patch.present) {
const auto previous_config = runtime_.ethernetConfig();
EthernetConfig config = previous_config.value_or(EthernetConfig{});
if (ethernet_patch.ip.has_value()) config.ip = ethernet_patch.ip.value();
if (ethernet_patch.mask.has_value()) config.mask = ethernet_patch.mask.value();
if (ethernet_patch.gateway.has_value()) config.gateway = ethernet_patch.gateway.value();
if (ethernet_patch.dns.has_value()) config.dns = ethernet_patch.dns.value();
const bool previous_dhcp = !previous_config.has_value() || previous_config->ip.empty() ||
previous_config->ip == "0.0.0.0";
const bool dhcp = ethernet_patch.dhcp.value_or(previous_dhcp);
if (dhcp) {
if (!runtime_.clearEthernetConfig()) {
return fail("failed to save Ethernet DHCP configuration");
}
} else {
esp_ip4_addr_t parsed = {};
if (config.ip.empty() || config.mask.empty() || config.gateway.empty() ||
!ParseIpv4(config.ip, &parsed) || !ParseIpv4(config.mask, &parsed) ||
!ParseIpv4(config.gateway, &parsed) ||
(!config.dns.empty() && !ParseIpv4(config.dns, &parsed))) {
return fail("Ethernet static configuration requires valid ip, mask, and gateway");
}
if (!runtime_.setEthernetConfig(config)) {
return fail("failed to save Ethernet configuration");
}
}
if (applyEthernetConfig() != ESP_OK) {
return fail("failed to apply Ethernet configuration");
}
}
if (usb_patch.present) {
if (usb_patch.mode.has_value() && !runtime_.setUsbMode(usb_patch.mode.value())) {
return fail("failed to save usb.mode");
}
if (usb_patch.channel_index.has_value() &&
!runtime_.setUsbChannelIndex(usb_patch.channel_index.value())) {
return fail("failed to save usb.channelIndex");
}
}
for (const auto& patch : channel_patches) {
if (patch.gateway_id.has_value() &&
!controller_.setChannelGatewayId(patch.channel_index, patch.gateway_id.value())) {
return fail("failed to apply channel gatewayId");
}
if (patch.gateway_group.has_value() &&
!controller_.setChannelGatewayGroup(patch.channel_index, patch.gateway_group.value())) {
return fail("failed to apply channel gatewayGroup");
}
if (patch.name.has_value() &&
!controller_.setChannelName(patch.channel_index, patch.name.value())) {
return fail("failed to apply channel name");
}
}
return true;
}
GatewayBridgeHttpResponse GatewayNetworkService::gatewaySettingsSet(std::string_view body) {
cJSON* root = cJSON_ParseWithLength(body.data(), body.size());
if (root == nullptr) {
return gatewaySettingsError("invalid gateway settings JSON");
}
cJSON* settings = cJSON_GetObjectItemCaseSensitive(root, "settings");
std::string error;
const bool applied = applyGatewaySettings(settings, &error);
cJSON_Delete(root);
if (!applied) {
return gatewaySettingsError(error.empty() ? "failed to apply gateway settings" : error);
}
return gatewaySettingsGet();
}
std::string GatewayNetworkService::gatewaySettingsCloudResponse(std::string_view request) {
cJSON* request_json = cJSON_ParseWithLength(request.data(), request.size());
cJSON* request_id = nullptr;
std::string type;
if (request_json != nullptr) {
if (cJSON* request_id_item =
cJSON_GetObjectItemCaseSensitive(request_json, "requestId");
cJSON_IsString(request_id_item) || cJSON_IsNumber(request_id_item)) {
request_id = cJSON_Duplicate(request_id_item, true);
}
if (const char* value = JsonString(request_json, "type")) {
type = value;
}
}
GatewayBridgeHttpResponse result;
if (request_json == nullptr ||
(type != "gatewaySettingsGet" && type != "gatewaySettingsSet")) {
result = gatewaySettingsError("invalid gateway settings cloud request");
} else if (type == "gatewaySettingsGet") {
result = gatewaySettingsGet();
} else {
result = gatewaySettingsSet(request);
}
cJSON_Delete(request_json);
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
cJSON_Delete(request_id);
return {};
}
cJSON_AddStringToObject(response, "type", "gatewaySettings");
if (request_id != nullptr) {
cJSON_AddItemToObject(response, "requestId", request_id);
}
cJSON_AddStringToObject(response, "status",
result.err == ESP_OK ? "ok" : "error");
if (result.err != ESP_OK) {
cJSON* error_json = cJSON_ParseWithLength(result.body.data(), result.body.size());
if (const char* message = JsonString(error_json, "error")) {
cJSON_AddStringToObject(response, "error", message);
} else {
cJSON_AddStringToObject(response, "error", "failed to apply gateway settings");
}
cJSON_Delete(error_json);
}
// Always report the current sanitized document; the Wi-Fi password is never
// part of gatewaySettingsJson and therefore cannot be echoed to the cloud.
const auto settings_response = gatewaySettingsGet();
cJSON* settings_json =
cJSON_ParseWithLength(settings_response.body.data(), settings_response.body.size());
if (settings_json != nullptr) {
for (const char* key : {"schemaVersion", "settings", "capabilities", "build",
"restartRequired"}) {
cJSON* item = cJSON_GetObjectItemCaseSensitive(settings_json, key);
if (item != nullptr) {
cJSON_AddItemToObject(response, key, cJSON_Duplicate(item, true));
}
}
}
cJSON_Delete(settings_json);
const std::string rendered = PrintJson(response);
cJSON_Delete(response);
return rendered;
}
esp_err_t GatewayNetworkService::applyEthernetConfig() {
if (eth_netif_ == nullptr) {
return ESP_OK;
@@ -42,12 +42,26 @@ struct EthernetConfig {
std::string dns;
};
/// Boot-only USB personalities. The selected value is persisted in NVS so a
/// remote configuration survives reboot, but changing it cannot re-enumerate
/// an already-running USB device.
enum class GatewayUsbMode : uint8_t {
kDebug = 0,
kSetup = 1,
kTridonic = 2,
};
const char* GatewayUsbModeToString(GatewayUsbMode mode);
std::optional<GatewayUsbMode> GatewayUsbModeFromString(std::string_view value);
struct GatewayRuntimeConfig {
std::string_view project_name;
std::string_view version;
std::string serial_id;
bool default_ble_enabled{true};
bool default_cache_enabled{true};
GatewayUsbMode default_usb_mode{GatewayUsbMode::kDebug};
uint8_t default_usb_channel_index{0};
size_t command_queue_capacity{16};
};
@@ -84,6 +98,11 @@ class GatewaySettingsStore {
bool setEthernetConfig(const EthernetConfig& config);
bool clearEthernetConfig();
GatewayUsbMode getUsbMode(GatewayUsbMode default_value) const;
bool setUsbMode(GatewayUsbMode mode);
uint8_t getUsbChannelIndex(uint8_t default_value) const;
bool setUsbChannelIndex(uint8_t channel_index);
std::string getDeviceName(std::string_view fallback) const;
bool setDeviceName(std::string_view name);
std::string getGatewayName(uint8_t gateway_id, std::string_view fallback) const;
@@ -149,6 +168,10 @@ class GatewayRuntime {
std::optional<EthernetConfig> ethernetConfig() const;
bool setEthernetConfig(const EthernetConfig& config);
bool clearEthernetConfig();
GatewayUsbMode usbMode() const;
bool setUsbMode(GatewayUsbMode mode);
uint8_t usbChannelIndex() const;
bool setUsbChannelIndex(uint8_t channel_index);
void setCommandAddressResolver(std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> resolver);
GatewayDeviceInfo deviceInfo() const;
@@ -197,6 +220,8 @@ class GatewayRuntime {
size_t gateway_count_{0};
bool ble_enabled_{false};
bool cache_enabled_{true};
GatewayUsbMode usb_mode_{GatewayUsbMode::kDebug};
uint8_t usb_channel_index_{0};
CommandDropReason last_enqueue_drop_reason_{CommandDropReason::kNone};
std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> command_address_resolver_;
std::optional<WirelessInfo> wireless_info_;
@@ -26,6 +26,8 @@ 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 const char* kUsbModeKey = "usb_mode";
constexpr const char* kUsbChannelKey = "usb_channel";
constexpr size_t kMaxGatewayNameBytes = 32;
constexpr uint8_t kCommandFramePrefix0 = 0x28;
constexpr uint8_t kCommandFramePrefix1 = 0x01;
@@ -73,6 +75,31 @@ std::string ReadRuntimeSerialId() {
return std::string(serial);
}
const char* GatewayUsbModeToString(GatewayUsbMode mode) {
switch (mode) {
case GatewayUsbMode::kDebug:
return "debug";
case GatewayUsbMode::kSetup:
return "setup";
case GatewayUsbMode::kTridonic:
return "tridonic";
}
return "debug";
}
std::optional<GatewayUsbMode> GatewayUsbModeFromString(std::string_view value) {
if (value == "debug") {
return GatewayUsbMode::kDebug;
}
if (value == "setup") {
return GatewayUsbMode::kSetup;
}
if (value == "tridonic") {
return GatewayUsbMode::kTridonic;
}
return std::nullopt;
}
GatewaySettingsStore::GatewaySettingsStore() = default;
GatewaySettingsStore::~GatewaySettingsStore() {
@@ -227,6 +254,46 @@ bool GatewaySettingsStore::clearEthernetConfig() {
return ok && nvs_commit(handle_) == ESP_OK;
}
GatewayUsbMode GatewaySettingsStore::getUsbMode(GatewayUsbMode default_value) const {
if (handle_ == 0) {
return default_value;
}
uint8_t raw_mode = static_cast<uint8_t>(default_value);
if (nvs_get_u8(handle_, kUsbModeKey, &raw_mode) != ESP_OK ||
raw_mode > static_cast<uint8_t>(GatewayUsbMode::kTridonic)) {
return default_value;
}
return static_cast<GatewayUsbMode>(raw_mode);
}
bool GatewaySettingsStore::setUsbMode(GatewayUsbMode mode) {
if (handle_ == 0 || mode > GatewayUsbMode::kTridonic) {
return false;
}
return nvs_set_u8(handle_, kUsbModeKey, static_cast<uint8_t>(mode)) == ESP_OK &&
nvs_commit(handle_) == ESP_OK;
}
uint8_t GatewaySettingsStore::getUsbChannelIndex(uint8_t default_value) const {
if (handle_ == 0) {
return default_value;
}
uint8_t channel_index = default_value;
if (nvs_get_u8(handle_, kUsbChannelKey, &channel_index) != ESP_OK) {
return default_value;
}
return channel_index;
}
bool GatewaySettingsStore::setUsbChannelIndex(uint8_t channel_index) {
if (handle_ == 0) {
return false;
}
return nvs_set_u8(handle_, kUsbChannelKey, channel_index) == ESP_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()) {
@@ -375,6 +442,13 @@ esp_err_t GatewayRuntime::start() {
ble_enabled_ = settings_.getBleEnabled(config_.default_ble_enabled);
cache_enabled_ = settings_.getCacheEnabled(config_.default_cache_enabled);
usb_mode_ = settings_.getUsbMode(config_.default_usb_mode);
usb_channel_index_ = settings_.getUsbChannelIndex(config_.default_usb_channel_index);
if (usb_channel_index_ > 15) {
usb_channel_index_ = config_.default_usb_channel_index > 15
? 0
: config_.default_usb_channel_index;
}
if (!wireless_info_.has_value()) {
WirelessInfo info;
@@ -392,10 +466,11 @@ esp_err_t GatewayRuntime::start() {
}
ESP_LOGI(kTag,
"runtime project=%.*s version=%.*s serial=%s ble=%d cache=%d dali_bound=%d",
"runtime project=%.*s version=%.*s serial=%s ble=%d cache=%d usb=%s/%u dali_bound=%d",
static_cast<int>(config_.project_name.size()), config_.project_name.data(),
static_cast<int>(config_.version.size()), config_.version.data(),
config_.serial_id.c_str(), ble_enabled_, cache_enabled_,
GatewayUsbModeToString(usb_mode_), usb_channel_index_,
dali_domain_ != nullptr && dali_domain_->isBound());
return ESP_OK;
}
@@ -610,6 +685,62 @@ bool GatewayRuntime::clearEthernetConfig() {
return settings_.clearEthernetConfig();
}
GatewayUsbMode GatewayRuntime::usbMode() const {
LockGuard guard(command_lock_);
return usb_mode_;
}
bool GatewayRuntime::setUsbMode(GatewayUsbMode mode) {
if (mode > GatewayUsbMode::kTridonic) {
return false;
}
{
LockGuard guard(command_lock_);
if (usb_mode_ == mode) {
return true;
}
}
if (!settings_.setUsbMode(mode)) {
return false;
}
LockGuard guard(command_lock_);
usb_mode_ = mode;
return true;
}
uint8_t GatewayRuntime::usbChannelIndex() const {
LockGuard guard(command_lock_);
return usb_channel_index_;
}
bool GatewayRuntime::setUsbChannelIndex(uint8_t channel_index) {
if (channel_index > 15) {
return false;
}
if (dali_domain_ == nullptr) {
return false;
}
const auto channels = dali_domain_->channelInfo();
if (!std::any_of(channels.begin(), channels.end(),
[channel_index](const DaliChannelInfo& channel) {
return channel.channel_index == channel_index;
})) {
return false;
}
{
LockGuard guard(command_lock_);
if (usb_channel_index_ == channel_index) {
return true;
}
}
if (!settings_.setUsbChannelIndex(channel_index)) {
return false;
}
LockGuard guard(command_lock_);
usb_channel_index_ = channel_index;
return true;
}
void GatewayRuntime::setCommandAddressResolver(
std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> resolver) {
LockGuard guard(command_lock_);
@@ -5,6 +5,7 @@
#include "tinyusb.h"
#include "tinyusb_default_config.h"
#include <algorithm>
#include <array>
namespace gateway {
@@ -57,6 +58,16 @@ GatewayTridonicHid::GatewayTridonicHid(DaliDomainService& dali_domain, GatewayTr
esp_err_t GatewayTridonicHid::start() {
if (started_ || !config_.enabled) return ESP_OK;
const auto configured_channels = dali_domain_.channelInfo();
const auto channel = std::find_if(
configured_channels.begin(), configured_channels.end(), [this](const auto& value) {
return value.channel_index == config_.channel_index;
});
if (channel == configured_channels.end()) {
ESP_LOGE(kTag, "Tridonic HID channel %u is not configured; refusing to start",
config_.channel_index);
return ESP_ERR_INVALID_ARG;
}
tinyusb_config_t usb_config = TINYUSB_DEFAULT_CONFIG();
usb_config.descriptor.device = &kDeviceDescriptor;
usb_config.descriptor.full_speed_config = kConfigurationDescriptor;
@@ -34,6 +34,16 @@ esp_err_t GatewayUsbSetupBridge::start() {
ESP_LOGI(kTag, "USB Serial/JTAG setup bridge disabled; USB remains available for debug");
return ESP_OK;
}
const auto configured_channels = dali_domain_.channelInfo();
const auto channel = std::find_if(
configured_channels.begin(), configured_channels.end(), [this](const auto& value) {
return value.channel_index == config_.channel_index;
});
if (channel == configured_channels.end()) {
ESP_LOGE(kTag, "USB setup channel %u is not configured; refusing to start",
config_.channel_index);
return ESP_ERR_INVALID_ARG;
}
if (!usb_serial_jtag_is_driver_installed()) {
usb_serial_jtag_driver_config_t driver_config = {};