Controller APIs

The Controller class provides a convenience for reading multiple GPIOs at once and mapping their state to common controller buttons. It can optionally be configured to support joystick select, as well as to convert analog joystick values into digital directional values (up/down/left/right). It can also be used for just a subset of the buttons, should you wish to do so, by providing the GPIO configuration for the unused buttons to be -1.

API Reference

Header File

Classes

class Controller : public espp::BaseComponent

Class for managing controller input.

The controller can be configured to either use a digital d-pad or an analog 2-axis joystick with select button.

Digital configuration can support ABXY, start, select, and 4 digital directional inputs.

Anaolg Joystick Configuration can support ABXY, start, select, two axis (analog) joystick, and joystick select button. It will also convert the joystick analog values into digital d-pad buttons.

Digital Controller Example

    // make the controller - NOTE: this was designed for connecting the Sparkfun
    // Joystick Shield to the ESP32 S3 BOX
    espp::Controller controller(espp::Controller::DigitalConfig{
        // buttons short to ground, so they are active low. this will enable the
        // GPIO_PULLUP and invert the logic
        .active_low = true,
        .gpio_a = 38,      // D3 on the joystick shield
        .gpio_b = 39,      // D5 on the joystick shield
        .gpio_start = 42,  // D4 on the joystick shield
        .gpio_select = 21, // D6 on the joystick shield
        .log_level = espp::Logger::Verbosity::WARN});
    // and finally, make the task to periodically poll the controller and print
    // the state
    auto task_fn = [&quit_test, &controller](std::mutex &m, std::condition_variable &cv) {
      controller.update();
      bool is_a_pressed = controller.is_pressed(espp::Controller::Button::A);
      bool is_b_pressed = controller.is_pressed(espp::Controller::Button::B);
      bool is_select_pressed = controller.is_pressed(espp::Controller::Button::SELECT);
      bool is_start_pressed = controller.is_pressed(espp::Controller::Button::START);
      fmt::print("Controller buttons:\n"
                 "\tA:      {}\n"
                 "\tB:      {}\n"
                 "\tSelect: {}\n"
                 "\tStart:  {}\n",
                 is_a_pressed, is_b_pressed, is_select_pressed, is_start_pressed);
      quit_test = is_start_pressed && is_select_pressed;
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 500ms);
      }
      // we don't want to stop, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config =
                                {
                                    .name = "Controller Task",
                                    .stack_size_bytes = 6 * 1024,
                                },
                            .log_level = espp::Logger::Verbosity::WARN});
    task.start();

