update at 2026-03-21 18:44:12

This commit is contained in:
douboer@gmail.com
2026-03-21 18:44:12 +08:00
parent f9d715157f
commit 89b1f97a6f
52 changed files with 3510 additions and 562 deletions

Binary file not shown.

View File

@@ -0,0 +1,200 @@
{
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/26772867",
"assets_url": "https://api.github.com/repos/bfabiszewski/kterm/releases/26772867/assets",
"upload_url": "https://uploads.github.com/repos/bfabiszewski/kterm/releases/26772867/assets{?name,label}",
"html_url": "https://github.com/bfabiszewski/kterm/releases/tag/v2.6",
"id": 26772867,
"author": {
"login": "bfabiszewski",
"id": 3366666,
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/bfabiszewski",
"html_url": "https://github.com/bfabiszewski",
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"node_id": "MDc6UmVsZWFzZTI2NzcyODY3",
"tag_name": "v2.6",
"target_commitish": "master",
"name": "v2.6",
"draft": false,
"immutable": false,
"prerelease": false,
"created_at": "2020-05-21T20:24:58Z",
"updated_at": "2025-04-10T20:16:30Z",
"published_at": "2020-05-21T20:35:06Z",
"assets": [
{
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/221834164",
"id": 221834164,
"node_id": "RA_kwDOAHb9H84NOOu0",
"name": "kterm-kindle-2.6-armhf.zip",
"label": null,
"uploader": {
"login": "bfabiszewski",
"id": 3366666,
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/bfabiszewski",
"html_url": "https://github.com/bfabiszewski",
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/zip",
"state": "uploaded",
"size": 411954,
"digest": null,
"download_count": 30184,
"created_at": "2025-01-20T12:54:44Z",
"updated_at": "2025-01-20T12:54:46Z",
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6-armhf.zip"
},
{
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/221834154",
"id": 221834154,
"node_id": "RA_kwDOAHb9H84NOOuq",
"name": "kterm-kindle-2.6-armhf.zip.sig",
"label": null,
"uploader": {
"login": "bfabiszewski",
"id": 3366666,
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/bfabiszewski",
"html_url": "https://github.com/bfabiszewski",
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 310,
"digest": null,
"download_count": 201,
"created_at": "2025-01-20T12:54:43Z",
"updated_at": "2025-01-20T12:54:44Z",
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6-armhf.zip.sig"
},
{
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/20954116",
"id": 20954116,
"node_id": "MDEyOlJlbGVhc2VBc3NldDIwOTU0MTE2",
"name": "kterm-kindle-2.6.zip",
"label": null,
"uploader": {
"login": "bfabiszewski",
"id": 3366666,
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/bfabiszewski",
"html_url": "https://github.com/bfabiszewski",
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/zip",
"state": "uploaded",
"size": 417136,
"digest": null,
"download_count": 17785,
"created_at": "2020-05-21T20:35:43Z",
"updated_at": "2020-05-21T20:35:49Z",
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6.zip"
},
{
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/assets/221772992",
"id": 221772992,
"node_id": "RA_kwDOAHb9H84NN_zA",
"name": "kterm-kindle-2.6.zip.sig",
"label": null,
"uploader": {
"login": "bfabiszewski",
"id": 3366666,
"node_id": "MDQ6VXNlcjMzNjY2NjY=",
"avatar_url": "https://avatars.githubusercontent.com/u/3366666?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/bfabiszewski",
"html_url": "https://github.com/bfabiszewski",
"followers_url": "https://api.github.com/users/bfabiszewski/followers",
"following_url": "https://api.github.com/users/bfabiszewski/following{/other_user}",
"gists_url": "https://api.github.com/users/bfabiszewski/gists{/gist_id}",
"starred_url": "https://api.github.com/users/bfabiszewski/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/bfabiszewski/subscriptions",
"organizations_url": "https://api.github.com/users/bfabiszewski/orgs",
"repos_url": "https://api.github.com/users/bfabiszewski/repos",
"events_url": "https://api.github.com/users/bfabiszewski/events{/privacy}",
"received_events_url": "https://api.github.com/users/bfabiszewski/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 310,
"digest": null,
"download_count": 135,
"created_at": "2025-01-20T07:58:01Z",
"updated_at": "2025-01-20T07:58:01Z",
"browser_download_url": "https://github.com/bfabiszewski/kterm/releases/download/v2.6/kterm-kindle-2.6.zip.sig"
}
],
"tarball_url": "https://api.github.com/repos/bfabiszewski/kterm/tarball/v2.6",
"zipball_url": "https://api.github.com/repos/bfabiszewski/kterm/zipball/v2.6",
"body": "- add option to set cursor shape (thanks @efskap !)\r\n\r\n---\r\nfor firmware newer than 5.16.3 use a hard float version with `-armhf` suffix",
"reactions": {
"url": "https://api.github.com/repos/bfabiszewski/kterm/releases/26772867/reactions",
"total_count": 12,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 12,
"rocket": 0,
"eyes": 0
},
"mentions_count": 1
}

View File

@@ -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

View File

@@ -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 <theme-id> [orientation]"}
requested_orientation=${2:-}
# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。
# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑,
# 同时把“选主题”前移到进入 dashboard 之前。
if [ -n "$requested_orientation" ]; then
"$SWITCH_THEME_CMD" "$requested_theme_id" "$requested_orientation"
else
"$SWITCH_THEME_CMD" "$requested_theme_id"
fi
# 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

View File

@@ -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 顶部状态栏遮罩用于压住系统偶尔重画出来的时间、WiFi、电池图标。
# 遮罩只覆盖右上角状态区,避免继续压住 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}

View File

@@ -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

View File

@@ -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")"

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env sh
set -eu
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
ENV_FILE="$DIR/env.sh"
STATE_DIR="$DIR/state"
LOCATION_CACHE_FILE="$STATE_DIR/location.env"
FETCH_CMD="$DIR/../xh"
force_refresh=false
while [ "$#" -gt 0 ]; do
case "$1" in
--force)
force_refresh=true
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
shift
done
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
mkdir -p "$STATE_DIR"
current_epoch() {
date '+%s'
}
cache_refresh_due() {
if [ ! -f "$LOCATION_CACHE_FILE" ]; then
return 0
fi
last_epoch=$(awk -F= '/^export LOCATION_UPDATED_AT_EPOCH=/{gsub("'"'"'", "", $2); print $2; exit}' "$LOCATION_CACHE_FILE" 2>/dev/null || true)
if [ -z "$last_epoch" ]; then
return 0
fi
now_epoch=$(current_epoch)
[ $((now_epoch - last_epoch)) -ge $((LOCATION_REFRESH_INTERVAL_MINUTES * 60)) ]
}
extract_geoip_fields() {
payload_path=$1
lua - "$payload_path" <<'LUA'
local path = arg[1]
local file = io.open(path, "r")
if not file then
os.exit(1)
end
local data = file:read("*a")
file:close()
if data:match('"success"%s*:%s*false') then
os.exit(1)
end
local function match_string(source, pattern)
local value = source:match(pattern)
if not value then
return nil
end
return value
:gsub('\\"', '"')
:gsub("\\\\", "\\")
end
local city = match_string(data, '"city"%s*:%s*"(.-)"')
local latitude = data:match('"latitude"%s*:%s*(-?%d+%.?%d*)')
local longitude = data:match('"longitude"%s*:%s*(-?%d+%.?%d*)')
local timezone_block = data:match('"timezone"%s*:%s*(%b{})')
local timezone = timezone_block and match_string(timezone_block, '"id"%s*:%s*"(.-)"') or nil
if not latitude or not longitude then
os.exit(1)
end
print("CITY=" .. (city or "当前位置"))
print("LAT=" .. latitude)
print("LON=" .. longitude)
print("TIMEZONE=" .. (timezone or "Asia/Shanghai"))
LUA
}
quote_for_shell() {
printf "%s" "$1" | sed "s/'/'\\\\''/g"
}
if [ "$force_refresh" != true ] && ! cache_refresh_due; then
exit 0
fi
if [ ! -x "$FETCH_CMD" ]; then
echo "Location sync failed: xh is unavailable" >&2
exit 1
fi
payload_path="$STATE_DIR/location-response.$$"
rm -f "$payload_path"
if ! "$FETCH_CMD" -d -q -o "$payload_path" get "$LOCATION_GEOIP_URL"; then
rm -f "$payload_path"
echo "Location sync failed: GeoIP request failed" >&2
exit 1
fi
location_city=""
location_lat=""
location_lon=""
location_timezone=""
if ! parsed_output=$(extract_geoip_fields "$payload_path"); then
rm -f "$payload_path"
echo "Location sync failed: unable to parse GeoIP payload" >&2
exit 1
fi
rm -f "$payload_path"
while IFS='=' read -r key value; do
case "$key" in
CITY)
location_city=$value
;;
LAT)
location_lat=$value
;;
LON)
location_lon=$value
;;
TIMEZONE)
location_timezone=$value
;;
esac
done <<EOF
$parsed_output
EOF
if [ -z "$location_lat" ] || [ -z "$location_lon" ]; then
echo "Location sync failed: GeoIP payload missing coordinates" >&2
exit 1
fi
now_epoch=$(current_epoch)
now_iso=$(date '+%Y-%m-%dT%H:%M:%S%z' | sed 's/\(..\)$/:\1/')
cat >"$LOCATION_CACHE_FILE" <<EOF
export LOCATION_SOURCE='geoip'
export LOCATION_CITY='$(quote_for_shell "$location_city")'
export LOCATION_LAT='$(quote_for_shell "$location_lat")'
export LOCATION_LON='$(quote_for_shell "$location_lon")'
export LOCATION_TIMEZONE='$(quote_for_shell "$location_timezone")'
export LOCATION_UPDATED_AT='$(quote_for_shell "$now_iso")'
export LOCATION_UPDATED_AT_EPOCH='$(quote_for_shell "$now_epoch")'
EOF
printf 'Location synced: %s (%s,%s)\n' "$location_city" "$location_lat" "$location_lon"

