From 43a85ce697eebe9161f87283a7f5807f19371b79 Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 18 May 2026 21:46:51 +0200 Subject: [PATCH] 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 --- main/CMakeLists.txt | 28 ++++++++- main/README.md | 114 ++++++++++++++++++++++++++++++++++ main/cmd_handler.c | 80 ++++++++++++++++++++++++ main/cmd_handler.h | 26 ++++++++ main/cmd_version.c | 58 +++++++++++++++++ main/cmd_version.h | 6 ++ main/powerpod.c | 23 ++++--- main/proto/uart_messages.pb.c | 2 +- main/uart.c | 62 +++++++++++++++++- main/uart.h | 3 + main/uart_proto.c | 23 +++++++ main/uart_proto.h | 9 +++ 12 files changed, 422 insertions(+), 12 deletions(-) create mode 100644 main/README.md create mode 100644 main/cmd_handler.c create mode 100644 main/cmd_handler.h create mode 100644 main/cmd_version.c create mode 100644 main/cmd_version.h create mode 100644 main/uart_proto.c create mode 100644 main/uart_proto.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f75fff1..ff5c612 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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}\"") diff --git a/main/README.md b/main/README.md new file mode 100644 index 0000000..50ee5f5 --- /dev/null +++ b/main/README.md @@ -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 (1–252), 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 | diff --git a/main/cmd_handler.c b/main/cmd_handler.c new file mode 100644 index 0000000..c3700dc --- /dev/null +++ b/main/cmd_handler.c @@ -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 +#include + +#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); + } + } +} diff --git a/main/cmd_handler.h b/main/cmd_handler.h new file mode 100644 index 0000000..f26bc6e --- /dev/null +++ b/main/cmd_handler.h @@ -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 diff --git a/main/cmd_version.c b/main/cmd_version.c new file mode 100644 index 0000000..b37531b --- /dev/null +++ b/main/cmd_version.c @@ -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 + +#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)); + } +} diff --git a/main/cmd_version.h b/main/cmd_version.h new file mode 100644 index 0000000..c16a735 --- /dev/null +++ b/main/cmd_version.h @@ -0,0 +1,6 @@ +#ifndef CMD_VERSION_H +#define CMD_VERSION_H + +void cmd_version_register(void); + +#endif diff --git a/main/powerpod.c b/main/powerpod.c index 8f0e570..78944a1 100644 --- a/main/powerpod.c +++ b/main/powerpod.c @@ -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 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, - .value = current_digit, - .r = 5, - .g = 5, - .b = 0 - }; + led_command_t cmd = {.mode = LED_CMD_SET_DIGIT, + .value = current_digit, + .r = 5, + .g = 5, + .b = 0}; led_ring_send_command(&cmd); current_digit = (current_digit + 1) % 11; diff --git a/main/proto/uart_messages.pb.c b/main/proto/uart_messages.pb.c index 127f60e..66b0399 100644 --- a/main/proto/uart_messages.pb.c +++ b/main/proto/uart_messages.pb.c @@ -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 diff --git a/main/uart.c b/main/uart.c index 381387b..7e8e57a 100644 --- a/main/uart.c +++ b/main/uart.c @@ -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 +#include #include 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; +} diff --git a/main/uart.h b/main/uart.h index b9dec38..5a7c5a1 100644 --- a/main/uart.h +++ b/main/uart.h @@ -1,8 +1,10 @@ #ifndef UART_H #define UART_H +#include "esp_err.h" #include "freertos/idf_additions.h" #include +#include #include #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 diff --git a/main/uart_proto.c b/main/uart_proto.c new file mode 100644 index 0000000..148151b --- /dev/null +++ b/main/uart_proto.c @@ -0,0 +1,23 @@ +#include "uart_proto.h" +#include "pb_encode.h" +#include "uart.h" +#include + +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); +} diff --git a/main/uart_proto.h b/main/uart_proto.h new file mode 100644 index 0000000..c2c60cb --- /dev/null +++ b/main/uart_proto.h @@ -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