Jog Screen
Section titled “Jog Screen”Introduction
Section titled “Introduction”The jog screen (main/display/cnc/screens/jog_screen.cpp, namespace jogScreen) is the primary CNC manual motion control interface. It provides real-time axis position display, step-based and continuous jogging, homing, and work-coordinate zeroing — all driven by a rotary encoder, a 4-position hardware switch (or touch), and three virtual buttons.
The screen is shared across all supported CNC firmware targets (FluidNC, grbl, grblHAL). It lives in the cnc_shared module group and is reachable from the main screen. It returns to ESP3DScreenType::main on Back.
Architecture Overview
Section titled “Architecture Overview”Screen Layout
Section titled “Screen Layout”┌──────────────────────────────────────────────┐│ [ConnStatus] [FirmwareStatus] ││ ││ ┌──────┐ ││ │ X │ ← Axis chip/button ││ └──────┘ ││ ┌──────────────────────────────────────┐ ││ │ 123.456 │ │ ← WPos (big label)│ │ MPos:125.000 │ │ ← MPos (small label)│ └──────────────────────────────────────┘ ││ ┌─────────────┐ ┌─────────────────────┐ ││ │ 1.000mm │ │ 3000mm/min │ │ ← Step / Feedrate buttons│ └─────────────┘ └─────────────────────┘ ││ ████████████████████████████░░░░░░░░░░░░░ │ ← Planner buffer bar│ ││ [X][Y][Z] [A][B][C] │ ← Limit/probe pin indicators│ ││ [home] [homeAll] [zero] │ ← Home / Zero buttons│──────────────────────────────────────────────││ [OK/Jog-] [mode] [Back/Jog+] │ ← Virtual buttons└──────────────────────────────────────────────┘UI Elements
Section titled “UI Elements”| Element | LVGL object | Description |
|---|---|---|
| Axis chip | btnAxe + axe_label | Shows current axis letter (X/Y/Z/A/B/C) or lock icon; tappable to cycle axis (touch-only) or cycle extended axes (hardware switch + >4 axes) |
| Position button | position_btn | Contains WPos big label and MPos small label; is also the jog panel item. a14: unit badge (mm / in of the reported positions, degrees for a rotary axis) top right and active WCS badge (G54…, --- while unknown) bottom right; the button is widened for the badges and slides while the custom edit button is shown |
| Step button | step_btn | Cycles through preset step sizes + Custom slot; is the step panel item |
| Feedrate button | feedrate_btn | Cycles through preset feedrates + Custom slot; is the feedrate panel item |
| Planner bar | planner_progress_bar | Colour-coded bar showing free planner buffer (green/yellow/red) |
| Pin indicators | pins_labels[0..5] | Square chips for X/Y/Z (linear row) and A/B/C (rotary row); hidden unless that pin is active |
| Home button | home_btn | Homes the current single axis ($H{axis}); hidden when single-axis homing is disabled |
| Home All button | home_all_btn | Homes all axes ($H); hidden when no axis has a homing cycle |
| Zero button | zero_btn | Zeroes WPos for the current axis with G10 L20 P0 {axis}0 (a14, was G92): the zero is set in the active WCS and stored by the controller; G92 is a temporary offset added to every WCS, lost at power off on grbl. Same as the probe screen, FluidDial and ESP3D-WebUI |
| WCS button (a14) | — | Each press activates the next work coordinate system (G54 → G59 → G54), sent at once like Home / Zero. Its text is the WCS the next press sends; disabled unless idle, connected, unlocked and no job running (gcStateScreen::canChangeModalState()); a WCS sent but not yet reported by [GC:] lets a fast second press continue from it |
| Stop Jog button (a14) | — | Replaces Home / Home All / WCS / Zero while the machine state is Jog or Home (shown even when locked). In Jog: purge the queued jog lines, then a single realtime 0x85. In Home: soft reset (homing ignores jog cancel and feed hold), same sequence as the status screen STOP; the icon is then a stop |
| Edit indicator | edit_btn | Small edit symbol; appears above Step or Feedrate when on the Custom slot in editing mode; tapping or pressing OK launches the numeric input |
| Virtual buttons | VirtualButtonsComponent | 3 buttons whose meaning changes with JogUIMode |
Jog UI Modes
Section titled “Jog UI Modes”The centre virtual button (Button 1) cycles through three modes on each release. The mode governs what the left (Button 0) and right (Button 2) virtual buttons do.
Mode Mapping
Section titled “Mode Mapping”| Mode | Button 0 | Button 1 (centre) | Button 2 |
|---|---|---|---|
| NAVIGATION | OK — toggle encoder editing mode; or enter jog mode when jog item is focused | Mode cycle | Back |
| JOG_SEQUENTIAL | Jog − (single step on press) | Mode cycle | Jog + (single step on press) |
| JOG_CONTINUOUS | Jog − (large distance on hold; stops on release) | Mode cycle | Jog + (large distance on hold; stops on release) |
LVGL constraint: Button press/release callbacks run on Core 1 inside the LVGL task.
startContinuousJog()andstopContinuousJog()send GCode viaesp3dGcodeHandlerand must never block the LVGL thread. See screens_architecture.md for the threading model.
Axis Management
Section titled “Axis Management”Axis Count and Selection
Section titled “Axis Count and Selection”The screen supports 3 to 6 axes. A 4-position hardware switch (positions 0–3) selects the active axis. Position 3 is special:
axis_count <= 3: position 3 = lock state — toggles the user lock; the axis chip displays a lock icon.axis_count = 4: position 3 = the 4th real axis (A, or U when grblHAL$376renames it).axis_count >= 5: position 3 = extended axis slot that cycles through A/B/C via axis chip tap.
Touch-only boards (
!ESP3D_HARDWARE_SWITCH_FEATURE): tapping the axis chip cycles through all real axes in ordinal order (X→Y→Z→A→B→C→X). The lock position is excluded from the touch cycle.
Axis Naming
Section titled “Axis Naming”axisLetter(real_idx) reads ESP3DValuesIndex::axis_names from the values system. This string is populated by grblHAL when $376 renames axes (e.g. "XYZUVW"). FluidNC and standard grbl fall back to "XYZABC". Both the displayed axis chip and the jog GCode command letter use the same source, so the letter is always firmware-accurate.
Step and Feedrate Management
Section titled “Step and Feedrate Management”AxisValues Struct
Section titled “AxisValues Struct”Each real axis (indices 0–5) has an AxisValues entry in axis_values_map:
struct AxisValues { std::vector<std::string> step_values; // preset list from settings (e.g. "0.01;0.1;1;10") std::vector<std::string> feedrate_values; // preset list from settings size_t current_step_index; // 0..step_values.size() (last = Custom slot) size_t current_feedrate_index; std::string step_unit; // "mm" / "in" / "deg" std::string feedrate_unit; // "mm/min" / "in/min" / "deg/min" std::string custom_step_value; // session-only typed override; never persisted std::string custom_feedrate_value;};Settings Source
Section titled “Settings Source”| Axis | Step presets (string) | Feedrate presets (string) | Saved step idx (byte) | Saved feedrate idx (byte) |
|---|---|---|---|---|
| X | esp3d_x_jog_steps | esp3d_x_jog_feedrates | esp3d_x_jog_step_idx | esp3d_x_jog_feedrate_idx |
| Y | esp3d_y_jog_steps | esp3d_y_jog_feedrates | esp3d_y_jog_step_idx | esp3d_y_jog_feedrate_idx |
| Z | esp3d_z_jog_steps | esp3d_z_jog_feedrates | esp3d_z_jog_step_idx | esp3d_z_jog_feedrate_idx |
| A | esp3d_a_jog_steps | esp3d_a_jog_feedrates | esp3d_a_jog_step_idx | esp3d_a_jog_feedrate_idx |
| B | esp3d_b_jog_steps | esp3d_b_jog_feedrates | esp3d_b_jog_step_idx | esp3d_b_jog_feedrate_idx |
| C | esp3d_c_jog_steps | esp3d_c_jog_feedrates | esp3d_c_jog_step_idx | esp3d_c_jog_feedrate_idx |
Preset strings are semicolon-separated. Since a14 the linear axes lists hold one section per work unit ("mm;0.01;0.1;1;10;50;100;in;0.001;0.01;0.1;1;2;4"), and the screen uses the section of the active jog unit as is, with no conversion (see jog_unit_lists.md); rotary axes use a plain list in degrees. The lists are reloaded when the jog unit changes (indices restored from their saved settings, then clamped). The selected index byte is persisted to NVS on every encoder step. Indices are validated at load time and reset to 0 if out of bounds, handling preset list changes across firmware upgrades cleanly. The axis_values_map is not cleared across screen transitions unless axis_count changes, preserving user-selected indices when navigating back to the jog screen.
Custom Slot
Section titled “Custom Slot”The cycler includes one extra slot past the presets (index == step_values.size()). When selected:
- The edit indicator (
EDIT) appears above the active Step or Feedrate button. - Pressing OK, or tapping the indicator, opens the input_screen.md for floating-point input.
- The typed value is stored in
custom_step_value/custom_feedrate_value— session-only, never written to NVS. - If no value has been typed yet, the first preset is used as the actual jog value so the machine never receives an empty command string.
Jog unit (a14): for linear axes, the work unit G20/G21 (current_unit), or the report unit ($13 / report_inches, report_unit) when esp3d_jog_position_unit is on (read at each screen creation; the settings screen recreates the jog screen on return). Rotary axes always use degrees / degrees per minute. A change of the work unit (or of the report unit in position-unit mode) reloads the lists and labels, so the displayed unit always matches what the firmware applies (a 25.4× mistake otherwise).
Jog Command Generation
Section titled “Jog Command Generation”Single-step Jog (executeJog)
Section titled “Single-step Jog (executeJog)”Builds and sends:
$J=G91[unit]{axis}{sign}{step}F{feedrate}[unit] (jogUnitWord(), non-modal inside a $J): rotary axis → always G21 (degrees are never scaled, while a modal G20 could scale them on a firmware treating every axis as linear); linear axis → none in classic mode (the modal G20/G21 applies), the report unit word in position-unit mode.
Before sending, the planner buffer (ESP3DValuesIndex::buffer_blocks) is compared against ESP3D_PLANNER_BUFFER_SIZE_THRESHOLD = 5. If available blocks are at or below this threshold the command is silently dropped to avoid overflowing the firmware’s motion planner.
Continuous Jog (startContinuousJog / stopContinuousJog)
Section titled “Continuous Jog (startContinuousJog / stopContinuousJog)”Sends a single $J=G91 with a very large distance so the machine jogs until explicitly cancelled:
The 20 ms delay before sending the large-distance command gives the firmware time to process the pre-start cancel and clear its internal buffer.
Position Display
Section titled “Position Display”WPos (work position) drives the large label. MPos (machine position) drives the MPos:... small label. They have independent 250 ms timestamps and pending flags because handle() dispatches all position callbacks from a single status report in the same LVGL tick — a shared timestamp would throttle the second group whenever the first passed through.
The 300 ms flush timer runs continuously and only calls updatePositionDisplay() when either pending flag is set. This guarantees the display converges to the correct value even when motion has stopped and no further status reports arrive.
Planner Buffer Bar
Section titled “Planner Buffer Bar”The bar maps available buffer blocks to 0–100 %. Colour thresholds are computed from planner_buffer_max_size (read from ESP3DValuesIndex::planner_blocks at creation):
| Available blocks | Bar colour | Design token |
|---|---|---|
| 0 – 5 | Red | ESP3D_ACCENT_ALERT_COLOR |
6 – (5 + max/2) | Yellow | ESP3D_ACCENT_ACTION_COLOR |
| Above yellow threshold | Green | ESP3D_ACCENT_SELECT_COLOR |
The red threshold is fixed at 5 to match ESP3D_PLANNER_BUFFER_SIZE_THRESHOLD. The yellow/green boundary divides the remaining range equally so the bar is visually proportional regardless of the firmware’s planner size.
Lock System
Section titled “Lock System”The screen participates in two independent lock dimensions managed by UIManager:
| Lock type | Trigger | UIManager call | Visual effect |
|---|---|---|---|
| User lock | Switch to position 3 when axis_count <= 3 | userSetLockState(true/false) | Lock icon on axis chip; lock beep |
| System lock | server_status != "C" or canSendData() == false | systemSetLockState(true/false) | All jog/home/zero disabled; labels dimmed |
Both combine in ui_manager.getLockState(). When locked:
- All interactive buttons are visually disabled (
LV_STATE_DISABLED). - Position labels are dimmed using the
text_disabledopacity token. - The jog command path aborts before sending.
updateAxisDisplay()applies change detection before any LVGL calls to avoid redundant work on every status report.
on_connection_status_update schedules a 1-second deferred updateAxisDisplay() via axis_refresh_timer to batch rapid consecutive status changes and avoid LVGL thread saturation during reconnection.
Homing Buttons
Section titled “Homing Buttons”Visibility is evaluated on every updateAxisDisplay() call using three helpers that read from ESP3DValues:
| Helper | Values index read | Returns true when |
|---|---|---|
canHomeAxis(idx) | homing_cycle_{x|y|z|a|b|c} | Value is "1" |
canHomeSingleAxis(idx) | homing_single_{x|y|z|a|b|c} | Value is "1" |
anyAxisCanHome() | Iterates all active axes via canHomeAxis | At least one axis returns true |
Button layout adapts dynamically to the visible combination:
Since a14 the bottom row holds 1 to 4 buttons, left to right Home, Home All, WCS, Zero (Zero keeps its right slot): 1 = centre, 2 = left / right, 3 = left / centre / right, 4 = the free width split in 5 equal gaps (edges included: the slot margins do not leave room for 4 buttons on a 240 px wide area). The button size is defined per resolution where the default does not fit 4 buttons (res_320_240: 45 px). updateBottomButtons() has its own change detection, so it is called on every update that can change them: the homing capabilities ($/axes/*/homing/* arrive seconds after connection, possibly after the screen was built) and the machine state. While jogging or homing only Stop Jog is shown (centred).
Handwheel Jog (a14)
Section titled “Handwheel Jog (a14)”Hardware encoder rotated while the jog item is active (handwheel_jog.md):
- The LVGL encoder events are only the permission gate (
handwheelOnGateEvent): screen awake, jog item active, mode allows it. The detents actually moved are read from the encoder driver (absolute position sampled every 10 ms, nothing lost, see physical_input.md). - Sensitivity 0 (classic,
esp3d_jog_wheel_sensitivity, read at screen entry): one$Jper batch, distance = detents × step, at the selected feedrate. Every detent is kept even when the machine cannot keep up, and stopping the wheel cancels nothing: 1 detent = 1 full step. - Sensitivity 1–100: the feedrate follows the wheel (wheel speed × step, capped at the selected feedrate); above a speed threshold (log-interpolated between sensitivity 1 and 100, with hysteresis and confirmation; calibrated 2026-10-19: slow 10–20, normal 20–80, fast 290–700 detents/s) a detent is worth the next step of the axis list; the distance is capped to what the machine can follow (estimated backlog) and detents beyond are dropped. Nothing is left queued, so stopping the wheel stops the axis without a jog cancel.
- Reversal (any sensitivity): jog cancel
0x85+ purge of the queued jog lines, then the opposite batch goes out on a later tick (the controller needs about 20 ms to process the cancel,deferredJogSendCb). A reversal is treated as an emergency reflex (field report 2026-10-14). - Detents that cannot be sent now (planner buffer full, cancel just sent) are kept and retried on the next tick while the gate is armed.
Other a14 Changes
Section titled “Other a14 Changes”- Planner bar: the denominator (
planner_blocks) is resynchronized on every update, since it may arrive after the screen was created; an unknown max shows the neutral select color instead of red. - Updates arriving right after
unsubscribe()are ignored (same guard ason_axis_count_update). - The initial focus is restored with
setFocusedItem(id, true)(no beep at creation). - The WCS badge is refreshed on each opening (grbl / grblHAL only report the parser state at connection;
$Gis re-sent when connected).
Panel Component Integration
Section titled “Panel Component Integration”The PanelComponent manages three encoder-navigable items. See screens_architecture.md for the full PanelComponent contract.
| Item | LVGL object | Panel mode interaction | Encoder action |
|---|---|---|---|
step_item_id | step_btn | NAVIGATION → EDITING (OK) | Cycle step preset (±1 slot, wraps to Custom) |
feedrate_item_id | feedrate_btn | NAVIGATION → EDITING (OK) | Cycle feedrate preset (±1 slot, wraps to Custom) |
jog_item_id | position_btn | Always EDITING when focused | Execute single jog in encoder direction |
The onPanelEvent() unified callback handles all panel events and applies automatic mode switching:
- Focusing
jog_item_idwhile in NAVIGATION → auto-switch to JOG_SEQUENTIAL. - Focusing
step_item_idorfeedrate_item_idwhile in JOG_SEQUENTIAL → auto-switch back to NAVIGATION. suppress_focus_event_prevents re-entrancy during programmatic transitions.
Touch-only boards (
!ESP3D_HARDWARE_ENCODER_FEATURE): the encoder is emulated when a panel item is active.setEncoderEmulation(true)remaps Button 1 to CW step and Button 2 to CCW step, swapping icons accordingly. See input_emulation.md for the full emulation architecture.
Event Subscriptions
Section titled “Event Subscriptions”All subscriptions are cleaned up in prepareForDestruction(). Each callback guards against post-unsubscribe delivery (a queued value snapshot may arrive after unsubscribe() returns) with a jog_screen_obj_instance->isValid() guard at entry.
Screen Lifecycle
Section titled “Screen Lifecycle”Creation is split across two deferred LVGL timer phases (ESP3D_DEFERED_SCREEN_CREATION_DELAY_MS between each) to stay within budget and avoid starving other LVGL tasks during the initialization burst.
Focus and editing state (last_focused_item_id, last_focused_was_editing) survive screen transitions. Returning from the Custom-value input screen restores the previously active panel item and its editing mode exactly as it was before the transition.
Data Flow Summary
Section titled “Data Flow Summary”Key Constants and Thresholds
Section titled “Key Constants and Thresholds”| Constant | Value | Purpose |
|---|---|---|
ESP3D_PLANNER_BUFFER_SIZE_THRESHOLD | 5 | Minimum free planner blocks before a jog command is sent |
CONTINUOUS_JOG_DISTANCE_LINEAR_MM | 10 000 mm | Distance sent for continuous linear (X/Y/Z) jog |
CONTINUOUS_JOG_DISTANCE_ROTARY_DEG | 36 000 deg | Distance sent for continuous rotary (A/B/C) jog |
POSITION_DISPLAY_MIN_INTERVAL_MS | 250 ms | Per-group throttle interval for WPos/MPos label updates |
position_flush_timer period | 300 ms | Timer that syncs labels after a throttled update when motion stops |
| Continuous jog stop strategy threshold | 1 000 ms | Duration above which realtime cancel is preferred over queue purge |
Related Documentation
Section titled “Related Documentation”- screens_architecture.md — overall screen-based UI architecture,
GenericScreen,UIManager, and LVGL threading model - screen_transitions_flow.md — timer-safe transition patterns used by
cleanup_timer_cbandtransition_timer_cb - Input_system.md — hardware encoder, 4-position switch, and virtual-button subsystem
- input_emulation.md — encoder emulation for touch-only boards (Button 1/2 remapped to CW/CCW steps)
- gcode_host_architecture.md — GCode pipeline,
purgeByRequestId, and priority system used by jog and cancel commands - gcode_host_streaming_flow.md — planner buffer flow control referenced by the progress bar
- ui_style_guide.md —
ThemeStyles,applyButtonDefaultStyle, and colour token constants used throughout this screen