Skip to content

Status Screen

File: main/display/cnc/screens/status_screen.cpp
Namespace: statusScreen
Module group: cnc_shared → status_screen


The status screen is the primary operational view of the CNC pendant. It presents real-time machine state across three independent areas:

AreaContentControlled by
Positions6-axis MPos/WPos display + limit-pin indicatorsHardware switch (or touch title strip)
Actions5 rotating sub-views (overrides, rapid, GC status, spindle/coolant, job)Hardware potentiometer (or icon button row)
ButtonsPhysical/virtual buttons with two behavioural modesButton 1 (cycles Navigation ↔ Control)

All content is created once with lazy loading and toggled show/hide to minimise memory and LVGL object churn on constrained ESP32 hardware.


┌─────────────────────────────────────┐ ← ESP3D_CIRCULAR_MENU_DIAMETER
│ [FW Status] [Conn Status] │ ← deferred overlay components
│ ┌─────────────────────────────────┐│
│ │ MPos / WPos (title) ││ ← title strip (touch zone on touch-only)
│ ├─────────────────────────────────┤│
│ │ X -123.456 | -123.456 ││
│ │ Y 456.789 | 456.789 ││ ← Positions area (ESP3D_STATUS_PANEL_HEIGHT)
│ │ Z -12.345 | -12.345 ││
│ │ ■ ■ ■ (pin indicators) ││
│ ├─────────────────────────────────┤│
│ │ [pot/icon bar] ──────────────── ││ ← progress bar / view selector band
│ │ ┌──────────────────────────┐ ││
│ │ │ Action view (one of 5) │ ││ ← Actions area (lazy views)
│ │ └──────────────────────────┘ ││
│ └─────────────────────────────────┘│
│ [BTN0] [BTN1] [BTN2] │ ← Virtual buttons row
└─────────────────────────────────────┘

Controls what is displayed in the positions area. Driven by the hardware 4-position switch (positions 0–3), or by tapping the title strip in touch-only builds (cycles 0–2; lock is excluded from touch cycling).

ValueEnumSwitch posDisplay
none0—Initial sentinel; nothing shown yet
mpos_only10MPos only, large font
wpos_only21WPos only, large font
mpos_and_wpos32Both columns, medium font
locked43Renders as mpos_and_wpos + UI lock engaged

Cycles via Button 1 press/release.

ValueButton 0Button 1Button 2
navigationSET (toggle panel edit)SWITCH (cycle mode)BACK (→ main screen)
controlSTOP ($X)SWITCH (cycle mode)PAUSE (!) or RESUME (~) based on firmware_status

Controlled by potentiometer (0–100 % mapped in a repeating 50-unit cycle, 10 units per view). Touch-only builds use a row/column of 5 icon buttons instead.

ValueIndexPot rangeContent
overrides00–9 %Feed + Spindle override buttons + reset
rapid_gcodes110–19 %Rapid override (25 / 50 / 100 %)
gc_status220–29 %G5X / G2X button (G54 over G21, opens the GC state screen), Feed rate, Spindle speed
spindle_coolant330–39 %M3/M4/M5 (spindle) + M7/M8/M9 (coolant)
job_status440–49 %Job status label + job progress bar + planner buffer bar
none255—No view active (pre-init sentinel)

VariableTypeDescription
current_positions_modePositionsAreaModeActive positions display mode
current_buttons_modeButtonsAreaModeActive buttons behavioural mode
current_action_viewActionAreaViewActive action sub-view
axis_countuint32_tActive axis count (1–6, from ESP3DValuesIndex::axis_count)
potentiometer_valueint32_tLast known potentiometer value (0–100)
action_area_enabledboolWhether action items are enabled (false = locked or disconnected)
lock_state_on_entryboolUser lock state captured on screen entry; restored when leaving switch position 3
VariableTypeDescription
preset_action_view_int8_tPre-configured view to show on create() (−1 = potentiometer decides)
preset_buttons_mode_int8_tPre-configured buttons mode on create() (−1 = navigation)
potentiometer_shift_int32_tCalculated unit offset applied to potentiometer readings when a preset view is active
resume_action_view_int8_tView to restore when returning from an input/message-box screen; not reset by prepareForDestruction() so it survives the destroy/recreate cycle
VariableDescription
last_wpos_display_update_ms / last_mpos_display_update_msSeparate throttle timestamps for WPos and MPos groups (250 ms minimum)
wpos_update_pending / mpos_update_pendingFlush flags: set when a group is throttled; cleared by the flush timer
position_flush_timer300 ms one-shot timer that drains pending position updates
last_planner_display_update_msThrottle timestamp for planner buffer bar (500 ms minimum)
last_known_pin_states[8]Last seen pin-states string; skips LVGL style calls when unchanged

