Skip to main content

bambu_rs/core/
firmware.rs

1//! Printer firmware versions.
2//!
3//! Bambu firmware versions look like `01.05.00.00` (dot-separated, usually
4//! zero-padded two-digit components). The capability registry keys on
5//! `(model, FirmwareVersion)`, so we need a type that parses these strings and
6//! orders them correctly — `01.04.99.00 < 01.05.00.00`, and trailing-zero
7//! components are insignificant (`01.05` == `01.05.00.00`).
8
9use std::cmp::Ordering;
10use std::fmt;
11
12/// A parsed, comparable Bambu firmware version.
13///
14/// Equality and ordering are defined on the *normalised* numeric components
15/// (trailing zero components dropped), so `01.05` and `01.05.00.00` compare
16/// equal. The original string is preserved for [`fmt::Display`].
17#[derive(Debug, Clone)]
18pub struct FirmwareVersion {
19    /// Original input (e.g. `"01.05.00.00"`), preserved for display.
20    raw: String,
21    /// Numeric components with insignificant trailing zeros removed.
22    components: Vec<u32>,
23}
24
25/// Error returned when a firmware version string cannot be parsed.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ParseFirmwareError {
28    input: String,
29}
30
31impl fmt::Display for ParseFirmwareError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "invalid firmware version: {:?}", self.input)
34    }
35}
36
37impl std::error::Error for ParseFirmwareError {}
38
39impl FirmwareVersion {
40    /// Parse a firmware version string such as `"01.05.00.00"` (an optional
41    /// leading `v`/`V` is accepted).
42    pub fn parse(s: &str) -> Result<Self, ParseFirmwareError> {
43        let raw = s.trim();
44        let err = || ParseFirmwareError {
45            input: s.to_string(),
46        };
47        let body = raw.strip_prefix(['v', 'V']).unwrap_or(raw);
48        if body.is_empty() {
49            return Err(err());
50        }
51        let mut components = body
52            .split('.')
53            .map(|part| part.parse::<u32>().map_err(|_| err()))
54            .collect::<Result<Vec<u32>, _>>()?;
55        // Drop insignificant trailing zeros so `01.05` == `01.05.00.00`.
56        while components.last() == Some(&0) {
57            components.pop();
58        }
59        Ok(Self {
60            raw: raw.to_string(),
61            components,
62        })
63    }
64
65    /// The normalised numeric components (trailing zeros removed).
66    pub fn components(&self) -> &[u32] {
67        &self.components
68    }
69}
70
71impl PartialEq for FirmwareVersion {
72    fn eq(&self, other: &Self) -> bool {
73        self.components == other.components
74    }
75}
76
77impl Eq for FirmwareVersion {}
78
79impl PartialOrd for FirmwareVersion {
80    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
81        Some(self.cmp(other))
82    }
83}
84
85impl Ord for FirmwareVersion {
86    fn cmp(&self, other: &Self) -> Ordering {
87        // Normalised components have no trailing zeros, so lexicographic
88        // comparison of the `Vec<u32>` matches numeric version ordering.
89        self.components.cmp(&other.components)
90    }
91}
92
93impl std::hash::Hash for FirmwareVersion {
94    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
95        self.components.hash(state);
96    }
97}
98
99impl serde::Serialize for FirmwareVersion {
100    /// Serialise as the original version string (e.g. `"01.07.02.00"`), so JSON
101    /// output round-trips what the device reported.
102    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
103        serializer.serialize_str(&self.raw)
104    }
105}
106
107impl fmt::Display for FirmwareVersion {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.write_str(&self.raw)
110    }
111}
112
113impl std::str::FromStr for FirmwareVersion {
114    type Err = ParseFirmwareError;
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        Self::parse(s)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn fw(s: &str) -> FirmwareVersion {
125        FirmwareVersion::parse(s).expect("should parse")
126    }
127
128    #[test]
129    fn parses_four_component_version() {
130        let v = fw("01.05.00.00");
131        assert_eq!(v.components(), &[1, 5]); // trailing zeros are insignificant
132    }
133
134    #[test]
135    fn accepts_optional_v_prefix() {
136        assert_eq!(fw("v01.05.00"), fw("01.05.00"));
137        assert_eq!(fw("V01.05.00"), fw("01.05.00"));
138    }
139
140    #[test]
141    fn trailing_zero_components_are_equal() {
142        assert_eq!(fw("01.05"), fw("01.05.00.00"));
143        assert_eq!(fw("01.05.00"), fw("01.05.00.00"));
144    }
145
146    #[test]
147    fn orders_by_numeric_component() {
148        assert!(fw("01.04.99.00") < fw("01.05.00.00"));
149        assert!(fw("01.05.00.00") < fw("01.05.01.00"));
150        assert!(fw("01.05.00.00") < fw("01.05.00.02"));
151        assert!(fw("01.05.01") > fw("01.05.00.02"));
152    }
153
154    #[test]
155    fn developer_mode_threshold_comparison() {
156        // A1: Developer Mode requires firmware >= 01.05.00.
157        let threshold = fw("01.05.00");
158        assert!(fw("01.04.00.00") < threshold);
159        assert!(fw("01.05.00.00") >= threshold);
160        assert!(fw("01.06.02.00") >= threshold);
161    }
162
163    #[test]
164    fn display_round_trips_original_string() {
165        assert_eq!(fw("01.05.00.00").to_string(), "01.05.00.00");
166    }
167
168    #[test]
169    fn rejects_non_numeric() {
170        assert!(FirmwareVersion::parse("not-a-version").is_err());
171        assert!(FirmwareVersion::parse("01.x.00").is_err());
172        assert!(FirmwareVersion::parse("").is_err());
173    }
174}