initial commit

This commit is contained in:
Sebastian Hugentobler 2024-11-17 10:27:30 +01:00
commit 17a8e573c0
Signed by: shu
GPG key ID: BB32CF3CA052C2F0
40 changed files with 4009 additions and 0 deletions

8
serial-comm/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "serial-comm"
version.workspace = true
edition.workspace = true
description.workspace = true
license.workspace = true
[dependencies]

150
serial-comm/src/cobs.rs Normal file
View file

@ -0,0 +1,150 @@
// sentinel value for backage boundaries
// could be anything but 0x00 is used as a default and makes sense
pub const SENTINEL: u8 = 0x00;
// max size of non SENTINEL values before a code split occurs
const SPLIT_BOUNDARY: u8 = 0xff;
/// Encode a byte array of size `INPUT` with an actual data length of`length`
/// to an output array of size `OUTPUT`, using the COBS algorithm.
///
/// Return a tuple of the structure (<encoded_length>, <output_array>)
pub fn encode<const INPUT: usize, const OUTPUT: usize>(
input: [u8; INPUT],
length: usize,
) -> (usize, [u8; OUTPUT]) {
let mut output: [u8; OUTPUT] = [0; OUTPUT];
let mut encode_idx = 1;
let mut code_idx = 0;
let mut code = 1;
for byte in input.iter().take(length) {
if *byte == SENTINEL {
output[code_idx] = code;
code = 1;
code_idx = encode_idx;
encode_idx += 1;
} else {
output[encode_idx] = *byte;
encode_idx += 1;
code += 1;
if code == SPLIT_BOUNDARY {
output[code_idx] = code;
code = 1;
code_idx = encode_idx;
if encode_idx < length {
encode_idx += 1;
}
}
}
}
if code_idx < OUTPUT {
output[code_idx] = code;
}
(encode_idx, output)
}
/// Decode a COBS encoded byte array of length `INPUT` and an actual data length of `length` to an
/// array of length `OUTPUT`.
///
/// Return a tuple of the structure (<decoded_length>, <output_array>)
pub fn decode<const INPUT: usize, const OUTPUT: usize>(
input: [u8; INPUT],
length: usize,
) -> (usize, [u8; OUTPUT]) {
let mut output: [u8; OUTPUT] = [0; OUTPUT];
let mut out_idx = 0;
let mut idx = 0;
while idx < length {
let code = input[idx];
idx += 1;
for _ in 1..code {
if idx < length {
output[out_idx] = input[idx];
out_idx += 1;
idx += 1;
}
}
if code != SPLIT_BOUNDARY && idx < length {
output[out_idx] = SENTINEL;
out_idx += 1;
}
}
(out_idx, output)
}
#[cfg(test)]
mod tests {
use crate::cobs::{decode, encode};
#[test]
fn test_paper_example() {
let input = [
0x45, 0x00, 0x00, 0x2c, 0x4c, 0x79, 0x00, 0x00, 0x40, 0x06, 0x4f, 0x37,
];
let output = [
0x02, 0x45, 0x01, 0x04, 0x2c, 0x4c, 0x79, 0x01, 0x05, 0x40, 0x06, 0x4f, 0x37,
];
assert_eq!(encode(input, 12), (13, output));
assert_eq!(decode(output, 13), (12, input));
}
#[test]
fn test_empty_input() {
let input = [];
let output = [0x01];
assert_eq!(encode(input, 0), (1, output));
assert_eq!(decode(output, 1), (0, input));
}
#[test]
fn test_all_zeros() {
let input = [0x00, 0x00, 0x00];
let output = [0x01, 0x01, 0x01, 0x01];
assert_eq!(encode(input, 3), (4, output));
assert_eq!(decode(output, 4), (3, input));
}
#[test]
fn test_no_zeros() {
let input = [0x01, 0x02, 0x03];
let output = [0x04, 0x01, 0x02, 0x03];
assert_eq!(encode(input, 3), (4, output));
assert_eq!(decode(output, 4), (3, input));
}
#[test]
fn test_no_zeros_in_254_bytes() {
let input: [u8; 254] = [0x01; 254];
// expected output will have the first byte as 0xFF followed by the 255 non-zero bytes.
let mut output: [u8; 255] = [0x01; 255];
output[0] = 0xff;
assert_eq!(encode(input, 254), (255, output));
assert_eq!(decode(output, 255), (254, input));
}
#[test]
fn test_no_zeros_in_254_bytes_and_additional_block() {
let mut input: [u8; 259] = [0x01; 259];
input[254..259].copy_from_slice(&[0x02, 0x00, 0x03, 0x0c, 0x01]);
// expected output will have the first byte as 0xFF followed by the 254 non-zero bytes.
let mut output: [u8; 261] = [0x01; 261];
output[0] = 0xff;
output[255..261].copy_from_slice(&[0x02, 0x02, 0x04, 0x03, 0x0c, 0x01]);
assert_eq!(encode(input, 259), (261, output));
assert_eq!(decode(output, 261), (259, input));
}
}

