civ.rs (19650B)
1 //! CI-V framing, BCD encoding, and frame parsing for the IC-7300. 2 3 use log::trace; 4 5 /// Frequency in Hz. 6 pub type Freq = u32; 7 8 /// Two frequencies within this many Hz are considered equal. 9 pub const TOL_HZ: i64 = 5; 10 11 /// CI-V frame preamble / terminator. 12 const PREAMBLE: [u8; 2] = [0xfe, 0xfe]; 13 const TERMINATOR: u8 = 0xfd; 14 15 /// IC-7300 radio address. 16 const RADIO_ADDR: u8 = 0x94; 17 /// Controller (this program) address. 18 const CONTROLLER_ADDR: u8 = 0xE1; 19 20 /// "Sets the current operating frequency" (set-only, no echo). 21 const CMD_SET_FREQ: u8 = 0x05; 22 /// "Sets and outputs the current operating frequency" — what the radio 23 /// broadcasts when CI-V Transceive is enabled. 24 const CMD_BROADCAST_FREQ: u8 = 0x00; 25 /// "Reads the current operating frequency" — side-effect-free, used to probe 26 /// the radio during baud-rate auto-detection. 27 const CMD_READ_FREQ: u8 = 0x03; 28 29 /// "Sets and outputs the current operating mode" — what the radio broadcasts 30 /// on a mode change when CI-V Transceive is enabled. 31 const CMD_BROADCAST_MODE: u8 = 0x01; 32 /// "Reads the current operating mode". 33 const CMD_READ_MODE: u8 = 0x04; 34 /// "Sets the current operating mode" (set-only, no echo). 35 const CMD_SET_MODE: u8 = 0x06; 36 /// "Misc memory/bank/rig control" — carrier for the filter-width subcommand. 37 const CMD_CTL_MEM: u8 = 0x1a; 38 /// `1A 03` subcommand: current passband (IF) filter width. 39 const SUB_FILT_WDTH: u8 = 0x03; 40 41 fn to_bcd(n: u8) -> u8 { 42 ((n / 10) * 16) + (n % 10) 43 } 44 45 fn from_bcd(n: u8) -> u8 { 46 ((n / 16) * 10) + (n % 16) 47 } 48 49 /// Encode a `0x05` set-frequency command. 50 /// 51 /// Frequency data is 10 BCD digits (5 bytes), least-significant pair first: 52 /// 1 Hz, 10 Hz, 100 Hz, 1 kHz, 10 kHz, 100 kHz, 1 MHz, 10 MHz, then the 53 /// 100 MHz and 1 GHz digits which are fixed at 0 for the IC-7300. 54 pub fn encode_set_freq(freq: Freq) -> Vec<u8> { 55 let ones_tens_hz = (freq % 100) as u8; 56 let hundreds_hz_khz = ((freq / 100) % 100) as u8; 57 let tens_hundreds_khz = ((freq / 10_000) % 100) as u8; 58 let ones_tens_mhz = ((freq / 1_000_000) % 100) as u8; 59 let frame = vec![ 60 PREAMBLE[0], 61 PREAMBLE[1], 62 RADIO_ADDR, 63 CONTROLLER_ADDR, 64 CMD_SET_FREQ, 65 to_bcd(ones_tens_hz), 66 to_bcd(hundreds_hz_khz), 67 to_bcd(tens_hundreds_khz), 68 to_bcd(ones_tens_mhz), 69 0x00, // 100 MHz + 1 GHz digits (fixed 0) 70 TERMINATOR, 71 ]; 72 trace!("encoded set_freq {} Hz -> {:02x?}", freq, frame); 73 frame 74 } 75 76 /// Enable CI-V Transceive (`1A 05 00 89 01`) so the radio pushes `0x00` 77 /// broadcasts on knob/mode changes. 78 pub fn encode_transceive_enable() -> Vec<u8> { 79 vec![ 80 PREAMBLE[0], 81 PREAMBLE[1], 82 RADIO_ADDR, 83 CONTROLLER_ADDR, 84 0x1a, 85 0x05, 86 0x00, 87 0x89, 88 0x01, 89 TERMINATOR, 90 ] 91 } 92 93 /// "Reads the current operating frequency" (`0x03`, no data). The radio 94 /// replies with a `0x03` frequency frame; used to probe the serial baud rate. 95 pub fn encode_read_freq() -> Vec<u8> { 96 vec![ 97 PREAMBLE[0], 98 PREAMBLE[1], 99 RADIO_ADDR, 100 CONTROLLER_ADDR, 101 CMD_READ_FREQ, 102 TERMINATOR, 103 ] 104 } 105 106 /// IC-7300 operating-mode data byte values (for the `0x01`/`0x04`/`0x06` 107 /// commands). See "Operating mode" in the CI-V reference guide. 108 pub mod icmode { 109 pub const LSB: u8 = 0x00; 110 pub const USB: u8 = 0x01; 111 pub const AM: u8 = 0x02; 112 pub const CW: u8 = 0x03; 113 pub const RTTY: u8 = 0x04; 114 pub const FM: u8 = 0x05; 115 pub const CW_R: u8 = 0x07; 116 pub const RTTY_R: u8 = 0x08; 117 } 118 119 /// SDR++ radio demodulator IDs. These mirror the `RADIO_IFACE_MODE_*` / `DemodID` 120 /// enum in SDR++'s `radio` decoder module (`decoder_modules/radio/src`). 121 /// A few constants are unused here but kept for completeness/documentation. 122 #[allow(dead_code)] 123 pub mod sdrmode { 124 pub const NFM: i32 = 0; 125 pub const WFM: i32 = 1; 126 pub const AM: i32 = 2; 127 pub const DSB: i32 = 3; 128 pub const USB: i32 = 4; 129 pub const CW: i32 = 5; 130 pub const LSB: i32 = 6; 131 pub const RAW: i32 = 7; 132 } 133 134 /// "Sets the current operating mode" (`0x06`, set-only, no echo). 135 pub fn encode_set_mode(mode: u8) -> Vec<u8> { 136 vec![ 137 PREAMBLE[0], 138 PREAMBLE[1], 139 RADIO_ADDR, 140 CONTROLLER_ADDR, 141 CMD_SET_MODE, 142 mode, 143 TERMINATOR, 144 ] 145 } 146 147 /// "Sets the current passband filter width" (`1A 03 <index>`) to one of the 148 /// IC-7300's discrete filter steps. 149 pub fn encode_set_filter(index: u8) -> Vec<u8> { 150 vec![ 151 PREAMBLE[0], 152 PREAMBLE[1], 153 RADIO_ADDR, 154 CONTROLLER_ADDR, 155 CMD_CTL_MEM, 156 SUB_FILT_WDTH, 157 to_bcd(index), 158 TERMINATOR, 159 ] 160 } 161 162 /// "Reads the current passband filter width" (`1A 03`, no data). The radio 163 /// replies with a `1A 03 <index>` frame. 164 pub fn encode_read_filter() -> Vec<u8> { 165 vec![ 166 PREAMBLE[0], 167 PREAMBLE[1], 168 RADIO_ADDR, 169 CONTROLLER_ADDR, 170 CMD_CTL_MEM, 171 SUB_FILT_WDTH, 172 TERMINATOR, 173 ] 174 } 175 176 /// Translate an IC-7300 operating-mode byte to an SDR++ demodulator ID. 177 /// 178 /// Best-effort for modes with no exact SDR++ equivalent: `RTTY` → `USB`, 179 /// `CW-R` → `CW`, `RTTY-R` → `LSB`, `FM` → `NFM`. 180 pub fn icmode_to_sdrmode(mode: u8) -> Option<i32> { 181 use icmode as r; 182 use sdrmode as s; 183 Some(match mode { 184 r::LSB => s::LSB, 185 r::USB => s::USB, 186 r::AM => s::AM, 187 r::CW => s::CW, 188 r::RTTY => s::USB, 189 r::FM => s::NFM, 190 r::CW_R => s::CW, 191 r::RTTY_R => s::LSB, 192 _ => return None, 193 }) 194 } 195 196 /// Translate an SDR++ demodulator ID to an IC-7300 operating-mode byte. 197 /// 198 /// `WFM`/`DSB`/`RAW` have no IC-7300 equivalent and return `None`. 199 pub fn sdrmode_to_icmode(mode: i32) -> Option<u8> { 200 use icmode as r; 201 use sdrmode as s; 202 Some(match mode { 203 s::LSB => r::LSB, 204 s::USB => r::USB, 205 s::AM => r::AM, 206 s::CW => r::CW, 207 s::NFM => r::FM, 208 _ => return None, 209 }) 210 } 211 212 /// Whether bandwidth sync applies to a given SDR++ mode. SSB/CW/AM have a 213 /// `1A 03` filter width; FM/NFM uses fixed FIL1/2/3 presets instead. 214 pub fn bandwidth_applicable(sdr_mode: i32) -> bool { 215 matches!( 216 sdr_mode, 217 sdrmode::AM | sdrmode::USB | sdrmode::LSB | sdrmode::CW 218 ) 219 } 220 221 /// SSB/CW/RTTY filter width in Hz for a filter index (0..=40). 222 /// Indices 0..=9 are 50..500 Hz (50 Hz steps); 10..=40 are 600..3600 Hz 223 /// (100 Hz steps). See "IF filter width settings" in the CI-V reference guide. 224 fn ssb_filter_hz(index: u8) -> Option<u32> { 225 match index { 226 0..=9 => Some(50 + u32::from(index) * 50), 227 10..=40 => Some(600 + u32::from(index - 10) * 100), 228 _ => None, 229 } 230 } 231 232 /// AM filter width in Hz for a filter index (0..=49): 200 Hz..10 kHz in 200 Hz 233 /// steps. 234 fn am_filter_hz(index: u8) -> Option<u32> { 235 if index <= 49 { 236 Some(200 + u32::from(index) * 200) 237 } else { 238 None 239 } 240 } 241 242 /// Map a filter index to Hz for the given mode class. 243 pub fn filter_index_to_hz(is_am: bool, index: u8) -> Option<u32> { 244 if is_am { 245 am_filter_hz(index) 246 } else { 247 ssb_filter_hz(index) 248 } 249 } 250 251 /// Map a bandwidth in Hz to the nearest IC-7300 filter index for the given mode 252 /// class. 253 pub fn hz_to_filter_index(is_am: bool, hz: u32) -> Option<u8> { 254 let max = if is_am { 49u8 } else { 40u8 }; 255 let mut best: Option<u8> = None; 256 let mut best_diff = u32::MAX; 257 for i in 0..=max { 258 let h = filter_index_to_hz(is_am, i)?; 259 let d = h.abs_diff(hz); 260 if d < best_diff { 261 best_diff = d; 262 best = Some(i); 263 } 264 } 265 best 266 } 267 268 #[derive(Debug, PartialEq, Eq)] 269 pub enum CivMsg { 270 Freq(Freq), 271 /// Operating mode byte (IC-7300 `icmode::*`). 272 Mode(u8), 273 /// Filter-width index (0..=49, BCD-decoded). 274 Filter(u8), 275 } 276 277 #[derive(Debug, PartialEq, Eq)] 278 pub enum CivMsgError { 279 InputTooShort, 280 InvalidStartOrEnd, 281 WrongAddress, 282 WrongCommand, 283 } 284 285 impl TryFrom<&[u8]> for CivMsg { 286 type Error = CivMsgError; 287 288 fn try_from(frame: &[u8]) -> Result<Self, Self::Error> { 289 if frame.len() < 5 { 290 return Err(CivMsgError::InputTooShort); 291 } 292 if frame[..2] != PREAMBLE || frame.last() != Some(&TERMINATOR) { 293 return Err(CivMsgError::InvalidStartOrEnd); 294 } 295 // Accept any frame *from* the radio (`to`/`from` = `* 94`). The 296 // destination (`frame[2]`) varies: `00` broadcast, `E0` the default 297 // controller address, or `E1` us. Echoes of our own commands have 298 // `to`/`from` = `94 E1` and are rejected here. 299 if frame[3] != RADIO_ADDR { 300 return Err(CivMsgError::WrongAddress); 301 } 302 // `0x00` (set-and-output / transceive broadcast) and `0x03` (read 303 // reply) both carry the same 5-byte BCD frequency payload. 304 if (frame[4] == CMD_BROADCAST_FREQ || frame[4] == CMD_READ_FREQ) && frame.len() == 11 { 305 let freq = from_bcd(frame[5]) as Freq 306 + from_bcd(frame[6]) as Freq * 100 307 + from_bcd(frame[7]) as Freq * 10_000 308 + from_bcd(frame[8]) as Freq * 1_000_000; 309 return Ok(CivMsg::Freq(freq)); 310 } 311 // `0x01` (transceive broadcast) and `0x04` (read reply) carry the 312 // operating mode as the first data byte; an optional FIL1/2/3 filter 313 // byte follows and is ignored. 314 if (frame[4] == CMD_BROADCAST_MODE || frame[4] == CMD_READ_MODE) && frame.len() >= 7 { 315 return Ok(CivMsg::Mode(frame[5])); 316 } 317 // `1A 03 <index>` — current passband filter width (read reply), a 318 // single BCD-encoded filter index. 319 if frame[4] == CMD_CTL_MEM && frame[5] == SUB_FILT_WDTH && frame.len() >= 8 { 320 return Ok(CivMsg::Filter(from_bcd(frame[6]))); 321 } 322 Err(CivMsgError::WrongCommand) 323 } 324 } 325 326 /// Streaming CI-V frame splitter. Accumulates bytes across reads and yields 327 /// complete frames, tolerating frames that span reads and stray/garbage bytes. 328 pub struct Framer { 329 buf: Vec<u8>, 330 } 331 332 impl Framer { 333 pub fn new() -> Self { 334 Framer { buf: Vec::new() } 335 } 336 337 pub fn push(&mut self, data: &[u8]) -> Vec<Vec<u8>> { 338 self.buf.extend_from_slice(data); 339 340 let mut frames = Vec::new(); 341 while let Some((start, end)) = next_frame(&self.buf) { 342 frames.push(self.buf[start..end].to_vec()); 343 self.buf.drain(..end); 344 } 345 346 // Drop leading garbage when no frame start is present, keeping a 347 // possible trailing 0xfe that may be half of a split preamble. 348 if self.buf.len() >= 2 && !self.buf.windows(2).any(|w| w == PREAMBLE) { 349 let keep = usize::from(self.buf.last() == Some(&PREAMBLE[0])); 350 let drop = self.buf.len() - keep; 351 if drop > 0 { 352 self.buf.drain(..drop); 353 } 354 } 355 356 // Resync if a frame never terminates. 357 if self.buf.len() > 4096 { 358 self.buf.clear(); 359 } 360 361 frames 362 } 363 } 364 365 fn next_frame(buf: &[u8]) -> Option<(usize, usize)> { 366 let start = buf.windows(2).position(|w| w == PREAMBLE)?; 367 let body = &buf[start + 2..]; 368 let end_rel = body.iter().position(|&b| b == TERMINATOR)?; 369 Some((start, start + 2 + end_rel + 1)) 370 } 371 372 impl Default for Framer { 373 fn default() -> Self { 374 Self::new() 375 } 376 } 377 378 #[cfg(test)] 379 mod tests { 380 use super::*; 381 382 #[test] 383 fn bcd_roundtrip() { 384 for n in 0u8..=99 { 385 assert_eq!(from_bcd(to_bcd(n)), n); 386 } 387 } 388 389 #[test] 390 fn set_freq_uses_command_05() { 391 let frame = encode_set_freq(7_100_000); 392 assert_eq!(frame.len(), 11); 393 assert_eq!(frame[..5], [0xfe, 0xfe, 0x94, 0xe1, 0x05]); 394 assert_eq!(frame[10], 0xfd); 395 } 396 397 #[test] 398 fn read_freq_uses_command_03() { 399 let frame = encode_read_freq(); 400 assert_eq!(frame, [0xfe, 0xfe, 0x94, 0xe1, 0x03, 0xfd]); 401 } 402 403 #[test] 404 fn encode_set_freq_matches_reference() { 405 for freq in [0, 1, 99, 7_100_000, 14_250_500, 99_999_999] { 406 let frame = encode_set_freq(freq); 407 // Decode our own 0x05 frame by re-reading the BCD digits. 408 let decoded = from_bcd(frame[5]) as u32 409 + from_bcd(frame[6]) as u32 * 100 410 + from_bcd(frame[7]) as u32 * 10_000 411 + from_bcd(frame[8]) as u32 * 1_000_000; 412 assert_eq!(decoded, freq, "freq {}", freq); 413 } 414 } 415 416 #[test] 417 fn parse_broadcast_freq() { 418 // 7.1 MHz broadcast from the radio: FE FE E1 94 00 <bcd> 00 FD 419 let frame = [ 420 0xfe, 0xfe, 0xe1, 0x94, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 421 ]; 422 assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Freq(7_100_000))); 423 } 424 425 #[test] 426 fn parse_read_freq_reply() { 427 // 7.1 MHz reply to a 0x03 read: FE FE E1 94 03 <bcd> 00 FD 428 let frame = [ 429 0xfe, 0xfe, 0xe1, 0x94, 0x03, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 430 ]; 431 assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Freq(7_100_000))); 432 } 433 434 #[test] 435 fn parse_broadcast_to_default_controller() { 436 // Broadcast directed at 0xE0 (default controller): FE FE E0 94 00 ... 437 let frame = [ 438 0xfe, 0xfe, 0xe0, 0x94, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 439 ]; 440 assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Freq(7_100_000))); 441 } 442 443 #[test] 444 fn rejects_echo_of_our_command() { 445 // Our own 0x05 set-freq command echoed back: FE FE 94 E1 05 ... 446 let frame = [ 447 0xfe, 0xfe, 0x94, 0xe1, 0x05, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 448 ]; 449 assert_eq!(CivMsg::try_from(&frame[..]), Err(CivMsgError::WrongAddress)); 450 } 451 452 #[test] 453 fn parse_rejects_wrong_address_and_command() { 454 let freq_frame = [ 455 0xfe, 0xfe, 0xe1, 0x94, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 456 ]; 457 let wrong_addr = [ 458 0xfe, 0xfe, 0xe1, 0x55, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 459 ]; 460 let wrong_cmd = [ 461 0xfe, 0xfe, 0xe1, 0x94, 0x09, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 462 ]; 463 let short = [0xfe, 0xfe, 0xe1, 0x94]; 464 let bad_end = [ 465 0xfe, 0xfe, 0xe1, 0x94, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0x00, 466 ]; 467 assert_eq!( 468 CivMsg::try_from(&freq_frame[..]), 469 Ok(CivMsg::Freq(7_100_000)) 470 ); 471 assert_eq!( 472 CivMsg::try_from(&wrong_addr[..]), 473 Err(CivMsgError::WrongAddress) 474 ); 475 assert_eq!( 476 CivMsg::try_from(&wrong_cmd[..]), 477 Err(CivMsgError::WrongCommand) 478 ); 479 assert_eq!( 480 CivMsg::try_from(&short[..]), 481 Err(CivMsgError::InputTooShort) 482 ); 483 assert_eq!( 484 CivMsg::try_from(&bad_end[..]), 485 Err(CivMsgError::InvalidStartOrEnd) 486 ); 487 } 488 489 #[test] 490 fn framer_splits_across_reads() { 491 let frame = [ 492 0xfe, 0xfe, 0xe1, 0x94, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 493 ]; 494 let mut f = Framer::new(); 495 assert!(f.push(&frame[..3]).is_empty()); 496 let out = f.push(&frame[3..]); 497 assert_eq!(out, vec![frame.to_vec()]); 498 } 499 500 #[test] 501 fn framer_drops_garbage_and_yields_frames() { 502 let frame = [ 503 0xfe, 0xfe, 0xe1, 0x94, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 504 ]; 505 let mut data = vec![0xaa, 0xbb]; 506 data.extend_from_slice(&frame); 507 data.extend_from_slice(&frame); 508 let out = Framer::new().push(&data); 509 assert_eq!(out, vec![frame.to_vec(), frame.to_vec()]); 510 } 511 512 #[test] 513 fn framer_handles_lone_preamble_byte() { 514 let frame = [ 515 0xfe, 0xfe, 0xe1, 0x94, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd, 516 ]; 517 let mut f = Framer::new(); 518 assert!(f.push(&[0xfe]).is_empty()); 519 let mut rest = vec![0xfe]; 520 rest.extend_from_slice(&frame[2..]); 521 let out = f.push(&rest); 522 assert_eq!(out, vec![frame.to_vec()]); 523 } 524 525 #[test] 526 fn encode_set_mode_uses_command_06() { 527 let frame = encode_set_mode(icmode::USB); 528 assert_eq!(frame, [0xfe, 0xfe, 0x94, 0xe1, 0x06, icmode::USB, 0xfd]); 529 } 530 531 #[test] 532 fn encode_set_filter_uses_1a_03() { 533 // index 28 -> BCD 0x28 534 let frame = encode_set_filter(28); 535 assert_eq!(frame, [0xfe, 0xfe, 0x94, 0xe1, 0x1a, 0x03, 0x28, 0xfd]); 536 } 537 538 #[test] 539 fn parse_mode_broadcast() { 540 // Radio transceive mode broadcast: FE FE 00 94 01 <mode> FD 541 let frame = [0xfe, 0xfe, 0x00, 0x94, 0x01, icmode::USB, 0xfd]; 542 assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Mode(icmode::USB))); 543 } 544 545 #[test] 546 fn parse_mode_read_reply() { 547 // Reply to 0x04 read: FE FE E1 94 04 <mode> <filter> FD 548 let frame = [0xfe, 0xfe, 0xe1, 0x94, 0x04, icmode::LSB, 0x01, 0xfd]; 549 assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Mode(icmode::LSB))); 550 } 551 552 #[test] 553 fn parse_filter_read_reply() { 554 // Reply to 1A 03 read: FE FE E1 94 1A 03 <bcd index> FD 555 let frame = [0xfe, 0xfe, 0xe1, 0x94, 0x1a, 0x03, 0x28, 0xfd]; 556 assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Filter(28))); 557 } 558 559 #[test] 560 fn mode_mapping_exact_and_best_effort() { 561 assert_eq!(icmode_to_sdrmode(icmode::LSB), Some(sdrmode::LSB)); 562 assert_eq!(icmode_to_sdrmode(icmode::USB), Some(sdrmode::USB)); 563 assert_eq!(icmode_to_sdrmode(icmode::AM), Some(sdrmode::AM)); 564 assert_eq!(icmode_to_sdrmode(icmode::CW), Some(sdrmode::CW)); 565 // best effort 566 assert_eq!(icmode_to_sdrmode(icmode::RTTY), Some(sdrmode::USB)); 567 assert_eq!(icmode_to_sdrmode(icmode::CW_R), Some(sdrmode::CW)); 568 assert_eq!(icmode_to_sdrmode(icmode::RTTY_R), Some(sdrmode::LSB)); 569 assert_eq!(icmode_to_sdrmode(icmode::FM), Some(sdrmode::NFM)); 570 571 assert_eq!(sdrmode_to_icmode(sdrmode::LSB), Some(icmode::LSB)); 572 assert_eq!(sdrmode_to_icmode(sdrmode::USB), Some(icmode::USB)); 573 assert_eq!(sdrmode_to_icmode(sdrmode::AM), Some(icmode::AM)); 574 assert_eq!(sdrmode_to_icmode(sdrmode::CW), Some(icmode::CW)); 575 assert_eq!(sdrmode_to_icmode(sdrmode::NFM), Some(icmode::FM)); 576 assert_eq!(sdrmode_to_icmode(sdrmode::WFM), None); 577 assert_eq!(sdrmode_to_icmode(sdrmode::DSB), None); 578 assert_eq!(sdrmode_to_icmode(sdrmode::RAW), None); 579 } 580 581 #[test] 582 fn filter_width_mapping() { 583 // SSB/CW table: index 0 = 50 Hz, 9 = 500 Hz, 10 = 600 Hz, 40 = 3600 Hz. 584 assert_eq!(filter_index_to_hz(false, 0), Some(50)); 585 assert_eq!(filter_index_to_hz(false, 9), Some(500)); 586 assert_eq!(filter_index_to_hz(false, 10), Some(600)); 587 assert_eq!(filter_index_to_hz(false, 40), Some(3600)); 588 assert_eq!(filter_index_to_hz(false, 41), None); 589 // AM table: 0 = 200 Hz, 49 = 10000 Hz. 590 assert_eq!(filter_index_to_hz(true, 0), Some(200)); 591 assert_eq!(filter_index_to_hz(true, 49), Some(10_000)); 592 assert_eq!(filter_index_to_hz(true, 50), None); 593 594 // Nearest-index round trips. 595 assert_eq!(hz_to_filter_index(false, 3600), Some(40)); 596 assert_eq!(hz_to_filter_index(false, 250), Some(4)); // 250 Hz 597 assert_eq!(hz_to_filter_index(true, 1000), Some(4)); // 1000 Hz 598 // Slightly off-step snaps to the nearest step (600 vs 700 -> 700). 599 assert_eq!(hz_to_filter_index(false, 670), Some(11)); 600 } 601 }