1use crate::error::{PoKeysError, Result};
9use log::{error, info, warn};
10use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, RwLock};
16use std::time::Duration;
17
18pub const DEFAULT_MODEL_DIR: &str = ".config/pokeys/models";
20
21pub const DEFAULT_RETRY_INTERVAL: u64 = 10;
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct PinModel {
27 pub capabilities: Vec<String>,
29
30 #[serde(default = "default_active")]
32 pub active: bool,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct DeviceModel {
38 pub name: String,
40
41 pub pins: HashMap<u8, PinModel>,
43}
44
45fn default_active() -> bool {
47 true
48}
49
50impl DeviceModel {
51 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
61 let content = fs::read_to_string(path.as_ref()).map_err(|e| {
62 PoKeysError::ModelLoadError(path.as_ref().to_string_lossy().to_string(), e.to_string())
63 })?;
64
65 let model: DeviceModel = serde_yaml::from_str(&content).map_err(|e| {
66 PoKeysError::ModelParseError(path.as_ref().to_string_lossy().to_string(), e.to_string())
67 })?;
68
69 model.validate()?;
70
71 Ok(model)
72 }
73
74 pub fn validate(&self) -> Result<()> {
83 if self.name.is_empty() {
85 return Err(PoKeysError::ModelValidationError(
86 "Model name cannot be empty".to_string(),
87 ));
88 }
89
90 if self.pins.is_empty() {
92 return Err(PoKeysError::ModelValidationError(
93 "Model must define at least one pin".to_string(),
94 ));
95 }
96
97 for (pin_num, pin) in &self.pins {
99 if pin.capabilities.is_empty() {
100 return Err(PoKeysError::ModelValidationError(format!(
101 "Pin {} must have at least one capability",
102 pin_num
103 )));
104 }
105 }
106
107 self.validate_related_capabilities()?;
109
110 Ok(())
111 }
112
113 fn validate_related_capabilities(&self) -> Result<()> {
122 self.validate_encoder_pairs()?;
124
125 self.validate_matrix_keyboard()?;
127
128 self.validate_pwm_channels()?;
130
131 Ok(())
132 }
133
134 fn validate_encoder_pairs(&self) -> Result<()> {
142 let mut encoder_a_pins = HashMap::new();
144 for (pin_num, pin) in &self.pins {
145 for capability in &pin.capabilities {
146 if capability.starts_with("Encoder_") && capability.ends_with("A") {
147 let encoder_id = &capability[8..capability.len() - 1]; encoder_a_pins.insert(encoder_id.to_string(), *pin_num);
149 }
150 }
151 }
152
153 for (encoder_id, pin_a) in &encoder_a_pins {
155 let encoder_b_capability = format!("Encoder_{}B", encoder_id);
156 let mut found_b = false;
157
158 for (pin_num, pin) in &self.pins {
159 if *pin_num != *pin_a
160 && pin
161 .capabilities
162 .iter()
163 .any(|cap| cap == &encoder_b_capability)
164 {
165 found_b = true;
166 break;
167 }
168 }
169
170 if !found_b {
171 return Err(PoKeysError::ModelValidationError(format!(
172 "Encoder {}A on pin {} has no corresponding {}B pin",
173 encoder_id, pin_a, encoder_b_capability
174 )));
175 }
176 }
177
178 Ok(())
179 }
180
181 fn validate_matrix_keyboard(&self) -> Result<()> {
189 let mut has_rows = false;
190 let mut has_columns = false;
191
192 for pin in self.pins.values() {
193 for capability in &pin.capabilities {
194 if capability.starts_with("MatrixKeyboard_Row") {
195 has_rows = true;
196 } else if capability.starts_with("MatrixKeyboard_Col") {
197 has_columns = true;
198 }
199 }
200 }
201
202 if has_rows && !has_columns {
203 return Err(PoKeysError::ModelValidationError(
204 "Matrix keyboard has rows but no columns".to_string(),
205 ));
206 }
207
208 if !has_rows && has_columns {
209 return Err(PoKeysError::ModelValidationError(
210 "Matrix keyboard has columns but no rows".to_string(),
211 ));
212 }
213
214 Ok(())
215 }
216
217 fn validate_pwm_channels(&self) -> Result<()> {
225 let mut pwm_channels = Vec::new();
227
228 for (pin_num, pin) in &self.pins {
229 for capability in &pin.capabilities {
230 if let Some(stripped) = capability.strip_prefix("PWM_") {
231 if let Ok(channel) = stripped.parse::<u32>() {
232 pwm_channels.push((channel, *pin_num));
233 }
234 }
235 }
236 }
237
238 pwm_channels.sort_by_key(|(channel, _)| *channel);
240
241 for (i, (channel, pin)) in pwm_channels.iter().enumerate() {
243 if *channel != (i + 1) as u32 {
244 return Err(PoKeysError::ModelValidationError(format!(
245 "PWM channels are not sequential: expected channel {}, found channel {} on pin {}",
246 i + 1,
247 channel,
248 pin
249 )));
250 }
251 }
252
253 Ok(())
254 }
255
256 pub fn is_pin_capability_supported(&self, pin_num: u8, capability: &str) -> bool {
267 if let Some(pin) = self.pins.get(&pin_num) {
268 pin.capabilities.iter().any(|cap| cap == capability)
269 } else {
270 false
271 }
272 }
273
274 pub fn get_pin_capabilities(&self, pin_num: u8) -> Vec<String> {
284 if let Some(pin) = self.pins.get(&pin_num) {
285 pin.capabilities.clone()
286 } else {
287 Vec::new()
288 }
289 }
290
291 pub fn get_related_capabilities(&self, pin_num: u8, capability: &str) -> Vec<(String, u8)> {
305 let mut related = Vec::new();
306
307 if capability.starts_with("Encoder_") && capability.len() >= 10 {
309 let encoder_id = &capability[8..capability.len() - 1]; let role = &capability[capability.len() - 1..]; let related_role = if role == "A" { "B" } else { "A" };
313 let related_capability = format!("Encoder_{}{}", encoder_id, related_role);
314
315 for (other_pin, pin_model) in &self.pins {
317 if *other_pin != pin_num
318 && pin_model
319 .capabilities
320 .iter()
321 .any(|cap| cap == &related_capability)
322 {
323 related.push((related_capability, *other_pin));
324 break;
325 }
326 }
327 }
328
329 if capability.starts_with("MatrixKeyboard_Row") {
331 for (other_pin, pin_model) in &self.pins {
333 if *other_pin != pin_num {
334 for cap in &pin_model.capabilities {
335 if cap.starts_with("MatrixKeyboard_Col") {
336 related.push((cap.clone(), *other_pin));
337 }
338 }
339 }
340 }
341 }
342
343 if capability.starts_with("MatrixKeyboard_Col") {
344 for (other_pin, pin_model) in &self.pins {
346 if *other_pin != pin_num {
347 for cap in &pin_model.capabilities {
348 if cap.starts_with("MatrixKeyboard_Row") {
349 related.push((cap.clone(), *other_pin));
350 }
351 }
352 }
353 }
354 }
355
356 related
357 }
358
359 pub fn validate_pin_capability(&self, pin_num: u8, capability: &str) -> Result<()> {
373 if !self.is_pin_capability_supported(pin_num, capability) {
375 return Err(PoKeysError::UnsupportedPinCapability(
376 pin_num,
377 capability.to_string(),
378 ));
379 }
380
381 let related = self.get_related_capabilities(pin_num, capability);
383
384 if capability.starts_with("Encoder_") && capability.ends_with("A") {
386 let encoder_id = &capability[8..capability.len() - 1]; let encoder_b_capability = format!("Encoder_{}B", encoder_id);
388
389 let mut found_b = false;
391 for (related_cap, related_pin) in &related {
392 if related_cap == &encoder_b_capability {
393 found_b = true;
394
395 if let Some(pin_model) = self.pins.get(related_pin) {
397 if !pin_model.active {
398 return Err(PoKeysError::RelatedPinInactive(
399 *related_pin,
400 related_cap.clone(),
401 ));
402 }
403 }
404
405 break;
406 }
407 }
408
409 if !found_b {
410 return Err(PoKeysError::MissingRelatedCapability(
411 pin_num,
412 capability.to_string(),
413 encoder_b_capability,
414 ));
415 }
416 }
417
418 if capability.starts_with("Encoder_") && capability.ends_with("B") {
420 let encoder_id = &capability[8..capability.len() - 1]; let encoder_a_capability = format!("Encoder_{}A", encoder_id);
422
423 let mut found_a = false;
425 for (related_cap, related_pin) in &related {
426 if related_cap == &encoder_a_capability {
427 found_a = true;
428
429 if let Some(pin_model) = self.pins.get(related_pin) {
431 if !pin_model.active {
432 return Err(PoKeysError::RelatedPinInactive(
433 *related_pin,
434 related_cap.clone(),
435 ));
436 }
437 }
438
439 break;
440 }
441 }
442
443 if !found_a {
444 return Err(PoKeysError::MissingRelatedCapability(
445 pin_num,
446 capability.to_string(),
447 encoder_a_capability,
448 ));
449 }
450 }
451
452 if capability.starts_with("MatrixKeyboard_Row") {
454 let mut found_col = false;
455 for pin in self.pins.values() {
456 if pin.active
457 && pin
458 .capabilities
459 .iter()
460 .any(|cap| cap.starts_with("MatrixKeyboard_Col"))
461 {
462 found_col = true;
463 break;
464 }
465 }
466
467 if !found_col {
468 return Err(PoKeysError::MissingRelatedCapability(
469 pin_num,
470 capability.to_string(),
471 "MatrixKeyboard_Col".to_string(),
472 ));
473 }
474 }
475
476 if capability.starts_with("MatrixKeyboard_Col") {
478 let mut found_row = false;
479 for pin in self.pins.values() {
480 if pin.active
481 && pin
482 .capabilities
483 .iter()
484 .any(|cap| cap.starts_with("MatrixKeyboard_Row"))
485 {
486 found_row = true;
487 break;
488 }
489 }
490
491 if !found_row {
492 return Err(PoKeysError::MissingRelatedCapability(
493 pin_num,
494 capability.to_string(),
495 "MatrixKeyboard_Row".to_string(),
496 ));
497 }
498 }
499
500 Ok(())
501 }
502
503 pub fn validate_led_matrix_config(
513 &self,
514 config: &crate::matrix::LedMatrixConfig,
515 ) -> Result<()> {
516 if config.matrix_id < 1 || config.matrix_id > 2 {
518 return Err(PoKeysError::ModelValidationError(format!(
519 "Invalid matrix ID: {}. Must be 1 or 2",
520 config.matrix_id
521 )));
522 }
523
524 let pins = match config.matrix_id {
526 1 => crate::matrix::LED_MATRIX_1_PINS,
527 2 => crate::matrix::LED_MATRIX_2_PINS,
528 _ => {
529 return Err(PoKeysError::ModelValidationError(format!(
530 "Invalid matrix ID: {}",
531 config.matrix_id
532 )));
533 }
534 };
535
536 for &pin in &pins {
538 if !self.is_pin_capability_supported(pin, "DigitalOutput") {
539 return Err(PoKeysError::ModelValidationError(format!(
540 "Pin {} does not support DigitalOutput capability required for LED matrix {}",
541 pin, config.matrix_id
542 )));
543 }
544 }
545
546 Ok(())
547 }
548
549 pub fn reserve_led_matrix_pins(&mut self, matrix_id: u8) -> Result<()> {
559 let pins = match matrix_id {
561 1 => crate::matrix::LED_MATRIX_1_PINS,
562 2 => crate::matrix::LED_MATRIX_2_PINS,
563 _ => {
564 return Err(PoKeysError::ModelValidationError(format!(
565 "Invalid matrix ID: {}",
566 matrix_id
567 )));
568 }
569 };
570
571 for &pin in &pins {
574 if !self.is_pin_capability_supported(pin, "DigitalOutput") {
575 return Err(PoKeysError::ModelValidationError(format!(
576 "Cannot reserve pin {} for LED matrix {}: pin does not support DigitalOutput",
577 pin, matrix_id
578 )));
579 }
580 }
581
582 Ok(())
583 }
584}
585
586pub fn get_default_model_dir() -> PathBuf {
595 let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
596 path.push(DEFAULT_MODEL_DIR);
597 path
598}
599
600pub fn get_model_path(device_name: &str, model_dir: Option<&Path>) -> PathBuf {
611 let dir = model_dir
612 .map(Path::to_path_buf)
613 .unwrap_or_else(get_default_model_dir);
614 dir.join(format!("{}.yaml", device_name))
615}
616
617pub fn load_model(device_name: &str, model_dir: Option<&Path>) -> Result<DeviceModel> {
628 let path = get_model_path(device_name, model_dir);
629 DeviceModel::from_file(path)
630}
631
632pub struct ModelMonitor {
634 watcher: Option<RecommendedWatcher>,
636
637 watch_dir: PathBuf,
639
640 models: Arc<RwLock<HashMap<String, DeviceModel>>>,
642
643 callback: Arc<dyn Fn(String, DeviceModel) + Send + Sync + 'static>,
645
646 running: bool,
648}
649
650impl ModelMonitor {
651 pub fn new<F>(model_dir: PathBuf, callback: F) -> Self
662 where
663 F: Fn(String, DeviceModel) + Send + Sync + 'static,
664 {
665 Self {
666 watcher: None,
667 watch_dir: model_dir,
668 models: Arc::new(RwLock::new(HashMap::new())),
669 callback: Arc::new(callback),
670 running: false,
671 }
672 }
673
674 pub fn start(&mut self) -> Result<()> {
680 if self.running {
681 return Ok(());
682 }
683
684 if !self.watch_dir.exists() {
686 fs::create_dir_all(&self.watch_dir).map_err(|e| {
687 PoKeysError::ModelDirCreateError(
688 self.watch_dir.to_string_lossy().to_string(),
689 e.to_string(),
690 )
691 })?;
692 }
693
694 self.load_existing_models()?;
696
697 let (tx, rx) = std::sync::mpsc::channel();
699
700 let mut watcher = notify::recommended_watcher(tx)
702 .map_err(|e| PoKeysError::ModelWatcherError(e.to_string()))?;
703
704 watcher
706 .watch(&self.watch_dir, RecursiveMode::NonRecursive)
707 .map_err(|e| PoKeysError::ModelWatcherError(e.to_string()))?;
708
709 self.watcher = Some(watcher);
710 self.running = true;
711
712 let models = self.models.clone();
714 let callback = self.callback.clone();
715
716 std::thread::spawn(move || {
718 let mut debouncer = HashMap::new();
719
720 for res in rx {
721 match res {
722 Ok(event) => {
723 if let EventKind::Modify(_) | EventKind::Create(_) = event.kind {
724 for path in event.paths {
725 if path.extension().is_some_and(|ext| ext == "yaml") {
726 let now = std::time::Instant::now();
728 let path_str = path.to_string_lossy().to_string();
729
730 if debouncer.get(&path_str).is_none_or(|last| {
732 now.duration_since(*last) > Duration::from_millis(100)
733 }) {
734 debouncer.insert(path_str, now);
735
736 if let Some(file_name) = path.file_stem() {
738 let device_name =
739 file_name.to_string_lossy().to_string();
740
741 match DeviceModel::from_file(&path) {
743 Ok(model) => {
744 {
746 let mut models = models.write().unwrap();
747 models.insert(
748 device_name.clone(),
749 model.clone(),
750 );
751 }
752
753 callback(device_name, model);
755 }
756 Err(e) => {
757 error!(
758 "Failed to load model from {}: {}",
759 path.display(),
760 e
761 );
762 }
763 }
764 }
765 }
766 }
767 }
768 }
769 }
770 Err(e) => {
771 error!("Watch error: {:?}", e);
772 }
773 }
774 }
775 });
776
777 info!(
778 "Model monitor started, watching directory: {}",
779 self.watch_dir.display()
780 );
781 Ok(())
782 }
783
784 pub fn stop(&mut self) -> Result<()> {
790 if !self.running {
791 return Ok(());
792 }
793
794 self.watcher = None;
795 self.running = false;
796
797 info!("Model monitor stopped");
798 Ok(())
799 }
800
801 fn load_existing_models(&self) -> Result<()> {
807 if !self.watch_dir.exists() {
808 return Ok(());
809 }
810
811 let entries = fs::read_dir(&self.watch_dir).map_err(|e| {
812 PoKeysError::ModelDirReadError(
813 self.watch_dir.to_string_lossy().to_string(),
814 e.to_string(),
815 )
816 })?;
817
818 for entry in entries {
819 let entry = entry.map_err(|e| {
820 PoKeysError::ModelDirReadError(
821 self.watch_dir.to_string_lossy().to_string(),
822 e.to_string(),
823 )
824 })?;
825 let path = entry.path();
826
827 if path.extension().is_some_and(|ext| ext == "yaml") {
828 if let Some(file_name) = path.file_stem() {
829 let device_name = file_name.to_string_lossy().to_string();
830
831 match DeviceModel::from_file(&path) {
832 Ok(model) => {
833 {
835 let mut models = self.models.write().unwrap();
836 models.insert(device_name.clone(), model.clone());
837 }
838
839 (self.callback)(device_name, model);
841 }
842 Err(e) => {
843 warn!("Failed to load model from {}: {}", path.display(), e);
844 }
845 }
846 }
847 }
848 }
849
850 Ok(())
851 }
852
853 pub fn get_model(&self, device_name: &str) -> Option<DeviceModel> {
863 let models = self.models.read().unwrap();
864 models.get(device_name).cloned()
865 }
866
867 pub fn get_all_models(&self) -> HashMap<String, DeviceModel> {
873 let models = self.models.read().unwrap();
874 models.clone()
875 }
876}
877
878pub fn copy_default_models_to_user_dir(model_dir: Option<&Path>) -> Result<()> {
891 let dir = model_dir
892 .map(Path::to_path_buf)
893 .unwrap_or_else(get_default_model_dir);
894
895 if !dir.exists() {
897 fs::create_dir_all(&dir).map_err(|e| {
898 PoKeysError::ModelDirCreateError(dir.to_string_lossy().to_string(), e.to_string())
899 })?;
900 }
901
902 let default_models = [
904 "PoKeys56U.yaml",
905 "PoKeys57U.yaml",
906 "PoKeys56E.yaml",
907 "PoKeys57E.yaml",
908 ];
909
910 let package_model_dir = std::env::current_exe()
912 .map_err(|e| {
913 PoKeysError::ModelDirReadError(
914 "Failed to get current executable path".to_string(),
915 e.to_string(),
916 )
917 })?
918 .parent()
919 .ok_or_else(|| {
920 PoKeysError::ModelDirReadError(
921 "Failed to get parent directory of executable".to_string(),
922 "No parent directory".to_string(),
923 )
924 })?
925 .join("models");
926
927 let package_model_dir = if !package_model_dir.exists() {
929 let crate_dir = std::env::var("CARGO_MANIFEST_DIR")
931 .map(PathBuf::from)
932 .unwrap_or_else(|_| {
933 PathBuf::from("pokeys-lib/models")
935 });
936
937 crate_dir.join("models")
938 } else {
939 package_model_dir
940 };
941
942 for model_file in &default_models {
944 let user_file_path = dir.join(model_file);
945
946 if user_file_path.exists() {
948 continue;
949 }
950
951 let package_file_path = package_model_dir.join(model_file);
953
954 if package_file_path.exists() {
955 fs::copy(&package_file_path, &user_file_path).map_err(|e| {
957 PoKeysError::ModelLoadError(
958 package_file_path.to_string_lossy().to_string(),
959 e.to_string(),
960 )
961 })?;
962
963 info!(
964 "Copied default model file {} to {}",
965 model_file,
966 user_file_path.display()
967 );
968 } else {
969 let source_file_path = PathBuf::from(format!("pokeys-lib/models/{}", model_file));
971
972 if source_file_path.exists() {
973 fs::copy(&source_file_path, &user_file_path).map_err(|e| {
975 PoKeysError::ModelLoadError(
976 source_file_path.to_string_lossy().to_string(),
977 e.to_string(),
978 )
979 })?;
980
981 info!(
982 "Copied default model file {} to {}",
983 model_file,
984 user_file_path.display()
985 );
986 } else {
987 warn!("Default model file {} not found", model_file);
988 }
989 }
990 }
991
992 Ok(())
993}
994
995#[cfg(test)]
996mod tests {
997 use super::*;
998 use tempfile::tempdir;
999
1000 #[test]
1001 fn test_device_model_validation() {
1002 let mut model = DeviceModel {
1004 name: "TestDevice".to_string(),
1005 pins: HashMap::new(),
1006 };
1007
1008 model.pins.insert(
1010 1,
1011 PinModel {
1012 capabilities: vec!["DigitalInput".to_string(), "DigitalOutput".to_string()],
1013 active: true,
1014 },
1015 );
1016
1017 model.pins.insert(
1018 2,
1019 PinModel {
1020 capabilities: vec!["DigitalInput".to_string(), "AnalogInput".to_string()],
1021 active: true,
1022 },
1023 );
1024
1025 assert!(model.validate().is_ok());
1027
1028 let mut invalid_model = model.clone();
1030 invalid_model.name = "".to_string();
1031 assert!(invalid_model.validate().is_err());
1032
1033 let invalid_model = DeviceModel {
1035 name: "TestDevice".to_string(),
1036 pins: HashMap::new(),
1037 };
1038 assert!(invalid_model.validate().is_err());
1039
1040 let mut invalid_model = model.clone();
1042 invalid_model.pins.insert(
1043 3,
1044 PinModel {
1045 capabilities: vec![],
1046 active: true,
1047 },
1048 );
1049 assert!(invalid_model.validate().is_err());
1050 }
1051
1052 #[test]
1053 fn test_related_capabilities() {
1054 let mut model = DeviceModel {
1056 name: "TestDevice".to_string(),
1057 pins: HashMap::new(),
1058 };
1059
1060 model.pins.insert(
1062 1,
1063 PinModel {
1064 capabilities: vec!["DigitalInput".to_string(), "Encoder_1A".to_string()],
1065 active: true,
1066 },
1067 );
1068
1069 model.pins.insert(
1070 2,
1071 PinModel {
1072 capabilities: vec!["DigitalInput".to_string(), "Encoder_1B".to_string()],
1073 active: true,
1074 },
1075 );
1076
1077 assert!(model.validate().is_ok());
1079
1080 let related = model.get_related_capabilities(1, "Encoder_1A");
1082 assert_eq!(related.len(), 1);
1083 assert_eq!(related[0].0, "Encoder_1B");
1084 assert_eq!(related[0].1, 2);
1085
1086 let mut invalid_model = model.clone();
1088 invalid_model.pins.get_mut(&2).unwrap().capabilities = vec!["DigitalInput".to_string()];
1089
1090 }
1093
1094 #[test]
1095 fn test_matrix_keyboard_validation() {
1096 let mut model = DeviceModel {
1098 name: "TestDevice".to_string(),
1099 pins: HashMap::new(),
1100 };
1101
1102 model.pins.insert(
1104 1,
1105 PinModel {
1106 capabilities: vec![
1107 "DigitalInput".to_string(),
1108 "MatrixKeyboard_Row1".to_string(),
1109 ],
1110 active: true,
1111 },
1112 );
1113
1114 model.pins.insert(
1115 2,
1116 PinModel {
1117 capabilities: vec![
1118 "DigitalInput".to_string(),
1119 "MatrixKeyboard_Row2".to_string(),
1120 ],
1121 active: true,
1122 },
1123 );
1124
1125 model.pins.insert(
1126 3,
1127 PinModel {
1128 capabilities: vec![
1129 "DigitalInput".to_string(),
1130 "MatrixKeyboard_Col1".to_string(),
1131 ],
1132 active: true,
1133 },
1134 );
1135
1136 model.pins.insert(
1137 4,
1138 PinModel {
1139 capabilities: vec![
1140 "DigitalInput".to_string(),
1141 "MatrixKeyboard_Col2".to_string(),
1142 ],
1143 active: true,
1144 },
1145 );
1146
1147 assert!(model.validate().is_ok());
1149
1150 let related = model.get_related_capabilities(1, "MatrixKeyboard_Row1");
1152 assert_eq!(related.len(), 2);
1153 assert!(
1154 related
1155 .iter()
1156 .any(|(cap, pin)| cap == "MatrixKeyboard_Col1" && *pin == 3)
1157 );
1158 assert!(
1159 related
1160 .iter()
1161 .any(|(cap, pin)| cap == "MatrixKeyboard_Col2" && *pin == 4)
1162 );
1163
1164 let mut invalid_model = model.clone();
1166 invalid_model.pins.remove(&3);
1167 invalid_model.pins.remove(&4);
1168
1169 }
1172
1173 #[test]
1174 fn test_yaml_serialization() {
1175 let mut model = DeviceModel {
1177 name: "TestDevice".to_string(),
1178 pins: HashMap::new(),
1179 };
1180
1181 model.pins.insert(
1183 1,
1184 PinModel {
1185 capabilities: vec!["DigitalInput".to_string(), "DigitalOutput".to_string()],
1186 active: true,
1187 },
1188 );
1189
1190 model.pins.insert(
1191 2,
1192 PinModel {
1193 capabilities: vec!["DigitalInput".to_string(), "AnalogInput".to_string()],
1194 active: true,
1195 },
1196 );
1197
1198 let yaml = serde_yaml::to_string(&model).unwrap();
1200
1201 let deserialized: DeviceModel = serde_yaml::from_str(&yaml).unwrap();
1203
1204 assert_eq!(model.name, deserialized.name);
1206 assert_eq!(model.pins.len(), deserialized.pins.len());
1207
1208 for (pin_num, pin) in &model.pins {
1209 let deserialized_pin = deserialized.pins.get(pin_num).unwrap();
1210 assert_eq!(pin.capabilities, deserialized_pin.capabilities);
1211 assert_eq!(pin.active, deserialized_pin.active);
1212 }
1213 }
1214
1215 #[test]
1216 fn test_model_file_loading() {
1217 let dir = tempdir().unwrap();
1219 let file_path = dir.path().join("TestDevice.yaml");
1220
1221 let mut model = DeviceModel {
1223 name: "TestDevice".to_string(),
1224 pins: HashMap::new(),
1225 };
1226
1227 model.pins.insert(
1229 1,
1230 PinModel {
1231 capabilities: vec!["DigitalInput".to_string(), "DigitalOutput".to_string()],
1232 active: true,
1233 },
1234 );
1235
1236 model.pins.insert(
1237 2,
1238 PinModel {
1239 capabilities: vec!["DigitalInput".to_string(), "AnalogInput".to_string()],
1240 active: true,
1241 },
1242 );
1243
1244 let yaml = serde_yaml::to_string(&model).unwrap();
1246
1247 fs::write(&file_path, yaml).unwrap();
1249
1250 let loaded_model = DeviceModel::from_file(&file_path).unwrap();
1252
1253 assert_eq!(model.name, loaded_model.name);
1255 assert_eq!(model.pins.len(), loaded_model.pins.len());
1256
1257 for (pin_num, pin) in &model.pins {
1258 let loaded_pin = loaded_model.pins.get(pin_num).unwrap();
1259 assert_eq!(pin.capabilities, loaded_pin.capabilities);
1260 assert_eq!(pin.active, loaded_pin.active);
1261 }
1262 }
1263}