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