1use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum Model {
13 A1Mini,
14 A1,
15 P1P,
16 P1S,
17 X1,
18 X1Carbon,
19 X1E,
20 H2D,
21 Unknown(String),
23}
24
25impl Model {
26 pub fn from_config_str(s: &str) -> Self {
29 let normalized: String = s
30 .chars()
31 .filter(|c| !matches!(c, ' ' | '-' | '_'))
32 .flat_map(char::to_lowercase)
33 .collect();
34 match normalized.as_str() {
35 "a1mini" => Model::A1Mini,
36 "a1" => Model::A1,
37 "p1p" => Model::P1P,
38 "p1s" => Model::P1S,
39 "x1" => Model::X1,
40 "x1c" | "x1carbon" => Model::X1Carbon,
41 "x1e" => Model::X1E,
42 "h2d" => Model::H2D,
43 _ => Model::Unknown(s.trim().to_string()),
44 }
45 }
46
47 pub fn as_str(&self) -> &str {
49 match self {
50 Model::A1Mini => "a1mini",
51 Model::A1 => "a1",
52 Model::P1P => "p1p",
53 Model::P1S => "p1s",
54 Model::X1 => "x1",
55 Model::X1Carbon => "x1c",
56 Model::X1E => "x1e",
57 Model::H2D => "h2d",
58 Model::Unknown(s) => s,
59 }
60 }
61
62 pub fn is_known(&self) -> bool {
64 !matches!(self, Model::Unknown(_))
65 }
66
67 pub fn from_device_code(code: &str) -> Self {
80 match code.trim() {
81 "N1" => Model::A1Mini,
82 "N2S" => Model::A1,
83 "C11" => Model::P1P,
84 "C12" => Model::P1S,
85 "C13" => Model::X1E,
86 "BL-P001" | "3DPrinter-X1-Carbon" => Model::X1Carbon,
87 "BL-P002" | "3DPrinter-X1" => Model::X1,
88 "O1D" => Model::H2D,
89 other => Model::Unknown(other.to_string()),
90 }
91 }
92
93 pub fn device_code(&self) -> Option<&'static str> {
96 Some(match self {
97 Model::A1Mini => "N1",
98 Model::A1 => "N2S",
99 Model::P1P => "C11",
100 Model::P1S => "C12",
101 Model::X1 => "BL-P002",
102 Model::X1Carbon => "BL-P001",
103 Model::X1E => "C13",
104 Model::H2D => "O1D",
105 Model::Unknown(_) => return None,
106 })
107 }
108}
109
110impl fmt::Display for Model {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(self.as_str())
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 const KNOWN: [Model; 8] = [
121 Model::A1Mini,
122 Model::A1,
123 Model::P1P,
124 Model::P1S,
125 Model::X1,
126 Model::X1Carbon,
127 Model::X1E,
128 Model::H2D,
129 ];
130
131 #[test]
132 fn parses_canonical_config_names() {
133 assert_eq!(Model::from_config_str("a1mini"), Model::A1Mini);
134 assert_eq!(Model::from_config_str("a1"), Model::A1);
135 assert_eq!(Model::from_config_str("p1s"), Model::P1S);
136 assert_eq!(Model::from_config_str("x1"), Model::X1);
137 assert_eq!(Model::from_config_str("x1c"), Model::X1Carbon);
138 assert_eq!(Model::from_config_str("h2d"), Model::H2D);
139 }
140
141 #[test]
142 fn config_parsing_is_lenient_about_case_and_separators() {
143 assert_eq!(Model::from_config_str("A1 mini"), Model::A1Mini);
144 assert_eq!(Model::from_config_str("A1-Mini"), Model::A1Mini);
145 assert_eq!(Model::from_config_str("a1_mini"), Model::A1Mini);
146 assert_eq!(Model::from_config_str("X1Carbon"), Model::X1Carbon);
147 }
148
149 #[test]
150 fn unknown_model_is_kept_verbatim() {
151 let m = Model::from_config_str("z9 ultra");
152 assert_eq!(m, Model::Unknown("z9 ultra".to_string()));
153 assert!(!m.is_known());
154 }
155
156 #[test]
157 fn resolves_vendor_canonical_device_codes() {
158 assert_eq!(Model::from_device_code("N1"), Model::A1Mini); assert_eq!(Model::from_device_code("N2S"), Model::A1); assert_eq!(Model::from_device_code("C11"), Model::P1P);
161 assert_eq!(Model::from_device_code("C12"), Model::P1S);
162 assert_eq!(Model::from_device_code("C13"), Model::X1E);
163 assert_eq!(Model::from_device_code("BL-P001"), Model::X1Carbon);
164 assert_eq!(Model::from_device_code("BL-P002"), Model::X1);
165 assert_eq!(Model::from_device_code("O1D"), Model::H2D);
166 }
167
168 #[test]
169 fn x1_and_x1_carbon_are_distinct_codes() {
170 assert_ne!(Model::X1, Model::X1Carbon);
171 assert_eq!(Model::X1.device_code(), Some("BL-P002"));
172 assert_eq!(Model::X1Carbon.device_code(), Some("BL-P001"));
173 }
174
175 #[test]
176 fn legacy_ssdp_strings_normalise_to_modern_models() {
177 assert_eq!(
178 Model::from_device_code("3DPrinter-X1-Carbon"),
179 Model::X1Carbon
180 );
181 assert_eq!(Model::from_device_code("3DPrinter-X1"), Model::X1);
182 }
183
184 #[test]
185 fn unrecognised_device_code_is_unknown() {
186 assert_eq!(
187 Model::from_device_code("ZZ9"),
188 Model::Unknown("ZZ9".to_string())
189 );
190 assert_eq!(Model::Unknown("ZZ9".into()).device_code(), None);
191 }
192
193 #[test]
194 fn device_codes_round_trip_for_all_known_models() {
195 for m in KNOWN {
196 let code = m.device_code().expect("known model has a device code");
197 assert_eq!(Model::from_device_code(code), m, "round-trip for {m}");
198 }
199 }
200
201 #[test]
202 fn config_names_round_trip_for_all_known_models() {
203 for m in KNOWN {
204 assert!(m.is_known());
205 assert_eq!(Model::from_config_str(m.as_str()), m);
206 }
207 }
208}