223 lines
6.7 KiB
C++
223 lines
6.7 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include <drivers/button_control.h>
|
|
|
|
#include <button_control_module.h>
|
|
|
|
#include <tactility/device.h>
|
|
#include <tactility/driver.h>
|
|
#include <tactility/drivers/gpio.h>
|
|
#include <tactility/drivers/gpio_controller.h>
|
|
#include <tactility/drivers/keyboard.h>
|
|
#include <tactility/error.h>
|
|
#include <tactility/log.h>
|
|
#include <tactility/time.h>
|
|
|
|
#include <lvgl.h>
|
|
|
|
#include <cstdlib>
|
|
|
|
#define TAG "ButtonControl"
|
|
#define GET_CONFIG(device) (static_cast<const ButtonControlConfig*>((device)->config))
|
|
|
|
// Worst case: both buttons complete a short/long press gesture (press + release, 2 events each)
|
|
// within the same read_key() polling interval.
|
|
constexpr auto BUTTON_CONTROL_PENDING_CAPACITY = 4;
|
|
|
|
struct ButtonControlPendingEvent {
|
|
uint32_t key;
|
|
bool pressed;
|
|
};
|
|
|
|
struct ButtonControlButtonState {
|
|
GpioDescriptor* descriptor;
|
|
bool in_use;
|
|
bool debounced_pressed;
|
|
uint32_t press_start_time;
|
|
uint32_t last_change_time;
|
|
uint32_t short_press_key;
|
|
uint32_t long_press_key;
|
|
};
|
|
|
|
struct ButtonControlInternal {
|
|
ButtonControlButtonState primary;
|
|
ButtonControlButtonState secondary;
|
|
ButtonControlPendingEvent pending[BUTTON_CONTROL_PENDING_CAPACITY];
|
|
uint8_t pending_head;
|
|
uint8_t pending_count;
|
|
};
|
|
|
|
static void push_pending(ButtonControlInternal* internal, uint32_t key, bool pressed) {
|
|
if (internal->pending_count >= BUTTON_CONTROL_PENDING_CAPACITY) {
|
|
LOG_W(TAG, "Pending event queue full, dropping event");
|
|
return;
|
|
}
|
|
uint8_t tail = (internal->pending_head + internal->pending_count) % BUTTON_CONTROL_PENDING_CAPACITY;
|
|
internal->pending[tail] = { .key = key, .pressed = pressed };
|
|
internal->pending_count++;
|
|
}
|
|
|
|
static bool pop_pending(ButtonControlInternal* internal, ButtonControlPendingEvent* out_event) {
|
|
if (internal->pending_count == 0) {
|
|
return false;
|
|
}
|
|
*out_event = internal->pending[internal->pending_head];
|
|
internal->pending_head = (internal->pending_head + 1) % BUTTON_CONTROL_PENDING_CAPACITY;
|
|
internal->pending_count--;
|
|
return true;
|
|
}
|
|
|
|
// region Driver lifecycle
|
|
|
|
static error_t acquire_button(const GpioPinSpec& pin, uint32_t short_press_key, uint32_t long_press_key, ButtonControlButtonState* out_state) {
|
|
if (pin.gpio_controller == nullptr) {
|
|
*out_state = {};
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
auto* descriptor = gpio_descriptor_acquire(pin.gpio_controller, pin.pin, pin.flags | GPIO_FLAG_DIRECTION_INPUT, GPIO_OWNER_GPIO);
|
|
if (descriptor == nullptr) {
|
|
LOG_E(TAG, "Failed to acquire GPIO descriptor");
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
*out_state = {
|
|
.descriptor = descriptor,
|
|
.in_use = true,
|
|
.debounced_pressed = false,
|
|
.press_start_time = 0,
|
|
.last_change_time = 0,
|
|
.short_press_key = short_press_key,
|
|
.long_press_key = long_press_key,
|
|
};
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static error_t start(Device* device) {
|
|
const auto* config = GET_CONFIG(device);
|
|
|
|
auto* internal = static_cast<ButtonControlInternal*>(malloc(sizeof(ButtonControlInternal)));
|
|
if (internal == nullptr) {
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
*internal = {};
|
|
|
|
bool two_button_mode = config->pin_secondary.gpio_controller != nullptr;
|
|
|
|
error_t error = acquire_button(
|
|
config->pin_primary,
|
|
two_button_mode ? LV_KEY_ENTER : LV_KEY_NEXT,
|
|
two_button_mode ? LV_KEY_ESC : LV_KEY_ENTER,
|
|
&internal->primary
|
|
);
|
|
if (error != ERROR_NONE) {
|
|
free(internal);
|
|
return error;
|
|
}
|
|
|
|
error = acquire_button(config->pin_secondary, LV_KEY_NEXT, LV_KEY_PREV, &internal->secondary);
|
|
if (error != ERROR_NONE) {
|
|
if (internal->primary.in_use) {
|
|
gpio_descriptor_release(internal->primary.descriptor);
|
|
}
|
|
free(internal);
|
|
return error;
|
|
}
|
|
|
|
device_set_driver_data(device, internal);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static error_t stop(Device* device) {
|
|
auto* internal = static_cast<ButtonControlInternal*>(device_get_driver_data(device));
|
|
|
|
if (internal->primary.in_use) {
|
|
gpio_descriptor_release(internal->primary.descriptor);
|
|
}
|
|
if (internal->secondary.in_use) {
|
|
gpio_descriptor_release(internal->secondary.descriptor);
|
|
}
|
|
|
|
free(internal);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region KeyboardApi
|
|
|
|
// Long press is only distinguished from short press at release time, by how long the button
|
|
// was held - not at the moment it's first pressed. This matches physical button behavior:
|
|
// you can't know a press is "long" until it either ends or the threshold is reached.
|
|
static void poll_button(const ButtonControlConfig* config, ButtonControlInternal* internal, ButtonControlButtonState* state) {
|
|
if (!state->in_use) {
|
|
return;
|
|
}
|
|
|
|
bool high = false;
|
|
if (gpio_descriptor_get_level(state->descriptor, &high) != ERROR_NONE) {
|
|
return;
|
|
}
|
|
bool raw_pressed = config->active_low ? !high : high;
|
|
|
|
uint32_t now = get_millis();
|
|
if ((now - state->last_change_time) < config->debounce_ms) {
|
|
return;
|
|
}
|
|
|
|
if (raw_pressed == state->debounced_pressed) {
|
|
return;
|
|
}
|
|
state->last_change_time = now;
|
|
state->debounced_pressed = raw_pressed;
|
|
|
|
if (raw_pressed) {
|
|
state->press_start_time = now;
|
|
return;
|
|
}
|
|
|
|
// Release: decide short vs. long press by elapsed hold duration, then queue a synthetic
|
|
// key tap (press followed by release) for the LVGL key this gesture maps to.
|
|
uint32_t held_ms = now - state->press_start_time;
|
|
uint32_t key = held_ms < config->long_press_ms ? state->short_press_key : state->long_press_key;
|
|
push_pending(internal, key, true);
|
|
push_pending(internal, key, false);
|
|
}
|
|
|
|
static error_t button_control_read_key(Device* device, KeyboardKeyData* data) {
|
|
const auto* config = GET_CONFIG(device);
|
|
auto* internal = static_cast<ButtonControlInternal*>(device_get_driver_data(device));
|
|
|
|
poll_button(config, internal, &internal->primary);
|
|
poll_button(config, internal, &internal->secondary);
|
|
|
|
ButtonControlPendingEvent event;
|
|
if (pop_pending(internal, &event)) {
|
|
data->key = event.key;
|
|
data->pressed = event.pressed;
|
|
data->continue_reading = internal->pending_count > 0;
|
|
} else {
|
|
data->key = 0;
|
|
data->pressed = false;
|
|
data->continue_reading = false;
|
|
}
|
|
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// endregion
|
|
|
|
static constexpr KeyboardApi button_control_api = {
|
|
.read_key = button_control_read_key,
|
|
};
|
|
|
|
Driver button_control_driver = {
|
|
.name = "button_control",
|
|
.compatible = (const char*[]) { "tactility,button-control", nullptr },
|
|
.start_device = start,
|
|
.stop_device = stop,
|
|
.api = &button_control_api,
|
|
.device_type = &KEYBOARD_TYPE,
|
|
.owner = &button_control_module,
|
|
.internal = nullptr
|
|
};
|