Files
tactility_apps/Apps/MediaKeys/main/Source/MediaKeys.cpp
T
Shadowtrance dbf850c434 Fixes and new apps (#30)
- M5 Unit Modules library + M5 Unit Test app
- Minor fixes for TodoList, TwoEleven, Snake, Brainfuck and Breakout
- Fixed SerialConsole to use the uart controller as it was broken in one of the many updates
- Fixed keyboard input in TwoEleven, Breakout, Magic8Ball and Snake
- Bluetooth Media Keys app, supports pressing physical keys to trigger the corresponding buttonmatrix button
- Epub Reader app
2026-06-07 15:56:32 +02:00

348 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "MediaKeys.h"
#include <tactility/log.h>
#include <esp_heap_caps.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <tactility/lvgl_fonts.h>
#include <tt_lvgl.h>
#include <tt_lvgl_toolbar.h>
static const char* TAG = "MediaKeys";
// Button layout: two rows of three media-control buttons
static const char* BTN_MAP[] = {
LV_SYMBOL_PREV, LV_SYMBOL_PLAY, LV_SYMBOL_NEXT, "\n",
LV_SYMBOL_MUTE, LV_SYMBOL_VOLUME_MID, LV_SYMBOL_VOLUME_MAX, ""
};
// Physical key → button matrix index mapping (B=prev, P=play, N=next, M=mute, D=vol-, U=vol+)
static const struct { uint32_t key; uint32_t btnIdx; } KEY_MAP[] = {
{ 'b', 0 }, { 'B', 0 },
{ 'p', 1 }, { 'P', 1 },
{ 'n', 2 }, { 'N', 2 },
{ 'm', 3 }, { 'M', 3 },
{ 'd', 4 }, { 'D', 4 },
{ 'u', 5 }, { 'U', 5 },
};
// HID Consumer Page (0x0C) usage codes for each button, in BTN_MAP order
static const uint16_t CONSUMER_USAGE[6] = {
0x00B6, // 0 PREV: Previous Track
0x00CD, // 1 PLAY: Play/Pause
0x00B5, // 2 NEXT: Next Track
0x00E2, // 3 MUTE: Mute
0x00EA, // 4 VOL_DOWN: Volume Decrement
0x00E9, // 5 VOL_UP: Volume Increment
};
// Data passed to the one-shot key-send task
struct SendKeyData {
struct Device* hidDevice;
uint16_t usage;
};
// ---- Static task / callback implementations ----
void MediaKeys::sendKeyTask(void* param) {
SendKeyData* data = static_cast<SendKeyData*>(param);
uint8_t press[2] = {
static_cast<uint8_t>(data->usage & 0xFF),
static_cast<uint8_t>((data->usage >> 8) & 0xFF)
};
bluetooth_hid_device_send_consumer(data->hidDevice, press, 2);
vTaskDelay(pdMS_TO_TICKS(50));
uint8_t release[2] = {0, 0};
bluetooth_hid_device_send_consumer(data->hidDevice, release, 2);
delete data;
vTaskDelete(nullptr);
}
void MediaKeys::onSwitchToggled(lv_event_t* event) {
if (lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return;
MediaKeys* self = static_cast<MediaKeys*>(lv_event_get_user_data(event));
if (!self) return;
bool enabled = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED);
self->handleSwitchToggle(enabled);
}
void MediaKeys::onButtonPressed(lv_event_t* event) {
if (lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return;
MediaKeys* self = static_cast<MediaKeys*>(lv_event_get_user_data(event));
if (!self) return;
uint32_t id = lv_btnmatrix_get_selected_btn(lv_event_get_target_obj(event));
if (id != LV_BTNMATRIX_BTN_NONE) {
self->handleButtonPress(id);
}
}
void MediaKeys::onKeyEvent(lv_event_t* event) {
if (lv_event_get_code(event) != LV_EVENT_KEY) return;
MediaKeys* self = static_cast<MediaKeys*>(lv_event_get_user_data(event));
if (!self || !self->_buttonMatrix) return;
uint32_t key = lv_event_get_key(event);
// Q or Esc exits key mode and returns focus to normal UI navigation
if (key == 'q' || key == 'Q' || key == LV_KEY_ESC) {
self->exitKeyMode();
return;
}
if (!self->_isEnabled) return;
for (auto& mapping : KEY_MAP) {
if (mapping.key == key) {
// Highlight: select the button and mark checked for visual feedback
self->_activeKeyBtn = mapping.btnIdx;
lv_buttonmatrix_set_selected_button(self->_buttonMatrix, mapping.btnIdx);
lv_buttonmatrix_set_button_ctrl(self->_buttonMatrix, mapping.btnIdx, LV_BTNMATRIX_CTRL_CHECKED);
// Restart the highlight-clear timer
if (self->_keyHighlightTimer) {
lv_timer_reset(self->_keyHighlightTimer);
lv_timer_resume(self->_keyHighlightTimer);
}
self->handleButtonPress(mapping.btnIdx);
return;
}
}
}
void MediaKeys::enterKeyMode() {
if (_keyboardActive || !_buttonMatrix) return;
lv_group_t* group = lv_group_get_default();
if (!group) return;
lv_group_add_obj(group, _buttonMatrix);
lv_group_focus_obj(_buttonMatrix);
lv_group_set_editing(group, true);
_keyboardActive = true;
LOG_I(TAG, "Key mode: ON (Q/Esc to exit)");
}
void MediaKeys::exitKeyMode() {
if (!_keyboardActive || !_buttonMatrix) return;
lv_group_t* group = lv_group_get_default();
if (group) lv_group_set_editing(group, false);
lv_group_remove_obj(_buttonMatrix);
_keyboardActive = false;
LOG_I(TAG, "Key mode: OFF");
}
void MediaKeys::onKeyHighlightTimer(lv_timer_t* t) {
MediaKeys* self = static_cast<MediaKeys*>(lv_timer_get_user_data(t));
if (!self || !self->_buttonMatrix) return;
if (self->_activeKeyBtn != LV_BTNMATRIX_BTN_NONE) {
lv_buttonmatrix_clear_button_ctrl(self->_buttonMatrix, self->_activeKeyBtn, LV_BTNMATRIX_CTRL_CHECKED);
self->_activeKeyBtn = LV_BTNMATRIX_BTN_NONE;
}
lv_obj_remove_state(self->_buttonMatrix, LV_STATE_FOCUSED);
lv_timer_pause(t);
}
void MediaKeys::btEventCallback(struct Device* /*device*/, void* context, struct BtEvent event) {
MediaKeys* self = static_cast<MediaKeys*>(context);
if (!self) return;
if (event.type == BT_EVENT_RADIO_STATE_CHANGED) {
LOG_I(TAG, "BT radio state: %d", (int)event.radio_state);
if (event.radio_state == BT_RADIO_STATE_ON) {
// Radio is now up - start HID (needs LVGL lock for UI update)
if (tt_lvgl_lock(1000)) {
// Re-check inside lock to avoid TOCTOU race with handleSwitchToggle(false)
if (self->_radioEnabling) {
self->startHid();
}
tt_lvgl_unlock();
}
} else if (event.radio_state == BT_RADIO_STATE_OFF && self->_isEnabled) {
// Radio dropped while we were active - revert UI
LOG_I(TAG, "BT radio turned off, disabling HID");
if (tt_lvgl_lock(1000)) {
if (tt_lvgl_hardware_keyboard_is_available()) self->exitKeyMode();
self->_hidDevice = nullptr;
self->_isEnabled = false;
self->_radioEnabling = false;
if (self->_switchWidget) lv_obj_remove_state(self->_switchWidget, LV_STATE_CHECKED);
if (self->_mainWrapper) lv_obj_add_flag(self->_mainWrapper, LV_OBJ_FLAG_HIDDEN);
tt_lvgl_unlock();
}
}
} else if (event.type == BT_EVENT_PROFILE_STATE_CHANGED && event.profile_state.profile == BT_PROFILE_HID_DEVICE) {
LOG_I(TAG, "HID device: %s", event.profile_state.state == BT_PROFILE_STATE_CONNECTED ? "connected" : "disconnected");
}
}
// ---- Instance methods ----
void MediaKeys::startHid() {
// Called once the BT radio is confirmed ON (either already was, or just came up).
// May be called from the BT event callback thread - LVGL must already be locked by caller.
_radioEnabling = false;
_hidDevice = bluetooth_hid_device_get_device();
if (!_hidDevice) {
LOG_E(TAG, "BLE HID device unavailable after radio on");
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
_btDevice = nullptr;
_isEnabled = false;
if (_switchWidget) lv_obj_remove_state(_switchWidget, LV_STATE_CHECKED);
return;
}
error_t err = bluetooth_hid_device_start(_hidDevice, BT_HID_DEVICE_MODE_KEYBOARD);
if (err != ERROR_NONE) {
LOG_E(TAG, "Failed to start HID device: %d", (int)err);
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
_btDevice = nullptr;
_hidDevice = nullptr;
_isEnabled = false;
if (_switchWidget) lv_obj_remove_state(_switchWidget, LV_STATE_CHECKED);
return;
}
if (_mainWrapper) lv_obj_remove_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
if (tt_lvgl_hardware_keyboard_is_available()) enterKeyMode();
}
void MediaKeys::handleSwitchToggle(bool enabled) {
LOG_I(TAG, "Switch: %s", enabled ? "ON" : "OFF");
_isEnabled = enabled;
if (enabled) {
_btDevice = bluetooth_find_first_ready_device();
if (!_btDevice) {
LOG_E(TAG, "No Bluetooth device found");
_isEnabled = false;
if (_switchWidget) lv_obj_remove_state(_switchWidget, LV_STATE_CHECKED);
return;
}
bluetooth_set_device_name(_btDevice, "Tactility Media Keys");
// Register callback before enabling radio so we don't miss the state-change event.
bluetooth_add_event_callback(_btDevice, this, btEventCallback);
enum BtRadioState radioState;
bluetooth_get_radio_state(_btDevice, &radioState);
if (radioState == BT_RADIO_STATE_ON) {
// Radio already up - start HID immediately.
_radioWasOff = false;
startHid();
} else {
// Turn the radio on; startHid() will be called from btEventCallback
// once BT_RADIO_STATE_ON fires.
LOG_I(TAG, "BT radio not on (state=%d), enabling...", (int)radioState);
_radioWasOff = true;
_radioEnabling = true;
bluetooth_set_radio_enabled(_btDevice, true);
}
} else {
_radioEnabling = false;
if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode();
if (_hidDevice) bluetooth_hid_device_stop(_hidDevice);
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false);
_radioWasOff = false;
_btDevice = nullptr;
_hidDevice = nullptr;
if (_mainWrapper) lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
}
}
void MediaKeys::handleButtonPress(uint32_t buttonId) {
if (!_hidDevice || !_isEnabled || buttonId >= 6) return;
if (!bluetooth_hid_device_is_connected(_hidDevice)) {
LOG_W(TAG, "Not connected, ignoring button %lu", buttonId);
return;
}
LOG_I(TAG, "Button %lu pressed", buttonId);
SendKeyData* data = new SendKeyData {_hidDevice, CONSUMER_USAGE[buttonId]};
if (xTaskCreate(sendKeyTask, "bt_key", 4096, data, tskIDLE_PRIORITY + 1, nullptr) != pdPASS) {
LOG_E(TAG, "Failed to create send task");
delete data;
}
}
void MediaKeys::onShow(AppHandle appHandle, lv_obj_t* parent) {
_parent = parent;
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
_switchWidget = tt_lvgl_toolbar_add_switch_action(toolbar);
lv_obj_add_event_cb(_switchWidget, onSwitchToggled, LV_EVENT_VALUE_CHANGED, this);
_mainWrapper = lv_obj_create(parent);
lv_obj_set_style_bg_color(_mainWrapper, lv_palette_darken(LV_PALETTE_GREY, 4), LV_PART_MAIN);
lv_obj_set_width(_mainWrapper, LV_PCT(100));
lv_obj_set_flex_grow(_mainWrapper, 1);
lv_obj_set_style_pad_all(_mainWrapper, 4, LV_PART_MAIN);
lv_obj_set_style_border_width(_mainWrapper, 0, LV_PART_MAIN);
lv_obj_remove_flag(_mainWrapper, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(_mainWrapper, LV_FLEX_FLOW_COLUMN);
_buttonMatrix = lv_btnmatrix_create(_mainWrapper);
lv_btnmatrix_set_map(_buttonMatrix, BTN_MAP);
lv_obj_set_style_text_font(_buttonMatrix, lvgl_get_text_font(FONT_SIZE_LARGE), LV_PART_ITEMS);
lv_obj_set_style_pad_all(_buttonMatrix, 5, LV_PART_MAIN);
lv_obj_set_style_pad_row(_buttonMatrix, 5, LV_PART_MAIN);
lv_obj_set_style_pad_column(_buttonMatrix, 5, LV_PART_MAIN);
lv_obj_set_style_border_width(_buttonMatrix, 0, LV_PART_MAIN);
lv_obj_set_style_bg_opa(_buttonMatrix, 0, LV_PART_MAIN);
if (lv_display_get_horizontal_resolution(nullptr) <= 240 ||
lv_display_get_vertical_resolution(nullptr) <= 240) {
lv_obj_set_size(_buttonMatrix, lv_pct(100), lv_pct(70));
} else {
lv_obj_set_size(_buttonMatrix, lv_pct(100), lv_pct(85));
}
lv_obj_set_flex_grow(_buttonMatrix, 1);
lv_obj_add_event_cb(_buttonMatrix, onButtonPressed, LV_EVENT_VALUE_CHANGED, this);
// Physical keyboard support: key events on the matrix (entered when BT enabled, Q/Esc exits)
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_obj_add_event_cb(_buttonMatrix, onKeyEvent, LV_EVENT_KEY, this);
_keyHighlightTimer = lv_timer_create(onKeyHighlightTimer, 150, this);
lv_timer_pause(_keyHighlightTimer);
}
lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
}
void MediaKeys::onHide(AppHandle /*appHandle*/) {
if (_hidDevice) bluetooth_hid_device_stop(_hidDevice);
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false);
_btDevice = nullptr;
_hidDevice = nullptr;
_isEnabled = false;
_radioEnabling = false;
_radioWasOff = false;
if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode();
if (_keyHighlightTimer) {
lv_timer_delete(_keyHighlightTimer);
_keyHighlightTimer = nullptr;
}
_activeKeyBtn = LV_BTNMATRIX_BTN_NONE;
_parent = nullptr;
_mainWrapper = nullptr;
_switchWidget = nullptr;
_buttonMatrix = nullptr;
}