From 89319125839f9b71dda75878a9806f048e2a4fe2 Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 19 May 2026 21:53:10 +0200 Subject: [PATCH] Dim LED ring, add blink mode, and signal OTA outcome on the ring. Default brightness is ~5%; UART blink mode and green/red pulses mark OTA success or failure. Failed UART uploads skip ESP-NOW distribution. Co-authored-by: Cursor --- goTool/README.md | 2 +- goTool/cmd_led_ring.go | 27 ++++++--- goTool/pb/uart_messages.pb.go | 33 ++++++++-- main/README.md | 8 ++- main/cmd_led_ring.c | 34 ++++++++--- main/cmd_ota.c | 92 +++++++++++++++++++++------- main/led_ring.c | 108 ++++++++++++++++++++++++++++++--- main/led_ring.h | 25 +++++++- main/ota_espnow.c | 24 ++++++++ main/ota_uart.c | 4 ++ main/ota_uart.h | 6 ++ main/proto/uart_messages.pb.h | 22 ++++--- main/proto/uart_messages.proto | 10 ++- 13 files changed, 324 insertions(+), 71 deletions(-) diff --git a/goTool/README.md b/goTool/README.md index 910fa1c..da75324 100644 --- a/goTool/README.md +++ b/goTool/README.md @@ -29,7 +29,7 @@ go run . -port /dev/ttyUSB0 clients | `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) | | `ota` | 16–19 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW | | `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) | -| `led-ring` | 8 | LED ring: `-mode clear\|progress\|digit`, `-progress`, `-digit`, RGB, `-intensity` | +| `led-ring` | 8 | LED ring: `-mode clear\|progress\|digit\|blink`, `-progress`, `-digit`, RGB, `-intensity` (0 = ~5 %), `-blink-ms`, `-blink-count` | `clients` requires slaves to have responded to master discover broadcasts first. diff --git a/goTool/cmd_led_ring.go b/goTool/cmd_led_ring.go index 34f9624..7dd2dac 100644 --- a/goTool/cmd_led_ring.go +++ b/goTool/cmd_led_ring.go @@ -11,17 +11,20 @@ const ( ledRingModeClear = 0 ledRingModeProgress = 1 ledRingModeDigit = 2 + ledRingModeBlink = 3 ) func runLedRing(sp *serialPort, args []string) error { fs := flag.NewFlagSet("led-ring", flag.ExitOnError) - mode := fs.String("mode", "progress", "clear, progress, or digit") + mode := fs.String("mode", "progress", "clear, progress, digit, or blink") progress := fs.Uint("progress", 0, "fill level 0–100 (mode=progress)") digit := fs.Uint("digit", 0, "digit 0–10 (mode=digit)") r := fs.Uint("r", 0, "red 0–255") g := fs.Uint("g", 255, "green 0–255") b := fs.Uint("b", 0, "blue 0–255") - intensity := fs.Uint("intensity", 255, "brightness 0–255") + intensity := fs.Uint("intensity", 0, "brightness 0–255 (0 = device default ~5%)") + blinkMs := fs.Uint("blink-ms", 350, "pulse length in ms (mode=blink)") + blinkCount := fs.Uint("blink-count", 1, "number of pulses (mode=blink)") if err := fs.Parse(args); err != nil { return err } @@ -34,18 +37,22 @@ func runLedRing(sp *serialPort, args []string) error { modeVal = ledRingModeProgress case "digit": modeVal = ledRingModeDigit + case "blink": + modeVal = ledRingModeBlink default: - return fmt.Errorf("unknown -mode %q (clear, progress, digit)", *mode) + return fmt.Errorf("unknown -mode %q (clear, progress, digit, blink)", *mode) } resp, err := sp.ledRingProgress(&pb.LedRingProgressRequest{ - Mode: modeVal, - Progress: uint32(*progress), - Digit: uint32(*digit), - R: uint32(*r), - G: uint32(*g), - B: uint32(*b), - Intensity: uint32(*intensity), + Mode: modeVal, + Progress: uint32(*progress), + Digit: uint32(*digit), + R: uint32(*r), + G: uint32(*g), + B: uint32(*b), + Intensity: uint32(*intensity), + BlinkMs: uint32(*blinkMs), + BlinkCount: uint32(*blinkCount), }) if err != nil { return err diff --git a/goTool/pb/uart_messages.pb.go b/goTool/pb/uart_messages.pb.go index 3e91d77..e9c8a11 100644 --- a/goTool/pb/uart_messages.pb.go +++ b/goTool/pb/uart_messages.pb.go @@ -1068,8 +1068,8 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 { return 0 } -// Host → device: LED ring display (progress bar, digit, or clear). -// mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10, same layout as firmware digit maps). +// Host → device: LED ring display (progress bar, digit, clear, or blink). +// mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10), 3=blink full ring. type LedRingProgressRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Mode uint32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"` @@ -1080,8 +1080,12 @@ type LedRingProgressRequest struct { R uint32 `protobuf:"varint,4,opt,name=r,proto3" json:"r,omitempty"` G uint32 `protobuf:"varint,5,opt,name=g,proto3" json:"g,omitempty"` B uint32 `protobuf:"varint,6,opt,name=b,proto3" json:"b,omitempty"` - // * 0–255 brightness scale applied to r/g/b - Intensity uint32 `protobuf:"varint,7,opt,name=intensity,proto3" json:"intensity,omitempty"` + // * 0–255 brightness scale; 0 = firmware default (~5 %) + Intensity uint32 `protobuf:"varint,7,opt,name=intensity,proto3" json:"intensity,omitempty"` + // * Pulse length in ms (mode=blink, default 350) + BlinkMs uint32 `protobuf:"varint,8,opt,name=blink_ms,json=blinkMs,proto3" json:"blink_ms,omitempty"` + // * Number of pulses (mode=blink, default 1) + BlinkCount uint32 `protobuf:"varint,9,opt,name=blink_count,json=blinkCount,proto3" json:"blink_count,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1165,6 +1169,20 @@ func (x *LedRingProgressRequest) GetIntensity() uint32 { return 0 } +func (x *LedRingProgressRequest) GetBlinkMs() uint32 { + if x != nil { + return x.BlinkMs + } + return 0 +} + +func (x *LedRingProgressRequest) GetBlinkCount() uint32 { + if x != nil { + return x.BlinkCount + } + return 0 +} + type LedRingProgressResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` @@ -1707,7 +1725,7 @@ const file_uart_messages_proto_rawDesc = "" + "\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" + "\x19EspNowUnicastTestResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\n" + - "\x03seq\x18\x02 \x01(\rR\x03seq\"\xa6\x01\n" + + "\x03seq\x18\x02 \x01(\rR\x03seq\"\xe2\x01\n" + "\x16LedRingProgressRequest\x12\x12\n" + "\x04mode\x18\x01 \x01(\rR\x04mode\x12\x1a\n" + "\bprogress\x18\x02 \x01(\rR\bprogress\x12\x14\n" + @@ -1715,7 +1733,10 @@ const file_uart_messages_proto_rawDesc = "" + "\x01r\x18\x04 \x01(\rR\x01r\x12\f\n" + "\x01g\x18\x05 \x01(\rR\x01g\x12\f\n" + "\x01b\x18\x06 \x01(\rR\x01b\x12\x1c\n" + - "\tintensity\x18\a \x01(\rR\tintensity\"y\n" + + "\tintensity\x18\a \x01(\rR\tintensity\x12\x19\n" + + "\bblink_ms\x18\b \x01(\rR\ablinkMs\x12\x1f\n" + + "\vblink_count\x18\t \x01(\rR\n" + + "blinkCount\"y\n" + "\x17LedRingProgressResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x12\n" + "\x04mode\x18\x02 \x01(\rR\x04mode\x12\x1a\n" + diff --git a/main/README.md b/main/README.md index 6bbf7c1..fd6aa7c 100644 --- a/main/README.md +++ b/main/README.md @@ -253,6 +253,8 @@ Inactive app partition is selected with `esp_ota_get_next_update_partition()`; ` `OTA_END` can take a long time on the wire (slave flash + ESP-NOW); the host should use a generous read timeout. +During OTA the LED ring shows progress at ~5 % brightness: **blue** while the image is written (UART on master, ESP-NOW on slaves), **green** on the master while it forwards the image to slaves over ESP-NOW. On **success** the ring gives one short **green** blink; on **failure** one **red** blink and ESP-NOW distribution is not started (failed UART upload / `OTA_END` validation). + `OTA_START_ESPNOW` (type `20`): re-run ESP-NOW distribution from the last staged image without a new UART upload (no-op if nothing staged). Implementation: `ota_uart.c` (4 KiB buffer, `esp_ota_write`), `ota_espnow.c`, `cmd_ota.c`. @@ -319,11 +321,12 @@ Control the 95-LED ring from the host. The firmware **does not** animate digits | Field | Meaning | |-------|---------| -| `mode` | `0` = clear, `1` = progress bar, `2` = digit | +| `mode` | `0` = clear, `1` = progress bar, `2` = digit, `3` = blink full ring | | `progress` | 0–100 (% of ring lit, mode `1`) | | `digit` | 0–10 (mode `2`, same segment maps as built-in digits) | | `r`, `g`, `b` | Color 0–255 | -| `intensity` | Brightness 0–255 (scaled into RGB; `0` → 255) | +| `intensity` | Brightness 0–255 (scaled into RGB; `0` → firmware default ~5 %) | +| `blink_ms`, `blink_count` | Pulse length and count (mode `3`; defaults 350 ms, 1) | **Response:** `led_ring_progress_response` (`success`, `mode`, `progress`, `digit`). @@ -331,6 +334,7 @@ Control the 95-LED ring from the host. The firmware **does not** animate digits go run . -port /dev/ttyUSB0 led-ring -mode progress -progress 75 -g 80 -b 255 go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 7 -r 255 -g 200 go run . -port /dev/ttyUSB0 led-ring -mode clear +go run . -port /dev/ttyUSB0 led-ring -mode blink -g 255 -blink-count 2 ``` ### CLIENT_INFO command diff --git a/main/cmd_led_ring.c b/main/cmd_led_ring.c index c9b3b14..8036f5b 100644 --- a/main/cmd_led_ring.c +++ b/main/cmd_led_ring.c @@ -8,6 +8,7 @@ static const char *TAG = "[LED_RING_CMD]"; #define LED_RING_MODE_CLEAR 0 #define LED_RING_MODE_PROGRESS 1 #define LED_RING_MODE_DIGIT 2 +#define LED_RING_MODE_BLINK 3 static uint8_t clamp_u8(uint32_t v) { if (v > 255) { @@ -23,13 +24,11 @@ static uint8_t clamp_progress(uint32_t v) { return (uint8_t)v; } -static void apply_intensity(uint8_t *r, uint8_t *g, uint8_t *b, uint8_t intensity) { +static uint8_t resolve_intensity(uint32_t intensity) { if (intensity == 0) { - return; + return LED_RING_DEFAULT_INTENSITY; } - *r = (uint16_t)(*r) * intensity / 255; - *g = (uint16_t)(*g) * intensity / 255; - *b = (uint16_t)(*b) * intensity / 255; + return clamp_u8(intensity); } static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit) { @@ -64,11 +63,7 @@ static void handle_led_ring(const uint8_t *data, size_t len) { uint8_t r = clamp_u8(req.r); uint8_t g = clamp_u8(req.g); uint8_t b = clamp_u8(req.b); - uint8_t intensity = clamp_u8(req.intensity); - if (intensity == 0) { - intensity = 255; - } - apply_intensity(&r, &g, &b, intensity); + uint8_t intensity = resolve_intensity(req.intensity); led_command_t cmd = {0}; @@ -106,6 +101,7 @@ static void handle_led_ring(const uint8_t *data, size_t len) { cmd.r = r; cmd.g = g; cmd.b = b; + cmd.intensity = intensity; led_ring_send_command(&cmd); ESP_LOGI(TAG, "digit %u rgb=%u,%u,%u", (unsigned)cmd.value, (unsigned)r, (unsigned)g, (unsigned)b); @@ -113,6 +109,24 @@ static void handle_led_ring(const uint8_t *data, size_t len) { return; } + case LED_RING_MODE_BLINK: { + cmd.mode = LED_CMD_BLINK; + cmd.r = r; + cmd.g = g; + cmd.b = b; + cmd.intensity = intensity; + cmd.blink_ms = (uint16_t)(req.blink_ms > 0 ? req.blink_ms : 350); + cmd.blink_count = req.blink_count > 0 ? (uint8_t)req.blink_count : 1; + if (cmd.blink_count == 0) { + cmd.blink_count = 1; + } + led_ring_send_command(&cmd); + ESP_LOGI(TAG, "blink x%u %u ms rgb=%u,%u,%u", (unsigned)cmd.blink_count, + (unsigned)cmd.blink_ms, (unsigned)r, (unsigned)g, (unsigned)b); + reply(true, mode, 0, 0); + return; + } + default: ESP_LOGW(TAG, "unknown mode %lu", (unsigned long)mode); reply(false, mode, 0, 0); diff --git a/main/cmd_ota.c b/main/cmd_ota.c index 8db3091..c3a236b 100644 --- a/main/cmd_ota.c +++ b/main/cmd_ota.c @@ -1,4 +1,5 @@ #include "cmd_ota.h" +#include "led_ring.h" #include "ota_espnow.h" #include "ota_uart.h" #include "uart_cmd.h" @@ -15,6 +16,15 @@ static const char *TAG = "[OTA_CMD]"; #define OTA_DIST_STACK 8192 #define OTA_DIST_PRIO 5 +/** UART OTA upload to this node (master). */ +#define OTA_LED_UART_R 0 +#define OTA_LED_UART_G 0 +#define OTA_LED_UART_B 255 +/** ESP-NOW distribution from master to slaves. */ +#define OTA_LED_ESPNOW_TX_R 0 +#define OTA_LED_ESPNOW_TX_G 255 +#define OTA_LED_ESPNOW_TX_B 0 + typedef struct { uint32_t written; int slot; @@ -32,6 +42,11 @@ static void send_ota_status(ota_uart_status_t status, uint32_t err_code) { uart_cmd_send(&response, TAG); } +static void send_ota_failed(uint32_t err_code) { + led_ring_ota_failed(); + send_ota_status(OTA_UART_ST_FAILED, err_code); +} + static void send_ota_distributing(uint32_t kind, uint32_t bytes_done, uint32_t target_slot) { alox_UartMessage response; @@ -46,7 +61,9 @@ static void send_ota_distributing(uint32_t kind, uint32_t bytes_done, static void ota_dist_aggregate(uint32_t bytes_done, uint32_t total_bytes, uint8_t slave_count) { - (void)total_bytes; + (void)slave_count; + led_ring_show_ota_progress(bytes_done, total_bytes, OTA_LED_ESPNOW_TX_R, + OTA_LED_ESPNOW_TX_G, OTA_LED_ESPNOW_TX_B); send_ota_distributing(OTA_DIST_AGGREGATE, bytes_done, (uint32_t)slave_count); } @@ -68,7 +85,7 @@ static void ota_prepare_task(void *param) { int slot = ota_uart_prepare(total_size); if (slot < 0) { - send_ota_status(OTA_UART_ST_FAILED, 1); + send_ota_failed(1); vTaskDelete(NULL); return; } @@ -82,6 +99,9 @@ static void ota_prepare_task(void *param) { response.payload.ota_status.error = 0; uart_cmd_send(&response, TAG); + led_ring_show_ota_progress(0, total_size, OTA_LED_UART_R, OTA_LED_UART_G, + OTA_LED_UART_B); + vTaskDelete(NULL); } @@ -90,7 +110,7 @@ static void handle_ota_start(const uint8_t *data, size_t len) { alox_OtaStartPayload req = alox_OtaStartPayload_init_zero; if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { - send_ota_status(OTA_UART_ST_FAILED, 2); + send_ota_failed( 2); return; } @@ -102,13 +122,13 @@ static void handle_ota_start(const uint8_t *data, size_t len) { if (req.total_size == 0) { ESP_LOGW(TAG, "OTA_START: total_size required"); - send_ota_status(OTA_UART_ST_FAILED, 3); + send_ota_failed( 3); return; } if (ota_uart_is_active()) { ESP_LOGW(TAG, "OTA_START while session active"); - send_ota_status(OTA_UART_ST_FAILED, 4); + send_ota_failed( 4); return; } @@ -116,7 +136,7 @@ static void handle_ota_start(const uint8_t *data, size_t len) { (void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create ota_prepare task"); - send_ota_status(OTA_UART_ST_FAILED, 5); + send_ota_failed( 5); } } @@ -125,7 +145,7 @@ static void handle_ota_payload(const uint8_t *data, size_t len) { if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { ESP_LOGW(TAG, "OTA_PAYLOAD decode failed"); - send_ota_status(OTA_UART_ST_FAILED, 10); + send_ota_failed( 10); return; } @@ -134,34 +154,47 @@ static void handle_ota_payload(const uint8_t *data, size_t len) { if (req_ptr == NULL) { ESP_LOGW(TAG, "OTA_PAYLOAD: missing ota_payload (which=%u)", (unsigned)uart_msg.which_payload); - send_ota_status(OTA_UART_ST_FAILED, 11); + send_ota_failed( 11); return; } if (req_ptr->data.size == 0) { ESP_LOGW(TAG, "OTA_PAYLOAD: empty data (seq=%lu)", (unsigned long)req_ptr->seq); - send_ota_status(OTA_UART_ST_FAILED, 11); + send_ota_failed( 11); return; } if (!ota_uart_is_active()) { ESP_LOGW(TAG, "OTA_PAYLOAD without active session (seq=%lu)", (unsigned long)req_ptr->seq); - send_ota_status(OTA_UART_ST_FAILED, 12); + send_ota_failed( 12); return; } ota_feed_result_t r = ota_uart_feed(req_ptr->data.bytes, req_ptr->data.size); if (r == OTA_FEED_ERROR) { - send_ota_status(OTA_UART_ST_FAILED, 13); + send_ota_failed( 13); return; } if (r == OTA_FEED_BLOCK_WRITTEN) { + uint32_t total = ota_uart_total_size(); + uint32_t done = ota_uart_bytes_written(); ESP_LOGI(TAG, "OTA block ack (%lu bytes in flash)", - (unsigned long)ota_uart_bytes_written()); + (unsigned long)done); + led_ring_show_ota_progress(done, total, OTA_LED_UART_R, OTA_LED_UART_G, + OTA_LED_UART_B); send_ota_status(OTA_UART_ST_BLOCK_ACK, 0); + return; + } + + if (r == OTA_FEED_OK) { + uint32_t total = ota_uart_total_size(); + if (total > 0) { + led_ring_show_ota_progress(ota_uart_bytes_received(), total, + OTA_LED_UART_R, OTA_LED_UART_G, OTA_LED_UART_B); + } } } @@ -175,19 +208,21 @@ static void ota_distribute_task(void *param) { const esp_partition_t *part = NULL; uint32_t image_size = 0; if (!ota_uart_get_staged_image(&part, &image_size)) { - send_ota_status(OTA_UART_ST_FAILED, 30); + send_ota_failed( 30); free(job); vTaskDelete(NULL); return; } + led_ring_show_ota_progress(0, image_size, OTA_LED_ESPNOW_TX_R, OTA_LED_ESPNOW_TX_G, + OTA_LED_ESPNOW_TX_B); send_ota_distributing(OTA_DIST_AGGREGATE, 0, 0); esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); if (err != ESP_OK) { ESP_LOGE(TAG, "slave OTA distribution failed: %s", esp_err_to_name(err)); ota_uart_clear_staged(); - send_ota_status(OTA_UART_ST_FAILED, 31); + send_ota_failed(31); free(job); vTaskDelete(NULL); return; @@ -195,12 +230,15 @@ static void ota_distribute_task(void *param) { err = ota_uart_apply_boot(); if (err != ESP_OK) { - send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); + send_ota_failed((uint32_t)err); free(job); vTaskDelete(NULL); return; } + led_ring_show_ota_progress(image_size, image_size, OTA_LED_ESPNOW_TX_R, + OTA_LED_ESPNOW_TX_G, OTA_LED_ESPNOW_TX_B); + alox_UartMessage response; uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS, alox_UartMessage_ota_status_tag); @@ -211,6 +249,7 @@ static void ota_distribute_task(void *param) { response.payload.ota_status.error = 0; uart_cmd_send(&response, TAG); + led_ring_ota_success(); free(job); vTaskDelete(NULL); } @@ -220,30 +259,36 @@ static void handle_ota_end(const uint8_t *data, size_t len) { (void)len; if (!ota_uart_is_active()) { - send_ota_status(OTA_UART_ST_FAILED, 20); + send_ota_failed( 20); return; } ota_dist_job_t *job = calloc(1, sizeof(*job)); if (job == NULL) { - send_ota_status(OTA_UART_ST_FAILED, 21); + send_ota_failed( 21); return; } job->written = ota_uart_bytes_written(); job->slot = ota_uart_target_slot(); + uint32_t uart_total = ota_uart_total_size(); bool success = false; esp_err_t err = ota_uart_finish(false, &success); if (err != ESP_OK || !success) { - send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); + send_ota_failed((uint32_t)err); free(job); return; } + if (uart_total > 0) { + led_ring_show_ota_progress(job->written, uart_total, OTA_LED_UART_R, + OTA_LED_UART_G, OTA_LED_UART_B); + } + if (xTaskCreate(ota_distribute_task, "ota_dist", OTA_DIST_STACK, job, OTA_DIST_PRIO, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create ota_dist task"); - send_ota_status(OTA_UART_ST_FAILED, 22); + send_ota_failed( 22); free(job); } } @@ -253,26 +298,26 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) { (void)len; if (ota_uart_is_active()) { - send_ota_status(OTA_UART_ST_FAILED, 40); + send_ota_failed( 40); return; } const esp_partition_t *part = NULL; uint32_t image_size = 0; if (!ota_uart_get_staged_image(&part, &image_size)) { - send_ota_status(OTA_UART_ST_FAILED, 41); + send_ota_failed( 41); return; } esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); if (err != ESP_OK) { - send_ota_status(OTA_UART_ST_FAILED, 42); + send_ota_failed( 42); return; } err = ota_uart_apply_boot(); if (err != ESP_OK) { - send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); + send_ota_failed( (uint32_t)err); return; } @@ -283,6 +328,7 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) { response.payload.ota_status.bytes_written = image_size; response.payload.ota_status.error = 0; uart_cmd_send(&response, TAG); + led_ring_ota_success(); } void cmd_ota_register(void) { diff --git a/main/led_ring.c b/main/led_ring.c index 5ca21c9..70a065a 100644 --- a/main/led_ring.c +++ b/main/led_ring.c @@ -3,6 +3,8 @@ #include "driver/i2c_types.h" #include "esp_err.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "led_strip.h" #include @@ -16,6 +18,8 @@ static led_strip_handle_t led_ring; #define RING_LEDS 95 #define LED_RING_PIN 7 +#define LED_RING_BLINK_ON_MS 350 +#define LED_RING_BLINK_OFF_MS 150 static QueueHandle_t led_queue; @@ -43,20 +47,33 @@ const uint8_t d9[] = {19, 20, 21, 22, 23, 24, 25, 26, 27, 46, 47, 58, const uint8_t d10[] = {46, 50, 57, 61, 65, 72, 76, 78, 80, 82, 84, 86, 88, 90, 92, 93, 94, 95}; -// Lookup Array for the Digits const digit_definition_t digit_lookup[] = { {d0, sizeof(d0)}, {d1, sizeof(d1)}, {d2, sizeof(d2)}, {d3, sizeof(d3)}, {d4, sizeof(d4)}, {d5, sizeof(d5)}, {d6, sizeof(d6)}, {d7, sizeof(d7)}, {d8, sizeof(d8)}, {d9, sizeof(d9)}, {d10, sizeof(d10)}}; +void led_ring_scale_rgb(uint8_t *r, uint8_t *g, uint8_t *b, uint8_t intensity) { + if (intensity == 0) { + intensity = LED_RING_DEFAULT_INTENSITY; + } + *r = (uint16_t)(*r) * intensity / 255; + *g = (uint16_t)(*g) * intensity / 255; + *b = (uint16_t)(*b) * intensity / 255; +} + +static void ring_fill_color(uint8_t r, uint8_t g, uint8_t b) { + for (uint32_t i = 0; i < RING_LEDS; i++) { + led_strip_set_pixel(led_ring, i, r, g, b); + } +} + void vTaskLedRing(void *pvParameters) { - /* LED Ring config */ led_strip_config_t ring_config = { .strip_gpio_num = LED_RING_PIN, .max_leds = RING_LEDS, }; led_strip_rmt_config_t rmt_ring_config = { - .resolution_hz = 10 * 1000 * 1000, // 10 MHz + .resolution_hz = 10 * 1000 * 1000, }; esp_err_t err = led_strip_new_rmt_device(&ring_config, &rmt_ring_config, &led_ring); @@ -67,17 +84,19 @@ void vTaskLedRing(void *pvParameters) { led_command_t cmd; while (1) { if (xQueueReceive(led_queue, &cmd, portMAX_DELAY)) { + uint8_t r = cmd.r; + uint8_t g = cmd.g; + uint8_t b = cmd.b; + led_ring_scale_rgb(&r, &g, &b, cmd.intensity); + led_strip_clear(led_ring); if (cmd.mode == LED_CMD_CLEAR) { /* ring already cleared */ } else if (cmd.mode == LED_CMD_SET_DIGIT && cmd.value <= 10) { digit_definition_t digit = digit_lookup[cmd.value]; - for (int i = 0; i < digit.count; i++) { - // Invert LED Counting for Now - led_strip_set_pixel(led_ring, RING_LEDS - digit.leds[i], cmd.r, cmd.g, - cmd.b); + led_strip_set_pixel(led_ring, RING_LEDS - digit.leds[i], r, g, b); } } else if (cmd.mode == LED_CMD_PROGRESS) { uint32_t lit = ((uint32_t)cmd.progress * RING_LEDS + 50) / 100; @@ -85,8 +104,23 @@ void vTaskLedRing(void *pvParameters) { lit = RING_LEDS; } for (uint32_t i = 0; i < lit; i++) { - led_strip_set_pixel(led_ring, i, cmd.r, cmd.g, cmd.b); + led_strip_set_pixel(led_ring, i, r, g, b); } + } else if (cmd.mode == LED_CMD_BLINK) { + uint16_t on_ms = cmd.blink_ms > 0 ? cmd.blink_ms : LED_RING_BLINK_ON_MS; + uint8_t count = cmd.blink_count > 0 ? cmd.blink_count : 1; + + for (uint8_t n = 0; n < count; n++) { + ring_fill_color(r, g, b); + led_strip_refresh(led_ring); + vTaskDelay(pdMS_TO_TICKS(on_ms)); + led_strip_clear(led_ring); + led_strip_refresh(led_ring); + if (n + 1 < count) { + vTaskDelay(pdMS_TO_TICKS(LED_RING_BLINK_OFF_MS)); + } + } + continue; } led_strip_refresh(led_ring); } @@ -103,3 +137,61 @@ void led_ring_send_command(led_command_t *cmd) { xQueueSend(led_queue, cmd, portMAX_DELAY); } } + +void led_ring_show_ota_clear(void) { + led_command_t cmd = {.mode = LED_CMD_CLEAR}; + led_ring_send_command(&cmd); +} + +void led_ring_show_ota_progress(uint32_t bytes_done, uint32_t total_bytes, + uint8_t r, uint8_t g, uint8_t b) { + static struct { + uint8_t pct; + uint8_t r, g, b; + } last = {255, 0, 0, 0}; + + if (total_bytes == 0) { + return; + } + + uint32_t pct32 = (bytes_done * 100u + total_bytes / 2) / total_bytes; + if (pct32 > 100) { + pct32 = 100; + } + uint8_t pct = (uint8_t)pct32; + + if (pct == last.pct && r == last.r && g == last.g && b == last.b) { + return; + } + last.pct = pct; + last.r = r; + last.g = g; + last.b = b; + + led_command_t cmd = { + .mode = LED_CMD_PROGRESS, + .progress = pct, + .r = r, + .g = g, + .b = b, + .intensity = LED_RING_DEFAULT_INTENSITY, + }; + led_ring_send_command(&cmd); +} + +void led_ring_blink_once(uint8_t r, uint8_t g, uint8_t b) { + led_command_t cmd = { + .mode = LED_CMD_BLINK, + .r = r, + .g = g, + .b = b, + .intensity = LED_RING_DEFAULT_INTENSITY, + .blink_ms = LED_RING_BLINK_ON_MS, + .blink_count = 1, + }; + led_ring_send_command(&cmd); +} + +void led_ring_ota_success(void) { led_ring_blink_once(0, 255, 0); } + +void led_ring_ota_failed(void) { led_ring_blink_once(255, 0, 0); } diff --git a/main/led_ring.h b/main/led_ring.h index f6b9716..c5dc1ca 100644 --- a/main/led_ring.h +++ b/main/led_ring.h @@ -1,10 +1,17 @@ +#ifndef LED_RING_H +#define LED_RING_H + #include +/** Default RGB scale (~5 % of full brightness). */ +#define LED_RING_DEFAULT_INTENSITY 13 + typedef enum { LED_CMD_CLEAR, LED_CMD_SET_DIGIT, LED_CMD_SET_COLOR, - LED_CMD_PROGRESS + LED_CMD_PROGRESS, + LED_CMD_BLINK } led_mode_t; typedef struct { @@ -13,7 +20,23 @@ typedef struct { uint8_t r, g, b; uint8_t intensity; uint8_t progress; + uint16_t blink_ms; + uint8_t blink_count; } led_command_t; void led_ring_send_command(led_command_t *cmd); void led_ring_init(void); + +void led_ring_scale_rgb(uint8_t *r, uint8_t *g, uint8_t *b, uint8_t intensity); + +/** OTA feedback: ring fill 0–100 % with RGB. */ +void led_ring_show_ota_progress(uint32_t bytes_done, uint32_t total_bytes, + uint8_t r, uint8_t g, uint8_t b); +void led_ring_show_ota_clear(void); + +/** Single pulse on the full ring (blocking in LED task). */ +void led_ring_blink_once(uint8_t r, uint8_t g, uint8_t b); +void led_ring_ota_success(void); +void led_ring_ota_failed(void); + +#endif diff --git a/main/ota_espnow.c b/main/ota_espnow.c index d800d0b..3ebdb18 100644 --- a/main/ota_espnow.c +++ b/main/ota_espnow.c @@ -1,5 +1,6 @@ #include "ota_espnow.h" #include "app_config.h" +#include "led_ring.h" #include "client_registry.h" #include "esp_log.h" #include "esp_now_comm.h" @@ -27,6 +28,11 @@ static const char *TAG = "[OTA_ESPNOW]"; #define OTA_ST_SUCCESS 4u #define OTA_ST_FAILED 5u +/** ESP-NOW OTA receive on slave (blue progress bar). */ +#define OTA_LED_ESPNOW_RX_R 0 +#define OTA_LED_ESPNOW_RX_G 0 +#define OTA_LED_ESPNOW_RX_B 255 + #define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX static EventGroupHandle_t s_eg; @@ -170,6 +176,8 @@ static void ota_slave_prepare_task(void *param) { } send_slave_status(master_mac, OTA_ST_READY, 0, 0); + led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G, + OTA_LED_ESPNOW_RX_B); vTaskDelete(NULL); } @@ -214,13 +222,27 @@ void ota_espnow_slave_on_payload(const uint8_t master_mac[6], ota_feed_result_t r = ota_uart_feed(payload->data.bytes, payload->data.size); if (r == OTA_FEED_ERROR) { + led_ring_ota_failed(); send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13); return; } if (r == OTA_FEED_BLOCK_WRITTEN) { uint32_t written = ota_uart_bytes_written(); + uint32_t total = ota_uart_total_size(); ESP_LOGI(TAG, "block written %lu bytes -> ack master", (unsigned long)written); + led_ring_show_ota_progress(written, total, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G, + OTA_LED_ESPNOW_RX_B); send_slave_status(master_mac, OTA_ST_BLOCK_ACK, written, 0); + return; + } + + if (r == OTA_FEED_OK) { + uint32_t total = ota_uart_total_size(); + if (total > 0) { + led_ring_show_ota_progress(ota_uart_bytes_received(), total, + OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G, + OTA_LED_ESPNOW_RX_B); + } } } @@ -235,11 +257,13 @@ void ota_espnow_slave_on_end(const uint8_t master_mac[6]) { bool success = false; esp_err_t err = ota_uart_finish(true, &success); if (err != ESP_OK || !success) { + led_ring_ota_failed(); send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err); return; } send_slave_status(master_mac, OTA_ST_SUCCESS, written, 0); + led_ring_ota_success(); ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run", (unsigned long)written); } diff --git a/main/ota_uart.c b/main/ota_uart.c index 9a27927..0a6a25c 100644 --- a/main/ota_uart.c +++ b/main/ota_uart.c @@ -151,6 +151,10 @@ ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len) { uint32_t ota_uart_bytes_written(void) { return s_ota.written; } +uint32_t ota_uart_bytes_received(void) { return s_ota.received; } + +uint32_t ota_uart_total_size(void) { return s_ota.active ? s_ota.total_size : 0; } + bool ota_uart_get_staged_image(const esp_partition_t **partition_out, uint32_t *size_out) { if (!s_staged.valid || s_staged.partition == NULL) { diff --git a/main/ota_uart.h b/main/ota_uart.h index 83106e4..2cf6387 100644 --- a/main/ota_uart.h +++ b/main/ota_uart.h @@ -46,6 +46,12 @@ ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len); uint32_t ota_uart_bytes_written(void); +/** Bytes accepted in the current session (includes buffered block). */ +uint32_t ota_uart_bytes_received(void); + +/** Image size from OTA_START / ESP-NOW OTA_START; 0 if inactive. */ +uint32_t ota_uart_total_size(void); + /** * Flush remainder and esp_ota_end. When set_boot is false, the staged image * remains readable via ota_uart_get_staged_image() until ota_uart_apply_boot(). diff --git a/main/proto/uart_messages.pb.h b/main/proto/uart_messages.pb.h index d181cbf..205a773 100644 --- a/main/proto/uart_messages.pb.h +++ b/main/proto/uart_messages.pb.h @@ -96,8 +96,8 @@ typedef struct _alox_EspNowUnicastTestResponse { uint32_t seq; } alox_EspNowUnicastTestResponse; -/* Host → device: LED ring display (progress bar, digit, or clear). - mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10, same layout as firmware digit maps). */ +/* Host → device: LED ring display (progress bar, digit, clear, or blink). + mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10), 3=blink full ring. */ typedef struct _alox_LedRingProgressRequest { uint32_t mode; /* * 0–100: fraction of ring LEDs to light (mode=progress) */ @@ -107,8 +107,12 @@ typedef struct _alox_LedRingProgressRequest { uint32_t r; uint32_t g; uint32_t b; - /* * 0–255 brightness scale applied to r/g/b */ + /* * 0–255 brightness scale; 0 = firmware default (~5 %) */ uint32_t intensity; + /* * Pulse length in ms (mode=blink, default 350) */ + uint32_t blink_ms; + /* * Number of pulses (mode=blink, default 1) */ + uint32_t blink_count; } alox_LedRingProgressRequest; typedef struct _alox_LedRingProgressResponse { @@ -237,7 +241,7 @@ extern "C" { #define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0} #define alox_EspNowUnicastTestRequest_init_default {0, 0} #define alox_EspNowUnicastTestResponse_init_default {0, 0} -#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0} +#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_LedRingProgressResponse_init_default {0, 0, 0, 0} #define alox_OtaStartPayload_init_default {0} #define alox_OtaPayload_init_default {0, {0, {0}}} @@ -258,7 +262,7 @@ extern "C" { #define alox_AccelDeadzoneResponse_init_zero {0, 0, 0, 0} #define alox_EspNowUnicastTestRequest_init_zero {0, 0} #define alox_EspNowUnicastTestResponse_init_zero {0, 0} -#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0} +#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0} #define alox_OtaStartPayload_init_zero {0} #define alox_OtaPayload_init_zero {0, {0, {0}}} @@ -305,6 +309,8 @@ extern "C" { #define alox_LedRingProgressRequest_g_tag 5 #define alox_LedRingProgressRequest_b_tag 6 #define alox_LedRingProgressRequest_intensity_tag 7 +#define alox_LedRingProgressRequest_blink_ms_tag 8 +#define alox_LedRingProgressRequest_blink_count_tag 9 #define alox_LedRingProgressResponse_success_tag 1 #define alox_LedRingProgressResponse_mode_tag 2 #define alox_LedRingProgressResponse_progress_tag 3 @@ -469,7 +475,9 @@ X(a, STATIC, SINGULAR, UINT32, digit, 3) \ X(a, STATIC, SINGULAR, UINT32, r, 4) \ X(a, STATIC, SINGULAR, UINT32, g, 5) \ X(a, STATIC, SINGULAR, UINT32, b, 6) \ -X(a, STATIC, SINGULAR, UINT32, intensity, 7) +X(a, STATIC, SINGULAR, UINT32, intensity, 7) \ +X(a, STATIC, SINGULAR, UINT32, blink_ms, 8) \ +X(a, STATIC, SINGULAR, UINT32, blink_count, 9) #define alox_LedRingProgressRequest_CALLBACK NULL #define alox_LedRingProgressRequest_DEFAULT NULL @@ -588,7 +596,7 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg; #define alox_ClientInput_size 22 #define alox_EspNowUnicastTestRequest_size 12 #define alox_EspNowUnicastTestResponse_size 8 -#define alox_LedRingProgressRequest_size 42 +#define alox_LedRingProgressRequest_size 54 #define alox_LedRingProgressResponse_size 20 #define alox_OtaEndPayload_size 0 #define alox_OtaPayload_size 209 diff --git a/main/proto/uart_messages.proto b/main/proto/uart_messages.proto index dc04666..83cc3bf 100644 --- a/main/proto/uart_messages.proto +++ b/main/proto/uart_messages.proto @@ -110,8 +110,8 @@ message EspNowUnicastTestResponse { uint32 seq = 2; } -// Host → device: LED ring display (progress bar, digit, or clear). -// mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10, same layout as firmware digit maps). +// Host → device: LED ring display (progress bar, digit, clear, or blink). +// mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10), 3=blink full ring. message LedRingProgressRequest { uint32 mode = 1; /** 0–100: fraction of ring LEDs to light (mode=progress) */ @@ -121,8 +121,12 @@ message LedRingProgressRequest { uint32 r = 4; uint32 g = 5; uint32 b = 6; - /** 0–255 brightness scale applied to r/g/b */ + /** 0–255 brightness scale; 0 = firmware default (~5 %) */ uint32 intensity = 7; + /** Pulse length in ms (mode=blink, default 350) */ + uint32 blink_ms = 8; + /** Number of pulses (mode=blink, default 1) */ + uint32 blink_count = 9; } message LedRingProgressResponse {