8
serial-comm/src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
//! Simple communication protocol over a serial connection, leveraging the COBS algorithm for byte
//! stuffing
//! (S. Cheshire and M. Baker, "Consistent overhead byte stuffing," in IEEE/ACM Transactions on Networking, vol. 7, no. 2, pp. 159-172, April 1999, doi: 10.1109/90.769765).
#![cfg_attr(not(test), no_std)]
pub mod cobs;
pub mod protocol;

View file

@ -0,0 +1,50 @@
use core::str::Utf8Error;
const MAX_PARAMS: usize = 8;
pub trait Cobs<'a> {
/// Encode a command with the COBS algorithm.
///
/// Return a tuple of the structure (<encoded_length>, <output_array>)
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]);
}
pub trait Cmd<'a> {
fn from_bytes<const INPUT: usize>(
input: [u8; INPUT],
buffer: &'a mut [u8; INPUT],
) -> Result<Self, Utf8Error>
where
Self: Sized;
/// Read parameters from a byte array (not COBS encoded).
///
/// Every parameter is interpreted as an UTF-8 string.
///
/// Return a string array of size `MAX_PARAMS`.
fn params_from_bytes<const INPUT: usize, const PARAMS: usize>(
input: [u8; INPUT],
buffer: &mut [u8; INPUT],
) -> Result<[&str; MAX_PARAMS], Utf8Error> {
buffer.copy_from_slice(&input);
let param_count = buffer[0] as usize;
assert_eq!(param_count, PARAMS);
assert!(param_count <= MAX_PARAMS);
let param_offset = param_count + 1;
let mut param_idx = 0;
let mut params: [&str; MAX_PARAMS] = [""; MAX_PARAMS];
for i in 1..=param_count {
let param_length = buffer[i] as usize;
let param_start = param_idx + param_offset;
let param = &buffer[param_start..param_start + param_length];
let param = core::str::from_utf8(param)?;
params[i - 1] = param;
param_idx += param_length;
}
Ok(params)
}
}

View file

@ -0,0 +1,83 @@
use core::str::Utf8Error;
use crate::cobs;
use super::cmd::{Cmd, Cobs};
/// The error command carries a message.
pub struct ErrorCommand<'a> {
pub prefix: &'a str,
pub msg: &'a str,
}
impl<'a> ErrorCommand<'a> {
const PARAMS: u8 = 2;
const HEADER_LENGTH: usize = Self::PARAMS as usize + 1;
pub fn new(msg: &'a str) -> Self {
Self { prefix: "ER", msg }
}
}
impl<'a> Cobs<'a> for ErrorCommand<'a> {
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]) {
let prefix_bytes = self.prefix.as_bytes();
let msg_bytes = self.msg.as_bytes();
let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH;
let msg_end = prefix_bytes.len() + msg_bytes.len() + Self::HEADER_LENGTH;
let mut output: [u8; OUTPUT] = [0; OUTPUT];
output[0] = ErrorCommand::PARAMS;
output[1] = prefix_bytes.len() as u8;
output[2] = msg_bytes.len() as u8;
output[ErrorCommand::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes);
output[prefix_end..msg_end].copy_from_slice(msg_bytes);
cobs::encode(output, msg_end)
}
}
impl<'a> Cmd<'a> for ErrorCommand<'a> {
fn from_bytes<const INPUT: usize>(
input: [u8; INPUT],
buffer: &'a mut [u8; INPUT],
) -> Result<Self, Utf8Error> {
let params = <ErrorCommand<'a> as Cmd>::params_from_bytes::<
INPUT,
{ ErrorCommand::PARAMS as usize },
>(input, buffer)?;
Ok(Self {
prefix: params[0],
msg: params[1],
})
}
}
#[cfg(test)]
mod tests {
use super::ErrorCommand;
use crate::cobs;
use crate::protocol::cmd::{Cmd, Cobs};
#[test]
fn test_ok_cmd() {
let cmd = ErrorCommand::new("error");
let (length, cmd_cobs) = cmd.as_cobs::<64>();
assert_eq!(length, 11);
assert_eq!(
&cmd_cobs[0..11],
&[0x0b, 0x02, 0x02, 0x05, 0x45, 0x52, 0x65, 0x72, 0x72, 0x6f, 0x72]
);
let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 11);
let mut cmd_buffer: [u8; 64] = [0; 64];
let cmd = ErrorCommand::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap();
assert_eq!(cmd.prefix, "ER");
assert_eq!(cmd.msg, "error");
}
}

View file

@ -0,0 +1,6 @@
//! Specific commands for the protocol.
pub mod cmd;
pub mod error;
pub mod ok;
pub mod set_config;

View file

