Skip to content

BSP Board Initialization — Physical Inputs

BSP Board Initialization — Physical Inputs

Section titled “BSP Board Initialization — Physical Inputs”

The bsp_board_initialization_inputs module contains the four LVGL input-device (indev) read callbacks that bridge raw physical hardware into LVGL’s event system on the PiBot CNC Pendant v1.0 board. It is the only board in the multi-board firmware that includes this module, because it is the only target that ships a full physical control panel in addition to a touchscreen.

The four callbacks defined here are:

CallbackGuardPhysical deviceLVGL indev type
button_read_cbESP3D_HARDWARE_BUTTONS_FEATURE3 push-buttonsLV_INDEV_TYPE_BUTTON
encoder_read_cbESP3D_HARDWARE_ENCODER_FEATURERotary quadrature encoderLV_INDEV_TYPE_ENCODER
switch_read_cbESP3D_HARDWARE_SWITCH_FEATURE4-position selector switchLV_INDEV_TYPE_BUTTON
potentiometer_read_cbESP3D_HARDWARE_POTENTIOMETER_FEATUREAnalog potentiometer (ADC)LV_INDEV_TYPE_POINTER

All four callbacks share the same design contract:

  1. Read raw hardware state from the underlying physical driver (see bsp_physical_inputs.md).
  2. Validate state changes with edge-detection — only transitions fire events, not sustained levels.
  3. Gate events through the activity_manager so the first input after a display timeout wakes the screen without triggering a functional action.
  4. Dispatch a typed control_event_t payload to the active LVGL screen via lv_obj_send_event().

The callbacks are registered inside init_lvgl() (see bsp_board_initialization_lvgl.md), and the custom LVGL event codes they use are registered by control_events_init() (see bsp_bsp_control_events.md).


AttributeValue
Source fileboards/pibot_pendant_v1_0/components/bsp/board_init.c
Boardpibot_pendant_v1_0 only
Parent modulebsp_board_initialization.md
Sibling modulesbsp_board_initialization_lvgl.md, bsp_board_initialization_display.md, bsp_board_initialization_touch.md
Physical driversbsp_physical_inputs.md
Event typesbsp_bsp_control_events.md

Application Layer

LVGL Indev Framework

Physical Input Drivers

bsp_physical_inputs

BSP - pibot_pendant_v1_0

Input Callbacks (this module)

read_cb poll

read_cb poll

read_cb poll

read_cb poll

control_event_t

LV_EVENT_PRESSED / RELEASED

control_event_t

LV_EVENT_KEY

control_event_t

LV_EVENT_SWITCH_PRESSED / RELEASED

control_event_t

LV_EVENT_POTENTIOMETER_CHANGED

board_init()

init_lvgl()

control_events_init()

button_read_cb()

encoder_read_cb()

switch_read_cb()

potentiometer_read_cb()

phy_buttons_read()

phy_encoder_read()

phy_switch_read()

phy_potentiometer_read()

button_indev

LV_INDEV_TYPE_BUTTON

encoder_indev

LV_INDEV_TYPE_ENCODER

switch_indev

LV_INDEV_TYPE_BUTTON

potentiometer_indev

LV_INDEV_TYPE_POINTER

activity_manager

activity_process_event()

Active LVGL Screen

lv_screen_active()


LVGL Events dispatched to active screen

BSP Callbacks (this module)

phy_* drivers

Physical Hardware

GPIO / PCNT / ADC

phy_buttons_read(states[3])

phy_encoder_read(&clicks)

phy_switch_read(states[4])

phy_potentiometer_read(&adc_value)

button_read_cb

edge detect x3

wake-up gating

press_duration track

encoder_read_cb

adaptive speed throttle

rotation invert option

max 5 events / poll

switch_read_cb

edge detect x4

invalid state reject

potentiometer_read_cb

ADC to 0..100 map

adaptive threshold

direction tracking

LV_EVENT_PRESSED

LV_EVENT_RELEASED

(CONTROL_FAMILY_BUTTONS)

LV_EVENT_KEY

(CONTROL_FAMILY_ENCODER)

LV_EVENT_SWITCH_PRESSED

LV_EVENT_SWITCH_RELEASED

(CONTROL_FAMILY_SWITCH)

