From 5222737532196cf7c2847ae6c19bd30c68e7c97e Mon Sep 17 00:00:00 2001 From: lawther Date: Wed, 24 Jun 2026 12:58:54 +1000 Subject: [PATCH] Add capture buffer for unknown/error bus frames with web UI page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures frames that have no registered decoder (unknown) or that fail checksum/framing validation (error) into a fixed-capacity ring buffer with LFU eviction. Dedup is keyed on error status, length, and the first UNKNOWN_BUFFER_MAX_RAW_BYTES (64) bytes — frames that only differ beyond byte 64 collapse into one entry, which is a known limitation. Exposes the buffer via two HTTP endpoints: GET /unknown-msgs-json — JSON dump used by the web UI POST /unknown-msgs-clear — clears the buffer The web UI page at /unknown-msgs lists entries sorted by most-recently- seen, showing src/dst/cmd, decoded device names, hit count, timestamps, payload bytes, and the raw frame. Implementation notes: - unknown_buffer uses its own mutex; lock_for_read()/unlock_after_read() let the JSON handler iterate s_entries directly without a heap copy. - Hex strings use a single 193-byte stack buffer (MAX_RAW_BYTES * 3 + 1) reused for payload and raw fields; cJSON copies on each Add call. - unknown_buffer_record() is called after xSemaphoreGive(state_mutex) at all call sites to keep the pool-state critical section short. --- CHANGELOG.md | 1 + main/CMakeLists.txt | 1 + main/config.h | 2 +- main/main.c | 7 +- main/message_decoder.c | 12 +- main/tcp_bridge.c | 8 +- main/tcp_bridge.h | 5 +- main/unknown_buffer.c | 109 ++++++++++++++ main/unknown_buffer.h | 38 +++++ main/web_handlers.c | 287 +++++++++++++++++++++++++++++++++++++ test/run_tests.sh | 6 +- test/unknown_buffer_stub.c | 9 ++ 12 files changed, 473 insertions(+), 12 deletions(-) create mode 100644 main/unknown_buffer.c create mode 100644 main/unknown_buffer.h create mode 100644 test/unknown_buffer_stub.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef7a38..e4889e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Saving of unknown bus messages, added web UI to view them ### Changed - Note about CMD 0x05 observed with payload 0x00. Added to PROTOCOL.md and new sample trace. - Note about CMD 0x12 observed with payload 0x01 0x00. Added to PROTOCOL.md and new sample trace. diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 67813f5..e2fffb3 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -13,6 +13,7 @@ idf_component_register( "tcp_bridge.c" "message_decoder.c" "register_requester.c" + "unknown_buffer.c" "dns_server.c" INCLUDE_DIRS "." diff --git a/main/config.h b/main/config.h index 5d28be8..5256020 100644 --- a/main/config.h +++ b/main/config.h @@ -19,7 +19,7 @@ // HTTP Server #define HTTP_SERVER_PORT 80 -#define HTTP_MAX_URI_HANDLERS 14 // Number of endpoint handlers +#define HTTP_MAX_URI_HANDLERS 20 // Number of endpoint handlers #define HTTP_RECV_TIMEOUT_SEC 10 // Timeout for receiving requests #define HTTP_SEND_TIMEOUT_SEC 10 // Timeout for sending responses #define HTTP_STACK_SIZE 8192 // Stack size for HTTP server task diff --git a/main/main.c b/main/main.c index b773cdb..edaa538 100644 --- a/main/main.c +++ b/main/main.c @@ -19,6 +19,7 @@ #include "tcp_bridge.h" #include "message_decoder.h" #include "register_requester.h" +#include "unknown_buffer.h" // ==================== APPLICATION ===================== // All configuration values are in config.h @@ -53,7 +54,7 @@ static bool decode_wrapper(const uint8_t *data, int len) return decode_message(data, len, &s_decoder_context); } -static void frame_error_wrapper(tcp_bridge_frame_error_t error) +static void frame_error_wrapper(tcp_bridge_frame_error_t error, const uint8_t *data, int len) { if (xSemaphoreTake(s_pool_state_mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) != pdTRUE) { return; @@ -65,6 +66,9 @@ static void frame_error_wrapper(tcp_bridge_frame_error_t error) case TCP_BRIDGE_FRAME_ERR_NO_END: s_pool_state.errors_no_end++; break; } xSemaphoreGive(s_pool_state_mutex); + if (data && len > 0) { + unknown_buffer_record(data, len, true); + } } // ====================================================== @@ -146,6 +150,7 @@ void app_main(void) s_decoder_context.state_mutex = s_pool_state_mutex; // Initialize hardware + unknown_buffer_init(); bus_init(); ESP_ERROR_CHECK(led_init()); led_set_startup(); diff --git a/main/message_decoder.c b/main/message_decoder.c index 8991f96..bd3524b 100644 --- a/main/message_decoder.c +++ b/main/message_decoder.c @@ -2,6 +2,7 @@ #include "config.h" #include "mqtt_publish.h" #include "register_requester.h" +#include "unknown_buffer.h" #include "esp_log.h" #include #include @@ -2883,6 +2884,7 @@ bool decode_message(const uint8_t *data, int len, message_decoder_context_t *ctx ctx->pool_state->messages_error_total++; xSemaphoreGive(ctx->state_mutex); } + unknown_buffer_record(data, len, true); return false; } @@ -2950,6 +2952,8 @@ bool decode_message(const uint8_t *data, int len, message_decoder_context_t *ctx // Increment global and per-device counters. A frame with any validation // error counts as an error only, so decoded + unknown + errors = total. + bool record_frame = false; + bool record_is_error = false; if (ctx->state_mutex) { if (xSemaphoreTake(ctx->state_mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) == pdTRUE) { if (length_error || header_chk_error || data_chk_error) { @@ -2957,9 +2961,14 @@ bool decode_message(const uint8_t *data, int len, message_decoder_context_t *ctx if (length_error) ctx->pool_state->errors_length_mismatch++; if (header_chk_error) ctx->pool_state->errors_header_checksum++; if (data_chk_error) ctx->pool_state->errors_data_checksum++; + record_frame = true; + record_is_error = true; } else { if (decoded) ctx->pool_state->messages_decoded_total++; - else ctx->pool_state->messages_unknown_total++; + else { + ctx->pool_state->messages_unknown_total++; + record_frame = true; + } // Per-device counters (skip broadcast) if (!(src_hi == 0xFF && src_lo == 0xFF)) { @@ -2973,6 +2982,7 @@ bool decode_message(const uint8_t *data, int len, message_decoder_context_t *ctx xSemaphoreGive(ctx->state_mutex); } } + if (record_frame) unknown_buffer_record(data, len, record_is_error); return decoded; } diff --git a/main/tcp_bridge.c b/main/tcp_bridge.c index a934ef1..41adad1 100644 --- a/main/tcp_bridge.c +++ b/main/tcp_bridge.c @@ -133,7 +133,7 @@ static bool extract_and_process_message(int client_sock) hex_str[hex_pos] = '\0'; ESP_LOGW(TAG, "No start byte in buffer, discarding %d bytes: %s%s", s_msg_buffer_len, hex_str, s_msg_buffer_len > 32 ? "..." : ""); - if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_NO_START); + if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_NO_START, s_msg_buffer, s_msg_buffer_len); } s_msg_buffer_len = 0; return false; @@ -161,7 +161,7 @@ static bool extract_and_process_message(int client_sock) hex_str[hex_pos] = '\0'; ESP_LOGW(TAG, "Invalid control bytes: %02X %02X (expected 80 00), data: %s%s, discarding start byte", s_msg_buffer[5], s_msg_buffer[6], hex_str, s_msg_buffer_len > 32 ? "..." : ""); - if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_BAD_CONTROL); + if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_BAD_CONTROL, s_msg_buffer, s_msg_buffer_len); // Discard this start byte and look for next memmove(s_msg_buffer, &s_msg_buffer[1], s_msg_buffer_len - 1); s_msg_buffer_len--; @@ -241,7 +241,7 @@ static bool extract_and_process_message(int client_sock) hex_str[hex_pos] = '\0'; ESP_LOGW(TAG, "Buffer nearly full (%d bytes) without complete message, first 32 bytes: %s..., clearing", s_msg_buffer_len, hex_str); - if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_NO_END); + if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_NO_END, s_msg_buffer, s_msg_buffer_len); s_msg_buffer_len = 0; } @@ -347,7 +347,7 @@ static void tcp_bridge_task(void *pvParameters) } else { ESP_LOGW(TAG, "Reassembly buffer overflow (%d + %d > %d), clearing", s_msg_buffer_len, len, BUS_MESSAGE_MAX_SIZE); - if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_NO_END); + if (s_config.on_frame_error) s_config.on_frame_error(TCP_BRIDGE_FRAME_ERR_NO_END, s_msg_buffer, s_msg_buffer_len); s_msg_buffer_len = 0; } diff --git a/main/tcp_bridge.h b/main/tcp_bridge.h index 361701c..3e2537e 100644 --- a/main/tcp_bridge.h +++ b/main/tcp_bridge.h @@ -31,8 +31,9 @@ typedef enum { TCP_BRIDGE_FRAME_ERR_NO_END, // Buffer filled without a checksum+END match (too long / corrupt) } tcp_bridge_frame_error_t; -// Called when the reassembly layer discards data due to a framing error -typedef void (*tcp_bridge_frame_error_fn)(tcp_bridge_frame_error_t error); +// Called when the reassembly layer discards data due to a framing error. +// data/len are the raw bytes being discarded (may be NULL/0 for overflow cases). +typedef void (*tcp_bridge_frame_error_fn)(tcp_bridge_frame_error_t error, const uint8_t *data, int len); /** * Configuration structure for TCP bridge diff --git a/main/unknown_buffer.c b/main/unknown_buffer.c new file mode 100644 index 0000000..f57a540 --- /dev/null +++ b/main/unknown_buffer.c @@ -0,0 +1,109 @@ +#include "unknown_buffer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include +#include + +#define MUTEX_TIMEOUT_MS 100 + +static unknown_entry_t s_entries[UNKNOWN_BUFFER_CAPACITY]; +static int s_count = 0; +static SemaphoreHandle_t s_mutex = NULL; + +void unknown_buffer_init(void) +{ + s_mutex = xSemaphoreCreateMutex(); + s_count = 0; +} + +// Adds `data` to the record of unknown data frames. +// +// If the data matches an existing record, the hit count and last seen time are updated. +// Otherwise, a new record is created, evicting the least frequently used record if the +// buffer is full. +// +// Note on matching: A match is defined as the same error status, the same length, and +// the same first `UNKNOWN_BUFFER_MAX_RAW_BYTES` bytes. As we cap stored bytes to +// `UNKNOWN_BUFFER_MAX_RAW_BYTES`, this means we can't distinguish between two different +// frames that only differ in the bytes after `UNKNOWN_BUFFER_MAX_RAW_BYTES`. This is a +// known limitation of this implementation. +void unknown_buffer_record(const uint8_t *data, int len, bool is_error) +{ + if (!s_mutex || !data || len <= 0) return; + + int store_len = (len < UNKNOWN_BUFFER_MAX_RAW_BYTES) ? len : UNKNOWN_BUFFER_MAX_RAW_BYTES; + + time_t now = time(NULL); + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) != pdTRUE) return; + + for (int i = 0; i < s_count; i++) { + if (s_entries[i].is_error == is_error && + s_entries[i].raw_len == (uint16_t)len && + memcmp(s_entries[i].raw, data, (size_t)store_len) == 0) { + s_entries[i].hit_count++; + s_entries[i].last_seen = now; + xSemaphoreGive(s_mutex); + return; + } + } + + int slot; + if (s_count < UNKNOWN_BUFFER_CAPACITY) { + slot = s_count++; + } else { + // LFU eviction: evict the entry with the fewest hits; break ties on oldest first_seen + slot = 0; + for (int i = 1; i < UNKNOWN_BUFFER_CAPACITY; i++) { + if (s_entries[i].hit_count < s_entries[slot].hit_count || + (s_entries[i].hit_count == s_entries[slot].hit_count && + s_entries[i].first_seen < s_entries[slot].first_seen)) { + slot = i; + } + } + } + + unknown_entry_t *e = &s_entries[slot]; + e->raw_len = (uint16_t)len; + memcpy(e->raw, data, (size_t)store_len); + e->hit_count = 1; + e->first_seen = now; + e->last_seen = now; + e->is_error = is_error; + + xSemaphoreGive(s_mutex); +} + +void unknown_buffer_clear(void) +{ + if (!s_mutex) return; + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) == pdTRUE) { + s_count = 0; + xSemaphoreGive(s_mutex); + } +} + +locked_unknown_buffer_t unknown_buffer_lock_for_read(void) +{ + locked_unknown_buffer_t locked_buf = { + .entries = NULL, + .count = 0 + }; + + if (!s_mutex) { + return locked_buf; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) != pdTRUE) { + return locked_buf; + } + + locked_buf.entries = s_entries; + locked_buf.count = s_count; + return locked_buf; +} + +void unknown_buffer_unlock_after_read(void) +{ + if (s_mutex) xSemaphoreGive(s_mutex); +} diff --git a/main/unknown_buffer.h b/main/unknown_buffer.h new file mode 100644 index 0000000..1c87c3e --- /dev/null +++ b/main/unknown_buffer.h @@ -0,0 +1,38 @@ +#ifndef UNKNOWN_BUFFER_H +#define UNKNOWN_BUFFER_H + +#include +#include +#include +#include "config.h" + +#define UNKNOWN_BUFFER_CAPACITY 100 +#define UNKNOWN_BUFFER_MAX_RAW_BYTES 64 // store at most the first 64 bytes of each frame + +typedef struct { + uint16_t raw_len; // actual frame length (may exceed UNKNOWN_BUFFER_MAX_RAW_BYTES) + uint8_t raw[UNKNOWN_BUFFER_MAX_RAW_BYTES]; // first bytes of the frame + uint32_t hit_count; + time_t first_seen; // UTC epoch seconds (0 if NTP not synced) + time_t last_seen; // UTC epoch seconds (0 if NTP not synced) + bool is_error; // true = framing/checksum error; false = no handler matched +} unknown_entry_t; + +void unknown_buffer_init(void); + +// Record a frame. is_error=true for framing/checksum errors, false for unhandled frames. +void unknown_buffer_record(const uint8_t *data, int len, bool is_error); + +void unknown_buffer_clear(void); + +typedef struct { + const unknown_entry_t *entries; + int count; +} locked_unknown_buffer_t; + +// Acquires the buffer mutex and returns a read-only pointer to the entry array. +// The caller MUST call unknown_buffer_unlock_after_read() when done. +locked_unknown_buffer_t unknown_buffer_lock_for_read(void); +void unknown_buffer_unlock_after_read(void); + +#endif // UNKNOWN_BUFFER_H diff --git a/main/web_handlers.c b/main/web_handlers.c index 755313d..a4f10ea 100644 --- a/main/web_handlers.c +++ b/main/web_handlers.c @@ -4,6 +4,7 @@ #include "pool_state.h" #include "mqtt_poolclient.h" #include "message_decoder.h" +#include "unknown_buffer.h" #include "device_serial.h" #include "esp_wifi.h" #include "esp_log.h" @@ -20,6 +21,9 @@ static const char *TAG = "WEB_HANDLERS"; +#define _STR(x) #x +#define STR(x) _STR(x) + // Forward declaration - defined in main.c const char* get_gateway_comms_status_text(uint16_t code); @@ -97,6 +101,7 @@ char *get_page_nav(const char page) { "
  • WiFi Config
  • " "
  • MQTT Config
  • " "
  • Status
  • " + "
  • Unknown Messages
  • " "
  • Firmware Update
  • " "" "
    %s
    " @@ -109,6 +114,7 @@ char *get_page_nav(const char page) { page == 'w' ? cur : none, page == 'm' ? cur : none, page == 's' ? cur : none, + page == 'x' ? cur : none, page == 'u' ? cur : none, app_desc->version); if (n < 0) return NULL; @@ -121,6 +127,7 @@ char *get_page_nav(const char page) { page == 'w' ? cur : none, page == 'm' ? cur : none, page == 's' ? cur : none, + page == 'x' ? cur : none, page == 'u' ? cur : none, app_desc->version); return nav; @@ -904,6 +911,19 @@ static esp_err_t status_get_handler(httpd_req_t *req) } cJSON_AddItemToObject(root, "timers", timers); + // Memory + char free_heap_str[24], min_free_heap_str[24]; + uint32_t free_heap = esp_get_free_heap_size(); + uint32_t min_free_heap = esp_get_minimum_free_heap_size(); + snprintf(free_heap_str, sizeof(free_heap_str), "%lu.%lu KB", + (unsigned long)(free_heap / 1024), (unsigned long)((free_heap % 1024) * 10 / 1024)); + snprintf(min_free_heap_str, sizeof(min_free_heap_str), "%lu.%lu KB", + (unsigned long)(min_free_heap / 1024), (unsigned long)((min_free_heap % 1024) * 10 / 1024)); + cJSON *memory = cJSON_CreateObject(); + cJSON_AddStringToObject(memory, "free_heap", free_heap_str); + cJSON_AddStringToObject(memory, "min_free_heap", min_free_heap_str); + cJSON_AddItemToObject(root, "memory", memory); + // Timestamps uint64_t current_tick_ms = (uint64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; cJSON_AddNumberToObject(root, "last_update_ms", (double)state.last_update_ms); @@ -1539,6 +1559,252 @@ static esp_err_t test_decode_post_handler(httpd_req_t *req) return ESP_OK; } +// ====================================================== +// Unknown Messages Handlers +// ====================================================== + +static esp_err_t unknown_msgs_json_handler(httpd_req_t *req) +{ + locked_unknown_buffer_t locked = unknown_buffer_lock_for_read(); + const int count = locked.count; + const unknown_entry_t *snap = locked.entries; + if (snap == NULL) { + httpd_resp_send_err( + req, HTTPD_500_INTERNAL_SERVER_ERROR, "Couldn't acquire unknown buffer lock, try again" + ); + return ESP_FAIL; + } + + cJSON *root = cJSON_CreateObject(); + if (!root) { + unknown_buffer_unlock_after_read(); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"); + return ESP_FAIL; + } + cJSON_AddNumberToObject(root, "count", count); + cJSON *arr = cJSON_CreateArray(); + if (!arr) { + unknown_buffer_unlock_after_read(); + cJSON_Delete(root); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"); + return ESP_FAIL; + } + + char hex_buf[UNKNOWN_BUFFER_MAX_RAW_BYTES * 3 + 1]; + for (int i = 0; i < count; i++) { + const unknown_entry_t *e = &snap[i]; + cJSON *obj = cJSON_CreateObject(); + if (!obj) continue; + + // Decode address/command fields from the raw frame + // Frame layout: [START=0][SRC=1-2][DST=3-4][CTRL=5-6][CMD=7][LEN=8][HDR_CHK=9][DATA=10...] + char addr_str[8]; + char name_buf[16]; + uint8_t src_hi = (e->raw_len > 2) ? e->raw[1] : 0; + uint8_t src_lo = (e->raw_len > 2) ? e->raw[2] : 0; + uint8_t dst_hi = (e->raw_len > 4) ? e->raw[3] : 0; + uint8_t dst_lo = (e->raw_len > 4) ? e->raw[4] : 0; + uint8_t cmd = (e->raw_len > 7) ? e->raw[7] : 0; + + snprintf(addr_str, sizeof(addr_str), "0x%02X%02X", src_hi, src_lo); + cJSON_AddStringToObject(obj, "src", addr_str); + cJSON_AddStringToObject(obj, "src_name", + get_device_name(src_hi, src_lo, name_buf, sizeof(name_buf))); + + snprintf(addr_str, sizeof(addr_str), "0x%02X%02X", dst_hi, dst_lo); + cJSON_AddStringToObject(obj, "dst", addr_str); + cJSON_AddStringToObject(obj, "dst_name", + get_device_name(dst_hi, dst_lo, name_buf, sizeof(name_buf))); + + char cmd_str[7]; + snprintf(cmd_str, sizeof(cmd_str), "0x%02X", cmd); + cJSON_AddStringToObject(obj, "cmd", cmd_str); + + // Payload bytes (raw[10] to raw[raw_len-3]) as space-separated hex + // Cap at what was actually stored — raw[] only holds the first UNKNOWN_BUFFER_MAX_RAW_BYTES bytes. + int payload_start = 10; + int stored_len = ((int)e->raw_len < UNKNOWN_BUFFER_MAX_RAW_BYTES) ? (int)e->raw_len : UNKNOWN_BUFFER_MAX_RAW_BYTES; + int payload_end = ((int)e->raw_len - 2 < stored_len) ? (int)e->raw_len - 2 : stored_len; + int payload_len = (payload_end > payload_start) ? (payload_end - payload_start) : 0; + if (payload_len > 0) { + int pos = 0; + for (int j = 0; j < payload_len; j++) { + pos += snprintf(hex_buf + pos, sizeof(hex_buf) - pos, "%02X ", e->raw[payload_start + j]); + } + if (pos > 0) hex_buf[pos - 1] = '\0'; + cJSON_AddStringToObject(obj, "payload", hex_buf); + } else { + cJSON_AddStringToObject(obj, "payload", ""); + } + + // Raw frame bytes (capped at UNKNOWN_BUFFER_MAX_RAW_BYTES) as space-separated hex + { + int pos = 0; + for (int j = 0; j < stored_len; j++) { + pos += snprintf(hex_buf + pos, sizeof(hex_buf) - pos, "%02X ", e->raw[j]); + } + if (pos > 0) hex_buf[pos - 1] = '\0'; + cJSON_AddStringToObject(obj, "raw", hex_buf); + } + + cJSON_AddBoolToObject(obj, "is_error", e->is_error); + cJSON_AddNumberToObject(obj, "raw_len", (double)e->raw_len); + cJSON_AddNumberToObject(obj, "hits", (double)e->hit_count); + cJSON_AddNumberToObject(obj, "first_seen", (double)e->first_seen); + cJSON_AddNumberToObject(obj, "last_seen", (double)e->last_seen); + + cJSON_AddItemToArray(arr, obj); + } + + unknown_buffer_unlock_after_read(); + cJSON_AddItemToObject(root, "entries", arr); + + char *json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (!json) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON error"); + return ESP_FAIL; + } + + httpd_resp_set_type(req, "application/json; charset=UTF-8"); + httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); + free(json); + return ESP_OK; +} + +static esp_err_t unknown_msgs_clear_handler(httpd_req_t *req) +{ + char buf[32]; + int remaining = req->content_len; + while (remaining > 0) { + int ret = httpd_req_recv(req, buf, remaining < (int)sizeof(buf) ? remaining : (int)sizeof(buf)); + if (ret <= 0) break; + remaining -= ret; + } + + unknown_buffer_clear(); + + httpd_resp_set_type(req, "application/json; charset=UTF-8"); + httpd_resp_send(req, "{\"ok\":true}", HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t unknown_msgs_view_handler(httpd_req_t *req) +{ + const char *page_content = + "

    Unknown Bus Messages

    " + "

    Unrecognized and error frames captured from the bus " + "(max " STR(UNKNOWN_BUFFER_CAPACITY) " stored). Sorted by most recently seen.

    " + "
    " + "" + "
    " + "Raw JSON" + "" + "
    " + "
    " + "" + "" + "" + ""; + + char page_title[] = "Unknown Bus Messages"; + char *header = get_page_header(page_title); + char *nav = get_page_nav('x'); + char *footer = get_page_footer(); + + httpd_resp_set_type(req, "text/html; charset=UTF-8"); + httpd_resp_send_chunk(req, header, HTTPD_RESP_USE_STRLEN); + httpd_resp_send_chunk(req, nav, HTTPD_RESP_USE_STRLEN); + httpd_resp_send_chunk(req, page_content, HTTPD_RESP_USE_STRLEN); + httpd_resp_send_chunk(req, footer, HTTPD_RESP_USE_STRLEN); + httpd_resp_send_chunk(req, NULL, 0); + + free(footer); + free(nav); + free(header); + return ESP_OK; +} + // ====================================================== // URI Handlers // ====================================================== @@ -1609,6 +1875,24 @@ static const httpd_uri_t test_decode_uri = { .handler = test_decode_post_handler }; +static const httpd_uri_t unknown_msgs_json_uri = { + .uri = "/unknown_msgs", + .method = HTTP_GET, + .handler = unknown_msgs_json_handler +}; + +static const httpd_uri_t unknown_msgs_clear_uri = { + .uri = "/unknown_msgs/clear", + .method = HTTP_POST, + .handler = unknown_msgs_clear_handler +}; + +static const httpd_uri_t unknown_msgs_view_uri = { + .uri = "/unknown_msgs_view", + .method = HTTP_GET, + .handler = unknown_msgs_view_handler +}; + // ====================================================== @@ -1771,6 +2055,9 @@ esp_err_t web_handlers_register(httpd_handle_t server) httpd_register_uri_handler(server, &update_get_uri); httpd_register_uri_handler(server, &update_post_uri); httpd_register_uri_handler(server, &test_decode_uri); + httpd_register_uri_handler(server, &unknown_msgs_json_uri); + httpd_register_uri_handler(server, &unknown_msgs_clear_uri); + httpd_register_uri_handler(server, &unknown_msgs_view_uri); httpd_register_uri_handler(server, &static_files_uri); // /static/* wildcard httpd_register_uri_handler(server, &favicon_redirect_uri); // /favicon.ico -> /static/favicon.ico httpd_register_uri_handler(server, &robots_txt_uri); diff --git a/test/run_tests.sh b/test/run_tests.sh index cc9de97..051ec16 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -7,7 +7,7 @@ set -euo pipefail cd "$(dirname "$0")" -SHARED="log_capture.c" +SHARED=(log_capture.c unknown_buffer_stub.c) PASS=0 FAIL=0 ERRORS=() @@ -47,7 +47,7 @@ for test_src in test_*.c; do echo " Compiling: $test_src" echo "========================================" - if gcc -I. -I.. -o "$binary" "$test_src" "$main_src" "$SHARED" 2>&1; then + if gcc -I. -I.. -o "$binary" "$test_src" "$main_src" "${SHARED[@]}" 2>&1; then if "$binary"; then PASS=$((PASS + 1)) else @@ -67,7 +67,7 @@ if [ -f test_replay.c ]; then echo " Compiling: test_replay.c" echo "========================================" - if gcc -I. -I.. -o ./run_replay test_replay.c ../main/message_decoder.c "$SHARED" 2>&1; then + if gcc -I. -I.. -o ./run_replay test_replay.c ../main/message_decoder.c "${SHARED[@]}" 2>&1; then if ./run_replay; then PASS=$((PASS + 1)) else diff --git a/test/unknown_buffer_stub.c b/test/unknown_buffer_stub.c new file mode 100644 index 0000000..fcd75f2 --- /dev/null +++ b/test/unknown_buffer_stub.c @@ -0,0 +1,9 @@ +#include +#include + +// No-op stub — message_decoder.c calls this but the real implementation +// uses FreeRTOS primitives not available in the host test environment. +void unknown_buffer_record(const uint8_t *data, int len, bool is_error) +{ + (void)data; (void)len; (void)is_error; +}