Add Transport-Neutral Serial Session Broker
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user