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