Holds the strings and range for the numeric input screen (feed rate, spindle speed). Using a struct avoids multiple heap-allocated std::string temporaries in hot paths.

struct InputScreenContext {
std::string title_buffer;
std::string value_buffer;
std::string unit_buffer;
int32_t min_value;
int32_t max_value;
};

Maps an ActionAreaView to the panel item IDs that belong to it, used by enableView() and enableAllActionItems() to batch enable/disable panel items without view-specific switch statements.

struct ViewItemMapping {
ActionAreaView view;
int32_t *item_ids;
size_t item_count;
};

Button 2 / msg-box / input

setInitialState() (optional)

create()

createScreenContainer()

createPositionTitles()

createContainersAndProgressBar()

createPanelComponent()

createPositionsContentsArea()

createActionAreaViews()

initializeEventHandlers()

performInitialSetup()

createDeferredComponents() ← 100 ms timer

Screen active

User interaction / value updates

Leave screen?

prepareForDestruction()

cleanup_timer_cb() → transition_timer_cb()

onScreenDestroy() - LV_EVENT_DELETE

delete components, reset statics

  1. Resets all transition-guard state.
  2. Creates GenericScreen (container, virtual buttons, screen object).
  3. Creates title labels (MPos / WPos / dual).
  4. Creates the positions area and action area containers plus the pot progress bar / icon row.
  5. Creates PanelComponent (focusable item navigation).
  6. Calls createPositionsContentsArea() — initialises label pointer arrays to nullptr; actual labels are lazy.
  7. Calls createActionAreaViews() — registers view containers but does not populate them; views are lazy.
  8. initializeEventHandlers() — registers switch, potentiometer, and encoder LVGL events; subscribes to all ESP3DValues.
  9. performInitialSetup() — reads initial axis count, potentiometer and switch positions, applies any preset_action_view_ / preset_buttons_mode_, calls showPositionsMode() and the first showActionView().
  10. createDeferredComponents() — schedules a 100 ms timer that creates FirmwareStatusComponent and ConnectionStatusComponent.
  11. Registers onScreenDestroy on LV_EVENT_DELETE.
  12. Applies display rotation.

Called once before the LVGL object tree is deleted. Guards via cleanup_executed_ / is_prepared_for_destruction_ flags.

  • Resets preset_action_view_, preset_buttons_mode_, potentiometer_shift_, last_active_override_item_id_.
  • Removes LVGL event callbacks from the active screen.
  • Calls prepareForDestruction() on PanelComponent, FirmwareStatusComponent, ConnectionStatusComponent, and GenericScreen.
  • Unsubscribes all ESP3DValues subscriptions.
  • Deletes position_flush_timer and deferred_components_timer if they are still live.

Emergency cleanup if prepareForDestruction() was not already called. Then:

  • Deletes panel_component, firmware_status_component, connection_status_component.
  • Unregisters the screen from UIManager.
  • Deletes status_screen_obj_instance.
  • Resets all static pointers and value variables to safe defaults.

Labels are not created at screen construction. createPositionsContentsArea() only zeros the pointer arrays. showPositionsMode() creates whichever label objects are needed for the requested mode on first call, then reuses them on subsequent calls.

No

Yes

No

Yes

showPositionsMode(mode)

Hide all existing labels

Update title strip

(single ↔ dual)

current_positions_mode = mode

For each active axis

Label exists?

lv_label_create()

applyLabelStyle()

Set pos + show

Pin indicator exists?

lv_obj_create()

applyPinIndicatorStyle()

Set pos + show

Next axis

ModeAxis letter labelValue labelExtra
mpos_only / wpos_onlyaxis_name_labels[i] (large font)position_axis_labels[i] (large font)—
mpos_and_wpos / lockedaxis_name_mpos_labels[i] (medium font)position_mpos_labels[i] + position_wpos_labels[i] (medium font each)WPos column at ESP3D_CIRCULAR_MENU_DIAMETER/2 + ESP3D_CONTAINER_PAD

