Skip to content

pibot_pendant_v1_0_factory_app_input

Input subsystem for the PiBot Pendant v1.0 factory/recovery application. Provides polling-based drivers for all physical input peripherals—three push buttons, a quadrature rotary encoder, a capacitive touchscreen, and a buzzer for tactile feedback—built directly on ESP-IDF primitives without any RTOS queues, ISRs, or the abstractions used by the production BSP.


  1. Purpose & Scope
  2. Context in the Factory App
  3. Module Architecture
  4. Component Reference
  5. Data Types
  6. Input Event Flow
  7. Hardware Mapping
  8. Design Decisions
  9. Related Modules

The factory/recovery application runs independently of the main pendant firmware. Its purpose is hardware validation, OTA partition management, and SD-card-based firmware updates. Because it must work even when the main firmware is absent or corrupt, it cannot depend on the main firmware’s high-level BSP or driver stack.

This input module provides the absolute minimum needed to drive the pendant’s physical controls during a factory session:

PeripheralFilePurpose
Push buttons (×3)buttons.cMenu navigation, action confirmation
Rotary encoderencoder.cList scrolling, value adjustment
FT6336U touch paneltouch.c / touch.hTouch-based menu interaction
Passive buzzerbuzzer.cAudible feedback on user actions

All four drivers are polling-only. No interrupt service routines, no FreeRTOS event groups, and no ring buffers are used. The main application loop in pibot_pendant_v1_0_factory_app_main calls each driver directly.


Custom Bootloader

(pibot_pendant_v1_0_bootloader)

Hardware

pibot_pendant_v1_0_factory_app

calls

calls

calls

GPIO poll

PCNT read

I2C read

GPIO toggle

same bit-bang technique

main.c

(app_main / menu loop)

**input module** ← you are here

buttons · encoder · touch · buzzer

display module

gfx · ili9341

storage module

sdcard

tools

flash_all · flash_factory

generate_font · snap2png

Buttons

(GPIO, active-low)

Encoder

(PCNT quadrature)

Touch

(FT6336U / I2C)

Buzzer

(GPIO bit-bang)

ILI9341 LCD

(SPI)

SD Card

(SPI)

hooks.c

beep_short / beep_confirm

backup_and_erase_otadata

The factory app itself sits in boards/pibot_pendant_v1_0/Factory/ and is built as a completely separate ESP-IDF project from the main pendant firmware. It shares hardware pin definitions (hw_config.h) with the bootloader hooks but has no compile-time dependency on the main firmware source tree.


returns

uses enum

buttons

-gpio_num_t btn_pins[3]

+buttons_init() : void

+button_is_pressed(button_id_t) : bool

+button_wait_press(timeout_ms) : button_id_t

encoder

-pcnt_unit_handle_t s_pcnt_unit

-bool s_initialized

+encoder_init() : esp_err_t

+encoder_read() : int

touch

-bool s_initialized

-touch_write_byte(reg, data) : esp_err_t

-touch_read_registers(reg, data, len) : esp_err_t

+touch_init() : void

+touch_read() : touch_point_t

touch_point_t

+bool pressed

+int16_t x

+int16_t y

buzzer

+buzzer_init() : void

+buzzer_beep_short() : void

button_id_t

Board Config

FreeRTOS

ESP-IDF Peripherals

driver/gpio

driver/pulse_cnt

driver/i2c

esp_rom_sys

freertos/task

(vTaskDelay)

hw_config.h

(pin assignments)

factory_log.h

(FACTORY_LOGD macro)

buttons

encoder

touch

buzzer


File: boards/pibot_pendant_v1_0/Factory/main/buttons.c

Manages three tactile push buttons (BTN_1, BTN_2, BTN_3). Buttons are wired active-low: the GPIO reads 0 when pressed and 1 when released. Internal pull-ups are enabled; no external resistors are required.

void buttons_init(void);

Configures BUTTON_1_PIN, BUTTON_2_PIN, and BUTTON_3_PIN (from hw_config.h) as inputs with pull-ups enabled, no interrupts.

