update at 2026-03-18 13:35:19
This commit is contained in:
273
README.md
Normal file
273
README.md
Normal file
@@ -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` 撤回
|
||||||
@@ -4,15 +4,20 @@ set -eu
|
|||||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
WATCHTHIS_DIR="$ROOT_DIR/dash/staging/watchthis"
|
WATCHTHIS_DIR="$ROOT_DIR/dash/staging/watchthis"
|
||||||
POST_JAILBREAK_ROOT="$ROOT_DIR/dash/staging/post-jailbreak-root"
|
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"
|
SSH_HELPERS_DIR="$ROOT_DIR/scripts/kindle"
|
||||||
SYNC_SCRIPT="$ROOT_DIR/scripts/sync-layered-clock-to-kindle.sh"
|
SYNC_SCRIPT="$ROOT_DIR/scripts/sync-layered-clock-to-kindle.sh"
|
||||||
THEMES_JSON="$ROOT_DIR/calendar/config/themes.json"
|
THEMES_JSON="$ROOT_DIR/calendar/config/themes.json"
|
||||||
|
KTERM_GITHUB_REPO="bfabiszewski/kterm"
|
||||||
|
|
||||||
MODE="all"
|
MODE="all"
|
||||||
VOLUME_PATH="/Volumes/Kindle"
|
VOLUME_PATH="/Volumes/Kindle"
|
||||||
HOST_TARGET="kindle"
|
HOST_TARGET="kindle"
|
||||||
THEME_ID=""
|
THEME_ID=""
|
||||||
ORIENTATION=""
|
ORIENTATION=""
|
||||||
|
KTERM_PACKAGE=""
|
||||||
|
DOWNLOAD_KTERM=false
|
||||||
|
KTERM_VERSION="latest"
|
||||||
SHOW_BACKGROUND=true
|
SHOW_BACKGROUND=true
|
||||||
START_DASHBOARD=false
|
START_DASHBOARD=false
|
||||||
|
|
||||||
@@ -31,6 +36,9 @@ print_usage() {
|
|||||||
-k, --kindle <host> Kindle SSH 主机名,默认 kindle
|
-k, --kindle <host> Kindle SSH 主机名,默认 kindle
|
||||||
-t, --theme <theme-id> SSH 阶段切换到指定主题;默认使用 themes.json 的默认主题
|
-t, --theme <theme-id> SSH 阶段切换到指定主题;默认使用 themes.json 的默认主题
|
||||||
-o, --orientation <value> SSH 阶段切换到指定方向;默认使用 themes.json 的默认方向
|
-o, --orientation <value> SSH 阶段切换到指定方向;默认使用 themes.json 的默认方向
|
||||||
|
--kterm-package <path> 指定 KTerm 安装包;官方 release 用 .zip,也兼容外部 .bin
|
||||||
|
--download-kterm 在 Mac 侧联网下载 KTerm 到 dash/staging/kterm/,再预置到 Kindle
|
||||||
|
--kterm-version <tag> 下载指定 KTerm 版本;默认 latest
|
||||||
--no-background SSH 阶段不同步后立即切主题出图
|
--no-background SSH 阶段不同步后立即切主题出图
|
||||||
--start-dashboard SSH 阶段额外后台启动 dashboard 主循环
|
--start-dashboard SSH 阶段额外后台启动 dashboard 主循环
|
||||||
-h, --help 查看帮助
|
-h, --help 查看帮助
|
||||||
@@ -44,6 +52,20 @@ print_usage() {
|
|||||||
4. 搜索 ;log mrpi 安装 KUAL / MRPI / USBNetwork
|
4. 搜索 ;log mrpi 安装 KUAL / MRPI / USBNetwork
|
||||||
5. 首次进 KTerm 执行 sh /mnt/us/ssh-force-dropbear-22.sh
|
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 预置
|
阶段 A:先做 USB 预置
|
||||||
@@ -56,6 +78,8 @@ print_usage() {
|
|||||||
- Kindle 根目录出现 ssh-force-dropbear-22.sh 等脚本
|
- Kindle 根目录出现 ssh-force-dropbear-22.sh 等脚本
|
||||||
- Kindle 根目录出现 dashboard/、extensions/、mrpackages/
|
- Kindle 根目录出现 dashboard/、extensions/、mrpackages/
|
||||||
- Kindle 根目录出现 .demo/KV-5.13.6.zip、.demo/demo.json、.demo/goodreads/
|
- Kindle 根目录出现 .demo/KV-5.13.6.zip、.demo/demo.json、.demo/goodreads/
|
||||||
|
- 如果提供了 KTerm zip,extensions/ 里会被解压出 kterm/
|
||||||
|
- 如果提供了外部 KTerm bin,mrpackages/ 里会出现对应文件
|
||||||
5. 安全弹出 Kindle。
|
5. 安全弹出 Kindle。
|
||||||
|
|
||||||
阶段 B:在 Kindle 上完成 WatchThis 和越狱
|
阶段 B:在 Kindle 上完成 WatchThis 和越狱
|
||||||
@@ -77,6 +101,7 @@ print_usage() {
|
|||||||
- 首页出现 KUAL
|
- 首页出现 KUAL
|
||||||
- KUAL 菜单里有 Rename OTA Binaries
|
- KUAL 菜单里有 Rename OTA Binaries
|
||||||
- KUAL 菜单里有 kindle-dash
|
- KUAL 菜单里有 kindle-dash
|
||||||
|
- 如果本次预置了 KTerm 安装包,首页或搜索里应能找到 KTerm
|
||||||
4. 先在 KUAL 中执行:
|
4. 先在 KUAL 中执行:
|
||||||
Rename OTA Binaries -> Rename
|
Rename OTA Binaries -> Rename
|
||||||
|
|
||||||
@@ -114,9 +139,12 @@ print_usage() {
|
|||||||
2. 真正导 payload 的时机,是隐藏手势返回后,再次进入 ;demo -> Sideload Content。
|
2. 真正导 payload 的时机,是隐藏手势返回后,再次进入 ;demo -> Sideload Content。
|
||||||
3. 首次 SSH 最稳的入口是 KTerm,不是 USB 直连盲试。
|
3. 首次 SSH 最稳的入口是 KTerm,不是 USB 直连盲试。
|
||||||
4. post-ssh 阶段如果不带 -t/-o,会退回 themes.json 里的默认主题和方向。
|
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
|
||||||
|
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 all -t simple -o portrait
|
||||||
sh bootstrap-new-kindle.sh post-ssh -k kindle --start-dashboard
|
sh bootstrap-new-kindle.sh post-ssh -k kindle --start-dashboard
|
||||||
EOF
|
EOF
|
||||||
@@ -135,6 +163,115 @@ require_path() {
|
|||||||
fi
|
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 <<EOF
|
||||||
|
$release_info
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -z "$downloaded_name" ] || [ -z "$downloaded_url" ]; then
|
||||||
|
echo "无法解析 KTerm 下载地址。" >&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() {
|
kindle_volume_available() {
|
||||||
[ -d "$VOLUME_PATH" ]
|
[ -d "$VOLUME_PATH" ]
|
||||||
}
|
}
|
||||||
@@ -219,6 +356,31 @@ copy_post_jailbreak_bundle() {
|
|||||||
rsync -av --no-o --no-g "$POST_JAILBREAK_ROOT/dashboard/" "$VOLUME_PATH/dashboard/"
|
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() {
|
copy_ssh_helpers() {
|
||||||
require_path "$SSH_HELPERS_DIR/ssh-force-dropbear-22.sh" "SSH helper scripts"
|
require_path "$SSH_HELPERS_DIR/ssh-force-dropbear-22.sh" "SSH helper scripts"
|
||||||
|
|
||||||
@@ -251,12 +413,18 @@ prepare_storage() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log_step "USB" "检查并下载 KTerm 安装包"
|
||||||
|
download_kterm_if_requested
|
||||||
|
|
||||||
log_step "USB" "预置 WatchThis payload"
|
log_step "USB" "预置 WatchThis payload"
|
||||||
copy_watchthis_payload
|
copy_watchthis_payload
|
||||||
|
|
||||||
log_step "USB" "预置越狱后安装包"
|
log_step "USB" "预置越狱后安装包"
|
||||||
copy_post_jailbreak_bundle
|
copy_post_jailbreak_bundle
|
||||||
|
|
||||||
|
log_step "USB" "检查并预置 KTerm 安装包"
|
||||||
|
install_kterm_package_if_available
|
||||||
|
|
||||||
log_step "USB" "预置 SSH 恢复脚本"
|
log_step "USB" "预置 SSH 恢复脚本"
|
||||||
copy_ssh_helpers
|
copy_ssh_helpers
|
||||||
|
|
||||||
@@ -265,7 +433,7 @@ prepare_storage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepare_remote_helpers() {
|
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"
|
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
|
shift
|
||||||
ORIENTATION=${1:?"missing orientation"}
|
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)
|
--no-background)
|
||||||
SHOW_BACKGROUND=false
|
SHOW_BACKGROUND=false
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"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 On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
||||||
{"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"}
|
{"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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`.
|
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.
|
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`.
|
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
|
## 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`.
|
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.
|
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.
|
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
|
## KUAL
|
||||||
|
|
||||||
If you're using KUAL you can use simple extension to start this Dashboard
|
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`)
|
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
|
## 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 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`.
|
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.
|
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
|
## How this works
|
||||||
|
|
||||||
|
|||||||
220
dash/docs/kindle-city-location-plan.zh.md
Normal file
220
dash/docs/kindle-city-location-plan.zh.md
Normal file
@@ -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 恢复后,再按本方案分步实施。
|
||||||
262
dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md
Normal file
262
dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
- 预置 `WatchThis` payload
|
- 预置 `WatchThis` payload
|
||||||
- 预置 `KUAL / MRPI / USBNetwork / kindle-dash`
|
- 预置 `KUAL / MRPI / USBNetwork / kindle-dash`
|
||||||
|
- 可选预置 `KTerm`
|
||||||
- 预置 SSH 恢复脚本
|
- 预置 SSH 恢复脚本
|
||||||
- SSH 打通后自动同步 dashboard、切主题、立即出图
|
- SSH 打通后自动同步 dashboard、切主题、立即出图
|
||||||
|
|
||||||
@@ -32,6 +33,48 @@ sh /mnt/us/ssh-force-dropbear-22.sh
|
|||||||
- 尽量把 Mac 侧和文件预置自动化
|
- 尽量把 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`
|
### 1. `prepare-storage`
|
||||||
@@ -49,6 +92,7 @@ sh bootstrap-new-kindle.sh prepare-storage
|
|||||||
- 把 `Update_hotfix_watchthis_custom.bin` 放到 Kindle 根目录
|
- 把 `Update_hotfix_watchthis_custom.bin` 放到 Kindle 根目录
|
||||||
- 把 `extensions/`、`mrpackages/`、`dashboard/` 预置到 Kindle
|
- 把 `extensions/`、`mrpackages/`、`dashboard/` 预置到 Kindle
|
||||||
- 把 `scripts/kindle/*.sh` 拷到 Kindle 根目录,供 `KTerm` 使用
|
- 把 `scripts/kindle/*.sh` 拷到 Kindle 根目录,供 `KTerm` 使用
|
||||||
|
- 如果检测到 `KTerm` zip,也会一并解压到 `extensions/`
|
||||||
|
|
||||||
### 2. `post-ssh`
|
### 2. `post-ssh`
|
||||||
|
|
||||||
@@ -94,14 +138,15 @@ sh bootstrap-new-kindle.sh
|
|||||||
4. 通过 `Get Started` 完成越狱
|
4. 通过 `Get Started` 完成越狱
|
||||||
5. 搜索 `;log mrpi`
|
5. 搜索 `;log mrpi`
|
||||||
6. 在 `KUAL` 中先执行 `Rename OTA Binaries -> Rename`
|
6. 在 `KUAL` 中先执行 `Rename OTA Binaries -> Rename`
|
||||||
7. 连上 Wi‑Fi
|
7. 如果本轮没有预置 `KTerm`,这里先手工补装 `KTerm`
|
||||||
8. 打开 `KTerm`,执行:
|
8. 连上 Wi‑Fi
|
||||||
|
9. 打开 `KTerm`,执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sh /mnt/us/ssh-force-dropbear-22.sh
|
sh /mnt/us/ssh-force-dropbear-22.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
9. 回到 Mac,执行:
|
10. 回到 Mac,执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sh bootstrap-new-kindle.sh post-ssh
|
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)
|
[kindle-voyage-5.13.6-watchthis-zh.md](/Users/gavin/kindle-dash/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md)
|
||||||
- SSH 打通与 KTerm 兜底:
|
- 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)
|
[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)
|
||||||
|
|||||||
@@ -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-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-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-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)
|
- [scripts/kindle/ssh-stop-all.sh](/Users/gavin/kindle-dash/scripts/kindle/ssh-stop-all.sh)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
# Kindle Voyage 5.13.6 白屏/KUAL/SSH 交接文档
|
# Kindle Voyage 5.13.6 白屏/KUAL/SSH 交接文档
|
||||||
|
|
||||||
本文记录 2026-03-15 这轮 dashboard 调试在后半段进入的异常状态,目标是给下一次接手排障的人一个明确起点,避免继续沿着已经证伪或高风险的路径重复试错。
|
本文记录 2026-03-15 到 2026-03-17 这轮 dashboard 调试在后半段进入的异常状态,目标是给下一次接手排障的人一个明确起点,避免继续沿着已经证伪或高风险的路径重复试错。
|
||||||
|
|
||||||
## 当前交接状态
|
## 当前交接状态
|
||||||
|
|
||||||
截至本次更新时,设备不再处于“完全卡死且无法 SSH”的状态,而是进入了一个更窄的失败场景:
|
截至本次更新时,设备已经不再停留在“完全卡死且无法 SSH”的状态,结论也分成了两部分:
|
||||||
|
|
||||||
- Kindle 已能回到主页
|
- 已修住的:
|
||||||
- `ssh kindle` 已恢复可用
|
- `calendar -> 主页 -> KUAL -> 回主页` 已能正常工作
|
||||||
- 从 `KUAL -> Kindle Dashboard` 进入 dashboard 时,仍会复现白屏
|
- `ssh kindle` 已恢复可用
|
||||||
- 白屏出现时,dashboard 本身往往没有真正接管成功,更像是 `framework/KUAL` 启动链在中途被打断
|
- 当前默认架构已切到“保留原生 UI 栈”的 overlay 模式
|
||||||
- 当前最稳定的恢复路径,仍然是通过 SSH 执行 `./stop.sh`
|
- 仍未完全收口的:
|
||||||
|
- 从 `KUAL -> Kindle Dashboard` 进入 dashboard 时,仍出现过白屏
|
||||||
|
- 白屏出现时,dashboard 本身往往没有真正接管成功,更像是 `framework/KUAL` 启动链在中途被打断
|
||||||
|
|
||||||
补充记录一个当前仍未修住、但边界已经比较清楚的问题:
|
补充记录一个当前仍未修住、但边界已经比较清楚的问题:
|
||||||
|
|
||||||
- 从 `KUAL` 进入 dashboard 后,再尝试回到 dashboard / KUAL 的原生 UI 路径,仍可能落入白屏
|
- 从 `KUAL` 进入 dashboard 后,再尝试回到 dashboard / KUAL 的原生 UI 路径,仍可能落入白屏
|
||||||
- 这个问题不应再继续归因到背景图、时钟或页面布局
|
- 这个问题不应再继续归因到背景图、时钟或页面布局
|
||||||
- 当前更合理的判断,仍然是 `KUAL -> start.sh -> dash.sh` 的切换链路不稳定
|
- 当前更合理的判断,仍然是 `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 秒就休眠”这个问题,本轮已经修住。
|
所以“点 `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#L50) 调用 `stop_framework`
|
||||||
- [dash/src/dash.sh](/Users/gavin/kindle-dash/dash/src/dash.sh#L51) 停掉 `webreader`
|
- [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 之后,不应再期望当前屏幕仍然像普通 Kindle 页面那样可点击
|
||||||
- 也不应再期待“从 dashboard 直接返回刚才那个 KUAL 页面”
|
- 也不应再期待“从 dashboard 直接返回刚才那个 KUAL 页面”
|
||||||
|
|
||||||
|
补充:
|
||||||
|
|
||||||
|
- 截至 `2026-03-17`,默认实现已经改成 `KEEP_NATIVE_UI_STACK_RUNNING=true`
|
||||||
|
- 也就是保留 `framework / webreader` 存活,把 dashboard 当成覆盖显示层
|
||||||
|
- 在这条新路径上,`calendar -> 主页 -> KUAL -> 回主页` 已完成实机验证
|
||||||
|
|
||||||
### 3. 顶栏遮罩不处理触摸
|
### 3. 顶栏遮罩不处理触摸
|
||||||
|
|
||||||
右上角状态栏遮罩逻辑在:
|
右上角状态栏遮罩逻辑在:
|
||||||
@@ -60,45 +69,65 @@ Skipping system suspend, sleeping for 40s instead
|
|||||||
- “点不到 KUAL” 不是顶栏遮罩造成的
|
- “点不到 KUAL” 不是顶栏遮罩造成的
|
||||||
- 真正相关的是 `framework/webreader` 被停掉
|
- 真正相关的是 `framework/webreader` 被停掉
|
||||||
|
|
||||||
### 4. `stop.sh` 现在只负责恢复 UI 栈,不负责直接打开 KUAL
|
### 4. `stop.sh` 现在分成两种退出路径
|
||||||
|
|
||||||
当前 [dash/src/stop.sh](/Users/gavin/kindle-dash/dash/src/stop.sh) 已改成:
|
当前 [dash/src/stop.sh](/Users/gavin/kindle-dash/dash/src/stop.sh) 已改成:
|
||||||
|
|
||||||
- 停掉 `dash.sh`
|
- 默认 overlay 模式:
|
||||||
- 清掉 `preventScreenSaver`
|
- 停掉 `dash.sh`
|
||||||
- 启动 `framework`
|
- 清掉 `preventScreenSaver`
|
||||||
- 启动 `webreader`
|
- 停掉主题菜单和右下角长按监听
|
||||||
|
- 如果底层当前是 `KUAL`,再通过 `appmgrd stop` 把它正常退回首页
|
||||||
|
- 旧架构兼容模式:
|
||||||
|
- 停掉 `dash.sh`
|
||||||
|
- 清掉 `preventScreenSaver`
|
||||||
|
- 停干净 `framework / webreader / cvm`
|
||||||
|
- 再按顺序拉起 `framework`
|
||||||
|
- 等 `cvm` 回来后启动 `webreader`
|
||||||
|
|
||||||
也就是说它的职责是:
|
也就是说它的职责是:
|
||||||
|
|
||||||
- 让 Kindle 回到“应该可以恢复正常 UI”的状态
|
- 让 Kindle 从 dashboard 安全退回原生 UI
|
||||||
|
|
||||||
不是:
|
不是:
|
||||||
|
|
||||||
- 直接把 KUAL booklet 弹出来
|
- 直接把 KUAL booklet 弹出来
|
||||||
|
- 也不是走实验性的“快切换”
|
||||||
|
|
||||||
|
补充:
|
||||||
|
|
||||||
|
- 本轮新试过的“快路径”已经撤回
|
||||||
|
- 直接碰 `blanket` 或尝试 shell 里强拉 `booklet.home`,都可能把 `blanket / cvm` 打崩
|
||||||
|
- 当前 Voyage 5.13.6 仍以稳定恢复优先,快速切换需要改入口架构,不能继续堆在 `stop.sh` 上
|
||||||
|
|
||||||
补充一点:在白屏恢复过程中,`stop.sh` 已经比旧版稳定很多,但仍存在一种残留状态:
|
补充一点:在白屏恢复过程中,`stop.sh` 已经比旧版稳定很多,但仍存在一种残留状态:
|
||||||
|
|
||||||
- `framework` 和 `cvm` 已回来了
|
- `framework` 和 `cvm` 已回来了
|
||||||
- `webreader` 可能还停在 `stop/waiting`
|
- `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
|
```sh
|
||||||
lipc-set-prop com.lab126.booklet run "app://com.lab126.booklet.home"
|
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.booklet run "com.mobileread.ixtab.kindlelauncher.KualBooklet"
|
||||||
|
lipc-set-prop com.lab126.blanket unload splash
|
||||||
|
lipc-set-prop com.lab126.blanket unload screensaver
|
||||||
```
|
```
|
||||||
|
|
||||||
这条路不稳定,已经触发过 `cvm` 崩溃打包。设备上看到过:
|
这条路不稳定,已经触发过 `cvm` 崩溃打包。设备上看到过:
|
||||||
|
|
||||||
- `/mnt/us/documents/cvm_2886_..._crash_Mar_15_14.14.19_2026.tgz`
|
- `/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_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 启动路径
|
### 6. dashboard 本身可以工作,失败更像发生在 KUAL 启动路径
|
||||||
|
|
||||||
@@ -277,7 +306,7 @@ ssh kindle 'cd /mnt/us/dashboard && ./stop.sh'
|
|||||||
如果执行完 `./stop.sh` 后主页仍然没有回来,再补:
|
如果执行完 `./stop.sh` 后主页仍然没有回来,再补:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh kindle 'start webreader'
|
ssh kindle '/sbin/start webreader'
|
||||||
```
|
```
|
||||||
|
|
||||||
不要从 dashboard 页面直接尝试回 KUAL。
|
不要从 dashboard 页面直接尝试回 KUAL。
|
||||||
@@ -307,11 +336,45 @@ ssh kindle 'start webreader'
|
|||||||
3. 继续保留 `ssh kindle 'cd /mnt/us/dashboard && DEBUG=true ./start.sh'` 作为唯一已验证稳定的进入方式
|
3. 继续保留 `ssh kindle 'cd /mnt/us/dashboard && DEBUG=true ./start.sh'` 作为唯一已验证稳定的进入方式
|
||||||
4. 继续保留 `./stop.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 恢复后,再围绕下面三点做实机验证:
|
等 SSH 恢复后,再围绕下面三点做实机验证:
|
||||||
|
|
||||||
1. KUAL wrapper 是否还能触发 `framework` 被 TERM
|
1. KUAL wrapper 是否还能触发 `framework` 被 TERM
|
||||||
2. `start.sh` 的后台脱离方式是否足够彻底
|
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` 的边界切换不稳定
|
- dashboard 与 Kindle 原生 `framework/KUAL` 的边界切换不稳定
|
||||||
- `KUAL -> Kindle Dashboard` 这条启动链仍会白屏
|
- `KUAL -> Kindle Dashboard` 这条启动链仍会白屏
|
||||||
- 直接用 shell 强拉 booklet 会触发前台 Java 崩溃
|
- 直接用 shell 强拉 booklet,或手动碰 `blanket`,都会触发前台 `blanket / cvm` 崩溃
|
||||||
|
|
||||||
因此,当前最重要的不是继续调页面,而是:
|
因此,当前最重要的不是继续调页面,而是:
|
||||||
|
|
||||||
1. 保留当前已经可用的 SSH 启动/停止路径
|
1. 保留当前已经可用的 SSH 启动/停止路径
|
||||||
2. 修住 `KUAL -> Kindle Dashboard` 白屏
|
2. 修住 `KUAL -> Kindle Dashboard` 白屏
|
||||||
3. 在不再触发 `cvm` 崩溃的前提下,把“进入 dashboard”和“退出 dashboard”都收敛成稳定流程
|
3. 在不再触发 `blanket / cvm` 崩溃的前提下,把“进入 dashboard”和“退出 dashboard”都收敛成稳定流程
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
本文是评审方案,不是实机结论。
|
本文是评审方案,不是实机结论。
|
||||||
|
|
||||||
|
补充说明:当前仓库默认已经不启用这套运行态菜单,主题切换入口只保留在 KUAL。
|
||||||
|
本文保留为备选方案与历史设计记录,不代表当前默认交互。
|
||||||
|
|
||||||
当前 Kindle 已掉出 Wi-Fi,SSH 中断,因此这份文档的目标是:
|
当前 Kindle 已掉出 Wi-Fi,SSH 中断,因此这份文档的目标是:
|
||||||
|
|
||||||
- 先把“左右同时按下翻页键,呼出主题选择页面”的方案固定下来
|
- 先把“左右同时按下翻页键,呼出主题选择页面”的方案固定下来
|
||||||
@@ -14,7 +17,15 @@
|
|||||||
|
|
||||||
- Kindle Voyage
|
- Kindle Voyage
|
||||||
- 固件 5.13.6
|
- 固件 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. 已确认事实
|
## 1. 已确认事实
|
||||||
|
|
||||||
@@ -85,6 +96,7 @@ echo "mem" >/sys/power/state
|
|||||||
3. 用户通过翻页键在主题列表中移动选中项
|
3. 用户通过翻页键在主题列表中移动选中项
|
||||||
4. 用户再次同时按下左右翻页键,确认并切换主题
|
4. 用户再次同时按下左右翻页键,确认并切换主题
|
||||||
5. 切换完成后,立即刷新背景和时钟
|
5. 切换完成后,立即刷新背景和时钟
|
||||||
|
6. 菜单列表末尾可附带一个“返回首页”动作项,安全退出 dashboard 并恢复 Kindle 首页
|
||||||
|
|
||||||
本阶段非目标:
|
本阶段非目标:
|
||||||
|
|
||||||
@@ -113,7 +125,16 @@ echo "mem" >/sys/power/state
|
|||||||
|
|
||||||
- `PageUp`:向上移动
|
- `PageUp`:向上移动
|
||||||
- `PageDown`:向下移动
|
- `PageDown`:向下移动
|
||||||
- 再次双键同时按下:确认当前主题
|
- 再次双键同时按下:确认当前项
|
||||||
|
|
||||||
|
同时保留一个触摸兜底入口:
|
||||||
|
|
||||||
|
- 右下角长按:直接呼出同一份运行态主题菜单
|
||||||
|
|
||||||
|
其中当前项可以是:
|
||||||
|
|
||||||
|
- 某个主题:执行主题切换
|
||||||
|
- `Return Home`:退出 dashboard,恢复 framework / webreader,回到 Kindle 首页
|
||||||
|
|
||||||
这样做的原因:
|
这样做的原因:
|
||||||
|
|
||||||
@@ -221,12 +242,19 @@ Press both keys: apply
|
|||||||
3. 识别到组合键后,本机绘制菜单
|
3. 识别到组合键后,本机绘制菜单
|
||||||
4. 用户确认后,调用现有:
|
4. 用户确认后,调用现有:
|
||||||
- `/mnt/us/dashboard/switch-theme.sh <theme-id> [orientation]`
|
- `/mnt/us/dashboard/switch-theme.sh <theme-id> [orientation]`
|
||||||
|
- 或 `/mnt/us/dashboard/stop.sh`
|
||||||
5. `switch-theme.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_ENABLED=true
|
||||||
export THEME_MENU_EVENT_DEVICE="/dev/input/event2"
|
export THEME_MENU_EVENT_DEVICE="/dev/input/event2"
|
||||||
export THEME_MENU_COMBO_WINDOW_SECONDS=0.35
|
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_COMBO_WINDOW_SECONDS`
|
||||||
- 组合键判定窗口
|
- 组合键判定窗口
|
||||||
|
- `THEME_MENU_RUNTIME_DIR`
|
||||||
|
- 菜单服务的临时运行目录
|
||||||
|
- 需要放在支持 FIFO 的文件系统上,Voyage 上应使用 `/tmp`
|
||||||
|
|
||||||
## 9. 风险与限制
|
## 9. 风险与限制
|
||||||
|
|
||||||
@@ -408,8 +440,12 @@ export THEME_MENU_COMBO_WINDOW_SECONDS=0.35
|
|||||||
- 监听 `/dev/input/event2`
|
- 监听 `/dev/input/event2`
|
||||||
- 用短时间窗口识别 `PageUp + PageDown` 组合键
|
- 用短时间窗口识别 `PageUp + PageDown` 组合键
|
||||||
- 在运行态下绘制一个 KUAL 风格的文本主题菜单
|
- 在运行态下绘制一个 KUAL 风格的文本主题菜单
|
||||||
|
- 右下角长按也复用同一份菜单,而不是另起一套触摸 UI
|
||||||
- 用 `PageUp / PageDown` 导航
|
- 用 `PageUp / PageDown` 导航
|
||||||
- 再次双键确认
|
- 再次双键确认
|
||||||
- 最终复用现有 `switch-theme.sh` 完成主题切换
|
- 最终复用现有 `switch-theme.sh` 完成主题切换
|
||||||
|
|
||||||
这是当前成本最低、最容易恢复、也最适合在 SSH 不稳定阶段先落地评审的方案。
|
这是当前成本最低、最容易恢复、也最适合在 SSH 不稳定阶段先落地评审的方案。
|
||||||
|
|
||||||
|
如果菜单需要提供稳定退出入口,也可以在列表末尾追加一个 `Return Home` 动作项。
|
||||||
|
实现上应复用 `stop.sh` 的保守恢复链路,而不要直接强拉 `booklet.home`。
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env"
|
|||||||
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
||||||
THEME_MENU_SERVICE_CMD="$DIR/local/theme-menu-service.sh"
|
THEME_MENU_SERVICE_CMD="$DIR/local/theme-menu-service.sh"
|
||||||
THEME_MENU_LOG_FILE="$DIR/logs/theme-menu.log"
|
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"}
|
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
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_PASSES=${STATUS_MASK_PASSES:-3}
|
||||||
STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1}
|
STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1}
|
||||||
RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
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_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||||
@@ -39,7 +42,7 @@ num_refresh=0
|
|||||||
background_needs_redraw=true
|
background_needs_redraw=true
|
||||||
|
|
||||||
start_theme_menu_service() {
|
start_theme_menu_service() {
|
||||||
if [ "${THEME_MENU_ENABLED:-true}" != true ]; then
|
if [ "${THEME_MENU_ENABLED:-false}" != true ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -52,6 +55,20 @@ start_theme_menu_service() {
|
|||||||
nohup "$THEME_MENU_SERVICE_CMD" >>"$THEME_MENU_LOG_FILE" 2>&1 </dev/null &
|
nohup "$THEME_MENU_SERVICE_CMD" >>"$THEME_MENU_LOG_FILE" 2>&1 </dev/null &
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start_touch_home_service() {
|
||||||
|
if [ "${TOUCH_HOME_ENABLED:-false}" != true ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$TOUCH_HOME_SERVICE_CMD" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$TOUCH_HOME_LOG_FILE")"
|
||||||
|
pkill -f "$TOUCH_HOME_SERVICE_CMD" 2>/dev/null || true
|
||||||
|
nohup "$TOUCH_HOME_SERVICE_CMD" >>"$TOUCH_HOME_LOG_FILE" 2>&1 </dev/null &
|
||||||
|
}
|
||||||
|
|
||||||
load_theme_runtime_config() {
|
load_theme_runtime_config() {
|
||||||
if [ -f "$THEME_RUNTIME_ENV_FILE" ]; then
|
if [ -f "$THEME_RUNTIME_ENV_FILE" ]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
@@ -74,11 +91,17 @@ init() {
|
|||||||
echo "System suspend disabled, using normal sleep between refreshes."
|
echo "System suspend disabled, using normal sleep between refreshes."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$KEEP_NATIVE_UI_STACK_RUNNING" = true ]; then
|
||||||
|
echo "Keeping framework/webreader running for native UI overlay mode."
|
||||||
|
else
|
||||||
stop_framework
|
stop_framework
|
||||||
initctl stop webreader >/dev/null 2>&1
|
initctl stop webreader >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
||||||
start_theme_menu_service
|
start_theme_menu_service
|
||||||
|
start_touch_home_service
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_framework() {
|
stop_framework() {
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ mv "$TMP_FILE" "$ENV_FILE"
|
|||||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||||
# 然后立刻拉起新的 dashboard,避免用户还要再次手动启动。
|
# 然后立刻拉起新的 dashboard,避免用户还要再次手动启动。
|
||||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
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/theme-menu-service.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
"$DIR/start.sh"
|
"$DIR/launch-from-kual.sh"
|
||||||
|
|
||||||
echo "已关闭 Dashboard 调试模式,并自动重启 Kindle Dashboard。"
|
echo "已关闭 Dashboard 调试模式,并自动重启 Kindle Dashboard。"
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ mv "$TMP_FILE" "$ENV_FILE"
|
|||||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||||
# 然后立刻拉起新的 dashboard,避免用户还要在短时间内再点一次菜单。
|
# 然后立刻拉起新的 dashboard,避免用户还要在短时间内再点一次菜单。
|
||||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
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/theme-menu-service.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
"$DIR/start.sh"
|
"$DIR/launch-from-kual.sh"
|
||||||
|
|
||||||
echo "已开启 Dashboard 调试模式,并自动重启 Kindle Dashboard。"
|
echo "已开启 Dashboard 调试模式,并自动重启 Kindle Dashboard。"
|
||||||
|
|||||||
87
dash/src/launch-from-kual.sh
Executable file
87
dash/src/launch-from-kual.sh
Executable file
@@ -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 &
|
||||||
|
else
|
||||||
|
nohup /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 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$!" >"$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"
|
||||||
20
dash/src/launch-theme-from-kual.sh
Normal file
20
dash/src/launch-theme-from-kual.sh
Normal file
@@ -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 <theme-id> [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"
|
||||||
@@ -59,11 +59,34 @@ export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24}
|
|||||||
export STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3}
|
export STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3}
|
||||||
export STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1}
|
export STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1}
|
||||||
|
|
||||||
# 左右翻页键同时按下时,呼出主题菜单;
|
# 运行态主题菜单当前默认关闭,主题切换统一收口到 KUAL 入口。
|
||||||
# 菜单本身仍复用当前 dashboard 的运行方向,只切换 theme id。
|
# 如需恢复双翻页键菜单,可临时改回 true 做专项验证。
|
||||||
export THEME_MENU_ENABLED=${THEME_MENU_ENABLED:-true}
|
export THEME_MENU_ENABLED=${THEME_MENU_ENABLED:-false}
|
||||||
export THEME_MENU_EVENT_DEVICE=${THEME_MENU_EVENT_DEVICE:-"/dev/input/event2"}
|
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}
|
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,
|
# By default, partial screen updates are used to update the screen,
|
||||||
# to prevent the screen from flashing. After a few partial updates,
|
# to prevent the screen from flashing. After a few partial updates,
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
|||||||
THEMES_INDEX_PATH="$DIR/../themes.json"
|
THEMES_INDEX_PATH="$DIR/../themes.json"
|
||||||
THEME_JSON_LUA="$DIR/theme-json.lua"
|
THEME_JSON_LUA="$DIR/theme-json.lua"
|
||||||
SWITCH_THEME_CMD="$DIR/../switch-theme.sh"
|
SWITCH_THEME_CMD="$DIR/../switch-theme.sh"
|
||||||
|
STOP_DASHBOARD_CMD="$DIR/../stop.sh"
|
||||||
STATE_DIR="$DIR/state"
|
STATE_DIR="$DIR/state"
|
||||||
MENU_ITEMS_FILE="$STATE_DIR/theme-menu-items.tsv"
|
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"
|
EVENT_DEVICE_DEFAULT="/dev/input/event2"
|
||||||
THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35"
|
THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35"
|
||||||
|
HOME_MENU_ITEM_ID="__return_home__"
|
||||||
|
HOME_MENU_ITEM_LABEL="Return Home"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -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}
|
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}
|
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
|
menu_open=false
|
||||||
selected_index=1
|
selected_index=1
|
||||||
current_theme_id=${THEME_ID:-default}
|
current_theme_id=${THEME_ID:-default}
|
||||||
current_orientation=${ORIENTATION:-portrait}
|
current_orientation=${ORIENTATION:-portrait}
|
||||||
stream_pid=""
|
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() {
|
load_runtime() {
|
||||||
# 每次打开菜单前都重新读取当前主题和方向,避免显示过期状态。
|
# 每次打开菜单前都重新读取当前主题和方向,避免显示过期状态。
|
||||||
@@ -44,6 +94,8 @@ load_runtime() {
|
|||||||
load_menu_items() {
|
load_menu_items() {
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
lua "$THEME_JSON_LUA" list "$THEMES_INDEX_PATH" >"$MENU_ITEMS_FILE"
|
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() {
|
theme_count() {
|
||||||
@@ -100,18 +152,25 @@ render_menu() {
|
|||||||
theme_label=$(theme_field "$index" 2)
|
theme_label=$(theme_field "$index" 2)
|
||||||
theme_id=$(theme_field "$index" 1)
|
theme_id=$(theme_field "$index" 1)
|
||||||
prefix=" "
|
prefix=" "
|
||||||
|
line_text=""
|
||||||
|
|
||||||
if [ "$index" -eq "$selected_index" ]; then
|
if [ "$index" -eq "$selected_index" ]; then
|
||||||
prefix="> "
|
prefix="> "
|
||||||
fi
|
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))
|
row=$((row + 2))
|
||||||
index=$((index + 1))
|
index=$((index + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
print_line 3 18 "PageUp/PageDown: move"
|
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() {
|
wrap_index() {
|
||||||
@@ -141,9 +200,18 @@ apply_selection() {
|
|||||||
selected_theme_id=$(theme_field "$selected_index" 1)
|
selected_theme_id=$(theme_field "$selected_index" 1)
|
||||||
|
|
||||||
/usr/sbin/eips -c
|
/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 5 "Applying theme..."
|
||||||
print_line 3 7 "$selected_theme_id / $current_orientation"
|
print_line 3 7 "$selected_theme_id / $current_orientation"
|
||||||
|
|
||||||
"$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation"
|
"$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation"
|
||||||
menu_open=false
|
menu_open=false
|
||||||
}
|
}
|
||||||
@@ -153,13 +221,23 @@ open_menu() {
|
|||||||
load_menu_items
|
load_menu_items
|
||||||
selected_index=$(current_theme_index)
|
selected_index=$(current_theme_index)
|
||||||
menu_open=true
|
menu_open=true
|
||||||
|
log_event "open_menu theme=$current_theme_id orientation=$current_orientation selected_index=$selected_index"
|
||||||
render_menu
|
render_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_action() {
|
handle_action() {
|
||||||
action=$1
|
action=$1
|
||||||
|
log_event "action=$action menu_open=$menu_open"
|
||||||
|
|
||||||
case "$action" in
|
case "$action" in
|
||||||
|
open_menu)
|
||||||
|
if [ "$menu_open" = true ]; then
|
||||||
|
# 触摸热区重复触发时,不直接确认当前选项,只重画当前菜单。
|
||||||
|
render_menu
|
||||||
|
else
|
||||||
|
open_menu
|
||||||
|
fi
|
||||||
|
;;
|
||||||
combo)
|
combo)
|
||||||
if [ "$menu_open" = true ]; then
|
if [ "$menu_open" = true ]; then
|
||||||
apply_selection
|
apply_selection
|
||||||
@@ -247,11 +325,42 @@ cleanup() {
|
|||||||
kill "$stream_pid" 2>/dev/null || true
|
kill "$stream_pid" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$owns_runtime_state" != true ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
rm -f "$ACTION_FIFO"
|
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 <action>\n' "$0" >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
send_action_to_service "$2"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
mkdir -p "$STATE_DIR"
|
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
|
while true; do
|
||||||
rm -f "$ACTION_FIFO"
|
rm -f "$ACTION_FIFO"
|
||||||
|
|||||||
182
dash/src/local/touch-home-service.sh
Normal file
182
dash/src/local/touch-home-service.sh
Normal file
@@ -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
|
||||||
@@ -18,6 +18,11 @@ if [ "$DEBUG" = true ]; then
|
|||||||
"$DIR/dash.sh"
|
"$DIR/dash.sh"
|
||||||
else
|
else
|
||||||
# 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。
|
# 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。
|
||||||
# 这里必须用 nohup 脱离会话,否则后台的 dash.sh 会跟着收到 HUP 退出。
|
# 仅用 nohup 仍可能残留在同一个 session/group 里,KUAL/framework 切换时
|
||||||
|
# 依然可能把后台子进程一起打掉;这里额外用 setsid 彻底脱离会话。
|
||||||
|
if command -v setsid >/dev/null 2>&1; then
|
||||||
|
nohup setsid "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||||
|
else
|
||||||
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
174
dash/src/stop.sh
174
dash/src/stop.sh
@@ -1,19 +1,78 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# 退出 dashboard 时,不能只“启动一下 framework”就结束。
|
DIR="$(dirname "$0")"
|
||||||
# Voyage 5.13.6 上白屏往往来自 framework / webreader / cvm 半恢复状态:
|
ENV_FILE="$DIR/local/env.sh"
|
||||||
# 进程看起来在,但前台 Java UI 实际没起来。
|
|
||||||
# 这里统一做一次干净的 UI 栈重启,尽量把设备拉回正常首页/KUAL 可用状态。
|
# 退出 dashboard 时,统一走一条保守恢复路径:
|
||||||
|
# 1. 停掉 dashboard 自己的进程
|
||||||
|
# 2. 干净重启 framework / webreader / cvm
|
||||||
|
#
|
||||||
|
# Voyage 5.13.6 上试过的“快切换”路径会把 blanket/cvm 打崩,
|
||||||
|
# 当前这台机型仍以稳定恢复优先。
|
||||||
|
|
||||||
|
START_BIN=/sbin/start
|
||||||
|
STOP_BIN=/sbin/stop
|
||||||
|
STATUS_BIN=/sbin/status
|
||||||
|
INITCTL_BIN=/sbin/initctl
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false}
|
||||||
|
KUAL_APP_ID=${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}
|
||||||
|
|
||||||
|
run_job_cmd() {
|
||||||
|
bin_path=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [ -x "$bin_path" ]; then
|
||||||
|
"$bin_path" "$@" >/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() {
|
stop_job() {
|
||||||
job_name=$1
|
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() {
|
start_job() {
|
||||||
job_name=$1
|
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() {
|
wait_for_cvm() {
|
||||||
@@ -30,28 +89,99 @@ wait_for_cvm() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pkill -f dash.sh 2>/dev/null || true
|
ensure_job_stable_running() {
|
||||||
pkill -f start.sh 2>/dev/null || true
|
job_name=$1
|
||||||
pkill -f theme-menu-service.sh 2>/dev/null || true
|
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 已崩的白屏状态。
|
attempts=$((attempts + 1))
|
||||||
stop_job webreader
|
sleep 1
|
||||||
stop_job framework
|
done
|
||||||
killall cvm 2>/dev/null || true
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
if [ -x /etc/init.d/framework ]; then
|
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
|
/etc/init.d/framework start || true
|
||||||
else
|
else
|
||||||
start_job framework
|
start_job framework
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# framework 拉起后,先等 cvm 真正起来,再启动 webreader。
|
# framework 拉起后,先等 cvm 真正起来,再启动 webreader。
|
||||||
if ! wait_for_cvm; then
|
if ! wait_for_cvm; then
|
||||||
echo "警告:framework 已请求启动,但 cvm 未在预期时间内出现。"
|
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
|
fi
|
||||||
|
|
||||||
start_job webreader
|
stop_dashboard_processes
|
||||||
sleep 2
|
full_restore_ui
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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。"
|
|
||||||
@@ -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。"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# 调试启动脚本:强制关闭系统挂起,便于在 Kindle 上持续观察刷新效果。
|
|
||||||
DIR="$(dirname "$0")"
|
|
||||||
|
|
||||||
export DISABLE_SYSTEM_SUSPEND=true
|
|
||||||
|
|
||||||
exec "$DIR/start.sh"
|
|
||||||
@@ -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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
pkill -f dash.sh
|
|
||||||
@@ -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"
|
|
||||||
Binary file not shown.
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<extension>
|
|
||||||
<information>
|
|
||||||
<name>Kindle dashboard</name>
|
|
||||||
<id>pascalw-kindle-dash</id>
|
|
||||||
</information>
|
|
||||||
<menus>
|
|
||||||
<menu type="json" dynamic="true">menu.json</menu>
|
|
||||||
</menus>
|
|
||||||
</extension>
|
|
||||||
@@ -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"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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。"
|
|
||||||
@@ -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。"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# 调试启动脚本:强制关闭系统挂起,便于在 Kindle 上持续观察刷新效果。
|
|
||||||
DIR="$(dirname "$0")"
|
|
||||||
|
|
||||||
export DISABLE_SYSTEM_SUSPEND=true
|
|
||||||
|
|
||||||
exec "$DIR/start.sh"
|
|
||||||
@@ -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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
pkill -f dash.sh
|
|
||||||
@@ -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"
|
|
||||||
Binary file not shown.
31
dash/staging/kterm/README.md
Normal file
31
dash/staging/kterm/README.md
Normal file
@@ -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` 也会缓存到这个目录里,后续再次执行时可直接复用。
|
||||||
@@ -3,19 +3,78 @@ DEBUG=${DEBUG:-false}
|
|||||||
[ "$DEBUG" = true ] && set -x
|
[ "$DEBUG" = true ] && set -x
|
||||||
|
|
||||||
DIR="$(dirname "$0")"
|
DIR="$(dirname "$0")"
|
||||||
DASH_PNG="$DIR/dash.png"
|
BACKGROUND_PNG="$DIR/kindlebg.png"
|
||||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||||
LOW_BATTERY_CMD="$DIR/local/low-battery.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"}
|
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||||
SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600}
|
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
|
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_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||||
|
|
||||||
num_refresh=0
|
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 &
|
||||||
|
}
|
||||||
|
|
||||||
|
start_touch_home_service() {
|
||||||
|
if [ "${TOUCH_HOME_ENABLED:-false}" != true ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$TOUCH_HOME_SERVICE_CMD" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$TOUCH_HOME_LOG_FILE")"
|
||||||
|
pkill -f "$TOUCH_HOME_SERVICE_CMD" 2>/dev/null || true
|
||||||
|
nohup "$TOUCH_HOME_SERVICE_CMD" >>"$TOUCH_HOME_LOG_FILE" 2>&1 </dev/null &
|
||||||
|
}
|
||||||
|
|
||||||
|
load_theme_runtime_config() {
|
||||||
|
if [ -f "$THEME_RUNTIME_ENV_FILE" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$THEME_RUNTIME_ENV_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
||||||
@@ -26,17 +85,41 @@ init() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
/etc/init.d/framework stop
|
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
|
echo "System suspend disabled, using normal sleep between refreshes."
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
initctl stop webreader >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
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() {
|
prepare_sleep() {
|
||||||
echo "Preparing sleep"
|
echo "Preparing sleep"
|
||||||
|
|
||||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||||
|
background_needs_redraw=true
|
||||||
|
|
||||||
# Give screen time to refresh
|
# Give screen time to refresh
|
||||||
sleep 2
|
sleep 2
|
||||||
@@ -45,41 +128,151 @@ prepare_sleep() {
|
|||||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh_dashboard() {
|
now_epoch() {
|
||||||
echo "Refreshing dashboard"
|
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"
|
"$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=$?
|
fetch_status=$?
|
||||||
|
|
||||||
if [ "$fetch_status" -ne 0 ]; then
|
if [ "$fetch_status" -ne 0 ]; then
|
||||||
echo "Not updating screen, fetch-dashboard returned $fetch_status"
|
echo "Background fetch failed with $fetch_status"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then
|
store_background_timestamp
|
||||||
num_refresh=0
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# trigger a full refresh once in every 4 refreshes, to keep the screen clean
|
clock_force_full_refresh() {
|
||||||
echo "Full screen refresh"
|
eval "$("$DIR/local/clock-index.sh")"
|
||||||
/usr/sbin/eips -f -g "$DASH_PNG"
|
[ $((minute % CLOCK_FULL_REFRESH_INTERVAL_MINUTES)) -eq 0 ]
|
||||||
else
|
}
|
||||||
echo "Partial screen refresh"
|
|
||||||
/usr/sbin/eips -g "$DASH_PNG"
|
mask_system_status_overlay() {
|
||||||
|
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
||||||
|
return
|
||||||
fi
|
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))
|
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() {
|
log_battery_stats() {
|
||||||
battery_level=$(gasgauge-info -c)
|
battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown")
|
||||||
echo "$(date) Battery level: $battery_level."
|
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
|
if [ "$LOW_BATTERY_REPORTING" = true ]; then
|
||||||
|
case "$battery_level" in
|
||||||
|
*%)
|
||||||
battery_level_numeric=${battery_level%?}
|
battery_level_numeric=${battery_level%?}
|
||||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||||
fi
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +281,9 @@ rtc_sleep() {
|
|||||||
|
|
||||||
if [ "$DEBUG" = true ]; then
|
if [ "$DEBUG" = true ]; then
|
||||||
sleep "$duration"
|
sleep "$duration"
|
||||||
|
elif [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
|
echo "Skipping system suspend, sleeping for ${duration}s instead"
|
||||||
|
sleep "$duration"
|
||||||
else
|
else
|
||||||
# shellcheck disable=SC2039
|
# shellcheck disable=SC2039
|
||||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
||||||
@@ -95,26 +291,73 @@ rtc_sleep() {
|
|||||||
fi
|
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() {
|
main_loop() {
|
||||||
while true; do
|
while true; do
|
||||||
log_battery_stats
|
log_battery_stats
|
||||||
|
|
||||||
next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE")
|
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"
|
action="sleep"
|
||||||
prepare_sleep
|
prepare_sleep
|
||||||
else
|
else
|
||||||
|
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
|
echo "Debug mode active, skipping sleeping screen."
|
||||||
|
fi
|
||||||
action="suspend"
|
action="suspend"
|
||||||
refresh_dashboard
|
refresh_dashboard
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# take a bit of time before going to sleep, so this process can be aborted
|
actual_sleep_secs=$next_wakeup_secs
|
||||||
sleep 10
|
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"
|
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
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ END {
|
|||||||
mv "$TMP_FILE" "$ENV_FILE"
|
mv "$TMP_FILE" "$ENV_FILE"
|
||||||
|
|
||||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||||
# 避免旧进程继续按旧配置运行。
|
# 然后立刻拉起新的 dashboard,避免用户还要再次手动启动。
|
||||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
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。"
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ END {
|
|||||||
mv "$TMP_FILE" "$ENV_FILE"
|
mv "$TMP_FILE" "$ENV_FILE"
|
||||||
|
|
||||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||||
# 避免旧进程继续按旧配置进入系统挂起。
|
# 然后立刻拉起新的 dashboard,避免用户还要在短时间内再点一次菜单。
|
||||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
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。"
|
||||||
|
|||||||
87
dash/staging/post-jailbreak-root/dashboard/launch-from-kual.sh
Executable file
87
dash/staging/post-jailbreak-root/dashboard/launch-from-kual.sh
Executable file
@@ -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 &
|
||||||
|
else
|
||||||
|
nohup /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 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$!" >"$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"
|
||||||
@@ -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 <theme-id> [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"
|
||||||
@@ -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"
|
||||||
@@ -4,7 +4,89 @@
|
|||||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
||||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||||
|
# 调度计算依赖 next-wakeup 这个 Rust 程序,它要求使用 IANA 时区名。
|
||||||
|
# 这里必须保留 Asia/Shanghai,才能正确计算下一次唤醒时间。
|
||||||
export TIMEZONE=${TIMEZONE:-"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,
|
# By default, partial screen updates are used to update the screen,
|
||||||
# to prevent the screen from flashing. After a few partial updates,
|
# to prevent the screen from flashing. After a few partial updates,
|
||||||
@@ -13,6 +95,11 @@ export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
|||||||
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
||||||
export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
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,
|
# 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
|
# the dashboard will not be refreshed anymore, but instead show a
|
||||||
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
||||||
|
|||||||
@@ -1,4 +1,32 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# Fetch a new dashboard image, make sure to output it to "$1".
|
set -eu
|
||||||
# For example:
|
|
||||||
"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png
|
# 拉取低频背景图,调用方负责传入输出路径。
|
||||||
|
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"
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
343
dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua
Normal file
343
dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua
Normal file
@@ -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")
|
||||||
@@ -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 <action>\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
|
||||||
234
dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh
Normal file
234
dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh
Normal file
@@ -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
|
||||||
@@ -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
|
||||||
@@ -4,15 +4,25 @@ DEBUG=${DEBUG:-false}
|
|||||||
|
|
||||||
DIR="$(dirname "$0")"
|
DIR="$(dirname "$0")"
|
||||||
ENV_FILE="$DIR/local/env.sh"
|
ENV_FILE="$DIR/local/env.sh"
|
||||||
|
THEME_FILE="$DIR/local/theme.env"
|
||||||
LOG_FILE="$DIR/logs/dash.log"
|
LOG_FILE="$DIR/logs/dash.log"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$LOG_FILE")"
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||||
|
|
||||||
if [ "$DEBUG" = true ]; then
|
if [ "$DEBUG" = true ]; then
|
||||||
"$DIR/dash.sh"
|
"$DIR/dash.sh"
|
||||||
else
|
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 </dev/null &
|
||||||
|
else
|
||||||
|
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,2 +1,187 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
pkill -f dash.sh
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(dirname "$0")"
|
||||||
|
ENV_FILE="$DIR/local/env.sh"
|
||||||
|
|
||||||
|
# 退出 dashboard 时,统一走一条保守恢复路径:
|
||||||
|
# 1. 停掉 dashboard 自己的进程
|
||||||
|
# 2. 干净重启 framework / webreader / cvm
|
||||||
|
#
|
||||||
|
# Voyage 5.13.6 上试过的“快切换”路径会把 blanket/cvm 打崩,
|
||||||
|
# 当前这台机型仍以稳定恢复优先。
|
||||||
|
|
||||||
|
START_BIN=/sbin/start
|
||||||
|
STOP_BIN=/sbin/stop
|
||||||
|
STATUS_BIN=/sbin/status
|
||||||
|
INITCTL_BIN=/sbin/initctl
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false}
|
||||||
|
KUAL_APP_ID=${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}
|
||||||
|
|
||||||
|
run_job_cmd() {
|
||||||
|
bin_path=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [ -x "$bin_path" ]; then
|
||||||
|
"$bin_path" "$@" >/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
|
||||||
|
|||||||
46
dash/staging/post-jailbreak-root/dashboard/switch-theme.sh
Normal file
46
dash/staging/post-jailbreak-root/dashboard/switch-theme.sh
Normal file
@@ -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 <theme-id> [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" <<EOF
|
||||||
|
export THEME_ID='${THEME_ID}'
|
||||||
|
export ORIENTATION='${ORIENTATION}'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
||||||
|
"$CLOCK_RENDER_CMD" true
|
||||||
|
|
||||||
|
echo "Theme switched to ${THEME_ID} / ${ORIENTATION}"
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"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 On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
||||||
{"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"}
|
{"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
#!/usr/bin/env swift
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct HandConfig {
|
|
||||||
let sourcePath: String
|
|
||||||
let outputDirectoryName: String
|
|
||||||
let frameCount: Int
|
|
||||||
let sourceWidth: CGFloat
|
|
||||||
let sourceHeight: CGFloat
|
|
||||||
let pivotX: CGFloat
|
|
||||||
let pivotY: CGFloat
|
|
||||||
let digits: Int
|
|
||||||
let angleStep: CGFloat
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AssetError: Error, CustomStringConvertible {
|
|
||||||
case invalidImage(String)
|
|
||||||
case pngEncodingFailed(String)
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case let .invalidImage(path):
|
|
||||||
return "无法读取图片:\(path)"
|
|
||||||
case let .pngEncodingFailed(path):
|
|
||||||
return "无法编码 PNG:\(path)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let workingDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true)
|
|
||||||
let repoRoot = CommandLine.arguments.count > 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..<config.frameCount {
|
|
||||||
let angle = CGFloat(frameIndex) * config.angleStep
|
|
||||||
let renderedFrame = renderImage(size: targetSize) {
|
|
||||||
guard let context = NSGraphicsContext.current?.cgContext else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
context.translateBy(x: targetSize.width / 2, y: targetSize.height / 2)
|
|
||||||
context.rotate(by: angle * .pi / 180)
|
|
||||||
context.translateBy(x: -scaledPivotX, y: -scaledPivotY)
|
|
||||||
|
|
||||||
handImage.draw(
|
|
||||||
in: NSRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight),
|
|
||||||
from: NSRect(origin: .zero, size: handImage.size),
|
|
||||||
operation: .sourceOver,
|
|
||||||
fraction: 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let filename = String(format: "%0\(config.digits)d.png", frameIndex)
|
|
||||||
try savePNG(renderedFrame, to: outputDirectory.appendingPathComponent(filename))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try fileManager.createDirectory(at: outputRoot, withIntermediateDirectories: true)
|
|
||||||
try renderFace()
|
|
||||||
|
|
||||||
for config in handConfigs {
|
|
||||||
try renderHandFrames(config: config)
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Generated Kindle clock assets at \(outputRoot.path)")
|
|
||||||
} catch {
|
|
||||||
fputs("\(error)\n", stderr)
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# 强制清理残留 SSH 进程,然后在 22 端口拉起一份 usbnet 自带的 OpenSSH。
|
|
||||||
# 这份 sshd 会优先读取 /mnt/us/usbnet/etc/dot.ssh/authorized_keys。
|
|
||||||
|
|
||||||
TS="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo now)"
|
|
||||||
OUT_DIR="/mnt/us/ssh-debug/${TS}"
|
|
||||||
LOG_FILE="${OUT_DIR}/force-openssh-22.log"
|
|
||||||
PID_FILE="/mnt/us/usbnet/run/sshd-force-22.pid"
|
|
||||||
SOURCE_KEYS="/mnt/us/usbnet/etc/authorized_keys"
|
|
||||||
TARGET_KEYS="/mnt/us/usbnet/etc/dot.ssh/authorized_keys"
|
|
||||||
|
|
||||||
mkdir -p "${OUT_DIR}" /mnt/us/usbnet/run /mnt/us/usbnet/etc/dot.ssh
|
|
||||||
exec >"${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}"
|
|
||||||
@@ -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"
|
|
||||||
@@ -88,6 +88,9 @@ sync_dashboard_runtime() {
|
|||||||
rsync -av --no-o --no-g \
|
rsync -av --no-o --no-g \
|
||||||
"$ROOT_DIR/dash/src/start.sh" \
|
"$ROOT_DIR/dash/src/start.sh" \
|
||||||
"$ROOT_DIR/dash/src/dash.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" \
|
"$ROOT_DIR/dash/src/switch-theme.sh" \
|
||||||
"$KINDLE_TARGET":/mnt/us/dashboard/
|
"$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/clock-index.sh" \
|
||||||
"$ROOT_DIR/dash/src/local/render-clock.lua" \
|
"$ROOT_DIR/dash/src/local/render-clock.lua" \
|
||||||
"$ROOT_DIR/dash/src/local/render-clock.sh" \
|
"$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-menu-service.sh" \
|
||||||
"$ROOT_DIR/dash/src/local/theme-json.lua" \
|
"$ROOT_DIR/dash/src/local/theme-json.lua" \
|
||||||
"$ROOT_DIR/dash/src/local/theme-sync.sh" \
|
"$ROOT_DIR/dash/src/local/theme-sync.sh" \
|
||||||
"$KINDLE_TARGET":/mnt/us/dashboard/local/
|
"$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" "mkdir -p /mnt/us/dashboard/local/state"
|
||||||
ssh "$KINDLE_TARGET" "date '+%s' >/mnt/us/dashboard/local/state/background-updated-at"
|
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() {
|
sync_theme_bundle() {
|
||||||
rsync -av --no-o --no-g \
|
rsync -av --no-o --no-g \
|
||||||
"$ROOT_DIR/calendar/dist/themes.json" \
|
"$ROOT_DIR/calendar/dist/themes.json" \
|
||||||
@@ -175,6 +186,7 @@ update_default_clock_region_env() {
|
|||||||
|
|
||||||
sync_dashboard_runtime
|
sync_dashboard_runtime
|
||||||
sync_theme_bundle
|
sync_theme_bundle
|
||||||
|
sync_kual_extension
|
||||||
update_default_clock_region_env
|
update_default_clock_region_env
|
||||||
|
|
||||||
if [ -n "$THEME_FILTER" ]; then
|
if [ -n "$THEME_FILTER" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user