rack:「固件」页 —— 上传 .bin、选板推送 OTA、实时看进度 / 待验证 / 回滚
- firmware.py:固件库。上传时核 ESP 镜像头(0xE9 + app 描述 + project_name == phonerail + ≤3 MB),
存成 /root/phonerail/fw/<sha 前 16 位>.bin(nginx 7765 放出去),库里记版本 / 编译时间 / 上传人
- api:GET/POST/DELETE /api/firmware(请求体即 .bin,不引 multipart)、POST /api/boards/{id}/ota(进审计);
通用 /cmd 不放行 ota —— URL 与哈希只能由固件库生成;OTA 事件按 arg 显示中文、失败 / 回滚标红
- 前端:Firmware.vue + OtaState.vue;推送对话框默认不重启,勾「立刻重启」时列出每块板会断电的路并二次确认;
单板页显示 OTA 状态
- simboard 模拟 ota(下载进度 / 就绪 / 重启 / 自证);test_firmware.py 13 例,后端共 39 例(本机 + 服务器 3.8)
- service 加 --fw-dir;已部署。板 B 实测:上传 → 推送 → 就绪 → 同镜像带重启 → 不重下直接重启 → 自证健康
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+52
-1
@@ -17,6 +17,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import protocol
|
||||
from .db import now_ms
|
||||
from .firmware import SLOT_BYTES, FwError, FwStore
|
||||
from .hub import CmdTimeout, Hub
|
||||
from .recovery import Params, RecoveryManager
|
||||
|
||||
@@ -34,7 +35,8 @@ Hook = Callable[[], Awaitable[None]]
|
||||
|
||||
|
||||
def create_app(hub: Hub, rec: RecoveryManager, static_dir: Optional[str] = None,
|
||||
startup: Optional[List[Hook]] = None, shutdown: Optional[List[Hook]] = None) -> FastAPI:
|
||||
startup: Optional[List[Hook]] = None, shutdown: Optional[List[Hook]] = None,
|
||||
fw: Optional[FwStore] = None) -> FastAPI:
|
||||
# 用 lifespan 而不是 on_event:后者在新版 Starlette 里已经删了,服务器上的旧版两种都认。
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
@@ -111,6 +113,50 @@ def create_app(hub: Hub, rec: RecoveryManager, static_dir: Optional[str] = None,
|
||||
_board_or_404(hub, board)
|
||||
return hub.db.history(board, rail, start, end or now_ms())
|
||||
|
||||
# ---- 固件 / OTA(firmware.py)---------------------------------------------
|
||||
def _fw() -> FwStore:
|
||||
if fw is None:
|
||||
raise HTTPException(503, "后端没有配置固件目录(--fw-dir)")
|
||||
return fw
|
||||
|
||||
@app.get("/api/firmware")
|
||||
def fw_list():
|
||||
return {"items": _fw().list(), "slot": SLOT_BYTES}
|
||||
|
||||
@app.post("/api/firmware")
|
||||
async def fw_upload(req: Request, filename: str = ""):
|
||||
# 请求体就是 .bin 原样(application/octet-stream),不走 multipart:省一个依赖(python-multipart)
|
||||
store = _fw()
|
||||
data = await req.body()
|
||||
try:
|
||||
row = store.add(data, filename, _user(req))
|
||||
except FwError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
hub.broadcast({"t": "firmware"})
|
||||
return row
|
||||
|
||||
@app.delete("/api/firmware/{sha}")
|
||||
def fw_delete(sha: str):
|
||||
if not _fw().delete(sha):
|
||||
raise HTTPException(404, "固件库里没有这个镜像")
|
||||
hub.broadcast({"t": "firmware"})
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/api/boards/{board}/ota")
|
||||
async def ota(board: str, req: Request):
|
||||
"""推固件。body = {"sha256": ..., "reboot": false}。回应是板子的原话(act = start / reboot / ready),
|
||||
进度看遥测里的 `ota` 一块。⚠️ reboot=true 会让这块板上的五台手机断电一次。"""
|
||||
_board_or_404(hub, board)
|
||||
body = await req.json()
|
||||
try:
|
||||
cmd = _fw().command(body.get("sha256"), body.get("reboot", False))
|
||||
except FwError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
try:
|
||||
return await hub.call(board, cmd, _user(req))
|
||||
except CmdTimeout as e:
|
||||
raise HTTPException(504, str(e))
|
||||
|
||||
# ---- 事件 / 审计 / 恢复 ----------------------------------------------------
|
||||
@app.get("/api/events")
|
||||
def events(board: Optional[str] = None, limit: int = 200, before: Optional[int] = None):
|
||||
@@ -131,6 +177,11 @@ def create_app(hub: Hub, rec: RecoveryManager, static_dir: Optional[str] = None,
|
||||
d.update(code=code, name=zh, severe=d["type"] in protocol.EVENT_SEVERE)
|
||||
if d["type"] == 13:
|
||||
d["name"] += "(%s)" % protocol.CMD_NAMES.get(d["arg"], d["arg"])
|
||||
elif d["type"] == 18:
|
||||
d["name"] += "(%s)" % protocol.OTA_EV_NAMES.get(d["arg"], d["arg"])
|
||||
if d["arg"] == 3:
|
||||
d["name"] += ":" + protocol.OTA_ERR_NAMES.get(d["mv"], str(d["mv"]))
|
||||
d["severe"] = d["arg"] in (3, 5)
|
||||
out.append(d)
|
||||
return out
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户