This repository has been archived on 2026-08-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
donglora/vendor/cobs/src/enc.rs
T
swaitsandClaude Opus 4.6 3102fc747a 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>
2026-03-29 20:42:54 -06:00

287 lines
10 KiB
Rust

/// 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]);
}
}