diff --git a/.gitignore b/.gitignore index aeee04e..36f9301 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ data/tts-cache/ apps/miniprogram/utils/opsEnv.js calendar/node_modules +calendar/kindle-backgrounds/ dash/backups dash/downloads diff --git a/README.md b/README.md index 57e3e60..ce874c6 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ - `simple` / `default` 等主题已经接入当前运行链路 - Kindle 侧采用“低频背景 + 本机时钟重绘”的分层渲染方案 +- 通过 `scripts/sync-layered-clock-to-kindle.sh` 同步主题时,天气背景会优先按 Kindle 当前网络出口位置导出 - 新机 bootstrap 方案已实现 - 新机 bootstrap 当前仍是“方案已实现,真机恢复出厂闭环未验证” - `launch-from-kual.sh` 与 `setsid` 脱离方案已经落地到代码 @@ -39,6 +40,7 @@ - 导出 `kindlebg.png` - 导出多主题背景包和 `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) @@ -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 ``` +说明: + +- `post-ssh` 当前会同步 `dashboard` shell 脚本、`KUAL` 菜单和主题包 +- 它不会回补 `prepare-storage` 阶段预置的原生二进制,例如 `next-wakeup`、`xh` +- 所以这条路径默认前提仍然是:先跑过 `prepare-storage` + 注意:这条新机 bootstrap 方案当前仍未做“真机恢复出厂闭环验证”。 ## 常用命令 @@ -176,6 +193,31 @@ npm run build 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///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 @@ -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 ``` +补充: + +- 如果还要执行 `--start-dashboard`,默认同样要求设备上已经有 `prepare-storage` 预置的完整 dashboard 基础运行时 + ### SSH 恢复 如果 Kindle 上已经有 `KTerm`,但外部 SSH 不通: @@ -271,3 +317,6 @@ sh -n snapshot.sh 3. `ssh kindle` 依赖当前网络和 IP;换 Wi‑Fi 后可能要重新确认地址或 SSH 配置 4. `KUAL -> Dashboard` 与 `Dashboard -> 原生 UI/KUAL` 的边界切换在 `Kindle Voyage 5.13.6` 上仍不稳定 5. `stop.sh` 当前必须走保守恢复路径,实验性的“快速切换”方案已在 `2026-03-17` 撤回 +6. Voyage 右上角系统状态栏当前仍可能短暂闪现,顶部遮罩还没有做到完全压住 +7. 天气预报的位置当前使用 Kindle 侧 GeoIP 做“市级近似定位”,结果不保证精确到真实街道或精确城区 +8. 当前架构下,分钟级时钟刷新需要设备被唤醒;而设备一旦被唤醒,屏幕/灯光会一起进入可视态,因此暂时做不到“不亮灯唤醒并肉眼可见更新时钟” diff --git a/bootstrap-new-kindle.sh b/bootstrap-new-kindle.sh index e6d6ce8..6608772 100644 --- a/bootstrap-new-kindle.sh +++ b/bootstrap-new-kindle.sh @@ -29,7 +29,7 @@ print_usage() { 模式: all 默认。能预置 USB 存储就先预置;能连 SSH 就继续做 SSH 后半段 prepare-storage 只做 USB 存储预置 - post-ssh 只做 SSH 打通后的自动化收尾 + post-ssh 只做 SSH 打通后的自动化收尾;默认要求设备上已存在 prepare-storage 预置的 dashboard 基础运行时 选项: -v, --volume Kindle 挂载目录,默认 /Volumes/Kindle @@ -39,8 +39,8 @@ print_usage() { --kterm-package 指定 KTerm 安装包;官方 release 用 .zip,也兼容外部 .bin --download-kterm 在 Mac 侧联网下载 KTerm 到 dash/staging/kterm/,再预置到 Kindle --kterm-version 下载指定 KTerm 版本;默认 latest - --no-background SSH 阶段不同步后立即切主题出图 - --start-dashboard SSH 阶段额外后台启动 dashboard 主循环 + --no-background SSH 阶段同步后不立即切主题出图 + --start-dashboard SSH 阶段额外后台启动 dashboard 主循环;默认要求设备上已有完整 dashboard 运行时 -h, --help 查看帮助 说明: @@ -66,6 +66,11 @@ print_usage() { 6. 对这台 Voyage 5.13.6,脚本默认优先选择“不带 armhf 后缀”的 zip。 7. 如果脚本没有检测到 KTerm 包,会明确提示“这一步仍需手工补装 KTerm”。 +关于 post-ssh: + 1. 它当前会同步 dashboard 的 shell 脚本、KUAL 菜单和主题包。 + 2. 它不会回补 prepare-storage 阶段预置的原生二进制,例如 next-wakeup、xh。 + 3. 因此最稳的用法仍然是:先跑 prepare-storage,完成设备侧步骤后,再跑 post-ssh。 + 推荐操作顺序: 阶段 A:先做 USB 预置 @@ -86,12 +91,14 @@ print_usage() { 1. 恢复出厂。 2. 语言只选 English (United Kingdom)。 3. 到 Wi-Fi 页面时,不要真的联网;先按 WatchThis 文档进入 demo mode。 - 4. 第一次出现 Add Content / Sideload Content 时,只点 Done,不要接 USB。 - 5. 遇到 misconfiguration / Configure Device 时,执行隐藏手势回到可操作界面。 - 6. 再次进入 ;demo -> Sideload Content,这一次才是真正导入 payload 的时机。 - 因为阶段 A 已经预置好 .demo 内容,这里不需要再从 Mac 手工拷文件。 - 7. 退出 demo menu 后,进入 Help & User Guides -> Get Started 触发越狱。 - 8. 越狱成功后,Kindle 根目录应出现 mkk、libkh、rp。 + 4. 搜索 ;demo,进入 demo menu -> Sideload Content。 + 5. 到这个页面后再接 USB;因为阶段 A 已经预置好 .demo 内容,这里只需确认文件存在。 + 6. 弹出 Kindle,在设备上点 Done,退出 demo menu。 + 7. 进入 ;dsts -> Help & User Guides -> Get Started。 + 8. 按设备提示继续完成 register this demo -> Skip -> standard。 + 9. 重启后如果看到 Configure Device,用隐藏手势回到主页。 + 10. 搜索 ;uzb,再接 USB,把 Update_hotfix_watchthis_custom.bin 放到根目录。 + 11. 弹出设备后,进入 ;dsts -> Device Options -> Update Your Kindle。 阶段 C:安装 KUAL / MRPI / USBNetwork / dashboard 1. 在 Kindle 首页搜索: @@ -119,11 +126,12 @@ print_usage() { 1. 执行: sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait 2. 预期结果: - - 自动同步 dashboard 运行时和主题包 + - 自动同步 dashboard shell 脚本、KUAL 菜单和主题包 - Kindle 立即切到 simple / portrait - 屏幕马上显示背景和时钟 3. 如果还希望后台常驻 dashboard 主循环,再执行: sh bootstrap-new-kindle.sh post-ssh -t simple -o portrait --start-dashboard + 这一步默认建立在 prepare-storage 已经把完整 dashboard 基础运行时放进设备上的前提上。 最省事的直接用法: 1. USB 挂载时先运行: @@ -135,11 +143,12 @@ print_usage() { 这时它会自动继续做 SSH 后半段。 最容易做错的地方: - 1. 第一次 Add Content / Sideload Content 只能点 Done,不能在这一步导 payload。 - 2. 真正导 payload 的时机,是隐藏手势返回后,再次进入 ;demo -> Sideload Content。 + 1. `Get Started` 只负责触发越狱脚本;真正退出 demo 状态还要再执行一次 `Update Your Kindle` 安装 custom hotfix。 + 2. `Update Your Kindle` 之前,先确认根目录里是真正的 `Update_hotfix_watchthis_custom.bin`,不要把 `._Update_hotfix_watchthis_custom.bin` 误当成正确文件。 3. 首次 SSH 最稳的入口是 KTerm,不是 USB 直连盲试。 4. post-ssh 阶段如果不带 -t/-o,会退回 themes.json 里的默认主题和方向。 5. 如果本轮没有预置 KTerm 包,阶段 C 结束后仍需你手工补装 KTerm。 + 6. post-ssh 不是“从零补齐 dashboard”的入口;next-wakeup、xh 这类原生二进制仍来自 prepare-storage。 示例: sh bootstrap-new-kindle.sh prepare-storage @@ -465,7 +474,7 @@ print_post_ssh_summary() { SSH 阶段已完成: - 主机:$HOST_TARGET -- 已同步 dashboard 运行时与主题包 +- 已同步 dashboard shell 脚本、KUAL 菜单与主题包 - 当前用于出图的主题:$RESOLVED_THEME_ID / $RESOLVED_ORIENTATION 如果你要继续验证: @@ -477,7 +486,7 @@ EOF post_ssh() { 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 exit 1 fi @@ -485,7 +494,7 @@ post_ssh() { log_step "SSH" "修复设备侧 SSH 辅助脚本权限与 authorized_keys" prepare_remote_helpers - log_step "同步" "同步 dashboard 运行时和主题包到 Kindle" + log_step "同步" "同步 dashboard shell 脚本、KUAL 菜单和主题包到 Kindle" sync_dashboard_runtime if [ "$SHOW_BACKGROUND" = true ]; then diff --git a/calendar/config/themes.json b/calendar/config/themes.json index 1cc35a0..dad3873 100644 --- a/calendar/config/themes.json +++ b/calendar/config/themes.json @@ -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", "label": "Simple", diff --git a/calendar/scripts/export-theme-backgrounds.sh b/calendar/scripts/export-theme-backgrounds.sh index b4c8f06..e9af85a 100644 --- a/calendar/scripts/export-theme-backgrounds.sh +++ b/calendar/scripts/export-theme-backgrounds.sh @@ -4,12 +4,15 @@ set -eu ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)" CALENDAR_DIR="$ROOT_DIR/calendar" DIST_DIR="$CALENDAR_DIR/dist" +KINDLE_BACKGROUNDS_DIR="$CALENDAR_DIR/kindle-backgrounds" PORT=${PORT:-4173} SWIFT_SCRIPT="$CALENDAR_DIR/scripts/export-kindle-background.swift" THEMES_SOURCE="$CALENDAR_DIR/config/themes.json" THEME_FILTER="" ORIENTATION_FILTER="" +LOCATION_LAT="" +LOCATION_LON="" print_usage() { cat <<'EOF' @@ -19,12 +22,15 @@ print_usage() { 选项: --theme 只导出指定主题 --orientation 只导出指定方向;必须和 --theme 一起使用 + --location-lat 导图时显式覆盖天气定位纬度 + --location-lon 导图时显式覆盖天气定位经度 -h, --help 查看帮助 示例: sh scripts/export-theme-backgrounds.sh sh scripts/export-theme-backgrounds.sh --theme simple 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 } @@ -38,6 +44,14 @@ while [ "$#" -gt 0 ]; do shift 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) print_usage exit 0 @@ -57,6 +71,11 @@ if [ -n "$ORIENTATION_FILTER" ] && [ -z "$THEME_FILTER" ]; then exit 1 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=$( node --input-type=module -e " import fs from 'node:fs'; @@ -143,6 +162,7 @@ fi cd "$CALENDAR_DIR" 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 & 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 out_png="$DIST_DIR/$background_path" 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" + 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 + # Web 侧额外维护一份扁平命名的背景图目录,方便 nginx 单独暴露给 Kindle 拉图。 + # 主题 JSON 会把 background.url 指向这里,例如 /kindle-backgrounds/simple-portrait.png。 + cp "$out_png" "$flat_background_png" # 根目录的 kindlebg.png / clock-region.json 只给默认主题兜底使用。 # 定向导出其它主题时不覆盖它,避免把默认主题的运行时入口意外改掉。 diff --git a/calendar/scripts/generate-dashboard-manifest.mjs b/calendar/scripts/generate-dashboard-manifest.mjs index 999d14e..61e9f34 100644 --- a/calendar/scripts/generate-dashboard-manifest.mjs +++ b/calendar/scripts/generate-dashboard-manifest.mjs @@ -9,12 +9,14 @@ const clockRegionPath = path.join(distDir, 'clock-region.json'); const themesSourcePath = path.resolve(currentDir, '../config/themes.json'); const themesDistPath = path.join(distDir, 'themes.json'); const themesDir = path.join(distDir, 'themes'); +const kindleBackgroundsDir = path.resolve(currentDir, '../kindle-backgrounds'); const dashboardBaseUrl = 'https://shell.biboer.cn:20001'; const themesSource = JSON.parse(fs.readFileSync(themesSourcePath, 'utf8')); const generatedAt = new Date().toISOString(); -const defaultVariant = themesSource.themes.find((theme) => theme.id === themesSource.defaultThemeId)?.variants?.[themesSource.defaultOrientation]; -const defaultDeviceClock = defaultVariant ? toDeviceClock(defaultVariant, themesSource.defaultOrientation) : null; +const defaultTheme = themesSource.themes.find((theme) => theme.id === themesSource.defaultThemeId); +const defaultVariant = defaultTheme?.variants?.[themesSource.defaultOrientation]; +const defaultDeviceClock = defaultVariant ? buildRuntimeClock(defaultTheme.id, themesSource.defaultOrientation, defaultVariant) : null; const defaultClockRegion = defaultVariant ? { 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) { return { id: theme.id, label: theme.label, updatedAt: generatedAt, variants: Object.fromEntries( - Object.entries(theme.variants).map(([orientation, variant]) => [ - orientation, - { - devicePlacement: variant.devicePlacement, - background: { - path: variant.backgroundPath, - url: `${dashboardBaseUrl}/${variant.backgroundPath}`, - refreshIntervalMinutes: 120, + Object.entries(theme.variants).map(([orientation, variant]) => { + return [ + orientation, + { + devicePlacement: variant.devicePlacement, + background: { + // Kindle 端统一走扁平目录,避免设备侧自己拼主题子目录规则。 + 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(themesDir, { recursive: true }); +fs.mkdirSync(kindleBackgroundsDir, { recursive: true }); fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); fs.writeFileSync(themesDistPath, `${JSON.stringify(themesIndex, null, 2)}\n`, 'utf8'); diff --git a/calendar/src/components/CalendarCard.vue b/calendar/src/components/CalendarCard.vue index aad47fc..76fc3e5 100644 --- a/calendar/src/components/CalendarCard.vue +++ b/calendar/src/components/CalendarCard.vue @@ -36,7 +36,9 @@ function subLabelTone(cell: CalendarModel['cells'][number]) {

