koreader_D00901 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- koreader_d00901-0.1.0/LICENSE +21 -0
- koreader_d00901-0.1.0/PKG-INFO +188 -0
- koreader_d00901-0.1.0/README.md +166 -0
- koreader_d00901-0.1.0/pyproject.toml +58 -0
- koreader_d00901-0.1.0/pyproject.toml.orig +51 -0
- koreader_d00901-0.1.0/src/koreader_d00901/__init__.py +8 -0
- koreader_d00901-0.1.0/src/koreader_d00901/__main__.py +3 -0
- koreader_d00901-0.1.0/src/koreader_d00901/cli.py +168 -0
- koreader_d00901-0.1.0/src/koreader_d00901/device.py +148 -0
- koreader_d00901-0.1.0/src/koreader_d00901/errors.py +29 -0
- koreader_d00901-0.1.0/src/koreader_d00901/logs.py +31 -0
- koreader_d00901-0.1.0/src/koreader_d00901/packages.json +32 -0
- koreader_d00901-0.1.0/src/koreader_d00901/packages.py +150 -0
- koreader_d00901-0.1.0/src/koreader_d00901/pins.py +68 -0
- koreader_d00901-0.1.0/src/koreader_d00901/plan.py +57 -0
- koreader_d00901-0.1.0/src/koreader_d00901/tui.py +420 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Seth Barrett
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: koreader_D00901
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Install KOReader on a Kindle Keyboard (K3) from Linux.
|
|
5
|
+
Keywords: kindle,koreader,kindle-keyboard,k3,jailbreak,e-reader
|
|
6
|
+
Author: Seth Barrett
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
17
|
+
Requires-Dist: textual>=8.2,<9
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Project-URL: Homepage, https://github.com/sethbarrett50/koreader_D00901
|
|
20
|
+
Project-URL: Issues, https://github.com/sethbarrett50/koreader_D00901/issues
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# KOReader installer for the Kindle Keyboard (K3)
|
|
24
|
+
|
|
25
|
+
Use this at your own risk: it modifies your Kindle's firmware and I am not responsible for a unit that stops working.
|
|
26
|
+
|
|
27
|
+
## What this does
|
|
28
|
+
|
|
29
|
+
`koreader-d00901` (the `koreader_D00901` Python package) downloads pinned, checksum-verified packages and stages them on a Kindle Keyboard (K3) that is mounted as a USB drive. It chains:
|
|
30
|
+
|
|
31
|
+
1. The jailbreak, which lets the Kindle install custom update packages.
|
|
32
|
+
2. MKK (the developer certificates the Kindle needs to run custom Kindlets such as KUAL).
|
|
33
|
+
MKK's 2014 certificates expired in April 2025, so the installer then applies NiLuJe's 2025 developer keystore update, which adds the certificates current KUAL is signed with.
|
|
34
|
+
3. KUAL (the Kindle Unified Application Launcher), copied into `documents/`.
|
|
35
|
+
4. KOReader, merged into the Kindle's root.
|
|
36
|
+
5. Optionally, USBNetwork (`--usbnet`), for ssh access over USB.
|
|
37
|
+
|
|
38
|
+
## Before you start
|
|
39
|
+
|
|
40
|
+
- Charge the Kindle fully.
|
|
41
|
+
- Deregister it: Settings > Registration > Deregister.
|
|
42
|
+
- Turn wireless off.
|
|
43
|
+
- Find your firmware version at the bottom of the Settings page (for example 3.4.2).
|
|
44
|
+
- Find your serial prefix: the first four characters of the serial number (B006, B008 or B00A), listed under Device Info or on the back of the unit.
|
|
45
|
+
|
|
46
|
+
## Which files your Kindle needs
|
|
47
|
+
|
|
48
|
+
The installer picks the right files from your serial prefix and firmware range.
|
|
49
|
+
|
|
50
|
+
| Serial prefix | Variant |
|
|
51
|
+
| ------------- | ------- |
|
|
52
|
+
| B006 | k3g |
|
|
53
|
+
| B008 | k3w |
|
|
54
|
+
| B00A | k3gb |
|
|
55
|
+
|
|
56
|
+
Firmware 3.0-3.2 uses the jailbreak file marked `-3.0-to-3.2`. Firmware 3.3-3.4 uses the plain jailbreak file. The installer asks for the range as `3.0-3.2` or `3.3-3.4`.
|
|
57
|
+
|
|
58
|
+
## Running the installer
|
|
59
|
+
|
|
60
|
+
Install from PyPI, then connect the Kindle in USB drive mode and start the full-screen installer:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
uv tool install koreader_D00901 # or: pipx install koreader_D00901
|
|
64
|
+
koreader-d00901-tui
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To run it once without installing anything, use `uvx --from koreader_D00901 koreader-d00901-tui`. From a clone of this repo, use `uv run koreader-d00901-tui`.
|
|
68
|
+
|
|
69
|
+
Pick the firmware range and serial prefix, press **Install**, and follow the on-screen steps. Between updates it shows what to do on the Kindle, has an **Eject Kindle** button, and waits for **Continue**. **Dry run** shows the plan without changing anything.
|
|
70
|
+
|
|
71
|
+
The same install also runs as a plain terminal script. Do a dry run first: it prints the plan and changes nothing.
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
uv run koreader-d00901 --dry-run
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then run it for real:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
uv run koreader-d00901
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Flags (both commands take the same ones, except that `--dry-run` is CLI-only):
|
|
84
|
+
|
|
85
|
+
- `--dry-run`: print the plan only.
|
|
86
|
+
- `--mount PATH`: the Kindle's USB mount point. Without it the installer looks for a volume labelled Kindle.
|
|
87
|
+
- `--fw RANGE`: firmware range, `3.0-3.2` or `3.3-3.4`. Asked interactively if omitted.
|
|
88
|
+
- `--prefix PREFIX`: serial prefix, `B006`, `B008` or `B00A`. Asked interactively if omitted.
|
|
89
|
+
- `--usbnet`: also install USBNetwork.
|
|
90
|
+
- `--wipe-books`: delete the existing books in `documents/` before installing, which is handy for second-hand units. It lists everything it will delete and asks you to type `wipe` (the TUI shows a confirmation dialog). It keeps `KUAL-KDK-1.0.azw2` and the `dictionaries` folder, and never touches anything outside `documents/`. With `--dry-run` it only lists what it would delete.
|
|
91
|
+
- `--cache DIR`: where downloads are kept (default `~/.cache/k3-koreader`).
|
|
92
|
+
- `--log-file FILE`: where the debug log goes (default `CACHE/install.log`).
|
|
93
|
+
- `-v`, `--verbose`: also print debug logging on the terminal (CLI only).
|
|
94
|
+
- `--version`: print the version.
|
|
95
|
+
|
|
96
|
+
Every run writes a debug log. If an install fails, check `~/.cache/k3-koreader/install.log` first.
|
|
97
|
+
|
|
98
|
+
Each download is checked against the sha256 in `packages.json` and rejected if it does not match.
|
|
99
|
+
|
|
100
|
+
## On the Kindle
|
|
101
|
+
|
|
102
|
+
For each update the installer stages one `.bin` file and pauses. Then:
|
|
103
|
+
|
|
104
|
+
1. Eject and unplug the Kindle (safely eject it first; the update cannot run in USB drive mode).
|
|
105
|
+
2. Press [HOME] > [MENU] > Settings > [MENU] > Update Your Kindle.
|
|
106
|
+
3. Wait for the reboot, reconnect in USB drive mode and press Enter in the installer.
|
|
107
|
+
|
|
108
|
+
The order is jailbreak, then MKK, then the 2025 keystore (then USBNetwork if requested). After the last step, eject the Kindle, open KUAL from the library and choose KOReader.
|
|
109
|
+
|
|
110
|
+
## If something goes wrong
|
|
111
|
+
|
|
112
|
+
- Update Your Kindle is greyed out: the wrong file was staged. Check your serial prefix and firmware range and run the installer again.
|
|
113
|
+
- Never force an update by rebooting the Kindle by hand.
|
|
114
|
+
- If the Kindle hangs, hold the power button for about 15 seconds for a hard reboot.
|
|
115
|
+
- Deregistering or an over-the-air (OTA) update can remove the developer certificates. Reinstall MKK when that happens.
|
|
116
|
+
- KUAL says the title is "not signed by a registered developer": the 2025 keystore update did not apply. Copy `Update-mkk-20250419-<variant>-<prefix>_keystore-install.bin` from `DevCerts-20250419-KeyStore.zip` to the Kindle root and run Update Your Kindle again.
|
|
117
|
+
|
|
118
|
+
## USBNetwork (optional)
|
|
119
|
+
|
|
120
|
+
After installing with `--usbnet`, enable it from the Kindle's search bar:
|
|
121
|
+
|
|
122
|
+
1. Type `;debugOn` and press Enter.
|
|
123
|
+
2. Type `~usbNetwork` and press Enter.
|
|
124
|
+
3. Type `;debugOff` and press Enter.
|
|
125
|
+
|
|
126
|
+
Then connect over USB. On Debian the Kindle appears as a USB network interface, often `usb0` (check with `ip link`). Assign the computer side its address:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
sudo ip addr add 192.168.2.1/24 dev usb0
|
|
130
|
+
sudo ip link set usb0 up
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
(The older equivalent is `sudo ifconfig usb0 192.168.2.1`.) Then use `ssh root@192.168.2.2`. Login is passwordless only over USB with Wi-Fi mode off.
|
|
134
|
+
|
|
135
|
+
Before ejecting, bring the interface down with `sudo ip link set usb0 down`, and enter `~usbNetwork` again on the Kindle to switch back to USB drive mode.
|
|
136
|
+
|
|
137
|
+
## Pinned packages
|
|
138
|
+
|
|
139
|
+
| Package | File | sha256 (first 12) |
|
|
140
|
+
| ---------- | ---------------------------------------- | ----------------- |
|
|
141
|
+
| jailbreak | kindle-jailbreak-0.13.N-r18833.tar.xz | 8f5c8a78e3c4 |
|
|
142
|
+
| mkk | kindle-mkk-20141129-r18833.tar.xz | 3d7289f1ddcf |
|
|
143
|
+
| keystore | DevCerts-20250419-KeyStore.zip | 48426add32e3 |
|
|
144
|
+
| kual | KUAL-v2.7.37-gfcb45b5-20250419.tar.xz | 40c4b6871221 |
|
|
145
|
+
| usbnet | kindle-usbnetwork-0.57.N-k3.zip | 7d210bc41619 |
|
|
146
|
+
| koreader | koreader-kindle-legacy-v2026.07.1.zip | 0ed8d5eec422 |
|
|
147
|
+
|
|
148
|
+
## Credits
|
|
149
|
+
|
|
150
|
+
- Jailbreak: https://www.mobileread.com/forums/showthread.php?t=225030
|
|
151
|
+
- 2025 developer keystore (NiLuJe, 2025-04-19): https://www.mobileread.com/forums/attachment.php?attachmentid=215127&d=1745098511
|
|
152
|
+
- KUAL: https://www.mobileread.com/forums/showthread.php?t=203326
|
|
153
|
+
- USBNetwork: https://www.mobileread.com/forums/showthread.php?t=88004
|
|
154
|
+
- KOReader: https://github.com/koreader/koreader
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
make sync # uv sync --dev (ruff, ty, pytest)
|
|
160
|
+
make lint # ruff format + ruff check --fix
|
|
161
|
+
make typecheck # ty check
|
|
162
|
+
make test # pytest
|
|
163
|
+
make ci # everything CI runs, without fixes
|
|
164
|
+
make build # sdist + wheel into dist/
|
|
165
|
+
make smoke # install the wheel in a clean env and run both commands
|
|
166
|
+
make version-check # package changed since the last tag? then the version must be bumped
|
|
167
|
+
uv run python -m koreader_d00901.pins # re-download every pin and check it (about 100 MB)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
CI (`.github/workflows/ci.yml`) runs `make ci`, `make build` and `make smoke` on Python 3.14 for every push and PR. A weekly pin check (`.github/workflows/pins.yml`, also runnable from the Actions tab) re-downloads every package in `packages.json`, verifies its sha256, and confirms that it still holds every file the installer stages for each serial prefix and firmware range. If upstream drifts, it opens an issue (or comments on the open one).
|
|
171
|
+
|
|
172
|
+
### Releasing
|
|
173
|
+
|
|
174
|
+
1. Bump `version` in `pyproject.toml` (`uv version --bump patch`, `minor` or `major`), then merge to `main`. CI's `make version-check` fails any PR that changes `src/`, the dependencies or `uv.lock` since the last `vX.Y.Z` tag without raising the version, so every release gets a new version on PyPI.
|
|
175
|
+
2. Tag the merge commit `vX.Y.Z` (matching that version) and push the tag: `git tag v0.1.0 && git push origin v0.1.0`.
|
|
176
|
+
3. `.github/workflows/release.yml` checks the tag against the version, runs `make ci`, builds and smoke-tests the wheel, and publishes it to PyPI through trusted publishing (no API token is stored anywhere).
|
|
177
|
+
|
|
178
|
+
One-time setup on PyPI, before the first release: at https://pypi.org/manage/account/publishing/ add a pending trusted publisher with project `koreader_D00901`, owner `sethbarrett50`, repository `koreader_D00901`, workflow `release.yml` and environment `pypi`. In the GitHub repo settings, create an environment named `pypi` (adding yourself as a required reviewer gives you a manual approval step before each upload).
|
|
179
|
+
|
|
180
|
+
The code lives in `src/koreader_d00901/`:
|
|
181
|
+
|
|
182
|
+
- `plan.py`: which files a given serial prefix and firmware range need.
|
|
183
|
+
- `packages.py`: downloading, verifying and unpacking the packages pinned in `packages.json`.
|
|
184
|
+
- `device.py`: everything that touches the Kindle's USB volume.
|
|
185
|
+
- `cli.py` / `tui.py`: the two front ends.
|
|
186
|
+
- `errors.py`: `InstallerError` and its subclasses, which are reported to the user as one line.
|
|
187
|
+
- `logs.py`: logging setup (a debug log file on every run).
|
|
188
|
+
- `pins.py`: the upstream pin check used by the weekly workflow.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# KOReader installer for the Kindle Keyboard (K3)
|
|
2
|
+
|
|
3
|
+
Use this at your own risk: it modifies your Kindle's firmware and I am not responsible for a unit that stops working.
|
|
4
|
+
|
|
5
|
+
## What this does
|
|
6
|
+
|
|
7
|
+
`koreader-d00901` (the `koreader_D00901` Python package) downloads pinned, checksum-verified packages and stages them on a Kindle Keyboard (K3) that is mounted as a USB drive. It chains:
|
|
8
|
+
|
|
9
|
+
1. The jailbreak, which lets the Kindle install custom update packages.
|
|
10
|
+
2. MKK (the developer certificates the Kindle needs to run custom Kindlets such as KUAL).
|
|
11
|
+
MKK's 2014 certificates expired in April 2025, so the installer then applies NiLuJe's 2025 developer keystore update, which adds the certificates current KUAL is signed with.
|
|
12
|
+
3. KUAL (the Kindle Unified Application Launcher), copied into `documents/`.
|
|
13
|
+
4. KOReader, merged into the Kindle's root.
|
|
14
|
+
5. Optionally, USBNetwork (`--usbnet`), for ssh access over USB.
|
|
15
|
+
|
|
16
|
+
## Before you start
|
|
17
|
+
|
|
18
|
+
- Charge the Kindle fully.
|
|
19
|
+
- Deregister it: Settings > Registration > Deregister.
|
|
20
|
+
- Turn wireless off.
|
|
21
|
+
- Find your firmware version at the bottom of the Settings page (for example 3.4.2).
|
|
22
|
+
- Find your serial prefix: the first four characters of the serial number (B006, B008 or B00A), listed under Device Info or on the back of the unit.
|
|
23
|
+
|
|
24
|
+
## Which files your Kindle needs
|
|
25
|
+
|
|
26
|
+
The installer picks the right files from your serial prefix and firmware range.
|
|
27
|
+
|
|
28
|
+
| Serial prefix | Variant |
|
|
29
|
+
| ------------- | ------- |
|
|
30
|
+
| B006 | k3g |
|
|
31
|
+
| B008 | k3w |
|
|
32
|
+
| B00A | k3gb |
|
|
33
|
+
|
|
34
|
+
Firmware 3.0-3.2 uses the jailbreak file marked `-3.0-to-3.2`. Firmware 3.3-3.4 uses the plain jailbreak file. The installer asks for the range as `3.0-3.2` or `3.3-3.4`.
|
|
35
|
+
|
|
36
|
+
## Running the installer
|
|
37
|
+
|
|
38
|
+
Install from PyPI, then connect the Kindle in USB drive mode and start the full-screen installer:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
uv tool install koreader_D00901 # or: pipx install koreader_D00901
|
|
42
|
+
koreader-d00901-tui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To run it once without installing anything, use `uvx --from koreader_D00901 koreader-d00901-tui`. From a clone of this repo, use `uv run koreader-d00901-tui`.
|
|
46
|
+
|
|
47
|
+
Pick the firmware range and serial prefix, press **Install**, and follow the on-screen steps. Between updates it shows what to do on the Kindle, has an **Eject Kindle** button, and waits for **Continue**. **Dry run** shows the plan without changing anything.
|
|
48
|
+
|
|
49
|
+
The same install also runs as a plain terminal script. Do a dry run first: it prints the plan and changes nothing.
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
uv run koreader-d00901 --dry-run
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then run it for real:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
uv run koreader-d00901
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Flags (both commands take the same ones, except that `--dry-run` is CLI-only):
|
|
62
|
+
|
|
63
|
+
- `--dry-run`: print the plan only.
|
|
64
|
+
- `--mount PATH`: the Kindle's USB mount point. Without it the installer looks for a volume labelled Kindle.
|
|
65
|
+
- `--fw RANGE`: firmware range, `3.0-3.2` or `3.3-3.4`. Asked interactively if omitted.
|
|
66
|
+
- `--prefix PREFIX`: serial prefix, `B006`, `B008` or `B00A`. Asked interactively if omitted.
|
|
67
|
+
- `--usbnet`: also install USBNetwork.
|
|
68
|
+
- `--wipe-books`: delete the existing books in `documents/` before installing, which is handy for second-hand units. It lists everything it will delete and asks you to type `wipe` (the TUI shows a confirmation dialog). It keeps `KUAL-KDK-1.0.azw2` and the `dictionaries` folder, and never touches anything outside `documents/`. With `--dry-run` it only lists what it would delete.
|
|
69
|
+
- `--cache DIR`: where downloads are kept (default `~/.cache/k3-koreader`).
|
|
70
|
+
- `--log-file FILE`: where the debug log goes (default `CACHE/install.log`).
|
|
71
|
+
- `-v`, `--verbose`: also print debug logging on the terminal (CLI only).
|
|
72
|
+
- `--version`: print the version.
|
|
73
|
+
|
|
74
|
+
Every run writes a debug log. If an install fails, check `~/.cache/k3-koreader/install.log` first.
|
|
75
|
+
|
|
76
|
+
Each download is checked against the sha256 in `packages.json` and rejected if it does not match.
|
|
77
|
+
|
|
78
|
+
## On the Kindle
|
|
79
|
+
|
|
80
|
+
For each update the installer stages one `.bin` file and pauses. Then:
|
|
81
|
+
|
|
82
|
+
1. Eject and unplug the Kindle (safely eject it first; the update cannot run in USB drive mode).
|
|
83
|
+
2. Press [HOME] > [MENU] > Settings > [MENU] > Update Your Kindle.
|
|
84
|
+
3. Wait for the reboot, reconnect in USB drive mode and press Enter in the installer.
|
|
85
|
+
|
|
86
|
+
The order is jailbreak, then MKK, then the 2025 keystore (then USBNetwork if requested). After the last step, eject the Kindle, open KUAL from the library and choose KOReader.
|
|
87
|
+
|
|
88
|
+
## If something goes wrong
|
|
89
|
+
|
|
90
|
+
- Update Your Kindle is greyed out: the wrong file was staged. Check your serial prefix and firmware range and run the installer again.
|
|
91
|
+
- Never force an update by rebooting the Kindle by hand.
|
|
92
|
+
- If the Kindle hangs, hold the power button for about 15 seconds for a hard reboot.
|
|
93
|
+
- Deregistering or an over-the-air (OTA) update can remove the developer certificates. Reinstall MKK when that happens.
|
|
94
|
+
- KUAL says the title is "not signed by a registered developer": the 2025 keystore update did not apply. Copy `Update-mkk-20250419-<variant>-<prefix>_keystore-install.bin` from `DevCerts-20250419-KeyStore.zip` to the Kindle root and run Update Your Kindle again.
|
|
95
|
+
|
|
96
|
+
## USBNetwork (optional)
|
|
97
|
+
|
|
98
|
+
After installing with `--usbnet`, enable it from the Kindle's search bar:
|
|
99
|
+
|
|
100
|
+
1. Type `;debugOn` and press Enter.
|
|
101
|
+
2. Type `~usbNetwork` and press Enter.
|
|
102
|
+
3. Type `;debugOff` and press Enter.
|
|
103
|
+
|
|
104
|
+
Then connect over USB. On Debian the Kindle appears as a USB network interface, often `usb0` (check with `ip link`). Assign the computer side its address:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
sudo ip addr add 192.168.2.1/24 dev usb0
|
|
108
|
+
sudo ip link set usb0 up
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
(The older equivalent is `sudo ifconfig usb0 192.168.2.1`.) Then use `ssh root@192.168.2.2`. Login is passwordless only over USB with Wi-Fi mode off.
|
|
112
|
+
|
|
113
|
+
Before ejecting, bring the interface down with `sudo ip link set usb0 down`, and enter `~usbNetwork` again on the Kindle to switch back to USB drive mode.
|
|
114
|
+
|
|
115
|
+
## Pinned packages
|
|
116
|
+
|
|
117
|
+
| Package | File | sha256 (first 12) |
|
|
118
|
+
| ---------- | ---------------------------------------- | ----------------- |
|
|
119
|
+
| jailbreak | kindle-jailbreak-0.13.N-r18833.tar.xz | 8f5c8a78e3c4 |
|
|
120
|
+
| mkk | kindle-mkk-20141129-r18833.tar.xz | 3d7289f1ddcf |
|
|
121
|
+
| keystore | DevCerts-20250419-KeyStore.zip | 48426add32e3 |
|
|
122
|
+
| kual | KUAL-v2.7.37-gfcb45b5-20250419.tar.xz | 40c4b6871221 |
|
|
123
|
+
| usbnet | kindle-usbnetwork-0.57.N-k3.zip | 7d210bc41619 |
|
|
124
|
+
| koreader | koreader-kindle-legacy-v2026.07.1.zip | 0ed8d5eec422 |
|
|
125
|
+
|
|
126
|
+
## Credits
|
|
127
|
+
|
|
128
|
+
- Jailbreak: https://www.mobileread.com/forums/showthread.php?t=225030
|
|
129
|
+
- 2025 developer keystore (NiLuJe, 2025-04-19): https://www.mobileread.com/forums/attachment.php?attachmentid=215127&d=1745098511
|
|
130
|
+
- KUAL: https://www.mobileread.com/forums/showthread.php?t=203326
|
|
131
|
+
- USBNetwork: https://www.mobileread.com/forums/showthread.php?t=88004
|
|
132
|
+
- KOReader: https://github.com/koreader/koreader
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
make sync # uv sync --dev (ruff, ty, pytest)
|
|
138
|
+
make lint # ruff format + ruff check --fix
|
|
139
|
+
make typecheck # ty check
|
|
140
|
+
make test # pytest
|
|
141
|
+
make ci # everything CI runs, without fixes
|
|
142
|
+
make build # sdist + wheel into dist/
|
|
143
|
+
make smoke # install the wheel in a clean env and run both commands
|
|
144
|
+
make version-check # package changed since the last tag? then the version must be bumped
|
|
145
|
+
uv run python -m koreader_d00901.pins # re-download every pin and check it (about 100 MB)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
CI (`.github/workflows/ci.yml`) runs `make ci`, `make build` and `make smoke` on Python 3.14 for every push and PR. A weekly pin check (`.github/workflows/pins.yml`, also runnable from the Actions tab) re-downloads every package in `packages.json`, verifies its sha256, and confirms that it still holds every file the installer stages for each serial prefix and firmware range. If upstream drifts, it opens an issue (or comments on the open one).
|
|
149
|
+
|
|
150
|
+
### Releasing
|
|
151
|
+
|
|
152
|
+
1. Bump `version` in `pyproject.toml` (`uv version --bump patch`, `minor` or `major`), then merge to `main`. CI's `make version-check` fails any PR that changes `src/`, the dependencies or `uv.lock` since the last `vX.Y.Z` tag without raising the version, so every release gets a new version on PyPI.
|
|
153
|
+
2. Tag the merge commit `vX.Y.Z` (matching that version) and push the tag: `git tag v0.1.0 && git push origin v0.1.0`.
|
|
154
|
+
3. `.github/workflows/release.yml` checks the tag against the version, runs `make ci`, builds and smoke-tests the wheel, and publishes it to PyPI through trusted publishing (no API token is stored anywhere).
|
|
155
|
+
|
|
156
|
+
One-time setup on PyPI, before the first release: at https://pypi.org/manage/account/publishing/ add a pending trusted publisher with project `koreader_D00901`, owner `sethbarrett50`, repository `koreader_D00901`, workflow `release.yml` and environment `pypi`. In the GitHub repo settings, create an environment named `pypi` (adding yourself as a required reviewer gives you a manual approval step before each upload).
|
|
157
|
+
|
|
158
|
+
The code lives in `src/koreader_d00901/`:
|
|
159
|
+
|
|
160
|
+
- `plan.py`: which files a given serial prefix and firmware range need.
|
|
161
|
+
- `packages.py`: downloading, verifying and unpacking the packages pinned in `packages.json`.
|
|
162
|
+
- `device.py`: everything that touches the Kindle's USB volume.
|
|
163
|
+
- `cli.py` / `tui.py`: the two front ends.
|
|
164
|
+
- `errors.py`: `InstallerError` and its subclasses, which are reported to the user as one line.
|
|
165
|
+
- `logs.py`: logging setup (a debug log file on every run).
|
|
166
|
+
- `pins.py`: the upstream pin check used by the weekly workflow.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "koreader_D00901"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Install KOReader on a Kindle Keyboard (K3) from Linux."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"kindle",
|
|
11
|
+
"koreader",
|
|
12
|
+
"kindle-keyboard",
|
|
13
|
+
"k3",
|
|
14
|
+
"jailbreak",
|
|
15
|
+
"e-reader",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: End Users/Desktop",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Topic :: System :: Installation/Setup",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["textual>=8.2,<9"]
|
|
28
|
+
|
|
29
|
+
[[project.authors]]
|
|
30
|
+
name = "Seth Barrett"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/sethbarrett50/koreader_D00901"
|
|
34
|
+
Issues = "https://github.com/sethbarrett50/koreader_D00901/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
koreader-d00901 = "koreader_d00901.cli:main"
|
|
38
|
+
koreader-d00901-tui = "koreader_d00901.tui:main"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.9,<0.12"]
|
|
42
|
+
build-backend = "uv_build"
|
|
43
|
+
|
|
44
|
+
[tool.uv.build-backend]
|
|
45
|
+
module-name = "koreader_d00901"
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
|
|
50
|
+
[tool.ty.environment]
|
|
51
|
+
python-version = "3.14"
|
|
52
|
+
|
|
53
|
+
[dependency-groups]
|
|
54
|
+
dev = [
|
|
55
|
+
"ruff>=0.15.15,<1.0.0",
|
|
56
|
+
"ty>=0.0.97,<0.1.0",
|
|
57
|
+
"pytest>=9.0.3,<10.0.0",
|
|
58
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "koreader_D00901"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Install KOReader on a Kindle Keyboard (K3) from Linux."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
authors = [{ name = "Seth Barrett" }]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
keywords = ["kindle", "koreader", "kindle-keyboard", "k3", "jailbreak", "e-reader"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: End Users/Desktop",
|
|
15
|
+
"Operating System :: POSIX :: Linux",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: System :: Installation/Setup",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"textual>=8.2,<9",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/sethbarrett50/koreader_D00901"
|
|
27
|
+
Issues = "https://github.com/sethbarrett50/koreader_D00901/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
koreader-d00901 = "koreader_d00901.cli:main"
|
|
31
|
+
koreader-d00901-tui = "koreader_d00901.tui:main"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.9,<0.12"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[tool.uv.build-backend]
|
|
38
|
+
module-name = "koreader_d00901"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"ruff>=0.15.15,<1.0.0",
|
|
43
|
+
"ty>=0.0.97,<0.1.0",
|
|
44
|
+
"pytest>=9.0.3,<10.0.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
|
|
50
|
+
[tool.ty.environment]
|
|
51
|
+
python-version = "3.14"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Install KOReader on a Kindle Keyboard (K3) from Linux."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version('koreader_D00901')
|
|
7
|
+
except PackageNotFoundError: # running from a source tree that was never installed
|
|
8
|
+
__version__ = '0.0.0'
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Command-line installer: prompts on the terminal and waits for Enter between device updates."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from koreader_d00901 import __version__, device, packages, plan
|
|
11
|
+
from koreader_d00901.errors import ConfigError, InstallerError
|
|
12
|
+
from koreader_d00901.logs import DEFAULT_CACHE, setup_logging
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
DEVICE_STEPS = (
|
|
20
|
+
' First safely eject (unmount) the Kindle and unplug it from the computer;\n'
|
|
21
|
+
' the update cannot run while it is in USB drive mode.\n'
|
|
22
|
+
' Then on the Kindle: [HOME] > [MENU] > Settings > [MENU] > Update Your Kindle.\n'
|
|
23
|
+
' Never reboot the Kindle by hand to force the update.\n'
|
|
24
|
+
" If 'Update Your Kindle' is greyed out, the wrong file was staged."
|
|
25
|
+
)
|
|
26
|
+
DONE_MESSAGE = 'Done. Eject the Kindle, then on the device open the KUAL book in your library and choose KOReader.'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
30
|
+
parser.add_argument('--mount', type=Path, help='Kindle USB mount point (auto-detected if omitted)')
|
|
31
|
+
parser.add_argument('--fw', help='firmware range: ' + ', '.join(plan.FIRMWARE_RANGES))
|
|
32
|
+
parser.add_argument('--prefix', help='serial prefix: ' + ', '.join(plan.VARIANTS))
|
|
33
|
+
parser.add_argument('--usbnet', action='store_true', help='also install USBNetwork')
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
'--wipe-books', action='store_true', help='delete the existing books in documents/ first (asks to confirm)'
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument('--cache', type=Path, default=DEFAULT_CACHE, help='download cache directory')
|
|
38
|
+
parser.add_argument('--log-file', type=Path, help='debug log file (default CACHE/install.log)')
|
|
39
|
+
parser.add_argument('-v', '--verbose', action='store_true', help='show debug logging on the console')
|
|
40
|
+
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _ask_valid(prompt: str, value: str | None, check: Callable[[str], str], flag: str) -> str:
|
|
44
|
+
"""Validate a flag value (fatal when bad) or prompt until one validates."""
|
|
45
|
+
if value is not None:
|
|
46
|
+
try:
|
|
47
|
+
return check(value)
|
|
48
|
+
except ConfigError as exc:
|
|
49
|
+
raise ConfigError(f'{flag}: {exc}') from None
|
|
50
|
+
while True:
|
|
51
|
+
try:
|
|
52
|
+
return check(input(prompt))
|
|
53
|
+
except ConfigError as exc:
|
|
54
|
+
print(f' {exc}')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _copy_with_progress(pairs: list[tuple[Path, Path]], label: str) -> None:
|
|
58
|
+
"""Copy (src, dst) file pairs, printing a running count so slow USB writes don't look hung."""
|
|
59
|
+
every = max(1, len(pairs) // 100)
|
|
60
|
+
|
|
61
|
+
def show(i: int, total: int) -> None:
|
|
62
|
+
if i % every == 0 or i == total:
|
|
63
|
+
print(f'\r {label}: {i}/{total} files', end='', flush=True)
|
|
64
|
+
|
|
65
|
+
device.copy_files(pairs, show)
|
|
66
|
+
print()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _update_step(mount: Path, bin_path: Path, label: str) -> None:
|
|
70
|
+
device.stage_update(mount, bin_path)
|
|
71
|
+
print(f'\nStaged {bin_path.name} ({label}).')
|
|
72
|
+
print(DEVICE_STEPS)
|
|
73
|
+
input('Press Enter once the Kindle has rebooted and remounted... ')
|
|
74
|
+
while not mount.is_dir():
|
|
75
|
+
log.debug('waiting for %s to come back', mount)
|
|
76
|
+
input(f'Kindle not mounted at {mount} yet; reconnect it in USB drive mode and press Enter... ')
|
|
77
|
+
if device.update_pending(mount, bin_path):
|
|
78
|
+
log.warning('%s is still at the Kindle root; the update may not have applied.', bin_path.name)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def describe(entries: list[Path], limit: int = 20) -> list[str]:
|
|
82
|
+
"""Display lines for entries of documents/, capped at limit."""
|
|
83
|
+
lines = [f' {p.name}{"/" if p.is_dir() else ""}' for p in entries[:limit]]
|
|
84
|
+
if len(entries) > limit:
|
|
85
|
+
lines.append(f' ...and {len(entries) - limit} more')
|
|
86
|
+
return lines
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _wipe_step(mount: Path, dry_run: bool) -> None:
|
|
90
|
+
entries = device.list_books(mount)
|
|
91
|
+
if not entries:
|
|
92
|
+
print('No books to delete in documents/.')
|
|
93
|
+
return
|
|
94
|
+
verb = 'would delete' if dry_run else 'will delete'
|
|
95
|
+
print(f'--wipe-books {verb} {len(entries)} entries in documents/ (keeping KUAL and dictionaries):')
|
|
96
|
+
print('\n'.join(describe(entries)))
|
|
97
|
+
if dry_run:
|
|
98
|
+
return
|
|
99
|
+
answer = input(f"Type 'wipe' to delete these {len(entries)} entries (anything else skips this step): ")
|
|
100
|
+
if answer.strip().lower() != 'wipe':
|
|
101
|
+
print('Skipped; no books were deleted.')
|
|
102
|
+
log.info('wipe declined')
|
|
103
|
+
return
|
|
104
|
+
removed = device.wipe_books(mount, entries)
|
|
105
|
+
print(f'Deleted {removed} entries.')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run(args: argparse.Namespace) -> int:
|
|
109
|
+
fw = _ask_valid(f'Firmware range ({"/".join(plan.FIRMWARE_RANGES)}): ', args.fw, plan.check_firmware, '--fw')
|
|
110
|
+
prefix = _ask_valid(f'Serial prefix ({"/".join(plan.VARIANTS)}): ', args.prefix, plan.check_prefix, '--prefix')
|
|
111
|
+
files = plan.select_files(fw, prefix, args.usbnet)
|
|
112
|
+
mount = device.require_mount(args.mount)
|
|
113
|
+
steps = plan.update_steps(files)
|
|
114
|
+
log.debug('plan: fw=%s prefix=%s usbnet=%s mount=%s files=%s', fw, prefix, args.usbnet, mount, files)
|
|
115
|
+
|
|
116
|
+
if args.dry_run:
|
|
117
|
+
print(f'Dry run: mount={mount} firmware={fw} prefix={prefix}')
|
|
118
|
+
if args.wipe_books:
|
|
119
|
+
_wipe_step(mount, dry_run=True)
|
|
120
|
+
for key, label in steps:
|
|
121
|
+
print(f' would stage {files[key]} ({label})')
|
|
122
|
+
print(f' would copy {plan.KUAL_FILE} into documents/')
|
|
123
|
+
print(' would merge koreader/, extensions/, launchpad/ into the mount root')
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
if args.wipe_books:
|
|
127
|
+
_wipe_step(mount, dry_run=False)
|
|
128
|
+
|
|
129
|
+
print('Getting packages (cached after the first run):')
|
|
130
|
+
roots = packages.fetch_packages(plan.package_keys(files), args.cache, lambda name: print(f' {name}', flush=True))
|
|
131
|
+
bins = [(packages.member_path(roots[key], files[key]), label) for key, label in steps]
|
|
132
|
+
kual = packages.member_path(roots['kual'], files['kual'])
|
|
133
|
+
for bin_path, label in bins:
|
|
134
|
+
_update_step(mount, bin_path, label)
|
|
135
|
+
|
|
136
|
+
print(f'\nCopying {kual.name} to documents/')
|
|
137
|
+
device.install_kual(mount, kual)
|
|
138
|
+
|
|
139
|
+
pairs = device.koreader_pairs(roots['koreader'], mount)
|
|
140
|
+
size_mb = sum(src.stat().st_size for src, _ in pairs) / 1e6
|
|
141
|
+
print(
|
|
142
|
+
f'Copying KOReader ({len(pairs)} files, {size_mb:.0f} MB). '
|
|
143
|
+
'This takes several minutes on a K3; leave it plugged in.'
|
|
144
|
+
)
|
|
145
|
+
_copy_with_progress(pairs, 'KOReader')
|
|
146
|
+
print('Flushing writes to the Kindle...', flush=True)
|
|
147
|
+
device.flush()
|
|
148
|
+
print(f'\n{DONE_MESSAGE}')
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(argv: list[str] | None = None) -> int:
|
|
153
|
+
parser = argparse.ArgumentParser(prog='koreader-d00901', description='Install KOReader on a Kindle K3.')
|
|
154
|
+
add_common_args(parser)
|
|
155
|
+
parser.add_argument('--dry-run', action='store_true', help='print the plan only')
|
|
156
|
+
args = parser.parse_args(argv)
|
|
157
|
+
log_file = args.log_file or args.cache / 'install.log'
|
|
158
|
+
setup_logging(log_file, verbose=args.verbose)
|
|
159
|
+
log.debug('koreader-d00901 %s, args=%s', __version__, vars(args))
|
|
160
|
+
try:
|
|
161
|
+
return run(args)
|
|
162
|
+
except InstallerError as exc:
|
|
163
|
+
log.debug('install failed', exc_info=True)
|
|
164
|
+
print(f'error: {exc}\n(details in {log_file})', file=sys.stderr)
|
|
165
|
+
return 1
|
|
166
|
+
except KeyboardInterrupt:
|
|
167
|
+
print('\nInterrupted.')
|
|
168
|
+
return 130
|