Add Transport-Neutral Serial Session Broker
This commit is contained in:
@@ -12,7 +12,7 @@ Universal wireless serial adaptor firmware for the ESP32-S3.
|
||||
- 8 MB octal PSRAM
|
||||
- Adafruit MAX3243 full-pinout RS-232 breakout, product 5988
|
||||
|
||||
The firmware has completed **Phase 0 hardware characterization** and now includes the **Phase 1 serial-core foundation**. The MAX3243 diagnostics remain available, alongside a versioned NVS-backed serial configuration and a buffered UART1 service with modem-state monitoring and error counters. Neither the UART service nor an electrical test starts automatically at boot.
|
||||
The firmware has completed **Phase 0 hardware characterization** and the **Phase 1 serial-core foundation**. **Phase 2** adds a transport-neutral serial-session broker with one active writer, multiple observers, bounded per-client queues, event delivery, and slow-client isolation. The MAX3243 diagnostics and persistent serial configuration remain available. Neither the UART service nor an electrical test starts automatically at boot.
|
||||
|
||||
## Hardware wiring
|
||||
|
||||
@@ -50,8 +50,6 @@ serial defaults
|
||||
serial reset
|
||||
serial counters
|
||||
serial clear-counters
|
||||
serial send-hex <hex-bytes>
|
||||
serial read [maximum-bytes]
|
||||
```
|
||||
|
||||
Safe defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow control, and inactive DTR. Supported configuration values are:
|
||||
@@ -68,10 +66,37 @@ Safe defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow con
|
||||
|
||||
`serial set` changes the working configuration and safely restarts UART1 if the service is running. It does not write flash; use `serial save` to commit the current configuration to NVS. `serial defaults` changes RAM only, while `serial reset` applies and persists defaults. The firmware never erases the shared NVS partition automatically when storage is incompatible or unavailable.
|
||||
|
||||
The service uses independent software RX and TX streams. Calls into those streams are nonblocking, and a deasserted CTS cannot block service shutdown. `serial send-hex` and `serial read` are temporary binary-safe console clients for validation before the session broker and USB/network clients are added.
|
||||
The service uses independent software RX and TX streams. Calls into those streams are nonblocking, and a deasserted CTS cannot block service shutdown. UART data access is intentionally reserved for the session broker; the `serial` command controls configuration and lifecycle only.
|
||||
|
||||
UART1 has exclusive ownership while the service runs. Phase 0 commands will refuse to touch the port until `serial stop` releases it.
|
||||
|
||||
### Phase 2 session broker
|
||||
|
||||
The broker is initialized at boot and continuously drains the serial service whenever UART1 is running. It is transport-neutral: current console clients use the same API that native USB CDC, WebSocket, and SSH transports will use later.
|
||||
|
||||
```text
|
||||
broker status
|
||||
broker clients
|
||||
broker counters
|
||||
broker clear-counters
|
||||
broker connect <name>
|
||||
broker disconnect <client-id>
|
||||
broker request-writer <client-id>
|
||||
broker release-writer <client-id>
|
||||
broker force-writer <client-id|none>
|
||||
broker send-hex <client-id> <hex-bytes>
|
||||
broker read <client-id> [maximum-bytes]
|
||||
broker events <client-id>
|
||||
```
|
||||
|
||||
Each connection receives a generation-safe numeric ID. Stale IDs from disconnected clients cannot address a newly reused slot. Up to eight clients may connect, each with a bounded 4096-byte output queue and a 16-entry event queue.
|
||||
|
||||
UART RX is copied to every connected client. A full observer queue drops bytes only for that observer and records the loss; it never blocks UART reception or another client. With no clients, the broker still drains UART data and records it as unobserved.
|
||||
|
||||
Exactly one client may hold the writer lease. Competing requests are denied and generate events. Administrative forced reassignment atomically revokes the old writer and grants the new one. Bytes already accepted before revocation remain queued for transmission; revocation prevents future admission rather than purging the UART TX stream.
|
||||
|
||||
Connect, disconnect, writer grant, release, revoke, and denial events have a broker-global sequence number. Event queues are intentionally bounded, so future transports should reconcile sequence gaps against broker snapshots. The first and last broker connection also drive the Phase 1 `DTR=on-connect` policy.
|
||||
|
||||
### Phase 0 diagnostics
|
||||
|
||||
The retained hardware-characterization commands are:
|
||||
|
||||
@@ -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);
|
||||
@@ -209,9 +209,9 @@ Each output should be at a negative RS-232 voltage. Exact values vary with suppl
|
||||
|
||||
RTS and CTS remain ordinary GPIO signals during static and basic UART loopback tests. Only the two dedicated flow-control commands hand them to UART peripherals. Every test shuts the MAX3243 down while changing GPIO-matrix routing and restores all outputs to static logic 1 afterward.
|
||||
|
||||
## Phase 1 UART-service loopback
|
||||
## Phase 2 broker loopback
|
||||
|
||||
The Phase 1 service can be checked independently of the Phase 0 UART test implementation. Disconnect external peers, power down, remove all previous jumpers, and connect only:
|
||||
The Phase 2 broker can be checked independently of the Phase 0 UART test implementation. Disconnect external peers, power down, remove all previous jumpers, and connect only:
|
||||
|
||||
```text
|
||||
DE-9 pin 3 TX -> pin 2 RX
|
||||
@@ -222,19 +222,56 @@ Power up and use the default 115200 8N1 configuration:
|
||||
```text
|
||||
serial status
|
||||
serial start
|
||||
serial send-hex 0055aaff1b5b33316d
|
||||
serial read 64
|
||||
serial counters
|
||||
serial stop
|
||||
broker connect writer
|
||||
broker connect observer
|
||||
broker clients
|
||||
```
|
||||
|
||||
The read should return the exact bytes:
|
||||
On a fresh boot the returned IDs are normally 8 and 9, but always use the IDs printed by your device. In the commands below, substitute those values if they differ:
|
||||
|
||||
```text
|
||||
Read 9 bytes: 0055aaff1b5b33316d
|
||||
broker request-writer 8
|
||||
broker send-hex 8 0055aaff1b5b33316d
|
||||
broker read 8 64
|
||||
broker read 9 64
|
||||
```
|
||||
|
||||
If the first read occurs before UART1 has returned the bytes, it may report zero; run `serial read 64` again. Counters should show nine received, queued, and sent-to-UART bytes with no dropped bytes or UART errors. `serial stop` must return the RS-232 port owner to `idle`, after which Phase 0 commands are available again.
|
||||
Both clients are observers, including the active writer, so both reads should return the exact nine bytes:
|
||||
|
||||
```text
|
||||
Client 8 read 9 bytes: 0055aaff1b5b33316d
|
||||
Client 9 read 9 bytes: 0055aaff1b5b33316d
|
||||
```
|
||||
|
||||
If a read occurs before UART1 has returned the bytes, it may report zero; run it again. Next verify writer exclusion and forced reassignment:
|
||||
|
||||
```text
|
||||
broker request-writer 9
|
||||
broker send-hex 9 dead
|
||||
broker force-writer 9
|
||||
broker send-hex 9 112233
|
||||
broker read 8 64
|
||||
broker read 9 64
|
||||
broker events 8
|
||||
broker events 9
|
||||
broker counters
|
||||
```
|
||||
|
||||
The competing writer request and pre-reassignment send are expected to fail. After forced reassignment, both clients must receive `112233`. Events should describe the denied request, revocation of client 8, and grant to client 9, with the forced-change events reporting final writer 9.
|
||||
|
||||
Clean up in this order:
|
||||
|
||||
```text
|
||||
broker disconnect 9
|
||||
broker events 8
|
||||
broker disconnect 8
|
||||
broker status
|
||||
serial counters
|
||||
serial stop
|
||||
serial status
|
||||
```
|
||||
|
||||
Disconnecting client 9 automatically releases its writer lease. Client 8 receives the corresponding release and disconnect events; draining them before its own disconnect avoids intentionally counting them as discarded events. After the final disconnect, the broker should report zero clients and writer 0. `serial stop` must return the RS-232 port owner to `idle`, after which Phase 0 commands are available again.
|
||||
|
||||
Remove the loopback jumper with power off before connecting an external serial peer.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user