1use std::fmt;
18use std::path::PathBuf;
19use std::sync::{Arc, Mutex};
20
21use tokio::sync::{mpsc, oneshot};
22use umsh_lan_bearer::socket::LanConfig;
23use umsh_ulcp_device::{QueuedClass, QueuedNotice};
24use umsh_ulcp_simdev::host::HostPlatform;
25use umsh_ulcp_simdev::lan::{self, HostEvent, StateLock};
26use umsh_ulcp_simdev::soft::SoftDevice;
27
28const DEV_VERSION: &str = concat!("umsh-mobile-lan/", env!("CARGO_PKG_VERSION"));
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Error)]
32pub enum MobileVirtualRadioError {
33 StateUnavailable,
35 StateInUse,
38 WorkerUnavailable,
40 SocketUnavailable,
42}
43
44impl fmt::Display for MobileVirtualRadioError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.write_str(match self {
47 Self::StateUnavailable => "the virtual radio's state directory is unavailable",
48 Self::StateInUse => "another virtual radio is using this state directory",
49 Self::WorkerUnavailable => "the virtual radio's worker could not start",
50 Self::SocketUnavailable => "the virtual radio's local socket could not be made",
51 })
52 }
53}
54
55impl std::error::Error for MobileVirtualRadioError {}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
59pub enum MobileQueuedClass {
60 Direct,
62 Group,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Record)]
68pub struct MobileQueuedNoticeRecord {
69 pub class: MobileQueuedClass,
70 pub muted: bool,
73 pub depth: u32,
75}
76
77impl From<QueuedNotice> for MobileQueuedNoticeRecord {
78 fn from(notice: QueuedNotice) -> Self {
79 Self {
80 class: match notice.class {
81 QueuedClass::Direct => MobileQueuedClass::Direct,
82 QueuedClass::Group => MobileQueuedClass::Group,
83 },
84 muted: notice.muted,
85 depth: u32::try_from(notice.depth).unwrap_or(u32::MAX),
86 }
87 }
88}
89
90#[uniffi::export(with_foreign)]
95pub trait MobileVirtualRadioListener: Send + Sync {
96 fn on_queued(&self, notice: MobileQueuedNoticeRecord);
98}
99
100#[uniffi::export(with_foreign)]
106pub trait MobileWireSink: Send + Sync {
107 fn on_wire(&self, bytes: Vec<u8>);
109}
110
111#[derive(uniffi::Object)]
114pub struct MobileVirtualRadio {
115 events: mpsc::UnboundedSender<HostEvent>,
116 stop: Mutex<Option<oneshot::Sender<()>>>,
118}
119
120#[uniffi::export]
121impl MobileVirtualRadio {
122 #[uniffi::constructor]
130 pub fn start(
131 state_dir: String,
132 enable_phy: bool,
133 listener: Arc<dyn MobileVirtualRadioListener>,
134 ) -> Result<Arc<Self>, MobileVirtualRadioError> {
135 Self::launch(PathBuf::from(state_dir), None, enable_phy, listener)
136 }
137
138 #[uniffi::constructor]
144 pub fn serve(
145 state_dir: String,
146 socket_path: String,
147 enable_phy: bool,
148 listener: Arc<dyn MobileVirtualRadioListener>,
149 ) -> Result<Arc<Self>, MobileVirtualRadioError> {
150 Self::launch(
151 PathBuf::from(state_dir),
152 Some(PathBuf::from(socket_path)),
153 enable_phy,
154 listener,
155 )
156 }
157
158 pub fn attach(&self, sink: Arc<dyn MobileWireSink>) {
161 let _ = self.events.send(HostEvent::Attach(Box::new(move |bytes| {
162 sink.on_wire(bytes)
163 })));
164 }
165
166 pub fn receive(&self, bytes: Vec<u8>) {
168 let _ = self.events.send(HostEvent::Bytes(bytes));
169 }
170
171 pub fn detach(&self) {
173 let _ = self.events.send(HostEvent::Detach);
174 }
175
176 pub fn stop(&self) {
178 if let Ok(mut stop) = self.stop.lock() {
179 stop.take();
180 }
181 }
182}
183
184impl MobileVirtualRadio {
185 fn launch(
186 dir: PathBuf,
187 socket: Option<PathBuf>,
188 enable_phy: bool,
189 listener: Arc<dyn MobileVirtualRadioListener>,
190 ) -> Result<Arc<Self>, MobileVirtualRadioError> {
191 let platform =
192 HostPlatform::new(&dir).map_err(|_| MobileVirtualRadioError::StateUnavailable)?;
193 let lock = StateLock::try_acquire(&dir)
194 .map_err(|_| MobileVirtualRadioError::StateUnavailable)?
195 .ok_or(MobileVirtualRadioError::StateInUse)?;
196
197 let (events, host_events) = mpsc::unbounded_channel();
198 let socket_events = events.clone();
199 let (stop, stopped) = oneshot::channel::<()>();
200 let (ready_tx, ready_rx) = std::sync::mpsc::channel();
201 std::thread::Builder::new()
202 .name("umsh-virtual-radio".to_owned())
203 .spawn(move || {
204 let runtime = match tokio::runtime::Builder::new_current_thread()
205 .enable_all()
206 .build()
207 {
208 Ok(runtime) => runtime,
209 Err(_) => {
210 let _ = ready_tx.send(Err(MobileVirtualRadioError::WorkerUnavailable));
211 return;
212 }
213 };
214 let local = match &socket {
217 Some(path) => match runtime.block_on(async { lan::bind_local_socket(path) }) {
218 Ok(local) => Some(local),
219 Err(_) => {
220 let _ = ready_tx.send(Err(MobileVirtualRadioError::SocketUnavailable));
221 return;
222 }
223 },
224 None => None,
225 };
226 let mut device = SoftDevice::new(lan::session_config(DEV_VERSION), platform);
227 if enable_phy && !device.session().settings().enabled {
228 let _ = device.session_mut().toggle_radio(&mut |_| {});
229 }
230 let _ = ready_tx.send(Ok(()));
231 runtime.block_on(async move {
232 let running = Box::pin(lan::run(
235 device,
236 LanConfig::lan(),
237 host_events,
238 move |notice| listener.on_queued(notice.into()),
239 ));
240 let serving = async move {
241 if let Some(local) = local {
242 lan::serve(local, socket_events).await;
243 }
244 std::future::pending::<()>().await
245 };
246 tokio::select! {
247 () = running => {}
248 () = serving => {}
249 _ = stopped => {}
250 }
251 });
252 if let Some(path) = socket {
253 let _ = std::fs::remove_file(path);
254 }
255 drop(lock);
256 })
257 .map_err(|_| MobileVirtualRadioError::WorkerUnavailable)?;
258 ready_rx
259 .recv()
260 .map_err(|_| MobileVirtualRadioError::WorkerUnavailable)??;
261 Ok(Arc::new(Self {
262 events,
263 stop: Mutex::new(Some(stop)),
264 }))
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use std::sync::mpsc as std_mpsc;
271 use std::time::Duration;
272
273 use umsh_ulcp::{frame, hdlc, ids::prop};
274
275 use super::*;
276
277 struct Collect(Mutex<std_mpsc::Sender<Vec<u8>>>);
278
279 impl MobileVirtualRadioListener for Collect {
280 fn on_queued(&self, _: MobileQueuedNoticeRecord) {}
281 }
282
283 impl MobileWireSink for Collect {
284 fn on_wire(&self, bytes: Vec<u8>) {
285 let _ = self.0.lock().unwrap().send(bytes);
286 }
287 }
288
289 fn scratch(tag: &str) -> PathBuf {
290 let dir = std::env::temp_dir().join(format!(
291 "umsh-mobile-virtual-radio-{tag}-{}",
292 std::process::id()
293 ));
294 let _ = std::fs::remove_dir_all(&dir);
295 dir
296 }
297
298 #[test]
301 fn a_host_reaches_the_device() {
302 let dir = scratch("host");
303 let (wire_tx, wire) = std_mpsc::channel();
304 let listener = Arc::new(Collect(Mutex::new(wire_tx)));
305 let path = dir.to_string_lossy().into_owned();
306 let radio = MobileVirtualRadio::start(path.clone(), true, listener.clone()).unwrap();
307 assert_eq!(
308 MobileVirtualRadio::start(path.clone(), true, listener.clone()).err(),
309 Some(MobileVirtualRadioError::StateInUse)
310 );
311
312 radio.attach(listener.clone());
313 let mut request = [0u8; 16];
314 let len = frame::prop_get(&mut request, 1, prop::PHY_ENABLED).unwrap();
315 let mut encoded = vec![0; hdlc::max_encoded_len(len)];
316 let encoded_len = hdlc::encode_frame(&request[..len], &mut encoded).unwrap();
317 radio.receive(encoded[..encoded_len].to_vec());
318
319 let mut decoder = hdlc::Decoder::<512>::new();
320 let mut frames = Vec::new();
321 while frames.len() < 2 {
322 let bytes = wire
323 .recv_timeout(Duration::from_secs(2))
324 .expect("the device answers");
325 for byte in bytes {
326 if let Some(Ok(frame)) = decoder.push(byte) {
327 frames.push(frame.to_vec());
328 }
329 }
330 }
331 assert_eq!(frames[1].last(), Some(&1));
333
334 radio.stop();
335 let mut again = None;
337 for _ in 0..100 {
338 match MobileVirtualRadio::start(path.clone(), false, listener.clone()) {
339 Ok(radio) => {
340 again = Some(radio);
341 break;
342 }
343 Err(_) => std::thread::sleep(Duration::from_millis(10)),
344 }
345 }
346 assert!(again.is_some());
347 let _ = std::fs::remove_dir_all(&dir);
348 }
349
350 #[test]
353 fn a_serving_radio_answers_on_its_socket() {
354 use std::io::Read;
355
356 let dir = scratch("serve");
357 let socket = dir.join("ulcp");
358 let (wire_tx, _) = std_mpsc::channel();
359 let listener = Arc::new(Collect(Mutex::new(wire_tx)));
360 let radio = MobileVirtualRadio::serve(
361 dir.to_string_lossy().into_owned(),
362 socket.to_string_lossy().into_owned(),
363 false,
364 listener,
365 )
366 .unwrap();
367 let mut stream = std::os::unix::net::UnixStream::connect(&socket).unwrap();
368 stream
369 .set_read_timeout(Some(Duration::from_secs(2)))
370 .unwrap();
371 let mut buf = [0u8; 64];
372 assert!(stream.read(&mut buf).unwrap() > 0);
373
374 radio.stop();
375 for _ in 0..100 {
376 if !socket.exists() {
377 break;
378 }
379 std::thread::sleep(Duration::from_millis(10));
380 }
381 assert!(!socket.exists());
382 let _ = std::fs::remove_dir_all(&dir);
383 }
384}