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
24 files
+ 1989
225
Compare changes
  • Side-by-side
  • Inline

Files

.gitlab-ci.yml

0 → 100644
+11 −0
Original line number 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
+6 −4
Original line number Diff line number Diff line
[package]
name = "ham-cats"
version = "0.1.0"
version = "0.2.2"
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]
arrayvec = { version = "0.7.4", default-features = false }
@@ -12,6 +14,6 @@ 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 }
labrador-ldpc = "1.1"
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 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.
+2 −4
Original line number Diff line number Diff line
@@ -96,7 +96,7 @@ dependencies = [

[[package]]
name = "ham-cats"
version = "0.1.0"
version = "0.2.0"
dependencies = [
 "arrayvec",
 "bitvec",
@@ -133,9 +133,7 @@ dependencies = [

[[package]]
name = "labrador-ldpc"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ff4a0568f6322b64e06b6a6a0810e2f64670aa325f6e9dca24084e99917f459"
version = "1.1.1"

[[package]]
name = "libc"
+6 −1
Original line number Diff line number Diff line
@@ -30,9 +30,14 @@ 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
Original line number Diff line number Diff line
@@ -4,5 +4,7 @@ use libfuzzer_sys::fuzz_target;
use ham_cats::packet::Packet;

fuzz_target!(|data: &[u8]| {
    let _ = Packet::<1024>::fully_decode(data);
    let mut buf = [0; 1024];

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

use ham_cats::packet::Packet;
use std::convert::TryInto;
use ham_cats::{buffer::Buffer, packet::Packet};

fuzz_target!(|data: &[u8]| {
    if let Ok(data) = data.try_into() {
        let _ = Packet::<1024>::semi_decode(data);
    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);
    }
});
Original line number Diff line number Diff line
#![no_main]
use libfuzzer_sys::fuzz_target;

use ham_cats::packet::Packet;
use std::convert::TryInto;
use ham_cats::{buffer::Buffer, packet::Packet};

fuzz_target!(|data: &[u8]| {
    if let Ok(data) = data.try_into() {
        let _ = Packet::<1024>::decode(data);
    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 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);
});
+69 −19
Original line number Diff line number Diff line
use core::{borrow::Borrow, ops::Deref};
use core::ops::{Deref, DerefMut};

pub(crate) struct BufferOverflow;
#[derive(Debug)]
pub struct BufferOverflow;

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

impl<'a, const N: usize> Buffer<'a, N> {
    pub fn new(data: &'a mut [u8; N], i: usize) -> Self {
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 as_slice(&self) -> &'a [u8] {
            &self.data[..self.i]
    pub fn new_full(data: &'a mut [T; N]) -> Self {
        let i = data.len();

        Self::new(data, i)
    }

        pub fn as_slice_mut(&mut self) -> &'a [u8] {
            &mut self.data[..self.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: u8) -> Result<(), BufferOverflow> {
    pub fn try_push(&mut self, v: T) -> Result<(), BufferOverflow> {
        if self.i == N {
            return Err(BufferOverflow);
        }
@@ -35,7 +44,11 @@ impl<'a, const N: usize> Buffer<'a, N> {
        Ok(())
    }

    pub fn try_extend_from_slice(&mut self, other: &[u8]) -> Result<(), BufferOverflow> {
    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);
        }
@@ -46,7 +59,11 @@ impl<'a, const N: usize> Buffer<'a, N> {
        Ok(())
    }

    pub fn pop(&mut self) -> Option<u8> {
    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;
        }
@@ -55,18 +72,51 @@ impl<'a, const N: usize> Buffer<'a, N> {

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

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

        self.i = new_len;
    }

impl<'a, const N: usize> From<&'a mut [u8; N]> for Buffer<'a, N> {
    fn from(data: &'a mut [u8; N]) -> Self {
    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<'a, const N: usize> Deref for Buffer<'a, N> {
    type Target = [u8];
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]
    }
}
+45 −0
Original line number Diff line number Diff line
@@ -41,3 +41,48 @@ pub enum CommentError {
    #[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 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
    }
}
+74 −11
Original line number Diff line number Diff line
use arrayvec::{ArrayVec, CapacityError};
use bitvec::prelude::*;
use labrador_ldpc::decoder::DecodeFrom;

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

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

@@ -33,10 +36,10 @@ pub(crate) fn interleave<const N: usize>(

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

    let mut out: ArrayVec<u8, N> = ArrayVec::new();
    for _ in 0..data.len() {
        out.try_push(0)?;
    }
@@ -55,26 +58,86 @@ pub(crate) fn uninterleave<const N: usize>(
        }
    }

    Ok(out)
    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 orig =
            ArrayVec::try_from([0x84, 0x73, 0x12, 0xA3, 0xFF, 0x00, 0xC2, 0x1B, 0x77]).unwrap();
        let mut interleaved: ArrayVec<u8, 10> = ArrayVec::new();
        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 uninterleaved = uninterleave(&interleaved[1..]).unwrap();
        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, uninterleaved);
        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);
            }
        }
    }
}
+178 −36
Original line number Diff line number Diff line
use arrayvec::ArrayVec;
use labrador_ldpc::LDPCCode;
use labrador_ldpc::{decoder::DecodeFrom, LDPCCode};

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

@@ -37,6 +36,27 @@ macro_rules! dec_chunk {
    };
}

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;
@@ -84,7 +104,7 @@ pub(crate) fn encode<const N: usize>(data: &mut Buffer<N>) -> Result<(), EncodeE
    Ok(())
}

