From: Thomas Karpiniec Date: Wed, 22 Jan 2025 10:36:52 +0000 (+1100) Subject: Spruce up the high-level API for specifying addresses for transmission X-Git-Tag: v0.1.0~6 X-Git-Url: https://code.octet-stream.net/m17rt/commitdiff_plain/18349069c27a0b4fb2e39669fb535600805ad160?hp=488cd0f950a1754f8c5a34dc2617c927e466cc3b Spruce up the high-level API for specifying addresses for transmission --- diff --git a/Cargo.lock b/Cargo.lock index 3b49346..8faa53c 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,7 +398,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -458,6 +458,7 @@ dependencies = [ "log", "m17core", "serialport", + "thiserror 2.0.11", ] [[package]] @@ -536,7 +537,7 @@ dependencies = [ "log", "ndk-sys", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -769,7 +770,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -783,6 +793,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -806,7 +827,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] diff --git a/m17app/Cargo.toml b/m17app/Cargo.toml index bbd07f4..44a7b5f 100755 --- a/m17app/Cargo.toml +++ b/m17app/Cargo.toml @@ -17,3 +17,4 @@ cpal = "0.15.3" m17core = { path = "../m17core" } log = "0.4.22" serialport = {version = "4.7.0", default-features = false } +thiserror = "2.0.11" diff --git a/m17app/src/app.rs b/m17app/src/app.rs index b142663..a7bb3cd 100644 --- a/m17app/src/app.rs +++ b/m17app/src/app.rs @@ -1,4 +1,5 @@ use crate::adapter::{PacketAdapter, StreamAdapter}; +use crate::link_setup::LinkSetup; use crate::tnc::Tnc; use m17core::kiss::{KissBuffer, KissCommand, KissFrame}; use m17core::protocol::{EncryptionType, LsfFrame, PacketType, StreamFrame}; @@ -83,26 +84,34 @@ pub struct TxHandle { } impl TxHandle { - pub fn transmit_packet(&self, packet_type: PacketType, payload: &[u8]) { - // hang on where do we get the LSF details from? We need a destination obviously - // our source address needs to be configured here too - // also there is possible CAN, encryption, meta payload - - // we will immediately convert this into a KISS payload before sending into channel so we only need borrow on data + pub fn transmit_packet( + &self, + link_setup: &LinkSetup, + packet_type: &PacketType, + payload: &[u8], + ) { + let (pack_type, pack_type_len) = packet_type.as_proto(); + if pack_type_len + payload.len() > 823 { + // TODO: error for invalid transmission type + return; + } + let mut full_payload = vec![]; + full_payload.extend_from_slice(&pack_type[0..pack_type_len]); + full_payload.extend_from_slice(&payload); + let crc = m17core::crc::m17_crc(&full_payload); + full_payload.extend_from_slice(&crc.to_be_bytes()); + let kiss_frame = KissFrame::new_full_packet(&link_setup.raw.0, &full_payload).unwrap(); + let _ = self.event_tx.send(TncControlEvent::Kiss(kiss_frame)); } - // add more methods here for stream outgoing - - pub fn transmit_stream_start(&self, lsf: LsfFrame) { - // TODO: is asking for an LsfFrame a good idea or unfriendly API? - // What I should do here is create a LinkSetup struct which wraps an LsfFrame and can be loaded with a raw one - let kiss_frame = KissFrame::new_stream_setup(&lsf.0).unwrap(); + pub fn transmit_stream_start(&self, link_setup: &LinkSetup) { + let kiss_frame = KissFrame::new_stream_setup(&link_setup.raw.0).unwrap(); let _ = self.event_tx.send(TncControlEvent::Kiss(kiss_frame)); } // as long as there is only one TNC it is implied there is only ever one stream transmission in flight - pub fn transmit_stream_next(&self, stream: StreamFrame) { + pub fn transmit_stream_next(&self, stream: &StreamFrame) { let kiss_frame = KissFrame::new_stream_data(&stream).unwrap(); let _ = self.event_tx.send(TncControlEvent::Kiss(kiss_frame)); } diff --git a/m17app/src/error.rs b/m17app/src/error.rs new file mode 100644 index 0000000..f7079ca --- /dev/null +++ b/m17app/src/error.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum M17Error { + #[error("given callsign contains at least one character invalid in M17: {0}")] + InvalidCallsignCharacters(char), + + #[error("given callsign is {0} characters long; maximum is 9")] + CallsignTooLong(usize), +} diff --git a/m17app/src/lib.rs b/m17app/src/lib.rs index 6154585..ce67840 100755 --- a/m17app/src/lib.rs +++ b/m17app/src/lib.rs @@ -1,5 +1,7 @@ pub mod adapter; pub mod app; +pub mod error; +pub mod link_setup; pub mod serial; pub mod soundmodem; pub mod tnc; diff --git a/m17app/src/link_setup.rs b/m17app/src/link_setup.rs new file mode 100644 index 0000000..007dc78 --- /dev/null +++ b/m17app/src/link_setup.rs @@ -0,0 +1,101 @@ +use std::fmt::Display; + +use m17core::{ + address::{Address, Callsign, ALPHABET}, + protocol::LsfFrame, +}; + +use crate::error::M17Error; + +pub struct LinkSetup { + pub(crate) raw: LsfFrame, +} + +impl LinkSetup { + /// Provide a completed LsfFrame. + pub fn new_raw(frame: LsfFrame) -> Self { + Self { raw: frame } + } + + /// Set up an unencrypted voice stream with channel access number 0 and the given source and destination. + pub fn new_voice(source: &M17Address, destination: &M17Address) -> Self { + Self { + raw: LsfFrame::new_voice(source.address(), destination.address()), + } + } + + /// Set up an unencrypted packet data transmission with channel access number 0 and the given source and destination. + pub fn new_packet(source: &M17Address, destination: &M17Address) -> Self { + Self { + raw: LsfFrame::new_packet(source.address(), destination.address()), + } + } + + /// Configure the channel access number for this transmission, which may be from 0 to 15 inclusive. + pub fn set_channel_access_number(&mut self, channel_access_number: u8) { + self.raw.set_channel_access_number(channel_access_number); + } + + pub fn lich_part(&self, counter: u8) -> [u8; 5] { + let idx = counter as usize; + self.raw.0[idx * 5..(idx + 1) * 5].try_into().unwrap() + } +} + +/// Station address. High level version of `Address` from core. + +#[derive(Debug, Clone)] +pub struct M17Address(Address); + +impl M17Address { + pub fn new_broadcast() -> Self { + Self(Address::Broadcast) + } + + pub fn from_callsign(callsign: &str) -> Result { + let trimmed = callsign.trim().to_uppercase(); + let len = trimmed.len(); + if len > 9 { + return Err(M17Error::CallsignTooLong(len)); + } + let mut address = [b' '; 9]; + for (i, c) in trimmed.chars().enumerate() { + if !c.is_ascii() { + return Err(M17Error::InvalidCallsignCharacters(c)); + } + if !ALPHABET.contains(&(c as u8)) { + return Err(M17Error::InvalidCallsignCharacters(c)); + } + address[i] = c as u8; + } + Ok(Self(Address::Callsign(Callsign(address)))) + } + + pub(crate) fn address(&self) -> &Address { + &self.0 + } +} + +impl Display for M17Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Address::Invalid => unreachable!(), + Address::Callsign(ref callsign) => { + write!( + f, + "{}", + callsign + .0 + .iter() + .map(|c| *c as char) + .collect::() + .trim() + ) + } + Address::Reserved(_) => unreachable!(), + Address::Broadcast => { + write!(f, "") + } + } + } +} diff --git a/m17codec2/src/lib.rs b/m17codec2/src/lib.rs index 5146bca..5eb54a4 100755 --- a/m17codec2/src/lib.rs +++ b/m17codec2/src/lib.rs @@ -6,6 +6,8 @@ use cpal::{Sample, SampleFormat, SampleRate}; use log::debug; use m17app::adapter::StreamAdapter; use m17app::app::TxHandle; +use m17app::link_setup::LinkSetup; +use m17app::link_setup::M17Address; use m17core::address::Address; use m17core::address::Callsign; use m17core::protocol::LsfFrame; @@ -160,7 +162,13 @@ fn stream_thread(end: Receiver<()>, state: Arc>, output_card pub struct WavePlayer; impl WavePlayer { - pub fn play(path: PathBuf, tx: TxHandle) { + pub fn play( + path: PathBuf, + tx: TxHandle, + source: &M17Address, + destination: &M17Address, + channel_access_number: u8, + ) { let mut reader = hound::WavReader::open(path).unwrap(); let mut samples = reader.samples::(); @@ -172,14 +180,9 @@ impl WavePlayer { let mut next_tick = Instant::now() + TICK; let mut frame_number = 0; - // TODO: need a better way to create addresses from std strings - - let lsf = LsfFrame::new_voice( - &Address::Callsign(Callsign(b"VK7XT ".clone())), - &Address::Broadcast, - ); - - tx.transmit_stream_start(lsf.clone()); + let mut setup = LinkSetup::new_voice(source, destination); + setup.set_channel_access_number(channel_access_number); + tx.transmit_stream_start(&setup); loop { let mut last_one = false; @@ -196,11 +199,9 @@ impl WavePlayer { } codec.encode(&mut out, &in_buf); } - tx.transmit_stream_next(StreamFrame { + tx.transmit_stream_next(&StreamFrame { lich_idx: lsf_chunk as u8, - lich_part: lsf.0[lsf_chunk * 5..(lsf_chunk + 1) * 5] - .try_into() - .unwrap(), + lich_part: setup.lich_part(lsf_chunk as u8), frame_number, end_of_stream: last_one, stream_data: out_buf.clone(), diff --git a/m17core/src/address.rs b/m17core/src/address.rs index 48745b2..a224000 100755 --- a/m17core/src/address.rs +++ b/m17core/src/address.rs @@ -13,7 +13,7 @@ pub enum Address { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Callsign(pub [u8; 9]); -static ALPHABET: [u8; 40] = [ +pub static ALPHABET: [u8; 40] = [ b' ', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'-', b'/', b'.', diff --git a/m17core/src/protocol.rs b/m17core/src/protocol.rs index e4b3b15..1c9fa13 100755 --- a/m17core/src/protocol.rs +++ b/m17core/src/protocol.rs @@ -1,4 +1,7 @@ -use crate::address::{encode_address, Address}; +use crate::{ + address::{encode_address, Address}, + bits::BitsMut, +}; pub(crate) const LSF_SYNC: [i8; 8] = [1, 1, 1, 1, -1, -1, 1, -1]; pub(crate) const BERT_SYNC: [i8; 8] = [-1, 1, -1, -1, 1, 1, 1, 1]; @@ -209,6 +212,15 @@ impl LsfFrame { self.recalculate_crc(); } + pub fn set_channel_access_number(&mut self, number: u8) { + let mut bits = BitsMut::new(&mut self.0); + bits.set_bit(12 * 8 + 5, (number >> 3) & 1); + bits.set_bit(12 * 8 + 6, (number >> 2) & 1); + bits.set_bit(12 * 8 + 7, (number >> 1) & 1); + bits.set_bit(13 * 8 + 0, number & 1); + self.recalculate_crc(); + } + fn recalculate_crc(&mut self) { let new_crc = crate::crc::m17_crc(&self.0[0..28]); self.0[28..30].copy_from_slice(&new_crc.to_be_bytes()); @@ -295,3 +307,15 @@ impl Default for LichCollection { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn set_can() { + let mut frame = LsfFrame([0u8; 30]); + frame.set_channel_access_number(11); + assert_eq!(frame.channel_access_number(), 11); + } +} diff --git a/tools/m17rt-mod/src/main.rs b/tools/m17rt-mod/src/main.rs index cc06367..c616b2e 100644 --- a/tools/m17rt-mod/src/main.rs +++ b/tools/m17rt-mod/src/main.rs @@ -1,4 +1,5 @@ use m17app::app::M17App; +use m17app::link_setup::M17Address; use m17app::soundmodem::{ InputRrcFile, InputSoundcard, NullInputSource, NullOutputSink, NullPtt, OutputRrcFile, OutputSoundcard, Soundmodem, @@ -16,7 +17,13 @@ pub fn mod_test() { app.start(); std::thread::sleep(std::time::Duration::from_secs(1)); println!("Beginning playback..."); - WavePlayer::play(in_path, app.tx()); + WavePlayer::play( + in_path, + app.tx(), + &M17Address::from_callsign("VK7XT").unwrap(), + &M17Address::new_broadcast(), + 0, + ); println!("Playback complete, terminating in 5 secs"); std::thread::sleep(std::time::Duration::from_secs(5)); }