1use crate::{Status, pui, sint, uint};
3
4pub const FORMAT: u32 = 1;
5pub const VERSION: u32 = 1;
6
7pub struct Field {
8 pub name: &'static str,
9 pub register: u8,
10 pub unit: &'static str,
11 pub signed: bool,
12}
13
14macro_rules! fields {
15 ($(($name:literal, $register:literal, $unit:literal, $signed:literal)),* $(,)?) => {
16 pub const FIELDS: &[Field] = &[$(Field { name: $name, register: $register, unit: $unit, signed: $signed }),*];
17 };
18}
19
20fields![
22 ("raw_coulomb_count", 0x22, "mAh", true),
23 ("temperature", 0x06, "0.1 K", false),
24 ("internal_temperature", 0x28, "0.1 K", false),
25 ("average_current", 0x14, "mA", true),
26 ("average_power", 0x24, "mW", true),
27 ("time_to_empty", 0x16, "min", false),
28 ("time_to_full", 0x18, "min", false),
29 ("standby_current", 0x1a, "mA", true),
30 ("standby_time_to_empty", 0x1c, "min", false),
31 ("max_load_current", 0x1e, "mA", true),
32 ("max_load_time_to_empty", 0x20, "min", false),
33 ("cycle_count", 0x2a, "cycles", false),
34 ("state_of_health", 0x2e, "%", false),
35 ("charging_current", 0x32, "mA", false),
36];
37
38pub const MAX_ENCODED: usize = 4 + FIELDS.len() * 2;
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub struct Telemetry {
42 pub raw: [u16; FIELDS.len()],
44}
45
46impl Telemetry {
47 pub fn value(&self, index: usize) -> i32 {
48 if FIELDS[index].signed {
49 i32::from(self.raw[index] as i16)
50 } else {
51 i32::from(self.raw[index])
52 }
53 }
54
55 pub fn encode(&self, out: &mut [u8]) -> Result<usize, Status> {
56 let mut used = pui::encode(FORMAT, out).map_err(|_| Status::NOMEM)?;
57 used += pui::encode(VERSION, &mut out[used..]).map_err(|_| Status::NOMEM)?;
58 let width_offset = used;
59 out.get_mut(used..used + 2).ok_or(Status::NOMEM)?.fill(0);
60 used += 2;
61 let mut widths = 0u16;
62 for (i, field) in FIELDS.iter().enumerate() {
63 let len = if field.signed {
64 sint::encode(self.value(i), &mut out[used..]).map_err(|_| Status::NOMEM)?
65 } else {
66 uint::encode(self.raw[i].into(), &mut out[used..]).map_err(|_| Status::NOMEM)?
67 };
68 widths |= ((len - 1) as u16) << i;
69 used += len;
70 }
71 out[width_offset..width_offset + 2].copy_from_slice(&widths.to_le_bytes());
72 Ok(used)
73 }
74
75 pub fn decode(mut bytes: &[u8]) -> Result<Self, Status> {
76 fn next(bytes: &mut &[u8]) -> Result<u32, Status> {
77 let (v, n) = pui::decode(bytes).map_err(|_| Status::PARSE_ERROR)?;
78 *bytes = &bytes[n..];
79 Ok(v)
80 }
81 if next(&mut bytes)? != FORMAT || next(&mut bytes)? != VERSION {
82 return Err(Status::UNIMPLEMENTED);
83 }
84 let widths = u16::from_le_bytes(
85 bytes
86 .get(..2)
87 .ok_or(Status::PARSE_ERROR)?
88 .try_into()
89 .unwrap(),
90 );
91 bytes = &bytes[2..];
92 if widths >> FIELDS.len() != 0 {
93 return Err(Status::PARSE_ERROR);
94 }
95 let mut result = Self::default();
96 for (i, field) in FIELDS.iter().enumerate() {
97 let len = 1 + usize::from((widths >> i) & 1);
98 let value = bytes.get(..len).ok_or(Status::PARSE_ERROR)?;
99 result.raw[i] = if field.signed {
100 sint::decode(value).map_err(|_| Status::PARSE_ERROR)? as u16
101 } else {
102 uint::decode(value).map_err(|_| Status::PARSE_ERROR)? as u16
103 };
104 bytes = &bytes[len..];
105 }
106 if !bytes.is_empty() {
107 return Err(Status::PARSE_ERROR);
108 }
109 Ok(result)
110 }
111}
112
113impl core::fmt::Display for Telemetry {
114 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115 writeln!(f, "BQ27220 telemetry v1")?;
116 for (i, field) in FIELDS.iter().enumerate() {
117 write!(f, " {}: ", field.name)?;
118 if field.unit == "min" && self.raw[i] == u16::MAX {
119 writeln!(f, "unavailable (raw 65535)")?;
120 } else if field.register == 0x32 && self.raw[i] == u16::MAX {
121 writeln!(f, "maximum requested")?;
122 } else if field.unit == "0.1 K" {
123 let celsius = f64::from(self.raw[i]) / 10.0 - 273.15;
124 writeln!(f, "{celsius:.2} °C ({} {})", self.raw[i], field.unit)?;
125 } else {
126 writeln!(f, "{} {}", self.value(i), field.unit)?;
127 }
128 }
129 Ok(())
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn compact_widths_preserve_sign_sentinels_and_full_counter_range() {
139 let sample = Telemetry {
140 raw: [
141 65535,
142 2981,
143 3012,
144 (-100i16) as u16,
145 (-200i16) as u16,
146 65535,
147 0,
148 (-128i16) as u16,
149 120,
150 (-129i16) as u16,
151 256,
152 180,
153 91,
154 65535,
155 ],
156 };
157 let mut bytes = [0; MAX_ENCODED];
158 let len = sample.encode(&mut bytes).unwrap();
159 assert_eq!(len, 25);
160 assert_eq!(&bytes[..4], &[1, 1, 0x36, 0x26]);
161 assert_eq!(&bytes[9..12], &[0x9c, 0x38, 0xff]);
162 assert_eq!(Telemetry::decode(&bytes[..len]), Ok(sample));
163 assert_eq!(sample.value(3), -100);
164 assert_eq!(sample.value(0), -1);
165 for end in 0..len {
166 assert!(Telemetry::decode(&bytes[..end]).is_err());
167 let mut short = [0; MAX_ENCODED];
168 assert_eq!(sample.encode(&mut short[..end]), Err(Status::NOMEM));
169 }
170 assert!(Telemetry::decode(&bytes[..len + 1]).is_err());
171 bytes[3] |= 0x80;
172 assert_eq!(Telemetry::decode(&bytes[..len]), Err(Status::PARSE_ERROR));
173 bytes[1] = 2;
174 assert_eq!(Telemetry::decode(&bytes[..len]), Err(Status::UNIMPLEMENTED));
175 let extremes = Telemetry {
176 raw: [0x8000; FIELDS.len()],
177 };
178 assert_eq!(extremes.encode(&mut bytes), Ok(MAX_ENCODED));
179 assert_eq!(Telemetry::decode(&bytes), Ok(extremes));
180 }
181
182 #[test]
183 fn coulomb_counter_round_trips_every_signed_value_at_minimum_width() {
184 for value in i16::MIN..=i16::MAX {
185 let mut sample = Telemetry::default();
186 sample.raw[0] = value as u16;
187 let mut bytes = [0; MAX_ENCODED];
188 let len = sample.encode(&mut bytes).unwrap();
189 let two_bytes = !(-128..=127).contains(&value);
190 assert_eq!(len, 18 + usize::from(two_bytes));
191 assert_eq!(bytes[2] & 1, u8::from(two_bytes));
192 assert_eq!(
193 Telemetry::decode(&bytes[..len]).unwrap().value(0),
194 i32::from(value)
195 );
196 }
197 }
198}