Skip to main content

Overview

InputSource is an abstract base class that defines the interface for reading physical input devices such as buttons, switches, and external controllers. Implementations of this class handle the hardware-specific details of reading inputs and updating the shared InputState structure.

Class Definition

class InputSource {
  public:
    InputSource();
    virtual ~InputSource(){}
    virtual InputScanSpeed ScanSpeed() = 0;
    virtual void UpdateInputs(InputState &inputs) = 0;
};

Scan Speed Enumeration

enum class InputScanSpeed {
    SLOW,
    MEDIUM,
    FAST,
};
The InputScanSpeed enum allows input sources to declare their optimal scanning frequency. This enables the communication backend to selectively scan inputs based on performance requirements.
SLOW
InputScanSpeed
For infrequently changing inputs (e.g., configuration switches, mode selectors)
MEDIUM
InputScanSpeed
For inputs that change at moderate rates (e.g., analog sensors, external controllers)
FAST
InputScanSpeed
For primary button inputs that require minimal latency (e.g., GPIO buttons, matrix switches)

Constructor & Destructor

InputSource()

InputSource::InputSource()
Default constructor. Typically used to initialize hardware-specific state in derived classes.

~InputSource()

virtual ~InputSource(){}
Virtual destructor to ensure proper cleanup of derived classes.

Pure Virtual Methods

These methods must be implemented by any concrete class derived from InputSource.

ScanSpeed

virtual InputScanSpeed ScanSpeed() = 0
Returns the recommended scan speed for this input source. Returns: An InputScanSpeed enum value indicating how frequently this input source should be scanned. Usage: The communication backend uses this to implement selective scanning:
// Scan only FAST inputs in time-critical sections
backend.ScanInputs(InputScanSpeed::FAST);

// Scan all inputs periodically
backend.ScanInputs();

UpdateInputs

virtual void UpdateInputs(InputState &inputs) = 0
Reads the current state of the physical input device and updates the input state structure.
inputs
InputState&
Reference to the shared input state structure to update with current button states
Implementation Requirements:
  • Read the current hardware state
  • Update the relevant fields in the inputs structure
  • Handle any necessary debouncing or filtering
  • Should be fast and non-blocking
Multiple input sources can write to the same InputState structure. Use bitwise OR operations to combine inputs without overwriting values from other sources.

Built-in Implementations

HayBox includes several concrete implementations of InputSource for common input devices:

GpioButtonInput

class GpioButtonInput : public InputSource {
  public:
    GpioButtonInput(const GpioButtonMapping *button_mappings, size_t button_count);
    InputScanSpeed ScanSpeed();
    void UpdateInputs(InputState &inputs);

  protected:
    const GpioButtonMapping *_button_mappings;
    size_t _button_count;

  private:
    virtual void UpdateButtonState(InputState &inputs, size_t button_mapping_index, bool pressed);
};
Reads buttons connected directly to GPIO pins. Button Mapping Structure:
typedef struct {
    Button button;  // Which button in InputState to update
    uint pin;       // GPIO pin number
} GpioButtonMapping;

SwitchMatrixInput

Reads buttons arranged in a matrix configuration (rows and columns). More efficient for large numbers of buttons.

DebouncedGpioButtonInput

Extends GpioButtonInput with software debouncing to filter mechanical switch noise.

DebouncedSwitchMatrixInput

Combines switch matrix scanning with debouncing.

NunchukInput

Reads input from a Wii Nunchuk controller connected via I2C.

GamecubeControllerInput

Reads input from a GameCube controller connected to a GPIO pin.

Pca9671Input

Reads buttons from a PCA9671 I2C GPIO expander chip.

Usage Example

Creating a Simple GPIO Input Source

#include "core/InputSource.hpp"
#include "input/GpioButtonInput.hpp"

// Define button mappings
const GpioButtonMapping button_mappings[] = {
    { .button = Button_LF1, .pin = 0 },
    { .button = Button_LF2, .pin = 1 },
    { .button = Button_LF3, .pin = 2 },
    { .button = Button_LF4, .pin = 3 },
    { .button = Button_RF1, .pin = 4 },
    { .button = Button_RF2, .pin = 5 },
    // ... more buttons
};

// Create input source
GpioButtonInput gpio_input(
    button_mappings,
    sizeof(button_mappings) / sizeof(GpioButtonMapping)
);

Implementing a Custom Input Source

class CustomSensorInput : public InputSource {
  public:
    CustomSensorInput(uint8_t i2c_address) : _address(i2c_address) {
        // Initialize I2C hardware
        initI2C();
    }

