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