Skip to main content

bambu_rs/core/
start.rs

1//! Building the print-start command from resolved parameters — the one place the
2//! CLI and the serve agree on how a `.3mf`/`.gcode` becomes a `project_file` /
3//! `gcode_file`. Pure (no I/O): the caller resolves the on-printer path, parses
4//! the AMS map, and (optionally) inspects the plate; this just renders the
5//! command, folding in the plate-gcode md5 when an inspection is available so the
6//! printer can verify the file it is about to print.
7
8use std::path::Path;
9
10use crate::core::command::{Command, ProjectFile};
11use crate::core::project::PlateInspection;
12
13/// Resolved parameters for a print start: the path is already an on-printer path
14/// and the AMS map is already parsed. Render the wire command with
15/// [`build_command`].
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct PrintStartParams {
18    /// On-printer path, e.g. `/cache/x.gcode.3mf` — becomes `ftp://<path>`.
19    pub file: String,
20    pub plate: u32,
21    pub use_ams: bool,
22    pub ams_map: Vec<i32>,
23    pub bed_type: String,
24    pub timelapse: bool,
25}
26
27impl PrintStartParams {
28    /// Whether the target is a sliced `.3mf` project (vs a raw `.gcode`).
29    pub fn is_3mf(&self) -> bool {
30        self.file.to_ascii_lowercase().ends_with(".3mf")
31    }
32}
33
34/// Render the print-start command: `project_file` for a `.3mf`, `gcode_file` for
35/// raw `.gcode`. When `inspection` is present (for a `.3mf`), its plate-gcode md5
36/// is stamped into the `project_file` so the printer checks the file matches its
37/// bytes before printing; without one the md5 is left empty (the check is
38/// skipped), exactly as the builders did before this was shared.
39pub fn build_command(params: &PrintStartParams, inspection: Option<&PlateInspection>) -> Command {
40    if !params.is_3mf() {
41        return Command::GcodeFile(params.file.clone());
42    }
43    let name = Path::new(&params.file)
44        .file_name()
45        .and_then(|s| s.to_str())
46        .unwrap_or(&params.file)
47        .to_string();
48    let mut pf = ProjectFile::new(format!("ftp://{}", params.file), params.plate, name);
49    pf.bed_type = params.bed_type.clone();
50    pf.timelapse = params.timelapse;
51    if params.use_ams {
52        pf.use_ams = true;
53        pf.ams_mapping = params.ams_map.clone();
54    }
55    if let Some(insp) = inspection {
56        pf.md5 = insp.gcode_md5.clone();
57    }
58    Command::ProjectFile(pf)
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    fn params(file: &str) -> PrintStartParams {
66        PrintStartParams {
67            file: file.to_string(),
68            plate: 1,
69            use_ams: false,
70            ams_map: vec![],
71            bed_type: "auto".to_string(),
72            timelapse: false,
73        }
74    }
75
76    fn inspection(md5: &str) -> PlateInspection {
77        PlateInspection {
78            plate: 1,
79            gcode_md5: md5.to_string(),
80            sidecar_md5: None,
81            sidecar_matches: true,
82            bed_type: None,
83            filament_colors: vec![],
84            has_timelapse_blocks: false,
85        }
86    }
87
88    #[test]
89    fn three_mf_builds_a_project_file_with_ftp_url() {
90        let mut p = params("/cache/coin.gcode.3mf");
91        p.plate = 2;
92        p.use_ams = true;
93        p.ams_map = vec![0, 3];
94        p.timelapse = true;
95        match build_command(&p, None) {
96            Command::ProjectFile(pf) => {
97                assert_eq!(pf.url, "ftp:///cache/coin.gcode.3mf");
98                assert_eq!(pf.plate, 2);
99                assert_eq!(pf.subtask_name, "coin.gcode.3mf");
100                assert!(pf.use_ams);
101                assert_eq!(pf.ams_mapping, vec![0, 3]);
102                assert!(pf.timelapse);
103                assert!(pf.md5.is_empty(), "no inspection ⇒ no md5 check");
104            }
105            other => panic!("expected ProjectFile, got {other:?}"),
106        }
107    }
108
109    #[test]
110    fn an_inspection_stamps_the_plate_gcode_md5() {
111        match build_command(&params("/x.gcode.3mf"), Some(&inspection("abc123"))) {
112            Command::ProjectFile(pf) => assert_eq!(pf.md5, "abc123"),
113            other => panic!("expected ProjectFile, got {other:?}"),
114        }
115    }
116
117    #[test]
118    fn raw_gcode_builds_a_gcode_file_and_ignores_inspection() {
119        // A raw .gcode has no plate metadata, so an inspection can't apply.
120        assert!(matches!(
121            build_command(&params("/test.gcode"), Some(&inspection("abc"))),
122            Command::GcodeFile(f) if f == "/test.gcode"
123        ));
124    }
125}