View File

@@ -36,6 +36,7 @@ clock_minute_thickness=${CLOCK_MINUTE_THICKNESS:-5}
clock_center_radius=${CLOCK_CENTER_RADIUS:-7}
clock_rotation_degrees=${CLOCK_ROTATION_DEGREES:-0}
force_full_refresh=${1:-false}
apply_to_screen=${2:-true}
output_path="$DIR/state/clock-render.pgm"
eval "$("$DIR/clock-index.sh")"
@@ -65,6 +66,10 @@ lua "$DIR/render-clock.lua" \
"$clock_center_radius" \
"$clock_rotation_degrees"
if [ "$apply_to_screen" != true ]; then
exit 0
fi
if [ "$force_full_refresh" = true ]; then
# Kindle Voyage 当前这条链路里fbink 默认会叠加 viewport 修正,
# 导致图像在屏幕上出现双重偏移。这里强制关闭 viewport 修正,

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env sh
set -eu
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
ENV_FILE="$DIR/env.sh"
STATE_DIR="$DIR/state"
THEME_JSON_LUA="$DIR/theme-json.lua"
FETCH_CMD="$DIR/../xh"
WAIT_FOR_WIFI_CMD="$DIR/../wait-for-wifi.sh"
LOCAL_THEMES_INDEX="$DIR/../themes.json"
LOCAL_THEMES_DIR="$DIR/../themes"
LOCAL_BACKGROUND_DIR="$DIR/../kindle-backgrounds"
THEMES_INDEX_CACHE="$STATE_DIR/themes.json"
EXPECTED_THEMES_FILE="$STATE_DIR/expected-theme-configs.txt"
EXPECTED_BACKGROUNDS_FILE="$STATE_DIR/expected-theme-backgrounds.txt"
# shellcheck disable=SC1090
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
THEMES_INDEX_URL=${THEMES_INDEX_URL:-"https://shell.biboer.cn:20001/themes.json"}
mkdir -p "$STATE_DIR" "$LOCAL_THEMES_DIR" "$LOCAL_BACKGROUND_DIR"
fetch_to_path() {
url=$1
output_path=$2
tmp_path="${output_path}.tmp.$$"
rm -f "$tmp_path"
if ! "$FETCH_CMD" -d -q -o "$tmp_path" get "$url"; then
rm -f "$tmp_path"
return 1
fi
mv "$tmp_path" "$output_path"
return 0
}
is_valid_png() {
candidate_path=$1
if [ ! -f "$candidate_path" ]; then
return 1
fi
if command -v file >/dev/null 2>&1; then
if file "$candidate_path" 2>/dev/null | grep -q "PNG image data"; then
return 0
fi
fi
signature=$(dd if="$candidate_path" bs=8 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')
[ "$signature" = "89504e470d0a1a0a" ]
}
fetch_png_to_path() {
url=$1
output_path=$2
tmp_path="${output_path}.tmp.$$"
rm -f "$tmp_path"
if ! "$FETCH_CMD" -d -q -o "$tmp_path" get "$url"; then
rm -f "$tmp_path"
return 1
fi
if ! is_valid_png "$tmp_path"; then
rm -f "$tmp_path"
return 1
fi
mv "$tmp_path" "$output_path"
return 0
}
cleanup_stale_files() {
target_dir=$1
expected_file=$2
pattern=$3
for existing_path in "$target_dir"/$pattern; do
[ -e "$existing_path" ] || continue
existing_name=$(basename "$existing_path")
if ! grep -Fqx "$existing_name" "$expected_file"; then
rm -f "$existing_path"
fi
done
}
echo "Syncing all theme backgrounds from remote"
"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP"
if ! fetch_to_path "$THEMES_INDEX_URL" "$THEMES_INDEX_CACHE"; then
echo "Themes index fetch failed: $THEMES_INDEX_URL" >&2
exit 1
fi
cp "$THEMES_INDEX_CACHE" "$LOCAL_THEMES_INDEX"
: >"$EXPECTED_THEMES_FILE"
: >"$EXPECTED_BACKGROUNDS_FILE"
theme_configs=$(lua "$THEME_JSON_LUA" list-configs "$THEMES_INDEX_CACHE")
if [ -z "$theme_configs" ]; then
echo "Themes index is empty." >&2
exit 1
fi
printf '%s\n' "$theme_configs" | while IFS="$(printf '\t')" read -r theme_id config_url; do
[ -n "$theme_id" ] || continue
[ -n "$config_url" ] || continue
local_theme_config="$LOCAL_THEMES_DIR/$theme_id.json"
if ! fetch_to_path "$config_url" "$local_theme_config"; then
echo "Theme config fetch failed: $theme_id $config_url" >&2
exit 1
fi
printf '%s\n' "$(basename "$local_theme_config")" >>"$EXPECTED_THEMES_FILE"
theme_backgrounds=$(lua "$THEME_JSON_LUA" list-backgrounds "$local_theme_config")
if [ -z "$theme_backgrounds" ]; then
echo "Theme config has no backgrounds: $theme_id" >&2
exit 1
fi
printf '%s\n' "$theme_backgrounds" | while IFS="$(printf '\t')" read -r orientation background_path background_url; do
[ -n "$background_path" ] || continue
[ -n "$background_url" ] || continue
background_file=$(basename "$background_path")
local_background_path="$LOCAL_BACKGROUND_DIR/$background_file"
if ! fetch_png_to_path "$background_url" "$local_background_path"; then
echo "Theme background fetch failed: $theme_id $orientation $background_url" >&2
exit 1
fi
printf '%s\n' "$background_file" >>"$EXPECTED_BACKGROUNDS_FILE"
done
done
cleanup_stale_files "$LOCAL_THEMES_DIR" "$EXPECTED_THEMES_FILE" "*.json"
cleanup_stale_files "$LOCAL_BACKGROUND_DIR" "$EXPECTED_BACKGROUNDS_FILE" "*.png"
echo "Theme background sync completed"

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 </dev/null &
fi
printf '%s\n' "$!" >"$PID_FILE"
echo "start.sh spawned dashboard launcher pid=$!" >>"$LOG_FILE"
fi

View File

@@ -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}"

View File

@@ -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"},