#![cfg(feature = "translator-codec")]
use peat_btle::peat_mesh::{PeatMesh, PeatMeshConfig};
use peat_btle::translator::{
BleHealthStatus, BlePeripheral, BlePeripheralType, BlePosition, BleTranslator,
};
use peat_btle::NodeId;
use peat_mesh::sync::Document as MeshDocument;
use peat_mesh::transport::{TranslationContext, Translator};
use std::collections::HashMap;
use std::time::SystemTime;
async fn frame_platforms_doc(
translator: &BleTranslator,
peripheral: &BlePeripheral,
mesh_id: Option<&str>,
) -> Vec<u8> {
let value = translator.peripheral_to_platform_in_cell(peripheral, mesh_id);
let fields_map = value
.as_object()
.expect("peripheral_to_platform_in_cell returns a JSON object")
.clone();
let id = fields_map
.get("id")
.and_then(|v| v.as_str())
.map(String::from);
let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
for (k, v) in fields_map {
fields.insert(k, v);
}
let doc = MeshDocument {
id,
fields,
updated_at: SystemTime::now(),
};
let ctx = TranslationContext::outbound().with_collection(translator.platforms_collection());
translator
.encode_outbound(&doc, &ctx)
.await
.expect("encode_outbound returns framed bytes for platforms collection")
}
fn decode_received_frame(receiver: &PeatMesh, framed: &[u8]) -> serde_json::Value {
let result = receiver
.on_ble_data_received_anonymous("ble-test-peer", framed, 1_777_000_000_500)
.expect("receiver surfaces a DataReceivedResult for a 0xB6 platforms frame");
let frame = result
.decoded_translator_frame
.expect("DataReceivedResult.decoded_translator_frame populated for 0xB6 platforms frame");
assert_eq!(
frame.collection, "platforms",
"translator frame collection must round-trip as 'platforms'",
);
let envelope: serde_json::Value = serde_json::from_str(&frame.doc_json)
.expect("decoded_translator_frame.doc_json parses as JSON");
envelope
.get("fields")
.cloned()
.unwrap_or_else(|| panic!("MeshDocument envelope missing `fields` map: {envelope}"))
}
fn fresh_receiver(node_id: u32, callsign: &str) -> PeatMesh {
PeatMesh::new(PeatMeshConfig::new(
NodeId::new(node_id),
callsign,
"WEARTAK",
))
}
fn scout_peripheral(alerts: u8, position: Option<BlePosition>) -> BlePeripheral {
BlePeripheral {
id: 0x6462_A698,
parent_node: 0,
peripheral_type: BlePeripheralType::SoldierSensor,
callsign: "SCOUT-A698".to_string(),
health: BleHealthStatus {
battery_percent: 100,
heart_rate: None,
activity: 0,
alerts,
},
timestamp: 1_777_000_000_000,
position,
}
}
#[tokio::test]
async fn standing_peripheral_round_trips_without_man_down_alert() {
let translator = BleTranslator::with_defaults();
let receiver = fresh_receiver(0x089F_A635, "TABLET");
let standing = scout_peripheral(0, None);
let framed = frame_platforms_doc(&translator, &standing, Some("WEARTAK")).await;
let payload = decode_received_frame(&receiver, &framed);
assert_eq!(
payload.get("id").and_then(|v| v.as_str()),
Some("ble-6462A698"),
);
assert_eq!(
payload.get("name").and_then(|v| v.as_str()),
Some("SCOUT-A698"),
);
let alerts = payload
.get("alerts")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<&str>>())
.unwrap_or_default();
assert!(
!alerts.contains(&"man_down"),
"Standing peripheral must NOT carry man_down; got alerts={:?}",
alerts,
);
}
#[tokio::test]
async fn prone_peripheral_propagates_man_down_alert_to_receiver() {
let translator = BleTranslator::with_defaults();
let receiver = fresh_receiver(0x089F_A635, "TABLET");
let prone = scout_peripheral(BleHealthStatus::ALERT_MAN_DOWN, None);
let framed = frame_platforms_doc(&translator, &prone, Some("WEARTAK")).await;
let payload = decode_received_frame(&receiver, &framed);
let alerts = payload
.get("alerts")
.and_then(|v| v.as_array())
.expect("Prone peripheral encodes alerts array on the wire");
let alert_strings: Vec<&str> = alerts.iter().filter_map(|v| v.as_str()).collect();
assert!(
alert_strings.contains(&"man_down"),
"expected man_down to round-trip through translator; got {:?}",
alert_strings,
);
assert_eq!(
payload.get("id").and_then(|v| v.as_str()),
Some("ble-6462A698"),
);
}
#[tokio::test]
async fn prone_with_low_battery_propagates_both_alerts() {
let translator = BleTranslator::with_defaults();
let receiver = fresh_receiver(0x089F_A635, "TABLET");
let combined = scout_peripheral(
BleHealthStatus::ALERT_MAN_DOWN | BleHealthStatus::ALERT_LOW_BATTERY,
None,
);
let framed = frame_platforms_doc(&translator, &combined, Some("WEARTAK")).await;
let payload = decode_received_frame(&receiver, &framed);
let alert_strings: Vec<&str> = payload
.get("alerts")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
assert!(
alert_strings.contains(&"man_down"),
"compound alerts must include man_down; got {:?}",
alert_strings,
);
assert!(
alert_strings.contains(&"low_battery"),
"compound alerts must include low_battery; got {:?}",
alert_strings,
);
}
#[tokio::test]
async fn publish_translator_frame_round_trips_man_down_through_receiver() {
use peat_btle::translator::BleTranslator;
let publisher = PeatMesh::new(PeatMeshConfig::new(
NodeId::new(0x6462_A698),
"SCOUT-A698",
"WEARTAK",
));
let receiver = fresh_receiver(0x089F_A635, "TABLET");
let translator = BleTranslator::with_defaults();
let prone = scout_peripheral(BleHealthStatus::ALERT_MAN_DOWN, None);
let value = translator.peripheral_to_platform_in_cell(&prone, Some("WEARTAK"));
let fields_map = value
.as_object()
.expect("peripheral_to_platform_in_cell returns Object")
.clone();
let id = fields_map
.get("id")
.and_then(|v| v.as_str())
.map(String::from);
let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
for (k, v) in fields_map {
fields.insert(k, v);
}
let doc = MeshDocument {
id,
fields,
updated_at: SystemTime::now(),
};
let wire_bytes = publisher
.publish_translator_frame("platforms", &doc)
.expect("publish_translator_frame returns wire bytes for the platforms collection");
let payload = decode_received_frame(&receiver, &wire_bytes);
let alert_strings: Vec<&str> = payload
.get("alerts")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
assert!(
alert_strings.contains(&"man_down"),
"publish_translator_frame must propagate man_down through receive; got {:?}",
alert_strings,
);
assert_eq!(
payload.get("name").and_then(|v| v.as_str()),
Some("SCOUT-A698"),
);
}
#[tokio::test]
async fn publish_platform_advertisement_round_trips_man_down_through_receiver() {
let publisher = PeatMesh::new(PeatMeshConfig::new(
NodeId::new(0x6462_A698),
"SCOUT-A698",
"WEARTAK",
));
let receiver = fresh_receiver(0x089F_A635, "TABLET");
let prone = scout_peripheral(BleHealthStatus::ALERT_MAN_DOWN, None);
let wire_bytes = publisher
.publish_platform_advertisement(&prone)
.expect("publish_platform_advertisement returns wire bytes for a typed BlePeripheral");
let payload = decode_received_frame(&receiver, &wire_bytes);
let alert_strings: Vec<&str> = payload
.get("alerts")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
assert!(
alert_strings.contains(&"man_down"),
"slim publish path must propagate man_down through receive; got {:?}",
alert_strings,
);
assert_eq!(
payload.get("name").and_then(|v| v.as_str()),
Some("SCOUT-A698"),
);
}
#[tokio::test]
async fn slim_and_generic_publish_paths_emit_identical_wire_bytes() {
use peat_btle::translator::BleTranslator;
let publisher = PeatMesh::new(PeatMeshConfig::new(
NodeId::new(0x6462_A698),
"SCOUT-A698",
"WEARTAK",
));
let prone = scout_peripheral(BleHealthStatus::ALERT_MAN_DOWN, None);
let slim_bytes = publisher
.publish_platform_advertisement(&prone)
.expect("slim path returns bytes");
let translator = BleTranslator::with_defaults();
let value = translator.peripheral_to_platform_in_cell(&prone, Some("WEARTAK"));
let fields_map = value.as_object().expect("Object").clone();
let id = fields_map
.get("id")
.and_then(|v| v.as_str())
.map(String::from);
let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
for (k, v) in fields_map {
fields.insert(k, v);
}
let doc = MeshDocument {
id,
fields,
updated_at: SystemTime::now(),
};
let generic_bytes = publisher
.publish_translator_frame("platforms", &doc)
.expect("generic path returns bytes");
assert_eq!(
slim_bytes, generic_bytes,
"slim and generic publish paths must emit byte-identical \
wire bytes for the same peripheral; receivers must not be \
able to distinguish the two publishers",
);
}
#[tokio::test]
async fn publish_translator_frame_declines_unknown_collection() {
let publisher = PeatMesh::new(PeatMeshConfig::new(
NodeId::new(0x6462_A698),
"SCOUT-A698",
"WEARTAK",
));
let doc = MeshDocument {
id: Some("ble-6462A698".to_string()),
fields: HashMap::new(),
updated_at: SystemTime::now(),
};
assert!(
publisher
.publish_translator_frame("not-a-real-collection", &doc)
.is_none(),
"publish_translator_frame must decline (return None) for unknown collections",
);
}
#[tokio::test]
async fn position_and_alerts_coexist_on_wire() {
let translator = BleTranslator::with_defaults();
let receiver = fresh_receiver(0x089F_A635, "TABLET");
let with_position = scout_peripheral(
BleHealthStatus::ALERT_MAN_DOWN,
Some(BlePosition {
latitude: 33.71571,
longitude: -84.41128,
altitude: Some(285.5),
accuracy: None,
}),
);
let framed = frame_platforms_doc(&translator, &with_position, Some("WEARTAK")).await;
let payload = decode_received_frame(&receiver, &framed);
let lat = payload
.get("lat")
.and_then(|v| v.as_f64())
.expect("position encoded peripheral surfaces lat in decoded JSON");
let lon = payload
.get("lon")
.and_then(|v| v.as_f64())
.expect("position encoded peripheral surfaces lon in decoded JSON");
assert!((lat - 33.71571_f64).abs() < 1e-3, "lat mismatch: got {lat}");
assert!(
(lon - (-84.41128_f64)).abs() < 1e-3,
"lon mismatch: got {lon}"
);
let alert_strings: Vec<&str> = payload
.get("alerts")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
assert!(
alert_strings.contains(&"man_down"),
"alerts must remain alongside a position payload; got {:?}",
alert_strings,
);
}