GPIO config applied:

FieldValue
modeGPIO_MODE_INPUT
pull_up_enGPIO_PULLUP_ENABLE
pull_down_enGPIO_PULLDOWN_DISABLE
intr_typeGPIO_INTR_DISABLE
bool button_is_pressed(button_id_t btn);

Returns true if the GPIO for btn reads 0 (active-low). Returns false immediately if btn is out of the valid range (BTN_1–BTN_3).


button_id_t button_wait_press(uint32_t timeout_ms);

Blocking call. Polls all three buttons at 20 ms intervals, applying a 50 ms debounce on detection. Waits for the button to be released before returning, then adds a second 50 ms debounce after release.

timeout_msBehaviour
0Waits indefinitely
> 0Returns BTN_NONE after that many milliseconds if no press occurs
GPIO PinPoll TaskCallerGPIO PinPoll TaskCallerloop[poll every 20 ms]loop[wait for release, pollevery 20 ms]button_wait_press(timeout_ms)gpio_get_level()HIGH (not pressed)gpio_get_level()LOW (press detected)vTaskDelay 50 ms debouncegpio_get_level()LOW (confirmed)gpio_get_level()LOW (still held)gpio_get_level()HIGH (released)vTaskDelay 50 ms post-release debouncebutton_id_t BTN_1 / BTN_2 / BTN_3

File: boards/pibot_pendant_v1_0/Factory/main/encoder.c

Implements quadrature decoding of the pendant’s rotary encoder using the ESP32 PCNT (Pulse Counter) peripheral. This is fully polled: the hardware accumulates counts autonomously and the application reads accumulated clicks on demand.

ConstantValueNotes
PULSES_PER_DETENT4Adjust if rotation feels too sensitive or sluggish
High limit1000PCNT saturation limit
Low limit-1000PCNT saturation limit
Glitch filter1000 nsMatches main firmware setting in production BSP
esp_err_t encoder_init(void);

Sets up a PCNT unit with two channels for full quadrature decoding:

  • Channel A — detects edges on ENCODER_A_PIN, uses ENCODER_B_PIN as level
  • Channel B — detects edges on ENCODER_B_PIN, uses ENCODER_A_PIN as level

Both channels are configured for standard quadrature actions (increase on one edge direction, decrease on the other, inverted by the opposite pin level). Internal GPIO pull-ups are enabled; no external pull-ups are present on the board.

Returns ESP_OK on success; calls ESP_ERROR_CHECK internally on any PCNT API failure.

int encoder_read(void);

Reads the PCNT count, divides by PULSES_PER_DETENT to produce integer detent clicks, clears the counter, and returns the click count. Returns 0 if not initialized or no movement since last call.

Note: Remainder pulses (below one full detent) are discarded on each read. This prevents drift accumulation at typical 100 ms polling rates. The trade-off is negligible given the physical detent resolution of the encoder.

A rising edge, B is HIGH

PCNT count increments

A rising edge, B is LOW

PCNT count decrements

Idle

CW_Step

CCW_Step

Full detent = 4 PCNT edges

encoder_read() returns

accumulated count divided by 4

then clears counter


Files: boards/pibot_pendant_v1_0/Factory/main/touch.c,
boards/pibot_pendant_v1_0/Factory/main/touch.h

Provides a minimal polling interface to the FT6336U capacitive touch controller over I2C. Interrupt mode is deliberately disabled to keep the factory app self-contained and interrupt-free. Coordinates are returned in screen pixel space with no swap or invert transformations needed for this board.

void touch_init(void);

Configures the I2C master bus on TOUCH_I2C_PORT_IDX using TOUCH_SDA_PIN and TOUCH_SCL_PIN at TOUCH_I2C_FREQ_HZ (all from hw_config.h). Internal pull-ups are enabled on both SDA and SCL lines.

After bus installation, four FT6336U registers are configured:

