Kindle Paperwhite 2を、自分の新聞端末に作り替えた
脱獄したKindle Paperwhite 2の純正UIをCで置き換え、LANサーバーで作ったテキスト版を端末側で組む新聞リーダーを作りました。Javaフレームワークを止めたまま電源ボタンを生かすために、powerdを逆アセンブルした話も書きます。

手元のKindle Paperwhite 2を、純正UIを捨てて自分のホーム画面が開く端末に作り替えました。紙面はNewspaperをタップすると、最後に取得した版が開きます。6インチのE-Ink、758×1024、16階調。本を読むには小さく、けれど「紙」としては、いま手元にある画面の中でいちばん新聞に近い。
khome は、純正UIをCの小さなランチャーに置き換えるプロジェクトです。紙面はLAN内のサーバーが作り、端末はそれを検証してキャッシュし、文字と罫線は端末側で組みます。コードはMITで公開しています。
何が変わるか
| これまで | khome | |
|---|---|---|
| 起動後 | 純正のLibrary | khomeのホーム(時計・Apps・Favorites) |
| ニュース | ブラウザで各サイトを巡回 | 端末に落ちた版を読む |
| オフライン | 読めない | 最後に取得した版が読める |
| 純正に戻る | 常にそこにいる | /mnt/us/khome/disable を作って再起動 |
ホーム画面には時計、日付、通算日(NO. 296 / 365)、Apps、Favoritesが並びます。表示名はまだ仮のFOLIO、紙面の題字はKindle Timesです。

なぜ自分で描くのか
E-Inkで新聞を読みたいだけなら、既存のビューアで足りそうに見えます。ところが2段組の紙面となると、選択肢がありません。
- 純正のKF8/AZW3は
column-countを無視する。2段組はScribeのKFX(横向き)くらい - KOReader(crengine)にも段組みのモデルがなく、EPUBの
column-countは効かない - 2段組を扱うビューアはSupernoteやAndroid向けで、少なくともこのKindleでは動かない
つまり紙面のレイアウトは自分で持つしかありませんでした。ならばページ画像を配るのではなく、テキストを端末に送って、端末側で組む。題字も罫線も見出しもページ番号も、Cで描いています。
どう動くか
サーバー側は、公開フィードを取得し、記事の本文を抽出し、チェックサム付きのtext-v1バンドルを作ります。RSSは目次であって本文ではないので、本文はHTMLから取り出します。
index.tsv— 記事1行ずつ(ID、ジャンル、配信元、公開時刻、文字数、見出し、リード)articles/<id>.txt— 本文manifest.json/SHA256SUMS/latest-text.json— 同一性とハッシュ
端末側はfetch.shでlatest-text.jsonを取り、SHA256SUMSとmanifest.jsonを検証します。バンドル全体が正しいときだけcurrentをpreviousに回すので、取得に失敗してもキャッシュ済みの版は無傷です。khomeは通信より先にキャッシュを描くため、最後に取った版はオフラインでも読めます。
今日、実機でNewspaperを開いたときは、その場で取得が走って20261026-1802 / 30 stories / 7 pages、終了コード0で版が入れ替わりました。端末側にタイマーはなく、取得はNewspaperを開いたときか、KUALのRefreshを実行したときです。版の生成はサーバーのcronが07:00と18:00に回します。

