"""Общее состояние сервера: подключённые хотспоты, журнал событий, счётчики. Читается из потока веб-панели, пишется из цикла asyncio, поэтому всё под одним замком. Объём данных крошечный (десятки хотспотов, сотни строк лога), так что простой Lock дешевле и надёжнее любых очередей. """ from __future__ import annotations import threading import time from collections import deque from dataclasses import dataclass, field from typing import Any @dataclass class Peer: """Подключённый хотспот/ретранслятор.""" peer_id: int addr: tuple[str, int] callsign: str = "" rx_freq: str = "" tx_freq: str = "" colorcode: str = "" location: str = "" description: str = "" url: str = "" software: str = "" connected_at: float = field(default_factory=time.time) last_seen: float = field(default_factory=time.time) # стадия рукопожатия: login -> auth -> config -> ready stage: str = "login" salt: int = 0 rx_packets: int = 0 tx_packets: int = 0 def as_dict(self) -> dict[str, Any]: now = time.time() return { "peer_id": self.peer_id, "addr": f"{self.addr[0]}:{self.addr[1]}", "callsign": self.callsign, "location": self.location, "description": self.description, "software": self.software, "colorcode": self.colorcode, "rx_freq": self.rx_freq, "tx_freq": self.tx_freq, "stage": self.stage, "online_for": int(now - self.connected_at), "idle": round(now - self.last_seen, 1), "rx_packets": self.rx_packets, "tx_packets": self.tx_packets, } @dataclass class Call: """Идущая или завершившаяся передача.""" stream_id: int src_id: int dst_id: int slot: int group: bool peer_id: int started: float = field(default_factory=time.time) ended: float = 0.0 frames: int = 0 def as_dict(self) -> dict[str, Any]: end = self.ended or time.time() return { "stream_id": self.stream_id, "src_id": self.src_id, "dst_id": self.dst_id, "slot": self.slot, "type": "группа" if self.group else "приват", "peer_id": self.peer_id, "started": self.started, "duration": round(end - self.started, 1), "frames": self.frames, "active": self.ended == 0.0, } class State: def __init__(self, log_keep: int = 500) -> None: self._lock = threading.Lock() self.peers: dict[int, Peer] = {} self.log: deque[dict[str, Any]] = deque(maxlen=log_keep) self.calls: deque[Call] = deque(maxlen=50) self.active: dict[tuple[int, int], Call] = {} # (slot, stream_id) -> Call self.started = time.time() # ── журнал ────────────────────────────────────────────────────────────── def event(self, kind: str, text: str) -> None: """kind: info | call | peer | warn | error — панель красит по нему.""" with self._lock: self.log.append({"t": time.time(), "kind": kind, "text": text}) print(f"[{kind}] {text}", flush=True) # заодно в journalctl # ── хотспоты ──────────────────────────────────────────────────────────── def peer_add(self, peer: Peer) -> None: with self._lock: self.peers[peer.peer_id] = peer def peer_del(self, peer_id: int) -> Peer | None: with self._lock: return self.peers.pop(peer_id, None) def peer_get(self, peer_id: int) -> Peer | None: with self._lock: return self.peers.get(peer_id) def peers_ready(self) -> list[Peer]: with self._lock: return [p for p in self.peers.values() if p.stage == "ready"] # ── снимок для панели ─────────────────────────────────────────────────── def snapshot(self, log_since: float = 0.0) -> dict[str, Any]: with self._lock: return { "uptime": int(time.time() - self.started), "peers": [p.as_dict() for p in self.peers.values()], "calls": [c.as_dict() for c in reversed(self.calls)], "log": [e for e in self.log if e["t"] > log_since], "now": time.time(), }