nimphea/nimphea_ui_controls

Source   Edit  

UI Controls - Button and Potentiometer Monitoring

This module provides template-based monitoring for buttons and potentiometers with debouncing, double-click detection, retriggering, and deadband filtering. Events are generated in a UiEventQueue for processing by the UI system.

Features:

  • ButtonMonitor: Debounced button monitoring with double-click and repeat
  • PotMonitor: Potentiometer monitoring with idle/moving states and deadbands
  • Backend system: Flexible input sources via backend classes
  • Zero-cost abstraction: Template-based, compile-time polymorphism

Usage Pattern:

# Define your backend class
type
  MyButtonBackend = object
    pins: array[3, DaisyPin]

proc isButtonPressed(backend: var MyButtonBackend, buttonId: uint16): bool =
  # Read from your hardware
  result = backend.pins[buttonId].read()

# Create and use monitor
var backend = MyButtonBackend(pins: [PIN_D0, PIN_D1, PIN_D2])
var queue = initUiEventQueue()
var monitor = initButtonMonitor[MyButtonBackend, 3](queue, backend)

# In your main loop:
while true:
  monitor.process()
  # Process events from queue

Architecture:

  • C++ templates are wrapped as Nim generics with importcpp
  • Backend interface is defined as a concept (compile-time duck typing)
  • Template instantiation happens at C++ compile time
  • Zero runtime overhead compared to hand-written code

This module wraps libDaisy's ui/ButtonMonitor.h and ui/PotMonitor.h

These are example backend implementations that can be used directly or serve as templates for custom backends.

Types

AdcPotBackend[N] = object
  channels*: array[N, uint8]
  minValue*: cfloat
  maxValue*: cfloat
Simple ADC backend for potentiometers Reads from ADC channels and scales to 0.0-1.0 Source   Edit  
ButtonMonitor[BackendType; numButtons] {.
    importcpp: "daisy::ButtonMonitor<\'0, @>", bycopy.} = object

Monitors button states and generates events in a UiEventQueue

Template Parameters:

  • BackendType - Backend class that implements IsButtonPressed(uint16): bool
  • numButtons - Number of buttons to monitor (compile-time constant)

Features:

  • Software debouncing with configurable timeout
  • Double-click detection
  • Auto-repeat (retriggering) when held down
  • State queries (is button currently pressed?)

Backend Requirements: Your BackendType must implement:

proc isButtonPressed(backend: var BackendType, buttonId: uint16): bool

Source   Edit  
GpioButtonBackend[N] = object
  pins*: array[N, uint8]
  inverted*: bool
Simple GPIO backend for buttons Buttons should be wired with pull-ups (active-low) Source   Edit  
PotMonitor[BackendType; numPots] {.importcpp: "daisy::PotMonitor<\'0, @>",
                                   bycopy.} = object

Monitors potentiometer values and generates events in a UiEventQueue

Template Parameters:

  • BackendType - Backend class that implements GetPotValue(uint16): float
  • numPots - Number of potentiometers to monitor (compile-time constant)

Features:

  • Dual deadband system (idle vs moving)
  • Activity state tracking (idle/moving)
  • Configurable idle timeout
  • Movement detection with hysteresis

Backend Requirements: Your BackendType must implement:

proc getPotValue(backend: var BackendType, potId: uint16): float
Returns value in range 0.0 to 1.0

Source   Edit  

Procs

proc getBackend[B, N](this: var ButtonMonitor[B, N]): var B {.
    importcpp: "#.GetBackend()", header: "ui/ButtonMonitor.h", ...raises: [],
    tags: [], forbids: [].}

Get reference to the backend object

Useful for accessing backend-specific functionality or reconfiguring the backend at runtime.

Source   Edit  
proc getBackend[B, N](this: var PotMonitor[B, N]): var B {.
    importcpp: "#.GetBackend()", header: "ui/PotMonitor.h", ...raises: [],
    tags: [], forbids: [].}

Get reference to the backend object

Useful for accessing backend-specific functionality.

Source   Edit  
proc getCurrentPotValue[B, N](this: var PotMonitor[B, N]; potId: uint16): cfloat {.
    importcpp: "#.GetCurrentPotValue(#)", header: "ui/PotMonitor.h", ...raises: [],
    tags: [], forbids: [].}

Get the last debounced value of a potentiometer

Returns the value (0.0-1.0) that was most recently posted to the event queue. This is the debounced, filtered value, not the raw backend value.

Parameters:

  • potId - Potentiometer ID (0 to numPots-1)

Returns: Value in range 0.0 to 1.0, or -1.0 if potId is invalid

Example:

let volume = monitor.getCurrentPotValue(POT_VOLUME)
setAudioGain(volume)

Source   Edit  
proc getNumButtonsMonitored[B, N](this: var ButtonMonitor[B, N]): uint16 {.
    importcpp: "#.GetNumButtonsMonitored()", header: "ui/ButtonMonitor.h",
    ...raises: [], tags: [], forbids: [].}

Get number of buttons monitored

Returns the template parameter numButtons. Useful for generic code that doesn't know the compile-time constant.

Source   Edit  
proc getNumPotsMonitored[B, N](this: var PotMonitor[B, N]): uint16 {.
    importcpp: "#.GetNumPotsMonitored()", header: "ui/PotMonitor.h", ...raises: [],
    tags: [], forbids: [].}

Get number of potentiometers monitored

Returns the template parameter numPots.