    InputScanSpeed ScanSpeed() override {
        // Sensors typically don't need FAST scanning
        return InputScanSpeed::MEDIUM;
    }

    void UpdateInputs(InputState &inputs) override {
        // Read sensor data
        uint16_t sensor_value = readSensor(_address);

        // Map sensor value to button state
        // For example, activate button when sensor exceeds threshold
        if (sensor_value > THRESHOLD) {
            inputs.mb1 = true;
        }

        // Or map to multiple buttons based on ranges
        if (sensor_value > 1000) inputs.mb2 = true;
        if (sensor_value > 2000) inputs.mb3 = true;
    }

  private:
    uint8_t _address;
    
    void initI2C() {
        // Hardware-specific I2C initialization
    }

    uint16_t readSensor(uint8_t addr) {
        // Hardware-specific sensor reading
        return 0;
    }
};

Combining Multiple Input Sources

// Create multiple input sources
GpioButtonInput gpio_buttons(button_mappings, button_count);
NunchukInput nunchuk(NUNCHUK_I2C_ADDRESS);
CustomSensorInput sensor(SENSOR_I2C_ADDRESS);

// Add to array
InputSource *input_sources[] = {
    &gpio_buttons,
    &nunchuk,
    &sensor,
};

// Pass to communication backend
CommunicationBackend backend(
    input_state,
    input_sources,
    sizeof(input_sources) / sizeof(InputSource *)
);

Selective Scanning

The communication backend supports scanning input sources selectively based on their scan speed:
void loop() {
    // Scan FAST inputs every iteration for minimal latency
    backend.ScanInputs(InputScanSpeed::FAST);

    // Process outputs
    backend.UpdateOutputs();
    backend.SendReport();

    // Scan MEDIUM and SLOW inputs less frequently
    static uint32_t last_slow_scan = 0;
    if (millis() - last_slow_scan > 100) {  // Every 100ms
        backend.ScanInputs(InputScanSpeed::MEDIUM);
        backend.ScanInputs(InputScanSpeed::SLOW);
        last_slow_scan = millis();
    }
}
Calling backend.ScanInputs() without parameters scans all input sources regardless of their scan speed.

Input State Structure

The InputState structure contains all possible button states:
typedef struct _InputState {
    union {
        uint64_t buttons = 0;
        struct {
            // Left front buttons (lf1-lf16)
            bool lf1 : 1;
            bool lf2 : 1;
            // ... (lf3-lf16)
            
            // Right front buttons (rf1-rf16)
            bool rf1 : 1;
            bool rf2 : 1;
            // ... (rf3-rf16)
            
            // Left top buttons (lt1-lt8)
            bool lt1 : 1;
            // ... (lt2-lt8)
            
            // Right top buttons (rt1-rt8)
            bool rt1 : 1;
            // ... (rt2-rt8)
            
            // Middle buttons (mb1-mb12)
            bool mb1 : 1;
            // ... (mb2-mb12)
        };
    };

    // Nunchuk-specific inputs
    union {
        uint8_t nunchuk_buttons = 0;
        struct {
            bool nunchuk_connected : 1;
            bool nunchuk_c : 1;
            bool nunchuk_z : 1;
        };
    };
    int8_t nunchuk_x = 0;
    int8_t nunchuk_y = 0;
} InputState;

Best Practices

Always use bitwise OR when setting button states if multiple input sources might affect the same button. Never use direct assignment unless you’re certain no other input source will modify that button.
// Good: Combines with existing state
inputs.lf1 |= gpio_read(PIN_1);

// Bad: Overwrites other sources
inputs.lf1 = gpio_read(PIN_1);
Implement UpdateInputs() to be as fast as possible. Avoid blocking operations, delays, or excessive computation. This method is called every iteration of the main loop.
Return InputScanSpeed::FAST for primary button inputs that affect gameplay. Reserve SLOW for configuration inputs that rarely change.

Debouncing Considerations

Mechanical switches require debouncing to filter bounce noise. You can implement debouncing in two ways:
  1. Hardware debouncing: Use capacitors on button inputs
  2. Software debouncing: Use HayBox’s debounced input source classes
// Without debouncing (for clean digital signals)
GpioButtonInput gpio_input(button_mappings, button_count);

// With software debouncing (for mechanical switches)
DebouncedGpioButtonInput debounced_input(button_mappings, button_count);

Build docs developers (and LLMs) love