1
0

Updated documentation
Some checks failed
Build & Test WASM / build-and-test (push) Has been cancelled

This commit is contained in:
2026-03-15 23:08:41 +01:00
parent e792ef3c74
commit 601af2cec3
9 changed files with 68 additions and 428 deletions

View File

@@ -1,58 +1,66 @@
# KoboPatch Web UI
A fully client-side web application for applying [kobopatch](https://github.com/pgaskin/kobopatch) patches to Kobo e-readers. No backend required — runs entirely in the browser using WebAssembly.
Fully client-side web app for applying [kobopatch](https://github.com/pgaskin/kobopatch) patches to Kobo e-readers. No backend — runs entirely in the browser via WebAssembly. Can be hosted as a static site.
## Features
## User flow
- **Auto mode** (Chromium): detect your Kobo model and firmware via the File System Access API, then write the patched file directly back to the device
- **Manual mode** (all browsers): select your model and firmware version from dropdowns, download the result
- Firmware is downloaded automatically from Kobo's servers
- Step-by-step wizard with live build progress
- Patch descriptions and PatchGroup mutual exclusion
1. Select device (auto-detect via File System Access API on Chromium, or manual dropdowns on any browser)
2. Configure patches (enable/disable, PatchGroup mutual exclusion via radio buttons)
3. Build — firmware auto-downloaded from Kobo's CDN (`ereaderfiles.kobo.com`, CORS open), patched via WASM in a Web Worker
4. Write `KoboRoot.tgz` to device (Chromium auto mode) or download manually
## How it works
## File structure
1. Connect your Kobo via USB (or select your model/firmware manually)
2. Enable/disable patches in the configurator
3. Click **Build** — firmware is fetched from Kobo's CDN, patches are applied via WASM in a Web Worker
4. Write `KoboRoot.tgz` to your device or download it manually
5. Safely eject and reboot your Kobo
```
src/public/ # Webroot — serve this directory
index.html # Single-page app, 3-step wizard (Device → Patches → Build)
style.css
app.js # Step navigation, flow orchestration, firmware download with progress
kobo-device.js # KOBO_MODELS (serial prefix → name), FIRMWARE_DOWNLOADS (version+prefix → URL),
# getDevicesForVersion(), getFirmwareURL(), KoboDevice class (File System Access API)
patch-ui.js # PatchUI class: loads patch zips (JSZip), parses YAML, renders toggle UI,
# generates kobopatch.yaml config with overrides
kobopatch.js # KobopatchRunner: spawns Web Worker per build, handles progress/done/error messages
patch-worker.js # Web Worker: loads wasm_exec.js + kobopatch.wasm, runs patchFirmware(),
# posts progress back, transfers result buffer zero-copy
wasm_exec.js # Go WASM support runtime (copied from Go SDK by setup.sh, gitignored)
kobopatch.wasm # Compiled WASM binary (built by build.sh, gitignored)
patches/
index.json # [{ "version": "4.45.23646", "filename": "patches_4.45.23646.zip" }]
patches_*.zip # Each contains kobopatch.yaml + src/*.yaml patch files
## Building
kobopatch-wasm/ # WASM build
main.go # Go entry point: jsPatchFirmware() → patchFirmware() pipeline
# Accepts configYAML, firmwareZip, patchFiles, optional progressFn
# Returns { tgz: Uint8Array, log: string }
go.mod
setup.sh # Clones kobopatch source, copies wasm_exec.js
build.sh # GOOS=js GOARCH=wasm go build, copies .wasm to src/public/,
# sets ?ts= cache-bust timestamp in patch-worker.js
```
### Prerequisites
## Adding a new firmware version
- Go 1.21+ (for compiling kobopatch to WASM)
1. Add the patch zip to `src/public/patches/` and update `index.json`
2. Add firmware download URLs to `FIRMWARE_DOWNLOADS` in `kobo-device.js` (keyed by version then serial prefix)
3. The kobo CDN prefix per device family (e.g. `kobo12`, `kobo13`) is stable; the date path segment changes per release
### Setup & build
## Building the WASM binary
Requires Go 1.21+.
```bash
cd kobopatch-wasm
./setup.sh # clones kobopatch source, copies wasm_exec.js
./build.sh # compiles WASM, copies artifacts to src/public/
./setup.sh # first time only
./build.sh # compiles WASM, copies to src/public/
```
### Running locally
Any static file server works:
## Running locally
```bash
python3 -m http.server -d src/public/ 8888
```
Then open `http://localhost:8888`.
## Credits
## Supported devices
Currently supports firmware **4.45.23646** for:
- Kobo Libra Colour
- Kobo Clara BW (N365)
- Kobo Clara BW (P365)
- Kobo Clara Colour
Additional firmware versions can be added by placing patch zips in `src/public/patches/` and updating `index.json` and the firmware URL map in `kobo-device.js`.
## License
kobopatch is by [pgaskin](https://github.com/pgaskin/kobopatch). Patches are community-contributed via [MobileRead](https://www.mobileread.com/).
kobopatch by [pgaskin](https://github.com/pgaskin/kobopatch). Patches from [MobileRead](https://www.mobileread.com/).

View File

@@ -43,6 +43,7 @@
const buildStatus = document.getElementById('build-status');
const writeSuccess = document.getElementById('write-success');
const firmwareVersionLabel = document.getElementById('firmware-version-label');
const firmwareDeviceLabel = document.getElementById('firmware-device-label');
const patchCountHint = document.getElementById('patch-count-hint');
const allSteps = [stepConnect, stepManual, stepDevice, stepPatches, stepFirmware, stepBuilding, stepDone, stepError];
@@ -86,6 +87,7 @@
function configureFirmwareStep(version, prefix) {
firmwareURL = prefix ? getFirmwareURL(prefix, version) : null;
firmwareVersionLabel.textContent = version;
firmwareDeviceLabel.textContent = KOBO_MODELS[prefix] || prefix;
document.getElementById('firmware-download-url').textContent = firmwareURL || '';
}

View File

@@ -49,7 +49,7 @@
<select id="manual-model" hidden>
<option value="">-- Select your Kobo model --</option>
</select>
<button id="btn-manual-confirm" class="primary" disabled>Continue</button>
<button id="btn-manual-confirm" class="primary" disabled>Continue &#x203A;</button>
</section>
<!-- Step 1c: Device detected (auto mode info card) -->
@@ -70,7 +70,7 @@
</div>
<div id="device-status"></div>
<div class="step-actions">
<button id="btn-device-next" class="primary">Configure Patches</button>
<button id="btn-device-next" class="primary">Configure Patches &#x203A;</button>
</div>
</section>
@@ -79,8 +79,8 @@
<p>Enable or disable patches below. Patches in the same group are mutually exclusive.</p>
<div id="patch-container" class="patch-container-scroll"></div>
<div class="step-actions">
<button id="btn-patches-back" class="secondary">Back</button>
<button id="btn-patches-next" class="primary" disabled>Continue</button>
<button id="btn-patches-back" class="secondary">&#x2039; Back</button>
<button id="btn-patches-next" class="primary" disabled>Continue &#x203A;</button>
</div>
<p id="patch-count-hint" class="fallback-hint"></p>
</section>
@@ -88,11 +88,14 @@
<!-- Step 3: Review & Build -->
<section id="step-firmware" class="step" hidden>
<p id="firmware-auto-info">
Firmware <strong id="firmware-version-label"></strong> will be downloaded
automatically from Kobo's servers:<br>
Firmware <strong id="firmware-version-label"></strong> for
<strong id="firmware-device-label"></strong> will be downloaded
automatically from Kobo's servers and will be patched after the download completes:<br>
<code id="firmware-download-url"></code><br>
You can verify this URL on
<a href="https://help.kobo.com/hc/en-us/articles/35059171032727" target="_blank">Kobo's support page</a>.
<span id="firmware-verify-notice">
You can verify if this URL matches your Kobo's model on
<a href="https://help.kobo.com/hc/en-us/articles/35059171032727" target="_blank">Kobo's support page</a>. The most important bit is that "koboXX" matches, for example "kobo13" for Kobo Libra Color.
</span>
</p>
<!--
<p id="firmware-manual-info" hidden>
@@ -105,7 +108,7 @@
<input type="file" id="firmware-input" accept=".zip" hidden>
-->
<div class="step-actions">
<button id="btn-build-back" class="secondary">Back</button>
<button id="btn-build-back" class="secondary">&#x2039; Back</button>
<button id="btn-build" class="primary">Build Patched Firmware</button>
</div>
</section>

View File

@@ -163,10 +163,15 @@ h2 {
/* Step action buttons (back/next) */
.step-actions {
display: flex;
justify-content: space-between;
gap: 0.75rem;
margin-top: 1.25rem;
}
.step-actions .primary:first-child {
margin-left: auto;
}
/* Buttons */
button {
font-size: 0.9rem;
@@ -512,6 +517,10 @@ input[type="file"] {
border-radius: 4px;
}
#firmware-verify-notice {
font-size: 12px;
}
select {
display: block;
width: 100%;

View File

@@ -1,117 +0,0 @@
# Kobopatch Web UI - Architecture
## Overview
Fully client-side web app. No backend server needed — can be hosted as a static site.
kobopatch is compiled from Go to WebAssembly and runs entirely in the browser.
```
Browser
├── index.html + CSS + JS
├── patch-worker.js (Web Worker for off-thread patching)
├── kobopatch.wasm (Go compiled to WASM, loaded by worker)
├── Patch config zips (in src/public/patches/)
├── Firmware auto-download (fetched from ereaderfiles.kobo.com)
└── File System Access API (read/write Kobo USB drive, Chromium only)
```
## User Flow
### Auto mode (Chromium)
1. Connect Kobo via USB
2. Click "Select Kobo Drive" → browser reads `.kobo/version`
3. App detects model + firmware version from serial prefix
4. App shows available patches with toggles, grouped by target binary
5. User configures patches (enable/disable)
6. Click "Build" → firmware downloaded from Kobo CDN → WASM patches in Web Worker
7. App writes resulting `KoboRoot.tgz` to `.kobo/` on device (or download)
8. User ejects device and reboots Kobo
### Manual mode (all browsers)
1. Select firmware version from dropdown
2. Select Kobo model from dropdown (determines firmware download URL)
3. Configure patches, click "Build"
4. Download `KoboRoot.tgz` and manually copy to `.kobo/` on device
## Components
### `kobo-device.js` — Device Access & Firmware URLs
- File System Access API for reading `.kobo/version`
- Serial prefix → model name mapping (`KOBO_MODELS`)
- Firmware download URLs per version and device prefix (`FIRMWARE_DOWNLOADS`)
- Writing `KoboRoot.tgz` back to `.kobo/`
### `patch-ui.js` — Patch Configuration
- Parses patch YAML files from zip archives (handles CRLF line endings)
- Renders patch list with toggles grouped by target file
- Enforces PatchGroup mutual exclusion (radio buttons)
- Generates kobopatch.yaml config with overrides from UI state
### `patch-worker.js` — Web Worker
- Loads `wasm_exec.js` and `kobopatch.wasm` off the main thread
- Receives patch config + firmware via `postMessage`
- Sends progress updates back to main thread for live UI rendering
- Returns `KoboRoot.tgz` bytes via transferable buffer
### `kobopatch.wasm` — Patching Engine
- Go source in `kobopatch-wasm/`, compiled with `GOOS=js GOARCH=wasm`
- Custom WASM wrapper accepts in-memory inputs:
- Config YAML (generated from UI state)
- Firmware zip (auto-downloaded from Kobo CDN)
- Patch YAML files (from bundled zip)
- Optional progress callback (4th argument) for real-time status
- Returns `{ tgz: Uint8Array, log: string }`
- No filesystem or exec calls — everything in-memory
### `kobopatch.js` — Runner Interface
- Abstracts WASM loading and invocation
- Spawns Web Worker per build, handles progress/done/error messages
- Transfers firmware buffer zero-copy via transferable
### Static Assets
- Patch config zips in `src/public/patches/` with `index.json` index
- `wasm_exec.js` (Go's WASM support JS)
- The WASM binary itself
## File Structure
```
src/
public/ # Webroot (static hosting)
index.html # Single page app
style.css # Styling
app.js # Main controller / flow orchestration
kobo-device.js # File System Access API + device identification + firmware URLs
patch-ui.js # Patch list rendering + toggle logic
kobopatch.js # WASM runner interface
patch-worker.js # Web Worker for off-thread patching
wasm_exec.js # Go WASM support (from Go SDK, gitignored)
kobopatch.wasm # Compiled WASM binary (gitignored)
patches/
index.json # Available patch versions
patches_*.zip # Patch config zips (kobopatch.yaml + src/*.yaml)
kobopatch-wasm/
main.go # WASM entry point + patching pipeline
go.mod # Go module (replaces for kobopatch + yaml fork)
setup.sh # Clones kobopatch source, copies wasm_exec.js
build.sh # Compiles WASM, copies artifacts to src/public/
kobopatch-src/ # Cloned kobopatch Go source (gitignored)
wip/ # Planning docs
.github/workflows/ # CI (GitHub Actions / Gitea compatible)
```
## Key Constraints
- **Chromium-only for auto mode**: File System Access API not in Firefox/Safari
- Manual mode fallback: select model + firmware version from dropdowns
- **Firmware auto-download**: fetched from `ereaderfiles.kobo.com` (CORS allows `*`)
- URLs hardcoded per device prefix in `FIRMWARE_DOWNLOADS`
- Download URL displayed to user for verification
- **WASM binary size**: ~9.9MB uncompressed
- Mitigated by gzip compression on static hosting (~3-4MB)
- **Memory usage**: firmware zips are ~150-300MB, patching happens in-memory
- Should be fine on modern desktops (USB implies desktop use)
## Running
Any static file server, e.g.: `python3 -m http.server -d src/public/ 8888`

View File

@@ -1,65 +0,0 @@
# Device Identification
## Source: `.kobo/version`
The file contains a single line with 6 comma-separated fields:
```
N4284B5215352,4.9.77,4.45.23646,4.9.77,4.9.77,00000000-0000-0000-0000-000000000390
```
| Index | Value | Meaning |
|-------|-------|---------|
| 0 | `N4284B5215352` | Device serial number |
| 1 | `4.9.77` | Unknown (kernel version?) |
| 2 | `4.45.23646` | **Firmware version** (what kobopatch matches against) |
| 3 | `4.9.77` | Unknown |
| 4 | `4.9.77` | Unknown |
| 5 | `00000000-0000-0000-0000-000000000390` | Hardware platform UUID |
## Serial Prefix → Model Mapping
The first 3-4 characters of the serial identify the device model.
Source: https://help.kobo.com/hc/en-us/articles/360019676973
### Current eReaders
| Prefix | Model |
|--------|-------|
| N428 | Kobo Libra Colour |
| N367 | Kobo Clara Colour |
| N365 / P365 | Kobo Clara BW |
| N605 | Kobo Elipsa 2E |
| N506 | Kobo Clara 2E |
| N778 | Kobo Sage |
| N418 | Kobo Libra 2 |
| N604 | Kobo Elipsa |
| N306 | Kobo Nia |
| N873 | Kobo Libra H2O |
| N782 | Kobo Forma |
| N249 | Kobo Clara HD |
### Older eReaders
| Prefix | Model |
|--------|-------|
| N867 | Kobo Aura H2O Edition 2 |
| N709 | Kobo Aura ONE |
| N236 | Kobo Aura Edition 2 |
| N587 | Kobo Touch 2.0 |
| N437 | Kobo Glo HD |
| N250 | Kobo Aura H2O |
| N514 | Kobo Aura |
| N204 | Kobo Aura HD |
| N613 | Kobo Glo |
| N705 | Kobo Mini |
| N905 | Kobo Touch |
| N416 | Kobo Original |
| N647 / N47B | Kobo Wireless |
## Firmware
- The user provides their own firmware zip (not hosted by us for legal reasons)
- Both Libra Colour and Clara BW/Colour currently use firmware `4.45.23646`
- The firmware zip filename matches what `kobopatch.yaml` references: `kobo-update-4.45.23646.zip`
- Different device families may have different firmware zips even for the same version number

View File

@@ -1,85 +0,0 @@
# Kobopatch Patch Format
## kobopatch.yaml (Main Config)
```yaml
version: 4.45.23646
in: src/kobo-update-4.45.23646.zip
out: out/KoboRoot.tgz
log: out/log.txt
patchFormat: kobopatch
patches:
src/nickel.yaml: usr/local/Kobo/nickel
src/nickel_custom.yaml: usr/local/Kobo/nickel
src/libadobe.so.yaml: usr/local/Kobo/libadobe.so
src/libnickel.so.1.0.0.yaml: usr/local/Kobo/libnickel.so.1.0.0
src/librmsdk.so.1.0.0.yaml: usr/local/Kobo/librmsdk.so.1.0.0
src/cloud_sync.yaml: usr/local/Kobo/libnickel.so.1.0.0
overrides:
src/nickel.yaml:
Patch Name Here: yes
Another Patch: no
src/libnickel.so.1.0.0.yaml:
Some Other Patch: yes
```
The `overrides` section is what the web UI generates. Everything else stays fixed.
## Patch YAML Files
Each file contains one or more patches as top-level YAML keys:
```yaml
Patch Name:
- Enabled: no
- PatchGroup: Optional Group Name # patches in same group are mutually exclusive
- Description: |
Multi-line description text.
Can span multiple lines.
- <patch instructions...> # FindZlib, ReplaceBytes, etc. (opaque to UI)
```
### Fields the UI cares about
| Field | Required | Description |
|-------|----------|-------------|
| Name | yes | Top-level YAML key |
| Enabled | yes | `yes` or `no` - default state |
| Description | no | Human-readable description (single line or multi-line `\|` block) |
| PatchGroup | no | Mutual exclusion group - only one patch per group can be enabled |
### Patch Files and Their Targets
| File | Binary Target | Patch Count |
|------|--------------|-------------|
| nickel.yaml | nickel (main UI) | ~17 patches |
| nickel_custom.yaml | nickel | ~2 patches |
| libnickel.so.1.0.0.yaml | libnickel.so | ~50+ patches (largest) |
| libadobe.so.yaml | libadobe.so | 1 patch |
| librmsdk.so.1.0.0.yaml | librmsdk.so | ~10 patches |
| cloud_sync.yaml | libnickel.so | 1 patch |
## PatchGroup Rules
Patches with the same `PatchGroup` value within a file are mutually exclusive.
Only one can be enabled at a time. The UI should render these as radio buttons.
Example from libnickel.so.1.0.0.yaml:
- "My 10 line spacing values" (PatchGroup: Line spacing values alternatives)
- "My 24 line spacing values" (PatchGroup: Line spacing values alternatives)
## YAML Parsing Strategy
PHP doesn't have `yaml_parse` available on this system. Options:
1. Use a simple line-by-line parser that extracts only the fields we need
2. Install php-yaml extension
3. Use a pure PHP YAML library (e.g., Symfony YAML component)
The patch YAML structure is regular enough for a targeted parser:
- Top-level keys (no indentation, ending with `:`) are patch names
- `- Enabled: yes/no` on the next level
- `- Description: |` followed by indented text, or `- Description: single line`
- `- PatchGroup: group name`
- Everything else can be ignored

View File

@@ -1,67 +0,0 @@
# TODO
## Done
- [x] Device detection proof of concept (File System Access API)
- [x] Serial prefix → model mapping (verified against official Kobo help page)
- [x] Architecture planning (fully client-side WASM, no backend)
- [x] Installed Go via Homebrew (v1.26.1)
- [x] Verified all kobopatch tests pass natively + WASM
- [x] Created `kobopatch-wasm/` with setup.sh, build.sh, go.mod, main.go
- [x] WASM wrapper compiles successfully (9.9MB)
- [x] GitHub/Gitea CI workflow (build + test)
- [x] Patch UI: loads patches from zip, parses YAML, renders toggles
- [x] PatchGroup mutual exclusion (radio buttons)
- [x] Full app flow: connect → detect → configure patches → upload firmware → build → write/download
- [x] Patches served from `src/public/patches/` with `index.json` for version discovery
- [x] JSZip for client-side zip extraction
- [x] Renamed `src/frontend``src/public` (webroot)
- [x] Moved `patches/` into `src/public/patches/`
- [x] Manual mode fallback for non-Chromium browsers (model + firmware version dropdowns)
- [x] Auto-download firmware from Kobo's servers (CORS confirmed working)
- [x] Firmware download URLs hardcoded per device prefix and version
- [x] Firmware download URL displayed in build step for user verification
- [x] Fixed CRLF line ending bug in patch YAML parser
- [x] Copy `kobopatch.wasm` + `wasm_exec.js` to `src/public/` as part of build
- [x] Progress reporting: download % with MB, WASM log output in terminal window
- [x] WASM `patchFirmware` accepts optional progress callback (4th arg)
- [x] Verified patched binaries are byte-identical between native and WASM builds
- [x] Web Worker for WASM patching (non-blocking UI, live progress)
- [x] Cache-busting timestamp on WASM file (`?ts=` query string)
- [x] Matched log output to native kobopatch (no debug spam from patchfile.Log)
- [x] Step navigation: 3-step indicator (Device → Patches → Build) with back/forward
- [x] Discrete steps with proper state management
- [x] Scrollable patch list (50vh max height with border)
- [x] Toggleable patch descriptions (hidden by default, `?` button)
- [x] UI polish: renamed to "KoboPatch Web UI", styled firmware URL, patch count hint
- [x] Disambiguated identical model names in dropdown (serial prefix suffix)
## To Test
- [ ] End-to-end test in browser with real Kobo device (auto mode)
- [ ] Verify File System Access API write to `.kobo/KoboRoot.tgz`
- [ ] Test manual mode flow across Firefox/Safari/Chrome
## Remaining Work
- [ ] Better error messages for common failures
- [ ] Test with multiple firmware versions / patch zips
## Future / Polish
- [ ] Host as static site (GitHub Pages / Netlify)
- [ ] NickelMenu install/uninstall support
- [ ] Dark mode support
## Architecture Change Log
- **Switched from PHP backend to fully client-side WASM.**
Reason: avoid storing Kobo firmware files on a server (legal risk).
- **Patches served from zip files in `src/public/patches/`.**
App scans `patches/index.json` to find compatible patch zips for the detected firmware.
- **Firmware auto-downloaded from Kobo's CDN.**
`ereaderfiles.kobo.com` serves `Access-Control-Allow-Origin: *`, so direct `fetch()` works.
User no longer needs to provide firmware manually. URLs hardcoded in `kobo-device.js`.
- **Web Worker for WASM.**
Moves patching off the main thread so progress updates render live during the build.
`patch-worker.js` loads `wasm_exec.js` + `kobopatch.wasm`, communicates via `postMessage`.

View File

@@ -1,48 +0,0 @@
# WASM Feasibility — Confirmed
## Test Results
All kobopatch tests pass under both native and WASM targets.
### Native (`go test ./...`)
```
ok github.com/pgaskin/kobopatch/kobopatch 0.003s
ok github.com/pgaskin/kobopatch/patchfile/kobopatch 0.003s
ok github.com/pgaskin/kobopatch/patchfile/patch32lsb 0.002s
ok github.com/pgaskin/kobopatch/patchlib 0.796s
```
### WASM (`GOOS=js GOARCH=wasm`, run via Node.js)
```
ok github.com/pgaskin/kobopatch/kobopatch 0.158s
ok github.com/pgaskin/kobopatch/patchfile/kobopatch 0.162s
ok github.com/pgaskin/kobopatch/patchfile/patch32lsb 0.133s
ok github.com/pgaskin/kobopatch/patchlib 9.755s
```
### Notes
- `patchlib` tests are ~12x slower under WASM (9.7s vs 0.8s) — expected overhead
- All pure Go dependencies compile to WASM without issues
- No CGO, no OS-specific syscalls in the core libraries
- Go WASM executor: `$(go env GOROOT)/lib/wasm/go_js_wasm_exec`
- Node.js v25.8.1 used for WASM test execution
## What Works in WASM
- Binary patching (`patchlib`)
- ARM Thumb-2 instruction assembly (`patchlib/asm`)
- ELF symbol table parsing (`patchlib/syms`)
- YAML patch format parsing (`patchfile/kobopatch`)
- Binary patch format parsing (`patchfile/patch32lsb`)
- CSS parsing (`patchlib/css`)
- Zlib compression/decompression
- tar.gz reading/writing
- ZIP extraction
## What Won't Work in WASM (and doesn't need to)
- `os.Open` / `os.Create` — replace with in-memory I/O
- `os.Chdir` — not needed, use in-memory paths
- `exec.Command` (lrelease for translations) — skip, rare use case
- `ioutil.TempDir` — not needed with in-memory approach