kenwood_thd75/transport/
bluetooth.rs

1//! Native macOS Bluetooth RFCOMM transport.
2//!
3//! Bypasses the broken `IOUserBluetoothSerialDriver` serial port driver
4//! and talks directly to the radio via `IOBluetoothRFCOMMChannel`.
5//! Connections can be closed and reopened without restarting `bluetoothd`.
6//!
7//! This module is only available on macOS (`cfg(target_os = "macos")`).
8
9#[cfg(any(target_os = "macos", doc))]
10#[allow(unsafe_code)]
11mod inner {
12    use std::io;
13
14    use crate::error::TransportError;
15    use crate::transport::Transport;
16
17    unsafe extern "C" {
18        fn bt_rfcomm_open(device_name: *const i8, channel: u8) -> *mut std::ffi::c_void;
19        fn bt_rfcomm_write(handle: *mut std::ffi::c_void, data: *const u8, len: usize) -> i32;
20        fn bt_rfcomm_read_fd(handle: *mut std::ffi::c_void) -> i32;
21        fn bt_rfcomm_close(handle: *mut std::ffi::c_void);
22        fn bt_pump_runloop();
23    }
24
25    /// The RFCOMM channel for the TH-D75's SPP (Serial Port) service.
26    const SPP_CHANNEL: u8 = 2;
27
28    /// Default device name for BT discovery.
29    const DEFAULT_DEVICE_NAME: &str = "TH-D75";
30
31    /// Native macOS Bluetooth transport using `IOBluetooth` RFCOMM.
32    pub struct BluetoothTransport {
33        handle: *mut std::ffi::c_void,
34        read_fd: i32,
35    }
36
37    unsafe impl Send for BluetoothTransport {}
38    unsafe impl Sync for BluetoothTransport {}
39
40    impl std::fmt::Debug for BluetoothTransport {
41        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42            f.debug_struct("BluetoothTransport")
43                .field("handle", &self.handle)
44                .field("read_fd", &self.read_fd)
45                .finish()
46        }
47    }
48
49    impl BluetoothTransport {
50        /// Connect to a TH-D75 radio via Bluetooth RFCOMM.
51        ///
52        /// # Errors
53        ///
54        /// Returns [`TransportError::NotFound`] if no device found or RFCOMM fails.
55        pub fn open(device_name: Option<&str>) -> Result<Self, TransportError> {
56            let name = device_name.unwrap_or(DEFAULT_DEVICE_NAME);
57            tracing::info!(device = %name, channel = SPP_CHANNEL, "opening Bluetooth RFCOMM");
58
59            let c_name = std::ffi::CString::new(name).map_err(|_| TransportError::NotFound)?;
60            let handle = unsafe { bt_rfcomm_open(c_name.as_ptr(), SPP_CHANNEL) };
61            if handle.is_null() {
62                return Err(TransportError::NotFound);
63            }
64
65            let read_fd = unsafe { bt_rfcomm_read_fd(handle) };
66            if read_fd < 0 {
67                unsafe { bt_rfcomm_close(handle) };
68                return Err(TransportError::NotFound);
69            }
70
71            tracing::info!(device = %name, "Bluetooth RFCOMM connected");
72            Ok(Self { handle, read_fd })
73        }
74    }
75
76    impl Transport for BluetoothTransport {
77        async fn write(&mut self, data: &[u8]) -> Result<(), TransportError> {
78            tracing::debug!(bytes = data.len(), "BT write");
79            let ret = unsafe { bt_rfcomm_write(self.handle, data.as_ptr(), data.len()) };
80            if ret != 0 {
81                return Err(TransportError::Write(io::Error::other(
82                    "RFCOMM write failed",
83                )));
84            }
85            unsafe { bt_pump_runloop() };
86            Ok(())
87        }
88
89        async fn read(&mut self, buf: &mut [u8]) -> Result<usize, TransportError> {
90            loop {
91                unsafe { bt_pump_runloop() };
92
93                let r = unsafe { libc_read(self.read_fd, buf.as_mut_ptr(), buf.len()) };
94                if r > 0 {
95                    tracing::debug!(bytes = r, "BT read");
96                    #[allow(clippy::cast_sign_loss)]
97                    return Ok(r as usize);
98                }
99                if r == 0 {
100                    return Err(TransportError::Read(io::Error::new(
101                        io::ErrorKind::UnexpectedEof,
102                        "BT pipe closed",
103                    )));
104                }
105                let err = io::Error::last_os_error();
106                if err.kind() == io::ErrorKind::WouldBlock {
107                    tokio::time::sleep(std::time::Duration::from_millis(5)).await;
108                    continue;
109                }
110                return Err(TransportError::Read(err));
111            }
112        }
113
114        async fn close(&mut self) -> Result<(), TransportError> {
115            tracing::info!("closing Bluetooth RFCOMM");
116            if !self.handle.is_null() {
117                unsafe { bt_rfcomm_close(self.handle) };
118                self.handle = std::ptr::null_mut();
119                self.read_fd = -1;
120            }
121            Ok(())
122        }
123    }
124
125    impl Drop for BluetoothTransport {
126        fn drop(&mut self) {
127            if !self.handle.is_null() {
128                unsafe { bt_rfcomm_close(self.handle) };
129            }
130        }
131    }
132
133    /// Raw read syscall (avoids `libc` dependency).
134    unsafe fn libc_read(fd: i32, buf: *mut u8, len: usize) -> isize {
135        unsafe extern "C" {
136            fn read(fd: i32, buf: *mut u8, count: usize) -> isize;
137        }
138        unsafe { read(fd, buf, len) }
139    }
140}
141
142#[cfg(any(target_os = "macos", doc))]
143pub use inner::BluetoothTransport;