use std::{collections::BTreeMap, ops::RangeInclusive}; use crate::{ encoding::encode_vlq_int, messages::{MessageParser, MessageSkipperError}, }; /// Deserialised data dictionary returned by the microcontroller. /// See [klipper protocol docs](https://www.klipper3d.org/Protocol.html#data-dictionary) #[derive(Debug)] pub struct Dictionary { /// Map of message names to message IDs message_ids: BTreeMap, /// Map of message IDs to parsers message_parsers: BTreeMap, /// Map of config variable name to variable values config: BTreeMap, /// Map of declared [enumerations](https://www.klipper3d.org/Protocol.html#declaring-enumerations) enumerations: BTreeMap, /// Build version, if specified build_version: Option, /// Version, if specified version: Option, /// Any extra data in the dictionary response extra: BTreeMap, } impl Dictionary { /// Get message id by name pub fn message_id(&self, name: &str) -> Option { self.message_ids.get(name).copied() } /// Get message name by id pub fn message_name(&self, id: u16) -> Option<&str> { self.message_ids .iter() .find(|(_, i)| **i == id) .map(|(name, _)| name.as_str()) } /// Get message name by id pub fn message_parser(&self, id: u16) -> Option<&MessageParser> { self.message_parsers.get(&id) } /// Get config variable by name pub fn config_var(&self, var_name: &str) -> Option<&ConfigVar> { self.config.get(var_name) } /// Get enum definition by name pub fn enum_def(&self, name: &str) -> Option<&EnumDef> { self.enumerations.get(name) } /// Firmware build version, if specified pub fn build_version(&self) -> Option<&str> { self.build_version.as_deref() } /// Firmware version, if specified pub fn version(&self) -> Option<&str> { self.version.as_deref() } /// Extra data returned in the dictionary response pub fn extra(&self) -> &BTreeMap { &self.extra } /// Figure out the actual value of a command tag fn map_tag(tag: i16) -> Result { let mut buf = vec![]; encode_vlq_int(&mut buf, tag as u32); let v = if buf.len() > 1 { ((buf[0] as u16) & 0x7F) << 7 | (buf[1] as u16) & 0x7F } else { (buf[0] as u16) & 0x7F }; if v >= 1 << 14 { Err(DictionaryError::InvalidCommandTag(v)) } else { Ok(v) } } } impl TryFrom for Dictionary { type Error = DictionaryError; fn try_from(raw: RawDictionary) -> Result { let mut message_ids = BTreeMap::new(); let mut message_parsers = BTreeMap::new(); for (cmd, tag) in raw.commands { let mut split = cmd.split(' '); let name = split.next().ok_or(DictionaryError::EmptyCommand)?; let parser = MessageParser::new(name, split) .map_err(|e| DictionaryError::InvalidCommandFormat(name.to_string(), e))?; let tag = Self::map_tag(tag)?; message_parsers.insert(tag, parser); message_ids.insert(name.to_string(), tag); } for (resp, tag) in raw.responses { let mut split = resp.split(' '); let name = split.next().ok_or(DictionaryError::EmptyCommand)?; let parser = MessageParser::new(name, split) .map_err(|e| DictionaryError::InvalidCommandFormat(name.to_string(), e))?; let tag = Self::map_tag(tag)?; message_parsers.insert(tag, parser); message_ids.insert(name.to_string(), tag); } for (msg, tag) in raw.output { let parser = MessageParser::new_output(&msg) .map_err(|e| DictionaryError::InvalidCommandFormat(msg.to_string(), e))?; let tag = Self::map_tag(tag)?; message_parsers.insert(tag, parser); } Ok(Dictionary { message_ids, message_parsers, config: raw.config, enumerations: raw .enumerations .into_iter() .map(|(name, vals)| (name, EnumDef(vals))) .collect(), build_version: raw.build_versions, version: raw.version, extra: raw.extra, }) } } /// Definition of an [enumerations](https://www.klipper3d.org/Protocol.html#declaring-enumerations)'s possible variables. #[derive(Debug)] pub struct EnumDef(BTreeMap); impl EnumDef { /// Get the range of valid values for this enum /// Note that there's no guarantee the enum is actually contiguous for a range. pub fn valid_range(&self) -> RangeInclusive { let min = self .0 .values() .map(|v| match *v { EnumValue::Single(x) => x, EnumValue::Range(start, _end) => start, }) .min() .unwrap_or(0); let max = self .0 .values() .map(|v| match *v { EnumValue::Single(x) => x, EnumValue::Range(_start, end) => end, }) .max() .unwrap_or(0); min..=max } /// Lookup the name of the given enum value pub fn lookup_name(&self, val: i64) -> Option { self.0 .iter() .find(|(_, vals)| match **vals { EnumValue::Single(x) => x == val, EnumValue::Range(start, end) => (start..=end).contains(&val), }) .map(|(name, vals)| match vals { EnumValue::Single(_) => name.to_string(), EnumValue::Range(_, _) => todo!("modify name to end with correct number"), }) } } /// Error encountered when getting dictionary from microcontroller #[derive(thiserror::Error, Debug)] pub enum DictionaryError { /// Found an empty command #[error("empty command found")] EmptyCommand, /// Received a command in an invalid format #[error("invalid command format: {0}")] InvalidCommandFormat(String, MessageSkipperError), /// Received an output string with an invalid format #[error("invalid output format: {0}")] InvalidOutputFormat(String, MessageSkipperError), /// Received a command with an invalid tag #[error("command tag {0} output valid range of -32..95")] InvalidCommandTag(u16), } /// The raw JSON data dictionary response from the microcontroller #[derive(Debug, serde::Deserialize)] pub(crate) struct RawDictionary { #[serde(default)] config: BTreeMap, #[serde(default)] enumerations: BTreeMap>, #[serde(default)] commands: BTreeMap, #[serde(default)] responses: BTreeMap, #[serde(default)] output: BTreeMap, #[serde(default)] build_versions: Option, #[serde(default)] version: Option, #[serde(flatten)] extra: BTreeMap, } /// The value of a configuration key #[derive(Debug, Clone, PartialEq, serde::Deserialize)] #[serde(untagged)] pub enum ConfigVar { String(String), Number(f64), } /// Specifies a value an enumeration can take. /// Either one string corresponds to a single integer value, or it ends in a number and corresponds to a range of integer values. #[derive(Debug, serde::Deserialize)] #[serde(untagged)] enum EnumValue { Single(i64), Range(i64, i64), }