showActionView() creates a view’s LVGL objects the first time it is requested, then hides/shows the container on subsequent switches.

Yes

No

No

Yes

Yes

No

showActionView(view)

view == current_action_view?

return (no-op)

enableView(old_view, false)

Hide old container

current_action_view = view

Container populated?

createViewContainer()

createXxxView()

enableView(new_view, true)

Show container

Panel: setFocusedItem()

action_area_enabled == false?

enableAllActionItems(false)

Done


Receives LV_EVENT_SWITCH_PRESSED with a control_event_t carrying btn_id (0–3).

btn_idPositionsAreaModeNotes
0mpos_only—
1wpos_only—
2mpos_and_wpos—
3lockedForces userSetLockState(true); restores lock_state_on_entry on exit

Lock/unlock transitions trigger updateActionAreaState() which enables or disables all panel items and virtual buttons.

Receives LV_EVENT_POTENTIOMETER_CHANGED with steps (0–100). Maps to a view via mapPotentiometerToView() (50-unit repeating cycle, 10 units/view). Calls showActionView() on view change; updates the progress bar always.

Touch-only potentiometer emulation: A row (portrait) or column (landscape) of 5 icon buttons (pot_view_selector) replaces the progress bar. Each button directly selects its corresponding view by synthesising a LV_EVENT_POTENTIOMETER_CHANGED event and suppressing intermediate beeps.

Forwards LV_EVENT_KEY to PanelComponent::handleEncoderEvent(). Calls updateMCodeButtonsState() after each step to keep M-code button colours in sync with focus position.

Touch-only encoder emulation: When encoder_emulation_active_ is true, Button 1 emits +1 step and Button 2 emits −1 step via VirtualButtonsComponent::emitEncoderStep(). The pot icon row is hidden during editing via ShowEncoderControls().

ModeRelease action
navigationToggle PanelComponent between PANEL_NAVIGATION_MODE and PANEL_EDITING_MODE; show/hide encoder controls
controlSend $X (alarm unlock)

Press/release: advance current_buttons_mode mod 2, call updateButtonsArea(). In touch encoder-emulation mode: press emits +1 encoder step.

ModeRelease action
navigationNavigate to RETURN_SCREEN (main) via ESP3D_TRANSITION_START macro
controlIf firmware_status starts with "HOLD": send GRBL_RT_CYCLE_START; otherwise send GRBL_RT_FEED_HOLD with high priority

PanelComponent delivers four callback types:

TypeMeaningTypical action
ON_FOCUSEncoder moved focus to this itemDeactivate previous item, return panel to NAVIGATION_MODE, call ShowEncoderControls(false)
ON_ACTIVEItem entered edit modeHighlight button, call ShowEncoderControls(true) (overrides, WCS) or send G-code immediately (rapid, M-codes, reset buttons)
ON_CHANGEEncoder step while in edit modeSend realtime G-code byte (feed/spindle override step, WCS preview update)
ON_STANDBYItem exited edit modeDeactivate button, call ShowEncoderControls(false), send final WCS command if changed

The WCS selector moved out of the status screen. The G5X / G2X button of the gc_status view (two rows: active WCS over work unit, for example G54 / G21, spanning the left column) opens the GC state screen (gcStateScreen, main/display/cnc/screens/gc_state_screen.cpp/.h, screen type gc_state). The button stays reachable even when the view is otherwise disabled, since the screen guards its own edits. UX flow: gc_state_screen.md.

GC state screen:

  • Shows the parser state report ([GC:…], parser_state value), read-only.
  • G5X button: edited like the old status WCS item: activate → encoder / arrows cycle G54…G59 (nextWcs(), wraps; an extended G59.x or unknown value restarts from G54 / G59) → deactivate sends it, only if it changed and is still allowed. The button shows the WCS it will send (G54 active → G55).
  • Unit button: switches the work unit and the report unit together, no confirmation: mm = G21 + $13=0 ($report_inches=false on FluidNC), in = G20 + $13=1 ($report_inches=true); the reference is the report unit, not G20/G21. The report unit is read back afterwards. The setting persists on grbl / grblHAL ($13 in EEPROM), only in RAM on FluidNC. The button shows the other report unit.
  • Both buttons are enabled only when the machine is Idle, connected (unless NO_CONNECTION_LOCK), unlocked and no job is running or paused (canChangeModalState(), shared with the jog screen WCS button).
  • sendModalCommand(cmd) sends the modal command, then asks the parser state again ($G) where the firmware does not push [GC:] itself (FluidNC does); on failure it gives an error beep and returns false. unitCode(parser_state) returns G20 or G21 (last match wins, default G21).

