开学季:破解旧 Kindle 的冒险之旅
开学季!破解 Kindle 的冒险之旅
2025年4月21日
那是个星期一,我的女儿又上学迟到了。更糟糕的是,校车还早到了2分钟。在匆忙地确保她及时赶上校车后,我坐下来喝早咖啡,打开了 Hacker News。首页有一个有趣的条目:“所有 Kindle 现在都可以被越狱”。凑巧的是,我的旧 Kindle 就放在我旁边。这是来自宇宙的信号吗?只有一种方法可以知道,于是我开始了将我的 Kindle 变成一个上学准备仪表板的旅程。
Level 1:Kindle 越狱 ⛓️💥
Kindle 越狱是一个相对简单的过程。kindlemodding.org 的朋友们创建了一个用户友好的指南,其中包含简单的分步说明。该指南非常全面且用户友好,因此我在这里重复这些步骤将是徒劳的。KindModding Discord 社区也是一个询问故障排除问题的好地方。
简而言之,我越狱 Kindle 的主要步骤是:
- 确定你的 Kindle 型号。
- 安装 WinterBreak。(只需将 zip 文件放在 Kindle 中并重新启动。)
- 设置一个热修复,即确保越狱在更新后仍然存在。
- 安装 Kindle Unified App Launcher 和一个包安装程序 (MRPI)。
- 使用 USBNet 包在 Kindle 上启用 SSH。
网络恶作剧 ☎️
我在为 Kindle 设置 SSH 访问时遇到了第一个障碍。标准的 USBNet 包不适用于我的 Kindle,经过一番研究,我找到了 USBNetLite,它使 SSH 正常工作。
USBNetlite 包支持使用 2 种模式连接到 Kindle:
- RNDIS/Ethernet gadget。这显示为
usbx
(我的情况是usb0
)。 - 通过路由器分配的 IP 地址使用 SSH over network。这显示为
wlanx
(我的情况是wlan0
)。
[root@kindle us]# ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
...
usb0 Link encap:Ethernet HWaddr EE:19:00:00:00:00
inet addr:192.168.15.244 Bcast:192.168.15.255 Mask:255.255.255.0
...
wlan0 Link encap:Ethernet HWaddr 08:84:9D:7B:9C:F1
inet addr:192.168.2.71 Bcast:192.168.2.255 Mask:255.255.255.0
...
要启用 SSH network,我必须执行以下步骤:
- 从 KUAL 菜单启用 USBNet。
- 将 IP 地址手动设置为
192.168.15.201
或192.168.15.xxx
子网中的任何地址。 - 使用
ssh 192.168.15.244
(usb0 ethernet interface)或路由器分配的wlan0
地址进行 SSH 连接。 - 输入 /etc/config 文件中的密码。
这样我就获得了 Kindle 的 root 访问权限 🥷
[root@kindle us]# uname -a
Linux kindle 4.1.15-lab126 #1 SMP PREEMPT Thu Nov 28 11:37:06 UTC 2024 armv7l GNU/Linux
Level 2:在 Kindle 上召唤一个仪表板 ✨
Kindle OS 是 Linux 的精简版本,运行的是一个古老的内核和旧的 Gnu 实用程序,但没有包管理器。Kindle 有几个自定义工具,可以启用 Kindle UI 操作,例如名为 framework
的自定义 UI 框架及其助手 lab126_gui
。它的主力是 lipc
,一个 dbus
风格的程序,用于发出命令来驱动设备操作。
有几种方法可以为破解的 Kindle 开发应用程序。一些应用程序(如 KoReader)使用 FBInk 框架1,而另一些应用程序使用 Java applets2。我选择了简单的路径:禁用 framework,定期以全屏模式显示 PNG 图像3。
我修改了 OG 存储库中的主 dash.sh
脚本以满足我的需求。与旧的未维护的设备一样,xh
和 wget
都无法在我的 Kindle 的后台模式下工作。经过一些调试,我最终切换回普通的 curl
来从后端 API 获取 PNG。
该脚本基于 cron 计划循环运行:
- 如果 Kindle UI 框架和屏幕保护程序正在运行,则停止它们。
- 检查互联网是否可访问。
- 从服务器获取 PNG 图像
- 使用
eips
命令在 Kindle 上全屏显示它。
关于图像分辨率的说明:Kindle 需要 8 位灰度图像,否则将无法正确显示。你还需要使用
eips -i
命令找出 Kindle 的显示分辨率。(我的是 800x600)
在转向后端之前,我使用了一个虚拟图像来测试 Kindle 客户端。
Level 3:云端上的 API 服务器 🌤️
我设计了一个后端 API,可以实时收集数据并将其导出为 PNG 图像。为此,我想要一个始终处于运行状态、支持现代 Web 标准并具有令人愉悦的开发人员体验的平台。Cloudflare Developer Platform 满足所有条件,并且入门非常容易。
我为我的后端使用了以下工具/框架。
- Cloudflare Workers:处理传入请求和图像处理的主力。
- Hono JS:一个快速、轻量级的 JS 框架。
- Cloudflare KV:将网络请求缓存一小时
- Bun 和 TypeScript:我喜欢 Zig,并且 Bun 使用起来非常愉快。
天气
我是 Merry Sky 和为其提供支持的实时天气 API Pirate Weather API 的忠实粉丝。获得 API 密钥后,只需几个小时就可以微调我需要获取当前天气数据的数据。我喜欢 Cloudflare secrets 使存储可以在代码中轻松使用的 secrets 变得非常容易。
公共交通
我住在柏林,这里拥有强大的公共交通网络,因此获取可靠的公共交通数据很容易。我使用了 VBB Berlin 的 公共 API 来获取有关隔壁公交车站的实时更新。有趣的是,当我处理这部分时,发生了罢工,这帮助我为这个奇怪的 edge case 编写了代码。
// 🚧 有时 BVG 会罢工,因此 API 返回空的 departures[] 🚧
if (Array.isArray(departuresData.departures) && departuresData.departures.length === 0) {
console.log("No departures available.");
return null;
}
学校时间表
由于课程之间的穿插休息,设计学校时间表很有趣。我和女儿坐下来,我们很开心地设计了它背后的数据结构。事实证明我过度设计了它,她通过将其设置为值的数组来简化了它。她很开心地填写了数据,包括周末。由于它每年更改一次,因此我硬编码了数据,但我喜欢将来在 KV 或 D1 中共享它的可能性。
将所有内容整合在一起
有了所有数据后,我和女儿一起设计了仪表板 UI。
作为最终缺失的部分,我添加了一个自定义 HTTP Header
X-Battery-Level
,它将由 Kindle 客户端发送。
app.get("/api/internal/dashboard", async (c) => {
const weatherData = await getWeatherData(c);
const departuresData = await getRouteData(c);
const timeTable = await getTimetable(c);
const battery_level = c.req.header("X-Battery-Level") ?? "-99";
if (weatherData === null || departuresData === null || timeTable === null) {
return new Response("Could not create dash HTML page", { status: 500 });
}
let data: DashboardData = {
weatherData: weatherData,
departuresData: departuresData,
timeTable: timeTable,
batteryLevel: battery_level,
};
const renderedHtml = renderHtml(data);
return c.html(renderedHtml);
});
图像处理魔法 🪄
有了 HTML 仪表板,挑战就进入了下一个级别。要从 API 返回兼容的图像,我需要:
- 在 Worker 中渲染网页
- 截取屏幕截图
- 将图像从
sRGB 16 Color
转换为8-bit Gray scale
。
Cloudflare Workers 是一个沙盒环境,它对诸如 sharp 之类的工具的访问受到限制。但是,Cloudflare 和开发人员社区以 Cloudflare Browser rendering, Puppeteer, 和 cf-wasm 图像处理工具的形式提供了可行的替代方案。在试验了这些工具之后,我制定了将渲染的 PNG 以所需格式返回给 Kindle 客户端的计划。
第一步是使用 Cloudflare 的 Puppeteer 库截取屏幕截图。
// Use the same host as the current request
const host = new URL(c.req.url).origin;
let dashboardUrl = `${host}/api/internal/dashboard`;
dashboardUrl = new URL(dashboardUrl).toString();
const browser = await puppeteer.launch(c.env.MYBROWSER);
const page = await browser.newPage();
const battery_level = c.req.header("X-Battery-Level") ?? "-99";
page.setExtraHTTPHeaders({ "X-Battery-Level": battery_level });
await page.setViewport({
width: DASHBOARD_WIDTH,
height: DASHBOARD_HEIGHT,
});
await page.goto(dashboardUrl);
const img = await page.screenshot({
type: "png",
clip: {
x: 0,
y: 0,
width: DASHBOARD_WIDTH,
height: DASHBOARD_HEIGHT,
},
});
await browser.close();
有了干净的屏幕截图,下一步就是应用图像转换。自从我涉足图像处理算法以来已经有好几年了,但是在阅读了一些手册4-5之后,我发现我需要使用 cf-wasm 库的 EncodeOptions
将数据编码为正确的格式。
try {
const imageData = new Uint8Array(img);
const decodedImage = decode(imageData);
const { width, height, image: rawImage } = decodedImage;
// grayscaleData stores image data, 1 channel per pixel
const grayscaleData = new Uint8Array(width * height);
// We need to convert the image now to grayscale
for (let i = 0; i < rawImage.length; i += 4) {
const r = rawImage[i];
const g = rawImage[i + 1];
const b = rawImage[i + 2];
// We need to use Luminosity method for grayscale conversion
// This is a weighted average of the RGB color channels to make the images legible to human eyes
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
// The originial 4-channel data is from the 4-channel input: (width * height * 4)
// We need to store only the grayscale value from the single channel corresponding to width * height
grayscaleData[i / 4] == gray;
}
// We now encode the image back to PNG format without alpha and 8-bit depth
const outputPng = encode(grayscaleData, width, height, {
color: ColorType.Grayscale,
depth: BitDepth.Eight,
stripAlpha: true,
});
// Return grayscale PNG
return outputPng;
} catch (error) {
console.error("Grayscale conversion error:", error);
return null;
}
这将返回 Kindle 支持的格式的 PNG 图像,并在其全屏显示。🌟
最后的想法
破解 Kindle 并使其适用于仪表板非常有趣!Cloudflare Developer Platform 具有一些有趣的下一代范例,例如 Durable Objects、D1 和 KV。开发体验是无缝的,当我尝试做一些与众不同的事情时,它并没有让我感到任何压力。通过其背后的基础开源技术,部署非常简单。
该设备已经平稳运行了一个月,我每 2 周只需充电一次。我计划稍微更新一下 UX,这将是一个与女儿一起进行的有趣练习。她的朋友们也很喜欢这个想法,我计划与他们一起举办一个研讨会。毕竟,旧的未使用过的 Kindle 可以在 eBay 上以 20 欧元的价格找到,并且是一个很棒的黑客设备。
祝愿未来有更多这样的黑客冒险!🏴☠️
代码可在我的 Github 个人资料中找到:Kindle Dash Client School dashboard backend
- https://github.com/NiLuJe/FBInk ↩︎
- 是的,即使在 2025 年。有些技术很难消亡:https://wiki.mobileread.com/wiki/Kindlet_Index ↩︎
- 这受到 PascalW 的 kindle-dash 项目以及 Hemanth 后来添加的项目的启发 ↩︎
- 好吧,Claude 在这种情况下也提供了一些帮助 🤖 ↩︎
- https://en.wikipedia.org/wiki/Rec._709 ↩︎