Add Phase 1 UART service foundation

Add versioned NVS-backed configuration, buffered UART1 I/O, modem
monitoring, counters, and serial console controls. Coordinate UART1
ownership with Phase 0 diagnostics and document loopback verification.
This commit is contained in:
2026-08-22 23:23:13 +02:00
parent 126314a277
commit 535c27350d
13 changed files with 1961 additions and 30 deletions
+45 -2
View File
@@ -12,7 +12,7 @@ Universal wireless serial adaptor firmware for the ESP32-S3.
- 8 MB octal PSRAM - 8 MB octal PSRAM
- Adafruit MAX3243 full-pinout RS-232 breakout, product 5988 - Adafruit MAX3243 full-pinout RS-232 breakout, product 5988
The current firmware is a command-driven **Phase 0 hardware-characterization harness**. It validates the MAX3243 breakout, its three drivers, its five receivers, valid-signal detection, active-low shutdown, and UART1 loopback before development of the wireless serial firmware begins. Tests run only when explicitly requested at the console; booting the board does not start an electrical test. 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.
## Hardware wiring ## Hardware wiring
@@ -33,7 +33,48 @@ pio run --target upload
pio device monitor -b 115200 pio device monitor -b 115200
``` ```
The firmware starts an interactive console on UART0. Type `help` to display command descriptions. The Phase 0 commands are: The firmware starts an interactive console on UART0 with the prompt `serial-tool>`. Type `help` to display command descriptions.
### Phase 1 serial service
The `serial` command manages the working configuration and UART1 service:
```text
serial status
serial start
serial stop
serial set <baud|data-bits|parity|stop-bits|flow|dtr|rts-threshold> <value>
serial save
serial load
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:
| Parameter | Values |
|---|---|
| `baud` | 1101000000 |
| `data-bits` | `7`, `8` |
| `parity` | `none`, `even`, `odd` |
| `stop-bits` | `1`, `2` |
| `flow` | `none`, `rts-cts` |
| `dtr` | `inactive`, `active`, `on-connect` |
| `rts-threshold` | 1127 bytes |
`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.
UART1 has exclusive ownership while the service runs. Phase 0 commands will refuse to touch the port until `serial stop` releases it.
### Phase 0 diagnostics
The retained hardware-characterization commands are:
```text ```text
status status
@@ -50,6 +91,8 @@ rts-flow-test
`uart-loopback` defaults to `8N1` and 256 bytes. Its accepted payload range is 1512 bytes. `uart-suite` covers 300 through 250000 baud and all supported frame formats. `cts-flow-test` verifies transmit gating and exact resumption, while `rts-flow-test` uses UART2 as an internal traffic generator to verify automatic receive backpressure. Follow the command-specific loopback wiring in [`wiring.md`](wiring.md) before invoking any test. `uart-loopback` defaults to `8N1` and 256 bytes. Its accepted payload range is 1512 bytes. `uart-suite` covers 300 through 250000 baud and all supported frame formats. `cts-flow-test` verifies transmit gating and exact resumption, while `rts-flow-test` uses UART2 as an internal traffic generator to verify automatic receive backpressure. Follow the command-specific loopback wiring in [`wiring.md`](wiring.md) before invoking any test.
A mutex-protected port lease prevents diagnostics, UART1 service startup, and future clients from reconfiguring the same GPIOs concurrently. If a UART driver cannot be removed during cleanup, the firmware keeps the MAX3243 shut down and marks the port faulted until reboot rather than exposing an ambiguous hardware state.
The onboard RGB LED reports the most recent test-harness state: The onboard RGB LED reports the most recent test-harness state:
| Color | Meaning | | Color | Meaning |
+5
View File
@@ -3,6 +3,10 @@ idf_component_register(
"main.c" "main.c"
"status_led.c" "status_led.c"
"rs232_hw_test.c" "rs232_hw_test.c"
"rs232_port_owner.c"
"serial_config.c"
"serial_service.c"
"serial_console.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES REQUIRES
console console
@@ -12,4 +16,5 @@ idf_component_register(
esp_timer esp_timer
freertos freertos
led_strip led_strip
nvs_flash
) )
+27 -5
View File
@@ -4,17 +4,21 @@
#include "esp_log.h" #include "esp_log.h"
#include "esp_psram.h" #include "esp_psram.h"
#include "rs232_hw_test.h" #include "rs232_hw_test.h"
#include "rs232_port_owner.h"
#include "serial_config.h"
#include "serial_console.h"
#include "serial_service.h"
#include "status_led.h" #include "status_led.h"
#define CONSOLE_BAUD_RATE 115200 #define CONSOLE_BAUD_RATE 115200
#define CONSOLE_TX_GPIO 43 #define CONSOLE_TX_GPIO 43
#define CONSOLE_RX_GPIO 44 #define CONSOLE_RX_GPIO 44
static const char *TAG = "phase0"; static const char *TAG = "firmware";
void app_main(void) void app_main(void)
{ {
ESP_LOGI(TAG, "ESP32-S3 RS-232 Phase 0 hardware characterization started"); ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 1 started");
if (esp_psram_is_initialized()) { if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size()); ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
@@ -22,12 +26,29 @@ void app_main(void)
ESP_LOGW(TAG, "PSRAM is not initialized"); ESP_LOGW(TAG, "PSRAM is not initialized");
} }
/* Blue means that the test harness is initialized and waiting for a command. */ /* Blue means the firmware is initialized and waiting for a console command. */
ESP_ERROR_CHECK(status_led_init()); ESP_ERROR_CHECK(status_led_init());
ESP_ERROR_CHECK(rs232_port_owner_init());
ESP_ERROR_CHECK(rs232_hw_test_init()); ESP_ERROR_CHECK(rs232_hw_test_init());
serial_config_t serial_config;
bool used_stored_config = false;
esp_err_t config_error = serial_config_load(&serial_config, &used_stored_config);
if (config_error != ESP_OK) {
serial_config_defaults(&serial_config);
ESP_LOGW(
TAG,
"NVS serial configuration unavailable (%s); using RAM defaults without erasing storage",
esp_err_to_name(config_error));
}
ESP_ERROR_CHECK(serial_service_init(&serial_config));
ESP_LOGI(
TAG,
"Using %s serial configuration; UART service remains stopped until 'serial start'",
used_stored_config ? "stored" : "default");
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
repl_config.prompt = "rs232-test> "; repl_config.prompt = "serial-tool> ";
repl_config.max_cmdline_length = 160; repl_config.max_cmdline_length = 160;
repl_config.task_stack_size = 8192; repl_config.task_stack_size = 8192;
@@ -46,8 +67,9 @@ void app_main(void)
/* The REPL constructor initializes esp_console and installs `help`. */ /* The REPL constructor initializes esp_console and installs `help`. */
ESP_ERROR_CHECK(rs232_hw_test_register_console_commands()); ESP_ERROR_CHECK(rs232_hw_test_register_console_commands());
ESP_ERROR_CHECK(serial_console_register_commands());
ESP_ERROR_CHECK(esp_console_start_repl(repl)); ESP_ERROR_CHECK(esp_console_start_repl(repl));
ESP_LOGI(TAG, "Interactive test console ready at %d baud", CONSOLE_BAUD_RATE); ESP_LOGI(TAG, "Interactive test console ready at %d baud", CONSOLE_BAUD_RATE);
ESP_LOGI(TAG, "Type 'help' for commands; no test runs automatically"); ESP_LOGI(TAG, "Type 'help' for commands; no serial service or test runs automatically");
} }
+83 -10
View File
@@ -17,6 +17,7 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/queue.h" #include "freertos/queue.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "rs232_port_owner.h"
#include "status_led.h" #include "status_led.h"
#define STATIC_SETTLE_TIME_MS 20 #define STATIC_SETTLE_TIME_MS 20
@@ -59,6 +60,7 @@ static int s_rts_level = 1;
static bool s_transceiver_enabled = true; static bool s_transceiver_enabled = true;
static bool s_initialized; static bool s_initialized;
static bool s_uart_active; static bool s_uart_active;
static bool s_cleanup_fault;
typedef struct { typedef struct {
const char *name; const char *name;
@@ -253,7 +255,7 @@ static const serial_format_t *find_serial_format(const char *name)
return NULL; return NULL;
} }
static int command_status(int argc, char **argv) static int execute_status(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
@@ -278,7 +280,7 @@ static int command_status(int argc, char **argv)
return 0; return 0;
} }
static int command_transceiver(int argc, char **argv) static int execute_transceiver(int argc, char **argv)
{ {
if (argc != 2 || (strcmp(argv[1], "enable") != 0 && strcmp(argv[1], "disable") != 0)) { if (argc != 2 || (strcmp(argv[1], "enable") != 0 && strcmp(argv[1], "disable") != 0)) {
printf("Usage: transceiver <enable|disable>\n"); printf("Usage: transceiver <enable|disable>\n");
@@ -300,7 +302,7 @@ static int command_transceiver(int argc, char **argv)
return 0; return 0;
} }
static int command_drivers(int argc, char **argv) static int execute_drivers(int argc, char **argv)
{ {
int tx; int tx;
int dtr; int dtr;
@@ -402,21 +404,21 @@ static esp_err_t run_static_loopback(loopback_configuration_t configuration)
return all_passed ? ESP_OK : ESP_FAIL; return all_passed ? ESP_OK : ESP_FAIL;
} }
static int command_loopback_a(int argc, char **argv) static int execute_loopback_a(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
return run_static_loopback(LOOPBACK_CONFIGURATION_A) == ESP_OK ? 0 : 1; return run_static_loopback(LOOPBACK_CONFIGURATION_A) == ESP_OK ? 0 : 1;
} }
static int command_loopback_b(int argc, char **argv) static int execute_loopback_b(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
return run_static_loopback(LOOPBACK_CONFIGURATION_B) == ESP_OK ? 0 : 1; return run_static_loopback(LOOPBACK_CONFIGURATION_B) == ESP_OK ? 0 : 1;
} }
static int command_valid_test(int argc, char **argv) static int execute_valid_test(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
@@ -799,25 +801,33 @@ cleanup:;
} }
} }
bool deletion_failed = false;
if (driver_installed) { if (driver_installed) {
esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT); esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT);
if (delete_error != ESP_OK) { if (delete_error != ESP_OK) {
printf("Could not delete UART1 driver: %s\n", esp_err_to_name(delete_error)); printf("Could not delete UART1 driver: %s\n", esp_err_to_name(delete_error));
deletion_failed = true;
s_cleanup_fault = true;
if (result == ESP_OK) { if (result == ESP_OK) {
result = delete_error; result = delete_error;
} }
} }
} }
if (!deletion_failed) {
/* GPIO17/18 return to static idle mode, then the transceiver is re-enabled. */ /* GPIO17/18 return to static idle mode, then the transceiver is re-enabled. */
s_transceiver_enabled = true; s_transceiver_enabled = true;
esp_err_t restore_error = configure_static_gpio(true); esp_err_t restore_error = configure_static_gpio(true);
if (restore_error != ESP_OK) { if (restore_error != ESP_OK) {
printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error)); printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error));
s_cleanup_fault = true;
if (result == ESP_OK) { if (result == ESP_OK) {
result = restore_error; result = restore_error;
} }
} }
} else {
s_transceiver_enabled = false;
}
esp_err_t led_error = status_led_set(result == ESP_OK ? STATUS_LED_PASS : STATUS_LED_FAIL); esp_err_t led_error = status_led_set(result == ESP_OK ? STATUS_LED_PASS : STATUS_LED_FAIL);
if (led_error != ESP_OK) { if (led_error != ESP_OK) {
@@ -829,7 +839,7 @@ cleanup:;
return result; return result;
} }
static int command_uart_loopback(int argc, char **argv) static int execute_uart_loopback(int argc, char **argv)
{ {
long baud_rate; long baud_rate;
long payload_size = 256; long payload_size = 256;
@@ -859,7 +869,7 @@ static int command_uart_loopback(int argc, char **argv)
return run_uart_loopback((int)baud_rate, format, (size_t)payload_size) == ESP_OK ? 0 : 1; return run_uart_loopback((int)baud_rate, format, (size_t)payload_size) == ESP_OK ? 0 : 1;
} }
static int command_uart_suite(int argc, char **argv) static int execute_uart_suite(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
@@ -902,10 +912,13 @@ static esp_err_t finish_flow_test(
} }
/* Stop the traffic source before removing the receiver's backpressure. */ /* Stop the traffic source before removing the receiver's backpressure. */
bool deletion_failed = false;
if (generator_uart_installed) { if (generator_uart_installed) {
esp_err_t delete_error = uart_driver_delete(RS232_TEST_GENERATOR_UART_PORT); esp_err_t delete_error = uart_driver_delete(RS232_TEST_GENERATOR_UART_PORT);
if (delete_error != ESP_OK) { if (delete_error != ESP_OK) {
printf("Could not delete UART2 generator: %s\n", esp_err_to_name(delete_error)); printf("Could not delete UART2 generator: %s\n", esp_err_to_name(delete_error));
deletion_failed = true;
s_cleanup_fault = true;
if (result == ESP_OK) { if (result == ESP_OK) {
result = delete_error; result = delete_error;
} }
@@ -915,20 +928,27 @@ static esp_err_t finish_flow_test(
esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT); esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT);
if (delete_error != ESP_OK) { if (delete_error != ESP_OK) {
printf("Could not delete UART1 driver: %s\n", esp_err_to_name(delete_error)); printf("Could not delete UART1 driver: %s\n", esp_err_to_name(delete_error));
deletion_failed = true;
s_cleanup_fault = true;
if (result == ESP_OK) { if (result == ESP_OK) {
result = delete_error; result = delete_error;
} }
} }
} }
if (!deletion_failed) {
s_transceiver_enabled = true; s_transceiver_enabled = true;
esp_err_t restore_error = configure_static_gpio(true); esp_err_t restore_error = configure_static_gpio(true);
if (restore_error != ESP_OK) { if (restore_error != ESP_OK) {
printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error)); printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error));
s_cleanup_fault = true;
if (result == ESP_OK) { if (result == ESP_OK) {
result = restore_error; result = restore_error;
} }
} }
} else {
s_transceiver_enabled = false;
}
esp_err_t led_error = status_led_set(result == ESP_OK ? STATUS_LED_PASS : STATUS_LED_FAIL); esp_err_t led_error = status_led_set(result == ESP_OK ? STATUS_LED_PASS : STATUS_LED_FAIL);
if (led_error != ESP_OK) { if (led_error != ESP_OK) {
@@ -1164,7 +1184,7 @@ cleanup:;
return finish_flow_test(uart1_installed, false, result); return finish_flow_test(uart1_installed, false, result);
} }
static int command_cts_flow_test(int argc, char **argv) static int execute_cts_flow_test(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
@@ -1493,7 +1513,7 @@ cleanup:;
return result; return result;
} }
static int command_rts_flow_test(int argc, char **argv) static int execute_rts_flow_test(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
@@ -1502,6 +1522,59 @@ static int command_rts_flow_test(int argc, char **argv)
return run_rts_flow_test() == ESP_OK ? 0 : 1; return run_rts_flow_test() == ESP_OK ? 0 : 1;
} }
typedef int (*hardware_test_command_t)(int argc, char **argv);
static int run_owned_hardware_test(
hardware_test_command_t command,
int argc,
char **argv)
{
esp_err_t claim_error = rs232_port_claim(RS232_PORT_OWNER_PHASE0);
if (claim_error != ESP_OK) {
rs232_port_owner_t owner = rs232_port_get_owner();
printf("RS-232 port is owned by %s. Stop the serial service or reboot after a fault.\n",
rs232_port_owner_to_string(owner));
return 1;
}
s_cleanup_fault = false;
int result = command(argc, argv);
/* Never publish the port as idle after ambiguous cleanup or a surviving driver. */
if (s_cleanup_fault ||
uart_is_driver_installed(RS232_UART_PORT) ||
uart_is_driver_installed(RS232_TEST_GENERATOR_UART_PORT)) {
drive_transceiver_enabled(false);
rs232_port_mark_fault(RS232_PORT_OWNER_PHASE0);
printf("A diagnostic UART driver could not be removed; MAX3243 is disabled. Reboot required.\n");
return 1;
}
if (rs232_port_release(RS232_PORT_OWNER_PHASE0) != ESP_OK) {
rs232_port_mark_fault(RS232_PORT_OWNER_PHASE0);
printf("Could not release diagnostic port ownership; reboot required.\n");
return 1;
}
return result;
}
#define DEFINE_OWNED_COMMAND(name) \
static int command_##name(int argc, char **argv) \
{ \
return run_owned_hardware_test(execute_##name, argc, argv); \
}
DEFINE_OWNED_COMMAND(status)
DEFINE_OWNED_COMMAND(transceiver)
DEFINE_OWNED_COMMAND(drivers)
DEFINE_OWNED_COMMAND(loopback_a)
DEFINE_OWNED_COMMAND(loopback_b)
DEFINE_OWNED_COMMAND(valid_test)
DEFINE_OWNED_COMMAND(uart_loopback)
DEFINE_OWNED_COMMAND(uart_suite)
DEFINE_OWNED_COMMAND(cts_flow_test)
DEFINE_OWNED_COMMAND(rts_flow_test)
esp_err_t rs232_hw_test_init(void) esp_err_t rs232_hw_test_init(void)
{ {
s_transceiver_enabled = true; s_transceiver_enabled = true;
+89
View File
@@ -0,0 +1,89 @@
#include "rs232_port_owner.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
static SemaphoreHandle_t s_owner_mutex;
static rs232_port_owner_t s_owner = RS232_PORT_OWNER_NONE;
esp_err_t rs232_port_owner_init(void)
{
if (s_owner_mutex != NULL) {
return ESP_ERR_INVALID_STATE;
}
s_owner_mutex = xSemaphoreCreateMutex();
return s_owner_mutex != NULL ? ESP_OK : ESP_ERR_NO_MEM;
}
esp_err_t rs232_port_claim(rs232_port_owner_t owner)
{
if (s_owner_mutex == NULL || owner == RS232_PORT_OWNER_NONE || owner == RS232_PORT_OWNER_FAULT) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_owner_mutex, portMAX_DELAY);
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_owner == RS232_PORT_OWNER_NONE) {
s_owner = owner;
result = ESP_OK;
}
xSemaphoreGive(s_owner_mutex);
return result;
}
esp_err_t rs232_port_release(rs232_port_owner_t owner)
{
if (s_owner_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_owner_mutex, portMAX_DELAY);
esp_err_t result = ESP_ERR_INVALID_STATE;
if (s_owner == owner) {
s_owner = RS232_PORT_OWNER_NONE;
result = ESP_OK;
}
xSemaphoreGive(s_owner_mutex);
return result;
}
void rs232_port_mark_fault(rs232_port_owner_t previous_owner)
{
if (s_owner_mutex == NULL) {
return;
}
xSemaphoreTake(s_owner_mutex, portMAX_DELAY);
if (s_owner == previous_owner) {
s_owner = RS232_PORT_OWNER_FAULT;
}
xSemaphoreGive(s_owner_mutex);
}
rs232_port_owner_t rs232_port_get_owner(void)
{
if (s_owner_mutex == NULL) {
return RS232_PORT_OWNER_FAULT;
}
xSemaphoreTake(s_owner_mutex, portMAX_DELAY);
rs232_port_owner_t owner = s_owner;
xSemaphoreGive(s_owner_mutex);
return owner;
}
const char *rs232_port_owner_to_string(rs232_port_owner_t owner)
{
switch (owner) {
case RS232_PORT_OWNER_NONE:
return "idle";
case RS232_PORT_OWNER_PHASE0:
return "Phase 0 diagnostics";
case RS232_PORT_OWNER_SERVICE:
return "serial service";
case RS232_PORT_OWNER_FAULT:
return "fault (reboot required)";
default:
return "unknown";
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "esp_err.h"
typedef enum {
RS232_PORT_OWNER_NONE,
RS232_PORT_OWNER_PHASE0,
RS232_PORT_OWNER_SERVICE,
RS232_PORT_OWNER_FAULT,
} rs232_port_owner_t;
esp_err_t rs232_port_owner_init(void);
esp_err_t rs232_port_claim(rs232_port_owner_t owner);
esp_err_t rs232_port_release(rs232_port_owner_t owner);
void rs232_port_mark_fault(rs232_port_owner_t previous_owner);
rs232_port_owner_t rs232_port_get_owner(void);
const char *rs232_port_owner_to_string(rs232_port_owner_t owner);
+407
View File
@@ -0,0 +1,407 @@
#include "serial_config.h"
#include <stddef.h>
#include <string.h>
#include "nvs.h"
#include "nvs_flash.h"
void serial_config_defaults(serial_config_t *config)
{
if (config == NULL) {
return;
}
*config = (serial_config_t) {
.version = SERIAL_CONFIG_VERSION,
.baud_rate = 115200,
.data_bits = SERIAL_CONFIG_DATA_BITS_8,
.parity = SERIAL_CONFIG_PARITY_NONE,
.stop_bits = SERIAL_CONFIG_STOP_BITS_1,
.flow_control = SERIAL_CONFIG_FLOW_CONTROL_NONE,
.dtr_behavior = SERIAL_CONFIG_DTR_INACTIVE,
.rts_threshold = SERIAL_CONFIG_DEFAULT_RTS_THRESHOLD,
};
}
esp_err_t serial_config_validate(const serial_config_t *config)
{
if (config == NULL ||
config->version != SERIAL_CONFIG_VERSION ||
config->baud_rate < SERIAL_CONFIG_MIN_BAUD_RATE ||
config->baud_rate > SERIAL_CONFIG_MAX_BAUD_RATE ||
config->rts_threshold == 0 ||
config->rts_threshold > SERIAL_CONFIG_MAX_RTS_THRESHOLD) {
return ESP_ERR_INVALID_ARG;
}
switch (config->data_bits) {
case SERIAL_CONFIG_DATA_BITS_7:
case SERIAL_CONFIG_DATA_BITS_8:
break;
default:
return ESP_ERR_INVALID_ARG;
}
switch (config->parity) {
case SERIAL_CONFIG_PARITY_NONE:
case SERIAL_CONFIG_PARITY_EVEN:
case SERIAL_CONFIG_PARITY_ODD:
break;
default:
return ESP_ERR_INVALID_ARG;
}
switch (config->stop_bits) {
case SERIAL_CONFIG_STOP_BITS_1:
case SERIAL_CONFIG_STOP_BITS_2:
break;
default:
return ESP_ERR_INVALID_ARG;
}
switch (config->flow_control) {
case SERIAL_CONFIG_FLOW_CONTROL_NONE:
case SERIAL_CONFIG_FLOW_CONTROL_RTS_CTS:
break;
default:
return ESP_ERR_INVALID_ARG;
}
switch (config->dtr_behavior) {
case SERIAL_CONFIG_DTR_INACTIVE:
case SERIAL_CONFIG_DTR_ACTIVE:
case SERIAL_CONFIG_DTR_ON_CONNECT:
break;
default:
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
esp_err_t serial_config_to_uart_config(const serial_config_t *config, uart_config_t *uart_config)
{
if (uart_config == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = serial_config_validate(config);
if (err != ESP_OK) {
return err;
}
uart_word_length_t data_bits;
switch (config->data_bits) {
case SERIAL_CONFIG_DATA_BITS_7:
data_bits = UART_DATA_7_BITS;
break;
case SERIAL_CONFIG_DATA_BITS_8:
data_bits = UART_DATA_8_BITS;
break;
default:
return ESP_ERR_INVALID_ARG;
}
uart_parity_t parity;
switch (config->parity) {
case SERIAL_CONFIG_PARITY_NONE:
parity = UART_PARITY_DISABLE;
break;
case SERIAL_CONFIG_PARITY_EVEN:
parity = UART_PARITY_EVEN;
break;
case SERIAL_CONFIG_PARITY_ODD:
parity = UART_PARITY_ODD;
break;
default:
return ESP_ERR_INVALID_ARG;
}
uart_stop_bits_t stop_bits;
switch (config->stop_bits) {
case SERIAL_CONFIG_STOP_BITS_1:
stop_bits = UART_STOP_BITS_1;
break;
case SERIAL_CONFIG_STOP_BITS_2:
stop_bits = UART_STOP_BITS_2;
break;
default:
return ESP_ERR_INVALID_ARG;
}
uart_hw_flowcontrol_t flow_control;
switch (config->flow_control) {
case SERIAL_CONFIG_FLOW_CONTROL_NONE:
flow_control = UART_HW_FLOWCTRL_DISABLE;
break;
case SERIAL_CONFIG_FLOW_CONTROL_RTS_CTS:
flow_control = UART_HW_FLOWCTRL_CTS_RTS;
break;
default:
return ESP_ERR_INVALID_ARG;
}
*uart_config = (uart_config_t) {
.baud_rate = (int)config->baud_rate,
.data_bits = data_bits,
.parity = parity,
.stop_bits = stop_bits,
.flow_ctrl = flow_control,
.rx_flow_ctrl_thresh = (uint8_t)config->rts_threshold,
.source_clk = UART_SCLK_DEFAULT,
};
return ESP_OK;
}
bool serial_config_parse_data_bits(const char *text, serial_config_data_bits_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "7") == 0) {
*value = SERIAL_CONFIG_DATA_BITS_7;
return true;
}
if (strcmp(text, "8") == 0) {
*value = SERIAL_CONFIG_DATA_BITS_8;
return true;
}
return false;
}
const char *serial_config_data_bits_to_string(serial_config_data_bits_t value)
{
switch (value) {
case SERIAL_CONFIG_DATA_BITS_7:
return "7";
case SERIAL_CONFIG_DATA_BITS_8:
return "8";
default:
return NULL;
}
}
bool serial_config_parse_parity(const char *text, serial_config_parity_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "none") == 0) {
*value = SERIAL_CONFIG_PARITY_NONE;
return true;
}
if (strcmp(text, "even") == 0) {
*value = SERIAL_CONFIG_PARITY_EVEN;
return true;
}
if (strcmp(text, "odd") == 0) {
*value = SERIAL_CONFIG_PARITY_ODD;
return true;
}
return false;
}
const char *serial_config_parity_to_string(serial_config_parity_t value)
{
switch (value) {
case SERIAL_CONFIG_PARITY_NONE:
return "none";
case SERIAL_CONFIG_PARITY_EVEN:
return "even";
case SERIAL_CONFIG_PARITY_ODD:
return "odd";
default:
return NULL;
}
}
bool serial_config_parse_stop_bits(const char *text, serial_config_stop_bits_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "1") == 0) {
*value = SERIAL_CONFIG_STOP_BITS_1;
return true;
}
if (strcmp(text, "2") == 0) {
*value = SERIAL_CONFIG_STOP_BITS_2;
return true;
}
return false;
}
const char *serial_config_stop_bits_to_string(serial_config_stop_bits_t value)
{
switch (value) {
case SERIAL_CONFIG_STOP_BITS_1:
return "1";
case SERIAL_CONFIG_STOP_BITS_2:
return "2";
default:
return NULL;
}
}
bool serial_config_parse_flow_control(const char *text, serial_config_flow_control_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "none") == 0) {
*value = SERIAL_CONFIG_FLOW_CONTROL_NONE;
return true;
}
if (strcmp(text, "rts-cts") == 0) {
*value = SERIAL_CONFIG_FLOW_CONTROL_RTS_CTS;
return true;
}
return false;
}
const char *serial_config_flow_control_to_string(serial_config_flow_control_t value)
{
switch (value) {
case SERIAL_CONFIG_FLOW_CONTROL_NONE:
return "none";
case SERIAL_CONFIG_FLOW_CONTROL_RTS_CTS:
return "rts-cts";
default:
return NULL;
}
}
bool serial_config_parse_dtr_behavior(const char *text, serial_config_dtr_behavior_t *value)
{
if (text == NULL || value == NULL) {
return false;
}
if (strcmp(text, "inactive") == 0) {
*value = SERIAL_CONFIG_DTR_INACTIVE;
return true;
}
if (strcmp(text, "active") == 0) {
*value = SERIAL_CONFIG_DTR_ACTIVE;
return true;
}
if (strcmp(text, "on-connect") == 0) {
*value = SERIAL_CONFIG_DTR_ON_CONNECT;
return true;
}
return false;
}
const char *serial_config_dtr_behavior_to_string(serial_config_dtr_behavior_t value)
{
switch (value) {
case SERIAL_CONFIG_DTR_INACTIVE:
return "inactive";
case SERIAL_CONFIG_DTR_ACTIVE:
return "active";
case SERIAL_CONFIG_DTR_ON_CONNECT:
return "on-connect";
default:
return NULL;
}
}
esp_err_t serial_config_storage_init(void)
{
/*
* Never erase the shared default NVS partition automatically. Future Wi-Fi,
* certificates, and provisioning data will live there too; destructive
* recovery belongs behind an explicit factory-reset operation.
*/
return nvs_flash_init();
}
esp_err_t serial_config_load(serial_config_t *config, bool *used_stored_config)
{
if (config == NULL || used_stored_config == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Callers always receive a usable configuration when storage is absent or stale. */
serial_config_defaults(config);
*used_stored_config = false;
esp_err_t err = serial_config_storage_init();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(SERIAL_CONFIG_NVS_NAMESPACE, NVS_READONLY, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
size_t stored_size = 0;
err = nvs_get_blob(handle, SERIAL_CONFIG_NVS_BLOB_KEY, NULL, &stored_size);
if (err == ESP_ERR_NVS_NOT_FOUND || err == ESP_ERR_NVS_TYPE_MISMATCH) {
nvs_close(handle);
return ESP_OK;
}
if (err != ESP_OK) {
nvs_close(handle);
return err;
}
if (stored_size != sizeof(serial_config_t)) {
nvs_close(handle);
return ESP_OK;
}
serial_config_t stored_config;
err = nvs_get_blob(handle, SERIAL_CONFIG_NVS_BLOB_KEY, &stored_config, &stored_size);
nvs_close(handle);
if (err == ESP_ERR_NVS_INVALID_LENGTH) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
if (stored_size != sizeof(stored_config) || serial_config_validate(&stored_config) != ESP_OK) {
return ESP_OK;
}
*config = stored_config;
*used_stored_config = true;
return ESP_OK;
}
esp_err_t serial_config_save(const serial_config_t *config)
{
esp_err_t err = serial_config_validate(config);
if (err != ESP_OK) {
return err;
}
err = serial_config_storage_init();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(SERIAL_CONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
return err;
}
err = nvs_set_blob(handle, SERIAL_CONFIG_NVS_BLOB_KEY, config, sizeof(*config));
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
return err;
}
esp_err_t serial_config_reset_storage(void)
{
serial_config_t defaults;
serial_config_defaults(&defaults);
return serial_config_save(&defaults);
}
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "driver/uart.h"
#include "esp_err.h"
#define SERIAL_CONFIG_VERSION 1U
#define SERIAL_CONFIG_MIN_BAUD_RATE 110U
#define SERIAL_CONFIG_MAX_BAUD_RATE 1000000U
#define SERIAL_CONFIG_DEFAULT_RTS_THRESHOLD 96U
#define SERIAL_CONFIG_MAX_RTS_THRESHOLD 127U
#define SERIAL_CONFIG_NVS_NAMESPACE "serial"
#define SERIAL_CONFIG_NVS_BLOB_KEY "config"
typedef enum {
SERIAL_CONFIG_DATA_BITS_7,
SERIAL_CONFIG_DATA_BITS_8,
} serial_config_data_bits_t;
typedef enum {
SERIAL_CONFIG_PARITY_NONE,
SERIAL_CONFIG_PARITY_EVEN,
SERIAL_CONFIG_PARITY_ODD,
} serial_config_parity_t;
typedef enum {
SERIAL_CONFIG_STOP_BITS_1,
SERIAL_CONFIG_STOP_BITS_2,
} serial_config_stop_bits_t;
typedef enum {
SERIAL_CONFIG_FLOW_CONTROL_NONE,
SERIAL_CONFIG_FLOW_CONTROL_RTS_CTS,
} serial_config_flow_control_t;
typedef enum {
SERIAL_CONFIG_DTR_INACTIVE,
SERIAL_CONFIG_DTR_ACTIVE,
SERIAL_CONFIG_DTR_ON_CONNECT,
} serial_config_dtr_behavior_t;
/* Version and exact blob size make incompatible stored layouts fail safely. */
typedef struct {
uint32_t version;
uint32_t baud_rate;
serial_config_data_bits_t data_bits;
serial_config_parity_t parity;
serial_config_stop_bits_t stop_bits;
serial_config_flow_control_t flow_control;
serial_config_dtr_behavior_t dtr_behavior;
uint32_t rts_threshold;
} serial_config_t;
void serial_config_defaults(serial_config_t *config);
esp_err_t serial_config_validate(const serial_config_t *config);
esp_err_t serial_config_to_uart_config(const serial_config_t *config, uart_config_t *uart_config);
bool serial_config_parse_data_bits(const char *text, serial_config_data_bits_t *value);
const char *serial_config_data_bits_to_string(serial_config_data_bits_t value);
bool serial_config_parse_parity(const char *text, serial_config_parity_t *value);
const char *serial_config_parity_to_string(serial_config_parity_t value);
bool serial_config_parse_stop_bits(const char *text, serial_config_stop_bits_t *value);
const char *serial_config_stop_bits_to_string(serial_config_stop_bits_t value);
bool serial_config_parse_flow_control(const char *text, serial_config_flow_control_t *value);
const char *serial_config_flow_control_to_string(serial_config_flow_control_t value);
bool serial_config_parse_dtr_behavior(const char *text, serial_config_dtr_behavior_t *value);
const char *serial_config_dtr_behavior_to_string(serial_config_dtr_behavior_t value);
/* Storage operations initialize the default NVS partition before use. */
esp_err_t serial_config_storage_init(void);
esp_err_t serial_config_load(serial_config_t *config, bool *used_stored_config);
esp_err_t serial_config_save(const serial_config_t *config);
esp_err_t serial_config_reset_storage(void);
+377
View File
@@ -0,0 +1,377 @@
#include "serial_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 "rs232_port_owner.h"
#include "serial_config.h"
#include "serial_service.h"
static void print_usage(void)
{
printf("Usage:\n");
printf(" serial status\n");
printf(" serial start|stop\n");
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)
{
printf("Configuration v%lu: baud=%lu, data-bits=%s, parity=%s, stop-bits=%s, flow=%s, DTR=%s, RTS-threshold=%lu\n",
(unsigned long)config->version,
(unsigned long)config->baud_rate,
serial_config_data_bits_to_string(config->data_bits),
serial_config_parity_to_string(config->parity),
serial_config_stop_bits_to_string(config->stop_bits),
serial_config_flow_control_to_string(config->flow_control),
serial_config_dtr_behavior_to_string(config->dtr_behavior),
(unsigned long)config->rts_threshold);
}
static bool parse_unsigned(const char *text, uint32_t minimum, uint32_t maximum, uint32_t *value)
{
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 int show_status(void)
{
serial_config_t config;
esp_err_t err = serial_service_get_config(&config);
if (err != ESP_OK) {
printf("Could not read serial configuration: %s\n", esp_err_to_name(err));
return 1;
}
bool running = serial_service_is_running();
rs232_port_owner_t owner = rs232_port_get_owner();
printf("UART service: %s\n", running ? "running" : "stopped");
printf("RS-232 port owner: %s\n", rs232_port_owner_to_string(owner));
print_config(&config);
printf("Buffers: RX-available=%u TX-pending=%u\n",
(unsigned int)serial_service_rx_available(),
(unsigned int)serial_service_tx_pending());
if (running) {
serial_modem_state_t modem;
serial_service_get_modem_state(&modem);
printf("Modem asserted: DCD=%d DSR=%d CTS=%d RI=%d; valid-voltage VLD=%d\n",
modem.dcd, modem.dsr, modem.cts, modem.ri, modem.valid);
} else if (owner == RS232_PORT_OWNER_NONE) {
printf("Phase 0 hardware commands are available while the service is stopped.\n");
} else {
printf("Phase 0 commands are unavailable until the current owner releases the port.\n");
}
return 0;
}
static int show_counters(void)
{
serial_service_counters_t counters;
serial_service_get_counters(&counters);
printf("Data: RX=%" PRIu64 " RX-dropped=%" PRIu64
" TX-queued=%" PRIu64 " TX-to-UART=%" PRIu64
" TX-dropped=%" PRIu64 "\n",
counters.rx_bytes,
counters.rx_dropped_bytes,
counters.tx_queued_bytes,
counters.tx_sent_to_uart_bytes,
counters.tx_dropped_bytes);
printf("UART errors: frame=%" PRIu64 " parity=%" PRIu64
" FIFO-overflow=%" PRIu64 " buffer-full=%" PRIu64
" break=%" PRIu64 "\n",
counters.frame_errors,
counters.parity_errors,
counters.fifo_overflows,
counters.buffer_full_events,
counters.breaks);
printf("Modem transitions: DCD=%" PRIu64 " DSR=%" PRIu64
" CTS=%" PRIu64 " RI=%" PRIu64 " VLD=%" PRIu64 "\n",
counters.dcd_transitions,
counters.dsr_transitions,
counters.cts_transitions,
counters.ri_transitions,
counters.valid_transitions);
return 0;
}
static int set_parameter(const char *parameter, const char *value)
{
serial_config_t candidate;
esp_err_t err = serial_service_get_config(&candidate);
if (err != ESP_OK) {
printf("Could not read current configuration: %s\n", esp_err_to_name(err));
return 1;
}
if (strcmp(parameter, "baud") == 0) {
if (!parse_unsigned(
value,
SERIAL_CONFIG_MIN_BAUD_RATE,
SERIAL_CONFIG_MAX_BAUD_RATE,
&candidate.baud_rate)) {
printf("Baud rate must be %u..%u.\n",
SERIAL_CONFIG_MIN_BAUD_RATE,
SERIAL_CONFIG_MAX_BAUD_RATE);
return 1;
}
} else if (strcmp(parameter, "data-bits") == 0) {
if (!serial_config_parse_data_bits(value, &candidate.data_bits)) {
printf("Data bits must be 7 or 8.\n");
return 1;
}
} else if (strcmp(parameter, "parity") == 0) {
if (!serial_config_parse_parity(value, &candidate.parity)) {
printf("Parity must be none, even, or odd.\n");
return 1;
}
} else if (strcmp(parameter, "stop-bits") == 0) {
if (!serial_config_parse_stop_bits(value, &candidate.stop_bits)) {
printf("Stop bits must be 1 or 2.\n");
return 1;
}
} else if (strcmp(parameter, "flow") == 0) {
if (!serial_config_parse_flow_control(value, &candidate.flow_control)) {
printf("Flow control must be none or rts-cts.\n");
return 1;
}
} else if (strcmp(parameter, "dtr") == 0) {
if (!serial_config_parse_dtr_behavior(value, &candidate.dtr_behavior)) {
printf("DTR behavior must be inactive, active, or on-connect.\n");
return 1;
}
} else if (strcmp(parameter, "rts-threshold") == 0) {
if (!parse_unsigned(
value,
1,
SERIAL_CONFIG_MAX_RTS_THRESHOLD,
&candidate.rts_threshold)) {
printf("RTS threshold must be 1..%u.\n", SERIAL_CONFIG_MAX_RTS_THRESHOLD);
return 1;
}
} else {
printf("Unknown serial parameter '%s'.\n", parameter);
print_usage();
return 1;
}
err = serial_service_apply_config(&candidate);
if (err != ESP_OK) {
printf("Could not apply configuration: %s\n", esp_err_to_name(err));
return 1;
}
print_config(&candidate);
printf("Applied in RAM%s; run 'serial save' to persist it.\n",
serial_service_is_running() ? " after a controlled UART restart" : "");
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)) {
return show_status();
}
if (argc == 2 && strcmp(argv[1], "start") == 0) {
esp_err_t err = serial_service_start();
if (err != ESP_OK) {
printf("Could not start UART service: %s\n", esp_err_to_name(err));
return 1;
}
return show_status();
}
if (argc == 2 && strcmp(argv[1], "stop") == 0) {
esp_err_t err = serial_service_stop();
if (err != ESP_OK) {
printf("Could not stop UART service: %s\n", esp_err_to_name(err));
return 1;
}
return show_status();
}
if (argc == 4 && strcmp(argv[1], "set") == 0) {
return set_parameter(argv[2], argv[3]);
}
if (argc == 2 && strcmp(argv[1], "save") == 0) {
serial_config_t config;
esp_err_t err = serial_service_get_config(&config);
if (err == ESP_OK) {
err = serial_config_save(&config);
}
if (err != ESP_OK) {
printf("Could not save configuration: %s\n", esp_err_to_name(err));
return 1;
}
printf("Serial configuration saved to NVS.\n");
return 0;
}
if (argc == 2 && strcmp(argv[1], "load") == 0) {
serial_config_t config;
bool used_stored_config;
esp_err_t err = serial_config_load(&config, &used_stored_config);
if (err == ESP_OK) {
err = serial_service_apply_config(&config);
}
if (err != ESP_OK) {
printf("Could not load configuration: %s\n", esp_err_to_name(err));
return 1;
}
printf("Loaded %s configuration.\n", used_stored_config ? "stored" : "default");
print_config(&config);
return 0;
}
if (argc == 2 && strcmp(argv[1], "defaults") == 0) {
serial_config_t config;
serial_config_defaults(&config);
esp_err_t err = serial_service_apply_config(&config);
if (err != ESP_OK) {
printf("Could not apply defaults: %s\n", esp_err_to_name(err));
return 1;
}
printf("Defaults applied in RAM; run 'serial save' to persist them.\n");
print_config(&config);
return 0;
}
if (argc == 2 && strcmp(argv[1], "reset") == 0) {
serial_config_t previous;
serial_config_t defaults;
serial_config_defaults(&defaults);
esp_err_t err = serial_service_get_config(&previous);
if (err == ESP_OK) {
err = serial_service_apply_config(&defaults);
}
if (err == ESP_OK) {
err = serial_config_reset_storage();
if (err != ESP_OK) {
/* Keep runtime and persisted behavior aligned if NVS cannot commit. */
serial_service_apply_config(&previous);
}
}
if (err != ESP_OK) {
printf("Could not reset configuration: %s\n", esp_err_to_name(err));
return 1;
}
printf("Defaults applied and saved to NVS.\n");
print_config(&defaults);
return 0;
}
if (argc == 2 && strcmp(argv[1], "counters") == 0) {
return show_counters();
}
if (argc == 2 && strcmp(argv[1], "clear-counters") == 0) {
serial_service_clear_counters();
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;
}
esp_err_t serial_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "serial",
.help = "Configure and control the Phase 1 UART service; use 'serial' for usage/status",
.hint = NULL,
.func = &command_serial,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "esp_err.h"
/* Register Phase 1 serial configuration and UART-service console commands. */
esp_err_t serial_console_register_commands(void);
+720
View File
@@ -0,0 +1,720 @@
#include "serial_service.h"
#include <stdatomic.h>
#include <string.h>
#include "board_pins.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_check.h"
#include "esp_log.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/stream_buffer.h"
#include "freertos/task.h"
#include "rs232_hw_test.h"
#include "rs232_port_owner.h"
#define SERIAL_UART_RX_RING_SIZE 8192
#define SERIAL_UART_EVENT_QUEUE_SIZE 64
#define SERIAL_RX_STREAM_SIZE 16384
#define SERIAL_TX_STREAM_SIZE 8192
#define SERIAL_IO_CHUNK_SIZE 256
#define SERIAL_TASK_STACK_SIZE 4096
#define SERIAL_TASK_PRIORITY 10
#define SERIAL_TASK_IDLE_POLL_MS 50
#define SERIAL_TASK_TX_POLL_MS 10
#define SERIAL_STOP_TIMEOUT_MS 1000
static const char *TAG = "serial_service";
static SemaphoreHandle_t s_state_mutex;
static SemaphoreHandle_t s_task_stopped;
static StreamBufferHandle_t s_rx_stream;
static StreamBufferHandle_t s_tx_stream;
static QueueHandle_t s_uart_event_queue;
static TaskHandle_t s_event_task;
static portMUX_TYPE s_counter_lock = portMUX_INITIALIZER_UNLOCKED;
static serial_config_t s_config;
static serial_modem_state_t s_modem_state;
static serial_service_counters_t s_counters;
static bool s_initialized;
static atomic_bool s_running;
static atomic_bool s_stop_requested;
static atomic_size_t s_tx_task_pending;
static bool s_session_active;
static bool s_static_mode_safe;
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
{
TickType_t ticks = pdMS_TO_TICKS(milliseconds);
return (milliseconds > 0 && ticks == 0) ? 1 : ticks;
}
static void add_counter(uint64_t *counter, uint64_t amount)
{
taskENTER_CRITICAL(&s_counter_lock);
*counter += amount;
taskEXIT_CRITICAL(&s_counter_lock);
}
static serial_modem_state_t read_modem_state(void)
{
return (serial_modem_state_t) {
/* MAX3243 receiver outputs are low when modem-control inputs assert. */
.dcd = gpio_get_level(RS232_DCD_GPIO) == 0,
.dsr = gpio_get_level(RS232_DSR_GPIO) == 0,
.cts = gpio_get_level(RS232_CTS_GPIO) == 0,
.ri = gpio_get_level(RS232_RI_GPIO) == 0,
.valid = gpio_get_level(RS232_VALID_GPIO) != 0,
};
}
static void poll_modem_state(void)
{
serial_modem_state_t current = read_modem_state();
taskENTER_CRITICAL(&s_counter_lock);
if (current.dcd != s_modem_state.dcd) {
++s_counters.dcd_transitions;
}
if (current.dsr != s_modem_state.dsr) {
++s_counters.dsr_transitions;
}
if (current.cts != s_modem_state.cts) {
++s_counters.cts_transitions;
}
if (current.ri != s_modem_state.ri) {
++s_counters.ri_transitions;
}
if (current.valid != s_modem_state.valid) {
++s_counters.valid_transitions;
}
s_modem_state = current;
taskEXIT_CRITICAL(&s_counter_lock);
}
static bool configured_dtr_active(void)
{
switch (s_config.dtr_behavior) {
case SERIAL_CONFIG_DTR_ACTIVE:
return true;
case SERIAL_CONFIG_DTR_ON_CONNECT:
return s_session_active;
case SERIAL_CONFIG_DTR_INACTIVE:
default:
return false;
}
}
static esp_err_t prepare_gpio_for_uart(void)
{
s_static_mode_safe = false;
/* Keep every RS-232 driver disabled while GPIO-matrix routing changes. */
ESP_RETURN_ON_ERROR(gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0), TAG, "Shut down MAX3243");
const gpio_config_t shutdown_config = {
.pin_bit_mask = 1ULL << RS232_FORCE_OFF_N_GPIO,
.mode = GPIO_MODE_INPUT_OUTPUT_OD,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&shutdown_config), TAG, "Configure OFF GPIO");
/*
* TX and RTS start in their inactive logic-1 state. DTR follows policy,
* where logic 0 is the asserted RS-232 modem-control state.
*/
ESP_RETURN_ON_ERROR(gpio_set_level(RS232_TX_GPIO, 1), TAG, "Set TX idle latch");
ESP_RETURN_ON_ERROR(gpio_set_level(RS232_RTS_GPIO, 1), TAG, "Set RTS idle latch");
ESP_RETURN_ON_ERROR(
gpio_set_level(RS232_DTR_GPIO, configured_dtr_active() ? 0 : 1),
TAG,
"Set DTR policy latch");
const gpio_config_t output_config = {
.pin_bit_mask = (1ULL << RS232_TX_GPIO) |
(1ULL << RS232_RTS_GPIO) |
(1ULL << RS232_DTR_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&output_config), TAG, "Configure RS-232 outputs");
const gpio_config_t input_config = {
.pin_bit_mask = (1ULL << RS232_RX_GPIO) |
(1ULL << RS232_CTS_GPIO) |
(1ULL << RS232_DSR_GPIO) |
(1ULL << RS232_DCD_GPIO) |
(1ULL << RS232_RI_GPIO) |
(1ULL << RS232_VALID_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
return gpio_config(&input_config);
}
static void enqueue_received_data(const uint8_t *data, size_t size)
{
size_t accepted = xStreamBufferSend(s_rx_stream, data, size, 0);
add_counter(&s_counters.rx_bytes, size);
if (accepted < size) {
add_counter(&s_counters.rx_dropped_bytes, size - accepted);
}
}
static void drain_uart_receive_ring(size_t suggested_size)
{
uint8_t buffer[SERIAL_IO_CHUNK_SIZE];
size_t remaining = suggested_size;
while (!s_stop_requested) {
size_t request = sizeof(buffer);
if (remaining > 0 && remaining < request) {
request = remaining;
}
int count = uart_read_bytes(RS232_UART_PORT, buffer, request, 0);
if (count <= 0) {
break;
}
enqueue_received_data(buffer, (size_t)count);
if (remaining > 0) {
if ((size_t)count >= remaining) {
remaining = 0;
} else {
remaining -= (size_t)count;
}
}
/* For buffer-full recovery, continue until ring and stashed data are drained. */
if (suggested_size > 0 && remaining == 0) {
break;
}
}
}
static void handle_uart_event(const uart_event_t *event)
{
switch (event->type) {
case UART_DATA:
drain_uart_receive_ring(event->size);
break;
case UART_BUFFER_FULL:
add_counter(&s_counters.buffer_full_events, 1);
drain_uart_receive_ring(0);
break;
case UART_FIFO_OVF:
add_counter(&s_counters.fifo_overflows, 1);
uart_flush_input(RS232_UART_PORT);
xQueueReset(s_uart_event_queue);
break;
case UART_FRAME_ERR:
add_counter(&s_counters.frame_errors, 1);
break;
case UART_PARITY_ERR:
add_counter(&s_counters.parity_errors, 1);
break;
case UART_BREAK:
case UART_DATA_BREAK:
add_counter(&s_counters.breaks, 1);
break;
default:
break;
}
}
static void serial_event_task(void *context)
{
(void)context;
uint8_t pending[SERIAL_IO_CHUNK_SIZE];
size_t pending_size = 0;
size_t pending_offset = 0;
while (!s_stop_requested) {
if (pending_offset == pending_size) {
pending_size = xStreamBufferReceive(s_tx_stream, pending, sizeof(pending), 0);
pending_offset = 0;
s_tx_task_pending = pending_size;
}
if (pending_offset < pending_size) {
/*
* uart_tx_chars is nonblocking and therefore remains safe when CTS
* is deasserted indefinitely. Unsent bytes stay in this task's
* local pending buffer until hardware FIFO space is available.
*/
int sent = uart_tx_chars(
RS232_UART_PORT,
(const char *)(pending + pending_offset),
pending_size - pending_offset);
if (sent > 0) {
pending_offset += (size_t)sent;
s_tx_task_pending = pending_size - pending_offset;
add_counter(&s_counters.tx_sent_to_uart_bytes, (uint64_t)sent);
}
}
TickType_t event_wait = milliseconds_to_ticks(
pending_offset < pending_size || xStreamBufferBytesAvailable(s_tx_stream) > 0
? SERIAL_TASK_TX_POLL_MS
: SERIAL_TASK_IDLE_POLL_MS);
uart_event_t event;
if (xQueueReceive(s_uart_event_queue, &event, event_wait) == pdTRUE) {
handle_uart_event(&event);
}
/* Event-queue notifications can be dropped; the ring length is authoritative. */
drain_uart_receive_ring(0);
poll_modem_state();
}
size_t discarded = (pending_size - pending_offset) +
xStreamBufferBytesAvailable(s_tx_stream);
if (discarded > 0) {
add_counter(&s_counters.tx_dropped_bytes, discarded);
}
s_tx_task_pending = 0;
s_event_task = NULL;
xSemaphoreGive(s_task_stopped);
vTaskDelete(NULL);
}
static esp_err_t restore_static_mode_or_fault(void)
{
esp_err_t result = rs232_hw_test_init();
s_static_mode_safe = result == ESP_OK;
if (result != ESP_OK) {
gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
ESP_LOGE(TAG, "Static GPIO restoration failed; MAX3243 disabled and port faulted");
}
return result;
}
static esp_err_t cleanup_failed_start(
bool driver_installed,
bool restore_static_mode)
{
esp_err_t result = gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
if (driver_installed) {
esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT);
if (delete_error != ESP_OK) {
s_running = false;
s_stop_requested = true;
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
ESP_LOGE(TAG, "Could not delete UART1 after failed start; port faulted");
return delete_error;
}
}
s_uart_event_queue = NULL;
s_running = false;
s_stop_requested = false;
if (restore_static_mode) {
esp_err_t restore_error = restore_static_mode_or_fault();
if (result == ESP_OK) {
result = restore_error;
}
}
return result;
}
static esp_err_t start_locked(bool restore_static_on_failure)
{
if (s_running) {
return ESP_ERR_INVALID_STATE;
}
uart_config_t uart_config;
ESP_RETURN_ON_ERROR(serial_config_to_uart_config(&s_config, &uart_config), TAG, "Convert serial config");
bool driver_installed = false;
esp_err_t err = prepare_gpio_for_uart();
if (err == ESP_OK) {
err = uart_driver_install(
RS232_UART_PORT,
SERIAL_UART_RX_RING_SIZE,
0,
SERIAL_UART_EVENT_QUEUE_SIZE,
&s_uart_event_queue,
0);
driver_installed = err == ESP_OK;
}
if (err == ESP_OK) {
err = uart_param_config(RS232_UART_PORT, &uart_config);
}
if (err == ESP_OK) {
err = uart_set_line_inverse(RS232_UART_PORT, 0);
}
bool hardware_flow = s_config.flow_control == SERIAL_CONFIG_FLOW_CONTROL_RTS_CTS;
if (err == ESP_OK) {
err = uart_set_pin(
RS232_UART_PORT,
RS232_TX_GPIO,
RS232_RX_GPIO,
hardware_flow ? RS232_RTS_GPIO : UART_PIN_NO_CHANGE,
hardware_flow ? RS232_CTS_GPIO : UART_PIN_NO_CHANGE);
}
if (err == ESP_OK) {
err = uart_set_rx_full_threshold(RS232_UART_PORT, 64);
}
if (err != ESP_OK) {
esp_err_t cleanup_error = cleanup_failed_start(driver_installed, restore_static_on_failure);
return cleanup_error == ESP_OK ? err : cleanup_error;
}
if (xStreamBufferReset(s_rx_stream) != pdPASS ||
xStreamBufferReset(s_tx_stream) != pdPASS) {
cleanup_failed_start(driver_installed, restore_static_on_failure);
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_task_stopped, 0);
s_stop_requested = false;
s_tx_task_pending = 0;
err = gpio_set_level(RS232_FORCE_OFF_N_GPIO, 1);
if (err != ESP_OK) {
esp_err_t cleanup_error = cleanup_failed_start(driver_installed, restore_static_on_failure);
return cleanup_error == ESP_OK ? err : cleanup_error;
}
vTaskDelay(pdMS_TO_TICKS(20));
serial_modem_state_t initial_modem_state = read_modem_state();
taskENTER_CRITICAL(&s_counter_lock);
s_modem_state = initial_modem_state;
taskEXIT_CRITICAL(&s_counter_lock);
if (xTaskCreate(
serial_event_task,
"serial_uart",
SERIAL_TASK_STACK_SIZE,
NULL,
SERIAL_TASK_PRIORITY,
&s_event_task) != pdPASS) {
esp_err_t cleanup_error = cleanup_failed_start(driver_installed, restore_static_on_failure);
return cleanup_error == ESP_OK ? ESP_ERR_NO_MEM : cleanup_error;
}
s_running = true;
ESP_LOGI(
TAG,
"UART1 started: baud=%lu, data-bits=%s, parity=%s, stop-bits=%s, flow=%s, DTR=%s",
(unsigned long)s_config.baud_rate,
serial_config_data_bits_to_string(s_config.data_bits),
serial_config_parity_to_string(s_config.parity),
serial_config_stop_bits_to_string(s_config.stop_bits),
serial_config_flow_control_to_string(s_config.flow_control),
serial_config_dtr_behavior_to_string(s_config.dtr_behavior));
return ESP_OK;
}
static esp_err_t stop_locked(bool restore_static_mode)
{
if (!s_running) {
return ESP_OK;
}
s_stop_requested = true;
if (xSemaphoreTake(s_task_stopped, pdMS_TO_TICKS(SERIAL_STOP_TIMEOUT_MS)) != pdTRUE) {
ESP_LOGE(TAG, "UART service task did not quiesce within %d ms", SERIAL_STOP_TIMEOUT_MS);
return ESP_ERR_TIMEOUT;
}
size_t unread_rx = xStreamBufferBytesAvailable(s_rx_stream);
if (unread_rx > 0) {
add_counter(&s_counters.rx_dropped_bytes, unread_rx);
}
esp_err_t result = gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT);
if (delete_error != ESP_OK) {
s_running = false;
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
ESP_LOGE(TAG, "Could not delete UART1; MAX3243 remains disabled and port is faulted");
return delete_error;
}
s_uart_event_queue = NULL;
s_running = false;
s_stop_requested = false;
if (xStreamBufferReset(s_rx_stream) != pdPASS ||
xStreamBufferReset(s_tx_stream) != pdPASS) {
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
return ESP_ERR_INVALID_STATE;
}
if (restore_static_mode) {
esp_err_t restore_error = restore_static_mode_or_fault();
if (result == ESP_OK) {
result = restore_error;
}
}
ESP_LOGI(TAG, "UART1 stopped%s", restore_static_mode ? "; GPIOs restored to static idle mode" : " for reconfiguration");
return result;
}
esp_err_t serial_service_init(const serial_config_t *initial_config)
{
if (s_initialized) {
return ESP_ERR_INVALID_STATE;
}
ESP_RETURN_ON_ERROR(serial_config_validate(initial_config), TAG, "Validate initial config");
s_state_mutex = xSemaphoreCreateMutex();
s_task_stopped = xSemaphoreCreateBinary();
s_rx_stream = xStreamBufferCreate(SERIAL_RX_STREAM_SIZE, 1);
s_tx_stream = xStreamBufferCreate(SERIAL_TX_STREAM_SIZE, 1);
if (s_state_mutex == NULL || s_task_stopped == NULL ||
s_rx_stream == NULL || s_tx_stream == NULL) {
if (s_state_mutex != NULL) {
vSemaphoreDelete(s_state_mutex);
}
if (s_task_stopped != NULL) {
vSemaphoreDelete(s_task_stopped);
}
if (s_rx_stream != NULL) {
vStreamBufferDelete(s_rx_stream);
}
if (s_tx_stream != NULL) {
vStreamBufferDelete(s_tx_stream);
}
s_state_mutex = NULL;
s_task_stopped = NULL;
s_rx_stream = NULL;
s_tx_stream = NULL;
return ESP_ERR_NO_MEM;
}
s_config = *initial_config;
s_static_mode_safe = true;
s_initialized = true;
return ESP_OK;
}
esp_err_t serial_service_start(void)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
esp_err_t result = rs232_port_claim(RS232_PORT_OWNER_SERVICE);
if (result == ESP_OK) {
result = start_locked(true);
if (result != ESP_OK &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE &&
!uart_is_driver_installed(RS232_UART_PORT)) {
if (s_static_mode_safe) {
rs232_port_release(RS232_PORT_OWNER_SERVICE);
} else {
gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
}
}
}
xSemaphoreGive(s_state_mutex);
return result;
}
esp_err_t serial_service_stop(void)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
esp_err_t result = stop_locked(true);
if (!s_running &&
!uart_is_driver_installed(RS232_UART_PORT) &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE) {
if (s_static_mode_safe) {
esp_err_t release_error = rs232_port_release(RS232_PORT_OWNER_SERVICE);
if (result == ESP_OK) {
result = release_error;
}
} else {
gpio_set_level(RS232_FORCE_OFF_N_GPIO, 0);
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
}
}
xSemaphoreGive(s_state_mutex);
return result;
}
bool serial_service_is_running(void)
{
return atomic_load(&s_running);
}
esp_err_t serial_service_apply_config(const serial_config_t *config)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
ESP_RETURN_ON_ERROR(serial_config_validate(config), TAG, "Validate new config");
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
serial_config_t previous = s_config;
bool restart = s_running;
esp_err_t result = ESP_OK;
if (restart) {
result = stop_locked(false);
}
if (result == ESP_OK) {
s_config = *config;
if (restart) {
result = start_locked(false);
if (result != ESP_OK &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE) {
esp_err_t original_error = result;
ESP_LOGW(TAG, "New configuration failed; restoring previous UART configuration");
s_config = previous;
esp_err_t rollback_error = start_locked(false);
if (rollback_error != ESP_OK) {
ESP_LOGE(TAG, "Could not restore previous UART configuration: %s", esp_err_to_name(rollback_error));
if (!uart_is_driver_installed(RS232_UART_PORT)) {
esp_err_t restore_error = restore_static_mode_or_fault();
if (restore_error == ESP_OK) {
rs232_port_release(RS232_PORT_OWNER_SERVICE);
}
} else {
rs232_port_mark_fault(RS232_PORT_OWNER_SERVICE);
}
}
result = original_error;
}
}
} else if (!uart_is_driver_installed(RS232_UART_PORT) &&
rs232_port_get_owner() == RS232_PORT_OWNER_SERVICE) {
/* A failed stop that removed UART1 releases ownership only after safe restoration. */
esp_err_t restore_error = restore_static_mode_or_fault();
if (restore_error == ESP_OK) {
rs232_port_release(RS232_PORT_OWNER_SERVICE);
}
}
xSemaphoreGive(s_state_mutex);
return result;
}
esp_err_t serial_service_get_config(serial_config_t *config)
{
if (!s_initialized || config == NULL) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
*config = s_config;
xSemaphoreGive(s_state_mutex);
return ESP_OK;
}
size_t serial_service_read(uint8_t *data, size_t size)
{
if (!s_initialized || data == NULL || size == 0) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t received = 0;
if (s_running && !s_stop_requested) {
received = xStreamBufferReceive(s_rx_stream, data, size, 0);
}
xSemaphoreGive(s_state_mutex);
return received;
}
size_t serial_service_write(const uint8_t *data, size_t size)
{
if (!s_initialized || data == NULL || size == 0) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t accepted = 0;
if (s_running && !s_stop_requested) {
accepted = xStreamBufferSend(s_tx_stream, data, size, 0);
add_counter(&s_counters.tx_queued_bytes, accepted);
if (accepted < size) {
add_counter(&s_counters.tx_dropped_bytes, size - accepted);
}
}
xSemaphoreGive(s_state_mutex);
return accepted;
}
size_t serial_service_rx_available(void)
{
if (!s_initialized) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t available = xStreamBufferBytesAvailable(s_rx_stream);
xSemaphoreGive(s_state_mutex);
return available;
}
size_t serial_service_tx_pending(void)
{
if (!s_initialized) {
return 0;
}
xSemaphoreTake(s_state_mutex, portMAX_DELAY);
size_t pending = xStreamBufferBytesAvailable(s_tx_stream) +
atomic_load(&s_tx_task_pending);
xSemaphoreGive(s_state_mutex);
return pending;
}
esp_err_t serial_service_set_session_active(bool active)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
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);
}
xSemaphoreGive(s_state_mutex);
return result;
}
void serial_service_get_modem_state(serial_modem_state_t *state)
{
if (state == NULL) {
return;
}
taskENTER_CRITICAL(&s_counter_lock);
*state = s_modem_state;
taskEXIT_CRITICAL(&s_counter_lock);
}
void serial_service_get_counters(serial_service_counters_t *counters)
{
if (counters == NULL) {
return;
}
taskENTER_CRITICAL(&s_counter_lock);
*counters = s_counters;
taskEXIT_CRITICAL(&s_counter_lock);
}
void serial_service_clear_counters(void)
{
taskENTER_CRITICAL(&s_counter_lock);
memset(&s_counters, 0, sizeof(s_counters));
taskEXIT_CRITICAL(&s_counter_lock);
}
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "serial_config.h"
typedef struct {
bool dcd;
bool dsr;
bool cts;
bool ri;
bool valid;
} serial_modem_state_t;
typedef struct {
uint64_t rx_bytes;
uint64_t rx_dropped_bytes;
uint64_t tx_queued_bytes;
uint64_t tx_sent_to_uart_bytes;
uint64_t tx_dropped_bytes;
uint64_t frame_errors;
uint64_t parity_errors;
uint64_t fifo_overflows;
uint64_t buffer_full_events;
uint64_t breaks;
uint64_t dcd_transitions;
uint64_t dsr_transitions;
uint64_t cts_transitions;
uint64_t ri_transitions;
uint64_t valid_transitions;
} serial_service_counters_t;
/* Initialize service state without taking ownership of UART1 or driving traffic. */
esp_err_t serial_service_init(const serial_config_t *initial_config);
esp_err_t serial_service_start(void);
esp_err_t serial_service_stop(void);
bool serial_service_is_running(void);
/*
* Applying a configuration restarts a running UART in a controlled manner.
* If the new configuration cannot start, the service attempts to restore the
* previous configuration and reports the original failure.
*/
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
* logical RX consumer and TX producer; calls are serialized internally to
* satisfy FreeRTOS stream-buffer concurrency rules.
*/
size_t serial_service_read(uint8_t *data, size_t size);
size_t serial_service_write(const uint8_t *data, size_t size);
size_t serial_service_rx_available(void);
size_t serial_service_tx_pending(void);
/* DTR on-connect mode is driven by broker session ownership later. */
esp_err_t serial_service_set_session_active(bool active);
void serial_service_get_modem_state(serial_modem_state_t *state);
void serial_service_get_counters(serial_service_counters_t *counters);
void serial_service_clear_counters(void);
+29
View File
@@ -209,6 +209,35 @@ 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. 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
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:
```text
DE-9 pin 3 TX -> pin 2 RX
```
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
```
The read should return the exact bytes:
```text
Read 9 bytes: 0055aaff1b5b33316d
```
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.
Remove the loopback jumper with power off before connecting an external serial peer.
## Future hardware profiles ## Future hardware profiles
Alternative boards—such as the LILYGO T-Display-S3—or different RS-232 transceivers will receive separate profiles here. GPIO assignments must be reviewed for each board's display, buttons, USB connection, flash/PSRAM wiring, boot-strapping pins, and onboard peripherals. Alternative boards—such as the LILYGO T-Display-S3—or different RS-232 transceivers will receive separate profiles here. GPIO assignments must be reviewed for each board's display, buttons, USB connection, flash/PSRAM wiring, boot-strapping pins, and onboard peripherals.