]> code.octet-stream.net Git - m17rt/commitdiff
Voice UDP to RF conversion, and better sample rate management in codec2
authorThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 14 May 2025 09:30:04 +0000 (19:30 +1000)
committerThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 14 May 2025 09:30:04 +0000 (19:30 +1000)
32 files changed:
Cargo.lock [changed mode: 0755->0644]
Cargo.toml [changed mode: 0755->0644]
buildscripts/build.sh [changed mode: 0755->0644]
buildscripts/dist-generic.sh [changed mode: 0755->0644]
buildscripts/dist.sh [changed mode: 0755->0644]
buildscripts/lint.sh [changed mode: 0755->0644]
buildscripts/test.sh [changed mode: 0755->0644]
m17app/Cargo.toml [changed mode: 0755->0644]
m17app/src/lib.rs [changed mode: 0755->0644]
m17app/src/link_setup.rs
m17codec2/Cargo.toml [changed mode: 0755->0644]
m17codec2/src/lib.rs [changed mode: 0755->0644]
m17codec2/src/soundcards.rs [new file with mode: 0644]
m17core/Cargo.toml [changed mode: 0755->0644]
m17core/src/address.rs [changed mode: 0755->0644]
m17core/src/bits.rs [changed mode: 0755->0644]
m17core/src/crc.rs [changed mode: 0755->0644]
m17core/src/decode.rs [changed mode: 0755->0644]
m17core/src/fec.rs [changed mode: 0755->0644]
m17core/src/interleave.rs [changed mode: 0755->0644]
m17core/src/lib.rs [changed mode: 0755->0644]
m17core/src/protocol.rs [changed mode: 0755->0644]
m17core/src/random.rs [changed mode: 0755->0644]
m17core/src/reflector.rs [deleted file]
m17core/src/reflector/convert.rs [new file with mode: 0644]
m17core/src/reflector/mod.rs [new file with mode: 0644]
m17core/src/reflector/packet.rs [new file with mode: 0644]
m17core/src/shaping.rs [changed mode: 0755->0644]
tools/m17rt-demod/Cargo.toml [changed mode: 0755->0644]
tools/m17rt-demod/src/main.rs [changed mode: 0755->0644]
tools/m17rt-rxpacket/Cargo.toml [changed mode: 0755->0644]
tools/m17rt-rxpacket/src/main.rs [changed mode: 0755->0644]

old mode 100755 (executable)
new mode 100644 (file)
index e9719ee..294bbc3
@@ -471,6 +471,7 @@ dependencies = [
  "log",
  "m17app",
  "m17core",
+ "rubato",
  "thiserror 2.0.11",
 ]
 
@@ -616,6 +617,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -737,6 +747,16 @@ version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
+[[package]]
+name = "rubato"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "rustc-hash"
 version = "1.1.0"
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 2a701de676681aaefb4e6be74411affb18e432c1..239472505e5c88ee5edef75329424880ea1e2866 100644 (file)
@@ -79,7 +79,7 @@ impl M17Address {
         Ok(Self(Address::Callsign(Callsign(address))))
     }
 
-    pub(crate) fn address(&self) -> &Address {
+    pub fn address(&self) -> &Address {
         &self.0
     }
 }
old mode 100755 (executable)
new mode 100644 (file)
index 4b8014c..29f8323
@@ -19,4 +19,5 @@ hound = "3.5.1"
 m17core = { version = "0.1", path = "../m17core" }
 m17app = { version = "0.1", path = "../m17app" }
 log = "0.4.22"
+rubato = { version = "0.16.2", "default-features" = false }
 thiserror = "2.0.11"
old mode 100755 (executable)
new mode 100644 (file)
index 879000d..693cb09
@@ -12,6 +12,9 @@ use m17app::error::AdapterError;
 use m17app::link_setup::LinkSetup;
 use m17app::link_setup::M17Address;
 use m17app::StreamFrame;
+use rubato::Resampler;
+use rubato::SincFixedIn;
+use rubato::SincInterpolationParameters;
 use std::collections::VecDeque;
 use std::fs::File;
 use std::io::Write;
@@ -25,6 +28,10 @@ use std::time::Duration;
 use std::time::Instant;
 use thiserror::Error;
 
+pub mod soundcards;
+
+/// Write one or more 8-byte chunks of 3200-bit Codec2 to a raw S16LE file
+/// and return the samples.
 pub fn decode_codec2<P: AsRef<Path>>(data: &[u8], out_path: P) -> Vec<i16> {
     let codec2 = Codec2::new(Codec2Mode::MODE_3200);
     let var_name = codec2;
@@ -35,8 +42,6 @@ pub fn decode_codec2<P: AsRef<Path>>(data: &[u8], out_path: P) -> Vec<i16> {
         codec.decode(&mut samples, &data[i * 8..((i + 1) * 8)]);
         all_samples.append(&mut samples);
     }
-
-    // dude this works
     let mut speech_out = File::create(out_path).unwrap();
     for b in &all_samples {
         speech_out.write_all(&b.to_le_bytes()).unwrap();
@@ -58,8 +63,8 @@ impl Codec2Adapter {
                 out_buf: VecDeque::new(),
                 codec2: Codec2::new(Codec2Mode::MODE_3200),
                 end_tx: None,
+                resampler: None,
             })),
-            // TODO: this doesn't work on rpi. Use default_output_device() by default
             output_card: None,
         }
     }
@@ -81,6 +86,7 @@ struct AdapterState {
     out_buf: VecDeque<i16>,
     codec2: Codec2,
     end_tx: Option<Sender<()>>,
+    resampler: Option<SincFixedIn<f32>>,
 }
 
 impl StreamAdapter for Codec2Adapter {
@@ -94,7 +100,21 @@ impl StreamAdapter for Codec2Adapter {
         std::thread::spawn(move || stream_thread(end_rx, setup_tx, state, output_card));
         self.state.lock().unwrap().end_tx = Some(end_tx);
         // Propagate any errors arising in the thread
-        setup_rx.recv()?
+        let sample_rate = setup_rx.recv()??;
+        debug!("selected codec2 output sample rate {sample_rate}");
+        if sample_rate != 8000 {
+            let params = SincInterpolationParameters {
+                sinc_len: 256,
+                f_cutoff: 0.95,
+                oversampling_factor: 256,
+                interpolation: rubato::SincInterpolationType::Cubic,
+                window: rubato::WindowFunction::BlackmanHarris2,
+            };
+            // TODO: fix unwrap
+            self.state.lock().unwrap().resampler =
+                Some(SincFixedIn::new(sample_rate as f64 / 8000f64, 1.0, params, 160, 1).unwrap());
+        }
+        Ok(())
     }
 
     fn close(&self) -> Result<(), AdapterError> {
@@ -116,12 +136,21 @@ impl StreamAdapter for Codec2Adapter {
     fn stream_data(&self, _frame_number: u16, _is_final: bool, data: Arc<[u8; 16]>) {
         let mut state = self.state.lock().unwrap();
         for encoded in data.chunks(8) {
-            if state.out_buf.len() < 1024 {
+            if state.out_buf.len() < 8192 {
                 let mut samples = [i16::EQUILIBRIUM; 160]; // while assuming 3200
                 state.codec2.decode(&mut samples, encoded);
-                // TODO: maybe get rid of VecDeque so we can decode directly into ring buffer?
-                for s in samples {
-                    state.out_buf.push_back(s);
+                if let Some(resampler) = state.resampler.as_mut() {
+                    let samples_f: Vec<f32> =
+                        samples.iter().map(|s| *s as f32 / 16384.0f32).collect();
+                    let res = resampler.process(&vec![samples_f], None).unwrap();
+                    for s in &res[0] {
+                        state.out_buf.push_back((s * 16383.0f32) as i16);
+                    }
+                } else {
+                    // TODO: maybe get rid of VecDeque so we can decode directly into ring buffer?
+                    for s in samples {
+                        state.out_buf.push_back(s);
+                    }
                 }
             } else {
                 debug!("out_buf overflow");
@@ -130,17 +159,17 @@ impl StreamAdapter for Codec2Adapter {
     }
 }
 
-fn output_cb(data: &mut [i16], state: &Mutex<AdapterState>) {
+fn output_cb(data: &mut [i16], state: &Mutex<AdapterState>, channels: u16) {
     let mut state = state.lock().unwrap();
-    for d in data {
-        *d = state.out_buf.pop_front().unwrap_or(i16::EQUILIBRIUM);
+    for d in data.chunks_mut(channels as usize) {
+        d.fill(state.out_buf.pop_front().unwrap_or(i16::EQUILIBRIUM));
     }
 }
 
 /// Create and manage the stream from a dedicated thread since it's `!Send`
 fn stream_thread(
     end: Receiver<()>,
-    setup_tx: Sender<Result<(), AdapterError>>,
+    setup_tx: Sender<Result<u32, AdapterError>>,
     state: Arc<Mutex<AdapterState>>,
     output_card: Option<String>,
 ) {
@@ -177,10 +206,9 @@ fn stream_thread(
             return;
         }
     };
-    // TODO: channels == 1 doesn't work on a Raspberry Pi
-    // make this configurable and support interleaving LRLR stereo samples if using 2 channels
-    let config = match configs.find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
-    {
+    let config = match configs.find(|c| {
+        (c.channels() == 1 || c.channels() == 2) && c.sample_format() == SampleFormat::I16
+    }) {
         Some(c) => c,
         None => {
             let _ = setup_tx.send(Err(
@@ -190,11 +218,19 @@ fn stream_thread(
         }
     };
 
-    let config = config.with_sample_rate(SampleRate(8000));
+    let target_sample_rate =
+        if config.min_sample_rate().0 <= 8000 && config.max_sample_rate().0 >= 8000 {
+            8000
+        } else {
+            config.max_sample_rate().0
+        };
+    let channels = config.channels();
+
+    let config = config.with_sample_rate(SampleRate(target_sample_rate));
     let stream = match device.build_output_stream(
         &config.into(),
         move |data: &mut [i16], _info: &cpal::OutputCallbackInfo| {
-            output_cb(data, &state);
+            output_cb(data, &state, channels);
         },
         |e| {
             // trigger end_tx here? always more edge cases
@@ -219,7 +255,7 @@ fn stream_thread(
             return;
         }
     }
-    let _ = setup_tx.send(Ok(()));
+    let _ = setup_tx.send(Ok(target_sample_rate));
     let _ = end.recv();
     // it seems concrete impls of Stream have a Drop implementation that will handle termination
 }
diff --git a/m17codec2/src/soundcards.rs b/m17codec2/src/soundcards.rs
new file mode 100644 (file)
index 0000000..24cff0a
--- /dev/null
@@ -0,0 +1,61 @@
+//! Utilities for selecting suitable sound cards.
+
+use cpal::{
+    traits::{DeviceTrait, HostTrait},
+    SampleFormat,
+};
+
+/// List sound cards supported for audio output.
+///
+/// M17RT will handle any card with 1 or 2 channels and 16-bit output.
+pub fn supported_output_cards() -> Vec<String> {
+    let mut out = vec![];
+    let host = cpal::default_host();
+    let Ok(output_devices) = host.output_devices() else {
+        return out;
+    };
+    for d in output_devices {
+        let Ok(mut configs) = d.supported_output_configs() else {
+            continue;
+        };
+        if configs.any(|config| {
+            (config.channels() == 1 || config.channels() == 2)
+                && config.sample_format() == SampleFormat::I16
+        }) {
+            let Ok(name) = d.name() else {
+                continue;
+            };
+            out.push(name);
+        }
+    }
+    out.sort();
+    out
+}
+
+/// List sound cards supported for audio input.
+///
+///
+/// M17RT will handle any card with 1 or 2 channels and 16-bit output.
+pub fn supported_input_cards() -> Vec<String> {
+    let mut out = vec![];
+    let host = cpal::default_host();
+    let Ok(input_devices) = host.input_devices() else {
+        return out;
+    };
+    for d in input_devices {
+        let Ok(mut configs) = d.supported_input_configs() else {
+            continue;
+        };
+        if configs.any(|config| {
+            (config.channels() == 1 || config.channels() == 2)
+                && config.sample_format() == SampleFormat::I16
+        }) {
+            let Ok(name) = d.name() else {
+                continue;
+            };
+            out.push(name);
+        }
+    }
+    out.sort();
+    out
+}
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
diff --git a/m17core/src/reflector.rs b/m17core/src/reflector.rs
deleted file mode 100644 (file)
index 657d864..0000000
+++ /dev/null
@@ -1,549 +0,0 @@
-// Based on https://github.com/n7tae/mrefd/blob/master/Packet-Description.md
-// and the main M17 specification
-
-use crate::address::Address;
-use crate::protocol::LsfFrame;
-
-macro_rules! define_message {
-    ($t:tt, $sz:tt, $min_sz:tt, $magic:tt) => {
-        pub struct $t(pub [u8; $sz], pub usize);
-
-        impl $t {
-            pub fn new() -> Self {
-                let mut bytes = [0u8; $sz];
-                bytes[0..4].copy_from_slice($magic);
-                Self(bytes, $sz)
-            }
-
-            #[allow(clippy::double_comparisons)] // occurs in some macro invocations
-            #[allow(clippy::manual_range_contains)] // way more readable, good grief
-            pub fn from_bytes(b: &[u8]) -> Option<Self> {
-                let len = b.len();
-                if len > $sz || len < $min_sz {
-                    return None;
-                }
-                let mut s = Self([0; $sz], len);
-                s.0[0..len].copy_from_slice(b);
-                if !s.verify_integrity() {
-                    return None;
-                }
-                Some(s)
-            }
-
-            pub fn as_bytes(&self) -> &[u8] {
-                &self.0[0..self.1]
-            }
-        }
-
-        impl Default for $t {
-            fn default() -> Self {
-                Self::new()
-            }
-        }
-    };
-}
-
-macro_rules! impl_stream_id {
-    ($t:ty, $from:tt) => {
-        impl $t {
-            pub fn stream_id(&self) -> u16 {
-                u16::from_be_bytes([self.0[$from], self.0[$from + 1]])
-            }
-
-            pub fn set_stream_id(&mut self, id: u16) {
-                let bytes = id.to_be_bytes();
-                self.0[$from] = bytes[0];
-                self.0[$from + 1] = bytes[1];
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_link_setup {
-    ($t:ty, $from:tt) => {
-        impl $t {
-            pub fn link_setup_frame(&self) -> LsfFrame {
-                let mut frame = LsfFrame([0; 30]);
-                frame.0[0..28].copy_from_slice(&self.0[$from..($from + 28)]);
-                frame.recalculate_crc();
-                frame
-            }
-
-            pub fn set_link_setup_frame(&mut self, lsf: &LsfFrame) {
-                self.0[$from..($from + 28)].copy_from_slice(&lsf.0[0..28]);
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_link_setup_frame {
-    ($t:ty, $from:tt) => {
-        impl $t {
-            pub fn link_setup_frame(&self) -> LsfFrame {
-                let mut frame = LsfFrame([0; 30]);
-                frame.0[..].copy_from_slice(&self.0[$from..($from + 30)]);
-                frame
-            }
-
-            pub fn set_link_setup_frame(&mut self, lsf: &LsfFrame) {
-                debug_assert_eq!(lsf.check_crc(), 0);
-                self.0[$from..($from + 30)].copy_from_slice(&lsf.0);
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_frame_number {
-    ($t:ty, $from:tt) => {
-        impl $t {
-            pub fn frame_number(&self) -> u16 {
-                let frame_num = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]);
-                frame_num & 0x7fff
-            }
-
-            pub fn is_end_of_stream(&self) -> bool {
-                let frame_num = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]);
-                (frame_num & 0x8000) > 0
-            }
-
-            pub fn set_frame_number(&mut self, number: u16) {
-                let existing_eos = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]) & 0x8000;
-                let new = (existing_eos | (number & 0x7fff)).to_be_bytes();
-                self.0[$from] = new[0];
-                self.0[$from + 1] = new[1];
-                self.recalculate_crc();
-            }
-
-            pub fn set_end_of_stream(&mut self, eos: bool) {
-                let existing_fn = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]) & 0x7fff;
-                let new = (existing_fn | (if eos { 0x8000 } else { 0 })).to_be_bytes();
-                self.0[$from] = new[0];
-                self.0[$from + 1] = new[1];
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_payload {
-    ($t:ty, $from:tt, $to:tt) => {
-        impl $t {
-            pub fn payload(&self) -> &[u8] {
-                &self.0[$from..$to]
-            }
-
-            pub fn set_payload(&mut self, bytes: &[u8]) {
-                self.0[$from..$to].copy_from_slice(bytes);
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_modules {
-    ($t:ty, $from:tt, $to:tt) => {
-        impl $t {
-            pub fn modules(&self) -> ModulesIterator {
-                ModulesIterator::new(&self.0[$from..$to])
-            }
-
-            pub fn set_modules(&mut self, list: &str) {
-                debug_assert!(list.len() < 27);
-                let mut idx = $from;
-                for m in list.chars() {
-                    self.0[idx] = m as u8;
-                    idx += 1;
-                }
-                self.0[idx] = 0;
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_module {
-    ($t:ty, $at:tt) => {
-        impl $t {
-            pub fn module(&self) -> char {
-                self.0[$at] as char
-            }
-
-            pub fn set_module(&mut self, m: char) {
-                self.0[$at] = m as u8;
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_address {
-    ($t:ty, $from:tt) => {
-        impl $t {
-            pub fn address(&self) -> Address {
-                crate::address::decode_address(self.0[$from..($from + 6)].try_into().unwrap())
-            }
-
-            pub fn set_address(&mut self, address: Address) {
-                let encoded = crate::address::encode_address(&address);
-                self.0[$from..($from + 6)].copy_from_slice(&encoded);
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-macro_rules! impl_trailing_crc_verify {
-    ($t:ty) => {
-        impl $t {
-            pub fn verify_integrity(&self) -> bool {
-                crate::crc::m17_crc(&self.0) == 0
-            }
-
-            pub fn recalculate_crc(&mut self) {
-                let len = self.0.len();
-                let start_crc = crate::crc::m17_crc(&self.0[0..(len - 2)]).to_be_bytes();
-                self.0[len - 2] = start_crc[0];
-                self.0[len - 1] = start_crc[1];
-                debug_assert!(self.verify_integrity());
-            }
-        }
-    };
-}
-
-macro_rules! impl_internal_crc {
-    ($t:ty, $from:tt, $to:tt) => {
-        impl $t {
-            pub fn verify_integrity(&self) -> bool {
-                crate::crc::m17_crc(&self.0[$from..$to]) == 0
-            }
-
-            pub fn recalculate_crc(&mut self) {
-                // assume the last two bytes of the range are the CRC
-                let start_crc = crate::crc::m17_crc(&self.0[$from..($to - 2)]).to_be_bytes();
-                self.0[$to - 2] = start_crc[0];
-                self.0[$to - 1] = start_crc[1];
-                debug_assert!(self.verify_integrity());
-            }
-        }
-    };
-}
-
-macro_rules! no_crc {
-    ($t:ty) => {
-        impl $t {
-            pub fn verify_integrity(&self) -> bool {
-                true
-            }
-            pub fn recalculate_crc(&mut self) {}
-        }
-    };
-}
-
-macro_rules! impl_is_relayed {
-    ($t:ty) => {
-        impl $t {
-            pub fn is_relayed(&self) -> bool {
-                self.0[self.0.len() - 1] != 0
-            }
-
-            pub fn set_relayed(&mut self, relayed: bool) {
-                self.0[self.0.len() - 1] = if relayed { 1 } else { 0 };
-                self.recalculate_crc();
-            }
-        }
-    };
-}
-
-pub struct ModulesIterator<'a> {
-    modules: &'a [u8],
-    idx: usize,
-}
-
-impl<'a> ModulesIterator<'a> {
-    fn new(modules: &'a [u8]) -> Self {
-        Self { modules, idx: 0 }
-    }
-}
-
-impl Iterator for ModulesIterator<'_> {
-    type Item = char;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.idx < self.modules.len() {
-            if self.modules[self.idx] == 0 {
-                return None;
-            }
-            self.idx += 1;
-            return Some(self.modules[self.idx - 1] as char);
-        }
-        None
-    }
-}
-
-pub const MAGIC_VOICE: &[u8] = b"M17 ";
-pub const MAGIC_VOICE_HEADER: &[u8] = b"M17H";
-pub const MAGIC_VOICE_DATA: &[u8] = b"M17D";
-pub const MAGIC_PACKET: &[u8] = b"M17P";
-pub const MAGIC_ACKNOWLEDGE: &[u8] = b"ACKN";
-pub const MAGIC_CONNECT: &[u8] = b"CONN";
-pub const MAGIC_DISCONNECT: &[u8] = b"DISC";
-pub const MAGIC_LISTEN: &[u8] = b"LSTN";
-pub const MAGIC_NACK: &[u8] = b"NACK";
-pub const MAGIC_PING: &[u8] = b"PING";
-pub const MAGIC_PONG: &[u8] = b"PONG";
-
-/// Messages sent from a station/client to a reflector
-#[allow(clippy::large_enum_variant)]
-pub enum ClientMessage {
-    Voice(Voice),
-    VoiceHeader(VoiceHeader),
-    VoiceData(VoiceData),
-    Packet(Packet),
-    Pong(Pong),
-    Connect(Connect),
-    Listen(Listen),
-    Disconnect(Disconnect),
-}
-
-impl ClientMessage {
-    pub fn parse(bytes: &[u8]) -> Option<Self> {
-        if bytes.len() < 4 {
-            return None;
-        }
-        match &bytes[0..4] {
-            MAGIC_VOICE => Some(Self::Voice(Voice::from_bytes(bytes)?)),
-            MAGIC_VOICE_HEADER => Some(Self::VoiceHeader(VoiceHeader::from_bytes(bytes)?)),
-            MAGIC_VOICE_DATA => Some(Self::VoiceData(VoiceData::from_bytes(bytes)?)),
-            MAGIC_PACKET => Some(Self::Packet(Packet::from_bytes(bytes)?)),
-            MAGIC_PONG => Some(Self::Pong(Pong::from_bytes(bytes)?)),
-            MAGIC_CONNECT => Some(Self::Connect(Connect::from_bytes(bytes)?)),
-            MAGIC_LISTEN => Some(Self::Listen(Listen::from_bytes(bytes)?)),
-            MAGIC_DISCONNECT => Some(Self::Disconnect(Disconnect::from_bytes(bytes)?)),
-            _ => None,
-        }
-    }
-}
-
-/// Messages sent from a reflector to a station/client
-#[allow(clippy::large_enum_variant)]
-pub enum ServerMessage {
-    Voice(Voice),
-    VoiceHeader(VoiceHeader),
-    VoiceData(VoiceData),
-    Packet(Packet),
-    Ping(Ping),
-    DisconnectAcknowledge(DisconnectAcknowledge),
-    ForceDisconnect(ForceDisconnect),
-    ConnectAcknowledge(ConnectAcknowledge),
-    ConnectNack(ConnectNack),
-}
-
-impl ServerMessage {
-    pub fn parse(bytes: &[u8]) -> Option<Self> {
-        if bytes.len() < 4 {
-            return None;
-        }
-        match &bytes[0..4] {
-            MAGIC_VOICE => Some(Self::Voice(Voice::from_bytes(bytes)?)),
-            MAGIC_VOICE_HEADER => Some(Self::VoiceHeader(VoiceHeader::from_bytes(bytes)?)),
-            MAGIC_VOICE_DATA => Some(Self::VoiceData(VoiceData::from_bytes(bytes)?)),
-            MAGIC_PACKET => Some(Self::Packet(Packet::from_bytes(bytes)?)),
-            MAGIC_PING => Some(Self::Ping(Ping::from_bytes(bytes)?)),
-            MAGIC_DISCONNECT if bytes.len() == 4 => Some(Self::DisconnectAcknowledge(
-                DisconnectAcknowledge::from_bytes(bytes)?,
-            )),
-            MAGIC_DISCONNECT => Some(Self::ForceDisconnect(ForceDisconnect::from_bytes(bytes)?)),
-            MAGIC_ACKNOWLEDGE => Some(Self::ConnectAcknowledge(ConnectAcknowledge::from_bytes(
-                bytes,
-            )?)),
-            MAGIC_NACK => Some(Self::ConnectNack(ConnectNack::from_bytes(bytes)?)),
-            _ => None,
-        }
-    }
-}
-
-/// Messages sent and received between reflectors
-#[allow(clippy::large_enum_variant)]
-pub enum InterlinkMessage {
-    VoiceInterlink(VoiceInterlink),
-    VoiceHeaderInterlink(VoiceHeaderInterlink),
-    VoiceDataInterlink(VoiceDataInterlink),
-    PacketInterlink(PacketInterlink),
-    Ping(Ping),
-    ConnectInterlink(ConnectInterlink),
-    ConnectInterlinkAcknowledge(ConnectInterlinkAcknowledge),
-    ConnectNack(ConnectNack),
-    DisconnectInterlink(DisconnectInterlink),
-}
-
-impl InterlinkMessage {
-    pub fn parse(bytes: &[u8]) -> Option<Self> {
-        if bytes.len() < 4 {
-            return None;
-        }
-        match &bytes[0..4] {
-            MAGIC_VOICE => Some(Self::VoiceInterlink(VoiceInterlink::from_bytes(bytes)?)),
-            MAGIC_VOICE_HEADER => Some(Self::VoiceHeaderInterlink(
-                VoiceHeaderInterlink::from_bytes(bytes)?,
-            )),
-            MAGIC_VOICE_DATA => Some(Self::VoiceDataInterlink(VoiceDataInterlink::from_bytes(
-                bytes,
-            )?)),
-            MAGIC_PACKET => Some(Self::PacketInterlink(PacketInterlink::from_bytes(bytes)?)),
-            MAGIC_PING => Some(Self::Ping(Ping::from_bytes(bytes)?)),
-            MAGIC_CONNECT => Some(Self::ConnectInterlink(ConnectInterlink::from_bytes(bytes)?)),
-            MAGIC_ACKNOWLEDGE => Some(Self::ConnectInterlinkAcknowledge(
-                ConnectInterlinkAcknowledge::from_bytes(bytes)?,
-            )),
-            MAGIC_NACK => Some(Self::ConnectNack(ConnectNack::from_bytes(bytes)?)),
-            MAGIC_DISCONNECT => Some(Self::DisconnectInterlink(DisconnectInterlink::from_bytes(
-                bytes,
-            )?)),
-            _ => None,
-        }
-    }
-}
-
-define_message!(Voice, 54, 54, MAGIC_VOICE);
-impl_stream_id!(Voice, 4);
-impl_link_setup!(Voice, 6);
-impl_frame_number!(Voice, 34);
-impl_payload!(Voice, 36, 52);
-impl_trailing_crc_verify!(Voice);
-
-define_message!(VoiceHeader, 36, 36, MAGIC_VOICE_HEADER);
-impl_stream_id!(VoiceHeader, 4);
-impl_link_setup!(VoiceHeader, 6);
-impl_trailing_crc_verify!(VoiceHeader);
-
-define_message!(VoiceData, 26, 26, MAGIC_VOICE_DATA);
-impl_stream_id!(VoiceData, 4);
-impl_frame_number!(VoiceData, 6);
-impl_payload!(VoiceData, 8, 24);
-impl_trailing_crc_verify!(VoiceData);
-
-define_message!(Packet, 859, 38, MAGIC_PACKET);
-impl_link_setup_frame!(Packet, 4);
-
-impl Packet {
-    pub fn payload(&self) -> &[u8] {
-        &self.0[34..self.1]
-    }
-
-    pub fn set_payload(&mut self, bytes: &[u8]) {
-        let end = 34 + bytes.len();
-        self.0[34..end].copy_from_slice(bytes);
-        self.1 = end;
-    }
-
-    pub fn verify_integrity(&self) -> bool {
-        self.link_setup_frame().check_crc() == 0
-            && self.payload().len() >= 4
-            && crate::crc::m17_crc(self.payload()) == 0
-    }
-
-    pub fn recalculate_crc(&mut self) {
-        // LSF and payload should be confirmed valid before construction
-    }
-}
-
-define_message!(Pong, 10, 10, MAGIC_PONG);
-impl_address!(Pong, 4);
-no_crc!(Pong);
-
-define_message!(Connect, 11, 11, MAGIC_CONNECT);
-impl_address!(Connect, 4);
-impl_module!(Connect, 10);
-no_crc!(Connect);
-
-define_message!(Listen, 11, 11, MAGIC_LISTEN);
-impl_address!(Listen, 4);
-impl_module!(Listen, 10);
-no_crc!(Listen);
-
-define_message!(Disconnect, 10, 10, MAGIC_DISCONNECT);
-impl_address!(Disconnect, 4);
-no_crc!(Disconnect);
-
-define_message!(Ping, 10, 10, MAGIC_PING);
-impl_address!(Ping, 4);
-no_crc!(Ping);
-
-define_message!(DisconnectAcknowledge, 4, 4, MAGIC_DISCONNECT);
-no_crc!(DisconnectAcknowledge);
-
-define_message!(ForceDisconnect, 10, 10, MAGIC_DISCONNECT);
-impl_address!(ForceDisconnect, 4);
-no_crc!(ForceDisconnect);
-
-define_message!(ConnectAcknowledge, 4, 4, MAGIC_ACKNOWLEDGE);
-no_crc!(ConnectAcknowledge);
-
-define_message!(ConnectNack, 4, 4, MAGIC_NACK);
-no_crc!(ConnectNack);
-
-define_message!(VoiceInterlink, 55, 55, MAGIC_VOICE);
-impl_stream_id!(VoiceInterlink, 4);
-impl_link_setup!(VoiceInterlink, 6);
-impl_frame_number!(VoiceInterlink, 34);
-impl_payload!(VoiceInterlink, 36, 52);
-impl_internal_crc!(VoiceInterlink, 0, 54);
-impl_is_relayed!(VoiceInterlink);
-
-define_message!(VoiceHeaderInterlink, 37, 37, MAGIC_VOICE_HEADER);
-impl_stream_id!(VoiceHeaderInterlink, 4);
-impl_link_setup!(VoiceHeaderInterlink, 6);
-impl_internal_crc!(VoiceHeaderInterlink, 0, 36);
-impl_is_relayed!(VoiceHeaderInterlink);
-
-define_message!(VoiceDataInterlink, 27, 27, MAGIC_VOICE_DATA);
-impl_stream_id!(VoiceDataInterlink, 4);
-impl_frame_number!(VoiceDataInterlink, 6);
-impl_payload!(VoiceDataInterlink, 8, 24);
-impl_internal_crc!(VoiceDataInterlink, 0, 24);
-impl_is_relayed!(VoiceDataInterlink);
-
-define_message!(PacketInterlink, 860, 39, MAGIC_PACKET);
-impl_link_setup_frame!(PacketInterlink, 4);
-impl_is_relayed!(PacketInterlink);
-
-impl PacketInterlink {
-    pub fn payload(&self) -> &[u8] {
-        &self.0[34..(self.1 - 1)]
-    }
-
-    pub fn set_payload(&mut self, bytes: &[u8]) {
-        let is_relayed = self.is_relayed();
-        let end = 34 + bytes.len();
-        self.0[34..end].copy_from_slice(bytes);
-        self.1 = end + 1;
-        self.set_relayed(is_relayed);
-    }
-
-    pub fn verify_integrity(&self) -> bool {
-        self.link_setup_frame().check_crc() == 0
-            && self.payload().len() >= 4
-            && crate::crc::m17_crc(self.payload()) == 0
-    }
-
-    pub fn recalculate_crc(&mut self) {
-        // LSF and payload should be confirmed valid before construction
-    }
-}
-
-define_message!(ConnectInterlink, 37, 37, MAGIC_CONNECT);
-impl_address!(ConnectInterlink, 4);
-impl_modules!(ConnectInterlink, 10, 37);
-no_crc!(ConnectInterlink);
-
-define_message!(ConnectInterlinkAcknowledge, 37, 37, MAGIC_ACKNOWLEDGE);
-impl_address!(ConnectInterlinkAcknowledge, 4);
-impl_modules!(ConnectInterlinkAcknowledge, 10, 37);
-no_crc!(ConnectInterlinkAcknowledge);
-
-define_message!(DisconnectInterlink, 10, 10, MAGIC_DISCONNECT);
-impl_address!(DisconnectInterlink, 4);
-no_crc!(DisconnectInterlink);
diff --git a/m17core/src/reflector/convert.rs b/m17core/src/reflector/convert.rs
new file mode 100644 (file)
index 0000000..8e5a96d
--- /dev/null
@@ -0,0 +1,58 @@
+//! Utilities for converting streams between UDP and RF representations
+
+use crate::protocol::{LsfFrame, StreamFrame};
+
+use super::packet::Voice;
+
+/// Accepts `Voice` packets from a reflector and turns them into LSF and Stream frames.
+///
+/// This is the format required for the voice data to cross the KISS protocol boundary.
+pub struct VoiceToRf {
+    /// Link Setup most recently acquired
+    lsf: Option<LsfFrame>,
+    /// Which LICH part we are going to emit next, 0-5
+    lich_cnt: usize,
+}
+
+impl VoiceToRf {
+    pub fn new() -> Self {
+        Self {
+            lsf: None,
+            lich_cnt: 0,
+        }
+    }
+
+    /// For a Voice packet received from a reflector, return the frames that would be transmitted
+    /// on RF, including by reconstructing the LICH parts of the stream frame.
+    ///
+    /// If this is the start of a new or different stream transmission, this returns the Link Setup
+    /// Frame which comes first, then the first associated Stream frame.
+    ///
+    /// If this is a continuation of a transmission matching the previous LSF, then it returns only
+    /// the Stream frame.
+    pub fn next(&mut self, voice: &Voice) -> (Option<LsfFrame>, StreamFrame) {
+        let this_lsf = voice.link_setup_frame();
+        let emit_lsf = if Some(&this_lsf) != self.lsf.as_ref() {
+            self.lsf = Some(this_lsf.clone());
+            self.lich_cnt = 0;
+            true
+        } else {
+            false
+        };
+        let lsf = self.lsf.as_ref().unwrap();
+        let stream = StreamFrame {
+            lich_idx: self.lich_cnt as u8,
+            lich_part: (&lsf.0[self.lich_cnt * 5..(self.lich_cnt + 1) * 5])
+                .try_into()
+                .unwrap(),
+            frame_number: voice.frame_number(),
+            end_of_stream: voice.is_end_of_stream(),
+            stream_data: voice.payload().try_into().unwrap(),
+        };
+        let lsf = if emit_lsf { self.lsf.clone() } else { None };
+        if voice.is_end_of_stream() {
+            self.lsf = None;
+        }
+        (lsf, stream)
+    }
+}
diff --git a/m17core/src/reflector/mod.rs b/m17core/src/reflector/mod.rs
new file mode 100644 (file)
index 0000000..057bae0
--- /dev/null
@@ -0,0 +1,5 @@
+// Based on https://github.com/n7tae/mrefd/blob/master/Packet-Description.md
+// and the main M17 specification
+
+pub mod convert;
+pub mod packet;
diff --git a/m17core/src/reflector/packet.rs b/m17core/src/reflector/packet.rs
new file mode 100644 (file)
index 0000000..3f746d6
--- /dev/null
@@ -0,0 +1,548 @@
+//! UDP datagrams and binary encoding/decoding for client-reflector and reflector-reflector communication.
+
+use crate::address::Address;
+use crate::protocol::LsfFrame;
+
+macro_rules! define_message {
+    ($t:tt, $sz:tt, $min_sz:tt, $magic:tt) => {
+        pub struct $t(pub [u8; $sz], pub usize);
+
+        impl $t {
+            pub fn new() -> Self {
+                let mut bytes = [0u8; $sz];
+                bytes[0..4].copy_from_slice($magic);
+                Self(bytes, $sz)
+            }
+
+            #[allow(clippy::double_comparisons)] // occurs in some macro invocations
+            #[allow(clippy::manual_range_contains)] // way more readable, good grief
+            pub fn from_bytes(b: &[u8]) -> Option<Self> {
+                let len = b.len();
+                if len > $sz || len < $min_sz {
+                    return None;
+                }
+                let mut s = Self([0; $sz], len);
+                s.0[0..len].copy_from_slice(b);
+                if !s.verify_integrity() {
+                    return None;
+                }
+                Some(s)
+            }
+
+            pub fn as_bytes(&self) -> &[u8] {
+                &self.0[0..self.1]
+            }
+        }
+
+        impl Default for $t {
+            fn default() -> Self {
+                Self::new()
+            }
+        }
+    };
+}
+
+macro_rules! impl_stream_id {
+    ($t:ty, $from:tt) => {
+        impl $t {
+            pub fn stream_id(&self) -> u16 {
+                u16::from_be_bytes([self.0[$from], self.0[$from + 1]])
+            }
+
+            pub fn set_stream_id(&mut self, id: u16) {
+                let bytes = id.to_be_bytes();
+                self.0[$from] = bytes[0];
+                self.0[$from + 1] = bytes[1];
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_link_setup {
+    ($t:ty, $from:tt) => {
+        impl $t {
+            pub fn link_setup_frame(&self) -> LsfFrame {
+                let mut frame = LsfFrame([0; 30]);
+                frame.0[0..28].copy_from_slice(&self.0[$from..($from + 28)]);
+                frame.recalculate_crc();
+                frame
+            }
+
+            pub fn set_link_setup_frame(&mut self, lsf: &LsfFrame) {
+                self.0[$from..($from + 28)].copy_from_slice(&lsf.0[0..28]);
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_link_setup_frame {
+    ($t:ty, $from:tt) => {
+        impl $t {
+            pub fn link_setup_frame(&self) -> LsfFrame {
+                let mut frame = LsfFrame([0; 30]);
+                frame.0[..].copy_from_slice(&self.0[$from..($from + 30)]);
+                frame
+            }
+
+            pub fn set_link_setup_frame(&mut self, lsf: &LsfFrame) {
+                debug_assert_eq!(lsf.check_crc(), 0);
+                self.0[$from..($from + 30)].copy_from_slice(&lsf.0);
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_frame_number {
+    ($t:ty, $from:tt) => {
+        impl $t {
+            pub fn frame_number(&self) -> u16 {
+                let frame_num = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]);
+                frame_num & 0x7fff
+            }
+
+            pub fn is_end_of_stream(&self) -> bool {
+                let frame_num = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]);
+                (frame_num & 0x8000) > 0
+            }
+
+            pub fn set_frame_number(&mut self, number: u16) {
+                let existing_eos = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]) & 0x8000;
+                let new = (existing_eos | (number & 0x7fff)).to_be_bytes();
+                self.0[$from] = new[0];
+                self.0[$from + 1] = new[1];
+                self.recalculate_crc();
+            }
+
+            pub fn set_end_of_stream(&mut self, eos: bool) {
+                let existing_fn = u16::from_be_bytes([self.0[$from], self.0[$from + 1]]) & 0x7fff;
+                let new = (existing_fn | (if eos { 0x8000 } else { 0 })).to_be_bytes();
+                self.0[$from] = new[0];
+                self.0[$from + 1] = new[1];
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_payload {
+    ($t:ty, $from:tt, $to:tt) => {
+        impl $t {
+            pub fn payload(&self) -> &[u8] {
+                &self.0[$from..$to]
+            }
+
+            pub fn set_payload(&mut self, bytes: &[u8]) {
+                self.0[$from..$to].copy_from_slice(bytes);
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_modules {
+    ($t:ty, $from:tt, $to:tt) => {
+        impl $t {
+            pub fn modules(&self) -> ModulesIterator {
+                ModulesIterator::new(&self.0[$from..$to])
+            }
+
+            pub fn set_modules(&mut self, list: &str) {
+                debug_assert!(list.len() < 27);
+                let mut idx = $from;
+                for m in list.chars() {
+                    self.0[idx] = m as u8;
+                    idx += 1;
+                }
+                self.0[idx] = 0;
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_module {
+    ($t:ty, $at:tt) => {
+        impl $t {
+            pub fn module(&self) -> char {
+                self.0[$at] as char
+            }
+
+            pub fn set_module(&mut self, m: char) {
+                self.0[$at] = m as u8;
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_address {
+    ($t:ty, $from:tt) => {
+        impl $t {
+            pub fn address(&self) -> Address {
+                crate::address::decode_address(self.0[$from..($from + 6)].try_into().unwrap())
+            }
+
+            pub fn set_address(&mut self, address: Address) {
+                let encoded = crate::address::encode_address(&address);
+                self.0[$from..($from + 6)].copy_from_slice(&encoded);
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+macro_rules! impl_trailing_crc_verify {
+    ($t:ty) => {
+        impl $t {
+            pub fn verify_integrity(&self) -> bool {
+                crate::crc::m17_crc(&self.0) == 0
+            }
+
+            pub fn recalculate_crc(&mut self) {
+                let len = self.0.len();
+                let start_crc = crate::crc::m17_crc(&self.0[0..(len - 2)]).to_be_bytes();
+                self.0[len - 2] = start_crc[0];
+                self.0[len - 1] = start_crc[1];
+                debug_assert!(self.verify_integrity());
+            }
+        }
+    };
+}
+
+macro_rules! impl_internal_crc {
+    ($t:ty, $from:tt, $to:tt) => {
+        impl $t {
+            pub fn verify_integrity(&self) -> bool {
+                crate::crc::m17_crc(&self.0[$from..$to]) == 0
+            }
+
+            pub fn recalculate_crc(&mut self) {
+                // assume the last two bytes of the range are the CRC
+                let start_crc = crate::crc::m17_crc(&self.0[$from..($to - 2)]).to_be_bytes();
+                self.0[$to - 2] = start_crc[0];
+                self.0[$to - 1] = start_crc[1];
+                debug_assert!(self.verify_integrity());
+            }
+        }
+    };
+}
+
+macro_rules! no_crc {
+    ($t:ty) => {
+        impl $t {
+            pub fn verify_integrity(&self) -> bool {
+                true
+            }
+            pub fn recalculate_crc(&mut self) {}
+        }
+    };
+}
+
+macro_rules! impl_is_relayed {
+    ($t:ty) => {
+        impl $t {
+            pub fn is_relayed(&self) -> bool {
+                self.0[self.0.len() - 1] != 0
+            }
+
+            pub fn set_relayed(&mut self, relayed: bool) {
+                self.0[self.0.len() - 1] = if relayed { 1 } else { 0 };
+                self.recalculate_crc();
+            }
+        }
+    };
+}
+
+pub struct ModulesIterator<'a> {
+    modules: &'a [u8],
+    idx: usize,
+}
+
+impl<'a> ModulesIterator<'a> {
+    fn new(modules: &'a [u8]) -> Self {
+        Self { modules, idx: 0 }
+    }
+}
+
+impl Iterator for ModulesIterator<'_> {
+    type Item = char;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.idx < self.modules.len() {
+            if self.modules[self.idx] == 0 {
+                return None;
+            }
+            self.idx += 1;
+            return Some(self.modules[self.idx - 1] as char);
+        }
+        None
+    }
+}
+
+pub const MAGIC_VOICE: &[u8] = b"M17 ";
+pub const MAGIC_VOICE_HEADER: &[u8] = b"M17H";
+pub const MAGIC_VOICE_DATA: &[u8] = b"M17D";
+pub const MAGIC_PACKET: &[u8] = b"M17P";
+pub const MAGIC_ACKNOWLEDGE: &[u8] = b"ACKN";
+pub const MAGIC_CONNECT: &[u8] = b"CONN";
+pub const MAGIC_DISCONNECT: &[u8] = b"DISC";
+pub const MAGIC_LISTEN: &[u8] = b"LSTN";
+pub const MAGIC_NACK: &[u8] = b"NACK";
+pub const MAGIC_PING: &[u8] = b"PING";
+pub const MAGIC_PONG: &[u8] = b"PONG";
+
+/// Messages sent from a station/client to a reflector
+#[allow(clippy::large_enum_variant)]
+pub enum ClientMessage {
+    Voice(Voice),
+    VoiceHeader(VoiceHeader),
+    VoiceData(VoiceData),
+    Packet(Packet),
+    Pong(Pong),
+    Connect(Connect),
+    Listen(Listen),
+    Disconnect(Disconnect),
+}
+
+impl ClientMessage {
+    pub fn parse(bytes: &[u8]) -> Option<Self> {
+        if bytes.len() < 4 {
+            return None;
+        }
+        match &bytes[0..4] {
+            MAGIC_VOICE => Some(Self::Voice(Voice::from_bytes(bytes)?)),
+            MAGIC_VOICE_HEADER => Some(Self::VoiceHeader(VoiceHeader::from_bytes(bytes)?)),
+            MAGIC_VOICE_DATA => Some(Self::VoiceData(VoiceData::from_bytes(bytes)?)),
+            MAGIC_PACKET => Some(Self::Packet(Packet::from_bytes(bytes)?)),
+            MAGIC_PONG => Some(Self::Pong(Pong::from_bytes(bytes)?)),
+            MAGIC_CONNECT => Some(Self::Connect(Connect::from_bytes(bytes)?)),
+            MAGIC_LISTEN => Some(Self::Listen(Listen::from_bytes(bytes)?)),
+            MAGIC_DISCONNECT => Some(Self::Disconnect(Disconnect::from_bytes(bytes)?)),
+            _ => None,
+        }
+    }
+}
+
+/// Messages sent from a reflector to a station/client
+#[allow(clippy::large_enum_variant)]
+pub enum ServerMessage {
+    Voice(Voice),
+    VoiceHeader(VoiceHeader),
+    VoiceData(VoiceData),
+    Packet(Packet),
+    Ping(Ping),
+    DisconnectAcknowledge(DisconnectAcknowledge),
+    ForceDisconnect(ForceDisconnect),
+    ConnectAcknowledge(ConnectAcknowledge),
+    ConnectNack(ConnectNack),
+}
+
+impl ServerMessage {
+    pub fn parse(bytes: &[u8]) -> Option<Self> {
+        if bytes.len() < 4 {
+            return None;
+        }
+        match &bytes[0..4] {
+            MAGIC_VOICE => Some(Self::Voice(Voice::from_bytes(bytes)?)),
+            MAGIC_VOICE_HEADER => Some(Self::VoiceHeader(VoiceHeader::from_bytes(bytes)?)),
+            MAGIC_VOICE_DATA => Some(Self::VoiceData(VoiceData::from_bytes(bytes)?)),
+            MAGIC_PACKET => Some(Self::Packet(Packet::from_bytes(bytes)?)),
+            MAGIC_PING => Some(Self::Ping(Ping::from_bytes(bytes)?)),
+            MAGIC_DISCONNECT if bytes.len() == 4 => Some(Self::DisconnectAcknowledge(
+                DisconnectAcknowledge::from_bytes(bytes)?,
+            )),
+            MAGIC_DISCONNECT => Some(Self::ForceDisconnect(ForceDisconnect::from_bytes(bytes)?)),
+            MAGIC_ACKNOWLEDGE => Some(Self::ConnectAcknowledge(ConnectAcknowledge::from_bytes(
+                bytes,
+            )?)),
+            MAGIC_NACK => Some(Self::ConnectNack(ConnectNack::from_bytes(bytes)?)),
+            _ => None,
+        }
+    }
+}
+
+/// Messages sent and received between reflectors
+#[allow(clippy::large_enum_variant)]
+pub enum InterlinkMessage {
+    VoiceInterlink(VoiceInterlink),
+    VoiceHeaderInterlink(VoiceHeaderInterlink),
+    VoiceDataInterlink(VoiceDataInterlink),
+    PacketInterlink(PacketInterlink),
+    Ping(Ping),
+    ConnectInterlink(ConnectInterlink),
+    ConnectInterlinkAcknowledge(ConnectInterlinkAcknowledge),
+    ConnectNack(ConnectNack),
+    DisconnectInterlink(DisconnectInterlink),
+}
+
+impl InterlinkMessage {
+    pub fn parse(bytes: &[u8]) -> Option<Self> {
+        if bytes.len() < 4 {
+            return None;
+        }
+        match &bytes[0..4] {
+            MAGIC_VOICE => Some(Self::VoiceInterlink(VoiceInterlink::from_bytes(bytes)?)),
+            MAGIC_VOICE_HEADER => Some(Self::VoiceHeaderInterlink(
+                VoiceHeaderInterlink::from_bytes(bytes)?,
+            )),
+            MAGIC_VOICE_DATA => Some(Self::VoiceDataInterlink(VoiceDataInterlink::from_bytes(
+                bytes,
+            )?)),
+            MAGIC_PACKET => Some(Self::PacketInterlink(PacketInterlink::from_bytes(bytes)?)),
+            MAGIC_PING => Some(Self::Ping(Ping::from_bytes(bytes)?)),
+            MAGIC_CONNECT => Some(Self::ConnectInterlink(ConnectInterlink::from_bytes(bytes)?)),
+            MAGIC_ACKNOWLEDGE => Some(Self::ConnectInterlinkAcknowledge(
+                ConnectInterlinkAcknowledge::from_bytes(bytes)?,
+            )),
+            MAGIC_NACK => Some(Self::ConnectNack(ConnectNack::from_bytes(bytes)?)),
+            MAGIC_DISCONNECT => Some(Self::DisconnectInterlink(DisconnectInterlink::from_bytes(
+                bytes,
+            )?)),
+            _ => None,
+        }
+    }
+}
+
+define_message!(Voice, 54, 54, MAGIC_VOICE);
+impl_stream_id!(Voice, 4);
+impl_link_setup!(Voice, 6);
+impl_frame_number!(Voice, 34);
+impl_payload!(Voice, 36, 52);
+impl_trailing_crc_verify!(Voice);
+
+define_message!(VoiceHeader, 36, 36, MAGIC_VOICE_HEADER);
+impl_stream_id!(VoiceHeader, 4);
+impl_link_setup!(VoiceHeader, 6);
+impl_trailing_crc_verify!(VoiceHeader);
+
+define_message!(VoiceData, 26, 26, MAGIC_VOICE_DATA);
+impl_stream_id!(VoiceData, 4);
+impl_frame_number!(VoiceData, 6);
+impl_payload!(VoiceData, 8, 24);
+impl_trailing_crc_verify!(VoiceData);
+
+define_message!(Packet, 859, 38, MAGIC_PACKET);
+impl_link_setup_frame!(Packet, 4);
+
+impl Packet {
+    pub fn payload(&self) -> &[u8] {
+        &self.0[34..self.1]
+    }
+
+    pub fn set_payload(&mut self, bytes: &[u8]) {
+        let end = 34 + bytes.len();
+        self.0[34..end].copy_from_slice(bytes);
+        self.1 = end;
+    }
+
+    pub fn verify_integrity(&self) -> bool {
+        self.link_setup_frame().check_crc() == 0
+            && self.payload().len() >= 4
+            && crate::crc::m17_crc(self.payload()) == 0
+    }
+
+    pub fn recalculate_crc(&mut self) {
+        // LSF and payload should be confirmed valid before construction
+    }
+}
+
+define_message!(Pong, 10, 10, MAGIC_PONG);
+impl_address!(Pong, 4);
+no_crc!(Pong);
+
+define_message!(Connect, 11, 11, MAGIC_CONNECT);
+impl_address!(Connect, 4);
+impl_module!(Connect, 10);
+no_crc!(Connect);
+
+define_message!(Listen, 11, 11, MAGIC_LISTEN);
+impl_address!(Listen, 4);
+impl_module!(Listen, 10);
+no_crc!(Listen);
+
+define_message!(Disconnect, 10, 10, MAGIC_DISCONNECT);
+impl_address!(Disconnect, 4);
+no_crc!(Disconnect);
+
+define_message!(Ping, 10, 10, MAGIC_PING);
+impl_address!(Ping, 4);
+no_crc!(Ping);
+
+define_message!(DisconnectAcknowledge, 4, 4, MAGIC_DISCONNECT);
+no_crc!(DisconnectAcknowledge);
+
+define_message!(ForceDisconnect, 10, 10, MAGIC_DISCONNECT);
+impl_address!(ForceDisconnect, 4);
+no_crc!(ForceDisconnect);
+
+define_message!(ConnectAcknowledge, 4, 4, MAGIC_ACKNOWLEDGE);
+no_crc!(ConnectAcknowledge);
+
+define_message!(ConnectNack, 4, 4, MAGIC_NACK);
+no_crc!(ConnectNack);
+
+define_message!(VoiceInterlink, 55, 55, MAGIC_VOICE);
+impl_stream_id!(VoiceInterlink, 4);
+impl_link_setup!(VoiceInterlink, 6);
+impl_frame_number!(VoiceInterlink, 34);
+impl_payload!(VoiceInterlink, 36, 52);
+impl_internal_crc!(VoiceInterlink, 0, 54);
+impl_is_relayed!(VoiceInterlink);
+
+define_message!(VoiceHeaderInterlink, 37, 37, MAGIC_VOICE_HEADER);
+impl_stream_id!(VoiceHeaderInterlink, 4);
+impl_link_setup!(VoiceHeaderInterlink, 6);
+impl_internal_crc!(VoiceHeaderInterlink, 0, 36);
+impl_is_relayed!(VoiceHeaderInterlink);
+
+define_message!(VoiceDataInterlink, 27, 27, MAGIC_VOICE_DATA);
+impl_stream_id!(VoiceDataInterlink, 4);
+impl_frame_number!(VoiceDataInterlink, 6);
+impl_payload!(VoiceDataInterlink, 8, 24);
+impl_internal_crc!(VoiceDataInterlink, 0, 24);
+impl_is_relayed!(VoiceDataInterlink);
+
+define_message!(PacketInterlink, 860, 39, MAGIC_PACKET);
+impl_link_setup_frame!(PacketInterlink, 4);
+impl_is_relayed!(PacketInterlink);
+
+impl PacketInterlink {
+    pub fn payload(&self) -> &[u8] {
+        &self.0[34..(self.1 - 1)]
+    }
+
+    pub fn set_payload(&mut self, bytes: &[u8]) {
+        let is_relayed = self.is_relayed();
+        let end = 34 + bytes.len();
+        self.0[34..end].copy_from_slice(bytes);
+        self.1 = end + 1;
+        self.set_relayed(is_relayed);
+    }
+
+    pub fn verify_integrity(&self) -> bool {
+        self.link_setup_frame().check_crc() == 0
+            && self.payload().len() >= 4
+            && crate::crc::m17_crc(self.payload()) == 0
+    }
+
+    pub fn recalculate_crc(&mut self) {
+        // LSF and payload should be confirmed valid before construction
+    }
+}
+
+define_message!(ConnectInterlink, 37, 37, MAGIC_CONNECT);
+impl_address!(ConnectInterlink, 4);
+impl_modules!(ConnectInterlink, 10, 37);
+no_crc!(ConnectInterlink);
+
+define_message!(ConnectInterlinkAcknowledge, 37, 37, MAGIC_ACKNOWLEDGE);
+impl_address!(ConnectInterlinkAcknowledge, 4);
+impl_modules!(ConnectInterlinkAcknowledge, 10, 37);
+no_crc!(ConnectInterlinkAcknowledge);
+
+define_message!(DisconnectInterlink, 10, 10, MAGIC_DISCONNECT);
+impl_address!(DisconnectInterlink, 4);
+no_crc!(DisconnectInterlink);
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)