From acef815517b7cb4f82c49748d84032bb256f7a6d Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 13:59:13 +0900 Subject: [PATCH 01/59] =?UTF-8?q?README:=201.6.5=20=E8=B5=B7=E7=82=B9?= =?UTF-8?q?=E3=81=AE=E4=BD=9C=E3=82=8A=E7=9B=B4=E3=81=97=EF=BC=88vertical-?= =?UTF-8?q?1.6.5=EF=BC=89=E3=81=AE=E6=AE=B5=E9=9A=8E=E8=A1=A8=E3=81=A8?= =?UTF-8?q?=E6=AE=B5=E9=9A=8E1=E3=81=AE=E7=B5=90=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧ツリーの README を引き継ぎ、作り直しの方針・段階表・段階1の実機の観察を追加。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- README.md | 1513 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 1318 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index f6f68770dfc..8610a472065 100644 --- a/README.md +++ b/README.md @@ -1,297 +1,1420 @@ -# CrossPoint Reader +# CrossPoint Reader OST 改造版 + +XTEINK X3上で日本語EPUBファイルを読むために、CrossPoint Reader を改造しています。 +命名は面倒くさくなったのでOSakanaTaro版ってことでOST版です。 + +### 2026/09/23時点の開発方針 +- XTEINK X3で動けばいい + - aliexpressで買ったUSB接続ができないバージョンを使用 +- Claude Codeで開発する + - コンパイルなどはUbuntu 24.04仮想マシン上で実施 +- 本家をベースにする + - X3の最近のバージョンは液晶パネルが違う件への対応などが本家には入っているため + - 2026/08/09から1.5.0ベースで開発開始。本家にXteink x4 pro対応が入ったあたりで乖離が始まった + - 2026/08/20から1.6.0rcをベースにして再構成開始 + - 2026/09/06から1.6.0リリース版をベースに再構成 + - 2026/09/28から1.6.5リリース版をベースに再構成(ブランチ `vertical-1.6.5`) +- 日本語訳は付けない +- フォントはSDカード上に配置する + - 容量食うのでSDカード上に置く +- ページめくりボタンは左が進むで、右が戻る + - 縦書きフォーマット(RTL)のEPUBを開いた時だけその動作をする +- 青空文庫ダウンロードなどの機能は付けない +- 時計表示機能を追加 + - 磁石で貼り付けることができるので作業時の時計表示に使えるな、と実装 + - [Settings]-[Reader]-[Customize Status Bar]の[Clock UTC Offset]を「UTC+9:00」とすることで日本時間表示に +- テスト用EPUBファイルにて動作を確認 + - 一部、特殊なフォントを必要とする文字以外について表示できることが期待される +- ルビ表示について対応 + - 横書きであればルビに対応してるなら縦書きでも対応しておくかな、って +- 電書連 EPUB 3 制作ガイド ver.1.1.4に準拠したEPUB表示についての対策 + - ~~CSSが巨大すぎてメモリクラッシュを起こす件についての対応をやる必要があるのかという問題~~ + - ~~[Setings]-[Reader]-[Text Settings]にある[Style]にて「Embedded Style」を「OFF」にするとCSSを使わないため回避できる~~ + - こんな小さな端末で著者がやりたいという複雑な表現をやって期待通りの表示になるのか?と思いつつ対応 +- 本家との付き合い方(2026/09/02決定) + - UIの大きな変更は個別に追随しない。本家のタグが更新された時点でforkしなおし再実装する + - 基本的に縦書き・挿絵・フォントに関わる本家修正だけ取り込む +- フォントの明朝/ゴシック混植は実施しない + - 1冊のなかでフォント指定を途中から変える機能について + - 商業EPUBの91%が指定を持つが1冊あたり中央値40箇所で、2書体常駐のメモリ代償に見合わない +- 組版やレイアウトについての出典として下記を採用している + - EPUB 3.3 仕様書 https://www.w3.org/TR/epub/ + - EPUB 3.4 テスト仕様 https://w3c.github.io/epub-tests/index.html + - 日本語組版処理の要件(JLREQ) https://www.w3.org/TR/jlreq/ + - 特に JLREQ 3.3.2 ルビの付け方 + - Unicode Vertical Text Layout(UAX #50) https://www.unicode.org/reports/tr50/ + - Unicode Line Breaking Algorithm https://www.unicode.org/reports/tr14/ + - 電書協 EPUB 3 制作ガイド https://dpfj.or.jp/counsel/guide + - KADOKAWA-EPUB 制作仕様 および KADOKAWA-EPUB 再現指標 https://kadokawa-epub.bookwalker.co.jp/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89 +- 開発に際して下記URLをClaude codeに読み込ませている(実装内容まとめで使う略語) + - 本家 https://github.com/crosspoint-reader/crosspoint-reader + - JP版 https://github.com/zrn-ns/crosspoint-jp + - CJK版 https://github.com/CrossPoint-CJK/crosspoint-reader-cjk (旧URL https://github.com/aBER0724/crosspoint-reader-cjk ) + - Yomuka版 https://github.com/ponto1216-ai/crosspoint-jp + - 抹茶版 https://github.com/eszter007/matcha-reader + - freeink-sdk https://github.com/Free-Ink/freeink-sdk + - 以下は日本語縦書きには直接関係しない実装で、UIなどの機能面の参考として読む(2026/09/09 追加。同日、3つとも縦書き・ルビ・禁則・text-orientation などの実装が無いことをソースと履歴で確認済み) + - Papyrix版 https://github.com/bigbag/papyrix-reader + - Inx版 https://github.com/obijuankenobiii/inx + - witchhunt版 https://github.com/jpirnay/witchhunt-reader + - crossmux版 https://github.com/0x1abin/crossmux + +各フォークが本家のどのリリースまで取り込んでいるか(2026/09/22時点、GitHubの履歴を照合)。 + +| 版 | 取り込み済みの最新 | 本家との世代差 | 使用しているfreeink-sdk | +|---|---|---|---| +| 本家 | 1.6.5rc(2026/09/14 先行版。developの先端はさらに31件先、うち #2332 画像まわりの断片化対策、#3600 一覧の行を必要な分だけ作る、#3618 組み込むUI言語を選べる、#3528 語間・字間の設定、#3608 転送後に Library の索引を作り直す) | ― | ffcbb1c | +| **OST版 新ツリー** `feat/vertical-1.6.0based` | 1.6.0(2026/09/05) | 1.6.5rc が未取り込み(1.6.0から31件、develop先端までさらに31件)。#2925(段階表示JPEGの分離走査)と #3581(合字表の宙づり参照)は 09/17 に個別に取り込み済み。残りで当版に効きそうなのは断片化対処の連作 #3518/#3521/#3527(章レイアウト前のフォントキャッシュ解放)・#3398(フォント目録を1領域に)・#2332(画像まわり)と、#3439(X3のアンチエイリアス安定化)。いずれも 1.6.5 起点の作り直しで本家のまま採る | fde240f | +| 抹茶版 | 1.6.0(2026/09/20、安定版。本家 develop 09/20 時点 `8c84ef32` を含む) | 1.5.0 以来の安定版。X4 Pro・X4C・Sticky 対応、辞書引き(長押し・ボタン選択・縦書き対応・ページ跨ぎの語)、漫画の吹き出しの語の引き当て(変換のやり直しが必要)。独自分は辞書引き(漫画の吹き出しの語の引き当て、触れる操作)が中心。#282 は日本語の添え書体の区間表を Regular/Bold で共有して読み込みの山を半分にし、読み込み前に字形の保存を捨てる(20pt NotoSansJP で 26.6KB×2 が入らなかった件。当版の書体は区間が密で1書体3KBのため無縁) | 4b17a7bb | +| CJK版 | **本家タグを1つも含まない**(0.4.2、2026/09/05) | 本家と共通の祖先を持たない再構成履歴。以後の同期なし | 自前fork 62976c5 | +| JP版 | 1.2.0(2026/04/03) | 1.3.0 以降すべて未取り込み。独自開発は活発(v0.3.2=2026/09/19: #149 眠り画面の動的壁紙、#150 ホームの読書進捗率と Vega テーマ。09/08以降: **禁則処理の行分割 #144**、字形の鮮明さ設定 #145、UI書体のディセンダ #146、整形済みブロックの行間 #147、フォルダを開くたびの全走査をやめる #148/#136、横向きUI #138) | e38baecc | +| Yomuka版 | 1.2.0(2026/04/03) | JP版からの派生のため同じ。独自開発は活発(yomuka-v0.7.6.0=2026/09/17: SD書体の作業領域をページ間で使い回す(本家の「収まれば保持」方式の移植で当版は既に同等)、ルビ・上付きなどの小さい字に 10pt 専用の書体データを使う、縦中横の桁数を2/3桁で選べる、曲線引用符 ‘’“” を縦の升に正立で置く(`3f631371`/BIZUD向け補正 `9dcfc3dc`)、全冊の事前生成中のSD書き込み失敗を安全に止める) | 30011399 | +| crossmux版 | 1.6.0(2026/09/05) | 本家 1.6.0 起点(1.6.5rc は未取り込み)で独自 618 件。中国語(簡体字)対応の community fork。Apps(ゲーム・道具)、微信読書、AirPage、読書統計、Bluetooth ページめくり、33 言語を1 firmware に、C3 の読書メモリの上限管理。SD 書体は本家と同じ cpfont v4。**縦書き・禁則なし**(2026/09/22 に確認対象へ追加) | 自前fork 094976e | + +各版の最新リリース(先行版を含む。安定版だけを見るとJP版・Yomuka版の日付つき開発版や、 +Inx版・witchhunt版・抹茶版の先行版を取り落とす): + +| 版 | 最新 | 日付 | +|---|---|---| +| 本家 | 1.6.5rc(先行版) | 2026/09/14 | +| Yomuka版 | yomuka-v0.7.6.0 | 2026/09/17 | +| JP版 | v0.3.2 | 2026/09/19 | +| 抹茶版 | 1.6.0 | 2026/09/20 | +| Papyrix版 | v1.31.0 | 2026/09/18 | +| Inx版 | 1.0.20-BETA(先行版) | 2026/09/11 | +| witchhunt版 | 2.31(2.31.0-rc.1 も同日) | 2026/09/20 | +| **OST版** | 20260918-2(先行版) | 2026/09/18 | +| crossmux版 | 1.6.0(stable/nightly タグは動く先行版) | 2026/09/20 | +| CJK版 | 0.4.2 | 2026/09/05 | + +CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネル向けの修正を独自に入れている。 +他は本家のfreeink-sdkをそのまま参照している。 + +# 最近の更新内容について(2026/09/28) + +- 本家 1.6.5(2026/09/27 リリース)を起点に作り直しを開始(2026/09/28〜、ブランチ `vertical-1.6.5`) + - 段階0(素の 1.6.5 のビルド)と段階1(診断)まで完了。詳細は下の「1.6.5ベースへの作り直し」節 + - この時点のブランチには日本語フォントの扱い・縦書き・挿絵などはまだ入っていない(以下の各項目は 1.6.0 ベースの `feat/vertical-1.6.0based` の内容) +- 章の境目の待ちを減らすため、読書中の待機時間に次の章を先に組み立てる(2026/09/09〜) + - 商業書籍で章をまたぐたびに7秒前後待つのを、ページを表示して1.5秒以上操作が無い間に少しずつ済ませる + - ボタンの読み取りを止めないよう1回の作業を25ms以内に区切り、字形の一時記憶を汚さないよう配置計算の前後で空にする +- 縦書きの本文中の1文字大の画像(外字)は字として列に流す(2026/09/10〜) + - 幅が列の間隔以内・高さが列の半分以内の画像は前後の字と同じ列の同じ位置に入り、ルビも付く。それより大きいものは従来どおり独立した列 +- 描画中のボタン操作は別の系で読み取って保持し、階調の描き分けは押されたら打ち切る(2026/09/10〜) + - 効かない時間を「反応が遅れる時間」に変える。待ち表示(Indexing)は 1〜3 秒の章の組み立てでも出す +- 章内リンクの飛び先は、その要素の行が実際に載ったページを指す(2026/09/10〜) + - 段落の1行目が次ページへ送られる場合に1つ手前を指していた。章の保存の版を50に上げたので初回オープンで全書籍が作り直しになる +- 語数の上限で分割された長い段落の続きに、一字下げを再び入れない(2026/09/10〜) +- 配置に使うメモリが足りないときは落とさず、その章の組み立てを失敗させて作り直しに回す(2026/09/10〜) +- 字形のその場読みの置き場を8→48枠に広げる(2026/09/10〜) + - 階調は短冊ごとに同じページを描き直すため、8枠では二十数字が毎回押し出し合い、描き直すたびにSDから読み直していた。遅いページの表示が15.3秒→4.9秒 +- 字形の先読みの取り分を、一時記憶を保持する床と同じ値にする(2026/09/10〜) + - 別々の数字だったため、空きを使い切って作った一時記憶をその場で捨てていた。字の大きいフォントに変えると露呈し、1ページ7.8秒+中断(abort)になっていた +- 1行・1列を作る前に、必要な大きさの領域を試しに取って確かめる(2026/09/10〜) + - 取れなければそのページを諦めて報告する。従来は確保に失敗すると firmware ごと停止していた +- 縦書きの列送りを、フォント内部の行送りではなく全角の幅×1.5から求める(2026/09/10〜、章の版51) + - 書体を変えると同じ設定でも1ページの文字数が変わっていた(BIZ 11列/Noto 7列)。全角を実測して揃えた +- 挿絵のログは短冊の判定より後に出す/小さい外字もRAMに載せる(2026/09/11〜) + - 前者は1ページ12行のログで診断の記録を潰していた。後者は297バイトの外字が24KBの門番に断られていた +- 本家 develop を併合(2026/09/11〜、章の版53) + - SDK の `ed39106`(X3の余計な電源投入を止める)でページ送りが 3,773ms → 1,601ms。#3439 でX3の階調を安定化 +- 縦書きの一番右の列のルビが画面端で切れないようにする(2026/09/11〜) + - ルビは列の右隣に描かれるため右端の列だけ領域外に出ていた。画面余白で足りない分だけ確保する +- 挿絵の初回展開の内訳を測り、SdFat の SPI をまとめ転送にする(2026/09/11〜) + - 17.7秒のうち復号自体は16%で、残りは保管層。SdFat が ESP32 で1バイトずつ転送していた。**保管層が2〜3倍**になり挿絵1枚16秒→8.1秒 +- 本家から2件を取り込み(2026/09/17〜、`2026091703-diag`、実機確認済み) + - #2925 輝度と色を別々の走査に分けた段階表示JPEGが真っ白になる修正(JPEGDEC への3つ目の補正)。`image-format-test.epub` の A3/A4 で退行なし + - #3581 合字表を解放したあとも `stubData`/`miniData` が解放済みの表を指したままになる修正。当版は一覧画面に入るたび解放するので本家より踏みやすかった。日本語フォントは合字ゼロで無縁、欧文の本文フォント(Literata・Alegreya 等)で該当。Literata+`horizontal-regression-test.epub` で章一覧⇄読書を繰り返し、落ちないことを確認 +- 一覧画面に入るときの字形の解放を本家へ出すことを検討し、実測のうえ見送り(2026/09/17) +- 横書きの日本語で長い段落を開くと落ちる件(2026/09/17〜、`2026091705-diag`、実機確認済み) + - 横書きは空白の無い段落を1語として受け、字境を探す前処理が全文字ぶんの8バイトの記録を一塊で確保していた(本家 #2288 由来)。2,559 バイトの段落で 20.5KB の連続領域を章の組み立て中に要求し、最大連続 5KB で abort。1回の走査に直して 3.4KB にし、それも置けなければ分割を諦めて落ちない経路へ回す。縦書きは別経路で影響なし。配置の結果は同じなので章の版は据え置き + - 併せて、本棚に入るときも一覧画面と同じく字形の保持を無条件に手放す。18pt の日本語書体では1ページぶんの保持が約35KB あり、読書から戻った本棚が 空き20KB/最大連続7KB になっていた(→ 57KB/45KB) + - 本家 develop `3555ff55` の素のビルドに探針を入れて X3 で測ると、本を読んでいる最中でも章一覧に入る時点で最大連続 47KB が残り、章一覧の初回描画は解放の有無にかかわらず 641ms、戻った直後のページも 1.37 秒で差なし。当版で観測した 78 秒は 1.6.0 起点のツリーだけの症状で、本家は #3521/#3527 で既に解消済み。1.6.5 起点の作り直しではこの解放も最初は載せない +- **配布**: 本節までの内容を **pre-release 20260917(`2026091707`、非DIAG、`3a692cd9`)** として公開した(2026/09/17)。添付は `crosspoint-OST-2026091707.bin` のみ。フォントは変更が無いので 20260909 の `fonts-202609080.zip` をそのまま使う。ページ配置の保存形式 48→54・本の情報の保存形式 11→12 のため、初回オープンで全書籍の作り直しが走る +- 商業書籍の挿絵が枠だけになる/本棚の縮小表紙が出ない(2026/09/18〜、`2026091714-diag`、実機確認済み) + - 3段とも「大きい一塊をヒープに求めていた」のが原因だった。**挿絵**: 取り出しが 8KB の連続領域を2つ要り、最大連続 5,364 の章の組み立てで失敗(→ 512B まで半分ずつ下げて確保)。**表紙の取り出し**: deflate の窓 32,768 を一塊で要り、読書後の本棚は最大連続 20,468 で頭打ち(→ 画面用の記憶 48,000 を借りる)。**表紙の復号**: MCU 用の 23,040 を、直前に確保した復号器本体が割った穴から取ろうとして失敗(→ 同じく借りる) + - 併せて、表紙作りの失敗を `recent.json` に恒久記録するのをやめた(本当に表紙が無い本だけ記録する)。従来は一度失敗すると再起動しても二度と試さなかった + - 失敗点ごとの記録を足し、SDの `image-diag.txt` だけで場所が分かるようにした(`jpg OOM mcu 23040` の1行で原因が確定した) +- **配布**: 本節までの内容を **pre-release 20260918(`2026091801`、非DIAG、`4ed08c40`)** として公開した(2026/09/18)。前日の 20260917 の差し替えで、挿絵と縮小表紙の3件だけが差分。添付は `crosspoint-OST-2026091801.bin` のみ、フォントは 20260909 の `fonts-202609080.zip` をそのまま使う +- 配布版 2026091801 で15分読んだ末に章の境目で abort(2026/09/18〜、`2026091803`〜`1807-diag`、実機確認済み) + - 番地を引くと `addVerticalToken` の `std::deque::push_back`。「配置の前に領域を試す関門」は配置時にしか無く、押し込み時に無かった。`hasHeapForToken`(空き4KB/最大連続2KB)を入口に置き、割ったら組み立てを失敗扱いに + - 章の組み立てが極端に苦しい原因も1つ潰した。送り幅の表(768語)が満杯になると、表に無い字の**幅を測るためだけに字形本体を予備リング(48枠≈17KB)へ読んでいた**。`readAdvanceOnly`(常駐→予備リング→SDの12バイト)で幅だけ返し、`MeasureOnlyScope` で組み立て中に限る(描画中に使うと二度読みになり page_draw が 76→384ms に増えたので)。同じ本・同じ章で 組み立て中の最大連続 2,164→13,300、余裕の最低 7,432→20,436 + - 保存済みの章を開くと30KB台しか空かず1ページ分の字形が入らない(`prewarm_entry_fails`、その場読み888回、6秒/ページ)のは18ptの限界で以前からの挙動。別件 +- 横書きの禁則を JLREQ 3.1.7/3.1.8 の各類に広げる(2026/09/18〜、`2026091807-diag`、実機確認済み・章の版55) + - 横書きは本家 #2288(韓国語対応)由来の集合のままで括弧と句読点しか知らなかった。JLREQ 原文で確認: 3.1.7 の本則が cl-02〜cl-11・cl-13 を挙げ、**小書きの仮名(cl-11)も本則に含まれる**(Note の「行頭禁則としない方法」は採らない)。文字は付属書 A のとおりで、〒 は cl-12 に無いので入れない。……――は同じ字が続く間は切らない + - 横書きは「切ってよい組か」で判定して禁じられた組を1語に貼り合わせる方式なので、JP版 #144 のような追い出しの繰り返しは要らない(推移的に『「漢」』。は割れない)。縦書きの列末の戻しだけ1回→上限4回に(『「 の入れ子で「が列末に残っていた) + - 出典は JIS X 4051 ではなく JLREQ(W3C、日英併記で公開)に統一。**本家に PR #3609 として提出**(2026/09/18、ブランチ `fix/cjk-line-break-prohibitions`、`CjkLineBreak.h` に切り出し+gtest 9件、本家 develop `dc9c3eab` 版でも実機確認済み。審査待ち) + - 隣り合う組がすべて禁則になる人工的な試験文字列は1語が1行より長くなり「長い語を割る」予備の経路に落ちるので、現実的な語に差し替えた。`` の境目で語が貼り合わされ「見ているも」が広がるのは本家由来の別件 +- **配布**: 本節までの内容を **pre-release 20260918-2(`2026091808`、非DIAG、`15bdbaab`)** として公開した(2026/09/18)。同日の 2026091801 の差し替え。添付は `crosspoint-OST-2026091808.bin` のみ +- **配布(テクニカルプレビュー)**: 下の flash 複製の仕組みを **pre-release 20260923-tp(`2026092204-diag`、診断版のまま、`0aaf2ca7`)** として公開した(2026/09/23)。これまでの pre-release とは別系統で、普段使いは 20260918-2 のまま。説明文に「microSD のルートに動作記録(input-diag.txt など 5 種)が書き出される」旨と、複製の所要 2 分・firmware 書き換えで消える・電池 20% 未満は複製しない・戻し用の旧 firmware が失われる、を明記 +- **SD 書体を内蔵 flash に複製して読む(crossmux版 A の実装)**(2026/09/22〜23、`2026092204-diag`、実機確認済み: 初回起動の自動複製 6,549,504B が約 2 分で完了し、以降 `font=NotoSansJP_16.cpfont … src=flash flash_kb=6396 scale=18/16`。Home の空きヒープ 56.6KB/最大連続 51KB、本を開く各段階のヒープは複製 On/Off で同じ(sect 45KB、page1 33〜34KB)で、読み経路の常駐メモリは増えていない。確認中に商業本の第 44 章の作り直しでヒープが尽きて落ちた 1 件は Off でも同じ段階で同じ値になる既知の型で、本件とは無関係と判断) + - 仕組み: 使っていない側の firmware 領域(app0/app1 の空いている方、先頭 4KB のヘッダを除いて 6,549,504B)に選択中の書体ファイルの**先頭部分**を複製し、字形の読みを SD ではなく内蔵 flash から行う(`SdCardFontCache`、`HalOtaSlot`、`SdCardFont` 内の読み取り窓口 `FontFile`)。ファイル全体が入るなら全体、入らなければ入る分だけを複製し、**Regular(style 0)の実体が丸ごと入ること**を条件にする。当版の cpfont は Regular+Bold の 2 style で Regular が前半にあるので、複製の範囲外=Bold の字形だけが SD 読みになる。範囲外の読みは窓口が自動で SD に切り替える(SD のファイルは必要になるまで開かない) + - 18pt: NotoSansJP_18 は Regular だけで 7,621,409B あり入らないので、**16pt の実体を複製し 9/8 倍に拡大して 18pt を出す**(前項の検証機構を本番化。倍率は「選んだ大きさ/実体の大きさ」で上限 9/8、配布の大きさでは 18→16 の組だけが該当)。BIZUDGothic_18/BIZUDMincho_18 は Regular が 4.0〜4.2MB で本物が入る。配布 zip(fonts-202609080)の実測: NotoSansJP_16 の Regular 終端 6,160,269B(入る)、NotoSansJP_14 5,035,418B、BIZUDGothic_16 は全体 6,527,441B が入る + - 操作: Text Settings → Style に「Font Copy in Flash」を追加。On にすると複製を作る(進捗画面。SD→flash の複製と、flash からの読み戻し照合の 2 段で 6MB なら 1 分前後)。On のまま書体や大きさを変えると、変えた直後に複製を作り直す。失敗(入らない・書けない・照合不一致)は設定を Off に戻して理由を表示し SD 読みのまま。**firmware を書き換えると複製は消える**(書き換え先が同じ領域)ので、次の起動で設定が On なら自動で作り直してから Home に行く + - 電池: 複製は SD の読みと flash の書き込みを 1 分ほど続けるので、**電池 20% 未満(USB 給電中は除く)では複製を始めず設定を Off に戻す**(起動時の自動作り直しは Home に行く、設定画面では理由を表示)。途中で電池が切れてもヘッダが書けていないので複製なし扱いになるが、次の起動でまた同じ複製を始めてしまうのを避けるため(2026/09/22 ユーザー指摘) + - 診断: `font=` 行に `src=flash|sd flash_kb=… scale=n/d` を追加。`font_copy=` 行に起動時の判定(候補・倍率・容量・有効か・現在の読み元)と複製の結果(結果・大きさ・時間)を 3 件まで残し、複製の直後は強制で書き出す。18pt を選んで複製 On なら `font=NotoSansJP_16.cpfont … src=flash flash_kb=6396 scale=18/16`、Off なら `font=NotoSansJP_18.cpfont … src=sd scale=1/1` + - 安全側: 先頭 sector の消去→本体の複製(CRC-32 を取りながら)→flash からの読み戻し照合→ヘッダ(`OSTSDFC1`、経路・大きさ・ヘッダと TOC の FNV-1a・CRC)の書き込みの順。途中で電源が落ちてもヘッダ不正=複製なしとして SD から読む。読み込み時は経路・ファイル長・ヘッダと TOC の hash を照合するだけで本体の CRC は取り直さない(crossmux版と同じ)。書き換え直後で確定していない firmware では書かない(当版は Arduino の初期化が起動時に確定させるので実際は常に可)。firmware の書き換え先が空き側なので、書き換え前の firmware への戻しは失われる(当版の復旧手順は update.bin 前提で支障なし) + - 出典: crossmux版 PR #57(`perf: cache SD fonts in inactive OTA slot`)。当版は「先頭部分だけの複製」と「16pt の拡大」の組み合わせが違うので magic を別にした。前項の検証機構(`OST_SCALE_TEST`、内部値 118)は削除し、拡大の仕組みだけを残した +- 章ごとの CSS の読み直しが空の一覧で走り、章から CSS が全部抜ける件(2026/09/25、`2026092503-diag`、実機確認済み・章の版 56) + - KADOKAWA テンプレートのような大きな CSS は保存が「一部だけ」になり、章を組むたびにその章で使う規則だけを読み直す。本を開いた経路によっては CSS ファイルの一覧が空のままで、読み直しは何も読まずに規則ゼロで章を組んでいた。リアデイルの大地にて 8 巻で、1em 指定の外字画像(迸)が元の 128px のまま 1 列を占めた + - 一覧が空なら ZIP から CSS を探して補う。CSS なしで組まれた章を作り直すため章の版 55→56 +- 16pt→18pt の拡大で、字形の箱と画素の引き伸ばしを正確に 9/8 倍に(2026/09/25、`2026092501-diag`、実機確認済み) + - 寸法の切り上げと原点の四捨五入を別々に行い箱いっぱいに引き伸ばしていたため、実際の倍率が 1.13〜1.17 倍にばらつき、縦書きの 「」 が本物の 18pt より列の中心寄りに約 1px ずれ、墨も 3〜23% 多かった(線がやや太い)。元の墨の端を 9/8 倍して外側へ丸めた箱に、画素中心を正確な比で戻して補間する。墨の位置は理想から 0.1px 前後、墨の量は本物の 18pt とほぼ同じ +- 読書の後の Home で表紙の縮小画像が作れない件(2026/09/25、`2026092505-diag`、実機確認済み) + - Home のヒープを全部書き出して起動直後と比べると、読書は大きな物を何も残していない。本を開くたびに作り直される「最近の本」の文字列や、最初に使われた瞬間に作られる排他部品などの小物が、起動時の 60KB 前後の一続きの空きに散り、最大の塊が 17〜35KB に細っていた + - 縮小画像づくりは画面用の記憶を借りて作業用の行だけをそこから取っていたが、JPEG の解読器(17,884B の一塊)は一般の空きから確保していて先に失敗していた。借りた領域の先頭に解読器を置き、残りを作業用の行に回す(1440 幅で計 40.9KB、借りられる約 48KB に収まる) +- 診断の追加(2026/09/24〜25、診断版のみ): 空きメモリの最小値が下がった区間の記録(`heap_min_log=`、ページ描画の検査点 pg:load〜pg:msb)、Home に入った時点のヒープの全塊と作業単位の一覧(`heap-map-boot.txt`=起動後最初、`heap-map.txt`=以後)。最小値の底はページ描画中(白黒の描画中や階調の 1 面目)で 3KB 前後まで下がることが分かった(対策は上の 2026/09/26 の項) +- ページ描画中の空きメモリの底を上げる(2026/09/26、`2026092604-diag`、実機確認済み) + - 底は字形の予備置き場(その場読みの環)が空きを見ずに 48 枠まで育つところだった。空きが 16KB を割ったら 12 枠以上は増やさず古い枠を上書きして使い回す。描画中の最小の空きが 2.7KB 前後 → 6〜6.8KB + - 挿絵の事前変換も、最大連続だけでなく空きの総量が足りないときに画面用の記憶を借りる +- Home の空きが読書のあとで細切れになる件の根本対策(2026/09/26、`2026092605-diag`、実機確認済み) + - 前項の診断でヒープの全塊に確保元の印を付けて調べると、読書中に初めて作られてそのまま残る小物が起動時の一続きの空きに散っていた。小数の書式化の作業領域(作業単位ごと)、共有ポインタ用の排他部品、時刻取得の鍵、C++ 例外の管理情報、縦書きの全角幅の記憶、「最近の本」の一覧の伸長など + - これらを起動時と描画の作業単位の開始時に先に作らせる、固定の大きさにする、同じ内容なら書き換えない、のいずれかで読書中に新しく作らないようにした。読書のあとの Home の最大連続が 17〜35KB → 65.5KB(起動直後 69.6KB) + - 診断版には確保元の印(1 塊 12 バイト増)と、排他部品・newlib の鍵の作成元の記録を追加(通常版には入らない) +- 縦書きで入れ物(div)の上下の余白を、中の段落すべての列に効かせる(2026/09/26、`2026092607-diag`、実機確認済み・章の版 58) + - 縦書きでは上の余白が列の頭、下の余白が列の足の空きになる。入れ物の上の余白を最初の段落にだけ渡していたため、商業書籍の場面の区切り(五文字下げた入れ物の中に「・」だけの段落が三つ)で、二つ目と三つ目の「・」が列の頭に上がっていた(うちうち『逃げる魔法使い』で発見) + - 縦書きのときは段落が入れ物の上下の余白・詰め物(padding)を受け継ぐようにした。最初の段落に預けていた分とは大きい方を取り、二重に下げない(詰め物の入れ物で最初の段落だけ 4 文字下がる形で一度踏んだ) + - `vertical-test-suite.epub` のテスト25(場面の区切り、長い段落、入れ子、段落自身の余白、列の足側、詰め物、入れ物の外)で 7 項目とも合格 +- Home 画面の本体を、前の画面を消した後で作る(2026/09/26、`2026092608-diag`、実機確認済み) + - 前項の対策後も、読書のあとの Home で最大連続が 45〜51KB に割れることがあった。確保元を引くと Home 画面の本体(約 200 バイト)で、画面の切り替えは次の画面を作ってから前の画面を消すため、読書画面の物が小さな穴を埋めているうちに大きな空きの途中へ置かれていた + - Home へ戻るときは作成要求だけを預かり、読書画面を消してから作る。読書のあとの Home のヒープが起動直後と一致するようになった(空き 72.9KB/最大連続 65.5KB) +- 挿絵の事前変換の取り出しで、読み書きの区画も画面用の記憶を借りた残りに置く(2026/09/26、`2026092609-diag`、実機確認済み) + - 伸長の状態と窓(約41KB)は画面用の記憶から借りていたが、読み込み用・書き出し用の 8KB 区画2つはヒープから取っており、章の組み立て中の空きを 22.7KB → 4.8KB まで下げていた + - 借りた残り(約6.8KB)に区画を約3KB ずつ置くようにし、取り出しの前後の空きの減りが約18KB → 0.3KB、取り出し時間も 2.2〜2.3 秒 → 1.7 秒 +- 設定項目の一覧を常駐させず、使うときだけ作る(2026/09/26、`2026092613-diag`) + - 本家は一覧を一度だけ作って起動中ずっと持っていたため、約13.9KB のヒープが読書中も埋まっていた(空き 5〜6KB で描画している場面がある) + - 一覧が要るのは設定の読み書き・設定画面・Web の設定ページだけなので、そのたびに作り直す(数 ms) +- 診断の追加(2026/09/26、診断版のみ): 空きが 16KB を割ったとき確保元ごとの持ち分を `heap-low.txt`(章の組み立て中は `heap-low-build.txt`)に書き出す。確保元の印からは operator new・malloc の共通部分を除き、呼び出し元が見えるようにした +- 試験用 EPUB の整理(2026/09/26): `test/epubs-ja` の画像の試験4冊(表紙SVG・縦書きの中の画像・画素数・形式と書き方)を `image-test.epub` の1冊に統合し、`vertical-test-suite.epub` をテスト25まで入った版に差し替えた +- 空きの塊が細った読書中に、挿絵が枠だけになる件(2026/09/23、`2026092302-diag`、実機確認済み) + - JPEG の解読器(JPEGDEC)は X3 で 17,884B の一塊が要る。読書を続けて最大連続が 17,396B まで細ると、章の組み立て中の挿絵の事前変換が確保の段階で全部失敗し、表示時の再試行も書体の解放で塊が育たず代わりの枠のままだった(2 回とも 17,396B で失敗、19,444B 以上は成功) + - 事前変換は画面を描かないので、塊が足りないときだけ画面用の記憶を借りて解読器を置く(PNG の逐次展開・ZIP の展開と同じ仕組み)。実機で最大連続 15,348B のまま 2 枚とも約 3 秒で変換でき、`image-diag.txt` に `jpeg decoder in lent framebuffer` が出る。表示時のその場の変換は画面を描いている最中なので従来どおり +- 診断ファイル `input-diag.txt` の書き出しを分割(2026/09/23、`2026092301-diag`、実機確認済み) + - 2,304B の枠 1 つに全体を組んでいたため、`font_copy=` の追加後は末尾の説明が毎回切れ、描画の記録が 12 件たまると枠を超えて**書き出し自体を省いていた**(長く読んだ後ほど古い記録が残る)。前半・後半を同じ枠で順に書き、説明文は flash 上の定数から直接書く。`font_copy=` の行も途中で切れていたので枠を広げた(常駐 +144B) +- **拡縮の検証機構**(2026/09/22、`2026092201-diag`、実機確認済み。`-DOST_SCALE_TEST=1` のときだけ組み込まれ、通常版には入らない) + - 目的: crossmux版由来の「SD 書体を空き側の firmware 領域(実効 6.5MB)に複製して flash から読む」案(A)に向け、18pt の実体(NotoSansJP_18 は 15.6MB で入らない)の代わりに **16pt の実体を 9/8 倍に拡大して 18pt を出す**画質を実機で確かめる + - 作り: Text Settings → Size に「18 pt (16 x9/8)」(内部値 118)。`SdCardFont::load` に倍率を持たせ、TOC の行送り・上端・下端を 9/8 倍、字形が置き場(mini 領域・予備の環)に入る瞬間に字形記録(幅・高さは切り上げ、寄せ・送り幅は四捨五入)と 2 階調 bitmap(双一次で拡大して 4 段階に丸め直す、整数演算)を拡大。送り幅表もその場読みも倍率適用。書体の識別子と章の保存は 118 として本物の 16/18 と別扱い + - 実機: `font=NotoSansJP_16.cpfont advY=54`(本物の 18pt と同じ行送り)で起動、縦書き 146 升 174ms、先読み 225ms、その場読み 18 字、組版・禁則・約物・ルビに異常なし。画質は写真では本物と区別がつかず、最終判断はユーザーの目視待ち + - 前段の検討: 10pt→18pt(1.81 倍)や 12pt→18pt(1.52 倍)は 2 階調化で画の太さが不揃いになり本文には不可、16pt→18pt(1.12 倍)だけが許容範囲(`e-ink/pr-photos/font-scale-compare.png`)。FreeType(本家 PR #3646 の方式)は cmap+loca を丸ごと RAM に置くため X3 では不可(BIZUDGothic 258KB、NotoSansJP 203KB) +- 確認対象のフォークに **crossmux版**(0x1abin/crossmux、中国語対応の community fork)を追加(2026/09/22、ユーザー指示)。本家 1.6.0 起点、SDK も自前 fork、縦書き・禁則の実装なし。UI・機能面の参考(Apps、読書統計、C3 の読書メモリの上限管理)として見る +- 本家とフォークの更新確認(2026/09/22): 本家 develop は 09/20〜21 に #3630(一覧の NFC 合成の復元)・#3528(語間・字間の設定、章の版 47)・#3608(転送後に Library の索引を作り直す)。#3528 で PR #3609 が衝突したため develop の上に積み直し(章の版 48、CI 通過)。抹茶版が 1.6.0 安定版(辞書引きと漫画が中心、当版に無縁)。witchhunt版 2.31 で当版に関係する知見が3つ: (1) **章ごとの style の記憶(inline style と tag|class の解決結果)を 64 件で打ち切り**——PDF 由来の EPUB は語ごとに固有の inline style を持ち、記憶が 129KB まで育っていた(当版の CSS の省略と同じ問題の別の入口。1.6.5 起点の作り直し時に本家の `ChapterHtmlSlimParser` を見て要否判断)。(2) SD 書体を最も近い顔から拡大して 20〜26pt を出す(当版は 18pt が上限で、拡大は画質を落とすので採らない)。(3) SD の読み出しをセクタ単位で束ねる(当版は `USE_SPI_ARRAY_TRANSFER` で把握済み)。本家 PR #3646(TTF)は未マージ、#3469(X3 階調の縦縞)も未マージ +- **本家の Library 表示を日本語で使えるようにする読み対応を本家に PR #3651 として提出**(2026/09/21、枝 `feat/library-file-as-reading`、本家 develop `ce2b4fcd` 起点、当版のコードは含まない) + - 本家 Library(#3366)を実機で試した結果「日本語では利点が薄い」(漢字の題名が符号順、見出しが1字ごと)。EPUB の読み(`file-as`、蔵書 2,256 冊中 1,943 冊が題名に持つ)を並び・見出し・著者の組にだけ使い、表示は本の題名・著者名のまま + - 読み取り: `refines` の前後どちらの書き方も、EPUB 2 の `opf:file-as` 属性も読む。書誌の保存に読みを2欄追加(v11) + - 折りたたみ: カタカナ→ひらがな、全角英数→半角、ー・々・ゝゞ は語の一部、見出しは五十音の行頭(が→か、ぱ→は) + - 並びの鍵: 12 バイトの鍵にかなを1字1バイトで詰め(従来4字→12字)、それでも同じ題名は読みの全文で並べ直す(叢書の巻順のため)。著者順は読みが「姓 名」なので姓の推測をやめる。索引形式 3/折りたたみ版 4 + - 実機: Title で あ・か の見出し、ら でリアデイル 4→8 巻順、Author で読み シーズ の Ceez が さ〜た の間。読みの無い自作 EPUB は従来どおり漢字1字の見出し。host テスト 105 件通過 + - 別件として判明: 本家は著者欄「Ceez, てんまそ」(著者と絵師)を「姓, 名」と見なして「てんまそ Ceez」に反転する(著者2人の本すべて)。この PR には含めない + - PR の写真は fork の孤立した枝 `pr-assets` に置いた(claudeworks は非公開のため) +- 本家とフォークの更新確認(2026/09/20): 本家 develop は 09/18 以降 5件(#3613 網の画面を抜けるときに無線を切る、#3618 組み込むUI言語の選択、CI 2件、独語訳)で、当版に効くものなし。#3618 は `custom_i18n_builtin_langs = en` で 34 言語分の文字列をフラッシュから外せるので 1.6.5 起点の作り直しで使う。本家 SDK は当版の `fde240f` から 61件先(TTF 描画、一覧の行の要求方式 #105、X3 の絶対階調 #94 は当版に含まれ、本家側の PR #3469(X3 の階調画像の縦縞、BMP 表示と眠り画面が対象)は未マージのまま)。Yomuka版 v0.7.6.0 の候補は 2件: 曲線引用符 ‘’“” を縦の升に正立で置く(当版は横倒しのまま。要実機写真)、ルビ・上付きに 10pt 専用の書体データ(当版は本文書体の SUP 縮小。細い画の途切れが出ているか要写真)。抹茶版 #282(区間表の共有)・JP版 v0.3.2(動的壁紙・進捗率・テーマ)・witchhunt版 2.30(触れる操作・機種追加)は当版に無縁。当版の本家 PR は #3539・#3609 とも人のレビュー待ち +- 本家 1.6.5rc の蔵書表示が 4096 冊で止まる報告(討議 #3544)について当版を確認(2026/09/17) + - 上限は蔵書目録 `lib/LibraryIndex/`(#3366)の `CLIX_MAX_RECORDS = 4096` で、当版にはその機能自体が無い。当版で冊数に効くのは Browse Files の1フォルダあたりの件数(本家 1.6.0 と同じ作りで、見積もり数百件が頭打ち)と、`/.crosspoint/` 直下に1冊1フォルダで平らに増える作業用フォルダ(本家由来)。作り直しで蔵書表示を採ると 4096 の上限もそのまま入る +- 内蔵字形のページ枠は同じ書体で二重に取らない/挿絵の保存が無いページの四角の下書きは描かない(2026/09/10〜、抹茶版から) + - 前者は読まれない枠が4枠の1つを潰していた分の解消、後者は書き換え1回(約0.5秒)の削減 + - 挿絵ページが白黒→階調の2段階で出るのはX3の階調表示の仕組み上の挙動で、実用上の不都合が無いためこのままとする + -[![Fund contributors](https://img.shields.io/badge/%F0%9F%91%91_Fund_contributors-royalty.dev-BB953A?style=for-the-badge&labelColor=1a1a1a)](https://app.royalty.dev/crosspoint-reader/crosspoint-reader) +## 1.6.5ベースへの作り直し(2026/09/28〜) by claude code + +本家 1.6.5(タグ `1.6.5`=`93e98bb7`、2026/09/27)から `vertical-1.6.5` を作り、当版の機能を段階ごとに載せ直す。 +前回(1.6.0)は机上で取捨を決めたが、今回は本家が断片化・入力の取りこぼしに正面から取り組んだ後なので、 +**本家の対処をまず素のまま採り、実測で足りない箇所だけ当版の実装を戻す**。freeink-sdk は 1.6.5 が指す +`111fdcc7` を使う(当版が使っていた `fde240f` とパネル待ちの修正 ed39106 を含むので、SDK で持ち込むものは無い)。 + +| 段階 | 中身 | 状態 | +|---|---|---| +| 0 | 素の 1.6.5 のビルド | 完了(RAM 57,912B/flash 5,621,527B) | +| 1 | 診断(INPUT_DIAG、ビルド番号) | 完了(`2026092803-diag`、実機確認済み) | +| 2 | 時計と監視(#3562 の地域・夏時間の設定に載せ替え) | | +| 3 | 日本語フォント(生成フォントと描画の追加分。メモリ処理は入れない)+素の実測① | | +| 4 | 横書きの組版(禁則、長い段落) | | +| 5 | 縦書きの中核 | | +| 6 | 縦書きの字間(本家 #3528 の字間を縦書きの字送りにも効かせる、新規) | | +| 7 | 挿絵(本家 #2332 の上に載せる) | | +| 8 | 表紙(SVG の包み、縮小表紙) | | +| 8a | flash から書体を読む仕組み、16pt を 9/8 倍して 18pt に使う仕組み | | +| 8b | 書棚の読み順(本家 PR #3651) | | +| 9 | 実測②と不足分の補い(断片化対策・描画中の入力・本棚での解放など) | | +| 10 | 配布(非診断版、README の整理) | | + +### 段階1: 診断(2026-09-28、`2026092803-diag`、実機確認済み) +- `src/util/InputDiag.*` と `scripts/ost_version.py` は旧ツリーの最終形のまま持ち込み、呼び出しは本家 1.6.5 にある処理にだけ入れ直した + - 主ループの読み取り間隔と書き出し(書き出しは、描画中に主ループが途中で戻る本家 #3652 の分岐より手前に置いた)、画面の描画時間、Home に入った時点のヒープの一覧、読書画面の開く各段階・閉じたときのヒープ・章の組み立ての計時・ページ描画の検査点、章の保存のページごとの書き出し時間、クラッシュ報告の先頭のビルド番号 + - 字形の読み込み回数と字形置き場の解放記録は段階3、縦書き・挿絵・先読みの計測点は各段階で入れる。一覧画面の表示範囲の記録は、本家が一覧の作りを変えたため入れる場所が無く見送り +- 非診断版もビルドが通る(RAM は素の 1.6.5 と同じ、flash +236B) +- 実機での最初の観察(素の 1.6.5 に診断だけ、日本語 SD 書体・商業書籍の横書き、文字のアンチエイリアス ON) + - ページ送り 1.1〜1.3 秒、章の組み立て最長 3.5 秒(5回に分けて)、本を開く各段階の空き 95→70KB(最大連続 41KB のまま)、閉じると 96KB に戻る + - 空きの底は 11.4KB(最大連続 4.6KB)で、アンチエイリアスの帯描画の 8KB 作業領域を取った直後。その時点の持ち主の上位は、画面用の記憶 53,248B(SDK が起動時にヒープに取る。旧ツリーでも同じ)、設定項目の一覧 9,216B(本家は起動中ずっと保持。旧ツリーでは `8cc5f333` で使うときだけ作る形にした)、読書中の CSS 6,912B+4,352B、SD 書体の一覧 5,376B と書体ファイル名の文字列 35 個 3,272B、章の組み立て中の XML 読み取り用 4,352B + - Home の初回描画 7.1 秒は表紙の縮小画像の作成(823KB の JPEG の展開 3 秒+変換 2.5 秒)。初回だけ + +## 1.6.0ベースへの作り直し(2026/09/06〜) by claude code + +本家1.6.0が2026/09/05に正式リリースされたため、方針どおり**1.6.0(`54337e6d`)から新ブランチ +`feat/vertical-1.6.0based` を切り、旧ツリー `feat/vertical-1.6` の成果を段階ごとに移し直している**。 +移す元は旧ツリーの完成形(`68a4b4ca`、1.6.0rcから124コミット)で、コミット単位の機械的な移し替えではない。 +6段階の移植と実機確認を終え、**新ツリー初の配布版を pre-release 20260906(`2026090611`、非DIAG)として公開**した +(2026-09-06)。旧ツリーの配布版(pre-release 20260905=`2026090504`)はそのまま残す。同日、GitHubのデフォルトブランチを +`feat/vertical-1.6` から `feat/vertical-1.6.0based` に切り替えた。 + +### 本家 1.6.0 との差分一覧(2026-09-08時点、`c28f1cb5`) + +本家 v1.6.0(`54337e6d`)に対して、コミット21件、73ファイル、+9,601/−567行。領域ごとの内訳は次のとおり +(行数は追加/削除。本家側は 1.6.0 以降 #3397 のビルド環境更新1件だけで、当版と重なるのは体裁変更の3ファイル)。 + +**1. 縦書き組版(本体の中心)** + +| ファイル | 追加/削除 | 内容 | +|---|---|---| +| `lib/GfxRenderer/VerticalTextUtils.h` | +371/0 | 新規。UAX#50準拠の文字の向き分類、縦書き約物の位置表、小書き仮名の寄せ、禁則表(行頭・行末、〝〟や中点まで拡充済み) | +| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp/.h` | +1,043/−43 | 縦書きの字ごとの分解、縦中横の自動判定、ルビ・圏点の付け方、外字画像、変異選択子の読み飛ばし、文字の向き指定(`VERTICAL_FLIP`)、子孫指定子用の先祖要素の記録、列のページ配置 | +| `lib/Epub/Epub/ParsedText.cpp/.h` | +293/−24 | 右から左への列組み、列境界の禁則押し戻し、縦書きの字下げ(正負)、横書きの圏点 | +| `lib/Epub/Epub/blocks/TextBlock.cpp/.h` | +603/−5 | 縦書き列の描画(正立・横倒し・縦中横、ルビ、傍線・打ち消し線)、保存形式の縦書き拡張 | +| `lib/GfxRenderer/GfxRenderer.cpp/.h` | +206/−7 | 横倒し描画 `drawTextSideways`、縦書き字間設定、縦書き用の計測 | +| `lib/EpdFont/EpdFontFamily.h` | +5/0 | 書式ビット `VERTICAL_FLIP` | +| `src/activities/reader/EpubReaderActivity.cpp/.h`、`ReaderUtils.h` | +325/−40 | 縦書き本の自動判別、右開きのページ送り(左が進む)、縦書き時の余白と状態表示 | +| `lib/Epub/Epub/ReaderRenderSpec.h`、`Section.cpp` の一部 | — | 縦書きフラグと字間を配置の仕様に持たせる | + +**2. 書式(CSS)解析の拡張** + +| ファイル | 追加/削除 | 内容 | +|---|---|---| +| `lib/Epub/Epub/css/CssParser.cpp/.h`、`CssStyle.h` | +331/−46 | text-emphasis(圏点)、max-width/max-height、text-orientation/text-combine-upright(接頭辞付き含む)、二段の子孫指定子(`.vrtl .x`)、保存形式 v13 | +| `lib/Epub/Epub/css/CssSelectorUsage.cpp/.h` | +288/0 | 新規。章が実際に使う要素名・クラス名を集めて、読み込む規則を絞る | +| `lib/Epub/Epub.cpp/.h`、`Section.cpp` | +161/−9 | 章ごとの絞り込み解析、保存が打ち切られた場合の読み直し、空きメモリ24KBでの再解析 | + +**3. 挿絵** + +| ファイル | 追加/削除 | 内容 | +|---|---|---| +| `lib/Epub/Epub/converters/PngStreamDecoder.cpp/.h` | +382/0 | 新規。逐次読みのPNG展開 | +| `converters/ImageToFramebufferDecoder`、`Jpeg…`、`PngToFramebufferConverter` | +39/−6 | 寸法指定(幅・高さ・上限)と構築時の画像キャッシュ | +| `lib/Epub/Epub/blocks/ImageBlock.cpp/.h` | +107/−1 | 縦書きでの挿絵の置き方(列の流れに入れる、幅広は列を割り当てて上下中央) | +| `lib/Epub/Epub/parsers/ContentOpfParser.cpp/.h` | +8/0 | 画像の探索用 | + +**4. SDフォントのメモリ管理** + +| ファイル | 追加/削除 | 内容 | +|---|---|---| +| `lib/EpdFont/SdCardFont.cpp/.h` | +196/−65 | 字形領域を4KB刻みで確保(連続領域が取れない状況の回避)、縦書き字形の存在確認 | +| `lib/GfxRenderer/FontCacheManager.cpp/.h` | +21/0 | 一覧画面での字形キャッシュ解放 | +| `src/activities/UiListActivity.cpp` | +16/−1 | 同上の呼び出し | + +**5. 時計モードと固まり検知** + +| ファイル | 追加/削除 | 内容 | +|---|---|---| +| `src/activities/util/ClockActivity.cpp/.h`、`src/util/ClockFace.cpp/.h` | +484/0 | 新規。時計画面 | +| `src/images/ClockDigits.h`、`scripts/build_clock_digits.py` | +1,522/0 | 新規。時計の数字画像とその生成台本 | +| `lib/hal/HalClock.cpp/.h`、`HalPowerManager.cpp/.h`、`HalSystem.cpp`、`HalGPIO.cpp/.h` | +151/−12 | UTC補正、時計中の省電力、30秒の監視タイマーと再起動理由の記録 | +| `src/activities/ActivityManager.cpp/.h`、`HomeActivity.cpp/.h`、`Activity.h`、`main.cpp` | +140/−12 | Homeメニューの「Start clock mode」、監視の掛け外し、クラッシュ報告へのビルド番号 | +| `lib/I18n/translations/english.yaml` | +1/0 | メニュー文言 | + +**6. 診断基盤** + +| ファイル | 追加/削除 | 内容 | +|---|---|---| +| `src/util/InputDiag.cpp/.h` | +741/0 | 新規。ボタン応答・画像処理・空きメモリをSDのファイルに書き出す(`-DINPUT_DIAG=1` のときだけ組み込む) | + +**7. ビルド・配布・資料** + +| ファイル | 追加/削除 | 内容 | +|---|---|---| +| `scripts/ost_version.py`、`platformio.ini` | +171/0 | `crosspoint-OST-YYYYMMDDNN[-diag].bin` の命名とビルド番号 | +| `README.md` | — | 当版の記録 | +| `test/epubs-ja/`(EPUB 7冊+README) | +549/0 と 858KB | 縦書きテスト書、挿絵テスト書、寸法テスト書、横書き・縦書きの基本テスト | +| `src/util/DictHtmlPages.cpp` | +1/−1 | 配置仕様の引数追加に伴う1行 | + +**2026-09-09 の追加分**(上の表に未反映): `EpubReaderActivity.cpp/.h`(次の章の先組み +170)、`Section.cpp/.h`(組み立て1回の時間上限 +10)、 +`SdCardFont.cpp/.h`(寸法だけの字形集合への絵付き合成の抑止、字形集合を捨てた記録 +40)、`HalGPIO`・`MappedInputManager`(押下中の判定 +20)、 +`InputDiag.cpp/.h`(先組みと字形集合の診断行 +60)、`test/epubs-ja/vertical-test-suite.epub` にテスト22(42→46章)。 + +**本家と意図的に違えている点**(機能ではなく判断): UI文字の太さは本家どおり Regular(#3096 は採用しない)、 +「Extra Paragraph Spacing」ONでも縦書きの明示の字下げは効かせる(横書きの本家挙動とは異なる)、表は縦書きでは +格子ではなく積み上げ、列より幅の広い外字画像は独立した列(1文字大のものは 2026-09-10 から字として列に流す)。 + +---- + +### 配布フォントの出典と同梱文書(2026-09-08) + +SDカード用フォント `fonts-YYYYMMDD.zip` の3書体は、`feat/japanese-sd-fonts` ブランチの `lib/EpdFont/scripts/sd-fonts.yaml` +に書いた取得元から元フォントを取り、JIS X 0213 などの符号位置リストで収録範囲を絞って点画像形式(.cpfont)に変換したもの。 +太字は元フォントの太字から作り、字形は変えていない。元フォントはすべて SIL Open Font License 1.1。 + +| zip内の書体 | 元フォント(取得元) | 2026-08-12 取得時の版 | 著作権表示 | +|---|---|---|---| +| BIZUDGothic | BIZ UDGothic Regular/Bold — github.com/googlefonts/morisawa-biz-ud-gothic(main) | 1.051 | 2022 The BIZ UDGothic Project Authors(モリサワ) | +| BIZUDMincho | BIZ UDMincho Regular/Bold — github.com/googlefonts/morisawa-biz-ud-mincho(main) | 1.06 | 2022 The BIZ UDMincho Project Authors | +| NotoSansJP | Noto Sans CJK JP Regular/Bold(OTF)— github.com/notofonts/noto-cjk(main) | 2.004 | 2014–2021 Adobe | +| 後詰め(共通) | Noto Sans Math — github.com/notofonts/notofonts.github.io(main)、Noto Sans — 本家同梱 `builtinFonts/source/NotoSans` | 3.000/— | Google LLC/The Noto Project Authors | + +後詰めは元フォントに無い字(∼ ≈ などの記号)だけを補う。取得元は版を固定していないので、作り直すと元フォントの版が +上がっている可能性がある(NOTICE.txt に取得時の版を記録する)。 + +**2026-09-08 から、配布 zip に `fonts/NOTICE.txt`(上の表に当たる出典・版・著作権表示・後詰め・変換内容)と +`fonts/OFL-1.1.txt`(ライセンス本文)を同梱する**(ユーザー指示)。OFL は変換物を配る際もライセンス本文と著作権表示を +添えることを求めているため。生成は同ブランチの `lib/EpdFont/scripts/make-fonts-zip.py` で、元フォントの名前表から +版・著作権・Reserved Font Name の有無を読んで書き出す(4つの元フォントのいずれも Reserved Font Name は宣言していない)。 +最初の同梱版は `fonts-202609080.zip`(フォント本体は `fonts-202608240.zip` と1バイトも変わらない)。**pre-release 20260909 に添付済み**(2026-09-09。フォント本体は同一なので、`fonts-202608240.zip` を入れた microSD はそのままで足りる)。 + +### 本家 #3412 から挿絵の失敗記録の扱いを取り込み(2026-09-08、`2026090801-diag`、実機確認済み) + +本家 develop の `b7bceb56` #3412(KOSync のメモリ検査の修正)のうち、当版に効く部分だけを手で移した。展開に失敗した挿絵の記録を +「本を閉じるまで」から「そのページの描画の間だけ」に縮め、次にそのページを開いたときに読み解きをやり直す(`ImageBlock` の +改名と、`renderContents` の先頭で記録を消す1行)。X3 では一時的なメモリ不足で挿絵1枚が白い枠になることがあり、従来はその本を +閉じるまで戻らなかった。KOSync・PSRAM・表示用領域の貸し出しに関わる残りの部分は当版に無縁なので取り込まない。 + +`vertical-image-test.epub` は v12 として第13章「挿絵が多い章(展開の失敗と再挑戦)」を追加。480×360 の番号付き挿絵12枚を続けて置き、 +白い枠が出たら1ページ戻って再度進めば絵が戻る、という見かたを解説に書いた。実機(2026-09-08): 12枚すべてが番号順に出て、 +間の文も欠けなし。白い枠は一度も出なかったため再挑戦の経路そのものは未通過だが、退行なし。 + +### 次の章の先組みと、章の境目の描画の遅さの修正(2026-09-09、`2026090908-diag`、実機確認済み) + +**配布**: 本節までの内容を **pre-release 20260909(`2026090909`、非DIAG、`e83d2122`)** として公開した(2026-09-09)。 +添付は `crosspoint-OST-2026090909.bin` と `fonts-202609080.zip`(NOTICE.txt/OFL-1.1.txt 同梱、フォント本体は 202608240 と同一)。 +非DIAG版は `strings` で `input-diag` が 0 件、時計の監視が入っていることを確認してから添付した。 + +参考3ツリー(Papyrix版・Inx版・witchhunt版)の表示高速化を見比べた結果、当版の実測(`2026090609-diag`)では通常のページ送り +約460msのうち描画は約30msで残りは電子ペーパーの書き換えそのものであり、witchhunt版の「次ページの先描き」を移しても +40ms程度しか縮まない。Papyrix版の全体 `-O2` はフラッシュが 84.4→96.2% になるため不採用。本当に長いのは章の境目 +(章の組み立て 4.6〜8.8秒を含めて 7〜17秒)、挿絵ページ(3.2秒)、章の最初のページの淡い階調(2.2秒)で、まず章の境目を +「次の章の先組み」で潰すことにした(ユーザー承認 2026-09-09)。 + +**先組みの仕組み**(`EpubReaderActivity::tickLookaheadBuild`): いま読んでいる章の組み立てが終わっていて、ページ表示から +1.5秒以上ボタン操作が無いとき、次の章の配置を少しずつ組み立ててSDの保存(`sections/N.bin`)に書く。章を進めると保存を +読むだけになる。空きメモリ 40KB 未満か最大の連続領域 20KB 未満なら手を付けず、途中で別の章へ跳んだら部分保存として残す。 +章の元データの展開が要るときは章をまたぐときと同じ方法で表示用領域を一時的に借り、終わったら今のページを描き戻す +(画面は変わらない)。診断ファイルに `lookahead=starts N done N ticks N pages N total_ms N chunk_max_ms N (spine N at N MHz) +font_releases N` の行を追加。 + +**実機で直した3件**(すべて診断ファイルの数字で発見): + +1. `2026090902-diag`: 先組みの1ページ分が 17.6 秒。本体は操作が3秒途切れると CPU を 160MHz→10MHz に落とす(X3 は外部 + メモリ無しの設定)ため、その後の先組みが16分の1の速さで動いていた。0903 で、先組みの1回分に描画と同じ全速の保持を掛け、 + 進行中は主ループの待ちも省くようにした(終われば元どおり3秒で省電力へ)。 +2. `2026090903-diag`: 11秒待ってからのページ送りが無視された。X3 のボタンは抵抗の分圧を主ループが読み、5ms以上離れた2回の + 読み取りで同じ状態が続いて初めて確定する。先組みの1回分(1ページ=最長0.5秒)の間は読み取りが止まるので、その間に + 押して離した操作は消える。0906 で、1回分を「元データ1KBぶんの処理」単位で 25ms 上限に区切り(`Section::buildSomeMore` + の時間上限)、ボタンが押されている・確定待ち・押した直後・離した直後のいずれかなら1回休むようにした。実測の最長は 190ms + (1単位が重いページ)。待機中の次ページの字形先読み(既存、最長0.7秒)にも同じ条件を付けた。 +3. `2026090906-diag`/`2026090907-diag`: 目次から章を開いたときや、キャッシュ削除後の最初の開きで、1ページ目が出たあと + 19〜21秒ボタンが効かない(描画 23〜25秒)。白黒の描画は 47ms で正常なのに、淡い階調の描き分け2回が字形を 812〜945 個 + SD から1個ずつ読んでいた(8個しか入らない予備の置き場の使い回し)。原因は字形の一時記憶(ページ分の部分集合)の合成規則: + 章の組み立て(配置計算)は字形の**寸法だけ**を集めるが、その直後のページ描画はその集合に**絵付き**でページの字形を合成する + ため、章ぶん(最大512字)の絵を読み込む大きな塊になり、直後の空き不足で塊ごと捨てられて階調の描き分けが裸で走っていた。 + 先組みでも同じ膨張が起き(本を閉じたあとの空きが 54→34KB)、後のページの字形予算が削られていた。0907 で先組みの配置計算の + 前後に一時記憶を空にし、0908 で寸法だけの集合には絵付きの合成をしない(ページ分だけ作り直す)ようにした。あわせて + 一時記憶を捨てた時刻・呼び出し元・空き・字数を `mini_free=` 行に最新4件残す(番地は `dist/*.elf` で `addr2line`)。 + キャッシュ削除後の最初の開きは 5.9 秒(組み立て 1.2 秒+描画 4.1 秒。階調の1回目 2.2 秒は全面書き換えの完了待ち)で、 + 字形の個別読み込みは 0。 + +**残る章の境目のコスト**: 全面書き換え 1.05 秒+その完了待ち 2.2 秒+字形の先読み 0.4 秒。組み立ては先組みで隠れる。 +次はこの完了待ち(`gray_lsb` に含まれる `waitRefreshComplete`)を見る。 + +**テストEPUB**: `vertical-test-suite.epub` にテスト22「章の切り替えと次の章の先組み」を追加(第43〜46章、42→46章)。 +本文A(待つ場所、約950字)・本文B(先組みの対象、番号付き60段落・約13,000字)・本文C(Bと同じ長さの比較用、目次から +直接開く)。解説に見かた(1)効果(2)比較(3)取りこぼし(4)表示の正しさ(5)待機中の画面と、再試験時のキャッシュ削除を記載。 +実機(2026-09-09、`0906`〜`0908`): 先組み 40〜48 ページを約1秒で完了、1回押しで確実に切り替わり、体感の違和感なし。 + +**共有時の記録**: `2026090902`→`0903`→`0906`→`0907`→`0908` の各 diag を claudeworks で入れ替えながら確認。診断ファイルは +`/input-diag.txt`・`/input-diag-log.txt`・`/open-heap.txt`。 + +### 縦書きの外字画像を字として列に流す(2026-09-10、`2026091002-diag`、商業書籍で実機確認済み) + +前節の「№2」の続き。1文字大に直っても、当版は縦書きの本文中の画像を独立した列の上下中央に置いていたため、文の途中の +「№2」が別の列の真ん中に浮いていた(既知の制限「外字画像は独立した列」)。JP版 #129 と同じ考え方で、**幅が列の間隔 +(`getLineHeight`)以内で高さが列の半分以内の画像は、文字と同じ扱いの1語として列に流す**ようにした。 + +- 語の文字列に画像の寸法と保存先を埋め込む(先頭に私用領域の U+F8F0、続けて `幅,高さ,展開先元の場所`。 + `lib/Epub/Epub/InlineImageToken.h`)。章の保存形式は変えず、描画時に語の文字列から読み解く(縦中横・横倒しの判定と同じ方式)。 + `VerticalBehavior::InlineImage` を追加し、配置では高さの分だけ進む(字間の設定も同じく加える) +- 描画は `TextBlock::renderVertical` で、列の幅に対して中央に寄せ、`ImageBlock::render` で挿絵と同じ経路(組み立て時に作る + 縮小済みの保存 `.pxc`、無ければその場で展開)を使う。字形の一時記憶の走査中は描かない +- ルビの親文字の範囲は語の数で数えているので、`` の形はそのまま画像の横に読みが出る。ルビの範囲の長さと + 列の末端の計算は画像の高さを使う +- 解析器は画像の縮小済み保存を作ったあと、条件を満たせば段落を分けずに語として追加する(`IMG_DIAG("inline WxH pitch=N")`)。 + 幅が列より広い画像(幅1.5em・2em など)と高さが列の半分を超えるものは従来どおり独立した列。横書きは変えていない + +実機(2026-09-10、`2026091002-diag`): 『リアデイルの大地にて5』の「スキルマスター№2! 九条様管理下!」が同じ列に +1文字大で並び、直後のルビも崩れない。`vertical-image-test.epub` の第5・8・10・12章の合格条件(「前後の文字と同じ列に並ぶ」)は +元からこの動きを求める書き方なので変更なし。**第5章は5ページ全部合格**(同日、写真で照合): 【外字1em】と【連続外字】が +前後の字と同じ列に同じ大きさで入り、【大画像を縮小】は枠・左上の丸・右下の三角がすべて見える1文字大、【文の連続】は画像の前後で +文が切れず、**【ルビ付き外字】に読み「るび」が画像の横に出る**(これまで「未対応として記録」だった項目。ルビの範囲を語の数で +数える仕組みがそのまま効いた)。**第8章も合格**(同日): 【外字・正方】と【外字・縦長】(幅1em・高さ自動)が前後の字と +同じ列に入り、【外字・横長】(高さ1em・幅自動=列より広い)は設計どおり独立した列で本文と重ならない。挿絵の【前】【後】・全面・ +横長・【指定なし】(原寸)は従来どおり。**第10章も合格**(同日): 【基準】の1em角だけが字として流れ、【幅1.5em】【幅2em】 +【幅2em・横長】【高さ3em】(元が縦長で幅1.5em)【高さ4em】(正方形で幅4em)【幅6em】は幅が列の間隔(54px)を超えるので +設計どおり独立した列の上下中央。解説は「列を一本割り当てて置く実装は合格」としており本文とも重ならない。列の間隔+両側の +空き(38px の字幅に対して片側 8px)まで許せば幅1.5em(66px)も流せるが、ルビの置き場と当たるので今は間隔以内に留める。 +**第12章も合格**(同日): Kobo形式の区切り要素に包まれた【文中外字】が同じ列に1文字大、**【ルビ付き外字】に読み「るび」が出る** +(市販の本の912箇所の形)、【一字ずつの読み】と【区切りの境目】は従来どおり。これで第5・8・10・12章の外字に関わる項目は全部合格。 +テスト書の解説にある「読みが出ていなければ未対応として記録」の文言は次にテスト書を触るときに改める。 + +### 描画中のボタンを別の系で読み取る/階調の描き分けの打ち切り/Indexing を早く出す(2026-09-10、`2026091005-diag`、実機確認済み) + +「ボタンが効かない瞬間に待ち表示を出せないか」という問いへの答え。待ち表示は画面の書き換え(最短 0.3 秒)を処理の前に +先回りして出すしかなく、最も頻度の高い通常のページ送り(1.2〜4.5 秒)に付けると毎ページ遅くなってちらつく。そこで +表示ではなく、**押されたことを取りこぼさない**方向で実装した(ユーザー承認 2026-09-10)。 + +1. **描画中のボタン読み取り**(`HalGPIO::startBackgroundSampling`): 描画は専用の系(`ActivityManagerRender`、優先度1)で + 走り、主ループはその間 `RenderLock` で止まるので分圧の読み取りも止まっていた。描画の間だけ優先度2の小さな系 + (`btnsample`、5ms 周期、2回の ADC 読み)が `InputManager::update()` を代わりに呼び、押した・離したの立ち上がりを + 保持する。主ループの次の `HalGPIO::update()` が保持分を「いま起きた」として渡す(`wasPressed`/`wasReleased` に OR)。 + `update()` と読み取り系の排他は FreeRTOS の相互排他(ADC の読み取りは割り込み禁止の区間に入れない)。描画が終わると + 読み取り系は通知待ちで眠り、待機中は従来どおり主ループが読むので省電力の動きは変わらない +2. **階調の描き分けの打ち切り**: 短冊ごと(50〜100ms)に保持分を見て、押されていれば残りの描き分けと階調の書き換えを + 省き、白黒のページのまま `cleanupGrayscaleWithFrameBuffer()` で制御器の基準を戻して次へ進む。診断の `aa_aborts=` +3. **Indexing の条件緩和**: 目標 20→10 ページ、章 96KB かつ展開あり→32KB(展開の有無を問わず)、途中で出す待ち時間 + 1→0.5 秒、組み立ての単位 8→4 ページ(判定を細かくするため) +4. 章の組み立て中(Indexing)の押下も保持されるので組み立て後に効く。組み立て自体の打ち切りは未実装 + +実機(`2026091005-diag`、2分15秒): `aa_aborts=4`(ページを出した直後の押下で描き分けを打ち切って進んだ回数)、 +押した回数と確定した数は読み取り系の分が確定側に加わる形(84 episodes/130 edges)で、取りこぼしなし。 +主ループの読み取りの最長の空きは 20 秒→2 秒(描画中は読み取り系が受け持つので、この空きは取りこぼしにならない)。 +誤動作(押していないのに進む、二重に進む)の有無はユーザーの体感で確認。 + +同じ診断から: 挿絵ページ(512×694)の階調の描き分けは短冊ごとに縮小済み保存を SD から読み直しており(1回の描き分けで +7回・1.7 秒、2回で 3.5 秒)、ページ全体で 5.0 秒。RAM の置き場(`img_slot`)はこの大きさでは使われない。次の高速化候補。 + +### 本家の更新確認と #3463 の取り込み(2026-09-10、`2026091006-diag`) + +本家 develop は 1.6.0 に 09-09 の2件が加わった状態(リリースは 1.6.0 のまま)。 + +- **#3463**(`9ba16086`、`cherry-pick -x` で取り込み → `5a3d3bfe`): 操作が3秒途切れたあと主ループは 50ms 眠って読むため、 + 65ms 未満の押下が1回しか読まれず消えていた。眠りを 10ms 刻みにし、分圧が待機の電位(約4095)から離れた瞬間に起こす + (`HalGPIO::rawInputActive()`、`main.cpp` の待機の眠り)。当版の「描画中は別の系で読む」(同日)は描画中、こちらは待機中を + 受け持つ補完関係。本家の実測では押下の中央値 145ms・最短 31ms で、65ms 以下が 100% 消えていた +- **#3439**(`1f3d7458`、X3 の文字の階調がかすれる・斑点が出る #3400 の修正): **見送り**。新しいパネル(UC8279)の X3 での + 症状で、手元の X3(UC8253)ではテスト14 本文を 8 回送っても黒が保たれ斑点も出ない(写真で確認)。SDK の更新 + (cb9167d→7f6bd0f)を伴い、当版の階調の打ち切りと書き換えの順序が絡むため、次の本家のタグで fork し直す時点で一緒に入れる + +### 他フォークの確認と、抹茶版から2件の取り込み(2026-09-10、`2026091007-diag`、実機確認済み) + +9つのフォーク(本家・JP版・CJK版・Yomuka版・抹茶版・freeink-sdk・Papyrix版・Inx版・witchhunt版)の先端を取り直し、 +当版に無い修正を洗った。当版のコードで該当を確認し、効く条件は商業EPUB 955冊+Kobo形式 2,256冊で数えてから選んだ。 + +**取り込んだ2件**(どちらも抹茶版。配置の結果は変わらないので章の保存の版は据え置き): + +- **内蔵字形のページ枠を同じ書体で二重に取らない**(抹茶版 #218 相当、`FontDecompressor::prewarmCache`)。 + `getBitmap()` は `fontData` が一致した最初の枠で探索を打ち切るため、同じ書体の2枚目の枠は**永久に読まれない**まま + 4枠のうち1つを占め、別の書体が「全部埋まっている」で温められなくなる。1つの書体が見出し・本文・代替として + 複数の font id から届くので実際に起きる。既に枠がある書体は温めずに戻す(8行)。当版は本文がSDフォントなので主にUI側に効く +- **挿絵の保存が無いページで四角の下書きを描かない**(抹茶版 #220 相当、`EpubReaderActivity::renderContents`)。 + 四角を描いて1回書き換え、すぐ本番の描画で上書きしていた。電子ペーパーは前のページを保持するので、その1回 + (約0.5秒)は丸ごと省ける。**当版は章の組み立て中に `.pxc` を先に作るので、この経路は先作りが失敗したページしか通らない** + (`image-diag.txt` に `pregen ... FAIL` が出るページ)。本家の API である `renderWithImagePlaceholders()` と + `hasImagesNeedingDecode()` は定義を残してある + +**実機確認**(`2026091007-diag`、挿絵20ページの縦書きテスト書): 退行なし。`ui_prewarm_fail=0`、`prewarm_entry_fails=0`、 +`glyph_ondemand_last=0 max=7`、`page_draw_ms=156`(最大229)。挿絵は8枚とも `pregen stream ok` だったので、 +下書きの経路は今回1度も通っておらず、こちらの効果の実測は取れていない。 + +**挿絵ページが白黒→階調の2段階で出るのは仕組み上の挙動**(今回の変更とは無関係)。同じ診断の +`Page render (tiled): display=1053ms gray_lsb=2158ms gray_msb=35ms gray_display=226ms total=3848ms` がそのままで、 +白黒の土台を出す→2.2秒かけて4階調の面を組む(この間ずっと白黒版が見えている)→階調を重ねる、という順序。 +X3 のドライバは階調を当てる前に較正された白黒の土台を要求する(`Uc8253X3Driver::displayGrayscaleBase` のコメント)。 +**本家 #3439 を入れても変わらない**——あれは本文のアンチエイリアス経路だけで、PR 本文にも +「retain existing image-page handling」とある。**実用上の不都合は無いのでこのままとする**(2026-09-10、ユーザー判断)。 +縮めるなら (1) 土台を `displayBuffer(HALF/FAST)` から差分方式の `displayGrayscaleBase()` に替える +(当版の今のSDKに X3 向けの実装がある)、(2) SDK の `ed39106`(下記)の2案があり、どちらも計測から。 + +**見送り・不要と判断したもの**(調べた根拠つき): + +| 出どころ | 内容 | 判断 | +|---|---|---| +| freeink-sdk `ed39106` | X3(UC8253) の全面同期で電源が入っていても電源投入を送り、BUSY が下がらず約1秒空回りする | **有力候補として保留**。当版の SDK(cb9167d)にも同じコードがある。SDK main は本家の参照先より20件先で階調の大改造を含むため、丸ごと上げずに3行だけ当てるには SDK の fork が要る。診断で全面書き換えの時間を測ってから | +| witchhunt版 `5698985b` | 章内の飛び先が1ページ手前を指す(段落の1行目が次ページに送られた場合) | **該当あり・保留**。当版も `flushPendingAnchor()` で `completedPageCount` をその場で記録している。章内リンクを持つ本は 1,349/3,211(42%)。章の保存の内容が変わるので版上げが要る | +| Inx版 #115 | 長い段落の途中で一字下げが再発する | **該当あり・保留**。語数 320(本の書式使用時)/750 で分割したあと、`isFirstColumn`/`breakIndex==0` が再び真になる。320字超の段落を持つ本は 383/3,211(12%)。同じく版上げが要るので上と同時に | +| witchhunt版 `d4547ae6` | 章の飛び先表をSDへ逃がしてRAMを空ける | **不要**。当版は `span` の id を記録しないので、記録対象は1章あたり最大82個(3,211冊、中央値2、p99 27)。上限1024には遠く2KB程度 | +| Yomuka版 `fddb339` | 縦書きで小書きの仮名を上へ10%寄せる | **不採用**。JLREQ は縦組みで「字面を天地中央で右寄り」。当版は右へ1/8だけ寄せ上下は中央=仕様どおり | +| witchhunt版 `e0c99be6` ほか | 書式の #id 解決を省いて再解決を減らす | **該当なし**。当版の `resolveStyle` は tag/.class/tag.class しか引かない(`CssSelectorUsage`) | +| JP版 #123 | ルビが段落の最終行(縦書きは最終列)で消える | **該当なし**。当版は横書き側が存在する分だけ複写、縦書き側は `addVerticalToken` が常に長さを揃える | +| JP版 #129/Yomuka版 `99d0754` | 縦書きの本文中画像を語として流す | **実装済み**(2026-09-10、`2026091002-diag`) | +| Yomuka版 `2138120` | 書式の画像上限(max-width/max-height) | **実装済み**(保存形式 v13) | +| Yomuka版 `1e65fe8` | 圏点のゴマ点 U+FE45/FE46 を日本語フォントに入れる | **実装済み**。`cjk-symbols` の FE30–FE46 に含まれており(08-23 追加)、配布中の `fonts-202609080.zip` の3書体×Regular/Bold 全部に実体を確認 | +| Yomuka版 `dd8b64d` | SVGで包まれた画像の寸法を親の svg から取る | **保留**。当版も `` は読み、電書協型は内在寸法+画面合わせで正しく出る。該当ページを持つ本が多い(通常EPUB 552/955、Kobo形式 1,712/2,256)ので、不具合の報告が出るまで触らない | +| Yomuka版 `f8a9d0f` | PNG展開の作業領域をスタックからヒープへ | **不要**。当版の展開器は本体(約11KB+32KB)がヒープにあり、スタックに残るのは約3KB | +| CJK版 `1d6776b6` | zip の展開緩衝を 8KB→512B と段階的に縮める | **不要**。当版は 1KB で読んでいる | +| CJK版 `e9c66ee6` | 字形の予備リングに参照順の更新を入れる | **条件付き保留**。当版も順送りのみだが、返した `EpdGlyph*` の番地で予備かどうかを判定しているため、入れ替えると描画中の参照が壊れる。添字で持つ作りに直してから | +| JP版 #124 | zip の EOCD 解析の非整列読み | **不要**。X3(C3)で現に動作しており不具合ではない | +| 抹茶版 #214/#216 | 章の組み立て中の周波数の上げ下げ | **ほぼ対策済み**。当版は本組みが `render()` の中=電源保持の内側、先読みも 09-09 に保持を入れた | +| 抹茶版 `7523403a` | フォント配信画面が低メモリで異常終了する | **該当あり・低優先**。当版も `onEnter()` が無条件に `WiFi.mode(WIFI_STA)`(:73)。本を開いたまま入ると落ちうる | +| 本家 #2560 | HTTP配信のキャッシュ制御 | **無縁** | +| UI系全般 | JP版 #134–#138(横向き表示)、抹茶版 #230–#240(タッチ・辞書・図書館)、Papyrix版の時計とX3波形、CJK版のOTA、Inx版 | **非対象**(方針どおり) | + +### 字形の読み直しで遅くなるページの調査と修正(2026-09-10、`2026091021-diag`、実機確認済み) + +診断の `mini_free`(字形の一時記憶を捨てた記録)と `glyph_rebuild … total_ms`(積み直しに使った時間)が +セッション合計で12〜17秒に達しており、ページによっては表示に5〜15秒かかっていた。原因を追って複数箇所を +直したが、**効いたのは最後の1点だけ**だったので、経過も含めて残す。 + +**決め手: その場読みの置き場(overflow ring)が8枠しかなかった** + +一時記憶に載らなかった字は「その場読み」でSDから1字ずつ読み、予備の枠に置く。この枠が8個しかなかった。 +階調の描き分けは**短冊ごとに同じページを10回前後描き直す**ので、載らなかった字が二十数種類あると毎回 +押し出し合い、描き直すたびに全部読み直していた(1字約10ms)。枠を48に広げた(`OVERFLOW_CAPACITY`、 +16pt CJKで約4KB。枠は描画のたびに `clearCache()` で空になるので抱えっぱなしにはならない)。 + +同じ章(テスト24 本文)での実測: + +| 診断 | 前 | 後 | +|---|---|---| +| `aa_worst_lsb_ms … glyphs=` | 8,812ms / 699字 | **2,255ms / 0字** | +| `aa_worst_msb_ms … glyphs=` | 4,432ms / 378字 | **98ms / 0字** | +| `page_draw_ms` の最大 | 4,497ms | **464ms** | +| `render_max_ms` | 15,344ms | **4,917ms** | +| `glyph_ondemand` の最大 | 1,128 | **41** | +| `heap_min_free` | 5,364 | 5,604(悪化なし) | + +CJK版が同じ症状で 80→120 に広げていた(`e9c66ee6`)。当版は8のままだった。 + +**同時に入れた変更(効果は測れていないが筋は通る)** + +- 一時記憶の保持の床を 40KB→24KB(`MINI_RETAIN_MIN_FREE_HEAP`)。読書中の空きは36〜45KBで + この床をまたぐため、ページを描くたびに(`PrewarmScope` の生成→`clearCache()`)捨てていた。 + メモリを本当に要求する経路(章の組み立て・先読み・メニュー・フォント配信)は明示的に解放している +- 絵つきの一時記憶がある間は、metaOnly の要求(代替フォントに振り替わった文字列の測定)で + 作り直さない。作り直すとページの絵が捨てられる +- ページ自身の prewarm にも空きに応じた字数の上限を掛ける。従来は複数文字列の一括要求にしか + 掛かっておらず、積む途中で失敗して**全部捨てて**いた(空き5KBで76字ぶんを破棄した記録がある) +- 余裕の見込み(`PREWARM_HEAP_HEADROOM`)を16KB→8KB。16KBだと空きが少ないとき上限が0になり、 + 一時記憶を一切積まなくなっていた +- 走査の font 枠を 4→6(`MAX_SCAN_FONTS`)。上限を超えた font は黙って走査から落ちるため。 + 実測では `font_slots_lost=0` で該当しなかったが、保険として残す + +**回り道の記録(同日、いずれも撤回・修正済み)** + +- 階調の短冊用領域(7.9KB)を描画の先頭で確保するようにしたら、まさに一時記憶を積む時点で + 競合して**悪化した**。元の位置(階調に入る直前)に戻した +- 配置の門番(メモリ不足なら章の組み立てを失敗させる)が **abort を誘発した**。配置を諦めると + 語が消費されないので解析器が字を足し続け、`addVerticalToken` の push で落ちる。失敗と決まったら + 文字を受け取らないようにした。見積もりも過大で(380語に8.4KB要求)、縦は4B/語・床2KBに直した + +**診断に足した道具** + +- `glyph_miss=` … その場読みした符号位置の直近24件。**どの字が漏れているかで原因が割れる** + (本文の字=ページの積み込み失敗、数字やASCII=UI側、特定の書体だけ=走査の枠不足) +- `font_slots_lost=` … 走査の font 枠が足りずに落ちた回数 +- 遅い描画の記録(`input-diag-log.txt`)の先頭にも `glyph_miss=` を出す。定期報告は採取時 + (たいてい別の画面)の内容になってしまい、遅かったページの中身が分からなかった + +**残っている費用**(今回は手を付けていない) + +- 階調の描き分けそのものが1ページ約2.3秒。字形とは無関係で、書き換えの仕組み側の話 +- UI画面(一覧・メニュー)には一括プリウォームが無く、行ごとに1文字列ずつ積み直している。 + 旧ツリーの `UiGlyphPrewarm` 相当の移植が保留のまま + +### フォントを変えたあとの中断と極端な遅さ(2026-09-10、`2026091023-diag`、実機確認済み) + +前節の変更を入れた `2026091021-diag` で、**フォントを BIZ UDGothic に変えたところ中断(abort)した**。 +番地から引くと縦組みの列を作る `ParsedText.cpp` の `colRuby.assign()`、つまり **ふりがなの配列の確保**だった。 + +**原因1: 確保の失敗が報告にならない経路が残っていた** + +例外を使わない設定なので `std::vector` の確保は失敗すると報告ではなく firmware ごと停止する。前節で +段落規模の配列(字の高さ)と列の入れ物は「失敗しても止まらない」形にしたが、**1列ぶんの配列5本** +(本文・ふりがな・字体・座標2本)は素通しのままだった。1行・1列を作る前に**同じ大きさの領域を試しに +取って返す**関門を入れ、取れなければそのページを諦めて報告する(横組みの1行も同じ。失敗した行の語は +消費せずに残し、呼び出し側も止まるようにした)。 + +**原因2: 先読みの取り分と、保持の床が別々の数字だった** + +前節で `PREWARM_HEAP_HEADROOM` を16KB→8KB、`MINI_RETAIN_MIN_FREE_HEAP` を40KB→24KBにした。この2つが +食い違うと次のことが毎ページ起きる。 + +| 手順 | 結果 | +|---|---| +| 先読みが空き8KBまで使って一時記憶を作る | SDから何百字も読む | +| ページを描き終える | 空き9KB < 保持の床24KB → **作ったばかりの一時記憶を全部捨てる** | +| 次のページ | また最初から読み直す | + +診断の `Low heap opening menu (9184 free, 6388 max block)` が8KBの取り分とほぼ一致し、`glyph_miss=476`、 +1ページ7.8秒。**払った費用が毎ページ捨てられていた**。取り分を保持の床から導くようにして、二度と +ずれないようにした(`PREWARM_HEAP_HEADROOM = MINI_RETAIN_MIN_FREE_HEAP`)。作った一時記憶は必ず保持 +されるか、そもそも作らない。 -CrossPoint is open-source e-reader firmware - community-built, fully hackable, free forever. It's maintained by a growing community of developers and readers who believe your device should do what you want - not what a manufacturer decided for you. +**実測(BIZ UDGothic 12、同じ書籍・第13章)** -### Now running on: -- **ESP32C3-based** Xteink X4 and X3. -- **ESP32S3-based** Xteink X4Pro and X4Classic, Seeed reTerminal Sticky, M5PaperMono +| 診断 | `2026091021-diag` | `2026091023-diag` | +|---|---|---| +| 中断 | **abort(`colRuby.assign`)** | **なし**(176秒・挿絵ページと20分割の章組み立てを含む) | +| ページ送り | 4,565ms(打ち切りあり) | **1,704〜2,455ms** | +| `aa_worst_lsb_ms … glyphs=` | 2,144ms / 0字(押下で打ち切り) | 2,259ms / 0字 | +| `glyph_ondemand` の直近 | — | 49(最大513は章の組み立てを含む初回) | +| `heap_min_free` | — | 716(最も細ったところ。関門が受け止めて中断せず) | -Check [our Devices page](https://crosspointreader.com/devices) for the full list. +**分かったこと: このフォントは1ページ分の字形が物理的に入らない** -![CrossPoint Reader running on Xteink device](./docs/images/cover.jpg) +新しい診断行が出したのはこの2行。 -> If you're planning to buy an Xteink device, consider purchasing an **X3/X4 Developer Edition** through https://crosspointreader.com. CrossPoint receives a small share of each sale, helping fund development costs. +``` +font=BIZUDGothic_12.cpfont styles=2 advY=25 glyphs=24519 resident=6144 +prewarm_budget_min=66 wanted_then=67 free_then=47588 clips=9 +``` -## What can CrossPoint do? +空き47,588バイトから保持の床24KBを引いた23KBで**66字**しか置けない。つまり**1字あたり約350バイト** +(advY=25の2階調字形の実測平均)。1ページの異なり字数は約130なので、必要なのは約45KB——**空きとほぼ +同じ**で、描画に使う分を残すと入りきらない。入らなかった字はその場読みに回る(1ページ49字)。 +`clips=9` は先読みの上限に達したページ数。フォントを変える前に速かったのは、字形が小さく1ページ分が +収まっていたからで、**これはフォントの大きさが決める限界**であって直せる不具合ではない。 -- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, adaptive table layouts, native CJK ruby annotations, chapter navigation, footnotes, bookmarks, dictionary lookups ([StarDict](docs/dictionary.md)), go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more. +**診断に足した道具** -- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`. +- `font=` … 使用中の .cpfont の名前・字体数・字の高さ・収録字数・常駐量。「フォントを変えたら遅く + なった」という報告が、これまで診断だけでは読めなかった +- `prewarm_budget_min=` … 先読みが許された字数、必要だった字数、そのときの空き、上限に達した回数。 + **字形が大きくなったのか、組版が複雑になったのかを分ける**数字 -- **Touch reading**: follow EPUB links and look up words in the dictionary on touch-enabled devices. +**残っている費用**(今回は手を付けていない) -- **Screenshots.** +- 33×33の外字画像が**1ページの描画で10回以上再描画**されている(階調の短冊ごと)。1回約20msで約200ms +- 大きな章の初回オープンが20.8秒(うち章の組み立て12.5秒/20分割) -- **Custom fonts**: install your favorite fonts on the SD card. +### 書体を変えると1ページの文字数が変わる(2026-09-10、`2026091025-diag`、実機確認済み・章の版51) -- **Tilt page turn (X3 and Sticky)**. +フォント指定以外の設定をすべて同じにしても、1ページに入る文字数が書体で大きく違うという指摘。 +同じ書籍の同じ章で NotoSansJP が32ページ、BIZUDGothic と ExGothic が20ページだった。 -- **USB Drive mode (X4Pro)**: access the SD card as USB mass storage. +**原因: 縦書きの列の幅に、フォント内部の行送りをそのまま使っていた** -- **Library workflow**: indexed title/author search, recently-added and alphabetical views, multilingual grouping, folder browser, recent books, and SD-cache management. +配布中の .cpfont を直接読んだ値。 -- **Wireless workflows**: - - - File transfer web UI - - EPUB Optimizer - - Web settings UI/API (edit many device settings from browser) - - WebSocket fast uploads - - WebDAV handler - - AP mode (hotspot) and STA mode (join existing Wi-Fi), both with QR helpers - - Calibre wireless connect flow - - OPDS browser with saved servers (up to 8), search, pagination, and direct download - - OTA update checks and installs from GitHub releases +| ファイル | `advY`(焼き込まれた行送り) | 上へ | 下へ | +|---|---|---|---| +| BIZUDGothic_12 | **25** | 22 | −3 | +| BIZUDMincho_12 | **25** | 22 | −3 | +| NotoSansJP_12 | **36** | 29 | −8 | -- **Customization**: night mode, multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes including transparent overlays, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more. +同じ「12」でも1.44倍違う。三段階の仕様が重なってこうなる。 -- **Localization**: 34 UI languages and counting, including CJK font fallback and RTL support. +1. 大きさの設定は `<書体>_.cpfont` を選ぶだけで、実寸は変換時に焼き込み済み。変換は + `face.set_char_size(size, 150dpi)` で **全角25画素**(どの書体も同じ) +2. しかし `advanceY = norm_ceil(face.size.height)` を焼き込む。`face.size.height` は全角ではなく + **その書体自身が指定する推奨行送り**(`hhea` の上端・下端の合計)で、Noto Sans CJK は全角の + 約1.44倍、BIZ UD 系はほぼ全角ちょうど +3. 縦書きでは `columnWidth = getLineHeight()`(=`advanceY`)、列間はその1/4。つまり + **列送りが BIZ で全角の1.25倍、Noto で1.8倍**になっていた -### Coming soon: +全角幅は同じなので **1列あたりの字数は変わらず(どちらも19字)、列数だけが減る**。写真でもそのとおりだった。 -- More themes. +**直した内容: 列送りを全角基準にする** -- Web plugins. +JLREQ 2.3 は行送りを字の大きさに対する倍率(本文で1.5〜1.75倍)で定める。フォント内部の行送りを +使う仕様ではない。 -- Bluetooth pageturner. +- `GfxRenderer::getCjkCellWidth()` を新設し、**全角の幅をフォントから実測**する + (U+3000 →「あ」→「ア」→「国」の順。書体ごとに1回だけ測って覚える。字形がまだ読めない状態なら + 覚えずに次回やり直す) +- 列送り=**全角 × 1.5 × 行間設定**(`VERTICAL_COLUMN_PITCH_EM`)。行間設定の四段階(0.95/1.0/1.1/1.2) + と合わせて1.43〜1.8倍となり、JLREQ の範囲を覆う +- 文字の列・挿絵の列(1文字大か独立した列かの判定を含む)・**描画側の升の幅**を、すべて同じ値から求める +- 縦中横の判定にあった `行送り × 2/3` という全角幅の代用も本物に置き換え -- Much more! stay tuned. +**実測(同じ書籍・プロローグ、章の保存を作り直した後)** ---- +| 書体 | 前 | 後 | +|---|---|---| +| NotoSansJP | 32ページ(7列) | **28ページ(9列)** | +| BIZUDGothic | 20ページ(11列) | **28ページ(9列)** | +| ExGothic | 20ページ(11列) | **28ページ(9列)** | -## USB-locked devices (Xteink Unlocker) +列の切れ目・行頭行末・ふりがなの位置まで3書体で一致。12ptなら列送りは全書体38画素になる。 +BIZ で読んでいた密度からは約19%ゆるくなる(1.25倍→1.5倍)ので、詰めたい場合は行間設定の「狭い」で +1.43倍まで戻せる。 -Some Xteink units purchased from third-party stores (e.g. AliExpress) ship with USB flashing locked from the factory. -If your device is locked, you will need to use the **Xteink Unlocker** tool available at -https://crosspointreader.com/#unlock-tool before you can flash CrossPoint. +**横書きは変更していない。** 欧文には全角という単位が無く、行送りをフォントの上下端から取るのは +横書きでは正しいため。縦書きは日本語専用なので全角基準にできる。 -**You do not need this tool if you bought your device directly from xteink.com.** Those units are not locked. +**調べたが直す必要が無かった点**: 場面の区切り(`
`)が足す余白はフォントの行送り基準のままだが、 +縦書きでは `verticalIndent = max(textIndent, -topInset)` と負の値との比較にしか使われないため、 +列の頭下げには影響しない(負の字下げを列の外へはみ出させない歯止めとしてのみ働く)。 -**Not sure if your device is locked?** Power it on, connect the USB-C cable, and try flashing via the web flasher first (see -[Install firmware](#install-firmware) below). If the browser's serial device picker does not show your device, try a different -USB port or browser before assuming the device is locked. Only reach for the unlocker if the device still doesn't appear. +### 挿絵のログが診断を潰す/小さい外字がRAMに載らない(2026-09-11、`2026091101-diag`、実機確認済み) -> ### ⚠️ WARNING: READ THIS BEFORE USING THE UNLOCKER ⚠️ -> -> **The only officially supported firmwares in the unlock tool are CrossPoint and CrossInk.** -> -> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently -> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if -> the firmware you flashed doesn't support OTA, **there is no way out**. +「挿絵が1ページで10回以上再描画されている」として調べたが、**再描画はしていなかった**。診断が費用を +直接測っていて、`aa_worst_lsb … img_slot=0/0 stream=1 sd=8ms` — 1面1回・8ms。ログの間隔15〜23msは +その短冊全体を描く時間だった。短冊に掛からない挿絵は `glyphIntersectsStrip()` が既に飛ばしている。 +かわりに2点見つかった。 -## Install firmware +**① 「Rendering image at …」を短冊の判定より前に出していた** -### Web installer (recommended) +階調は短冊ごとに全画像を一巡するので、判定より前だと1ページで12行出る。ログ環は16行しかないので、 +**採取の目的だった内容がすべて押し出されていた**(2026-09-10 の遅い描画の記録は16行中12行がこの1行 +だった)。判定の後ろへ移した。 -1. Connect your device to your computer via USB-C and wake/unlock the device -2. Go to https://crosspointreader.com/#flash-tools, select your device (X3, X4, Xteink X4Pro, Seeed reTerminal Sticky, or M5PaperMono), and choose an official CrossPoint release. +| | 前 | 後 | +|---|---|---| +| 1ページの画像のログ | 12行(描画は1回) | **4行(描画4回)=1行につき必ず描画が伴う** | +| 記録に残った内訳 | なし | `AA split` / `Page render (tiled)` / `Rendered page in` / `Progress saved` | -### Web installer (specific version) +**② RAM置き場の門番が、要求量に関係なく空き24KB・最大確保8KBを求めていた** -1. Connect your device to your computer via USB-C and wake/unlock the device -2. Download the firmware file for your device from [Releases](https://github.com/crosspoint-reader/crosspoint-reader/releases), or compile yourself. -3. Go to https://crosspointreader.com/#flash-tools, select your device, click "Custom .bin" and upload the firmware file. +置き場(`pxcSlot`)は画面いっぱいの挿絵(約93KB)を想定した数字で門番を作ってあった。ところが +33×33の外字は2階調で **297バイト**しか要らない。空きの少ないページほど断られる——SDに戻る費用が +一番高い場面で断られていた。4KB以下の要求には4KBの余裕で足りるようにした(大きい画像の条件は不変)。 -### Revert to Official Firmware - -To revert to the official firmware, you can also flash the latest official firmware using https://crosspointreader.com/#flash-tools. +``` +前: img_slot=0/0 stream=1 一度も載らない +後: img_slot=1/0 stream=4 白黒の回で載り、階調の回は当たる +``` -### Command line +**残っている費用**: 置き場は1枚しか持てないので、外字が複数あるページは2枚目以降がSDへ戻る +(実測1面32ms、1ページ65ms=ページ全体4,156msの約1.6%)。数枚持てるようにすれば消せる(外字1枚 +300バイト)。効果が小さいので保留。 -1. Install [`esptool`](https://github.com/espressif/esptool): +**この節の教訓**: ログの間隔を費用と読み違えた。診断に数字がある場合はそちらを先に見る。 -```bash -pip install esptool -``` +### 章の組み立てとページ送りの費用を測る(2026-09-11、`2026091105-diag`、実機測定のみ・機能変更なし) -2. Download the firmware file for your device from the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases). -3. Connect your device via USB-C. -4. Find the device port. On Linux, run `dmesg` after connecting. On macOS: +「大きな章の初回オープンが20.8秒(章の組み立て12.5秒/20分割)」を追った。**推測で手を付ける前に +内訳を出す**方針で、診断に3行足して測った結果、**組み立てには狙うべき一点が無く、ページ送りのほうの +最大の費用は計測の区切り方の誤りだった**。 -```bash -log stream --predicate 'subsystem == "com.apple.iokit"' --info -``` +**組み立て: 挿絵の展開が全部だった** -5. Flash an X3 or X4: +| 同じ章(20ページ)| 挿絵の画素保存なし | 保存あり | +|---|---|---| +| `build_total_max_ms` | 3,784ms | **1,028ms** | +| `build_page_write` | 100ms | 58ms | +| `build_font`(文字幅表のSD読み) | 203ms | 54ms | +| 残り(構文解析・書式照合・配置) | ~3,480ms | **~916ms** | -```bash -esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin -``` +差の2,756msは 480×360 の挿絵1枚あたり約470ms×8枚。設計どおり(描画時は空きが足りず失敗するので +組み立て時に作る)。本文側は**1ページ約46ms**で、書き出しも文字幅表も誤差。 - Flash an Xteink X4Pro, Seeed reTerminal Sticky, or M5PaperMono: +事前に立てた仮説「文字幅表が768字で打ち止めになり、以後は測るたびに1字ずつ読む」は**外れ**だった。 +実測は `table 13/768 full_skips 0`。打ち止めには全く届いていない。 -```bash -esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin -``` +商業書籍の127ページの章の12.5秒もこの単価の積み上げで、しかも章ごとに1回きり・次の章は待機中に +先組みしている。20.8秒になったのは章の版を50→51に上げて全部作り直させたため。**ここで打ち切る。** -### Manual +**ページ送り: `gray_lsb` の中に書き換え待ちが入っていた** -See [Development quick start](#development-quick-start) below. +1回目の階調が2,293ms、2回目が140ms。同じ処理なのに60倍という差が長く残っていて、コード中の注記も +「1回目が何かを暖め、2回目はそれを見つける」と書いていた。繰り返しを「短冊をRAMで組む」と +「パネルへ送る」に分けて測ると—— ---- +``` +AA split: lsb=2293ms | msb=140ms +AA phases: lsb draw=97ms push=36ms | msb draw=99ms push=41ms +``` -## Custom SD-card fonts +2回目は 99+41=140 でぴったり。1回目は 97+36=133 しかなく、**2,160msが繰り返しの外**にあった。 +その正体は階調に入る直前の `renderer.waitRefreshComplete()`——**白黒の書き換えの完了待ち**で、 +これが `gray_lsb` の計測区間に含まれていた。1回目と2回目の処理量は同じで、差は最初から無かった。 -On devices with external RAM enabled in CrossPoint, copy `.ttf`, `.otf`, or `.ttc` files to the SD card and select them as reader fonts. Put one file in `/fonts/` or `/.fonts/`, or put one family's files in a subfolder. See the [SD card font guide](./docs/sd-card-fonts.md) for the folder layout and styles. +読み直したページ送りの内訳: -On other devices, convert the font to `.cpfont` first. `.cpfont` files also work on devices with external RAM enabled and have better performance. No firmware reflash is needed to add fonts. +| 項目 | ms | 種別 | +|---|---|---| +| prewarm | 619 | CPU | +| bw_render | 132 | CPU | +| display | 1,080 | **パネル** | +| 書き換え待ち | 2,160 | **パネル** | +| 階調1回目 | 133 | CPU | +| 階調2回目 | 140 | CPU | +| gray_display | 228 | **パネル** | +| cleanup | 78 | CPU | +| 合計 | 4,570 | パネル 3,468(**76%**) | -To make `.cpfont` files: +**階調の描画そのものは273ms**。以前この節に書いた「階調の描き分けそのものが1ページ約2.3秒」は +誤りで、その2.3秒は書き換え待ちだった。 -1. Go to https://crosspointreader.com/fonts and open the "SD-card font builder" form. -2. Upload up to four styles (regular, bold, italic, bold-italic), set the family name, point sizes, and Unicode range. -3. Download the generated `.cpfont` files. -4. Copy them to your SD card under `/fonts/YourFont/` (or `/.fonts/YourFont/` to hide the folder). -5. Select the font on the device from the font settings. +計測の区切りを直し、`AA split` と `Page render (tiled)` に `wait=` を分けて出す。診断にも +`aa_refresh_wait= (max )` を追加。 -Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py` script unmodified, so output matches a local host build. +**次の行き先**: CPU側でページ送りから削れるものはもう多くない(prewarm 619ms が最大)。パネル側の +3.5秒が本体で、後回しにしているSDKの `ed39106`(X3 UC8253 の余計な電源投入、全面書き換え1回あたり +約1秒)がそこに直接効く。なお全面書き換えを待たずに階調を組んでおく経路は既にあるが +(`lsbPlaneBuf`)、1面47.5KB+余裕60KBで**空き107KBを要求する**ため、読書中に40〜70KBしかない +X3では一度も走らない。 ---- +**この節の教訓**: 計測の区切りが誤っていると、同じ誤りを何度でも追う。区間の中に待ちが入って +いないかを最初に疑う。 -## Documentation +### 本家 develop の併合と、右端のルビ(2026-09-11、`2026091115-diag`、実機確認済み・章の版53) -- [User Guide](./USER_GUIDE.md) -- [Web server usage](./docs/webserver.md) -- [Web server endpoints](./docs/webserver-endpoints.md) -- [Project scope](./SCOPE.md) -- [Contributing docs](./docs/contributing/README.md) -- [Touch and UI development](./docs/contributing/touch-and-ui.md) - how to build new screens on the FreeInkUI activity bases (UiListActivity and friends), plus build envs for the non-Xteink touch devices +ページ送りの費用がパネル側に偏っていることが前節で分かり、そこに効く SDK 修正 `ed39106` +(X3 UC8253 の余計な電源投入)を取りに行った。調べると**本家は 2026-09-10 に取り込み済み**で、 +Yomuka版・抹茶版は未取り込みだった(SDKの固定先が全て 08-23 以前)。当版が独自に判断する必要は +なく、本家に追随すればよい話だった。 ---- +**再fork は不要だった** -## Development quick start +方針は「本家のタグが更新された時点で fork し直す」だが、今回は**素直な併合で足りた**。 -### Prerequisites +| | ファイル | 行 | +|---|---|---| +| 当版が加えた分(1.6.0 から) | 80 | +10,586/−589 | +| 本家が進めた分 | 106 | +2,236/−585 | +| **両方が触ったファイル** | **15** | — | -- [pioarduino PlatformIO Core](https://github.com/pioarduino/platformio-core) or [VS Code + pioarduino IDE](https://github.com/pioarduino/pioarduino-vscode-ide) -- Python 3.8+ -- `clang-format` 21 -- USB-C cable supporting data transfer +試験併合の結果は **104ファイルが自動併合、衝突は5ファイル8箇所・142行**。**縦書きの中核 +(`VerticalTextUtils` `TextBlock` `ParsedText` `Section` `CssParser` `PngStreamDecoder`)は本家が +一切触っておらず衝突ゼロ**で、当版の1万行の大半は本家の変更と物理的に重ならない。 -### Setup +衝突の解決: `Epub.h`/`ReaderUtils.h` は当版の縦書き用の宣言と本家の引数変更を両立、 +`ImageBlock.cpp` は当版の `hasValidCache()` 分離とログ位置を維持、`ChapterHtmlSlimParser.cpp` は +当版の診断と本家の `hidden` 属性対応を両方、`EpubReaderActivity.cpp` は本家の +`chapterPosition()` の整理を採って当版のメニューの低メモリ保護と nothrow 生成を載せ直した。 -```bash -git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader -cd crosspoint-reader +**文字として併合できても意味は保証されない**: ビルドが `HalGPIO::rawInputActive()` の二重定義を +検出した。#3463 を当版が先に取り込んでいたため、本家側の同じ実装が別位置に入っていた。 -# if cloned without --recursive: -git submodule update --init --recursive -``` +**入ったもの** -### Nix/NixOS +- **SDK `cb9167d` → `fde240fa`(29コミット)**。本命は `ed39106`「電源が入っているときは + POWER_ON を繰り返さない」。余計な PON は BUSY が下がらないため待機が 1000ms の上限まで回る +- **#3439** X3の階調の安定化。X3 は `asyncBase=false` となり、階調の組み立てを白黒の書き換えに + 重ねる経路が無効になる(**X3では安全でない**と本家が判定した。前節で「要求メモリを見直せば + 使える」と書いたのは誤りで、使ってはいけない経路だった)。当版の短冊方式は `stripUploads=true` + でそのまま残る +- **#3478** 階調機能の判定を `GrayscaleCapabilities` に集約 -Nix/NixOS users can enter the development shell with either `nix develop` (flakes) or `nix-shell`: +**実測(本文ページ)** -```bash -nix develop -f nix -# or -nix-shell nix -``` +| 項目 | 併合前 | 併合後 | +|---|---|---| +| 書き換え待ち(`aa_refresh_wait`) | 2,120ms | **1ms** | +| 白黒の表示 | 1,053ms | **579ms** | +| 階調 1回目+2回目 | 2,192ms | **220ms** | +| **ページ全体** | **3,773ms** | **1,601ms** | -To flash a connected ESP32-C3 device, enable PlatformIO's udev rules in your NixOS configuration: +**見送った本家の更新**: #3390(`hidden` 属性)は手持ちの商業書籍513冊を走査して**0冊・0箇所**、 +効果がない。#3036(macOS由来のNFDファイル名)は中身がラテン系と結合ハングルのみで、**日本語の +濁点(U+3099/U+309A)は対象外**。同じ問題は日本語にもあるので、ファイル一覧で「か゛」を +見かけたら同じ関数を広げるのが対処になる。#3437 はUI(方針により見送り)。 -```nix -services.udev.packages = with pkgs; [ platformio-core.udev ]; -``` +**右端のルビが画面端で切れていた** -After rebuilding the system configuration, reconnect the device or reload udev rules. +併合とは無関係の、縦書き移植(段階5)以来の不具合。ルビは列の右隣に描かれるが +(`TextBlock.cpp` の `rubyX = cellX + cellWidth`)、ページの一番右の列は本文領域の右端に +密着していたため、そのルビが領域外に出ていた。 -### Build / flash / monitor +最初はルビ幅(升の半分)をそのまま本文領域から引いたが、診断で**1ページから列を1つ丸ごと +奪っている**ことが分かった。 -```bash -pio run --target upload ``` - -### Contributor pre-PR checks - -```bash -./bin/clang-format-fix -pio check -e default -pio run -e default +vert_layout=cell 38 pitch 57 ruby_reserve 19 viewport 512x756 -> 8 cols + 確保なし (512-38)/57+1 = 9列 + 確保19 (512-19-38)/57+1 = 8列 ← 境目は18。1画素超えて11%の文字数を失っていた ``` -### Debugging +ルビは**利用者が設定した画面余白の上に出て構わない**(そこは実際にパネル上にある。枠の下に +隠れる bezel の分は使えない)。確保量を「ルビ幅 − 画面余白」にして、既定なら 19−5=14。 +**9列を保ったままルビが完全に出る**。実機で確認済み(ページ数 206→153)。 -After flashing the new features, it’s recommended to capture detailed logs from the serial port. - -First, make sure all required Python packages are installed: - -```python -python3 -m pip install pyserial colorama matplotlib -``` +**この節の失敗**: 確保量を直したとき、配置が変わるのに章の保存の版を据え置いた。「まだ配布して +いないから」と判断したが一つ前の版は配布済みで、実機はその配置で保存を作っていた。修正が一度も +実行されず、診断の `vert_layout=cell 0 … -> 0 cols`(組み立てが走っていない)で発覚した。 +**配置に触ったら版を上げる**。 -After that run the script: +**診断に足した道具** -```sh -# For Linux -# This was tested on Debian and should work on most Linux systems. -python3 scripts/debugging_monitor.py +- `vert_layout=cell <升> pitch <列送り> ruby_reserve <右端の確保> viewport <幅x高さ> -> <列数>` + 縦書きのページがどの寸法で組まれたか。フォントの大きさ・余白・行間の設定と列数を突き合わせられる +- `font=` 行が「最後に読み込んだフォント」を出していたのを本文用に直した。UI用の代替サイズが + 後から読み込まれるため、本文が18ptなのに `font=ExGothic_12` と出ていた +- `layout_giveup=<回数> last=kind …` 配置を諦める6か所のどれが断ったか +- 失敗時の記録が、書き出し待ちの「遅い描画」の記録に塞がれていたのを、追い出せるようにした + (`flushNow()` で即座に書き出す) -# For macOS -python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101 -``` +### 挿絵の初回展開が1枚8〜17秒(2026-09-11、`2026091121-diag`、実機測定・上限は保管層と判明) -Minor adjustments may be required for Windows. +商業書籍の 1440×2048 の挿絵を初めて開くと1枚8〜17秒かかる。1画素あたり約3,700サイクルに見えたので +JPEGの扱い方を疑ったが、**復号そのものは妥当で、時間はファイルの読み書きに消えていた**。 ---- +**内訳を測る** -## Internals +`pregen` の ms が展開(zipからの取り出しとSDへの書き出し)と復号を1つの数字にしていたので分け、 +復号の中も分けた。 -CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based on this constraint. +``` +pregen extract ok ms=8563 +jpeg split total=8283 io=4270/846 draw=343 flush=901 prog=0 scale=1/2 +``` -### Data caching +| 項目 | ms | 割合 | +|---|---|---| +| 展開(zipから取り出してSDへ) | 8,563 | **50%** | +| JPEGDEC の読み込み(846回) | 4,270 | **25%** | +| JPEGDEC 自身の復号 | 2,769 | 16% | +| 画素保存の書き戻し | 901 | 5% | +| 拡縮+網点 | 343 | 2% | -The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the -cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows: +累進形式ではなく(`prog=0`)縮尺 1/2 も妥当。**JPEGの扱い方に問題は無かった**。復号2,769msは +1画素あたり約600サイクルで、この機械としては妥当な値。 -```text -.crosspoint/ -├── epub_/ # one directory per book, named by content hash -│ ├── progress.bin # reading position (chapter, page, etc.) -│ ├── cover.bmp # generated cover image -│ ├── book.bin # metadata: title, author, spine, TOC -│ ├── css_rules.cache # parsed CSS rule cache -│ ├── img_* # rendered image cache files -│ └── sections/ # per-chapter layout cache -│ ├── 0.bin -│ ├── 1.bin -│ └── ... -├── settings.json # device settings -├── state.json # resume/runtime state -└── recent.json # recent books list -``` +**往復を減らしても変わらなかった** -Removing `/.crosspoint` clears all cached metadata and forces a full regeneration on next open. Book deletes, overwrites, and moves done through the firmware or web UI clear or re-key matching caches; manual SD-card edits may leave stale cache directories behind. +JPEGDEC は1KB前後ずつ要求してくるので、8KBの緩衝を1枚挟んだ。 -For more details on the internal file structures, see the [file formats document](./docs/file-formats.md). +| | 前 | 後 | +|---|---|---| +| 読み込み | 4,270ms/846回 | 3,825ms/**163回** | +| 展開 | 8,563ms | 7,914ms | ---- +**回数は1/5になったのに時間は7%しか減らない**。163回×8KB=1.3MBを3,825msで読んで**340KB/s**。 +展開は1.4MBで7,914ms=**180KB/s**。緩衝の補充が毎回呼んでいた `seek` も外したが変わらなかった。 -## Contributing +別の経路でも同じだった。表紙のサムネイル生成は SD への書き出しが無いのに—— -Contributions are welcome. If you're new to the codebase, start with the [contributing docs](./docs/contributing/README.md). For things to work on, check the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) — leave a comment before starting so we don't duplicate effort. +``` +[ZIP] Decompressed 633921 bytes into 633770 bytes ← 3,886ms = 163KB/s +``` -Everyone here is a volunteer, so please be respectful and patient. For governance and community expectations, see [GOVERNANCE.md](./GOVERNANCE.md). +151バイトしか縮まない(=実質そのまま通すだけの)データで163KB/s。**保管層そのものが +160〜340KB/s**で、SDのSPIは既定の40MHz(5MB/s相当)なので**数%しか出ていない**。挿絵の経路を +どう調整してもここが頭打ちで、同じ割合が字形の読み込み・画素保存・章の保存・サムネイルにも効く。 ---- +**この節の失敗**: 展開の塊を 4096→16384 に広げたら、deflate 済みの項目は塊を**2つ** malloc する +ため32KB連続を要求し、最大32KBの状況で**外字の取り出しが失敗した**(`pregen extract FAIL`)。 +実測で塊の大きさがほぼ効かないと分かっていたので 8192(`extractItemToFile` と同じ実績値)へ戻した。 -## Community forks +**あわせて直したもの**: `readerRenderSpec()` の呼び出し3か所のうち `rightMargin` を設定していたのは +1か所だけで、待機中の先組みが組んだ章だけルビの確保が19(前景は14)になり、**章によって1ページの +列数が変わっていた**。設定由来の値なので `readerRenderSpec()` 自身に入れた。 -One of the best things about open source is that anyone can take the code in a different direction. If you need something outside CrossPoint's [scope](./SCOPE.md), check out the community forks: +**残っている費用**: 表紙のサムネイル生成が Home 画面を8.4秒止める(展開3.9秒+復号3.0秒)。 +挿絵と同じ原因。保管層の速度は別途の調査対象。 -- [CrossInk](https://github.com/uxjulia/CrossInk) — UX focused with minimal reading stats and broader customizations for the reading experience. +### 保管層が遅い原因は SdFat の1バイト転送だった(2026-09-11、`2026091124-diag`、実機確認済み) -- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes. +前節で挿絵の時間の75%がファイルの読み書きに消えており、経路を問わず**160〜340KB/s**しか出ていない +ことが分かった。40MHz のバスは MB/s 級を運べるはずで、桁が合わない。 -- [inx](https://github.com/obijuankenobiii/inx) — Completely reimagines the user interface with tabbed navigation. +**外したほうの仮説**: X3 の SD は画面と同じ SPI バス(GPIO 8/10/7)を使い、C3 の SPI2 が直結で使える +のは 2/6/7/10 なので SCLK 8 は GPIO マトリクス経由。ESP-IDF はこの経路を読み出し26.6MHzと定めており、 +SDK は既定で40MHzを要求している——タイミング余裕が無く再試行しているのでは、と考えて20MHzを試した。 -- [Witch(hunt) Reader](https://github.com/jpirnay/witchhunt-reader) — More faithful CSS styling and background work for slightly snappier interaction. Weather information panel. Markdown support. +| クロック | 634KBの展開 | +|---|---| +| 40MHz | 3,886ms | +| 20MHz | 4,342ms | -**Note:** Many of these features will make their way into CrossPoint over time. Each project chooses its own priorities and tradeoffs. +**半分にしても10%しか遅くならない**。純粋にクロック律速なら半分になるので、転送時間は全体の約1割で +残り9割は1回ごとの手間、と分かった。既定に戻し、`sd_clock_hz=` を診断に残した(どのクロックで出た +数字かが分からないと比較できない)。 -Want to build your own device? Be sure to check out the [de-link](https://github.com/iandchasse/de-link) project or [OnePage Reader](https://github.com/MoveCall/onepage-reader). +**当たったほう**: `SdFatConfig.h` は ESP32 では `USE_SPI_ARRAY_TRANSFER` を **0** にする +(RP2040 と UNO R4 のみが例外)。0 のとき `SdSpiArduinoDriver::receive` は—— ---- +```cpp +for (size_t i = 0; i < count; i++) buf[i] = m_spi->transfer(0XFF); +``` -CrossPoint Reader is **not affiliated with Xteink or any device manufacturer**. +**1バイトずつ**転送する。Arduino-ESP32 の `transfer(uint8_t)` は毎回データレジスタを書いて起動し +完了を待つので1バイト約2.8µs、8KBで23ms——実測の**23.5ms/8KB とぴたり一致**する。40MHzのバス自体は +8KBを0.2msで運べるので、**99%が1バイトごとの手間**だった。クロックを半分にしても10%しか変わらなかった +のも、転送が元々ごく一部だったからで辻褄が合う。 + +`platformio.ini` に `-DUSE_SPI_ARRAY_TRANSFER=1` の1行(送信側で512バイトのスタック領域を使う)。 + +**実測(同じ1440×2048の挿絵)** + +| 項目 | 前 | 後 | +|---|---|---| +| 展開(zip→SD) | 7,914ms | **3,089ms** | +| JPEGDEC の読み込み | 3,825ms/163回 | **1,335ms**/163回 | +| 復号全体 | 7,882ms | **4,979ms** | +| **挿絵1枚の合計** | **約16,000ms** | **約8,139ms** | +| 外字(128×128)の展開+変換 | 169ms | **90ms** | +| 8KBあたりの読み | 23.5ms(340KB/s) | **8.2ms(1MB/s)** | + +副次: `page_prewarm_ms` 363〜590 → **154**、`glyph_rebuild total_ms` 400〜1,600 → **175**。 +**保管層を通る全部**(字形・挿絵・章の保存・zip の取り出し・表紙のサムネイル)に効く。 + +あわせて、表紙のサムネイルの取り出しだけ塊が 1024 だったのを 8192 に揃えた(634KB に対し620回の +往復になっていた。Home 画面を8.4〜9.2秒止めている経路)。 + +**これは X3 固有ではなく ESP32 全般の話**で、本家・他フォーク・SDK も同じ既定で動いているはず。 + +**この節の失敗**: 展開の塊を 4096→16384 に広げたら、deflate 済みの項目は塊を**2つ** malloc するため +32KB連続を要求し、最大32KBの状況で外字の取り出しが失敗した(`pregen extract FAIL`)。塊の大きさが +ほぼ効かないことは実測で分かっていたので 8192 へ戻した。**塊を増やす前に、その塊がいくつ確保されるかを +見る**。 + +### 章内の飛び先のページと、長い段落の途中の字下げ(2026-09-10、`2026091011-diag`、実機確認済み・章の版50) + +他フォークの確認で見つけた2件を直した。どちらも章の保存の中身が変わるので `SECTION_FILE_VERSION` を +49→50 に上げてある(更新後の初回オープンで全書籍の章が作り直しになる)。 + +**1. 章内の飛び先が1ページ手前を指す**(witchhunt版 `5698985b` と同じ不具合) + +`flushPendingAnchor()` は飛び先(id)を「その要素が現れた時点の `completedPageCount`」で記録していた。 +段落の1行目がページに入りきらず次へ送られる場合、記録は1つ手前のページのままになる。注のリンクから +飛ぶと目当ての段落が画面に無く、ページを1つ送って初めて現れる、という形で出る。目次の飛び先だけは +記録の前にページを切るので無事だった。 + +飛び先を待ち行列(上限16)に積み、`addLineToPage`/`addColumnToPage`/挿絵の配置2箇所/罫線の配置で +「その行が実際に載ったページ」として確定するようにした。1行も生まないまま章が終わった場合は最後の +ページに割り当てる。上限1024の数え方も待ち行列のぶんを足す。縦書きの表は当版では各セルの行が +`addLineToPage` を通るので追加の対応は不要だった。 + +**2. 長い段落の途中で一字下げが再発する**(Inx版 #115 と同じ不具合) + +語数が上限(本の書式を使うとき320語、使わないとき750語)を超えた段落は何回かに分けて配置されるが、 +続きの回も「段落の一行目」と見なされて字下げが入っていた。`ParsedText` に「この段落は既に行(縦書きは列) +を出したか」を持たせ、横書きの `resolveFirstLineIndent` と縦書きの `verticalIndent` の両方で見る。 +行分割の計算(`computeLineBreaks`/`computeHyphenatedLineBreaks`)も同じ判定を通るので、測る幅と描く幅が +ずれない。 + +**テスト資産**: `vertical-test-suite.epub` にテスト23(長い段落の途中の字下げ)とテスト24(章内リンクの +飛び先ページ)を追加した(生成は `test-assets/corpus-survey/patch_suite6.py`、epubcheck 5.3.0 で追加分に +エラーなし)。テスト23は書式指定 `text-indent:3em` の1,464字の段落と、自動の一字下げに任せる1,438字の段落の +2本立てで、読書設定の段落間の空きに左右されずに確かめられる。テスト24は最初のページに01〜24のリンク、 +その下に長さを25〜104字で変えた24個の目印段落を置き、どれかの段落の頭がページの境目に当たるようにしてある。 + +**実機確認**(`2026091011-diag`): +- テスト23: 1/24 で〔見本〕と〔1〕の2箇所だけが3文字ぶん下がる(16字=19字−3)。2/24 は先頭列が19字=満杯で、 + ページと区切りをまたいでも下がらない +- テスト24: **07 は 5/14、14 は 9/14** に着地。どちらも【NN】が着地ページの右端の列の先頭=以前ずれていた形。 + 列の途中から始まる06は元から正しく4/14 +- 注意: 最初の試行では07・14とも1つ手前に着いた。作り直し後は正しくなったので、前の状態の章の保存 + (部分保存か、別ビルドで組んだ配置)が残っていたものと見られる。再発したら `e-ink/dump_section_anchors.py` + で章の保存の飛び先表を直接読み、記録側か読み出し側かを切り分けられる + +### 配置がメモリ不足で落ちるのを止める(2026-09-10、`2026091011-diag`) + +`2026091008` の実機でページ送り中に異常終了した。ELFで逆引きすると発生源は +`ParsedText::layoutVerticalColumns` の `wordHeights.reserve`(`ParsedText.cpp`、2026-09-06 の縦書き移植から +あるコード)で、`-fno-exceptions` では `operator new` が失敗時に nullptr を返さず `std::terminate` を呼ぶ。 +直前のログに「1024バイトの確保に失敗」が出ており、原因は空きメモリの枯渇。26秒前には +`Low heap opening menu (5340 free, 2164 max block)` も出ていた。 + +- **配置に入る前に見積もりと空きを比べる**。縦書き6バイト/語・横書き12バイト/語(行分割の dp と ans が + 4バイトずつ要るため横が高い)+6KBの床。足りなければその章の組み立てを失敗として返し、`abandonBuild()` で + 保存を書かずに終わる。**落ちる代わりに「次に開いたときに作り直し」**になる +- `wordHeights` を nothrow の配列に、列の `TextBlock` も nothrow 生成+`valid()` 確認に +- **区切り(soft flush)の取りこぼしを直した**。判定を語を足す途中でも行う。従来は文字の塊を読み終えてから + しか見ないため、上限320語に対して648語まで溜まり、配置が確保する山が倍になっていた。 + 最初の修正では語の切れ目(`partWordBufferIndex == 0`)に置いたが、空白の無い日本語は200バイトの + 溜め込みが埋まったときにしか字を渡さず、しかも3バイトの字が境目をまたぐと溜め込みが空にならないため + ほぼ発火していなかった(`words_max=471` で判明)。溜め込みが埋まった直後にも判定するようにした +- 診断版に `build_headroom_min=` を追加(配置に入る直前の空き・最大連続・語数の最小値と、扱った語数の最大値) + +**実機確認**: `2026091010-diag`(10分の読書)で `build_headroom_min=13920 max_alloc_then=10228 words_then=447`。 +447語なら見積もりは8.8KBなので約5KBの余裕を残して通っており、門番が誤って章を拒否してはいない。落ちた章 +(テスト23本文)が今度は最後まで組み上がった。続く `2026091011-diag` でテスト23本文を組み直させたところ +`build_headroom_min=24360 max_alloc_then=18420 words_then=380` で **`words_max=471`→`380`**。上限320に対する +超過が151語から60語(溜め込み1杯=約66字)に収まり、区切りの修正が効いていることを確認した。 +`heap_min_free` は 3,756/8,188/2,920 バイトまで落ちており、**落ちなくなっただけで狭さ自体は残っている** +(`OOM: grayscale strip scratch (7920 bytes); skipping AA this page` や +`132 glyphs loaded on demand in the BW pass; skipping AA this page` が出る)。 + +同じ診断から次の候補: 字形の一時記憶の作り直しにセッション合計18.6秒(`glyph_rebuild ... total_ms=18656`、 +`mini_free=120`)、および「BW描画で128字を1字ずつSDから読み階調を諦めた」6秒のページ(`aa_aborts=2`)。 + +### 章の配置の保存の版を上げる(2026-09-09、未配布・次のリリースに含める) + +商業書籍の本文中の外字画像「№2」(`img.gaiji`、`width:1em;height:1em`)が X3 で元画像の 128px のまま出る報告。診断で追うと、 +キャッシュ消去後の組み立てでは 44×44(1文字大)に置かれ(`image-diag.txt`: `placed 44x44 … vert=1`)、章ごとの絞り込み再解析も +8ファイル全部を読んで 9 規則で完了していた(`css done rules=9 result=0 filtered=1`)。つまり現行の解析は正しく、報告の表示は +**09-06 夜の書式対応(tag.class・二段の指定子・縦書き字下げ)より前に組み立てた章の保存**を読んでいたもの。章の保存 +(`sections/N.bin`)には配置結果がそのまま入るので、書式の解釈が変わっても版を上げなければ古い配置が残る。09-06 は +`SECTION_FILE_VERSION` を 48 に上げたのが昼(部分保存の再解析の修正)で、夜の書式対応では上げていなかった。pre-release +20260906/20260909 の「初回オープン時に配置の作り直しが走る」はスタイルの保存(`CSS_CACHE_VERSION` 13)の話で、章の配置は対象外 +だった。`SECTION_FILE_VERSION` を 49 に上げ(部分保存の目印も連動)、次のリリースで全章が作り直されるようにする。 +**以後、配置の結果が変わる修正では必ずこの版を上げる。** + +同じ診断から: 保存の無い章の途中(第13章の126ページ目)を読書位置から開くと、そこまでの組み立てに 38.4 秒(16 回、最長 10.8 秒)、 +描画まで 46.4 秒。続く描画で階調用の作業領域(7.9KB)が取れず、そのページだけ白黒で出た(`OOM: grayscale strip scratch`)。 +先組みは章の途中を開く場合には効かない。外字画像そのものは「独立した列の上下中央」に置かれる既知の制限が残る(文の途中の +「№2」が別の列の真ん中に浮く)。1文字大の画像を字として列に流す実装(JP版 #129 と同じ考え方)が次の候補。 + +### 段階と進み具合(ユーザー指定の順序): + +| 段階 | 内容 | 状態 | +|---|---|---| +| 1 | 1.6.0から `feat/vertical-1.6.0based` を作成、SDK `cb9167d` | 完了(2026-09-06) | +| 2 | 診断基盤(INPUT_DIAG、ビルド番号、クラッシュ報告の先頭行) | 完了・実機確認済み(2026-09-06、`2026090601-diag`) | +| 3 | 時計機能の移植と、固まった時の再起動の実機検証 | 完了・実機確認済み(2026-09-06、`2026090602-diag`) | +| 4 | フォント(SDフォントの字形領域の分割・キャッシュ解放・ヒープ門番) | 完了・実機確認済み(2026-09-06、`2026090604-diag`) | +| 5 | 縦書き(縦組版コア・電書協CSSのメモリ管理・ルビ/圏点/約物/UAX#50・傍線) | 完了・実機確認済み(2026-09-06、`2026090606-diag`、テスト書16章すべて旧ツリーと一致) | +| 6 | 挿絵(章内画像、寸法指定、ページ送りの速度) | 完了・実機確認済み(2026-09-06、`2026090609-diag`〜`0610-diag`)。コードは段階5に同梱。商業書籍1冊の寸法指定が旧ツリーと同値、`image-size-test.epub` 13サイズ全部合格、`vertical-image-test.epub` 全章が旧ツリーと一致 | + +### 旧ツリーとの照合(2026-09-06、6段階完了後) +旧ツリー `feat/vertical-1.6` が本家 `1.6.0rc` から**加えた行**(生成フォントの表・README・テスト書を除く87ファイル、 +5,443行)を、新ツリーの同じファイルに1行ずつ探した。見つからなかったのは384行で、内訳は全部が説明できる: + +| 見つからなかった行 | 何か | 判断 | +|---|---|---| +| UIフォント関係のスクリプト3本(286行)、`convert-builtin-fonts.sh`、`main.cpp` の Medium フォント宣言、`build-font-ids.sh` | 本家 #3096「UI文字を Ubuntu Medium にする」と、その手直し #3186・光学補正済み Noto UI 面。旧ツリーは採用していた | **本家が 1.6.0 で #3096 を取り消した**(#3314)。新ツリーは本家に従い Regular。旧ツリーの見た目に戻すなら移植が必要(下記) | +| `CssParser.cpp/.h`(32行)、`Epub.cpp/.h`(18行) | 解析中の空き32KB門番、`validateCache`、`cssComplete` で保存を止める旧方式 | 本家 #3005 の「上限付き保存+部分キャッシュ」の上に作り直したので形が違う。機能は「部分なら章ごとに選別解析」で置き換え済み(段階6の項) | +| `fontconvert.py` の `--mono`/`--autohint-font`(18行) | #3096 系の1bit描画オプション | 上と同じ理由で不要。SDフォント側(`fontconvert_sdcard.py`・`sd-fonts.yaml`)は全行一致 | +| `ChapterHtmlSlimParser.cpp`(7行) | 圏点を先頭トークンにまとめて付ける旧形(本節で直した側) | 新ツリーのほうが正しい | +| `ReaderUtils.h`(1行) | 長押し設定OFF時に押下で送る判定 | 本家 1.6.0 が「長押し/離した」判定に変えたのを取り込み、`usePress` 分岐は残してある | +| `HalGPIO.cpp`・`Section.cpp` の各1〜2行 | 条件式の書き方、版番号(旧49/新48) | 同等。版番号は違えば作り直すので混在しても問題ない | + +**本家 1.6.0 の変更で、旧ツリーが取り込んでいた11件**(#3107 #3009 #3122 #3134 #3102 #3191 #3244 #3119 #3222 +#3144 #3186)は、#3096 の取り消しを除き**すべて 1.6.0 本体に入っている**。新ツリー側で改めて当てる必要はない。 + +**新ツリーにだけあるもの**: 本家 1.6.0 の79コミット(言語別フォント #3146、表の列描画 #2654、リンクのタッチ操作 +#3296、SD電源断と単押し起動 #3341、冷間起動の電源ボタン誤判定 #3248、X4 Classic 対応 #3279、リーダーのツールバー +メニュー #3182、辞書まわり、KOSync・OPDS 修正ほか)と、この作り直しで加えた**部分キャッシュ時の章ごと選別解析** +(`95a7600c`)、**圏点の文字ごと付与**(`f1dd384f`)、縦書き本での表の縦積みへの退避、`wordLinkIds` との歩調合わせ。 + +**UI文字の太さは本家どおり Regular にする**(2026-09-06 決定)。旧ツリー(公開中の 2026090504)は Ubuntu Medium +だったが、本家が #3096 を採用しなかった(#3314 で取り消し)以上、新版でも採用しない。1bit描画で Regular は縦線が +2px、Medium は3pxになる差があり、本家は表示崩れ(#3186 で直した文字の重なり)を理由に戻している。 + +### 1.6.0で変わったビルドのしかた +1.6.0は `firmware_tuned` という設定でArduino基盤そのものを再構築し、ヒープを32〜37KB回収する +(タスクの持ち分の縮小、WiFiの処理をIRAMから外す、使わない部品の除外)。初回ビルドで基盤を作り直すため +7分半かかり、ESP-IDF用のPython部品(`idf-component-manager` ほか)が環境に無いと設定段階で止まる。 +以降のビルドは1分半〜3分半。素の1.6.0のX3向けビルドは RAM 17.1%・Flash 83.5%。 + +### 段階2: 診断基盤(2026-09-06、実機確認済み) +`InputDiag` 本体と `scripts/ost_version.py` は旧ツリーからそのまま複製し、差し込み口だけを1.6.0側の +コードに合わせて置き直した: 主ループの入力採取と書き出し、画面描画ごとの所要時間と5秒超の記録採取、 +一覧画面の行配置、読書画面の本を開く6段階のヒープ・章構築の所要・ページ描画の内訳、`crash_report.txt` +先頭のビルド番号。字形の読み込み回数などSDフォント側の計測値はフォント段階まで0のまま。 + +実機で採れた最初の数字が、そのまま後の段階の動機になっている。**素の1.6.0に日本語SDフォントを載せると、 +読書中の空きが最低2.3KB・最大連続4.8KBまで落ち、テスト書の第2章は「出力バッファのメモリ確保に失敗」で +3回再試行の末に章の構築に失敗した**(診断が `section-build-failed` として直前ログ付きで捕捉)。 +一覧画面の描画も空き7〜9KBで1.9〜4.8秒かかっている。旧ツリーではフォント段階(キャッシュ解放・領域分割)と +縦書き段階(CSSのメモリ管理)で解消していたものが、1.6.0素の状態では丸ごと残っていることの確認になった。 +本を開く6段階は enter 93KB → page1 62KB(最大連続 71KB → 33KB)。 + +### 段階3: 時計機能と、固まった時の再起動(2026-09-06、実機検証済み) +旧ツリーの完成形をそのまま移した: 時計画面(`ClockActivity`/`ClockFace`/数字の図案)、RTCの日時取得 +(`HalClock::getLocalDateTime`)、電池電圧の読み取り、CPU速度切り替えの直列化(`HalPowerManager`)、 +待機中の降速を「入力の有無」で判定する主ループ、時計画面限定の30秒監視(主処理・描画処理、全ビルド)、 +止まったタスク名をクラッシュ報告に残す処理、故意ハングの引き金(`CLOCK_HANG_TRIGGERS`/ +`DEBUG_RENDER_WATCHDOG` の試験ビルドのみ)、Homeメニューの「Start clock mode」。 +1.6.0側に #3009(起床時の残像取り)が既に入っていたため、旧ツリーで自前に持っていた `goHome` の引数は +そのまま使えた。1.6.0が足したクラッシュ報告の「Reset reason」行とも共存する。 + +実機検証(`2026090602-diag`=配布版と同じ監視経路、`DEBUG_RENDER_WATCHDOG` なし): +- 側面ボタン → `forced hang: render task` → 主処理は `minute turn` を記録し続け → 30秒で再起動、 + 理由欄 `ActivityManager (CPU 0)`、`Reset reason: TASK_WDT` +- 前面ボタン → `forced hang: main task` → 30秒で再起動、理由欄 `loopTask (CPU 0)` +- 時計は11分の連続表示で分更新445ms・残像取り3.19秒、**空き76KB・最大連続71KBで一定**。旧ツリーの + 44KBより30KB以上多い(1.6.0の基盤再構築による回収分と、SDフォントの常駐がまだ無いことの両方) + +**前面ボタンの試験で1回引き金が引かれなかった**。直前ログの周波数の上げ下げから押下は約1.2秒で、 +X3の前面「決定」キーは電源ボタンと線を共有しており、SDKの入力処理が**0.4秒以上の押下を電源ボタン +の操作として扱う**(`CONFIRM_POWER_HOLD_MS`)。電源短押しは既定で無視なので何も起きなかったように見えた。 +短く叩いて再試験し合格。旧SDKも同じ判定なので、試験手順としては「前面は短く叩く」が正しい。 + +旧ツリーの配布版 `2026090504`(同じ監視・同じ時計)では給電なし11時間11分の連続表示で誤発火なし、 +残量100%→96%、−14mV/h(2026-09-05〜06)。1.6.0側での長時間放置は次の段階のビルドと並行して行う。 + +### 段階4: フォント=SDフォントのメモリ管理(2026-09-06、実機確認済み) +旧ツリーのコミットを差分として1.6.0に当て直した(3方向マージ): 字形領域を4KB片で組み立てる方式、 +符号位置の受け皿を必要量まで伸ばす方式、SDフォントのキャッシュ解放(一覧画面を開くとき・飢えたヒープで +本を開くとき・章の展開が断片化ヒープに当たったとき・低ヒープで読書メニューを開くとき)、読書画面の +ページ表示と章構築のヒープ門番、診断への字形読み込み回数・領域作り直し回数・走査結果の接続。 + +**本家1.6.0の #3126(字形領域が取れないとき半分ずつ縮めて再試行)は外した**。同じ問題への別解で、 +当版の分割方式が入れば再試行は決して走らない死んだ経路になるため、定数・欄・再試行ループを撤去した。 +内蔵UIフォントは1.6.0のまま(本家が Ubuntu Medium を取り消した判断に合わせる)。SDフォントのファイル形式は +変わらず、`fonts-202608240.zip` がそのまま使える。 + +実機(`2026090604-diag`): 段階2で構築に失敗したテスト書の第2章が開く。本を開く6段階の最大連続ブロックが +sect 33KB→55KB、page1 33KB→47KB に改善。字形の計測欄が生きた(`page_scan_last=88b/3f`)。 +**残る問題(旧ツリーから引き継ぎ、移植による後退ではない)**: 読書後にファイル一覧を開くと1画面の描画で +字形領域の作り直しが16〜21回走り、3.0〜4.5秒かかる(旧ツリーでも 3.9〜4.6秒・12〜14回の記録がある)。 +一覧の行ごとに逐次で字形を読むためで、読書ページ式の一括事前読み込みを一覧画面にも適用する必要がある。 +起動直後の一覧は0.5〜0.9秒なので、読書が残したヒープの状態に依存する。 + +### 段階5: 縦書き(2026-09-06、実機確認済み) +**やり方を変えた。** 旧ツリーの縦書き関連コミット(約25件)を順に当てると、本家1.6.0が同じファイルに入れた +変更(タッチでのリンク #3296、表の段組み #2654、CSS保存方式の書き換え #3005、RTL継承 #3198、 +段落間隔 #3221、脚注の上付き #3355)と衝突を繰り返す。そこで**旧ツリーの完成形をファイル単位で +1.6.0に3方向マージ**した(本家が触っていないファイルは丸ごと複製、両方が触ったファイルは +`git diff rc..old -- F | git apply -3`)。衝突は10ファイル29箇所。解析器と章構築(`ChapterHtmlSlimParser`・ +`Section`・`ImageBlock`)は縦書きと挿絵で共有しているため、**段階6の挿絵のコードも同じ変更に入った**。 +検証だけを段階の順に分けている。 + +手を入れた箇所: +- **CSS解析器は本家1.6.0の新実装を土台に作り直した。** 本家は規則の保存を可変長の連想配列から上限付きの + 固定領域(選択子プール+スタイルの重複排除、`-fno-exceptions`で落ちない)へ書き換えており、当版の + 「メモリ不足で落ちない」目的と同じ。その上に当版の機能を載せ直した: 圏点(`text-emphasis`、`-epub-`/`-webkit-` + 接頭辞込み)と `max-width`/`max-height` の解釈と保存形式(記録に1バイト+2長さ、定義ビット18〜20)、 + 章ごとの選別読み込み(登録時とキャッシュ読み込み時の両方でフィルタ)、描画時のヒープ門番の撤去。 + 本家が足した「部分キャッシュを残して後で再試行」「メモリ不足なら章構築を延期」はそのまま生きる。 + CSSキャッシュの版は12(本家11と記録長が違う) +- **章の版は46**: 本家の40〜45と旧ツリーの40〜49のどちらの番号とも重ならない値。全書籍が初回オープンで + 配置を作り直す +- 語の並列配列: 本家の `wordLinkIds` と当版の `wordVerticalBehaviors` を両方保持し、縦書きの追加・削除経路 + でも歩調を合わせた +- 語の追加: 本家のリンク識別(語ごとのlink id)と当版の縦書き分岐を同じ関数で両立。**縦書き本では本家の + 表の段組みを使わず平坦化に固定**(段組みは横書き座標で置かれるため) +- ページ送りボタン: 本家の長押しスキップと、当版の右開き(RTL)での左右反転・側面ボタンの上書きを合成 +- 読書画面は本家の道具箱型メニューなど新UIをそのまま残し、縦書き判定・RTLページ送り・AA見送り・ + 挿絵ページの残像取りだけを当てた + +実機(`2026090606-diag`、`vertical-test-suite.epub` 全16章を写真で照合): **テスト1〜16すべて旧ツリーと同じ +表示**。約物・記号の向き(UAX#50)、補助面漢字、互換漢字の代替、圏点5種と入れ子解除、実体参照2経路、 +欧文語間空白、小書き仮名の右寄せ、40行の長大段落と1300字の漢字連続、太字/下線/取り消し線のタグ版と +クラス版。20分・177描画・368回のボタン操作で異常終了なし。診断: ページ描画1.2〜1.5秒(字形領域の +作り直しが毎ページ1〜4回、計39秒)、空き最低3.3KB(旧ツリーの同種の読書でも4.3〜16KBまで落ちていた)、 +CSSは20規則を完全読み込み、`resolveStyle` の結果が診断に出る。 + +旧ツリーから引き継いだ既知の差(後退ではない): 縦書きの段落配置(中央/末尾寄せ)は未実装(`text-indent` は +2026-09-06 に対応、下の「縦書きの書式対応の拡充」)、Extra Paragraph Spacing ONでは自動字下げが無効、 +中黒「・」(U+30FB/半角U+FF65)の行頭禁則(同日に表へ追加)、読書後の一覧画面が遅い。 + +### 段階6: 挿絵(2026-09-06、完了・商業書籍1冊とテスト書2冊で実機確認) +KADOKAWA テンプレートの縦書きライトノベル(旧ツリーが 2026-08-31 に検証したのと同じ本)で、 +`2026090606`〜`0608` は**挿絵の寸法が開くたびに変わった**(容器いっぱい/小さく/その中間)。診断で原因が +3つ重なっていると分かり、順に直した: + +1. **部分キャッシュを完全扱いしていた**。本家1.6.0の保存方式は規則索引・選択子・書式を連続領域で倍々に + 伸ばすため、最大連続ブロック18〜22KBの断片化ヒープでは32KBへの成長に失敗し、テンプレート + 3,172規則のうち296件で打ち切る。本家はそれを「部分キャッシュ」として保存し、章構築はそれを + 読み込み成功として使っていた。画像用クラスは切り落とされた側にあるので `fit ... rules=0` になる。 + → 部分キャッシュを読んだ章構築は、旧ツリーと同じく**章ごとの選別解析をやり直す** + (`CssParser::lastCacheLoadPartial`)。章の版を47に上げ、規則なしで組んだ章を作り直させた +2. **選別解析にも全体解析用の門番(空き64KB)が掛かっていた**。章構築時の空きは47〜66KBなので、 + 6ファイル全部を読み飛ばす回と読める回が混在し、同じ挿絵が別の大きさになった。 + → 選別解析は24KBで走らせる(登録は数十規則、初期領域は索引1KB・選択子4KB・書式16件、 + ファイルは512バイトずつ流し読み)。全体解析の64KBはそのまま +3. **診断が章構築を遅くしていた**。Kobo由来の `koboSpan` が全文に付く本で、クラス付きspanごとの + 診断1行がSDカードへの書き直し1回になり、1章39回で数秒を食い、CSSと画像の行を記録の輪から押し出した。 + → 太字・斜体・線・圏点のどれかに解決したspanだけ書く + +`2026090609-diag` の結果は旧ツリーの 08-31 の記録と**同値**: 3,172規則中7件だけ登録(`result=0 filtered=1`)、 +166x180→70x76(max-height 10%)、195x244→60x76、199x1345→89x605(80%)、570x1600→269x756(`fit` のみ)。 +章題ページの羊とロゴが1文字大なのは著者指定(高さ10%)で、0606で大きく見えていたほうが誤りだった。 +章の初回表示は17秒(構築9.2秒+選別解析+挿絵5枚の前生成)で、その間は入力を受け付けない。 +旧ツリーの「画像の多い章で11秒」より長く、選別解析の分が乗っている。改善候補。 + +テスト書2冊の結果(同じ `2026090609-diag`): + +- `image-size-test.epub`(横書き、実書籍954冊の実測寸法13種、各ページ画像1枚のみ): **13ページ全部合格**。 + 外枠の欠け・円の歪み・四隅の向きの異常なし。縦長9種(720×1024〜1748×2480)は全面に収まり、 + 横長4種(2048×1589・2868×2048・3049×1300 など)は横幅いっぱいで上寄せ、下は余白。 + 最大の5.87Mpx(2868×2048)と最長辺3049×1300も止まらず表示。1px縞は縮小率によって + グレーに潰れるか、間引き位置しだいで真っ黒/薄いグレーの一様な塗りになる(2048×1589で観察)。 + 縮小器が近い画素を拾うだけで平均を取らないためで、線画のテスト図形でしか目立たず、対処しない。 + 3049×1300(約6分の1)では中央の小さな文字が読めなくなる。これは判定外の「実用下限」の観察項目。 +- `vertical-image-test.epub`(縦書き): 全章が旧ツリーの既知の挙動と一致 + (外字画像は1マス幅の独立した列を占める、全面・40%・横長は正しく縮小、1bit/4bit PNG と svg包みは表示)。 + 第3章(同じ縞画像を4経路で参照)は4枚とも表示。画像は列の流れに入り、ページの右端に画像、その左に + 次の見出しが続く並びで、読み順(見出し→画像→次の見出し)どおり。1ページに画像1枚ずつ、2〜5ページ目。 +- 縦書き本のページ送り(左が進む、側面ボタン)は想定どおり。章境界の描画は7.4秒 + (章の組版5.2秒+字形再構築3回)、灰色階調の描画は約2.2秒で、いずれも旧ツリーと同じ傾向。 + +横書きの回帰確認で1件直した(`2026090610-diag`、章の版48)。横書きの圏点は、対象の文字列を1回で +まとめて処理し**先頭の1文字にまとめて全部の点をルビとして付けていた**ため、13文字の強調で先頭の +「は」に13個分のルビが乗り、ルビ幅に合わせて「は 一 文」と行が引き伸ばされた。横書きの本文は +CJK文字を1文字1トークンに分けて積むので、トークンごとにその文字数分の点を付ける形に直した。 +旧ツリーでも同じ挙動だった(移植で持ち込んだ既存の不具合)。 + +### 商業EPUB蔵書の取得し直しに伴うテストEPUB3冊の更新(2026-09-06、調査とテスト資産のみ・FW変更なし) + +蔵書の取得し直しが完了し(旧 `protect-epub` は廃止、`new-protect-epub` に一本化)、2,256ファイル・識別子で重複を除いて +**1,941冊(リフロー型878・固定レイアウト1,063、縦書き1,814)**を対象に、これまで3回に分けて行った調査 +(挿絵の書き方955冊、画像寸法954冊、使用文字とフォント収録範囲955冊)を一つの集計器で取り直した。本文は保存も引用も +していない(タグの形・クラス名・CSSの値・画像の見出し部・文字の出現数だけ)。集計器と数値だけの結果は +`e-ink/test-assets/corpus-survey/`(リポジトリ外)に置いた。 + +**蔵書の性質で新しく分かったこと**: リフロー型はほぼ全冊がKobo書店の形式(KEPUB)で(ルビを持つ852冊の全部で確認)、本文のすべての文字列が読書位置記録用の +`` で包まれている。ルビの親文字・読み・外字画像も一つずつ包まれ、 +ルビ160万箇所のうち約39万箇所(528冊)が親文字一字ごとに読みを分けた形(`ruby` の中に `rt` が複数)、 +`rp` の括弧も span で包まれる(21冊・23,001箇所、XHTMLの規則上は不正だが実在する)。これまでのテスト書は +どれもこの形を含んでいなかった。 + +**数値の主な変化と新しい発見**(リフロー型878冊、詳細は `test/epubs-ja/README`): + +| 項目 | 前回 | 今回 | +|---|---|---| +| 挿絵の寸法指定が max-* クラス | ほぼ全て | 757冊(固定幅8・width属性2・style属性2) | +| svgで包んだ挿絵 | 527/955冊・約7万箇所 | 408/878冊・8,071箇所(コミックは全ページ) | +| 無圧縮格納の画像 | 0/10万枚超 | 0/209,851枚 | +| 文庫(リフロー)画像の中央値/上位10% | 844×1200/1120×1600 | **1135×1600/1440×2048**(加わった出版社の口絵が大きい) | +| 画素数最大 | 2868×2048(コミック) | 2868×2048(**文庫の見開き口絵**に移った) | +| 外字画像の大きさ指定 | 1em角のみ想定 | 幅2em 833・幅1.5em 74・幅6em 62・高さ3em 82・高さ4em 87箇所 | +| 挿絵だけのページ | 未調査 | vrtlファイル9,841・**hltr(横組み指定)ファイル6,355ページ**、hltrの画像ページを持つ本352冊 | +| ルビ付き外字画像 | 1例を想定 | 1,329箇所・120冊 | +| upright/sideways クラス | 未調査 | 147冊・3,605箇所/226冊・451箇所。CSSは接頭辞付き(-webkit-/-epub-)だけで書かれる | +| 段落の text-indent/text-align クラス | 未調査 | 421冊/705冊。負の値(ぶら下げ、`text-indent:-Nem; padding-top:Nem`)1万箇所超 | +| span の font-size クラス | 未調査 | 641冊(90% 3,864・110% 2,087・0.7em 1,002箇所) | +| フォント未収録の文字 | ℃・溺蓮煉・𠮟 など | 3書体共通で未収録はなし(1冊限りのタイ文字・ヘブライ文字・合字を除く)。BIZ UD系のみ **Ⓒ(56冊)**・逬(4冊)・溺(11冊)・煉(3冊)が元フォント未収録。**IVS異体字選択子 U+E0100 が1冊**にあり、現状は読み飛ばさない | +| 頻出記号 | — | ─(U+2500)637冊・31万回(ダッシュ「──」)、〝〟で始まる段落6,223(禁則表に未登録)、⁉634回・‼ | + +**テストEPUBへの反映**(3冊とも epubcheck は従来からの意図的な不整合以外エラーなし。**テスト17は実機で5項目すべて合格**(2026-09-06、`2026090611`): 文の分割・ルビ・一字ずつの読み・rp の括弧・装飾クラスの内側の区切り要素のどれも「なし」と同じ見え方で、Kobo形式の本は現状の解析器でそのまま読める。**テスト18も実機確認済み**: 予想どおり upright/sideways の指定は無視され、指定あり・比較の両段落が同じ見え方(矢印は既定どおり下向き、全角の!?ーは正立のまま)。周りの字への影響は無い。**テスト19・20も実機確認済み**: 19は text-indent(正負)・padding-top・margin-top の全部が無視され、全段落が天から始まる(予想どおり)。ただし **text-align:center の段落の最初の列で、末尾の1字の前に空きが入る**現象を新しく確認した(同じ形の文で right では出ない。要調査)。20は span の font-size が全部無視され、同じ大きさで出る(予想どおり。ルビと列は乱れない)。**テスト21も実機確認済み**(NotoSansJP): ─―‐は縦の線(合格)、⁉‼♡♥※●◆①は正立(合格)だが Ⅱ(U+2161)は写真では横倒しに見えたが、**2026-09-08 に拡大写真で正立を確認**(セリフの無い2本の縦線が低解像度の写真で「=」に見えていただけ。変換済みフォントの字形も縦長で保存されていることを確かめた。規格どおりで問題なし)、Ⓒ・逬は表示(合格)、**葛の直後に四角が出る=IVS未対応を確認(同日 `2026090612-diag` で修正・実機合格)**、数字の桁数別の形は合格、〝〟の禁則は列の変わり目に来ず判定できず(文の長さを調節する)。**`image-size-test.epub` は14ページ全部合格**(2026-09-06、`2026090611`): 新しく入った 3360×1187・2868×2048(5.87Mpx)・2700×1410・1832×2600 を含め、どれも幅いっぱい・外枠四辺あり・正円で、横長は上寄せ。**`vertical-image-test.epub` v11 の第10〜12章も実機確認済み**: 第10章は幅1.5em・2em・2em横長・高さ3em・4em・幅6emの全部が指定どおりの大きさで本文と重ならない(列をまたぐ幅の絵は列を割り当てて上下中央に置かれる)。第11章は hltr+img/hltr+svg/vrtl+img の挿絵ページが全部表示され、送り順 A→B→C→D→第12章 も正しい。D の文字ページは既知の制限どおり縦で出る。第12章は区切り要素に包まれた外字・一字ずつの読み・分割された文が合格、ルビ付き外字の読みは既知の制限どおり付かない。**3冊とも実機確認完了**): + +- `image-size-test.epub` を新しい実測値で作り直した(13→14ページ)。文庫の中央値・上位10%・最長辺 3360×1187・ + 最大画素 2868×2048、コミック現行の最長辺 2700×1410・最大画素 1748×2480、旧世代の最頻 710×1024、大判の最大 1832×2600。 + まとめページに前回との比較表と測りかた(固定レイアウトは各本12枚前後の見出し部を読み、残りはsvgのimage属性。 + 読んだ46,166枚のうち属性と食い違い68枚)を載せた。生成器 `test-assets/build_image_size_test.py` を更新 +- `vertical-image-test.epub` を v11 に(9→18ページ)。第7・9章の数値を更新し、第10章「1em以外のem単位の外字・小画像」、 + 第11章「別ファイルの挿絵ページ(hltr+img/hltr+svg/vrtl+img/hltr文字ページの4ファイル、送り順の確認)」、 + 第12章「Kobo形式の区切り要素と外字・ルビ」を追加 +- `vertical-test-suite.epub` にテスト17〜21を追加(32→42章)。17: Kobo形式の区切り要素(同じ文の「あり/なし」を対にして + 比較。文の分割・ルビ・一字ずつのルビ・rp・縦中横・傍点・太字)、18: upright/sideways の明示指定 + (**当版は未対応と分かっている**ので記録用)、19: 電書協テンプレートの字下げ・ぶら下げ・天アキ(同じく未対応の記録用)、 + 20: 文中の font-size 指定、21: 頻度順の記号(─―‐の向き、〝〟の禁則、⁉‼♡♥※●◆①Ⅱの正立、Ⓒ・逬の収録、 + IVS の読み飛ばし、数字の桁数別の形)。テスト8・9の解説に再調査の冊数を1文ずつ追記 + +**実装への含意(今回はFWを変えていない)**: 優先度順に、(1) IVS(U+E0100〜E01EF)をVS15/16と同じく読み飛ばす(**2026-09-06 実施**: 読み飛ばしを VS1〜VS16 の U+FE00〜FE0F 全部と IVS の U+E0100〜E01EF に広げた。`2026090612-diag` でテスト21の「葛󠄀」の四角が消えたのを実機確認)、 +(2) 〝(U+301D)を行末禁則・〟(U+301F)を行頭禁則に加える、(3) 接頭辞付きの text-orientation を読んで upright/sideways +を効かせる、(4) 縦書きの text-indent(正負)と margin-top の天アキ、(5) span の font-size、(6) text-align:center の列で末尾の1字の前に空きが入る件の原因調査。(3)〜(5) は市販の本での +使用冊数が多く、既知の制限として残していたものに実際の頻度が付いた形になる。 + +### 縦書きの書式対応の拡充(2026-09-06、前節の実装候補(2)〜(6)を優先度順に実施・`2026090613-diag`・実機確認済み) + +前節の「実装への含意」(2)〜(6)を順に片付けた。(5)だけは見送り(理由は下)。 + +**(2) 禁則表の拡充**(`VerticalTextUtils.h`): 〝(U+301D)を行末禁則、〟(U+301F)を行頭禁則に加えた。同じ性質の +“‘(行末)と ”’(行頭)も一緒に登録。あわせて JLREQ の行頭禁則のうち表に無かった類——ハイフン類「‐–〜゠~」 +(cl-03)、中点類「・・」(cl-05。段階5の節で候補にしていた中黒の件はこれで解消)、分離禁止「‥…〳〴〵」(cl-08)、 +繰返し記号「々〻ゝゞヽヾ」(cl-09)——を行頭禁則に加えた。 + +**(6) text-align:center の列で末尾の1字の前に空きが見える件は、原因が分かり不具合ではなかった。** 18pt NotoSansJP で +1マスは38px。写真の【中央】の第1列は 【中央】この段落は+半角17字+。+縦 で 715px、【右】の第1列は 686px、 +【基準】の第1列は 684px(次の1字を足すと722pxで入らない)。つまり列の容量は715〜721pxで、【中央】の列だけが +容量ぎりぎりまで詰まっていた。「。」の後の空きを写真の画素で測ると【中央】も【右】も同じ47px——縦書きの句点は +縦書き用の字形「︒」で1マスを使い、点がマスの上端に付くので残りが空きに見える、いつもの形。最後の1字が +状態表示の直上まで下がったので目についただけで、中央揃え特有の現象ではない。中央揃え・右揃えは縦書きでは列の +位置を変えないという現状の扱いはそのまま。 + +**(3) 文字の向きの明示指定**(`CssParser`・`ChapterHtmlSlimParser`・`TextBlock`・`EpdFontFamily.h`): +`text-orientation`(upright/sideways/sideways-right/mixed)を、市販の本が使う `-webkit-`/`-epub-` 接頭辞付きも +含めて読む。縦中横の `text-combine-upright: all`(電書協の `tcy` クラスが使う `-webkit-text-combine: horizontal` +の形も)も同じ欄で持つ。仕組み: ページの保存形式に字ごとの向きの配列を足す代わりに、語の書式ビットの空きの1ビット +(`VERTICAL_FLIP`)を「文字の種類から決まる向きを反転する」印にした。描画側は従来どおり文字の種類から向きを決め、 +印が付いていれば逆にする。これで章の保存形式(`SECTION_FILE_VERSION`)は変えずに済む。 +- 正立指定: 半角の英数字・記号・矢印は1字ずつ1マスを占め、マスの中央に置く。もともと正立する字(漢字・かな・全角記号) + はそのまま +- 横倒し指定: もともと倒れる字(欧文・数字・括弧・長音)は従来どおりひとつながりの語に、正立する字(全角の!?、かな) + は1字ずつ倒す。1〜2桁の数字と「!?」は自動で縦中横になる字なので、横倒し指定の中では印を付けて倒す +- 縦中横指定: 自動でならない並び(3桁の数字、「EU」「3.5」)を1マスに正立で入れる。幅が1.5マスを超える並びは + 横倒しのまま。マスの幅を超える分は左右に均等にはみ出す + +**(4) 縦書きの text-indent と天アキ**(`ParsedText.cpp`・`CssParser`・`CssSelectorUsage`): 縦書きの列組みで +明示の `text-indent` を正負とも効かせる。負の値(ぶら下げ)は電書協テンプレートが `padding-top` と対で書くので、 +列の天のずらし(前からあった)が空けた分だけ第1列を上に戻す形になる。対が無い場合は天のずらしの範囲で止める。 +「Extra Paragraph Spacing」ONでも明示の指定は効かせる(横書きの本家実装はONのとき正の明示指定も捨てるが、縦書きの +市販本ではこのクラスが特定の段落だけに付いていて著者の意図があるため。自動の字下げは従来どおりONで無効)。 +天アキ(`margin-top`/`padding-top`)は列の天のずらしとして前から入っていたのに効かなかった原因は、**電書協テンプレートが +これらのクラスを `.vrtl .h-indent-1em`、`.hltr .start-1em` のように html 要素の書字方向クラスを親に付けた二段の指定子で +書いていて、当版の書式解析器が空白を含む指定子を全部捨てていた**こと。テスト19の指定は全部この形で、実物の +テンプレート(1,905規則)では956規則がこの形だった。そこで二段の子孫指定子(`a b`、a・bは要素名/.クラス/要素名.クラス) +を読むようにした。保存は「a b」の1個の空白区切り、照合はパーサが開いている先祖要素の要素名とクラスを積んでおいて +(`ancestorStack`)、要素の書式を引くときに先祖ごとに「先祖 .クラス」「先祖 要素名」「先祖 要素名.クラス」を引く。 +CSSの優先度どおり単段の規則より後に重ねる。章ごとの絞り込み(`CssSelectorUsage`)は二段とも章に現れるときだけ残す。 +書式の保存形式は `CSS_CACHE_VERSION` 12→13(1バイト追加+空白付きの鍵)で、初回オープン時に書式の保存だけ作り直しが走る。 +- 副作用として横書きの本でも `.hltr .h-indent-1em{text-indent:-1em; padding-left:1em}` などが効くようになる(横書きの + ぶら下げは本家の組版がもともと負の text-indent と padding-left を扱える) +- メモリの面: 電書協テンプレートは単段625規則の時点で固有の書式本体が583種と上限256を超えていて、すでに + 「保存は打ち切り、章ごとに絞り込んで読み直す」経路に乗っている。二段の規則が増えても同じ経路のままで、 + 章ごとの絞り込み後の規則数は変わらず少ない + +**(5) span の font-size は見送り。** 当版のフォントは大きさごとに別ファイルの点画像で、同時に読み込むのは1つの +大きさだけ(メモリ)。ルビ・上付きの50%縮小だけが描画にあり、70/90/110/150%の拡縮を足すと点画像の拡縮で字が崩れる。 +もう一つの大きさを読み込む案は空きメモリ(読書中48〜80KB)に対して大きすぎる。既知の制限として残す。 + +**実機での見かた**(`vertical-test-suite.epub` は解説を新しい合格条件に書き換え、テスト18に【縦中横】、テスト21に +【引用・連続】の段落を足した。epubcheck は従来の意図的な不整合のみ): +1. テスト18: 見かた(1)〜(5)。正立の A x 3 & + と →、横倒しの!?ー、縦中横の 100・EU・3.5(1234 は横倒しのままが正) + → **実機で5項目とも合格**(2026-09-06、`2026090613-diag`、NotoSansJP 18pt)。A x 3 & + が1字1マスで正立、→が右向き、!?が横倒し(【比較】側は正立のまま)、100・EU・3.5 が1マスに横並び、1234 は横倒し +2. テスト19: 見かた(1)〜(4)。字下げ1em、ぶら下げ1em/2em(一行目は天から、二行目以降が下がる)、天アキ1em/3em + → **実機で(2)〜(4)合格、(1)は判定保留**(2026-09-06、`2026090613-diag`)。ぶら下げは二行目以降(ページをまたぐ続きの列も)、天アキは全行が指定の文字数分下がり、中央・右揃えは列の位置が変わらない。二段の指定子 `.vrtl .xxx` が読めていることの確認を兼ねる。(1)は【字下げ1em】が一字分下がっているものの、端末の「Extra Paragraph Spacing」がOFFで【基準】にも当版の自動の字下げ(1マス)が入り、差が見えなかった(写真の見直しで判明。仕様どおりで不具合ではない)。テスト書に【字下げ2em】の段落を足し、再確認で **(1)も合格**: 【基準】(自動の1マス)≈【字下げ1em】<【字下げ2em】の順に下がる。なお em の換算は横書きと同じく字面の高さ(18pt NotoSansJP で44px)なので、1em は1マス(38px)より一割ほど深い。実用上の差は小さく、既知の癖として残す +3. テスト21: 見かた(2)。【引用】【引用・連続】で〝が列末・〟が列頭に残っていないこと + → 実機(18pt)では9か所の列の変わり目のどこにも引用符が来ておらず、字形の送り幅からの計算でも引用符が変わり目に掛かる位置に無かったため、**禁則が働いたかは判定できず**。三字ごとに引用符が来る【引用・密】(〝一〟〝二〟…)を足したが、実機では列が18マスで3マスの組が6組ずつ割り切れ、これも変わり目が全部組の境目に落ちて判定できず。そこで組の間に0〜2字のかなを挟み、列の長さが14〜26字のどれでも禁則なしなら必ず二か所以上で違反が出る並びを計算で作って差し替え、**再確認で合格**: 「…〝三〟い」の列と「…ちつ〝丑〟てと」の列がそれぞれ1マス空けて終わり、次の列が〝四〟・〝寅〟で始まる(列末に来るはずの〝が引き取られた形)。全部の列で三字の組が崩れず、列末の〝・列頭の〟は無い。ほかの段落(線・正立記号・収録・異体字・数字)は前回と同じ結果で退行なし +4. 退行確認: テスト17(Kobo形式)が前回と同じこと、商業書籍1冊で会話文のぶら下げ・扉の天アキが崩れていないこと + → **商業書籍1冊(電書協形式、KEPUB)で確認済み**(2026-09-06): 会話文の「」の行は天から、地の文は一字下げ、章扉の挿絵ページと章の最初のページも従来どおりで、二段の指定子が効くようになった副作用は見えない。テスト20(font-size、見送り)は前回と同じ見え方で退行なし From 540cc65b00f6759e66e79862624d23e6e10be905 Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 14:26:01 +0900 Subject: [PATCH 02/59] =?UTF-8?q?feat:=201.6.5=20=E8=B5=B7=E7=82=B9?= =?UTF-8?q?=E3=81=AE=E6=96=B0=E3=83=84=E3=83=AA=E3=83=BC=E3=81=AB=E6=99=82?= =?UTF-8?q?=E8=A8=88=E3=83=A2=E3=83=BC=E3=83=89=E3=81=A8=E7=9B=A3=E8=A6=96?= =?UTF-8?q?=E3=82=92=E8=BC=89=E3=81=9B=E3=82=8B=EF=BC=88=E4=BD=9C=E3=82=8A?= =?UTF-8?q?=E7=9B=B4=E3=81=97=E3=81=AE=E6=AE=B5=E9=9A=8E2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 時計の画面・数字の画像・12時間表示は旧ツリー feat/vertical-1.6.0based の まま。時刻は本家 #3562 の地域・夏時間の設定に従わせ、時計の画面向けに RTC を毎回読んで localtime_r で変換する HalClock::getLocalDateTime() を 足した(本家の localTime() は 10 秒ごとにしか RTC を読まず、秒から次の 書き換えを決める時計には古い)。旧版の UTC 時差を自前で足す計算は廃止。 ほかは旧ツリーのまま: 時計の描画と時計表示中のメイン処理の 30 秒監視 (すべてのビルド、固まった作業の名前を crash_report.txt に残す)、CPU の 速度切り替えの直列化、電池電圧、Activity::needsFullSpeed/watchesRender、 入力と自動スリープの時計の分離、Home のメニュー末尾の Clock。 実機確認: 2026092806-diag(CLOCK_HANG_TRIGGERS)。1.6.0 の UTC+9 は本家の 移行処理で引き継がれ、Tokyo / Seoul でも同じ表示。メイン処理・描画の 強制停止とも 30 秒以内に再起動し loopTask/ActivityManager の名前が残った。 時計表示中 10MHz、読み取り間隔 76ms 以内、空き 78〜80KB で一定。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- lib/I18n/translations/english.yaml | 1 + lib/hal/HalClock.cpp | 19 + lib/hal/HalClock.h | 8 + lib/hal/HalPowerManager.cpp | 29 +- lib/hal/HalPowerManager.h | 8 +- lib/hal/HalSystem.cpp | 35 + scripts/build_clock_digits.py | 210 ++++ src/activities/Activity.h | 13 + src/activities/ActivityManager.cpp | 51 + src/activities/ActivityManager.h | 4 +- src/activities/home/HomeActivity.cpp | 11 +- src/activities/home/HomeActivity.h | 6 +- src/activities/util/ClockActivity.cpp | 239 +++++ src/activities/util/ClockActivity.h | 73 ++ src/images/ClockDigits.h | 1312 +++++++++++++++++++++++++ src/main.cpp | 30 +- src/util/ClockFace.cpp | 140 +++ src/util/ClockFace.h | 32 + 18 files changed, 2198 insertions(+), 23 deletions(-) create mode 100644 scripts/build_clock_digits.py create mode 100644 src/activities/util/ClockActivity.cpp create mode 100644 src/activities/util/ClockActivity.h create mode 100644 src/images/ClockDigits.h create mode 100644 src/util/ClockFace.cpp create mode 100644 src/util/ClockFace.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 4567821f853..cdfbdd074ad 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -8,6 +8,7 @@ STR_BOOTING: "BOOTING" STR_SLEEPING: "SLEEPING" STR_ENTERING_SLEEP: "Going to sleep" STR_BROWSE_FILES: "Browse Files" +STR_START_CLOCK_MODE: "Start clock mode" STR_FILE_TRANSFER: "File Transfer" STR_SETTINGS_TITLE: "Settings" STR_CONTINUE_READING: "Continue Reading" diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 1ebeedb7d2b..101575f9413 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -52,6 +52,25 @@ bool HalClock::localTime(struct tm& out) const { return true; } +bool HalClock::getLocalDateTime(Rtc::DateTime& out) const { + if (!_available) return false; + + Rtc::DateTime utc; + if (!_sdkRtc.now(utc)) return false; + const time_t epoch = epochFromUtc(utc); + struct tm local; + localtime_r(&epoch, &local); + + out.year = static_cast(local.tm_year + 1900); + out.month = static_cast(local.tm_mon + 1); + out.day = static_cast(local.tm_mday); + out.hour = static_cast(local.tm_hour); + out.minute = static_cast(local.tm_min); + out.second = static_cast(local.tm_sec); + out.weekday = static_cast(local.tm_wday); + return true; +} + bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const { struct tm local; if (!localTime(local)) return false; diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index 69bb22cb603..5f3139cea5a 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -34,6 +34,14 @@ class HalClock { // Returns false if RTC is not available. bool localTime(struct tm& out) const; + // Current date and time in the configured timezone, seconds included. + // + // Separate from localTime() because that one serves a 10 s cache, which is fine for an + // HH:MM label but not for the clock face: it schedules its next repaint from the seconds. + // Reads the RTC on every call -- the caller runs about once a minute. + // Returns false if the RTC is absent or reports its time unreliable. + bool getLocalDateTime(Rtc::DateTime& out) const; + // Get current local hour (0-23) and minute (0-59). // Returns false if RTC is not available. bool getTime(uint8_t& hour, uint8_t& minute) const; diff --git a/lib/hal/HalPowerManager.cpp b/lib/hal/HalPowerManager.cpp index 90518ecf843..5d2ce10249d 100644 --- a/lib/hal/HalPowerManager.cpp +++ b/lib/hal/HalPowerManager.cpp @@ -1,5 +1,6 @@ #include "HalPowerManager.h" +#include #include #include #include @@ -42,28 +43,31 @@ void HalPowerManager::setPowerSaving(bool enabled) { enabled = false; } - // Note: We don't use mutex here to avoid too much overhead, - // it's not very important if we read a slightly stale value for currentLockMode + // Held across the frequency change itself, not just the mode read. setCpuFrequencyMhz takes + // the Arduino APB-change mutex and the SPI bus mutex in opposite orders across its + // before/after callbacks, so two tasks inside it at once can deadlock each other. Lock() + // sets currentLockMode and releases modeMutex before calling here, so no re-entry. + xSemaphoreTake(modeMutex, portMAX_DELAY); const LockMode mode = currentLockMode; if (mode == None && enabled && !isLowPower) { LOG_DBG("PWR", "Going to low-power mode"); - if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) { + if (setCpuFrequencyMhz(LOW_POWER_FREQ)) { + isLowPower = true; + } else { LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", LOW_POWER_FREQ); - return; } - isLowPower = true; - } else if ((!enabled || mode != None) && isLowPower) { LOG_DBG("PWR", "Restoring normal CPU frequency"); - if (!setCpuFrequencyMhz(normalFreq)) { + if (setCpuFrequencyMhz(normalFreq)) { + isLowPower = false; + } else { LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", normalFreq); - return; } - isLowPower = false; } - // Otherwise, no change needed + + xSemaphoreGive(modeMutex); } void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { @@ -183,3 +187,8 @@ HalPowerManager::Lock::~Lock() { } xSemaphoreGive(powerManager.modeMutex); } + +uint16_t HalPowerManager::getBatteryMillivolts() const { + static const BatteryMonitor battery; + return battery.readMillivolts(); +} diff --git a/lib/hal/HalPowerManager.h b/lib/hal/HalPowerManager.h index d2cc99c12b0..5e2214a8faa 100644 --- a/lib/hal/HalPowerManager.h +++ b/lib/hal/HalPowerManager.h @@ -22,7 +22,7 @@ class HalPowerManager { enum LockMode { None, NormalSpeed }; LockMode currentLockMode = None; - SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode + SemaphoreHandle_t modeMutex = nullptr; // Protects currentLockMode and serializes CPU frequency changes public: #if BOARD_HAS_PSRAM @@ -45,6 +45,12 @@ class HalPowerManager { // Get battery percentage (range 0-100) uint16_t getBatteryPercentage() const; + // Pack voltage in millivolts, straight from the gauge with no caching or + // smoothing. Percentage moves in whole points and can sit still for a long + // time; millivolts is what makes a drain measurable over an hour rather than + // a day. 0 when the board has no gauge. + uint16_t getBatteryMillivolts() const; + // RAII helper class to manage power saving locks // Usage: create an instance of Lock in a scope to disable power saving, for example when running a task that needs // full performance. When the Lock instance is destroyed (goes out of scope), power saving will be re-enabled. diff --git a/lib/hal/HalSystem.cpp b/lib/hal/HalSystem.cpp index a40b6a8bab9..c1f405eb387 100644 --- a/lib/hal/HalSystem.cpp +++ b/lib/hal/HalSystem.cpp @@ -10,6 +10,7 @@ #include "esp_private/esp_cpu_internal.h" #include "esp_private/esp_system_attr.h" #include "esp_private/panic_internal.h" +#include "esp_task_wdt.h" // Generated per build by scripts/ost_version.py. The version macro names the commit, which // cannot tell two builds apart while the tree carries uncommitted changes -- and a crash // report read against the wrong firmware is worse than no report. @@ -46,6 +47,40 @@ void IRAM_ATTR __wrap_panic_abort(const char* message) { __real_panic_abort(message); } +// A task watchdog timeout never reaches __wrap_panic_abort: task_wdt_timeout_abort() sets +// g_panic_abort directly instead of calling panic_abort(), so --wrap has nothing to intercept +// and panicMessage stays empty. The backtrace wrapper below still fires, which is why a +// watchdog reset shows a stack but no reason -- the shape seen on 2026-08-20. +// +// This is the weak hook the TWDT ISR calls before aborting. esp_task_wdt_print_triggered_tasks +// walks the subscriber list and hands each name to the callback, so the reason line ends up +// naming the task that stopped feeding -- the one thing the stack alone cannot say, since the +// backtrace belongs to whatever task the timer interrupt happened to land on. +struct WdtReasonSink { + int len; +}; + +static void IRAM_ATTR appendWdtReason(void* opaque, const char* msg) { + auto* sink = static_cast(opaque); + if (!msg) return; + for (int i = 0; msg[i] && sink->len < (int)sizeof(panicMessage) - 1; i++) { + // The handler is fed several fragments including newlines; keep it to one line. + panicMessage[sink->len++] = (msg[i] == '\n' || msg[i] == '\r') ? ' ' : msg[i]; + } + panicMessage[sink->len] = '\0'; +} + +static DRAM_ATTR const char WDT_REASON_PREFIX[] = "Task watchdog: "; +void IRAM_ATTR esp_task_wdt_isr_user_handler(void) { + WdtReasonSink sink{0}; + for (; WDT_REASON_PREFIX[sink.len] && sink.len < (int)sizeof(panicMessage) - 1; sink.len++) { + panicMessage[sink.len] = WDT_REASON_PREFIX[sink.len]; + } + panicMessage[sink.len] = '\0'; + esp_task_wdt_print_triggered_tasks(appendWdtReason, &sink, nullptr); + panicCaptureMarker = PANIC_CAPTURE_MAGIC; +} + void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) { if (!frame) { __real_panic_print_backtrace(frame, core); diff --git a/scripts/build_clock_digits.py b/scripts/build_clock_digits.py new file mode 100644 index 00000000000..2e942f35370 --- /dev/null +++ b/scripts/build_clock_digits.py @@ -0,0 +1,210 @@ +"""Render the clock face's digits into a flash-resident 1bpp header. + +Clock mode draws with GfxRenderer::drawImage, not with a font: the glyphs it +needs are eleven ASCII characters that never change, and going through the font +path would mean either an SD-card font (the timer-wake path never mounts the +card) or a built-in font capped at 18 pt, which is far too small for a clock +read across a room. + +Output format matches FreeInkDisplay::blitImage: one bit per pixel, MSB is the +leftmost pixel, each row padded to a whole number of bytes, and a 0 bit is black +ink. That is the same convention as src/images/Logo120.h. + +drawImage rotates only the origin, never the bits (see the TODO in +GfxRenderer::drawImage), so the rotation has to be baked in here. --rotate takes +the quarter-turns to apply; regenerate with a different value if the face comes +out sideways on the device. + +Usage: + python3 scripts/build_clock_digits.py [--font PATH] [--large N] [--small N] + [--rotate 0|1|2|3] [--out PATH] +""" + +import argparse +import os +import sys + +try: + import freetype +except ImportError: + sys.exit("freetype-py is required: pip install freetype-py") + +# The clock face draws dates and times, so these eleven and nothing else. +GLYPHS = "0123456789:/" + +# AM/PM markers for the 12-hour format, kept as their own set so the wide 'M' +# does not stretch the digits' common box (every cell in a set is padded to the +# widest glyph, and the 24-hour face's metrics must not change). +LETTERS = "APM" + +# C identifiers for characters that cannot appear in one. +NAMES = {":": "Colon", "/": "Slash"} + + +def glyph_name(ch): + if ch in NAMES: + return NAMES[ch] + return f"Digit{ch}" if ch.isdigit() else f"Letter{ch}" + + +def render(face, ch, pixel_size): + """Render one character to a (width, height, rows-of-bools) bitmap, ink=True.""" + face.set_pixel_sizes(0, pixel_size) + face.load_char(ch, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_MONO) + bitmap = face.glyph.bitmap + width, rows, pitch = bitmap.width, bitmap.rows, bitmap.pitch + + # FT_RENDER_MODE_MONO packs 8 pixels per byte, MSB first, with a set bit + # meaning ink -- the opposite polarity to the output, inverted below. + pixels = [[False] * width for _ in range(rows)] + for y in range(rows): + for x in range(width): + byte = bitmap.buffer[y * pitch + (x >> 3)] + pixels[y][x] = bool(byte & (0x80 >> (x & 7))) + return width, rows, pixels + + +def pad_to_common_box(glyphs, pixel_size): + """Put every glyph on one baseline in one box, so the caller can lay them out + by advancing a fixed pitch instead of tracking per-glyph metrics on device.""" + max_ascent = max(g["bearing_y"] for g in glyphs) + max_descent = max(g["rows"] - g["bearing_y"] for g in glyphs) + box_h = max_ascent + max_descent + box_w = max(g["width"] for g in glyphs) + + for g in glyphs: + padded = [[False] * box_w for _ in range(box_h)] + top = max_ascent - g["bearing_y"] + left = (box_w - g["width"]) // 2 # centred: digits read as a column of equal cells + for y in range(g["rows"]): + for x in range(g["width"]): + if g["pixels"][y][x]: + padded[top + y][left + x] = True + g["pixels"] = padded + g["width"] = box_w + g["rows"] = box_h + return box_w, box_h, max_descent + + +def rotate(pixels, width, height, quarter_turns): + """Rotate clockwise by `quarter_turns` 90-degree steps.""" + for _ in range(quarter_turns % 4): + rotated = [[False] * height for _ in range(width)] + for y in range(height): + for x in range(width): + rotated[x][height - 1 - y] = pixels[y][x] + pixels = rotated + width, height = height, width + return pixels, width, height + + +def pack(pixels, width, height): + """1bpp, MSB first, rows padded to whole bytes, 0 = black.""" + row_bytes = (width + 7) // 8 + out = bytearray() + for y in range(height): + row = bytearray(b"\xff" * row_bytes) # start white + for x in range(width): + if pixels[y][x]: + row[x >> 3] &= ~(0x80 >> (x & 7)) & 0xFF + out += row + return bytes(out) + + +def emit_array(name, data, width, height): + lines = [f"// {name}: {width}x{height}, 1bpp MSB-first, 0 = black", + f"static const uint8_t {name}[] = {{"] + row_bytes = (width + 7) // 8 + for i in range(0, len(data), row_bytes): + chunk = data[i:i + row_bytes] + lines.append(" " + " ".join(f"0x{b:02x}," for b in chunk)) + lines.append("};") + return "\n".join(lines) + + +def build_set(face, pixel_size, prefix, rotate_turns, chars=GLYPHS): + glyphs = [] + for ch in chars: + width, rows, pixels = render(face, ch, pixel_size) + glyphs.append({ + "ch": ch, + "width": width, + "rows": rows, + "pixels": pixels, + "bearing_y": face.glyph.bitmap_top, + }) + box_w, box_h, descent = pad_to_common_box(glyphs, pixel_size) + + blocks = [] + for g in glyphs: + pixels, w, h = rotate(g["pixels"], g["width"], g["rows"], rotate_turns) + name = f"{prefix}{glyph_name(g['ch'])}" + blocks.append(emit_array(name, pack(pixels, w, h), w, h)) + g["out_w"], g["out_h"] = w, h + + out_w, out_h = glyphs[0]["out_w"], glyphs[0]["out_h"] + table = [f"// Indexed by ClockGlyph. Every cell is {out_w}x{out_h}, so the face advances a fixed pitch.", + f"static const uint8_t* const {prefix}Glyphs[] = {{"] + table += [f" {prefix}{glyph_name(ch)}," for ch in chars] + table.append("};") + + header = [ + f"#define {prefix.upper()}_WIDTH {out_w}", + f"#define {prefix.upper()}_HEIGHT {out_h}", + f"// Unrotated cell, for laying the face out in logical coordinates.", + f"#define {prefix.upper()}_CELL_W {box_w}", + f"#define {prefix.upper()}_CELL_H {box_h}", + f"// Rows below the baseline in the (unrotated) cell. Sets differ -- '/'", + f"// descends, letters do not -- so mixing sets on one line needs baseline", + f"// alignment, not bottom-edge alignment.", + f"#define {prefix.upper()}_DESCENT {descent}", + ] + return "\n".join(header) + "\n\n" + "\n\n".join(blocks) + "\n\n" + "\n".join(table) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--font", default="/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf") + ap.add_argument("--large", type=int, default=150, help="pixel size for the time") + ap.add_argument("--small", type=int, default=56, help="pixel size for the date") + ap.add_argument("--rotate", type=int, default=0, + help="quarter turns clockwise baked into the bitmaps (drawImage does not rotate bits)") + ap.add_argument("--out", default="src/images/ClockDigits.h") + args = ap.parse_args() + + if not os.path.exists(args.font): + sys.exit(f"font not found: {args.font}") + face = freetype.Face(args.font) + + parts = [ + "#pragma once", + "#include ", + "", + "// Generated by scripts/build_clock_digits.py -- do not edit.", + f"// font={os.path.basename(args.font)} large={args.large} small={args.small} rotate={args.rotate}", + "//", + "// Clock mode's glyphs, drawn with GfxRenderer::drawImage rather than a font: the", + "// timer-wake path never mounts the SD card, and the built-in fonts stop at 18 pt.", + "// The rotation is baked in because drawImage rotates the origin but not the bits.", + "", + "// Order matches ClockGlyph in ClockFace.h.", + "", + build_set(face, args.large, "ClockLarge", args.rotate), + "", + build_set(face, args.small, "ClockSmall", args.rotate), + "", + build_set(face, args.small, "ClockAmPm", args.rotate, chars=LETTERS), + "", + ] + text = "\n".join(parts) + + os.makedirs(os.path.dirname(args.out), exist_ok=True) + with open(args.out, "w", encoding="utf-8") as f: + f.write(text) + + size = len(text.encode()) + print(f"{args.out}: {size} bytes of source") + + +if __name__ == "__main__": + main() diff --git a/src/activities/Activity.h b/src/activities/Activity.h index 0b7366f1db8..4e743876501 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -43,6 +43,19 @@ class Activity { virtual bool skipLoopDelay() { return false; } virtual bool preventAutoSleep() { return false; } + // Whether the main loop should hold the CPU at full speed for this activity. + // Defaults to preventAutoSleep() because that is what holding off auto-sleep + // used to imply: an activity kept awake for network or transfer work needs the + // clock too. One that is awake only to own the panel -- the clock -- overrides + // this to false and idles at the low-power frequency between repaints. + virtual bool needsFullSpeed() { return preventAutoSleep(); } + // True for a screen whose renders should be watched by the task watchdog in every build, not + // only under DEBUG_RENDER_WATCHDOG. ActivityManager::renderTaskLoop subscribes the render task + // around each render of such a screen, so a render that never returns reboots the device + // (30 s, see the constructor) instead of leaving it dead until the battery is flat. Only for + // screens whose renders are known to be short: a first reader render can legitimately run for + // seconds on SD font cache generation and would be shot by this. + virtual bool watchesRender() const { return false; } // Exclusive storage activities suspend global controls and normal activity // transitions so no filesystem code races a raw SD-card owner. virtual bool requiresExclusiveStorageLoop() const { return false; } diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index a777b2faba3..8737db7124a 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -26,6 +27,7 @@ #include "settings/OpdsServerListActivity.h" #include "settings/SettingsActivity.h" #include "util/BmpViewerActivity.h" +#include "util/ClockActivity.h" #include "util/FrontlightPanelActivity.h" #include "util/FullScreenMessageActivity.h" #include "util/InputDiag.h" @@ -56,6 +58,29 @@ void ActivityManager::begin() { renderTaskCore // Keep long renders/cover decodes off CPU 0's idle watchdog when available ); assert(renderTaskHandle != nullptr && "Failed to create render task"); + + // A render that never returns leaves the device with no way back. The render task is + // subscribed to no watchdog and the idle task is not checked either (sdkconfig.default: + // CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 is not set), while powering off is decided in + // loop() -- the task the stuck render is starving. Nothing on the outside of the case + // can interrupt it: the reset button is under the glued-down screen. The only remedy is + // to flatten the battery, which takes a day or more. + // + // Watching a task turns that into a reboot, and the panic path leaves the stuck task's + // name in crash_report.txt (HalSystem: esp_task_wdt_isr_user_handler). Who is watched + // depends on the build: under DEBUG_RENDER_WATCHDOG every render is (renderTaskLoop); + // otherwise only the renders of screens that ask for it (Activity::watchesRender -- the + // clock, whose two hangs cost a day each) plus whatever those screens subscribe themselves. + // The timeout is set here for both cases and is deliberately loose: a first render can + // legitimately take seconds (SD font cache generation, cover decode), and this only has to + // tell "slow" apart from "never". The IDF default is 5 s, which a clock clean refresh + // (3.2 s) would sit uncomfortably close to. No task is subscribed by this call alone, so + // a build where nothing asks to be watched behaves exactly as before. + esp_task_wdt_config_t wdtConfig = {}; + wdtConfig.timeout_ms = 30000; + wdtConfig.idle_core_mask = 0; + wdtConfig.trigger_panic = true; + esp_task_wdt_reconfigure(&wdtConfig); } void ActivityManager::renderTaskTrampoline(void* param) { @@ -66,10 +91,27 @@ void ActivityManager::renderTaskTrampoline(void* param) { void ActivityManager::renderTaskLoop() { while (true) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); +#ifdef DEBUG_RENDER_WATCHDOG + // Subscribed around the render alone. This task spends the rest of its life blocked on + // the notify above, where a watchdog would fire on a device that is merely idle. The + // window it covers therefore includes waiting for the render lock, which is where a + // render already stuck would hold it. + esp_task_wdt_add(nullptr); +#endif // Acquire the lock before reading currentActivity to avoid a TOCTOU race // where the main task deletes the activity between the null-check and render(). RenderLock lock; if (currentActivity) { + // Per-screen watch for builds without DEBUG_RENDER_WATCHDOG (there the subscription above + // already covers this). Decided while the lock is held, so currentActivity is stable; the + // answer is kept on the stack because render() may release the lock and the main task may + // then destroy the activity. Subscribed before the power lock on purpose: the clock's + // deadlock (2026-09-02) was inside HalPowerManager::Lock, not inside render(). + bool watchedForScreen = false; +#ifndef DEBUG_RENDER_WATCHDOG + watchedForScreen = currentActivity->watchesRender(); + if (watchedForScreen) esp_task_wdt_add(nullptr); +#endif HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering #ifdef INPUT_DIAG // Snapshot the name onto the stack before rendering. render() receives the lock by value and @@ -101,6 +143,7 @@ void ActivityManager::renderTaskLoop() { InputDiag::captureLogs(reason); } #endif + if (watchedForScreen) esp_task_wdt_delete(nullptr); } // Notify any task blocked in requestUpdateAndWait() that the render is done. TaskHandle_t waiter = nullptr; @@ -111,6 +154,9 @@ void ActivityManager::renderTaskLoop() { if (waiter) { xTaskNotify(waiter, 1, eIncrement); } +#ifdef DEBUG_RENDER_WATCHDOG + esp_task_wdt_delete(nullptr); +#endif } } @@ -347,6 +393,9 @@ void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily:: replaceActivity(std::make_unique(renderer, mappedInput, std::move(message), style)); } +// Pushed, not replaced: Back leaves the clock and lands back on Home. +void ActivityManager::goToClock() { pushActivity(std::make_unique(renderer, mappedInput)); } + void ActivityManager::goHome(HomeMenuItem initialMenuItem, bool cleanInitialRefresh) { if (initialMenuItem == HomeMenuItem::NONE && currentActivity) { const auto& activityName = currentActivity->name; @@ -389,6 +438,8 @@ void ActivityManager::popActivity() { bool ActivityManager::preventAutoSleep() const { return currentActivity && currentActivity->preventAutoSleep(); } +bool ActivityManager::needsFullSpeed() const { return currentActivity && currentActivity->needsFullSpeed(); } + bool ActivityManager::requiresExclusiveStorageLoop() const { return currentActivity && currentActivity->requiresExclusiveStorageLoop(); } diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 9fbb0e7095e..454105cdaac 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -17,7 +17,7 @@ class Activity; // forward declaration class RenderLock; // forward declaration -enum class HomeMenuItem { NONE, FILE_BROWSER, LIBRARY, OPDS_BROWSER, FILE_TRANSFER, SETTINGS_MENU }; +enum class HomeMenuItem { NONE, FILE_BROWSER, LIBRARY, OPDS_BROWSER, FILE_TRANSFER, SETTINGS_MENU, CLOCK_MODE }; /** * ActivityManager @@ -84,6 +84,7 @@ class ActivityManager { void goToFileTransfer(); void goToUsbDrive(); void goToSettings(); + void goToClock(); void goToFileBrowser(std::string path = {}); void goToLibrary(); void goToBrowser(); @@ -102,6 +103,7 @@ class ActivityManager { void popActivity(); bool preventAutoSleep() const; + bool needsFullSpeed() const; bool requiresExclusiveStorageLoop() const; bool isReaderActivity() const; bool handleForcedRefresh(); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 63adbe3923a..884ef4a8509 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -27,7 +27,7 @@ #include "util/InputDiag.h" int HomeActivity::getMenuItemCount() const { - int count = 4; // File Browser, Library, File transfer, Settings + int count = 5; // File Browser, Library, File transfer, Settings, Clock mode if (!recentBooks.empty()) { count += recentBooks.size(); } @@ -325,6 +325,9 @@ void HomeActivity::loop() { case HomeMenuItem::SETTINGS_MENU: onSettingsOpen(); break; + case HomeMenuItem::CLOCK_MODE: + onClockModeStart(); + break; default: break; } @@ -511,8 +514,8 @@ void HomeActivity::render(RenderLock&&) { // Build menu items dynamically std::vector menuItems = {tr(STR_BROWSE_FILES), tr(STR_LIBRARY), tr(STR_FILE_TRANSFER), - tr(STR_SETTINGS_TITLE)}; - std::vector menuIcons = {Folder, Library, Transfer, Settings}; + tr(STR_SETTINGS_TITLE), tr(STR_START_CLOCK_MODE)}; + std::vector menuIcons = {Folder, Library, Transfer, Settings, Recent}; if (hasOpdsServers) { menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER)); @@ -562,3 +565,5 @@ void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); } void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); } void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); } + +void HomeActivity::onClockModeStart() { activityManager.goToClock(); } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index b9f5c2243b1..bca1200a4f9 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -46,6 +46,8 @@ class HomeActivity final : public Activity { if (item == HomeMenuItem::FILE_TRANSFER) return i; ++i; if (item == HomeMenuItem::SETTINGS_MENU) return i; + ++i; + if (item == HomeMenuItem::CLOCK_MODE) return i; return 0; } @@ -56,7 +58,8 @@ class HomeActivity final : public Activity { if (idx == i++) return HomeMenuItem::LIBRARY; if (hasOpdsUrl && idx == i++) return HomeMenuItem::OPDS_BROWSER; if (idx == i++) return HomeMenuItem::FILE_TRANSFER; - if (idx == i) return HomeMenuItem::SETTINGS_MENU; + if (idx == i++) return HomeMenuItem::SETTINGS_MENU; + if (idx == i) return HomeMenuItem::CLOCK_MODE; return HomeMenuItem::NONE; } void onSelectBook(const std::string& path); @@ -65,6 +68,7 @@ class HomeActivity final : public Activity { void onSettingsOpen(); void onFileTransferOpen(); void onOpdsBrowserOpen(); + void onClockModeStart(); int getMenuItemCount() const; bool storeCoverBuffer(); // Store frame buffer for cover image diff --git a/src/activities/util/ClockActivity.cpp b/src/activities/util/ClockActivity.cpp new file mode 100644 index 00000000000..9e44cd7a35c --- /dev/null +++ b/src/activities/util/ClockActivity.cpp @@ -0,0 +1,239 @@ +#include "ClockActivity.h" + +#include +#include +#include +#include +#include + +#include + +#include "CrossPointSettings.h" +#include "util/ClockFace.h" + +namespace { + +// Where the drain measurement lands. Separate from /input-diag.txt: that file is +// rewritten every few seconds by the render diagnostics, while this one holds +// two readings a session apart and has to survive between them. +constexpr char BATTERY_LOG_PATH[] = "/clock-battery.txt"; + +// How often the RTC is asked whether the minute has turned. Cheap next to the +// panel refresh it schedules -- one short I2C read against a second of waveform +// -- and it bounds how late a repaint can be by this much rather than by the +// loop period. +constexpr unsigned long MINUTE_POLL_MS = 500; + +// Repaint period used only when there is no readable clock to follow. +constexpr unsigned long BLIND_REDRAW_MS = 60000; + +// Minutes between the clean passes that clear what the fast updates leave. +constexpr uint8_t CLEAN_REFRESH_MINUTES = 30; + +} // namespace + +void ClockActivity::onEnter() { + Activity::onEnter(); + + // Watch the main loop while this screen is up, in every build. The renders are watched by + // ActivityManager (watchesRender). Both hangs seen here left Back dead, which is the main + // loop's job -- a watchdog on the render task alone would not have fired. Scoped to the clock + // because other screens block the main loop on purpose (web server, OPDS, font download), + // where a 30 s timeout would panic on healthy behaviour. A false trigger here costs a reboot + // and a crash report; the hang it guards against costs the owner a day. + esp_task_wdt_add(nullptr); + + startMs = millis(); + lastDrawMs = startMs; + lastPollMs = startMs; + haveStartTime = halClock.getLocalDateTime(startTime); + startPercent = powerManager.getBatteryPercentage(); + startMillivolts = powerManager.getBatteryMillivolts(); + + // Written now as well as on the way out, so a session that ends with a flat + // battery or a pulled card still leaves its starting point behind. + writeBatteryLog(/*finished=*/false); + + requestUpdate(); +} + +void ClockActivity::onExit() { + esp_task_wdt_delete(nullptr); + + writeBatteryLog(/*finished=*/true); + + // ClockFace::render leaves the renderer in the panel's native orientation. + // Every other screen is laid out for Portrait, so hand it back or Home comes + // up sideways. + renderer.setOrientation(GfxRenderer::Orientation::Portrait); + Activity::onExit(); +} + +void ClockActivity::loop() { + // Before the early returns below, so the feed covers every path through the loop. + esp_task_wdt_reset(); + + // No mappedInput.update() here. The main loop polls once per iteration just + // before calling this, and a second poll re-samples the pins and clears the + // edge it set -- the release would be gone before wasReleased() is asked. + // Only the activities that block inside their own inner loop poll for + // themselves. + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + finish(); + return; + } + +#if defined(DEBUG_RENDER_WATCHDOG) || defined(CLOCK_HANG_TRIGGERS) + // Self-test for the recovery path, not a test of the clock. CLOCK_HANG_TRIGGERS exists so + // the shipped watch (no DEBUG_RENDER_WATCHDOG) can be exercised on the device too. Both real hangs left the + // device dead for a day, and the watchdog that was supposed to prevent that has never + // once been seen to fire -- on 2026-08-27 the flag was passed to a build whose tree did + // not implement it, and it failed silently. These two triggers make the failure happen on + // purpose, so "the device reboots within 30 s and names the stuck task" is something + // observed rather than assumed. One trigger per watched task, because they are subscribed + // in different places and either subscription could be the one that is missing. + // Both front buttons other than Back, and both side buttons, so the tester does not have to + // know their own remapping to run this. Right is left out: on the X3 it shares GPIO3 with the + // power button, and a trigger that fires on power-off would be indistinguishable from a hang. + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || + mappedInput.wasReleased(MappedInputManager::Button::Left)) { + LOG_DBG("CLK", "forced hang: main task"); + while (true) { + delay(1000); // yields, so only the watchdog feed stops -- the CPU is not spun + } + } + if (mappedInput.wasReleased(MappedInputManager::Button::Up) || + mappedInput.wasReleased(MappedInputManager::Button::Down)) { + LOG_DBG("CLK", "forced hang: render task requested"); + forceRenderHang = true; + requestUpdate(); + return; + } +#endif + + const unsigned long now = millis(); + if (now - lastPollMs < MINUTE_POLL_MS) return; + lastPollMs = now; + + Rtc::DateTime current; + if (halClock.getLocalDateTime(current)) { + // Repaint when the minute turns, not a fixed period after the screen + // opened, so the face changes on the minute the way a clock should. The + // panel takes a second or so to settle, so the new digits land just after + // the boundary rather than exactly on it. + if (current.minute != renderedMinute) { + // Breadcrumbs, not tracing: the log ring lives in RTC memory and survives a panic + // reboot, so the last lines before a hang say which half of the minute cycle it was + // in. One line per minute keeps a few cycles of history in the 16-line ring. + LOG_DBG("CLK", "minute turn -> %02u:%02u", current.hour, current.minute); + renderedMinute = current.minute; + lastDrawMs = now; + requestUpdate(); + } + return; + } + + if (now - lastDrawMs >= BLIND_REDRAW_MS) { + lastDrawMs = now; + requestUpdate(); + } +} + +void ClockActivity::render(RenderLock&&) { +#if defined(DEBUG_RENDER_WATCHDOG) || defined(CLOCK_HANG_TRIGGERS) + if (forceRenderHang) { + LOG_DBG("CLK", "forced hang: render task"); + while (true) { + delay(1000); + } + } +#endif + + Rtc::DateTime now; + const bool haveTime = halClock.getLocalDateTime(now); + if (haveTime) { + renderedMinute = now.minute; + } else { + LOG_ERR("CLK", "Clock mode: RTC unavailable"); + } + + // Anchored to the wall clock rather than to a count, so the clean pass lands + // on the hour and the half hour however long the screen has been up. The + // counter only covers the case where there is no clock to anchor to. + bool clean = cleanPending; + if (!clean) { + clean = haveTime ? (now.minute % CLEAN_REFRESH_MINUTES == 0) : (++paintsSinceClean >= CLEAN_REFRESH_MINUTES); + } + if (clean) { + cleanPending = false; + paintsSinceClean = 0; + } + + LOG_DBG("CLK", "paint %02u:%02u %s%s", now.hour, now.minute, clean ? "clean" : "fast", haveTime ? "" : " (no RTC)"); + ClockFace::render(renderer, haveTime ? &now : nullptr, clean ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH, + SETTINGS.clockFormat == 1); +} + +void ClockActivity::writeBatteryLog(const bool finished) const { + char buf[512]; + int len = snprintf(buf, sizeof(buf), "# clock mode battery log\n"); + + if (haveStartTime) { + len += snprintf(buf + len, sizeof(buf) - len, "start %04u/%02u/%02u %02u:%02u:%02u bat=%u%% %umV\n", + startTime.year, startTime.month, startTime.day, startTime.hour, startTime.minute, startTime.second, + startPercent, startMillivolts); + } else { + len += snprintf(buf + len, sizeof(buf) - len, "start (no RTC) bat=%u%% %umV\n", startPercent, startMillivolts); + } + + if (finished) { + Rtc::DateTime endTime; + const bool haveEnd = halClock.getLocalDateTime(endTime); + const uint16_t endPercent = powerManager.getBatteryPercentage(); + const uint16_t endMillivolts = powerManager.getBatteryMillivolts(); + + if (haveEnd) { + len += + snprintf(buf + len, sizeof(buf) - len, "end %04u/%02u/%02u %02u:%02u:%02u bat=%u%% %umV\n", endTime.year, + endTime.month, endTime.day, endTime.hour, endTime.minute, endTime.second, endPercent, endMillivolts); + } else { + len += snprintf(buf + len, sizeof(buf) - len, "end (no RTC) bat=%u%% %umV\n", endPercent, endMillivolts); + } + + // Elapsed from millis() rather than the two timestamps: it is monotonic and + // does not care whether the RTC was readable at either end. + const unsigned long elapsedMs = millis() - startMs; + const unsigned long elapsedMin = elapsedMs / 60000UL; + const int dPercent = static_cast(endPercent) - static_cast(startPercent); + const int dMillivolts = static_cast(endMillivolts) - static_cast(startMillivolts); + len += snprintf(buf + len, sizeof(buf) - len, "elapsed %lu min d_bat=%+d%% d_mV=%+d\n", elapsedMin, dPercent, + dMillivolts); + + // Per-hour rates, in tenths so the line stays integer-only. Below a minute + // the extrapolation says more about the clock than the battery, so skip it. + if (elapsedMs >= 60000UL) { + const long perHourPercentTenths = static_cast(dPercent) * 10L * 3600000L / static_cast(elapsedMs); + const long perHourMv = static_cast(dMillivolts) * 3600000L / static_cast(elapsedMs); + // Sign printed separately. Splitting a negative tenths value into integer + // and fraction loses it whenever the integer part truncates to zero, which + // is every rate slower than 1 %/h -- i.e. every rate worth measuring. + len += snprintf(buf + len, sizeof(buf) - len, "rate %s%ld.%ld %%/h %ld mV/h\n", + perHourPercentTenths < 0 ? "-" : "", labs(perHourPercentTenths) / 10, + labs(perHourPercentTenths) % 10, perHourMv); + } + } else { + len += snprintf(buf + len, sizeof(buf) - len, "end (still running)\n"); + } + + if (len <= 0 || static_cast(len) >= sizeof(buf)) { + LOG_ERR("CLK", "Battery log did not fit"); + return; + } + + HalFile file; + if (!Storage.openFileForWrite("CLK", BATTERY_LOG_PATH, file)) { + LOG_ERR("CLK", "Failed to open %s", BATTERY_LOG_PATH); + return; + } + file.write(reinterpret_cast(buf), static_cast(len)); +} diff --git a/src/activities/util/ClockActivity.h b/src/activities/util/ClockActivity.h new file mode 100644 index 00000000000..3fcf800974b --- /dev/null +++ b/src/activities/util/ClockActivity.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +#include "activities/Activity.h" + +// Clock mode: a full-screen clock that repaints on the minute and does nothing +// else until Back leaves it. +// +// It stays resident rather than sleeping between updates because on this +// hardware it has to. Deep sleep here drives GPIO13 low, which cuts the battery +// rail -- measured 2026-08-17 with a wake counter that never left zero, so no +// deep-sleep timer can bring the device back. Staying awake is the only way to +// own the panel for longer than one frame. +class ClockActivity final : public Activity { + public: + ClockActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) : Activity("Clock", renderer, mappedInput) {} + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + + // The whole point of the mode is that it stays up: the inactivity timeout + // would otherwise put the device to sleep out from under it. + bool preventAutoSleep() override { return true; } + + // ...but without pinning the CPU. A clock is idle by nature, and the default + // (full speed for anything holding off auto-sleep) exists for activities doing + // real work between frames, which this one is not. + bool needsFullSpeed() override { return false; } + // Renders here are short (fast update ~0.45 s, clean refresh ~3.2 s) and happen without any + // user input, which is what made this the one screen that could hang unattended. + bool watchesRender() const override { return true; } + + private: + void writeBatteryLog(bool finished) const; + + // Minute last painted, so the repaint fires on the minute rather than a fixed + // period after whenever the screen was opened. 0xFF until the first paint. + uint8_t renderedMinute = 0xFF; + // Throttles the RTC poll that watches for the minute to turn. + unsigned long lastPollMs = 0; + // Fallback repaint clock, used only when the RTC cannot be read: without a + // time there is no minute to follow, and the screen should still refresh. + unsigned long lastDrawMs = 0; + + // Waveform policy. Minute updates change one or two digits, so they go out on + // the gentle FAST LUT; every half hour one update uses the single-pass HALF + // waveform (0xD7) to clear what FAST leaves behind. + // + // Not FULL: that selects the multi-flash GC waveform (0xF7), which the OEM + // firmware never runs in normal operation -- see the note on + // SleepActivity::renderDefaultSleepScreen. Driving it 1440 times a day is + // more particle cycling than a panel should be asked for, and it is also the + // slowest, which is why the face used to land 2.7 s after the minute turned. + bool cleanPending = true; // first paint after entering: the panel holds another screen + uint8_t paintsSinceClean = 0; // fallback counter for when there is no readable clock + + // Battery log endpoints. Captured on entry, written out with the closing + // reading so one session lands in the file as a pair. + Rtc::DateTime startTime{}; + bool haveStartTime = false; + unsigned long startMs = 0; + uint16_t startPercent = 0; + uint16_t startMillivolts = 0; + +#if defined(DEBUG_RENDER_WATCHDOG) || defined(CLOCK_HANG_TRIGGERS) + // Set by the Up trigger, read by render(): the hang has to happen inside the + // render task, which only runs when it is asked to paint. + bool forceRenderHang = false; +#endif +}; diff --git a/src/images/ClockDigits.h b/src/images/ClockDigits.h new file mode 100644 index 00000000000..7225941b745 --- /dev/null +++ b/src/images/ClockDigits.h @@ -0,0 +1,1312 @@ +#pragma once +#include + +// Generated by scripts/build_clock_digits.py -- do not edit. +// font=DejaVuSans-Bold.ttf large=150 small=56 rotate=0 +// +// Clock mode's glyphs, drawn with GfxRenderer::drawImage rather than a font: the +// timer-wake path never mounts the SD card, and the built-in fonts stop at 18 pt. +// The rotation is baked in because drawImage rotates the origin but not the bits. + +// Order matches ClockGlyph in ClockFace.h. + +#define CLOCKLARGE_WIDTH 91 +#define CLOCKLARGE_HEIGHT 125 +// Unrotated cell, for laying the face out in logical coordinates. +#define CLOCKLARGE_CELL_W 91 +#define CLOCKLARGE_CELL_H 125 + +// ClockLargeDigit0: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit0[] = { + 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x7f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, + 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, + 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, + 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, + 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, + 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, + 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, + 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, + 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0x80, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, + 0x00, 0x7f, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0x07, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, + 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, + 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, + 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x7f, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xf0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, + 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit1: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit1[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit2: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit2[] = { + 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xfc, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, + 0x07, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xfc, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xe0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit3: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit3[] = { + 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0x80, 0x00, 0x07, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x80, + 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x07, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x80, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf1, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x07, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x01, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xf0, 0x00, 0x03, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, 0x00, 0x0f, 0xff, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xf0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit4: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit4[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x80, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xe0, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0x80, 0x00, 0x07, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x1f, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x7f, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0, 0x00, + 0x03, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x07, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf0, 0x00, + 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xe0, 0x00, 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0x80, 0x00, 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit5: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit5[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x00, 0x00, 0x07, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0x00, 0x01, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, + 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf0, 0x00, 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xf0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit6: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit6[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xf8, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0xff, 0x80, 0x3f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xc0, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xbf, 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x00, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xc0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xc0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, + 0x00, 0x00, 0x07, 0xff, 0xff, 0x80, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xc0, 0x00, + 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0xff, 0xc0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, + 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, + 0x00, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x7f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xfe, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0x80, 0x00, 0x00, + 0x03, 0xff, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit7: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit7[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit8: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit8[] = { + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xf8, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, + 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, + 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, + 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, + 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, + 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, + 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, + 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x0f, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xc0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, + 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xc0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x0f, + 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xf0, + 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, + 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0x01, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, + 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, + 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, + 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xe0, + 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, + 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x01, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xf8, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x07, 0xff, + 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, + 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeDigit9: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeDigit9[] = { + 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0x7f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf0, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, + 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xe0, + 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, + 0x01, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, + 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, + 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, + 0xe0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, + 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0x7f, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xff, + 0xff, 0x80, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x7f, 0xe0, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, + 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x3c, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x07, 0xf8, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x1f, 0xf8, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x01, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0x3f, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x87, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0x80, 0x3f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x80, 0x03, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x80, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, + 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeColon: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeColon[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockLargeSlash: 91x125, 1bpp MSB-first, 0 = black +static const uint8_t ClockLargeSlash[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x07, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x01, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, + 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xc0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// Indexed by ClockGlyph. Every cell is 91x125, so the face advances a fixed pitch. +static const uint8_t* const ClockLargeGlyphs[] = { + ClockLargeDigit0, ClockLargeDigit1, ClockLargeDigit2, ClockLargeDigit3, ClockLargeDigit4, ClockLargeDigit5, + ClockLargeDigit6, ClockLargeDigit7, ClockLargeDigit8, ClockLargeDigit9, ClockLargeColon, ClockLargeSlash, +}; + +#define CLOCKSMALL_WIDTH 34 +#define CLOCKSMALL_HEIGHT 47 +// Unrotated cell, for laying the face out in logical coordinates. +#define CLOCKSMALL_CELL_W 34 +#define CLOCKSMALL_CELL_H 47 + +// ClockSmallDigit0: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit0[] = { + 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xc0, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0x00, 0x0f, + 0xff, 0xf8, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x01, 0xe0, + 0x01, 0xff, 0xe0, 0x07, 0xf8, 0x01, 0xff, 0xc0, 0x07, 0xf8, 0x00, 0xff, 0xc0, 0x0f, 0xfc, 0x00, 0xff, 0x80, 0x0f, + 0xfc, 0x00, 0x7f, 0x80, 0x0f, 0xfc, 0x00, 0x7f, 0x80, 0x0f, 0xfc, 0x00, 0x7f, 0x80, 0x1f, 0xfe, 0x00, 0x7f, 0x00, + 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, + 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, + 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x00, 0x1f, 0xfe, + 0x00, 0x3f, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0x80, 0x1f, 0xfe, 0x00, 0x7f, 0x80, 0x0f, 0xfc, 0x00, 0x7f, 0x80, 0x0f, + 0xfc, 0x00, 0x7f, 0x80, 0x0f, 0xfc, 0x00, 0x7f, 0xc0, 0x0f, 0xfc, 0x00, 0xff, 0xc0, 0x07, 0xf8, 0x00, 0xff, 0xe0, + 0x07, 0xf8, 0x01, 0xff, 0xe0, 0x01, 0xe0, 0x01, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x07, 0xff, + 0xf8, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xc0, 0x00, 0xff, + 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit1: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit1[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x03, 0xff, 0xff, 0xf0, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, + 0xff, 0xc0, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x00, 0x03, + 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x30, 0x03, 0xff, 0xff, 0xc7, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, + 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, + 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, + 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, + 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, + 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit2: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit2[] = { + 0xff, 0x80, 0x03, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x7f, 0xff, 0xc0, 0x00, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x00, 0x0f, + 0xff, 0xc0, 0x00, 0x00, 0x07, 0xff, 0xc0, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x3f, 0x80, + 0x01, 0xff, 0xc1, 0xff, 0xe0, 0x01, 0xff, 0xc7, 0xff, 0xf0, 0x00, 0xff, 0xdf, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, + 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x03, 0xff, + 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xfe, 0x00, 0x1f, + 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xc0, 0x03, + 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xfe, 0x00, 0x1f, 0xff, 0xff, 0xfc, 0x00, + 0x3f, 0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xff, 0xc0, + 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, + 0xc0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit3: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit3[] = { + 0xfe, 0x00, 0x07, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x1f, 0xff, 0xe0, 0x00, 0x00, 0x0f, + 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xe0, 0xff, 0x00, + 0x01, 0xff, 0xef, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, + 0xf0, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x03, 0xff, 0xff, + 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0x00, 0x07, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0x00, 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x07, + 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xf0, + 0x01, 0xff, 0xff, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf0, 0x00, 0xff, 0xbf, 0xff, 0xf0, 0x00, 0xff, 0x8f, + 0xff, 0xe0, 0x01, 0xff, 0x80, 0xff, 0x00, 0x01, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, + 0x80, 0x00, 0x00, 0x07, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0x80, 0x00, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x00, 0xff, + 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit4: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit4[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xfe, 0x00, 0x0f, + 0xff, 0xff, 0xfc, 0x00, 0x0f, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xff, 0xf0, 0x00, + 0x0f, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xc0, 0x00, 0x0f, 0xff, 0xff, 0x80, + 0x40, 0x0f, 0xff, 0xff, 0x80, 0xc0, 0x0f, 0xff, 0xff, 0x00, 0xc0, 0x0f, 0xff, 0xfe, 0x01, 0xc0, 0x0f, 0xff, 0xfe, + 0x03, 0xc0, 0x0f, 0xff, 0xfc, 0x03, 0xc0, 0x0f, 0xff, 0xf8, 0x07, 0xc0, 0x0f, 0xff, 0xf8, 0x0f, 0xc0, 0x0f, 0xff, + 0xf0, 0x0f, 0xc0, 0x0f, 0xff, 0xe0, 0x1f, 0xc0, 0x0f, 0xff, 0xe0, 0x3f, 0xc0, 0x0f, 0xff, 0xc0, 0x3f, 0xc0, 0x0f, + 0xff, 0xc0, 0x7f, 0xc0, 0x0f, 0xff, 0x80, 0xff, 0xc0, 0x0f, 0xff, 0x00, 0xff, 0xc0, 0x0f, 0xff, 0x01, 0xff, 0xc0, + 0x0f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, + 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, + 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit5: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit5[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x07, + 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, + 0x07, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x1f, + 0xff, 0xff, 0xff, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xe0, + 0x10, 0x07, 0xff, 0xff, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x00, 0x0f, 0xff, + 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xe0, 0xff, 0x00, 0x01, + 0xff, 0xe7, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xf8, + 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xbf, 0xff, 0xf0, 0x00, 0xff, 0x8f, 0xff, 0xe0, 0x01, 0xff, 0x83, + 0xff, 0xc0, 0x01, 0xff, 0x80, 0x7f, 0x00, 0x01, 0xff, 0x80, 0x00, 0x00, 0x03, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, + 0x80, 0x00, 0x00, 0x0f, 0xff, 0x80, 0x00, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xff, 0xff, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit6: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit6[] = { + 0xff, 0xfe, 0x00, 0x1f, 0xff, 0xff, 0xf0, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, + 0xff, 0xfe, 0x00, 0x00, 0x03, 0xff, 0xfc, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x3f, + 0x83, 0xff, 0xf0, 0x01, 0xff, 0xfb, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x07, + 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x1f, 0xff, 0xff, 0xff, 0xc0, + 0x1f, 0x00, 0xff, 0xff, 0x80, 0x18, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, + 0x80, 0x00, 0x00, 0x03, 0xff, 0x80, 0x00, 0x00, 0x01, 0xff, 0x80, 0x00, 0x00, 0x01, 0xff, 0x80, 0x01, 0xf0, 0x00, + 0xff, 0x80, 0x03, 0xf8, 0x00, 0xff, 0x80, 0x07, 0xfc, 0x00, 0x7f, 0x80, 0x07, 0xfc, 0x00, 0x7f, 0x80, 0x0f, 0xfe, + 0x00, 0x7f, 0x80, 0x0f, 0xfe, 0x00, 0x7f, 0x80, 0x0f, 0xfe, 0x00, 0x7f, 0xc0, 0x0f, 0xfe, 0x00, 0x7f, 0xc0, 0x0f, + 0xfe, 0x00, 0x7f, 0xc0, 0x0f, 0xfe, 0x00, 0x7f, 0xe0, 0x07, 0xfc, 0x00, 0x7f, 0xe0, 0x07, 0xfc, 0x00, 0xff, 0xe0, + 0x03, 0xf8, 0x00, 0xff, 0xf0, 0x01, 0xf0, 0x01, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xf8, 0x00, 0x00, 0x03, 0xff, + 0xfc, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xc0, 0x00, 0x7f, + 0xff, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit7: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit7[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, + 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, + 0x00, 0xff, 0x80, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, + 0xf0, 0x03, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, + 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0x80, 0x0f, 0xff, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x1f, 0xff, + 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xfe, 0x00, 0x7f, + 0xff, 0xff, 0xfc, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x01, + 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, + 0x80, 0x1f, 0xff, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, + 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xfc, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit8: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit8[] = { + 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x07, + 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x03, 0xf0, + 0x00, 0xff, 0xc0, 0x07, 0xf8, 0x00, 0xff, 0xc0, 0x0f, 0xfc, 0x00, 0xff, 0xc0, 0x0f, 0xfc, 0x00, 0xff, 0xc0, 0x0f, + 0xfc, 0x00, 0xff, 0xc0, 0x0f, 0xfc, 0x00, 0xff, 0xc0, 0x0f, 0xfc, 0x00, 0xff, 0xe0, 0x07, 0xf8, 0x01, 0xff, 0xf0, + 0x03, 0xf0, 0x03, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfe, 0x00, 0x00, 0x1f, 0xff, + 0xff, 0x80, 0x00, 0x7f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x03, + 0xff, 0xe0, 0x03, 0xf0, 0x01, 0xff, 0xc0, 0x07, 0xf8, 0x00, 0xff, 0xc0, 0x0f, 0xfc, 0x00, 0xff, 0x80, 0x1f, 0xfe, + 0x00, 0x7f, 0x80, 0x1f, 0xfe, 0x00, 0x7f, 0x80, 0x1f, 0xfe, 0x00, 0x7f, 0x80, 0x1f, 0xfe, 0x00, 0x7f, 0x80, 0x1f, + 0xfe, 0x00, 0x7f, 0x80, 0x1f, 0xfe, 0x00, 0x7f, 0x80, 0x1f, 0xfe, 0x00, 0x7f, 0x80, 0x0f, 0xfc, 0x00, 0x7f, 0xc0, + 0x07, 0xf8, 0x00, 0xff, 0xc0, 0x03, 0xf0, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x00, 0x01, 0xff, + 0xf0, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallDigit9: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallDigit9[] = { + 0xff, 0xf0, 0x07, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0x00, 0x1f, + 0xff, 0xf8, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x03, 0xe0, + 0x03, 0xff, 0xc0, 0x07, 0xf0, 0x01, 0xff, 0xc0, 0x0f, 0xf8, 0x01, 0xff, 0x80, 0x1f, 0xf8, 0x01, 0xff, 0x80, 0x1f, + 0xfc, 0x00, 0xff, 0x80, 0x1f, 0xfc, 0x00, 0xff, 0x80, 0x1f, 0xfc, 0x00, 0xff, 0x80, 0x1f, 0xfc, 0x00, 0xff, 0x80, + 0x1f, 0xfc, 0x00, 0x7f, 0x80, 0x1f, 0xfc, 0x00, 0x7f, 0x80, 0x0f, 0xf8, 0x00, 0x7f, 0x80, 0x0f, 0xf8, 0x00, 0x7f, + 0xc0, 0x07, 0xf0, 0x00, 0x7f, 0xc0, 0x03, 0xe0, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, + 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xfc, 0x00, 0x00, 0x00, 0x7f, 0xff, 0x00, 0x06, + 0x00, 0x7f, 0xff, 0xc0, 0x3e, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0xff, 0xff, 0xff, 0xfc, 0x00, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xf7, + 0xff, 0xe0, 0x03, 0xff, 0xf0, 0x7f, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x07, 0xff, 0xf0, 0x00, 0x00, 0x0f, 0xff, + 0xf0, 0x00, 0x00, 0x1f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xff, 0xf0, 0x00, 0x03, 0xff, + 0xff, 0xfe, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallColon: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallColon[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, + 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, + 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, + 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, + 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, + 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockSmallSlash: 34x47, 1bpp MSB-first, 0 = black +static const uint8_t ClockSmallSlash[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xf0, 0x3f, + 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, + 0x7f, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xff, + 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, + 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, + 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, + 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xf8, 0x1f, + 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xf0, + 0x3f, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, + 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, + 0xff, 0xc0, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, + 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xfe, 0x07, 0xff, + 0xff, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, +}; + +// Indexed by ClockGlyph. Every cell is 34x47, so the face advances a fixed pitch. +static const uint8_t* const ClockSmallGlyphs[] = { + ClockSmallDigit0, ClockSmallDigit1, ClockSmallDigit2, ClockSmallDigit3, ClockSmallDigit4, ClockSmallDigit5, + ClockSmallDigit6, ClockSmallDigit7, ClockSmallDigit8, ClockSmallDigit9, ClockSmallColon, ClockSmallSlash, +}; + +// --- 12-hour AM/PM markers --- +// Same generator, appended later (freetype's mono hinting differs across +// versions, so regenerating the digit sets above would have subtly changed +// the already-verified 24-hour face; only this set was taken from the rerun). +// font=DejaVuSans-Bold.ttf size=56 rotate=0, glyph order: A, P, M. + +// Rows below the baseline in each set's cell (the generator now emits these; +// added here by hand because the digit sets above are kept byte-identical). +// '/' descends 14px at 150px and 5px at 56px; A/P/M sit on the baseline. +#define CLOCKLARGE_DESCENT 14 +#define CLOCKSMALL_DESCENT 5 +#define CLOCKAMPM_DESCENT 0 + +#define CLOCKAMPM_WIDTH 45 +#define CLOCKAMPM_HEIGHT 41 +// Unrotated cell, for laying the face out in logical coordinates. +#define CLOCKAMPM_CELL_W 45 +#define CLOCKAMPM_CELL_H 41 + +// ClockAmPmLetterA: 45x41, 1bpp MSB-first, 0 = black +static const uint8_t ClockAmPmLetterA[] = { + 0xff, 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x03, 0xff, 0xff, 0xff, + 0xfe, 0x00, 0x03, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x03, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x01, 0xff, 0xff, 0xff, 0xfc, + 0x00, 0x01, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xf0, 0x02, 0x00, 0x7f, 0xff, 0xff, 0xf0, 0x02, 0x00, 0x7f, 0xff, 0xff, 0xf0, 0x02, 0x00, + 0x7f, 0xff, 0xff, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xc0, 0x07, 0x80, 0x1f, + 0xff, 0xff, 0xc0, 0x0f, 0x80, 0x1f, 0xff, 0xff, 0xc0, 0x0f, 0x80, 0x1f, 0xff, 0xff, 0x80, 0x1f, 0xc0, 0x0f, 0xff, + 0xff, 0x80, 0x1f, 0xc0, 0x0f, 0xff, 0xff, 0x80, 0x1f, 0xc0, 0x0f, 0xff, 0xff, 0x00, 0x3f, 0xe0, 0x07, 0xff, 0xff, + 0x00, 0x3f, 0xe0, 0x07, 0xff, 0xfe, 0x00, 0x3f, 0xe0, 0x03, 0xff, 0xfe, 0x00, 0x7f, 0xf0, 0x03, 0xff, 0xfe, 0x00, + 0x7f, 0xf0, 0x03, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x00, + 0x00, 0x01, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x03, 0xff, 0xfe, 0x00, + 0x3f, 0xe0, 0x03, 0xff, 0xfe, 0x00, 0x3f, 0xe0, 0x07, 0xff, 0xff, 0x00, 0x3f, 0xc0, 0x07, 0xff, 0xff, 0x00, 0x1f, + 0xc0, 0x07, 0xff, 0xff, 0x00, 0x1f, 0xc0, 0x0f, 0xff, 0xff, 0x80, 0x1f, 0x80, 0x0f, 0xff, 0xff, 0x80, 0x0f, +}; + +// ClockAmPmLetterP: 45x41, 1bpp MSB-first, 0 = black +static const uint8_t ClockAmPmLetterP[] = { + 0xf8, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0xff, 0x80, 0x03, 0xff, 0xf8, 0x00, 0xff, + 0xc0, 0x01, 0xff, 0xf8, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xf8, 0x00, 0xff, 0xf0, 0x01, 0xff, 0xf8, 0x00, 0xff, 0xf0, + 0x01, 0xff, 0xf8, 0x00, 0xff, 0xf0, 0x01, 0xff, 0xf8, 0x00, 0xff, 0xf0, 0x01, 0xff, 0xf8, 0x00, 0xff, 0xf0, 0x01, + 0xff, 0xf8, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xf8, 0x00, 0xff, 0xc0, 0x01, 0xff, 0xf8, 0x00, 0xff, 0x80, 0x03, 0xff, + 0xf8, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf8, + 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xf8, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, +}; + +// ClockAmPmLetterM: 45x41, 1bpp MSB-first, 0 = black +static const uint8_t ClockAmPmLetterM[] = { + 0x00, 0x03, 0xff, 0xfe, 0x00, 0x07, 0x00, 0x03, 0xff, 0xfe, 0x00, 0x07, 0x00, 0x03, 0xff, 0xfe, 0x00, 0x07, 0x00, + 0x01, 0xff, 0xfc, 0x00, 0x07, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x07, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x07, 0x00, 0x00, + 0xff, 0xf8, 0x00, 0x07, 0x00, 0x00, 0xff, 0xf0, 0x00, 0x07, 0x00, 0x00, 0x7f, 0xf0, 0x00, 0x07, 0x00, 0x00, 0x7f, + 0xf0, 0x00, 0x07, 0x00, 0x00, 0x3f, 0xe0, 0x00, 0x07, 0x00, 0x00, 0x3f, 0xe0, 0x00, 0x07, 0x00, 0x20, 0x1f, 0xc0, + 0x20, 0x07, 0x00, 0x20, 0x1f, 0xc0, 0x20, 0x07, 0x00, 0x20, 0x1f, 0xc0, 0x20, 0x07, 0x00, 0x30, 0x0f, 0x80, 0x60, + 0x07, 0x00, 0x30, 0x0f, 0x80, 0x60, 0x07, 0x00, 0x38, 0x07, 0x00, 0xe0, 0x07, 0x00, 0x38, 0x07, 0x00, 0xe0, 0x07, + 0x00, 0x3c, 0x06, 0x01, 0xe0, 0x07, 0x00, 0x3c, 0x02, 0x01, 0xe0, 0x07, 0x00, 0x3c, 0x02, 0x01, 0xe0, 0x07, 0x00, + 0x3e, 0x00, 0x03, 0xe0, 0x07, 0x00, 0x3e, 0x00, 0x03, 0xe0, 0x07, 0x00, 0x3f, 0x00, 0x07, 0xe0, 0x07, 0x00, 0x3f, + 0x00, 0x07, 0xe0, 0x07, 0x00, 0x3f, 0x80, 0x07, 0xe0, 0x07, 0x00, 0x3f, 0x80, 0x0f, 0xe0, 0x07, 0x00, 0x3f, 0x80, + 0x0f, 0xe0, 0x07, 0x00, 0x3f, 0xc0, 0x1f, 0xe0, 0x07, 0x00, 0x3f, 0xc0, 0x1f, 0xe0, 0x07, 0x00, 0x3f, 0xe0, 0x3f, + 0xe0, 0x07, 0x00, 0x3f, 0xe0, 0x3f, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, + 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, + 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, +}; + +// Indexed by ClockGlyph. Every cell is 45x41, so the face advances a fixed pitch. +static const uint8_t* const ClockAmPmGlyphs[] = { + ClockAmPmLetterA, + ClockAmPmLetterP, + ClockAmPmLetterM, +}; diff --git a/src/main.cpp b/src/main.cpp index 31086ceac58..d47af873ae6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -652,12 +652,26 @@ void loop() { } } - // Check for any user activity (button press or release) or active background work - static unsigned long lastActivityTime = millis(); - if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || gpio.wasTouchActivity() || halTiltSensor.hadActivity() || - activityManager.preventAutoSleep()) { - lastActivityTime = millis(); // Reset inactivity timer - powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity + // Check for any user activity (button press or release) or active background work. + // Two clocks, because "do not sleep the device" and "the user is still doing + // things" are different claims. An activity that holds off auto-sleep can be + // idle for hours -- the clock is -- and it should still downclock between + // repaints like any other idle screen. + static unsigned long lastActivityTime = millis(); // gates the auto-sleep timeout + static unsigned long lastInputTime = millis(); // gates downclocking + const bool userActivity = + gpio.wasAnyPressed() || gpio.wasAnyReleased() || gpio.wasTouchActivity() || halTiltSensor.hadActivity(); + if (userActivity) { + lastInputTime = millis(); + } + if (userActivity || activityManager.preventAutoSleep()) { + lastActivityTime = millis(); // Reset inactivity timer + } + // Separate again: an activity can need to stay awake without needing the clock + // speed. needsFullSpeed() defaults to preventAutoSleep(), so everything that + // held the CPU up before still does. + if (userActivity || activityManager.needsFullSpeed()) { + powerManager.setPowerSaving(false); // Restore normal CPU frequency } // Let wake continue as soon as its hold has been verified. The release can @@ -835,7 +849,9 @@ void loop() { powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested yield(); // Give FreeRTOS a chance to run tasks, but return immediately } else { - if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) { + // Keyed on input, not the auto-sleep timer: an activity that holds off + // auto-sleep (the clock) still downclocks once the user stops pressing. + if (millis() - lastInputTime >= HalPowerManager::IDLE_POWER_SAVING_MS) { // If we've been inactive for a while, increase the delay to save power powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity // Sleep in short slices and wake the poll as soon as a button contact closes. diff --git a/src/util/ClockFace.cpp b/src/util/ClockFace.cpp new file mode 100644 index 00000000000..66f59799132 --- /dev/null +++ b/src/util/ClockFace.cpp @@ -0,0 +1,140 @@ +#include "ClockFace.h" + +#include "images/ClockDigits.h" + +namespace { + +// Index into ClockLargeGlyphs / ClockSmallGlyphs. Matches the GLYPHS order in +// scripts/build_clock_digits.py. +constexpr uint8_t GLYPH_COLON = 10; +constexpr uint8_t GLYPH_SLASH = 11; + +// Gap between cells. The generator already centres each glyph in a common box, +// so this is spacing between boxes rather than per-glyph side bearing. +constexpr int LARGE_GAP = 6; +constexpr int SMALL_GAP = 3; + +// Between the date line and the time line. +constexpr int LINE_GAP = 40; + +struct GlyphSet { + const uint8_t* const* glyphs; + int width; + int height; + int gap; + int descent; // rows below the baseline in the cell; differs per set ('/' descends, A/P/M don't) +}; + +constexpr GlyphSet LARGE{ClockLargeGlyphs, CLOCKLARGE_WIDTH, CLOCKLARGE_HEIGHT, LARGE_GAP, CLOCKLARGE_DESCENT}; +constexpr GlyphSet SMALL{ClockSmallGlyphs, CLOCKSMALL_WIDTH, CLOCKSMALL_HEIGHT, SMALL_GAP, CLOCKSMALL_DESCENT}; +constexpr GlyphSet AMPM{ClockAmPmGlyphs, CLOCKAMPM_WIDTH, CLOCKAMPM_HEIGHT, SMALL_GAP, CLOCKAMPM_DESCENT}; + +// Index into ClockAmPmGlyphs (glyph order: A, P, M). +constexpr uint8_t GLYPH_A = 0; +constexpr uint8_t GLYPH_P = 1; +constexpr uint8_t GLYPH_M = 2; + +// Between the time digits and the AM/PM marker. +constexpr int AMPM_GAP = 18; + +int lineWidth(const GlyphSet& set, const uint8_t count) { + if (count == 0) return 0; + return count * set.width + (count - 1) * set.gap; +} + +void drawLine(const GfxRenderer& renderer, const GlyphSet& set, const uint8_t* indices, const uint8_t count, + const int originX, const int y) { + int x = originX; + for (uint8_t i = 0; i < count; i++) { + renderer.drawImage(set.glyphs[indices[i]], x, y, set.width, set.height); + x += set.width + set.gap; + } +} + +// YYYY/MM/DD as glyph indices. +uint8_t buildDate(const Rtc::DateTime& now, uint8_t* out) { + uint8_t n = 0; + const uint16_t year = now.year; + out[n++] = static_cast((year / 1000) % 10); + out[n++] = static_cast((year / 100) % 10); + out[n++] = static_cast((year / 10) % 10); + out[n++] = static_cast(year % 10); + out[n++] = GLYPH_SLASH; + out[n++] = static_cast(now.month / 10); + out[n++] = static_cast(now.month % 10); + out[n++] = GLYPH_SLASH; + out[n++] = static_cast(now.day / 10); + out[n++] = static_cast(now.day % 10); + return n; +} + +// HH:MM (or H:MM in 12-hour style, which drops the leading zero) as glyph indices. +uint8_t buildTime(const Rtc::DateTime& now, const bool twelveHour, uint8_t* out) { + uint8_t n = 0; + uint8_t hour = now.hour; + if (twelveHour) { + hour = static_cast(hour % 12); + if (hour == 0) hour = 12; + } + if (!twelveHour || hour >= 10) { + out[n++] = static_cast(hour / 10); + } + out[n++] = static_cast(hour % 10); + out[n++] = GLYPH_COLON; + out[n++] = static_cast(now.minute / 10); + out[n++] = static_cast(now.minute % 10); + return n; +} + +} // namespace + +namespace ClockFace { + +void render(const GfxRenderer& renderer, const Rtc::DateTime* now, const HalDisplay::RefreshMode mode, + const bool twelveHour) { + // Native panel orientation: rotateCoordinates is the identity here, so the + // bitmaps land exactly as generated. Any turn the device needs is baked into + // them by scripts/build_clock_digits.py --rotate. + const_cast(renderer).setOrientation(GfxRenderer::LandscapeCounterClockwise); + + renderer.clearScreen(); + + const int screenW = renderer.getScreenWidth(); + const int screenH = renderer.getScreenHeight(); + + if (now != nullptr) { + uint8_t date[12]; + uint8_t time[8]; + const uint8_t dateCount = buildDate(*now, date); + const uint8_t timeCount = buildTime(*now, twelveHour, time); + + const int blockH = SMALL.height + LINE_GAP + LARGE.height; + const int topY = (screenH - blockH) / 2; + const int timeY = topY + SMALL.height + LINE_GAP; + + drawLine(renderer, SMALL, date, dateCount, (screenW - lineWidth(SMALL, dateCount)) / 2, topY); + + if (twelveHour) { + // Centre the digits and the marker as one composition. The marker slot + // works like a segment LCD's: AM sits in the upper position (top of the + // digits), PM in the lower one. The lower position aligns by BASELINE, + // not by cell bottom -- the digit set's cell extends below the baseline + // for '/', the letter set's does not, so bottom edges sit 14px apart. + const bool isAm = now->hour < 12; + const uint8_t marker[2] = {isAm ? GLYPH_A : GLYPH_P, GLYPH_M}; + const int digitsW = lineWidth(LARGE, timeCount); + const int markerW = lineWidth(AMPM, 2); + const int originX = (screenW - (digitsW + AMPM_GAP + markerW)) / 2; + const int baselineY = timeY + LARGE.height - LARGE.descent; + const int markerY = isAm ? timeY : baselineY - (AMPM.height - AMPM.descent); + drawLine(renderer, LARGE, time, timeCount, originX, timeY); + drawLine(renderer, AMPM, marker, 2, originX + digitsW + AMPM_GAP, markerY); + } else { + drawLine(renderer, LARGE, time, timeCount, (screenW - lineWidth(LARGE, timeCount)) / 2, timeY); + } + } + + renderer.displayBuffer(mode); +} + +} // namespace ClockFace diff --git a/src/util/ClockFace.h b/src/util/ClockFace.h new file mode 100644 index 00000000000..2cb5cca4a7d --- /dev/null +++ b/src/util/ClockFace.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +// Clock mode's face: "YYYY/MM/DD" over "HH:MM", drawn from the 1bpp bitmaps in +// src/images/ClockDigits.h. +// +// Deliberately free of everything the rest of the UI needs. It takes no +// activity, no theme, no font and no settings, because the path that calls it +// most often is a timer wake that has not mounted the SD card and never will -- +// see the clock-mode branch in setup(). Fonts would break that: an SD font is +// on the card, and the built-in ones stop at 18 pt. +namespace ClockFace { + +// Draws the face and pushes it to the panel. Sets the orientation itself: the +// bitmaps carry their own rotation (drawImage rotates the origin but not the +// bits), so they are laid out in the panel's native orientation and the caller +// does not get to choose. +// +// `now` is null when the RTC could not be read; the face is then blank rather +// than showing a time that is not the time. +// +// `mode` is the caller's, because the right waveform depends on how often this +// runs rather than on what it draws. See ClockActivity for the policy. +// +// `twelveHour` mirrors the status bar's Clock Format setting: false keeps the +// 24-hour "HH:MM", true switches to "H:MM" with an AM/PM marker beside it. +// Passed as a value so this stays free of CrossPointSettings (see above). +void render(const GfxRenderer& renderer, const Rtc::DateTime* now, HalDisplay::RefreshMode mode, bool twelveHour); + +} // namespace ClockFace From b4fb5a6e8b7ca9540b72c383addf28416a5432ce Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 14:26:05 +0900 Subject: [PATCH 03/59] =?UTF-8?q?README:=20=E4=BD=9C=E3=82=8A=E7=9B=B4?= =?UTF-8?q?=E3=81=97=E3=81=AE=E6=AE=B5=E9=9A=8E2=EF=BC=88=E6=99=82?= =?UTF-8?q?=E8=A8=88=E3=81=A8=E7=9B=A3=E8=A6=96=EF=BC=89=E3=81=AE=E7=B5=90?= =?UTF-8?q?=E6=9E=9C=E3=81=A8=E3=80=81=E6=97=A5=E6=9C=AC=E6=99=82=E9=96=93?= =?UTF-8?q?=E3=81=AE=E8=A8=AD=E5=AE=9A=E5=A0=B4=E6=89=80=E3=81=AE=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8610a472065..31e3ba6a213 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ XTEINK X3上で日本語EPUBファイルを読むために、CrossPoint Reader - 青空文庫ダウンロードなどの機能は付けない - 時計表示機能を追加 - 磁石で貼り付けることができるので作業時の時計表示に使えるな、と実装 - - [Settings]-[Reader]-[Customize Status Bar]の[Clock UTC Offset]を「UTC+9:00」とすることで日本時間表示に + - [Settings]-[System]-[Clock]の[Timezone]を「Tokyo / Seoul」とすることで日本時間表示に(1.6.5 から。1.6.0 ベースの版では[Settings]-[Reader]-[Customize Status Bar]の[Clock UTC Offset]を「UTC+9:00」) - テスト用EPUBファイルにて動作を確認 - 一部、特殊なフォントを必要とする文字以外について表示できることが期待される - ルビ表示について対応 @@ -93,7 +93,7 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ # 最近の更新内容について(2026/09/28) - 本家 1.6.5(2026/09/27 リリース)を起点に作り直しを開始(2026/09/28〜、ブランチ `vertical-1.6.5`) - - 段階0(素の 1.6.5 のビルド)と段階1(診断)まで完了。詳細は下の「1.6.5ベースへの作り直し」節 + - 段階0(素の 1.6.5 のビルド)、段階1(診断)、段階2(時計と監視)まで完了。詳細は下の「1.6.5ベースへの作り直し」節 - この時点のブランチには日本語フォントの扱い・縦書き・挿絵などはまだ入っていない(以下の各項目は 1.6.0 ベースの `feat/vertical-1.6.0based` の内容) - 章の境目の待ちを減らすため、読書中の待機時間に次の章を先に組み立てる(2026/09/09〜) - 商業書籍で章をまたぐたびに7秒前後待つのを、ページを表示して1.5秒以上操作が無い間に少しずつ済ませる @@ -225,7 +225,7 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ |---|---|---| | 0 | 素の 1.6.5 のビルド | 完了(RAM 57,912B/flash 5,621,527B) | | 1 | 診断(INPUT_DIAG、ビルド番号) | 完了(`2026092803-diag`、実機確認済み) | -| 2 | 時計と監視(#3562 の地域・夏時間の設定に載せ替え) | | +| 2 | 時計と監視(#3562 の地域・夏時間の設定に載せ替え) | 完了(`2026092806-diag`、実機確認済み) | | 3 | 日本語フォント(生成フォントと描画の追加分。メモリ処理は入れない)+素の実測① | | | 4 | 横書きの組版(禁則、長い段落) | | | 5 | 縦書きの中核 | | @@ -247,6 +247,13 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ - 空きの底は 11.4KB(最大連続 4.6KB)で、アンチエイリアスの帯描画の 8KB 作業領域を取った直後。その時点の持ち主の上位は、画面用の記憶 53,248B(SDK が起動時にヒープに取る。旧ツリーでも同じ)、設定項目の一覧 9,216B(本家は起動中ずっと保持。旧ツリーでは `8cc5f333` で使うときだけ作る形にした)、読書中の CSS 6,912B+4,352B、SD 書体の一覧 5,376B と書体ファイル名の文字列 35 個 3,272B、章の組み立て中の XML 読み取り用 4,352B - Home の初回描画 7.1 秒は表紙の縮小画像の作成(823KB の JPEG の展開 3 秒+変換 2.5 秒)。初回だけ +### 段階2: 時計と監視(2026-09-28、`2026092806-diag`、実機確認済み) +- 時計モードの画面・数字の画像・12時間表示は旧ツリーのまま。Home のメニューの末尾(Settings の次)に「Start clock mode」 +- 時刻は本家 #3562 の地域・夏時間の設定に従う。本家の時刻の読み出しは 10 秒ごとにしか RTC を読まず秒が古くなるため、時計の画面向けに RTC を毎回読んで地域の規則で変換する `HalClock::getLocalDateTime()` を足した。旧版の UTC の時差を自前で足す計算は廃止 + - 1.6.0 の「UTC+9」の設定は本家の移行処理で一覧の「UTC+9」として引き継がれ、表示は変わらなかった。「Tokyo / Seoul」に切り替えても同じ +- 時計の描画と時計表示中のメイン処理を 30 秒の監視に登録(すべてのビルド)。CPU の速度切り替えの直列化、電池電圧の記録、「自動スリープさせない」と「速度を保つ」の区別(時計表示中は 10MHz)も旧ツリーのまま +- 実機確認: わざと固まらせる検証ボタン(CLOCK_HANG_TRIGGERS)で、メイン処理(Confirm)・描画(側面ボタン)とも 30 秒以内に再起動し、`crash_report.txt` にそれぞれ `loopTask`/`ActivityManager` の名前が残った。時計表示中は `cpu_mhz_min=10`、ボタンの読み取り間隔 76ms 以内、空き 78〜80KB で一定 + ## 1.6.0ベースへの作り直し(2026/09/06〜) by claude code 本家1.6.0が2026/09/05に正式リリースされたため、方針どおり**1.6.0(`54337e6d`)から新ブランチ From cfb228662229c9d8754bb4514fbb0df6806e4f10 Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 14:59:43 +0900 Subject: [PATCH 04/59] =?UTF-8?q?diag:=20=E5=AD=97=E5=BD=A2=E3=81=AE?= =?UTF-8?q?=E8=A8=88=E6=B8=AC=E3=82=92=E5=85=A5=E3=82=8C=E3=80=81=E7=95=B0?= =?UTF-8?q?=E4=BD=93=E5=AD=97=E9=81=B8=E6=8A=9E=E5=AD=90=E3=82=92=E8=AA=AD?= =?UTF-8?q?=E3=81=BF=E9=A3=9B=E3=81=B0=E3=81=99=EF=BC=88=E4=BD=9C=E3=82=8A?= =?UTF-8?q?=E7=9B=B4=E3=81=97=E3=81=AE=E6=AE=B5=E9=9A=8E3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SdCardFont に計測用の数を足す(メモリの扱いは変えない): overflow で1字 ずつ読んだ回数、置き場の作り直しの回数と時間、prewarm の入口での断念、 幅の表の作業量と上限超え、置き場の解放記録、本文の書体(最初に読んだ 大きさ)。FontCacheManager は描画前の走査の結果と書体数の不足を数える (走査できる書体数は 4 のまま)。GfxRenderer で集計し、描画ごと・ページ ごと・章の組み立てごとに InputDiag に渡す。 章の本文で異体字選択子(VS1〜VS16、IVS VS17〜VS256)を読み飛ばし、親字 だけを描く(旧ツリー 1e8ca4f7)。組み方が変わるので章の版 48→49。 素の実測①(2026092807-diag、商業書籍の横書き、NotoSansJP 18pt): 離れた 章への移動 39.2 秒。幅の表 768 字の上限を超えた後、幅の測定ごとに字形の 画像まで読む(1回の描画で 2,817 字)。空きの底 2.7KB(本の開き直し)、 読んだ後の Home は最大連続 17.4KB。ボタンの取りこぼし無し。 非診断ビルドも通る(RAM 58,056B)。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- docs/file-formats.md | 9 ++- lib/EpdFont/SdCardFont.cpp | 65 ++++++++++++++++++- lib/EpdFont/SdCardFont.h | 42 +++++++++++- lib/EpdFont/SdCardFontManager.cpp | 3 +- lib/Epub/Epub/Section.cpp | 3 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 22 +++++++ lib/GfxRenderer/FontCacheManager.cpp | 19 +++++- lib/GfxRenderer/FontCacheManager.h | 13 ++++ lib/GfxRenderer/GfxRenderer.cpp | 53 +++++++++++++++ lib/GfxRenderer/GfxRenderer.h | 10 +++ src/activities/ActivityManager.cpp | 8 ++- src/activities/reader/EpubReaderActivity.cpp | 9 +++ src/util/InputDiag.cpp | 16 ++++- 13 files changed, 260 insertions(+), 12 deletions(-) diff --git a/docs/file-formats.md b/docs/file-formats.md index 8e0c4d2529c..d52bc7adeab 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -90,6 +90,13 @@ if (parsedSize != fileSize) { ## `section.bin` +### Version 49 + +Version 49 keeps the version 48 serialized layout unchanged. It was bumped +because variation selectors (U+FE00..U+FE0F and U+E0100..U+E01EF) are now +dropped while parsing instead of being laid out as replacement glyphs, so +cached line breaks and word positions from version 48 no longer match. + ### Version 48 Version 48 keeps the version 47 serialized layout unchanged. It was bumped @@ -200,7 +207,7 @@ import std.mem; import std.string; import std.core; -#define EXPECTED_VERSION 48 +#define EXPECTED_VERSION 49 #define MAX_STRING_LENGTH 65535 #define FOOTNOTE_NUMBER_LEN 32 #define FOOTNOTE_HREF_LEN 256 diff --git a/lib/EpdFont/SdCardFont.cpp b/lib/EpdFont/SdCardFont.cpp index 8760ba02d74..0681f90d029 100644 --- a/lib/EpdFont/SdCardFont.cpp +++ b/lib/EpdFont/SdCardFont.cpp @@ -11,6 +11,7 @@ #include #include +#include "../../src/util/InputDiag.h" #include "EpdFontFamily.h" // Resident SD-font buffers (glyph/kern arenas, interval + advance tables, the @@ -104,7 +105,27 @@ SdCardFont::~SdCardFont() { freeAll(); } // --- Per-style free/cleanup --- +namespace { +SdCardFont::MiniFreeEvent miniFreeEventLog[SdCardFont::MINI_FREE_EVENTS]; +uint32_t miniFreeEventTotal = 0; +} // namespace + +const SdCardFont::MiniFreeEvent* SdCardFont::miniFreeEvents(uint32_t& total) { + total = miniFreeEventTotal; + return miniFreeEventLog; +} + void SdCardFont::freeStyleMiniData(PerStyle& s) { + if (s.miniGlyphCount > 0) { + auto& ev = miniFreeEventLog[miniFreeEventTotal % MINI_FREE_EVENTS]; + ev.caller = reinterpret_cast(__builtin_return_address(0)); + ev.ms = millis(); + ev.freeHeap = ESP.getFreeHeap(); + ev.glyphs = static_cast(s.miniGlyphCount); + ev.style = static_cast(&s - styles_); + ev.metadataOnly = s.miniMetadataOnly; + miniFreeEventTotal++; + } psramDeleteArray(s.miniIntervals); s.miniIntervals = nullptr; psramDeleteArray(s.miniGlyphs); @@ -523,7 +544,7 @@ void SdCardFont::computeStyleFileOffsets(PerStyle& s, uint32_t baseOffset) { // --- Load --- -bool SdCardFont::load(const char* path) { +bool SdCardFont::load(const char* path, const bool isReaderFont) { freeAll(); if (strlen(path) >= sizeof(filePath_)) { LOG_ERR("SDCF", "Path too long (%zu bytes, max %zu)", strlen(path), sizeof(filePath_) - 1); @@ -776,13 +797,26 @@ bool SdCardFont::load(const char* path) { loaded_ = true; LOG_DBG("SDCF", "Loaded: %s (v%u, %u styles)", path, CPFONT_VERSION, styleCount_); + uint8_t firstAdvanceY = 0; + uint32_t firstGlyphCount = 0; + uint32_t residentBytes = 0; for (uint8_t i = 0; i < MAX_STYLES; i++) { if (!styles_[i].present) continue; const auto& h = styles_[i].header; + residentBytes += + h.intervalCount * static_cast(styles_[i].intervalsAreBmp16 ? sizeof(PerStyle::BmpInterval16) + : sizeof(EpdUnicodeInterval)); + if (firstAdvanceY == 0) { + firstAdvanceY = h.advanceY; + firstGlyphCount = h.glyphCount; + } LOG_DBG("SDCF", " style[%u]: %u intervals, %u glyphs, advY=%u, asc=%d, desc=%d, kernL=%u, kernR=%u, ligs=%u", i, h.intervalCount, h.glyphCount, h.advanceY, h.ascender, h.descender, h.kernLeftEntryCount, h.kernRightEntryCount, h.ligaturePairCount); } + if (isReaderFont) { + InputDiag::noteFontChoice(path, styleCount_, firstAdvanceY, firstGlyphCount, residentBytes, false, 0, 1, 1); + } return true; } @@ -859,7 +893,10 @@ int SdCardFont::prewarm(TextGetter getter, const void* ctx, uint32_t textCount, } } } - if (cpBudget == 0) return -1; + if (cpBudget == 0) { + prewarmEntryFails_++; + return -1; + } // Step 1: Extract unique codepoints from the UTF-8 texts (shared across all styles). // Dedup uses O(n^2) linear scan — worst case is MAX_PAGE_GLYPHS (512) unique codepoints @@ -870,6 +907,7 @@ int SdCardFont::prewarm(TextGetter getter, const void* ctx, uint32_t textCount, std::unique_ptr codepoints(new (std::nothrow) uint32_t[MAX_PAGE_GLYPHS]); if (!codepoints) { LOG_ERR("SDCF", "Failed to allocate codepoint buffer (%u bytes)", MAX_PAGE_GLYPHS * 4); + prewarmEntryFails_++; return -1; } uint32_t cpCount = 0; @@ -1026,6 +1064,16 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 } } + // Past the resident-subset check, so everything below is a rebuild. Counted and timed on the + // way out because no other figure records one: the reads go straight into the arena rather + // than the overflow ring, and the buffers are reused rather than dropped. + struct RebuildTimer { + SdCardFont& self; + unsigned long startMs; + ~RebuildTimer() { self.miniRebuildMs_ += static_cast(millis() - startMs); } + } rebuildTimer{*this, millis()}; + miniRebuilds_++; + // Trim oversized buffers only on a cache miss. Trimming when a scope closes // would discard a freshly prefetched page before its actual draw. Count each // rebuild once, retaining the arena until several pages use less than 3/4. @@ -1477,6 +1525,8 @@ uint16_t SdCardFont::getAdvance(uint32_t codepoint, uint8_t style) const { // Caller owns the codepoints buffer. int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask) { int totalMissed = 0; + const unsigned long fetchStartMs = millis(); + advanceFetchCalls_++; for (uint8_t si = 0; si < MAX_STYLES; si++) { if (!(styleMask & (1 << si)) || !styles_[si].present) continue; const auto& s = styles_[si]; @@ -1484,7 +1534,13 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun // Stop fetching once the cache is full — further inserts would be dropped // by the merge anyway. The renderer fast path tolerates missing entries // (returns 0); the slow path is still correct for those codepoints. - if (advanceTableSize_[si] >= ADVANCE_CACHE_LIMIT) continue; + if (advanceTableSize_[si] >= ADVANCE_CACHE_LIMIT) { + // Counted, not just skipped: past the cap every measurement of an uncached codepoint + // falls through to a per-glyph load, so this is the point where a build's font cost + // changes character. + advanceFullSkips_++; + continue; + } // For each codepoint in `codepoints`, skip those already cached, then // resolve to a glyph index. Build a parallel array sorted by glyph index @@ -1574,6 +1630,7 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun ADVANCE_CACHE_LIMIT); } + advanceFetchMs_ += static_cast(millis() - fetchStartMs); return totalMissed; } @@ -1758,6 +1815,8 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) { self->overflow_[slot].codepoint = codepoint; self->overflow_[slot].styleIdx = styleIdx; + self->overflowLoads_++; + InputDiag::noteGlyphMiss(codepoint, styleIdx); LOG_DBG("SDCF", "Overflow: loaded U+%04X style %u on demand (slot %u/%u)", codepoint, styleIdx, slot, OVERFLOW_CAPACITY); diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index 3411c993094..36de2eb0311 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -26,6 +26,18 @@ class SdCardFont { static constexpr int PREWARM_ARENA_TOO_LARGE = -2; static constexpr uint8_t MAX_STYLES = 4; + // Recent frees of a style's mini arena, for INPUT_DIAG (who dropped the page's glyphs, and when). + struct MiniFreeEvent { + uint32_t caller = 0; + uint32_t ms = 0; + uint32_t freeHeap = 0; + uint16_t glyphs = 0; + uint8_t style = 0; + bool metadataOnly = false; + }; + static constexpr uint8_t MINI_FREE_EVENTS = 4; + static const MiniFreeEvent* miniFreeEvents(uint32_t& total); + SdCardFont() = default; ~SdCardFont(); // Owns raw buffers freed in dtor — no shallow-copy semantics. Make any @@ -38,7 +50,8 @@ class SdCardFont { // Load .cpfont file: reads header + intervals into RAM, records file layout offsets. // Supports v4 (multi-style) format. // Returns true on success. - bool load(const char* path); + // isReaderFont: the family's body-text size (its first load), reported by INPUT_DIAG. + bool load(const char* path, bool isReaderFont = false); // Pre-read glyphs needed for the given UTF-8 text from SD card. // styleMask: bitmask of styles to prewarm (bit 0=regular, 1=bold, 2=italic, 3=bolditalic). @@ -141,6 +154,26 @@ class SdCardFont { void resetStats(); const Stats& getStats() const { return stats_; } + // Counters for INPUT_DIAG. Glyphs read one at a time through the overflow ring because no + // prewarm covered them; mini-arena rebuilds (the reads go straight into the arena, so no other + // figure records them) and their time; prewarms that bailed before any style was touched; and + // the advance-table work a section build spends its font time on. + uint32_t overflowLoads() const { return overflowLoads_; } + uint32_t miniRebuilds() const { return miniRebuilds_; } + uint32_t miniRebuildMs() const { return miniRebuildMs_; } + uint32_t prewarmEntryFails() const { return prewarmEntryFails_; } + uint32_t advanceFetchCalls() const { return advanceFetchCalls_; } + uint32_t advanceFetchMs() const { return advanceFetchMs_; } + uint32_t advanceFullSkips() const { return advanceFullSkips_; } + uint32_t advanceTableMax() const { + uint32_t largest = 0; + for (uint8_t i = 0; i < MAX_STYLES; i++) { + if (advanceTableSize_[i] > largest) largest = advanceTableSize_[i]; + } + return largest; + } + static constexpr uint32_t advanceTableLimit() { return ADVANCE_CACHE_LIMIT; } + // Content hash of the file header + style TOC entries (computed during load). // Used to generate deterministic font IDs for section cache invalidation. uint32_t contentHash() const { return contentHash_; } @@ -313,6 +346,13 @@ class SdCardFont { void mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sortedNew, uint32_t newCount); Stats stats_; + uint32_t overflowLoads_ = 0; + uint32_t miniRebuilds_ = 0; + uint32_t miniRebuildMs_ = 0; + uint32_t prewarmEntryFails_ = 0; + uint32_t advanceFetchCalls_ = 0; + uint32_t advanceFetchMs_ = 0; + uint32_t advanceFullSkips_ = 0; uint32_t contentHash_ = 0; bool loaded_ = false; diff --git a/lib/EpdFont/SdCardFontManager.cpp b/lib/EpdFont/SdCardFontManager.cpp index 45aa8244c35..ad0100cc4bf 100644 --- a/lib/EpdFont/SdCardFontManager.cpp +++ b/lib/EpdFont/SdCardFontManager.cpp @@ -35,7 +35,8 @@ int SdCardFontManager::loadFile(const SdCardFontFileInfo& file, const char* fami return 0; } - if (!font->load(file.path.c_str())) { + // The family's first load is the body-text size; the UI fallback sizes follow it. + if (!font->load(file.path.c_str(), loaded_.empty())) { LOG_ERR("SDMGR", "Failed to load %s", file.path.c_str()); delete font; return 0; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index be0240de452..d31c65e6469 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -55,7 +55,8 @@ namespace { // v47: Word and character spacing in the header (cache validation); cached BlockStyle stores only character spacing. // v48: Hangul words wrap at spaces; with hyphenation on they may also split at a line end. // Justification no longer stretches between syllables. -constexpr uint8_t SECTION_FILE_VERSION = 48; +// v49: Variation selectors (VS1-VS16, IVS VS17-VS256) are dropped instead of drawn as tofu. +constexpr uint8_t SECTION_FILE_VERSION = 49; // Written into the version field while a build is in progress; patched to // SECTION_FILE_VERSION only when the build is finalized. An abandoned / // crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index e379e57e977..61ede0707ff 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -1728,6 +1728,28 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char } } + // Skip variation selectors. These are zero-width hints about which glyph variant to + // draw for the preceding character; they have no glyph of their own and no font + // carries one. Left unskipped they fall through to the replacement-glyph path and + // draw a visible tofu box per occurrence. + // - VS1..VS16 (U+FE00..U+FE0F = 0xEF 0xB8 0x80..0x8F). VS15/VS16 are the text/emoji + // presentation pair; one real book in the Kakuyomu/Narou corpus scan carried 275 of + // them (word-processor autocorrect commonly appends VS15 after ☆ and similar marks). + // - Ideographic variation selectors VS17..VS256 (U+E0100..U+E01EF = 0xF3 0xA0 0x84..0x87 + // 0x80..0xBF), which the Adobe-Japan1 / Hanyo-Denshi collections use to pick a kanji + // variant. The bitmap fonts have one glyph per codepoint, so the base character is + // drawn and the selector dropped. + if (s[i] == static_cast(0xEF) && i + 2 < len && s[i + 1] == static_cast(0xB8) && + (static_cast(s[i + 2]) & 0xF0) == 0x80) { + i += 2; + continue; + } + if (s[i] == static_cast(0xF3) && i + 3 < len && s[i + 1] == static_cast(0xA0) && + static_cast(s[i + 2]) >= 0x84 && static_cast(s[i + 2]) <= 0x87) { + i += 3; + continue; + } + // If we're about to run out of space, then cut the word off and start a new one. // For CJK text (no spaces), this is the primary word-breaking mechanism. // We must avoid splitting multi-byte UTF-8 sequences across word boundaries, diff --git a/lib/GfxRenderer/FontCacheManager.cpp b/lib/GfxRenderer/FontCacheManager.cpp index c721ee83d37..b409347a703 100644 --- a/lib/GfxRenderer/FontCacheManager.cpp +++ b/lib/GfxRenderer/FontCacheManager.cpp @@ -149,7 +149,14 @@ void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::S } } if (fontSlot == scanFontCount_) { - if (scanFontCount_ >= MAX_SCAN_FONTS) return; + if (scanFontCount_ >= MAX_SCAN_FONTS) { + // Dropping the font here means every glyph it draws faults in one at a time later. + if (scanFontOverflow_ == 0) { + LOG_DBG("FCM", "Scan font slots (%u) exhausted; font %d will load on demand", MAX_SCAN_FONTS, fontId); + } + scanFontOverflow_++; + return; + } scanFontIds_[scanFontCount_++] = fontId; } @@ -198,9 +205,15 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage } void FontCacheManager::PrewarmScope::endScanAndPrewarm() { + const bool wasScanning = manager_->scanMode_ == ScanMode::Scanning; manager_->scanMode_ = ScanMode::None; if (manager_->scanCodepointCount_ == 0) return; + if (wasScanning) { + manager_->lastScanBytes_ = 0; + manager_->lastScanFonts_ = 0; + } + std::sort(manager_->scanCodepoints_, manager_->scanCodepoints_ + manager_->scanCodepointCount_); uint16_t groupStarts[SCAN_GROUP_COUNT] = {}; @@ -226,6 +239,10 @@ void FontCacheManager::PrewarmScope::endScanAndPrewarm() { const uint8_t fontSlot = static_cast(group) / 4; const uint8_t style = static_cast(group) & 0x03; + if (wasScanning) { + manager_->lastScanBytes_ += static_cast(output - utf8Text); + manager_->lastScanFonts_++; + } // This group is the complete glyph set for one font/style in this render. manager_->prewarmCache(manager_->scanFontIds_[fontSlot], utf8Text, 1 << style, /*accumulate=*/false); } diff --git a/lib/GfxRenderer/FontCacheManager.h b/lib/GfxRenderer/FontCacheManager.h index c538f3daef2..f2662d59dd3 100644 --- a/lib/GfxRenderer/FontCacheManager.h +++ b/lib/GfxRenderer/FontCacheManager.h @@ -36,6 +36,16 @@ class FontCacheManager { // The FontDecompressor pointer, needed by GfxRenderer::getGlyphBitmap() FontDecompressor* getDecompressor() const { return fontDecompressor_; } + // What the last completed scan pass handed to prewarmCache: total UTF-8 bytes and how many + // font-and-style groups were batched. Zero bytes after a page scope means the scan hook never + // fired -- the draw path bypassed drawText's recording, which no other figure distinguishes + // from a prewarm that ran and failed. Read by the INPUT_DIAG page hook. + uint32_t lastScanBytes() const { return lastScanBytes_; } + uint8_t lastScanFonts() const { return lastScanFonts_; } + // Times a draw named a font the scan had no slot left for. Non-zero means the page's prewarm + // was incomplete by construction. + uint32_t scanFontOverflows() const { return scanFontOverflow_; } + // RAII scope for two-pass prewarm pattern class PrewarmScope { public: @@ -79,4 +89,7 @@ class FontCacheManager { uint16_t scanCodepointCount_ = 0; uint8_t scanFontCount_ = 0; bool scanOverflowWarned_ = false; + uint32_t scanFontOverflow_ = 0; + uint32_t lastScanBytes_ = 0; + uint8_t lastScanFonts_ = 0; }; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index cadb99ce88e..81a18e95154 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -44,6 +44,59 @@ uint16_t getSdCardSpaceAdvance(SdCardFont& font, const EpdFontFamily::Style styl } } // namespace +uint32_t GfxRenderer::glyphOnDemandLoads() const { + uint32_t loads = 0; + for (const auto& [fontId, font] : sdCardFonts_) { + (void)fontId; + if (font != nullptr) loads += font->overflowLoads(); + } + return loads; +} + +uint32_t GfxRenderer::glyphMiniRebuilds() const { + uint32_t rebuilds = 0; + for (const auto& [fontId, font] : sdCardFonts_) { + (void)fontId; + if (font != nullptr) rebuilds += font->miniRebuilds(); + } + return rebuilds; +} + +uint32_t GfxRenderer::glyphMiniRebuildMs() const { + uint32_t ms = 0; + for (const auto& [fontId, font] : sdCardFonts_) { + (void)fontId; + if (font != nullptr) ms += font->miniRebuildMs(); + } + return ms; +} + +uint32_t GfxRenderer::glyphPrewarmEntryFails() const { + uint32_t fails = 0; + for (const auto& [fontId, font] : sdCardFonts_) { + (void)fontId; + if (font != nullptr) fails += font->prewarmEntryFails(); + } + return fails; +} + +void GfxRenderer::sdAdvanceStats(uint32_t& calls, uint32_t& ms, uint32_t& tableMax, uint32_t& tableLimit, + uint32_t& fullSkips) const { + calls = 0; + ms = 0; + tableMax = 0; + fullSkips = 0; + tableLimit = SdCardFont::advanceTableLimit(); + for (const auto& [fontId, font] : sdCardFonts_) { + (void)fontId; + if (font == nullptr) continue; + calls += font->advanceFetchCalls(); + ms += font->advanceFetchMs(); + fullSkips += font->advanceFullSkips(); + if (font->advanceTableMax() > tableMax) tableMax = font->advanceTableMax(); + } +} + namespace { const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 6a948279137..ab98b01e948 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -184,6 +184,16 @@ class GfxRenderer { } const std::map& getSdCardFonts() const { return sdCardFonts_; } bool isSdCardFont(int fontId) const { return sdCardFonts_.count(fontId) > 0; } + + // INPUT_DIAG totals across the registered SD-card fonts (see the SdCardFont counters). + // On-demand loads and arena rebuilds are the pair that tells "the prewarm is not landing" from + // "a caller warms one string at a time"; a warm that covers the text leaves both at zero. + uint32_t glyphOnDemandLoads() const; + uint32_t glyphMiniRebuilds() const; + uint32_t glyphMiniRebuildMs() const; + uint32_t glyphPrewarmEntryFails() const; + void sdAdvanceStats(uint32_t& calls, uint32_t& ms, uint32_t& tableMax, uint32_t& tableLimit, + uint32_t& fullSkips) const; // TTF (vector) fonts rendered via TtfEpdFont/FreeInkFont. Registered like an // ordinary EpdFontFamily (insertFont), plus tracked here so ensureSdCardFontReady() // rebuilds their per-page glyph set on demand — the eager analogue of the SD diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 8737db7124a..642b07aefc7 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -121,6 +121,9 @@ void ActivityManager::renderTaskLoop() { char renderedName[16]; snprintf(renderedName, sizeof(renderedName), "%s", currentActivity->name.c_str()); const unsigned long renderStart = millis(); + const uint32_t onDemandStart = renderer.glyphOnDemandLoads(); + const uint32_t rebuildStart = renderer.glyphMiniRebuilds(); + const uint32_t rebuildMsStart = renderer.glyphMiniRebuildMs(); InputDiag::noteRenderStart(); #endif // Night mode is a global output polarity applied to every activity. @@ -129,8 +132,9 @@ void ActivityManager::renderTaskLoop() { currentActivity->render(std::move(lock)); #ifdef INPUT_DIAG const unsigned long renderDurationMs = millis() - renderStart; - // The glyph counters (on-demand loads, arena rebuilds) come with the font stage; 0 until then. - InputDiag::noteRender(renderedName, renderDurationMs, 0, 0, 0); + InputDiag::noteRender(renderedName, renderDurationMs, renderer.glyphOnDemandLoads() - onDemandStart, + renderer.glyphMiniRebuilds() - rebuildStart, + renderer.glyphMiniRebuildMs() - rebuildMsStart); // A render this slow isn't drawing -- it's stuck somewhere upstream (SD I/O, glyph // cache, allocation). The 16-line log ring is system-wide and short, so whatever ran // during the stall is likely still in it right now; a routine render would evict it diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6c7113b6cb5..bb0ba1aeb85 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1406,6 +1406,11 @@ void EpubReaderActivity::renderBook() { if (buildChunkCountThisRender > 0) { InputDiag::noteBuildTotal(currentSpineIndex, buildAccumMsThisRender, buildChunkCountThisRender); + // Cumulative across the session, not per render: what the report needs is where a build's time + // went, not a per-chunk series. + uint32_t advCalls = 0, advMs = 0, advTableMax = 0, advTableLimit = 0, advFullSkips = 0; + renderer.sdAdvanceStats(advCalls, advMs, advTableMax, advTableLimit, advFullSkips); + InputDiag::noteBuildFontWork(advCalls, advMs, advTableMax, advTableLimit, advFullSkips); } // Section loaded (or built far enough for the requested page). InputDiag::noteOpenStage(3, "sect"); @@ -1607,6 +1612,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or // its own SD pass after the scope ends. renderStatusBar(); scope.endScanAndPrewarm(); + // No-op unless built with INPUT_DIAG: what the scan collected and whether any prewarm bailed at + // entry, so a fast prewarm that left the draw cold can be attributed without the log ring. + InputDiag::noteScanOutcome(fcm->lastScanBytes(), fcm->lastScanFonts(), renderer.glyphPrewarmEntryFails(), + fcm->scanFontOverflows()); const auto tPrewarm = millis(); InputDiag::notePoint("pg:prewarm"); diff --git a/src/util/InputDiag.cpp b/src/util/InputDiag.cpp index f48ba58da84..b9def363f45 100644 --- a/src/util/InputDiag.cpp +++ b/src/util/InputDiag.cpp @@ -1277,10 +1277,22 @@ void InputDiag::flush(const bool inputActive) { Storage.rename(DIAG_PATH, "/input-diag.prev.txt"); } - // SdCardFont's mini-subset free log (caller@ms free=KB g=glyphs s=style m=metadataOnly) comes - // back with the font stage of the 1.6.5 re-fork; until then the line reports nothing. + // Newest first: caller@ms free=KB g=glyphs s=style m=metadataOnly. char miniFreeBuf[4 * 48] = ""; uint32_t miniFreeTotal = 0; + { + const auto* ev = SdCardFont::miniFreeEvents(miniFreeTotal); + size_t off = 0; + const uint32_t shown = miniFreeTotal < SdCardFont::MINI_FREE_EVENTS ? miniFreeTotal : SdCardFont::MINI_FREE_EVENTS; + for (uint32_t i = 0; i < shown && off < sizeof(miniFreeBuf); i++) { + const auto& e = ev[(miniFreeTotal - 1 - i) % SdCardFont::MINI_FREE_EVENTS]; + const int n = + snprintf(miniFreeBuf + off, sizeof(miniFreeBuf) - off, "%s0x%08x@%u free=%uK g=%u s=%u m=%d", i ? " | " : "", + e.caller, e.ms, e.freeHeap / 1024, e.glyphs, e.style, e.metadataOnly ? 1 : 0); + if (n <= 0) break; + off += static_cast(n); + } + } char glyphMissBuf[GLYPH_MISS_EVENTS * 12] = ""; formatGlyphMisses(glyphMissBuf, sizeof(glyphMissBuf)); From 618428500060ead07432a0aa39af5fd9664b1eea Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 14:59:47 +0900 Subject: [PATCH 05/59] =?UTF-8?q?README:=20=E4=BD=9C=E3=82=8A=E7=9B=B4?= =?UTF-8?q?=E3=81=97=E3=81=AE=E6=AE=B5=E9=9A=8E3=EF=BC=88=E5=AD=97?= =?UTF-8?q?=E5=BD=A2=E3=81=AE=E8=A8=88=E6=B8=AC=E3=81=A8=E7=B4=A0=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E6=B8=AC=E2=91=A0=EF=BC=89=E3=81=AE=E7=B5=90=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31e3ba6a213..4ff82b06e2c 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ # 最近の更新内容について(2026/09/28) - 本家 1.6.5(2026/09/27 リリース)を起点に作り直しを開始(2026/09/28〜、ブランチ `vertical-1.6.5`) - - 段階0(素の 1.6.5 のビルド)、段階1(診断)、段階2(時計と監視)まで完了。詳細は下の「1.6.5ベースへの作り直し」節 + - 段階0(素の 1.6.5 のビルド)、段階1(診断)、段階2(時計と監視)、段階3(字形の計測と素の実測)まで完了。詳細は下の「1.6.5ベースへの作り直し」節 - この時点のブランチには日本語フォントの扱い・縦書き・挿絵などはまだ入っていない(以下の各項目は 1.6.0 ベースの `feat/vertical-1.6.0based` の内容) - 章の境目の待ちを減らすため、読書中の待機時間に次の章を先に組み立てる(2026/09/09〜) - 商業書籍で章をまたぐたびに7秒前後待つのを、ページを表示して1.5秒以上操作が無い間に少しずつ済ませる @@ -226,7 +226,7 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ | 0 | 素の 1.6.5 のビルド | 完了(RAM 57,912B/flash 5,621,527B) | | 1 | 診断(INPUT_DIAG、ビルド番号) | 完了(`2026092803-diag`、実機確認済み) | | 2 | 時計と監視(#3562 の地域・夏時間の設定に載せ替え) | 完了(`2026092806-diag`、実機確認済み) | -| 3 | 日本語フォント(生成フォントと描画の追加分。メモリ処理は入れない)+素の実測① | | +| 3 | 日本語フォント(生成フォントと描画の追加分。メモリ処理は入れない)+素の実測① | 完了(`2026092807-diag`、実測済み) | | 4 | 横書きの組版(禁則、長い段落) | | | 5 | 縦書きの中核 | | | 6 | 縦書きの字間(本家 #3528 の字間を縦書きの字送りにも効かせる、新規) | | @@ -254,6 +254,16 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ - 時計の描画と時計表示中のメイン処理を 30 秒の監視に登録(すべてのビルド)。CPU の速度切り替えの直列化、電池電圧の記録、「自動スリープさせない」と「速度を保つ」の区別(時計表示中は 10MHz)も旧ツリーのまま - 実機確認: わざと固まらせる検証ボタン(CLOCK_HANG_TRIGGERS)で、メイン処理(Confirm)・描画(側面ボタン)とも 30 秒以内に再起動し、`crash_report.txt` にそれぞれ `loopTask`/`ActivityManager` の名前が残った。時計表示中は `cpu_mhz_min=10`、ボタンの読み取り間隔 76ms 以内、空き 78〜80KB で一定 +### 段階3: 字形の計測と素の実測①(2026-09-28、`2026092807-diag`) +- 旧ツリーのフォント層の変更を仕分けた。SD に置く日本語書体はそのまま 1.6.5 で使え、生成フォントの定義はフォント用ブランチにあるので firmware で移すものは無い。メモリ処理(4KB 分割の置き場、予備枠 8→48、解放の経路など)は段階9、縦書き用は段階5、flash から読む仕組みは段階8a、挿絵・階調は段階7 +- 入れたもの: 字形を1つずつ読んだ回数・置き場の作り直し(回数と時間)・置き場の解放記録・描画前の走査の結果・幅の表の作業量・本文の書体を診断に出す計測(書体のメモリの扱いは変えない)。異体字選択子(VS1〜VS16、IVS)の読み飛ばし(章の版 48→49) +- 素の実測①(商業書籍の横書き、NotoSansJP 18pt、アンチエイリアス ON) + - **離れた章への移動が 39.2 秒**(組み立て 34.6 秒)。本家は字の幅を 768 字分まで表に覚え、日本語の章はそれを超えるため、以降の幅の測定のたびに字形の画像まで SD から1字ずつ読む(1回の描画で 2,817 字)。旧ツリーの `15bdbaab` が塞いでいた穴なので、段階9から前倒しして持ち込む + - 同じ本を開き直した直後に空き 2.7KB(最大連続 1.2KB)。持ち主は本の CSS 36KB、設定項目の一覧 9KB、画面用の記憶 53KB など + - 読んだ後の Home で空き 62.5KB に対し最大連続 17.4KB(起動直後 102KB/98KB)。旧ツリーの `7239d0bb`/`f3293ca7` が直した形と同じ + - ボタンの取りこぼしは無し(押下 230 回すべて受け付け) + - 2点目・3点目は縦書きと挿絵を載せた後の実測②(段階9)で判断する + ## 1.6.0ベースへの作り直し(2026/09/06〜) by claude code 本家1.6.0が2026/09/05に正式リリースされたため、方針どおり**1.6.0(`54337e6d`)から新ブランチ From b6cd58174e9cdab989be52a465f49c15f988f205 Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 22:03:00 +0900 Subject: [PATCH 06/59] =?UTF-8?q?perf:=20=E7=AB=A0=E3=81=AE=E7=B5=84?= =?UTF-8?q?=E3=81=BF=E7=AB=8B=E3=81=A6=E4=B8=AD=E3=81=AF=E5=AD=97=E3=81=AE?= =?UTF-8?q?=E5=B9=85=E3=81=A0=E3=81=91=E3=82=92=E8=AA=AD=E3=81=BF=E3=80=81?= =?UTF-8?q?=E9=96=8B=E3=81=84=E3=81=9F=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=A8=E8=A6=9A=E3=81=88=E3=81=9F=E5=B9=85=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E3=81=84=E5=9B=9E=E3=81=99=EF=BC=88=E6=AE=B5=E9=9A=8E9?= =?UTF-8?q?=E3=81=8B=E3=82=89=E5=89=8D=E5=80=92=E3=81=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 幅の表が満杯の後は readAdvanceOnly で字形の画像を読まずに幅だけ返す(MeasureOnlyScope の間だけ) - 組み立ての1回分の間は書体ファイルを開いたままにし、SD から読んだ幅を 512 字分覚える - 診断: メモリの持ち主の記録を INPUT_DIAG_ALLOC_TAGS 指定時だけに、build_adv_only= を追加 同じ章の組み立て 20.7 秒 → 12.6 秒(2026092815-diag、実機)。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- lib/EpdFont/SdCardFont.cpp | 74 ++++++++++++++++++++ lib/EpdFont/SdCardFont.h | 26 +++++++ lib/Epub/Epub/Section.cpp | 6 ++ lib/GfxRenderer/GfxRenderer.cpp | 27 +++++++ lib/GfxRenderer/GfxRenderer.h | 24 ++++++- src/activities/reader/EpubReaderActivity.cpp | 3 + src/util/InputDiag.cpp | 32 +++++++-- src/util/InputDiag.h | 4 ++ 8 files changed, 191 insertions(+), 5 deletions(-) diff --git a/lib/EpdFont/SdCardFont.cpp b/lib/EpdFont/SdCardFont.cpp index 0681f90d029..c2f6f11f82b 100644 --- a/lib/EpdFont/SdCardFont.cpp +++ b/lib/EpdFont/SdCardFont.cpp @@ -1499,6 +1499,80 @@ bool SdCardFont::hasAdvanceTable() const { return false; } +// A section build measures text and never draws it, yet once the advance table is full +// (ADVANCE_CACHE_LIMIT) every uncached codepoint went through the glyph-miss path and pulled its +// bitmap into the overflow ring just to read advanceX. Japanese chapters routinely use more +// distinct characters than the table holds: one 18 pt chapter build read 2,817 bitmaps in a +// single render (full_skips 416) and took 34.6 s. The glyph record is twelve bytes and sits +// before the bitmaps in the file, so read that and nothing else. +uint16_t SdCardFont::readAdvanceOnly(const uint32_t codepoint, uint8_t styleIdx) const { + styleIdx &= (MAX_STYLES - 1); + if (!loaded_ || !styles_[styleIdx].present) return 0; + const auto& s = styles_[styleIdx]; + advanceOnlyCalls_++; + + // Resident arena first: the same interval lookup EpdFont::getGlyph does before it asks for a + // load, so a measurement of a prewarmed glyph costs no SD access. + if (s.miniData.intervals && s.miniData.intervalCount > 0 && s.miniData.glyph) { + const auto* begin = s.miniData.intervals; + const auto* end = begin + s.miniData.intervalCount; + const auto it = std::upper_bound( + begin, end, codepoint, [](const uint32_t value, const EpdUnicodeInterval& iv) { return value < iv.first; }); + if (it != begin) { + const auto& iv = *(it - 1); + if (codepoint <= iv.last) return s.miniData.glyph[iv.offset + (codepoint - iv.first)].advanceX; + } + } + for (uint32_t i = 0; i < overflowCount_; i++) { + if (overflow_[i].codepoint == codepoint && overflow_[i].styleIdx == styleIdx) return overflow_[i].glyph.advanceX; + } + + const uint32_t memoKey = codepoint | (static_cast(styleIdx) << 22) | 0x80000000u; + const uint32_t memoSlot = (codepoint ^ (codepoint >> 9)) & (MEASURE_MEMO_SLOTS - 1); + if (measureMemoKeys_ && measureMemoKeys_[memoSlot] == memoKey) return measureMemoAdvances_[memoSlot]; + + const int32_t globalIdx = findGlobalGlyphIndex(s, codepoint); + if (globalIdx < 0) return 0; + struct SdTimer { + const SdCardFont& self; + unsigned long startMs; + ~SdTimer() { self.advanceOnlyMs_ += static_cast(millis() - startMs); } + } sdTimer{*this, millis()}; + advanceOnlySdReads_++; + if (!measureFile_) { + measureFile_ = makeUniqueNoThrow(); + if (!measureFile_) return 0; + } + if (!*measureFile_ && !Storage.openFileForRead("SDCF", filePath_, *measureFile_)) return 0; + EpdGlyph glyph = {}; + const uint32_t off = s.glyphsFileOffset + static_cast(globalIdx) * sizeof(EpdGlyph); + if (!measureFile_->seekSet(off) || + measureFile_->read(reinterpret_cast(&glyph), sizeof(EpdGlyph)) != sizeof(EpdGlyph)) { + return 0; + } + if (!measureMemoKeys_) { + measureMemoKeys_ = makeUniqueNoThrow(MEASURE_MEMO_SLOTS); + measureMemoAdvances_ = makeUniqueNoThrow(MEASURE_MEMO_SLOTS); + if (!measureMemoKeys_ || !measureMemoAdvances_) { + measureMemoKeys_.reset(); + measureMemoAdvances_.reset(); + } else { + memset(measureMemoKeys_.get(), 0, MEASURE_MEMO_SLOTS * sizeof(uint32_t)); + } + } + if (measureMemoKeys_) { + measureMemoKeys_[memoSlot] = memoKey; + measureMemoAdvances_[memoSlot] = glyph.advanceX; + } + return glyph.advanceX; +} + +void SdCardFont::closeMeasureFile() const { + measureFile_.reset(); + measureMemoKeys_.reset(); + measureMemoAdvances_.reset(); +} + uint16_t SdCardFont::getAdvance(uint32_t codepoint, uint8_t style) const { style &= (MAX_STYLES - 1); if (!advanceTable_[style]) return 0; diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index 36de2eb0311..8ba30b4df7c 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -1,12 +1,15 @@ #pragma once #include +#include #include #include #include "EpdFont.h" #include "EpdFontData.h" +class HalFile; + // On-disk binary format version for .cpfont files. Defined as a preprocessor // macro (rather than a constexpr) so it can be stringified into the SD-fonts // release URL — see FONT_MANIFEST_URL in FontDownloadActivity.h. No integer @@ -94,6 +97,12 @@ class SdCardFont { // Look up advanceX for a codepoint from the advance table. // Returns the 12.4 fixed-point advance, or 0 if not found. uint16_t getAdvance(uint32_t codepoint, uint8_t style) const; + // Advance of one codepoint the table does not hold, without loading its bitmap: the resident + // arena, then the overflow ring, then the 12-byte glyph record straight from the card. 0 when + // the font has no such glyph (or the read failed), so callers can fall back to a full load. + uint16_t readAdvanceOnly(uint32_t codepoint, uint8_t styleIdx) const; + // Close the file readAdvanceOnly keeps open for the duration of a measuring pass. + void closeMeasureFile() const; // Returns true if advance table is populated for at least one style. bool hasAdvanceTable() const; @@ -165,6 +174,10 @@ class SdCardFont { uint32_t advanceFetchCalls() const { return advanceFetchCalls_; } uint32_t advanceFetchMs() const { return advanceFetchMs_; } uint32_t advanceFullSkips() const { return advanceFullSkips_; } + // readAdvanceOnly: calls, the ones that had to read the card, and the time those reads took. + uint32_t advanceOnlyCalls() const { return advanceOnlyCalls_; } + uint32_t advanceOnlySdReads() const { return advanceOnlySdReads_; } + uint32_t advanceOnlyMs() const { return advanceOnlyMs_; } uint32_t advanceTableMax() const { uint32_t largest = 0; for (uint8_t i = 0; i < MAX_STYLES; i++) { @@ -353,6 +366,19 @@ class SdCardFont { uint32_t advanceFetchCalls_ = 0; uint32_t advanceFetchMs_ = 0; uint32_t advanceFullSkips_ = 0; + mutable uint32_t advanceOnlyCalls_ = 0; + mutable uint32_t advanceOnlySdReads_ = 0; + mutable uint32_t advanceOnlyMs_ = 0; + // Kept open across readAdvanceOnly calls while a build measures (closed by closeMeasureFile): + // opening the file by its long name was nearly all of each 3.9 ms read. + mutable std::unique_ptr measureFile_; + // Widths readAdvanceOnly already read from the card during this measuring pass, direct-mapped + // by codepoint (key = codepoint | style << 22 | valid bit). A chapter repeats its characters, so + // most calls past the full advance table are for a character read moments before. 3 KB, freed + // with the file. + static constexpr uint32_t MEASURE_MEMO_SLOTS = 512; + mutable std::unique_ptr measureMemoKeys_; + mutable std::unique_ptr measureMemoAdvances_; uint32_t contentHash_ = 0; bool loaded_ = false; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index d31c65e6469..a6387d494ca 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -279,6 +279,11 @@ bool Section::createSectionFile(const ReaderRenderSpec& spec, const std::functio } bool Section::startBuild(const ReaderRenderSpec& spec, const std::function& popupFn) { + // A build measures and never draws, so every uncached codepoint's width comes from the glyph + // record on the card rather than a bitmap load into the overflow ring (see readAdvanceOnly). + // Scoped over the whole build, not just ParsedText's layout: the parser measures too (list + // markers, table cells, inline-image decisions). + const GfxRenderer::MeasureOnlyScope measureOnly(renderer); if (build_) { LOG_ERR("SCT", "startBuild called while a build is already active"); return false; @@ -472,6 +477,7 @@ bool Section::startBuild(const ReaderRenderSpec& spec, const std::functionparser) { LOG_ERR("SCT", "buildSomeMore with no active build"); return false; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 81a18e95154..4c09c33aba7 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -97,6 +97,26 @@ void GfxRenderer::sdAdvanceStats(uint32_t& calls, uint32_t& ms, uint32_t& tableM } } +void GfxRenderer::closeSdMeasureFiles() const { + for (const auto& [fontId, font] : sdCardFonts_) { + (void)fontId; + if (font != nullptr) font->closeMeasureFile(); + } +} + +void GfxRenderer::sdAdvanceOnlyStats(uint32_t& calls, uint32_t& sdReads, uint32_t& ms) const { + calls = 0; + sdReads = 0; + ms = 0; + for (const auto& [fontId, font] : sdCardFonts_) { + (void)fontId; + if (font == nullptr) continue; + calls += font->advanceOnlyCalls(); + sdReads += font->advanceOnlySdReads(); + ms += font->advanceOnlyMs(); + } +} + namespace { const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir); @@ -2186,6 +2206,13 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami } int32_t advFP = sdIt->second->getAdvance(cp, styleIdx); if (!utf8IsCombiningMark(cp)) { + if (advFP == 0 && measureOnly_) { + // Layout only: not in the advance table (full, or not yet fetched), and nothing is + // about to draw this glyph, so read the advance alone rather than loading a bitmap + // into the overflow ring to answer a question about width. A render draws the glyph + // next, so there the load is the single SD trip and this path is skipped. + advFP = sdIt->second->readAdvanceOnly(cp, styleIdx); + } if (advFP == 0) { const EpdGlyph* glyph = font.getGlyph(cp, style); advFP = glyph ? glyph->advanceX : 0; diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index ab98b01e948..3e42d20e3fe 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -91,6 +91,11 @@ class GfxRenderer { mutable int _stripY0 = 0; mutable int _stripRows = 0; mutable bool _stripActive = false; + // True while a section build is measuring text it will not draw. getTextAdvanceX then answers + // an uncached codepoint from the glyph record on the card instead of loading its bitmap; at + // render time the bitmap is wanted next anyway, so the load stays the cheaper path there. + // Set through MeasureOnlyScope. + mutable bool measureOnly_ = false; mutable int clipLeft_ = 0; mutable int clipTop_ = 0; mutable int clipRight_ = 32767; @@ -194,7 +199,9 @@ class GfxRenderer { uint32_t glyphPrewarmEntryFails() const; void sdAdvanceStats(uint32_t& calls, uint32_t& ms, uint32_t& tableMax, uint32_t& tableLimit, uint32_t& fullSkips) const; - // TTF (vector) fonts rendered via TtfEpdFont/FreeInkFont. Registered like an + void sdAdvanceOnlyStats(uint32_t& calls, uint32_t& sdReads, uint32_t& ms) const; + // Close the font files readAdvanceOnly held open for the measuring pass that just ended. + void closeSdMeasureFiles() const; // TTF (vector) fonts rendered via TtfEpdFont/FreeInkFont. Registered like an // ordinary EpdFontFamily (insertFont), plus tracked here so ensureSdCardFontReady() // rebuilds their per-page glyph set on demand — the eager analogue of the SD // font prewarm. The TtfEpdFont is owned by the caller (SdCardFontSystem). @@ -425,6 +432,21 @@ class GfxRenderer { bool restoreFrameBufferAfterBuild(); bool hasFrameBuffer() const { return frameBuffer != nullptr; } + class MeasureOnlyScope { + public: + explicit MeasureOnlyScope(const GfxRenderer& r) : r_(r), prev_(r.measureOnly_) { r_.measureOnly_ = true; } + ~MeasureOnlyScope() { + r_.measureOnly_ = prev_; + if (!prev_) r_.closeSdMeasureFiles(); + } + MeasureOnlyScope(const MeasureOnlyScope&) = delete; + MeasureOnlyScope& operator=(const MeasureOnlyScope&) = delete; + + private: + const GfxRenderer& r_; + bool prev_; + }; + // RAII form of the loan above, for blocking build regions with early-return // error paths: restores on scope exit (or explicitly via end()). Display the // popup/screen the panel should hold BEFORE constructing one. Constructing diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index bb0ba1aeb85..7b9679c43b6 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1411,6 +1411,9 @@ void EpubReaderActivity::renderBook() { uint32_t advCalls = 0, advMs = 0, advTableMax = 0, advTableLimit = 0, advFullSkips = 0; renderer.sdAdvanceStats(advCalls, advMs, advTableMax, advTableLimit, advFullSkips); InputDiag::noteBuildFontWork(advCalls, advMs, advTableMax, advTableLimit, advFullSkips); + uint32_t onlyCalls = 0, onlySd = 0, onlyMs = 0; + renderer.sdAdvanceOnlyStats(onlyCalls, onlySd, onlyMs); + InputDiag::noteBuildAdvanceOnly(onlyCalls, onlySd, onlyMs); } // Section loaded (or built far enough for the requested page). InputDiag::noteOpenStage(3, "sect"); diff --git a/src/util/InputDiag.cpp b/src/util/InputDiag.cpp index b9def363f45..fc3b1069636 100644 --- a/src/util/InputDiag.cpp +++ b/src/util/InputDiag.cpp @@ -350,6 +350,9 @@ uint32_t buildFontMs = 0; uint32_t buildFontTableMax = 0; uint32_t buildFontTableLimit = 0; uint32_t buildFontFullSkips = 0; +uint32_t buildAdvOnlyCalls = 0; +uint32_t buildAdvOnlySdReads = 0; +uint32_t buildAdvOnlyMs = 0; // Page-glyph prewarm budgets. The minimum is what the tightest page got; clips count the pages // that wanted more glyphs than the heap would pay for. @@ -519,12 +522,19 @@ uint8_t imgEventCount = 0; // total recorded; ring position = count % IMG_EVENT // changes for free(): the tag rides inside the block. namespace { constexpr size_t ALLOC_TAG_BYTES = 12; +// Without the malloc wrappers the last 12 bytes of a block are the caller's data, not a tag. +#ifdef INPUT_DIAG_ALLOC_TAGS +constexpr bool ALLOC_TAGS = true; +#else +constexpr bool ALLOC_TAGS = false; +#endif inline bool looksLikeCode(const uint32_t v) { return (v >= 0x42000000u && v < 0x42800000u) || (v >= 0x40380000u && v < 0x403E0000u); } // Addresses that name no owner: the malloc wrappers themselves and libstdc++'s operator new // family (new, new[], and their nothrow forms sit next to each other). Tags made only of these // said "operator new[] (nothrow)" for 65 KB of the heap and nothing more (heap-low, 2026-09-26). +#ifdef INPUT_DIAG_ALLOC_TAGS bool isAllocPlumbing(uint32_t v); inline void writeAllocTag(void* p, const size_t n, void* ra) { @@ -545,8 +555,14 @@ inline void writeAllocTag(void* p, const size_t n, void* ra) { } memcpy(static_cast(p) + n, tag, sizeof(tag)); } +#endif } // namespace +// Allocation owner tags are opt-in: every malloc/calloc/realloc walks up to 96 stack words and +// grows the block by 12 bytes, which made a 187-page chapter build take 35.9 s against ~10 s +// without diagnostics (2026-09-28). Define INPUT_DIAG_ALLOC_TAGS together with +// -Wl,--wrap=malloc/calloc/realloc when the heap maps need their by= owners. +#ifdef INPUT_DIAG_ALLOC_TAGS extern "C" { void* __wrap_malloc(size_t n); void* __wrap_calloc(size_t count, size_t n); @@ -599,6 +615,7 @@ void* __wrap_realloc(void* old, size_t n) { return p; } } +#endif namespace { struct HeapBlockRec { @@ -659,7 +676,7 @@ bool heapWalkRecord(walker_heap_into_t, walker_block_info_t block, void* user) { // Poisoning header: 4-byte magic, then the requested size; the tag is its last 12 bytes. uint32_t requested = 0; if (block.size >= 8) memcpy(&requested, static_cast(block.ptr) + 4, sizeof(requested)); - if (requested >= ALLOC_TAG_BYTES && requested + 8 <= block.size) { + if (ALLOC_TAGS && requested >= ALLOC_TAG_BYTES && requested + 8 <= block.size) { memcpy(r.tag, static_cast(block.ptr) + 8 + requested - ALLOC_TAG_BYTES, sizeof(r.tag)); } } @@ -702,7 +719,7 @@ bool heapLowRecord(walker_heap_into_t, walker_block_info_t block, void* user) { uint32_t tag[3] = {0, 0, 0}; uint32_t requested = 0; if (block.size >= 8) memcpy(&requested, static_cast(block.ptr) + 4, sizeof(requested)); - if (requested >= ALLOC_TAG_BYTES && requested + 8 <= block.size) { + if (ALLOC_TAGS && requested >= ALLOC_TAG_BYTES && requested + 8 <= block.size) { memcpy(tag, static_cast(block.ptr) + 8 + requested - ALLOC_TAG_BYTES, sizeof(tag)); if (!looksLikeCode(tag[0])) tag[0] = tag[1] = tag[2] = 0; } @@ -1199,6 +1216,12 @@ void InputDiag::noteBuildFontWork(const uint32_t calls, const uint32_t ms, const buildFontFullSkips = fullSkips; } +void InputDiag::noteBuildAdvanceOnly(const uint32_t calls, const uint32_t sdReads, const uint32_t ms) { + buildAdvOnlyCalls = calls; + buildAdvOnlySdReads = sdReads; + buildAdvOnlyMs = ms; +} + void InputDiag::noteLookaheadStart() { lookaheadStarts++; } void InputDiag::noteLookaheadRelease() { lookaheadReleases++; } @@ -1375,6 +1398,7 @@ void InputDiag::flush(const bool inputActive) { "aa_phases=lsb draw %ums push %ums | msb draw %ums push %ums\n" "build_page_write=%u pages %ums\n" "build_font=%u calls %ums table %u/%u full_skips %u\n" + "build_adv_only=%u calls %u sd %ums\n" "ui_prewarm_heap_max=%d\n" "list_band=y%d+h%d row%d -> %d rows (screen %d)\n", fontCopyEvents[fontCopyTotal >= FONT_COPY_EVENTS ? fontCopyTotal % FONT_COPY_EVENTS : 0], @@ -1387,8 +1411,8 @@ void InputDiag::flush(const bool inputActive) { layoutGiveUps, layoutGiveUpKind, layoutGiveUpTokens, layoutGiveUpFree, layoutGiveUpMaxAlloc, aaRefreshWaitMs, aaRefreshWaitMaxMs, aaLsbDrawMs, aaLsbPushMs, aaMsbDrawMs, aaMsbPushMs, buildPageWrites, buildPageWriteMs, buildFontCalls, buildFontMs, buildFontTableMax, buildFontTableLimit, - buildFontFullSkips, uiPrewarmHeapMax, listBandY, listBandHeight, listRowHeightPx, listVisibleRowCount, - listScreenHeight); + buildFontFullSkips, buildAdvOnlyCalls, buildAdvOnlySdReads, buildAdvOnlyMs, uiPrewarmHeapMax, + listBandY, listBandHeight, listRowHeightPx, listVisibleRowCount, listScreenHeight); if (len < 0) len = 0; if (static_cast(len) >= sizeof(reportBuf)) len = static_cast(sizeof(reportBuf) - 1); diff --git a/src/util/InputDiag.h b/src/util/InputDiag.h index 5d4622abdc4..94e7f5e0ce4 100644 --- a/src/util/InputDiag.h +++ b/src/util/InputDiag.h @@ -183,6 +183,9 @@ class InputDiag { // a per-glyph load, so a non-zero full_skips changes what the rest of the build is doing. static void noteBuildFontWork(uint32_t calls, uint32_t ms, uint32_t tableMax, uint32_t tableLimit, uint32_t fullSkips); + // Widths measured past the full table without loading bitmaps (SdCardFont::readAdvanceOnly): + // calls, how many went to the card, and the time those card reads took. Cumulative. + static void noteBuildAdvanceOnly(uint32_t calls, uint32_t sdReads, uint32_t ms); // One page-glyph prewarm: how many glyphs the heap allowed and how many the page asked for. // When the two meet, the budget bit and the rest of the page faults in one glyph at a time -- @@ -268,6 +271,7 @@ class InputDiag { static void noteGrayscalePhases(unsigned long, unsigned long, unsigned long, unsigned long) {} static void noteBuildPageWrite(unsigned long) {} static void noteBuildFontWork(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) {} + static void noteBuildAdvanceOnly(uint32_t, uint32_t, uint32_t) {} static void noteLookaheadStart() {} static void noteLookaheadChunk(int, uint16_t, unsigned long, bool) {} static void noteLookaheadRelease() {} From dbce99bd7d05349d2fe5014dfc0878c935c46bfd Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 22:03:04 +0900 Subject: [PATCH 07/59] =?UTF-8?q?README:=20=E5=B9=85=E3=81=A0=E3=81=91?= =?UTF-8?q?=E3=82=92=E8=AA=AD=E3=82=80=E7=B5=8C=E8=B7=AF=E3=81=AE=E5=89=8D?= =?UTF-8?q?=E5=80=92=E3=81=97=E3=81=AE=E7=B5=90=E6=9E=9C=E3=81=A8=E3=80=81?= =?UTF-8?q?=E6=AE=B5=E9=9A=8E3=E3=81=AE=2039=20=E7=A7=92=E3=81=AE=E8=A8=82?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 4ff82b06e2c..7a41f6c5dd9 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ - 本家 1.6.5(2026/09/27 リリース)を起点に作り直しを開始(2026/09/28〜、ブランチ `vertical-1.6.5`) - 段階0(素の 1.6.5 のビルド)、段階1(診断)、段階2(時計と監視)、段階3(字形の計測と素の実測)まで完了。詳細は下の「1.6.5ベースへの作り直し」節 + - 段階9から前倒しで、字の幅だけを読む経路を持ち込んだ。離れた章への移動(185ページの章)で組み立て 20.7 秒 → 12.6 秒、体感 21 秒 → 14 秒(`2026092815-diag`) - この時点のブランチには日本語フォントの扱い・縦書き・挿絵などはまだ入っていない(以下の各項目は 1.6.0 ベースの `feat/vertical-1.6.0based` の内容) - 章の境目の待ちを減らすため、読書中の待機時間に次の章を先に組み立てる(2026/09/09〜) - 商業書籍で章をまたぐたびに7秒前後待つのを、ページを表示して1.5秒以上操作が無い間に少しずつ済ませる @@ -264,6 +265,18 @@ CJK版だけは freeink-sdk 自体も自前でforkしていて、SSD1677パネ - ボタンの取りこぼしは無し(押下 230 回すべて受け付け) - 2点目・3点目は縦書きと挿絵を載せた後の実測②(段階9)で判断する +### 段階3の続き: 幅だけを読む経路の前倒し(2026-09-28、`2026092815-diag`、実機確認済み) +- 測り方: 商業書籍の横書き、キャッシュを消して電源を入れ直し、目次の8番目から1つ前のページへ戻る(前の章 177KB・約 180 ページを最初から組み立てる) +- **段階3の 39.2 秒の大半は診断版の記録の重さだった**(訂正)。メモリの持ち主の記録(1 塊ごとの印と呼び出し元の遡り)を外すと、同じ操作の組み立てが 31.4 秒 → 20.9 秒。持ち主の記録は `INPUT_DIAG_ALLOC_TAGS` を付けたときだけ有効にした。非診断版でも体感 21 秒で、残りは本物の遅さ +- 入れたもの(旧ツリーの `15bdbaab` を元に作り直し) + - 幅の表(768 字)が満杯になった後は、字形の画像を読まずに幅だけを返す `readAdvanceOnly`(常駐の置き場 → 予備の環 → SD の 12 バイトの字の記録)。章の組み立ての間(`MeasureOnlyScope`)だけ使う + - 組み立ての間は書体ファイルを開いたままにする(1 回の読みが 3.9ms → 1.15ms) + - SD から読んだ幅を 512 字分の表(約 3KB)に覚える。組み立ての1回分(8 ページ)ごとにファイルと一緒に捨てるので、読書中・Home には残らない + - 診断に `build_adv_only=`(呼ばれた回数・SD を読んだ回数・時間)を追加 +- 結果(同じ章): 組み立て 20.7 秒 → **12.6 秒**、体感 → **14 秒**。幅だけの読みは 2,734 回すべて SD・10.7 秒 → 2,580 回のうち SD は 1,269 回・2.3 秒。残りは配置の計算そのもの(約 6.6 秒)、ページの書き出し 2.3 秒、表の範囲の幅の読み 1.4 秒 +- 試して戻したもの: 組み立て中だけ幅の表を 2,048 字に広げる案は、時間が変わらず(21.3 秒)空きが減ったので取り消した +- 本を開く途中の空き(`open_heap` の sect)が 93KB の回と 40〜55KB の回があるのは、今回の変更ではなく、起動直後に開いた章の組み立てが1回で終わるかどうかの差。終わらない間は XML の読み取り器を次の回まで持ち続ける(本家の作り)。その状態でのページ描画で空きの底 3.9KB(最大連続 2.2KB)があり、段階9で扱う + ## 1.6.0ベースへの作り直し(2026/09/06〜) by claude code 本家1.6.0が2026/09/05に正式リリースされたため、方針どおり**1.6.0(`54337e6d`)から新ブランチ From 49da3fe08b88a043231f7cca1fea3660b8ee681d Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 22:59:20 +0900 Subject: [PATCH 08/59] =?UTF-8?q?fix:=20=E6=A8=AA=E6=9B=B8=E3=81=8D?= =?UTF-8?q?=E3=81=AE=E7=A6=81=E5=89=87=E3=82=92=20JLREQ=203.1.7=EF=BC=8F3.?= =?UTF-8?q?1.8=20=E3=81=AE=E5=90=84=E9=A1=9E=E3=81=AB=E5=BA=83=E3=81=92?= =?UTF-8?q?=E3=82=8B=EF=BC=88=E6=9C=AC=E5=AE=B6=20PR=20#3609=20=E3=82=92?= =?UTF-8?q?=E5=8F=96=E3=82=8A=E8=BE=BC=E3=81=BF=E3=80=81=E4=BD=9C=E3=82=8A?= =?UTF-8?q?=E7=9B=B4=E3=81=97=E3=81=AE=E6=AE=B5=E9=9A=8E4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本家に提出中の 708e284a(CjkLineBreak.h+gtest 9件)をそのまま持ち込む。1.6.5 で ハングルの行分けが cjkBoundaryAllowsBreak に分かれていたので、その中身を CjkLineBreak::hasCjkBreakOpportunityBetween に置き換え、ハングルも同じ規則を通す。 字の集合は旧ツリー da6b16b6 と完全に一致。章の版 49→50。 実機(2026092816-diag、horizontal-regression-test 第1章): 行頭の ー・々〜%」。、と 小書き仮名、行末の始め括弧、……―― の分割のいずれも出ない。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- lib/Epub/Epub/CjkLineBreak.h | 231 +++++++++++++++++++++++ lib/Epub/Epub/ParsedText.cpp | 148 +++++---------- lib/Epub/Epub/Section.cpp | 7 +- test/CMakeLists.txt | 1 + test/cjk_line_break/CMakeLists.txt | 15 ++ test/cjk_line_break/CjkLineBreakTest.cpp | 103 ++++++++++ 6 files changed, 405 insertions(+), 100 deletions(-) create mode 100644 lib/Epub/Epub/CjkLineBreak.h create mode 100644 test/cjk_line_break/CMakeLists.txt create mode 100644 test/cjk_line_break/CjkLineBreakTest.cpp diff --git a/lib/Epub/Epub/CjkLineBreak.h b/lib/Epub/Epub/CjkLineBreak.h new file mode 100644 index 00000000000..80036c681da --- /dev/null +++ b/lib/Epub/Epub/CjkLineBreak.h @@ -0,0 +1,231 @@ +#pragma once + +#include + +#include + +// Line-break prohibitions between adjacent CJK characters. ParsedText tokenizes CJK text one +// character at a time and consults hasCjkBreakOpportunityBetween() for every adjacent pair; +// a pair with no opportunity stays in one token, which is how a prohibited character is kept +// off the head or foot of a line. The classes follow JLREQ (W3C, Requirements for Japanese Text +// Layout): 3.1.7 lists what cannot start a line (cl-02 to cl-11, cl-13), 3.1.8 what cannot end +// one (cl-01, and cl-12 by the same reasoning), and cl-08 runs are not split. Members are taken +// from Appendix A. https://www.w3.org/TR/jlreq/ +namespace CjkLineBreak { + +inline bool isNoBreakBeforeCjkPunctuation(const uint32_t cp) { + switch (cp) { + case '.': + case ',': + case ':': + case ';': + case '!': + case '?': + case ')': + case ']': + case '}': + case 0x00BB: // » + case 0x2019: // ’ + case 0x201D: // ” + case 0x3001: // 、 + case 0x3002: // 。 + case 0x3009: // 〉 + case 0x300B: // 》 + case 0x300D: // 」 + case 0x300F: // 』 + case 0x3011: // 】 + case 0x3015: // 〕 + case 0x3017: // 〗 + case 0x3019: // 〙 + case 0x301B: // 〛 + case 0xFF01: // ! + case 0xFF09: // ) + case 0xFF0C: // , + case 0xFF0E: // . + case 0xFF1A: // : + case 0xFF1B: // ; + case 0xFF1F: // ? + case 0xFF3D: // ] + case 0xFF5D: // } + // JLREQ 3.1.7 (Characters Not Starting a Line) lists these classes in the rule itself; the + // original set carried only brackets and sentence punctuation. Members follow Appendix A. + case 0x30FB: // ・ (cl-05 middle dots) + case 0xFF65: // ・ + case 0x2010: // ‐ (cl-03 hyphens) + case 0x2013: // – + case 0x301C: // 〜 + case 0x30A0: // ゠ + case 0xFF5E: // ~ + case 0x2014: // — (cl-08 inseparable characters) + case 0x2015: // ― + case 0x2025: // ‥ + case 0x2026: // … + case 0x3005: // 々 (cl-09 iteration marks) + case 0x303B: // 〻 + case 0x309D: // ゝ + case 0x309E: // ゞ + case 0x30FD: // ヽ + case 0x30FE: // ヾ + case 0x30FC: // ー (cl-10 prolonged sound mark) + case 0xFF70: // ー + case 0xFF9E: // ゙ (halfwidth voiced marks attach to the preceding kana) + case 0xFF9F: // ゚ + case 0x00B0: // ° (cl-13 postfixed abbreviations) + case 0x2030: // ‰ + case 0x2032: // ′ + case 0x2033: // ″ + case 0x2103: // ℃ + case 0xFF05: // % + case 0x301F: // 〟 (cl-02 closing quotation marks missing above) + case 0xFF60: // ⦆ + case 0xFF61: // 。 (halfwidth full stop and comma) + case 0xFF64: // 、 + // cl-11 small kana: JLREQ 3.1.7 lists them in the rule itself; the Note records that some + // books relax this together with 々 and ー, but the principle keeps them off the line head. + case 0x3041: // ぁ + case 0x3043: // ぃ + case 0x3045: // ぅ + case 0x3047: // ぇ + case 0x3049: // ぉ + case 0x3063: // っ + case 0x3083: // ゃ + case 0x3085: // ゅ + case 0x3087: // ょ + case 0x308E: // ゎ + case 0x3095: // ゕ + case 0x3096: // ゖ + case 0x30A1: // ァ + case 0x30A3: // ィ + case 0x30A5: // ゥ + case 0x30A7: // ェ + case 0x30A9: // ォ + case 0x30C3: // ッ + case 0x30E3: // ャ + case 0x30E5: // ュ + case 0x30E7: // ョ + case 0x30EE: // ヮ + case 0x30F5: // ヵ + case 0x30F6: // ヶ + case 0x31F0: // ㇰ .. ㇿ (Ainu small katakana) + case 0x31F1: + case 0x31F2: + case 0x31F3: + case 0x31F4: + case 0x31F5: + case 0x31F6: + case 0x31F7: + case 0x31F8: + case 0x31F9: + case 0x31FA: + case 0x31FB: + case 0x31FC: + case 0x31FD: + case 0x31FE: + case 0x31FF: + // cl-04 dividing punctuation beyond ! and ? + case 0x203C: // ‼ + case 0x2047: // ⁇ + case 0x2048: // ⁈ + case 0x2049: // ⁉ + // cl-13 postfixed abbreviations, the rest of Appendix A.13 + case 0x0025: // % + case 0x00A2: // ¢ + case 0xFFE0: // ¢ + case 0x2113: // ℓ + case 0x33CB: // ㏋ + case 0x3303: // ㌃ .. ㍄ squared unit symbols + case 0x330D: + case 0x3314: + case 0x3318: + case 0x3322: + case 0x3323: + case 0x3326: + case 0x3327: + case 0x332B: + case 0x3336: + case 0x333B: + case 0x3349: + case 0x334A: + case 0x334D: + case 0x3351: + case 0x3357: + case 0x338E: + case 0x338F: + case 0x339C: + case 0x339D: + case 0x339E: + case 0x33A1: + case 0x33C4: + return true; + default: + return false; + } +} + +inline bool isNoBreakAfterCjkPunctuation(const uint32_t cp) { + switch (cp) { + case '(': + case '[': + case '{': + case 0x00AB: // « + case 0x2018: // ‘ + case 0x201C: // “ + case 0x3008: // 〈 + case 0x300A: // 《 + case 0x300C: // 「 + case 0x300E: // 『 + case 0x3010: // 【 + case 0x3014: // 〔 + case 0x3016: // 〖 + case 0x3018: // 〘 + case 0x301A: // 〚 + case 0xFF08: // ( + case 0xFF3B: // [ + case 0xFF5B: // { + case 0x301D: // 〝 (cl-01 opening quotation marks missing above) + case 0xFF5F: // ⦅ + case 0x0024: // $ (cl-12 prefixed abbreviations, JLREQ 3.1.8 / Appendix A.12: they belong to the number that + // follows) + case 0x00A3: // £ + case 0x00A5: // ¥ + case 0x20AC: // € + case 0x2116: // № + case 0x0023: // # + case 0xFF03: // # + case 0xFF04: // $ + case 0xFFE1: // £ + case 0xFFE5: // ¥ + return true; + default: + return false; + } +} + +// cl-08 inseparable characters: a run of the same leader or dash (……, ――) is one mark +// and must not be split. Only same-character pairs qualify; a leader followed by text +// may break after it. +inline bool isCjkInseparablePair(const uint32_t leftCp, const uint32_t rightCp) { + if (leftCp != rightCp) return false; + switch (leftCp) { + case 0x2014: // — + case 0x2015: // ― + case 0x2025: // ‥ + case 0x2026: // … + case 0x3033: // 〳 + case 0x3034: // 〴 + case 0x3035: // 〵 + return true; + default: + return false; + } +} + +inline bool hasCjkBreakOpportunityBetween(const uint32_t leftCp, const uint32_t rightCp) { + if (!utf8IsCjkBreakable(leftCp) && !utf8IsCjkBreakable(rightCp)) return false; + if (isNoBreakAfterCjkPunctuation(leftCp) || isNoBreakBeforeCjkPunctuation(rightCp)) return false; + if (isCjkInseparablePair(leftCp, rightCp)) return false; + if (utf8IsCombiningMark(rightCp)) return false; + return true; +} + +} // namespace CjkLineBreak diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index fd0a49d10eb..c4a7dc7894e 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -1,5 +1,6 @@ #include "ParsedText.h" +#include #include #include #include @@ -14,6 +15,7 @@ #include #include +#include "CjkLineBreak.h" #include "TokenBoundary.h" #include "hyphenation/HyphenationCommon.h" #include "hyphenation/Hyphenator.h" @@ -66,72 +68,6 @@ uint32_t lastCodepoint(const std::string_view word) { bool containsSoftHyphen(const std::string_view word) { return word.find(SOFT_HYPHEN_UTF8) != std::string_view::npos; } -bool isNoBreakBeforeCjkPunctuation(const uint32_t cp) { - switch (cp) { - case '.': - case ',': - case ':': - case ';': - case '!': - case '?': - case ')': - case ']': - case '}': - case 0x00BB: // » - case 0x2019: // ’ - case 0x201D: // ” - case 0x3001: // 、 - case 0x3002: // 。 - case 0x3009: // 〉 - case 0x300B: // 》 - case 0x300D: // 」 - case 0x300F: // 』 - case 0x3011: // 】 - case 0x3015: // 〕 - case 0x3017: // 〗 - case 0x3019: // 〙 - case 0x301B: // 〛 - case 0xFF01: // ! - case 0xFF09: // ) - case 0xFF0C: // , - case 0xFF0E: // . - case 0xFF1A: // : - case 0xFF1B: // ; - case 0xFF1F: // ? - case 0xFF3D: // ] - case 0xFF5D: // } - return true; - default: - return false; - } -} - -bool isNoBreakAfterCjkPunctuation(const uint32_t cp) { - switch (cp) { - case '(': - case '[': - case '{': - case 0x00AB: // « - case 0x2018: // ‘ - case 0x201C: // “ - case 0x3008: // 〈 - case 0x300A: // 《 - case 0x300C: // 「 - case 0x300E: // 『 - case 0x3010: // 【 - case 0x3014: // 〔 - case 0x3016: // 〖 - case 0x3018: // 〘 - case 0x301A: // 〚 - case 0xFF08: // ( - case 0xFF3B: // [ - case 0xFF5B: // { - return true; - default: - return false; - } -} - bool containsCjkBreakableCodepoint(const std::string& text) { const auto* ptr = reinterpret_cast(text.c_str()); while (*ptr) { @@ -154,18 +90,11 @@ uint32_t countCodepoints(const std::string_view text) { return count; } -bool cjkBoundaryAllowsBreak(const uint32_t leftCp, const uint32_t rightCp) { - if (!utf8IsCjkBreakable(leftCp) && !utf8IsCjkBreakable(rightCp)) return false; - if (isNoBreakAfterCjkPunctuation(leftCp) || isNoBreakBeforeCjkPunctuation(rightCp)) return false; - if (utf8IsCombiningMark(rightCp)) return false; - return true; -} - // Korean separates words with spaces, so a boundary touching Hangul is not a gap-less break inside // a line. hangulLineEndBreaks() still lets a Hangul word split there at a line end. bool hasCjkBreakOpportunityBetween(const uint32_t leftCp, const uint32_t rightCp) { if (utf8IsHangul(leftCp) || utf8IsHangul(rightCp)) return false; - return cjkBoundaryAllowsBreak(leftCp, rightCp); + return CjkLineBreak::hasCjkBreakOpportunityBetween(leftCp, rightCp); } // Line-end split points inside a Hangul word, using the CJK boundary rules (no hyphen is drawn). @@ -178,7 +107,7 @@ std::vector hangulLineEndBreaks(const std::string& word) while (*ptr) { const size_t offset = static_cast(ptr - start); const uint32_t cur = utf8NextCodepoint(&ptr); - if ((utf8IsHangul(prev) || utf8IsHangul(cur)) && cjkBoundaryAllowsBreak(prev, cur)) { + if ((utf8IsHangul(prev) || utf8IsHangul(cur)) && CjkLineBreak::hasCjkBreakOpportunityBetween(prev, cur)) { breaks.push_back({offset, false}); } prev = cur; @@ -187,36 +116,44 @@ std::vector hangulLineEndBreaks(const std::string& word) } std::vector cjkCharacterBreakByteOffsets(const std::string& text) { - struct CodepointBoundary { - uint32_t cp; - size_t endOffset; - }; - - std::vector codepoints; - codepoints.reserve(text.size()); - bool hasCjkBreakable = false; - + // Single pass over the codepoints, keeping only the previous one. The break rule + // (hasCjkBreakOpportunityBetween) looks at adjacent pairs, so no per-codepoint table + // is needed. The earlier form materialised every codepoint as an 8-byte record first, + // which for a Japanese paragraph -- one unspaced "word" -- cost 8x the paragraph's + // bytes in one block: 20 KB for a 2.5 KB paragraph, requested mid-build where the + // largest free block was 5 KB. With -fno-exceptions the failed reserve was a + // terminate() (crash 2026-09-17, horizontal-regression-test ch.14). The result vector + // is bounded by one entry per codepoint; pure CJK is 3 bytes each, so reserve a third + // and let a mixed run grow. + std::vector allowedOffsets; const auto* ptr = reinterpret_cast(text.c_str()); const auto* const start = ptr; + uint32_t prevCp = utf8NextCodepoint(&ptr); + if (prevCp == 0) return {}; + size_t prevEnd = static_cast(ptr - start); + bool hasCjkBreakable = utf8IsCjkBreakable(prevCp); + bool reserved = false; while (*ptr) { const uint32_t cp = utf8NextCodepoint(&ptr); if (cp == 0) break; - if (utf8IsCjkBreakable(cp)) { - hasCjkBreakable = true; + if (utf8IsCjkBreakable(cp)) hasCjkBreakable = true; + if (hasCjkBreakOpportunityBetween(prevCp, cp)) { + if (!reserved) { + // First opportunity found: size the result once. Skip the whole per-character + // split when even that block cannot be placed -- the word then stays one token, + // and the line pre-check reports the page instead of the allocator aborting. + const size_t want = text.size() / 3 + 1; + constexpr size_t RESERVE_HEADROOM = 4 * 1024; + if (ESP.getMaxAllocHeap() < want * sizeof(size_t) + RESERVE_HEADROOM) return {}; + allowedOffsets.reserve(want); + reserved = true; + } + allowedOffsets.push_back(prevEnd); } - codepoints.push_back({cp, static_cast(ptr - start)}); - } - - if (!hasCjkBreakable || codepoints.size() < 2) return {}; - - std::vector allowedOffsets; - allowedOffsets.reserve(codepoints.size() - 1); - for (size_t i = 0; i + 1 < codepoints.size(); ++i) { - const uint32_t current = codepoints[i].cp; - const uint32_t next = codepoints[i + 1].cp; - if (!hasCjkBreakOpportunityBetween(current, next)) continue; - allowedOffsets.push_back(codepoints[i].endOffset); + prevCp = cp; + prevEnd = static_cast(ptr - start); } + if (!hasCjkBreakable) return {}; return allowedOffsets; } @@ -709,8 +646,19 @@ void ParsedText::ensureRubyCapacity() { // and no large contiguous reallocation to avoid). Kept for call-site stability. } +bool ParsedText::beginsWithIdeographicSpace() const { return !words.empty() && firstCodepoint(wordAt(0)) == 0x3000; } + int ParsedText::resolveFirstLineIndent(const bool isFirstLine, const GfxRenderer& renderer, const int fontId) const { - if (!isFirstLine || !isNaturalAlign) { + // lineEmitted: a paragraph past the parser's soft-flush threshold reaches this in several + // passes, and every pass would otherwise call its own first line the paragraph's first line + // and indent it again mid-paragraph. + if (!isFirstLine || lineEmitted || !isNaturalAlign) { + return 0; + } + // A leading U+3000 is the indent. Adding ours would set the line in twice -- the shape most + // paperback-derived EPUBs take. An explicit text-indent below still wins: that is the author + // asking for a specific measure rather than relying on the default. + if (!blockStyle.textIndentDefined && beginsWithIdeographicSpace()) { return 0; } if (blockStyle.textIndentDefined) { @@ -1735,6 +1683,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const droppedWords = true; return; } + lineEmitted = true; processLine(std::move(block), lineVisibleOffset); return; } @@ -1760,5 +1709,6 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const droppedWords = true; // see the non-focus branch above return; } + lineEmitted = true; processLine(std::move(block), lineVisibleOffset); } diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index a6387d494ca..43ffce4ce19 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -56,7 +56,12 @@ namespace { // v48: Hangul words wrap at spaces; with hyphenation on they may also split at a line end. // Justification no longer stretches between syllables. // v49: Variation selectors (VS1-VS16, IVS VS17-VS256) are dropped instead of drawn as tofu. -constexpr uint8_t SECTION_FILE_VERSION = 49; +// v50: CJK line-break prohibitions cover the JLREQ classes (hyphens, middle dots, iteration marks, +// prolonged sound mark, small kana, prefixed/postfixed abbreviations), so cached layouts change. +// v51: In-chapter anchors name the page their first line lands on; a soft-flushed paragraph +// is indented once, and not at all on top of a leading U+3000; its top margin goes before +// its first line instead of before the final pass. +constexpr uint8_t SECTION_FILE_VERSION = 51; // Written into the version field while a build is in progress; patched to // SECTION_FILE_VERSION only when the build is finalized. An abandoned / // crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 184721f52db..bab07c15908 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -54,6 +54,7 @@ add_subdirectory(combining_marks) add_subdirectory(credential_integrity) add_subdirectory(xml_parser_utils) add_subdirectory(token_boundary) +add_subdirectory(cjk_line_break) add_subdirectory(koreader_xpath_resolver) add_subdirectory(progress_comparison) add_subdirectory(font_cache_manager) diff --git a/test/cjk_line_break/CMakeLists.txt b/test/cjk_line_break/CMakeLists.txt new file mode 100644 index 00000000000..93c0b82fb50 --- /dev/null +++ b/test/cjk_line_break/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(CjkLineBreakTest + CjkLineBreakTest.cpp +) + +target_include_directories(CjkLineBreakTest PRIVATE + ${REPO_ROOT}/lib/Utf8 + ${REPO_ROOT}/lib/Epub/Epub +) + +target_link_libraries(CjkLineBreakTest PRIVATE + crosspoint_test_common + GTest::gtest_main +) + +gtest_discover_tests(CjkLineBreakTest) diff --git a/test/cjk_line_break/CjkLineBreakTest.cpp b/test/cjk_line_break/CjkLineBreakTest.cpp new file mode 100644 index 00000000000..8077b6bd9ae --- /dev/null +++ b/test/cjk_line_break/CjkLineBreakTest.cpp @@ -0,0 +1,103 @@ +#include + +#include + +#include "CjkLineBreak.h" + +using CjkLineBreak::hasCjkBreakOpportunityBetween; + +namespace { +constexpr uint32_t KANJI = 0x6F22; // 漢 +constexpr uint32_t HIRAGANA = 0x3042; // あ +constexpr uint32_t HANGUL = 0xAC00; // 가 +} // namespace + +TEST(CjkLineBreak, PlainIdeographsMayBreakBetweenEachOther) { + EXPECT_TRUE(hasCjkBreakOpportunityBetween(KANJI, KANJI)); + EXPECT_TRUE(hasCjkBreakOpportunityBetween(KANJI, HIRAGANA)); + EXPECT_TRUE(hasCjkBreakOpportunityBetween(HANGUL, HANGUL)); +} + +TEST(CjkLineBreak, LatinPairsAreNotCjkOpportunities) { + EXPECT_FALSE(hasCjkBreakOpportunityBetween('a', 'b')); + EXPECT_FALSE(hasCjkBreakOpportunityBetween('5', 0x00B0)); // 5° stays a Latin word +} + +// JLREQ 3.1.7: characters that cannot start a line stay attached to what precedes them. +TEST(CjkLineBreak, LineStartProhibitedCharactersStayWithThePrecedingCharacter) { + constexpr uint32_t cps[] = { + 0x3001, 0x3002, 0x300D, 0xFF09, // 、。」) (cl-06/07, cl-02) + 0x30FC, 0xFF70, // ー ー (cl-10 prolonged sound mark) + 0x30FB, 0xFF65, // ・ ・ (cl-05 middle dots) + 0x3005, 0x309D, 0x30FD, 0x303B, // 々 ゝ ヽ 〻 (cl-09 iteration marks) + 0x2010, 0x2013, 0x301C, 0xFF5E, // ‐ – 〜 ~ (cl-03 hyphens) + 0x2026, 0x2025, 0x2015, // … ‥ ― (cl-08 inseparable) + 0xFF05, 0x2103, 0x2032, 0x00B0, // % ℃ ′ ° (cl-13 postfixed abbreviations) + 0x301F, 0xFF61, 0xFF64, // 〟 。 、 + }; + for (const uint32_t cp : cps) { + EXPECT_FALSE(hasCjkBreakOpportunityBetween(KANJI, cp)) << "U+" << std::hex << cp << " may not start a line"; + EXPECT_FALSE(hasCjkBreakOpportunityBetween(HIRAGANA, cp)) << "U+" << std::hex << cp << " may not start a line"; + } +} + +// JLREQ 3.1.7: characters that cannot end a line stay attached to what follows them. +TEST(CjkLineBreak, LineEndProhibitedCharactersStayWithTheFollowingCharacter) { + constexpr uint32_t cps[] = { + 0x300C, 0x300E, 0xFF08, 0x3008, // 「『(〈 (cl-01) + 0x301D, 0xFF5F, // 〝 ⦅ + 0x00A5, 0xFFE5, 0x2116, 0xFF04, // ¥ ¥ № $ (cl-12 prefixed abbreviations) + }; + for (const uint32_t cp : cps) { + EXPECT_FALSE(hasCjkBreakOpportunityBetween(cp, KANJI)) << "U+" << std::hex << cp << " may not end a line"; + } +} + +// A prohibited character still releases the character after it: 「漢 glues, 漢」glues, but 」漢 breaks. +TEST(CjkLineBreak, ProhibitionsAreOneSided) { + EXPECT_TRUE(hasCjkBreakOpportunityBetween(0x300D, KANJI)); // 」漢 + EXPECT_TRUE(hasCjkBreakOpportunityBetween(0x30FC, KANJI)); // ー漢 + EXPECT_TRUE(hasCjkBreakOpportunityBetween(KANJI, 0x300C)); // 漢「 + EXPECT_TRUE(hasCjkBreakOpportunityBetween(0x3002, 0x300C)); // 。「 +} + +// Nested brackets chain: every pair in 『「漢」』 is glued, so the run never splits. +TEST(CjkLineBreak, NestedBracketsChainThroughGluedPairs) { + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0x300E, 0x300C)); // 『「 + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0x300C, KANJI)); // 「漢 + EXPECT_FALSE(hasCjkBreakOpportunityBetween(KANJI, 0x300D)); // 漢」 + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0x300D, 0x300F)); // 」』 + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0x300F, 0x3002)); // 』。 +} + +// cl-08: a run of the same leader is one mark. It may still break after the run ends. +TEST(CjkLineBreak, InseparableRunsStayTogetherButReleaseWhatFollows) { + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0x2026, 0x2026)); // …… + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0x2015, 0x2015)); // ―― + EXPECT_FALSE(hasCjkBreakOpportunityBetween(KANJI, 0x2026)); // 漢… + EXPECT_TRUE(hasCjkBreakOpportunityBetween(0x2026, KANJI)); // …漢 + // 〳〴〵 sit inside the CJK Symbols block, so the pair rule is what keeps them together; + // … and ― are General Punctuation and already stay glued by the block test above. + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0x3033, 0x3033)); // 〳〳 + EXPECT_TRUE(hasCjkBreakOpportunityBetween(0x3033, KANJI)); // 〳漢 +} + +// cl-11 small kana are in the 3.1.7 rule itself (the Note records a relaxed practice that lets +// them start a line; the principle does not). +TEST(CjkLineBreak, SmallKanaMayNotStartALine) { + constexpr uint32_t cps[] = {0x3063, 0x3083, 0x30A1, 0x30C3, 0x30F5, 0x31F0}; // っ ゃ ァ ッ ヵ ㇰ + for (const uint32_t cp : cps) { + EXPECT_FALSE(hasCjkBreakOpportunityBetween(KANJI, cp)) << "U+" << std::hex << cp; + EXPECT_FALSE(hasCjkBreakOpportunityBetween(HIRAGANA, cp)) << "U+" << std::hex << cp; + } + EXPECT_TRUE(hasCjkBreakOpportunityBetween(0x3063, KANJI)); // っ漢: still releases what follows +} + +// cl-04 dividing punctuation beyond ! and ?, cl-12/13 abbreviations from Appendix A. +TEST(CjkLineBreak, AppendixAMembersBeyondTheAsciiForms) { + EXPECT_FALSE(hasCjkBreakOpportunityBetween(KANJI, 0x203C)); // 漢‼ + EXPECT_FALSE(hasCjkBreakOpportunityBetween(KANJI, 0x2049)); // 漢⁉ + EXPECT_FALSE(hasCjkBreakOpportunityBetween(KANJI, 0x00A2)); // 漢¢ + EXPECT_FALSE(hasCjkBreakOpportunityBetween(0xFF04, KANJI)); // $漢 + EXPECT_TRUE(hasCjkBreakOpportunityBetween(KANJI, 0x3012)); // 漢〒: not a JLREQ prefix class +} From 72237ceadfe93e0fcb440dd68699e8b32becc06f Mon Sep 17 00:00:00 2001 From: OSAKANA TARO Date: Mon, 28 Sep 2026 22:59:24 +0900 Subject: [PATCH 09/59] =?UTF-8?q?fix:=20=E9=95=B7=E3=81=84=E6=AE=B5?= =?UTF-8?q?=E8=90=BD=E3=81=AE=E5=AD=97=E5=A2=83=E3=82=921=E5=9B=9E?= =?UTF-8?q?=E3=81=AE=E8=B5=B0=E6=9F=BB=E3=81=A7=E4=BD=9C=E3=82=8A=E3=80=81?= =?UTF-8?q?=E5=88=86=E3=81=91=E3=81=A6=E7=B5=84=E3=82=93=E3=81=A0=E7=B6=9A?= =?UTF-8?q?=E3=81=8D=E3=81=AB=E5=AD=97=E4=B8=8B=E3=81=92=E3=83=BB=E4=B8=8A?= =?UTF-8?q?=E3=81=AE=E4=BD=99=E7=99=BD=E3=82=92=E5=85=A5=E3=82=8C=E3=81=AA?= =?UTF-8?q?=E3=81=84=EF=BC=88=E6=AE=B5=E9=9A=8E4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 字境の表(旧ツリー b18977ad): 段落の8倍の一塊を求めず、隣り合う2字を1回の走査で 判定する。2,559 バイトの段落で一時記憶 20.5KB → 3.4KB。置けなければ分割を諦める - 語数の上限で分けて組んだ続きの回は段落の1行目ではない(旧ツリー 82eed024 の2番、 lineEmitted)。U+3000 で始まる段落には端末の字下げを重ねない - 段落の上の余白は最初に行を出す回の前に1回だけ足す。従来は最後の回(makePages)で 足していたため、分けて組んだ長い段落では段落の途中に 0.5em の空きが入り、頭には 入らなかった(旧ツリーも同じ) - 章内の飛び先は、その段落の行が実際に載ったページを指す(旧ツリー 82eed024 の1番) - 章の版 50→51 実機(2026092818-diag、horizontal-regression-test 第13章): 落ちない、段落の頭は1字、 途中に字下げも空きも無い、見出しと本文の間に余白、「目印へ飛ぶ」が目印の段落の 1行目があるページに着く。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GSMiDKH4CDUUtu78uJCe4F --- lib/Epub/Epub/ParsedText.h | 19 +++++ .../Epub/parsers/ChapterHtmlSlimParser.cpp | 73 ++++++++++++++++--- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 8 +- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index a277aa01a0c..26a6287fc7a 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -64,6 +64,13 @@ class ParsedText { bool hyphenationEnabled; bool focusReadingEnabled; bool isNaturalAlign; + // True once this paragraph has handed a line to the page. The parser soft-flushes a paragraph + // over the word threshold, so layout runs several times over the same ParsedText; each run + // numbers its own lines from zero and would otherwise re-apply the first-line indent at every + // flush boundary. Never reset: one ParsedText is one paragraph (startNewTextBlock builds a + // fresh one). + bool lineEmitted = false; + bool topSpacingClaimed = false; bool hasRtlWord; bool droppedWords = false; std::vector reorderedWordsScratch; @@ -85,6 +92,10 @@ class ParsedText { int calculateRubyExtraEndOffset(size_t lineStartIdx, size_t lineBreakIdx, const GfxRenderer& renderer, int fontId) const; int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const; + // True when the paragraph already opens with an ideographic space (U+3000), which + // paperback-derived EPUBs use as the first-line indent itself. Adding the reader's own + // indent on top of it sets the line in two characters. + bool beginsWithIdeographicSpace() const; std::vector computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector& wordWidths, std::vector& continuesVec, std::vector& noSpaceBeforeVec); @@ -126,6 +137,14 @@ class ParsedText { BlockStyle& getBlockStyle() { return blockStyle; } size_t size() const { return words.size(); } bool isEmpty() const { return words.empty(); } + // True the first time only. The block's top margin belongs before its first line, and a + // paragraph past the soft-flush threshold is laid out in several passes -- whichever pass + // comes first claims it, so it is neither lost at the head nor inserted mid-paragraph. + bool claimTopSpacing() { + if (topSpacingClaimed) return false; + topSpacingClaimed = true; + return true; + } // True once any word was dropped because the text arena could not allocate. // Callers must treat the block as incomplete and fail the section build. bool hadDroppedWords() const { return droppedWords; } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 61ede0707ff..d1a5b75a50b 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -45,6 +45,9 @@ constexpr size_t TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS = 320; // every text fragment (e.g. Kobo KePub spans). The cap prevents unbounded heap growth // on resource-constrained devices (~380KB heap). TOC anchors bypass this cap. constexpr size_t MAX_ANCHORS_PER_CHAPTER = 1024; +// Anchors held back until their block places something. Consecutive empty anchored elements are +// rare; past this many, the rest fall back to naming the page in progress. +constexpr size_t MAX_ANCHORS_AWAITING_PLACEMENT = 16; // Reuse serializable PageLine/PageHorizontalRule elements for a small grid. constexpr int16_t TABLE_CELL_HORIZONTAL_PADDING = 4; @@ -309,11 +312,26 @@ void ChapterHtmlSlimParser::flushPendingAnchor() { } } - // Record deferred anchor after previous block is flushed (and any TOC page break) - anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); + // Hold the anchor until a line of its block has actually been placed. completedPageCount here + // is the page in progress, and the block's first line may not fit on it -- addLineToPage then + // emits that page and puts the line on the next one. A TOC anchor came out right before this + // change only because it forces the break above first; nothing else did. + if (anchorsAwaitingPlacement.size() < MAX_ANCHORS_AWAITING_PLACEMENT) { + anchorsAwaitingPlacement.push_back(std::move(pendingAnchorId)); + } else { + anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); + } pendingAnchorId.clear(); } +void ChapterHtmlSlimParser::commitAnchorsAwaitingPlacement() { + if (anchorsAwaitingPlacement.empty()) return; + for (auto& anchor : anchorsAwaitingPlacement) { + anchorData.push_back({std::move(anchor), static_cast(completedPageCount)}); + } + anchorsAwaitingPlacement.clear(); +} + void ChapterHtmlSlimParser::setCurrentPageVisibleOffset(const uint32_t offset) { if (currentPageVisibleOffsetSet) return; // The first page always begins at the start of the body, even when the XHTML @@ -493,6 +511,8 @@ void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) { currentPage->elements.push_back(std::move(pageRule)); setCurrentPageVisibleOffset(visibleTextOffset); currentPageNextY = static_cast(currentPageNextY + ruleThickness + bottomSpacing); + // The rule is on this page, so anything still waiting is on it too. + commitAnchorsAwaitingPlacement(); if (!pendingAnchorId.empty()) { anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); @@ -751,7 +771,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* const char* idValue = atts[i + 1]; const bool isTocAnchor = std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end(); - if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) { + if (isTocAnchor || + (!isNonNavigableInlineElement(name) && + self->anchorData.size() + self->anchorsAwaitingPlacement.size() < MAX_ANCHORS_PER_CHAPTER)) { // Flush a displaced anchor before overwriting. Consecutive non-block elements // (e.g.