From: Thomas Karpiniec Date: Wed, 14 May 2025 09:30:04 +0000 (+1000) Subject: Voice UDP to RF conversion, and better sample rate management in codec2 X-Git-Url: https://code.octet-stream.net/m17rt/commitdiff_plain/2c7d53c113f4e19f24c928b2c5eca1c3f6f799af?ds=inline;hp=1f1869d3e36f7192892fad069ef159faba208ccd Voice UDP to RF conversion, and better sample rate management in codec2 --- diff --git a/Cargo.lock b/Cargo.lock old mode 100755 new mode 100644 index e9719ee..294bbc3 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 diff --git a/buildscripts/build.sh b/buildscripts/build.sh old mode 100755 new mode 100644 diff --git a/buildscripts/dist-generic.sh b/buildscripts/dist-generic.sh old mode 100755 new mode 100644 diff --git a/buildscripts/dist.sh b/buildscripts/dist.sh old mode 100755 new mode 100644 diff --git a/buildscripts/lint.sh b/buildscripts/lint.sh old mode 100755 new mode 100644 diff --git a/buildscripts/test.sh b/buildscripts/test.sh old mode 100755 new mode 100644 diff --git a/m17app/Cargo.toml b/m17app/Cargo.toml old mode 100755 new mode 100644 diff --git a/m17app/src/lib.rs b/m17app/src/lib.rs old mode 100755 new mode 100644 diff --git a/m17app/src/link_setup.rs b/m17app/src/link_setup.rs index 2a701de..2394725 100644 --- a/m17app/src/link_setup.rs +++ b/m17app/src/link_setup.rs @@ -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 } } diff --git a/m17codec2/Cargo.toml b/m17codec2/Cargo.toml old mode 100755 new mode 100644 index 4b8014c..29f8323 --- a/m17codec2/Cargo.toml +++ b/m17codec2/Cargo.toml @@ -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" diff --git a/m17codec2/src/lib.rs b/m17codec2/src/lib.rs old mode 100755 new mode 100644 index 879000d..693cb09 --- a/m17codec2/src/lib.rs +++ b/m17codec2/src/lib.rs @@ -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>(data: &[u8], out_path: P) -> Vec { let codec2 = Codec2::new(Codec2Mode::MODE_3200); let var_name = codec2; @@ -35,8 +42,6 @@ pub fn decode_codec2>(data: &[u8], out_path: P) -> Vec { 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, codec2: Codec2, end_tx: Option>, + resampler: Option>, } 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 = + 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) { +fn output_cb(data: &mut [i16], state: &Mutex, 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>, + setup_tx: Sender>, state: Arc>, output_card: Option, ) { @@ -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 index 0000000..24cff0a --- /dev/null +++ b/m17codec2/src/soundcards.rs @@ -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 { + 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 { + 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 +} diff --git a/m17core/Cargo.toml b/m17core/Cargo.toml old mode 100755 new mode 100644 diff --git a/m17core/src/address.rs b/m17core/src/address.rs old mode 100755 new mode 100644 diff --git a/m17core/src/bits.rs b/m17core/src/bits.rs old mode 100755 new mode 100644 diff --git a/m17core/src/crc.rs b/m17core/src/crc.rs old mode 100755 new mode 100644 diff --git a/m17core/src/decode.rs b/m17core/src/decode.rs old mode 100755 new mode 100644 diff --git a/m17core/src/fec.rs b/m17core/src/fec.rs old mode 100755 new mode 100644 diff --git a/m17core/src/interleave.rs b/m17core/src/interleave.rs old mode 100755 new mode 100644 diff --git a/m17core/src/lib.rs b/m17core/src/lib.rs old mode 100755 new mode 100644 diff --git a/m17core/src/protocol.rs b/m17core/src/protocol.rs old mode 100755 new mode 100644 diff --git a/m17core/src/random.rs b/m17core/src/random.rs old mode 100755 new mode 100644 diff --git a/m17core/src/reflector.rs b/m17core/src/reflector.rs deleted file mode 100644 index 657d864..0000000 --- a/m17core/src/reflector.rs +++ /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 { - 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 { - 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 { - 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 { - 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 { - 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 index 0000000..8e5a96d --- /dev/null +++ b/m17core/src/reflector/convert.rs @@ -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, + /// 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, 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 index 0000000..057bae0 --- /dev/null +++ b/m17core/src/reflector/mod.rs @@ -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 index 0000000..3f746d6 --- /dev/null +++ b/m17core/src/reflector/packet.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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/shaping.rs b/m17core/src/shaping.rs old mode 100755 new mode 100644 diff --git a/tools/m17rt-demod/Cargo.toml b/tools/m17rt-demod/Cargo.toml old mode 100755 new mode 100644 diff --git a/tools/m17rt-demod/src/main.rs b/tools/m17rt-demod/src/main.rs old mode 100755 new mode 100644 diff --git a/tools/m17rt-rxpacket/Cargo.toml b/tools/m17rt-rxpacket/Cargo.toml old mode 100755 new mode 100644 diff --git a/tools/m17rt-rxpacket/src/main.rs b/tools/m17rt-rxpacket/src/main.rs old mode 100755 new mode 100644