1use uom::si::f64::{Ratio, Time};
4use uom::si::ratio::percent;
5use uom::si::time::microsecond;
6
7use crate::error::KMError;
8
9pub const SETTINGS_A_SIZE: usize = 96;
11pub const SETTINGS_B_SIZE: usize = 84;
13pub 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#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Settings {
65 bytes: [u8; SETTINGS_SIZE],
66}
67
68impl Settings {
69 pub fn from_bytes(bytes: &[u8]) -> Result<Self, KMError> {
71 let bytes: [u8; SETTINGS_SIZE] = bytes.try_into().map_err(|_| {
72 KMError::InvalidPacket(format!(
73 "Settings payload must be exactly {SETTINGS_SIZE} bytes, got {}",
74 bytes.len()
75 ))
76 })?;
77
78 validate_checksum("Settings-A", &bytes[..SETTINGS_A_SIZE], SETTINGS_A_CHECKSUM_OFFSET)?;
79 validate_checksum(
80 "Settings-B",
81 &bytes[SETTINGS_B_OFFSET..],
82 SETTINGS_B_CHECKSUM_OFFSET - SETTINGS_B_OFFSET,
83 )?;
84
85 Ok(Self { bytes })
86 }
87
88 pub fn to_bytes(&self) -> [u8; SETTINGS_SIZE] {
90 self.bytes
91 }
92
93 pub fn settings_a_raw(&self) -> &[u8] {
95 &self.bytes[..SETTINGS_A_SIZE]
96 }
97
98 pub fn settings_b_raw(&self) -> &[u8] {
100 &self.bytes[SETTINGS_B_OFFSET..]
101 }
102
103 pub fn settings_a_flags(&self) -> u32 {
105 read_u32(&self.bytes, 0)
106 }
107
108 pub fn settings_b_flags(&self) -> u32 {
110 read_u32(&self.bytes, SETTINGS_B_OFFSET)
111 }
112
113 fn settings_a_fields(&self) -> SettingsAFields {
114 SettingsAFields::from_bytes(self.settings_a_flags().to_le_bytes())
115 }
116
117 fn settings_b_fields(&self) -> SettingsBFields {
118 SettingsBFields::from_bytes(self.settings_b_flags().to_le_bytes())
119 }
120
121 pub fn language_selection(&self) -> u8 {
127 self.settings_a_fields().language_selection()
128 }
129
130 pub fn is_uncalibrated(&self) -> bool {
132 self.settings_a_fields().uncalibrated()
133 }
134
135 pub fn brightness(&self) -> Ratio {
139 let percent_value = self.settings_a_fields().brightness_percent().min(100);
140 Ratio::new::<percent>(f64::from(percent_value))
141 }
142
143 pub fn sample_interval(&self) -> Time {
145 Time::new::<microsecond>(f64::from(read_u16(&self.bytes, SAMPLE_INTERVAL_OFFSET)))
146 }
147
148 pub fn screen_orientation(&self) -> u8 {
150 self.settings_b_fields().screen_orientation()
151 }
152
153 pub fn mtools_device_mode(&self) -> u8 {
155 self.settings_b_fields().mtools_device_mode()
156 }
157
158 pub fn selected_main_page(&self) -> u8 {
160 self.settings_b_fields().selected_main_page()
161 }
162
163 pub fn device_name_raw(&self) -> &[u8] {
165 &self.bytes[DEVICE_NAME_OFFSET..DEVICE_NAME_END]
166 }
167
168 pub fn device_name(&self) -> Option<&str> {
170 let raw = self.device_name_raw();
171 let length = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len());
172 std::str::from_utf8(&raw[..length]).ok()
173 }
174
175 pub fn settings_a_checksum(&self) -> u32 {
177 read_u32(&self.bytes, SETTINGS_A_CHECKSUM_OFFSET)
178 }
179
180 pub fn settings_b_checksum(&self) -> u32 {
182 read_u32(&self.bytes, SETTINGS_B_CHECKSUM_OFFSET)
183 }
184}
185
186fn validate_checksum(name: &str, block: &[u8], checksum_offset: usize) -> Result<(), KMError> {
187 let stored = read_u32(block, checksum_offset);
188 let calculated = crc32fast::hash(&block[..checksum_offset]);
189 if stored != calculated {
190 return Err(KMError::InvalidPacket(format!(
191 "{name} checksum mismatch: stored 0x{stored:08x}, calculated 0x{calculated:08x}"
192 )));
193 }
194 Ok(())
195}
196
197fn read_u16(bytes: &[u8], offset: usize) -> u16 {
198 u16::from_le_bytes(bytes[offset..offset + 2].try_into().expect("validated settings offset"))
199}
200
201fn read_u32(bytes: &[u8], offset: usize) -> u32 {
202 u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("validated settings offset"))
203}
204
205#[cfg(feature = "python")]
206impl<'py> pyo3::IntoPyObject<'py> for Settings {
207 type Target = pyo3::types::PyDict;
208 type Output = pyo3::Bound<'py, Self::Target>;
209 type Error = pyo3::PyErr;
210
211 fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
212 use pyo3::types::PyDictMethods;
213 use uom::si::ratio::percent;
214 use uom::si::time::microsecond;
215
216 let dict = pyo3::types::PyDict::new(py);
217 dict.set_item("settings_a_flags", self.settings_a_flags())?;
218 dict.set_item("settings_b_flags", self.settings_b_flags())?;
219 dict.set_item("language_selection", self.language_selection())?;
220 dict.set_item("uncalibrated", self.is_uncalibrated())?;
221 dict.set_item("brightness_percent", self.brightness().get::<percent>())?;
222 dict.set_item("sample_interval_us", self.sample_interval().get::<microsecond>())?;
223 dict.set_item("screen_orientation", self.screen_orientation())?;
224 dict.set_item("mtools_device_mode", self.mtools_device_mode())?;
225 dict.set_item("selected_main_page", self.selected_main_page())?;
226 dict.set_item("device_name", self.device_name())?;
227 dict.set_item("settings_a_checksum", self.settings_a_checksum())?;
228 dict.set_item("settings_b_checksum", self.settings_b_checksum())?;
229 dict.set_item("raw", hex::encode(self.to_bytes()))?;
230 Ok(dict)
231 }
232}