Kindle基于linkss二次开发信息看板

一、项目概述

本项目实现 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/

文件名称

功能说明

template.html

网页模板,色块风格仪表盘;上下 6:4 布局,顶部天气区域、底部待办通栏(2 列 3 行共 6 条待办)

render_dash.py

核心生成脚本① 读取todo.json

待办清单② 请求高德 API 获取武汉天气预报③ 将数据填充进 html 模板,生成静态网页weather_dash.html

todo.json

待办事项数据源,可直接编辑修改清单内容

run_dashboard.sh

自动化执行脚本,串联整套流程:执行 python 渲染 → wkhtmltoimage 生成图片 → 推送 Kindle → 清理临时文件

to_kindle.py

将网页图片转成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 常驻循环服务

  1. 循环脚本持续运行,每 5 分钟执行一次完整生成 + 推送流程
  2. 设置开机自动启动;程序异常自动重启,保障长期稳定运行
  3. 配套日志输出,方便排查执行故障

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

✅ 优点:开机自动运行;网络异常崩溃会自动重启;日志统一管理。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值