Analog Controller Example

    // make the adc we'll be reading from
    std::vector<espp::AdcConfig> channels{
        {.unit = ADC_UNIT_2,
         .channel = ADC_CHANNEL_1, // (x) Analog 0 on the joystick shield
         .attenuation = ADC_ATTEN_DB_12},
        {.unit = ADC_UNIT_2,
         .channel = ADC_CHANNEL_2, // (y) Analog 1 on the joystick shield
         .attenuation = ADC_ATTEN_DB_12}};
    espp::OneshotAdc adc(espp::OneshotAdc::Config{
        .unit = ADC_UNIT_2,
        .channels = channels,
    });
    // make the function which will get the raw data from the ADC and convert to
    // uncalibrated [-1,1]
    auto read_joystick = [&adc, &channels](float *x, float *y) -> bool {
      auto maybe_x_mv = adc.read_mv(channels[0]);
      auto maybe_y_mv = adc.read_mv(channels[1]);
      if (maybe_x_mv.has_value() && maybe_y_mv.has_value()) {
        auto x_mv = maybe_x_mv.value();
        auto y_mv = maybe_y_mv.value();
        *x = (x_mv / 1700.0f - 1.0f);
        *y = (y_mv / 1700.0f - 1.0f);
        return true;
      }
      return false;
    };
    // make the controller - NOTE: this was designed for connecting the Sparkfun
    // Joystick Shield to the ESP32 S3 BOX
    espp::Controller controller(espp::Controller::AnalogJoystickConfig{
        // buttons short to ground, so they are active low. this will enable the
        // GPIO_PULLUP and invert the logic
        .active_low = true,
        .gpio_a = 38,               // D3 on the joystick shield
        .gpio_b = 39,               // D5 on the joystick shield
        .gpio_x = -1,               // we're using this as start...
        .gpio_y = -1,               // we're using this as select...
        .gpio_start = 42,           // D4 on the joystick shield
        .gpio_select = 21,          // D6 on the joystick shield
        .gpio_joystick_select = -1, // D2 on the joystick shield
        .joystick_config = {.x_calibration = {.center = 0.0f,
                                              .center_deadband = 0.2f,
                                              .minimum = -1.0f,
                                              .maximum = 1.0f},
                            .y_calibration = {.center = 0.0f,
                                              .center_deadband = 0.2f,
                                              .minimum = -1.0f,
                                              .maximum = 1.0f},
                            .get_values = read_joystick,
                            .log_level = espp::Logger::Verbosity::WARN},
        .log_level = espp::Logger::Verbosity::WARN});
    // and finally, make the task to periodically poll the controller and print
    // the state
    auto task_fn = [&quit_test, &controller](std::mutex &m, std::condition_variable &cv) {
      controller.update();
      bool is_a_pressed = controller.is_pressed(espp::Controller::Button::A);
      bool is_b_pressed = controller.is_pressed(espp::Controller::Button::B);
      bool is_select_pressed = controller.is_pressed(espp::Controller::Button::SELECT);
      bool is_start_pressed = controller.is_pressed(espp::Controller::Button::START);
      bool is_up_pressed = controller.is_pressed(espp::Controller::Button::UP);
      bool is_down_pressed = controller.is_pressed(espp::Controller::Button::DOWN);
      bool is_left_pressed = controller.is_pressed(espp::Controller::Button::LEFT);
      bool is_right_pressed = controller.is_pressed(espp::Controller::Button::RIGHT);
      fmt::print("Controller buttons:\n"
                 "\tA:      {}\n"
                 "\tB:      {}\n"
                 "\tSelect: {}\n"
                 "\tStart:  {}\n"
                 "\tUp:     {}\n"
                 "\tDown:   {}\n"
                 "\tLeft:   {}\n"
                 "\tRight:  {}\n",
                 is_a_pressed, is_b_pressed, is_select_pressed, is_start_pressed, is_up_pressed,
                 is_down_pressed, is_left_pressed, is_right_pressed);
      quit_test = is_start_pressed && is_select_pressed;
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 500ms);
      }
      // we don't want to stop, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config =
                                {
                                    .name = "Controller Task",
                                    .stack_size_bytes = 6 * 1024,
                                },
                            .log_level = espp::Logger::Verbosity::WARN});
    task.start();