Source   Edit  
proc init[B, N](this: var ButtonMonitor[B, N]; queue: var UiEventQueue;
                backend: var B; debounceTimeoutMs: uint16 = 50;
                doubleClickTimeoutMs: uint32 = 500;
                retriggerTimeoutMs: uint32 = 2000;
                retriggerPeriodMs: uint32 = 50) {.importcpp: "#.Init(@)",
    header: "ui/ButtonMonitor.h", ...raises: [], tags: [], forbids: [].}

Initialize the button monitor

Parameters:

  • queue - UiEventQueue to post events to
  • backend - Backend object that provides button states
  • debounceTimeoutMs - Debounce timeout in ms (default 50ms, 0 to disable)
  • doubleClickTimeoutMs - Max time between clicks to detect double-click (default 500ms)
  • retriggerTimeoutMs - Hold time before auto-repeat starts (default 2000ms, 0 to disable)
  • retriggerPeriodMs - Auto-repeat rate when held (default 50ms = 20 Hz)

Example:

var monitor: ButtonMonitor[MyBackend, 4]
monitor.init(eventQueue, buttonBackend, debounceTimeoutMs = 30)

Source   Edit  
proc init[B, N](this: var PotMonitor[B, N]; queue: var UiEventQueue;
                backend: var B; idleTimeoutMs: uint16 = 500;
                deadBandIdle: cfloat = 0.000977; deadBand: cfloat = 0.000244) {.
    importcpp: "#.Init(@)", header: "ui/PotMonitor.h", ...raises: [], tags: [],
    forbids: [].}

Initialize the potentiometer monitor

Parameters:

  • queue - UiEventQueue to post events to
  • backend - Backend object that provides pot values (0.0-1.0)
  • idleTimeoutMs - Time without movement before entering idle state (default 500ms)
  • deadBandIdle - Deadband when idle (default 1/1024 โ‰ˆ 0.001 = 10-bit resolution)
  • deadBand - Deadband when moving (default 1/4096 โ‰ˆ 0.0002 = 12-bit resolution)

Deadband Explanation:

  • Idle deadband (larger): Prevents noise from triggering movement when pot is stationary
  • Moving deadband (smaller): Provides smoother tracking once movement detected
  • Choose based on ADC resolution and expected noise level

Example:

var monitor: PotMonitor[MyAdcBackend, 8]
monitor.init(eventQueue, adcBackend,
             idleTimeoutMs = 300,
             deadBandIdle = 0.002,   # 2 ADC counts on 12-bit
             deadBand = 0.0005)       # 0.5 ADC counts on 12-bit

Source   Edit  
proc isButtonPressed[B, N](this: var ButtonMonitor[B, N]; buttonId: uint16): bool {.
    importcpp: "#.IsButtonPressed(#)", header: "ui/ButtonMonitor.h", ...raises: [],
    tags: [], forbids: [].}

Query current state of a button (debounced)

Returns true if button is currently pressed (after debounce timeout), false if released or during debounce transition.

Parameters:

  • buttonId - Button ID (0 to numButtons-1)

Returns: Current debounced button state

Example:

if monitor.isButtonPressed(0):
  echo "Button 0 is pressed"

Source   Edit  
proc isMoving[B, N](this: var PotMonitor[B, N]; potId: uint16): bool {.
    importcpp: "#.IsMoving(#)", header: "ui/PotMonitor.h", ...raises: [], tags: [],
    forbids: [].}

Query if potentiometer is currently being moved

Returns true if pot has moved within the idle timeout period, false if pot has been stationary for longer than timeout.

Parameters:

  • potId - Potentiometer ID (0 to numPots-1)

Returns: true if moving, false if idle

Use Case: Useful for UI feedback (highlight active controls) or parameter locking (ignore MIDI changes while user is touching the pot).

Example:

if monitor.isMoving(potId):
  display.drawHighlight(potId)

Source   Edit  
proc process[B, N](this: var ButtonMonitor[B, N]) {.importcpp: "#.Process()",
    header: "ui/ButtonMonitor.h", ...raises: [], tags: [], forbids: [].}

Check all buttons and generate events

Call this regularly (e.g., from main loop) to poll button states and generate events. Recommended call rate: 100-1000 Hz (1-10ms interval).

Events Generated:

  • ButtonPressed - when button is pressed (after debounce)
  • ButtonReleased - when button is released (after debounce)
  • ButtonPressed (repeated) - during auto-repeat phase

Example:

while true:
  buttonMonitor.process()
  daisy.delay(10)  # 100 Hz polling

Source   Edit  
proc process[B, N](this: var PotMonitor[B, N]) {.importcpp: "#.Process()",
    header: "ui/PotMonitor.h", ...raises: [], tags: [], forbids: [].}

Check all potentiometers and generate events

Call this regularly (e.g., from main loop) to poll pot values and generate events. Recommended call rate: 100-1000 Hz.

Events Generated:

  • PotMoved - when pot value changes beyond deadband
  • PotActivityChanged - when pot transitions idleโ†”moving

Example:

while true:
  potMonitor.process()
  daisy.delay(10)  # 100 Hz polling

Source   Edit  

Templates

template createButtonMonitor[B; N: static int](backendType: typedesc[B];
    numButtons: static int): untyped

Template to create a ButtonMonitor type with less boilerplate

Example:

type MyMonitor = createButtonMonitor(GpioButtonBackend, 4)
var monitor: MyMonitor

Source   Edit  
template createPotMonitor[B; N: static int](backendType: typedesc[B];
    numPots: static int): untyped

Template to create a PotMonitor type with less boilerplate

Example:

type MyMonitor = createPotMonitor(AdcPotBackend, 8)
var monitor: MyMonitor

Source   Edit