Skip to content

grbl_module_change_tool

The grbl_module_change_tool module implements the Tool Change screen for the grbl CNC firmware target. It provides a complete user workflow for managing tool changes on a grbl-controlled CNC machine, supporting two distinct operation modes: a physical CHANGE (M6T) that pauses the firmware to let the operator physically swap the cutting tool, and a logical SET (M61Q) that updates the active tool number in firmware state without moving the machine.

The screen is LVGL-driven, runs on Core 1, and follows the same safe timer-based transition and lifecycle patterns used across the rest of the pendant UI. It subscribes to the ESP3DValues observable system for firmware state, parser state, and live machine positions; all updates are validated for change before touching LVGL objects.

Source file: main/display/cnc/grbl/screens/change_tool_screen.cpp
Screen type enum: ESP3DScreenType::change_tool


createScreen()

navigate to

setReturnScreen / result

navigate back

optional overlay

optional overlay

GCode commands

subscriptions

grbl_module_screen_router

esp3d_screen_type.cpp

grbl_module_change_tool

change_tool_screen.cpp

grbl_module_probe

probe_screen.cpp

main_screen

grbl_module_connection_status

ConnectionStatusComponent

cnc_shared / firmware_status

FirmwareStatusComponent

CNC_Firmware_Integration

esp3d_gcode_host_service

Core_Platform / ESP3DValues

The screen is registered in the grbl screen router (grbl_module_screen_router) alongside all other grbl-target screens. When the router receives ESP3DScreenType::change_tool, it delegates to changeToolScreen::create().


grbl_module_change_tool

changeToolScreen namespace

ToolChangeState enum

ToolChangeMode enum

PanelComponent

tool selector header

Position Display

WPos / MPos

Status Bar

FirmwareStatusComponent overlay

ConnectionStatusComponent overlay

ComponentRole
ToolChangeStateInternal state machine — drives UI label, button labels, and allowed actions
ToolChangeModeSelects the GCode to send — CHANGE (M6 T<n>) or SET (M61 Q<n>)
PanelComponentInteractive header showing Mode and Target tool fields; encoder navigates/edits
Position displayTwo-row live display of Work-Position (WPos) and Machine-Position (MPos) per axis
Status barSingle-line text area reflecting the current state message
FirmwareStatusComponentOptional overlay — shows firmware alarm/busy state; shared across CNC screens
ConnectionStatusComponentOptional overlay — shows transport link health; tappable to open connection status screen

The tool change lifecycle is modelled as an explicit state machine. The current state drives which actions the centre button exposes (Start / Resume / Cancel) and whether the panel and back button are accessible.

create() - firmware IDLE

Button1 CHANGE mode\nM6 T target sent

Button1 SET mode\nM61 Q target sent

firmware status HOLD\ntool change pause

ALARM or timeout

Button1 RESUME\nGRBL_RT_CYCLE_START sent

Button1 CANCEL\nfeed-hold + reset

firmware status IDLE\ncurrent_tool = target_tool

ALARM or timeout

firmware recovers to IDLE

firmware recovers to IDLE\nHOLD to IDLE or error to IDLE

user edits next target\nor re-enters screen

IDLE

MOVING_TO_CHANGE

SUCCESS

WAITING_USER

FAILED

PROBING

CANCELLED

StateMeaningCentre Button Label
IDLEReady to startStart
MOVING_TO_CHANGEM6 command sent; firmware moving to change positionCancel
WAITING_USERFirmware halted (HOLD); operator swaps tool manuallyResume
PROBINGFirmware probing tool length after resumeCancel
SUCCESSTool change confirmed; current_tool updatedStart (next change)
FAILEDError, alarm, timeout, or same-tool detectedStart (retry)
CANCELLEDUser cancelled; firmware being reset(disabled)

SET mode (M61)

Select target T n

Press Start

Send M61 Q n

Firmware updates

current tool - no move

Optional: probe prompt

CHANGE mode (M6)

Select target T n

Press Start

Send M6 T n

Firmware moves to

tool change position

Firmware HOLD

Swap tool physically

Press Resume

Firmware probes

tool length

Success - T n active

The active mode is toggled by pressing OK (Button 0) while the Mode panel item is focused.


The screen uses three hardware/virtual buttons mapped to GenericScreen:

ButtonPhysical IDNormal labelContext-sensitive behaviour
OKButton 0OK iconToggle Mode field OR toggle encoder editing on Target field
Change / ActionButton 1Change iconStart / Resume / Cancel depending on current_state
BackButton 2Back iconNavigate to return screen (blocked during active operations and probe decision)
current_stateButton 1 actionGCode / command sent
IDLE + CHANGE modeStart tool changeM6 T<target>
IDLE + SET modeSet current toolM61 Q<target>
WAITING_USERResume (cycle start)GRBL_RT_CYCLE_START (realtime, high priority)
MOVING_TO_CHANGE or PROBINGCancelfeed-hold + reset sequence

parser_state

firmware_status

position_wx/wy/wz

position_mx/my/mz

server_status

ESP3DValues

observable

on_parser_state_update

extracts current tool T number

on_firmware_status_update

drives state transitions + alarm/recovery

on_positions_update

buffered via position_flush_timer

on_connection_status_update

logged and delegated to overlay

changeToolScreen

internal state

Position values arrive at high frequency from the firmware. A dedicated LVGL timer (position_flush_timer) coalesces updates and writes to the display at most every POSITION_DISPLAY_MIN_INTERVAL_MS + 50 ms. Pending flags (wpos_update_pending, mpos_update_pending) track dirty state between ticks.

Position Displayposition_flush_timeron_positions_updateESP3DValuesFirmwarePosition Displayposition_flush_timeron_positions_updateESP3DValuesFirmwareposition updatecallbackstore value, set pending flagevery interval ms - flush if pending

on_firmware_status_update Transition Logic

Section titled “on_firmware_status_update Transition Logic”

This is the primary driver of the state machine. It handles four categories of event:

  1. MOVING_TO_CHANGE → HOLD detected → transition to WAITING_USER (operator must swap tool)
  2. WAITING_USER → RUN detected → transition to PROBING (firmware is now probing)
  3. PROBING → IDLE detected → transition to SUCCESS; current_tool set to target_tool
  4. ALARM detected (any non-editable state) → revert tool in firmware; transition to FAILED
  5. HOLD→IDLE or error→IDLE recovery → transition back to IDLE or FAILED (same-tool guard)

GenericScreen / LVGLESP3DValues subscriptionschangeToolScreen::create()createScreen()GenericScreen / LVGLESP3DValues subscriptionschangeToolScreen::create()createScreen()Screen is now live on Core 1create()disable sound, reset transition statenew GenericScreen (3 buttons)createToolSelectorHeader()createPositionDisplay()createStatusBar()new FirmwareStatusComponent (std::nothrow)new ConnectionStatusComponent (std::nothrow)subscribe parser_state, firmware_status, 6x positions, server_statussetToolChangeState(IDLE or FAILED)handleProbeScreenReturn()restore soundonScreenDestroy (LV_EVENT_DELETE)prepareForDestruction()unsubscribe alldelete panel, overlaysui_manager.unregisterScreen()delete screen_instance

The module uses the project-standard ESP3D_TRANSITION_* macros to guard against double-free and premature destruction:

  • is_prepared_for_destruction — set before any navigation starts; suppresses subscription callbacks.
  • cleanup_timer + transition_timer — two-phase LVGL timer sequence: cleanup_timer_cb finalises teardown, then transition_timer_cb calls createScreen() for the next screen.
  • Back navigation is blocked while current_state is MOVING_TO_CHANGE, WAITING_USER, or PROBING, and while awaiting_probe_decision is true.

When a SET (M61Q) operation succeeds and an optional probe step is desired, the screen presents an in-screen prompt offering two choices:

  • Probe — navigates to grbl_module_probe with the change tool screen set as the return target.
  • Ignore — dismisses the prompt and stays on the SUCCESS state.
probeScreenProbe prompt overlaychangeToolScreenprobeScreenProbe prompt overlaychangeToolScreenshowProbePrompt(true)onIgnoreButtonClickSUCCESS state - "current tool set to TX"onProbeButtonClickprobeScreen::setReturnScreen(change_tool)probeScreen::clearResult()prepareForDestruction()createScreen(ESP3DScreenType::probe)returns to change_tool screenhandleProbeScreenReturn()read probeScreen::getResult()

The header area is an interactive PanelComponent with two items:

Panel ItemIDContentEncoder interaction
ModeITEM_ID_MODECHANGE or SET textNot encoder-editable; toggled with OK button
TargetITEM_ID_TARGETT<n> target tool numberEncoder increments/decrements value in editing mode

The encoder transitions between navigation mode (focus moves between items) and editing mode (encoder changes the focused item’s value) via panel_component->enableEncoderFor(). The target tool value is persisted in saved_target_tool across screen re-creation (e.g., returning from the probe screen).


A watchdog timeout_timer is started when MOVING_TO_CHANGE begins. If firmware does not reach HOLD within the configured timeout:

