Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. A Kobo eReader device (Clara HD, Libra, Sage, etc.)
  2. KOReader installed on your Kobo device
  3. Access to the Kobo filesystem (usually via USB or file manager)

Installation Method

  1. Download the latest release

  2. Extract and install the plugin

    • Extract kobo.koplugin.zip to obtain the kobo.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/
  3. Extract and install the patches

    • Extract kobo-patches.zip to 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 patches folder may be missing; create [KOReader]/patches/ if needed.
  4. 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:

  1. Open the KOReader menu (tap the top of the screen)
  2. Navigate to ToolsKobo Library
  3. Toggle Enable virtual library
  4. 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:

  1. Reading Kobo’s database (KoboReader.sqlite) for book metadata
  2. Creating virtual paths in the format KOBO_VIRTUAL://BOOKID/filename
  3. Mapping virtual paths to actual kepub file locations
  4. 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 PathActual 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

  1. 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:

  1. Monitoring reading progress in both KOReader and Kobo
  2. Comparing timestamps to determine the most recent reading session
  3. 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:

  1. Go to Paired devices and select the device you want to configure.
  2. Choose “Configure key bindings” from the device menu - a list of available actions will appear.
  3. Select an action you want to bind to a button.
  4. Choose “Register button” - the system will now listen for the next button press on your device.
  5. Press a button on your Bluetooth device - the system will capture and bind it to the selected action.
  6. 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:

  1. Open Settings → Network → Bluetooth → Settings → Auto-detection.
  2. 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).
  3. (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:

  1. Open Settings → Network → Bluetooth → Settings → Auto-connect.
  2. Enable “Auto-connect to nearby devices” — this starts scanning and will automatically connect to paired devices that come within range.
  3. (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:

Bluetooth Settings

Manage Bluetooth devices and configure button mappings for MTK-based Kobo devices:

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

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 syncing
  • SILENT: Sync automatically without confirmation
  • NEVER: Skip sync in this scenario

See Sync Direction Settings for details on the four behavior settings.

Settings Menu Navigation

Accessing Settings

  1. Open KOReader file browser
  2. Open the menu (top-left corner)
  3. 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 ItemTypeFunction
Sync reading state with KoboToggleEnable/disable all sync functionality
Enable automatic sync on virtual libraryToggleEnable/disable automatic progress sync on library access (books are always displayed)
Sync reading state nowActionManually trigger progress sync for all books
Sync behaviorSubmenuConfigure sync direction and behavior
Refresh libraryActionRefresh virtual library metadata
AboutInfoDisplay 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

  1. Open the file browser in KOReader
  2. Open the menu (top-left corner)
  3. Select “Kobo Library” → “Sync reading state now”
  4. Wait for sync to complete
  5. 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:

  1. A book has different reading positions in KOReader and Kobo
  2. Sync Direction Settings are set to PROMPT for that scenario
  3. The sync decision is about to be made (newer or older state)

Dialog Behavior

When you see a dialog, you have two options:

ButtonEffect
YesProceed with the sync operation
NoSkip 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

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

  1. 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

  1. Put your Bluetooth device in pairing mode (refer to your device’s manual)
  2. Navigate to Settings → Network → Bluetooth
  3. Choose “Scan for devices”
  4. Select your device from the list
  5. 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

  1. Navigate to Settings → Network → Bluetooth → Paired devices
  2. Select any device from the list

The options shown depend on the device’s current state.

Available Options

OptionShows WhenFunction
ConnectDevice is not currently connectedEstablishes a connection to the device. The device will be ready to use with KOReader once connected. Bluetooth must be enabled.
DisconnectDevice is currently connectedCloses 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 BindingsDevice is connected and the plugin supports key bindingsOpens 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 BindingsThe device has existing key binding configurationsRemoves all button mappings for this device. You’ll be asked to confirm before the key bindings are cleared.
TrustDevice is not currently trustedMarks the device as trusted. Trusted devices can connect to your Kobo without requiring confirmation each time they connect.
UntrustDevice is currently trustedRemoves the trusted status from the device. The device will require confirmation before connecting in the future.
ForgetAlways shownRemoves 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:

  1. Navigate to Settings → Network → Bluetooth → Paired devices
  2. Select the device you want to configure
  3. Choose “Configure key bindings”
  4. Select a category (e.g., Reader, Device, Screen and lights)
  5. Select an action from the category list
  6. Choose “Register button”
  7. Press the button on your Bluetooth device you want to use for this action
  8. 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:

  1. Navigate to the device’s key binding configuration
  2. Select the action you want to unbind
  3. Choose “Remove binding”

Remove All Bindings

To clear all button mappings for a device:

  1. Navigate to the device options in Settings → Network → Bluetooth → Paired devices
  2. 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

  1. Go to Settings → Network → Bluetooth → Settings
  2. Toggle “Auto-resume after wake” to enable or disable the feature

How It Works

When auto-resume is enabled:

  1. When your device suspends, the plugin automatically turns off Bluetooth to save battery
  2. 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
  3. 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.

  1. Go to Settings → Network → Bluetooth → Settings
  2. 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

  1. When Bluetooth is enabled and auto-detection is turned on, the plugin starts polling every second
  2. 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
  3. When a new device is detected after startup, a notification is shown
  4. 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

  1. When Bluetooth is enabled and auto-connect is turned on, the plugin starts scanning for nearby paired devices in discovery/pairing/broadcasting mode
  2. 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
  3. 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)
  4. 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

  1. Open KOReader top menu
  2. Navigate to Settings → Network → Bluetooth.
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 ItemTypeFunction
Enable/DisableToggleTurn Bluetooth on or off
Scan for devicesActionScan for new devices to pair
Paired devicesSubmenuView and manage all paired Bluetooth devices
Connect/DisconnectActionConnect to or disconnect from a specific device
Configure key bindingsSubmenuSet up button mappings for a connected device
Remove deviceActionRemove device from paired list
SettingsSubmenuBluetooth settings submenu
Auto-resume after wakeToggleAutomatically re-enable Bluetooth after device wakes up
Show status in footerToggleDisplay Bluetooth status in the reader’s footer bar
Auto-detectionSubmenuAuto-detection settings submenu
Auto-detect connecting devicesToggleEnable/disable polling for auto-connected devices
Stop detection after connectionToggleStop polling once a device successfully connects
Auto-connectSubmenuAuto-connect settings submenu
Auto-connect to nearby devicesToggleEnable/disable automatic connection to nearby devices
Stop auto-connect after connectToggleStop scanning once a device successfully connects
Register buttonActionCapture a button press to bind to selected action
Remove bindingActionRemove 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

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:

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

Workflows

This section covers the detailed workflows for using the Kobo Plugin with Komga/Calibre Web.

Available Workflows

Reading a New Book

  1. 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
  2. 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)
  3. Select and open a book

    • Book automatically opens at the correct position based on synced progress
    • Latest progress from Kobo is already synced
  4. Read with KOReader’s features

    • Enjoy KOReader’s customizable fonts, margins, and reading features
    • Progress is tracked normally
  5. Close the book

    • Reading progress is automatically synced back to Kobo
    • No manual action needed

