use base64::Engine;
use peat_btle::{
config::BleConfig,
platform::{linux::BluerAdapter, BleAdapter, DiscoveredDevice},
security::MeshGenesis,
NodeId, PeatMesh, PeatMeshConfig,
};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let args: Vec<String> = std::env::args().collect();
let callsign = args
.iter()
.position(|a| a == "--callsign")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("PI-RESP");
let genesis_b64 = args
.iter()
.position(|a| a == "--genesis")
.and_then(|i| args.get(i + 1))
.cloned();
let mesh_id_arg = args
.iter()
.position(|a| a == "--mesh-id")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str());
let use_encryption = args.iter().any(|a| a == "--encrypt");
let hostname = std::fs::read_to_string("/etc/hostname")
.unwrap_or_else(|_| "rpi".to_string())
.trim()
.to_string();
let node_id = NodeId::new(
hostname
.bytes()
.fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)),
);
const TEST_SECRET: [u8; 32] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e,
0x1f, 0x20,
];
let (mesh_id, use_encryption, mesh_config) = if let Some(ref b64) = genesis_b64 {
let genesis_bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.unwrap_or_else(|e| {
base64::engine::general_purpose::STANDARD_NO_PAD
.decode(b64)
.unwrap_or_else(|_| panic!("Invalid base64 genesis: {}", e))
});
let genesis =
MeshGenesis::decode(&genesis_bytes).expect("Failed to decode genesis (invalid format)");
let mesh_id = genesis.mesh_id();
let secret = genesis.encryption_secret();
let config = PeatMeshConfig::new(node_id, callsign, &mesh_id).with_encryption(secret);
(mesh_id, true, config)
} else {
let mesh_id = mesh_id_arg.unwrap_or("TEST").to_string();
let mut config = PeatMeshConfig::new(node_id, callsign, &mesh_id);
if use_encryption {
config = config.with_encryption(TEST_SECRET);
}
(mesh_id, use_encryption, config)
};
log::info!("===========================================");
log::info!("Peat BLE Responder (Loopback Test Node)");
log::info!("===========================================");
log::info!("Node ID: 0x{:08X}", node_id.as_u32());
log::info!("Callsign: {}", callsign);
log::info!("Mesh ID: {}", mesh_id);
log::info!("Encrypt: {}", use_encryption);
if genesis_b64.is_some() {
log::info!("Genesis: YES (shared genesis provided)");
}
log::info!("===========================================");
let mesh = Arc::new(RwLock::new(PeatMesh::new(mesh_config)));
log::info!("Mesh initialized, starting BLE adapter...");
let adapter = BluerAdapter::new().await?;
log::info!(
"Bluetooth adapter: {} ({})",
adapter.adapter_name(),
adapter.address().unwrap_or_else(|| "unknown".to_string())
);
let ble_config = BleConfig::new(node_id);
let mut adapter = adapter;
adapter.init(&ble_config).await?;
adapter.set_discovery_callback(Some(Arc::new(move |device: DiscoveredDevice| {
if device.is_peat_node {
log::info!(
"Discovered Peat node: {} ({})",
device.name.as_deref().unwrap_or("unknown"),
device.address
);
if let Some(peer_id) = device.node_id {
log::info!(
" Node ID: 0x{:08X}, RSSI: {}",
peer_id.as_u32(),
device.rssi
);
}
}
})));
adapter.register_gatt_service().await?;
log::info!("GATT service registered");
let adapter = Arc::new(adapter);
let mesh_for_callback = mesh.clone();
adapter
.set_sync_data_callback(move |data| {
let mesh = mesh_for_callback.clone();
tokio::spawn(async move {
let now = now_ms();
let mesh_guard = mesh.read().await;
if let Some(result) =
mesh_guard.on_ble_data_received_anonymous("gatt-peer", &data, now)
{
log::info!(
"Received sync from node 0x{:08X}: counter_changed={}, emergency={}",
result.source_node.as_u32(),
result.counter_changed,
result.is_emergency
);
if let Some(cs) = &result.callsign {
log::info!(" Peer callsign: {}", cs);
}
} else {
log::debug!(
"Received {} bytes (decrypt/parse failed or no change)",
data.len()
);
}
});
})
.await;
adapter.start().await?;
log::info!("===========================================");
log::info!("Responder running. Press Ctrl+C to stop.");
log::info!("Advertising as: PEAT_{}-{:08X}", mesh_id, node_id.as_u32());
log::info!("Waiting for connections...");
log::info!("===========================================");
let mut interval = tokio::time::interval(Duration::from_secs(1));
let mut tick_count = 0u64;
loop {
tokio::select! {
_ = interval.tick() => {
tick_count += 1;
let mesh_guard = mesh.read().await;
let doc = if use_encryption {
mesh_guard.build_full_delta_document(now_ms())
} else {
mesh_guard.build_document()
};
adapter.update_sync_state(&doc).await;
if tick_count % 10 == 0 {
log::debug!("Tick {} - updated sync_state ({} bytes, delta={})", tick_count, doc.len(), use_encryption);
}
if tick_count % 10 == 0 {
let peer_count = mesh_guard.peer_count();
let connected = mesh_guard.connected_count();
let total = mesh_guard.total_count();
let app_docs = mesh_guard.app_document_count();
log::info!(
"Status [tick {}]: {} discovered, {} connected, {} total mesh count, {} app docs",
tick_count, peer_count, connected, total, app_docs
);
}
}
_ = tokio::signal::ctrl_c() => {
log::info!("Shutting down...");
break;
}
}
}
adapter.stop().await?;
log::info!("Responder stopped");
Ok(())
}