一、项目概述
本项目实现 Linux 主机自动生成仪表盘图片 → WiFi 推送至越狱 Kindle 8 → Kindle 自动加载自定义屏保 完整自动化流程。仪表盘包含实时天气、4 日天气预报、今日待办清单,定时自动刷新,实现 Kindle 变身电子墨水信息看板。

1. 硬件与固件前置
- Kindle 8(固件 ≤ 5.16.2.1.1,ARMv7 Linux3.10.53-lab126)
- 设备已完整越狱(KUAL 可用,可获得 root 权限)
- 安装 USBNetwork
- 屏保强制读取路径:
/mnt/us/linkss/screensavers/
Kindle 开机 / 休眠时,自动读取该目录内图片作为锁屏屏保。
2. 整体业务流程
todo.json(待办数据源)
↓
render_dash.py Python预处理数据(高德天气API)
↓
template.html 静态网页模板渲染填充数据
↓
wkhtmltoimage 将HTML页面渲染为PNG图片
↓
to_kindle.py 将网页图片转成Kindle图片
↓
update_today-wiif.sh Kidle端WiFi感知版屏保图片拉取脚本
↓
Kindle8 更新/mnt/us/linkss/screensavers/bg_ss00.png
↓
ScreenSavers Hack加载图片bg_ss00.png,作为设备休眠屏保
二、 ScreenSavers Hack安装
1. 功能说明
ScreenSavers Hack 社区俗称 linkss,是 NiLuJe 开源的底层屏保劫持插件,直接接管亚马逊原生 powerd 屏保流程,替换广告 / 官方默认壁纸:
- 屏蔽亚马逊广告屏保:彻底关闭开机、休眠推送的付费广告,永久替换为自定义图片
- 本地图片轮播:读取
/mnt/us/linkss/screensavers/目录内灰度图片,每次休眠随机 / 顺序切换 - 兼容全 K5 机型(你的 Kindle8):适配固件 5.10~5.16、内核 3.10.53-lab126
- 自定义休眠文字:可在屏保底部叠加电量、时间、自定义文本
- 分辨率自适应:自动适配 6 寸 Kindle8 屏幕 800×600 灰度图
- KUAL 可视化控制:一键开关自定义屏保、临时恢复原生广告屏保、刷新图片缓存
关键限制:它只能读取本地指定目录图片
- 强制图片路径:
/mnt/us/linkss/screensavers/,不支持自定义其他文件夹 - 原生不具备网络拉取、动态生成日历 / 待办的能力:仅静态本地图片,无法联网下载、渲染文字面板
- 图片命名要求:必须纯数字命名(
001.png002.jpg),否则不会加载 - 格式约束:仅支持 8 位灰度 PNG/JPG,彩色图会失真、残影严重
2. 安装步骤
kindle-linkss-0.25.N-r18981.tar
下载 链接: kindle-diy_免费高速下载|百度网盘-分享无限制
官方指南:Kindle Touch/PaperWhite ScreenSavers Hack
- 首先确保关闭了广告特惠,并且安装了 MRPI 和 KUAL;
- 用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
- 解压缩下载到的 kindle-linkss-0.25.N-rxxx.tar.xz,得到一个文件夹;
- 把文件夹内的 Update_linkss_0.25.N_install_pw2_kt2_kv_pw3.bin 拷贝到 Kindle 里 mrpackages 文件夹中(如果是 KPW1 或 Touch 请选择另外那个 bin 文件);
- 弹出 Kindle 磁盘,进入 Kindle 界面,打开 KUAL,依次点击菜单【Helper → Install MR Packages】(或在搜索栏输入
;log mrpi并回车); - 耐心等待 linkss 安装,直到安装完成后 Kindle 重启完毕;
- 再次用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
在 Kindle 根目录下会出现一个名为 linkss 的文件夹,把你想要设为屏保的图片放到该文件夹下的 screensavers 文件夹中即可。需要注意的是,屏保图片应按照如 bg_ss00.png、bg_ss01.png … bg_ss19.png … 这样的序列方式命名。屏保图片的规格请参照下面所列参数。
屏保规格:
为达到更好的视觉效果,建议屏保图片按照下面不同设备相对应的图片尺寸制作:
Kindle 2/3/4/5/7/8/10:PNG 格式,宽高 600*800 像素
如果制作的 png 图片无法正常使用,请尝试将屏保图片处理成 8 位深度的灰度图。需要注意的是,请不要把一个 jpg 图片通过更改后缀名的方式更改成 png,这样得到的仍然是无法使用的 jpg 图片。
3. 菜单功能说明

