update at 2026-03-18 13:35:19
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
DASH_PNG="$DIR/dash.png"
|
||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||
LOW_BATTERY_CMD="$DIR/local/low-battery.sh"
|
||||
|
||||
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600}
|
||||
RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||
|
||||
LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||
|
||||
num_refresh=0
|
||||
|
||||
init() {
|
||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
||||
echo "Missing required configuration."
|
||||
echo "Timezone: ${TIMEZONE:-(not set)}."
|
||||
echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
||||
|
||||
/etc/init.d/framework stop
|
||||
initctl stop webreader >/dev/null 2>&1
|
||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
||||
}
|
||||
|
||||
prepare_sleep() {
|
||||
echo "Preparing sleep"
|
||||
|
||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||
|
||||
# Give screen time to refresh
|
||||
sleep 2
|
||||
|
||||
# Ensure a full screen refresh is triggered after wake from sleep
|
||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||
}
|
||||
|
||||
refresh_dashboard() {
|
||||
echo "Refreshing dashboard"
|
||||
"$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP"
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$DASH_PNG"
|
||||
fetch_status=$?
|
||||
|
||||
if [ "$fetch_status" -ne 0 ]; then
|
||||
echo "Not updating screen, fetch-dashboard returned $fetch_status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then
|
||||
num_refresh=0
|
||||
|
||||
# trigger a full refresh once in every 4 refreshes, to keep the screen clean
|
||||
echo "Full screen refresh"
|
||||
/usr/sbin/eips -f -g "$DASH_PNG"
|
||||
else
|
||||
echo "Partial screen refresh"
|
||||
/usr/sbin/eips -g "$DASH_PNG"
|
||||
fi
|
||||
|
||||
num_refresh=$((num_refresh + 1))
|
||||
}
|
||||
|
||||
log_battery_stats() {
|
||||
battery_level=$(gasgauge-info -c)
|
||||
echo "$(date) Battery level: $battery_level."
|
||||
|
||||
if [ "$LOW_BATTERY_REPORTING" = true ]; then
|
||||
battery_level_numeric=${battery_level%?}
|
||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
rtc_sleep() {
|
||||
duration=$1
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
sleep "$duration"
|
||||
else
|
||||
# shellcheck disable=SC2039
|
||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
||||
echo "mem" >/sys/power/state
|
||||
fi
|
||||
}
|
||||
|
||||
main_loop() {
|
||||
while true; do
|
||||
log_battery_stats
|
||||
|
||||
next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE")
|
||||
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then
|
||||
action="sleep"
|
||||
prepare_sleep
|
||||
else
|
||||
action="suspend"
|
||||
refresh_dashboard
|
||||
fi
|
||||
|
||||
# take a bit of time before going to sleep, so this process can be aborted
|
||||
sleep 10
|
||||
|
||||
echo "Going to $action, next wakeup in ${next_wakeup_secs}s"
|
||||
|
||||
rtc_sleep "$next_wakeup_secs"
|
||||
done
|
||||
}
|
||||
|
||||
init
|
||||
main_loop
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 持久关闭调试模式:修改 env.sh,恢复正常的省电挂起行为。
|
||||
set -eu
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
TMP_FILE="$DIR/local/env.sh.tmp"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "未找到配置文件:$ENV_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 只替换目标配置,避免重复追加同一环境变量。
|
||||
awk '
|
||||
BEGIN {
|
||||
updated = 0
|
||||
}
|
||||
/^export DISABLE_SYSTEM_SUSPEND=/ {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=false"
|
||||
updated = 1
|
||||
next
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (!updated) {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=false"
|
||||
}
|
||||
}
|
||||
' "$ENV_FILE" > "$TMP_FILE"
|
||||
|
||||
mv "$TMP_FILE" "$ENV_FILE"
|
||||
|
||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||
# 避免旧进程继续按旧配置运行。
|
||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
||||
|
||||
echo "已关闭 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。"
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 持久开启调试模式:修改 env.sh,让后续普通启动也不进入系统挂起。
|
||||
set -eu
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
TMP_FILE="$DIR/local/env.sh.tmp"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "未找到配置文件:$ENV_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 只替换目标配置,避免重复追加同一环境变量。
|
||||
awk '
|
||||
BEGIN {
|
||||
updated = 0
|
||||
}
|
||||
/^export DISABLE_SYSTEM_SUSPEND=/ {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=true"
|
||||
updated = 1
|
||||
next
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (!updated) {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=true"
|
||||
}
|
||||
}
|
||||
' "$ENV_FILE" > "$TMP_FILE"
|
||||
|
||||
mv "$TMP_FILE" "$ENV_FILE"
|
||||
|
||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||
# 避免旧进程继续按旧配置进入系统挂起。
|
||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
||||
|
||||
echo "已开启 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。"
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Export environment variables here
|
||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||
|
||||
# By default, partial screen updates are used to update the screen,
|
||||
# to prevent the screen from flashing. After a few partial updates,
|
||||
# the screen will start to look a bit distorted (due to e-ink ghosting).
|
||||
# 测试阶段强制每次都做一次全刷,避免首页残影和局部刷新的旧内容干扰验证。
|
||||
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
||||
export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
|
||||
# When the time until the next wakeup is greater or equal to this number,
|
||||
# the dashboard will not be refreshed anymore, but instead show a
|
||||
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
||||
# during the day, for example.
|
||||
export SLEEP_SCREEN_INTERVAL=3600
|
||||
|
||||
export LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
export LOW_BATTERY_THRESHOLD_PERCENT=10
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Fetch a new dashboard image, make sure to output it to "$1".
|
||||
# For example:
|
||||
"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
battery_level_percentage=$1
|
||||
last_battery_report_state="$(dirname "$0")/state/last_battery_report"
|
||||
|
||||
previous_report_timestamp=$(cat "$last_battery_report_state" 2>/dev/null || echo '-1')
|
||||
now=$(date +%s)
|
||||
|
||||
# Implement desired logic here. The example below for example only reports low
|
||||
# battery every 24 hours.
|
||||
|
||||
if [ "$previous_report_timestamp" -eq -1 ] ||
|
||||
[ $((now - previous_report_timestamp)) -gt 86400 ]; then
|
||||
# Replace this with for example an HTTP call via curl, or xh
|
||||
echo "Reporting low battery: $battery_level_percentage%"
|
||||
|
||||
echo "$now" >"$last_battery_report_state"
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 调试启动脚本:强制关闭系统挂起,便于在 Kindle 上持续观察刷新效果。
|
||||
DIR="$(dirname "$0")"
|
||||
|
||||
export DISABLE_SYSTEM_SUSPEND=true
|
||||
|
||||
exec "$DIR/start.sh"
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
LOG_FILE="$DIR/logs/dash.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
"$DIR/dash.sh"
|
||||
else
|
||||
"$DIR/dash.sh" >>"$LOG_FILE" 2>&1 &
|
||||
fi
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
pkill -f dash.sh
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
test_ip=$1
|
||||
|
||||
if [ -z "$test_ip" ]; then
|
||||
echo "No test ip specified"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_wifi() {
|
||||
max_retry=30
|
||||
counter=0
|
||||
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
|
||||
# shellcheck disable=SC2181
|
||||
while [ $? -ne 0 ]; do
|
||||
[ $counter -eq $max_retry ] && echo "Couldn't connect to Wi-Fi" && exit 1
|
||||
counter=$((counter + 1))
|
||||
|
||||
sleep 1
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_wifi
|
||||
echo "Wi-Fi connected"
|
||||
Binary file not shown.
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension>
|
||||
<information>
|
||||
<name>Kindle dashboard</name>
|
||||
<id>pascalw-kindle-dash</id>
|
||||
</information>
|
||||
<menus>
|
||||
<menu type="json" dynamic="true">menu.json</menu>
|
||||
</menus>
|
||||
</extension>
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"items": [
|
||||
{"name": "Kindle Dashboard", "action": "/mnt/us/dashboard/start.sh"},
|
||||
{"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
||||
{"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"}
|
||||
]
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
DASH_PNG="$DIR/dash.png"
|
||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||
LOW_BATTERY_CMD="$DIR/local/low-battery.sh"
|
||||
|
||||
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600}
|
||||
RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||
|
||||
LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||
|
||||
num_refresh=0
|
||||
|
||||
init() {
|
||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
||||
echo "Missing required configuration."
|
||||
echo "Timezone: ${TIMEZONE:-(not set)}."
|
||||
echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
||||
|
||||
/etc/init.d/framework stop
|
||||
initctl stop webreader >/dev/null 2>&1
|
||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
||||
}
|
||||
|
||||
prepare_sleep() {
|
||||
echo "Preparing sleep"
|
||||
|
||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||
|
||||
# Give screen time to refresh
|
||||
sleep 2
|
||||
|
||||
# Ensure a full screen refresh is triggered after wake from sleep
|
||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||
}
|
||||
|
||||
refresh_dashboard() {
|
||||
echo "Refreshing dashboard"
|
||||
"$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP"
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$DASH_PNG"
|
||||
fetch_status=$?
|
||||
|
||||
if [ "$fetch_status" -ne 0 ]; then
|
||||
echo "Not updating screen, fetch-dashboard returned $fetch_status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then
|
||||
num_refresh=0
|
||||
|
||||
# trigger a full refresh once in every 4 refreshes, to keep the screen clean
|
||||
echo "Full screen refresh"
|
||||
/usr/sbin/eips -f -g "$DASH_PNG"
|
||||
else
|
||||
echo "Partial screen refresh"
|
||||
/usr/sbin/eips -g "$DASH_PNG"
|
||||
fi
|
||||
|
||||
num_refresh=$((num_refresh + 1))
|
||||
}
|
||||
|
||||
log_battery_stats() {
|
||||
battery_level=$(gasgauge-info -c)
|
||||
echo "$(date) Battery level: $battery_level."
|
||||
|
||||
if [ "$LOW_BATTERY_REPORTING" = true ]; then
|
||||
battery_level_numeric=${battery_level%?}
|
||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
rtc_sleep() {
|
||||
duration=$1
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
sleep "$duration"
|
||||
else
|
||||
# shellcheck disable=SC2039
|
||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
||||
echo "mem" >/sys/power/state
|
||||
fi
|
||||
}
|
||||
|
||||
main_loop() {
|
||||
while true; do
|
||||
log_battery_stats
|
||||
|
||||
next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE")
|
||||
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then
|
||||
action="sleep"
|
||||
prepare_sleep
|
||||
else
|
||||
action="suspend"
|
||||
refresh_dashboard
|
||||
fi
|
||||
|
||||
# take a bit of time before going to sleep, so this process can be aborted
|
||||
sleep 10
|
||||
|
||||
echo "Going to $action, next wakeup in ${next_wakeup_secs}s"
|
||||
|
||||
rtc_sleep "$next_wakeup_secs"
|
||||
done
|
||||
}
|
||||
|
||||
init
|
||||
main_loop
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 持久关闭调试模式:修改 env.sh,恢复正常的省电挂起行为。
|
||||
set -eu
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
TMP_FILE="$DIR/local/env.sh.tmp"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "未找到配置文件:$ENV_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 只替换目标配置,避免重复追加同一环境变量。
|
||||
awk '
|
||||
BEGIN {
|
||||
updated = 0
|
||||
}
|
||||
/^export DISABLE_SYSTEM_SUSPEND=/ {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=false"
|
||||
updated = 1
|
||||
next
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (!updated) {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=false"
|
||||
}
|
||||
}
|
||||
' "$ENV_FILE" > "$TMP_FILE"
|
||||
|
||||
mv "$TMP_FILE" "$ENV_FILE"
|
||||
|
||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||
# 避免旧进程继续按旧配置运行。
|
||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
||||
|
||||
echo "已关闭 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。"
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 持久开启调试模式:修改 env.sh,让后续普通启动也不进入系统挂起。
|
||||
set -eu
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
TMP_FILE="$DIR/local/env.sh.tmp"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "未找到配置文件:$ENV_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 只替换目标配置,避免重复追加同一环境变量。
|
||||
awk '
|
||||
BEGIN {
|
||||
updated = 0
|
||||
}
|
||||
/^export DISABLE_SYSTEM_SUSPEND=/ {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=true"
|
||||
updated = 1
|
||||
next
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (!updated) {
|
||||
print "export DISABLE_SYSTEM_SUSPEND=true"
|
||||
}
|
||||
}
|
||||
' "$ENV_FILE" > "$TMP_FILE"
|
||||
|
||||
mv "$TMP_FILE" "$ENV_FILE"
|
||||
|
||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||
# 避免旧进程继续按旧配置进入系统挂起。
|
||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
||||
|
||||
echo "已开启 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。"
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Export environment variables here
|
||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||
|
||||
# By default, partial screen updates are used to update the screen,
|
||||
# to prevent the screen from flashing. After a few partial updates,
|
||||
# the screen will start to look a bit distorted (due to e-ink ghosting).
|
||||
# 测试阶段强制每次都做一次全刷,避免首页残影和局部刷新的旧内容干扰验证。
|
||||
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
||||
export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
|
||||
# When the time until the next wakeup is greater or equal to this number,
|
||||
# the dashboard will not be refreshed anymore, but instead show a
|
||||
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
||||
# during the day, for example.
|
||||
export SLEEP_SCREEN_INTERVAL=3600
|
||||
|
||||
export LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
export LOW_BATTERY_THRESHOLD_PERCENT=10
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Fetch a new dashboard image, make sure to output it to "$1".
|
||||
# For example:
|
||||
"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
battery_level_percentage=$1
|
||||
last_battery_report_state="$(dirname "$0")/state/last_battery_report"
|
||||
|
||||
previous_report_timestamp=$(cat "$last_battery_report_state" 2>/dev/null || echo '-1')
|
||||
now=$(date +%s)
|
||||
|
||||
# Implement desired logic here. The example below for example only reports low
|
||||
# battery every 24 hours.
|
||||
|
||||
if [ "$previous_report_timestamp" -eq -1 ] ||
|
||||
[ $((now - previous_report_timestamp)) -gt 86400 ]; then
|
||||
# Replace this with for example an HTTP call via curl, or xh
|
||||
echo "Reporting low battery: $battery_level_percentage%"
|
||||
|
||||
echo "$now" >"$last_battery_report_state"
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 调试启动脚本:强制关闭系统挂起,便于在 Kindle 上持续观察刷新效果。
|
||||
DIR="$(dirname "$0")"
|
||||
|
||||
export DISABLE_SYSTEM_SUSPEND=true
|
||||
|
||||
exec "$DIR/start.sh"
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
LOG_FILE="$DIR/logs/dash.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
"$DIR/dash.sh"
|
||||
else
|
||||
"$DIR/dash.sh" >>"$LOG_FILE" 2>&1 &
|
||||
fi
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
pkill -f dash.sh
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
test_ip=$1
|
||||
|
||||
if [ -z "$test_ip" ]; then
|
||||
echo "No test ip specified"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_wifi() {
|
||||
max_retry=30
|
||||
counter=0
|
||||
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
|
||||
# shellcheck disable=SC2181
|
||||
while [ $? -ne 0 ]; do
|
||||
[ $counter -eq $max_retry ] && echo "Couldn't connect to Wi-Fi" && exit 1
|
||||
counter=$((counter + 1))
|
||||
|
||||
sleep 1
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_wifi
|
||||
echo "Wi-Fi connected"
|
||||
Binary file not shown.
31
dash/staging/kterm/README.md
Normal file
31
dash/staging/kterm/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# KTerm 安装包放置说明
|
||||
|
||||
如果你要让 `bootstrap-new-kindle.sh` 在 `prepare-storage` 阶段一并预置 `KTerm`,请把官方 `KTerm` release 的安装包 `.zip` 放到这个目录。
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
dash/staging/kterm/kterm-kindle-*.zip
|
||||
```
|
||||
|
||||
脚本会自动尝试拾取这个目录下的第一个 `.zip` 文件,并直接解压到 Kindle 的 `extensions/`。
|
||||
|
||||
如果你不想放在仓库里,也可以执行时显式指定:
|
||||
|
||||
```sh
|
||||
sh bootstrap-new-kindle.sh prepare-storage --kterm-package /绝对路径/kterm-kindle-*.zip
|
||||
```
|
||||
|
||||
如果你希望脚本直接在 Mac 侧联网下载,也可以:
|
||||
|
||||
```sh
|
||||
sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version latest
|
||||
```
|
||||
|
||||
或固定版本:
|
||||
|
||||
```sh
|
||||
sh bootstrap-new-kindle.sh prepare-storage --download-kterm --kterm-version v2.6
|
||||
```
|
||||
|
||||
下载后的 `.zip` 也会缓存到这个目录里,后续再次执行时可直接复用。
|
||||
@@ -3,19 +3,78 @@ DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
DASH_PNG="$DIR/dash.png"
|
||||
BACKGROUND_PNG="$DIR/kindlebg.png"
|
||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||
LOW_BATTERY_CMD="$DIR/local/low-battery.sh"
|
||||
CLOCK_RENDER_CMD="$DIR/local/render-clock.sh"
|
||||
STATE_DIR="$DIR/local/state"
|
||||
BACKGROUND_TIMESTAMP_FILE="$STATE_DIR/background-updated-at"
|
||||
THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env"
|
||||
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
||||
THEME_MENU_SERVICE_CMD="$DIR/local/theme-menu-service.sh"
|
||||
THEME_MENU_LOG_FILE="$DIR/logs/theme-menu.log"
|
||||
TOUCH_HOME_SERVICE_CMD="$DIR/local/touch-home-service.sh"
|
||||
TOUCH_HOME_LOG_FILE="$DIR/logs/touch-home.log"
|
||||
|
||||
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600}
|
||||
DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false}
|
||||
BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
||||
CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15}
|
||||
PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
||||
MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60}
|
||||
MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
||||
STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
||||
STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
||||
STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
||||
STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372}
|
||||
STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24}
|
||||
STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3}
|
||||
STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1}
|
||||
RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||
KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false}
|
||||
|
||||
LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||
|
||||
num_refresh=0
|
||||
background_needs_redraw=true
|
||||
|
||||
start_theme_menu_service() {
|
||||
if [ "${THEME_MENU_ENABLED:-false}" != true ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -x "$THEME_MENU_SERVICE_CMD" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$THEME_MENU_LOG_FILE")"
|
||||
pkill -f "$THEME_MENU_SERVICE_CMD" 2>/dev/null || true
|
||||
nohup "$THEME_MENU_SERVICE_CMD" >>"$THEME_MENU_LOG_FILE" 2>&1 </dev/null &
|
||||
}
|
||||
|
||||
start_touch_home_service() {
|
||||
if [ "${TOUCH_HOME_ENABLED:-false}" != true ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -x "$TOUCH_HOME_SERVICE_CMD" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$TOUCH_HOME_LOG_FILE")"
|
||||
pkill -f "$TOUCH_HOME_SERVICE_CMD" 2>/dev/null || true
|
||||
nohup "$TOUCH_HOME_SERVICE_CMD" >>"$TOUCH_HOME_LOG_FILE" 2>&1 </dev/null &
|
||||
}
|
||||
|
||||
load_theme_runtime_config() {
|
||||
if [ -f "$THEME_RUNTIME_ENV_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$THEME_RUNTIME_ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
init() {
|
||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
||||
@@ -26,17 +85,41 @@ init() {
|
||||
fi
|
||||
|
||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
if [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||
echo "System suspend disabled, using normal sleep between refreshes."
|
||||
fi
|
||||
|
||||
if [ "$KEEP_NATIVE_UI_STACK_RUNNING" = true ]; then
|
||||
echo "Keeping framework/webreader running for native UI overlay mode."
|
||||
else
|
||||
stop_framework
|
||||
initctl stop webreader >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
/etc/init.d/framework stop
|
||||
initctl stop webreader >/dev/null 2>&1
|
||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
||||
start_theme_menu_service
|
||||
start_touch_home_service
|
||||
}
|
||||
|
||||
stop_framework() {
|
||||
# 不同 Kindle 固件停止 framework 的入口不完全一致。
|
||||
# Voyage 5.13.6 上没有 /etc/init.d/framework,需要走 upstart 入口。
|
||||
if [ -x /etc/init.d/framework ]; then
|
||||
/etc/init.d/framework stop || true
|
||||
return
|
||||
fi
|
||||
|
||||
stop framework >/dev/null 2>&1 || initctl stop framework >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
prepare_sleep() {
|
||||
echo "Preparing sleep"
|
||||
|
||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||
background_needs_redraw=true
|
||||
|
||||
# Give screen time to refresh
|
||||
sleep 2
|
||||
@@ -45,41 +128,151 @@ prepare_sleep() {
|
||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||
}
|
||||
|
||||
refresh_dashboard() {
|
||||
echo "Refreshing dashboard"
|
||||
now_epoch() {
|
||||
date '+%s'
|
||||
}
|
||||
|
||||
background_refresh_due() {
|
||||
load_theme_runtime_config
|
||||
|
||||
if [ ! -f "$BACKGROUND_PNG" ] || [ ! -f "$BACKGROUND_TIMESTAMP_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
current_epoch=$(now_epoch)
|
||||
last_background_epoch=$(cat "$BACKGROUND_TIMESTAMP_FILE")
|
||||
refresh_interval_seconds=$((BACKGROUND_REFRESH_INTERVAL_MINUTES * 60))
|
||||
|
||||
[ $((current_epoch - last_background_epoch)) -ge "$refresh_interval_seconds" ]
|
||||
}
|
||||
|
||||
store_background_timestamp() {
|
||||
now_epoch >"$BACKGROUND_TIMESTAMP_FILE"
|
||||
}
|
||||
|
||||
fetch_background() {
|
||||
echo "Refreshing background"
|
||||
"$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP"
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$DASH_PNG"
|
||||
if "$THEME_SYNC_CMD" >/dev/null; then
|
||||
load_theme_runtime_config
|
||||
elif [ ! -f "$THEME_RUNTIME_ENV_FILE" ]; then
|
||||
echo "Theme sync failed and no runtime theme config is available."
|
||||
return 1
|
||||
else
|
||||
echo "Theme sync failed, using cached runtime theme config."
|
||||
load_theme_runtime_config
|
||||
fi
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG"
|
||||
fetch_status=$?
|
||||
|
||||
if [ "$fetch_status" -ne 0 ]; then
|
||||
echo "Not updating screen, fetch-dashboard returned $fetch_status"
|
||||
echo "Background fetch failed with $fetch_status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then
|
||||
num_refresh=0
|
||||
store_background_timestamp
|
||||
return 0
|
||||
}
|
||||
|
||||
# trigger a full refresh once in every 4 refreshes, to keep the screen clean
|
||||
echo "Full screen refresh"
|
||||
/usr/sbin/eips -f -g "$DASH_PNG"
|
||||
else
|
||||
echo "Partial screen refresh"
|
||||
/usr/sbin/eips -g "$DASH_PNG"
|
||||
clock_force_full_refresh() {
|
||||
eval "$("$DIR/local/clock-index.sh")"
|
||||
[ $((minute % CLOCK_FULL_REFRESH_INTERVAL_MINUTES)) -eq 0 ]
|
||||
}
|
||||
|
||||
mask_system_status_overlay() {
|
||||
if [ "$STATUS_MASK_ENABLED" != true ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Voyage 上 framework/appmgrd 偶尔会把右上角时间与状态图标重新画回屏幕。
|
||||
# 这里在每次 dashboard 刷新后,用顶部空白带的白底把它盖掉。
|
||||
# 实测需要延迟后再补盖一次,否则系统可能会在我们第一次覆盖后再重画一遍。
|
||||
pass=1
|
||||
while [ "$pass" -le "$STATUS_MASK_PASSES" ]; do
|
||||
fbink -q -V -B WHITE -k \
|
||||
"top=$STATUS_MASK_TOP,left=$STATUS_MASK_LEFT,width=$STATUS_MASK_WIDTH,height=$STATUS_MASK_HEIGHT"
|
||||
|
||||
if [ "$pass" -lt "$STATUS_MASK_PASSES" ] && [ "$STATUS_MASK_DELAY_SECONDS" -gt 0 ]; then
|
||||
sleep "$STATUS_MASK_DELAY_SECONDS"
|
||||
fi
|
||||
|
||||
pass=$((pass + 1))
|
||||
done
|
||||
}
|
||||
|
||||
refresh_dashboard() {
|
||||
background_refreshed=false
|
||||
|
||||
if background_refresh_due; then
|
||||
if fetch_background; then
|
||||
background_refreshed=true
|
||||
echo "Full screen refresh"
|
||||
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
||||
elif [ ! -f "$BACKGROUND_PNG" ]; then
|
||||
echo "No cached background available."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$background_refreshed" = false ] && [ ! -f "$BACKGROUND_PNG" ]; then
|
||||
echo "No cached background available."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$background_refreshed" = false ] && [ "$background_needs_redraw" = true ]; then
|
||||
echo "Restoring cached background"
|
||||
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
||||
background_needs_redraw=false
|
||||
fi
|
||||
|
||||
if [ "$background_refreshed" = true ]; then
|
||||
background_needs_redraw=false
|
||||
fi
|
||||
|
||||
if [ "$background_refreshed" = true ] || clock_force_full_refresh; then
|
||||
echo "Clock patch full refresh"
|
||||
"$CLOCK_RENDER_CMD" true
|
||||
else
|
||||
echo "Clock patch partial refresh"
|
||||
"$CLOCK_RENDER_CMD" false
|
||||
fi
|
||||
|
||||
mask_system_status_overlay
|
||||
|
||||
num_refresh=$((num_refresh + 1))
|
||||
}
|
||||
|
||||
powerd_get_prop() {
|
||||
prop_name=$1
|
||||
|
||||
if ! command -v lipc-get-prop >/dev/null 2>&1; then
|
||||
echo "unavailable"
|
||||
return 0
|
||||
fi
|
||||
|
||||
lipc-get-prop com.lab126.powerd "$prop_name" 2>/dev/null || echo "unavailable"
|
||||
}
|
||||
|
||||
log_battery_stats() {
|
||||
battery_level=$(gasgauge-info -c)
|
||||
echo "$(date) Battery level: $battery_level."
|
||||
battery_level=$(gasgauge-info -c 2>/dev/null || echo "unknown")
|
||||
charging_state=$(powerd_get_prop isCharging)
|
||||
battery_state_info=$(powerd_get_prop battStateInfo)
|
||||
|
||||
# 同时记录 powerd 的充电标志与原始电池状态,便于直接从 dash.log
|
||||
# 判断 Kindle 是否识别到外部供电,以及是否真的在充电。
|
||||
echo "$(date) Battery level: $battery_level. isCharging: $charging_state. battStateInfo: $battery_state_info."
|
||||
|
||||
if [ "$LOW_BATTERY_REPORTING" = true ]; then
|
||||
battery_level_numeric=${battery_level%?}
|
||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||
fi
|
||||
case "$battery_level" in
|
||||
*%)
|
||||
battery_level_numeric=${battery_level%?}
|
||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -88,6 +281,9 @@ rtc_sleep() {
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
sleep "$duration"
|
||||
elif [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||
echo "Skipping system suspend, sleeping for ${duration}s instead"
|
||||
sleep "$duration"
|
||||
else
|
||||
# shellcheck disable=SC2039
|
||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
||||
@@ -95,26 +291,73 @@ rtc_sleep() {
|
||||
fi
|
||||
}
|
||||
|
||||
manual_wake_detected() {
|
||||
requested_duration=$1
|
||||
actual_duration=$2
|
||||
|
||||
if [ "$DEBUG" = true ] || [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$requested_duration" -le "$MANUAL_WAKE_EARLY_TOLERANCE_SECONDS" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
[ "$actual_duration" -lt $((requested_duration - MANUAL_WAKE_EARLY_TOLERANCE_SECONDS)) ]
|
||||
}
|
||||
|
||||
hold_after_manual_wake() {
|
||||
requested_duration=$1
|
||||
sleep_started_at=$2
|
||||
sleep_finished_at=$3
|
||||
actual_duration=$((sleep_finished_at - sleep_started_at))
|
||||
|
||||
if ! manual_wake_detected "$requested_duration" "$actual_duration"; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Manual wake detected after ${actual_duration}s, keeping awake for ${MANUAL_WAKE_KEEP_AWAKE_SECONDS}s"
|
||||
|
||||
# 短按电源键提前唤醒后,先把 dashboard 内容恢复回来,
|
||||
# 再给出一段明确的可交互窗口,避免 2~3 秒内再次休眠。
|
||||
refresh_dashboard || true
|
||||
sleep "$MANUAL_WAKE_KEEP_AWAKE_SECONDS"
|
||||
}
|
||||
|
||||
main_loop() {
|
||||
while true; do
|
||||
log_battery_stats
|
||||
|
||||
next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE")
|
||||
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" != true ]; then
|
||||
action="sleep"
|
||||
prepare_sleep
|
||||
else
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ] && [ "$DISABLE_SYSTEM_SUSPEND" = true ]; then
|
||||
echo "Debug mode active, skipping sleeping screen."
|
||||
fi
|
||||
action="suspend"
|
||||
refresh_dashboard
|
||||
fi
|
||||
|
||||
# take a bit of time before going to sleep, so this process can be aborted
|
||||
sleep 10
|
||||
actual_sleep_secs=$next_wakeup_secs
|
||||
if [ "$actual_sleep_secs" -gt "$PRE_SLEEP_GRACE_SECONDS" ]; then
|
||||
actual_sleep_secs=$((actual_sleep_secs - PRE_SLEEP_GRACE_SECONDS))
|
||||
else
|
||||
actual_sleep_secs=0
|
||||
fi
|
||||
|
||||
# 预留一小段可中断窗口,便于在 Kindle 本机或 SSH 下手动终止进程。
|
||||
# 这段时间必须从 rtc_sleep 中扣掉,否则每分钟刷新会长期晚于计划时间。
|
||||
sleep "$PRE_SLEEP_GRACE_SECONDS"
|
||||
|
||||
echo "Going to $action, next wakeup in ${next_wakeup_secs}s"
|
||||
|
||||
rtc_sleep "$next_wakeup_secs"
|
||||
sleep_started_at=$(now_epoch)
|
||||
rtc_sleep "$actual_sleep_secs"
|
||||
sleep_finished_at=$(now_epoch)
|
||||
hold_after_manual_wake "$actual_sleep_secs" "$sleep_started_at" "$sleep_finished_at"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,12 @@ END {
|
||||
mv "$TMP_FILE" "$ENV_FILE"
|
||||
|
||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||
# 避免旧进程继续按旧配置运行。
|
||||
# 然后立刻拉起新的 dashboard,避免用户还要再次手动启动。
|
||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
||||
pkill -f "$DIR/start.sh" 2>/dev/null || true
|
||||
pkill -f "$DIR/local/theme-menu-service.sh" 2>/dev/null || true
|
||||
pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true
|
||||
sleep 1
|
||||
"$DIR/launch-from-kual.sh"
|
||||
|
||||
echo "已关闭 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。"
|
||||
echo "已关闭 Dashboard 调试模式,并自动重启 Kindle Dashboard。"
|
||||
|
||||
@@ -35,7 +35,12 @@ END {
|
||||
mv "$TMP_FILE" "$ENV_FILE"
|
||||
|
||||
# 已运行的 dashboard 进程不会重新读取 env.sh,切换后先停掉它,
|
||||
# 避免旧进程继续按旧配置进入系统挂起。
|
||||
# 然后立刻拉起新的 dashboard,避免用户还要在短时间内再点一次菜单。
|
||||
pkill -f "$DIR/dash.sh" 2>/dev/null || true
|
||||
pkill -f "$DIR/start.sh" 2>/dev/null || true
|
||||
pkill -f "$DIR/local/theme-menu-service.sh" 2>/dev/null || true
|
||||
pkill -f "$DIR/local/touch-home-service.sh" 2>/dev/null || true
|
||||
sleep 1
|
||||
"$DIR/launch-from-kual.sh"
|
||||
|
||||
echo "已开启 Dashboard 调试模式。当前 Dashboard 已停止,请重新启动 Kindle Dashboard。"
|
||||
echo "已开启 Dashboard 调试模式,并自动重启 Kindle Dashboard。"
|
||||
|
||||
87
dash/staging/post-jailbreak-root/dashboard/launch-from-kual.sh
Executable file
87
dash/staging/post-jailbreak-root/dashboard/launch-from-kual.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
LOG_FILE="$DIR/logs/kual-launch.log"
|
||||
PID_FILE="$DIR/logs/kual-launch.pid"
|
||||
# overlay 模式下,用户期望从 KUAL 点击后几乎直接进入 dashboard。
|
||||
# 这里仍然要求 KUAL 先正常退出,但默认不再额外人为等待,
|
||||
# 只在需要回退到更保守的切换节奏时,再通过环境变量显式加回延迟。
|
||||
LAUNCH_DELAY_SECONDS="${KUAL_LAUNCH_DELAY_SECONDS:-0}"
|
||||
KUAL_QUIT_GRACE_SECONDS="${KUAL_QUIT_GRACE_SECONDS:-0}"
|
||||
KUAL_APP_ID="${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
case "$LAUNCH_DELAY_SECONDS" in
|
||||
''|*[!0-9]*)
|
||||
LAUNCH_DELAY_SECONDS=0
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$KUAL_QUIT_GRACE_SECONDS" in
|
||||
''|*[!0-9]*)
|
||||
KUAL_QUIT_GRACE_SECONDS=0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
old_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||
if [ -n "${old_pid:-}" ] && kill -0 "$old_pid" 2>/dev/null; then
|
||||
kill "$old_pid" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# KUAL 直接同步执行 start.sh 时,framework/KUAL 正在切换的那一瞬间,
|
||||
# 后台 dash 进程仍可能留在同一个 session 里被一起 TERM 掉。
|
||||
# 这里仍然先让独立 session 请求 KUAL 正常退出,但默认不再故意停留在原生首页,
|
||||
# 而是把启动等待压到 0,尽量直接切到 dashboard。
|
||||
if command -v setsid >/dev/null 2>&1; then
|
||||
nohup setsid /bin/sh -c '
|
||||
delay=$1
|
||||
target_dir=$2
|
||||
kual_app_id=$3
|
||||
quit_grace=$4
|
||||
|
||||
# 复用 KUAL 自己的 appmgrd stop 路径,尽量让它先干净退回原生 UI。
|
||||
if command -v lipc-set-prop >/dev/null 2>&1; then
|
||||
lipc-set-prop com.lab126.appmgrd stop "$kual_app_id" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ "$quit_grace" -gt 0 ] 2>/dev/null; then
|
||||
sleep "$quit_grace"
|
||||
fi
|
||||
|
||||
if [ "$delay" -gt 0 ] 2>/dev/null; then
|
||||
sleep "$delay"
|
||||
fi
|
||||
|
||||
exec "$target_dir/start.sh"
|
||||
' sh "$LAUNCH_DELAY_SECONDS" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||
else
|
||||
nohup /bin/sh -c '
|
||||
delay=$1
|
||||
target_dir=$2
|
||||
kual_app_id=$3
|
||||
quit_grace=$4
|
||||
|
||||
# 复用 KUAL 自己的 appmgrd stop 路径,尽量让它先干净退回原生 UI。
|
||||
if command -v lipc-set-prop >/dev/null 2>&1; then
|
||||
lipc-set-prop com.lab126.appmgrd stop "$kual_app_id" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ "$quit_grace" -gt 0 ] 2>/dev/null; then
|
||||
sleep "$quit_grace"
|
||||
fi
|
||||
|
||||
if [ "$delay" -gt 0 ] 2>/dev/null; then
|
||||
sleep "$delay"
|
||||
fi
|
||||
|
||||
exec "$target_dir/start.sh"
|
||||
' sh "$LAUNCH_DELAY_SECONDS" "$DIR" "$KUAL_APP_ID" "$KUAL_QUIT_GRACE_SECONDS" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||
fi
|
||||
|
||||
echo "$!" >"$PID_FILE"
|
||||
echo "$(date 2>/dev/null || true) requested KUAL quit, scheduled dashboard launch with quit_grace=${KUAL_QUIT_GRACE_SECONDS}s launch_delay=${LAUNCH_DELAY_SECONDS}s" >>"$LOG_FILE"
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
SWITCH_THEME_CMD="$DIR/switch-theme.sh"
|
||||
LAUNCH_FROM_KUAL_CMD="$DIR/launch-from-kual.sh"
|
||||
|
||||
requested_theme_id=${1:?"usage: launch-theme-from-kual.sh <theme-id> [orientation]"}
|
||||
requested_orientation=${2:-}
|
||||
|
||||
# KUAL 里的主题入口先切主题,再复用现有的 launch-from-kual 启动链。
|
||||
# 这样可以保留当前已经收敛过的 KUAL 退出与 detached 启动逻辑,
|
||||
# 同时把“选主题”前移到进入 dashboard 之前。
|
||||
if [ -n "$requested_orientation" ]; then
|
||||
"$SWITCH_THEME_CMD" "$requested_theme_id" "$requested_orientation"
|
||||
else
|
||||
"$SWITCH_THEME_CMD" "$requested_theme_id"
|
||||
fi
|
||||
|
||||
exec "$LAUNCH_FROM_KUAL_CMD"
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
to_decimal() {
|
||||
printf '%s' "$1" | awk '{
|
||||
sub(/^0+/, "", $0)
|
||||
if ($0 == "") {
|
||||
$0 = 0
|
||||
}
|
||||
print $0
|
||||
}'
|
||||
}
|
||||
|
||||
clock_values_from_offset() {
|
||||
epoch_seconds=$(date '+%s')
|
||||
offset_minutes=$1
|
||||
|
||||
# 这里用 Lua 直接按 Unix 时间戳加偏移换算时分,
|
||||
# 避免 Kindle 上的 BusyBox date 对 Asia/Shanghai 这类时区名支持不完整。
|
||||
lua - "$epoch_seconds" "$offset_minutes" <<'LUA'
|
||||
local epoch_seconds = assert(tonumber(arg[1]), "missing epoch seconds")
|
||||
local offset_minutes = assert(tonumber(arg[2]), "missing offset minutes")
|
||||
local seconds_per_day = 24 * 60 * 60
|
||||
local local_seconds = epoch_seconds + offset_minutes * 60
|
||||
local seconds_of_day = ((local_seconds % seconds_per_day) + seconds_per_day) % seconds_per_day
|
||||
local hour = math.floor(seconds_of_day / 3600)
|
||||
local minute = math.floor((seconds_of_day % 3600) / 60)
|
||||
|
||||
io.write(string.format("%02d %02d\n", hour, minute))
|
||||
LUA
|
||||
}
|
||||
|
||||
current_clock_values() {
|
||||
# 表盘显示优先走固定 UTC 偏移,避免依赖设备对时区字符串的支持。
|
||||
# 这样 next-wakeup 仍可继续使用 TIMEZONE=Asia/Shanghai 做 cron 计算,
|
||||
# 表盘则稳定显示北京时间。
|
||||
if [ -n "${CLOCK_TIME_OFFSET_MINUTES:-}" ]; then
|
||||
clock_values_from_offset "$CLOCK_TIME_OFFSET_MINUTES"
|
||||
return
|
||||
fi
|
||||
|
||||
# 如果没有单独配置偏移,再回退到传统的 TZ=date 方案,兼容历史配置。
|
||||
if [ -n "${TIMEZONE:-}" ]; then
|
||||
if timezone_clock=$(TZ="$TIMEZONE" date '+%H %M' 2>/dev/null); then
|
||||
printf '%s\n' "$timezone_clock"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# 最后才回退到系统默认时区,至少保证脚本仍能继续工作。
|
||||
date '+%H %M'
|
||||
}
|
||||
|
||||
if [ "$#" -ge 2 ]; then
|
||||
hour_value=$1
|
||||
minute_value=$2
|
||||
else
|
||||
set -- $(current_clock_values)
|
||||
hour_value=$1
|
||||
minute_value=$2
|
||||
fi
|
||||
|
||||
hour_decimal=$(to_decimal "$hour_value")
|
||||
minute_decimal=$(to_decimal "$minute_value")
|
||||
hour_index=$(( (hour_decimal % 12) * 60 + minute_decimal ))
|
||||
|
||||
printf 'hour=%s\n' "$hour_decimal"
|
||||
printf 'minute=%s\n' "$minute_decimal"
|
||||
printf 'hour_index=%03d\n' "$hour_index"
|
||||
printf 'minute_index=%02d\n' "$minute_decimal"
|
||||
@@ -4,7 +4,89 @@
|
||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||
# 调度计算依赖 next-wakeup 这个 Rust 程序,它要求使用 IANA 时区名。
|
||||
# 这里必须保留 Asia/Shanghai,才能正确计算下一次唤醒时间。
|
||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||
# Kindle 上的 BusyBox date 对 IANA 时区名支持不稳定,直接拿 TIMEZONE 画表盘会退回 UTC。
|
||||
# 北京时间全年固定为 UTC+8,没有夏令时,所以表盘单独走固定偏移,避免依赖系统时区解析。
|
||||
export CLOCK_TIME_OFFSET_MINUTES=${CLOCK_TIME_OFFSET_MINUTES:-480}
|
||||
export BACKGROUND_URL=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"}
|
||||
export THEMES_INDEX_URL=${THEMES_INDEX_URL:-"https://shell.biboer.cn:20001/themes.json"}
|
||||
export THEMES_INDEX_REFRESH_INTERVAL_MINUTES=${THEMES_INDEX_REFRESH_INTERVAL_MINUTES:-1440}
|
||||
export THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440}
|
||||
export THEME_ID=${THEME_ID:-"default"}
|
||||
export ORIENTATION=${ORIENTATION:-"portrait"}
|
||||
export BACKGROUND_REFRESH_INTERVAL_MINUTES=${BACKGROUND_REFRESH_INTERVAL_MINUTES:-120}
|
||||
export CLOCK_REGION_X=${CLOCK_REGION_X:-347}
|
||||
export CLOCK_REGION_Y=${CLOCK_REGION_Y:-55}
|
||||
export CLOCK_REGION_WIDTH=${CLOCK_REGION_WIDTH:-220}
|
||||
export CLOCK_REGION_HEIGHT=${CLOCK_REGION_HEIGHT:-220}
|
||||
export CLOCK_FULL_REFRESH_INTERVAL_MINUTES=${CLOCK_FULL_REFRESH_INTERVAL_MINUTES:-15}
|
||||
|
||||
# 本机时钟外观参数:
|
||||
# 页面改版导致时钟区域尺寸变化时,通常只需要改 CLOCK_REGION_*,
|
||||
# 这组比例参数会随新的宽高自动缩放。
|
||||
# 如果只是想微调指针长短、粗细或刻度长度,再改下面这些值即可,不用改 Lua 代码。
|
||||
export CLOCK_FACE_RADIUS_RATIO=${CLOCK_FACE_RADIUS_RATIO:-0.47}
|
||||
export CLOCK_FACE_STROKE=${CLOCK_FACE_STROKE:-3}
|
||||
export CLOCK_TICK_OUTER_INSET=${CLOCK_TICK_OUTER_INSET:-6}
|
||||
export CLOCK_MAJOR_TICK_LENGTH=${CLOCK_MAJOR_TICK_LENGTH:-14}
|
||||
export CLOCK_MINOR_TICK_LENGTH=${CLOCK_MINOR_TICK_LENGTH:-7}
|
||||
export CLOCK_MAJOR_TICK_THICKNESS=${CLOCK_MAJOR_TICK_THICKNESS:-4}
|
||||
export CLOCK_MINOR_TICK_THICKNESS=${CLOCK_MINOR_TICK_THICKNESS:-2}
|
||||
export CLOCK_HOUR_LENGTH_RATIO=${CLOCK_HOUR_LENGTH_RATIO:-0.48}
|
||||
export CLOCK_MINUTE_LENGTH_RATIO=${CLOCK_MINUTE_LENGTH_RATIO:-0.72}
|
||||
export CLOCK_HOUR_THICKNESS=${CLOCK_HOUR_THICKNESS:-9}
|
||||
export CLOCK_MINUTE_THICKNESS=${CLOCK_MINUTE_THICKNESS:-5}
|
||||
export CLOCK_CENTER_RADIUS=${CLOCK_CENTER_RADIUS:-7}
|
||||
|
||||
# 进入 rtc suspend 前预留的可中断窗口,方便在调试时及时停止进程。
|
||||
# 这段时间会从真正的休眠时长里扣掉,避免分钟刷新慢一拍。
|
||||
export PRE_SLEEP_GRACE_SECONDS=${PRE_SLEEP_GRACE_SECONDS:-10}
|
||||
# 手动短按电源键把 Kindle 提前唤醒后,额外保持前台显示的秒数。
|
||||
# 这样用户有足够时间看屏、切主题或继续交互,而不会立刻再次休眠。
|
||||
export MANUAL_WAKE_KEEP_AWAKE_SECONDS=${MANUAL_WAKE_KEEP_AWAKE_SECONDS:-60}
|
||||
# 如果实际休眠时长比计划值至少少这么多秒,就认为是被用户手动提前唤醒。
|
||||
export MANUAL_WAKE_EARLY_TOLERANCE_SECONDS=${MANUAL_WAKE_EARLY_TOLERANCE_SECONDS:-5}
|
||||
|
||||
# Voyage 顶部状态栏遮罩:用于压住系统偶尔重画出来的时间、Wi-Fi、电池图标。
|
||||
# 当前坐标只覆盖页面顶部空白带,不会擦到天气卡上边框。
|
||||
export STATUS_MASK_ENABLED=${STATUS_MASK_ENABLED:-true}
|
||||
export STATUS_MASK_LEFT=${STATUS_MASK_LEFT:-700}
|
||||
export STATUS_MASK_TOP=${STATUS_MASK_TOP:-0}
|
||||
export STATUS_MASK_WIDTH=${STATUS_MASK_WIDTH:-372}
|
||||
export STATUS_MASK_HEIGHT=${STATUS_MASK_HEIGHT:-24}
|
||||
export STATUS_MASK_PASSES=${STATUS_MASK_PASSES:-3}
|
||||
export STATUS_MASK_DELAY_SECONDS=${STATUS_MASK_DELAY_SECONDS:-1}
|
||||
|
||||
# 运行态主题菜单当前默认关闭,主题切换统一收口到 KUAL 入口。
|
||||
# 如需恢复双翻页键菜单,可临时改回 true 做专项验证。
|
||||
export THEME_MENU_ENABLED=${THEME_MENU_ENABLED:-false}
|
||||
export THEME_MENU_EVENT_DEVICE=${THEME_MENU_EVENT_DEVICE:-"/dev/input/event2"}
|
||||
export THEME_MENU_COMBO_WINDOW_SECONDS=${THEME_MENU_COMBO_WINDOW_SECONDS:-0.35}
|
||||
# `/mnt/us` 是 FAT 分区,不能创建 FIFO;菜单监听器的临时管道必须放在 `/tmp`。
|
||||
export THEME_MENU_RUNTIME_DIR=${THEME_MENU_RUNTIME_DIR:-"/tmp/kindle-dash-theme-menu"}
|
||||
|
||||
# 默认模式:保留 Kindle 原生 UI 栈,让 dashboard 只负责画面覆盖与休眠调度。
|
||||
# 这条路线已经在 Voyage 5.13.6 上实机验证通过:
|
||||
# calendar -> 主页 -> KUAL -> 回主页 可正常工作。
|
||||
export KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-true}
|
||||
|
||||
# 从 KUAL 进入 dashboard 时,默认不再刻意停留在原生首页。
|
||||
# 如果某台机器仍需要更保守的切换节奏,可把这两个值临时调大后再做回归验证。
|
||||
export KUAL_QUIT_GRACE_SECONDS=${KUAL_QUIT_GRACE_SECONDS:-0}
|
||||
export KUAL_LAUNCH_DELAY_SECONDS=${KUAL_LAUNCH_DELAY_SECONDS:-0}
|
||||
|
||||
# 右下角长按热区当前默认关闭,避免和底层原生桌面的触摸响应冲突。
|
||||
# 如需恢复这条实验入口,可临时改回 true,但当前默认只保留 KUAL 主题入口。
|
||||
export TOUCH_HOME_ENABLED=${TOUCH_HOME_ENABLED:-false}
|
||||
export TOUCH_HOME_EVENT_DEVICE=${TOUCH_HOME_EVENT_DEVICE:-"/dev/input/event1"}
|
||||
export TOUCH_HOME_RUNTIME_DIR=${TOUCH_HOME_RUNTIME_DIR:-"/tmp/kindle-dash-touch-home"}
|
||||
export TOUCH_HOME_SCREEN_WIDTH=${TOUCH_HOME_SCREEN_WIDTH:-1072}
|
||||
export TOUCH_HOME_SCREEN_HEIGHT=${TOUCH_HOME_SCREEN_HEIGHT:-1448}
|
||||
export TOUCH_HOME_HOTSPOT_WIDTH_RATIO=${TOUCH_HOME_HOTSPOT_WIDTH_RATIO:-0.18}
|
||||
export TOUCH_HOME_HOTSPOT_HEIGHT_RATIO=${TOUCH_HOME_HOTSPOT_HEIGHT_RATIO:-0.18}
|
||||
export TOUCH_HOME_LONG_PRESS_SECONDS=${TOUCH_HOME_LONG_PRESS_SECONDS:-1.2}
|
||||
|
||||
# By default, partial screen updates are used to update the screen,
|
||||
# to prevent the screen from flashing. After a few partial updates,
|
||||
@@ -13,6 +95,11 @@ export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
||||
export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
|
||||
# 调试开关:设为 true 后,主循环仍会按计划拉图和刷新屏幕,
|
||||
# 不会进入 sleeping.png 分支,也不会把 Kindle 写入 /sys/power/state 进入系统挂起。
|
||||
# 适合通过 KUAL 或普通 start.sh 连续观察效果,调试结束后再改回 false。
|
||||
export DISABLE_SYSTEM_SUSPEND=${DISABLE_SYSTEM_SUSPEND:-false}
|
||||
|
||||
# When the time until the next wakeup is greater or equal to this number,
|
||||
# the dashboard will not be refreshed anymore, but instead show a
|
||||
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
#!/usr/bin/env sh
|
||||
# Fetch a new dashboard image, make sure to output it to "$1".
|
||||
# For example:
|
||||
"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png
|
||||
set -eu
|
||||
|
||||
# 拉取低频背景图,调用方负责传入输出路径。
|
||||
output_path=${1:?"missing output path"}
|
||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="$DIR/env.sh"
|
||||
THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
||||
|
||||
# fetch-dashboard 既会被 dash.sh 调,也会被 switch-theme.sh 单独调。
|
||||
# 因此这里每次都重新读取一次运行时主题配置,确保拿到当前背景地址。
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE"
|
||||
|
||||
background_path=${BACKGROUND_PATH:-""}
|
||||
background_url=${BACKGROUND_URL:-"https://shell.biboer.cn:20001/kindlebg.png"}
|
||||
local_background_path=""
|
||||
|
||||
if [ -n "$background_path" ]; then
|
||||
local_background_path="$DIR/../$background_path"
|
||||
fi
|
||||
|
||||
# 主题背景如果已经随本地部署同步到 Kindle,优先直接拷贝本地文件,
|
||||
# 这样切换主题时不依赖远端图片资源是否已经发布完成。
|
||||
if [ -n "$local_background_path" ] && [ -f "$local_background_path" ]; then
|
||||
cp "$local_background_path" "$output_path"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"$DIR/../xh" -d -q -o "$output_path" get "$background_url"
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
-- 在 Kindle 本机生成一张时钟区域位图,然后交给 fbink 刷到屏幕。
|
||||
-- 这里不再依赖透明 PNG 叠图,避免 eips 处理 alpha 时出现整块发黑的问题。
|
||||
|
||||
local output_path = assert(arg[1], "missing output path")
|
||||
local width = tonumber((assert(arg[2], "missing width")))
|
||||
local height = tonumber((assert(arg[3], "missing height")))
|
||||
local hour_value = tonumber((assert(arg[4], "missing hour")))
|
||||
local minute_value = tonumber((assert(arg[5], "missing minute")))
|
||||
|
||||
local function number_arg(index, fallback)
|
||||
local value = arg[index]
|
||||
if value == nil or value == "" then
|
||||
return fallback
|
||||
end
|
||||
|
||||
local numeric = tonumber(value)
|
||||
if numeric == nil then
|
||||
return fallback
|
||||
end
|
||||
|
||||
return numeric
|
||||
end
|
||||
|
||||
local WHITE = 255
|
||||
local BLACK = 0
|
||||
local cx = (width - 1) / 2
|
||||
local cy = (height - 1) / 2
|
||||
-- 偶数尺寸下如果半径直接顶到边界,右侧和下侧会更容易出现裁切感。
|
||||
-- 这里把描边厚度的一半让给外圈,保持四边可视留白更均匀。
|
||||
local face_stroke = number_arg(7, 3)
|
||||
local face_radius = math.max(1, math.min(width, height) * number_arg(6, 0.47) - face_stroke / 2)
|
||||
local major_tick_outer_inset = number_arg(8, 6)
|
||||
local minor_tick_outer_inset = number_arg(9, major_tick_outer_inset)
|
||||
local major_tick_length = number_arg(10, 14)
|
||||
local minor_tick_length = number_arg(11, 7)
|
||||
local major_tick_thickness = number_arg(12, 4)
|
||||
local minor_tick_thickness = number_arg(13, 2)
|
||||
local hour_length_ratio = number_arg(14, 0.48)
|
||||
local hour_back_length_ratio = number_arg(15, 0)
|
||||
local minute_length_ratio = number_arg(16, 0.72)
|
||||
local minute_back_length_ratio = number_arg(17, 0)
|
||||
local hour_thickness = number_arg(18, 9)
|
||||
local minute_thickness = number_arg(19, 5)
|
||||
local center_radius = number_arg(20, 7)
|
||||
local rotation_degrees = number_arg(21, 0)
|
||||
local rotation_radians = math.rad(rotation_degrees)
|
||||
|
||||
local pixels = {}
|
||||
for index = 1, width * height do
|
||||
pixels[index] = WHITE
|
||||
end
|
||||
|
||||
local function pixel_index(x, y)
|
||||
return y * width + x + 1
|
||||
end
|
||||
|
||||
local function set_pixel(x, y, value)
|
||||
if x < 0 or y < 0 or x >= width or y >= height then
|
||||
return
|
||||
end
|
||||
|
||||
pixels[pixel_index(x, y)] = value
|
||||
end
|
||||
|
||||
local function fill_disk(x, y, radius, value)
|
||||
local r2 = radius * radius
|
||||
local min_x = math.floor(x - radius)
|
||||
local max_x = math.ceil(x + radius)
|
||||
local min_y = math.floor(y - radius)
|
||||
local max_y = math.ceil(y + radius)
|
||||
|
||||
for py = min_y, max_y do
|
||||
for px = min_x, max_x do
|
||||
local dx = px - x
|
||||
local dy = py - y
|
||||
if dx * dx + dy * dy <= r2 then
|
||||
set_pixel(px, py, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function draw_circle(radius, thickness, value)
|
||||
local samples = 720
|
||||
for step = 0, samples - 1 do
|
||||
local angle = (step / samples) * math.pi * 2
|
||||
local x = cx + math.cos(angle) * radius
|
||||
local y = cy + math.sin(angle) * radius
|
||||
fill_disk(x, y, thickness / 2, value)
|
||||
end
|
||||
end
|
||||
|
||||
local function fill_bar(origin_x, origin_y, angle, start_length, end_length, thickness, value)
|
||||
local start_pos = math.min(start_length, end_length)
|
||||
local end_pos = math.max(start_length, end_length)
|
||||
local ux = math.cos(angle)
|
||||
local uy = math.sin(angle)
|
||||
local vx = -uy
|
||||
local vy = ux
|
||||
local half_thickness = thickness / 2
|
||||
local corners = {
|
||||
{
|
||||
x = origin_x + ux * start_pos + vx * half_thickness,
|
||||
y = origin_y + uy * start_pos + vy * half_thickness,
|
||||
},
|
||||
{
|
||||
x = origin_x + ux * start_pos - vx * half_thickness,
|
||||
y = origin_y + uy * start_pos - vy * half_thickness,
|
||||
},
|
||||
{
|
||||
x = origin_x + ux * end_pos + vx * half_thickness,
|
||||
y = origin_y + uy * end_pos + vy * half_thickness,
|
||||
},
|
||||
{
|
||||
x = origin_x + ux * end_pos - vx * half_thickness,
|
||||
y = origin_y + uy * end_pos - vy * half_thickness,
|
||||
},
|
||||
}
|
||||
|
||||
local min_x = math.floor(math.min(corners[1].x, corners[2].x, corners[3].x, corners[4].x))
|
||||
local max_x = math.ceil(math.max(corners[1].x, corners[2].x, corners[3].x, corners[4].x))
|
||||
local min_y = math.floor(math.min(corners[1].y, corners[2].y, corners[3].y, corners[4].y))
|
||||
local max_y = math.ceil(math.max(corners[1].y, corners[2].y, corners[3].y, corners[4].y))
|
||||
|
||||
for py = min_y, max_y do
|
||||
for px = min_x, max_x do
|
||||
local sample_x = px + 0.5
|
||||
local sample_y = py + 0.5
|
||||
local dx = sample_x - origin_x
|
||||
local dy = sample_y - origin_y
|
||||
local longitudinal = dx * ux + dy * uy
|
||||
local lateral = dx * vx + dy * vy
|
||||
|
||||
if longitudinal >= start_pos and longitudinal <= end_pos and math.abs(lateral) <= half_thickness then
|
||||
set_pixel(px, py, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function draw_ticks()
|
||||
for tick = 0, 59 do
|
||||
local is_major = tick % 5 == 0
|
||||
local angle = (tick / 60) * math.pi * 2 - math.pi / 2 + rotation_radians
|
||||
local outer = face_radius - (is_major and major_tick_outer_inset or minor_tick_outer_inset)
|
||||
local inner = outer - (is_major and major_tick_length or minor_tick_length)
|
||||
fill_bar(cx, cy, angle, inner, outer, is_major and major_tick_thickness or minor_tick_thickness, BLACK)
|
||||
end
|
||||
end
|
||||
|
||||
local function draw_hands()
|
||||
local hour_angle = (hour_value % 12) * 30 + minute_value * 0.5
|
||||
local minute_angle = minute_value * 6
|
||||
local hour_radians = math.rad(hour_angle + rotation_degrees - 90)
|
||||
local minute_radians = math.rad(minute_angle + rotation_degrees - 90)
|
||||
|
||||
fill_bar(cx, cy, hour_radians, -face_radius * hour_back_length_ratio, face_radius * hour_length_ratio, hour_thickness, BLACK)
|
||||
fill_bar(
|
||||
cx,
|
||||
cy,
|
||||
minute_radians,
|
||||
-face_radius * minute_back_length_ratio,
|
||||
face_radius * minute_length_ratio,
|
||||
minute_thickness,
|
||||
BLACK
|
||||
)
|
||||
fill_disk(cx, cy, center_radius, BLACK)
|
||||
end
|
||||
|
||||
draw_circle(face_radius, face_stroke, BLACK)
|
||||
draw_ticks()
|
||||
draw_hands()
|
||||
|
||||
local file = assert(io.open(output_path, "wb"))
|
||||
file:write(string.format("P5\n%d %d\n255\n", width, height))
|
||||
|
||||
local char_cache = {
|
||||
[WHITE] = string.char(WHITE),
|
||||
[BLACK] = string.char(BLACK),
|
||||
}
|
||||
|
||||
for y = 0, height - 1 do
|
||||
local row = {}
|
||||
for x = 0, width - 1 do
|
||||
local value = pixels[pixel_index(x, y)]
|
||||
row[#row + 1] = char_cache[value] or string.char(value)
|
||||
end
|
||||
file:write(table.concat(row))
|
||||
end
|
||||
|
||||
file:close()
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="$DIR/env.sh"
|
||||
THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
||||
|
||||
# 单独执行本脚本时,也需要读取同一份坐标配置。
|
||||
# 否则会退回到脚本内默认值,导致手工调试与主循环绘制位置不一致。
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
# 主题切换后,时钟区域坐标和绘制参数会落在运行时 env 里。
|
||||
# 这里额外覆盖一次,保证分钟级重绘与最近一次主题配置保持一致。
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE"
|
||||
|
||||
clock_region_x=${CLOCK_REGION_X:-262}
|
||||
clock_region_y=${CLOCK_REGION_Y:-55}
|
||||
clock_region_width=${CLOCK_REGION_WIDTH:-220}
|
||||
clock_region_height=${CLOCK_REGION_HEIGHT:-220}
|
||||
clock_face_radius_ratio=${CLOCK_FACE_RADIUS_RATIO:-0.47}
|
||||
clock_face_stroke=${CLOCK_FACE_STROKE:-3}
|
||||
clock_tick_outer_inset=${CLOCK_TICK_OUTER_INSET:-6}
|
||||
clock_major_tick_outer_inset=${CLOCK_MAJOR_TICK_OUTER_INSET:-$clock_tick_outer_inset}
|
||||
clock_minor_tick_outer_inset=${CLOCK_MINOR_TICK_OUTER_INSET:-$clock_tick_outer_inset}
|
||||
clock_major_tick_length=${CLOCK_MAJOR_TICK_LENGTH:-14}
|
||||
clock_minor_tick_length=${CLOCK_MINOR_TICK_LENGTH:-7}
|
||||
clock_major_tick_thickness=${CLOCK_MAJOR_TICK_THICKNESS:-4}
|
||||
clock_minor_tick_thickness=${CLOCK_MINOR_TICK_THICKNESS:-2}
|
||||
clock_hour_length_ratio=${CLOCK_HOUR_LENGTH_RATIO:-0.48}
|
||||
clock_hour_back_length_ratio=${CLOCK_HOUR_BACK_LENGTH_RATIO:-0}
|
||||
clock_minute_length_ratio=${CLOCK_MINUTE_LENGTH_RATIO:-0.72}
|
||||
clock_minute_back_length_ratio=${CLOCK_MINUTE_BACK_LENGTH_RATIO:-0}
|
||||
clock_hour_thickness=${CLOCK_HOUR_THICKNESS:-9}
|
||||
clock_minute_thickness=${CLOCK_MINUTE_THICKNESS:-5}
|
||||
clock_center_radius=${CLOCK_CENTER_RADIUS:-7}
|
||||
clock_rotation_degrees=${CLOCK_ROTATION_DEGREES:-0}
|
||||
force_full_refresh=${1:-false}
|
||||
output_path="$DIR/state/clock-render.pgm"
|
||||
|
||||
eval "$("$DIR/clock-index.sh")"
|
||||
|
||||
mkdir -p "$DIR/state"
|
||||
|
||||
lua "$DIR/render-clock.lua" \
|
||||
"$output_path" \
|
||||
"$clock_region_width" \
|
||||
"$clock_region_height" \
|
||||
"$hour" \
|
||||
"$minute" \
|
||||
"$clock_face_radius_ratio" \
|
||||
"$clock_face_stroke" \
|
||||
"$clock_major_tick_outer_inset" \
|
||||
"$clock_minor_tick_outer_inset" \
|
||||
"$clock_major_tick_length" \
|
||||
"$clock_minor_tick_length" \
|
||||
"$clock_major_tick_thickness" \
|
||||
"$clock_minor_tick_thickness" \
|
||||
"$clock_hour_length_ratio" \
|
||||
"$clock_hour_back_length_ratio" \
|
||||
"$clock_minute_length_ratio" \
|
||||
"$clock_minute_back_length_ratio" \
|
||||
"$clock_hour_thickness" \
|
||||
"$clock_minute_thickness" \
|
||||
"$clock_center_radius" \
|
||||
"$clock_rotation_degrees"
|
||||
|
||||
if [ "$force_full_refresh" = true ]; then
|
||||
# Kindle Voyage 当前这条链路里,fbink 默认会叠加 viewport 修正,
|
||||
# 导致图像在屏幕上出现双重偏移。这里强制关闭 viewport 修正,
|
||||
# 让坐标与网页导出的像素坐标保持一致。
|
||||
fbink -q -V -f -g "file=$output_path,x=$clock_region_x,y=$clock_region_y"
|
||||
else
|
||||
fbink -q -V -g "file=$output_path,x=$clock_region_x,y=$clock_region_y"
|
||||
fi
|
||||
343
dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua
Normal file
343
dash/staging/post-jailbreak-root/dashboard/local/theme-json.lua
Normal file
@@ -0,0 +1,343 @@
|
||||
-- 读取 calendar 产出的主题 JSON,并把 Kindle 侧真正需要的字段提取出来。
|
||||
-- 这里实现一个精简 JSON 解析器,避免在设备上额外依赖 jq / python。
|
||||
|
||||
local command = assert(arg[1], "missing command")
|
||||
|
||||
local function read_file(path)
|
||||
local file = assert(io.open(path, "rb"))
|
||||
local content = file:read("*a")
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function decode(json)
|
||||
local position = 1
|
||||
local length = #json
|
||||
|
||||
local function skip_whitespace()
|
||||
while position <= length do
|
||||
local ch = json:sub(position, position)
|
||||
if ch ~= " " and ch ~= "\n" and ch ~= "\r" and ch ~= "\t" then
|
||||
break
|
||||
end
|
||||
position = position + 1
|
||||
end
|
||||
end
|
||||
|
||||
local parse_value
|
||||
|
||||
local function parse_string()
|
||||
position = position + 1
|
||||
local parts = {}
|
||||
|
||||
while position <= length do
|
||||
local ch = json:sub(position, position)
|
||||
|
||||
if ch == '"' then
|
||||
position = position + 1
|
||||
return table.concat(parts)
|
||||
end
|
||||
|
||||
if ch == "\\" then
|
||||
local escape = json:sub(position + 1, position + 1)
|
||||
local replacements = {
|
||||
['"'] = '"',
|
||||
["\\"] = "\\",
|
||||
["/"] = "/",
|
||||
["b"] = "\b",
|
||||
["f"] = "\f",
|
||||
["n"] = "\n",
|
||||
["r"] = "\r",
|
||||
["t"] = "\t",
|
||||
}
|
||||
|
||||
if escape == "u" then
|
||||
error("unsupported unicode escape")
|
||||
end
|
||||
|
||||
local replacement = replacements[escape]
|
||||
if not replacement then
|
||||
error("invalid string escape")
|
||||
end
|
||||
|
||||
parts[#parts + 1] = replacement
|
||||
position = position + 2
|
||||
else
|
||||
parts[#parts + 1] = ch
|
||||
position = position + 1
|
||||
end
|
||||
end
|
||||
|
||||
error("unterminated string")
|
||||
end
|
||||
|
||||
local function parse_number()
|
||||
local start_pos = position
|
||||
|
||||
while position <= length do
|
||||
local ch = json:sub(position, position)
|
||||
if not ch:match("[%d%+%-%.eE]") then
|
||||
break
|
||||
end
|
||||
position = position + 1
|
||||
end
|
||||
|
||||
local value = tonumber(json:sub(start_pos, position - 1))
|
||||
if value == nil then
|
||||
error("invalid number")
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
local function parse_array()
|
||||
position = position + 1
|
||||
skip_whitespace()
|
||||
|
||||
local result = {}
|
||||
if json:sub(position, position) == "]" then
|
||||
position = position + 1
|
||||
return result
|
||||
end
|
||||
|
||||
while true do
|
||||
result[#result + 1] = parse_value()
|
||||
skip_whitespace()
|
||||
|
||||
local ch = json:sub(position, position)
|
||||
if ch == "]" then
|
||||
position = position + 1
|
||||
return result
|
||||
end
|
||||
|
||||
if ch ~= "," then
|
||||
error("invalid array separator")
|
||||
end
|
||||
|
||||
position = position + 1
|
||||
skip_whitespace()
|
||||
end
|
||||
end
|
||||
|
||||
local function parse_object()
|
||||
position = position + 1
|
||||
skip_whitespace()
|
||||
|
||||
local result = {}
|
||||
if json:sub(position, position) == "}" then
|
||||
position = position + 1
|
||||
return result
|
||||
end
|
||||
|
||||
while true do
|
||||
if json:sub(position, position) ~= '"' then
|
||||
error("object key must be string")
|
||||
end
|
||||
|
||||
local key = parse_string()
|
||||
skip_whitespace()
|
||||
|
||||
if json:sub(position, position) ~= ":" then
|
||||
error("missing object colon")
|
||||
end
|
||||
|
||||
position = position + 1
|
||||
result[key] = parse_value()
|
||||
skip_whitespace()
|
||||
|
||||
local ch = json:sub(position, position)
|
||||
if ch == "}" then
|
||||
position = position + 1
|
||||
return result
|
||||
end
|
||||
|
||||
if ch ~= "," then
|
||||
error("invalid object separator")
|
||||
end
|
||||
|
||||
position = position + 1
|
||||
skip_whitespace()
|
||||
end
|
||||
end
|
||||
|
||||
function parse_value()
|
||||
skip_whitespace()
|
||||
|
||||
local ch = json:sub(position, position)
|
||||
if ch == "{" then
|
||||
return parse_object()
|
||||
end
|
||||
|
||||
if ch == "[" then
|
||||
return parse_array()
|
||||
end
|
||||
|
||||
if ch == '"' then
|
||||
return parse_string()
|
||||
end
|
||||
|
||||
if ch == "-" or ch:match("%d") then
|
||||
return parse_number()
|
||||
end
|
||||
|
||||
if json:sub(position, position + 3) == "true" then
|
||||
position = position + 4
|
||||
return true
|
||||
end
|
||||
|
||||
if json:sub(position, position + 4) == "false" then
|
||||
position = position + 5
|
||||
return false
|
||||
end
|
||||
|
||||
if json:sub(position, position + 3) == "null" then
|
||||
position = position + 4
|
||||
return nil
|
||||
end
|
||||
|
||||
error("unexpected token")
|
||||
end
|
||||
|
||||
local result = parse_value()
|
||||
skip_whitespace()
|
||||
|
||||
if position <= length then
|
||||
error("unexpected trailing content")
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function find_theme(index_data, theme_id)
|
||||
for _, theme in ipairs(index_data.themes or {}) do
|
||||
if theme.id == theme_id then
|
||||
return theme
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function orientation_exists(theme, orientation)
|
||||
for _, value in ipairs(theme.orientations or {}) do
|
||||
if value == orientation then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function first_orientation(theme, fallback)
|
||||
if orientation_exists(theme, fallback) then
|
||||
return fallback
|
||||
end
|
||||
|
||||
return (theme.orientations or {})[1] or fallback
|
||||
end
|
||||
|
||||
local function shell_quote(value)
|
||||
return "'" .. tostring(value):gsub("'", [['"'"']]) .. "'"
|
||||
end
|
||||
|
||||
local function write_runtime_env(path, theme_id, orientation, variant)
|
||||
local file = assert(io.open(path, "wb"))
|
||||
|
||||
local lines = {
|
||||
"export THEME_ID=" .. shell_quote(theme_id),
|
||||
"export ORIENTATION=" .. shell_quote(orientation),
|
||||
"export THEME_DEVICE_PLACEMENT=" .. shell_quote(variant.devicePlacement or ""),
|
||||
"export BACKGROUND_PATH=" .. shell_quote((variant.background and variant.background.path) or ""),
|
||||
"export BACKGROUND_URL=" .. shell_quote(assert(variant.background.url, "missing background url")),
|
||||
"export BACKGROUND_REFRESH_INTERVAL_MINUTES=" .. tostring(variant.background.refreshIntervalMinutes or 120),
|
||||
"export CLOCK_REGION_X=" .. tostring(assert(variant.clock.x, "missing clock x")),
|
||||
"export CLOCK_REGION_Y=" .. tostring(assert(variant.clock.y, "missing clock y")),
|
||||
"export CLOCK_REGION_WIDTH=" .. tostring(assert(variant.clock.width, "missing clock width")),
|
||||
"export CLOCK_REGION_HEIGHT=" .. tostring(assert(variant.clock.height, "missing clock height")),
|
||||
"export CLOCK_FACE_RADIUS_RATIO=" .. tostring(variant.clock.faceRadiusRatio or 0.47),
|
||||
"export CLOCK_FACE_STROKE=" .. tostring(variant.clock.faceStroke or 3),
|
||||
"export CLOCK_TICK_OUTER_INSET=" .. tostring(variant.clock.tickOuterInset or 6),
|
||||
"export CLOCK_MAJOR_TICK_OUTER_INSET=" .. tostring(variant.clock.majorTickOuterInset or variant.clock.tickOuterInset or 6),
|
||||
"export CLOCK_MINOR_TICK_OUTER_INSET=" .. tostring(variant.clock.minorTickOuterInset or variant.clock.tickOuterInset or 6),
|
||||
"export CLOCK_MAJOR_TICK_LENGTH=" .. tostring(variant.clock.majorTickLength or 14),
|
||||
"export CLOCK_MINOR_TICK_LENGTH=" .. tostring(variant.clock.minorTickLength or 7),
|
||||
"export CLOCK_MAJOR_TICK_THICKNESS=" .. tostring(variant.clock.majorTickThickness or 4),
|
||||
"export CLOCK_MINOR_TICK_THICKNESS=" .. tostring(variant.clock.minorTickThickness or 2),
|
||||
"export CLOCK_HOUR_LENGTH_RATIO=" .. tostring(variant.clock.hourLengthRatio or 0.48),
|
||||
"export CLOCK_HOUR_BACK_LENGTH_RATIO=" .. tostring(variant.clock.hourBackLengthRatio or 0),
|
||||
"export CLOCK_MINUTE_LENGTH_RATIO=" .. tostring(variant.clock.minuteLengthRatio or 0.72),
|
||||
"export CLOCK_MINUTE_BACK_LENGTH_RATIO=" .. tostring(variant.clock.minuteBackLengthRatio or 0),
|
||||
"export CLOCK_HOUR_THICKNESS=" .. tostring(variant.clock.hourThickness or 9),
|
||||
"export CLOCK_MINUTE_THICKNESS=" .. tostring(variant.clock.minuteThickness or 5),
|
||||
"export CLOCK_CENTER_RADIUS=" .. tostring(variant.clock.centerRadius or 7),
|
||||
"export CLOCK_ROTATION_DEGREES=" .. tostring(variant.clock.rotationDegrees or 0),
|
||||
}
|
||||
|
||||
file:write(table.concat(lines, "\n"))
|
||||
file:write("\n")
|
||||
file:close()
|
||||
end
|
||||
|
||||
if command == "resolve" then
|
||||
local index_path = assert(arg[2], "missing themes index path")
|
||||
local requested_theme_id = arg[3] or ""
|
||||
local requested_orientation = arg[4] or ""
|
||||
local index_data = decode(read_file(index_path))
|
||||
|
||||
local theme = find_theme(index_data, requested_theme_id)
|
||||
if not theme then
|
||||
theme = find_theme(index_data, index_data.defaultThemeId) or assert((index_data.themes or {})[1], "themes index empty")
|
||||
end
|
||||
|
||||
local orientation = requested_orientation
|
||||
if orientation == "" then
|
||||
orientation = index_data.defaultOrientation or "portrait"
|
||||
end
|
||||
orientation = first_orientation(theme, orientation)
|
||||
|
||||
io.write("THEME_ID=", theme.id, "\n")
|
||||
io.write("ORIENTATION=", orientation, "\n")
|
||||
io.write("CONFIG_URL=", assert(theme.configUrl, "missing config url"), "\n")
|
||||
return
|
||||
end
|
||||
|
||||
if command == "write-runtime" then
|
||||
local theme_path = assert(arg[2], "missing theme config path")
|
||||
local theme_id = assert(arg[3], "missing theme id")
|
||||
local requested_orientation = assert(arg[4], "missing orientation")
|
||||
local output_path = assert(arg[5], "missing output path")
|
||||
local theme_data = decode(read_file(theme_path))
|
||||
local variants = assert(theme_data.variants, "missing variants")
|
||||
local variant = variants[requested_orientation] or variants.portrait or variants.landscape
|
||||
|
||||
if not variant then
|
||||
error("missing usable variant")
|
||||
end
|
||||
|
||||
local resolved_orientation = requested_orientation
|
||||
if variants[requested_orientation] == nil then
|
||||
if variants.portrait ~= nil then
|
||||
resolved_orientation = "portrait"
|
||||
elseif variants.landscape ~= nil then
|
||||
resolved_orientation = "landscape"
|
||||
end
|
||||
end
|
||||
|
||||
write_runtime_env(output_path, theme_id, resolved_orientation, variant)
|
||||
io.write("THEME_ID=", theme_id, "\n")
|
||||
io.write("ORIENTATION=", resolved_orientation, "\n")
|
||||
return
|
||||
end
|
||||
|
||||
if command == "list" then
|
||||
local index_path = assert(arg[2], "missing themes index path")
|
||||
local index_data = decode(read_file(index_path))
|
||||
|
||||
for _, theme in ipairs(index_data.themes or {}) do
|
||||
io.write(theme.id or "", "\t")
|
||||
io.write(theme.label or theme.id or "", "\t")
|
||||
io.write(table.concat(theme.orientations or {}, ","), "\n")
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
error("unknown command")
|
||||
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="$DIR/env.sh"
|
||||
THEME_FILE="$DIR/theme.env"
|
||||
THEME_RUNTIME_ENV_FILE="$DIR/state/theme-runtime.env"
|
||||
THEMES_INDEX_PATH="$DIR/../themes.json"
|
||||
THEME_JSON_LUA="$DIR/theme-json.lua"
|
||||
SWITCH_THEME_CMD="$DIR/../switch-theme.sh"
|
||||
STOP_DASHBOARD_CMD="$DIR/../stop.sh"
|
||||
STATE_DIR="$DIR/state"
|
||||
MENU_ITEMS_FILE="$STATE_DIR/theme-menu-items.tsv"
|
||||
RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-theme-menu"
|
||||
EVENT_DEVICE_DEFAULT="/dev/input/event2"
|
||||
THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT="0.35"
|
||||
HOME_MENU_ITEM_ID="__return_home__"
|
||||
HOME_MENU_ITEM_LABEL="Return Home"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE"
|
||||
|
||||
EVENT_DEVICE=${THEME_MENU_EVENT_DEVICE:-$EVENT_DEVICE_DEFAULT}
|
||||
THEME_MENU_COMBO_WINDOW_SECONDS=${THEME_MENU_COMBO_WINDOW_SECONDS:-$THEME_MENU_COMBO_WINDOW_SECONDS_DEFAULT}
|
||||
RUNTIME_DIR=${THEME_MENU_RUNTIME_DIR:-$RUNTIME_DIR_DEFAULT}
|
||||
ACTION_FIFO="$RUNTIME_DIR/theme-menu-actions.fifo"
|
||||
PID_FILE="$RUNTIME_DIR/theme-menu-service.pid"
|
||||
|
||||
menu_open=false
|
||||
selected_index=1
|
||||
current_theme_id=${THEME_ID:-default}
|
||||
current_orientation=${ORIENTATION:-portrait}
|
||||
stream_pid=""
|
||||
owns_runtime_state=false
|
||||
|
||||
log_event() {
|
||||
# 调试阶段把状态机动作写进日志,便于确认按键是否真的被识别到。
|
||||
printf '%s theme-menu: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2
|
||||
}
|
||||
|
||||
service_running() {
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
existing_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||
if [ -z "$existing_pid" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
kill -0 "$existing_pid" 2>/dev/null
|
||||
}
|
||||
|
||||
send_action_to_service() {
|
||||
requested_action=$1
|
||||
attempts=0
|
||||
|
||||
if ! service_running; then
|
||||
printf 'theme-menu-service not running\n' >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# action fifo 由主监听循环持有;正常情况下会一直存在。
|
||||
# 这里保守重试几次,兼容监听器刚重启、fifo 正在重建的瞬间。
|
||||
while [ "$attempts" -lt 3 ]; do
|
||||
if [ -p "$ACTION_FIFO" ]; then
|
||||
printf '%s\n' "$requested_action" >"$ACTION_FIFO"
|
||||
return 0
|
||||
fi
|
||||
|
||||
attempts=$((attempts + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
printf 'theme-menu-service action fifo not ready\n' >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
load_runtime() {
|
||||
# 每次打开菜单前都重新读取当前主题和方向,避免显示过期状态。
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_RUNTIME_ENV_FILE" ] && . "$THEME_RUNTIME_ENV_FILE"
|
||||
|
||||
current_theme_id=${THEME_ID:-default}
|
||||
current_orientation=${ORIENTATION:-portrait}
|
||||
}
|
||||
|
||||
load_menu_items() {
|
||||
mkdir -p "$STATE_DIR"
|
||||
lua "$THEME_JSON_LUA" list "$THEMES_INDEX_PATH" >"$MENU_ITEMS_FILE"
|
||||
# 在主题列表末尾追加一个动作项,用于安全退出 dashboard 并恢复 Kindle 首页。
|
||||
printf '%s\t%s\n' "$HOME_MENU_ITEM_ID" "$HOME_MENU_ITEM_LABEL" >>"$MENU_ITEMS_FILE"
|
||||
}
|
||||
|
||||
theme_count() {
|
||||
awk 'END { print NR + 0 }' "$MENU_ITEMS_FILE"
|
||||
}
|
||||
|
||||
theme_field() {
|
||||
row=$1
|
||||
column=$2
|
||||
awk -F '\t' -v target_row="$row" -v target_column="$column" '
|
||||
NR == target_row {
|
||||
print $target_column
|
||||
exit
|
||||
}
|
||||
' "$MENU_ITEMS_FILE"
|
||||
}
|
||||
|
||||
current_theme_index() {
|
||||
awk -F '\t' -v current_theme="$current_theme_id" '
|
||||
$1 == current_theme {
|
||||
print NR
|
||||
found = 1
|
||||
exit
|
||||
}
|
||||
END {
|
||||
if (!found) {
|
||||
print 1
|
||||
}
|
||||
}
|
||||
' "$MENU_ITEMS_FILE"
|
||||
}
|
||||
|
||||
print_line() {
|
||||
col=$1
|
||||
row=$2
|
||||
text=$3
|
||||
/usr/sbin/eips "$col" "$row" "$text"
|
||||
}
|
||||
|
||||
render_menu() {
|
||||
total_themes=$(theme_count)
|
||||
current_label=$(theme_field "$selected_index" 2)
|
||||
|
||||
/usr/sbin/eips -c
|
||||
print_line 3 1 "Kindle Dashboard"
|
||||
print_line 3 3 "Theme Menu"
|
||||
print_line 3 5 "Orientation: $current_orientation"
|
||||
print_line 3 6 "Selected: $current_label"
|
||||
print_line 3 7 "--------------------------------"
|
||||
|
||||
row=9
|
||||
index=1
|
||||
while [ "$index" -le "$total_themes" ]; do
|
||||
theme_label=$(theme_field "$index" 2)
|
||||
theme_id=$(theme_field "$index" 1)
|
||||
prefix=" "
|
||||
line_text=""
|
||||
|
||||
if [ "$index" -eq "$selected_index" ]; then
|
||||
prefix="> "
|
||||
fi
|
||||
|
||||
if [ "$theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
||||
line_text="${prefix}${theme_label}"
|
||||
else
|
||||
line_text="${prefix}${theme_label} (${theme_id})"
|
||||
fi
|
||||
|
||||
print_line 3 "$row" "$line_text"
|
||||
row=$((row + 2))
|
||||
index=$((index + 1))
|
||||
done
|
||||
|
||||
print_line 3 18 "PageUp/PageDown: move"
|
||||
print_line 3 20 "Press both keys: select"
|
||||
}
|
||||
|
||||
wrap_index() {
|
||||
next_index=$1
|
||||
total_themes=$(theme_count)
|
||||
|
||||
if [ "$next_index" -lt 1 ]; then
|
||||
printf '%s\n' "$total_themes"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$next_index" -gt "$total_themes" ]; then
|
||||
printf '1\n'
|
||||
return
|
||||
fi
|
||||
|
||||
printf '%s\n' "$next_index"
|
||||
}
|
||||
|
||||
move_selection() {
|
||||
delta=$1
|
||||
selected_index=$(wrap_index $((selected_index + delta)))
|
||||
render_menu
|
||||
}
|
||||
|
||||
apply_selection() {
|
||||
selected_theme_id=$(theme_field "$selected_index" 1)
|
||||
|
||||
/usr/sbin/eips -c
|
||||
if [ "$selected_theme_id" = "$HOME_MENU_ITEM_ID" ]; then
|
||||
log_event "apply return_home"
|
||||
print_line 3 5 "Returning home..."
|
||||
print_line 3 7 "Restoring framework/webreader"
|
||||
menu_open=false
|
||||
"$STOP_DASHBOARD_CMD"
|
||||
return
|
||||
fi
|
||||
|
||||
log_event "apply theme=$selected_theme_id orientation=$current_orientation"
|
||||
print_line 3 5 "Applying theme..."
|
||||
print_line 3 7 "$selected_theme_id / $current_orientation"
|
||||
"$SWITCH_THEME_CMD" "$selected_theme_id" "$current_orientation"
|
||||
menu_open=false
|
||||
}
|
||||
|
||||
open_menu() {
|
||||
load_runtime
|
||||
load_menu_items
|
||||
selected_index=$(current_theme_index)
|
||||
menu_open=true
|
||||
log_event "open_menu theme=$current_theme_id orientation=$current_orientation selected_index=$selected_index"
|
||||
render_menu
|
||||
}
|
||||
|
||||
handle_action() {
|
||||
action=$1
|
||||
log_event "action=$action menu_open=$menu_open"
|
||||
|
||||
case "$action" in
|
||||
open_menu)
|
||||
if [ "$menu_open" = true ]; then
|
||||
# 触摸热区重复触发时,不直接确认当前选项,只重画当前菜单。
|
||||
render_menu
|
||||
else
|
||||
open_menu
|
||||
fi
|
||||
;;
|
||||
combo)
|
||||
if [ "$menu_open" = true ]; then
|
||||
apply_selection
|
||||
else
|
||||
open_menu
|
||||
fi
|
||||
;;
|
||||
pageup)
|
||||
if [ "$menu_open" = true ]; then
|
||||
move_selection -1
|
||||
fi
|
||||
;;
|
||||
pagedown)
|
||||
if [ "$menu_open" = true ]; then
|
||||
move_selection 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
action_stream() {
|
||||
evtest --grab "$EVENT_DEVICE" 2>/dev/null | awk -v combo_window="$THEME_MENU_COMBO_WINDOW_SECONDS" '
|
||||
function extract_time(line, match_count, time_value) {
|
||||
match_count = match(line, /time [0-9]+\.[0-9]+/)
|
||||
if (match_count == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
time_value = substr(line, RSTART + 5, RLENGTH - 5)
|
||||
return time_value + 0
|
||||
}
|
||||
|
||||
function emit(name) {
|
||||
print name
|
||||
fflush()
|
||||
}
|
||||
|
||||
{
|
||||
current_time = extract_time($0)
|
||||
|
||||
if ($0 ~ /code 104 \(PageUp\), value 1/) {
|
||||
if (pending_key == "down" && current_time - pending_time <= combo_window) {
|
||||
pending_key = ""
|
||||
emit("combo")
|
||||
next
|
||||
}
|
||||
|
||||
pending_key = "up"
|
||||
pending_time = current_time
|
||||
next
|
||||
}
|
||||
|
||||
if ($0 ~ /code 109 \(PageDown\), value 1/) {
|
||||
if (pending_key == "up" && current_time - pending_time <= combo_window) {
|
||||
pending_key = ""
|
||||
emit("combo")
|
||||
next
|
||||
}
|
||||
|
||||
pending_key = "down"
|
||||
pending_time = current_time
|
||||
next
|
||||
}
|
||||
|
||||
if ($0 ~ /code 104 \(PageUp\), value 0/) {
|
||||
if (pending_key == "up") {
|
||||
pending_key = ""
|
||||
emit("pageup")
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if ($0 ~ /code 109 \(PageDown\), value 0/) {
|
||||
if (pending_key == "down") {
|
||||
pending_key = ""
|
||||
emit("pagedown")
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$stream_pid" ]; then
|
||||
kill "$stream_pid" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$owns_runtime_state" != true ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
rm -f "$ACTION_FIFO"
|
||||
rm -f "$PID_FILE"
|
||||
}
|
||||
|
||||
ensure_single_instance() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
existing_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||
if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
||||
log_event "service_already_running pid=$existing_pid"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\n' "$$" >"$PID_FILE"
|
||||
owns_runtime_state=true
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "trigger" ]; then
|
||||
if [ "$#" -ne 2 ]; then
|
||||
printf 'usage: %s trigger <action>\n' "$0" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
send_action_to_service "$2"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
mkdir -p "$STATE_DIR"
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
ensure_single_instance
|
||||
log_event "service_started event_device=$EVENT_DEVICE runtime_dir=$RUNTIME_DIR combo_window=$THEME_MENU_COMBO_WINDOW_SECONDS"
|
||||
|
||||
while true; do
|
||||
rm -f "$ACTION_FIFO"
|
||||
mkfifo "$ACTION_FIFO"
|
||||
|
||||
action_stream >"$ACTION_FIFO" &
|
||||
stream_pid=$!
|
||||
|
||||
while IFS= read -r action; do
|
||||
handle_action "$action"
|
||||
done <"$ACTION_FIFO"
|
||||
|
||||
wait "$stream_pid" 2>/dev/null || true
|
||||
stream_pid=""
|
||||
rm -f "$ACTION_FIFO"
|
||||
sleep 1
|
||||
done
|
||||
234
dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh
Normal file
234
dash/staging/post-jailbreak-root/dashboard/local/theme-sync.sh
Normal file
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="$DIR/env.sh"
|
||||
THEME_FILE="$DIR/theme.env"
|
||||
STATE_DIR="$DIR/state"
|
||||
LOCAL_THEMES_INDEX="$DIR/../themes.json"
|
||||
LOCAL_THEMES_DIR="$DIR/../themes"
|
||||
THEMES_INDEX_CACHE="$STATE_DIR/themes.json"
|
||||
THEMES_INDEX_TIMESTAMP_FILE="$STATE_DIR/themes-updated-at"
|
||||
CURRENT_THEME_CACHE="$STATE_DIR/current-theme.json"
|
||||
CURRENT_THEME_ID_FILE="$STATE_DIR/current-theme-id"
|
||||
CURRENT_THEME_TIMESTAMP_FILE="$STATE_DIR/current-theme-updated-at"
|
||||
THEME_RUNTIME_ENV_FILE="$STATE_DIR/theme-runtime.env"
|
||||
THEME_JSON_LUA="$DIR/theme-json.lua"
|
||||
FETCH_CMD="$DIR/../xh"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
force_index=false
|
||||
force_theme=false
|
||||
requested_theme_id=${THEME_ID:-default}
|
||||
requested_orientation=${ORIENTATION:-portrait}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force-index)
|
||||
force_index=true
|
||||
;;
|
||||
--force-theme)
|
||||
force_theme=true
|
||||
;;
|
||||
--theme)
|
||||
shift
|
||||
requested_theme_id=${1:?"missing theme id"}
|
||||
;;
|
||||
--orientation)
|
||||
shift
|
||||
requested_orientation=${1:?"missing orientation"}
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
THEMES_INDEX_URL=${THEMES_INDEX_URL:-"https://shell.biboer.cn:20001/themes.json"}
|
||||
THEMES_INDEX_REFRESH_INTERVAL_MINUTES=${THEMES_INDEX_REFRESH_INTERVAL_MINUTES:-1440}
|
||||
THEME_CONFIG_REFRESH_INTERVAL_MINUTES=${THEME_CONFIG_REFRESH_INTERVAL_MINUTES:-1440}
|
||||
|
||||
now_epoch() {
|
||||
date '+%s'
|
||||
}
|
||||
|
||||
write_timestamp() {
|
||||
date '+%s' >"$1"
|
||||
}
|
||||
|
||||
refresh_due() {
|
||||
timestamp_file=$1
|
||||
interval_minutes=$2
|
||||
|
||||
if [ ! -f "$timestamp_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
current_epoch=$(now_epoch)
|
||||
last_epoch=$(cat "$timestamp_file" 2>/dev/null || echo 0)
|
||||
[ $((current_epoch - last_epoch)) -ge $((interval_minutes * 60)) ]
|
||||
}
|
||||
|
||||
fetch_to_path() {
|
||||
url=$1
|
||||
output_path=$2
|
||||
tmp_path="${output_path}.tmp.$$"
|
||||
|
||||
rm -f "$tmp_path"
|
||||
|
||||
if ! "$FETCH_CMD" -d -q -o "$tmp_path" get "$url"; then
|
||||
rm -f "$tmp_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv "$tmp_path" "$output_path"
|
||||
return 0
|
||||
}
|
||||
|
||||
parse_kv_file() {
|
||||
file_path=$1
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
THEME_ID)
|
||||
resolved_theme_id=$value
|
||||
;;
|
||||
ORIENTATION)
|
||||
resolved_orientation=$value
|
||||
;;
|
||||
CONFIG_URL)
|
||||
resolved_config_url=$value
|
||||
;;
|
||||
esac
|
||||
done <"$file_path"
|
||||
}
|
||||
|
||||
sync_themes_index() {
|
||||
if [ -f "$LOCAL_THEMES_INDEX" ]; then
|
||||
cp "$LOCAL_THEMES_INDEX" "$THEMES_INDEX_CACHE"
|
||||
write_timestamp "$THEMES_INDEX_TIMESTAMP_FILE"
|
||||
echo "Themes index refreshed from local bundle"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 主题清单是全局入口,平时按天同步一次即可。
|
||||
# 真正切换主题时会走 --force-index,确保马上拿到最新列表。
|
||||
if [ "$force_index" = true ] || [ ! -f "$THEMES_INDEX_CACHE" ] || refresh_due "$THEMES_INDEX_TIMESTAMP_FILE" "$THEMES_INDEX_REFRESH_INTERVAL_MINUTES"; then
|
||||
if fetch_to_path "$THEMES_INDEX_URL" "$THEMES_INDEX_CACHE"; then
|
||||
write_timestamp "$THEMES_INDEX_TIMESTAMP_FILE"
|
||||
echo "Themes index refreshed"
|
||||
elif [ ! -f "$THEMES_INDEX_CACHE" ]; then
|
||||
echo "Themes index fetch failed and no cache is available." >&2
|
||||
return 1
|
||||
else
|
||||
echo "Themes index fetch failed, using cached copy."
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
resolve_selected_theme() {
|
||||
# 这里统一做一次主题和方向的兜底解析:
|
||||
# 如果本地请求的 theme / orientation 已失效,就按 themes.json 的默认值回退。
|
||||
resolve_output_file="$STATE_DIR/theme-resolve.$$"
|
||||
lua "$THEME_JSON_LUA" resolve "$THEMES_INDEX_CACHE" "$requested_theme_id" "$requested_orientation" >"$resolve_output_file"
|
||||
resolved_theme_id=""
|
||||
resolved_orientation=""
|
||||
resolved_config_url=""
|
||||
parse_kv_file "$resolve_output_file"
|
||||
rm -f "$resolve_output_file"
|
||||
|
||||
if [ -z "$resolved_theme_id" ] || [ -z "$resolved_orientation" ] || [ -z "$resolved_config_url" ]; then
|
||||
echo "Unable to resolve theme selection." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
sync_theme_config() {
|
||||
local_theme_config="$LOCAL_THEMES_DIR/${resolved_theme_id}.json"
|
||||
|
||||
if [ -f "$local_theme_config" ]; then
|
||||
cp "$local_theme_config" "$CURRENT_THEME_CACHE"
|
||||
printf '%s\n' "$resolved_theme_id" >"$CURRENT_THEME_ID_FILE"
|
||||
write_timestamp "$CURRENT_THEME_TIMESTAMP_FILE"
|
||||
echo "Theme config refreshed from local bundle: $resolved_theme_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 主题配置按 theme 维度缓存;
|
||||
# orientation 只是同一个主题 JSON 里的 variant,切换方向不需要重新拉整份配置。
|
||||
needs_theme_fetch=$force_theme
|
||||
cached_theme_id=$(cat "$CURRENT_THEME_ID_FILE" 2>/dev/null || echo "")
|
||||
|
||||
if [ ! -f "$CURRENT_THEME_CACHE" ] || [ ! -f "$CURRENT_THEME_ID_FILE" ]; then
|
||||
needs_theme_fetch=true
|
||||
elif [ "$cached_theme_id" != "$resolved_theme_id" ]; then
|
||||
needs_theme_fetch=true
|
||||
elif refresh_due "$CURRENT_THEME_TIMESTAMP_FILE" "$THEME_CONFIG_REFRESH_INTERVAL_MINUTES"; then
|
||||
needs_theme_fetch=true
|
||||
fi
|
||||
|
||||
if [ "$needs_theme_fetch" = true ]; then
|
||||
if fetch_to_path "$resolved_config_url" "$CURRENT_THEME_CACHE"; then
|
||||
printf '%s\n' "$resolved_theme_id" >"$CURRENT_THEME_ID_FILE"
|
||||
write_timestamp "$CURRENT_THEME_TIMESTAMP_FILE"
|
||||
echo "Theme config refreshed: $resolved_theme_id"
|
||||
elif [ ! -f "$CURRENT_THEME_CACHE" ] || [ "$cached_theme_id" != "$resolved_theme_id" ]; then
|
||||
echo "Theme config fetch failed and no matching cache is available." >&2
|
||||
return 1
|
||||
else
|
||||
echo "Theme config fetch failed, using cached copy."
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
write_runtime_env() {
|
||||
# runtime env 是 Kindle 真正消费的运行时配置快照。
|
||||
# fetch-dashboard / render-clock / dash.sh 都只读这一份,避免各自重复解析 JSON。
|
||||
runtime_output_file="$STATE_DIR/theme-runtime-result.$$"
|
||||
lua "$THEME_JSON_LUA" write-runtime \
|
||||
"$CURRENT_THEME_CACHE" \
|
||||
"$resolved_theme_id" \
|
||||
"$resolved_orientation" \
|
||||
"$THEME_RUNTIME_ENV_FILE" >"$runtime_output_file"
|
||||
|
||||
resolved_theme_id=""
|
||||
resolved_orientation=""
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
THEME_ID)
|
||||
resolved_theme_id=$value
|
||||
;;
|
||||
ORIENTATION)
|
||||
resolved_orientation=$value
|
||||
;;
|
||||
esac
|
||||
done <"$runtime_output_file"
|
||||
rm -f "$runtime_output_file"
|
||||
|
||||
if [ -z "$resolved_theme_id" ] || [ -z "$resolved_orientation" ]; then
|
||||
echo "Unable to write runtime theme env." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf 'THEME_ID=%s\n' "$resolved_theme_id"
|
||||
printf 'ORIENTATION=%s\n' "$resolved_orientation"
|
||||
return 0
|
||||
}
|
||||
|
||||
sync_themes_index
|
||||
resolve_selected_theme
|
||||
sync_theme_config
|
||||
write_runtime_env
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="$DIR/env.sh"
|
||||
THEME_MENU_SERVICE_CMD="$DIR/theme-menu-service.sh"
|
||||
STOP_DASHBOARD_CMD="$DIR/../stop.sh"
|
||||
RUNTIME_DIR_DEFAULT="/tmp/kindle-dash-touch-home"
|
||||
TOUCH_DEVICE_DEFAULT="/dev/input/event1"
|
||||
SCREEN_WIDTH_DEFAULT="1072"
|
||||
SCREEN_HEIGHT_DEFAULT="1448"
|
||||
HOTSPOT_WIDTH_RATIO_DEFAULT="0.18"
|
||||
HOTSPOT_HEIGHT_RATIO_DEFAULT="0.18"
|
||||
LONG_PRESS_SECONDS_DEFAULT="1.2"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
RUNTIME_DIR=${TOUCH_HOME_RUNTIME_DIR:-$RUNTIME_DIR_DEFAULT}
|
||||
PID_FILE="$RUNTIME_DIR/touch-home-service.pid"
|
||||
ACTION_FIFO="$RUNTIME_DIR/touch-home-actions.fifo"
|
||||
TOUCH_DEVICE=${TOUCH_HOME_EVENT_DEVICE:-$TOUCH_DEVICE_DEFAULT}
|
||||
SCREEN_WIDTH=${TOUCH_HOME_SCREEN_WIDTH:-$SCREEN_WIDTH_DEFAULT}
|
||||
SCREEN_HEIGHT=${TOUCH_HOME_SCREEN_HEIGHT:-$SCREEN_HEIGHT_DEFAULT}
|
||||
HOTSPOT_WIDTH_RATIO=${TOUCH_HOME_HOTSPOT_WIDTH_RATIO:-$HOTSPOT_WIDTH_RATIO_DEFAULT}
|
||||
HOTSPOT_HEIGHT_RATIO=${TOUCH_HOME_HOTSPOT_HEIGHT_RATIO:-$HOTSPOT_HEIGHT_RATIO_DEFAULT}
|
||||
LONG_PRESS_SECONDS=${TOUCH_HOME_LONG_PRESS_SECONDS:-$LONG_PRESS_SECONDS_DEFAULT}
|
||||
|
||||
stream_pid=""
|
||||
owns_runtime_state=false
|
||||
|
||||
log_event() {
|
||||
# 统一把触摸热区服务日志写到 stderr,交给上层 nohup 重定向到日志文件。
|
||||
printf '%s touch-home: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$stream_pid" ]; then
|
||||
kill "$stream_pid" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$owns_runtime_state" != true ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
rm -f "$ACTION_FIFO"
|
||||
rm -f "$PID_FILE"
|
||||
}
|
||||
|
||||
ensure_single_instance() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
existing_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||
if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
||||
log_event "service_already_running pid=$existing_pid"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\n' "$$" >"$PID_FILE"
|
||||
owns_runtime_state=true
|
||||
}
|
||||
|
||||
trigger_theme_menu() {
|
||||
log_event "trigger_theme_menu"
|
||||
|
||||
# 右下角长按现在优先复用主题菜单服务,让用户先看到主题列表。
|
||||
# 如果菜单服务意外没起来,再回退到旧的 stop.sh 路径,避免设备失去退出入口。
|
||||
if "$THEME_MENU_SERVICE_CMD" trigger open_menu; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_event "trigger_theme_menu_failed fallback_home"
|
||||
"$STOP_DASHBOARD_CMD"
|
||||
}
|
||||
|
||||
event_stream() {
|
||||
evtest --grab "$TOUCH_DEVICE" 2>/dev/null | awk \
|
||||
-v screen_width="$SCREEN_WIDTH" \
|
||||
-v screen_height="$SCREEN_HEIGHT" \
|
||||
-v hotspot_width_ratio="$HOTSPOT_WIDTH_RATIO" \
|
||||
-v hotspot_height_ratio="$HOTSPOT_HEIGHT_RATIO" \
|
||||
-v long_press_seconds="$LONG_PRESS_SECONDS" '
|
||||
BEGIN {
|
||||
hotspot_min_x = int(screen_width * (1 - hotspot_width_ratio))
|
||||
hotspot_min_y = int(screen_height * (1 - hotspot_height_ratio))
|
||||
touch_active = 0
|
||||
touch_x = -1
|
||||
touch_y = -1
|
||||
touch_start_time = 0
|
||||
already_triggered = 0
|
||||
}
|
||||
|
||||
function extract_time(line, matched) {
|
||||
matched = match(line, /time [0-9]+\.[0-9]+/)
|
||||
if (matched == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return substr(line, RSTART + 5, RLENGTH - 5) + 0
|
||||
}
|
||||
|
||||
function emit(name) {
|
||||
print name
|
||||
fflush()
|
||||
}
|
||||
|
||||
function reset_touch() {
|
||||
touch_active = 0
|
||||
touch_x = -1
|
||||
touch_y = -1
|
||||
touch_start_time = 0
|
||||
already_triggered = 0
|
||||
}
|
||||
|
||||
{
|
||||
current_time = extract_time($0)
|
||||
|
||||
# Voyage 上实际输出是 "code 57 (MT Tracking ID)",但不同 evtest 版本
|
||||
# 也可能写成 ABS_MT_TRACKING_ID,这里同时兼容两种格式。
|
||||
if (($0 ~ /code 57 \(MT Tracking ID\)/ || $0 ~ /ABS_MT_TRACKING_ID/) && $0 ~ /value -1/) {
|
||||
reset_touch()
|
||||
next
|
||||
}
|
||||
|
||||
if ($0 ~ /code 57 \(MT Tracking ID\)/ || $0 ~ /ABS_MT_TRACKING_ID/) {
|
||||
touch_active = 1
|
||||
touch_start_time = current_time
|
||||
already_triggered = 0
|
||||
next
|
||||
}
|
||||
|
||||
if ($0 ~ /code 53 \(MT X\)/ || $0 ~ /ABS_MT_POSITION_X/) {
|
||||
if (match($0, /value -?[0-9]+/)) {
|
||||
touch_x = substr($0, RSTART + 6, RLENGTH - 6) + 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if ($0 ~ /code 54 \(MT Y\)/ || $0 ~ /ABS_MT_POSITION_Y/) {
|
||||
if (match($0, /value -?[0-9]+/)) {
|
||||
touch_y = substr($0, RSTART + 6, RLENGTH - 6) + 0
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
if ($0 ~ /Report Sync/ || $0 ~ /SYN_REPORT/) {
|
||||
if (touch_active && !already_triggered && touch_x >= hotspot_min_x && touch_y >= hotspot_min_y) {
|
||||
if (current_time - touch_start_time >= long_press_seconds) {
|
||||
already_triggered = 1
|
||||
emit("trigger_theme_menu")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
ensure_single_instance
|
||||
log_event "service_started touch_device=$TOUCH_DEVICE long_press_seconds=$LONG_PRESS_SECONDS hotspot_width_ratio=$HOTSPOT_WIDTH_RATIO hotspot_height_ratio=$HOTSPOT_HEIGHT_RATIO"
|
||||
|
||||
while true; do
|
||||
rm -f "$ACTION_FIFO"
|
||||
mkfifo "$ACTION_FIFO"
|
||||
|
||||
event_stream >"$ACTION_FIFO" &
|
||||
stream_pid=$!
|
||||
|
||||
while IFS= read -r action; do
|
||||
case "$action" in
|
||||
trigger_theme_menu)
|
||||
trigger_theme_menu
|
||||
;;
|
||||
esac
|
||||
done <"$ACTION_FIFO"
|
||||
|
||||
wait "$stream_pid" 2>/dev/null || true
|
||||
stream_pid=""
|
||||
rm -f "$ACTION_FIFO"
|
||||
sleep 1
|
||||
done
|
||||
@@ -4,15 +4,25 @@ DEBUG=${DEBUG:-false}
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
THEME_FILE="$DIR/local/theme.env"
|
||||
LOG_FILE="$DIR/logs/dash.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
"$DIR/dash.sh"
|
||||
else
|
||||
"$DIR/dash.sh" >>"$LOG_FILE" 2>&1 &
|
||||
# 通过 SSH 或 KUAL 触发时,父 shell 很快就会退出。
|
||||
# 仅用 nohup 仍可能残留在同一个 session/group 里,KUAL/framework 切换时
|
||||
# 依然可能把后台子进程一起打掉;这里额外用 setsid 彻底脱离会话。
|
||||
if command -v setsid >/dev/null 2>&1; then
|
||||
nohup setsid "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||
else
|
||||
nohup "$DIR/dash.sh" >>"$LOG_FILE" 2>&1 </dev/null &
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,2 +1,187 @@
|
||||
#!/usr/bin/env sh
|
||||
pkill -f dash.sh
|
||||
set -eu
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
|
||||
# 退出 dashboard 时,统一走一条保守恢复路径:
|
||||
# 1. 停掉 dashboard 自己的进程
|
||||
# 2. 干净重启 framework / webreader / cvm
|
||||
#
|
||||
# Voyage 5.13.6 上试过的“快切换”路径会把 blanket/cvm 打崩,
|
||||
# 当前这台机型仍以稳定恢复优先。
|
||||
|
||||
START_BIN=/sbin/start
|
||||
STOP_BIN=/sbin/stop
|
||||
STATUS_BIN=/sbin/status
|
||||
INITCTL_BIN=/sbin/initctl
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
KEEP_NATIVE_UI_STACK_RUNNING=${KEEP_NATIVE_UI_STACK_RUNNING:-false}
|
||||
KUAL_APP_ID=${KUAL_APP_ID:-app://com.mobileread.ixtab.kindlelauncher}
|
||||
|
||||
run_job_cmd() {
|
||||
bin_path=$1
|
||||
shift
|
||||
|
||||
if [ -x "$bin_path" ]; then
|
||||
"$bin_path" "$@" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 127
|
||||
}
|
||||
|
||||
job_running() {
|
||||
job_name=$1
|
||||
job_state=$(
|
||||
if [ -x "$STATUS_BIN" ]; then
|
||||
"$STATUS_BIN" "$job_name" 2>/dev/null || true
|
||||
elif [ -x "$INITCTL_BIN" ]; then
|
||||
"$INITCTL_BIN" status "$job_name" 2>/dev/null || true
|
||||
else
|
||||
true
|
||||
fi
|
||||
)
|
||||
|
||||
case "$job_state" in
|
||||
*"start/running"*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
stop_job() {
|
||||
job_name=$1
|
||||
|
||||
if ! job_running "$job_name"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
run_job_cmd "$STOP_BIN" "$job_name" || run_job_cmd "$INITCTL_BIN" stop "$job_name" || true
|
||||
}
|
||||
|
||||
start_job() {
|
||||
job_name=$1
|
||||
|
||||
if job_running "$job_name"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
run_job_cmd "$START_BIN" "$job_name" || run_job_cmd "$INITCTL_BIN" start "$job_name" || true
|
||||
}
|
||||
|
||||
wait_for_cvm() {
|
||||
attempts=0
|
||||
while [ "$attempts" -lt 12 ]; do
|
||||
if pgrep -x cvm >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
attempts=$((attempts + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_job_stable_running() {
|
||||
job_name=$1
|
||||
max_attempts=$2
|
||||
stable_required=$3
|
||||
attempts=0
|
||||
stable_hits=0
|
||||
|
||||
# Voyage 上 webreader 经常会出现“刚被拉起,1-2 秒后又掉回 stop/waiting”的状态。
|
||||
# 这里不再只发一次 start,而是循环观察一段时间,直到连续多次都看到 start/running,
|
||||
# 才把恢复流程视为成功。
|
||||
while [ "$attempts" -lt "$max_attempts" ]; do
|
||||
if job_running "$job_name"; then
|
||||
stable_hits=$((stable_hits + 1))
|
||||
if [ "$stable_hits" -ge "$stable_required" ]; then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
stable_hits=0
|
||||
start_job "$job_name"
|
||||
fi
|
||||
|
||||
attempts=$((attempts + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
full_restore_ui() {
|
||||
# 先把残留 UI 栈彻底停干净,避免 webreader 存活但 cvm 已崩的白屏状态。
|
||||
stop_job webreader
|
||||
stop_job framework
|
||||
killall cvm 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
if [ -x /etc/init.d/framework ]; then
|
||||
/etc/init.d/framework start || true
|
||||
else
|
||||
start_job framework
|
||||
fi
|
||||
|
||||
# framework 拉起后,先等 cvm 真正起来,再启动 webreader。
|
||||
if ! wait_for_cvm; then
|
||||
echo "警告:framework 已请求启动,但 cvm 未在预期时间内出现。"
|
||||
fi
|
||||
|
||||
if ! ensure_job_stable_running webreader 12 3; then
|
||||
echo "警告:webreader 未能稳定恢复,可能仍需手工执行 /sbin/start webreader。"
|
||||
fi
|
||||
}
|
||||
|
||||
stop_dashboard_processes() {
|
||||
pkill -f dash.sh 2>/dev/null || true
|
||||
pkill -f start.sh 2>/dev/null || true
|
||||
pkill -f theme-menu-service.sh 2>/dev/null || true
|
||||
pkill -f touch-home-service.sh 2>/dev/null || true
|
||||
lipc-set-prop com.lab126.powerd preventScreenSaver 0 2>/dev/null || true
|
||||
}
|
||||
|
||||
native_active_app() {
|
||||
if ! command -v lipc-get-prop >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
lipc-get-prop com.lab126.appmgrd activeApp 2>/dev/null || true
|
||||
}
|
||||
|
||||
return_to_home_from_kual() {
|
||||
active_app=$(native_active_app)
|
||||
|
||||
case "$active_app" in
|
||||
com.mobileread.ixtab.kindlelauncher|app://com.mobileread.ixtab.kindlelauncher)
|
||||
# 保留原生 UI 栈时,dashboard 底下通常仍是 KUAL。
|
||||
# 这里复用 launch-from-kual 已验证过的 appmgrd stop 路径,把 KUAL 正常退回首页。
|
||||
if command -v lipc-set-prop >/dev/null 2>&1; then
|
||||
lipc-set-prop com.lab126.appmgrd stop "$KUAL_APP_ID" >/dev/null 2>&1 || true
|
||||
sleep 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
soft_stop_for_native_ui_overlay() {
|
||||
# 原生 UI 栈保持存活时,退出 dashboard 只需要停掉我们自己的覆盖进程,
|
||||
# 不要再重建 framework/webreader/cvm,否则会重新引入旧架构的切换风险。
|
||||
stop_dashboard_processes
|
||||
return_to_home_from_kual
|
||||
}
|
||||
|
||||
if [ "$KEEP_NATIVE_UI_STACK_RUNNING" = true ]; then
|
||||
soft_stop_for_native_ui_overlay
|
||||
exit 0
|
||||
fi
|
||||
|
||||
stop_dashboard_processes
|
||||
full_restore_ui
|
||||
|
||||
46
dash/staging/post-jailbreak-root/dashboard/switch-theme.sh
Normal file
46
dash/staging/post-jailbreak-root/dashboard/switch-theme.sh
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
THEME_FILE="$DIR/local/theme.env"
|
||||
THEME_RUNTIME_ENV_FILE="$DIR/local/state/theme-runtime.env"
|
||||
BACKGROUND_TIMESTAMP_FILE="$DIR/local/state/background-updated-at"
|
||||
BACKGROUND_PNG="$DIR/kindlebg.png"
|
||||
WAIT_FOR_WIFI_CMD="$DIR/wait-for-wifi.sh"
|
||||
THEME_SYNC_CMD="$DIR/local/theme-sync.sh"
|
||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||
CLOCK_RENDER_CMD="$DIR/local/render-clock.sh"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$THEME_FILE" ] && . "$THEME_FILE"
|
||||
|
||||
requested_theme_id=${1:?"usage: switch-theme.sh <theme-id> [orientation]"}
|
||||
requested_orientation=${2:-${ORIENTATION:-portrait}}
|
||||
|
||||
mkdir -p "$DIR/local/state"
|
||||
|
||||
# 切换主题时必须立刻联网拉到最新配置和背景,
|
||||
# 否则用户会看到 theme.env 已更新,但屏幕内容仍停留在旧主题。
|
||||
echo "Switching theme to $requested_theme_id / $requested_orientation"
|
||||
"$WAIT_FOR_WIFI_CMD" "$WIFI_TEST_IP"
|
||||
"$THEME_SYNC_CMD" --force-index --force-theme --theme "$requested_theme_id" --orientation "$requested_orientation" >/dev/null
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
. "$THEME_RUNTIME_ENV_FILE"
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$BACKGROUND_PNG"
|
||||
date '+%s' >"$BACKGROUND_TIMESTAMP_FILE"
|
||||
|
||||
# 只有在主题配置和背景都成功拉取后,才把当前选择持久化到 theme.env。
|
||||
cat >"$THEME_FILE" <<EOF
|
||||
export THEME_ID='${THEME_ID}'
|
||||
export ORIENTATION='${ORIENTATION}'
|
||||
EOF
|
||||
|
||||
/usr/sbin/eips -f -g "$BACKGROUND_PNG"
|
||||
"$CLOCK_RENDER_CMD" true
|
||||
|
||||
echo "Theme switched to ${THEME_ID} / ${ORIENTATION}"
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"items": [
|
||||
{"name": "Kindle Dashboard", "action": "/mnt/us/dashboard/start.sh"},
|
||||
{
|
||||
"name": "Kindle Dashboard",
|
||||
"priority": -998,
|
||||
"items": [
|
||||
{"name": "Default", "priority": 1, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "default", "exitmenu": true},
|
||||
{"name": "Paper", "priority": 2, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "paper", "exitmenu": true},
|
||||
{"name": "Classic", "priority": 3, "action": "/mnt/us/dashboard/launch-theme-from-kual.sh", "params": "classic", "exitmenu": true}
|
||||
]
|
||||
},
|
||||
{"name": "Dashboard Debug On", "action": "/mnt/us/dashboard/debug-on.sh"},
|
||||
{"name": "Dashboard Debug Off", "action": "/mnt/us/dashboard/debug-off.sh"}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user