Serial Client Module
Section titled “Serial Client Module”Introduction
Section titled “Introduction”The Serial Client module (main/modules/serial/) implements the UART-based hardware serial transport for CNC machine communication. It is the primary CNC link in the default pendant configuration (serial/USB CNC + WiFi remote), where the pendant communicates with the CNC controller (FluidNC, grbl, grblHAL) over a direct UART connection.
The module provides a full-duplex, line-oriented UART channel, a bidirectional message queue (using the shared ESP3DClient infrastructure), an automatic connection handshake, a periodic ping/inactivity-timeout mechanism, and run-time controls for baud-rate change and RX/TX pin swap.
Build-model note: Only one CNC transport is active at a time. When
SOCKET_CLIENT_SERVICEis OFF (the default), serial is the CNC link. Seeconnection_management.mdfor the full transport-selection model and the feature resource matrix for memory constraints.
Architecture Overview
Section titled “Architecture Overview”File Reference
Section titled “File Reference”| File | Role |
|---|---|
main/modules/serial/esp3d_serial_config.h | Configuration struct esp3d_serial_config_t |
main/modules/serial/esp3d_serial_client.h | Class declaration ESP3DSerialClient |
main/modules/serial/esp3d_serial_client.cpp | Implementation + FreeRTOS task |
Core Components
Section titled “Core Components”esp3d_serial_config_t
Section titled “esp3d_serial_config_t”Defined in esp3d_serial_config.h. Groups all UART driver parameters into a single struct so the client can be (re-)configured without scattering constants through the code.
typedef struct { uart_port_t port; // UART_NUM_0 / UART_NUM_1 / UART_NUM_2 gpio_num_t rx_pin; // Physical RX GPIO gpio_num_t tx_pin; // Physical TX GPIO gpio_num_t rts_pin; // RTS GPIO (GPIO_NUM_NC if unused) gpio_num_t cts_pin; // CTS GPIO (GPIO_NUM_NC if unused) bool swap_rx_tx; // Runtime pin-swap flag uart_config_t uart_config; // ESP-IDF standard UART config (baud, bits, parity…) size_t rx_buffer_size; // Driver RX ring-buffer (bytes) size_t tx_buffer_size; // Driver TX ring-buffer (bytes) uint16_t rx_flush_timeout; // ms — flush partial line if no new byte arrives uint32_t task_priority; // FreeRTOS priority for the RX task uint32_t task_stack_size; // RX task stack in bytes BaseType_t task_core; // Core affinity (0 or 1)} esp3d_serial_config_t;The single global instance esp3dSerialConfig is populated at compile time from board-specific macros (UART_PORT_IDX, UART_RX_PIN, UART_TX_PIN, etc.) defined via serial_def.h → board_config.h. loadSettings() then overrides baud_rate and swap_rx_tx at runtime from NVS.
ESP3DSerialClient
Section titled “ESP3DSerialClient”Declared in esp3d_serial_client.h, inherits from ESP3DClient (see esp3d_core.md).
Class Diagram
Section titled “Class Diagram”Key Member Roles
Section titled “Key Member Roles”| Member | Type | Purpose |
|---|---|---|
_started | bool | UART driver installed and RX task running |
_active | bool | Processing enabled. false after disconnect(), true after reconnect() |
_init_command_sent | bool | Init handshake sent to CNC; reset on timeout/reconnect/disconnect |
_ping_command_sent | bool | Ping dispatched (informational; guards ping tracking) |
_data | uint8_t* | Raw read buffer — direct output of uart_read_bytes() |
_buffer | uint8_t* | Line assembly buffer — accumulates bytes until \n/\r or buffer full |
_bufferPos | size_t | Current write position in _buffer |
_config | ptr | Pointer to active config struct (points to esp3dSerialConfig) |
_xHandle | TaskHandle_t | Handle of esp3d_serial_rx_task; cleared by the task before self-delete |
Lifecycle Management
Section titled “Lifecycle Management”begin()
Section titled “begin()”- BT memory release — if neither
ESP3D_BT_SERIAL_FEATUREnorESP3D_BT_BLE_FEATUREis selected, releases BT controller memory (recovers ~70 KB DRAM). - Queue sizing — RX max 4 KB (2 KB for non-CNC outputs), TX max 4 KB (to buffer a full
[ESP0]help output in one pass at 115 200 baud). loadSettings()— readsesp3d_baud_rateandesp3d_swap_rx_txfrom NVS; falls back to defaults on invalid values.- Buffer allocation — two
malloc()blocks ofrx_buffer_sizebytes each; aborts on failure, logging free heap and largest free block. - Mutex init — separate
pthread_mutex_tfor RX queue and TX queue. - UART driver install —
uart_driver_install()with doubled ring-buffer for headroom. - Pin config —
_configure_uart_pins()handles optional swap. - Task creation —
esp3d_task_create_pinned()on the configured core. - Initial status — sets
connection_status = "T"(connecting) viaESP3DValuesif serial is the output client.
Gracefully shuts down in reverse order: signals the task by clearing _started, waits up to 500 ms for the task to self-delete (the task calls resetTaskHandle() before vTaskDelete(NULL)), force-deletes if it stalls, destroys mutexes (nulling the base-class pointer to prevent stale-mutex UB on re-entry), uninstalls the UART driver, then frees buffers.
disconnect() / reconnect()
Section titled “disconnect() / reconnect()”| Action | disconnect() | reconnect() |
|---|---|---|
_active | false | true |
_init_command_sent | true (suppress re-init) | false (allow re-init) |
| UART RX ring-buffer | flushed | flushed |
| Client RX queue | cleared | cleared |
| Client TX queue | cleared | not cleared |
connection_status | "?" | "T" (connecting) |
| Immediate ping | — | yes (via sendPingCommand()) |
disconnect() clears the TX queue to prevent stale stream/script traffic from timing out on an unattended target. reconnect() is called from the UI retry button — it flushes stale RX data then sends a ping immediately so the CNC responds without waiting up to 10 s for the periodic ping timer.
Connection State Machine
Section titled “Connection State Machine”Status codes used in ESP3DValuesIndex::connection_status:
| Code | Meaning |
|---|---|
"T" | Connecting / trying |
"C" | Connected and identified |
"?" | Disconnected / unknown |
"U" | Uninitialised (before begin()) |
Data Flow
Section titled “Data Flow”RX Path (CNC → Pendant)
Section titled “RX Path (CNC → Pendant)”TX Path (Pendant → CNC)
Section titled “TX Path (Pendant → CNC)”Key design points:
- The RX task calls
readSerial()which blocks for up to 10 ms insideuart_read_bytes(). No extravTaskDelayis added to minimise ACK latency during GCode streaming. - The TX drain runs in
handle()on the LVGL task (Core 1), gated bycanSendData(). This prevents TX from draining while_active == false. - The RX drain in
handle()is intentionally not gated by_activeso incoming firmware responses are still processed during a disconnect event. - When the RX queue is full,
pushMsgToRxQueue()evicts old messages by processing them immediately (esp3dCommands.process()) to make room, rather than silently dropping the incoming message.
Ping & Inactivity Timeout
Section titled “Ping & Inactivity Timeout”This mechanism is compiled only when ESP3D_DISPLAY_FEATURE is enabled and only runs when serial (or uart_ext) is the active output client.
Constants:
| Constant | Value | Purpose |
|---|---|---|
ESP3D_PING_INTERVAL | 10 000 ms | Period between automatic pings when idle |
ESP3D_INACTIVITY_TIMEOUT | 30 000 ms | Declare disconnection after this many ms with no valid data |
BT module SCANNING lines do not update lastActivity, so the ping/timeout cycle operates correctly even when a Bluetooth bridge module is attached and producing periodic output.
Initialization Handshake
Section titled “Initialization Handshake”Runtime Configuration
Section titled “Runtime Configuration”Baud Rate Change (change_baud_rate)
Section titled “Baud Rate Change (change_baud_rate)”Resetting _init_command_sent ensures the handshake is replayed at the new baud rate once the CNC responds.
RX/TX Pin Swap (swap_rx_tx)
Section titled “RX/TX Pin Swap (swap_rx_tx)”Some cable configurations have RX and TX physically crossed. swap_rx_tx(bool) re-routes the GPIO signal assignments via esp_rom_gpio_connect_out/in_signal() without reinstalling the UART driver. The setting is persisted to NVS by the Settings screen.
_configure_uart_pins() is called by both begin() and swap_rx_tx() so pin setup is always consistent.
Memory Layout
Section titled “Memory Layout”Allocation rules (per ESP32 memory constraints):
- Both
_dataand_bufferaremalloc()’d inbegin(). If either fails,begin()logs total free heap and largest free block, then returnsfalse. - Buffers are
free()’d inend()before the function returns. rx_buffer_sizecomes fromUART_RX_BUFFER_SIZE * 2(board config); the UART driver ring-buffer is doubled again (rx_buffer_size * 2) for driver headroom.- TX queue max is 4 KB so the full
[ESP0]help output (~2700 bytes) fits in one pass. At 115 200 baud the UART drains ~115 bytes per 10 ms tick; a 1 KB limit causes livelock where each drain cycle frees less than the next entry adds. - See
esp32_memory_constraints.mdfor heap, fragmentation, and WiFi-vs-BT RAM trade-offs that affect sizing decisions.
a14 Changes
Section titled “a14 Changes”- Own answers sent at once. Received messages go through
processRxMsg(), andprocess()first callssendOwnAnswer(): a long answer to a command received on this link ([ESP400],[ESP0],[ESP740]…) is written at once instead of overflowing the TX queue. The transport write is factored intowriteTxMsg(), shared with thehandle()TX drain. - Low-memory admission and lost lines. Each received line first goes through
rxAdmit()(a refused line is dropped on purpose); allocation failures callesp3d_rx_line_lost(), and a line not sent to the CNC callsesp3d_tx_line_lost()(itsokwill never come, S21). See CNC_Firmware_Integration.md, “Lost Lines, Input Overflow and Resets”. - Two roles.
isCncLink()is true when serial / UART Ext is the CNC output: controller responses, no authentication. Otherwise UART0 is a command console: with authentication (and serial authentication not disabled) its level is kept by the client and checked on every line throughcheckCommandChannelLine(); a guest may only send ESP commands, gets answers to its own commands only (no machine stream) and a refusal text otherwise;logout()serves[ESP500]LOGOUT. A (re)started console starts as guest. See esp3d_x_stream_authentication.md, section 2.2. - UART overflow detection (input overflow S7). Without flow control the driver loses bytes inside lines when its RX ring is full, which could reach the CNC as truncated but valid G-code. A UART event queue (
UART_EVENT_QUEUE_SIZE= 20) is installed only to seeUART_FIFO_OVF/UART_BUFFER_FULL;readSerial()drains it without blocking every loop, andcheckUartOverflow()also catches a ring found full when the events were dropped. Reaction: the buffered data is flushed, the current line and the rest of the broken line up to the next end char are dropped, and the loss is reported as a local buffer overflow of this source (on the CNC link the flow is reset and the streams stop with an error). Overflow events during a pause concern purged data and are not a loss. - Complete writes.
uart_write_bytes()may return a partial count when the TX ring is full; the remainder used to be dropped, truncating G-code mid-word (S1000→S1). The client now loops until the whole message is written, sleeping one real tick when there is no progress and giving up after about 500 ms of a stalled TX buffer. - Boot ping. As soon as the gcode host task runs, a ping is sent (like
reconnect()), so a controller that is already running answers at once and the startup sequence, which waits forserver_statusC, does not wait for the first 10 s ping. The init command ($I) is written straight to the TX queue, not through the gcode host, because it is sent when host commands may be stuck waiting for anok; itsokis unexpected for the host, which never lets such an ack end a command not sent yet. - Robust
begin(). NoESP_ERROR_CHECK(a failure must notabort()); any failure after the driver install rolls back throughend()so a laterbegin()starts clean. - Rate-limited drop report. RX messages dropped by
pushMsgToRxQueue()are reported once per second with a count (one error log per line under a flood kept the core 0 task busy until the task watchdog fired, 2026-10-20). - The reported client type is
uart_extwhen it is the output client,serialotherwise.
Build-Time Feature Flags
Section titled “Build-Time Feature Flags”| Flag | Effect on this module |
|---|---|
ESP3D_DISPLAY_FEATURE | Enables BT-noise filtering, ping/timeout loop, and connection_status / server_status updates via ESP3DValues |
ESP3D_BT_SERIAL_FEATURE / ESP3D_BT_BLE_FEATURE | When both absent, begin() calls esp_bt_controller_mem_release(ESP_BT_MODE_BTDM) to recover BT RAM |
ESP3D_UART_EXT_FEATURE | RX message origin / target set to uart_ext when that client is the active output client |
ESP3D_DISABLE_SERIAL_AUTHENTICATION_FEATURE | Forces authentication_level = admin on all incoming RX messages |
TARGET_IS_NONE | Skips swap_rx_tx NVS read; getInitCommand() returns "" (handshake bypassed) |
CONFIG_UART_ISR_IN_IRAM | Sets ESP_INTR_FLAG_IRAM for the UART driver ISR to avoid cache-miss stalls during flash operations |
Integration with Other Modules
Section titled “Integration with Other Modules”| Collaborator | Interaction |
|---|---|
esp3d_core.md — ESP3DClient | Base class — dual-deque message queues, message factory (newMsg, setDataContent, deleteMsg), mutex helpers, priority enqueue |
esp3d_commands.md — ESP3DCommands | process(msg) called for each dequeued RX message; getOutputClient() checked to decide routing and noise filtering |
gcode_host.md — ESP3DGCodeHandlerService | getInitCommand(), sendPingCommand(), resetStartupCommandsSent() — target-firmware-specific hooks |
values.md — ESP3DValues | set_value(connection_status / server_status) for UI feedback; thread-safe get_value() overload used from the RX task |
ESP3DSettings | readUint32(esp3d_baud_rate), readByte(esp3d_swap_rx_tx) from NVS on loadSettings() |
ESP3DRenderingClient | Receives CNC responses when serial is the output client (message target = all_clients) |
bluetooth_serial | Sibling transport module — same ESP3DClient base and connection-status model |
socket_client.md | Sibling transport module — same ESP3DClient base and connection-status model |
usb_serial.md | Sibling transport module — same ESP3DClient base and connection-status model |
gcode_host.md | Streaming flow-control details, in-flight cap mechanism, and startup command sequence |
CNC_Firmware_Integration.md | Overall CNC firmware integration overview |
Communication_Transports.md | Parent module — all four transport modules in context |
Global Instance
Section titled “Global Instance”// esp3d_serial_client.cpp — definitionESP3DSerialClient serialClient;
// esp3d_serial_client.h — declaration for other translation unitsextern ESP3DSerialClient serialClient;serialClient is a file-scope singleton. Other modules (GCode host, commands dispatcher, UI settings screen) reference it via the extern declaration. The board configuration singleton esp3dSerialConfig is defined in the same .cpp file and handed to the client via configure(&esp3dSerialConfig) inside loadSettings().