Skip to content

FluidNC Module — Probe Screen

The probe screen is a FluidNC-specific UI screen that provides an interactive CNC probing interface on the pendant. It orchestrates full G38.2 probing cycles — fast seek, retract, precise feed, and work-coordinate-zero application — driven by a deterministic state machine that reacts to live firmware status and [PRB:...] messages from FluidNC.

The screen is implemented in main/display/cnc/fluidnc/screens/probe_screen.cpp under the probeScreen namespace and exposes a small public API for inter-screen communication.

Parallel probe screens exist for grbl and grblHAL targets (same structure, same GCode commands). This document covers only the FluidNC variant. For the shared CNC screen infrastructure see fluidnc_module.md and UI_Framework_Screens.md.


GCode Output (ESP3DGCodeHandler)

Value Subscriptions (ESP3DValues)

LVGL UI Layer

probeScreen namespace

on_firmware_status_update

on_probe_status_update

on_error_status_update

on_axis_count_update

on_axis_count_update

on_pins_state_update

on_positions_update

on_connection_status_update

probeScreen::create()

Probe State Machine

(ProbeState enum)

Probe Sequence

startProbeSequence()

stopProbeSequence()

sendProbeCommand()

Public API

getResult() / clearResult()

setReturnScreen()

GenericScreen

(screen container + rotation)

VirtualButtonsComponent

(OK / Play-Stop / Back)

PanelComponent

(4 probe param items)

FirmwareStatusComponent

(overlay, deferred)

ConnectionStatusComponent

(overlay, deferred)

Axis Button

btnAxe + axe_label

Position Display

(WPos big + MPos small)

Probe Parameter Buttons 2x2

Offset / MaxTravel / Retract / FeedRate

Probe Status Bar

(label + spinner + icon)

Pin State Indicators

(7 squares: X Y Z A B C P)

firmware_status

probe_status

last_error_status

axis_count

pin_states

position_w[x-c]

position_m[x-c]

server_status

? (realtime)

G91 G38.2 axis travel F feed

G91 G1 axis retract F feed

G91 G38.2 axis travel F feed_half

G10 L20 P0 axis offset

+ restore work unit / distance mode

! Feed Hold (realtime)

+ requestResetBeforeResume()


Core Platform

Common UI Infrastructure

CNC Shared

FluidNC Module

uses

uses

uses

subscribes to

sends GCode via

reads lock state from

translates labels via

setReturnScreen

getResult

navigates to

returns to

returns to

probe_screen

(this module)

change_tool_screen

setReturnScreen / getResult

files_screen

main_screen

GenericScreen

PanelComponent

input_screen

(numeric InputEditor)

ESP3DValues

(observable system)

ESP3DGCodeHandler

UIManager

(lock state, theme, orientation)

ESP3DTranslationService


Holds the four user-configurable probe parameters per axis. Since a14 they are stored in NVS (settings esp3d_<axis>_probe_params, edited from this screen only) as "offset;max_travel;retract;feed_rate". Linear axes hold one section per work unit (mm;…;in;…, selected by G20/G21 without conversion, see inch_mm_units_reference.md); rotary axes (degrees) a plain list. probe_axis_values_map (keyed by real axis index 0=X … 5=C) caches the values of the unit they were loaded for, with a per-axis loaded unit (-1 = not loaded, 0 = mm, 1 = inch); they are reloaded when the probe unit changes. An edit is written into the section of the active unit, the other section is kept. A malformed stored list falls back to the setting default.

FieldDefault X/Y (mm / in)Default Z (mm / in)Default A/B/C (°)Description
offset0 / 010 / 0.40Work-coordinate offset applied via G10 L20 after probing
max_travel30 / 1.2−50 / −2360Maximum probe travel; negative = probe toward machine origin
retract5 / 0.23 / 0.1210Retract distance after first contact (always positive)
feed_rate300 / 12200 / 8100Seek and retract feed rate; the precise feed uses half this value

Probe unit (a14): the unit of a linear axis is the work unit G20/G21, or the report unit ($13 / report_inches) when esp3d_jog_position_unit (“Commands in position unit”) is on, read at each screen creation; rotary axes are never in inches. Values are shown and sent with up to 4 decimals (trailing zeros removed): one decimal used to round them (a 15.05 mm plate became Z15.1, 0.59 in became Z0.6). The feed rate is an integer in mm/min and has 1 decimal in in/min.

Used for inter-screen communication. Declared in probe_screen.h.

