1use serde_json::Value;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct HmsEntry {
25 pub attr: u32,
26 pub code: u32,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Module {
34 MotionController,
36 Mainboard,
38 Ams,
40 Toolhead,
42 Xcam,
44 Unknown(u8),
45}
46
47impl HmsEntry {
48 pub fn new(attr: u32, code: u32) -> Self {
49 Self { attr, code }
50 }
51
52 fn groups(&self) -> [u16; 4] {
53 [
54 (self.attr >> 16) as u16,
55 (self.attr & 0xFFFF) as u16,
56 (self.code >> 16) as u16,
57 (self.code & 0xFFFF) as u16,
58 ]
59 }
60
61 pub fn code_string(&self) -> String {
63 let [a, b, c, d] = self.groups();
64 format!("HMS_{a:04X}_{b:04X}_{c:04X}_{d:04X}")
65 }
66
67 pub fn code_hyphen(&self) -> String {
70 let [a, b, c, d] = self.groups();
71 format!("{a:04X}-{b:04X}-{c:04X}-{d:04X}")
72 }
73
74 pub fn severity_raw(&self) -> u16 {
79 (self.code >> 16) as u16
80 }
81
82 pub fn module(&self) -> Module {
84 match ((self.attr >> 24) & 0xFF) as u8 {
85 0x03 => Module::MotionController,
86 0x05 => Module::Mainboard,
87 0x07 => Module::Ams,
88 0x08 => Module::Toolhead,
89 0x0C => Module::Xcam,
90 other => Module::Unknown(other),
91 }
92 }
93
94 pub fn is_lidar(&self) -> bool {
96 matches!(self.module(), Module::Xcam)
97 }
98
99 pub fn wiki_url(&self) -> String {
101 let [a, b, c, d] = self.groups();
102 format!(
103 "https://wiki.bambulab.com/en/x1/troubleshooting/hmscode/{a:04X}_{b:04X}_{c:04X}_{d:04X}"
104 )
105 }
106}
107
108pub fn decode_report_hms(state: &Value) -> Vec<HmsEntry> {
111 let Some(arr) = state.pointer("/print/hms").and_then(Value::as_array) else {
112 return Vec::new();
113 };
114 arr.iter()
115 .filter_map(|e| {
116 let attr = e.get("attr").and_then(Value::as_u64)?;
117 let code = e.get("code").and_then(Value::as_u64)?;
118 if attr == 0 || code == 0 {
119 return None;
120 }
121 Some(HmsEntry::new(attr as u32, code as u32))
122 })
123 .collect()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use serde_json::json;
130
131 #[test]
132 fn worked_example_decodes_like_the_official_wiki() {
133 let e = HmsEntry::new(0x0300_0100, 0x0001_0007);
136 assert_eq!(e.code_string(), "HMS_0300_0100_0001_0007");
137 assert_eq!(e.code_hyphen(), "0300-0100-0001-0007");
138 assert_eq!(e.severity_raw(), 1);
139 assert_eq!(e.module(), Module::MotionController);
140 assert!(!e.is_lidar());
141 assert!(e.wiki_url().ends_with("/hmscode/0300_0100_0001_0007"));
142 }
143
144 #[test]
145 fn module_is_decoded_from_the_attr_high_byte() {
146 assert_eq!(HmsEntry::new(0x0500_0000, 1).module(), Module::Mainboard);
147 assert_eq!(HmsEntry::new(0x0700_0000, 1).module(), Module::Ams);
148 assert_eq!(HmsEntry::new(0x0800_0000, 1).module(), Module::Toolhead);
149 assert_eq!(
150 HmsEntry::new(0x0A00_0000, 1).module(),
151 Module::Unknown(0x0A)
152 );
153 }
154
155 #[test]
156 fn xcam_codes_are_flagged_as_lidar() {
157 let e = HmsEntry::new(0x0C00_0100, 0x0003_0001);
158 assert_eq!(e.module(), Module::Xcam);
159 assert!(e.is_lidar());
160 }
161
162 #[test]
163 fn observed_a1mini_fixture_has_no_active_alerts() {
164 let raw = include_str!("../../tests/fixtures/pushall-n1-idle.json");
165 let fixture: Value = serde_json::from_str(raw).unwrap();
166 assert_eq!(decode_report_hms(&fixture["message"]), Vec::new());
168 }
169
170 #[test]
171 fn decode_skips_zero_padding_entries() {
172 let state = json!({ "print": { "hms": [
173 { "attr": 0, "code": 0 },
174 { "attr": 50331904, "code": 65543 }, { "attr": 123, "code": 0 },
176 ]}});
177 let decoded = decode_report_hms(&state);
178 assert_eq!(decoded, vec![HmsEntry::new(0x0300_0100, 0x0001_0007)]);
179 }
180
181 #[test]
182 fn missing_hms_field_decodes_to_empty() {
183 assert_eq!(decode_report_hms(&json!({ "print": {} })), Vec::new());
184 assert_eq!(decode_report_hms(&json!({})), Vec::new());
185 }
186}