开学季!破解 Kindle 的冒险之旅

2025年4月21日

那是个星期一,我的女儿又上学迟到了。更糟糕的是,校车还早到了2分钟。在匆忙地确保她及时赶上校车后,我坐下来喝早咖啡,打开了 Hacker News。首页有一个有趣的条目:“所有 Kindle 现在都可以被越狱”。凑巧的是,我的旧 Kindle 就放在我旁边。这是来自宇宙的信号吗?只有一种方法可以知道,于是我开始了将我的 Kindle 变成一个上学准备仪表板的旅程。

Level 1:Kindle 越狱 ⛓️‍💥

Kindle 越狱是一个相对简单的过程。kindlemodding.org 的朋友们创建了一个用户友好的指南,其中包含简单的分步说明。该指南非常全面且用户友好,因此我在这里重复这些步骤将是徒劳的。KindModding Discord 社区也是一个询问故障排除问题的好地方。

简而言之,我越狱 Kindle 的主要步骤是:

  1. 确定你的 Kindle 型号。
  2. 安装 WinterBreak。(只需将 zip 文件放在 Kindle 中并重新启动。)
  3. 设置一个热修复,即确保越狱在更新后仍然存在。
  4. 安装 Kindle Unified App Launcher 和一个包安装程序 (MRPI)。
  5. 使用 USBNet 包在 Kindle 上启用 SSH。
网络恶作剧 ☎️

我在为 Kindle 设置 SSH 访问时遇到了第一个障碍。标准的 USBNet 包不适用于我的 Kindle,经过一番研究,我找到了 USBNetLite,它使 SSH 正常工作。

USBNetlite 包支持使用 2 种模式连接到 Kindle:

  1. RNDIS/Ethernet gadget。这显示为 usbx(我的情况是 usb0)。
  2. 通过路由器分配的 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,我必须执行以下步骤:

  1. 从 KUAL 菜单启用 USBNet。
  2. 将 IP 地址手动设置为 192.168.15.201192.168.15.xxx 子网中的任何地址。
  3. 使用 ssh 192.168.15.244(usb0 ethernet interface)或路由器分配的 wlan0 地址进行 SSH 连接。
  4. 输入 /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 脚本以满足我的需求。与旧的未维护的设备一样,xhwget 都无法在我的 Kindle 的后台模式下工作。经过一些调试,我最终切换回普通的 curl 来从后端 API 获取 PNG。

该脚本基于 cron 计划循环运行:

  1. 如果 Kindle UI 框架和屏幕保护程序正在运行,则停止它们。
  2. 检查互联网是否可访问。
  3. 从服务器获取 PNG 图像
  4. 使用 eips 命令在 Kindle 上全屏显示它。

关于图像分辨率的说明:Kindle 需要 8 位灰度图像,否则将无法正确显示。你还需要使用 eips -i 命令找出 Kindle 的显示分辨率。(我的是 800x600)

在转向后端之前,我使用了一个虚拟图像来测试 Kindle 客户端。

Level 3:云端上的 API 服务器 🌤️

我设计了一个后端 API,可以实时收集数据并将其导出为 PNG 图像。为此,我想要一个始终处于运行状态、支持现代 Web 标准并具有令人愉悦的开发人员体验的平台。Cloudflare Developer Platform 满足所有条件,并且入门非常容易。

我为我的后端使用了以下工具/框架。

  1. Cloudflare Workers:处理传入请求和图像处理的主力。
  2. Hono JS:一个快速、轻量级的 JS 框架。
  3. Cloudflare KV:将网络请求缓存一小时
  4. Bun 和 TypeScript:我喜欢 Zig,并且 Bun 使用起来非常愉快。

Architecture

天气

我是 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。 Architecture 作为最终缺失的部分,我添加了一个自定义 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 返回兼容的图像,我需要:

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 图像,并在其全屏显示。🌟

Dash Image

最后的想法

破解 Kindle 并使其适用于仪表板非常有趣!Cloudflare Developer Platform 具有一些有趣的下一代范例,例如 Durable Objects、D1 和 KV。开发体验是无缝的,当我尝试做一些与众不同的事情时,它并没有让我感到任何压力。通过其背后的基础开源技术,部署非常简单。

该设备已经平稳运行了一个月,我每 2 周只需充电一次。我计划稍微更新一下 UX,这将是一个与女儿一起进行的有趣练习。她的朋友们也很喜欢这个想法,我计划与他们一起举办一个研讨会。毕竟,旧的未使用过的 Kindle 可以在 eBay 上以 20 欧元的价格找到,并且是一个很棒的黑客设备。

祝愿未来有更多这样的黑客冒险!🏴‍☠️

代码可在我的 Github 个人资料中找到:Kindle Dash Client School dashboard backend

  1. https://github.com/NiLuJe/FBInk ↩︎
  2. 是的,即使在 2025 年。有些技术很难消亡:https://wiki.mobileread.com/wiki/Kindlet_Index ↩︎
  3. 这受到 PascalW 的 kindle-dash 项目以及 Hemanth 后来添加的项目的启发 ↩︎
  4. 好吧,Claude 在这种情况下也提供了一些帮助 🤖 ↩︎
  5. https://en.wikipedia.org/wiki/Rec._709 ↩︎