enum class ProbeResult : uint8_t {
NONE = 0, // No probe performed / reset
SUCCESS = 1, // Probe completed and offset applied
FAILED = 2, // Error, alarm, or no contact
CANCELLED = 3 // User pressed STOP
};

Internal state machine enum controlling which GCode commands are sent and which firmware replies are expected.

StateDescription
IDLEReady, awaiting user action
PRE_CHECKMachine already Idle: start at once (the value is read directly, since ESP3DValues only notifies on change); otherwise ? is sent and the screen waits for Idle. A stable non-Idle state that never becomes Idle on its own (Hold, Alarm, Door, error) refuses the start and reports it; transient states (Run, Jog, Home) wait
SEEKFast G38.2 probe in progress, waiting for [PRB:...]
RETRACTRetract G1 in progress, waiting for Idle
FEEDSlow precise G38.2 in progress, waiting for [PRB:...]
APPLY_OFFSETG10 L20 in progress, waiting for Idle
SUCCESSSequence complete, offset applied
FAILEDAny step produced an error, alarm, or no contact
CANCELLEDUser pressed STOP: feed hold sent and the next resume armed as a soft reset

User presses PLAY

firmware_status IDLE\nsend G91 G38.2 F_feed

firmware_status ALARM or error\nor stable Hold / Door at start

firmware_status RUN\nwaiting

probe_status contact 1\nsend G91 G1 F feed_rate

probe_status contact 0\nno contact detected

firmware_status ALARM or error

firmware_status IDLE\nsend G91 G38.2 F_feed_half

firmware_status ALARM or error

probe_status contact 1\nsend G10 L20 P0

probe_status contact 0

firmware_status ALARM or error

firmware_status IDLE\noffset applied, modes restored

firmware_status ALARM or error

User presses PLAY again

User presses PLAY again

User presses PLAY again

User presses STOP\nsend feed-hold

User presses STOP\nsend feed-hold

User presses STOP\nsend feed-hold

User presses STOP\nsend feed-hold

IDLE

PRE_CHECK

SEEK

FAILED

RETRACT

FEED

APPLY_OFFSET

SUCCESS

CANCELLED

Timeout safety: A one-shot LVGL timer is started at startProbeSequence() and covers the whole sequence: SEEK = |max_travel| at feed_rate, RETRACT = retract at feed_rate, FEED = |max_travel| at feed_rate / 2. Its duration is clamp(sum of step times (min) × 60 × 1.5 + 5, 10, 300) seconds (the limit was 120 s, too short with the half-speed pass on long travels). On expiry, feed-hold (!) is sent and the state transitions to FAILED.

STOP (a14): the feed hold is followed by esp3dGcodeHandler.requestResetBeforeResume(): resuming would restart the cancelled probe move, so the next ~ becomes a host purge + soft reset (see cnc_fluidnc.md). The hold keeps the machine, and the CANCELLED / FAILED report, in place until the user acts. Pressing STOP again in CANCELLED sends nothing (no second !). Error values are matched case-insensitively (ERROR:<code>, ERROR:<msg> or a raw Error: line).


Probe StepGCode SentNotes
PRE_CHECK? (realtime), only if not already IdleConfirms machine is Idle before starting
SEEKG91 [unit] G38.2 {Axis}{max_travel} F{feed_rate}Fast probe pass; max_travel may be negative (e.g., Z down)
RETRACTG91 [unit] G1 {Axis}{±retract} F{feed_rate}Retract in opposite direction of max_travel sign, at the user feed rate (a fixed F200 meant 200 in/min in G20)
FEEDG91 G38.2 {Axis}{max_travel} F{feed_rate/2}Slow precise probe
APPLY_OFFSETG10 L20 P0 {Axis}{offset}, then the restore lineSet work-coordinate zero at probed position + offset; then restore the work unit and distance mode read from [GC:] at start
STOP / Timeout! (realtime Feed Hold)Sent on user cancel or safety timeout

Modal words (prepareProbeModalState(), a14): every probe line is G91 and carries a unit word (G20 / G21) when the probe unit is not the work unit (report unit with esp3d_jog_position_unit, or a rotary axis in G20). Both are modal, so the last line of a successful sequence restores the distance mode and the work unit read from [GC:]. Nothing is restored after a failure (alarm) or a STOP (hold, then reset on resume): the reset restores the controller’s default modes, and a queued line would be refused in alarm. With an unknown parser state (no [GC:] yet) nothing is restored.

The axis letter is resolved from ESP3DValuesIndex::axis_names to correctly honor grblHAL $376 UVW naming (e.g., U, V, W instead of A, B, C). FluidNC falls back to XYZABC when the field is absent.


