Add command queue dispatcher and VERSION UART handler.

Centralize command dispatch over a FreeRTOS queue so UART and future
ESP-NOW transports can register handlers; implement the protobuf VERSION
command with framed nanopb responses including build git hash.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-18 21:46:51 +02:00
parent 6b7ccb4256
commit 43a85ce697
12 changed files with 422 additions and 12 deletions

View File

@ -1,2 +1,26 @@
idf_component_register(SRCS "powerpod.c" "led_ring.c" "uart.c"
INCLUDE_DIRS ".")
execute_process(
COMMAND git -C ${CMAKE_CURRENT_LIST_DIR}/.. rev-parse --short=8 HEAD
OUTPUT_VARIABLE POWERPOD_GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT POWERPOD_GIT_HASH)
set(POWERPOD_GIT_HASH "unknown")
endif()
idf_component_register(
SRCS
"powerpod.c"
"led_ring.c"
"uart.c"
"uart_proto.c"
"cmd_handler.c"
"cmd_version.c"
"proto/uart_messages.pb.c"
"proto/pb_encode.c"
"proto/pb_common.c"
INCLUDE_DIRS
"."
"proto")
target_compile_definitions(${COMPONENT_LIB}
PRIVATE "POWERPOD_GIT_HASH=\"${POWERPOD_GIT_HASH}\"")

114
main/README.md Normal file
View File

@ -0,0 +1,114 @@
# Command handler
Generic command dispatch for Powerpod. Transports (UART today, ESP-NOW later) enqueue messages on a shared FreeRTOS queue; a dispatcher task invokes registered callbacks by message ID.
## Architecture
```
UART / ESP-NOW → generic_msg_t queue → vCmdDispatcherTask → registered handler
```
- **`cmd_handler`** — queue, registration, dispatcher task
- **`uart`** — framed serial input, converts packets to `generic_msg_t`
- **`powerpod.c`** — creates the queue, calls `init_cmdHandler()` then `init_uart()`
Initialize the command handler **before** UART so the dispatcher is running when packets arrive.
```c
cmd_queue = xQueueCreate(10, sizeof(generic_msg_t));
init_cmdHandler(cmd_queue);
init_uart(cmd_queue);
```
## UART frame format
Packets on UART1 (921600 baud, pins TX=2 / RX=3):
| Field | Value |
|-----------|--------------------------------------------|
| Start | `0xAA` |
| Length | 1 byte, payload size (1252), non-zero |
| Payload | `length` bytes |
| Checksum | XOR of all payload bytes |
| Stop | `0xCC` |
**Payload layout for the command handler:**
| Offset | Meaning |
|--------|----------------------------------|
| 0 | Command ID (`msg_id`, uint8/16) |
| 1… | Arguments (passed to handler) |
Example: command `0x01` with arguments `0x02 0x03` → payload `01 02 03`, length = 3.
The dispatcher strips the first byte; handlers receive only the argument bytes.
## API
### `msg_register_handler(uint16_t id, msg_callback_t cb)`
Register a callback for a command ID. Up to 32 handlers. Re-registering the same ID updates the callback.
```c
static void on_ping(const uint8_t *data, size_t len) {
ESP_LOGI("app", "ping, %u bytes", (unsigned)len);
}
msg_register_handler(0x01, on_ping);
```
Callback signature:
```c
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
```
### `msg_post(uint16_t id, const uint8_t *data, size_t len)`
Enqueue a command from firmware (e.g. ESP-NOW receive path) without UART. Copies `data` into heap memory; the dispatcher frees it after the handler returns.
```c
uint8_t args[] = {0x02, 0x03};
msg_post(0x01, args, sizeof(args));
```
Returns `ESP_OK`, `ESP_ERR_NO_MEM`, `ESP_ERR_TIMEOUT` (queue full), or `ESP_ERR_INVALID_STATE`.
## Adding a new command
1. Pick a command ID (first byte of UART payload).
2. Implement a handler in `powerpod.c` (or a dedicated module).
3. Call `msg_register_handler()` after `init_cmdHandler()`.
4. From a host tool, send a framed UART packet with that ID in byte 0.
## VERSION command (`MessageType.VERSION` = 3)
Implemented in `cmd_version.c`. Request is a UART frame with payload `03` (command byte only).
Response frame payload:
| Byte 0 | Bytes 1… |
|--------|----------|
| `0x03` | nanopb-encoded `UartMessage` with `type = VERSION` and `version_response` set |
`VersionResponse` fields:
- `version``POWERPOD_FW_VERSION` (default `1`, override at compile time)
- `git_hash` — short git hash from build (`POWERPOD_GIT_HASH`, from `git rev-parse`)
Register additional proto commands the same way: handler + `uart_send_uart_message()` for replies.
## ESP-NOW (planned)
Parse incoming ESP-NOW data in the Wi-Fi layer and call `msg_post()` with the same ID + payload layout as UART (ID separate, arguments in `data`). No changes to `cmd_handler` required.
## Files
| File | Role |
|-----------------|-------------------------------------------|
| `cmd_handler.h` | Types and public API |
| `cmd_handler.c` | Queue dispatch, registration, `msg_post` |
| `uart.c` | Framed UART parser → queue |
| `powerpod.c` | Queue creation and init order |
| `cmd_version.c` | VERSION command handler |
| `uart_proto.c` | Encode/send `UartMessage` over UART |

