umsh_ulcp/
battery_gauge_telemetry.rs

1//! Fast, read-only BQ27220 standard-command telemetry.
2use 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
20// TI SLUUBD4A standard commands. Ordering is part of version 1.
21fields![
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    /// Original register bits, including documented sentinel values.
43    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}