#![allow(missing_docs)]
use std::sync::Arc;
#[cfg(target_os = "android")]
static ANDROID_LOGGER_INIT: std::sync::Once = std::sync::Once::new();
#[cfg(target_os = "android")]
fn ensure_android_logger() {
ANDROID_LOGGER_INIT.call_once(|| {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Info)
.with_tag("PeatFFI"),
);
});
}
use crate::observer::DisconnectReason as ObserverDisconnectReason;
use crate::peat_mesh::{self, DataReceivedResult as InternalDataReceivedResult};
use crate::peer::{
ConnectionState as InternalConnectionState,
FullStateCountSummary as InternalFullStateCountSummary, IndirectPeer as InternalIndirectPeer,
PeatPeer as InternalPeatPeer, PeerConnectionState as InternalPeerConnectionState,
StateCountSummary as InternalStateCountSummary,
};
use crate::platform::DisconnectReason as PlatformDisconnectReason;
use crate::security::{
DeviceIdentity as InternalDeviceIdentity, IdentityAttestation as InternalIdentityAttestation,
MembershipPolicy, MeshGenesis as InternalMeshGenesis, RegistryResult,
};
use crate::sync::crdt::{EventType as InternalEventType, PeripheralType as InternalPeripheralType};
use crate::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum PeripheralType {
Unknown,
SoldierSensor,
FixedSensor,
Relay,
}
impl From<InternalPeripheralType> for PeripheralType {
fn from(pt: InternalPeripheralType) -> Self {
match pt {
InternalPeripheralType::Unknown => PeripheralType::Unknown,
InternalPeripheralType::SoldierSensor => PeripheralType::SoldierSensor,
InternalPeripheralType::FixedSensor => PeripheralType::FixedSensor,
InternalPeripheralType::Relay => PeripheralType::Relay,
}
}
}
impl From<PeripheralType> for InternalPeripheralType {
fn from(pt: PeripheralType) -> Self {
match pt {
PeripheralType::Unknown => InternalPeripheralType::Unknown,
PeripheralType::SoldierSensor => InternalPeripheralType::SoldierSensor,
PeripheralType::FixedSensor => InternalPeripheralType::FixedSensor,
PeripheralType::Relay => InternalPeripheralType::Relay,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum EventType {
None,
Ping,
NeedAssist,
Emergency,
Moving,
InPosition,
Ack,
}
impl From<InternalEventType> for EventType {
fn from(et: InternalEventType) -> Self {
match et {
InternalEventType::None => EventType::None,
InternalEventType::Ping => EventType::Ping,
InternalEventType::NeedAssist => EventType::NeedAssist,
InternalEventType::Emergency => EventType::Emergency,
InternalEventType::Moving => EventType::Moving,
InternalEventType::InPosition => EventType::InPosition,
InternalEventType::Ack => EventType::Ack,
}
}
}
impl From<EventType> for InternalEventType {
fn from(et: EventType) -> Self {
match et {
EventType::None => InternalEventType::None,
EventType::Ping => InternalEventType::Ping,
EventType::NeedAssist => InternalEventType::NeedAssist,
EventType::Emergency => InternalEventType::Emergency,
EventType::Moving => InternalEventType::Moving,
EventType::InPosition => InternalEventType::InPosition,
EventType::Ack => InternalEventType::Ack,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum DisconnectReason {
LocalRequest,
RemoteRequest,
Timeout,
LinkLoss,
ConnectionFailed,
Unknown,
}
impl From<DisconnectReason> for ObserverDisconnectReason {
fn from(r: DisconnectReason) -> Self {
match r {
DisconnectReason::LocalRequest => ObserverDisconnectReason::LocalRequest,
DisconnectReason::RemoteRequest => ObserverDisconnectReason::RemoteRequest,
DisconnectReason::Timeout => ObserverDisconnectReason::Timeout,
DisconnectReason::LinkLoss => ObserverDisconnectReason::LinkLoss,
DisconnectReason::ConnectionFailed => ObserverDisconnectReason::ConnectionFailed,
DisconnectReason::Unknown => ObserverDisconnectReason::Unknown,
}
}
}
impl From<ObserverDisconnectReason> for DisconnectReason {
fn from(r: ObserverDisconnectReason) -> Self {
match r {
ObserverDisconnectReason::LocalRequest => DisconnectReason::LocalRequest,
ObserverDisconnectReason::RemoteRequest => DisconnectReason::RemoteRequest,
ObserverDisconnectReason::Timeout => DisconnectReason::Timeout,
ObserverDisconnectReason::LinkLoss => DisconnectReason::LinkLoss,
ObserverDisconnectReason::ConnectionFailed => DisconnectReason::ConnectionFailed,
ObserverDisconnectReason::Unknown => DisconnectReason::Unknown,
}
}
}
impl From<PlatformDisconnectReason> for DisconnectReason {
fn from(r: PlatformDisconnectReason) -> Self {
match r {
PlatformDisconnectReason::LocalRequest => DisconnectReason::LocalRequest,
PlatformDisconnectReason::RemoteRequest => DisconnectReason::RemoteRequest,
PlatformDisconnectReason::Timeout => DisconnectReason::Timeout,
PlatformDisconnectReason::LinkLoss => DisconnectReason::LinkLoss,
PlatformDisconnectReason::ConnectionFailed => DisconnectReason::ConnectionFailed,
PlatformDisconnectReason::Unknown => DisconnectReason::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum ConnectionState {
Discovered,
Connecting,
Connected,
Degraded,
Disconnecting,
Disconnected,
Lost,
}
impl From<InternalConnectionState> for ConnectionState {
fn from(s: InternalConnectionState) -> Self {
match s {
InternalConnectionState::Discovered => ConnectionState::Discovered,
InternalConnectionState::Connecting => ConnectionState::Connecting,
InternalConnectionState::Connected => ConnectionState::Connected,
InternalConnectionState::Degraded => ConnectionState::Degraded,
InternalConnectionState::Disconnecting => ConnectionState::Disconnecting,
InternalConnectionState::Disconnected => ConnectionState::Disconnected,
InternalConnectionState::Lost => ConnectionState::Lost,
}
}
}
impl From<ConnectionState> for InternalConnectionState {
fn from(s: ConnectionState) -> Self {
match s {
ConnectionState::Discovered => InternalConnectionState::Discovered,
ConnectionState::Connecting => InternalConnectionState::Connecting,
ConnectionState::Connected => InternalConnectionState::Connected,
ConnectionState::Degraded => InternalConnectionState::Degraded,
ConnectionState::Disconnecting => InternalConnectionState::Disconnecting,
ConnectionState::Disconnected => InternalConnectionState::Disconnected,
ConnectionState::Lost => InternalConnectionState::Lost,
}
}
}
fn event_type_from_u8(value: u8) -> Option<EventType> {
match value {
0 => Some(EventType::None),
1 => Some(EventType::Ping),
2 => Some(EventType::NeedAssist),
3 => Some(EventType::Emergency),
4 => Some(EventType::Moving),
5 => Some(EventType::InPosition),
6 => Some(EventType::Ack),
_ => None,
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct PeatPeer {
pub node_id: u32,
pub identifier: String,
pub name: Option<String>,
pub mesh_id: Option<String>,
pub rssi: i8,
pub is_connected: bool,
pub last_seen_ms: u64,
}
impl From<InternalPeatPeer> for PeatPeer {
fn from(p: InternalPeatPeer) -> Self {
PeatPeer {
node_id: p.node_id.as_u32(),
identifier: p.identifier.clone(),
name: p.name.clone(),
mesh_id: p.mesh_id.clone(),
rssi: p.rssi,
is_connected: p.is_connected,
last_seen_ms: p.last_seen_ms,
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct DataReceivedResult {
pub source_node: u32,
pub is_emergency: bool,
pub is_ack: bool,
pub counter_changed: bool,
pub emergency_changed: bool,
pub total_count: u64,
pub event_timestamp: u64,
pub relay_data: Option<Vec<u8>>,
pub origin_node: Option<u32>,
pub hop_count: u8,
pub callsign: Option<String>,
pub battery_percent: Option<u8>,
pub heart_rate: Option<u8>,
pub event_type: Option<EventType>,
pub latitude: Option<f32>,
pub longitude: Option<f32>,
pub altitude: Option<f32>,
pub activity_level: Option<u8>,
pub alerts: Option<u8>,
pub decoded_translator_frame: Option<DecodedTranslatorFrame>,
pub peat_lite_document: Option<PeatLiteDocumentFrame>,
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct DecodedTranslatorFrame {
pub collection: String,
pub doc_json: String,
pub peer: Option<String>,
}
impl From<crate::peat_mesh::DecodedTranslatorFrame> for DecodedTranslatorFrame {
fn from(f: crate::peat_mesh::DecodedTranslatorFrame) -> Self {
Self {
collection: f.collection,
doc_json: f.doc_json,
peer: f.peer,
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct PeatLiteDocumentFrame {
pub source_node_id: u32,
pub seq_num: u32,
pub flags: u8,
pub is_tombstone: bool,
pub collection: String,
pub doc_id: String,
pub timestamp_ms: i64,
pub body: Vec<u8>,
pub body_utf8: Option<String>,
}
impl From<crate::peat_mesh::PeatLiteDocumentFrame> for PeatLiteDocumentFrame {
fn from(f: crate::peat_mesh::PeatLiteDocumentFrame) -> Self {
let is_tombstone = f.is_tombstone();
let body_utf8 = std::str::from_utf8(&f.body).ok().map(|s| s.to_string());
Self {
source_node_id: f.source_node_id,
seq_num: f.seq_num,
flags: f.flags,
is_tombstone,
collection: f.collection,
doc_id: f.doc_id,
timestamp_ms: f.timestamp_ms,
body: f.body,
body_utf8,
}
}
}
impl From<InternalDataReceivedResult> for DataReceivedResult {
fn from(r: InternalDataReceivedResult) -> Self {
DataReceivedResult {
source_node: r.source_node.as_u32(),
is_emergency: r.is_emergency,
is_ack: r.is_ack,
counter_changed: r.counter_changed,
emergency_changed: r.emergency_changed,
total_count: r.total_count,
event_timestamp: r.event_timestamp,
relay_data: r.relay_data,
origin_node: r.origin_node.map(|n| n.as_u32()),
hop_count: r.hop_count,
callsign: r.callsign,
battery_percent: r.battery_percent,
heart_rate: r.heart_rate,
event_type: r.event_type.and_then(event_type_from_u8),
latitude: r.latitude,
longitude: r.longitude,
altitude: r.altitude,
activity_level: r.activity_level,
alerts: r.alerts,
decoded_translator_frame: r.decoded_translator_frame.map(Into::into),
peat_lite_document: r.peat_lite_document.map(Into::into),
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct PeerConnectionState {
pub node_id: u32,
pub identifier: String,
pub state: ConnectionState,
pub discovered_at: u64,
pub connected_at: Option<u64>,
pub disconnected_at: Option<u64>,
pub disconnect_reason: Option<DisconnectReason>,
pub last_rssi: Option<i8>,
pub connection_count: u32,
pub documents_synced: u32,
pub bytes_received: u64,
pub bytes_sent: u64,
pub last_seen_ms: u64,
pub name: Option<String>,
pub mesh_id: Option<String>,
}
impl From<InternalPeerConnectionState> for PeerConnectionState {
fn from(p: InternalPeerConnectionState) -> Self {
PeerConnectionState {
node_id: p.node_id.as_u32(),
identifier: p.identifier,
state: p.state.into(),
discovered_at: p.discovered_at,
connected_at: p.connected_at,
disconnected_at: p.disconnected_at,
disconnect_reason: p.disconnect_reason.map(|r| r.into()),
last_rssi: p.last_rssi,
connection_count: p.connection_count,
documents_synced: p.documents_synced,
bytes_received: p.bytes_received,
bytes_sent: p.bytes_sent,
last_seen_ms: p.last_seen_ms,
name: p.name,
mesh_id: p.mesh_id,
}
}
}
#[derive(Debug, Clone, Copy, uniffi::Record)]
pub struct StateCountSummary {
pub discovered: u32,
pub connecting: u32,
pub connected: u32,
pub degraded: u32,
pub disconnecting: u32,
pub disconnected: u32,
pub lost: u32,
}
impl From<InternalStateCountSummary> for StateCountSummary {
fn from(s: InternalStateCountSummary) -> Self {
StateCountSummary {
discovered: s.discovered as u32,
connecting: s.connecting as u32,
connected: s.connected as u32,
degraded: s.degraded as u32,
disconnecting: s.disconnecting as u32,
disconnected: s.disconnected as u32,
lost: s.lost as u32,
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct ViaPeerRoute {
pub via_node_id: u32,
pub hop_count: u8,
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct IndirectPeer {
pub node_id: u32,
pub min_hops: u8,
pub via_peers: Vec<ViaPeerRoute>,
pub discovered_at: u64,
pub last_seen_ms: u64,
pub messages_received: u32,
pub callsign: Option<String>,
}
impl From<InternalIndirectPeer> for IndirectPeer {
fn from(p: InternalIndirectPeer) -> Self {
IndirectPeer {
node_id: p.node_id.as_u32(),
min_hops: p.min_hops,
via_peers: p
.via_peers
.iter()
.map(|(&node_id, &hop_count)| ViaPeerRoute {
via_node_id: node_id.as_u32(),
hop_count,
})
.collect(),
discovered_at: p.discovered_at,
last_seen_ms: p.last_seen_ms,
messages_received: p.messages_received,
callsign: p.callsign,
}
}
}
#[derive(Debug, Clone, Copy, uniffi::Record)]
pub struct FullStateCountSummary {
pub direct: StateCountSummary,
pub one_hop: u32,
pub two_hop: u32,
pub three_hop: u32,
}
impl From<InternalFullStateCountSummary> for FullStateCountSummary {
fn from(s: InternalFullStateCountSummary) -> Self {
FullStateCountSummary {
direct: s.direct.into(),
one_hop: s.one_hop as u32,
two_hop: s.two_hop as u32,
three_hop: s.three_hop as u32,
}
}
}
#[derive(uniffi::Object)]
pub struct PeatMesh {
inner: peat_mesh::PeatMesh,
}
#[uniffi::export]
impl PeatMesh {
#[uniffi::constructor]
pub fn new(node_id: u32, callsign: &str, mesh_id: &str) -> Arc<Self> {
#[cfg(target_os = "android")]
ensure_android_logger();
let config = crate::PeatMeshConfig::new(NodeId::new(node_id), callsign, mesh_id);
Arc::new(Self {
inner: peat_mesh::PeatMesh::new(config),
})
}
#[uniffi::constructor]
pub fn new_with_peripheral(
node_id: u32,
callsign: &str,
mesh_id: &str,
peripheral_type: PeripheralType,
) -> Arc<Self> {
#[cfg(target_os = "android")]
ensure_android_logger();
let config = crate::PeatMeshConfig::new(NodeId::new(node_id), callsign, mesh_id)
.with_peripheral_type(peripheral_type.into());
Arc::new(Self {
inner: peat_mesh::PeatMesh::new(config),
})
}
#[uniffi::constructor]
pub fn new_from_genesis(
callsign: &str,
identity: &DeviceIdentity,
genesis: &MeshGenesis,
) -> Arc<Self> {
#[cfg(target_os = "android")]
ensure_android_logger();
Arc::new(Self {
inner: peat_mesh::PeatMesh::from_genesis(
&genesis.inner,
identity.inner.clone(),
callsign,
),
})
}
pub fn decrypt_only(&self, data: &[u8]) -> Option<Vec<u8>> {
self.inner.decrypt_only(data)
}
pub fn build_document(&self) -> Vec<u8> {
self.inner.build_document()
}
pub fn tick(&self, now_ms: u64) -> Option<Vec<u8>> {
self.inner.tick(now_ms)
}
pub fn build_delta_document_for_peer(&self, peer_id: u32, now_ms: u64) -> Option<Vec<u8>> {
self.inner
.build_delta_document_for_peer(&crate::NodeId::new(peer_id), now_ms)
}
pub fn build_full_delta_document(&self, now_ms: u64) -> Vec<u8> {
self.inner.build_full_delta_document(now_ms)
}
pub fn send_emergency(&self, timestamp_ms: u64) -> Vec<u8> {
self.inner.send_emergency(timestamp_ms)
}
pub fn send_ack(&self, timestamp_ms: u64) -> Vec<u8> {
self.inner.send_ack(timestamp_ms)
}
pub fn is_emergency_active(&self) -> bool {
self.inner.is_emergency_active()
}
pub fn is_ack_active(&self) -> bool {
self.inner.is_ack_active()
}
pub fn broadcast_bytes(&self, payload: &[u8]) -> Vec<u8> {
self.inner.broadcast_bytes(payload)
}
#[cfg(feature = "peat-lite-frame")]
pub fn publish_peat_lite_document(&self, envelope_payload: &[u8]) -> Option<Vec<u8>> {
self.inner.publish_peat_lite_document(envelope_payload)
}
pub fn on_ble_discovered(
&self,
identifier: &str,
name: Option<String>,
rssi: i8,
mesh_id: Option<String>,
now_ms: u64,
) -> Option<PeatPeer> {
self.inner
.on_ble_discovered(
identifier,
name.as_deref(),
rssi,
mesh_id.as_deref(),
now_ms,
)
.map(|p| p.into())
}
pub fn on_ble_connected(&self, identifier: &str, now_ms: u64) {
self.inner.on_ble_connected(identifier, now_ms);
}
pub fn on_incoming_connection(&self, identifier: &str, node_id: u32, now_ms: u64) -> bool {
self.inner
.on_incoming_connection(identifier, NodeId::new(node_id), now_ms)
}
pub fn on_ble_disconnected(&self, identifier: &str, reason: DisconnectReason) -> Option<u32> {
self.inner
.on_ble_disconnected(identifier, reason.into())
.map(|n| n.as_u32())
}
pub fn on_ble_data_received(
&self,
identifier: &str,
data: &[u8],
now_ms: u64,
) -> Option<DataReceivedResult> {
self.inner
.on_ble_data_received(identifier, data, now_ms)
.map(|r| r.into())
}
pub fn on_ble_data_received_anonymous(
&self,
identifier: &str,
data: &[u8],
now_ms: u64,
) -> Option<DataReceivedResult> {
self.inner
.on_ble_data_received_anonymous(identifier, data, now_ms)
.map(|r| r.into())
}
pub fn acknowledge_polled_translator_consumer(&self) {
self.inner.acknowledge_polled_translator_consumer();
}
pub fn peer_count(&self) -> u32 {
self.inner.peer_count() as u32
}
pub fn connected_count(&self) -> u32 {
self.inner.connected_count() as u32
}
pub fn total_count(&self) -> u32 {
self.inner.total_count() as u32
}
pub fn get_peer_callsign(&self, node_id: u32) -> Option<String> {
self.inner.get_peer_callsign(NodeId::new(node_id))
}
pub fn get_connected_peers(&self) -> Vec<PeatPeer> {
self.inner
.get_connected_peers()
.into_iter()
.map(|p| p.into())
.collect()
}
pub fn degraded_peer_count(&self) -> u32 {
self.inner.get_degraded_peers().len() as u32
}
pub fn recently_disconnected_count(&self, within_ms: u64, now_ms: u64) -> u32 {
self.inner
.get_recently_disconnected(within_ms, now_ms)
.len() as u32
}
pub fn lost_peer_count(&self) -> u32 {
self.inner.get_lost_peers().len() as u32
}
pub fn is_peer_known(&self, node_id: u32) -> bool {
self.inner.is_peer_known(NodeId::new(node_id))
}
pub fn matches_mesh(&self, device_mesh_id: Option<String>) -> bool {
self.inner.matches_mesh(device_mesh_id.as_deref())
}
pub fn get_peer_connection_state(&self, node_id: u32) -> Option<PeerConnectionState> {
self.inner
.get_peer_connection_state(NodeId::new(node_id))
.map(|p| p.into())
}
pub fn get_degraded_peers(&self) -> Vec<PeerConnectionState> {
self.inner
.get_degraded_peers()
.into_iter()
.map(|p| p.into())
.collect()
}
pub fn get_lost_peers(&self) -> Vec<PeerConnectionState> {
self.inner
.get_lost_peers()
.into_iter()
.map(|p| p.into())
.collect()
}
pub fn get_connection_state_counts(&self) -> StateCountSummary {
self.inner.get_connection_state_counts().into()
}
pub fn get_indirect_peers(&self) -> Vec<IndirectPeer> {
self.inner
.get_indirect_peers()
.into_iter()
.map(|p| p.into())
.collect()
}
pub fn get_full_state_counts(&self) -> FullStateCountSummary {
self.inner.get_full_state_counts().into()
}
pub fn update_location(&self, latitude: f32, longitude: f32, altitude: Option<f32>) {
self.inner.update_location(latitude, longitude, altitude);
}
pub fn clear_location(&self) {
self.inner.clear_location();
}
pub fn update_callsign(&self, callsign: &str) {
self.inner.update_callsign(callsign);
}
pub fn update_heart_rate(&self, bpm: u8) {
self.inner.update_heart_rate(bpm);
}
pub fn update_alerts(&self, alerts: u8) {
self.inner.update_alerts(alerts);
}
pub fn set_peripheral_event(&self, event_type: EventType, timestamp_ms: u64) {
self.inner
.set_peripheral_event(event_type.into(), timestamp_ms);
}
pub fn clear_peripheral_event(&self) {
self.inner.clear_peripheral_event();
}
#[allow(clippy::too_many_arguments)]
pub fn update_peripheral_state(
&self,
callsign: &str,
battery_percent: u8,
heart_rate: Option<u8>,
latitude: Option<f32>,
longitude: Option<f32>,
altitude: Option<f32>,
event_type: Option<EventType>,
timestamp_ms: u64,
) {
self.inner.update_peripheral_state(
callsign,
battery_percent,
heart_rate,
latitude,
longitude,
altitude,
event_type.map(|e| e.into()),
timestamp_ms,
);
}
pub fn has_identity(&self) -> bool {
self.inner.has_identity()
}
pub fn get_public_key(&self) -> Option<Vec<u8>> {
self.inner.public_key().map(|k| k.to_vec())
}
pub fn create_attestation(&self, timestamp_ms: u64) -> Option<Vec<u8>> {
self.inner
.create_attestation(timestamp_ms)
.map(|a| a.encode())
}
pub fn verify_peer_identity(&self, attestation_bytes: &[u8]) -> bool {
if let Some(attestation) = InternalIdentityAttestation::decode(attestation_bytes) {
matches!(
self.inner.verify_peer_identity(&attestation),
RegistryResult::Registered | RegistryResult::Verified
)
} else {
false
}
}
pub fn is_peer_identity_known(&self, node_id: u32) -> bool {
self.inner.is_peer_identity_known(NodeId::new(node_id))
}
pub fn known_identity_count(&self) -> u32 {
self.inner.known_identity_count() as u32
}
pub fn sign(&self, message: &[u8]) -> Option<Vec<u8>> {
self.inner.sign(message).map(|s| s.to_vec())
}
pub fn verify_peer_signature(&self, node_id: u32, message: &[u8], signature: &[u8]) -> bool {
if signature.len() != 64 {
return false;
}
let mut sig_array = [0u8; 64];
sig_array.copy_from_slice(signature);
self.inner
.verify_peer_signature(NodeId::new(node_id), message, &sig_array)
}
pub fn is_encryption_enabled(&self) -> bool {
self.inner.is_encryption_enabled()
}
pub fn is_relay_enabled(&self) -> bool {
self.inner.is_relay_enabled()
}
pub fn check_canned_message(&self, source_node: u32, timestamp: u64, ttl_ms: u64) -> bool {
self.inner
.check_canned_message(source_node, timestamp, ttl_ms)
}
pub fn mark_canned_message_seen(&self, source_node: u32, timestamp: u64) {
self.inner.mark_canned_message_seen(source_node, timestamp);
}
pub fn get_connected_peer_identifiers(&self) -> Vec<String> {
self.inner.get_connected_peer_identifiers()
}
pub fn app_document_count(&self) -> u32 {
self.inner.app_document_count() as u32
}
}
#[cfg(all(feature = "translator-codec", feature = "uniffi"))]
#[uniffi::export]
impl PeatMesh {
pub fn set_decoded_document_json_callback(
&self,
cb: Box<dyn crate::DecodedDocumentJsonCallback>,
) {
self.inner.set_decoded_document_json_callback(cb);
}
}
#[derive(uniffi::Object)]
pub struct DeviceIdentity {
inner: InternalDeviceIdentity,
}
#[uniffi::export]
impl DeviceIdentity {
#[uniffi::constructor]
pub fn generate() -> Arc<Self> {
Arc::new(Self {
inner: InternalDeviceIdentity::generate(),
})
}
pub fn get_public_key(&self) -> Vec<u8> {
self.inner.public_key().to_vec()
}
pub fn get_private_key(&self) -> Vec<u8> {
self.inner.private_key_bytes().to_vec()
}
pub fn get_node_id(&self) -> u32 {
self.inner.node_id().as_u32()
}
pub fn create_attestation(&self, timestamp_ms: u64) -> Vec<u8> {
self.inner.create_attestation(timestamp_ms).encode()
}
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
self.inner.sign(message).to_vec()
}
}
#[derive(uniffi::Object)]
pub struct MeshGenesis {
inner: InternalMeshGenesis,
}
#[uniffi::export]
impl MeshGenesis {
#[uniffi::constructor]
pub fn create(mesh_name: &str, founder_identity: &DeviceIdentity) -> Arc<Self> {
Arc::new(Self {
inner: InternalMeshGenesis::create(
mesh_name,
&founder_identity.inner,
MembershipPolicy::Open,
),
})
}
pub fn get_mesh_id(&self) -> String {
self.inner.mesh_id()
}
pub fn get_encryption_secret(&self) -> Vec<u8> {
self.inner.encryption_secret().to_vec()
}
pub fn encode(&self) -> Vec<u8> {
self.inner.encode()
}
}
#[derive(uniffi::Object)]
pub struct IdentityAttestation {
inner: InternalIdentityAttestation,
}
#[uniffi::export]
impl IdentityAttestation {
pub fn verify(&self) -> bool {
self.inner.verify()
}
pub fn get_node_id(&self) -> u32 {
self.inner.node_id.as_u32()
}
pub fn get_public_key(&self) -> Vec<u8> {
self.inner.public_key.to_vec()
}
}
#[uniffi::export]
pub fn derive_node_id_from_mac(mac_address: &str) -> u32 {
NodeId::from_mac_string(mac_address)
.map(|n| n.as_u32())
.unwrap_or(0)
}
#[uniffi::export]
pub fn create_peat_mesh_with_encryption(
node_id: u32,
callsign: &str,
mesh_id: &str,
encryption_secret: &[u8],
) -> Option<std::sync::Arc<PeatMesh>> {
if encryption_secret.len() != 32 {
return None;
}
let mut secret = [0u8; 32];
secret.copy_from_slice(encryption_secret);
let config =
crate::PeatMeshConfig::new(NodeId::new(node_id), callsign, mesh_id).with_encryption(secret);
Some(std::sync::Arc::new(PeatMesh {
inner: peat_mesh::PeatMesh::new(config),
}))
}
#[uniffi::export]
pub fn restore_device_identity(private_key: &[u8]) -> Option<std::sync::Arc<DeviceIdentity>> {
if private_key.len() < 32 {
return None;
}
let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(&private_key[..32]);
InternalDeviceIdentity::from_private_key(&key_bytes)
.ok()
.map(|inner| std::sync::Arc::new(DeviceIdentity { inner }))
}
#[uniffi::export]
pub fn decode_mesh_genesis(encoded: &[u8]) -> Option<std::sync::Arc<MeshGenesis>> {
InternalMeshGenesis::decode(encoded).map(|inner| std::sync::Arc::new(MeshGenesis { inner }))
}
#[uniffi::export]
pub fn decode_identity_attestation(encoded: &[u8]) -> Option<std::sync::Arc<IdentityAttestation>> {
InternalIdentityAttestation::decode(encoded)
.map(|inner| std::sync::Arc::new(IdentityAttestation { inner }))
}
use crate::reconnect::{
PeerReconnectionStats as InternalPeerReconnectionStats,
ReconnectionConfig as InternalReconnectionConfig,
ReconnectionManager as InternalReconnectionManager,
ReconnectionStatus as InternalReconnectionStatus,
};
#[derive(Debug, Clone, uniffi::Record)]
pub struct ReconnectionConfig {
pub base_delay_ms: u64,
pub max_delay_ms: u64,
pub max_attempts: u32,
pub check_interval_ms: u64,
pub use_flat_delay: bool,
pub reset_on_exhaustion: bool,
}
impl Default for ReconnectionConfig {
fn default() -> Self {
Self {
base_delay_ms: 2000,
max_delay_ms: 60000,
max_attempts: 10,
check_interval_ms: 5000,
use_flat_delay: false,
reset_on_exhaustion: false,
}
}
}
impl From<ReconnectionConfig> for InternalReconnectionConfig {
fn from(c: ReconnectionConfig) -> Self {
let mut config = InternalReconnectionConfig::new(
std::time::Duration::from_millis(c.base_delay_ms),
std::time::Duration::from_millis(c.max_delay_ms),
c.max_attempts,
std::time::Duration::from_millis(c.check_interval_ms),
);
config.use_flat_delay = c.use_flat_delay;
config.reset_on_exhaustion = c.reset_on_exhaustion;
config
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
pub enum ReconnectionStatus {
Ready,
Waiting {
remaining_ms: u64,
},
Exhausted {
attempts: u32,
},
NotTracked,
}
impl From<InternalReconnectionStatus> for ReconnectionStatus {
fn from(s: InternalReconnectionStatus) -> Self {
match s {
InternalReconnectionStatus::Ready => ReconnectionStatus::Ready,
InternalReconnectionStatus::Waiting { remaining } => ReconnectionStatus::Waiting {
remaining_ms: remaining.as_millis() as u64,
},
InternalReconnectionStatus::Exhausted { attempts } => {
ReconnectionStatus::Exhausted { attempts }
}
InternalReconnectionStatus::NotTracked => ReconnectionStatus::NotTracked,
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct PeerReconnectionStats {
pub attempts: u32,
pub max_attempts: u32,
pub disconnected_duration_ms: u64,
pub next_attempt_delay_ms: u64,
}
impl From<InternalPeerReconnectionStats> for PeerReconnectionStats {
fn from(s: InternalPeerReconnectionStats) -> Self {
PeerReconnectionStats {
attempts: s.attempts,
max_attempts: s.max_attempts,
disconnected_duration_ms: s.disconnected_duration.as_millis() as u64,
next_attempt_delay_ms: s.next_attempt_delay.as_millis() as u64,
}
}
}
#[derive(uniffi::Object)]
pub struct ReconnectionManager {
inner: std::sync::Mutex<InternalReconnectionManager>,
}
#[uniffi::export]
impl ReconnectionManager {
#[uniffi::constructor]
pub fn new(config: ReconnectionConfig) -> Arc<Self> {
Arc::new(Self {
inner: std::sync::Mutex::new(InternalReconnectionManager::new(config.into())),
})
}
#[uniffi::constructor]
pub fn with_defaults() -> Arc<Self> {
Arc::new(Self {
inner: std::sync::Mutex::new(InternalReconnectionManager::with_defaults()),
})
}
pub fn track_disconnection(&self, address: String) {
self.inner.lock().unwrap().track_disconnection(address);
}
pub fn is_tracked(&self, address: &str) -> bool {
self.inner.lock().unwrap().is_tracked(address)
}
pub fn get_status(&self, address: &str) -> ReconnectionStatus {
self.inner.lock().unwrap().get_status(address).into()
}
pub fn get_peers_to_reconnect(&self) -> Vec<String> {
self.inner.lock().unwrap().get_peers_to_reconnect()
}
pub fn record_attempt(&self, address: &str) {
self.inner.lock().unwrap().record_attempt(address);
}
pub fn on_connection_success(&self, address: &str) {
self.inner.lock().unwrap().on_connection_success(address);
}
pub fn stop_tracking(&self, address: &str) {
self.inner.lock().unwrap().stop_tracking(address);
}
pub fn clear(&self) {
self.inner.lock().unwrap().clear();
}
pub fn tracked_count(&self) -> u32 {
self.inner.lock().unwrap().tracked_count() as u32
}
pub fn get_peer_stats(&self, address: &str) -> Option<PeerReconnectionStats> {
self.inner
.lock()
.unwrap()
.get_peer_stats(address)
.map(|s| s.into())
}
pub fn check_interval_ms(&self) -> u64 {
self.inner.lock().unwrap().check_interval().as_millis() as u64
}
}
use crate::peer_lifetime::{
PeerInfo as InternalPeerInfo, PeerLifetimeConfig as InternalPeerLifetimeConfig,
PeerLifetimeManager as InternalPeerLifetimeManager,
PeerLifetimeStats as InternalPeerLifetimeStats, StalePeerInfo as InternalStalePeerInfo,
StaleReason as InternalStaleReason,
};
#[derive(Debug, Clone, uniffi::Record)]
pub struct PeerLifetimeConfig {
pub disconnected_timeout_ms: u64,
pub connected_timeout_ms: u64,
pub cleanup_interval_ms: u64,
}
impl Default for PeerLifetimeConfig {
fn default() -> Self {
Self {
disconnected_timeout_ms: 30000,
connected_timeout_ms: 60000,
cleanup_interval_ms: 10000,
}
}
}
impl From<PeerLifetimeConfig> for InternalPeerLifetimeConfig {
fn from(c: PeerLifetimeConfig) -> Self {
InternalPeerLifetimeConfig::new(
std::time::Duration::from_millis(c.disconnected_timeout_ms),
std::time::Duration::from_millis(c.connected_timeout_ms),
std::time::Duration::from_millis(c.cleanup_interval_ms),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum StaleReason {
DisconnectedTimeout,
ConnectedTimeout,
}
impl From<InternalStaleReason> for StaleReason {
fn from(r: InternalStaleReason) -> Self {
match r {
InternalStaleReason::DisconnectedTimeout => StaleReason::DisconnectedTimeout,
InternalStaleReason::ConnectedTimeout => StaleReason::ConnectedTimeout,
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct StalePeerInfo {
pub address: String,
pub reason: StaleReason,
pub time_since_last_seen_ms: u64,
pub was_connected: bool,
}
impl From<InternalStalePeerInfo> for StalePeerInfo {
fn from(p: InternalStalePeerInfo) -> Self {
StalePeerInfo {
address: p.address,
reason: p.reason.into(),
time_since_last_seen_ms: p.time_since_last_seen.as_millis() as u64,
was_connected: p.was_connected,
}
}
}
#[derive(Debug, Clone, Copy, uniffi::Record)]
pub struct PeerLifetimeStats {
pub total_tracked: u32,
pub connected: u32,
pub disconnected: u32,
}
impl From<InternalPeerLifetimeStats> for PeerLifetimeStats {
fn from(s: InternalPeerLifetimeStats) -> Self {
PeerLifetimeStats {
total_tracked: s.total_tracked as u32,
connected: s.connected as u32,
disconnected: s.disconnected as u32,
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct PeerInfo {
pub connected: bool,
pub time_since_last_seen_ms: u64,
pub time_since_first_seen_ms: u64,
pub time_since_disconnect_ms: Option<u64>,
}
impl From<InternalPeerInfo> for PeerInfo {
fn from(p: InternalPeerInfo) -> Self {
PeerInfo {
connected: p.connected,
time_since_last_seen_ms: p.time_since_last_seen.as_millis() as u64,
time_since_first_seen_ms: p.time_since_first_seen.as_millis() as u64,
time_since_disconnect_ms: p.time_since_disconnect.map(|d| d.as_millis() as u64),
}
}
}
#[derive(uniffi::Object)]
pub struct PeerLifetimeManager {
inner: std::sync::Mutex<InternalPeerLifetimeManager>,
}
#[uniffi::export]
impl PeerLifetimeManager {
#[uniffi::constructor]
pub fn new(config: PeerLifetimeConfig) -> Arc<Self> {
Arc::new(Self {
inner: std::sync::Mutex::new(InternalPeerLifetimeManager::new(config.into())),
})
}
#[uniffi::constructor]
pub fn with_defaults() -> Arc<Self> {
Arc::new(Self {
inner: std::sync::Mutex::new(InternalPeerLifetimeManager::with_defaults()),
})
}
pub fn on_peer_activity(&self, address: &str, connected: bool) {
self.inner
.lock()
.unwrap()
.on_peer_activity(address, connected);
}
pub fn on_peer_disconnected(&self, address: &str) {
self.inner.lock().unwrap().on_peer_disconnected(address);
}
pub fn is_tracked(&self, address: &str) -> bool {
self.inner.lock().unwrap().is_tracked(address)
}
pub fn is_connected(&self, address: &str) -> bool {
self.inner.lock().unwrap().is_connected(address)
}
pub fn get_stale_peers(&self) -> Vec<StalePeerInfo> {
self.inner
.lock()
.unwrap()
.get_stale_peers()
.into_iter()
.map(|p| p.into())
.collect()
}
pub fn get_stale_peer_addresses(&self) -> Vec<String> {
self.inner.lock().unwrap().get_stale_peer_addresses()
}
pub fn remove_peer(&self, address: &str) -> bool {
self.inner.lock().unwrap().remove_peer(address)
}
pub fn cleanup_stale_peers(&self) -> Vec<StalePeerInfo> {
self.inner
.lock()
.unwrap()
.cleanup_stale_peers()
.into_iter()
.map(|p| p.into())
.collect()
}
pub fn stats(&self) -> PeerLifetimeStats {
self.inner.lock().unwrap().stats().into()
}
pub fn get_peer_info(&self, address: &str) -> Option<PeerInfo> {
self.inner
.lock()
.unwrap()
.get_peer_info(address)
.map(|p| p.into())
}
pub fn clear(&self) {
self.inner.lock().unwrap().clear();
}
pub fn tracked_count(&self) -> u32 {
self.inner.lock().unwrap().tracked_count() as u32
}
pub fn cleanup_interval_ms(&self) -> u64 {
self.inner.lock().unwrap().cleanup_interval().as_millis() as u64
}
}
use crate::address_rotation::{
AddressRotationHandler as InternalAddressRotationHandler,
AddressRotationStats as InternalAddressRotationStats,
DeviceLookupResult as InternalDeviceLookupResult, DevicePattern as InternalDevicePattern,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum DevicePattern {
WearTak,
WearOs,
Peat,
Unknown,
}
impl From<InternalDevicePattern> for DevicePattern {
fn from(p: InternalDevicePattern) -> Self {
match p {
InternalDevicePattern::WearTak => DevicePattern::WearTak,
InternalDevicePattern::WearOs => DevicePattern::WearOs,
InternalDevicePattern::Peat => DevicePattern::Peat,
InternalDevicePattern::Unknown => DevicePattern::Unknown,
}
}
}
impl DevicePattern {
pub fn rotates_addresses(&self) -> bool {
matches!(self, DevicePattern::WearTak | DevicePattern::WearOs)
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct DeviceLookupResult {
pub node_id: u32,
pub current_address: String,
pub address_changed: bool,
pub previous_address: Option<String>,
}
impl From<InternalDeviceLookupResult> for DeviceLookupResult {
fn from(r: InternalDeviceLookupResult) -> Self {
DeviceLookupResult {
node_id: r.node_id.as_u32(),
current_address: r.current_address,
address_changed: r.address_changed,
previous_address: r.previous_address,
}
}
}
#[derive(Debug, Clone, Copy, uniffi::Record)]
pub struct AddressRotationStats {
pub devices_with_names: u32,
pub total_devices: u32,
pub address_mappings: u32,
}
impl From<InternalAddressRotationStats> for AddressRotationStats {
fn from(s: InternalAddressRotationStats) -> Self {
AddressRotationStats {
devices_with_names: s.devices_with_names as u32,
total_devices: s.total_devices as u32,
address_mappings: s.address_mappings as u32,
}
}
}
#[derive(uniffi::Object)]
pub struct AddressRotationHandler {
inner: std::sync::Mutex<InternalAddressRotationHandler>,
}
#[uniffi::export]
impl AddressRotationHandler {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {
inner: std::sync::Mutex::new(InternalAddressRotationHandler::new()),
})
}
pub fn register_device(&self, name: &str, address: &str, node_id: u32) {
self.inner
.lock()
.unwrap()
.register_device(name, address, NodeId::new(node_id));
}
pub fn lookup_by_name(&self, name: &str) -> Option<u32> {
self.inner
.lock()
.unwrap()
.lookup_by_name(name)
.map(|n| n.as_u32())
}
pub fn lookup_by_address(&self, address: &str) -> Option<u32> {
self.inner
.lock()
.unwrap()
.lookup_by_address(address)
.map(|n| n.as_u32())
}
pub fn get_address(&self, node_id: u32) -> Option<String> {
self.inner
.lock()
.unwrap()
.get_address(&NodeId::new(node_id))
.cloned()
}
pub fn get_name(&self, node_id: u32) -> Option<String> {
self.inner
.lock()
.unwrap()
.get_name(&NodeId::new(node_id))
.cloned()
}
pub fn on_device_discovered(&self, name: &str, address: &str) -> Option<DeviceLookupResult> {
self.inner
.lock()
.unwrap()
.on_device_discovered(name, address)
.map(|r| r.into())
}
pub fn update_address(&self, name: &str, new_address: &str) -> bool {
self.inner.lock().unwrap().update_address(name, new_address)
}
pub fn update_name(&self, node_id: u32, new_name: &str) {
self.inner
.lock()
.unwrap()
.update_name(NodeId::new(node_id), new_name);
}
pub fn remove_device(&self, node_id: u32) {
self.inner
.lock()
.unwrap()
.remove_device(&NodeId::new(node_id));
}
pub fn clear(&self) {
self.inner.lock().unwrap().clear();
}
pub fn device_count(&self) -> u32 {
self.inner.lock().unwrap().device_count() as u32
}
pub fn stats(&self) -> AddressRotationStats {
self.inner.lock().unwrap().stats().into()
}
}
#[uniffi::export]
pub fn detect_device_pattern(name: &str) -> DevicePattern {
crate::address_rotation::detect_device_pattern(name).into()
}
#[uniffi::export]
pub fn is_weartak_device(name: &str) -> bool {
crate::address_rotation::is_weartak_device(name)
}
#[uniffi::export]
pub fn normalize_weartak_name(name: &str) -> String {
crate::address_rotation::normalize_weartak_name(name).to_string()
}
#[uniffi::export]
pub fn device_pattern_rotates_addresses(pattern: DevicePattern) -> bool {
pattern.rotates_addresses()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peripheral_type_round_trip() {
let variants = [
(PeripheralType::Unknown, InternalPeripheralType::Unknown),
(
PeripheralType::SoldierSensor,
InternalPeripheralType::SoldierSensor,
),
(
PeripheralType::FixedSensor,
InternalPeripheralType::FixedSensor,
),
(PeripheralType::Relay, InternalPeripheralType::Relay),
];
for (uniffi_val, internal_val) in &variants {
let converted: InternalPeripheralType = (*uniffi_val).into();
assert_eq!(converted, *internal_val);
let back: PeripheralType = converted.into();
assert_eq!(back, *uniffi_val);
}
}
#[test]
fn test_event_type_round_trip() {
let variants = [
(EventType::None, InternalEventType::None),
(EventType::Ping, InternalEventType::Ping),
(EventType::NeedAssist, InternalEventType::NeedAssist),
(EventType::Emergency, InternalEventType::Emergency),
(EventType::Moving, InternalEventType::Moving),
(EventType::InPosition, InternalEventType::InPosition),
(EventType::Ack, InternalEventType::Ack),
];
for (uniffi_val, internal_val) in &variants {
let converted: InternalEventType = (*uniffi_val).into();
assert_eq!(converted, *internal_val);
let back: EventType = converted.into();
assert_eq!(back, *uniffi_val);
}
}
#[test]
fn test_disconnect_reason_round_trip_observer() {
let variants = [
(
DisconnectReason::LocalRequest,
ObserverDisconnectReason::LocalRequest,
),
(
DisconnectReason::RemoteRequest,
ObserverDisconnectReason::RemoteRequest,
),
(DisconnectReason::Timeout, ObserverDisconnectReason::Timeout),
(
DisconnectReason::LinkLoss,
ObserverDisconnectReason::LinkLoss,
),
(
DisconnectReason::ConnectionFailed,
ObserverDisconnectReason::ConnectionFailed,
),
(DisconnectReason::Unknown, ObserverDisconnectReason::Unknown),
];
for (uniffi_val, observer_val) in &variants {
let converted: ObserverDisconnectReason = (*uniffi_val).into();
assert_eq!(converted, *observer_val);
let back: DisconnectReason = converted.into();
assert_eq!(back, *uniffi_val);
}
}
#[test]
fn test_disconnect_reason_from_platform() {
let variants = [
(
PlatformDisconnectReason::LocalRequest,
DisconnectReason::LocalRequest,
),
(
PlatformDisconnectReason::RemoteRequest,
DisconnectReason::RemoteRequest,
),
(PlatformDisconnectReason::Timeout, DisconnectReason::Timeout),
(
PlatformDisconnectReason::LinkLoss,
DisconnectReason::LinkLoss,
),
(
PlatformDisconnectReason::ConnectionFailed,
DisconnectReason::ConnectionFailed,
),
(PlatformDisconnectReason::Unknown, DisconnectReason::Unknown),
];
for (platform_val, expected) in &variants {
let converted: DisconnectReason = (*platform_val).into();
assert_eq!(converted, *expected);
}
}
#[test]
fn test_connection_state_round_trip() {
let variants = [
(
ConnectionState::Discovered,
InternalConnectionState::Discovered,
),
(
ConnectionState::Connecting,
InternalConnectionState::Connecting,
),
(
ConnectionState::Connected,
InternalConnectionState::Connected,
),
(ConnectionState::Degraded, InternalConnectionState::Degraded),
(
ConnectionState::Disconnecting,
InternalConnectionState::Disconnecting,
),
(
ConnectionState::Disconnected,
InternalConnectionState::Disconnected,
),
(ConnectionState::Lost, InternalConnectionState::Lost),
];
for (uniffi_val, internal_val) in &variants {
let converted: InternalConnectionState = (*uniffi_val).into();
assert_eq!(converted, *internal_val);
let back: ConnectionState = converted.into();
assert_eq!(back, *uniffi_val);
}
}
#[test]
fn test_event_type_from_u8_valid() {
assert_eq!(event_type_from_u8(0), Some(EventType::None));
assert_eq!(event_type_from_u8(1), Some(EventType::Ping));
assert_eq!(event_type_from_u8(2), Some(EventType::NeedAssist));
assert_eq!(event_type_from_u8(3), Some(EventType::Emergency));
assert_eq!(event_type_from_u8(4), Some(EventType::Moving));
assert_eq!(event_type_from_u8(5), Some(EventType::InPosition));
assert_eq!(event_type_from_u8(6), Some(EventType::Ack));
}
#[test]
fn test_event_type_from_u8_invalid() {
assert_eq!(event_type_from_u8(7), None);
assert_eq!(event_type_from_u8(255), None);
}
#[test]
fn test_hive_mesh_new() {
let mesh = PeatMesh::new(42, "ALPHA", "test-mesh");
assert_eq!(mesh.peer_count(), 0);
assert_eq!(mesh.connected_count(), 0);
assert!(!mesh.is_emergency_active());
assert!(!mesh.is_ack_active());
}
#[test]
fn test_hive_mesh_new_with_peripheral() {
let mesh = PeatMesh::new_with_peripheral(42, "BRAVO", "test-mesh", PeripheralType::Relay);
assert_eq!(mesh.peer_count(), 0);
}
#[test]
fn test_hive_mesh_from_genesis() {
let identity = DeviceIdentity::generate();
let genesis = MeshGenesis::create("test-network", &identity);
let mesh = PeatMesh::new_from_genesis("CHARLIE", &identity, &genesis);
assert!(mesh.has_identity());
assert!(mesh.is_encryption_enabled());
}
#[test]
fn test_derive_node_id_from_mac_valid() {
let id = derive_node_id_from_mac("AA:BB:CC:DD:EE:FF");
assert_ne!(id, 0);
}
#[test]
fn test_derive_node_id_from_mac_invalid() {
assert_eq!(derive_node_id_from_mac("not-a-mac"), 0);
assert_eq!(derive_node_id_from_mac(""), 0);
}
#[test]
fn test_create_peat_mesh_with_encryption_valid() {
let secret = [0xABu8; 32];
let mesh = create_peat_mesh_with_encryption(1, "TEST", "mesh-1", &secret);
assert!(mesh.is_some());
assert!(mesh.unwrap().is_encryption_enabled());
}
#[test]
fn test_create_peat_mesh_with_encryption_wrong_length() {
assert!(create_peat_mesh_with_encryption(1, "TEST", "mesh-1", &[0u8; 16]).is_none());
assert!(create_peat_mesh_with_encryption(1, "TEST", "mesh-1", &[]).is_none());
assert!(create_peat_mesh_with_encryption(1, "TEST", "mesh-1", &[0u8; 64]).is_none());
}
#[test]
fn test_device_identity_generate() {
let id = DeviceIdentity::generate();
assert_eq!(id.get_public_key().len(), 32);
assert_eq!(id.get_private_key().len(), 32);
assert_ne!(id.get_node_id(), 0);
}
#[test]
fn test_device_identity_deterministic_node_id() {
let id = DeviceIdentity::generate();
let node_id_1 = id.get_node_id();
let node_id_2 = id.get_node_id();
assert_eq!(node_id_1, node_id_2);
}
#[test]
fn test_device_identity_sign_and_attestation() {
let id = DeviceIdentity::generate();
let sig = id.sign(b"test message");
assert_eq!(sig.len(), 64);
let attestation = id.create_attestation(1000);
assert!(!attestation.is_empty());
}
#[test]
fn test_restore_device_identity_round_trip() {
let id = DeviceIdentity::generate();
let private_key = id.get_private_key();
let restored = restore_device_identity(&private_key);
assert!(restored.is_some());
let restored = restored.unwrap();
assert_eq!(restored.get_public_key(), id.get_public_key());
assert_eq!(restored.get_node_id(), id.get_node_id());
}
#[test]
fn test_restore_device_identity_invalid() {
assert!(restore_device_identity(&[]).is_none());
assert!(restore_device_identity(&[0u8; 16]).is_none());
}
#[test]
fn test_mesh_genesis_create() {
let identity = DeviceIdentity::generate();
let genesis = MeshGenesis::create("my-mesh", &identity);
assert!(!genesis.get_mesh_id().is_empty());
assert_eq!(genesis.get_encryption_secret().len(), 32);
}
#[test]
fn test_mesh_genesis_encode_decode_round_trip() {
let identity = DeviceIdentity::generate();
let genesis = MeshGenesis::create("round-trip-mesh", &identity);
let encoded = genesis.encode();
assert!(!encoded.is_empty());
let decoded = decode_mesh_genesis(&encoded);
assert!(decoded.is_some());
let decoded = decoded.unwrap();
assert_eq!(decoded.get_mesh_id(), genesis.get_mesh_id());
assert_eq!(
decoded.get_encryption_secret(),
genesis.get_encryption_secret()
);
}
#[test]
fn test_decode_mesh_genesis_invalid() {
assert!(decode_mesh_genesis(&[]).is_none());
assert!(decode_mesh_genesis(&[0xFF; 10]).is_none());
}
#[test]
fn test_identity_attestation_round_trip() {
let identity = DeviceIdentity::generate();
let attestation_bytes = identity.create_attestation(5000);
let attestation = decode_identity_attestation(&attestation_bytes);
assert!(attestation.is_some());
let attestation = attestation.unwrap();
assert!(attestation.verify());
assert_eq!(attestation.get_node_id(), identity.get_node_id());
assert_eq!(attestation.get_public_key(), identity.get_public_key());
}
#[test]
fn test_decode_identity_attestation_invalid() {
assert!(decode_identity_attestation(&[]).is_none());
assert!(decode_identity_attestation(&[0xFF; 10]).is_none());
}
#[test]
fn test_hive_mesh_identity_verify_peer() {
let id_a = DeviceIdentity::generate();
let genesis = MeshGenesis::create("id-test", &id_a);
let mesh_a = PeatMesh::new_from_genesis("ALPHA", &id_a, &genesis);
let id_b = DeviceIdentity::generate();
let attestation_bytes = id_b.create_attestation(1000);
let initial_count = mesh_a.known_identity_count();
assert!(mesh_a.verify_peer_identity(&attestation_bytes));
assert!(mesh_a.is_peer_identity_known(id_b.get_node_id()));
assert_eq!(mesh_a.known_identity_count(), initial_count + 1);
}
#[test]
fn test_hive_mesh_verify_peer_identity_invalid() {
let id = DeviceIdentity::generate();
let genesis = MeshGenesis::create("test", &id);
let mesh = PeatMesh::new_from_genesis("TEST", &id, &genesis);
assert!(!mesh.verify_peer_identity(&[]));
assert!(!mesh.verify_peer_identity(&[0xFF; 10]));
}
#[test]
fn test_hive_mesh_sign_and_verify() {
let id_a = DeviceIdentity::generate();
let genesis = MeshGenesis::create("sig-test", &id_a);
let mesh = PeatMesh::new_from_genesis("ALPHA", &id_a, &genesis);
let message = b"important data";
let signature = mesh.sign(message);
assert!(signature.is_some());
let signature = signature.unwrap();
assert_eq!(signature.len(), 64);
}
#[test]
fn test_hive_mesh_verify_peer_signature_wrong_length() {
let id = DeviceIdentity::generate();
let genesis = MeshGenesis::create("test", &id);
let mesh = PeatMesh::new_from_genesis("TEST", &id, &genesis);
assert!(!mesh.verify_peer_signature(42, b"msg", &[0u8; 32]));
assert!(!mesh.verify_peer_signature(42, b"msg", &[]));
}
#[test]
fn test_hive_mesh_build_document() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
let doc = mesh.build_document();
assert!(!doc.is_empty());
}
#[test]
fn test_hive_mesh_emergency_cycle() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
assert!(!mesh.is_emergency_active());
let data = mesh.send_emergency(1000);
assert!(!data.is_empty());
assert!(mesh.is_emergency_active());
let ack_data = mesh.send_ack(2000);
assert!(!ack_data.is_empty());
assert!(mesh.is_ack_active());
}
#[test]
fn test_hive_mesh_update_location() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
mesh.update_location(37.7749, -122.4194, Some(10.0));
mesh.clear_location();
}
#[test]
fn test_hive_mesh_update_peripheral_state() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
mesh.update_peripheral_state(
"ALPHA",
85,
Some(72),
Some(37.7749),
Some(-122.4194),
Some(10.0),
Some(EventType::Moving),
1000,
);
}
#[test]
fn test_hive_mesh_matches_mesh() {
let mesh = PeatMesh::new(1, "TEST", "my-mesh");
assert!(mesh.matches_mesh(Some("my-mesh".to_string())));
assert!(!mesh.matches_mesh(Some("other-mesh".to_string())));
}
#[test]
fn test_hive_mesh_peer_not_known() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
assert!(!mesh.is_peer_known(999));
assert!(mesh.get_peer_callsign(999).is_none());
assert!(mesh.get_peer_connection_state(999).is_none());
}
#[test]
fn test_hive_mesh_empty_peer_lists() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
assert!(mesh.get_connected_peers().is_empty());
assert!(mesh.get_degraded_peers().is_empty());
assert!(mesh.get_lost_peers().is_empty());
assert!(mesh.get_indirect_peers().is_empty());
assert!(mesh.get_connected_peer_identifiers().is_empty());
}
#[test]
fn test_hive_mesh_state_counts_initial() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
let counts = mesh.get_connection_state_counts();
assert_eq!(counts.discovered, 0);
assert_eq!(counts.connected, 0);
assert_eq!(counts.lost, 0);
let full = mesh.get_full_state_counts();
assert_eq!(full.direct.connected, 0);
assert_eq!(full.one_hop, 0);
}
#[test]
fn test_hive_mesh_tick_returns_none_initially() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
assert!(mesh.tick(1000).is_none());
}
#[test]
fn test_hive_mesh_delta_document() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
let delta = mesh.build_delta_document_for_peer(999, 1000);
assert!(delta.is_some());
let full = mesh.build_full_delta_document(1000);
assert!(!full.is_empty());
}
#[test]
fn test_hive_mesh_broadcast_bytes() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
let result = mesh.broadcast_bytes(b"test payload");
assert!(!result.is_empty());
}
#[test]
fn test_hive_mesh_canned_message_dedup() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
assert!(mesh.check_canned_message(42, 1000, 5000));
mesh.mark_canned_message_seen(42, 1000);
assert!(!mesh.check_canned_message(42, 1000, 5000));
}
#[test]
fn test_reconnection_manager_defaults() {
let mgr = ReconnectionManager::with_defaults();
assert_eq!(mgr.tracked_count(), 0);
assert!(mgr.check_interval_ms() > 0);
}
#[test]
fn test_reconnection_manager_track_and_status() {
let mgr = ReconnectionManager::new(ReconnectionConfig {
base_delay_ms: 100,
max_delay_ms: 1000,
max_attempts: 3,
check_interval_ms: 500,
use_flat_delay: false,
reset_on_exhaustion: false,
});
mgr.track_disconnection("AA:BB:CC:DD:EE:FF".to_string());
assert!(mgr.is_tracked("AA:BB:CC:DD:EE:FF"));
assert_eq!(mgr.tracked_count(), 1);
let status = mgr.get_status("AA:BB:CC:DD:EE:FF");
assert!(matches!(
status,
ReconnectionStatus::Ready | ReconnectionStatus::Waiting { .. }
));
assert_eq!(mgr.get_status("unknown"), ReconnectionStatus::NotTracked);
}
#[test]
fn test_reconnection_manager_connection_success() {
let mgr = ReconnectionManager::with_defaults();
mgr.track_disconnection("peer-1".to_string());
assert_eq!(mgr.tracked_count(), 1);
mgr.on_connection_success("peer-1");
assert!(!mgr.is_tracked("peer-1"));
}
#[test]
fn test_reconnection_manager_clear() {
let mgr = ReconnectionManager::with_defaults();
mgr.track_disconnection("a".to_string());
mgr.track_disconnection("b".to_string());
assert_eq!(mgr.tracked_count(), 2);
mgr.clear();
assert_eq!(mgr.tracked_count(), 0);
}
#[test]
fn test_peer_lifetime_manager_defaults() {
let mgr = PeerLifetimeManager::with_defaults();
assert_eq!(mgr.tracked_count(), 0);
assert!(mgr.cleanup_interval_ms() > 0);
}
#[test]
fn test_peer_lifetime_manager_track_peer() {
let mgr = PeerLifetimeManager::with_defaults();
mgr.on_peer_activity("peer-1", true);
assert!(mgr.is_tracked("peer-1"));
assert!(mgr.is_connected("peer-1"));
assert_eq!(mgr.tracked_count(), 1);
let stats = mgr.stats();
assert_eq!(stats.total_tracked, 1);
assert_eq!(stats.connected, 1);
assert_eq!(stats.disconnected, 0);
}
#[test]
fn test_peer_lifetime_manager_disconnect() {
let mgr = PeerLifetimeManager::with_defaults();
mgr.on_peer_activity("peer-1", true);
mgr.on_peer_disconnected("peer-1");
assert!(mgr.is_tracked("peer-1"));
assert!(!mgr.is_connected("peer-1"));
}
#[test]
fn test_peer_lifetime_manager_remove() {
let mgr = PeerLifetimeManager::with_defaults();
mgr.on_peer_activity("peer-1", true);
assert!(mgr.remove_peer("peer-1"));
assert!(!mgr.is_tracked("peer-1"));
assert!(!mgr.remove_peer("peer-1")); }
#[test]
fn test_peer_lifetime_manager_clear() {
let mgr = PeerLifetimeManager::with_defaults();
mgr.on_peer_activity("a", true);
mgr.on_peer_activity("b", false);
mgr.clear();
assert_eq!(mgr.tracked_count(), 0);
}
#[test]
fn test_address_rotation_handler_register_and_lookup() {
let handler = AddressRotationHandler::new();
handler.register_device("WT-WEAROS-1234", "AA:BB:CC:DD:EE:FF", 42);
assert_eq!(handler.lookup_by_name("WT-WEAROS-1234"), Some(42));
assert_eq!(handler.lookup_by_address("AA:BB:CC:DD:EE:FF"), Some(42));
assert_eq!(
handler.get_address(42),
Some("AA:BB:CC:DD:EE:FF".to_string())
);
assert_eq!(handler.get_name(42), Some("WT-WEAROS-1234".to_string()));
}
#[test]
fn test_address_rotation_handler_unknown_lookups() {
let handler = AddressRotationHandler::new();
assert!(handler.lookup_by_name("unknown").is_none());
assert!(handler.lookup_by_address("00:00:00:00:00:00").is_none());
assert!(handler.get_address(999).is_none());
assert!(handler.get_name(999).is_none());
}
#[test]
fn test_address_rotation_handler_remove_and_clear() {
let handler = AddressRotationHandler::new();
handler.register_device("dev-1", "addr-1", 1);
handler.register_device("dev-2", "addr-2", 2);
assert_eq!(handler.device_count(), 2);
handler.remove_device(1);
assert!(handler.lookup_by_name("dev-1").is_none());
handler.clear();
assert_eq!(handler.device_count(), 0);
}
#[test]
fn test_detect_device_pattern_weartak() {
assert_eq!(
detect_device_pattern("WT-WEAROS-1234"),
DevicePattern::WearTak
);
}
#[test]
fn test_is_weartak_device_fn() {
assert!(is_weartak_device("WT-WEAROS-1234"));
assert!(!is_weartak_device("SomeOtherDevice"));
}
#[test]
fn test_device_pattern_rotates() {
assert!(device_pattern_rotates_addresses(DevicePattern::WearTak));
assert!(device_pattern_rotates_addresses(DevicePattern::WearOs));
assert!(!device_pattern_rotates_addresses(DevicePattern::Peat));
assert!(!device_pattern_rotates_addresses(DevicePattern::Unknown));
}
#[test]
fn test_hive_mesh_on_ble_discovered() {
let mesh = PeatMesh::new(1, "TEST", "my-mesh");
let peer = mesh.on_ble_discovered(
"AA:BB:CC:DD:EE:FF",
Some("PEAT_MESH-00000002".to_string()),
-50,
Some("my-mesh".to_string()),
1000,
);
assert!(peer.is_some());
let peer = peer.unwrap();
assert_eq!(peer.identifier, "AA:BB:CC:DD:EE:FF");
assert_eq!(peer.rssi, -50);
}
#[test]
fn test_hive_mesh_on_ble_discovered_no_name() {
let mesh = PeatMesh::new(1, "TEST", "my-mesh");
let peer = mesh.on_ble_discovered(
"AA:BB:CC:DD:EE:FF",
None,
-70,
Some("my-mesh".to_string()),
1000,
);
assert!(peer.is_none());
}
#[test]
fn test_hive_mesh_on_ble_discovered_wrong_mesh() {
let mesh = PeatMesh::new(1, "TEST", "my-mesh");
let peer = mesh.on_ble_discovered(
"AA:BB:CC:DD:EE:FF",
Some("PEAT_MESH-00000002".to_string()),
-50,
Some("other-mesh".to_string()),
1000,
);
assert!(peer.is_none());
}
#[test]
fn test_hive_mesh_on_ble_disconnected_unknown_peer() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
let result = mesh.on_ble_disconnected("unknown-addr", DisconnectReason::Timeout);
assert!(result.is_none());
}
#[test]
fn test_hive_mesh_on_ble_data_received_invalid() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
let result = mesh.on_ble_data_received("unknown", &[0xFF; 10], 1000);
assert!(result.is_none());
}
#[test]
fn test_hive_mesh_decrypt_only_no_encryption() {
let mesh = PeatMesh::new(1, "TEST", "mesh");
let result = mesh.decrypt_only(&[0x01, 0x02, 0x03]);
let _ = result;
}
#[test]
fn test_encrypted_mesh_document_round_trip() {
let id_a = DeviceIdentity::generate();
let genesis = MeshGenesis::create("enc-test", &id_a);
let mesh_a = PeatMesh::new_from_genesis("ALPHA", &id_a, &genesis);
let id_b = DeviceIdentity::generate();
let secret = genesis.get_encryption_secret();
let mesh_b = create_peat_mesh_with_encryption(
id_b.get_node_id(),
"BRAVO",
&genesis.get_mesh_id(),
&secret,
)
.expect("should create encrypted mesh");
let doc = mesh_a.build_document();
assert!(!doc.is_empty());
let decrypted = mesh_b.decrypt_only(&doc);
assert!(decrypted.is_some());
}
}