Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
1 result

Target

Select target project
No results found
Select Git revision
  • master
1 result
Show changes

Commits on Source 61

32 files
+ 4077
690
Compare changes
  • Side-by-side
  • Inline

Files

.gitlab-ci.yml

0 → 100644
+11 −0
Original line number Original line Diff line number Diff line
image: "rust:latest"

before_script:
  - rustup component add rustfmt
  - rustup component add clippy

test:
  script:
    - cargo fmt -- --check
    - cargo clippy --all-targets --all-features -- -D warnings
    - cargo test
+12 −3
Original line number Original line Diff line number Diff line
[package]
[package]
name = "ham-cats"
name = "ham-cats"
version = "0.1.0"
version = "0.2.2"
edition = "2021"
edition = "2021"

license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "Reference implementations for CATS, the ham radio protocol"
repository = "https://gitlab.scd31.com/cats/ham_cats"
homepage = "https://cats.radio"
categories = ["embedded", "encoding", "no-std", "no-std::no-alloc"]


[dependencies]
[dependencies]
arrayvec = { version = "0.7.4", default-features = false }
arrayvec = { version = "0.7.4", default-features = false }
bitvec = { version = "1.0.1", default-features = false }
crc = "3.0.1"
encoding_rs = { version = "0.8.33", default-features = false }
half = { version = "2.3.1", default-features = false }
half = { version = "2.3.1", default-features = false }
labrador-ldpc = "1.2.1"
paste = "1.0.14"
snafu = { version = "0.7.5", default-features = false }

LICENSE.md

0 → 100644
+26 −0
Original line number Original line Diff line number Diff line
The MIT License (MIT)
=====================

Copyright © 2023 CATS

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.

fuzz/.gitignore

0 → 100644
+3 −0
Original line number Original line Diff line number Diff line
target
corpus
artifacts

fuzz/Cargo.lock

0 → 100644
+243 −0
Original line number Original line Diff line number Diff line
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "arbitrary"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"

[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"

[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
 "funty",
 "radium",
 "tap",
 "wyz",
]

[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
 "jobserver",
 "libc",
]

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "crc"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
dependencies = [
 "crc-catalog",
]

[[package]]
name = "crc-catalog"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"

[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"

[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"

[[package]]
name = "encoding_rs"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
 "cfg-if",
]

[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"

[[package]]
name = "half"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872"
dependencies = [
 "cfg-if",
 "crunchy",
]

[[package]]
name = "ham-cats"
version = "0.2.0"
dependencies = [
 "arrayvec",
 "bitvec",
 "crc",
 "encoding_rs",
 "half",
 "labrador-ldpc",
 "paste",
 "snafu",
]

[[package]]
name = "ham-cats-fuzz"
version = "0.0.0"
dependencies = [
 "ham-cats",
 "libfuzzer-sys",
]

[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"

[[package]]
name = "jobserver"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
dependencies = [
 "libc",
]

[[package]]
name = "labrador-ldpc"
version = "1.1.1"

[[package]]
name = "libc"
version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"

[[package]]
name = "libfuzzer-sys"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
dependencies = [
 "arbitrary",
 "cc",
 "once_cell",
]

[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"

[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"

[[package]]
name = "proc-macro2"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"

[[package]]
name = "snafu"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6"
dependencies = [
 "doc-comment",
 "snafu-derive",
]

[[package]]
name = "snafu-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"

[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
 "tap",
]

fuzz/Cargo.toml

0 → 100644
+43 −0
Original line number Original line Diff line number Diff line
[package]
name = "ham-cats-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"

[dependencies.ham-cats]
path = ".."

# Prevent this from interfering with workspaces
[workspace]
members = ["."]

[[bin]]
name = "fuzz_target_1"
path = "fuzz_targets/fuzz_target_1.rs"
test = false
doc = false

[[bin]]
name = "fuzz_target_2"
path = "fuzz_targets/fuzz_target_2.rs"
test = false
doc = false

[[bin]]
name = "fuzz_target_3"
path = "fuzz_targets/fuzz_target_3.rs"
test = false
doc = false

[[bin]]
name = "fuzz_target_4"
path = "fuzz_targets/fuzz_target_4.rs"
test = false
doc = false
+10 −0
Original line number Original line Diff line number Diff line
#![no_main]
use libfuzzer_sys::fuzz_target;

use ham_cats::packet::Packet;

fuzz_target!(|data: &[u8]| {
    let mut buf = [0; 1024];

    let _ = Packet::<1024>::fully_decode(data, &mut buf);
});
+12 −0
Original line number Original line Diff line number Diff line
#![no_main]
use libfuzzer_sys::fuzz_target;

use ham_cats::{buffer::Buffer, packet::Packet};

fuzz_target!(|data: &[u8]| {
    let mut buf = [0; 1024];
    let mut buf = Buffer::new_empty(&mut buf);
    if buf.try_extend_from_slice(data).is_ok() {
        let _ = Packet::<1024>::semi_decode(buf);
    }
});
+12 −0
Original line number Original line Diff line number Diff line
#![no_main]
use libfuzzer_sys::fuzz_target;

use ham_cats::{buffer::Buffer, packet::Packet};

fuzz_target!(|data: &[u8]| {
    let mut buf = [0; 1024];
    let mut buf = Buffer::new_empty(&mut buf);
    if buf.try_extend_from_slice(data).is_ok() {
        let _ = Packet::<1024>::decode(buf);
    }
});
+16 −0
Original line number Original line Diff line number Diff line
#![no_main]
use libfuzzer_sys::fuzz_target;

use ham_cats::packet::Packet;
use std::convert::TryInto;

fuzz_target!(|data: &[u8]| {
    // [u8] -> [i8]
    let mut data: Vec<_> = data
        .iter()
        .map(|x| (u16::from(*x) as i16 - 128).try_into().unwrap())
        .collect();

    let mut buf = [0; 1024];
    let _ = Packet::<1024>::fully_decode_soft::<8192, i8>(&mut data, &mut buf);
});

src/buffer.rs

0 → 100644
+122 −0
Original line number Original line Diff line number Diff line
use core::ops::{Deref, DerefMut};

#[derive(Debug)]
pub struct BufferOverflow;

#[derive(Debug)]
pub struct Buffer<'a, const N: usize, T = u8> {
    data: &'a mut [T; N],
    i: usize,
}

impl<'a, const N: usize, T: Copy> Buffer<'a, N, T> {
    /// Constructs a new `Buffer`.
    /// `data` is the backing array.
    /// `i` is the number of elements in `data` that contain data (and should thus be exposed by `Buffer`)
    pub fn new(data: &'a mut [T; N], i: usize) -> Self {
        assert!(i <= data.len());

        Self { data, i }
    }

    pub fn new_full(data: &'a mut [T; N]) -> Self {
        let i = data.len();

        Self::new(data, i)
    }

    pub fn new_empty(data: &'a mut [T; N]) -> Self {
        Self::new(data, 0)
    }

    pub const fn remaining_capacity(&self) -> usize {
        N - self.i
    }

    pub fn try_push(&mut self, v: T) -> Result<(), BufferOverflow> {
        if self.i == N {
            return Err(BufferOverflow);
        }

        self.data[self.i] = v;
        self.i += 1;

        Ok(())
    }

    pub fn push(&mut self, v: T) {
        self.try_push(v).unwrap();
    }

    pub fn try_extend_from_slice(&mut self, other: &[T]) -> Result<(), BufferOverflow> {
        if self.remaining_capacity() < other.len() {
            return Err(BufferOverflow);
        }

        self.data[self.i..(self.i + other.len())].copy_from_slice(other);
        self.i += other.len();

        Ok(())
    }

    pub fn extend(&mut self, other: &[T]) {
        self.try_extend_from_slice(other).unwrap();
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.i == 0 {
            return None;
        }

        self.i -= 1;

        Some(self.data[self.i])
    }

    pub fn truncate(&mut self, new_len: usize) {
        assert!(self.i >= new_len);

        self.i = new_len;
    }

    pub fn drain(&mut self, start: usize, end: usize) {
        assert!(end >= start);
        assert!(end <= self.i);
        let delta = end - start;
        let surplus = self.len() - end;

        for i in start..(start + surplus) {
            self[i] = self[i + delta];
        }

        self.i -= delta;
    }

    pub fn clone_backing<'b>(&self, buf: &'b mut [T; N]) -> Buffer<'b, N, T> {
        let mut out = Buffer::new_empty(buf);

        out.extend(self);

        out
    }
}

impl<'a, const N: usize, T> From<&'a mut [T; N]> for Buffer<'a, N, T> {
    fn from(data: &'a mut [T; N]) -> Self {
        Self { data, i: 0 }
    }
}

impl<const N: usize, T> Deref for Buffer<'_, N, T> {
    type Target = [T];

    fn deref(&self) -> &Self::Target {
        &self.data[..self.i]
    }
}

impl<const N: usize, T> DerefMut for Buffer<'_, N, T> {
    fn deref_mut(&mut self) -> &mut [T] {
        &mut self.data[..self.i]
    }
}

