Add LED ring control per client and broadcast over REST and WebSocket.

Solid color mode fills all ring LEDs; master routes UART commands to slaves
via ESPNOW_LED_RING. goTool exposes POST /api/led-ring, WebSocket set_led_ring,
and a dashboard LED panel with master/slave/all targets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 19:24:55 +02:00
co-authored by Cursor
parent 47c75110c9
commit eb67a46158
21 changed files with 759 additions and 133 deletions
+53 -4
View File
@@ -24,7 +24,8 @@ typedef enum _alox_EspNowMessageType {
alox_EspNowMessageType_ESPNOW_FIND_ME = 10,
alox_EspNowMessageType_ESPNOW_RESTART = 11,
alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE = 12,
alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM = 13
alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM = 13,
alox_EspNowMessageType_ESPNOW_LED_RING = 14
} alox_EspNowMessageType;
/* Struct definitions */
@@ -75,6 +76,20 @@ typedef struct _alox_EspNowAccelSample {
int32_t z;
} alox_EspNowAccelSample;
/* * Master → slave: LED ring command (same modes as UART LedRingProgressRequest). */
typedef struct _alox_EspNowLedRing {
uint32_t client_id;
uint32_t mode;
uint32_t progress;
uint32_t digit;
uint32_t r;
uint32_t g;
uint32_t b;
uint32_t intensity;
uint32_t blink_ms;
uint32_t blink_count;
} alox_EspNowLedRing;
/* Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS). */
typedef struct _alox_EspNowOtaStart {
uint32_t total_size;
@@ -116,6 +131,7 @@ typedef struct _alox_EspNowMessage {
alox_EspNowRestart restart;
alox_EspNowAccelSample accel_sample;
alox_EspNowAccelStream accel_stream;
alox_EspNowLedRing led_ring;
} payload;
} alox_EspNowMessage;
@@ -126,8 +142,9 @@ extern "C" {
/* Helper constants for enums */
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM+1))
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_LED_RING
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_LED_RING+1))
@@ -153,6 +170,7 @@ extern "C" {
#define alox_EspNowAccelDeadzone_init_default {0, 0}
#define alox_EspNowAccelStream_init_default {0, 0}
#define alox_EspNowAccelSample_init_default {0, 0, 0, 0}
#define alox_EspNowLedRing_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowOtaStart_init_default {0}
#define alox_EspNowOtaPayload_init_default {0, {0, {0}}}
#define alox_EspNowOtaEnd_init_default {0}
@@ -166,6 +184,7 @@ extern "C" {
#define alox_EspNowAccelDeadzone_init_zero {0, 0}
#define alox_EspNowAccelStream_init_zero {0, 0}
#define alox_EspNowAccelSample_init_zero {0, 0, 0, 0}
#define alox_EspNowLedRing_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowOtaStart_init_zero {0}
#define alox_EspNowOtaPayload_init_zero {0, {0, {0}}}
#define alox_EspNowOtaEnd_init_zero {0}
@@ -191,6 +210,16 @@ extern "C" {
#define alox_EspNowAccelSample_x_tag 2
#define alox_EspNowAccelSample_y_tag 3
#define alox_EspNowAccelSample_z_tag 4
#define alox_EspNowLedRing_client_id_tag 1
#define alox_EspNowLedRing_mode_tag 2
#define alox_EspNowLedRing_progress_tag 3
#define alox_EspNowLedRing_digit_tag 4
#define alox_EspNowLedRing_r_tag 5
#define alox_EspNowLedRing_g_tag 6
#define alox_EspNowLedRing_b_tag 7
#define alox_EspNowLedRing_intensity_tag 8
#define alox_EspNowLedRing_blink_ms_tag 9
#define alox_EspNowLedRing_blink_count_tag 10
#define alox_EspNowOtaStart_total_size_tag 1
#define alox_EspNowOtaPayload_seq_tag 1
#define alox_EspNowOtaPayload_data_tag 2
@@ -211,6 +240,7 @@ extern "C" {
#define alox_EspNowMessage_restart_tag 12
#define alox_EspNowMessage_accel_sample_tag 13
#define alox_EspNowMessage_accel_stream_tag 14
#define alox_EspNowMessage_led_ring_tag 15
/* Struct field encoding specification for nanopb */
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \
@@ -263,6 +293,20 @@ X(a, STATIC, SINGULAR, SINT32, z, 4)
#define alox_EspNowAccelSample_CALLBACK NULL
#define alox_EspNowAccelSample_DEFAULT NULL
#define alox_EspNowLedRing_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, UINT32, mode, 2) \
X(a, STATIC, SINGULAR, UINT32, progress, 3) \
X(a, STATIC, SINGULAR, UINT32, digit, 4) \
X(a, STATIC, SINGULAR, UINT32, r, 5) \
X(a, STATIC, SINGULAR, UINT32, g, 6) \
X(a, STATIC, SINGULAR, UINT32, b, 7) \
X(a, STATIC, SINGULAR, UINT32, intensity, 8) \
X(a, STATIC, SINGULAR, UINT32, blink_ms, 9) \
X(a, STATIC, SINGULAR, UINT32, blink_count, 10)
#define alox_EspNowLedRing_CALLBACK NULL
#define alox_EspNowLedRing_DEFAULT NULL
#define alox_EspNowOtaStart_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
#define alox_EspNowOtaStart_CALLBACK NULL
@@ -300,7 +344,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,ota_status,payload.ota_status), 10)
X(a, STATIC, ONEOF, MESSAGE, (payload,find_me,payload.find_me), 11) \
X(a, STATIC, ONEOF, MESSAGE, (payload,restart,payload.restart), 12) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_sample,payload.accel_sample), 13) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream), 14)
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream), 14) \
X(a, STATIC, ONEOF, MESSAGE, (payload,led_ring,payload.led_ring), 15)
#define alox_EspNowMessage_CALLBACK NULL
#define alox_EspNowMessage_DEFAULT NULL
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
@@ -316,6 +361,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream),
#define alox_EspNowMessage_payload_restart_MSGTYPE alox_EspNowRestart
#define alox_EspNowMessage_payload_accel_sample_MSGTYPE alox_EspNowAccelSample
#define alox_EspNowMessage_payload_accel_stream_MSGTYPE alox_EspNowAccelStream
#define alox_EspNowMessage_payload_led_ring_MSGTYPE alox_EspNowLedRing
extern const pb_msgdesc_t alox_EspNowUnicastTest_msg;
extern const pb_msgdesc_t alox_EspNowFindMe_msg;
@@ -325,6 +371,7 @@ extern const pb_msgdesc_t alox_EspNowSlavePresence_msg;
extern const pb_msgdesc_t alox_EspNowAccelDeadzone_msg;
extern const pb_msgdesc_t alox_EspNowAccelStream_msg;
extern const pb_msgdesc_t alox_EspNowAccelSample_msg;
extern const pb_msgdesc_t alox_EspNowLedRing_msg;
extern const pb_msgdesc_t alox_EspNowOtaStart_msg;
extern const pb_msgdesc_t alox_EspNowOtaPayload_msg;
extern const pb_msgdesc_t alox_EspNowOtaEnd_msg;
@@ -340,6 +387,7 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowAccelDeadzone_fields &alox_EspNowAccelDeadzone_msg
#define alox_EspNowAccelStream_fields &alox_EspNowAccelStream_msg
#define alox_EspNowAccelSample_fields &alox_EspNowAccelSample_msg
#define alox_EspNowLedRing_fields &alox_EspNowLedRing_msg
#define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg
#define alox_EspNowOtaPayload_fields &alox_EspNowOtaPayload_msg
#define alox_EspNowOtaEnd_fields &alox_EspNowOtaEnd_msg
@@ -355,6 +403,7 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowAccelStream_size 8
#define alox_EspNowDiscover_size 6
#define alox_EspNowFindMe_size 6
#define alox_EspNowLedRing_size 60
#define alox_EspNowOtaEnd_size 0
#define alox_EspNowOtaPayload_size 209
#define alox_EspNowOtaStart_size 6