Introduction
The Kobo Plugin for KOReader enables seamless integration with your Kobo device’s native library. This plugin creates a virtual library that allows you to browse and read kepub books from your Kobo’s Nickel OS directly within KOReader, while maintaining synchronization of reading progress between both applications.
What is this plugin?
This plugin bridges the gap between KOReader and Kobo’s native reading experience by:
- Creating a virtual library from your Kobo’s kepub collection
- Syncing reading progress bidirectionally between KOReader and Kobo
- Providing seamless access to kepub files without manual file management
- Maintaining compatibility with Kobo’s native reading features
Key Benefits
Komga / Calibre Web Users
- Streamline your workflow by syncing books only through Kobo’s native system
- Read Komga or Calibre Web synced books directly in KOReader without duplicate setup
- Let Kobo handle progress synchronization while enjoying KOReader’s enhanced reading features
Bluetooth Page Turners and Keyboards
- Manage Bluetooth devices directly from KOReader
- Connect to paired Bluetooth devices using dispatcher actions
- Configure key bindings on Bluetooth input devices for custom actions
Requirements
- Kobo eReader device running KOReader
- Kepub books in your Kobo library (unencrypted)
- KOReader version with plugin support
Supported Platforms
This plugin is specifically designed for and tested on Kobo eReader devices. It requires access to Kobo’s internal database and file structure.
Installation
Prerequisites
Before installing the Kobo Plugin, ensure you have:
- A Kobo eReader device (Clara HD, Libra, Sage, etc.)
- KOReader installed on your Kobo device
- Access to the Kobo filesystem (usually via USB or file manager)
Installation Method
-
Download the latest release
- Go to the latest release page
- Download
kobo.koplugin.zipandkobo-patches.zip
-
Extract and install the plugin
- Extract
kobo.koplugin.zipto obtain thekobo.koplugin/folder - Copy the entire
kobo.koplugin/folder to your KOReader plugins directory on the Kobo device - The final path should be:
[KOReader]/plugins/kobo.koplugin/
- Extract
-
Extract and install the patches
- Extract
kobo-patches.zipto get the patch files (e.g.,2-*.lua) - Copy these patch files directly into your KOReader patches folder on the Kobo device
- Final location:
[KOReader]/patches/2-*.lua(patch files directly in the patches folder) - Note: The
patchesfolder may be missing; create[KOReader]/patches/if needed.
- Extract
-
Restart KOReader
- Restart KOReader on your Kobo device for the plugin to load and become active
Next Steps
After installation, see the Getting Started guide to learn how to access your virtual Kobo library and configure sync settings.
Features Overview
The Kobo Plugin provides comprehensive integration between KOReader and your Kobo device’s native library system.
Core Features
🗃️ Virtual Library Access
- Browse your entire Kobo kepub collection from within KOReader
- Automatic book discovery from Kobo’s database
- Rich metadata display (covers, titles, authors, progress)
🔄 Bidirectional Reading Sync
- Sync reading progress between KOReader and Kobo
- Automatic detection of the most recent reading position
- Support for multiple sync strategies and user preferences
⚙️ Flexible Sync Configuration
- Granular control over sync behavior
- Direction-specific settings (FROM Kobo, TO Kobo)
- Scenario-based sync rules (newer vs. older progress)
- Manual and automatic sync modes
📱 Bluetooth Management
- Enable/disable Bluetooth from KOReader
- Scan for and pair Bluetooth devices
- Manage connections to paired devices
- Configure key bindings on Bluetooth input devices
- Quick device connection via dispatcher actions
Virtual Library
The Virtual Library is the core feature that creates a seamless bridge between your Kobo’s kepub collection and KOReader’s file system.
Enabling and Disabling
The virtual library feature can be toggled on or off through the plugin settings:
- Open the KOReader menu (tap the top of the screen)
- Navigate to Tools → Kobo Library
- Toggle Enable virtual library
- Restart KOReader when prompted
Note: A restart is required for the change to take effect. When disabled, the virtual library will not be accessible, and all sync-related menu items will be hidden.
The virtual library is enabled by default when you first install the plugin.
How It Works
The plugin creates a virtual filesystem layer that presents your Kobo books as if they were regular files in KOReader’s file browser. This virtual representation is built by:
- Reading Kobo’s database (
KoboReader.sqlite) for book metadata - Creating virtual paths in the format
KOBO_VIRTUAL://BOOKID/filename - Mapping virtual paths to actual kepub file locations
- Providing file system operations for seamless KOReader integration
Virtual Path Structure
Kobo Library/
├── Harry Potter and the Philosopher's Stone.kepub.epub
├── Harry Potter and the Chamber of Secrets.kepub.epub
├── 1984.kepub.epub
├── Animal Farm.kepub.epub
└── [More books...]
Path Translation
| Virtual Path | Actual Path |
|---|---|
KOBO_VIRTUAL://ABC123/book.kepub.epub | /mnt/onboard/.kobo/kepub/ABC123 |
Library Organization
Flat Structure
Books are presented in a single flat directory without subfolders:
- Book files appear with their titles from Kobo’s database
- File extensions are preserved (
.kepub.epub) - No subdirectories: All books in one folder for simple browsing
Metadata Integration
Each virtual book entry includes:
- Title: From Kobo’s database or filename fallback
- Author: Primary author from book metadata
- Cover: Extracted from Kobo’s cover cache
- Series: Extracted from Kobo’s database
Troubleshooting Virtual Library
Missing Books
- DRM protection: Encrypted books cannot be accessed
Reading State Sync
The Reading State Sync feature maintains consistent reading progress between KOReader and Kobo’s native reader by synchronizing position, completion status, and reading statistics.
Note: When syncing to Kobo, reading position is updated only at chapter boundaries. Fine-grained position within a chapter is not preserved.
Overview
Reading State Sync works by:
- Monitoring reading progress in both KOReader and Kobo
- Comparing timestamps to determine the most recent reading session
- Syncing progress and status in the appropriate direction
How Sync Works
Bidirectional Synchronization
The plugin supports sync in both directions:
FROM Kobo TO KOReader (Pull)
- Retrieves progress from Kobo’s database
- Updates KOReader’s document settings
- Preserves reading position and completion status
FROM KOReader TO Kobo (Push)
- Reads KOReader’s progress from sidecar files
- Writes to Kobo’s SQLite database
- Updates reading statistics in Kobo
Timestamp-Based Sync Decision
graph TD
A[Read KOReader Progress] --> B[Read Kobo Progress]
B --> C{Compare Timestamps}
C -->|Kobo Newer| D[Sync FROM Kobo]
C -->|KOReader Newer| E[Sync TO Kobo]
C -->|Same/No Data| F[No Sync Needed]
D --> G[Update KOReader Data]
E --> H[Update Kobo Database]
F --> I[Done]
G --> I
H --> I
Sync Decision Flowchart
flowchart TD
A[Start Sync] --> B{Progress Data Exists?}
B -- "Neither" --> C[No Sync Needed]
B -- "Both" --> D[Compare Timestamps]
B -- "Only Kobo" --> E[Sync FROM Kobo]
B -- "Only KOReader" --> F[Sync TO Kobo]
D -- "Kobo Newer" --> E
D -- "KOReader Newer" --> F
D -- "Same/No Change" --> C
E --> G[Update KOReader Data]
F --> H[Update Kobo Database]
G --> I[Done]
H --> I
C --> I
Data Sources
KOReader Data
Reading progress is stored in .sdr sidecar files managed by KOReader.
Kobo Data
Progress is read from the KoboReader.sqlite database used by the Kobo system.
Automatic Sync Triggers
Library Access
graph TD
A[Open Virtual Library] --> B{Auto-sync Enabled?}
B -->|No| D[Show Library]
B -->|Yes| C{Already Synced Since KOReader Started?}
C -->|Yes| D
C -->|No| E[Sync All Books]
E --> D
Document Close
graph TD
A[Close Document] --> B{Is Virtual Kepub?}
B -->|No| D[Normal Close]
B -->|Yes| C[Extract Book ID]
C --> E{Auto-sync Enabled?}
E -->|No| F[Skip Sync]
E -->|Yes| G[Sync TO Kobo]
G --> H["Update Position<br/>(Chapter Boundary Only)"]
H --> I[Return to Virtual Library]
F --> I
Important Limitation: When syncing progress to Kobo, the position is updated only at chapter boundaries. Fine-grained position within a chapter is not preserved by Kobo’s native reader.
However, when Kobo syncs progress back to KOReader, KOReader opens the book at the exact percentage received from Kobo, providing fine-grained positioning.
Bluetooth support
The Kobo plugin provides Bluetooth management for MTK-based Kobo devices. You can enable/disable Bluetooth, scan for nearby devices, pair and connect to devices, and manage paired devices from KOReader.
Supported devices
- Kobo Libra Colour
- Kobo Clara BW / Colour
- Kobo Elipsa 2E
How to use
- Open main menu and choose Settings → Network → Bluetooth.
- Use “Enable/Disable” to toggle Bluetooth.
- Open “Paired devices” to see devices you have previously paired (including devices paired via Kobo
Nickel). From the paired devices list you can:
- Connect or disconnect a device
- Open the key binding configuration (when connected) to map device events to actions
- Forget a device to unpair it and remove it from the list
Note: Paired devices can only be connected when they are nearby and discoverable. Use “Scan for devices” to detect nearby devices (including paired devices that are currently discoverable). If a paired device appears in the scan results, you can connect to it from the Paired devices list or directly from the scan results.
Configuring key bindings
When you connect a Bluetooth device that supports button input (such as a remote or keyboard), you can map its buttons to KOReader actions.
To configure key bindings for a device:
- Go to Paired devices and select the device you want to configure.
- Choose “Configure key bindings” from the device menu - a list of available actions will appear.
- Select an action you want to bind to a button.
- Choose “Register button” - the system will now listen for the next button press on your device.
- Press a button on your Bluetooth device - the system will capture and bind it to the selected action.
- Repeat from step 3 for other actions you want to configure.
The available actions are defined in
src/lib/bluetooth/available_actions.lua.
If an action you need is missing, you can contribute by adding it to this file following the same
pattern as existing actions. See the plugin development documentation for details.
For more details, see key-bindings.
Dispatcher integration
The plugin registers Bluetooth actions with KOReader’s dispatcher system at startup, allowing you to control Bluetooth using gestures, profiles, or other dispatcher-aware features.
Bluetooth Control Actions
The following control actions are registered automatically:
- Enable Bluetooth — Turns Bluetooth on
- Disable Bluetooth — Turns Bluetooth off
- Toggle Bluetooth — Toggles Bluetooth on/off based on current state
- Scan for Bluetooth Devices — Starts a device scan and shows results
Device Connection Actions
The plugin also registers actions for each paired Bluetooth device, allowing you to connect to specific devices directly via dispatcher actions.
All Bluetooth actions can be found in the dispatcher system under the “Device” category.
Auto-detection of connecting devices
For Bluetooth devices that automatically reconnect (e.g., page turners that wake from sleep), you can enable auto-detection polling. When enabled, the plugin monitors for newly connected devices and automatically opens their input handlers so key bindings work immediately.
To enable auto-detection:
- Open Settings → Network → Bluetooth → Settings → Auto-detection.
- Enable “Auto-detect connecting devices” — this starts polling and will automatically open input handlers for devices that reconnect (for example, page turners that wake from sleep).
- (Optional) Enable “Stop detection after connection” to stop polling once a device successfully connects; leave it disabled to continue detecting additional devices.
For devices that don’t auto-connect, use the dispatcher “Connect to device” action instead. See the auto-detection settings documentation for details on when to use each approach.
Auto-connecting to nearby devices
For Bluetooth devices that require the Kobo to initiate the connection (devices in discovery/pairing/broadcasting mode), you can enable auto-connect. When enabled, the plugin scans for nearby paired devices and automatically connects to them when they come into range.
To enable auto-connect:
- Open Settings → Network → Bluetooth → Settings → Auto-connect.
- Enable “Auto-connect to nearby devices” — this starts scanning and will automatically connect to paired devices that come within range.
- (Optional) Enable “Stop auto-connect after connection” to stop scanning once a device successfully connects; leave it disabled to continue scanning for additional devices.
For devices that auto-reconnect on their own, use auto-detection instead. See the auto-connect settings documentation for details on when to use each approach.
Notes and tips
- Bluetooth is only supported on Kobo devices with MediaTek (MTK) hardware. If your device does not support Bluetooth, the menu will not be shown.
- When Bluetooth is enabled, KOReader prevents the device from entering standby until you disable Bluetooth.
- The device will still automatically suspend or shutdown according to your power settings when Bluetooth is enabled.
- Paired devices are remembered in the plugin settings so you can reconnect even if Bluetooth is off at startup.
Settings
The Kobo Plugin provides comprehensive settings to control how the plugin operates, particularly around reading state synchronization between KOReader and Kobo.
Settings Categories
Virtual Library
- Virtual Library Overview - Overview and configuration for the virtual library feature, including how to enable or disable it and any restart requirements. See the linked page for full details and examples.
Sync Settings
Control how and when reading progress is synchronized between KOReader and Kobo’s native reader:
- Sync Settings Overview - Core settings for enabling and configuring sync
- Sync Direction Settings - Detailed control over sync behavior in each direction
- Sync Configuration Examples - Common configuration patterns for different use cases
Bluetooth Settings
Manage Bluetooth devices and configure button mappings for MTK-based Kobo devices:
- Bluetooth Settings Overview - Overview of Bluetooth management and configuration options
- Paired Devices - Managing paired Bluetooth devices and connections
- Key Bindings - Configuring button mappings for Bluetooth devices
- Menu Navigation - How to access Bluetooth settings and menu hierarchy
All settings are stored in KOReader’s configuration and persist across restarts.
Virtual Library Overview
- Enable virtual library - Toggle the virtual library feature on or off. When enabled, you can access your Kobo library from within KOReader. When disabled, the virtual library and all sync-related menu items will be hidden. A restart is required for changes to take effect. (Default: enabled)
Details
When the virtual library is enabled, the plugin exposes a virtual view of your Kobo device’s library inside KOReader. This virtual view is populated from Kobo’s metadata and lets you browse, search, and open titles backed by Kobo’s database without switching to the native Kobo reader.
Behavior
- Enabling the virtual library adds a virtual folder in KOReader that mirrors your Kobo library entries.
- Disabling the virtual library hides the virtual folder and removes sync-related menu items; it does not delete your actual book files.
- Changing this setting requires restarting KOReader for the virtual filesystem and related menu entries to be fully initialized or removed.
Sync Settings Overview
The plugin provides granular control over reading state synchronization behavior through comprehensive settings that let you customize how and when sync operations occur.
Documentation
- Core Settings: Main controls, direction toggles, and behavior options with defaults table
- Settings Menu Navigation: How to access settings and menu hierarchy reference
- Manual Sync: Triggering manual sync operations and sync behavior
- Sync Decision Dialog: Understanding the dialog shown when sync requires user input
Core Settings
All sync settings are stored in KOReader’s configuration and persist across restarts. Here are the core settings:
Main Controls
sync_reading_state (Default: false)
Global enable/disable for all sync functionality. When disabled, no synchronization occurs between KOReader and Kobo.
Menu: Kobo Library → Sync reading state with Kobo
enable_auto_sync (Default: false)
Auto-sync reading progress when opening the virtual library. The sync only happens once per KOReader startup. Books are always displayed in the virtual library regardless of this setting.
Menu: Kobo Library → Enable automatic sync on virtual library
Direction Controls
enable_sync_from_kobo (Default: false)
Allow pulling progress from Kobo to KOReader (FROM Kobo sync).
Menu: Kobo Library → Sync behavior → Enable sync FROM Kobo TO KOReader
enable_sync_to_kobo (Default: true)
Allow pushing progress from KOReader to Kobo (TO Kobo sync).
Menu: Kobo Library → Sync behavior → Enable sync FROM KOReader TO Kobo
Behavior Controls
Granular settings for different sync scenarios with three options each:
PROMPT: Ask user before syncingSILENT: Sync automatically without confirmationNEVER: Skip sync in this scenario
See Sync Direction Settings for details on the four behavior settings.
Settings Menu Navigation
Accessing Settings
- Open KOReader file browser
- Open the menu (top-left corner)
- Select “Kobo Library” → Settings
Settings Hierarchy
Kobo Library
├── Sync reading state with Kobo [Toggle]
├── Enable automatic sync on virtual library [Toggle]
├── Sync reading state now [Action]
├── Sync behavior [Submenu]
│ ├── Enable sync FROM Kobo TO KOReader [Toggle]
│ ├── Enable sync FROM KOReader TO Kobo [Toggle]
│ ├── From Kobo to KOReader [Submenu]
│ │ ├── Sync from newer state (Current: Prompt)
│ │ └── Sync from older state (Current: Never)
│ └── From KOReader to Kobo [Submenu]
│ ├── Sync to newer state (Current: Silent)
│ └── Sync to older state (Current: Never)
├── Refresh library [Action]
└── About [Info]
Menu Item Reference
| Menu Item | Type | Function |
|---|---|---|
| Sync reading state with Kobo | Toggle | Enable/disable all sync functionality |
| Enable automatic sync on virtual library | Toggle | Enable/disable automatic progress sync on library access (books are always displayed) |
| Sync reading state now | Action | Manually trigger progress sync for all books |
| Sync behavior | Submenu | Configure sync direction and behavior |
| Refresh library | Action | Refresh virtual library metadata |
| About | Info | Display plugin information |
Manual Sync
You can trigger a manual sync at any time via the menu: Kobo Library → Sync reading state now
This respects all the sync behavior settings configured in Sync Direction Settings.
Manual Sync Process
- Open the file browser in KOReader
- Open the menu (top-left corner)
- Select “Kobo Library” → “Sync reading state now”
- Wait for sync to complete
- See confirmation when done
Sync Behavior with Manual Sync
Manual sync respects all configured settings:
- sync_from_kobo_newer and sync_from_kobo_older: Control pull behavior
- sync_to_kobo_newer and sync_to_kobo_older: Control push behavior
- PROMPT settings will show dialogs for each decision
- SILENT settings will sync automatically
- NEVER settings will skip those scenarios
Sync Decision Dialog
When Sync Direction Settings are set to PROMPT, the plugin shows a dialog whenever a sync decision needs to be made.
Dialog Format
Book: The Great Gatsby
Kobo: 45% (2024-01-15 14:30)
KOReader: 38% (2024-01-14 22:15)
Sync newer reading progress from Kobo?
When Dialogs Appear
Dialogs appear when:
- A book has different reading positions in KOReader and Kobo
- Sync Direction Settings are set to PROMPT for that scenario
- The sync decision is about to be made (newer or older state)
Dialog Behavior
When you see a dialog, you have two options:
| Button | Effect |
|---|---|
| Yes | Proceed with the sync operation |
| No | Skip this sync decision, no changes are made |
Example Scenarios
Scenario 1: Pull newer from Kobo
Book: The Great Gatsby
Kobo: 60% (2024-01-15 14:30)
KOReader: 40% (2024-01-14 22:15)
Sync newer reading progress from Kobo?
- Kobo has newer timestamp (newer progress)
- Appears when
sync_from_kobo_newer = PROMPT - Click Yes to update KOReader to 60%, or No to keep 40%
Scenario 2: Push older to Kobo
Book: The Great Gatsby
KOReader: 35% (2024-01-13 10:00)
Kobo: 50% (2024-01-14 22:15)
Sync older reading progress to Kobo?
- KOReader has older progress (less complete)
- Appears when
sync_to_kobo_older = PROMPT - Click Yes to revert Kobo to 35%, or No to keep 50%
Sync Direction Settings
These settings control sync behavior based on which side (Kobo or KOReader) has newer progress. Each setting has three options: PROMPT (ask user), SILENT (automatic), or NEVER (skip).
FROM Kobo (Pull) Sync Settings
sync_from_kobo_newer (Default: PROMPT)
When to use: Kobo has more recent reading progress than KOReader
Options:
- Prompt: Show a dialog and ask whether to pull Kobo’s newer progress
- Silent: Automatically pull newer progress from Kobo without asking
- Never: Keep KOReader progress, ignore newer Kobo data
Example: Kobo shows 45% (read yesterday), KOReader shows 30% (read 2 days ago). This setting decides what happens.
Menu: Kobo Library → Sync behavior → From Kobo to KOReader → Sync from newer state
sync_from_kobo_older (Default: NEVER)
When to use: Kobo has a newer timestamp but lower progress percentage than KOReader
Options:
- Prompt: Show a dialog and ask whether to pull Kobo’s progress despite it being less complete
- Silent: Automatically pull Kobo’s progress, overwriting KOReader’s higher progress
- Never: Keep KOReader’s higher progress, don’t regress to lower Kobo percentage (recommended)
Example: Kobo shows 40% (read today - newer timestamp), KOReader shows 80% (read 2 days ago - older timestamp). This setting decides what happens.
Menu: Kobo Library → Sync behavior → From Kobo to KOReader → Sync from older state
FROM KOReader (Push) Sync Settings
sync_to_kobo_newer (Default: SILENT)
When to use: KOReader has more recent reading progress than Kobo
Options:
- Prompt: Show a dialog and ask whether to push KOReader’s newer progress
- Silent: Automatically push newer progress to Kobo without asking (recommended)
- Never: Keep Kobo progress, don’t update with newer KOReader data
Example: KOReader shows 65% (read today), Kobo shows 50% (read yesterday). This setting decides what happens.
Menu: Kobo Library → Sync behavior → From KOReader to Kobo → Sync to newer state
sync_to_kobo_older (Default: NEVER)
When to use: KOReader has a newer timestamp but lower progress percentage than Kobo
Options:
- Prompt: Show a dialog and ask whether to push KOReader’s progress despite it being less complete (unusual)
- Silent: Automatically push KOReader’s progress, overwriting Kobo’s higher progress (unusual)
- Never: Keep Kobo’s higher progress, don’t regress to lower KOReader percentage (recommended)
Example: KOReader shows 40% (read today - newer timestamp), Kobo shows 80% (read 2 days ago - older timestamp). This setting decides what happens.
Menu: Kobo Library → Sync behavior → From KOReader to Kobo → Sync to older state
Understanding “Newer” vs “Older”
The plugin compares the timestamps of when progress was last updated:
- Newer: The more recent progress update (more recently modified timestamp)
- Older: The less recent progress update (older modification timestamp)
However, “older” in the setting names refers to scenarios where the source has a newer timestamp but lower progress percentage. This can happen when someone re-reads earlier parts of a book.
These settings let you decide whether to keep progress based on timestamps or based on completion percentage in each direction.
Sync Configuration Examples
Common configuration patterns for different use cases. Choose the pattern that best matches your reading habits.
Conservative Sync
Use case: You want full control and visibility over every sync decision.
Settings:
sync_reading_state = true
enable_auto_sync = false
enable_sync_from_kobo = true
enable_sync_to_kobo = true
sync_from_kobo_newer = PROMPT -- Ask before pulling newer
sync_from_kobo_older = NEVER -- Never regress progress
sync_to_kobo_newer = PROMPT -- Ask before pushing newer
sync_to_kobo_older = NEVER -- Never regress progress
Automatic Sync (Both directions, prefer newer)
Use case: You read on both systems and want seamless synchronization.
Settings:
sync_reading_state = true
enable_auto_sync = true
enable_sync_from_kobo = true
enable_sync_to_kobo = true
sync_from_kobo_newer = SILENT -- Auto-pull newer
sync_from_kobo_older = NEVER -- Keep higher KOReader progress
sync_to_kobo_newer = SILENT -- Auto-push newer
sync_to_kobo_older = NEVER -- Keep higher Kobo progress
KOReader Primary
Use case: You primarily read in KOReader and want Kobo to follow.
Settings:
sync_reading_state = true
enable_auto_sync = false
enable_sync_from_kobo = false -- Don't pull from Kobo
enable_sync_to_kobo = true
sync_to_kobo_newer = SILENT -- Auto-push newer to Kobo
sync_to_kobo_older = NEVER -- Don't regress Kobo
Kobo Primary
Use case: You primarily read in Kobo native reader and want KOReader to follow.
Settings:
sync_reading_state = true
enable_auto_sync = true
enable_sync_from_kobo = true
enable_sync_to_kobo = false -- Don't push to Kobo
sync_from_kobo_newer = SILENT -- Auto-pull newer from Kobo
sync_from_kobo_older = NEVER -- Don't regress KOReader
Manual Sync Only
Use case: You want complete control and prefer to trigger sync manually.
Settings:
sync_reading_state = true
enable_auto_sync = false
enable_sync_from_kobo = true
enable_sync_to_kobo = true
sync_from_kobo_newer = PROMPT -- Always ask
sync_from_kobo_older = PROMPT -- Always ask
sync_to_kobo_newer = PROMPT -- Always ask
sync_to_kobo_older = PROMPT -- Always ask
Bluetooth Settings Overview
The plugin provides Bluetooth management for MTK-based Kobo devices, allowing you to pair devices, connect to them, and configure button mappings for Bluetooth remotes and keyboards.
Documentation
- Paired Devices: Managing paired Bluetooth devices and connections
- Device Options Menu: Available actions for individual devices
- Key Bindings: Configuring button mappings for Bluetooth devices
- Auto-Detection: Settings for automatically detecting reconnecting devices
- Auto-Connect: Settings for automatically connecting to nearby paired devices
- Menu Navigation: How to access Bluetooth settings and menu hierarchy reference
Paired Devices
The plugin maintains a list of all Bluetooth devices that have been paired with your Kobo, including devices paired through Kobo Nickel.
Accessing Paired Devices
- Navigate to Settings → Network → Bluetooth → Paired devices
You’ll see a list of all devices paired with your Kobo. Each device shows its name and current connection status.
Pairing a New Device
- Put your Bluetooth device in pairing mode (refer to your device’s manual)
- Navigate to Settings → Network → Bluetooth
- Choose “Scan for devices”
- Select your device from the list
- Follow any on-screen prompts to complete pairing
Device Options Menu
Select any device from the paired devices list to open the device options menu. See Device Options Menu for details on available actions.
Notes
- Paired devices can only be connected when they are nearby and discoverable. Use “Scan for devices” to detect nearby devices, including previously paired devices that have become discoverable
Device Options Menu
When you select a device from the Paired Devices list, a menu appears with options for managing that device.
Accessing the Device Options Menu
- Navigate to Settings → Network → Bluetooth → Paired devices
- Select any device from the list
The options shown depend on the device’s current state.
Available Options
| Option | Shows When | Function |
|---|---|---|
| Connect | Device is not currently connected | Establishes a connection to the device. The device will be ready to use with KOReader once connected. Bluetooth must be enabled. |
| Disconnect | Device is currently connected | Closes the active connection. The device remains paired but will no longer be actively connected. You can reconnect at any time without needing to pair again. |
| Configure Key Bindings | Device is connected and the plugin supports key bindings | Opens a menu to set up button mappings for remote controls and keyboards. You can assign actions to buttons on your Bluetooth device. See Key Bindings for detailed configuration instructions. |
| Reset Key Bindings | The device has existing key binding configurations | Removes all button mappings for this device. You’ll be asked to confirm before the key bindings are cleared. |
| Trust | Device is not currently trusted | Marks the device as trusted. Trusted devices can connect to your Kobo without requiring confirmation each time they connect. |
| Untrust | Device is currently trusted | Removes the trusted status from the device. The device will require confirmation before connecting in the future. |
| Forget | Always shown | Removes the device from your paired devices list. This unpairs the device from your Kobo. You’ll need to pair it again to use it. The device’s key bindings (if any) are also removed. |
Key Bindings
When you connect a Bluetooth device that has buttons (like a remote control or keyboard), you can configure which buttons trigger which KOReader actions.
Available Actions
The plugin automatically provides access to KOReader actions that you can bind to your Bluetooth device buttons. These actions are organized into categories:
- General - Common actions like showing menus and navigation
- Device - Device-specific functions like toggling frontlight, WiFi, and power options
- Screen and lights - Adjust frontlight brightness, warmth, and screen settings
- File browser - Actions for managing files and folders
- Reader - Reading-related actions like page navigation, bookmarks, and annotations
- Reflowable documents - Font size, line spacing, and text formatting (for EPUBs, etc.)
- Fixed layout documents - Zoom, rotation, and page fitting (for PDFs, CBZ, etc.)
The full list of available actions is provided by KOReader’s dispatcher system and may vary depending on your KOReader version and installed plugins.
Configuring Key Bindings
To set up button mappings for a connected device:
- Navigate to Settings → Network → Bluetooth → Paired devices
- Select the device you want to configure
- Choose “Configure key bindings”
- Select a category (e.g., Reader, Device, Screen and lights)
- Select an action from the category list
- Choose “Register button”
- Press the button on your Bluetooth device you want to use for this action
- The binding is saved automatically
Repeat steps 4-8 for each button you want to configure.
Removing a Key Binding
To remove a button mapping:
- Navigate to the device’s key binding configuration
- Select the action you want to unbind
- Choose “Remove binding”
Remove All Bindings
To clear all button mappings for a device:
- Navigate to the device options in Settings → Network → Bluetooth → Paired devices
- Select “Reset key bindings”
Multiple Devices
Each Bluetooth device can have its own unique button configuration. The mappings you create for one device won’t affect other devices.
Persistence
Key bindings are saved automatically and persist across KOReader restarts. You only need to configure them once per device.
Auto-resume After Wake
Overview
The plugin includes an “Auto-resume after wake” feature that can automatically re-enable Bluetooth when your device wakes from sleep if Bluetooth was enabled before the device was suspended.
Enabling Auto-resume
- Go to Settings → Network → Bluetooth → Settings
- Toggle “Auto-resume after wake” to enable or disable the feature
How It Works
When auto-resume is enabled:
- When your device suspends, the plugin automatically turns off Bluetooth to save battery
- If “Auto-resume after wake” is enabled and Bluetooth was on before suspend:
- The plugin will automatically turn Bluetooth back on when the device wakes
- It will attempt to reconnect to previously connected devices
- Your key bindings will remain active
- If the setting is disabled, Bluetooth will remain off after wake and you’ll need to manually re-enable it
WiFi Interaction
When Bluetooth auto-resumes after wake, the plugin needs to temporarily enable WiFi (MTK Bluetooth requires WiFi to be on). The plugin handles WiFi restoration based on KOReader’s “Auto-restore WiFi after resume” setting:
- If KOReader’s “Auto-restore WiFi” is disabled, WiFi will be turned back off after Bluetooth finishes enabling (since WiFi was only enabled temporarily for Bluetooth)
- If KOReader’s “Auto-restore WiFi” is enabled, WiFi state will be managed by KOReader’s own restoration logic
This ensures that enabling Bluetooth doesn’t unexpectedly change your WiFi preferences.
Battery Considerations
When auto-resume is disabled:
- Bluetooth will remain off after wake
- You’ll need to manually turn Bluetooth back on using the menu or a dispatcher action
- This can help save battery if you don’t need Bluetooth active all the time
Footer Status
Overview
The “Show status in footer” feature displays Bluetooth status in the reader’s footer bar. This allows you to see at a glance whether Bluetooth is enabled or disabled while reading, without having to open menus.
Enabling Footer Status
- Go to Settings → Network → Bluetooth → Settings
- Toggle “Show status in footer” to enable or disable the feature
How It Works
When footer status is enabled:
- An icon or text appears in the reader’s footer bar showing Bluetooth status
- The display adapts based on your footer settings:
- Icons mode: Shows (Bluetooth on) or (Bluetooth off)
- Compact items mode: Shows (Bluetooth on) or (Bluetooth off)
- Text mode: Shows “BT: On” or “BT: Off”
- The status updates automatically when you enable or disable Bluetooth
- If “hide empty generators” is enabled in footer settings and Bluetooth is off, the indicator may be hidden
Default Behavior
The footer status feature is enabled by default.
Auto-Detection Settings
The auto-detection feature monitors for Bluetooth devices that automatically reconnect to your Kobo device and opens their input handlers so key bindings continue to work.
When to Use Auto-Detection
Use Auto-Detection For
- Devices that auto-reconnect - Some Bluetooth devices (page turners, remotes, keyboards) automatically reconnect when they wake from sleep or when Bluetooth is enabled on the Kobo
- Hands-free reconnection - When you want your device to be ready to use without manual intervention after the device reconnects
Use Dispatcher “Connect to Device” Action For
- Devices that don’t auto-connect - If your device requires manual connection each time
- On-demand connections - When you want to trigger a connection with a gesture or profile
- Power saving - When you don’t want continuous polling for device connections
See Using Dispatcher to Connect Bluetooth for details on the dispatcher approach.
Settings
Auto-detect connecting devices
When enabled, the plugin polls every second to check if any paired Bluetooth devices have connected. If a connected device is found without an open input handler, it automatically opens one.
- Default: Disabled
- When enabled: Polls for connected devices every 1 second
- Notification: Shows a notification when a device is auto-detected (except during initial startup)
Stop detection after connection
When enabled, auto-detection polling stops after the first successful device connection. This saves resources if you typically only use one Bluetooth device at a time.
- Default: Enabled
- Only active when: “Auto-detect connecting devices” is enabled
- Behavior: Polling resumes when Bluetooth is toggled off and back on
How It Works
- When Bluetooth is enabled and auto-detection is turned on, the plugin starts polling every second
- Each poll cycle:
- Loads the current list of paired devices from the system
- Checks which devices are marked as “connected” by the Bluetooth stack
- For any connected device without an open input handler, opens one
- When a new device is detected after startup, a notification is shown
- If “Stop detection after connection” is enabled, polling stops after the first successful connection
Auto-Connect Settings
The auto-connect feature monitors for nearby paired Bluetooth devices in discovery/pairing mode and automatically initiates connections when they come within range. This is useful for devices that don’t auto-reconnect and require the Kobo to start the connection process.
When to Use Auto-Connect
Use Auto-Connect For
- Devices requiring Kobo-initiated connection - Bluetooth devices that are in discovery, pairing, or broadcasting mode and need the Kobo to initiate the connection
- Devices that don’t auto-reconnect - Devices that disconnect and don’t automatically reconnect when turned back on or when Bluetooth is re-enabled
Use Auto-Detection For
- Devices that auto-reconnect - Some Bluetooth devices (page turners, remotes, keyboards) automatically reconnect when they wake from sleep or when Bluetooth is re-enabled
- Already-connected devices - If your devices already handle reconnection on their own
See Auto-Detection for details on handling devices that auto-reconnect.
Use Dispatcher “Connect to Device” Action For
- On-demand connections - When you want to trigger a connection with a gesture or profile
- Power saving - When you don’t want continuous scanning for nearby devices
See Using Dispatcher to Connect Bluetooth for details on the dispatcher approach.
Settings
Auto-connect to nearby devices
When enabled, the plugin continuously scans for nearby paired Bluetooth devices in discovery/pairing/broadcasting mode. When a paired device comes within range (detected via RSSI signal strength), the plugin automatically initiates a connection to that device.
- Default: Disabled
- When enabled: Continuously scans and monitors RSSI for all paired devices
- Connection initiation: The Kobo (not the device) initiates the connection request
- Notification: Shows a notification when a device is auto-connected
- Requirements: Device must be paired, in discovery/broadcasting mode, and not currently connected
Stop auto-connect after connection
When enabled, auto-connect scanning stops after the first successful device connection. This saves battery and prevents unnecessary scanning if you typically only use one Bluetooth device at a time.
- Default: Enabled
- Only active when: “Auto-connect to nearby devices” is enabled
- Behavior: Scanning automatically resumes when a connected device disconnects or when Bluetooth is toggled off and back on
How It Works
- When Bluetooth is enabled and auto-connect is turned on, the plugin starts scanning for nearby paired devices in discovery/pairing/broadcasting mode
- For each paired device, the plugin monitors the RSSI (signal strength):
- RSSI indicates how strong the wireless signal is from the device
- When a device’s RSSI changes, it means the device has come into range or moved away
- When a paired device with a valid RSSI is detected (in range), the plugin:
- Checks if it’s not already connected
- Verifies it’s in your paired devices list
- Automatically initiates a connection request (the Kobo starts the connection)
- If “Stop auto-connect after connection” is enabled:
- Scanning stops after the first successful connection
- Scanning resumes when that device disconnects
- Scanning also resumes when Bluetooth is toggled off and back on
Bluetooth Menu Navigation
Accessing Bluetooth Settings
- Open KOReader top menu
- Navigate to Settings → Network → Bluetooth.
Menu Hierarchy
Settings → Network → Bluetooth
├── Enable/Disable [Toggle]
├── Scan for devices [Action]
├── Paired devices [Submenu]
│ ├── Device 1 [Submenu]
│ │ ├── Connect/Disconnect [Action]
│ │ ├── Configure key bindings [Submenu]
│ │ │ ├── Action 1 → Register button / Remove binding
│ │ │ ├── Action 2 → Register button / Remove binding
│ │ │ └── ...
│ │ └── Remove device [Action]
│ ├── Device 2 [Submenu]
│ └── ...
└── Settings [Submenu]
├── Auto-resume after wake [Toggle]
├── Show status in footer [Toggle]
├── Auto-detection [Submenu]
│ ├── Auto-detect connecting devices [Toggle]
│ └── Stop detection after connection [Toggle]
└── Auto-connect [Submenu]
├── Auto-connect to nearby devices [Toggle]
└── Stop auto-connect after connection [Toggle]
Menu Item Reference
| Menu Item | Type | Function |
|---|---|---|
| Enable/Disable | Toggle | Turn Bluetooth on or off |
| Scan for devices | Action | Scan for new devices to pair |
| Paired devices | Submenu | View and manage all paired Bluetooth devices |
| Connect/Disconnect | Action | Connect to or disconnect from a specific device |
| Configure key bindings | Submenu | Set up button mappings for a connected device |
| Remove device | Action | Remove device from paired list |
| Settings | Submenu | Bluetooth settings submenu |
| Auto-resume after wake | Toggle | Automatically re-enable Bluetooth after device wakes up |
| Show status in footer | Toggle | Display Bluetooth status in the reader’s footer bar |
| Auto-detection | Submenu | Auto-detection settings submenu |
| Auto-detect connecting devices | Toggle | Enable/disable polling for auto-connected devices |
| Stop detection after connection | Toggle | Stop polling once a device successfully connects |
| Auto-connect | Submenu | Auto-connect settings submenu |
| Auto-connect to nearby devices | Toggle | Enable/disable automatic connection to nearby devices |
| Stop auto-connect after connect | Toggle | Stop scanning once a device successfully connects |
| Register button | Action | Capture a button press to bind to selected action |
| Remove binding | Action | Remove button mapping for selected action |
Important Notes
- Bluetooth menu is only visible on MTK-based Kobo devices (Libra Colour, Clara BW/Colour)
- “Configure key bindings” only appears when a device is connected
- When Bluetooth is enabled, the device will not enter standby mode
- The device will still suspend or shutdown according to your power settings
- “Scan for devices” scans for nearby Bluetooth devices. Use it to discover new devices to pair and to check whether previously paired devices are currently nearby and discoverable.
Usage Scenarios
This section provides detailed walkthroughs for common use cases of the Kobo Plugin.
Scenarios
- Komga / Calibre Web Integration: Seamless book syncing between Komga/Calibre Web through Kobo
Komga / Calibre Web Integration
Goal
Streamline your workflow by syncing books through Kobo’s native system. Read Komga or Calibre Web synced books directly in KOReader while maintaining seamless reading progress synchronization.
Use Case
You use Komga or Calibre Web to manage your digital library and sync books to your Kobo device. You want to:
- Keep Kobo as your single source of truth for books
- Enjoy KOReader’s superior reading features and customization
- Have reading progress automatically sync between Kobo and KOReader
- Avoid managing duplicate book copies or complex sync setups
- Kobo syncs progress back to Komga/Calibre Web, completing the loop
Benefits
Simplified Setup
- One book source (Komga/Calibre Web → Kobo)
- No need to manually manage books in multiple locations
- KOReader automatically sees all books from your sync service
- Reading progress syncs back to Komga/Calibre Web through Kobo’s sync mechanism
Best of Both Worlds
- Use KOReader’s superior reader
- Leverage Komga/Calibre Web’s library management
- Use Kobo’s native sync capabilities
Setup
Prerequisites
- Komga or Calibre Web configured and syncing books to Kobo
- Kobo Plugin installed and enabled in KOReader
- Sync enabled in plugin settings
Important Limitation
When syncing reading progress to Kobo, the position is rounded to chapter boundaries. This means Kobo’s native reader will open at the nearest chapter rather than the exact position where you stopped in KOReader. However, when KOReader receives progress from Kobo, it opens at the exact percentage, providing fine-grained positioning.
Configuration
Use these recommended settings:
✅ Sync reading state with Kobo: ON
✅ Enable automatic sync on virtual library: ON
✅ Enable sync FROM Kobo TO KOReader: ON
✅ Enable sync FROM KOReader TO Kobo: ON
Sync from Kobo (newer): SILENT
Sync to Kobo (newer): SILENT
Sync from Kobo (older): NEVER
Sync to Kobo (older): NEVER
These settings mean:
- Sync is enabled globally
- Auto-sync is enabled (syncs automatically when accessing the virtual library)
- Progress always syncs when you close books or access the library
- Never sync older/less complete progress (prevents losing progress)
Workflows
Detailed step-by-step workflows for common reading scenarios:
- Reading a New Book: Setting up and opening a book for the first time
- Continuing in Kobo Native: Switching between KOReader and Kobo’s native reader
- Return to KOReader: Coming back to KOReader after reading in Kobo
See the Workflows page for detailed instructions.
Sync Flow Diagram
The Sync Flow Diagram visualizes how books and reading progress move through the system.
Next Steps
- Review Sync Settings for advanced configuration
- See Settings Menu Navigation for how to access settings
Workflows
This section covers the detailed workflows for using the Kobo Plugin with Komga/Calibre Web.
Available Workflows
- Reading a New Book: Setting up and opening a book for the first time
- Continuing in Kobo Native: Switching between KOReader and Kobo’s native reader
- Return to KOReader: Coming back to KOReader after reading in Kobo
Reading a New Book
-
Book arrives from Komga/Calibre Web
- Your book sync service pushes new books to Kobo
- Books appear in both Kobo’s native library and the Kobo Plugin’s virtual library
-
Open KOReader and browse the virtual library
- Navigate to the Kobo Library in KOReader
- All books are displayed (books are always synced)
- Plugin automatically syncs reading progress for all books (auto-sync is enabled)
-
Select and open a book
- Book automatically opens at the correct position based on synced progress
- Latest progress from Kobo is already synced
-
Read with KOReader’s features
- Enjoy KOReader’s customizable fonts, margins, and reading features
- Progress is tracked normally
-
Close the book
- Reading progress is automatically synced back to Kobo
- No manual action needed
Continuing in Kobo Native
-
Switch to Kobo native reader
- Open the book in Kobo’s native reading app
- Book automatically opens at your last read position from KOReader
- Note: Kobo rounds to chapter boundaries, so the exact position may vary slightly
- Continue reading with Kobo’s features if desired
-
Make progress
- Read in Kobo’s native reader
- Close when done
Return to KOReader
-
Open KOReader and browse the virtual library
- Navigate back to the Kobo Library
- All books are displayed (books are always synced)
- Plugin automatically syncs reading progress for all books again (auto-sync enabled)
-
Open the same book
- Book automatically opens at the updated position based on latest synced progress
- KOReader uses fine-grained percentage positioning from Kobo
- Latest progress from Kobo is already synced
-
Keep reading
- Make additional progress in KOReader
- Close when done (progress syncs automatically back to Kobo)
Sync Flow Diagram
Note: This diagram assumes the recommended settings are enabled (auto-sync ON).
Important Limitation: When syncing to Kobo, position is rounded to chapter boundaries. When Kobo syncs back to KOReader, KOReader opens at the exact percentage from Kobo.
sequenceDiagram
participant Komga as Komga/<br/>Calibre Web
participant Kobo as Kobo<br/>Database
participant VLib as Virtual<br/>Library
participant KOR as KOReader
Komga->>Kobo: New book or update
Note over VLib: User opens KOReader
KOR->>VLib: Browse library
VLib->>Kobo: Read book list
Note over VLib: Auto-sync triggered (first open per session)
VLib->>Kobo: Sync all books progress
Kobo->>VLib: Return latest progress
Note over KOR: User selects and opens book
KOR->>KOR: Open book at last synced position (fine-grained %)
Note over KOR: User reads to 45%
Note over KOR: User closes book
Note over KOR,Kobo: Sync TO Kobo (rounded to chapter boundary)
KOR->>Kobo: Write 45% progress (as chapter position)
Kobo->>Kobo: Update book status
Note over Kobo: User opens Kobo native
Kobo->>Kobo: Read progress (at chapter boundary)
Kobo->>Kobo: Open book at chapter boundary
Note over Kobo: User reads to 60%
Note over Kobo: User closes book
Kobo->>Kobo: Update to 60%
Note over VLib: User opens KOReader again (new session)
KOR->>VLib: Browse library
VLib->>Kobo: Read book list
Note over VLib: Auto-sync triggered (first open in new session)
VLib->>Kobo: Sync all books progress
Kobo->>VLib: Return 60% progress
Note over KOR: User selects and opens book
Note over KOR: Sync FROM Kobo (fine-grained %)
KOR->>KOR: Open book at 60% (fine-grained position from Kobo)
This diagram shows the complete flow of how books and reading progress move through the system when using Komga or Calibre Web with the Kobo Plugin. The key points are:
- Books are always displayed in the virtual library (independent of sync setting)
- Reading progress syncs automatically when:
- Accessing the virtual library (if auto-sync enabled) - syncs all books once per session
- Closing a book - always syncs that book’s progress to Kobo (rounded to chapter boundary)
- KOReader uses fine-grained percentages when opening books (from either app)
- Kobo uses chapter-based positioning when opening books
Bluetooth Dispatcher Integration
Goal
Quickly connect to paired Bluetooth devices through dispatcher actions. Use gestures, profiles, or other dispatcher-aware features to trigger connections to your Bluetooth remotes, keyboards, or page turners.
Use Case
You have a paired Bluetooth device and want to:
- Connect to it with a single gesture
- Trigger device connections from other KOReader plugins or profiles
- Avoid navigating menus to connect to frequently-used devices
Benefits
Streamlined Connectivity
- One gesture to connect to your Bluetooth device
- Automatic connection if Bluetooth is off (plugin enables it first)
How It Works
Automatic Registration
When KOReader starts, the plugin automatically registers dispatcher actions for all your paired Bluetooth devices. Each device gets a unique action id based on its MAC address.
You can find the registered actions in the dispatcher system under the “Device” category.
What Happens When You Trigger the Action
- The plugin checks if Bluetooth is enabled
- If disabled, it automatically turns Bluetooth on
- It attempts to connect to the device
- You see a confirmation message
Setup
Prerequisites
- At least one Bluetooth device paired with your Kobo
- Kobo Plugin installed and enabled in KOReader
For instructions on how to pair a Bluetooth device, see the Bluetooth feature documentation.
Using with Gestures and Profiles
Any KOReader feature that supports dispatcher actions can trigger device connections. Check your gesture system or profile documentation for how to assign dispatcher actions.
Next Steps
- Review Bluetooth feature documentation for pairing and managing devices
Architecture Overview
The Kobo Plugin bridges two different reading progress tracking systems: Kobo’s centralized SQLite database and KOReader’s distributed sidecar files.
Core Concept
graph LR
K[Kobo Database<br/>Centralized<br/>Chapter-based] <-->|Plugin<br/>Sync| R[KOReader Sidecars<br/>Distributed<br/>Percentage-based]
style K fill:#fff3e0
style R fill:#e1f5ff
The plugin acts as a translator and synchronizer between these two fundamentally different systems.
Key Topics
High-Level Architecture
Visual overview of components and their relationships. Start here to understand the overall system design.
Database & Data Storage
How both systems store reading progress, why they’re different, and how the plugin bridges the gap. This is the most important section for understanding the plugin’s core functionality.
High-Level Architecture
This chapter contains the visual overviews and high-level component relationships.
Architecture
architecture-beta
group koreader(mdi:laptop)[KOReader Environment]
service ui(mdi:palette)[User Interface] in koreader
service fm(mdi:folder-open)[File Manager] in koreader
service dr(mdi:book-open-page-variant)[Document Reader] in koreader
service ps(mdi:puzzle)[Plugin System] in koreader
group plugin_core(mdi:cog)[Plugin Core]
service mp(mdi:play-circle)[Main Plugin] in plugin_core
service vl(mdi:library)[Virtual Library] in plugin_core
service rss(mdi:sync)[Reading State Sync] in plugin_core
service meta(mdi:tag-multiple)[Metadata Parser] in plugin_core
group extensions(mdi:sitemap)[Extensions]
service uie(mdi:palette-advanced)[UI Extensions] in extensions
service fse(mdi:folder-network)[Filesystem Extensions] in extensions
service dce(mdi:file-document)[Document Extensions] in extensions
service dse(mdi:cog-box)[DocSettings Extensions] in extensions
group kobo_system(mdi:harddisk)[Kobo System]
service db(mdi:database)[SQLite Database] in kobo_system
service kf(mdi:book-open-blank-variant)[Kepub Files] in kobo_system
junction toDB
junction toRSS
junction extA
junction extB
junction extC
junction extD
junction extE
ui:R --> L:fm
fm:R --> L:vl
vl:T --> B:meta
meta:R -- L:toDB
toDB:B --> T:db
vl:B --> T:kf
dr:T -- B:toRSS
toRSS:R --> L:rss
rss:R -- T:toDB
mp:R -- L:extA
extA:R -- L:extB
extB:R -- L:extC
extC:R -- L:extD
extD:R -- L:extE
extB:B --> T:uie
extC:B --> T:fse
extD:B --> T:dce
extE:B --> T:dse
Virtual Library Implementation
The virtual library system creates a seamless integration between Kobo’s native kepub collection and KOReader’s file browser, allowing users to access their Kobo library without switching between reading applications.
Overview
The virtual library implementation consists of several key components:
- Metadata Parser (
src/metadata_parser.lua) - Reads Kobo’s SQLite database to retrieve book metadata - Virtual Filesystem (
src/filesystem_ext.lua) - Provides filesystem operations for virtual paths - File Chooser Extensions (
src/filechooser_ext.lua) - Integrates the virtual library into KOReader’s file browser
Key Features
- Automatic Discovery: Scans Kobo’s kepub directory and matches files with database metadata
- DRM Detection: Identifies encrypted books to prevent access errors
- Metadata Integration: Displays book titles, authors, and cover images from Kobo’s database
- Transparent Access: Books appear as regular files in KOReader’s file browser
Topics
- DRM Detection - How the plugin identifies encrypted books
DRM Detection
The virtual library needs to identify which books are encrypted with DRM to prevent users from attempting to open books that KOReader cannot read. This is critical for providing a good user experience and avoiding error messages when browsing the library.
Why Content-Based Detection?
Historical Approach: rights.xml
Earlier approaches to DRM detection relied on checking for the presence of a rights.xml file in
the EPUB/KEPUB archive. This file is part of Adobe’s ADEPT DRM system and typically contains
metadata about the DRM protection.
Problems with this approach:
- False Positives: Some DRM-free books may contain
rights.xmlfiles that are simply empty or contain non-restrictive metadata - Incomplete: Not all DRM systems use
rights.xml- other protection schemes exist - Unreliable: The presence of the file doesn’t guarantee the content is actually encrypted
Current Approach: Content Examination
The plugin now examines the actual content files within the EPUB/KEPUB archive to determine if they are readable. This provides a more reliable detection mechanism that works across different DRM implementations.
References:
- https://github.com/OGKevin/kobo.koplugin/issues/119
Database & Data Storage Overview
This section explains how the plugin interacts with both Kobo’s database and KOReader’s storage system to synchronize reading progress.
The Fundamental Difference
graph TD
subgraph "Kobo's Approach"
A[Single SQLite Database] --> B[All books in one place]
B --> C[Chapter-based positioning]
C --> D[Unknown coordinate format]
end
subgraph "KOReader's Approach"
E[Sidecar files per book] --> F[Distributed storage]
F --> G[Percentage-based positioning]
G --> H[Precise decimal positioning]
end
style A fill:#fff3e0
style E fill:#e1f5ff
Why This Matters
These architectural differences create the core challenge the plugin solves:
- Storage location: Kobo uses one central database, KOReader uses individual files
- Position format: Kobo uses chapter+coordinate system (format unknown), KOReader uses percentages
- Precision: Kobo can point to specific positions within chapters, KOReader tracks overall progress
- Timestamps: Kobo stores ISO 8601 strings in database, KOReader uses file modification times
Quick Reference
Data Flow Summary
sequenceDiagram
participant K as Kobo DB
participant P as Plugin
participant R as KOReader
Note over K,R: Reading State Sync
P->>K: Read: percent, timestamp, status
P->>R: Read: percent_finished, file mtime
P->>P: Compare timestamps
alt Kobo is newer
P->>R: Update percent_finished
Note over R: KOReader gets Kobo's progress
else KOReader is newer
P->>K: Update at chapter boundary
Note over K: Kobo gets KOReader's progress
else Equal
Note over P: No sync needed
end
Key Files in Codebase
| File | Purpose |
|---|---|
src/lib/kobo_state_reader.lua | Reads progress from Kobo database |
src/lib/kobo_state_writer.lua | Writes progress to Kobo database |
src/lib/sync_decision_maker.lua | Decides when/how to sync |
src/reading_state_sync.lua | Coordinates sync operations |
src/metadata_parser.lua | Queries Kobo database for book info |
See the individual topics above for detailed explanations of how each system works and how the plugin bridges them.
Kobo Database
This section covers the Kobo SQLite database and how the plugin interacts with it.
Contents
- Schema - Database structure and field definitions
- Progress Storage - How reading progress is calculated and stored
- Queries - SQL queries used by the plugin
Overview
The Kobo database is a SQLite database located at /mnt/onboard/.kobo/KoboReader.sqlite. It
contains all book metadata, reading progress, and user annotations for books purchased from the Kobo
store or synced through Kobo’s ecosystem.
The plugin reads from this database to pull reading progress into KOReader, and writes to it to push KOReader’s progress back to Kobo.
Kobo Database Schema
This document describes the Kobo SQLite database schema and the key tables used by the plugin.
Database Location
The Kobo database is located at:
/mnt/onboard/.kobo/KoboReader.sqlite
Key Tables
Content Table
The primary table containing book and chapter information.
Relevant Fields
| Field | Type | Purpose | Example |
|---|---|---|---|
ContentID | TEXT | Unique identifier (PRIMARY KEY) | "0N3773Z7HFPXB" |
ContentType | INTEGER | 6 = Book entry, 9 = Chapter entry | 6 |
BookTitle | TEXT | Book title | "The Great Gatsby" |
Attribution | TEXT | Author information | "F. Scott Fitzgerald" |
___PercentRead | INTEGER | Reading progress (0-100) | 67 |
___FileOffset | INTEGER | Cumulative percentage where chapter starts | 50 (chapter starts at 50%) |
___FileSize | INTEGER | Percentage size of this chapter | 10 (chapter is 10% of book) |
DateLastRead | TEXT | Last reading timestamp (ISO 8601) | "2024-01-15 14:30:00.000+00:00" |
ReadStatus | INTEGER | Reading status code (see below) | 1 |
ChapterIDBookmarked | TEXT | Current chapter bookmark | "chapter1.html#kobo.1.1" |
ReadStatus Codes
0 = Unread/Unopened -- Book never opened
1 = Reading -- Currently reading
2 = Finished -- Book completed
3 = Reading (alt) -- Alternative reading status
ContentType Values
6 = Book entry -- Main book record
9 = Chapter entry -- Individual chapter records
Timestamp Format
Kobo uses ISO 8601 format:
2024-01-15 14:30:00.000+00:00
The plugin converts between Unix timestamps and this format:
-- Parse Kobo timestamp to Unix timestamp
function parseKoboTimestamp(date_string)
local year, month, day, hour, min, sec =
date_string:match("(%d+)-(%d+)-(%d+)[T ](%d+):(%d+):(%d+)")
return os.time({
year = tonumber(year),
month = tonumber(month),
day = tonumber(day),
hour = tonumber(hour),
min = tonumber(min),
sec = tonumber(sec),
})
end
-- Format Unix timestamp to Kobo format
function formatKoboTimestamp(timestamp)
return os.date("!%Y-%m-%d %H:%M:%S.000+00:00", timestamp)
end
Progress Storage in Kobo Database
This document explains how reading progress is stored and calculated in the Kobo database.
Book vs Chapter Entries
Each book has:
- One ContentType=6 entry: The main book record with overall metadata
- Multiple ContentType=9 entries: One for each chapter
graph TD
B[Book Entry<br/>ContentType=6<br/>ContentID: 0N3773Z7HFPXB] --> C1[Chapter 1<br/>ContentType=9<br/>ContentID: 0N3773Z7HFPXB!!chapter1.html]
B --> C2[Chapter 2<br/>ContentType=9<br/>ContentID: 0N3773Z7HFPXB!!chapter2.html]
B --> C3[Chapter 3<br/>ContentType=9<br/>ContentID: 0N3773Z7HFPXB!!chapter3.html]
style B fill:#e1f5ff
style C1 fill:#fff3e0
style C2 fill:#fff3e0
style C3 fill:#fff3e0
Progress Calculation
Progress is calculated based on percentage ranges of chapters:
-- ___FileOffset = cumulative percentage where chapter starts (stored by Kobo)
-- ___FileSize = percentage size of the chapter
-- ___PercentRead (in chapter entry) = progress within this chapter (0-100)
-- To calculate overall progress, the plugin uses ___FileOffset directly:
-- 1. Looks up the current chapter entry
-- 2. Gets ___FileOffset (where chapter starts)
-- 3. Adds current chapter contribution: (chapter_size * chapter_percent / 100)
-- Example calculation:
chapter_offset = current_chapter.___FileOffset -- Where chapter starts
current_chapter_contribution = (current_chapter.___FileSize * current_chapter.___PercentRead) / 100
overall_percent = chapter_offset + current_chapter_contribution
Example:
Book: 3 chapters (ordered by ___FileOffset)
Chapter 1: ___FileOffset=0, ___FileSize=30 (spans 0-30%) [completed]
Chapter 2: ___FileOffset=30, ___FileSize=40 (spans 30-70%) [reading at 50%]
Chapter 3: ___FileOffset=70, ___FileSize=30 (spans 70-100%) [not started]
Calculation:
- Completed chapters: 30% (Chapter 1)
- Current chapter: 40 * 0.5 = 20%
- Overall: 30 + 20 = 50%
Important:
___FileOffsetand___FileSizeare percentage values, not byte counts- All chapter
___FileSizevalues sum to 100
Why Chapter Boundaries?
The plugin can only write progress at chapter boundaries because:
- Kobo’s chapter structure: Each chapter is a separate database row (ContentType=9)
- Bookmark format limitation: The
ChapterIDBookmarkedformat is"chapter.html#kobo.x.y"wherex.yare some form of coordinates (assumed to be in the HTML document, but exact format is unknown) - Coordinate mapping unknown: There is no known way to convert KOReader’s position data into
the
x.yformat that Kobo uses - Chapter-level precision: The plugin defaults to
#kobo.1.1(start of chapter) for all bookmarks
When syncing to Kobo, the plugin:
- Finds the chapter that contains the target percentage
- Sets
ChapterIDBookmarkedto that chapter’s ID - Updates that chapter’s
___PercentReadto reflect position within the chapter - Updates the main book entry’s
___PercentReadto the overall percentage
graph LR
A[KOReader Position<br/>67.3%] --> B[Find Chapter<br/>Chapter with ___FileOffset ≤ 67]
B --> C[Found: Chapter 3<br/>___FileOffset=60, ___FileSize=20]
C --> D[Calculate within-chapter %<br/>67 - 60 / 20 = 35%]
D --> E[Update Chapter 3<br/>___PercentRead = 35]
E --> F[Update Book Entry<br/>___PercentRead = 67]
F --> G[Set Bookmark<br/>ChapterIDBookmarked = chapter3.html#kobo.1.1]
Database Queries
This document lists all SQL queries used by the plugin to interact with the Kobo database.
Virtual Library Book Discovery
The virtual library uses a reverse lookup approach to discover books:
-- Load all book metadata once at startup
SELECT ContentID, Title, Attribution, Publisher, Series, SeriesNumber, ___PercentRead
FROM content
WHERE ContentType = 6
AND ContentID NOT LIKE 'file://%'
Why Reverse Lookup?
Previously, the plugin queried the database first with ID format filters (NOT LIKE '%-%' to
exclude UUID-style IDs), then checked if those files existed. This failed for books synced from
Calibre Web which use UUID-style IDs like a3a06c7b-f1a0-4f6b-8fae-33b6926124e4.
The current approach:
- Loads all metadata once from the database and caches it in memory
- Fetches all books with ContentType=6 (books)
- Excludes file:// prefixed paths which are not actual kepub files
- Results are cached and reused until the database is modified
- Scans the kepub directory (
/mnt/onboard/.kobo/kepub/) for all files - Filters out encrypted files by checking for valid ZIP/EPUB signature (PK\x03\x04)
- Looks up metadata in cache for unencrypted files (no additional database queries)
- Merges the results to create the final accessible book list
This approach:
- Supports all book ID formats regardless of naming conventions
- Single database query for all metadata
- In-memory cache reused across multiple lookups
Reading Progress (Pull from Kobo)
-- Main book query
SELECT DateLastRead, ReadStatus, ChapterIDBookmarked, ___PercentRead
FROM content
WHERE ContentID = ? AND ContentType = 6
LIMIT 1
-- Chapter lookup (to calculate exact progress using ___FileOffset directly)
SELECT ContentID, ___FileOffset, ___FileSize, ___PercentRead
FROM content
WHERE ContentID LIKE '?%' AND ContentType = 9
AND (ContentID LIKE '%?' OR ContentID LIKE '%?#%')
LIMIT 1
Writing Progress (Push to Kobo)
-- Find target chapter using ___FileOffset
SELECT ContentID, ___FileOffset, ___FileSize
FROM content
WHERE ContentID LIKE '?%' AND ContentType = 9
AND ___FileOffset <= ?
ORDER BY ___FileOffset DESC
LIMIT 1
-- Fallback: Get last chapter (if position is beyond all chapters)
SELECT ContentID
FROM content
WHERE ContentID LIKE '?%' AND ContentType = 9
ORDER BY ___FileOffset DESC
LIMIT 1
-- Update main book entry
UPDATE content
SET ___PercentRead = ?,
DateLastRead = ?,
ReadStatus = ?,
ChapterIDBookmarked = ?
WHERE ContentID = ? AND ContentType = 6
-- Update current chapter entry
UPDATE content
SET ___PercentRead = ?
WHERE ContentID = ? AND ContentType = 9
Data Flow Diagram
sequenceDiagram
participant P as Plugin
participant DB as Kobo Database
participant BE as Book Entry<br/>(ContentType=6)
participant CE as Chapter Entries<br/>(ContentType=9)
Note over P: Reading from Kobo
P->>DB: Query book entry
DB->>BE: SELECT DateLastRead, ReadStatus, ChapterIDBookmarked, ___PercentRead
BE-->>P: Return book data
P->>DB: Query chapters
DB->>CE: SELECT ContentID, ___FileOffset, ___FileSize, ___PercentRead
CE-->>P: Return chapter data
P->>P: Calculate total progress<br/>from chapter offsets/sizes
Note over P: Writing to Kobo
P->>P: Find target chapter<br/>for percentage
P->>DB: Update chapter entry
DB->>CE: UPDATE ___PercentRead
P->>DB: Update book entry
DB->>BE: UPDATE ___PercentRead, DateLastRead,<br/>ReadStatus, ChapterIDBookmarked
KOReader Data Storage
This section covers how KOReader stores reading progress and metadata.
Contents
- DocSettings - Sidecar file structure and percent calculation
- ReadHistory - Timestamp tracking and challenges
- Data Flow - How data moves between systems
Overview
KOReader stores reading progress in “sidecar” files alongside each book. These are Lua table files that contain the reading position, status, and other metadata.
Unlike Kobo’s centralized database, KOReader uses a distributed approach where each book has its own metadata file. The plugin reads from these files to push progress to Kobo, and writes to them when pulling progress from Kobo.
KOReader DocSettings
This document describes how KOReader stores reading progress in sidecar files.
DocSettings (Sidecar Files)
KOReader stores reading progress in “sidecar” files alongside the book files. These are Lua tables serialized to disk.
File Location
For a book at /mnt/onboard/.kobo/kepub/book.epub, the sidecar is at:
/mnt/onboard/.kobo/kepub/book.sdr/metadata.epub.lua
Key Fields
{
-- Core progress data
percent_finished = 0.673, -- 0.0 to 1.0 (67.3% read)
last_percent = 0.673, -- Last known percent
-- Status and metadata
summary = {
status = "reading", -- "reading", "complete", or "finished"
modified = "2024-01-15", -- Last modification date
},
-- Page/position data (depends on document type)
last_xpointer = "/body/div[2]/p[15]", -- Position in EPUB
page = 42, -- Current page number (PDFs)
-- Timestamps (stored by ReadHistory, not in sidecar directly)
-- See ReadHistory section below
}
How KOReader Calculates Percent
The percent_finished field is calculated differently based on document type:
EPUB (Reflowable)
-- Position is tracked by XPointer (path in DOM tree)
-- Percentage = (current_position_bytes / total_document_bytes)
-- Example:
percent_finished = 0.673 -- 67.3% through the document
PDF (Fixed Layout)
-- Position is tracked by page number
-- Percentage = (current_page / total_pages)
-- Example:
-- Page 42 of 100 pages
percent_finished = 0.42 -- 42%
ReadHistory and Timestamps
This document explains how KOReader tracks reading history and the challenges of determining accurate timestamps.
ReadHistory
KOReader maintains a separate ReadHistory object that tracks:
- When books were last opened
- Reading duration
- Final reading position
Location
-- Global ReadHistory module
require("readhistory")
Data Structure
ReadHistory.hist = {
["/path/to/book.epub"] = {
file = "/path/to/book.epub",
time = 1705330200, -- Unix timestamp of last access
},
}
Timestamp Challenges
The plugin needs to determine “when was this book last read” to compare with Kobo’s timestamp.
Source of Timestamp
The plugin uses ReadHistory only as the source of truth for KOReader timestamps:
ReadHistory.hist = {
["/path/to/book.epub"] = {
file = "/path/to/book.epub",
time = 1705330200, -- Unix timestamp from ReadHistory
},
}
Critical Validation: Sidecar File Check
A ReadHistory timestamp is only valid if the sidecar file exists:
function getValidatedKOReaderTimestamp(doc_path)
-- 1. Get timestamp from ReadHistory
local kr_timestamp = getKOReaderTimestampFromHistory(doc_path)
if kr_timestamp == 0 then
return 0
end
-- 2. Validate that sidecar file exists
local has_sidecar = DocSettings:hasSidecarFile(doc_path)
-- 3. Return 0 if no sidecar (no actual reading progress)
if not has_sidecar then
-- This ensures PULL from Kobo when KOReader has no valid data
return 0
end
return kr_timestamp
end
Why Sidecar Validation Matters
Without a sidecar (.sdr) file, there’s no actual reading progress in KOReader. A ReadHistory entry
without a sidecar is unreliable:
- Could be from after a reset that deleted the
.sdrfile - Could be a stale entry from a deleted book
By returning 0 when no sidecar exists, the plugin ensures:
- PULL from Kobo: Kobo’s timestamp will always be newer than
0
Data Flow and Status Mapping
This document shows how data flows between the plugin and KOReader, and how status values are converted.
Data Flow: Reading from KOReader
sequenceDiagram
participant P as Plugin
participant DS as DocSettings
participant RH as ReadHistory
participant FS as File System
P->>DS: readSetting("percent_finished")
DS-->>P: 0.673
P->>DS: readSetting("summary")
DS-->>P: {status: "reading"}
P->>RH: Get timestamp for book
RH-->>P: 1705330200 or 0
P->>FS: Check if sidecar exists
FS-->>P: true/false
alt Sidecar exists
P->>P: Use ReadHistory timestamp<br/>= 1705330200
else No sidecar
P->>P: Return 0<br/>(ensures PULL from Kobo)
end
Data Flow: Writing to KOReader
sequenceDiagram
participant P as Plugin
participant DS as DocSettings
participant BL as BookList Cache
Note over P: Syncing 67% from Kobo
P->>DS: saveSetting("percent_finished", 0.67)
P->>DS: saveSetting("last_percent", 0.67)
P->>DS: Read current summary
DS-->>P: {status: "reading", modified: "2024-01-14"}
P->>P: Update status if needed<br/>(67% -> still "reading")
P->>DS: saveSetting("summary", updated_summary)
P->>DS: flush() (write to disk)
P->>BL: Update UI cache
Note over BL: BookList widget will show 67%
Status Mapping
KOReader and Kobo use different status values:
| KOReader | Kobo ReadStatus | Meaning |
|---|---|---|
"reading" | 1 | In progress |
"complete" | 2 | Finished |
"finished" | 2 | Finished (alternative) |
| (not set) | 0 | Unopened |
-- Converting Kobo -> KOReader
function StatusConverter.koboToKoreader(kobo_status)
if kobo_status == 0 then return nil end -- Unopened
if kobo_status == 2 then return "complete" end -- Finished
return "reading" -- 1 or 3
end
-- Converting KOReader -> Kobo
function StatusConverter.koreaderToKobo(kr_status)
if kr_status == "complete" or kr_status == "finished" then
return 2 -- Finished
end
return 1 -- Reading
end
Example: Complete Sync Flow
sequenceDiagram
participant K as Kobo DB
participant P as Plugin
participant DS as DocSettings
participant RH as ReadHistory
Note over P: User opens book in virtual library
P->>K: Read Kobo state
K-->>P: 45%, timestamp: 1705300000
P->>DS: Read KOReader state
DS-->>P: percent_finished: 0.67
P->>RH: Get last read time
RH-->>P: 1705330000
P->>P: Compare timestamps<br/>KOReader newer (1705330000 > 1705300000)
Note over P: Decision: Push KOReader -> Kobo
P->>K: Write 67% to Kobo<br/>at chapter boundary
K-->>P: Success
Note over K: Kobo now shows 67%<br/>at start of chapter 2
Sync Decision Logic
This document explains how the plugin decides when to sync, in which direction, and how conflicts are resolved.
Sync Triggers
The plugin performs sync operations at specific times:
1. Virtual Library Access (Auto-sync)
graph TD
A[Open Virtual Library] --> B{Auto-sync enabled?}
B -->|Yes| C[For each book]
B -->|No| D[Just show library]
C --> E[Compare timestamps]
E --> F{Which is newer?}
F -->|Kobo| G[PULL: Kobo -> KOReader]
F -->|KOReader| H[PUSH: KOReader -> Kobo]
F -->|Equal| I[Skip sync]
2. Document Close
graph TD
A[Close Document] --> B{Is Kepub?}
B -->|No| C[Normal close]
B -->|Yes| D{Auto-sync enabled?}
D -->|No| E[Skip sync]
D -->|Yes| F[PUSH: KOReader -> Kobo]
F --> G[Update Kobo with current time]
3. Manual Sync
graph TD
A[User triggers manual sync] --> B[Read both states]
B --> C[Compare timestamps]
C --> D{Which is newer?}
D -->|Kobo| E[PULL: Kobo -> KOReader]
D -->|KOReader| F[PUSH: KOReader -> Kobo]
D -->|Equal| G[No sync needed]
Timestamp Comparison
The core of sync decision-making is comparing timestamps:
function syncBidirectional(book_id, doc_settings)
-- Read both states
local kobo_state = readKoboState(book_id)
local kr_percent = doc_settings:readSetting("percent_finished") or 0
local kr_timestamp = getValidatedKOReaderTimestamp(doc_path)
-- Check if both sides are complete
if areBothSidesComplete(kobo_state, kr_percent, kr_status) then
return false -- Skip sync
end
-- Compare timestamps
if kobo_state.timestamp > kr_timestamp then
-- Kobo is newer -> PULL
return executePullFromKobo(book_id, doc_settings, kobo_state, kr_percent, kr_timestamp)
end
-- KOReader is newer (or equal) -> PUSH
return executePushToKobo(book_id, doc_settings, kobo_state, kr_percent, kr_timestamp)
end
Special Cases
Both Sides Complete
If both systems show the book as finished, skip sync to avoid unnecessary writes:
function areBothSidesComplete(kobo_state, kr_percent, kr_status)
local kobo_complete = (kobo_state.status == "complete") or
(kobo_state.percent_read >= 100)
local kr_complete = (kr_status == "complete") or
(kr_status == "finished") or
(kr_percent >= 1.0)
return kobo_complete and kr_complete
end
-- Usage in sync logic
if areBothSidesComplete(kobo_state, kr_percent, kr_status) then
logger.info("Both sides complete, skipping sync")
return false
end
Unopened Books (ReadStatus=0)
Books that have never been opened in Kobo’s native reader need special handling:
-- When PULLING from Kobo
if kobo_state.kobo_status == 0 and kobo_state.percent_read == 0 then
-- Book is unopened in Kobo, don't sync TO KOReader
-- (Would overwrite KOReader progress with 0%)
logger.info("Skipping sync for unopened book")
return false
end
-- When PUSHING to Kobo
-- Always allow! This is how users start reading in KOReader first
This asymmetry is important:
- PULL: Don’t overwrite KOReader progress with Kobo’s 0%
- PUSH: Always allow KOReader to update Kobo (user may have started in KOReader)
Missing Sidecar File
If a book has ReadHistory but no sidecar file, the ReadHistory timestamp is considered unreliable:
function getValidatedKOReaderTimestamp(doc_path)
local kr_timestamp = getKOReaderTimestampFromHistory(doc_path)
if kr_timestamp == 0 then
return 0
end
-- Check if sidecar file exists
local has_sidecar = DocSettings:hasSidecarFile(doc_path)
if not has_sidecar then
-- No sidecar = book never actually read in KOReader
-- ReadHistory might be from just opening/previewing
logger.dbg("No sidecar file found - ignoring ReadHistory timestamp")
return 0
end
-- Sidecar exists, return the ReadHistory timestamp
return kr_timestamp
end
This ensures that if KOReader has no actual reading progress data, Kobo’s timestamp will be considered newer (or equal), triggering a PULL from Kobo.
User Approval Dialogs
Depending on settings, the plugin may prompt the user before syncing:
function syncIfApproved(from_kobo, to_kobo, sync_callback, sync_details)
-- Check if user wants a prompt for this direction
local needs_approval = false
if from_kobo and self.settings.from_kobo_prompt then
needs_approval = true
end
if to_kobo and self.settings.to_kobo_prompt then
needs_approval = true
end
if needs_approval then
-- Show dialog with details
showSyncDialog(sync_details, function(approved)
if approved then
sync_callback()
end
end)
else
-- Silent sync
sync_callback()
end
end
Conflict Resolution Flow
graph TD
A[Sync Triggered] --> B[Read Both States]
B --> C{Both complete?}
C -->|Yes| D[Skip sync]
C -->|No| E{Compare timestamps}
E --> F{Which newer?}
F -->|Kobo newer| G{Kobo unopened?}
F -->|KOReader newer| K[PUSH to Kobo]
F -->|Equal| L[No sync needed]
G -->|Yes| H[Skip sync<br/>Keep KOReader state]
G -->|No| I{User approval needed?}
I -->|Yes| J[Show dialog]
I -->|No| M[PULL from Kobo]
J --> N{User approves?}
N -->|Yes| M
N -->|No| O[Cancel sync]
K --> P{User approval needed?}
P -->|Yes| Q[Show dialog]
P -->|No| R[Write to Kobo]
Q --> S{User approves?}
S -->|Yes| R
S -->|No| T[Cancel sync]
Why Chapter Boundaries?
When syncing TO Kobo, the position is rounded to chapter boundaries. This is a technical limitation, not a design choice.
The Problem
KOReader tracks position as:
percent_finished = 0.673 -- 67.3% through the book
Kobo tracks position as:
ChapterIDBookmarked = "chapter2.html#kobo.3.5"
-- Where "3.5" are coordinates (format unknown, possibly HTML-based)
The coordinate format is unknown. While KOReader tracks its own position data:
- Overall percentage (0.0 to 1.0)
- Current page in the document
- Internal positioning information
There is no known way to convert this into the x.y coordinate format that Kobo uses.
The Solution
The plugin:
- Calculates which chapter contains the 67.3% position
- Finds that chapter in Kobo’s database using SQL
- Sets the bookmark to the start of that chapter
function findChapterForPercentage(conn, book_id, percent_read)
-- Use SQL to find the chapter that starts at or before the target position
-- ___FileOffset is already the cumulative percentage (calculated by Kobo)
local chapters_res = conn:exec(
"SELECT ContentID, ___FileOffset, ___FileSize FROM content " ..
"WHERE ContentID LIKE '" .. book_id .. "%' " ..
"AND ContentType = 9 " ..
"AND ___FileOffset <= " .. percent_read .. " " ..
"ORDER BY ___FileOffset DESC LIMIT 1"
)
if chapters_res and chapters_res[1] and #chapters_res[1] > 0 then
local chapter_id = chapters_res[1][1]
local chapter_offset = chapters_res[2][1]
local chapter_size = chapters_res[3][1]
-- Calculate progress within this chapter
local within_chapter = percent_read - chapter_offset
local chapter_percent = (within_chapter / chapter_size) * 100
-- Return the chapter bookmark
return chapter_id .. "#kobo.1.1" -- Start of chapter (default coordinate)
end
end
Why Not Calculate the Coordinates?
- Format unknown: The exact meaning and calculation of the
x.ycoordinate format used by Kobo is not documented or known - No conversion method: There is no known way to map KOReader’s position data to Kobo’s coordinate system
- Chapter-level fallback: Using
#kobo.1.1(start of chapter) is the safe default - Progress still tracked: The chapter’s
___PercentReadfield stores within-chapter progress percentage
The Result
graph LR
A[KOReader<br/>67.3%<br/>Mid-chapter] -->|Sync to Kobo| B[Kobo<br/>67%<br/>Chapter start]
B -->|Sync back to KOReader| C[KOReader<br/>67.0%<br/>Chapter start]
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#e1f5ff
Full Sync Decision Pseudocode
function syncBidirectional(book_id, doc_settings)
-- 1. Read states
local kobo_state = readKoboState(book_id)
local kr_percent = doc_settings:readSetting("percent_finished") or 0
local kr_timestamp = getValidatedKOReaderTimestamp(doc_path)
local kr_status = doc_settings:readSetting("summary").status
-- 2. Check if both complete
if areBothSidesComplete(kobo_state, kr_percent, kr_status) then
return false -- Skip sync
end
-- 3. Compare timestamps
if kobo_state.timestamp > kr_timestamp then
-- Kobo is newer -> PULL
return executePullFromKobo(book_id, doc_settings, kobo_state, kr_percent, kr_timestamp)
end
-- KOReader is newer or equal -> PUSH
return executePushToKobo(book_id, doc_settings, kobo_state, kr_percent, kr_timestamp)
end
Configuration Options
Users can control sync behavior through settings:
| Setting | Purpose | Default |
|---|---|---|
sync_enabled | Enable/disable all sync | ON |
auto_sync_enabled | Auto-sync on library access | ON |
from_kobo_enabled | Allow PULL from Kobo | ON |
to_kobo_enabled | Allow PUSH to Kobo | ON |
from_kobo_prompt | Prompt before PULL | SILENT |
to_kobo_prompt | Prompt before PUSH | SILENT |
These settings create different sync strategies:
- Kobo → KOReader only: Disable
to_kobo_enabled - KOReader → Kobo only: Disable
from_kobo_enabled - Manual control: Enable prompts for both directions
- Fully automatic: Disable all prompts (recommended)
See Settings Documentation for details.
Bluetooth Integration
This section covers the Bluetooth integration in the Kobo plugin, including dispatcher actions and key binding configuration for Bluetooth input devices.
Supported devices
MTK-based Kobo devices with Bluetooth support:
- Kobo Libra Colour
- Kobo Clara BW / Colour
Contents
- Dispatcher Integration - How paired devices are registered as dispatcher actions
- Key Bindings - Adding and configuring Bluetooth key bindings and custom actions
Bluetooth Dispatcher Integration
This document explains the design decisions behind registering Bluetooth actions and devices as dispatcher actions and how it integrates with KOReader’s lifecycle.
Overview
The plugin exposes Bluetooth control actions and paired Bluetooth devices as dispatcher actions, allowing other plugins and features to control Bluetooth state and trigger device connections. This requires careful handling of device state management and lifecycle coordination.
Registered Actions
Bluetooth Control Actions
The plugin registers the following control actions:
| Action ID | Title | Description |
|---|---|---|
enable | Enable Bluetooth | Turns Bluetooth on |
disable | Disable Bluetooth | Turns Bluetooth off |
toggle | Toggle Bluetooth | Toggles Bluetooth on/off based on state |
scan | Scan for Bluetooth Devices | Starts device scan and shows results |
These are registered via registerBluetoothActionsWithDispatcher() during plugin initialization.
Device Connection Actions
Each paired device gets a unique action ID based on its MAC address:
bluetooth_connect_<MAC_WITH_UNDERSCORES>. These are registered via
onDispatcherRegisterActions().
Design Decisions
Device List Persistence
Paired devices are stored in plugin settings rather than queried from Bluetooth in real-time.
Why: According to the Bluetooth investigation, D-Bus commands return no data when Bluetooth is disabled. This means dispatcher actions would fail to register at startup if Bluetooth is off. By maintaining a persistent list in settings, actions are always available regardless of Bluetooth state.
Implementation: When Bluetooth is turned on or devices are accessed, the paired device list is
synchronized from the system into plugin.settings.paired_devices. This ensures the dispatcher
always has current information.
Standby Prevention During Bluetooth
When Bluetooth is enabled, the plugin calls UIManager:preventStandby() to keep the device awake.
Why: MTK Bluetooth hardware requires the system to remain active to maintain connections. If the device suspends, Bluetooth connections are lost.
Trade-off: Users cannot use device suspension while Bluetooth is enabled. This is documented in the user-facing feature guide so users understand the behavior.
Action Registration at Startup
Dispatcher actions for Bluetooth control and paired devices are registered during plugin
initialization. Control actions are registered via registerBluetoothActionsWithDispatcher(), and
device actions via onDispatcherRegisterActions().
Why: The dispatcher needs to know about available actions before user interactions. Registering at startup ensures all actions are available immediately.
Benefit: Users can use dispatcher actions in gestures, profiles, and other automation features without additional setup.
Unique Action IDs
Control actions use simple identifiers (enable, disable, toggle, scan). Each device gets a
stable action ID based on its MAC address: bluetooth_connect_<MAC_WITH_UNDERSCORES>.
Why: MAC addresses are unique identifiers that persist across reboots. This ensures the same device always has the same action ID, allowing users to configure gestures that survive restarts.
Connection Flow
When a device connection dispatcher action is executed:
- Plugin checks if Bluetooth is enabled
- If disabled, it automatically turns Bluetooth on
- Device connection is attempted
- User sees status message
This flow is transparent to the dispatcher caller - they simply trigger an action ID without needing to manage Bluetooth state.
Control Action Flow
When a Bluetooth control action is executed:
- enable: Calls
turnBluetoothOn() - disable: Calls
turnBluetoothOff(true)(with popup notification) - toggle: Calls
toggleBluetooth(true)(with popup notification when turning off) - scan: Calls
scanAndShowDevices()
The onBluetoothAction(action_id) method handles routing to the appropriate method.
Integration with Investigations
This implementation is based on findings from the Bluetooth Control investigation:
- D-Bus limitations: Understanding that D-Bus returns no data when Bluetooth is off informed the decision to persist device lists in settings
- Non-idempotent kernel driver: The investigation’s findings about kernel panic on driver reloading informed the decision to prevent device suspension while Bluetooth is active, avoiding unnecessary driver state changes
- D-Bus auto-activation: Knowing that a single method call triggers initialization, the plugin optimizes by only calling when necessary
Related Documentation
- Bluetooth Control Investigation - Technical findings about Kobo’s Bluetooth implementation
- Bluetooth feature guide - User-facing documentation
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.
Investigations
This section contains technical investigations and research notes for experimental features and system integrations that are being explored for the kobo.koplugin.
Investigations
Bluetooth Integration
Investigating methods to control Kobo device Bluetooth functionality from KOReader via Linux D-Bus interfaces (BlueZ). The goal is to enable/disable Bluetooth, discover devices, and connect to paired accessories (gamepads, audio devices) without relying on Nickel’s UI.
Key Areas:
- D-Bus command reference for BlueZ adapter control
- Event sequence analysis from dbus-monitor captures
- Shell script implementation approach
- Lua integration strategies (os.execute vs FFI)
- Fallback option using libnickel direct calls
Purpose of Investigations
Investigation documents serve to:
- Document Research: Track findings, commands, and technical details during exploration
- Share Knowledge: Provide context for future contributors working on these features
- Future Reference: Preserve information that may be useful even if the feature isn’t immediately implemented
Bluetooth Control Investigations
This section documents the technical investigation for Bluetooth control on different Kobo device types.
Devices
MTK Devices
- Uses MTK-specific Bluetooth implementation
- D-Bus service:
com.kobo.mtk.bluedroid - Custom command set and initialization sequence
- See MTK Documentation
Non-MTK Devices (Libra 2, etc.)
- Uses standard Linux BlueZ stack
- D-Bus service:
org.bluez - Standard Bluetooth operations
- See Libra 2 Documentation
Bluetooth Control Investigation
Device: Kobo Libra Colour (MTK Bluetooth chipset)
Overview
This investigation documents how to control Bluetooth on Kobo e-readers from the system level, enabling programmatic control without Nickel’s UI. The investigation focuses on understanding the D-Bus interface, service initialization, and safe shutdown procedures.
Key Findings
Custom D-Bus Wrapper
Kobo does NOT expose the standard org.bluez D-Bus service. Instead, all BlueZ operations are
routed through com.kobo.mtk.bluedroid.
Evidence:
# Standard BlueZ name does NOT exist
$ dbus-send --system --dest=org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus.GetNameOwner \
string:org.bluez
Error: Could not get owner of name 'org.bluez': no such name
# Kobo's wrapper only returns adapter properties after Bluetooth is started
$ dbus-send --system --dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.GetAll \
string:org.bluez.Adapter1
# (Returns properties only if Bluetooth is running)
D-Bus Auto-Activation
Analysis of Nickel’s Bluetooth initialization (strace, 7678 lines) revealed:
Timeline:
15:18:19.863 - Nickel calls BluedroidManager1.On()
15:18:19.947 - D-Bus auto-starts service (84ms later)
15:18:22.986 - Adapter ready (3.1 seconds after start)
A single D-Bus method call triggers the entire initialization via D-Bus auto-activation from
/usr/share/dbus-1/system-services/com.kobo.mtk.bluedroid.service.
Non-Idempotent Kernel Driver
See Known Issues for details.
Navigation
- Architecture - Stack components and D-Bus services
- Initialization - How to start Bluetooth
- Operations - Scan, connect, status commands
- Shutdown - Safe shutdown procedure
- Known Issues - Kernel panic investigation
- Input Device Mapping - Mapping Bluetooth devices to
/dev/input/eventN
References
- KOReader Issue #12739 - Kernel panic on exit
- NickelMenu PR #152 - libnickel integration
Bluetooth Stack Architecture
Components
Kobo’s Bluetooth implementation consists of three main processes:
| Process | Path | Purpose |
|---|---|---|
mtkbtd-launcher | /usr/local/Kobo/ | Launch script for MTK Bluetooth |
mtkbtd | /usr/local/Kobo/mtkbtd | MediaTek Bluetooth daemon |
btservice | /usr/bin/btservice | Bluetooth service (spawned by mtkbtd) |
Kernel Modules
wmt_drv- MediaTek WMT driver (main driver)wmt_chrdev_wifi- WiFi character devicewmt_cdev_bt- Bluetooth character devicewlan_drv_gen4m- WLAN driver
Checking Loaded Modules
# Verify modules are loaded
lsmod | grep -E "(wmt|wlan|bt)"
# Expected output:
# wlan_drv_gen4m 1908365 0 - Live 0xbf14a000 (O)
# wmt_cdev_bt 16871 0 - Live 0xbf141000 (O)
# wmt_chrdev_wifi 12825 1 wlan_drv_gen4m, Live 0xbf138000 (O)
# wmt_drv 1059215 4 wlan_drv_gen4m,wmt_cdev_bt,wmt_chrdev_wifi, Live 0xbf000000 (O)
D-Bus Services
Kobo uses a custom D-Bus wrapper instead of standard BlueZ interfaces:
| Service Name | Purpose |
|---|---|
com.kobo.mtk.bluedroid | Main Bluetooth service (Kobo’s wrapper) |
com.kobo.bluetooth.Agent | Pairing/authentication agent |
org.bluez | NOT EXPOSED - use mtk.bluedroid instead |
Key Discovery: All D-Bus calls must use com.kobo.mtk.bluedroid as destination, not
org.bluez.
D-Bus Service File
Service auto-activation is configured in:
/usr/share/dbus-1/system-services/com.kobo.mtk.bluedroid.service
This allows D-Bus to automatically start mtkbtd-launcher.sh when a method is called on
com.kobo.mtk.bluedroid, even if the service isn’t running.
Verifying Service Availability
# List available D-Bus services
dbus-send --system --print-reply \
--dest=org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus.ListNames | grep -E "(bluez|bluetooth|mtk)"
# Expected output when running:
# string "com.kobo.mtk.bluedroid"
# string "com.kobo.bluetooth.Agent"
# Check if Bluetooth service exists
dbus-send --system --print-reply \
--dest=org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus.GetNameOwner \
string:com.kobo.mtk.bluedroid
Process Verification
# Check if Bluetooth processes are running
ps aux | grep -E "(mtkbtd|btservice)" | grep -v grep
# Expected output when running:
# root 1178 0.0 0.0 1234 567 ? S 15:18 0:00 {mtkbtd-launcher} /bin/sh /usr/local/Kobo/mtkbtd-launcher.sh
# root 1179 0.0 0.1 2345 1234 ? Sl 15:18 0:00 /usr/local/Kobo/mtkbtd -skipFontLoad -platform kobo:noscreen --debug
# root 1181 0.0 0.0 1234 567 ? S 15:18 0:00 /usr/bin/btservice
Bluetooth Initialization
D-Bus Auto-Activation Sequence
Analysis of Nickel’s Bluetooth initialization (strace output, 7678 lines) revealed the exact D-Bus sequence used.
Timeline
15:18:19.863567 - Nickel calls BluedroidManager1.On()
Service doesn't exist, D-Bus queues the call
15:18:19.947362 - D-Bus auto-starts service (84ms later)
NameOwnerChanged: com.kobo.mtk.bluedroid → :1.2
15:18:22.986149 - Bluetooth adapter ready (3.1 seconds after start)
InterfacesAdded: /org/bluez/hci0 with org.bluez.Adapter1
Key Finding: A single D-Bus method call (BluedroidManager1.On()) triggers the entire
initialization sequence via D-Bus auto-activation.
Initialization Commands
Full Initialization Script
#!/usr/bin/env bash
# Enable Bluetooth on Kobo
echo "Step 1: Call On() method - triggers D-Bus auto-activation (this command blocks until initialization is complete)"
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/ \
com.kobo.bluetooth.BluedroidManager1.On
# No need to wait after On(); the command only returns when initialization is done
echo "Step 2: Power on the adapter"
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Set \
string:org.bluez.Adapter1 \
string:Powered \
variant:boolean:true
echo "Step 3: Verify adapter is powered"
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Adapter1 \
string:Powered
Critical Notes
- Use
com.kobo.mtk.bluedroidas destination for all D-Bus calls, notorg.bluez - Auto-activation - D-Bus starts the service automatically when the method is called
- The object paths still use
/org/bluez/hci0but the service name iscom.kobo.mtk.bluedroid
Verification
# Check if processes started
ps aux | grep -E "(mtkbtd|btservice)" | grep -v grep
# Check if service is registered
dbus-send --system --print-reply \
--dest=org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus.ListNames | grep "com.kobo.mtk.bluedroid"
# Check adapter properties
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.GetAll \
string:org.bluez.Adapter1
Bluetooth Operations
Scan for Devices
Start Discovery
#!/usr/bin/env bash
# Scan for Bluetooth devices
echo "Starting discovery..."
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.bluez.Adapter1.StartDiscovery
echo "Scanning for 5 seconds..."
sleep 5
echo "Stopping discovery..."
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.bluez.Adapter1.StopDiscovery
List Discovered Devices
# List all discovered devices
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/ \
org.freedesktop.DBus.ObjectManager.GetManagedObjects \
| grep -A 10 '/org/bluez/hci0/dev_'
# Example output:
# /org/bluez/hci0/dev_E4_17_D8_EC_04_1E
# string "Name"
# variant string "My Bluetooth Device"
# string "Address"
# variant string "E4:17:D8:EC:04:1E"
Note: Device paths use underscores in MAC addresses (e.g., E4_17_D8_EC_04_1E), not colons.
Connect to Device
Connection Script
#!/usr/bin/env bash
# Connect to a Bluetooth device
# Usage: ./connect.sh DEVICE_MAC
# MAC format: XX_XX_XX_XX_XX_XX (underscores, not colons)
DEVICE_MAC="$1"
if [ -z "$DEVICE_MAC" ]; then
echo "Usage: $0 DEVICE_MAC (e.g., E4_17_D8_EC_04_1E)"
exit 1
fi
echo "Step 1: Check if device needs pairing"
PAIRED=$(dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Device1 \
string:Paired 2>&1 | grep boolean | awk '{print $3}')
if [ "$PAIRED" = "false" ]; then
echo "Pairing device..."
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.bluez.Device1.Pair
sleep 3
fi
echo "Step 2: Set device as trusted"
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.freedesktop.DBus.Properties.Set \
string:org.bluez.Device1 \
string:Trusted \
variant:boolean:true
echo "Step 3: Connect to device"
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.bluez.Device1.Connect
echo "Step 4: Verify connection"
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Device1 \
string:Connected
Handling “AlreadyConnected” Error
If you get Error org.bluez.Error.AlreadyConnected: already connected when the device isn’t
actually connected, the device is in a stale state. Performing a new device scan (discovery) clears
the stale state. A disconnect is not required.
Check Device Status
Get Device Properties
#!/usr/bin/env bash
# Check device connection status
# Usage: ./device_status.sh DEVICE_MAC
DEVICE_MAC="$1"
if [ -z "$DEVICE_MAC" ]; then
echo "Usage: $0 DEVICE_MAC"
exit 1
fi
echo "=== All Device Properties ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.freedesktop.DBus.Properties.GetAll \
string:org.bluez.Device1
echo ""
echo "=== Connected Status ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Device1 \
string:Connected
echo ""
echo "=== Paired Status ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Device1 \
string:Paired
echo ""
echo "=== Trusted Status ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Device1 \
string:Trusted
Check Adapter Status
#!/usr/bin/env bash
# Check Bluetooth adapter status
echo "=== All Adapter Properties ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.GetAll \
string:org.bluez.Adapter1
echo ""
echo "=== Powered Status ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Adapter1 \
string:Powered
echo ""
echo "=== Discovering Status ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Adapter1 \
string:Discovering
echo ""
echo "=== Discoverable Status ==="
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Get \
string:org.bluez.Adapter1 \
string:Discoverable
Disconnect Device
#!/usr/bin/env bash
# Disconnect from a Bluetooth device
# Usage: ./disconnect.sh DEVICE_MAC
DEVICE_MAC="$1"
if [ -z "$DEVICE_MAC" ]; then
echo "Usage: $0 DEVICE_MAC"
exit 1
fi
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0/dev_"${DEVICE_MAC}" \
org.bluez.Device1.Disconnect
Remove Device
#!/usr/bin/env bash
# Remove (unpair) a Bluetooth device
# Usage: ./remove.sh DEVICE_MAC
DEVICE_MAC="$1"
if [ -z "$DEVICE_MAC" ]; then
echo "Usage: $0 DEVICE_MAC"
exit 1
fi
# Remove device from adapter
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.bluez.Adapter1.RemoveDevice \
objpath:/org/bluez/hci0/dev_"${DEVICE_MAC}"
Bluetooth Power Off
Shutdown is not effective; a reboot is required to be able to restart nickel.
To power off Bluetooth before rebooting:
dbus-send --system --dest=com.kobo.mtk.bluedroid \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Set \
string:org.bluez.Adapter1 \
string:Powered \
variant:boolean:false
dbus-send --system --print-reply \
--dest=com.kobo.mtk.bluedroid \
/ \
com.kobo.bluetooth.BluedroidManager1.Off
See Known Issues for details.
Bluetooth Input Device Mapping
Overview
This document describes how Bluetooth HID (Human Interface Device) input devices are mapped to
/dev/input/eventN device nodes on Kobo devices, and how to programmatically detect them.
Device Path Structure
When a Bluetooth HID device connects (keyboard, gamepad, remote, etc.), the Linux kernel creates:
- Character device:
/dev/input/eventN(where N is typically 4 or higher) - Sysfs entry:
/sys/class/input/eventN(symlink to device) - Input device:
/sys/class/input/inputM(underlying input device)
Example: Connected Gamepad
$ ls -la /sys/class/input/event4
lrwxrwxrwx 1 root root 0 Nov 15 16:36 event4 -> \
../../devices/virtual/misc/uhid/0005:2DC8:9021.0004/input/input7/event4
Key observations:
- Path contains
uhid→ indicates Bluetooth/USB HID device - Format:
uhid/<BUS>:<VENDOR>:<PRODUCT>.<INSTANCE>/input/inputN/eventN - HID ID:
0005:2DC8:9021.00040005= Bus ID (Bluetooth)2DC8= Vendor ID9021= Product ID0004= Instance number
Built-in Kobo Devices
For comparison, built-in devices use platform paths:
event0 -> ../../devices/platform/ntx_event0/input/input0/event0 # E-ink touch
event1 -> ../../devices/platform/1001e000.i2c/i2c-2/2-0010/input/input1/event1 # I2C device
event2 -> ../../devices/platform/1001e000.i2c/i2c-2/2-001e/input/input2/event2 # I2C device
event3 -> ../../devices/platform/10019000.i2c/i2c-1/1-004b/bd71828-pwrkey.6.auto/input/input3/event3 # Power button
These paths do not contain uhid, making them easy to distinguish from Bluetooth devices.
Detection Strategy
Identifying Bluetooth Input Devices
To detect Bluetooth input devices, scan /sys/class/input/ for symlinks containing uhid:
#!/bin/bash
# Find all Bluetooth input devices
for event in /sys/class/input/event*; do
# Read symlink target
target=$(readlink "$event")
# Check if it contains 'uhid'
if echo "$target" | grep -q "uhid"; then
event_num=$(basename "$event")
echo "Bluetooth device: /dev/input/$event_num"
fi
done
Output example:
Bluetooth device: /dev/input/event4
Correlation Challenge
The D-Bus Bluetooth interface does not provide a direct mapping between Bluetooth MAC addresses
and /dev/input/ paths.
D-Bus provides:
Address: E4:17:D8:EC:04:1E
Name: 8BitDo Micro gamepad
Modalias: "" ← Empty!
Kernel provides:
HID ID: 0005:2DC8:9021.0004
Path: /dev/input/event4
No direct correlation exists between the MAC address from D-Bus and the HID device ID from the kernel.
Why Modalias is Empty
The Modalias field in BlueZ typically contains vendor/product IDs, but on Kobo’s MTK chipset it’s
empty. Likely reasons:
- Custom BlueZ wrapper: Kobo uses
com.kobo.mtk.bluedroidinstead of standardorg.bluez - Limited D-Bus exposure: MTK implementation doesn’t expose full device properties
- HID profile timing: Modalias may not be populated until after full HID connection
Potential Correlation Methods
1. Device Name Matching
Device names are available in sysfs and can be matched against D-Bus device names:
$ cat /sys/class/input/event4/device/name
8BitDo Micro gamepad
D-Bus device name:
Name: "8BitDo Micro gamepad"
Sysfs device name:
/sys/class/input/event4/device/name: "8BitDo Micro gamepad"
When these names match exactly, a direct correlation can be established between the Bluetooth MAC address (from D-Bus) and the input device path (from kernel).
References
- Linux Input Subsystem:
/Documentation/input/input.txtin kernel source - BlueZ HID profile documentation
uhidkernel module:/Documentation/hid/uhid.txt- D-Bus BlueZ API
Known Issues
Kernel Panic on Nickel Restart
Severity: Critical - Device reboots
Affects: All Kobo devices with MTK Bluetooth chipset
Problem Description
When exiting KOReader back to Nickel after using Bluetooth, the device experiences a kernel NULL pointer dereference panic and reboots.
Error Details
Unable to handle kernel NULL pointer dereference at virtual address 00000008
PC is at osal_fifo_init+0x18/0x6c [wlan_drv_gen4m]
LR is at kalIoctl+0x1c0/0x8d4 [wlan_drv_gen4m]
Full kernel panic logs and analysis: KOReader Issue #12739
Root Cause
The MediaTek WiFi driver (wlan_drv_gen4m) has non-idempotent initialization. The driver’s
initialization code is not designed to be called multiple times in the same boot session.
When Nickel attempts to re-initialize the Bluetooth stack (even with modules already loaded and
processes terminated), the driver crashes in osal_fifo_init due to attempting to initialize
already-initialized structures.
Attempted Solutions
✅ Proper D-Bus Shutdown
# Gracefully shut down via D-Bus
dbus-send --system --dest=com.kobo.mtk.bluedroid \
/ com.kobo.bluetooth.BluedroidManager1.Off
Result: Off() method completes successfully, but doesn’t prevent panic.
✅ Process Termination
# Kill all Bluetooth processes
killall -KILL btservice mtkbtd
Result: Processes terminated successfully, but panic still occurs.
✅ Keep Kernel Modules Loaded
# Verify modules remain loaded (do NOT unload)
lsmod | grep -E "(wmt|wlan|bt)"
Result: Modules stay loaded as required, but panic still occurs on Nickel restart.
❌ Complete Cleanup
Even with all of the above:
- Devices disconnected
- Discovery stopped
- Adapter powered off
- Service Off() called
- Processes terminated
- Modules kept loaded
The kernel panic still occurs when Nickel restarts.
Evidence from Investigation
Verified shutdown procedure execution:
[root@monza root]# BTSERVICE_PID=$(pgrep btservice)
[root@monza root]# MTKBTD_PID=$(pgrep mtkbtd)
[root@monza root]# kill -TERM $BTSERVICE_PID $MTKBTD_PID
[root@monza root]# sleep 2
[root@monza root]# ps aux | grep -E "(mtkbtd|btservice)" | grep -v grep
[root@monza root]# # No output - processes terminated
[root@monza root]# lsmod | grep wmt
wmt_drv 1059215 4 wlan_drv_gen4m,wmt_cdev_bt,wmt_chrdev_wifi, Live 0xbf000000 (O)
[root@monza root]# # Modules still loaded as required
Despite clean shutdown, testing confirmed device reboots when returning to Nickel.
Hypothesis
Nickel’s Bluetooth initialization expects a pristine driver state. Even though:
- Userspace processes are terminated
- Kernel modules remain loaded
- D-Bus services are stopped
Some hardware state or driver-internal state persists that conflicts with Nickel’s initialization expectations. The driver attempts to re-initialize structures that are already initialized, causing the NULL pointer dereference.
Potential Causes
- Hardware state not reset - Bluetooth chip registers/state not cleared
- Driver global state - Static/global variables in kernel module not reset
- Character device state -
/dev/stpbtor/dev/wmt*in unexpected state - Resource conflict - IRQ, DMA, or memory mappings not released properly
Current Workaround
None available. Using Bluetooth in KOReader requires device reboot before returning to Nickel.
References
- KOReader Issue #12739 - Original bug report with kernel panic analysis
- NickelMenu PR #152 - Discussion of libnickel Bluetooth integration
Bluetooth Control on Kobo Libra 2
Device Information
- Model: Kobo Libra 2
- Chipset: Freescale i.MX6SLL
- Bluetooth Stack: Standard Linux BlueZ
- D-Bus Service:
org.bluez
Key Differences from MTK
Libra 2 uses standard BlueZ instead of MTK’s com.kobo.mtk.bluedroid service.
Turn On/Off Bluetooth Stack
Turn On Bluetooth
- Start Bluetooth daemon:
/libexec/bluetooth/bluetoothd &
- Reset HCI interface:
hciconfig hci0 down
hciconfig hci0 up
- Power on the Bluetooth adapter:
dbus-send --system --print-reply \
--dest=org.bluez \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Set \
string:org.bluez.Adapter1 \
string:Powered \
variant:boolean:true
Turn Off Bluetooth
- Power off the adapter:
dbus-send --system --print-reply \
--dest=org.bluez \
/org/bluez/hci0 \
org.freedesktop.DBus.Properties.Set \
string:org.bluez.Adapter1 \
string:Powered \
variant:boolean:false
- Stop Bluetooth daemon:
killall bluetoothd
Bluetooth Device Operations
Device Discovery
Start Discovery
dbus-send --system --print-reply \
--dest=org.bluez \
/org/bluez/hci0 \
org.bluez.Adapter1.StartDiscovery
Stop Discovery
dbus-send --system --print-reply \
--dest=org.bluez \
/org/bluez/hci0 \
org.bluez.Adapter1.StopDiscovery
List Discovered Devices
dbus-send --system --print-reply \
--dest=org.bluez \
/ \
org.freedesktop.DBus.ObjectManager.GetManagedObjects
Connecting and Disconnecting
Connect to Device
Connect to a paired device (example with Kobo Remote):
dbus-send --system --print-reply \
--dest=org.bluez \
/org/bluez/hci0/dev_A4_3C_D7_6D_0D_3B \
org.bluez.Device1.Connect
Disconnect Device
dbus-send --system --print-reply \
--dest=org.bluez \
/org/bluez/hci0/dev_A4_3C_D7_6D_0D_3B \
org.bluez.Device1.Disconnect
Connected Device Information
When connected, the Kobo Remote appears as:
Device Path: /dev/input/event5
Device Name: "Kobo Remote"
Bus Type: 0005 (Bluetooth HID)
Handlers: kbd event5
Verification Commands
Check input devices:
# List all input devices
ls -la /dev/input/event*
# Check device information
cat /proc/bus/input/devices | grep -A5 -B5 "Kobo Remote"
# Get device name
cat /sys/class/input/event5/device/name
Symlink Structure
The device symlink reveals it’s a Bluetooth device:
$ ls -la /sys/class/input/event5
lrwxrwxrwx 1 root root 0 Dec 17 11:38 /sys/class/input/event5 ->
../../devices/virtual/misc/uhid/0005:000D:0000.0019/input/input29/event5
The presence of uhid in the path identifies it as a Bluetooth HID device.
Input Event Monitoring
Monitor key presses from the device:
# Show raw input events
hexdump -C /dev/input/event5
# Example output for button presses:
# 00000000 a3 86 42 69 ce b7 05 00 04 00 04 00 51 00 07 00 |..Bi........Q...|
# 00000010 a3 86 42 69 ce b7 05 00 01 00 6c 00 01 00 00 00 |..Bi......l.....|
Key Code Mapping
Common key codes from Kobo Remote:
0x6c(108) =KEY_RIGHT- Right button0x67(103) =KEY_UP- Up button- Other buttons map to standard Linux input key codes
This allows button remapping and isolated input handling separate from the device’s built-in controls.