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