Skip to main content

km003c_lib/
settings.rs

1//! Read-only parsing for the two persisted KM003C settings blocks.
2
3use uom::si::f64::{Ratio, Time};
4use uom::si::ratio::percent;
5use uom::si::time::microsecond;
6
7use crate::error::KMError;
8
9/// Size of the first independently checksummed settings block.
10pub const SETTINGS_A_SIZE: usize = 96;
11/// Size of the second independently checksummed settings block.
12pub const SETTINGS_B_SIZE: usize = 84;
13/// Combined payload size returned by GetData(Settings).
14pub const SETTINGS_SIZE: usize = SETTINGS_A_SIZE + SETTINGS_B_SIZE;
15
16const SETTINGS_A_CHECKSUM_OFFSET: usize = 0x5c;
17const SETTINGS_B_OFFSET: usize = SETTINGS_A_SIZE;
18const SETTINGS_B_CHECKSUM_OFFSET: usize = SETTINGS_B_OFFSET + 0x50;
19const SAMPLE_INTERVAL_OFFSET: usize = 0x08;
20const DEVICE_NAME_OFFSET: usize = 0x70;
21const DEVICE_NAME_END: usize = 0xb0;
22
23mod fields {
24    #![allow(
25        dead_code,
26        reason = "modular-bitfield generates constructors for these read-only views"
27    )]
28
29    use modular_bitfield::prelude::*;
30
31    #[bitfield(bytes = 4)]
32    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33    pub(super) struct SettingsAFields {
34        pub language_selection: B1,
35        #[skip]
36        __: B1,
37        pub uncalibrated: bool,
38        pub brightness_percent: B7,
39        #[skip]
40        __: B22,
41    }
42
43    #[bitfield(bytes = 4)]
44    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
45    pub(super) struct SettingsBFields {
46        pub screen_orientation: B2,
47        pub mtools_device_mode: B2,
48        #[skip]
49        __: B2,
50        pub selected_main_page: B4,
51        #[skip]
52        __: B22,
53    }
54}
55
56use fields::{SettingsAFields, SettingsBFields};
57
58/// Lossless, read-only representation of a GetData(Settings) payload.
59///
60/// Unknown fields remain available through [`Self::settings_a_raw`] and
61/// [`Self::settings_b_raw`]. Only fields whose meanings are corroborated by
62/// KM003C V1.9.9 firmware consumers have semantic accessors.
63#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct Settings {
66    #[cfg_attr(feature = "serde", serde(with = "serde_big_array"))]
67    bytes: [u8; SETTINGS_SIZE],
68}
69
70#[cfg(feature = "serde")]
71mod serde_big_array {
72    //! `[u8; 180]` predates const-generic serde impls, so it needs a shim.
73
74    use super::SETTINGS_SIZE;
75    use serde::{Deserialize, Deserializer, Serializer, de::Error as _};
76
77    pub(super) fn serialize<S: Serializer>(bytes: &[u8; SETTINGS_SIZE], serializer: S) -> Result<S::Ok, S::Error> {
78        serializer.serialize_bytes(bytes)
79    }
80
81    pub(super) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; SETTINGS_SIZE], D::Error> {
82        let bytes = Vec::<u8>::deserialize(deserializer)?;
83        bytes.try_into().map_err(|bytes: Vec<u8>| {
84            D::Error::custom(format!(
85                "settings payload must be exactly {SETTINGS_SIZE} bytes, got {}",
86                bytes.len()
87            ))
88        })
89    }
90}
91
92impl Settings {
93    /// Parse both settings blocks and validate their independent CRC-32 values.
94    pub fn from_bytes(bytes: &[u8]) -> Result<Self, KMError> {
95        let bytes: [u8; SETTINGS_SIZE] = bytes.try_into().map_err(|_| {
96            KMError::InvalidPacket(format!(
97                "Settings payload must be exactly {SETTINGS_SIZE} bytes, got {}",
98                bytes.len()
99            ))
100        })?;
101
102        validate_checksum("Settings-A", &bytes[..SETTINGS_A_SIZE], SETTINGS_A_CHECKSUM_OFFSET)?;
103        validate_checksum(
104            "Settings-B",
105            &bytes[SETTINGS_B_OFFSET..],
106            SETTINGS_B_CHECKSUM_OFFSET - SETTINGS_B_OFFSET,
107        )?;
108
109        Ok(Self { bytes })
110    }
111
112    /// Serialize the exact captured settings payload, including unknown fields.
113    pub fn to_bytes(&self) -> [u8; SETTINGS_SIZE] {
114        self.bytes
115    }
116
117    /// Return the complete first persisted settings block.
118    pub fn settings_a_raw(&self) -> &[u8] {
119        &self.bytes[..SETTINGS_A_SIZE]
120    }
121
122    /// Return the complete second persisted settings block.
123    pub fn settings_b_raw(&self) -> &[u8] {
124        &self.bytes[SETTINGS_B_OFFSET..]
125    }
126
127    /// Raw settings-A flags, including fields whose meanings remain unknown.
128    pub fn settings_a_flags(&self) -> u32 {
129        read_u32(&self.bytes, 0)
130    }
131
132    /// Raw settings-B flags, including fields whose meanings remain unknown.
133    pub fn settings_b_flags(&self) -> u32 {
134        read_u32(&self.bytes, SETTINGS_B_OFFSET)
135    }
136
137    fn settings_a_fields(&self) -> SettingsAFields {
138        SettingsAFields::from_bytes(self.settings_a_flags().to_le_bytes())
139    }
140
141    fn settings_b_fields(&self) -> SettingsBFields {
142        SettingsBFields::from_bytes(self.settings_b_flags().to_le_bytes())
143    }
144
145    /// Firmware language-table selection, encoded as 0 or 1.
146    ///
147    /// The mapping from these indices to a particular language is not yet
148    /// independently confirmed, so this accessor deliberately returns the
149    /// stored index rather than guessing an enum variant.
150    pub fn language_selection(&self) -> u8 {
151        self.settings_a_fields().language_selection()
152    }
153
154    /// Whether firmware marks the device as requiring calibration.
155    pub fn is_uncalibrated(&self) -> bool {
156        self.settings_a_fields().uncalibrated()
157    }
158
159    /// Display brightness as applied by firmware, in percent.
160    ///
161    /// The stored field is seven bits wide; firmware clamps values above 100.
162    pub fn brightness(&self) -> Ratio {
163        let percent_value = self.settings_a_fields().brightness_percent().min(100);
164        Ratio::new::<percent>(f64::from(percent_value))
165    }
166
167    /// ADC sample interval stored in the settings block.
168    pub fn sample_interval(&self) -> Time {
169        Time::new::<microsecond>(f64::from(read_u16(&self.bytes, SAMPLE_INTERVAL_OFFSET)))
170    }
171
172    /// Display orientation index stored by firmware, in the range 0..=3.
173    pub fn screen_orientation(&self) -> u8 {
174        self.settings_b_fields().screen_orientation()
175    }
176
177    /// Mtools device-mode index stored by firmware, in the range 0..=3.
178    pub fn mtools_device_mode(&self) -> u8 {
179        self.settings_b_fields().mtools_device_mode()
180    }
181
182    /// Persisted main-page index, in the range 0..=15.
183    pub fn selected_main_page(&self) -> u8 {
184        self.settings_b_fields().selected_main_page()
185    }
186
187    /// Raw 64-byte device-name field, including trailing zero padding.
188    pub fn device_name_raw(&self) -> &[u8] {
189        &self.bytes[DEVICE_NAME_OFFSET..DEVICE_NAME_END]
190    }
191
192    /// UTF-8 device name when the stored bytes are valid UTF-8.
193    pub fn device_name(&self) -> Option<&str> {
194        let raw = self.device_name_raw();
195        let length = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len());
196        std::str::from_utf8(&raw[..length]).ok()
197    }
198
199    /// Stored CRC-32 for the first settings block.
200    pub fn settings_a_checksum(&self) -> u32 {
201        read_u32(&self.bytes, SETTINGS_A_CHECKSUM_OFFSET)
202    }
203
204    /// Stored CRC-32 for the second settings block.
205    pub fn settings_b_checksum(&self) -> u32 {
206        read_u32(&self.bytes, SETTINGS_B_CHECKSUM_OFFSET)
207    }
208}
209
210fn validate_checksum(name: &str, block: &[u8], checksum_offset: usize) -> Result<(), KMError> {
211    let stored = read_u32(block, checksum_offset);
212    let calculated = crc32fast::hash(&block[..checksum_offset]);
213    if stored != calculated {
214        return Err(KMError::InvalidPacket(format!(
215            "{name} checksum mismatch: stored 0x{stored:08x}, calculated 0x{calculated:08x}"
216        )));
217    }
218    Ok(())
219}
220
221fn read_u16(bytes: &[u8], offset: usize) -> u16 {
222    u16::from_le_bytes(bytes[offset..offset + 2].try_into().expect("validated settings offset"))
223}
224
225fn read_u32(bytes: &[u8], offset: usize) -> u32 {
226    u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("validated settings offset"))
227}
228
229#[cfg(feature = "python")]
230impl<'py> pyo3::IntoPyObject<'py> for Settings {
231    type Target = pyo3::types::PyDict;
232    type Output = pyo3::Bound<'py, Self::Target>;
233    type Error = pyo3::PyErr;
234
235    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
236        use pyo3::types::PyDictMethods;
237        use uom::si::ratio::percent;
238        use uom::si::time::microsecond;
239
240        let dict = pyo3::types::PyDict::new(py);
241        dict.set_item("settings_a_flags", self.settings_a_flags())?;
242        dict.set_item("settings_b_flags", self.settings_b_flags())?;
243        dict.set_item("language_selection", self.language_selection())?;
244        dict.set_item("uncalibrated", self.is_uncalibrated())?;
245        dict.set_item("brightness_percent", self.brightness().get::<percent>())?;
246        dict.set_item("sample_interval_us", self.sample_interval().get::<microsecond>())?;
247        dict.set_item("screen_orientation", self.screen_orientation())?;
248        dict.set_item("mtools_device_mode", self.mtools_device_mode())?;
249        dict.set_item("selected_main_page", self.selected_main_page())?;
250        dict.set_item("device_name", self.device_name())?;
251        dict.set_item("settings_a_checksum", self.settings_a_checksum())?;
252        dict.set_item("settings_b_checksum", self.settings_b_checksum())?;
253        dict.set_item("raw", hex::encode(self.to_bytes()))?;
254        Ok(dict)
255    }
256}