Skip to content

Settings Selection Screens

The Settings Selection Screens module is a collection of five single-purpose list-picker screens that allow the user to choose a value from a predefined set of options and save it to NVS (Non-Volatile Storage). Each screen is built on top of ListMenuScreen and follows a strictly uniform lifecycle pattern, making them consistent, memory-safe, and safe to destroy from within LVGL event callbacks.

The five screens are:

ScreenNamespaceSetting modified
Baud RatebaudrateScreenesp3d_baud_rate
Polling IntervalpollingScreenesp3d_polling_interval (via GCode handler)
Screen TimeoutscreenTimeoutScreenesp3d_screen_timeout
Output ClientoutputSelectionScreenesp3d_output_client / esp3d_radio_mode
LanguagelanguagesScreenactive translation pack

The settings selection screens are leaf nodes reached from the settings navigation flow:

main_screen
└── settings_screen (circular menu)
├── baudrate_screen ← this module
├── output_selection_screen ← this module
├── languages_screen ← this module
└── settings_list_screen
├── polling_screen ← this module
└── screen_timeout_screen (*) ← this module
(* requires ESP3D_BRIGHTNESS_CONTROL_FEATURE)

None of the screens in this module open further sub-screens, except output_selection_screen, which may open a message_box_screen confirmation dialog before restarting.

DependencyUsed by
ListMenuScreenall five screens (base class)
UIManager (esp3d_ui.h)all five screens
ESP3DSettingsbaudrate, polling, screen_timeout, output_selection
ESP3DTranslationServiceall five screens (labels and language list)
esp3d_resources (icons)all five screens (back_b, ok_b, refresh_b)
message_box_screenoutput_selection (restart confirmation)
ActivityMonitoringscreen_timeout (live timeout update)
ESP3DGCodeHandlerServicepolling (live interval update)
ESP3DSerialClient / ESP3DUartExtClientbaudrate (live baud rate change)
ESP3DNetworkoutput_selection (radio mode switch)

All five screens implement the same state machine enforced through shared macros (ESP3D_TRANSITION_START, ESP3D_TRANSITION_RESET, ESP3D_CLEANUP_TIMER_BODY, ESP3D_PREPARE_DESTRUCTION_GUARD, ESP3D_TRANSITION_TIMER_BODY).

create()
├── reset transition state (ESP3D_TRANSITION_RESET)
├── build item list
├── new ListMenuScreen(...) [std::nothrow + try/catch]
├── ui_manager.registerScreen(type, screen, prepareForDestruction)
├── lv_obj_add_event_cb(..., LV_EVENT_DELETE, onScreenDestroy)
└── move_to_index(initial_selection)
─── user presses Back ───────────────────────────────────────────
└── onBackButtonRelease()
└── ESP3D_TRANSITION_START → cleanup_timer_cb scheduled
cleanup_timer_cb()
├── prepareForDestruction()
│ └── screen->prepareForDestruction() [idempotent guard]
└── schedule transition_timer_cb(delay)
transition_timer_cb()
└── createScreen(next_screen_target) [dispatches to target]
─── LVGL deletes screen object ──────────────────────────────────
└── onScreenDestroy()
├── prepareForDestruction() (if not already called)
├── ui_manager.unregisterScreen(...)
└── delete instance_ptr; ptr = nullptr
FunctionRole
create()Builds item list, creates ListMenuScreen, registers with UIManager (with its prepareForDestruction, so a global overlay such as the critical alert can tear the screen down, see ui_core.md), attaches LV_EVENT_DELETE callback
onScreenDestroy()LVGL delete callback: triggers prepareForDestruction() if not yet done, then unregisters and frees the ListMenuScreen
prepareForDestruction()Idempotent (via ESP3D_PREPARE_DESTRUCTION_GUARD): calls screen->prepareForDestruction() to cancel pending LVGL timers and callbacks
cleanup_timer_cb()Fires after button release; calls prepareForDestruction(), then schedules transition_timer_cb
transition_timer_cb()Fires after ESP3D_TRANSITION_SCREEN_DELAY_MS; calls createScreen(next_screen_target)
PositionDefault assignmentlanguages_screen
Button 1 (left)OKSet / Select
Button 2 (middle)(empty)Refresh
Button 3 (right)BackBack

