rack:自动恢复的平线判据按实测重定(p95−p05 + 中位数下限)

原判据「窗口内 max − min < 5 mA」抓不到真卡死:10-04 第 1 路平在 520 mA 五个半小时,
偶发尖峰让 590 个窗口的 max − min 都在 17 mA 以上,一个都判不出来。

- 平线改成:p95 − p05 < flat_ua(30 mA)且中位数 ≥ flat_min_ua(100 mA,新增参数)。
  下限是给空闲低功耗留的:睡着的手机平在 ~10 mA 是正常的。
- 过低不变(窗内最大值 < 30 mA):睡着的手机每 25 s 被长轮询叫醒,窗内最大值都在 213 mA 以上。
- 真实遥测回放:两次真卡死(10-04 第 1 路、10-05 09:40 第 5 路平在 382 mA)都判出;
  亮屏常醒 ~26 h × 4 路、空闲睡着 ~6 h × 5 路、试验 / OTA 期间零误判。
- 测试 +4(带尖峰的平线 / 睡着是健康的 / 睡下去再没醒 = 过低 / 低位平线不下结论),恢复页加「中位数不低于」。

依据和局限写在 recovery.py 模块头。已部署;五路的自动恢复开关仍然全关,还没真正触发过一次。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-06 05:14:22 +08:00
共同撰写人 Claude Opus 5.5
父节点 d1fab040cc
当前提交 ca7435242f
共修改 5 个文件,包含 65 行新增和 15 行删除
+29 -9
查看文件
@@ -1,12 +1,28 @@
"""自动崩溃恢复(需求 §4.1 两级重启:先软后硬),2026-09-27 定为「先只看电流」。
服务器够不到手机的 adb(手机在机架那边的 NAT 后面),所以这一版的判据只来自板子遥测:
· FLAT:窗口内电流几乎不变(max − min < flat_ua)且不低 —— 卡死的手机常见的样子;
· LOW :窗口内电流一直低于 low_ua —— 关机 / 掉到待机,却本该在跑。
· FLAT:窗口内电流平在一个不低的水平上 —— **中位数 ≥ flat_min_ua 且 p95 − p05 < flat_ua**。卡死的手机就是这样;
· LOW :窗口内电流一直低于 low_ua —— 关机,或者睡下去之后再也没醒过(代理不轮询了)。
adb 探活留了接口(`Prober`),手机能从服务器够到(比如经 frp)时再接上。
⚠️ 这两条阈值**还没有实测依据**(刀片还没插、手机还没上架),默认值是按常识填的。
所以自动恢复**默认关闭、逐台打开**,而且:
阈值的依据(2026-10-06,板 B 五台小米8 的 1 Hz 遥测,300 s 窗口每 30 s 滑一次):
| 状态 | 窗口数 | 中位数 | p95 − p05 | max − min | 窗内最大值 |
|---|---|---|---|---|---|
| 真卡死(10-04 第 1 路,平在 520 mA 五个半小时) | 590 | 518~524 mA | 5~20 mA | 17~189 mA | — |
| 健康、亮屏常醒(10-04 17:00 ~ 10-05 20:00,第 1~4 路) | 4×3187 | ≥ 213 mA | ≥ 42 mA | ≥ 141 mA | — |
| 健康、空闲睡着(10-05 23:15 起 5.8 h,五路) | 5×665 | 7~12 mA | ≥ 8 mA | ≥ 213 mA | ≥ 221 mA |
· 平线原来用 max − min < 5 mA:真卡死时偶尔冒一个尖峰,那 590 个窗口**一个都判不出来**。换成对尖峰不敏感的
p95 − p05,卡死 ≤ 20 mA、健康亮屏 ≥ 42 mA,取 30 mA。
· 平线还要「不低」(中位数 ≥ 100 mA):空闲睡着的手机 95 % 的时间都平在 ~10 mA(ROM 文档 §4.18),那是正常的。
卡死的手机 CPU 是醒着的(见过 254 / 258 / 365 / 520 mA),不会掉到那么低。
· 过低仍是「窗内最大值 < 30 mA」:睡着的手机每 25 s 被代理的长轮询叫醒一次,窗内最大值都在 221 mA 以上;
5 分钟里一个尖峰都没有 = 关机了,或者睡死了。
⚠️ 样本只有**一次**真卡死和一晚上的睡眠数据;过低那一条没有真实的关机样本。还没开过一次自动恢复。
⚠️ 熄屏但 WiFi 省电没还回去(旧 ROM / rc 触发器没生效)时会平在 ~178 mA,会被判成平线 —— 那本身也是该处理的异常。
所以自动恢复仍然**默认关闭、逐台打开**,而且:
· 只在「这台手机开了自动恢复 + 板子在线 + 刀片在位 + 这一路有电 + 执行器空闲」时才判定,
任何一条不满足就把积累的历史清掉 —— 失联不动作(§10.2);
· 每小时最多 `max_per_hour` 次,超了就 GAVE_UP 停手、告警,要人来复位 ——
@@ -33,8 +49,9 @@ IDLE, SOFT, HARD, GAVE_UP = "idle", "soft", "hard", "gave_up"
@dataclass
class Params:
window_s: int = 300 # 判定窗口
flat_ua: int = 5000 # 窗口内电流摆幅小于它 = 平线(5 mA)
low_ua: int = 30000 # 窗口内电流一直低于它 = 像关机(30 mA)
flat_ua: int = 30000 # 平线:窗口内 p95 − p05 小于它(30 mA)……
flat_min_ua: int = 100000 # ……并且中位数不低于它(100 mA):睡着的手机平在 ~10 mA 是正常的
low_ua: int = 30000 # 窗口内电流一直低于它 = 像关机 / 睡死(30 mA)
settle_s: int = 120 # 动作之后先等它重启完,这段不看
max_per_hour: int = 3 # 每台每小时最多动作几次
soft_ms: int = 10000 # 一级:长按电源键时长(需求 §4.1 ≈10 s)
@@ -78,10 +95,13 @@ class Detector:
def verdict(self, now: int) -> Optional[str]:
if not self.full(now):
return None
vals = [u for _, u in self.s]
if max(vals) < self.p.low_ua:
vals = sorted(u for _, u in self.s)
n = len(vals)
if vals[-1] < self.p.low_ua:
return "low"
if max(vals) - min(vals) < self.p.flat_ua:
# 用分位数而不是 max − min:卡死时偶发的一个尖峰不该把平线判据废掉(依据见模块头)
spread = vals[n * 95 // 100] - vals[n * 5 // 100]
if vals[n // 2] >= self.p.flat_min_ua and spread < self.p.flat_ua:
return "flat"
return None