I2C Analog Controller Example

    // make the I2C that we'll use to communicate
    espp::I2c i2c({
        .port = I2C_NUM_1,
        .sda_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SDA_GPIO,
        .scl_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SCL_GPIO,
    });
    // make the actual ads class
    espp::Ads1x15 ads(espp::Ads1x15::Ads1015Config{
        .write = std::bind(&espp::I2c::write, &i2c, std::placeholders::_1, std::placeholders::_2,
                           std::placeholders::_3),
        .read = std::bind(&espp::I2c::read, &i2c, std::placeholders::_1, std::placeholders::_2,
                          std::placeholders::_3)});
    // make the task which will get the raw data from the I2C ADC and convert to
    // uncalibrated [-1,1]
    std::atomic<float> joystick_x{0};
    std::atomic<float> joystick_y{0};
    auto ads_read_task_fn = [&joystick_x, &joystick_y, &ads](std::mutex &m,
                                                             std::condition_variable &cv) {
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        using namespace std::chrono_literals;
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 20ms);
      }
      std::error_code ec;
      auto x_mv = ads.sample_mv(1, ec);
      if (ec) {
        fmt::print("error reading x: {}\n", ec.message());
        return false;
      }
      auto y_mv = ads.sample_mv(0, ec);
      if (ec) {
        fmt::print("error reading y: {}\n", ec.message());
        return false;
      }
      joystick_x.store(x_mv / 1700.0f - 1.0f);
      // y is inverted so negate it
      joystick_y.store(-(y_mv / 1700.0f - 1.0f));
      // we don't want to stop, so return false
      return false;
    };
    auto ads_task = espp::Task::make_unique({.callback = ads_read_task_fn,
                                             .task_config =
                                                 {
                                                     .name = "ADS Task",
                                                     .stack_size_bytes{4 * 1024},
                                                 },
                                             .log_level = espp::Logger::Verbosity::INFO});
    ads_task->start();
    // make the read joystick function used by the controller
    auto read_joystick = [&joystick_x, &joystick_y](float *x, float *y) -> bool {
      *x = joystick_x.load();
      *y = joystick_y.load();
      return true;
    };
    // make the controller - NOTE: this was designed for connecting the Adafruit
    // JoyBonnet to the ESP32 S3 BOX
    espp::Controller controller(espp::Controller::AnalogJoystickConfig{
        // buttons short to ground, so they are active low. this will enable the
        // GPIO_PULLUP and invert the logic
        .active_low = true,
        .gpio_a = 38,      // pin 32 on the joybonnet
        .gpio_b = 39,      // pin 31 on the joybonnet
        .gpio_x = -1,      // pin 36 on the joybonnet
        .gpio_y = -1,      // pin 33 on the joybonnet
        .gpio_start = 42,  // pin 37 on the joybonnet
        .gpio_select = 21, // pin 38 on the joybonnet
        .gpio_joystick_select = -1,
        .joystick_config = {.x_calibration = {.center = 0.0f,
                                              .center_deadband = 0.2f,
                                              .minimum = -1.0f,
                                              .maximum = 1.0f},
                            .y_calibration = {.center = 0.0f,
                                              .center_deadband = 0.2f,
                                              .minimum = -1.0f,
                                              .maximum = 1.0f},
                            .get_values = read_joystick,
                            .log_level = espp::Logger::Verbosity::WARN},
        .log_level = espp::Logger::Verbosity::WARN});
    // and finally, make the task to periodically poll the controller and print
    // the state
    auto task_fn = [&quit_test, &controller](std::mutex &m, std::condition_variable &cv) {
      controller.update();
      bool is_a_pressed = controller.is_pressed(espp::Controller::Button::A);
      bool is_b_pressed = controller.is_pressed(espp::Controller::Button::B);
      bool is_select_pressed = controller.is_pressed(espp::Controller::Button::SELECT);
      bool is_start_pressed = controller.is_pressed(espp::Controller::Button::START);
      bool is_up_pressed = controller.is_pressed(espp::Controller::Button::UP);
      bool is_down_pressed = controller.is_pressed(espp::Controller::Button::DOWN);
      bool is_left_pressed = controller.is_pressed(espp::Controller::Button::LEFT);
      bool is_right_pressed = controller.is_pressed(espp::Controller::Button::RIGHT);
      fmt::print("Controller buttons:\n"
                 "\tA:      {}\n"
                 "\tB:      {}\n"
                 "\tSelect: {}\n"
                 "\tStart:  {}\n"
                 "\tUp:     {}\n"
                 "\tDown:   {}\n"
                 "\tLeft:   {}\n"
                 "\tRight:  {}\n",
                 is_a_pressed, is_b_pressed, is_select_pressed, is_start_pressed, is_up_pressed,
                 is_down_pressed, is_left_pressed, is_right_pressed);
      quit_test = is_start_pressed && is_select_pressed;
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 500ms);
      }
      // we don't want to stop, return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config =
                                {
                                    .name = "Controller Task",
                                    .stack_size_bytes = 6 * 1024,
                                },
                            .log_level = espp::Logger::Verbosity::WARN});
    task.start();

Public Types

enum class Button : int

The buttons that the controller supports.

Values:

enumerator A
enumerator B
enumerator X
enumerator Y
enumerator SELECT
enumerator START
enumerator UP
enumerator DOWN
enumerator LEFT
enumerator RIGHT
enumerator JOYSTICK_SELECT
enumerator LAST_UNUSED

Public Functions

