飞牛 fnOS 自动开关机 FPK 应用制作记录

背景

我的 NAS 平时并不需要 24 小时满负载运行,更多时候只是固定时间段用来同步、备份、下载和访问家庭服务。所以我一直希望它能像群晖 DSM 一样,在系统里直接配置“几点开机、几点关机”,不用每次手动操作,也不用为了一个固定任务让机器整天开着。

之前用群晖时,这个功能是系统自带的,入口清晰,交互也足够简单。但换到飞牛 fnOS 后,我在官方应用商店里没有找到类似的应用。底层其实并不复杂,Linux 可以通过 RTC wakealarm 做定时唤醒,通过 cron 或 systemd 做定时关机;问题是命令行方案虽然能用,但每次修改时间、检查状态、排查是否生效都不够方便。

于是我做了一个飞牛 FPK 应用,把命令行方案包装成一个类似群晖体验的图形界面应用:可以新增、编辑、启用、停用、删除开关机计划,并在 fnOS 桌面窗口里直接使用。

最终效果如下:

新增计划弹窗:

先验证命令行方案

做 GUI 之前,我先在 fnOS 上验证了最小可行方案。自动开机依赖主板 RTC 唤醒能力,核心是写入:

/sys/class/rtc/rtc0/wakealarm

比如每天 15:00 开机,可以先写一个脚本:

cat > /usr/local/bin/set-next-wake.sh << 'EOF'
#!/bin/bash
# 每天 15:00 开机
WAKE_HOUR="15:00"
NOW=$(date +%s)
TODAY_WAKE=$(date -d "today $WAKE_HOUR" +%s)

# 如果今天的开机时间还没到,就用今天;否则用明天
if [ "$NOW" -lt "$TODAY_WAKE" ]; then
    NEXT=$TODAY_WAKE
else
    NEXT=$(date -d "tomorrow $WAKE_HOUR" +%s)
fi

echo 0 > /sys/class/rtc/rtc0/wakealarm
echo "$NEXT" > /sys/class/rtc/rtc0/wakealarm
logger "RTC wake set to $(date -d @$NEXT '+%Y-%m-%d %H:%M:%S')"
EOF
chmod +x /usr/local/bin/set-next-wake.sh

然后注册一个 systemd 服务,在系统关机前刷新下一次 RTC 唤醒时间:

cat > /etc/systemd/system/rtc-wake.service << 'EOF'
[Unit]
Description=Set RTC wakeup alarm before shutdown
DefaultDependencies=no
Before=shutdown.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStop=/usr/local/bin/set-next-wake.sh

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable rtc-wake.service
systemctl start rtc-wake.service

定时关机可以直接写入 root crontab,例如每天 20:00 关机:

( crontab -l 2>/dev/null; echo "0 20 * * * /sbin/poweroff" ) | crontab -

检查是否生效:

cat /proc/driver/rtc | grep -E "alrm|alarm_IRQ"
systemctl is-enabled rtc-wake.service
crontab -l | grep poweroff
date -d @$(cat /sys/class/rtc/rtc0/wakealarm)

这里有一个很容易误判的点:/proc/driver/rtc 里看到的 alrm_time 可能是 UTC 时间,而不是你当前时区的显示时间。我的机器时区是 Asia/Shanghai,计划 15:00 开机时,date -d @$(cat /sys/class/rtc/rtc0/wakealarm) 显示的是 Fri Jun 19 03:00:00 PM CST 2026,但 /proc/driver/rtc 里看到的是 07:00:00。这不是错了,而是 UTC 和东八区的差异。

命令验证通过后,才值得继续做应用化。

从命令变成应用

我希望这个应用解决三个问题:

  • 不用手写 shell、systemd、crontab。
  • 能像群晖一样,通过表格管理多条计划。
  • 能在 fnOS 里以正常窗口方式打开,而不是跳到一个单独网页。

最初的界面参考了群晖的“硬件和电源 - 电源计划”交互,再用 OpenDesign 画了一版适合 fnOS 的界面稿:

最终应用去掉了和飞牛无关的“常规、硬盘休眠、不断电系统”等参考项,只保留“开关机计划”。交互上保留了常用的新增、编辑、删除、启用、停用、应用计划、测试开机计划。删除计划增加二次确认,避免误删。

技术栈选择尽量克制:

  • 后端:Python 3 标准库 HTTP server
  • 前端:原生 HTML / CSS / JavaScript
  • 系统写入:RTC wakealarm、systemd、root crontab
  • 打包:fnOS fnpack

没有用 Docker,也没有引入复杂依赖。原因很直接:这个应用必须写宿主机的 /sys/class/rtc/rtc0/wakealarm/etc/systemd/system 和 root crontab。对于这种强宿主权限的小工具,原生 FPK 比容器更合适。

核心实现思路

应用内部把计划保存成结构化 JSON,每条计划包含:

  • 操作:开机或关机
  • 时间:小时和分钟
  • 日期类型:每天、工作日、周末或自定义星期
  • 状态:启用或停用

应用计划时做两类写入。

第一类是关机计划,渲染成 root crontab。为了不污染用户已有 crontab,我用了托管区标记:

# BEGIN AUTOPOWER
...
# END AUTOPOWER

