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 <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-19 21:53:10 +02:00
parent 508b684fdf
commit 8931912583
13 changed files with 324 additions and 71 deletions

View File

@ -29,7 +29,7 @@ go run . -port /dev/ttyUSB0 clients
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
| `ota` | 1619 | 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.

View File

@ -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 0100 (mode=progress)")
digit := fs.Uint("digit", 0, "digit 010 (mode=digit)")
r := fs.Uint("r", 0, "red 0255")
g := fs.Uint("g", 255, "green 0255")
b := fs.Uint("b", 0, "blue 0255")
intensity := fs.Uint("intensity", 255, "brightness 0255")
intensity := fs.Uint("intensity", 0, "brightness 0255 (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,8 +37,10 @@ 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{
@ -46,6 +51,8 @@ func runLedRing(sp *serialPort, args []string) error {
G: uint32(*g),
B: uint32(*b),
Intensity: uint32(*intensity),
BlinkMs: uint32(*blinkMs),
BlinkCount: uint32(*blinkCount),
})
if err != nil {
return err

View File

@ -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 (0100 %), 2=digit (010, same layout as firmware digit maps).
// Host → device: LED ring display (progress bar, digit, clear, or blink).
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 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"`
// * 0255 brightness scale applied to r/g/b
// * 0255 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" +

View File

@ -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` | 0100 (% of ring lit, mode `1`) |
| `digit` | 010 (mode `2`, same segment maps as built-in digits) |
| `r`, `g`, `b` | Color 0255 |
| `intensity` | Brightness 0255 (scaled into RGB; `0` → 255) |
| `intensity` | Brightness 0255 (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

View File

@ -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);

View File

@ -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) {

View File

@ -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 <stdint.h>
@ -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); }

View File

@ -1,10 +1,17 @@
#ifndef LED_RING_H
#define LED_RING_H
#include <stdint.h>
/** 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 0100 % 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

View File

@ -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);
}

View File

@ -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) {

View File

@ -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().

View File

@ -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 (0100 %), 2=digit (010, same layout as firmware digit maps). */
/* Host → device: LED ring display (progress bar, digit, clear, or blink).
mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink full ring. */
typedef struct _alox_LedRingProgressRequest {
uint32_t mode;
/* * 0100: 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;
/* * 0255 brightness scale applied to r/g/b */
/* * 0255 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

View File

@ -110,8 +110,8 @@ message EspNowUnicastTestResponse {
uint32 seq = 2;
}
// Host device: LED ring display (progress bar, digit, or clear).
// mode: 0=clear, 1=progress (0100 %), 2=digit (010, same layout as firmware digit maps).
// Host device: LED ring display (progress bar, digit, clear, or blink).
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink full ring.
message LedRingProgressRequest {
uint32 mode = 1;
/** 0100: fraction of ring LEDs to light (mode=progress) */
@ -121,8 +121,12 @@ message LedRingProgressRequest {
uint32 r = 4;
uint32 g = 5;
uint32 b = 6;
/** 0255 brightness scale applied to r/g/b */
/** 0255 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 {