kenwood_thd75/transport/
serial.rs1use tokio::io::{AsyncReadExt, AsyncWriteExt};
30use tokio_serial::{FlowControl, SerialPort, SerialStream};
31
32use crate::error::TransportError;
33
34use super::Transport;
35
36const BT_BAUD: u32 = 9600;
38
39#[derive(Debug)]
46pub struct SerialTransport {
47 port: SerialStream,
48}
49
50impl SerialTransport {
51 pub const DEFAULT_BAUD: u32 = 115_200;
53
54 pub const USB_VID: u16 = 0x2166;
56
57 pub const USB_PID: u16 = 0x9023;
59
60 #[must_use]
62 pub fn is_bluetooth_port(path: &str) -> bool {
63 let lower = path.to_lowercase();
64 lower.contains("th-d75")
65 || lower.contains("rfcomm")
66 || (lower.contains("bluetooth") && !lower.contains("incoming"))
67 }
68
69 pub fn open(path: &str, baud: u32) -> Result<Self, TransportError> {
79 let is_bt = Self::is_bluetooth_port(path);
80 let actual_baud = if is_bt { BT_BAUD } else { baud };
81 let flow = if is_bt {
82 FlowControl::Hardware
83 } else {
84 FlowControl::None
85 };
86
87 tracing::info!(
88 path = %path,
89 baud = actual_baud,
90 bluetooth = is_bt,
91 flow_control = ?flow,
92 "opening serial port"
93 );
94
95 let builder = tokio_serial::new(path, actual_baud).flow_control(flow);
96 let port = SerialStream::open(&builder).map_err(|e| TransportError::Open {
97 path: path.to_owned(),
98 source: e.into(),
99 })?;
100 tracing::info!(path = %path, "serial port opened successfully");
101 Ok(Self { port })
102 }
103
104 pub fn discover_usb() -> Result<Vec<tokio_serial::SerialPortInfo>, TransportError> {
112 tracing::debug!(
113 vid = %format_args!("0x{:04X}", Self::USB_VID),
114 pid = %format_args!("0x{:04X}", Self::USB_PID),
115 "scanning for TH-D75 USB devices"
116 );
117 let ports = tokio_serial::available_ports().map_err(|e| TransportError::Open {
118 path: "<enumeration>".to_owned(),
119 source: e.into(),
120 })?;
121
122 let matching: Vec<_> = ports
123 .into_iter()
124 .filter(|p| {
125 matches!(
126 &p.port_type,
127 tokio_serial::SerialPortType::UsbPort(info)
128 if info.vid == Self::USB_VID && info.pid == Self::USB_PID
129 )
130 })
131 .collect();
132
133 tracing::info!(count = matching.len(), "discovered TH-D75 USB devices");
134 Ok(matching)
135 }
136
137 pub fn discover_bluetooth() -> Result<Vec<tokio_serial::SerialPortInfo>, TransportError> {
145 tracing::debug!("scanning for TH-D75 Bluetooth SPP devices");
146 let ports = tokio_serial::available_ports().map_err(|e| TransportError::Open {
147 path: "<enumeration>".to_owned(),
148 source: e.into(),
149 })?;
150
151 let matching: Vec<_> = ports
152 .into_iter()
153 .filter(|p| Self::is_bluetooth_port(&p.port_name))
154 .collect();
155
156 tracing::info!(
157 count = matching.len(),
158 "discovered TH-D75 Bluetooth devices"
159 );
160 Ok(matching)
161 }
162}
163
164impl Transport for SerialTransport {
165 fn set_baud_rate(&mut self, baud: u32) -> Result<(), TransportError> {
166 tracing::info!(baud, "changing serial baud rate");
167 self.port
168 .set_baud_rate(baud)
169 .map_err(|e| TransportError::Open {
170 path: String::new(),
171 source: std::io::Error::other(e.to_string()),
172 })
173 }
174
175 async fn write(&mut self, data: &[u8]) -> Result<(), TransportError> {
176 tracing::debug!(bytes = data.len(), "writing to transport");
177 tracing::trace!(raw = ?data, "raw bytes sent");
178 self.port
179 .write_all(data)
180 .await
181 .map_err(TransportError::Write)?;
182 self.port.flush().await.map_err(TransportError::Write)?;
183 Ok(())
184 }
185
186 async fn read(&mut self, buf: &mut [u8]) -> Result<usize, TransportError> {
187 let n = self.port.read(buf).await.map_err(TransportError::Read)?;
188 tracing::debug!(bytes = n, "read from transport");
189 tracing::trace!(raw = ?&buf[..n], "raw bytes received");
190 Ok(n)
191 }
192
193 async fn close(&mut self) -> Result<(), TransportError> {
194 tracing::info!("closing serial transport");
195 self.port
196 .shutdown()
197 .await
198 .map_err(TransportError::Disconnected)?;
199 Ok(())
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn constants_match_re_data() {
209 assert_eq!(SerialTransport::USB_VID, 0x2166);
210 assert_eq!(SerialTransport::USB_PID, 0x9023);
211 assert_eq!(SerialTransport::DEFAULT_BAUD, 115_200);
212 }
213
214 #[test]
215 fn bluetooth_port_detection() {
216 assert!(SerialTransport::is_bluetooth_port("/dev/cu.TH-D75"));
217 assert!(SerialTransport::is_bluetooth_port("/dev/tty.TH-D75"));
218 assert!(SerialTransport::is_bluetooth_port("/dev/rfcomm0"));
219 assert!(!SerialTransport::is_bluetooth_port("/dev/cu.usbmodem1101"));
220 assert!(!SerialTransport::is_bluetooth_port(
221 "/dev/cu.Bluetooth-Incoming-Port"
222 ));
223 assert!(!SerialTransport::is_bluetooth_port("COM3"));
224 }
225}