All new calls use std::nothrow inside a try/catch block. An uncaught std::bad_alloc inside an LVGL callback would call std::terminate and reset the ESP32. On failure the function logs the error and returns, leaving the previous screen intact.

Item list vectors also wrap their construction in try/catch: if allocation fails partway through, create() detects the empty or partial list and returns early without creating a broken screen.

Every screen’s display*Item() callback follows the same layout:

  1. lv_obj_clean(container) — clear before redraw.
  2. If item is the current active value: create LV_SYMBOL_OK label at LV_ALIGN_LEFT_MID with ESP3D_INDICATOR_SUCCESS_COLOR.
  3. Create text label at ESP3D_LIST_START_TEXT offset with LV_LABEL_LONG_DOT overflow handling.
  4. Disable scrollbars on both label and container.

File: main/display/screens/baudrate_screen.cpp

Purpose: Lets the user pick a serial baud rate from SupportedBaudList. The change is applied immediately to the UART driver without a restart.

struct BaudrateItem {
uint32_t baudrate; // e.g. 115200
std::string display; // e.g. "115200 bps"
};
onBaudrateItemClick(item)
├── esp3dXsettings.writeUint32(esp3d_baud_rate, value)
└── serialClient.change_baud_rate(value) [or uartExtClient if active]
└── on success → refresh_visible_items() [update checkmark]
  • Item list is built from the board-specific SupportedBaudList[] / SupportedBaudListSize compile-time constants.
  • Each entry is labelled "<value> bps" using the ESP3DLabel::bps translation.
  • When ESP3D_UART_EXT_FEATURE is enabled, the live baud rate change is routed to uartExtClient.change_baud_rate() if that is the active output client.
  • Navigation: Back → ESP3DScreenType::settings

File: main/display/screens/polling_screen.cpp

Purpose: Lets the user pick the CNC status-report polling interval from SupportedPollingList. The change is applied live via the GCode handler service.

struct PollingItem {
uint32_t interval; // milliseconds; 0 = disabled
std::string display; // e.g. "200 ms", "1 sec", "None"
};
onPollingItemClick(item)
└── esp3dGcodeHandler.updateReportingInterval(interval_ms)
├── saves to NVS (esp3d_polling_interval)
├── updates live reporting timer
└── on success → refresh_visible_items() [update checkmark]
  • Display formatting: 0 → "None", < 1000 ms → "X ms", ≥ 1000 ms → "X sec".
  • grblHAL constraint: values > 1000 ms are skipped from the list because grblHAL’s $481 auto-report interval is capped at 1000 ms.
  • The updateReportingInterval() call handles both NVS persistence and live timer update in one step.
  • Navigation: Back → ESP3DScreenType::settings_list

File: main/display/screens/screen_timeout_screen.cpp

Compile guard: ESP3D_BRIGHTNESS_CONTROL_FEATURE

Purpose: Lets the user pick a screen auto-off timeout from SupportedTimeoutList. The change is saved to NVS and immediately applied to the activity monitoring subscriber.

struct TimeoutItem {
uint32_t timeout_seconds; // 0 = disabled
std::string display; // e.g. "30 sec", "5 min", "1 hour", "None"
};
onTimeoutItemClick(item)
├── esp3dXsettings.writeUint32(esp3d_screen_timeout, seconds)
└── ActivityMonitoring::g_screenSubscriber.setTimeout(seconds * 1000)
└── on success → refresh_visible_items() [update checkmark]
  • Display formatting: 0 → "None", < 60 s → "X sec", < 3600 s → "X min", ≥ 3600 s → "X hour".
  • The timeout value is converted to milliseconds before being passed to ActivitySubscriber::setTimeout().
  • This screen is absent from the build when ESP3D_BRIGHTNESS_CONTROL_FEATURE is not defined. The settings_list_screen also conditionally omits its menu entry under the same guard.
  • Navigation: Back → ESP3DScreenType::settings_list