Small mm / in labels sit right after the MPos / WPos titles (updatePositionUnitLabels()): the unit of the positions in the status report (report_unit, from $13 / report_inches), not G20/G21. positionUnitText(rotary) (degrees for a rotary axis) and parserStateHasWord() are shared by the status, jog, probe and tool change screens, which also use it for the active WCS badge.


ActionCommandPriority
Feed override +10 %GRBL_RT_FEED_INC_10high
Feed override −10 %GRBL_RT_FEED_DEC_10high
Feed override resetGRBL_RT_FEED_100_PERCENTnormal
Spindle override +10 %GRBL_RT_SPINDLE_INC_10high
Spindle override −10 %GRBL_RT_SPINDLE_DEC_10high
Spindle override resetGRBL_RT_SPINDLE_100_PERCENThigh
Rapid 25 %GRBL_RT_RAPID_25_PERCENTnormal
Rapid 50 %GRBL_RT_RAPID_50_PERCENTnormal
Rapid 100 %GRBL_RT_RAPID_100_PERCENThigh
Spindle CWM3normal
Spindle CCWM4normal
Spindle offM5normal
Coolant mist toggleGRBL_RT_COOLANT_MIST_TOGGLEhigh
Coolant flood toggleGRBL_RT_COOLANT_FLOOD_TOGGLEhigh
Both coolant off (M9)mist toggle + flood togglehigh × 2
Status requestGRBL_RT_STATUS_REPORTnormal
Alarm unlock$Xnormal
Feed hold (pause)GRBL_RT_FEED_HOLDhigh
Cycle start (resume)GRBL_RT_CYCLE_STARTnormal
WCS selectG54…G59 (from the GC state screen)normal
Feed rateF<value>, typed and shown in the work unit; an unchanged value is not sent (a14)normal
Spindle speedS<value>normal
Parser state refresh$G after F / S (not on FluidNC, which pushes [GC:] on S / F changes)normal
STOPstop feeding the host stream, then soft reset 0x18 (flushes the planner)high

All subscriptions are registered in initializeEventHandlers() and removed in prepareForDestruction().

ESP3DValues subscriptions

axis_count

buffer_blocks

server_status

pin_states

position_mx/my/mz/ma/mb/mc

position_wx/wy/wz/wa/wb/wc

feed_override

rapid_override

spindle_override

feed_rate

spindle_speed

parser_state

job_status

job_progress

firmware_status

on_axis_count_update

on_planner_buffer_update

on_connection_status_update

on_pins_state_update

on_positions_update

on_overrides_update

on_parser_state_update

on_job_status_update

on_job_progress_update

on_firmware_status_update

CallbackKey behaviour
on_axis_count_updateClamps to 1–6; calls updatePositionsAreaMode() which re-runs showPositionsMode()
on_positions_updateThrottled: 250 ms per group (WPos/MPos tracked separately). Pending updates flushed by position_flush_timer. Only updates lv_label_set_text() on already-visible labels; layout changes deferred to mode-change paths.
on_pins_state_updateChange detection: compares against last_known_pin_states; skips all LVGL style operations if string is identical. Fires at up to 10 Hz from the CNC status poll; change detection prevents redundant lv_obj_set_style_* calls.
on_connection_status_updateMaps "C" → systemSetLockState(false); any other value → systemSetLockState(true). Calls updateActionAreaState().
on_overrides_updateRoutes by ESP3DValuesIndex: updates feed_override_item_id text (F: X%), spindle_override_item_id text (S: X%), rapid_override_label, feed_value_label, or spindle_value_label.
on_parser_state_updateExtracts WCS (extractWCS), feed (F value), spindle (S value); updates respective labels; calls updateMCodeButtonsState().
on_job_status_updateUpdates job_status_label text.
on_job_progress_updateUpdates job_progress_label text and job_progress_bar value with animation.
on_planner_buffer_updateThrottled: 500 ms. Sets planner_progress_bar value; calls updatePlannerProgressBarStyle() to colour bar green/orange/red (≥73 % → green, ≥40 % → orange, <40 % → red).
on_firmware_status_updateIf current_buttons_mode == control, calls updateButtonsArea() to switch Button 2 icon between PAUSE and RESUME.