pub(crate) fn decode<const N: usize>(data_av: &mut ArrayVec<u8, N>) -> Option<()> {
pub(crate) fn decode<const N: usize>(data_av: &mut Buffer<N>) -> Option<()> {
    if data_av.len() < 2 {
        return None;
    }
@@ -154,28 +174,125 @@ pub(crate) fn decode<const N: usize>(data_av: &mut ArrayVec<u8, N>) -> Option<()
    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 arrayvec::ArrayVec;
    use crate::soft_bit::FromHardBit;
    use bitvec::{order::Msb0, view::BitView};

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

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

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

        encode(&mut data).unwrap();
@@ -185,51 +302,50 @@ mod tests {

    #[test]
    fn basic_encode_decode_short() {
        let mut data: ArrayVec<u8, 32> = ArrayVec::new();
        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();
        let orig = data.clone_backing(&mut buf2);

        encode(&mut data).unwrap();

        decode(&mut data).unwrap();

        assert_eq!(orig, data);
        assert_eq!(*orig, *data);
    }

    #[test]
    fn basic_encode_decode() {
        let mut data: ArrayVec<u8, 8191> = ArrayVec::new();
        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*#$)(*#@)"
                    .iter()
                    .cloned(),
            );
            data.extend(b"This is a test packet. jsalksjd093809324JASLD:LKD*#$)(*#@)");
        }
        let orig = data.clone();
        let orig = data.clone_backing(&mut buf2);

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

        decode(&mut data).unwrap();

        assert_eq!(orig, data);
        assert_eq!(*orig, *data);
    }

    #[test]
    fn encode_decode_with_bit_flips() {
        let mut data: ArrayVec<u8, 8191> = ArrayVec::new();
        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"
                    .iter()
                    .cloned(),
            );
            data.extend(b"jsalksjd093809324JASLD:LKD*#$)(*#@) Another test packet");
        }
        let orig = data.clone();
        let orig = data.clone_backing(&mut buf2);

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

        data[234] ^= 0x55;
        data[0] ^= 0xAA;
@@ -237,6 +353,32 @@ mod tests {

        decode(&mut data).unwrap();

        assert_eq!(orig, data);
        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);
    }
}
+4 −2
Original line number Diff line number Diff line
#![no_std]
#![cfg_attr(not(test), no_std)]

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 whitener;

mod buffer;
mod utf8;
+322 −51

File changed.

Preview size limit exceeded, changes collapsed.

src/soft_bit.rs

