1use std::fmt;
2use std::net::SocketAddr;
3
4use num_complex::Complex;
5
6#[derive(Debug, thiserror::Error)]
11pub enum ProtocolError {
12 #[error("IO error: {0}")]
13 Io(#[from] std::io::Error),
14 #[error("no devices found")]
15 NoDevicesFound,
16 #[error("not connected")]
17 NotConnected,
18 #[error("invalid packet: {0}")]
19 InvalidPacket(String),
20 #[error("timeout")]
21 Timeout,
22 #[error("connection lost")]
23 ConnectionLost,
24}
25
26pub type Result<T> = std::result::Result<T, ProtocolError>;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum HpsdrHw {
37 Hermes,
38 HermesLite,
39}
40
41impl HpsdrHw {
42 pub fn p1_code(self) -> u8 {
44 match self {
45 Self::Hermes => 1,
46 Self::HermesLite => 6,
47 }
48 }
49
50 pub fn from_p1_code(code: u8) -> Option<Self> {
53 match code {
54 1 => Some(Self::Hermes),
55 6 => Some(Self::HermesLite),
56 _ => None,
57 }
58 }
59
60 pub fn from_name(name: &str) -> Option<Self> {
64 match name.to_lowercase().as_str() {
65 "hermes" => Some(Self::Hermes),
66 "hermeslite" | "hermeslite2" => Some(Self::HermesLite),
67 _ => None,
68 }
69 }
70
71 pub fn all_names() -> &'static [&'static str] {
73 &["hermes", "hermeslite"]
74 }
75
76 pub fn max_ddcs(self) -> u8 {
79 match self {
80 Self::Hermes => 4,
81 Self::HermesLite => 2,
82 }
83 }
84
85 pub fn rx_meter_cal_offset(self) -> f64 {
89 match self {
90 Self::Hermes => -20.0,
91 Self::HermesLite => -19.0,
92 }
93 }
94}
95
96impl fmt::Display for HpsdrHw {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self {
99 Self::Hermes => write!(f, "Hermes"),
100 Self::HermesLite => write!(f, "HermesLite"),
101 }
102 }
103}
104
105pub const SAMPLE_RATES_P1: &[(u32, u8)] = &[(48_000, 0), (96_000, 1), (192_000, 2), (384_000, 3)];
110
111pub fn sample_rate_to_p1_code(rate: u32) -> u8 {
112 for &(r, c) in SAMPLE_RATES_P1 {
113 if r == rate {
114 return c;
115 }
116 }
117 0
118}
119
120pub fn p1_code_to_sample_rate(code: u8) -> Option<u32> {
121 for &(r, c) in SAMPLE_RATES_P1 {
122 if c == code {
123 return Some(r);
124 }
125 }
126 None
127}
128
129pub fn alex_tx_lpf_for_freq(freq_hz: u32) -> u8 {
136 if freq_hz <= 2_500_000 {
137 0x08 } else if freq_hz <= 5_000_000 {
139 0x04 } else if freq_hz <= 8_000_000 {
141 0x02 } else if freq_hz <= 16_500_000 {
143 0x01 } else if freq_hz <= 24_000_000 {
145 0x40 } else if freq_hz <= 35_600_000 {
147 0x20 } else {
149 0x10 }
151}
152
153pub fn alex_rx_hpf_for_freq(freq_hz: u32) -> u8 {
156 if freq_hz < 1_500_000 {
157 0x20 } else if freq_hz < 6_500_000 {
159 0x10 } else if freq_hz < 9_500_000 {
161 0x08 } else if freq_hz < 13_000_000 {
163 0x04 } else if freq_hz < 20_000_000 {
165 0x01 } else if freq_hz < 50_000_000 {
167 0x02 } else {
169 0x42 }
171}
172
173pub fn n2adr_oc_for_freq(freq_hz: u32) -> u8 {
180 if freq_hz <= 2_000_000 {
181 1 } else if freq_hz <= 4_000_000 {
183 66 } else if freq_hz <= 8_000_000 {
185 68 } else if freq_hz <= 15_000_000 {
187 72 } else if freq_hz <= 22_000_000 {
189 80 } else if freq_hz <= 30_000_000 {
191 96 } else {
193 64 }
195}
196
197#[derive(Debug, Clone)]
202pub struct DiscoveredDevice {
203 pub addr: SocketAddr,
204 pub mac: [u8; 6],
205 pub hw_type: HpsdrHw,
206 pub firmware_version: u8,
207 pub num_rxs: u8,
208 pub status: u8,
209}
210
211impl fmt::Display for DiscoveredDevice {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 write!(
214 f,
215 "{} at {} (MAC={}, FW={}, RXs={})",
216 self.hw_type,
217 self.addr,
218 mac_to_string(&self.mac),
219 self.firmware_version,
220 self.num_rxs,
221 )
222 }
223}
224
225pub fn mac_to_string(mac: &[u8; 6]) -> String {
226 mac.iter()
227 .map(|b| format!("{:02x}", b))
228 .collect::<Vec<_>>()
229 .join(":")
230}
231
232#[derive(Debug, Clone, Default)]
237pub struct RadioStatus {
238 pub ptt: bool,
239 pub adc_overflow: u8,
240 pub forward_power: u16,
241 pub reverse_power: u16,
242 pub exciter_power: u16,
243 pub supply_voltage: u16,
244 pub pa_current: u16,
245}
246
247#[inline]
257pub fn unpack_iq_24bit(buf: &[u8], offset: usize) -> Complex<f64> {
258 let i_raw = ((buf[offset] as i32) << 24)
259 | ((buf[offset + 1] as i32) << 16)
260 | ((buf[offset + 2] as i32) << 8);
261 let q_raw = ((buf[offset + 3] as i32) << 24)
262 | ((buf[offset + 4] as i32) << 16)
263 | ((buf[offset + 5] as i32) << 8);
264 Complex::new(
265 i_raw as f64 / 2_147_483_648.0,
266 -(q_raw as f64 / 2_147_483_648.0),
267 )
268}
269
270#[inline]
275pub fn pack_iq_24bit_into(buf: &mut [u8], offset: usize, sample: Complex<f64>) -> usize {
276 pack_iq_24bit_into_ex(buf, offset, sample, false)
277}
278
279#[inline]
283pub fn pack_iq_24bit_into_negate_q(buf: &mut [u8], offset: usize, sample: Complex<f64>) -> usize {
284 pack_iq_24bit_into_ex(buf, offset, sample, true)
285}
286
287#[inline]
293pub fn pack_iq_24bit_into_ex(
294 buf: &mut [u8],
295 offset: usize,
296 sample: Complex<f64>,
297 negate_q: bool,
298) -> usize {
299 let max_val: f64 = 8_388_607.0;
300 let iv = (sample.re.clamp(-1.0, 1.0) * max_val) as i32;
301 let q = if negate_q { -sample.im } else { sample.im };
302 let qv = (q.clamp(-1.0, 1.0) * max_val) as i32;
303 let iu = iv as u32 & 0xFF_FFFF;
304 let qu = qv as u32 & 0xFF_FFFF;
305 buf[offset] = ((iu >> 16) & 0xFF) as u8;
306 buf[offset + 1] = ((iu >> 8) & 0xFF) as u8;
307 buf[offset + 2] = (iu & 0xFF) as u8;
308 buf[offset + 3] = ((qu >> 16) & 0xFF) as u8;
309 buf[offset + 4] = ((qu >> 8) & 0xFF) as u8;
310 buf[offset + 5] = (qu & 0xFF) as u8;
311 offset + 6
312}
313
314pub fn unpack_tx_iq_16bit(data: &[u8]) -> Vec<Complex<f64>> {
317 let n_blocks = data.len() / 8;
318 let mut samples = Vec::with_capacity(n_blocks);
319 for k in 0..n_blocks {
320 let off = k * 8;
321 let i_val = i16::from_be_bytes([data[off + 4], data[off + 5]]);
322 let q_val = i16::from_be_bytes([data[off + 6], data[off + 7]]);
323 samples.push(Complex::new(i_val as f64 / 32768.0, q_val as f64 / 32768.0));
324 }
325 samples
326}
327
328pub fn pack_tx_iq_16bit(samples: &[Complex<f64>]) -> Vec<u8> {
331 let mut buf = vec![0u8; samples.len() * 8];
332 for (k, s) in samples.iter().enumerate() {
333 let off = k * 8;
334 let i_val = (s.re.clamp(-1.0, 1.0) * 32767.0) as i16;
336 let q_val = (s.im.clamp(-1.0, 1.0) * 32767.0) as i16;
337 let i_bytes = i_val.to_be_bytes();
338 let q_bytes = q_val.to_be_bytes();
339 buf[off + 4] = i_bytes[0];
340 buf[off + 5] = i_bytes[1];
341 buf[off + 6] = q_bytes[0];
342 buf[off + 7] = q_bytes[1];
343 }
344 buf
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
356 fn iq_24bit_roundtrip_zero() {
357 let sample = Complex::new(0.0, 0.0);
358 let mut buf = [0u8; 6];
359 pack_iq_24bit_into_negate_q(&mut buf, 0, sample);
360 let out = unpack_iq_24bit(&buf, 0);
361 assert!(out.re.abs() < 1e-6);
362 assert!(out.im.abs() < 1e-6);
363 }
364
365 #[test]
366 fn iq_24bit_roundtrip_positive_one() {
367 let sample = Complex::new(1.0, 1.0);
368 let mut buf = [0u8; 6];
369 pack_iq_24bit_into_negate_q(&mut buf, 0, sample);
370 let out = unpack_iq_24bit(&buf, 0);
371 assert!((out.re - 1.0).abs() < 2e-7 + 1.0 / 8_388_607.0);
373 assert!((out.im - 1.0).abs() < 2e-7 + 1.0 / 8_388_607.0);
374 }
375
376 #[test]
377 fn iq_24bit_roundtrip_negative_one() {
378 let sample = Complex::new(-1.0, -1.0);
379 let mut buf = [0u8; 6];
380 pack_iq_24bit_into_negate_q(&mut buf, 0, sample);
381 let out = unpack_iq_24bit(&buf, 0);
382 assert!((out.re - (-1.0)).abs() < 2e-7 + 1.0 / 8_388_607.0);
383 assert!((out.im - (-1.0)).abs() < 2e-7 + 1.0 / 8_388_607.0);
384 }
385
386 #[test]
387 fn iq_24bit_roundtrip_half() {
388 let sample = Complex::new(0.5, -0.5);
389 let mut buf = [0u8; 6];
390 pack_iq_24bit_into_negate_q(&mut buf, 0, sample);
391 let out = unpack_iq_24bit(&buf, 0);
392 assert!((out.re - 0.5).abs() < 2e-7);
393 assert!((out.im - (-0.5)).abs() < 2e-7);
394 }
395
396 #[test]
397 fn iq_24bit_known_bit_pattern() {
398 let sample = Complex::new(0.0, 0.0);
400 let mut buf = [0u8; 6];
401 pack_iq_24bit_into(&mut buf, 0, sample);
402 assert_eq!(buf, [0, 0, 0, 0, 0, 0]);
403 }
404
405 #[test]
406 fn iq_24bit_clamps_out_of_range() {
407 let sample = Complex::new(2.0, -2.0);
409 let mut buf = [0u8; 6];
410 pack_iq_24bit_into_negate_q(&mut buf, 0, sample);
411 let out = unpack_iq_24bit(&buf, 0);
412 assert!((out.re - 1.0).abs() < 2e-7 + 1.0 / 8_388_607.0);
413 assert!((out.im - (-1.0)).abs() < 2e-7 + 1.0 / 8_388_607.0);
414 }
415
416 #[test]
417 fn iq_24bit_offset_packing() {
418 let sample = Complex::new(0.25, 0.75);
420 let mut buf = [0u8; 12];
421 pack_iq_24bit_into_negate_q(&mut buf, 6, sample);
422 let out = unpack_iq_24bit(&buf, 6);
423 assert!((out.re - 0.25).abs() < 2e-7);
424 assert!((out.im - 0.75).abs() < 2e-7);
425 }
426
427 #[test]
430 fn tx_iq_16bit_roundtrip() {
431 let samples = vec![
432 Complex::new(0.5, -0.5),
433 Complex::new(0.0, 1.0),
434 Complex::new(-1.0, 0.0),
435 ];
436 let packed = pack_tx_iq_16bit(&samples);
437 assert_eq!(packed.len(), 24); let unpacked = unpack_tx_iq_16bit(&packed);
439 assert_eq!(unpacked.len(), 3);
440 for (orig, recovered) in samples.iter().zip(unpacked.iter()) {
441 assert!((orig.re - recovered.re).abs() < 5e-5);
443 assert!((orig.im - recovered.im).abs() < 5e-5);
444 }
445 }
446
447 #[test]
448 fn tx_iq_16bit_lr_bytes_zero() {
449 let samples = vec![Complex::new(0.5, 0.5)];
451 let packed = pack_tx_iq_16bit(&samples);
452 assert_eq!(packed[0], 0); assert_eq!(packed[1], 0); assert_eq!(packed[2], 0); assert_eq!(packed[3], 0); }
457
458 #[test]
461 fn hpsdr_hw_p1_roundtrip() {
462 for hw in [HpsdrHw::Hermes, HpsdrHw::HermesLite] {
463 let code = hw.p1_code();
464 assert_eq!(
465 HpsdrHw::from_p1_code(code),
466 Some(hw),
467 "P1 roundtrip failed for {:?}",
468 hw
469 );
470 }
471 }
472
473 #[test]
474 fn hpsdr_hw_unknown_code_returns_none() {
475 assert_eq!(HpsdrHw::from_p1_code(99), None);
476 }
477
478 #[test]
479 fn from_name_accepts_canonical_and_alias() {
480 assert_eq!(HpsdrHw::from_name("hermes"), Some(HpsdrHw::Hermes));
481 assert_eq!(HpsdrHw::from_name("HERMES"), Some(HpsdrHw::Hermes));
482 assert_eq!(HpsdrHw::from_name("hermeslite"), Some(HpsdrHw::HermesLite));
483 assert_eq!(HpsdrHw::from_name("hermeslite2"), Some(HpsdrHw::HermesLite));
485 assert_eq!(HpsdrHw::from_name("nope"), None);
486 }
487
488 #[test]
491 fn max_ddcs_positive() {
492 for hw in [HpsdrHw::Hermes, HpsdrHw::HermesLite] {
493 assert!(hw.max_ddcs() >= 1);
494 }
495 }
496
497 #[test]
500 fn sample_rate_code_roundtrip() {
501 for &(rate, code) in SAMPLE_RATES_P1 {
502 assert_eq!(sample_rate_to_p1_code(rate), code);
503 assert_eq!(p1_code_to_sample_rate(code), Some(rate));
504 }
505 }
506
507 #[test]
508 fn sample_rate_unknown_returns_zero() {
509 assert_eq!(sample_rate_to_p1_code(12345), 0);
510 }
511
512 #[test]
513 fn p1_code_unknown_returns_none() {
514 assert_eq!(p1_code_to_sample_rate(99), None);
515 }
516
517 #[test]
520 fn mac_to_string_format() {
521 let mac = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02];
522 let s = mac_to_string(&mac);
523 assert_eq!(s, "de:ad:be:ef:01:02");
524 }
525
526 #[test]
527 fn mac_to_string_zeros() {
528 let mac = [0x00; 6];
529 assert_eq!(mac_to_string(&mac), "00:00:00:00:00:00");
530 }
531
532 #[test]
535 fn radio_status_defaults() {
536 let status = RadioStatus::default();
537 assert!(!status.ptt);
538 assert_eq!(status.adc_overflow, 0);
539 assert_eq!(status.forward_power, 0);
540 assert_eq!(status.reverse_power, 0);
541 assert_eq!(status.exciter_power, 0);
542 }
543}