Continuing in Kobo Native

  1. 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
  2. Make progress

    • Read in Kobo’s native reader
    • Close when done

Return to KOReader

  1. 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)
  2. 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
  3. 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

  1. The plugin checks if Bluetooth is enabled
  2. If disabled, it automatically turns Bluetooth on
  3. It attempts to connect to the device
  4. 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

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:

  1. Metadata Parser (src/metadata_parser.lua) - Reads Kobo’s SQLite database to retrieve book metadata
  2. Virtual Filesystem (src/filesystem_ext.lua) - Provides filesystem operations for virtual paths
  3. 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

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:

  1. False Positives: Some DRM-free books may contain rights.xml files that are simply empty or contain non-restrictive metadata
  2. Incomplete: Not all DRM systems use rights.xml - other protection schemes exist
  3. 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:

  1. Storage location: Kobo uses one central database, KOReader uses individual files
  2. Position format: Kobo uses chapter+coordinate system (format unknown), KOReader uses percentages
  3. Precision: Kobo can point to specific positions within chapters, KOReader tracks overall progress
  4. 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

FilePurpose
src/lib/kobo_state_reader.luaReads progress from Kobo database
src/lib/kobo_state_writer.luaWrites progress to Kobo database
src/lib/sync_decision_maker.luaDecides when/how to sync
src/reading_state_sync.luaCoordinates sync operations
src/metadata_parser.luaQueries 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

