Skip to content

Commit 6f818d9

Browse files
committed
Implement control packet handling for mesh node discovery
1 parent 7980c7a commit 6f818d9

File tree

6 files changed

+520
-0
lines changed

6 files changed

+520
-0
lines changed

examples/discover_nodes.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Minimal example: Discover nearby mesh nodes.
4+
5+
This example demonstrates how to broadcast a discovery request
6+
and collect responses from nearby repeaters and nodes in the mesh network.
7+
8+
The discovery request is sent as a zero-hop broadcast, and nearby nodes
9+
will respond with their public key and signal strength information.
10+
11+
Features:
12+
- Asynchronous callback-based response collection
13+
- Configurable discovery filter (node types to discover)
14+
- Signal strength data (SNR and RSSI) for each discovered node
15+
- Automatic timeout after specified duration
16+
"""
17+
18+
import asyncio
19+
import random
20+
import time
21+
22+
from common import create_mesh_node
23+
24+
from pymc_core.protocol.packet_builder import PacketBuilder
25+
26+
27+
async def discover_nodes(
28+
radio_type: str = "waveshare",
29+
serial_port: str = "/dev/ttyUSB0",
30+
timeout: float = 5.0,
31+
filter_mask: int = 0x02,
32+
):
33+
"""
34+
Discover nearby mesh nodes using control packets.
35+
36+
Args:
37+
radio_type: Radio hardware type ("waveshare", "uconsole", etc.)
38+
serial_port: Serial port for KISS TNC
39+
timeout: How long to wait for responses (seconds)
40+
filter_mask: Node types to discover (0x02 = repeaters only)
41+
"""
42+
mesh_node, identity = create_mesh_node("DiscoveryNode", radio_type, serial_port)
43+
44+
# Dictionary to store discovered nodes
45+
discovered_nodes = {}
46+
47+
# Create callback to collect discovery responses
48+
def on_discovery_response(response_data: dict):
49+
"""Handle discovery response callback."""
50+
tag = response_data.get("tag", 0)
51+
node_type = response_data.get("node_type", 0)
52+
inbound_snr = response_data.get("inbound_snr", 0.0) # Their RX of our request
53+
response_snr = response_data.get("response_snr", 0.0) # Our RX of their response
54+
rssi = response_data.get("rssi", 0)
55+
pub_key = response_data.get("pub_key", "")
56+
timestamp = response_data.get("timestamp", 0)
57+
58+
# Get node type name
59+
node_type_names = {1: "Repeater", 2: "Chat Node", 3: "Room Server"}
60+
node_type_name = node_type_names.get(node_type, f"Unknown({node_type})")
61+
62+
# Store node info
63+
node_id = pub_key[:16] # Use first 8 bytes as ID
64+
if node_id not in discovered_nodes:
65+
discovered_nodes[node_id] = {
66+
"pub_key": pub_key,
67+
"node_type": node_type_name,
68+
"inbound_snr": inbound_snr,
69+
"response_snr": response_snr,
70+
"rssi": rssi,
71+
"timestamp": timestamp,
72+
}
73+
74+
print(
75+
f"✓ Discovered {node_type_name}: {node_id}... "
76+
f"(TX→RX SNR: {inbound_snr:+.1f}dB, RX←TX SNR: {response_snr:+.1f}dB, "
77+
f"RSSI: {rssi}dBm)"
78+
)
79+
80+
# Get the control handler and set up callback
81+
control_handler = mesh_node.dispatcher.control_handler
82+
if not control_handler:
83+
print("Error: Control handler not available")
84+
return
85+
86+
# Generate random tag for this discovery request
87+
discovery_tag = random.randint(0, 0xFFFFFFFF)
88+
89+
# Set up callback for responses matching this tag
90+
control_handler.set_response_callback(discovery_tag, on_discovery_response)
91+
92+
# Create discovery request packet
93+
# filter_mask: 0x02 = bit 1 set = discover repeaters
94+
# since: 0 = discover all nodes regardless of modification time
95+
pkt = PacketBuilder.create_discovery_request(
96+
tag=discovery_tag, filter_mask=filter_mask, since=0, prefix_only=False
97+
)
98+
99+
print(f"Sending discovery request (tag: 0x{discovery_tag:08X})...")
100+
print(f"Filter mask: 0x{filter_mask:02X} (node types to discover)")
101+
print(f"Waiting {timeout} seconds for responses...\n")
102+
103+
# Send as zero-hop broadcast (no routing path)
104+
success = await mesh_node.dispatcher.send_packet(pkt, wait_for_ack=False)
105+
106+
if success:
107+
print("Discovery request sent successfully")
108+
109+
# Wait for responses
110+
start_time = time.time()
111+
while time.time() - start_time < timeout:
112+
await asyncio.sleep(0.1)
113+
114+
# Display results
115+
print(f"\n{'='*60}")
116+
print(f"Discovery complete - found {len(discovered_nodes)} node(s)")
117+
print(f"{'='*60}\n")
118+
119+
if discovered_nodes:
120+
for node_id, info in discovered_nodes.items():
121+
print(f"Node: {node_id}...")
122+
print(f" Type: {info['node_type']}")
123+
print(f" TX→RX SNR: {info['inbound_snr']:+.1f} dB (our request at their end)")
124+
print(f" RX←TX SNR: {info['response_snr']:+.1f} dB (their response at our end)")
125+
print(f" RSSI: {info['rssi']} dBm")
126+
print(f" Public Key: {info['pub_key']}")
127+
print()
128+
else:
129+
print("No nodes discovered.")
130+
print("This could mean:")
131+
print(" - No nodes are within range")
132+
print(" - No nodes match the filter criteria")
133+
print(" - Radio configuration mismatch")
134+
135+
else:
136+
print("Failed to send discovery request")
137+
138+
# Clean up callback
139+
control_handler.clear_response_callback(discovery_tag)
140+
141+
142+
def main():
143+
"""Main function for running the discovery example."""
144+
import argparse
145+
146+
parser = argparse.ArgumentParser(description="Discover nearby mesh nodes")
147+
parser.add_argument(
148+
"--radio-type",
149+
choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc"],
150+
default="waveshare",
151+
help="Radio hardware type (default: waveshare)",
152+
)
153+
parser.add_argument(
154+
"--serial-port",
155+
default="/dev/ttyUSB0",
156+
help="Serial port for KISS TNC (default: /dev/ttyUSB0)",
157+
)
158+
parser.add_argument(
159+
"--timeout",
160+
type=float,
161+
default=5.0,
162+
help="Discovery timeout in seconds (default: 5.0)",
163+
)
164+
parser.add_argument(
165+
"--filter",
166+
type=lambda x: int(x, 0),
167+
default=0x02,
168+
help="Node type filter mask (default: 0x02 for repeaters)",
169+
)
170+
171+
args = parser.parse_args()
172+
173+
print(f"Using {args.radio_type} radio configuration")
174+
if args.radio_type == "kiss-tnc":
175+
print(f"Serial port: {args.serial_port}")
176+
177+
asyncio.run(
178+
discover_nodes(args.radio_type, args.serial_port, args.timeout, args.filter)
179+
)
180+
181+
182+
if __name__ == "__main__":
183+
main()

src/pymc_core/node/dispatcher.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
AckHandler,
1919
AdvertHandler,
2020
AnonReqResponseHandler,
21+
ControlHandler,
2122
GroupTextHandler,
2223
LoginResponseHandler,
2324
PathHandler,
@@ -227,6 +228,15 @@ def register_default_handlers(
227228
# Keep a reference for the node
228229
self.trace_handler = trace_handler
229230

231+
# CONTROL handler for node discovery
232+
control_handler = ControlHandler(self._log)
233+
self.register_handler(
234+
ControlHandler.payload_type(),
235+
control_handler,
236+
)
237+
# Keep a reference for the node
238+
self.control_handler = control_handler
239+
230240
self._logger.info("Default handlers registered.")
231241

232242
# Set up a fallback handler for unknown packet types

src/pymc_core/node/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .ack import AckHandler
66
from .advert import AdvertHandler
77
from .base import BaseHandler
8+
from .control import ControlHandler
89
from .group_text import GroupTextHandler
910
from .login_response import AnonReqResponseHandler, LoginResponseHandler
1011
from .path import PathHandler
@@ -23,4 +24,5 @@
2324
"ProtocolResponseHandler",
2425
"AnonReqResponseHandler",
2526
"TraceHandler",
27+
"ControlHandler",
2628
]

0 commit comments

Comments
 (0)