sync.rs (12116B)
1 //! The background sync thread: reads CI-V frames from the IC-7300 and polls the 2 //! SDR++ VFO frequency, mirroring changes in both directions with dedup. 3 4 use std::sync::atomic::{AtomicBool, Ordering}; 5 use std::sync::{Arc, Mutex}; 6 use std::time::{Duration, Instant}; 7 8 use log::{debug, error, trace}; 9 use serialport::{DataBits, Parity, StopBits}; 10 11 use crate::civ::{self, CivMsg, CivMsgError, Framer, Freq, TOL_HZ}; 12 13 const DEFAULT_BAUD: u32 = 115_200; 14 const DETECT_TIMEOUT: Duration = Duration::from_millis(300); 15 const READ_TIMEOUT: Duration = Duration::from_millis(100); 16 const POLL_INTERVAL: Duration = Duration::from_millis(100); 17 /// How often to poll the radio's filter width (radio -> SDR bandwidth). 18 const FILTER_POLL_INTERVAL: Duration = Duration::from_millis(500); 19 20 /// Candidate rates probed during auto-detection, most common first. Standard 21 /// Icom CI-V rates (19200/9600/4800) precede the USB-only high rates. 22 const BAUD_CANDIDATES: [u32; 6] = [19_200, 9_600, 4_800, 115_200, 38_400, 57_600]; 23 24 pub fn run( 25 port: String, 26 baud: u32, 27 sync_mode: Arc<AtomicBool>, 28 stop: Arc<AtomicBool>, 29 status: Arc<Mutex<String>>, 30 ) { 31 set_status(&status, "Connecting..."); 32 33 // 0 means "auto": probe the radio with a read-frequency command at each 34 // candidate rate and use the first that yields a valid CI-V frame. 35 let baud = if baud == 0 { 36 detect_baud(&port).unwrap_or(DEFAULT_BAUD) 37 } else { 38 baud 39 }; 40 41 let mut serial = match open_port(&port, baud) { 42 Ok(serial) => serial, 43 Err(e) => { 44 set_status(&status, &format!("Serial error: {}", e)); 45 error!("failed to open serial port {} @ {}: {}", port, baud, e); 46 return; 47 } 48 }; 49 set_status(&status, &format!("Connected @ {} baud", baud)); 50 51 // Make the radio broadcast frequency changes to us. 52 let transceive = civ::encode_transceive_enable(); 53 if let Err(e) = serial.write_all(&transceive) { 54 set_status(&status, &format!("Serial error: {}", e)); 55 error!("failed to enable CI-V transceive: {}", e); 56 return; 57 } 58 59 // Adopt the SDR's current state as the baseline. 60 let mut freq = unsafe { crate::civ_bridge_get_vfo_freq() }.round() as Freq; 61 debug!("initial frequency baseline: {} Hz", freq); 62 63 // Baseline mode (-1 means the SDR VFO is not a "radio" demodulator) and 64 // bandwidth for the mode/filter sync. 65 let mut mode = unsafe { crate::civ_bridge_get_mode() }; 66 let mut bw = unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32; 67 68 // On connect, the SDR++ VFO is authoritative: push its current frequency, 69 // mode, and filter width to the radio once so the two start in agreement. 70 let frame = civ::encode_set_freq(freq); 71 if let Err(e) = serial.write_all(&frame) { 72 set_status(&status, &format!("Serial error: {}", e)); 73 error!("initial frequency sync failed: {}", e); 74 return; 75 } 76 if sync_mode.load(Ordering::Relaxed) && mode >= 0 { 77 if let Some(m) = civ::sdrmode_to_icmode(mode) { 78 if let Err(e) = serial.write_all(&civ::encode_set_mode(m)) { 79 set_status(&status, &format!("Serial error: {}", e)); 80 error!("initial mode sync failed: {}", e); 81 return; 82 } 83 } 84 if civ::bandwidth_applicable(mode) { 85 let is_am = mode == civ::sdrmode::AM; 86 if let Some(i) = civ::hz_to_filter_index(is_am, bw) { 87 if let Err(e) = serial.write_all(&civ::encode_set_filter(i)) { 88 set_status(&status, &format!("Serial error: {}", e)); 89 error!("initial filter sync failed: {}", e); 90 return; 91 } 92 bw = civ::filter_index_to_hz(is_am, i).unwrap_or(bw); 93 } 94 } 95 } 96 debug!( 97 "pushed initial state to radio: {} Hz, mode {}, bw {} Hz", 98 freq, mode, bw 99 ); 100 101 let mut framer = Framer::new(); 102 let mut read_buf = [0u8; 1024]; 103 let mut last_poll = Instant::now(); 104 let mut last_filter_poll = Instant::now(); 105 106 while !stop.load(Ordering::Relaxed) { 107 // Read the live toggle each iteration so the checkbox takes effect 108 // even while connected. 109 let sync_mode = sync_mode.load(Ordering::Relaxed); 110 111 // Radio -> SDR: parse incoming CI-V frames. 112 match serial.read(&mut read_buf) { 113 Ok(0) => {} 114 Ok(n) => { 115 trace!("read {} bytes: {:02x?}", n, &read_buf[..n]); 116 for frame in framer.push(&read_buf[..n]) { 117 match CivMsg::try_from(frame.as_slice()) { 118 Ok(CivMsg::Freq(f)) => { 119 trace!("radio -> SDR: {} Hz", f); 120 if freq_diff(f, freq) > TOL_HZ { 121 debug!("tuning SDR to {} Hz", f); 122 unsafe { crate::civ_bridge_tune(f as f64) }; 123 freq = f; 124 } 125 } 126 Ok(CivMsg::Mode(m)) if sync_mode => { 127 if let Some(s) = civ::icmode_to_sdrmode(m) { 128 if s != mode { 129 debug!("radio -> SDR mode: {}", s); 130 unsafe { crate::civ_bridge_set_mode(s) }; 131 mode = s; 132 bw = 133 unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32; 134 } 135 } 136 } 137 Ok(CivMsg::Filter(i)) if sync_mode => { 138 let is_am = mode == civ::sdrmode::AM; 139 if let Some(hz) = civ::filter_index_to_hz(is_am, i) { 140 if freq_diff(hz, bw) > bw_tolerance(is_am) { 141 debug!("radio -> SDR bandwidth: {} Hz", hz); 142 unsafe { crate::civ_bridge_set_bandwidth(hz as f64) }; 143 bw = hz; 144 } 145 } 146 } 147 Ok(_) => {} 148 Err(CivMsgError::WrongAddress) => { 149 // Echo of our own command (`to`/`from` = `94 E1`), 150 // e.g. the `1A 03` read poll; expected and noisy. 151 } 152 Err(e) => debug!("ignored CI-V frame ({:?}): {:02x?}", e, frame), 153 } 154 } 155 } 156 Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {} 157 Err(e) => { 158 set_status(&status, &format!("Serial error: {}", e)); 159 error!("serial read failed: {}", e); 160 break; 161 } 162 } 163 164 // SDR -> Radio: poll frequency/mode/bandwidth every 100 ms. 165 if last_poll.elapsed() >= POLL_INTERVAL { 166 let sdr = unsafe { crate::civ_bridge_get_vfo_freq() }.round() as Freq; 167 if freq_diff(sdr, freq) > TOL_HZ { 168 debug!("setting radio to {} Hz", sdr); 169 let frame = civ::encode_set_freq(sdr); 170 if let Err(e) = serial.write_all(&frame) { 171 set_status(&status, &format!("Serial error: {}", e)); 172 error!("serial write failed: {}", e); 173 break; 174 } 175 freq = sdr; 176 } 177 178 if sync_mode { 179 let s = unsafe { crate::civ_bridge_get_mode() }; 180 if s >= 0 && s != mode { 181 if let Some(m) = civ::sdrmode_to_icmode(s) { 182 debug!("setting radio mode to {:02x}", m); 183 let frame = civ::encode_set_mode(m); 184 if let Err(e) = serial.write_all(&frame) { 185 set_status(&status, &format!("Serial error: {}", e)); 186 error!("serial write failed: {}", e); 187 break; 188 } 189 } 190 mode = s; 191 bw = unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32; 192 } 193 194 if civ::bandwidth_applicable(mode) { 195 let sdr_bw = unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32; 196 let is_am = mode == civ::sdrmode::AM; 197 if freq_diff(sdr_bw, bw) > bw_tolerance(is_am) { 198 if let Some(i) = civ::hz_to_filter_index(is_am, sdr_bw) { 199 debug!("setting radio filter to index {}", i); 200 let frame = civ::encode_set_filter(i); 201 if let Err(e) = serial.write_all(&frame) { 202 set_status(&status, &format!("Serial error: {}", e)); 203 error!("serial write failed: {}", e); 204 break; 205 } 206 bw = civ::filter_index_to_hz(is_am, i).unwrap_or(sdr_bw); 207 } 208 } 209 } 210 } 211 212 last_poll = Instant::now(); 213 } 214 215 // Radio -> SDR bandwidth: poll the radio's filter width (no transceive 216 // broadcast for width, so read it explicitly). 217 if sync_mode && last_filter_poll.elapsed() >= FILTER_POLL_INTERVAL { 218 if civ::bandwidth_applicable(mode) { 219 let frame = civ::encode_read_filter(); 220 if let Err(e) = serial.write_all(&frame) { 221 set_status(&status, &format!("Serial error: {}", e)); 222 error!("serial write failed: {}", e); 223 break; 224 } 225 } 226 last_filter_poll = Instant::now(); 227 } 228 } 229 230 set_status(&status, "Disconnected"); 231 } 232 233 fn open_port(port: &str, baud: u32) -> Result<Box<dyn serialport::SerialPort>, serialport::Error> { 234 serialport::new(port, baud) 235 .data_bits(DataBits::Eight) 236 .parity(Parity::None) 237 .stop_bits(StopBits::One) 238 .timeout(READ_TIMEOUT) 239 .open() 240 } 241 242 /// Try each candidate baud rate, sending a side-effect-free `0x03` read 243 /// command and watching for a parseable CI-V frame in reply. 244 fn detect_baud(port: &str) -> Option<u32> { 245 for &baud in &BAUD_CANDIDATES { 246 let mut serial = match open_port(port, baud) { 247 Ok(serial) => serial, 248 Err(_) => continue, 249 }; 250 if serial.write_all(&civ::encode_read_freq()).is_err() { 251 continue; 252 } 253 254 let mut framer = Framer::new(); 255 let mut buf = [0u8; 256]; 256 let deadline = Instant::now() + DETECT_TIMEOUT; 257 let mut found = false; 258 while Instant::now() < deadline && !found { 259 match serial.read(&mut buf) { 260 Ok(0) => {} 261 Ok(n) => { 262 for frame in framer.push(&buf[..n]) { 263 if CivMsg::try_from(frame.as_slice()).is_ok() { 264 found = true; 265 } 266 } 267 } 268 Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {} 269 Err(_) => break, 270 } 271 } 272 if found { 273 debug!("auto-detected serial baud: {}", baud); 274 return Some(baud); 275 } 276 debug!("no CI-V reply at {} baud", baud); 277 } 278 None 279 } 280 281 fn freq_diff(a: Freq, b: Freq) -> i64 { 282 (a as i64 - b as i64).abs() 283 } 284 285 /// Half-step bandwidth tolerance in Hz. AM steps are 200 Hz, SSB/CW steps are 286 /// 50/100 Hz, so this is enough to avoid ping-pong while still tracking real 287 /// changes. 288 fn bw_tolerance(is_am: bool) -> i64 { 289 if is_am { 290 100 291 } else { 292 50 293 } 294 } 295 296 fn set_status(status: &Arc<Mutex<String>>, text: &str) { 297 if let Ok(mut s) = status.lock() { 298 *s = text.to_string(); 299 } 300 }