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
+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;