esp_csi_rs/protocol.rs
1//! ESP-NOW wire-format packets and the magic-prefix framing helpers.
2//!
3//! The central transmits [`ControlPacket`]s and the peripheral replies with
4//! [`PeripheralPacket`] presence beacons. In auto-pairing mode each frame is
5//! prefixed with a 4-byte little-endian magic (see [`serialize_with_magic`] /
6//! [`parse_with_magic`]); in manual-pairing mode no magic is sent and the
7//! source-MAC filter is the discriminator.
8
9use serde::{Deserialize, Serialize};
10
11pub(crate) static CENTRAL_MAGIC_NUMBER: u32 = 0xA8912BF0;
12pub(crate) static PERIPHERAL_MAGIC_NUMBER: u32 = !CENTRAL_MAGIC_NUMBER;
13
14/// Control packet sent from Central to Peripheral.
15///
16/// In auto-pairing mode the serialized frame is prefixed with a 4-byte
17/// little-endian magic (see [`serialize_with_magic`] / [`parse_with_magic`]);
18/// in manual-pairing mode no magic is sent and the source-MAC filter is the
19/// discriminator. Both nodes must agree on the pairing mode (and on the
20/// `statistics` feature, which gates `sequence_number`) for frames to parse.
21#[derive(Serialize, Deserialize, Debug, PartialEq)]
22pub struct ControlPacket {
23 /// Whether the central is currently in collector mode; the peripheral
24 /// mirrors this flag to keep the pair in sync.
25 pub is_collector: bool,
26 /// Monotonic sequence number used to detect drops/reordering. Only present
27 /// when the `statistics` feature is enabled, to keep the frame small.
28 #[cfg(feature = "statistics")]
29 pub sequence_number: u32,
30}
31
32impl ControlPacket {
33 /// Create a new control packet with the collector flag (and, under the
34 /// `statistics` feature, a sequence number).
35 pub fn new(
36 is_collector: bool,
37 #[cfg(feature = "statistics")] sequence_number: u32,
38 ) -> Self {
39 Self {
40 is_collector,
41 #[cfg(feature = "statistics")]
42 sequence_number,
43 }
44 }
45}
46
47/// Peripheral reply packet — a pure presence beacon.
48///
49/// Carries no payload fields; in auto-pairing mode it is sent as the 4-byte
50/// magic prefix alone, and in manual-pairing mode as a single sentinel byte
51/// (ESP-NOW frames should not be zero-length).
52#[derive(Serialize, Deserialize, Debug, PartialEq)]
53pub struct PeripheralPacket;
54
55impl PeripheralPacket {
56 /// Create a new peripheral presence beacon.
57 pub fn new() -> Self {
58 Self
59 }
60}
61
62impl Default for PeripheralPacket {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68/// One-byte sentinel used for manual-mode peripheral replies so the frame is
69/// never zero-length.
70pub(crate) const PERIPHERAL_BEACON_SENTINEL: u8 = 0;
71
72/// Serialize `packet` into `buf`, optionally prefixed with a 4-byte
73/// little-endian `magic`. Returns the populated slice. When `send_magic` is
74/// false only the postcard body is written (manual-pairing mode).
75pub(crate) fn serialize_with_magic<'a, T: Serialize>(
76 packet: &T,
77 magic: u32,
78 send_magic: bool,
79 buf: &'a mut [u8],
80) -> Result<&'a [u8], postcard::Error> {
81 if send_magic {
82 buf[0..4].copy_from_slice(&magic.to_le_bytes());
83 let body = postcard::to_slice(packet, &mut buf[4..])?;
84 let len = 4 + body.len();
85 Ok(&buf[..len])
86 } else {
87 let body = postcard::to_slice(packet, buf)?;
88 let len = body.len();
89 Ok(&buf[..len])
90 }
91}
92
93/// Parse a frame produced by [`serialize_with_magic`]. When `expect_magic` is
94/// true the leading 4 bytes must equal `magic`, otherwise `None` is returned
95/// (caller bumps the magic-drop counter). The remaining bytes are postcard
96/// decoded into `T`.
97///
98/// Uses `take_from_bytes` rather than `from_bytes` so any trailing bytes after
99/// the encoded `T` are ignored — the CPU-test TX pads `ControlPacket` frames up
100/// to the cell payload size, and those pad bytes must not fail the decode.
101pub(crate) fn parse_with_magic<T: for<'de> Deserialize<'de>>(
102 data: &[u8],
103 magic: u32,
104 expect_magic: bool,
105) -> Option<T> {
106 let body = if expect_magic {
107 if data.len() < 4 || u32::from_le_bytes(data[0..4].try_into().ok()?) != magic {
108 return None;
109 }
110 &data[4..]
111 } else {
112 data
113 };
114 postcard::take_from_bytes::<T>(body).ok().map(|(v, _rest)| v)
115}