#include "gateway_network.hpp" #include "dali_domain.hpp" #include "gateway_bridge.hpp" #include "gateway_controller.hpp" #include "gateway_runtime.hpp" #include "cJSON.h" #include "driver/gpio.h" #include "driver/spi_master.h" #include "esp_event.h" #include "esp_eth_driver.h" #include "esp_eth_mac_spi.h" #include "esp_log.h" #include "esp_mac.h" #include "esp_netif.h" #include "esp_netif_ip_addr.h" #include "esp_smartconfig.h" #include "esp_system.h" #include "esp_wifi.h" #include "lwip/inet.h" #include #include #include #include #include #include #include namespace gateway { 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: explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) { if (lock_ != nullptr) { xSemaphoreTake(lock_, portMAX_DELAY); } } ~LockGuard() { if (lock_ != nullptr) { xSemaphoreGive(lock_); } } private: SemaphoreHandle_t lock_; }; bool HexValue(char ch, uint8_t& value) { if (ch >= '0' && ch <= '9') { value = static_cast(ch - '0'); return true; } if (ch >= 'a' && ch <= 'f') { value = static_cast(10 + ch - 'a'); return true; } if (ch >= 'A' && ch <= 'F') { value = static_cast(10 + ch - 'A'); return true; } return false; } std::vector DecodeHex(std::string_view hex) { if ((hex.size() & 1U) != 0U) { return {}; } std::vector bytes; bytes.reserve(hex.size() / 2); for (size_t i = 0; i < hex.size(); i += 2) { uint8_t high = 0; uint8_t low = 0; if (!HexValue(hex[i], high) || !HexValue(hex[i + 1], low)) { return {}; } bytes.push_back(static_cast((high << 4) | low)); } return bytes; } std::string MacToHex(const uint8_t mac[6]) { char out[13] = {0}; std::snprintf(out, sizeof(out), "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); return std::string(out); } std::string BoundedString(const uint8_t* data, size_t len) { if (data == nullptr) { return {}; } size_t actual_len = 0; while (actual_len < len && data[actual_len] != 0) { ++actual_len; } return std::string(reinterpret_cast(data), actual_len); } 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 {}; } char* rendered = cJSON_PrintUnformatted(node); if (rendered == nullptr) { return {}; } std::string out(rendered); cJSON_free(rendered); return out; } esp_err_t ReadRequestBody(httpd_req_t* req, std::string& body) { body.clear(); if (req == nullptr) { return ESP_ERR_INVALID_ARG; } body.resize(static_cast(req->content_len)); int received = 0; while (received < req->content_len) { const int ret = httpd_req_recv(req, body.data() + received, req->content_len - received); if (ret <= 0) { return ESP_FAIL; } received += ret; } return ESP_OK; } esp_err_t RegisterUri(httpd_handle_t server, const char* uri, httpd_method_t method, esp_err_t (*handler)(httpd_req_t*), void* user_ctx) { httpd_uri_t route = {}; route.uri = uri; route.method = method; route.handler = handler; route.user_ctx = user_ctx; return httpd_register_uri_handler(server, &route); } std::string QueryValue(httpd_req_t* req, const char* key) { if (req == nullptr || key == nullptr) { return {}; } const size_t len = httpd_req_get_url_query_len(req) + 1; if (len <= 1) { return {}; } std::string query(len, '\0'); if (httpd_req_get_url_query_str(req, query.data(), query.size()) != ESP_OK) { return {}; } char value[64] = {0}; if (httpd_query_key_value(query.c_str(), key, value, sizeof(value)) != ESP_OK) { return {}; } return std::string(value); } std::string QueryString(httpd_req_t* req) { if (req == nullptr) { return {}; } const size_t len = httpd_req_get_url_query_len(req) + 1; if (len <= 1) { return {}; } std::string query(len, '\0'); if (httpd_req_get_url_query_str(req, query.data(), query.size()) != ESP_OK) { return {}; } if (!query.empty() && query.back() == '\0') { query.pop_back(); } return query; } std::optional QueryGatewayId(httpd_req_t* req) { const auto raw = QueryValue(req, "gw"); if (raw.empty()) { return std::nullopt; } char* end = nullptr; const long parsed = std::strtol(raw.c_str(), &end, 10); if (end == raw.c_str() || *end != '\0' || parsed < 0 || parsed > 255) { return std::nullopt; } return static_cast(parsed); } esp_err_t SendJsonResponse(httpd_req_t* req, const GatewayBridgeHttpResponse& response) { if (response.err == ESP_ERR_INVALID_ARG) { return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, response.body.c_str()); } if (response.err == ESP_ERR_NOT_FOUND) { return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, response.body.c_str()); } if (response.err != ESP_OK) { return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, response.body.c_str()); } httpd_resp_set_type(req, "application/json"); return httpd_resp_send(req, response.body.data(), response.body.size()); } } // namespace GatewayNetworkService::GatewayNetworkService(GatewayController& controller, GatewayRuntime& runtime, DaliDomainService& dali_domain, GatewayNetworkServiceConfig config, GatewayBridgeService* bridge_service) : controller_(controller), runtime_(runtime), dali_domain_(dali_domain), config_(config), bridge_service_(bridge_service), udp_lock_(xSemaphoreCreateMutex()) {} esp_err_t GatewayNetworkService::start() { if (started_) { return ESP_OK; } esp_err_t err = ensureNetworkStack(); if (err != ESP_OK) { return err; } if (config_.ethernet_enabled) { err = startEthernet(); if (err != ESP_OK) { if (config_.ethernet_ignore_init_failure) { ESP_LOGW(kTag, "Ethernet init failed; Ethernet is disabled for this boot: %s", esp_err_to_name(err)); config_.ethernet_enabled = false; } else { return err; } } } if (config_.espnow_setup_startup_enabled) { err = startSetupAp(); if (err != ESP_OK) { return err; } } else if (config_.smartconfig_startup_enabled) { err = startSmartconfig(); if (err != ESP_OK) { return err; } } else if (config_.wifi_enabled) { err = startWifi(); if (err != ESP_OK) { return err; } } err = configureStatusLed(); if (err != ESP_OK) { 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(); if (err != ESP_OK) { return err; } } if (config_.udp_enabled) { err = startUdpTask(); if (err != ESP_OK) { return err; } } err = startBootButtonTask(); if (err != ESP_OK) { return err; } started_ = true; ESP_LOGI(kTag, "network service started eth=%d wifi=%d http=%d udp=%d", config_.ethernet_enabled, config_.wifi_enabled, config_.http_enabled, config_.udp_enabled); return ESP_OK; } esp_err_t GatewayNetworkService::ensureNetworkStack() { esp_err_t err = esp_netif_init(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to init esp_netif: %s", esp_err_to_name(err)); return err; } err = esp_event_loop_create_default(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to create default event loop: %s", esp_err_to_name(err)); return err; } return ESP_OK; } esp_err_t GatewayNetworkService::startEthernet() { if (ethernet_started_) { return ESP_OK; } #if CONFIG_ETH_SPI_ETHERNET_W5500 if (eth_netif_ == nullptr) { esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH(); eth_netif_ = esp_netif_new(&netif_config); if (eth_netif_ == nullptr) { ESP_LOGE(kTag, "failed to create Ethernet netif"); return ESP_ERR_NO_MEM; } } if (!ethernet_event_handlers_registered_) { esp_err_t err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleEthernetEvent, this); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to register Ethernet event handler: %s", esp_err_to_name(err)); stopEthernet(); return err; } err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &GatewayNetworkService::HandleEthernetEvent, this); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to register Ethernet IP event handler: %s", esp_err_to_name(err)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister( ETH_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleEthernetEvent)); stopEthernet(); return err; } ethernet_event_handlers_registered_ = true; } if (config_.ethernet_spi_int_gpio >= 0) { esp_err_t err = gpio_install_isr_service(0); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to install GPIO ISR service for Ethernet: %s", esp_err_to_name(err)); stopEthernet(); return err; } } const auto spi_host = static_cast(config_.ethernet_spi_host); spi_bus_config_t bus_config = {}; bus_config.miso_io_num = config_.ethernet_spi_miso_gpio; bus_config.mosi_io_num = config_.ethernet_spi_mosi_gpio; bus_config.sclk_io_num = config_.ethernet_spi_sclk_gpio; bus_config.quadwp_io_num = -1; bus_config.quadhd_io_num = -1; esp_err_t err = spi_bus_initialize(spi_host, &bus_config, SPI_DMA_CH_AUTO); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to initialize Ethernet SPI host %d: %s", config_.ethernet_spi_host, esp_err_to_name(err)); stopEthernet(); return err; } spi_device_interface_config_t spi_device_config = {}; spi_device_config.mode = 0; spi_device_config.clock_speed_hz = static_cast(config_.ethernet_spi_clock_mhz) * 1000 * 1000; spi_device_config.spics_io_num = config_.ethernet_spi_cs_gpio; spi_device_config.queue_size = 20; eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); mac_config.rx_task_stack_size = config_.ethernet_rx_task_stack_size; eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); phy_config.phy_addr = config_.ethernet_phy_addr; phy_config.reset_gpio_num = config_.ethernet_phy_reset_gpio; eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_host, &spi_device_config); w5500_config.int_gpio_num = config_.ethernet_spi_int_gpio; w5500_config.poll_period_ms = config_.ethernet_poll_period_ms; if (w5500_config.int_gpio_num < 0 && w5500_config.poll_period_ms == 0) { w5500_config.poll_period_ms = 100; } eth_mac_ = esp_eth_mac_new_w5500(&w5500_config, &mac_config); if (eth_mac_ == nullptr) { ESP_LOGE(kTag, "failed to create W5500 Ethernet MAC"); stopEthernet(); return ESP_ERR_NO_MEM; } eth_phy_ = esp_eth_phy_new_w5500(&phy_config); if (eth_phy_ == nullptr) { ESP_LOGE(kTag, "failed to create W5500 Ethernet PHY"); stopEthernet(); return ESP_ERR_NO_MEM; } esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(eth_mac_, eth_phy_); err = esp_eth_driver_install(ð_config, ð_handle_); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to install Ethernet driver: %s", esp_err_to_name(err)); stopEthernet(); return err; } uint8_t eth_mac[6] = {}; err = esp_read_mac(eth_mac, ESP_MAC_ETH); if (err == ESP_OK) { err = esp_eth_ioctl(eth_handle_, ETH_CMD_S_MAC_ADDR, eth_mac); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to set Ethernet MAC address: %s", esp_err_to_name(err)); stopEthernet(); return err; } EthernetInfo info; info.mac = MacToHex(eth_mac); runtime_.setEthernetInfo(std::move(info)); } else { ESP_LOGW(kTag, "failed to read Ethernet MAC address: %s", esp_err_to_name(err)); } eth_glue_ = esp_eth_new_netif_glue(eth_handle_); if (eth_glue_ == nullptr) { ESP_LOGE(kTag, "failed to create Ethernet netif glue"); stopEthernet(); return ESP_ERR_NO_MEM; } err = esp_netif_attach(eth_netif_, eth_glue_); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to attach Ethernet netif: %s", esp_err_to_name(err)); stopEthernet(); return err; } err = esp_eth_start(eth_handle_); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to start Ethernet: %s", esp_err_to_name(err)); stopEthernet(); return err; } ethernet_started_ = true; err = probeEthernetStartup(); if (err != ESP_OK) { ESP_LOGE(kTag, "Ethernet startup probe failed: %s", esp_err_to_name(err)); stopEthernet(); return err; } ESP_LOGI(kTag, "Ethernet W5500 started spi_host=%d sclk=%d mosi=%d miso=%d cs=%d int=%d reset=%d", config_.ethernet_spi_host, config_.ethernet_spi_sclk_gpio, config_.ethernet_spi_mosi_gpio, config_.ethernet_spi_miso_gpio, config_.ethernet_spi_cs_gpio, config_.ethernet_spi_int_gpio, config_.ethernet_phy_reset_gpio); return ESP_OK; #else ESP_LOGW(kTag, "Ethernet requested but W5500 support is not enabled in esp-eth"); return ESP_ERR_NOT_SUPPORTED; #endif } esp_err_t GatewayNetworkService::probeEthernetStartup() { if (eth_handle_ == nullptr || !ethernet_started_) { return ESP_ERR_INVALID_STATE; } esp_err_t err = esp_eth_stop(eth_handle_); ethernet_started_ = false; if (err != ESP_OK) { return err; } err = esp_eth_start(eth_handle_); if (err == ESP_OK) { ethernet_started_ = true; } return err; } void GatewayNetworkService::stopEthernet() { if (ethernet_event_handlers_registered_) { ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister( IP_EVENT, IP_EVENT_ETH_GOT_IP, &GatewayNetworkService::HandleEthernetEvent)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister( ETH_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleEthernetEvent)); ethernet_event_handlers_registered_ = false; } if (eth_handle_ != nullptr) { const esp_err_t stop_err = esp_eth_stop(eth_handle_); if (stop_err != ESP_OK && stop_err != ESP_ERR_INVALID_STATE) { ESP_LOGW(kTag, "failed to stop Ethernet during disable: %s", esp_err_to_name(stop_err)); } ethernet_started_ = false; } if (eth_glue_ != nullptr) { const esp_err_t glue_err = esp_eth_del_netif_glue(eth_glue_); if (glue_err != ESP_OK) { ESP_LOGW(kTag, "failed to delete Ethernet netif glue: %s", esp_err_to_name(glue_err)); } else { eth_glue_ = nullptr; } } if (eth_handle_ != nullptr) { const esp_err_t uninstall_err = esp_eth_driver_uninstall(eth_handle_); if (uninstall_err != ESP_OK) { ESP_LOGW(kTag, "failed to uninstall Ethernet driver: %s", esp_err_to_name(uninstall_err)); } else { eth_handle_ = nullptr; } } if (eth_phy_ != nullptr && eth_handle_ == nullptr) { ESP_ERROR_CHECK_WITHOUT_ABORT(eth_phy_->del(eth_phy_)); eth_phy_ = nullptr; } if (eth_mac_ != nullptr && eth_handle_ == nullptr) { ESP_ERROR_CHECK_WITHOUT_ABORT(eth_mac_->del(eth_mac_)); eth_mac_ = nullptr; } if (eth_netif_ != nullptr && eth_glue_ == nullptr) { esp_netif_destroy(eth_netif_); eth_netif_ = nullptr; } runtime_.clearEthernetInfo(); } esp_err_t GatewayNetworkService::startWifi() { if (wifi_started_) { return ESP_OK; } stopSmartconfig(); stopEspNow(); setup_ap_started_ = false; if (wifi_sta_netif_ == nullptr) { wifi_sta_netif_ = esp_netif_create_default_wifi_sta(); if (wifi_sta_netif_ == nullptr) { ESP_LOGE(kTag, "failed to create default Wi-Fi STA netif"); return ESP_ERR_NO_MEM; } } wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); esp_err_t err = esp_wifi_init(&wifi_init_config); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to init Wi-Fi: %s", esp_err_to_name(err)); return err; } if (!wifi_event_handlers_registered_) { err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleWifiEvent, this); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to register Wi-Fi event handler: %s", esp_err_to_name(err)); return err; } err = esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &GatewayNetworkService::HandleWifiEvent, this); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to register IP event handler: %s", esp_err_to_name(err)); return err; } wifi_event_handlers_registered_ = true; } ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_storage(WIFI_STORAGE_RAM)); err = esp_wifi_set_mode(WIFI_MODE_STA); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to set Wi-Fi mode STA: %s", esp_err_to_name(err)); return err; } const auto device_info = runtime_.deviceInfo(); if (device_info.wlan.has_value() && !device_info.wlan->ssid.empty()) { wifi_config_t wifi_config = {}; std::strncpy(reinterpret_cast(wifi_config.sta.ssid), device_info.wlan->ssid.c_str(), sizeof(wifi_config.sta.ssid) - 1); std::strncpy(reinterpret_cast(wifi_config.sta.password), device_info.wlan->password.c_str(), sizeof(wifi_config.sta.password) - 1); err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to set Wi-Fi credentials: %s", esp_err_to_name(err)); return err; } } err = esp_wifi_start(); if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) { ESP_LOGE(kTag, "failed to start Wi-Fi: %s", esp_err_to_name(err)); return err; } uint8_t mac[6] = {0}; if (device_info.wlan.has_value() && !device_info.wlan->ssid.empty() && esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) { WirelessInfo wireless = device_info.wlan.value_or(WirelessInfo{}); wireless.mac = MacToHex(mac); runtime_.setWirelessInfo(std::move(wireless)); } wifi_started_ = true; controller_.setWirelessSetupMode(false); ESP_LOGI(kTag, "Wi-Fi STA started has_credentials=%d", device_info.wlan.has_value() && !device_info.wlan->ssid.empty()); return ESP_OK; } esp_err_t GatewayNetworkService::startSetupAp() { stopSmartconfig(); if (wifi_ap_netif_ == nullptr) { wifi_ap_netif_ = esp_netif_create_default_wifi_ap(); if (wifi_ap_netif_ == nullptr) { ESP_LOGE(kTag, "failed to create default Wi-Fi AP netif"); return ESP_ERR_NO_MEM; } esp_netif_ip_info_t ip_info = {}; IP4_ADDR(&ip_info.ip, 192, 168, 3, 1); IP4_ADDR(&ip_info.gw, 192, 168, 3, 1); IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_stop(wifi_ap_netif_)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_set_ip_info(wifi_ap_netif_, &ip_info)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_start(wifi_ap_netif_)); } wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); esp_err_t err = esp_wifi_init(&wifi_init_config); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to init Wi-Fi for setup AP: %s", esp_err_to_name(err)); return err; } if (wifi_started_) { stopEspNow(); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); } wifi_config_t ap_config = {}; std::strncpy(reinterpret_cast(ap_config.ap.ssid), kSetupApSsid, sizeof(ap_config.ap.ssid) - 1); ap_config.ap.ssid_len = std::strlen(kSetupApSsid); ap_config.ap.channel = 1; ap_config.ap.authmode = WIFI_AUTH_OPEN; ap_config.ap.max_connection = 4; err = esp_wifi_set_mode(WIFI_MODE_AP); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to set Wi-Fi AP mode: %s", esp_err_to_name(err)); return err; } err = esp_wifi_set_config(WIFI_IF_AP, &ap_config); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to configure setup AP: %s", esp_err_to_name(err)); return err; } err = esp_wifi_start(); if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) { ESP_LOGE(kTag, "failed to start setup AP: %s", esp_err_to_name(err)); 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; controller_.setWirelessSetupMode(true); ESP_LOGI(kTag, "setup AP started ssid=%s ip=192.168.3.1", kSetupApSsid); return ESP_OK; } esp_err_t GatewayNetworkService::startSmartconfig() { if (!config_.smartconfig_enabled) { ESP_LOGW(kTag, "smartconfig requested but not supported"); return ESP_ERR_NOT_SUPPORTED; } if (smartconfig_started_) { return ESP_OK; } config_.wifi_enabled = true; if (setup_ap_started_) { stopEspNow(); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); wifi_started_ = false; setup_ap_started_ = false; } esp_err_t err = startWifi(); if (err != ESP_OK) { return err; } if (!smartconfig_event_handler_registered_) { err = esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &GatewayNetworkService::HandleWifiEvent, this); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(kTag, "failed to register smartconfig event handler: %s", esp_err_to_name(err)); return err; } smartconfig_event_handler_registered_ = true; } err = esp_smartconfig_set_type(SC_TYPE_ESPTOUCH); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to set smartconfig type: %s", esp_err_to_name(err)); return err; } ESP_ERROR_CHECK_WITHOUT_ABORT(esp_esptouch_set_timeout(config_.smartconfig_timeout_sec)); smartconfig_start_config_t smartconfig = SMARTCONFIG_START_CONFIG_DEFAULT(); err = esp_smartconfig_start(&smartconfig); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to start smartconfig: %s", esp_err_to_name(err)); return err; } smartconfig_pending_wireless_.reset(); smartconfig_started_ = true; controller_.setWirelessSetupMode(true); ESP_LOGI(kTag, "ESP-Touch smartconfig started timeout=%us", config_.smartconfig_timeout_sec); return ESP_OK; } void GatewayNetworkService::stopSmartconfig() { if (!smartconfig_started_) { return; } ESP_ERROR_CHECK_WITHOUT_ABORT(esp_smartconfig_stop()); smartconfig_started_ = false; smartconfig_pending_wireless_.reset(); } 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; } gpio_config_t io_config = {}; io_config.pin_bit_mask = 1ULL << static_cast(config_.status_led_gpio); io_config.mode = GPIO_MODE_OUTPUT; io_config.pull_up_en = GPIO_PULLUP_DISABLE; io_config.pull_down_en = GPIO_PULLDOWN_DISABLE; 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 status LED GPIO%d: %s", config_.status_led_gpio, esp_err_to_name(err)); return err; } setStatusLed(false); 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; config.lru_purge_enable = true; esp_err_t err = httpd_start(&http_server_, &config); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to start HTTP server: %s", esp_err_to_name(err)); return err; } struct Route { const char* uri; httpd_method_t method; esp_err_t (*handler)(httpd_req_t*); }; const Route routes[] = { {"/info", HTTP_GET, &GatewayNetworkService::HandleInfoGet}, {"/dali/cmd", HTTP_GET, &GatewayNetworkService::HandleCommandGet}, {"/dali/cmd", HTTP_POST, &GatewayNetworkService::HandleCommandPost}, {"/bridge", HTTP_GET, &GatewayNetworkService::HandleBridgeGet}, {"/bridge", HTTP_POST, &GatewayNetworkService::HandleBridgePost}, {"/led/1", HTTP_GET, &GatewayNetworkService::HandleLedOnGet}, {"/led/0", HTTP_GET, &GatewayNetworkService::HandleLedOffGet}, {"/jq.js", HTTP_GET, &GatewayNetworkService::HandleJqJsGet}, }; for (const auto& route : routes) { err = RegisterUri(http_server_, route.uri, route.method, route.handler, this); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to register %s handler: %s", route.uri, esp_err_to_name(err)); return err; } } return ESP_OK; } esp_err_t GatewayNetworkService::startUdpTask() { if (udp_task_handle_ != nullptr) { return ESP_OK; } const BaseType_t created = xTaskCreate(&GatewayNetworkService::UdpTaskEntry, "gateway_udp", config_.udp_task_stack_size, this, config_.udp_task_priority, &udp_task_handle_); if (created != pdPASS) { udp_task_handle_ = nullptr; ESP_LOGE(kTag, "failed to create UDP task"); return ESP_ERR_NO_MEM; } 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); if (service != nullptr) { service->handleWifiEvent(event_base, event_id, event_data); } } 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::HandleEthernetEvent(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { auto* service = static_cast(arg); if (service != nullptr) { service->handleEthernetEvent(event_base, event_id, event_data); } } void GatewayNetworkService::handleEthernetEvent(esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == ETH_EVENT) { esp_eth_handle_t handle = eth_handle_; if (event_data != nullptr) { handle = *static_cast(event_data); } if (event_id == ETHERNET_EVENT_CONNECTED) { uint8_t mac[6] = {}; if (handle != nullptr && esp_eth_ioctl(handle, ETH_CMD_G_MAC_ADDR, mac) == ESP_OK) { const std::string mac_hex = MacToHex(mac); EthernetInfo info = runtime_.deviceInfo().eth.value_or(EthernetInfo{}); info.mac = mac_hex; runtime_.setEthernetInfo(std::move(info)); ESP_LOGI(kTag, "Ethernet link up mac=%s", mac_hex.c_str()); } else { ESP_LOGI(kTag, "Ethernet link up"); } return; } if (event_id == ETHERNET_EVENT_DISCONNECTED) { runtime_.clearEthernetIp(); ESP_LOGI(kTag, "Ethernet link down"); return; } if (event_id == ETHERNET_EVENT_START) { ESP_LOGI(kTag, "Ethernet driver started"); return; } if (event_id == ETHERNET_EVENT_STOP) { runtime_.clearEthernetIp(); ESP_LOGI(kTag, "Ethernet driver stopped"); } return; } if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP && event_data != nullptr) { auto* event = static_cast(event_data); char ip[16] = {0}; esp_ip4addr_ntoa(&event->ip_info.ip, ip, sizeof(ip)); EthernetInfo info = runtime_.deviceInfo().eth.value_or(EthernetInfo{}); uint8_t mac[6] = {}; if (eth_handle_ != nullptr && esp_eth_ioctl(eth_handle_, ETH_CMD_G_MAC_ADDR, mac) == ESP_OK) { info.mac = MacToHex(mac); } info.ip = ip; runtime_.setEthernetInfo(std::move(info)); ESP_LOGI(kTag, "Ethernet got IP %s", ip); } } void GatewayNetworkService::handleWifiEvent(esp_event_base_t event_base, int32_t event_id, void* event_data) { if (!config_.wifi_enabled) { return; } if (event_base == SC_EVENT) { if (event_id == SC_EVENT_SCAN_DONE) { ESP_LOGI(kTag, "smartconfig scan done"); } else if (event_id == SC_EVENT_FOUND_CHANNEL) { ESP_LOGI(kTag, "smartconfig found channel"); } else if (event_id == SC_EVENT_GOT_SSID_PSWD && event_data != nullptr) { auto* event = static_cast(event_data); wifi_config_t wifi_config = {}; std::memcpy(wifi_config.sta.ssid, event->ssid, sizeof(wifi_config.sta.ssid)); std::memcpy(wifi_config.sta.password, event->password, sizeof(wifi_config.sta.password)); wifi_config.sta.bssid_set = event->bssid_set; if (event->bssid_set) { std::memcpy(wifi_config.sta.bssid, event->bssid, sizeof(wifi_config.sta.bssid)); } WirelessInfo wireless; wireless.ssid = BoundedString(event->ssid, sizeof(event->ssid)); wireless.password = BoundedString(event->password, sizeof(event->password)); uint8_t mac[6] = {}; if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) { wireless.mac = MacToHex(mac); } smartconfig_pending_wireless_ = wireless; ESP_LOGI(kTag, "smartconfig got credentials ssid=%s", wireless.ssid.c_str()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } else if (event_id == SC_EVENT_SEND_ACK_DONE) { stopSmartconfig(); controller_.setWirelessSetupMode(false); ESP_LOGI(kTag, "smartconfig ACK sent"); } return; } if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { const auto info = runtime_.deviceInfo(); if (info.wlan.has_value() && !info.wlan->ssid.empty()) { ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } return; } if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { auto info = runtime_.deviceInfo(); if (info.wlan.has_value()) { const bool has_credentials = !info.wlan->ssid.empty(); info.wlan->ip.clear(); runtime_.setWirelessInfo(std::move(*info.wlan)); if (has_credentials && !smartconfig_started_) { ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_connect()); } } return; } if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP && event_data != nullptr) { auto* event = static_cast(event_data); char ip[16] = {0}; esp_ip4addr_ntoa(&event->ip_info.ip, ip, sizeof(ip)); WirelessInfo wireless = smartconfig_pending_wireless_.value_or( runtime_.deviceInfo().wlan.value_or(WirelessInfo{})); wireless.ip = ip; uint8_t mac[6] = {}; if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) { wireless.mac = MacToHex(mac); } runtime_.setWirelessInfo(std::move(wireless)); ESP_LOGI(kTag, "Wi-Fi got IP %s", ip); } } 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) { ESP_LOGE(kTag, "failed to create UDP socket"); udp_task_handle_ = nullptr; vTaskDelete(nullptr); return; } sockaddr_in local_addr = {}; local_addr.sin_family = AF_INET; local_addr.sin_port = htons(config_.udp_port); local_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(udp_socket_, reinterpret_cast(&local_addr), sizeof(local_addr)) != 0) { ESP_LOGE(kTag, "failed to bind UDP socket on port %u", config_.udp_port); close(udp_socket_); udp_socket_ = -1; udp_task_handle_ = nullptr; vTaskDelete(nullptr); return; } ESP_LOGI(kTag, "UDP router listening on port %u", config_.udp_port); while (true) { uint8_t buffer[kUdpBufferSize] = {0}; sockaddr_storage remote_addr = {}; socklen_t remote_addr_len = sizeof(remote_addr); const int read_len = recvfrom(udp_socket_, buffer, sizeof(buffer), 0, reinterpret_cast(&remote_addr), &remote_addr_len); if (read_len <= 0) { continue; } { LockGuard guard(udp_lock_); udp_remote_addr_ = remote_addr; udp_remote_addr_len_ = remote_addr_len; has_udp_remote_ = true; } controller_.enqueueCommandFrame(std::vector(buffer, buffer + read_len)); } } 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; } sockaddr_storage remote_addr = {}; socklen_t remote_addr_len = 0; { LockGuard guard(udp_lock_); if (!has_udp_remote_) { return; } remote_addr = udp_remote_addr_; remote_addr_len = udp_remote_addr_len_; } sendto(udp_socket_, frame.data(), frame.size(), 0, reinterpret_cast(&remote_addr), remote_addr_len); } void GatewayNetworkService::handleWifiControl(uint8_t mode) { if (mode == 0) { config_.wifi_enabled = false; stopSmartconfig(); stopEspNow(); controller_.setWirelessSetupMode(false); if (wifi_started_) { ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect()); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); wifi_started_ = false; setup_ap_started_ = false; } auto info = runtime_.deviceInfo(); if (info.wlan.has_value()) { info.wlan->ip.clear(); runtime_.setWirelessInfo(std::move(*info.wlan)); } return; } if (mode == 101) { config_.wifi_enabled = true; ESP_ERROR_CHECK_WITHOUT_ABORT(startSetupAp()); return; } if (mode == 100) { ESP_ERROR_CHECK_WITHOUT_ABORT(startSmartconfig()); return; } if (mode == 1) { config_.wifi_enabled = true; stopSmartconfig(); if (setup_ap_started_) { stopEspNow(); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); wifi_started_ = false; setup_ap_started_ = false; } ESP_ERROR_CHECK_WITHOUT_ABORT(startWifi()); } } std::string GatewayNetworkService::deviceInfoJson() const { const auto info = runtime_.deviceInfo(); cJSON* root = cJSON_CreateObject(); if (root == nullptr) { return {}; } cJSON_AddStringToObject(root, "serialId", info.serial_id.c_str()); cJSON_AddStringToObject(root, "type", info.type.c_str()); cJSON_AddStringToObject(root, "project", info.project.c_str()); cJSON_AddStringToObject(root, "version", info.version.c_str()); cJSON* gateway_info = cJSON_CreateObject(); if (gateway_info != nullptr) { cJSON_AddNumberToObject(gateway_info, "count", static_cast(info.dali_gateway_count)); cJSON_AddItemToObject(root, "daliGatewayInfo", gateway_info); } if (info.wlan.has_value()) { cJSON* wlan = cJSON_CreateObject(); if (wlan != nullptr) { cJSON_AddStringToObject(wlan, "mac", info.wlan->mac.c_str()); cJSON_AddStringToObject(wlan, "IP", info.wlan->ip.c_str()); cJSON_AddStringToObject(wlan, "ssid", info.wlan->ssid.c_str()); cJSON_AddStringToObject(wlan, "passwd", info.wlan->password.c_str()); cJSON_AddItemToObject(root, "wlanInfo", wlan); } } if (info.eth.has_value()) { cJSON* eth = cJSON_CreateObject(); if (eth != nullptr) { cJSON_AddStringToObject(eth, "mac", info.eth->mac.c_str()); cJSON_AddStringToObject(eth, "IP", info.eth->ip.c_str()); cJSON_AddItemToObject(root, "ethInfo", eth); } } const std::string rendered = PrintJson(root); cJSON_Delete(root); return rendered; } std::string GatewayNetworkService::deviceInfoDoubleEncodedJson() const { const std::string inner = deviceInfoJson(); cJSON* outer = cJSON_CreateString(inner.c_str()); if (outer == nullptr) { return {}; } const std::string rendered = PrintJson(outer); cJSON_Delete(outer); return rendered; } std::string GatewayNetworkService::gatewaySnapshotJson() { const auto snapshot = controller_.snapshot(); cJSON* root = cJSON_CreateObject(); if (root == nullptr) { return {}; } cJSON_AddNumberToObject(root, "count", static_cast(snapshot.channels.size())); cJSON* gw_info = cJSON_CreateObject(); if (gw_info != nullptr) { cJSON_AddBoolToObject(gw_info, "setupMode", snapshot.setup_mode); cJSON_AddBoolToObject(gw_info, "wlSetupMode", snapshot.wireless_setup_mode); cJSON_AddBoolToObject(gw_info, "bleEnabled", snapshot.ble_enabled); cJSON_AddBoolToObject(gw_info, "wifiEnabled", snapshot.wifi_enabled); cJSON_AddBoolToObject(gw_info, "IPRouter", snapshot.ip_router_enabled); cJSON_AddBoolToObject(gw_info, "iSceneEnabled", snapshot.internal_scene_supported); cJSON_AddBoolToObject(gw_info, "iGroupEnabled", snapshot.internal_group_supported); cJSON_AddItemToObject(root, "gwInfo", gw_info); } cJSON* channels = cJSON_CreateArray(); if (channels != nullptr) { for (const auto& channel : snapshot.channels) { cJSON* item = cJSON_CreateObject(); if (item == nullptr) { continue; } cJSON_AddNumberToObject(item, "channel", channel.channel_index + 1); cJSON_AddNumberToObject(item, "gw", channel.gateway_id); cJSON_AddStringToObject(item, "name", channel.name.c_str()); cJSON_AddStringToObject(item, "phy", channel.phy.c_str()); cJSON_AddNumberToObject(item, "sceneMaskLow", channel.scene_mask_low); cJSON_AddNumberToObject(item, "sceneMaskHigh", channel.scene_mask_high); cJSON_AddNumberToObject(item, "groupMaskLow", channel.group_mask_low); cJSON_AddNumberToObject(item, "groupMaskHigh", channel.group_mask_high); cJSON_AddBoolToObject(item, "isAllocAddr", channel.allocating); cJSON_AddNumberToObject(item, "lastAllocAddr", channel.last_alloc_addr); cJSON_AddItemToArray(channels, item); } cJSON_AddItemToObject(root, "channels", channels); } const std::string rendered = PrintJson(root); cJSON_Delete(root); return rendered; } void GatewayNetworkService::setStatusLed(bool on) { if (config_.status_led_gpio < 0) { return; } const bool level = config_.status_led_active_high ? on : !on; gpio_set_level(static_cast(config_.status_led_gpio), level ? 1 : 0); } esp_err_t GatewayNetworkService::HandleInfoGet(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { return ESP_FAIL; } const std::string payload = service->deviceInfoDoubleEncodedJson(); httpd_resp_set_type(req, "application/json"); return httpd_resp_send(req, payload.data(), payload.size()); } esp_err_t GatewayNetworkService::HandleCommandGet(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { return ESP_FAIL; } const std::string payload = service->gatewaySnapshotJson(); httpd_resp_set_type(req, "application/json"); return httpd_resp_send(req, payload.data(), payload.size()); } esp_err_t GatewayNetworkService::HandleCommandPost(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { return ESP_FAIL; } std::string body; if (ReadRequestBody(req, body) != ESP_OK) { return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request"); } cJSON* root = cJSON_ParseWithLength(body.c_str(), body.size()); if (root == nullptr) { return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request: json decode error"); } const cJSON* command = cJSON_GetObjectItemCaseSensitive(root, "command"); if (!cJSON_IsString(command) || command->valuestring == nullptr) { cJSON_Delete(root); return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request: missing command"); } const auto frame = DecodeHex(command->valuestring); cJSON_Delete(root); if (frame.empty()) { return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request: invalid command hex"); } service->controller_.enqueueCommandFrame(frame); httpd_resp_set_type(req, "text/plain"); return httpd_resp_sendstr(req, "ok"); } esp_err_t GatewayNetworkService::HandleBridgeGet(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { return ESP_FAIL; } return service->sendBridgeResponse(req, false); } esp_err_t GatewayNetworkService::HandleBridgePost(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { return ESP_FAIL; } return service->sendBridgeResponse(req, true); } esp_err_t GatewayNetworkService::sendBridgeResponse(httpd_req_t* req, bool post) { if (bridge_service_ == nullptr) { return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Bridge service is not enabled"); } const std::string action = QueryValue(req, "action"); const auto gateway_id = QueryGatewayId(req); const int selected_gateway_id = gateway_id.has_value() ? gateway_id.value() : -1; if (!post) { return SendJsonResponse(req, bridge_service_->handleGet(action, selected_gateway_id, QueryString(req))); } std::string body; if (ReadRequestBody(req, body) != ESP_OK) { return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request"); } return SendJsonResponse(req, bridge_service_->handlePost(action, selected_gateway_id, body)); } esp_err_t GatewayNetworkService::HandleLedOnGet(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { return ESP_FAIL; } service->setStatusLed(true); httpd_resp_set_type(req, "text/plain"); return httpd_resp_sendstr(req, "ok"); } esp_err_t GatewayNetworkService::HandleLedOffGet(httpd_req_t* req) { auto* service = static_cast(req->user_ctx); if (service == nullptr) { return ESP_FAIL; } service->setStatusLed(false); httpd_resp_set_type(req, "text/plain"); return httpd_resp_sendstr(req, "ok"); } esp_err_t GatewayNetworkService::HandleJqJsGet(httpd_req_t* req) { FILE* file = std::fopen("/jq.js", "rb"); if (file == nullptr) { return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not Found/jq.js"); } httpd_resp_set_type(req, "application/javascript"); char buffer[512] = {0}; while (true) { const size_t read_len = std::fread(buffer, 1, sizeof(buffer), file); if (read_len > 0 && httpd_resp_send_chunk(req, buffer, read_len) != ESP_OK) { std::fclose(file); return ESP_FAIL; } if (read_len < sizeof(buffer)) { break; } } std::fclose(file); return httpd_resp_send_chunk(req, nullptr, 0); } } // namespace gateway