Files
pyMC_Repeater/repeater/data_acquisition/websocket_handler.py
T
Lloyd 599e4628d9 Changes include:
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.
2026-01-18 20:15:50 +00:00

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")