Skip to content

CNC Firmware Integration

The CNC_Firmware_Integration module is the bidirectional bridge between the pendant firmware and the connected CNC controller. It is responsible for:

  • Scheduling, pacing, and acknowledging every outbound G-code or ESP command through a priority queue and FreeRTOS streaming engine (ESP3DGCodeHostService).
  • Parsing firmware protocol responses (status reports, ok/error acknowledgements, file listings, parser state) and publishing decoded values into the observable ESP3DValues store so the UI reacts in real time.
  • Enforcing flow-control gates before each transmitted line to avoid overrunning the controller’s RX buffer and planner queue.
  • Managing firmware-family-specific session lifecycle (startup sequences, MPG token negotiation, status polling).

The module is compiled once per firmware target. CMake selects exactly one firmware handler and one flow-control implementation at build time — there is no runtime polymorphism overhead.

The same core (ESP3DGCodeHostService, ESP3DXStream) also serves the 3D printer domain: a Marlin handler and a 3D printer flow layer exist as groundwork (main/target/3dprinter/). Repetier and Smoothieware are placeholders only (description.txt, no code).


Core - ESP3DValues

CNC Firmware Integration

Command Producers

process msg

process msg

process msg

process msg

gcodeHostFlow*

classify RX / send

set_value

subscriptions

dispatch

bytes

Output Transports - one active

Serial UART

USB Serial

BT SPP / BLE

Socket / WebSocket

UI Screens

jog / status / macros / probe

ESP3DCommands

ESP700 / ESP701

HTTP / WebSocket

handlers

Macro Manager

SD / Flash scripts

ESP3DXStream

Transport orchestrator

ESP3DGCodeHostService

Scheduler · State machine · File I/O

gcodeHostFlow*

Flow control gates

ESP3DGCodeHandlerService

Firmware protocol handler

esp3dXValues

Observable state store

CNC Firmware

FluidNC / grblHAL / GRBL

Core - shared across all targets

gcodeHostFlow*

esp3dGcodeHandler.*

Handler - one compiled per build

cnc_fluidnc

target/cnc/fluidnc/

cnc_grblhal

target/cnc/grblhal/

cnc_grbl

target/cnc/grbl/

marlin (groundwork)

target/3dprinter/marlin/

none_target

target/none/

Flow layer - one compiled per build

CNC flow

target/cnc/modules/gcode_host/

ok credit + planner + RX gates

3D printer flow

target/3dprinter/modules/gcode_host/

ok credit + RX window

None flow

target/none/modules/gcode_host/

Permissive stubs

ESP3DGCodeHostService

main/modules/gcode_host/

ESP3DXStream

main/modules/gcode_host/


The host service maintains two independent stream queues processed every 10 ms tick:

takes precedence

fallback when _scripts empty

_streams - long-running file job queue

fs_stream - job 1

fs_stream - job 2

_scripts - immediate / priority queue

single_command

multiple_commands

fs_script / sd_script

_handle_stream_selection()

Active stream

_current_stream_ptr

Scripts always preempt file streams at ready_to_read_cursor or paused states. A file stream paused while scripts run auto-resumes once the script queue drains.


Each queued stream advances through the following states (up to 4 steps per 10 ms tick):

file opened / command ready

no pending request

pause requested

G-code line read

cursor == totalSize

at in-flight cap

credits available

flow gate closed - yield + save line

ok received

2 s silence, realtime ? keepalive

error received, command rejected

timeout 10 s, link lost

resume notification

_resume_pending set

start

ready_to_read_cursor

read_cursor

pause

send_gcode_command

end

wait_for_ack

error

paused

resume

The keepalive ? is sent on the high-priority path (no flow credit), so the 10 s timeout only fires on a dead link, not during a feed hold or a long dwell. The same 2 s ping protects a closed flow gate: if the pings stay unanswered for 10 s, the link is declared lost. An error: is a negative ack of exactly one line: one credit is released, the current stream and the main file stream go to error (command_rejected); there is no resend. Detail: gcode_host_streaming_flow.md, “ACK, timeout, and errors”.


gcodeHostFlowCanSendLine() evaluates four conditions before any normal G-code line may be sent:

No

Yes

No

Yes

No

Yes

No

Yes

gcodeHostFlowCanSendLine(line_len)

pending_ok < max_in_flight?

planner blocks > threshold?

inflight_bytes + wire_len ≤ rx_buffer_size?

wire_len ≤ rx_bytes_free snapshot?

✅ can send

🚫 yield to ready_to_read_cursor

(save line - scripts refresh Bf)