LV_EVENT_POTENTIOMETER_CHANGED

(CONTROL_FAMILY_POTENTIOMETER)

activity_manager

wake-up arbitration


All four callbacks fill a control_event_t struct and pass it as the LVGL event user-data pointer. The receiving screen inspects family_id to distinguish the input source when multiple indev types are active simultaneously.

typedef struct {
lv_indev_t *indev; // Handle of the LVGL indev that fired
uint32_t btn_id; // Button / switch position index (0-based)
lv_indev_type_t type; // LVGL indev type
control_family_t family_id; // BUTTONS / ENCODER / SWITCH / POTENTIOMETER
int32_t steps; // Encoder: +/-1 per click | Potentiometer: 0-100
uint32_t press_duration; // Buttons: hold time ms | others: 0
} control_event_t;
Callbackbtn_idfamily_idstepspress_duration
button_read_cb0 / 1 / 2CONTROL_FAMILY_BUTTONS0ms since press
encoder_read_cb0CONTROL_FAMILY_ENCODER+1 or -10
switch_read_cb0 / 1 / 2 / 3CONTROL_FAMILY_SWITCH00
potentiometer_read_cb0CONTROL_FAMILY_POTENTIOMETER0-1000

Guard: ESP3D_HARDWARE_BUTTONS_FEATURE
Manages: 3 push-buttons (indices 0, 1, 2)
LVGL indev type: LV_INDEV_TYPE_BUTTON

The callback uses full edge detection across all three buttons inside a single LVGL poll. It deliberately processes all release edges before all press edges to avoid losing transitions when both occur in the same poll window.

VariablePurpose
last_states[3]Previous button state per button (edge detection)
button_consumed_for_wakeup[3]Marks a press that caused a wake-up — its paired release is also suppressed
press_start_time[3]Timestamp (ms) of each press start, used to compute press_duration on release
button_events[3]Pre-allocated control_event_t per button; btn_id = 0/1/2, family_id = CONTROL_FAMILY_BUTTONS

ESP_ERR

ESP_OK

yes

yes

no

no

yes

true (active)

false (wake-up)

no

button_read_cb invoked

phy_buttons_read(states[3])

return - no events

Pass 1: scan all buttons for RELEASE

last=pressed AND current=released

Transition found

for button i?

consumed_for_wakeup[i]?

Clear flag

Skip release event

activity_process_event()

Calculate press_duration

Send LV_EVENT_RELEASED with control_event_t

Pass 2: scan all buttons for PRESS

last=released AND current=pressed

Transition found

for button i?

activity_process_event()

returns true?

Record press_start_time

Send LV_EVENT_PRESSED with control_event_t

Set consumed_for_wakeup[i]=true

Suppress event

End

LVGL EventCondition
LV_EVENT_PRESSEDRising edge, system was already active
LV_EVENT_RELEASEDFalling edge, press was not a wake-up

Guard: ESP3D_HARDWARE_ENCODER_FEATURE
Manages: Single quadrature rotary encoder (ESP32 PCNT-based)
LVGL indev type: LV_INDEV_TYPE_ENCODER

The encoder driver accumulates pulse counts between polls. This callback converts the accumulated count into throttled LV_EVENT_KEY events using an adaptive speed system that reduces latency for fast spins while still filtering noise at slow speeds.

time_since_lastApplied min_intervalEffect
>= ENCODER_SPEED_THRESHOLD_SLOW_MS80 msSlow turn — relaxed gating
>= ENCODER_SPEED_THRESHOLD_NORMAL_MS40 msNormal turn
>= ENCODER_SPEED_THRESHOLD_FAST_MS20 msFast turn
< ENCODER_SPEED_THRESHOLD_FAST_MS10 msVery fast — minimal suppression

At most 5 events are generated per poll cycle to prevent LVGL queue saturation during rapid spins.

clicks == 0

clicks != 0

wake-up (false)

active (true)

yes

no

no

yes

encoder_read_cb invoked

phy_encoder_read(&clicks)

data->RELEASED, return

Activity throttle: 100 ms window

activity_process_event()

wake-up?

Ignore all encoder events

data->RELEASED, return

time_since_last = now - last_output_time

< 10 ms?