@ -0,0 +1,76 @@
use core::str::Utf8Error;
use crate::cobs;
use super::cmd::{Cmd, Cobs};
// The Ok command has no parameters, it should merely signify something went ok.
pub struct OkCommand<'a> {
pub prefix: &'a str,
}
impl<'a> OkCommand<'a> {
const PARAMS: u8 = 1;
const HEADER_LENGTH: usize = Self::PARAMS as usize + 1;
pub fn new() -> Self {
Self { prefix: "OK" }
}
}
impl<'a> Default for OkCommand<'a> {
fn default() -> Self {
Self::new()
}
}
impl<'a> Cobs<'a> for OkCommand<'a> {
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]) {
let prefix_bytes = self.prefix.as_bytes();
let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH;
let mut output: [u8; OUTPUT] = [0; OUTPUT];
output[0] = OkCommand::PARAMS;
output[1] = prefix_bytes.len() as u8;
output[OkCommand::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes);
cobs::encode(output, prefix_end)
}
}
impl<'a> Cmd<'a> for OkCommand<'a> {
fn from_bytes<const INPUT: usize>(
input: [u8; INPUT],
buffer: &'a mut [u8; INPUT],
) -> Result<Self, Utf8Error> {
let params = <OkCommand<'a> as Cmd>::params_from_bytes::<
INPUT,
{ OkCommand::PARAMS as usize },
>(input, buffer)?;
Ok(Self { prefix: params[0] })
}
}
#[cfg(test)]
mod tests {
use super::OkCommand;
use crate::cobs;
use crate::protocol::cmd::{Cmd, Cobs};
#[test]
fn test_ok_cmd() {
let cmd = OkCommand::new();
let (length, cmd_cobs) = cmd.as_cobs::<64>();
assert_eq!(length, 5);
assert_eq!(&cmd_cobs[0..5], &[0x05, 0x01, 0x02, 0x4f, 0x4b]);
let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 5);
let mut cmd_buffer: [u8; 64] = [0; 64];
let cmd = OkCommand::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap();
assert_eq!(cmd.prefix, "OK");
}
}

View file

@ -0,0 +1,92 @@
use core::str::Utf8Error;
use crate::cobs;
use super::cmd::{Cmd, Cobs};
#[derive(Debug)]
/// The SetConfig command sets the value of a key.
pub struct SetConfig<'a> {
pub key: &'a str,
pub value: &'a str,
}
impl<'a> SetConfig<'a> {
pub const PREFIX: &'a str = "SC";
const PARAMS: u8 = 3;
const HEADER_LENGTH: usize = Self::PARAMS as usize + 1;
pub fn new(key: &'a str, value: &'a str) -> Self {
Self { key, value }
}
}
impl<'a> Cobs<'a> for SetConfig<'a> {
/// Encode a SetConfig command with the COBS algorithm.
///
/// Return a tuple of the structure (<encoded_length>, <output_array>)
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]) {
let prefix_bytes = Self::PREFIX.as_bytes();
let key_bytes = self.key.as_bytes();
let val_bytes = self.value.as_bytes();
let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH;
let key_end = prefix_end + key_bytes.len();
let val_end = key_end + val_bytes.len();
let mut output: [u8; OUTPUT] = [0; OUTPUT];
output[0] = Self::PARAMS;
output[1] = prefix_bytes.len() as u8;
output[2] = key_bytes.len() as u8;
output[3] = val_bytes.len() as u8;
output[Self::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes);
output[prefix_end..key_end].copy_from_slice(key_bytes);
output[key_end..val_end].copy_from_slice(val_bytes);
cobs::encode(output, val_end)
}
}
impl<'a> Cmd<'a> for SetConfig<'a> {
fn from_bytes<const INPUT: usize>(
input: [u8; INPUT],
buffer: &'a mut [u8; INPUT],
) -> Result<Self, Utf8Error> {
let params = <SetConfig<'a> as Cmd>::params_from_bytes::<
INPUT,
{ SetConfig::PARAMS as usize },
>(input, buffer)?;
Ok(Self {
key: params[1],
value: params[2],
})
}
}
#[cfg(test)]
mod tests {
use crate::cobs;
use crate::protocol::cmd::{Cmd, Cobs};
use crate::protocol::set_config::SetConfig;
#[test]
fn test_config_cmd() {
let cmd = SetConfig::new("ssid", "pw");
let (length, cmd_cobs) = cmd.as_cobs::<64>();
assert_eq!(length, 13);
assert_eq!(
&cmd_cobs[0..13],
&[0x0d, 0x03, 0x02, 0x04, 0x02, 0x53, 0x43, 0x73, 0x73, 0x69, 0x64, 0x70, 0x77]
);
let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 13);
let mut cmd_buffer: [u8; 64] = [0; 64];
let cmd = SetConfig::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap();
assert_eq!(cmd.key, "ssid");
assert_eq!(cmd.value, "pw");
}
}