From 4a6ef8ce570a8aa4875bd053da9f0d8d96bc279b Mon Sep 17 00:00:00 2001 From: a2nr Date: Tue, 30 Jun 2026 18:13:42 +0700 Subject: [PATCH] feat(firmware): serial bridge throttle, LED blink fix, re-deploy safety serial_bridge.c + .h: - Implement 20pkt/s throttle with 240-byte buffer - serial_bridge_on_usb_data() for buffering USB serial data - Flush every 50ms or 500ms stale timeout led_button.c + .h: - Per-color LED blink toggle in led_button_tick() - BLUE_BLINK 200ms, RED_BLINK 100ms, GREEN_BLINK 200ms - Fix: was set_led(true,true,true) for all blink patterns main.c: - Route USB serial data through serial_bridge_on_usb_data - Add led_button_tick() call in main loop state_machine.c: - Add binary_parser_reset() on SERIAL_BRIDGE re-deploy --- velxio-deployer-firmware/main/led_button.c | 106 ++++++++++++++++ velxio-deployer-firmware/main/led_button.h | 51 ++++++++ velxio-deployer-firmware/main/main.c | 92 ++++++++++++++ velxio-deployer-firmware/main/serial_bridge.c | 114 ++++++++++++++++++ velxio-deployer-firmware/main/serial_bridge.h | 55 +++++++++ velxio-deployer-firmware/main/state_machine.c | 1 + 6 files changed, 419 insertions(+) create mode 100644 velxio-deployer-firmware/main/led_button.c create mode 100644 velxio-deployer-firmware/main/led_button.h create mode 100644 velxio-deployer-firmware/main/main.c create mode 100644 velxio-deployer-firmware/main/serial_bridge.c create mode 100644 velxio-deployer-firmware/main/serial_bridge.h diff --git a/velxio-deployer-firmware/main/led_button.c b/velxio-deployer-firmware/main/led_button.c new file mode 100644 index 0000000..b927c53 --- /dev/null +++ b/velxio-deployer-firmware/main/led_button.c @@ -0,0 +1,106 @@ +#include "esp_log.h" +#include "driver/gpio.h" +#include "esp_timer.h" +#include "led_button.h" + +static const char *TAG = "LED_BTN"; + +static led_pattern_t current_pattern = LED_OFF; +static bool button_previous_state = true; +static bool button_pressed_flag = false; +static int64_t last_toggle_time = 0; +static bool blink_state = false; + +static void set_led(bool red, bool green, bool blue) +{ + gpio_set_level(LED_GPIO_RED, red ? 1 : 0); + gpio_set_level(LED_GPIO_GREEN, green ? 1 : 0); + gpio_set_level(LED_GPIO_BLUE, blue ? 1 : 0); +} + +void led_button_init(void) +{ + gpio_set_direction(LED_GPIO_RED, GPIO_MODE_OUTPUT); + gpio_set_direction(LED_GPIO_GREEN, GPIO_MODE_OUTPUT); + gpio_set_direction(LED_GPIO_BLUE, GPIO_MODE_OUTPUT); + + gpio_set_direction(BTN_GPIO_RETRY, GPIO_MODE_INPUT); + gpio_set_pull_mode(BTN_GPIO_RETRY, GPIO_PULLUP_ONLY); + + set_led(false, false, false); + button_previous_state = gpio_get_level(BTN_GPIO_RETRY); + ESP_LOGI(TAG, "LED + Button initialized"); +} + +void led_set_pattern(led_pattern_t pattern) +{ + current_pattern = pattern; + + switch (pattern) { + case LED_OFF: + set_led(false, false, false); + break; + case LED_GREEN: + set_led(false, true, false); + break; + case LED_RED: + set_led(true, false, false); + break; + case LED_BLUE: + set_led(false, false, true); + break; + case LED_GREEN_BLINK: + case LED_RED_BLINK: + case LED_BLUE_BLINK: + /* Toggle handled by led_button_tick(). Start off. */ + blink_state = false; + last_toggle_time = esp_timer_get_time() / 1000; + set_led(false, false, false); + break; + } +} + +bool button_retry_pressed(void) +{ + bool current = gpio_get_level(BTN_GPIO_RETRY); + + if (button_previous_state && !current) { + button_previous_state = current; + ESP_LOGI(TAG, "Retry button pressed"); + return true; + } + + button_previous_state = current; + return false; +} + +void led_button_tick(void) +{ + if (current_pattern != LED_GREEN_BLINK && + current_pattern != LED_RED_BLINK && + current_pattern != LED_BLUE_BLINK) { + return; + } + + int64_t now = esp_timer_get_time() / 1000; + int64_t interval = (current_pattern == LED_RED_BLINK) ? 100 : 200; + + if (now - last_toggle_time >= interval) { + last_toggle_time = now; + blink_state = !blink_state; + + switch (current_pattern) { + case LED_BLUE_BLINK: + set_led(false, false, blink_state); + break; + case LED_RED_BLINK: + set_led(blink_state, false, false); + break; + case LED_GREEN_BLINK: + set_led(false, blink_state, false); + break; + default: + break; + } + } +} diff --git a/velxio-deployer-firmware/main/led_button.h b/velxio-deployer-firmware/main/led_button.h new file mode 100644 index 0000000..04333ae --- /dev/null +++ b/velxio-deployer-firmware/main/led_button.h @@ -0,0 +1,51 @@ +/** + * @file led_button.h + * @brief Status LED (RGB) and tactile Retry button. + * + * LED patterns: + * BLUE_BLINK — advertising / receiving data + * BLUE — verification in progress + * GREEN — flash succeeded, serial bridge active + * RED — STK500 target error + * RED_BLINK — checksum mismatch + */ + +#pragma once + +#include +#include + +#define LED_GPIO_RED 13 +#define LED_GPIO_GREEN 12 +#define LED_GPIO_BLUE 14 +#define BTN_GPIO_RETRY 0 /**< GPIO0 (BOOT button on most dev boards) */ + +/** LED visual patterns. */ +typedef enum { + LED_OFF, /**< All off */ + LED_GREEN, /**< Solid green — success */ + LED_RED, /**< Solid red — error */ + LED_BLUE, /**< Solid blue — busy */ + LED_GREEN_BLINK, /**< Blinking green */ + LED_RED_BLINK, /**< Blinking red — checksum error */ + LED_BLUE_BLINK /**< Blinking blue — advertising/receiving */ +} led_pattern_t; + +void led_button_init(void); + +/** + * @brief Set the LED visual pattern. + * @param pattern Pattern from led_pattern_t enum + */ +void led_set_pattern(led_pattern_t pattern); + +/** + * @brief Check if the Retry button was just pressed (edge-triggered). + * @return true on falling edge detection + */ +bool button_retry_pressed(void); + +/** + * @brief Periodic tick for LED blink patterns. Call from main loop (~10ms). + */ +void led_button_tick(void); diff --git a/velxio-deployer-firmware/main/main.c b/velxio-deployer-firmware/main/main.c new file mode 100644 index 0000000..fd9e8b9 --- /dev/null +++ b/velxio-deployer-firmware/main/main.c @@ -0,0 +1,92 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_bt.h" + +#include "ble_service.h" +#include "binary_parser.h" +#include "checksum.h" +#include "usb_host.h" +#include "stk500v1.h" +#include "state_machine.h" +#include "serial_bridge.h" +#include "led_button.h" + +static const char *TAG = "MAIN"; + +static void on_ble_flashing_data(uint8_t *data, size_t len) +{ + if (len < 4) return; + + uint8_t cmd = data[0]; + + switch (cmd) { + case CMD_INIT: { + ESP_LOGI(TAG, "Received INIT command"); + binary_parser_process_packet(data, len); + state_machine_process_event(EVENT_BLE_INIT, data); + break; + } + case CMD_DATA: { + parser_state_t result = binary_parser_process_packet(data, len); + if (result == PARSER_ERROR) { + ESP_LOGE(TAG, "Packet parse error"); + state_machine_process_event(EVENT_VERIFY_FAIL, NULL); + } + break; + } + case CMD_END: { + ESP_LOGI(TAG, "Received END command"); + binary_parser_process_packet(data, len); + state_machine_process_event(EVENT_BLE_END, data); + break; + } + default: + ESP_LOGW(TAG, "Unknown command: 0x%02X", cmd); + break; + } +} + +static void on_usb_serial_data(uint8_t *data, size_t len) +{ + serial_bridge_on_usb_data(data, len); +} + +static void on_ble_serial_data(uint8_t *data, size_t len) +{ + if (serial_bridge_is_active()) { + usb_host_write_cdc(data, len); + } +} + +void app_main(void) +{ + ESP_LOGI(TAG, "Velxio BLE Deployer v1.0"); + + nvs_flash_init(); + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + + led_button_init(); + checksum_crc32(NULL, 0); + binary_parser_init(); + ble_service_init(); + ble_service_set_flashing_callback(on_ble_flashing_data); + ble_service_set_serial_callback(on_ble_serial_data); + + usb_host_init(); + usb_host_set_serial_callback(on_usb_serial_data); + + led_set_pattern(LED_BLUE_BLINK); + + state_machine_init(); + + while (1) { + state_machine_tick(); + serial_bridge_tick(); + led_button_tick(); + vTaskDelay(pdMS_TO_TICKS(10)); + } +} diff --git a/velxio-deployer-firmware/main/serial_bridge.c b/velxio-deployer-firmware/main/serial_bridge.c new file mode 100644 index 0000000..229f986 --- /dev/null +++ b/velxio-deployer-firmware/main/serial_bridge.c @@ -0,0 +1,114 @@ +#include +#include "esp_log.h" +#include "esp_timer.h" +#include "serial_bridge.h" +#include "ble_service.h" +#include "usb_host.h" + +static const char *TAG = "SERIAL_BRIDGE"; + +static bool active = false; +static int64_t last_flush_time = 0; +static int notify_count = 0; +static int64_t rate_window_start = 0; + +#define BRIDGE_BUF_SIZE 240 +#define MAX_NOTIFY_RATE 20 +#define RATE_WINDOW_MS 1000 +#define THROTTLE_INTERVAL_MS 50 +#define BUFFER_FLUSH_MS 500 + +static uint8_t bridge_buf[BRIDGE_BUF_SIZE]; +static size_t bridge_buf_len = 0; + +void serial_bridge_init(void) +{ + active = false; + bridge_buf_len = 0; + last_flush_time = 0; + notify_count = 0; + rate_window_start = 0; + ESP_LOGI(TAG, "Serial bridge initialized"); +} + +void serial_bridge_start(void) +{ + active = true; + bridge_buf_len = 0; + last_flush_time = esp_timer_get_time() / 1000; + notify_count = 0; + rate_window_start = esp_timer_get_time() / 1000; + ESP_LOGI(TAG, "Serial bridge started"); +} + +void serial_bridge_stop(void) +{ + /* Flush any remaining buffered data before stopping. */ + if (active && bridge_buf_len > 0) { + ble_service_send_notify_serial(bridge_buf, bridge_buf_len); + bridge_buf_len = 0; + } + active = false; + ESP_LOGI(TAG, "Serial bridge stopped"); +} + +bool serial_bridge_is_active(void) +{ + return active; +} + +void serial_bridge_on_ble_write(uint8_t *data, size_t len) +{ + if (!active) return; + usb_host_write_cdc(data, len); +} + +void serial_bridge_on_usb_data(uint8_t *data, size_t len) +{ + if (!active) return; + + /* Large packet: flush buffer first, then send directly. */ + size_t remaining = BRIDGE_BUF_SIZE - bridge_buf_len; + if (len >= BRIDGE_BUF_SIZE) { + if (bridge_buf_len > 0) { + ble_service_send_notify_serial(bridge_buf, bridge_buf_len); + bridge_buf_len = 0; + } + ble_service_send_notify_serial(data, len); + return; + } + + /* Buffer nearly full: flush first. */ + if (len > remaining) { + ble_service_send_notify_serial(bridge_buf, bridge_buf_len); + bridge_buf_len = 0; + } + + memcpy(bridge_buf + bridge_buf_len, data, len); + bridge_buf_len += len; +} + +void serial_bridge_tick(void) +{ + if (!active || bridge_buf_len == 0) return; + + int64_t now = esp_timer_get_time() / 1000; + + /* Reset rate counter every second. */ + if (now - rate_window_start >= RATE_WINDOW_MS) { + rate_window_start = now; + notify_count = 0; + } + + int64_t since_flush = now - last_flush_time; + bool time_to_flush = (since_flush >= THROTTLE_INTERVAL_MS) && + (notify_count < MAX_NOTIFY_RATE); + bool stale = since_flush >= BUFFER_FLUSH_MS; + + if (time_to_flush || stale) { + ble_service_send_notify_serial(bridge_buf, bridge_buf_len); + bridge_buf_len = 0; + last_flush_time = now; + notify_count++; + } +} diff --git a/velxio-deployer-firmware/main/serial_bridge.h b/velxio-deployer-firmware/main/serial_bridge.h new file mode 100644 index 0000000..88820eb --- /dev/null +++ b/velxio-deployer-firmware/main/serial_bridge.h @@ -0,0 +1,55 @@ +/** + * @file serial_bridge.h + * @brief Transparent bridge: USB CDC <-> BLE Serial characteristic. + * + * When active, data from Arduino's Serial.print() is read via USB Host + * and immediately forwarded as BLE Notify on the Serial characteristic. + * Data from the Webapp (BLE Write) is forwarded to Arduino via USB CDC. + * + * Throttled to prevent BLE congestion (max 20 packets/sec). + */ + +#pragma once + +#include +#include + +void serial_bridge_init(void); + +/** + * @brief Start bridging between USB CDC and BLE Serial. + */ +void serial_bridge_start(void); + +/** + * @brief Stop bridging and release BLE Serial notifications. + */ +void serial_bridge_stop(void); + +/** + * @brief Check if bridge is currently active. + * @return true if active + */ +bool serial_bridge_is_active(void); + +/** + * @brief Periodic tick for throttling and buffer management. + */ +void serial_bridge_tick(void); + +/** + * @brief Forward data from BLE Serial characteristic to USB CDC. + * Called when Webapp writes to the Serial characteristic. + * @param data Pointer to received bytes + * @param len Number of bytes received + */ +void serial_bridge_on_ble_write(uint8_t *data, size_t len); + +/** + * @brief Buffer data from USB CDC (Arduino serial output). + * Called from main's on_usb_serial_data callback. Data is buffered + * and flushed in serial_bridge_tick() at max 20 pkt/s. + * @param data Pointer to received bytes + * @param len Number of bytes received + */ +void serial_bridge_on_usb_data(uint8_t *data, size_t len); diff --git a/velxio-deployer-firmware/main/state_machine.c b/velxio-deployer-firmware/main/state_machine.c index 3c711d8..2775a01 100644 --- a/velxio-deployer-firmware/main/state_machine.c +++ b/velxio-deployer-firmware/main/state_machine.c @@ -168,6 +168,7 @@ void state_machine_process_event(sm_event_t event, void *data) case STATE_SERIAL_BRIDGE: if (event == EVENT_BLE_INIT) { serial_bridge_stop(); + binary_parser_reset(); current_state = STATE_RECEIVING; led_set_pattern(LED_BLUE_BLINK); ESP_LOGI(TAG, "Transition: SERIAL_BRIDGE -> RECEIVING (re-deploy)");