mirror of
https://github.com/eddieoz/LoRa-Mesh-Analyzer.git
synced 2026-08-02 06:52:55 +02:00
feat: Introduce router cluster analysis mode for active node discovery with configurable radius and tests.
This commit is contained in:
@@ -7,7 +7,7 @@ from .utils import get_val, haversine
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ActiveTester:
|
||||
def __init__(self, interface, priority_nodes=None, auto_discovery_roles=None, auto_discovery_limit=5, online_nodes=None, local_node_id=None, traceroute_timeout=60, test_interval=30):
|
||||
def __init__(self, interface, priority_nodes=None, auto_discovery_roles=None, auto_discovery_limit=5, online_nodes=None, local_node_id=None, traceroute_timeout=60, test_interval=30, analysis_mode='distance', cluster_radius=2000):
|
||||
self.interface = interface
|
||||
self.priority_nodes = priority_nodes if priority_nodes else []
|
||||
self.auto_discovery_roles = auto_discovery_roles if auto_discovery_roles else ['ROUTER', 'REPEATER']
|
||||
@@ -19,6 +19,8 @@ class ActiveTester:
|
||||
self.current_priority_index = 0
|
||||
self.pending_traceroute = None # Store ID of node we are waiting for
|
||||
self.traceroute_timeout = traceroute_timeout # Seconds to wait for a response
|
||||
self.analysis_mode = analysis_mode
|
||||
self.cluster_radius = cluster_radius
|
||||
|
||||
# Reporting Data
|
||||
self.test_results = [] # List of dicts: {node_id, status, rtt, hops, snr, timestamp}
|
||||
@@ -70,6 +72,9 @@ class ActiveTester:
|
||||
Selects nodes based on lastHeard timestamp, roles, and geolocation.
|
||||
Uses the existing node database instead of waiting for packets.
|
||||
"""
|
||||
if self.analysis_mode == 'router_clusters':
|
||||
return self._get_router_cluster_nodes()
|
||||
|
||||
candidates = []
|
||||
nodes = self.interface.nodes
|
||||
|
||||
@@ -218,6 +223,88 @@ class ActiveTester:
|
||||
selected_ids = [c['id'] for c in final_candidates]
|
||||
return selected_ids
|
||||
|
||||
def _get_router_cluster_nodes(self):
|
||||
"""
|
||||
Selects nodes that are within cluster_radius of known routers.
|
||||
"""
|
||||
logger.info(f"Auto-discovery mode: Router Clusters (Radius: {self.cluster_radius}m)")
|
||||
nodes = self.interface.nodes
|
||||
routers = []
|
||||
|
||||
# 1. Identify Routers with Position
|
||||
for node_id, node in nodes.items():
|
||||
user = get_val(node, 'user', {})
|
||||
role = get_val(user, 'role')
|
||||
|
||||
is_router = False
|
||||
if isinstance(role, int):
|
||||
if role in [2, 3, 4, 9]: # ROUTER, ROUTER_CLIENT, REPEATER, ROUTER_LATE
|
||||
is_router = True
|
||||
elif role in ['ROUTER', 'REPEATER', 'ROUTER_CLIENT', 'ROUTER_LATE']:
|
||||
is_router = True
|
||||
|
||||
if is_router:
|
||||
pos = get_val(node, 'position', {})
|
||||
lat = get_val(pos, 'latitude')
|
||||
lon = get_val(pos, 'longitude')
|
||||
|
||||
# Handle integer coordinates if needed
|
||||
if lat is None:
|
||||
lat_i = get_val(pos, 'latitude_i') or get_val(pos, 'latitudeI')
|
||||
if lat_i is not None: lat = lat_i / 1e7
|
||||
if lon is None:
|
||||
lon_i = get_val(pos, 'longitude_i') or get_val(pos, 'longitudeI')
|
||||
if lon_i is not None: lon = lon_i / 1e7
|
||||
|
||||
if lat is not None and lon is not None:
|
||||
routers.append({
|
||||
'id': node_id,
|
||||
'lat': lat,
|
||||
'lon': lon
|
||||
})
|
||||
|
||||
logger.info(f"Found {len(routers)} routers with position.")
|
||||
|
||||
# 2. Find Neighbors for each Router
|
||||
candidates = set()
|
||||
|
||||
for r in routers:
|
||||
for node_id, node in nodes.items():
|
||||
if node_id == r['id']: continue
|
||||
|
||||
# Check if we should ignore this node (e.g. no lastHeard)
|
||||
last_heard = get_val(node, 'lastHeard')
|
||||
if not last_heard: continue
|
||||
|
||||
pos = get_val(node, 'position', {})
|
||||
lat = get_val(pos, 'latitude')
|
||||
lon = get_val(pos, 'longitude')
|
||||
|
||||
# Handle integer coordinates
|
||||
if lat is None:
|
||||
lat_i = get_val(pos, 'latitude_i') or get_val(pos, 'latitudeI')
|
||||
if lat_i is not None: lat = lat_i / 1e7
|
||||
if lon is None:
|
||||
lon_i = get_val(pos, 'longitude_i') or get_val(pos, 'longitudeI')
|
||||
if lon_i is not None: lon = lon_i / 1e7
|
||||
|
||||
if lat is not None and lon is not None:
|
||||
dist = haversine(r['lat'], r['lon'], lat, lon)
|
||||
if dist <= self.cluster_radius:
|
||||
candidates.add(node_id)
|
||||
|
||||
# 3. Select Nodes
|
||||
# Convert to list and sort/limit
|
||||
candidate_list = list(candidates)
|
||||
|
||||
# Sort by lastHeard (most recent first)
|
||||
candidate_list.sort(key=lambda nid: get_val(nodes[nid], 'lastHeard', 0), reverse=True)
|
||||
|
||||
selected = candidate_list[:self.auto_discovery_limit]
|
||||
logger.info(f"Selected {len(selected)} nodes near routers: {selected}")
|
||||
|
||||
return selected
|
||||
|
||||
def send_traceroute(self, dest_node_id):
|
||||
"""
|
||||
Sends a traceroute request to the destination node.
|
||||
|
||||
+89
-45
@@ -14,9 +14,10 @@ class NetworkHealthAnalyzer:
|
||||
self.ch_util_threshold = thresholds.get('channel_utilization', 25.0)
|
||||
self.air_util_threshold = thresholds.get('air_util_tx', 7.0) # Updated default to 7%
|
||||
self.router_density_threshold = thresholds.get('router_density_threshold', 2000)
|
||||
self.active_threshold_seconds = thresholds.get('active_threshold_seconds', 7200)
|
||||
self.max_nodes_long_fast = self.config.get('max_nodes_for_long_fast', 60)
|
||||
|
||||
def analyze(self, nodes, packet_history=None, my_node=None):
|
||||
def analyze(self, nodes, packet_history=None, my_node=None, test_results=None):
|
||||
"""
|
||||
Analyzes the node DB and packet history for potential issues.
|
||||
Returns a list of issue strings.
|
||||
@@ -79,7 +80,7 @@ class NetworkHealthAnalyzer:
|
||||
issues.extend(self.check_hop_counts(packet_history, nodes))
|
||||
|
||||
# --- Geospatial Analysis ---
|
||||
issues.extend(self.check_router_density(nodes))
|
||||
issues.extend(self.check_router_density(nodes, test_results))
|
||||
issues.extend(self.check_network_size_and_preset(nodes))
|
||||
if my_node:
|
||||
issues.extend(self.check_signal_vs_distance(nodes, my_node))
|
||||
@@ -155,6 +156,12 @@ class NetworkHealthAnalyzer:
|
||||
|
||||
if r['id'] in route_hex:
|
||||
relay_count += 1
|
||||
|
||||
# Check return path as well
|
||||
route_back = res.get('route_back', [])
|
||||
route_back_hex = [f"!{n:08x}" if isinstance(n, int) else n for n in route_back]
|
||||
if r['id'] in route_back_hex:
|
||||
relay_count += 1
|
||||
|
||||
# C. Channel Util
|
||||
ch_util = get_val(r['metrics'], 'channelUtilization', 0)
|
||||
@@ -171,6 +178,8 @@ class NetworkHealthAnalyzer:
|
||||
stats.append({
|
||||
'id': r['id'],
|
||||
'name': r['name'],
|
||||
'lat': r['lat'],
|
||||
'lon': r['lon'],
|
||||
'role': r['role'],
|
||||
'neighbors_2km': total_neighbors,
|
||||
'routers_2km': nearby_routers,
|
||||
@@ -326,62 +335,97 @@ class NetworkHealthAnalyzer:
|
||||
but we can warn based on size.
|
||||
"""
|
||||
issues = []
|
||||
total_nodes = len(nodes)
|
||||
issues = []
|
||||
|
||||
if total_nodes > self.max_nodes_long_fast:
|
||||
issues.append(f"Network Size: {total_nodes} nodes detected. If using LONG_FAST, consider switching to a faster preset (e.g. LONG_MODERATE or SHORT_FAST) to reduce collision probability.")
|
||||
# Filter for active nodes
|
||||
current_time = time.time()
|
||||
active_nodes = 0
|
||||
|
||||
for node in nodes.values():
|
||||
last_heard = get_val(node, 'lastHeard', 0)
|
||||
# Some nodes might use 'last_heard' or other keys, but standard is usually lastHeard in the node dict
|
||||
# If it's 0, it might be very old or unknown.
|
||||
|
||||
if current_time - last_heard < self.active_threshold_seconds:
|
||||
active_nodes += 1
|
||||
|
||||
if active_nodes > self.max_nodes_long_fast:
|
||||
issues.append(f"Network Size: {active_nodes} active nodes detected (seen in last {self.active_threshold_seconds/3600:.1f}h). If using LONG_FAST, consider switching to a faster preset (e.g. LONG_MODERATE or SHORT_FAST) to reduce collision probability.")
|
||||
|
||||
return issues
|
||||
|
||||
def check_router_density(self, nodes):
|
||||
def check_router_density(self, nodes, test_results=None):
|
||||
"""
|
||||
Checks for high density of routers.
|
||||
New Logic: Check for > 2 routers within 2km radius of each other.
|
||||
Identifies clusters of routers within 'router_density_threshold'.
|
||||
Recommends keeping the most effective router (highest relay count) and demoting others.
|
||||
"""
|
||||
issues = []
|
||||
routers = []
|
||||
|
||||
# 1. Get Router Stats (includes relay counts)
|
||||
stats = self.get_router_stats(nodes, test_results)
|
||||
# Map ID to stat for easy lookup
|
||||
stat_map = {s['id']: s for s in stats}
|
||||
|
||||
# Filter for routers with valid position
|
||||
for node_id, node in nodes.items():
|
||||
user = get_val(node, 'user', {})
|
||||
role = get_val(user, 'role')
|
||||
|
||||
is_router = False
|
||||
if isinstance(role, int):
|
||||
if role in [2, 3, 4, 9]: # ROUTER, ROUTER_CLIENT, REPEATER, ROUTER_LATE
|
||||
is_router = True
|
||||
elif role in ['ROUTER', 'REPEATER', 'ROUTER_CLIENT', 'ROUTER_LATE']:
|
||||
is_router = True
|
||||
|
||||
pos = get_val(node, 'position', {})
|
||||
lat = get_val(pos, 'latitude')
|
||||
lon = get_val(pos, 'longitude')
|
||||
|
||||
if is_router and lat is not None and lon is not None:
|
||||
routers.append({
|
||||
'id': node_id,
|
||||
'name': get_node_name(node, node_id),
|
||||
'lat': lat,
|
||||
'lon': lon
|
||||
})
|
||||
routers = []
|
||||
for s in stats:
|
||||
# get_router_stats already filters for routers with position
|
||||
routers.append(s)
|
||||
|
||||
if not routers:
|
||||
return issues
|
||||
|
||||
# 2. Build Clusters
|
||||
# Adjacency list: index -> list of neighbor indices
|
||||
adj = {i: [] for i in range(len(routers))}
|
||||
|
||||
# Check density for each router
|
||||
reported_pairs = set()
|
||||
|
||||
for i, r1 in enumerate(routers):
|
||||
nearby_routers = []
|
||||
for j, r2 in enumerate(routers):
|
||||
if i == j: continue
|
||||
|
||||
for i in range(len(routers)):
|
||||
for j in range(i + 1, len(routers)):
|
||||
r1 = routers[i]
|
||||
r2 = routers[j]
|
||||
dist = haversine(r1['lat'], r1['lon'], r2['lat'], r2['lon'])
|
||||
if dist < self.router_density_threshold:
|
||||
nearby_routers.append(r2)
|
||||
|
||||
if dist < self.router_density_threshold:
|
||||
adj[i].append(j)
|
||||
adj[j].append(i)
|
||||
|
||||
# Find connected components (clusters)
|
||||
visited = set()
|
||||
clusters = []
|
||||
|
||||
for i in range(len(routers)):
|
||||
if i not in visited:
|
||||
component = []
|
||||
stack = [i]
|
||||
visited.add(i)
|
||||
while stack:
|
||||
curr = stack.pop()
|
||||
component.append(routers[curr])
|
||||
for neighbor in adj[curr]:
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
stack.append(neighbor)
|
||||
if len(component) > 1:
|
||||
clusters.append(component)
|
||||
|
||||
# 3. Analyze Clusters and Generate Recommendations
|
||||
for cluster in clusters:
|
||||
# Sort by relay_count (desc), then neighbors_2km (desc)
|
||||
# We want the "best" router first
|
||||
cluster.sort(key=lambda x: (x['relay_count'], x['neighbors_2km']), reverse=True)
|
||||
|
||||
if len(nearby_routers) >= 1:
|
||||
# Construct a unique key for this cluster to avoid duplicate messages
|
||||
# (Simple approach: just report for the center node)
|
||||
names = [r['name'] for r in nearby_routers]
|
||||
issues.append(f"Topology: High Router Density! '{r1['name']}' has {len(nearby_routers)} other routers within {self.router_density_threshold}m ({', '.join(names)}). Consider changing some to CLIENT.")
|
||||
best_router = cluster[0]
|
||||
others = cluster[1:]
|
||||
|
||||
other_names = [o['name'] for o in others]
|
||||
|
||||
# Construct message
|
||||
msg = f"Topology: High Router Density! Found cluster of {len(cluster)} routers. "
|
||||
msg += f"Best positioned seems to be '{best_router['name']}' ({best_router['relay_count']} relays). "
|
||||
msg += f"Consider changing others to CLIENT: {', '.join(other_names)}."
|
||||
|
||||
issues.append(msg)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
+14
-3
@@ -112,6 +112,9 @@ class MeshMonitor:
|
||||
# Create ActiveTester with auto-discovery (no online_nodes needed)
|
||||
traceroute_timeout = self.config.get('traceroute_timeout', 60)
|
||||
test_interval = self.config.get('active_test_interval', 30)
|
||||
analysis_mode = self.config.get('analysis_mode', 'distance')
|
||||
cluster_radius = self.config.get('cluster_radius', 2000)
|
||||
|
||||
self.active_tester = ActiveTester(
|
||||
self.interface,
|
||||
priority_nodes=[], # Empty - will trigger auto-discovery
|
||||
@@ -120,7 +123,9 @@ class MeshMonitor:
|
||||
online_nodes=set(), # Not used anymore - discovery uses lastHeard
|
||||
local_node_id=local_id,
|
||||
traceroute_timeout=traceroute_timeout,
|
||||
test_interval=test_interval
|
||||
test_interval=test_interval,
|
||||
analysis_mode=analysis_mode,
|
||||
cluster_radius=cluster_radius
|
||||
)
|
||||
|
||||
logger.info("Active testing started with auto-discovered nodes.")
|
||||
@@ -152,6 +157,9 @@ class MeshMonitor:
|
||||
|
||||
traceroute_timeout = self.config.get('traceroute_timeout', 60)
|
||||
test_interval = self.config.get('active_test_interval', 30)
|
||||
analysis_mode = self.config.get('analysis_mode', 'distance')
|
||||
cluster_radius = self.config.get('cluster_radius', 2000)
|
||||
|
||||
self.active_tester = ActiveTester(
|
||||
self.interface,
|
||||
priority_nodes=priority_nodes,
|
||||
@@ -159,7 +167,9 @@ class MeshMonitor:
|
||||
auto_discovery_limit=auto_discovery_limit,
|
||||
local_node_id=local_id,
|
||||
traceroute_timeout=traceroute_timeout,
|
||||
test_interval=test_interval
|
||||
test_interval=test_interval,
|
||||
analysis_mode=analysis_mode,
|
||||
cluster_radius=cluster_radius
|
||||
)
|
||||
|
||||
self.main_loop()
|
||||
@@ -330,7 +340,8 @@ class MeshMonitor:
|
||||
my_node = self.interface.localNode
|
||||
|
||||
# Run Analysis
|
||||
issues = self.analyzer.analyze(nodes, packet_history=self.packet_history, my_node=my_node)
|
||||
test_results = self.active_tester.test_results if self.active_tester else []
|
||||
issues = self.analyzer.analyze(nodes, packet_history=self.packet_history, my_node=my_node, test_results=test_results)
|
||||
|
||||
# Run Router Efficiency Analysis (using accumulated test results if available)
|
||||
if self.active_tester:
|
||||
|
||||
@@ -9,6 +9,12 @@ priority_nodes:
|
||||
log_level: info
|
||||
|
||||
# Auto-Discovery Settings (Used if priority_nodes is empty)
|
||||
# Analysis Mode: 'distance' (default) or 'router_clusters'
|
||||
analysis_mode: distance
|
||||
|
||||
# Radius for router cluster analysis (in meters)
|
||||
cluster_radius: 2000
|
||||
|
||||
# Roles to prioritize for auto-discovery
|
||||
auto_discovery_roles:
|
||||
- ROUTER
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import unittest
|
||||
import time
|
||||
from mesh_monitor.analyzer import NetworkHealthAnalyzer
|
||||
|
||||
class TestNetworkSize(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Config with small max nodes for testing
|
||||
self.config = {
|
||||
'max_nodes_for_long_fast': 5,
|
||||
'thresholds': {
|
||||
'active_threshold_seconds': 3600 # 1 hour
|
||||
}
|
||||
}
|
||||
self.analyzer = NetworkHealthAnalyzer(config=self.config)
|
||||
|
||||
def test_active_nodes_warning(self):
|
||||
current_time = time.time()
|
||||
nodes = {}
|
||||
|
||||
# Add 6 active nodes (should trigger warning since max is 5)
|
||||
for i in range(6):
|
||||
nodes[f'!active{i}'] = {'lastHeard': current_time - 100}
|
||||
|
||||
# Add 10 inactive nodes (should be ignored)
|
||||
for i in range(10):
|
||||
nodes[f'!inactive{i}'] = {'lastHeard': current_time - 7200} # 2 hours ago
|
||||
|
||||
issues = self.analyzer.check_network_size_and_preset(nodes)
|
||||
|
||||
print("\nActive Nodes Warning Issues:", issues)
|
||||
|
||||
# Should have a warning because 6 active > 5 max
|
||||
self.assertTrue(any("Network Size" in i for i in issues))
|
||||
self.assertTrue(any("6 active nodes" in i for i in issues))
|
||||
|
||||
def test_no_warning_if_inactive(self):
|
||||
current_time = time.time()
|
||||
nodes = {}
|
||||
|
||||
# Add 3 active nodes (under limit of 5)
|
||||
for i in range(3):
|
||||
nodes[f'!active{i}'] = {'lastHeard': current_time - 100}
|
||||
|
||||
# Add 50 inactive nodes (total > 5, but active < 5)
|
||||
for i in range(50):
|
||||
nodes[f'!inactive{i}'] = {'lastHeard': current_time - 7200}
|
||||
|
||||
issues = self.analyzer.check_network_size_and_preset(nodes)
|
||||
|
||||
print("\nNo Warning Issues:", issues)
|
||||
|
||||
# Should NOT have a warning
|
||||
self.assertFalse(any("Network Size" in i for i in issues))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,140 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
from mesh_monitor.active_tests import ActiveTester
|
||||
|
||||
class TestRouterClusters(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.mock_interface = MagicMock()
|
||||
self.tester = ActiveTester(
|
||||
self.mock_interface,
|
||||
analysis_mode='router_clusters',
|
||||
cluster_radius=2000,
|
||||
auto_discovery_limit=5
|
||||
)
|
||||
|
||||
def test_router_identification(self):
|
||||
# Setup nodes: 1 Router, 1 Client
|
||||
self.mock_interface.nodes = {
|
||||
'!router1': {
|
||||
'user': {'role': 'ROUTER'},
|
||||
'position': {'latitude': 10.0, 'longitude': 10.0},
|
||||
'lastHeard': 100
|
||||
},
|
||||
'!client1': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 10.0, 'longitude': 10.0},
|
||||
'lastHeard': 100
|
||||
}
|
||||
}
|
||||
|
||||
# We need to mock _get_router_cluster_nodes to NOT be called automatically if we were testing run_next_test
|
||||
# But here we are testing _get_router_cluster_nodes directly
|
||||
|
||||
# This test just verifies the logic inside _get_router_cluster_nodes
|
||||
# It doesn't really test "identification" in isolation because the method does everything.
|
||||
# Let's test the whole flow.
|
||||
|
||||
selected = self.tester._get_router_cluster_nodes()
|
||||
# Client1 is at same location as Router1, so dist is 0 < 2000
|
||||
self.assertIn('!client1', selected)
|
||||
# Router1 is excluded from its own neighbor list?
|
||||
# The logic says: if node_id == r['id']: continue
|
||||
# So Router1 should NOT be in the list unless it's near ANOTHER router.
|
||||
self.assertNotIn('!router1', selected)
|
||||
|
||||
def test_radius_check(self):
|
||||
# Router at (0,0)
|
||||
# Client1 at (0.01, 0) ~ 1.1km (Inside 2km)
|
||||
# Client2 at (0.03, 0) ~ 3.3km (Outside 2km)
|
||||
self.mock_interface.nodes = {
|
||||
'!router1': {
|
||||
'user': {'role': 'ROUTER'},
|
||||
'position': {'latitude': 0.0, 'longitude': 0.0},
|
||||
'lastHeard': 100
|
||||
},
|
||||
'!client1': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.01, 'longitude': 0.0},
|
||||
'lastHeard': 100
|
||||
},
|
||||
'!client2': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.03, 'longitude': 0.0},
|
||||
'lastHeard': 100
|
||||
}
|
||||
}
|
||||
|
||||
selected = self.tester._get_router_cluster_nodes()
|
||||
self.assertIn('!client1', selected)
|
||||
self.assertNotIn('!client2', selected)
|
||||
|
||||
def test_limit_and_sorting(self):
|
||||
# Router at (0,0)
|
||||
# 3 Clients inside radius
|
||||
# Limit is 2
|
||||
# Sort by lastHeard
|
||||
self.tester.auto_discovery_limit = 2
|
||||
self.mock_interface.nodes = {
|
||||
'!router1': {
|
||||
'user': {'role': 'ROUTER'},
|
||||
'position': {'latitude': 0.0, 'longitude': 0.0},
|
||||
'lastHeard': 100
|
||||
},
|
||||
'!client1': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.01, 'longitude': 0.0},
|
||||
'lastHeard': 200 # Newest
|
||||
},
|
||||
'!client2': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.01, 'longitude': 0.0},
|
||||
'lastHeard': 100 # Oldest
|
||||
},
|
||||
'!client3': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.01, 'longitude': 0.0},
|
||||
'lastHeard': 150 # Middle
|
||||
}
|
||||
}
|
||||
|
||||
selected = self.tester._get_router_cluster_nodes()
|
||||
self.assertEqual(len(selected), 2)
|
||||
self.assertEqual(selected[0], '!client1') # Newest first
|
||||
self.assertEqual(selected[1], '!client3') # Then middle
|
||||
self.assertNotIn('!client2', selected) # Oldest dropped
|
||||
|
||||
def test_active_node_selection(self):
|
||||
# Router at (0,0)
|
||||
# Client1: Inside radius, but no lastHeard (inactive/unknown)
|
||||
# Client2: Inside radius, lastHeard=0 (inactive)
|
||||
# Client3: Inside radius, lastHeard=100 (active)
|
||||
self.mock_interface.nodes = {
|
||||
'!router1': {
|
||||
'user': {'role': 'ROUTER'},
|
||||
'position': {'latitude': 0.0, 'longitude': 0.0},
|
||||
'lastHeard': 100
|
||||
},
|
||||
'!client1': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.01, 'longitude': 0.0}
|
||||
# No lastHeard
|
||||
},
|
||||
'!client2': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.01, 'longitude': 0.0},
|
||||
'lastHeard': 0
|
||||
},
|
||||
'!client3': {
|
||||
'user': {'role': 'CLIENT'},
|
||||
'position': {'latitude': 0.01, 'longitude': 0.0},
|
||||
'lastHeard': 100
|
||||
}
|
||||
}
|
||||
|
||||
selected = self.tester._get_router_cluster_nodes()
|
||||
self.assertIn('!client3', selected)
|
||||
self.assertNotIn('!client1', selected)
|
||||
self.assertNotIn('!client2', selected)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user