From 535c27350d92e9ba1664ec03a053a6d27f72c02e Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 22 Aug 2026 23:23:13 +0200 Subject: [PATCH] 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. --- README.md | 47 ++- src/CMakeLists.txt | 5 + src/main.c | 32 +- src/rs232_hw_test.c | 119 +++++-- src/rs232_port_owner.c | 89 +++++ src/rs232_port_owner.h | 17 + src/serial_config.c | 407 +++++++++++++++++++++++ src/serial_config.h | 76 +++++ src/serial_console.c | 377 +++++++++++++++++++++ src/serial_console.h | 6 + src/serial_service.c | 720 +++++++++++++++++++++++++++++++++++++++++ src/serial_service.h | 67 ++++ wiring.md | 29 ++ 13 files changed, 1961 insertions(+), 30 deletions(-) create mode 100644 src/rs232_port_owner.c create mode 100644 src/rs232_port_owner.h create mode 100644 src/serial_config.c create mode 100644 src/serial_config.h create mode 100644 src/serial_console.c create mode 100644 src/serial_console.h create mode 100644 src/serial_service.c create mode 100644 src/serial_service.h diff --git a/README.md b/README.md index 58716c7..15d9563 100644 --- a/README.md +++ b/README.md @@ -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 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 @@ -33,7 +33,48 @@ pio run --target upload 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 +serial save +serial load +serial defaults +serial reset +serial counters +serial clear-counters +serial send-hex +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` | 110–1000000 | +| `data-bits` | `7`, `8` | +| `parity` | `none`, `even`, `odd` | +| `stop-bits` | `1`, `2` | +| `flow` | `none`, `rts-cts` | +| `dtr` | `inactive`, `active`, `on-connect` | +| `rts-threshold` | 1–127 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 status @@ -50,6 +91,8 @@ rts-flow-test `uart-loopback` defaults to `8N1` and 256 bytes. Its accepted payload range is 1–512 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: | Color | Meaning | diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e9e67cd..b449a09 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,10 @@ idf_component_register( "main.c" "status_led.c" "rs232_hw_test.c" + "rs232_port_owner.c" + "serial_config.c" + "serial_service.c" + "serial_console.c" INCLUDE_DIRS "." REQUIRES console @@ -12,4 +16,5 @@ idf_component_register( esp_timer freertos led_strip + nvs_flash ) diff --git a/src/main.c b/src/main.c index 7b2529c..9722a3d 100644 --- a/src/main.c +++ b/src/main.c @@ -4,17 +4,21 @@ #include "esp_log.h" #include "esp_psram.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" #define CONSOLE_BAUD_RATE 115200 #define CONSOLE_TX_GPIO 43 #define CONSOLE_RX_GPIO 44 -static const char *TAG = "phase0"; +static const char *TAG = "firmware"; 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()) { 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"); } - /* 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(rs232_port_owner_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(); - repl_config.prompt = "rs232-test> "; + repl_config.prompt = "serial-tool> "; repl_config.max_cmdline_length = 160; repl_config.task_stack_size = 8192; @@ -46,8 +67,9 @@ 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(esp_console_start_repl(repl)); 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"); } diff --git a/src/rs232_hw_test.c b/src/rs232_hw_test.c index d5f7448..5fd5497 100644 --- a/src/rs232_hw_test.c +++ b/src/rs232_hw_test.c @@ -17,6 +17,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" +#include "rs232_port_owner.h" #include "status_led.h" #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_initialized; static bool s_uart_active; +static bool s_cleanup_fault; typedef struct { const char *name; @@ -253,7 +255,7 @@ static const serial_format_t *find_serial_format(const char *name) return NULL; } -static int command_status(int argc, char **argv) +static int execute_status(int argc, char **argv) { (void)argc; (void)argv; @@ -278,7 +280,7 @@ static int command_status(int argc, char **argv) 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)) { printf("Usage: transceiver \n"); @@ -300,7 +302,7 @@ static int command_transceiver(int argc, char **argv) return 0; } -static int command_drivers(int argc, char **argv) +static int execute_drivers(int argc, char **argv) { int tx; int dtr; @@ -402,21 +404,21 @@ static esp_err_t run_static_loopback(loopback_configuration_t configuration) 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)argv; 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)argv; 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)argv; @@ -799,24 +801,32 @@ cleanup:; } } + bool deletion_failed = false; if (driver_installed) { esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT); if (delete_error != ESP_OK) { 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) { result = delete_error; } } } - /* GPIO17/18 return to static idle mode, then the transceiver is re-enabled. */ - s_transceiver_enabled = true; - esp_err_t restore_error = configure_static_gpio(true); - if (restore_error != ESP_OK) { - printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error)); - if (result == ESP_OK) { - result = restore_error; + if (!deletion_failed) { + /* GPIO17/18 return to static idle mode, then the transceiver is re-enabled. */ + s_transceiver_enabled = true; + esp_err_t restore_error = configure_static_gpio(true); + if (restore_error != ESP_OK) { + printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error)); + s_cleanup_fault = true; + if (result == ESP_OK) { + 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); @@ -829,7 +839,7 @@ cleanup:; return result; } -static int command_uart_loopback(int argc, char **argv) +static int execute_uart_loopback(int argc, char **argv) { long baud_rate; 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; } -static int command_uart_suite(int argc, char **argv) +static int execute_uart_suite(int argc, char **argv) { (void)argc; (void)argv; @@ -902,10 +912,13 @@ static esp_err_t finish_flow_test( } /* Stop the traffic source before removing the receiver's backpressure. */ + bool deletion_failed = false; if (generator_uart_installed) { esp_err_t delete_error = uart_driver_delete(RS232_TEST_GENERATOR_UART_PORT); if (delete_error != ESP_OK) { 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) { result = delete_error; } @@ -915,19 +928,26 @@ static esp_err_t finish_flow_test( esp_err_t delete_error = uart_driver_delete(RS232_UART_PORT); if (delete_error != ESP_OK) { 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) { result = delete_error; } } } - s_transceiver_enabled = true; - esp_err_t restore_error = configure_static_gpio(true); - if (restore_error != ESP_OK) { - printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error)); - if (result == ESP_OK) { - result = restore_error; + if (!deletion_failed) { + s_transceiver_enabled = true; + esp_err_t restore_error = configure_static_gpio(true); + if (restore_error != ESP_OK) { + printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error)); + s_cleanup_fault = true; + if (result == ESP_OK) { + 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); @@ -1164,7 +1184,7 @@ cleanup:; 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)argv; @@ -1493,7 +1513,7 @@ cleanup:; 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)argv; @@ -1502,6 +1522,59 @@ static int command_rts_flow_test(int argc, char **argv) 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) { s_transceiver_enabled = true; diff --git a/src/rs232_port_owner.c b/src/rs232_port_owner.c new file mode 100644 index 0000000..f28b8cf --- /dev/null +++ b/src/rs232_port_owner.c @@ -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"; + } +} diff --git a/src/rs232_port_owner.h b/src/rs232_port_owner.h new file mode 100644 index 0000000..8817953 --- /dev/null +++ b/src/rs232_port_owner.h @@ -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); diff --git a/src/serial_config.c b/src/serial_config.c new file mode 100644 index 0000000..fe0072e --- /dev/null +++ b/src/serial_config.c @@ -0,0 +1,407 @@ +#include "serial_config.h" + +#include +#include + +#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); +} diff --git a/src/serial_config.h b/src/serial_config.h new file mode 100644 index 0000000..1e3a1a5 --- /dev/null +++ b/src/serial_config.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + +#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); diff --git a/src/serial_console.c b/src/serial_console.c new file mode 100644 index 0000000..8fe6f67 --- /dev/null +++ b/src/serial_console.c @@ -0,0 +1,377 @@ +#include "serial_console.h" + +#include +#include +#include +#include +#include + +#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 \n"); + printf(" serial save|load|defaults|reset\n"); + printf(" serial counters|clear-counters\n"); + printf(" serial send-hex \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); +} diff --git a/src/serial_console.h b/src/serial_console.h new file mode 100644 index 0000000..dc2a527 --- /dev/null +++ b/src/serial_console.h @@ -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); diff --git a/src/serial_service.c b/src/serial_service.c new file mode 100644 index 0000000..bfcc93a --- /dev/null +++ b/src/serial_service.c @@ -0,0 +1,720 @@ +#include "serial_service.h" + +#include +#include + +#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); +} diff --git a/src/serial_service.h b/src/serial_service.h new file mode 100644 index 0000000..7705709 --- /dev/null +++ b/src/serial_service.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +#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); diff --git a/wiring.md b/wiring.md index 68d6e65..c7117c8 100644 --- a/wiring.md +++ b/wiring.md @@ -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. +## 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 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.