"""Last Heard — кто и когда выходил в эфир. Строится по потоку кадров, а не по хуку вызовов ядра: тот не отдаёт номер хотспота, а без него не ответить на вопрос «через какое устройство слышали». Начало передачи определяется по смене stream_id (заголовки теряются в эфире), конец — по терминатору ИЛИ по тишине: терминаторы теряются так же часто. """ from __future__ import annotations import time from .analyzer import CALL_GROUP from .state import Call # 360 мс = 6 пропущенных голосовых кадров. Столько же ждёт HBlink, прежде чем # считать передачу законченной. Берём с запасом, чтобы не рвать вызов на джиттере. SILENCE_TIMEOUT = 2.0 class LastHeard: def __init__(self, state) -> None: self.state = state self._open: dict[tuple[int, int], Call] = {} # (peer_id, slot) -> вызов self._sid: dict[tuple[int, int], bytes] = {} # (peer_id, slot) -> stream_id self._seen: dict[tuple[int, int], float] = {} # (peer_id, slot) -> когда был кадр def feed(self, peer, dmr) -> None: key = (int(dmr.peer_id), int(dmr.slot)) sid = dmr.stream_id now = time.time() if self._sid.get(key) != sid: # новая передача self._close(key, "новый поток") call = Call( stream_id=sid if isinstance(sid, int) else int.from_bytes(bytes(sid or b"\0"), "big"), src_id=int(dmr.src or 0), dst_id=int(dmr.dst or 0), slot=int(dmr.slot), group=(dmr.call_type == CALL_GROUP), peer_id=int(dmr.peer_id), ) self._open[key] = call self._sid[key] = sid with self.state._lock: self.state.calls.append(call) call = self._open.get(key) if call is not None: call.frames += 1 self._seen[key] = now if getattr(dmr, "is_terminator", False): self._close(key, "терминатор") def sweep(self) -> None: """Закрыть передачи, по которым перестали приходить кадры. Без этого зависший вызов вечно висел бы «активным»: терминатор теряется в эфире регулярно, полагаться только на него нельзя. """ now = time.time() for key, last in list(self._seen.items()): if now - last > SILENCE_TIMEOUT: self._close(key, "тишина") def _close(self, key, why: str) -> None: call = self._open.pop(key, None) self._sid.pop(key, None) self._seen.pop(key, None) if call is None or call.ended: return call.ended = time.time() if call.frames > 1: # одиночный кадр — не передача, а шум kind = "группа TG %s" % call.dst_id if call.group else "приват %s" % call.dst_id self.state.event( "call", f"эфир: {call.src_id} → {kind} · TS{call.slot} · " f"{round(call.ended - call.started, 1)} с · хотспот {call.peer_id}", )