Yes

No

No

Yes

No

Yes

updateActionAreaState()

ui_manager.getLockState()?

should_enable = false

server_status == 'C'?

(skipped if NO_CONNECTION_LOCK)

should_enable = true

action_area_enabled changed?

no-op

enableAllActionItems(should_enable)

panel_component->disableItem() for all view items

VirtualButtonsComponent: update buttons with enabled flag

lv_obj_add/clear_state(LV_STATE_DISABLED) on action buttons

The G5X / G2X button is always re-enabled after the batch disable: it only opens the GC state screen, whose edits are guarded there.

Contextual button rules (a14):

  • OK (SET): disabled in the job status view (no editable item there); edit mode is left first so the user can always exit it.
  • STOP: only enabled while the machine moves or holds, or a job is streaming (a host-streamed job can be between status reports while the firmware still reports IDLE); it also depends on the job status.
  • PAUSE / RESUME: feed hold in RUN / JOG, cycle start in HOLD.
  • Enabling only applies to the visible view: hidden views stay disabled, otherwise the encoder would navigate to invisible items. When disabling drops the panel focus, it is given back to the first enabled item of the visible view.
  • View → item id tables are reset per instance (stale ids could alias the next instance’s panel items).

Feed and spindle display (a14): F is typed and shown in the work unit (G20/G21), like the jog feedrates; the [GC:] F is converted for display when the firmware reports it in another unit (FluidNC always reports mm/min: F254 for F10 in G20). Validating an unchanged (possibly rounded) value sends nothing, so the programmed feed is never altered. r = real (status report FS:), s = set (parser state).

View and focus restore (a14): leaving for an input or message box remembers the current view and the focused item (item ids change on recreate); both are restored at the end of create().


View 3 (spindle_coolant) shows M3/M4/M5 and M7/M8/M9 buttons. Their background colour reflects the current machine state parsed from ESP3DValuesIndex::parser_state via esp3dGcodeHandler.parseMCodes().

StateBackgroundIcon colour
ActiveESP3D_ACCENT_ACTIVE_COLORtext_on_active token
InactiveESP3D_SCREEN_BACKGROUND_COLORtext_primary token

Default rules when parser state is ambiguous:

  • No spindle M-code reported → M5 shown as active.
  • No coolant M-code reported → M9 shown as active.
  • M7 and M8 can coexist; M9 excludes both.

updateMCodeButtonsState() is called from on_parser_state_update, encoder_event_handler, showActionView(), and enableAllActionItems().


pot_value (0–100)
↓
If preset_action_view_ != -1:
pot_value += potentiometer_shift_
pot_value = ((pot_value % 50) + 50) % 50 ← wrap into 0–49
↓
cycle_pos = pot_value % 50
0– 9 → overrides
10–19 → rapid_gcodes
20–29 → gc_status
30–39 → spindle_coolant
40–49 → job_status

potentiometer_shift_ is computed in performInitialSetup() as:

potentiometer_shift_ = (preset_action_view_ - natural_view) * 10

so that the potentiometer’s physical resting position maps to the preset view without requiring the user to move the knob.


Screen transitions follow the project-wide ESP3D_TRANSITION_START / ESP3D_CLEANUP_TIMER_BODY / ESP3D_TRANSITION_TIMER_BODY macro chain (see screen_transitions_flow.md):

  1. cleanup_timer_cb fires after ESP3D_TRANSITION_SCREEN_DELAY_MS: calls prepareForDestruction(), schedules transition_timer.
  2. transition_timer_cb fires after the second delay: calls createScreen() on the target screen.
  3. onScreenDestroy fires when LVGL destroys the old screen object tree.

The is_prepared_for_destruction_ and cleanup_executed_ guards prevent double-cleanup if the user triggers a second transition before the first timer fires.

