From e67ea96c8a3d7c23ba29c6ed91ddb451927176a1 Mon Sep 17 00:00:00 2001 From: Thomas Karpiniec Date: Fri, 27 Dec 2024 11:32:33 +1100 Subject: [PATCH 1/1] Initial public commit - basic demod and high level structure in place --- .gitignore | 1 + Cargo.lock | 313 +++++++++++++++++++++++++++++++ Cargo.toml | 5 + LICENCE.TXT | 7 + README.TXT | 58 ++++++ demod/Cargo.toml | 15 ++ demod/src/main.rs | 52 ++++++ m17app/Cargo.toml | 14 ++ m17app/src/app.rs | 151 +++++++++++++++ m17app/src/lib.rs | 2 + m17app/src/tnc.rs | 60 ++++++ m17codec2/Cargo.toml | 15 ++ m17codec2/src/lib.rs | 22 +++ m17core/Cargo.toml | 13 ++ m17core/src/address.rs | 87 +++++++++ m17core/src/bits.rs | 139 ++++++++++++++ m17core/src/crc.rs | 31 ++++ m17core/src/decode.rs | 143 ++++++++++++++ m17core/src/fec.rs | 266 ++++++++++++++++++++++++++ m17core/src/interleave.rs | 48 +++++ m17core/src/kiss.rs | 382 ++++++++++++++++++++++++++++++++++++++ m17core/src/lib.rs | 17 ++ m17core/src/modem.rs | 162 ++++++++++++++++ m17core/src/protocol.rs | 191 +++++++++++++++++++ m17core/src/random.rs | 13 ++ m17core/src/shaping.rs | 119 ++++++++++++ m17core/src/tnc.rs | 38 ++++ m17core/src/traits.rs | 3 + 28 files changed, 2367 insertions(+) create mode 100644 .gitignore create mode 100755 Cargo.lock create mode 100755 Cargo.toml create mode 100644 LICENCE.TXT create mode 100644 README.TXT create mode 100755 demod/Cargo.toml create mode 100755 demod/src/main.rs create mode 100755 m17app/Cargo.toml create mode 100644 m17app/src/app.rs create mode 100755 m17app/src/lib.rs create mode 100644 m17app/src/tnc.rs create mode 100755 m17codec2/Cargo.toml create mode 100755 m17codec2/src/lib.rs create mode 100755 m17core/Cargo.toml create mode 100755 m17core/src/address.rs create mode 100755 m17core/src/bits.rs create mode 100755 m17core/src/crc.rs create mode 100755 m17core/src/decode.rs create mode 100755 m17core/src/fec.rs create mode 100755 m17core/src/interleave.rs create mode 100644 m17core/src/kiss.rs create mode 100755 m17core/src/lib.rs create mode 100644 m17core/src/modem.rs create mode 100755 m17core/src/protocol.rs create mode 100755 m17core/src/random.rs create mode 100755 m17core/src/shaping.rs create mode 100644 m17core/src/tnc.rs create mode 100644 m17core/src/traits.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..4130f9e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,313 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "binfield_matrix" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0781f107aa08bd90c847b3d83f4ff55e60de33993bc9e9bc829abecbbe230b50" +dependencies = [ + "num-traits", +] + +[[package]] +name = "cai_golay" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb662354a5193c9c521599ed9b59d13e51900b06716a15b6528e9ef045b2c81c" +dependencies = [ + "binfield_matrix", +] + +[[package]] +name = "codec2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefd04ca4a2f096acf5f44da5e5931436d030a620901f1fe8fa773e6b9de65b" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "demod" +version = "0.1.0" +dependencies = [ + "env_logger", + "log", + "m17app", + "m17codec2", + "m17core", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "m17app" +version = "0.1.0" +dependencies = [ + "log", + "m17core", +] + +[[package]] +name = "m17codec2" +version = "0.1.0" +dependencies = [ + "codec2", + "m17app", + "m17core", +] + +[[package]] +name = "m17core" +version = "0.1.0" +dependencies = [ + "cai_golay", + "crc", + "log", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..349414a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ "demod", + "m17app", "m17codec2", "m17core", +] diff --git a/LICENCE.TXT b/LICENCE.TXT new file mode 100644 index 0000000..4b4e102 --- /dev/null +++ b/LICENCE.TXT @@ -0,0 +1,7 @@ +Copyright (c) 2024 Thomas Karpiniec + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.TXT b/README.TXT new file mode 100644 index 0000000..27d4231 --- /dev/null +++ b/README.TXT @@ -0,0 +1,58 @@ +================== + M17 RUST TOOLKIT +================== + +As of Dec 2024 this software is still under active development. The description that follows is not fully implemented yet. + +M17RT is a collection of Rust crates and utilities to make it as easy to possible to implement programs that use the M17 +Protocol for amateur radio: . + + + ┌──────────────────────────────────────┐ <. + .> │ m17app │ | Fan in data from adapters to TNC + High level API | │ - High-level API for packets/streams │ | Fan out data from radio to adapters + For PC-based apps | │ - Sound card integration │ <. + | │ - TCP client/server KISS │ vv KISS ^^ + .> │ - Multithreading │ <. + └──────────────────────────────────────┘ | Soundmodem worker thread: + | Takes a sound card, PTT, + ┌──────────────────────────────────────┐ | assembles the components + .> │ m17core │ | from m17core and puts it + Low level API | │ - M17 KISS protocol │ | behind a KISS interface. + no_std, no heap | │ - TNC / CSMA │ | + could be no-float | │ - M17 Data Link │ | + .> │ - Baseband Modem │ <. + └──────────────────────────────────────┘ + + +When you are writing an M17 packet or voice application you will target the high-level API and not concern yourself with +what kind of TNC will ultimately be used. It is modular - you could use a serial KISS modem, a TCP KISS service running +on another host or supplied by another program, or engage the built-in soundmodem by supplying a soundcard and PTT config. +This could be configured at runtime in your program. + +Equally, the soundmodem can also be used as an independent module with any other M17 application that expects to speak to +a KISS TNC, including M17 applications that do not use this toolkit or are not written in Rust. + +The basic structure of a program is that you will configure your TNC, use it to initialise an M17App, then add adapters to +the M17App which will handle all or a subset of the traffic. They will all share the same TNC. + +Codec2 support follows the same pattern - the m17codec2 crate will provide standard M17App stream adapters to handle both: + mic -> encode -> transmit stream + incoming stream -> decode -> output on sound card + +Splitting this into a separate crate serves two purposes. This reduces the dependency count if your app does not actually +use codec2. It also means you can avoid statically linking LGPL code into your Rust binary if you are relying on M17RT's +permissive licence. In this situation you can probably still find a way to use codec2 but it's not going to be as simple +as putting this in your Cargo.toml since Rust makes dynamic linking difficult. + +Finally, there will be a series of utility binaries for modulation, demodulation, creating a KISS TCP server, etc. These +may be useful in their own right but their primary purpose is to test and demonstrate the toolkit. User-facing programs +should be their own projects that will provide proper attention to detail for their use cases. + +========= + LICENCE +========= + +Copyright 2024 Thomas Karpiniec + +M17 Rust Toolkit is made available under the MIT Licence. See LICENCE.TXT for details. diff --git a/demod/Cargo.toml b/demod/Cargo.toml new file mode 100755 index 0000000..55e93d9 --- /dev/null +++ b/demod/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "demod" +version = "0.1.0" +edition = "2021" +license = "MIT" +authors = ["Thomas Karpiniec >, + event_tx: mpsc::SyncSender, +} + +impl M17App { + pub fn new(mut tnc: T) -> Self { + let write_tnc = tnc.try_clone().unwrap(); + let (event_tx, event_rx) = mpsc::sync_channel(128); + let listeners = Arc::new(RwLock::new(Listeners::new())); + spawn_reader(tnc, listeners.clone()); + spawn_writer(write_tnc, event_rx); + Self { + listeners, + event_tx, + } + } + + pub fn add_packet_listener(&self, listener: P) -> usize { + let mut listeners = self.listeners.write().unwrap(); + let id = listeners.next; + listeners.next += 1; + listeners.packet.insert(id, Box::new(listener)); + id + } + + pub fn add_stream_listener(&self, listener: S) -> usize { + let mut listeners = self.listeners.write().unwrap(); + let id = listeners.next; + listeners.next += 1; + listeners.stream.insert(id, Box::new(listener)); + id + } + + pub fn remove_packet_listener(&self, id: usize) { + self.listeners.write().unwrap().packet.remove(&id); + } + + pub fn remove_stream_listener(&self, id: usize) { + self.listeners.write().unwrap().stream.remove(&id); + } + + pub fn transmit_packet(&self, type_code: 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 + } + + // add more methods here for stream outgoing + + pub fn transmit_stream_start(&self /* lsf?, payload? what needs to be configured ?! */) {} + + // 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, /* next payload, */ end_of_stream: bool) {} + + pub fn start(&self) { + let _ = self.event_tx.send(TncControlEvent::Start); + } + + pub fn close(&self) { + let _ = self.event_tx.send(TncControlEvent::Close); + } +} + +/// Synchronised structure for listeners subscribing to packets and streams. +/// +/// Each listener will be notified in turn of each event. +struct Listeners { + /// Identifier to be assigned to the next listener, starting from 0 + next: usize, + packet: HashMap>, + stream: HashMap>, +} + +impl Listeners { + fn new() -> Self { + Self { + next: 0, + packet: HashMap::new(), + stream: HashMap::new(), + } + } +} + +/// Carries a request from a method on M17App to the TNC's writer thread, which will execute it. +enum TncControlEvent { + Kiss(KissFrame), + Start, + Close, +} + +fn spawn_reader(mut tnc: T, listeners: Arc>) { + std::thread::spawn(move || { + let mut buf = [0u8; 1713]; + let mut n = 0; + loop { + // I want to call tnc.read() here + // Probably these needs a helper in m17core::kiss? It will be common to both TNC and host + + // After a read... + // if this does not start with FEND, forget all data up until first FEND + // if we start with a FEND, see if there is another FEND with at least one byte between + // for each such case, turn that FEND..=FEND slice into a KissFrame and attempt to parse it + // once all such pairs have been handled... + // move the last FEND onwards back to the start of the buffer + // - if there is no room to do so, this is an oversize frame. purge everything and carry on. + // perform next read from end + } + }); +} + +fn spawn_writer(mut tnc: T, event_rx: mpsc::Receiver) { + std::thread::spawn(move || { + while let Ok(ev) = event_rx.recv() { + match ev { + TncControlEvent::Kiss(k) => { + if let Err(e) = tnc.write_all(&k.as_bytes()) { + debug!("kiss send err: {:?}", e); + return; + } + } + TncControlEvent::Start => { + if let Err(e) = tnc.start() { + debug!("tnc start err: {:?}", e); + return; + } + } + TncControlEvent::Close => { + if let Err(e) = tnc.close() { + debug!("tnc close err: {:?}", e); + return; + } + } + } + } + }); +} diff --git a/m17app/src/lib.rs b/m17app/src/lib.rs new file mode 100755 index 0000000..0e1f9ef --- /dev/null +++ b/m17app/src/lib.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod tnc; diff --git a/m17app/src/tnc.rs b/m17app/src/tnc.rs new file mode 100644 index 0000000..56cb661 --- /dev/null +++ b/m17app/src/tnc.rs @@ -0,0 +1,60 @@ +use std::io::{self, ErrorKind, Read, Write}; + +use m17core::tnc::SoftTnc; + +/// +pub trait Tnc: Read + Write + Sized { + fn try_clone(&mut self) -> Result; + fn start(&mut self) -> Result<(), TncError>; + fn close(&mut self) -> Result<(), TncError>; +} + +#[derive(Debug)] +pub enum TncError { + General(String), +} + +// TODO: move the following to its own module + +pub struct Soundmodem { + tnc: SoftTnc, + config: SoundmodemConfig, +} + +pub struct SoundmodemConfig { + // sound cards, PTT, etc. +} + +impl Read for Soundmodem { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.tnc + .read_kiss(buf) + .map_err(|s| io::Error::new(ErrorKind::Other, format!("{:?}", s))) + } +} + +impl Write for Soundmodem { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.tnc + .write_kiss(buf) + .map_err(|s| io::Error::new(ErrorKind::Other, format!("{:?}", s))) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Tnc for Soundmodem { + fn try_clone(&mut self) -> Result { + unimplemented!(); + } + + fn start(&mut self) -> Result<(), TncError> { + unimplemented!(); + } + + fn close(&mut self) -> Result<(), TncError> { + unimplemented!(); + } +} diff --git a/m17codec2/Cargo.toml b/m17codec2/Cargo.toml new file mode 100755 index 0000000..7c3780e --- /dev/null +++ b/m17codec2/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "m17codec2" +version = "0.1.0" +edition = "2021" +keywords = ["amateur", "radio", "m17", "ham"] +license = "MIT" +authors = ["Thomas Karpiniec >(data: &[u8], out_path: P) { + let codec2 = Codec2::new(Codec2Mode::MODE_3200); + let var_name = codec2; + let mut codec = var_name; + let mut all_samples: Vec = vec![]; + for i in 0..(data.len() / 8) { + let mut samples = vec![0; codec.samples_per_frame()]; + 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(); + } +} diff --git a/m17core/Cargo.toml b/m17core/Cargo.toml new file mode 100755 index 0000000..53159c6 --- /dev/null +++ b/m17core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "m17core" +version = "0.1.0" +edition = "2021" +keywords = ["amateur", "radio", "m17", "ham", "no_std"] +license = "MIT" +authors = ["Thomas Karpiniec Address { + let full = u64::from_be_bytes([ + 0, 0, encoded[0], encoded[1], encoded[2], encoded[3], encoded[4], encoded[5], + ]); + match full { + m @ 1..=0xEE6B27FFFFFF => Address::Callsign(decode_base_40(m)), + m @ 0xEE6B28000000..=0xFFFFFFFFFFFE => Address::Reserved(m), + 0xFFFFFFFFFFFF => Address::Broadcast, + _ => Address::Invalid, + } +} + +fn decode_base_40(mut encoded: u64) -> Callsign { + let mut callsign = Callsign([b' '; 9]); + let mut pos = 0; + while encoded > 0 { + callsign.0[pos] = ALPHABET[(encoded % 40) as usize]; + encoded /= 40; + pos += 1; + } + callsign +} + +#[allow(dead_code)] +pub fn encode_address(address: &Address) -> [u8; 6] { + let mut out: u64 = 0; + match address { + Address::Invalid => (), + Address::Callsign(call) => { + for c in call.0.iter().rev() { + let c = c.to_ascii_uppercase(); + if let Some(pos) = ALPHABET.iter().position(|alpha| *alpha == c) { + out = out * 40 + pos as u64; + } + } + } + Address::Reserved(m) => out = *m, + Address::Broadcast => out = 0xFFFFFFFFFFFF, + } + out.to_be_bytes()[2..].try_into().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn address_encode() { + let encoded = encode_address(&Address::Callsign(Callsign( + b"AB1CD ".as_slice().try_into().unwrap(), + ))); + assert_eq!(encoded, [0x00, 0x00, 0x00, 0x9f, 0xdd, 0x51]); + } + + #[test] + fn address_decode() { + let decoded = decode_address([0x00, 0x00, 0x00, 0x9f, 0xdd, 0x51]); + assert_eq!( + decoded, + Address::Callsign(Callsign(b"AB1CD ".as_slice().try_into().unwrap())) + ); + } +} diff --git a/m17core/src/bits.rs b/m17core/src/bits.rs new file mode 100755 index 0000000..7b39995 --- /dev/null +++ b/m17core/src/bits.rs @@ -0,0 +1,139 @@ +use core::ops::Deref; + +pub(crate) struct BitsBase(T) +where + T: AsRef<[u8]>; + +impl BitsBase +where + T: AsRef<[u8]>, +{ + pub(crate) fn get_bit(&self, idx: usize) -> u8 { + self.0.as_ref()[idx / 8] >> (7 - (idx % 8)) & 0x01 + } + + pub(crate) fn iter(&self) -> BitsIterator { + BitsIterator { bits: self, idx: 0 } + } +} + +pub(crate) struct BitsIterator<'a, T> +where + T: AsRef<[u8]>, +{ + bits: &'a BitsBase, + idx: usize, +} + +impl Iterator for BitsIterator<'_, T> +where + T: AsRef<[u8]>, +{ + type Item = u8; + + fn next(&mut self) -> Option { + if self.idx >= self.bits.0.as_ref().len() * 8 { + return None; + } + let bit = self.bits.get_bit(self.idx); + self.idx += 1; + Some(bit) + } +} + +pub(crate) struct Bits<'a>(BitsBase<&'a [u8]>); + +impl<'a> Bits<'a> { + pub(crate) fn new(data: &'a [u8]) -> Self { + Self(BitsBase(data)) + } +} + +impl<'a> Deref for Bits<'a> { + type Target = BitsBase<&'a [u8]>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub(crate) struct BitsMut<'a>(BitsBase<&'a mut [u8]>); + +impl<'a> Deref for BitsMut<'a> { + type Target = BitsBase<&'a mut [u8]>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> BitsMut<'a> { + pub(crate) fn new(data: &'a mut [u8]) -> Self { + Self(BitsBase(data)) + } + + pub(crate) fn set_bit(&mut self, idx: usize, value: u8) { + let slice = &mut self.0 .0; + let existing = slice[idx / 8]; + if value == 0 { + slice[idx / 8] = existing & !(1 << (7 - (idx % 8))); + } else { + slice[idx / 8] = existing | (1 << (7 - (idx % 8))); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bits_readonly() { + let data: [u8; 2] = [0b00001111, 0b10101010]; + let bits = Bits::new(&data); + assert_eq!(bits.get_bit(0), 0); + assert_eq!(bits.get_bit(1), 0); + assert_eq!(bits.get_bit(4), 1); + assert_eq!(bits.get_bit(8), 1); + assert_eq!(bits.get_bit(9), 0); + } + + #[test] + fn bits_modifying() { + let mut data: [u8; 2] = [0b00001111, 0b10101010]; + let mut bits = BitsMut::new(&mut data); + + assert_eq!(bits.get_bit(0), 0); + bits.set_bit(0, 1); + assert_eq!(bits.get_bit(0), 1); + + assert_eq!(bits.get_bit(4), 1); + bits.set_bit(4, 0); + assert_eq!(bits.get_bit(4), 0); + + assert_eq!(bits.get_bit(9), 0); + bits.set_bit(9, 1); + assert_eq!(bits.get_bit(9), 1); + + assert_eq!(data, [0b10000111, 0b11101010]); + } + + #[test] + fn bits_iter() { + let data: [u8; 2] = [0b00110111, 0b10101010]; + let bits = Bits::new(&data); + let mut it = bits.iter(); + assert_eq!(it.next(), Some(0)); + assert_eq!(it.next(), Some(0)); + assert_eq!(it.next(), Some(1)); + assert_eq!(it.next(), Some(1)); + assert_eq!(it.next(), Some(0)); + assert_eq!(it.next(), Some(1)); + for _ in 0..8 { + let _ = it.next(); + } + assert_eq!(it.next(), Some(1)); + assert_eq!(it.next(), Some(0)); + assert_eq!(it.next(), None); + } +} diff --git a/m17core/src/crc.rs b/m17core/src/crc.rs new file mode 100755 index 0000000..92a9b96 --- /dev/null +++ b/m17core/src/crc.rs @@ -0,0 +1,31 @@ +pub const M17_ALG: crc::Algorithm = crc::Algorithm { + width: 16, + poly: 0x5935, + init: 0xFFFF, + refin: false, + refout: false, + xorout: 0x0000, + check: 0x772B, + residue: 0x0000, +}; + +pub fn m17_crc(input: &[u8]) -> u16 { + let crc = crc::Crc::::new(&M17_ALG); + let mut digest = crc.digest(); + digest.update(input); + digest.finalize() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn crc_test_vectors() { + assert_eq!(m17_crc(&[]), 0xFFFF); + assert_eq!(m17_crc("A".as_bytes()), 0x206E); + assert_eq!(m17_crc("123456789".as_bytes()), 0x772B); + let bytes: Vec = (0x00..=0xFF).collect(); + assert_eq!(m17_crc(&bytes), 0x1C31); + } +} diff --git a/m17core/src/decode.rs b/m17core/src/decode.rs new file mode 100755 index 0000000..60c210e --- /dev/null +++ b/m17core/src/decode.rs @@ -0,0 +1,143 @@ +use crate::{ + bits::BitsMut, + fec::{self, p_1, p_2}, + interleave::interleave, + protocol::{LsfFrame, StreamFrame, BERT_SYNC, LSF_SYNC, PACKET_SYNC, STREAM_SYNC}, + random::random_xor, +}; +use log::debug; + +const PLUS_THREE: [u8; 2] = [0, 1]; +const PLUS_ONE: [u8; 2] = [0, 0]; +const MINUS_ONE: [u8; 2] = [1, 0]; +const MINUS_THREE: [u8; 2] = [1, 1]; + +fn decode_sample(sample: f32) -> [u8; 2] { + if sample > 0.667 { + PLUS_THREE + } else if sample > 0.0 { + PLUS_ONE + } else if sample > -0.667 { + MINUS_ONE + } else { + MINUS_THREE + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum SyncBurst { + Lsf, + Bert, + Stream, + Packet, +} + +impl SyncBurst { + pub(crate) fn target(&self) -> [i8; 8] { + match self { + Self::Lsf => LSF_SYNC, + Self::Bert => BERT_SYNC, + Self::Stream => STREAM_SYNC, + Self::Packet => PACKET_SYNC, + } + } +} + +const SYNC_MIN_GAIN: f32 = 16.0; +const SYNC_BIT_THRESHOLD: f32 = 0.3; +pub const SYNC_THRESHOLD: f32 = 100.0; + +pub(crate) fn sync_burst_correlation(target: [i8; 8], samples: &[f32]) -> (f32, f32, f32) { + let mut pos_max: f32 = f32::MIN; + let mut neg_max: f32 = f32::MAX; + for i in 0..8 { + pos_max = pos_max.max(samples[i * 10]); + neg_max = neg_max.min(samples[i * 10]); + } + let gain = (pos_max - neg_max) / 2.0; + let shift = pos_max + neg_max; + if gain < SYNC_MIN_GAIN { + return (f32::MAX, gain, shift); + } + let mut diff = 0.0; + for i in 0..8 { + let sym_diff = (((samples[i * 10] - shift) / gain) - target[i] as f32).abs(); + if sym_diff > SYNC_BIT_THRESHOLD { + return (f32::MAX, gain, shift); + } + diff += sym_diff; + } + (diff, gain, shift) +} + +/// Decode frame and return contents after the sync burst +pub(crate) fn frame_initial_decode(frame: &[f32] /* length 192 */) -> [u8; 46] { + let mut decoded = [0u8; 48]; + let mut decoded_bits = BitsMut::new(&mut decoded); + for (idx, s) in frame.iter().enumerate() { + let dibits = decode_sample(*s); + decoded_bits.set_bit(idx * 2, dibits[0]); + decoded_bits.set_bit(idx * 2 + 1, dibits[1]); + } + random_xor(&mut decoded[2..]); + interleave(&decoded[2..]) +} + +pub(crate) fn parse_lsf(frame: &[f32] /* length 192 */) -> Option { + let deinterleaved = frame_initial_decode(frame); + let lsf = match fec::decode(&deinterleaved, 240, p_1) { + Some(lsf) => LsfFrame(lsf), + None => return None, + }; + debug!("full lsf: {:?}", lsf.0); + let crc = lsf.crc(); + debug!("recv crc: {:04X}", crc); + debug!("destination: {:?}", lsf.destination()); + debug!("source: {:?}", lsf.source()); + debug!("mode: {:?}", lsf.mode()); + debug!("data type: {:?}", lsf.data_type()); + debug!("encryption type: {:?}", lsf.encryption_type()); + debug!("can: {}", lsf.channel_access_number()); + debug!("meta: {:?}", lsf.meta()); + Some(lsf) +} + +pub(crate) fn try_lich_decode(type2_bits: &[u8]) -> Option<(u8, [u8; 5])> { + let mut decoded = 0u64; + for (input_idx, input_bytes) in type2_bits.chunks(3).enumerate() { + let mut input: u32 = 0; + for (idx, byte) in input_bytes.iter().enumerate() { + input |= (*byte as u32) << (16 - (8 * idx)); + } + let (val, _dist) = cai_golay::extended::decode(input)?; + decoded |= (val as u64) << ((3 - input_idx) * 12); + } + let b = decoded.to_be_bytes(); + Some((b[7] >> 5, [b[2], b[3], b[4], b[5], b[6]])) +} + +pub(crate) fn parse_stream(frame: &[f32] /* length 192 */) -> Option { + let deinterleaved = frame_initial_decode(frame); + let stream_part = &deinterleaved[12..]; + let stream = match fec::decode(stream_part, 144, p_2) { + Some(stream) => stream, + None => return None, + }; + let frame_num = u16::from_be_bytes([stream[0], stream[1]]); + let eos = (frame_num & 0x8000) > 0; + let frame_num = frame_num & 0x7fff; // higher layer has to handle wraparound + debug!("frame number: {frame_num}, codec2: {:?}", &stream[2..18]); + + if let Some((counter, part)) = try_lich_decode(&deinterleaved[0..12]) { + debug!("LICH: received part {counter}"); + Some(StreamFrame { + lich_idx: counter, + lich_part: part, + frame_number: frame_num, + end_of_stream: eos, + stream_data: stream[2..18].try_into().unwrap(), + }) + } else { + None + } +} diff --git a/m17core/src/fec.rs b/m17core/src/fec.rs new file mode 100755 index 0000000..7538417 --- /dev/null +++ b/m17core/src/fec.rs @@ -0,0 +1,266 @@ +use crate::bits::{Bits, BitsMut}; +use log::debug; + +struct Transition { + input: u8, + output: [u8; 2], + source: usize, +} + +static TRANSITIONS: [Transition; 32] = [ + Transition { + input: 0, + output: [0, 0], + source: 0, + }, + Transition { + input: 0, + output: [1, 1], + source: 1, + }, + Transition { + input: 0, + output: [1, 0], + source: 2, + }, + Transition { + input: 0, + output: [0, 1], + source: 3, + }, + Transition { + input: 0, + output: [0, 1], + source: 4, + }, + Transition { + input: 0, + output: [1, 0], + source: 5, + }, + Transition { + input: 0, + output: [1, 1], + source: 6, + }, + Transition { + input: 0, + output: [0, 0], + source: 7, + }, + Transition { + input: 0, + output: [0, 1], + source: 8, + }, + Transition { + input: 0, + output: [1, 0], + source: 9, + }, + Transition { + input: 0, + output: [1, 1], + source: 10, + }, + Transition { + input: 0, + output: [0, 0], + source: 11, + }, + Transition { + input: 0, + output: [0, 0], + source: 12, + }, + Transition { + input: 0, + output: [1, 1], + source: 13, + }, + Transition { + input: 0, + output: [1, 0], + source: 14, + }, + Transition { + input: 0, + output: [0, 1], + source: 15, + }, + Transition { + input: 1, + output: [1, 1], + source: 0, + }, + Transition { + input: 1, + output: [0, 0], + source: 1, + }, + Transition { + input: 1, + output: [0, 1], + source: 2, + }, + Transition { + input: 1, + output: [1, 0], + source: 3, + }, + Transition { + input: 1, + output: [1, 0], + source: 4, + }, + Transition { + input: 1, + output: [0, 1], + source: 5, + }, + Transition { + input: 1, + output: [0, 0], + source: 6, + }, + Transition { + input: 1, + output: [1, 1], + source: 7, + }, + Transition { + input: 1, + output: [1, 0], + source: 8, + }, + Transition { + input: 1, + output: [0, 1], + source: 9, + }, + Transition { + input: 1, + output: [0, 0], + source: 10, + }, + Transition { + input: 1, + output: [1, 1], + source: 11, + }, + Transition { + input: 1, + output: [1, 1], + source: 12, + }, + Transition { + input: 1, + output: [0, 0], + source: 13, + }, + Transition { + input: 1, + output: [0, 1], + source: 14, + }, + Transition { + input: 1, + output: [1, 0], + source: 15, + }, +]; + +pub(crate) fn p_1(step: usize) -> (bool, bool) { + let mod61 = step % 61; + let is_even = mod61 % 2 == 0; + (mod61 > 30 || is_even, mod61 < 30 || is_even) +} + +pub(crate) fn p_2(step: usize) -> (bool, bool) { + let mod6 = step % 6; + (true, mod6 != 5) +} + +fn best_previous(table: &[[u8; 32]; 244], step: usize, state: usize) -> u8 { + if step == 0 { + if state == 0 { + return 0; + } else { + return u8::MAX; + } + } + let prev1 = table[step - 1][state * 2]; + let prev2 = table[step - 1][state * 2 + 1]; + prev1.min(prev2) +} + +fn hamming_distance(first: &[u8], second: &[u8]) -> u8 { + first + .iter() + .zip(second.iter()) + .map(|(x, y)| if *x == *y { 0 } else { 1 }) + .sum() +} + +// maximum 368 type 3 bits, maximum 240 type 1 bits, 4 flush bits +pub(crate) fn decode( + type3: &[u8], // up to len 46 + input_len: usize, + puncture: fn(usize) -> (bool, bool), +) -> Option<[u8; 30]> { + let type3_bits = Bits::new(type3); + let mut type3_iter = type3_bits.iter(); + let mut table = [[0u8; 32]; 244]; + for step in 0..(input_len + 4) { + let (use_g1, use_g2) = puncture(step); + let split_idx = if use_g1 && use_g2 { 2 } else { 1 }; + let mut input_bits = [0u8; 2]; + input_bits[0] = type3_iter.next().unwrap(); + let step_input = if split_idx == 1 { + &input_bits[0..1] + } else { + input_bits[1] = type3_iter.next().unwrap(); + &input_bits[0..2] + }; + for (t_idx, t) in TRANSITIONS.iter().enumerate() { + let t_offer = if use_g1 && use_g2 { + &t.output[..] + } else if use_g1 { + &t.output[0..1] + } else { + &t.output[1..2] + }; + let step_dist = hamming_distance(step_input, t_offer); + table[step][t_idx] = best_previous(&table, step, t.source).saturating_add(step_dist); + } + } + let (mut best_idx, best) = table[input_len + 3] + .iter() + .enumerate() + .min_by_key(|(_, i)| *i) + .unwrap(); + debug!("Best score is {best}, transition {best_idx}"); + if *best > 6 { + None + } else { + let mut out = [0u8; 30]; + let mut out_bits = BitsMut::new(&mut out); + for step in (0..(input_len + 4)).rev() { + let input = TRANSITIONS[best_idx].input; + if step < input_len { + out_bits.set_bit(step, input); + } + if step > 0 { + let state = TRANSITIONS[best_idx].source; + let prev1 = table[step - 1][state * 2]; + let prev2 = table[step - 1][state * 2 + 1]; + best_idx = if prev1 < prev2 { + state * 2 + } else { + state * 2 + 1 + }; + } + } + Some(out) + } +} diff --git a/m17core/src/interleave.rs b/m17core/src/interleave.rs new file mode 100755 index 0000000..2db984b --- /dev/null +++ b/m17core/src/interleave.rs @@ -0,0 +1,48 @@ +use crate::bits::{Bits, BitsMut}; + +static MAPPING: [usize; 368] = [ + 0, 137, 90, 227, 180, 317, 270, 39, 360, 129, 82, 219, 172, 309, 262, 31, 352, 121, 74, 211, + 164, 301, 254, 23, 344, 113, 66, 203, 156, 293, 246, 15, 336, 105, 58, 195, 148, 285, 238, 7, + 328, 97, 50, 187, 140, 277, 230, 367, 320, 89, 42, 179, 132, 269, 222, 359, 312, 81, 34, 171, + 124, 261, 214, 351, 304, 73, 26, 163, 116, 253, 206, 343, 296, 65, 18, 155, 108, 245, 198, 335, + 288, 57, 10, 147, 100, 237, 190, 327, 280, 49, 2, 139, 92, 229, 182, 319, 272, 41, 362, 131, + 84, 221, 174, 311, 264, 33, 354, 123, 76, 213, 166, 303, 256, 25, 346, 115, 68, 205, 158, 295, + 248, 17, 338, 107, 60, 197, 150, 287, 240, 9, 330, 99, 52, 189, 142, 279, 232, 1, 322, 91, 44, + 181, 134, 271, 224, 361, 314, 83, 36, 173, 126, 263, 216, 353, 306, 75, 28, 165, 118, 255, 208, + 345, 298, 67, 20, 157, 110, 247, 200, 337, 290, 59, 12, 149, 102, 239, 192, 329, 282, 51, 4, + 141, 94, 231, 184, 321, 274, 43, 364, 133, 86, 223, 176, 313, 266, 35, 356, 125, 78, 215, 168, + 305, 258, 27, 348, 117, 70, 207, 160, 297, 250, 19, 340, 109, 62, 199, 152, 289, 242, 11, 332, + 101, 54, 191, 144, 281, 234, 3, 324, 93, 46, 183, 136, 273, 226, 363, 316, 85, 38, 175, 128, + 265, 218, 355, 308, 77, 30, 167, 120, 257, 210, 347, 300, 69, 22, 159, 112, 249, 202, 339, 292, + 61, 14, 151, 104, 241, 194, 331, 284, 53, 6, 143, 96, 233, 186, 323, 276, 45, 366, 135, 88, + 225, 178, 315, 268, 37, 358, 127, 80, 217, 170, 307, 260, 29, 350, 119, 72, 209, 162, 299, 252, + 21, 342, 111, 64, 201, 154, 291, 244, 13, 334, 103, 56, 193, 146, 283, 236, 5, 326, 95, 48, + 185, 138, 275, 228, 365, 318, 87, 40, 177, 130, 267, 220, 357, 310, 79, 32, 169, 122, 259, 212, + 349, 302, 71, 24, 161, 114, 251, 204, 341, 294, 63, 16, 153, 106, 243, 196, 333, 286, 55, 8, + 145, 98, 235, 188, 325, 278, 47, +]; + +pub fn interleave(payload: &[u8]) -> [u8; 46] { + let payload_bits = Bits::new(payload); + let mut new = [0; 46]; + let mut new_bits = BitsMut::new(&mut new); + for (input, output) in MAPPING.iter().enumerate() { + new_bits.set_bit(*output, payload_bits.get_bit(input)); + } + new +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn interleaving_table() { + let mut mapping = [0; 368]; + for i in 0..368i64 { + mapping[i as usize] = ((45 * i + 92 * i.pow(2)) % 368) as usize; + } + println!("{mapping:?}"); + assert_eq!(mapping, MAPPING); + } +} diff --git a/m17core/src/kiss.rs b/m17core/src/kiss.rs new file mode 100644 index 0000000..c270de2 --- /dev/null +++ b/m17core/src/kiss.rs @@ -0,0 +1,382 @@ +// Note FEND and FESC both have the top two bits set. In the header byte this corresponds +// to high port numbers which are never used by M17 so we needn't bother (un)escaping it. + +const FEND: u8 = 0xC0; +const FESC: u8 = 0xDB; +const TFEND: u8 = 0xDC; +const TFESC: u8 = 0xDD; + +pub const PORT_PACKET_BASIC: u8 = 0; +pub const PORT_PACKET_FULL: u8 = 1; +pub const PORT_STREAM: u8 = 2; + +/// Maximum theoretical frame size for any valid M17 KISS frame. +/// +/// In M17 Full Packet Mode a 30-byte LSF is merged with a packet which may be up to +/// 825 bytes in length. Supposing an (impossible) worst case that every byte is FEND +/// or FESC, 1710 bytes is the maximum expected payload. With a FEND at each end and +/// the KISS frame header byte we get 1713. +pub const MAX_FRAME_LEN: usize = 1713; + +/// Holder for any valid M17 KISS frame. +/// +/// For efficiency, `data` and `len` are exposed directly and received KISS data may +/// be streamed directly into a pre-allocated `KissFrame`. +pub struct KissFrame { + pub data: [u8; MAX_FRAME_LEN], + pub len: usize, +} + +impl KissFrame { + /// Request to transmit a data packet (basic mode). + /// + /// A raw payload up to 822 bytes can be provided. The TNC will mark it as Raw format + /// and automatically calculate the checksum. If required it will also chunk the packet + /// into individual frames and transmit them sequentially. + pub fn new_basic_packet(payload: &[u8]) -> Result { + // M17 packet payloads can be up to 825 bytes in length + // Type prefix (RAW = 0x00) occupies the first byte + // Last 2 bytes are checksum + if payload.len() > 822 { + return Err(KissError::PayloadTooBig); + } + let mut data = [0u8; MAX_FRAME_LEN]; + let mut i = 0; + push(&mut data, &mut i, FEND); + push( + &mut data, + &mut i, + kiss_header(PORT_PACKET_BASIC, KissCommand::DataFrame.proto_value()), + ); + + i += escape(payload, &mut data[i..]); + push(&mut data, &mut i, FEND); + + Ok(KissFrame { data, len: i }) + } + + /// Request to transmit a data packet (full mode). + /// + /// Sender must provide a 30-byte LSF and a full packet payload (up to 825 bytes) + /// that will be combined into the frame. The packet payload must include the type + /// code prefix and the CRC, both of which would have been calculated by the TNC if + /// it was basic mode. + pub fn new_full_packet(lsf: &[u8], packet: &[u8]) -> Result { + if lsf.len() != 30 { + return Err(KissError::LsfWrongSize); + } + if packet.len() > 825 { + return Err(KissError::PayloadTooBig); + } + let mut data = [0u8; MAX_FRAME_LEN]; + let mut i = 0; + push(&mut data, &mut i, FEND); + push( + &mut data, + &mut i, + kiss_header(PORT_PACKET_FULL, KissCommand::DataFrame.proto_value()), + ); + i += escape(lsf, &mut data[i..]); + i += escape(packet, &mut data[i..]); + push(&mut data, &mut i, FEND); + + Ok(KissFrame { data, len: i }) + } + + /// Request to begin a stream data transfer (e.g. voice). + /// + /// An LSF payload of exactly 30 bytes must be provided. + /// + /// This must be followed by at least one stream data payload, ending with one that + /// has the end of stream (EOS) bit set. + pub fn new_stream_setup(lsf: &[u8]) -> Result { + if lsf.len() != 30 { + return Err(KissError::LsfWrongSize); + } + let mut data = [0u8; MAX_FRAME_LEN]; + let mut i = 0; + push(&mut data, &mut i, FEND); + push( + &mut data, + &mut i, + kiss_header(PORT_STREAM, KissCommand::DataFrame.proto_value()), + ); + i += escape(lsf, &mut data[i..]); + push(&mut data, &mut i, FEND); + + Ok(KissFrame { data, len: i }) + } + + /// Transmit a segment of data in a stream transfer (e.g. voice). + /// + /// A data payload of 26 bytes including metadata must be provided. This must follow + /// exactly the prescribed format (H.5.2 in the spec). The TNC will be watching for + /// the EOS flag to know that this transmission has ended. + pub fn new_stream_data(stream_data: &[u8]) -> Result { + if stream_data.len() != 26 { + return Err(KissError::StreamDataWrongSize); + } + let mut data = [0u8; MAX_FRAME_LEN]; + let mut i = 0; + push(&mut data, &mut i, FEND); + push( + &mut data, + &mut i, + kiss_header(PORT_STREAM, KissCommand::DataFrame.proto_value()), + ); + i += escape(stream_data, &mut data[i..]); + push(&mut data, &mut i, FEND); + + Ok(KissFrame { data, len: i }) + } + + /// Request to set the TxDelay + pub fn new_set_tx_delay(port: u8, units: u8) -> Self { + let mut data = [0u8; MAX_FRAME_LEN]; + let mut i = 0; + push(&mut data, &mut i, FEND); + push( + &mut data, + &mut i, + kiss_header(port, KissCommand::TxDelay.proto_value()), + ); + push(&mut data, &mut i, units); + push(&mut data, &mut i, FEND); + + KissFrame { data, len: i } + } + + /// Request to set the persistence parameter P + pub fn new_set_p(port: u8, units: u8) -> Self { + let mut data = [0u8; MAX_FRAME_LEN]; + let mut i = 0; + push(&mut data, &mut i, FEND); + push( + &mut data, + &mut i, + kiss_header(port, KissCommand::P.proto_value()), + ); + push(&mut data, &mut i, units); + push(&mut data, &mut i, FEND); + + KissFrame { data, len: i } + } + + /// Request to set full duplex or not + pub fn set_full_duplex(port: u8, full_duplex: bool) -> Self { + let mut data = [0u8; MAX_FRAME_LEN]; + let mut i = 0; + push(&mut data, &mut i, FEND); + push( + &mut data, + &mut i, + kiss_header(port, KissCommand::FullDuplex.proto_value()), + ); + push(&mut data, &mut i, if full_duplex { 1 } else { 0 }); + push(&mut data, &mut i, FEND); + + KissFrame { data, len: i } + } + + /// Return this frame's KISS command type. + pub fn command(&self) -> Result { + KissCommand::from_proto(self.header_byte()? & 0x0f) + } + + /// Return the KISS port to which this frame relates. Should be 0, 1 or 2. + pub fn port(&self) -> Result { + Ok(self.header_byte()? >> 4) + } + + /// Payload part of the frame between the header byte and the trailing FEND, unescaped. + pub fn decode_payload(&self, out: &mut [u8]) -> Result { + let start = self + .data + .iter() + .enumerate() + .skip_while(|(_, b)| **b == FEND) + .skip(1) + .next() + .ok_or(KissError::MalformedKissFrame)? + .0; + let end = self.data[start..] + .iter() + .enumerate() + .skip_while(|(_, b)| **b != FEND) + .next() + .ok_or(KissError::MalformedKissFrame)? + .0 + + start; + Ok(unescape(&self.data[start..end], out)) + } + + /// Borrow the frame as a slice + pub fn as_bytes(&self) -> &[u8] { + &self.data[..self.len] + } + + /// Return the header byte of the KISS frame, skipping over 0 or more prepended FENDs. + fn header_byte(&self) -> Result { + Ok(self + .data + .iter() + .skip_while(|b| **b == FEND) + .next() + .cloned() + .ok_or(KissError::MalformedKissFrame)?) + } +} + +fn kiss_header(port: u8, command: u8) -> u8 { + (port << 4) | (command & 0x0f) +} + +fn push(data: &mut [u8], idx: &mut usize, value: u8) { + data[*idx] = value; + *idx += 1; +} + +pub enum KissCommand { + DataFrame, + TxDelay, + P, + FullDuplex, +} + +impl KissCommand { + fn from_proto(value: u8) -> Result { + Ok(match value { + 0 => KissCommand::DataFrame, + 1 => KissCommand::TxDelay, + 2 => KissCommand::P, + 5 => KissCommand::FullDuplex, + _ => return Err(KissError::UnsupportedKissCommand), + }) + } + + fn proto_value(&self) -> u8 { + match self { + KissCommand::DataFrame => 0, + KissCommand::TxDelay => 1, + KissCommand::P => 2, + KissCommand::FullDuplex => 5, + } + } +} + +#[derive(Debug)] +pub enum KissError { + MalformedKissFrame, + UnsupportedKissCommand, + PayloadTooBig, + LsfWrongSize, + StreamDataWrongSize, +} + +fn escape(src: &[u8], dst: &mut [u8]) -> usize { + let mut i = 0; + let mut j = 0; + while i < src.len() && j < dst.len() { + if src[i] == FEND { + dst[j] = FESC; + j += 1; + dst[j] = TFEND; + } else if src[i] == FESC { + dst[j] = FESC; + j += 1; + dst[j] = TFESC; + } else { + dst[j] = src[i]; + } + i += 1; + j += 1; + } + j +} + +fn unescape(src: &[u8], dst: &mut [u8]) -> usize { + let mut i = 0; + let mut j = 0; + while i < src.len() && j < dst.len() { + if src[i] == FESC { + if i == src.len() - 1 { + break; + } + i += 1; + if src[i] == TFEND { + dst[j] = FEND; + } else if src[i] == TFESC { + dst[j] = FESC; + } + } else { + dst[j] = src[i]; + } + i += 1; + j += 1; + } + j +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_escape() { + let mut buf = [0u8; 1024]; + + let src = [0, 1, 2, 3, 4, 5]; + let n = escape(&src, &mut buf); + assert_eq!(n, 6); + assert_eq!(&buf[0..6], src); + + let src = [0, 1, TFESC, 3, TFEND, 5]; + let n = escape(&src, &mut buf); + assert_eq!(n, 6); + assert_eq!(&buf[0..6], src); + + let src = [0, 1, FEND, 3, 4, 5]; + let n = escape(&src, &mut buf); + assert_eq!(n, 7); + assert_eq!(&buf[0..7], &[0, 1, FESC, TFEND, 3, 4, 5]); + + let src = [0, 1, 2, 3, 4, FESC]; + let n = escape(&src, &mut buf); + assert_eq!(n, 7); + assert_eq!(&buf[0..7], &[0, 1, 2, 3, 4, FESC, TFESC]); + } + + #[test] + fn test_unescape() { + let mut buf = [0u8; 1024]; + + let src = [0, 1, 2, 3, 4, 5]; + let n = unescape(&src, &mut buf); + assert_eq!(n, 6); + assert_eq!(&buf[0..6], src); + + let src = [0, 1, TFESC, 3, TFEND, 5]; + let n = unescape(&src, &mut buf); + assert_eq!(n, 6); + assert_eq!(&buf[0..6], src); + + let src = [0, 1, FESC, TFEND, 3, 4, 5]; + let n = unescape(&src, &mut buf); + assert_eq!(n, 6); + assert_eq!(&buf[0..6], &[0, 1, FEND, 3, 4, 5]); + + let src = [0, 1, 2, 3, 4, FESC, TFESC]; + let n = unescape(&src, &mut buf); + assert_eq!(n, 6); + assert_eq!(&buf[0..6], &[0, 1, 2, 3, 4, FESC]); + } + + #[test] + fn basic_packet_roundtrip() { + let f = KissFrame::new_basic_packet(&[0, 1, 2, 3]).unwrap(); + assert_eq!(f.as_bytes(), &[FEND, 0, 0, 1, 2, 3, FEND]); + let mut buf = [0u8; 1024]; + let n = f.decode_payload(&mut buf).unwrap(); + assert_eq!(&buf[..n], &[0, 1, 2, 3]); + } +} diff --git a/m17core/src/lib.rs b/m17core/src/lib.rs new file mode 100755 index 0000000..c1e94a8 --- /dev/null +++ b/m17core/src/lib.rs @@ -0,0 +1,17 @@ +#![allow(clippy::needless_range_loop)] +#![cfg_attr(not(test), no_std)] + +pub mod address; +pub mod kiss; +pub mod modem; +pub mod protocol; +pub mod tnc; +pub mod traits; + +mod bits; +mod crc; +mod decode; +mod fec; +mod interleave; +mod random; +mod shaping; diff --git a/m17core/src/modem.rs b/m17core/src/modem.rs new file mode 100644 index 0000000..af36ab4 --- /dev/null +++ b/m17core/src/modem.rs @@ -0,0 +1,162 @@ +use crate::decode::{parse_lsf, parse_stream, sync_burst_correlation, SyncBurst, SYNC_THRESHOLD}; +use crate::protocol::Frame; +use crate::shaping::RRC_48K; +use log::debug; + +pub trait Demodulator { + fn demod(&mut self, sample: i16) -> Option; + fn data_carrier_detect(&self) -> bool; +} + +/// Converts a sequence of samples into frames. +pub struct SoftDemodulator { + /// Circular buffer of incoming samples for calculating the RRC filtered value + filter_win: [i16; 81], + /// Current position in filter_win + filter_cursor: usize, + /// Circular buffer of shaped samples for performing decodes based on the last 192 symbols + rx_win: [f32; 1920], + /// Current position in rx_cursor + rx_cursor: usize, + /// A position that we are considering decoding due to decent sync + candidate: Option, + /// How many samples have we received? + sample: u64, + /// Remaining samples to ignore so once we already parse a frame we flush it out in full + suppress: u16, +} + +impl SoftDemodulator { + pub fn new() -> Self { + SoftDemodulator { + filter_win: [0i16; 81], + filter_cursor: 0, + rx_win: [0f32; 1920], + rx_cursor: 0, + candidate: None, + sample: 0, + suppress: 0, + } + } +} + +impl Demodulator for SoftDemodulator { + fn demod(&mut self, sample: i16) -> Option { + self.filter_win[self.filter_cursor] = sample; + self.filter_cursor = (self.filter_cursor + 1) % 81; + let mut out: f32 = 0.0; + for i in 0..81 { + let filter_idx = (self.filter_cursor + i) % 81; + out += RRC_48K[i] * self.filter_win[filter_idx] as f32; + } + + self.rx_win[self.rx_cursor] = out; + self.rx_cursor = (self.rx_cursor + 1) % 1920; + + self.sample += 1; + + if self.suppress > 0 { + self.suppress -= 1; + return None; + } + + let mut burst_window = [0f32; 71]; + for i in 0..71 { + let c = (self.rx_cursor + i) % 1920; + burst_window[i] = self.rx_win[c]; + } + + for burst in [ + SyncBurst::Lsf, + SyncBurst::Bert, + SyncBurst::Stream, + SyncBurst::Packet, + ] { + let (diff, max, shift) = sync_burst_correlation(burst.target(), &burst_window); + if diff < SYNC_THRESHOLD { + let mut new_candidate = true; + if let Some(c) = self.candidate.as_mut() { + if diff > c.diff { + c.age += 1; + new_candidate = false; + } + } + if new_candidate { + self.candidate = Some(DecodeCandidate { + burst, + age: 1, + diff, + gain: max, + shift, + }); + } + } + if diff >= SYNC_THRESHOLD + && self + .candidate + .as_ref() + .map(|c| c.burst == burst) + .unwrap_or(false) + { + if let Some(c) = self.candidate.take() { + let start_idx = self.rx_cursor + 1920 - (c.age as usize); + let start_sample = self.sample - c.age as u64; + let mut pkt_samples = [0f32; 192]; + for i in 0..192 { + let rx_idx = (start_idx + i * 10) % 1920; + pkt_samples[i] = (self.rx_win[rx_idx] - c.shift) / c.gain; + } + match c.burst { + SyncBurst::Lsf => { + debug!( + "Found LSF at sample {} diff {} max {} shift {}", + start_sample, c.diff, c.gain, c.shift + ); + if let Some(frame) = parse_lsf(&pkt_samples) { + self.suppress = 191 * 10; + return Some(Frame::Lsf(frame)); + } + } + SyncBurst::Bert => { + debug!("Found BERT at sample {} diff {}", start_sample, c.diff); + } + SyncBurst::Stream => { + debug!( + "Found STREAM at sample {} diff {} max {} shift {}", + start_sample, c.diff, c.gain, c.shift + ); + if let Some(frame) = parse_stream(&pkt_samples) { + self.suppress = 191 * 10; + return Some(Frame::Stream(frame)); + } + } + SyncBurst::Packet => { + debug!("Found PACKET at sample {} diff {}", start_sample, c.diff) + } + } + } + } + } + + None + } + + fn data_carrier_detect(&self) -> bool { + false + } +} + +impl Default for SoftDemodulator { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +pub(crate) struct DecodeCandidate { + burst: SyncBurst, + age: u8, + diff: f32, + gain: f32, + shift: f32, +} diff --git a/m17core/src/protocol.rs b/m17core/src/protocol.rs new file mode 100755 index 0000000..96def5b --- /dev/null +++ b/m17core/src/protocol.rs @@ -0,0 +1,191 @@ +use crate::address::Address; + +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 STREAM_SYNC: [i8; 8] = [-1, -1, -1, -1, 1, 1, -1, 1]; +pub(crate) const PACKET_SYNC: [i8; 8] = [1, -1, 1, 1, -1, -1, -1, -1]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Mode { + Packet, + Stream, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DataType { + Reserved, + Data, + Voice, + VoiceAndData, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EncryptionType { + None, + Scrambler, + Aes, + Other, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Frame { + Lsf(LsfFrame), + Stream(StreamFrame), + // Packet + // BERT +} + +pub enum PacketType { + /// RAW + Raw, + /// AX.25 + Ax25, + /// APRS + Aprs, + /// 6LoWPAN + SixLowPan, + /// IPv4 + Ipv4, + /// SMS + Sms, + /// Winlink + Winlink, + /// Custom identifier + Other(char), +} + +impl PacketType { + pub fn as_proto(&self) -> ([u8; 4], usize) { + match self { + PacketType::Raw => ([0, 0, 0, 0], 1), + PacketType::Ax25 => ([1, 0, 0, 0], 1), + PacketType::Aprs => ([2, 0, 0, 0], 1), + PacketType::SixLowPan => ([3, 0, 0, 0], 1), + PacketType::Ipv4 => ([4, 0, 0, 0], 1), + PacketType::Sms => ([5, 0, 0, 0], 1), + PacketType::Winlink => ([6, 0, 0, 0], 1), + PacketType::Other(c) => { + let mut buf = [0u8; 4]; + let s = c.encode_utf8(&mut buf); + let len = s.len(); + (buf, len) + } + } + } + + pub fn from_proto(&self, buf: &[u8]) -> Option { + buf.utf8_chunks() + .next() + .and_then(|chunk| chunk.valid().chars().next()) + .map(|c| match c as u32 { + 0x00 => PacketType::Raw, + 0x01 => PacketType::Ax25, + 0x02 => PacketType::Aprs, + 0x03 => PacketType::SixLowPan, + 0x04 => PacketType::Ipv4, + 0x05 => PacketType::Sms, + 0x06 => PacketType::Winlink, + _ => PacketType::Other(c), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LsfFrame(pub [u8; 30]); + +impl LsfFrame { + pub fn crc(&self) -> u16 { + crate::crc::m17_crc(&self.0) + } + + pub fn destination(&self) -> Address { + crate::address::decode_address((&self.0[0..6]).try_into().unwrap()) + } + + pub fn source(&self) -> Address { + crate::address::decode_address((&self.0[6..12]).try_into().unwrap()) + } + + pub fn mode(&self) -> Mode { + if self.0[12] & 0x01 > 0 { + Mode::Stream + } else { + Mode::Packet + } + } + + pub fn data_type(&self) -> DataType { + match (self.0[12] >> 1) & 0x03 { + 0b00 => DataType::Reserved, + 0b01 => DataType::Data, + 0b10 => DataType::Voice, + 0b11 => DataType::VoiceAndData, + _ => unreachable!(), + } + } + + pub fn encryption_type(&self) -> EncryptionType { + match (self.0[12] >> 3) & 0x03 { + 0b00 => EncryptionType::None, + 0b01 => EncryptionType::Scrambler, + 0b10 => EncryptionType::Aes, + 0b11 => EncryptionType::Other, + _ => unreachable!(), + } + } + + pub fn channel_access_number(&self) -> u8 { + (self.0[12] >> 7) & 0x0f + } + + pub fn meta(&self) -> [u8; 14] { + self.0[14..28].try_into().unwrap() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StreamFrame { + /// Which LICH segment is given in this frame, from 0 to 5 inclusive + pub lich_idx: u8, + /// Decoded LICH segment + pub lich_part: [u8; 5], + /// Which frame in the transmission this is, starting from 0 + pub frame_number: u16, + /// Is this the last frame in the transmission? + pub end_of_stream: bool, + /// Raw application data in this frame + pub stream_data: [u8; 16], +} + +pub struct LichCollection([Option<[u8; 5]>; 6]); + +impl LichCollection { + pub fn new() -> Self { + Self([None; 6]) + } + + pub fn valid_segments(&self) -> usize { + self.0.iter().filter(|s| s.is_some()).count() + } + + pub fn set_segment(&mut self, counter: u8, part: [u8; 5]) { + self.0[counter as usize] = Some(part); + } + + pub fn try_assemble(&self) -> Option<[u8; 30]> { + let mut out = [0u8; 30]; + for (i, segment) in self.0.iter().enumerate() { + let Some(segment) = segment else { + return None; + }; + for (j, seg_val) in segment.iter().enumerate() { + out[i * 5 + j] = *seg_val; + } + } + Some(out) + } +} + +impl Default for LichCollection { + fn default() -> Self { + Self::new() + } +} diff --git a/m17core/src/random.rs b/m17core/src/random.rs new file mode 100755 index 0000000..e43cf68 --- /dev/null +++ b/m17core/src/random.rs @@ -0,0 +1,13 @@ +//! Randomiser for frame payloads + +const RANDOM_SEQ: [u8; 46] = [ + 0xD6, 0xB5, 0xE2, 0x30, 0x82, 0xFF, 0x84, 0x62, 0xBA, 0x4E, 0x96, 0x90, 0xD8, 0x98, 0xDD, 0x5D, + 0x0C, 0xC8, 0x52, 0x43, 0x91, 0x1D, 0xF8, 0x6E, 0x68, 0x2F, 0x35, 0xDA, 0x14, 0xEA, 0xCD, 0x76, + 0x19, 0x8D, 0xD5, 0x80, 0xD1, 0x33, 0x87, 0x13, 0x57, 0x18, 0x2D, 0x29, 0x78, 0xC3, +]; + +pub fn random_xor(data: &mut [u8]) { + for (idx, byte) in data.iter_mut().enumerate() { + *byte ^= RANDOM_SEQ[idx % 46]; + } +} diff --git a/m17core/src/shaping.rs b/m17core/src/shaping.rs new file mode 100755 index 0000000..735c890 --- /dev/null +++ b/m17core/src/shaping.rs @@ -0,0 +1,119 @@ +pub static RRC_48K: [f32; 81] = [ + -0.0031955054, + -0.002930098, + -0.001940547, + -0.00035607078, + 0.0015469185, + 0.003389342, + 0.0047616027, + 0.0053105336, + 0.0048244493, + 0.003297721, + 0.00095865194, + -0.0017498062, + -0.00423843, + -0.005881418, + -0.006149877, + -0.0047450834, + -0.0017040828, + 0.0025476913, + 0.0072151264, + 0.011230345, + 0.013421123, + 0.012729687, + 0.008449026, + 0.00043672565, + -0.010734711, + -0.023725418, + -0.03649577, + -0.04649801, + -0.0509759, + -0.04733776, + -0.03355284, + -0.008513286, + 0.027694825, + 0.07365995, + 0.1266812, + 0.18297966, + 0.23806532, + 0.28721792, + 0.3260201, + 0.35087407, + 0.35943073, + 0.35087407, + 0.3260201, + 0.28721792, + 0.23806532, + 0.18297966, + 0.1266812, + 0.07365995, + 0.027694825, + -0.008513286, + -0.03355284, + -0.04733776, + -0.0509759, + -0.04649801, + -0.03649577, + -0.023725418, + -0.010734711, + 0.00043672565, + 0.008449026, + 0.012729687, + 0.013421123, + 0.011230345, + 0.0072151264, + 0.0025476913, + -0.0017040828, + -0.0047450834, + -0.006149877, + -0.005881418, + -0.00423843, + -0.0017498062, + 0.00095865194, + 0.003297721, + 0.0048244493, + 0.0053105336, + 0.0047616027, + 0.003389342, + 0.0015469185, + -0.00035607078, + -0.001940547, + -0.002930098, + -0.0031955054, +]; + +#[cfg(test)] +mod test { + use std::f32::consts::PI; + + #[test] + fn calculate_rrc_coefficients() { + let mut rrc = [0.0; 81]; + let roll_off = 0.5; + let t_s: f32 = 10.0; + + let inf_t = t_s / (4.0 * roll_off); + + for i in 0..81 { + let t = (i as isize - 40) as f32; + + if t == 0.0 { + rrc[i] = 1.0 / t_s.sqrt() * ((1.0 - roll_off) + (4.0 * roll_off / PI)); + } else if t == inf_t || t == -inf_t { + rrc[i] = roll_off / (2.0 * t_s).sqrt() + * ((1.0 + 2.0 / PI) * f32::sin(PI / (4.0 * roll_off)) + + (1.0 - 2.0 / PI) * f32::cos(PI / (4.0 * roll_off))); + } else { + rrc[i] = 1.0 / t_s.sqrt() + * (f32::sin((PI * t * (1.0 - roll_off)) / t_s) + + (4.0 * roll_off * t) / t_s * f32::cos((PI * t * (1.0 + roll_off)) / t_s)) + / (PI * t / t_s * (1.0 - (4.0 * roll_off * t / t_s).powi(2))); + } + } + + println!("{:?}", rrc); + for (a, b) in rrc.iter().zip(super::RRC_48K.iter()) { + assert!((a - b).abs() < 0.00001); + } + } +} diff --git a/m17core/src/tnc.rs b/m17core/src/tnc.rs new file mode 100644 index 0000000..a6af088 --- /dev/null +++ b/m17core/src/tnc.rs @@ -0,0 +1,38 @@ +use crate::protocol::Frame; + +/// Handles the KISS protocol and frame management for `SoftModulator` and `SoftDemodulator`. +/// +/// These components work alongside each other. User is responsible for chaining them together +/// or doing something else with the data. +pub struct SoftTnc {} + +impl SoftTnc { + /// Process an individual `Frame` that has been decoded by the modem. + pub fn handle_frame(&mut self, _frame: Frame) -> Result<(), SoftTncError> { + Ok(()) + } + + /// + pub fn advance_samples(&mut self, _samples: u64) {} + + pub fn set_data_carrier_detect(&mut self, _dcd: bool) {} + + pub fn read_tx_frame(&mut self) -> Result, SoftTncError> { + // yes we want to deal with Frames here + // it's important to establish successful decode that SoftDemodulator is aware of the frame innards + Ok(None) + } + + pub fn read_kiss(&mut self, _buf: &mut [u8]) -> Result { + Ok(0) + } + + pub fn write_kiss(&mut self, _buf: &[u8]) -> Result { + Ok(0) + } +} + +#[derive(Debug)] +pub enum SoftTncError { + General(&'static str), +} diff --git a/m17core/src/traits.rs b/m17core/src/traits.rs new file mode 100644 index 0000000..c846ba4 --- /dev/null +++ b/m17core/src/traits.rs @@ -0,0 +1,3 @@ +pub trait PacketListener {} + +pub trait StreamListener {} -- 2.39.5