inline explicit Controller(const DigitalConfig &config)

Create a Digital controller.

inline explicit Controller(const AnalogJoystickConfig &config)

Create an analog joystick controller.

inline explicit Controller(const DualConfig &config)

Create a dual d-pad + analog joystick controller.

inline ~Controller()

Destroys the controller and deletes the associated dedicated GPIO bundle.

inline State get_state()

Get the most recent state structure for the controller.

Returns

State structure for the inputs - updated when update() was last called.

inline bool is_pressed(const Button input)

Return true if the input was pressed, false otherwise.

Parameters

input – The Button of interest.

Returns

True if input was pressed last time update() was called.

inline void update()

Read the current button values and update the internal input state accordingly.

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters

rate_limit – The rate limit to use for the logger

struct AnalogJoystickConfig

Configuration for the controller to be used with a joystick (which has a center-press / select button), no d-pad.

Note

In this configuration, the controller will create and manage a joystick component to read the analog axes of the joystick and convert them into digital up/down/left/right signals as a virtual d-pad.

Public Members

bool active_low = {true}

Whether the buttons are active-low (default) or not.

int gpio_a = {-1}

GPIO for the A button.

int gpio_b = {-1}

GPIO for the B button.

int gpio_x = {-1}

GPIO for the X button.

int gpio_y = {-1}

GPIO for the Y button.

int gpio_start = {-1}

GPIO for the START button.

int gpio_select = {-1}

GPIO for the SELECT button.

int gpio_joystick_select = {-1}

GPIO for the JOYSTICK SELECT button.

espp::Joystick::Config joystick_config

Configuration for the analog joystick which will be used to read digital direction values.

espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}

Verbosity for the logger.

struct DigitalConfig

Configuration for the controller to use d-pad only, no joystick.

Public Members

bool active_low = {true}

Whether the buttons are active-low (default) or not.

int gpio_a = {-1}

GPIO for the A button.

int gpio_b = {-1}

GPIO for the B button.

int gpio_x = {-1}

GPIO for the X button.

int gpio_y = {-1}

GPIO for the Y button.

int gpio_start = {-1}

GPIO for the START button.

int gpio_select = {-1}

GPIO for the SELECT button.

int gpio_up = {-1}

GPIO for the UP button.

int gpio_down = {-1}

GPIO for the DOWN button.

int gpio_left = {-1}

GPIO for the LEFT button.

int gpio_right = {-1}

GPIO for the RIGHT button.

espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}

Verbosity for the logger.

struct DualConfig

Configuration for the controller to be used with both a joystick (which has a center-press / select button), and a d-pad.

Note

In this configuration, the controller will NOT create / manage a joystick component. The configured d-pad provides the digital directions so the analog joystick values should be retrieved from a separately managed joystick component.

Public Members

bool active_low = {true}

Whether the buttons are active-low (default) or not.

int gpio_a = {-1}

GPIO for the A button.

int gpio_b = {-1}

GPIO for the B button.

int gpio_x = {-1}

GPIO for the X button.

int gpio_y = {-1}

GPIO for the Y button.

int gpio_start = {-1}

GPIO for the START button.

int gpio_select = {-1}

GPIO for the SELECT button.

int gpio_up = {-1}

GPIO for the UP button.

int gpio_down = {-1}

GPIO for the DOWN button.

int gpio_left = {-1}

GPIO for the LEFT button.

int gpio_right = {-1}

GPIO for the RIGHT button.

int gpio_joystick_select = {-1}

GPIO for the JOYSTICK SELECT button.

espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}

Verbosity for the logger.

struct State

Packed bit structure containing the state (pressed=1) of each button.

Public Members

uint32_t a

State of the A button.

uint32_t b

State of the B button.

uint32_t x

State of the X button.

uint32_t y

State of the Y button.

uint32_t select

State of the SELECT button.

uint32_t start

State of the START button.

uint32_t up

State of the UP button.

uint32_t down

State of the DOWN button.

uint32_t left

State of the LEFT button.

uint32_t right

State of the RIGHT button.

uint32_t joystick_select

State of the Joystick Select button.