update at 2026-03-21 18:44:12
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@ data/tts-cache/
|
|||||||
apps/miniprogram/utils/opsEnv.js
|
apps/miniprogram/utils/opsEnv.js
|
||||||
|
|
||||||
calendar/node_modules
|
calendar/node_modules
|
||||||
|
calendar/kindle-backgrounds/
|
||||||
dash/backups
|
dash/backups
|
||||||
dash/downloads
|
dash/downloads
|
||||||
|
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
- `simple` / `default` 等主题已经接入当前运行链路
|
- `simple` / `default` 等主题已经接入当前运行链路
|
||||||
- Kindle 侧采用“低频背景 + 本机时钟重绘”的分层渲染方案
|
- Kindle 侧采用“低频背景 + 本机时钟重绘”的分层渲染方案
|
||||||
|
- 通过 `scripts/sync-layered-clock-to-kindle.sh` 同步主题时,天气背景会优先按 Kindle 当前网络出口位置导出
|
||||||
- 新机 bootstrap 方案已实现
|
- 新机 bootstrap 方案已实现
|
||||||
- 新机 bootstrap 当前仍是“方案已实现,真机恢复出厂闭环未验证”
|
- 新机 bootstrap 当前仍是“方案已实现,真机恢复出厂闭环未验证”
|
||||||
- `launch-from-kual.sh` 与 `setsid` 脱离方案已经落地到代码
|
- `launch-from-kual.sh` 与 `setsid` 脱离方案已经落地到代码
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
- 导出 `kindlebg.png`
|
- 导出 `kindlebg.png`
|
||||||
- 导出多主题背景包和 `themes.json`
|
- 导出多主题背景包和 `themes.json`
|
||||||
- 预览不同主题与方向
|
- 预览不同主题与方向
|
||||||
|
- Web 预览页里的天气位置仍优先走浏览器定位;Kindle 实机同步时则优先读取 Kindle 侧 GeoIP 缓存
|
||||||
|
|
||||||
关键文件:
|
关键文件:
|
||||||
|
|
||||||
@@ -56,6 +58,15 @@
|
|||||||
- 切换主题、维护运行时主题状态
|
- 切换主题、维护运行时主题状态
|
||||||
- 在需要时拉起主题菜单服务
|
- 在需要时拉起主题菜单服务
|
||||||
|
|
||||||
|
运行规则:
|
||||||
|
|
||||||
|
- 时钟刷新原则:无论 `debug on` 还是 `debug off`,设备侧时钟都按分钟调度刷新一次
|
||||||
|
- `debug on`:设备不进入真 suspend,保持常亮,并持续按分钟刷新
|
||||||
|
- `debug off`:设备仍按分钟刷新时钟;普通分钟刷新只更新后台缓存,不主动把时钟刷到可视屏幕,刷新后立即回到低功耗调度
|
||||||
|
- `debug off` 下短按 `power`:设备进入一个 5 分钟的可视窗口;窗口内时钟仍按分钟刷新,窗口结束后恢复到普通低功耗调度
|
||||||
|
- 通过 `KUAL` 启动 dashboard 或切换主题后回到 calendar:同样进入一个 5 分钟的可视窗口,避免用户回来时看到静止时钟
|
||||||
|
- 背景图与主题资源保持低频更新;分钟级高频更新只发生在本机时钟区域,不依赖网络
|
||||||
|
|
||||||
关键文件:
|
关键文件:
|
||||||
|
|
||||||
- [dash.sh](/Users/gavin/kindle-dash/dash/src/dash.sh)
|
- [dash.sh](/Users/gavin/kindle-dash/dash/src/dash.sh)
|
||||||
@@ -161,6 +172,12 @@ sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version late
|
|||||||
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
||||||
```
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `post-ssh` 当前会同步 `dashboard` shell 脚本、`KUAL` 菜单和主题包
|
||||||
|
- 它不会回补 `prepare-storage` 阶段预置的原生二进制,例如 `next-wakeup`、`xh`
|
||||||
|
- 所以这条路径默认前提仍然是:先跑过 `prepare-storage`
|
||||||
|
|
||||||
注意:这条新机 bootstrap 方案当前仍未做“真机恢复出厂闭环验证”。
|
注意:这条新机 bootstrap 方案当前仍未做“真机恢复出厂闭环验证”。
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
@@ -176,6 +193,31 @@ npm run build
|
|||||||
npm run export:themes
|
npm run export:themes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Web 端定时生成与发布
|
||||||
|
|
||||||
|
如果 Web 服务器需要每小时自动生成最新背景图,并把结果放到固定静态目录,使用:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh scripts/publish-calendar-dist.sh --output-dir /path/to/web-root --once
|
||||||
|
```
|
||||||
|
|
||||||
|
这条命令会:
|
||||||
|
|
||||||
|
- 调用 `npm run export:themes` 生成最新 `kindlebg.png`、`themes/<theme>/<orientation>/kindlebg.png`
|
||||||
|
- 同步 `calendar/dist/` 整个目录到你指定的 Web 根目录
|
||||||
|
|
||||||
|
如果希望常驻运行、每 60 分钟自动重跑一次,去掉 `--once`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh scripts/publish-calendar-dist.sh --output-dir /path/to/web-root
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 当前导图依赖 `calendar/scripts/export-kindle-background.swift`,因此这条自动发布链路需要运行在 macOS 上
|
||||||
|
- 如果你的线上静态服务器是 Linux,只适合把“发布目录”指到该 Linux 机器挂载出来的目录,或者先在 macOS 上生成后再额外同步
|
||||||
|
- Web 服务器应该直接服务这个发布目录,而不是只服务 `vite build` 产出的 HTML;否则 `.png` 路径可能回落成 `index.html`
|
||||||
|
|
||||||
### 主题与截图
|
### 主题与截图
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -194,6 +236,10 @@ sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version late
|
|||||||
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
||||||
```
|
```
|
||||||
|
|
||||||
|
补充:
|
||||||
|
|
||||||
|
- 如果还要执行 `--start-dashboard`,默认同样要求设备上已经有 `prepare-storage` 预置的完整 dashboard 基础运行时
|
||||||
|
|
||||||
### SSH 恢复
|
### SSH 恢复
|
||||||
|
|
||||||
如果 Kindle 上已经有 `KTerm`,但外部 SSH 不通:
|
如果 Kindle 上已经有 `KTerm`,但外部 SSH 不通:
|
||||||
@@ -271,3 +317,6 @@ sh -n snapshot.sh
|
|||||||
3. `ssh kindle` 依赖当前网络和 IP;换 Wi‑Fi 后可能要重新确认地址或 SSH 配置
|
3. `ssh kindle` 依赖当前网络和 IP;换 Wi‑Fi 后可能要重新确认地址或 SSH 配置
|
||||||
4. `KUAL -> Dashboard` 与 `Dashboard -> 原生 UI/KUAL` 的边界切换在 `Kindle Voyage 5.13.6` 上仍不稳定
|
4. `KUAL -> Dashboard` 与 `Dashboard -> 原生 UI/KUAL` 的边界切换在 `Kindle Voyage 5.13.6` 上仍不稳定
|
||||||
5. `stop.sh` 当前必须走保守恢复路径,实验性的“快速切换”方案已在 `2026-03-17` 撤回
|
5. `stop.sh` 当前必须走保守恢复路径,实验性的“快速切换”方案已在 `2026-03-17` 撤回
|
||||||
|
6. Voyage 右上角系统状态栏当前仍可能短暂闪现,顶部遮罩还没有做到完全压住
|
||||||
|
7. 天气预报的位置当前使用 Kindle 侧 GeoIP 做“市级近似定位”,结果不保证精确到真实街道或精确城区
|
||||||
|
8. 当前架构下,分钟级时钟刷新需要设备被唤醒;而设备一旦被唤醒,屏幕/灯光会一起进入可视态,因此暂时做不到“不亮灯唤醒并肉眼可见更新时钟”
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ print_usage() {
|
|||||||
模式:
|
模式:
|
||||||
all 默认。能预置 USB 存储就先预置;能连 SSH 就继续做 SSH 后半段
|
all 默认。能预置 USB 存储就先预置;能连 SSH 就继续做 SSH 后半段
|
||||||
prepare-storage 只做 USB 存储预置
|
prepare-storage 只做 USB 存储预置
|
||||||
post-ssh 只做 SSH 打通后的自动化收尾
|
post-ssh 只做 SSH 打通后的自动化收尾;默认要求设备上已存在 prepare-storage 预置的 dashboard 基础运行时
|
||||||
|
|
||||||
选项:
|
选项:
|
||||||
-v, --volume <path> Kindle 挂载目录,默认 /Volumes/Kindle
|
-v, --volume <path> Kindle 挂载目录,默认 /Volumes/Kindle
|
||||||
@@ -39,8 +39,8 @@ print_usage() {
|
|||||||
--kterm-package <path> 指定 KTerm 安装包;官方 release 用 .zip,也兼容外部 .bin
|
--kterm-package <path> 指定 KTerm 安装包;官方 release 用 .zip,也兼容外部 .bin
|
||||||
--download-kterm 在 Mac 侧联网下载 KTerm 到 dash/staging/kterm/,再预置到 Kindle
|
--download-kterm 在 Mac 侧联网下载 KTerm 到 dash/staging/kterm/,再预置到 Kindle
|
||||||
--kterm-version <tag> 下载指定 KTerm 版本;默认 latest
|
--kterm-version <tag> 下载指定 KTerm 版本;默认 latest
|
||||||
--no-background SSH 阶段不同步后立即切主题出图
|
--no-background SSH 阶段同步后不立即切主题出图
|
||||||
--start-dashboard SSH 阶段额外后台启动 dashboard 主循环
|
--start-dashboard SSH 阶段额外后台启动 dashboard 主循环;默认要求设备上已有完整 dashboard 运行时
|
||||||
-h, --help 查看帮助
|
-h, --help 查看帮助
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
@@ -66,6 +66,11 @@ print_usage() {
|
|||||||
6. 对这台 Voyage 5.13.6,脚本默认优先选择“不带 armhf 后缀”的 zip。
|
6. 对这台 Voyage 5.13.6,脚本默认优先选择“不带 armhf 后缀”的 zip。
|
||||||
7. 如果脚本没有检测到 KTerm 包,会明确提示“这一步仍需手工补装 KTerm”。
|
7. 如果脚本没有检测到 KTerm 包,会明确提示“这一步仍需手工补装 KTerm”。
|
||||||
|
|
||||||
|
关于 post-ssh:
|
||||||
|
1. 它当前会同步 dashboard 的 shell 脚本、KUAL 菜单和主题包。
|
||||||
|
2. 它不会回补 prepare-storage 阶段预置的原生二进制,例如 next-wakeup、xh。
|
||||||
|
3. 因此最稳的用法仍然是:先跑 prepare-storage,完成设备侧步骤后,再跑 post-ssh。
|
||||||
|
|
||||||
推荐操作顺序:
|
推荐操作顺序:
|
||||||
|
|
||||||
阶段 A:先做 USB 预置
|
阶段 A:先做 USB 预置
|
||||||
@@ -86,12 +91,14 @@ print_usage() {
|
|||||||
1. 恢复出厂。
|
1. 恢复出厂。
|
||||||
2. 语言只选 English (United Kingdom)。
|
2. 语言只选 English (United Kingdom)。
|
||||||
3. 到 Wi-Fi 页面时,不要真的联网;先按 WatchThis 文档进入 demo mode。
|
3. 到 Wi-Fi 页面时,不要真的联网;先按 WatchThis 文档进入 demo mode。
|
||||||
4. 第一次出现 Add Content / Sideload Content 时,只点 Done,不要接 USB。
|
4. 搜索 ;demo,进入 demo menu -> Sideload Content。
|
||||||
5. 遇到 misconfiguration / Configure Device 时,执行隐藏手势回到可操作界面。
|
5. 到这个页面后再接 USB;因为阶段 A 已经预置好 .demo 内容,这里只需确认文件存在。
|
||||||
6. 再次进入 ;demo -> Sideload Content,这一次才是真正导入 payload 的时机。
|
6. 弹出 Kindle,在设备上点 Done,退出 demo menu。
|
||||||
因为阶段 A 已经预置好 .demo 内容,这里不需要再从 Mac 手工拷文件。
|
7. 进入 ;dsts -> Help & User Guides -> Get Started。
|
||||||
7. 退出 demo menu 后,进入 Help & User Guides -> Get Started 触发越狱。
|
8. 按设备提示继续完成 register this demo -> Skip -> standard。
|
||||||
8. 越狱成功后,Kindle 根目录应出现 mkk、libkh、rp。
|
9. 重启后如果看到 Configure Device,用隐藏手势回到主页。
|
||||||
|
10. 搜索 ;uzb,再接 USB,把 Update_hotfix_watchthis_custom.bin 放到根目录。
|
||||||
|
11. 弹出设备后,进入 ;dsts -> Device Options -> Update Your Kindle。
|
||||||
|
|
||||||
阶段 C:安装 KUAL / MRPI / USBNetwork / dashboard
|
阶段 C:安装 KUAL / MRPI / USBNetwork / dashboard
|
||||||
1. 在 Kindle 首页搜索:
|
1. 在 Kindle 首页搜索:
|
||||||
@@ -119,11 +126,12 @@ print_usage() {
|
|||||||
1. 执行:
|
1. 执行:
|
||||||
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
||||||
2. 预期结果:
|
2. 预期结果:
|
||||||
- 自动同步 dashboard 运行时和主题包
|
- 自动同步 dashboard shell 脚本、KUAL 菜单和主题包
|
||||||
- Kindle 立即切到 simple / portrait
|
- Kindle 立即切到 simple / portrait
|
||||||
- 屏幕马上显示背景和时钟
|
- 屏幕马上显示背景和时钟
|
||||||
3. 如果还希望后台常驻 dashboard 主循环,再执行:
|
3. 如果还希望后台常驻 dashboard 主循环,再执行:
|
||||||
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait --start-dashboard
|
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait --start-dashboard
|
||||||
|
这一步默认建立在 prepare-storage 已经把完整 dashboard 基础运行时放进设备上的前提上。
|
||||||
|
|
||||||
最省事的直接用法:
|
最省事的直接用法:
|
||||||
1. USB 挂载时先运行:
|
1. USB 挂载时先运行:
|
||||||
@@ -135,11 +143,12 @@ print_usage() {
|
|||||||
这时它会自动继续做 SSH 后半段。
|
这时它会自动继续做 SSH 后半段。
|
||||||
|
|
||||||
最容易做错的地方:
|
最容易做错的地方:
|
||||||
1. 第一次 Add Content / Sideload Content 只能点 Done,不能在这一步导 payload。
|
1. `Get Started` 只负责触发越狱脚本;真正退出 demo 状态还要再执行一次 `Update Your Kindle` 安装 custom hotfix。
|
||||||
2. 真正导 payload 的时机,是隐藏手势返回后,再次进入 ;demo -> Sideload Content。
|
2. `Update Your Kindle` 之前,先确认根目录里是真正的 `Update_hotfix_watchthis_custom.bin`,不要把 `._Update_hotfix_watchthis_custom.bin` 误当成正确文件。
|
||||||
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。
|
5. 如果本轮没有预置 KTerm 包,阶段 C 结束后仍需你手工补装 KTerm。
|
||||||
|
6. post-ssh 不是“从零补齐 dashboard”的入口;next-wakeup、xh 这类原生二进制仍来自 prepare-storage。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
sh bootstrap-new-kindle.sh prepare-storage
|
sh bootstrap-new-kindle.sh prepare-storage
|
||||||
@@ -465,7 +474,7 @@ print_post_ssh_summary() {
|
|||||||
|
|
||||||
SSH 阶段已完成:
|
SSH 阶段已完成:
|
||||||
- 主机:$HOST_TARGET
|
- 主机:$HOST_TARGET
|
||||||
- 已同步 dashboard 运行时与主题包
|
- 已同步 dashboard shell 脚本、KUAL 菜单与主题包
|
||||||
- 当前用于出图的主题:$RESOLVED_THEME_ID / $RESOLVED_ORIENTATION
|
- 当前用于出图的主题:$RESOLVED_THEME_ID / $RESOLVED_ORIENTATION
|
||||||
|
|
||||||
如果你要继续验证:
|
如果你要继续验证:
|
||||||
@@ -477,7 +486,7 @@ EOF
|
|||||||
|
|
||||||
post_ssh() {
|
post_ssh() {
|
||||||
if ! ssh_reachable; then
|
if ! ssh_reachable; then
|
||||||
echo "当前还无法通过 SSH 连接 $HOST_TARGET。" >&2
|
echo "当前还无法通过 SSH 连接 ${HOST_TARGET}。" >&2
|
||||||
echo "请先在 Kindle 上连上 Wi-Fi,并在 KTerm 执行 sh /mnt/us/ssh-force-dropbear-22.sh" >&2
|
echo "请先在 Kindle 上连上 Wi-Fi,并在 KTerm 执行 sh /mnt/us/ssh-force-dropbear-22.sh" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -485,7 +494,7 @@ post_ssh() {
|
|||||||
log_step "SSH" "修复设备侧 SSH 辅助脚本权限与 authorized_keys"
|
log_step "SSH" "修复设备侧 SSH 辅助脚本权限与 authorized_keys"
|
||||||
prepare_remote_helpers
|
prepare_remote_helpers
|
||||||
|
|
||||||
log_step "同步" "同步 dashboard 运行时和主题包到 Kindle"
|
log_step "同步" "同步 dashboard shell 脚本、KUAL 菜单和主题包到 Kindle"
|
||||||
sync_dashboard_runtime
|
sync_dashboard_runtime
|
||||||
|
|
||||||
if [ "$SHOW_BACKGROUND" = true ]; then
|
if [ "$SHOW_BACKGROUND" = true ]; then
|
||||||
|
|||||||
@@ -75,152 +75,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "paper",
|
|
||||||
"label": "Paper",
|
|
||||||
"preview": {
|
|
||||||
"pageBackground": "#f2eee5",
|
|
||||||
"paper": "#fcfaf4",
|
|
||||||
"panelBackground": "#fffdf8",
|
|
||||||
"frameStroke": "#7e6b57",
|
|
||||||
"frameStrokeStrong": "#5f5143",
|
|
||||||
"frameMuted": "rgba(126, 107, 87, 0.32)",
|
|
||||||
"mutedInk": "#5a5148",
|
|
||||||
"badgeFill": "#f3ede0",
|
|
||||||
"bodyFont": "'Songti SC', 'STSong', serif",
|
|
||||||
"displayFont": "'Baskerville', 'Times New Roman', 'Songti SC', serif",
|
|
||||||
"titleFont": "'Songti SC', 'STSong', serif",
|
|
||||||
"cardRadius": "1.7rem",
|
|
||||||
"panelRadius": "1.1rem"
|
|
||||||
},
|
|
||||||
"variants": {
|
|
||||||
"portrait": {
|
|
||||||
"devicePlacement": "logo_bottom",
|
|
||||||
"viewport": {
|
|
||||||
"width": 1072,
|
|
||||||
"height": 1448
|
|
||||||
},
|
|
||||||
"backgroundPath": "themes/paper/portrait/kindlebg.png",
|
|
||||||
"clock": {
|
|
||||||
"x": 347,
|
|
||||||
"y": 55,
|
|
||||||
"width": 220,
|
|
||||||
"height": 220,
|
|
||||||
"faceRadiusRatio": 0.47,
|
|
||||||
"faceStroke": 3,
|
|
||||||
"tickOuterInset": 6,
|
|
||||||
"majorTickLength": 14,
|
|
||||||
"minorTickLength": 7,
|
|
||||||
"majorTickThickness": 4,
|
|
||||||
"minorTickThickness": 2,
|
|
||||||
"hourLengthRatio": 0.48,
|
|
||||||
"minuteLengthRatio": 0.72,
|
|
||||||
"hourThickness": 9,
|
|
||||||
"minuteThickness": 5,
|
|
||||||
"centerRadius": 7
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"landscape": {
|
|
||||||
"devicePlacement": "logo_right",
|
|
||||||
"viewport": {
|
|
||||||
"width": 1448,
|
|
||||||
"height": 1072
|
|
||||||
},
|
|
||||||
"backgroundPath": "themes/paper/landscape/kindlebg.png",
|
|
||||||
"clock": {
|
|
||||||
"x": 659,
|
|
||||||
"y": 57,
|
|
||||||
"width": 220,
|
|
||||||
"height": 220,
|
|
||||||
"faceRadiusRatio": 0.47,
|
|
||||||
"faceStroke": 3,
|
|
||||||
"tickOuterInset": 6,
|
|
||||||
"majorTickLength": 14,
|
|
||||||
"minorTickLength": 7,
|
|
||||||
"majorTickThickness": 4,
|
|
||||||
"minorTickThickness": 2,
|
|
||||||
"hourLengthRatio": 0.48,
|
|
||||||
"minuteLengthRatio": 0.72,
|
|
||||||
"hourThickness": 9,
|
|
||||||
"minuteThickness": 5,
|
|
||||||
"centerRadius": 7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "classic",
|
|
||||||
"label": "Classic",
|
|
||||||
"preview": {
|
|
||||||
"pageBackground": "#ece6da",
|
|
||||||
"paper": "#ffffff",
|
|
||||||
"panelBackground": "#fefefe",
|
|
||||||
"frameStroke": "#3d352c",
|
|
||||||
"frameStrokeStrong": "#1f1a15",
|
|
||||||
"frameMuted": "rgba(61, 53, 44, 0.3)",
|
|
||||||
"mutedInk": "#3d352c",
|
|
||||||
"badgeFill": "#f3efe8",
|
|
||||||
"bodyFont": "'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', sans-serif",
|
|
||||||
"displayFont": "'Palatino Linotype', 'Book Antiqua', 'Songti SC', serif",
|
|
||||||
"titleFont": "'Palatino Linotype', 'Book Antiqua', 'Songti SC', serif",
|
|
||||||
"cardRadius": "1.25rem",
|
|
||||||
"panelRadius": "0.92rem"
|
|
||||||
},
|
|
||||||
"variants": {
|
|
||||||
"portrait": {
|
|
||||||
"devicePlacement": "logo_bottom",
|
|
||||||
"viewport": {
|
|
||||||
"width": 1072,
|
|
||||||
"height": 1448
|
|
||||||
},
|
|
||||||
"backgroundPath": "themes/classic/portrait/kindlebg.png",
|
|
||||||
"clock": {
|
|
||||||
"x": 347,
|
|
||||||
"y": 55,
|
|
||||||
"width": 220,
|
|
||||||
"height": 220,
|
|
||||||
"faceRadiusRatio": 0.47,
|
|
||||||
"faceStroke": 3,
|
|
||||||
"tickOuterInset": 6,
|
|
||||||
"majorTickLength": 14,
|
|
||||||
"minorTickLength": 7,
|
|
||||||
"majorTickThickness": 4,
|
|
||||||
"minorTickThickness": 2,
|
|
||||||
"hourLengthRatio": 0.48,
|
|
||||||
"minuteLengthRatio": 0.72,
|
|
||||||
"hourThickness": 9,
|
|
||||||
"minuteThickness": 5,
|
|
||||||
"centerRadius": 7
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"landscape": {
|
|
||||||
"devicePlacement": "logo_right",
|
|
||||||
"viewport": {
|
|
||||||
"width": 1448,
|
|
||||||
"height": 1072
|
|
||||||
},
|
|
||||||
"backgroundPath": "themes/classic/landscape/kindlebg.png",
|
|
||||||
"clock": {
|
|
||||||
"x": 659,
|
|
||||||
"y": 57,
|
|
||||||
"width": 220,
|
|
||||||
"height": 220,
|
|
||||||
"faceRadiusRatio": 0.47,
|
|
||||||
"faceStroke": 3,
|
|
||||||
"tickOuterInset": 6,
|
|
||||||
"majorTickLength": 14,
|
|
||||||
"minorTickLength": 7,
|
|
||||||
"majorTickThickness": 4,
|
|
||||||
"minorTickThickness": 2,
|
|
||||||
"hourLengthRatio": 0.48,
|
|
||||||
"minuteLengthRatio": 0.72,
|
|
||||||
"hourThickness": 9,
|
|
||||||
"minuteThickness": 5,
|
|
||||||
"centerRadius": 7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "simple",
|
"id": "simple",
|
||||||
"label": "Simple",
|
"label": "Simple",
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ set -eu
|
|||||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)"
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)"
|
||||||
CALENDAR_DIR="$ROOT_DIR/calendar"
|
CALENDAR_DIR="$ROOT_DIR/calendar"
|
||||||
DIST_DIR="$CALENDAR_DIR/dist"
|
DIST_DIR="$CALENDAR_DIR/dist"
|
||||||
|
KINDLE_BACKGROUNDS_DIR="$CALENDAR_DIR/kindle-backgrounds"
|
||||||
PORT=${PORT:-4173}
|
PORT=${PORT:-4173}
|
||||||
SWIFT_SCRIPT="$CALENDAR_DIR/scripts/export-kindle-background.swift"
|
SWIFT_SCRIPT="$CALENDAR_DIR/scripts/export-kindle-background.swift"
|
||||||
THEMES_SOURCE="$CALENDAR_DIR/config/themes.json"
|
THEMES_SOURCE="$CALENDAR_DIR/config/themes.json"
|
||||||
|
|
||||||
THEME_FILTER=""
|
THEME_FILTER=""
|
||||||
ORIENTATION_FILTER=""
|
ORIENTATION_FILTER=""
|
||||||
|
LOCATION_LAT=""
|
||||||
|
LOCATION_LON=""
|
||||||
|
|
||||||
print_usage() {
|
print_usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
@@ -19,12 +22,15 @@ print_usage() {
|
|||||||
选项:
|
选项:
|
||||||
--theme <theme-id> 只导出指定主题
|
--theme <theme-id> 只导出指定主题
|
||||||
--orientation <value> 只导出指定方向;必须和 --theme 一起使用
|
--orientation <value> 只导出指定方向;必须和 --theme 一起使用
|
||||||
|
--location-lat <value> 导图时显式覆盖天气定位纬度
|
||||||
|
--location-lon <value> 导图时显式覆盖天气定位经度
|
||||||
-h, --help 查看帮助
|
-h, --help 查看帮助
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
sh scripts/export-theme-backgrounds.sh
|
sh scripts/export-theme-backgrounds.sh
|
||||||
sh scripts/export-theme-backgrounds.sh --theme simple
|
sh scripts/export-theme-backgrounds.sh --theme simple
|
||||||
sh scripts/export-theme-backgrounds.sh --theme simple --orientation portrait
|
sh scripts/export-theme-backgrounds.sh --theme simple --orientation portrait
|
||||||
|
sh scripts/export-theme-backgrounds.sh --location-lat 30.274084 --location-lon 120.15507
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +44,14 @@ while [ "$#" -gt 0 ]; do
|
|||||||
shift
|
shift
|
||||||
ORIENTATION_FILTER=${1:?"missing orientation"}
|
ORIENTATION_FILTER=${1:?"missing orientation"}
|
||||||
;;
|
;;
|
||||||
|
--location-lat)
|
||||||
|
shift
|
||||||
|
LOCATION_LAT=${1:?"missing location latitude"}
|
||||||
|
;;
|
||||||
|
--location-lon)
|
||||||
|
shift
|
||||||
|
LOCATION_LON=${1:?"missing location longitude"}
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
print_usage
|
print_usage
|
||||||
exit 0
|
exit 0
|
||||||
@@ -57,6 +71,11 @@ if [ -n "$ORIENTATION_FILTER" ] && [ -z "$THEME_FILTER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if { [ -n "$LOCATION_LAT" ] && [ -z "$LOCATION_LON" ]; } || { [ -z "$LOCATION_LAT" ] && [ -n "$LOCATION_LON" ]; }; then
|
||||||
|
echo "--location-lat 和 --location-lon 必须同时提供。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
selection_output=$(
|
selection_output=$(
|
||||||
node --input-type=module -e "
|
node --input-type=module -e "
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@@ -143,6 +162,7 @@ fi
|
|||||||
|
|
||||||
cd "$CALENDAR_DIR"
|
cd "$CALENDAR_DIR"
|
||||||
npm run build >/dev/null
|
npm run build >/dev/null
|
||||||
|
mkdir -p "$KINDLE_BACKGROUNDS_DIR"
|
||||||
|
|
||||||
python3 -m http.server "$PORT" -d "$DIST_DIR" >/tmp/kindle-calendar-http.log 2>&1 &
|
python3 -m http.server "$PORT" -d "$DIST_DIR" >/tmp/kindle-calendar-http.log 2>&1 &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
@@ -153,8 +173,15 @@ sleep 1
|
|||||||
printf '%s\n' "$EXPORT_ITEMS" | while IFS="$(printf '\t')" read -r theme_id orientation background_path; do
|
printf '%s\n' "$EXPORT_ITEMS" | while IFS="$(printf '\t')" read -r theme_id orientation background_path; do
|
||||||
out_png="$DIST_DIR/$background_path"
|
out_png="$DIST_DIR/$background_path"
|
||||||
out_region="${out_png%.png}.clock-region.json"
|
out_region="${out_png%.png}.clock-region.json"
|
||||||
|
flat_background_png="$KINDLE_BACKGROUNDS_DIR/${theme_id}-${orientation}.png"
|
||||||
url="http://127.0.0.1:$PORT/?mode=background&theme=$theme_id&orientation=$orientation"
|
url="http://127.0.0.1:$PORT/?mode=background&theme=$theme_id&orientation=$orientation"
|
||||||
|
if [ -n "$LOCATION_LAT" ] && [ -n "$LOCATION_LON" ]; then
|
||||||
|
url="${url}&location-lat=${LOCATION_LAT}&location-lon=${LOCATION_LON}"
|
||||||
|
fi
|
||||||
/usr/bin/swift "$SWIFT_SCRIPT" "$url" "$out_png" "$out_region" >/dev/null
|
/usr/bin/swift "$SWIFT_SCRIPT" "$url" "$out_png" "$out_region" >/dev/null
|
||||||
|
# Web 侧额外维护一份扁平命名的背景图目录,方便 nginx 单独暴露给 Kindle 拉图。
|
||||||
|
# 主题 JSON 会把 background.url 指向这里,例如 /kindle-backgrounds/simple-portrait.png。
|
||||||
|
cp "$out_png" "$flat_background_png"
|
||||||
|
|
||||||
# 根目录的 kindlebg.png / clock-region.json 只给默认主题兜底使用。
|
# 根目录的 kindlebg.png / clock-region.json 只给默认主题兜底使用。
|
||||||
# 定向导出其它主题时不覆盖它,避免把默认主题的运行时入口意外改掉。
|
# 定向导出其它主题时不覆盖它,避免把默认主题的运行时入口意外改掉。
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ const clockRegionPath = path.join(distDir, 'clock-region.json');
|
|||||||
const themesSourcePath = path.resolve(currentDir, '../config/themes.json');
|
const themesSourcePath = path.resolve(currentDir, '../config/themes.json');
|
||||||
const themesDistPath = path.join(distDir, 'themes.json');
|
const themesDistPath = path.join(distDir, 'themes.json');
|
||||||
const themesDir = path.join(distDir, 'themes');
|
const themesDir = path.join(distDir, 'themes');
|
||||||
|
const kindleBackgroundsDir = path.resolve(currentDir, '../kindle-backgrounds');
|
||||||
const dashboardBaseUrl = 'https://shell.biboer.cn:20001';
|
const dashboardBaseUrl = 'https://shell.biboer.cn:20001';
|
||||||
|
|
||||||
const themesSource = JSON.parse(fs.readFileSync(themesSourcePath, 'utf8'));
|
const themesSource = JSON.parse(fs.readFileSync(themesSourcePath, 'utf8'));
|
||||||
const generatedAt = new Date().toISOString();
|
const generatedAt = new Date().toISOString();
|
||||||
const defaultVariant = themesSource.themes.find((theme) => theme.id === themesSource.defaultThemeId)?.variants?.[themesSource.defaultOrientation];
|
const defaultTheme = themesSource.themes.find((theme) => theme.id === themesSource.defaultThemeId);
|
||||||
const defaultDeviceClock = defaultVariant ? toDeviceClock(defaultVariant, themesSource.defaultOrientation) : null;
|
const defaultVariant = defaultTheme?.variants?.[themesSource.defaultOrientation];
|
||||||
|
const defaultDeviceClock = defaultVariant ? buildRuntimeClock(defaultTheme.id, themesSource.defaultOrientation, defaultVariant) : null;
|
||||||
const defaultClockRegion = defaultVariant
|
const defaultClockRegion = defaultVariant
|
||||||
? {
|
? {
|
||||||
x: defaultDeviceClock.x,
|
x: defaultDeviceClock.x,
|
||||||
@@ -95,30 +97,74 @@ function toDeviceClock(variant, orientation) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveVariantClock(themeId, orientation, variant) {
|
||||||
|
const regionPath = path.join(distDir, 'themes', themeId, orientation, 'kindlebg.clock-region.json');
|
||||||
|
const exportedRegion = fs.existsSync(regionPath)
|
||||||
|
? JSON.parse(fs.readFileSync(regionPath, 'utf8'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...variant,
|
||||||
|
clock: {
|
||||||
|
...variant.clock,
|
||||||
|
...(exportedRegion
|
||||||
|
? {
|
||||||
|
x: exportedRegion.x,
|
||||||
|
y: exportedRegion.y,
|
||||||
|
width: exportedRegion.width,
|
||||||
|
height: exportedRegion.height,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRuntimeClock(themeId, orientation, variant) {
|
||||||
|
const resolvedVariant = resolveVariantClock(themeId, orientation, variant);
|
||||||
|
const hasExportedRegion =
|
||||||
|
resolvedVariant.clock.x !== variant.clock.x ||
|
||||||
|
resolvedVariant.clock.y !== variant.clock.y ||
|
||||||
|
resolvedVariant.clock.width !== variant.clock.width ||
|
||||||
|
resolvedVariant.clock.height !== variant.clock.height;
|
||||||
|
|
||||||
|
if (orientation === 'landscape' && hasExportedRegion) {
|
||||||
|
return {
|
||||||
|
...resolvedVariant.clock,
|
||||||
|
rotationDegrees: 90,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return toDeviceClock(resolvedVariant, orientation);
|
||||||
|
}
|
||||||
|
|
||||||
function buildThemeConfig(theme) {
|
function buildThemeConfig(theme) {
|
||||||
return {
|
return {
|
||||||
id: theme.id,
|
id: theme.id,
|
||||||
label: theme.label,
|
label: theme.label,
|
||||||
updatedAt: generatedAt,
|
updatedAt: generatedAt,
|
||||||
variants: Object.fromEntries(
|
variants: Object.fromEntries(
|
||||||
Object.entries(theme.variants).map(([orientation, variant]) => [
|
Object.entries(theme.variants).map(([orientation, variant]) => {
|
||||||
orientation,
|
return [
|
||||||
{
|
orientation,
|
||||||
devicePlacement: variant.devicePlacement,
|
{
|
||||||
background: {
|
devicePlacement: variant.devicePlacement,
|
||||||
path: variant.backgroundPath,
|
background: {
|
||||||
url: `${dashboardBaseUrl}/${variant.backgroundPath}`,
|
// Kindle 端统一走扁平目录,避免设备侧自己拼主题子目录规则。
|
||||||
refreshIntervalMinutes: 120,
|
path: `kindle-backgrounds/${theme.id}-${orientation}.png`,
|
||||||
|
url: `${dashboardBaseUrl}/kindle-backgrounds/${theme.id}-${orientation}.png`,
|
||||||
|
refreshIntervalMinutes: 120,
|
||||||
|
},
|
||||||
|
clock: buildRuntimeClock(theme.id, orientation, variant),
|
||||||
},
|
},
|
||||||
clock: toDeviceClock(variant, orientation),
|
];
|
||||||
},
|
}),
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(distDir, { recursive: true });
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
fs.mkdirSync(themesDir, { recursive: true });
|
fs.mkdirSync(themesDir, { recursive: true });
|
||||||
|
fs.mkdirSync(kindleBackgroundsDir, { recursive: true });
|
||||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
||||||
fs.writeFileSync(themesDistPath, `${JSON.stringify(themesIndex, null, 2)}\n`, 'utf8');
|
fs.writeFileSync(themesDistPath, `${JSON.stringify(themesIndex, null, 2)}\n`, 'utf8');
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ function subLabelTone(cell: CalendarModel['cells'][number]) {
|
|||||||
<p class="calendar-card__weekday">{{ model.weekdayLabel }}</p>
|
<p class="calendar-card__weekday">{{ model.weekdayLabel }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AnalogClock :date="date" :mode="mode" :size="220" />
|
<div class="calendar-card__clock-wrap">
|
||||||
|
<AnalogClock :date="date" :mode="mode" :size="220" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendar-card__panel">
|
<div class="calendar-card__panel">
|
||||||
@@ -110,6 +112,11 @@ function subLabelTone(cell: CalendarModel['cells'][number]) {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-card__clock-wrap {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-card__headline {
|
.calendar-card__headline {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SimpleAnalogClock :date="date" :mode="mode" :size="480" />
|
<div class="simple-dashboard__clock-wrap simple-dashboard__clock-wrap--portrait">
|
||||||
|
<SimpleAnalogClock :date="date" :mode="mode" :size="480" />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="simple-calendar simple-calendar--portrait">
|
<section class="simple-calendar simple-calendar--portrait">
|
||||||
@@ -176,7 +178,9 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<SimpleAnalogClock :date="date" :mode="mode" :size="480" />
|
<div class="simple-dashboard__clock-wrap">
|
||||||
|
<SimpleAnalogClock :date="date" :mode="mode" :size="480" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="simple-dashboard__column simple-dashboard__column--right">
|
<div class="simple-dashboard__column simple-dashboard__column--right">
|
||||||
@@ -254,7 +258,8 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
height: 480px;
|
height: 480px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 0 16px;
|
/* 竖版顶部区按 Figma 节点 284:7 额外下压 32px。 */
|
||||||
|
padding: 32px 16px 0;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
@@ -275,6 +280,17 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.simple-dashboard__clock-wrap {
|
||||||
|
width: 480px;
|
||||||
|
height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-dashboard__clock-wrap--portrait {
|
||||||
|
/* simple 竖版时钟整体下移 16px,和顶部信息区拉开间距。 */
|
||||||
|
padding-top: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.simple-dashboard__summary {
|
.simple-dashboard__summary {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -304,6 +320,10 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 横版日期标题组需要保留左侧 24px 对齐边距。 */
|
||||||
|
padding-left: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-dashboard__day {
|
.simple-dashboard__day {
|
||||||
@@ -370,7 +390,10 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
width: 480px;
|
width: 480px;
|
||||||
|
box-sizing: border-box;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
/* 横版地点行比标题再向右缩进一档。 */
|
||||||
|
padding-left: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-dashboard__location-icon {
|
.simple-dashboard__location-icon {
|
||||||
@@ -395,7 +418,7 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.simple-dashboard__location--landscape {
|
.simple-dashboard__location--landscape {
|
||||||
width: 424px;
|
flex: 1 1 auto;
|
||||||
font-size: 56px;
|
font-size: 56px;
|
||||||
line-height: 57px;
|
line-height: 57px;
|
||||||
}
|
}
|
||||||
@@ -406,7 +429,10 @@ function isCompactCalendarLabel(label: string) {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
width: 480px;
|
width: 480px;
|
||||||
height: 99px;
|
height: 99px;
|
||||||
|
box-sizing: border-box;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
/* 横版天气摘要整体向右留出 24px,与 Figma 左列对齐。 */
|
||||||
|
padding-left: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-dashboard__metric {
|
.simple-dashboard__metric {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { computed } from 'vue';
|
|||||||
import WeatherGlyph from './WeatherGlyph.vue';
|
import WeatherGlyph from './WeatherGlyph.vue';
|
||||||
import {
|
import {
|
||||||
HUMIDITY_ICON_ASSET,
|
HUMIDITY_ICON_ASSET,
|
||||||
|
PM25_ICON_ASSET,
|
||||||
SUNRISE_ICON_ASSET,
|
SUNRISE_ICON_ASSET,
|
||||||
SUNSET_ICON_ASSET,
|
SUNSET_ICON_ASSET,
|
||||||
VISIBILITY_ICON_ASSET,
|
VISIBILITY_ICON_ASSET,
|
||||||
@@ -45,7 +46,7 @@ const metrics = computed(() => {
|
|||||||
? '暂无'
|
? '暂无'
|
||||||
: `${props.weather.aqi}${props.weather.aqiLabel}`,
|
: `${props.weather.aqi}${props.weather.aqiLabel}`,
|
||||||
accent: 'metric-pill--air',
|
accent: 'metric-pill--air',
|
||||||
icon: null,
|
icon: PM25_ICON_ASSET,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '能见度',
|
label: '能见度',
|
||||||
@@ -131,7 +132,6 @@ function forecastKind(day: ForecastDay) {
|
|||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span v-else class="metric-pill__dot" />
|
|
||||||
<span>{{ metric.label }}</span>
|
<span>{{ metric.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="metric-pill__value">{{ metric.value }}</p>
|
<p class="metric-pill__value">{{ metric.value }}</p>
|
||||||
@@ -245,14 +245,6 @@ function forecastKind(day: ForecastDay) {
|
|||||||
filter: brightness(0) saturate(100%);
|
filter: brightness(0) saturate(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-pill__dot {
|
|
||||||
width: 0.42rem;
|
|
||||||
height: 0.42rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weather-card__forecast {
|
.weather-card__forecast {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import lightRainIcon from '../../../assets/小雨.svg';
|
|||||||
import nightIcon from '../../../assets/晚上.svg';
|
import nightIcon from '../../../assets/晚上.svg';
|
||||||
import clearIcon from '../../../assets/晴天.svg';
|
import clearIcon from '../../../assets/晴天.svg';
|
||||||
import humidityIcon from '../../../assets/湿度.svg';
|
import humidityIcon from '../../../assets/湿度.svg';
|
||||||
|
import pm25Icon from '../../../assets/simple/pm25.svg';
|
||||||
import visibilityIcon from '../../../assets/能见度.svg';
|
import visibilityIcon from '../../../assets/能见度.svg';
|
||||||
import sleetIcon from '../../../assets/雨夹雪.svg';
|
import sleetIcon from '../../../assets/雨夹雪.svg';
|
||||||
import windSpeedIcon from '../../../assets/风速.svg';
|
import windSpeedIcon from '../../../assets/风速.svg';
|
||||||
@@ -79,3 +80,4 @@ export const WIND_SPEED_ICON_ASSET = windSpeedIcon;
|
|||||||
export const SUNRISE_ICON_ASSET = sunriseIcon;
|
export const SUNRISE_ICON_ASSET = sunriseIcon;
|
||||||
export const SUNSET_ICON_ASSET = sunsetIcon;
|
export const SUNSET_ICON_ASSET = sunsetIcon;
|
||||||
export const VISIBILITY_ICON_ASSET = visibilityIcon;
|
export const VISIBILITY_ICON_ASSET = visibilityIcon;
|
||||||
|
export const PM25_ICON_ASSET = pm25Icon;
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export interface WeatherSnapshot {
|
|||||||
aqiLabel: string;
|
aqiLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SearchLocationOverride {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_LOCATION: LocationCoordinates = {
|
const DEFAULT_LOCATION: LocationCoordinates = {
|
||||||
latitude: 30.274084,
|
latitude: 30.274084,
|
||||||
longitude: 120.15507,
|
longitude: 120.15507,
|
||||||
@@ -193,7 +198,34 @@ async function reverseGeocodeLocation(latitude: number, longitude: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveLocation(): Promise<LocationCoordinates> {
|
function parseSearchLocationOverride(search: string): SearchLocationOverride | null {
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
const latitude = Number(params.get('location-lat'));
|
||||||
|
const longitude = Number(params.get('location-lon'));
|
||||||
|
|
||||||
|
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveLocation(search = window.location.search): Promise<LocationCoordinates> {
|
||||||
|
const searchOverride = parseSearchLocationOverride(search);
|
||||||
|
|
||||||
|
if (searchOverride) {
|
||||||
|
const label = await reverseGeocodeLocation(searchOverride.latitude, searchOverride.longitude);
|
||||||
|
|
||||||
|
return {
|
||||||
|
latitude: searchOverride.latitude,
|
||||||
|
longitude: searchOverride.longitude,
|
||||||
|
label: label ?? '当前位置',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!('geolocation' in navigator)) {
|
if (!('geolocation' in navigator)) {
|
||||||
return DEFAULT_LOCATION;
|
return DEFAULT_LOCATION;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,86 @@ img {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait {
|
||||||
|
/* default 纵版给底部额外留出 32px,避免鸡汤卡片继续贴底溢出。 */
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.3rem 1.3rem 32px;
|
||||||
|
grid-template-rows: minmax(0, 1fr) 232px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .calendar-card {
|
||||||
|
/* 顶部日历区整体压紧一点,把高度让给底部鸡汤。 */
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .calendar-card__hero {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 272px;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .calendar-card__day {
|
||||||
|
font-size: calc(5.9rem * var(--theme-font-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .calendar-card__lunar-day,
|
||||||
|
.page-shell--default .dashboard-grid--portrait .calendar-card__weekday {
|
||||||
|
font-size: calc(1.64rem * var(--theme-font-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .calendar-card__clock-wrap {
|
||||||
|
width: 272px;
|
||||||
|
height: 272px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .calendar-card__clock-wrap :is(.analog-clock) {
|
||||||
|
transform: scale(1.2363636364);
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .weather-card {
|
||||||
|
/* 纵版天气卡压缩预报和指标区,给底部鸡汤腾空间。 */
|
||||||
|
grid-template-rows: auto minmax(0, 1.08fr) minmax(0, 0.72fr) minmax(0, 0.82fr);
|
||||||
|
gap: 0.54rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .weather-card__hero {
|
||||||
|
gap: 0.62rem;
|
||||||
|
padding: 0.7rem 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .weather-card__forecast {
|
||||||
|
gap: 0.28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .forecast-pill {
|
||||||
|
padding: 0.26rem 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .forecast-pill__label {
|
||||||
|
font-size: calc(0.82rem * var(--theme-font-scale, 1) * var(--forecast-pill-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .forecast-pill__temp {
|
||||||
|
font-size: calc(1rem * var(--theme-font-scale, 1) * var(--forecast-pill-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .weather-card__fact-icon,
|
||||||
|
.page-shell--default .dashboard-grid--portrait .metric-pill__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--portrait .quote-card__icon {
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .weather-card__hero-main .glyph--large {
|
||||||
|
width: 5.6rem;
|
||||||
|
height: 5.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-grid--landscape {
|
.dashboard-grid--landscape {
|
||||||
grid-template-columns: minmax(0, 1.24fr) minmax(21rem, 0.76fr);
|
grid-template-columns: minmax(0, 1.24fr) minmax(21rem, 0.76fr);
|
||||||
grid-template-rows: minmax(0, 1fr) 216px;
|
grid-template-rows: minmax(0, 1fr) 216px;
|
||||||
@@ -196,6 +276,56 @@ img {
|
|||||||
font-size: calc(6.2rem * var(--theme-font-scale, 1));
|
font-size: calc(6.2rem * var(--theme-font-scale, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card {
|
||||||
|
/* default 横版顶部整体下压,避开 Kindle 右上角状态栏遮罩。 */
|
||||||
|
gap: 32px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card__hero {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
|
align-items: end;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card__headline {
|
||||||
|
padding-left: 48px;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card__day {
|
||||||
|
font-size: calc(9rem * var(--theme-font-scale, 1));
|
||||||
|
line-height: 0.78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card__lunar-day,
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card__weekday {
|
||||||
|
font-size: calc(2.75rem * var(--theme-font-scale, 1));
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card__clock-wrap {
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .calendar-card__clock-wrap :is(.analog-clock) {
|
||||||
|
transform: scale(1.4545454545);
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .weather-card__fact-icon,
|
||||||
|
.page-shell--default .dashboard-grid--landscape .metric-pill__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell--default .dashboard-grid--landscape .quote-card__icon {
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-grid--landscape .weather-card {
|
.dashboard-grid--landscape .weather-card {
|
||||||
grid-template-rows: auto minmax(0, 1fr) minmax(0, 0.86fr) minmax(0, 0.9fr);
|
grid-template-rows: auto minmax(0, 1fr) minmax(0, 0.86fr) minmax(0, 0.9fr);
|
||||||
gap: 0.58rem;
|
gap: 0.58rem;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "Kindle Dashboard",
|
"name": "主题选择",
|
||||||
"priority": -998,
|
"priority": -998,
|
||||||
"items": [
|
"items": [
|
||||||
{"name": "Default", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default", "exitmenu": true},
|
{"name": "default-横屏", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default landscape", "exitmenu": true},
|
||||||
{"name": "Paper", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "paper", "exitmenu": true},
|
{"name": "default-竖屏", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default portrait", "exitmenu": true},
|
||||||
{"name": "Classic", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "classic", "exitmenu": true}
|
{"name": "simple-横屏", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "simple landscape", "exitmenu": true},
|
||||||
|
{"name": "simple-竖屏", "priority": 4, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "simple portrait", "exitmenu": true}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
{"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ If you're using KUAL you can use simple extension to start this Dashboard
|
|||||||
For on-device debugging without suspending the Kindle, set `DISABLE_SYSTEM_SUSPEND=true` in `local/env.sh`.
|
For on-device debugging without suspending the Kindle, set `DISABLE_SYSTEM_SUSPEND=true` in `local/env.sh`.
|
||||||
The dashboard loop will keep running, skip the `sleeping.png` branch, and use a normal `sleep` between refreshes instead of writing to `/sys/power/state`.
|
The dashboard loop will keep running, skip the `sleeping.png` branch, and use a normal `sleep` between refreshes instead of writing to `/sys/power/state`.
|
||||||
|
|
||||||
|
运行规则:
|
||||||
|
|
||||||
|
* 无论 `debug on` 还是 `debug off`,设备侧时钟都按分钟调度刷新一次。
|
||||||
|
* `DEBUG=true` 或 `DISABLE_SYSTEM_SUSPEND=true` 时,设备不进入真 suspend,dashboard 保持可见,并持续按分钟刷新。
|
||||||
|
* 普通低功耗模式下,设备仍会按分钟唤醒并刷新时钟,但常规分钟刷新只更新后台缓存,不主动把时钟刷到可视屏幕。
|
||||||
|
* 手动短按 `power` 的行为单独处理:dashboard 会保持 5 分钟可视窗口,并在窗口内继续按分钟刷新,窗口结束后回到普通低功耗循环。
|
||||||
|
* 通过 KUAL 启动 dashboard 或切换主题后回到 calendar,也会给同样的 5 分钟可视窗口。
|
||||||
|
* 分钟级时钟刷新完全在本机完成;背景图和主题资源同步仍保持低频。
|
||||||
|
|
||||||
If you're connected over SSH you can also run `DEBUG=true ./start.sh` to keep the process in the foreground with shell tracing enabled.
|
If you're connected over SSH you can also run `DEBUG=true ./start.sh` to keep the process in the foreground with shell tracing enabled.
|
||||||
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`.
|
||||||
@@ -70,6 +79,7 @@ On Voyage 5.13.6, if `stop.sh` finishes but the home UI is still missing, the cu
|
|||||||
* This code periodically downloads a dashboard background image from an HTTP(s) endpoint.
|
* This code periodically downloads a dashboard background image from an HTTP(s) endpoint.
|
||||||
* The interval can be configured in `dist/local/env.sh` using a cron expression.
|
* The interval can be configured in `dist/local/env.sh` using a cron expression.
|
||||||
* When the layered clock renderer is enabled, the Kindle re-renders the clock region locally every minute.
|
* When the layered clock renderer is enabled, the Kindle re-renders the clock region locally every minute.
|
||||||
|
* 普通模式下,常规分钟刷新只更新后台缓存,不主动把时钟刷到可视屏幕;手动短按 `power` 或从 KUAL 回到 calendar,会打开一个 5 分钟的可视窗口,结束后再回到普通循环。
|
||||||
* During the update intervals the device is suspended to RAM to save power.
|
* During the update intervals the device is suspended to RAM to save power.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
@@ -80,11 +80,15 @@ sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version v2.6
|
|||||||
|
|
||||||
### 4. WatchThis 导入点
|
### 4. WatchThis 导入点
|
||||||
|
|
||||||
必须特别注意:
|
按这轮实机成功路径,直接记录下面这组顺序:
|
||||||
|
|
||||||
1. 第一次 `Add Content / Sideload Content` 只能点 `Done`
|
1. 搜索 `;demo`
|
||||||
2. 不要在第一次导入点接 USB
|
2. 进入 `demo menu`
|
||||||
3. 真正的 payload 导入点,是隐藏手势返回后,再次进入 `;demo -> Sideload Content`
|
3. 选择 `Sideload Content`
|
||||||
|
4. 到这个页面后再接 USB
|
||||||
|
5. 确认 `.demo/KV-5.13.6.zip`、`.demo/demo.json`、`.demo/goodreads/` 都在
|
||||||
|
6. 弹出设备后在 Kindle 上点 `Done`
|
||||||
|
7. 退出 `demo menu`
|
||||||
|
|
||||||
这里的详细说明看:
|
这里的详细说明看:
|
||||||
|
|
||||||
@@ -93,19 +97,43 @@ sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version v2.6
|
|||||||
|
|
||||||
因为 bootstrap 已经把 `.demo` payload 预置好了,这一轮不需要你在导入点再手工从 Mac 拷 `KV-5.13.6.zip`。
|
因为 bootstrap 已经把 `.demo` payload 预置好了,这一轮不需要你在导入点再手工从 Mac 拷 `KV-5.13.6.zip`。
|
||||||
|
|
||||||
### 5. 触发越狱并验收
|
### 5. 触发越狱脚本
|
||||||
|
|
||||||
完成 `Get Started` 后,设备会重启并执行越狱脚本。
|
接下来按这个顺序走:
|
||||||
|
|
||||||
验收标准:
|
1. `;dsts -> Help & User Guides -> Get Started`
|
||||||
|
2. `register this demo` 三个输入框都填 `111`
|
||||||
|
3. `Fetching available demo types` 选 `Skip`
|
||||||
|
4. demo type 选 `standard`
|
||||||
|
5. 设备重启
|
||||||
|
6. 落到 `Configure Device` 时,不要点进去,用隐藏手势回到主页
|
||||||
|
|
||||||
- Kindle 用户存储根目录出现 `mkk`
|
这里不要急着跑 `;log mrpi`,因为还差 custom hotfix 这一步。
|
||||||
- Kindle 用户存储根目录出现 `libkh`
|
|
||||||
- Kindle 用户存储根目录出现 `rp`
|
|
||||||
|
|
||||||
如果没有这三个目录,说明这轮 `WatchThis` 没真正落地,不要继续往后做。
|
### 6. 安装 WatchThis custom hotfix
|
||||||
|
|
||||||
### 6. 安装 KUAL / MRPI / USBNetwork / dashboard / KTerm
|
回到主页后:
|
||||||
|
|
||||||
|
1. 搜索 `;uzb`
|
||||||
|
2. 接 USB
|
||||||
|
3. 确认 Kindle 根目录存在 `Update_hotfix_watchthis_custom.bin`
|
||||||
|
4. 安全弹出 Kindle
|
||||||
|
5. 搜索 `;dsts`
|
||||||
|
6. 进入 `Device Options`
|
||||||
|
7. 点 `Update Your Kindle`
|
||||||
|
8. 等设备重启回主页
|
||||||
|
|
||||||
|
如果这里报 `Update Error 2`,第一件事先检查根目录里是否真的有:
|
||||||
|
|
||||||
|
- `Update_hotfix_watchthis_custom.bin`
|
||||||
|
|
||||||
|
不要把 macOS 生成的:
|
||||||
|
|
||||||
|
- `._Update_hotfix_watchthis_custom.bin`
|
||||||
|
|
||||||
|
误当成真正的 hotfix 文件。
|
||||||
|
|
||||||
|
### 7. 安装 KUAL / MRPI / USBNetwork / dashboard / KTerm
|
||||||
|
|
||||||
回到首页后:
|
回到首页后:
|
||||||
|
|
||||||
@@ -125,7 +153,7 @@ Rename OTA Binaries -> Rename
|
|||||||
|
|
||||||
## Wi‑Fi 和 SSH 验证
|
## Wi‑Fi 和 SSH 验证
|
||||||
|
|
||||||
### 7. 接回 Wi‑Fi
|
### 8. 接回 Wi‑Fi
|
||||||
|
|
||||||
让 Kindle 连到和 Mac 同一个主 Wi‑Fi。
|
让 Kindle 连到和 Mac 同一个主 Wi‑Fi。
|
||||||
|
|
||||||
@@ -134,7 +162,7 @@ Rename OTA Binaries -> Rename
|
|||||||
- Guest Wi‑Fi
|
- Guest Wi‑Fi
|
||||||
- 开了客户端隔离的网络
|
- 开了客户端隔离的网络
|
||||||
|
|
||||||
### 8. 在 KTerm 里拉起 DropBear
|
### 9. 在 KTerm 里拉起 DropBear
|
||||||
|
|
||||||
打开 `KTerm`,执行:
|
打开 `KTerm`,执行:
|
||||||
|
|
||||||
@@ -151,7 +179,7 @@ sh /mnt/us/ssh-force-dropbear-22.sh
|
|||||||
dropbear... TCP *:22 (LISTEN)
|
dropbear... TCP *:22 (LISTEN)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9. 在 Mac 上确认 SSH
|
### 10. 在 Mac 上确认 SSH
|
||||||
|
|
||||||
回到 Mac:
|
回到 Mac:
|
||||||
|
|
||||||
@@ -180,7 +208,7 @@ ps -ef | grep -E 'sshd|dropbear|telnetd' | grep -v grep
|
|||||||
|
|
||||||
## Dashboard 闭环验证
|
## Dashboard 闭环验证
|
||||||
|
|
||||||
### 10. 让 bootstrap 跑后半段
|
### 11. 让 bootstrap 跑后半段
|
||||||
|
|
||||||
在 Mac 上执行:
|
在 Mac 上执行:
|
||||||
|
|
||||||
@@ -188,9 +216,15 @@ ps -ef | grep -E 'sshd|dropbear|telnetd' | grep -v grep
|
|||||||
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
||||||
```
|
```
|
||||||
|
|
||||||
|
前提说明:
|
||||||
|
|
||||||
|
- 这一步默认建立在第 2 步 `prepare-storage` 已成功落地的前提上
|
||||||
|
- 当前 `post-ssh` 会同步 dashboard shell 脚本、`KUAL` 菜单和主题包
|
||||||
|
- 它不会回补 `prepare-storage` 预置的原生二进制,例如 `next-wakeup`、`xh`
|
||||||
|
|
||||||
预期结果:
|
预期结果:
|
||||||
|
|
||||||
- 同步当前 dashboard 运行时
|
- 同步当前 dashboard shell 脚本、`KUAL` 菜单和主题包
|
||||||
- 同步主题包
|
- 同步主题包
|
||||||
- 设备立即切到 `simple / portrait`
|
- 设备立即切到 `simple / portrait`
|
||||||
- 屏幕出现背景与时钟
|
- 屏幕出现背景与时钟
|
||||||
@@ -201,7 +235,9 @@ sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
|||||||
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait --start-dashboard
|
sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait --start-dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
### 11. 抓首张验收图
|
这一步同样默认要求设备上已经有 `prepare-storage` 预置的完整 dashboard 基础运行时。
|
||||||
|
|
||||||
|
### 12. 抓首张验收图
|
||||||
|
|
||||||
在 Mac 上执行:
|
在 Mac 上执行:
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
- 预置 `KUAL / MRPI / USBNetwork / kindle-dash`
|
- 预置 `KUAL / MRPI / USBNetwork / kindle-dash`
|
||||||
- 可选预置 `KTerm`
|
- 可选预置 `KTerm`
|
||||||
- 预置 SSH 恢复脚本
|
- 预置 SSH 恢复脚本
|
||||||
- SSH 打通后自动同步 dashboard、切主题、立即出图
|
- SSH 打通后自动同步 dashboard shell 脚本 / KUAL 菜单 / 主题包,切主题并立即出图
|
||||||
|
|
||||||
对应脚本:
|
对应脚本:
|
||||||
|
|
||||||
@@ -106,9 +106,14 @@ sh bootstrap-new-kindle.sh post-ssh
|
|||||||
|
|
||||||
- 修复设备侧 SSH 辅助脚本权限
|
- 修复设备侧 SSH 辅助脚本权限
|
||||||
- 尝试同步 `authorized_keys`
|
- 尝试同步 `authorized_keys`
|
||||||
- 同步 dashboard 运行时和主题包
|
- 同步 dashboard shell 脚本、KUAL 菜单和主题包
|
||||||
- 立即切到指定主题并把背景画到屏幕上
|
- 立即切到指定主题并把背景画到屏幕上
|
||||||
|
|
||||||
|
当前边界:
|
||||||
|
|
||||||
|
- `post-ssh` 不会回补 `prepare-storage` 阶段预置的原生二进制,例如 `next-wakeup`、`xh`
|
||||||
|
- 所以它的默认前提仍然是:这台设备之前已经执行过 `prepare-storage`,或设备上本来就已有完整 dashboard 基础运行时
|
||||||
|
|
||||||
可选:
|
可选:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -116,6 +121,8 @@ sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait
|
|||||||
sh bootstrap-new-kindle.sh post-ssh --start-dashboard
|
sh bootstrap-new-kindle.sh post-ssh --start-dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
其中 `--start-dashboard` 的稳定前提也是一样:设备上必须已经具备完整 dashboard 基础运行时。
|
||||||
|
|
||||||
### 3. `all`
|
### 3. `all`
|
||||||
|
|
||||||
默认模式:
|
默认模式:
|
||||||
@@ -135,23 +142,29 @@ sh bootstrap-new-kindle.sh
|
|||||||
1. 恢复出厂并进入 demo mode
|
1. 恢复出厂并进入 demo mode
|
||||||
2. 到真正的 `Sideload Content` 时机
|
2. 到真正的 `Sideload Content` 时机
|
||||||
3. 让脚本已预置好的 `.demo` payload 生效
|
3. 让脚本已预置好的 `.demo` payload 生效
|
||||||
4. 通过 `Get Started` 完成越狱
|
4. `;dsts -> Help & User Guides -> Get Started`
|
||||||
5. 搜索 `;log mrpi`
|
5. 继续完成 demo 流程里的 `register this demo -> Skip -> standard`
|
||||||
6. 在 `KUAL` 中先执行 `Rename OTA Binaries -> Rename`
|
6. 重启后遇到 `Configure Device`,用隐藏手势回到主页
|
||||||
7. 如果本轮没有预置 `KTerm`,这里先手工补装 `KTerm`
|
7. 搜索 `;uzb` 挂载 USB,把 `Update_hotfix_watchthis_custom.bin` 放到根目录
|
||||||
8. 连上 Wi‑Fi
|
8. `;dsts -> Device Options -> Update Your Kindle`
|
||||||
9. 打开 `KTerm`,执行:
|
9. 回到主页后搜索 `;log mrpi`
|
||||||
|
10. 在 `KUAL` 中先执行 `Rename OTA Binaries -> Rename`
|
||||||
|
11. 如果本轮没有预置 `KTerm`,这里先手工补装 `KTerm`
|
||||||
|
12. 连上 Wi‑Fi
|
||||||
|
13. 打开 `KTerm`,执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sh /mnt/us/ssh-force-dropbear-22.sh
|
sh /mnt/us/ssh-force-dropbear-22.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
10. 回到 Mac,执行:
|
14. 回到 Mac,执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sh bootstrap-new-kindle.sh post-ssh
|
sh bootstrap-new-kindle.sh post-ssh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
这里默认仍然建立在第 1 步已经成功做过 `prepare-storage` 的前提上。
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- WatchThis 越狱路径:
|
- WatchThis 越狱路径:
|
||||||
|
|||||||
@@ -1,34 +1,57 @@
|
|||||||
# Kindle Voyage 5.13.6 一次成功路径
|
# Kindle Voyage 5.13.6 WatchThis 实测成功路径
|
||||||
|
|
||||||
这篇文档只覆盖下面这个组合:
|
|
||||||
|
恢复出厂
|
||||||
|
USB - .demo、DONT_CHECK_BATTERY、hotfix 和安装包一次性写回去
|
||||||
|
Demo Activation,点 Yes, 重启
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
这份文档只覆盖下面这个组合:
|
||||||
|
|
||||||
- 机型:`Kindle Voyage (KV)`
|
- 机型:`Kindle Voyage (KV)`
|
||||||
- 固件:`5.13.6`
|
- 固件:`5.13.6`
|
||||||
- 目标:完成越狱,并部署 `KUAL`、`MRPI`、`renameotabin` 和 `kindle-dash`
|
- 目标:完成 `WatchThis` 越狱、安装 `custom hotfix`,再通过 `MRPI` 落地 `KUAL / USBNetwork / KTerm / kindle-dash`
|
||||||
|
|
||||||
如果设备型号或固件版本不同,不要直接照抄本文。
|
如果机型或固件版本不同,不要直接照抄。
|
||||||
|
|
||||||
## 核心结论
|
## 核心结论
|
||||||
|
|
||||||
`Kindle Voyage 5.13.6` 应该走 `WatchThis`,不要走 `LanguageBreak`。
|
这台 `KV + 5.13.6` 当前最稳的顺序,不是“`Get Started` 后立刻检查 `mkk/libkh/rp`”,而是下面这条完整链路:
|
||||||
|
|
||||||
这次实操里,前面大部分失败都来自两个错误:
|
1. 恢复出厂,语言只选 `English (United Kingdom)`
|
||||||
|
2. 先输入;enter_demo, `;demo`
|
||||||
|
demo activation - yes - shipping mode error(电量低的原因)
|
||||||
|
1. 把 Kindle 接上 USB。
|
||||||
|
2. 我在根目录创建一个空文件 DONT_CHECK_BATTERY。
|
||||||
|
3. `demo menu -> Sideload Content`,在这里接 USB,把 `.demo/` payload 写进去
|
||||||
|
- .demo/KV-5.13.6.zip
|
||||||
|
- .demo/demo.json
|
||||||
|
- .demo/goodreads/
|
||||||
|
- extensions/
|
||||||
|
- mrpackages/
|
||||||
|
- dashboard/
|
||||||
|
- Update_hotfix_watchthis_custom.bin
|
||||||
|
4. `;dsts -> Help & User Guides -> Get Started`
|
||||||
|
5. 按 demo 流程继续:`register this demo -> Skip -> standard`
|
||||||
|
6. 遇到 `Configure Device` 就用隐藏手势回到主页
|
||||||
|
7. 回到主页后搜索 `;uzb`,
|
||||||
|
Update Error 2 -- 插usb,继续重启到 register this demo
|
||||||
|
输入三个111
|
||||||
|
standard 页面 (详见下面说明) ……
|
||||||
|
|
||||||
- 误走了 `LanguageBreak`
|
**再接 USB,把 `Update_hotfix_watchthis_custom.bin` 放到根目录**
|
||||||
- 在 demo 菜单里点错了分支,提前进入了 `Resell Device` / `销售设备`
|
8. `;dsts -> Device Options -> Update Your Kindle`
|
||||||
|
9. 回到主页后搜索 `;log mrpi`
|
||||||
|
10. 首页出现 `KUAL`
|
||||||
|
|
||||||
对这台设备,正确思路非常简单:
|
这才是这轮实机验证真正成功的路径。
|
||||||
|
|
||||||
1. 用 `WatchThis` 进入 demo mode
|
|
||||||
2. 只在正确的 `Sideload Content` 时机导入 `KV-5.13.6.zip`
|
|
||||||
3. 用 `Get Started` 触发越狱脚本
|
|
||||||
4. 安装 `KUAL/MRPI`
|
|
||||||
5. 用 `renameotabin` 关闭 OTA
|
|
||||||
6. 再部署并启动 `kindle-dash`
|
|
||||||
|
|
||||||
## 需要准备的文件
|
## 需要准备的文件
|
||||||
|
|
||||||
### WatchThis
|
### WatchThis payload
|
||||||
|
|
||||||
来自 `watchthis-jailbreak-r03.zip`:
|
来自 `watchthis-jailbreak-r03.zip`:
|
||||||
|
|
||||||
@@ -36,203 +59,216 @@
|
|||||||
- `demo.json`
|
- `demo.json`
|
||||||
- `Update_hotfix_watchthis_custom.bin`
|
- `Update_hotfix_watchthis_custom.bin`
|
||||||
|
|
||||||
在本仓库里对应的是:
|
本仓库里的对应路径:
|
||||||
|
|
||||||
- `staging/watchthis/KV-5.13.6/KV-5.13.6.zip`
|
- `dash/staging/watchthis/KV-5.13.6/KV-5.13.6.zip`
|
||||||
- `staging/watchthis/KV-5.13.6/demo.json`
|
- `dash/staging/watchthis/KV-5.13.6/demo.json`
|
||||||
- `staging/watchthis/Update_hotfix_watchthis_custom.bin`
|
- `dash/staging/watchthis/Update_hotfix_watchthis_custom.bin`
|
||||||
|
|
||||||
### 越狱后安装包
|
### 越狱后安装包
|
||||||
|
|
||||||
- `extensions/MRInstaller`
|
本仓库已经整理好的目录:
|
||||||
- `mrpackages/Update_KUALBooklet_HDRepack.bin`
|
|
||||||
- `extensions/renameotabin`
|
|
||||||
- `extensions/kindle-dash`
|
|
||||||
- `dashboard/`
|
|
||||||
|
|
||||||
在本仓库里已经整理到:
|
- `dash/staging/post-jailbreak-root/extensions/`
|
||||||
|
- `dash/staging/post-jailbreak-root/mrpackages/`
|
||||||
|
- `dash/staging/post-jailbreak-root/dashboard/`
|
||||||
|
|
||||||
- `staging/post-jailbreak-root/extensions/`
|
如果你已经执行过:
|
||||||
- `staging/post-jailbreak-root/mrpackages/`
|
|
||||||
- `staging/post-jailbreak-root/dashboard/`
|
```sh
|
||||||
|
sh bootstrap-new-kindle.sh prepare-storage
|
||||||
|
```
|
||||||
|
|
||||||
|
这些内容通常已经预置到 Kindle 用户存储,不需要再手工拷一次。
|
||||||
|
|
||||||
## 一次成功的正确路径
|
## 一次成功的正确路径
|
||||||
|
|
||||||
### 1. 恢复出厂并进入 demo mode
|
### 1. 恢复出厂并进入 demo menu
|
||||||
|
|
||||||
1. 先恢复出厂设置。
|
1. 先恢复出厂设置。
|
||||||
2. 语言选择页只选 `English (United Kingdom)`。
|
2. 语言选择页只选 `English (United Kingdom)`。
|
||||||
这一步非常关键,不要选中文。
|
3. 不要联网。
|
||||||
3. 到 Wi‑Fi 页面后,随便点一个网络,再立刻退回,不要真的联网。
|
4. 在搜索框输入 `;demo`。
|
||||||
4. 在搜索栏输入 `;enter_demo`。
|
5. 进入 `demo menu`。
|
||||||
5. 如果 `;enter_demo` 没反应,走备用入口:
|
|
||||||
- 用 USB 连接电脑
|
|
||||||
- 在 Kindle 根目录创建空文件 `DONT_CHECK_BATTERY`
|
|
||||||
- 弹出设备
|
|
||||||
- 回到 Kindle 搜索输入 `;demo`
|
|
||||||
6. 如果看到 `Demo Activation`,点 `Yes`。
|
|
||||||
7. 设备重启并进入 demo 流程后:
|
|
||||||
- 跳过 Wi‑Fi
|
|
||||||
- 店铺注册信息全部填假值
|
|
||||||
- `Fetching available demo types` 选 `Skip`
|
|
||||||
- demo type 选 `standard`
|
|
||||||
|
|
||||||
### 2. 第一次出现 Sideload Content 时不要导入 payload
|
如果这里没有进入 `demo menu`,再考虑备用入口;对本次实机成功路径来说,直接 `;demo` 就够了。
|
||||||
|
|
||||||
1. 第一次出现 `Add Content` / `Sideload Content` 提示时,只点 `Done`。
|
### 2. 在 Sideload Content 页面导入 payload
|
||||||
2. 这一步不要接 USB。
|
|
||||||
3. 这一步也不要导入 `KV-5.13.6.zip`。
|
|
||||||
|
|
||||||
这是最容易做错的一步。第一次 `Done` 只是让 demo setup 继续往下走,不是真正的 payload 导入点。
|
1. 在 `demo menu` 里点 `Sideload Content`。
|
||||||
|
2. 到这个页面后,再接 USB。
|
||||||
|
3. 在 Kindle 根目录确认下面三个东西存在:
|
||||||
|
|
||||||
### 3. 跳过 misconfiguration 锁页
|
```text
|
||||||
|
.demo/KV-5.13.6.zip
|
||||||
|
.demo/demo.json
|
||||||
|
.demo/goodreads/
|
||||||
|
```
|
||||||
|
|
||||||
demo setup 完成后,大概率会落到 `Configure Device` / misconfiguration 页面。
|
4. 安全弹出 Kindle。
|
||||||
|
5. 回到 Kindle,点 `Done`。
|
||||||
|
6. 退出 `demo menu`。
|
||||||
|
|
||||||
不要点 `Configure Device`,直接做隐藏手势:
|
如果你在 Mac 上手工操作,对应文件结构就是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Volumes/Kindle/.demo/KV-5.13.6.zip
|
||||||
|
/Volumes/Kindle/.demo/demo.json
|
||||||
|
/Volumes/Kindle/.demo/goodreads/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 用 Get Started 触发越狱脚本
|
||||||
|
|
||||||
|
1. 退出 `demo menu` 后,搜索输入 `;dsts`。
|
||||||
|
2. 进入 `Help & User Guides`。
|
||||||
|
3. 点 `Get Started`。
|
||||||
|
|
||||||
|
接下来按这轮实测成功路径继续:
|
||||||
|
|
||||||
|
1. 出现 `register this demo` 时,三个输入框都填 `111`
|
||||||
|
2. `Fetching available demo types` 选 `Skip`
|
||||||
|
3. 出现 demo type 时选 `standard`
|
||||||
|
|
||||||
|
之后设备会重启。
|
||||||
|
|
||||||
|
### 4. 遇到 Configure Device 时不要点进去
|
||||||
|
|
||||||
|
重启后如果看到 `Configure Device`,不要点 `Configure Device`,直接做隐藏手势:
|
||||||
|
|
||||||
1. 在屏幕右下角用两根手指同时轻点一下
|
1. 在屏幕右下角用两根手指同时轻点一下
|
||||||
2. 两指立刻抬起
|
2. 两指立刻抬起
|
||||||
3. 马上用一根手指从右下向左滑
|
3. 马上用一根手指从右下向左滑
|
||||||
|
|
||||||
触发成功后会回到可操作界面。
|
成功后会回到主页。
|
||||||
|
|
||||||
### 4. 真正的 payload 导入点
|
### 5. 用 ;uzb 暴露用户存储并写入 hotfix
|
||||||
|
|
||||||
1. 回到可操作界面后,搜索输入 `;demo`
|
回到主页后:
|
||||||
2. 进入 demo menu
|
|
||||||
3. 选择 `Sideload Content` / `导入内容`
|
1. 搜索输入 `;uzb`
|
||||||
4. 到这一步再接 USB
|
2. 再接 USB
|
||||||
5. 在 Kindle 根目录创建 `.demo/`
|
3. 在 Kindle 根目录确认存在:
|
||||||
6. 把下面三个东西放进去:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
.demo/KV-5.13.6.zip
|
/Volumes/Kindle/Update_hotfix_watchthis_custom.bin
|
||||||
.demo/demo.json
|
|
||||||
.demo/goodreads/ <- 空目录
|
|
||||||
```
|
```
|
||||||
|
|
||||||
如果你在 Mac 上操作,可以直接用:
|
如果没有,就把仓库里的:
|
||||||
|
|
||||||
```sh
|
```text
|
||||||
mkdir -p /Volumes/Kindle/.demo/goodreads
|
dash/staging/watchthis/Update_hotfix_watchthis_custom.bin
|
||||||
cp staging/watchthis/KV-5.13.6/KV-5.13.6.zip /Volumes/Kindle/.demo/
|
|
||||||
cp staging/watchthis/KV-5.13.6/demo.json /Volumes/Kindle/.demo/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
拷到 Kindle 根目录。
|
||||||
|
|
||||||
然后:
|
然后:
|
||||||
|
|
||||||
1. 弹出 Kindle
|
1. 安全弹出 Kindle
|
||||||
2. 在 Kindle 上点 `Done`
|
2. 回到设备
|
||||||
3. 退出 demo menu
|
|
||||||
|
|
||||||
## 5. 触发越狱脚本
|
### 6. 手工安装 custom hotfix
|
||||||
|
|
||||||
1. 退出 demo menu 后,输入 `;dsts`
|
这一步不要再走 `Help & User Guides`。
|
||||||
如果 `;dsts` 没反应,也可以从顶部下拉进入设置。
|
|
||||||
2. 打开 `Help & User Guides`
|
|
||||||
3. 再点 `Get Started`
|
|
||||||
4. 设备会重启
|
|
||||||
5. 越狱脚本会在下次启动时运行
|
|
||||||
|
|
||||||
如果这里弹 `Application Error`,官方补救是:
|
正确路径是:
|
||||||
|
|
||||||
|
1. 搜索输入 `;dsts`
|
||||||
|
2. 进入 `Device Options`
|
||||||
|
3. 点 `Update Your Kindle`
|
||||||
|
|
||||||
|
这一步跑完后设备会重启,并退出 demo 状态,回到正常主页。
|
||||||
|
|
||||||
|
## 7. 用 MRPI 安装 KUAL / USBNetwork / KTerm / dashboard
|
||||||
|
|
||||||
|
回到主页后,搜索输入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
;log mrpi
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `mrpackages/` 已经预置到根目录,MRPI 会开始安装。
|
||||||
|
|
||||||
|
实测成功判据:
|
||||||
|
|
||||||
|
- 主页出现 `KUAL`
|
||||||
|
- 如果这轮预置了 `KTerm`,后续也应该能找到 `KTerm`
|
||||||
|
|
||||||
|
## 8. 进入 KUAL 后的顺序
|
||||||
|
|
||||||
|
进入 `KUAL` 后,先执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Rename OTA Binaries -> Rename
|
||||||
|
```
|
||||||
|
|
||||||
|
不要先跑 `Kindle Dashboard`,先把 OTA 关掉。
|
||||||
|
|
||||||
|
## 9. 这轮流程里最容易做错的地方
|
||||||
|
|
||||||
|
### 不要走 LanguageBreak
|
||||||
|
|
||||||
|
`KV + 5.13.6` 应走 `WatchThis`,不要走 `LanguageBreak`。
|
||||||
|
|
||||||
|
### 不要点 Resell Device / Remote Reset / Configure WiFi
|
||||||
|
|
||||||
|
这几个都不是这条成功路径的一部分。
|
||||||
|
|
||||||
|
### `Get Started` 后不要直接去找 `Update Your Kindle`
|
||||||
|
|
||||||
|
先要完成:
|
||||||
|
|
||||||
|
- `register this demo`
|
||||||
|
- `Skip`
|
||||||
|
- `standard`
|
||||||
|
- 重启后隐藏手势回主页
|
||||||
|
|
||||||
|
然后才是:
|
||||||
|
|
||||||
|
- `;uzb`
|
||||||
|
- 写入 `Update_hotfix_watchthis_custom.bin`
|
||||||
|
- `Device Options -> Update Your Kindle`
|
||||||
|
|
||||||
|
### `Update Your Kindle` 之前必须先写 hotfix bin
|
||||||
|
|
||||||
|
`Update Your Kindle` 对应的是安装:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Update_hotfix_watchthis_custom.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
如果根目录里没有这个文件,就很容易出现 `Software Update / Update Error 2`。
|
||||||
|
|
||||||
|
### 不要把 `mkk/libkh/rp` 当成当前这条流程里的唯一人工判据
|
||||||
|
|
||||||
|
这轮最实用的成功判据是:
|
||||||
|
|
||||||
|
1. `Update Your Kindle` 能正常跑完并重启回主页
|
||||||
|
2. `;log mrpi` 能执行
|
||||||
|
3. 首页出现 `KUAL`
|
||||||
|
|
||||||
|
## 10. 如果某一步出错
|
||||||
|
|
||||||
|
### `Get Started` 后出现 Application Error
|
||||||
|
|
||||||
|
按上游 `WatchThis` README 的补救方式:
|
||||||
|
|
||||||
1. 长按电源键强制重启
|
1. 长按电源键强制重启
|
||||||
2. 再进 demo menu
|
2. 再进 `demo menu`
|
||||||
3. 再执行一次 `Sideload Content -> Done`
|
3. 再执行一次 `Sideload Content -> Done`
|
||||||
4. 这次不要再接 USB
|
4. 这次不要再接 USB
|
||||||
|
|
||||||
## 6. 成功判据
|
### `Update Your Kindle` 报 `Update Error 2`
|
||||||
|
|
||||||
对这台设备,下面这些现象说明越狱已经落地:
|
先检查 Kindle 根目录里是不是真的有:
|
||||||
|
|
||||||
- Kindle 用户存储根目录出现 `mkk`
|
```text
|
||||||
- Kindle 用户存储根目录出现 `libkh`
|
Update_hotfix_watchthis_custom.bin
|
||||||
- Kindle 用户存储根目录出现 `rp`
|
|
||||||
|
|
||||||
如果这三个目录都没有,基本就是前面的 `WatchThis` 没真正成功。
|
|
||||||
|
|
||||||
## 7. 安装 KUAL / MRPI / kindle-dash
|
|
||||||
|
|
||||||
越狱落地后,把这些目录复制到 Kindle:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rsync -a staging/post-jailbreak-root/extensions/ /Volumes/Kindle/extensions/
|
|
||||||
rsync -a staging/post-jailbreak-root/mrpackages/ /Volumes/Kindle/mrpackages/
|
|
||||||
rsync -a staging/post-jailbreak-root/dashboard/ /Volumes/Kindle/dashboard/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
然后:
|
注意不要把 macOS 生成的:
|
||||||
|
|
||||||
1. 弹出 Kindle
|
```text
|
||||||
2. 回到首页搜索输入 `;log mrpi`
|
._Update_hotfix_watchthis_custom.bin
|
||||||
3. 等安装完成
|
```
|
||||||
4. 首页会出现 `KUAL` 卡片
|
|
||||||
|
|
||||||
## 8. 启动顺序
|
误当成真正的 hotfix 文件。
|
||||||
|
|
||||||
进入 `KUAL` 后,先做这个顺序:
|
|
||||||
|
|
||||||
1. `Rename OTA Binaries -> Rename`
|
|
||||||
2. 再运行 `Kindle Dashboard`
|
|
||||||
|
|
||||||
不要先跑 `Kindle Dashboard`,否则后面如果 OTA 没关掉,还存在自动升级把越狱覆盖掉的风险。
|
|
||||||
|
|
||||||
## 9. kindle-dash 默认行为
|
|
||||||
|
|
||||||
本项目默认不会在 Kindle 本机实时渲染页面,而是定时去下载一张图片来显示。
|
|
||||||
|
|
||||||
因此:
|
|
||||||
|
|
||||||
- 如果没有联网,`Kindle Dashboard` 看起来会像“卡住”
|
|
||||||
- 如果刷新计划不覆盖当前时间,会显示 `kindle is sleeping`
|
|
||||||
- 图片最好直接按 Voyage 原生分辨率出图:`1072 x 1448`
|
|
||||||
|
|
||||||
默认抓图脚本在:
|
|
||||||
|
|
||||||
- `src/local/fetch-dashboard.sh`
|
|
||||||
|
|
||||||
默认刷新计划在:
|
|
||||||
|
|
||||||
- `src/local/env.sh`
|
|
||||||
|
|
||||||
## 10. 这台设备上确认过的坑
|
|
||||||
|
|
||||||
### 不要走 `LanguageBreak`
|
|
||||||
|
|
||||||
`KV + 5.13.6` 应走 `WatchThis`。误走 `LanguageBreak` 会导致:
|
|
||||||
|
|
||||||
- `;demo -> Yes -> 重启 -> 回普通系统`
|
|
||||||
- `;uzb`、`;dsts` 行为异常
|
|
||||||
- 反复进入错误的 demo 分支
|
|
||||||
|
|
||||||
### 不要点 `Resell Device` / `销售设备`
|
|
||||||
|
|
||||||
这个分支会把流程带到 shipping mode / demo 出厂流程,和 `WatchThis` 正常路径无关。
|
|
||||||
|
|
||||||
如果你是在 `WatchThis` 流程里,demo menu 里真正要点的是:
|
|
||||||
|
|
||||||
- `Sideload Content`
|
|
||||||
|
|
||||||
不是:
|
|
||||||
|
|
||||||
- `Resell Device`
|
|
||||||
- `Remote Reset`
|
|
||||||
- `Configure WiFi`
|
|
||||||
|
|
||||||
### 第一次 `Add Content` 只能点 `Done`
|
|
||||||
|
|
||||||
真正要接 USB 导 payload 的时机,是秘密手势之后再次 `;demo -> Sideload Content` 的那一次。
|
|
||||||
|
|
||||||
### 看到左上角只有一小块图片,不一定是失败
|
|
||||||
|
|
||||||
这通常只是图片尺寸不匹配。
|
|
||||||
|
|
||||||
例如本项目自带的 `sleeping.png` 只有 `600x800`,放到 Voyage 上就只会显示在左上角一部分区域。
|
|
||||||
|
|
||||||
## 参考
|
|
||||||
|
|
||||||
- WatchThis 包内说明:`watchthis-jailbreak-r03.zip` 中的 `watchthis-release/README.md`
|
|
||||||
- 项目主说明:`README.md`
|
|
||||||
- 图片抓取说明:`src/local/fetch-dashboard.sh`
|
|
||||||
- 本地调度配置:`src/local/env.sh`
|
|
||||||
|
|||||||
360
dash/src/dash.sh
360
dash/src/dash.sh
@@ -11,35 +11,69 @@ STATE_DIR="$DIR/local/state"
|
|||||||
BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at"
|
BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at"
|
||||||
THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env"
|
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"
|
||||||
|
PID_FILE="$STATE_DIR/dashboard.pid"
|
||||||
|
THEME_BACKGROUND_SYNC_CMD="$DIR/local/sync-theme-backgrounds.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_SERVICE_CMD="$DIR/local/touch-home-service.sh"
|
||||||
TOUCH_HOME_LOG_FILE="$DIR/logs/touch-home.log"
|
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"}
|
||||||
|
SCHEDULED_SCREEN_WAKE_ENABLED=${SCHEDULED_SCREEN_WAKE_ENABLED:-true}
|
||||||
|
REMOTE_SYNC_SCHEDULE=${REMOTE_SYNC_SCHEDULE:-"10 0 * * *"}
|
||||||
|
REMOTE_SYNC_ENABLED=${REMOTE_SYNC_ENABLED:-true}
|
||||||
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}
|
DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false}
|
||||||
BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
||||||
CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15}
|
CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15}
|
||||||
PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
||||||
MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60}
|
MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300}
|
||||||
MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
||||||
|
SLEEPING_SCREEN_ENABLED=${SLEEPING_SCREEN_ENABLED:-true}
|
||||||
STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
||||||
STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
||||||
STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
||||||
STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372}
|
STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360}
|
||||||
STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24}
|
STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48}
|
||||||
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_WAKEUP_ENABLE=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||||
KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false}
|
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
|
background_needs_redraw=true
|
||||||
|
dashboard_exit_reason=normal
|
||||||
|
|
||||||
|
on_dashboard_term() {
|
||||||
|
dashboard_exit_reason=SIGTERM
|
||||||
|
echo "Dashboard received SIGTERM"
|
||||||
|
exit 143
|
||||||
|
}
|
||||||
|
|
||||||
|
on_dashboard_hup() {
|
||||||
|
dashboard_exit_reason=SIGHUP
|
||||||
|
echo "Dashboard received SIGHUP"
|
||||||
|
exit 129
|
||||||
|
}
|
||||||
|
|
||||||
|
on_dashboard_int() {
|
||||||
|
dashboard_exit_reason=SIGINT
|
||||||
|
echo "Dashboard received SIGINT"
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
on_dashboard_exit() {
|
||||||
|
exit_status=$?
|
||||||
|
echo "Dashboard exiting status=$exit_status reason=$dashboard_exit_reason pid=$$"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap on_dashboard_term TERM
|
||||||
|
trap on_dashboard_hup HUP
|
||||||
|
trap on_dashboard_int INT
|
||||||
|
trap on_dashboard_exit EXIT
|
||||||
|
|
||||||
start_theme_menu_service() {
|
start_theme_menu_service() {
|
||||||
if [ "${THEME_MENU_ENABLED:-false}" != true ]; then
|
if [ "${THEME_MENU_ENABLED:-false}" != true ]; then
|
||||||
@@ -77,14 +111,13 @@ load_theme_runtime_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
if [ -z "$TIMEZONE" ]; then
|
||||||
echo "Missing required configuration."
|
echo "Missing required configuration."
|
||||||
echo "Timezone: ${TIMEZONE:-(not set)}."
|
echo "Timezone: ${TIMEZONE:-(not set)}."
|
||||||
echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
echo "Starting dashboard with refresh_schedule=${REFRESH_SCHEDULE:-disabled} sync_schedule=${REMOTE_SYNC_SCHEDULE:-disabled} scheduled_screen_wake=${SCHEDULED_SCREEN_WAKE_ENABLED}..."
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
@@ -99,7 +132,7 @@ init() {
|
|||||||
fi
|
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
|
keep_screen_visible
|
||||||
start_theme_menu_service
|
start_theme_menu_service
|
||||||
start_touch_home_service
|
start_touch_home_service
|
||||||
}
|
}
|
||||||
@@ -118,13 +151,20 @@ stop_framework() {
|
|||||||
prepare_sleep() {
|
prepare_sleep() {
|
||||||
echo "Preparing sleep"
|
echo "Preparing sleep"
|
||||||
|
|
||||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
if [ "$SLEEPING_SCREEN_ENABLED" = true ]; then
|
||||||
background_needs_redraw=true
|
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||||
|
background_needs_redraw=true
|
||||||
|
|
||||||
# Give screen time to refresh
|
# 只有真的画了 sleeping.png,才需要额外等待这次整屏刷新落完。
|
||||||
sleep 2
|
sleep 2
|
||||||
|
else
|
||||||
|
# overlay/静默同步模式下,直接保留当前 dashboard 画面直到真正挂起,
|
||||||
|
# 避免 Voyage 把 600x800 的 sleeping.png 只画在左上角,压住日历内容。
|
||||||
|
echo "Sleeping screen disabled, keeping current dashboard visible before suspend."
|
||||||
|
fi
|
||||||
|
|
||||||
# Ensure a full screen refresh is triggered after wake from sleep
|
# 无论休眠前是否显示 sleeping.png,唤醒后的下一次恢复都强制走一次整屏刷新,
|
||||||
|
# 避免长时间 suspend 之后局部刷新把残影继续带到下一轮。
|
||||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +183,29 @@ background_refresh_due() {
|
|||||||
last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE")
|
last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE")
|
||||||
refresh_interval_seconds=$((BACKGROUND_REFRESH_INTERVAL_MINUTES * 60))
|
refresh_interval_seconds=$((BACKGROUND_REFRESH_INTERVAL_MINUTES * 60))
|
||||||
|
|
||||||
|
# 背景里包含当天日历时,不能只按“距离上次刷新过去了多少分钟”判断。
|
||||||
|
# 只要北京时间跨天了,就应该在下一轮调度里至少再拉一次新背景。
|
||||||
|
current_day_id=$(lua - "$current_epoch" "${CLOCK_TIME_OFFSET_MINUTES:-0}" <<'LUA'
|
||||||
|
local epoch_seconds = assert(tonumber(arg[1]), "missing epoch seconds")
|
||||||
|
local offset_minutes = tonumber(arg[2]) or 0
|
||||||
|
local seconds_per_day = 24 * 60 * 60
|
||||||
|
local local_seconds = epoch_seconds + offset_minutes * 60
|
||||||
|
io.write(math.floor(local_seconds / seconds_per_day))
|
||||||
|
LUA
|
||||||
|
)
|
||||||
|
last_day_id=$(lua - "$last_background_epoch" "${CLOCK_TIME_OFFSET_MINUTES:-0}" <<'LUA'
|
||||||
|
local epoch_seconds = assert(tonumber(arg[1]), "missing epoch seconds")
|
||||||
|
local offset_minutes = tonumber(arg[2]) or 0
|
||||||
|
local seconds_per_day = 24 * 60 * 60
|
||||||
|
local local_seconds = epoch_seconds + offset_minutes * 60
|
||||||
|
io.write(math.floor(local_seconds / seconds_per_day))
|
||||||
|
LUA
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$current_day_id" != "$last_day_id" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
[ $((current_epoch - last_background_epoch)) -ge "$refresh_interval_seconds" ]
|
[ $((current_epoch - last_background_epoch)) -ge "$refresh_interval_seconds" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +239,161 @@ fetch_background() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screen_wake_enabled() {
|
||||||
|
# 这里的语义只代表“分钟刷新时是否把屏幕维持在可视态”。
|
||||||
|
# 分钟调度本身是否存在,不能再由这个开关决定。
|
||||||
|
# debug on 的目标就是高频亮屏调试,所以这里直接强制开启可见刷新。
|
||||||
|
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$SCHEDULED_SCREEN_WAKE_ENABLED" = true ]
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_schedule_enabled() {
|
||||||
|
[ -n "${REFRESH_SCHEDULE:-}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_screen_visible() {
|
||||||
|
# 可视窗口和 debug on 都需要把 powerd 留在可视态,
|
||||||
|
# 否则刚画完时钟就可能被系统重新收回到睡眠界面。
|
||||||
|
lipc-set-prop com.lab126.powerd preventScreenSaver 1 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
allow_screen_sleep() {
|
||||||
|
# debug on 需要常亮;普通低功耗模式下才把屏幕控制权交还给 powerd。
|
||||||
|
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
|
keep_screen_visible
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
lipc-set-prop com.lab126.powerd preventScreenSaver 0 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_sync_enabled() {
|
||||||
|
[ "$REMOTE_SYNC_ENABLED" = true ]
|
||||||
|
}
|
||||||
|
|
||||||
|
next_schedule_seconds() {
|
||||||
|
schedule=$1
|
||||||
|
"$DIR/next-wakeup" --schedule="$schedule" --timezone="$TIMEZONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_next_wakeup() {
|
||||||
|
max_wait=2147483647
|
||||||
|
next_display_secs=$max_wait
|
||||||
|
next_sync_secs=$max_wait
|
||||||
|
|
||||||
|
# debug off 即使不允许自动亮屏,分钟级时钟刷新也必须继续调度。
|
||||||
|
if refresh_schedule_enabled; then
|
||||||
|
next_display_secs=$(next_schedule_seconds "$REFRESH_SCHEDULE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if remote_sync_enabled && [ -n "${REMOTE_SYNC_SCHEDULE:-}" ]; then
|
||||||
|
next_sync_secs=$(next_schedule_seconds "$REMOTE_SYNC_SCHEDULE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$next_display_secs" -eq "$max_wait" ] && [ "$next_sync_secs" -eq "$max_wait" ]; then
|
||||||
|
echo "next_wakeup_reason=display"
|
||||||
|
echo "next_wakeup_secs=3600"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$next_display_secs" -le "$next_sync_secs" ]; then
|
||||||
|
echo "next_wakeup_reason=display"
|
||||||
|
echo "next_wakeup_secs=$next_display_secs"
|
||||||
|
else
|
||||||
|
echo "next_wakeup_reason=sync"
|
||||||
|
echo "next_wakeup_secs=$next_sync_secs"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
silent_sync() {
|
||||||
|
sync_failed=false
|
||||||
|
|
||||||
|
# 每天 00:10 的静默唤醒先把全量主题图和主题 JSON 拉到本地,
|
||||||
|
# 这样后续切主题可以完全走本地目录,不依赖当下网络。
|
||||||
|
if ! "$THEME_BACKGROUND_SYNC_CMD"; then
|
||||||
|
echo "Theme background sync failed"
|
||||||
|
sync_failed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if background_refresh_due; then
|
||||||
|
if fetch_background; then
|
||||||
|
# 后台静默同步时只更新缓存,不主动点亮 screen。
|
||||||
|
# 下次手动点亮或进入 dashboard 时,再把新背景完整恢复到屏幕上。
|
||||||
|
background_needs_redraw=true
|
||||||
|
else
|
||||||
|
echo "Silent sync background refresh failed"
|
||||||
|
sync_failed=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Silent sync skipped current background refresh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$sync_failed" = true ]; then
|
||||||
|
echo "Silent sync failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Silent sync completed"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_dashboard_hidden() {
|
||||||
|
if background_refresh_due; then
|
||||||
|
if fetch_background; then
|
||||||
|
# 普通分钟调度在 debug off 下只更新缓存,不主动把新背景刷到屏幕上。
|
||||||
|
background_needs_redraw=true
|
||||||
|
elif [ ! -f "$BACKGROUND_PNG" ]; then
|
||||||
|
echo "No cached background available for hidden refresh."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Clock cache refresh without screen wake"
|
||||||
|
"$CLOCK_RENDER_CMD" false false
|
||||||
|
}
|
||||||
|
|
||||||
clock_force_full_refresh() {
|
clock_force_full_refresh() {
|
||||||
eval "$("$DIR/local/clock-index.sh")"
|
eval "$("$DIR/local/clock-index.sh")"
|
||||||
[ $((minute % CLOCK_FULL_REFRESH_INTERVAL_MINUTES)) -eq 0 ]
|
[ $((minute % CLOCK_FULL_REFRESH_INTERVAL_MINUTES)) -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hold_visible_window_until() {
|
||||||
|
keep_awake_until=$1
|
||||||
|
|
||||||
|
# 只要用户当前正看着 calendar,这段窗口里就继续让分钟刷新走可视路径。
|
||||||
|
keep_screen_visible
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
now=$(now_epoch)
|
||||||
|
if [ "$now" -ge "$keep_awake_until" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
seconds_until_next_minute=$((60 - (now % 60)))
|
||||||
|
remaining_awake_seconds=$((keep_awake_until - now))
|
||||||
|
|
||||||
|
if [ "$seconds_until_next_minute" -gt "$remaining_awake_seconds" ]; then
|
||||||
|
sleep "$remaining_awake_seconds"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$seconds_until_next_minute"
|
||||||
|
refresh_dashboard || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
mask_system_status_overlay_once() {
|
||||||
|
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
fbink -q -V -B WHITE -k \
|
||||||
|
"top=$STATUS_MASK_TOP,left=$STATUS_MASK_LEFT,width=$STATUS_MASK_WIDTH,height=$STATUS_MASK_HEIGHT"
|
||||||
|
}
|
||||||
|
|
||||||
mask_system_status_overlay() {
|
mask_system_status_overlay() {
|
||||||
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
||||||
return
|
return
|
||||||
@@ -191,8 +404,7 @@ mask_system_status_overlay() {
|
|||||||
# 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。
|
# 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。
|
||||||
pass=1
|
pass=1
|
||||||
while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do
|
while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do
|
||||||
fbink -q -V -B WHITE -k \
|
mask_system_status_overlay_once
|
||||||
"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
|
if [ "$pass" -lt "$STATUS_MASK_PASSES" ] && [ "$STATUS_MASK_DELAY_SECONDS" -gt 0 ]; then
|
||||||
sleep "$STATUS_MASK_DELAY_SECONDS"
|
sleep "$STATUS_MASK_DELAY_SECONDS"
|
||||||
@@ -255,6 +467,24 @@ powerd_get_prop() {
|
|||||||
lipc-get-prop com.lab126.powerd "$prop_name" 2>/dev/null || echo "unavailable"
|
lipc-get-prop com.lab126.powerd "$prop_name" 2>/dev/null || echo "unavailable"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screen_is_visibly_active() {
|
||||||
|
power_state=$(powerd_get_prop state)
|
||||||
|
case "$power_state" in
|
||||||
|
active|Active)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
power_status=$(powerd_get_prop status)
|
||||||
|
case "$power_status" in
|
||||||
|
*"Powerd state: Active"*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
log_battery_stats() {
|
log_battery_stats() {
|
||||||
battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown")
|
battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown")
|
||||||
charging_state=$(powerd_get_prop isCharging)
|
charging_state=$(powerd_get_prop isCharging)
|
||||||
@@ -285,8 +515,36 @@ rtc_sleep() {
|
|||||||
echo "Skipping system suspend, sleeping for ${duration}s instead"
|
echo "Skipping system suspend, sleeping for ${duration}s instead"
|
||||||
sleep "$duration"
|
sleep "$duration"
|
||||||
else
|
else
|
||||||
# shellcheck disable=SC2039
|
# Voyage 这代机器不一定还有旧的 mxc_rtc.0/wakeup_enable 接口;
|
||||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
# 当前实机上能稳定看到的是 /sys/class/rtc/rtc0/wakealarm。
|
||||||
|
# 这里先兼容旧接口,找不到时再回退到标准 wakealarm 写法。
|
||||||
|
if [ -f "$RTC_WAKEUP_ENABLE" ]; then
|
||||||
|
current_wakeup_value=$(cat "$RTC_WAKEUP_ENABLE" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$current_wakeup_value" ] && [ "$current_wakeup_value" -eq 0 ] 2>/dev/null; then
|
||||||
|
echo -n "$duration" >"$RTC_WAKEUP_ENABLE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rtc_wakealarm_path=""
|
||||||
|
|
||||||
|
if [ -f /sys/class/rtc/rtc0/wakealarm ]; then
|
||||||
|
rtc_wakealarm_path=/sys/class/rtc/rtc0/wakealarm
|
||||||
|
elif [ -f /sys/class/rtc/rtc1/wakealarm ]; then
|
||||||
|
rtc_wakealarm_path=/sys/class/rtc/rtc1/wakealarm
|
||||||
|
elif [ -f /sys/class/rtc/rtc2/wakealarm ]; then
|
||||||
|
rtc_wakealarm_path=/sys/class/rtc/rtc2/wakealarm
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$rtc_wakealarm_path" ]; then
|
||||||
|
wake_epoch=$(( $(now_epoch) + duration ))
|
||||||
|
echo 0 >"$rtc_wakealarm_path" 2>/dev/null || true
|
||||||
|
echo "$wake_epoch" >"$rtc_wakealarm_path"
|
||||||
|
else
|
||||||
|
echo "警告:未找到可用的 RTC 唤醒接口,回退到普通 sleep ${duration}s"
|
||||||
|
sleep "$duration"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo "mem" >/sys/power/state
|
echo "mem" >/sys/power/state
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -311,6 +569,7 @@ hold_after_manual_wake() {
|
|||||||
sleep_started_at=$2
|
sleep_started_at=$2
|
||||||
sleep_finished_at=$3
|
sleep_finished_at=$3
|
||||||
actual_duration=$((sleep_finished_at - sleep_started_at))
|
actual_duration=$((sleep_finished_at - sleep_started_at))
|
||||||
|
keep_awake_until=$((sleep_finished_at + MANUAL_WAKE_KEEP_AWAKE_SECONDS))
|
||||||
|
|
||||||
if ! manual_wake_detected "$requested_duration" "$actual_duration"; then
|
if ! manual_wake_detected "$requested_duration" "$actual_duration"; then
|
||||||
return
|
return
|
||||||
@@ -318,17 +577,22 @@ hold_after_manual_wake() {
|
|||||||
|
|
||||||
echo "Manual wake detected after ${actual_duration}s, keeping awake for ${MANUAL_WAKE_KEEP_AWAKE_SECONDS}s"
|
echo "Manual wake detected after ${actual_duration}s, keeping awake for ${MANUAL_WAKE_KEEP_AWAKE_SECONDS}s"
|
||||||
|
|
||||||
|
# 手动唤醒期间,系统可能已经把自己的睡眠/锁屏层画回前台。
|
||||||
|
# 这里先强制要求下一次 refresh_dashboard 完整恢复 calendar 背景,
|
||||||
|
# 不能只补一层时钟 patch。
|
||||||
|
background_needs_redraw=true
|
||||||
|
|
||||||
# 短按电源键提前唤醒后,先把 dashboard 内容恢复回来,
|
# 短按电源键提前唤醒后,先把 dashboard 内容恢复回来,
|
||||||
# 再给出一段明确的可交互窗口,避免 2~3 秒内再次休眠。
|
# 再在这段明确的可交互窗口里按分钟补画时钟,避免只刷一次后就停住。
|
||||||
refresh_dashboard || true
|
refresh_dashboard || true
|
||||||
sleep "$MANUAL_WAKE_KEEP_AWAKE_SECONDS"
|
hold_visible_window_until "$keep_awake_until"
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
eval "$(compute_next_wakeup)"
|
||||||
|
|
||||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then
|
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then
|
||||||
action="sleep"
|
action="sleep"
|
||||||
@@ -337,8 +601,12 @@ main_loop() {
|
|||||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
echo "Debug mode active, skipping sleeping screen."
|
echo "Debug mode active, skipping sleeping screen."
|
||||||
fi
|
fi
|
||||||
action="suspend"
|
|
||||||
refresh_dashboard
|
if [ "$next_wakeup_reason" = "sync" ]; then
|
||||||
|
action="silent-sync"
|
||||||
|
else
|
||||||
|
action="suspend"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
actual_sleep_secs=$next_wakeup_secs
|
actual_sleep_secs=$next_wakeup_secs
|
||||||
@@ -350,6 +618,13 @@ main_loop() {
|
|||||||
|
|
||||||
# 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。
|
# 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。
|
||||||
# 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。
|
# 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。
|
||||||
|
# 普通分钟调度已经改成隐藏刷新,因此 debug off 下应该把屏幕控制权交还给 powerd,
|
||||||
|
# 避免设备每分钟都被重新拉成可视亮屏态。
|
||||||
|
if screen_wake_enabled; then
|
||||||
|
keep_screen_visible
|
||||||
|
else
|
||||||
|
allow_screen_sleep
|
||||||
|
fi
|
||||||
sleep "$PRE_SLEEP_GRACE_SECONDS"
|
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"
|
||||||
@@ -357,9 +632,46 @@ main_loop() {
|
|||||||
sleep_started_at=$(now_epoch)
|
sleep_started_at=$(now_epoch)
|
||||||
rtc_sleep "$actual_sleep_secs"
|
rtc_sleep "$actual_sleep_secs"
|
||||||
sleep_finished_at=$(now_epoch)
|
sleep_finished_at=$(now_epoch)
|
||||||
hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at"
|
|
||||||
|
# Voyage 在 rtc 唤醒刚回来的瞬间,右上角系统状态栏可能会先闪回前台。
|
||||||
|
# 只要系统已经把屏幕拉成可视态,就先立即补一层白色遮罩,
|
||||||
|
# 再进入后面的手动唤醒判断和 dashboard 刷新,尽量缩短这段闪现时间。
|
||||||
|
if screen_is_visibly_active; then
|
||||||
|
mask_system_status_overlay_once
|
||||||
|
fi
|
||||||
|
|
||||||
|
if manual_wake_detected "$actual_sleep_secs" "$((sleep_finished_at - sleep_started_at))"; then
|
||||||
|
hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$next_wakeup_reason" = "sync" ]; then
|
||||||
|
silent_sync || true
|
||||||
|
else
|
||||||
|
if screen_wake_enabled; then
|
||||||
|
refresh_dashboard || true
|
||||||
|
elif screen_is_visibly_active; then
|
||||||
|
# Voyage 在当前 overlay + RTC suspend 架构下,
|
||||||
|
# 普通分钟唤醒有时会被系统自己拉成可视态。
|
||||||
|
# 既然用户已经看得到这次唤醒,就顺手把时钟补到当前分钟,
|
||||||
|
# 避免出现“亮灯了,但表盘还是旧时间”的错觉。
|
||||||
|
refresh_dashboard || true
|
||||||
|
else
|
||||||
|
refresh_dashboard_hidden || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
init
|
init
|
||||||
|
printf '%s\n' "$$" >"$PID_FILE"
|
||||||
|
echo "Dashboard boot pid=$$ at $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
|
refresh_dashboard || true
|
||||||
|
|
||||||
|
# 从 KUAL 进入 dashboard 或主题切换后回到 calendar,本身就是一次显式用户操作。
|
||||||
|
# debug off 下也应该给同样的 5 分钟可视窗口,让用户回来后看到会继续走动的时钟。
|
||||||
|
if [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then
|
||||||
|
hold_visible_window_until $(( $(now_epoch) + MANUAL_WAKE_KEEP_AWAKE_SECONDS ))
|
||||||
|
fi
|
||||||
|
|
||||||
main_loop
|
main_loop
|
||||||
|
|||||||
@@ -2,19 +2,99 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
DIR="$(dirname "$0")"
|
DIR="$(dirname "$0")"
|
||||||
|
ENV_FILE="$DIR/local/env.sh"
|
||||||
SWITCH_THEME_CMD="$DIR/switch-theme.sh"
|
SWITCH_THEME_CMD="$DIR/switch-theme.sh"
|
||||||
LAUNCH_FROM_KUAL_CMD="$DIR/launch-from-kual.sh"
|
START_DASHBOARD_CMD="$DIR/start.sh"
|
||||||
|
LOG_FILE="$DIR/logs/kual-theme-launch.log"
|
||||||
|
|
||||||
requested_theme_id=${1:?"usage: launch-theme-from-kual.sh <theme-id> [orientation]"}
|
requested_theme_id=${1:?"usage: launch-theme-from-kual.sh <theme-id> [orientation]"}
|
||||||
requested_orientation=${2:-}
|
requested_orientation=${2:-}
|
||||||
|
|
||||||
# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。
|
# shellcheck disable=SC1090
|
||||||
# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑,
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
# 同时把“选主题”前移到进入 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"
|
KUAL_QUIT_GRACE_SECONDS="${KUAL_QUIT_GRACE_SECONDS:-0}"
|
||||||
|
KUAL_APP_ID="${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}"
|
||||||
|
|
||||||
|
# KUAL 菜单项本身应该尽快返回,让 KUAL 自己先退回首页。
|
||||||
|
# 真正的“切主题 + 启动 dashboard”放到独立 session 里继续跑,
|
||||||
|
# 避免动作链被 KUAL 退出过程提前打断。
|
||||||
|
# 这里直接起 start.sh,不再额外绕一层 launch-from-kual.sh,
|
||||||
|
# 尽量减少首页闪出的可见窗口。
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
|
if command -v setsid >/dev/null 2>&1; then
|
||||||
|
nohup setsid /bin/sh -c '
|
||||||
|
theme_id=$1
|
||||||
|
theme_orientation=$2
|
||||||
|
switch_cmd=$3
|
||||||
|
start_cmd=$4
|
||||||
|
log_file=$5
|
||||||
|
target_dir=$6
|
||||||
|
kual_app_id=$7
|
||||||
|
quit_grace=$8
|
||||||
|
|
||||||
|
printf "%s launch-theme worker start theme=%s orientation=%s\n" "$(date 2>/dev/null || true)" "$theme_id" "${theme_orientation:-default}" >>"$log_file"
|
||||||
|
|
||||||
|
# 旧 dashboard 还活着时,会继续按旧状态补画时钟,导致画面叠层。
|
||||||
|
# 这里在真正切主题前先清掉旧实例,确保后面只剩一条主循环。
|
||||||
|
pkill -f "$target_dir/dash.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/theme-menu-service.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/touch-home-service.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 主题链路也沿用 KUAL 的正常退出路径,避免我们刚把 calendar 画出来,
|
||||||
|
# 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
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$quit_grace" -gt 0 ] 2>/dev/null; then
|
||||||
|
sleep "$quit_grace"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$theme_orientation" ]; then
|
||||||
|
"$switch_cmd" "$theme_id" "$theme_orientation" >>"$log_file" 2>&1
|
||||||
|
else
|
||||||
|
"$switch_cmd" "$theme_id" >>"$log_file" 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$start_cmd" >>"$log_file" 2>&1
|
||||||
|
' sh "$requested_theme_id" "$requested_orientation" "$SWITCH_THEME_CMD" "$START_DASHBOARD_CMD" "$LOG_FILE" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >/dev/null 2>&1 &
|
||||||
|
else
|
||||||
|
nohup /bin/sh -c '
|
||||||
|
theme_id=$1
|
||||||
|
theme_orientation=$2
|
||||||
|
switch_cmd=$3
|
||||||
|
start_cmd=$4
|
||||||
|
log_file=$5
|
||||||
|
target_dir=$6
|
||||||
|
kual_app_id=$7
|
||||||
|
quit_grace=$8
|
||||||
|
|
||||||
|
printf "%s launch-theme worker start theme=%s orientation=%s\n" "$(date 2>/dev/null || true)" "$theme_id" "${theme_orientation:-default}" >>"$log_file"
|
||||||
|
|
||||||
|
# 旧 dashboard 还活着时,会继续按旧状态补画时钟,导致画面叠层。
|
||||||
|
# 这里在真正切主题前先清掉旧实例,确保后面只剩一条主循环。
|
||||||
|
pkill -f "$target_dir/dash.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/theme-menu-service.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/touch-home-service.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 主题链路也沿用 KUAL 的正常退出路径,避免我们刚把 calendar 画出来,
|
||||||
|
# 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
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$quit_grace" -gt 0 ] 2>/dev/null; then
|
||||||
|
sleep "$quit_grace"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$theme_orientation" ]; then
|
||||||
|
"$switch_cmd" "$theme_id" "$theme_orientation" >>"$log_file" 2>&1
|
||||||
|
else
|
||||||
|
"$switch_cmd" "$theme_id" >>"$log_file" 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$start_cmd" >>"$log_file" 2>&1
|
||||||
|
' sh "$requested_theme_id" "$requested_orientation" "$SWITCH_THEME_CMD" "$START_DASHBOARD_CMD" "$LOG_FILE" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >/dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
# Export environment variables here
|
# Export environment variables here
|
||||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
# 设备侧时钟刷新节奏。
|
||||||
|
# 当前默认每分钟刷新一次;debug on 与 debug off 都走这条分钟调度。
|
||||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||||
|
# 是否允许按 REFRESH_SCHEDULE 自动点亮 screen。
|
||||||
|
# 默认关闭;debug off 下分钟刷新仍会执行,但不会因为调度本身把屏幕维持在可视态。
|
||||||
|
# 只有 debug on,或手动短按 power 进入的 5 分钟窗口,才把屏幕留在可视态。
|
||||||
|
export SCHEDULED_SCREEN_WAKE_ENABLED=${SCHEDULED_SCREEN_WAKE_ENABLED:-false}
|
||||||
|
# 静默远端同步的唤醒节奏。
|
||||||
|
# 默认每天凌晨 00:10 唤醒一次,用来拉当天最新背景、主题 JSON 和全量主题图片,
|
||||||
|
# 但不主动点亮 screen。
|
||||||
|
export REMOTE_SYNC_SCHEDULE=${REMOTE_SYNC_SCHEDULE:-"10 0 * * *"}
|
||||||
|
export REMOTE_SYNC_ENABLED=${REMOTE_SYNC_ENABLED:-true}
|
||||||
# 调度计算依赖 next-wakeup 这个 Rust 程序,它要求使用 IANA 时区名。
|
# 调度计算依赖 next-wakeup 这个 Rust 程序,它要求使用 IANA 时区名。
|
||||||
# 这里必须保留 Asia/Shanghai,才能正确计算下一次唤醒时间。
|
# 这里必须保留 Asia/Shanghai,才能正确计算下一次唤醒时间。
|
||||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||||
@@ -16,7 +26,21 @@ export THEMES_INDEX_REFRESH_INTERVAL_MINUTES=${THEMES_INDEX_REFRESH_INTERVAL_MIN
|
|||||||
export THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440}
|
export THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440}
|
||||||
export THEME_ID=${THEME_ID:-"default"}
|
export THEME_ID=${THEME_ID:-"default"}
|
||||||
export ORIENTATION=${ORIENTATION:-"portrait"}
|
export ORIENTATION=${ORIENTATION:-"portrait"}
|
||||||
export BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
# 天气目前仍然是随背景一起导出,不是 Kindle 本机实时渲染。
|
||||||
|
# 如果要让背景里的天气跟 Kindle 当前网络出口位置走,就要优先使用
|
||||||
|
# “同步时按 Kindle 位置导出的本地主题背景”,而不是服务器上的通用背景。
|
||||||
|
export WEATHER_USE_KINDLE_LOCATION=${WEATHER_USE_KINDLE_LOCATION:-true}
|
||||||
|
# Kindle 侧位置缓存使用 GeoIP 做“市级近似定位”,不追求街道级精度。
|
||||||
|
# 失败时必须稳定回退到固定城市,避免影响整条 dashboard 链路。
|
||||||
|
export LOCATION_GEOIP_URL=${LOCATION_GEOIP_URL:-"https://ipwho.is/"}
|
||||||
|
export LOCATION_REFRESH_INTERVAL_MINUTES=${LOCATION_REFRESH_INTERVAL_MINUTES:-720}
|
||||||
|
export LOCATION_FALLBACK_CITY=${LOCATION_FALLBACK_CITY:-"杭州"}
|
||||||
|
export LOCATION_FALLBACK_LAT=${LOCATION_FALLBACK_LAT:-30.274084}
|
||||||
|
export LOCATION_FALLBACK_LON=${LOCATION_FALLBACK_LON:-120.155070}
|
||||||
|
export LOCATION_FALLBACK_TIMEZONE=${LOCATION_FALLBACK_TIMEZONE:-"Asia/Shanghai"}
|
||||||
|
# 日历背景在跨天时必须至少更新一次;平时不需要高频拉图。
|
||||||
|
# 这里默认按 24 小时做兜底,同时 dash.sh 里还会在北京时间跨天后强制补一次刷新。
|
||||||
|
export BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-1440}
|
||||||
export CLOCK_REGION_X=${CLOCK_REGION_X:-347}
|
export CLOCK_REGION_X=${CLOCK_REGION_X:-347}
|
||||||
export CLOCK_REGION_Y=${CLOCK_REGION_Y:-55}
|
export CLOCK_REGION_Y=${CLOCK_REGION_Y:-55}
|
||||||
export CLOCK_REGION_WIDTH=${CLOCK_REGION_WIDTH:-220}
|
export CLOCK_REGION_WIDTH=${CLOCK_REGION_WIDTH:-220}
|
||||||
@@ -44,18 +68,22 @@ export CLOCK_CENTER_RADIUS=${CLOCK_CENTER_RADIUS:-7}
|
|||||||
# 这段时间会从真正的休眠时长里扣掉,避免分钟刷新慢一拍。
|
# 这段时间会从真正的休眠时长里扣掉,避免分钟刷新慢一拍。
|
||||||
export PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
export PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
||||||
# 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。
|
# 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。
|
||||||
# 这样用户有足够时间看屏、切主题或继续交互,而不会立刻再次休眠。
|
# debug off 下这里固定为 300 秒,也就是按 power 之后保持 5 分钟可视窗口。
|
||||||
export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60}
|
export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300}
|
||||||
# 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。
|
# 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。
|
||||||
export MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
export MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
||||||
|
# 默认 overlay/静默同步模式下,不再提前显示 sleeping.png。
|
||||||
|
# Voyage 上这张图只有 600x800,提前显示时会只占左上角一块并盖住当前 calendar。
|
||||||
|
# 如需恢复旧的“休眠前先显示提示图”行为,可手动改回 true。
|
||||||
|
export SLEEPING_SCREEN_ENABLED=${SLEEPING_SCREEN_ENABLED:-false}
|
||||||
|
|
||||||
# Voyage 顶部状态栏遮罩:用于压住系统偶尔重画出来的时间、Wi-Fi、电池图标。
|
# Voyage 顶部状态栏遮罩:用于压住系统偶尔重画出来的时间、Wi‑Fi、电池图标。
|
||||||
# 当前坐标只覆盖页面顶部空白带,不会擦到天气卡上边框。
|
# 遮罩只覆盖右上角状态区,避免继续压住 calendar 自己的顶部内容。
|
||||||
export STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
export STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
||||||
export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-600}
|
||||||
export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
||||||
export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372}
|
export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360}
|
||||||
export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24}
|
export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48}
|
||||||
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}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ output_path=${1:?"missing output path"}
|
|||||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
ENV_FILE="$DIR/env.sh"
|
ENV_FILE="$DIR/env.sh"
|
||||||
THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
||||||
|
local_only=false
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--local-only" ]; then
|
||||||
|
local_only=true
|
||||||
|
shift
|
||||||
|
output_path=${1:?"missing output path"}
|
||||||
|
fi
|
||||||
|
|
||||||
# fetch-dashboard 既会被 dash.sh 调,也会被 switch-theme.sh 单独调。
|
# fetch-dashboard 既会被 dash.sh 调,也会被 switch-theme.sh 单独调。
|
||||||
# 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。
|
# 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。
|
||||||
@@ -16,17 +23,74 @@ THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
|||||||
|
|
||||||
background_path=${BACKGROUND_PATH:-""}
|
background_path=${BACKGROUND_PATH:-""}
|
||||||
background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"}
|
background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"}
|
||||||
|
weather_use_kindle_location=${WEATHER_USE_KINDLE_LOCATION:-false}
|
||||||
local_background_path=""
|
local_background_path=""
|
||||||
|
|
||||||
if [ -n "$background_path" ]; then
|
if [ -n "$background_path" ]; then
|
||||||
local_background_path="$DIR/../$background_path"
|
local_background_path="$DIR/../$background_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 主题背景如果已经随本地部署同步到 Kindle,优先直接拷贝本地文件,
|
copy_local_background() {
|
||||||
# 这样切换主题时不依赖远端图片资源是否已经发布完成。
|
if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then
|
||||||
if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then
|
cp "$local_background_path" "$output_path"
|
||||||
cp "$local_background_path" "$output_path"
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "本地主题包缺少背景图:${background_path:-unknown}" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_valid_png() {
|
||||||
|
candidate_path=$1
|
||||||
|
|
||||||
|
if [ ! -f "$candidate_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kindle 端已经确认有 file 命令;如果将来某台机器没有,再回退到 PNG 签名检查。
|
||||||
|
if command -v file >/dev/null 2>&1; then
|
||||||
|
if file "$candidate_path" 2>/dev/null | grep -q "PNG image data"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
signature=$(dd if="$candidate_path" bs=8 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||||
|
[ "$signature" = "89504e470d0a1a0a" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$local_only" = true ]; then
|
||||||
|
# KUAL 触发的切主题链路必须尽快返回,所以这里严格只读本地素材。
|
||||||
|
# 如果本地包缺图,直接失败,让问题暴露出来,而不是把联网等待塞进交互里。
|
||||||
|
copy_local_background
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$weather_use_kindle_location" = true ] && copy_local_background; then
|
||||||
|
# 背景里的天气信息当前仍是导图时烘焙进去的。
|
||||||
|
# 开启 Kindle 位置天气后,必须优先使用同步到设备的本地主题背景,
|
||||||
|
# 否则后续定时刷新会被服务器上的通用背景覆盖掉。
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$DIR/../xh" -d -q -o "$output_path" get "$background_url"
|
# 定时背景刷新和启动阶段走这里时,优先尝试拉远端最新背景。
|
||||||
|
# 如果远端暂时失败,再回退到本地包,至少保证 dashboard 还能继续显示。
|
||||||
|
remote_tmp_path="${output_path}.remote.$$"
|
||||||
|
rm -f "$remote_tmp_path"
|
||||||
|
|
||||||
|
if "$DIR/../xh" -d -q -o "$remote_tmp_path" get "$background_url"; then
|
||||||
|
if is_valid_png "$remote_tmp_path"; then
|
||||||
|
mv "$remote_tmp_path" "$output_path"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "远端背景不是有效 PNG,忽略本次响应并回退本地主题包:$background_url" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$remote_tmp_path"
|
||||||
|
|
||||||
|
if copy_local_background; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "远端背景拉取失败,且本地主题包也缺少背景图:${background_path:-unknown}" >&2
|
||||||
|
exit 1
|
||||||
|
|||||||
64
dash/src/local/location-env.sh
Normal file
64
dash/src/local/location-env.sh
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
|
ENV_FILE="$DIR/env.sh"
|
||||||
|
STATE_DIR="$DIR/state"
|
||||||
|
LOCATION_CACHE_FILE="$STATE_DIR/location.env"
|
||||||
|
LOCATION_SYNC_CMD="$DIR/location-sync.sh"
|
||||||
|
refresh_if_needed=false
|
||||||
|
force_refresh=false
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--refresh-if-needed)
|
||||||
|
refresh_if_needed=true
|
||||||
|
;;
|
||||||
|
--force-refresh)
|
||||||
|
force_refresh=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
if [ "$force_refresh" = true ]; then
|
||||||
|
"$LOCATION_SYNC_CMD" --force >/dev/null 2>&1 || true
|
||||||
|
elif [ "$refresh_if_needed" = true ]; then
|
||||||
|
"$LOCATION_SYNC_CMD" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
location_source="fallback"
|
||||||
|
location_city=${LOCATION_FALLBACK_CITY:-杭州}
|
||||||
|
location_lat=${LOCATION_FALLBACK_LAT:-30.274084}
|
||||||
|
location_lon=${LOCATION_FALLBACK_LON:-120.155070}
|
||||||
|
location_timezone=${LOCATION_FALLBACK_TIMEZONE:-Asia/Shanghai}
|
||||||
|
|
||||||
|
if [ -f "$LOCATION_CACHE_FILE" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$LOCATION_CACHE_FILE"
|
||||||
|
|
||||||
|
if [ -n "${LOCATION_LAT:-}" ] && [ -n "${LOCATION_LON:-}" ]; then
|
||||||
|
location_source=${LOCATION_SOURCE:-geoip}
|
||||||
|
location_city=${LOCATION_CITY:-$location_city}
|
||||||
|
location_lat=${LOCATION_LAT}
|
||||||
|
location_lon=${LOCATION_LON}
|
||||||
|
location_timezone=${LOCATION_TIMEZONE:-$location_timezone}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
quote_for_shell() {
|
||||||
|
printf "%s" "$1" | sed "s/'/'\\\\''/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
printf "export LOCATION_SOURCE='%s'\n" "$(quote_for_shell "$location_source")"
|
||||||
|
printf "export LOCATION_CITY='%s'\n" "$(quote_for_shell "$location_city")"
|
||||||
|
printf "export LOCATION_LAT='%s'\n" "$(quote_for_shell "$location_lat")"
|
||||||
|
printf "export LOCATION_LON='%s'\n" "$(quote_for_shell "$location_lon")"
|
||||||
|
printf "export LOCATION_TIMEZONE='%s'\n" "$(quote_for_shell "$location_timezone")"
|
||||||
164
dash/src/local/location-sync.sh
Normal file
164
dash/src/local/location-sync.sh
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
|
ENV_FILE="$DIR/env.sh"
|
||||||
|
STATE_DIR="$DIR/state"
|
||||||
|
LOCATION_CACHE_FILE="$STATE_DIR/location.env"
|
||||||
|
FETCH_CMD="$DIR/../xh"
|
||||||
|
force_refresh=false
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--force)
|
||||||
|
force_refresh=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
current_epoch() {
|
||||||
|
date '+%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_refresh_due() {
|
||||||
|
if [ ! -f "$LOCATION_CACHE_FILE" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
last_epoch=$(awk -F= '/^export LOCATION_UPDATED_AT_EPOCH=/{gsub("'"'"'", "", $2); print $2; exit}' "$LOCATION_CACHE_FILE" 2>/dev/null || true)
|
||||||
|
if [ -z "$last_epoch" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
now_epoch=$(current_epoch)
|
||||||
|
[ $((now_epoch - last_epoch)) -ge $((LOCATION_REFRESH_INTERVAL_MINUTES * 60)) ]
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_geoip_fields() {
|
||||||
|
payload_path=$1
|
||||||
|
|
||||||
|
lua - "$payload_path" <<'LUA'
|
||||||
|
local path = arg[1]
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
if data:match('"success"%s*:%s*false') then
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function match_string(source, pattern)
|
||||||
|
local value = source:match(pattern)
|
||||||
|
if not value then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
:gsub('\\"', '"')
|
||||||
|
:gsub("\\\\", "\\")
|
||||||
|
end
|
||||||
|
|
||||||
|
local city = match_string(data, '"city"%s*:%s*"(.-)"')
|
||||||
|
local latitude = data:match('"latitude"%s*:%s*(-?%d+%.?%d*)')
|
||||||
|
local longitude = data:match('"longitude"%s*:%s*(-?%d+%.?%d*)')
|
||||||
|
local timezone_block = data:match('"timezone"%s*:%s*(%b{})')
|
||||||
|
local timezone = timezone_block and match_string(timezone_block, '"id"%s*:%s*"(.-)"') or nil
|
||||||
|
|
||||||
|
if not latitude or not longitude then
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
print("CITY=" .. (city or "当前位置"))
|
||||||
|
print("LAT=" .. latitude)
|
||||||
|
print("LON=" .. longitude)
|
||||||
|
print("TIMEZONE=" .. (timezone or "Asia/Shanghai"))
|
||||||
|
LUA
|
||||||
|
}
|
||||||
|
|
||||||
|
quote_for_shell() {
|
||||||
|
printf "%s" "$1" | sed "s/'/'\\\\''/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$force_refresh" != true ] && ! cache_refresh_due; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$FETCH_CMD" ]; then
|
||||||
|
echo "Location sync failed: xh is unavailable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload_path="$STATE_DIR/location-response.$$"
|
||||||
|
rm -f "$payload_path"
|
||||||
|
|
||||||
|
if ! "$FETCH_CMD" -d -q -o "$payload_path" get "$LOCATION_GEOIP_URL"; then
|
||||||
|
rm -f "$payload_path"
|
||||||
|
echo "Location sync failed: GeoIP request failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
location_city=""
|
||||||
|
location_lat=""
|
||||||
|
location_lon=""
|
||||||
|
location_timezone=""
|
||||||
|
|
||||||
|
if ! parsed_output=$(extract_geoip_fields "$payload_path"); then
|
||||||
|
rm -f "$payload_path"
|
||||||
|
echo "Location sync failed: unable to parse GeoIP payload" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$payload_path"
|
||||||
|
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
case "$key" in
|
||||||
|
CITY)
|
||||||
|
location_city=$value
|
||||||
|
;;
|
||||||
|
LAT)
|
||||||
|
location_lat=$value
|
||||||
|
;;
|
||||||
|
LON)
|
||||||
|
location_lon=$value
|
||||||
|
;;
|
||||||
|
TIMEZONE)
|
||||||
|
location_timezone=$value
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done <<EOF
|
||||||
|
$parsed_output
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -z "$location_lat" ] || [ -z "$location_lon" ]; then
|
||||||
|
echo "Location sync failed: GeoIP payload missing coordinates" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
now_epoch=$(current_epoch)
|
||||||
|
now_iso=$(date '+%Y-%m-%dT%H:%M:%S%z' | sed 's/\(..\)$/:\1/')
|
||||||
|
|
||||||
|
cat >"$LOCATION_CACHE_FILE" <<EOF
|
||||||
|
export LOCATION_SOURCE='geoip'
|
||||||
|
export LOCATION_CITY='$(quote_for_shell "$location_city")'
|
||||||
|
export LOCATION_LAT='$(quote_for_shell "$location_lat")'
|
||||||
|
export LOCATION_LON='$(quote_for_shell "$location_lon")'
|
||||||
|
export LOCATION_TIMEZONE='$(quote_for_shell "$location_timezone")'
|
||||||
|
export LOCATION_UPDATED_AT='$(quote_for_shell "$now_iso")'
|
||||||
|
export LOCATION_UPDATED_AT_EPOCH='$(quote_for_shell "$now_epoch")'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
printf 'Location synced: %s (%s,%s)\n' "$location_city" "$location_lat" "$location_lon"
|
||||||
@@ -36,6 +36,7 @@ clock_minute_thickness=${CLOCK_MINUTE_THICKNESS:-5}
|
|||||||
clock_center_radius=${CLOCK_CENTER_RADIUS:-7}
|
clock_center_radius=${CLOCK_CENTER_RADIUS:-7}
|
||||||
clock_rotation_degrees=${CLOCK_ROTATION_DEGREES:-0}
|
clock_rotation_degrees=${CLOCK_ROTATION_DEGREES:-0}
|
||||||
force_full_refresh=${1:-false}
|
force_full_refresh=${1:-false}
|
||||||
|
apply_to_screen=${2:-true}
|
||||||
output_path="$DIR/state/clock-render.pgm"
|
output_path="$DIR/state/clock-render.pgm"
|
||||||
|
|
||||||
eval "$("$DIR/clock-index.sh")"
|
eval "$("$DIR/clock-index.sh")"
|
||||||
@@ -65,6 +66,10 @@ lua "$DIR/render-clock.lua" \
|
|||||||
"$clock_center_radius" \
|
"$clock_center_radius" \
|
||||||
"$clock_rotation_degrees"
|
"$clock_rotation_degrees"
|
||||||
|
|
||||||
|
if [ "$apply_to_screen" != true ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$force_full_refresh" = true ]; then
|
if [ "$force_full_refresh" = true ]; then
|
||||||
# Kindle Voyage 当前这条链路里,fbink 默认会叠加 viewport 修正,
|
# Kindle Voyage 当前这条链路里,fbink 默认会叠加 viewport 修正,
|
||||||
# 导致图像在屏幕上出现双重偏移。这里强制关闭 viewport 修正,
|
# 导致图像在屏幕上出现双重偏移。这里强制关闭 viewport 修正,
|
||||||
|
|||||||
149
dash/src/local/sync-theme-backgrounds.sh
Normal file
149
dash/src/local/sync-theme-backgrounds.sh
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
|
ENV_FILE="$DIR/env.sh"
|
||||||
|
STATE_DIR="$DIR/state"
|
||||||
|
THEME_JSON_LUA="$DIR/theme-json.lua"
|
||||||
|
FETCH_CMD="$DIR/../xh"
|
||||||
|
WAIT_FOR_WIFI_CMD="$DIR/../wait-for-wifi.sh"
|
||||||
|
LOCAL_THEMES_INDEX="$DIR/../themes.json"
|
||||||
|
LOCAL_THEMES_DIR="$DIR/../themes"
|
||||||
|
LOCAL_BACKGROUND_DIR="$DIR/../kindle-backgrounds"
|
||||||
|
THEMES_INDEX_CACHE="$STATE_DIR/themes.json"
|
||||||
|
EXPECTED_THEMES_FILE="$STATE_DIR/expected-theme-configs.txt"
|
||||||
|
EXPECTED_BACKGROUNDS_FILE="$STATE_DIR/expected-theme-backgrounds.txt"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
THEMES_INDEX_URL=${THEMES_INDEX_URL:-"https://shell.biboer.cn:20001/themes.json"}
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR" "$LOCAL_THEMES_DIR" "$LOCAL_BACKGROUND_DIR"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
is_valid_png() {
|
||||||
|
candidate_path=$1
|
||||||
|
|
||||||
|
if [ ! -f "$candidate_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v file >/dev/null 2>&1; then
|
||||||
|
if file "$candidate_path" 2>/dev/null | grep -q "PNG image data"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
signature=$(dd if="$candidate_path" bs=8 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||||
|
[ "$signature" = "89504e470d0a1a0a" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_png_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
|
||||||
|
|
||||||
|
if ! is_valid_png "$tmp_path"; then
|
||||||
|
rm -f "$tmp_path"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$tmp_path" "$output_path"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_stale_files() {
|
||||||
|
target_dir=$1
|
||||||
|
expected_file=$2
|
||||||
|
pattern=$3
|
||||||
|
|
||||||
|
for existing_path in "$target_dir"/$pattern; do
|
||||||
|
[ -e "$existing_path" ] || continue
|
||||||
|
|
||||||
|
existing_name=$(basename "$existing_path")
|
||||||
|
if ! grep -Fqx "$existing_name" "$expected_file"; then
|
||||||
|
rm -f "$existing_path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Syncing all theme backgrounds from remote"
|
||||||
|
"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP"
|
||||||
|
|
||||||
|
if ! fetch_to_path "$THEMES_INDEX_URL" "$THEMES_INDEX_CACHE"; then
|
||||||
|
echo "Themes index fetch failed: $THEMES_INDEX_URL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$THEMES_INDEX_CACHE" "$LOCAL_THEMES_INDEX"
|
||||||
|
: >"$EXPECTED_THEMES_FILE"
|
||||||
|
: >"$EXPECTED_BACKGROUNDS_FILE"
|
||||||
|
|
||||||
|
theme_configs=$(lua "$THEME_JSON_LUA" list-configs "$THEMES_INDEX_CACHE")
|
||||||
|
|
||||||
|
if [ -z "$theme_configs" ]; then
|
||||||
|
echo "Themes index is empty." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$theme_configs" | while IFS="$(printf '\t')" read -r theme_id config_url; do
|
||||||
|
[ -n "$theme_id" ] || continue
|
||||||
|
[ -n "$config_url" ] || continue
|
||||||
|
|
||||||
|
local_theme_config="$LOCAL_THEMES_DIR/$theme_id.json"
|
||||||
|
if ! fetch_to_path "$config_url" "$local_theme_config"; then
|
||||||
|
echo "Theme config fetch failed: $theme_id $config_url" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$(basename "$local_theme_config")" >>"$EXPECTED_THEMES_FILE"
|
||||||
|
|
||||||
|
theme_backgrounds=$(lua "$THEME_JSON_LUA" list-backgrounds "$local_theme_config")
|
||||||
|
if [ -z "$theme_backgrounds" ]; then
|
||||||
|
echo "Theme config has no backgrounds: $theme_id" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$theme_backgrounds" | while IFS="$(printf '\t')" read -r orientation background_path background_url; do
|
||||||
|
[ -n "$background_path" ] || continue
|
||||||
|
[ -n "$background_url" ] || continue
|
||||||
|
|
||||||
|
background_file=$(basename "$background_path")
|
||||||
|
local_background_path="$LOCAL_BACKGROUND_DIR/$background_file"
|
||||||
|
|
||||||
|
if ! fetch_png_to_path "$background_url" "$local_background_path"; then
|
||||||
|
echo "Theme background fetch failed: $theme_id $orientation $background_url" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$background_file" >>"$EXPECTED_BACKGROUNDS_FILE"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup_stale_files "$LOCAL_THEMES_DIR" "$EXPECTED_THEMES_FILE" "*.json"
|
||||||
|
cleanup_stale_files "$LOCAL_BACKGROUND_DIR" "$EXPECTED_BACKGROUNDS_FILE" "*.png"
|
||||||
|
|
||||||
|
echo "Theme background sync completed"
|
||||||
@@ -234,6 +234,56 @@ local function first_orientation(theme, fallback)
|
|||||||
return (theme.orientations or {})[1] or fallback
|
return (theme.orientations or {})[1] or fallback
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function orientation_label(orientation)
|
||||||
|
if orientation == "landscape" then
|
||||||
|
return "横屏"
|
||||||
|
end
|
||||||
|
|
||||||
|
if orientation == "portrait" then
|
||||||
|
return "竖屏"
|
||||||
|
end
|
||||||
|
|
||||||
|
return orientation
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ordered_variant_keys(variants)
|
||||||
|
local keys = {}
|
||||||
|
local seen = {}
|
||||||
|
local preferred = {"portrait", "landscape"}
|
||||||
|
|
||||||
|
for _, key in ipairs(preferred) do
|
||||||
|
if variants[key] ~= nil then
|
||||||
|
keys[#keys + 1] = key
|
||||||
|
seen[key] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for key, _ in pairs(variants) do
|
||||||
|
if not seen[key] then
|
||||||
|
keys[#keys + 1] = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(keys, function(left, right)
|
||||||
|
return left < right
|
||||||
|
end)
|
||||||
|
|
||||||
|
for index = #preferred, 1, -1 do
|
||||||
|
local key = preferred[index]
|
||||||
|
if seen[key] then
|
||||||
|
for key_index, value in ipairs(keys) do
|
||||||
|
if value == key then
|
||||||
|
table.remove(keys, key_index)
|
||||||
|
table.insert(keys, 1, key)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
local function shell_quote(value)
|
local function shell_quote(value)
|
||||||
return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'"
|
return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'"
|
||||||
end
|
end
|
||||||
@@ -330,11 +380,45 @@ end
|
|||||||
if command == "list" then
|
if command == "list" then
|
||||||
local index_path = assert(arg[2], "missing themes index path")
|
local index_path = assert(arg[2], "missing themes index path")
|
||||||
local index_data = decode(read_file(index_path))
|
local index_data = decode(read_file(index_path))
|
||||||
|
local orientation_order = {"landscape", "portrait"}
|
||||||
|
|
||||||
for _, theme in ipairs(index_data.themes or {}) do
|
for _, theme in ipairs(index_data.themes or {}) do
|
||||||
io.write(theme.id or "", "\t")
|
for _, orientation in ipairs(orientation_order) do
|
||||||
io.write(theme.label or theme.id or "", "\t")
|
if orientation_exists(theme, orientation) then
|
||||||
io.write(table.concat(theme.orientations or {}, ","), "\n")
|
io.write(theme.id or "", "\t")
|
||||||
|
io.write((theme.id or "") .. "-" .. orientation_label(orientation), "\t")
|
||||||
|
io.write(orientation, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == "list-configs" 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
|
||||||
|
if theme.id ~= nil and theme.configUrl ~= nil then
|
||||||
|
io.write(theme.id, "\t", theme.configUrl, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == "list-backgrounds" then
|
||||||
|
local theme_path = assert(arg[2], "missing theme config path")
|
||||||
|
local theme_data = decode(read_file(theme_path))
|
||||||
|
local variants = assert(theme_data.variants, "missing variants")
|
||||||
|
|
||||||
|
for _, orientation in ipairs(ordered_variant_keys(variants)) do
|
||||||
|
local variant = variants[orientation]
|
||||||
|
local background = variant.background or {}
|
||||||
|
if background.path ~= nil and background.url ~= nil then
|
||||||
|
io.write(orientation, "\t", background.path, "\t", background.url, "\n")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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_ID="__return_home__"
|
||||||
HOME_MENU_ITEM_LABEL="Return Home"
|
HOME_MENU_ITEM_LABEL="返回"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
@@ -114,8 +114,8 @@ theme_field() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
current_theme_index() {
|
current_theme_index() {
|
||||||
awk -F '\t' -v current_theme="$current_theme_id" '
|
awk -F '\t' -v current_theme="$current_theme_id" -v current_orientation="$current_orientation" '
|
||||||
$1 == current_theme {
|
$1 == current_theme && $3 == current_orientation {
|
||||||
print NR
|
print NR
|
||||||
found = 1
|
found = 1
|
||||||
exit
|
exit
|
||||||
@@ -138,6 +138,9 @@ print_line() {
|
|||||||
render_menu() {
|
render_menu() {
|
||||||
total_themes=$(theme_count)
|
total_themes=$(theme_count)
|
||||||
current_label=$(theme_field "$selected_index" 2)
|
current_label=$(theme_field "$selected_index" 2)
|
||||||
|
grid_left_col=3
|
||||||
|
grid_right_col=24
|
||||||
|
grid_start_row=8
|
||||||
|
|
||||||
/usr/sbin/eips -c
|
/usr/sbin/eips -c
|
||||||
print_line 3 1 "Kindle Dashboard"
|
print_line 3 1 "Kindle Dashboard"
|
||||||
@@ -146,26 +149,35 @@ render_menu() {
|
|||||||
print_line 3 6 "Selected: $current_label"
|
print_line 3 6 "Selected: $current_label"
|
||||||
print_line 3 7 "--------------------------------"
|
print_line 3 7 "--------------------------------"
|
||||||
|
|
||||||
row=9
|
|
||||||
index=1
|
index=1
|
||||||
while [ "$index" -le "$total_themes" ]; do
|
while [ "$index" -le "$total_themes" ]; do
|
||||||
theme_label=$(theme_field "$index" 2)
|
theme_label=$(theme_field "$index" 2)
|
||||||
theme_id=$(theme_field "$index" 1)
|
|
||||||
prefix=" "
|
prefix=" "
|
||||||
line_text=""
|
line_text=""
|
||||||
|
row=0
|
||||||
|
col=0
|
||||||
|
|
||||||
if [ "$index" -eq "$selected_index" ]; then
|
if [ "$index" -eq "$selected_index" ]; then
|
||||||
prefix="> "
|
prefix="> "
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
line_text="${prefix}${theme_label}"
|
||||||
line_text="${prefix}${theme_label}"
|
|
||||||
|
if [ "$index" -le 8 ]; then
|
||||||
|
row_offset=$(( (index - 1) / 2 ))
|
||||||
|
row=$((grid_start_row + row_offset * 2))
|
||||||
|
|
||||||
|
if [ $((index % 2)) -eq 1 ]; then
|
||||||
|
col=$grid_left_col
|
||||||
|
else
|
||||||
|
col=$grid_right_col
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
line_text="${prefix}${theme_label} (${theme_id})"
|
row=16
|
||||||
|
col=3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_line 3 "$row" "$line_text"
|
print_line "$col" "$row" "$line_text"
|
||||||
row=$((row + 2))
|
|
||||||
index=$((index + 1))
|
index=$((index + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -198,6 +210,7 @@ move_selection() {
|
|||||||
|
|
||||||
apply_selection() {
|
apply_selection() {
|
||||||
selected_theme_id=$(theme_field "$selected_index" 1)
|
selected_theme_id=$(theme_field "$selected_index" 1)
|
||||||
|
selected_orientation=$(theme_field "$selected_index" 3)
|
||||||
|
|
||||||
/usr/sbin/eips -c
|
/usr/sbin/eips -c
|
||||||
if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
||||||
@@ -209,10 +222,10 @@ apply_selection() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_event "apply theme=$selected_theme_id orientation=$current_orientation"
|
log_event "apply theme=$selected_theme_id orientation=$selected_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 / $selected_orientation"
|
||||||
"$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation"
|
"$SWITCH_THEME_CMD" "$selected_theme_id" "$selected_orientation"
|
||||||
menu_open=false
|
menu_open=false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ mkdir -p "$STATE_DIR"
|
|||||||
|
|
||||||
force_index=false
|
force_index=false
|
||||||
force_theme=false
|
force_theme=false
|
||||||
|
local_only=false
|
||||||
requested_theme_id=${THEME_ID:-default}
|
requested_theme_id=${THEME_ID:-default}
|
||||||
requested_orientation=${ORIENTATION:-portrait}
|
requested_orientation=${ORIENTATION:-portrait}
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ while [ "$#" -gt 0 ]; do
|
|||||||
--force-theme)
|
--force-theme)
|
||||||
force_theme=true
|
force_theme=true
|
||||||
;;
|
;;
|
||||||
|
--local-only)
|
||||||
|
local_only=true
|
||||||
|
;;
|
||||||
--theme)
|
--theme)
|
||||||
shift
|
shift
|
||||||
requested_theme_id=${1:?"missing theme id"}
|
requested_theme_id=${1:?"missing theme id"}
|
||||||
@@ -118,6 +122,11 @@ sync_themes_index() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$local_only" = true ]; then
|
||||||
|
echo "本地主题包缺少 themes.json。" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 主题清单是全局入口,平时按天同步一次即可。
|
# 主题清单是全局入口,平时按天同步一次即可。
|
||||||
# 真正切换主题时会走 --force-index,确保马上拿到最新列表。
|
# 真正切换主题时会走 --force-index,确保马上拿到最新列表。
|
||||||
if [ "$force_index" = true ] || [ ! -f "$THEMES_INDEX_CACHE" ] || refresh_due "$THEMES_INDEX_TIMESTAMP_FILE" "$THEMES_INDEX_REFRESH_INTERVAL_MINUTES"; then
|
if [ "$force_index" = true ] || [ ! -f "$THEMES_INDEX_CACHE" ] || refresh_due "$THEMES_INDEX_TIMESTAMP_FILE" "$THEMES_INDEX_REFRESH_INTERVAL_MINUTES"; then
|
||||||
@@ -165,6 +174,11 @@ sync_theme_config() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$local_only" = true ]; then
|
||||||
|
echo "本地主题包缺少主题配置:$resolved_theme_id" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 主题配置按 theme 维度缓存;
|
# 主题配置按 theme 维度缓存;
|
||||||
# orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。
|
# orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。
|
||||||
needs_theme_fetch=$force_theme
|
needs_theme_fetch=$force_theme
|
||||||
|
|||||||
@@ -6,15 +6,32 @@ DIR="$(dirname "$0")"
|
|||||||
ENV_FILE="$DIR/local/env.sh"
|
ENV_FILE="$DIR/local/env.sh"
|
||||||
THEME_FILE="$DIR/local/theme.env"
|
THEME_FILE="$DIR/local/theme.env"
|
||||||
LOG_FILE="$DIR/logs/dash.log"
|
LOG_FILE="$DIR/logs/dash.log"
|
||||||
|
PID_FILE="$DIR/local/state/dashboard.pid"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$LOG_FILE")"
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
mkdir -p "$DIR/local/state"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||||
|
|
||||||
|
stop_existing_dashboard_processes() {
|
||||||
|
# 主题切换后再次 start 时,旧的 dash 主循环如果还活着,
|
||||||
|
# 会继续按旧主题或旧坐标补画时钟,最终在屏幕上叠出两个时钟。
|
||||||
|
# 这里统一先清掉旧实例,再启动新的单实例 dashboard。
|
||||||
|
pkill -f "$DIR/dash.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
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_existing_dashboard_processes
|
||||||
|
|
||||||
|
echo "start.sh invoked at $(date '+%Y-%m-%d %H:%M:%S %Z') with DEBUG=$DEBUG" >>"$LOG_FILE"
|
||||||
|
|
||||||
if [ "$DEBUG" = true ]; then
|
if [ "$DEBUG" = true ]; then
|
||||||
|
printf '%s\n' "$$" >"$PID_FILE"
|
||||||
"$DIR/dash.sh"
|
"$DIR/dash.sh"
|
||||||
else
|
else
|
||||||
# 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。
|
# 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。
|
||||||
@@ -25,4 +42,7 @@ else
|
|||||||
else
|
else
|
||||||
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$!" >"$PID_FILE"
|
||||||
|
echo "start.sh spawned dashboard launcher pid=$!" >>"$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ THEME_FILE="$DIR/local/theme.env"
|
|||||||
THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env"
|
THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env"
|
||||||
BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at"
|
BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at"
|
||||||
BACKGROUND_PNG="$DIR/kindlebg.png"
|
BACKGROUND_PNG="$DIR/kindlebg.png"
|
||||||
WAIT_FOR_WIFI_CMD="$DIR/wait-for-wifi.sh"
|
|
||||||
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
||||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||||
CLOCK_RENDER_CMD="$DIR/local/render-clock.sh"
|
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
@@ -22,16 +20,19 @@ requested_orientation=${2:-${ORIENTATION:-portrait}}
|
|||||||
|
|
||||||
mkdir -p "$DIR/local/state"
|
mkdir -p "$DIR/local/state"
|
||||||
|
|
||||||
# 切换主题时必须立刻联网拉到最新配置和背景,
|
|
||||||
# 否则用户会看到 theme.env 已更新,但屏幕内容仍停留在旧主题。
|
|
||||||
echo "Switching theme to $requested_theme_id / $requested_orientation"
|
echo "Switching theme to $requested_theme_id / $requested_orientation"
|
||||||
"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP"
|
# KUAL 切主题的目标是尽快把 calendar 画出来。
|
||||||
"$THEME_SYNC_CMD" --force-index --force-theme --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null
|
# 这里强制只用设备上已经同步好的本地主题素材,避免因为网络抖动
|
||||||
|
# 又回到首页停留几秒,破坏“直接切到 calendar”的体感。
|
||||||
|
"$THEME_SYNC_CMD" --local-only --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$THEME_RUNTIME_ENV_FILE"
|
. "$THEME_RUNTIME_ENV_FILE"
|
||||||
|
|
||||||
"$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG"
|
# 背景图同样只从本地主题包读取。
|
||||||
|
# 如果本地包缺图,直接失败,让问题暴露出来;补素材应走后台同步或重新部署,
|
||||||
|
# 不能把联网等待塞回用户点击主题这条交互链路里。
|
||||||
|
"$FETCH_DASHBOARD_CMD" --local-only "$BACKGROUND_PNG"
|
||||||
date '+%s' >"$BACKGROUND_TIMESTAMP_FILE"
|
date '+%s' >"$BACKGROUND_TIMESTAMP_FILE"
|
||||||
|
|
||||||
# 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。
|
# 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。
|
||||||
@@ -41,6 +42,9 @@ export ORIENTATION='${ORIENTATION}'
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
||||||
"$CLOCK_RENDER_CMD" true
|
|
||||||
|
# 这里不再同步阻塞等待时钟重绘。
|
||||||
|
# 背景先尽快上屏,让用户从 KUAL 返回后更快看到 calendar;
|
||||||
|
# 后续的时钟和遮罩交给 start.sh 拉起的主循环补画。
|
||||||
|
|
||||||
echo "Theme switched to ${THEME_ID} / ${ORIENTATION}"
|
echo "Theme switched to ${THEME_ID} / ${ORIENTATION}"
|
||||||
|
|||||||
BIN
dash/staging/kterm/kterm-kindle-2.6.zip
Normal file
BIN
dash/staging/kterm/kterm-kindle-2.6.zip
Normal file
Binary file not shown.
200
dash/staging/kterm/kterm-release-latest.json
Normal file
200
dash/staging/kterm/kterm-release-latest.json
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/26772867",
|
||||||
|
"assets_url": "https://api.github.com/repos/bfabiszewski/kterm/releases/26772867/assets",
|
||||||
|
"upload_url": "https://uploads.github.com/repos/bfabiszewski/kterm/releases/26772867/assets{?name,label}",
|
||||||
|
"html_url": "https://github.com/bfabiszewski/kterm/releases/tag/v2.6",
|
||||||
|
"id": 26772867,
|
||||||
|
"author": {
|
||||||
|
"login": "bfabiszewski",
|
||||||
|
"id": 3366666,
|
||||||
|
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/bfabiszewski",
|
||||||
|
"html_url": "https://github.com/bfabiszewski",
|
||||||
|
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
|
||||||
|
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
|
||||||
|
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"user_view_type": "public",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"node_id": "MDc6UmVsZWFzZTI2NzcyODY3",
|
||||||
|
"tag_name": "v2.6",
|
||||||
|
"target_commitish": "master",
|
||||||
|
"name": "v2.6",
|
||||||
|
"draft": false,
|
||||||
|
"immutable": false,
|
||||||
|
"prerelease": false,
|
||||||
|
"created_at": "2020-05-21T20:24:58Z",
|
||||||
|
"updated_at": "2025-04-10T20:16:30Z",
|
||||||
|
"published_at": "2020-05-21T20:35:06Z",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/221834164",
|
||||||
|
"id": 221834164,
|
||||||
|
"node_id": "RA_kwDOAHb9H84NOOu0",
|
||||||
|
"name": "kterm-kindle-2.6-armhf.zip",
|
||||||
|
"label": null,
|
||||||
|
"uploader": {
|
||||||
|
"login": "bfabiszewski",
|
||||||
|
"id": 3366666,
|
||||||
|
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/bfabiszewski",
|
||||||
|
"html_url": "https://github.com/bfabiszewski",
|
||||||
|
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
|
||||||
|
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
|
||||||
|
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"user_view_type": "public",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"content_type": "application/zip",
|
||||||
|
"state": "uploaded",
|
||||||
|
"size": 411954,
|
||||||
|
"digest": null,
|
||||||
|
"download_count": 30184,
|
||||||
|
"created_at": "2025-01-20T12:54:44Z",
|
||||||
|
"updated_at": "2025-01-20T12:54:46Z",
|
||||||
|
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6-armhf.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/221834154",
|
||||||
|
"id": 221834154,
|
||||||
|
"node_id": "RA_kwDOAHb9H84NOOuq",
|
||||||
|
"name": "kterm-kindle-2.6-armhf.zip.sig",
|
||||||
|
"label": null,
|
||||||
|
"uploader": {
|
||||||
|
"login": "bfabiszewski",
|
||||||
|
"id": 3366666,
|
||||||
|
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/bfabiszewski",
|
||||||
|
"html_url": "https://github.com/bfabiszewski",
|
||||||
|
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
|
||||||
|
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
|
||||||
|
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"user_view_type": "public",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"content_type": "application/octet-stream",
|
||||||
|
"state": "uploaded",
|
||||||
|
"size": 310,
|
||||||
|
"digest": null,
|
||||||
|
"download_count": 201,
|
||||||
|
"created_at": "2025-01-20T12:54:43Z",
|
||||||
|
"updated_at": "2025-01-20T12:54:44Z",
|
||||||
|
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6-armhf.zip.sig"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/20954116",
|
||||||
|
"id": 20954116,
|
||||||
|
"node_id": "MDEyOlJlbGVhc2VBc3NldDIwOTU0MTE2",
|
||||||
|
"name": "kterm-kindle-2.6.zip",
|
||||||
|
"label": null,
|
||||||
|
"uploader": {
|
||||||
|
"login": "bfabiszewski",
|
||||||
|
"id": 3366666,
|
||||||
|
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/bfabiszewski",
|
||||||
|
"html_url": "https://github.com/bfabiszewski",
|
||||||
|
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
|
||||||
|
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
|
||||||
|
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"user_view_type": "public",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"content_type": "application/zip",
|
||||||
|
"state": "uploaded",
|
||||||
|
"size": 417136,
|
||||||
|
"digest": null,
|
||||||
|
"download_count": 17785,
|
||||||
|
"created_at": "2020-05-21T20:35:43Z",
|
||||||
|
"updated_at": "2020-05-21T20:35:49Z",
|
||||||
|
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/221772992",
|
||||||
|
"id": 221772992,
|
||||||
|
"node_id": "RA_kwDOAHb9H84NN_zA",
|
||||||
|
"name": "kterm-kindle-2.6.zip.sig",
|
||||||
|
"label": null,
|
||||||
|
"uploader": {
|
||||||
|
"login": "bfabiszewski",
|
||||||
|
"id": 3366666,
|
||||||
|
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/bfabiszewski",
|
||||||
|
"html_url": "https://github.com/bfabiszewski",
|
||||||
|
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
|
||||||
|
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
|
||||||
|
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"user_view_type": "public",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"content_type": "application/octet-stream",
|
||||||
|
"state": "uploaded",
|
||||||
|
"size": 310,
|
||||||
|
"digest": null,
|
||||||
|
"download_count": 135,
|
||||||
|
"created_at": "2025-01-20T07:58:01Z",
|
||||||
|
"updated_at": "2025-01-20T07:58:01Z",
|
||||||
|
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6.zip.sig"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tarball_url": "https://api.github.com/repos/bfabiszewski/kterm/tarball/v2.6",
|
||||||
|
"zipball_url": "https://api.github.com/repos/bfabiszewski/kterm/zipball/v2.6",
|
||||||
|
"body": "- add option to set cursor shape (thanks @efskap !)\r\n\r\n---\r\nfor firmware newer than 5.16.3 use a hard float version with `-armhf` suffix",
|
||||||
|
"reactions": {
|
||||||
|
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/26772867/reactions",
|
||||||
|
"total_count": 12,
|
||||||
|
"+1": 0,
|
||||||
|
"-1": 0,
|
||||||
|
"laugh": 0,
|
||||||
|
"hooray": 0,
|
||||||
|
"confused": 0,
|
||||||
|
"heart": 12,
|
||||||
|
"rocket": 0,
|
||||||
|
"eyes": 0
|
||||||
|
},
|
||||||
|
"mentions_count": 1
|
||||||
|
}
|
||||||
@@ -11,35 +11,69 @@ STATE_DIR="$DIR/local/state"
|
|||||||
BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at"
|
BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at"
|
||||||
THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env"
|
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"
|
||||||
|
PID_FILE="$STATE_DIR/dashboard.pid"
|
||||||
|
THEME_BACKGROUND_SYNC_CMD="$DIR/local/sync-theme-backgrounds.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_SERVICE_CMD="$DIR/local/touch-home-service.sh"
|
||||||
TOUCH_HOME_LOG_FILE="$DIR/logs/touch-home.log"
|
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"}
|
||||||
|
SCHEDULED_SCREEN_WAKE_ENABLED=${SCHEDULED_SCREEN_WAKE_ENABLED:-true}
|
||||||
|
REMOTE_SYNC_SCHEDULE=${REMOTE_SYNC_SCHEDULE:-"10 0 * * *"}
|
||||||
|
REMOTE_SYNC_ENABLED=${REMOTE_SYNC_ENABLED:-true}
|
||||||
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}
|
DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false}
|
||||||
BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
||||||
CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15}
|
CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15}
|
||||||
PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
||||||
MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60}
|
MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300}
|
||||||
MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
||||||
|
SLEEPING_SCREEN_ENABLED=${SLEEPING_SCREEN_ENABLED:-true}
|
||||||
STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
||||||
STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
||||||
STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
||||||
STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372}
|
STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360}
|
||||||
STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24}
|
STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48}
|
||||||
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_WAKEUP_ENABLE=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||||
KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false}
|
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
|
background_needs_redraw=true
|
||||||
|
dashboard_exit_reason=normal
|
||||||
|
|
||||||
|
on_dashboard_term() {
|
||||||
|
dashboard_exit_reason=SIGTERM
|
||||||
|
echo "Dashboard received SIGTERM"
|
||||||
|
exit 143
|
||||||
|
}
|
||||||
|
|
||||||
|
on_dashboard_hup() {
|
||||||
|
dashboard_exit_reason=SIGHUP
|
||||||
|
echo "Dashboard received SIGHUP"
|
||||||
|
exit 129
|
||||||
|
}
|
||||||
|
|
||||||
|
on_dashboard_int() {
|
||||||
|
dashboard_exit_reason=SIGINT
|
||||||
|
echo "Dashboard received SIGINT"
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
on_dashboard_exit() {
|
||||||
|
exit_status=$?
|
||||||
|
echo "Dashboard exiting status=$exit_status reason=$dashboard_exit_reason pid=$$"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap on_dashboard_term TERM
|
||||||
|
trap on_dashboard_hup HUP
|
||||||
|
trap on_dashboard_int INT
|
||||||
|
trap on_dashboard_exit EXIT
|
||||||
|
|
||||||
start_theme_menu_service() {
|
start_theme_menu_service() {
|
||||||
if [ "${THEME_MENU_ENABLED:-false}" != true ]; then
|
if [ "${THEME_MENU_ENABLED:-false}" != true ]; then
|
||||||
@@ -77,14 +111,13 @@ load_theme_runtime_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
if [ -z "$TIMEZONE" ]; then
|
||||||
echo "Missing required configuration."
|
echo "Missing required configuration."
|
||||||
echo "Timezone: ${TIMEZONE:-(not set)}."
|
echo "Timezone: ${TIMEZONE:-(not set)}."
|
||||||
echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
echo "Starting dashboard with refresh_schedule=${REFRESH_SCHEDULE:-disabled} sync_schedule=${REMOTE_SYNC_SCHEDULE:-disabled} scheduled_screen_wake=${SCHEDULED_SCREEN_WAKE_ENABLED}..."
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
@@ -99,7 +132,7 @@ init() {
|
|||||||
fi
|
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
|
keep_screen_visible
|
||||||
start_theme_menu_service
|
start_theme_menu_service
|
||||||
start_touch_home_service
|
start_touch_home_service
|
||||||
}
|
}
|
||||||
@@ -118,13 +151,20 @@ stop_framework() {
|
|||||||
prepare_sleep() {
|
prepare_sleep() {
|
||||||
echo "Preparing sleep"
|
echo "Preparing sleep"
|
||||||
|
|
||||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
if [ "$SLEEPING_SCREEN_ENABLED" = true ]; then
|
||||||
background_needs_redraw=true
|
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||||
|
background_needs_redraw=true
|
||||||
|
|
||||||
# Give screen time to refresh
|
# 只有真的画了 sleeping.png,才需要额外等待这次整屏刷新落完。
|
||||||
sleep 2
|
sleep 2
|
||||||
|
else
|
||||||
|
# overlay/静默同步模式下,直接保留当前 dashboard 画面直到真正挂起,
|
||||||
|
# 避免 Voyage 把 600x800 的 sleeping.png 只画在左上角,压住日历内容。
|
||||||
|
echo "Sleeping screen disabled, keeping current dashboard visible before suspend."
|
||||||
|
fi
|
||||||
|
|
||||||
# Ensure a full screen refresh is triggered after wake from sleep
|
# 无论休眠前是否显示 sleeping.png,唤醒后的下一次恢复都强制走一次整屏刷新,
|
||||||
|
# 避免长时间 suspend 之后局部刷新把残影继续带到下一轮。
|
||||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +183,29 @@ background_refresh_due() {
|
|||||||
last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE")
|
last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE")
|
||||||
refresh_interval_seconds=$((BACKGROUND_REFRESH_INTERVAL_MINUTES * 60))
|
refresh_interval_seconds=$((BACKGROUND_REFRESH_INTERVAL_MINUTES * 60))
|
||||||
|
|
||||||
|
# 背景里包含当天日历时,不能只按“距离上次刷新过去了多少分钟”判断。
|
||||||
|
# 只要北京时间跨天了,就应该在下一轮调度里至少再拉一次新背景。
|
||||||
|
current_day_id=$(lua - "$current_epoch" "${CLOCK_TIME_OFFSET_MINUTES:-0}" <<'LUA'
|
||||||
|
local epoch_seconds = assert(tonumber(arg[1]), "missing epoch seconds")
|
||||||
|
local offset_minutes = tonumber(arg[2]) or 0
|
||||||
|
local seconds_per_day = 24 * 60 * 60
|
||||||
|
local local_seconds = epoch_seconds + offset_minutes * 60
|
||||||
|
io.write(math.floor(local_seconds / seconds_per_day))
|
||||||
|
LUA
|
||||||
|
)
|
||||||
|
last_day_id=$(lua - "$last_background_epoch" "${CLOCK_TIME_OFFSET_MINUTES:-0}" <<'LUA'
|
||||||
|
local epoch_seconds = assert(tonumber(arg[1]), "missing epoch seconds")
|
||||||
|
local offset_minutes = tonumber(arg[2]) or 0
|
||||||
|
local seconds_per_day = 24 * 60 * 60
|
||||||
|
local local_seconds = epoch_seconds + offset_minutes * 60
|
||||||
|
io.write(math.floor(local_seconds / seconds_per_day))
|
||||||
|
LUA
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$current_day_id" != "$last_day_id" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
[ $((current_epoch - last_background_epoch)) -ge "$refresh_interval_seconds" ]
|
[ $((current_epoch - last_background_epoch)) -ge "$refresh_interval_seconds" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +239,161 @@ fetch_background() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screen_wake_enabled() {
|
||||||
|
# 这里的语义只代表“分钟刷新时是否把屏幕维持在可视态”。
|
||||||
|
# 分钟调度本身是否存在,不能再由这个开关决定。
|
||||||
|
# debug on 的目标就是高频亮屏调试,所以这里直接强制开启可见刷新。
|
||||||
|
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$SCHEDULED_SCREEN_WAKE_ENABLED" = true ]
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_schedule_enabled() {
|
||||||
|
[ -n "${REFRESH_SCHEDULE:-}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_screen_visible() {
|
||||||
|
# 可视窗口和 debug on 都需要把 powerd 留在可视态,
|
||||||
|
# 否则刚画完时钟就可能被系统重新收回到睡眠界面。
|
||||||
|
lipc-set-prop com.lab126.powerd preventScreenSaver 1 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
allow_screen_sleep() {
|
||||||
|
# debug on 需要常亮;普通低功耗模式下才把屏幕控制权交还给 powerd。
|
||||||
|
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
|
keep_screen_visible
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
lipc-set-prop com.lab126.powerd preventScreenSaver 0 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_sync_enabled() {
|
||||||
|
[ "$REMOTE_SYNC_ENABLED" = true ]
|
||||||
|
}
|
||||||
|
|
||||||
|
next_schedule_seconds() {
|
||||||
|
schedule=$1
|
||||||
|
"$DIR/next-wakeup" --schedule="$schedule" --timezone="$TIMEZONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_next_wakeup() {
|
||||||
|
max_wait=2147483647
|
||||||
|
next_display_secs=$max_wait
|
||||||
|
next_sync_secs=$max_wait
|
||||||
|
|
||||||
|
# debug off 即使不允许自动亮屏,分钟级时钟刷新也必须继续调度。
|
||||||
|
if refresh_schedule_enabled; then
|
||||||
|
next_display_secs=$(next_schedule_seconds "$REFRESH_SCHEDULE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if remote_sync_enabled && [ -n "${REMOTE_SYNC_SCHEDULE:-}" ]; then
|
||||||
|
next_sync_secs=$(next_schedule_seconds "$REMOTE_SYNC_SCHEDULE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$next_display_secs" -eq "$max_wait" ] && [ "$next_sync_secs" -eq "$max_wait" ]; then
|
||||||
|
echo "next_wakeup_reason=display"
|
||||||
|
echo "next_wakeup_secs=3600"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$next_display_secs" -le "$next_sync_secs" ]; then
|
||||||
|
echo "next_wakeup_reason=display"
|
||||||
|
echo "next_wakeup_secs=$next_display_secs"
|
||||||
|
else
|
||||||
|
echo "next_wakeup_reason=sync"
|
||||||
|
echo "next_wakeup_secs=$next_sync_secs"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
silent_sync() {
|
||||||
|
sync_failed=false
|
||||||
|
|
||||||
|
# 每天 00:10 的静默唤醒先把全量主题图和主题 JSON 拉到本地,
|
||||||
|
# 这样后续切主题可以完全走本地目录,不依赖当下网络。
|
||||||
|
if ! "$THEME_BACKGROUND_SYNC_CMD"; then
|
||||||
|
echo "Theme background sync failed"
|
||||||
|
sync_failed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if background_refresh_due; then
|
||||||
|
if fetch_background; then
|
||||||
|
# 后台静默同步时只更新缓存,不主动点亮 screen。
|
||||||
|
# 下次手动点亮或进入 dashboard 时,再把新背景完整恢复到屏幕上。
|
||||||
|
background_needs_redraw=true
|
||||||
|
else
|
||||||
|
echo "Silent sync background refresh failed"
|
||||||
|
sync_failed=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Silent sync skipped current background refresh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$sync_failed" = true ]; then
|
||||||
|
echo "Silent sync failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Silent sync completed"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_dashboard_hidden() {
|
||||||
|
if background_refresh_due; then
|
||||||
|
if fetch_background; then
|
||||||
|
# 普通分钟调度在 debug off 下只更新缓存,不主动把新背景刷到屏幕上。
|
||||||
|
background_needs_redraw=true
|
||||||
|
elif [ ! -f "$BACKGROUND_PNG" ]; then
|
||||||
|
echo "No cached background available for hidden refresh."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Clock cache refresh without screen wake"
|
||||||
|
"$CLOCK_RENDER_CMD" false false
|
||||||
|
}
|
||||||
|
|
||||||
clock_force_full_refresh() {
|
clock_force_full_refresh() {
|
||||||
eval "$("$DIR/local/clock-index.sh")"
|
eval "$("$DIR/local/clock-index.sh")"
|
||||||
[ $((minute % CLOCK_FULL_REFRESH_INTERVAL_MINUTES)) -eq 0 ]
|
[ $((minute % CLOCK_FULL_REFRESH_INTERVAL_MINUTES)) -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hold_visible_window_until() {
|
||||||
|
keep_awake_until=$1
|
||||||
|
|
||||||
|
# 只要用户当前正看着 calendar,这段窗口里就继续让分钟刷新走可视路径。
|
||||||
|
keep_screen_visible
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
now=$(now_epoch)
|
||||||
|
if [ "$now" -ge "$keep_awake_until" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
seconds_until_next_minute=$((60 - (now % 60)))
|
||||||
|
remaining_awake_seconds=$((keep_awake_until - now))
|
||||||
|
|
||||||
|
if [ "$seconds_until_next_minute" -gt "$remaining_awake_seconds" ]; then
|
||||||
|
sleep "$remaining_awake_seconds"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$seconds_until_next_minute"
|
||||||
|
refresh_dashboard || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
mask_system_status_overlay_once() {
|
||||||
|
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
fbink -q -V -B WHITE -k \
|
||||||
|
"top=$STATUS_MASK_TOP,left=$STATUS_MASK_LEFT,width=$STATUS_MASK_WIDTH,height=$STATUS_MASK_HEIGHT"
|
||||||
|
}
|
||||||
|
|
||||||
mask_system_status_overlay() {
|
mask_system_status_overlay() {
|
||||||
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
||||||
return
|
return
|
||||||
@@ -191,8 +404,7 @@ mask_system_status_overlay() {
|
|||||||
# 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。
|
# 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。
|
||||||
pass=1
|
pass=1
|
||||||
while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do
|
while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do
|
||||||
fbink -q -V -B WHITE -k \
|
mask_system_status_overlay_once
|
||||||
"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
|
if [ "$pass" -lt "$STATUS_MASK_PASSES" ] && [ "$STATUS_MASK_DELAY_SECONDS" -gt 0 ]; then
|
||||||
sleep "$STATUS_MASK_DELAY_SECONDS"
|
sleep "$STATUS_MASK_DELAY_SECONDS"
|
||||||
@@ -255,6 +467,24 @@ powerd_get_prop() {
|
|||||||
lipc-get-prop com.lab126.powerd "$prop_name" 2>/dev/null || echo "unavailable"
|
lipc-get-prop com.lab126.powerd "$prop_name" 2>/dev/null || echo "unavailable"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screen_is_visibly_active() {
|
||||||
|
power_state=$(powerd_get_prop state)
|
||||||
|
case "$power_state" in
|
||||||
|
active|Active)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
power_status=$(powerd_get_prop status)
|
||||||
|
case "$power_status" in
|
||||||
|
*"Powerd state: Active"*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
log_battery_stats() {
|
log_battery_stats() {
|
||||||
battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown")
|
battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown")
|
||||||
charging_state=$(powerd_get_prop isCharging)
|
charging_state=$(powerd_get_prop isCharging)
|
||||||
@@ -285,8 +515,36 @@ rtc_sleep() {
|
|||||||
echo "Skipping system suspend, sleeping for ${duration}s instead"
|
echo "Skipping system suspend, sleeping for ${duration}s instead"
|
||||||
sleep "$duration"
|
sleep "$duration"
|
||||||
else
|
else
|
||||||
# shellcheck disable=SC2039
|
# Voyage 这代机器不一定还有旧的 mxc_rtc.0/wakeup_enable 接口;
|
||||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
# 当前实机上能稳定看到的是 /sys/class/rtc/rtc0/wakealarm。
|
||||||
|
# 这里先兼容旧接口,找不到时再回退到标准 wakealarm 写法。
|
||||||
|
if [ -f "$RTC_WAKEUP_ENABLE" ]; then
|
||||||
|
current_wakeup_value=$(cat "$RTC_WAKEUP_ENABLE" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$current_wakeup_value" ] && [ "$current_wakeup_value" -eq 0 ] 2>/dev/null; then
|
||||||
|
echo -n "$duration" >"$RTC_WAKEUP_ENABLE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rtc_wakealarm_path=""
|
||||||
|
|
||||||
|
if [ -f /sys/class/rtc/rtc0/wakealarm ]; then
|
||||||
|
rtc_wakealarm_path=/sys/class/rtc/rtc0/wakealarm
|
||||||
|
elif [ -f /sys/class/rtc/rtc1/wakealarm ]; then
|
||||||
|
rtc_wakealarm_path=/sys/class/rtc/rtc1/wakealarm
|
||||||
|
elif [ -f /sys/class/rtc/rtc2/wakealarm ]; then
|
||||||
|
rtc_wakealarm_path=/sys/class/rtc/rtc2/wakealarm
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$rtc_wakealarm_path" ]; then
|
||||||
|
wake_epoch=$(( $(now_epoch) + duration ))
|
||||||
|
echo 0 >"$rtc_wakealarm_path" 2>/dev/null || true
|
||||||
|
echo "$wake_epoch" >"$rtc_wakealarm_path"
|
||||||
|
else
|
||||||
|
echo "警告:未找到可用的 RTC 唤醒接口,回退到普通 sleep ${duration}s"
|
||||||
|
sleep "$duration"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo "mem" >/sys/power/state
|
echo "mem" >/sys/power/state
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -311,6 +569,7 @@ hold_after_manual_wake() {
|
|||||||
sleep_started_at=$2
|
sleep_started_at=$2
|
||||||
sleep_finished_at=$3
|
sleep_finished_at=$3
|
||||||
actual_duration=$((sleep_finished_at - sleep_started_at))
|
actual_duration=$((sleep_finished_at - sleep_started_at))
|
||||||
|
keep_awake_until=$((sleep_finished_at + MANUAL_WAKE_KEEP_AWAKE_SECONDS))
|
||||||
|
|
||||||
if ! manual_wake_detected "$requested_duration" "$actual_duration"; then
|
if ! manual_wake_detected "$requested_duration" "$actual_duration"; then
|
||||||
return
|
return
|
||||||
@@ -318,17 +577,22 @@ hold_after_manual_wake() {
|
|||||||
|
|
||||||
echo "Manual wake detected after ${actual_duration}s, keeping awake for ${MANUAL_WAKE_KEEP_AWAKE_SECONDS}s"
|
echo "Manual wake detected after ${actual_duration}s, keeping awake for ${MANUAL_WAKE_KEEP_AWAKE_SECONDS}s"
|
||||||
|
|
||||||
|
# 手动唤醒期间,系统可能已经把自己的睡眠/锁屏层画回前台。
|
||||||
|
# 这里先强制要求下一次 refresh_dashboard 完整恢复 calendar 背景,
|
||||||
|
# 不能只补一层时钟 patch。
|
||||||
|
background_needs_redraw=true
|
||||||
|
|
||||||
# 短按电源键提前唤醒后,先把 dashboard 内容恢复回来,
|
# 短按电源键提前唤醒后,先把 dashboard 内容恢复回来,
|
||||||
# 再给出一段明确的可交互窗口,避免 2~3 秒内再次休眠。
|
# 再在这段明确的可交互窗口里按分钟补画时钟,避免只刷一次后就停住。
|
||||||
refresh_dashboard || true
|
refresh_dashboard || true
|
||||||
sleep "$MANUAL_WAKE_KEEP_AWAKE_SECONDS"
|
hold_visible_window_until "$keep_awake_until"
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
eval "$(compute_next_wakeup)"
|
||||||
|
|
||||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then
|
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then
|
||||||
action="sleep"
|
action="sleep"
|
||||||
@@ -337,8 +601,12 @@ main_loop() {
|
|||||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||||
echo "Debug mode active, skipping sleeping screen."
|
echo "Debug mode active, skipping sleeping screen."
|
||||||
fi
|
fi
|
||||||
action="suspend"
|
|
||||||
refresh_dashboard
|
if [ "$next_wakeup_reason" = "sync" ]; then
|
||||||
|
action="silent-sync"
|
||||||
|
else
|
||||||
|
action="suspend"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
actual_sleep_secs=$next_wakeup_secs
|
actual_sleep_secs=$next_wakeup_secs
|
||||||
@@ -350,6 +618,13 @@ main_loop() {
|
|||||||
|
|
||||||
# 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。
|
# 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。
|
||||||
# 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。
|
# 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。
|
||||||
|
# 普通分钟调度已经改成隐藏刷新,因此 debug off 下应该把屏幕控制权交还给 powerd,
|
||||||
|
# 避免设备每分钟都被重新拉成可视亮屏态。
|
||||||
|
if screen_wake_enabled; then
|
||||||
|
keep_screen_visible
|
||||||
|
else
|
||||||
|
allow_screen_sleep
|
||||||
|
fi
|
||||||
sleep "$PRE_SLEEP_GRACE_SECONDS"
|
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"
|
||||||
@@ -357,9 +632,46 @@ main_loop() {
|
|||||||
sleep_started_at=$(now_epoch)
|
sleep_started_at=$(now_epoch)
|
||||||
rtc_sleep "$actual_sleep_secs"
|
rtc_sleep "$actual_sleep_secs"
|
||||||
sleep_finished_at=$(now_epoch)
|
sleep_finished_at=$(now_epoch)
|
||||||
hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at"
|
|
||||||
|
# Voyage 在 rtc 唤醒刚回来的瞬间,右上角系统状态栏可能会先闪回前台。
|
||||||
|
# 只要系统已经把屏幕拉成可视态,就先立即补一层白色遮罩,
|
||||||
|
# 再进入后面的手动唤醒判断和 dashboard 刷新,尽量缩短这段闪现时间。
|
||||||
|
if screen_is_visibly_active; then
|
||||||
|
mask_system_status_overlay_once
|
||||||
|
fi
|
||||||
|
|
||||||
|
if manual_wake_detected "$actual_sleep_secs" "$((sleep_finished_at - sleep_started_at))"; then
|
||||||
|
hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$next_wakeup_reason" = "sync" ]; then
|
||||||
|
silent_sync || true
|
||||||
|
else
|
||||||
|
if screen_wake_enabled; then
|
||||||
|
refresh_dashboard || true
|
||||||
|
elif screen_is_visibly_active; then
|
||||||
|
# Voyage 在当前 overlay + RTC suspend 架构下,
|
||||||
|
# 普通分钟唤醒有时会被系统自己拉成可视态。
|
||||||
|
# 既然用户已经看得到这次唤醒,就顺手把时钟补到当前分钟,
|
||||||
|
# 避免出现“亮灯了,但表盘还是旧时间”的错觉。
|
||||||
|
refresh_dashboard || true
|
||||||
|
else
|
||||||
|
refresh_dashboard_hidden || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
init
|
init
|
||||||
|
printf '%s\n' "$$" >"$PID_FILE"
|
||||||
|
echo "Dashboard boot pid=$$ at $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
|
refresh_dashboard || true
|
||||||
|
|
||||||
|
# 从 KUAL 进入 dashboard 或主题切换后回到 calendar,本身就是一次显式用户操作。
|
||||||
|
# debug off 下也应该给同样的 5 分钟可视窗口,让用户回来后看到会继续走动的时钟。
|
||||||
|
if [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then
|
||||||
|
hold_visible_window_until $(( $(now_epoch) + MANUAL_WAKE_KEEP_AWAKE_SECONDS ))
|
||||||
|
fi
|
||||||
|
|
||||||
main_loop
|
main_loop
|
||||||
|
|||||||
@@ -2,19 +2,99 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
DIR="$(dirname "$0")"
|
DIR="$(dirname "$0")"
|
||||||
|
ENV_FILE="$DIR/local/env.sh"
|
||||||
SWITCH_THEME_CMD="$DIR/switch-theme.sh"
|
SWITCH_THEME_CMD="$DIR/switch-theme.sh"
|
||||||
LAUNCH_FROM_KUAL_CMD="$DIR/launch-from-kual.sh"
|
START_DASHBOARD_CMD="$DIR/start.sh"
|
||||||
|
LOG_FILE="$DIR/logs/kual-theme-launch.log"
|
||||||
|
|
||||||
requested_theme_id=${1:?"usage: launch-theme-from-kual.sh <theme-id> [orientation]"}
|
requested_theme_id=${1:?"usage: launch-theme-from-kual.sh <theme-id> [orientation]"}
|
||||||
requested_orientation=${2:-}
|
requested_orientation=${2:-}
|
||||||
|
|
||||||
# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。
|
# shellcheck disable=SC1090
|
||||||
# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑,
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
# 同时把“选主题”前移到进入 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"
|
KUAL_QUIT_GRACE_SECONDS="${KUAL_QUIT_GRACE_SECONDS:-0}"
|
||||||
|
KUAL_APP_ID="${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}"
|
||||||
|
|
||||||
|
# KUAL 菜单项本身应该尽快返回,让 KUAL 自己先退回首页。
|
||||||
|
# 真正的“切主题 + 启动 dashboard”放到独立 session 里继续跑,
|
||||||
|
# 避免动作链被 KUAL 退出过程提前打断。
|
||||||
|
# 这里直接起 start.sh,不再额外绕一层 launch-from-kual.sh,
|
||||||
|
# 尽量减少首页闪出的可见窗口。
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
|
if command -v setsid >/dev/null 2>&1; then
|
||||||
|
nohup setsid /bin/sh -c '
|
||||||
|
theme_id=$1
|
||||||
|
theme_orientation=$2
|
||||||
|
switch_cmd=$3
|
||||||
|
start_cmd=$4
|
||||||
|
log_file=$5
|
||||||
|
target_dir=$6
|
||||||
|
kual_app_id=$7
|
||||||
|
quit_grace=$8
|
||||||
|
|
||||||
|
printf "%s launch-theme worker start theme=%s orientation=%s\n" "$(date 2>/dev/null || true)" "$theme_id" "${theme_orientation:-default}" >>"$log_file"
|
||||||
|
|
||||||
|
# 旧 dashboard 还活着时,会继续按旧状态补画时钟,导致画面叠层。
|
||||||
|
# 这里在真正切主题前先清掉旧实例,确保后面只剩一条主循环。
|
||||||
|
pkill -f "$target_dir/dash.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/theme-menu-service.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/touch-home-service.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 主题链路也沿用 KUAL 的正常退出路径,避免我们刚把 calendar 画出来,
|
||||||
|
# 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
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$quit_grace" -gt 0 ] 2>/dev/null; then
|
||||||
|
sleep "$quit_grace"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$theme_orientation" ]; then
|
||||||
|
"$switch_cmd" "$theme_id" "$theme_orientation" >>"$log_file" 2>&1
|
||||||
|
else
|
||||||
|
"$switch_cmd" "$theme_id" >>"$log_file" 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$start_cmd" >>"$log_file" 2>&1
|
||||||
|
' sh "$requested_theme_id" "$requested_orientation" "$SWITCH_THEME_CMD" "$START_DASHBOARD_CMD" "$LOG_FILE" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >/dev/null 2>&1 &
|
||||||
|
else
|
||||||
|
nohup /bin/sh -c '
|
||||||
|
theme_id=$1
|
||||||
|
theme_orientation=$2
|
||||||
|
switch_cmd=$3
|
||||||
|
start_cmd=$4
|
||||||
|
log_file=$5
|
||||||
|
target_dir=$6
|
||||||
|
kual_app_id=$7
|
||||||
|
quit_grace=$8
|
||||||
|
|
||||||
|
printf "%s launch-theme worker start theme=%s orientation=%s\n" "$(date 2>/dev/null || true)" "$theme_id" "${theme_orientation:-default}" >>"$log_file"
|
||||||
|
|
||||||
|
# 旧 dashboard 还活着时,会继续按旧状态补画时钟,导致画面叠层。
|
||||||
|
# 这里在真正切主题前先清掉旧实例,确保后面只剩一条主循环。
|
||||||
|
pkill -f "$target_dir/dash.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/theme-menu-service.sh" 2>/dev/null || true
|
||||||
|
pkill -f "$target_dir/local/touch-home-service.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 主题链路也沿用 KUAL 的正常退出路径,避免我们刚把 calendar 画出来,
|
||||||
|
# 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
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$quit_grace" -gt 0 ] 2>/dev/null; then
|
||||||
|
sleep "$quit_grace"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$theme_orientation" ]; then
|
||||||
|
"$switch_cmd" "$theme_id" "$theme_orientation" >>"$log_file" 2>&1
|
||||||
|
else
|
||||||
|
"$switch_cmd" "$theme_id" >>"$log_file" 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$start_cmd" >>"$log_file" 2>&1
|
||||||
|
' sh "$requested_theme_id" "$requested_orientation" "$SWITCH_THEME_CMD" "$START_DASHBOARD_CMD" "$LOG_FILE" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >/dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
# Export environment variables here
|
# Export environment variables here
|
||||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
# 设备侧时钟刷新节奏。
|
||||||
|
# 当前默认每分钟刷新一次;debug on 与 debug off 都走这条分钟调度。
|
||||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||||
|
# 是否允许按 REFRESH_SCHEDULE 自动点亮 screen。
|
||||||
|
# 默认关闭;debug off 下分钟刷新仍会执行,但不会因为调度本身把屏幕维持在可视态。
|
||||||
|
# 只有 debug on,或手动短按 power 进入的 5 分钟窗口,才把屏幕留在可视态。
|
||||||
|
export SCHEDULED_SCREEN_WAKE_ENABLED=${SCHEDULED_SCREEN_WAKE_ENABLED:-false}
|
||||||
|
# 静默远端同步的唤醒节奏。
|
||||||
|
# 默认每天凌晨 00:10 唤醒一次,用来拉当天最新背景、主题 JSON 和全量主题图片,
|
||||||
|
# 但不主动点亮 screen。
|
||||||
|
export REMOTE_SYNC_SCHEDULE=${REMOTE_SYNC_SCHEDULE:-"10 0 * * *"}
|
||||||
|
export REMOTE_SYNC_ENABLED=${REMOTE_SYNC_ENABLED:-true}
|
||||||
# 调度计算依赖 next-wakeup 这个 Rust 程序,它要求使用 IANA 时区名。
|
# 调度计算依赖 next-wakeup 这个 Rust 程序,它要求使用 IANA 时区名。
|
||||||
# 这里必须保留 Asia/Shanghai,才能正确计算下一次唤醒时间。
|
# 这里必须保留 Asia/Shanghai,才能正确计算下一次唤醒时间。
|
||||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||||
@@ -16,7 +26,16 @@ export THEMES_INDEX_REFRESH_INTERVAL_MINUTES=${THEMES_INDEX_REFRESH_INTERVAL_MIN
|
|||||||
export THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440}
|
export THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440}
|
||||||
export THEME_ID=${THEME_ID:-"default"}
|
export THEME_ID=${THEME_ID:-"default"}
|
||||||
export ORIENTATION=${ORIENTATION:-"portrait"}
|
export ORIENTATION=${ORIENTATION:-"portrait"}
|
||||||
export BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
export WEATHER_USE_KINDLE_LOCATION=${WEATHER_USE_KINDLE_LOCATION:-true}
|
||||||
|
export LOCATION_GEOIP_URL=${LOCATION_GEOIP_URL:-"https://ipwho.is/"}
|
||||||
|
export LOCATION_REFRESH_INTERVAL_MINUTES=${LOCATION_REFRESH_INTERVAL_MINUTES:-720}
|
||||||
|
export LOCATION_FALLBACK_CITY=${LOCATION_FALLBACK_CITY:-"杭州"}
|
||||||
|
export LOCATION_FALLBACK_LAT=${LOCATION_FALLBACK_LAT:-30.274084}
|
||||||
|
export LOCATION_FALLBACK_LON=${LOCATION_FALLBACK_LON:-120.155070}
|
||||||
|
export LOCATION_FALLBACK_TIMEZONE=${LOCATION_FALLBACK_TIMEZONE:-"Asia/Shanghai"}
|
||||||
|
# 日历背景在跨天时必须至少更新一次;平时不需要高频拉图。
|
||||||
|
# 这里默认按 24 小时做兜底,同时 dash.sh 里还会在北京时间跨天后强制补一次刷新。
|
||||||
|
export BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-1440}
|
||||||
export CLOCK_REGION_X=${CLOCK_REGION_X:-347}
|
export CLOCK_REGION_X=${CLOCK_REGION_X:-347}
|
||||||
export CLOCK_REGION_Y=${CLOCK_REGION_Y:-55}
|
export CLOCK_REGION_Y=${CLOCK_REGION_Y:-55}
|
||||||
export CLOCK_REGION_WIDTH=${CLOCK_REGION_WIDTH:-220}
|
export CLOCK_REGION_WIDTH=${CLOCK_REGION_WIDTH:-220}
|
||||||
@@ -44,18 +63,22 @@ export CLOCK_CENTER_RADIUS=${CLOCK_CENTER_RADIUS:-7}
|
|||||||
# 这段时间会从真正的休眠时长里扣掉,避免分钟刷新慢一拍。
|
# 这段时间会从真正的休眠时长里扣掉,避免分钟刷新慢一拍。
|
||||||
export PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
export PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
||||||
# 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。
|
# 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。
|
||||||
# 这样用户有足够时间看屏、切主题或继续交互,而不会立刻再次休眠。
|
# debug off 下这里固定为 300 秒,也就是按 power 之后保持 5 分钟可视窗口。
|
||||||
export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60}
|
export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300}
|
||||||
# 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。
|
# 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。
|
||||||
export MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
export MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
||||||
|
# 默认 overlay/静默同步模式下,不再提前显示 sleeping.png。
|
||||||
|
# Voyage 上这张图只有 600x800,提前显示时会只占左上角一块并盖住当前 calendar。
|
||||||
|
# 如需恢复旧的“休眠前先显示提示图”行为,可手动改回 true。
|
||||||
|
export SLEEPING_SCREEN_ENABLED=${SLEEPING_SCREEN_ENABLED:-false}
|
||||||
|
|
||||||
# Voyage 顶部状态栏遮罩:用于压住系统偶尔重画出来的时间、Wi-Fi、电池图标。
|
# Voyage 顶部状态栏遮罩:用于压住系统偶尔重画出来的时间、Wi‑Fi、电池图标。
|
||||||
# 当前坐标只覆盖页面顶部空白带,不会擦到天气卡上边框。
|
# 遮罩只覆盖右上角状态区,避免继续压住 calendar 自己的顶部内容。
|
||||||
export STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
export STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
||||||
export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-600}
|
||||||
export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
||||||
export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372}
|
export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360}
|
||||||
export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24}
|
export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48}
|
||||||
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}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ output_path=${1:?"missing output path"}
|
|||||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
ENV_FILE="$DIR/env.sh"
|
ENV_FILE="$DIR/env.sh"
|
||||||
THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
||||||
|
local_only=false
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--local-only" ]; then
|
||||||
|
local_only=true
|
||||||
|
shift
|
||||||
|
output_path=${1:?"missing output path"}
|
||||||
|
fi
|
||||||
|
|
||||||
# fetch-dashboard 既会被 dash.sh 调,也会被 switch-theme.sh 单独调。
|
# fetch-dashboard 既会被 dash.sh 调,也会被 switch-theme.sh 单独调。
|
||||||
# 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。
|
# 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。
|
||||||
@@ -16,17 +23,71 @@ THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
|||||||
|
|
||||||
background_path=${BACKGROUND_PATH:-""}
|
background_path=${BACKGROUND_PATH:-""}
|
||||||
background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"}
|
background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"}
|
||||||
|
weather_use_kindle_location=${WEATHER_USE_KINDLE_LOCATION:-false}
|
||||||
local_background_path=""
|
local_background_path=""
|
||||||
|
|
||||||
if [ -n "$background_path" ]; then
|
if [ -n "$background_path" ]; then
|
||||||
local_background_path="$DIR/../$background_path"
|
local_background_path="$DIR/../$background_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 主题背景如果已经随本地部署同步到 Kindle,优先直接拷贝本地文件,
|
copy_local_background() {
|
||||||
# 这样切换主题时不依赖远端图片资源是否已经发布完成。
|
if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then
|
||||||
if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then
|
cp "$local_background_path" "$output_path"
|
||||||
cp "$local_background_path" "$output_path"
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "本地主题包缺少背景图:${background_path:-unknown}" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_valid_png() {
|
||||||
|
candidate_path=$1
|
||||||
|
|
||||||
|
if [ ! -f "$candidate_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kindle 端已经确认有 file 命令;如果将来某台机器没有,再回退到 PNG 签名检查。
|
||||||
|
if command -v file >/dev/null 2>&1; then
|
||||||
|
if file "$candidate_path" 2>/dev/null | grep -q "PNG image data"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
signature=$(dd if="$candidate_path" bs=8 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||||
|
[ "$signature" = "89504e470d0a1a0a" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$local_only" = true ]; then
|
||||||
|
# KUAL 触发的切主题链路必须尽快返回,所以这里严格只读本地素材。
|
||||||
|
# 如果本地包缺图,直接失败,让问题暴露出来,而不是把联网等待塞进交互里。
|
||||||
|
copy_local_background
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$weather_use_kindle_location" = true ] && copy_local_background; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$DIR/../xh" -d -q -o "$output_path" get "$background_url"
|
# 定时背景刷新和启动阶段走这里时,优先尝试拉远端最新背景。
|
||||||
|
# 如果远端暂时失败,再回退到本地包,至少保证 dashboard 还能继续显示。
|
||||||
|
remote_tmp_path="${output_path}.remote.$$"
|
||||||
|
rm -f "$remote_tmp_path"
|
||||||
|
|
||||||
|
if "$DIR/../xh" -d -q -o "$remote_tmp_path" get "$background_url"; then
|
||||||
|
if is_valid_png "$remote_tmp_path"; then
|
||||||
|
mv "$remote_tmp_path" "$output_path"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "远端背景不是有效 PNG,忽略本次响应并回退本地主题包:$background_url" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$remote_tmp_path"
|
||||||
|
|
||||||
|
if copy_local_background; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "远端背景拉取失败,且本地主题包也缺少背景图:${background_path:-unknown}" >&2
|
||||||
|
exit 1
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
|
ENV_FILE="$DIR/env.sh"
|
||||||
|
STATE_DIR="$DIR/state"
|
||||||
|
LOCATION_CACHE_FILE="$STATE_DIR/location.env"
|
||||||
|
LOCATION_SYNC_CMD="$DIR/location-sync.sh"
|
||||||
|
refresh_if_needed=false
|
||||||
|
force_refresh=false
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--refresh-if-needed)
|
||||||
|
refresh_if_needed=true
|
||||||
|
;;
|
||||||
|
--force-refresh)
|
||||||
|
force_refresh=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
if [ "$force_refresh" = true ]; then
|
||||||
|
"$LOCATION_SYNC_CMD" --force >/dev/null 2>&1 || true
|
||||||
|
elif [ "$refresh_if_needed" = true ]; then
|
||||||
|
"$LOCATION_SYNC_CMD" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
location_source="fallback"
|
||||||
|
location_city=${LOCATION_FALLBACK_CITY:-杭州}
|
||||||
|
location_lat=${LOCATION_FALLBACK_LAT:-30.274084}
|
||||||
|
location_lon=${LOCATION_FALLBACK_LON:-120.155070}
|
||||||
|
location_timezone=${LOCATION_FALLBACK_TIMEZONE:-Asia/Shanghai}
|
||||||
|
|
||||||
|
if [ -f "$LOCATION_CACHE_FILE" ]; then
|
||||||
|
. "$LOCATION_CACHE_FILE"
|
||||||
|
|
||||||
|
if [ -n "${LOCATION_LAT:-}" ] && [ -n "${LOCATION_LON:-}" ]; then
|
||||||
|
location_source=${LOCATION_SOURCE:-geoip}
|
||||||
|
location_city=${LOCATION_CITY:-$location_city}
|
||||||
|
location_lat=${LOCATION_LAT}
|
||||||
|
location_lon=${LOCATION_LON}
|
||||||
|
location_timezone=${LOCATION_TIMEZONE:-$location_timezone}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
quote_for_shell() {
|
||||||
|
printf "%s" "$1" | sed "s/'/'\\\\''/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
printf "export LOCATION_SOURCE='%s'\n" "$(quote_for_shell "$location_source")"
|
||||||
|
printf "export LOCATION_CITY='%s'\n" "$(quote_for_shell "$location_city")"
|
||||||
|
printf "export LOCATION_LAT='%s'\n" "$(quote_for_shell "$location_lat")"
|
||||||
|
printf "export LOCATION_LON='%s'\n" "$(quote_for_shell "$location_lon")"
|
||||||
|
printf "export LOCATION_TIMEZONE='%s'\n" "$(quote_for_shell "$location_timezone")"
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
|
ENV_FILE="$DIR/env.sh"
|
||||||
|
STATE_DIR="$DIR/state"
|
||||||
|
LOCATION_CACHE_FILE="$STATE_DIR/location.env"
|
||||||
|
FETCH_CMD="$DIR/../xh"
|
||||||
|
force_refresh=false
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--force)
|
||||||
|
force_refresh=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
current_epoch() {
|
||||||
|
date '+%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_refresh_due() {
|
||||||
|
if [ ! -f "$LOCATION_CACHE_FILE" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
last_epoch=$(awk -F= '/^export LOCATION_UPDATED_AT_EPOCH=/{gsub("'"'"'", "", $2); print $2; exit}' "$LOCATION_CACHE_FILE" 2>/dev/null || true)
|
||||||
|
if [ -z "$last_epoch" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
now_epoch=$(current_epoch)
|
||||||
|
[ $((now_epoch - last_epoch)) -ge $((LOCATION_REFRESH_INTERVAL_MINUTES * 60)) ]
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_geoip_fields() {
|
||||||
|
payload_path=$1
|
||||||
|
|
||||||
|
lua - "$payload_path" <<'LUA'
|
||||||
|
local path = arg[1]
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
if data:match('"success"%s*:%s*false') then
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function match_string(source, pattern)
|
||||||
|
local value = source:match(pattern)
|
||||||
|
if not value then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
:gsub('\\"', '"')
|
||||||
|
:gsub("\\\\", "\\")
|
||||||
|
end
|
||||||
|
|
||||||
|
local city = match_string(data, '"city"%s*:%s*"(.-)"')
|
||||||
|
local latitude = data:match('"latitude"%s*:%s*(-?%d+%.?%d*)')
|
||||||
|
local longitude = data:match('"longitude"%s*:%s*(-?%d+%.?%d*)')
|
||||||
|
local timezone_block = data:match('"timezone"%s*:%s*(%b{})')
|
||||||
|
local timezone = timezone_block and match_string(timezone_block, '"id"%s*:%s*"(.-)"') or nil
|
||||||
|
|
||||||
|
if not latitude or not longitude then
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
print("CITY=" .. (city or "当前位置"))
|
||||||
|
print("LAT=" .. latitude)
|
||||||
|
print("LON=" .. longitude)
|
||||||
|
print("TIMEZONE=" .. (timezone or "Asia/Shanghai"))
|
||||||
|
LUA
|
||||||
|
}
|
||||||
|
|
||||||
|
quote_for_shell() {
|
||||||
|
printf "%s" "$1" | sed "s/'/'\\\\''/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$force_refresh" != true ] && ! cache_refresh_due; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$FETCH_CMD" ]; then
|
||||||
|
echo "Location sync failed: xh is unavailable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload_path="$STATE_DIR/location-response.$$"
|
||||||
|
rm -f "$payload_path"
|
||||||
|
|
||||||
|
if ! "$FETCH_CMD" -d -q -o "$payload_path" get "$LOCATION_GEOIP_URL"; then
|
||||||
|
rm -f "$payload_path"
|
||||||
|
echo "Location sync failed: GeoIP request failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
location_city=""
|
||||||
|
location_lat=""
|
||||||
|
location_lon=""
|
||||||
|
location_timezone=""
|
||||||
|
|
||||||
|
if ! parsed_output=$(extract_geoip_fields "$payload_path"); then
|
||||||
|
rm -f "$payload_path"
|
||||||
|
echo "Location sync failed: unable to parse GeoIP payload" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$payload_path"
|
||||||
|
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
case "$key" in
|
||||||
|
CITY)
|
||||||
|
location_city=$value
|
||||||
|
;;
|
||||||
|
LAT)
|
||||||
|
location_lat=$value
|
||||||
|
;;
|
||||||
|
LON)
|
||||||
|
location_lon=$value
|
||||||
|
;;
|
||||||
|
TIMEZONE)
|
||||||
|
location_timezone=$value
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done <<EOF
|
||||||
|
$parsed_output
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -z "$location_lat" ] || [ -z "$location_lon" ]; then
|
||||||
|
echo "Location sync failed: GeoIP payload missing coordinates" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
now_epoch=$(current_epoch)
|
||||||
|
now_iso=$(date '+%Y-%m-%dT%H:%M:%S%z' | sed 's/\(..\)$/:\1/')
|
||||||
|
|
||||||
|
cat >"$LOCATION_CACHE_FILE" <<EOF
|
||||||
|
export LOCATION_SOURCE='geoip'
|
||||||
|
export LOCATION_CITY='$(quote_for_shell "$location_city")'
|
||||||
|
export LOCATION_LAT='$(quote_for_shell "$location_lat")'
|
||||||
|
export LOCATION_LON='$(quote_for_shell "$location_lon")'
|
||||||
|
export LOCATION_TIMEZONE='$(quote_for_shell "$location_timezone")'
|
||||||
|
export LOCATION_UPDATED_AT='$(quote_for_shell "$now_iso")'
|
||||||
|
export LOCATION_UPDATED_AT_EPOCH='$(quote_for_shell "$now_epoch")'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
printf 'Location synced: %s (%s,%s)\n' "$location_city" "$location_lat" "$location_lon"
|
||||||
@@ -36,6 +36,7 @@ clock_minute_thickness=${CLOCK_MINUTE_THICKNESS:-5}
|
|||||||
clock_center_radius=${CLOCK_CENTER_RADIUS:-7}
|
clock_center_radius=${CLOCK_CENTER_RADIUS:-7}
|
||||||
clock_rotation_degrees=${CLOCK_ROTATION_DEGREES:-0}
|
clock_rotation_degrees=${CLOCK_ROTATION_DEGREES:-0}
|
||||||
force_full_refresh=${1:-false}
|
force_full_refresh=${1:-false}
|
||||||
|
apply_to_screen=${2:-true}
|
||||||
output_path="$DIR/state/clock-render.pgm"
|
output_path="$DIR/state/clock-render.pgm"
|
||||||
|
|
||||||
eval "$("$DIR/clock-index.sh")"
|
eval "$("$DIR/clock-index.sh")"
|
||||||
@@ -65,6 +66,10 @@ lua "$DIR/render-clock.lua" \
|
|||||||
"$clock_center_radius" \
|
"$clock_center_radius" \
|
||||||
"$clock_rotation_degrees"
|
"$clock_rotation_degrees"
|
||||||
|
|
||||||
|
if [ "$apply_to_screen" != true ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$force_full_refresh" = true ]; then
|
if [ "$force_full_refresh" = true ]; then
|
||||||
# Kindle Voyage 当前这条链路里,fbink 默认会叠加 viewport 修正,
|
# Kindle Voyage 当前这条链路里,fbink 默认会叠加 viewport 修正,
|
||||||
# 导致图像在屏幕上出现双重偏移。这里强制关闭 viewport 修正,
|
# 导致图像在屏幕上出现双重偏移。这里强制关闭 viewport 修正,
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||||
|
ENV_FILE="$DIR/env.sh"
|
||||||
|
STATE_DIR="$DIR/state"
|
||||||
|
THEME_JSON_LUA="$DIR/theme-json.lua"
|
||||||
|
FETCH_CMD="$DIR/../xh"
|
||||||
|
WAIT_FOR_WIFI_CMD="$DIR/../wait-for-wifi.sh"
|
||||||
|
LOCAL_THEMES_INDEX="$DIR/../themes.json"
|
||||||
|
LOCAL_THEMES_DIR="$DIR/../themes"
|
||||||
|
LOCAL_BACKGROUND_DIR="$DIR/../kindle-backgrounds"
|
||||||
|
THEMES_INDEX_CACHE="$STATE_DIR/themes.json"
|
||||||
|
EXPECTED_THEMES_FILE="$STATE_DIR/expected-theme-configs.txt"
|
||||||
|
EXPECTED_BACKGROUNDS_FILE="$STATE_DIR/expected-theme-backgrounds.txt"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
|
|
||||||
|
THEMES_INDEX_URL=${THEMES_INDEX_URL:-"https://shell.biboer.cn:20001/themes.json"}
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR" "$LOCAL_THEMES_DIR" "$LOCAL_BACKGROUND_DIR"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
is_valid_png() {
|
||||||
|
candidate_path=$1
|
||||||
|
|
||||||
|
if [ ! -f "$candidate_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v file >/dev/null 2>&1; then
|
||||||
|
if file "$candidate_path" 2>/dev/null | grep -q "PNG image data"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
signature=$(dd if="$candidate_path" bs=8 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||||
|
[ "$signature" = "89504e470d0a1a0a" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_png_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
|
||||||
|
|
||||||
|
if ! is_valid_png "$tmp_path"; then
|
||||||
|
rm -f "$tmp_path"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$tmp_path" "$output_path"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_stale_files() {
|
||||||
|
target_dir=$1
|
||||||
|
expected_file=$2
|
||||||
|
pattern=$3
|
||||||
|
|
||||||
|
for existing_path in "$target_dir"/$pattern; do
|
||||||
|
[ -e "$existing_path" ] || continue
|
||||||
|
|
||||||
|
existing_name=$(basename "$existing_path")
|
||||||
|
if ! grep -Fqx "$existing_name" "$expected_file"; then
|
||||||
|
rm -f "$existing_path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Syncing all theme backgrounds from remote"
|
||||||
|
"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP"
|
||||||
|
|
||||||
|
if ! fetch_to_path "$THEMES_INDEX_URL" "$THEMES_INDEX_CACHE"; then
|
||||||
|
echo "Themes index fetch failed: $THEMES_INDEX_URL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$THEMES_INDEX_CACHE" "$LOCAL_THEMES_INDEX"
|
||||||
|
: >"$EXPECTED_THEMES_FILE"
|
||||||
|
: >"$EXPECTED_BACKGROUNDS_FILE"
|
||||||
|
|
||||||
|
theme_configs=$(lua "$THEME_JSON_LUA" list-configs "$THEMES_INDEX_CACHE")
|
||||||
|
|
||||||
|
if [ -z "$theme_configs" ]; then
|
||||||
|
echo "Themes index is empty." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$theme_configs" | while IFS="$(printf '\t')" read -r theme_id config_url; do
|
||||||
|
[ -n "$theme_id" ] || continue
|
||||||
|
[ -n "$config_url" ] || continue
|
||||||
|
|
||||||
|
local_theme_config="$LOCAL_THEMES_DIR/$theme_id.json"
|
||||||
|
if ! fetch_to_path "$config_url" "$local_theme_config"; then
|
||||||
|
echo "Theme config fetch failed: $theme_id $config_url" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$(basename "$local_theme_config")" >>"$EXPECTED_THEMES_FILE"
|
||||||
|
|
||||||
|
theme_backgrounds=$(lua "$THEME_JSON_LUA" list-backgrounds "$local_theme_config")
|
||||||
|
if [ -z "$theme_backgrounds" ]; then
|
||||||
|
echo "Theme config has no backgrounds: $theme_id" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$theme_backgrounds" | while IFS="$(printf '\t')" read -r orientation background_path background_url; do
|
||||||
|
[ -n "$background_path" ] || continue
|
||||||
|
[ -n "$background_url" ] || continue
|
||||||
|
|
||||||
|
background_file=$(basename "$background_path")
|
||||||
|
local_background_path="$LOCAL_BACKGROUND_DIR/$background_file"
|
||||||
|
|
||||||
|
if ! fetch_png_to_path "$background_url" "$local_background_path"; then
|
||||||
|
echo "Theme background fetch failed: $theme_id $orientation $background_url" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$background_file" >>"$EXPECTED_BACKGROUNDS_FILE"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup_stale_files "$LOCAL_THEMES_DIR" "$EXPECTED_THEMES_FILE" "*.json"
|
||||||
|
cleanup_stale_files "$LOCAL_BACKGROUND_DIR" "$EXPECTED_BACKGROUNDS_FILE" "*.png"
|
||||||
|
|
||||||
|
echo "Theme background sync completed"
|
||||||
@@ -234,6 +234,56 @@ local function first_orientation(theme, fallback)
|
|||||||
return (theme.orientations or {})[1] or fallback
|
return (theme.orientations or {})[1] or fallback
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function orientation_label(orientation)
|
||||||
|
if orientation == "landscape" then
|
||||||
|
return "横屏"
|
||||||
|
end
|
||||||
|
|
||||||
|
if orientation == "portrait" then
|
||||||
|
return "竖屏"
|
||||||
|
end
|
||||||
|
|
||||||
|
return orientation
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ordered_variant_keys(variants)
|
||||||
|
local keys = {}
|
||||||
|
local seen = {}
|
||||||
|
local preferred = {"portrait", "landscape"}
|
||||||
|
|
||||||
|
for _, key in ipairs(preferred) do
|
||||||
|
if variants[key] ~= nil then
|
||||||
|
keys[#keys + 1] = key
|
||||||
|
seen[key] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for key, _ in pairs(variants) do
|
||||||
|
if not seen[key] then
|
||||||
|
keys[#keys + 1] = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(keys, function(left, right)
|
||||||
|
return left < right
|
||||||
|
end)
|
||||||
|
|
||||||
|
for index = #preferred, 1, -1 do
|
||||||
|
local key = preferred[index]
|
||||||
|
if seen[key] then
|
||||||
|
for key_index, value in ipairs(keys) do
|
||||||
|
if value == key then
|
||||||
|
table.remove(keys, key_index)
|
||||||
|
table.insert(keys, 1, key)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
local function shell_quote(value)
|
local function shell_quote(value)
|
||||||
return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'"
|
return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'"
|
||||||
end
|
end
|
||||||
@@ -330,11 +380,45 @@ end
|
|||||||
if command == "list" then
|
if command == "list" then
|
||||||
local index_path = assert(arg[2], "missing themes index path")
|
local index_path = assert(arg[2], "missing themes index path")
|
||||||
local index_data = decode(read_file(index_path))
|
local index_data = decode(read_file(index_path))
|
||||||
|
local orientation_order = {"landscape", "portrait"}
|
||||||
|
|
||||||
for _, theme in ipairs(index_data.themes or {}) do
|
for _, theme in ipairs(index_data.themes or {}) do
|
||||||
io.write(theme.id or "", "\t")
|
for _, orientation in ipairs(orientation_order) do
|
||||||
io.write(theme.label or theme.id or "", "\t")
|
if orientation_exists(theme, orientation) then
|
||||||
io.write(table.concat(theme.orientations or {}, ","), "\n")
|
io.write(theme.id or "", "\t")
|
||||||
|
io.write((theme.id or "") .. "-" .. orientation_label(orientation), "\t")
|
||||||
|
io.write(orientation, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == "list-configs" 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
|
||||||
|
if theme.id ~= nil and theme.configUrl ~= nil then
|
||||||
|
io.write(theme.id, "\t", theme.configUrl, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == "list-backgrounds" then
|
||||||
|
local theme_path = assert(arg[2], "missing theme config path")
|
||||||
|
local theme_data = decode(read_file(theme_path))
|
||||||
|
local variants = assert(theme_data.variants, "missing variants")
|
||||||
|
|
||||||
|
for _, orientation in ipairs(ordered_variant_keys(variants)) do
|
||||||
|
local variant = variants[orientation]
|
||||||
|
local background = variant.background or {}
|
||||||
|
if background.path ~= nil and background.url ~= nil then
|
||||||
|
io.write(orientation, "\t", background.path, "\t", background.url, "\n")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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_ID="__return_home__"
|
||||||
HOME_MENU_ITEM_LABEL="Return Home"
|
HOME_MENU_ITEM_LABEL="返回"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
@@ -114,8 +114,8 @@ theme_field() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
current_theme_index() {
|
current_theme_index() {
|
||||||
awk -F '\t' -v current_theme="$current_theme_id" '
|
awk -F '\t' -v current_theme="$current_theme_id" -v current_orientation="$current_orientation" '
|
||||||
$1 == current_theme {
|
$1 == current_theme && $3 == current_orientation {
|
||||||
print NR
|
print NR
|
||||||
found = 1
|
found = 1
|
||||||
exit
|
exit
|
||||||
@@ -138,6 +138,9 @@ print_line() {
|
|||||||
render_menu() {
|
render_menu() {
|
||||||
total_themes=$(theme_count)
|
total_themes=$(theme_count)
|
||||||
current_label=$(theme_field "$selected_index" 2)
|
current_label=$(theme_field "$selected_index" 2)
|
||||||
|
grid_left_col=3
|
||||||
|
grid_right_col=24
|
||||||
|
grid_start_row=8
|
||||||
|
|
||||||
/usr/sbin/eips -c
|
/usr/sbin/eips -c
|
||||||
print_line 3 1 "Kindle Dashboard"
|
print_line 3 1 "Kindle Dashboard"
|
||||||
@@ -146,26 +149,35 @@ render_menu() {
|
|||||||
print_line 3 6 "Selected: $current_label"
|
print_line 3 6 "Selected: $current_label"
|
||||||
print_line 3 7 "--------------------------------"
|
print_line 3 7 "--------------------------------"
|
||||||
|
|
||||||
row=9
|
|
||||||
index=1
|
index=1
|
||||||
while [ "$index" -le "$total_themes" ]; do
|
while [ "$index" -le "$total_themes" ]; do
|
||||||
theme_label=$(theme_field "$index" 2)
|
theme_label=$(theme_field "$index" 2)
|
||||||
theme_id=$(theme_field "$index" 1)
|
|
||||||
prefix=" "
|
prefix=" "
|
||||||
line_text=""
|
line_text=""
|
||||||
|
row=0
|
||||||
|
col=0
|
||||||
|
|
||||||
if [ "$index" -eq "$selected_index" ]; then
|
if [ "$index" -eq "$selected_index" ]; then
|
||||||
prefix="> "
|
prefix="> "
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
line_text="${prefix}${theme_label}"
|
||||||
line_text="${prefix}${theme_label}"
|
|
||||||
|
if [ "$index" -le 8 ]; then
|
||||||
|
row_offset=$(( (index - 1) / 2 ))
|
||||||
|
row=$((grid_start_row + row_offset * 2))
|
||||||
|
|
||||||
|
if [ $((index % 2)) -eq 1 ]; then
|
||||||
|
col=$grid_left_col
|
||||||
|
else
|
||||||
|
col=$grid_right_col
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
line_text="${prefix}${theme_label} (${theme_id})"
|
row=16
|
||||||
|
col=3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_line 3 "$row" "$line_text"
|
print_line "$col" "$row" "$line_text"
|
||||||
row=$((row + 2))
|
|
||||||
index=$((index + 1))
|
index=$((index + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -198,6 +210,7 @@ move_selection() {
|
|||||||
|
|
||||||
apply_selection() {
|
apply_selection() {
|
||||||
selected_theme_id=$(theme_field "$selected_index" 1)
|
selected_theme_id=$(theme_field "$selected_index" 1)
|
||||||
|
selected_orientation=$(theme_field "$selected_index" 3)
|
||||||
|
|
||||||
/usr/sbin/eips -c
|
/usr/sbin/eips -c
|
||||||
if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
||||||
@@ -209,10 +222,10 @@ apply_selection() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_event "apply theme=$selected_theme_id orientation=$current_orientation"
|
log_event "apply theme=$selected_theme_id orientation=$selected_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 / $selected_orientation"
|
||||||
"$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation"
|
"$SWITCH_THEME_CMD" "$selected_theme_id" "$selected_orientation"
|
||||||
menu_open=false
|
menu_open=false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ mkdir -p "$STATE_DIR"
|
|||||||
|
|
||||||
force_index=false
|
force_index=false
|
||||||
force_theme=false
|
force_theme=false
|
||||||
|
local_only=false
|
||||||
requested_theme_id=${THEME_ID:-default}
|
requested_theme_id=${THEME_ID:-default}
|
||||||
requested_orientation=${ORIENTATION:-portrait}
|
requested_orientation=${ORIENTATION:-portrait}
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ while [ "$#" -gt 0 ]; do
|
|||||||
--force-theme)
|
--force-theme)
|
||||||
force_theme=true
|
force_theme=true
|
||||||
;;
|
;;
|
||||||
|
--local-only)
|
||||||
|
local_only=true
|
||||||
|
;;
|
||||||
--theme)
|
--theme)
|
||||||
shift
|
shift
|
||||||
requested_theme_id=${1:?"missing theme id"}
|
requested_theme_id=${1:?"missing theme id"}
|
||||||
@@ -118,6 +122,11 @@ sync_themes_index() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$local_only" = true ]; then
|
||||||
|
echo "本地主题包缺少 themes.json。" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 主题清单是全局入口,平时按天同步一次即可。
|
# 主题清单是全局入口,平时按天同步一次即可。
|
||||||
# 真正切换主题时会走 --force-index,确保马上拿到最新列表。
|
# 真正切换主题时会走 --force-index,确保马上拿到最新列表。
|
||||||
if [ "$force_index" = true ] || [ ! -f "$THEMES_INDEX_CACHE" ] || refresh_due "$THEMES_INDEX_TIMESTAMP_FILE" "$THEMES_INDEX_REFRESH_INTERVAL_MINUTES"; then
|
if [ "$force_index" = true ] || [ ! -f "$THEMES_INDEX_CACHE" ] || refresh_due "$THEMES_INDEX_TIMESTAMP_FILE" "$THEMES_INDEX_REFRESH_INTERVAL_MINUTES"; then
|
||||||
@@ -165,6 +174,11 @@ sync_theme_config() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$local_only" = true ]; then
|
||||||
|
echo "本地主题包缺少主题配置:$resolved_theme_id" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 主题配置按 theme 维度缓存;
|
# 主题配置按 theme 维度缓存;
|
||||||
# orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。
|
# orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。
|
||||||
needs_theme_fetch=$force_theme
|
needs_theme_fetch=$force_theme
|
||||||
|
|||||||
@@ -6,15 +6,32 @@ DIR="$(dirname "$0")"
|
|||||||
ENV_FILE="$DIR/local/env.sh"
|
ENV_FILE="$DIR/local/env.sh"
|
||||||
THEME_FILE="$DIR/local/theme.env"
|
THEME_FILE="$DIR/local/theme.env"
|
||||||
LOG_FILE="$DIR/logs/dash.log"
|
LOG_FILE="$DIR/logs/dash.log"
|
||||||
|
PID_FILE="$DIR/local/state/dashboard.pid"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$LOG_FILE")"
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
mkdir -p "$DIR/local/state"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||||
|
|
||||||
|
stop_existing_dashboard_processes() {
|
||||||
|
# 主题切换后再次 start 时,旧的 dash 主循环如果还活着,
|
||||||
|
# 会继续按旧主题或旧坐标补画时钟,最终在屏幕上叠出两个时钟。
|
||||||
|
# 这里统一先清掉旧实例,再启动新的单实例 dashboard。
|
||||||
|
pkill -f "$DIR/dash.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
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_existing_dashboard_processes
|
||||||
|
|
||||||
|
echo "start.sh invoked at $(date '+%Y-%m-%d %H:%M:%S %Z') with DEBUG=$DEBUG" >>"$LOG_FILE"
|
||||||
|
|
||||||
if [ "$DEBUG" = true ]; then
|
if [ "$DEBUG" = true ]; then
|
||||||
|
printf '%s\n' "$$" >"$PID_FILE"
|
||||||
"$DIR/dash.sh"
|
"$DIR/dash.sh"
|
||||||
else
|
else
|
||||||
# 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。
|
# 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。
|
||||||
@@ -25,4 +42,7 @@ else
|
|||||||
else
|
else
|
||||||
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$!" >"$PID_FILE"
|
||||||
|
echo "start.sh spawned dashboard launcher pid=$!" >>"$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ THEME_FILE="$DIR/local/theme.env"
|
|||||||
THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env"
|
THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env"
|
||||||
BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at"
|
BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at"
|
||||||
BACKGROUND_PNG="$DIR/kindlebg.png"
|
BACKGROUND_PNG="$DIR/kindlebg.png"
|
||||||
WAIT_FOR_WIFI_CMD="$DIR/wait-for-wifi.sh"
|
|
||||||
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
||||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||||
CLOCK_RENDER_CMD="$DIR/local/render-clock.sh"
|
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||||
@@ -22,16 +20,19 @@ requested_orientation=${2:-${ORIENTATION:-portrait}}
|
|||||||
|
|
||||||
mkdir -p "$DIR/local/state"
|
mkdir -p "$DIR/local/state"
|
||||||
|
|
||||||
# 切换主题时必须立刻联网拉到最新配置和背景,
|
|
||||||
# 否则用户会看到 theme.env 已更新,但屏幕内容仍停留在旧主题。
|
|
||||||
echo "Switching theme to $requested_theme_id / $requested_orientation"
|
echo "Switching theme to $requested_theme_id / $requested_orientation"
|
||||||
"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP"
|
# KUAL 切主题的目标是尽快把 calendar 画出来。
|
||||||
"$THEME_SYNC_CMD" --force-index --force-theme --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null
|
# 这里强制只用设备上已经同步好的本地主题素材,避免因为网络抖动
|
||||||
|
# 又回到首页停留几秒,破坏“直接切到 calendar”的体感。
|
||||||
|
"$THEME_SYNC_CMD" --local-only --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$THEME_RUNTIME_ENV_FILE"
|
. "$THEME_RUNTIME_ENV_FILE"
|
||||||
|
|
||||||
"$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG"
|
# 背景图同样只从本地主题包读取。
|
||||||
|
# 如果本地包缺图,直接失败,让问题暴露出来;补素材应走后台同步或重新部署,
|
||||||
|
# 不能把联网等待塞回用户点击主题这条交互链路里。
|
||||||
|
"$FETCH_DASHBOARD_CMD" --local-only "$BACKGROUND_PNG"
|
||||||
date '+%s' >"$BACKGROUND_TIMESTAMP_FILE"
|
date '+%s' >"$BACKGROUND_TIMESTAMP_FILE"
|
||||||
|
|
||||||
# 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。
|
# 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。
|
||||||
@@ -41,6 +42,9 @@ export ORIENTATION='${ORIENTATION}'
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
||||||
"$CLOCK_RENDER_CMD" true
|
|
||||||
|
# 这里不再同步阻塞等待时钟重绘。
|
||||||
|
# 背景先尽快上屏,让用户从 KUAL 返回后更快看到 calendar;
|
||||||
|
# 后续的时钟和遮罩交给 start.sh 拉起的主循环补画。
|
||||||
|
|
||||||
echo "Theme switched to ${THEME_ID} / ${ORIENTATION}"
|
echo "Theme switched to ${THEME_ID} / ${ORIENTATION}"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "Kindle Dashboard",
|
"name": "主题选择",
|
||||||
"priority": -998,
|
"priority": -998,
|
||||||
"items": [
|
"items": [
|
||||||
{"name": "Default", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default", "exitmenu": true},
|
{"name": "default-横屏", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default landscape", "exitmenu": true},
|
||||||
{"name": "Paper", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "paper", "exitmenu": true},
|
{"name": "default-竖屏", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default portrait", "exitmenu": true},
|
||||||
{"name": "Classic", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "classic", "exitmenu": true}
|
{"name": "simple-横屏", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "simple landscape", "exitmenu": true},
|
||||||
|
{"name": "simple-竖屏", "priority": 4, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "simple portrait", "exitmenu": true}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
{"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
||||||
|
|||||||
@@ -21,11 +21,17 @@ echo "Source keys: ${SOURCE_KEYS}"
|
|||||||
|
|
||||||
for target_dir in "${ROOT_HOME}/.ssh" /root/.ssh /var/local/root/.ssh /mnt/us/usbnet/etc/dot.ssh; do
|
for target_dir in "${ROOT_HOME}/.ssh" /root/.ssh /var/local/root/.ssh /mnt/us/usbnet/etc/dot.ssh; do
|
||||||
echo "--- target: ${target_dir} ---"
|
echo "--- target: ${target_dir} ---"
|
||||||
mkdir -p "${target_dir}"
|
if ! mkdir -p "${target_dir}" 2>/dev/null; then
|
||||||
|
echo "skip: cannot create ${target_dir}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if [ -f "${target_dir}/authorized_keys" ]; then
|
if [ -f "${target_dir}/authorized_keys" ]; then
|
||||||
cp "${target_dir}/authorized_keys" "${target_dir}/authorized_keys.bak.${TS}" || true
|
cp "${target_dir}/authorized_keys" "${target_dir}/authorized_keys.bak.${TS}" || true
|
||||||
fi
|
fi
|
||||||
cp "${SOURCE_KEYS}" "${target_dir}/authorized_keys"
|
if ! cp "${SOURCE_KEYS}" "${target_dir}/authorized_keys"; then
|
||||||
|
echo "skip: cannot write ${target_dir}/authorized_keys"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
chmod 700 "${target_dir}" 2>/dev/null || true
|
chmod 700 "${target_dir}" 2>/dev/null || true
|
||||||
chmod 600 "${target_dir}/authorized_keys" 2>/dev/null || true
|
chmod 600 "${target_dir}/authorized_keys" 2>/dev/null || true
|
||||||
ls -ld "${target_dir}" "${target_dir}/authorized_keys" 2>/dev/null || true
|
ls -ld "${target_dir}" "${target_dir}/authorized_keys" 2>/dev/null || true
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>cn.biboer.kindle-dash.refresh-kindle-backgrounds</string>
|
||||||
|
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/gavin/kindle-dash</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/bin/sh</string>
|
||||||
|
<string>/Users/gavin/kindle-dash/scripts/refresh-kindle-backgrounds.sh</string>
|
||||||
|
<string>--once</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PATH</key>
|
||||||
|
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>ProcessType</key>
|
||||||
|
<string>Background</string>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/gavin/Library/Logs/kindle-dash/refresh-kindle-backgrounds.log</string>
|
||||||
|
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/gavin/Library/Logs/kindle-dash/refresh-kindle-backgrounds.err.log</string>
|
||||||
|
|
||||||
|
<key>StartCalendarInterval</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>4</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>6</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>8</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>10</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>12</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>14</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>16</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>18</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>20</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>22</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
162
scripts/publish-calendar-dist.sh
Normal file
162
scripts/publish-calendar-dist.sh
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||||
|
CALENDAR_DIR="$ROOT_DIR/calendar"
|
||||||
|
DIST_DIR="$CALENDAR_DIR/dist"
|
||||||
|
|
||||||
|
OUTPUT_DIR=""
|
||||||
|
INTERVAL_MINUTES=60
|
||||||
|
RUN_ONCE=false
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法:
|
||||||
|
sh scripts/publish-calendar-dist.sh --output-dir <dir> [选项]
|
||||||
|
|
||||||
|
作用:
|
||||||
|
1. 调用 calendar 现有导出链路生成最新背景图与主题资源
|
||||||
|
2. 把 calendar/dist 整体发布到指定目录
|
||||||
|
3. 默认每 60 分钟自动重跑一次;加 --once 只跑一轮
|
||||||
|
|
||||||
|
选项:
|
||||||
|
-o, --output-dir <dir> 发布目标目录,通常是 Web 服务器实际对外提供静态文件的目录
|
||||||
|
--interval-minutes <n> 自动生成间隔,默认 60 分钟
|
||||||
|
--once 只生成并发布一轮
|
||||||
|
-h, --help 查看帮助
|
||||||
|
|
||||||
|
示例:
|
||||||
|
sh scripts/publish-calendar-dist.sh --output-dir /srv/calendar-dashboard --once
|
||||||
|
sh scripts/publish-calendar-dist.sh --output-dir /srv/calendar-dashboard
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_positive_integer() {
|
||||||
|
value=$1
|
||||||
|
name=$2
|
||||||
|
|
||||||
|
case "$value" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "$name 必须是正整数:$value" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$value" -le 0 ]; then
|
||||||
|
echo "$name 必须大于 0:$value" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
publish_once() {
|
||||||
|
temp_publish_dir=$(mktemp -d "${TMPDIR:-/tmp}/kindle-dash-publish.XXXXXX")
|
||||||
|
publish_status=0
|
||||||
|
|
||||||
|
log "开始生成 Web 端背景图与主题资源"
|
||||||
|
(
|
||||||
|
cd "$CALENDAR_DIR"
|
||||||
|
npm run export:themes
|
||||||
|
) || publish_status=$?
|
||||||
|
|
||||||
|
if [ "$publish_status" -ne 0 ]; then
|
||||||
|
rm -rf "$temp_publish_dir"
|
||||||
|
return "$publish_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "开始发布到目标目录:$OUTPUT_DIR"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# 先把本轮导出结果完整复制到临时目录,确认结构无误后再整体 rsync 到目标目录,
|
||||||
|
# 避免 Web 根目录在导出过程中暴露半成品文件。
|
||||||
|
rsync -a --delete "$DIST_DIR"/ "$temp_publish_dir"/ || publish_status=$?
|
||||||
|
if [ "$publish_status" -eq 0 ]; then
|
||||||
|
rsync -a --delete "$temp_publish_dir"/ "$OUTPUT_DIR"/ || publish_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$temp_publish_dir"
|
||||||
|
|
||||||
|
if [ "$publish_status" -ne 0 ]; then
|
||||||
|
return "$publish_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "发布完成:$OUTPUT_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep_until_next_run() {
|
||||||
|
interval_seconds=$((INTERVAL_MINUTES * 60))
|
||||||
|
now_epoch=$(date '+%s')
|
||||||
|
next_epoch=$(( ((now_epoch / interval_seconds) + 1) * interval_seconds ))
|
||||||
|
sleep_seconds=$((next_epoch - now_epoch))
|
||||||
|
|
||||||
|
if [ "$sleep_seconds" -le 0 ]; then
|
||||||
|
sleep_seconds=$interval_seconds
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "等待 ${sleep_seconds} 秒后执行下一轮生成"
|
||||||
|
sleep "$sleep_seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-o|--output-dir)
|
||||||
|
shift
|
||||||
|
OUTPUT_DIR=${1:?"missing output dir"}
|
||||||
|
;;
|
||||||
|
--interval-minutes)
|
||||||
|
shift
|
||||||
|
INTERVAL_MINUTES=${1:?"missing interval minutes"}
|
||||||
|
;;
|
||||||
|
--once)
|
||||||
|
RUN_ONCE=true
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
print_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "未知参数: $1" >&2
|
||||||
|
echo >&2
|
||||||
|
print_usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$OUTPUT_DIR" ]; then
|
||||||
|
echo "必须通过 --output-dir 指定发布目录。" >&2
|
||||||
|
echo >&2
|
||||||
|
print_usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
validate_positive_integer "$INTERVAL_MINUTES" "interval-minutes"
|
||||||
|
|
||||||
|
# 当前导图链路依赖 macOS 自带的 Swift + WKWebView。
|
||||||
|
# 如果部署机不是 macOS,这一步会失败,届时需要单独换成跨平台截图方案。
|
||||||
|
if [ ! -x /usr/bin/swift ]; then
|
||||||
|
echo "未找到 /usr/bin/swift,当前机器无法使用现有 WebKit 导图链路。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if publish_once; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
log "本轮发布失败,退出码:$status"
|
||||||
|
if [ "$RUN_ONCE" = true ]; then
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$RUN_ONCE" = true ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep_until_next_run
|
||||||
|
done
|
||||||
115
scripts/refresh-kindle-backgrounds.sh
Normal file
115
scripts/refresh-kindle-backgrounds.sh
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||||
|
CALENDAR_DIR="$ROOT_DIR/calendar"
|
||||||
|
INTERVAL_MINUTES=60
|
||||||
|
RUN_ONCE=false
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法:
|
||||||
|
sh scripts/refresh-kindle-backgrounds.sh [选项]
|
||||||
|
|
||||||
|
作用:
|
||||||
|
1. 构建 calendar Web 产物
|
||||||
|
2. 生成主题背景图到 calendar/kindle-backgrounds/
|
||||||
|
3. 默认每 60 分钟重跑一次;加 --once 只跑一轮
|
||||||
|
|
||||||
|
选项:
|
||||||
|
--interval-minutes <n> 自动生成间隔,默认 60 分钟
|
||||||
|
--once 只生成一轮
|
||||||
|
-h, --help 查看帮助
|
||||||
|
|
||||||
|
示例:
|
||||||
|
sh scripts/refresh-kindle-backgrounds.sh --once
|
||||||
|
sh scripts/refresh-kindle-backgrounds.sh --interval-minutes 30
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_positive_integer() {
|
||||||
|
value=$1
|
||||||
|
name=$2
|
||||||
|
|
||||||
|
case "$value" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "$name 必须是正整数:$value" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$value" -le 0 ]; then
|
||||||
|
echo "$name 必须大于 0:$value" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_once() {
|
||||||
|
log "开始生成 Kindle 背景图到 $CALENDAR_DIR/kindle-backgrounds"
|
||||||
|
(
|
||||||
|
cd "$CALENDAR_DIR"
|
||||||
|
npm run export:themes
|
||||||
|
)
|
||||||
|
log "背景图生成完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep_until_next_run() {
|
||||||
|
interval_seconds=$((INTERVAL_MINUTES * 60))
|
||||||
|
now_epoch=$(date '+%s')
|
||||||
|
next_epoch=$(( ((now_epoch / interval_seconds) + 1) * interval_seconds ))
|
||||||
|
sleep_seconds=$((next_epoch - now_epoch))
|
||||||
|
|
||||||
|
if [ "$sleep_seconds" -le 0 ]; then
|
||||||
|
sleep_seconds=$interval_seconds
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "等待 ${sleep_seconds} 秒后执行下一轮生成"
|
||||||
|
sleep "$sleep_seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--interval-minutes)
|
||||||
|
shift
|
||||||
|
INTERVAL_MINUTES=${1:?"missing interval minutes"}
|
||||||
|
;;
|
||||||
|
--once)
|
||||||
|
RUN_ONCE=true
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
print_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "未知参数: $1" >&2
|
||||||
|
echo >&2
|
||||||
|
print_usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
validate_positive_integer "$INTERVAL_MINUTES" "interval-minutes"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if refresh_once; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
log "本轮背景图生成失败,退出码:$status"
|
||||||
|
if [ "$RUN_ONCE" = true ]; then
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$RUN_ONCE" = true ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep_until_next_run
|
||||||
|
done
|
||||||
@@ -6,6 +6,8 @@ KINDLE_TARGET="kindle"
|
|||||||
THEME_FILTER=""
|
THEME_FILTER=""
|
||||||
ORIENTATION_FILTER=""
|
ORIENTATION_FILTER=""
|
||||||
CLOCK_REGION_JSON="$ROOT_DIR/calendar/dist/clock-region.json"
|
CLOCK_REGION_JSON="$ROOT_DIR/calendar/dist/clock-region.json"
|
||||||
|
LOCATION_LAT=""
|
||||||
|
LOCATION_LON=""
|
||||||
|
|
||||||
print_usage() {
|
print_usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
@@ -50,20 +52,6 @@ if [ -n "$ORIENTATION_FILTER" ] && [ -z "$THEME_FILTER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$ROOT_DIR/calendar"
|
|
||||||
if [ -n "$THEME_FILTER" ]; then
|
|
||||||
if [ -n "$ORIENTATION_FILTER" ]; then
|
|
||||||
npm run export:themes -- --theme "$THEME_FILTER" --orientation "$ORIENTATION_FILTER" >/dev/null
|
|
||||||
CLOCK_REGION_JSON="$ROOT_DIR/calendar/dist/themes/$THEME_FILTER/$ORIENTATION_FILTER/kindlebg.clock-region.json"
|
|
||||||
else
|
|
||||||
npm run export:themes -- --theme "$THEME_FILTER" >/dev/null
|
|
||||||
CLOCK_REGION_JSON=""
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
npm run export:themes >/dev/null
|
|
||||||
fi
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
|
|
||||||
clock_region_value() {
|
clock_region_value() {
|
||||||
key=$1
|
key=$1
|
||||||
python3 - "$CLOCK_REGION_JSON" "$key" <<'PY'
|
python3 - "$CLOCK_REGION_JSON" "$key" <<'PY'
|
||||||
@@ -98,19 +86,69 @@ sync_dashboard_runtime() {
|
|||||||
"$ROOT_DIR/dash/src/local/env.sh" \
|
"$ROOT_DIR/dash/src/local/env.sh" \
|
||||||
"$ROOT_DIR/dash/src/local/fetch-dashboard.sh" \
|
"$ROOT_DIR/dash/src/local/fetch-dashboard.sh" \
|
||||||
"$ROOT_DIR/dash/src/local/clock-index.sh" \
|
"$ROOT_DIR/dash/src/local/clock-index.sh" \
|
||||||
|
"$ROOT_DIR/dash/src/local/location-env.sh" \
|
||||||
|
"$ROOT_DIR/dash/src/local/location-sync.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/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/sync-theme-backgrounds.sh" \
|
||||||
"$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/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" "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/location-env.sh /mnt/us/dashboard/local/location-sync.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/sync-theme-backgrounds.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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve_kindle_location_override() {
|
||||||
|
location_output=""
|
||||||
|
|
||||||
|
if ! location_output=$(ssh "$KINDLE_TARGET" "/mnt/us/dashboard/local/location-env.sh --refresh-if-needed" 2>/dev/null); then
|
||||||
|
echo "Skipped Kindle location override: unable to resolve remote location"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOCATION_LAT=$(printf '%s\n' "$location_output" | awk -F= '/^export LOCATION_LAT=/{gsub("'"'"'", "", $2); print $2; exit}')
|
||||||
|
LOCATION_LON=$(printf '%s\n' "$location_output" | awk -F= '/^export LOCATION_LON=/{gsub("'"'"'", "", $2); print $2; exit}')
|
||||||
|
|
||||||
|
if [ -z "$LOCATION_LAT" ] || [ -z "$LOCATION_LON" ]; then
|
||||||
|
echo "Skipped Kindle location override: remote coordinates unavailable"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using Kindle location override: ${LOCATION_LAT},${LOCATION_LON}"
|
||||||
|
}
|
||||||
|
|
||||||
|
export_theme_bundle_locally() {
|
||||||
|
cd "$ROOT_DIR/calendar"
|
||||||
|
set -- npm run export:themes --
|
||||||
|
|
||||||
|
if [ -n "$THEME_FILTER" ]; then
|
||||||
|
set -- "$@" --theme "$THEME_FILTER"
|
||||||
|
|
||||||
|
if [ -n "$ORIENTATION_FILTER" ]; then
|
||||||
|
set -- "$@" --orientation "$ORIENTATION_FILTER"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$LOCATION_LAT" ] && [ -n "$LOCATION_LON" ]; then
|
||||||
|
set -- "$@" --location-lat "$LOCATION_LAT" --location-lon "$LOCATION_LON"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$@" >/dev/null
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
if [ -n "$THEME_FILTER" ]; then
|
||||||
|
if [ -n "$ORIENTATION_FILTER" ]; then
|
||||||
|
CLOCK_REGION_JSON="$ROOT_DIR/calendar/dist/themes/$THEME_FILTER/$ORIENTATION_FILTER/kindlebg.clock-region.json"
|
||||||
|
else
|
||||||
|
CLOCK_REGION_JSON=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
sync_kual_extension() {
|
sync_kual_extension() {
|
||||||
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/extensions/kindle-dash"
|
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/extensions/kindle-dash"
|
||||||
rsync -av --no-o --no-g \
|
rsync -av --no-o --no-g \
|
||||||
@@ -124,15 +162,15 @@ sync_theme_bundle() {
|
|||||||
"$KINDLE_TARGET":/mnt/us/dashboard/
|
"$KINDLE_TARGET":/mnt/us/dashboard/
|
||||||
|
|
||||||
if [ -z "$THEME_FILTER" ]; then
|
if [ -z "$THEME_FILTER" ]; then
|
||||||
if [ -f "$ROOT_DIR/calendar/dist/kindlebg.png" ]; then
|
|
||||||
rsync -av --no-o --no-g \
|
|
||||||
"$ROOT_DIR/calendar/dist/kindlebg.png" \
|
|
||||||
"$KINDLE_TARGET":/mnt/us/dashboard/
|
|
||||||
fi
|
|
||||||
|
|
||||||
rsync -av --no-o --no-g \
|
rsync -av --no-o --no-g \
|
||||||
"$ROOT_DIR/calendar/dist/themes/" \
|
"$ROOT_DIR/calendar/dist/themes/" \
|
||||||
"$KINDLE_TARGET":/mnt/us/dashboard/themes/
|
"$KINDLE_TARGET":/mnt/us/dashboard/themes/
|
||||||
|
|
||||||
|
if [ -d "$ROOT_DIR/calendar/kindle-backgrounds" ]; then
|
||||||
|
rsync -av --no-o --no-g \
|
||||||
|
"$ROOT_DIR/calendar/kindle-backgrounds/" \
|
||||||
|
"$KINDLE_TARGET":/mnt/us/dashboard/kindle-backgrounds/
|
||||||
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -140,6 +178,21 @@ sync_theme_bundle() {
|
|||||||
"$ROOT_DIR/calendar/dist/themes/$THEME_FILTER.json" \
|
"$ROOT_DIR/calendar/dist/themes/$THEME_FILTER.json" \
|
||||||
"$KINDLE_TARGET":/mnt/us/dashboard/themes/
|
"$KINDLE_TARGET":/mnt/us/dashboard/themes/
|
||||||
|
|
||||||
|
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/dashboard/kindle-backgrounds"
|
||||||
|
|
||||||
|
if [ -n "$ORIENTATION_FILTER" ]; then
|
||||||
|
flat_background_path="$ROOT_DIR/calendar/kindle-backgrounds/$THEME_FILTER-$ORIENTATION_FILTER.png"
|
||||||
|
if [ -f "$flat_background_path" ]; then
|
||||||
|
rsync -av --no-o --no-g \
|
||||||
|
"$flat_background_path" \
|
||||||
|
"$KINDLE_TARGET":/mnt/us/dashboard/kindle-backgrounds/
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rsync -av --no-o --no-g \
|
||||||
|
"$ROOT_DIR/calendar/kindle-backgrounds/$THEME_FILTER-"*.png \
|
||||||
|
"$KINDLE_TARGET":/mnt/us/dashboard/kindle-backgrounds/
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$ORIENTATION_FILTER" ]; then
|
if [ -n "$ORIENTATION_FILTER" ]; then
|
||||||
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/dashboard/themes/$THEME_FILTER/$ORIENTATION_FILTER"
|
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/dashboard/themes/$THEME_FILTER/$ORIENTATION_FILTER"
|
||||||
rsync -av --no-o --no-g \
|
rsync -av --no-o --no-g \
|
||||||
@@ -154,6 +207,40 @@ sync_theme_bundle() {
|
|||||||
"$KINDLE_TARGET":/mnt/us/dashboard/themes/$THEME_FILTER/
|
"$KINDLE_TARGET":/mnt/us/dashboard/themes/$THEME_FILTER/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh_current_background_cache() {
|
||||||
|
# 不能再把本地默认导出的 kindlebg.png 直接覆盖到设备根目录;
|
||||||
|
# 否则设备当前主题若不是默认主题,重启后就会出现“背景回默认、时钟还按旧主题画”的分叉。
|
||||||
|
# 这里统一在 Kindle 端按当前 theme-runtime.env 重新生成根目录背景缓存。
|
||||||
|
if ssh "$KINDLE_TARGET" "/mnt/us/dashboard/local/fetch-dashboard.sh --local-only /mnt/us/dashboard/kindlebg.png"; then
|
||||||
|
ssh "$KINDLE_TARGET" "date '+%s' >/mnt/us/dashboard/local/state/background-updated-at"
|
||||||
|
echo "Current background cache refreshed on device"
|
||||||
|
else
|
||||||
|
echo "Skipped current background cache refresh: current runtime theme assets unavailable"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_current_theme_runtime() {
|
||||||
|
# 主题包同步完之后,设备上的 theme-runtime.env 仍可能停留在旧坐标。
|
||||||
|
# render-clock / fetch-dashboard 实际都读这份运行时快照,
|
||||||
|
# 所以这里必须按“当前主题 + 当前方向”在 Kindle 本地重写一次,
|
||||||
|
# 让时钟位置和背景图一起跟随最新的 calendar 导出结果。
|
||||||
|
if ssh "$KINDLE_TARGET" "set -eu
|
||||||
|
theme_id=default
|
||||||
|
orientation=portrait
|
||||||
|
if [ -f /mnt/us/dashboard/local/theme.env ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /mnt/us/dashboard/local/theme.env
|
||||||
|
theme_id=\${THEME_ID:-\$theme_id}
|
||||||
|
orientation=\${ORIENTATION:-\$orientation}
|
||||||
|
fi
|
||||||
|
/mnt/us/dashboard/local/theme-sync.sh --local-only --theme \"\$theme_id\" --orientation \"\$orientation\" >/dev/null
|
||||||
|
"; then
|
||||||
|
echo "Current theme runtime refreshed on device"
|
||||||
|
else
|
||||||
|
echo "Skipped current theme runtime refresh: unable to resolve current theme locally"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
update_default_clock_region_env() {
|
update_default_clock_region_env() {
|
||||||
if [ -z "$CLOCK_REGION_JSON" ] || [ ! -f "$CLOCK_REGION_JSON" ]; then
|
if [ -z "$CLOCK_REGION_JSON" ] || [ ! -f "$CLOCK_REGION_JSON" ]; then
|
||||||
echo "Skipped CLOCK_REGION_* env update: no single background region selected"
|
echo "Skipped CLOCK_REGION_* env update: no single background region selected"
|
||||||
@@ -185,9 +272,13 @@ update_default_clock_region_env() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sync_dashboard_runtime
|
sync_dashboard_runtime
|
||||||
|
resolve_kindle_location_override
|
||||||
|
export_theme_bundle_locally
|
||||||
sync_theme_bundle
|
sync_theme_bundle
|
||||||
sync_kual_extension
|
sync_kual_extension
|
||||||
update_default_clock_region_env
|
update_default_clock_region_env
|
||||||
|
refresh_current_theme_runtime
|
||||||
|
refresh_current_background_cache
|
||||||
|
|
||||||
if [ -n "$THEME_FILTER" ]; then
|
if [ -n "$THEME_FILTER" ]; then
|
||||||
if [ -n "$ORIENTATION_FILTER" ]; then
|
if [ -n "$ORIENTATION_FILTER" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user