文件
phonerail/rack
zhou yang和Claude Opus 5.5 7bf30e3c38 固件 0.1.0-c3:计划内重启不断电(OTA / 回滚锁住 EFUSE_EN,新固件接管;SR-6 改写)
- warm_esp.{h,c}:warm_restart() 记下开着的路(RTC_NOINIT,魔数 + 掩码 + 反码)→ gpio_hold 锁五根 EN → 重启;
  目标镜像没有能力标记(第 288 字节 PRCAPS1,.rodata_custom_desc)就不锁 —— 老固件写不动锁住的 EN。
  warm_take():软件复位 + 标记对得上才接管,一次性;再读 EN 真电平核对
- bringup:adopt 掩码;rail_en_init_keep() 接管的路写高、五根一律放开 hold(崩溃时旧锁可能还在);
  刀片不在 / mux 失联的接管路立刻关(SLOT_EMPTY arg 1);第二段已开着的路不重开、不等 PG
- pca9555_init_keep():方向寄存器是本固件写的才留 VBUS(掉过电的片子锁存器是 0xFF),键 / EDL 清零
- HAL:hal_gpio_hold();ESP 输出脚配 INPUT_OUTPUT(读得到真电平);sim 给 hold 建模
- 事件 LOG_EV_OTA arg 7 = 接管;兜底定时器回滚故意不走接管
- 顶层 CMake:phonerail.h 进 CMAKE_CONFIGURE_DEPENDS(否则镜像头版本停在旧值);-u g_phonerail_caps 防 gc
- test_bringup 加 6 例(接管 / 非计划复位全关 / 空槽 / mux 失联 / PCA 掉过电 / hold 冻结),全工程 448 例全绿
- rack:固件库识别能力标记,推送对话框按「支持 / 不支持重启不断电」给不同提示(已部署)
- 需求文档:SR-6 改为「非计划复位全关」+ 计划内重启例外;§10.9 新增「重启不断电」
- 板 B:台架镜像(空槽强制 CH1 在位,未提交)开 CH1 → OTA 带重启 → 事件 18/7 mv=1,CH1 一直开着;
  现跑 0.1.0-c3。未测:示波器看重启期间 EN / 输出毛刺(要插刀片)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 14:35:52 +08:00
..

PhoneRail 机架管理(服务器前后端)

需求文档 D7 的实现:板子经 WiFi → MQTT(TLS)连到 home 服务器,这里把遥测存下来、画出来, 让人在网页上操作每一台手机,并按电流判据做自动崩溃恢复(先软后硬)。

板子 ──mqtts 8883──▶ mosquitto(evse 共用)──127.0.0.1:1884──▶ 后端 prrack(127.0.0.1:7764)
                                                                  │  SQLite(WAL)
浏览器 ──▶ rack.home.example:7755(统一门户,auth_request)──────────┘  同进程托管前端 dist
目录 内容
backend/prrack/ protocol.py 板侧协议镜像(命令白名单 / 事件表 / seq)· bus.py MQTT 抽象(paho / 进程内假 broker)· hub.py 核心 · recovery.py 自动恢复 · db.py 存储 · firmware.py 固件库 + OTA 命令 · api.py HTTP+WS · simboard.py 模拟板子 · main.py 入口
backend/tests/ 39 例(pytest),本机与服务器(Python 3.8)都跑
frontend/ Vue3 + Vite + Element Plus + ECharts(按需引入)
deploy/ deploy.sh 一键部署 · systemd unit · 每日备份

规矩(都来自板侧协议 §10.8,改代码前先看)

  1. 重传用同一个 seq(Hub.call)。换 seq 重发 = 「按 10 秒电源键」可能执行两遍。测试 test_retransmit_reuses_the_same_seq… 钉着。
  2. 失联不动作:板子离线 / 遥测中断只标状态;自动恢复只在「开了 + 在线 + 在位 + 有电 + 执行器空闲」时判定。
  3. 时间用服务器收到的时刻(板子没有墙钟),事件表另存板子的 t_ms 作参考。
  4. 只绑 127.0.0.1:X-Auth-User(门户覆盖式注入)按 L2 信任,只用于审计「谁按的」;main.py 拒绝非回环绑定。
  5. 命令白名单在 protocol.build_command:wifi / mqtt(只收 USB)与 events(服务器自己同步)不给界面。板子的校验仍是唯一权威。