When the screen leaves to an input or message-box screen, resume_action_view_ is set to current_action_view before calling prepareForDestruction(). On recreation, performInitialSetup() reads resume_action_view_ and converts it to preset_action_view_ so the same view is restored automatically.


createDeferredComponents() schedules a one-shot lv_timer at ESP3D_DEFERED_SCREEN_CREATION_DELAY_MS (100 ms) to create:

The timer handle is stored in deferred_components_timer so prepareForDestruction() can cancel it if the screen is destroyed before it fires (e.g. rapid navigation).


setInitialState(ActionView view, ButtonsMode buttons_mode)

Section titled “setInitialState(ActionView view, ButtonsMode buttons_mode)”

Pre-configures the view and buttons mode that create() will apply. Must be called before create(). Pass −1 to either parameter to use defaults (potentiometer-driven view, navigation mode).

ParameterTypeDefaultDescription
viewActionView (maps to ActionAreaView)−1View to show on screen entry
buttons_modeButtonsMode (maps to ButtonsAreaMode)−1Buttons mode on entry

Called by main_screen.cpp when launching the status screen from a specific context (e.g. a running job → job_status view in control mode).

Creates and activates the status screen. Throws std::runtime_error on critical allocation failures (screen container, panel component). Non-critical failures (firmware/connection status components) are logged but do not abort creation.


axisLetterAt(uint32_t i) resolves the display letter for axis index i:

  1. Reads ESP3DValuesIndex::axis_names (populated from grblHAL $376 UVW response).
  2. Falls back to "XYZABC" if unset (FluidNC, grblHAL ≤3 axes).

Pin state matching in updatePinsStates() also uses axisLetterAt() to correctly match grblHAL |Pn: reports against renamed UVW axes.


Feature flagWhen OFFWhen ON
ESP3D_HARDWARE_ENCODER_FEATURENo physical encoder; encoder emulation via Button 1/2 when editingPhysical encoder events routed through encoder_event_handler
ESP3D_HARDWARE_POTENTIOMETER_FEATURENo physical potentiometer; 5 icon buttons replace progress bar for view selectionPotentiometer steps drives mapPotentiometerToView() + progress bar
ESP3D_HARDWARE_SWITCH_FEATURENo physical switch; title strip becomes a touch button cycling mpos_only → wpos_only → mpos_and_wpos4-position switch drives PositionsAreaMode directly
ESP3D_NO_CONNECTION_LOCK_FEATUREon_connection_status_update locks the action area when disconnectedAction area state is independent of connection status

Following the project-wide LVGL constraints (single-threaded on Core 1):

TechniqueWhere applied
Throttled callbackson_positions_update (250 ms, separate WPos/MPos timestamps); on_planner_buffer_update (500 ms)
Flush timerposition_flush_timer (300 ms one-shot) drains pending position updates that were throttled
Change detectionon_pins_state_update — skips all lv_obj_set_style_* calls when pin string is identical
Lazy object creationPositions labels and action view containers created on first use only
Show/hide instead of create/deleteAll position label sets and all 5 view containers are hidden, not destroyed, on mode/view change
Deferred heavy componentsFirmwareStatusComponent and ConnectionStatusComponent created 100 ms after screen creation
Sound suppressionESP3D_SAVE_SOUND_STATE_AND_DISABLE wraps view-switch sequences that would otherwise trigger multiple overlapping beeps

ComponentRoleReference
GenericScreenBase screen container + virtual buttonsscreen_base_infrastructure.md
PanelComponentFocusable item navigation + encoder dispatchingscreens_architecture.md
FirmwareStatusComponentTop-left state overlayfirmware_status.md
ConnectionStatusComponentTop-right connection overlaygrbl_module_connection_status.md
VirtualButtonsComponentPhysical/virtual button management + switch/pot emulationInput_system.md
inputScreenNumeric/text input modal (feed rate, spindle speed)screens_architecture.md
messageBoxScreenInformation/confirmation modal (GC info)screens_architecture.md
ESP3DValues / esp3dXValuesObservable system — all CNC stateconnection_management.md
esp3dGcodeHandlerG-code send + parser helpers (parseMCodes, extractValue, extractWCS)gcode_host_architecture.md
UIManager / ui_managerTheme tokens, lock state, screen registrationscreens_architecture.md
ESP3DTranslationServiceLabel translationscreens_architecture.md