From ab04ff8413f99a4e202027d3fecd8a38921483c9 Mon Sep 17 00:00:00 2001 From: YoungSoo Shin Date: Tue, 2 Sep 2025 11:19:50 +0900 Subject: [PATCH] Update: websocket optimization - Use protobuf - Eliminate unnecessary optimization logic - UART, sensor, status data transmitted as pb data Signed-off-by: YoungSoo Shin --- main/CMakeLists.txt | 24 +- main/idf_component.yml | 1 + main/proto/status.pb.c | 24 ++ main/proto/status.pb.h | 155 ++++++++ main/service/monitor.c | 118 ++++--- main/service/webserver.c | 1 + main/service/webserver.h | 6 +- main/service/ws.c | 222 +++++------- page/package-lock.json | 740 ++++++++++++++++++++++++++++++++++++++- page/package.json | 17 +- page/src/main.js | 107 +++--- proto/status.proto | 38 ++ 12 files changed, 1197 insertions(+), 256 deletions(-) create mode 100644 main/proto/status.pb.c create mode 100644 main/proto/status.pb.h create mode 100644 proto/status.proto diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index a1ebc1a..eae90d5 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -2,6 +2,12 @@ set(WEB_APP_SOURCE_DIR ${CMAKE_SOURCE_DIR}/page) set(GZ_OUTPUT_FILE ${WEB_APP_SOURCE_DIR}/dist/index.html.gz) +set(PROTO_DIR ${CMAKE_SOURCE_DIR}/proto) +set(PROTO_OUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto) +set(PROTO_FILE ${PROTO_DIR}/status.proto) +set(PROTO_C_FILE ${PROTO_OUT_DIR}/status.pb.c) +set(PROTO_H_FILE ${PROTO_OUT_DIR}/status.pb.h) + # Check npm is available find_program(NPM_EXECUTABLE npm) if (NOT NPM_EXECUTABLE) @@ -10,8 +16,8 @@ endif () # Register the component. Now, CMake knows how GZ_OUTPUT_FILE is generated # and can correctly handle the dependency for embedding. -idf_component_register(SRC_DIRS "app" "nconfig" "wifi" "indicator" "system" "service" - INCLUDE_DIRS "include" +idf_component_register(SRC_DIRS "app" "nconfig" "wifi" "indicator" "system" "service" "proto" + INCLUDE_DIRS "include" "proto" EMBED_FILES ${GZ_OUTPUT_FILE} ) @@ -48,4 +54,18 @@ add_custom_target(build_web_app ALL DEPENDS ${GZ_OUTPUT_FILE} ) +add_custom_command( + OUTPUT ${PROTO_C_FILE} ${PROTO_H_FILE} + COMMAND protoc --nanopb_out=${PROTO_OUT_DIR} status.proto + WORKING_DIRECTORY ${PROTO_DIR} + DEPENDS ${PROTO_FILE} + COMMENT "Generating C sources from ${PROTO_FILE} using nanopb" + VERBATIM +) +add_custom_target(protobuf_generate ALL + DEPENDS ${PROTO_C_FILE} ${PROTO_H_FILE} +) + +add_dependencies(${COMPONENT_LIB} build_web_app) +add_dependencies(${COMPONENT_LIB} protobuf_generate) \ No newline at end of file diff --git a/main/idf_component.yml b/main/idf_component.yml index fa3df99..a133602 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -3,3 +3,4 @@ dependencies: joltwallet/littlefs: ==1.20.1 esp-idf-lib/ina3221: ^1.1.7 esp-idf-lib/pca9557: ^1.0.7 + nikas-belogolov/nanopb: ^1.0.0 diff --git a/main/proto/status.pb.c b/main/proto/status.pb.c new file mode 100644 index 0000000..3b5abc2 --- /dev/null +++ b/main/proto/status.pb.c @@ -0,0 +1,24 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.8 */ + +#include "status.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(SensorChannelData, SensorChannelData, AUTO) + + +PB_BIND(SensorData, SensorData, AUTO) + + +PB_BIND(WifiStatus, WifiStatus, AUTO) + + +PB_BIND(UartData, UartData, AUTO) + + +PB_BIND(StatusMessage, StatusMessage, AUTO) + + + diff --git a/main/proto/status.pb.h b/main/proto/status.pb.h new file mode 100644 index 0000000..2e82d92 --- /dev/null +++ b/main/proto/status.pb.h @@ -0,0 +1,155 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.8 */ + +#ifndef PB_STATUS_PB_H_INCLUDED +#define PB_STATUS_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* Represents data for a single sensor channel */ +typedef struct _SensorChannelData { + float voltage; + float current; + float power; +} SensorChannelData; + +/* Contains data for all sensor channels and system info */ +typedef struct _SensorData { + bool has_usb; + SensorChannelData usb; + bool has_main; + SensorChannelData main; + bool has_vin; + SensorChannelData vin; + uint32_t timestamp; + uint32_t uptime_sec; +} SensorData; + +/* Contains WiFi connection status */ +typedef struct _WifiStatus { + bool connected; + pb_callback_t ssid; + int32_t rssi; +} WifiStatus; + +/* Contains raw UART data */ +typedef struct _UartData { + pb_callback_t data; +} UartData; + +/* Top-level message for all websocket communication */ +typedef struct _StatusMessage { + pb_size_t which_payload; + union { + SensorData sensor_data; + WifiStatus wifi_status; + UartData uart_data; + } payload; +} StatusMessage; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define SensorChannelData_init_default {0, 0, 0} +#define SensorData_init_default {false, SensorChannelData_init_default, false, SensorChannelData_init_default, false, SensorChannelData_init_default, 0, 0} +#define WifiStatus_init_default {0, {{NULL}, NULL}, 0} +#define UartData_init_default {{{NULL}, NULL}} +#define StatusMessage_init_default {0, {SensorData_init_default}} +#define SensorChannelData_init_zero {0, 0, 0} +#define SensorData_init_zero {false, SensorChannelData_init_zero, false, SensorChannelData_init_zero, false, SensorChannelData_init_zero, 0, 0} +#define WifiStatus_init_zero {0, {{NULL}, NULL}, 0} +#define UartData_init_zero {{{NULL}, NULL}} +#define StatusMessage_init_zero {0, {SensorData_init_zero}} + +/* Field tags (for use in manual encoding/decoding) */ +#define SensorChannelData_voltage_tag 1 +#define SensorChannelData_current_tag 2 +#define SensorChannelData_power_tag 3 +#define SensorData_usb_tag 1 +#define SensorData_main_tag 2 +#define SensorData_vin_tag 3 +#define SensorData_timestamp_tag 4 +#define SensorData_uptime_sec_tag 5 +#define WifiStatus_connected_tag 1 +#define WifiStatus_ssid_tag 2 +#define WifiStatus_rssi_tag 3 +#define UartData_data_tag 1 +#define StatusMessage_sensor_data_tag 1 +#define StatusMessage_wifi_status_tag 2 +#define StatusMessage_uart_data_tag 3 + +/* Struct field encoding specification for nanopb */ +#define SensorChannelData_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, FLOAT, voltage, 1) \ +X(a, STATIC, SINGULAR, FLOAT, current, 2) \ +X(a, STATIC, SINGULAR, FLOAT, power, 3) +#define SensorChannelData_CALLBACK NULL +#define SensorChannelData_DEFAULT NULL + +#define SensorData_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, usb, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, main, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, vin, 3) \ +X(a, STATIC, SINGULAR, UINT32, timestamp, 4) \ +X(a, STATIC, SINGULAR, UINT32, uptime_sec, 5) +#define SensorData_CALLBACK NULL +#define SensorData_DEFAULT NULL +#define SensorData_usb_MSGTYPE SensorChannelData +#define SensorData_main_MSGTYPE SensorChannelData +#define SensorData_vin_MSGTYPE SensorChannelData + +#define WifiStatus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, connected, 1) \ +X(a, CALLBACK, SINGULAR, STRING, ssid, 2) \ +X(a, STATIC, SINGULAR, INT32, rssi, 3) +#define WifiStatus_CALLBACK pb_default_field_callback +#define WifiStatus_DEFAULT NULL + +#define UartData_FIELDLIST(X, a) \ +X(a, CALLBACK, SINGULAR, BYTES, data, 1) +#define UartData_CALLBACK pb_default_field_callback +#define UartData_DEFAULT NULL + +#define StatusMessage_FIELDLIST(X, a) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,sensor_data,payload.sensor_data), 1) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,wifi_status,payload.wifi_status), 2) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,uart_data,payload.uart_data), 3) +#define StatusMessage_CALLBACK NULL +#define StatusMessage_DEFAULT NULL +#define StatusMessage_payload_sensor_data_MSGTYPE SensorData +#define StatusMessage_payload_wifi_status_MSGTYPE WifiStatus +#define StatusMessage_payload_uart_data_MSGTYPE UartData + +extern const pb_msgdesc_t SensorChannelData_msg; +extern const pb_msgdesc_t SensorData_msg; +extern const pb_msgdesc_t WifiStatus_msg; +extern const pb_msgdesc_t UartData_msg; +extern const pb_msgdesc_t StatusMessage_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define SensorChannelData_fields &SensorChannelData_msg +#define SensorData_fields &SensorData_msg +#define WifiStatus_fields &WifiStatus_msg +#define UartData_fields &UartData_msg +#define StatusMessage_fields &StatusMessage_msg + +/* Maximum encoded size of messages (where known) */ +/* WifiStatus_size depends on runtime parameters */ +/* UartData_size depends on runtime parameters */ +/* StatusMessage_size depends on runtime parameters */ +#define STATUS_PB_H_MAX_SIZE SensorData_size +#define SensorChannelData_size 15 +#define SensorData_size 63 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/main/service/monitor.c b/main/service/monitor.c index d281285..693534e 100644 --- a/main/service/monitor.c +++ b/main/service/monitor.c @@ -2,10 +2,8 @@ // Created by shinys on 25. 8. 18.. // - #include "monitor.h" #include -#include "cJSON.h" #include "datalog.h" #include "esp_log.h" #include "esp_netif.h" @@ -13,16 +11,20 @@ #include "esp_wifi_types_generic.h" #include "freertos/FreeRTOS.h" #include "ina3221.h" +#include "pb.h" +#include "pb_encode.h" +#include "status.pb.h" #include "webserver.h" #include "wifi.h" #define PM_SDA CONFIG_I2C_GPIO_SDA #define PM_SCL CONFIG_I2C_GPIO_SCL -const char* channel_names[] = {"USB", "MAIN", "VIN"}; +#define PB_BUFFER_SIZE 256 + +static const char* TAG = "monitor"; ina3221_t ina3221 = { - /* shunt values are 100 mOhm for each channel */ .shunt = {10, 10, 10}, .mask.mask_register = INA3221_DEFAULT_MASK, .i2c_dev = {0}, @@ -40,69 +42,100 @@ ina3221_t ina3221 = { }, }; -// Timer callback function to read sensor data +static bool encode_string(pb_ostream_t* stream, const pb_field_t* field, void* const* arg) +{ + const char* str = (const char*)(*arg); + if (!str) + { + return true; // Nothing to encode + } + if (!pb_encode_tag_for_field(stream, field)) + { + return false; + } + return pb_encode_string(stream, (uint8_t*)str, strlen(str)); +} + +static void send_pb_message(const pb_msgdesc_t* fields, const void* src_struct) +{ + uint8_t buffer[PB_BUFFER_SIZE]; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + + if (!pb_encode(&stream, fields, src_struct)) + { + ESP_LOGE(TAG, "Failed to encode protobuf message: %s", PB_GET_ERROR(&stream)); + return; + } + + push_data_to_ws(buffer, stream.bytes_written); +} + static void sensor_timer_callback(void* arg) { - // Get system uptime int64_t uptime_us = esp_timer_get_time(); uint32_t uptime_sec = (uint32_t)(uptime_us / 1000000); uint32_t timestamp = (uint32_t)time(NULL); - channel_data_t channel_data[NUM_CHANNELS]; + channel_data_t channel_data_log[NUM_CHANNELS]; + + StatusMessage message = StatusMessage_init_zero; + message.which_payload = StatusMessage_sensor_data_tag; + SensorData* sensor_data = &message.payload.sensor_data; + + sensor_data->has_usb = true; + sensor_data->has_main = true; + sensor_data->has_vin = true; + + SensorChannelData* channels[] = {&sensor_data->usb, &sensor_data->main, &sensor_data->vin}; - // Create JSON object with sensor data - cJSON* root = cJSON_CreateObject(); for (uint8_t i = 0; i < INA3221_BUS_NUMBER; i++) { float voltage, current, power; - ina3221_get_bus_voltage(&ina3221, i, &voltage); ina3221_get_shunt_value(&ina3221, i, NULL, ¤t); current /= 1000.0f; // mA to A power = voltage * current; - // Populate data for datalog - channel_data[i].voltage = voltage; - channel_data[i].current = current; - channel_data[i].power = power; + // For datalog + channel_data_log[i] = (channel_data_t){.voltage = voltage, .current = current, .power = power}; - // Populate data for websocket - cJSON* v = cJSON_AddObjectToObject(root, channel_names[i]); - cJSON_AddNumberToObject(v, "voltage", voltage); - cJSON_AddNumberToObject(v, "current", current); - cJSON_AddNumberToObject(v, "power", power); + // For protobuf + channels[i]->voltage = voltage; + channels[i]->current = current; + channels[i]->power = power; } - // Add data to log file - datalog_add(timestamp, channel_data); + // datalog_add(timestamp, channel_data_log); - cJSON_AddStringToObject(root, "type", "sensor_data"); - cJSON_AddNumberToObject(root, "timestamp", timestamp); - cJSON_AddNumberToObject(root, "uptime_sec", uptime_sec); + sensor_data->timestamp = timestamp; + sensor_data->uptime_sec = uptime_sec; - // Push data to WebSocket clients - push_data_to_ws(root); + send_pb_message(StatusMessage_fields, &message); } static void status_wifi_callback(void* arg) { wifi_ap_record_t ap_info; - cJSON* root = cJSON_CreateObject(); + StatusMessage message = StatusMessage_init_zero; + message.which_payload = StatusMessage_wifi_status_tag; + WifiStatus* wifi_status = &message.payload.wifi_status; if (wifi_get_current_ap_info(&ap_info) == ESP_OK) { - cJSON_AddStringToObject(root, "type", "wifi_status"); - cJSON_AddBoolToObject(root, "connected", true); - cJSON_AddStringToObject(root, "ssid", (const char*)ap_info.ssid); - cJSON_AddNumberToObject(root, "rssi", ap_info.rssi); + wifi_status->connected = true; + wifi_status->ssid.funcs.encode = &encode_string; + wifi_status->ssid.arg = (void*)ap_info.ssid; + wifi_status->rssi = ap_info.rssi; } else { - cJSON_AddBoolToObject(root, "connected", false); + wifi_status->connected = false; + wifi_status->ssid.arg = ""; // Empty string + wifi_status->rssi = 0; } - push_data_to_ws(root); + send_pb_message(StatusMessage_fields, &message); } static esp_timer_handle_t sensor_timer; @@ -111,24 +144,15 @@ static esp_timer_handle_t wifi_status_timer; void init_status_monitor() { ESP_ERROR_CHECK(ina3221_init_desc(&ina3221, 0x40, 0, PM_SDA, PM_SCL)); - - // logger datalog_init(); - // Timer configuration - const esp_timer_create_args_t sensor_timer_args = { - .callback = &sensor_timer_callback, - .name = "sensor_reading_timer" // Optional name for debugging - }; - - const esp_timer_create_args_t wifi_timer_args = { - .callback = &status_wifi_callback, - .name = "wifi_status_timer" // Optional name for debugging - }; + const esp_timer_create_args_t sensor_timer_args = {.callback = &sensor_timer_callback, + .name = "sensor_reading_timer"}; + const esp_timer_create_args_t wifi_timer_args = {.callback = &status_wifi_callback, .name = "wifi_status_timer"}; ESP_ERROR_CHECK(esp_timer_create(&sensor_timer_args, &sensor_timer)); ESP_ERROR_CHECK(esp_timer_create(&wifi_timer_args, &wifi_status_timer)); - ESP_ERROR_CHECK(esp_timer_start_periodic(sensor_timer, 1000000)); // 1sec - ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_status_timer, 1000000 * 5)); // 5s in microseconds + ESP_ERROR_CHECK(esp_timer_start_periodic(sensor_timer, 1000000)); + ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_status_timer, 1000000 * 5)); } diff --git a/main/service/webserver.c b/main/service/webserver.c index ca42bf0..4259462 100644 --- a/main/service/webserver.c +++ b/main/service/webserver.c @@ -67,6 +67,7 @@ void start_webserver(void) httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 1024 * 8; config.max_uri_handlers = 10; + config.task_priority = 12; if (httpd_start(&server, &config) != ESP_OK) { diff --git a/main/service/webserver.h b/main/service/webserver.h index cac3025..93f993d 100644 --- a/main/service/webserver.h +++ b/main/service/webserver.h @@ -4,13 +4,15 @@ #ifndef ODROID_REMOTE_HTTP_WEBSERVER_H #define ODROID_REMOTE_HTTP_WEBSERVER_H -#include "cJSON.h" + #include "esp_http_server.h" +#include +#include void register_wifi_endpoint(httpd_handle_t server); void register_ws_endpoint(httpd_handle_t server); void register_control_endpoint(httpd_handle_t server); -void push_data_to_ws(cJSON* data); +void push_data_to_ws(const uint8_t* data, size_t len); void register_reboot_endpoint(httpd_handle_t server); esp_err_t change_baud_rate(int baud_rate); diff --git a/main/service/ws.c b/main/service/ws.c index 76969db..5db4997 100644 --- a/main/service/ws.c +++ b/main/service/ws.c @@ -2,13 +2,15 @@ // Created by shinys on 25. 8. 18.. // -#include "cJSON.h" #include "driver/uart.h" #include "esp_err.h" #include "esp_http_server.h" #include "esp_log.h" #include "freertos/semphr.h" #include "nconfig.h" +#include "pb.h" +#include "pb_encode.h" +#include "status.pb.h" #include "webserver.h" #define UART_NUM UART_NUM_1 @@ -16,13 +18,13 @@ #define UART_TX_PIN CONFIG_GPIO_UART_TX #define UART_RX_PIN CONFIG_GPIO_UART_RX #define CHUNK_SIZE (1024) +#define PB_UART_BUFFER_SIZE (CHUNK_SIZE + 64) static const char* TAG = "ws-uart"; static int client_fd = -1; static SemaphoreHandle_t client_fd_mutex; -// Unified message structure for the websocket queue enum ws_message_type { WS_MSG_STATUS, @@ -32,25 +34,28 @@ enum ws_message_type struct ws_message { enum ws_message_type type; + uint8_t* data; + size_t len; +}; - union - { - struct - { - cJSON* data; - } status; - - struct - { - uint8_t* data; - size_t len; - } uart; - } content; +struct bytes_arg +{ + const void* data; + size_t len; }; static QueueHandle_t ws_queue; -// Unified task to send data from the queue to the websocket client +static bool encode_bytes_callback(pb_ostream_t* stream, const pb_field_t* field, void* const* arg) +{ + struct bytes_arg* br = (struct bytes_arg*)(*arg); + if (!pb_encode_tag_for_field(stream, field)) + { + return false; + } + return pb_encode_string(stream, (uint8_t*)br->data, br->len); +} + static void unified_ws_sender_task(void* arg) { httpd_handle_t server = (httpd_handle_t)arg; @@ -67,41 +72,17 @@ static void unified_ws_sender_task(void* arg) if (fd <= 0) { xSemaphoreGive(client_fd_mutex); - // Free memory if client is not connected - if (msg.type == WS_MSG_STATUS) - { - cJSON_Delete(msg.content.status.data); - } - else - { - free(msg.content.uart.data); - } + free(msg.data); continue; } httpd_ws_frame_t ws_pkt = {0}; - esp_err_t err = ESP_FAIL; + ws_pkt.payload = msg.data; + ws_pkt.len = msg.len; + ws_pkt.type = HTTPD_WS_TYPE_BINARY; - if (msg.type == WS_MSG_STATUS) - { - char* json_string = cJSON_Print(msg.content.status.data); - cJSON_Delete(msg.content.status.data); - - ws_pkt.payload = (uint8_t*)json_string; - ws_pkt.len = strlen(json_string); - ws_pkt.type = HTTPD_WS_TYPE_TEXT; - err = httpd_ws_send_frame_async(server, fd, &ws_pkt); - free(json_string); - } - else - { - // WS_MSG_UART - ws_pkt.payload = msg.content.uart.data; - ws_pkt.len = msg.content.uart.len; - ws_pkt.type = HTTPD_WS_TYPE_BINARY; - err = httpd_ws_send_frame_async(server, fd, &ws_pkt); - free(msg.content.uart.data); - } + esp_err_t err = httpd_ws_send_frame_async(server, fd, &ws_pkt); + free(msg.data); if (err != ESP_OK) { @@ -114,7 +95,6 @@ static void unified_ws_sender_task(void* arg) } else { - // Queue receive timed out, send a PING to keep connection alive xSemaphoreTake(client_fd_mutex, portMAX_DELAY); int fd = client_fd; if (fd > 0) @@ -138,125 +118,77 @@ static void unified_ws_sender_task(void* arg) static void uart_polling_task(void* arg) { static uint8_t data_buf[BUF_SIZE]; - const TickType_t MIN_POLLING_INTERVAL = pdMS_TO_TICKS(1); - const TickType_t MAX_POLLING_INTERVAL = pdMS_TO_TICKS(10); - const TickType_t READ_TIMEOUT = pdMS_TO_TICKS(5); - - TickType_t current_interval = MIN_POLLING_INTERVAL; - int consecutive_empty_polls = 0; - int cached_client_fd = -1; - TickType_t last_client_check = 0; - const TickType_t CLIENT_CHECK_INTERVAL = pdMS_TO_TICKS(100); + static uint8_t pb_buffer[PB_UART_BUFFER_SIZE]; while (1) { - TickType_t current_time = xTaskGetTickCount(); + xSemaphoreTake(client_fd_mutex, portMAX_DELAY); + int fd = client_fd; + xSemaphoreGive(client_fd_mutex); - if (current_time - last_client_check >= CLIENT_CHECK_INTERVAL) + if (fd <= 0) { - xSemaphoreTake(client_fd_mutex, portMAX_DELAY); - cached_client_fd = client_fd; - xSemaphoreGive(client_fd_mutex); - last_client_check = current_time; + vTaskDelay(pdMS_TO_TICKS(100)); + continue; } size_t available_len; - esp_err_t err = uart_get_buffered_data_len(UART_NUM, &available_len); + uart_get_buffered_data_len(UART_NUM, &available_len); - if (err != ESP_OK || available_len == 0) + if (available_len == 0) { - consecutive_empty_polls++; - if (consecutive_empty_polls > 5) - { - current_interval = MAX_POLLING_INTERVAL; - } - else if (consecutive_empty_polls > 2) - { - current_interval = pdMS_TO_TICKS(5); - } - - if (cached_client_fd <= 0) - { - vTaskDelay(pdMS_TO_TICKS(50)); - continue; - } - - vTaskDelay(current_interval); + vTaskDelay(pdMS_TO_TICKS(10)); continue; } - consecutive_empty_polls = 0; - current_interval = MIN_POLLING_INTERVAL; + size_t read_len = (available_len > BUF_SIZE) ? BUF_SIZE : available_len; + int bytes_read = uart_read_bytes(UART_NUM, data_buf, read_len, pdMS_TO_TICKS(5)); - if (cached_client_fd <= 0) - { - uart_flush_input(UART_NUM); - continue; - } - - size_t total_processed = 0; - while (available_len > 0 && total_processed < BUF_SIZE) - { - size_t read_size = - (available_len > (BUF_SIZE - total_processed)) ? (BUF_SIZE - total_processed) : available_len; - - int bytes_read = uart_read_bytes(UART_NUM, data_buf + total_processed, read_size, READ_TIMEOUT); - - if (bytes_read <= 0) - { - break; - } - - total_processed += bytes_read; - available_len -= bytes_read; - - uart_get_buffered_data_len(UART_NUM, &available_len); - } - - if (total_processed > 0) + if (bytes_read > 0) { size_t offset = 0; - - while (offset < total_processed) + while (offset < bytes_read) { - const size_t chunk_size = - (total_processed - offset > CHUNK_SIZE) ? CHUNK_SIZE : (total_processed - offset); + size_t chunk_size = (bytes_read - offset > CHUNK_SIZE) ? CHUNK_SIZE : (bytes_read - offset); + + StatusMessage message = StatusMessage_init_zero; + message.which_payload = StatusMessage_uart_data_tag; + struct bytes_arg arg = {.data = data_buf + offset, .len = chunk_size}; + message.payload.uart_data.data.funcs.encode = &encode_bytes_callback; + message.payload.uart_data.data.arg = &arg; + + pb_ostream_t stream = pb_ostream_from_buffer(pb_buffer, sizeof(pb_buffer)); + if (!pb_encode(&stream, StatusMessage_fields, &message)) + { + ESP_LOGE(TAG, "Failed to encode uart data: %s", PB_GET_ERROR(&stream)); + offset += chunk_size; + continue; + } struct ws_message msg; msg.type = WS_MSG_UART; - msg.content.uart.data = malloc(chunk_size); - if (!msg.content.uart.data) + msg.len = stream.bytes_written; + msg.data = malloc(msg.len); + + if (!msg.data) { ESP_LOGE(TAG, "Failed to allocate memory for uart ws msg"); - break; + offset += chunk_size; + continue; } - memcpy(msg.content.uart.data, data_buf + offset, chunk_size); - msg.content.uart.len = chunk_size; + memcpy(msg.data, pb_buffer, msg.len); - if (xQueueSend(ws_queue, &msg, 0) != pdPASS) + if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(10)) != pdPASS) { - if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(5)) != pdPASS) - { - ESP_LOGW(TAG, "ws sender queue full, dropping %zu bytes", chunk_size); - free(msg.content.uart.data); - } + ESP_LOGW(TAG, "ws sender queue full, dropping %zu bytes", chunk_size); + free(msg.data); } offset += chunk_size; } } - - if (available_len > 0) - { - vTaskDelay(MIN_POLLING_INTERVAL); - } - else - { - vTaskDelay(current_interval); - } } - vTaskDelete(NULL); } @@ -267,20 +199,17 @@ static esp_err_t ws_handler(httpd_req_t* req) xSemaphoreTake(client_fd_mutex, portMAX_DELAY); if (client_fd > 0) { - // A client is already connected. Reject the new connection. ESP_LOGW(TAG, "Another client tried to connect, but a session is already active. Rejecting."); xSemaphoreGive(client_fd_mutex); httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Another client is already connected"); return ESP_FAIL; } - // No client is connected. Accept the new one. int new_fd = httpd_req_to_sockfd(req); ESP_LOGI(TAG, "Accepting new websocket connection: %d", new_fd); client_fd = new_fd; xSemaphoreGive(client_fd_mutex); - // Reset queue and flush UART buffer for the new session xQueueReset(ws_queue); uart_flush_input(UART_NUM); return ESP_OK; @@ -312,7 +241,6 @@ static esp_err_t ws_handler(httpd_req_t* req) void register_ws_endpoint(httpd_handle_t server) { size_t baud_rate_len; - nconfig_get_str_len(UART_BAUD_RATE, &baud_rate_len); char buf[baud_rate_len]; nconfig_read(UART_BAUD_RATE, buf, baud_rate_len); @@ -323,32 +251,38 @@ void register_ws_endpoint(httpd_handle_t server) .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - // .source_clk = UART_SCLK_APB, }; ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config)); ESP_ERROR_CHECK(uart_set_pin(UART_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); - ESP_ERROR_CHECK(uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 0, NULL, 0)); + ESP_ERROR_CHECK(uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 20, NULL, 0)); httpd_uri_t ws = {.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .user_ctx = NULL, .is_websocket = true}; httpd_register_uri_handler(server, &ws); client_fd_mutex = xSemaphoreCreateMutex(); - ws_queue = xQueueCreate(10, sizeof(struct ws_message)); // Combined queue + ws_queue = xQueueCreate(10, sizeof(struct ws_message)); xTaskCreate(uart_polling_task, "uart_polling_task", 1024 * 4, NULL, 8, NULL); xTaskCreate(unified_ws_sender_task, "ws_sender_task", 1024 * 6, server, 9, NULL); } -void push_data_to_ws(cJSON* data) +void push_data_to_ws(const uint8_t* data, size_t len) { struct ws_message msg; msg.type = WS_MSG_STATUS; - msg.content.status.data = data; + msg.data = malloc(len); + if (!msg.data) + { ESP_LOGE(TAG, "Failed to allocate memory for status ws msg"); + return; + } + memcpy(msg.data, data, len); + msg.len = len; + if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(10)) != pdPASS) { ESP_LOGW(TAG, "WS queue full, dropping status message"); - cJSON_Delete(data); + free(msg.data); } } diff --git a/page/package-lock.json b/page/package-lock.json index 294f8a3..0dbf89c 100644 --- a/page/package-lock.json +++ b/page/package-lock.json @@ -12,14 +12,62 @@ "@xterm/xterm": "^5.5.0", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", - "chart.js": "^4.4.3" + "chart.js": "^4.4.3", + "protobufjs": "^7.5.4" }, "devDependencies": { + "protobufjs-cli": "^1.1.2", "vite": "^7.0.4", "vite-plugin-compression": "^0.5.1", "vite-plugin-singlefile": "^2.0.1" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -436,6 +484,18 @@ "node": ">=18" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -451,6 +511,60 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", @@ -717,6 +831,36 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dependencies": { + "undici-types": "~7.10.0" + } + }, "node_modules/@xterm/addon-fit": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0.tgz", @@ -730,6 +874,27 @@ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -745,6 +910,24 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/bootstrap": { "version": "5.3.7", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", @@ -778,6 +961,15 @@ } ] }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -790,6 +982,18 @@ "node": ">=8" } }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -852,6 +1056,24 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -893,6 +1115,112 @@ "@esbuild/win32-x64": "0.25.8" } }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -933,6 +1261,12 @@ "node": ">=12" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -947,6 +1281,26 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -962,6 +1316,23 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -971,6 +1342,44 @@ "node": ">=0.12.0" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", + "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -983,6 +1392,93 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -1008,6 +1504,39 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1032,6 +1561,32 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1078,6 +1633,84 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs-cli": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz", + "integrity": "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/rollup": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", @@ -1117,6 +1750,28 @@ "fsevents": "~2.3.2" } }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1126,6 +1781,18 @@ "node": ">=0.10.0" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1154,6 +1821,15 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1166,6 +1842,47 @@ "node": ">=8.0" } }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -1278,6 +1995,27 @@ "rollup": "^4.44.1", "vite": "^5.4.11 || ^6.0.0 || ^7.0.0" } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true } } } diff --git a/page/package.json b/page/package.json index 75f8356..6202b2f 100644 --- a/page/package.json +++ b/page/package.json @@ -4,20 +4,23 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" + "dev": "npm run build:proto && vite", + "build": "npm run build:proto && vite build", + "preview": "vite preview", + "build:proto": "pbjs -t static-module -w es6 -o src/proto.js ../proto/status.proto" }, "devDependencies": { + "protobufjs-cli": "^1.1.2", "vite": "^7.0.4", - "vite-plugin-singlefile": "^2.0.1", - "vite-plugin-compression": "^0.5.1" + "vite-plugin-compression": "^0.5.1", + "vite-plugin-singlefile": "^2.0.1" }, "dependencies": { + "@xterm/addon-fit": "^0.9.0", "@xterm/xterm": "^5.5.0", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", - "@xterm/addon-fit": "^0.9.0", - "chart.js": "^4.4.3" + "chart.js": "^4.4.3", + "protobufjs": "^7.5.4" } } diff --git a/page/src/main.js b/page/src/main.js index ffafa7d..2d32cd3 100644 --- a/page/src/main.js +++ b/page/src/main.js @@ -10,9 +10,10 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap-icons/font/bootstrap-icons.css'; import './style.css'; -// --- Module Imports --- -import {initWebSocket} from './websocket.js'; -import {setupTerminal, term} from './terminal.js'; +// --- Module Imports -- - +import { StatusMessage } from './proto.js'; +import { initWebSocket } from './websocket.js'; +import { setupTerminal, term } from './terminal.js'; import { applyTheme, initUI, @@ -21,14 +22,13 @@ import { updateWebsocketStatus, updateWifiStatusUI } from './ui.js'; -import {setupEventListeners} from './events.js'; +import { setupEventListeners } from './events.js'; + +// --- Globals --- +// StatusMessage is imported directly from the generated proto.js file. // --- WebSocket Event Handlers --- -/** - * Callback function for when the WebSocket connection is successfully opened. - * Updates the UI to show an 'Online' status and fetches the initial control status. - */ function onWsOpen() { updateWebsocketStatus(true); if (term) { @@ -36,85 +36,86 @@ function onWsOpen() { } } -/** - * Callback function for when the WebSocket connection is closed. - * Updates the UI to show an 'Offline' status and attempts to reconnect after a delay. - */ function onWsClose() { updateWebsocketStatus(false); if (term) { term.write('\r\n\x1b[31mConnection closed. Reconnecting...\x1b[0m\r\n'); } - // Attempt to re-establish the connection after 2 seconds setTimeout(connect, 2000); } /** - * Callback function for when a message is received from the WebSocket server. - * It handles both JSON messages (for sensor and status updates) and binary data (for the terminal). + * Callback for when a message is received from the WebSocket server. + * This version includes extensive logging for debugging purposes. * @param {MessageEvent} event - The WebSocket message event. */ function onWsMessage(event) { - if (typeof event.data === 'string') { - try { - const message = JSON.parse(event.data); - if (message.type === 'sensor_data') { - updateSensorUI(message); - } else if (message.type === 'wifi_status') { - updateWifiStatusUI(message); + // Log any incoming message to the console for debugging. + + if (!(event.data instanceof ArrayBuffer)) { + console.warn('Message is not an ArrayBuffer, skipping protobuf decoding.'); + return; + } + + const buffer = new Uint8Array(event.data); + try { + const decodedMessage = StatusMessage.decode(buffer); + const payloadType = decodedMessage.payload; + + switch (payloadType) { + case 'sensorData': { + const sensorData = decodedMessage.sensorData; + if (sensorData) { + const sensorPayload = { + ...sensorData, + USB: sensorData.usb, + MAIN: sensorData.main, + VIN: sensorData.vin, + }; + // Log the exact data being sent to the UI function + updateSensorUI(sensorPayload); + } + break; } - } catch (e) { - // Ignore non-JSON string messages + + case 'wifiStatus': + updateWifiStatusUI(decodedMessage.wifiStatus); + break; + + case 'uartData': + if (term && decodedMessage.uartData && decodedMessage.uartData.data) { + term.write(decodedMessage.uartData.data); + } + break; + + default: + console.warn('Received message with unknown or empty payload type:', payloadType); + break; } - } else if (term && event.data instanceof ArrayBuffer) { - // Write raw UART data to the terminal - const data = new Uint8Array(event.data); - term.write(data); + } catch (e) { + console.error('Error decoding protobuf message:', e); } } + // --- Application Initialization --- -/** - * Establishes the connection-related parts of the application. - * Fetches initial status and initializes WebSocket. - */ function connect() { - // Fetch initial status on page load or reconnect updateControlStatus(); - - // Establish the WebSocket connection with the defined handlers - initWebSocket({ - onOpen: onWsOpen, - onClose: onWsClose, - onMessage: onWsMessage - }); + initWebSocket({ onOpen: onWsOpen, onClose: onWsClose, onMessage: onWsMessage }); } -/** - * Initializes the entire application. - * This function sets up the UI, theme, terminal, and event listeners. - * It should only be called once when the DOM is loaded. - */ function initialize() { - // Initialize basic UI components initUI(); - - // Set up the interactive components first setupTerminal(); - // Apply the saved theme or detect the user's preferred theme. const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); applyTheme(savedTheme); - // Attach all event listeners to the DOM elements setupEventListeners(); - // Start the connection process connect(); } // --- Start Application --- - -// Wait for the DOM to be fully loaded before initializing the application. document.addEventListener('DOMContentLoaded', initialize); diff --git a/proto/status.proto b/proto/status.proto new file mode 100644 index 0000000..5ca17d1 --- /dev/null +++ b/proto/status.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +// Represents data for a single sensor channel +message SensorChannelData { + float voltage = 1; + float current = 2; + float power = 3; +} + +// Contains data for all sensor channels and system info +message SensorData { + SensorChannelData usb = 1; + SensorChannelData main = 2; + SensorChannelData vin = 3; + uint32 timestamp = 4; + uint32 uptime_sec = 5; +} + +// Contains WiFi connection status +message WifiStatus { + bool connected = 1; + string ssid = 2; + int32 rssi = 3; +} + +// Contains raw UART data +message UartData { + bytes data = 1; +} + +// Top-level message for all websocket communication +message StatusMessage { + oneof payload { + SensorData sensor_data = 1; + WifiStatus wifi_status = 2; + UartData uart_data = 3; + } +} \ No newline at end of file