{{ model.weekdayLabel }}

- +
+ +
@@ -110,6 +112,11 @@ function subLabelTone(cell: CalendarModel['cells'][number]) { gap: 1rem; } +.calendar-card__clock-wrap { + width: 220px; + height: 220px; +} + .calendar-card__headline { display: grid; align-content: start; diff --git a/calendar/src/components/SimpleDashboard.vue b/calendar/src/components/SimpleDashboard.vue index 1d0851a..46c6aee 100644 --- a/calendar/src/components/SimpleDashboard.vue +++ b/calendar/src/components/SimpleDashboard.vue @@ -109,7 +109,9 @@ function isCompactCalendarLabel(label: string) {
- +
+ +
@@ -176,7 +178,9 @@ function isCompactCalendarLabel(label: string) {
- +
+ +
@@ -254,7 +258,8 @@ function isCompactCalendarLabel(label: string) { height: 480px; justify-content: center; gap: 16px; - padding: 0 16px; + /* 竖版顶部区按 Figma 节点 284:7 额外下压 32px。 */ + padding: 32px 16px 0; justify-self: start; align-self: start; } @@ -275,6 +280,17 @@ function isCompactCalendarLabel(label: string) { 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 { min-width: 0; } @@ -304,6 +320,10 @@ function isCompactCalendarLabel(label: string) { display: flex; align-items: flex-end; gap: 24px; + width: 100%; + box-sizing: border-box; + /* 横版日期标题组需要保留左侧 24px 对齐边距。 */ + padding-left: 24px; } .simple-dashboard__day { @@ -370,7 +390,10 @@ function isCompactCalendarLabel(label: string) { align-items: center; gap: 24px; width: 480px; + box-sizing: border-box; min-width: 0; + /* 横版地点行比标题再向右缩进一档。 */ + padding-left: 48px; } .simple-dashboard__location-icon { @@ -395,7 +418,7 @@ function isCompactCalendarLabel(label: string) { } .simple-dashboard__location--landscape { - width: 424px; + flex: 1 1 auto; font-size: 56px; line-height: 57px; } @@ -406,7 +429,10 @@ function isCompactCalendarLabel(label: string) { gap: 24px; width: 480px; height: 99px; + box-sizing: border-box; overflow: visible; + /* 横版天气摘要整体向右留出 24px,与 Figma 左列对齐。 */ + padding-left: 24px; } .simple-dashboard__metric { diff --git a/calendar/src/components/WeatherCard.vue b/calendar/src/components/WeatherCard.vue index 4612f29..ee6dabe 100644 --- a/calendar/src/components/WeatherCard.vue +++ b/calendar/src/components/WeatherCard.vue @@ -4,6 +4,7 @@ import { computed } from 'vue'; import WeatherGlyph from './WeatherGlyph.vue'; import { HUMIDITY_ICON_ASSET, + PM25_ICON_ASSET, SUNRISE_ICON_ASSET, SUNSET_ICON_ASSET, VISIBILITY_ICON_ASSET, @@ -45,7 +46,7 @@ const metrics = computed(() => { ? '暂无' : `${props.weather.aqi}${props.weather.aqiLabel}`, accent: 'metric-pill--air', - icon: null, + icon: PM25_ICON_ASSET, }, { label: '能见度', @@ -131,7 +132,6 @@ function forecastKind(day: ForecastDay) { alt="" aria-hidden="true" /> - {{ metric.label }}

{{ metric.value }}

@@ -245,14 +245,6 @@ function forecastKind(day: ForecastDay) { 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 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); diff --git a/calendar/src/lib/icon-assets.ts b/calendar/src/lib/icon-assets.ts index 25d7388..bb4d437 100644 --- a/calendar/src/lib/icon-assets.ts +++ b/calendar/src/lib/icon-assets.ts @@ -8,6 +8,7 @@ import lightRainIcon from '../../../assets/小雨.svg'; import nightIcon from '../../../assets/晚上.svg'; import clearIcon from '../../../assets/晴天.svg'; import humidityIcon from '../../../assets/湿度.svg'; +import pm25Icon from '../../../assets/simple/pm25.svg'; import visibilityIcon from '../../../assets/能见度.svg'; import sleetIcon 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 SUNSET_ICON_ASSET = sunsetIcon; export const VISIBILITY_ICON_ASSET = visibilityIcon; +export const PM25_ICON_ASSET = pm25Icon; diff --git a/calendar/src/lib/weather.ts b/calendar/src/lib/weather.ts index d02a3ef..63e1af1 100644 --- a/calendar/src/lib/weather.ts +++ b/calendar/src/lib/weather.ts @@ -26,6 +26,11 @@ export interface WeatherSnapshot { aqiLabel: string; } +interface SearchLocationOverride { + latitude: number; + longitude: number; +} + const DEFAULT_LOCATION: LocationCoordinates = { latitude: 30.274084, longitude: 120.15507, @@ -193,7 +198,34 @@ async function reverseGeocodeLocation(latitude: number, longitude: number) { } } -export async function resolveLocation(): Promise { +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 { + 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)) { return DEFAULT_LOCATION; } diff --git a/calendar/src/style.css b/calendar/src/style.css index 7c1d810..1db16c4 100644 --- a/calendar/src/style.css +++ b/calendar/src/style.css @@ -153,6 +153,86 @@ img { 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 { grid-template-columns: minmax(0, 1.24fr) minmax(21rem, 0.76fr); grid-template-rows: minmax(0, 1fr) 216px; @@ -196,6 +276,56 @@ img { 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 { grid-template-rows: auto minmax(0, 1fr) minmax(0, 0.86fr) minmax(0, 0.9fr); gap: 0.58rem; diff --git a/dash/KUAL/kindle-dash/menu.json b/dash/KUAL/kindle-dash/menu.json index 1a788cb..0f5f27a 100644 --- a/dash/KUAL/kindle-dash/menu.json +++ b/dash/KUAL/kindle-dash/menu.json @@ -1,12 +1,13 @@ { "items": [ { - "name": "Kindle Dashboard", + "name": "主题选择", "priority": -998, "items": [ - {"name": "Default", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default", "exitmenu": true}, - {"name": "Paper", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "paper", "exitmenu": true}, - {"name": "Classic", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "classic", "exitmenu": true} + {"name": "default-横屏", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default landscape", "exitmenu": true}, + {"name": "default-竖屏", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default portrait", "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"}, diff --git a/dash/README.md b/dash/README.md index d690798..906805d 100644 --- a/dash/README.md +++ b/dash/README.md @@ -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`. 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 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`. @@ -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. * 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. +* 普通模式下,常规分钟刷新只更新后台缓存,不主动把时钟刷到可视屏幕;手动短按 `power` 或从 KUAL 回到 calendar,会打开一个 5 分钟的可视窗口,结束后再回到普通循环。 * During the update intervals the device is suspended to RAM to save power. ## Notes diff --git a/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md b/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md index 465dfe4..f0151ac 100644 --- a/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md +++ b/dash/docs/kindle-voyage-5.13.6-bootstrap-validation-zh.md @@ -80,11 +80,15 @@ sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version v2.6 ### 4. WatchThis 导入点 -必须特别注意: +按这轮实机成功路径,直接记录下面这组顺序: -1. 第一次 `Add Content / Sideload Content` 只能点 `Done` -2. 不要在第一次导入点接 USB -3. 真正的 payload 导入点,是隐藏手势返回后,再次进入 `;demo -> Sideload Content` +1. 搜索 `;demo` +2. 进入 `demo menu` +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`。 -### 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` -- Kindle 用户存储根目录出现 `libkh` -- Kindle 用户存储根目录出现 `rp` +这里不要急着跑 `;log mrpi`,因为还差 custom hotfix 这一步。 -如果没有这三个目录,说明这轮 `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 验证 -### 7. 接回 Wi‑Fi +### 8. 接回 Wi‑Fi 让 Kindle 连到和 Mac 同一个主 Wi‑Fi。 @@ -134,7 +162,7 @@ Rename OTA Binaries -> Rename - Guest Wi‑Fi - 开了客户端隔离的网络 -### 8. 在 KTerm 里拉起 DropBear +### 9. 在 KTerm 里拉起 DropBear 打开 `KTerm`,执行: @@ -151,7 +179,7 @@ sh /mnt/us/ssh-force-dropbear-22.sh dropbear... TCP *:22 (LISTEN) ``` -### 9. 在 Mac 上确认 SSH +### 10. 在 Mac 上确认 SSH 回到 Mac: @@ -180,7 +208,7 @@ ps -ef | grep -E 'sshd|dropbear|telnetd' | grep -v grep ## Dashboard 闭环验证 -### 10. 让 bootstrap 跑后半段 +### 11. 让 bootstrap 跑后半段 在 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 ``` +前提说明: + +- 这一步默认建立在第 2 步 `prepare-storage` 已成功落地的前提上 +- 当前 `post-ssh` 会同步 dashboard shell 脚本、`KUAL` 菜单和主题包 +- 它不会回补 `prepare-storage` 预置的原生二进制,例如 `next-wakeup`、`xh` + 预期结果: -- 同步当前 dashboard 运行时 +- 同步当前 dashboard shell 脚本、`KUAL` 菜单和主题包 - 同步主题包 - 设备立即切到 `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 ``` -### 11. 抓首张验收图 +这一步同样默认要求设备上已经有 `prepare-storage` 预置的完整 dashboard 基础运行时。 + +### 12. 抓首张验收图 在 Mac 上执行: diff --git a/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md b/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md index 5fe57c0..2c7115d 100644 --- a/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md +++ b/dash/docs/kindle-voyage-5.13.6-bootstrap-zh.md @@ -8,7 +8,7 @@ - 预置 `KUAL / MRPI / USBNetwork / kindle-dash` - 可选预置 `KTerm` - 预置 SSH 恢复脚本 -- SSH 打通后自动同步 dashboard、切主题、立即出图 +- SSH 打通后自动同步 dashboard shell 脚本 / KUAL 菜单 / 主题包,切主题并立即出图 对应脚本: @@ -106,9 +106,14 @@ sh bootstrap-new-kindle.sh post-ssh - 修复设备侧 SSH 辅助脚本权限 - 尝试同步 `authorized_keys` -- 同步 dashboard 运行时和主题包 +- 同步 dashboard shell 脚本、KUAL 菜单和主题包 - 立即切到指定主题并把背景画到屏幕上 +当前边界: + +- `post-ssh` 不会回补 `prepare-storage` 阶段预置的原生二进制,例如 `next-wakeup`、`xh` +- 所以它的默认前提仍然是:这台设备之前已经执行过 `prepare-storage`,或设备上本来就已有完整 dashboard 基础运行时 + 可选: ```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 ``` +其中 `--start-dashboard` 的稳定前提也是一样:设备上必须已经具备完整 dashboard 基础运行时。 + ### 3. `all` 默认模式: @@ -135,23 +142,29 @@ sh bootstrap-new-kindle.sh 1. 恢复出厂并进入 demo mode 2. 到真正的 `Sideload Content` 时机 3. 让脚本已预置好的 `.demo` payload 生效 -4. 通过 `Get Started` 完成越狱 -5. 搜索 `;log mrpi` -6. 在 `KUAL` 中先执行 `Rename OTA Binaries -> Rename` -7. 如果本轮没有预置 `KTerm`,这里先手工补装 `KTerm` -8. 连上 Wi‑Fi -9. 打开 `KTerm`,执行: +4. `;dsts -> Help & User Guides -> Get Started` +5. 继续完成 demo 流程里的 `register this demo -> Skip -> standard` +6. 重启后遇到 `Configure Device`,用隐藏手势回到主页 +7. 搜索 `;uzb` 挂载 USB,把 `Update_hotfix_watchthis_custom.bin` 放到根目录 +8. `;dsts -> Device Options -> Update Your Kindle` +9. 回到主页后搜索 `;log mrpi` +10. 在 `KUAL` 中先执行 `Rename OTA Binaries -> Rename` +11. 如果本轮没有预置 `KTerm`,这里先手工补装 `KTerm` +12. 连上 Wi‑Fi +13. 打开 `KTerm`,执行: ```sh sh /mnt/us/ssh-force-dropbear-22.sh ``` -10. 回到 Mac,执行: +14. 回到 Mac,执行: ```sh sh bootstrap-new-kindle.sh post-ssh ``` +这里默认仍然建立在第 1 步已经成功做过 `prepare-storage` 的前提上。 + ## 相关文档 - WatchThis 越狱路径: diff --git a/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md b/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md index 81c5c49..c9375cb 100644 --- a/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md +++ b/dash/docs/kindle-voyage-5.13.6-watchthis-zh.md @@ -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)` - 固件:`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` -- 在 demo 菜单里点错了分支,提前进入了 `Resell Device` / `销售设备` +**再接 USB,把 `Update_hotfix_watchthis_custom.bin` 放到根目录** +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`: @@ -36,203 +59,216 @@ - `demo.json` - `Update_hotfix_watchthis_custom.bin` -在本仓库里对应的是: +本仓库里的对应路径: -- `staging/watchthis/KV-5.13.6/KV-5.13.6.zip` -- `staging/watchthis/KV-5.13.6/demo.json` -- `staging/watchthis/Update_hotfix_watchthis_custom.bin` +- `dash/staging/watchthis/KV-5.13.6/KV-5.13.6.zip` +- `dash/staging/watchthis/KV-5.13.6/demo.json` +- `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. 先恢复出厂设置。 2. 语言选择页只选 `English (United Kingdom)`。 - 这一步非常关键,不要选中文。 -3. 到 Wi‑Fi 页面后,随便点一个网络,再立刻退回,不要真的联网。 -4. 在搜索栏输入 `;enter_demo`。 -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` +3. 不要联网。 +4. 在搜索框输入 `;demo`。 +5. 进入 `demo menu`。 -### 2. 第一次出现 Sideload Content 时不要导入 payload +如果这里没有进入 `demo menu`,再考虑备用入口;对本次实机成功路径来说,直接 `;demo` 就够了。 -1. 第一次出现 `Add Content` / `Sideload Content` 提示时,只点 `Done`。 -2. 这一步不要接 USB。 -3. 这一步也不要导入 `KV-5.13.6.zip`。 +### 2. 在 Sideload Content 页面导入 payload -这是最容易做错的一步。第一次 `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. 在屏幕右下角用两根手指同时轻点一下 2. 两指立刻抬起 3. 马上用一根手指从右下向左滑 -触发成功后会回到可操作界面。 +成功后会回到主页。 -### 4. 真正的 payload 导入点 +### 5. 用 ;uzb 暴露用户存储并写入 hotfix -1. 回到可操作界面后,搜索输入 `;demo` -2. 进入 demo menu -3. 选择 `Sideload Content` / `导入内容` -4. 到这一步再接 USB -5. 在 Kindle 根目录创建 `.demo/` -6. 把下面三个东西放进去: +回到主页后: + +1. 搜索输入 `;uzb` +2. 再接 USB +3. 在 Kindle 根目录确认存在: ```text -.demo/KV-5.13.6.zip -.demo/demo.json -.demo/goodreads/ <- 空目录 +/Volumes/Kindle/Update_hotfix_watchthis_custom.bin ``` -如果你在 Mac 上操作,可以直接用: +如果没有,就把仓库里的: -```sh -mkdir -p /Volumes/Kindle/.demo/goodreads -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/ +```text +dash/staging/watchthis/Update_hotfix_watchthis_custom.bin ``` +拷到 Kindle 根目录。 + 然后: -1. 弹出 Kindle -2. 在 Kindle 上点 `Done` -3. 退出 demo menu +1. 安全弹出 Kindle +2. 回到设备 -## 5. 触发越狱脚本 +### 6. 手工安装 custom hotfix -1. 退出 demo menu 后,输入 `;dsts` - 如果 `;dsts` 没反应,也可以从顶部下拉进入设置。 -2. 打开 `Help & User Guides` -3. 再点 `Get Started` -4. 设备会重启 -5. 越狱脚本会在下次启动时运行 +这一步不要再走 `Help & User Guides`。 -如果这里弹 `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. 长按电源键强制重启 -2. 再进 demo menu +2. 再进 `demo menu` 3. 再执行一次 `Sideload Content -> Done` 4. 这次不要再接 USB -## 6. 成功判据 +### `Update Your Kindle` 报 `Update Error 2` -对这台设备,下面这些现象说明越狱已经落地: +先检查 Kindle 根目录里是不是真的有: -- Kindle 用户存储根目录出现 `mkk` -- Kindle 用户存储根目录出现 `libkh` -- 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/ +```text +Update_hotfix_watchthis_custom.bin ``` -然后: +注意不要把 macOS 生成的: -1. 弹出 Kindle -2. 回到首页搜索输入 `;log mrpi` -3. 等安装完成 -4. 首页会出现 `KUAL` 卡片 +```text +._Update_hotfix_watchthis_custom.bin +``` -## 8. 启动顺序 - -进入 `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` +误当成真正的 hotfix 文件。 diff --git a/dash/src/dash.sh b/dash/src/dash.sh index 15e6d2e..7bca7bc 100755 --- a/dash/src/dash.sh +++ b/dash/src/dash.sh @@ -11,35 +11,69 @@ STATE_DIR="$DIR/local/state" BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at" THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env" THEME_SYNC_CMD="$DIR/local/theme-sync.sh" +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_LOG_FILE="$DIR/logs/theme-menu.log" TOUCH_HOME_SERVICE_CMD="$DIR/local/touch-home-service.sh" TOUCH_HOME_LOG_FILE="$DIR/logs/touch-home.log" REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"} +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} SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600} DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false} BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120} CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15} PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10} -MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60} +MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300} 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_LEFT=${STATUS_MASK_LEFT:-700} STATUS_MASK_TOP=${STATUS_MASK_TOP:-0} -STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372} -STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} +STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360} +STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48} STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} -RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable +RTC_WAKEUP_ENABLE=/sys/devices/platform/mxc_rtc.0/wakeup_enable KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false} - LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10} num_refresh=0 background_needs_redraw=true +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() { if [ "${THEME_MENU_ENABLED:-false}" != true ]; then @@ -77,14 +111,13 @@ load_theme_runtime_config() { } init() { - if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then + if [ -z "$TIMEZONE" ]; then echo "Missing required configuration." echo "Timezone: ${TIMEZONE:-(not set)}." - echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}." exit 1 fi - echo "Starting dashboard with $REFRESH_SCHEDULE refresh..." + 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" if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then @@ -99,7 +132,7 @@ init() { fi 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_touch_home_service } @@ -118,13 +151,20 @@ stop_framework() { prepare_sleep() { echo "Preparing sleep" - /usr/sbin/eips -f -g "$DIR/sleeping.png" - background_needs_redraw=true + if [ "$SLEEPING_SCREEN_ENABLED" = true ]; then + /usr/sbin/eips -f -g "$DIR/sleeping.png" + background_needs_redraw=true - # Give screen time to refresh - sleep 2 + # 只有真的画了 sleeping.png,才需要额外等待这次整屏刷新落完。 + 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 } @@ -143,6 +183,29 @@ background_refresh_due() { last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE") 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" ] } @@ -176,11 +239,161 @@ fetch_background() { 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() { eval "$("$DIR/local/clock-index.sh")" [ $((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() { if [ "$STATUS_MASK_ENABLED" != true ]; then return @@ -191,8 +404,7 @@ mask_system_status_overlay() { # 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。 pass=1 while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do - fbink -q -V -B WHITE -k \ - "top=$STATUS_MASK_TOP,left=$STATUS_MASK_LEFT,width=$STATUS_MASK_WIDTH,height=$STATUS_MASK_HEIGHT" + mask_system_status_overlay_once if [ "$pass" -lt "$STATUS_MASK_PASSES" ] && [ "$STATUS_MASK_DELAY_SECONDS" -gt 0 ]; then 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" } +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() { battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown") charging_state=$(powerd_get_prop isCharging) @@ -285,8 +515,36 @@ rtc_sleep() { echo "Skipping system suspend, sleeping for ${duration}s instead" sleep "$duration" else - # shellcheck disable=SC2039 - [ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC" + # Voyage 这代机器不一定还有旧的 mxc_rtc.0/wakeup_enable 接口; + # 当前实机上能稳定看到的是 /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 fi } @@ -311,6 +569,7 @@ hold_after_manual_wake() { sleep_started_at=$2 sleep_finished_at=$3 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 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" + # 手动唤醒期间,系统可能已经把自己的睡眠/锁屏层画回前台。 + # 这里先强制要求下一次 refresh_dashboard 完整恢复 calendar 背景, + # 不能只补一层时钟 patch。 + background_needs_redraw=true + # 短按电源键提前唤醒后,先把 dashboard 内容恢复回来, - # 再给出一段明确的可交互窗口,避免 2~3 秒内再次休眠。 + # 再在这段明确的可交互窗口里按分钟补画时钟,避免只刷一次后就停住。 refresh_dashboard || true - sleep "$MANUAL_WAKE_KEEP_AWAKE_SECONDS" + hold_visible_window_until "$keep_awake_until" } main_loop() { while true; do 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 action="sleep" @@ -337,8 +601,12 @@ main_loop() { if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then echo "Debug mode active, skipping sleeping screen." fi - action="suspend" - refresh_dashboard + + if [ "$next_wakeup_reason" = "sync" ]; then + action="silent-sync" + else + action="suspend" + fi fi actual_sleep_secs=$next_wakeup_secs @@ -350,6 +618,13 @@ main_loop() { # 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。 # 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。 + # 普通分钟调度已经改成隐藏刷新,因此 debug off 下应该把屏幕控制权交还给 powerd, + # 避免设备每分钟都被重新拉成可视亮屏态。 + if screen_wake_enabled; then + keep_screen_visible + else + allow_screen_sleep + fi sleep "$PRE_SLEEP_GRACE_SECONDS" echo "Going to $action, next wakeup in ${next_wakeup_secs}s" @@ -357,9 +632,46 @@ main_loop() { sleep_started_at=$(now_epoch) rtc_sleep "$actual_sleep_secs" sleep_finished_at=$(now_epoch) - hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at" + + # 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 } 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 diff --git a/dash/src/launch-theme-from-kual.sh b/dash/src/launch-theme-from-kual.sh index c507ed6..b56fea1 100644 --- a/dash/src/launch-theme-from-kual.sh +++ b/dash/src/launch-theme-from-kual.sh @@ -2,19 +2,99 @@ set -eu DIR="$(dirname "$0")" +ENV_FILE="$DIR/local/env.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 [orientation]"} requested_orientation=${2:-} -# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。 -# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑, -# 同时把“选主题”前移到进入 dashboard 之前。 -if [ -n "$requested_orientation" ]; then - "$SWITCH_THEME_CMD" "$requested_theme_id" "$requested_orientation" -else - "$SWITCH_THEME_CMD" "$requested_theme_id" -fi +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" -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 diff --git a/dash/src/local/env.sh b/dash/src/local/env.sh index d0e9d0c..c034faa 100644 --- a/dash/src/local/env.sh +++ b/dash/src/local/env.sh @@ -2,8 +2,18 @@ # Export environment variables here export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1} -# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。 +# 设备侧时钟刷新节奏。 +# 当前默认每分钟刷新一次;debug on 与 debug off 都走这条分钟调度。 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 时区名。 # 这里必须保留 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_ID=${THEME_ID:-"default"} 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_Y=${CLOCK_REGION_Y:-55} 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} # 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。 -# 这样用户有足够时间看屏、切主题或继续交互,而不会立刻再次休眠。 -export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60} +# debug off 下这里固定为 300 秒,也就是按 power 之后保持 5 分钟可视窗口。 +export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300} # 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。 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_LEFT=${STATUS_MASK_LEFT:-700} +export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-600} export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0} -export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372} -export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} +export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360} +export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48} export STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} export STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} diff --git a/dash/src/local/fetch-dashboard.sh b/dash/src/local/fetch-dashboard.sh index 897126d..1f05c54 100755 --- a/dash/src/local/fetch-dashboard.sh +++ b/dash/src/local/fetch-dashboard.sh @@ -6,6 +6,13 @@ output_path=${1:?"missing output path"} DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" ENV_FILE="$DIR/env.sh" THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" +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 单独调。 # 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。 @@ -16,17 +23,74 @@ THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" background_path=${BACKGROUND_PATH:-""} background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"} +weather_use_kindle_location=${WEATHER_USE_KINDLE_LOCATION:-false} local_background_path="" if [ -n "$background_path" ]; then local_background_path="$DIR/../$background_path" fi -# 主题背景如果已经随本地部署同步到 Kindle,优先直接拷贝本地文件, -# 这样切换主题时不依赖远端图片资源是否已经发布完成。 -if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then - cp "$local_background_path" "$output_path" +copy_local_background() { + if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then + 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 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 diff --git a/dash/src/local/location-env.sh b/dash/src/local/location-env.sh new file mode 100644 index 0000000..51e2f9c --- /dev/null +++ b/dash/src/local/location-env.sh @@ -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")" diff --git a/dash/src/local/location-sync.sh b/dash/src/local/location-sync.sh new file mode 100644 index 0000000..b2c4910 --- /dev/null +++ b/dash/src/local/location-sync.sh @@ -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 <&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" </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" diff --git a/dash/src/local/theme-json.lua b/dash/src/local/theme-json.lua index faf19ba..d245753 100644 --- a/dash/src/local/theme-json.lua +++ b/dash/src/local/theme-json.lua @@ -234,6 +234,56 @@ local function first_orientation(theme, fallback) return (theme.orientations or {})[1] or fallback 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) return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'" end @@ -330,11 +380,45 @@ end if command == "list" then local index_path = assert(arg[2], "missing themes index path") local index_data = decode(read_file(index_path)) + local orientation_order = {"landscape", "portrait"} for _, theme in ipairs(index_data.themes or {}) do - io.write(theme.id or "", "\t") - io.write(theme.label or theme.id or "", "\t") - io.write(table.concat(theme.orientations or {}, ","), "\n") + for _, orientation in ipairs(orientation_order) do + if orientation_exists(theme, orientation) then + 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 return diff --git a/dash/src/local/theme-menu-service.sh b/dash/src/local/theme-menu-service.sh index 4c0d603..f00adb7 100644 --- a/dash/src/local/theme-menu-service.sh +++ b/dash/src/local/theme-menu-service.sh @@ -15,7 +15,7 @@ RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-theme-menu" EVENT_DEVICE_DEFAULT="/dev/input/event2" THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35" HOME_MENU_ITEM_ID="__return_home__" -HOME_MENU_ITEM_LABEL="Return Home" +HOME_MENU_ITEM_LABEL="返回" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" @@ -114,8 +114,8 @@ theme_field() { } current_theme_index() { - awk -F '\t' -v current_theme="$current_theme_id" ' - $1 == current_theme { + awk -F '\t' -v current_theme="$current_theme_id" -v current_orientation="$current_orientation" ' + $1 == current_theme && $3 == current_orientation { print NR found = 1 exit @@ -138,6 +138,9 @@ print_line() { render_menu() { total_themes=$(theme_count) current_label=$(theme_field "$selected_index" 2) + grid_left_col=3 + grid_right_col=24 + grid_start_row=8 /usr/sbin/eips -c print_line 3 1 "Kindle Dashboard" @@ -146,26 +149,35 @@ render_menu() { print_line 3 6 "Selected: $current_label" print_line 3 7 "--------------------------------" - row=9 index=1 while [ "$index" -le "$total_themes" ]; do theme_label=$(theme_field "$index" 2) - theme_id=$(theme_field "$index" 1) prefix=" " line_text="" + row=0 + col=0 if [ "$index" -eq "$selected_index" ]; then prefix="> " 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 - line_text="${prefix}${theme_label} (${theme_id})" + row=16 + col=3 fi - print_line 3 "$row" "$line_text" - row=$((row + 2)) + print_line "$col" "$row" "$line_text" index=$((index + 1)) done @@ -198,6 +210,7 @@ move_selection() { apply_selection() { selected_theme_id=$(theme_field "$selected_index" 1) + selected_orientation=$(theme_field "$selected_index" 3) /usr/sbin/eips -c if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then @@ -209,10 +222,10 @@ apply_selection() { return 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 7 "$selected_theme_id / $current_orientation" - "$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation" + print_line 3 7 "$selected_theme_id / $selected_orientation" + "$SWITCH_THEME_CMD" "$selected_theme_id" "$selected_orientation" menu_open=false } diff --git a/dash/src/local/theme-sync.sh b/dash/src/local/theme-sync.sh index 02f35f6..9c19f31 100644 --- a/dash/src/local/theme-sync.sh +++ b/dash/src/local/theme-sync.sh @@ -25,6 +25,7 @@ mkdir -p "$STATE_DIR" force_index=false force_theme=false +local_only=false requested_theme_id=${THEME_ID:-default} requested_orientation=${ORIENTATION:-portrait} @@ -36,6 +37,9 @@ while [ "$#" -gt 0 ]; do --force-theme) force_theme=true ;; + --local-only) + local_only=true + ;; --theme) shift requested_theme_id=${1:?"missing theme id"} @@ -118,6 +122,11 @@ sync_themes_index() { return 0 fi + if [ "$local_only" = true ]; then + echo "本地主题包缺少 themes.json。" >&2 + return 1 + fi + # 主题清单是全局入口,平时按天同步一次即可。 # 真正切换主题时会走 --force-index,确保马上拿到最新列表。 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 fi + if [ "$local_only" = true ]; then + echo "本地主题包缺少主题配置:$resolved_theme_id" >&2 + return 1 + fi + # 主题配置按 theme 维度缓存; # orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。 needs_theme_fetch=$force_theme diff --git a/dash/src/start.sh b/dash/src/start.sh index 51799fc..8fd558f 100755 --- a/dash/src/start.sh +++ b/dash/src/start.sh @@ -6,15 +6,32 @@ DIR="$(dirname "$0")" ENV_FILE="$DIR/local/env.sh" THEME_FILE="$DIR/local/theme.env" LOG_FILE="$DIR/logs/dash.log" +PID_FILE="$DIR/local/state/dashboard.pid" mkdir -p "$(dirname "$LOG_FILE")" +mkdir -p "$DIR/local/state" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" # shellcheck disable=SC1090 [ -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 + printf '%s\n' "$$" >"$PID_FILE" "$DIR/dash.sh" else # 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。 @@ -25,4 +42,7 @@ else else nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 "$PID_FILE" + echo "start.sh spawned dashboard launcher pid=$!" >>"$LOG_FILE" fi diff --git a/dash/src/switch-theme.sh b/dash/src/switch-theme.sh index b62867f..665cf7e 100644 --- a/dash/src/switch-theme.sh +++ b/dash/src/switch-theme.sh @@ -7,10 +7,8 @@ THEME_FILE="$DIR/local/theme.env" THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env" BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at" BACKGROUND_PNG="$DIR/kindlebg.png" -WAIT_FOR_WIFI_CMD="$DIR/wait-for-wifi.sh" THEME_SYNC_CMD="$DIR/local/theme-sync.sh" FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh" -CLOCK_RENDER_CMD="$DIR/local/render-clock.sh" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" @@ -22,16 +20,19 @@ requested_orientation=${2:-${ORIENTATION:-portrait}} mkdir -p "$DIR/local/state" -# 切换主题时必须立刻联网拉到最新配置和背景, -# 否则用户会看到 theme.env 已更新,但屏幕内容仍停留在旧主题。 echo "Switching theme to $requested_theme_id / $requested_orientation" -"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP" -"$THEME_SYNC_CMD" --force-index --force-theme --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null +# KUAL 切主题的目标是尽快把 calendar 画出来。 +# 这里强制只用设备上已经同步好的本地主题素材,避免因为网络抖动 +# 又回到首页停留几秒,破坏“直接切到 calendar”的体感。 +"$THEME_SYNC_CMD" --local-only --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null # shellcheck disable=SC1090 . "$THEME_RUNTIME_ENV_FILE" -"$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG" +# 背景图同样只从本地主题包读取。 +# 如果本地包缺图,直接失败,让问题暴露出来;补素材应走后台同步或重新部署, +# 不能把联网等待塞回用户点击主题这条交互链路里。 +"$FETCH_DASHBOARD_CMD" --local-only "$BACKGROUND_PNG" date '+%s' >"$BACKGROUND_TIMESTAMP_FILE" # 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。 @@ -41,6 +42,9 @@ export ORIENTATION='${ORIENTATION}' EOF /usr/sbin/eips -f -g "$BACKGROUND_PNG" -"$CLOCK_RENDER_CMD" true + +# 这里不再同步阻塞等待时钟重绘。 +# 背景先尽快上屏,让用户从 KUAL 返回后更快看到 calendar; +# 后续的时钟和遮罩交给 start.sh 拉起的主循环补画。 echo "Theme switched to ${THEME_ID} / ${ORIENTATION}" diff --git a/dash/staging/kterm/kterm-kindle-2.6.zip b/dash/staging/kterm/kterm-kindle-2.6.zip new file mode 100644 index 0000000..85d7117 Binary files /dev/null and b/dash/staging/kterm/kterm-kindle-2.6.zip differ diff --git a/dash/staging/kterm/kterm-release-latest.json b/dash/staging/kterm/kterm-release-latest.json new file mode 100644 index 0000000..01a5b3e --- /dev/null +++ b/dash/staging/kterm/kterm-release-latest.json @@ -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 +} diff --git a/dash/staging/post-jailbreak-root/dashboard/dash.sh b/dash/staging/post-jailbreak-root/dashboard/dash.sh index 15e6d2e..7bca7bc 100755 --- a/dash/staging/post-jailbreak-root/dashboard/dash.sh +++ b/dash/staging/post-jailbreak-root/dashboard/dash.sh @@ -11,35 +11,69 @@ STATE_DIR="$DIR/local/state" BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at" THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env" THEME_SYNC_CMD="$DIR/local/theme-sync.sh" +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_LOG_FILE="$DIR/logs/theme-menu.log" TOUCH_HOME_SERVICE_CMD="$DIR/local/touch-home-service.sh" TOUCH_HOME_LOG_FILE="$DIR/logs/touch-home.log" REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"} +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} SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600} DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false} BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120} CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15} PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10} -MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60} +MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300} 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_LEFT=${STATUS_MASK_LEFT:-700} STATUS_MASK_TOP=${STATUS_MASK_TOP:-0} -STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372} -STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} +STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360} +STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48} STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} -RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable +RTC_WAKEUP_ENABLE=/sys/devices/platform/mxc_rtc.0/wakeup_enable KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false} - LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false} LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10} num_refresh=0 background_needs_redraw=true +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() { if [ "${THEME_MENU_ENABLED:-false}" != true ]; then @@ -77,14 +111,13 @@ load_theme_runtime_config() { } init() { - if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then + if [ -z "$TIMEZONE" ]; then echo "Missing required configuration." echo "Timezone: ${TIMEZONE:-(not set)}." - echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}." exit 1 fi - echo "Starting dashboard with $REFRESH_SCHEDULE refresh..." + 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" if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then @@ -99,7 +132,7 @@ init() { fi 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_touch_home_service } @@ -118,13 +151,20 @@ stop_framework() { prepare_sleep() { echo "Preparing sleep" - /usr/sbin/eips -f -g "$DIR/sleeping.png" - background_needs_redraw=true + if [ "$SLEEPING_SCREEN_ENABLED" = true ]; then + /usr/sbin/eips -f -g "$DIR/sleeping.png" + background_needs_redraw=true - # Give screen time to refresh - sleep 2 + # 只有真的画了 sleeping.png,才需要额外等待这次整屏刷新落完。 + 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 } @@ -143,6 +183,29 @@ background_refresh_due() { last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE") 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" ] } @@ -176,11 +239,161 @@ fetch_background() { 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() { eval "$("$DIR/local/clock-index.sh")" [ $((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() { if [ "$STATUS_MASK_ENABLED" != true ]; then return @@ -191,8 +404,7 @@ mask_system_status_overlay() { # 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。 pass=1 while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do - fbink -q -V -B WHITE -k \ - "top=$STATUS_MASK_TOP,left=$STATUS_MASK_LEFT,width=$STATUS_MASK_WIDTH,height=$STATUS_MASK_HEIGHT" + mask_system_status_overlay_once if [ "$pass" -lt "$STATUS_MASK_PASSES" ] && [ "$STATUS_MASK_DELAY_SECONDS" -gt 0 ]; then 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" } +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() { battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown") charging_state=$(powerd_get_prop isCharging) @@ -285,8 +515,36 @@ rtc_sleep() { echo "Skipping system suspend, sleeping for ${duration}s instead" sleep "$duration" else - # shellcheck disable=SC2039 - [ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC" + # Voyage 这代机器不一定还有旧的 mxc_rtc.0/wakeup_enable 接口; + # 当前实机上能稳定看到的是 /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 fi } @@ -311,6 +569,7 @@ hold_after_manual_wake() { sleep_started_at=$2 sleep_finished_at=$3 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 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" + # 手动唤醒期间,系统可能已经把自己的睡眠/锁屏层画回前台。 + # 这里先强制要求下一次 refresh_dashboard 完整恢复 calendar 背景, + # 不能只补一层时钟 patch。 + background_needs_redraw=true + # 短按电源键提前唤醒后,先把 dashboard 内容恢复回来, - # 再给出一段明确的可交互窗口,避免 2~3 秒内再次休眠。 + # 再在这段明确的可交互窗口里按分钟补画时钟,避免只刷一次后就停住。 refresh_dashboard || true - sleep "$MANUAL_WAKE_KEEP_AWAKE_SECONDS" + hold_visible_window_until "$keep_awake_until" } main_loop() { while true; do 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 action="sleep" @@ -337,8 +601,12 @@ main_loop() { if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then echo "Debug mode active, skipping sleeping screen." fi - action="suspend" - refresh_dashboard + + if [ "$next_wakeup_reason" = "sync" ]; then + action="silent-sync" + else + action="suspend" + fi fi actual_sleep_secs=$next_wakeup_secs @@ -350,6 +618,13 @@ main_loop() { # 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。 # 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。 + # 普通分钟调度已经改成隐藏刷新,因此 debug off 下应该把屏幕控制权交还给 powerd, + # 避免设备每分钟都被重新拉成可视亮屏态。 + if screen_wake_enabled; then + keep_screen_visible + else + allow_screen_sleep + fi sleep "$PRE_SLEEP_GRACE_SECONDS" echo "Going to $action, next wakeup in ${next_wakeup_secs}s" @@ -357,9 +632,46 @@ main_loop() { sleep_started_at=$(now_epoch) rtc_sleep "$actual_sleep_secs" sleep_finished_at=$(now_epoch) - hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at" + + # 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 } 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 diff --git a/dash/staging/post-jailbreak-root/dashboard/launch-theme-from-kual.sh b/dash/staging/post-jailbreak-root/dashboard/launch-theme-from-kual.sh index c507ed6..b56fea1 100644 --- a/dash/staging/post-jailbreak-root/dashboard/launch-theme-from-kual.sh +++ b/dash/staging/post-jailbreak-root/dashboard/launch-theme-from-kual.sh @@ -2,19 +2,99 @@ set -eu DIR="$(dirname "$0")" +ENV_FILE="$DIR/local/env.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 [orientation]"} requested_orientation=${2:-} -# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。 -# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑, -# 同时把“选主题”前移到进入 dashboard 之前。 -if [ -n "$requested_orientation" ]; then - "$SWITCH_THEME_CMD" "$requested_theme_id" "$requested_orientation" -else - "$SWITCH_THEME_CMD" "$requested_theme_id" -fi +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" -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 diff --git a/dash/staging/post-jailbreak-root/dashboard/local/env.sh b/dash/staging/post-jailbreak-root/dashboard/local/env.sh index d0e9d0c..7b9eae9 100644 --- a/dash/staging/post-jailbreak-root/dashboard/local/env.sh +++ b/dash/staging/post-jailbreak-root/dashboard/local/env.sh @@ -2,8 +2,18 @@ # Export environment variables here export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1} -# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。 +# 设备侧时钟刷新节奏。 +# 当前默认每分钟刷新一次;debug on 与 debug off 都走这条分钟调度。 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 时区名。 # 这里必须保留 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_ID=${THEME_ID:-"default"} 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_Y=${CLOCK_REGION_Y:-55} 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} # 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。 -# 这样用户有足够时间看屏、切主题或继续交互,而不会立刻再次休眠。 -export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60} +# debug off 下这里固定为 300 秒,也就是按 power 之后保持 5 分钟可视窗口。 +export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-300} # 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。 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_LEFT=${STATUS_MASK_LEFT:-700} +export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-600} export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0} -export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372} -export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24} +export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-360} +export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-48} export STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3} export STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1} diff --git a/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh b/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh index 897126d..4ce782e 100755 --- a/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh +++ b/dash/staging/post-jailbreak-root/dashboard/local/fetch-dashboard.sh @@ -6,6 +6,13 @@ output_path=${1:?"missing output path"} DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" ENV_FILE="$DIR/env.sh" THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" +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 单独调。 # 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。 @@ -16,17 +23,71 @@ THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env" background_path=${BACKGROUND_PATH:-""} background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"} +weather_use_kindle_location=${WEATHER_USE_KINDLE_LOCATION:-false} local_background_path="" if [ -n "$background_path" ]; then local_background_path="$DIR/../$background_path" fi -# 主题背景如果已经随本地部署同步到 Kindle,优先直接拷贝本地文件, -# 这样切换主题时不依赖远端图片资源是否已经发布完成。 -if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then - cp "$local_background_path" "$output_path" +copy_local_background() { + if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then + 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 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 diff --git a/dash/staging/post-jailbreak-root/dashboard/local/location-env.sh b/dash/staging/post-jailbreak-root/dashboard/local/location-env.sh new file mode 100644 index 0000000..8084aff --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/location-env.sh @@ -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")" diff --git a/dash/staging/post-jailbreak-root/dashboard/local/location-sync.sh b/dash/staging/post-jailbreak-root/dashboard/local/location-sync.sh new file mode 100644 index 0000000..a0a61e6 --- /dev/null +++ b/dash/staging/post-jailbreak-root/dashboard/local/location-sync.sh @@ -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 <&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" </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" diff --git a/dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua b/dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua index faf19ba..d245753 100644 --- a/dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua +++ b/dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua @@ -234,6 +234,56 @@ local function first_orientation(theme, fallback) return (theme.orientations or {})[1] or fallback 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) return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'" end @@ -330,11 +380,45 @@ end if command == "list" then local index_path = assert(arg[2], "missing themes index path") local index_data = decode(read_file(index_path)) + local orientation_order = {"landscape", "portrait"} for _, theme in ipairs(index_data.themes or {}) do - io.write(theme.id or "", "\t") - io.write(theme.label or theme.id or "", "\t") - io.write(table.concat(theme.orientations or {}, ","), "\n") + for _, orientation in ipairs(orientation_order) do + if orientation_exists(theme, orientation) then + 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 return diff --git a/dash/staging/post-jailbreak-root/dashboard/local/theme-menu-service.sh b/dash/staging/post-jailbreak-root/dashboard/local/theme-menu-service.sh index 4c0d603..f00adb7 100644 --- a/dash/staging/post-jailbreak-root/dashboard/local/theme-menu-service.sh +++ b/dash/staging/post-jailbreak-root/dashboard/local/theme-menu-service.sh @@ -15,7 +15,7 @@ RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-theme-menu" EVENT_DEVICE_DEFAULT="/dev/input/event2" THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35" HOME_MENU_ITEM_ID="__return_home__" -HOME_MENU_ITEM_LABEL="Return Home" +HOME_MENU_ITEM_LABEL="返回" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" @@ -114,8 +114,8 @@ theme_field() { } current_theme_index() { - awk -F '\t' -v current_theme="$current_theme_id" ' - $1 == current_theme { + awk -F '\t' -v current_theme="$current_theme_id" -v current_orientation="$current_orientation" ' + $1 == current_theme && $3 == current_orientation { print NR found = 1 exit @@ -138,6 +138,9 @@ print_line() { render_menu() { total_themes=$(theme_count) current_label=$(theme_field "$selected_index" 2) + grid_left_col=3 + grid_right_col=24 + grid_start_row=8 /usr/sbin/eips -c print_line 3 1 "Kindle Dashboard" @@ -146,26 +149,35 @@ render_menu() { print_line 3 6 "Selected: $current_label" print_line 3 7 "--------------------------------" - row=9 index=1 while [ "$index" -le "$total_themes" ]; do theme_label=$(theme_field "$index" 2) - theme_id=$(theme_field "$index" 1) prefix=" " line_text="" + row=0 + col=0 if [ "$index" -eq "$selected_index" ]; then prefix="> " 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 - line_text="${prefix}${theme_label} (${theme_id})" + row=16 + col=3 fi - print_line 3 "$row" "$line_text" - row=$((row + 2)) + print_line "$col" "$row" "$line_text" index=$((index + 1)) done @@ -198,6 +210,7 @@ move_selection() { apply_selection() { selected_theme_id=$(theme_field "$selected_index" 1) + selected_orientation=$(theme_field "$selected_index" 3) /usr/sbin/eips -c if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then @@ -209,10 +222,10 @@ apply_selection() { return 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 7 "$selected_theme_id / $current_orientation" - "$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation" + print_line 3 7 "$selected_theme_id / $selected_orientation" + "$SWITCH_THEME_CMD" "$selected_theme_id" "$selected_orientation" menu_open=false } diff --git a/dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh b/dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh index 02f35f6..9c19f31 100644 --- a/dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh +++ b/dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh @@ -25,6 +25,7 @@ mkdir -p "$STATE_DIR" force_index=false force_theme=false +local_only=false requested_theme_id=${THEME_ID:-default} requested_orientation=${ORIENTATION:-portrait} @@ -36,6 +37,9 @@ while [ "$#" -gt 0 ]; do --force-theme) force_theme=true ;; + --local-only) + local_only=true + ;; --theme) shift requested_theme_id=${1:?"missing theme id"} @@ -118,6 +122,11 @@ sync_themes_index() { return 0 fi + if [ "$local_only" = true ]; then + echo "本地主题包缺少 themes.json。" >&2 + return 1 + fi + # 主题清单是全局入口,平时按天同步一次即可。 # 真正切换主题时会走 --force-index,确保马上拿到最新列表。 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 fi + if [ "$local_only" = true ]; then + echo "本地主题包缺少主题配置:$resolved_theme_id" >&2 + return 1 + fi + # 主题配置按 theme 维度缓存; # orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。 needs_theme_fetch=$force_theme diff --git a/dash/staging/post-jailbreak-root/dashboard/start.sh b/dash/staging/post-jailbreak-root/dashboard/start.sh index 51799fc..8fd558f 100755 --- a/dash/staging/post-jailbreak-root/dashboard/start.sh +++ b/dash/staging/post-jailbreak-root/dashboard/start.sh @@ -6,15 +6,32 @@ DIR="$(dirname "$0")" ENV_FILE="$DIR/local/env.sh" THEME_FILE="$DIR/local/theme.env" LOG_FILE="$DIR/logs/dash.log" +PID_FILE="$DIR/local/state/dashboard.pid" mkdir -p "$(dirname "$LOG_FILE")" +mkdir -p "$DIR/local/state" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" # shellcheck disable=SC1090 [ -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 + printf '%s\n' "$$" >"$PID_FILE" "$DIR/dash.sh" else # 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。 @@ -25,4 +42,7 @@ else else nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 "$PID_FILE" + echo "start.sh spawned dashboard launcher pid=$!" >>"$LOG_FILE" fi diff --git a/dash/staging/post-jailbreak-root/dashboard/switch-theme.sh b/dash/staging/post-jailbreak-root/dashboard/switch-theme.sh index b62867f..665cf7e 100644 --- a/dash/staging/post-jailbreak-root/dashboard/switch-theme.sh +++ b/dash/staging/post-jailbreak-root/dashboard/switch-theme.sh @@ -7,10 +7,8 @@ THEME_FILE="$DIR/local/theme.env" THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env" BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at" BACKGROUND_PNG="$DIR/kindlebg.png" -WAIT_FOR_WIFI_CMD="$DIR/wait-for-wifi.sh" THEME_SYNC_CMD="$DIR/local/theme-sync.sh" FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh" -CLOCK_RENDER_CMD="$DIR/local/render-clock.sh" # shellcheck disable=SC1090 [ -f "$ENV_FILE" ] && . "$ENV_FILE" @@ -22,16 +20,19 @@ requested_orientation=${2:-${ORIENTATION:-portrait}} mkdir -p "$DIR/local/state" -# 切换主题时必须立刻联网拉到最新配置和背景, -# 否则用户会看到 theme.env 已更新,但屏幕内容仍停留在旧主题。 echo "Switching theme to $requested_theme_id / $requested_orientation" -"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP" -"$THEME_SYNC_CMD" --force-index --force-theme --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null +# KUAL 切主题的目标是尽快把 calendar 画出来。 +# 这里强制只用设备上已经同步好的本地主题素材,避免因为网络抖动 +# 又回到首页停留几秒,破坏“直接切到 calendar”的体感。 +"$THEME_SYNC_CMD" --local-only --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null # shellcheck disable=SC1090 . "$THEME_RUNTIME_ENV_FILE" -"$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG" +# 背景图同样只从本地主题包读取。 +# 如果本地包缺图,直接失败,让问题暴露出来;补素材应走后台同步或重新部署, +# 不能把联网等待塞回用户点击主题这条交互链路里。 +"$FETCH_DASHBOARD_CMD" --local-only "$BACKGROUND_PNG" date '+%s' >"$BACKGROUND_TIMESTAMP_FILE" # 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。 @@ -41,6 +42,9 @@ export ORIENTATION='${ORIENTATION}' EOF /usr/sbin/eips -f -g "$BACKGROUND_PNG" -"$CLOCK_RENDER_CMD" true + +# 这里不再同步阻塞等待时钟重绘。 +# 背景先尽快上屏,让用户从 KUAL 返回后更快看到 calendar; +# 后续的时钟和遮罩交给 start.sh 拉起的主循环补画。 echo "Theme switched to ${THEME_ID} / ${ORIENTATION}" diff --git a/dash/staging/post-jailbreak-root/extensions/kindle-dash/menu.json b/dash/staging/post-jailbreak-root/extensions/kindle-dash/menu.json index 1a788cb..0f5f27a 100644 --- a/dash/staging/post-jailbreak-root/extensions/kindle-dash/menu.json +++ b/dash/staging/post-jailbreak-root/extensions/kindle-dash/menu.json @@ -1,12 +1,13 @@ { "items": [ { - "name": "Kindle Dashboard", + "name": "主题选择", "priority": -998, "items": [ - {"name": "Default", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default", "exitmenu": true}, - {"name": "Paper", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "paper", "exitmenu": true}, - {"name": "Classic", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "classic", "exitmenu": true} + {"name": "default-横屏", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default landscape", "exitmenu": true}, + {"name": "default-竖屏", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default portrait", "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"}, diff --git a/scripts/kindle/ssh-fix-all-keys.sh b/scripts/kindle/ssh-fix-all-keys.sh index 63c62d3..0693b8f 100644 --- a/scripts/kindle/ssh-fix-all-keys.sh +++ b/scripts/kindle/ssh-fix-all-keys.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 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 cp "${target_dir}/authorized_keys" "${target_dir}/authorized_keys.bak.${TS}" || true 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 600 "${target_dir}/authorized_keys" 2>/dev/null || true ls -ld "${target_dir}" "${target_dir}/authorized_keys" 2>/dev/null || true diff --git a/scripts/launchd/cn.biboer.kindle-dash.refresh-kindle-backgrounds.plist b/scripts/launchd/cn.biboer.kindle-dash.refresh-kindle-backgrounds.plist new file mode 100644 index 0000000..5fc08cd --- /dev/null +++ b/scripts/launchd/cn.biboer.kindle-dash.refresh-kindle-backgrounds.plist @@ -0,0 +1,109 @@ + + + + + Label + cn.biboer.kindle-dash.refresh-kindle-backgrounds + + WorkingDirectory + /Users/gavin/kindle-dash + + ProgramArguments + + /bin/sh + /Users/gavin/kindle-dash/scripts/refresh-kindle-backgrounds.sh + --once + + + EnvironmentVariables + + PATH + /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin + + + ProcessType + Background + + StandardOutPath + /Users/gavin/Library/Logs/kindle-dash/refresh-kindle-backgrounds.log + + StandardErrorPath + /Users/gavin/Library/Logs/kindle-dash/refresh-kindle-backgrounds.err.log + + StartCalendarInterval + + + Hour + 0 + Minute + 1 + + + Hour + 2 + Minute + 1 + + + Hour + 4 + Minute + 1 + + + Hour + 6 + Minute + 1 + + + Hour + 8 + Minute + 1 + + + Hour + 10 + Minute + 1 + + + Hour + 12 + Minute + 1 + + + Hour + 14 + Minute + 1 + + + Hour + 16 + Minute + 1 + + + Hour + 18 + Minute + 1 + + + Hour + 20 + Minute + 1 + + + Hour + 22 + Minute + 1 + + + + diff --git a/scripts/publish-calendar-dist.sh b/scripts/publish-calendar-dist.sh new file mode 100644 index 0000000..6e64cdb --- /dev/null +++ b/scripts/publish-calendar-dist.sh @@ -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 [选项] + +作用: + 1. 调用 calendar 现有导出链路生成最新背景图与主题资源 + 2. 把 calendar/dist 整体发布到指定目录 + 3. 默认每 60 分钟自动重跑一次;加 --once 只跑一轮 + +选项: + -o, --output-dir 发布目标目录,通常是 Web 服务器实际对外提供静态文件的目录 + --interval-minutes 自动生成间隔,默认 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 diff --git a/scripts/refresh-kindle-backgrounds.sh b/scripts/refresh-kindle-backgrounds.sh new file mode 100644 index 0000000..ff25626 --- /dev/null +++ b/scripts/refresh-kindle-backgrounds.sh @@ -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 自动生成间隔,默认 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 diff --git a/scripts/sync-layered-clock-to-kindle.sh b/scripts/sync-layered-clock-to-kindle.sh index 0d8f63d..2bff1b7 100644 --- a/scripts/sync-layered-clock-to-kindle.sh +++ b/scripts/sync-layered-clock-to-kindle.sh @@ -6,6 +6,8 @@ KINDLE_TARGET="kindle" THEME_FILTER="" ORIENTATION_FILTER="" CLOCK_REGION_JSON="$ROOT_DIR/calendar/dist/clock-region.json" +LOCATION_LAT="" +LOCATION_LON="" print_usage() { cat <<'EOF' @@ -50,20 +52,6 @@ if [ -n "$ORIENTATION_FILTER" ] && [ -z "$THEME_FILTER" ]; then exit 1 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() { key=$1 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/fetch-dashboard.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.sh" \ "$ROOT_DIR/dash/src/local/touch-home-service.sh" \ "$ROOT_DIR/dash/src/local/theme-menu-service.sh" \ "$ROOT_DIR/dash/src/local/theme-json.lua" \ + "$ROOT_DIR/dash/src/local/sync-theme-backgrounds.sh" \ "$ROOT_DIR/dash/src/local/theme-sync.sh" \ "$KINDLE_TARGET":/mnt/us/dashboard/local/ - ssh "$KINDLE_TARGET" "chmod +x /mnt/us/dashboard/start.sh /mnt/us/dashboard/dash.sh /mnt/us/dashboard/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" "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() { ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/extensions/kindle-dash" rsync -av --no-o --no-g \ @@ -124,15 +162,15 @@ sync_theme_bundle() { "$KINDLE_TARGET":/mnt/us/dashboard/ 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 \ "$ROOT_DIR/calendar/dist/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 fi @@ -140,6 +178,21 @@ sync_theme_bundle() { "$ROOT_DIR/calendar/dist/themes/$THEME_FILTER.json" \ "$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 ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/dashboard/themes/$THEME_FILTER/$ORIENTATION_FILTER" rsync -av --no-o --no-g \ @@ -154,6 +207,40 @@ sync_theme_bundle() { "$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() { if [ -z "$CLOCK_REGION_JSON" ] || [ ! -f "$CLOCK_REGION_JSON" ]; then echo "Skipped CLOCK_REGION_* env update: no single background region selected" @@ -185,9 +272,13 @@ update_default_clock_region_env() { } sync_dashboard_runtime +resolve_kindle_location_override +export_theme_bundle_locally sync_theme_bundle sync_kual_extension update_default_clock_region_env +refresh_current_theme_runtime +refresh_current_background_cache if [ -n "$THEME_FILTER" ]; then if [ -n "$ORIENTATION_FILTER" ]; then diff --git a/todo.md b/todo.md index d06bbf9..e0e6c55 100644 --- a/todo.md +++ b/todo.md @@ -5,4 +5,6 @@ 1. dual - dashborad,白屏问题 1. calendar项目融合xcs 1. calendar中嵌入图片 - +1. 右上角系统状态栏遮罩还没有完全压住 +1. 天气预报位置当前基于 GeoIP,只能保证市级近似,不保证精确 +1. 时钟分钟刷新仍依赖设备唤醒;当前唤醒会亮灯,无法做到“不亮灯唤醒但仍肉眼可见更新”