]> code.octet-stream.net Git - m17rt/commitdiff
Spruce up the high-level API for specifying addresses for transmission
authorThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 22 Jan 2025 10:36:52 +0000 (21:36 +1100)
committerThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 22 Jan 2025 10:36:52 +0000 (21:36 +1100)
Cargo.lock
m17app/Cargo.toml
m17app/src/app.rs
m17app/src/error.rs [new file with mode: 0644]
m17app/src/lib.rs
m17app/src/link_setup.rs [new file with mode: 0644]
m17codec2/src/lib.rs
m17core/src/address.rs
m17core/src/protocol.rs
tools/m17rt-mod/src/main.rs

index 3b493469d3d0ccf47600e8e0caa6d91332d38114..8faa53c885876ea570602474c0d859717a1c1fbd 100755 (executable)
@@ -398,7 +398,7 @@ dependencies = [
  "combine",
  "jni-sys",
  "log",
  "combine",
  "jni-sys",
  "log",
- "thiserror",
+ "thiserror 1.0.69",
  "walkdir",
  "windows-sys 0.45.0",
 ]
  "walkdir",
  "windows-sys 0.45.0",
 ]
@@ -458,6 +458,7 @@ dependencies = [
  "log",
  "m17core",
  "serialport",
  "log",
  "m17core",
  "serialport",
+ "thiserror 2.0.11",
 ]
 
 [[package]]
 ]
 
 [[package]]
@@ -536,7 +537,7 @@ dependencies = [
  "log",
  "ndk-sys",
  "num_enum",
  "log",
  "ndk-sys",
  "num_enum",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
 ]
 
 [[package]]
@@ -769,7 +770,16 @@ version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
 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]]
 ]
 
 [[package]]
@@ -783,6 +793,17 @@ dependencies = [
  "syn",
 ]
 
  "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"
 [[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 = [
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815"
 dependencies = [
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
 ]
 
 [[package]]
index bbd07f4e6b4bba42dc7fa8c5adc5c8ae705c7794..44a7b5fb2e14373c9629768bd1301012ccb6187e 100755 (executable)
@@ -17,3 +17,4 @@ cpal = "0.15.3"
 m17core = { path = "../m17core" }
 log = "0.4.22"
 serialport = {version = "4.7.0", default-features = false }
 m17core = { path = "../m17core" }
 log = "0.4.22"
 serialport = {version = "4.7.0", default-features = false }
+thiserror = "2.0.11"
index b1426638e28e1dd4a3dcef0d1ec96c5cbd2db5a9..a7bb3cd291b18efbf154ec5d54807fe32faf408a 100644 (file)
@@ -1,4 +1,5 @@
 use crate::adapter::{PacketAdapter, StreamAdapter};
 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};
 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 {
 }
 
 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
 
         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));
     }
         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 (file)
index 0000000..f7079ca
--- /dev/null
@@ -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),
+}
index 61545858499c8a5b2db18b0fa7febfece37ccc57..ce67840994f63ec28e5f62bd8c6c8a86470e5f6e 100755 (executable)
@@ -1,5 +1,7 @@
 pub mod adapter;
 pub mod app;
 pub mod adapter;
 pub mod app;
+pub mod error;
+pub mod link_setup;
 pub mod serial;
 pub mod soundmodem;
 pub mod tnc;
 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 (file)
index 0000000..007dc78
--- /dev/null
@@ -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<Self, M17Error> {
+        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::<String>()
+                        .trim()
+                )
+            }
+            Address::Reserved(_) => unreachable!(),
+            Address::Broadcast => {
+                write!(f, "<BROADCAST>")
+            }
+        }
+    }
+}
index 5146bcaac5678f7cbfc70b4e5eec5b2ae9e74c49..5eb54a4d6d4f20b7c2721bc85b2f39e6afcb71d2 100755 (executable)
@@ -6,6 +6,8 @@ use cpal::{Sample, SampleFormat, SampleRate};
 use log::debug;
 use m17app::adapter::StreamAdapter;
 use m17app::app::TxHandle;
 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;
 use m17core::address::Address;
 use m17core::address::Callsign;
 use m17core::protocol::LsfFrame;
@@ -160,7 +162,13 @@ fn stream_thread(end: Receiver<()>, state: Arc<Mutex<AdapterState>>, output_card
 pub struct WavePlayer;
 
 impl WavePlayer {
 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::<i16>();
 
         let mut reader = hound::WavReader::open(path).unwrap();
         let mut samples = reader.samples::<i16>();
 
@@ -172,14 +180,9 @@ impl WavePlayer {
         let mut next_tick = Instant::now() + TICK;
         let mut frame_number = 0;
 
         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;
 
         loop {
             let mut last_one = false;
@@ -196,11 +199,9 @@ impl WavePlayer {
                 }
                 codec.encode(&mut out, &in_buf);
             }
                 }
                 codec.encode(&mut out, &in_buf);
             }
-            tx.transmit_stream_next(StreamFrame {
+            tx.transmit_stream_next(&StreamFrame {
                 lich_idx: lsf_chunk as u8,
                 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(),
                 frame_number,
                 end_of_stream: last_one,
                 stream_data: out_buf.clone(),
index 48745b2941e320e307e8a95b4342a13ebe24d1d9..a224000d07e2c266491d15c55d2f3fe7c42ca707 100755 (executable)
@@ -13,7 +13,7 @@ pub enum Address {
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Callsign(pub [u8; 9]);
 
 #[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'.',
     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'.',
index e4b3b15f508f37a71e0862614219b94be8f6d487..1c9fa13a9bb9e1605e8e32bbf81f941587cd5ecf 100755 (executable)
@@ -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];
 
 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();
     }
 
         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());
     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()
     }
 }
         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);
+    }
+}
index cc063676cb350022944e6cf6a6697415148cc158..c616b2e8b0110247a8b40f67cf8c7170dd0aeae7 100644 (file)
@@ -1,4 +1,5 @@
 use m17app::app::M17App;
 use m17app::app::M17App;
+use m17app::link_setup::M17Address;
 use m17app::soundmodem::{
     InputRrcFile, InputSoundcard, NullInputSource, NullOutputSink, NullPtt, OutputRrcFile,
     OutputSoundcard, Soundmodem,
 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...");
     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));
 }
     println!("Playback complete, terminating in 5 secs");
     std::thread::sleep(std::time::Duration::from_secs(5));
 }