From 65ffc9a9fc046274c6e74ecdc513c563315c78c8 Mon Sep 17 00:00:00 2001 From: Paul Tagliamonte Date: Thu, 3 Oct 2024 21:36:16 -0400 Subject: [PATCH] Add in MachineState to the Control trait, HTTP Endpoint (#118) This is a first step as we start to chip away on #68 slash #49 and others. This will add a standard part of our "Control" trait to check on the state of a printer -- is the printer running? idle? paused mid-print? Broken? I implemented this for Moonraker (since I was able to test it), and stubbed in Unknown elsewhere. USB is doable here -- and I think we can do Bambu, but that may wait until I get into the office to play with it myself. --- moonraker/src/lib.rs | 1 + moonraker/src/status.rs | 67 +++++++++++++++++++++++++++++++++++++ openapi/api.json | 72 +++++++++++++++++++++++++++++++++++++++- src/any_machine.rs | 6 +++- src/bambu/control.rs | 6 +++- src/lib.rs | 4 +-- src/moonraker/control.rs | 16 ++++++++- src/noop.rs | 8 +++-- src/server/endpoints.rs | 9 ++++- src/sync.rs | 7 ++-- src/traits.rs | 33 ++++++++++++++++++ src/usb/control.rs | 6 +++- 12 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 moonraker/src/status.rs diff --git a/moonraker/src/lib.rs b/moonraker/src/lib.rs index 5ae732e..538fa1e 100644 --- a/moonraker/src/lib.rs +++ b/moonraker/src/lib.rs @@ -12,6 +12,7 @@ mod metrics; mod print; +mod status; mod upload; use anyhow::Result; diff --git a/moonraker/src/status.rs b/moonraker/src/status.rs new file mode 100644 index 0000000..b970e8c --- /dev/null +++ b/moonraker/src/status.rs @@ -0,0 +1,67 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +use super::Client; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct VirtualSdcard { + pub progress: f64, + pub file_position: f64, + pub is_active: bool, + pub file_path: Option, + pub file_size: f64, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct Webhooks { + pub state: String, + pub state_message: String, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PrintStats { + pub print_duration: f64, + pub total_duration: f64, + pub filament_used: f64, + pub filename: String, + pub state: String, + pub message: String, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct Status { + pub virtual_sdcard: VirtualSdcard, + pub webhooks: Webhooks, + pub print_stats: PrintStats, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct QueryResponse { + status: Status, + eventtime: f64, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct QueryResponseWrapper { + result: QueryResponse, +} + +impl Client { + /// Print an uploaded file. + pub async fn status(&self) -> Result { + tracing::debug!(base = self.url_base, "requesting status"); + let client = reqwest::Client::new(); + + let resp: QueryResponseWrapper = client + .get(format!( + "{}/printer/objects/query?webhooks&virtual_sdcard&print_stats", + self.url_base + )) + .send() + .await? + .json() + .await?; + + Ok(resp.result.status) + } +} diff --git a/openapi/api.json b/openapi/api.json index 9af909f..f960588 100644 --- a/openapi/api.json +++ b/openapi/api.json @@ -113,12 +113,21 @@ ], "description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!", "nullable": true + }, + "state": { + "allOf": [ + { + "$ref": "#/components/schemas/MachineState" + } + ], + "description": "Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job." } }, "required": [ "id", "machine_type", - "make_model" + "make_model", + "state" ], "type": "object" }, @@ -143,6 +152,67 @@ }, "type": "object" }, + "MachineState": { + "description": "Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job.", + "oneOf": [ + { + "description": "If a print state can not be resolved at this time, an Unknown may be returned.", + "enum": [ + "Unknown" + ], + "type": "string" + }, + { + "description": "Idle, and ready for another job.", + "enum": [ + "Idle" + ], + "type": "string" + }, + { + "description": "Running a job -- 3D printing or CNC-ing a part.", + "enum": [ + "Running" + ], + "type": "string" + }, + { + "description": "Machine is currently offline or unreachable.", + "enum": [ + "Offline" + ], + "type": "string" + }, + { + "description": "Job is underway but halted, waiting for some action to take place.", + "enum": [ + "Paused" + ], + "type": "string" + }, + { + "description": "Job is finished, but waiting manual action to move back to Idle.", + "enum": [ + "Complete" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "The printer has failed and is in an unknown state that may require manual attention to resolve. The inner value is a human readable description of what specifically has failed.", + "properties": { + "Failed": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "Failed" + ], + "type": "object" + } + ] + }, "MachineType": { "description": "Specific technique by which this Machine takes a design, and produces a real-world 3D object.", "oneOf": [ diff --git a/src/any_machine.rs b/src/any_machine.rs index f84a7b0..9e0b920 100644 --- a/src/any_machine.rs +++ b/src/any_machine.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use crate::{Control as ControlTrait, MachineInfo, MachineMakeModel, MachineType, Volume}; +use crate::{Control as ControlTrait, MachineInfo, MachineMakeModel, MachineState, MachineType, Volume}; /// AnyMachine is any supported machine. #[non_exhaustive] @@ -128,4 +128,8 @@ impl ControlTrait for AnyMachine { async fn healthy(&self) -> bool { for_all!(|self, machine| { machine.healthy().await }) } + + async fn state(&self) -> Result { + for_all!(|self, machine| { machine.state().await }) + } } diff --git a/src/bambu/control.rs b/src/bambu/control.rs index 25f6507..4c5a33e 100644 --- a/src/bambu/control.rs +++ b/src/bambu/control.rs @@ -3,7 +3,7 @@ use bambulabs::{client::Client, command::Command}; use super::{PrinterInfo, X1Carbon}; use crate::{ - Control as ControlTrait, MachineInfo as MachineInfoTrait, MachineMakeModel, MachineType, + Control as ControlTrait, MachineInfo as MachineInfoTrait, MachineMakeModel, MachineState, MachineType, SuspendControl as SuspendControlTrait, ThreeMfControl as ThreeMfControlTrait, ThreeMfTemporaryFile, Volume, }; @@ -74,6 +74,10 @@ impl ControlTrait for X1Carbon { // TODO: fix this true } + + async fn state(&self) -> Result { + Ok(MachineState::Unknown) + } } impl SuspendControlTrait for X1Carbon { diff --git a/src/lib.rs b/src/lib.rs index c1e8af7..deddd09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,8 +43,8 @@ use serde::{Deserialize, Serialize}; pub use slicer::AnySlicer; pub use sync::SharedMachine; pub use traits::{ - Control, GcodeControl, GcodeSlicer, GcodeTemporaryFile, MachineInfo, MachineMakeModel, MachineType, SuspendControl, - ThreeMfControl, ThreeMfSlicer, ThreeMfTemporaryFile, + Control, GcodeControl, GcodeSlicer, GcodeTemporaryFile, MachineInfo, MachineMakeModel, MachineState, MachineType, + SuspendControl, ThreeMfControl, ThreeMfSlicer, ThreeMfTemporaryFile, }; /// A specific file containing a design to be manufactured. diff --git a/src/moonraker/control.rs b/src/moonraker/control.rs index 4f1e768..27fe28e 100644 --- a/src/moonraker/control.rs +++ b/src/moonraker/control.rs @@ -6,7 +6,7 @@ use moonraker::InfoResponse; use super::Client; use crate::{ Control as ControlTrait, GcodeControl as GcodeControlTrait, GcodeTemporaryFile, MachineInfo as MachineInfoTrait, - MachineMakeModel, MachineType, SuspendControl as SuspendControlTrait, Volume, + MachineMakeModel, MachineState, MachineType, SuspendControl as SuspendControlTrait, Volume, }; /// Information about the connected Moonraker-based printer. @@ -64,6 +64,20 @@ impl ControlTrait for Client { async fn healthy(&self) -> bool { self.client.info().await.is_ok() } + + async fn state(&self) -> Result { + let status = self.client.status().await?; + + Ok(match status.print_stats.state.as_str() { + "printing" => MachineState::Running, + "standby" => MachineState::Idle, + "paused" => MachineState::Paused, + "complete" => MachineState::Complete, + "cancelled" => MachineState::Complete, + "error" => MachineState::Failed(Some(status.print_stats.message.to_owned())), + _ => MachineState::Unknown, + }) + } } impl SuspendControlTrait for Client { diff --git a/src/noop.rs b/src/noop.rs index 02f805b..dd43db7 100644 --- a/src/noop.rs +++ b/src/noop.rs @@ -5,8 +5,8 @@ use anyhow::Result; use crate::{ Control as ControlTrait, GcodeControl as GcodeControlTrait, GcodeTemporaryFile, MachineInfo as MachineInfoTrait, - MachineMakeModel, MachineType, SuspendControl as SuspendControlTrait, ThreeMfControl as ThreeMfControlTrait, - ThreeMfTemporaryFile, Volume, + MachineMakeModel, MachineState, MachineType, SuspendControl as SuspendControlTrait, + ThreeMfControl as ThreeMfControlTrait, ThreeMfTemporaryFile, Volume, }; /// Noop-machine will no-op, well, everything. @@ -70,6 +70,10 @@ impl ControlTrait for Noop { async fn healthy(&self) -> bool { true } + + async fn state(&self) -> Result { + Ok(MachineState::Unknown) + } } impl SuspendControlTrait for Noop { diff --git a/src/server/endpoints.rs b/src/server/endpoints.rs index 006f827..27a16bf 100644 --- a/src/server/endpoints.rs +++ b/src/server/endpoints.rs @@ -5,7 +5,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::{Context, CorsResponseOk}; -use crate::{AnyMachine, Control, DesignFile, MachineInfo, MachineMakeModel, MachineType, TemporaryFile, Volume}; +use crate::{ + AnyMachine, Control, DesignFile, MachineInfo, MachineMakeModel, MachineState, MachineType, TemporaryFile, Volume, +}; /// Return the OpenAPI schema in JSON format. #[endpoint { @@ -68,6 +70,10 @@ pub struct MachineInfoResponse { /// What "close" means is up to you! pub max_part_volume: Option, + /// Status of the printer -- be it printing, idle, or unreachable. This + /// may dictate if a machine is capable of taking a new job. + pub state: MachineState, + /// Additional, per-machine information which is specific to the /// underlying machine type. pub extra: Option, @@ -83,6 +89,7 @@ impl MachineInfoResponse { make_model: machine_info.make_model(), machine_type: machine_info.machine_type(), max_part_volume: machine_info.max_part_volume(), + state: machine.state().await?, extra: match machine { AnyMachine::Moonraker(_) => Some(ExtraMachineInfoResponse::Moonraker {}), AnyMachine::Usb(_) => Some(ExtraMachineInfoResponse::Usb {}), diff --git a/src/sync.rs b/src/sync.rs index 0f42866..871bab3 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,9 +1,7 @@ +use crate::{Control, MachineState}; use std::sync::Arc; - use tokio::sync::Mutex; -use crate::Control; - /// Wrapper around an `Arc>`, which helpfully will handle /// the locking to expose a [Control] without the caller having to care /// that this is a shared handle. @@ -42,4 +40,7 @@ where async fn healthy(&self) -> bool { self.0.lock().await.healthy().await } + async fn state(&self) -> Result { + self.0.lock().await.state().await + } } diff --git a/src/traits.rs b/src/traits.rs index 10d69c7..8da9bd9 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -50,6 +50,36 @@ pub trait MachineInfo { fn max_part_volume(&self) -> Option; } +/// Current state of the machine -- be it printing, idle or offline. This can +/// be used to determine if a printer is in the correct state to take a new +/// job. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub enum MachineState { + /// If a print state can not be resolved at this time, an Unknown may + /// be returned. + Unknown, + + /// Idle, and ready for another job. + Idle, + + /// Running a job -- 3D printing or CNC-ing a part. + Running, + + /// Machine is currently offline or unreachable. + Offline, + + /// Job is underway but halted, waiting for some action to take place. + Paused, + + /// Job is finished, but waiting manual action to move back to Idle. + Complete, + + /// The printer has failed and is in an unknown state that may require + /// manual attention to resolve. The inner value is a human + /// readable description of what specifically has failed. + Failed(Option), +} + /// A `Machine` is something that can take a 3D model (in one of the /// supported formats), and create a physical, real-world copy of /// that model. @@ -87,6 +117,9 @@ pub trait Control { /// `false` means the machine is no longer reachable or usable, and /// ought to be removed. fn healthy(&self) -> impl Future; + + /// Return the state of the printer. + fn state(&self) -> impl Future>; } /// [ControlGcode] is used by Machines that accept gcode, control commands diff --git a/src/usb/control.rs b/src/usb/control.rs index 0723bee..f92013e 100644 --- a/src/usb/control.rs +++ b/src/usb/control.rs @@ -9,7 +9,7 @@ use tokio_serial::SerialStream; use crate::{ gcode::Client, Control as ControlTrait, GcodeControl as GcodeControlTrait, GcodeTemporaryFile, - MachineInfo as MachineInfoTrait, MachineMakeModel, MachineType, Volume, + MachineInfo as MachineInfoTrait, MachineMakeModel, MachineState, MachineType, Volume, }; /// Handle to a USB based gcode 3D printer. @@ -132,6 +132,10 @@ impl ControlTrait for Usb { self.client.lock().await.stop().await } + async fn state(&self) -> Result { + Ok(MachineState::Unknown) + } + async fn healthy(&self) -> bool { // TODO: fix this, do a gcode ping or something? true