1
2use crate::{
4 pdf_manager::PDFManager,
5 send_message,
6 service::{Service, WebSocketWrite},
7};
8
9use async_trait::async_trait;
10use base64::{engine::general_purpose, Engine};
11use easy_trace::prelude::{debug, error, info};
12use futures_util::SinkExt;
13use lazy_static::lazy_static;
14use md5;
15use regex::{Regex, RegexBuilder};
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use std::{
19 collections::HashMap,
20 fs::File,
21 io::Write,
22 path::Path,
23 process::{Command, Stdio},
24 sync::Arc,
25};
26use printers::common::base::job::PrinterJobOptions;
27use tokio::sync::Mutex;
28use tokio::task;
29use tokio::time::timeout;
30use tokio::time::{sleep, Duration};
31
32#[cfg(target_os = "windows")]
34use {
35 std::{
36 os::windows::{
37 process::CommandExt,
38 },
39 },
40};
41
42
43
44use printers::common::base::printer::Printer;
45use sharing::paths::WKHTMLTOPDF_PATH;
46use sharing::utils::ConfigEnv;
47
48type BoxError = Box<dyn std::error::Error + Send + Sync>;
49
50lazy_static! {
51 static ref PDF_MANAGER: Mutex<PDFManager> = Mutex::new(PDFManager::new());
53}
54
55pub const PRINT_VERSION: &str = "1.0.0";
57pub const BUFFER_OPEN_DRAWER: &[u8] = b"\x1B\x70\x00\x64\xC8";
59pub const COMMAND_VIEWER_NAME: &str = "CommandViewer";
65
66pub struct PrintService {
68 printers: Arc<Mutex<Vec<Printer>>>, list_printers: Arc<Mutex<Vec<String>>>, config: ConfigEnv, command_viewer: bool, }
73
74impl Clone for PrintService {
75 fn clone(&self) -> Self {
76 PrintService {
77 printers: Arc::clone(&self.printers),
78 list_printers: Arc::clone(&self.list_printers),
79 config: self.config.clone(),
80 command_viewer: self.command_viewer,
81 }
82 }
83}
84
85#[derive(Deserialize, Serialize, Debug, Clone)]
87#[serde(rename_all = "PascalCase")]
88pub enum PrintAction {
89 Print {
90 content: String,
91 printer_name: Option<String>,
92 copies: Option<u32>,
93 open: bool,
94 },
95 OpenDrawer {
96 printer_name: String,
97 },
98 Check,
99 ListPrinters,
100 Unknown,
101}
102
103impl PrintService {
104 pub async fn new(mut config: ConfigEnv) -> Self {
106 let devices = PrintService::list_printers().await;
107 println!("PrintService initialized with {} printers", devices.len());
108
109 let simplified_devices: Vec<String> = devices
111 .iter()
112 .map(|p| format!("name: {}, driver_name: {}", p.name, p.driver_name))
113 .collect();
114 debug!("PrintService initialized with printers: {:?}", simplified_devices);
115
116 let mut names: std::collections::HashSet<String> = devices.iter().map(|p| p.name.clone()).collect();
118
119 info!("modules configured: {:?}", config.modules);
121 let command_viewer = config.modules.iter().any(|m| m == "commandViewer");
122 if command_viewer {
123 names.insert(COMMAND_VIEWER_NAME.to_string());
124 }
125 info!("CommandViewer enabled: {}", command_viewer);
126
127 if let Some(cfg_list) = &config.list_printers {
129 for p in cfg_list {
130 names.insert(p.clone());
131 }
132 } else {
133 info!("No printers configured in config, using detected ones");
134 }
135
136 let mut merged: Vec<String> = names.into_iter().collect();
138 merged.sort();
139
140 config.list_printers = Some(merged.clone());
142
143 info!("PRINTERS NAMES (merged): {:?}", merged);
144
145 config.save(); PrintService {
148 printers: Arc::new(Mutex::new(devices)),
149 list_printers: Arc::new(Mutex::new(merged)),
150 config,
151 command_viewer,
152 }
153 }
154
155 async fn add_printer_string(&mut self, printer_name: &str) {
156 info!("LIST_PRINTERS BEFORE: {:?}", self.config.list_printers);
157 let mut printers = self.list_printers.lock().await;
158 if !printers.contains(&printer_name.to_string()) {
159 printers.push(printer_name.to_string());
160 }
161 self.config.list_printers
162 .get_or_insert_with(Vec::new)
163 .push(printer_name.to_string());
164 info!("list_printers updated: {:?}", self.config.list_printers);
165 self.config.save();
166 }
167
168 async fn get_print_list(&self) -> Vec<String> {
188 let devices = PrintService::list_printers().await;
189
190 let mut names: std::collections::HashSet<String> =
192 devices.iter().map(|p| p.name.clone()).collect();
193
194 {
200 let current = self.list_printers.lock().await;
201 for name in current.iter() {
202 names.insert(name.clone());
203 }
204 }
205
206 if self.command_viewer {
207 names.insert(COMMAND_VIEWER_NAME.to_string());
208 }
209 if let Some(cfg_list) = &self.config.list_printers {
210 for p in cfg_list {
211 names.insert(p.clone());
212 }
213 }
214
215 let mut merged: Vec<String> = names.into_iter().collect();
216 merged.sort();
217
218 *self.printers.lock().await = devices;
220 *self.list_printers.lock().await = merged.clone();
221
222 merged
223 }
224
225
226 fn extract_css_from_html(&self, html: &str) -> HashMap<String, String> {
231 let mut extracted_css = HashMap::new();
232
233 extracted_css.insert("margin-top".to_string(), "0mm".to_string());
234 extracted_css.insert("margin-right".to_string(), "0mm".to_string());
235 extracted_css.insert("margin-bottom".to_string(), "0mm".to_string());
236 extracted_css.insert("margin-left".to_string(), "0mm".to_string());
237 extracted_css.insert("page-width".to_string(), "72mm".to_string());
238 extracted_css.insert("page-height".to_string(), "297mm".to_string());
239
240 let page_re = RegexBuilder::new(r"@page\s*\{\s*([^}]*)\s*\}")
241 .dot_matches_new_line(true)
242 .build();
243
244 match page_re {
245 Ok(page_re) => {
246 if let Some(page_match) = page_re.captures(html) {
248 let page_css = page_match.get(1).map_or("", |m| m.as_str());
249
250 if let Ok(size_re) = Regex::new(r"size:\s*([\d.]+mm)\s+([\d.]+mm)(?:\s+\w+)?;")
252 {
253 if let Some(size_match) = size_re.captures(page_css) {
254 let page_width = size_match.get(1).map_or("", |m| m.as_str());
255 let page_height = size_match.get(2).map_or("", |m| m.as_str());
256 extracted_css.insert("page-width".to_string(), page_width.to_string());
257 extracted_css
258 .insert("page-height".to_string(), page_height.to_string());
259 }
260 }
261
262 if let Ok(margin_re) =
264 Regex::new(r"margin-(top|right|bottom|left):\s*([\d.]+[a-z]+);")
265 {
266 for margin_match in margin_re.captures_iter(page_css) {
267 let margin_name = margin_match.get(1).map_or("", |m| m.as_str());
268 let margin_value = margin_match.get(2).map_or("", |m| m.as_str());
269 extracted_css.insert(
270 format!("margin-{}", margin_name),
271 margin_value.to_string(),
272 );
273 }
274 }
275 }
276 }
277 Err(e) => {
278 error!("Error to create regex: {}", e);
279 }
280 }
281
282 extracted_css
283 }
284
285 async fn run_action(&mut self, action: PrintAction) -> (i32, String) {
287 match action {
288 PrintAction::Print {
289 content,
290 printer_name,
291 copies,
292 open,
293 } => {
294 info!("Printing content");
295
296 let print_result = self
298 .save_and_print_pdf(&content, printer_name.as_deref(), copies.unwrap_or(1))
299 .await;
300
301 match print_result {
302 Ok(_) => {
303 info!("Print job completed successfully.");
304
305 if open {
306 if let Some(printer_name) = printer_name {
307 info!("Opening drawer for printer: {}", printer_name);
308 let (status, message) = self.open_drawer(&printer_name).await;
309 if status != 0 {
310 error!("Failed to open drawer: {}", message);
311 } else {
312 info!("Drawer opened successfully");
313 }
314 } else {
315 error!("Printer name is not provided for opening drawer");
316 }
317 }
318
319 (0, "print ok".to_string())
320 }
321 Err(e) => {
322 error!("Failed to print: {}", e);
323 (1, "print failed".to_string())
324 }
325 }
326 }
327 PrintAction::OpenDrawer { printer_name } => {
328 info!("Opening drawer for printer: {}", printer_name);
329 self.open_drawer(&printer_name).await
330 }
331 PrintAction::Check => {
332 info!("Performing check action");
333 (0, "check ok".to_string())
334 }
335 PrintAction::ListPrinters => {
336 let printers = self.printers.lock();
337 let printer_names = printers
338 .await
339 .iter()
340 .map(|p| p.name.clone())
341 .collect::<Vec<String>>();
342 (0, serde_json::to_string(&printer_names).unwrap())
343 }
344 PrintAction::Unknown => {
345 error!("Unknown action");
346 (1, "unknown action".to_string())
347 }
348 }
349 }
350
351 async fn send_html_to_kitchen(
352 &self,
353 decoded_html: String,
354 _id: &str,
355 print_name: &str,
356 ) -> Result<(), BoxError> {
357 use serde_json::json;
358 use tokio_tungstenite::{connect_async, tungstenite::Message};
359
360 let url = "ws://127.0.0.1:9001"; let (mut socket, _) = connect_async(url).await?;
362 let uuid = uuid::Uuid::new_v4();
364 let command_message = json!({
366 "action": "addCommand",
367 "data": {
368 "id": format!("{}", uuid), "html": decoded_html,
370 "order_no": "",
371 "archived": false,
372 "printer": print_name,
373 }
374 });
375
376 socket
378 .send(Message::Text(command_message.to_string()))
379 .await?;
380 info!("HTML sent to kitchen");
382 Ok(())
383 }
384
385 #[cfg(not(target_os = "windows"))]
397 async fn print_pdf(
399 &self,
400 file_path: &Path,
401 printer_name: Option<&str>,
402 copies: u32,
403 ) -> Result<(), BoxError> {
404 info!("Printing with printers crate: {:?} printer {:?} copies {}",
405 file_path, printer_name, copies);
406
407 if !file_path.exists() {
409 error!("PDF file not found: {:?}", file_path);
410 return Err("PDF file not found".into());
411 }
412
413 let printer_name = printer_name.unwrap_or("Default");
414
415 if printer_name == COMMAND_VIEWER_NAME {
417 info!("Skipping print for CommandViewer");
418 return Ok(());
419 }
420
421 let printer = if printer_name == "Default" {
423 printers::get_default_printer()
424 } else {
425 printers::get_printer_by_name(printer_name)
426 };
427
428 let printer = match printer {
429 Some(p) => p,
430 None => {
431 let new_printers = PrintService::list_printers().await;
433 let mut printers_cache = self.printers.lock().await;
434 *printers_cache = new_printers;
435
436 return Err(format!("Printer '{}' not found", printer_name).into());
437 }
438 };
439
440 for copy in 1..=copies {
442 let job_name = format!("Print Job {} (copy {}/{})",
443 file_path.file_name().unwrap_or_default().to_string_lossy(),
444 copy, copies);
445
446 let options = PrinterJobOptions {
447 name: Some(&job_name),
448 raw_properties: &[],
449 };
450
451 match printer.print_file(file_path.to_str().unwrap(), options) {
452 Ok(_) => {
453 info!("Print job {} sent successfully", job_name);
454 }
455 Err(e) => {
456 error!("Failed to print copy {}: {:?}", copy, e);
457 return Err(format!("Failed to print: {:?}", e).into());
458 }
459 }
460 }
461
462 info!("All {} copies printed successfully", copies);
463 Ok(())
464 }
465
466 pub async fn send_to_printer(&self, printer_name: &str, data: &[u8]) -> Result<(), BoxError> {
468 info!("Sending raw data to printer: {} ({} bytes)", printer_name, data.len());
469
470 let printers = self.printers.lock().await;
472 let printer = printers.iter()
473 .find(|p| p.name == printer_name)
474 .ok_or_else(|| format!("Printer '{}' not found", printer_name))?
475 .clone();
476
477 drop(printers); info!("Found printer: {} (driver: {})", printer.name, printer.driver_name);
480
481 let result = tokio::task::spawn_blocking({
483 let data = data.to_vec();
484 move || {
485 let options = PrinterJobOptions {
486 name: Some("Open Drawer Command"),
487 raw_properties: &[],
488 };
489
490 printer.print(&data, options)
491 }
492 }).await;
493
494 match result {
495 Ok(Ok(job)) => {
496 info!("Raw data sent successfully. Job: {:?}", job);
497 Ok(())
498 }
499 Ok(Err(e)) => {
500 error!("Failed to print raw data: {:?}", e);
501 Err(format!("Print failed: {:?}", e).into())
502 }
503 Err(e) => {
504 error!("Task spawn failed: {}", e);
505 Err(format!("Task spawn failed: {}", e).into())
506 }
507 }
508 }
509
510 pub async fn open_drawer(&self, printer_name: &str) -> (i32, String) {
512 info!("Opening drawer for printer: {}", printer_name);
513
514 match self.send_to_printer(printer_name, BUFFER_OPEN_DRAWER).await {
515 Ok(_) => {
516 info!("Drawer opened successfully");
517 (0, "drawer opened".to_string())
518 }
519 Err(e) => {
520 error!("Failed to open drawer: {}", e);
521 (1, "drawer failed".to_string())
522 }
523 }
524 }
525
526
527 async fn save_and_print_pdf(
528 &mut self,
529 content: &str,
530 printer_name: Option<&str>,
531 copies: u32,
532 ) -> Result<(), BoxError> {
533 let job_dir = sharing::paths::jobs_dir();
535 if !job_dir.exists() {
536 match std::fs::create_dir_all(&job_dir) {
537 Ok(_) => {
538 info!("Created jobs directory");
539 }
540 Err(e) => {
541 error!("Failed to create jobs directory: {}", e);
542 return Err(e.into());
543 }
544 }
545 }
546
547 let decoded_html = match general_purpose::STANDARD.decode(content) {
549 Ok(decoded) => decoded,
550 Err(e) => {
551 error!("Failed to decode content: {}", e);
552 return Err(e.into());
553 }
554 };
555
556 let decoded_html_str = String::from_utf8(decoded_html.clone())
558 .map_err(|e| format!("Failed to convert decoded content to string: {}", e))?;
559
560 let css_properties = self.extract_css_from_html(&decoded_html_str);
562
563 let page_width = css_properties
565 .get("page-width")
566 .unwrap_or(&"72mm".to_string())
567 .clone();
568 let page_height = css_properties
569 .get("page-height")
570 .unwrap_or(&"297mm".to_string())
571 .clone();
572 let margin_top = css_properties
573 .get("margin-top")
574 .unwrap_or(&"0mm".to_string())
575 .clone();
576 let margin_right = css_properties
577 .get("margin-right")
578 .unwrap_or(&"0mm".to_string())
579 .clone();
580 let margin_bottom = css_properties
581 .get("margin-bottom")
582 .unwrap_or(&"0mm".to_string())
583 .clone();
584 let margin_left = css_properties
585 .get("margin-left")
586 .unwrap_or(&"0mm".to_string())
587 .clone();
588
589 let file_md5 = format!("{:x}", md5::compute(decoded_html_str.as_bytes()));
590 let name = printer_name.unwrap_or("POS-80C");
591
592 if self.command_viewer{
593 if let Err(e) = self
594 .send_html_to_kitchen(decoded_html_str.clone(), &file_md5.clone(), name)
595 .await
596 {
597 error!("Failed to send HTML to kitchen: {}", e);
598 }
599 }
600
601 let pdf_filename = format!("{}_{}.pdf", self.config.service_name, file_md5);
603 let pdf_path = job_dir.join(pdf_filename);
604 info!("PDF path: {:?}", pdf_path);
605
606 if !pdf_path.exists() {
608 let html_filename = format!("{}_{}.html", self.config.service_name, file_md5);
610 let html_path = job_dir.join(html_filename);
611 let mut file = match File::create(&html_path) {
612 Ok(f) => f,
613 Err(e) => {
614 error!("Failed to create HTML file: {}", e);
615 return Err(e.into());
616 }
617 };
618 match file.write_all(&decoded_html.clone()) {
619 Ok(_) => {
620 info!("HTML content saved to file: {:?}", html_path);
621 }
622 Err(e) => {
623 error!("Failed to write HTML content to file: {}", e);
624 return Err(e.into());
625 }
626 }
627
628
629 if !Path::new(sharing::paths::WKHTMLTOPDF_PATH.as_str()).exists() {
631 info!("WKHTMLTOPDF_PATH: {:?}", sharing::paths::WKHTMLTOPDF_PATH);
632 return Err("wkhtmltopdf executable not found".into());
633 }
634 let wkhtmltopdf_path = WKHTMLTOPDF_PATH.as_str().to_string();
635 let pdf_path_clone = pdf_path.clone();
637 let html_path_clone = html_path.clone();
638
639 info!("Converting HTML to PDF");
640 let result = timeout(
641 Duration::from_secs(10),
642 task::spawn_blocking(move || {
643 let mut command = Command::new(&wkhtmltopdf_path);
644 command
645 .arg("--page-width")
646 .arg(page_width)
647 .arg("--page-height")
648 .arg(page_height)
649 .arg("--margin-top")
650 .arg(margin_top)
651 .arg("--margin-right")
652 .arg(margin_right)
653 .arg("--margin-bottom")
654 .arg(margin_bottom)
655 .arg("--margin-left")
656 .arg(margin_left)
657 .arg("--print-media-type")
658 .arg("--no-pdf-compression") .arg("--image-quality")
660 .arg("100")
661 .arg(&html_path_clone)
662 .arg(&pdf_path_clone)
663 .stdout(Stdio::null())
664 .stderr(Stdio::null());
665
666 #[cfg(target_os = "windows")]
668 command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
669
670 let status = command
671 .spawn()
672 .map_err(|e| format!("Failed to execute wkhtmltopdf: {}", e))?
673 .wait()
674 .map_err(|e| format!("Failed to wait for wkhtmltopdf: {}", e))?;
675
676 if status.success() {
677 Ok(())
678 } else {
679 Err(format!("wkhtmltopdf failed with exit code: {:?}", status))
680 }
681 }),
682 )
683 .await??;
684 match result {
685 Ok(_) => {
686 info!("HTML successfully converted to PDF.");
687 }
688 Err(e) => {
689 error!("Failed to convert HTML to PDF: {}", e);
690 return Err(e.into());
691 }
692 }
693
694
695 match std::fs::remove_file(&html_path) {
697 Ok(_) => {
698 info!("HTML file deleted: {:?}", html_path);
699 }
700 Err(e) => {
701 error!("Failed to delete HTML file: {}", e);
702 return Err(e.into());
703 }
704 }
705 }
706
707 info!("PDF file generated: {:?}", pdf_path);
708 if name.contains(COMMAND_VIEWER_NAME) {
710 if !self.list_printers.lock().await.contains(&name.to_string()) {
712 info!("Adding CommandViewer to printer list");
713 self.add_printer_string(name).await;
714 }
715 PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
716 return Ok(());
717 }
718 #[cfg(target_os = "windows")]
720 {
721 match self.print_with_sumatra(pdf_path.as_path(), printer_name, copies).await {
722 Ok(_) => {
723 info!("Print job sent successfully");
724 }
725 Err(e) => {
726 error!("Failed to send print job: {}", e);
727 PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
728 return Err(e.into());
729 }
730 }
731 }
732 #[cfg(not(target_os = "windows"))]
733 {
734 match self.print_pdf(pdf_path.as_path(), printer_name, copies).await {
735 Ok(_) => {
736 info!("Print job sent successfully");
737 }
738 Err(e) => {
739 error!("Failed to send print job: {}", e);
740 crate::printsrvc::PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
741 return Err(e.into());
742 }
743 }
744 }
745
746 PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
747 Ok(())
748 }
749
750
751 #[cfg(target_os = "windows")]
752 async fn print_with_sumatra(
763 &self,
764 file_path: &Path,
765 printer_name: Option<&str>,
766 copies: u32,
767 ) -> Result<(), BoxError> {
768 info!(
769 "Printing with SumatraPDF file {:?} printer {:?} copies {}",
770 file_path, printer_name, copies
771 );
772 if !Path::new(sharing::paths::SUMATRA_PATH.as_str()).exists() {
773 return Err("SumatraPDF executable not found".into());
774 }
775 let name = printer_name.unwrap_or("Default Printer");
776
777 let printer_exists = {
779 let printers = self.printers.lock().await; printers.iter().any(|p| p.name == name) || name == COMMAND_VIEWER_NAME
781 };
782
783 if !printer_exists {
785 let new_printers = PrintService::list_printers().await;
786 let mut printers = self.printers.lock().await; if !new_printers.iter().any(|p| p.name == name) {
788 return Err("Printer not found".into());
789 }
790 *printers = new_printers;
791 }
792
793 for _ in 0..copies {
794 let file_path = file_path.to_path_buf(); let name = name.to_string(); let result = timeout(
798 Duration::from_secs(5),
799 task::spawn_blocking(move || {
800 Command::new(sharing::paths::SUMATRA_PATH.as_str())
801 .arg("-print-to")
802 .arg(name)
803 .arg("-print-settings")
804 .arg("noscale") .arg(file_path)
806 .stdout(Stdio::null())
807 .stderr(Stdio::null())
808 .status()
809 .expect("Failed to execute print command")
810 }),
811 )
812 .await;
813
814 match result {
815 Ok(Ok(status)) => {
816 if !status.success() {
817 error!("º command failed: {:?}", status);
818 return Err("Print command failed".into());
819 }
820 }
821 Ok(Err(e)) => {
822 error!("Task spawn failed: {:?}", e);
823 return Err("Task spawn failed".into());
824 }
825 Err(_) => {
826 error!("Print command timed out");
827 return Err("Print command timed out".into());
828 }
829 }
830 }
831
832 info!("Print job sent successfully");
833 Ok(())
834 }
835
836 pub async fn _find_printer(&self, printer_name: &str) -> Option<Printer> {
845 self.printers
846 .lock()
847 .await
848 .iter()
849 .find(|p| p.name == printer_name)
850 .cloned()
851 }
852
853
854
855 async fn list_printers() -> Vec<Printer> {
860 #[cfg(target_os = "linux")]
861 {
862 printers::get_printers()
863 }
864
865 #[cfg(target_os = "windows")]
866 {
867 printers::get_printers()
868 }
869
870 #[cfg(target_os = "macos")]
871 {
872 printers::get_printers()
873 }
874
875 #[cfg(target_os = "ios")]
876 {
877 Vec::new()
879 }
880 }
881
882 async fn process_action(&mut self, action: Value, write: WebSocketWrite) -> (i32, String) {
891 let action_type = match action.get("ACTION").and_then(Value::as_str) {
892 Some(action_str) => action_str,
893 None => {
894 error!("Missing or invalid ACTION field");
895 return (1, "Missing ACTION field".to_string());
896 }
897 };
898
899 if action_type == "getPrinters" {
900 info!("Processing getPrinters action");
901 let printers = self.get_print_list().await;
902
903 let uuid = match action.get("UUIDV4") {
904 Some(Value::String(uuid)) => uuid,
905 _ => {
906 error!("Missing or invalid MESSAGE_UUID field");
907 return (1, "Missing MESSAGE_UUID field".to_string());
908 }
909 };
910 let response = json!({
911 "SERVICE_NAME": "PRINT",
912 "SERVICE_VERS": PRINT_VERSION,
913 "MESSAGE_TYPE": "RESPONSE",
914 "MESSAGE_EXEC": "SUCCESS",
915 "MESSAGE_UUID": uuid,
916 "MESSAGE_DATA": printers,
917 }).to_string();
918
919 send_message(&write, response).await;
920 return (0, "Printers list sent".to_string());
921 }
922
923 let action_map = match action.get("ACTION") {
924 Some(Value::String(action_str)) => {
925 match serde_json::from_str::<serde_json::Map<String, Value>>(action_str) {
926 Ok(map) => map,
927 Err(e) => {
928 error!("Failed to parse ACTION as JSON object: {}", e);
929 return (1, format!("Invalid ACTION format: {}", e));
930 }
931 }
932 }
933 _ => {
934 error!("Missing or invalid ACTION field");
935 return (1, "Missing ACTION field".to_string());
936 }
937 };
938
939 for (device_name, device_actions) in action_map {
940 info!("Processing actions for device: {}", device_name);
941
942 if device_actions.is_null() {
943 return (0, "Device is reachable".to_string());
944 }
945
946 match &device_actions {
948 Value::Object(actions) => {
949 debug!("Device actions is object: {:?}", actions);
950
951 if let Some(Value::String(print_content)) = actions.get("print") {
953 info!("Found print content, length: {}", print_content.len());
954
955 if let Some(print_action) = self.parse_print_action(&device_name, &actions) {
956 let result = self.run_action(print_action).await;
957 if result.0 != 0 {
958 return result;
959 }
960 }
961 } else {
962 if let Some(open_action) = self.parse_open_action(&device_name, &actions) {
964 info!("open_action {:?}", open_action);
965 let result = self.run_action(open_action).await;
966 if result.0 != 0 {
967 return result;
968 }
969 }
970 }
971 }
972 Value::String(content) => {
973 info!("Device actions is string (assuming base64), length: {}", content.len());
975
976 let print_action = PrintAction::Print {
977 content: content.clone(),
978 printer_name: Some(device_name.clone()),
979 copies: Some(1),
980 open: false,
981 };
982
983 let result = self.run_action(print_action).await;
984 if result.0 != 0 {
985 return result;
986 }
987 }
988 _ => {
989 let error_msg = format!("Actions for '{}' must be a JSON object or string, got: {:?}",
990 device_name, device_actions);
991 error!("{}", error_msg);
992 return (1, error_msg);
993 }
994 }
995 }
996
997 (0, "All actions processed successfully".to_string())
998 }
999
1000 fn parse_print_action(
1010 &self,
1011 device: &str,
1012 actions: &serde_json::Map<String, Value>,
1013 ) -> Option<PrintAction> {
1014 if let Some(print_content) = actions.get("print") {
1015 let content = match print_content {
1016 Value::String(s) => {
1017 info!("Print content for device {}: {} chars", device, s.len());
1018 s.clone()
1019 }
1020 _ => {
1021 error!("Print content for device {} is not a string: {:?}", device, print_content);
1022 return None;
1023 }
1024 };
1025
1026 let copies = actions
1027 .get("copies")
1028 .and_then(Value::as_u64)
1029 .map(|c| c as u32)
1030 .unwrap_or(1);
1031
1032 let open = actions
1033 .get("open")
1034 .and_then(Value::as_bool)
1035 .unwrap_or(false);
1036
1037 info!("Parsed print action - device: {}, copies: {}, open: {}", device, copies, open);
1038
1039 Some(PrintAction::Print {
1040 content,
1041 printer_name: Some(device.to_string()),
1042 copies: Some(copies),
1043 open,
1044 })
1045 } else {
1046 debug!("No 'print' field found in actions for device: {}", device);
1047 None
1048 }
1049 }
1050
1051 fn parse_open_action(
1061 &self,
1062 device: &str,
1063 actions: &serde_json::Map<String, Value>,
1064 ) -> Option<PrintAction> {
1065 if let Some(open_drawer) = actions.get("open") {
1066 if open_drawer.as_bool().unwrap_or(false) {
1067 return Some(PrintAction::OpenDrawer {
1068 printer_name: device.to_string(),
1069 });
1070 }
1071 }
1072 None
1073 }
1074}
1075
1076#[async_trait]
1077impl Service for PrintService {
1078 async fn run(&self, action: Value, _write: WebSocketWrite) -> (i32, String) {
1091 let deserialized_action = if action.is_string() {
1092 match serde_json::from_str::<Value>(action.as_str().unwrap_or("")) {
1093 Ok(val) => val,
1094 Err(err) => {
1095 error!("Failed to parse string JSON: {}", err);
1096 return (1, format!("Invalid action format: {}", err));
1097 }
1098 }
1099 } else {
1100 action
1101 };
1102
1103 let result: Arc<Mutex<Option<(i32, String)>>> = Arc::new(Mutex::new(None)); let result_clone = Arc::clone(&result);
1105 let mut self_clone = self.clone();
1106
1107 tokio::spawn(async move {
1109 let process_result = self_clone.process_action(deserialized_action, _write).await;
1110 let mut lock = result_clone.lock().await;
1111 *lock = Some(process_result);
1112 });
1113
1114 let start = tokio::time::Instant::now();
1116 while start.elapsed() < Duration::from_secs(2) {
1117 {
1118 let lock = result.lock().await;
1119 if let Some((code, msg)) = &*lock {
1120 return (*code, msg.clone()); }
1122 }
1123 sleep(Duration::from_millis(100)).await; }
1125
1126 (0, "Action is being processed asynchronously".to_string())
1128 }
1129
1130 fn as_any(&self) -> &dyn std::any::Any {
1136 self
1137 }
1138
1139 fn stop_service(&self) {
1141 info!("Stopping PrintService...");
1142 }
1143
1144 fn get_version(&self) -> String {
1150 PRINT_VERSION.to_string()
1151 }
1152}