The screen uses a GenericScreen container with three zones:

┌──────────────────────────────────────────┐
│ [Axis label / lock icon] WPos: 0.000 │
│ MPos: 0.000 │
│ [O: 10.0] [T: -50.0] │
│ [R: 3.0] [F: 200 ] │
│ ────────────────────────────────────── │
│ Probe status text... [spinner/icon]│
│ X Y Z A B C P (pin indicators) │
├──────────────────────────────────────────┤
│ [OK] [Play/Stop] [Back] │
└──────────────────────────────────────────┘
  • Axis button (btnAxe): shows the active axis letter or a lock icon. Tapping cycles axes in touch mode; hardware-switch mode uses physical position.
  • Position display: two stacked labels — WPos (large font, work coordinates) and MPos (medium font, machine coordinates) for the selected axis.
  • Parameter buttons (2×2 grid): O: offset, T: max travel, R: retract, F: feed rate. All four are items in PanelComponent and are encoder-navigable.
  • Status bar: a scrolling text label plus either a spinner (active states) or a colored icon (ok_b green = SUCCESS, close_b red = FAILED/CANCELLED).
  • Pin indicators: 7 small square labels (X Y Z A B C P) that appear/hide based on active pin states from [PRB:...] and limit inputs; only pins up to axis_count are shown (P=probe pin is always visible).
  • Button 1 (center): icon swaps dynamically between play_b and stop_b to reflect the current probe state.

raw GCode and status

parsed fields

firmware_status

probe_status PRB

last_error_status

axis_count

pin_states

position_w[x-c]

position_m[x-c]

server_status

FluidNC Firmware

(serial / USB / TCP)

ESP3DGCodeHandlerService

(parses FluidNC output)

ESP3DValues

(observable store)

on_firmware_status_update

state machine transitions

on_probe_status_update

contact detection

on_error_status_update

FAILED if probing active

on_axis_count_update

reinit params, redraw axis UI

on_pins_state_update

change detect then updatePinsStates

on_positions_update

throttled 250ms per group

on_connection_status_update

system lock or unlock

LayerMechanismDetail
Layer 2 — Pin statesString change detectionlast_known_pin_states[] compared via strcmp; all 7 LVGL hide/show calls skipped when the string is unchanged (callbacks fire at 10 Hz)
Layer 3 — Position displayPer-group timestamp throttleSeparate last_wpos_display_update_ms / last_mpos_display_update_ms; minimum interval 250 ms per group so both WPos and MPos callbacks are throttled independently
Layer 3 — Position flushposition_flush_timerFires every 300 ms to apply any pending throttled update, ensuring no update is permanently lost
Deferred overlayslv_timer_create at startupFirmwareStatusComponent and ConnectionStatusComponent are created after ESP3D_DEFERED_SCREEN_CREATION_DELAY_MS to avoid blocking the initial screen paint

The screen supports 3 to 6 physical axes via axis_count (driven by ESP3DValuesIndex::axis_count):

axis_count value

3 axes

Slot 0=X 1=Y 2=Z

Slot 3 = LOCK position

4 axes

Slot 0=X 1=Y 2=Z 3=A

5 or 6 axes (extended)

Slot 0=X 1=Y 2=Z

Slot 3 cycles A, B, C

via extended_axis_index

Touch mode (!ESP3D_HARDWARE_SWITCH_FEATURE): tapping the axis label advances through all real axes in a loop (X→Y→Z→A→…→X). The lock position is not part of the touch cycle — locking is managed by the main screen.

Hardware switch mode (ESP3D_HARDWARE_SWITCH_FEATURE): the physical 4-position switch drives axis selection. The axis label button is only clickable when axis_count > 4 and the switch is in slot 3, to cycle the extended axis (A/B/C).

Probe parameter values (offset, max_travel, retract, feed_rate) are stored per real axis index. initializeProbeAxisValues() only inserts missing entries — existing user edits are never overwritten.


LVGLprobeScreenUIManagerCaller (e.g. main_screen)LVGLprobeScreenUIManagerCaller (e.g. main_screen)Screen is active, user interactscreate()new GenericScreen + PanelComponentregisterScreen(probe)subscribe to ESP3DValues (14 indices)lv_obj_add_event_cb(DELETE, onScreenDestroy)User presses Back (onButton2Release)cleanup_timer triggers prepareForDestructionstop probe sequence if active (send feed-hold)cancel probe_timeout_timercancel position_flush_timerunsubscribe all 14 ESP3DValues indicestransition_timer calls createScreen(next_target)LV_EVENT_DELETE fires onScreenDestroyunregisterScreen(probe)delete GenericScreen instance