山場1: 純正UIを止めると、電源ボタンが死ぬ
khomeはXと周辺の純正サービスを生かしたまま、Javaフレームワーク(cvm)を止めて動きます。cvmは1分ごとにステータスバーの時計を描き続けるので、画面をkhomeが持っている間は止める必要がありました。
ところがcvmを止めると、電源ボタンの入力が捨てられます。スリープに入れない。ここが最初の壁でした。
powerdを逆アセンブルすると、判定は1箇所に集約されていました。com.lab126.kafのframeworkStartedが取れて非0であること、そしてcom.lab126.blanketのloadにsplashが含まれないこと。どちらかが失敗すると、powerdは「スプラッシュ中」と判断して電源ボタンを捨てます。cvmが無いということは、まさにこの状態です。
そこでcom.lab126.kafを名乗り、frameworkStarted=1を返す小さな代役を書きました(fakekaf)。必要に応じてblanketのsplashを外し、FrameworkStartedイベントを送ります。実機では、cvmを止めたままfakekafを常駐させて電源ボタンを3回往復させ、3回ともスリープと復帰が効きました。深いサスペンドからの復帰だけは未確認です。
逆アセンブルのアドレスや根拠の命令は、docs/spikes/fakekaf.mdに全部残しています。アドレスと条件は実機のバイナリから読みました。設定値の一部は推定で、そこは資料に明記しています。
山場2: cvm無しでブラウザを全画面にする
同じことがブラウザにも起きます。cvmが無いとcom.lab126.chromebarも存在せず、pillowのtitleBar(41px)とsearchBar(75px)のウィンドウが作られません。結果としてブラウザは758×1024+0+0で開きます(このときawesomeのtopOffsetが0になっている、と推定されています)。config.xmlは触っていません。
もうひとつは起動スプラッシュです。blanket unload splashで外すだけで、白い画面の上にブラウザがそのまま出ます。
向きの切り替えはもっと厄介でした。winmgrのorientationはこのファームウェアでは読み取り専用で、orientationLockを書いても_O:の付かない窓は回りません。ブラウザの向きはブラウザ自身のlocalStorage、つまりSQLiteのItemTableにあるsettingsが持っています。
なので、ホームのトグルから次の順で書き換えます。
- ブラウザが動いていれば止める(WebKitが設定をメモリに持つため)
ItemTableのsettingsを読み、hex()で現行値を控えるjson_setで$.orientationだけをlandscapeLeftかautoに差し替える- 読み戻して意図と違えば、控えたhexをそのまま書き戻す
横向きの間はシートを出さず、画面の端からのプルで直接ホームに戻る。縦に戻ったことはフレームバッファの寸法で確認してから描く。この辺りは、失敗した試験と戻し方までdocs/adr/0001-...に残っています。
紙面をCで組む
描画は758×1024の8ビットキャンバスです。文字はstb_truetype、罫線は矩形と線、アイコンは事前に作ったPNG。描いたら/dev/fb0に転送し、FBInkにGC16かGL16のリフレッシュを頼みます。
16階調しかないので、グレーは2段まで。影、ぼかし、グラデーション、半透明は使いません。毎分変わる時計だけは、1つの矩形を部分リフレッシュします。E-Inkは全画面更新で閃くので、動くものを小さく閉じ込めるのがコツでした。
デザインは先にHTMLで3案作り、Cで再現できる範囲まで削ってから実装しました。その代わり、テストで描画を縛っています。
- デザインモックとの差が1.2%以内(最大の差分塊が1,200px以下)
- 実機ゴールデンとはピクセル一致
- 出力は8ビット、16階調(0, 17, …, 255)のみ
意図しない描画変更はここで検出されます。

数字
| 部分 | 行数・枚数 |
|---|---|
| 端末の描画・入力(C、ヘッダ込み) | 15,076行 |
| 端末のテスト(C / Python / shell) | 11,439行 |
scripts/ 全体(抽出・配信・開発ツール) |
3,976行 |
| リポジトリ内のPNG(ゴールデン・モック・アイコン) | 78枚 |
端末側にPythonは入れていません。取得も抽出もサーバーの仕事で、端末は描いて、タップを読んで、lipcで電源とWi-Fiを扱うだけ。この分担にしたことで、端末側にPythonもネットワーク処理も置かずに済んでいます。
まとめ
6インチのE-Inkは、通知も動画も要らない「読むためだけの画面」です。新聞にはちょうどいい。けれど純正UIを置き換えると、電源ボタンやブラウザのように、思わぬところでつまずきます。そのひとつひとつを実機で確認して潰したのが、このプロジェクトの本体でした。
いまはスリープから起こすと、khomeのホームがすぐ出ます。Newspaperをタップして紙面をめくると、見出しから本文が開く。それだけの端末ですが、毎朝の最初の数分は、たしかに気持ちがいい。
なお、この記事のスクリーンショットはテスト用の架空の記事(台風の号)を描画したものです。実機に入っている版は実際のニュースなので、そのままは載せていません。Wi-Fi名もHOME-5Gという架空の表記に差し替えてあります。
脱獄とシステムUIの置き換えは、端末を文鎮化させる可能性があります。復旧できる見込みのない端末では試さないでください。USBかSSHで/mnt/us/khome/disableを置いて再起動すれば、純正のLibraryに戻せます。