RegisterAddressValue WrittenEffect
FT6336U_DEVICE_MODE0x000x00Normal operating mode
FT6336U_THRESHHOLD0x8040Touch detection threshold
FT6336U_TOUCHRATE_ACTIVE0x880x0EActive report rate
FT6336U_INTERRUPT_MODE0xA40Polling mode (interrupts disabled)
touch_point_t touch_read(void);

Reads 5 bytes starting at register 0x02 (touch count + first touch point coordinates). Returns a touch_point_t with pressed = false and x = y = -1 when:

  • The driver is not initialized
  • The I2C transaction fails (50 ms timeout)
  • No touch is detected (touch_points == 0)
  • The touch count is ≥ 6 (indicates a read error or garbage)

On a valid touch, coordinates are extracted as:

x = ((buf[1] & 0x0F) << 8) | buf[2] // high nibble of byte 1 + full byte 2
y = ((buf[3] & 0x0F) << 8) | buf[4] // high nibble of byte 3 + full byte 4
FT6336UI2C Mastertouch_read()FT6336UI2C Mastertouch_read()write+read starting at reg 0x02START + device addr (W) + 0x02ACKREPEATED START + device addr (R)buf[0] touch_countbuf[1] xH + event_flagbuf[2] xLbuf[3] yH + touch_idbuf[4] yL5 bytes (50ms timeout)decode x, y if touch_count in [1..5]return touch_point_t

File: boards/pibot_pendant_v1_0/Factory/main/buzzer.c

Drives a passive piezo buzzer using GPIO bit-banging. No PWM (LEDC) peripheral is used, keeping the implementation minimal and compatible with any free GPIO. The same technique is used in the custom bootloader’s hooks (pibot_pendant_v1_0_bootloader: beep_short / beep_confirm).

ConstantValueNotes
BEEP_FREQ_HZ2700 HzAudible mid-range frequency for small piezos
BEEP_DURATION_MS40 msShort enough to be non-intrusive
Half-period~185 µs1 000 000 / (2 × 2700)
Total cycles1082700 Hz × 0.040 s
void buzzer_init(void);

Configures BUZZER_PIN (from hw_config.h) as a push-pull GPIO output and drives it low (silent state).

void buzzer_beep_short(void);

Generates a 40 ms square wave at 2700 Hz by toggling the GPIO pin and calling esp_rom_delay_us() between each toggle. This is a blocking, busy-wait call.

It is acceptable in the factory app because:

  1. The app is single-threaded — no concurrent LVGL or critical FreeRTOS tasks run during feedback.
  2. 40 ms latency on button/encoder feedback is imperceptible to the operator.
BUZZER_PINCallerBUZZER_PINCalleresp_rom_delay_us(185µs)esp_rom_delay_us(185µs)loop[108 cycles (40ms total)]buzzer_beep_short()gpio_set_level HIGHgpio_set_level LOWreturns (pin remains LOW / silent)

Defined in touch.h:

typedef struct {
bool pressed; // true if a touch is currently detected
int16_t x; // x-coordinate in screen pixels (-1 if not pressed)
int16_t y; // y-coordinate in screen pixels (-1 if not pressed)
} touch_point_t;
ValueMeaning
BTN_NONENo button pressed / timeout expired
BTN_1First physical button
BTN_2Second physical button
BTN_3Third physical button

The main loop in main.c (app_main) polls the input module at each iteration. The diagram below shows how all four subsystems feed into the factory application’s menu and action dispatch:

clicks != 0

pressed = true

BTN_1/2/3

virtual button ID

app_main loop iteration

encoder_read

returns click count

touch_read

returns touch_point_t

button_is_pressed / button_wait_press

menu_move

scroll selection up/down

touch_hint_hit_test

map screen coords to button zone

dispatch_button

map button ID to action

draw_menu

refresh display

execute_selected_action

SD update / OTA boot / etc.

buzzer_beep_short

audible confirm


All pin assignments are centralised in hw_config.h. The table below lists the logical names this module references:

