Skip to content

Commit bfc1d85

Browse files
committed
add control responder example
1 parent 6f818d9 commit bfc1d85

File tree

5 files changed

+163
-10
lines changed

5 files changed

+163
-10
lines changed

examples/discover_nodes.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@
2323

2424
from pymc_core.protocol.packet_builder import PacketBuilder
2525

26+
# ADV_TYPE_REPEATER = 2, so filter mask is (1 << 2) = 0x04
27+
FILTER_REPEATERS = 0x04 # Bit 2 set for repeater node type
28+
2629

2730
async def discover_nodes(
2831
radio_type: str = "waveshare",
2932
serial_port: str = "/dev/ttyUSB0",
3033
timeout: float = 5.0,
31-
filter_mask: int = 0x02,
34+
filter_mask: int = FILTER_REPEATERS,
3235
):
3336
"""
3437
Discover nearby mesh nodes using control packets.
@@ -37,7 +40,7 @@ async def discover_nodes(
3740
radio_type: Radio hardware type ("waveshare", "uconsole", etc.)
3841
serial_port: Serial port for KISS TNC
3942
timeout: How long to wait for responses (seconds)
40-
filter_mask: Node types to discover (0x02 = repeaters only)
43+
filter_mask: Node types to discover (CONTACT_TYPE_REPEATER = repeaters only)
4144
"""
4245
mesh_node, identity = create_mesh_node("DiscoveryNode", radio_type, serial_port)
4346

@@ -90,7 +93,7 @@ def on_discovery_response(response_data: dict):
9093
control_handler.set_response_callback(discovery_tag, on_discovery_response)
9194