FieldTypePurposeExample
ContentIDTEXTUnique identifier (PRIMARY KEY)"0N3773Z7HFPXB"
ContentTypeINTEGER6 = Book entry, 9 = Chapter entry6
BookTitleTEXTBook title"The Great Gatsby"
AttributionTEXTAuthor information"F. Scott Fitzgerald"
___PercentReadINTEGERReading progress (0-100)67
___FileOffsetINTEGERCumulative percentage where chapter starts50 (chapter starts at 50%)
___FileSizeINTEGERPercentage size of this chapter10 (chapter is 10% of book)
DateLastReadTEXTLast reading timestamp (ISO 8601)"2024-01-15 14:30:00.000+00:00"
ReadStatusINTEGERReading status code (see below)1
ChapterIDBookmarkedTEXTCurrent 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:

  • ___FileOffset and ___FileSize are percentage values, not byte counts
  • All chapter ___FileSize values sum to 100

Why Chapter Boundaries?

The plugin can only write progress at chapter boundaries because:

  1. Kobo’s chapter structure: Each chapter is a separate database row (ContentType=9)
  2. Bookmark format limitation: The ChapterIDBookmarked format is "chapter.html#kobo.x.y" where x.y are some form of coordinates (assumed to be in the HTML document, but exact format is unknown)
  3. Coordinate mapping unknown: There is no known way to convert KOReader’s position data into the x.y format that Kobo uses
  4. Chapter-level precision: The plugin defaults to #kobo.1.1 (start of chapter) for all bookmarks

When syncing to Kobo, the plugin:

  1. Finds the chapter that contains the target percentage
  2. Sets ChapterIDBookmarked to that chapter’s ID
  3. Updates that chapter’s ___PercentRead to reflect position within the chapter
  4. Updates the main book entry’s ___PercentRead to 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:

  1. 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
  2. Scans the kepub directory (/mnt/onboard/.kobo/kepub/) for all files
  3. Filters out encrypted files by checking for valid ZIP/EPUB signature (PK\x03\x04)
  4. Looks up metadata in cache for unencrypted files (no additional database queries)
  5. 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

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 .sdr file
  • 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:

KOReaderKobo ReadStatusMeaning
"reading"1In progress
"complete"2Finished
"finished"2Finished (alternative)
(not set)0Unopened
-- 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:

  1. Calculates which chapter contains the 67.3% position
  2. Finds that chapter in Kobo’s database using SQL
  3. 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?

  1. Format unknown: The exact meaning and calculation of the x.y coordinate format used by Kobo is not documented or known
  2. No conversion method: There is no known way to map KOReader’s position data to Kobo’s coordinate system
  3. Chapter-level fallback: Using #kobo.1.1 (start of chapter) is the safe default
  4. Progress still tracked: The chapter’s ___PercentRead field 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:

SettingPurposeDefault
sync_enabledEnable/disable all syncON
auto_sync_enabledAuto-sync on library accessON
from_kobo_enabledAllow PULL from KoboON
to_kobo_enabledAllow PUSH to KoboON
from_kobo_promptPrompt before PULLSILENT
to_kobo_promptPrompt before PUSHSILENT

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

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 IDTitleDescription
enableEnable BluetoothTurns Bluetooth on
disableDisable BluetoothTurns Bluetooth off
toggleToggle BluetoothToggles Bluetooth on/off based on state
scanScan for Bluetooth DevicesStarts 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:

  1. Plugin checks if Bluetooth is enabled
  2. If disabled, it automatically turns Bluetooth on
  3. Device connection is attempted
  4. 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

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 = 1 or args = -1)

3. lib/bluetooth/dispatcher_helper.lua

Helper module that extracts dispatcher actions using introspection:

  • Uses debug.getupvalue() to access dispatcher’s internal settingsList and dispatcher_menu_order
  • Returns actions organized by category with metadata (event names, titles, arguments, etc.)
  • Caches results for performance
  • Returns nil if extraction fails (triggering static fallback)