File: main/display/screens/output_selection_screen.cpp

Compile guard: (ESP3D_BT_SERIAL_FEATURE || ESP3D_BT_BLE_FEATURE) || ESP3D_WIFI_FEATURE || ESP3D_UART_EXT_FEATURE

Purpose: Lets the user select the CNC communication output client. Since a14 every change is saved to NVS and applied by a board restart, on every build.

Client typeFeature flagDisplay label
serialalways includedESP3DLabel::serial
bt_serialESP3D_BT_SERIAL_FEATUREESP3DLabel::bluetooth
bt_bleESP3D_BT_BLE_FEATUREESP3DLabel::bluetooth_ble
socket_clientESP3D_SOCKET_CLIENT_FEATUREESP3DLabel::wifi
websocket_clientESP3D_WS_CLIENT_SERVICE_FEATUREESP3DLabel::wifi_websocket
uart_extESP3D_UART_EXT_FEATUREESP3DLabel::external_module

The running client (esp3dCommands.getOutputClient()) and the saved one (esp3d_output_client in NVS) can differ until the next boot, so each row shows one of three states:

IndicatorMeaning
LV_SYMBOL_OK (success color)Running and saved: active and persistent
-> (success color)Running, but another client is saved for the next boot
LV_SYMBOL_WARNING (warning color)Saved for the next boot, not running yet

Before a14 the saved client showed a check mark at once, which suggested the switch had already happened (field report 2026-10-14).

onOutputItemClick(item)
├── [item == running client?] → beep, return
├── [item == client saved in NVS?] → beep, showRestartConfirmation(), return
├── writeByte(esp3d_output_client, new_client)
├── writeByte(esp3d_radio_mode, derived_radio_mode)
├── refresh the visible rows
└── showRestartConfirmation()
├── [confirmed] → show_information("Board will restart...")
│ → esp_restart() after 2 s
└── [cancelled] → stay; the row shows the warning icon

Clicking a row that is already saved for the next boot offers the restart again, so the user can still trigger it after cancelling.

FieldTypeNotes
esp3d_output_clientuint8_tThe selected ESP3DClientType
esp3d_radio_modeuint8_tDerived from client type: off, wifi_sta, bluetooth_serial, bluetooth_ble
  • Both esp3d_output_client and esp3d_radio_mode are written atomically so they remain consistent across reboots.
  • Every change needs a restart. The former live switch between serial and WiFi was removed: setOutputClient() changed the routing at once while setModeAsync() took seconds to connect, and bytes in flight on the old channel were cut in the middle of a G-code line (field report 2026-10-14). BT and UART Ext always needed a restart (radio stack, boot-time client selection). The old live-apply branch is still in the source but unreachable (constexpr bool needs_restart = true).
  • esp3d_radio_mode is derived from the client: bluetooth_serial / bluetooth_ble for BT, wifi_sta for socket and WebSocket clients, off otherwise. In debug builds that log over the network (ESP3D_LOG_BACKEND 3 = telnet or 4 = WebSocket) the current radio mode is kept for serial / UART Ext, so the logs stay reachable.
  • The prepareForDestruction function pointer is passed to message_box_screen so the current screen is properly torn down before the dialog appears.
  • Navigation: Back → ESP3DScreenType::settings

Files: main/display/screens/languages_screen.cpp, main/display/screens/languages_screen.h

Purpose: Lets the user select a UI language from the language packs stored in the ui_resources partition. The change is applied immediately — no restart required. The title bar itself updates to reflect the new locale.

LanguageInfo Structure (from languages_screen.h)

