feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+413
View File
@@ -0,0 +1,413 @@
/// The [`CobsDecoder`] type is used to decode a stream of bytes to a
/// given mutable output slice. This is often useful when heap data
/// structures are not available, or when not all message bytes are
/// received at a single point in time.
#[derive(Debug)]
pub struct CobsDecoder<'a> {
/// Destination slice for decoded message
dest: &'a mut [u8],
/// Index of next byte to write in `dest`
dest_idx: usize,
/// Decoder state as an enum
state: DecoderState,
}
/// The [`DecoderState`] is used to track the current state of a
/// streaming decoder. This struct does not contain the output buffer
/// (or a reference to one), and can be used when streaming the decoded
/// output to a custom data type.
#[derive(Debug)]
pub enum DecoderState {
/// State machine has not received any non-zero bytes
Idle,
/// 1-254 bytes, can be header or 00
Grab(u8),
/// 255 bytes, will be a header next
GrabChain(u8),
}
fn add(to: &mut [u8], idx: usize, data: u8) -> Result<(), DecodeError> {
*to.get_mut(idx).ok_or(DecodeError::TargetBufTooSmall)? = data;
Ok(())
}
/// [`DecodeResult`] represents the possible non-error outcomes of
/// pushing an encoded data byte into the [`DecoderState`] state machine
pub enum DecodeResult {
/// The given input byte did not prompt an output byte, either because the
/// state machine is still idle, or we have just processed a header byte.
/// More data is needed to complete the message.
NoData,
/// We have received a complete and well-encoded COBS message. The
/// contents of the associated output buffer may now be used
DataComplete,
/// The following byte should be appended to the current end of the decoded
/// output buffer.
/// More data is needed to complete the message.
DataContinue(u8),
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DecodeError {
#[error("empty input frame")]
EmptyFrame,
#[error("frame with invalid format, written {decoded_bytes:?} to decoded buffer")]
InvalidFrame {
/// Number of bytes written to the decoded buffer.
decoded_bytes: usize,
},
#[error("target buffer too small")]
TargetBufTooSmall,
}
impl DecoderState {
/// Push a single encoded byte into the state machine. If the input was
/// unexpected, such as an early end of a framed message segment, an Error will
/// be returned, and the current associated output buffer contents should be discarded.
///
/// If a complete message is indicated, the decoding state machine will automatically
/// reset itself to the Idle state, and may be used to begin decoding another message.
///
/// NOTE: Sentinel value must be included in the input to this function for the
/// decoding to complete
pub fn feed(&mut self, data: u8) -> Result<DecodeResult, DecodeError> {
use DecodeResult::*;
use DecoderState::*;
let (ret, state) = match (&self, data) {
// Currently Idle, received a terminator, ignore, stay idle
(Idle, 0x00) => (Ok(NoData), Idle),
// Currently Idle, received a byte indicating the
// next 255 bytes have no zeroes, so we will have 254 unmodified
// data bytes, then an overhead byte
(Idle, 0xFF) => (Ok(NoData), GrabChain(0xFE)),
// Currently Idle, received a byte indicating there will be a
// zero that must be modified in the next 1..=254 bytes
(Idle, n) => (Ok(NoData), Grab(n - 1)),
// We have reached the end of a data run indicated by an overhead
// byte, AND we have recieved the message terminator. This was a
// well framed message!
(Grab(0), 0x00) => (Ok(DataComplete), Idle),
// We have reached the end of a data run indicated by an overhead
// byte, and the next segment of 254 bytes will have no modified
// sentinel bytes
(Grab(0), 0xFF) => (Ok(DataContinue(0)), GrabChain(0xFE)),
// We have reached the end of a data run indicated by an overhead
// byte, and we will treat this byte as a modified sentinel byte.
// place the sentinel byte in the output, and begin processing the
// next non-sentinel sequence
(Grab(0), n) => (Ok(DataContinue(0)), Grab(n - 1)),
// We were not expecting the sequence to terminate, but here we are.
// Report an error due to early terminated message
(Grab(_), 0) => (Err(DecodeError::InvalidFrame { decoded_bytes: 0 }), Idle),
// We have not yet reached the end of a data run, decrement the run
// counter, and place the byte into the decoded output
(Grab(i), n) => (Ok(DataContinue(n)), Grab(*i - 1)),
// We have reached the end of a data run indicated by an overhead
// byte, AND we have recieved the message terminator. This was a
// well framed message!
(GrabChain(0), 0x00) => (Ok(DataComplete), Idle),
// We have reached the end of a data run, and we will begin another
// data run with an overhead byte expected at the end
(GrabChain(0), 0xFF) => (Ok(NoData), GrabChain(0xFE)),
// We have reached the end of a data run, and we will expect `n` data
// bytes unmodified, followed by a sentinel byte that must be modified
(GrabChain(0), n) => (Ok(NoData), Grab(n - 1)),
// We were not expecting the sequence to terminate, but here we are.
// Report an error due to early terminated message
(GrabChain(_), 0) => (Err(DecodeError::InvalidFrame { decoded_bytes: 0 }), Idle),
// We have not yet reached the end of a data run, decrement the run
// counter, and place the byte into the decoded output
(GrabChain(i), n) => (Ok(DataContinue(n)), GrabChain(*i - 1)),
};
*self = state;
ret
}
}
impl<'a> CobsDecoder<'a> {
/// Create a new streaming Cobs Decoder. Provide the output buffer
/// for the decoded message to be placed in
pub fn new(dest: &'a mut [u8]) -> CobsDecoder<'a> {
CobsDecoder {
dest,
dest_idx: 0,
state: DecoderState::Idle,
}
}
/// Push a single byte into the streaming CobsDecoder. Return values mean:
///
/// * Ok(None) - State machine okay, more data needed
/// * Ok(Some(N)) - A message of N bytes was successfully decoded
/// * Err([DecodeError]) - Message decoding failed
///
/// NOTE: Sentinel value must be included in the input to this function for the
/// decoding to complete
pub fn feed(&mut self, data: u8) -> Result<Option<usize>, DecodeError> {
match self.state.feed(data) {
Err(_) => Err(DecodeError::InvalidFrame {
decoded_bytes: self.dest_idx,
}),
Ok(DecodeResult::NoData) => Ok(None),
Ok(DecodeResult::DataContinue(n)) => {
add(self.dest, self.dest_idx, n)?;
self.dest_idx += 1;
Ok(None)
}
Ok(DecodeResult::DataComplete) => Ok(Some(self.dest_idx)),
}
}
/// Push a slice of bytes into the streaming CobsDecoder. Return values mean:
///
/// * Ok(None) - State machine okay, more data needed
/// * Ok(Some((N, M))) - A message of N bytes was successfully decoded,
/// using M bytes from `data` (and earlier data)
/// * Err([DecodeError]) - Message decoding failed
///
/// NOTE: Sentinel value must be included in the input to this function for the
/// decoding to complete
pub fn push(&mut self, data: &[u8]) -> Result<Option<(usize, usize)>, DecodeError> {
for (consumed_idx, d) in data.iter().enumerate() {
let opt_decoded_bytes = self.feed(*d)?;
if let Some(decoded_bytes_ct) = opt_decoded_bytes {
// convert from index to number of bytes consumed
return Ok(Some((decoded_bytes_ct, consumed_idx + 1)));
}
}
Ok(None)
}
}
/// Decodes the `source` buffer into the `dest` buffer.
///
/// This function uses the typical sentinel value of 0.
///
/// # Failures
///
/// This will return `Err(())` if there was a decoding error. Otherwise,
/// it will return `Ok(n)` where `n` is the length of the decoded message.
pub fn decode(source: &[u8], dest: &mut [u8]) -> Result<usize, DecodeError> {
if source.is_empty() {
return Err(DecodeError::EmptyFrame);
}
let mut dec = CobsDecoder::new(dest);
// Did we decode a message, using some or all of the buffer?
if let Some((d_used, _s_used)) = dec.push(source)? {
return Ok(d_used);
}
// If we consumed the entire buffer, but did NOT get a message,
// AND the message did not end with a zero, try providing one to
// complete the decoding.
if source.last() != Some(&0) {
// Explicitly push sentinel of zero
if let Some((d_used, _s_used)) = dec.push(&[0])? {
return Ok(d_used);
}
}
// Nope, no early message, no missing terminator, just failed to decode
Err(DecodeError::InvalidFrame {
decoded_bytes: dec.dest_idx,
})
}
/// A report of the source and destination bytes used during in-place decoding
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct DecodeReport {
// The number of source bytes used, NOT INCLUDING the sentinel byte,
// if there was one.
pub src_used: usize,
// The number of bytes of the source buffer that now include the
// decoded result
pub dst_used: usize,
}
/// Decodes a message in-place.
///
/// This is the same function as [decode_in_place], but provides a report
/// of both the number of source bytes consumed as well as the size of the
/// destination used.
pub fn decode_in_place_report(buf: &mut [u8]) -> Result<DecodeReport, DecodeError> {
let mut source_index = 0;
let mut dest_index = 0;
if buf.is_empty() {
return Err(DecodeError::EmptyFrame);
}
// Stop at the first terminator, if any
let src_end = if let Some(end) = buf.iter().position(|b| *b == 0) {
end
} else {
buf.len()
};
while source_index < src_end {
let code = buf[source_index];
if source_index + code as usize > src_end && code != 1 {
return Err(DecodeError::InvalidFrame {
decoded_bytes: dest_index,
});
}
source_index += 1;
for _ in 1..code {
*buf.get_mut(dest_index)
.ok_or(DecodeError::TargetBufTooSmall)? = buf[source_index];
source_index += 1;
dest_index += 1;
}
if 0xFF != code && source_index < src_end {
*buf.get_mut(dest_index)
.ok_or(DecodeError::TargetBufTooSmall)? = 0;
dest_index += 1;
}
}
Ok(DecodeReport {
dst_used: dest_index,
src_used: source_index,
})
}
/// Decodes a message in-place.
///
/// This is the same function as [decode], but replaces the encoded message
/// with the decoded message instead of writing to another buffer.
///
/// The returned `usize` is the number of bytes used for the DECODED value,
/// NOT the number of source bytes consumed during decoding.
pub fn decode_in_place(buff: &mut [u8]) -> Result<usize, DecodeError> {
decode_in_place_report(buff).map(|r| r.dst_used)
}
/// Decodes the `source` buffer into the `dest` buffer using an arbitrary sentinel value.
///
/// This is done by XOR-ing each byte of the source message with the chosen sentinel value,
/// which transforms the message into the same message encoded with a sentinel value of 0.
/// Then the regular decoding transformation is performed.
///
/// The returned `usize` is the number of bytes used for the DECODED value,
/// NOT the number of source bytes consumed during decoding.
pub fn decode_with_sentinel(
source: &[u8],
dest: &mut [u8],
sentinel: u8,
) -> Result<usize, DecodeError> {
for (x, y) in source.iter().zip(dest.iter_mut()) {
*y = *x ^ sentinel;
}
decode_in_place(dest)
}
/// Decodes a message in-place using an arbitrary sentinel value.
///
/// The returned `usize` is the number of bytes used for the DECODED value,
/// NOT the number of source bytes consumed during decoding.
pub fn decode_in_place_with_sentinel(buff: &mut [u8], sentinel: u8) -> Result<usize, DecodeError> {
for x in buff.iter_mut() {
*x ^= sentinel;
}
decode_in_place(buff)
}
#[cfg(feature = "alloc")]
/// Decodes the `source` buffer into a vector.
pub fn decode_vec(source: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let mut decoded = alloc::vec![0; source.len()];
let n = decode(source, &mut decoded[..])?;
decoded.truncate(n);
Ok(decoded)
}
#[cfg(feature = "alloc")]
/// Decodes the `source` buffer into a vector with an arbitrary sentinel value.
pub fn decode_vec_with_sentinel(
source: &[u8],
sentinel: u8,
) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let mut decoded = alloc::vec![0; source.len()];
let n = decode_with_sentinel(source, &mut decoded[..], sentinel)?;
decoded.truncate(n);
Ok(decoded)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_malforemd() {
let malformed_buf: [u8; 32] = [
68, 69, 65, 68, 66, 69, 69, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
let mut dest_buf: [u8; 32] = [0; 32];
if let Err(DecodeError::InvalidFrame { decoded_bytes }) =
decode(&malformed_buf, &mut dest_buf)
{
assert_eq!(decoded_bytes, 7);
} else {
panic!("decoding worked when it should not have");
}
}
#[test]
fn decode_empty() {
#[cfg(feature = "alloc")]
matches!(decode_vec(&[]).unwrap_err(), DecodeError::EmptyFrame);
matches!(
decode_in_place(&mut []).unwrap_err(),
DecodeError::EmptyFrame
);
matches!(
decode(&[], &mut [0; 256]).unwrap_err(),
DecodeError::EmptyFrame
);
}
#[test]
#[cfg(feature = "alloc")]
fn decode_target_buf_too_small() {
let encoded = &[3, 10, 11, 2, 12];
let expected_decoded_len = 4;
for i in 0..expected_decoded_len - 1 {
let mut dest = alloc::vec![0; i];
let result = decode(encoded, &mut dest);
assert_eq!(result, Err(DecodeError::TargetBufTooSmall));
}
}
}
+286
View File
@@ -0,0 +1,286 @@
/// The [`CobsEncoder`] type is used to encode a stream of bytes to a
/// given mutable output slice. This is often useful when heap data
/// structures are not available, or when not all message bytes are
/// received at a single point in time.
#[derive(Debug)]
pub struct CobsEncoder<'a> {
dest: &'a mut [u8],
dest_idx: usize,
state: EncoderState,
might_be_done: bool,
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[error("out of bounds error during encoding")]
pub struct DestBufTooSmallError;
/// The [`EncoderState`] is used to track the current state of a
/// streaming encoder. This struct does not contain the output buffer
/// (or a reference to one), and can be used when streaming the encoded
/// output to a custom data type
///
/// **IMPORTANT NOTE**: When implementing a custom streaming encoder,
/// the [`EncoderState`] state machine assumes that the output buffer
/// **ALREADY** contains a single placeholder byte, and no other bytes.
/// This placeholder byte will be later modified with the first distance
/// to the next header/zero byte.
#[derive(Clone, Debug)]
pub struct EncoderState {
code_idx: usize,
num_bt_sent: u8,
offset_idx: u8,
}
/// [`PushResult`] is used to represent the changes to an (encoded)
/// output data buffer when an unencoded byte is pushed into [`EncoderState`].
pub enum PushResult {
/// The returned byte should be placed at the current end of the data buffer
AddSingle(u8),
/// The byte at the given index should be replaced with the given byte.
/// Additionally, a placeholder byte should be inserted at the current
/// end of the output buffer to be later modified
ModifyFromStartAndSkip((usize, u8)),
/// The byte at the given index should be replaced with the given byte.
/// Then, the last u8 in this tuple should be inserted at the end of the
/// current output buffer. Finally, a placeholder byte should be inserted at
/// the current end of the output buffer to be later modified if the encoding process is
/// not done yet.
ModifyFromStartAndPushAndSkip((usize, u8, u8)),
}
impl Default for EncoderState {
/// Create a default initial state representation for a COBS encoder
fn default() -> Self {
Self {
code_idx: 0,
num_bt_sent: 1,
offset_idx: 1,
}
}
}
impl EncoderState {
/// Push a single unencoded byte into the encoder state machine
pub fn push(&mut self, data: u8) -> PushResult {
if data == 0 {
let ret = PushResult::ModifyFromStartAndSkip((self.code_idx, self.num_bt_sent));
self.code_idx += usize::from(self.offset_idx);
self.num_bt_sent = 1;
self.offset_idx = 1;
ret
} else {
self.num_bt_sent += 1;
self.offset_idx += 1;
if 0xFF == self.num_bt_sent {
let ret = PushResult::ModifyFromStartAndPushAndSkip((
self.code_idx,
self.num_bt_sent,
data,
));
self.num_bt_sent = 1;
self.code_idx += usize::from(self.offset_idx);
self.offset_idx = 1;
ret
} else {
PushResult::AddSingle(data)
}
}
}
/// Finalize the encoding process for a single message.
/// The byte at the given index should be replaced with the given value,
/// and the sentinel value (typically 0u8) must be inserted at the current
/// end of the output buffer, serving as a framing byte.
pub fn finalize(self) -> (usize, u8) {
(self.code_idx, self.num_bt_sent)
}
}
impl<'a> CobsEncoder<'a> {
/// Create a new streaming Cobs Encoder
pub fn new(out_buf: &'a mut [u8]) -> CobsEncoder<'a> {
CobsEncoder {
dest: out_buf,
dest_idx: 1,
state: EncoderState::default(),
might_be_done: false,
}
}
/// Push a slice of data to be encoded
pub fn push(&mut self, data: &[u8]) -> Result<(), DestBufTooSmallError> {
// TODO: could probably check if this would fit without
// iterating through all data
// There was the possibility that the encoding process is done, but more data is pushed
// instead of a `finalize` call, so the destination index needs to be incremented.
if self.might_be_done {
self.dest_idx += 1;
self.might_be_done = false;
}
for (slice_idx, val) in data.iter().enumerate() {
use PushResult::*;
match self.state.push(*val) {
AddSingle(y) => {
*self
.dest
.get_mut(self.dest_idx)
.ok_or(DestBufTooSmallError)? = y;
}
ModifyFromStartAndSkip((idx, mval)) => {
*self.dest.get_mut(idx).ok_or(DestBufTooSmallError)? = mval;
}
ModifyFromStartAndPushAndSkip((idx, mval, nval1)) => {
*self.dest.get_mut(idx).ok_or(DestBufTooSmallError)? = mval;
*self
.dest
.get_mut(self.dest_idx)
.ok_or(DestBufTooSmallError)? = nval1;
// Do not increase index if these is the possibility that we are finished.
if slice_idx == data.len() - 1 {
// If push is called again, the index will be incremented. If finalize
// is called, there is no need to increment the index.
self.might_be_done = true;
} else {
self.dest_idx += 1;
}
}
}
// All branches above require advancing the pointer at least once
self.dest_idx += 1;
}
Ok(())
}
/// Complete encoding of the output message. Does NOT terminate
/// the message with the sentinel value
pub fn finalize(self) -> usize {
if self.dest_idx == 1 {
return 0;
}
// Get the last index that needs to be fixed
let (idx, mval) = self.state.finalize();
// If the current code index is outside of the destination slice,
// we do not need to write it out
if let Some(i) = self.dest.get_mut(idx) {
*i = mval;
}
self.dest_idx
}
}
/// Encodes the `source` buffer into the `dest` buffer.
///
/// This function assumes the typical sentinel value of 0, but does not terminate the encoded
/// message with the sentinel value. This should be done by the caller to ensure proper framing.
///
/// # Returns
///
/// The number of bytes written to in the `dest` buffer.
///
/// # Panics
///
/// This function will panic if the `dest` buffer is not large enough for the
/// encoded message. You can calculate the size the `dest` buffer needs to be with
/// the [crate::max_encoding_length] function.
pub fn encode(source: &[u8], dest: &mut [u8]) -> usize {
let mut enc = CobsEncoder::new(dest);
enc.push(source).unwrap();
enc.finalize()
}
/// Attempts to encode the `source` buffer into the `dest` buffer.
///
/// This function assumes the typical sentinel value of 0, but does not terminate the encoded
/// message with the sentinel value. This should be done by the caller to ensure proper framing.
///
/// # Returns
///
/// The number of bytes written to in the `dest` buffer.
///
/// If the destination buffer does not have enough room, an error will be returned.
pub fn try_encode(source: &[u8], dest: &mut [u8]) -> Result<usize, DestBufTooSmallError> {
let mut enc = CobsEncoder::new(dest);
enc.push(source)?;
Ok(enc.finalize())
}
/// Encodes the `source` buffer into the `dest` buffer using an
/// arbitrary sentinel value.
///
/// This is done by first encoding the message with the typical sentinel value
/// of 0, then XOR-ing each byte of the encoded message with the chosen sentinel
/// value. This will ensure that the sentinel value doesn't show up in the encoded
/// message. See the paper "Consistent Overhead Byte Stuffing" for details.
///
/// This function does not terminate the encoded message with the sentinel value. This should be
/// done by the caller to ensure proper framing.
///
/// # Returns
///
/// The number of bytes written to in the `dest` buffer.
pub fn encode_with_sentinel(source: &[u8], dest: &mut [u8], sentinel: u8) -> usize {
let encoded_size = encode(source, dest);
for x in &mut dest[..encoded_size] {
*x ^= sentinel;
}
encoded_size
}
#[cfg(feature = "alloc")]
/// Encodes the `source` buffer into a vector, using the [encode] function.
pub fn encode_vec(source: &[u8]) -> alloc::vec::Vec<u8> {
let mut encoded = alloc::vec![0; crate::max_encoding_length(source.len())];
let encoded_len = encode(source, &mut encoded[..]);
encoded.truncate(encoded_len);
encoded
}
#[cfg(feature = "alloc")]
/// Encodes the `source` buffer into a vector with an arbitrary sentinel value, using the
/// [encode_with_sentinel] function.
pub fn encode_vec_with_sentinel(source: &[u8], sentinel: u8) -> alloc::vec::Vec<u8> {
let mut encoded = alloc::vec![0; crate::max_encoding_length(source.len())];
let encoded_len = encode_with_sentinel(source, &mut encoded[..], sentinel);
encoded.truncate(encoded_len);
encoded
}
#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
use super::*;
#[test]
#[cfg(feature = "alloc")]
fn encode_target_buf_too_small() {
let source = &[10, 11, 0, 12];
let expected = &[3, 10, 11, 2, 12];
for len in 0..expected.len() {
let mut dest = alloc::vec![0; len];
matches!(
try_encode(source, &mut dest).unwrap_err(),
DestBufTooSmallError
);
}
}
#[test]
#[cfg(feature = "alloc")]
#[should_panic]
fn encode_target_buf_too_small_panicking() {
let source = &[10, 11, 0, 12];
let expected = &[3, 10, 11, 2, 12];
encode(source, &mut alloc::vec![0; expected.len() - 1]);
}
}
+89
View File
@@ -0,0 +1,89 @@
//! # `cobs`
//!
//! This is an implementation of the Consistent Overhead Byte Stuffing (COBS) algorithm in Rust.
//!
//! COBS is an algorithm for transforming a message into an encoding where a specific value (the
//! "sentinel" value) is not used. This value can then be used to mark frame boundaries in a serial
//! communication channel.
//!
//! See the [wikipedia article](https://www.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) for details.
//!
//! ## Features
//!
//! `cobs` supports various runtime environments and is also suitable for `no_std` environments.
//!
//! ### Default features
//!
//! - [`std`](https://doc.rust-lang.org/std/): Enables functionality relying on the standard library
//! and also activates the `alloc` feature. Currently only adds [std::error::Error] support for the
//! library error types.
//! - [`alloc`](https://doc.rust-lang.org/alloc/): Enables features which operate on containers
//! like [alloc::vec::Vec](https://doc.rust-lang.org/beta/alloc/vec/struct.Vec.html).
//! Enabled by the `std` feature.
//!
//! ### Optional features
//!
//! - [`defmt`](https://docs.rs/defmt/latest/defmt/): Adds `defmt::Format` derives on some data
//! structures and error types.
//! - [`serde`](https://serde.rs/): Adds `serde` derives on some data structures and error types.
#![no_std]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
// In the future, don't do this.
mod dec;
mod enc;
pub use crate::dec::*;
pub use crate::enc::*;
/// Calculates the maximum overhead when encoding a message with the given length.
/// The overhead is a maximum of [n/254] bytes (one in 254 bytes) rounded up.
#[inline]
pub const fn max_encoding_overhead(source_len: usize) -> usize {
source_len.div_ceil(254)
}
/// Calculates the maximum possible size of an encoded message given the length
/// of the source message. This may be useful for calculating how large the
/// `dest` buffer needs to be in the encoding functions.
#[inline]
pub const fn max_encoding_length(source_len: usize) -> usize {
source_len + max_encoding_overhead(source_len)
}
#[cfg(test)]
mod tests {
use super::*;
// Usable in const context
const ENCODED_BUF: [u8; max_encoding_length(5)] = [0; max_encoding_length(5)];
#[test]
fn test_buf_len() {
assert_eq!(ENCODED_BUF.len(), 6);
}
#[test]
fn test_overhead_empty() {
assert_eq!(max_encoding_overhead(0), 0);
}
#[test]
fn test_overhead_one() {
assert_eq!(max_encoding_overhead(1), 1);
}
#[test]
fn test_overhead_larger() {
assert_eq!(max_encoding_overhead(253), 1);
assert_eq!(max_encoding_overhead(254), 1);
}
#[test]
fn test_overhead_two() {
assert_eq!(max_encoding_overhead(255), 2);
}
}