Skip: too fast

data->RELEASED, return

Select min_interval

based on time_since_last

time_since_last

>= min_interval?

Skip

data->RELEASED, return

Apply ENCODER_INVERT_ROTATION flag

step = +/-1, key = LV_KEY_RIGHT/LEFT

events_to_send = min(abs_clicks, 5)

Loop: send N x LV_EVENT_KEY

with control_event_t.steps = step

Update last_output_time after each event

LVGL Eventdata->keycontrol_event_t.steps
LV_EVENT_KEY (clockwise)LV_KEY_RIGHT+1
LV_EVENT_KEY (counter-CW)LV_KEY_LEFT-1

Guard: ESP3D_HARDWARE_SWITCH_FEATURE
Manages: 4-position selector switch (indices 0, 1, 2, 3)
LVGL indev type: LV_INDEV_TYPE_BUTTON

The selector switch encodes a physical position as exactly one active GPIO among four. The callback validates the reading, rejects invalid states (e.g. multiple positions simultaneously active during mechanical bouncing), and fires one press event on position change and one release event on the old position de-asserting.

Important: Invalid states (ESP_ERR_INVALID_RESPONSE) are silently discarded without calling activity_process_event(). This prevents spurious display wake-ups during mechanical switch transitions.

ESP_ERR_INVALID_RESPONSE

Other ESP_ERR

ESP_OK

yes

no match

yes

no match

switch_read_cb invoked

phy_switch_read(states[4])

Log warning

Do NOT trigger activity

return

Log error, return

Scan for RELEASE

last=true AND current=false

Position i released?

Send LV_EVENT_SWITCH_RELEASED

last_states[i]=false

return

Scan for PRESS

last=false AND current=true

Position i pressed?

activity_process_event()

Send LV_EVENT_SWITCH_PRESSED

last_states[i]=true

return

No change

Events dispatched (runtime-registered custom codes)

Section titled “Events dispatched (runtime-registered custom codes)”
LVGL EventRegistered by
LV_EVENT_SWITCH_PRESSEDcontrol_events_init() via lv_event_register_id()
LV_EVENT_SWITCH_RELEASEDcontrol_events_init() via lv_event_register_id()

Guard: ESP3D_HARDWARE_POTENTIOMETER_FEATURE
Manages: Single analog potentiometer on an ESP32 ADC channel
LVGL indev type: LV_INDEV_TYPE_POINTER

The potentiometer provides a continuous 12-bit ADC reading (0–4095) mapped to a 0–100 integer range. ADC readings contain thermal and electrical noise that can oscillate around a stable mechanical position. The callback uses an adaptive threshold that tightens during active use and relaxes during inactivity to separate genuine movement from noise.

ConditionThresholdRationale
After POT_INACTIVITY_THRESHOLD_MS idlePOT_WAKE_THRESHOLD_MAPPED (high)Noise at rest must not wake the display
Direction reversal (active use)1Catch reversals immediately for responsive UI
Descent (100 to 0) during active use2Slightly more sensitive going down
Ascent (0 to 100) during active use3Standard threshold going up

ESP_ERR_INVALID_RESPONSE

no driver-level change

Other ESP_ERR

ESP_OK

yes

no

yes

no

yes

no, delta < 0

no, delta >= 0

no

yes

potentiometer_read_cb invoked

phy_potentiometer_read(&adc_value)

data->RELEASED, return

Log error

data->RELEASED, return

mapped = adc_value x 100 / 4095

First read

initialization?

Store last_mapped_value = mapped

Store last_activity_time = now

Return RELEASED

delta = mapped - last_mapped_value

Detect direction change

Inactive > POT_INACTIVITY_THRESHOLD_MS?

threshold = POT_WAKE_THRESHOLD_MAPPED

Direction changed?

threshold = 1

threshold = 2

threshold = 3

abs(delta) >= threshold?

data->RELEASED

Update last_activity_time

activity_process_event()

control_event_t.steps = mapped

Send LV_EVENT_POTENTIOMETER_CHANGED

Update last_mapped_value + direction

Events dispatched (runtime-registered custom codes)

Section titled “Events dispatched (runtime-registered custom codes)”
LVGL EventRegistered bycontrol_event_t.steps
LV_EVENT_POTENTIOMETER_CHANGEDcontrol_events_init() via lv_event_register_id()Mapped value 0–100

