feat(p2p): mesh stack, LoRa mock transport, and relay demo

Implement transport abstraction (TCP/iroh), announce and routing table,
multi-hop mesh router, truncated-address link layer, and LoRa mock
medium with fragmentation plus EU868-style duty-cycle accounting.
Add mesh_lora_relay_demo and scripts/mesh-demo.sh. Relax CBOR vs JSON
size assertion to match fixed-size cryptographic overhead. Extend
.gitignore for nested targets and node_modules.

Made-with: Cursor
This commit is contained in:
2026-03-30 21:19:12 +02:00
parent d469999c2a
commit f9ac921a0c
20 changed files with 4042 additions and 6 deletions

View File

@@ -0,0 +1,96 @@
//! Simulated mesh leg: **A (LoRa)** → **B (LoRa + TCP relay)** → **C (TCP)** → zurück über B → **A**.
//!
//! Uses [`quicprochat_p2p::transport_lora::LoRaMockMedium`] — keine Hardware.
//!
//! ```text
//! Node A Node B Node C
//! LoRa addr 0x01 LoRa 0x02 + TCP listen TCP (WiFi / LAN)
//! │ │ │
//! └──── LoRa ───────┘ │
//! └──────── TCP ──────────────┘
//! ```
//!
//! Run: `cargo run -p quicprochat-p2p --example mesh_lora_relay_demo`
use std::sync::Arc;
use std::time::Duration;
use quicprochat_p2p::transport::{MeshTransport, TransportAddr};
use quicprochat_p2p::transport_lora::{DutyCycleTracker, LoRaConfig, LoRaMockMedium};
use quicprochat_p2p::transport_tcp::TcpTransport;
const ADDR_A: [u8; 4] = [0x01, 0, 0, 0];
const ADDR_B: [u8; 4] = [0x02, 0, 0, 0];
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let medium = LoRaMockMedium::new();
let duty = Arc::new(DutyCycleTracker::new(3_600_000));
let lora_a = medium
.connect(ADDR_A, LoRaConfig::default(), Arc::clone(&duty))
.await?;
let lora_b = medium
.connect(ADDR_B, LoRaConfig::default(), Arc::clone(&duty))
.await?;
let tcp_b = TcpTransport::bind("127.0.0.1:0").await?;
let tcp_c = TcpTransport::bind("127.0.0.1:0").await?;
let c_listen = tcp_c.local_addr();
let b_listen = tcp_b.local_addr();
let c_addr = TransportAddr::Socket(c_listen);
let b_addr = TransportAddr::Socket(b_listen);
println!(
"LoRa mock mesh demo: B relays LoRa <-> TCP (B TCP {}, C TCP {})",
b_listen, c_listen
);
let relay = tokio::spawn(async move {
for _ in 0..2 {
tokio::select! {
p = lora_b.recv() => {
let p = p.expect("B LoRa recv");
println!("B: LoRa from {} -> TCP ({} bytes)", p.from, p.data.len());
tcp_b.send(&c_addr, &p.data).await.expect("B TCP send to C");
}
p = tcp_b.recv() => {
let p = p.expect("B TCP recv");
println!("B: TCP -> LoRa A ({} bytes)", p.data.len());
lora_b
.send(&TransportAddr::LoRa(ADDR_A), &p.data)
.await
.expect("B LoRa send to A");
}
}
}
});
let c_task = tokio::spawn(async move {
let pkt = tcp_c.recv().await.expect("C TCP recv");
println!("C: got {} bytes from B relay", pkt.data.len());
assert_eq!(pkt.data, b"hello via mesh");
tcp_c
.send(&b_addr, b"ack from C")
.await
.expect("C TCP send");
});
tokio::time::sleep(Duration::from_millis(50)).await;
lora_a
.send(&TransportAddr::LoRa(ADDR_B), b"hello via mesh")
.await?;
let reply = lora_a.recv().await?;
println!("A: LoRa reply {} bytes", reply.data.len());
assert_eq!(reply.data, b"ack from C");
c_task.await.expect("node C task panicked");
relay.await.expect("relay task panicked");
lora_a.close().await.ok();
println!("Done: LoRa + TCP relay path OK.");
Ok(())
}