diff --git a/README.md b/README.md new file mode 100644 index 0000000..57e3e60 --- /dev/null +++ b/README.md @@ -0,0 +1,273 @@ +# kindle-dash + +一个面向 Kindle Voyage 的低功耗电子墨水仪表盘项目。 +当前仓库把整条链路都放在一起: + +- `calendar/`:在电脑上生成背景图、主题包和前端预览 +- `dash/`:运行在 Kindle 上,负责拉图、渲染时钟、休眠唤醒、主题切换 +- `scripts/`:同步、抓图、SSH 恢复等运维脚本 +- `bootstrap-new-kindle.sh`:新机预置与 SSH 打通后的自动化入口 +- `snapshot.sh`:同步主题、切换主题、抓取 Kindle 实机画面的统一入口 + +## 当前状态 + +- `simple` / `default` 等主题已经接入当前运行链路 +- Kindle 侧采用“低频背景 + 本机时钟重绘”的分层渲染方案 +- 新机 bootstrap 方案已实现 +- 新机 bootstrap 当前仍是“方案已实现,真机恢复出厂闭环未验证” +- `launch-from-kual.sh` 与 `setsid` 脱离方案已经落地到代码 +- 主题入口当前只保留在 `KUAL`;运行态双翻页键菜单与右下角长按默认关闭 +- 但 `KUAL -> Dashboard` 与 `Dashboard -> 原生 UI/KUAL` 的边界切换在 `Kindle Voyage 5.13.6` 上仍不稳定 + +## 项目目标 + +这个仓库不是单纯做一个网页,而是把下面几件事收成一套: + +1. 在电脑上生成适合 Kindle 分辨率的背景图和主题资源 +2. 把这些资源同步到已越狱的 Kindle +3. 让 Kindle 在低功耗模式下周期性刷新背景 +4. 在设备本机按分钟重绘时钟,不依赖网络 +5. 提供主题切换、实机截图、SSH 恢复和新机 bootstrap 工具 + +## 架构概览 + +### 1. 前端层 `calendar/` + +作用: + +- 使用 Vue 生成 Kindle 仪表盘布局 +- 导出 `kindlebg.png` +- 导出多主题背景包和 `themes.json` +- 预览不同主题与方向 + +关键文件: + +- [App.vue](/Users/gavin/kindle-dash/calendar/src/App.vue) +- [SimpleDashboard.vue](/Users/gavin/kindle-dash/calendar/src/components/SimpleDashboard.vue) +- [themes.json](/Users/gavin/kindle-dash/calendar/config/themes.json) + +### 2. 设备运行层 `dash/` + +作用: + +- 在 Kindle 上拉取或读取背景图 +- 根据调度决定刷新还是休眠 +- 在本机用 Lua + FBInk 渲染时钟 +- 切换主题、维护运行时主题状态 +- 在需要时拉起主题菜单服务 + +关键文件: + +- [dash.sh](/Users/gavin/kindle-dash/dash/src/dash.sh) +- [switch-theme.sh](/Users/gavin/kindle-dash/dash/src/switch-theme.sh) +- [render-clock.lua](/Users/gavin/kindle-dash/dash/src/local/render-clock.lua) +- [theme-sync.sh](/Users/gavin/kindle-dash/dash/src/local/theme-sync.sh) + +### 3. 运维层 `scripts/` + +作用: + +- 把当前 runtime 和主题包同步到 Kindle +- 抓取 Kindle 实机屏幕 +- 恢复 Kindle 侧 SSH +- 协助 USBNetwork 调试 + +关键入口: + +- [sync-layered-clock-to-kindle.sh](/Users/gavin/kindle-dash/scripts/sync-layered-clock-to-kindle.sh) +- [capture-kindle-screen.sh](/Users/gavin/kindle-dash/scripts/capture-kindle-screen.sh) +- [ssh-force-dropbear-22.sh](/Users/gavin/kindle-dash/scripts/kindle/ssh-force-dropbear-22.sh) + +## 目录结构 + +```text +kindle-dash/ +├── assets/ 前端静态资源 +├── bootstrap-new-kindle.sh 新机预置/后半段自动化入口 +├── snapshot.sh 同步 + 切主题 + 抓图统一入口 +├── calendar/ Vue 前端与背景导出 +├── dash/ Kindle 侧 runtime、KUAL、文档与 staging +├── scripts/ 同步、抓图、SSH 与网络辅助脚本 +└── tmp/ 本地实机截图和临时产物 +``` + +## 部署模型 + +### 已越狱 Kindle + +这条路径适合设备已经有 `KUAL`、`USBNetwork`、SSH: + +1. 在电脑上构建前端: + +```sh +cd calendar +npm install +npm run typecheck +npm run build +``` + +2. 同步到 Kindle: + +```sh +cd /Users/gavin/kindle-dash +sh scripts/sync-layered-clock-to-kindle.sh kindle +``` + +3. 切主题并立即出图: + +```sh +ssh kindle '/mnt/us/dashboard/switch-theme.sh simple portrait' +``` + +4. 如需启动 dashboard 主循环: + +```sh +ssh kindle 'cd /mnt/us/dashboard && ./start.sh' +``` + +调试或排障时,当前更推荐: + +```sh +ssh kindle 'cd /mnt/us/dashboard && DEBUG=true ./start.sh' +``` + +原因: + +- 这条路径已实机验证可用 +- 不依赖 `KUAL` 的 UI 切换链路 +- 出问题时可以直接看前台日志 + +### 新机 / 恢复出厂后 Kindle + +这条路径适合 `Kindle Voyage 5.13.6`: + +1. USB 挂载后执行预置: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest +``` + +2. 在 Kindle 上完成: + +- `WatchThis` +- `;log mrpi` +- `Rename OTA Binaries -> Rename` +- 接回 Wi‑Fi +- `KTerm` 中执行 `sh /mnt/us/ssh-force-dropbear-22.sh` + +3. 回到 Mac,执行后半段: + +```sh +sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait +``` + +注意:这条新机 bootstrap 方案当前仍未做“真机恢复出厂闭环验证”。 + +## 常用命令 + +### 前端开发 + +```sh +cd calendar +npm install +npm run dev +npm run typecheck +npm run build +npm run export:themes +``` + +### 主题与截图 + +```sh +sh snapshot.sh --list +sh snapshot.sh -t simple +sh snapshot.sh -t simple -o portrait +sh snapshot.sh --capture-only -b current-screen +sh snapshot.sh --date +``` + +### 新机 bootstrap + +```sh +sh bootstrap-new-kindle.sh -h +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest +sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait +``` + +### SSH 恢复 + +如果 Kindle 上已经有 `KTerm`,但外部 SSH 不通: + +```sh +sh /mnt/us/ssh-force-dropbear-22.sh +``` + +然后在 Mac 上: + +```sh +ssh kindle +``` + +## 使用说明 + +### 日常使用 + +- Kindle 正常联网时,背景和主题配置可以更新 +- 本机时钟按分钟重绘,不依赖网络 +- 即使远端刷新失败,只要本地还有缓存背景,设备通常还能继续显示旧背景和本机时钟 + +### 换 Wi‑Fi 后 + +- 不影响 USB 预置 +- 可能影响 `ssh kindle` +- 可能影响主题强制同步和远端背景刷新 +- 如果新网络能正常出外网,日常显示通常不受影响 + +## 验证与验收 + +当前仓库里可直接运行的校验主要是: + +```sh +cd calendar +npm run typecheck +npm run build +``` + +以及脚本级别语法检查,例如: + +```sh +sh -n bootstrap-new-kindle.sh +sh -n snapshot.sh +``` + +说明: + +- 当前仓库没有可用的 `lint` 脚本 +- 当前仓库没有可用的 `test` 脚本 + +## 文档索引 + +推荐按下面顺序看: + +1. 根仓库说明: + [README.md](/Users/gavin/kindle-dash/README.md) +2. Kindle runtime 英文原始说明: + [dash/README.md](/Users/gavin/kindle-dash/dash/README.md) +3. Voyage 5.13.6 越狱路径: + [kindle-voyage-5.13.6-watchthis-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md) +4. SSH / KTerm 兜底: + [kindle-voyage-5.13.6-dual-ssh-playbook-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-dual-ssh-playbook-zh.md) +5. Bootstrap 总说明: + [kindle-voyage-5.13.6-bootstrap-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md) +6. Bootstrap 闭环验证清单: + [kindle-voyage-5.13.6-bootstrap-validation-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md) +7. 白屏/KUAL/SSH 交接: + [kindle-voyage-5.13.6-white-screen-handoff-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-white-screen-handoff-zh.md) + +## 已知限制 + +1. `WatchThis` / demo / `;log mrpi` 仍然不能完全自动化 +2. `bootstrap-new-kindle.sh` 当前还没有做一次真机恢复出厂闭环验收 +3. `ssh kindle` 依赖当前网络和 IP;换 Wi‑Fi 后可能要重新确认地址或 SSH 配置 +4. `KUAL -> Dashboard` 与 `Dashboard -> 原生 UI/KUAL` 的边界切换在 `Kindle Voyage 5.13.6` 上仍不稳定 +5. `stop.sh` 当前必须走保守恢复路径,实验性的“快速切换”方案已在 `2026-03-17` 撤回 diff --git a/bootstrap-new-kindle.sh b/bootstrap-new-kindle.sh index f5b3c4d..e6d6ce8 100644 --- a/bootstrap-new-kindle.sh +++ b/bootstrap-new-kindle.sh @@ -4,15 +4,20 @@ set -eu ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" WATCHTHIS_DIR="$ROOT_DIR/dash/staging/watchthis" POST_JAILBREAK_ROOT="$ROOT_DIR/dash/staging/post-jailbreak-root" +KTERM_STAGING_DIR="$ROOT_DIR/dash/staging/kterm" SSH_HELPERS_DIR="$ROOT_DIR/scripts/kindle" SYNC_SCRIPT="$ROOT_DIR/scripts/sync-layered-clock-to-kindle.sh" THEMES_JSON="$ROOT_DIR/calendar/config/themes.json" +KTERM_GITHUB_REPO="bfabiszewski/kterm" MODE="all" VOLUME_PATH="/Volumes/Kindle" HOST_TARGET="kindle" THEME_ID="" ORIENTATION="" +KTERM_PACKAGE="" +DOWNLOAD_KTERM=false +KTERM_VERSION="latest" SHOW_BACKGROUND=true START_DASHBOARD=false @@ -31,6 +36,9 @@ print_usage() { -k, --kindle Kindle SSH 主机名,默认 kindle -t, --theme SSH 阶段切换到指定主题;默认使用 themes.json 的默认主题 -o, --orientation SSH 阶段切换到指定方向;默认使用 themes.json 的默认方向 + --kterm-package 指定 KTerm 安装包;官方 release 用 .zip,也兼容外部 .bin + --download-kterm 在 Mac 侧联网下载 KTerm 到 dash/staging/kterm/,再预置到 Kindle + --kterm-version 下载指定 KTerm 版本;默认 latest --no-background SSH 阶段不同步后立即切主题出图 --start-dashboard SSH 阶段额外后台启动 dashboard 主循环 -h, --help 查看帮助 @@ -44,6 +52,20 @@ print_usage() { 4. 搜索 ;log mrpi 安装 KUAL / MRPI / USBNetwork 5. 首次进 KTerm 执行 sh /mnt/us/ssh-force-dropbear-22.sh +关于 KTerm: + 1. 当前仓库默认不自带 KTerm 安装包。 + 2. 官方 KTerm release 使用 .zip,解压后是 kterm/ 扩展目录。 + 3. 如果你手头已经有 KTerm 安装包,可以: + - 放到 dash/staging/kterm/ 目录下,脚本会自动尝试拾取 + - 或执行时显式传:--kterm-package /绝对路径/kterm-kindle-*.zip + 4. 如果你希望脚本直接从网上拉,可以执行: + --download-kterm --kterm-version latest + 或: + --download-kterm --kterm-version v2.6 + 5. 下载发生在 Mac 侧,文件会缓存到 dash/staging/kterm/,然后再解压或复制到 Kindle。 + 6. 对这台 Voyage 5.13.6,脚本默认优先选择“不带 armhf 后缀”的 zip。 + 7. 如果脚本没有检测到 KTerm 包,会明确提示“这一步仍需手工补装 KTerm”。 + 推荐操作顺序: 阶段 A:先做 USB 预置 @@ -56,6 +78,8 @@ print_usage() { - Kindle 根目录出现 ssh-force-dropbear-22.sh 等脚本 - Kindle 根目录出现 dashboard/、extensions/、mrpackages/ - Kindle 根目录出现 .demo/KV-5.13.6.zip、.demo/demo.json、.demo/goodreads/ + - 如果提供了 KTerm zip,extensions/ 里会被解压出 kterm/ + - 如果提供了外部 KTerm bin,mrpackages/ 里会出现对应文件 5. 安全弹出 Kindle。 阶段 B:在 Kindle 上完成 WatchThis 和越狱 @@ -77,6 +101,7 @@ print_usage() { - 首页出现 KUAL - KUAL 菜单里有 Rename OTA Binaries - KUAL 菜单里有 kindle-dash + - 如果本次预置了 KTerm 安装包,首页或搜索里应能找到 KTerm 4. 先在 KUAL 中执行: Rename OTA Binaries -> Rename @@ -114,9 +139,12 @@ print_usage() { 2. 真正导 payload 的时机,是隐藏手势返回后,再次进入 ;demo -> Sideload Content。 3. 首次 SSH 最稳的入口是 KTerm,不是 USB 直连盲试。 4. post-ssh 阶段如果不带 -t/-o,会退回 themes.json 里的默认主题和方向。 + 5. 如果本轮没有预置 KTerm 包,阶段 C 结束后仍需你手工补装 KTerm。 示例: sh bootstrap-new-kindle.sh prepare-storage + sh bootstrap-new-kindle.sh prepare-storage --kterm-package ~/Downloads/kterm-kindle-2.6.zip + sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest sh bootstrap-new-kindle.sh all -t simple -o portrait sh bootstrap-new-kindle.sh post-ssh -k kindle --start-dashboard EOF @@ -135,6 +163,115 @@ require_path() { fi } +detect_kterm_package() { + if [ -n "$KTERM_PACKAGE" ]; then + if [ ! -f "$KTERM_PACKAGE" ]; then + echo "指定的 KTerm 安装包不存在: $KTERM_PACKAGE" >&2 + exit 1 + fi + printf '%s\n' "$KTERM_PACKAGE" + return 0 + fi + + if [ ! -d "$KTERM_STAGING_DIR" ]; then + return 1 + fi + + set +e + set -- "$KTERM_STAGING_DIR"/*.zip "$KTERM_STAGING_DIR"/*.bin + status=$? + set -e + + if [ "$status" -ne 0 ] || [ ! -e "$1" ]; then + return 1 + fi + + printf '%s\n' "$1" +} + +download_kterm_if_requested() { + if [ "$DOWNLOAD_KTERM" != true ]; then + return + fi + + mkdir -p "$KTERM_STAGING_DIR" + metadata_json="$KTERM_STAGING_DIR/kterm-release-${KTERM_VERSION}.json" + + case "$KTERM_VERSION" in + latest) + release_api_url="https://api.github.com/repos/$KTERM_GITHUB_REPO/releases/latest" + ;; + *) + release_api_url="https://api.github.com/repos/$KTERM_GITHUB_REPO/releases/tags/$KTERM_VERSION" + ;; + esac + + echo "正在从 GitHub 获取 KTerm release 元数据: $release_api_url" + curl -fsSL "$release_api_url" -o "$metadata_json" + + release_info="$(python3 - "$metadata_json" <<'PY' +import json +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +data = json.loads(path.read_text()) +assets = data.get("assets", []) + +preferred = None +fallback = None +for asset in assets: + name = asset.get("name", "") + url = asset.get("browser_download_url", "") + if not name.endswith(".zip"): + continue + if fallback is None: + fallback = (name, url) + if "armhf" not in name.lower(): + preferred = (name, url) + break + +selected = preferred or fallback +if not selected: + raise SystemExit("未找到可用的 KTerm zip 资产") + +tag = data.get("tag_name", "unknown") +print(f"TAG={tag}") +print(f"NAME={selected[0]}") +print(f"URL={selected[1]}") +PY +)" + + downloaded_tag="" + downloaded_name="" + downloaded_url="" + while IFS='=' read -r key value; do + case "$key" in + TAG) + downloaded_tag=$value + ;; + NAME) + downloaded_name=$value + ;; + URL) + downloaded_url=$value + ;; + esac + done <&2 + exit 1 + fi + + downloaded_path="$KTERM_STAGING_DIR/$downloaded_name" + echo "下载 KTerm: tag=$downloaded_tag asset=$downloaded_name" + curl -fL "$downloaded_url" -o "$downloaded_path" + KTERM_PACKAGE="$downloaded_path" +} + kindle_volume_available() { [ -d "$VOLUME_PATH" ] } @@ -219,6 +356,31 @@ copy_post_jailbreak_bundle() { rsync -av --no-o --no-g "$POST_JAILBREAK_ROOT/dashboard/" "$VOLUME_PATH/dashboard/" } +install_kterm_package_if_available() { + detected_kterm_package=$(detect_kterm_package || true) + if [ -z "$detected_kterm_package" ]; then + echo "未检测到 KTerm 安装包;本轮只预置 SSH 恢复脚本,KTerm 仍需手工补装。" + return + fi + + case "$detected_kterm_package" in + *.zip) + mkdir -p "$VOLUME_PATH/extensions" + unzip -oq "$detected_kterm_package" -d "$VOLUME_PATH/extensions" + echo "已预置 KTerm 扩展包并解压到 extensions/: $(basename "$detected_kterm_package")" + ;; + *.bin) + mkdir -p "$VOLUME_PATH/mrpackages" + cp "$detected_kterm_package" "$VOLUME_PATH/mrpackages/" + echo "已预置 KTerm bin 到 mrpackages/: $(basename "$detected_kterm_package")" + ;; + *) + echo "不支持的 KTerm 安装包格式: $detected_kterm_package" >&2 + exit 1 + ;; + esac +} + copy_ssh_helpers() { require_path "$SSH_HELPERS_DIR/ssh-force-dropbear-22.sh" "SSH helper scripts" @@ -251,12 +413,18 @@ prepare_storage() { exit 1 fi + log_step "USB" "检查并下载 KTerm 安装包" + download_kterm_if_requested + log_step "USB" "预置 WatchThis payload" copy_watchthis_payload log_step "USB" "预置越狱后安装包" copy_post_jailbreak_bundle + log_step "USB" "检查并预置 KTerm 安装包" + install_kterm_package_if_available + log_step "USB" "预置 SSH 恢复脚本" copy_ssh_helpers @@ -265,7 +433,7 @@ prepare_storage() { } prepare_remote_helpers() { - ssh "$HOST_TARGET" "chmod +x /mnt/us/ssh-collect.sh /mnt/us/ssh-fix-all-keys.sh /mnt/us/ssh-force-dropbear-22.sh /mnt/us/ssh-force-openssh-22.sh /mnt/us/ssh-stop-all.sh 2>/dev/null || true" + ssh "$HOST_TARGET" "chmod +x /mnt/us/ssh-collect.sh /mnt/us/ssh-fix-all-keys.sh /mnt/us/ssh-force-dropbear-22.sh /mnt/us/ssh-stop-all.sh 2>/dev/null || true" ssh "$HOST_TARGET" "if [ -f /mnt/us/ssh-fix-all-keys.sh ]; then sh /mnt/us/ssh-fix-all-keys.sh; fi" } @@ -355,6 +523,17 @@ while [ "$#" -gt 0 ]; do shift ORIENTATION=${1:?"missing orientation"} ;; + --kterm-package) + shift + KTERM_PACKAGE=${1:?"missing KTerm package path"} + ;; + --download-kterm) + DOWNLOAD_KTERM=true + ;; + --kterm-version) + shift + KTERM_VERSION=${1:?"missing KTerm version"} + ;; --no-background) SHOW_BACKGROUND=false ;; diff --git a/dash/KUAL/kindle-dash/menu.json b/dash/KUAL/kindle-dash/menu.json index abaf0bc..1a788cb 100644 --- a/dash/KUAL/kindle-dash/menu.json +++ b/dash/KUAL/kindle-dash/menu.json @@ -1,6 +1,14 @@ { "items": [ - {"name": "Kindle Dashboard", "action": "/mnt/us/dashboard/start.sh"}, + { + "name": "Kindle Dashboard", + "priority": -998, + "items": [ + {"name": "Default", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default", "exitmenu": true}, + {"name": "Paper", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "paper", "exitmenu": true}, + {"name": "Classic", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "classic", "exitmenu": true} + ] + }, {"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"}, {"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"} ] diff --git a/dash/README.md b/dash/README.md index 7b75dd0..d690798 100644 --- a/dash/README.md +++ b/dash/README.md @@ -28,7 +28,7 @@ In my case I use a [dashbling](https://github.com/pascalw/dashbling) dashboard t 4. Start dashboard with `/mnt/us/dashboard/start.sh`. Note that the device will go into suspend about 10-15 seconds after you start the dashboard. 5. To leave dashboard mode and get back to the normal Kindle UI/KUAL, run `/mnt/us/dashboard/stop.sh`. - This now stops `dash.sh` and restores the Kindle framework. + On Voyage 5.13.6 this intentionally uses a conservative UI-stack restore instead of a fast handoff, because the faster paths were not stable. ## Upgrading @@ -41,12 +41,18 @@ If you're running kindle-dash already and want to update to the latest version f 5. Start dashboard with `/mnt/us/dashboard/start.sh`. Note that the device will go into suspend about 10-15 seconds after you start the dashboard. 6. Run `/mnt/us/dashboard/stop.sh` when you want to restore the normal Kindle UI/KUAL. + This uses the same conservative restore path on Voyage 5.13.6. ## KUAL If you're using KUAL you can use simple extension to start this Dashboard 1. Copy folder `kindle-dash` from `KUAL` folder to the kual `extensions` folder. (located in `/mnt/us/extensions`) +2. The `Kindle Dashboard` entry is now a theme submenu in KUAL. +3. Each theme item calls `/mnt/us/dashboard/launch-theme-from-kual.sh`, which switches the theme first and then delegates to `/mnt/us/dashboard/launch-from-kual.sh`. +4. `launch-from-kual.sh` still quits KUAL first and uses a detached session, but by default it no longer inserts an extra visible handoff delay before starting the dashboard. +5. The runtime page-key menu and the bottom-right touch hotspot are now disabled by default; theme entry is intentionally kept only in KUAL. +6. On Kindle Voyage 5.13.6, this wrapper chain is in place but the KUAL/native-UI handoff is still not considered fully stable. For debugging, prefer `ssh kindle 'cd /mnt/us/dashboard && DEBUG=true ./start.sh'`. ## Debugging @@ -57,6 +63,7 @@ If you're connected over SSH you can also run `DEBUG=true ./start.sh` to keep th If you're launching from KUAL, `Dashboard Debug On` now persists `DISABLE_SYSTEM_SUSPEND=true` and immediately restarts the dashboard in one tap. `Dashboard Debug Off` restores the normal low-power behavior and also restarts the dashboard immediately. If you're connected over SSH and only want a one-off foreground session, you can still run `/mnt/us/dashboard/start-debug.sh`. Each dashboard loop now also writes `isCharging` and `battStateInfo` from `com.lab126.powerd` into `logs/dash.log`, which makes it easier to confirm whether the Kindle actually detected external power while debugging. +On Voyage 5.13.6, if `stop.sh` finishes but the home UI is still missing, the current fallback is still to start `webreader` manually with `/sbin/start webreader`. ## How this works diff --git a/dash/docs/kindle-city-location-plan.zh.md b/dash/docs/kindle-city-location-plan.zh.md new file mode 100644 index 0000000..5703360 --- /dev/null +++ b/dash/docs/kindle-city-location-plan.zh.md @@ -0,0 +1,220 @@ +# Kindle 市级位置方案 + +## 背景 + +当前仓库里的天气位置来源仍在 `calendar/` 前端: + +- [calendar/src/lib/weather.ts](/Users/gavin/kindle-dash/calendar/src/lib/weather.ts) 会优先尝试 `navigator.geolocation` +- 失败时回退到固定默认值 `杭州` + +这条路径不适合当前 Kindle 运行架构,原因有两个: + +1. Kindle 实机显示的不是一个在设备上实时运行的天气网页,而是先在别处渲染,再同步为背景图。 +2. 如果由 Web 服务器代查 IP 位置,拿到的是服务器公网 IP,对应的是服务器所在城市,不是 Kindle 所在城市。 + +本方案的目标是: + +- 位置精度只要求到“市”颗粒度 +- 位置来源应尽量接近 Kindle 当前网络出口 +- 在 SSH 暂时不可用的情况下,先把方案文档明确下来,后续恢复 SSH 后再实现 + +## 结论 + +浏览器网页定位不应继续作为主路径。 + +推荐改成: + +1. Kindle 侧自己请求 GeoIP 接口,获取当前公网 IP 对应的城市、经纬度和时区 +2. 把结果缓存到本地 +3. 天气与相关展示统一读取这份缓存 +4. 当 GeoIP 不可用或结果异常时,回退到 Kindle 侧固定配置的 `city/lat/lon/timezone` + +这是一个“市级近似定位”方案,不是 GPS 精确定位方案。 + +## 为什么 Kindle 侧 GeoIP 可行 + +GeoIP 的前提是“由目标设备自己发请求”。 + +对当前项目,正确的请求路径应该是: + +```text +Kindle -> GeoIP 服务 -> 返回 city / lat / lon / timezone +``` + +而不是: + +```text +Kindle -> 你的 Web 服务 -> Web 服务代查 GeoIP +``` + +后一种方式只会得到 Web 服务所在机房或服务器出口的位置。 + +如果 Kindle 走家庭 Wi-Fi,且只要求“市级”颗粒度,GeoIP 通常够用。 +如果 Kindle 走手机热点、VPN、代理、企业网络或运营商 NAT,结果可能偏到邻近城市,必须接受这种误差。 + +## 目标能力 + +实现后需要具备以下能力: + +1. Kindle 联网后,能够自己获取当前网络出口对应的城市信息 +2. 获取结果会写入本地缓存 +3. 断网时仍可继续使用上次缓存 +4. GeoIP 失败时可回退到固定配置 +5. 只需要市级位置,不追求街道级精度 + +## 推荐数据模型 + +建议在 Kindle 侧维护一份位置缓存文件。 + +路径建议: + +```text +/mnt/us/dashboard/local/state/location.env +``` + +字段建议: + +```text +LOCATION_SOURCE=geoip +LOCATION_CITY=杭州 +LOCATION_LAT=30.274084 +LOCATION_LON=120.155070 +LOCATION_TIMEZONE=Asia/Shanghai +LOCATION_UPDATED_AT=2026-03-17T10:00:00+08:00 +``` + +同时保留一份固定兜底配置,例如: + +```text +LOCATION_FALLBACK_CITY=杭州 +LOCATION_FALLBACK_LAT=30.274084 +LOCATION_FALLBACK_LON=120.155070 +LOCATION_FALLBACK_TIMEZONE=Asia/Shanghai +``` + +## 运行时脚本设计 + +建议新增 Kindle 侧脚本: + +```text +dash/src/local/location-sync.sh +``` + +职责: + +1. 检查网络是否可用 +2. 由 Kindle 自己请求 GeoIP 接口 +3. 解析响应中的城市、经纬度、时区 +4. 校验字段是否完整 +5. 写入本地缓存 +6. 失败时保留旧缓存,不要清空已有结果 + +建议同时新增一个只负责读取并导出环境变量的脚本,例如: + +```text +dash/src/local/location-env.sh +``` + +职责: + +1. 优先读取新鲜的 GeoIP 缓存 +2. 没有新鲜缓存时读取旧缓存 +3. 缓存不存在或无效时回退到固定配置 +4. 对外输出统一的 `LOCATION_CITY/LAT/LON/TIMEZONE` + +## 刷新策略 + +位置不需要高频刷新。 + +建议策略: + +1. 启动 dashboard 且确认联网后,首次同步一次位置 +2. 每 12 小时或 24 小时刷新一次 +3. 手动切换主题时不强制刷新位置 +4. 天气刷新与位置刷新解耦 +5. 位置同步失败时继续使用现有缓存 + +## 与现有架构的衔接方式 + +当前项目是“背景图 + Kindle 本地时钟覆盖”的架构,不是“Kindle 上实时跑完整天气网页”的架构。 + +因此接入位置数据时,有两条可选路径。 + +### 路径 A:最小改动路径 + +保留当前背景图生成方式,但让背景图生成时显式读取 Kindle 侧缓存导出的城市与经纬度。 + +优点: + +- 改动面相对小 +- 仍可复用现有天气卡片布局与导出流程 + +缺点: + +- 需要重新梳理“背景图生成端”如何拿到 Kindle 缓存 +- 如果背景仍在 Mac 侧导出,就必须把 Kindle 缓存同步回 Mac 或转成请求参数 + +### 路径 B:更符合现状的运行时路径 + +把“城市文本、天气数据”也逐步下沉到 Kindle 运行时,和本地时钟一样由设备端控制。 + +优点: + +- 位置与天气都由 Kindle 自己决定 +- 不会再混入 Mac 浏览器位置或服务器 IP 位置 + +缺点: + +- 改动更大 +- 需要重新设计天气卡的本地渲染方式 + +## 当前推荐 + +短期推荐先做: + +1. Kindle 侧 GeoIP 获取 +2. 本地缓存 +3. 固定配置兜底 + +等 SSH 恢复后,再决定位置数据如何喂给天气展示层。 + +也就是说,先把“位置来源”稳定下来,再改“展示方式”。 + +## 实施顺序 + +建议按下面顺序落地: + +1. 新增 `location-sync.sh` +2. 新增 `location-env.sh` +3. 在 Kindle 运行时目录中引入固定兜底配置 +4. 在主循环启动阶段接入位置缓存刷新 +5. 让天气读取逻辑改为优先使用 Kindle 侧位置缓存 +6. 最后补文档和手动刷新命令 + +## 验收标准 + +实现完成后,至少应满足以下验收条件: + +1. Kindle 联网后可生成位置缓存文件 +2. 缓存中至少包含 `city/lat/lon/timezone` +3. 断网后仍能继续使用旧缓存 +4. GeoIP 失败时会稳定回退到固定城市 +5. 显示出的城市与 Kindle 当前所在城市大体一致 + +## 注意事项 + +1. GeoIP 只能提供近似位置,不要当作精确定位 +2. 手机热点、VPN、代理、企业网络都会降低城市准确率 +3. 城市级结果可以用于天气展示,但不适合做严格地理判断 +4. 任何实现都必须保证“失败不影响 dashboard 主循环” + +## 现阶段状态 + +当前仅完成方案设计,尚未开始代码实现。 + +直接原因是: + +- 目前 SSH 还未恢复 +- 设备侧脚本还不能方便地下发、调试和验证 + +后续等 SSH 恢复后,再按本方案分步实施。 diff --git a/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md b/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md new file mode 100644 index 0000000..465dfe4 --- /dev/null +++ b/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md @@ -0,0 +1,262 @@ +# Kindle Voyage 5.13.6 Bootstrap 闭环验证清单 + +## 目标 + +用一台已经越狱过、同型号同固件的 `Kindle Voyage 5.13.6` 做一次“恢复出厂 -> 重新越狱 -> 安装 KUAL/MRPI/USBNetwork/KTerm -> 打通 SSH -> 显示 dashboard”的闭环验证,确认: + +- [bootstrap-new-kindle.sh](/Users/gavin/kindle-dash/bootstrap-new-kindle.sh) 现在的方案是否足够完整 +- 哪些步骤已经可以自动化 +- 哪些步骤仍然必须人工完成 + +## 重要前提 + +执行前先确认: + +1. 设备型号仍然是 `Kindle Voyage` +2. 固件版本仍然是 `5.13.6` +3. 你接受这是破坏性验证 +4. 设备里当前的 `KUAL`、SSH、dashboard、主题配置、日志都会被清掉 + +如果固件版本不是 `5.13.6`,不要按这份清单执行。 + +## 重置前准备 + +### 1. 先备份当前设备侧用户存储 + +把 Kindle 通过 USB 挂载到 Mac 后,至少备份这些目录: + +```text +/Volumes/Kindle/dashboard +/Volumes/Kindle/extensions +/Volumes/Kindle/mrpackages +/Volumes/Kindle/documents +``` + +如果你只想做最小备份,也至少把下面这些拷走: + +- `dashboard/` +- `extensions/` +- `documents/` 里你关心的 crash 包和日志 + +### 2. 在 Mac 侧准备 bootstrap 预置 + +推荐直接用带 KTerm 下载的版本: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest +``` + +如果你不想拉 latest,也可以固定版本: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version v2.6 +``` + +这一步的预期结果: + +- Kindle 根目录出现 `Update_hotfix_watchthis_custom.bin` +- Kindle 根目录出现 `dashboard/` +- Kindle 根目录出现 `extensions/` +- Kindle 根目录出现 `mrpackages/` +- Kindle 根目录出现 `ssh-force-dropbear-22.sh` +- Kindle 根目录出现 `.demo/KV-5.13.6.zip` +- Kindle 根目录出现 `.demo/demo.json` +- Kindle 根目录出现 `.demo/goodreads/` +- 如果下载了 `KTerm`,`extensions/` 里会直接出现 `kterm/` + +做完后安全弹出 Kindle。 + +## 设备侧执行顺序 + +### 3. 恢复出厂 + +在 Kindle 上执行恢复出厂。 + +恢复后开始首启流程时: + +1. 语言只选 `English (United Kingdom)` +2. 到 Wi‑Fi 页面时,不要真的联网 +3. 进入 demo mode,按 [kindle-voyage-5.13.6-watchthis-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md#L61) 的流程操作 + +### 4. WatchThis 导入点 + +必须特别注意: + +1. 第一次 `Add Content / Sideload Content` 只能点 `Done` +2. 不要在第一次导入点接 USB +3. 真正的 payload 导入点,是隐藏手势返回后,再次进入 `;demo -> Sideload Content` + +这里的详细说明看: + +- [kindle-voyage-5.13.6-watchthis-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md#L80) +- [kindle-voyage-5.13.6-watchthis-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md#L100) + +因为 bootstrap 已经把 `.demo` payload 预置好了,这一轮不需要你在导入点再手工从 Mac 拷 `KV-5.13.6.zip`。 + +### 5. 触发越狱并验收 + +完成 `Get Started` 后,设备会重启并执行越狱脚本。 + +验收标准: + +- Kindle 用户存储根目录出现 `mkk` +- Kindle 用户存储根目录出现 `libkh` +- Kindle 用户存储根目录出现 `rp` + +如果没有这三个目录,说明这轮 `WatchThis` 没真正落地,不要继续往后做。 + +### 6. 安装 KUAL / MRPI / USBNetwork / dashboard / KTerm + +回到首页后: + +1. 搜索 `;log mrpi` +2. 等 MRPI 跑完 +3. 回首页确认: + - 有 `KUAL` + - 如果本轮预置了 `KTerm`,应该已经能看到 `KTerm` + +然后进入 `KUAL`,先执行: + +```text +Rename OTA Binaries -> Rename +``` + +不要先跑 `Kindle Dashboard`。 + +## Wi‑Fi 和 SSH 验证 + +### 7. 接回 Wi‑Fi + +让 Kindle 连到和 Mac 同一个主 Wi‑Fi。 + +不要用: + +- Guest Wi‑Fi +- 开了客户端隔离的网络 + +### 8. 在 KTerm 里拉起 DropBear + +打开 `KTerm`,执行: + +```sh +sh /mnt/us/ssh-force-dropbear-22.sh +/mnt/us/usbnet/bin/lsof -n -P -iTCP:22 +``` + +验收标准: + +应该看到类似: + +```text +dropbear... TCP *:22 (LISTEN) +``` + +### 9. 在 Mac 上确认 SSH + +回到 Mac: + +```sh +ssh kindle +``` + +登录后执行: + +```sh +id +uname -a +ps -ef | grep -E 'sshd|dropbear|telnetd' | grep -v grep +``` + +验收标准: + +- `uid=0(root)` +- 存在 `dropbearmulti dropbear ... -p 22 ...` + +如果这一步失败,按: + +- [kindle-voyage-5.13.6-dual-ssh-playbook-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-dual-ssh-playbook-zh.md#L210) + +继续排障,不要直接判断 bootstrap 失败。 + +## Dashboard 闭环验证 + +### 10. 让 bootstrap 跑后半段 + +在 Mac 上执行: + +```sh +sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait +``` + +预期结果: + +- 同步当前 dashboard 运行时 +- 同步主题包 +- 设备立即切到 `simple / portrait` +- 屏幕出现背景与时钟 + +如果你还要验证后台常驻主循环,再执行: + +```sh +sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait --start-dashboard +``` + +### 11. 抓首张验收图 + +在 Mac 上执行: + +```sh +sh snapshot.sh -t simple -o portrait +``` + +或者如果你不想再切主题,只抓当前画面: + +```sh +sh snapshot.sh --capture-only -b bootstrap-validation +``` + +验收标准: + +- 本地成功生成 `physical.png` +- Kindle 上显示 `simple / portrait` +- 时钟、天气、日历、鸡汤都正常 + +## 这轮验证要回答的问题 + +执行完后,明确记录下面 5 个结论: + +1. `prepare-storage --download-kterm` 是否足够把 USB 预置做完整 +2. `WatchThis` 是否能在“已有一轮越狱历史”的同机上再次稳定跑通 +3. `;log mrpi` 后,`KUAL / USBNetwork / KTerm / dashboard` 是否都能落地 +4. `KTerm -> ssh-force-dropbear-22.sh -> ssh kindle` 是否仍是最稳入口 +5. `post-ssh` 是否已经能把 dashboard 自动拉到“可见、可抓图”的状态 + +## 失败时怎么回退 + +如果中途失败,按这个顺序收敛: + +1. 先不要继续改 dashboard 页面代码 +2. 先判断失败落在哪一层: + - `WatchThis` + - `MRPI` + - `KTerm` + - `SSH` + - `post-ssh` +3. 如果已经拿到 `KTerm`,优先在本机看进程和端口 +4. 如果已经拿到 `ssh kindle`,优先保留日志和 `/mnt/us/ssh-debug/` +5. 不要把 `KUAL -> Dashboard -> 再返回 KUAL` 当成验收项 + +## 推荐实际执行命令 + +如果你要按最少分支走,直接按这个顺序: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest +``` + +设备侧完成恢复出厂、WatchThis、`;log mrpi`、Wi‑Fi、KTerm 拉起 SSH 后,再执行: + +```sh +sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait --start-dashboard +sh snapshot.sh --capture-only -b bootstrap-validation +``` diff --git a/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md b/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md index 6bc6a3b..5fe57c0 100644 --- a/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md +++ b/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md @@ -6,6 +6,7 @@ - 预置 `WatchThis` payload - 预置 `KUAL / MRPI / USBNetwork / kindle-dash` +- 可选预置 `KTerm` - 预置 SSH 恢复脚本 - SSH 打通后自动同步 dashboard、切主题、立即出图 @@ -32,6 +33,48 @@ sh /mnt/us/ssh-force-dropbear-22.sh - 尽量把 Mac 侧和文件预置自动化 - 把设备侧必须手工的动作压到最少 +## KTerm 说明 + +当前仓库默认不自带 `KTerm` 安装包。 + +脚本支持两种方式把 `KTerm` 纳入预置: + +1. 直接把官方 `KTerm` release 的 `.zip` 放到: + +```text +dash/staging/kterm/kterm-kindle-*.zip +``` + +2. 或执行脚本时显式传入: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --kterm-package /绝对路径/kterm-kindle-*.zip +``` + +3. 或直接让脚本在 Mac 侧联网下载: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest +``` + +也可以固定版本: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version v2.6 +``` + +如果脚本没有找到 `KTerm` 安装包,它不会报错退出,但会明确提示: + +- 本轮只预置 SSH 恢复脚本 +- `KTerm` 仍需手工补装 + +下载逻辑说明: + +- 下载发生在 Mac 侧,不在 Kindle 设备侧进行 +- 下载后的 `.zip` 会缓存到 `dash/staging/kterm/` +- 对 `Kindle Voyage 5.13.6`,脚本默认优先选择不带 `armhf` 后缀的 `.zip` +- 预置时会直接解压到 Kindle 的 `extensions/` + ## 脚本模式 ### 1. `prepare-storage` @@ -49,6 +92,7 @@ sh bootstrap-new-kindle.sh prepare-storage - 把 `Update_hotfix_watchthis_custom.bin` 放到 Kindle 根目录 - 把 `extensions/`、`mrpackages/`、`dashboard/` 预置到 Kindle - 把 `scripts/kindle/*.sh` 拷到 Kindle 根目录,供 `KTerm` 使用 +- 如果检测到 `KTerm` zip,也会一并解压到 `extensions/` ### 2. `post-ssh` @@ -94,14 +138,15 @@ sh bootstrap-new-kindle.sh 4. 通过 `Get Started` 完成越狱 5. 搜索 `;log mrpi` 6. 在 `KUAL` 中先执行 `Rename OTA Binaries -> Rename` -7. 连上 Wi‑Fi -8. 打开 `KTerm`,执行: +7. 如果本轮没有预置 `KTerm`,这里先手工补装 `KTerm` +8. 连上 Wi‑Fi +9. 打开 `KTerm`,执行: ```sh sh /mnt/us/ssh-force-dropbear-22.sh ``` -9. 回到 Mac,执行: +10. 回到 Mac,执行: ```sh sh bootstrap-new-kindle.sh post-ssh @@ -113,3 +158,5 @@ sh bootstrap-new-kindle.sh post-ssh [kindle-voyage-5.13.6-watchthis-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md) - SSH 打通与 KTerm 兜底: [kindle-voyage-5.13.6-dual-ssh-playbook-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-dual-ssh-playbook-zh.md) +- 恢复出厂后的 bootstrap 闭环验证: + [kindle-voyage-5.13.6-bootstrap-validation-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md) diff --git a/dash/docs/kindle-voyage-5.13.6-dual-ssh-playbook-zh.md b/dash/docs/kindle-voyage-5.13.6-dual-ssh-playbook-zh.md index c212405..4cd2a68 100644 --- a/dash/docs/kindle-voyage-5.13.6-dual-ssh-playbook-zh.md +++ b/dash/docs/kindle-voyage-5.13.6-dual-ssh-playbook-zh.md @@ -345,7 +345,6 @@ cat /mnt/us/usbnet/etc/config - [scripts/kindle/ssh-collect.sh](/Users/gavin/kindle-dash/scripts/kindle/ssh-collect.sh) - [scripts/kindle/ssh-fix-all-keys.sh](/Users/gavin/kindle-dash/scripts/kindle/ssh-fix-all-keys.sh) -- [scripts/kindle/ssh-force-openssh-22.sh](/Users/gavin/kindle-dash/scripts/kindle/ssh-force-openssh-22.sh) - [scripts/kindle/ssh-force-dropbear-22.sh](/Users/gavin/kindle-dash/scripts/kindle/ssh-force-dropbear-22.sh) - [scripts/kindle/ssh-stop-all.sh](/Users/gavin/kindle-dash/scripts/kindle/ssh-stop-all.sh) diff --git a/dash/docs/kindle-voyage-5.13.6-white-screen-handoff-zh.md b/dash/docs/kindle-voyage-5.13.6-white-screen-handoff-zh.md index ad88dfa..980d1c8 100644 --- a/dash/docs/kindle-voyage-5.13.6-white-screen-handoff-zh.md +++ b/dash/docs/kindle-voyage-5.13.6-white-screen-handoff-zh.md @@ -1,22 +1,25 @@ # Kindle Voyage 5.13.6 白屏/KUAL/SSH 交接文档 -本文记录 2026-03-15 这轮 dashboard 调试在后半段进入的异常状态,目标是给下一次接手排障的人一个明确起点,避免继续沿着已经证伪或高风险的路径重复试错。 +本文记录 2026-03-15 到 2026-03-17 这轮 dashboard 调试在后半段进入的异常状态,目标是给下一次接手排障的人一个明确起点,避免继续沿着已经证伪或高风险的路径重复试错。 ## 当前交接状态 -截至本次更新时,设备不再处于“完全卡死且无法 SSH”的状态,而是进入了一个更窄的失败场景: +截至本次更新时,设备已经不再停留在“完全卡死且无法 SSH”的状态,结论也分成了两部分: -- Kindle 已能回到主页 -- `ssh kindle` 已恢复可用 -- 从 `KUAL -> Kindle Dashboard` 进入 dashboard 时,仍会复现白屏 -- 白屏出现时,dashboard 本身往往没有真正接管成功,更像是 `framework/KUAL` 启动链在中途被打断 -- 当前最稳定的恢复路径,仍然是通过 SSH 执行 `./stop.sh` +- 已修住的: + - `calendar -> 主页 -> KUAL -> 回主页` 已能正常工作 + - `ssh kindle` 已恢复可用 + - 当前默认架构已切到“保留原生 UI 栈”的 overlay 模式 +- 仍未完全收口的: + - 从 `KUAL -> Kindle Dashboard` 进入 dashboard 时,仍出现过白屏 + - 白屏出现时,dashboard 本身往往没有真正接管成功,更像是 `framework/KUAL` 启动链在中途被打断 补充记录一个当前仍未修住、但边界已经比较清楚的问题: - 从 `KUAL` 进入 dashboard 后,再尝试回到 dashboard / KUAL 的原生 UI 路径,仍可能落入白屏 - 这个问题不应再继续归因到背景图、时钟或页面布局 - 当前更合理的判断,仍然是 `KUAL -> start.sh -> dash.sh` 的切换链路不稳定 +- `2026-03-17` 这轮新增验证还能确认:直接碰 `blanket` 或强拉 `booklet.home`,会进一步触发 `blanket / cvm` 崩溃 这份文档只记录当前交接结论,不再继续尝试修复。 @@ -37,9 +40,9 @@ Skipping system suspend, sleeping for 40s instead 所以“点 `Dashboard Debug On` 之后 3 秒就休眠”这个问题,本轮已经修住。 -### 2. `dashboard` 模式不是可交互界面 +### 2. 旧架构里,`dashboard` 不是可交互界面 -当前实现里,dashboard 启动后会主动停掉 Kindle 的前台 UI: +旧实现里,dashboard 启动后会主动停掉 Kindle 的前台 UI: - [dash/src/dash.sh](/Users/gavin/kindle-dash/dash/src/dash.sh#L50) 调用 `stop_framework` - [dash/src/dash.sh](/Users/gavin/kindle-dash/dash/src/dash.sh#L51) 停掉 `webreader` @@ -49,6 +52,12 @@ Skipping system suspend, sleeping for 40s instead - 进入 dashboard 之后,不应再期望当前屏幕仍然像普通 Kindle 页面那样可点击 - 也不应再期待“从 dashboard 直接返回刚才那个 KUAL 页面” +补充: + +- 截至 `2026-03-17`,默认实现已经改成 `KEEP_NATIVE_UI_STACK_RUNNING=true` +- 也就是保留 `framework / webreader` 存活,把 dashboard 当成覆盖显示层 +- 在这条新路径上,`calendar -> 主页 -> KUAL -> 回主页` 已完成实机验证 + ### 3. 顶栏遮罩不处理触摸 右上角状态栏遮罩逻辑在: @@ -60,45 +69,65 @@ Skipping system suspend, sleeping for 40s instead - “点不到 KUAL” 不是顶栏遮罩造成的 - 真正相关的是 `framework/webreader` 被停掉 -### 4. `stop.sh` 现在只负责恢复 UI 栈,不负责直接打开 KUAL +### 4. `stop.sh` 现在分成两种退出路径 当前 [dash/src/stop.sh](/Users/gavin/kindle-dash/dash/src/stop.sh) 已改成: -- 停掉 `dash.sh` -- 清掉 `preventScreenSaver` -- 启动 `framework` -- 启动 `webreader` +- 默认 overlay 模式: + - 停掉 `dash.sh` + - 清掉 `preventScreenSaver` + - 停掉主题菜单和右下角长按监听 + - 如果底层当前是 `KUAL`,再通过 `appmgrd stop` 把它正常退回首页 +- 旧架构兼容模式: + - 停掉 `dash.sh` + - 清掉 `preventScreenSaver` + - 停干净 `framework / webreader / cvm` + - 再按顺序拉起 `framework` + - 等 `cvm` 回来后启动 `webreader` 也就是说它的职责是: -- 让 Kindle 回到“应该可以恢复正常 UI”的状态 +- 让 Kindle 从 dashboard 安全退回原生 UI 不是: - 直接把 KUAL booklet 弹出来 +- 也不是走实验性的“快切换” + +补充: + +- 本轮新试过的“快路径”已经撤回 +- 直接碰 `blanket` 或尝试 shell 里强拉 `booklet.home`,都可能把 `blanket / cvm` 打崩 +- 当前 Voyage 5.13.6 仍以稳定恢复优先,快速切换需要改入口架构,不能继续堆在 `stop.sh` 上 补充一点:在白屏恢复过程中,`stop.sh` 已经比旧版稳定很多,但仍存在一种残留状态: - `framework` 和 `cvm` 已回来了 - `webreader` 可能还停在 `stop/waiting` -这时手工再执行一次 `start webreader`,主页通常就能回来。 +针对这个问题,`stop.sh` 现在已经补成“循环检查并补拉起 `webreader`,直到连续几次都保持 `start/running`”。 +如果未来仍然遇到个别恢复失败,再把 `/sbin/start webreader` 当作人工兜底,不再作为默认恢复步骤。 -### 5. 直接 `booklet run` 的试探命令不安全 +### 5. 直接 `booklet run` 或手动 `blanket unload` 的试探命令不安全 -本轮为了验证能否从 shell 里直接拉起主页或 KUAL,试过两类命令: +本轮为了验证能否从 shell 里直接拉起主页、KUAL,或者手动剥掉前台遮罩,试过几类命令: ```sh lipc-set-prop com.lab126.booklet run "app://com.lab126.booklet.home" lipc-set-prop com.lab126.booklet run "com.mobileread.ixtab.kindlelauncher.KualBooklet" +lipc-set-prop com.lab126.blanket unload splash +lipc-set-prop com.lab126.blanket unload screensaver ``` 这条路不稳定,已经触发过 `cvm` 崩溃打包。设备上看到过: - `/mnt/us/documents/cvm_2886_..._crash_Mar_15_14.14.19_2026.tgz` - `/mnt/us/documents/cvm_5551_..._crash_Mar_15_14.18.54_2026.tgz` +- `/mnt/us/documents/cvm_18914_..._crash_Mar_17_09.16.13_2026.tgz` +- `/mnt/us/documents/cvm_22294_..._crash_Mar_17_09.21.03_2026.tgz` +- `/mnt/us/documents/blanket_13032_..._crash_Mar_17_09.10.13_2026.tgz` -因此下次接手时,不要再直接复用这两条命令。 +因此下次接手时,不要再直接复用这些命令,也不要把“先手动卸掉 `blanket` 再看前台会不会回来”当成安全恢复手段。 ### 6. dashboard 本身可以工作,失败更像发生在 KUAL 启动路径 @@ -277,7 +306,7 @@ ssh kindle 'cd /mnt/us/dashboard && ./stop.sh' 如果执行完 `./stop.sh` 后主页仍然没有回来,再补: ```sh -ssh kindle 'start webreader' +ssh kindle '/sbin/start webreader' ``` 不要从 dashboard 页面直接尝试回 KUAL。 @@ -307,11 +336,45 @@ ssh kindle 'start webreader' 3. 继续保留 `ssh kindle 'cd /mnt/us/dashboard && DEBUG=true ./start.sh'` 作为唯一已验证稳定的进入方式 4. 继续保留 `./stop.sh` 作为唯一已验证稳定的退出恢复方式 +截至 2026-03-18,这个修复方向已经按最小改动落地到仓库: + +1. KUAL 顶层菜单改成 `Kindle Dashboard -> 主题列表` +2. 具体主题项调用 `/mnt/us/dashboard/launch-theme-from-kual.sh` +3. `launch-theme-from-kual.sh` 会先切主题,再委托 `/mnt/us/dashboard/launch-from-kual.sh` +4. `launch-from-kual.sh` 仍会先请求 KUAL 正常退出,并通过 `setsid` 脱离当前会话后启动 `start.sh` +5. 默认 `KUAL_QUIT_GRACE_SECONDS=0`、`KUAL_LAUNCH_DELAY_SECONDS=0`,不再人为停留在原生首页,尽量做到点击后直接进入 calendar overlay +6. `start.sh` 的非调试后台启动也额外加上了 `setsid` + +### E. 2026-03-17 新架构:保留原生 UI 栈 + +为解决“长期显示 calendar,同时仍能切回首页/KUAL”,仓库里新增并默认启用了: + +- `KEEP_NATIVE_UI_STACK_RUNNING=true` + +当前行为是: + +- `dash.sh` 启动时不再主动停止 `framework / webreader` +- `stop.sh` 在该开关打开时,只停止 dashboard 自己,不再重建 `framework / webreader / cvm` +- dashboard 作为“覆盖显示层 + RTC suspend 调度”运行,而不是“替代原生 UI 的前台” +- 主题切换入口当前只保留在 KUAL;运行态双翻页键菜单与右下角长按默认关闭 +- 实机已验证:`calendar -> 主页 -> KUAL -> 回主页` 全链路正常 + +这条路线已经证明比“启动时停掉 framework/webreader”更符合当前需求。 +7. `Dashboard Debug On/Off` 也统一改走同一条 wrapper 启动链 + +这只能说明“修复方向已经进入代码”,还不能替代真实设备上的交互验收。 +更具体地说,截至 `2026-03-17` 的真机实验结果是: + +1. wrapper 已经落地 +2. `stop.sh` 的实验性快切换已经撤回 +3. 当前真正安全的进入方式仍然只有 SSH 前台调试 +4. 当前真正安全的退出方式仍然只有保守恢复版 `./stop.sh` + 等 SSH 恢复后,再围绕下面三点做实机验证: 1. KUAL wrapper 是否还能触发 `framework` 被 TERM 2. `start.sh` 的后台脱离方式是否足够彻底 -3. `stop.sh` 后是否还需要补 `start webreader` +3. `stop.sh` 后是否还需要补 `/sbin/start webreader` ## 这轮涉及的关键文件 @@ -331,10 +394,10 @@ ssh kindle 'start webreader' - dashboard 与 Kindle 原生 `framework/KUAL` 的边界切换不稳定 - `KUAL -> Kindle Dashboard` 这条启动链仍会白屏 -- 直接用 shell 强拉 booklet 会触发前台 Java 崩溃 +- 直接用 shell 强拉 booklet,或手动碰 `blanket`,都会触发前台 `blanket / cvm` 崩溃 因此,当前最重要的不是继续调页面,而是: 1. 保留当前已经可用的 SSH 启动/停止路径 2. 修住 `KUAL -> Kindle Dashboard` 白屏 -3. 在不再触发 `cvm` 崩溃的前提下,把“进入 dashboard”和“退出 dashboard”都收敛成稳定流程 +3. 在不再触发 `blanket / cvm` 崩溃的前提下,把“进入 dashboard”和“退出 dashboard”都收敛成稳定流程 diff --git a/dash/docs/pagepress-theme-menu-plan.zh.md b/dash/docs/pagepress-theme-menu-plan.zh.md index ac2505c..5d6f430 100644 --- a/dash/docs/pagepress-theme-menu-plan.zh.md +++ b/dash/docs/pagepress-theme-menu-plan.zh.md @@ -4,6 +4,9 @@ 本文是评审方案,不是实机结论。 +补充说明:当前仓库默认已经不启用这套运行态菜单,主题切换入口只保留在 KUAL。 +本文保留为备选方案与历史设计记录,不代表当前默认交互。 + 当前 Kindle 已掉出 Wi-Fi,SSH 中断,因此这份文档的目标是: - 先把“左右同时按下翻页键,呼出主题选择页面”的方案固定下来 @@ -14,7 +17,15 @@ - Kindle Voyage - 固件 5.13.6 -- dashboard 运行时会停掉 framework / webreader +- 当前默认实现为“保留 framework / webreader 的 overlay 模式” + +补充: + +- 本文很多菜单设计最初建立在“dashboard 会停掉原生 UI 栈”的旧架构上 +- 截至 `2026-03-17`,默认实现已经切到“保留原生 UI 栈” +- 因此文中提到的 `Return Home`,应以 `stop.sh` 的当前行为为准: + - overlay 模式下:停掉 dashboard,并在底层是 KUAL 时把它正常退回首页 + - 旧架构兼容模式下:保守恢复 `framework / cvm / webreader` ## 1. 已确认事实 @@ -85,6 +96,7 @@ echo "mem" >/sys/power/state 3. 用户通过翻页键在主题列表中移动选中项 4. 用户再次同时按下左右翻页键,确认并切换主题 5. 切换完成后,立即刷新背景和时钟 +6. 菜单列表末尾可附带一个“返回首页”动作项,安全退出 dashboard 并恢复 Kindle 首页 本阶段非目标: @@ -113,7 +125,16 @@ echo "mem" >/sys/power/state - `PageUp`:向上移动 - `PageDown`:向下移动 -- 再次双键同时按下:确认当前主题 +- 再次双键同时按下:确认当前项 + +同时保留一个触摸兜底入口: + +- 右下角长按:直接呼出同一份运行态主题菜单 + +其中当前项可以是: + +- 某个主题:执行主题切换 +- `Return Home`:退出 dashboard,恢复 framework / webreader,回到 Kindle 首页 这样做的原因: @@ -221,12 +242,19 @@ Press both keys: apply 3. 识别到组合键后,本机绘制菜单 4. 用户确认后,调用现有: - `/mnt/us/dashboard/switch-theme.sh [orientation]` + - 或 `/mnt/us/dashboard/stop.sh` 5. `switch-theme.sh` 继续负责: - 同步主题配置 - 拉最新背景 - 刷新屏幕 - 重绘时钟 +如果选中的是 `Return Home`,则不走主题切换,而是调用 `stop.sh`: + +- 停掉 dashboard 和菜单监听器 +- 恢复 `framework / cvm / webreader` +- 回到 Kindle 首页 + 也就是说,菜单服务只负责: - 触发 @@ -309,6 +337,7 @@ Press both keys: apply export THEME_MENU_ENABLED=true export THEME_MENU_EVENT_DEVICE="/dev/input/event2" export THEME_MENU_COMBO_WINDOW_SECONDS=0.35 +export THEME_MENU_RUNTIME_DIR="/tmp/kindle-dash-theme-menu" ``` 说明: @@ -320,6 +349,9 @@ export THEME_MENU_COMBO_WINDOW_SECONDS=0.35 - 后续换机型时可以只改这个 - `THEME_MENU_COMBO_WINDOW_SECONDS` - 组合键判定窗口 +- `THEME_MENU_RUNTIME_DIR` + - 菜单服务的临时运行目录 + - 需要放在支持 FIFO 的文件系统上,Voyage 上应使用 `/tmp` ## 9. 风险与限制 @@ -408,8 +440,12 @@ export THEME_MENU_COMBO_WINDOW_SECONDS=0.35 - 监听 `/dev/input/event2` - 用短时间窗口识别 `PageUp + PageDown` 组合键 - 在运行态下绘制一个 KUAL 风格的文本主题菜单 +- 右下角长按也复用同一份菜单,而不是另起一套触摸 UI - 用 `PageUp / PageDown` 导航 - 再次双键确认 - 最终复用现有 `switch-theme.sh` 完成主题切换 这是当前成本最低、最容易恢复、也最适合在 SSH 不稳定阶段先落地评审的方案。 + +如果菜单需要提供稳定退出入口,也可以在列表末尾追加一个 `Return Home` 动作项。 +实现上应复用 `stop.sh` 的保守恢复链路,而不要直接强拉 `booklet.home`。 diff --git a/dash/src/dash.sh b/dash/src/dash.sh index 88f81fb..15e6d2e 100755 --- a/dash/src/dash.sh +++ b/dash/src/dash.sh @@ -13,6 +13,8 @@ THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env" THEME_SYNC_CMD="$DIR/local/theme-sync.sh" THEME_MENU_SERVICE_CMD="$DIR/local/theme-menu-service.sh" THEME_MENU_LOG_FILE="$DIR/logs/theme-menu.log" +TOUCH_HOME_SERVICE_CMD="$DIR/local/touch-home-service.sh" +TOUCH_HOME_LOG_FILE="$DIR/logs/touch-home.log" REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"} FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0} @@ -31,6 +33,7 @@ STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable +KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false} LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10} @@ -39,7 +42,7 @@ num_refresh=0 background_needs_redraw=true start_theme_menu_service() { - if [ "${THEME_MENU_ENABLED:-true}" != true ]; then + if [ "${THEME_MENU_ENABLED:-false}" != true ]; then return fi @@ -52,6 +55,20 @@ start_theme_menu_service() { nohup "$THEME_MENU_SERVICE_CMD" >>"$THEME_MENU_LOG_FILE" 2>&1 /dev/null || true + nohup "$TOUCH_HOME_SERVICE_CMD" >>"$TOUCH_HOME_LOG_FILE" 2>&1 /dev/null 2>&1 + if [ "$KEEP_NATIVE_UI_STACK_RUNNING" = true ]; then + echo "Keeping framework/webreader running for native UI overlay mode." + else + stop_framework + initctl stop webreader >/dev/null 2>&1 + fi + echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor lipc-set-prop com.lab126.powerd preventScreenSaver 1 start_theme_menu_service + start_touch_home_service } stop_framework() { diff --git a/dash/src/debug-off.sh b/dash/src/debug-off.sh index 305ecad..50c7a90 100755 --- a/dash/src/debug-off.sh +++ b/dash/src/debug-off.sh @@ -37,8 +37,10 @@ mv "$TMP_FILE" "$ENV_FILE" # 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, # 然后立刻拉起新的 dashboard,避免用户还要再次手动启动。 pkill -f "$DIR/dash.sh" 2>/dev/null || true +pkill -f "$DIR/start.sh" 2>/dev/null || true pkill -f "$DIR/local/theme-menu-service.sh" 2>/dev/null || true +pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true sleep 1 -"$DIR/start.sh" +"$DIR/launch-from-kual.sh" echo "已关闭 Dashboard 调试模式,并自动重启 Kindle Dashboard。" diff --git a/dash/src/debug-on.sh b/dash/src/debug-on.sh index c552f8e..33e70a9 100755 --- a/dash/src/debug-on.sh +++ b/dash/src/debug-on.sh @@ -37,8 +37,10 @@ mv "$TMP_FILE" "$ENV_FILE" # 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, # 然后立刻拉起新的 dashboard,避免用户还要在短时间内再点一次菜单。 pkill -f "$DIR/dash.sh" 2>/dev/null || true +pkill -f "$DIR/start.sh" 2>/dev/null || true pkill -f "$DIR/local/theme-menu-service.sh" 2>/dev/null || true +pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true sleep 1 -"$DIR/start.sh" +"$DIR/launch-from-kual.sh" echo "已开启 Dashboard 调试模式,并自动重启 Kindle Dashboard。" diff --git a/dash/src/launch-from-kual.sh b/dash/src/launch-from-kual.sh new file mode 100755 index 0000000..ba32ee5 --- /dev/null +++ b/dash/src/launch-from-kual.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(dirname "$0")" +LOG_FILE="$DIR/logs/kual-launch.log" +PID_FILE="$DIR/logs/kual-launch.pid" +# overlay 模式下,用户期望从 KUAL 点击后几乎直接进入 dashboard。 +# 这里仍然要求 KUAL 先正常退出,但默认不再额外人为等待, +# 只在需要回退到更保守的切换节奏时,再通过环境变量显式加回延迟。 +LAUNCH_DELAY_SECONDS="${KUAL_LAUNCH_DELAY_SECONDS:-0}" +KUAL_QUIT_GRACE_SECONDS="${KUAL_QUIT_GRACE_SECONDS:-0}" +KUAL_APP_ID="${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}" + +mkdir -p "$(dirname "$LOG_FILE")" + +case "$LAUNCH_DELAY_SECONDS" in + ''|*[!0-9]*) + LAUNCH_DELAY_SECONDS=0 + ;; +esac + +case "$KUAL_QUIT_GRACE_SECONDS" in + ''|*[!0-9]*) + KUAL_QUIT_GRACE_SECONDS=0 + ;; +esac + +if [ -f "$PID_FILE" ]; then + old_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "${old_pid:-}" ] && kill -0 "$old_pid" 2>/dev/null; then + kill "$old_pid" 2>/dev/null || true + sleep 1 + fi +fi + +# KUAL 直接同步执行 start.sh 时,framework/KUAL 正在切换的那一瞬间, +# 后台 dash 进程仍可能留在同一个 session 里被一起 TERM 掉。 +# 这里仍然先让独立 session 请求 KUAL 正常退出,但默认不再故意停留在原生首页, +# 而是把启动等待压到 0,尽量直接切到 dashboard。 +if command -v setsid >/dev/null 2>&1; then + nohup setsid /bin/sh -c ' + delay=$1 + target_dir=$2 + kual_app_id=$3 + quit_grace=$4 + + # 复用 KUAL 自己的 appmgrd stop 路径,尽量让它先干净退回原生 UI。 + if command -v lipc-set-prop >/dev/null 2>&1; then + lipc-set-prop com.lab126.appmgrd stop "$kual_app_id" >/dev/null 2>&1 || true + fi + + if [ "$quit_grace" -gt 0 ] 2>/dev/null; then + sleep "$quit_grace" + fi + + if [ "$delay" -gt 0 ] 2>/dev/null; then + sleep "$delay" + fi + + exec "$target_dir/start.sh" + ' sh "$LAUNCH_DELAY_SECONDS" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >>"$LOG_FILE" 2>&1 /dev/null 2>&1; then + lipc-set-prop com.lab126.appmgrd stop "$kual_app_id" >/dev/null 2>&1 || true + fi + + if [ "$quit_grace" -gt 0 ] 2>/dev/null; then + sleep "$quit_grace" + fi + + if [ "$delay" -gt 0 ] 2>/dev/null; then + sleep "$delay" + fi + + exec "$target_dir/start.sh" + ' sh "$LAUNCH_DELAY_SECONDS" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >>"$LOG_FILE" 2>&1 "$PID_FILE" +echo "$(date 2>/dev/null || true) requested KUAL quit, scheduled dashboard launch with quit_grace=${KUAL_QUIT_GRACE_SECONDS}s launch_delay=${LAUNCH_DELAY_SECONDS}s" >>"$LOG_FILE" diff --git a/dash/src/launch-theme-from-kual.sh b/dash/src/launch-theme-from-kual.sh new file mode 100644 index 0000000..c507ed6 --- /dev/null +++ b/dash/src/launch-theme-from-kual.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(dirname "$0")" +SWITCH_THEME_CMD="$DIR/switch-theme.sh" +LAUNCH_FROM_KUAL_CMD="$DIR/launch-from-kual.sh" + +requested_theme_id=${1:?"usage: launch-theme-from-kual.sh [orientation]"} +requested_orientation=${2:-} + +# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。 +# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑, +# 同时把“选主题”前移到进入 dashboard 之前。 +if [ -n "$requested_orientation" ]; then + "$SWITCH_THEME_CMD" "$requested_theme_id" "$requested_orientation" +else + "$SWITCH_THEME_CMD" "$requested_theme_id" +fi + +exec "$LAUNCH_FROM_KUAL_CMD" diff --git a/dash/src/local/env.sh b/dash/src/local/env.sh index a37ac42..d0e9d0c 100644 --- a/dash/src/local/env.sh +++ b/dash/src/local/env.sh @@ -59,11 +59,34 @@ export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} export STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} export STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} -# 左右翻页键同时按下时,呼出主题菜单; -# 菜单本身仍复用当前 dashboard 的运行方向,只切换 theme id。 -export THEME_MENU_ENABLED=${THEME_MENU_ENABLED:-true} +# 运行态主题菜单当前默认关闭,主题切换统一收口到 KUAL 入口。 +# 如需恢复双翻页键菜单,可临时改回 true 做专项验证。 +export THEME_MENU_ENABLED=${THEME_MENU_ENABLED:-false} export THEME_MENU_EVENT_DEVICE=${THEME_MENU_EVENT_DEVICE:-"/dev/input/event2"} export THEME_MENU_COMBO_WINDOW_SECONDS=${THEME_MENU_COMBO_WINDOW_SECONDS:-0.35} +# `/mnt/us` 是 FAT 分区,不能创建 FIFO;菜单监听器的临时管道必须放在 `/tmp`。 +export THEME_MENU_RUNTIME_DIR=${THEME_MENU_RUNTIME_DIR:-"/tmp/kindle-dash-theme-menu"} + +# 默认模式:保留 Kindle 原生 UI 栈,让 dashboard 只负责画面覆盖与休眠调度。 +# 这条路线已经在 Voyage 5.13.6 上实机验证通过: +# calendar -> 主页 -> KUAL -> 回主页 可正常工作。 +export KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-true} + +# 从 KUAL 进入 dashboard 时,默认不再刻意停留在原生首页。 +# 如果某台机器仍需要更保守的切换节奏,可把这两个值临时调大后再做回归验证。 +export KUAL_QUIT_GRACE_SECONDS=${KUAL_QUIT_GRACE_SECONDS:-0} +export KUAL_LAUNCH_DELAY_SECONDS=${KUAL_LAUNCH_DELAY_SECONDS:-0} + +# 右下角长按热区当前默认关闭,避免和底层原生桌面的触摸响应冲突。 +# 如需恢复这条实验入口,可临时改回 true,但当前默认只保留 KUAL 主题入口。 +export TOUCH_HOME_ENABLED=${TOUCH_HOME_ENABLED:-false} +export TOUCH_HOME_EVENT_DEVICE=${TOUCH_HOME_EVENT_DEVICE:-"/dev/input/event1"} +export TOUCH_HOME_RUNTIME_DIR=${TOUCH_HOME_RUNTIME_DIR:-"/tmp/kindle-dash-touch-home"} +export TOUCH_HOME_SCREEN_WIDTH=${TOUCH_HOME_SCREEN_WIDTH:-1072} +export TOUCH_HOME_SCREEN_HEIGHT=${TOUCH_HOME_SCREEN_HEIGHT:-1448} +export TOUCH_HOME_HOTSPOT_WIDTH_RATIO=${TOUCH_HOME_HOTSPOT_WIDTH_RATIO:-0.18} +export TOUCH_HOME_HOTSPOT_HEIGHT_RATIO=${TOUCH_HOME_HOTSPOT_HEIGHT_RATIO:-0.18} +export TOUCH_HOME_LONG_PRESS_SECONDS=${TOUCH_HOME_LONG_PRESS_SECONDS:-1.2} # By default, partial screen updates are used to update the screen, # to prevent the screen from flashing. After a few partial updates, diff --git a/dash/src/local/theme-menu-service.sh b/dash/src/local/theme-menu-service.sh index 7c2e891..4c0d603 100644 --- a/dash/src/local/theme-menu-service.sh +++ b/dash/src/local/theme-menu-service.sh @@ -8,11 +8,14 @@ THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" THEMES_INDEX_PATH="$DIR/../themes.json" THEME_JSON_LUA="$DIR/theme-json.lua" SWITCH_THEME_CMD="$DIR/../switch-theme.sh" +STOP_DASHBOARD_CMD="$DIR/../stop.sh" STATE_DIR="$DIR/state" MENU_ITEMS_FILE="$STATE_DIR/theme-menu-items.tsv" -ACTION_FIFO="$STATE_DIR/theme-menu-actions.fifo" +RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-theme-menu" EVENT_DEVICE_DEFAULT="/dev/input/event2" THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35" +HOME_MENU_ITEM_ID="__return_home__" +HOME_MENU_ITEM_LABEL="Return Home" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" @@ -23,12 +26,59 @@ THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35" EVENT_DEVICE=${THEME_MENU_EVENT_DEVICE:-$EVENT_DEVICE_DEFAULT} THEME_MENU_COMBO_WINDOW_SECONDS=${THEME_MENU_COMBO_WINDOW_SECONDS:-$THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT} +RUNTIME_DIR=${THEME_MENU_RUNTIME_DIR:-$RUNTIME_DIR_DEFAULT} +ACTION_FIFO="$RUNTIME_DIR/theme-menu-actions.fifo" +PID_FILE="$RUNTIME_DIR/theme-menu-service.pid" menu_open=false selected_index=1 current_theme_id=${THEME_ID:-default} current_orientation=${ORIENTATION:-portrait} stream_pid="" +owns_runtime_state=false + +log_event() { + # 调试阶段把状态机动作写进日志,便于确认按键是否真的被识别到。 + printf '%s theme-menu: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2 +} + +service_running() { + if [ ! -f "$PID_FILE" ]; then + return 1 + fi + + existing_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -z "$existing_pid" ]; then + return 1 + fi + + kill -0 "$existing_pid" 2>/dev/null +} + +send_action_to_service() { + requested_action=$1 + attempts=0 + + if ! service_running; then + printf 'theme-menu-service not running\n' >&2 + return 1 + fi + + # action fifo 由主监听循环持有;正常情况下会一直存在。 + # 这里保守重试几次,兼容监听器刚重启、fifo 正在重建的瞬间。 + while [ "$attempts" -lt 3 ]; do + if [ -p "$ACTION_FIFO" ]; then + printf '%s\n' "$requested_action" >"$ACTION_FIFO" + return 0 + fi + + attempts=$((attempts + 1)) + sleep 1 + done + + printf 'theme-menu-service action fifo not ready\n' >&2 + return 1 +} load_runtime() { # 每次打开菜单前都重新读取当前主题和方向,避免显示过期状态。 @@ -44,6 +94,8 @@ load_runtime() { load_menu_items() { mkdir -p "$STATE_DIR" lua "$THEME_JSON_LUA" list "$THEMES_INDEX_PATH" >"$MENU_ITEMS_FILE" + # 在主题列表末尾追加一个动作项,用于安全退出 dashboard 并恢复 Kindle 首页。 + printf '%s\t%s\n' "$HOME_MENU_ITEM_ID" "$HOME_MENU_ITEM_LABEL" >>"$MENU_ITEMS_FILE" } theme_count() { @@ -100,18 +152,25 @@ render_menu() { theme_label=$(theme_field "$index" 2) theme_id=$(theme_field "$index" 1) prefix=" " + line_text="" if [ "$index" -eq "$selected_index" ]; then prefix="> " fi - print_line 3 "$row" "${prefix}${theme_label} (${theme_id})" + if [ "$theme_id" = "$HOME_MENU_ITEM_ID" ]; then + line_text="${prefix}${theme_label}" + else + line_text="${prefix}${theme_label} (${theme_id})" + fi + + print_line 3 "$row" "$line_text" row=$((row + 2)) index=$((index + 1)) done print_line 3 18 "PageUp/PageDown: move" - print_line 3 20 "Press both keys: apply" + print_line 3 20 "Press both keys: select" } wrap_index() { @@ -141,9 +200,18 @@ apply_selection() { selected_theme_id=$(theme_field "$selected_index" 1) /usr/sbin/eips -c + if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then + log_event "apply return_home" + print_line 3 5 "Returning home..." + print_line 3 7 "Restoring framework/webreader" + menu_open=false + "$STOP_DASHBOARD_CMD" + return + fi + + log_event "apply theme=$selected_theme_id orientation=$current_orientation" print_line 3 5 "Applying theme..." print_line 3 7 "$selected_theme_id / $current_orientation" - "$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation" menu_open=false } @@ -153,13 +221,23 @@ open_menu() { load_menu_items selected_index=$(current_theme_index) menu_open=true + log_event "open_menu theme=$current_theme_id orientation=$current_orientation selected_index=$selected_index" render_menu } handle_action() { action=$1 + log_event "action=$action menu_open=$menu_open" case "$action" in + open_menu) + if [ "$menu_open" = true ]; then + # 触摸热区重复触发时,不直接确认当前选项,只重画当前菜单。 + render_menu + else + open_menu + fi + ;; combo) if [ "$menu_open" = true ]; then apply_selection @@ -247,11 +325,42 @@ cleanup() { kill "$stream_pid" 2>/dev/null || true fi + if [ "$owns_runtime_state" != true ]; then + return + fi + rm -f "$ACTION_FIFO" + rm -f "$PID_FILE" } +ensure_single_instance() { + if [ -f "$PID_FILE" ]; then + existing_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then + log_event "service_already_running pid=$existing_pid" + exit 0 + fi + fi + + printf '%s\n' "$$" >"$PID_FILE" + owns_runtime_state=true +} + +if [ "${1:-}" = "trigger" ]; then + if [ "$#" -ne 2 ]; then + printf 'usage: %s trigger \n' "$0" >&2 + exit 64 + fi + + send_action_to_service "$2" + exit $? +fi + trap cleanup EXIT INT TERM mkdir -p "$STATE_DIR" +mkdir -p "$RUNTIME_DIR" +ensure_single_instance +log_event "service_started event_device=$EVENT_DEVICE runtime_dir=$RUNTIME_DIR combo_window=$THEME_MENU_COMBO_WINDOW_SECONDS" while true; do rm -f "$ACTION_FIFO" diff --git a/dash/src/local/touch-home-service.sh b/dash/src/local/touch-home-service.sh new file mode 100644 index 0000000..6441518 --- /dev/null +++ b/dash/src/local/touch-home-service.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +ENV_FILE="$DIR/env.sh" +THEME_MENU_SERVICE_CMD="$DIR/theme-menu-service.sh" +STOP_DASHBOARD_CMD="$DIR/../stop.sh" +RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-touch-home" +TOUCH_DEVICE_DEFAULT="/dev/input/event1" +SCREEN_WIDTH_DEFAULT="1072" +SCREEN_HEIGHT_DEFAULT="1448" +HOTSPOT_WIDTH_RATIO_DEFAULT="0.18" +HOTSPOT_HEIGHT_RATIO_DEFAULT="0.18" +LONG_PRESS_SECONDS_DEFAULT="1.2" + +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" + +RUNTIME_DIR=${TOUCH_HOME_RUNTIME_DIR:-$RUNTIME_DIR_DEFAULT} +PID_FILE="$RUNTIME_DIR/touch-home-service.pid" +ACTION_FIFO="$RUNTIME_DIR/touch-home-actions.fifo" +TOUCH_DEVICE=${TOUCH_HOME_EVENT_DEVICE:-$TOUCH_DEVICE_DEFAULT} +SCREEN_WIDTH=${TOUCH_HOME_SCREEN_WIDTH:-$SCREEN_WIDTH_DEFAULT} +SCREEN_HEIGHT=${TOUCH_HOME_SCREEN_HEIGHT:-$SCREEN_HEIGHT_DEFAULT} +HOTSPOT_WIDTH_RATIO=${TOUCH_HOME_HOTSPOT_WIDTH_RATIO:-$HOTSPOT_WIDTH_RATIO_DEFAULT} +HOTSPOT_HEIGHT_RATIO=${TOUCH_HOME_HOTSPOT_HEIGHT_RATIO:-$HOTSPOT_HEIGHT_RATIO_DEFAULT} +LONG_PRESS_SECONDS=${TOUCH_HOME_LONG_PRESS_SECONDS:-$LONG_PRESS_SECONDS_DEFAULT} + +stream_pid="" +owns_runtime_state=false + +log_event() { + # 统一把触摸热区服务日志写到 stderr,交给上层 nohup 重定向到日志文件。 + printf '%s touch-home: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2 +} + +cleanup() { + if [ -n "$stream_pid" ]; then + kill "$stream_pid" 2>/dev/null || true + fi + + if [ "$owns_runtime_state" != true ]; then + return + fi + + rm -f "$ACTION_FIFO" + rm -f "$PID_FILE" +} + +ensure_single_instance() { + if [ -f "$PID_FILE" ]; then + existing_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then + log_event "service_already_running pid=$existing_pid" + exit 0 + fi + fi + + printf '%s\n' "$$" >"$PID_FILE" + owns_runtime_state=true +} + +trigger_theme_menu() { + log_event "trigger_theme_menu" + + # 右下角长按现在优先复用主题菜单服务,让用户先看到主题列表。 + # 如果菜单服务意外没起来,再回退到旧的 stop.sh 路径,避免设备失去退出入口。 + if "$THEME_MENU_SERVICE_CMD" trigger open_menu; then + return 0 + fi + + log_event "trigger_theme_menu_failed fallback_home" + "$STOP_DASHBOARD_CMD" +} + +event_stream() { + evtest --grab "$TOUCH_DEVICE" 2>/dev/null | awk \ + -v screen_width="$SCREEN_WIDTH" \ + -v screen_height="$SCREEN_HEIGHT" \ + -v hotspot_width_ratio="$HOTSPOT_WIDTH_RATIO" \ + -v hotspot_height_ratio="$HOTSPOT_HEIGHT_RATIO" \ + -v long_press_seconds="$LONG_PRESS_SECONDS" ' + BEGIN { + hotspot_min_x = int(screen_width * (1 - hotspot_width_ratio)) + hotspot_min_y = int(screen_height * (1 - hotspot_height_ratio)) + touch_active = 0 + touch_x = -1 + touch_y = -1 + touch_start_time = 0 + already_triggered = 0 + } + + function extract_time(line, matched) { + matched = match(line, /time [0-9]+\.[0-9]+/) + if (matched == 0) { + return 0 + } + + return substr(line, RSTART + 5, RLENGTH - 5) + 0 + } + + function emit(name) { + print name + fflush() + } + + function reset_touch() { + touch_active = 0 + touch_x = -1 + touch_y = -1 + touch_start_time = 0 + already_triggered = 0 + } + + { + current_time = extract_time($0) + + # Voyage 上实际输出是 "code 57 (MT Tracking ID)",但不同 evtest 版本 + # 也可能写成 ABS_MT_TRACKING_ID,这里同时兼容两种格式。 + if (($0 ~ /code 57 \(MT Tracking ID\)/ || $0 ~ /ABS_MT_TRACKING_ID/) && $0 ~ /value -1/) { + reset_touch() + next + } + + if ($0 ~ /code 57 \(MT Tracking ID\)/ || $0 ~ /ABS_MT_TRACKING_ID/) { + touch_active = 1 + touch_start_time = current_time + already_triggered = 0 + next + } + + if ($0 ~ /code 53 \(MT X\)/ || $0 ~ /ABS_MT_POSITION_X/) { + if (match($0, /value -?[0-9]+/)) { + touch_x = substr($0, RSTART + 6, RLENGTH - 6) + 0 + } + next + } + + if ($0 ~ /code 54 \(MT Y\)/ || $0 ~ /ABS_MT_POSITION_Y/) { + if (match($0, /value -?[0-9]+/)) { + touch_y = substr($0, RSTART + 6, RLENGTH - 6) + 0 + } + next + } + + if ($0 ~ /Report Sync/ || $0 ~ /SYN_REPORT/) { + if (touch_active && !already_triggered && touch_x >= hotspot_min_x && touch_y >= hotspot_min_y) { + if (current_time - touch_start_time >= long_press_seconds) { + already_triggered = 1 + emit("trigger_theme_menu") + } + } + } + } + ' +} + +trap cleanup EXIT INT TERM +mkdir -p "$RUNTIME_DIR" +ensure_single_instance +log_event "service_started touch_device=$TOUCH_DEVICE long_press_seconds=$LONG_PRESS_SECONDS hotspot_width_ratio=$HOTSPOT_WIDTH_RATIO hotspot_height_ratio=$HOTSPOT_HEIGHT_RATIO" + +while true; do + rm -f "$ACTION_FIFO" + mkfifo "$ACTION_FIFO" + + event_stream >"$ACTION_FIFO" & + stream_pid=$! + + while IFS= read -r action; do + case "$action" in + trigger_theme_menu) + trigger_theme_menu + ;; + esac + done <"$ACTION_FIFO" + + wait "$stream_pid" 2>/dev/null || true + stream_pid="" + rm -f "$ACTION_FIFO" + sleep 1 +done diff --git a/dash/src/start.sh b/dash/src/start.sh index 591fd67..51799fc 100755 --- a/dash/src/start.sh +++ b/dash/src/start.sh @@ -18,6 +18,11 @@ if [ "$DEBUG" = true ]; then "$DIR/dash.sh" else # 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。 - # 这里必须用 nohup 脱离会话,否则后台的 dash.sh 会跟着收到 HUP 退出。 - nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 /dev/null 2>&1; then + nohup setsid "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 >"$LOG_FILE" 2>&1 /dev/null 2>&1 + return $? + fi + + return 127 +} + +job_running() { + job_name=$1 + job_state=$( + if [ -x "$STATUS_BIN" ]; then + "$STATUS_BIN" "$job_name" 2>/dev/null || true + elif [ -x "$INITCTL_BIN" ]; then + "$INITCTL_BIN" status "$job_name" 2>/dev/null || true + else + true + fi + ) + + case "$job_state" in + *"start/running"*) + return 0 + ;; + esac + + return 1 +} stop_job() { job_name=$1 - stop "$job_name" >/dev/null 2>&1 || initctl stop "$job_name" >/dev/null 2>&1 || true + + if ! job_running "$job_name"; then + return 0 + fi + + run_job_cmd "$STOP_BIN" "$job_name" || run_job_cmd "$INITCTL_BIN" stop "$job_name" || true } start_job() { job_name=$1 - start "$job_name" >/dev/null 2>&1 || initctl start "$job_name" >/dev/null 2>&1 || true + + if job_running "$job_name"; then + return 0 + fi + + run_job_cmd "$START_BIN" "$job_name" || run_job_cmd "$INITCTL_BIN" start "$job_name" || true } wait_for_cvm() { @@ -30,28 +89,99 @@ wait_for_cvm() { return 1 } -pkill -f dash.sh 2>/dev/null || true -pkill -f start.sh 2>/dev/null || true -pkill -f theme-menu-service.sh 2>/dev/null || true +ensure_job_stable_running() { + job_name=$1 + max_attempts=$2 + stable_required=$3 + attempts=0 + stable_hits=0 -lipc-set-prop com.lab126.powerd preventScreenSaver 0 2>/dev/null || true + # Voyage 上 webreader 经常会出现“刚被拉起,1-2 秒后又掉回 stop/waiting”的状态。 + # 这里不再只发一次 start,而是循环观察一段时间,直到连续多次都看到 start/running, + # 才把恢复流程视为成功。 + while [ "$attempts" -lt "$max_attempts" ]; do + if job_running "$job_name"; then + stable_hits=$((stable_hits + 1)) + if [ "$stable_hits" -ge "$stable_required" ]; then + return 0 + fi + else + stable_hits=0 + start_job "$job_name" + fi -# 先把残留 UI 栈彻底停干净,避免 webreader 存活但 cvm 已崩的白屏状态。 -stop_job webreader -stop_job framework -killall cvm 2>/dev/null || true -sleep 2 + attempts=$((attempts + 1)) + sleep 1 + done -if [ -x /etc/init.d/framework ]; then - /etc/init.d/framework start || true -else - start_job framework + return 1 +} + +full_restore_ui() { + # 先把残留 UI 栈彻底停干净,避免 webreader 存活但 cvm 已崩的白屏状态。 + stop_job webreader + stop_job framework + killall cvm 2>/dev/null || true + sleep 2 + + if [ -x /etc/init.d/framework ]; then + /etc/init.d/framework start || true + else + start_job framework + fi + + # framework 拉起后,先等 cvm 真正起来,再启动 webreader。 + if ! wait_for_cvm; then + echo "警告:framework 已请求启动,但 cvm 未在预期时间内出现。" + fi + + if ! ensure_job_stable_running webreader 12 3; then + echo "警告:webreader 未能稳定恢复,可能仍需手工执行 /sbin/start webreader。" + fi +} + +stop_dashboard_processes() { + pkill -f dash.sh 2>/dev/null || true + pkill -f start.sh 2>/dev/null || true + pkill -f theme-menu-service.sh 2>/dev/null || true + pkill -f touch-home-service.sh 2>/dev/null || true + lipc-set-prop com.lab126.powerd preventScreenSaver 0 2>/dev/null || true +} + +native_active_app() { + if ! command -v lipc-get-prop >/dev/null 2>&1; then + return 0 + fi + + lipc-get-prop com.lab126.appmgrd activeApp 2>/dev/null || true +} + +return_to_home_from_kual() { + active_app=$(native_active_app) + + case "$active_app" in + com.mobileread.ixtab.kindlelauncher|app://com.mobileread.ixtab.kindlelauncher) + # 保留原生 UI 栈时,dashboard 底下通常仍是 KUAL。 + # 这里复用 launch-from-kual 已验证过的 appmgrd stop 路径,把 KUAL 正常退回首页。 + if command -v lipc-set-prop >/dev/null 2>&1; then + lipc-set-prop com.lab126.appmgrd stop "$KUAL_APP_ID" >/dev/null 2>&1 || true + sleep 1 + fi + ;; + esac +} + +soft_stop_for_native_ui_overlay() { + # 原生 UI 栈保持存活时,退出 dashboard 只需要停掉我们自己的覆盖进程, + # 不要再重建 framework/webreader/cvm,否则会重新引入旧架构的切换风险。 + stop_dashboard_processes + return_to_home_from_kual +} + +if [ "$KEEP_NATIVE_UI_STACK_RUNNING" = true ]; then + soft_stop_for_native_ui_overlay + exit 0 fi -# framework 拉起后,先等 cvm 真正起来,再启动 webreader。 -if ! wait_for_cvm; then - echo "警告:framework 已请求启动,但 cvm 未在预期时间内出现。" -fi - -start_job webreader -sleep 2 +stop_dashboard_processes +full_restore_ui diff --git a/dash/staging/device/dashboard/dash.sh b/dash/staging/device/dashboard/dash.sh deleted file mode 100755 index a1eaa09..0000000 --- a/dash/staging/device/dashboard/dash.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env sh -DEBUG=${DEBUG:-false} -[ "$DEBUG" = true ] && set -x - -DIR="$(dirname "$0")" -DASH_PNG="$DIR/dash.png" -FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh" -LOW_BATTERY_CMD="$DIR/local/low-battery.sh" - -REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"} -FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0} -SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600} -RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable - -LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} -LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10} - -num_refresh=0 - -init() { - if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then - echo "Missing required configuration." - echo "Timezone: ${TIMEZONE:-(not set)}." - echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}." - exit 1 - fi - - echo "Starting dashboard with $REFRESH_SCHEDULE refresh..." - - /etc/init.d/framework stop - initctl stop webreader >/dev/null 2>&1 - echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor - lipc-set-prop com.lab126.powerd preventScreenSaver 1 -} - -prepare_sleep() { - echo "Preparing sleep" - - /usr/sbin/eips -f -g "$DIR/sleeping.png" - - # Give screen time to refresh - sleep 2 - - # Ensure a full screen refresh is triggered after wake from sleep - num_refresh=$FULL_DISPLAY_REFRESH_RATE -} - -refresh_dashboard() { - echo "Refreshing dashboard" - "$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP" - - "$FETCH_DASHBOARD_CMD" "$DASH_PNG" - fetch_status=$? - - if [ "$fetch_status" -ne 0 ]; then - echo "Not updating screen, fetch-dashboard returned $fetch_status" - return 1 - fi - - if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then - num_refresh=0 - - # trigger a full refresh once in every 4 refreshes, to keep the screen clean - echo "Full screen refresh" - /usr/sbin/eips -f -g "$DASH_PNG" - else - echo "Partial screen refresh" - /usr/sbin/eips -g "$DASH_PNG" - fi - - num_refresh=$((num_refresh + 1)) -} - -log_battery_stats() { - battery_level=$(gasgauge-info -c) - echo "$(date) Battery level: $battery_level." - - if [ "$LOW_BATTERY_REPORTING" = true ]; then - battery_level_numeric=${battery_level%?} - if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then - "$LOW_BATTERY_CMD" "$battery_level_numeric" - fi - fi -} - -rtc_sleep() { - duration=$1 - - if [ "$DEBUG" = true ]; then - sleep "$duration" - else - # shellcheck disable=SC2039 - [ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC" - echo "mem" >/sys/power/state - fi -} - -main_loop() { - while true; do - log_battery_stats - - next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE") - - if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then - action="sleep" - prepare_sleep - else - action="suspend" - refresh_dashboard - fi - - # take a bit of time before going to sleep, so this process can be aborted - sleep 10 - - echo "Going to $action, next wakeup in ${next_wakeup_secs}s" - - rtc_sleep "$next_wakeup_secs" - done -} - -init -main_loop diff --git a/dash/staging/device/dashboard/debug-off.sh b/dash/staging/device/dashboard/debug-off.sh deleted file mode 100755 index 382e41c..0000000 --- a/dash/staging/device/dashboard/debug-off.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env sh - -# 持久关闭调试模式:修改 env.sh,恢复正常的省电挂起行为。 -set -eu - -DIR="$(dirname "$0")" -ENV_FILE="$DIR/local/env.sh" -TMP_FILE="$DIR/local/env.sh.tmp" - -if [ ! -f "$ENV_FILE" ]; then - echo "未找到配置文件:$ENV_FILE" - exit 1 -fi - -# 只替换目标配置,避免重复追加同一环境变量。 -awk ' -BEGIN { - updated = 0 -} -/^export DISABLE_SYSTEM_SUSPEND=/ { - print "export DISABLE_SYSTEM_SUSPEND=false" - updated = 1 - next -} -{ - print -} -END { - if (!updated) { - print "export DISABLE_SYSTEM_SUSPEND=false" - } -} -' "$ENV_FILE" > "$TMP_FILE" - -mv "$TMP_FILE" "$ENV_FILE" - -# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, -# 避免旧进程继续按旧配置运行。 -pkill -f "$DIR/dash.sh" 2>/dev/null || true - -echo "已关闭 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。" diff --git a/dash/staging/device/dashboard/debug-on.sh b/dash/staging/device/dashboard/debug-on.sh deleted file mode 100755 index 9b6b26b..0000000 --- a/dash/staging/device/dashboard/debug-on.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env sh - -# 持久开启调试模式:修改 env.sh,让后续普通启动也不进入系统挂起。 -set -eu - -DIR="$(dirname "$0")" -ENV_FILE="$DIR/local/env.sh" -TMP_FILE="$DIR/local/env.sh.tmp" - -if [ ! -f "$ENV_FILE" ]; then - echo "未找到配置文件:$ENV_FILE" - exit 1 -fi - -# 只替换目标配置,避免重复追加同一环境变量。 -awk ' -BEGIN { - updated = 0 -} -/^export DISABLE_SYSTEM_SUSPEND=/ { - print "export DISABLE_SYSTEM_SUSPEND=true" - updated = 1 - next -} -{ - print -} -END { - if (!updated) { - print "export DISABLE_SYSTEM_SUSPEND=true" - } -} -' "$ENV_FILE" > "$TMP_FILE" - -mv "$TMP_FILE" "$ENV_FILE" - -# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, -# 避免旧进程继续按旧配置进入系统挂起。 -pkill -f "$DIR/dash.sh" 2>/dev/null || true - -echo "已开启 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。" diff --git a/dash/staging/device/dashboard/local/env.sh b/dash/staging/device/dashboard/local/env.sh deleted file mode 100644 index 2e74d96..0000000 --- a/dash/staging/device/dashboard/local/env.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env sh - -# Export environment variables here -export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1} -# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。 -export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"} -export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"} - -# By default, partial screen updates are used to update the screen, -# to prevent the screen from flashing. After a few partial updates, -# the screen will start to look a bit distorted (due to e-ink ghosting). -# 测试阶段强制每次都做一次全刷,避免首页残影和局部刷新的旧内容干扰验证。 -# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。 -export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0} - -# When the time until the next wakeup is greater or equal to this number, -# the dashboard will not be refreshed anymore, but instead show a -# 'kindle is sleeping' screen. This can be useful if your schedule only runs -# during the day, for example. -export SLEEP_SCREEN_INTERVAL=3600 - -export LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} -export LOW_BATTERY_THRESHOLD_PERCENT=10 diff --git a/dash/staging/device/dashboard/local/fetch-dashboard.sh b/dash/staging/device/dashboard/local/fetch-dashboard.sh deleted file mode 100755 index 0733233..0000000 --- a/dash/staging/device/dashboard/local/fetch-dashboard.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -# Fetch a new dashboard image, make sure to output it to "$1". -# For example: -"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png diff --git a/dash/staging/device/dashboard/local/low-battery.sh b/dash/staging/device/dashboard/local/low-battery.sh deleted file mode 100644 index dc70ced..0000000 --- a/dash/staging/device/dashboard/local/low-battery.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env sh -battery_level_percentage=$1 -last_battery_report_state="$(dirname "$0")/state/last_battery_report" - -previous_report_timestamp=$(cat "$last_battery_report_state" 2>/dev/null || echo '-1') -now=$(date +%s) - -# Implement desired logic here. The example below for example only reports low -# battery every 24 hours. - -if [ "$previous_report_timestamp" -eq -1 ] || - [ $((now - previous_report_timestamp)) -gt 86400 ]; then - # Replace this with for example an HTTP call via curl, or xh - echo "Reporting low battery: $battery_level_percentage%" - - echo "$now" >"$last_battery_report_state" -fi diff --git a/dash/staging/device/dashboard/next-wakeup b/dash/staging/device/dashboard/next-wakeup deleted file mode 100755 index 382d28d..0000000 Binary files a/dash/staging/device/dashboard/next-wakeup and /dev/null differ diff --git a/dash/staging/device/dashboard/sleeping.png b/dash/staging/device/dashboard/sleeping.png deleted file mode 100644 index 5da94ac..0000000 Binary files a/dash/staging/device/dashboard/sleeping.png and /dev/null differ diff --git a/dash/staging/device/dashboard/start-debug.sh b/dash/staging/device/dashboard/start-debug.sh deleted file mode 100755 index e2bd070..0000000 --- a/dash/staging/device/dashboard/start-debug.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env sh - -# 调试启动脚本:强制关闭系统挂起,便于在 Kindle 上持续观察刷新效果。 -DIR="$(dirname "$0")" - -export DISABLE_SYSTEM_SUSPEND=true - -exec "$DIR/start.sh" diff --git a/dash/staging/device/dashboard/start.sh b/dash/staging/device/dashboard/start.sh deleted file mode 100755 index f4f38c1..0000000 --- a/dash/staging/device/dashboard/start.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env sh -DEBUG=${DEBUG:-false} -[ "$DEBUG" = true ] && set -x - -DIR="$(dirname "$0")" -ENV_FILE="$DIR/local/env.sh" -LOG_FILE="$DIR/logs/dash.log" - -mkdir -p "$(dirname "$LOG_FILE")" - -# shellcheck disable=SC1090 -[ -f "$ENV_FILE" ] && . "$ENV_FILE" - -if [ "$DEBUG" = true ]; then - "$DIR/dash.sh" -else - "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 & -fi diff --git a/dash/staging/device/dashboard/stop.sh b/dash/staging/device/dashboard/stop.sh deleted file mode 100755 index ecc884e..0000000 --- a/dash/staging/device/dashboard/stop.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -pkill -f dash.sh diff --git a/dash/staging/device/dashboard/wait-for-wifi.sh b/dash/staging/device/dashboard/wait-for-wifi.sh deleted file mode 100755 index 7785fc6..0000000 --- a/dash/staging/device/dashboard/wait-for-wifi.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env sh -test_ip=$1 - -if [ -z "$test_ip" ]; then - echo "No test ip specified" - exit 1 -fi - -wait_for_wifi() { - max_retry=30 - counter=0 - - ping -c 1 "$test_ip" >/dev/null 2>&1 - - # shellcheck disable=SC2181 - while [ $? -ne 0 ]; do - [ $counter -eq $max_retry ] && echo "Couldn't connect to Wi-Fi" && exit 1 - counter=$((counter + 1)) - - sleep 1 - ping -c 1 "$test_ip" >/dev/null 2>&1 - done -} - -wait_for_wifi -echo "Wi-Fi connected" diff --git a/dash/staging/device/dashboard/xh b/dash/staging/device/dashboard/xh deleted file mode 100755 index b52ba80..0000000 Binary files a/dash/staging/device/dashboard/xh and /dev/null differ diff --git a/dash/staging/device/extensions/kindle-dash/config.xml b/dash/staging/device/extensions/kindle-dash/config.xml deleted file mode 100644 index da580b2..0000000 --- a/dash/staging/device/extensions/kindle-dash/config.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - Kindle dashboard - pascalw-kindle-dash - - - menu.json - - \ No newline at end of file diff --git a/dash/staging/device/extensions/kindle-dash/menu.json b/dash/staging/device/extensions/kindle-dash/menu.json deleted file mode 100644 index abaf0bc..0000000 --- a/dash/staging/device/extensions/kindle-dash/menu.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "items": [ - {"name": "Kindle Dashboard", "action": "/mnt/us/dashboard/start.sh"}, - {"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"}, - {"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"} - ] -} diff --git a/dash/staging/kindle-dash-release/dash.sh b/dash/staging/kindle-dash-release/dash.sh deleted file mode 100755 index a1eaa09..0000000 --- a/dash/staging/kindle-dash-release/dash.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env sh -DEBUG=${DEBUG:-false} -[ "$DEBUG" = true ] && set -x - -DIR="$(dirname "$0")" -DASH_PNG="$DIR/dash.png" -FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh" -LOW_BATTERY_CMD="$DIR/local/low-battery.sh" - -REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"} -FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0} -SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600} -RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable - -LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} -LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10} - -num_refresh=0 - -init() { - if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then - echo "Missing required configuration." - echo "Timezone: ${TIMEZONE:-(not set)}." - echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}." - exit 1 - fi - - echo "Starting dashboard with $REFRESH_SCHEDULE refresh..." - - /etc/init.d/framework stop - initctl stop webreader >/dev/null 2>&1 - echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor - lipc-set-prop com.lab126.powerd preventScreenSaver 1 -} - -prepare_sleep() { - echo "Preparing sleep" - - /usr/sbin/eips -f -g "$DIR/sleeping.png" - - # Give screen time to refresh - sleep 2 - - # Ensure a full screen refresh is triggered after wake from sleep - num_refresh=$FULL_DISPLAY_REFRESH_RATE -} - -refresh_dashboard() { - echo "Refreshing dashboard" - "$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP" - - "$FETCH_DASHBOARD_CMD" "$DASH_PNG" - fetch_status=$? - - if [ "$fetch_status" -ne 0 ]; then - echo "Not updating screen, fetch-dashboard returned $fetch_status" - return 1 - fi - - if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then - num_refresh=0 - - # trigger a full refresh once in every 4 refreshes, to keep the screen clean - echo "Full screen refresh" - /usr/sbin/eips -f -g "$DASH_PNG" - else - echo "Partial screen refresh" - /usr/sbin/eips -g "$DASH_PNG" - fi - - num_refresh=$((num_refresh + 1)) -} - -log_battery_stats() { - battery_level=$(gasgauge-info -c) - echo "$(date) Battery level: $battery_level." - - if [ "$LOW_BATTERY_REPORTING" = true ]; then - battery_level_numeric=${battery_level%?} - if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then - "$LOW_BATTERY_CMD" "$battery_level_numeric" - fi - fi -} - -rtc_sleep() { - duration=$1 - - if [ "$DEBUG" = true ]; then - sleep "$duration" - else - # shellcheck disable=SC2039 - [ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC" - echo "mem" >/sys/power/state - fi -} - -main_loop() { - while true; do - log_battery_stats - - next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE") - - if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then - action="sleep" - prepare_sleep - else - action="suspend" - refresh_dashboard - fi - - # take a bit of time before going to sleep, so this process can be aborted - sleep 10 - - echo "Going to $action, next wakeup in ${next_wakeup_secs}s" - - rtc_sleep "$next_wakeup_secs" - done -} - -init -main_loop diff --git a/dash/staging/kindle-dash-release/debug-off.sh b/dash/staging/kindle-dash-release/debug-off.sh deleted file mode 100755 index 382e41c..0000000 --- a/dash/staging/kindle-dash-release/debug-off.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env sh - -# 持久关闭调试模式:修改 env.sh,恢复正常的省电挂起行为。 -set -eu - -DIR="$(dirname "$0")" -ENV_FILE="$DIR/local/env.sh" -TMP_FILE="$DIR/local/env.sh.tmp" - -if [ ! -f "$ENV_FILE" ]; then - echo "未找到配置文件:$ENV_FILE" - exit 1 -fi - -# 只替换目标配置,避免重复追加同一环境变量。 -awk ' -BEGIN { - updated = 0 -} -/^export DISABLE_SYSTEM_SUSPEND=/ { - print "export DISABLE_SYSTEM_SUSPEND=false" - updated = 1 - next -} -{ - print -} -END { - if (!updated) { - print "export DISABLE_SYSTEM_SUSPEND=false" - } -} -' "$ENV_FILE" > "$TMP_FILE" - -mv "$TMP_FILE" "$ENV_FILE" - -# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, -# 避免旧进程继续按旧配置运行。 -pkill -f "$DIR/dash.sh" 2>/dev/null || true - -echo "已关闭 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。" diff --git a/dash/staging/kindle-dash-release/debug-on.sh b/dash/staging/kindle-dash-release/debug-on.sh deleted file mode 100755 index 9b6b26b..0000000 --- a/dash/staging/kindle-dash-release/debug-on.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env sh - -# 持久开启调试模式:修改 env.sh,让后续普通启动也不进入系统挂起。 -set -eu - -DIR="$(dirname "$0")" -ENV_FILE="$DIR/local/env.sh" -TMP_FILE="$DIR/local/env.sh.tmp" - -if [ ! -f "$ENV_FILE" ]; then - echo "未找到配置文件:$ENV_FILE" - exit 1 -fi - -# 只替换目标配置,避免重复追加同一环境变量。 -awk ' -BEGIN { - updated = 0 -} -/^export DISABLE_SYSTEM_SUSPEND=/ { - print "export DISABLE_SYSTEM_SUSPEND=true" - updated = 1 - next -} -{ - print -} -END { - if (!updated) { - print "export DISABLE_SYSTEM_SUSPEND=true" - } -} -' "$ENV_FILE" > "$TMP_FILE" - -mv "$TMP_FILE" "$ENV_FILE" - -# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, -# 避免旧进程继续按旧配置进入系统挂起。 -pkill -f "$DIR/dash.sh" 2>/dev/null || true - -echo "已开启 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。" diff --git a/dash/staging/kindle-dash-release/local/env.sh b/dash/staging/kindle-dash-release/local/env.sh deleted file mode 100644 index 2e74d96..0000000 --- a/dash/staging/kindle-dash-release/local/env.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env sh - -# Export environment variables here -export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1} -# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。 -export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"} -export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"} - -# By default, partial screen updates are used to update the screen, -# to prevent the screen from flashing. After a few partial updates, -# the screen will start to look a bit distorted (due to e-ink ghosting). -# 测试阶段强制每次都做一次全刷,避免首页残影和局部刷新的旧内容干扰验证。 -# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。 -export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0} - -# When the time until the next wakeup is greater or equal to this number, -# the dashboard will not be refreshed anymore, but instead show a -# 'kindle is sleeping' screen. This can be useful if your schedule only runs -# during the day, for example. -export SLEEP_SCREEN_INTERVAL=3600 - -export LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} -export LOW_BATTERY_THRESHOLD_PERCENT=10 diff --git a/dash/staging/kindle-dash-release/local/fetch-dashboard.sh b/dash/staging/kindle-dash-release/local/fetch-dashboard.sh deleted file mode 100755 index 0733233..0000000 --- a/dash/staging/kindle-dash-release/local/fetch-dashboard.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -# Fetch a new dashboard image, make sure to output it to "$1". -# For example: -"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png diff --git a/dash/staging/kindle-dash-release/local/low-battery.sh b/dash/staging/kindle-dash-release/local/low-battery.sh deleted file mode 100644 index dc70ced..0000000 --- a/dash/staging/kindle-dash-release/local/low-battery.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env sh -battery_level_percentage=$1 -last_battery_report_state="$(dirname "$0")/state/last_battery_report" - -previous_report_timestamp=$(cat "$last_battery_report_state" 2>/dev/null || echo '-1') -now=$(date +%s) - -# Implement desired logic here. The example below for example only reports low -# battery every 24 hours. - -if [ "$previous_report_timestamp" -eq -1 ] || - [ $((now - previous_report_timestamp)) -gt 86400 ]; then - # Replace this with for example an HTTP call via curl, or xh - echo "Reporting low battery: $battery_level_percentage%" - - echo "$now" >"$last_battery_report_state" -fi diff --git a/dash/staging/kindle-dash-release/next-wakeup b/dash/staging/kindle-dash-release/next-wakeup deleted file mode 100755 index 382d28d..0000000 Binary files a/dash/staging/kindle-dash-release/next-wakeup and /dev/null differ diff --git a/dash/staging/kindle-dash-release/sleeping.png b/dash/staging/kindle-dash-release/sleeping.png deleted file mode 100644 index 5da94ac..0000000 Binary files a/dash/staging/kindle-dash-release/sleeping.png and /dev/null differ diff --git a/dash/staging/kindle-dash-release/start-debug.sh b/dash/staging/kindle-dash-release/start-debug.sh deleted file mode 100755 index e2bd070..0000000 --- a/dash/staging/kindle-dash-release/start-debug.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env sh - -# 调试启动脚本:强制关闭系统挂起,便于在 Kindle 上持续观察刷新效果。 -DIR="$(dirname "$0")" - -export DISABLE_SYSTEM_SUSPEND=true - -exec "$DIR/start.sh" diff --git a/dash/staging/kindle-dash-release/start.sh b/dash/staging/kindle-dash-release/start.sh deleted file mode 100755 index f4f38c1..0000000 --- a/dash/staging/kindle-dash-release/start.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env sh -DEBUG=${DEBUG:-false} -[ "$DEBUG" = true ] && set -x - -DIR="$(dirname "$0")" -ENV_FILE="$DIR/local/env.sh" -LOG_FILE="$DIR/logs/dash.log" - -mkdir -p "$(dirname "$LOG_FILE")" - -# shellcheck disable=SC1090 -[ -f "$ENV_FILE" ] && . "$ENV_FILE" - -if [ "$DEBUG" = true ]; then - "$DIR/dash.sh" -else - "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 & -fi diff --git a/dash/staging/kindle-dash-release/stop.sh b/dash/staging/kindle-dash-release/stop.sh deleted file mode 100755 index ecc884e..0000000 --- a/dash/staging/kindle-dash-release/stop.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -pkill -f dash.sh diff --git a/dash/staging/kindle-dash-release/wait-for-wifi.sh b/dash/staging/kindle-dash-release/wait-for-wifi.sh deleted file mode 100755 index 7785fc6..0000000 --- a/dash/staging/kindle-dash-release/wait-for-wifi.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env sh -test_ip=$1 - -if [ -z "$test_ip" ]; then - echo "No test ip specified" - exit 1 -fi - -wait_for_wifi() { - max_retry=30 - counter=0 - - ping -c 1 "$test_ip" >/dev/null 2>&1 - - # shellcheck disable=SC2181 - while [ $? -ne 0 ]; do - [ $counter -eq $max_retry ] && echo "Couldn't connect to Wi-Fi" && exit 1 - counter=$((counter + 1)) - - sleep 1 - ping -c 1 "$test_ip" >/dev/null 2>&1 - done -} - -wait_for_wifi -echo "Wi-Fi connected" diff --git a/dash/staging/kindle-dash-release/xh b/dash/staging/kindle-dash-release/xh deleted file mode 100755 index b52ba80..0000000 Binary files a/dash/staging/kindle-dash-release/xh and /dev/null differ diff --git a/dash/staging/kterm/README.md b/dash/staging/kterm/README.md new file mode 100644 index 0000000..7b803c8 --- /dev/null +++ b/dash/staging/kterm/README.md @@ -0,0 +1,31 @@ +# KTerm 安装包放置说明 + +如果你要让 `bootstrap-new-kindle.sh` 在 `prepare-storage` 阶段一并预置 `KTerm`,请把官方 `KTerm` release 的安装包 `.zip` 放到这个目录。 + +例如: + +```text +dash/staging/kterm/kterm-kindle-*.zip +``` + +脚本会自动尝试拾取这个目录下的第一个 `.zip` 文件,并直接解压到 Kindle 的 `extensions/`。 + +如果你不想放在仓库里,也可以执行时显式指定: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --kterm-package /绝对路径/kterm-kindle-*.zip +``` + +如果你希望脚本直接在 Mac 侧联网下载,也可以: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest +``` + +或固定版本: + +```sh +sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version v2.6 +``` + +下载后的 `.zip` 也会缓存到这个目录里,后续再次执行时可直接复用。 diff --git a/dash/staging/post-jailbreak-root/dashboard/dash.sh b/dash/staging/post-jailbreak-root/dashboard/dash.sh index a1eaa09..15e6d2e 100755 --- a/dash/staging/post-jailbreak-root/dashboard/dash.sh +++ b/dash/staging/post-jailbreak-root/dashboard/dash.sh @@ -3,19 +3,78 @@ DEBUG=${DEBUG:-false} [ "$DEBUG" = true ] && set -x DIR="$(dirname "$0")" -DASH_PNG="$DIR/dash.png" +BACKGROUND_PNG="$DIR/kindlebg.png" FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh" LOW_BATTERY_CMD="$DIR/local/low-battery.sh" +CLOCK_RENDER_CMD="$DIR/local/render-clock.sh" +STATE_DIR="$DIR/local/state" +BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at" +THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env" +THEME_SYNC_CMD="$DIR/local/theme-sync.sh" +THEME_MENU_SERVICE_CMD="$DIR/local/theme-menu-service.sh" +THEME_MENU_LOG_FILE="$DIR/logs/theme-menu.log" +TOUCH_HOME_SERVICE_CMD="$DIR/local/touch-home-service.sh" +TOUCH_HOME_LOG_FILE="$DIR/logs/touch-home.log" REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"} FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0} SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600} +DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false} +BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120} +CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15} +PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10} +MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60} +MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5} +STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true} +STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700} +STATUS_MASK_TOP=${STATUS_MASK_TOP:-0} +STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372} +STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} +STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} +STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable +KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false} LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10} num_refresh=0 +background_needs_redraw=true + +start_theme_menu_service() { + if [ "${THEME_MENU_ENABLED:-false}" != true ]; then + return + fi + + if [ ! -x "$THEME_MENU_SERVICE_CMD" ]; then + return + fi + + mkdir -p "$(dirname "$THEME_MENU_LOG_FILE")" + pkill -f "$THEME_MENU_SERVICE_CMD" 2>/dev/null || true + nohup "$THEME_MENU_SERVICE_CMD" >>"$THEME_MENU_LOG_FILE" 2>&1 /dev/null || true + nohup "$TOUCH_HOME_SERVICE_CMD" >>"$TOUCH_HOME_LOG_FILE" 2>&1 /dev/null 2>&1 + fi - /etc/init.d/framework stop - initctl stop webreader >/dev/null 2>&1 echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor lipc-set-prop com.lab126.powerd preventScreenSaver 1 + start_theme_menu_service + start_touch_home_service +} + +stop_framework() { + # 不同 Kindle 固件停止 framework 的入口不完全一致。 + # Voyage 5.13.6 上没有 /etc/init.d/framework,需要走 upstart 入口。 + if [ -x /etc/init.d/framework ]; then + /etc/init.d/framework stop || true + return + fi + + stop framework >/dev/null 2>&1 || initctl stop framework >/dev/null 2>&1 || true } prepare_sleep() { echo "Preparing sleep" /usr/sbin/eips -f -g "$DIR/sleeping.png" + background_needs_redraw=true # Give screen time to refresh sleep 2 @@ -45,41 +128,151 @@ prepare_sleep() { num_refresh=$FULL_DISPLAY_REFRESH_RATE } -refresh_dashboard() { - echo "Refreshing dashboard" +now_epoch() { + date '+%s' +} + +background_refresh_due() { + load_theme_runtime_config + + if [ ! -f "$BACKGROUND_PNG" ] || [ ! -f "$BACKGROUND_TIMESTAMP_FILE" ]; then + return 0 + fi + + current_epoch=$(now_epoch) + last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE") + refresh_interval_seconds=$((BACKGROUND_REFRESH_INTERVAL_MINUTES * 60)) + + [ $((current_epoch - last_background_epoch)) -ge "$refresh_interval_seconds" ] +} + +store_background_timestamp() { + now_epoch >"$BACKGROUND_TIMESTAMP_FILE" +} + +fetch_background() { + echo "Refreshing background" "$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP" - "$FETCH_DASHBOARD_CMD" "$DASH_PNG" + if "$THEME_SYNC_CMD" >/dev/null; then + load_theme_runtime_config + elif [ ! -f "$THEME_RUNTIME_ENV_FILE" ]; then + echo "Theme sync failed and no runtime theme config is available." + return 1 + else + echo "Theme sync failed, using cached runtime theme config." + load_theme_runtime_config + fi + + "$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG" fetch_status=$? if [ "$fetch_status" -ne 0 ]; then - echo "Not updating screen, fetch-dashboard returned $fetch_status" + echo "Background fetch failed with $fetch_status" return 1 fi - if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then - num_refresh=0 + store_background_timestamp + return 0 +} - # trigger a full refresh once in every 4 refreshes, to keep the screen clean - echo "Full screen refresh" - /usr/sbin/eips -f -g "$DASH_PNG" - else - echo "Partial screen refresh" - /usr/sbin/eips -g "$DASH_PNG" +clock_force_full_refresh() { + eval "$("$DIR/local/clock-index.sh")" + [ $((minute % CLOCK_FULL_REFRESH_INTERVAL_MINUTES)) -eq 0 ] +} + +mask_system_status_overlay() { + if [ "$STATUS_MASK_ENABLED" != true ]; then + return fi + # Voyage 上 framework/appmgrd 偶尔会把右上角时间与状态图标重新画回屏幕。 + # 这里在每次 dashboard 刷新后,用顶部空白带的白底把它盖掉。 + # 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。 + pass=1 + while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do + fbink -q -V -B WHITE -k \ + "top=$STATUS_MASK_TOP,left=$STATUS_MASK_LEFT,width=$STATUS_MASK_WIDTH,height=$STATUS_MASK_HEIGHT" + + if [ "$pass" -lt "$STATUS_MASK_PASSES" ] && [ "$STATUS_MASK_DELAY_SECONDS" -gt 0 ]; then + sleep "$STATUS_MASK_DELAY_SECONDS" + fi + + pass=$((pass + 1)) + done +} + +refresh_dashboard() { + background_refreshed=false + + if background_refresh_due; then + if fetch_background; then + background_refreshed=true + echo "Full screen refresh" + /usr/sbin/eips -f -g "$BACKGROUND_PNG" + elif [ ! -f "$BACKGROUND_PNG" ]; then + echo "No cached background available." + return 1 + fi + fi + + if [ "$background_refreshed" = false ] && [ ! -f "$BACKGROUND_PNG" ]; then + echo "No cached background available." + return 1 + fi + + if [ "$background_refreshed" = false ] && [ "$background_needs_redraw" = true ]; then + echo "Restoring cached background" + /usr/sbin/eips -f -g "$BACKGROUND_PNG" + background_needs_redraw=false + fi + + if [ "$background_refreshed" = true ]; then + background_needs_redraw=false + fi + + if [ "$background_refreshed" = true ] || clock_force_full_refresh; then + echo "Clock patch full refresh" + "$CLOCK_RENDER_CMD" true + else + echo "Clock patch partial refresh" + "$CLOCK_RENDER_CMD" false + fi + + mask_system_status_overlay + num_refresh=$((num_refresh + 1)) } +powerd_get_prop() { + prop_name=$1 + + if ! command -v lipc-get-prop >/dev/null 2>&1; then + echo "unavailable" + return 0 + fi + + lipc-get-prop com.lab126.powerd "$prop_name" 2>/dev/null || echo "unavailable" +} + log_battery_stats() { - battery_level=$(gasgauge-info -c) - echo "$(date) Battery level: $battery_level." + battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown") + charging_state=$(powerd_get_prop isCharging) + battery_state_info=$(powerd_get_prop battStateInfo) + + # 同时记录 powerd 的充电标志与原始电池状态,便于直接从 dash.log + # 判断 Kindle 是否识别到外部供电,以及是否真的在充电。 + echo "$(date) Battery level: $battery_level. isCharging: $charging_state. battStateInfo: $battery_state_info." if [ "$LOW_BATTERY_REPORTING" = true ]; then - battery_level_numeric=${battery_level%?} - if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then - "$LOW_BATTERY_CMD" "$battery_level_numeric" - fi + case "$battery_level" in + *%) + battery_level_numeric=${battery_level%?} + if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then + "$LOW_BATTERY_CMD" "$battery_level_numeric" + fi + ;; + esac fi } @@ -88,6 +281,9 @@ rtc_sleep() { if [ "$DEBUG" = true ]; then sleep "$duration" + elif [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then + echo "Skipping system suspend, sleeping for ${duration}s instead" + sleep "$duration" else # shellcheck disable=SC2039 [ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC" @@ -95,26 +291,73 @@ rtc_sleep() { fi } +manual_wake_detected() { + requested_duration=$1 + actual_duration=$2 + + if [ "$DEBUG" = true ] || [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then + return 1 + fi + + if [ "$requested_duration" -le "$MANUAL_WAKE_EARLY_TOLERANCE_SECONDS" ]; then + return 1 + fi + + [ "$actual_duration" -lt $((requested_duration - MANUAL_WAKE_EARLY_TOLERANCE_SECONDS)) ] +} + +hold_after_manual_wake() { + requested_duration=$1 + sleep_started_at=$2 + sleep_finished_at=$3 + actual_duration=$((sleep_finished_at - sleep_started_at)) + + if ! manual_wake_detected "$requested_duration" "$actual_duration"; then + return + fi + + echo "Manual wake detected after ${actual_duration}s, keeping awake for ${MANUAL_WAKE_KEEP_AWAKE_SECONDS}s" + + # 短按电源键提前唤醒后,先把 dashboard 内容恢复回来, + # 再给出一段明确的可交互窗口,避免 2~3 秒内再次休眠。 + refresh_dashboard || true + sleep "$MANUAL_WAKE_KEEP_AWAKE_SECONDS" +} + main_loop() { while true; do log_battery_stats next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE") - if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then + if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then action="sleep" prepare_sleep else + if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then + echo "Debug mode active, skipping sleeping screen." + fi action="suspend" refresh_dashboard fi - # take a bit of time before going to sleep, so this process can be aborted - sleep 10 + actual_sleep_secs=$next_wakeup_secs + if [ "$actual_sleep_secs" -gt "$PRE_SLEEP_GRACE_SECONDS" ]; then + actual_sleep_secs=$((actual_sleep_secs - PRE_SLEEP_GRACE_SECONDS)) + else + actual_sleep_secs=0 + fi + + # 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。 + # 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。 + sleep "$PRE_SLEEP_GRACE_SECONDS" echo "Going to $action, next wakeup in ${next_wakeup_secs}s" - rtc_sleep "$next_wakeup_secs" + sleep_started_at=$(now_epoch) + rtc_sleep "$actual_sleep_secs" + sleep_finished_at=$(now_epoch) + hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at" done } diff --git a/dash/staging/post-jailbreak-root/dashboard/debug-off.sh b/dash/staging/post-jailbreak-root/dashboard/debug-off.sh index 382e41c..50c7a90 100755 --- a/dash/staging/post-jailbreak-root/dashboard/debug-off.sh +++ b/dash/staging/post-jailbreak-root/dashboard/debug-off.sh @@ -35,7 +35,12 @@ END { mv "$TMP_FILE" "$ENV_FILE" # 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, -# 避免旧进程继续按旧配置运行。 +# 然后立刻拉起新的 dashboard,避免用户还要再次手动启动。 pkill -f "$DIR/dash.sh" 2>/dev/null || true +pkill -f "$DIR/start.sh" 2>/dev/null || true +pkill -f "$DIR/local/theme-menu-service.sh" 2>/dev/null || true +pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true +sleep 1 +"$DIR/launch-from-kual.sh" -echo "已关闭 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。" +echo "已关闭 Dashboard 调试模式,并自动重启 Kindle Dashboard。" diff --git a/dash/staging/post-jailbreak-root/dashboard/debug-on.sh b/dash/staging/post-jailbreak-root/dashboard/debug-on.sh index 9b6b26b..33e70a9 100755 --- a/dash/staging/post-jailbreak-root/dashboard/debug-on.sh +++ b/dash/staging/post-jailbreak-root/dashboard/debug-on.sh @@ -35,7 +35,12 @@ END { mv "$TMP_FILE" "$ENV_FILE" # 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它, -# 避免旧进程继续按旧配置进入系统挂起。 +# 然后立刻拉起新的 dashboard,避免用户还要在短时间内再点一次菜单。 pkill -f "$DIR/dash.sh" 2>/dev/null || true +pkill -f "$DIR/start.sh" 2>/dev/null || true +pkill -f "$DIR/local/theme-menu-service.sh" 2>/dev/null || true +pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true +sleep 1 +"$DIR/launch-from-kual.sh" -echo "已开启 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。" +echo "已开启 Dashboard 调试模式,并自动重启 Kindle Dashboard。" diff --git a/dash/staging/post-jailbreak-root/dashboard/launch-from-kual.sh b/dash/staging/post-jailbreak-root/dashboard/launch-from-kual.sh new file mode 100755 index 0000000..ba32ee5 --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/launch-from-kual.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(dirname "$0")" +LOG_FILE="$DIR/logs/kual-launch.log" +PID_FILE="$DIR/logs/kual-launch.pid" +# overlay 模式下,用户期望从 KUAL 点击后几乎直接进入 dashboard。 +# 这里仍然要求 KUAL 先正常退出,但默认不再额外人为等待, +# 只在需要回退到更保守的切换节奏时,再通过环境变量显式加回延迟。 +LAUNCH_DELAY_SECONDS="${KUAL_LAUNCH_DELAY_SECONDS:-0}" +KUAL_QUIT_GRACE_SECONDS="${KUAL_QUIT_GRACE_SECONDS:-0}" +KUAL_APP_ID="${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}" + +mkdir -p "$(dirname "$LOG_FILE")" + +case "$LAUNCH_DELAY_SECONDS" in + ''|*[!0-9]*) + LAUNCH_DELAY_SECONDS=0 + ;; +esac + +case "$KUAL_QUIT_GRACE_SECONDS" in + ''|*[!0-9]*) + KUAL_QUIT_GRACE_SECONDS=0 + ;; +esac + +if [ -f "$PID_FILE" ]; then + old_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "${old_pid:-}" ] && kill -0 "$old_pid" 2>/dev/null; then + kill "$old_pid" 2>/dev/null || true + sleep 1 + fi +fi + +# KUAL 直接同步执行 start.sh 时,framework/KUAL 正在切换的那一瞬间, +# 后台 dash 进程仍可能留在同一个 session 里被一起 TERM 掉。 +# 这里仍然先让独立 session 请求 KUAL 正常退出,但默认不再故意停留在原生首页, +# 而是把启动等待压到 0,尽量直接切到 dashboard。 +if command -v setsid >/dev/null 2>&1; then + nohup setsid /bin/sh -c ' + delay=$1 + target_dir=$2 + kual_app_id=$3 + quit_grace=$4 + + # 复用 KUAL 自己的 appmgrd stop 路径,尽量让它先干净退回原生 UI。 + if command -v lipc-set-prop >/dev/null 2>&1; then + lipc-set-prop com.lab126.appmgrd stop "$kual_app_id" >/dev/null 2>&1 || true + fi + + if [ "$quit_grace" -gt 0 ] 2>/dev/null; then + sleep "$quit_grace" + fi + + if [ "$delay" -gt 0 ] 2>/dev/null; then + sleep "$delay" + fi + + exec "$target_dir/start.sh" + ' sh "$LAUNCH_DELAY_SECONDS" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >>"$LOG_FILE" 2>&1 /dev/null 2>&1; then + lipc-set-prop com.lab126.appmgrd stop "$kual_app_id" >/dev/null 2>&1 || true + fi + + if [ "$quit_grace" -gt 0 ] 2>/dev/null; then + sleep "$quit_grace" + fi + + if [ "$delay" -gt 0 ] 2>/dev/null; then + sleep "$delay" + fi + + exec "$target_dir/start.sh" + ' sh "$LAUNCH_DELAY_SECONDS" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >>"$LOG_FILE" 2>&1 "$PID_FILE" +echo "$(date 2>/dev/null || true) requested KUAL quit, scheduled dashboard launch with quit_grace=${KUAL_QUIT_GRACE_SECONDS}s launch_delay=${LAUNCH_DELAY_SECONDS}s" >>"$LOG_FILE" diff --git a/dash/staging/post-jailbreak-root/dashboard/launch-theme-from-kual.sh b/dash/staging/post-jailbreak-root/dashboard/launch-theme-from-kual.sh new file mode 100644 index 0000000..c507ed6 --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/launch-theme-from-kual.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(dirname "$0")" +SWITCH_THEME_CMD="$DIR/switch-theme.sh" +LAUNCH_FROM_KUAL_CMD="$DIR/launch-from-kual.sh" + +requested_theme_id=${1:?"usage: launch-theme-from-kual.sh [orientation]"} +requested_orientation=${2:-} + +# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。 +# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑, +# 同时把“选主题”前移到进入 dashboard 之前。 +if [ -n "$requested_orientation" ]; then + "$SWITCH_THEME_CMD" "$requested_theme_id" "$requested_orientation" +else + "$SWITCH_THEME_CMD" "$requested_theme_id" +fi + +exec "$LAUNCH_FROM_KUAL_CMD" diff --git a/dash/staging/post-jailbreak-root/dashboard/local/clock-index.sh b/dash/staging/post-jailbreak-root/dashboard/local/clock-index.sh new file mode 100644 index 0000000..4ee276b --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/clock-index.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env sh +set -eu + +to_decimal() { + printf '%s' "$1" | awk '{ + sub(/^0+/, "", $0) + if ($0 == "") { + $0 = 0 + } + print $0 + }' +} + +clock_values_from_offset() { + epoch_seconds=$(date '+%s') + offset_minutes=$1 + + # 这里用 Lua 直接按 Unix 时间戳加偏移换算时分, + # 避免 Kindle 上的 BusyBox date 对 Asia/Shanghai 这类时区名支持不完整。 + lua - "$epoch_seconds" "$offset_minutes" <<'LUA' +local epoch_seconds = assert(tonumber(arg[1]), "missing epoch seconds") +local offset_minutes = assert(tonumber(arg[2]), "missing offset minutes") +local seconds_per_day = 24 * 60 * 60 +local local_seconds = epoch_seconds + offset_minutes * 60 +local seconds_of_day = ((local_seconds % seconds_per_day) + seconds_per_day) % seconds_per_day +local hour = math.floor(seconds_of_day / 3600) +local minute = math.floor((seconds_of_day % 3600) / 60) + +io.write(string.format("%02d %02d\n", hour, minute)) +LUA +} + +current_clock_values() { + # 表盘显示优先走固定 UTC 偏移,避免依赖设备对时区字符串的支持。 + # 这样 next-wakeup 仍可继续使用 TIMEZONE=Asia/Shanghai 做 cron 计算, + # 表盘则稳定显示北京时间。 + if [ -n "${CLOCK_TIME_OFFSET_MINUTES:-}" ]; then + clock_values_from_offset "$CLOCK_TIME_OFFSET_MINUTES" + return + fi + + # 如果没有单独配置偏移,再回退到传统的 TZ=date 方案,兼容历史配置。 + if [ -n "${TIMEZONE:-}" ]; then + if timezone_clock=$(TZ="$TIMEZONE" date '+%H %M' 2>/dev/null); then + printf '%s\n' "$timezone_clock" + return + fi + fi + + # 最后才回退到系统默认时区,至少保证脚本仍能继续工作。 + date '+%H %M' +} + +if [ "$#" -ge 2 ]; then + hour_value=$1 + minute_value=$2 +else + set -- $(current_clock_values) + hour_value=$1 + minute_value=$2 +fi + +hour_decimal=$(to_decimal "$hour_value") +minute_decimal=$(to_decimal "$minute_value") +hour_index=$(( (hour_decimal % 12) * 60 + minute_decimal )) + +printf 'hour=%s\n' "$hour_decimal" +printf 'minute=%s\n' "$minute_decimal" +printf 'hour_index=%03d\n' "$hour_index" +printf 'minute_index=%02d\n' "$minute_decimal" diff --git a/dash/staging/post-jailbreak-root/dashboard/local/env.sh b/dash/staging/post-jailbreak-root/dashboard/local/env.sh index 2e74d96..d0e9d0c 100644 --- a/dash/staging/post-jailbreak-root/dashboard/local/env.sh +++ b/dash/staging/post-jailbreak-root/dashboard/local/env.sh @@ -4,7 +4,89 @@ export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1} # 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。 export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"} +# 调度计算依赖 next-wakeup 这个 Rust 程序,它要求使用 IANA 时区名。 +# 这里必须保留 Asia/Shanghai,才能正确计算下一次唤醒时间。 export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"} +# Kindle 上的 BusyBox date 对 IANA 时区名支持不稳定,直接拿 TIMEZONE 画表盘会退回 UTC。 +# 北京时间全年固定为 UTC+8,没有夏令时,所以表盘单独走固定偏移,避免依赖系统时区解析。 +export CLOCK_TIME_OFFSET_MINUTES=${CLOCK_TIME_OFFSET_MINUTES:-480} +export BACKGROUND_URL=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"} +export THEMES_INDEX_URL=${THEMES_INDEX_URL:-"https://shell.biboer.cn:20001/themes.json"} +export THEMES_INDEX_REFRESH_INTERVAL_MINUTES=${THEMES_INDEX_REFRESH_INTERVAL_MINUTES:-1440} +export THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440} +export THEME_ID=${THEME_ID:-"default"} +export ORIENTATION=${ORIENTATION:-"portrait"} +export BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120} +export CLOCK_REGION_X=${CLOCK_REGION_X:-347} +export CLOCK_REGION_Y=${CLOCK_REGION_Y:-55} +export CLOCK_REGION_WIDTH=${CLOCK_REGION_WIDTH:-220} +export CLOCK_REGION_HEIGHT=${CLOCK_REGION_HEIGHT:-220} +export CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15} + +# 本机时钟外观参数: +# 页面改版导致时钟区域尺寸变化时,通常只需要改 CLOCK_REGION_*, +# 这组比例参数会随新的宽高自动缩放。 +# 如果只是想微调指针长短、粗细或刻度长度,再改下面这些值即可,不用改 Lua 代码。 +export CLOCK_FACE_RADIUS_RATIO=${CLOCK_FACE_RADIUS_RATIO:-0.47} +export CLOCK_FACE_STROKE=${CLOCK_FACE_STROKE:-3} +export CLOCK_TICK_OUTER_INSET=${CLOCK_TICK_OUTER_INSET:-6} +export CLOCK_MAJOR_TICK_LENGTH=${CLOCK_MAJOR_TICK_LENGTH:-14} +export CLOCK_MINOR_TICK_LENGTH=${CLOCK_MINOR_TICK_LENGTH:-7} +export CLOCK_MAJOR_TICK_THICKNESS=${CLOCK_MAJOR_TICK_THICKNESS:-4} +export CLOCK_MINOR_TICK_THICKNESS=${CLOCK_MINOR_TICK_THICKNESS:-2} +export CLOCK_HOUR_LENGTH_RATIO=${CLOCK_HOUR_LENGTH_RATIO:-0.48} +export CLOCK_MINUTE_LENGTH_RATIO=${CLOCK_MINUTE_LENGTH_RATIO:-0.72} +export CLOCK_HOUR_THICKNESS=${CLOCK_HOUR_THICKNESS:-9} +export CLOCK_MINUTE_THICKNESS=${CLOCK_MINUTE_THICKNESS:-5} +export CLOCK_CENTER_RADIUS=${CLOCK_CENTER_RADIUS:-7} + +# 进入 rtc suspend 前预留的可中断窗口,方便在调试时及时停止进程。 +# 这段时间会从真正的休眠时长里扣掉,避免分钟刷新慢一拍。 +export PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10} +# 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。 +# 这样用户有足够时间看屏、切主题或继续交互,而不会立刻再次休眠。 +export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60} +# 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。 +export MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5} + +# Voyage 顶部状态栏遮罩:用于压住系统偶尔重画出来的时间、Wi-Fi、电池图标。 +# 当前坐标只覆盖页面顶部空白带,不会擦到天气卡上边框。 +export STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true} +export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700} +export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0} +export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372} +export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} +export STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} +export STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} + +# 运行态主题菜单当前默认关闭,主题切换统一收口到 KUAL 入口。 +# 如需恢复双翻页键菜单,可临时改回 true 做专项验证。 +export THEME_MENU_ENABLED=${THEME_MENU_ENABLED:-false} +export THEME_MENU_EVENT_DEVICE=${THEME_MENU_EVENT_DEVICE:-"/dev/input/event2"} +export THEME_MENU_COMBO_WINDOW_SECONDS=${THEME_MENU_COMBO_WINDOW_SECONDS:-0.35} +# `/mnt/us` 是 FAT 分区,不能创建 FIFO;菜单监听器的临时管道必须放在 `/tmp`。 +export THEME_MENU_RUNTIME_DIR=${THEME_MENU_RUNTIME_DIR:-"/tmp/kindle-dash-theme-menu"} + +# 默认模式:保留 Kindle 原生 UI 栈,让 dashboard 只负责画面覆盖与休眠调度。 +# 这条路线已经在 Voyage 5.13.6 上实机验证通过: +# calendar -> 主页 -> KUAL -> 回主页 可正常工作。 +export KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-true} + +# 从 KUAL 进入 dashboard 时,默认不再刻意停留在原生首页。 +# 如果某台机器仍需要更保守的切换节奏,可把这两个值临时调大后再做回归验证。 +export KUAL_QUIT_GRACE_SECONDS=${KUAL_QUIT_GRACE_SECONDS:-0} +export KUAL_LAUNCH_DELAY_SECONDS=${KUAL_LAUNCH_DELAY_SECONDS:-0} + +# 右下角长按热区当前默认关闭,避免和底层原生桌面的触摸响应冲突。 +# 如需恢复这条实验入口,可临时改回 true,但当前默认只保留 KUAL 主题入口。 +export TOUCH_HOME_ENABLED=${TOUCH_HOME_ENABLED:-false} +export TOUCH_HOME_EVENT_DEVICE=${TOUCH_HOME_EVENT_DEVICE:-"/dev/input/event1"} +export TOUCH_HOME_RUNTIME_DIR=${TOUCH_HOME_RUNTIME_DIR:-"/tmp/kindle-dash-touch-home"} +export TOUCH_HOME_SCREEN_WIDTH=${TOUCH_HOME_SCREEN_WIDTH:-1072} +export TOUCH_HOME_SCREEN_HEIGHT=${TOUCH_HOME_SCREEN_HEIGHT:-1448} +export TOUCH_HOME_HOTSPOT_WIDTH_RATIO=${TOUCH_HOME_HOTSPOT_WIDTH_RATIO:-0.18} +export TOUCH_HOME_HOTSPOT_HEIGHT_RATIO=${TOUCH_HOME_HOTSPOT_HEIGHT_RATIO:-0.18} +export TOUCH_HOME_LONG_PRESS_SECONDS=${TOUCH_HOME_LONG_PRESS_SECONDS:-1.2} # By default, partial screen updates are used to update the screen, # to prevent the screen from flashing. After a few partial updates, @@ -13,6 +95,11 @@ export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"} # 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。 export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0} +# 调试开关:设为 true 后,主循环仍会按计划拉图和刷新屏幕, +# 不会进入 sleeping.png 分支,也不会把 Kindle 写入 /sys/power/state 进入系统挂起。 +# 适合通过 KUAL 或普通 start.sh 连续观察效果,调试结束后再改回 false。 +export DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false} + # When the time until the next wakeup is greater or equal to this number, # the dashboard will not be refreshed anymore, but instead show a # 'kindle is sleeping' screen. This can be useful if your schedule only runs diff --git a/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh b/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh index 0733233..897126d 100755 --- a/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh +++ b/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh @@ -1,4 +1,32 @@ #!/usr/bin/env sh -# Fetch a new dashboard image, make sure to output it to "$1". -# For example: -"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png +set -eu + +# 拉取低频背景图,调用方负责传入输出路径。 +output_path=${1:?"missing output path"} +DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +ENV_FILE="$DIR/env.sh" +THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" + +# fetch-dashboard 既会被 dash.sh 调,也会被 switch-theme.sh 单独调。 +# 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。 +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" +# shellcheck disable=SC1090 +[ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE" + +background_path=${BACKGROUND_PATH:-""} +background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"} +local_background_path="" + +if [ -n "$background_path" ]; then + local_background_path="$DIR/../$background_path" +fi + +# 主题背景如果已经随本地部署同步到 Kindle,优先直接拷贝本地文件, +# 这样切换主题时不依赖远端图片资源是否已经发布完成。 +if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then + cp "$local_background_path" "$output_path" + exit 0 +fi + +"$DIR/../xh" -d -q -o "$output_path" get "$background_url" diff --git a/dash/staging/post-jailbreak-root/dashboard/local/render-clock.lua b/dash/staging/post-jailbreak-root/dashboard/local/render-clock.lua new file mode 100644 index 0000000..6dc1233 --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/render-clock.lua @@ -0,0 +1,191 @@ +-- 在 Kindle 本机生成一张时钟区域位图,然后交给 fbink 刷到屏幕。 +-- 这里不再依赖透明 PNG 叠图,避免 eips 处理 alpha 时出现整块发黑的问题。 + +local output_path = assert(arg[1], "missing output path") +local width = tonumber((assert(arg[2], "missing width"))) +local height = tonumber((assert(arg[3], "missing height"))) +local hour_value = tonumber((assert(arg[4], "missing hour"))) +local minute_value = tonumber((assert(arg[5], "missing minute"))) + +local function number_arg(index, fallback) + local value = arg[index] + if value == nil or value == "" then + return fallback + end + + local numeric = tonumber(value) + if numeric == nil then + return fallback + end + + return numeric +end + +local WHITE = 255 +local BLACK = 0 +local cx = (width - 1) / 2 +local cy = (height - 1) / 2 +-- 偶数尺寸下如果半径直接顶到边界,右侧和下侧会更容易出现裁切感。 +-- 这里把描边厚度的一半让给外圈,保持四边可视留白更均匀。 +local face_stroke = number_arg(7, 3) +local face_radius = math.max(1, math.min(width, height) * number_arg(6, 0.47) - face_stroke / 2) +local major_tick_outer_inset = number_arg(8, 6) +local minor_tick_outer_inset = number_arg(9, major_tick_outer_inset) +local major_tick_length = number_arg(10, 14) +local minor_tick_length = number_arg(11, 7) +local major_tick_thickness = number_arg(12, 4) +local minor_tick_thickness = number_arg(13, 2) +local hour_length_ratio = number_arg(14, 0.48) +local hour_back_length_ratio = number_arg(15, 0) +local minute_length_ratio = number_arg(16, 0.72) +local minute_back_length_ratio = number_arg(17, 0) +local hour_thickness = number_arg(18, 9) +local minute_thickness = number_arg(19, 5) +local center_radius = number_arg(20, 7) +local rotation_degrees = number_arg(21, 0) +local rotation_radians = math.rad(rotation_degrees) + +local pixels = {} +for index = 1, width * height do + pixels[index] = WHITE +end + +local function pixel_index(x, y) + return y * width + x + 1 +end + +local function set_pixel(x, y, value) + if x < 0 or y < 0 or x >= width or y >= height then + return + end + + pixels[pixel_index(x, y)] = value +end + +local function fill_disk(x, y, radius, value) + local r2 = radius * radius + local min_x = math.floor(x - radius) + local max_x = math.ceil(x + radius) + local min_y = math.floor(y - radius) + local max_y = math.ceil(y + radius) + + for py = min_y, max_y do + for px = min_x, max_x do + local dx = px - x + local dy = py - y + if dx * dx + dy * dy <= r2 then + set_pixel(px, py, value) + end + end + end +end + +local function draw_circle(radius, thickness, value) + local samples = 720 + for step = 0, samples - 1 do + local angle = (step / samples) * math.pi * 2 + local x = cx + math.cos(angle) * radius + local y = cy + math.sin(angle) * radius + fill_disk(x, y, thickness / 2, value) + end +end + +local function fill_bar(origin_x, origin_y, angle, start_length, end_length, thickness, value) + local start_pos = math.min(start_length, end_length) + local end_pos = math.max(start_length, end_length) + local ux = math.cos(angle) + local uy = math.sin(angle) + local vx = -uy + local vy = ux + local half_thickness = thickness / 2 + local corners = { + { + x = origin_x + ux * start_pos + vx * half_thickness, + y = origin_y + uy * start_pos + vy * half_thickness, + }, + { + x = origin_x + ux * start_pos - vx * half_thickness, + y = origin_y + uy * start_pos - vy * half_thickness, + }, + { + x = origin_x + ux * end_pos + vx * half_thickness, + y = origin_y + uy * end_pos + vy * half_thickness, + }, + { + x = origin_x + ux * end_pos - vx * half_thickness, + y = origin_y + uy * end_pos - vy * half_thickness, + }, + } + + local min_x = math.floor(math.min(corners[1].x, corners[2].x, corners[3].x, corners[4].x)) + local max_x = math.ceil(math.max(corners[1].x, corners[2].x, corners[3].x, corners[4].x)) + local min_y = math.floor(math.min(corners[1].y, corners[2].y, corners[3].y, corners[4].y)) + local max_y = math.ceil(math.max(corners[1].y, corners[2].y, corners[3].y, corners[4].y)) + + for py = min_y, max_y do + for px = min_x, max_x do + local sample_x = px + 0.5 + local sample_y = py + 0.5 + local dx = sample_x - origin_x + local dy = sample_y - origin_y + local longitudinal = dx * ux + dy * uy + local lateral = dx * vx + dy * vy + + if longitudinal >= start_pos and longitudinal <= end_pos and math.abs(lateral) <= half_thickness then + set_pixel(px, py, value) + end + end + end +end + +local function draw_ticks() + for tick = 0, 59 do + local is_major = tick % 5 == 0 + local angle = (tick / 60) * math.pi * 2 - math.pi / 2 + rotation_radians + local outer = face_radius - (is_major and major_tick_outer_inset or minor_tick_outer_inset) + local inner = outer - (is_major and major_tick_length or minor_tick_length) + fill_bar(cx, cy, angle, inner, outer, is_major and major_tick_thickness or minor_tick_thickness, BLACK) + end +end + +local function draw_hands() + local hour_angle = (hour_value % 12) * 30 + minute_value * 0.5 + local minute_angle = minute_value * 6 + local hour_radians = math.rad(hour_angle + rotation_degrees - 90) + local minute_radians = math.rad(minute_angle + rotation_degrees - 90) + + fill_bar(cx, cy, hour_radians, -face_radius * hour_back_length_ratio, face_radius * hour_length_ratio, hour_thickness, BLACK) + fill_bar( + cx, + cy, + minute_radians, + -face_radius * minute_back_length_ratio, + face_radius * minute_length_ratio, + minute_thickness, + BLACK + ) + fill_disk(cx, cy, center_radius, BLACK) +end + +draw_circle(face_radius, face_stroke, BLACK) +draw_ticks() +draw_hands() + +local file = assert(io.open(output_path, "wb")) +file:write(string.format("P5\n%d %d\n255\n", width, height)) + +local char_cache = { + [WHITE] = string.char(WHITE), + [BLACK] = string.char(BLACK), +} + +for y = 0, height - 1 do + local row = {} + for x = 0, width - 1 do + local value = pixels[pixel_index(x, y)] + row[#row + 1] = char_cache[value] or string.char(value) + end + file:write(table.concat(row)) +end + +file:close() diff --git a/dash/staging/post-jailbreak-root/dashboard/local/render-clock.sh b/dash/staging/post-jailbreak-root/dashboard/local/render-clock.sh new file mode 100644 index 0000000..8e810dd --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/render-clock.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +ENV_FILE="$DIR/env.sh" +THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" + +# 单独执行本脚本时,也需要读取同一份坐标配置。 +# 否则会退回到脚本内默认值,导致手工调试与主循环绘制位置不一致。 +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" +# 主题切换后,时钟区域坐标和绘制参数会落在运行时 env 里。 +# 这里额外覆盖一次,保证分钟级重绘与最近一次主题配置保持一致。 +# shellcheck disable=SC1090 +[ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE" + +clock_region_x=${CLOCK_REGION_X:-262} +clock_region_y=${CLOCK_REGION_Y:-55} +clock_region_width=${CLOCK_REGION_WIDTH:-220} +clock_region_height=${CLOCK_REGION_HEIGHT:-220} +clock_face_radius_ratio=${CLOCK_FACE_RADIUS_RATIO:-0.47} +clock_face_stroke=${CLOCK_FACE_STROKE:-3} +clock_tick_outer_inset=${CLOCK_TICK_OUTER_INSET:-6} +clock_major_tick_outer_inset=${CLOCK_MAJOR_TICK_OUTER_INSET:-$clock_tick_outer_inset} +clock_minor_tick_outer_inset=${CLOCK_MINOR_TICK_OUTER_INSET:-$clock_tick_outer_inset} +clock_major_tick_length=${CLOCK_MAJOR_TICK_LENGTH:-14} +clock_minor_tick_length=${CLOCK_MINOR_TICK_LENGTH:-7} +clock_major_tick_thickness=${CLOCK_MAJOR_TICK_THICKNESS:-4} +clock_minor_tick_thickness=${CLOCK_MINOR_TICK_THICKNESS:-2} +clock_hour_length_ratio=${CLOCK_HOUR_LENGTH_RATIO:-0.48} +clock_hour_back_length_ratio=${CLOCK_HOUR_BACK_LENGTH_RATIO:-0} +clock_minute_length_ratio=${CLOCK_MINUTE_LENGTH_RATIO:-0.72} +clock_minute_back_length_ratio=${CLOCK_MINUTE_BACK_LENGTH_RATIO:-0} +clock_hour_thickness=${CLOCK_HOUR_THICKNESS:-9} +clock_minute_thickness=${CLOCK_MINUTE_THICKNESS:-5} +clock_center_radius=${CLOCK_CENTER_RADIUS:-7} +clock_rotation_degrees=${CLOCK_ROTATION_DEGREES:-0} +force_full_refresh=${1:-false} +output_path="$DIR/state/clock-render.pgm" + +eval "$("$DIR/clock-index.sh")" + +mkdir -p "$DIR/state" + +lua "$DIR/render-clock.lua" \ + "$output_path" \ + "$clock_region_width" \ + "$clock_region_height" \ + "$hour" \ + "$minute" \ + "$clock_face_radius_ratio" \ + "$clock_face_stroke" \ + "$clock_major_tick_outer_inset" \ + "$clock_minor_tick_outer_inset" \ + "$clock_major_tick_length" \ + "$clock_minor_tick_length" \ + "$clock_major_tick_thickness" \ + "$clock_minor_tick_thickness" \ + "$clock_hour_length_ratio" \ + "$clock_hour_back_length_ratio" \ + "$clock_minute_length_ratio" \ + "$clock_minute_back_length_ratio" \ + "$clock_hour_thickness" \ + "$clock_minute_thickness" \ + "$clock_center_radius" \ + "$clock_rotation_degrees" + +if [ "$force_full_refresh" = true ]; then + # Kindle Voyage 当前这条链路里,fbink 默认会叠加 viewport 修正, + # 导致图像在屏幕上出现双重偏移。这里强制关闭 viewport 修正, + # 让坐标与网页导出的像素坐标保持一致。 + fbink -q -V -f -g "file=$output_path,x=$clock_region_x,y=$clock_region_y" +else + fbink -q -V -g "file=$output_path,x=$clock_region_x,y=$clock_region_y" +fi diff --git a/dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua b/dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua new file mode 100644 index 0000000..faf19ba --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua @@ -0,0 +1,343 @@ +-- 读取 calendar 产出的主题 JSON,并把 Kindle 侧真正需要的字段提取出来。 +-- 这里实现一个精简 JSON 解析器,避免在设备上额外依赖 jq / python。 + +local command = assert(arg[1], "missing command") + +local function read_file(path) + local file = assert(io.open(path, "rb")) + local content = file:read("*a") + file:close() + return content +end + +local function decode(json) + local position = 1 + local length = #json + + local function skip_whitespace() + while position <= length do + local ch = json:sub(position, position) + if ch ~= " " and ch ~= "\n" and ch ~= "\r" and ch ~= "\t" then + break + end + position = position + 1 + end + end + + local parse_value + + local function parse_string() + position = position + 1 + local parts = {} + + while position <= length do + local ch = json:sub(position, position) + + if ch == '"' then + position = position + 1 + return table.concat(parts) + end + + if ch == "\\" then + local escape = json:sub(position + 1, position + 1) + local replacements = { + ['"'] = '"', + ["\\"] = "\\", + ["/"] = "/", + ["b"] = "\b", + ["f"] = "\f", + ["n"] = "\n", + ["r"] = "\r", + ["t"] = "\t", + } + + if escape == "u" then + error("unsupported unicode escape") + end + + local replacement = replacements[escape] + if not replacement then + error("invalid string escape") + end + + parts[#parts + 1] = replacement + position = position + 2 + else + parts[#parts + 1] = ch + position = position + 1 + end + end + + error("unterminated string") + end + + local function parse_number() + local start_pos = position + + while position <= length do + local ch = json:sub(position, position) + if not ch:match("[%d%+%-%.eE]") then + break + end + position = position + 1 + end + + local value = tonumber(json:sub(start_pos, position - 1)) + if value == nil then + error("invalid number") + end + + return value + end + + local function parse_array() + position = position + 1 + skip_whitespace() + + local result = {} + if json:sub(position, position) == "]" then + position = position + 1 + return result + end + + while true do + result[#result + 1] = parse_value() + skip_whitespace() + + local ch = json:sub(position, position) + if ch == "]" then + position = position + 1 + return result + end + + if ch ~= "," then + error("invalid array separator") + end + + position = position + 1 + skip_whitespace() + end + end + + local function parse_object() + position = position + 1 + skip_whitespace() + + local result = {} + if json:sub(position, position) == "}" then + position = position + 1 + return result + end + + while true do + if json:sub(position, position) ~= '"' then + error("object key must be string") + end + + local key = parse_string() + skip_whitespace() + + if json:sub(position, position) ~= ":" then + error("missing object colon") + end + + position = position + 1 + result[key] = parse_value() + skip_whitespace() + + local ch = json:sub(position, position) + if ch == "}" then + position = position + 1 + return result + end + + if ch ~= "," then + error("invalid object separator") + end + + position = position + 1 + skip_whitespace() + end + end + + function parse_value() + skip_whitespace() + + local ch = json:sub(position, position) + if ch == "{" then + return parse_object() + end + + if ch == "[" then + return parse_array() + end + + if ch == '"' then + return parse_string() + end + + if ch == "-" or ch:match("%d") then + return parse_number() + end + + if json:sub(position, position + 3) == "true" then + position = position + 4 + return true + end + + if json:sub(position, position + 4) == "false" then + position = position + 5 + return false + end + + if json:sub(position, position + 3) == "null" then + position = position + 4 + return nil + end + + error("unexpected token") + end + + local result = parse_value() + skip_whitespace() + + if position <= length then + error("unexpected trailing content") + end + + return result +end + +local function find_theme(index_data, theme_id) + for _, theme in ipairs(index_data.themes or {}) do + if theme.id == theme_id then + return theme + end + end + return nil +end + +local function orientation_exists(theme, orientation) + for _, value in ipairs(theme.orientations or {}) do + if value == orientation then + return true + end + end + return false +end + +local function first_orientation(theme, fallback) + if orientation_exists(theme, fallback) then + return fallback + end + + return (theme.orientations or {})[1] or fallback +end + +local function shell_quote(value) + return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'" +end + +local function write_runtime_env(path, theme_id, orientation, variant) + local file = assert(io.open(path, "wb")) + + local lines = { + "export THEME_ID=" .. shell_quote(theme_id), + "export ORIENTATION=" .. shell_quote(orientation), + "export THEME_DEVICE_PLACEMENT=" .. shell_quote(variant.devicePlacement or ""), + "export BACKGROUND_PATH=" .. shell_quote((variant.background and variant.background.path) or ""), + "export BACKGROUND_URL=" .. shell_quote(assert(variant.background.url, "missing background url")), + "export BACKGROUND_REFRESH_INTERVAL_MINUTES=" .. tostring(variant.background.refreshIntervalMinutes or 120), + "export CLOCK_REGION_X=" .. tostring(assert(variant.clock.x, "missing clock x")), + "export CLOCK_REGION_Y=" .. tostring(assert(variant.clock.y, "missing clock y")), + "export CLOCK_REGION_WIDTH=" .. tostring(assert(variant.clock.width, "missing clock width")), + "export CLOCK_REGION_HEIGHT=" .. tostring(assert(variant.clock.height, "missing clock height")), + "export CLOCK_FACE_RADIUS_RATIO=" .. tostring(variant.clock.faceRadiusRatio or 0.47), + "export CLOCK_FACE_STROKE=" .. tostring(variant.clock.faceStroke or 3), + "export CLOCK_TICK_OUTER_INSET=" .. tostring(variant.clock.tickOuterInset or 6), + "export CLOCK_MAJOR_TICK_OUTER_INSET=" .. tostring(variant.clock.majorTickOuterInset or variant.clock.tickOuterInset or 6), + "export CLOCK_MINOR_TICK_OUTER_INSET=" .. tostring(variant.clock.minorTickOuterInset or variant.clock.tickOuterInset or 6), + "export CLOCK_MAJOR_TICK_LENGTH=" .. tostring(variant.clock.majorTickLength or 14), + "export CLOCK_MINOR_TICK_LENGTH=" .. tostring(variant.clock.minorTickLength or 7), + "export CLOCK_MAJOR_TICK_THICKNESS=" .. tostring(variant.clock.majorTickThickness or 4), + "export CLOCK_MINOR_TICK_THICKNESS=" .. tostring(variant.clock.minorTickThickness or 2), + "export CLOCK_HOUR_LENGTH_RATIO=" .. tostring(variant.clock.hourLengthRatio or 0.48), + "export CLOCK_HOUR_BACK_LENGTH_RATIO=" .. tostring(variant.clock.hourBackLengthRatio or 0), + "export CLOCK_MINUTE_LENGTH_RATIO=" .. tostring(variant.clock.minuteLengthRatio or 0.72), + "export CLOCK_MINUTE_BACK_LENGTH_RATIO=" .. tostring(variant.clock.minuteBackLengthRatio or 0), + "export CLOCK_HOUR_THICKNESS=" .. tostring(variant.clock.hourThickness or 9), + "export CLOCK_MINUTE_THICKNESS=" .. tostring(variant.clock.minuteThickness or 5), + "export CLOCK_CENTER_RADIUS=" .. tostring(variant.clock.centerRadius or 7), + "export CLOCK_ROTATION_DEGREES=" .. tostring(variant.clock.rotationDegrees or 0), + } + + file:write(table.concat(lines, "\n")) + file:write("\n") + file:close() +end + +if command == "resolve" then + local index_path = assert(arg[2], "missing themes index path") + local requested_theme_id = arg[3] or "" + local requested_orientation = arg[4] or "" + local index_data = decode(read_file(index_path)) + + local theme = find_theme(index_data, requested_theme_id) + if not theme then + theme = find_theme(index_data, index_data.defaultThemeId) or assert((index_data.themes or {})[1], "themes index empty") + end + + local orientation = requested_orientation + if orientation == "" then + orientation = index_data.defaultOrientation or "portrait" + end + orientation = first_orientation(theme, orientation) + + io.write("THEME_ID=", theme.id, "\n") + io.write("ORIENTATION=", orientation, "\n") + io.write("CONFIG_URL=", assert(theme.configUrl, "missing config url"), "\n") + return +end + +if command == "write-runtime" then + local theme_path = assert(arg[2], "missing theme config path") + local theme_id = assert(arg[3], "missing theme id") + local requested_orientation = assert(arg[4], "missing orientation") + local output_path = assert(arg[5], "missing output path") + local theme_data = decode(read_file(theme_path)) + local variants = assert(theme_data.variants, "missing variants") + local variant = variants[requested_orientation] or variants.portrait or variants.landscape + + if not variant then + error("missing usable variant") + end + + local resolved_orientation = requested_orientation + if variants[requested_orientation] == nil then + if variants.portrait ~= nil then + resolved_orientation = "portrait" + elseif variants.landscape ~= nil then + resolved_orientation = "landscape" + end + end + + write_runtime_env(output_path, theme_id, resolved_orientation, variant) + io.write("THEME_ID=", theme_id, "\n") + io.write("ORIENTATION=", resolved_orientation, "\n") + return +end + +if command == "list" then + local index_path = assert(arg[2], "missing themes index path") + local index_data = decode(read_file(index_path)) + + for _, theme in ipairs(index_data.themes or {}) do + io.write(theme.id or "", "\t") + io.write(theme.label or theme.id or "", "\t") + io.write(table.concat(theme.orientations or {}, ","), "\n") + end + + return +end + +error("unknown command") diff --git a/dash/staging/post-jailbreak-root/dashboard/local/theme-menu-service.sh b/dash/staging/post-jailbreak-root/dashboard/local/theme-menu-service.sh new file mode 100644 index 0000000..4c0d603 --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/theme-menu-service.sh @@ -0,0 +1,380 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +ENV_FILE="$DIR/env.sh" +THEME_FILE="$DIR/theme.env" +THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" +THEMES_INDEX_PATH="$DIR/../themes.json" +THEME_JSON_LUA="$DIR/theme-json.lua" +SWITCH_THEME_CMD="$DIR/../switch-theme.sh" +STOP_DASHBOARD_CMD="$DIR/../stop.sh" +STATE_DIR="$DIR/state" +MENU_ITEMS_FILE="$STATE_DIR/theme-menu-items.tsv" +RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-theme-menu" +EVENT_DEVICE_DEFAULT="/dev/input/event2" +THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35" +HOME_MENU_ITEM_ID="__return_home__" +HOME_MENU_ITEM_LABEL="Return Home" + +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" +# shellcheck disable=SC1090 +[ -f "$THEME_FILE" ] && . "$THEME_FILE" +# shellcheck disable=SC1090 +[ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE" + +EVENT_DEVICE=${THEME_MENU_EVENT_DEVICE:-$EVENT_DEVICE_DEFAULT} +THEME_MENU_COMBO_WINDOW_SECONDS=${THEME_MENU_COMBO_WINDOW_SECONDS:-$THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT} +RUNTIME_DIR=${THEME_MENU_RUNTIME_DIR:-$RUNTIME_DIR_DEFAULT} +ACTION_FIFO="$RUNTIME_DIR/theme-menu-actions.fifo" +PID_FILE="$RUNTIME_DIR/theme-menu-service.pid" + +menu_open=false +selected_index=1 +current_theme_id=${THEME_ID:-default} +current_orientation=${ORIENTATION:-portrait} +stream_pid="" +owns_runtime_state=false + +log_event() { + # 调试阶段把状态机动作写进日志,便于确认按键是否真的被识别到。 + printf '%s theme-menu: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2 +} + +service_running() { + if [ ! -f "$PID_FILE" ]; then + return 1 + fi + + existing_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -z "$existing_pid" ]; then + return 1 + fi + + kill -0 "$existing_pid" 2>/dev/null +} + +send_action_to_service() { + requested_action=$1 + attempts=0 + + if ! service_running; then + printf 'theme-menu-service not running\n' >&2 + return 1 + fi + + # action fifo 由主监听循环持有;正常情况下会一直存在。 + # 这里保守重试几次,兼容监听器刚重启、fifo 正在重建的瞬间。 + while [ "$attempts" -lt 3 ]; do + if [ -p "$ACTION_FIFO" ]; then + printf '%s\n' "$requested_action" >"$ACTION_FIFO" + return 0 + fi + + attempts=$((attempts + 1)) + sleep 1 + done + + printf 'theme-menu-service action fifo not ready\n' >&2 + return 1 +} + +load_runtime() { + # 每次打开菜单前都重新读取当前主题和方向,避免显示过期状态。 + # shellcheck disable=SC1090 + [ -f "$THEME_FILE" ] && . "$THEME_FILE" + # shellcheck disable=SC1090 + [ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE" + + current_theme_id=${THEME_ID:-default} + current_orientation=${ORIENTATION:-portrait} +} + +load_menu_items() { + mkdir -p "$STATE_DIR" + lua "$THEME_JSON_LUA" list "$THEMES_INDEX_PATH" >"$MENU_ITEMS_FILE" + # 在主题列表末尾追加一个动作项,用于安全退出 dashboard 并恢复 Kindle 首页。 + printf '%s\t%s\n' "$HOME_MENU_ITEM_ID" "$HOME_MENU_ITEM_LABEL" >>"$MENU_ITEMS_FILE" +} + +theme_count() { + awk 'END { print NR + 0 }' "$MENU_ITEMS_FILE" +} + +theme_field() { + row=$1 + column=$2 + awk -F '\t' -v target_row="$row" -v target_column="$column" ' + NR == target_row { + print $target_column + exit + } + ' "$MENU_ITEMS_FILE" +} + +current_theme_index() { + awk -F '\t' -v current_theme="$current_theme_id" ' + $1 == current_theme { + print NR + found = 1 + exit + } + END { + if (!found) { + print 1 + } + } + ' "$MENU_ITEMS_FILE" +} + +print_line() { + col=$1 + row=$2 + text=$3 + /usr/sbin/eips "$col" "$row" "$text" +} + +render_menu() { + total_themes=$(theme_count) + current_label=$(theme_field "$selected_index" 2) + + /usr/sbin/eips -c + print_line 3 1 "Kindle Dashboard" + print_line 3 3 "Theme Menu" + print_line 3 5 "Orientation: $current_orientation" + print_line 3 6 "Selected: $current_label" + print_line 3 7 "--------------------------------" + + row=9 + index=1 + while [ "$index" -le "$total_themes" ]; do + theme_label=$(theme_field "$index" 2) + theme_id=$(theme_field "$index" 1) + prefix=" " + line_text="" + + if [ "$index" -eq "$selected_index" ]; then + prefix="> " + fi + + if [ "$theme_id" = "$HOME_MENU_ITEM_ID" ]; then + line_text="${prefix}${theme_label}" + else + line_text="${prefix}${theme_label} (${theme_id})" + fi + + print_line 3 "$row" "$line_text" + row=$((row + 2)) + index=$((index + 1)) + done + + print_line 3 18 "PageUp/PageDown: move" + print_line 3 20 "Press both keys: select" +} + +wrap_index() { + next_index=$1 + total_themes=$(theme_count) + + if [ "$next_index" -lt 1 ]; then + printf '%s\n' "$total_themes" + return + fi + + if [ "$next_index" -gt "$total_themes" ]; then + printf '1\n' + return + fi + + printf '%s\n' "$next_index" +} + +move_selection() { + delta=$1 + selected_index=$(wrap_index $((selected_index + delta))) + render_menu +} + +apply_selection() { + selected_theme_id=$(theme_field "$selected_index" 1) + + /usr/sbin/eips -c + if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then + log_event "apply return_home" + print_line 3 5 "Returning home..." + print_line 3 7 "Restoring framework/webreader" + menu_open=false + "$STOP_DASHBOARD_CMD" + return + fi + + log_event "apply theme=$selected_theme_id orientation=$current_orientation" + print_line 3 5 "Applying theme..." + print_line 3 7 "$selected_theme_id / $current_orientation" + "$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation" + menu_open=false +} + +open_menu() { + load_runtime + load_menu_items + selected_index=$(current_theme_index) + menu_open=true + log_event "open_menu theme=$current_theme_id orientation=$current_orientation selected_index=$selected_index" + render_menu +} + +handle_action() { + action=$1 + log_event "action=$action menu_open=$menu_open" + + case "$action" in + open_menu) + if [ "$menu_open" = true ]; then + # 触摸热区重复触发时,不直接确认当前选项,只重画当前菜单。 + render_menu + else + open_menu + fi + ;; + combo) + if [ "$menu_open" = true ]; then + apply_selection + else + open_menu + fi + ;; + pageup) + if [ "$menu_open" = true ]; then + move_selection -1 + fi + ;; + pagedown) + if [ "$menu_open" = true ]; then + move_selection 1 + fi + ;; + esac +} + +action_stream() { + evtest --grab "$EVENT_DEVICE" 2>/dev/null | awk -v combo_window="$THEME_MENU_COMBO_WINDOW_SECONDS" ' + function extract_time(line, match_count, time_value) { + match_count = match(line, /time [0-9]+\.[0-9]+/) + if (match_count == 0) { + return 0 + } + + time_value = substr(line, RSTART + 5, RLENGTH - 5) + return time_value + 0 + } + + function emit(name) { + print name + fflush() + } + + { + current_time = extract_time($0) + + if ($0 ~ /code 104 \(PageUp\), value 1/) { + if (pending_key == "down" && current_time - pending_time <= combo_window) { + pending_key = "" + emit("combo") + next + } + + pending_key = "up" + pending_time = current_time + next + } + + if ($0 ~ /code 109 \(PageDown\), value 1/) { + if (pending_key == "up" && current_time - pending_time <= combo_window) { + pending_key = "" + emit("combo") + next + } + + pending_key = "down" + pending_time = current_time + next + } + + if ($0 ~ /code 104 \(PageUp\), value 0/) { + if (pending_key == "up") { + pending_key = "" + emit("pageup") + } + next + } + + if ($0 ~ /code 109 \(PageDown\), value 0/) { + if (pending_key == "down") { + pending_key = "" + emit("pagedown") + } + } + } + ' +} + +cleanup() { + if [ -n "$stream_pid" ]; then + kill "$stream_pid" 2>/dev/null || true + fi + + if [ "$owns_runtime_state" != true ]; then + return + fi + + rm -f "$ACTION_FIFO" + rm -f "$PID_FILE" +} + +ensure_single_instance() { + if [ -f "$PID_FILE" ]; then + existing_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then + log_event "service_already_running pid=$existing_pid" + exit 0 + fi + fi + + printf '%s\n' "$$" >"$PID_FILE" + owns_runtime_state=true +} + +if [ "${1:-}" = "trigger" ]; then + if [ "$#" -ne 2 ]; then + printf 'usage: %s trigger \n' "$0" >&2 + exit 64 + fi + + send_action_to_service "$2" + exit $? +fi + +trap cleanup EXIT INT TERM +mkdir -p "$STATE_DIR" +mkdir -p "$RUNTIME_DIR" +ensure_single_instance +log_event "service_started event_device=$EVENT_DEVICE runtime_dir=$RUNTIME_DIR combo_window=$THEME_MENU_COMBO_WINDOW_SECONDS" + +while true; do + rm -f "$ACTION_FIFO" + mkfifo "$ACTION_FIFO" + + action_stream >"$ACTION_FIFO" & + stream_pid=$! + + while IFS= read -r action; do + handle_action "$action" + done <"$ACTION_FIFO" + + wait "$stream_pid" 2>/dev/null || true + stream_pid="" + rm -f "$ACTION_FIFO" + sleep 1 +done diff --git a/dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh b/dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh new file mode 100644 index 0000000..02f35f6 --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +ENV_FILE="$DIR/env.sh" +THEME_FILE="$DIR/theme.env" +STATE_DIR="$DIR/state" +LOCAL_THEMES_INDEX="$DIR/../themes.json" +LOCAL_THEMES_DIR="$DIR/../themes" +THEMES_INDEX_CACHE="$STATE_DIR/themes.json" +THEMES_INDEX_TIMESTAMP_FILE="$STATE_DIR/themes-updated-at" +CURRENT_THEME_CACHE="$STATE_DIR/current-theme.json" +CURRENT_THEME_ID_FILE="$STATE_DIR/current-theme-id" +CURRENT_THEME_TIMESTAMP_FILE="$STATE_DIR/current-theme-updated-at" +THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env" +THEME_JSON_LUA="$DIR/theme-json.lua" +FETCH_CMD="$DIR/../xh" + +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" +# shellcheck disable=SC1090 +[ -f "$THEME_FILE" ] && . "$THEME_FILE" + +mkdir -p "$STATE_DIR" + +force_index=false +force_theme=false +requested_theme_id=${THEME_ID:-default} +requested_orientation=${ORIENTATION:-portrait} + +while [ "$#" -gt 0 ]; do + case "$1" in + --force-index) + force_index=true + ;; + --force-theme) + force_theme=true + ;; + --theme) + shift + requested_theme_id=${1:?"missing theme id"} + ;; + --orientation) + shift + requested_orientation=${1:?"missing orientation"} + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac + shift +done + +THEMES_INDEX_URL=${THEMES_INDEX_URL:-"https://shell.biboer.cn:20001/themes.json"} +THEMES_INDEX_REFRESH_INTERVAL_MINUTES=${THEMES_INDEX_REFRESH_INTERVAL_MINUTES:-1440} +THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440} + +now_epoch() { + date '+%s' +} + +write_timestamp() { + date '+%s' >"$1" +} + +refresh_due() { + timestamp_file=$1 + interval_minutes=$2 + + if [ ! -f "$timestamp_file" ]; then + return 0 + fi + + current_epoch=$(now_epoch) + last_epoch=$(cat "$timestamp_file" 2>/dev/null || echo 0) + [ $((current_epoch - last_epoch)) -ge $((interval_minutes * 60)) ] +} + +fetch_to_path() { + url=$1 + output_path=$2 + tmp_path="${output_path}.tmp.$$" + + rm -f "$tmp_path" + + if ! "$FETCH_CMD" -d -q -o "$tmp_path" get "$url"; then + rm -f "$tmp_path" + return 1 + fi + + mv "$tmp_path" "$output_path" + return 0 +} + +parse_kv_file() { + file_path=$1 + while IFS='=' read -r key value; do + case "$key" in + THEME_ID) + resolved_theme_id=$value + ;; + ORIENTATION) + resolved_orientation=$value + ;; + CONFIG_URL) + resolved_config_url=$value + ;; + esac + done <"$file_path" +} + +sync_themes_index() { + if [ -f "$LOCAL_THEMES_INDEX" ]; then + cp "$LOCAL_THEMES_INDEX" "$THEMES_INDEX_CACHE" + write_timestamp "$THEMES_INDEX_TIMESTAMP_FILE" + echo "Themes index refreshed from local bundle" + return 0 + fi + + # 主题清单是全局入口,平时按天同步一次即可。 + # 真正切换主题时会走 --force-index,确保马上拿到最新列表。 + if [ "$force_index" = true ] || [ ! -f "$THEMES_INDEX_CACHE" ] || refresh_due "$THEMES_INDEX_TIMESTAMP_FILE" "$THEMES_INDEX_REFRESH_INTERVAL_MINUTES"; then + if fetch_to_path "$THEMES_INDEX_URL" "$THEMES_INDEX_CACHE"; then + write_timestamp "$THEMES_INDEX_TIMESTAMP_FILE" + echo "Themes index refreshed" + elif [ ! -f "$THEMES_INDEX_CACHE" ]; then + echo "Themes index fetch failed and no cache is available." >&2 + return 1 + else + echo "Themes index fetch failed, using cached copy." + fi + fi + + return 0 +} + +resolve_selected_theme() { + # 这里统一做一次主题和方向的兜底解析: + # 如果本地请求的 theme / orientation 已失效,就按 themes.json 的默认值回退。 + resolve_output_file="$STATE_DIR/theme-resolve.$$" + lua "$THEME_JSON_LUA" resolve "$THEMES_INDEX_CACHE" "$requested_theme_id" "$requested_orientation" >"$resolve_output_file" + resolved_theme_id="" + resolved_orientation="" + resolved_config_url="" + parse_kv_file "$resolve_output_file" + rm -f "$resolve_output_file" + + if [ -z "$resolved_theme_id" ] || [ -z "$resolved_orientation" ] || [ -z "$resolved_config_url" ]; then + echo "Unable to resolve theme selection." >&2 + return 1 + fi + + return 0 +} + +sync_theme_config() { + local_theme_config="$LOCAL_THEMES_DIR/${resolved_theme_id}.json" + + if [ -f "$local_theme_config" ]; then + cp "$local_theme_config" "$CURRENT_THEME_CACHE" + printf '%s\n' "$resolved_theme_id" >"$CURRENT_THEME_ID_FILE" + write_timestamp "$CURRENT_THEME_TIMESTAMP_FILE" + echo "Theme config refreshed from local bundle: $resolved_theme_id" + return 0 + fi + + # 主题配置按 theme 维度缓存; + # orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。 + needs_theme_fetch=$force_theme + cached_theme_id=$(cat "$CURRENT_THEME_ID_FILE" 2>/dev/null || echo "") + + if [ ! -f "$CURRENT_THEME_CACHE" ] || [ ! -f "$CURRENT_THEME_ID_FILE" ]; then + needs_theme_fetch=true + elif [ "$cached_theme_id" != "$resolved_theme_id" ]; then + needs_theme_fetch=true + elif refresh_due "$CURRENT_THEME_TIMESTAMP_FILE" "$THEME_CONFIG_REFRESH_INTERVAL_MINUTES"; then + needs_theme_fetch=true + fi + + if [ "$needs_theme_fetch" = true ]; then + if fetch_to_path "$resolved_config_url" "$CURRENT_THEME_CACHE"; then + printf '%s\n' "$resolved_theme_id" >"$CURRENT_THEME_ID_FILE" + write_timestamp "$CURRENT_THEME_TIMESTAMP_FILE" + echo "Theme config refreshed: $resolved_theme_id" + elif [ ! -f "$CURRENT_THEME_CACHE" ] || [ "$cached_theme_id" != "$resolved_theme_id" ]; then + echo "Theme config fetch failed and no matching cache is available." >&2 + return 1 + else + echo "Theme config fetch failed, using cached copy." + fi + fi + + return 0 +} + +write_runtime_env() { + # runtime env 是 Kindle 真正消费的运行时配置快照。 + # fetch-dashboard / render-clock / dash.sh 都只读这一份,避免各自重复解析 JSON。 + runtime_output_file="$STATE_DIR/theme-runtime-result.$$" + lua "$THEME_JSON_LUA" write-runtime \ + "$CURRENT_THEME_CACHE" \ + "$resolved_theme_id" \ + "$resolved_orientation" \ + "$THEME_RUNTIME_ENV_FILE" >"$runtime_output_file" + + resolved_theme_id="" + resolved_orientation="" + while IFS='=' read -r key value; do + case "$key" in + THEME_ID) + resolved_theme_id=$value + ;; + ORIENTATION) + resolved_orientation=$value + ;; + esac + done <"$runtime_output_file" + rm -f "$runtime_output_file" + + if [ -z "$resolved_theme_id" ] || [ -z "$resolved_orientation" ]; then + echo "Unable to write runtime theme env." >&2 + return 1 + fi + + printf 'THEME_ID=%s\n' "$resolved_theme_id" + printf 'ORIENTATION=%s\n' "$resolved_orientation" + return 0 +} + +sync_themes_index +resolve_selected_theme +sync_theme_config +write_runtime_env diff --git a/dash/staging/post-jailbreak-root/dashboard/local/touch-home-service.sh b/dash/staging/post-jailbreak-root/dashboard/local/touch-home-service.sh new file mode 100644 index 0000000..6441518 --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/touch-home-service.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +ENV_FILE="$DIR/env.sh" +THEME_MENU_SERVICE_CMD="$DIR/theme-menu-service.sh" +STOP_DASHBOARD_CMD="$DIR/../stop.sh" +RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-touch-home" +TOUCH_DEVICE_DEFAULT="/dev/input/event1" +SCREEN_WIDTH_DEFAULT="1072" +SCREEN_HEIGHT_DEFAULT="1448" +HOTSPOT_WIDTH_RATIO_DEFAULT="0.18" +HOTSPOT_HEIGHT_RATIO_DEFAULT="0.18" +LONG_PRESS_SECONDS_DEFAULT="1.2" + +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" + +RUNTIME_DIR=${TOUCH_HOME_RUNTIME_DIR:-$RUNTIME_DIR_DEFAULT} +PID_FILE="$RUNTIME_DIR/touch-home-service.pid" +ACTION_FIFO="$RUNTIME_DIR/touch-home-actions.fifo" +TOUCH_DEVICE=${TOUCH_HOME_EVENT_DEVICE:-$TOUCH_DEVICE_DEFAULT} +SCREEN_WIDTH=${TOUCH_HOME_SCREEN_WIDTH:-$SCREEN_WIDTH_DEFAULT} +SCREEN_HEIGHT=${TOUCH_HOME_SCREEN_HEIGHT:-$SCREEN_HEIGHT_DEFAULT} +HOTSPOT_WIDTH_RATIO=${TOUCH_HOME_HOTSPOT_WIDTH_RATIO:-$HOTSPOT_WIDTH_RATIO_DEFAULT} +HOTSPOT_HEIGHT_RATIO=${TOUCH_HOME_HOTSPOT_HEIGHT_RATIO:-$HOTSPOT_HEIGHT_RATIO_DEFAULT} +LONG_PRESS_SECONDS=${TOUCH_HOME_LONG_PRESS_SECONDS:-$LONG_PRESS_SECONDS_DEFAULT} + +stream_pid="" +owns_runtime_state=false + +log_event() { + # 统一把触摸热区服务日志写到 stderr,交给上层 nohup 重定向到日志文件。 + printf '%s touch-home: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2 +} + +cleanup() { + if [ -n "$stream_pid" ]; then + kill "$stream_pid" 2>/dev/null || true + fi + + if [ "$owns_runtime_state" != true ]; then + return + fi + + rm -f "$ACTION_FIFO" + rm -f "$PID_FILE" +} + +ensure_single_instance() { + if [ -f "$PID_FILE" ]; then + existing_pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then + log_event "service_already_running pid=$existing_pid" + exit 0 + fi + fi + + printf '%s\n' "$$" >"$PID_FILE" + owns_runtime_state=true +} + +trigger_theme_menu() { + log_event "trigger_theme_menu" + + # 右下角长按现在优先复用主题菜单服务,让用户先看到主题列表。 + # 如果菜单服务意外没起来,再回退到旧的 stop.sh 路径,避免设备失去退出入口。 + if "$THEME_MENU_SERVICE_CMD" trigger open_menu; then + return 0 + fi + + log_event "trigger_theme_menu_failed fallback_home" + "$STOP_DASHBOARD_CMD" +} + +event_stream() { + evtest --grab "$TOUCH_DEVICE" 2>/dev/null | awk \ + -v screen_width="$SCREEN_WIDTH" \ + -v screen_height="$SCREEN_HEIGHT" \ + -v hotspot_width_ratio="$HOTSPOT_WIDTH_RATIO" \ + -v hotspot_height_ratio="$HOTSPOT_HEIGHT_RATIO" \ + -v long_press_seconds="$LONG_PRESS_SECONDS" ' + BEGIN { + hotspot_min_x = int(screen_width * (1 - hotspot_width_ratio)) + hotspot_min_y = int(screen_height * (1 - hotspot_height_ratio)) + touch_active = 0 + touch_x = -1 + touch_y = -1 + touch_start_time = 0 + already_triggered = 0 + } + + function extract_time(line, matched) { + matched = match(line, /time [0-9]+\.[0-9]+/) + if (matched == 0) { + return 0 + } + + return substr(line, RSTART + 5, RLENGTH - 5) + 0 + } + + function emit(name) { + print name + fflush() + } + + function reset_touch() { + touch_active = 0 + touch_x = -1 + touch_y = -1 + touch_start_time = 0 + already_triggered = 0 + } + + { + current_time = extract_time($0) + + # Voyage 上实际输出是 "code 57 (MT Tracking ID)",但不同 evtest 版本 + # 也可能写成 ABS_MT_TRACKING_ID,这里同时兼容两种格式。 + if (($0 ~ /code 57 \(MT Tracking ID\)/ || $0 ~ /ABS_MT_TRACKING_ID/) && $0 ~ /value -1/) { + reset_touch() + next + } + + if ($0 ~ /code 57 \(MT Tracking ID\)/ || $0 ~ /ABS_MT_TRACKING_ID/) { + touch_active = 1 + touch_start_time = current_time + already_triggered = 0 + next + } + + if ($0 ~ /code 53 \(MT X\)/ || $0 ~ /ABS_MT_POSITION_X/) { + if (match($0, /value -?[0-9]+/)) { + touch_x = substr($0, RSTART + 6, RLENGTH - 6) + 0 + } + next + } + + if ($0 ~ /code 54 \(MT Y\)/ || $0 ~ /ABS_MT_POSITION_Y/) { + if (match($0, /value -?[0-9]+/)) { + touch_y = substr($0, RSTART + 6, RLENGTH - 6) + 0 + } + next + } + + if ($0 ~ /Report Sync/ || $0 ~ /SYN_REPORT/) { + if (touch_active && !already_triggered && touch_x >= hotspot_min_x && touch_y >= hotspot_min_y) { + if (current_time - touch_start_time >= long_press_seconds) { + already_triggered = 1 + emit("trigger_theme_menu") + } + } + } + } + ' +} + +trap cleanup EXIT INT TERM +mkdir -p "$RUNTIME_DIR" +ensure_single_instance +log_event "service_started touch_device=$TOUCH_DEVICE long_press_seconds=$LONG_PRESS_SECONDS hotspot_width_ratio=$HOTSPOT_WIDTH_RATIO hotspot_height_ratio=$HOTSPOT_HEIGHT_RATIO" + +while true; do + rm -f "$ACTION_FIFO" + mkfifo "$ACTION_FIFO" + + event_stream >"$ACTION_FIFO" & + stream_pid=$! + + while IFS= read -r action; do + case "$action" in + trigger_theme_menu) + trigger_theme_menu + ;; + esac + done <"$ACTION_FIFO" + + wait "$stream_pid" 2>/dev/null || true + stream_pid="" + rm -f "$ACTION_FIFO" + sleep 1 +done diff --git a/dash/staging/post-jailbreak-root/dashboard/start.sh b/dash/staging/post-jailbreak-root/dashboard/start.sh index f4f38c1..51799fc 100755 --- a/dash/staging/post-jailbreak-root/dashboard/start.sh +++ b/dash/staging/post-jailbreak-root/dashboard/start.sh @@ -4,15 +4,25 @@ DEBUG=${DEBUG:-false} DIR="$(dirname "$0")" ENV_FILE="$DIR/local/env.sh" +THEME_FILE="$DIR/local/theme.env" LOG_FILE="$DIR/logs/dash.log" mkdir -p "$(dirname "$LOG_FILE")" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" +# shellcheck disable=SC1090 +[ -f "$THEME_FILE" ] && . "$THEME_FILE" if [ "$DEBUG" = true ]; then "$DIR/dash.sh" else - "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 & + # 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。 + # 仅用 nohup 仍可能残留在同一个 session/group 里,KUAL/framework 切换时 + # 依然可能把后台子进程一起打掉;这里额外用 setsid 彻底脱离会话。 + if command -v setsid >/dev/null 2>&1; then + nohup setsid "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 >"$LOG_FILE" 2>&1 /dev/null 2>&1 + return $? + fi + + return 127 +} + +job_running() { + job_name=$1 + job_state=$( + if [ -x "$STATUS_BIN" ]; then + "$STATUS_BIN" "$job_name" 2>/dev/null || true + elif [ -x "$INITCTL_BIN" ]; then + "$INITCTL_BIN" status "$job_name" 2>/dev/null || true + else + true + fi + ) + + case "$job_state" in + *"start/running"*) + return 0 + ;; + esac + + return 1 +} + +stop_job() { + job_name=$1 + + if ! job_running "$job_name"; then + return 0 + fi + + run_job_cmd "$STOP_BIN" "$job_name" || run_job_cmd "$INITCTL_BIN" stop "$job_name" || true +} + +start_job() { + job_name=$1 + + if job_running "$job_name"; then + return 0 + fi + + run_job_cmd "$START_BIN" "$job_name" || run_job_cmd "$INITCTL_BIN" start "$job_name" || true +} + +wait_for_cvm() { + attempts=0 + while [ "$attempts" -lt 12 ]; do + if pgrep -x cvm >/dev/null 2>&1; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 1 + done + + return 1 +} + +ensure_job_stable_running() { + job_name=$1 + max_attempts=$2 + stable_required=$3 + attempts=0 + stable_hits=0 + + # Voyage 上 webreader 经常会出现“刚被拉起,1-2 秒后又掉回 stop/waiting”的状态。 + # 这里不再只发一次 start,而是循环观察一段时间,直到连续多次都看到 start/running, + # 才把恢复流程视为成功。 + while [ "$attempts" -lt "$max_attempts" ]; do + if job_running "$job_name"; then + stable_hits=$((stable_hits + 1)) + if [ "$stable_hits" -ge "$stable_required" ]; then + return 0 + fi + else + stable_hits=0 + start_job "$job_name" + fi + + attempts=$((attempts + 1)) + sleep 1 + done + + return 1 +} + +full_restore_ui() { + # 先把残留 UI 栈彻底停干净,避免 webreader 存活但 cvm 已崩的白屏状态。 + stop_job webreader + stop_job framework + killall cvm 2>/dev/null || true + sleep 2 + + if [ -x /etc/init.d/framework ]; then + /etc/init.d/framework start || true + else + start_job framework + fi + + # framework 拉起后,先等 cvm 真正起来,再启动 webreader。 + if ! wait_for_cvm; then + echo "警告:framework 已请求启动,但 cvm 未在预期时间内出现。" + fi + + if ! ensure_job_stable_running webreader 12 3; then + echo "警告:webreader 未能稳定恢复,可能仍需手工执行 /sbin/start webreader。" + fi +} + +stop_dashboard_processes() { + pkill -f dash.sh 2>/dev/null || true + pkill -f start.sh 2>/dev/null || true + pkill -f theme-menu-service.sh 2>/dev/null || true + pkill -f touch-home-service.sh 2>/dev/null || true + lipc-set-prop com.lab126.powerd preventScreenSaver 0 2>/dev/null || true +} + +native_active_app() { + if ! command -v lipc-get-prop >/dev/null 2>&1; then + return 0 + fi + + lipc-get-prop com.lab126.appmgrd activeApp 2>/dev/null || true +} + +return_to_home_from_kual() { + active_app=$(native_active_app) + + case "$active_app" in + com.mobileread.ixtab.kindlelauncher|app://com.mobileread.ixtab.kindlelauncher) + # 保留原生 UI 栈时,dashboard 底下通常仍是 KUAL。 + # 这里复用 launch-from-kual 已验证过的 appmgrd stop 路径,把 KUAL 正常退回首页。 + if command -v lipc-set-prop >/dev/null 2>&1; then + lipc-set-prop com.lab126.appmgrd stop "$KUAL_APP_ID" >/dev/null 2>&1 || true + sleep 1 + fi + ;; + esac +} + +soft_stop_for_native_ui_overlay() { + # 原生 UI 栈保持存活时,退出 dashboard 只需要停掉我们自己的覆盖进程, + # 不要再重建 framework/webreader/cvm,否则会重新引入旧架构的切换风险。 + stop_dashboard_processes + return_to_home_from_kual +} + +if [ "$KEEP_NATIVE_UI_STACK_RUNNING" = true ]; then + soft_stop_for_native_ui_overlay + exit 0 +fi + +stop_dashboard_processes +full_restore_ui diff --git a/dash/staging/post-jailbreak-root/dashboard/switch-theme.sh b/dash/staging/post-jailbreak-root/dashboard/switch-theme.sh new file mode 100644 index 0000000..b62867f --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/switch-theme.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env sh +set -eu + +DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +ENV_FILE="$DIR/local/env.sh" +THEME_FILE="$DIR/local/theme.env" +THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env" +BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at" +BACKGROUND_PNG="$DIR/kindlebg.png" +WAIT_FOR_WIFI_CMD="$DIR/wait-for-wifi.sh" +THEME_SYNC_CMD="$DIR/local/theme-sync.sh" +FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh" +CLOCK_RENDER_CMD="$DIR/local/render-clock.sh" + +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" +# shellcheck disable=SC1090 +[ -f "$THEME_FILE" ] && . "$THEME_FILE" + +requested_theme_id=${1:?"usage: switch-theme.sh [orientation]"} +requested_orientation=${2:-${ORIENTATION:-portrait}} + +mkdir -p "$DIR/local/state" + +# 切换主题时必须立刻联网拉到最新配置和背景, +# 否则用户会看到 theme.env 已更新,但屏幕内容仍停留在旧主题。 +echo "Switching theme to $requested_theme_id / $requested_orientation" +"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP" +"$THEME_SYNC_CMD" --force-index --force-theme --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null + +# shellcheck disable=SC1090 +. "$THEME_RUNTIME_ENV_FILE" + +"$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG" +date '+%s' >"$BACKGROUND_TIMESTAMP_FILE" + +# 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。 +cat >"$THEME_FILE" < 1 - ? URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true) - : workingDirectory -let outputRoot = CommandLine.arguments.count > 2 - ? URL(fileURLWithPath: CommandLine.arguments[2], isDirectory: true) - : repoRoot.appendingPathComponent("assets/kindle-clock", isDirectory: true) - -let faceSourceURL = repoRoot.appendingPathComponent("assets/clock-face.png") -let targetSize = NSSize(width: 220, height: 220) -let faceSourceSize = NSSize(width: 431, height: 431) -let scale = targetSize.width / faceSourceSize.width - -let handConfigs = [ - HandConfig( - sourcePath: "assets/hour-hand.png", - outputDirectoryName: "hour-hand", - frameCount: 720, - sourceWidth: 32, - sourceHeight: 205, - pivotX: 13, - pivotY: 138, - digits: 3, - angleStep: 0.5 - ), - HandConfig( - sourcePath: "assets/minite-hand.png", - outputDirectoryName: "minute-hand", - frameCount: 60, - sourceWidth: 32, - sourceHeight: 288, - pivotX: 15, - pivotY: 203, - digits: 2, - angleStep: 6 - ), -] - -func loadImage(at url: URL) throws -> NSImage { - guard let image = NSImage(contentsOf: url) else { - throw AssetError.invalidImage(url.path) - } - - return image -} - -func savePNG(_ image: NSImage, to url: URL) throws { - guard - let tiffRepresentation = image.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: tiffRepresentation), - let pngData = bitmap.representation(using: .png, properties: [:]) - else { - throw AssetError.pngEncodingFailed(url.path) - } - - try fileManager.createDirectory( - at: url.deletingLastPathComponent(), - withIntermediateDirectories: true - ) - try pngData.write(to: url) -} - -func renderImage(size: NSSize, draw: () -> Void) -> NSImage { - let image = NSImage(size: size) - image.lockFocusFlipped(true) - NSColor.clear.setFill() - NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill() - draw() - image.unlockFocus() - return image -} - -func renderFace() throws { - let faceImage = try loadImage(at: faceSourceURL) - let renderedFace = renderImage(size: targetSize) { - faceImage.draw( - in: NSRect(origin: .zero, size: targetSize), - from: NSRect(origin: .zero, size: faceImage.size), - operation: .sourceOver, - fraction: 1 - ) - } - - try savePNG(renderedFace, to: outputRoot.appendingPathComponent("clock-face.png")) -} - -func renderHandFrames(config: HandConfig) throws { - let sourceURL = repoRoot.appendingPathComponent(config.sourcePath) - let handImage = try loadImage(at: sourceURL) - let outputDirectory = outputRoot.appendingPathComponent(config.outputDirectoryName, isDirectory: true) - let scaledWidth = config.sourceWidth * scale - let scaledHeight = config.sourceHeight * scale - let scaledPivotX = config.pivotX * scale - let scaledPivotY = config.pivotY * scale - - try fileManager.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - - for frameIndex in 0.."${LOG_FILE}" 2>&1 - -echo "=== FORCE OPENSSH 22 ===" -date 2>/dev/null || true -id 2>/dev/null || true - -if [ -f "${SOURCE_KEYS}" ]; then - cp "${SOURCE_KEYS}" "${TARGET_KEYS}" - chmod 600 "${TARGET_KEYS}" 2>/dev/null || true -fi -chmod 755 /mnt/us/usbnet/etc/dot.ssh 2>/dev/null || true - -killall sshd 2>/dev/null || true -killall dropbear 2>/dev/null || true -killall dropbearmulti 2>/dev/null || true -sleep 1 - -rm -f "${PID_FILE}" 2>/dev/null || true -iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT 2>/dev/null || true - -( - exec /mnt/us/usbnet/sbin/sshd -D -e \ - -f /mnt/us/usbnet/etc/sshd_config \ - -o ListenAddress=0.0.0.0 \ - -o Port=22 \ - -o PidFile="${PID_FILE}" \ - -o AuthorizedKeysFile="${TARGET_KEYS}" \ - -o PasswordAuthentication=no \ - -o KbdInteractiveAuthentication=no \ - -o PubkeyAuthentication=yes \ - -o PermitRootLogin=yes \ - -o HostKey=/mnt/us/usbnet/etc/ssh_host_rsa_key \ - -o HostKey=/mnt/us/usbnet/etc/ssh_host_ecdsa_key \ - -o HostKey=/mnt/us/usbnet/etc/ssh_host_ed25519_key -) & - -LAUNCHER_PID="$!" -echo "${LAUNCHER_PID}" > "${OUT_DIR}/launcher.pid" -sleep 1 - -echo "launcher pid: ${LAUNCHER_PID}" -echo "pid file: ${PID_FILE}" -if [ -x /mnt/us/usbnet/bin/lsof ]; then - /mnt/us/usbnet/bin/lsof -n -P -iTCP:22 2>/dev/null || true -fi - -echo "=== DONE ===" -echo "${OUT_DIR}" diff --git a/scripts/sync-layered-clock-assets.sh b/scripts/sync-layered-clock-assets.sh deleted file mode 100644 index 807a396..0000000 --- a/scripts/sync-layered-clock-assets.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env sh -set -eu - -ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" -TMP_DIR="${TMPDIR:-/tmp}/kindle-clock-assets" -KINDLE_TARGET=${1:-kindle} - -rm -rf "$TMP_DIR" -/usr/bin/swift "$ROOT_DIR/scripts/generate-kindle-clock-assets.swift" "$ROOT_DIR" "$TMP_DIR" - -ssh "$KINDLE_TARGET" 'mkdir -p /mnt/us/dashboard/assets/hour-hand /mnt/us/dashboard/assets/minute-hand' -rsync -av --no-o --no-g --delete "$TMP_DIR"/ "$KINDLE_TARGET":/mnt/us/dashboard/assets/ - -echo "Clock assets synced to $KINDLE_TARGET:/mnt/us/dashboard/assets" diff --git a/scripts/sync-layered-clock-to-kindle.sh b/scripts/sync-layered-clock-to-kindle.sh index 663baa1..0d8f63d 100644 --- a/scripts/sync-layered-clock-to-kindle.sh +++ b/scripts/sync-layered-clock-to-kindle.sh @@ -88,6 +88,9 @@ sync_dashboard_runtime() { rsync -av --no-o --no-g \ "$ROOT_DIR/dash/src/start.sh" \ "$ROOT_DIR/dash/src/dash.sh" \ + "$ROOT_DIR/dash/src/stop.sh" \ + "$ROOT_DIR/dash/src/launch-from-kual.sh" \ + "$ROOT_DIR/dash/src/launch-theme-from-kual.sh" \ "$ROOT_DIR/dash/src/switch-theme.sh" \ "$KINDLE_TARGET":/mnt/us/dashboard/ @@ -97,16 +100,24 @@ sync_dashboard_runtime() { "$ROOT_DIR/dash/src/local/clock-index.sh" \ "$ROOT_DIR/dash/src/local/render-clock.lua" \ "$ROOT_DIR/dash/src/local/render-clock.sh" \ + "$ROOT_DIR/dash/src/local/touch-home-service.sh" \ "$ROOT_DIR/dash/src/local/theme-menu-service.sh" \ "$ROOT_DIR/dash/src/local/theme-json.lua" \ "$ROOT_DIR/dash/src/local/theme-sync.sh" \ "$KINDLE_TARGET":/mnt/us/dashboard/local/ - ssh "$KINDLE_TARGET" "chmod +x /mnt/us/dashboard/start.sh /mnt/us/dashboard/dash.sh /mnt/us/dashboard/switch-theme.sh /mnt/us/dashboard/local/fetch-dashboard.sh /mnt/us/dashboard/local/clock-index.sh /mnt/us/dashboard/local/render-clock.sh /mnt/us/dashboard/local/theme-menu-service.sh /mnt/us/dashboard/local/theme-sync.sh" + ssh "$KINDLE_TARGET" "chmod +x /mnt/us/dashboard/start.sh /mnt/us/dashboard/dash.sh /mnt/us/dashboard/stop.sh /mnt/us/dashboard/launch-from-kual.sh /mnt/us/dashboard/launch-theme-from-kual.sh /mnt/us/dashboard/switch-theme.sh /mnt/us/dashboard/local/fetch-dashboard.sh /mnt/us/dashboard/local/clock-index.sh /mnt/us/dashboard/local/render-clock.sh /mnt/us/dashboard/local/touch-home-service.sh /mnt/us/dashboard/local/theme-menu-service.sh /mnt/us/dashboard/local/theme-sync.sh" ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/dashboard/local/state" ssh "$KINDLE_TARGET" "date '+%s' >/mnt/us/dashboard/local/state/background-updated-at" } +sync_kual_extension() { + ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/extensions/kindle-dash" + rsync -av --no-o --no-g \ + "$ROOT_DIR/dash/KUAL/kindle-dash/" \ + "$KINDLE_TARGET":/mnt/us/extensions/kindle-dash/ +} + sync_theme_bundle() { rsync -av --no-o --no-g \ "$ROOT_DIR/calendar/dist/themes.json" \ @@ -175,6 +186,7 @@ update_default_clock_region_env() { sync_dashboard_runtime sync_theme_bundle +sync_kual_extension update_default_clock_region_env if [ -n "$THEME_FILTER" ]; then diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..d06bbf9 --- /dev/null +++ b/todo.md @@ -0,0 +1,8 @@ + +1. 左右翻页键同时按住呼出主题选择。 +1. 一键刷机 +1. 定位 dash/docs/kindle-city-location-plan.zh.md +1. dual - dashborad,白屏问题 +1. calendar项目融合xcs +1. calendar中嵌入图片 +