FirmwareGCodeHostchangeToolScreentimeout_timerFirmwareGCodeHostchangeToolScreentimeout_timeralt[firmware already IDLE][firmware still busy]timeoutCallback()send feed-hold + resetrealtime commandsetToolChangeState(FAILED, "timeout")pending_timeout_revert = trueon_firmware_status_update("IDLE")setToolChangeState(FAILED, "timeout")

The pending_timeout_revert flag is a deferred-failure mechanism: if the firmware is not yet IDLE when the timeout fires, the FAILED state is applied on the next IDLE transition instead, avoiding a race between the timer and the firmware status callback.


on_parser_state_update parses the grbl $G/$I style parser state string to extract the active tool number (T<n>), keeping current_tool in sync with what the firmware reports. Updates are applied only when current_state is an editable state (IDLE, SUCCESS, FAILED, CANCELLED) to avoid overwriting an in-progress operation.


DependencyModule referenceUsed for
GenericScreencommon_screensScreen container + 3-button virtual button bar
PanelComponentui_componentsInteractive mode/target selector with encoder support
FirmwareStatusComponentcnc_sharedOptional firmware alarm overlay
ConnectionStatusComponentgrbl_module_connection_statusOptional transport health overlay
probeScreengrbl_module_probeOptional post-SET probe workflow
ESP3DValues / esp3dXValuesvaluesObservable system for firmware state, positions, connection
ESP3DGCodeHostServicegcode_hostSending M6, M61, real-time cycle-start, feed-hold commands
ESP3DTranslationServicetranslationsAll user-visible status messages
UIManager / ui_managerui_coreScreen registration, orientation angle
createScreen() routergrbl_module_screen_routerScreen transitions to/from change_tool

The grbl change tool screen is one of three parallel implementations sharing the same UX design:

change_tool_screen UX pattern

grbl_module_change_tool

M6 T / M61 Q - grbl dialect

grblHAL version

grblHAL dialect

fluidnc_module_change_tool

FluidNC dialect

All three implement identical state machines and UX flows. The grbl variant uses grbl-specific GCode commands (M6 T<n>, M61 Q<n>, GRBL_RT_CYCLE_START) and parses grbl’s status response format. See fluidnc_module_change_tool for the FluidNC equivalent.


  • All UI operations (object creation, event callbacks, timer callbacks) run on Core 1 in the LVGL task context — never from communication or GCode host tasks.
  • The position_flush_timer coalesces high-frequency position updates so the LVGL task is not overloaded by direct lv_label_set_text calls on every firmware status poll.
  • Screen destruction is deferred via cleanup_timer / transition_timer to avoid deleting LVGL objects inside event callbacks.
  • All heap allocations for overlay components use std::nothrow; allocation failure downgrades the overlay to absent rather than crashing the firmware.

  • screen_instance (GenericScreen*), panel_component, firmware_status_component, and connection_status_component are heap-allocated with new (std::nothrow).
  • last_firmware_state is a std::string used only in non-critical update paths; not on hot paths.
  • Static state variables (current_tool, target_tool, saved_target_tool, returning_from_probe, etc.) are module-scoped statics. They persist across screen re-creation within the same firmware session so that navigating away to the probe screen and returning does not lose the user’s tool selection or operation context.
  • Cancel while moving. Cancel during MOVING_TO_CHANGE / PROBING sends a feed hold and requestResetBeforeResume(): the next resume becomes a soft reset, so the cancelled sequence can never restart (same as probe STOP).
  • No timeout while the user works. No timeout runs while the screen waits for the user to swap the tool (WAITING_USER); a fresh TOOL_CHANGE_TIMEOUT_MS (60 s) starts when the sequence goes on. A pending timeout ends on IDLE only: any other change (for example HOLD → RUN after a late Resume) is not the end of the sequence; an ALARM ends it through the alarm handling.
  • Error display. Errors get a warning icon under the message (error color); the message has a fixed width and scrolls instead of going edge to edge. Error values are matched case-insensitively (ERROR:<code> / Error:…).
  • Probe from the tool change. “User stopped the probe” and “backed out without probing” are reported like “Ignore”: M61Q already succeeded, nothing was cancelled. The probe screen opened from here measures Z and shows a hint instead of Idle.
  • Sound mute guard. SoundMuteGuard (RAII) restores the saved mute state on every exit path of create() (normal return, early return, exception), so the buzzer can never stay muted until reboot.
  • Small mm / in unit after the MPos / WPos titles (unit of the reported positions).