Skip to content

BSP Control Events

The bsp_control_events module is the hardware-to-UI event bridge layer of the Board Support Package (BSP). It defines the unified event payload structure (control_event_t) and the custom LVGL event registration system (control_events_init) that translate physical hardware inputs — buttons, rotary encoders, 4-position switches, potentiometers, and touchscreens — into LVGL events consumable by UI screens.

Every supported board carries its own copy of these files, but the structure and API are identical across all boards. This uniformity lets upper-layer screen code handle control events independently of which physical hardware or board is present.

This module is a direct child of bsp_board_initialization (which calls control_events_init()) and a sibling of bsp_physical_inputs (which provides the raw hardware read functions). The events it produces are consumed by the UI Framework screens and CNC UI screens.


UI Layer

LVGL Event System

Board Support Package (BSP)

raw reads

builds control_event_t

lv_obj_send_event

lv_obj_send_event

Physical Hardware Drivers

(bsp_physical_inputs)

LVGL Read Callbacks

button_read_cb / encoder_read_cb

switch_read_cb / potentiometer_read_cb

touch_read_cb

bsp_control_events

control_event_t · control_family_t

control_events_init()

Standard Events

LV_EVENT_PRESSED

LV_EVENT_RELEASED

LV_EVENT_KEY

Custom Events (registered at runtime)

LV_EVENT_SWITCH_PRESSED

LV_EVENT_SWITCH_RELEASED

LV_EVENT_POTENTIOMETER_CHANGED

Active Screen

(lv_screen_active())

CNC Screens

jog_screen · status_screen

macro_screen…


The module is replicated per board under boards/<board>/components/bsp/. All boards share an identical structure for control_event_t and control_family_t.

FileRole
control_types.hType definitions: control_family_t enum, custom event extern declarations, control_events_init() prototype
control_types.cImplementation of control_events_init() — registers custom event IDs with LVGL
control_event.hDefines the unified control_event_t payload struct (includes control_types.h)
esp3d_snapshot.h(pibot_pendant_v1_0 only) Thread-safe snapshot_state_t for screen capture
Boardcontrol_event.hcontrol_types.cesp3d_snapshot.h
dlc32_max_lcd✓——
esp32_2432s028r✓✓—
esp32_3248s035c✓✓—
esp32_3248s035r✓✓—
esp32s3_4827s043c✓✓—
esp32s3_8048_touch_lcd_7✓✓—
esp32s3_8048s043c✓✓—
esp32s3_8048s050c✓✓—
esp32s3_8048s070c✓✓—
esp32s3_bzm_tft35_gt911✓✓—
esp32s3_hmi43v3✓✓—
esp32s3_zx3d50ce02s_usrc_4832✓✓—
pibot_pendant_v1_0✓✓✓

control_family_t — Hardware Input Family

Section titled “control_family_t — Hardware Input Family”

Defined in control_types.h. Classifies the physical source of a control event.

typedef enum {
CONTROL_FAMILY_BUTTONS = 0, // Momentary push buttons (up to 3)
CONTROL_FAMILY_SWITCH, // 4-position rotary/toggle switch
CONTROL_FAMILY_TOUCH, // Touchscreen (reserved for future use)
CONTROL_FAMILY_ENCODER, // Rotary quadrature encoder
CONTROL_FAMILY_POTENTIOMETER // Analog potentiometer (ADC)
} control_family_t;

Defined in control_event.h. This struct is passed as the user_data pointer in every lv_obj_send_event() call made by the BSP read callbacks. The receiving screen casts the event data back to control_event_t *.

typedef struct {
lv_indev_t *indev; // LVGL input device handle for this control
uint32_t btn_id; // 0-based index: which button/switch position fired
lv_indev_type_t type; // LVGL device type (BUTTON, ENCODER, POINTER…)
control_family_t family_id; // Which hardware family fired this event
int32_t steps; // Encoder: ±1 per detent; Potentiometer: 0–100 mapped
uint32_t press_duration; // Time button was held (ms); 0 for non-buttons
} control_event_t;

Field semantics by family:

family_idbtn_idstepspress_durationLVGL event code sent
CONTROL_FAMILY_BUTTONS0, 1, 20ms heldLV_EVENT_PRESSED / LV_EVENT_RELEASED
CONTROL_FAMILY_SWITCH0–3 (position)00LV_EVENT_SWITCH_PRESSED / LV_EVENT_SWITCH_RELEASED
CONTROL_FAMILY_ENCODER0+1 (CW) / -1 (CCW)0LV_EVENT_KEY
CONTROL_FAMILY_POTENTIOMETER00–100 (mapped ADC)0LV_EVENT_POTENTIOMETER_CHANGED
CONTROL_FAMILY_TOUCH———LVGL native touch events (no control_event_t)

snapshot_state_t — Screen Capture State (pibot_pendant_v1_0 only)

