feat: Add DALI raw frame handling and USB setup bridge

- Introduced DaliRawFrame structure to encapsulate raw frame data.
- Enhanced DaliDomainService to manage raw frame sinks and processing.
- Implemented raw frame task for asynchronous handling of incoming DALI frames.
- Integrated raw frame handling in GatewayBleBridge and GatewayNetworkService.
- Added GatewayUsbSetupBridge to facilitate USB Serial/JTAG communication with DALI.
- Configured ESP-NOW for wireless communication and setup management.
- Updated GatewayRuntime to support clearing wireless credentials on boot button long press.
- Enhanced CMakeLists to include new components and dependencies.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Tony
2026-04-30 04:15:05 +08:00
parent 3d8d00c3dd
commit 4ce3513dd2
20 changed files with 984 additions and 25 deletions
+7 -6
View File
@@ -5,16 +5,17 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
## Layout
- `apps/`: standard ESP-IDF applications for each firmware role.
- `apps/gateway/main/Kconfig.projbuild`: project-visible gateway-role settings such as per-channel native/serial PHY selection, gateway ids, and pin mapping.
- `apps/gateway/main/Kconfig.projbuild`: project-visible gateway-role settings such as per-channel native/serial PHY selection, gateway ids, pin mapping, and startup transport policy.
- `components/`: reusable components shared by all gateway applications.
- `gateway_core/`: boot profile and top-level role bootstrap.
- `dali/`: vendored ESP-IDF DALI HAL/backend reused from LuatOS.
- `dali_domain/`: native DALI domain facade over `dali_cpp`.
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`.
- `dali/`: vendored ESP-IDF DALI HAL/backend reused from LuatOS, including native raw receive fan-out.
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications.
- `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out.
- `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, and setup AP mode for the native gateway.
- `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset for the native gateway.
- `gateway_runtime/`: persistent runtime state, command queueing, and device info services.
- `gateway_usb_setup/`: optional USB Serial/JTAG setup bridge; disabled by default so USB remains available for debug at boot.
## 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, 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 plus incoming `FFF1`/`FFF2`/`FFF3` writes into the native controller and DALI domain, and a `gateway_network` service that starts the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, Wi-Fi STA startup from persisted credentials, and the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`. 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, and 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`, Wi-Fi STA lifecycle, 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, and BOOT-button Wi-Fi credential clearing. Startup behavior is configured in `main/Kconfig.projbuild`: BLE is enabled by default, Wi-Fi STA and ESP-NOW setup mode are disabled by default, and the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected. 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.
+1 -1
View File
@@ -1,6 +1,6 @@
idf_component_register(
SRCS "app_main.cpp"
REQUIRES gateway_core gateway_controller gateway_network dali_domain gateway_runtime gateway_ble log
REQUIRES gateway_core gateway_controller gateway_network dali_domain gateway_runtime gateway_ble gateway_usb_setup log
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
+102
View File
@@ -258,6 +258,90 @@ config GATEWAY_DALI_BAUDRATE
help
Runtime baudrate used when initializing the local DALI bus.
menu "Gateway Startup Services"
config GATEWAY_BLE_SUPPORTED
bool "BLE gateway transport is supported"
default y
help
Builds and starts the BLE gateway bridge. Runtime BLE enable state is controlled separately.
config GATEWAY_START_BLE_ENABLED
bool "Enable BLE at startup"
depends on GATEWAY_BLE_SUPPORTED
default y
help
Default runtime BLE state when no persisted BLE setting exists. Lua gateway behavior starts BLE by default.
config GATEWAY_WIFI_SUPPORTED
bool "Wi-Fi gateway transport is supported"
default y
help
Keeps Wi-Fi control, HTTP/UDP networking, setup AP, and ESP-NOW setup paths available.
config GATEWAY_START_WIFI_STA_ENABLED
bool "Start Wi-Fi STA at startup"
depends on GATEWAY_WIFI_SUPPORTED
default n
help
Connect to persisted Wi-Fi credentials at boot. Disabled by default so wireless stays off until commanded.
config GATEWAY_ESPNOW_SETUP_SUPPORTED
bool "ESP-NOW setup transport is supported"
depends on GATEWAY_WIFI_SUPPORTED
default y
help
Enables ESP-NOW setup ingress when setup AP mode is entered.
config GATEWAY_START_ESPNOW_SETUP_ENABLED
bool "Enter ESP-NOW setup mode at startup"
depends on GATEWAY_ESPNOW_SETUP_SUPPORTED
default n
help
Starts the setup AP and ESP-NOW setup ingress immediately at boot. Disabled by default.
choice GATEWAY_USB_STARTUP_MODE
prompt "USB Serial/JTAG startup mode"
default GATEWAY_USB_STARTUP_DEBUG_JTAG
help
Select whether the built-in USB Serial/JTAG interface remains available for debug or is claimed by the setup bridge.
config GATEWAY_USB_STARTUP_DEBUG_JTAG
bool "USB Serial/JTAG debug interface"
config GATEWAY_USB_STARTUP_SETUP_SERIAL
bool "USB Serial/JTAG setup bridge"
endchoice
config GATEWAY_USB_SETUP_CHANNEL_INDEX
int "USB setup DALI channel index"
depends on GATEWAY_USB_STARTUP_SETUP_SERIAL
range 0 1
default 0
help
Native zero-based DALI channel used for short raw USB setup frames on the single USB stream.
config GATEWAY_USB_SETUP_RX_BUFFER
int "USB setup RX buffer bytes"
depends on GATEWAY_USB_STARTUP_SETUP_SERIAL
range 64 4096
default 256
config GATEWAY_USB_SETUP_TX_BUFFER
int "USB setup TX buffer bytes"
depends on GATEWAY_USB_STARTUP_SETUP_SERIAL
range 64 4096
default 256
config GATEWAY_USB_SETUP_READ_TIMEOUT_MS
int "USB setup read timeout ms"
depends on GATEWAY_USB_STARTUP_SETUP_SERIAL
range 1 1000
default 20
endmenu
menu "Gateway Network Services"
config GATEWAY_NETWORK_HTTP_ENABLED
@@ -296,6 +380,24 @@ config GATEWAY_STATUS_LED_ACTIVE_HIGH
depends on GATEWAY_STATUS_LED_GPIO >= 0
default y
config GATEWAY_BOOT_BUTTON_GPIO
int "BOOT button GPIO"
range -1 48
default 0
help
GPIO used for Lua-compatible setup entry and Wi-Fi credential clearing. Set to -1 to disable.
config GATEWAY_BOOT_BUTTON_ACTIVE_LOW
bool "BOOT button is active low"
depends on GATEWAY_BOOT_BUTTON_GPIO >= 0
default y
config GATEWAY_BOOT_BUTTON_LONG_PRESS_MS
int "BOOT button long press ms"
depends on GATEWAY_BOOT_BUTTON_GPIO >= 0
range 500 10000
default 3000
endmenu
endmenu
+98 -6
View File
@@ -4,6 +4,7 @@
#include "gateway_core.hpp"
#include "gateway_network.hpp"
#include "gateway_runtime.hpp"
#include "gateway_usb_setup.hpp"
#include "esp_log.h"
#include "sdkconfig.h"
@@ -28,16 +29,83 @@
#define CONFIG_GATEWAY_STATUS_LED_GPIO -1
#endif
#ifndef CONFIG_GATEWAY_BOOT_BUTTON_GPIO
#define CONFIG_GATEWAY_BOOT_BUTTON_GPIO -1
#endif
#ifndef CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS
#define CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS 3000
#endif
#ifndef CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX
#define CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX 0
#endif
#ifndef CONFIG_GATEWAY_USB_SETUP_RX_BUFFER
#define CONFIG_GATEWAY_USB_SETUP_RX_BUFFER 256
#endif
#ifndef CONFIG_GATEWAY_USB_SETUP_TX_BUFFER
#define CONFIG_GATEWAY_USB_SETUP_TX_BUFFER 256
#endif
#ifndef CONFIG_GATEWAY_USB_SETUP_READ_TIMEOUT_MS
#define CONFIG_GATEWAY_USB_SETUP_READ_TIMEOUT_MS 20
#endif
namespace {
constexpr const char* kProjectName = "DALI_485_Gateway";
constexpr const char* kProjectVersion = "0.1.0";
constexpr const char* kTag = "gateway_main";
#ifdef CONFIG_GATEWAY_WIFI_SUPPORTED
constexpr bool kWifiSupported = true;
#else
constexpr bool kWifiSupported = false;
#endif
#ifdef CONFIG_GATEWAY_START_WIFI_STA_ENABLED
constexpr bool kWifiStartupEnabled = true;
#else
constexpr bool kWifiStartupEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_BLE_SUPPORTED
constexpr bool kBleSupported = true;
#else
constexpr bool kBleSupported = false;
#endif
#ifdef CONFIG_GATEWAY_START_BLE_ENABLED
constexpr bool kBleStartupEnabled = true;
#else
constexpr bool kBleStartupEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_ESPNOW_SETUP_SUPPORTED
constexpr bool kEspnowSetupSupported = true;
#else
constexpr bool kEspnowSetupSupported = false;
#endif
#ifdef CONFIG_GATEWAY_START_ESPNOW_SETUP_ENABLED
constexpr bool kEspnowSetupStartupEnabled = true;
#else
constexpr bool kEspnowSetupStartupEnabled = false;
#endif
#ifdef CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL
constexpr bool kUsbSetupStartupEnabled = true;
#else
constexpr bool kUsbSetupStartupEnabled = false;
#endif
std::unique_ptr<gateway::DaliDomainService> s_dali_domain;
std::unique_ptr<gateway::GatewayRuntime> s_runtime;
std::unique_ptr<gateway::GatewayController> s_controller;
std::unique_ptr<gateway::GatewayNetworkService> s_network;
std::unique_ptr<gateway::GatewayBleBridge> s_ble_bridge;
std::unique_ptr<gateway::GatewayUsbSetupBridge> s_usb_setup_bridge;
[[maybe_unused]] void LogBindError(const char* channel_name, esp_err_t err) {
if (err != ESP_OK) {
@@ -239,11 +307,11 @@ extern "C" void app_main(void) {
const gateway::BootProfile profile{
gateway::AppRole::kGateway,
"gateway",
kWifiSupported,
kBleSupported,
true,
true,
true,
true,
true,
kEspnowSetupSupported,
kUsbSetupStartupEnabled,
};
gateway::GatewayCore core(profile);
@@ -258,6 +326,7 @@ extern "C" void app_main(void) {
kProjectName,
kProjectVersion,
gateway::ReadRuntimeSerialId(),
kBleStartupEnabled,
},
s_dali_domain.get());
ESP_ERROR_CHECK(s_runtime->start());
@@ -278,7 +347,10 @@ extern "C" void app_main(void) {
if (profile.enable_wifi || profile.enable_eth) {
gateway::GatewayNetworkServiceConfig network_config;
network_config.wifi_enabled = profile.enable_wifi;
network_config.wifi_enabled = profile.enable_wifi && kWifiStartupEnabled;
network_config.espnow_setup_enabled = profile.enable_espnow;
network_config.espnow_setup_startup_enabled =
profile.enable_espnow && kEspnowSetupStartupEnabled;
#ifdef CONFIG_GATEWAY_NETWORK_HTTP_ENABLED
network_config.http_enabled = true;
#else
@@ -292,13 +364,20 @@ extern "C" void app_main(void) {
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.status_led_gpio = CONFIG_GATEWAY_STATUS_LED_GPIO;
network_config.boot_button_gpio = CONFIG_GATEWAY_BOOT_BUTTON_GPIO;
network_config.boot_button_long_press_ms = CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS;
#ifdef CONFIG_GATEWAY_STATUS_LED_ACTIVE_HIGH
network_config.status_led_active_high = true;
#else
network_config.status_led_active_high = false;
#endif
#ifdef CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW
network_config.boot_button_active_low = true;
#else
network_config.boot_button_active_low = false;
#endif
s_network = std::make_unique<gateway::GatewayNetworkService>(*s_controller, *s_runtime,
network_config);
*s_dali_domain, network_config);
ESP_ERROR_CHECK(s_network->start());
}
@@ -308,6 +387,19 @@ extern "C" void app_main(void) {
ESP_ERROR_CHECK(s_ble_bridge->start());
}
if (profile.enable_usb) {
gateway::GatewayUsbSetupBridgeConfig usb_config;
usb_config.enabled = true;
usb_config.channel_index = static_cast<uint8_t>(CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX);
usb_config.rx_buffer_size = static_cast<size_t>(CONFIG_GATEWAY_USB_SETUP_RX_BUFFER);
usb_config.tx_buffer_size = static_cast<size_t>(CONFIG_GATEWAY_USB_SETUP_TX_BUFFER);
usb_config.read_timeout_ms = static_cast<uint32_t>(CONFIG_GATEWAY_USB_SETUP_READ_TIMEOUT_MS);
s_usb_setup_bridge = std::make_unique<gateway::GatewayUsbSetupBridge>(*s_controller,
*s_dali_domain,
usb_config);
ESP_ERROR_CHECK(s_usb_setup_bridge->start());
}
const auto device_info = s_runtime->deviceInfo();
std::printf("gateway_main: dali domain implementation=%s bound=%d channels=%u\n",
s_dali_domain->implementationName(), s_dali_domain->isBound(),
+16
View File
@@ -620,6 +620,19 @@ CONFIG_GATEWAY_CHANNEL2_PHY_DISABLED=y
# CONFIG_GATEWAY_ENABLE_DALI_BUS is not set
#
# Gateway Startup Services
#
CONFIG_GATEWAY_BLE_SUPPORTED=y
CONFIG_GATEWAY_START_BLE_ENABLED=y
CONFIG_GATEWAY_WIFI_SUPPORTED=y
# CONFIG_GATEWAY_START_WIFI_STA_ENABLED is not set
CONFIG_GATEWAY_ESPNOW_SETUP_SUPPORTED=y
# CONFIG_GATEWAY_START_ESPNOW_SETUP_ENABLED is not set
CONFIG_GATEWAY_USB_STARTUP_DEBUG_JTAG=y
# CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set
# end of Gateway Startup Services
#
# Gateway Network Services
#
@@ -628,6 +641,9 @@ CONFIG_GATEWAY_NETWORK_HTTP_PORT=80
CONFIG_GATEWAY_NETWORK_UDP_ROUTER_ENABLED=y
CONFIG_GATEWAY_NETWORK_UDP_PORT=2020
CONFIG_GATEWAY_STATUS_LED_GPIO=-1
CONFIG_GATEWAY_BOOT_BUTTON_GPIO=0
CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW=y
CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS=3000
# end of Gateway Network Services
# end of Gateway App
+25 -6
View File
@@ -213,6 +213,7 @@ QueueHandle_t dali_send_queue; // [Dali_msg_t] from other tasks to dali t
QueueHandle_t dali_send_reply_queue; // [Dali_msg_t] from dali task to other tasks
QueueHandle_t dali_receive_queue; // alias to bus 0 for compatibility
QueueHandle_t dali_receive_queues[DALI_PHY_COUNT];
QueueHandle_t dali_raw_receive_queue; // [Dali_msg_t] non-consuming receive fan-out
// internal queues for debug data
struct Dali_rx_dbg_data {
@@ -250,6 +251,19 @@ static inline bool bus_valid(uint8_t bus_id) {
return bus_id < DALI_PHY_COUNT && s_bus[bus_id].inited;
}
static inline void publish_rx_frame_from_isr(Dali_msg_t *msg, QueueHandle_t queue, BaseType_t *yield)
{
if (msg == NULL) {
return;
}
if (queue) {
xQueueSendToBackFromISR(queue, msg, yield);
}
if (dali_raw_receive_queue) {
xQueueSendToBackFromISR(dali_raw_receive_queue, msg, yield);
}
}
// GPIO ISR handler
// define rx_gpio_isr_handler on any edge
static void IRAM_ATTR rx_gpio_isr_handler(void* arg)
@@ -524,9 +538,7 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
// rx_data.status = DALI_FRAME_ERROR; // should be set inside ISR
bus->rx_data.length = bus->rx_data_bit_counter; // set length of data
// rx_data.data[0] = 0xAA; // debug
if (bus->rx_queue) {
xQueueSendToBackFromISR(bus->rx_queue, &bus->rx_data, &yield); // send data to queue
}
publish_rx_frame_from_isr(&bus->rx_data, bus->rx_queue, &yield); // send data to queue
}
}
else if(bus->rx_state == RX_STATE_DATA || bus->rx_state == RX_STATE_STOP) {
@@ -537,9 +549,7 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
bus->rx_data.status = DALI_FRAME_OK; // frame is OK
bus->rx_data.length = bus->rx_data_bit_counter; // set length of data
// rx_data.data[0] = 0xBB; // debug
if (bus->rx_queue) {
xQueueSendToBackFromISR(bus->rx_queue, &bus->rx_data, &yield); // send data to queue
}
publish_rx_frame_from_isr(&bus->rx_data, bus->rx_queue, &yield); // send data to queue
}
}
}
@@ -617,6 +627,9 @@ static void ensure_common_queues(void)
if (!rx_dbg_queue) {
rx_dbg_queue = xQueueCreate(CONFIG_DALI_DEBUG_QUEUE_LEN, sizeof(Dali_rx_dbg_data_t));
}
if (!dali_raw_receive_queue) {
dali_raw_receive_queue = xQueueCreate(CONFIG_DALI_RX_QUEUE_LEN, sizeof(Dali_msg_t));
}
#if CONFIG_DALI_ENABLE_DEBUG_TASK
if (!s_debug_task_created) {
xTaskCreate(debug_task, "debug_task", CONFIG_DALI_DEBUG_TASK_STACK_SIZE, NULL, CONFIG_DALI_DEBUG_TASK_PRIORITY, NULL); // at low priority !!!
@@ -931,4 +944,10 @@ esp_err_t dali_hal_get_bus_info(uint8_t bus_id, dali_hal_bus_info_t *info)
return ESP_OK;
}
QueueHandle_t dali_hal_raw_receive_queue(void)
{
ensure_common_queues();
return dali_raw_receive_queue;
}
#endif // CONFIG_IDF_TARGET
+2
View File
@@ -204,6 +204,7 @@ extern QueueHandle_t dali_send_queue;
extern QueueHandle_t dali_send_reply_queue;
extern QueueHandle_t dali_receive_queue;
extern QueueHandle_t dali_receive_queues[DALI_PHY_COUNT];
extern QueueHandle_t dali_raw_receive_queue;
extern uint8_t rx_debug_enabled; // 1 - enable debug for received messages, timing, etc
@@ -224,5 +225,6 @@ esp_err_t dali_hal_set_baudrate(uint32_t baudrate);
uint32_t dali_hal_get_baudrate(void);
size_t dali_hal_get_inited_buses(uint8_t *ids, size_t max_ids);
esp_err_t dali_hal_get_bus_info(uint8_t bus_id, dali_hal_bus_info_t *info);
QueueHandle_t dali_hal_raw_receive_queue(void);
void dali_task(void *pvParameters);
@@ -10,6 +10,9 @@
#include <vector>
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
class Dali;
class DaliComm;
@@ -65,6 +68,13 @@ struct DaliChannelInfo {
std::string name;
};
struct DaliRawFrame {
uint8_t channel_index{0};
uint8_t gateway_id{0};
DaliPhyKind phy_kind{DaliPhyKind::kCustom};
std::vector<uint8_t> data;
};
class DaliDomainService {
public:
DaliDomainService();
@@ -78,6 +88,7 @@ class DaliDomainService {
const char* implementationName() const;
size_t channelCount() const;
std::vector<DaliChannelInfo> channelInfo() const;
void addRawFrameSink(std::function<void(const DaliRawFrame& frame)> sink);
bool resetBus(uint8_t gateway_id) const;
bool writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const;
@@ -108,9 +119,17 @@ class DaliDomainService {
DaliChannel* findChannelByGateway(uint8_t gateway_id);
const DaliChannel* findChannelByGateway(uint8_t gateway_id) const;
DaliChannel* findChannelByIndex(uint8_t channel_index);
const DaliChannel* findChannelByHardwareBus(uint8_t bus_id) const;
bool hasSerialPort(int uart_port) const;
esp_err_t startRawFrameTask();
static void RawFrameTaskEntry(void* arg);
void rawFrameTaskLoop();
void notifyRawFrameSinks(const DaliRawFrame& frame);
std::vector<std::unique_ptr<DaliChannel>> channels_;
std::vector<std::function<void(const DaliRawFrame& frame)>> raw_frame_sinks_;
SemaphoreHandle_t raw_frame_sink_lock_{nullptr};
TaskHandle_t raw_frame_task_handle_{nullptr};
};
} // namespace gateway
+94 -1
View File
@@ -112,7 +112,8 @@ struct DaliDomainService::DaliChannel {
std::optional<DaliSerialBusConfig> serial_bus;
};
DaliDomainService::DaliDomainService() = default;
DaliDomainService::DaliDomainService()
: raw_frame_sink_lock_(xSemaphoreCreateMutex()) {}
DaliDomainService::~DaliDomainService() = default;
@@ -172,6 +173,10 @@ esp_err_t DaliDomainService::bindHardwareBus(const DaliHardwareBusConfig& config
channel->phy_kind = DaliPhyKind::kNativeHardware;
channel->hardware_bus = config;
}
err = startRawFrameTask();
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to start raw frame task: %s", esp_err_to_name(err));
}
ESP_LOGI(kTag, "bound channel=%u gateway=%u hardware bus=%u tx=%u rx=%u baudrate=%lu",
config.channel_index, config.gateway_id, config.bus_id, config.tx_pin, config.rx_pin,
static_cast<unsigned long>(config.baudrate));
@@ -290,6 +295,19 @@ std::vector<DaliChannelInfo> DaliDomainService::channelInfo() const {
return info;
}
void DaliDomainService::addRawFrameSink(std::function<void(const DaliRawFrame& frame)> sink) {
if (!sink) {
return;
}
if (raw_frame_sink_lock_ != nullptr) {
xSemaphoreTake(raw_frame_sink_lock_, portMAX_DELAY);
}
raw_frame_sinks_.push_back(std::move(sink));
if (raw_frame_sink_lock_ != nullptr) {
xSemaphoreGive(raw_frame_sink_lock_);
}
}
bool DaliDomainService::resetBus(uint8_t gateway_id) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->comm != nullptr && channel->comm->resetBus();
@@ -446,6 +464,81 @@ DaliDomainService::DaliChannel* DaliDomainService::findChannelByIndex(uint8_t ch
return it == channels_.end() ? nullptr : it->get();
}
const DaliDomainService::DaliChannel* DaliDomainService::findChannelByHardwareBus(
uint8_t bus_id) const {
const auto it = std::find_if(channels_.begin(), channels_.end(), [bus_id](const auto& channel) {
return channel->hardware_bus.has_value() && channel->hardware_bus->bus_id == bus_id;
});
return it == channels_.end() ? nullptr : it->get();
}
esp_err_t DaliDomainService::startRawFrameTask() {
if (raw_frame_task_handle_ != nullptr) {
return ESP_OK;
}
QueueHandle_t queue = dali_hal_raw_receive_queue();
if (queue == nullptr) {
return ESP_ERR_INVALID_STATE;
}
const BaseType_t created = xTaskCreate(&DaliDomainService::RawFrameTaskEntry,
"dali_raw_rx", 4096, this, 4,
&raw_frame_task_handle_);
if (created != pdPASS) {
raw_frame_task_handle_ = nullptr;
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
void DaliDomainService::RawFrameTaskEntry(void* arg) {
static_cast<DaliDomainService*>(arg)->rawFrameTaskLoop();
}
void DaliDomainService::rawFrameTaskLoop() {
QueueHandle_t queue = dali_hal_raw_receive_queue();
Dali_msg_t message = {};
while (true) {
if (queue == nullptr) {
vTaskDelay(pdMS_TO_TICKS(100));
queue = dali_hal_raw_receive_queue();
continue;
}
if (xQueueReceive(queue, &message, portMAX_DELAY) != pdTRUE) {
continue;
}
if (message.status != DALI_FRAME_OK) {
continue;
}
const auto* channel = findChannelByHardwareBus(message.id);
if (channel == nullptr) {
continue;
}
size_t byte_count = (static_cast<size_t>(message.length) + 7U) / 8U;
if (byte_count > DALI_MAX_BYTES) {
byte_count = DALI_MAX_BYTES;
}
DaliRawFrame frame;
frame.channel_index = channel->config.channel_index;
frame.gateway_id = channel->config.gateway_id;
frame.phy_kind = channel->phy_kind;
frame.data.assign(message.data, message.data + byte_count);
notifyRawFrameSinks(frame);
}
}
void DaliDomainService::notifyRawFrameSinks(const DaliRawFrame& frame) {
if (raw_frame_sink_lock_ != nullptr) {
xSemaphoreTake(raw_frame_sink_lock_, portMAX_DELAY);
}
auto sinks = raw_frame_sinks_;
if (raw_frame_sink_lock_ != nullptr) {
xSemaphoreGive(raw_frame_sink_lock_);
}
for (const auto& sink : sinks) {
sink(frame);
}
}
bool DaliDomainService::hasSerialPort(int uart_port) const {
return std::any_of(channels_.begin(), channels_.end(), [uart_port](const auto& channel) {
return channel->serial_bus.has_value() && channel->serial_bus->uart_port == uart_port;
@@ -14,6 +14,7 @@ struct ble_gatt_access_ctxt;
namespace gateway {
class DaliDomainService;
struct DaliRawFrame;
class GatewayController;
class GatewayRuntime;
@@ -38,6 +39,7 @@ class GatewayBleBridge {
void stopAdvertising();
void notifyCharacteristic(size_t index, const std::vector<uint8_t>& payload);
void handleGatewayNotification(const std::vector<uint8_t>& frame);
void handleDaliRawFrame(const DaliRawFrame& frame);
void handleRawWrite(size_t channel_index, const std::vector<uint8_t>& payload);
void handleGatewayWrite(const std::vector<uint8_t>& payload);
std::string resolvedDeviceName() const;
@@ -177,6 +177,7 @@ esp_err_t GatewayBleBridge::start() {
[this](const std::vector<uint8_t>& frame) { handleGatewayNotification(frame); });
controller_.addBleStateSink([this](bool enabled) { setEnabled(enabled); });
controller_.addGatewayNameSink([this](uint8_t) { refreshDeviceName(); });
dali_domain_.addRawFrameSink([this](const DaliRawFrame& frame) { handleDaliRawFrame(frame); });
const esp_err_t err = initNimble();
if (err != ESP_OK) {
@@ -372,6 +373,13 @@ void GatewayBleBridge::handleGatewayNotification(const std::vector<uint8_t>& fra
last_notify_at_us_ = now;
}
void GatewayBleBridge::handleDaliRawFrame(const DaliRawFrame& frame) {
if (!enabled_ || conn_handle_ == kInvalidConnectionHandle || frame.data.empty()) {
return;
}
notifyCharacteristic(frame.channel_index, frame.data);
}
void GatewayBleBridge::handleRawWrite(size_t channel_index, const std::vector<uint8_t>& payload) {
const auto channels = dali_domain_.channelInfo();
const auto channel_it = std::find_if(channels.begin(), channels.end(),
@@ -320,6 +320,9 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
if (config_.ble_supported) {
feature |= 0x02;
}
if (config_.wifi_supported) {
feature |= 0x04;
}
if (config_.ip_router_supported && ip_router_enabled_) {
feature |= 0x08;
}
+1 -1
View File
@@ -1,7 +1,7 @@
idf_component_register(
SRCS "src/gateway_network.cpp"
INCLUDE_DIRS "include"
REQUIRES esp_event esp_http_server esp_netif esp_wifi freertos gateway_controller gateway_runtime log lwip espressif__cjson
REQUIRES dali_domain esp_event esp_http_server esp_netif esp_wifi freertos gateway_controller gateway_runtime log lwip espressif__cjson
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>
@@ -8,6 +9,7 @@
#include "esp_event.h"
#include "esp_http_server.h"
#include "esp_netif.h"
#include "esp_now.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
@@ -16,16 +18,25 @@
namespace gateway {
class GatewayController;
class DaliDomainService;
struct DaliRawFrame;
class GatewayRuntime;
struct GatewayNetworkServiceConfig {
bool wifi_enabled{true};
bool espnow_setup_enabled{true};
bool espnow_setup_startup_enabled{false};
bool http_enabled{true};
bool udp_enabled{true};
uint16_t http_port{80};
uint16_t udp_port{2020};
int status_led_gpio{-1};
bool status_led_active_high{true};
int boot_button_gpio{-1};
bool boot_button_active_low{true};
uint32_t boot_button_long_press_ms{3000};
uint32_t boot_button_task_stack_size{2048};
UBaseType_t boot_button_task_priority{2};
uint32_t udp_task_stack_size{4096};
UBaseType_t udp_task_priority{4};
};
@@ -33,12 +44,13 @@ struct GatewayNetworkServiceConfig {
class GatewayNetworkService {
public:
GatewayNetworkService(GatewayController& controller, GatewayRuntime& runtime,
GatewayNetworkServiceConfig config = {});
DaliDomainService& dali_domain, GatewayNetworkServiceConfig config = {});
esp_err_t start();
private:
static void UdpTaskEntry(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);
static esp_err_t HandleCommandPost(httpd_req_t* req);
@@ -47,17 +59,29 @@ class GatewayNetworkService {
static esp_err_t HandleJqJsGet(httpd_req_t* req);
static void HandleWifiEvent(void* arg, esp_event_base_t event_base, int32_t event_id,
void* event_data);
static void HandleEspNowReceive(const esp_now_recv_info_t* info, const uint8_t* data,
int data_len);
esp_err_t ensureNetworkStack();
esp_err_t startWifi();
esp_err_t startSetupAp();
esp_err_t startEspNow();
void stopEspNow();
esp_err_t addEspNowPeer(const uint8_t* mac, bool broadcast = false);
esp_err_t sendEspNowJson(const uint8_t* mac, const std::string& payload);
esp_err_t configureStatusLed();
esp_err_t startHttpServer();
esp_err_t startUdpTask();
esp_err_t configureBootButton();
esp_err_t startBootButtonTask();
void udpTaskLoop();
void bootButtonTaskLoop();
void handleGatewayNotification(const std::vector<uint8_t>& frame);
void handleWifiControl(uint8_t mode);
void handleWifiEvent(esp_event_base_t event_base, int32_t event_id, void* event_data);
void handleEspNowReceive(const esp_now_recv_info_t* info, const uint8_t* data, int data_len);
void handleSetupUartFrame(int setup_id, const std::vector<uint8_t>& frame);
void handleDaliRawFrame(const DaliRawFrame& frame);
std::string deviceInfoJson() const;
std::string deviceInfoDoubleEncodedJson() const;
std::string gatewaySnapshotJson();
@@ -65,6 +89,7 @@ class GatewayNetworkService {
GatewayController& controller_;
GatewayRuntime& runtime_;
DaliDomainService& dali_domain_;
GatewayNetworkServiceConfig config_;
bool started_{false};
httpd_handle_t http_server_{nullptr};
@@ -72,6 +97,10 @@ class GatewayNetworkService {
esp_netif_t* wifi_ap_netif_{nullptr};
bool wifi_started_{false};
bool setup_ap_started_{false};
bool espnow_started_{false};
bool espnow_connected_{false};
std::array<uint8_t, 6> espnow_peer_{};
TaskHandle_t boot_button_task_handle_{nullptr};
TaskHandle_t udp_task_handle_{nullptr};
int udp_socket_{-1};
SemaphoreHandle_t udp_lock_{nullptr};
@@ -1,5 +1,6 @@
#include "gateway_network.hpp"
#include "dali_domain.hpp"
#include "gateway_controller.hpp"
#include "gateway_runtime.hpp"
@@ -9,6 +10,7 @@
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_netif_ip_addr.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "lwip/inet.h"
@@ -26,6 +28,9 @@ namespace {
constexpr const char* kTag = "gateway_network";
constexpr const char* kSetupApSsid = "LAMMIN_Gateway";
constexpr size_t kUdpBufferSize = 256;
constexpr uint8_t kEspNowBroadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
GatewayNetworkService* s_espnow_service = nullptr;
class LockGuard {
public:
@@ -86,6 +91,43 @@ std::string MacToHex(const uint8_t mac[6]) {
return std::string(out);
}
std::string LocalMacHex(wifi_interface_t interface) {
uint8_t mac[6] = {};
if (esp_wifi_get_mac(interface, mac) != ESP_OK) {
return {};
}
return MacToHex(mac);
}
const char* JsonString(cJSON* parent, const char* name) {
cJSON* item = cJSON_GetObjectItem(parent, name);
return cJSON_IsString(item) ? item->valuestring : nullptr;
}
std::vector<uint8_t> BytesFromJsonString(const char* value) {
if (value == nullptr) {
return {};
}
std::string_view text(value);
auto decoded = DecodeHex(text);
if (!decoded.empty() || text.empty()) {
return decoded;
}
return std::vector<uint8_t>(text.begin(), text.end());
}
std::string BytesToHex(const std::vector<uint8_t>& bytes) {
static constexpr char kHex[] = "0123456789ABCDEF";
std::string out;
out.reserve(bytes.size() * 2);
for (uint8_t byte : bytes) {
out.push_back(kHex[(byte >> 4) & 0x0F]);
out.push_back(kHex[byte & 0x0F]);
}
return out;
}
std::string PrintJson(cJSON* node) {
if (node == nullptr) {
return {};
@@ -132,8 +174,9 @@ esp_err_t RegisterUri(httpd_handle_t server, const char* uri, httpd_method_t met
GatewayNetworkService::GatewayNetworkService(GatewayController& controller,
GatewayRuntime& runtime,
DaliDomainService& dali_domain,
GatewayNetworkServiceConfig config)
: controller_(controller), runtime_(runtime), config_(config),
: controller_(controller), runtime_(runtime), dali_domain_(dali_domain), config_(config),
udp_lock_(xSemaphoreCreateMutex()) {}
esp_err_t GatewayNetworkService::start() {
@@ -146,7 +189,12 @@ esp_err_t GatewayNetworkService::start() {
return err;
}
if (config_.wifi_enabled) {
if (config_.espnow_setup_startup_enabled) {
err = startSetupAp();
if (err != ESP_OK) {
return err;
}
} else if (config_.wifi_enabled) {
err = startWifi();
if (err != ESP_OK) {
return err;
@@ -158,9 +206,15 @@ esp_err_t GatewayNetworkService::start() {
return err;
}
err = configureBootButton();
if (err != ESP_OK) {
return err;
}
controller_.addNotificationSink(
[this](const std::vector<uint8_t>& frame) { handleGatewayNotification(frame); });
controller_.addWifiStateSink([this](uint8_t mode) { handleWifiControl(mode); });
dali_domain_.addRawFrameSink([this](const DaliRawFrame& frame) { handleDaliRawFrame(frame); });
if (config_.http_enabled) {
err = startHttpServer();
@@ -176,6 +230,11 @@ esp_err_t GatewayNetworkService::start() {
}
}
err = startBootButtonTask();
if (err != ESP_OK) {
return err;
}
started_ = true;
ESP_LOGI(kTag, "network service started http=%d udp=%d", config_.http_enabled,
config_.udp_enabled);
@@ -202,6 +261,7 @@ esp_err_t GatewayNetworkService::startWifi() {
if (wifi_started_) {
return ESP_OK;
}
stopEspNow();
setup_ap_started_ = false;
if (wifi_sta_netif_ == nullptr) {
@@ -299,6 +359,7 @@ esp_err_t GatewayNetworkService::startSetupAp() {
}
if (wifi_started_) {
stopEspNow();
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
}
@@ -327,12 +388,106 @@ esp_err_t GatewayNetworkService::startSetupAp() {
return err;
}
if (config_.espnow_setup_enabled) {
err = startEspNow();
if (err != ESP_OK) {
ESP_LOGW(kTag, "setup AP started without ESP-NOW: %s", esp_err_to_name(err));
}
}
wifi_started_ = true;
setup_ap_started_ = true;
ESP_LOGI(kTag, "setup AP started ssid=%s ip=192.168.3.1", kSetupApSsid);
return ESP_OK;
}
esp_err_t GatewayNetworkService::startEspNow() {
if (espnow_started_) {
return ESP_OK;
}
esp_err_t err = esp_now_init();
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to init ESP-NOW: %s", esp_err_to_name(err));
return err;
}
s_espnow_service = this;
err = esp_now_register_recv_cb(&GatewayNetworkService::HandleEspNowReceive);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to register ESP-NOW RX callback: %s", esp_err_to_name(err));
esp_now_deinit();
s_espnow_service = nullptr;
return err;
}
err = addEspNowPeer(kEspNowBroadcastMac, true);
if (err != ESP_OK) {
esp_now_unregister_recv_cb();
esp_now_deinit();
s_espnow_service = nullptr;
return err;
}
espnow_connected_ = false;
espnow_peer_.fill(0);
espnow_started_ = true;
ESP_LOGI(kTag, "ESP-NOW setup ingress started local_mac=%s", LocalMacHex(WIFI_IF_AP).c_str());
return ESP_OK;
}
void GatewayNetworkService::stopEspNow() {
if (!espnow_started_) {
return;
}
esp_now_unregister_recv_cb();
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_now_deinit());
if (s_espnow_service == this) {
s_espnow_service = nullptr;
}
espnow_connected_ = false;
espnow_started_ = false;
espnow_peer_.fill(0);
}
esp_err_t GatewayNetworkService::addEspNowPeer(const uint8_t* mac, bool broadcast) {
if (mac == nullptr) {
return ESP_ERR_INVALID_ARG;
}
if (esp_now_is_peer_exist(mac)) {
return ESP_OK;
}
esp_now_peer_info_t peer = {};
std::memcpy(peer.peer_addr, mac, sizeof(peer.peer_addr));
peer.channel = 0;
peer.ifidx = setup_ap_started_ || broadcast ? WIFI_IF_AP : WIFI_IF_STA;
peer.encrypt = false;
esp_err_t err = esp_now_add_peer(&peer);
if (err == ESP_ERR_ESPNOW_EXIST) {
return ESP_OK;
}
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to add ESP-NOW peer %s: %s", MacToHex(mac).c_str(),
esp_err_to_name(err));
}
return err;
}
esp_err_t GatewayNetworkService::sendEspNowJson(const uint8_t* mac, const std::string& payload) {
if (mac == nullptr || payload.empty()) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = addEspNowPeer(mac, std::memcmp(mac, kEspNowBroadcastMac, 6) == 0);
if (err != ESP_OK) {
return err;
}
return esp_now_send(mac, reinterpret_cast<const uint8_t*>(payload.data()), payload.size());
}
esp_err_t GatewayNetworkService::configureStatusLed() {
if (config_.status_led_gpio < 0) {
return ESP_OK;
@@ -354,6 +509,25 @@ esp_err_t GatewayNetworkService::configureStatusLed() {
return ESP_OK;
}
esp_err_t GatewayNetworkService::configureBootButton() {
if (config_.boot_button_gpio < 0) {
return ESP_OK;
}
gpio_config_t io_config = {};
io_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(config_.boot_button_gpio);
io_config.mode = GPIO_MODE_INPUT;
io_config.pull_up_en = config_.boot_button_active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE;
io_config.pull_down_en = config_.boot_button_active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE;
io_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&io_config);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to configure boot button GPIO%d: %s", config_.boot_button_gpio,
esp_err_to_name(err));
}
return err;
}
esp_err_t GatewayNetworkService::startHttpServer() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = config_.http_port;
@@ -409,10 +583,31 @@ esp_err_t GatewayNetworkService::startUdpTask() {
return ESP_OK;
}
esp_err_t GatewayNetworkService::startBootButtonTask() {
if (config_.boot_button_gpio < 0 || boot_button_task_handle_ != nullptr) {
return ESP_OK;
}
const BaseType_t created =
xTaskCreate(&GatewayNetworkService::BootButtonTaskEntry, "gateway_boot_btn",
config_.boot_button_task_stack_size, this, config_.boot_button_task_priority,
&boot_button_task_handle_);
if (created != pdPASS) {
boot_button_task_handle_ = nullptr;
ESP_LOGE(kTag, "failed to create boot button task");
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
void GatewayNetworkService::UdpTaskEntry(void* arg) {
static_cast<GatewayNetworkService*>(arg)->udpTaskLoop();
}
void GatewayNetworkService::BootButtonTaskEntry(void* arg) {
static_cast<GatewayNetworkService*>(arg)->bootButtonTaskLoop();
}
void GatewayNetworkService::HandleWifiEvent(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) {
auto* service = static_cast<GatewayNetworkService*>(arg);
@@ -421,6 +616,13 @@ void GatewayNetworkService::HandleWifiEvent(void* arg, esp_event_base_t event_ba
}
}
void GatewayNetworkService::HandleEspNowReceive(const esp_now_recv_info_t* info,
const uint8_t* data, int data_len) {
if (s_espnow_service != nullptr) {
s_espnow_service->handleEspNowReceive(info, data, data_len);
}
}
void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t event_id,
void* event_data) {
if (!config_.wifi_enabled) {
@@ -459,6 +661,139 @@ void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t
}
}
void GatewayNetworkService::handleEspNowReceive(const esp_now_recv_info_t* info,
const uint8_t* data, int data_len) {
if (!espnow_started_ || info == nullptr || info->src_addr == nullptr || data == nullptr ||
data_len <= 0) {
return;
}
const std::string payload(reinterpret_cast<const char*>(data),
static_cast<size_t>(data_len));
cJSON* root = cJSON_Parse(payload.c_str());
if (root == nullptr) {
ESP_LOGW(kTag, "ignored non-JSON ESP-NOW setup packet len=%d", data_len);
return;
}
const char* type = JsonString(root, "type");
if (type == nullptr) {
cJSON_Delete(root);
return;
}
addEspNowPeer(info->src_addr);
const std::string local_mac = LocalMacHex(WIFI_IF_AP);
if (std::strcmp(type, "connReq") == 0) {
cJSON* response = cJSON_CreateObject();
if (response != nullptr) {
cJSON_AddStringToObject(response, "type", "connRsp");
cJSON_AddStringToObject(response, "data", "");
cJSON_AddStringToObject(response, "dst", MacToHex(info->src_addr).c_str());
cJSON_AddStringToObject(response, "src", local_mac.c_str());
cJSON_AddStringToObject(response, "pmk", "");
const std::string rendered = PrintJson(response);
sendEspNowJson(kEspNowBroadcastMac, rendered);
cJSON_Delete(response);
}
cJSON_Delete(root);
return;
}
if (std::strcmp(type, "connAck") == 0) {
if (!espnow_connected_) {
std::memcpy(espnow_peer_.data(), info->src_addr, espnow_peer_.size());
espnow_connected_ = true;
ESP_LOGI(kTag, "ESP-NOW setup peer connected mac=%s", MacToHex(info->src_addr).c_str());
}
cJSON_Delete(root);
return;
}
if (std::strcmp(type, "echo") == 0) {
if (espnow_connected_) {
cJSON* response = cJSON_CreateObject();
if (response != nullptr) {
cJSON_AddStringToObject(response, "type", "echoRsp");
cJSON_AddStringToObject(response, "data", "");
const std::string rendered = PrintJson(response);
sendEspNowJson(info->src_addr, rendered);
cJSON_Delete(response);
}
}
cJSON_Delete(root);
return;
}
if (std::strcmp(type, "cmd") == 0 || std::strcmp(type, "data") == 0) {
const auto frame = BytesFromJsonString(JsonString(root, "data"));
if (!frame.empty()) {
controller_.enqueueCommandFrame(frame);
}
cJSON_Delete(root);
return;
}
if (std::strcmp(type, "uart") == 0) {
cJSON* num = cJSON_GetObjectItem(root, "num");
const auto frame = BytesFromJsonString(JsonString(root, "data"));
if (cJSON_IsNumber(num) && !frame.empty()) {
handleSetupUartFrame(num->valueint, frame);
}
cJSON_Delete(root);
return;
}
cJSON_Delete(root);
}
void GatewayNetworkService::handleSetupUartFrame(int setup_id,
const std::vector<uint8_t>& frame) {
if (frame.empty()) {
return;
}
if (frame.size() >= 7) {
controller_.enqueueCommandFrame(frame);
return;
}
const uint8_t channel_index = static_cast<uint8_t>(setup_id - 3);
for (const auto& channel : dali_domain_.channelInfo()) {
if (channel.channel_index == channel_index) {
if (!dali_domain_.writeBridgeFrame(channel.gateway_id, frame.data(), frame.size())) {
ESP_LOGW(kTag, "failed to forward ESP-NOW setup UART%d frame to gateway=%u", setup_id,
channel.gateway_id);
}
return;
}
}
ESP_LOGW(kTag, "ignored setup UART%d frame for unbound DALI channel", setup_id);
}
void GatewayNetworkService::handleDaliRawFrame(const DaliRawFrame& frame) {
if (!espnow_started_ || !espnow_connected_ || frame.data.empty()) {
return;
}
cJSON* payload = cJSON_CreateObject();
if (payload == nullptr) {
return;
}
cJSON_AddStringToObject(payload, "type", "uart");
cJSON_AddNumberToObject(payload, "num", static_cast<double>(frame.channel_index + 3));
const std::string data_hex = BytesToHex(frame.data);
cJSON_AddStringToObject(payload, "data", data_hex.c_str());
const std::string rendered = PrintJson(payload);
cJSON_Delete(payload);
if (!rendered.empty()) {
sendEspNowJson(espnow_peer_.data(), rendered);
}
}
void GatewayNetworkService::udpTaskLoop() {
udp_socket_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (udp_socket_ < 0) {
@@ -504,6 +839,47 @@ void GatewayNetworkService::udpTaskLoop() {
}
}
void GatewayNetworkService::bootButtonTaskLoop() {
vTaskDelay(pdMS_TO_TICKS(2000));
const TickType_t poll_ticks = pdMS_TO_TICKS(100);
const uint32_t long_press_ms = std::max<uint32_t>(config_.boot_button_long_press_ms, 100);
auto is_pressed = [this]() {
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.boot_button_gpio));
return config_.boot_button_active_low ? level == 0 : level != 0;
};
while (true) {
if (!is_pressed()) {
vTaskDelay(poll_ticks);
continue;
}
uint32_t pressed_ms = 0;
while (is_pressed()) {
vTaskDelay(poll_ticks);
pressed_ms += 100;
}
if (pressed_ms >= long_press_ms) {
ESP_LOGW(kTag, "BOOT long press clears Wi-Fi credentials and restarts");
runtime_.clearWirelessInfo();
stopEspNow();
if (wifi_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
}
vTaskDelay(pdMS_TO_TICKS(300));
esp_restart();
} else {
ESP_LOGI(kTag, "BOOT short press enters setup AP mode");
handleWifiControl(101);
}
vTaskDelay(pdMS_TO_TICKS(300));
}
}
void GatewayNetworkService::handleGatewayNotification(const std::vector<uint8_t>& frame) {
if (!config_.udp_enabled || udp_socket_ < 0 || frame.empty()) {
return;
@@ -527,6 +903,7 @@ void GatewayNetworkService::handleGatewayNotification(const std::vector<uint8_t>
void GatewayNetworkService::handleWifiControl(uint8_t mode) {
if (mode == 0) {
config_.wifi_enabled = false;
stopEspNow();
if (wifi_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
@@ -549,6 +926,7 @@ void GatewayNetworkService::handleWifiControl(uint8_t mode) {
if (mode == 1) {
config_.wifi_enabled = true;
if (setup_ap_started_) {
stopEspNow();
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
wifi_started_ = false;
setup_ap_started_ = false;
@@ -33,6 +33,7 @@ struct GatewayRuntimeConfig {
std::string_view project_name;
std::string_view version;
std::string serial_id;
bool default_ble_enabled{true};
size_t command_queue_capacity{16};
};
@@ -60,6 +61,7 @@ class GatewaySettingsStore {
std::optional<std::string> getWifiSsid() const;
std::optional<std::string> getWifiPassword() const;
bool setWifiCredentials(std::string_view ssid, std::string_view password);
bool clearWifiCredentials();
std::string getGatewayName(uint8_t gateway_id, std::string_view fallback) const;
bool setGatewayName(uint8_t gateway_id, std::string_view name);
@@ -99,6 +101,7 @@ class GatewayRuntime {
void setGatewayCount(size_t gateway_count);
void setWirelessInfo(WirelessInfo info);
bool clearWirelessInfo();
void setCommandAddressResolver(std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> resolver);
GatewayDeviceInfo deviceInfo() const;
@@ -122,6 +122,22 @@ bool GatewaySettingsStore::setWifiCredentials(std::string_view ssid,
return writeString(kWifiSsidKey, ssid) && writeString(kWifiPasswordKey, password);
}
bool GatewaySettingsStore::clearWifiCredentials() {
if (handle_ == 0) {
return false;
}
esp_err_t ssid_err = nvs_erase_key(handle_, kWifiSsidKey);
esp_err_t password_err = nvs_erase_key(handle_, kWifiPasswordKey);
if (ssid_err == ESP_ERR_NVS_NOT_FOUND) {
ssid_err = ESP_OK;
}
if (password_err == ESP_ERR_NVS_NOT_FOUND) {
password_err = ESP_OK;
}
return ssid_err == ESP_OK && password_err == ESP_OK && nvs_commit(handle_) == ESP_OK;
}
std::string GatewaySettingsStore::getGatewayName(uint8_t gateway_id,
std::string_view fallback) const {
const auto value = readString(makeGatewayNameKey(gateway_id));
@@ -193,7 +209,7 @@ esp_err_t GatewayRuntime::start() {
return err;
}
ble_enabled_ = settings_.getBleEnabled(profile_.enable_ble);
ble_enabled_ = settings_.getBleEnabled(config_.default_ble_enabled);
if (!wireless_info_.has_value()) {
WirelessInfo info;
@@ -331,6 +347,14 @@ void GatewayRuntime::setWirelessInfo(WirelessInfo info) {
}
}
bool GatewayRuntime::clearWirelessInfo() {
{
LockGuard guard(command_lock_);
wireless_info_.reset();
}
return settings_.clearWifiCredentials();
}
void GatewayRuntime::setCommandAddressResolver(
std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> resolver) {
LockGuard guard(command_lock_);
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/gateway_usb_setup.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_domain esp_driver_usb_serial_jtag freertos gateway_controller log
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,48 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "dali_domain.hpp"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
namespace gateway {
class GatewayController;
struct GatewayUsbSetupBridgeConfig {
bool enabled{false};
uint8_t channel_index{0};
size_t rx_buffer_size{256};
size_t tx_buffer_size{256};
uint32_t read_timeout_ms{20};
uint32_t write_timeout_ms{20};
uint32_t task_stack_size{4096};
UBaseType_t task_priority{4};
};
class GatewayUsbSetupBridge {
public:
GatewayUsbSetupBridge(GatewayController& controller, DaliDomainService& dali_domain,
GatewayUsbSetupBridgeConfig config = {});
esp_err_t start();
private:
static void TaskEntry(void* arg);
void taskLoop();
void handleBytes(const uint8_t* data, size_t len);
void handleRawFrame(const DaliRawFrame& frame);
uint8_t setupGatewayId() const;
GatewayController& controller_;
DaliDomainService& dali_domain_;
GatewayUsbSetupBridgeConfig config_;
TaskHandle_t task_handle_{nullptr};
bool started_{false};
};
} // namespace gateway
@@ -0,0 +1,113 @@
#include "gateway_usb_setup.hpp"
#include "gateway_controller.hpp"
#include "driver/usb_serial_jtag.h"
#include "esp_log.h"
#include <algorithm>
namespace gateway {
namespace {
constexpr const char* kTag = "gateway_usb";
constexpr size_t kCommandFrameMinLen = 7;
}
GatewayUsbSetupBridge::GatewayUsbSetupBridge(GatewayController& controller,
DaliDomainService& dali_domain,
GatewayUsbSetupBridgeConfig config)
: controller_(controller), dali_domain_(dali_domain), config_(config) {}
esp_err_t GatewayUsbSetupBridge::start() {
if (started_) {
return ESP_OK;
}
if (!config_.enabled) {
ESP_LOGI(kTag, "USB Serial/JTAG setup bridge disabled; USB remains available for debug");
return ESP_OK;
}
if (!usb_serial_jtag_is_driver_installed()) {
usb_serial_jtag_driver_config_t driver_config = {};
driver_config.rx_buffer_size = static_cast<int>(config_.rx_buffer_size);
driver_config.tx_buffer_size = static_cast<int>(config_.tx_buffer_size);
esp_err_t err = usb_serial_jtag_driver_install(&driver_config);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to install USB Serial/JTAG driver: %s", esp_err_to_name(err));
return err;
}
}
dali_domain_.addRawFrameSink([this](const DaliRawFrame& frame) { handleRawFrame(frame); });
const BaseType_t ok = xTaskCreate(&GatewayUsbSetupBridge::TaskEntry, "gateway_usb_setup",
static_cast<uint32_t>(config_.task_stack_size), this,
config_.task_priority, &task_handle_);
if (ok != pdPASS) {
ESP_LOGE(kTag, "failed to create USB setup task");
return ESP_ERR_NO_MEM;
}
started_ = true;
ESP_LOGI(kTag, "USB Serial/JTAG setup bridge started channel=%u", config_.channel_index);
return ESP_OK;
}
void GatewayUsbSetupBridge::TaskEntry(void* arg) {
auto* self = static_cast<GatewayUsbSetupBridge*>(arg);
self->taskLoop();
}
void GatewayUsbSetupBridge::taskLoop() {
std::vector<uint8_t> buffer(std::max<size_t>(config_.rx_buffer_size, 64));
const TickType_t timeout = pdMS_TO_TICKS(config_.read_timeout_ms);
while (true) {
const int read_len = usb_serial_jtag_read_bytes(buffer.data(), buffer.size(), timeout);
if (read_len > 0) {
handleBytes(buffer.data(), static_cast<size_t>(read_len));
}
}
}
void GatewayUsbSetupBridge::handleBytes(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0) {
return;
}
if (len >= kCommandFrameMinLen) {
controller_.enqueueCommandFrame(std::vector<uint8_t>(data, data + len));
return;
}
const uint8_t gateway_id = setupGatewayId();
if (!dali_domain_.writeBridgeFrame(gateway_id, data, len)) {
ESP_LOGW(kTag, "failed to write USB raw setup frame channel=%u len=%u", config_.channel_index,
static_cast<unsigned>(len));
}
}
void GatewayUsbSetupBridge::handleRawFrame(const DaliRawFrame& frame) {
if (!config_.enabled || frame.channel_index != config_.channel_index || frame.data.empty()) {
return;
}
const int written = usb_serial_jtag_write_bytes(frame.data.data(), frame.data.size(),
pdMS_TO_TICKS(config_.write_timeout_ms));
if (written < 0 || static_cast<size_t>(written) != frame.data.size()) {
ESP_LOGW(kTag, "failed to forward USB raw setup frame channel=%u len=%u", frame.channel_index,
static_cast<unsigned>(frame.data.size()));
}
}
uint8_t GatewayUsbSetupBridge::setupGatewayId() const {
for (const auto& channel : dali_domain_.channelInfo()) {
if (channel.channel_index == config_.channel_index) {
return channel.gateway_id;
}
}
return config_.channel_index;
}
} // namespace gateway