src/error.rs

0 → 100644
+88 −0
Original line number Original line Diff line number Diff line
use snafu::prelude::*;

#[derive(Debug, Snafu)]
pub enum DecodeError {
    #[snafu(display("Unexpected end of input"))]
    UnexpectedEndOfInput,

    #[snafu(display("Malformed whisker at position `{}`", position))]
    MalformedWhisker { position: usize },

    #[snafu(display("Duplicate whisker at position `{}`", position))]
    DuplicateWhisker { position: usize },

    #[snafu(display("Comment data is not valid UTF-8"))]
    InvalidComment,

    #[snafu(display("CRC checksum mismatch"))]
    CrcMismatch,

    #[snafu(display("LDPC Decode error"))]
    LdpcError,

    #[snafu(display("Given data does not fit in the CATS packet"))]
    Overflow,
}

#[derive(Debug, Snafu)]
pub enum EncodeError {
    #[snafu(display("New data is a duplicate of an existing data"))]
    DuplicateData,

    #[snafu(display("Given data causes CATS packet to overflow"))]
    CatsOverflow,
}

#[derive(Debug, Snafu)]
pub enum CommentError {
    #[snafu(display("CATS packet does not have comment data"))]
    NoComment,

    #[snafu(display("Given buffer too small for comment data"))]
    BufferOverflow,
}

#[derive(Debug, Snafu)]
pub enum DigipeatError {
    #[snafu(display("No route"))]
    NoRoute,

    #[snafu(display("Identification is us"))]
    Us,

    #[snafu(display("Max hops hit"))]
    MaxHops,

    #[snafu(display("Already digipeated by this node"))]
    AlreadyDigipeated,

    #[snafu(display("Future hop(s) are already set"))]
    SetDestiny,
}

#[derive(Debug, Snafu, PartialEq, Eq)]
pub enum AppendNodeError {
    #[snafu(display("Future hop(s) already set to a different node"))]
    SetFuture,

    #[snafu(display("Max hops hit"))]
    HopsOverflow,

    #[snafu(display("Node already in route"))]
    DuplicateNode,

    #[snafu(display("Given data causes the route whisker to overflow"))]
    RouteOverflow,
}

#[derive(Debug, Snafu, PartialEq, Eq)]
pub enum PacketRouteAppendError {
    #[snafu(display("No route whisker on packet"))]
    NoRouteWhisker,

    #[snafu(display("Route error"))]
    Route { error: AppendNodeError },

    #[snafu(display("CATS packet overflow"))]
    PacketOverflow,
}

src/identity.rs

0 → 100644
+19 −0
Original line number Original line Diff line number Diff line
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct Identity<'a> {
    callsign: &'a str,
    ssid: u8,
}

impl<'a> Identity<'a> {
    pub fn new(callsign: &'a str, ssid: u8) -> Self {
        Self { callsign, ssid }
    }

    pub fn callsign(&self) -> &'a str {
        self.callsign
    }

    pub fn ssid(&self) -> u8 {
        self.ssid
    }
}

src/interleaver.rs

0 → 100644
+143 −0
Original line number Original line Diff line number Diff line
use bitvec::prelude::*;
use labrador_ldpc::decoder::DecodeFrom;

use crate::{
    buffer::{Buffer, BufferOverflow},
    error::EncodeError,
};

pub(crate) fn interleave<const N: usize>(
    data: &[u8],
    out: &mut Buffer<N>,
) -> Result<(), EncodeError> {
    let bv = data.view_bits::<Msb0>();

    let initial_length = out.len();
    for _ in 0..data.len() {
        out.try_push(0).map_err(|_| EncodeError::CatsOverflow)?;
    }
    let bv_out = out[initial_length..].view_bits_mut::<Msb0>();

    let mut out_i = 0;
    for i in 0..32 {
        for j in (0..bv.len()).step_by(32) {
            if i + j >= bv.len() {
                continue;
            }

            bv_out.set(out_i, bv[i + j]);

            out_i += 1;
        }
    }

    Ok(())
}

pub(crate) fn uninterleave<const N: usize>(
    data: &[u8],
    out: &mut Buffer<N>,
) -> Result<(), BufferOverflow> {
    let bv = data.view_bits::<Msb0>();

    for _ in 0..data.len() {
        out.try_push(0)?;
    }
    let bv_out = out.view_bits_mut::<Msb0>();

    let mut out_i = 0;
    for i in 0..32 {
        for j in (0..bv.len()).step_by(32) {
            if i + j >= bv.len() {
                continue;
            }

            bv_out.set(i + j, bv[out_i]);

            out_i += 1;
        }
    }

    Ok(())
}