9295
# Create discovery request packet
93-
# filter_mask: 0x02 = bit 1 set = discover repeaters
96+
# filter_mask: 0x04 = bit 2 set (1 << ADV_TYPE_REPEATER where ADV_TYPE_REPEATER=2)
9497
# since: 0 = discover all nodes regardless of modification time
9598
pkt = PacketBuilder.create_discovery_request(
9699
tag=discovery_tag, filter_mask=filter_mask, since=0, prefix_only=False
@@ -164,8 +167,8 @@ def main():
164167
parser.add_argument(
165168
"--filter",
166169
type=lambda x: int(x, 0),
167-
default=0x02,
168-
help="Node type filter mask (default: 0x02 for repeaters)",
170+
default=FILTER_REPEATERS,
171+
help="Node type filter mask (default: 0x04 for repeaters, bit position = node type)",
169172
)
170173

171174
args = parser.parse_args()

examples/respond_to_discovery.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Minimal example: Respond to discovery requests.
4+
5+
This example demonstrates how to listen for discovery requests from other nodes
6+
and automatically respond with this node's information.
7+
8+
Simply run this script and it will respond to any discovery requests until stopped.
9+
"""
10+
11+
import asyncio
12+
13+
from common import create_mesh_node
14+
15+
from pymc_core.protocol.packet_builder import PacketBuilder
16+
17+
# Node type values from C++ AdvertDataHelpers.h
18+
ADV_TYPE_REPEATER = 2
19+
ADV_TYPE_CHAT_NODE = 1
20+
ADV_TYPE_ROOM_SERVER = 3
21+
22+
23+
async def respond_to_discovery(
24+
radio_type: str = "waveshare",
25+
serial_port: str = "/dev/ttyUSB0",
26+
node_type: int = ADV_TYPE_CHAT_NODE,
27+
):
28+
"""
29+
Listen for discovery requests and respond with node information.
30+
31+
Args:
32+
radio_type: Radio hardware type ("waveshare", "uconsole", etc.)
33+
serial_port: Serial port for KISS TNC
34+
node_type: Type of this node (1=chat, 2=repeater, 3=room_server)
35+
"""
36+
mesh_node, identity = create_mesh_node("DiscoveryResponder", radio_type, serial_port)
37+
38+
# Get our public key for responses
39+
our_pub_key = identity.get_public_key()
40+
41+
# Node type names for logging
42+
node_type_names = {
43+
ADV_TYPE_CHAT_NODE: "Chat Node",
44+
ADV_TYPE_REPEATER: "Repeater",
45+
ADV_TYPE_ROOM_SERVER: "Room Server",
46+
}
47+
node_type_name = node_type_names.get(node_type, f"Unknown({node_type})")
48+
49+
# Create callback to handle discovery requests
50+
def on_discovery_request(request_data: dict):
51+
"""Handle incoming discovery request."""
52+
tag = request_data.get("tag", 0)
53+
filter_byte = request_data.get("filter", 0)
54+
prefix_only = request_data.get("prefix_only", False)
55+
snr = request_data.get("snr", 0.0)
56+
rssi = request_data.get("rssi", 0)
57+
58+
print(
59+
f"📡 Discovery request: tag=0x{tag:08X}, "
60+
f"filter=0x{filter_byte:02X}, SNR={snr:+.1f}dB, RSSI={rssi}dBm"
61+
)
62+
63+
# Check if filter matches our node type
64+
filter_mask = 1 << node_type
65+
if (filter_byte & filter_mask) == 0:
66+
print(f" ↳ Filter doesn't match, ignoring")
67+
return
68+
69+
# Create and send discovery response
70+
print(f" ↳ Sending response...")
71+
72+
pkt = PacketBuilder.create_discovery_response(
73+
tag=tag,
74+
node_type=node_type,
75+
inbound_snr=snr,
76+
pub_key=our_pub_key,
77+
prefix_only=prefix_only,
78+
)
79+
80+
# Send the response
81+
asyncio.create_task(send_response(mesh_node, pkt, tag))
82+
83+
async def send_response(node, pkt, tag):
84+
"""Send discovery response packet."""
85+
try:
86+
success = await node.dispatcher.send_packet(pkt, wait_for_ack=False)
87+
if success:
88+
print(f" ✓ Response sent\n")
89+
else:
90+
print(f" ✗ Failed to send\n")
91+
except Exception as e:
92+
print(f" ✗ Error: {e}\n")
93+
94+
# Get the control handler and set up request callback
95+
control_handler = mesh_node.dispatcher.control_handler
96+
if not control_handler:
97+
print("Error: Control handler not available")
98+
return
99+
100+
control_handler.set_request_callback(on_discovery_request)
101+
102+
print(f" Listening for discovery requests as {node_type_name}")
103+
print(f" Node type: {node_type} (filter: 0x{1 << node_type:02X})")
104+
print(f" Public key: {our_pub_key.hex()[:32]}...")
105+
print(f" Press Ctrl+C to stop\n")
106+
107+
# Listen forever
108+
try:
109+
while True:
110+
await asyncio.sleep(1)
111+
except KeyboardInterrupt:
112+
print("\n\n Stopped\n")
113+
114+
control_handler.clear_request_callback()
115+
116+
117+
def main():
118+
"""Main function for running the discovery responder example."""
119+
import argparse
120+
121+
parser = argparse.ArgumentParser(description="Respond to mesh node discovery requests")
122+
parser.add_argument(
123+
"--radio-type",
124+
choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc"],
125+
default="waveshare",
126+
help="Radio hardware type (default: waveshare)",
127+
)
128+
parser.add_argument(
129+
"--serial-port",
130+
default="/dev/ttyUSB0",
131+
help="Serial port for KISS TNC (default: /dev/ttyUSB0)",
132+
)
133+
parser.add_argument(
134+
"--node-type",
135+
type=int,
136+
choices=[1, 2, 3],
137+
default=ADV_TYPE_CHAT_NODE,
138+
help="Node type: 1=chat, 2=repeater, 3=room_server (default: 1)",
139+
)
140+
141+
args = parser.parse_args()
142+
143+
print(f"Using {args.radio_type} radio configuration")
144+
if args.radio_type == "kiss-tnc":
145+
print(f"Serial port: {args.serial_port}")
146+
147+
asyncio.run(respond_to_discovery(args.radio_type, args.serial_port, args.node_type))
148+
149+
150+
if __name__ == "__main__":
151+
main()

src/pymc_core/node/handlers/control.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
CTL_TYPE_NODE_DISCOVER_REQ = 0x80 # Discovery request
1616
CTL_TYPE_NODE_DISCOVER_RESP = 0x90 # Discovery response
1717

18-
# Node type filter flags (bit positions in filter byte)
19-
ADV_TYPE_REPEATER = 1
20-
2118

2219
class ControlHandler:
2320
"""Handler for control packets (payload type 0x0B).

src/pymc_core/protocol/packet_builder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -907,8 +907,8 @@ def create_discovery_request(
907907
```python
908908
import random
909909
tag = random.randint(0, 0xFFFFFFFF)
910-
# Filter for repeaters (bit 1)
911-
packet = PacketBuilder.create_discovery_request(tag, filter_mask=0x02)
910+
# Filter for repeaters: ADV_TYPE_REPEATER=2, so (1 << 2) = 0x04
911+
packet = PacketBuilder.create_discovery_request(tag, filter_mask=0x04)
912912
# Send as zero-hop broadcast
913913
```
914914
"""

src/pymc_core/protocol/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
0x07: "ANON_REQ",
5050
0x08: "PATH",
5151
0x09: "TRACE",
52+
0x0A: "MULTIPART",
53+
0x0B: "CONTROL",
5254
0x0F: "RAW_CUSTOM",
5355
}
5456

0 commit comments

Comments
 (0)