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
119#[derive(Debug, Default, Clone)]
120struct ObservedStats {
121 success_count: u32,
122 failure_count: u32,
123 latency_ema: Option<f32>,
124 hop_ema: Option<f32>,
125 last_success_at: Option<Instant>,
126}
127
128impl ObservedStats {
129 fn record_success(&mut self, latency_ms: u32, hop_count: u8) {
130 self.success_count = self.success_count.saturating_add(1);
131 self.latency_ema = Some(update_ema(
132 self.latency_ema,
133 latency_ms as f32,
134 LATENCY_EMA_ALPHA,
135 ));
136 self.hop_ema = Some(update_ema(
137 self.hop_ema,
138 hop_count as f32,
139 HOP_COUNT_EMA_ALPHA,
140 ));
141 self.last_success_at = Some(Instant::now());
142 self.compact();
143 }
144
145 fn record_failure(&mut self) {
146 self.failure_count = self.failure_count.saturating_add(1);
147 self.compact();
148 }
149
150 fn compact(&mut self) {
151 let total = self.success_count.saturating_add(self.failure_count);
152 if total > OBSERVED_STATS_COMPACT_THRESHOLD {
153 self.success_count /= 2;
154 self.failure_count /= 2;
155 }
156 }
157
158 fn delivery_ratio(&self) -> Option<f32> {
159 let total = self.success_count + self.failure_count;
160 if total == 0 {
161 None
162 } else {
163 Some(self.success_count as f32 / total as f32)
164 }
165 }
166
167 fn drop_ratio(&self) -> Option<f32> {
168 self.delivery_ratio()
169 .map(|ratio| (1.0 - ratio).clamp(0.0, 1.0))
170 }
171
172 fn apply_to_metrics(&self, metrics: &mut TransportMetrics) {
173 if self.success_count + self.failure_count > 0 {
174 if metrics.success_count + metrics.failure_count == 0 {
185 metrics.success_count = self.success_count;
186 metrics.failure_count = self.failure_count;
187 metrics.delivery_ratio = self.delivery_ratio();
188 metrics.drop_rate = self.drop_ratio();
189 }
190 }
191
192 if let Some(latency) = self.latency_ema {
193 metrics.latency_ms = Some(latency.round().clamp(0.0, u32::MAX as f32) as u32);
194 }
195
196 if let Some(hop) = self.hop_ema {
197 metrics.average_hop_count = Some(hop);
198 }
199 }
200}
201
202fn update_ema(current: Option<f32>, new_value: f32, alpha: f32) -> f32 {
203 let alpha = alpha.clamp(0.0, 1.0);
204 match current {
205 Some(existing) => existing * (1.0 - alpha) + new_value * alpha,
206 None => new_value,
207 }
208}
209
210impl TransportManager {
211 pub fn new(selector: TransportSelector) -> Self {
213 Self {
214 transports: HashMap::new(),
215 current_transport: None,
216 selector,
217 observations: HashMap::new(),
218 dors_event_callback: None,
219 routing_decision_callback: None,
220 routing_diagnostic: false,
221 last_escalation_trigger_emitted: None,
222 peer_binary_wire: HashSet::new(),
223 device_battery: None,
224 device_is_charging: false,
225 }
226 }
227
228 pub fn set_device_battery(&mut self, level: u8, is_charging: bool) {
242 self.device_battery = Some(level.min(100));
243 self.device_is_charging = is_charging;
244 }
245
246 pub fn device_battery(&self) -> (Option<u8>, bool) {
249 (self.device_battery, self.device_is_charging)
250 }
251
252 fn apply_device_battery(&self, metrics: &mut TransportMetrics) {
264 let Some(level) = self.device_battery else {
265 return;
266 };
267 if metrics.battery_level.is_none() {
268 metrics.battery_level = Some(level);
269 metrics.is_charging = self.device_is_charging;
270 }
271 }
272
273 pub fn peer_supports_binary_wire(&self, peer_id: &str) -> bool {
276 self.peer_binary_wire.contains(peer_id)
277 }
278
279 pub fn mark_peer_binary_wire(&mut self, peer_id: &str, supported: bool) {
283 if supported {
284 if !self.peer_binary_wire.contains(peer_id)
286 && self.peer_binary_wire.len() >= MAX_PEER_BINARY_WIRE_ENTRIES
287 {
288 self.peer_binary_wire.clear();
289 }
290 self.peer_binary_wire.insert(peer_id.to_string());
291 } else {
292 self.peer_binary_wire.remove(peer_id);
293 }
294 }
295
296 pub fn mark_peer_nostr_pubkey(&mut self, peer_id: &str, pubkey_hex: Option<&str>) {
311 let Some(transport) = self.transports.get(&TransportType::Nostr) else {
312 return;
313 };
314 if let Some(nostr) = transport
315 .as_any()
316 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
317 {
318 match pubkey_hex {
319 Some(pubkey_hex) => nostr.set_peer_nostr_pubkey(peer_id, pubkey_hex),
320 None => nostr.forget_peer_nostr_pubkey(peer_id),
321 }
322 }
323 }
324
325 pub fn clear_peer_nostr_pubkey(&mut self, peer_id: &str) {
333 if let Some(nostr) = self.transports.get(&TransportType::Nostr).and_then(|t| {
334 t.as_any()
335 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
336 }) {
337 nostr.forget_peer_nostr_pubkey(peer_id);
338 }
339 }
340
341 pub fn nostr_public_key(&self) -> Option<String> {
344 self.transports
345 .get(&TransportType::Nostr)?
346 .as_any()
347 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
348 .map(|nostr| nostr.public_key_hex())
349 }
350
351 pub fn set_nostr_sealing_enabled(&mut self, enabled: bool) {
353 if let Some(nostr) = self.transports.get(&TransportType::Nostr).and_then(|t| {
354 t.as_any()
355 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
356 }) {
357 nostr.set_sealing_enabled(enabled);
358 }
359 }
360
361 pub fn set_nostr_cold_contact_enabled(&mut self, enabled: bool) {
363 if let Some(nostr) = self.nostr_transport() {
364 nostr.set_cold_contact_enabled(enabled);
365 }
366 }
367
368 pub fn publish_nostr_key_package(&mut self, slot_id: &str, message: &Message) -> Result<()> {
381 let Some(nostr) = self.nostr_transport() else {
382 return Ok(());
383 };
384 let payload = nostr.serialize_message(message)?;
385 nostr.publish_key_package(slot_id, payload);
386 Ok(())
387 }
388
389 pub fn take_failed_nostr_publications(&self) -> Vec<String> {
394 self.nostr_transport()
395 .map(|nostr| nostr.take_failed_publications())
396 .unwrap_or_default()
397 }
398
399 pub fn nostr_cold_contact_active(&self) -> bool {
401 self.nostr_transport()
402 .map(|nostr| nostr.cold_contact_enabled())
403 .unwrap_or(false)
404 }
405
406 pub fn set_nostr_username_discovery_enabled(&mut self, enabled: bool) {
408 if let Some(nostr) = self.nostr_transport() {
409 nostr.set_discovery_enabled(enabled);
410 }
411 }
412
413 pub fn nostr_discovery_active(&self) -> bool {
418 self.nostr_transport()
419 .map(|nostr| nostr.discovery_enabled())
420 .unwrap_or(false)
421 }
422
423 pub fn publish_nostr_discovery_record(&mut self, username: Username, payload: Vec<u8>) {
425 if let Some(nostr) = self.nostr_transport() {
426 nostr.publish_discovery_record(username, payload);
427 }
428 }
429
430 pub fn retract_nostr_discovery_record(&mut self, username: Username, payload: Vec<u8>) {
432 if let Some(nostr) = self.nostr_transport() {
433 nostr.retract_discovery_record(username, payload);
434 }
435 }
436
437 pub fn take_failed_nostr_discovery_publications(&self) -> Vec<String> {
439 self.nostr_transport()
440 .map(|nostr| nostr.take_failed_discovery_publications())
441 .unwrap_or_default()
442 }
443
444 pub fn take_confirmed_nostr_discovery_publications(&self) -> Vec<String> {
446 self.nostr_transport()
447 .map(|nostr| nostr.take_confirmed_discovery_publications())
448 .unwrap_or_default()
449 }
450
451 pub fn has_nostr_transport(&self) -> bool {
459 self.nostr_transport().is_some()
460 }
461
462 pub fn resolve_nostr_username(&self, username: Username) -> ResolveRequest {
467 self.nostr_transport()
468 .map(|nostr| nostr.resolve_username(username))
469 .unwrap_or(ResolveRequest::Disabled)
470 }
471
472 pub fn cancel_nostr_username_resolution(&self, username: &Username) -> bool {
478 self.nostr_transport()
479 .map(|nostr| nostr.cancel_username_resolution(username))
480 .unwrap_or(false)
481 }
482
483 fn nostr_transport(&self) -> Option<&offline_protocol_transport::nostr::NostrTransport> {
484 self.transports.get(&TransportType::Nostr).and_then(|t| {
485 t.as_any()
486 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
487 })
488 }
489
490 pub fn set_dors_event_callback(&mut self, callback: Option<Arc<dyn Fn(Event) + Send + Sync>>) {
492 self.dors_event_callback = callback;
493 }
494
495 pub(crate) fn set_routing_decision_callback(
506 &mut self,
507 callback: Option<Arc<dyn Fn(RoutingDecision) + Send + Sync>>,
508 ) {
509 self.routing_decision_callback = callback;
510 }
511
512 pub(crate) fn set_routing_diagnostic(&mut self, enabled: bool) {
522 self.routing_diagnostic = enabled;
523 }
524
525 fn emit_dors_event(&self, event: Event) {
526 if let Some(ref cb) = self.dors_event_callback {
527 cb(event);
528 }
529 }
530
531 fn emit_routing_decision<F: FnOnce() -> RoutingDecision>(&self, build: F) {
535 if let Some(ref cb) = self.routing_decision_callback {
536 cb(build());
537 }
538 }
539
540 fn escalation_trigger_reason_to_code(r: EscalationTriggerReason) -> DorsEscalationReasonCode {
541 match r {
542 EscalationTriggerReason::RetryThreshold => DorsEscalationReasonCode::RetryThreshold,
543 EscalationTriggerReason::PoorSignal => DorsEscalationReasonCode::PoorSignal,
544 EscalationTriggerReason::Congestion => DorsEscalationReasonCode::Congestion,
545 EscalationTriggerReason::LowTtl => DorsEscalationReasonCode::LowTtl,
546 EscalationTriggerReason::LowSuccessRate => DorsEscalationReasonCode::LowSuccessRate,
547 }
548 }
549
550 fn dors_reason_code_to_routing(code: DorsReasonCode) -> RoutingReasonCode {
551 match code {
552 DorsReasonCode::InitialSelection => RoutingReasonCode::InitialSelection,
553 DorsReasonCode::PrimarySelected => RoutingReasonCode::PrimarySelected,
554 DorsReasonCode::PrimarySuccess => RoutingReasonCode::PrimarySuccess,
555 DorsReasonCode::FallbackSuccess => RoutingReasonCode::FallbackSuccess,
556 DorsReasonCode::EscalationApplied => RoutingReasonCode::EscalationApplied,
557 DorsReasonCode::CurrentUnavailable => RoutingReasonCode::CurrentUnavailable,
558 }
559 }
560
561 fn escalation_reason_code_to_routing(code: DorsEscalationReasonCode) -> RoutingReasonCode {
562 match code {
563 DorsEscalationReasonCode::FallbackSuccess => RoutingReasonCode::FallbackSuccess,
564 DorsEscalationReasonCode::RetryThreshold => RoutingReasonCode::RetryThreshold,
565 DorsEscalationReasonCode::PoorSignal => RoutingReasonCode::PoorSignal,
566 DorsEscalationReasonCode::Congestion => RoutingReasonCode::Congestion,
567 DorsEscalationReasonCode::LowTtl => RoutingReasonCode::LowTtl,
568 DorsEscalationReasonCode::LowSuccessRate => RoutingReasonCode::LowSuccessRate,
569 }
570 }
571
572 #[allow(clippy::too_many_arguments)]
581 fn build_routing_decision(
582 &self,
583 now_ms: i64,
584 phase: RoutingPhase,
585 from: Option<TransportType>,
586 to: Option<TransportType>,
587 winning_score: Option<f32>,
588 reason_code: Option<RoutingReasonCode>,
589 detailed_scores: Option<&[(TransportType, TransportScore)]>,
590 ) -> RoutingDecision {
591 let scores = match (self.routing_diagnostic, detailed_scores) {
598 (true, Some(detailed)) => detailed
599 .iter()
600 .map(|(t, s)| {
601 let mut s = s.clone();
602 s.total = display_routing_score(s.total);
603 (*t, s)
604 })
605 .collect(),
606 _ => Vec::new(),
607 };
608 RoutingDecision {
609 timestamp_ms: now_ms,
610 phase,
611 from,
612 to,
613 winning_score: winning_score.map(display_routing_score),
614 reason_code,
615 scores,
616 }
617 }
618
619 fn emit_escalation_trigger_if_deduped(
621 &mut self,
622 now_ms: i64,
623 reason_code: DorsEscalationReasonCode,
624 from: TransportType,
625 to: TransportType,
626 reason_detail: Option<String>,
627 detailed_scores: Option<&[(TransportType, TransportScore)]>,
628 ) {
629 let now = std::time::Instant::now();
630 let emit = match &self.last_escalation_trigger_emitted {
631 Some((last_code, last_at)) => {
632 *last_code != reason_code
633 || last_at.elapsed().as_secs() >= ESCALATION_TRIGGER_DEDUPE_SECS
634 }
635 None => true,
636 };
637 if emit {
638 self.last_escalation_trigger_emitted = Some((reason_code, now));
639 self.emit_dors_event(Event::dors_escalation_triggered(
640 DorsEscalationPhase::Triggered,
641 from.to_string(),
642 to.to_string(),
643 reason_code,
644 reason_detail,
645 ));
646 self.emit_routing_decision(|| {
647 self.build_routing_decision(
648 now_ms,
649 RoutingPhase::Escalated,
650 Some(from),
651 Some(to),
652 None,
653 Some(Self::escalation_reason_code_to_routing(reason_code)),
654 detailed_scores,
655 )
656 });
657 }
658 }
659
660 pub fn add_transport(&mut self, transport_type: TransportType, transport: Box<dyn Transport>) {
667 self.transports.insert(transport_type, Arc::from(transport));
668 }
669
670 pub fn send(&mut self, message: &Message) -> Result<()> {
692 let stamped;
712 let message = if message.wire_codec() != WireCodec::BinaryV1
713 && self.peer_binary_wire.contains(message.recipient.as_str())
714 {
715 let mut m = message.clone();
716 m.set_wire_codec(WireCodec::BinaryV1);
717 stamped = m;
718 &stamped
719 } else {
720 message
721 };
722
723 let available = self.get_available_transports();
724 if available.is_empty() {
725 return Err(Error::Transport(TransportError::TransportNotAvailable(
726 "No available transport".to_string(),
727 )));
728 }
729
730 let now_ms = Utc::now().timestamp_millis();
734
735 let previous = self.current_transport;
736
737 let primary = self
739 .selector
740 .select_transport(message, &available)
741 .ok_or_else(|| {
742 Error::Transport(TransportError::TransportNotAvailable(
743 "No suitable transport selected from available transports".to_string(),
744 ))
745 })?;
746
747 let detailed_scores: Option<Vec<(TransportType, TransportScore)>> =
752 if self.routing_diagnostic && self.routing_decision_callback.is_some() {
753 Some(self.selector.score_and_rank_detailed(message, &available))
754 } else {
755 None
756 };
757 let detailed_slice = detailed_scores.as_deref();
758 let scored: Vec<(TransportType, f32)> = match detailed_slice {
759 Some(d) => d.iter().map(|(t, s)| (*t, s.total)).collect(),
760 None => self.selector.score_and_rank(message, &available),
761 };
762 let scores: Vec<(String, f32)> = scored
766 .iter()
767 .map(|(t, s)| (t.to_string(), display_routing_score(*s)))
768 .collect();
769 self.emit_dors_event(Event::dors_score_updated(scores));
770
771 self.emit_routing_decision(|| {
772 self.build_routing_decision(
773 now_ms,
774 RoutingPhase::ScoreUpdated,
775 previous,
776 None,
777 None,
778 None,
779 detailed_slice,
780 )
781 });
782 let primary_score = scored
785 .iter()
786 .find(|(t, _)| *t == primary)
787 .map(|(_, s)| display_routing_score(*s))
788 .unwrap_or(0.0);
789 let selection_reason = if previous.is_none() {
790 DorsReasonCode::InitialSelection
791 } else {
792 DorsReasonCode::PrimarySelected
793 };
794 self.emit_dors_event(Event::dors_transport_selected(
795 previous.map(|t| t.to_string()),
796 primary.to_string(),
797 selection_reason,
798 primary_score,
799 ));
800 self.emit_routing_decision(|| {
801 self.build_routing_decision(
802 now_ms,
803 RoutingPhase::Selected,
804 previous,
805 Some(primary),
806 Some(primary_score),
807 Some(Self::dors_reason_code_to_routing(selection_reason)),
808 detailed_slice,
809 )
810 });
811
812 let primary_result = {
814 let transport = self
815 .transports
816 .get(&primary)
817 .ok_or_else(|| Error::Other(format!("Transport {:?} not found", primary)))?;
818 transport.send(message)
819 };
820
821 match primary_result {
822 Ok(()) => {
823 self.current_transport = Some(primary);
824 if previous != Some(primary) {
825 let reason_code = if previous.is_some_and(|p| !available.contains_key(&p)) {
826 DorsReasonCode::CurrentUnavailable
827 } else {
828 DorsReasonCode::PrimarySuccess
829 };
830 self.emit_dors_event(Event::dors_transport_switched(
831 previous.map(|t| t.to_string()),
832 primary.to_string(),
833 reason_code,
834 None,
835 ));
836 self.emit_routing_decision(|| {
837 self.build_routing_decision(
838 now_ms,
839 RoutingPhase::Switched,
840 previous,
841 Some(primary),
842 Some(primary_score),
843 Some(Self::dors_reason_code_to_routing(reason_code)),
844 detailed_slice,
845 )
846 });
847 }
848 return Ok(());
849 }
850 Err(e) => {
851 let is_peer_routing = matches!(e, TransportError::PeerNotReachable(_));
852 if is_peer_routing {
853 debug!(
854 transport = ?primary,
855 error = %e,
856 "Peer not reachable via primary transport, trying fallback"
857 );
858 } else {
859 warn!(
860 transport = ?primary,
861 error = %e,
862 "Primary transport send failed, trying fallback"
863 );
864 self.selector.record_retry_failure(primary);
865 }
866 }
867 }
868
869 let mut last_error = None;
873 let mut attempted: Vec<TransportType> = vec![primary];
874
875 for (transport_type, _score) in &scored {
876 if *transport_type == primary {
877 continue;
878 }
879
880 if primary == TransportType::BLE && *transport_type == TransportType::WiFiDirect {
882 if let Some(trigger_reason) = self.selector.escalation_trigger_reason() {
883 let reason_code = Self::escalation_trigger_reason_to_code(trigger_reason);
884 self.emit_escalation_trigger_if_deduped(
885 now_ms,
886 reason_code,
887 primary,
888 *transport_type,
889 None,
890 detailed_slice,
891 );
892 }
893 }
894
895 let transport = match self.transports.get(transport_type) {
896 Some(t) => t,
897 None => continue,
898 };
899
900 attempted.push(*transport_type);
901
902 match transport.send(message) {
903 Ok(()) => {
904 self.current_transport = Some(*transport_type);
905 self.selector.set_current_transport(*transport_type);
906 if previous != Some(*transport_type) {
908 let reason_code = if primary == TransportType::BLE
909 && *transport_type == TransportType::WiFiDirect
910 {
911 DorsReasonCode::EscalationApplied
912 } else {
913 DorsReasonCode::FallbackSuccess
914 };
915 self.emit_dors_event(Event::dors_transport_switched(
916 previous.map(|t| t.to_string()),
917 transport_type.to_string(),
918 reason_code,
919 Some("primary send failed, fallback succeeded".to_string()),
920 ));
921 let fallback = *transport_type;
922 self.emit_routing_decision(|| {
923 self.build_routing_decision(
924 now_ms,
925 RoutingPhase::Switched,
926 previous,
927 Some(fallback),
928 None,
929 Some(Self::dors_reason_code_to_routing(reason_code)),
930 detailed_slice,
931 )
932 });
933 }
934 if primary == TransportType::BLE && *transport_type == TransportType::WiFiDirect
936 {
937 self.emit_dors_event(Event::dors_escalation_triggered(
938 DorsEscalationPhase::Applied,
939 primary.to_string(),
940 transport_type.to_string(),
941 DorsEscalationReasonCode::FallbackSuccess,
942 Some("primary BLE send failed, fallback to WiFi succeeded".to_string()),
943 ));
944 let escalated = *transport_type;
945 self.emit_routing_decision(|| {
946 self.build_routing_decision(
947 now_ms,
948 RoutingPhase::Escalated,
949 Some(primary),
950 Some(escalated),
951 None,
952 Some(Self::escalation_reason_code_to_routing(
953 DorsEscalationReasonCode::FallbackSuccess,
954 )),
955 detailed_slice,
956 )
957 });
958 }
959 return Ok(());
960 }
961 Err(e) => {
962 if !matches!(e, TransportError::PeerNotReachable(_)) {
963 warn!(
964 transport = ?transport_type,
965 error = %e,
966 "Fallback transport send failed, trying next"
967 );
968 self.selector.record_retry_failure(*transport_type);
969 }
970 last_error = Some(e);
971 }
972 }
973 }
974
975 let terminal_error = last_error.unwrap_or_else(|| {
976 TransportError::SendFailed(format!("All transports failed (tried {:?})", attempted))
977 });
978 Err(Error::Transport(terminal_error))
979 }
980
981 pub fn send_via_transport(
992 &mut self,
993 message: &Message,
994 transport_type: TransportType,
995 ) -> Result<()> {
996 let transport = self
997 .transports
998 .get(&transport_type)
999 .ok_or_else(|| Error::Other(format!("Transport {:?} not found", transport_type)))?;
1000
1001 if transport.status() != TransportStatus::Available {
1002 return Err(Error::Other(format!(
1003 "Transport {:?} is not available",
1004 transport_type
1005 )));
1006 }
1007
1008 transport.send(message).map_err(Error::Transport)?;
1015
1016 self.current_transport = Some(transport_type);
1018
1019 Ok(())
1020 }
1021
1022 pub fn mesh_neighbors(&self) -> Vec<MeshNeighbor> {
1036 let mut neighbors: Vec<MeshNeighbor> = Vec::new();
1037
1038 for transport_type in MESH_TRANSPORTS {
1039 let Some(transport) = self.transports.get(transport_type) else {
1040 continue;
1041 };
1042 for link in transport.connected_peers() {
1043 if neighbors.iter().any(|n| n.peer_id == link.peer_id) {
1044 continue;
1045 }
1046 neighbors.push(MeshNeighbor {
1047 peer_id: link.peer_id,
1048 rssi: link.rssi,
1049 transport: *transport_type,
1050 });
1051 }
1052 }
1053
1054 neighbors
1055 }
1056
1057 pub fn can_reach_without_carrying(&self, peer_id: &str) -> bool {
1091 self.has_infrastructure_carrier() || self.mesh_can_address(peer_id)
1092 }
1093
1094 pub fn has_infrastructure_carrier(&self) -> bool {
1102 self.transports.iter().any(|(transport_type, transport)| {
1103 !MESH_TRANSPORTS.contains(transport_type)
1104 && transport.status() == TransportStatus::Available
1105 })
1106 }
1107
1108 pub fn mesh_can_address(&self, peer_id: &str) -> bool {
1114 MESH_TRANSPORTS.iter().any(|transport_type| {
1115 self.transports
1116 .get(transport_type)
1117 .filter(|transport| transport.status() == TransportStatus::Available)
1118 .is_some_and(|transport| {
1119 transport
1120 .connected_peers()
1121 .iter()
1122 .any(|link| link.peer_id == peer_id)
1123 })
1124 })
1125 }
1126
1127 pub fn is_mesh_transport(transport_type: TransportType) -> bool {
1134 MESH_TRANSPORTS.contains(&transport_type)
1135 }
1136
1137 pub fn can_address_via(&self, transport_type: TransportType, peer_id: &str) -> bool {
1152 let Some(transport) = self.transports.get(&transport_type) else {
1153 return false;
1154 };
1155 if transport.status() != TransportStatus::Available {
1156 return false;
1157 }
1158 if !MESH_TRANSPORTS.contains(&transport_type) {
1159 return true;
1160 }
1161 transport
1162 .connected_peers()
1163 .iter()
1164 .any(|link| link.peer_id == peer_id)
1165 }
1166
1167 pub fn send_to_neighbor(&self, peer_id: &str, message: &Message) -> Result<TransportType> {
1179 let stamped;
1180 let message = if message.wire_codec() != WireCodec::BinaryV1
1181 && self.peer_binary_wire.contains(peer_id)
1182 {
1183 let mut m = message.clone();
1184 m.set_wire_codec(WireCodec::BinaryV1);
1185 stamped = m;
1186 &stamped
1187 } else if message.wire_codec() == WireCodec::BinaryV1
1188 && !self.peer_binary_wire.contains(peer_id)
1189 {
1190 let mut m = message.clone();
1194 m.set_wire_codec(WireCodec::Json);
1195 stamped = m;
1196 &stamped
1197 } else {
1198 message
1199 };
1200
1201 let mut last_error: Option<String> = None;
1202
1203 for transport_type in MESH_TRANSPORTS {
1204 let Some(transport) = self.transports.get(transport_type) else {
1205 continue;
1206 };
1207 if transport.status() != TransportStatus::Available {
1208 continue;
1209 }
1210 match transport.send_to_peer(peer_id, message) {
1211 Ok(()) => return Ok(*transport_type),
1212 Err(e) => last_error = Some(e.to_string()),
1213 }
1214 }
1215
1216 Err(Error::Transport(TransportError::PeerNotReachable(format!(
1217 "no mesh transport holds a link to {}{}",
1218 peer_id,
1219 last_error.map(|e| format!(" ({})", e)).unwrap_or_default()
1220 ))))
1221 }
1222
1223 pub fn receive(&self) -> Result<Option<(TransportType, Message)>> {
1229 for (transport_type, transport) in &self.transports {
1231 match transport.receive() {
1232 Ok(Some(message)) => {
1233 debug!(
1234 transport = ?transport_type,
1235 sender = %message.sender,
1236 recipient = %message.recipient,
1237 "TransportManager found message"
1238 );
1239 return Ok(Some((*transport_type, message)));
1240 }
1241 Ok(None) => {
1242 }
1244 Err(e) => {
1245 warn!(
1246 transport = ?transport_type,
1247 error = %e,
1248 "Transport receive error"
1249 );
1250 }
1251 }
1252 }
1253 Ok(None)
1254 }
1255
1256 pub fn get_available_transports(&self) -> HashMap<TransportType, TransportMetrics> {
1262 let mut available = HashMap::new();
1263
1264 for (transport_type, transport) in &self.transports {
1265 let maybe_metrics = {
1266 let status = transport.status();
1267 if status == TransportStatus::Available {
1268 Some(transport.metrics())
1269 } else {
1270 debug!(
1271 transport = ?transport_type,
1272 status = ?status,
1273 "Transport excluded from available set"
1274 );
1275 None
1276 }
1277 };
1278
1279 if let Some(mut metrics) = maybe_metrics {
1280 if let Some(stats) = self.observations.get(transport_type) {
1281 stats.apply_to_metrics(&mut metrics);
1282 }
1283 self.apply_device_battery(&mut metrics);
1284 available.insert(*transport_type, metrics);
1285 }
1286 }
1287
1288 available
1289 }
1290
1291 pub fn current_transport(&self) -> Option<TransportType> {
1293 self.current_transport
1294 }
1295
1296 pub fn get_all_transport_statuses(&self) -> HashMap<TransportType, TransportStatus> {
1303 let mut out = HashMap::with_capacity(self.transports.len());
1304 for (transport_type, transport) in &self.transports {
1305 out.insert(*transport_type, transport.status());
1306 }
1307 out
1308 }
1309
1310 pub fn snapshot_status_and_available(
1318 &self,
1319 ) -> (
1320 HashMap<TransportType, TransportStatus>,
1321 HashMap<TransportType, TransportMetrics>,
1322 ) {
1323 let mut statuses = HashMap::with_capacity(self.transports.len());
1324 let mut available = HashMap::new();
1325
1326 for (transport_type, transport) in &self.transports {
1327 let status = transport.status();
1328 let metrics = if status == TransportStatus::Available {
1329 Some(transport.metrics())
1330 } else {
1331 None
1332 };
1333 statuses.insert(*transport_type, status);
1334 if let Some(mut metrics) = metrics {
1335 if let Some(stats) = self.observations.get(transport_type) {
1336 stats.apply_to_metrics(&mut metrics);
1337 }
1338 self.apply_device_battery(&mut metrics);
1339 available.insert(*transport_type, metrics);
1340 }
1341 }
1342
1343 (statuses, available)
1344 }
1345
1346 pub fn start(&mut self) -> Result<()> {
1348 for (transport_type, transport) in &self.transports {
1349 transport.start().map_err(|e| {
1350 Error::Other(format!(
1351 "Failed to start transport {:?}: {}",
1352 transport_type, e
1353 ))
1354 })?;
1355 }
1356 Ok(())
1357 }
1358
1359 pub fn stop(&mut self) -> Result<()> {
1361 for (transport_type, transport) in &self.transports {
1362 transport.stop().map_err(|e| {
1363 Error::Other(format!(
1364 "Failed to stop transport {:?}: {}",
1365 transport_type, e
1366 ))
1367 })?;
1368 }
1369 Ok(())
1370 }
1371
1372 pub fn get_transport(&self, transport_type: TransportType) -> Option<Arc<dyn Transport>> {
1374 self.transports.get(&transport_type).cloned()
1375 }
1376
1377 pub fn remove_transport(&mut self, transport_type: TransportType) {
1383 self.transports.remove(&transport_type);
1384
1385 if self.current_transport == Some(transport_type) {
1387 self.current_transport = None;
1388 }
1389
1390 self.observations.remove(&transport_type);
1391 }
1392
1393 pub fn get_active_transports(&self) -> Vec<TransportType> {
1399 self.transports.keys().copied().collect()
1400 }
1401
1402 pub fn should_escalate_to_wifi(&self) -> bool {
1404 self.selector.should_escalate_to_wifi()
1405 }
1406
1407 pub fn record_retry_failure(&mut self, transport_type: TransportType) {
1409 self.selector.record_retry_failure(transport_type);
1410 }
1411
1412 pub fn reset_retry_count(&mut self, transport_type: TransportType) {
1414 self.selector.reset_retry_count(transport_type);
1415 }
1416
1417 pub fn record_delivery_success(
1419 &mut self,
1420 transport_type: TransportType,
1421 latency_ms: u32,
1422 hop_count: u8,
1423 ) {
1424 let stats = self.observations.entry(transport_type).or_default();
1425 stats.record_success(latency_ms, hop_count);
1426 }
1427
1428 pub fn record_delivery_failure(&mut self, transport_type: TransportType) {
1430 let stats = self.observations.entry(transport_type).or_default();
1431 stats.record_failure();
1432 }
1433
1434 pub fn update_selector_config(&mut self, config: DorsConfig) {
1437 self.selector.update_config(config);
1438 }
1439
1440 pub fn selector_config(&self) -> &DorsConfig {
1442 self.selector.config()
1443 }
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448 use super::*;
1449 use offline_protocol_core::{AppId, UserId};
1450 use offline_protocol_router::DorsConfig;
1451 use offline_protocol_transport::{mock::MockTransport, TransportType};
1452 use std::sync::Mutex;
1453
1454 fn create_test_message() -> Message {
1455 Message::new(
1456 UserId::new("alice").unwrap(),
1457 UserId::new("bob").unwrap(),
1458 AppId::new("test").unwrap(),
1459 "Test message",
1460 )
1461 }
1462
1463 #[test]
1464 fn test_transport_manager_creation() {
1465 let selector = TransportSelector::with_config(DorsConfig::default());
1466 let manager = TransportManager::new(selector);
1467 assert_eq!(manager.transports.len(), 0);
1468 assert!(manager.current_transport().is_none());
1469 }
1470
1471 #[test]
1472 fn test_add_transport() {
1473 let selector = TransportSelector::with_config(DorsConfig::default());
1474 let mut manager = TransportManager::new(selector);
1475
1476 let transport = Box::new(MockTransport::new(TransportType::BLE));
1477 manager.add_transport(TransportType::BLE, transport);
1478
1479 assert_eq!(manager.transports.len(), 1);
1480 }
1481
1482 #[test]
1483 fn test_start_stop_through_shared_transport_handle() {
1484 let selector = TransportSelector::with_config(DorsConfig::default());
1485 let mut manager = TransportManager::new(selector);
1486 manager.add_transport(
1487 TransportType::BLE,
1488 Box::new(MockTransport::new(TransportType::BLE)),
1489 );
1490
1491 let handle = manager
1494 .get_transport(TransportType::BLE)
1495 .expect("transport registered");
1496 assert_eq!(handle.status(), TransportStatus::Unavailable);
1497
1498 manager.start().unwrap();
1499 assert_eq!(handle.status(), TransportStatus::Available);
1500
1501 manager.stop().unwrap();
1502 assert_eq!(handle.status(), TransportStatus::Disconnected);
1503 }
1504
1505 #[test]
1506 fn test_send_message() {
1507 let selector = TransportSelector::with_config(DorsConfig::default());
1508 let mut manager = TransportManager::new(selector);
1509
1510 let transport = MockTransport::new(TransportType::BLE);
1511 transport.start().unwrap();
1512 manager.add_transport(TransportType::BLE, Box::new(transport));
1513
1514 let message = create_test_message();
1515 let result = manager.send(&message);
1516 assert!(result.is_ok());
1517 }
1518
1519 #[test]
1520 fn send_stamps_binary_only_for_capable_peer() {
1521 let selector = TransportSelector::with_config(DorsConfig::default());
1522 let mut manager = TransportManager::new(selector);
1523 let transport = MockTransport::new(TransportType::BLE);
1524 transport.start().unwrap();
1525 manager.add_transport(TransportType::BLE, Box::new(transport));
1526
1527 manager.send(&create_test_message()).unwrap();
1529 manager.mark_peer_binary_wire("bob", true);
1531 manager.send(&create_test_message()).unwrap();
1532
1533 let mock = manager
1534 .transports
1535 .get(&TransportType::BLE)
1536 .unwrap()
1537 .as_any()
1538 .downcast_ref::<MockTransport>()
1539 .unwrap();
1540 let sent = mock.sent_messages();
1541 assert_eq!(sent.len(), 2);
1542 assert_eq!(sent[0].wire_codec(), WireCodec::Json);
1543 assert_eq!(sent[1].wire_codec(), WireCodec::BinaryV1);
1544 }
1545
1546 #[test]
1547 fn send_reverts_to_json_after_peer_downgrades() {
1548 let selector = TransportSelector::with_config(DorsConfig::default());
1549 let mut manager = TransportManager::new(selector);
1550 let transport = MockTransport::new(TransportType::BLE);
1551 transport.start().unwrap();
1552 manager.add_transport(TransportType::BLE, Box::new(transport));
1553
1554 manager.mark_peer_binary_wire("bob", true);
1556 assert!(manager.peer_supports_binary_wire("bob"));
1557 manager.send(&create_test_message()).unwrap();
1558
1559 manager.mark_peer_binary_wire("bob", false);
1564 assert!(!manager.peer_supports_binary_wire("bob"));
1565 manager.send(&create_test_message()).unwrap();
1566
1567 let mock = manager
1568 .transports
1569 .get(&TransportType::BLE)
1570 .unwrap()
1571 .as_any()
1572 .downcast_ref::<MockTransport>()
1573 .unwrap();
1574 let sent = mock.sent_messages();
1575 assert_eq!(sent.len(), 2);
1576 assert_eq!(sent[0].wire_codec(), WireCodec::BinaryV1);
1577 assert_eq!(sent[1].wire_codec(), WireCodec::Json);
1578 }
1579
1580 #[test]
1581 fn test_dors_events_emitted_when_callback_set() {
1582 let selector = TransportSelector::with_config(DorsConfig::default());
1583 let mut manager = TransportManager::new(selector);
1584
1585 let transport = MockTransport::new(TransportType::BLE);
1586 transport.start().unwrap();
1587 manager.add_transport(TransportType::BLE, Box::new(transport));
1588
1589 let events: std::sync::Arc<Mutex<Vec<Event>>> = std::sync::Arc::new(Mutex::new(Vec::new()));
1590 let events_clone = events.clone();
1591 manager.set_dors_event_callback(Some(Arc::new(move |e| {
1592 events_clone.lock().unwrap().push(e);
1593 })));
1594
1595 let message = create_test_message();
1596 let _ = manager.send(&message);
1597
1598 let captured = events.lock().unwrap();
1599 assert!(
1600 captured
1601 .iter()
1602 .any(|e| matches!(e, crate::events::Event::DorsScoreUpdated { .. })),
1603 "expected dors_score_updated"
1604 );
1605 assert!(
1606 captured
1607 .iter()
1608 .any(|e| { matches!(e, crate::events::Event::DorsTransportSelected { .. }) }),
1609 "expected dors_transport_selected"
1610 );
1611 }
1612
1613 #[test]
1614 fn test_receive_message() {
1615 let selector = TransportSelector::with_config(DorsConfig::default());
1616 let mut manager = TransportManager::new(selector);
1617
1618 let transport = MockTransport::new(TransportType::BLE);
1619 transport.start().unwrap();
1620 let message = create_test_message();
1621 transport.queue_message(message.clone());
1622
1623 manager.add_transport(TransportType::BLE, Box::new(transport));
1624
1625 let received = manager.receive().unwrap();
1626 assert!(matches!(received, Some((TransportType::BLE, _))));
1627 }
1628
1629 #[test]
1630 fn test_record_delivery_success_enriches_metrics() {
1631 let selector = TransportSelector::with_config(DorsConfig::default());
1632 let mut manager = TransportManager::new(selector);
1633
1634 let transport = MockTransport::new(TransportType::BLE);
1635 transport.start().unwrap();
1636 manager.add_transport(TransportType::BLE, Box::new(transport));
1637
1638 manager.record_delivery_success(TransportType::BLE, 150, 2);
1639
1640 let metrics = manager.get_available_transports();
1641 let ble_metrics = metrics.get(&TransportType::BLE).expect("metrics");
1642
1643 assert_eq!(ble_metrics.success_count, 1);
1644 assert_eq!(ble_metrics.failure_count, 0);
1645 assert!(ble_metrics.delivery_ratio.expect("delivery ratio") > 0.99);
1646 assert_eq!(ble_metrics.average_hop_count, Some(2.0));
1647 assert_eq!(ble_metrics.latency_ms, Some(150));
1648 }
1649
1650 #[test]
1651 fn test_record_delivery_failure_enriches_metrics() {
1652 let selector = TransportSelector::with_config(DorsConfig::default());
1653 let mut manager = TransportManager::new(selector);
1654
1655 let transport = MockTransport::new(TransportType::BLE);
1656 transport.start().unwrap();
1657 manager.add_transport(TransportType::BLE, Box::new(transport));
1658
1659 manager.record_delivery_failure(TransportType::BLE);
1660 let metrics = manager.get_available_transports();
1661 let ble_metrics = metrics.get(&TransportType::BLE).expect("metrics");
1662
1663 assert_eq!(ble_metrics.success_count, 0);
1664 assert_eq!(ble_metrics.failure_count, 1);
1665 assert!(ble_metrics.drop_rate.expect("drop ratio") > 0.99);
1666 }
1667
1668 #[test]
1674 fn test_device_battery_fills_transports_that_report_none() {
1675 let selector = TransportSelector::with_config(DorsConfig::default());
1676 let mut manager = TransportManager::new(selector);
1677
1678 for transport_type in [TransportType::BLE, TransportType::WiFiDirect] {
1679 let transport = MockTransport::new(transport_type);
1680 transport.start().unwrap();
1681 manager.add_transport(transport_type, Box::new(transport));
1682 }
1683
1684 assert_eq!(manager.device_battery(), (None, false));
1685 manager.set_device_battery(42, true);
1686 assert_eq!(manager.device_battery(), (Some(42), true));
1687
1688 let metrics = manager.get_available_transports();
1689 for transport_type in [TransportType::BLE, TransportType::WiFiDirect] {
1690 let m = metrics.get(&transport_type).expect("metrics");
1691 assert_eq!(m.battery_level, Some(42));
1692 assert!(m.is_charging);
1693 }
1694
1695 let (_statuses, available) = manager.snapshot_status_and_available();
1698 let m = available.get(&TransportType::BLE).expect("metrics");
1699 assert_eq!(m.battery_level, Some(42));
1700 assert!(m.is_charging);
1701 }
1702
1703 #[test]
1708 fn test_transport_reported_battery_wins_over_device_feed() {
1709 let selector = TransportSelector::with_config(DorsConfig::default());
1710 let mut manager = TransportManager::new(selector);
1711
1712 let transport = MockTransport::new(TransportType::BLE);
1713 transport.start().unwrap();
1714 transport.set_metrics(TransportMetrics {
1715 battery_level: Some(7),
1716 is_charging: false,
1717 ..Default::default()
1718 });
1719 manager.add_transport(TransportType::BLE, Box::new(transport));
1720
1721 manager.set_device_battery(90, true);
1722
1723 let metrics = manager.get_available_transports();
1724 let m = metrics.get(&TransportType::BLE).expect("metrics");
1725 assert_eq!(m.battery_level, Some(7));
1726 assert!(!m.is_charging);
1727 }
1728
1729 #[test]
1730 fn test_device_battery_is_clamped() {
1731 let selector = TransportSelector::with_config(DorsConfig::default());
1732 let mut manager = TransportManager::new(selector);
1733 manager.set_device_battery(240, false);
1734 assert_eq!(manager.device_battery(), (Some(100), false));
1735 }
1736}