Every callback integrates with activity_process_event() from the esp3d_activity_manager component. The function simultaneously resets the inactivity timer and returns a boolean that indicates whether the system was already awake:

  • true — system was active; process the event normally and forward it to the UI.
  • false — this event caused the wake-up; consume it silently, do not forward to the UI.
Active Screenactivity_managerInput CallbackLVGL tickActive Screenactivity_managerInput CallbackLVGL tickDisplay timeout - screen asleepNormal active usepoll read_cbactivity_process_event()false (was sleeping, now awake)mark event as consumed_for_wakeupdata->RELEASED (event suppressed)poll read_cbactivity_process_event()true (already active)lv_obj_send_event(screen, LV_EVENT_*, &control_event_t)LV_RES_OK

The switch callback does not call activity_process_event() on invalid states, preventing noise-driven wake-ups during mechanical bouncing.


The following table summarises how each indev is configured inside init_lvgl(). Button and switch indевs use dummy point coordinates {-1, -1} because event routing is performed manually via lv_obj_send_event(), bypassing LVGL’s internal button-to-point hit-testing.

Indev handlelv_indev_type_tread_cbExtra configuration
button_indevLV_INDEV_TYPE_BUTTONbutton_read_cbbutton_points[3] all set to {-1, -1}
encoder_indevLV_INDEV_TYPE_ENCODERencoder_read_cb—
switch_indevLV_INDEV_TYPE_BUTTONswitch_read_cbswitch_points[4] all set to {-1, -1}
potentiometer_indevLV_INDEV_TYPE_POINTERpotentiometer_read_cb—

Each callback and its corresponding indev registration are individually wrapped in feature guards, allowing the pibot_pendant_v1_0 target to be built with any subset of physical inputs enabled.

Feature Guards - CMakeLists.txt

ESP3D_HARDWARE_BUTTONS_FEATURE

ESP3D_HARDWARE_ENCODER_FEATURE

ESP3D_HARDWARE_SWITCH_FEATURE

ESP3D_HARDWARE_POTENTIOMETER_FEATURE

button_read_cb

+ button_indev registration

encoder_read_cb

+ encoder_indev registration

switch_read_cb

+ switch_indev registration

potentiometer_read_cb

+ potentiometer_indev registration


These callbacks are registered as the last hardware step inside init_lvgl(), which is itself called near the end of board_init(). control_events_init() must run after init_lvgl() because lv_event_register_id() requires LVGL to be initialised first.

control_events_init()init_lvgl()phy_* DriversDisplay Driverboard_init()app_main / ESP3DX::begin()control_events_init()init_lvgl()phy_* DriversDisplay Driverboard_init()app_main / ESP3DX::begin()Registers button_read_cb,encoder_read_cb,switch_read_cb,potentiometer_read_cbas LVGL indev read callbackslv_event_register_id() forLV_EVENT_SWITCH_PRESSEDLV_EVENT_SWITCH_RELEASEDLV_EVENT_POTENTIOMETER_CHANGEDboard_init()activity_manager_init()backlight_configure() + ili9341_spi_configure()init_touch_controller() [if TOUCH_FEATURE]phy_buttons_configure() [if BUTTONS]phy_encoder_configure() [if ENCODER]phy_switch_configure() [if SWITCH]phy_potentiometer_configure() [if POTENTIOMETER]buzzer_configure() [if BUZZER]init_lvgl()ESP_OKcontrol_events_init()doneESP_OK
DocumentRelationship
bsp_board_initialization.mdParent module — board_init() orchestration, all-boards overview
bsp_board_initialization_lvgl.mdSibling — init_lvgl() implementation; where these callbacks are registered
bsp_board_initialization_display.mdSibling — lvgl_flush_cb, notify_lvgl_flush_ready, display pipeline
bsp_board_initialization_touch.mdSibling — touch_read_cb, init_touch_controller()
bsp_physical_inputs.mdPhysical driver layer: phy_buttons, phy_encoder, phy_potentiometer, phy_switch
bsp_bsp_control_events.mdcontrol_event_t type, control_events_init(), custom LVGL event IDs