0 → 100644
+40 −0
Original line number 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());
    }
}
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ impl Destination {
    }

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

    pub fn callsign(&self) -> &str {
Original line number Diff line number Diff line
use core::fmt::{Debug, Display};
use half::f16;

#[derive(Debug, PartialEq, Clone)]
#[derive(PartialEq, Clone)]
pub struct Gps {
    latitude: i32,
    longitude: i32,
@@ -33,8 +34,14 @@ impl Gps {
        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;
        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,
@@ -55,7 +62,7 @@ impl Gps {
    }

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

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
@@ -93,3 +100,68 @@ impl Gps {
        })
    }
}

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 }")
    }
}
Original line number Diff line number Diff line
@@ -2,13 +2,13 @@ use arrayvec::ArrayString;

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

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

        Some(Self {
+135 −37
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ mod comment;
mod destination;
mod gps;
mod identification;
mod node_info;
mod route;
mod timestamp;
mod unknown;
@@ -15,7 +16,8 @@ pub use self::{
    destination::Destination,
    gps::Gps,
    identification::Identification,
    route::{Route, RouteIter, RouteNode},
    node_info::{NodeInfo, NodeInfoBuilder},
    route::{PastHop, Route, RouteHop, RouteIter},
    timestamp::Timestamp,
    unknown::Unknown,
};
@@ -27,6 +29,7 @@ pub(crate) const COMMENT_TYPE: u8 = 0x03;
pub(crate) const ROUTE_TYPE: u8 = 0x04;
pub(crate) const DESTINATION_TYPE: u8 = 0x05;
pub(crate) const ARBITRARY_TYPE: u8 = 0x06;
pub(crate) const NODE_INFO_TYPE: u8 = 0x09;

#[derive(Debug)]
pub enum Whisker {
@@ -37,6 +40,7 @@ pub enum Whisker {
    Route(Route),
    Destination(Destination),
    Arbitrary(Arbitrary),
    NodeInfo(NodeInfo),
    Unknown(Unknown),
}

@@ -137,6 +141,9 @@ impl<'a> WhiskerIter<'a> {
            ARBITRARY_TYPE => Whisker::Arbitrary(
                Arbitrary::decode(data).ok_or(DecodeError::MalformedWhisker { position })?,
            ),
            NODE_INFO_TYPE => Whisker::NodeInfo(
                NodeInfo::decode(data).ok_or(DecodeError::MalformedWhisker { position })?,
            ),

            // safe to unwrap because we know len has to be 255 or less, since it's a u8
            whisker_type => {
@@ -148,7 +155,7 @@ impl<'a> WhiskerIter<'a> {
    }
}

impl<'a> Iterator for WhiskerIter<'a> {
impl Iterator for WhiskerIter<'_> {
    type Item = Result<Whisker, DecodeError>;

    fn next(&mut self) -> Option<Self::Item> {
@@ -172,7 +179,7 @@ impl<'a> ValidatedWhiskerIter<'a> {
    }
}

impl<'a> Iterator for ValidatedWhiskerIter<'a> {
impl Iterator for ValidatedWhiskerIter<'_> {
    type Item = Whisker;

    fn next(&mut self) -> Option<Self::Item> {
@@ -184,7 +191,10 @@ impl<'a> Iterator for ValidatedWhiskerIter<'a> {
mod tests {
    use half::f16;

    use crate::whisker::route::RouteNode;
    use crate::{
        identity::Identity,
        whisker::route::{PastHop, RouteHop},
    };

    use super::*;

@@ -194,7 +204,7 @@ mod tests {
        let call = "VE9ABCDEFGZZ4839-???";
        let ssid = 17;

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

        let encoded = ident.encode(&mut buf).unwrap();
@@ -238,7 +248,7 @@ mod tests {

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

    #[test]
@@ -248,11 +258,58 @@ mod tests {
            23.45,
            f16::from_f32(45.02),
            24,
            360.0,
            359.0,
            f16::from_f32(12.3),
        );

        assert_eq!(358.59375, gps.heading());

        let gps = Gps::new(
            4.0,
            23.45,
            f16::from_f32(45.02),
            24,
            957.47,
            f16::from_f32(12.3),
        );

        assert_eq!(957.65625 - 360.0 * 2.0, gps.heading());
    }

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

        assert_eq!(0.0, gps.heading());

        let gps = Gps::new(
            4.0,
            23.45,
            f16::from_f32(45.02),
            24,
            -22.0,
            f16::from_f32(12.3),
        );

        assert_eq!(337.5, gps.heading());

        let gps = Gps::new(
            4.0,
            23.45,
            f16::from_f32(45.02),
            24,
            -1206.0,
            f16::from_f32(12.3),
        );

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

    #[test]
@@ -287,43 +344,47 @@ mod tests {
    #[test]
    fn route_push_and_iter() {
        let mut route = Route::new(34);
        route.push_callsign("VE2XYZ", 23, false).unwrap();
        route
            .push_past(PastHop::new(Identity::new("VE2XYZ", 23), None))
            .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());
        route.push_future(Identity::new("VE9AAAAA", 94)).unwrap();
        // past after future - not allowed
        assert!(route
            .push_past(PastHop::new(Identity::new("VE9AAAAA", 94), None))
            .is_none());

        // too long
        assert!(route
            .push_callsign(
            .push_future(Identity::new(
                "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());
            )).is_none());

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

        let mut iter = route.iter();
        assert_eq!(
            RouteNode::Identity("VE2XYZ", 23, false),
            RouteHop::Past(PastHop::new(Identity::new("VE2XYZ", 23), None)),
            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!(RouteHop::Internet, iter.next().unwrap());
        assert_eq!(RouteHop::Internet, iter.next().unwrap());
        assert_eq!(RouteHop::Internet, iter.next().unwrap());
        assert_eq!(
            RouteNode::Identity("VE9AAAAA", 94, true),
            RouteHop::Future(Identity::new("VE9AAAAA", 94)),
            iter.next().unwrap()
        );
        assert_eq!(
            RouteNode::Identity("This is the last callsign", 0, true),
            RouteHop::Future(Identity::new("This is the last callsign", 0)),
            iter.next().unwrap()
        );
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(RouteHop::Internet, iter.next().unwrap());
        assert_eq!(None, iter.next());

        assert_eq!(34, route.max_hops);
@@ -332,13 +393,15 @@ mod tests {
    #[test]
    fn route_e2e() {
        let mut route = Route::new(34);
        route.push_callsign("VE2XYZ", 23, false).unwrap();
        route
            .push_past(PastHop::new(Identity::new("VE2XYZ", 23), Some(0.0)))
            .unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_callsign("VE9AAAAA", 94, true).unwrap();
        route.push_future(Identity::new("VE9AAAAA", 94)).unwrap();
        route
            .push_callsign("This is the last callsign", 0, true)
            .push_future(Identity::new("This is the last callsign", 0))
            .unwrap();
        route.push_internet().unwrap();

@@ -352,44 +415,70 @@ mod tests {
    #[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);
        ex1.push_past(PastHop::new(Identity::new("VE1ABC", 0), Some(-96.0)))
            .unwrap();
        ex1.push_past(PastHop::new(Identity::new("VE2DEF", 234), Some(-13.0)))
            .unwrap();
        ex1.push_past(PastHop::new(Identity::new("VE3XYZ", 14), Some(-106.0)))
            .unwrap();

        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
                0x1C, 0x04, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0x60, 0x56, 0x45, 0x32,
                0x44, 0x45, 0x46, 0xFF, 0xEA, 0xDC, 0x56, 0x45, 0x33, 0x58, 0x59, 0x5A, 0xFF, 0x0E,
                0x51
            ],
            &encoded
        );

        let mut ex1_5 = Route::new(4);
        ex1_5
            .push_past(PastHop::new(Identity::new("VE1ABC", 0), Some(-96.0)))
            .unwrap();
        ex1_5.push_future(Identity::new("VE2DEF", 234)).unwrap();
        ex1_5.push_future(Identity::new("VE3XYZ", 14)).unwrap();

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

        let mut ex2 = Route::new(3);
        ex2.push_callsign("VE1ABC", 0, false);
        ex2.push_past(PastHop::new(Identity::new("VE1ABC", 0), None))
            .unwrap();
        ex2.push_internet();
        ex2.push_callsign("VE2DEF", 234, false);
        ex2.push_past(PastHop::new(Identity::new("VE2DEF", 234), Some(-86.5)))
            .unwrap();
        ex2.push_internet();
        ex2.push_callsign("VE3XYZ", 14, false);
        ex2.push_past(PastHop::new(Identity::new("VE3XYZ", 14), Some(-65.0)))
            .unwrap();

        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
                0x1E, 0x03, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0x00, 0xFE, 0x56, 0x45,
                0x32, 0x44, 0x45, 0x46, 0xFF, 0xEA, 0x6E, 0xFE, 0x56, 0x45, 0x33, 0x58, 0x59, 0x5A,
                0xFF, 0x0E, 0x8E
            ],
            &encoded
        );

        let mut ex3 = Route::new(0);
        ex3.push_callsign("VE1ABC", 0, false);
        ex3.push_past(PastHop::new(Identity::new("VE1ABC", 0), Some(-42.5)))
            .unwrap();
        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],
            &[0x0C, 0x00, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0xB0, 0xFE, 0xFE],
            &encoded
        );
    }
@@ -407,6 +496,15 @@ mod tests {
        assert_eq!(dest, decoded);
    }

    #[test]
    fn dest_ack() {
        let dest = Destination::new(true, 84, "abc", 17).unwrap();
        assert_eq!(84, dest.ack_num());
        assert!(dest.is_ack());
        assert_eq!("abc", dest.callsign());
        assert_eq!(17, dest.ssid());
    }

    #[test]
    fn arbitrary_e2e() {
        let data = b"Hello world! This is an example comment";
+466 −0
Original line number Diff line number Diff line
#[derive(Copy, Clone, Debug)]
pub struct NodeInfo {
    hardware_id: Option<u16>,
    software_id: Option<u8>,
    uptime: Option<u32>,
    antenna_height: Option<u8>,
    antenna_gain: Option<u8>,
    tx_power: Option<u8>,
    voltage: Option<u8>,
    xcvr_temperature: Option<i8>,
    battery_charge: Option<u8>,
    altitude: Option<f32>,
    balloon: bool,
    ambient_temperature: Option<i8>,
    ambient_humidity: Option<u8>,
    ambient_pressure: Option<u16>,
}

impl NodeInfo {
    pub fn builder() -> NodeInfoBuilder {
        NodeInfoBuilder::default()
    }

    pub fn hardware_id(&self) -> Option<u16> {
        self.hardware_id
    }

    pub fn software_id(&self) -> Option<u8> {
        self.software_id
    }

    /// Seconds
    pub fn uptime(&self) -> Option<u32> {
        self.uptime
    }

    /// Meters
    pub fn antenna_height(&self) -> Option<u8> {
        self.antenna_height
    }

    /// dBi
    pub fn antenna_gain(&self) -> Option<f64> {
        self.antenna_gain.map(|g| g as f64 / 4.0)
    }

    /// dBm
    pub fn tx_power(&self) -> Option<f64> {
        self.tx_power.map(|p| p as f64 / 4.0)
    }

    /// Volts
    pub fn voltage(&self) -> Option<f64> {
        self.voltage.map(|v| v as f64 / 10.0)
    }

    /// Degrees C
    pub fn xcvr_temperature(&self) -> Option<i8> {
        self.xcvr_temperature
    }

    /// Percent
    pub fn battery_charge(&self) -> Option<f64> {
        self.battery_charge.map(|b| b as f64 / 2.55)
    }

    /// Higher precision altitude than what's available in the GPS whisker. Takes precedent if set. Useful for high altitude balloons.
    /// Meters
    pub fn altitude(&self) -> Option<f32> {
        self.altitude
    }

    /// If true, this whisker was emitted by a high altitude balloon payload
    pub fn is_balloon(&self) -> bool {
        self.balloon
    }

    /// Degrees C
    pub fn ambient_temperature(&self) -> Option<i8> {
        self.ambient_temperature
    }

    /// Relative humidity, percent
    pub fn ambient_humidity(&self) -> Option<f64> {
        self.ambient_humidity.map(|x| x as f64 / 2.55)
    }

    /// Decapascal (daPa)
    pub fn ambient_pressure(&self) -> Option<u16> {
        self.ambient_pressure
    }

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

        let mut i = 4;

        // TODO these could be macros
        if let Some(x) = self.hardware_id {
            bitmask |= 1;

            buf.get_mut(i..(i + 2))?.copy_from_slice(&x.to_le_bytes());
            i += 2;
        }

        if let Some(x) = self.software_id {
            bitmask |= 2;

            *buf.get_mut(i)? = x;
            i += 1;
        }

        if let Some(x) = self.uptime {
            bitmask |= 4;

            buf.get_mut(i..(i + 4))?.copy_from_slice(&x.to_le_bytes());
            i += 4;
        }

        if let Some(x) = self.antenna_height {
            bitmask |= 8;

            *buf.get_mut(i)? = x;
            i += 1;
        }

        if let Some(x) = self.antenna_gain {
            bitmask |= 16;

            *buf.get_mut(i)? = x;
            i += 1;
        }

        if let Some(x) = self.tx_power {
            bitmask |= 32;

            *buf.get_mut(i)? = x;
            i += 1;
        }

        if let Some(x) = self.voltage {
            bitmask |= 64;

            *buf.get_mut(i)? = x;
            i += 1;
        }

        if let Some(x) = self.xcvr_temperature {
            bitmask |= 128;

            *buf.get_mut(i)? = x.to_le_bytes()[0];
            i += 1;
        }

        if let Some(x) = self.battery_charge {
            bitmask |= 256;

            *buf.get_mut(i)? = x;
            i += 1;
        }

        if let Some(x) = self.altitude {
            bitmask |= 512;

            buf.get_mut(i..(i + 4))?.copy_from_slice(&x.to_le_bytes());
            i += 4;
        }

        if self.balloon {
            bitmask |= 1024;
        }

        if let Some(x) = self.ambient_temperature {
            bitmask |= 2048;

            *buf.get_mut(i)? = x.to_le_bytes()[0];
            i += 1;
        }

        if let Some(x) = self.ambient_humidity {
            bitmask |= 4096;

            *buf.get_mut(i)? = x;
            i += 1;
        }

        if let Some(x) = self.ambient_pressure {
            bitmask |= 8192;

            buf.get_mut(i..(i + 2))?.copy_from_slice(&x.to_le_bytes());
            i += 2;
        }

        buf[0] = (i - 1).try_into().ok()?;
        buf[1..4].copy_from_slice(&bitmask.to_be_bytes()[1..]);

        Some(&buf[..i])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let bitmask = u32::from_be_bytes([0, *data.get(1)?, *data.get(2)?, *data.get(3)?]);

        let mut builder = NodeInfoBuilder::default();
        let mut i = 4;

        if bitmask & 1 > 0 {
            builder.hardware_id = Some(u16::from_le_bytes([*data.get(i)?, *data.get(i + 1)?]));
            i += 2;
        }

        if bitmask & 2 > 0 {
            builder.software_id = Some(*data.get(i)?);
            i += 1;
        }

        if bitmask & 4 > 0 {
            builder.uptime = Some(u32::from_le_bytes([
                *data.get(i)?,
                *data.get(i + 1)?,
                *data.get(i + 2)?,
                *data.get(i + 3)?,
            ]));

            i += 4;
        }

        if bitmask & 8 > 0 {
            builder.antenna_height = Some(*data.get(i)?);
            i += 1;
        }

        if bitmask & 16 > 0 {
            builder.antenna_gain = Some(*data.get(i)?);
            i += 1;
        }

        if bitmask & 32 > 0 {
            builder.tx_power = Some(*data.get(i)?);
            i += 1;
        }

        if bitmask & 64 > 0 {
            builder.voltage = Some(*data.get(i)?);
            i += 1;
        }

        if bitmask & 128 > 0 {
            builder.xcvr_temperature = Some(i8::from_le_bytes([*data.get(i)?]));
            i += 1;
        }

        if bitmask & 256 > 0 {
            builder.battery_charge = Some(*data.get(i)?);
            i += 1;
        }

        if bitmask & 512 > 0 {
            builder.altitude = Some(f32::from_le_bytes(
                data.get(i..(i + 4))?.try_into().unwrap(),
            ));
            i += 4;
        }

        builder.balloon = bitmask & 1024 > 0;

        if bitmask & 2048 > 0 {
            builder.ambient_temperature = Some(i8::from_le_bytes([*data.get(i)?]));
            i += 1;
        }

        if bitmask & 4096 > 0 {
            builder.ambient_humidity = Some(*data.get(i)?);
            i += 1;
        }

        if bitmask & 8192 > 0 {
            builder.ambient_pressure = Some(u16::from_le_bytes(
                data.get(i..(i + 2))?.try_into().unwrap(),
            ));
            i += 2;
        }

        // prevent unused variable warning
        let _ = i;

        Some(builder.build())
    }
}

#[derive(Default, Copy, Clone)]
pub struct NodeInfoBuilder {
    hardware_id: Option<u16>,
    software_id: Option<u8>,
    uptime: Option<u32>,
    antenna_height: Option<u8>,
    antenna_gain: Option<u8>,
    tx_power: Option<u8>,
    voltage: Option<u8>,
    xcvr_temperature: Option<i8>,
    battery_charge: Option<u8>,
    altitude: Option<f32>,
    balloon: bool,
    ambient_temperature: Option<i8>,
    ambient_humidity: Option<u8>,
    ambient_pressure: Option<u16>,
}

impl NodeInfoBuilder {
    pub fn build(self) -> NodeInfo {
        let NodeInfoBuilder {
            hardware_id,
            software_id,
            uptime,
            antenna_height,
            antenna_gain,
            tx_power,
            voltage,
            xcvr_temperature,
            battery_charge,
            altitude,
            balloon,
            ambient_temperature,
            ambient_humidity,
            ambient_pressure,
        } = self;

        NodeInfo {
            hardware_id,
            software_id,
            uptime,
            antenna_height,
            antenna_gain,
            tx_power,
            voltage,
            xcvr_temperature,
            battery_charge,
            altitude,
            balloon,
            ambient_temperature,
            ambient_humidity,
            ambient_pressure,
        }
    }

    pub fn hardware_id(mut self, val: u16) -> Self {
        self.hardware_id = Some(val);

        self
    }

    pub fn software_id(mut self, val: u8) -> Self {
        self.software_id = Some(val);

        self
    }

    /// Seconds
    pub fn uptime(mut self, val: u32) -> Self {
        self.uptime = Some(val);

        self
    }

    /// Meters
    pub fn antenna_height(mut self, val: u8) -> Self {
        self.antenna_height = Some(val);

        self
    }

    /// dBi
    pub fn antenna_gain(mut self, val: f64) -> Self {
        self.antenna_gain = Some((val * 4.0).min(255.0) as u8);

        self
    }

    /// dBm
    pub fn tx_power(mut self, val: f64) -> Self {
        self.tx_power = Some((val * 4.0).min(255.0) as u8);

        self
    }

    /// Volts
    pub fn voltage(mut self, val: f64) -> Self {
        self.voltage = Some((val * 10.0).min(255.0) as u8);

        self
    }

    /// Degrees C
    pub fn xcvr_temperature(mut self, val: i8) -> Self {
        self.xcvr_temperature = Some(val);

        self
    }

    /// Percent
    pub fn battery_charge(mut self, val: f64) -> Self {
        self.battery_charge = Some((val * 2.55).min(255.0) as u8);

        self
    }

    /// Higher precision altitude than what's available in the GPS whisker. Takes precedent if set. Useful for high altitude balloons.
    /// Meters
    pub fn altitude(mut self, val: f32) -> Self {
        self.altitude = Some(val);

        self
    }

    /// If true, this whisker was emitted by a high altitude balloon payload
    /// Please only set this on actual balloon payloads. Otherwise it can cause issues for downstream users!
    pub fn set_balloon(mut self) -> Self {
        self.balloon = true;

        self
    }

    /// Degrees C
    pub fn ambient_temperature(mut self, val: i8) -> Self {
        self.ambient_temperature = Some(val);

        self
    }

    /// Relative humidity, percent
    pub fn ambient_humidity(mut self, val: f64) -> Self {
        self.ambient_humidity = Some((val * 2.55).min(255.0) as u8);

        self
    }

    /// Decapascal (daPa)
    pub fn ambient_pressure(mut self, val: u16) -> Self {
        self.ambient_pressure = Some(val);

        self
    }
}

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

    // verify examples in the standard doc
    #[test]
    fn node_info_doc_examples() {
        let node_info = NodeInfoBuilder::default()
            .hardware_id(7408)
            .uptime(98)
            .tx_power(30.0)
            .voltage(12.8)
            .build();

        let mut buf = [0; 255];
        let encoded = node_info.encode(&mut buf).unwrap();

        assert_eq!(
            [0x0B, 0x00, 0x00, 0x65, 0xF0, 0x1C, 0x62, 0x00, 0x00, 0x00, 0x78, 0x80],
            encoded
        );
    }
}
Original line number Diff line number Diff line
use arrayvec::ArrayVec;

use crate::{error::AppendNodeError, identity::Identity};

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Route {
    pub max_hops: u8,
@@ -19,29 +21,63 @@ impl Route {
    }

    /// 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;
    /// Returns `None` if attempting to push past after future hop
    /// See the specs for more information
    #[must_use]
    pub fn push_hop(&mut self, hop: RouteHop) -> Option<()> {
        match hop {
            RouteHop::Internet => self.push_internet(),
            RouteHop::Past(past_hop) => self.push_past(past_hop),
            RouteHop::Future(ident) => self.push_future(ident),
        }
    }

    /// Returns `None` if there isn't enough space for the callsign
    /// Returns `None` if attempting to push after future hop
    /// See the specs for more information
    #[must_use]
    pub fn push_past(&mut self, past_hop: PastHop) -> Option<()> {
        let ident = past_hop.identity();
        let len = ident.callsign().as_bytes().len() + 3;
        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 {
        if self.has_future {
            return None;
        }

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

        Some(())
    }

    /// Returns `None` if there isn't enough space for the callsign
    #[must_use]
    pub fn push_future(&mut self, ident: Identity) -> Option<()> {
        let len = ident.callsign().as_bytes().len() + 2;
        let free_space = self.path.capacity() - self.path.len();

        if len > free_space {
            return None;
        }

        self.has_future = true;

        // safe to unwrap since we already did a length check
        self.path
            .try_extend_from_slice(ident.callsign().as_bytes())
            .unwrap();
        self.path.push(0xFD);
        self.path.push(ident.ssid());

        Some(())
    }
@@ -59,6 +95,68 @@ impl Route {
        Some(())
    }

    /// Append a callsign/ssid pair to the route, intelligently.
    /// I.e. replace future node if possible
    /// Returns an Err if the route is out of space, or appending the node doesn't make logical sense
    /// (there's a future node that doesn't match)
    /// This only appends past hops.
    pub fn append_hop(&mut self, new_hop: PastHop) -> Result<(), AppendNodeError> {
        let mut new_route = Route::new(self.max_hops);

        let mut already_inserted = false;
        for rn in self.iter() {
            match rn {
                RouteHop::Internet => new_route
                    .push_internet()
                    .ok_or(AppendNodeError::RouteOverflow)?,
                RouteHop::Past(prev_past_hop) => {
                    let us = prev_past_hop.identity() == new_hop.identity();

                    if us {
                        return Err(AppendNodeError::DuplicateNode);
                    } else {
                        new_route
                            .push_past(prev_past_hop)
                            .ok_or(AppendNodeError::RouteOverflow)?;
                    }
                }
                RouteHop::Future(prev_ident) => {
                    let us = prev_ident == new_hop.identity();

                    if us {
                        if already_inserted {
                            return Err(AppendNodeError::DuplicateNode);
                        } else {
                            new_route
                                .push_past(new_hop)
                                .ok_or(AppendNodeError::RouteOverflow)?;
                            already_inserted = true;
                        }
                    } else if already_inserted {
                        new_route
                            .push_future(prev_ident)
                            .ok_or(AppendNodeError::RouteOverflow)?;
                    } else {
                        return Err(AppendNodeError::SetFuture);
                    }
                }
            }
        }

        if !already_inserted {
            new_route
                .push_past(new_hop)
                .ok_or(AppendNodeError::RouteOverflow)?;
        }

        if new_route.iter().count() > usize::from(new_route.max_hops) {
            return Err(AppendNodeError::HopsOverflow);
        }

        *self = new_route;
        Ok(())
    }

    pub fn iter(&'_ self) -> RouteIter<'_> {
        RouteIter::new(self)
    }
@@ -73,7 +171,6 @@ impl Route {
        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))?;
@@ -85,57 +182,320 @@ impl Route {

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

        Some(Self {
        let s = Self {
            max_hops,
            path,
            has_future,
        })
        };

        if UntrustedRouteIter::new(&s).any(|v| v.is_err()) {
            return None;
        }

        Some(s)
    }
}

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

#[derive(Debug)]
pub struct RouteIter<'a> {
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct PastHop<'a> {
    identity: Identity<'a>,
    rssi: u8,
}

impl<'a> PastHop<'a> {
    pub fn new(identity: Identity<'a>, rssi: Option<f64>) -> Self {
        let rssi = match rssi {
            Some(rssi) => (rssi * 1.5 + 240.0).max(1.0) as u8,
            None => 0,
        };

        Self { identity, rssi }
    }

    pub fn identity(&self) -> Identity {
        self.identity
    }

    pub fn rssi(&self) -> Option<f64> {
        if self.rssi == 0 {
            None
        } else {
            Some(((self.rssi as f64) - 240.0) / 1.5)
        }
    }
}

#[derive(Debug, Clone)]
struct UntrustedRouteIter<'a> {
    route: &'a Route,
    i: usize,
    seen_future: bool,
    dead: bool,
}

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

impl<'a> Iterator for RouteIter<'a> {
    type Item = RouteNode<'a>;
    // Returns Err(()) if invalid
    fn maybe_next(&mut self) -> Result<RouteHop<'a>, ()> {
        let i_start = self.i;
        self.i += 1;

        if *self.route.path.get(i_start).ok_or(())? == 0xFE {
            return Ok(RouteHop::Internet);
        }

        while *self.route.path.get(self.i).ok_or(())? != 0xFD && self.route.path[self.i] != 0xFF {
            self.i += 1;
        }

        let callsign = core::str::from_utf8(self.route.path.get(i_start..self.i).ok_or(())?)
            .map_err(|_| ())?;

        let is_future = *self.route.path.get(self.i).ok_or(())? == 0xFD;
        if self.seen_future && !is_future {
            // past after future - not allowed
            return Err(());
        }
        self.seen_future |= is_future;

        let ssid = *self.route.path.get(self.i + 1).ok_or(())?;
        self.i += 2;

        if is_future {
            Ok(RouteHop::Future(Identity::new(callsign, ssid)))
        } else {
            let rssi = *self.route.path.get(self.i).ok_or(())?;
            self.i += 1;

            Ok(RouteHop::Past(PastHop {
                identity: Identity::new(callsign, ssid),
                rssi,
            }))
        }
    }
}

impl<'a> Iterator for UntrustedRouteIter<'a> {
    type Item = Result<RouteHop<'a>, ()>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.dead {
            return None;
        }

        if self.i == self.route.path.len() {
            return None;
        }

        let i_start = self.i;
        self.i += 1;
        let r = self.maybe_next();

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

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

        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;
#[derive(Clone)]
pub struct RouteIter<'a> {
    iter: UntrustedRouteIter<'a>,
}

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

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

    fn next(&mut self) -> Option<Self::Item> {
        Some(self.iter.next()?.unwrap())
    }
}

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

    #[test]
    fn route_rssi() {
        let x = -66.0;
        let r = PastHop::new(Identity::new("C0", 23), Some(x));
        assert_eq!(x, r.rssi().unwrap());

        let x = 10.0;
        let r = PastHop::new(Identity::new("C0", 23), Some(x));
        assert_eq!(x, r.rssi().unwrap());

        let x = -158.0;
        let r = PastHop::new(Identity::new("C0", 23), Some(x));
        assert_eq!(x, r.rssi().unwrap());

        let r = PastHop::new(Identity::new("C0", 23), None);
        assert_eq!(None, r.rssi());
    }

    #[test]
    fn append_fails_when_existing_future() {
        let mut r = Route::new(5);
        r.push_past(PastHop::new(Identity::new("C1", 0), None))
            .unwrap();
        r.push_past(PastHop::new(Identity::new("C2", 0), None))
            .unwrap();
        r.push_future(Identity::new("C3", 0)).unwrap();

        assert_eq!(
            AppendNodeError::SetFuture,
            r.append_hop(PastHop::new(Identity::new("C3", 1), None))
                .unwrap_err()
        );

        assert_eq!(
            AppendNodeError::SetFuture,
            r.append_hop(PastHop::new(Identity::new("C4", 0), None))
                .unwrap_err()
        );
    }

    #[test]
    fn append_fails_when_would_exceed_max_hops() {
        let mut r = Route::new(3);
        r.append_hop(PastHop::new(Identity::new("C1", 0), None))
            .unwrap();
        r.append_hop(PastHop::new(Identity::new("C2", 0), None))
            .unwrap();
        r.append_hop(PastHop::new(Identity::new("C2", 1), None))
            .unwrap();
        assert_eq!(
            AppendNodeError::HopsOverflow,
            r.append_hop(PastHop::new(Identity::new("C4", 0), None))
                .unwrap_err()
        );
    }

    #[test]
    fn append_fails_when_already_in_route() {
        let mut r = Route::new(3);
        r.append_hop(PastHop::new(Identity::new("C1", 0), None))
            .unwrap();

        assert_eq!(
            AppendNodeError::DuplicateNode,
            r.append_hop(PastHop::new(Identity::new("C1", 0), None))
                .unwrap_err()
        );
    }

    #[test]
    fn append_overwrites_future() {
        let mut r = Route::new(5);
        r.push_past(PastHop::new(Identity::new("C1", 0), None))
            .unwrap();
        r.push_past(PastHop::new(Identity::new("C2", 0), None))
            .unwrap();
        r.push_future(Identity::new("C3", 0)).unwrap();
        r.push_future(Identity::new("C4", 0)).unwrap();

        r.append_hop(PastHop::new(Identity::new("C3", 0), None))
            .unwrap();

        let mut iter = r.iter();
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C1", 0), None))),
            iter.next()
        );
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C2", 0), None))),
            iter.next()
        );
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C3", 0), None))),
            iter.next()
        );
        assert_eq!(Some(RouteHop::Future(Identity::new("C4", 0))), iter.next());
        assert_eq!(None, iter.next());
    }

    #[test]
    fn append_overwrites_future_even_when_at_hop_limit() {
        let mut r = Route::new(4);
        r.push_past(PastHop::new(Identity::new("C1", 0), None))
            .unwrap();
        r.push_past(PastHop::new(Identity::new("C2", 0), None))
            .unwrap();
        r.push_future(Identity::new("C3", 0)).unwrap();
        r.push_future(Identity::new("C4", 0)).unwrap();

        r.append_hop(PastHop::new(Identity::new("C3", 0), None))
            .unwrap();

        let mut iter = r.iter();
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C1", 0), None))),
            iter.next()
        );
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C2", 0), None))),
            iter.next()
        );
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C3", 0), None))),
            iter.next()
        );
        assert_eq!(Some(RouteHop::Future(Identity::new("C4", 0,))), iter.next());
        assert_eq!(None, iter.next());
    }

    #[test]
    fn append_appends_when_no_future() {
        let mut r = Route::new(5);
        r.push_past(PastHop::new(Identity::new("C1", 0), None))
            .unwrap();
        r.push_past(PastHop::new(Identity::new("C2", 0), None))
            .unwrap();
        r.push_past(PastHop::new(Identity::new("C3", 0), None))
            .unwrap();

        r.append_hop(PastHop::new(Identity::new("C4", 0), None))
            .unwrap();

        Some(RouteNode::Identity(callsign, ssid, is_future))
        let mut iter = r.iter();
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C1", 0), None))),
            iter.next()
        );
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C2", 0), None))),
            iter.next()
        );
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C3", 0), None))),
            iter.next()
        );
        assert_eq!(
            Some(RouteHop::Past(PastHop::new(Identity::new("C4", 0), None))),
            iter.next()
        );
        assert_eq!(None, iter.next());
    }
}
+83 −9
Original line number Diff line number Diff line
const WHITE: [u8; 16] = [
    0xe9, 0xcf, 0x67, 0x20, 0x19, 0x1a, 0x07, 0xdc, 0xc0, 0x72, 0x79, 0x97, 0x51, 0xf7, 0xdd, 0x93,
];
const START_STATE: u16 = 0xE9CF;

