feat(gateway): add GatewayNetworkService and enhance runtime channel handling
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
#include "gateway_network.hpp"
|
||||
|
||||
#include "gateway_controller.hpp"
|
||||
#include "gateway_runtime.hpp"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "lwip/inet.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace gateway {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kTag = "gateway_network";
|
||||
constexpr size_t kUdpBufferSize = 256;
|
||||
|
||||
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<uint8_t>(ch - '0');
|
||||
return true;
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'f') {
|
||||
value = static_cast<uint8_t>(10 + ch - 'a');
|
||||
return true;
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'F') {
|
||||
value = static_cast<uint8_t>(10 + ch - 'A');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DecodeHex(std::string_view hex) {
|
||||
if ((hex.size() & 1U) != 0U) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> 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<uint8_t>((high << 4) | low));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
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<size_t>(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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GatewayNetworkService::GatewayNetworkService(GatewayController& controller,
|
||||
GatewayRuntime& runtime,
|
||||
GatewayNetworkServiceConfig config)
|
||||
: controller_(controller), runtime_(runtime), config_(config),
|
||||
udp_lock_(xSemaphoreCreateMutex()) {}
|
||||
|
||||
esp_err_t GatewayNetworkService::start() {
|
||||
if (started_) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t err = ensureNetworkStack();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
controller_.addNotificationSink(
|
||||
[this](const std::vector<uint8_t>& frame) { handleGatewayNotification(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;
|
||||
}
|
||||
}
|
||||
|
||||
started_ = true;
|
||||
ESP_LOGI(kTag, "network service started http=%d udp=%d", 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::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;
|
||||
}
|
||||
|
||||
httpd_uri_t info_uri = {};
|
||||
info_uri.uri = "/info";
|
||||
info_uri.method = HTTP_GET;
|
||||
info_uri.handler = &GatewayNetworkService::HandleInfoGet;
|
||||
info_uri.user_ctx = this;
|
||||
err = httpd_register_uri_handler(http_server_, &info_uri);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(kTag, "failed to register /info handler: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
httpd_uri_t command_uri = {};
|
||||
command_uri.uri = "/dali/cmd";
|
||||
command_uri.method = HTTP_POST;
|
||||
command_uri.handler = &GatewayNetworkService::HandleCommandPost;
|
||||
command_uri.user_ctx = this;
|
||||
err = httpd_register_uri_handler(http_server_, &command_uri);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(kTag, "failed to register /dali/cmd handler: %s", 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;
|
||||
}
|
||||
|
||||
void GatewayNetworkService::UdpTaskEntry(void* arg) {
|
||||
static_cast<GatewayNetworkService*>(arg)->udpTaskLoop();
|
||||
}
|
||||
|
||||
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<sockaddr*>(&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<sockaddr*>(&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<uint8_t>(buffer, buffer + read_len));
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayNetworkService::handleGatewayNotification(const std::vector<uint8_t>& 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<const sockaddr*>(&remote_addr), remote_addr_len);
|
||||
}
|
||||
|
||||
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<double>(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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
esp_err_t GatewayNetworkService::HandleInfoGet(httpd_req_t* req) {
|
||||
auto* service = static_cast<GatewayNetworkService*>(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::HandleCommandPost(httpd_req_t* req) {
|
||||
auto* service = static_cast<GatewayNetworkService*>(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");
|
||||
}
|
||||
|
||||
} // namespace gateway
|
||||
Reference in New Issue
Block a user