80
main/cmd_handler.c Normal file
View File

@ -0,0 +1,80 @@
#include "cmd_handler.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include <stdlib.h>
#include <string.h>
#define MAX_HANDLERS 32
static const char *TAG = "[CMDH]";
static QueueHandle_t cmd_queue;
static msg_binding_t handlers[MAX_HANDLERS];
static int handler_count;
void init_cmdHandler(QueueHandle_t queue) {
cmd_queue = queue;
xTaskCreate(vCmdDispatcherTask, "cmd_dispatch", 4096, NULL, 5, NULL);
}
esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb) {
if (cb == NULL) {
return ESP_ERR_INVALID_ARG;
}
for (int i = 0; i < handler_count; i++) {
if (handlers[i].msg_id == id) {
handlers[i].callback = cb;
return ESP_OK;
}
}
if (handler_count >= MAX_HANDLERS) {
return ESP_ERR_NO_MEM;
}
handlers[handler_count].msg_id = id;
handlers[handler_count].callback = cb;
handler_count++;
return ESP_OK;
}
esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len) {
if (cmd_queue == NULL) {
return ESP_ERR_INVALID_STATE;
}
generic_msg_t msg = {.msg_id = id, .len = len, .payload = NULL};
if (len > 0) {
msg.payload = malloc(len);
if (msg.payload == NULL) {
return ESP_ERR_NO_MEM;
}
memcpy(msg.payload, data, len);
}
if (xQueueSend(cmd_queue, &msg, pdMS_TO_TICKS(100)) != pdPASS) {
free(msg.payload);
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
void vCmdDispatcherTask(void *param) {
generic_msg_t msg;
while (1) {
if (xQueueReceive(cmd_queue, &msg, portMAX_DELAY) == pdPASS) {
bool handled = false;
for (int i = 0; i < handler_count; i++) {
if (handlers[i].msg_id == msg.msg_id) {
handlers[i].callback(msg.payload, msg.len);
handled = true;
break;
}
}
if (!handled) {
ESP_LOGW(TAG, "no handler for msg_id 0x%04x (%u bytes)", msg.msg_id,
(unsigned)msg.len);
}
free(msg.payload);
}
}
}

26
main/cmd_handler.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef CMD_HANDLER_H
#define CMD_HANDLER_H
#include "esp_err.h"
#include "freertos/idf_additions.h"
typedef struct {
uint16_t msg_id;
uint8_t *payload;
size_t len;
} generic_msg_t;
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
typedef struct {
uint16_t msg_id;
msg_callback_t callback;
} msg_binding_t;
void init_cmdHandler(QueueHandle_t queue);
void vCmdDispatcherTask(void *param);
esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb);
esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len);
#endif

