Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions components/CAN/CAN.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "io.h"
#include "statemachine.h"
#include "config.h"
#include "telemetry.h"

static const char* TAG = "CAN";

Expand Down Expand Up @@ -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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve actual CAN payload length when queuing telemetry

telemetryQueueFrame is always called with a hard-coded length of 8, so any CAN frame with DLC < 8 will be serialized with extra trailing bytes that are not part of that frame. This corrupts the WebSocket downlink data for variable-length CAN traffic and can break consumers that rely on accurate payload length; pass the frame’s actual received length instead.

Useful? React with 👍 / 👎.

// 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){
Expand Down Expand Up @@ -89,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]);
}
}
}

Expand Down Expand Up @@ -207,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);
Expand Down
2 changes: 1 addition & 1 deletion components/CAN/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
3 changes: 3 additions & 0 deletions components/telemetry/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
215 changes: 215 additions & 0 deletions components/telemetry/telemetry.c
Original file line number Diff line number Diff line change
@@ -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 <string.h>

#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;
Comment on lines +88 to +91

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail the handshake when WebSocket client capacity is reached

When ws_count has reached MAX_WS_CLIENTS, the handler logs a rejection but still returns ESP_OK, which allows the WebSocket upgrade to succeed for a client that is never added to ws_fds. In that state the client appears connected but never receives telemetry, and stale tracked clients can effectively block new usable sessions; return an error/close the socket when capacity is exceeded.

Useful? React with 👍 / 👎.

}

// 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
}
6 changes: 6 additions & 0 deletions components/telemetry/telemetry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

#include <stdint.h>

void initTelemetry();
void telemetryQueueFrame(uint32_t canId, uint8_t *data, uint8_t len);
2 changes: 1 addition & 1 deletion main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
idf_component_register(SRCS "main.c" INCLUDE_DIRS "." "../components" "../include" REQUIRES "io" "CAN" "periodic" "BMS" "statemachine")
idf_component_register(SRCS "main.c" INCLUDE_DIRS "." "../components" "../include" REQUIRES "io" "CAN" "periodic" "BMS" "statemachine" "telemetry")
3 changes: 3 additions & 0 deletions main/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "CAN.h"
#include "io.h"
#include "periodic.h"
#include "telemetry.h"

// Code entry point
void app_main() {
Expand All @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion sdkconfig.debug
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sdkconfig.main
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down