from zerodev1
This commit is contained in:
parent
07d941304c
commit
df090f766b
1 changed files with 254 additions and 0 deletions
254
examples/ble_dual_node_echo.py
Normal file
254
examples/ble_dual_node_echo.py
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Two-node BLE Reticulum echo proof.
|
||||
|
||||
Goal:
|
||||
Run the same program on zerodev1 and zerodev2. Each node:
|
||||
- derives its node name from hostname unless --name is supplied
|
||||
- loads or creates a stable Reticulum app identity
|
||||
- creates one inbound destination
|
||||
- announces that destination over configured Reticulum interfaces
|
||||
- optionally connects to a peer destination hash
|
||||
- prints every message received over a Reticulum link
|
||||
|
||||
After cloning the SD card:
|
||||
1. Change zerodev2 hostname.
|
||||
2. Remove cloned Reticulum transport state on zerodev2.
|
||||
3. Use a unique app identity file per host, or delete the cloned one.
|
||||
4. Configure BLEInterface on both Pis with central+peripheral enabled.
|
||||
5. Run this program on both nodes.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import RNS
|
||||
|
||||
APP_NAME = "ble_reticulum_poc"
|
||||
APP_ASPECT = "echo"
|
||||
|
||||
running = True
|
||||
active_links = {}
|
||||
active_links_lock = threading.Lock()
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||
|
||||
|
||||
def stop(_signum=None, _frame=None):
|
||||
global running
|
||||
running = False
|
||||
|
||||
|
||||
def normalise_hex(h):
|
||||
return h.replace(":", "").replace(" ", "").strip().lower()
|
||||
|
||||
|
||||
def default_identity_path(configdir, node_name):
|
||||
if configdir is None:
|
||||
configdir = os.path.expanduser("~/.reticulum")
|
||||
return os.path.join(configdir, f"{APP_NAME}_{node_name}.identity")
|
||||
|
||||
|
||||
def load_or_create_identity(path):
|
||||
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||
|
||||
if os.path.isfile(path):
|
||||
identity = RNS.Identity.from_file(path)
|
||||
if identity is None:
|
||||
raise RuntimeError(f"Could not load identity from {path}")
|
||||
log(f"Loaded identity: {path}")
|
||||
return identity
|
||||
|
||||
identity = RNS.Identity()
|
||||
if not identity.to_file(path):
|
||||
raise RuntimeError(f"Could not save new identity to {path}")
|
||||
os.chmod(path, 0o600)
|
||||
log(f"Created new identity: {path}")
|
||||
return identity
|
||||
|
||||
|
||||
def link_key(link):
|
||||
return RNS.prettyhexrep(link.link_id)
|
||||
|
||||
|
||||
def send_link_packet(link, text):
|
||||
payload = text.encode("utf-8")
|
||||
RNS.Packet(link, payload, create_receipt=False).send()
|
||||
|
||||
|
||||
def link_packet_received(message, packet):
|
||||
try:
|
||||
text = message.decode("utf-8", errors="replace")
|
||||
except AttributeError:
|
||||
text = str(message)
|
||||
|
||||
link = getattr(packet, "link", None)
|
||||
peer = link_key(link) if link else "unknown-link"
|
||||
log(f"RX link={peer}: {text}")
|
||||
|
||||
|
||||
def link_closed(link):
|
||||
key = link_key(link)
|
||||
with active_links_lock:
|
||||
active_links.pop(key, None)
|
||||
log(f"Link closed: {key}")
|
||||
|
||||
|
||||
def outbound_link_established(link):
|
||||
key = link_key(link)
|
||||
link.set_packet_callback(link_packet_received)
|
||||
link.set_link_closed_callback(link_closed)
|
||||
|
||||
with active_links_lock:
|
||||
active_links[key] = link
|
||||
|
||||
log(f"Outbound link established: {key}")
|
||||
send_link_packet(link, f"hello from {NODE_NAME}")
|
||||
|
||||
|
||||
def inbound_link_established(link):
|
||||
key = link_key(link)
|
||||
link.set_packet_callback(link_packet_received)
|
||||
link.set_link_closed_callback(link_closed)
|
||||
|
||||
with active_links_lock:
|
||||
active_links[key] = link
|
||||
|
||||
log(f"Inbound link established: {key}")
|
||||
send_link_packet(link, f"hello back from {NODE_NAME}")
|
||||
|
||||
|
||||
def direct_packet_received(data, packet):
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
log(f"RX direct packet: {text}")
|
||||
|
||||
|
||||
def announce_loop(destination, interval):
|
||||
while running:
|
||||
destination.announce(app_data=NODE_NAME.encode("utf-8"))
|
||||
log(f"Announced {RNS.prettyhexrep(destination.hash)} as {NODE_NAME}")
|
||||
|
||||
for _ in range(interval):
|
||||
if not running:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def heartbeat_loop(interval):
|
||||
seq = 0
|
||||
while running:
|
||||
time.sleep(interval)
|
||||
seq += 1
|
||||
|
||||
with active_links_lock:
|
||||
links = list(active_links.values())
|
||||
|
||||
for link in links:
|
||||
if link.status == RNS.Link.ACTIVE:
|
||||
send_link_packet(link, f"heartbeat {seq} from {NODE_NAME}")
|
||||
|
||||
|
||||
def wait_for_path(destination_hash, timeout):
|
||||
started = time.time()
|
||||
|
||||
while running and not RNS.Transport.has_path(destination_hash):
|
||||
remaining = timeout - (time.time() - started)
|
||||
if remaining <= 0:
|
||||
return False
|
||||
|
||||
log(f"Requesting path to {RNS.prettyhexrep(destination_hash)}")
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
|
||||
for _ in range(20):
|
||||
if RNS.Transport.has_path(destination_hash) or not running:
|
||||
return RNS.Transport.has_path(destination_hash)
|
||||
time.sleep(0.25)
|
||||
|
||||
return RNS.Transport.has_path(destination_hash)
|
||||
|
||||
|
||||
def connect_to_peer(peer_hexhash, timeout):
|
||||
destination_hash = bytes.fromhex(normalise_hex(peer_hexhash))
|
||||
|
||||
if not wait_for_path(destination_hash, timeout):
|
||||
log(f"No path to peer {RNS.prettyhexrep(destination_hash)} after {timeout}s")
|
||||
return None
|
||||
|
||||
peer_identity = RNS.Identity.recall(destination_hash)
|
||||
if peer_identity is None:
|
||||
log(f"Path exists but identity recall failed for {RNS.prettyhexrep(destination_hash)}")
|
||||
return None
|
||||
|
||||
peer_destination = RNS.Destination(
|
||||
peer_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
APP_ASPECT,
|
||||
)
|
||||
|
||||
log(f"Establishing link to {RNS.prettyhexrep(destination_hash)}")
|
||||
link = RNS.Link(peer_destination)
|
||||
link.set_link_established_callback(outbound_link_established)
|
||||
link.set_link_closed_callback(link_closed)
|
||||
return link
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="BLE Reticulum two-node echo proof")
|
||||
parser.add_argument("--config", default=None, help="Reticulum config directory")
|
||||
parser.add_argument("--identity", default=None, help="App identity file path")
|
||||
parser.add_argument("--name", default=None, help="Node name, default: hostname")
|
||||
parser.add_argument("--peer", default=None, help="Peer destination hash to connect to")
|
||||
parser.add_argument("--announce-interval", type=int, default=30)
|
||||
parser.add_argument("--send-interval", type=int, default=10)
|
||||
parser.add_argument("--path-timeout", type=int, default=120)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
|
||||
args = parse_args()
|
||||
NODE_NAME = args.name or socket.gethostname().split(".")[0]
|
||||
identity_path = args.identity or default_identity_path(args.config, NODE_NAME)
|
||||
|
||||
log(f"Starting node {NODE_NAME}")
|
||||
log(f"Reticulum config: {args.config or '~/.reticulum'}")
|
||||
|
||||
reticulum = RNS.Reticulum(args.config)
|
||||
identity = load_or_create_identity(identity_path)
|
||||
|
||||
destination = RNS.Destination(
|
||||
identity,
|
||||
RNS.Destination.IN,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
APP_ASPECT,
|
||||
)
|
||||
destination.set_link_established_callback(inbound_link_established)
|
||||
destination.set_packet_callback(direct_packet_received)
|
||||
destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
|
||||
|
||||
log(f"Destination hash: {RNS.prettyhexrep(destination.hash)}")
|
||||
log("Use this hash as --peer on the other node.")
|
||||
|
||||
threading.Thread(target=announce_loop, args=(destination, args.announce_interval), daemon=True).start()
|
||||
threading.Thread(target=heartbeat_loop, args=(args.send_interval,), daemon=True).start()
|
||||
|
||||
if args.peer:
|
||||
threading.Thread(target=connect_to_peer, args=(args.peer, args.path_timeout), daemon=True).start()
|
||||
|
||||
while running:
|
||||
time.sleep(0.5)
|
||||
|
||||
log("Stopping")
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue