"combine",
"jni-sys",
"log",
- "thiserror",
+ "thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
"log",
"m17core",
"serialport",
+ "thiserror 2.0.11",
]
[[package]]
"log",
"ndk-sys",
"num_enum",
- "thiserror",
+ "thiserror 1.0.69",
]
[[package]]
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]]
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815"
dependencies = [
- "thiserror",
+ "thiserror 1.0.69",
]
[[package]]
m17core = { path = "../m17core" }
log = "0.4.22"
serialport = {version = "4.7.0", default-features = false }
+thiserror = "2.0.11"
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};
}
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));
}
--- /dev/null
+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),
+}
pub mod adapter;
pub mod app;
+pub mod error;
+pub mod link_setup;
pub mod serial;
pub mod soundmodem;
pub mod tnc;
--- /dev/null
+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>")
+ }
+ }
+ }
+}
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;
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 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;
}
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(),
#[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'.',
-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];
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());
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);
+ }
+}
use m17app::app::M17App;
+use m17app::link_setup::M17Address;
use m17app::soundmodem::{
InputRrcFile, InputSoundcard, NullInputSource, NullOutputSink, NullPtt, OutputRrcFile,
OutputSoundcard, Soundmodem,
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));
}