- ✓ Show current version 显示当前 ScreenSavers Hack 版本,勾选代表启用版本信息打印。
- Disable ScreenSavers Hack临时关闭插件:关闭自定义屏保,恢复 Kindle 原生官方屏保。 再次点击即可重新启用自定义屏保。
- Enable verbose mode 开启详细日志;调试屏保加载异常、黑屏、不切换图片时打开,日常使用可以关闭。
- Enable autoreboot feature (Legacy Def.) 旧版兼容:修改屏保配置后自动重启框架,新版本一般不需要,建议保持关闭。
- Restart framework now ⭐【高频使用】立刻重启屏保服务。 更换图片、修改屏保模式后,点这个重载配置,不用重启整台 Kindle。
- Screen Savers Behavior ▽(下拉菜单,核心设置) 点开后可以选择屏保运行模式,常用选项:
- Random:随机轮询图片
- Sequential:按文件名顺序
三、 Kindle 终端改造部分
1. 改造步骤
- 完成设备越狱
- 安装 ScreenSavers Hack 屏保插件
- 插件固定读取路径:
/mnt/us/linkss/screensavers/ - 屏保文件名称固定:
bg_ss00.png - 配套 wifi 拉取脚本:Kindle 部署 WiFi 文件拉取脚本,拉取Linux 主机的图片
注意:Kindle 必须保持 WiFi 连接,保证与 Linux 主机网络互通。
本方案使用 SCP 复制文件到本机,Linux 主机不需要复杂配置(Http 服务)。
延伸备选方案(摆脱 SSH,规避休眠断开麻烦)
主机 192.168.1.12 搭建简易 http 服务,用wget获取 md5,不需要维持 ssh 连接:
# 主机执行
cd /home/tony/today && python3 -m http.server 8080
然后在 Kindle 使用:
wget -q -O - http://192.168.1.12:8080/md5.txt
好处:轻量,不需要持久 SSH 密钥,连接开销更小。需要我可以把脚本改成 wget 版本。
2. ssh 自动登录
脚本文件中使用 scp 定时拉取文件,需要 kindle 能自动免密登录 Linux 主机。
步骤 1:先确认本地密钥
先执行,查看是否已经生成密钥:
ls -l /mnt/us/usbnet/etc/dot.ssh/*

如果没有输出,先手动生成密钥(一路回车,不要设置密钥密码):
ssh-keygen -t ed25519
若远端老旧系统不支持 ed25519,换成ssh-keygen -t rsa
步骤 2:手动推送公钥
cat /mnt/us/usbnet/etc/dot.ssh/id_ed25519.pub | ssh tony@192.168.1.12 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
如果你生成的是 rsa 密钥,替换文件名:
cat /mnt/us/usbnet/etc/dot.ssh/id_rsa.pub | ssh tony@192.168.1.12 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
执行时输入一次 tony 账户密码。
步骤 3:测试免密登录
ssh tony@192.168.1.12 hostname
✅ 不需要输入密码,代表免密配置成功。
3. 屏保文件拉取脚本
脚本文件上传到 kindle 设备的/mnt/us 目录下,后端运行。
/mnt/us/update_today-wiif.sh &
update_today-wiif.sh
#!/bin/sh
REMOTE_USER="tony"
REMOTE_IP="192.168.1.12"
REMOTE_FILE="/home/tony/today/bg_ss00.png"
LOCAL_TARGET="/mnt/us/linkss/screensavers/bg_ss00.png"
TMP_STORE="/mnt/us/.tmp_bg_ss00.png"
LOG="/mnt/us/fetch_ss.log"
GATEWAY="192.168.1.1"
# 在线轮询间隔
INTERVAL_ONLINE=30
# WiFi断开休眠时拉长轮询间隔,省电
INTERVAL_OFFLINE=60
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "${LOG}"
}
log "===== WiFi感知版屏保拉取脚本启动 ====="
while true
do
# 检测网络连通
ping -c 1 -W 2 ${GATEWAY} > /dev/null 2>&1
if [ $? -eq 0 ]; then
# WiFi在线
REMOTE_MD5=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
"${REMOTE_USER}@${REMOTE_IP}" "md5sum ${REMOTE_FILE} 2>/dev/null | awk '{print \$1}'")
if [ -n "${REMOTE_MD5}" ]; then
if [ -f "${LOCAL_TARGET}" ]; then
LOCAL_MD5=$(md5sum "${LOCAL_TARGET}" | awk '{print $1}')
else
LOCAL_MD5=""
fi
if [ "${REMOTE_MD5}" != "${LOCAL_MD5}" ]; then
log "🔔 远端图片更新,开始下载"
if scp -q -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
"${REMOTE_USER}@${REMOTE_IP}:${REMOTE_FILE}" "${TMP_STORE}"; then
mv "${TMP_STORE}" "${LOCAL_TARGET}"
log "✅ 图片更新完成"
# /mnt/us/linkss/bin/restart_framework
else
log "❌ scp下载失败"
rm -f "${TMP_STORE}"
fi
fi
else
log "⚠️ SSH无法获取远端MD5"
fi
sleep ${INTERVAL_ONLINE}
else
# WiFi离线,大概率深度休眠,拉长等待
sleep ${INTERVAL_OFFLINE}
fi
done
四、Linux 端开发
1. Linux 主机端文件清单(存放路径 /home/tony/today/)
| 文件名称 | 功能说明 |
|
| 网页模板,色块风格仪表盘;上下 6:4 布局,顶部天气区域、底部待办通栏(2 列 3 行共 6 条待办) |
|
| 核心生成脚本① 读取 待办清单② 请求高德 API 获取武汉天气预报③ 将数据填充进 html 模板,生成静态网页 |
|
| 待办事项数据源,可直接编辑修改清单内容 |
|
| 自动化执行脚本,串联整套流程:执行 python 渲染 → wkhtmltoimage 生成图片 → 推送 Kindle → 清理临时文件 |
|
| 将网页图片转成Kindle 使用的图片,规格: 600x800 大小,16 位色,自动缩放,横向图片 |
2. 页面关键 UI 设计要点
- 画布尺寸:固定 800×600(适配 Kindle8 分辨率)
- 布局:上下区域比例 6:4
- 上部:实时天气(浅灰色块
#d8d8d8)+ 4 天天气预报(白底) - 下部:整块浅灰色背景通栏,「今日待办」标题右侧横向展示日期、星期
- 天气预报精简布局:去除多余风力文字,气温紧贴天气图标,压缩垂直占用高度
- 待办条目:采用 Flex 自动换行实现 2 列 3 行,最多 6 条任务(兼容 wkhtmltoimage 老旧渲染内核,放弃 CSS Grid)
- 页面无前端交互 JS,纯静态页面,提升无头渲染兼容性
3. 待办事项数据源todo.json
{
"list": [
"1. 处理图片屏保批量优化",
"2. 更新Kindle锁屏壁纸",
"3. 调试自动拉取脚本",
"4. 整理设备自动化流程",
"5. 备份今日配置文件",
"6. 无事项"
]
}
4. 网页模板template.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Kindle 仪表盘 800×600</title>
<style>
*{
margin:0;
padding:0;
box-sizing:border-box;
}
body{
width:800px;
height:600px;
background:#ffffff;
color:#1a1a1a;
font-family:"SimHei","Microsoft YaHei",sans-serif;
overflow:hidden;
padding:16px;
}
/* 整体布局 6:4 上下分割 上360px 下224px */
.wrap-top{
width:768px;
height:340px;
margin-bottom:16px;
display:flex;
flex-direction:column;
gap:12px;
}
.wrap-bottom{
width:768px;
height:244px;
background:#d8d8d8;
padding:20px;
}
/* ========== 顶部实时天气 背景#d8d8d8 ========== */
.top-now-weather{
width:100%;
height:132px;
background:#d8d8d8;
display:flex;
align-items:center;
justify-content:space-between;
padding:0 24px;
}
.city-block{
display:flex;
flex-direction:column;
align-items:center;
gap:8px;
}
.city-circle{
width:72px;
height:72px;
border-radius:50%;
background:#111;
color:#fff;
display:flex;
align-items:center;
justify-content:center;
font-size:32px;
}
.city-name{
font-size:20px;
font-weight:bold;
}
.temp-block{
display:flex;
align-items:center;
gap:16px;
}
.weather-icon-circle{
width:72px;
height:72px;
border-radius:50%;
background:#e8f0fe;
display:flex;
align-items:center;
justify-content:center;
font-size:34px;
}
.temp-num{
font-size:66px;
font-weight:bold;
}
.weather-desc{
font-size:20px;
color:#444;
text-align:center;
}
.weather-right-info{
text-align:right;
font-size:18px;
line-height:1.7;
color:#333;
}
/* ========== 4天预报区域 白底,精简布局降低高度 ========== */
.forecast-wrap{
display:flex;
gap:12px;
flex:1;
}
.forecast-item{
flex:1;
background:#fff;
text-align:center;
padding:8px 4px;
border:1px solid #eee;
}
.forecast-date{
font-size:16px;
font-weight:bold;
margin-bottom:6px;
}
.forecast-label{
font-size:13px;
color:#555;
margin:4px 0;
}
.forecast-row{
display:flex;
align-items:center;
justify-content:center;
gap:6px;
font-size:18px;
margin:4px 0;
}
.forecast-icon{
font-size:28px;
}
/* ========== 底部待办区域 ========== */
.todo-header-bar{
display:flex;
justify-content:space-between;
align-items:center;
padding-bottom:10px;
margin-bottom:12px;
border-bottom:1px solid #999;
}
.todo-title{
font-size:26px;
font-weight:bold;
}
.date-group-right{
font-size:18px;
text-align:right;
}
/* 待办:2列布局,最多6条 */
.todo-container{
display:flex;
flex-wrap: wrap;
gap:0 16px;
}
.todo-item{
flex: 0 0 calc(50% - 8px); /* 50%宽度,减去一半gap,实现均分两列 */
font-size:18px;
padding:7px 0;
border-bottom:1px solid #777;
}
</style>
</head>
<body>
<div class="wrap-top">
<!-- 顶部实时天气 -->
<div class="top-now-weather">
<div class="city-block">
<div class="city-circle">🏙</div>
<div class="city-name">武汉</div>
</div>
<div class="temp-block">
<div class="weather-icon-circle">{{now_icon}}</div>
<div>
<div class="temp-num">{{now_temp}}°</div>
<div class="weather-desc">{{now_weather}}</div>
</div>
</div>
<div class="weather-right-info">
<div>风向风力:{{wind_text}}</div>
<div>湿度:{{humidity}}%</div>
<div>{{report_time}}</div>
</div>
</div>
<!-- 4天预报 -->
<div class="forecast-wrap">
{{forecast_html}}
</div>
</div>
<!-- 底部区域 -->
<div class="wrap-bottom">
<div class="todo-header-bar">
<div class="todo-title">今日待办</div>
<div class="date-group-right">
{{date_str}} {{week_str}} {{lunar_str}}
</div>
</div>
<div class="todo-container">
{{todo_html}}
</div>
</div>
</body>
</html>
5. 核心生成脚本render_dash.py
import requests
import datetime
import json
# ===================== 配置区 =====================
AMAP_KEY = "你的高德天气API-key"
CITY_ADCODE = "420100" # 武汉
TEMPLATE_PATH = "./template.html"
OUT_HTML = "./weather_dash.html"
TODO_JSON_PATH = "./todo.json"
# ==================================================
weather_icon_map = {
"晴":"☀",
"多云":"⛅",
"阴":"☁",
"小雨":"🌧",
"中雨":"🌧",
"大雨":"🌧",
"雷阵雨":"⛈",
"小雪":"❄",
"中雪":"❄",
"大雪":"❄",
"雾":"🌫",
"霾":"🌫"
}
week_cn = ["周日","周一","周二","周三","周四","周五","周六"]
def fetch_live_weather():
url = f"https://restapi.amap.com/v3/weather/weatherInfo?city={CITY_ADCODE}&key={AMAP_KEY}&extensions=base"
resp = requests.get(url, timeout=10)
data = resp.json()
print("=== 实时天气(base)返回 ===")
print(data)
if data["status"] != "1":
raise Exception(f"实时天气接口失败 status={data['status']}")
if "lives" not in data or len(data["lives"]) == 0:
raise Exception("实时数据缺失 lives")
return data["lives"][0]
def fetch_forecast_weather():
url = f"https://restapi.amap.com/v3/weather/weatherInfo?city={CITY_ADCODE}&key={AMAP_KEY}&extensions=all"
resp = requests.get(url, timeout=10)
data = resp.json()
print("=== 预报天气(all)返回 ===")
print(data)
if data["status"] != "1":
raise Exception(f"预报接口失败 status={data['status']}")
if "forecasts" not in data or len(data["forecasts"]) == 0:
raise Exception("预报数据缺失 forecasts")
return data["forecasts"][0]["casts"]
def build_forecast_html(forecast_list):
html_parts = []
for i in range(4):
day = forecast_list[i]
w = int(day["week"])
if w == 7:
idx = 0
else:
idx = w
week_str = week_cn[idx]
item = f'''
<div class="forecast-item">
<div class="forecast-date">{day["date"]} {week_str}</div>
<div class="forecast-label">白天</div>
<div class="forecast-row">
<span class="forecast-icon">{weather_icon_map.get(day["dayweather"], "🌤")}</span>
<span>{day["daytemp"]}°</span>
</div>
<div class="forecast-label">夜间</div>
<div class="forecast-row">
<span class="forecast-icon">{weather_icon_map.get(day["nightweather"], "🌤")}</span>
<span>{day["nighttemp"]}°</span>
</div>
<div class="forecast-temp">{day["daywind"]} {day["daypower"]}级</div>
</div>
'''
html_parts.append(item)
return "\n".join(html_parts)
def build_todo_html():
# 读取todo.json
with open(TODO_JSON_PATH, "r", encoding="utf-8") as f:
todo_data = json.load(f)
html_parts = []
for text in todo_data["list"]:
html_parts.append(f'<div class="todo-item">{text}</div>')
return "\n".join(html_parts)
def get_date_info():
now = datetime.datetime.now()
date_str = now.strftime("%Y-%m-%d")
wd = now.weekday() + 1
if wd ==7:
week_str = "周日"
else:
week_str = week_cn[wd]
lunar_str = " 【91创客空间】"
return date_str, week_str, lunar_str
def main():
# 默认兜底模拟数据
live_data = {
"temperature":"--",
"weather":"未知",
"winddirection":"--",
"windpower":"0",
"humidity":"--",
"reporttime":"获取失败"
}
forecast_data = [
{"date":"07-24","week":"5","dayweather":"多云","daytemp":"28","daywind":"南风","daypower":"2",
"nightweather":"多云","nighttemp":"23","nightwind":"南风","nightpower":"1"},
{"date":"07-25","week":"6","dayweather":"晴","daytemp":"30","daywind":"东风","daypower":"2",
"nightweather":"晴","nighttemp":"24","nightwind":"东风","nightpower":"1"},
{"date":"07-26","week":"0","dayweather":"多云","daytemp":"29","daywind":"东南风","daypower":"2",
"nightweather":"多云","nighttemp":"24","nightwind":"东南风","nightpower":"1"},
{"date":"07-27","week":"1","dayweather":"小雨","daytemp":"26","daywind":"北风","daypower":"3",
"nightweather":"小雨","nighttemp":"22","nightwind":"北风","nightpower":"2"},
]
try:
live_data = fetch_live_weather()
forecast_data = fetch_forecast_weather()
except Exception as e:
print(f"⚠️ 天气接口异常:{e},使用兜底模拟数据")
forecast_html = build_forecast_html(forecast_data)
todo_html = build_todo_html()
date_str, week_str, lunar_str = get_date_info()
replace_map = {
"{{now_icon}}": weather_icon_map.get(live_data["weather"], "🌤"),
"{{now_temp}}": live_data["temperature"],
"{{now_weather}}": live_data["weather"],
"{{wind_text}}": f'{live_data["winddirection"]} ≤{live_data["windpower"]}级',
"{{humidity}}": live_data["humidity"],
"{{report_time}}": live_data["reporttime"],
"{{forecast_html}}": forecast_html,
"{{todo_html}}": todo_html,
"{{date_str}}": date_str,
"{{week_str}}": week_str,
"{{lunar_str}}": lunar_str
}
with open(TEMPLATE_PATH, "r", encoding="utf-8") as f:
template = f.read()
for k, v in replace_map.items():
template = template.replace(k, v)
with open(OUT_HTML, "w", encoding="utf-8") as f:
f.write(template)
print(f"✅ 已生成 {OUT_HTML}")
if __name__ == "__main__":
main()
6. Kindle图片转换to_kindle.py
import sys
import argparse
from pathlib import Path
from PIL import Image, ImageOps, ImageEnhance
def convert_for_oasis_fixed(input_path, output_path,
target_size=(600, 800),
contrast_factor=1.4, # 对比度系数,按需修改
brightness_factor=1.1): # 亮度系数
try:
with Image.open(input_path) as img:
# EXIF自动转正
img = ImageOps.exif_transpose(img)
w, h = img.size
print(f"处理中: {input_path} | Mode: {img.mode} | 校正后尺寸: {w}×{h}")
# 横图判定:宽 > 高,顺时针旋转90°
if w > h:
print(f"检测到横图 {w}×{h},顺时针旋转90°")
img = img.rotate(90, expand=True)
w, h = img.size
print(f"旋转后尺寸: {w}×{h}")
# 1. 强制转为 RGB
if img.mode != 'RGB':
img = img.convert('RGB')
# ========== 新增对比度、亮度增强 ==========
# 对比度增强
contrast_enhancer = ImageEnhance.Contrast(img)
img = contrast_enhancer.enhance(contrast_factor)
# 亮度增强(可选,如果图片足够亮可以设为1.0关闭)
brightness_enhancer = ImageEnhance.Brightness(img)
img = brightness_enhancer.enhance(brightness_factor)
# ===========================================
# 2. Resize 居中适配目标尺寸 (600,800)
img_fitted = ImageOps.fit(
img,
target_size,
method=Image.Resampling.LANCZOS,
centering=(0.5, 0.5)
)
# 3. 构建 16 色灰度调色板
palette_img = Image.new('P', (1, 1))
palette_data = []
for i in range(16):
val = int(i * 255 / 15)
palette_data.extend((val, val, val))
# 补齐256色所需长度
palette_data.extend([0] * (768 - len(palette_data)))
palette_img.putpalette(palette_data)
# 4. 量化 + Floyd-Steinberg 抖动
img_dithered_p = img_fitted.quantize(
palette=palette_img,
dither=Image.Dither.FLOYDSTEINBERG
)
# 5. 转回灰度L模式
final_img = img_dithered_p.convert('L')
# 6. 保存
final_img.save(output_path, "PNG", optimize=True)
print(f"✅ 完成: {output_path} | 输出尺寸 {final_img.size}\n")
except Exception as e:
print(f"❌ 错误: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Kindle图片预处理:EXIF校正+横图右转90°+对比度调节+16级灰度抖动")
parser.add_argument("input", help="输入图片路径")
parser.add_argument("-o", "--output", help="输出图片路径 (可选)")
parser.add_argument("-c", "--contrast", type=float, default=1.4, help="对比度系数,默认1.4")
parser.add_argument("-b", "--brightness", type=float, default=1.1, help="亮度系数,默认1.1")
args = parser.parse_args()
input_p = Path(args.input)
if not input_p.exists():
sys.exit("找不到输入文件")
if args.output:
output_p = Path(args.output)
else:
output_p = input_p.with_name(f"bg_ss00.png")
convert_for_oasis_fixed(
input_p,
output_p,
contrast_factor=args.contrast,
brightness_factor=args.brightness
)
五、Linux 端定时启动方案
方案选择:Systemd 常驻循环服务
- 循环脚本持续运行,每 5 分钟执行一次完整生成 + 推送流程
- 设置开机自动启动;程序异常自动重启,保障长期稳定运行
- 配套日志输出,方便排查执行故障
1. Linux 处理脚本run_dashboard.sh
脚本补全末尾换行、修正语法,保存为 /home/tony/today/run_dashboard.sh
#!/bin/bash
HTML_FILE="/home/tony/today/weather_dash.html"
TMP_PNG="/home/tony/today/ss_tmp_render.png"
python3 /home/tony/today/render_dash.py
wkhtmltoimage --width 800 --height 600 file://${HTML_FILE} ${TMP_PNG}
python3 /home/tony/today/to_kindle.py ${TMP_PNG}
rm -f ${TMP_PNG}
echo "[OK] 屏保图片生成完成"
赋予执行权限:
chmod +x /home/tony/today/run_dashboard.sh
2. Systemd 常驻服务
原理:服务开机启动,脚本内部写死循环,每 5 分钟执行一次整套流程。
步骤 1:新建循环启动脚本 loop_run.sh
#!/bin/bash
# 无限循环,每5分钟执行一次
while true; do
/home/tony/today/run_dashboard.sh >> /home/tony/today/dashboard_log.txt 2>&1
sleep 300
done
授权:
chmod +x /home/tony/today/loop_run.sh
步骤 2:创建 systemd 服务文件
sudo nano /etc/systemd/system/dashboard-screen.service
粘贴内容:
[Unit]
Description=Kindle Weather Dashboard Auto Render
After=network-online.target
Wants=network-online.target
[Service]
User=tony
WorkingDirectory=/home/tony/today
ExecStart=/home/tony/today/loop_run.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
步骤 3:启用开机自启 + 启动服务
# 重载systemd配置
sudo systemctl daemon-reload
# 设置开机自启动
sudo systemctl enable dashboard-screen.service
# 立即启动服务
sudo systemctl start dashboard-screen.service
常用管理命令
# 查看运行状态
systemctl status dashboard-screen.service
# 停止服务
sudo systemctl stop dashboard-screen.service
# 重启服务
sudo systemctl restart dashboard-screen.service
# 查看日志
tail -f /home/tony/today/dashboard_log.txt
✅ 优点:开机自动运行;网络异常崩溃会自动重启;日志统一管理。


5941

被折叠的 条评论
为什么被折叠?