pub(crate) fn uninterleave_soft<const N: usize, T: DecodeFrom>(
    data: &[T],
    out: &mut Buffer<N, T>,
) -> Result<(), BufferOverflow> {
    for _ in 0..data.len() {
        out.try_push(T::zero())?;
    }

    let mut out_i = 0;
    for i in 0..32 {
        for j in (0..data.len()).step_by(32) {
            if i + j >= data.len() {
                continue;
            }

            out[i + j] = data[out_i];

            out_i += 1;
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::soft_bit::FromHardBit;

    #[test]
    fn interleaver_works() {
        let mut data = [0x84, 0x73, 0x12, 0xA3, 0xFF, 0x00, 0xC2, 0x1B, 0x77];
        let orig = Buffer::new_full(&mut data);

        let mut interleaved = [0; 10];
        let mut interleaved = Buffer::new(&mut interleaved, 0);
        interleaved.push(b'H');

        interleave(&orig, &mut interleaved).unwrap();

        let expected = [b'H', 0xCD, 0xB5, 0xDB, 0x2A, 0x0A, 0x52, 0x0C, 0x89, 0x4F];
        assert_eq!(expected, interleaved[..]);

        let mut uninterleaved = [0; 10];
        let mut uninterleaved = Buffer::new(&mut uninterleaved, 0);
        uninterleave(&interleaved[1..], &mut uninterleaved).unwrap();

        assert_eq!(*orig, *uninterleaved);
    }

    #[test]
    fn hard_interleave_soft_uninterleave() {
        let mut data = [0x84, 0x73, 0x12, 0xA3, 0xFF, 0x00, 0xC2, 0x1B, 0x77];
        let orig = Buffer::new_full(&mut data);

        let mut interleaved = [0; 10];
        let mut interleaved = Buffer::new(&mut interleaved, 0);
        interleaved.push(b'H');

        interleave(&orig, &mut interleaved).unwrap();

        let mut soft_interleaved = [0.0; 10 * 8];
        for (i, b) in interleaved.iter().enumerate() {
            for j in 0..8 {
                soft_interleaved[8 * i + j] = f32::from_hard_bit(b & (1 << (7 - j)) > 0);
            }
        }

        let mut uninterleaved = [0.0; 10 * 8];
        let mut uninterleaved = Buffer::new(&mut uninterleaved, 0);
        uninterleave_soft(&soft_interleaved[8..], &mut uninterleaved).unwrap();

        assert_eq!(orig.len() * 8, uninterleaved.len());
        for (i, b) in orig.iter().enumerate() {
            for j in 0..8 {
                assert_eq!(uninterleaved[8 * i + j].hard_bit(), *b & (1 << (7 - j)) > 0);
            }
        }
    }
}

src/ldpc.rs

0 → 100644
+384 −0
Original line number Original line Diff line number Diff line
use labrador_ldpc::{decoder::DecodeFrom, LDPCCode};

use crate::{buffer::Buffer, error::EncodeError};

macro_rules! enc_chunk {
    ($d:ident, $i:ident, $t:ident, $n:literal) => {
        ::paste::paste! {
        let mut codeword = [0; $n * 2];
        codeword[0..$n].copy_from_slice(&$d[$i..($i + $n)]);
        labrador_ldpc::LDPCCode::[<$t>].encode(&mut codeword);
        $d.try_extend_from_slice(&codeword[$n..])
            .map_err(|_| EncodeError::CatsOverflow)?;

            $i += $n;
        }
    };
}

macro_rules! dec_chunk {
    ($d:ident, $p:ident, $w:ident, $t:ident, $n:literal) => {
        ::paste::paste! {
            let code_data = &mut $d[..$n];
            let code_parity = &$p.get_mut(..$n)?;

            let mut input = [0; $n * 2];
            input[..$n].copy_from_slice(code_data);
            input[$n..].copy_from_slice(code_parity);
            const CODE: LDPCCode = LDPCCode::[<$t>];
            let mut out = [0; CODE.output_len()];
            CODE.decode_bf(&input, &mut out, &mut $w[..CODE.decode_bf_working_len()], 16);
            code_data.copy_from_slice(&out[..$n]);

            $d = &mut $d[$n..];
            $p = &mut $p[$n..];
        }
    };
}

macro_rules! dec_chunk_soft {
    ($d:ident, $p:ident, $w:ident, $w_u8:ident, $out:ident, $t:ident, $n:literal) => {
        ::paste::paste! {
            let code_data = &mut $d[..$n];
            let code_parity = &$p.get_mut(..$n)?;

            let mut input = [T::zero(); $n * 2];
            input[..$n].copy_from_slice(code_data);
            input[$n..].copy_from_slice(code_parity);
            const CODE: LDPCCode = LDPCCode::[<$t>];

			let mut out_tmp = [0; CODE.output_len()];
            CODE.decode_ms(&input, &mut out_tmp, &mut $w[..CODE.decode_ms_working_len()], &mut $w_u8[..CODE.decode_ms_working_u8_len()], 16);
			$out.try_extend_from_slice(&out_tmp[..$n/8]).ok()?;

            $d = &mut $d[$n..];
            $p = &mut $p[$n..];
        }
    };
}

// On failure this still modifies the data array!
pub(crate) fn encode<const N: usize>(data: &mut Buffer<N>) -> Result<(), EncodeError> {
    let mut i = 0;
    let n = data.len();

    loop {
        match n - i {
            512.. => {
                enc_chunk!(data, i, TM8192, 512);
            }

            128.. => {
                enc_chunk!(data, i, TM2048, 128);
            }

            32.. => {
                enc_chunk!(data, i, TC512, 32);
            }

            16.. => {
                enc_chunk!(data, i, TC256, 16);
            }

            8.. => {
                enc_chunk!(data, i, TC128, 8);
            }

            0 => break,

            _ => {
                let mut codeword = [0xAA; 16];
                codeword[0..(n - i)].copy_from_slice(&data[i..n]);
                LDPCCode::TC128.encode(&mut codeword);
                data.try_extend_from_slice(&codeword[8..])
                    .map_err(|_| EncodeError::CatsOverflow)?;

                i = n;
            }
        }
    }

    data.try_extend_from_slice(&u16::try_from(n).unwrap().to_le_bytes())
        .map_err(|_| EncodeError::CatsOverflow)?;

    Ok(())
}

pub(crate) fn decode<const N: usize>(data_av: &mut Buffer<N>) -> Option<()> {
    if data_av.len() < 2 {
        return None;
    }

    let len = [data_av[data_av.len() - 2], data_av[data_av.len() - 1]];
    let len = u16::from_le_bytes(len).into();

    if len >= data_av.len() {
        return None;
    }

    let (mut data, parity) = data_av.split_at_mut(len);
    let mut parity = parity.get_mut(..parity.len().checked_sub(2)?)?;

    let mut working = [0; LDPCCode::TM8192.decode_bf_working_len()];

    loop {
        match data.len() {
            512.. => {
                dec_chunk!(data, parity, working, TM8192, 512);
            }

            128.. => {
                dec_chunk!(data, parity, working, TM2048, 128);
            }

            32.. => {
                dec_chunk!(data, parity, working, TC512, 32);
            }

            16.. => {
                dec_chunk!(data, parity, working, TC256, 16);
            }

            8.. => {
                dec_chunk!(data, parity, working, TC128, 8);
            }

            0 => break,

            _ => {
                let mut code_data = [0xAA; 8];
                code_data[..data.len()].copy_from_slice(data);
                let code_parity = &parity.get_mut(..8)?;

                let mut input = [0; 16];
                input[..8].copy_from_slice(&code_data);
                input[8..].copy_from_slice(code_parity);
                let mut out = [0; LDPCCode::TC128.output_len()];
                LDPCCode::TC128.decode_bf(
                    &input,
                    &mut out,
                    &mut working[..LDPCCode::TC128.decode_bf_working_len()],
                    16,
                );
                data.copy_from_slice(&out[..data.len()]);

                data = &mut data[..0];
                parity = &mut parity[..0];
            }
        }
    }

    // remove the parity data
    data_av.truncate(len);

    Some(())
}

pub(crate) fn decode_soft<const N: usize, const M: usize, T: DecodeFrom>(
    data_av: &mut Buffer<N, T>,
    out: &mut Buffer<M>,
) -> Option<()> {
    if data_av.len() % 8 != 0 {
        return None;
    }

    if data_av.len() < 16 {
        return None;
    }

    let len: usize = len_from_soft(data_av[(data_av.len() - 16)..].try_into().unwrap()).into();

    if len * 8 + 16 >= data_av.len() {
        return None;
    }

    let data_len = data_av.len().checked_sub(16)?;
    let data_av = data_av.get_mut(..data_len)?;

    let (mut data, mut parity) = data_av.split_at_mut(len * 8);

    let mut working = [T::zero(); LDPCCode::TM8192.decode_ms_working_len()];
    let mut working_u8 = [0; LDPCCode::TM8192.decode_ms_working_u8_len()];

    loop {
        match data.len() {
            4096.. => {
                dec_chunk_soft!(data, parity, working, working_u8, out, TM8192, 4096);
            }

            1024.. => {
                dec_chunk_soft!(data, parity, working, working_u8, out, TM2048, 1024);
            }

            256.. => {
                dec_chunk_soft!(data, parity, working, working_u8, out, TC512, 256);
            }

            128.. => {
                dec_chunk_soft!(data, parity, working, working_u8, out, TC256, 128);
            }

            64.. => {
                dec_chunk_soft!(data, parity, working, working_u8, out, TC128, 64);
            }

            0 => break,

            _ => {
                // Extra bits are padded with 0xAA
                // We need to tell the soft decoder that these bits can't have flipped
                // So we use T::maxval
                let mut code_data = [
                    1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
                    1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
                    1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
                ]
                .map(|x| if x > 0 { -T::maxval() } else { T::maxval() });
                code_data[..data.len()].copy_from_slice(data);
                let code_parity = &parity.get_mut(..64)?;

                let mut input = [T::zero(); 128];
                input[..64].copy_from_slice(&code_data);
                input[64..].copy_from_slice(code_parity);
                let mut tmp_out = [0; LDPCCode::TC128.output_len()];
                LDPCCode::TC128.decode_ms(
                    &input,
                    &mut tmp_out,
                    &mut working[..LDPCCode::TC128.decode_ms_working_len()],
                    &mut working_u8[..LDPCCode::TC128.decode_ms_working_u8_len()],
                    16,
                );
                out.try_extend_from_slice(&tmp_out[..(data.len() / 8)])
                    .ok()?;

                data = &mut data[..0];
                parity = &mut parity[..0];
            }
        }
    }

    Some(())
}

fn len_from_soft<T: DecodeFrom>(bits: &[T; 16]) -> u16 {
    let mut upper = 0;
    for b in &bits[0..8] {
        upper <<= 1;
        upper |= u8::from(b.hard_bit());
    }

    let mut lower = 0;
    for b in &bits[8..] {
        lower <<= 1;
        lower |= u8::from(b.hard_bit());
    }

    u16::from_le_bytes([upper, lower])
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::soft_bit::FromHardBit;
    use bitvec::{order::Msb0, view::BitView};

    #[test]
    fn len_test() {
        // from the example in the docs
        let mut buf = [0; 8191];
        let mut data = Buffer::new_empty(&mut buf);

        for _ in 0..41 {
            data.extend(b"Example packet data  wueirpqwerwrywqoeiruy29346129384761");
        }
        data.extend(b"Example packet data  wueirpqwerwrywqoeiru346129384761");

        assert_eq!(2349, data.len());

        encode(&mut data).unwrap();

        assert_eq!(4703, data.len());
    }

    #[test]
    fn basic_encode_decode_short() {
        let mut buf = [0; 32];
        let mut buf2 = [0; 32];
        let mut data = Buffer::new_empty(&mut buf);
        data.try_extend_from_slice(b"Hello world!").unwrap();
        let orig = data.clone_backing(&mut buf2);

        encode(&mut data).unwrap();

        decode(&mut data).unwrap();

        assert_eq!(*orig, *data);
    }

    #[test]
    fn basic_encode_decode() {
        let mut buf = [0; 8191];
        let mut buf2 = [0; 8191];
        let mut data = Buffer::new_empty(&mut buf);
        for _ in 0..50 {
            data.extend(b"This is a test packet. jsalksjd093809324JASLD:LKD*#$)(*#@)");
        }
        let orig = data.clone_backing(&mut buf2);

        encode(&mut data).unwrap();
        assert_ne!(*orig, *data);

        decode(&mut data).unwrap();

        assert_eq!(*orig, *data);
    }

    #[test]
    fn encode_decode_with_bit_flips() {
        let mut buf = [0; 8191];
        let mut buf2 = [0; 8191];
        let mut data = Buffer::new_empty(&mut buf);

        for _ in 0..50 {
            data.extend(b"jsalksjd093809324JASLD:LKD*#$)(*#@) Another test packet");
        }
        let orig = data.clone_backing(&mut buf2);

        encode(&mut data).unwrap();
        assert_ne!(*orig, *data);

        data[234] ^= 0x55;
        data[0] ^= 0xAA;
        data[999] ^= 0x43;

        decode(&mut data).unwrap();

        assert_eq!(*orig, *data);
    }

    #[test]
    fn basic_encode_decode_soft() {
        let mut buf = [0; 8191];
        let mut buf2 = [0; 8191];
        let mut data = Buffer::new_empty(&mut buf);
        for _ in 0..50 {
            data.extend(b"This is a test packet. jsalksjd093809324JASLD:LKD*#$)(*#@)");
        }
        let orig = data.clone_backing(&mut buf2);

        encode(&mut data).unwrap();
        assert_ne!(*orig, *data);

        let mut soft = [0.0; 8191 * 8];
        let mut soft = Buffer::new_empty(&mut soft);
        for b in data.view_bits::<Msb0>() {
            soft.push(f32::from_hard_bit(*b));
        }

        let mut out = [0; 8191];
        let mut out = Buffer::new_empty(&mut out);
        decode_soft(&mut soft, &mut out).unwrap();

        assert_eq!(*orig, *out);
    }
}
+10 −3
Original line number Original line Diff line number Diff line
#![no_std]
#![cfg_attr(not(test), no_std)]


// mod raw;
pub mod buffer;
pub mod error;
pub mod identity;
pub mod interleaver;
pub mod ldpc;
pub mod packet;
pub mod soft_bit;
pub mod whisker;
pub mod whisker;
pub mod whitener;


// pub use raw::RawData;
mod utf8;

src/packet.rs

0 → 100644
+818 −0

File added.

Preview size limit exceeded, changes collapsed.

src/raw.rs

deleted100644 → 0
+0 −6
Original line number Original line Diff line number Diff line
use arrayvec::ArrayString;

pub struct RawData {
    identification: Option<ArrayString<255>>,
    timestamp: 
}

src/soft_bit.rs

0 → 100644
+40 −0
Original line number Original line Diff line number Diff line
pub use labrador_ldpc::decoder::DecodeFrom;

// Less than zero = 1 bit
// Greater than zero = 0 bit
pub trait FromHardBit {
    fn from_hard_bit(bit: bool) -> Self;
}

impl<T: DecodeFrom> FromHardBit for T {
    fn from_hard_bit(bit: bool) -> Self {
        if bit {
            -Self::one()
        } else {
            Self::one()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn e2e() {
        assert!(f32::from_hard_bit(true).hard_bit());
        assert!(!f32::from_hard_bit(false).hard_bit());

        assert!(f64::from_hard_bit(true).hard_bit());
        assert!(!f64::from_hard_bit(false).hard_bit());

        assert!(i8::from_hard_bit(true).hard_bit());
        assert!(!i8::from_hard_bit(false).hard_bit());

        assert!(i16::from_hard_bit(true).hard_bit());
        assert!(!i16::from_hard_bit(false).hard_bit());

        assert!(i32::from_hard_bit(true).hard_bit());
        assert!(!i32::from_hard_bit(false).hard_bit());
    }
}

src/utf8.rs

0 → 100644
+43 −0
Original line number Original line Diff line number Diff line
use encoding_rs::{DecoderResult, UTF_8};

pub fn validate<I: Iterator<Item = u8>>(iter: I) -> bool {
    let mut decoder = UTF_8.new_decoder();

    let mut dest = [0; 4];
    for b in iter {
        let (status, _, _) = decoder.decode_to_utf8_without_replacement(&[b], &mut dest, false);

        if !valid_result(status) {
            return false;
        }
    }

    let (status, _, _) = decoder.decode_to_utf8_without_replacement(&[], &mut dest, true);

    valid_result(status)
}

fn valid_result(result: DecoderResult) -> bool {
    match result {
        DecoderResult::InputEmpty => true,
        DecoderResult::OutputFull => unreachable!(),
        DecoderResult::Malformed(_, _) => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validate_valid() {
        let valid = b"I am valid\xf0\x90\x8c\xbc";
        assert!(validate(valid.iter().cloned()));
    }

    #[test]
    fn validate_invalid() {
        let invalid = b"I am invalid\xc3\x28";
        assert!(!validate(invalid.iter().cloned()));
    }
}

src/whisker.rs

deleted100644 → 0
+0 −678
Original line number Original line Diff line number Diff line
use core::str::FromStr;

use arrayvec::{ArrayString, ArrayVec};
use half::f16;

pub enum Whisker {
    Identification(Identification),
    Timestamp(Timestamp),
    Gps(Gps),
    Comment(Comment),
    Route(Route),
    Destination(Destination),
}

#[derive(Debug, PartialEq, Eq)]
pub struct Identification {
    pub icon: u16,
    pub callsign: ArrayString<252>,
    pub ssid: u8,
}

impl Identification {
    pub fn new(icon: u16, call: &str, ssid: u8) -> Option<Self> {
        let callsign = ArrayString::from(call).ok()?;

        Some(Self {
            icon,
            callsign,
            ssid,
        })
    }

    /// Returns none if there is not enough space in the buffer
    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        // safe to unwrap because we know the length is <= 252
        let len: u8 = self.callsign.as_bytes().len().try_into().unwrap();
        *buf.get_mut(0)? = len + 3;

        buf.get_mut(1..3)?.copy_from_slice(&self.icon.to_le_bytes());

        let mut len = 3;
        for (i, b) in self.callsign.as_bytes().iter().enumerate() {
            *buf.get_mut(i + 3)? = *b;
            len += 1
        }

        *buf.get_mut(len)? = self.ssid;
        len += 1;

        Some(&buf[0..len])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let len = (*data.first()?).into();

        let icon = u16::from_le_bytes([*data.get(1)?, *data.get(2)?]);

        let callsign = core::str::from_utf8(data.get(3..len)?).ok()?;
        let callsign = ArrayString::from(callsign).ok()?;

        let ssid = *data.get(len)?;

        Some(Self {
            icon,
            callsign,
            ssid,
        })
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct Timestamp(u64);

impl Timestamp {
    /// Returns none if the given unix timestamp doesn't fit in 5 bytes
    pub fn new(unix_time: u64) -> Option<Self> {
        // must fit in 5 bytes
        if unix_time > (1 << (5 * 8)) - 1 {
            return None;
        }

        Some(Self(unix_time))
    }

    pub fn unix_time(&self) -> u64 {
        self.0
    }

    /// Returns none if there is not enough space in the buffer
    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        if buf.len() < 6 {
            return None;
        }

        buf[0] = 5;
        buf[1..6].copy_from_slice(&self.0.to_le_bytes()[0..5]);

        Some(&buf[0..6])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        if data.len() < 6 {
            return None;
        }
        if data[0] != 5 {
            return None;
        }

        let mut extended_data = [0; 8];
        extended_data[0..5].copy_from_slice(&data[1..6]);

        Some(Self(u64::from_le_bytes(extended_data)))
    }
}

#[derive(Debug, PartialEq)]
pub struct Gps {
    latitude: i32,
    longitude: i32,
    pub altitude: f16,
    pub max_error: u8,
    heading: u8,
    pub speed: f16,
}

impl Gps {
    /// Constructs a new GPS whisker.
    ///
    /// latitude: degrees
    /// longitude: degrees
    /// altitude: meters
    /// max_error: maximum error distance from specified lat/lon/alt, meters
    /// heading: direction relative to north, degrees
    /// speed: meters per second
    pub fn new(
        latitude: f64,
        longitude: f64,
        altitude: f16,
        max_error: u8,
        heading: f64,
        speed: f16,
    ) -> Self {
        let latitude = latitude.clamp(-89.999, 89.999);
        let latitude = (latitude * ((1u32 << 31) as f64) / 90.0) as i32;

        let longitude = longitude.clamp(-179.999, 179.999);
        let longitude = (longitude * ((1u32 << 31) as f64) / 180.0) as i32;

        let heading = heading.clamp(0.0, 359.999);
        let heading = (heading * 128.0 / 360.0) as u8;

        Self {
            latitude,
            longitude,
            altitude,
            max_error,
            heading,
            speed,
        }
    }

    pub fn latitude(&self) -> f64 {
        (self.latitude as f64) / (1u32 << 31) as f64 * 90.0
    }

    pub fn longitude(&self) -> f64 {
        (self.longitude as f64) / (1u32 << 31) as f64 * 180.0
    }

    pub fn heading(&self) -> f64 {
        self.heading as f64 / 128.0 * 360.0
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let buf = buf.get_mut(0..15)?;
        buf[0] = 14;
        buf[1..5].copy_from_slice(&self.latitude.to_le_bytes());
        buf[5..9].copy_from_slice(&self.longitude.to_le_bytes());
        buf[9..11].copy_from_slice(&self.altitude.to_le_bytes());
        buf[11] = self.max_error;
        buf[12] = self.heading;
        buf[13..15].copy_from_slice(&self.speed.to_le_bytes());

        Some(buf)
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let data = data.get(0..15)?;
        if data[0] != 14 {
            return None;
        }
        let latitude = i32::from_le_bytes(data[1..5].try_into().unwrap());
        let longitude = i32::from_le_bytes(data[5..9].try_into().unwrap());
        let altitude = f16::from_le_bytes(data[9..11].try_into().unwrap());
        let max_error = data[11];
        let heading = data[12];
        let speed = f16::from_le_bytes(data[13..15].try_into().unwrap());

        Some(Self {
            latitude,
            longitude,
            altitude,
            max_error,
            heading,
            speed,
        })
    }
}

pub struct Comment(ArrayVec<u8, 255>);

impl Comment {
    pub fn new(data: &[u8]) -> Option<Self> {
        let mut av = ArrayVec::new();
        av.try_extend_from_slice(data).ok()?;

        Some(Self(av))
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let len = self.0.len();

        // length must be <= 255, so this is safe
        *buf.get_mut(0)? = len.try_into().unwrap();
        buf.get_mut(1..(len + 1))?.copy_from_slice(&self.0);

        Some(&buf[0..(len + 1)])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let len: usize = (*data.first()?).into();
        let content = data.get(1..(len + 1))?;

        Self::new(content)
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct Route {
    pub max_hops: u8,
    path: ArrayVec<u8, 254>,
    has_future: bool,
}

impl Route {
    pub fn new(max_hops: u8) -> Self {
        let path = ArrayVec::new();

        Self {
            max_hops,
            path,
            has_future: false,
        }
    }

    /// Returns `None` if there isn't enough space for the callsign
    /// Returns `None` if attempting to pushed is_used=true after an existing is_used=false
    /// is_future=false when this callsign has digipeated
    /// is_future=true when this callsign is expected to be part of the route, but isn't yet
    /// see the specs for more information
    pub fn push_callsign(&mut self, callsign: &str, ssid: u8, is_future: bool) -> Option<()> {
        let len = callsign.as_bytes().len() + 2;
        let free_space = self.path.capacity() - self.path.len();

        if len > free_space {
            return None;
        }

        self.has_future = self.has_future || is_future;
        if self.has_future && !is_future {
            return None;
        }

        // safe to unwrap since we already did a length check
        self.path
            .try_extend_from_slice(callsign.as_bytes())
            .unwrap();
        self.path.push(if is_future { 0xFD } else { 0xFF });
        self.path.push(ssid);

        Some(())
    }

    /// Returns `None` if there isn't enough space
    pub fn push_internet(&mut self) -> Option<()> {
        let free_space = self.path.capacity() - self.path.len();

        if free_space < 1 {
            return None;
        }

        self.path.push(0xFE);

        Some(())
    }

    pub fn iter(&'_ self) -> RouteIter<'_> {
        RouteIter::new(self)
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let packet_len = self.path.len() + 1;
        let buf = buf.get_mut(0..(packet_len + 1))?;
        buf[0] = packet_len.try_into().unwrap();
        buf[1] = self.max_hops;
        buf[2..(packet_len + 1)].copy_from_slice(&self.path);

        Some(buf)
    }

    // TODO reject packets that have a 0xFF anywhere after an FD
    pub fn decode(data: &[u8]) -> Option<Self> {
        let len: usize = (*data.first()?).into();
        let data = data.get(1..(len + 1))?;

        let max_hops = data[0];

        let mut path = ArrayVec::new();
        path.try_extend_from_slice(&data[1..]).unwrap();

        let has_future = data[1..].iter().any(|x| *x == 0xFD);

        Some(Self {
            max_hops,
            path,
            has_future,
        })
    }
}

#[derive(Debug, Eq, PartialEq)]
pub enum RouteNode<'a> {
    Internet,
    Identity(&'a str, u8, bool),
}

#[derive(Debug)]
pub struct RouteIter<'a> {
    route: &'a Route,
    i: usize,
}

impl<'a> RouteIter<'a> {
    fn new(route: &'a Route) -> Self {
        Self { route, i: 0 }
    }
}

impl<'a> Iterator for RouteIter<'a> {
    type Item = RouteNode<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.i == self.route.path.len() {
            return None;
        }

        let i_start = self.i;
        self.i += 1;

        if self.route.path[i_start] == 0xFE {
            return Some(RouteNode::Internet);
        }

        while self.route.path[self.i] != 0xFD && self.route.path[self.i] != 0xFF {
            self.i += 1;
        }

        let callsign = core::str::from_utf8(&self.route.path[i_start..self.i]).unwrap();
        let is_future = self.route.path[self.i] == 0xFD;
        self.i += 1;
        let ssid = self.route.path[self.i];
        self.i += 1;

        Some(RouteNode::Identity(callsign, ssid, is_future))
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct Destination {
    ack: u8,
    callsign: ArrayString<253>,
    ssid: u8,
}

impl Destination {
    /// Returns none if the ack number is > 127
    /// Returns none if the dest callsign is too long
    /// Returns none is is_ack is true and ack_num is 0
    pub fn new(is_ack: bool, ack_num: u8, dest_callsign: &str, dest_ssid: u8) -> Option<Self> {
        if ack_num > 127 {
            return None;
        }
        if is_ack && ack_num == 0 {
            return None;
        }
        let ack = if is_ack { (1 << 7) | ack_num } else { ack_num };
        let callsign = ArrayString::from_str(dest_callsign).ok()?;
        let ssid = dest_ssid;

        Some(Self {
            ack,
            callsign,
            ssid,
        })
    }

    pub fn is_ack(&self) -> bool {
        self.ack & (1 << 7) > 0
    }

    pub fn ack_num(&self) -> u8 {
        self.ack
    }

    pub fn callsign(&self) -> &str {
        self.callsign.as_str()
    }

    pub fn ssid(&self) -> u8 {
        self.ssid
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let n = self.callsign.len() + 2;
        *buf.get_mut(0)? = n.try_into().unwrap();
        *buf.get_mut(1)? = self.ack;
        buf.get_mut(2..n)?.copy_from_slice(self.callsign.as_bytes());
        *buf.get_mut(n)? = self.ssid;

        Some(&buf[0..(n + 1)])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let n = data.first()?;
        let call_length: usize = n.checked_sub(2)?.into();

        let ack = *data.get(1)?;
        if ack == 0b10000000 {
            // is_ack is true and # is 0
            return None;
        }

        let callsign = core::str::from_utf8(data.get(2..(call_length + 2))?).ok()?;
        let callsign = ArrayString::from_str(callsign).ok()?;

        let ssid = *data.get(call_length + 2)?;

        Some(Self {
            ack,
            callsign,
            ssid,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ident_e2e() {
        let icon = 2738;
        let call = "VE9ABCDEFGZZ4839-???";
        let ssid = 17;

        let ident = Identification::new(icon, call, ssid).unwrap();
        let mut buf = [0; 256];

        let encoded = ident.encode(&mut buf).unwrap();
        let decoded = Identification::decode(encoded).unwrap();
        assert_eq!(ident, decoded);
        assert_eq!(call, &decoded.callsign);
        assert_eq!(ssid, decoded.ssid);
    }

    #[test]
    fn new_timestamp() {
        let t = 34894;
        assert_eq!(t, Timestamp::new(t).unwrap().unix_time());

        let t = 1 << (5 * 8);
        assert_eq!(None, Timestamp::new(t));
    }

    #[test]
    fn timestamp_e2e() {
        let t = 34894;
        let timestamp = Timestamp::new(t).unwrap();

        let mut buf = [0; 256];
        let encoded = timestamp.encode(&mut buf).unwrap();
        let decoded = Timestamp::decode(encoded).unwrap();
        assert_eq!(timestamp, decoded);
        assert_eq!(t, timestamp.unix_time());
    }

    #[test]
    fn new_gps() {
        let gps = Gps::new(
            90.0,
            -180.0,
            f16::from_f32(45.02),
            24,
            123.45,
            f16::from_f32(12.3),
        );

        assert_eq!(89.99899999704212, gps.latitude());
        assert_eq!(-179.9989999551326, gps.longitude());
        assert_eq!(120.9375, gps.heading());
    }

    #[test]
    fn gps_max_heading() {
        let gps = Gps::new(
            4.0,
            23.45,
            f16::from_f32(45.02),
            24,
            360.0,
            f16::from_f32(12.3),
        );

        assert_eq!(357.1875, gps.heading());
    }

    #[test]
    fn gps_e2e() {
        let gps = Gps::new(
            90.0,
            -180.0,
            f16::from_f32(45.02),
            24,
            123.45,
            f16::from_f32(12.3),
        );

        let mut buf = [0; 256];
        let encoded = gps.encode(&mut buf).unwrap();
        let decoded = Gps::decode(encoded).unwrap();
        assert_eq!(gps, decoded);
    }

    #[test]
    fn comment_e2e() {
        let data = b"Hello world! This is an example comment";
        let comment = Comment::new(data).unwrap();
        let mut buf = [0; 256];
        let encoded = comment.encode(&mut buf).unwrap();
        let decoded = Comment::decode(encoded).unwrap();
        let decoded2 = Comment::decode(&buf).unwrap();
        assert_eq!(data[..], decoded.0[..]);
        assert_eq!(data[..], decoded2.0[..]);
    }

    #[test]
    fn route_push_and_iter() {
        let mut route = Route::new(34);
        route.push_callsign("VE2XYZ", 23, false).unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_callsign("VE9AAAAA", 94, true).unwrap();
        assert!(route.push_callsign("VE9AAAAA", 94, false).is_none());

        // too long
        assert!(route
            .push_callsign(
                "lsdfjslkdfjlksdjflksfjsdklfjsdklfjsdklfjsdklfjsklfsef;jklsdfjkl;sdf;klsdf;klsjdfJSDJFSKL:DFJDSL:KFskldfj;slkdfjsdkl;fjdskl;fjsdfl;kjsdfl;ksdjfkl;ssdfl;kjsdfl;ksdjf;sdklsd;lfkjsdlfk;jsdl;fkjsd;klfjsd;fljsf;oidfjgwper0tujdfgndfjkl;gjnergjol;kehfgo;dijge;oghdfkl;gjdfkl;gjdeior;lgjedr;ioghjdorighndeklo;grjiop[",
                20,
                true,
            ).is_none());

        route
            .push_callsign("This is the last callsign", 0, true)
            .unwrap();
        route.push_internet().unwrap();

        let mut iter = route.iter();
        assert_eq!(
            RouteNode::Identity("VE2XYZ", 23, false),
            iter.next().unwrap()
        );
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(
            RouteNode::Identity("VE9AAAAA", 94, true),
            iter.next().unwrap()
        );
        assert_eq!(
            RouteNode::Identity("This is the last callsign", 0, true),
            iter.next().unwrap()
        );
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(None, iter.next());

        assert_eq!(34, route.max_hops);
    }

    #[test]
    fn route_e2e() {
        let mut route = Route::new(34);
        route.push_callsign("VE2XYZ", 23, false).unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_callsign("VE9AAAAA", 94, true).unwrap();
        route
            .push_callsign("This is the last callsign", 0, true)
            .unwrap();
        route.push_internet().unwrap();

        let mut buf = [0; 256];
        let encoded = route.encode(&mut buf).unwrap();
        let decoded = Route::decode(encoded).unwrap();
        assert_eq!(route, decoded);
    }

    // verify examples in the standard doc
    #[test]
    fn route_doc_examples() {
        let mut ex1 = Route::new(4);
        ex1.push_callsign("VE1ABC", 0, false);
        ex1.push_callsign("VE2DEF", 234, false);
        ex1.push_callsign("VE3XYZ", 14, false);

        let mut buf = [0; 256];
        let encoded = ex1.encode(&mut buf).unwrap();
        assert_eq!(
            &[
                0x19, 0x04, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0x56, 0x45, 0x32, 0x44,
                0x45, 0x46, 0xFF, 0xEA, 0x56, 0x45, 0x33, 0x58, 0x59, 0x5A, 0xFF, 0x0E
            ],
            &encoded
        );

        let mut ex2 = Route::new(3);
        ex2.push_callsign("VE1ABC", 0, false);
        ex2.push_internet();
        ex2.push_callsign("VE2DEF", 234, false);
        ex2.push_internet();
        ex2.push_callsign("VE3XYZ", 14, false);

        let encoded = ex2.encode(&mut buf).unwrap();
        assert_eq!(
            &[
                0x1B, 0x03, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0xFE, 0x56, 0x45, 0x32,
                0x44, 0x45, 0x46, 0xFF, 0xEA, 0xFE, 0x56, 0x45, 0x33, 0x58, 0x59, 0x5A, 0xFF, 0x0E
            ],
            &encoded
        );

        let mut ex3 = Route::new(0);
        ex3.push_callsign("VE1ABC", 0, false);
        ex3.push_internet();
        ex3.push_internet();

        let encoded = ex3.encode(&mut buf).unwrap();
        assert_eq!(
            &[0x0B, 0x00, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0xFE, 0xFE],
            &encoded
        );
    }

    #[test]
    fn dest_e2e() {
        assert_eq!(None, Destination::new(true, 0, "dest", 36));
        assert_eq!(None, Destination::new(false, 128, "abc", 0));
        assert!(Destination::new(false, 0, "anc", 8).is_some());

        let dest = Destination::new(true, 64, "mrow", 31).unwrap();
        let mut buf = [0; 256];
        let encoded = dest.encode(&mut buf).unwrap();
        let decoded = Destination::decode(encoded).unwrap();
        assert_eq!(dest, decoded);
    }
}
+30 −0
Original line number Original line Diff line number Diff line
use arrayvec::ArrayVec;

#[derive(Debug, Clone)]
pub struct Arbitrary(pub ArrayVec<u8, 255>);

impl Arbitrary {
    pub fn new(data: &[u8]) -> Option<Self> {
        let mut av = ArrayVec::new();
        av.try_extend_from_slice(data).ok()?;

        Some(Self(av))
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let len = self.0.len();

        // length must be <= 255, so this is safe
        *buf.get_mut(0)? = len.try_into().unwrap();
        buf.get_mut(1..(len + 1))?.copy_from_slice(&self.0);

        Some(&buf[0..(len + 1)])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let len: usize = (*data.first()?).into();
        let content = data.get(1..(len + 1))?;

        Self::new(content)
    }
}

src/whisker/comment.rs

0 → 100644
+34 −0
Original line number Original line Diff line number Diff line
use arrayvec::ArrayVec;

#[derive(Debug)]
pub struct Comment(ArrayVec<u8, 255>);

impl Comment {
    pub fn new(data: &[u8]) -> Option<Self> {
        let mut av = ArrayVec::new();
        av.try_extend_from_slice(data).ok()?;

        Some(Self(av))
    }

    pub fn internal_data(&self) -> &ArrayVec<u8, 255> {
        &self.0
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let len = self.0.len();

        // length must be <= 255, so this is safe
        *buf.get_mut(0)? = len.try_into().unwrap();
        buf.get_mut(1..(len + 1))?.copy_from_slice(&self.0);

        Some(&buf[0..(len + 1)])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let len: usize = (*data.first()?).into();
        let content = data.get(1..(len + 1))?;

        Self::new(content)
    }
}
+81 −0
Original line number Original line Diff line number Diff line
use core::str::FromStr;

use arrayvec::ArrayString;

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Destination {
    ack: u8,
    callsign: ArrayString<253>,
    ssid: u8,
}

impl Destination {
    /// Returns none if the ack number is > 127
    /// Returns none if the dest callsign is too long
    /// Returns none is is_ack is true and ack_num is 0
    pub fn new(is_ack: bool, ack_num: u8, dest_callsign: &str, dest_ssid: u8) -> Option<Self> {
        if ack_num > 127 {
            return None;
        }
        if is_ack && ack_num == 0 {
            return None;
        }
        let ack = if is_ack { (1 << 7) | ack_num } else { ack_num };
        let callsign = ArrayString::from_str(dest_callsign).ok()?;
        let ssid = dest_ssid;

        Some(Self {
            ack,
            callsign,
            ssid,
        })
    }

    pub fn is_ack(&self) -> bool {
        self.ack & (1 << 7) > 0
    }

    pub fn ack_num(&self) -> u8 {
        self.ack & !(1 << 7)
    }

    pub fn callsign(&self) -> &str {
        self.callsign.as_str()
    }

    pub fn ssid(&self) -> u8 {
        self.ssid
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let n = self.callsign.len() + 2;
        *buf.get_mut(0)? = n.try_into().unwrap();
        *buf.get_mut(1)? = self.ack;
        buf.get_mut(2..n)?.copy_from_slice(self.callsign.as_bytes());
        *buf.get_mut(n)? = self.ssid;

        Some(&buf[0..(n + 1)])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let n = data.first()?;
        let call_length: usize = n.checked_sub(2)?.into();

        let ack = *data.get(1)?;
        if ack == 0b10000000 {
            // is_ack is true and # is 0
            return None;
        }

        let callsign = core::str::from_utf8(data.get(2..(call_length + 2))?).ok()?;
        let callsign = ArrayString::from_str(callsign).ok()?;

        let ssid = *data.get(call_length + 2)?;

        Some(Self {
            ack,
            callsign,
            ssid,
        })
    }
}

src/whisker/gps.rs

0 → 100644
+167 −0
Original line number Original line Diff line number Diff line
use core::fmt::{Debug, Display};
use half::f16;

#[derive(PartialEq, Clone)]
pub struct Gps {
    latitude: i32,
    longitude: i32,
    pub altitude: f16,
    pub max_error: u8,
    heading: u8,
    pub speed: f16,
}

impl Gps {
    /// Constructs a new GPS whisker.
    ///
    /// latitude: degrees
    /// longitude: degrees
    /// altitude: meters
    /// max_error: maximum error distance from specified lat/lon/alt, meters
    /// heading: direction relative to north, degrees
    /// speed: meters per second
    pub fn new(
        latitude: f64,
        longitude: f64,
        altitude: f16,
        max_error: u8,
        heading: f64,
        speed: f16,
    ) -> Self {
        let latitude = latitude.clamp(-89.999, 89.999);
        let latitude = (latitude * ((1u32 << 31) as f64) / 90.0) as i32;

        let longitude = longitude.clamp(-179.999, 179.999);
        let longitude = (longitude * ((1u32 << 31) as f64) / 180.0) as i32;

        let heading = if heading >= 0.0 {
            heading % 360.0
        } else {
            // slightly hacky no-std floor
            let factor = (-heading / 360.0) as u32 as f64;
            360.0 * (1.0 + factor) + heading
        };
        let heading = round(heading * 128.0 / 180.0) as u8;

        Self {
            latitude,
            longitude,
            altitude,
            max_error,
            heading,
            speed,
        }
    }

    pub fn latitude(&self) -> f64 {
        (self.latitude as f64) / (1u32 << 31) as f64 * 90.0
    }

    pub fn longitude(&self) -> f64 {
        (self.longitude as f64) / (1u32 << 31) as f64 * 180.0
    }

    pub fn heading(&self) -> f64 {
        self.heading as f64 / 128.0 * 180.0
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let buf = buf.get_mut(0..15)?;
        buf[0] = 14;
        buf[1..5].copy_from_slice(&self.latitude.to_le_bytes());
        buf[5..9].copy_from_slice(&self.longitude.to_le_bytes());
        buf[9..11].copy_from_slice(&self.altitude.to_le_bytes());
        buf[11] = self.max_error;
        buf[12] = self.heading;
        buf[13..15].copy_from_slice(&self.speed.to_le_bytes());

        Some(buf)
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let data = data.get(0..15)?;
        if data[0] != 14 {
            return None;
        }
        let latitude = i32::from_le_bytes(data[1..5].try_into().unwrap());
        let longitude = i32::from_le_bytes(data[5..9].try_into().unwrap());
        let altitude = f16::from_le_bytes(data[9..11].try_into().unwrap());
        let max_error = data[11];
        let heading = data[12];
        let speed = f16::from_le_bytes(data[13..15].try_into().unwrap());

        Some(Self {
            latitude,
            longitude,
            altitude,
            max_error,
            heading,
            speed,
        })
    }
}

impl Debug for Gps {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        f.debug_struct("Gps")
            .field("latitude", &DebugUnits(self.latitude(), "°"))
            .field("longitude", &DebugUnits(self.longitude(), "°"))
            .field("altitude", &DebugUnits(self.altitude, " m"))
            .field("max_error", &DebugUnits(self.max_error, " m"))
            .field("heading", &DebugUnits(self.heading(), "°"))
            .field("speed", &DebugUnits(self.speed, " m/s"))
            .finish()
    }
}

struct DebugUnits<'a, T>(T, &'a str);

impl<T: Display> Debug for DebugUnits<'_, T> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}{}", self.0, self.1)
    }
}

// no-std and it's not worth bringing in a library for this
fn round(v: f64) -> u32 {
    let floor = v as u32;
    let delta = v - floor as f64;
    if delta <= 0.5 {
        floor
    } else {
        floor + 1
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn heading_is_correct() {
        let gps = Gps::new(0.0, 0.0, f16::from_f32(0.0), 0, 359.0, f16::from_f32(0.0));
        assert_eq!(gps.heading, 255);

        let gps = Gps::new(0.0, 0.0, f16::from_f32(0.0), 0, 0.0, f16::from_f32(0.0));
        assert_eq!(gps.heading, 0);

        let gps = Gps::new(0.0, 0.0, f16::from_f32(0.0), 0, -20.0, f16::from_f32(0.0));
        assert_eq!(gps.heading, 242);

        let gps = Gps::new(0.0, 0.0, f16::from_f32(0.0), 0, 719.0, f16::from_f32(0.0));
        assert_eq!(gps.heading, 255);

        let gps = Gps::new(0.0, 0.0, f16::from_f32(0.0), 0, 180.0, f16::from_f32(0.0));
        assert_eq!(gps.heading, 128);

        let gps = Gps::new(0.0, 0.0, f16::from_f32(0.0), 0, 540.0, f16::from_f32(0.0));
        assert_eq!(gps.heading, 128);
    }

    #[test]
    fn debug_printing() {
        let gps = Gps::new(0.0, 0.0, f16::from_f32(0.0), 0, 359.0, f16::from_f32(0.0));
        let x = format!("{gps:?}");
        assert_eq!(x,"Gps { latitude: 0°, longitude: 0°, altitude: 0 m, max_error: 0 m, heading: 358.59375°, speed: 0 m/s }")
    }
}
+57 −0
Original line number Original line Diff line number Diff line
use arrayvec::ArrayString;

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Identification {
    pub callsign: ArrayString<252>,
    pub ssid: u8,
    pub icon: u16,
}

impl Identification {
    pub fn new(call: &str, ssid: u8, icon: u16) -> Option<Self> {
        let callsign = ArrayString::from(call).ok()?;

        Some(Self {
            icon,
            callsign,
            ssid,
        })
    }

    /// Returns none if there is not enough space in the buffer
    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        // safe to unwrap because we know the length is <= 252
        let len: u8 = self.callsign.as_bytes().len().try_into().unwrap();
        *buf.get_mut(0)? = len + 3;

        buf.get_mut(1..3)?.copy_from_slice(&self.icon.to_le_bytes());

        let mut len = 3;
        for (i, b) in self.callsign.as_bytes().iter().enumerate() {
            *buf.get_mut(i + 3)? = *b;
            len += 1
        }

        *buf.get_mut(len)? = self.ssid;
        len += 1;

        Some(&buf[0..len])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let len = (*data.first()?).into();

        let icon = u16::from_le_bytes([*data.get(1)?, *data.get(2)?]);

        let callsign = core::str::from_utf8(data.get(3..len)?).ok()?;
        let callsign = ArrayString::from(callsign).ok()?;

        let ssid = *data.get(len)?;

        Some(Self {
            icon,
            callsign,
            ssid,
        })
    }
}