How Actions Are Loaded

Dynamic Loading (Primary Method)

  1. available_actions.lua calls dispatcher_helper.get_dispatcher_actions_ordered()
  2. The helper introspects KOReader’s dispatcher module to extract all registered actions
  3. Actions are organized into categories based on their category flags
  4. Essential actions with custom arguments are merged on top (overriding extracted versions)
  5. The final categorized list is returned

Static Fallback (Backup Method)

If dynamic extraction fails:

  1. A minimal static list of core navigation and UI actions is used
  2. Essential actions are merged with static fallback actions
  3. 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 direction
  • increase_font / decrease_font - Font size adjustment
  • increase_frontlight / decrease_frontlight - Brightness control
  • increase_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 pressed
  • args: Optional arguments to pass to the event
  • args_func: Optional function to generate arguments dynamically
  • toggle: Optional toggle state for toggle-type actions
  • category: 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

  1. User presses a button on the Bluetooth device
  2. The InputDeviceHandler detects the button press via the device’s input event interface
  3. CRITICAL: The handler executes UIManager.event_hook:execute("InputEvent") to notify other components of user activity
  4. The handler looks up the configured action ID for that button (format: "category:action_id")
  5. The action is retrieved from the action lookup map (pre-built at module load time)
  6. The corresponding KOReader event is triggered with any arguments or argument functions
  7. 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:

  1. Document Research: Track findings, commands, and technical details during exploration
  2. Share Knowledge: Provide context for future contributors working on these features
  3. 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

MTK kernel modules (`wmt_drv`, `wmt_cdev_bt`, etc.) have non-idempotent initialization. Unloading and reloading causes NULL pointer dereference kernel panic.

See Known Issues for details.

References

Bluetooth Stack Architecture

Components

Kobo’s Bluetooth implementation consists of three main processes:

ProcessPathPurpose
mtkbtd-launcher/usr/local/Kobo/Launch script for MTK Bluetooth
mtkbtd/usr/local/Kobo/mtkbtdMediaTek Bluetooth daemon
btservice/usr/bin/btserviceBluetooth service (spawned by mtkbtd)

Kernel Modules

These modules must remain loaded at all times. Unloading them will remove Wi-Fi support, and does not prevent kernel panic on shutdown. Restoration is required to recover Wi-Fi functionality. (See [Known Issues](./05-known-issues.md))
  • wmt_drv - MediaTek WMT driver (main driver)
  • wmt_chrdev_wifi - WiFi character device
  • wmt_cdev_bt - Bluetooth character device
  • wlan_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 NamePurpose
com.kobo.mtk.bluedroidMain Bluetooth service (Kobo’s wrapper)
com.kobo.bluetooth.AgentPairing/authentication agent
org.bluezNOT 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

  1. Use com.kobo.mtk.bluedroid as destination for all D-Bus calls, not org.bluez
  2. Auto-activation - D-Bus starts the service automatically when the method is called
  3. The object paths still use /org/bluez/hci0 but the service name is com.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:

  1. Character device: /dev/input/eventN (where N is typically 4 or higher)
  2. Sysfs entry: /sys/class/input/eventN (symlink to device)
  3. 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.0004
    • 0005 = Bus ID (Bluetooth)
    • 2DC8 = Vendor ID
    • 9021 = Product ID
    • 0004 = 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:

  1. Custom BlueZ wrapper: Kobo uses com.kobo.mtk.bluedroid instead of standard org.bluez
  2. Limited D-Bus exposure: MTK implementation doesn’t expose full device properties
  3. 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.txt in kernel source
  • BlueZ HID profile documentation
  • uhid kernel 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

  1. Hardware state not reset - Bluetooth chip registers/state not cleared
  2. Driver global state - Static/global variables in kernel module not reset
  3. Character device state - /dev/stpbt or /dev/wmt* in unexpected state
  4. 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

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

  1. Start Bluetooth daemon:
/libexec/bluetooth/bluetoothd &
  1. Reset HCI interface:
hciconfig hci0 down
hciconfig hci0 up
  1. 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

  1. 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
  1. 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

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 button
  • 0x67 (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.