mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
Add thread safety to MeshConnector node caches and StatusData
- 4a: Add threading.Lock to MeshConnector protecting _node_names and _node_positions dicts that are read/written from Meshtastic's pubsub thread callbacks (_on_receive, _on_node_update, _cache_node_info) and read from async code (get_node_position, get_node_name) - 4b: Add threading.Lock to StatusData protecting counters and activity list that are written from the async event loop and read from the HTTP server thread in to_dict() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c1f2c48494
commit
dca03500ec
2 changed files with 75 additions and 60 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
|
@ -46,6 +47,7 @@ class MeshConnector:
|
||||||
self._node_names: dict[str, str] = {}
|
self._node_names: dict[str, str] = {}
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connected(self) -> bool:
|
def connected(self) -> bool:
|
||||||
|
|
@ -127,34 +129,36 @@ class MeshConnector:
|
||||||
if not self._interface:
|
if not self._interface:
|
||||||
return
|
return
|
||||||
|
|
||||||
for node_id, node in self._interface.nodes.items():
|
with self._lock:
|
||||||
# Cache name
|
for node_id, node in self._interface.nodes.items():
|
||||||
if user := node.get("user"):
|
# Cache name
|
||||||
name = user.get("shortName") or user.get("longName") or node_id
|
if user := node.get("user"):
|
||||||
self._node_names[node_id] = name
|
name = user.get("shortName") or user.get("longName") or node_id
|
||||||
|
self._node_names[node_id] = name
|
||||||
|
|
||||||
# Cache position
|
# Cache position
|
||||||
if position := node.get("position"):
|
if position := node.get("position"):
|
||||||
lat = position.get("latitude")
|
lat = position.get("latitude")
|
||||||
lon = position.get("longitude")
|
lon = position.get("longitude")
|
||||||
if lat is not None and lon is not None:
|
if lat is not None and lon is not None:
|
||||||
self._node_positions[node_id] = (lat, lon)
|
self._node_positions[node_id] = (lat, lon)
|
||||||
|
|
||||||
def _on_node_update(self, node, interface) -> None:
|
def _on_node_update(self, node, interface) -> None:
|
||||||
"""Handle node info updates."""
|
"""Handle node info updates."""
|
||||||
node_id = f"!{node['num']:08x}"
|
node_id = f"!{node['num']:08x}"
|
||||||
|
|
||||||
# Update name cache
|
with self._lock:
|
||||||
if user := node.get("user"):
|
# Update name cache
|
||||||
name = user.get("shortName") or user.get("longName") or node_id
|
if user := node.get("user"):
|
||||||
self._node_names[node_id] = name
|
name = user.get("shortName") or user.get("longName") or node_id
|
||||||
|
self._node_names[node_id] = name
|
||||||
|
|
||||||
# Update position cache
|
# Update position cache
|
||||||
if position := node.get("position"):
|
if position := node.get("position"):
|
||||||
lat = position.get("latitude")
|
lat = position.get("latitude")
|
||||||
lon = position.get("longitude")
|
lon = position.get("longitude")
|
||||||
if lat is not None and lon is not None:
|
if lat is not None and lon is not None:
|
||||||
self._node_positions[node_id] = (lat, lon)
|
self._node_positions[node_id] = (lat, lon)
|
||||||
|
|
||||||
def _on_receive(self, packet, interface) -> None:
|
def _on_receive(self, packet, interface) -> None:
|
||||||
"""Handle incoming text message."""
|
"""Handle incoming text message."""
|
||||||
|
|
@ -175,8 +179,11 @@ class MeshConnector:
|
||||||
# Determine if DM (sent directly to us, not broadcast)
|
# Determine if DM (sent directly to us, not broadcast)
|
||||||
is_dm = to_num == self._my_node_id
|
is_dm = to_num == self._my_node_id
|
||||||
|
|
||||||
# Get sender name
|
with self._lock:
|
||||||
sender_name = self._node_names.get(sender_num, sender_num)
|
# Get sender name
|
||||||
|
sender_name = self._node_names.get(sender_num, sender_num)
|
||||||
|
# Get position if available
|
||||||
|
position = self._node_positions.get(sender_num)
|
||||||
|
|
||||||
# Create message object
|
# Create message object
|
||||||
msg = MeshMessage(
|
msg = MeshMessage(
|
||||||
|
|
@ -189,8 +196,8 @@ class MeshConnector:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach position if available
|
# Attach position if available
|
||||||
if sender_num in self._node_positions:
|
if position:
|
||||||
msg._position = self._node_positions[sender_num]
|
msg._position = position
|
||||||
|
|
||||||
# Schedule callback on event loop
|
# Schedule callback on event loop
|
||||||
self._loop.call_soon_threadsafe(
|
self._loop.call_soon_threadsafe(
|
||||||
|
|
@ -258,7 +265,8 @@ class MeshConnector:
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (latitude, longitude) or None if not available
|
Tuple of (latitude, longitude) or None if not available
|
||||||
"""
|
"""
|
||||||
return self._node_positions.get(node_id)
|
with self._lock:
|
||||||
|
return self._node_positions.get(node_id)
|
||||||
|
|
||||||
def get_node_name(self, node_id: str) -> str:
|
def get_node_name(self, node_id: str) -> str:
|
||||||
"""Get cached name for a node.
|
"""Get cached name for a node.
|
||||||
|
|
@ -269,4 +277,5 @@ class MeshConnector:
|
||||||
Returns:
|
Returns:
|
||||||
Node name or the node ID if name not available
|
Node name or the node ID if name not available
|
||||||
"""
|
"""
|
||||||
return self._node_names.get(node_id, node_id)
|
with self._lock:
|
||||||
|
return self._node_names.get(node_id, node_id)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
@ -18,6 +19,7 @@ class StatusData:
|
||||||
"""Container for status information."""
|
"""Container for status information."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._lock = threading.Lock()
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
self.message_count = 0
|
self.message_count = 0
|
||||||
self.response_count = 0
|
self.response_count = 0
|
||||||
|
|
@ -29,31 +31,34 @@ class StatusData:
|
||||||
|
|
||||||
def record_message(self, sender_id: str, sender_name: str):
|
def record_message(self, sender_id: str, sender_name: str):
|
||||||
"""Record an incoming message."""
|
"""Record an incoming message."""
|
||||||
self.message_count += 1
|
with self._lock:
|
||||||
self.last_message_time = time.time()
|
self.message_count += 1
|
||||||
self.connected_nodes.add(sender_id)
|
self.last_message_time = time.time()
|
||||||
|
self.connected_nodes.add(sender_id)
|
||||||
|
|
||||||
self.recent_activity.append({
|
self.recent_activity.append({
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"time": datetime.now().isoformat(),
|
"time": datetime.now().isoformat(),
|
||||||
"sender": sender_name,
|
"sender": sender_name,
|
||||||
})
|
})
|
||||||
# Keep only last 20 activities
|
# Keep only last 20 activities
|
||||||
self.recent_activity = self.recent_activity[-20:]
|
self.recent_activity = self.recent_activity[-20:]
|
||||||
|
|
||||||
def record_response(self):
|
def record_response(self):
|
||||||
"""Record an outgoing response."""
|
"""Record an outgoing response."""
|
||||||
self.response_count += 1
|
with self._lock:
|
||||||
|
self.response_count += 1
|
||||||
|
|
||||||
def record_error(self, error: str):
|
def record_error(self, error: str):
|
||||||
"""Record an error."""
|
"""Record an error."""
|
||||||
self.error_count += 1
|
with self._lock:
|
||||||
self.recent_activity.append({
|
self.error_count += 1
|
||||||
"type": "error",
|
self.recent_activity.append({
|
||||||
"time": datetime.now().isoformat(),
|
"type": "error",
|
||||||
"error": error[:100],
|
"time": datetime.now().isoformat(),
|
||||||
})
|
"error": error[:100],
|
||||||
self.recent_activity = self.recent_activity[-20:]
|
})
|
||||||
|
self.recent_activity = self.recent_activity[-20:]
|
||||||
|
|
||||||
def get_uptime(self) -> str:
|
def get_uptime(self) -> str:
|
||||||
"""Get formatted uptime string."""
|
"""Get formatted uptime string."""
|
||||||
|
|
@ -75,22 +80,23 @@ class StatusData:
|
||||||
|
|
||||||
def to_dict(self, include_activity: bool = False) -> dict:
|
def to_dict(self, include_activity: bool = False) -> dict:
|
||||||
"""Convert to dictionary for JSON response."""
|
"""Convert to dictionary for JSON response."""
|
||||||
data = {
|
with self._lock:
|
||||||
"status": "online",
|
data = {
|
||||||
"uptime": self.get_uptime(),
|
"status": "online",
|
||||||
"uptime_seconds": int(time.time() - self.start_time),
|
"uptime": self.get_uptime(),
|
||||||
"messages_received": self.message_count,
|
"uptime_seconds": int(time.time() - self.start_time),
|
||||||
"responses_sent": self.response_count,
|
"messages_received": self.message_count,
|
||||||
"errors": self.error_count,
|
"responses_sent": self.response_count,
|
||||||
"connected_nodes": len(self.connected_nodes),
|
"errors": self.error_count,
|
||||||
"using_fallback": self.using_fallback,
|
"connected_nodes": len(self.connected_nodes),
|
||||||
}
|
"using_fallback": self.using_fallback,
|
||||||
|
}
|
||||||
|
|
||||||
if self.last_message_time:
|
if self.last_message_time:
|
||||||
data["last_message_ago"] = int(time.time() - self.last_message_time)
|
data["last_message_ago"] = int(time.time() - self.last_message_time)
|
||||||
|
|
||||||
if include_activity:
|
if include_activity:
|
||||||
data["recent_activity"] = self.recent_activity
|
data["recent_activity"] = list(self.recent_activity)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue