Add Transport-Neutral Serial Session Broker

This commit is contained in:
2026-08-23 00:01:50 +02:00
parent 535c27350d
commit 2524f43fac
11 changed files with 1407 additions and 102 deletions
+2
View File
@@ -7,6 +7,8 @@ idf_component_register(
"serial_config.c"
"serial_service.c"
"serial_console.c"
"session_broker.c"
"session_console.c"
INCLUDE_DIRS "."
REQUIRES
console
+5 -1
View File
@@ -8,6 +8,8 @@
#include "serial_config.h"
#include "serial_console.h"
#include "serial_service.h"
#include "session_broker.h"
#include "session_console.h"
#include "status_led.h"
#define CONSOLE_BAUD_RATE 115200
@@ -18,7 +20,7 @@ static const char *TAG = "firmware";
void app_main(void)
{
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 1 started");
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 2 started");
if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
@@ -42,6 +44,7 @@ void app_main(void)
esp_err_to_name(config_error));
}
ESP_ERROR_CHECK(serial_service_init(&serial_config));
ESP_ERROR_CHECK(session_broker_init());
ESP_LOGI(
TAG,
"Using %s serial configuration; UART service remains stopped until 'serial start'",
@@ -68,6 +71,7 @@ void app_main(void)
/* The REPL constructor initializes esp_console and installs `help`. */
ESP_ERROR_CHECK(rs232_hw_test_register_console_commands());
ESP_ERROR_CHECK(serial_console_register_commands());
ESP_ERROR_CHECK(session_console_register_commands());
ESP_ERROR_CHECK(esp_console_start_repl(repl));
ESP_LOGI(TAG, "Interactive test console ready at %d baud", CONSOLE_BAUD_RATE);
+9 -83
View File
@@ -20,8 +20,6 @@ static void print_usage(void)
printf(" serial set <baud|data-bits|parity|stop-bits|flow|dtr|rts-threshold> <value>\n");
printf(" serial save|load|defaults|reset\n");
printf(" serial counters|clear-counters\n");
printf(" serial send-hex <hex-bytes>\n");
printf(" serial read [maximum-bytes]\n");
}
static void print_config(const serial_config_t *config)
@@ -39,6 +37,15 @@ static void print_config(const serial_config_t *config)
static bool parse_unsigned(const char *text, uint32_t minimum, uint32_t maximum, uint32_t *value)
{
if (text == NULL || *text == '\0') {
return false;
}
for (const char *character = text; *character != '\0'; ++character) {
if (*character < '0' || *character > '9') {
return false;
}
}
char *end = NULL;
errno = 0;
unsigned long parsed = strtoul(text, &end, 10);
@@ -184,72 +191,6 @@ static int set_parameter(const char *parameter, const char *value)
return 0;
}
static int hexadecimal_value(char character)
{
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return -1;
}
static int send_hexadecimal(const char *text)
{
/* The 160-character REPL line comfortably carries at most 64 hex bytes. */
uint8_t data[64];
size_t text_length = strlen(text);
if (text_length == 0 || (text_length % 2) != 0 || text_length > sizeof(data) * 2) {
printf("Provide 1..64 bytes as an even number of hexadecimal digits without separators.\n");
return 1;
}
size_t data_length = text_length / 2;
for (size_t index = 0; index < data_length; ++index) {
int high = hexadecimal_value(text[index * 2]);
int low = hexadecimal_value(text[index * 2 + 1]);
if (high < 0 || low < 0) {
size_t invalid_index = index * 2 + (high < 0 ? 0 : 1);
printf("Invalid hexadecimal digit at character %u.\n", (unsigned int)invalid_index);
return 1;
}
data[index] = (uint8_t)((high << 4) | low);
}
size_t accepted = serial_service_write(data, data_length);
printf("Queued %u of %u bytes.\n", (unsigned int)accepted, (unsigned int)data_length);
return accepted == data_length ? 0 : 1;
}
static int read_hexadecimal(int argc, char **argv)
{
uint32_t maximum = 512;
if (argc == 3 && !parse_unsigned(argv[2], 1, 512, &maximum)) {
printf("Read size must be 1..512 bytes.\n");
return 1;
}
if (argc > 3) {
print_usage();
return 1;
}
uint8_t data[512];
size_t received = serial_service_read(data, maximum);
printf("Read %u byte%s", (unsigned int)received, received == 1 ? "" : "s");
if (received > 0) {
printf(": ");
for (size_t index = 0; index < received; ++index) {
printf("%02x", data[index]);
}
}
printf("\n");
return 0;
}
static int command_serial(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
@@ -345,21 +286,6 @@ static int command_serial(int argc, char **argv)
printf("Serial counters cleared.\n");
return 0;
}
if (argc == 3 && strcmp(argv[1], "send-hex") == 0) {
if (!serial_service_is_running()) {
printf("UART service is stopped; run 'serial start' first.\n");
return 1;
}
return send_hexadecimal(argv[2]);
}
if ((argc == 2 || argc == 3) && strcmp(argv[1], "read") == 0) {
if (!serial_service_is_running()) {
printf("UART service is stopped; run 'serial start' first.\n");
return 1;
}
return read_hexadecimal(argc, argv);
}
print_usage();
return 1;
}
+9 -3
View File
@@ -625,7 +625,9 @@ size_t serial_service_read(uint8_t *data, size_t size)
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
if (xSemaphoreTake(s_state_mutex, 0) != pdTRUE) {
return 0;
}
size_t received = 0;
if (s_running && !s_stop_requested) {
received = xStreamBufferReceive(s_rx_stream, data, size, 0);
@@ -640,7 +642,9 @@ size_t serial_service_write(const uint8_t *data, size_t size)
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
if (xSemaphoreTake(s_state_mutex, 0) != pdTRUE) {
return 0;
}
size_t accepted = 0;
if (s_running && !s_stop_requested) {
accepted = xStreamBufferSend(s_tx_stream, data, size, 0);
@@ -683,11 +687,13 @@ esp_err_t serial_service_set_session_active(bool active)
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
s_session_active = active;
esp_err_t result = ESP_OK;
if (s_running && s_config.dtr_behavior == SERIAL_CONFIG_DTR_ON_CONNECT) {
result = gpio_set_level(RS232_DTR_GPIO, active ? 0 : 1);
}
if (result == ESP_OK) {
s_session_active = active;
}
xSemaphoreGive(s_state_mutex);
return result;
}
+1 -2
View File
@@ -48,9 +48,8 @@ bool serial_service_is_running(void);
esp_err_t serial_service_apply_config(const serial_config_t *config);
esp_err_t serial_service_get_config(serial_config_t *config);
/* Future broker clients use these binary-transparent, bounded buffer APIs. */
/*
* Access is intentionally nonblocking. The session broker will be the sole
* Access is intentionally nonblocking. The session broker is the sole
* logical RX consumer and TX producer; calls are serialized internally to
* satisfy FreeRTOS stream-buffer concurrency rules.
*/
+688
View File
@@ -0,0 +1,688 @@
#include "session_broker.h"
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/stream_buffer.h"
#include "freertos/task.h"
#include "serial_service.h"
#define SESSION_BROKER_RX_CHUNK_SIZE 256U
#define SESSION_BROKER_TASK_STACK_SIZE 4096U
#define SESSION_BROKER_TASK_PRIORITY 9U
#define SESSION_BROKER_IDLE_POLL_MS 5U
#define SESSION_BROKER_SLOT_BITS 3U
#define SESSION_BROKER_SLOT_MASK ((1U << SESSION_BROKER_SLOT_BITS) - 1U)
#define SESSION_BROKER_MAX_GENERATION (UINT32_MAX >> SESSION_BROKER_SLOT_BITS)
_Static_assert(
SESSION_BROKER_MAX_CLIENTS == (1U << SESSION_BROKER_SLOT_BITS),
"Client slot bit width must exactly represent SESSION_BROKER_MAX_CLIENTS");
typedef struct {
StreamBufferHandle_t output;
QueueHandle_t events;
session_broker_client_id_t id;
uint32_t generation;
session_broker_client_type_t type;
char name[SESSION_BROKER_CLIENT_NAME_MAX + 1U];
bool connected;
session_broker_client_counters_t counters;
} session_broker_slot_t;
static SemaphoreHandle_t s_mutex;
static TaskHandle_t s_broker_task;
static session_broker_slot_t s_slots[SESSION_BROKER_MAX_CLIENTS];
static session_broker_client_id_t s_writer_id;
static uint32_t s_connected_clients;
static uint64_t s_event_sequence;
static session_broker_global_counters_t s_counters;
static bool s_initialized;
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
{
TickType_t ticks = pdMS_TO_TICKS(milliseconds);
return (milliseconds > 0U && ticks == 0U) ? 1U : ticks;
}
static bool client_type_valid(session_broker_client_type_t type)
{
return type >= SESSION_BROKER_CLIENT_CONSOLE &&
type <= SESSION_BROKER_CLIENT_INTERNAL;
}
static session_broker_client_id_t make_client_id(size_t slot_index, uint32_t generation)
{
return (session_broker_client_id_t)((generation << SESSION_BROKER_SLOT_BITS) |
(uint32_t)slot_index);
}
static session_broker_slot_t *find_slot_locked(session_broker_client_id_t client_id)
{
if (client_id == SESSION_BROKER_NO_CLIENT) {
return NULL;
}
size_t slot_index = client_id & SESSION_BROKER_SLOT_MASK;
if (slot_index >= SESSION_BROKER_MAX_CLIENTS) {
return NULL;
}
session_broker_slot_t *slot = &s_slots[slot_index];
if (!slot->connected || slot->id != client_id) {
return NULL;
}
return slot;
}
static void fill_client_snapshot_locked(const session_broker_slot_t *slot,
session_broker_client_snapshot_t *snapshot)
{
snapshot->id = slot->id;
snapshot->type = slot->type;
memcpy(snapshot->name, slot->name, sizeof(snapshot->name));
snapshot->is_writer = slot->id == s_writer_id;
snapshot->output_bytes_pending = xStreamBufferBytesAvailable(slot->output);
snapshot->events_pending = (uint32_t)uxQueueMessagesWaiting(slot->events);
snapshot->counters = slot->counters;
}
static void broadcast_event_locked(session_broker_event_type_t type,
session_broker_client_id_t client_id,
session_broker_client_id_t writer_id)
{
session_broker_event_t event = {
.sequence = ++s_event_sequence,
.type = type,
.client_id = client_id,
.writer_id = writer_id,
};
++s_counters.events_generated;
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS; ++i) {
session_broker_slot_t *slot = &s_slots[i];
if (!slot->connected) {
continue;
}
if (xQueueSend(slot->events, &event, 0) == pdTRUE) {
++slot->counters.events_queued;
++s_counters.events_queued;
} else {
++slot->counters.event_drops;
++s_counters.event_drops;
}
}
}
static void fan_out_rx_locked(const uint8_t *data, size_t size)
{
s_counters.uart_rx_bytes += size;
if (s_connected_clients == 0U) {
/* UART RX is always drained, even when nobody can observe the bytes. */
s_counters.unobserved_rx_bytes += size;
return;
}
/*
* The broker task is the sole output-stream writer. Every send is
* zero-time, so a slow observer only fills its own bounded stream and
* accrues drops; it can never stall UART draining or another observer.
*/
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS; ++i) {
session_broker_slot_t *slot = &s_slots[i];
if (!slot->connected) {
continue;
}
slot->counters.uart_rx_bytes += size;
size_t queued = xStreamBufferSend(slot->output, data, size, 0);
size_t dropped = size - queued;
slot->counters.output_queued_bytes += queued;
slot->counters.output_dropped_bytes += dropped;
s_counters.output_queued_bytes += queued;
s_counters.output_dropped_bytes += dropped;
}
}
static void broker_task(void *context)
{
(void)context;
uint8_t data[SESSION_BROKER_RX_CHUNK_SIZE];
for (;;) {
/*
* Hold the broker mutex across the serial read and fan-out. This gives
* every byte batch an exact order relative to connect/disconnect and
* prevents data crossing client-generation boundaries.
*/
xSemaphoreTake(s_mutex, portMAX_DELAY);
size_t received = serial_service_read(data, sizeof(data));
if (received > 0U) {
fan_out_rx_locked(data, received);
}
xSemaphoreGive(s_mutex);
if (received == 0U) {
vTaskDelay(milliseconds_to_ticks(SESSION_BROKER_IDLE_POLL_MS));
}
}
}
static void cleanup_allocations(void)
{
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS; ++i) {
if (s_slots[i].events != NULL) {
vQueueDelete(s_slots[i].events);
s_slots[i].events = NULL;
}
if (s_slots[i].output != NULL) {
vStreamBufferDelete(s_slots[i].output);
s_slots[i].output = NULL;
}
}
if (s_mutex != NULL) {
vSemaphoreDelete(s_mutex);
s_mutex = NULL;
}
}
esp_err_t session_broker_init(void)
{
if (s_initialized || s_mutex != NULL) {
return ESP_ERR_INVALID_STATE;
}
memset(s_slots, 0, sizeof(s_slots));
memset(&s_counters, 0, sizeof(s_counters));
s_writer_id = SESSION_BROKER_NO_CLIENT;
s_connected_clients = 0U;
s_event_sequence = 0U;
s_broker_task = NULL;
s_mutex = xSemaphoreCreateMutex();
if (s_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS; ++i) {
s_slots[i].output = xStreamBufferCreate(SESSION_BROKER_OUTPUT_SIZE, 1U);
s_slots[i].events = xQueueCreate(SESSION_BROKER_EVENT_QUEUE_LENGTH,
sizeof(session_broker_event_t));
if (s_slots[i].output == NULL || s_slots[i].events == NULL) {
cleanup_allocations();
return ESP_ERR_NO_MEM;
}
}
if (xTaskCreate(broker_task,
"session_broker",
SESSION_BROKER_TASK_STACK_SIZE,
NULL,
SESSION_BROKER_TASK_PRIORITY,
&s_broker_task) != pdPASS) {
s_broker_task = NULL;
cleanup_allocations();
return ESP_ERR_NO_MEM;
}
s_initialized = true;
return ESP_OK;
}
esp_err_t session_broker_connect(session_broker_client_type_t type,
const char *name,
session_broker_client_id_t *client_id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (!client_type_valid(type) || name == NULL || client_id == NULL) {
return ESP_ERR_INVALID_ARG;
}
size_t name_length = strnlen(name, SESSION_BROKER_CLIENT_NAME_MAX + 1U);
if (name_length > SESSION_BROKER_CLIENT_NAME_MAX) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_slot_t *slot = NULL;
size_t slot_index = 0U;
for (; slot_index < SESSION_BROKER_MAX_CLIENTS; ++slot_index) {
if (!s_slots[slot_index].connected) {
slot = &s_slots[slot_index];
break;
}
}
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NO_MEM;
}
/* DTR on-connect must succeed before this connection becomes observable. */
if (s_connected_clients == 0U) {
esp_err_t result = serial_service_set_session_active(true);
if (result != ESP_OK) {
xSemaphoreGive(s_mutex);
return result;
}
}
uint32_t generation = slot->generation + 1U;
if (generation == 0U || generation > SESSION_BROKER_MAX_GENERATION) {
generation = 1U;
}
xStreamBufferReset(slot->output);
xQueueReset(slot->events);
memset(&slot->counters, 0, sizeof(slot->counters));
slot->generation = generation;
slot->id = make_client_id(slot_index, generation);
slot->type = type;
memset(slot->name, 0, sizeof(slot->name));
memcpy(slot->name, name, name_length);
slot->connected = true;
++s_connected_clients;
++slot->counters.connections;
++s_counters.connections;
*client_id = slot->id;
broadcast_event_locked(SESSION_BROKER_EVENT_CONNECT, slot->id, s_writer_id);
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_disconnect(session_broker_client_id_t client_id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
/* A failed last-client DTR transition leaves the session fully intact. */
if (s_connected_clients == 1U) {
esp_err_t dtr_error = serial_service_set_session_active(false);
if (dtr_error != ESP_OK) {
xSemaphoreGive(s_mutex);
return dtr_error;
}
}
bool released_writer = s_writer_id == client_id;
slot->connected = false;
--s_connected_clients;
if (released_writer) {
s_writer_id = SESSION_BROKER_NO_CLIENT;
++slot->counters.writer_releases;
++slot->counters.writer_changes;
++s_counters.writer_releases;
++s_counters.writer_changes;
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_RELEASED,
client_id,
s_writer_id);
}
++s_counters.disconnections;
broadcast_event_locked(SESSION_BROKER_EVENT_DISCONNECT, client_id, s_writer_id);
/* Account for accepted output/events that the disconnect discards. */
size_t unread_output = xStreamBufferBytesAvailable(slot->output);
UBaseType_t unpopped_events = uxQueueMessagesWaiting(slot->events);
slot->counters.output_dropped_bytes += unread_output;
slot->counters.event_drops += unpopped_events;
s_counters.output_dropped_bytes += unread_output;
s_counters.event_drops += unpopped_events;
/* Zero-time resets are mutex-protected against the broker writer/reader. */
xStreamBufferReset(slot->output);
xQueueReset(slot->events);
slot->id = SESSION_BROKER_NO_CLIENT;
slot->name[0] = '\0';
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_request_writer(session_broker_client_id_t client_id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
++slot->counters.writer_requests;
++s_counters.writer_requests;
if (s_writer_id == client_id) {
xSemaphoreGive(s_mutex);
return ESP_OK;
}
if (s_writer_id != SESSION_BROKER_NO_CLIENT) {
++slot->counters.writer_denials;
++s_counters.writer_denials;
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_DENIED,
client_id,
s_writer_id);
xSemaphoreGive(s_mutex);
return ESP_ERR_INVALID_STATE;
}
s_writer_id = client_id;
++slot->counters.writer_grants;
++slot->counters.writer_changes;
++s_counters.writer_grants;
++s_counters.writer_changes;
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_GRANTED,
client_id,
s_writer_id);
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_release_writer(session_broker_client_id_t client_id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
if (s_writer_id != client_id) {
xSemaphoreGive(s_mutex);
return ESP_ERR_INVALID_STATE;
}
s_writer_id = SESSION_BROKER_NO_CLIENT;
++slot->counters.writer_releases;
++slot->counters.writer_changes;
++s_counters.writer_releases;
++s_counters.writer_changes;
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_RELEASED,
client_id,
s_writer_id);
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_force_writer(session_broker_client_id_t client_id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_slot_t *new_writer = NULL;
if (client_id != SESSION_BROKER_NO_CLIENT) {
new_writer = find_slot_locked(client_id);
if (new_writer == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
}
if (s_writer_id == client_id) {
xSemaphoreGive(s_mutex);
return ESP_OK;
}
session_broker_client_id_t old_writer_id = s_writer_id;
session_broker_slot_t *old_writer = find_slot_locked(old_writer_id);
/* Every emitted event carries the final atomic ownership state. */
s_writer_id = client_id;
if (old_writer != NULL) {
++old_writer->counters.writer_revocations;
++old_writer->counters.writer_changes;
++s_counters.writer_revocations;
}
if (new_writer != NULL) {
++new_writer->counters.writer_grants;
++new_writer->counters.writer_changes;
++s_counters.writer_grants;
}
++s_counters.writer_changes;
if (old_writer != NULL) {
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_REVOKED,
old_writer_id,
s_writer_id);
}
if (new_writer != NULL) {
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_GRANTED,
new_writer->id,
s_writer_id);
}
xSemaphoreGive(s_mutex);
return ESP_OK;
}
session_broker_client_id_t session_broker_get_writer_id(void)
{
if (!s_initialized) {
return SESSION_BROKER_NO_CLIENT;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_client_id_t writer_id = s_writer_id;
xSemaphoreGive(s_mutex);
return writer_id;
}
esp_err_t session_broker_write(session_broker_client_id_t client_id,
const uint8_t *data,
size_t size,
size_t *accepted)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (accepted == NULL || (data == NULL && size > 0U)) {
return ESP_ERR_INVALID_ARG;
}
*accepted = 0U;
if (xSemaphoreTake(s_mutex, 0) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
s_counters.tx_rejected_bytes += size;
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
if (s_writer_id != client_id) {
slot->counters.tx_rejected_bytes += size;
s_counters.tx_rejected_bytes += size;
xSemaphoreGive(s_mutex);
return ESP_ERR_INVALID_STATE;
}
size_t queued = serial_service_write(data, size);
size_t rejected = size - queued;
slot->counters.tx_accepted_bytes += queued;
slot->counters.tx_rejected_bytes += rejected;
s_counters.tx_accepted_bytes += queued;
s_counters.tx_rejected_bytes += rejected;
*accepted = queued;
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_read(session_broker_client_id_t client_id,
uint8_t *data,
size_t size,
size_t *received)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (received == NULL || (data == NULL && size > 0U)) {
return ESP_ERR_INVALID_ARG;
}
*received = 0U;
if (xSemaphoreTake(s_mutex, 0) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
if (size == 0U) {
xSemaphoreGive(s_mutex);
return ESP_OK;
}
size_t count = xStreamBufferReceive(slot->output, data, size, 0);
slot->counters.output_read_bytes += count;
s_counters.output_read_bytes += count;
*received = count;
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_pop_event(session_broker_client_id_t client_id,
session_broker_event_t *event)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (event == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (xSemaphoreTake(s_mutex, 0) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
if (xQueueReceive(slot->events, event, 0) != pdTRUE) {
xSemaphoreGive(s_mutex);
return ESP_ERR_TIMEOUT;
}
++slot->counters.events_popped;
++s_counters.events_popped;
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_get_client_snapshot(session_broker_client_id_t client_id,
session_broker_client_snapshot_t *snapshot)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
fill_client_snapshot_locked(slot, snapshot);
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_get_global_snapshot(session_broker_global_snapshot_t *snapshot)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
snapshot->writer_id = s_writer_id;
snapshot->connected_clients = s_connected_clients;
snapshot->latest_event_sequence = s_event_sequence;
snapshot->counters = s_counters;
xSemaphoreGive(s_mutex);
return ESP_OK;
}
size_t session_broker_list_clients(session_broker_client_snapshot_t *clients,
size_t capacity)
{
if (!s_initialized || (clients == NULL && capacity > 0U)) {
return 0U;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
if (clients == NULL) {
size_t count = s_connected_clients;
xSemaphoreGive(s_mutex);
return count;
}
size_t copied = 0U;
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS && copied < capacity; ++i) {
if (!s_slots[i].connected) {
continue;
}
fill_client_snapshot_locked(&s_slots[i], &clients[copied]);
++copied;
}
xSemaphoreGive(s_mutex);
return copied;
}
esp_err_t session_broker_clear_counters(void)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
memset(&s_counters, 0, sizeof(s_counters));
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS; ++i) {
memset(&s_slots[i].counters, 0, sizeof(s_slots[i].counters));
}
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_clear_client_counters(session_broker_client_id_t client_id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
session_broker_slot_t *slot = find_slot_locked(client_id);
if (slot == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
memset(&slot->counters, 0, sizeof(slot->counters));
xSemaphoreGive(s_mutex);
return ESP_OK;
}
+165
View File
@@ -0,0 +1,165 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SESSION_BROKER_MAX_CLIENTS 8U
#define SESSION_BROKER_CLIENT_NAME_MAX 23U
#define SESSION_BROKER_OUTPUT_SIZE 4096U
#define SESSION_BROKER_EVENT_QUEUE_LENGTH 16U
typedef uint32_t session_broker_client_id_t;
/* ID zero is reserved for "no client" and, specifically, "no writer". */
#define SESSION_BROKER_NO_CLIENT ((session_broker_client_id_t)0U)
typedef enum {
SESSION_BROKER_CLIENT_CONSOLE = 0,
SESSION_BROKER_CLIENT_USB,
SESSION_BROKER_CLIENT_WEB,
SESSION_BROKER_CLIENT_SSH,
SESSION_BROKER_CLIENT_INTERNAL,
} session_broker_client_type_t;
typedef enum {
SESSION_BROKER_EVENT_CONNECT = 0,
SESSION_BROKER_EVENT_DISCONNECT,
SESSION_BROKER_EVENT_WRITER_GRANTED,
SESSION_BROKER_EVENT_WRITER_RELEASED,
SESSION_BROKER_EVENT_WRITER_REVOKED,
SESSION_BROKER_EVENT_WRITER_DENIED,
} session_broker_event_type_t;
/*
* Events contain no pointers, so transports can encode them without lifetime
* concerns. sequence is broker-global and strictly increases per event.
* client_id identifies the subject; writer_id is the writer after the event.
*/
typedef struct {
uint64_t sequence;
session_broker_event_type_t type;
session_broker_client_id_t client_id;
session_broker_client_id_t writer_id;
} session_broker_event_t;
typedef struct {
/* UART bytes considered for delivery while this client was connected. */
uint64_t uart_rx_bytes;
uint64_t output_queued_bytes;
uint64_t output_read_bytes;
uint64_t output_dropped_bytes;
uint64_t tx_accepted_bytes;
uint64_t tx_rejected_bytes;
uint64_t connections;
uint64_t writer_requests;
uint64_t writer_grants;
uint64_t writer_releases;
uint64_t writer_revocations;
uint64_t writer_denials;
uint64_t writer_changes;
uint64_t events_queued;
uint64_t events_popped;
uint64_t event_drops;
} session_broker_client_counters_t;
typedef struct {
uint64_t uart_rx_bytes;
/* UART RX drained when there were no connected observers. */
uint64_t unobserved_rx_bytes;
/* Queue/read/drop totals count one copy per client observer. */
uint64_t output_queued_bytes;
uint64_t output_read_bytes;
uint64_t output_dropped_bytes;
uint64_t tx_accepted_bytes;
uint64_t tx_rejected_bytes;
uint64_t connections;
uint64_t disconnections;
uint64_t writer_requests;
uint64_t writer_grants;
uint64_t writer_releases;
uint64_t writer_revocations;
uint64_t writer_denials;
uint64_t writer_changes;
uint64_t events_generated;
uint64_t events_queued;
uint64_t events_popped;
uint64_t event_drops;
} session_broker_global_counters_t;
typedef struct {
session_broker_client_id_t id;
session_broker_client_type_t type;
char name[SESSION_BROKER_CLIENT_NAME_MAX + 1U];
bool is_writer;
size_t output_bytes_pending;
uint32_t events_pending;
session_broker_client_counters_t counters;
} session_broker_client_snapshot_t;
typedef struct {
session_broker_client_id_t writer_id;
uint32_t connected_clients;
uint64_t latest_event_sequence;
session_broker_global_counters_t counters;
} session_broker_global_snapshot_t;
/*
* Allocates all eight output streams and event queues, then starts the
* permanent broker task. The serial service must already be initialized
* before the first client connects.
*/
esp_err_t session_broker_init(void);
esp_err_t session_broker_connect(session_broker_client_type_t type,
const char *name,
session_broker_client_id_t *client_id);
esp_err_t session_broker_disconnect(session_broker_client_id_t client_id);
/*
* There is exactly one writer lease. Requests are granted only while idle;
* a competing request emits WRITER_DENIED. Force with ID zero revokes the
* current writer without assigning a replacement.
*/
esp_err_t session_broker_request_writer(session_broker_client_id_t client_id);
esp_err_t session_broker_release_writer(session_broker_client_id_t client_id);
esp_err_t session_broker_force_writer(session_broker_client_id_t client_id);
session_broker_client_id_t session_broker_get_writer_id(void);
/*
* Binary-transparent, zero-time data operations. write is accepted only from
* the current writer. read has one logical caller per connected transport.
* Partial transfers are normal and are reported through the output count.
*/
esp_err_t session_broker_write(session_broker_client_id_t client_id,
const uint8_t *data,
size_t size,
size_t *accepted);
esp_err_t session_broker_read(session_broker_client_id_t client_id,
uint8_t *data,
size_t size,
size_t *received);
esp_err_t session_broker_pop_event(session_broker_client_id_t client_id,
session_broker_event_t *event);
esp_err_t session_broker_get_client_snapshot(session_broker_client_id_t client_id,
session_broker_client_snapshot_t *snapshot);
esp_err_t session_broker_get_global_snapshot(session_broker_global_snapshot_t *snapshot);
/* Returns the number copied. Pass NULL with capacity zero to count clients. */
size_t session_broker_list_clients(session_broker_client_snapshot_t *clients,
size_t capacity);
/* Counter clearing does not reset client IDs, queued data, or event sequence. */
esp_err_t session_broker_clear_counters(void);
esp_err_t session_broker_clear_client_counters(session_broker_client_id_t client_id);
#ifdef __cplusplus
}
#endif
+447
View File
@@ -0,0 +1,447 @@
#include "session_console.h"
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_console.h"
#include "esp_err.h"
#include "session_broker.h"
#define CONSOLE_HEX_MAX_BYTES 48
#define CONSOLE_READ_MAX_BYTES 512
static void print_usage(void)
{
printf("Usage:\n");
printf(" broker status|clients|counters|clear-counters\n");
printf(" broker connect <name>\n");
printf(" broker disconnect <client-id>\n");
printf(" broker request-writer <client-id>\n");
printf(" broker release-writer <client-id>\n");
printf(" broker force-writer <client-id|none>\n");
printf(" broker send-hex <client-id> <hex-bytes>\n");
printf(" broker read <client-id> [maximum-bytes]\n");
printf(" broker events <client-id>\n");
}
static bool parse_u32(const char *text, uint32_t minimum, uint32_t maximum, uint32_t *value)
{
if (text == NULL || *text == '\0') {
return false;
}
for (const char *character = text; *character != '\0'; ++character) {
if (*character < '0' || *character > '9') {
return false;
}
}
char *end = NULL;
errno = 0;
unsigned long parsed = strtoul(text, &end, 10);
if (errno != 0 || end == text || *end != '\0' ||
parsed < minimum || parsed > maximum) {
return false;
}
*value = (uint32_t)parsed;
return true;
}
static bool parse_client_id(const char *text, session_broker_client_id_t *client_id)
{
uint32_t value;
if (!parse_u32(text, 1, UINT32_MAX, &value)) {
return false;
}
*client_id = value;
return true;
}
static const char *client_type_name(session_broker_client_type_t type)
{
switch (type) {
case SESSION_BROKER_CLIENT_CONSOLE:
return "console";
case SESSION_BROKER_CLIENT_USB:
return "usb";
case SESSION_BROKER_CLIENT_WEB:
return "web";
case SESSION_BROKER_CLIENT_SSH:
return "ssh";
case SESSION_BROKER_CLIENT_INTERNAL:
return "internal";
default:
return "unknown";
}
}
static const char *event_type_name(session_broker_event_type_t type)
{
switch (type) {
case SESSION_BROKER_EVENT_CONNECT:
return "connect";
case SESSION_BROKER_EVENT_DISCONNECT:
return "disconnect";
case SESSION_BROKER_EVENT_WRITER_GRANTED:
return "writer-granted";
case SESSION_BROKER_EVENT_WRITER_RELEASED:
return "writer-released";
case SESSION_BROKER_EVENT_WRITER_REVOKED:
return "writer-revoked";
case SESSION_BROKER_EVENT_WRITER_DENIED:
return "writer-denied";
default:
return "unknown";
}
}
static int show_status(void)
{
session_broker_global_snapshot_t snapshot;
esp_err_t err = session_broker_get_global_snapshot(&snapshot);
if (err != ESP_OK) {
printf("Could not read broker status: %s\n", esp_err_to_name(err));
return 1;
}
printf("Broker clients=%" PRIu32 " writer=%lu latest-event=%" PRIu64 "\n",
snapshot.connected_clients,
(unsigned long)snapshot.writer_id,
snapshot.latest_event_sequence);
printf("UART-RX=%" PRIu64 " unobserved=%" PRIu64
" observer-queued=%" PRIu64 " observer-read=%" PRIu64
" observer-dropped=%" PRIu64 "\n",
snapshot.counters.uart_rx_bytes,
snapshot.counters.unobserved_rx_bytes,
snapshot.counters.output_queued_bytes,
snapshot.counters.output_read_bytes,
snapshot.counters.output_dropped_bytes);
return 0;
}
static int show_clients(void)
{
session_broker_client_snapshot_t clients[SESSION_BROKER_MAX_CLIENTS];
size_t count = session_broker_list_clients(clients, SESSION_BROKER_MAX_CLIENTS);
if (count == 0) {
printf("No broker clients connected.\n");
return 0;
}
printf("ID type writer pending events name\n");
for (size_t index = 0; index < count; ++index) {
const session_broker_client_snapshot_t *client = &clients[index];
printf("%-10lu %-9s %-6s %-7u %-6" PRIu32 " %s\n",
(unsigned long)client->id,
client_type_name(client->type),
client->is_writer ? "yes" : "no",
(unsigned int)client->output_bytes_pending,
client->events_pending,
client->name);
}
return 0;
}
static int show_counters(void)
{
session_broker_global_snapshot_t snapshot;
esp_err_t err = session_broker_get_global_snapshot(&snapshot);
if (err != ESP_OK) {
printf("Could not read broker counters: %s\n", esp_err_to_name(err));
return 1;
}
const session_broker_global_counters_t *counter = &snapshot.counters;
printf("RX: UART=%" PRIu64 " unobserved=%" PRIu64
" queued=%" PRIu64 " read=%" PRIu64 " dropped=%" PRIu64 "\n",
counter->uart_rx_bytes,
counter->unobserved_rx_bytes,
counter->output_queued_bytes,
counter->output_read_bytes,
counter->output_dropped_bytes);
printf("TX: accepted=%" PRIu64 " rejected=%" PRIu64 "\n",
counter->tx_accepted_bytes,
counter->tx_rejected_bytes);
printf("Sessions: connect=%" PRIu64 " disconnect=%" PRIu64
" writer-requests=%" PRIu64 " grants=%" PRIu64
" releases=%" PRIu64 " revocations=%" PRIu64
" denials=%" PRIu64 " changes=%" PRIu64 "\n",
counter->connections,
counter->disconnections,
counter->writer_requests,
counter->writer_grants,
counter->writer_releases,
counter->writer_revocations,
counter->writer_denials,
counter->writer_changes);
printf("Events: generated=%" PRIu64 " queued=%" PRIu64
" popped=%" PRIu64 " dropped=%" PRIu64 "\n",
counter->events_generated,
counter->events_queued,
counter->events_popped,
counter->event_drops);
return 0;
}
static int connect_client(const char *name)
{
session_broker_client_id_t client_id;
esp_err_t err = session_broker_connect(
SESSION_BROKER_CLIENT_CONSOLE,
name,
&client_id);
if (err != ESP_OK) {
printf("Could not connect broker client: %s\n", esp_err_to_name(err));
return 1;
}
printf("Connected console client '%s' with ID %lu.\n", name, (unsigned long)client_id);
return 0;
}
static int disconnect_client(const char *id_text)
{
session_broker_client_id_t client_id;
if (!parse_client_id(id_text, &client_id)) {
printf("Invalid client ID.\n");
return 1;
}
esp_err_t err = session_broker_disconnect(client_id);
if (err != ESP_OK) {
printf("Could not disconnect client %lu: %s\n", (unsigned long)client_id, esp_err_to_name(err));
return 1;
}
printf("Disconnected client %lu.\n", (unsigned long)client_id);
return 0;
}
static int change_writer(const char *operation, const char *id_text)
{
session_broker_client_id_t client_id;
if (!parse_client_id(id_text, &client_id)) {
printf("Invalid client ID.\n");
return 1;
}
esp_err_t err;
if (strcmp(operation, "request-writer") == 0) {
err = session_broker_request_writer(client_id);
} else {
err = session_broker_release_writer(client_id);
}
if (err != ESP_OK) {
printf("Writer operation failed for client %lu: %s\n",
(unsigned long)client_id, esp_err_to_name(err));
return 1;
}
printf("Writer is now %lu.\n", (unsigned long)session_broker_get_writer_id());
return 0;
}
static int force_writer(const char *id_text)
{
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
if (strcmp(id_text, "none") != 0 && !parse_client_id(id_text, &client_id)) {
printf("Writer must be a connected client ID or 'none'.\n");
return 1;
}
esp_err_t err = session_broker_force_writer(client_id);
if (err != ESP_OK) {
printf("Could not force writer: %s\n", esp_err_to_name(err));
return 1;
}
printf("Writer is now %lu.\n", (unsigned long)session_broker_get_writer_id());
return 0;
}
static int hexadecimal_value(char character)
{
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return -1;
}
static int send_hexadecimal(const char *id_text, const char *text)
{
session_broker_client_id_t client_id;
if (!parse_client_id(id_text, &client_id)) {
printf("Invalid client ID.\n");
return 1;
}
uint8_t data[CONSOLE_HEX_MAX_BYTES];
size_t text_length = strlen(text);
if (text_length == 0 || (text_length % 2) != 0 || text_length > sizeof(data) * 2) {
printf("Provide 1..%d bytes as hexadecimal digits without separators.\n", CONSOLE_HEX_MAX_BYTES);
return 1;
}
size_t data_length = text_length / 2;
for (size_t index = 0; index < data_length; ++index) {
int high = hexadecimal_value(text[index * 2]);
int low = hexadecimal_value(text[index * 2 + 1]);
if (high < 0 || low < 0) {
size_t invalid_index = index * 2 + (high < 0 ? 0 : 1);
printf("Invalid hexadecimal digit at character %u.\n", (unsigned int)invalid_index);
return 1;
}
data[index] = (uint8_t)((high << 4) | low);
}
size_t accepted = 0;
esp_err_t err = session_broker_write(client_id, data, data_length, &accepted);
if (err != ESP_OK) {
printf("Client %lu could not write: %s\n", (unsigned long)client_id, esp_err_to_name(err));
return 1;
}
printf("Broker accepted %u of %u bytes from client %lu.\n",
(unsigned int)accepted,
(unsigned int)data_length,
(unsigned long)client_id);
return accepted == data_length ? 0 : 1;
}
static int read_hexadecimal(int argc, char **argv)
{
session_broker_client_id_t client_id;
if (!parse_client_id(argv[2], &client_id)) {
printf("Invalid client ID.\n");
return 1;
}
uint32_t maximum = CONSOLE_READ_MAX_BYTES;
if (argc == 4 && !parse_u32(argv[3], 1, CONSOLE_READ_MAX_BYTES, &maximum)) {
printf("Read size must be 1..%d bytes.\n", CONSOLE_READ_MAX_BYTES);
return 1;
}
uint8_t data[CONSOLE_READ_MAX_BYTES];
size_t received = 0;
esp_err_t err = session_broker_read(client_id, data, maximum, &received);
if (err != ESP_OK) {
printf("Could not read client %lu: %s\n", (unsigned long)client_id, esp_err_to_name(err));
return 1;
}
printf("Client %lu read %u byte%s",
(unsigned long)client_id,
(unsigned int)received,
received == 1 ? "" : "s");
if (received > 0) {
printf(": ");
for (size_t index = 0; index < received; ++index) {
printf("%02x", data[index]);
}
}
printf("\n");
return 0;
}
static int show_events(const char *id_text)
{
session_broker_client_id_t client_id;
if (!parse_client_id(id_text, &client_id)) {
printf("Invalid client ID.\n");
return 1;
}
session_broker_client_snapshot_t snapshot;
esp_err_t err = session_broker_get_client_snapshot(client_id, &snapshot);
if (err != ESP_OK) {
printf("Unknown client %lu: %s\n", (unsigned long)client_id, esp_err_to_name(err));
return 1;
}
unsigned int count = 0;
session_broker_event_t event;
while (true) {
err = session_broker_pop_event(client_id, &event);
if (err == ESP_ERR_TIMEOUT) {
break;
}
if (err != ESP_OK) {
printf("Could not pop events for client %lu: %s\n",
(unsigned long)client_id, esp_err_to_name(err));
return 1;
}
printf("#%" PRIu64 " %-16s subject=%lu writer=%lu\n",
event.sequence,
event_type_name(event.type),
(unsigned long)event.client_id,
(unsigned long)event.writer_id);
++count;
}
if (count == 0) {
printf("No pending events for client %lu.\n", (unsigned long)client_id);
}
return 0;
}
static int command_broker(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
return show_status();
}
if (argc == 2 && strcmp(argv[1], "clients") == 0) {
return show_clients();
}
if (argc == 2 && strcmp(argv[1], "counters") == 0) {
return show_counters();
}
if (argc == 2 && strcmp(argv[1], "clear-counters") == 0) {
esp_err_t err = session_broker_clear_counters();
if (err != ESP_OK) {
printf("Could not clear broker counters: %s\n", esp_err_to_name(err));
return 1;
}
printf("Broker counters cleared.\n");
return 0;
}
if (argc == 3 && strcmp(argv[1], "connect") == 0) {
return connect_client(argv[2]);
}
if (argc == 3 && strcmp(argv[1], "disconnect") == 0) {
return disconnect_client(argv[2]);
}
if (argc == 3 &&
(strcmp(argv[1], "request-writer") == 0 ||
strcmp(argv[1], "release-writer") == 0)) {
return change_writer(argv[1], argv[2]);
}
if (argc == 3 && strcmp(argv[1], "force-writer") == 0) {
return force_writer(argv[2]);
}
if (argc == 4 && strcmp(argv[1], "send-hex") == 0) {
return send_hexadecimal(argv[2], argv[3]);
}
if ((argc == 3 || argc == 4) && strcmp(argv[1], "read") == 0) {
return read_hexadecimal(argc, argv);
}
if (argc == 3 && strcmp(argv[1], "events") == 0) {
return show_events(argv[2]);
}
print_usage();
return 1;
}
esp_err_t session_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "broker",
.help = "Manage Phase 2 broker clients, writer ownership, events, and binary data",
.hint = NULL,
.func = &command_broker,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "esp_err.h"
/* Register the transport-neutral Phase 2 session-broker test console. */
esp_err_t session_console_register_commands(void);