Skip to content

Commit

Permalink
Add in MachineState to the Control trait, HTTP Endpoint (#118)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
paultag authored Oct 4, 2024
1 parent 67f6825 commit 65ffc9a
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 13 deletions.
1 change: 1 addition & 0 deletions moonraker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

mod metrics;
mod print;
mod status;
mod upload;

use anyhow::Result;
Expand Down
67 changes: 67 additions & 0 deletions moonraker/src/status.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
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<Status> {
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)
}
}
72 changes: 71 additions & 1 deletion openapi/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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": [
Expand Down
6 changes: 5 additions & 1 deletion src/any_machine.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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<MachineState> {
for_all!(|self, machine| { machine.state().await })
}
}
6 changes: 5 additions & 1 deletion src/bambu/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -74,6 +74,10 @@ impl ControlTrait for X1Carbon {
// TODO: fix this
true
}

async fn state(&self) -> Result<MachineState> {
Ok(MachineState::Unknown)
}
}

impl SuspendControlTrait for X1Carbon {
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion src/moonraker/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -64,6 +64,20 @@ impl ControlTrait for Client {
async fn healthy(&self) -> bool {
self.client.info().await.is_ok()
}

async fn state(&self) -> Result<MachineState> {
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 {
Expand Down
8 changes: 6 additions & 2 deletions src/noop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -70,6 +70,10 @@ impl ControlTrait for Noop {
async fn healthy(&self) -> bool {
true
}

async fn state(&self) -> Result<MachineState> {
Ok(MachineState::Unknown)
}
}

impl SuspendControlTrait for Noop {
Expand Down
9 changes: 8 additions & 1 deletion src/server/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -68,6 +70,10 @@ pub struct MachineInfoResponse {
/// What "close" means is up to you!
pub max_part_volume: Option<Volume>,

/// 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<ExtraMachineInfoResponse>,
Expand All @@ -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 {}),
Expand Down
7 changes: 4 additions & 3 deletions src/sync.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use crate::{Control, MachineState};
use std::sync::Arc;

use tokio::sync::Mutex;

use crate::Control;

/// Wrapper around an `Arc<Mutex<Control>>`, which helpfully will handle
/// the locking to expose a [Control] without the caller having to care
/// that this is a shared handle.
Expand Down Expand Up @@ -42,4 +40,7 @@ where
async fn healthy(&self) -> bool {
self.0.lock().await.healthy().await
}
async fn state(&self) -> Result<MachineState, Self::Error> {
self.0.lock().await.state().await
}
}
33 changes: 33 additions & 0 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ pub trait MachineInfo {
fn max_part_volume(&self) -> Option<Volume>;
}

/// 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<String>),
}

/// 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.
Expand Down Expand Up @@ -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<Output = bool>;

/// Return the state of the printer.
fn state(&self) -> impl Future<Output = Result<MachineState, Self::Error>>;
}

/// [ControlGcode] is used by Machines that accept gcode, control commands
Expand Down
6 changes: 5 additions & 1 deletion src/usb/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -132,6 +132,10 @@ impl ControlTrait for Usb {
self.client.lock().await.stop().await
}

async fn state(&self) -> Result<MachineState> {
Ok(MachineState::Unknown)
}

async fn healthy(&self) -> bool {
// TODO: fix this, do a gcode ping or something?
true
Expand Down

0 comments on commit 65ffc9a

Please sign in to comment.