initial commit
This commit is contained in:
commit
17a8e573c0
40 changed files with 4009 additions and 0 deletions
8
serial-comm/Cargo.toml
Normal file
8
serial-comm/Cargo.toml
Normal 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
150
serial-comm/src/cobs.rs
Normal 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
8
serial-comm/src/lib.rs
Normal 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;
|
50
serial-comm/src/protocol/cmd.rs
Normal file
50
serial-comm/src/protocol/cmd.rs
Normal 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)
|
||||
}
|
||||
}
|
83
serial-comm/src/protocol/error.rs
Normal file
83
serial-comm/src/protocol/error.rs
Normal 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");
|
||||
}
|
||||
}
|
6
serial-comm/src/protocol/mod.rs
Normal file
6
serial-comm/src/protocol/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
//! Specific commands for the protocol.
|
||||
|
||||
pub mod cmd;
|
||||
pub mod error;
|
||||
pub mod ok;
|
||||
pub mod set_config;
|
76
serial-comm/src/protocol/ok.rs
Normal file
76
serial-comm/src/protocol/ok.rs
Normal 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");
|
||||
}
|
||||
}
|
92
serial-comm/src/protocol/set_config.rs
Normal file
92
serial-comm/src/protocol/set_config.rs
Normal 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");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue