From 54c639d381450ba9b416f00cd37d5b3496e83aba Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 00:45:06 +0000 Subject: [PATCH 1/2] add WiFi AP hotspot + WebSocket CAN downlink for Pecan dashboard - New telemetry component: WiFi AP (SSID "wfr-mobo", open), WebSocket server on port 80 at /ws - Broadcasts all CAN RX frames as JSON arrays in legacy v1 format: [{"time":ms, "canId":id, "data":[b0..b7]}] - Batches frames every 100ms, drops if queue full (no backpressure) - Enable CONFIG_HTTPD_WS_SUPPORT in sdkconfig.main and sdkconfig.debug https://claude.ai/code/session_0117iHbd3hAwUoxxeRuyWCAF --- components/CAN/CAN.c | 2 + components/CAN/CMakeLists.txt | 2 +- components/telemetry/CMakeLists.txt | 3 + components/telemetry/telemetry.c | 215 ++++++++++++++++++++++++++++ components/telemetry/telemetry.h | 6 + main/CMakeLists.txt | 2 +- main/main.c | 3 + sdkconfig.debug | 2 +- sdkconfig.main | 2 +- 9 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 components/telemetry/CMakeLists.txt create mode 100644 components/telemetry/telemetry.c create mode 100644 components/telemetry/telemetry.h diff --git a/components/CAN/CAN.c b/components/CAN/CAN.c index fbc132b..74dd264 100644 --- a/components/CAN/CAN.c +++ b/components/CAN/CAN.c @@ -10,6 +10,7 @@ #include "io.h" #include "statemachine.h" #include "config.h" +#include "telemetry.h" static const char* TAG = "CAN"; @@ -41,6 +42,7 @@ void canTask(void *arg) ESP_LOGI(TAG, "Recieved frame!"); union CANBuffer_u rx_data; memcpy(rx_data.array, rx_frame.buffer, 8); + telemetryQueueFrame(rx_frame.header.id, rx_data.array, 8); // ESP_LOGI(TAG,"Recieved bits: %X,%X,%X,%X,%X,%X,%X,%X",rx_data.array[0],rx_data.array[1],rx_data.array[2],rx_data.array[3],rx_data.array[4],rx_data.array[5],rx_data.array[6],rx_data.array[7]); // Module voltages if(rx_frame.header.id >= 1006 && rx_frame.header.id <= 1030){ diff --git a/components/CAN/CMakeLists.txt b/components/CAN/CMakeLists.txt index ec46b6f..0c52de8 100644 --- a/components/CAN/CMakeLists.txt +++ b/components/CAN/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "CAN.c" INCLUDE_DIRS "." "../../include" PRIV_REQUIRES esp_driver_twai - REQUIRES driver BMS io statemachine + REQUIRES driver BMS io statemachine telemetry ) \ No newline at end of file diff --git a/components/telemetry/CMakeLists.txt b/components/telemetry/CMakeLists.txt new file mode 100644 index 0000000..0f2c8d7 --- /dev/null +++ b/components/telemetry/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "telemetry.c" + INCLUDE_DIRS "." "../../include" + REQUIRES esp_wifi esp_http_server esp_netif nvs_flash esp_event json esp_timer) diff --git a/components/telemetry/telemetry.c b/components/telemetry/telemetry.c new file mode 100644 index 0000000..eb3855b --- /dev/null +++ b/components/telemetry/telemetry.c @@ -0,0 +1,215 @@ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_timer.h" +#include "nvs_flash.h" +#include "esp_http_server.h" +#include "cJSON.h" +#include "telemetry.h" +#include + +#define WIFI_SSID "wfr-mobo" +#define MAX_WS_CLIENTS 4 +#define FRAME_QUEUE_SIZE 64 +#define BATCH_SIZE 32 +#define BATCH_INTERVAL_MS 100 + +static const char *TAG = "telemetry"; +static httpd_handle_t server = NULL; +static QueueHandle_t frameQueue; +static SemaphoreHandle_t clientMutex; + +typedef struct { + uint32_t canId; + uint8_t data[8]; + uint8_t len; + int64_t timestamp_ms; +} telemetry_frame_t; + +// Connected WebSocket client file descriptors +static int ws_fds[MAX_WS_CLIENTS]; +static int ws_count = 0; + +// ---- WiFi AP ---- + +static void wifi_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { + if (event_id == WIFI_EVENT_AP_STACONNECTED) { + ESP_LOGI(TAG, "Station connected to AP"); + } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) { + ESP_LOGI(TAG, "Station disconnected from AP"); + } +} + +static void init_wifi_ap(void) { + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL)); + + wifi_config_t wifi_config = { + .ap = { + .ssid = WIFI_SSID, + .ssid_len = sizeof(WIFI_SSID) - 1, + .channel = 1, + .max_connection = MAX_WS_CLIENTS, + .authmode = WIFI_AUTH_OPEN, + }, + }; + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + + ESP_LOGI(TAG, "WiFi AP started. SSID: %s", WIFI_SSID); +} + +// ---- WebSocket Server ---- + +static esp_err_t ws_handler(httpd_req_t *req) { + if (req->method == HTTP_GET) { + // WebSocket handshake — register this client + int fd = httpd_req_to_sockfd(req); + xSemaphoreTake(clientMutex, portMAX_DELAY); + if (ws_count < MAX_WS_CLIENTS) { + ws_fds[ws_count++] = fd; + ESP_LOGI(TAG, "WS client connected fd=%d (total=%d)", fd, ws_count); + } else { + ESP_LOGW(TAG, "WS client rejected, max reached"); + } + xSemaphoreGive(clientMutex); + return ESP_OK; + } + + // Downlink only — ignore any incoming WS frames + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(ws_pkt)); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + return httpd_ws_recv_frame(req, &ws_pkt, 0); +} + +static void remove_ws_client(int idx) { + // Caller must hold clientMutex + ws_fds[idx] = ws_fds[--ws_count]; +} + +static void start_ws_server(void) { + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = 80; + + if (httpd_start(&server, &config) == ESP_OK) { + httpd_uri_t ws_uri = { + .uri = "/ws", + .method = HTTP_GET, + .handler = ws_handler, + .is_websocket = true, + }; + httpd_register_uri_handler(server, &ws_uri); + ESP_LOGI(TAG, "WebSocket server started on port %d", config.server_port); + } else { + ESP_LOGE(TAG, "Failed to start HTTP server"); + } +} + +// ---- Broadcast Task ---- + +static void telemetry_task(void *arg) { + telemetry_frame_t batch[BATCH_SIZE]; + int batch_count; + + while (1) { + batch_count = 0; + TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(BATCH_INTERVAL_MS); + + // Collect frames until batch is full or interval expires + while (batch_count < BATCH_SIZE) { + TickType_t now = xTaskGetTickCount(); + TickType_t remaining = (deadline > now) ? (deadline - now) : 0; + if (remaining == 0 && batch_count > 0) break; + + if (xQueueReceive(frameQueue, &batch[batch_count], + remaining > 0 ? remaining : pdMS_TO_TICKS(BATCH_INTERVAL_MS)) == pdTRUE) { + batch_count++; + } else { + break; + } + } + + if (batch_count == 0) continue; + + xSemaphoreTake(clientMutex, portMAX_DELAY); + if (ws_count == 0) { + xSemaphoreGive(clientMutex); + continue; + } + + // Build JSON array: [{"time":ms,"canId":id,"data":[b0,b1,...]}] + cJSON *array = cJSON_CreateArray(); + for (int i = 0; i < batch_count; i++) { + cJSON *obj = cJSON_CreateObject(); + cJSON_AddNumberToObject(obj, "time", (double)batch[i].timestamp_ms); + cJSON_AddNumberToObject(obj, "canId", batch[i].canId); + + cJSON *data_arr = cJSON_CreateArray(); + for (int j = 0; j < batch[i].len; j++) { + cJSON_AddItemToArray(data_arr, cJSON_CreateNumber(batch[i].data[j])); + } + cJSON_AddItemToObject(obj, "data", data_arr); + cJSON_AddItemToArray(array, obj); + } + + char *json = cJSON_PrintUnformatted(array); + cJSON_Delete(array); + + if (json) { + httpd_ws_frame_t ws_pkt = { + .type = HTTPD_WS_TYPE_TEXT, + .payload = (uint8_t *)json, + .len = strlen(json), + }; + + for (int i = ws_count - 1; i >= 0; i--) { + esp_err_t ret = httpd_ws_send_frame_async(server, ws_fds[i], &ws_pkt); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "WS client fd=%d send failed, removing", ws_fds[i]); + remove_ws_client(i); + } + } + free(json); + } + xSemaphoreGive(clientMutex); + } +} + +// ---- Public API ---- + +void initTelemetry(void) { + clientMutex = xSemaphoreCreateMutex(); + frameQueue = xQueueCreate(FRAME_QUEUE_SIZE, sizeof(telemetry_frame_t)); + + init_wifi_ap(); + start_ws_server(); + + xTaskCreate(telemetry_task, "telemetry", 8192, NULL, 3, NULL); + ESP_LOGI(TAG, "Telemetry initialized"); +} + +void telemetryQueueFrame(uint32_t canId, uint8_t *data, uint8_t len) { + telemetry_frame_t frame = { + .canId = canId, + .len = len > 8 ? 8 : len, + .timestamp_ms = esp_timer_get_time() / 1000, + }; + memcpy(frame.data, data, frame.len); + xQueueSend(frameQueue, &frame, 0); // non-blocking, drop if full +} diff --git a/components/telemetry/telemetry.h b/components/telemetry/telemetry.h new file mode 100644 index 0000000..bc3e00a --- /dev/null +++ b/components/telemetry/telemetry.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +void initTelemetry(); +void telemetryQueueFrame(uint32_t canId, uint8_t *data, uint8_t len); diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 262179a..5924fda 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1 +1 @@ -idf_component_register(SRCS "main.c" INCLUDE_DIRS "." "../components" "../include" REQUIRES "io" "CAN" "periodic" "BMS" "statemachine") \ No newline at end of file +idf_component_register(SRCS "main.c" INCLUDE_DIRS "." "../components" "../include" REQUIRES "io" "CAN" "periodic" "BMS" "statemachine" "telemetry") \ No newline at end of file diff --git a/main/main.c b/main/main.c index 83ec607..f427e71 100644 --- a/main/main.c +++ b/main/main.c @@ -4,6 +4,7 @@ #include "CAN.h" #include "io.h" #include "periodic.h" +#include "telemetry.h" // Code entry point void app_main() { @@ -14,8 +15,10 @@ void app_main() { esp_log_level_set("io", CONFIG_LOG_MAXIMUM_LEVEL); esp_log_level_set("periodic", CONFIG_LOG_MAXIMUM_LEVEL); esp_log_level_set("statemachine", CONFIG_LOG_MAXIMUM_LEVEL); + esp_log_level_set("telemetry", CONFIG_LOG_MAXIMUM_LEVEL); //init functions go here + initTelemetry(); initIO(); initCAN(); diff --git a/sdkconfig.debug b/sdkconfig.debug index b6ea943..c300e5a 100644 --- a/sdkconfig.debug +++ b/sdkconfig.debug @@ -976,7 +976,7 @@ CONFIG_HTTPD_MAX_URI_LEN=512 CONFIG_HTTPD_ERR_RESP_NO_DELAY=y CONFIG_HTTPD_PURGE_BUF_LEN=32 # CONFIG_HTTPD_LOG_PURGE_DATA is not set -# CONFIG_HTTPD_WS_SUPPORT is not set +CONFIG_HTTPD_WS_SUPPORT=y # CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000 # end of HTTP Server diff --git a/sdkconfig.main b/sdkconfig.main index b0bdd1e..d7de45a 100644 --- a/sdkconfig.main +++ b/sdkconfig.main @@ -976,7 +976,7 @@ CONFIG_HTTPD_MAX_URI_LEN=512 CONFIG_HTTPD_ERR_RESP_NO_DELAY=y CONFIG_HTTPD_PURGE_BUF_LEN=32 # CONFIG_HTTPD_LOG_PURGE_DATA is not set -# CONFIG_HTTPD_WS_SUPPORT is not set +CONFIG_HTTPD_WS_SUPPORT=y # CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000 # end of HTTP Server From 83516f320ed405d45017a6177973d2b64901724c Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Sun, 15 Feb 2026 23:49:07 -0700 Subject: [PATCH 2/2] Update CAN.c, build passed --- components/CAN/CAN.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/CAN/CAN.c b/components/CAN/CAN.c index 74dd264..6eaa574 100644 --- a/components/CAN/CAN.c +++ b/components/CAN/CAN.c @@ -91,6 +91,7 @@ void canTask(void *arg) //update module timeout for(int i = 0; i < 5; i++){ modules[i].timeout = pdTICKS_TO_MS(xTaskGetTickCount() - lastModuleTimestamp[i]); + } } } @@ -209,10 +210,10 @@ void canTxPeriodic(){ //charging message txMessage.header.ide = true; txMessage.header.id = id_ElconLimits; - canTxBuffer.elconLimits.maxChargeCurrent_lo = (CHARGE_TARGET * 10) & 0xFF; - canTxBuffer.elconLimits.maxChargeCurrent_hi = ((CHARGE_TARGET * 10) & 0xFF00)<<8; + canTxBuffer.elconLimits.maxChargeVoltage_lo = (CHARGE_TARGET * 10) & 0xFF; + canTxBuffer.elconLimits.maxChargeVoltage_hi = ((CHARGE_TARGET * 10) & 0xFF00) >> 8; canTxBuffer.elconLimits.maxChargeCurrent_lo = (CHARGE_CURRENT * 10) & 0xFF; - canTxBuffer.elconLimits.maxChargeCurrent_hi = ((CHARGE_CURRENT * 10) & 0xFF00)<<8; + canTxBuffer.elconLimits.maxChargeCurrent_hi = ((CHARGE_CURRENT * 10) & 0xFF00) >> 8; canTxBuffer.elconLimits.control = moboState.currentState == CHARGING ? 1 : 0; txMessage.buffer = canTxBuffer.array; twai_node_transmit(mobo_node_handle, &txMessage,0);