Skip to main content

ghpascon_rust/devices/
device_manager.rs

1//! `DeviceManager` — loads and manages multiple RFID and generic devices.
2//!
3//! Each device is configured via a `.json` file in a directory.
4//! The `"reader"` field determines the type.
5//! All other fields are optional — defaults are applied automatically.
6
7use serde_json::Value;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::{Arc, Mutex};
11use tokio::task::JoinHandle;
12
13use super::generic::serial::SerialDevice;
14use super::generic::tcp::TcpDevice;
15use super::printer::sato::{SatoPrinter, SatoWs4Printer};
16use super::rfid::r700::R700;
17use super::rfid::x714::X714;
18
19pub type EventHandler = dyn FnMut(&str, &str, Option<Value>) + Send + 'static;
20pub type SharedEventHandler = Arc<Mutex<Box<EventHandler>>>;
21
22static CONFIG_EXAMPLES: &[(&str, fn() -> HashMap<String, Value>)] = &[
23    ("X714_DEFAULT", || {
24        super::rfid::x714::config_example::x714_default_map()
25    }),
26    ("X714_SERIAL", || {
27        super::rfid::x714::config_example::x714_map()
28    }),
29    ("X714_TCP", || {
30        super::rfid::x714::config_example::x714_tcp_map()
31    }),
32    ("X714_BLE", || {
33        super::rfid::x714::config_example::x714_ble_map()
34    }),
35    ("X714_ALL", || {
36        super::rfid::x714::config_example::x714_all_map()
37    }),
38    ("R700_IOT", || {
39        super::rfid::r700::config_example::r700_iot_map()
40    }),
41    ("R700_IOT_DICT", || {
42        super::rfid::r700::config_example::r700_iot_dict_map()
43    }),
44    ("R700_IOT_GPI", || {
45        super::rfid::r700::config_example::r700_iot_gpi_map()
46    }),
47    ("R700_PROTECTED_INVENTORY", || {
48        super::rfid::r700::config_example::r700_protected_inventory_map()
49    }),
50    ("SERIAL", || {
51        super::generic::serial::config_example::serial_default_map()
52    }),
53    ("SERIAL_CUSTOM", || {
54        super::generic::serial::config_example::serial_custom_map()
55    }),
56    ("TCP", || {
57        super::generic::tcp::config_example::tcp_default_map()
58    }),
59    ("TCP_CUSTOM", || {
60        super::generic::tcp::config_example::tcp_custom_map()
61    }),
62    ("SATO", || {
63        super::printer::sato::config_example::sato_default_map()
64    }),
65    ("SATO_WS4", || {
66        super::printer::sato::config_example::sato_ws4_map()
67    }),
68];
69
70pub enum Device {
71    X714(X714),
72    R700(R700),
73    Serial(SerialDevice),
74    Tcp(TcpDevice),
75    Sato(SatoPrinter),
76    SatoWs4(SatoWs4Printer),
77}
78
79impl Clone for Device {
80    fn clone(&self) -> Self {
81        match self {
82            Self::X714(d) => Self::X714(d.clone()),
83            Self::R700(d) => Self::R700(d.clone()),
84            Self::Serial(d) => Self::Serial(d.clone()),
85            Self::Tcp(d) => Self::Tcp(d.clone()),
86            Self::Sato(d) => Self::Sato(d.clone()),
87            Self::SatoWs4(d) => Self::SatoWs4(d.clone()),
88        }
89    }
90}
91
92impl Device {
93    pub fn name(&self) -> &str {
94        match self {
95            Self::X714(d) => &d.config.name,
96            Self::R700(d) => &d.config.name,
97            Self::Serial(d) => &d.config.name,
98            Self::Tcp(d) => &d.config.name,
99            Self::Sato(d) => &d.config.name,
100            Self::SatoWs4(d) => &d.config.name,
101        }
102    }
103
104    pub fn device_type(&self) -> &'static str {
105        match self {
106            Self::X714(_) => "X714",
107            Self::R700(_) => "R700_IOT",
108            Self::Serial(_) => "SERIAL",
109            Self::Tcp(_) => "TCP",
110            Self::Sato(_) => "SATO",
111            Self::SatoWs4(_) => "SATO_WS4",
112        }
113    }
114
115    pub fn device_class(&self) -> &'static str {
116        match self {
117            Self::X714(_) => "X714",
118            Self::R700(_) => "R700",
119            Self::Serial(_) => "SerialDevice",
120            Self::Tcp(_) => "TcpDevice",
121            Self::Sato(_) => "SatoPrinter",
122            Self::SatoWs4(_) => "SatoWs4Printer",
123        }
124    }
125
126    pub fn is_connected(&self) -> bool {
127        match self {
128            Self::X714(d) => d.is_connected(),
129            Self::R700(d) => d.is_connected(),
130            Self::Serial(d) => d.is_connected(),
131            Self::Tcp(d) => d.is_connected(),
132            Self::Sato(d) => d.is_connected(),
133            Self::SatoWs4(d) => d.is_connected(),
134        }
135    }
136
137    pub fn is_reading(&self) -> bool {
138        match self {
139            Self::X714(d) => d.is_reading(),
140            Self::R700(d) => d.is_reading(),
141            Self::Serial(_) => false,
142            Self::Tcp(_) => false,
143            Self::Sato(_) => false,
144            Self::SatoWs4(_) => false,
145        }
146    }
147
148    pub fn is_gpi_trigger_on(&self) -> bool {
149        match self {
150            Self::X714(d) => d.config.gpi_start,
151            Self::R700(d) => d.config.gpi_start,
152            Self::Serial(_) | Self::Tcp(_) | Self::Sato(_) | Self::SatoWs4(_) => false,
153        }
154    }
155
156    pub fn serial_number(&self) -> Option<String> {
157        match self {
158            Self::X714(d) => d.serial_number(),
159            Self::R700(d) => d.serial_number(),
160            Self::Serial(_) => None,
161            Self::Tcp(_) => None,
162            Self::Sato(_) => None,
163            Self::SatoWs4(_) => None,
164        }
165    }
166
167    pub fn can_print(&self) -> bool {
168        match self {
169            Self::Sato(d) => d.can_print(),
170            Self::SatoWs4(d) => d.can_print(),
171            _ => false,
172        }
173    }
174
175    pub fn pending_print_jobs(&self) -> usize {
176        match self {
177            Self::Sato(d) => d.pending_print_jobs(),
178            Self::SatoWs4(d) => d.pending_print_jobs(),
179            _ => 0,
180        }
181    }
182
183    pub fn to_map(&self) -> HashMap<String, Value> {
184        match self {
185            Self::X714(d) => d.to_map(),
186            Self::R700(d) => d.to_map(),
187            Self::Serial(d) => d.to_map(),
188            Self::Tcp(d) => d.to_map(),
189            Self::Sato(d) => d.to_map(),
190            Self::SatoWs4(d) => d.to_map(),
191        }
192    }
193
194    pub fn connect_instruction(&self) -> String {
195        match self {
196            Self::X714(d) => d.connect_instruction(),
197            Self::R700(d) => d.connect_instruction(),
198            Self::Serial(d) => d.connect_instruction(),
199            Self::Tcp(d) => d.connect_instruction(),
200            Self::Sato(d) => d.connect_instruction(),
201            Self::SatoWs4(d) => d.connect_instruction(),
202        }
203    }
204
205    pub fn set_event_handler(&mut self, handler: SharedEventHandler) {
206        match self {
207            Self::X714(d) => d.set_event_handler(handler),
208            Self::R700(d) => d.set_event_handler(handler),
209            Self::Serial(d) => d.set_event_handler(handler),
210            Self::Tcp(d) => d.set_event_handler(handler),
211            Self::Sato(d) => d.set_event_handler(handler),
212            Self::SatoWs4(d) => d.set_event_handler(handler),
213        }
214    }
215
216    pub async fn connect(&self) {
217        match self {
218            Self::X714(d) => d.connect().await,
219            Self::R700(d) => d.connect().await,
220            Self::Serial(d) => d.connect().await,
221            Self::Tcp(d) => d.connect().await,
222            Self::Sato(d) => d.connect().await,
223            Self::SatoWs4(d) => d.0.connect().await,
224        }
225    }
226
227    pub async fn close(&self) {
228        match self {
229            Self::X714(d) => d.close().await,
230            Self::R700(d) => d.close().await,
231            Self::Serial(d) => d.close().await,
232            Self::Tcp(d) => d.close().await,
233            Self::Sato(d) => d.close().await,
234            Self::SatoWs4(d) => d.0.close().await,
235        }
236    }
237
238    pub async fn start_inventory(&self) -> Result<(), String> {
239        match self {
240            Self::X714(d) => d.start_inventory().await,
241            Self::R700(d) => d.start_inventory().await,
242            _ => Err("device type does not support this operation".to_string()),
243        }
244    }
245
246    pub async fn stop_inventory(&self) -> Result<(), String> {
247        match self {
248            Self::X714(d) => d.stop_inventory().await,
249            Self::R700(d) => d.stop_inventory().await,
250            _ => Err("device type does not support this operation".to_string()),
251        }
252    }
253
254    pub async fn write_epc(
255        &self,
256        target_identifier: Option<&str>,
257        target_value: Option<&str>,
258        new_epc: &str,
259        password: &str,
260    ) -> Result<(), String> {
261        match self {
262            Self::X714(d) => {
263                d.write_epc(target_identifier, target_value, new_epc, password)
264                    .await
265            }
266            Self::R700(d) => {
267                d.write_epc(target_identifier, target_value, new_epc, password)
268                    .await
269            }
270            _ => Err("device type does not support this operation".to_string()),
271        }
272    }
273
274    pub async fn write_gpo(
275        &self,
276        pin: u8,
277        state: bool,
278        control: &str,
279        time_ms: u64,
280    ) -> Result<(), String> {
281        match self {
282            Self::X714(d) => d.write_gpo(pin, state, control, time_ms).await,
283            Self::R700(d) => d.write_gpo(pin, state, control, time_ms as u32).await,
284            _ => Err("device type does not support this operation".to_string()),
285        }
286    }
287}
288
289/// Snapshot of device state (does not hold a reference to the device).
290#[derive(Debug, Clone, serde::Serialize)]
291pub struct DeviceInfo {
292    pub name: String,
293    pub device_type: String,
294    pub device_class: String,
295    pub is_connected: bool,
296    pub is_reading: bool,
297    pub is_gpi_trigger_on: bool,
298    pub can_print: bool,
299    pub to_print: usize,
300    pub has_serial_number: bool,
301    pub serial_number: String,
302    pub connect_instruction: String,
303    pub current_config: HashMap<String, Value>,
304}
305
306/// Manages multiple devices: loads JSON configs, connects, dispatches events.
307pub struct DeviceManager {
308    pub devices: Vec<Device>,
309    devices_path: PathBuf,
310    event_handler: Option<SharedEventHandler>,
311    connect_tasks: Vec<JoinHandle<()>>,
312}
313
314impl DeviceManager {
315    pub fn new<P: AsRef<Path>>(devices_path: P) -> Self {
316        Self {
317            devices: Vec::new(),
318            devices_path: devices_path.as_ref().to_path_buf(),
319            event_handler: None,
320            connect_tasks: Vec::new(),
321        }
322    }
323
324    pub fn with_event_handler(mut self, handler: SharedEventHandler) -> Self {
325        self.event_handler = Some(handler);
326        self
327    }
328
329    pub fn set_event_handler(&mut self, handler: SharedEventHandler) {
330        self.event_handler = Some(handler);
331    }
332
333    pub fn load_devices(&mut self) {
334        self.devices.clear();
335
336        if !self.devices_path.exists() {
337            match std::fs::create_dir_all(&self.devices_path) {
338                Ok(_) => eprintln!("📁 Directory created: {}", self.devices_path.display()),
339                Err(e) => {
340                    eprintln!(
341                        "❌ Could not create directory '{}': {e}",
342                        self.devices_path.display()
343                    );
344                    return;
345                }
346            }
347        }
348
349        let entries = match std::fs::read_dir(&self.devices_path) {
350            Ok(e) => e,
351            Err(e) => {
352                eprintln!("❌ Error listing '{}': {e}", self.devices_path.display());
353                return;
354            }
355        };
356
357        for entry in entries.flatten() {
358            let path = entry.path();
359            if path.extension().and_then(|e| e.to_str()) != Some("json") {
360                continue;
361            }
362
363            let filename = path
364                .file_name()
365                .unwrap_or_default()
366                .to_string_lossy()
367                .to_string();
368            let name = filename.trim_end_matches(".json").to_string();
369
370            eprintln!("📄 Reading '{}'â€Ļ", filename);
371
372            let content = match std::fs::read_to_string(&path) {
373                Ok(s) => s,
374                Err(e) => {
375                    eprintln!("❌ Error reading '{}': {e}", filename);
376                    continue;
377                }
378            };
379
380            let raw: HashMap<String, Value> = match serde_json::from_str(&content) {
381                Ok(d) => d,
382                Err(e) => {
383                    eprintln!("❌ Invalid JSON in '{}': {e}", filename);
384                    continue;
385                }
386            };
387
388            let data: HashMap<String, Value> = raw
389                .into_iter()
390                .map(|(k, v)| (k.to_lowercase(), v))
391                .collect();
392
393            let reader_type = match data.get("reader").and_then(|v| v.as_str()) {
394                Some(t) => t.to_string(),
395                None => {
396                    eprintln!("âš ī¸  '{}' has no 'reader' field — skipped", filename);
397                    continue;
398                }
399            };
400
401            self.add_device(&name, &reader_type, data);
402        }
403
404        self.assign_event_handler();
405        eprintln!("✅ {} device(s) loaded", self.devices.len());
406    }
407
408    pub fn add_device(&mut self, name: &str, device_type: &str, mut data: HashMap<String, Value>) {
409        data.insert("name".to_string(), Value::String(name.to_string()));
410
411        match device_type.to_uppercase().as_str() {
412            "X714" => match X714::from_map(data) {
413                Ok(d) => {
414                    eprintln!("  ✅ X714 '{}' → {}", name, d.connect_instruction());
415                    self.devices.push(Device::X714(d));
416                }
417                Err(e) => eprintln!("  ❌ X714 '{}' config error: {e}", name),
418            },
419            "R700_IOT" | "R700" => match R700::from_map(data) {
420                Ok(d) => {
421                    eprintln!("  ✅ R700 '{}' → {}", name, d.connect_instruction());
422                    self.devices.push(Device::R700(d));
423                }
424                Err(e) => eprintln!("  ❌ R700 '{}' config error: {e}", name),
425            },
426            "SERIAL" => {
427                let d = SerialDevice::from_map(data);
428                eprintln!("  ✅ SERIAL '{}' → {}", name, d.connect_instruction());
429                self.devices.push(Device::Serial(d));
430            }
431            "TCP" => {
432                let d = TcpDevice::from_map(data);
433                eprintln!("  ✅ TCP '{}' → {}", name, d.connect_instruction());
434                self.devices.push(Device::Tcp(d));
435            }
436            "SATO" => {
437                let d = SatoPrinter::from_map(data);
438                eprintln!("  ✅ SATO '{}' → {}", name, d.connect_instruction());
439                self.devices.push(Device::Sato(d));
440            }
441            "SATO_WS4" => {
442                let d = SatoWs4Printer::from_map(data);
443                eprintln!("  ✅ SATO_WS4 '{}' → {}", name, d.connect_instruction());
444                self.devices.push(Device::SatoWs4(d));
445            }
446            other => eprintln!("  âš ī¸  Unknown type '{}' for '{}' — skipped", other, name),
447        }
448    }
449
450    pub fn assign_event_handler(&mut self) {
451        let Some(handler) = &self.event_handler else {
452            return;
453        };
454        for device in &mut self.devices {
455            device.set_event_handler(Arc::clone(handler));
456        }
457    }
458
459    pub async fn connect_devices(&mut self, force: bool) {
460        let active = self
461            .connect_tasks
462            .iter()
463            .filter(|t| !t.is_finished())
464            .count();
465
466        if active > 0 && !force {
467            eprintln!(
468                "â„šī¸  {} active connection task(s) — use force=true to restart",
469                active
470            );
471            return;
472        }
473
474        self.cancel_connect_tasks().await;
475        self.disconnect_devices().await;
476        self.load_devices();
477
478        let mut tasks = Vec::new();
479        for device in &self.devices {
480            let d = device.clone();
481            let name_display = d.connect_instruction();
482            eprintln!("🚀 Connecting '{}'â€Ļ  ({})", d.name(), name_display);
483            let handle = tokio::spawn(async move { d.connect().await });
484            tasks.push(handle);
485        }
486
487        eprintln!("â„šī¸  {} connection task(s) started", tasks.len());
488        self.connect_tasks = tasks;
489    }
490
491    pub async fn cancel_connect_tasks(&mut self) {
492        let n = self.connect_tasks.len();
493        for task in self.connect_tasks.drain(..) {
494            task.abort();
495        }
496        if n > 0 {
497            eprintln!("🛑 {} task(s) cancelled", n);
498        }
499    }
500
501    pub async fn disconnect_devices(&mut self) {
502        for device in &self.devices {
503            device.close().await;
504        }
505        self.devices.clear();
506    }
507
508    pub fn len(&self) -> usize {
509        self.devices.len()
510    }
511    pub fn is_empty(&self) -> bool {
512        self.devices.is_empty()
513    }
514
515    pub fn get_device_names(&self) -> Vec<String> {
516        self.devices.iter().map(|d| d.name().to_string()).collect()
517    }
518
519    pub fn get_device(&self, name: &str) -> Option<&Device> {
520        self.devices.iter().find(|d| d.name() == name)
521    }
522
523    pub fn get_device_mut(&mut self, name: &str) -> Option<&mut Device> {
524        self.devices.iter_mut().find(|d| d.name() == name)
525    }
526
527    pub fn get_device_info(&self, name: Option<&str>) -> Vec<DeviceInfo> {
528        match name {
529            Some(n) => self
530                .get_device(n)
531                .map(|d| vec![Self::build_info(d)])
532                .unwrap_or_default(),
533            None => self.devices.iter().map(Self::build_info).collect(),
534        }
535    }
536
537    fn build_info(d: &Device) -> DeviceInfo {
538        let serial_number = d.serial_number();
539        let has_serial_number = d.is_connected() && serial_number.is_some();
540        DeviceInfo {
541            name: d.name().to_string(),
542            device_type: d.device_type().to_string(),
543            device_class: d.device_class().to_string(),
544            is_connected: d.is_connected(),
545            is_reading: d.is_connected() && d.is_reading(),
546            is_gpi_trigger_on: d.is_gpi_trigger_on(),
547            can_print: d.can_print(),
548            to_print: d.pending_print_jobs(),
549            has_serial_number,
550            serial_number: serial_number.unwrap_or_else(|| "Unknown".to_string()),
551            connect_instruction: d.connect_instruction(),
552            current_config: d.to_map(),
553        }
554    }
555
556    pub fn any_device_reading(&self) -> bool {
557        self.devices
558            .iter()
559            .any(|d| d.is_connected() && d.is_reading())
560    }
561
562    pub fn get_serial_number(&self, name: &str) -> Option<String> {
563        let d = self.get_device(name)?;
564        if !d.is_connected() {
565            return None;
566        }
567        d.serial_number()
568    }
569
570    pub fn get_device_config(&self, name: &str) -> Option<HashMap<String, Value>> {
571        self.get_device(name).map(Device::to_map)
572    }
573
574    pub fn get_device_configs(&self) -> HashMap<String, HashMap<String, Value>> {
575        self.devices
576            .iter()
577            .map(|d| (d.name().to_string(), d.to_map()))
578            .collect()
579    }
580
581    pub async fn start_inventory(&self, name: &str) -> Result<(), String> {
582        let d = self
583            .get_device(name)
584            .ok_or_else(|| format!("device '{}' not found", name))?;
585        if !d.is_connected() {
586            return Err(format!("device '{}' is not connected", name));
587        }
588        d.start_inventory().await.map_err(|e| {
589            eprintln!("❌ start_inventory '{}': {e}", name);
590            e
591        })
592    }
593
594    pub async fn stop_inventory(&self, name: &str) -> Result<(), String> {
595        let d = self
596            .get_device(name)
597            .ok_or_else(|| format!("device '{}' not found", name))?;
598        if !d.is_connected() {
599            return Err(format!("device '{}' is not connected", name));
600        }
601        d.stop_inventory().await.map_err(|e| {
602            eprintln!("❌ stop_inventory '{}': {e}", name);
603            e
604        })
605    }
606
607    pub async fn start_inventory_all(&self) -> HashMap<String, bool> {
608        let mut results = HashMap::new();
609        for d in &self.devices {
610            if d.is_connected() {
611                let ok = d.start_inventory().await.is_ok();
612                results.insert(d.name().to_string(), ok);
613            }
614        }
615        results
616    }
617
618    pub async fn stop_inventory_all(&self) -> HashMap<String, bool> {
619        let mut results = HashMap::new();
620        for d in &self.devices {
621            if d.is_connected() {
622                let ok = d.stop_inventory().await.is_ok();
623                results.insert(d.name().to_string(), ok);
624            }
625        }
626        results
627    }
628
629    pub async fn write_epc(
630        &self,
631        name: &str,
632        target_identifier: Option<&str>,
633        target_value: Option<&str>,
634        new_epc: &str,
635        password: &str,
636    ) -> Result<(), String> {
637        let d = self
638            .get_device(name)
639            .ok_or_else(|| format!("device '{}' not found", name))?;
640        if !d.is_connected() {
641            return Err(format!("device '{}' is not connected", name));
642        }
643        d.write_epc(target_identifier, target_value, new_epc, password)
644            .await
645    }
646
647    pub async fn write_gpo(
648        &self,
649        name: &str,
650        pin: u8,
651        state: bool,
652        control: &str,
653        time_ms: u64,
654    ) -> Result<(), String> {
655        let d = self
656            .get_device(name)
657            .ok_or_else(|| format!("device '{}' not found", name))?;
658        if !d.is_connected() {
659            return Err(format!("device '{}' is not connected", name));
660        }
661        d.write_gpo(pin, state, control, time_ms).await
662    }
663
664    pub fn get_config_examples() -> Vec<&'static str> {
665        CONFIG_EXAMPLES.iter().map(|(name, _)| *name).collect()
666    }
667
668    pub fn get_config_example(name: &str) -> Option<HashMap<String, Value>> {
669        CONFIG_EXAMPLES
670            .iter()
671            .find(|(n, _)| n.eq_ignore_ascii_case(name))
672            .map(|(_, f)| f())
673    }
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679    use serde_json::json;
680
681    #[test]
682    fn device_info_includes_runtime_and_config_fields() {
683        let mut manager = DeviceManager::new("/tmp/unused");
684        manager.add_device(
685            "dock-reader",
686            "X714",
687            HashMap::from([
688                ("reader".to_string(), Value::String("X714".to_string())),
689                (
690                    "connection_type".to_string(),
691                    Value::String("TCP".to_string()),
692                ),
693                ("ip".to_string(), Value::String("192.168.1.50".to_string())),
694                ("tcp_port".to_string(), json!(23)),
695                ("gpi_start".to_string(), Value::Bool(true)),
696            ]),
697        );
698
699        let info = manager
700            .get_device_info(Some("dock-reader"))
701            .into_iter()
702            .next()
703            .expect("device info");
704
705        assert_eq!(info.name, "dock-reader");
706        assert_eq!(info.device_type, "X714");
707        assert_eq!(info.device_class, "X714");
708        assert!(!info.is_connected);
709        assert!(!info.is_reading);
710        assert!(info.is_gpi_trigger_on);
711        assert!(!info.can_print);
712        assert_eq!(info.to_print, 0);
713        assert!(!info.has_serial_number);
714        assert_eq!(info.serial_number, "Unknown");
715        assert_eq!(
716            info.current_config
717                .get("connection_type")
718                .and_then(Value::as_str),
719            Some("TCP")
720        );
721    }
722
723    #[test]
724    fn config_examples_list_only_supported_devices() {
725        let examples = DeviceManager::get_config_examples();
726        assert!(examples.contains(&"X714_DEFAULT"));
727        assert_eq!(examples.len(), 15);
728    }
729}