Section titled “snapshot_state_t — Screen Capture State (pibot_pendant_v1_0 only)”

Defined in esp3d_snapshot.h, conditionally compiled under #if ESP3D_SNAPSHOT_FEATURE.

typedef struct {
FILE *file; // Open output file handle
volatile bool error; // Error flag (set on I/O failure)
uint32_t expected_pixels; // Total pixel count for the full capture
volatile uint32_t captured_pixels; // Pixels written so far (volatile, atomic access)
SemaphoreHandle_t mutex; // FreeRTOS mutex for concurrent access
volatile bool initialized; // System initialised flag
volatile bool ongoing; // Capture in progress (replaces .active)
} snapshot_state_t;
extern snapshot_state_t g_snapshot;

This structure is accessed from both the LVGL render task (Core 1) and snapshot management code. The mutex and volatile markers ensure safe concurrent access. See the Factory App Snapshot documentation for snapshot lifecycle management.


The module registers three events that extend LVGL’s built-in event set. Because they are registered via lv_event_register_id() at runtime, they receive IDs outside LVGL’s reserved range and are never intercepted by LVGL’s native widget event routing.

Global VariableRegistered ByFired When
LV_EVENT_SWITCH_PRESSEDcontrol_events_init()A 4-position switch changes to a new position
LV_EVENT_SWITCH_RELEASEDcontrol_events_init()The previous switch position is released
LV_EVENT_POTENTIOMETER_CHANGEDcontrol_events_init()Potentiometer ADC value exceeds the adaptive change threshold

All three are declared extern in control_types.h and initialized to LV_EVENT_ALL (sentinel) at module scope in control_types.c. Valid IDs are assigned only after control_events_init() runs.

⚠️ LVGL constraint: Do not use these event code variables before control_events_init() has been called. Their initial value LV_EVENT_ALL will match every event and cause incorrect dispatch.


void control_events_init(void);
// Declared in: control_types.h
// Implemented in: control_types.c

