commit ea6fd69abcc51d15c4f62f071e2b208c96d3ebff Author: sigil-03 Date: Thu Sep 11 17:31:53 2025 -0600 initial commit after modification of prusatool-rs to make libraryish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2d2c8df --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "prusalib-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +reqwest = { version = "0.12.23", features = ["blocking"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.143" +thiserror = "2.0.16" +toml = "0.9.5" +uuid = { version = "1.18.1", features = ["v4"] } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ff9537a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +mod prusa; diff --git a/src/prusa.rs b/src/prusa.rs new file mode 100644 index 0000000..5643302 --- /dev/null +++ b/src/prusa.rs @@ -0,0 +1,277 @@ +use core::time::Duration; +use std::fs; +use thiserror::Error; +use serde::Deserialize; +use reqwest::blocking::Client; +use uuid::Uuid; + + +#[derive(Deserialize, Debug)] +struct Telemetry { + #[serde(rename = "temp-bed")] + temp_bed: f32, + #[serde(rename = "temp-nozzle")] + temp_nozzle: f32, +} + +#[derive(Deserialize, Debug)] +struct State { + text: String, +} + +#[derive(Deserialize, Debug)] +struct Info { + telemetry: Telemetry, + // temperature: Temperature, + state: State, + +} + +#[derive(Deserialize, Debug)] +struct StorageInfo { + storage_list: Vec, +} + +#[derive(Deserialize, Debug)] +struct Storage { + path: String, + name: String, +} + +#[derive(Deserialize, Debug)] +pub struct PrusaConfig { + api_key: String, + ip_addr: String, +} + +pub struct Prusa { + config: PrusaConfig, + client: Client, +} + +impl Prusa { + pub fn new(config: PrusaConfig) -> Self { + Self { + config, + client: Client::new(), + } + } + + + pub fn new_from_file(filepath: &str) -> Result { + let file_data = fs::read_to_string(filepath)?; + let config: PrusaConfig = toml::from_str(&file_data)?; + Ok(Self::new(config)) + } + + + pub fn get_info(&self) -> Result { + let api_target = "/api/printer"; + let resp = self.client.get(format!("http://{}{api_target}", self.config.ip_addr)) + .header("X-Api-Key", &self.config.api_key) + .send()?; + let text = resp.text()?; + // println!("{text}"); + let info: Info = serde_json::from_str(&text)?; + // println!("{info:?}"); + Ok(info) + } + + // TODO return this into a specific type + pub fn get_storage_info(&self) -> Result { + let api_target = "/api/v1/storage"; + let resp = self.client.get(format!("http://{}{api_target}", self.config.ip_addr)) + .header("X-Api-Key", &self.config.api_key) + .send()?; + let text = resp.text()?; + // println!("{text}"); + let info: StorageInfo = serde_json::from_str(&text)?; + + Ok(info) + } + + // Prusalink load file schema: + // content: + // application/octet-stream: + // schema: + // type: string + // format: binary + + + // Need parameters + /* + parameters: + - in: header + name: Content-Length + description: Length of file to upload + schema: + type: integer + example: 101342 + - in: header + name: Content-Type + description: Type of uploaded media + schema: + type: string + default: application/octet-stream + - in: header + name: Print-After-Upload + description: Whether to start printing the file after upload + schema: + type: string + description: ?0=False, ?1=True, according RFC8941/3.3.6 + enum: [ "?0", "?1" ] + default: "?0" + - in: header + name: Overwrite + description: Whether to overwrite already existing files + schema: + type: string + description: ?0=False, ?1=True, according RFC8941/3.3.6 + enum: ["?0", "?1"] + default: "?0" + */ + + // TODO: async + pub fn try_load_file(&self, filepath: &str) -> Result { + let data = fs::read(filepath)?; + + let uuid = Uuid::new_v4(); + // TODO: Allow storage selection + // For now, assume that we're using only USB: + let api_target = format!("/api/v1/files/usb/{uuid}.bgcode"); + let resp = self.client.put(format!("http://{}{api_target}", self.config.ip_addr)) + .header("X-Api-Key", &self.config.api_key) + .header("Content-Type", "application/octet-stream") + .header("Content-Length", data.len()) + .header("Print-After-Upload", "0") + .header("Overwrite", "0") + .body(data) + .timeout(Duration::from_secs(60)) + .send()?; + let text = resp.text()?; + println!("{text}"); + + // todo!("Implement UUID Handling"); + Ok(uuid) + } + + // /api/v1/files/{storage}/{path}: + // + // post: + /* summary: Start print of file if there's no print job running + description: Body is ignored + requestBody: + required: false + content: {} + responses: + 204: + description: No Content + 401: + $ref: "#/components/responses/Unauthorized" + 404: + $ref: "#/components/responses/NotFound" + 409: + $ref: "#/components/responses/Conflict" + head: + summary: file presence and state check + responses: + 200: + description: OK + headers: + Read-Only: + description: Whether the file or storage is read-only + required: true + schema: + type: boolean + example: true + Currently-Printed: + description: Whether this file is currently being printed + required: true + schema: + type: boolean + example: true + */ + + // TODO: async + pub fn try_print_file(&self, id: &Uuid) -> Result<(), Error> { + // /api/v1/files/{storage}/{path} + let api_target = format!("/api/v1/files/usb/{id}.bgcode"); + let resp = self.client.post(format!("http://{}{api_target}", self.config.ip_addr)) + .header("X-Api-Key", &self.config.api_key) + .header("Content-Type", "application/octet-stream") + .send()?; + let text = resp.text()?; + println!("{text}"); + + Ok(()) + } + + + // TODO: poorly named, should be something else so as not to confuse with print operations + pub fn print_info(&self) -> Result<(), Error> { + let info = self.get_info()?; + println!("\n--------------------"); + println!("STATE:\t{}", info.state.text); + println!("NOZZLE:\t{}", info.telemetry.temp_nozzle); + println!("BED:\t{}", info.telemetry.temp_bed); + println!("--------------------\n"); + + Ok(()) + } + + pub fn print_storage_info(&self) -> Result<(), Error> { + let info = self.get_storage_info()?; + for (index, storage) in info.storage_list.iter().enumerate() { + println!("\n------------------"); + println!("STORAGE: {index}"); + println!(" PATH:\t{}", storage.path); + println!(" NAME:\t{}", storage.name); + println!("--------------------\n"); + } + Ok(()) + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("File error")] + FileError(#[from] std::io::Error), + #[error("Toml Parse Error")] + TomlError(#[from] toml::de::Error), + #[error("HTTP(S) Error")] + HttpError(#[from] reqwest::Error), + #[error("Serde JSON Error")] + JsonError(#[from] serde_json::Error), +} + + +// fn main() -> Result<(), Error> { +// let cli = Cli::parse(); +// let file_data = fs::read_to_string(cli.printer_config)?; +// let config: Config = toml::from_str(&file_data)?; +// // println!("{:?}", config); +// +// match config.printer.model.as_str() { +// "prusa-mk4" => { +// let prusa = Prusa::new(&config.printer.api_key, &config.printer.ip_addr); +// match cli.command { +// Command::Info => { +// prusa.print_info()?; +// prusa.print_storage_info()?; +// }, +// // Should generate UUID for the filename: +// Command::Load {filepath, print_immediately} => { +// let uuid = prusa.try_load_file(&filepath)?; +// if print_immediately { +// prusa.try_print_file(&uuid)?; +// } +// println!("Loaded as UUID:\n{uuid}"); +// }, +// Command::Print {file_id} => prusa.try_print_file(&file_id)?, +// } +// } +// _ => println!("Unrecognized printer type") +// } +// +// Ok(()) +// }