feat(fapp): add integration demo + update status
examples/fapp_demo.rs: - Therapist publishes SlotAnnounce - Relay caches and handles query - Patient sends SlotQuery, gets response - Shows full FappRouter API flow docs/status.md: - Updated FAPP integration status - FappRouter now implemented - Remaining: multi-node test, SlotReserve/Confirm, LoRa
This commit is contained in:
@@ -39,3 +39,7 @@ thiserror = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "fapp_demo"
|
||||||
|
path = "../../examples/fapp_demo.rs"
|
||||||
|
|||||||
@@ -19,19 +19,31 @@
|
|||||||
- Location hint is PLZ only (e.g. "80331") — never exact address
|
- Location hint is PLZ only (e.g. "80331") — never exact address
|
||||||
- Anti-spam: Approbation hash binding, signature verification, sequence-based dedup, rate limiting, TTL enforcement
|
- Anti-spam: Approbation hash binding, signature verification, sequence-based dedup, rate limiting, TTL enforcement
|
||||||
|
|
||||||
### FAPP integration — next steps
|
### FAPP integration — status
|
||||||
|
|
||||||
**Current state:** Spec (`docs/specs/fapp-protocol.md`) and Rust module (`crates/quicprochat-p2p/src/fapp.rs`) with store, matching, and tests are in place. **FAPP is not integrated** — `mesh_router.rs` does not decode, route, or emit FAPP CBOR yet.
|
**2026-04-01: FappRouter implemented!**
|
||||||
|
|
||||||
**Next steps**
|
New `fapp_router.rs` module:
|
||||||
1. **Wire into `mesh_router`:** On incoming mesh traffic, recognize FAPP message types; verify signatures and hop rules per spec; update `FappStore`; forward SlotAnnounce/Query/Response where applicable; align with `announce.rs` capability bits (`CAP_FAPP_*`).
|
- `FappAction` enum: Ignore, Dropped, Forward, QueryResponse
|
||||||
2. **Test on stack:** Multi-node TCP/mesh demo first (two+ nodes, therapist/relay/patient roles); then LoRa hardware when available (airtime, fragmentation, duty cycle).
|
- Wire format: 1-byte tag (0x01-0x05) + CBOR body
|
||||||
3. **After wiring:** SlotReserve/SlotConfirm E2E (e.g. X25519), anonymous return-path for SlotQuery responses, per-therapist rate limits in `FappStore`.
|
- `FappRouter` struct with shared `RoutingTable` + `TransportManager`
|
||||||
|
- `handle_incoming()` decodes and dispatches FAPP frames
|
||||||
|
- `process_slot_announce()` with relay/flood logic (dedup, hop check, store, forward)
|
||||||
|
- `process_slot_query()` answers from local `FappStore`
|
||||||
|
- `broadcast_announce()` / `send_query()` for outbound floods
|
||||||
|
- `drain_pending_sends()` for async send integration
|
||||||
|
- 3 unit tests passing
|
||||||
|
|
||||||
|
**Remaining steps**
|
||||||
|
1. **Integration test:** Multi-node demo (therapist → relay → patient flow)
|
||||||
|
2. **Wire to P2pNode:** Add `FappRouter` to `start_with_mesh()` or similar
|
||||||
|
3. **SlotReserve/SlotConfirm:** E2E encrypted reservation flow
|
||||||
|
4. **LoRa test:** Verify FAPP over constrained links
|
||||||
|
|
||||||
**Definition of done**
|
**Definition of done**
|
||||||
- Router end-to-end path for FAPP matches spec (verify, dedup, TTL, forwarding semantics).
|
- announce → query → response works over multi-hop (automated or manual)
|
||||||
- Integration proof: announce → query → response over at least one multi-hop path (automated test or documented manual run).
|
- SlotReserve/Confirm E2E encryption works
|
||||||
- Hardware check: same scenario on LoRa, or a short note in status on what blocked it.
|
- LoRa test or documented blocker
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
142
examples/fapp_demo.rs
Normal file
142
examples/fapp_demo.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
//! Minimal FAPP ([`quicprochat_p2p::fapp`]) demo: therapist publishes a slot, relay caches it,
|
||||||
|
//! patient floods a query; the relay answers from its in-memory store.
|
||||||
|
//!
|
||||||
|
//! Uses mock [`TransportAddr`] values and [`RoutingTable`](quicprochat_p2p::routing_table::RoutingTable)
|
||||||
|
//! seeds — no sockets or iroh.
|
||||||
|
//!
|
||||||
|
//! Run: `cargo run -p quicprochat-p2p --example fapp_demo`
|
||||||
|
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use quicprochat_p2p::announce::{MeshAnnounce, CAP_RELAY};
|
||||||
|
use quicprochat_p2p::fapp::{
|
||||||
|
Fachrichtung, FappStore, Kostentraeger, Modalitaet, SlotAnnounce, SlotQuery, SlotType,
|
||||||
|
TimeSlot, CAP_FAPP_PATIENT, CAP_FAPP_RELAY, CAP_FAPP_THERAPIST,
|
||||||
|
};
|
||||||
|
use quicprochat_p2p::fapp_router::{FappAction, FappRouter};
|
||||||
|
use quicprochat_p2p::identity::MeshIdentity;
|
||||||
|
use quicprochat_p2p::routing_table::RoutingTable;
|
||||||
|
use quicprochat_p2p::transport::TransportAddr;
|
||||||
|
use quicprochat_p2p::transport_manager::TransportManager;
|
||||||
|
|
||||||
|
/// Insert one synthetic route so [`FappRouter::broadcast_announce`] / [`FappRouter::send_query`]
|
||||||
|
/// have a next hop (see [`quicprochat_p2p::fapp_router::FappRouter::drain_pending_sends`]).
|
||||||
|
fn seed_mock_neighbor(table: &mut RoutingTable, next_hop: TransportAddr) {
|
||||||
|
let peer = MeshIdentity::generate();
|
||||||
|
let mut announce = MeshAnnounce::with_sequence(&peer, CAP_RELAY, vec![], 8, 1);
|
||||||
|
announce.hop_count = 1;
|
||||||
|
let _ = table.update(&announce, "mock", next_hop);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let relay_inbox: TransportAddr = TransportAddr::Raw(b"link-therapist-to-relay".to_vec());
|
||||||
|
let patient_outbound: TransportAddr = TransportAddr::Raw(b"link-patient-flood".to_vec());
|
||||||
|
|
||||||
|
// --- Therapist: can publish; routing table points at the "relay" hop ---
|
||||||
|
let therapist_routes = Arc::new(RwLock::new(RoutingTable::new(Duration::from_secs(300))));
|
||||||
|
{
|
||||||
|
let mut table = therapist_routes
|
||||||
|
.write()
|
||||||
|
.map_err(|e| anyhow::anyhow!("therapist routes lock poisoned: {e}"))?;
|
||||||
|
seed_mock_neighbor(&mut table, relay_inbox.clone());
|
||||||
|
}
|
||||||
|
let therapist = FappRouter::new(
|
||||||
|
FappStore::new(),
|
||||||
|
therapist_routes,
|
||||||
|
Arc::new(TransportManager::new()),
|
||||||
|
CAP_FAPP_THERAPIST,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Patient + relay: start with an empty table so the announce is cached but not re-flooded ---
|
||||||
|
let patient_relay_routes = Arc::new(RwLock::new(RoutingTable::new(Duration::from_secs(300))));
|
||||||
|
let patient_relay = FappRouter::new(
|
||||||
|
FappStore::new(),
|
||||||
|
Arc::clone(&patient_relay_routes),
|
||||||
|
Arc::new(TransportManager::new()),
|
||||||
|
CAP_FAPP_PATIENT | CAP_FAPP_RELAY,
|
||||||
|
);
|
||||||
|
|
||||||
|
let therapist_id = MeshIdentity::generate();
|
||||||
|
let announce = SlotAnnounce::new(
|
||||||
|
&therapist_id,
|
||||||
|
vec![Fachrichtung::Verhaltenstherapie],
|
||||||
|
vec![Modalitaet::Praxis],
|
||||||
|
vec![Kostentraeger::GKV],
|
||||||
|
"80331".into(),
|
||||||
|
vec![TimeSlot {
|
||||||
|
start_unix: 1,
|
||||||
|
duration_minutes: 50,
|
||||||
|
slot_type: SlotType::Therapie,
|
||||||
|
}],
|
||||||
|
[0xAA; 32],
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1) Therapist broadcasts SlotAnnounce (queued to mock relay address).
|
||||||
|
therapist
|
||||||
|
.broadcast_announce(announce.clone())
|
||||||
|
.context("broadcast_announce")?;
|
||||||
|
let mut pending = therapist
|
||||||
|
.drain_pending_sends()
|
||||||
|
.context("therapist drain_pending_sends")?;
|
||||||
|
let (to_relay, announce_wire) = pending
|
||||||
|
.pop()
|
||||||
|
.context("expected one pending frame from therapist")?;
|
||||||
|
println!("Therapist queued announce -> {to_relay} ({} bytes)", announce_wire.len());
|
||||||
|
assert_eq!(to_relay, relay_inbox);
|
||||||
|
|
||||||
|
// 2) Relay receives the wire frame (in real code: [`TransportManager::send`] / recv).
|
||||||
|
let relay_action = patient_relay.handle_incoming(&announce_wire);
|
||||||
|
println!("Relay handled announce: {relay_action:?}");
|
||||||
|
|
||||||
|
// Add a mock neighbor so `send_query` can enqueue a flood (API demo only).
|
||||||
|
{
|
||||||
|
let mut table = patient_relay_routes
|
||||||
|
.write()
|
||||||
|
.map_err(|e| anyhow::anyhow!("patient/relay routes lock poisoned: {e}"))?;
|
||||||
|
seed_mock_neighbor(&mut table, patient_outbound.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Patient floods a SlotQuery.
|
||||||
|
let query = SlotQuery {
|
||||||
|
query_id: [0x42; 16],
|
||||||
|
fachrichtung: Some(Fachrichtung::Verhaltenstherapie),
|
||||||
|
modalitaet: None,
|
||||||
|
kostentraeger: None,
|
||||||
|
plz_prefix: Some("803".into()),
|
||||||
|
earliest: None,
|
||||||
|
latest: None,
|
||||||
|
slot_type: None,
|
||||||
|
max_results: 5,
|
||||||
|
};
|
||||||
|
patient_relay.send_query(query).context("send_query")?;
|
||||||
|
pending = patient_relay
|
||||||
|
.drain_pending_sends()
|
||||||
|
.context("patient drain_pending_sends")?;
|
||||||
|
let (flood_dest, query_wire) = pending
|
||||||
|
.pop()
|
||||||
|
.context("expected one pending query frame")?;
|
||||||
|
println!("Patient queued query flood -> {flood_dest} ({} bytes)", query_wire.len());
|
||||||
|
assert_eq!(flood_dest, patient_outbound);
|
||||||
|
|
||||||
|
// 4) Same relay node decodes the query and answers from [`FappStore`].
|
||||||
|
match patient_relay.handle_incoming(&query_wire) {
|
||||||
|
FappAction::QueryResponse(resp) => {
|
||||||
|
println!(
|
||||||
|
"Relay query response: query_id={:02x?}.. matches={}",
|
||||||
|
&resp.query_id[..4],
|
||||||
|
resp.matches.len()
|
||||||
|
);
|
||||||
|
let first = resp
|
||||||
|
.matches
|
||||||
|
.first()
|
||||||
|
.context("expected at least one matching announce")?;
|
||||||
|
assert_eq!(first.id, announce.id);
|
||||||
|
}
|
||||||
|
other => anyhow::bail!("expected QueryResponse, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user