1use crate::constants::{HOP_COUNT_EMA_ALPHA, LATENCY_EMA_ALPHA, OBSERVED_STATS_COMPACT_THRESHOLD};
8use crate::events::{DorsEscalationPhase, DorsEscalationReasonCode, DorsReasonCode, Event};
9use crate::telemetry::routing::{RoutingDecision, RoutingPhase, RoutingReasonCode};
10use crate::{Error, Result};
11use chrono::Utc;
12use offline_protocol_core::{Message, Username, WireCodec};
13use offline_protocol_router::{
14 display_routing_score, DorsConfig, EscalationTriggerReason, TransportScore, TransportSelector,
15};
16use offline_protocol_transport::{
17 nostr::ResolveRequest, Error as TransportError, Transport, TransportMetrics, TransportStatus,
18 TransportType,
19};
20use std::collections::{HashMap, HashSet};
21use std::sync::Arc;
22use std::time::Instant;
23use tracing::{debug, warn};
24
25pub struct TransportManager {
27 transports: HashMap<TransportType, Arc<dyn Transport>>,
33
34 current_transport: Option<TransportType>,
36
37 selector: TransportSelector,
39
40 observations: HashMap<TransportType, ObservedStats>,
42
43 dors_event_callback: Option<Arc<dyn Fn(Event) + Send + Sync>>,
45
46 routing_decision_callback: Option<Arc<dyn Fn(RoutingDecision) + Send + Sync>>,
51
52 routing_diagnostic: bool,
57
58 last_escalation_trigger_emitted: Option<(DorsEscalationReasonCode, std::time::Instant)>,
60
61 peer_binary_wire: HashSet<String>,
65
66 device_battery: Option<u8>,
70
71 device_is_charging: bool,
73}
74
75const MESH_TRANSPORTS: &[TransportType] = &[TransportType::WiFiDirect, TransportType::BLE];
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct MeshNeighbor {
88 pub peer_id: String,
90 pub rssi: Option<i16>,
92 pub transport: TransportType,
94}
95
96impl MeshNeighbor {
97 pub fn link_quality(&self) -> u8 {
103 match self.rssi {
104 Some(rssi) => offline_protocol_transport::LinkQuality::from_rssi(rssi).value(),
105 None => offline_protocol_transport::DEFAULT_LINK_QUALITY_WITHOUT_RSSI,
106 }
107 }
108}
109
110const ESCALATION_TRIGGER_DEDUPE_SECS: u64 = 30;
112
113const MAX_PEER_BINARY_WIRE_ENTRIES: usize = 4096;
118
119fn has_send_confirmation_loop(transport_type: TransportType) -> bool {
123 match transport_type {
124 TransportType::Internet | TransportType::Reticulum | TransportType::Nostr => true,
125 TransportType::BLE | TransportType::WiFiDirect => false,
126 }
127}
128
129#[derive(Debug, Default, Clone)]
130struct ObservedStats {
131 success_count: u32,
132 failure_count: u32,
133 latency_ema: Option<f32>,
134 hop_ema: Option<f32>,
135 last_success_at: Option<Instant>,
136}
137
138impl ObservedStats {
139 fn record_success(&mut self, latency_ms: u32, hop_count: u8) {
140 self.success_count = self.success_count.saturating_add(1);
141 self.latency_ema = Some(update_ema(
142 self.latency_ema,
143 latency_ms as f32,
144 LATENCY_EMA_ALPHA,
145 ));
146 self.hop_ema = Some(update_ema(
147 self.hop_ema,
148 hop_count as f32,
149 HOP_COUNT_EMA_ALPHA,
150 ));
151 self.last_success_at = Some(Instant::now());
152 self.compact();
153 }
154
155 fn record_failure(&mut self) {
156 self.failure_count = self.failure_count.saturating_add(1);
157 self.compact();
158 }
159
160 fn compact(&mut self) {
161 let total = self.success_count.saturating_add(self.failure_count);
162 if total > OBSERVED_STATS_COMPACT_THRESHOLD {
163 self.success_count /= 2;
164 self.failure_count /= 2;
165 }
166 }
167
168 fn delivery_ratio(&self) -> Option<f32> {
169 let total = self.success_count + self.failure_count;
170 if total == 0 {
171 None
172 } else {
173 Some(self.success_count as f32 / total as f32)
174 }
175 }
176
177 fn drop_ratio(&self) -> Option<f32> {
178 self.delivery_ratio()
179 .map(|ratio| (1.0 - ratio).clamp(0.0, 1.0))
180 }
181
182 fn apply_to_metrics(&self, transport_type: TransportType, metrics: &mut TransportMetrics) {
193 if self.success_count + self.failure_count > 0 {
194 let transport_owns_counts = has_send_confirmation_loop(transport_type)
195 && metrics.success_count + metrics.failure_count > 0;
196 if !transport_owns_counts {
197 metrics.success_count = self.success_count;
198 metrics.failure_count = self.failure_count;
199 metrics.delivery_ratio = self.delivery_ratio();
200 metrics.drop_rate = self.drop_ratio();
201 }
202 }
203
204 if let Some(latency) = self.latency_ema {
205 metrics.latency_ms = Some(latency.round().clamp(0.0, u32::MAX as f32) as u32);
206 }
207
208 if let Some(hop) = self.hop_ema {
209 metrics.average_hop_count = Some(hop);
210 }
211 }
212}
213
214fn update_ema(current: Option<f32>, new_value: f32, alpha: f32) -> f32 {
215 let alpha = alpha.clamp(0.0, 1.0);
216 match current {
217 Some(existing) => existing * (1.0 - alpha) + new_value * alpha,
218 None => new_value,
219 }
220}
221
222impl TransportManager {
223 pub fn new(selector: TransportSelector) -> Self {
225 Self {
226 transports: HashMap::new(),
227 current_transport: None,
228 selector,
229 observations: HashMap::new(),
230 dors_event_callback: None,
231 routing_decision_callback: None,
232 routing_diagnostic: false,
233 last_escalation_trigger_emitted: None,
234 peer_binary_wire: HashSet::new(),
235 device_battery: None,
236 device_is_charging: false,
237 }
238 }
239
240 pub fn set_device_battery(&mut self, level: u8, is_charging: bool) {
254 self.device_battery = Some(level.min(100));
255 self.device_is_charging = is_charging;
256 }
257
258 pub fn device_battery(&self) -> (Option<u8>, bool) {
261 (self.device_battery, self.device_is_charging)
262 }
263
264 fn apply_device_battery(&self, metrics: &mut TransportMetrics) {
276 let Some(level) = self.device_battery else {
277 return;
278 };
279 if metrics.battery_level.is_none() {
280 metrics.battery_level = Some(level);
281 metrics.is_charging = self.device_is_charging;
282 }
283 }
284
285 pub fn peer_supports_binary_wire(&self, peer_id: &str) -> bool {
288 self.peer_binary_wire.contains(peer_id)
289 }
290
291 pub fn mark_peer_binary_wire(&mut self, peer_id: &str, supported: bool) {
295 if supported {
296 if !self.peer_binary_wire.contains(peer_id)
298 && self.peer_binary_wire.len() >= MAX_PEER_BINARY_WIRE_ENTRIES
299 {
300 self.peer_binary_wire.clear();
301 }
302 self.peer_binary_wire.insert(peer_id.to_string());
303 } else {
304 self.peer_binary_wire.remove(peer_id);
305 }
306 }
307
308 pub fn mark_peer_nostr_pubkey(&mut self, peer_id: &str, pubkey_hex: Option<&str>) {
323 let Some(transport) = self.transports.get(&TransportType::Nostr) else {
324 return;
325 };
326 if let Some(nostr) = transport
327 .as_any()
328 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
329 {
330 match pubkey_hex {
331 Some(pubkey_hex) => nostr.set_peer_nostr_pubkey(peer_id, pubkey_hex),
332 None => nostr.forget_peer_nostr_pubkey(peer_id),
333 }
334 }
335 }
336
337 pub fn clear_peer_nostr_pubkey(&mut self, peer_id: &str) {
345 if let Some(nostr) = self.transports.get(&TransportType::Nostr).and_then(|t| {
346 t.as_any()
347 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
348 }) {
349 nostr.forget_peer_nostr_pubkey(peer_id);
350 }
351 }
352
353 pub fn nostr_public_key(&self) -> Option<String> {
356 self.transports
357 .get(&TransportType::Nostr)?
358 .as_any()
359 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
360 .map(|nostr| nostr.public_key_hex())
361 }
362
363 pub fn set_nostr_sealing_enabled(&mut self, enabled: bool) {
365 if let Some(nostr) = self.transports.get(&TransportType::Nostr).and_then(|t| {
366 t.as_any()
367 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
368 }) {
369 nostr.set_sealing_enabled(enabled);
370 }
371 }
372
373 pub fn set_nostr_cold_contact_enabled(&mut self, enabled: bool) {
375 if let Some(nostr) = self.nostr_transport() {
376 nostr.set_cold_contact_enabled(enabled);
377 }
378 }
379
380 pub fn publish_nostr_key_package(&mut self, slot_id: &str, message: &Message) -> Result<()> {
393 let Some(nostr) = self.nostr_transport() else {
394 return Ok(());
395 };
396 let payload = nostr.serialize_message(message)?;
397 nostr.publish_key_package(slot_id, payload);
398 Ok(())
399 }
400
401 pub fn take_failed_nostr_publications(&self) -> Vec<String> {
406 self.nostr_transport()
407 .map(|nostr| nostr.take_failed_publications())
408 .unwrap_or_default()
409 }
410
411 pub fn nostr_cold_contact_active(&self) -> bool {
413 self.nostr_transport()
414 .map(|nostr| nostr.cold_contact_enabled())
415 .unwrap_or(false)
416 }
417
418 pub fn set_nostr_username_discovery_enabled(&mut self, enabled: bool) {
420 if let Some(nostr) = self.nostr_transport() {
421 nostr.set_discovery_enabled(enabled);
422 }
423 }
424
425 pub fn nostr_discovery_active(&self) -> bool {
430 self.nostr_transport()
431 .map(|nostr| nostr.discovery_enabled())
432 .unwrap_or(false)
433 }
434
435 pub fn publish_nostr_discovery_record(&mut self, username: Username, payload: Vec<u8>) {
437 if let Some(nostr) = self.nostr_transport() {
438 nostr.publish_discovery_record(username, payload);
439 }
440 }
441
442 pub fn retract_nostr_discovery_record(&mut self, username: Username, payload: Vec<u8>) {
444 if let Some(nostr) = self.nostr_transport() {
445 nostr.retract_discovery_record(username, payload);
446 }
447 }
448
449 pub fn take_failed_nostr_discovery_publications(&self) -> Vec<String> {
451 self.nostr_transport()
452 .map(|nostr| nostr.take_failed_discovery_publications())
453 .unwrap_or_default()
454 }
455
456 pub fn take_confirmed_nostr_discovery_publications(&self) -> Vec<String> {
458 self.nostr_transport()
459 .map(|nostr| nostr.take_confirmed_discovery_publications())
460 .unwrap_or_default()
461 }
462
463 pub fn has_nostr_transport(&self) -> bool {
471 self.nostr_transport().is_some()
472 }
473
474 pub fn resolve_nostr_username(&self, username: Username) -> ResolveRequest {
479 self.nostr_transport()
480 .map(|nostr| nostr.resolve_username(username))
481 .unwrap_or(ResolveRequest::Disabled)
482 }
483
484 pub fn cancel_nostr_username_resolution(&self, username: &Username) -> bool {
490 self.nostr_transport()
491 .map(|nostr| nostr.cancel_username_resolution(username))
492 .unwrap_or(false)
493 }
494
495 fn nostr_transport(&self) -> Option<&offline_protocol_transport::nostr::NostrTransport> {
496 self.transports.get(&TransportType::Nostr).and_then(|t| {
497 t.as_any()
498 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
499 })
500 }
501
502 pub fn set_dors_event_callback(&mut self, callback: Option<Arc<dyn Fn(Event) + Send + Sync>>) {
504 self.dors_event_callback = callback;
505 }
506
507 pub(crate) fn set_routing_decision_callback(
519 &mut self,
520 callback: Option<Arc<dyn Fn(RoutingDecision) + Send + Sync>>,
521 ) {
522 self.routing_decision_callback = callback;
523 }
524
525 pub(crate) fn set_routing_diagnostic(&mut self, enabled: bool) {
535 self.routing_diagnostic = enabled;
536 }
537
538 fn emit_dors_event(&self, event: Event) {
539 if let Some(ref cb) = self.dors_event_callback {
540 cb(event);
541 }
542 }
543
544 fn emit_routing_decision<F: FnOnce() -> RoutingDecision>(&self, build: F) {
548 if let Some(ref cb) = self.routing_decision_callback {
549 cb(build());
550 }
551 }
552
553 fn escalation_trigger_reason_to_code(r: EscalationTriggerReason) -> DorsEscalationReasonCode {
554 match r {
555 EscalationTriggerReason::RetryThreshold => DorsEscalationReasonCode::RetryThreshold,
556 EscalationTriggerReason::PoorSignal => DorsEscalationReasonCode::PoorSignal,
557 EscalationTriggerReason::Congestion => DorsEscalationReasonCode::Congestion,
558 EscalationTriggerReason::LowTtl => DorsEscalationReasonCode::LowTtl,
559 EscalationTriggerReason::LowSuccessRate => DorsEscalationReasonCode::LowSuccessRate,
560 }
561 }
562
563 fn dors_reason_code_to_routing(code: DorsReasonCode) -> RoutingReasonCode {
564 match code {
565 DorsReasonCode::InitialSelection => RoutingReasonCode::InitialSelection,
566 DorsReasonCode::PrimarySelected => RoutingReasonCode::PrimarySelected,
567 DorsReasonCode::PrimarySuccess => RoutingReasonCode::PrimarySuccess,
568 DorsReasonCode::FallbackSuccess => RoutingReasonCode::FallbackSuccess,
569 DorsReasonCode::EscalationApplied => RoutingReasonCode::EscalationApplied,
570 DorsReasonCode::CurrentUnavailable => RoutingReasonCode::CurrentUnavailable,
571 }
572 }
573
574 fn escalation_reason_code_to_routing(code: DorsEscalationReasonCode) -> RoutingReasonCode {
575 match code {
576 DorsEscalationReasonCode::FallbackSuccess => RoutingReasonCode::FallbackSuccess,
577 DorsEscalationReasonCode::RetryThreshold => RoutingReasonCode::RetryThreshold,
578 DorsEscalationReasonCode::PoorSignal => RoutingReasonCode::PoorSignal,
579 DorsEscalationReasonCode::Congestion => RoutingReasonCode::Congestion,
580 DorsEscalationReasonCode::LowTtl => RoutingReasonCode::LowTtl,
581 DorsEscalationReasonCode::LowSuccessRate => RoutingReasonCode::LowSuccessRate,
582 }
583 }
584
585 #[allow(clippy::too_many_arguments)]
594 fn build_routing_decision(
595 &self,
596 now_ms: i64,
597 phase: RoutingPhase,
598 from: Option<TransportType>,
599 to: Option<TransportType>,
600 winning_score: Option<f32>,
601 reason_code: Option<RoutingReasonCode>,
602 detailed_scores: Option<&[(TransportType, TransportScore)]>,
603 ) -> RoutingDecision {
604 let scores = match (self.routing_diagnostic, detailed_scores) {
611 (true, Some(detailed)) => detailed
612 .iter()
613 .map(|(t, s)| {
614 let mut s = s.clone();
615 s.total = display_routing_score(s.total);
616 (*t, s)
617 })
618 .collect(),
619 _ => Vec::new(),
620 };
621 RoutingDecision {
622 timestamp_ms: now_ms,
623 phase,
624 from,
625 to,
626 winning_score: winning_score.map(display_routing_score),
627 reason_code,
628 scores,
629 }
630 }
631
632 fn emit_escalation_trigger_if_deduped(
634 &mut self,
635 now_ms: i64,
636 reason_code: DorsEscalationReasonCode,
637 from: TransportType,
638 to: TransportType,
639 reason_detail: Option<String>,
640 detailed_scores: Option<&[(TransportType, TransportScore)]>,
641 ) {
642 let now = std::time::Instant::now();
643 let emit = match &self.last_escalation_trigger_emitted {
644 Some((last_code, last_at)) => {
645 *last_code != reason_code
646 || last_at.elapsed().as_secs() >= ESCALATION_TRIGGER_DEDUPE_SECS
647 }
648 None => true,
649 };
650 if emit {
651 self.last_escalation_trigger_emitted = Some((reason_code, now));
652 self.emit_dors_event(Event::dors_escalation_triggered(
653 DorsEscalationPhase::Triggered,
654 from.to_string(),
655 to.to_string(),
656 reason_code,
657 reason_detail,
658 ));
659 self.emit_routing_decision(|| {
660 self.build_routing_decision(
661 now_ms,
662 RoutingPhase::Escalated,
663 Some(from),
664 Some(to),
665 None,
666 Some(Self::escalation_reason_code_to_routing(reason_code)),
667 detailed_scores,
668 )
669 });
670 }
671 }
672
673 pub fn add_transport(&mut self, transport_type: TransportType, transport: Box<dyn Transport>) {
680 self.transports.insert(transport_type, Arc::from(transport));
681 }
682
683 pub fn send(&mut self, message: &Message) -> Result<()> {
705 let stamped = self.stamp_negotiated_wire_codec(message);
725 let message = stamped.as_ref().unwrap_or(message);
726
727 let available = self.get_available_transports();
728 if available.is_empty() {
729 return Err(Error::Transport(TransportError::TransportNotAvailable(
730 "No available transport".to_string(),
731 )));
732 }
733
734 let now_ms = Utc::now().timestamp_millis();
738
739 let previous = self.current_transport;
740
741 let primary = self
743 .selector
744 .select_transport(message, &available)
745 .ok_or_else(|| {
746 Error::Transport(TransportError::TransportNotAvailable(
747 "No suitable transport selected from available transports".to_string(),
748 ))
749 })?;
750
751 let detailed_scores: Option<Vec<(TransportType, TransportScore)>> =
756 if self.routing_diagnostic && self.routing_decision_callback.is_some() {
757 Some(self.selector.score_and_rank_detailed(message, &available))
758 } else {
759 None
760 };
761 let detailed_slice = detailed_scores.as_deref();
762 let scored: Vec<(TransportType, f32)> = match detailed_slice {
763 Some(d) => d.iter().map(|(t, s)| (*t, s.total)).collect(),
764 None => self.selector.score_and_rank(message, &available),
765 };
766 let scores: Vec<(String, f32)> = scored
770 .iter()
771 .map(|(t, s)| (t.to_string(), display_routing_score(*s)))
772 .collect();
773 self.emit_dors_event(Event::dors_score_updated(scores));
774
775 self.emit_routing_decision(|| {
776 self.build_routing_decision(
777 now_ms,
778 RoutingPhase::ScoreUpdated,
779 previous,
780 None,
781 None,
782 None,
783 detailed_slice,
784 )
785 });
786 let primary_score = scored
789 .iter()
790 .find(|(t, _)| *t == primary)
791 .map(|(_, s)| display_routing_score(*s))
792 .unwrap_or(0.0);
793 let selection_reason = if previous.is_none() {
794 DorsReasonCode::InitialSelection
795 } else {
796 DorsReasonCode::PrimarySelected
797 };
798 self.emit_dors_event(Event::dors_transport_selected(
799 previous.map(|t| t.to_string()),
800 primary.to_string(),
801 selection_reason,
802 primary_score,
803 ));
804 self.emit_routing_decision(|| {
805 self.build_routing_decision(
806 now_ms,
807 RoutingPhase::Selected,
808 previous,
809 Some(primary),
810 Some(primary_score),
811 Some(Self::dors_reason_code_to_routing(selection_reason)),
812 detailed_slice,
813 )
814 });
815
816 let mut primary_refusal = None;
818 let primary_result = {
819 let transport = self
820 .transports
821 .get(&primary)
822 .ok_or_else(|| Error::Other(format!("Transport {:?} not found", primary)))?;
823 transport.send(message)
824 };
825
826 match primary_result {
827 Ok(()) => {
828 self.current_transport = Some(primary);
829 if previous != Some(primary) {
830 let reason_code = if previous.is_some_and(|p| !available.contains_key(&p)) {
831 DorsReasonCode::CurrentUnavailable
832 } else {
833 DorsReasonCode::PrimarySuccess
834 };
835 self.emit_dors_event(Event::dors_transport_switched(
836 previous.map(|t| t.to_string()),
837 primary.to_string(),
838 reason_code,
839 None,
840 ));
841 self.emit_routing_decision(|| {
842 self.build_routing_decision(
843 now_ms,
844 RoutingPhase::Switched,
845 previous,
846 Some(primary),
847 Some(primary_score),
848 Some(Self::dors_reason_code_to_routing(reason_code)),
849 detailed_slice,
850 )
851 });
852 }
853 return Ok(());
854 }
855 Err(e) => {
856 let is_peer_routing = matches!(e, TransportError::PeerNotReachable(_));
857 if is_peer_routing {
858 debug!(
859 transport = ?primary,
860 error = %e,
861 "Peer not reachable via primary transport, trying fallback"
862 );
863 primary_refusal = Some(e);
870 } else {
871 warn!(
872 transport = ?primary,
873 error = %e,
874 "Primary transport send failed, trying fallback"
875 );
876 self.selector.record_retry_failure(primary);
877 }
878 }
879 }
880
881 let mut last_error = primary_refusal;
885 let mut attempted: Vec<TransportType> = vec![primary];
886
887 for (transport_type, _score) in &scored {
888 if *transport_type == primary {
889 continue;
890 }
891
892 if primary == TransportType::BLE && *transport_type == TransportType::WiFiDirect {
894 if let Some(trigger_reason) = self.selector.escalation_trigger_reason() {
895 let reason_code = Self::escalation_trigger_reason_to_code(trigger_reason);
896 self.emit_escalation_trigger_if_deduped(
897 now_ms,
898 reason_code,
899 primary,
900 *transport_type,
901 None,
902 detailed_slice,
903 );
904 }
905 }
906
907 let transport = match self.transports.get(transport_type) {
908 Some(t) => t,
909 None => continue,
910 };
911
912 attempted.push(*transport_type);
913
914 match transport.send(message) {
915 Ok(()) => {
916 self.current_transport = Some(*transport_type);
917 self.selector.set_current_transport(*transport_type);
918 if previous != Some(*transport_type) {
920 let reason_code = if primary == TransportType::BLE
921 && *transport_type == TransportType::WiFiDirect
922 {
923 DorsReasonCode::EscalationApplied
924 } else {
925 DorsReasonCode::FallbackSuccess
926 };
927 self.emit_dors_event(Event::dors_transport_switched(
928 previous.map(|t| t.to_string()),
929 transport_type.to_string(),
930 reason_code,
931 Some("primary send failed, fallback succeeded".to_string()),
932 ));
933 let fallback = *transport_type;
934 self.emit_routing_decision(|| {
935 self.build_routing_decision(
936 now_ms,
937 RoutingPhase::Switched,
938 previous,
939 Some(fallback),
940 None,
941 Some(Self::dors_reason_code_to_routing(reason_code)),
942 detailed_slice,
943 )
944 });
945 }
946 if primary == TransportType::BLE && *transport_type == TransportType::WiFiDirect
948 {
949 self.emit_dors_event(Event::dors_escalation_triggered(
950 DorsEscalationPhase::Applied,
951 primary.to_string(),
952 transport_type.to_string(),
953 DorsEscalationReasonCode::FallbackSuccess,
954 Some("primary BLE send failed, fallback to WiFi succeeded".to_string()),
955 ));
956 let escalated = *transport_type;
957 self.emit_routing_decision(|| {
958 self.build_routing_decision(
959 now_ms,
960 RoutingPhase::Escalated,
961 Some(primary),
962 Some(escalated),
963 None,
964 Some(Self::escalation_reason_code_to_routing(
965 DorsEscalationReasonCode::FallbackSuccess,
966 )),
967 detailed_slice,
968 )
969 });
970 }
971 return Ok(());
972 }
973 Err(e) => {
974 if !matches!(e, TransportError::PeerNotReachable(_)) {
975 warn!(
976 transport = ?transport_type,
977 error = %e,
978 "Fallback transport send failed, trying next"
979 );
980 self.selector.record_retry_failure(*transport_type);
981 }
982 last_error = Some(e);
983 }
984 }
985 }
986
987 let terminal_error = last_error.unwrap_or_else(|| {
988 TransportError::SendFailed(format!("All transports failed (tried {:?})", attempted))
989 });
990 Err(Error::Transport(terminal_error))
991 }
992
993 pub fn send_via_transport(
1004 &mut self,
1005 message: &Message,
1006 transport_type: TransportType,
1007 ) -> Result<()> {
1008 let transport = self
1009 .transports
1010 .get(&transport_type)
1011 .ok_or_else(|| Error::Other(format!("Transport {:?} not found", transport_type)))?;
1012
1013 if transport.status() != TransportStatus::Available {
1014 return Err(Error::Other(format!(
1015 "Transport {:?} is not available",
1016 transport_type
1017 )));
1018 }
1019
1020 let stamped = self.stamp_negotiated_wire_codec(message);
1031 let message = stamped.as_ref().unwrap_or(message);
1032 transport.send(message).map_err(Error::Transport)?;
1033
1034 self.current_transport = Some(transport_type);
1036
1037 Ok(())
1038 }
1039
1040 pub fn mesh_neighbors(&self) -> Vec<MeshNeighbor> {
1054 let mut neighbors: Vec<MeshNeighbor> = Vec::new();
1055
1056 for transport_type in MESH_TRANSPORTS {
1057 let Some(transport) = self.transports.get(transport_type) else {
1058 continue;
1059 };
1060 for link in transport.connected_peers() {
1061 if neighbors.iter().any(|n| n.peer_id == link.peer_id) {
1062 continue;
1063 }
1064 neighbors.push(MeshNeighbor {
1065 peer_id: link.peer_id,
1066 rssi: link.rssi,
1067 transport: *transport_type,
1068 });
1069 }
1070 }
1071
1072 neighbors
1073 }
1074
1075 fn stamp_negotiated_wire_codec(&self, message: &Message) -> Option<Message> {
1087 if message.wire_codec() == WireCodec::BinaryV1
1088 || !self.peer_binary_wire.contains(message.recipient.as_str())
1089 {
1090 return None;
1091 }
1092 let mut stamped = message.clone();
1093 stamped.set_wire_codec(WireCodec::BinaryV1);
1094 Some(stamped)
1095 }
1096
1097 pub fn mesh_transport_for(&self, peer_id: &str) -> Option<TransportType> {
1104 MESH_TRANSPORTS.iter().copied().find(|transport_type| {
1105 self.transports
1106 .get(transport_type)
1107 .filter(|transport| transport.status() == TransportStatus::Available)
1108 .is_some_and(|transport| {
1109 transport
1110 .connected_peers()
1111 .iter()
1112 .any(|link| link.peer_id == peer_id)
1113 })
1114 })
1115 }
1116
1117 pub fn holds_mesh_link(&self, transport_type: TransportType, peer_id: &str) -> bool {
1125 MESH_TRANSPORTS.contains(&transport_type)
1126 && self
1127 .transports
1128 .get(&transport_type)
1129 .filter(|transport| transport.status() == TransportStatus::Available)
1130 .is_some_and(|transport| {
1131 transport
1132 .connected_peers()
1133 .iter()
1134 .any(|link| link.peer_id == peer_id)
1135 })
1136 }
1137
1138 pub fn available_infrastructure_carriers(&self) -> Vec<TransportType> {
1144 self.transports
1145 .iter()
1146 .filter(|(transport_type, transport)| {
1147 !MESH_TRANSPORTS.contains(transport_type)
1148 && transport.status() == TransportStatus::Available
1149 })
1150 .map(|(transport_type, _)| *transport_type)
1151 .collect()
1152 }
1153
1154 pub fn can_reach_without_carrying(&self, peer_id: &str) -> bool {
1188 self.has_infrastructure_carrier() || self.mesh_can_address(peer_id)
1189 }
1190
1191 pub fn has_infrastructure_carrier(&self) -> bool {
1199 self.transports.iter().any(|(transport_type, transport)| {
1200 !MESH_TRANSPORTS.contains(transport_type)
1201 && transport.status() == TransportStatus::Available
1202 })
1203 }
1204
1205 pub fn queued_message_ids(&self) -> std::collections::HashSet<String> {
1208 self.transports
1209 .values()
1210 .flat_map(|transport| transport.queued_message_ids())
1211 .collect()
1212 }
1213
1214 pub fn mesh_can_address(&self, peer_id: &str) -> bool {
1220 self.mesh_transport_for(peer_id).is_some()
1221 }
1222
1223 pub fn is_mesh_transport(transport_type: TransportType) -> bool {
1230 MESH_TRANSPORTS.contains(&transport_type)
1231 }
1232
1233 pub fn can_address_via(&self, transport_type: TransportType, peer_id: &str) -> bool {
1248 let Some(transport) = self.transports.get(&transport_type) else {
1249 return false;
1250 };
1251 if transport.status() != TransportStatus::Available {
1252 return false;
1253 }
1254 if !MESH_TRANSPORTS.contains(&transport_type) {
1255 return true;
1256 }
1257 transport
1258 .connected_peers()
1259 .iter()
1260 .any(|link| link.peer_id == peer_id)
1261 }
1262
1263 pub fn send_to_neighbor(&self, peer_id: &str, message: &Message) -> Result<TransportType> {
1275 let stamped;
1276 let message = if message.wire_codec() != WireCodec::BinaryV1
1277 && self.peer_binary_wire.contains(peer_id)
1278 {
1279 let mut m = message.clone();
1280 m.set_wire_codec(WireCodec::BinaryV1);
1281 stamped = m;
1282 &stamped
1283 } else if message.wire_codec() == WireCodec::BinaryV1
1284 && !self.peer_binary_wire.contains(peer_id)
1285 {
1286 let mut m = message.clone();
1290 m.set_wire_codec(WireCodec::Json);
1291 stamped = m;
1292 &stamped
1293 } else {
1294 message
1295 };
1296
1297 let mut last_error: Option<String> = None;
1298
1299 for transport_type in MESH_TRANSPORTS {
1300 let Some(transport) = self.transports.get(transport_type) else {
1301 continue;
1302 };
1303 if transport.status() != TransportStatus::Available {
1304 continue;
1305 }
1306 match transport.send_to_peer(peer_id, message) {
1307 Ok(()) => return Ok(*transport_type),
1308 Err(e) => last_error = Some(e.to_string()),
1309 }
1310 }
1311
1312 Err(Error::Transport(TransportError::PeerNotReachable(format!(
1313 "no mesh transport holds a link to {}{}",
1314 peer_id,
1315 last_error.map(|e| format!(" ({})", e)).unwrap_or_default()
1316 ))))
1317 }
1318
1319 pub fn receive(&self) -> Result<Option<(TransportType, Message)>> {
1325 for (transport_type, transport) in &self.transports {
1327 match transport.receive() {
1328 Ok(Some(message)) => {
1329 debug!(
1330 transport = ?transport_type,
1331 sender = %message.sender,
1332 recipient = %message.recipient,
1333 "TransportManager found message"
1334 );
1335 return Ok(Some((*transport_type, message)));
1336 }
1337 Ok(None) => {
1338 }
1340 Err(e) => {
1341 warn!(
1342 transport = ?transport_type,
1343 error = %e,
1344 "Transport receive error"
1345 );
1346 }
1347 }
1348 }
1349 Ok(None)
1350 }
1351
1352 pub fn get_available_transports(&self) -> HashMap<TransportType, TransportMetrics> {
1358 let mut available = HashMap::new();
1359
1360 for (transport_type, transport) in &self.transports {
1361 let maybe_metrics = {
1362 let status = transport.status();
1363 if status == TransportStatus::Available {
1364 Some(transport.metrics())
1365 } else {
1366 debug!(
1367 transport = ?transport_type,
1368 status = ?status,
1369 "Transport excluded from available set"
1370 );
1371 None
1372 }
1373 };
1374
1375 if let Some(mut metrics) = maybe_metrics {
1376 if let Some(stats) = self.observations.get(transport_type) {
1377 stats.apply_to_metrics(*transport_type, &mut metrics);
1378 }
1379 self.apply_device_battery(&mut metrics);
1380 available.insert(*transport_type, metrics);
1381 }
1382 }
1383
1384 available
1385 }
1386
1387 pub fn current_transport(&self) -> Option<TransportType> {
1389 self.current_transport
1390 }
1391
1392 pub fn get_all_transport_statuses(&self) -> HashMap<TransportType, TransportStatus> {
1399 let mut out = HashMap::with_capacity(self.transports.len());
1400 for (transport_type, transport) in &self.transports {
1401 out.insert(*transport_type, transport.status());
1402 }
1403 out
1404 }
1405
1406 pub fn snapshot_status_and_available(
1414 &self,
1415 ) -> (
1416 HashMap<TransportType, TransportStatus>,
1417 HashMap<TransportType, TransportMetrics>,
1418 ) {
1419 let mut statuses = HashMap::with_capacity(self.transports.len());
1420 let mut available = HashMap::new();
1421
1422 for (transport_type, transport) in &self.transports {
1423 let status = transport.status();
1424 let metrics = if status == TransportStatus::Available {
1425 Some(transport.metrics())
1426 } else {
1427 None
1428 };
1429 statuses.insert(*transport_type, status);
1430 if let Some(mut metrics) = metrics {
1431 if let Some(stats) = self.observations.get(transport_type) {
1432 stats.apply_to_metrics(*transport_type, &mut metrics);
1433 }
1434 self.apply_device_battery(&mut metrics);
1435 available.insert(*transport_type, metrics);
1436 }
1437 }
1438
1439 (statuses, available)
1440 }
1441
1442 pub fn start(&mut self) -> Result<()> {
1444 for (transport_type, transport) in &self.transports {
1445 transport.start().map_err(|e| {
1446 Error::Other(format!(
1447 "Failed to start transport {:?}: {}",
1448 transport_type, e
1449 ))
1450 })?;
1451 }
1452 Ok(())
1453 }
1454
1455 pub fn stop(&mut self) -> Result<()> {
1457 for (transport_type, transport) in &self.transports {
1458 transport.stop().map_err(|e| {
1459 Error::Other(format!(
1460 "Failed to stop transport {:?}: {}",
1461 transport_type, e
1462 ))
1463 })?;
1464 }
1465 Ok(())
1466 }
1467
1468 pub fn get_transport(&self, transport_type: TransportType) -> Option<Arc<dyn Transport>> {
1470 self.transports.get(&transport_type).cloned()
1471 }
1472
1473 pub fn remove_transport(&mut self, transport_type: TransportType) {
1479 self.transports.remove(&transport_type);
1480
1481 if self.current_transport == Some(transport_type) {
1483 self.current_transport = None;
1484 }
1485
1486 self.observations.remove(&transport_type);
1487 }
1488
1489 pub fn get_active_transports(&self) -> Vec<TransportType> {
1495 self.transports.keys().copied().collect()
1496 }
1497
1498 pub fn should_escalate_to_wifi(&self) -> bool {
1500 self.selector.should_escalate_to_wifi()
1501 }
1502
1503 pub fn record_retry_failure(&mut self, transport_type: TransportType) {
1505 self.selector.record_retry_failure(transport_type);
1506 }
1507
1508 pub fn reset_retry_count(&mut self, transport_type: TransportType) {
1510 self.selector.reset_retry_count(transport_type);
1511 }
1512
1513 pub fn record_delivery_success(
1515 &mut self,
1516 transport_type: TransportType,
1517 latency_ms: u32,
1518 hop_count: u8,
1519 ) {
1520 let stats = self.observations.entry(transport_type).or_default();
1521 stats.record_success(latency_ms, hop_count);
1522 }
1523
1524 pub fn record_delivery_failure(&mut self, transport_type: TransportType) {
1526 let stats = self.observations.entry(transport_type).or_default();
1527 stats.record_failure();
1528 }
1529
1530 #[cfg(test)]
1531 pub(crate) fn observed_failure_count(&self, transport_type: TransportType) -> u32 {
1532 self.observations
1533 .get(&transport_type)
1534 .map_or(0, |stats| stats.failure_count)
1535 }
1536
1537 pub fn update_selector_config(&mut self, config: DorsConfig) {
1540 self.selector.update_config(config);
1541 }
1542
1543 pub fn selector_config(&self) -> &DorsConfig {
1545 self.selector.config()
1546 }
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551 use super::*;
1552 use offline_protocol_core::{AppId, UserId};
1553 use offline_protocol_router::DorsConfig;
1554 use offline_protocol_transport::{mock::MockTransport, TransportType};
1555 use std::sync::Mutex;
1556
1557 fn create_test_message() -> Message {
1558 Message::new(
1559 UserId::new("alice").unwrap(),
1560 UserId::new("bob").unwrap(),
1561 AppId::new("test").unwrap(),
1562 "Test message",
1563 )
1564 }
1565
1566 #[test]
1567 fn test_transport_manager_creation() {
1568 let selector = TransportSelector::with_config(DorsConfig::default());
1569 let manager = TransportManager::new(selector);
1570 assert_eq!(manager.transports.len(), 0);
1571 assert!(manager.current_transport().is_none());
1572 }
1573
1574 #[test]
1575 fn test_add_transport() {
1576 let selector = TransportSelector::with_config(DorsConfig::default());
1577 let mut manager = TransportManager::new(selector);
1578
1579 let transport = Box::new(MockTransport::new(TransportType::BLE));
1580 manager.add_transport(TransportType::BLE, transport);
1581
1582 assert_eq!(manager.transports.len(), 1);
1583 }
1584
1585 #[test]
1586 fn test_start_stop_through_shared_transport_handle() {
1587 let selector = TransportSelector::with_config(DorsConfig::default());
1588 let mut manager = TransportManager::new(selector);
1589 manager.add_transport(
1590 TransportType::BLE,
1591 Box::new(MockTransport::new(TransportType::BLE)),
1592 );
1593
1594 let handle = manager
1597 .get_transport(TransportType::BLE)
1598 .expect("transport registered");
1599 assert_eq!(handle.status(), TransportStatus::Unavailable);
1600
1601 manager.start().unwrap();
1602 assert_eq!(handle.status(), TransportStatus::Available);
1603
1604 manager.stop().unwrap();
1605 assert_eq!(handle.status(), TransportStatus::Disconnected);
1606 }
1607
1608 #[test]
1609 fn test_send_message() {
1610 let selector = TransportSelector::with_config(DorsConfig::default());
1611 let mut manager = TransportManager::new(selector);
1612
1613 let transport = MockTransport::new(TransportType::BLE);
1614 transport.start().unwrap();
1615 manager.add_transport(TransportType::BLE, Box::new(transport));
1616
1617 let message = create_test_message();
1618 let result = manager.send(&message);
1619 assert!(result.is_ok());
1620 }
1621
1622 #[test]
1623 fn send_stamps_binary_only_for_capable_peer() {
1624 let selector = TransportSelector::with_config(DorsConfig::default());
1625 let mut manager = TransportManager::new(selector);
1626 let transport = MockTransport::new(TransportType::BLE);
1627 transport.start().unwrap();
1628 manager.add_transport(TransportType::BLE, Box::new(transport));
1629
1630 manager.send(&create_test_message()).unwrap();
1632 manager.mark_peer_binary_wire("bob", true);
1634 manager.send(&create_test_message()).unwrap();
1635
1636 let mock = manager
1637 .transports
1638 .get(&TransportType::BLE)
1639 .unwrap()
1640 .as_any()
1641 .downcast_ref::<MockTransport>()
1642 .unwrap();
1643 let sent = mock.sent_messages();
1644 assert_eq!(sent.len(), 2);
1645 assert_eq!(sent[0].wire_codec(), WireCodec::Json);
1646 assert_eq!(sent[1].wire_codec(), WireCodec::BinaryV1);
1647 }
1648
1649 #[test]
1650 fn send_reverts_to_json_after_peer_downgrades() {
1651 let selector = TransportSelector::with_config(DorsConfig::default());
1652 let mut manager = TransportManager::new(selector);
1653 let transport = MockTransport::new(TransportType::BLE);
1654 transport.start().unwrap();
1655 manager.add_transport(TransportType::BLE, Box::new(transport));
1656
1657 manager.mark_peer_binary_wire("bob", true);
1659 assert!(manager.peer_supports_binary_wire("bob"));
1660 manager.send(&create_test_message()).unwrap();
1661
1662 manager.mark_peer_binary_wire("bob", false);
1667 assert!(!manager.peer_supports_binary_wire("bob"));
1668 manager.send(&create_test_message()).unwrap();
1669
1670 let mock = manager
1671 .transports
1672 .get(&TransportType::BLE)
1673 .unwrap()
1674 .as_any()
1675 .downcast_ref::<MockTransport>()
1676 .unwrap();
1677 let sent = mock.sent_messages();
1678 assert_eq!(sent.len(), 2);
1679 assert_eq!(sent[0].wire_codec(), WireCodec::BinaryV1);
1680 assert_eq!(sent[1].wire_codec(), WireCodec::Json);
1681 }
1682
1683 #[test]
1684 fn test_dors_events_emitted_when_callback_set() {
1685 let selector = TransportSelector::with_config(DorsConfig::default());
1686 let mut manager = TransportManager::new(selector);
1687
1688 let transport = MockTransport::new(TransportType::BLE);
1689 transport.start().unwrap();
1690 manager.add_transport(TransportType::BLE, Box::new(transport));
1691
1692 let events: std::sync::Arc<Mutex<Vec<Event>>> = std::sync::Arc::new(Mutex::new(Vec::new()));
1693 let events_clone = events.clone();
1694 manager.set_dors_event_callback(Some(Arc::new(move |e| {
1695 events_clone.lock().unwrap().push(e);
1696 })));
1697
1698 let message = create_test_message();
1699 let _ = manager.send(&message);
1700
1701 let captured = events.lock().unwrap();
1702 assert!(
1703 captured
1704 .iter()
1705 .any(|e| matches!(e, crate::events::Event::DorsScoreUpdated { .. })),
1706 "expected dors_score_updated"
1707 );
1708 assert!(
1709 captured
1710 .iter()
1711 .any(|e| { matches!(e, crate::events::Event::DorsTransportSelected { .. }) }),
1712 "expected dors_transport_selected"
1713 );
1714 }
1715
1716 #[test]
1717 fn test_receive_message() {
1718 let selector = TransportSelector::with_config(DorsConfig::default());
1719 let mut manager = TransportManager::new(selector);
1720
1721 let transport = MockTransport::new(TransportType::BLE);
1722 transport.start().unwrap();
1723 let message = create_test_message();
1724 transport.queue_message(message.clone());
1725
1726 manager.add_transport(TransportType::BLE, Box::new(transport));
1727
1728 let received = manager.receive().unwrap();
1729 assert!(matches!(received, Some((TransportType::BLE, _))));
1730 }
1731
1732 #[test]
1733 fn test_record_delivery_success_enriches_metrics() {
1734 let selector = TransportSelector::with_config(DorsConfig::default());
1735 let mut manager = TransportManager::new(selector);
1736
1737 let transport = MockTransport::new(TransportType::BLE);
1738 transport.start().unwrap();
1739 manager.add_transport(TransportType::BLE, Box::new(transport));
1740
1741 manager.record_delivery_success(TransportType::BLE, 150, 2);
1742
1743 let metrics = manager.get_available_transports();
1744 let ble_metrics = metrics.get(&TransportType::BLE).expect("metrics");
1745
1746 assert_eq!(ble_metrics.success_count, 1);
1747 assert_eq!(ble_metrics.failure_count, 0);
1748 assert!(ble_metrics.delivery_ratio.expect("delivery ratio") > 0.99);
1749 assert_eq!(ble_metrics.average_hop_count, Some(2.0));
1750 assert_eq!(ble_metrics.latency_ms, Some(150));
1751 }
1752
1753 #[test]
1754 fn test_record_delivery_failure_enriches_metrics() {
1755 let selector = TransportSelector::with_config(DorsConfig::default());
1756 let mut manager = TransportManager::new(selector);
1757
1758 let transport = MockTransport::new(TransportType::BLE);
1759 transport.start().unwrap();
1760 manager.add_transport(TransportType::BLE, Box::new(transport));
1761
1762 manager.record_delivery_failure(TransportType::BLE);
1763 let metrics = manager.get_available_transports();
1764 let ble_metrics = metrics.get(&TransportType::BLE).expect("metrics");
1765
1766 assert_eq!(ble_metrics.success_count, 0);
1767 assert_eq!(ble_metrics.failure_count, 1);
1768 assert!(ble_metrics.drop_rate.expect("drop ratio") > 0.99);
1769 }
1770
1771 #[test]
1775 fn engine_observations_win_over_a_mesh_transports_own_counts() {
1776 let selector = TransportSelector::with_config(DorsConfig::default());
1777 let mut manager = TransportManager::new(selector);
1778
1779 let transport = MockTransport::new(TransportType::BLE);
1780 transport.start().unwrap();
1781 transport.set_metrics(TransportMetrics {
1782 failure_count: 1,
1783 delivery_ratio: Some(0.0),
1784 drop_rate: Some(1.0),
1785 ..Default::default()
1786 });
1787 manager.add_transport(TransportType::BLE, Box::new(transport));
1788
1789 manager.record_delivery_success(TransportType::BLE, 150, 1);
1790
1791 let metrics = manager.get_available_transports();
1792 let ble = metrics.get(&TransportType::BLE).expect("metrics");
1793 assert_eq!((ble.success_count, ble.failure_count), (1, 0));
1794 assert_eq!(ble.delivery_ratio, Some(1.0));
1795 assert_eq!(ble.drop_rate, Some(0.0));
1796 }
1797
1798 #[test]
1802 fn a_confirmation_loop_transport_keeps_its_own_counts() {
1803 let selector = TransportSelector::with_config(DorsConfig::default());
1804 let mut manager = TransportManager::new(selector);
1805
1806 let transport = MockTransport::new(TransportType::Internet);
1807 transport.start().unwrap();
1808 transport.set_metrics(TransportMetrics {
1809 success_count: 5,
1810 failure_count: 1,
1811 delivery_ratio: Some(5.0 / 6.0),
1812 drop_rate: Some(1.0 / 6.0),
1813 ..Default::default()
1814 });
1815 manager.add_transport(TransportType::Internet, Box::new(transport));
1816
1817 manager.record_delivery_failure(TransportType::Internet);
1818
1819 let metrics = manager.get_available_transports();
1820 let internet = metrics.get(&TransportType::Internet).expect("metrics");
1821 assert_eq!((internet.success_count, internet.failure_count), (5, 1));
1822 assert_eq!(internet.delivery_ratio, Some(5.0 / 6.0));
1823 }
1824
1825 #[test]
1830 fn a_dropped_inbound_fragment_does_not_escalate_ble() {
1831 use offline_protocol_transport::constants::BLE_MAX_FRAGMENT_ASSEMBLIES;
1832 use offline_protocol_transport::{BleTransport, PeerDevice};
1833
1834 let selector = TransportSelector::with_config(DorsConfig::default());
1835 let mut manager = TransportManager::new(selector);
1836
1837 let ble = BleTransport::new("alice-device");
1838 ble.start().unwrap();
1839 ble.on_peer_discovered(PeerDevice {
1840 device_id: "bob".to_string(),
1841 address: "AA:BB:CC:DD:EE:FF".to_string(),
1842 rssi: -60,
1843 last_seen: std::time::SystemTime::now(),
1844 connected: true,
1845 });
1846 manager.add_transport(TransportType::BLE, Box::new(ble));
1847 let wifi = MockTransport::new(TransportType::WiFiDirect);
1848 wifi.start().unwrap();
1849 manager.add_transport(TransportType::WiFiDirect, Box::new(wifi));
1850
1851 let ble = manager.get_transport(TransportType::BLE).expect("ble");
1854 let ble_ref = ble.as_any().downcast_ref::<BleTransport>().expect("ble");
1855 for _ in 0..=BLE_MAX_FRAGMENT_ASSEMBLIES {
1856 let large = Message::new(
1857 UserId::new("bob").unwrap(),
1858 UserId::new("alice").unwrap(),
1859 AppId::new("test").unwrap(),
1860 "x".repeat(2_000),
1861 );
1862 let fragments = ble_ref.fragment_message(&large).expect("fragments");
1863 assert!(fragments.len() > 1, "the message must span fragments");
1864 ble.on_fragment_received_from(fragments[0].clone(), "bob".to_string())
1865 .expect("fragment accepted");
1866 }
1867 drop(ble);
1868
1869 for _ in 0..8 {
1871 manager.send(&create_test_message()).expect("send");
1872 }
1873
1874 assert!(
1875 !manager.should_escalate_to_wifi(),
1876 "a receive-side reassembly drop must not read as a failing BLE link"
1877 );
1878 let metrics = manager.get_available_transports();
1879 assert_ne!(
1880 metrics[&TransportType::BLE].delivery_ratio,
1881 Some(0.0),
1882 "BLE must not report a 0% delivery ratio with no failed delivery"
1883 );
1884
1885 manager.record_delivery_success(TransportType::BLE, 120, 1);
1886 let metrics = manager.get_available_transports();
1887 assert_eq!(metrics[&TransportType::BLE].delivery_ratio, Some(1.0));
1888 }
1889
1890 #[test]
1894 fn a_sole_carrier_refusing_the_recipient_ends_as_peer_not_reachable() {
1895 let selector = TransportSelector::with_config(DorsConfig::default());
1896 let mut manager = TransportManager::new(selector);
1897
1898 let transport = MockTransport::new(TransportType::BLE);
1899 transport.start().unwrap();
1900 transport.set_reject_unknown_recipients(true);
1901 manager.add_transport(TransportType::BLE, Box::new(transport));
1902
1903 let err = manager.send(&create_test_message()).unwrap_err();
1904 assert!(
1905 matches!(err, Error::Transport(TransportError::PeerNotReachable(_))),
1906 "got {err:?}"
1907 );
1908 }
1909
1910 #[test]
1916 fn test_device_battery_fills_transports_that_report_none() {
1917 let selector = TransportSelector::with_config(DorsConfig::default());
1918 let mut manager = TransportManager::new(selector);
1919
1920 for transport_type in [TransportType::BLE, TransportType::WiFiDirect] {
1921 let transport = MockTransport::new(transport_type);
1922 transport.start().unwrap();
1923 manager.add_transport(transport_type, Box::new(transport));
1924 }
1925
1926 assert_eq!(manager.device_battery(), (None, false));
1927 manager.set_device_battery(42, true);
1928 assert_eq!(manager.device_battery(), (Some(42), true));
1929
1930 let metrics = manager.get_available_transports();
1931 for transport_type in [TransportType::BLE, TransportType::WiFiDirect] {
1932 let m = metrics.get(&transport_type).expect("metrics");
1933 assert_eq!(m.battery_level, Some(42));
1934 assert!(m.is_charging);
1935 }
1936
1937 let (_statuses, available) = manager.snapshot_status_and_available();
1940 let m = available.get(&TransportType::BLE).expect("metrics");
1941 assert_eq!(m.battery_level, Some(42));
1942 assert!(m.is_charging);
1943 }
1944
1945 #[test]
1950 fn test_transport_reported_battery_wins_over_device_feed() {
1951 let selector = TransportSelector::with_config(DorsConfig::default());
1952 let mut manager = TransportManager::new(selector);
1953
1954 let transport = MockTransport::new(TransportType::BLE);
1955 transport.start().unwrap();
1956 transport.set_metrics(TransportMetrics {
1957 battery_level: Some(7),
1958 is_charging: false,
1959 ..Default::default()
1960 });
1961 manager.add_transport(TransportType::BLE, Box::new(transport));
1962
1963 manager.set_device_battery(90, true);
1964
1965 let metrics = manager.get_available_transports();
1966 let m = metrics.get(&TransportType::BLE).expect("metrics");
1967 assert_eq!(m.battery_level, Some(7));
1968 assert!(!m.is_charging);
1969 }
1970
1971 #[test]
1972 fn test_device_battery_is_clamped() {
1973 let selector = TransportSelector::with_config(DorsConfig::default());
1974 let mut manager = TransportManager::new(selector);
1975 manager.set_device_battery(240, false);
1976 assert_eq!(manager.device_battery(), (Some(100), false));
1977 }
1978}