use uom::si::f64::{Ratio, Time};
use uom::si::ratio::percent;
use uom::si::time::microsecond;
use crate::error::KMError;
pub const SETTINGS_A_SIZE: usize = 96;
pub const SETTINGS_B_SIZE: usize = 84;
pub const SETTINGS_SIZE: usize = SETTINGS_A_SIZE + SETTINGS_B_SIZE;
const SETTINGS_A_CHECKSUM_OFFSET: usize = 0x5c;
const SETTINGS_B_OFFSET: usize = SETTINGS_A_SIZE;
const SETTINGS_B_CHECKSUM_OFFSET: usize = SETTINGS_B_OFFSET + 0x50;
const SAMPLE_INTERVAL_OFFSET: usize = 0x08;
const DEVICE_NAME_OFFSET: usize = 0x70;
const DEVICE_NAME_END: usize = 0xb0;
mod fields {
#![allow(
dead_code,
reason = "modular-bitfield generates constructors for these read-only views"
)]
use modular_bitfield::prelude::*;
#[bitfield(bytes = 4)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct SettingsAFields {
pub language_selection: B1,
#[skip]
__: B1,
pub uncalibrated: bool,
pub brightness_percent: B7,
#[skip]
__: B22,
}
#[bitfield(bytes = 4)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct SettingsBFields {
pub screen_orientation: B2,
pub mtools_device_mode: B2,
#[skip]
__: B2,
pub selected_main_page: B4,
#[skip]
__: B22,
}
}
use fields::{SettingsAFields, SettingsBFields};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Settings {
#[cfg_attr(feature = "serde", serde(with = "serde_big_array"))]
bytes: [u8; SETTINGS_SIZE],
}
#[cfg(feature = "serde")]
mod serde_big_array {
use super::SETTINGS_SIZE;
use serde::{Deserialize, Deserializer, Serializer, de::Error as _};
pub(super) fn serialize<S: Serializer>(bytes: &[u8; SETTINGS_SIZE], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(bytes)
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; SETTINGS_SIZE], D::Error> {
let bytes = Vec::<u8>::deserialize(deserializer)?;
bytes.try_into().map_err(|bytes: Vec<u8>| {
D::Error::custom(format!(
"settings payload must be exactly {SETTINGS_SIZE} bytes, got {}",
bytes.len()
))
})
}
}
impl Settings {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, KMError> {
let bytes: [u8; SETTINGS_SIZE] = bytes.try_into().map_err(|_| {
KMError::InvalidPacket(format!(
"Settings payload must be exactly {SETTINGS_SIZE} bytes, got {}",
bytes.len()
))
})?;
validate_checksum("Settings-A", &bytes[..SETTINGS_A_SIZE], SETTINGS_A_CHECKSUM_OFFSET)?;
validate_checksum(
"Settings-B",
&bytes[SETTINGS_B_OFFSET..],
SETTINGS_B_CHECKSUM_OFFSET - SETTINGS_B_OFFSET,
)?;
Ok(Self { bytes })
}
pub fn to_bytes(&self) -> [u8; SETTINGS_SIZE] {
self.bytes
}
pub fn settings_a_raw(&self) -> &[u8] {
&self.bytes[..SETTINGS_A_SIZE]
}
pub fn settings_b_raw(&self) -> &[u8] {
&self.bytes[SETTINGS_B_OFFSET..]
}
pub fn settings_a_flags(&self) -> u32 {
read_u32(&self.bytes, 0)
}
pub fn settings_b_flags(&self) -> u32 {
read_u32(&self.bytes, SETTINGS_B_OFFSET)
}
fn settings_a_fields(&self) -> SettingsAFields {
SettingsAFields::from_bytes(self.settings_a_flags().to_le_bytes())
}
fn settings_b_fields(&self) -> SettingsBFields {
SettingsBFields::from_bytes(self.settings_b_flags().to_le_bytes())
}
pub fn language_selection(&self) -> u8 {
self.settings_a_fields().language_selection()
}
pub fn is_uncalibrated(&self) -> bool {
self.settings_a_fields().uncalibrated()
}
pub fn brightness(&self) -> Ratio {
let percent_value = self.settings_a_fields().brightness_percent().min(100);
Ratio::new::<percent>(f64::from(percent_value))
}
pub fn sample_interval(&self) -> Time {
Time::new::<microsecond>(f64::from(read_u16(&self.bytes, SAMPLE_INTERVAL_OFFSET)))
}
pub fn screen_orientation(&self) -> u8 {
self.settings_b_fields().screen_orientation()
}
pub fn mtools_device_mode(&self) -> u8 {
self.settings_b_fields().mtools_device_mode()
}
pub fn selected_main_page(&self) -> u8 {
self.settings_b_fields().selected_main_page()
}
pub fn device_name_raw(&self) -> &[u8] {
&self.bytes[DEVICE_NAME_OFFSET..DEVICE_NAME_END]
}
pub fn device_name(&self) -> Option<&str> {
let raw = self.device_name_raw();
let length = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len());
std::str::from_utf8(&raw[..length]).ok()
}
pub fn settings_a_checksum(&self) -> u32 {
read_u32(&self.bytes, SETTINGS_A_CHECKSUM_OFFSET)
}
pub fn settings_b_checksum(&self) -> u32 {
read_u32(&self.bytes, SETTINGS_B_CHECKSUM_OFFSET)
}
}
fn validate_checksum(name: &str, block: &[u8], checksum_offset: usize) -> Result<(), KMError> {
let stored = read_u32(block, checksum_offset);
let calculated = crc32fast::hash(&block[..checksum_offset]);
if stored != calculated {
return Err(KMError::InvalidPacket(format!(
"{name} checksum mismatch: stored 0x{stored:08x}, calculated 0x{calculated:08x}"
)));
}
Ok(())
}
fn read_u16(bytes: &[u8], offset: usize) -> u16 {
u16::from_le_bytes(bytes[offset..offset + 2].try_into().expect("validated settings offset"))
}
fn read_u32(bytes: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("validated settings offset"))
}
#[cfg(feature = "python")]
impl<'py> pyo3::IntoPyObject<'py> for Settings {
type Target = pyo3::types::PyDict;
type Output = pyo3::Bound<'py, Self::Target>;
type Error = pyo3::PyErr;
fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
use pyo3::types::PyDictMethods;
use uom::si::ratio::percent;
use uom::si::time::microsecond;
let dict = pyo3::types::PyDict::new(py);
dict.set_item("settings_a_flags", self.settings_a_flags())?;
dict.set_item("settings_b_flags", self.settings_b_flags())?;
dict.set_item("language_selection", self.language_selection())?;
dict.set_item("uncalibrated", self.is_uncalibrated())?;
dict.set_item("brightness_percent", self.brightness().get::<percent>())?;
dict.set_item("sample_interval_us", self.sample_interval().get::<microsecond>())?;
dict.set_item("screen_orientation", self.screen_orientation())?;
dict.set_item("mtools_device_mode", self.mtools_device_mode())?;
dict.set_item("selected_main_page", self.selected_main_page())?;
dict.set_item("device_name", self.device_name())?;
dict.set_item("settings_a_checksum", self.settings_a_checksum())?;
dict.set_item("settings_b_checksum", self.settings_b_checksum())?;
dict.set_item("raw", hex::encode(self.to_bytes()))?;
Ok(dict)
}
}