pub(crate) fn whiten(data: &mut [u8]) {
    for (i, d) in data.iter_mut().enumerate() {
        *d ^= WHITE[i % 15];
    let mut state = START_STATE;

    for d in data.iter_mut() {
        let b;
        (b, state) = lfsr_byte(state);
        *d ^= b;
    }
}

// (byte, state)
fn lfsr_byte(mut state: u16) -> (u8, u16) {
    let mut out = 0;
    for i in (0..8).rev() {
        out |= u8::try_from(state & 1).unwrap() << i;
        state = lfsr(state);
    }

    (out, state)
}

// https://en.wikipedia.org/wiki/Linear-feedback_shift_register#Galois_LFSRs
fn lfsr(mut state: u16) -> u16 {
    let lsb = state & 1;
    state >>= 1;
    if lsb > 0 {
        state ^= 0xB400; // apply toggle mask
    }

    state
}

#[cfg(test)]
@@ -14,10 +38,10 @@ mod tests {

    #[test]
    fn basic() {
        let mut data: ArrayVec<u8, 512> =
            ArrayVec::try_from(&b"Hello world! The quick brown fox jumped over the lazy dog"[..])
                .unwrap();
        let orig = data.clone();
        let mut data = [0; 64];
        data[0..57]
            .clone_from_slice(&b"Hello world! The quick brown fox jumped over the lazy dog"[..]);
        let orig = data;

        whiten(&mut data);
        assert_ne!(orig, data);
@@ -25,4 +49,54 @@ mod tests {
        whiten(&mut data);
        assert_eq!(orig, data);
    }

    #[test]
    fn test_lfsr() {
        let start = 0xACE1;
        let end_expected = 0xE270;

        let state = lfsr(start);

        assert_eq!(end_expected, state);
    }

    #[test]
    fn test_lfsr_byte() {
        let start = 0xE9CF;
        let (out, state) = lfsr_byte(start);
        assert_eq!(0xF3, out);
        assert_eq!(0xE3B1, state);
    }

    #[test]
    fn test_doc_example() {
        let start = 0xE9CF;
        let expected_out = [
            0xF3, 0x8D, 0xD0, 0x6E, 0x1F, 0x65, 0x75, 0x75, 0xA5, 0xBA, 0xA9, 0xD0, 0x7A, 0x1D,
            0x1, 0x21,
        ];

        let mut actual_out = [0; 16];
        let mut state = start;
        for a in &mut actual_out {
            let (out, ns) = lfsr_byte(state);
            state = ns;
            *a = out;
        }

        assert_eq!(expected_out, actual_out);
    }

    #[test]
    fn test_doc_example_through_whitener() {
        let expected_out = [
            0xF3, 0x8D, 0xD0, 0x6E, 0x1F, 0x65, 0x75, 0x75, 0xA5, 0xBA, 0xA9, 0xD0, 0x7A, 0x1D,
            0x1, 0x21,
        ];

        let mut actual_out = [0; 16];
        whiten(&mut actual_out);

        assert_eq!(expected_out, actual_out);
    }
}