推固件(OTA,板侧 C2)

界面「固件」页:上传 firmware/build/phonerail.bin → 选板子推送。进度、待验证、回滚都直接显示遥测里的 ota 一块。

  • 上传时后端核镜像头:首字节 0xE9、有 app 描述、project_name == phonerail、≤3 MB,不合格直接拒(不用等板子下完 1 MB 再报错)。 存成 /root/phonerail/fw/<sha256 前 16 位>.bin,由 nginx 7765 放出去(deploy/nginx-phonerail-fw.conf,故意不过门户: 板子过不了门禁;完整性靠命令里的 sha256,镜像里没有机密)。
  • 推送 = POST /api/boards/{id}/ota {"sha256","reboot"},进审计。通用 /cmd 不放行 ota:URL / 哈希只能由固件库生成。
  • ⚠️ 勾「立刻重启」= 这块板上开着的手机全部硬断电一次(ESP 复位时 EN 下拉),界面会列出哪几路并二次确认。 不勾就只下载 + 切分区,下次复位生效;同一个镜像已就绪时再推一次并勾重启,不重下。
  • 新固件 120 s 内连不上 broker 板子自己回滚,界面显示「回滚过:版本号」,事件里有「固件升级(回滚)」。
  • 命令行等价物:firmware/tools/ota_push.py(不经过后端,直接用 broker 后端账号)。

本机开发

cd backend && python -m prrack.main --sim 2 --db dev.db --static ../frontend/dist --fw-dir ./fwdev   # 两块模拟板,第 1 块第 2 路「卡死」;--fw-dir 打开固件页
cd frontend && pnpm dev                                                             # http://localhost:5173,/api 反代到 7764
cd backend && python -m pytest -q tests

部署 / 运维

bash rack/deploy/deploy.sh          # 构建 → 传 → 服务器上 Python 3.8 跑测试(不过不换)→ 重启
项 值
服务 systemd phonerail-rack,/root/phonerail-rack/{backend,frontend/dist,venv,data}
日志 /var/log/phonerail-rack.log
库 /root/phonerail-rack/data/rack.db;每日 03:40 sqlite3 .backup 到 backup/,留 14 份
MQTT phonerail-backend @ 127.0.0.1:1884,口令读 /root/phonerail/mqtt.env
门户 应用 id 16,key rack、端口 7764(phonerail 已被需求文档站占用);授权 admin + staff,不给 viewer(没有只读模式,能断电重启)
保留期 原始 1 s 遥测 2 天;分钟聚合 90 天;事件 / 审计 / 恢复记录永久

加一块新板:USB 配网(firmware/tools/prlink.py 发 wifi / mqtt)+ 服务器上给它开 MQTT 账号(见 memory 里的 broker 说明),上线后自动出现在总览。

踩过的坑

  • uvicorn 没装 websockets 时不报错:/api/ws 被当成普通 GET 落到 SPA 兜底,回 200 HTML,页面能开但永不刷新。 TestClient 自带 WS 实现测不出来 ⇒ main.py 启动自检 + test_a_real_websocket_library_is_installed(部署时在服务器上跑)。
  • 服务器 Python 3.8 + 老 pip:starlette 最高 0.44.0、websockets 13.1;新版本在清华镜像上直接「找不到」。
  • 本机有 HTTP 代理(HTTP_PROXY=127.0.0.1:7890),经它访问不存在的公网端口会回 502 而不是连不上;验「端口没暴露」要加 --noproxy '*'。
  • 门户的 /admin/grants 是整张矩阵覆盖写:脚本化加授权要先读出现有全部授权再加新的。改门户前先备份 portal.db。
  • 门户进程把应用表缓存在内存,直接改库它看不见(没有重载信号,只能重启);走它自己的 /admin/apps 接口会自动 reloadAcl()。

已知限制 / 下一步

  • 自动恢复阈值没有实测依据(平线 5 mA / 过低 30 mA / 窗口 5 分钟)。插上刀片和手机后先看几天曲线再定,默认逐台关闭。
  • adb 探活:接口 recovery.Prober 已留,服务器目前够不着手机(机架在 192.168.8.x 的 NAT 后面)。
  • 前端 Element Plus 是整包引入(gzip ≈335 KB),手机首屏偏慢;改成按需引入可以再砍一半。
  • 没有细分权限:门户授权了这个应用的人都能操作;审计记着是谁。