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) | | `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` | 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) | | `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. `clients` requires slaves to have responded to master discover broadcasts first.

View File

@ -11,17 +11,20 @@ const (
ledRingModeClear = 0 ledRingModeClear = 0
ledRingModeProgress = 1 ledRingModeProgress = 1
ledRingModeDigit = 2 ledRingModeDigit = 2
ledRingModeBlink = 3
) )
func runLedRing(sp *serialPort, args []string) error { func runLedRing(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("led-ring", flag.ExitOnError) 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)") progress := fs.Uint("progress", 0, "fill level 0100 (mode=progress)")
digit := fs.Uint("digit", 0, "digit 010 (mode=digit)") digit := fs.Uint("digit", 0, "digit 010 (mode=digit)")
r := fs.Uint("r", 0, "red 0255") r := fs.Uint("r", 0, "red 0255")
g := fs.Uint("g", 255, "green 0255") g := fs.Uint("g", 255, "green 0255")
b := fs.Uint("b", 0, "blue 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 { if err := fs.Parse(args); err != nil {
return err return err
} }
@ -34,18 +37,22 @@ func runLedRing(sp *serialPort, args []string) error {
modeVal = ledRingModeProgress modeVal = ledRingModeProgress
case "digit": case "digit":
modeVal = ledRingModeDigit modeVal = ledRingModeDigit
case "blink":
modeVal = ledRingModeBlink
default: 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{ resp, err := sp.ledRingProgress(&pb.LedRingProgressRequest{
Mode: modeVal, Mode: modeVal,
Progress: uint32(*progress), Progress: uint32(*progress),
Digit: uint32(*digit), Digit: uint32(*digit),
R: uint32(*r), R: uint32(*r),
G: uint32(*g), G: uint32(*g),
B: uint32(*b), B: uint32(*b),
Intensity: uint32(*intensity), Intensity: uint32(*intensity),
BlinkMs: uint32(*blinkMs),
BlinkCount: uint32(*blinkCount),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -1068,8 +1068,8 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 {
return 0 return 0
} }
// Host → device: LED ring display (progress bar, digit, or clear). // Host → device: LED ring display (progress bar, digit, clear, or blink).
// mode: 0=clear, 1=progress (0100 %), 2=digit (010, same layout as firmware digit maps). // mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink full ring.
type LedRingProgressRequest struct { type LedRingProgressRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Mode uint32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"` 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"` R uint32 `protobuf:"varint,4,opt,name=r,proto3" json:"r,omitempty"`
G uint32 `protobuf:"varint,5,opt,name=g,proto3" json:"g,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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1165,6 +1169,20 @@ func (x *LedRingProgressRequest) GetIntensity() uint32 {
return 0 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 { type LedRingProgressResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` 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" + "\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" +
"\x19EspNowUnicastTestResponse\x12\x18\n" + "\x19EspNowUnicastTestResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\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" + "\x16LedRingProgressRequest\x12\x12\n" +
"\x04mode\x18\x01 \x01(\rR\x04mode\x12\x1a\n" + "\x04mode\x18\x01 \x01(\rR\x04mode\x12\x1a\n" +
"\bprogress\x18\x02 \x01(\rR\bprogress\x12\x14\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" + "\x01r\x18\x04 \x01(\rR\x01r\x12\f\n" +
"\x01g\x18\x05 \x01(\rR\x01g\x12\f\n" + "\x01g\x18\x05 \x01(\rR\x01g\x12\f\n" +
"\x01b\x18\x06 \x01(\rR\x01b\x12\x1c\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" + "\x17LedRingProgressResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x12\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x12\n" +
"\x04mode\x18\x02 \x01(\rR\x04mode\x12\x1a\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. `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). `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`. 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 | | 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`) | | `progress` | 0100 (% of ring lit, mode `1`) |
| `digit` | 010 (mode `2`, same segment maps as built-in digits) | | `digit` | 010 (mode `2`, same segment maps as built-in digits) |
| `r`, `g`, `b` | Color 0255 | | `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`). **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 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 digit -digit 7 -r 255 -g 200
go run . -port /dev/ttyUSB0 led-ring -mode clear 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 ### 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_CLEAR 0
#define LED_RING_MODE_PROGRESS 1 #define LED_RING_MODE_PROGRESS 1
#define LED_RING_MODE_DIGIT 2 #define LED_RING_MODE_DIGIT 2
#define LED_RING_MODE_BLINK 3
static uint8_t clamp_u8(uint32_t v) { static uint8_t clamp_u8(uint32_t v) {
if (v > 255) { if (v > 255) {
@ -23,13 +24,11 @@ static uint8_t clamp_progress(uint32_t v) {
return (uint8_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) { if (intensity == 0) {
return; return LED_RING_DEFAULT_INTENSITY;
} }
*r = (uint16_t)(*r) * intensity / 255; return clamp_u8(intensity);
*g = (uint16_t)(*g) * intensity / 255;
*b = (uint16_t)(*b) * intensity / 255;
} }
static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit) { 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 r = clamp_u8(req.r);
uint8_t g = clamp_u8(req.g); uint8_t g = clamp_u8(req.g);
uint8_t b = clamp_u8(req.b); uint8_t b = clamp_u8(req.b);
uint8_t intensity = clamp_u8(req.intensity); uint8_t intensity = resolve_intensity(req.intensity);
if (intensity == 0) {
intensity = 255;
}
apply_intensity(&r, &g, &b, intensity);
led_command_t cmd = {0}; 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.r = r;
cmd.g = g; cmd.g = g;
cmd.b = b; cmd.b = b;
cmd.intensity = intensity;
led_ring_send_command(&cmd); led_ring_send_command(&cmd);
ESP_LOGI(TAG, "digit %u rgb=%u,%u,%u", (unsigned)cmd.value, (unsigned)r, ESP_LOGI(TAG, "digit %u rgb=%u,%u,%u", (unsigned)cmd.value, (unsigned)r,
(unsigned)g, (unsigned)b); (unsigned)g, (unsigned)b);
@ -113,6 +109,24 @@ static void handle_led_ring(const uint8_t *data, size_t len) {
return; 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: default:
ESP_LOGW(TAG, "unknown mode %lu", (unsigned long)mode); ESP_LOGW(TAG, "unknown mode %lu", (unsigned long)mode);
reply(false, mode, 0, 0); reply(false, mode, 0, 0);

View File

@ -1,4 +1,5 @@
#include "cmd_ota.h" #include "cmd_ota.h"
#include "led_ring.h"
#include "ota_espnow.h" #include "ota_espnow.h"
#include "ota_uart.h" #include "ota_uart.h"
#include "uart_cmd.h" #include "uart_cmd.h"
@ -15,6 +16,15 @@ static const char *TAG = "[OTA_CMD]";
#define OTA_DIST_STACK 8192 #define OTA_DIST_STACK 8192
#define OTA_DIST_PRIO 5 #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 { typedef struct {
uint32_t written; uint32_t written;
int slot; 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); 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, static void send_ota_distributing(uint32_t kind, uint32_t bytes_done,
uint32_t target_slot) { uint32_t target_slot) {
alox_UartMessage response; 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, static void ota_dist_aggregate(uint32_t bytes_done, uint32_t total_bytes,
uint8_t slave_count) { 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); 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); int slot = ota_uart_prepare(total_size);
if (slot < 0) { if (slot < 0) {
send_ota_status(OTA_UART_ST_FAILED, 1); send_ota_failed(1);
vTaskDelete(NULL); vTaskDelete(NULL);
return; return;
} }
@ -82,6 +99,9 @@ static void ota_prepare_task(void *param) {
response.payload.ota_status.error = 0; response.payload.ota_status.error = 0;
uart_cmd_send(&response, TAG); 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); 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; alox_OtaStartPayload req = alox_OtaStartPayload_init_zero;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
send_ota_status(OTA_UART_ST_FAILED, 2); send_ota_failed( 2);
return; return;
} }
@ -102,13 +122,13 @@ static void handle_ota_start(const uint8_t *data, size_t len) {
if (req.total_size == 0) { if (req.total_size == 0) {
ESP_LOGW(TAG, "OTA_START: total_size required"); ESP_LOGW(TAG, "OTA_START: total_size required");
send_ota_status(OTA_UART_ST_FAILED, 3); send_ota_failed( 3);
return; return;
} }
if (ota_uart_is_active()) { if (ota_uart_is_active()) {
ESP_LOGW(TAG, "OTA_START while session active"); ESP_LOGW(TAG, "OTA_START while session active");
send_ota_status(OTA_UART_ST_FAILED, 4); send_ota_failed( 4);
return; 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, (void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO,
NULL) != pdPASS) { NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create ota_prepare task"); 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) { if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "OTA_PAYLOAD decode failed"); ESP_LOGW(TAG, "OTA_PAYLOAD decode failed");
send_ota_status(OTA_UART_ST_FAILED, 10); send_ota_failed( 10);
return; return;
} }
@ -134,34 +154,47 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
if (req_ptr == NULL) { if (req_ptr == NULL) {
ESP_LOGW(TAG, "OTA_PAYLOAD: missing ota_payload (which=%u)", ESP_LOGW(TAG, "OTA_PAYLOAD: missing ota_payload (which=%u)",
(unsigned)uart_msg.which_payload); (unsigned)uart_msg.which_payload);
send_ota_status(OTA_UART_ST_FAILED, 11); send_ota_failed( 11);
return; return;
} }
if (req_ptr->data.size == 0) { if (req_ptr->data.size == 0) {
ESP_LOGW(TAG, "OTA_PAYLOAD: empty data (seq=%lu)", ESP_LOGW(TAG, "OTA_PAYLOAD: empty data (seq=%lu)",
(unsigned long)req_ptr->seq); (unsigned long)req_ptr->seq);
send_ota_status(OTA_UART_ST_FAILED, 11); send_ota_failed( 11);
return; return;
} }
if (!ota_uart_is_active()) { if (!ota_uart_is_active()) {
ESP_LOGW(TAG, "OTA_PAYLOAD without active session (seq=%lu)", ESP_LOGW(TAG, "OTA_PAYLOAD without active session (seq=%lu)",
(unsigned long)req_ptr->seq); (unsigned long)req_ptr->seq);
send_ota_status(OTA_UART_ST_FAILED, 12); send_ota_failed( 12);
return; return;
} }
ota_feed_result_t r = ota_feed_result_t r =
ota_uart_feed(req_ptr->data.bytes, req_ptr->data.size); ota_uart_feed(req_ptr->data.bytes, req_ptr->data.size);
if (r == OTA_FEED_ERROR) { if (r == OTA_FEED_ERROR) {
send_ota_status(OTA_UART_ST_FAILED, 13); send_ota_failed( 13);
return; return;
} }
if (r == OTA_FEED_BLOCK_WRITTEN) { 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)", 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); 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; const esp_partition_t *part = NULL;
uint32_t image_size = 0; uint32_t image_size = 0;
if (!ota_uart_get_staged_image(&part, &image_size)) { if (!ota_uart_get_staged_image(&part, &image_size)) {
send_ota_status(OTA_UART_ST_FAILED, 30); send_ota_failed( 30);
free(job); free(job);
vTaskDelete(NULL); vTaskDelete(NULL);
return; 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); send_ota_distributing(OTA_DIST_AGGREGATE, 0, 0);
esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "slave OTA distribution failed: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "slave OTA distribution failed: %s", esp_err_to_name(err));
ota_uart_clear_staged(); ota_uart_clear_staged();
send_ota_status(OTA_UART_ST_FAILED, 31); send_ota_failed(31);
free(job); free(job);
vTaskDelete(NULL); vTaskDelete(NULL);
return; return;
@ -195,12 +230,15 @@ static void ota_distribute_task(void *param) {
err = ota_uart_apply_boot(); err = ota_uart_apply_boot();
if (err != ESP_OK) { if (err != ESP_OK) {
send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); send_ota_failed((uint32_t)err);
free(job); free(job);
vTaskDelete(NULL); vTaskDelete(NULL);
return; 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; alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS, uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS,
alox_UartMessage_ota_status_tag); alox_UartMessage_ota_status_tag);
@ -211,6 +249,7 @@ static void ota_distribute_task(void *param) {
response.payload.ota_status.error = 0; response.payload.ota_status.error = 0;
uart_cmd_send(&response, TAG); uart_cmd_send(&response, TAG);
led_ring_ota_success();
free(job); free(job);
vTaskDelete(NULL); vTaskDelete(NULL);
} }
@ -220,30 +259,36 @@ static void handle_ota_end(const uint8_t *data, size_t len) {
(void)len; (void)len;
if (!ota_uart_is_active()) { if (!ota_uart_is_active()) {
send_ota_status(OTA_UART_ST_FAILED, 20); send_ota_failed( 20);
return; return;
} }
ota_dist_job_t *job = calloc(1, sizeof(*job)); ota_dist_job_t *job = calloc(1, sizeof(*job));
if (job == NULL) { if (job == NULL) {
send_ota_status(OTA_UART_ST_FAILED, 21); send_ota_failed( 21);
return; return;
} }
job->written = ota_uart_bytes_written(); job->written = ota_uart_bytes_written();
job->slot = ota_uart_target_slot(); job->slot = ota_uart_target_slot();
uint32_t uart_total = ota_uart_total_size();
bool success = false; bool success = false;
esp_err_t err = ota_uart_finish(false, &success); esp_err_t err = ota_uart_finish(false, &success);
if (err != ESP_OK || !success) { if (err != ESP_OK || !success) {
send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); send_ota_failed((uint32_t)err);
free(job); free(job);
return; 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, if (xTaskCreate(ota_distribute_task, "ota_dist", OTA_DIST_STACK, job,
OTA_DIST_PRIO, NULL) != pdPASS) { OTA_DIST_PRIO, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create ota_dist task"); ESP_LOGE(TAG, "failed to create ota_dist task");
send_ota_status(OTA_UART_ST_FAILED, 22); send_ota_failed( 22);
free(job); free(job);
} }
} }
@ -253,26 +298,26 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
(void)len; (void)len;
if (ota_uart_is_active()) { if (ota_uart_is_active()) {
send_ota_status(OTA_UART_ST_FAILED, 40); send_ota_failed( 40);
return; return;
} }
const esp_partition_t *part = NULL; const esp_partition_t *part = NULL;
uint32_t image_size = 0; uint32_t image_size = 0;
if (!ota_uart_get_staged_image(&part, &image_size)) { if (!ota_uart_get_staged_image(&part, &image_size)) {
send_ota_status(OTA_UART_ST_FAILED, 41); send_ota_failed( 41);
return; return;
} }
esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
if (err != ESP_OK) { if (err != ESP_OK) {
send_ota_status(OTA_UART_ST_FAILED, 42); send_ota_failed( 42);
return; return;
} }
err = ota_uart_apply_boot(); err = ota_uart_apply_boot();
if (err != ESP_OK) { if (err != ESP_OK) {
send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); send_ota_failed( (uint32_t)err);
return; 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.bytes_written = image_size;
response.payload.ota_status.error = 0; response.payload.ota_status.error = 0;
uart_cmd_send(&response, TAG); uart_cmd_send(&response, TAG);
led_ring_ota_success();
} }
void cmd_ota_register(void) { void cmd_ota_register(void) {

View File

@ -3,6 +3,8 @@
#include "driver/i2c_types.h" #include "driver/i2c_types.h"
#include "esp_err.h" #include "esp_err.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "led_strip.h" #include "led_strip.h"
#include <stdint.h> #include <stdint.h>
@ -16,6 +18,8 @@ static led_strip_handle_t led_ring;
#define RING_LEDS 95 #define RING_LEDS 95
#define LED_RING_PIN 7 #define LED_RING_PIN 7
#define LED_RING_BLINK_ON_MS 350
#define LED_RING_BLINK_OFF_MS 150
static QueueHandle_t led_queue; 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, const uint8_t d10[] = {46, 50, 57, 61, 65, 72, 76, 78, 80,
82, 84, 86, 88, 90, 92, 93, 94, 95}; 82, 84, 86, 88, 90, 92, 93, 94, 95};
// Lookup Array for the Digits
const digit_definition_t digit_lookup[] = { const digit_definition_t digit_lookup[] = {
{d0, sizeof(d0)}, {d1, sizeof(d1)}, {d2, sizeof(d2)}, {d3, sizeof(d3)}, {d0, sizeof(d0)}, {d1, sizeof(d1)}, {d2, sizeof(d2)}, {d3, sizeof(d3)},
{d4, sizeof(d4)}, {d5, sizeof(d5)}, {d6, sizeof(d6)}, {d7, sizeof(d7)}, {d4, sizeof(d4)}, {d5, sizeof(d5)}, {d6, sizeof(d6)}, {d7, sizeof(d7)},
{d8, sizeof(d8)}, {d9, sizeof(d9)}, {d10, sizeof(d10)}}; {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) { void vTaskLedRing(void *pvParameters) {
/* LED Ring config */
led_strip_config_t ring_config = { led_strip_config_t ring_config = {
.strip_gpio_num = LED_RING_PIN, .strip_gpio_num = LED_RING_PIN,
.max_leds = RING_LEDS, .max_leds = RING_LEDS,
}; };
led_strip_rmt_config_t rmt_ring_config = { led_strip_rmt_config_t rmt_ring_config = {
.resolution_hz = 10 * 1000 * 1000, // 10 MHz .resolution_hz = 10 * 1000 * 1000,
}; };
esp_err_t err = esp_err_t err =
led_strip_new_rmt_device(&ring_config, &rmt_ring_config, &led_ring); led_strip_new_rmt_device(&ring_config, &rmt_ring_config, &led_ring);
@ -67,17 +84,19 @@ void vTaskLedRing(void *pvParameters) {
led_command_t cmd; led_command_t cmd;
while (1) { while (1) {
if (xQueueReceive(led_queue, &cmd, portMAX_DELAY)) { 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); led_strip_clear(led_ring);
if (cmd.mode == LED_CMD_CLEAR) { if (cmd.mode == LED_CMD_CLEAR) {
/* ring already cleared */ /* ring already cleared */
} else if (cmd.mode == LED_CMD_SET_DIGIT && cmd.value <= 10) { } else if (cmd.mode == LED_CMD_SET_DIGIT && cmd.value <= 10) {
digit_definition_t digit = digit_lookup[cmd.value]; digit_definition_t digit = digit_lookup[cmd.value];
for (int i = 0; i < digit.count; i++) { for (int i = 0; i < digit.count; i++) {
// Invert LED Counting for Now led_strip_set_pixel(led_ring, RING_LEDS - digit.leds[i], r, g, b);
led_strip_set_pixel(led_ring, RING_LEDS - digit.leds[i], cmd.r, cmd.g,
cmd.b);
} }
} else if (cmd.mode == LED_CMD_PROGRESS) { } else if (cmd.mode == LED_CMD_PROGRESS) {
uint32_t lit = ((uint32_t)cmd.progress * RING_LEDS + 50) / 100; uint32_t lit = ((uint32_t)cmd.progress * RING_LEDS + 50) / 100;
@ -85,8 +104,23 @@ void vTaskLedRing(void *pvParameters) {
lit = RING_LEDS; lit = RING_LEDS;
} }
for (uint32_t i = 0; i < lit; i++) { 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); led_strip_refresh(led_ring);
} }
@ -103,3 +137,61 @@ void led_ring_send_command(led_command_t *cmd) {
xQueueSend(led_queue, cmd, portMAX_DELAY); 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> #include <stdint.h>
/** Default RGB scale (~5 % of full brightness). */
#define LED_RING_DEFAULT_INTENSITY 13
typedef enum { typedef enum {
LED_CMD_CLEAR, LED_CMD_CLEAR,
LED_CMD_SET_DIGIT, LED_CMD_SET_DIGIT,
LED_CMD_SET_COLOR, LED_CMD_SET_COLOR,
LED_CMD_PROGRESS LED_CMD_PROGRESS,
LED_CMD_BLINK
} led_mode_t; } led_mode_t;
typedef struct { typedef struct {
@ -13,7 +20,23 @@ typedef struct {
uint8_t r, g, b; uint8_t r, g, b;
uint8_t intensity; uint8_t intensity;
uint8_t progress; uint8_t progress;
uint16_t blink_ms;
uint8_t blink_count;
} led_command_t; } led_command_t;
void led_ring_send_command(led_command_t *cmd); void led_ring_send_command(led_command_t *cmd);
void led_ring_init(void); 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 "ota_espnow.h"
#include "app_config.h" #include "app_config.h"
#include "led_ring.h"
#include "client_registry.h" #include "client_registry.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_now_comm.h" #include "esp_now_comm.h"
@ -27,6 +28,11 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_ST_SUCCESS 4u #define OTA_ST_SUCCESS 4u
#define OTA_ST_FAILED 5u #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 #define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX
static EventGroupHandle_t s_eg; 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); 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); vTaskDelete(NULL);
} }
@ -214,13 +222,27 @@ void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
ota_feed_result_t r = ota_feed_result_t r =
ota_uart_feed(payload->data.bytes, payload->data.size); ota_uart_feed(payload->data.bytes, payload->data.size);
if (r == OTA_FEED_ERROR) { if (r == OTA_FEED_ERROR) {
led_ring_ota_failed();
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13); send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13);
return; return;
} }
if (r == OTA_FEED_BLOCK_WRITTEN) { if (r == OTA_FEED_BLOCK_WRITTEN) {
uint32_t written = ota_uart_bytes_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); 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); 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; bool success = false;
esp_err_t err = ota_uart_finish(true, &success); esp_err_t err = ota_uart_finish(true, &success);
if (err != ESP_OK || !success) { if (err != ESP_OK || !success) {
led_ring_ota_failed();
send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err); send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err);
return; return;
} }
send_slave_status(master_mac, OTA_ST_SUCCESS, written, 0); 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", ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run",
(unsigned long)written); (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_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, bool ota_uart_get_staged_image(const esp_partition_t **partition_out,
uint32_t *size_out) { uint32_t *size_out) {
if (!s_staged.valid || s_staged.partition == NULL) { 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); 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 * 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(). * 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; uint32_t seq;
} alox_EspNowUnicastTestResponse; } alox_EspNowUnicastTestResponse;
/* Host → device: LED ring display (progress bar, digit, or clear). /* Host → device: LED ring display (progress bar, digit, clear, or blink).
mode: 0=clear, 1=progress (0100 %), 2=digit (010, same layout as firmware digit maps). */ mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink full ring. */
typedef struct _alox_LedRingProgressRequest { typedef struct _alox_LedRingProgressRequest {
uint32_t mode; uint32_t mode;
/* * 0100: fraction of ring LEDs to light (mode=progress) */ /* * 0100: fraction of ring LEDs to light (mode=progress) */
@ -107,8 +107,12 @@ typedef struct _alox_LedRingProgressRequest {
uint32_t r; uint32_t r;
uint32_t g; uint32_t g;
uint32_t b; uint32_t b;
/* * 0255 brightness scale applied to r/g/b */ /* * 0255 brightness scale; 0 = firmware default (~5 %) */
uint32_t intensity; 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; } alox_LedRingProgressRequest;
typedef struct _alox_LedRingProgressResponse { typedef struct _alox_LedRingProgressResponse {
@ -237,7 +241,7 @@ extern "C" {
#define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0} #define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0}
#define alox_EspNowUnicastTestRequest_init_default {0, 0} #define alox_EspNowUnicastTestRequest_init_default {0, 0}
#define alox_EspNowUnicastTestResponse_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_LedRingProgressResponse_init_default {0, 0, 0, 0}
#define alox_OtaStartPayload_init_default {0} #define alox_OtaStartPayload_init_default {0}
#define alox_OtaPayload_init_default {0, {0, {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_AccelDeadzoneResponse_init_zero {0, 0, 0, 0}
#define alox_EspNowUnicastTestRequest_init_zero {0, 0} #define alox_EspNowUnicastTestRequest_init_zero {0, 0}
#define alox_EspNowUnicastTestResponse_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_LedRingProgressResponse_init_zero {0, 0, 0, 0}
#define alox_OtaStartPayload_init_zero {0} #define alox_OtaStartPayload_init_zero {0}
#define alox_OtaPayload_init_zero {0, {0, {0}}} #define alox_OtaPayload_init_zero {0, {0, {0}}}
@ -305,6 +309,8 @@ extern "C" {
#define alox_LedRingProgressRequest_g_tag 5 #define alox_LedRingProgressRequest_g_tag 5
#define alox_LedRingProgressRequest_b_tag 6 #define alox_LedRingProgressRequest_b_tag 6
#define alox_LedRingProgressRequest_intensity_tag 7 #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_success_tag 1
#define alox_LedRingProgressResponse_mode_tag 2 #define alox_LedRingProgressResponse_mode_tag 2
#define alox_LedRingProgressResponse_progress_tag 3 #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, r, 4) \
X(a, STATIC, SINGULAR, UINT32, g, 5) \ X(a, STATIC, SINGULAR, UINT32, g, 5) \
X(a, STATIC, SINGULAR, UINT32, b, 6) \ 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_CALLBACK NULL
#define alox_LedRingProgressRequest_DEFAULT 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_ClientInput_size 22
#define alox_EspNowUnicastTestRequest_size 12 #define alox_EspNowUnicastTestRequest_size 12
#define alox_EspNowUnicastTestResponse_size 8 #define alox_EspNowUnicastTestResponse_size 8
#define alox_LedRingProgressRequest_size 42 #define alox_LedRingProgressRequest_size 54
#define alox_LedRingProgressResponse_size 20 #define alox_LedRingProgressResponse_size 20
#define alox_OtaEndPayload_size 0 #define alox_OtaEndPayload_size 0
#define alox_OtaPayload_size 209 #define alox_OtaPayload_size 209

View File

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