rack:fan 命令放行保持时间 sec,风扇断电显示「停」

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-06 14:35:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 ece65f7c19
当前提交 f2e0a7363e
共修改 5 个文件,包含 30 行新增和 5 行删除
+2 -1
查看文件
@@ -77,7 +77,8 @@ bash rack/deploy/deploy.sh # 构建 → 传 → 服务器上 Python 3.8
| 门户 | 应用 id 16,key `rack`、端口 7764(`phonerail` 已被需求文档站占用);授权 admin + staff,**不给 viewer**(没有只读模式,能断电重启) | | 门户 | 应用 id 16,key `rack`、端口 7764(`phonerail` 已被需求文档站占用);授权 admin + staff,**不给 viewer**(没有只读模式,能断电重启) |
| 保留期 | 原始 1 s 遥测 2 天;分钟聚合 90 天;事件 / 审计 / 恢复记录永久 | | 保留期 | 原始 1 s 遥测 2 天;分钟聚合 90 天;事件 / 审计 / 恢复记录永久 |
加一块新板:USB 配网(`firmware/tools/prlink.py` 发 `wifi` / `mqtt`)+ 服务器上给它开 MQTT 账号(见 memory 里的 broker 说明),上线后自动出现在总览。 加一块新板:配网(固件 ≥0.1.19:手机连板子自己开的热点 `PhoneRail-xxxxxx`,在 192.168.4.1 网页里填 WiFi 与 MQTT,
热点口令 `88888888`;或 USB:`firmware/tools/prlink.py` 发 `wifi` / `mqtt`)+ 服务器上给它开 MQTT 账号(见 memory 里的 broker 说明),上线后自动出现在总览。
## 踩过的坑 ## 踩过的坑
+9 -1
查看文件
@@ -172,7 +172,15 @@ def build_command(req: dict) -> dict:
duty = req.get("duty") duty = req.get("duty")
if not isinstance(duty, int) or isinstance(duty, bool) or not 0 <= duty <= 1000: if not isinstance(duty, int) or isinstance(duty, bool) or not 0 <= duty <= 1000:
raise CmdError("风扇下限必须是 0~1000‰(0 = 自动)") raise CmdError("风扇下限必须是 0~1000‰(0 = 自动)")
return {"cmd": cmd, "duty": duty} out = {"cmd": cmd, "duty": duty}
# 固件 ≥0.1.21:`sec` = 这个下限保持多少秒,到期自动回落到自动(没给 = 一直保持)。
sec = req.get("sec")
if sec is not None:
if not isinstance(sec, int) or isinstance(sec, bool) or not 0 <= sec <= 86400:
raise CmdError("风扇下限的保持时间必须是 0~86400 秒(0 / 不给 = 一直保持)")
if sec:
out["sec"] = sec
return out
raise CmdError("不支持的命令:%r" % (cmd,)) raise CmdError("不支持的命令:%r" % (cmd,))
+4 -1
查看文件
@@ -185,7 +185,10 @@ class SimBoard:
return {"ok": True, "oldest": 0, "next": len(self.events), "ev": ev} return {"ok": True, "oldest": 0, "next": len(self.events), "ev": ev}
if cmd == "fan": if cmd == "fan":
self.fan_floor = req["duty"] self.fan_floor = req["duty"]
return {"ok": True, "floor": self.fan_floor} rsp = {"ok": True, "floor": self.fan_floor}
if "sec" in req:
rsp["sec"] = req["sec"] # 模拟板不倒计时:只把形状回对
return rsp
if req.get("seq") is None: if req.get("seq") is None:
return {"ok": False, "err": "no_seq"} return {"ok": False, "err": "no_seq"}
if cmd == "hub": # 0.1.14:固件 hubctl.h 的命令形状;不模拟倒计时 if cmd == "hub": # 0.1.14:固件 hubctl.h 的命令形状;不模拟倒计时
+12
查看文件
@@ -59,12 +59,24 @@ def test_build_command_whitelist_and_ranges():
{"cmd": "power", "rail": True, "on": True}, # bool 不是 int {"cmd": "power", "rail": True, "on": True}, # bool 不是 int
{"cmd": "vbus", "rail": 1, "on": 1}, {"cmd": "vbus", "rail": 1, "on": 1},
{"cmd": "fan", "duty": 1001}, {"cmd": "fan", "duty": 1001},
{"cmd": "fan", "duty": 500, "sec": 86401},
{"cmd": "fan", "duty": 500, "sec": -1},
{"cmd": "fan", "duty": 500, "sec": "60"},
{"cmd": "fan", "duty": 500, "sec": True},
] ]
for b in bad: for b in bad:
with pytest.raises(protocol.CmdError): with pytest.raises(protocol.CmdError):
protocol.build_command(b) protocol.build_command(b)
def test_fan_floor_may_carry_a_hold_time():
"""固件 0.1.21:`sec` 原样带下去;0 / 不给 = 一直保持,命令里就不出现这个键(老固件不认它也没关系)。"""
assert protocol.build_command({"cmd": "fan", "duty": 600}) == {"cmd": "fan", "duty": 600}
assert protocol.build_command({"cmd": "fan", "duty": 600, "sec": 0}) == {"cmd": "fan", "duty": 600}
assert protocol.build_command({"cmd": "fan", "duty": 600, "sec": 90}) == {"cmd": "fan", "duty": 600, "sec": 90}
assert protocol.build_command({"cmd": "fan", "duty": 0, "sec": 86400}) == {"cmd": "fan", "duty": 0, "sec": 86400}
def test_seq_is_strictly_increasing_and_in_range(): def test_seq_is_strictly_increasing_and_in_range():
t = [1700000000.0] t = [1700000000.0]
g = protocol.SeqGen(clock=lambda: t[0]) g = protocol.SeqGen(clock=lambda: t[0])
+3 -2
查看文件
@@ -46,9 +46,10 @@
<div class="g"><div class="gh"><span class="gk">风扇</span> <div class="g"><div class="gh"><span class="gk">风扇</span>
<el-select v-model="fanFloor" size="small" class="floor" aria-label="风扇占空比下限" @change="setFan"> <el-select v-model="fanFloor" size="small" class="floor" aria-label="风扇占空比下限" @change="setFan">
<el-option :value="0" label="自动" /> <el-option :value="0" label="自动" />
<el-option v-for="d in [500, 700, 850, 1000]" :key="d" :value="d" :label="`不低于 ${d / 10}%`" /> <el-option v-for="d in [300, 500, 700, 850, 1000]" :key="d" :value="d" :label="`不低于 ${d / 10}%`" />
</el-select></div> </el-select></div>
<div class="gvs"><span class="gv readout">{{ (b.status.fan?.duty ?? 0) / 10 }}<small>%</small></span> <div class="gvs"><span v-if="b.status.fan?.en === false" class="gv readout" title="板温低于 40 °C:风扇断电(固件 ≥0.1.21)">停</span>
<span v-else class="gv readout">{{ (b.status.fan?.duty ?? 0) / 10 }}<small>%</small></span>
<span v-for="(r, i) in b.status.fan?.rpm || []" :key="i" class="gv sm readout" :class="{ bad: b.status.fan?.stall?.[i] }">{{ r ?? '—' }}<small>rpm</small></span></div></div> <span v-for="(r, i) in b.status.fan?.rpm || []" :key="i" class="gv sm readout" :class="{ bad: b.status.fan?.stall?.[i] }">{{ r ?? '—' }}<small>rpm</small></span></div></div>
<div class="g"><div class="gh"><span class="gk">巡检</span></div> <div class="g"><div class="gh"><span class="gk">巡检</span></div>
<div class="gvs"><span class="gv readout">{{ b.status.safety?.cycles }}<small>圈</small></span> <div class="gvs"><span class="gv readout">{{ b.status.safety?.cycles }}<small>圈</small></span>