1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use flume::Sender;
6
7use crate::{CoreMsg, FfiApp, FipsBleCommand, FipsBleEvent};
8
9#[derive(uniffi::Object)]
10pub struct FfiFipsBle {
11 runtime: tokio::runtime::Runtime,
12 adapter: Arc<fips_core::transport::ble::host::HostBleAdapter>,
13 core_tx: Sender<CoreMsg>,
14 closed: AtomicBool,
15}
16
17#[derive(Debug, Clone, uniffi::Error)]
18pub enum FipsBleBridgeError {
19 Initialization(String),
20}
21
22impl std::fmt::Display for FipsBleBridgeError {
23 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 Self::Initialization(message) => formatter.write_str(message),
26 }
27 }
28}
29
30impl std::error::Error for FipsBleBridgeError {}
31
32#[uniffi::export]
33impl FfiFipsBle {
34 #[uniffi::constructor]
35 pub fn new(app: Arc<FfiApp>) -> Result<Arc<Self>, FipsBleBridgeError> {
36 let runtime = tokio::runtime::Builder::new_multi_thread()
37 .worker_threads(1)
38 .enable_time()
39 .build()
40 .map_err(|error| {
41 FipsBleBridgeError::Initialization(format!(
42 "Could not create the FIPS BLE runtime: {error}"
43 ))
44 })?;
45 let local_token = format!(
46 "iris-{}",
47 SystemTime::now()
48 .duration_since(UNIX_EPOCH)
49 .unwrap_or_default()
50 .as_nanos()
51 );
52 let (io, adapter) = runtime
53 .block_on(async {
54 fips_core::transport::ble::host::HostBleIo::channel("mobile", local_token, 256)
55 })
56 .map_err(|error| {
57 FipsBleBridgeError::Initialization(format!(
58 "Could not create the FIPS BLE bridge: {error}"
59 ))
60 })?;
61 let (reply_tx, reply_rx) = flume::bounded(1);
62 app.background_tx
63 .send(CoreMsg::AttachHostBle {
64 attachment: crate::core::HostBleAttachment::new(io),
65 reply_tx,
66 })
67 .map_err(|error| {
68 FipsBleBridgeError::Initialization(format!(
69 "Could not request the FIPS BLE bridge attachment: {error}"
70 ))
71 })?;
72 let attach_result = reply_rx
73 .recv_timeout(Duration::from_secs(2))
74 .map_err(|error| {
75 FipsBleBridgeError::Initialization(format!(
76 "The Iris core did not attach the FIPS BLE bridge: {error}"
77 ))
78 })?;
79 attach_result.map_err(FipsBleBridgeError::Initialization)?;
80 Ok(Arc::new(Self {
81 runtime,
82 adapter: Arc::new(adapter),
83 core_tx: app.background_tx.clone(),
84 closed: AtomicBool::new(false),
85 }))
86 }
87
88 pub fn next_command(&self, timeout_ms: u64) -> Option<FipsBleCommand> {
89 let timeout = Duration::from_millis(timeout_ms.clamp(1, 60_000));
90 self.runtime.block_on(async {
91 tokio::time::timeout(timeout, self.adapter.next_command())
92 .await
93 .ok()
94 .flatten()
95 .map(Into::into)
96 })
97 }
98
99 pub fn emit(&self, event: FipsBleEvent) -> bool {
100 self.runtime
101 .block_on(self.adapter.emit(event.into()))
102 .is_ok()
103 }
104
105 pub fn detach(&self) {
106 if self.closed.swap(true, Ordering::SeqCst) {
107 return;
108 }
109 let (reply_tx, reply_rx) = flume::bounded(1);
110 if self.core_tx.send(CoreMsg::DetachHostBle(reply_tx)).is_ok() {
111 let _ = reply_rx.recv_timeout(Duration::from_secs(2));
112 }
113 }
114}
115
116impl From<fips_core::transport::ble::host::HostBleCommand> for FipsBleCommand {
117 fn from(command: fips_core::transport::ble::host::HostBleCommand) -> Self {
118 use fips_core::transport::ble::host::HostBleCommand as Host;
119 match command {
120 Host::Listen {
121 request_id,
122 preferred_psm,
123 } => Self::Listen {
124 request_id,
125 preferred_psm,
126 },
127 Host::StopListening => Self::StopListening,
128 Host::StartAdvertising {
129 request_id,
130 bootstrap,
131 } => Self::StartAdvertising {
132 request_id,
133 bootstrap,
134 },
135 Host::StopAdvertising { request_id } => Self::StopAdvertising { request_id },
136 Host::StartScanning { request_id } => Self::StartScanning { request_id },
137 Host::StopScanning => Self::StopScanning,
138 Host::Connect {
139 request_id,
140 peer_token,
141 psm,
142 } => Self::Connect {
143 request_id,
144 peer_token,
145 psm,
146 },
147 Host::Write {
148 request_id,
149 connection_id,
150 bytes,
151 } => Self::Write {
152 request_id,
153 connection_id,
154 bytes,
155 },
156 Host::Close { connection_id } => Self::Close { connection_id },
157 }
158 }
159}
160
161impl From<FipsBleEvent> for fips_core::transport::ble::host::HostBleEvent {
162 fn from(event: FipsBleEvent) -> Self {
163 use fips_core::transport::ble::host::HostBleEvent as Host;
164 match event {
165 FipsBleEvent::Listening { request_id, psm } => Host::Listening { request_id, psm },
166 FipsBleEvent::AdvertisingStarted { request_id } => {
167 Host::AdvertisingStarted { request_id }
168 }
169 FipsBleEvent::AdvertisingStopped { request_id } => {
170 Host::AdvertisingStopped { request_id }
171 }
172 FipsBleEvent::ScanningStarted { request_id } => Host::ScanningStarted { request_id },
173 FipsBleEvent::PeerDiscovered {
174 peer_token,
175 bootstrap,
176 } => Host::PeerDiscovered {
177 peer_token,
178 bootstrap,
179 },
180 FipsBleEvent::Connected {
181 request_id,
182 connection_id,
183 peer_token,
184 send_segment_mtu,
185 receive_segment_mtu,
186 } => Host::Connected {
187 request_id,
188 connection_id,
189 peer_token,
190 send_segment_mtu,
191 receive_segment_mtu,
192 },
193 FipsBleEvent::IncomingConnection {
194 connection_id,
195 peer_token,
196 send_segment_mtu,
197 receive_segment_mtu,
198 } => Host::IncomingConnection {
199 connection_id,
200 peer_token,
201 send_segment_mtu,
202 receive_segment_mtu,
203 },
204 FipsBleEvent::BytesReceived {
205 connection_id,
206 bytes,
207 } => Host::BytesReceived {
208 connection_id,
209 bytes,
210 },
211 FipsBleEvent::WriteCompleted { request_id } => Host::WriteCompleted { request_id },
212 FipsBleEvent::Disconnected {
213 connection_id,
214 reason,
215 } => Host::Disconnected {
216 connection_id,
217 reason,
218 },
219 FipsBleEvent::Failed {
220 request_id,
221 message,
222 } => Host::Failed {
223 request_id,
224 message,
225 },
226 }
227 }
228}