diff --git a/README.md b/README.md index c3b8760..3329bde 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/apps/gateway/main/CMakeLists.txt b/apps/gateway/main/CMakeLists.txt index a95d1e4..63cbb34 100644 --- a/apps/gateway/main/CMakeLists.txt +++ b/apps/gateway/main/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index 6f2c814..8b970bf 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -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 \ No newline at end of file diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index f31f278..4744c0d 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -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 s_dali_domain; std::unique_ptr s_runtime; std::unique_ptr s_controller; std::unique_ptr s_network; std::unique_ptr s_ble_bridge; +std::unique_ptr 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(CONFIG_GATEWAY_NETWORK_HTTP_PORT); network_config.udp_port = static_cast(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(*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(CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX); + usb_config.rx_buffer_size = static_cast(CONFIG_GATEWAY_USB_SETUP_RX_BUFFER); + usb_config.tx_buffer_size = static_cast(CONFIG_GATEWAY_USB_SETUP_TX_BUFFER); + usb_config.read_timeout_ms = static_cast(CONFIG_GATEWAY_USB_SETUP_READ_TIMEOUT_MS); + s_usb_setup_bridge = std::make_unique(*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(), diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index f3687bb..cc5abec 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -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 diff --git a/components/dali/src/dali_hal_idf5.c b/components/dali/src/dali_hal_idf5.c index 80db335..603361b 100644 --- a/components/dali/src/dali_hal_idf5.c +++ b/components/dali/src/dali_hal_idf5.c @@ -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 diff --git a/components/dali/src/include/dali_hal.h b/components/dali/src/include/dali_hal.h index a6c3649..ba5d0c6 100644 --- a/components/dali/src/include/dali_hal.h +++ b/components/dali/src/include/dali_hal.h @@ -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); diff --git a/components/dali_domain/include/dali_domain.hpp b/components/dali_domain/include/dali_domain.hpp index 896c797..e0e81c5 100644 --- a/components/dali_domain/include/dali_domain.hpp +++ b/components/dali_domain/include/dali_domain.hpp @@ -10,6 +10,9 @@ #include #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 data; +}; + class DaliDomainService { public: DaliDomainService(); @@ -78,6 +88,7 @@ class DaliDomainService { const char* implementationName() const; size_t channelCount() const; std::vector channelInfo() const; + void addRawFrameSink(std::function 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> channels_; + std::vector> raw_frame_sinks_; + SemaphoreHandle_t raw_frame_sink_lock_{nullptr}; + TaskHandle_t raw_frame_task_handle_{nullptr}; }; } // namespace gateway \ No newline at end of file diff --git a/components/dali_domain/src/dali_domain.cpp b/components/dali_domain/src/dali_domain.cpp index c6dd05c..feef2f0 100644 --- a/components/dali_domain/src/dali_domain.cpp +++ b/components/dali_domain/src/dali_domain.cpp @@ -112,7 +112,8 @@ struct DaliDomainService::DaliChannel { std::optional 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(config.baudrate)); @@ -290,6 +295,19 @@ std::vector DaliDomainService::channelInfo() const { return info; } +void DaliDomainService::addRawFrameSink(std::function 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(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(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; diff --git a/components/gateway_ble/include/gateway_ble.hpp b/components/gateway_ble/include/gateway_ble.hpp index 4c8bf38..3084932 100644 --- a/components/gateway_ble/include/gateway_ble.hpp +++ b/components/gateway_ble/include/gateway_ble.hpp @@ -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& payload); void handleGatewayNotification(const std::vector& frame); + void handleDaliRawFrame(const DaliRawFrame& frame); void handleRawWrite(size_t channel_index, const std::vector& payload); void handleGatewayWrite(const std::vector& payload); std::string resolvedDeviceName() const; diff --git a/components/gateway_ble/src/gateway_ble.cpp b/components/gateway_ble/src/gateway_ble.cpp index 59d9ca8..1d7ecb5 100644 --- a/components/gateway_ble/src/gateway_ble.cpp +++ b/components/gateway_ble/src/gateway_ble.cpp @@ -177,6 +177,7 @@ esp_err_t GatewayBleBridge::start() { [this](const std::vector& 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& 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& payload) { const auto channels = dali_domain_.channelInfo(); const auto channel_it = std::find_if(channels.begin(), channels.end(), diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index 8898c64..2395347 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -320,6 +320,9 @@ void GatewayController::dispatchCommand(const std::vector& command) { if (config_.ble_supported) { feature |= 0x02; } + if (config_.wifi_supported) { + feature |= 0x04; + } if (config_.ip_router_supported && ip_router_enabled_) { feature |= 0x08; } diff --git a/components/gateway_network/CMakeLists.txt b/components/gateway_network/CMakeLists.txt index a87a58a..fa632ae 100644 --- a/components/gateway_network/CMakeLists.txt +++ b/components/gateway_network/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/components/gateway_network/include/gateway_network.hpp b/components/gateway_network/include/gateway_network.hpp index 4127dde..fcafaee 100644 --- a/components/gateway_network/include/gateway_network.hpp +++ b/components/gateway_network/include/gateway_network.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -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& 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& 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 espnow_peer_{}; + TaskHandle_t boot_button_task_handle_{nullptr}; TaskHandle_t udp_task_handle_{nullptr}; int udp_socket_{-1}; SemaphoreHandle_t udp_lock_{nullptr}; diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp index 17d0b2d..8a0617c 100644 --- a/components/gateway_network/src/gateway_network.cpp +++ b/components/gateway_network/src/gateway_network.cpp @@ -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 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(text.begin(), text.end()); +} + +std::string BytesToHex(const std::vector& 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& 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(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(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(arg)->udpTaskLoop(); } +void GatewayNetworkService::BootButtonTaskEntry(void* arg) { + static_cast(arg)->bootButtonTaskLoop(); +} + void GatewayNetworkService::HandleWifiEvent(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { auto* service = static_cast(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(data), + static_cast(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& frame) { + if (frame.empty()) { + return; + } + + if (frame.size() >= 7) { + controller_.enqueueCommandFrame(frame); + return; + } + + const uint8_t channel_index = static_cast(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(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(config_.boot_button_long_press_ms, 100); + + auto is_pressed = [this]() { + const int level = gpio_get_level(static_cast(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& frame) { if (!config_.udp_enabled || udp_socket_ < 0 || frame.empty()) { return; @@ -527,6 +903,7 @@ void GatewayNetworkService::handleGatewayNotification(const std::vector 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; diff --git a/components/gateway_runtime/include/gateway_runtime.hpp b/components/gateway_runtime/include/gateway_runtime.hpp index f5452e7..3898093 100644 --- a/components/gateway_runtime/include/gateway_runtime.hpp +++ b/components/gateway_runtime/include/gateway_runtime.hpp @@ -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 getWifiSsid() const; std::optional 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 resolver); GatewayDeviceInfo deviceInfo() const; diff --git a/components/gateway_runtime/src/gateway_runtime.cpp b/components/gateway_runtime/src/gateway_runtime.cpp index 6bda29c..0260897 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -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 resolver) { LockGuard guard(command_lock_); diff --git a/components/gateway_usb_setup/CMakeLists.txt b/components/gateway_usb_setup/CMakeLists.txt new file mode 100644 index 0000000..3c89b9b --- /dev/null +++ b/components/gateway_usb_setup/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/components/gateway_usb_setup/include/gateway_usb_setup.hpp b/components/gateway_usb_setup/include/gateway_usb_setup.hpp new file mode 100644 index 0000000..b0f5a25 --- /dev/null +++ b/components/gateway_usb_setup/include/gateway_usb_setup.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +#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 \ No newline at end of file diff --git a/components/gateway_usb_setup/src/gateway_usb_setup.cpp b/components/gateway_usb_setup/src/gateway_usb_setup.cpp new file mode 100644 index 0000000..e51645d --- /dev/null +++ b/components/gateway_usb_setup/src/gateway_usb_setup.cpp @@ -0,0 +1,113 @@ +#include "gateway_usb_setup.hpp" + +#include "gateway_controller.hpp" + +#include "driver/usb_serial_jtag.h" +#include "esp_log.h" + +#include + +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(config_.rx_buffer_size); + driver_config.tx_buffer_size = static_cast(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(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(arg); + self->taskLoop(); +} + +void GatewayUsbSetupBridge::taskLoop() { + std::vector buffer(std::max(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(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(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(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(written) != frame.data.size()) { + ESP_LOGW(kTag, "failed to forward USB raw setup frame channel=%u len=%u", frame.channel_index, + static_cast(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 \ No newline at end of file