每次应用计划时,只替换这两个标记之间的内容,保留其它 crontab 任务。

第二类是开机计划。应用会计算下一次即将发生的开机时间,把 Unix timestamp 写入:

/sys/class/rtc/rtc0/wakealarm

同时安装一个 systemd shutdown hook,确保系统关机前重新计算下一次唤醒时间。否则如果只在页面点击“应用计划”时写一次 RTC,机器被唤醒后不一定会自动滚动到下一天的计划。

FPK 打包过程

FPK 这部分是这次最容易踩坑的地方。最开始我尝试手写包结构,安装时遇到过报错。后面改成用官方 fnpack create 重新生成工程,再把自己的前后端和脚本迁进去,稳定很多。

最终包结构大致是:

fpk/
├── manifest
├── config/
│   ├── privilege
│   └── resource
├── cmd/
│   ├── main
│   ├── install_init
│   ├── install_callback
│   ├── upgrade_init
│   ├── upgrade_callback
│   ├── uninstall_init
│   ├── uninstall_callback
│   ├── config_init
│   └── config_callback
├── app/
│   └── AutoPower/
└── ICON.PNG / ICON_256.PNG

前端页面、Python 服务和配置文件会同步到 fpk/app/AutoPower,再通过脚本构建:

FPK_VERSION=1.0.4 ./scripts/build-fpk.sh

产物会输出到:

dist-fpk/autopower-1.0.4.fpk

关键注意点

1. privilege 不要创建 root 用户

安装失败时,我遇到过类似“本地用户已存在”的问题。原因是 FPK 的权限配置里如果写了 username/groupname: root,安装器可能会尝试创建这个用户或用户组,而系统里 root 本来就存在。

这个应用需要 root 权限运行,但不应该创建 root 用户。最后保留为:

{
  "defaults": {
    "run-as": "root"
  }
}

这样应用以 root 身份运行,才能写 RTC、systemd 和 crontab,同时不会触发用户创建冲突。

2. fnOS 窗口模式不是普通 url

最开始应用可以打开,但它是单独跳到:

http://NAS_IP:3868/

这不像 fnOS 其它应用的窗口体验。后来确认入口配置里不能用普通新页面打开方式,而要配置成 iframe 窗口模式。关键配置类似:

{
  "type": "iframe",
  "protocol": "http",
  "port": "3868",
  "url": "/",
  "allUsers": true
}

改完后应用才会以 fnOS 桌面窗口打开。

3. 桌面快捷方式需要 app/ui/config

要让桌面出现应用图标,除了有图标文件,还要有桌面入口配置。关键点包括:

desktop_uidir=ui
desktop_applaunchname=autopower.Application
service_port=3868

如果覆盖安装后桌面入口没刷新,可以在 fnOS 上重启应用中心服务:

systemctl restart trim_app_center.service

4. 弹窗要适配小窗口

fnOS 应用窗口可以被用户拉得比较矮。新增计划里的日期选择 popup 如果按普通网页写,很容易出现下半部分被窗口裁掉、也无法滚动的问题。

最后需要给弹层做最大高度、滚动区域,并处理点击空白区域自动收起。否则在 NAS 桌面窗口里体验会明显不如系统应用。

5. 测试时间要按东八区显示

底层 RTC 写入的是 timestamp,系统 RTC 信息又可能显示 UTC。如果界面上直接展示底层字段,用户会误以为计划错了 8 小时。

所以界面里的“测试开机计划”和状态展示要明确按本地东八区时间显示;底部状态可以保留 RTC 原始信息,但要知道它和本地时间不是一回事。

6. 本地开发环境不能验证真实写入

这个项目可以在 macOS 上本地跑前后端:

python3 src/autopower_server.py \
  --host 127.0.0.1 \
  --port 3868 \
  --data-dir /tmp/autopower-dev \
  --web-dir web

但不要指望在 macOS 上验证“应用计划”真的写入系统,因为 macOS 没有 fnOS 的 /sys/class/rtc/rtc0/wakealarm、systemd 和 root crontab 环境。本地主要验证 UI、接口和计划计算逻辑;真实写入必须在 fnOS 上安装 FPK 后测。

安装和使用

源码和安装包放在 GitHub:

安装方式:

  1. 打开飞牛应用中心。
  2. 选择手动安装。
  3. 上传 autopower-1.0.4.fpk
  4. 安装完成后,从桌面图标打开“自动开关机”。
  5. 新增开机、关机计划,确认后点击“应用计划”。

我当前的使用方式是每天 15:00 自动开机,每天 20:00 自动关机。对于只在固定时间段使用的家庭 NAS 来说,这样既保留了自动化,也减少了无意义运行时间。

总结

这个应用本身不复杂,真正需要注意的是 fnOS FPK 的集成细节,以及 Linux RTC 定时唤醒和时区显示之间的差异。

命令行方案适合验证原理,但长期使用还是 GUI 更舒服。尤其是 NAS 这种家庭基础设施,很多功能不是“能不能用”的问题,而是“以后修改和排查是否方便”的问题。把脚本包装成一个飞牛桌面应用后,开关机计划就从一次性的命令配置,变成了可以长期维护的小工具。