58
main/cmd_version.c Normal file
View File

@ -0,0 +1,58 @@
#include "cmd_handler.h"
#include "cmd_version.h"
#include "esp_log.h"
#include "pb_encode.h"
#include "uart_messages.pb.h"
#include "uart_proto.h"
#include <string.h>
#ifndef POWERPOD_FW_VERSION
#define POWERPOD_FW_VERSION 1u
#endif
#ifndef POWERPOD_GIT_HASH
#define POWERPOD_GIT_HASH "unknown"
#endif
static const char *TAG = "[VERSION]";
static bool encode_git_hash(pb_ostream_t *stream, const pb_field_t *field,
void *const *arg) {
const char *str = (const char *)*arg;
if (str == NULL) {
str = "";
}
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
return pb_encode_string(stream, (const pb_byte_t *)str, strlen(str));
}
static void handle_version(const uint8_t *data, size_t len) {
(void)data;
(void)len;
alox_UartMessage response = alox_UartMessage_init_zero;
response.type = alox_MessageType_VERSION;
response.which_payload = alox_UartMessage_version_response_tag;
response.payload.version_response.version = POWERPOD_FW_VERSION;
response.payload.version_response.git_hash.funcs.encode = encode_git_hash;
response.payload.version_response.git_hash.arg = (void *)POWERPOD_GIT_HASH;
esp_err_t err = uart_send_uart_message(&response);
if (err != ESP_OK) {
ESP_LOGE(TAG, "failed to send version response: %s", esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "version=%u git=%s", (unsigned)POWERPOD_FW_VERSION,
POWERPOD_GIT_HASH);
}
void cmd_version_register(void) {
esp_err_t err =
msg_register_handler(alox_MessageType_VERSION, handle_version);
if (err != ESP_OK) {
ESP_LOGE(TAG, "register failed: %s", esp_err_to_name(err));
}
}

6
main/cmd_version.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef CMD_VERSION_H
#define CMD_VERSION_H
void cmd_version_register(void);
#endif

View File

@ -1,4 +1,6 @@
#include "powerpod.h"
#include "cmd_handler.h"
#include "cmd_version.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "driver/i2c_types.h"
@ -7,9 +9,11 @@
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include "led_ring.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "uart.h"
#include <stdint.h>
enum MASTER_STATES {
@ -47,6 +51,8 @@ static i2c_master_dev_handle_t io_expander;
static struct app_config_t App_Config;
static QueueHandle_t cmd_queue;
uint8_t reverse_high_nibble_lut(uint8_t n) {
static const uint8_t lookup[] = {0x0, 0x8, 0x4, 0xC, 0x2, 0xA, 0x6, 0xE,
0x1, 0x9, 0x5, 0xD, 0x3, 0xB, 0x7, 0xF};
@ -123,15 +129,18 @@ void app_main(void) {
led_ring_init();
cmd_queue = xQueueCreate(10, sizeof(generic_msg_t));
init_cmdHandler(cmd_queue);
init_uart(cmd_queue);
cmd_version_register();
uint8_t current_digit = 10;
while (1) {
led_command_t cmd = {
.mode = LED_CMD_SET_DIGIT,
led_command_t cmd = {.mode = LED_CMD_SET_DIGIT,
.value = current_digit,
.r = 5,
.g = 5,
.b = 0
};
.b = 0};
led_ring_send_command(&cmd);
current_digit = (current_digit + 1) % 11;

View File

@ -1,7 +1,7 @@
/* Automatically generated nanopb constant definitions */
/* Generated by nanopb-1.0.0-dev */
#include "main/proto/uart_messages.pb.h"
#include "uart_messages.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif

View File

@ -1,3 +1,4 @@
#include "cmd_handler.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_log.h"
@ -8,11 +9,40 @@
#include "portmacro.h"
#include "uart.h"
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
static const char *TAG = "[UART]";
static QueueHandle_t uart_cmd_queue;
static bool uart_enqueue_packet(const uart_packet_t *packet) {
if (packet->len == 0) {
return false;
}
generic_msg_t msg = {
.msg_id = packet->payload[0],
.len = packet->len > 1 ? packet->len - 1 : 0,
.payload = NULL,
};
if (msg.len > 0) {
msg.payload = malloc(msg.len);
if (msg.payload == NULL) {
ESP_LOGE(TAG, "failed to allocate command payload");
return false;
}
memcpy(msg.payload, &packet->payload[1], msg.len);
}
if (xQueueSend(uart_cmd_queue, &msg, 0) != pdPASS) {
free(msg.payload);
ESP_LOGW(TAG, "command queue full");
return false;
}
return true;
}
void init_uart(QueueHandle_t cmd_queue) {
uart_cmd_queue = cmd_queue;
uart_config_t uart_config = {// .baud_rate = 115200, // 921600, 115200
@ -44,8 +74,9 @@ void uart_read_task(void *param) {
if (len > 0) {
for (int i = 0; i < len; ++i) {
if (parse_uart_byte(data[i], &packet)) {
ESP_LOGI("UART", "Paket empfangen! Länge: %d", packet.len);
xQueueSend(uart_cmd_queue, &packet, 0);
ESP_LOGI(TAG, "packet received, len=%d, cmd=0x%02x", packet.len,
packet.len > 0 ? packet.payload[0] : 0);
uart_enqueue_packet(&packet);
}
}
last_byte_time = xTaskGetTickCount();
@ -107,3 +138,30 @@ bool parse_uart_byte(uint8_t byte, uart_packet_t *p) {
}
return false;
}
esp_err_t uart_send_framed(const uint8_t *payload, size_t len) {
if (payload == NULL || len == 0 || len > MAX_PAYLOAD_SIZE) {
return ESP_ERR_INVALID_ARG;
}
uint8_t checksum = 0;
for (size_t i = 0; i < len; i++) {
checksum ^= payload[i];
}
uint8_t frame[4 + MAX_PAYLOAD_SIZE];
size_t pos = 0;
frame[pos++] = START_MARKER;
frame[pos++] = (uint8_t)len;
memcpy(&frame[pos], payload, len);
pos += len;
frame[pos++] = checksum;
frame[pos++] = STOP_MARKER;
int written =
uart_write_bytes(UART_NUM, frame, pos);
if (written < 0 || (size_t)written != pos) {
return ESP_FAIL;
}
return ESP_OK;
}

View File

@ -1,8 +1,10 @@
#ifndef UART_H
#define UART_H
#include "esp_err.h"
#include "freertos/idf_additions.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define UART_NUM UART_NUM_1
#define UART_BUF_SIZE 256
@ -38,5 +40,6 @@ typedef struct {
void init_uart(QueueHandle_t cmd_queue);
void uart_read_task(void *param);
bool parse_uart_byte(uint8_t byte, uart_packet_t *p);
esp_err_t uart_send_framed(const uint8_t *payload, size_t len);
#endif

23
main/uart_proto.c Normal file
View File

@ -0,0 +1,23 @@
#include "uart_proto.h"
#include "pb_encode.h"
#include "uart.h"
#include <string.h>
esp_err_t uart_send_uart_message(const alox_UartMessage *msg) {
uint8_t pb_buf[MAX_PAYLOAD_SIZE];
pb_ostream_t stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
if (!pb_encode(&stream, alox_UartMessage_fields, msg)) {
return ESP_FAIL;
}
uint8_t payload[MAX_PAYLOAD_SIZE];
if (stream.bytes_written + 1 > sizeof(payload)) {
return ESP_ERR_NO_MEM;
}
payload[0] = (uint8_t)msg->type;
memcpy(&payload[1], pb_buf, stream.bytes_written);
return uart_send_framed(payload, stream.bytes_written + 1);
}

9
main/uart_proto.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef UART_PROTO_H
#define UART_PROTO_H
#include "esp_err.h"
#include "uart_messages.pb.h"
esp_err_t uart_send_uart_message(const alox_UartMessage *msg);
#endif