Skip to main content

bambu_rs/core/
version.rs

1//! Parsed `info.get_version` response — the printer's module/firmware inventory.
2//!
3//! `get_version` answers with a `module[]` array, one entry per hardware/software
4//! component (`ota`, `esp32`, `mc`, `th`, `ams_f1/0`, …). The **`ota`** module's
5//! `sw_ver` is the printer's user-facing firmware version — the value the
6//! capability registry keys on. Shapes are from a **real A1 mini capture**
7//! (`tests/fixtures/get_version-a1mini.json`).
8
9use crate::core::firmware::FirmwareVersion;
10use serde::Serialize;
11use serde_json::Value;
12
13/// One module reported by `get_version` (a hardware/software component).
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct Module {
16    /// Module name, e.g. `ota`, `esp32`, `mc`, `ams_f1/0`.
17    pub name: String,
18    /// Hardware revision, e.g. `OTA`, `AP05`, `AMS_F102`.
19    pub hw_ver: Option<String>,
20    /// Software/firmware version of this module, e.g. `01.07.02.00`.
21    pub sw_ver: Option<String>,
22    /// Marketing name when the module carries one (`Bambu Lab A1 mini`,
23    /// `AMS Lite`); empty strings are dropped to `None`.
24    pub product_name: Option<String>,
25}
26
27impl Module {
28    /// Parse one `module[]` entry; `None` when it has no `name`.
29    fn from_value(v: &Value) -> Option<Module> {
30        let name = v.get("name").and_then(Value::as_str)?.to_string();
31        let nonempty = |key: &str| {
32            v.get(key)
33                .and_then(Value::as_str)
34                .filter(|s| !s.is_empty())
35                .map(str::to_owned)
36        };
37        Some(Module {
38            name,
39            hw_ver: nonempty("hw_ver"),
40            sw_ver: nonempty("sw_ver"),
41            product_name: nonempty("product_name"),
42        })
43    }
44}
45
46/// The printer's version inventory, parsed from an `info.get_version` response.
47#[derive(Debug, Clone, PartialEq, Serialize)]
48pub struct DeviceVersion {
49    /// The OTA module's `sw_ver` as a parsed firmware version — the printer's
50    /// user-facing firmware, which the capability registry keys on. `None` when
51    /// there's no `ota` module or its `sw_ver` doesn't parse.
52    pub firmware: Option<FirmwareVersion>,
53    /// Every reported module, in report order.
54    pub modules: Vec<Module>,
55}
56
57impl DeviceVersion {
58    /// Parse from the object under `info` in a `get_version` response (the value
59    /// holding `command: "get_version"` and the `module` array).
60    pub fn from_info(info: &Value) -> Self {
61        let modules: Vec<Module> = info
62            .get("module")
63            .and_then(Value::as_array)
64            .map(|arr| arr.iter().filter_map(Module::from_value).collect())
65            .unwrap_or_default();
66        let firmware = modules
67            .iter()
68            .find(|m| m.name == "ota")
69            .and_then(|m| m.sw_ver.as_deref())
70            .and_then(|s| FirmwareVersion::parse(s).ok());
71        DeviceVersion { firmware, modules }
72    }
73
74    /// Find a module by exact name.
75    pub fn module(&self, name: &str) -> Option<&Module> {
76        self.modules.iter().find(|m| m.name == name)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use serde_json::json;
84
85    fn fixture() -> Value {
86        let raw = include_str!("../../tests/fixtures/get_version-a1mini.json");
87        serde_json::from_str(raw).expect("valid fixture json")
88    }
89
90    #[test]
91    fn parses_firmware_from_the_ota_module() {
92        let info = &fixture()["message"]["info"];
93        let v = DeviceVersion::from_info(info);
94        // The OTA module's sw_ver is the printer's firmware.
95        assert_eq!(
96            v.firmware.as_ref().map(ToString::to_string).as_deref(),
97            Some("01.07.02.00")
98        );
99    }
100
101    #[test]
102    fn parses_all_modules_in_order() {
103        let v = DeviceVersion::from_info(&fixture()["message"]["info"]);
104        let names: Vec<&str> = v.modules.iter().map(|m| m.name.as_str()).collect();
105        assert_eq!(names, ["ota", "esp32", "mc", "th", "ams_f1/0"]);
106    }
107
108    #[test]
109    fn module_carries_hw_and_product_name() {
110        let v = DeviceVersion::from_info(&fixture()["message"]["info"]);
111        let ota = v.module("ota").unwrap();
112        assert_eq!(ota.hw_ver.as_deref(), Some("OTA"));
113        assert_eq!(ota.product_name.as_deref(), Some("Bambu Lab A1 mini"));
114        // The AMS Lite module is identified by its product name.
115        let ams = v.module("ams_f1/0").unwrap();
116        assert_eq!(ams.hw_ver.as_deref(), Some("AMS_F102"));
117        assert_eq!(ams.product_name.as_deref(), Some("AMS Lite"));
118        // A module with an empty product_name drops it to None.
119        assert_eq!(v.module("esp32").unwrap().product_name, None);
120    }
121
122    #[test]
123    fn missing_ota_module_yields_no_firmware() {
124        let info = json!({ "command": "get_version", "module": [
125            { "name": "esp32", "sw_ver": "01.16.39.58" }
126        ]});
127        let v = DeviceVersion::from_info(&info);
128        assert_eq!(v.firmware, None);
129        assert_eq!(v.modules.len(), 1);
130    }
131
132    #[test]
133    fn empty_or_absent_module_array_is_empty_inventory() {
134        let v = DeviceVersion::from_info(&json!({ "command": "get_version" }));
135        assert_eq!(v.firmware, None);
136        assert!(v.modules.is_empty());
137    }
138}