diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2a6c8ae..2da29eb 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -15,6 +15,8 @@ idf_component_register( "uart_proto.c" "cmd_handler.c" "cmd_version.c" + "cmd_client_info.c" + "client_registry.c" "esp_now_comm.c" "proto/uart_messages.pb.c" "proto/pb_encode.c" diff --git a/main/README.md b/main/README.md index 1404310..bf0c194 100644 --- a/main/README.md +++ b/main/README.md @@ -28,7 +28,7 @@ ESP32-S3 firmware for Powerpod nodes. Master and slave devices run the **same bi | Master | Yes — command handler, protobuf replies | Broadcasts discover; collects slave info | | Slave | No | Responds to discover with slave info | -Planned: master forwards aggregated `ClientInfo` to the PC over UART (`CLIENT_INFO` in `uart_messages.proto`). +Master keeps a **client registry** (`client_registry.c`) of slaves seen via ESP-NOW. The PC can query it with the `CLIENT_INFO` UART command. ## Boot configuration @@ -156,7 +156,7 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 = | ID | Name | Status | |----|------|--------| | 3 | `VERSION` | Implemented (`cmd_version.c`) | -| 4 | `CLIENT_INFO` | Planned — slave list to PC | +| 4 | `CLIENT_INFO` | Implemented (`cmd_client_info.c`) — slave list from registry | | 5 | `CLIENT_INPUT` | Planned | | 16–20 | OTA / ESP-NOW OTA | Planned | @@ -182,6 +182,24 @@ Build embeds `POWERPOD_GIT_HASH` via `git rev-parse` in `main/CMakeLists.txt`. Encoding: `uart_send_uart_message()` in `uart_proto.c`. +### CLIENT_INFO command + +**Request:** framed payload `04` only (`MessageType.CLIENT_INFO`). + +**Response:** payload `04` + nanopb `UartMessage` with `client_info_response.clients` — one `ClientInfo` per registered slave (from ESP-NOW `SLAVE_INFO`). + +Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping` (milliseconds since boot, updated on each `SLAVE_INFO`). + +## Client registry + +| API | Description | +|-----|-------------| +| `client_registry_init()` | Clear all slots (called from `esp_now_comm_init`) | +| `client_registry_upsert(mac, id, version, …)` | Add or refresh client; updates ping timestamps | +| `client_registry_count()` / `client_registry_at(i)` | Iterate for UART encoding | + +Slaves register when the master receives `SLAVE_INFO` on the matching network. + ## Host tool (`goTool/`) Go CLI to test UART from a PC connected to the **master** only. @@ -189,7 +207,8 @@ Go CLI to test UART from a PC connected to the **master** only. ```bash cd goTool go mod tidy -go run . -port /dev/ttyUSB0 +go run . -port /dev/ttyUSB0 version +go run . -port /dev/ttyUSB0 clients ``` | Flag | Default | Description | @@ -197,7 +216,10 @@ go run . -port /dev/ttyUSB0 | `-port` | (required) | Serial device, e.g. `/dev/ttyUSB0` | | `-baud` | `921600` | Must match `UART_BAUD_RATE` | -Sends VERSION, reads framed response, prints `version` and `git_hash`. +| Command | Description | +|---------|-------------| +| `version` | Firmware version and git hash | +| `clients` | Registered slaves from master client registry | Regenerate Go protobuf: @@ -232,6 +254,8 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running ` | `uart_proto.c/h` | Encode/send `UartMessage` | | `cmd_handler.c/h` | Command queue and dispatch | | `cmd_version.c/h` | VERSION handler | +| `cmd_client_info.c/h` | CLIENT_INFO handler | +| `client_registry.c/h` | Registered slave table | | `led_ring.c/h` | LED digit display | | `proto/uart_messages.proto` | Protocol schema | | `proto/*.pb.c/h` | Generated nanopb | diff --git a/main/client_registry.c b/main/client_registry.c new file mode 100644 index 0000000..03dc969 --- /dev/null +++ b/main/client_registry.c @@ -0,0 +1,104 @@ +#include "client_registry.h" +#include "freertos/FreeRTOS.h" +#include "freertos/idf_additions.h" +#include + +typedef struct { + client_info_t info; + bool active; +} client_slot_t; + +static client_slot_t s_clients[CLIENT_REGISTRY_MAX]; + +static uint32_t now_ms(void) { + return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); +} + +static bool mac_equal(const uint8_t *a, const uint8_t *b) { + return memcmp(a, b, CLIENT_MAC_LEN) == 0; +} + +void client_registry_init(void) { memset(s_clients, 0, sizeof(s_clients)); } + +const client_info_t * +client_registry_find_by_mac(const uint8_t mac[CLIENT_MAC_LEN]) { + if (mac == NULL) { + return NULL; + } + for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) { + if (s_clients[i].active && mac_equal(s_clients[i].info.mac, mac)) { + return &s_clients[i].info; + } + } + return NULL; +} + +esp_err_t client_registry_upsert(const uint8_t mac[CLIENT_MAC_LEN], uint32_t id, + uint32_t version, bool available, bool used, + bool *out_is_new) { + if (mac == NULL) { + return ESP_ERR_INVALID_ARG; + } + + uint32_t ts = now_ms(); + client_slot_t *slot = NULL; + bool is_new = false; + + for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) { + if (s_clients[i].active && mac_equal(s_clients[i].info.mac, mac)) { + slot = &s_clients[i]; + break; + } + } + + if (slot == NULL) { + for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) { + if (!s_clients[i].active) { + slot = &s_clients[i]; + slot->active = true; + memcpy(slot->info.mac, mac, CLIENT_MAC_LEN); + is_new = true; + break; + } + } + if (slot == NULL) { + return ESP_ERR_NO_MEM; + } + } + + slot->info.id = id; + slot->info.version = version; + slot->info.available = available; + slot->info.used = used; + slot->info.last_ping = ts; + slot->info.last_success_ping = ts; + + if (out_is_new != NULL) { + *out_is_new = is_new; + } + return ESP_OK; +} + +size_t client_registry_count(void) { + size_t n = 0; + for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) { + if (s_clients[i].active) { + n++; + } + } + return n; +} + +const client_info_t *client_registry_at(size_t index) { + size_t n = 0; + for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) { + if (!s_clients[i].active) { + continue; + } + if (n == index) { + return &s_clients[i].info; + } + n++; + } + return NULL; +} diff --git a/main/client_registry.h b/main/client_registry.h new file mode 100644 index 0000000..fc093b7 --- /dev/null +++ b/main/client_registry.h @@ -0,0 +1,33 @@ +#ifndef CLIENT_REGISTRY_H +#define CLIENT_REGISTRY_H + +#include "esp_err.h" +#include +#include +#include + +#define CLIENT_REGISTRY_MAX 16 +#define CLIENT_MAC_LEN 6 + +typedef struct { + uint32_t id; + bool available; + bool used; + uint8_t mac[CLIENT_MAC_LEN]; + uint32_t last_ping; + uint32_t last_success_ping; + uint32_t version; +} client_info_t; + +void client_registry_init(void); + +/** Register or refresh a client; sets last_ping and last_success_ping to now. */ +esp_err_t client_registry_upsert(const uint8_t mac[CLIENT_MAC_LEN], uint32_t id, + uint32_t version, bool available, bool used, + bool *out_is_new); + +size_t client_registry_count(void); +const client_info_t *client_registry_at(size_t index); +const client_info_t *client_registry_find_by_mac(const uint8_t mac[CLIENT_MAC_LEN]); + +#endif diff --git a/main/cmd_client_info.c b/main/cmd_client_info.c new file mode 100644 index 0000000..69c8a00 --- /dev/null +++ b/main/cmd_client_info.c @@ -0,0 +1,77 @@ +#include "client_registry.h" +#include "cmd_client_info.h" +#include "cmd_handler.h" +#include "esp_log.h" +#include "pb_encode.h" +#include "uart_messages.pb.h" +#include "uart_proto.h" + +static const char *TAG = "[CLIENT_INFO]"; + +static bool encode_client_mac(pb_ostream_t *stream, const pb_field_t *field, + void *const *arg) { + const uint8_t *mac = (const uint8_t *)*arg; + if (mac == NULL) { + return true; + } + if (!pb_encode_tag_for_field(stream, field)) { + return false; + } + return pb_encode_string(stream, mac, CLIENT_MAC_LEN); +} + +static bool encode_clients_list(pb_ostream_t *stream, const pb_field_t *field, + void *const *arg) { + (void)arg; + + size_t count = client_registry_count(); + for (size_t i = 0; i < count; i++) { + const client_info_t *client = client_registry_at(i); + if (client == NULL) { + continue; + } + + alox_ClientInfo proto = alox_ClientInfo_init_zero; + proto.id = client->id; + proto.available = client->available; + proto.used = client->used; + proto.last_ping = client->last_ping; + proto.last_success_ping = client->last_success_ping; + proto.version = client->version; + proto.mac.funcs.encode = encode_client_mac; + proto.mac.arg = (void *)client->mac; + + if (!pb_encode_tag_for_field(stream, field)) { + return false; + } + if (!pb_encode_submessage(stream, alox_ClientInfo_fields, &proto)) { + return false; + } + } + return true; +} + +static void handle_client_info(const uint8_t *data, size_t len) { + (void)data; + (void)len; + + alox_UartMessage response = alox_UartMessage_init_zero; + response.type = alox_MessageType_CLIENT_INFO; + response.which_payload = alox_UartMessage_client_info_response_tag; + response.payload.client_info_response.clients.funcs.encode = + encode_clients_list; + response.payload.client_info_response.clients.arg = NULL; + + ESP_LOGI(TAG, "sending %u clients", (unsigned)client_registry_count()); + + if (uart_send_uart_message(&response) != ESP_OK) { + ESP_LOGE(TAG, "failed to send response"); + } +} + +void cmd_client_info_register(void) { + if (msg_register_handler(alox_MessageType_CLIENT_INFO, handle_client_info) != + ESP_OK) { + ESP_LOGE(TAG, "register failed"); + } +} diff --git a/main/cmd_client_info.h b/main/cmd_client_info.h new file mode 100644 index 0000000..57abb81 --- /dev/null +++ b/main/cmd_client_info.h @@ -0,0 +1,6 @@ +#ifndef CMD_CLIENT_INFO_H +#define CMD_CLIENT_INFO_H + +void cmd_client_info_register(void); + +#endif diff --git a/main/esp_now_comm.c b/main/esp_now_comm.c index bfbd320..afd9469 100644 --- a/main/esp_now_comm.c +++ b/main/esp_now_comm.c @@ -1,3 +1,4 @@ +#include "client_registry.h" #include "esp_now_comm.h" #include "esp_err.h" #include "esp_event.h" @@ -20,8 +21,6 @@ #define ESPNOW_MSG_DISCOVER 1 #define ESPNOW_MSG_SLAVE_INFO 2 #define ESPNOW_DISCOVER_INTERVAL_MS 500 -#define ESPNOW_MAX_SLAVES 16 - static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; @@ -45,19 +44,9 @@ typedef struct __attribute__((packed)) { uint8_t used; } espnow_slave_info_packet_t; -typedef struct { - uint8_t mac[ESP_NOW_ETH_ALEN]; - uint32_t slave_id; - uint32_t version; - bool available; - bool used; - bool seen; -} slave_entry_t; - static app_config_t s_config; static uint8_t s_wifi_channel; static uint8_t s_own_mac[ESP_NOW_ETH_ALEN]; -static slave_entry_t s_slaves[ESPNOW_MAX_SLAVES]; static bool s_slave_joined; static uint8_t s_master_mac[ESP_NOW_ETH_ALEN]; @@ -97,30 +86,6 @@ static esp_err_t ensure_peer(const uint8_t *mac) { static esp_err_t ensure_broadcast_peer(void) { return ensure_peer(ESPNOW_BCAST); } -static slave_entry_t *find_slave(const uint8_t *mac) { - for (int i = 0; i < ESPNOW_MAX_SLAVES; i++) { - if (s_slaves[i].seen && mac_equal(s_slaves[i].mac, mac)) { - return &s_slaves[i]; - } - } - return NULL; -} - -static slave_entry_t *alloc_slave(const uint8_t *mac) { - slave_entry_t *existing = find_slave(mac); - if (existing != NULL) { - return existing; - } - for (int i = 0; i < ESPNOW_MAX_SLAVES; i++) { - if (!s_slaves[i].seen) { - memcpy(s_slaves[i].mac, mac, ESP_NOW_ETH_ALEN); - s_slaves[i].seen = true; - return &s_slaves[i]; - } - } - return NULL; -} - static void send_slave_info(const uint8_t *dest_mac) { espnow_slave_info_packet_t pkt = { .magic = ESPNOW_MAGIC, @@ -169,22 +134,19 @@ static void handle_slave_info(const espnow_slave_info_packet_t *pkt) { return; } - bool is_new = (find_slave(pkt->mac) == NULL); - slave_entry_t *entry = alloc_slave(pkt->mac); - if (entry == NULL) { - ESP_LOGW(TAG, "slave table full"); + bool is_new = false; + esp_err_t err = client_registry_upsert( + pkt->mac, pkt->slave_id, pkt->version, pkt->available != 0, + pkt->used != 0, &is_new); + if (err != ESP_OK) { + ESP_LOGW(TAG, "client registry full"); return; } - entry->slave_id = pkt->slave_id; - entry->version = pkt->version; - entry->available = pkt->available != 0; - entry->used = pkt->used != 0; - char mac_str[18]; mac_to_str(pkt->mac, mac_str, sizeof(mac_str)); if (is_new) { - ESP_LOGI(TAG, "slave joined id=%lu mac=%s ver=%lu", + ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu", (unsigned long)pkt->slave_id, mac_str, (unsigned long)pkt->version); } @@ -278,7 +240,7 @@ esp_err_t esp_now_comm_init(const app_config_t *config) { memset(&s_config, 0, sizeof(s_config)); memcpy(&s_config, config, sizeof(s_config)); - memset(s_slaves, 0, sizeof(s_slaves)); + client_registry_init(); s_slave_joined = false; memset(s_master_mac, 0, sizeof(s_master_mac)); diff --git a/main/powerpod.c b/main/powerpod.c index 8eee094..3886839 100644 --- a/main/powerpod.c +++ b/main/powerpod.c @@ -1,5 +1,6 @@ #include "app_config.h" #include "cmd_handler.h" +#include "cmd_client_info.h" #include "cmd_version.h" #include "esp_now_comm.h" #include "powerpod.h" @@ -133,6 +134,7 @@ void app_main(void) { init_cmdHandler(cmd_queue); init_uart(cmd_queue); cmd_version_register(); + cmd_client_info_register(); } uint8_t current_digit = 10;