Bluetooth key bindings and custom actions
This document explains how Bluetooth key bindings work with the dynamic dispatcher system and how actions are provided to users.
Overview
Bluetooth key binding support allows users to map buttons on Bluetooth input devices (remotes, keyboards, etc.) to KOReader actions. The system dynamically extracts all available actions from KOReader’s dispatcher at runtime, providing users with access to hundreds of actions without manual maintenance.
Architecture
The key binding system consists of several components:
1. bluetooth_keybindings.lua
The main module that:
- Manages button press event handling from Bluetooth devices
- Stores and retrieves key bindings per device (MAC address)
- Triggers the appropriate KOReader events when buttons are pressed
- Provides UI for users to configure bindings
2. lib/bluetooth/available_actions.lua
Dynamically loads all available actions at runtime by:
- Extracting actions from KOReader’s dispatcher using
dispatcher_helper - Organizing actions into categories (General, Device, Screen, Reader, etc.)
- Providing fallback static actions if dynamic extraction fails
- Merging essential actions that require specific arguments (e.g.,
args = 1orargs = -1)
3. lib/bluetooth/dispatcher_helper.lua
Helper module that extracts dispatcher actions using introspection:
- Uses
debug.getupvalue()to access dispatcher’s internalsettingsListanddispatcher_menu_order - Returns actions organized by category with metadata (event names, titles, arguments, etc.)
- Caches results for performance
- Returns
nilif extraction fails (triggering static fallback)
How Actions Are Loaded
Dynamic Loading (Primary Method)
available_actions.luacallsdispatcher_helper.get_dispatcher_actions_ordered()- The helper introspects KOReader’s dispatcher module to extract all registered actions
- Actions are organized into categories based on their category flags
- Essential actions with custom arguments are merged on top (overriding extracted versions)
- The final categorized list is returned
Static Fallback (Backup Method)
If dynamic extraction fails:
- A minimal static list of core navigation and UI actions is used
- Essential actions are merged with static fallback actions
- Actions are organized into the same category structure
Essential Actions
Certain actions require specific arguments that aren’t provided by the dispatcher (e.g., args = 1
for next page vs args = -1 for previous page). These are defined in _get_essential_actions() and
always override extracted actions:
next_page/prev_page- Page navigation with directionincrease_font/decrease_font- Font size adjustmentincrease_frontlight/decrease_frontlight- Brightness controlincrease_frontlight_warmth/decrease_frontlight_warmth- Warmth control
Action Structure
Each action extracted from the dispatcher has the following structure:
id: Unique identifier for the action (from dispatcher)title: Display name shown to users in the UI (translated)event: KOReader event name to trigger when the button is pressedargs: Optional arguments to pass to the eventargs_func: Optional function to generate arguments dynamicallytoggle: Optional toggle state for toggle-type actionscategory: String category name (for logging/debugging)- Category flags:
general,device,screen,filemanager,reader,rolling,paging
Example action from dispatcher:
{
id = "show_menu",
title = "Show menu",
event = "ShowMenu",
description = "Show menu",
general = true,
reader = true,
}
Adding New Actions
Using KOReader’s Dispatcher
Preferred method: Register your action with KOReader’s dispatcher system. The Bluetooth key binding system will automatically detect and expose it to users.
In your plugin or KOReader module:
local Dispatcher = require("dispatcher")
Dispatcher:registerAction("my_custom_action", {
category = "none",
event = "MyCustomEvent",
title = _("My Custom Action"),
general = true, -- Shows in General category
reader = true, -- Shows in Reader category
})
The action will automatically appear in the Bluetooth key binding configuration UI once registered.
Adding Essential Actions
If your action requires specific arguments that the dispatcher doesn’t provide (e.g., directional
arguments like 1 vs -1), add it to _get_essential_actions() in
src/lib/bluetooth/available_actions.lua:
{
id = "my_directional_action",
title = _("My Action (Forward)"),
event = "MyEvent",
args = 1, -- Custom argument
description = _("Description of what this does"),
reader = true, -- Category flag
},
Essential actions override any dispatcher-provided actions with the same ID and category.
How Key Bindings Work Internally
- User presses a button on the Bluetooth device
- The
InputDeviceHandlerdetects the button press via the device’s input event interface - CRITICAL: The handler executes
UIManager.event_hook:execute("InputEvent")to notify other components of user activity - The handler looks up the configured action ID for that button (format:
"category:action_id") - The action is retrieved from the action lookup map (pre-built at module load time)
- The corresponding KOReader event is triggered with any arguments or argument functions
- KOReader processes the event normally
InputEvent Hook for Autosuspend Integration
When a Bluetooth key event is received, the code must execute the InputEvent hook:
UIManager.event_hook:execute("InputEvent")
This is essential for KOReader’s autosuspend plugin to work correctly with Bluetooth input. The autosuspend plugin relies on this hook to detect user activity and reset its standby timer. Without this call, the device would go into standby even while the user is actively using Bluetooth controls.
The hook is called in BluetoothKeyBindings:onBluetoothKeyEvent() immediately when a key press
event is received, before processing the key binding. This ensures timely notification of user
activity to all interested components.
When implementing new input handlers or modifying key event processing, always ensure this hook is called to maintain proper autosuspend behavior.