Destruction follows the standard ESP3D_TRANSITION_* macro pattern shared across all screens. prepareForDestruction() is guarded against double execution by cleanup_executed_ and is_prepared_for_destruction_ flags. If the probe sequence is active when destruction is triggered, a realtime feed-hold (!) is sent before unsubscribing.


Declared in main/display/cnc/fluidnc/screens/probe_screen.h.

FunctionSignatureDescription
createvoid create()Creates and loads the probe screen. Throws std::runtime_error on critical allocation failure (screen, PanelComponent).
setReturnScreenvoid setReturnScreen(ESP3DScreenType screen)Overrides the default return target (main screen). Must be called before create() if a custom caller needs to receive the result. Resets to default after each transition.
clearResultvoid clearResult()Resets last_result to ProbeResult::NONE. Should be called before navigating to the probe screen.
getResultProbeResult getResult()Returns the outcome of the last completed probe sequence.

Typical Caller Flow (e.g., change_tool_screen)

Section titled “Typical Caller Flow (e.g., change_tool_screen)”
probeScreenchange_tool_screenprobeScreenchange_tool_screenUser runs probe sequence to completionclearResult()setReturnScreen(change_tool)create()navigates back to change_toolgetResult()ProbeResult SUCCESS or FAILED or CANCELLED
DependencyRoleReference
GenericScreenHost screen container, rotation, VirtualButtonsComponentUI_Framework_Screens.md
PanelComponentEncoder-navigable list of the 4 probe parameter itemsUI_Framework_Screens.md
FirmwareStatusComponentOverlay showing FluidNC machine statefluidnc_module.md
ConnectionStatusComponentOverlay showing transport connection statefluidnc_module_connection_status.md
inputScreenNumeric InputEditor for editing probe parametersUI_Framework_Screens.md
ESP3DValuesObservable system — subscriptions drive all UI updatesCore_Platform_Infrastructure.md
ESP3DGCodeHandlerServiceSends G38.2 / G1 / G10 and realtime ? / ! commandsCNC_Firmware_Integration.md
UIManagerLock state (user + system), theme tokens, orientation angleUI_Framework_Screens.md
ESP3DTranslationServiceAll user-visible strings (step labels, error messages)Core_Platform_Infrastructure.md
esp_timerHigh-resolution monotonic clock for position throttle timestampsESP-IDF built-in

  • No std::string in hot-path formatting: probe parameter display values are formatted into fixed-size char buffer[32] with snprintf, avoiding heap allocation in high-frequency paths.
  • std::map for probe values: acceptable because the map is accessed only during user interaction (axis selection, parameter edits, sequence start), never inside a high-frequency subscription callback.
  • No throw from LVGL timer callbacks: deferred overlay creation uses new (std::nothrow) and logs errors rather than throwing; an uncaught exception inside an LVGL timer callback would invoke std::terminate and reset the board.
  • Axis letter resolution at call time: getAxisLetter() reads ESP3DValuesIndex::axis_names on each call (not cached) to correctly reflect grblHAL $376 UVW naming throughout the session without requiring a re-render on setting changes.
  • Focus persistence through InputEditor round-trip: saved_focus_item_id stores the item being edited before navigating to input_screen. On screen re-creation (return from InputEditor), panel_component->setFocusedItem(saved_focus_item_id) restores the visual highlight immediately.
  • Dual-callback state machine: firmware_status drives phase transitions (detecting Idle between steps), while probe_status confirms or denies physical contact within SEEK and FEED phases. Both callbacks are required; neither alone is sufficient to advance the sequence correctly.
  • Positions on axis change (a14): ESP3DValues only notifies on change, so a machine at rest never refreshes them; the screen reads the current values of the new axis instead of keeping the previous axis or 0000.000.
  • Position button badges (a14): the unit of the reported positions (mm / in, position_unit.h) top right, and the active WCS (the one G10 L20 P0 overwrites) bottom right, refreshed on each opening (on grbl / grblHAL the parser state is only queried at connection; $G is re-sent unless not connected).
  • Opened from the tool change screen (a14): the probe is set to Z and the status line tells the user what to do instead of Idle (nothing moves until Play).
  • Lock visual feedback: when UIManager::getLockState() is true, all interactive elements (parameter buttons, position labels, panel items) receive LV_OPA_30 and LV_STATE_DISABLED. The VirtualButtonsComponent re-renders with lock-aware styles. Locking is triggered by either the hardware switch position 3 (≤ 3-axis mode) or a non-connected transport (server_status != "C").