rack:刷机模式界面与事件;事件 21 新增 254「断电自动退出」
- 单板页「更多 → 刷机模式(1 小时)/ 加钟 30 分钟」,chg>0 时蓝色「刷机模式 · 充电中」、FLT 显示为「反向阻断」 - 协议白名单 flash_mode;事件 21 命名:开 / 手动关 / 到期 / 加钟 / 断电自动退出 - ⚠️ 254 带 0x80 位,必须先于「加钟」判断,否则显示成「加钟 126 分钟」 48 例全绿,已部署。 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
这个提交包含在:
@@ -188,6 +188,13 @@ def create_app(hub: Hub, rec: RecoveryManager, static_dir: Optional[str] = None,
|
|||||||
if d["arg"] == 3:
|
if d["arg"] == 3:
|
||||||
d["name"] += ":" + protocol.OTA_ERR_NAMES.get(d["mv"], str(d["mv"]))
|
d["name"] += ":" + protocol.OTA_ERR_NAMES.get(d["mv"], str(d["mv"]))
|
||||||
d["severe"] = d["arg"] in (3, 5)
|
d["severe"] = d["arg"] in (3, 5)
|
||||||
|
elif d["type"] == 21:
|
||||||
|
a = d["arg"]
|
||||||
|
d["name"] += "(%s)" % ("手动关" if a == protocol.CHG_EV_OFF else
|
||||||
|
"到期" if a == protocol.CHG_EV_EXPIRED else
|
||||||
|
"断电自动退出" if a == protocol.CHG_EV_DROPPED else
|
||||||
|
"加钟 %d 分钟" % (a & 0x7F) if a & protocol.CHG_EV_EXTEND else
|
||||||
|
"开 %d 分钟" % a)
|
||||||
elif d["type"] == 19:
|
elif d["type"] == 19:
|
||||||
failed = bool(d["arg"] & protocol.BOOT_CHECK_FAILED)
|
failed = bool(d["arg"] & protocol.BOOT_CHECK_FAILED)
|
||||||
d["name"] += "(按了 %d 次,%s)" % (d["arg"] & 0x0F, "没开起来" if failed else "开起来了")
|
d["name"] += "(按了 %d 次,%s)" % (d["arg"] & 0x0F, "没开起来" if failed else "开起来了")
|
||||||
|
|||||||
@@ -36,10 +36,16 @@ EVENT_TYPES: Dict[int, Tuple[str, str]] = {
|
|||||||
16: ("PD_LIMIT", "PD 限功率"),
|
16: ("PD_LIMIT", "PD 限功率"),
|
||||||
17: ("PG_TIMEOUT", "上电等不到 PG"), # A15:arg bit0 = FLT# 在报,bit1 = 按电压判
|
17: ("PG_TIMEOUT", "上电等不到 PG"), # A15:arg bit0 = FLT# 在报,bit1 = 按电压判
|
||||||
18: ("OTA", "固件升级"), # C2:arg 1 开始 / 2 就绪 / 3 失败(mV 列=原因) / 4 自证健康 / 5 回滚 / 6 为换固件重启
|
18: ("OTA", "固件升级"), # C2:arg 1 开始 / 2 就绪 / 3 失败(mV 列=原因) / 4 自证健康 / 5 回滚 / 6 为换固件重启
|
||||||
19: ("BOOT_CHECK", "开机重按"),
|
19: ("BOOT_CHECK", "开机重按"), # 0.1.2:arg bit0~3 = 按了几次,bit7 = 最终没开起来(电源键弹片接触不良)
|
||||||
20: ("BLADE_MUTE", "刀片不应答关路"), # 0.1.4:eFuse 开着、本路 INA226 + PCA9555 连续 200 ms 都不应答 # 0.1.2:arg bit0~3 = 按了几次,bit7 = 最终没开起来(电源键弹片接触不良)
|
20: ("BLADE_MUTE", "刀片不应答关路"), # 0.1.4:eFuse 开着、本路 INA226 + PCA9555 连续 200 ms 都不应答
|
||||||
|
21: ("CHG_MODE", "刷机模式"), # 0.1.5:arg 1~120 = 开(分钟),0 = 手动关,255 = 到期;0.1.6:0x80|分钟 = 加钟;0.1.13:254 = 断电自动退出
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# LOG_EV_CHG_MODE 的 arg(firmware/main/chgmode.h)。⚠️ 冻结。
|
||||||
|
CHG_EV_OFF, CHG_EV_EXPIRED, CHG_EV_EXTEND = 0, 255, 0x80
|
||||||
|
CHG_EV_DROPPED = 254 # 0.1.13:这一路断了电(刀片拔走 / 安全判据关路),自动退出。⚠️ 带 0x80 位,要先于「加钟」判
|
||||||
|
FLASH_MODE_MAX_MIN = 120
|
||||||
|
|
||||||
# LOG_EV_OTA 的 arg(firmware/main/ota.h 的 ota_ev_t)与失败原因(ota_err_t,失败那条的 mV 列)。⚠️ 冻结。
|
# LOG_EV_OTA 的 arg(firmware/main/ota.h 的 ota_ev_t)与失败原因(ota_err_t,失败那条的 mV 列)。⚠️ 冻结。
|
||||||
OTA_EV_NAMES = {1: "开始下载", 2: "已就绪", 3: "失败", 4: "新固件自证健康", 5: "回滚", 6: "为换固件重启",
|
OTA_EV_NAMES = {1: "开始下载", 2: "已就绪", 3: "失败", 4: "新固件自证健康", 5: "回滚", 6: "为换固件重启",
|
||||||
7: "计划内重启:接管、没断电"}
|
7: "计划内重启:接管、没断电"}
|
||||||
@@ -53,7 +59,7 @@ BOOT_CHECK_FAILED = 0x80 # LOG_EV_BOOT_CHECK 的 arg bit7
|
|||||||
# cmd_id_t(firmware/main/cmd.h),CMD_REJECT 事件的 arg 就是它。
|
# cmd_id_t(firmware/main/cmd.h),CMD_REJECT 事件的 arg 就是它。
|
||||||
CMD_NAMES = {1: "status", 2: "set_voltage", 3: "power_cycle", 4: "key", 5: "edl", 6: "vbus",
|
CMD_NAMES = {1: "status", 2: "set_voltage", 3: "power_cycle", 4: "key", 5: "edl", 6: "vbus",
|
||||||
7: "fan", 8: "events", 9: "power", 10: "wifi", 11: "mqtt", 12: "net",
|
7: "fan", 8: "events", 9: "power", 10: "wifi", 11: "mqtt", 12: "net",
|
||||||
13: "ota"}
|
13: "ota", 14: "flash_mode"}
|
||||||
|
|
||||||
KEYS = ("pwr", "vup", "vdn")
|
KEYS = ("pwr", "vup", "vdn")
|
||||||
KEY_MAX_MS = 15000
|
KEY_MAX_MS = 15000
|
||||||
@@ -104,6 +110,24 @@ def build_command(req: dict) -> dict:
|
|||||||
return {"cmd": cmd, "rail": _rail(req.get("rail")), "keys": keys, "ms": ms}
|
return {"cmd": cmd, "rail": _rail(req.get("rail")), "keys": keys, "ms": ms}
|
||||||
if cmd in ("vbus", "power"):
|
if cmd in ("vbus", "power"):
|
||||||
return {"cmd": cmd, "rail": _rail(req.get("rail")), "on": _bool(req.get("on"), "on")}
|
return {"cmd": cmd, "rail": _rail(req.get("rail")), "on": _bool(req.get("on"), "on")}
|
||||||
|
if cmd == "flash_mode":
|
||||||
|
# 0.1.5 刷机模式:VBUS 通着时允许手机充电、不断 USB。板子最多同时 2 路(多了回 state)。
|
||||||
|
# 0.1.6 加钟:{"add": 分钟},与 on / min 互斥;只给开着的窗口加,剩余最多 120 分钟(板子截断)。
|
||||||
|
if req.get("add") is not None:
|
||||||
|
a = req.get("add")
|
||||||
|
if not isinstance(a, int) or isinstance(a, bool) or not 1 <= a <= FLASH_MODE_MAX_MIN:
|
||||||
|
raise CmdError("加钟必须是 1~%d 分钟" % FLASH_MODE_MAX_MIN)
|
||||||
|
if req.get("on") is not None or req.get("min") is not None:
|
||||||
|
raise CmdError("加钟不能和 on / min 一起用")
|
||||||
|
return {"cmd": cmd, "rail": _rail(req.get("rail")), "add": a}
|
||||||
|
on = _bool(req.get("on"), "on")
|
||||||
|
out = {"cmd": cmd, "rail": _rail(req.get("rail")), "on": on}
|
||||||
|
if on and req.get("min") is not None:
|
||||||
|
m = req.get("min")
|
||||||
|
if not isinstance(m, int) or isinstance(m, bool) or not 1 <= m <= FLASH_MODE_MAX_MIN:
|
||||||
|
raise CmdError("刷机模式时长必须是 1~%d 分钟" % FLASH_MODE_MAX_MIN)
|
||||||
|
out["min"] = m
|
||||||
|
return out
|
||||||
if cmd == "fan":
|
if cmd == "fan":
|
||||||
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:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class SimRail:
|
|||||||
self.busy_until = 0
|
self.busy_until = 0
|
||||||
self.act = "idle"
|
self.act = "idle"
|
||||||
self.pending: Optional[str] = None # 序列结束时要做的事
|
self.pending: Optional[str] = None # 序列结束时要做的事
|
||||||
|
self.chg_until = 0 # 刷机模式截止(模拟时钟 ms),0 = 没开
|
||||||
|
|
||||||
|
|
||||||
class SimBoard:
|
class SimBoard:
|
||||||
@@ -132,7 +133,9 @@ class SimBoard:
|
|||||||
rails.append({"r": r.rail, "pr": r.present, "en": r.en, "vb": r.vb, "set": r.set_mv,
|
rails.append({"r": r.rail, "pr": r.present, "en": r.en, "vb": r.vb, "set": r.set_mv,
|
||||||
"mv": (r.set_mv if r.en else 0) if ok else None,
|
"mv": (r.set_mv if r.en else 0) if ok else None,
|
||||||
"ua": r.ua if ok else None, "pg": r.en if ok else None,
|
"ua": r.ua if ok else None, "pg": r.en if ok else None,
|
||||||
"flt": False if ok else None, "st": r.act, "res": "-"})
|
"flt": False if ok else None,
|
||||||
|
"chg": max(0, (r.chg_until - self.now + 999) // 1000) if r.chg_until else 0,
|
||||||
|
"st": r.act, "res": "-"})
|
||||||
return {"ok": True, "fw": self.fw, "up": self.up_ms, "in": {"mv": 12030, "ua": 1500000},
|
return {"ok": True, "fw": self.fw, "up": self.up_ms, "in": {"mv": 12030, "ua": 1500000},
|
||||||
"rails": rails, "temp": [35000, 36500],
|
"rails": rails, "temp": [35000, 36500],
|
||||||
"fan": {"duty": max(300, self.fan_floor), "floor": self.fan_floor, "en": True,
|
"fan": {"duty": max(300, self.fan_floor), "floor": self.fan_floor, "en": True,
|
||||||
@@ -231,7 +234,31 @@ class SimBoard:
|
|||||||
r.en = req["on"]
|
r.en = req["on"]
|
||||||
if not r.en:
|
if not r.en:
|
||||||
r.vb = False
|
r.vb = False
|
||||||
|
if r.chg_until > self.now:
|
||||||
|
r.chg_until = 0
|
||||||
|
self._ev(rail, 21, 0)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
if cmd == "flash_mode": # 固件 chgmode.h 的规则:最多 2 路、关着电不许开、关电一并退出
|
||||||
|
if "add" in req: # 0.1.6 加钟:只给开着的加,剩余截断到 120 分钟
|
||||||
|
if r.chg_until <= self.now:
|
||||||
|
return {"ok": False, "err": "state"}
|
||||||
|
r.chg_until = min(r.chg_until + req["add"] * 60000, self.now + 120 * 60000)
|
||||||
|
self._ev(rail, 21, 0x80 | req["add"])
|
||||||
|
return {"ok": True, "left": (r.chg_until - self.now + 999) // 1000}
|
||||||
|
if not req["on"]:
|
||||||
|
if r.chg_until > self.now:
|
||||||
|
self._ev(rail, 21, 0)
|
||||||
|
r.chg_until = 0
|
||||||
|
return {"ok": True, "left": 0}
|
||||||
|
if not r.en:
|
||||||
|
return {"ok": False, "err": "state"}
|
||||||
|
active = sum(1 for x in self.rails if x is not r and x.chg_until > self.now)
|
||||||
|
if active >= 2:
|
||||||
|
return {"ok": False, "err": "state"}
|
||||||
|
m = req.get("min", 60)
|
||||||
|
r.chg_until = self.now + m * 60000
|
||||||
|
self._ev(rail, 21, m)
|
||||||
|
return {"ok": True, "left": m * 60}
|
||||||
if cmd == "set_voltage":
|
if cmd == "set_voltage":
|
||||||
r.set_mv = req["mv"]
|
r.set_mv = req["mv"]
|
||||||
return {"ok": True, "set": r.set_mv}
|
return {"ok": True, "set": r.set_mv}
|
||||||
|
|||||||
@@ -185,3 +185,36 @@ def test_board_history_sums_rail_power(env):
|
|||||||
assert db.downsample(base + 180000) > 0
|
assert db.downsample(base + 180000) > 0
|
||||||
old = db.board_history(B, base - 10 * 86400000, base + 120000)
|
old = db.board_history(B, base - 10 * 86400000, base + 120000)
|
||||||
assert old["resolution"] == "minute" and old["points"][0][10] == 5900
|
assert old["resolution"] == "minute" and old["points"][0][10] == 5900
|
||||||
|
|
||||||
|
|
||||||
|
def test_flash_mode_round_trip_and_two_rail_limit(env):
|
||||||
|
c, hub, sim, db = env
|
||||||
|
post = lambda body: c.post("/api/boards/%s/cmd" % B, json=body).json()
|
||||||
|
for rail in (1, 2, 3):
|
||||||
|
assert post({"cmd": "power", "rail": rail, "on": True})["ok"] is True
|
||||||
|
r = post({"cmd": "flash_mode", "rail": 1, "on": True, "min": 10})
|
||||||
|
assert r["ok"] is True and r["left"] == 600
|
||||||
|
assert post({"cmd": "flash_mode", "rail": 2, "on": True})["ok"] is True
|
||||||
|
r = post({"cmd": "flash_mode", "rail": 3, "on": True})
|
||||||
|
assert r["ok"] is False and r["err"] == "state" # 第 3 路被板子拒
|
||||||
|
assert post({"cmd": "flash_mode", "rail": 1, "on": False})["left"] == 0
|
||||||
|
assert post({"cmd": "flash_mode", "rail": 3, "on": True})["ok"] is True # 腾出名额
|
||||||
|
st = post({"cmd": "status"})
|
||||||
|
assert [x["chg"] > 0 for x in st["rails"]][:3] == [False, True, True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_flash_mode_extend_round_trip(env):
|
||||||
|
c, hub, sim, db = env
|
||||||
|
post = lambda body: c.post("/api/boards/%s/cmd" % B, json=body).json()
|
||||||
|
assert post({"cmd": "power", "rail": 1, "on": True})["ok"] is True
|
||||||
|
r = post({"cmd": "flash_mode", "rail": 1, "add": 30})
|
||||||
|
assert r["ok"] is False and r["err"] == "state" # 没开着不能加钟
|
||||||
|
assert post({"cmd": "flash_mode", "rail": 1, "on": True})["left"] == 3600 # 默认 1 小时
|
||||||
|
assert post({"cmd": "flash_mode", "rail": 1, "add": 30})["left"] == 5400
|
||||||
|
assert post({"cmd": "flash_mode", "rail": 1, "add": 120})["left"] == 7200 # 截断
|
||||||
|
now = 10 ** 13
|
||||||
|
db.many("INSERT INTO events (board_id, seq, t_ms, rail, type, arg, mv, ua, received) VALUES (?,?,?,?,?,?,?,?,?)",
|
||||||
|
[(B, 9101, 1, 1, 21, 0x80 | 30, 0, 0, now), (B, 9102, 2, 2, 21, 254, 0, 0, now)])
|
||||||
|
ev = {e["seq"]: e for e in c.get("/api/events", params={"board": B}).json()}
|
||||||
|
assert "加钟 30 分钟" in ev[9101]["name"]
|
||||||
|
assert "断电自动退出" in ev[9102]["name"] # 0.1.13:254 带 0x80 位,不能被当成「加钟 126 分钟」
|
||||||
|
|||||||
@@ -302,3 +302,29 @@ def test_auto_recovery_ignores_rails_that_are_off(tmp_path):
|
|||||||
|
|
||||||
run(go())
|
run(go())
|
||||||
assert not [r for r in sim.received if r.get("cmd") in ("key", "power_cycle")]
|
assert not [r for r in sim.received if r.get("cmd") in ("key", "power_cycle")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_flash_mode_command_and_event_numbers():
|
||||||
|
# 固件 0.1.5:命令 14 / 事件 21(cmd.h / evlog.h 冻结编号)
|
||||||
|
assert protocol.CMD_NAMES[14] == "flash_mode" and protocol.EVENT_TYPES[21][0] == "CHG_MODE"
|
||||||
|
assert protocol.EVENT_TYPES[19][0] == "BOOT_CHECK" and protocol.EVENT_TYPES[20][0] == "BLADE_MUTE"
|
||||||
|
assert protocol.build_command({"cmd": "flash_mode", "rail": 2, "on": True}) == {"cmd": "flash_mode", "rail": 2, "on": True}
|
||||||
|
assert protocol.build_command({"cmd": "flash_mode", "rail": 2, "on": True, "min": 45})["min"] == 45
|
||||||
|
assert "min" not in protocol.build_command({"cmd": "flash_mode", "rail": 2, "on": False, "min": 5})
|
||||||
|
for bad in ({"cmd": "flash_mode", "rail": 2, "on": True, "min": 0},
|
||||||
|
{"cmd": "flash_mode", "rail": 2, "on": True, "min": 121},
|
||||||
|
{"cmd": "flash_mode", "rail": 2, "on": True, "min": True},
|
||||||
|
{"cmd": "flash_mode", "rail": 6, "on": True},
|
||||||
|
{"cmd": "flash_mode", "rail": 2}):
|
||||||
|
with pytest.raises(protocol.CmdError):
|
||||||
|
protocol.build_command(bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flash_mode_extend_command():
|
||||||
|
assert protocol.build_command({"cmd": "flash_mode", "rail": 1, "add": 30}) == {"cmd": "flash_mode", "rail": 1, "add": 30}
|
||||||
|
for bad in ({"cmd": "flash_mode", "rail": 1, "add": 0},
|
||||||
|
{"cmd": "flash_mode", "rail": 1, "add": 121},
|
||||||
|
{"cmd": "flash_mode", "rail": 1, "add": 30, "on": True},
|
||||||
|
{"cmd": "flash_mode", "rail": 1, "add": 30, "min": 10}):
|
||||||
|
with pytest.raises(protocol.CmdError):
|
||||||
|
protocol.build_command(bad)
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
<span :class="{ on: r.en }">直供{{ r.en ? '开' : '关' }}</span>
|
<span :class="{ on: r.en }">直供{{ r.en ? '开' : '关' }}</span>
|
||||||
<span :class="{ on: r.vb }">USB{{ r.vb ? '通' : '断' }}</span>
|
<span :class="{ on: r.vb }">USB{{ r.vb ? '通' : '断' }}</span>
|
||||||
<span :class="{ on: r.pg, bad: r.en && r.pg === false }">PG</span>
|
<span :class="{ on: r.pg, bad: r.en && r.pg === false }">PG</span>
|
||||||
<span :class="{ bad: r.flt }" v-if="r.flt">FLT</span>
|
<span v-if="r.flt && r.chg > 0" title="刷机模式下手机充电器把电池端顶高,eFuse 进入反向阻断,FLT 随之拉低:正常指示,不是故障">反向阻断</span>
|
||||||
|
<span :class="{ bad: r.flt }" v-else-if="r.flt">FLT</span>
|
||||||
|
<span class="chg" v-if="r.chg > 0" :title="'刷机模式:USB 通着时允许手机充电、不断 USB(固件 0.1.5)'">刷机 {{ Math.ceil(r.chg / 60) }} 分</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ar" @click.stop>
|
<div class="ar" @click.stop>
|
||||||
@@ -39,6 +41,8 @@
|
|||||||
<el-dropdown-item command="recovery">进 Recovery(音量上 + 电源)</el-dropdown-item>
|
<el-dropdown-item command="recovery">进 Recovery(音量上 + 电源)</el-dropdown-item>
|
||||||
<el-dropdown-item command="edl">进 EDL / 9008</el-dropdown-item>
|
<el-dropdown-item command="edl">进 EDL / 9008</el-dropdown-item>
|
||||||
<el-dropdown-item command="vbus" divided>{{ r?.vb ? '断开 USB' : '接通 USB' }}</el-dropdown-item>
|
<el-dropdown-item command="vbus" divided>{{ r?.vb ? '断开 USB' : '接通 USB' }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="flash">{{ r?.chg > 0 ? '退出刷机模式' : '刷机模式(1 小时)…' }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="r?.chg > 0" command="flash_add">刷机模式加钟 30 分钟</el-dropdown-item>
|
||||||
<el-dropdown-item command="power">{{ r?.en ? '断电(保持关闭)' : '上电' }}</el-dropdown-item>
|
<el-dropdown-item command="power">{{ r?.en ? '断电(保持关闭)' : '上电' }}</el-dropdown-item>
|
||||||
<el-dropdown-item command="volt">调电压…</el-dropdown-item>
|
<el-dropdown-item command="volt">调电压…</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
@@ -104,10 +108,25 @@ function more(c) {
|
|||||||
case 'edl': return run('进 EDL', { cmd: 'edl' }, true)
|
case 'edl': return run('进 EDL', { cmd: 'edl' }, true)
|
||||||
case 'vbus': return run(on.vb ? '断开 USB' : '接通 USB', { cmd: 'vbus', on: !on.vb }, on.vb)
|
case 'vbus': return run(on.vb ? '断开 USB' : '接通 USB', { cmd: 'vbus', on: !on.vb }, on.vb)
|
||||||
case 'power': return run(on.en ? '断电' : '上电', { cmd: 'power', on: !on.en }, on.en)
|
case 'power': return run(on.en ? '断电' : '上电', { cmd: 'power', on: !on.en }, on.en)
|
||||||
|
case 'flash': return flashMode(!(on.chg > 0))
|
||||||
|
case 'flash_add': return run('刷机模式加钟 30 分钟', { cmd: 'flash_mode', add: 30 }) // 板子截断到剩余 120 分钟
|
||||||
case 'volt': mv.value = on.set || 3900; voltOpen.value = true
|
case 'volt': mv.value = on.set || 3900; voltOpen.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 固件 0.1.5 刷机模式:窗口内 USB 通着也允许手机充电(节点被充电器顶到 ~4.4 V 不断 USB),
|
||||||
|
// 超过 4.45 V / 反灌 / 欠压照样保护;最多同时 2 路(板子回 state);1 小时到期自动恢复。
|
||||||
|
async function flashMode(on) {
|
||||||
|
if (!on) return run('退出刷机模式', { cmd: 'flash_mode', on: false })
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`${who()} 进入刷机模式 1 小时?期间 USB 接通时允许手机充电、不会因为电压被顶高而断 USB,` +
|
||||||
|
'电池端会停在约 4.4 V。最多同时 2 路;到期自动恢复。开完再「接通 USB」。',
|
||||||
|
'刷机模式', { type: 'warning', confirmButtonText: '进入' })
|
||||||
|
} catch { return }
|
||||||
|
return run('刷机模式', { cmd: 'flash_mode', on: true, min: 60 })
|
||||||
|
}
|
||||||
|
|
||||||
const voltOpen = ref(false)
|
const voltOpen = ref(false)
|
||||||
const mv = ref(3900)
|
const mv = ref(3900)
|
||||||
async function setVolt() {
|
async function setVolt() {
|
||||||
@@ -147,6 +166,7 @@ async function saveAuto(v) {
|
|||||||
.flags span { font-size: 12px; padding: 0 6px; border-radius: 4px; border: 1px solid var(--line); color: var(--muted); }
|
.flags span { font-size: 12px; padding: 0 6px; border-radius: 4px; border: 1px solid var(--line); color: var(--muted); }
|
||||||
.flags span.on { color: var(--ink); border-color: var(--ink-2); }
|
.flags span.on { color: var(--ink); border-color: var(--ink-2); }
|
||||||
.flags span.bad { color: var(--st-bad); border-color: var(--st-bad); }
|
.flags span.bad { color: var(--st-bad); border-color: var(--st-bad); }
|
||||||
|
.flags span.chg { color: var(--st-warn); border-color: currentColor; }
|
||||||
.ar { display: flex; align-items: center; gap: 6px; }
|
.ar { display: flex; align-items: center; gap: 6px; }
|
||||||
.acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: auto; }
|
.acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: auto; }
|
||||||
.acts .el-button + .el-button { margin-left: 0; }
|
.acts .el-button + .el-button { margin-left: 0; }
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ import { mA, V, W, C, ago, fmtTime, railsMw, railHealth, LEVEL_COLOR, RECOVERY_Z
|
|||||||
// 与 backend/prrack/protocol.py 的 EVENT_TYPES / EVENT_SEVERE 同一张表(数字来自固件 evlog.h)
|
// 与 backend/prrack/protocol.py 的 EVENT_TYPES / EVENT_SEVERE 同一张表(数字来自固件 evlog.h)
|
||||||
const EV = ['', '板子上电', '事件丢失', '过压关路', '欠压关路', '反灌断 VBUS', '顶高断 VBUS', 'eFuse 故障',
|
const EV = ['', '板子上电', '事件丢失', '过压关路', '欠压关路', '反灌断 VBUS', '顶高断 VBUS', 'eFuse 故障',
|
||||||
'PG 变化', 'I²C 失联', '过温降载', '风扇停转', '重启序列', '命令被拒', 'mux 失联全断', '刀片不在位', 'PD 限功率',
|
'PG 变化', 'I²C 失联', '过温降载', '风扇停转', '重启序列', '命令被拒', 'mux 失联全断', '刀片不在位', 'PD 限功率',
|
||||||
'上电等不到 PG', '固件升级', '开机重按', '刀片不应答关路']
|
'上电等不到 PG', '固件升级', '开机重按', '刀片不应答关路', '刷机模式']
|
||||||
const SEVERE = new Set([3, 4, 5, 6, 7, 9, 11, 14, 17, 20])
|
const SEVERE = new Set([3, 4, 5, 6, 7, 9, 11, 14, 17, 20])
|
||||||
// 开机重按(19)只有 arg bit7「最终没开起来」才算严重
|
// 开机重按(19)只有 arg bit7「最终没开起来」才算严重
|
||||||
const isSevere = ev => SEVERE.has(ev[3]) || (ev[3] === 19 && (ev[4] & 0x80) !== 0)
|
const isSevere = ev => SEVERE.has(ev[3]) || (ev[3] === 19 && (ev[4] & 0x80) !== 0)
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export function railHealth(board, r) {
|
|||||||
if (!r.pr) return { level: 'empty', text: '空槽' }
|
if (!r.pr) return { level: 'empty', text: '空槽' }
|
||||||
if (r.st && r.st !== 'idle') return { level: 'busy', text: ACT_ZH[r.st] || r.st }
|
if (r.st && r.st !== 'idle') return { level: 'busy', text: ACT_ZH[r.st] || r.st }
|
||||||
if (!r.en) return { level: 'off', text: '已断电' }
|
if (!r.en) return { level: 'off', text: '已断电' }
|
||||||
|
// 固件 ≥0.1.5 刷机模式:手机充电器把节点顶高时 eFuse(TPS25940A)进入反向阻断,FLT 随之拉低 ——
|
||||||
|
// 2026-09-28 台架实测与 RCB 同进同退,是正常指示,不是故障。模式外的 FLT 仍按故障标红。
|
||||||
|
if (r.chg > 0) return { level: 'busy', text: r.flt ? '刷机模式 · 充电中' : '刷机模式' }
|
||||||
if (r.flt) return { level: 'bad', text: 'eFuse 故障' }
|
if (r.flt) return { level: 'bad', text: 'eFuse 故障' }
|
||||||
if (r.pg === false) return { level: 'bad', text: 'PG 丢失' }
|
if (r.pg === false) return { level: 'bad', text: 'PG 丢失' }
|
||||||
if (r.mv == null || r.ua == null) return { level: 'warn', text: '读不到' }
|
if (r.mv == null || r.ua == null) return { level: 'warn', text: '读不到' }
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户