GateThreshold
ok creditgetStreamMaxInFlight() — 4 for FluidNC / grblHAL / GRBL
Planner buffer≤ 5 free blocks → blocked (kPlannerBlocksThreshold)
RX window (hard)getStreamRxBufferSize() — grblHAL: 1024 B, FluidNC: 256 B, GRBL: 128 B by default, updated from [OPT:]
RX bytes snapshot`

The 3D printer flow (Marlin groundwork) keeps only the ok credit (1 line in flight) and the same character-counting RX window (120 B): classic Marlin has no |Bf:| planner report.


Featurecnc_grblcnc_fluidnccnc_grblhal
Status reportingPoll ? at intervalAuto-report ($Report/Interval)Poll ? / auto-report ($481)
SD card filesPendant SD only (streamed)Firmware SD ($SD/Run=)Firmware SD (firmware run cmd)
MPG token machineNoNoYes (4 states, 0x8B)
Homing queries$22 onlyPer-axis ($/axes/x/homing/…)$22 + $44–$49
Error formaterror:N (codes 1–38)error:N (codes 1–152)error:N (codes 1–88, 253)
Alarm formatALARM:N (codes 1–9)[MSG:ERR:…] richALARM:N (codes 1–21)
RX buffer128 B (from [OPT:])256 B1024 B
Startup phases4-phase6-phase10-phase
Macro executionStream from pendant SD$SD/Run=<path>Firmware run command

High-priority messages bypass flow control entirely:

Yes

No

process(msg)

msg->priority == high?

_forwardUrgent(msg)

Bypass flow - TX front

(realtime: !, ~, 0x18, 0x85, ?)

Normal RX queue

Stream state machine + flow control


A line lost on the way to or from the controller breaks the ok-credit accounting (an ok never comes, or comes for a line the host does not know). The host handles every loss explicitly instead of waiting for a timeout. The detection is allocation-free and works from any task (main/core/esp3d_client.cpp); the host task consumes the flags in handle().

EventDetectionHost reaction
Memory saturation (case A) — an input line refused because the heap is below the admission reserve (6144 B free / 2048 B largest block; CNC link vital lines down to 3072 / 1024 B)esp3d_rx_overflow_raise()Full purge of every queue, critical alert, input closed: every input except the CNC link and single safety realtime commands (! ~ ? 0x18 0x85) is refused until the user presses Restart or Recover. The firmware status icon and screen show ERROR while the input is closed
Local buffer overflow (case B) — a queue of one origin full while memory is fineesp3d_rx_buffer_lost(origin)Only that origin is purged, non-blocking notice; an SD job from another origin continues
Line for the controller not sent (write error, TX queue full on the CNC output client)esp3d_tx_line_lost(client)Flow reset, streams stopped, notice “Lines to CNC not sent: stream stopped”
Isolated input line dropped under low memory (no burst)esp3d_rx_isolated_drop_take()Line dropped, no purge; warning “Low memory: command dropped” (at most once per 10 s)
Controller session reset — welcome banner after a soft reset or a controller rebootthe firmware handler calls gcodeHostService.requestControllerSessionReset()consumeControllerSessionReset() drops the host state; a running job is aborted with a message
User Restart / Recover (firmware status screen, critical alert)gcodeHostService.resetAll()Waits up to 500 ms for the host tick to be idle, drops every queue, clears the overflow flags, reopens the input, clears the critical alert. If refused (timeout), the soft reset sent next still triggers the session reset path

Purges called from other tasks (resetAll(), purgeByOrigin(), purgeByRequestId()) run under the same exclusion as the host tick (_emergency_stopping + wait for idle). Detail: gcode_host_streaming_flow.md (“Purges from another task”) and esp32_memory_constraints.md §3 “Low-memory admission guard”.


main/modules/gcode_host/
├── esp3d_gcode_host_service.h/.cpp ← ESP3DGCodeHostService, dual queues, state machine
├── esp3d_gcode_host_types.h ← Enums: stream type, stream state, host state, errors
├── esp3d_x_stream.h/.cpp ← ESP3DXStream, transport orchestrator
main/target/cnc/modules/gcode_host/
└── esp3d_gcode_host_flow.cpp ← CNC flow gates (ok credit + planner + RX window)
main/target/none/modules/gcode_host/
└── esp3d_gcode_host_flow.cpp ← Permissive stubs (TARGET_FW_NONE)
main/target/cnc/fluidnc/
└── esp3d_gcode_handler_service.h/.cpp ← FluidNC protocol handler
main/target/cnc/grbl/
└── esp3d_gcode_handler_service.h/.cpp ← GRBL 1.1 protocol handler
main/target/cnc/grblhal/
└── esp3d_gcode_handler_service.h/.cpp ← grblHAL protocol handler
main/target/3dprinter/modules/gcode_host/
└── esp3d_gcode_host_flow.cpp ← 3D printer flow (ok credit + RX window)
main/target/3dprinter/marlin/
└── esp3d_gcode_handler_service.h/.cpp ← Marlin protocol handler (groundwork)
main/target/none/
└── esp3d_gcode_handler_service.h/.cpp ← Null stub handler (inline methods; the .cpp only defines the global instance)
main/core/esp3d_client.cpp ← Low-memory admission guard, lost-line / overflow flags

DocumentTopic
docs/architecture/gcode_host_architecture.mdCore + domain flow split, CMake wiring, testing scope
docs/architecture/gcode_host_streaming_flow.mdState machine detail, pause/resume/abort semantics, flow gates
docs/architecture/esp3d_message_priority_system.mdNormal vs high-priority message routing
docs/architecture/connection_management.mdTransport lifecycle, connection status
docs/features/feature_resource_matrix.mdFeature compatibility, WiFi/CNC usage model
docs/guides/esp32_memory_constraints.mdHeap, fragmentation, allocation rules for stream nodes
docs/guides/esp3d_log_guide.mdesp3d_log macros, flow instrumentation
docs/ongoing_reflection/input_overflow_postmortem.mdInput overflow cases A / B, S7 (UART ring), S21 (TX loss), test status