Driverhw_config.h SymbolPeripheral
buttons.cBUTTON_1_PINGPIO input, internal pull-up
buttons.cBUTTON_2_PINGPIO input, internal pull-up
buttons.cBUTTON_3_PINGPIO input, internal pull-up
encoder.cENCODER_A_PINPCNT edge input + GPIO pull-up
encoder.cENCODER_B_PINPCNT level input + GPIO pull-up
touch.cTOUCH_SDA_PINI2C SDA, internal pull-up
touch.cTOUCH_SCL_PINI2C SCL, internal pull-up
touch.cTOUCH_I2C_PORT_IDXI2C bus number (0 or 1)
touch.cTOUCH_I2C_ADDR_HEXFT6336U 7-bit I2C address
touch.cTOUCH_I2C_FREQ_HZI2C clock speed
buzzer.cBUZZER_PINGPIO push-pull output

For authoritative pin values and the board schematic see docs/hardware/pibot-cnc-pendant-hardware-documentation.md.


All input is polled deliberately. The factory app runs a simple cooperative loop; there is no need for the interrupt-driven, event-queue architecture used by the production BSP (pibot_pendant_v1_0_bsp). Polling eliminates ISR complexity and reduces the risk of stack or priority issues that could destabilise the recovery environment.

The production BSP wires buttons, encoder, and touch into LVGL’s indev system through button_read_cb, encoder_read_cb, potentiometer_read_cb, switch_read_cb, and touch_read_cb callbacks. The factory app bypasses all of this and drives the raw peripherals directly, removing the LVGL dependency entirely.

Although polling is the theme, the PCNT peripheral is still used for the encoder because software-based quadrature counting at 20–100 ms polling intervals would miss fast rotation. PCNT counts continuously in hardware and the count is simply read when the application needs it — the best of both worlds for a polling architecture. The glitch filter (1 µs) is intentionally kept identical to the production BSP setting.

Using esp_rom_delay_us busy-loops rather than LEDC/PWM avoids consuming a PWM timer channel and keeps the buzzer fully independent of any timer resource availability at factory time. The 40 ms blocking duration is acceptable because no concurrent UI tasks exist.
See also the identical approach in pibot_pendant_v1_0_bootloader (beep_short / beep_confirm / buzzer_tone in hooks.c).

The FT6336U on this board is physically oriented so that raw I2C coordinates map directly to screen pixel coordinates with no swap or invert. This is explicitly noted in touch.c to prevent future maintainers from adding unnecessary transformations. The production touch_ft6336u_def.h for this board uses the same defaults for the same reason.

The 50 ms double-sample debounce in button_wait_press (press confirmation + post-release settling) was validated against the pendant’s mechanical buttons. The encoder does not need software debounce because the PCNT 1 µs glitch filter handles electrical noise at the hardware level.


ModuleRelationship
pibot_pendant_v1_0_factory_app_mainConsumer — calls all four drivers from the app_main event loop; dispatches events via dispatch_button, touch_hint_hit_test, and encoder_read
pibot_pendant_v1_0_factory_app_displaySibling — display is updated (draw_menu, show_status) after input events are processed
pibot_pendant_v1_0_factory_app_storageSibling — SD card mount/unmount actions are triggered by button and touch events
pibot_pendant_v1_0_bootloaderParallel implementation — bootloader hooks.c uses the same bit-bang buzzer technique and reads the same button GPIO to decide whether to erase OTA data at boot time
pibot_pendant_v1_0_bspProduction counterpart — wires the same hardware peripherals into LVGL indev via button_read_cb, encoder_read_cb, touch_read_cb, potentiometer_read_cb, and switch_read_cb
physical_inputsUnderlying hardware abstraction — phy_encoder, phy_buttons, phy_switch, and phy_potentiometer configs used by the production BSP (not used by the factory app)
buzzerProduction buzzer stack — hardware/common/drivers/buzzer/ + esp3d_sound.h (sound module) used by the main firmware; the factory app uses its own simpler, self-contained implementation