"""每台手机「自己说的」那一半:在不在线、在不在跑活、主板 / CPU 温度。 板子只知道这一路有没有电、多大电流;手机主板多热、代理还活没活着,是机架代理(PhoneRailAgent) 向 AppTask 后端(127.0.0.1:7736)报的。这里每隔几秒去那边读一次 `/api/fleet/phones`, 按机架位对到「哪块板的第几路」,放进板子快照的 `phones[].live`。 规矩: 1. **只读、只显示。** 这些数不参与任何判据 —— 自动恢复仍然只看电流(recovery.py),不看手机温度也不看它在不在线。 2. **读不到就是没有。** AppTask 后端挂了 / 令牌不对 / 超时 ⇒ 整份 live 清空,界面上那一行消失, 不留着上一次的温度冒充现在的。 3. **对不上不猜。** 机架位形如 `B-3`(板名-路号):只有一块板时全归它;多块板时只认「板子名字就是那个字母」的。 没填机架位、路号不在 1~5、板名对不上的手机,一律不摆。 """ import asyncio import json import logging import re import urllib.request from typing import Dict, List, Optional, Tuple log = logging.getLogger("prrack.fleet") POLL_S = 5.0 TIMEOUT_S = 4.0 RAIL_RE = re.compile(r"^(.*?)[-_ ]?([1-5])$") def _board_key(name: str) -> str: return re.sub(r"[\s板]", "", name or "").upper() def map_phones(phones: List[dict], boards: List[Tuple[str, str]]) -> Dict[Tuple[str, int], dict]: """fleet 的手机表 → {(板子 id, 路号): live}。`boards` 是 [(id, name)]。规矩 3 在这里。""" out: Dict[Tuple[str, int], dict] = {} for p in phones: m = RAIL_RE.match(str(p.get("rail") or "").strip()) if not m or not p.get("enabled", True): continue prefix, rail = m.group(1).upper(), int(m.group(2)) if len(boards) == 1: board = boards[0][0] else: hit = [b for b, name in boards if prefix and _board_key(name) == prefix] if len(hit) != 1: continue board = hit[0] at = p.get("temp_at") out[(board, rail)] = { "fleet_rail": p.get("rail"), "online": bool(p.get("online")), "busy": bool(p.get("busy")), "agent": p.get("version") or "", "ip": p.get("ip") or "", "temp_board_mc": p.get("temp_board_mc"), "temp_cpu_mc": p.get("temp_cpu_mc"), "temp_at": at * 1000 if isinstance(at, int) else None, # fleet 是秒,这边一律毫秒 } return out class FleetPoller: def __init__(self, hub, url: str, token: str): self.hub = hub self.url = url.rstrip("/") + "/api/fleet/phones" self.token = token self._warned = False def _fetch(self) -> List[dict]: req = urllib.request.Request(self.url, headers={"X-Auth": self.token}) with urllib.request.urlopen(req, timeout=TIMEOUT_S) as r: return json.loads(r.read().decode()).get("phones") or [] async def run(self): loop = asyncio.get_running_loop() while True: live: Optional[Dict[Tuple[str, int], dict]] try: phones = await loop.run_in_executor(None, self._fetch) boards = [(r["id"], r["name"]) for r in self.hub.db.q("SELECT id, name FROM boards")] live = map_phones(phones, boards) self._warned = False except Exception as e: # 读不到:清空(规矩 2),只在第一次吭一声 if not self._warned: log.warning("AppTask 的手机表读不到(%s)—— 手机温度 / 在线状态不显示", e) self._warned = True live = {} self.hub.set_phone_live(live) await asyncio.sleep(POLL_S)