mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-05-08 22:44:27 +02:00
599e4628d9
Features Neighbour details modal with full info and map view WebSocket support with heartbeat and automatic reconnection Improved signal quality calculations (SNR-based RSSI) Route-based pagination for faster initial loads UI Mobile sidebar tweaks (logout, version info, lazy-loaded charts) Sorting added to the neighbour table Packet view now shows multi-hop paths CAD calibration charts respect light/dark themes Statistics charts now show the full requested time range Performance Reduced polling when WebSocket is active Lazy loading for heavier components Noise floor data capped to keep charts responsive Technical Improved type safety across API responses Contrast improvements for accessibility Cleaner WebSocket and MQTT reconnection handling Additional metrics added to heartbeat stats Bug fixes Corrected noise floor history query Fixed authentication for CAD calibration streams Nothing major required from users — just update and carry on. As always, shout if something looks off.
116 lines
3.4 KiB
Python
116 lines
3.4 KiB
Python
"""
|
|
WebSocket handler for real-time packet updates - simple ws4py implementation
|
|
"""
|
|
import json
|
|
import logging
|
|
import threading
|
|
import time
|
|
import cherrypy
|
|
from ws4py.websocket import WebSocket
|
|
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
|
|
|
logger = logging.getLogger("WebSocket")
|
|
|
|
# Suppress noisy ws4py error logs for normal disconnections (ConnectionResetError, etc.)
|
|
logging.getLogger('ws4py').setLevel(logging.CRITICAL)
|
|
|
|
# Global set of connected clients
|
|
_connected_clients = set()
|
|
|
|
# Heartbeat configuration
|
|
PING_INTERVAL = 30 # seconds
|
|
_heartbeat_thread = None
|
|
_heartbeat_running = False
|
|
|
|
|
|
class PacketWebSocket(WebSocket):
|
|
|
|
def opened(self):
|
|
"""Called when a WebSocket connection is established"""
|
|
_connected_clients.add(self)
|
|
logger.info(f"WebSocket connected. Total clients: {len(_connected_clients)}")
|
|
|
|
def closed(self, code, reason=None):
|
|
"""Called when a WebSocket connection is closed"""
|
|
_connected_clients.discard(self)
|
|
logger.info(f"WebSocket disconnected. Total clients: {len(_connected_clients)}")
|
|
|
|
def received_message(self, message):
|
|
"""Handle messages from client"""
|
|
try:
|
|
data = json.loads(str(message))
|
|
if data.get("type") == "ping":
|
|
self.send(json.dumps({"type": "pong"}))
|
|
elif data.get("type") == "pong":
|
|
# Client responded to our ping
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def broadcast_packet(packet_data: dict):
|
|
|
|
if not _connected_clients:
|
|
return
|
|
|
|
message = json.dumps({"type": "packet", "data": packet_data})
|
|
|
|
for client in list(_connected_clients):
|
|
try:
|
|
client.send(message)
|
|
except Exception as e:
|
|
logger.error(f"WebSocket send error: {e}")
|
|
_connected_clients.discard(client)
|
|
|
|
|
|
def broadcast_stats(stats_data: dict):
|
|
|
|
if not _connected_clients:
|
|
return
|
|
|
|
message = json.dumps({"type": "stats", "data": stats_data})
|
|
|
|
for client in list(_connected_clients):
|
|
try:
|
|
client.send(message)
|
|
except Exception as e:
|
|
logger.error(f"WebSocket send error: {e}")
|
|
_connected_clients.discard(client)
|
|
|
|
|
|
def _heartbeat_loop():
|
|
"""Background thread to send periodic pings to all connected clients"""
|
|
global _heartbeat_running
|
|
|
|
while _heartbeat_running:
|
|
time.sleep(PING_INTERVAL)
|
|
|
|
if not _connected_clients:
|
|
continue
|
|
|
|
ping_message = json.dumps({"type": "ping"})
|
|
|
|
for client in list(_connected_clients):
|
|
try:
|
|
client.send(ping_message)
|
|
except Exception as e:
|
|
logger.debug(f"Heartbeat ping failed: {e}")
|
|
_connected_clients.discard(client)
|
|
|
|
|
|
def init_websocket():
|
|
"""Initialize WebSocket plugin and start heartbeat"""
|
|
global _heartbeat_thread, _heartbeat_running
|
|
|
|
WebSocketPlugin(cherrypy.engine).subscribe()
|
|
cherrypy.tools.websocket = WebSocketTool()
|
|
|
|
# Start heartbeat thread
|
|
if not _heartbeat_running:
|
|
_heartbeat_running = True
|
|
_heartbeat_thread = threading.Thread(target=_heartbeat_loop, daemon=True)
|
|
_heartbeat_thread.start()
|
|
logger.info(f"WebSocket initialized with {PING_INTERVAL}s heartbeat")
|
|
else:
|
|
logger.info("WebSocket initialized")
|