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:
@@ -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` | 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
|
```text
|
||||||
status
|
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.
|
`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:
|
The onboard RGB LED reports the most recent test-harness state:
|
||||||
|
|
||||||
| Color | Meaning |
|
| Color | Meaning |
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-23
@@ -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,24 +801,32 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GPIO17/18 return to static idle mode, then the transceiver is re-enabled. */
|
if (!deletion_failed) {
|
||||||
s_transceiver_enabled = true;
|
/* GPIO17/18 return to static idle mode, then the transceiver is re-enabled. */
|
||||||
esp_err_t restore_error = configure_static_gpio(true);
|
s_transceiver_enabled = true;
|
||||||
if (restore_error != ESP_OK) {
|
esp_err_t restore_error = configure_static_gpio(true);
|
||||||
printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error));
|
if (restore_error != ESP_OK) {
|
||||||
if (result == ESP_OK) {
|
printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error));
|
||||||
result = 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);
|
esp_err_t led_error = status_led_set(result == ESP_OK ? STATUS_LED_PASS : STATUS_LED_FAIL);
|
||||||
@@ -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,19 +928,26 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s_transceiver_enabled = true;
|
if (!deletion_failed) {
|
||||||
esp_err_t restore_error = configure_static_gpio(true);
|
s_transceiver_enabled = true;
|
||||||
if (restore_error != ESP_OK) {
|
esp_err_t restore_error = configure_static_gpio(true);
|
||||||
printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error));
|
if (restore_error != ESP_OK) {
|
||||||
if (result == ESP_OK) {
|
printf("Could not restore static GPIO mode: %s\n", esp_err_to_name(restore_error));
|
||||||
result = 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);
|
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);
|
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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user