Section titled “LanguageInfo Structure (from languages_screen.h)”
struct LanguageInfo {
std::string code; // e.g. "fr-fr", "default"
std::string display; // e.g. "Français", "English"
LanguageInfo(const std::string& c, const std::string& d) : code(c), display(d) {}
};
create()
└── build_language_list()
├── esp3dTranslationService.getLanguagesList()
└── populate scanned_languages_ from values[] + labels[]
└── ListMenuScreen(...)
└── onScreenCreated() → select_current_language()
─── user taps item or Set button ────────────────────────────────
onLanguageItemClick(item)
└── ui_manager.setLanguage(code)
├── on success → update_title_text(translate(select_language))
└── refresh_visible_items() [update checkmark]
─── user presses Refresh button ─────────────────────────────────
onRefreshButtonRelease()
└── refresh_language_list()
├── build_language_list() [re-read partition slots]
├── updateItemList(...)
└── select_current_language() [scroll to active entry]
  • Language data comes from esp3dTranslationService, which reads available language slots directly from the ui_resources partition — no filesystem scan is needed and the operation is instant.
  • select_current_language() scrolls the list to the entry whose code matches esp3dTranslationService.getLanguageCode(), falling back to "default" if none is set.
  • After a language change, update_title_text() is called with the re-translated select_language label so the screen title immediately reflects the new language.
  • The Refresh button re-reads the partition in case a language pack was added via SD card update without rebooting.
  • Navigation: Back → ESP3DScreenType::settings

Files: main/display/screens/range_editor_screen.cpp, main/display/screens/range_editor_screen.h (new in a14)

Purpose: Generic editor for a bounded integer value (for example 0–100). An arc shows the value in its range, with the title above and, in the center, an optional description of the current value and the value itself. It is not a ListMenuScreen.

InputAction
Encoder rotationValue − / + one step per detent
Buttons 0 / 2 (left / right)- / +, or Set / Back
Button 1 (middle)Switches buttons 0 / 2 between the two modes (opens in - / + mode)
TouchThe arc can be dragged
FieldRole
title, unitShown above the arc / appended to the value (for example %). The title is copied into rangeEditorScreen::shared_title, because translate() returns a shared buffer
min_value, max_value, step, valueRange, change per detent or press, initial value (clamped)
describe(value, user_data)Optional text for the current value
on_change(value, user_data)Optional live preview on each change (for example brightness)
on_set(value, user_data)Called on Set with the final value: save it here
on_back(initial_value, user_data)Optional, called on Back (for example revert a preview)
return_screenScreen opened after Set or Back
prepare_current_screen_destructionCaller cleanup, run in a 0 ms timer before the editor is created

Same pattern as inputScreen / messageBoxScreen (screen_transitions_flow.md, Pattern 3): the caller calls rangeEditorScreen::create(data) directly from its click handler (no ESP3D_TRANSITION_START). The caller cleanup runs in a 0 ms timer, then the editor is created in a deferred timer. Set and Back return to return_screen with the standard ESP3D_TRANSITION_START flow. The arc uses applyRangeArcStyle() (ui_core.md).

Current caller: settings_list_screen → onWheelSensitivityClick() (ESP3D_HARDWARE_ENCODER_FEATURE), which edits esp3d_jog_wheel_sensitivity (0 = classic jog, 1–100 = proportional, see handwheel_jog.md).


The table below summarises where each screen is opened from and which function triggers the transition.

ScreenOpened fromTrigger function
baudrate_screensettings_screenBaud rate menu entry callback
output_selection_screensettings_screenOutput client menu entry callback
languages_screensettings_screenLanguage menu entry callback
polling_screensettings_list_screenonPollingClick()
screen_timeout_screensettings_list_screenonScreenTimeoutClick()
range_editor_screensettings_list_screenonWheelSensitivityClick() (direct create(), see above)

All callers of the list screens use the ESP3D_TRANSITION_START macro, which saves the current scroll position (last_selected_index), prepares the calling screen for destruction, and schedules the target screen creation via transition_timer_cb.