Make the filament source explicit, and print from the AMS
`start_uploaded_file` hardcoded `use_ams: false` / `ams_mapping: ""`, which silently overrode whatever the slicer had chosen. On a P1P with an AMS and a project sliced for it, that is not a missing feature - it points the print at the external spool. Confirmed on 2026-10-11: the print started, heated to 250C, reached 1%, then the printer paused itself with `print_error: 50364420` and `tray_now: 255`. Nothing in the command's reply said so; `result` was `success`, because the command had been accepted. So `FilamentSource` is now a required argument with no default, because there is no safe default - picking wrong draws the wrong filament or none. The `ExternalSpool` variant sends the empty *string* the schema's trailing `oneOf` demands, and `ams` takes slots per project filament index and pads to every tray the printer has, per the padding rule in `schemas/bambulab/v1/common/ams/ams_mapping.schema.json`. It refuses an empty mapping (padding `[]` would mean `use_ams: true` with nothing mapped), one longer than the printer has trays, and any negative that is not -1. Confirmed live with the real mapping: `ams_mapping: [3,-1,-1,-1,-1]`, `use_ams: true` took the printer to RUNNING, `tray_now` became 3 - the slot the project was sliced for - and it ran past 1% to 13% with `print_error: 0`, where the external-spool attempt had paused. Finding the mapping needed reading the project back off the card, so FTPS gains `download`/`download_file` and the probe a `fetch` step. The archive's `Metadata/slice_info.config` lists each plate's filaments with `tray_info_idx` and colour - here one PETG GFG99 at `#2850E0`, matching AMS slot 3's `2850E0FF` - and `Metadata/plate_1.gcode` is present, which also confirms the hardcoded `param`. `upload_project_file` is left on the external spool and is the one remaining place that can pick wrong, because it is vendor-generic and an AMS mapping is a Bambu concept. It is unreachable - there is no start-print command on the uplink - and the fix is for it to derive the mapping from the `.3mf` it already holds, which needs a zip reader and so a dependency decision.
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use continuum_proxy::fleet;
|
use continuum_proxy::fleet;
|
||||||
use continuum_proxy::printer::{GenericPrinter, PrintControl, PrinterHandle};
|
use continuum_proxy::printer::{FilamentSource, GenericPrinter, PrintControl, PrinterHandle};
|
||||||
|
|
||||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
const STATUS_TIMEOUT: Duration = Duration::from_secs(15);
|
const STATUS_TIMEOUT: Duration = Duration::from_secs(15);
|
||||||
@@ -44,6 +44,10 @@ const WATCH: Duration = Duration::from_secs(20);
|
|||||||
/// A file name that cannot exist in `cache/`, for `probe-missing`.
|
/// A file name that cannot exist in `cache/`, for `probe-missing`.
|
||||||
const MISSING_FILE: &str = "continuum-probe-no-such-file.gcode";
|
const MISSING_FILE: &str = "continuum-probe-no-such-file.gcode";
|
||||||
|
|
||||||
|
/// Every tray a P1P with one AMS has: 4 AMS slots plus the external spool.
|
||||||
|
/// `ams_mapping` has to be padded to this length - see the schema.
|
||||||
|
const P1P_TRAY_COUNT: usize = 5;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Without this there is no subscriber, so the `trace!` of every raw
|
// Without this there is no subscriber, so the `trace!` of every raw
|
||||||
@@ -108,7 +112,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
// whether the command shape is understood.
|
// whether the command shape is understood.
|
||||||
println!("\n sending print.gcode_file for a file that does not exist:");
|
println!("\n sending print.gcode_file for a file that does not exist:");
|
||||||
println!(" param -> /cache/{MISSING_FILE}");
|
println!(" param -> /cache/{MISSING_FILE}");
|
||||||
match bambu.start_uploaded_file(MISSING_FILE).await {
|
match bambu
|
||||||
|
.start_uploaded_file(MISSING_FILE, &FilamentSource::ExternalSpool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(()) => println!(" the printer ACCEPTED it (result: success)"),
|
Ok(()) => println!(" the printer ACCEPTED it (result: success)"),
|
||||||
Err(err) => anyhow::bail!("the printer answered: {err}"),
|
Err(err) => anyhow::bail!("the printer answered: {err}"),
|
||||||
}
|
}
|
||||||
@@ -139,8 +146,32 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
This commits the machine to physical work. Be at the printer."
|
This commits the machine to physical work. Be at the printer."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
println!("\n starting print of '{file}' ...");
|
// `--ams=3` or `--ams=3,-1,0` picks AMS slots per project
|
||||||
match bambu.start_uploaded_file(&file).await {
|
// filament index; without it, the external spool. There is no
|
||||||
|
// default that is safe for both, so the flag has to be explicit.
|
||||||
|
let filament = match args.iter().find_map(|a| a.strip_prefix("--ams=")) {
|
||||||
|
Some(list) => {
|
||||||
|
let slots = list
|
||||||
|
.split(',')
|
||||||
|
.map(|slot| slot.trim().parse::<i32>())
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|err| anyhow::anyhow!("--ams needs a comma-separated list of slot numbers: {err}"))?;
|
||||||
|
// 4 AMS slots + 1 external spool on a P1P with one AMS.
|
||||||
|
// See the padding rule in the ams_mapping schema.
|
||||||
|
FilamentSource::ams(slots, P1P_TRAY_COUNT)
|
||||||
|
.map_err(|err| anyhow::anyhow!("{err}"))?
|
||||||
|
}
|
||||||
|
None => FilamentSource::ExternalSpool,
|
||||||
|
};
|
||||||
|
match &filament {
|
||||||
|
FilamentSource::ExternalSpool => {
|
||||||
|
println!("\n starting print of '{file}' from the EXTERNAL SPOOL ...")
|
||||||
|
}
|
||||||
|
FilamentSource::Ams(slots) => {
|
||||||
|
println!("\n starting print of '{file}' with ams_mapping {slots:?} ...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match bambu.start_uploaded_file(&file, &filament).await {
|
||||||
Ok(()) => println!(" accepted"),
|
Ok(()) => println!(" accepted"),
|
||||||
Err(err) => anyhow::bail!("the printer answered: {err}"),
|
Err(err) => anyhow::bail!("the printer answered: {err}"),
|
||||||
}
|
}
|
||||||
@@ -175,6 +206,20 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `fetch <path> <dest>` copies one file off the card. Read-only.
|
||||||
|
"fetch" => {
|
||||||
|
let (Some(path), Some(dest)) = (args.get(3), args.get(4)) else {
|
||||||
|
anyhow::bail!("`fetch` needs a path on the printer and a local destination");
|
||||||
|
};
|
||||||
|
match bambu.download_file(path).await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
std::fs::write(dest, &bytes)?;
|
||||||
|
println!(" {} bytes -> {dest}", bytes.len());
|
||||||
|
}
|
||||||
|
Err(err) => anyhow::bail!("fetch failed: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
other => anyhow::bail!("unknown step '{other}'"),
|
other => anyhow::bail!("unknown step '{other}'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+190
-6
@@ -226,6 +226,82 @@ pub struct BambuPrinter {
|
|||||||
connection: Option<(super::bambu_mqtt::BambuMqttSession, BambuProtocolVersion)>,
|
connection: Option<(super::bambu_mqtt::BambuMqttSession, BambuProtocolVersion)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a project print draws its filament from.
|
||||||
|
///
|
||||||
|
/// This exists because the alternative - hardcoding `use_ams: false` -
|
||||||
|
/// silently overrode the slicer's own choice. Confirmed on a P1P
|
||||||
|
/// (2026-10-11): a project sliced for AMS slot 3 started, heated to
|
||||||
|
/// 250C, reached 1% and then the printer paused itself with
|
||||||
|
/// `print_error: 50364420` and `tray_now: 255`, the external spool. The
|
||||||
|
/// print was never going to work, and nothing in the command's reply
|
||||||
|
/// said so - `result` was `success`, because the command was accepted.
|
||||||
|
///
|
||||||
|
/// So this is a required argument rather than an option with a default.
|
||||||
|
/// There is no safe default: picking the wrong source draws the wrong
|
||||||
|
/// filament or none at all.
|
||||||
|
pub enum FilamentSource {
|
||||||
|
/// The external spool (`vt_tray`). `use_ams: false`, and
|
||||||
|
/// `ams_mapping` must be the empty *string* - the schema's trailing
|
||||||
|
/// `oneOf` ties those two together.
|
||||||
|
ExternalSpool,
|
||||||
|
/// The AMS, with a mapping from the project's filament indices to
|
||||||
|
/// AMS slots. Build it with `FilamentSource::ams`.
|
||||||
|
Ams(Vec<i32>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilamentSource {
|
||||||
|
/// `slots[i]` is the AMS slot the project's filament index `i` should
|
||||||
|
/// draw from, or `-1` for an index this project does not use.
|
||||||
|
///
|
||||||
|
/// `tray_count` is every tray the printer has, AMS slots *and* external
|
||||||
|
/// spools - 5 for a P1P with one AMS. The mapping is padded to that
|
||||||
|
/// length with `-1`, because
|
||||||
|
/// `schemas/bambulab/v1/common/ams/ams_mapping.schema.json` says to
|
||||||
|
/// always pad it and an under-length array is one of the ways this goes
|
||||||
|
/// wrong quietly.
|
||||||
|
pub fn ams(slots: impl Into<Vec<i32>>, tray_count: usize) -> Result<Self, PrinterError> {
|
||||||
|
let mut slots: Vec<i32> = slots.into();
|
||||||
|
|
||||||
|
if slots.is_empty() {
|
||||||
|
return Err(PrinterError(
|
||||||
|
"an AMS mapping needs at least one filament index; use ExternalSpool instead".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if slots.len() > tray_count {
|
||||||
|
return Err(PrinterError(format!(
|
||||||
|
"an AMS mapping of {} entries cannot fit a printer with {tray_count} trays",
|
||||||
|
slots.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if let Some(bad) = slots.iter().find(|slot| **slot < -1) {
|
||||||
|
return Err(PrinterError(format!(
|
||||||
|
"{bad} is not a valid AMS slot: use -1 for an unused filament index"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
slots.resize(tray_count, -1);
|
||||||
|
Ok(Self::Ams(slots))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes `use_ams` and `ams_mapping` into a `print.project_file` body.
|
||||||
|
///
|
||||||
|
/// The two always move together: a real mapping requires
|
||||||
|
/// `use_ams: true`, and the no-AMS case requires `ams_mapping` to be the
|
||||||
|
/// empty string rather than an empty array.
|
||||||
|
fn apply(&self, body: &mut serde_json::Value) {
|
||||||
|
match self {
|
||||||
|
Self::ExternalSpool => {
|
||||||
|
body["use_ams"] = serde_json::Value::Bool(false);
|
||||||
|
body["ams_mapping"] = serde_json::Value::String(String::new());
|
||||||
|
}
|
||||||
|
Self::Ams(slots) => {
|
||||||
|
body["use_ams"] = serde_json::Value::Bool(true);
|
||||||
|
body["ams_mapping"] = serde_json::json!(slots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BambuPrinter {
|
impl BambuPrinter {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: &str,
|
id: &str,
|
||||||
@@ -541,6 +617,30 @@ impl BambuPrinter {
|
|||||||
self.list_dir(super::bambu_ftps::CACHE_DIR).await
|
self.list_dir(super::bambu_ftps::CACHE_DIR).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads one file back off the printer's SD card.
|
||||||
|
///
|
||||||
|
/// Read-only, and the counterpart to `upload_file`. Needed because a
|
||||||
|
/// `.3mf` sitting on the card is the only record of which filament its
|
||||||
|
/// plates were sliced for, which is what `ams_mapping` has to agree
|
||||||
|
/// with.
|
||||||
|
pub async fn download_file(&self, path: &str) -> Result<Vec<u8>, PrinterError> {
|
||||||
|
let connector = self
|
||||||
|
.tls
|
||||||
|
.build_connector()
|
||||||
|
.map_err(|err| PrinterError(format!("{}: TLS setup failed: {err}", self.base.name)))?;
|
||||||
|
|
||||||
|
let host = self.base.host.clone();
|
||||||
|
let access_code = self.access_code.clone();
|
||||||
|
let path = path.to_string();
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
super::bambu_ftps::download(&host, &access_code, connector, &path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| PrinterError(format!("{}: FTPS download thread failed: {err}", self.base.name)))?
|
||||||
|
.map_err(|err| PrinterError(format!("{}: FTPS download failed: {err}", self.base.name)))
|
||||||
|
}
|
||||||
|
|
||||||
/// `list_cache` for any directory on the printer's SD card.
|
/// `list_cache` for any directory on the printer's SD card.
|
||||||
///
|
///
|
||||||
/// Read-only, and the way to answer "what is the path to this file?"
|
/// Read-only, and the way to answer "what is the path to this file?"
|
||||||
@@ -726,7 +826,11 @@ impl BambuPrinter {
|
|||||||
/// check which the printer accepts before relying on that field, and
|
/// check which the printer accepts before relying on that field, and
|
||||||
/// fix continuum-schemas if it's wrong. The confirmed body above sets
|
/// fix continuum-schemas if it's wrong. The confirmed body above sets
|
||||||
/// neither, so this is still open.
|
/// neither, so this is still open.
|
||||||
pub async fn start_uploaded_file(&self, file_name: &str) -> Result<(), PrinterError> {
|
pub async fn start_uploaded_file(
|
||||||
|
&self,
|
||||||
|
file_name: &str,
|
||||||
|
filament: &FilamentSource,
|
||||||
|
) -> Result<(), PrinterError> {
|
||||||
let (session, version) =
|
let (session, version) =
|
||||||
self.connection.as_ref().ok_or_else(|| PrinterError(format!("{} is not connected", self.base.name)))?;
|
self.connection.as_ref().ok_or_else(|| PrinterError(format!("{} is not connected", self.base.name)))?;
|
||||||
|
|
||||||
@@ -755,10 +859,9 @@ impl BambuPrinter {
|
|||||||
}
|
}
|
||||||
body["subtask_name"] = serde_json::Value::String(String::new());
|
body["subtask_name"] = serde_json::Value::String(String::new());
|
||||||
|
|
||||||
// See point 3 in this method's doc comment - these two are a
|
// These two are a pair, not independent switches - see
|
||||||
// pair, not independent switches.
|
// `FilamentSource::apply`.
|
||||||
body["use_ams"] = serde_json::Value::Bool(false);
|
filament.apply(&mut body);
|
||||||
body["ams_mapping"] = serde_json::Value::String(String::new());
|
|
||||||
} else {
|
} else {
|
||||||
// The root, for the same reason: it is where `upload_file` puts
|
// The root, for the same reason: it is where `upload_file` puts
|
||||||
// things. Unverified - see this method's doc comment, which
|
// things. Unverified - see this method's doc comment, which
|
||||||
@@ -917,7 +1020,21 @@ impl GenericPrinter for BambuPrinter {
|
|||||||
super::bambu_ftps::CACHE_DIR
|
super::bambu_ftps::CACHE_DIR
|
||||||
);
|
);
|
||||||
|
|
||||||
self.start_uploaded_file(&file_name).await
|
// The external spool, because this trait method has no way to know
|
||||||
|
// any better: `upload_project_file` is vendor-generic and an AMS
|
||||||
|
// mapping is a Bambu concept, so it cannot appear in the signature.
|
||||||
|
//
|
||||||
|
// That makes this the one remaining place that can silently pick the
|
||||||
|
// wrong filament source - see `FilamentSource` for what that does to
|
||||||
|
// a real print. It is not currently reachable: there is no
|
||||||
|
// start-print command on the uplink, so nothing calls this. The fix
|
||||||
|
// is for this method to *derive* the mapping from the `.3mf` it
|
||||||
|
// already holds in `data` - `Metadata/slice_info.config` lists each
|
||||||
|
// plate's filaments with their `tray_info_idx` and colour, which is
|
||||||
|
// exactly what has to be matched against the AMS report. That needs
|
||||||
|
// a zip reader, which is a dependency decision, which is why it is
|
||||||
|
// not done here yet.
|
||||||
|
self.start_uploaded_file(&file_name, &FilamentSource::ExternalSpool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the real JSON payload straight from continuum-schemas'
|
/// Builds the real JSON payload straight from continuum-schemas'
|
||||||
@@ -943,3 +1060,70 @@ impl GenericPrinter for BambuPrinter {
|
|||||||
session.publish("print", &body).await
|
session.publish("print", &body).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// The padding rule from
|
||||||
|
/// `schemas/bambulab/v1/common/ams/ams_mapping.schema.json`: always pad
|
||||||
|
/// to every tray the printer has. An under-length array is one of the
|
||||||
|
/// ways a mapping goes wrong quietly.
|
||||||
|
#[test]
|
||||||
|
fn an_ams_mapping_is_padded_to_the_tray_count() {
|
||||||
|
let FilamentSource::Ams(slots) = FilamentSource::ams(vec![3], 5).expect("valid") else {
|
||||||
|
panic!("expected an AMS source");
|
||||||
|
};
|
||||||
|
assert_eq!(slots, vec![3, -1, -1, -1, -1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_already_full_length_mapping_is_left_alone() {
|
||||||
|
let FilamentSource::Ams(slots) =
|
||||||
|
FilamentSource::ams(vec![3, -1, 0, -1, -1], 5).expect("valid")
|
||||||
|
else {
|
||||||
|
panic!("expected an AMS source");
|
||||||
|
};
|
||||||
|
assert_eq!(slots, vec![3, -1, 0, -1, -1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_empty_mapping_is_refused_rather_than_padded_into_nonsense() {
|
||||||
|
// Padding `[]` would produce `[-1,-1,-1,-1,-1]`: `use_ams: true` with
|
||||||
|
// every filament unmapped, which is not a thing to send a printer.
|
||||||
|
assert!(FilamentSource::ams(Vec::new(), 5).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_mapping_longer_than_the_printer_has_trays_is_refused() {
|
||||||
|
assert!(FilamentSource::ams(vec![0, 1, 2, 3, 0, 1], 5).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn minus_one_is_the_only_negative_slot_allowed() {
|
||||||
|
assert!(FilamentSource::ams(vec![-1], 5).is_ok());
|
||||||
|
assert!(FilamentSource::ams(vec![-2], 5).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pair that cost a real print: `use_ams` and `ams_mapping` have to
|
||||||
|
/// move together, and the no-AMS case needs the empty *string* rather
|
||||||
|
/// than an empty array - the schema's trailing `oneOf` says so.
|
||||||
|
#[test]
|
||||||
|
fn the_external_spool_sends_an_empty_string_not_an_empty_array() {
|
||||||
|
let mut body = serde_json::json!({});
|
||||||
|
FilamentSource::ExternalSpool.apply(&mut body);
|
||||||
|
assert_eq!(body["use_ams"], serde_json::json!(false));
|
||||||
|
assert_eq!(body["ams_mapping"], serde_json::json!(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_ams_source_sends_an_array_and_turns_use_ams_on() {
|
||||||
|
let mut body = serde_json::json!({});
|
||||||
|
FilamentSource::ams(vec![3], 5).expect("valid").apply(&mut body);
|
||||||
|
assert_eq!(body["use_ams"], serde_json::json!(true));
|
||||||
|
assert_eq!(body["ams_mapping"], serde_json::json!([3, -1, -1, -1, -1]));
|
||||||
|
// Never the external-spool spelling while use_ams is on: that
|
||||||
|
// combination is what silently printed from the wrong source.
|
||||||
|
assert!(!body["ams_mapping"].is_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,42 @@ pub fn upload(
|
|||||||
Ok(written)
|
Ok(written)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Blocking: reads one file off the printer's SD card.
|
||||||
|
///
|
||||||
|
/// Read-only. The reason this exists: a `.3mf` already on the card is the
|
||||||
|
/// only record of which filament each of its plates was sliced for, and
|
||||||
|
/// `ams_mapping` cannot be built without that - see
|
||||||
|
/// `schemas/bambulab/v1/common/ams/ams_mapping.schema.json`. Printing a
|
||||||
|
/// project with the wrong mapping is not a cosmetic error; it draws the
|
||||||
|
/// wrong filament, or none.
|
||||||
|
pub fn download(
|
||||||
|
host: &str,
|
||||||
|
access_code: &str,
|
||||||
|
connector: native_tls::TlsConnector,
|
||||||
|
path: &str,
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let mut ftp = NativeTlsFtpStream::connect_secure_implicit(
|
||||||
|
(host, FTPS_PORT),
|
||||||
|
NativeTlsConnector::from(connector),
|
||||||
|
host,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ftp.login(FTPS_USER, access_code)?;
|
||||||
|
// A `.3mf` is a zip archive; letting the server treat it as text is free
|
||||||
|
// corruption, exactly as on the way up.
|
||||||
|
ftp.transfer_type(FileType::Binary)?;
|
||||||
|
|
||||||
|
// `retr_as_buffer` rather than `retr_as_stream`: suppaftp 12's
|
||||||
|
// `TransferStream` finalizes itself on drop, and the files being read
|
||||||
|
// here are project archives of a few hundred KB, so there is nothing to
|
||||||
|
// gain from streaming them.
|
||||||
|
let bytes = ftp.retr_as_buffer(path)?.into_inner();
|
||||||
|
|
||||||
|
let _ = ftp.quit();
|
||||||
|
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Blocking: lists `cache/` on the printer.
|
/// Blocking: lists `cache/` on the printer.
|
||||||
///
|
///
|
||||||
/// Read-only, and the counterpart to `upload`. The previous version of this
|
/// Read-only, and the counterpart to `upload`. The previous version of this
|
||||||
|
|||||||
+3
-1
@@ -35,7 +35,9 @@ mod klipper;
|
|||||||
mod marlin_serial;
|
mod marlin_serial;
|
||||||
mod prusa;
|
mod prusa;
|
||||||
|
|
||||||
pub use bambu::{BambuPrinter, BambuProtocolPreference, BambuProtocolVersion, BambuTls};
|
pub use bambu::{
|
||||||
|
BambuPrinter, BambuProtocolPreference, BambuProtocolVersion, BambuTls, FilamentSource,
|
||||||
|
};
|
||||||
pub use bambu_commands::{
|
pub use bambu_commands::{
|
||||||
AmsState, AmsTray, AmsUnit, BambuReport, BambuRequest, CommandEnvelope, EmptyTray, GetVersionRequest,
|
AmsState, AmsTray, AmsUnit, BambuReport, BambuRequest, CommandEnvelope, EmptyTray, GetVersionRequest,
|
||||||
GetVersionResponse, LoadedTray, ModuleInfo, PrintReport, PushallRequest, XcamSettings,
|
GetVersionResponse, LoadedTray, ModuleInfo, PrintReport, PushallRequest, XcamSettings,
|
||||||
|
|||||||
Reference in New Issue
Block a user