1use core::num::NonZeroU8;
8
9pub const TX_POWER_DEFAULT: i8 = 0x7F;
11pub const TX_POWER_MAX: i8 = 0x7E;
13
14pub const TX_FLAG_NOCCA: u8 = 1 << 0;
16pub const TX_FLAG_NODUTY: u8 = 1 << 1;
18
19pub const RX_FLAG_BUFFERED: u8 = 1 << 0;
22pub const RX_FLAG_ACKED: u8 = 1 << 1;
25pub const RX_FLAG_SELF_TX: u8 = 1 << 2;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum MetaError {
32 Truncated,
34 BufferTooSmall,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct TxMeta {
41 pub power: i8,
44 pub flags: u8,
46}
47
48impl Default for TxMeta {
49 fn default() -> Self {
50 Self {
51 power: TX_POWER_DEFAULT,
52 flags: 0,
53 }
54 }
55}
56
57impl TxMeta {
58 pub const WIRE_LEN: usize = 2;
59
60 pub fn encode(self, out: &mut [u8]) -> Result<usize, MetaError> {
61 let [power, flags, ..] = out else {
62 return Err(MetaError::BufferTooSmall);
63 };
64 *power = self.power as u8;
65 *flags = self.flags;
66 Ok(Self::WIRE_LEN)
67 }
68
69 pub fn decode(input: &[u8]) -> Result<Self, MetaError> {
70 match input {
71 [] => Ok(Self::default()),
72 [power, flags, ..] => Ok(Self {
73 power: *power as i8,
74 flags: *flags,
75 }),
76 _ => Err(MetaError::Truncated),
77 }
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86pub struct RxMeta {
87 pub rssi_dbm: Option<i16>,
91 pub lqi: Option<NonZeroU8>,
93 pub snr_cb: Option<i16>,
98}
99
100impl RxMeta {
101 pub const WIRE_LEN: usize = 4;
102
103 pub fn encode(self, out: &mut [u8]) -> Result<usize, MetaError> {
104 if out.len() < Self::WIRE_LEN {
105 return Err(MetaError::BufferTooSmall);
106 }
107 out[0] = match self.rssi_dbm {
108 None => 0xFF,
109 Some(rssi) => (-rssi).clamp(0, 254) as u8,
110 };
111 out[1] = self.lqi.map(NonZeroU8::get).unwrap_or(0);
112 let snr = self.snr_cb.unwrap_or(i16::MIN);
115 out[2..4].copy_from_slice(&snr.to_le_bytes());
116 Ok(Self::WIRE_LEN)
117 }
118
119 pub fn decode(input: &[u8]) -> Result<Self, MetaError> {
120 match input {
121 [] => Ok(Self::default()),
122 [rssi, lqi, snr_lo, snr_hi, ..] => {
123 let snr = i16::from_le_bytes([*snr_lo, *snr_hi]);
124 Ok(Self {
125 rssi_dbm: (*rssi != 0xFF).then(|| -i16::from(*rssi)),
126 lqi: NonZeroU8::new(*lqi),
127 snr_cb: (snr != i16::MIN).then_some(snr),
128 })
129 }
130 _ => Err(MetaError::Truncated),
131 }
132 }
133}
134
135#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
142pub struct BufferedRxMeta {
143 pub rx: RxMeta,
144 pub flags: u8,
146 pub age_s: u32,
148}
149
150impl BufferedRxMeta {
151 pub const WIRE_LEN: usize = RxMeta::WIRE_LEN + 5;
152
153 pub fn encode(self, out: &mut [u8]) -> Result<usize, MetaError> {
154 if out.len() < Self::WIRE_LEN {
155 return Err(MetaError::BufferTooSmall);
156 }
157 self.rx.encode(out)?;
158 out[RxMeta::WIRE_LEN] = self.flags;
159 out[RxMeta::WIRE_LEN + 1..Self::WIRE_LEN].copy_from_slice(&self.age_s.to_le_bytes());
160 Ok(Self::WIRE_LEN)
161 }
162
163 pub fn decode(input: &[u8]) -> Result<Self, MetaError> {
164 let rx = RxMeta::decode(input)?;
165 let trailer = input.get(RxMeta::WIRE_LEN..).unwrap_or(&[]);
166 let (flags, age_s) = match trailer {
167 [] => (0, 0),
168 [flags] => (*flags, 0),
169 [flags, age @ ..] if age.len() >= 4 => (
170 *flags,
171 u32::from_le_bytes(age[..4].try_into().expect("length checked")),
172 ),
173 _ => return Err(MetaError::Truncated),
174 };
175 Ok(Self { rx, flags, age_s })
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn tx_round_trip() {
185 let meta = TxMeta {
186 power: 22,
187 flags: TX_FLAG_NODUTY,
188 };
189 let mut buf = [0u8; TxMeta::WIRE_LEN];
190 assert_eq!(meta.encode(&mut buf).unwrap(), TxMeta::WIRE_LEN);
191 assert_eq!(buf, [22, 0x02]);
192 assert_eq!(TxMeta::decode(&buf).unwrap(), meta);
193 }
194
195 #[test]
196 fn tx_absent_is_default() {
197 assert_eq!(TxMeta::decode(&[]).unwrap(), TxMeta::default());
198 assert_eq!(TxMeta::default().power, TX_POWER_DEFAULT);
199 assert_eq!(TxMeta::decode(&[0x00]), Err(MetaError::Truncated));
200 }
201
202 #[test]
203 fn rx_round_trip() {
204 let meta = RxMeta {
205 rssi_dbm: Some(-91),
206 lqi: NonZeroU8::new(200),
207 snr_cb: Some(-53),
208 };
209 let mut buf = [0u8; RxMeta::WIRE_LEN];
210 meta.encode(&mut buf).unwrap();
211 assert_eq!(buf[0], 91);
213 assert_eq!(RxMeta::decode(&buf).unwrap(), meta);
214 }
215
216 #[test]
217 fn rx_sentinels() {
218 let mut buf = [0u8; RxMeta::WIRE_LEN];
219 RxMeta::default().encode(&mut buf).unwrap();
220 assert_eq!(buf, [0xFF, 0x00, 0x00, 0x80]);
222 assert_eq!(RxMeta::decode(&buf).unwrap(), RxMeta::default());
223 assert_eq!(RxMeta::decode(&[]).unwrap(), RxMeta::default());
224 assert_eq!(RxMeta::decode(&[91, 0, 0]), Err(MetaError::Truncated));
225 }
226
227 #[test]
228 fn buffered_round_trip_and_boundary_truncation() {
229 let meta = BufferedRxMeta {
230 rx: RxMeta {
231 rssi_dbm: Some(-101),
232 lqi: NonZeroU8::new(17),
233 snr_cb: Some(-22),
234 },
235 flags: RX_FLAG_BUFFERED | RX_FLAG_ACKED,
236 age_s: 3_601,
237 };
238 let mut buf = [0u8; BufferedRxMeta::WIRE_LEN];
239 assert_eq!(meta.encode(&mut buf).unwrap(), BufferedRxMeta::WIRE_LEN);
240 assert_eq!(BufferedRxMeta::decode(&buf).unwrap(), meta);
241
242 assert_eq!(
244 BufferedRxMeta::decode(&[]).unwrap(),
245 BufferedRxMeta::default()
246 );
247 let base_only = BufferedRxMeta::decode(&buf[..RxMeta::WIRE_LEN]).unwrap();
248 assert_eq!(base_only.rx, meta.rx);
249 assert_eq!((base_only.flags, base_only.age_s), (0, 0));
250 let with_flags = BufferedRxMeta::decode(&buf[..RxMeta::WIRE_LEN + 1]).unwrap();
251 assert_eq!(with_flags.flags, meta.flags);
252 assert_eq!(with_flags.age_s, 0);
253
254 for len in RxMeta::WIRE_LEN + 2..BufferedRxMeta::WIRE_LEN {
256 assert_eq!(
257 BufferedRxMeta::decode(&buf[..len]),
258 Err(MetaError::Truncated)
259 );
260 }
261 }
262
263 #[test]
264 fn buffered_decode_matches_minimal_live_encoding() {
265 let rx = RxMeta {
268 rssi_dbm: Some(-91),
269 lqi: None,
270 snr_cb: Some(55),
271 };
272 let mut buf = [0u8; RxMeta::WIRE_LEN];
273 rx.encode(&mut buf).unwrap();
274 let buffered = BufferedRxMeta::decode(&buf).unwrap();
275 assert_eq!(
276 buffered,
277 BufferedRxMeta {
278 rx,
279 flags: 0,
280 age_s: 0
281 }
282 );
283 }
284
285 #[test]
286 fn rx_snr_negative_one_round_trips() {
287 let meta = RxMeta {
290 rssi_dbm: Some(0),
291 lqi: None,
292 snr_cb: Some(-1),
293 };
294 let mut buf = [0u8; RxMeta::WIRE_LEN];
295 meta.encode(&mut buf).unwrap();
296 assert_eq!(RxMeta::decode(&buf).unwrap().snr_cb, Some(-1));
297 }
298}