Registers the three custom LVGL event codes. Must be called after lv_init() and before any read callback can fire.

  • Called unconditionally from board_init() on every board that has a display (#if ESP3D_DISPLAY_FEATURE).
  • Internally calls lv_event_register_id() three times, storing returned codes in the global lv_event_code_t variables.
  • Safe to call only once per boot; re-calling would double-register and waste event IDs.

control_events_init()init_lvgl()Physical Hardware Initboard_init()Application (main.cpp)control_events_init()init_lvgl()Physical Hardware Initboard_init()Application (main.cpp)lv_event_register_id() × 3assigns LV_EVENT_SWITCH_PRESSEDassigns LV_EVENT_SWITCH_RELEASEDassigns LV_EVENT_POTENTIOMETER_CHANGEDboard_init()phy_buttons_configure() [if BUTTONS feature]phy_encoder_configure() [if ENCODER feature]phy_switch_configure() [if SWITCH feature]phy_potentiometer_configure() [if POTENTIOMETER feature]init_lvgl() → lv_init() + register indev callbackscontrol_events_init()returnsESP_OK

⚠️ Critical ordering constraint: control_events_init() must execute after lv_init(). This is enforced by the board_init() call sequence. Calling it before lv_init() will produce undefined event IDs.


UI Screen Handler

LVGL Send

Event Construction

BSP Callbacks (Core 1 - LVGL task)

Hardware Layer

phy_buttons_read

phy_encoder_read

phy_switch_read

phy_potentiometer_read

touch driver

PRESSED / RELEASED

KEY + steps

SWITCH_PRESSED / RELEASED

POTENTIOMETER_CHANGED

Physical Input

(GPIO / ADC / PCNT)

button_read_cb()

encoder_read_cb()

switch_read_cb()

potentiometer_read_cb()

touch_read_cb()

control_event_t

{indev, btn_id, type,

family_id, steps,

press_duration}

lv_obj_send_event(

active_screen,

event_code,

&control_event)

on_xxx_press() / on_xxx_release()

encoder_event_handler()

switch_event_cb()

potentiometer_event_cb()

Read CallbackEvent TriggerLVGL Event Code Sent
button_read_cbPress detected (rising edge)LV_EVENT_PRESSED
button_read_cbRelease detected (falling edge)LV_EVENT_RELEASED (with press_duration)
encoder_read_cbRotation detected (adaptive throttle)LV_EVENT_KEY (one event per detent, max 5 per call)
switch_read_cbPrevious position releasedLV_EVENT_SWITCH_RELEASED
switch_read_cbNew position pressedLV_EVENT_SWITCH_PRESSED
potentiometer_read_cbADC change exceeds adaptive thresholdLV_EVENT_POTENTIOMETER_CHANGED

All read callbacks interact with the activity manager (see Core Platform & Application Services) via activity_process_event() before dispatching events. This implements screen wake-up logic:

  • If activity_process_event() returns false, the input was consumed as a wake-up event — no LVGL event is dispatched for that press/rotation.
  • If true, normal LVGL event dispatch proceeds.
  • The encoder callback applies a 100 ms throttle on activity_process_event() calls to prevent excessive wake checks during sustained rotation.

returns false

(wake-up consumed)

returns true

(system already active)

Input detected

activity_process_event()

Suppress LVGL event

mark button consumed

Build control_event_t

lv_obj_send_event()

The button callback additionally tracks a button_consumed_for_wakeup[] flag per button index to suppress the corresponding release event when the press was used for wake-up.


The pibot_pendant_v1_0 board is the most input-rich configuration and exercises all control_family_t values simultaneously:

LVGL indev handles

Physical Inputs (pibot_pendant_v1_0)

Button 0

Button 1

Button 2

Rotary Encoder

Switch pos 0

Switch pos 1

Switch pos 2

Switch pos 3

Potentiometer (ADC)

Touchscreen (XPT2046)

button indev

(CONTROL_FAMILY_BUTTONS)

encoder indev

(CONTROL_FAMILY_ENCODER)

switch indev

(CONTROL_FAMILY_SWITCH)

pointer indev

(CONTROL_FAMILY_POTENTIOMETER)

touch indev

(CONTROL_FAMILY_TOUCH)

The encoder callback implements 4-level adaptive throttling to balance responsiveness vs. CPU load on Core 1:

Time since last eventMin interval enforcedUse case
≥ ENCODER_SPEED_THRESHOLD_SLOW_MS80 msSlow deliberate rotation
≥ ENCODER_SPEED_THRESHOLD_NORMAL_MS40 msNormal rotation
≥ ENCODER_SPEED_THRESHOLD_FAST_MS20 msFast rotation
< fast threshold10 msVery fast spin

A hard cap of 5 events per callback invocation prevents LVGL task overload during rapid turns.

The potentiometer callback uses a direction-aware adaptive threshold (ADC value mapped to 0–100) to distinguish intentional changes from ADC noise:

ConditionThresholdReason
After POT_INACTIVITY_THRESHOLD_MS of silencePOT_WAKE_THRESHOLD_MAPPEDFilters ADC noise during idle to prevent false wake-ups
Direction reversal detected1Ultra-sensitive on reversal (active use)
Descending value (100→0)2More responsive for CNC feed-rate override decrease
Ascending value (0→100)3Normal sensitivity

UI Screens

LVGL

bsp_physical_inputs

bsp_board_initialization

bsp_control_events (this module)

must precede

registers custom IDs

calls after lv_init

reads hardware via

builds

sends event via

control_types.h

control_family_t enum

LV_EVENT_xxx extern declarations

control_events_init() prototype

control_types.c

control_events_init() implementation

lv_event_register_id() × 3

control_event.h

control_event_t struct

esp3d_snapshot.h

snapshot_state_t

(pibot_pendant_v1_0 only)

board_init()

calls control_events_init()

after lv_init()

Read callbacks

button_read_cb()

encoder_read_cb()

switch_read_cb()

potentiometer_read_cb()

phy_buttons

phy_encoder

phy_switch

phy_potentiometer

lv_init()

lv_event_register_id()

lv_obj_send_event()

Generic Screens

(UI Framework)

CNC Screens

(CNC Firmware UI)


Rather than separate structures per input type, a single payload lets screens register one LVGL event handler and dispatch on family_id. This keeps the UI layer decoupled from hardware specifics and avoids per-board screen code divergence.

Why custom LVGL events for Switch and Potentiometer?

Section titled “Why custom LVGL events for Switch and Potentiometer?”

LV_EVENT_PRESSED and LV_EVENT_RELEASED are intercepted and consumed by LVGL’s widget system (buttons, lists, etc.) before propagating to the screen object. Custom event codes registered via lv_event_register_id() bypass widget interception — they reach the screen object directly, enabling switch and potentiometer events to drive screen-level logic without interference.

Why replicate per board instead of a shared component?

Section titled “Why replicate per board instead of a shared component?”

Each board’s BSP component is intentionally self-contained. The control_types.h / control_event.h pair is small (no board-specific logic), making duplication low-cost. The build system remains simple — each board’s CMakeLists.txt configures only its own BSP component with no cross-board dependencies.


DocumentRelationship
bsp_board_initializationCalls control_events_init() from board_init() after LVGL init; implements the read callbacks
bsp (bsp_physical_inputs)Provides phy_xxx_read() functions consumed by the read callbacks
UI Framework & Generic ScreensScreens receive and handle control_event_t via LVGL event handlers
CNC Firmware UI Screensswitch_event_cb, encoder_event_handler, potentiometer_event_cb process control events
Core Platform & Application Servicesactivity_manager / activity_process_event() integrated in all read callbacks
Input System ArchitectureHigher-level description of the input handling strategy
Factory App — Snapshot Systemsnapshot_state_t lifecycle and usage detail