1#![forbid(unsafe_code)]
20
21use std::collections::HashMap;
22use std::time::Instant;
23
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum SensorKind {
32 Temperature,
34 Imu,
36 Camera,
38 Audio,
40 Distance,
42 Pressure,
44 Encoder,
46 Gps,
48 Power,
50 Custom,
52}
53
54impl SensorKind {
55 #[must_use]
56 pub const fn as_str(self) -> &'static str {
57 match self {
58 Self::Temperature => "temperature",
59 Self::Imu => "imu",
60 Self::Camera => "camera",
61 Self::Audio => "audio",
62 Self::Distance => "distance",
63 Self::Pressure => "pressure",
64 Self::Encoder => "encoder",
65 Self::Gps => "gps",
66 Self::Power => "power",
67 Self::Custom => "custom",
68 }
69 }
70}
71
72impl std::fmt::Display for SensorKind {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.write_str(self.as_str())
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum ActuatorKind {
84 Motor,
86 Relay,
88 Display,
90 Speaker,
92 Valve,
94 Thermal,
96 Custom,
98}
99
100impl ActuatorKind {
101 #[must_use]
102 pub const fn as_str(self) -> &'static str {
103 match self {
104 Self::Motor => "motor",
105 Self::Relay => "relay",
106 Self::Display => "display",
107 Self::Speaker => "speaker",
108 Self::Valve => "valve",
109 Self::Thermal => "thermal",
110 Self::Custom => "custom",
111 }
112 }
113}
114
115impl std::fmt::Display for ActuatorKind {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.write_str(self.as_str())
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct SensorReading {
126 pub sensor_id: String,
128 pub kind: SensorKind,
130 pub value: f64,
132 pub extra: Vec<f64>,
134 pub timestamp: f64,
136 pub confidence: f64,
138}
139
140impl SensorReading {
141 #[must_use]
143 pub fn new(sensor_id: impl Into<String>, kind: SensorKind, value: f64) -> Self {
144 Self {
145 sensor_id: sensor_id.into(),
146 kind,
147 value,
148 extra: Vec::new(),
149 timestamp: now_secs(),
150 confidence: 1.0,
151 }
152 }
153
154 #[must_use]
156 pub fn with_extra(mut self, extra: Vec<f64>) -> Self {
157 self.extra = extra;
158 self
159 }
160
161 #[must_use]
163 pub const fn with_confidence(mut self, confidence: f64) -> Self {
164 self.confidence = confidence.clamp(0.0, 1.0);
165 self
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ActuatorCommand {
174 pub actuator_id: String,
176 pub kind: ActuatorKind,
178 pub value: f64,
180 pub params: Vec<f64>,
182 pub timestamp: f64,
184}
185
186impl ActuatorCommand {
187 #[must_use]
189 pub fn new(actuator_id: impl Into<String>, kind: ActuatorKind, value: f64) -> Self {
190 Self {
191 actuator_id: actuator_id.into(),
192 kind,
193 value,
194 params: Vec::new(),
195 timestamp: now_secs(),
196 }
197 }
198
199 #[must_use]
201 pub fn with_params(mut self, params: Vec<f64>) -> Self {
202 self.params = params;
203 self
204 }
205}
206
207pub trait SensorDevice: Send + Sync {
214 fn id(&self) -> &str;
216
217 fn kind(&self) -> SensorKind;
219
220 fn read(&self) -> Option<SensorReading>;
222
223 fn is_available(&self) -> bool {
225 true
226 }
227}
228
229pub trait ActuatorDevice: Send + Sync {
234 fn id(&self) -> &str;
236
237 fn kind(&self) -> ActuatorKind;
239
240 fn command(&self, cmd: &ActuatorCommand) -> Result<(), String>;
242
243 fn is_available(&self) -> bool {
245 true
246 }
247
248 fn e_stop(&self) -> Result<(), String> {
250 Ok(())
251 }
252}
253
254pub struct StubSensor {
259 id: String,
260 kind: SensorKind,
261 value: f64,
262}
263
264impl StubSensor {
265 #[must_use]
266 pub fn new(id: impl Into<String>, kind: SensorKind, value: f64) -> Self {
267 Self {
268 id: id.into(),
269 kind,
270 value,
271 }
272 }
273}
274
275impl SensorDevice for StubSensor {
276 fn id(&self) -> &str {
277 &self.id
278 }
279
280 fn kind(&self) -> SensorKind {
281 self.kind
282 }
283
284 fn read(&self) -> Option<SensorReading> {
285 Some(SensorReading::new(&self.id, self.kind, self.value))
286 }
287}
288
289pub struct StubActuator {
291 id: String,
292 kind: ActuatorKind,
293 last_command: std::sync::Mutex<Option<ActuatorCommand>>,
294}
295
296impl StubActuator {
297 #[must_use]
298 pub fn new(id: impl Into<String>, kind: ActuatorKind) -> Self {
299 Self {
300 id: id.into(),
301 kind,
302 last_command: std::sync::Mutex::new(None),
303 }
304 }
305
306 #[must_use]
308 pub fn last_command(&self) -> Option<ActuatorCommand> {
309 self.last_command.lock().map_or(None, |c| c.clone())
310 }
311}
312
313impl ActuatorDevice for StubActuator {
314 fn id(&self) -> &str {
315 &self.id
316 }
317
318 fn kind(&self) -> ActuatorKind {
319 self.kind
320 }
321
322 fn command(&self, cmd: &ActuatorCommand) -> Result<(), String> {
323 let Ok(mut last) = self.last_command.lock() else {
324 return Err("sensorimotor last-command lock poisoned".to_string());
325 };
326 *last = Some(cmd.clone());
327 Ok(())
328 }
329}
330
331pub struct SensorimotorBus {
338 sensors: HashMap<String, Box<dyn SensorDevice>>,
339 actuators: HashMap<String, Box<dyn ActuatorDevice>>,
340 reading_history: VecDeque<SensorReading>,
342 max_history: usize,
344 commands_sent: u64,
346 readings_collected: u64,
348}
349
350use std::collections::VecDeque;
351
352impl SensorimotorBus {
353 #[must_use]
355 pub fn new(max_history: usize) -> Self {
356 Self {
357 sensors: HashMap::new(),
358 actuators: HashMap::new(),
359 reading_history: VecDeque::with_capacity(max_history),
360 max_history,
361 commands_sent: 0,
362 readings_collected: 0,
363 }
364 }
365
366 pub fn register_sensor(&mut self, sensor: Box<dyn SensorDevice>) {
368 self.sensors.insert(sensor.id().to_string(), sensor);
369 }
370
371 pub fn register_actuator(&mut self, actuator: Box<dyn ActuatorDevice>) {
373 self.actuators.insert(actuator.id().to_string(), actuator);
374 }
375
376 pub fn poll_all(&mut self) -> Vec<SensorReading> {
378 let readings: Vec<SensorReading> = self.sensors.values().filter_map(|s| s.read()).collect();
379
380 for r in &readings {
381 self.reading_history.push_back(r.clone());
382 if self.reading_history.len() > self.max_history {
383 self.reading_history.pop_front();
384 }
385 }
386
387 self.readings_collected += u64::try_from(readings.len()).unwrap_or(0);
388 readings
389 }
390
391 #[must_use]
393 pub fn read_sensor(&self, sensor_id: &str) -> Option<SensorReading> {
394 self.sensors.get(sensor_id).and_then(|s| s.read())
395 }
396
397 pub fn send_command(&mut self, cmd: &ActuatorCommand) -> Result<(), String> {
399 let actuator = self
400 .actuators
401 .get(&cmd.actuator_id)
402 .ok_or_else(|| format!("actuator '{}' not registered", cmd.actuator_id))?;
403
404 actuator.command(cmd)?;
405 self.commands_sent += 1;
406 Ok(())
407 }
408
409 #[must_use]
411 pub fn e_stop_all(&self) -> Vec<String> {
412 self.actuators
413 .values()
414 .filter_map(|a| a.e_stop().err())
415 .collect()
416 }
417
418 #[must_use]
420 pub const fn recent_readings(&self) -> &[SensorReading] {
421 &[]
424 }
425
426 #[must_use]
428 pub fn history(&self) -> Vec<SensorReading> {
429 self.reading_history.iter().cloned().collect()
430 }
431
432 #[must_use]
434 pub fn sensor_count(&self) -> usize {
435 self.sensors.len()
436 }
437
438 #[must_use]
440 pub fn actuator_count(&self) -> usize {
441 self.actuators.len()
442 }
443
444 #[must_use]
446 pub const fn readings_collected(&self) -> u64 {
447 self.readings_collected
448 }
449
450 #[must_use]
452 pub const fn commands_sent(&self) -> u64 {
453 self.commands_sent
454 }
455
456 #[must_use]
458 pub fn sensor_ids(&self) -> Vec<String> {
459 self.sensors.keys().cloned().collect()
460 }
461
462 #[must_use]
464 pub fn actuator_ids(&self) -> Vec<String> {
465 self.actuators.keys().cloned().collect()
466 }
467}
468
469impl Default for SensorimotorBus {
470 fn default() -> Self {
471 Self::new(256)
472 }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct ReflexRule {
484 pub sensor_id: String,
486 pub actuator_id: String,
488 pub actuator_kind: ActuatorKind,
490 pub threshold: f64,
492 pub command_value: f64,
494 pub trigger_above: bool,
496 pub cooldown_secs: f64,
498}
499
500impl ReflexRule {
501 #[must_use]
503 pub fn above(
504 sensor_id: impl Into<String>,
505 actuator_id: impl Into<String>,
506 actuator_kind: ActuatorKind,
507 threshold: f64,
508 command_value: f64,
509 cooldown_secs: f64,
510 ) -> Self {
511 Self {
512 sensor_id: sensor_id.into(),
513 actuator_id: actuator_id.into(),
514 actuator_kind,
515 threshold,
516 command_value,
517 trigger_above: true,
518 cooldown_secs,
519 }
520 }
521
522 #[must_use]
524 pub fn below(
525 sensor_id: impl Into<String>,
526 actuator_id: impl Into<String>,
527 actuator_kind: ActuatorKind,
528 threshold: f64,
529 command_value: f64,
530 cooldown_secs: f64,
531 ) -> Self {
532 Self {
533 sensor_id: sensor_id.into(),
534 actuator_id: actuator_id.into(),
535 actuator_kind,
536 threshold,
537 command_value,
538 trigger_above: false,
539 cooldown_secs,
540 }
541 }
542
543 #[must_use]
545 pub fn is_triggered(&self, reading: &SensorReading) -> bool {
546 if self.trigger_above {
547 reading.value > self.threshold
548 } else {
549 reading.value < self.threshold
550 }
551 }
552}
553
554pub struct ReflexLoop {
556 rules: Vec<ReflexRule>,
557 last_trigger: HashMap<String, Instant>,
558}
559
560impl ReflexLoop {
561 #[must_use]
563 pub fn new() -> Self {
564 Self {
565 rules: Vec::new(),
566 last_trigger: HashMap::new(),
567 }
568 }
569
570 pub fn add_rule(&mut self, rule: ReflexRule) {
572 self.rules.push(rule);
573 }
574
575 pub fn evaluate(&mut self, readings: &[SensorReading]) -> Vec<ActuatorCommand> {
578 let mut commands = Vec::new();
579 let now = Instant::now();
580
581 for rule in &self.rules {
582 if let Some(&last) = self.last_trigger.get(&rule.sensor_id) {
584 let elapsed = now.duration_since(last).as_secs_f64();
585 if elapsed < rule.cooldown_secs {
586 continue;
587 }
588 }
589
590 if let Some(reading) = readings.iter().find(|r| r.sensor_id == rule.sensor_id) {
592 if rule.is_triggered(reading) {
593 commands.push(ActuatorCommand::new(
594 &rule.actuator_id,
595 rule.actuator_kind,
596 rule.command_value,
597 ));
598 self.last_trigger.insert(rule.sensor_id.clone(), now);
599 }
600 }
601 }
602
603 commands
604 }
605
606 #[must_use]
608 pub fn rule_count(&self) -> usize {
609 self.rules.len()
610 }
611}
612
613impl Default for ReflexLoop {
614 fn default() -> Self {
615 Self::new()
616 }
617}
618
619pub struct SysfsSensor {
631 id: String,
632 kind: SensorKind,
633 path: String,
634 scale: f64,
635 available: bool,
636}
637
638impl SysfsSensor {
639 #[must_use]
644 pub fn new(
645 id: impl Into<String>,
646 kind: SensorKind,
647 path: impl Into<String>,
648 scale: f64,
649 ) -> Self {
650 let path = path.into();
651 let available = std::path::Path::new(&path).exists();
652 Self {
653 id: id.into(),
654 kind,
655 path,
656 scale,
657 available,
658 }
659 }
660
661 #[must_use]
663 pub fn thermal(zone: usize) -> Self {
664 Self::new(
665 format!("thermal_zone{zone}"),
666 SensorKind::Temperature,
667 format!("/sys/class/thermal/thermal_zone{zone}/temp"),
668 1000.0,
669 )
670 }
671
672 #[must_use]
674 pub fn battery(name: &str) -> Self {
675 Self::new(
676 format!("battery_{name}"),
677 SensorKind::Power,
678 format!("/sys/class/power_supply/{name}/capacity"),
679 1.0,
680 )
681 }
682
683 #[must_use]
685 pub fn fan(hwmon: usize, fan: usize) -> Self {
686 Self::new(
687 format!("fan{hwmon}_{fan}"),
688 SensorKind::Custom,
689 format!("/sys/class/hwmon/hwmon{hwmon}/fan{fan}_input"),
690 1.0,
691 )
692 }
693}
694
695impl SensorDevice for SysfsSensor {
696 fn id(&self) -> &str {
697 &self.id
698 }
699
700 fn kind(&self) -> SensorKind {
701 self.kind
702 }
703
704 fn read(&self) -> Option<SensorReading> {
705 if !self.available {
706 return None;
707 }
708 let raw = std::fs::read_to_string(&self.path).ok()?;
709 let trimmed = raw.trim();
710 let value: f64 = trimmed.parse().ok()?;
711 let scaled = if self.scale > 0.0 {
712 value / self.scale
713 } else {
714 value
715 };
716 Some(SensorReading::new(&self.id, self.kind, scaled))
717 }
718
719 fn is_available(&self) -> bool {
720 self.available
721 }
722}
723
724impl std::fmt::Debug for SysfsSensor {
725 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
726 f.debug_struct("SysfsSensor")
727 .field("id", &self.id)
728 .field("kind", &self.kind)
729 .field("path", &self.path)
730 .field("scale", &self.scale)
731 .field("available", &self.available)
732 .finish()
733 }
734}
735
736pub type SensorParser = Box<dyn Fn(&str) -> Option<f64> + Send + Sync>;
738
739pub struct ProcfsSensor {
747 id: String,
748 kind: SensorKind,
749 path: String,
750 parser: SensorParser,
752 available: bool,
753}
754
755impl ProcfsSensor {
756 #[must_use]
758 pub fn new(
759 id: impl Into<String>,
760 kind: SensorKind,
761 path: impl Into<String>,
762 parser: SensorParser,
763 ) -> Self {
764 let path = path.into();
765 let available = std::path::Path::new(&path).exists();
766 Self {
767 id: id.into(),
768 kind,
769 path,
770 parser,
771 available,
772 }
773 }
774
775 #[must_use]
777 pub fn loadavg() -> Self {
778 Self::new(
779 "cpu_loadavg",
780 SensorKind::Custom,
781 "/proc/loadavg",
782 Box::new(|content: &str| {
783 content
784 .split_whitespace()
785 .next()
786 .and_then(|s| s.parse::<f64>().ok())
787 }),
788 )
789 }
790
791 #[must_use]
795 pub fn mem_pressure() -> Self {
796 Self::new(
797 "mem_pressure",
798 SensorKind::Custom,
799 "/proc/meminfo",
800 Box::new(|content: &str| {
801 let mut mem_total = None;
802 let mut mem_avail = None;
803 for line in content.lines() {
804 if line.starts_with("MemTotal:") {
805 mem_total = parse_proc_kb(line);
806 } else if line.starts_with("MemAvailable:") {
807 mem_avail = parse_proc_kb(line);
808 }
809 }
810 match (mem_total, mem_avail) {
811 (Some(total), Some(avail)) if total > 0 => {
812 Some(1.0 - (avail as f64 / total as f64).min(1.0))
813 }
814 _ => None,
815 }
816 }),
817 )
818 }
819}
820
821impl SensorDevice for ProcfsSensor {
822 fn id(&self) -> &str {
823 &self.id
824 }
825
826 fn kind(&self) -> SensorKind {
827 self.kind
828 }
829
830 fn read(&self) -> Option<SensorReading> {
831 if !self.available {
832 return None;
833 }
834 let content = std::fs::read_to_string(&self.path).ok()?;
835 let value = (self.parser)(&content)?;
836 Some(SensorReading::new(&self.id, self.kind, value))
837 }
838
839 fn is_available(&self) -> bool {
840 self.available
841 }
842}
843
844impl std::fmt::Debug for ProcfsSensor {
845 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846 f.debug_struct("ProcfsSensor")
847 .field("id", &self.id)
848 .field("kind", &self.kind)
849 .field("path", &self.path)
850 .field("available", &self.available)
851 .finish_non_exhaustive()
852 }
853}
854
855fn parse_proc_kb(line: &str) -> Option<u64> {
857 line.split(':')
858 .nth(1)
859 .and_then(|s| s.split_whitespace().next())
860 .and_then(|s| s.parse::<u64>().ok())
861}
862
863pub struct CpuUsageSensor {
871 prev_idle: Option<u64>,
872 prev_total: Option<u64>,
873}
874
875impl CpuUsageSensor {
876 #[must_use]
877 pub const fn new() -> Self {
878 Self {
879 prev_idle: None,
880 prev_total: None,
881 }
882 }
883}
884
885impl Default for CpuUsageSensor {
886 fn default() -> Self {
887 Self::new()
888 }
889}
890
891impl SensorDevice for CpuUsageSensor {
892 fn id(&self) -> &str {
893 "cpu_usage"
894 }
895
896 fn kind(&self) -> SensorKind {
897 SensorKind::Custom
898 }
899
900 fn read(&self) -> Option<SensorReading> {
901 let content = std::fs::read_to_string("/proc/stat").ok()?;
902 let first_line = content.lines().next()?;
903 if !first_line.starts_with("cpu ") {
904 return None;
905 }
906 let fields: Vec<u64> = first_line
907 .split_whitespace()
908 .skip(1)
909 .filter_map(|s| s.parse::<u64>().ok())
910 .collect();
911 if fields.len() < 4 {
912 return None;
913 }
914 let idle = fields[3];
915 let total: u64 = fields.iter().sum();
916 let usage = match (self.prev_idle, self.prev_total) {
917 (Some(prev_idle), Some(prev_total)) => {
918 let idle_delta = idle.saturating_sub(prev_idle) as f64;
919 let total_delta = total.saturating_sub(prev_total) as f64;
920 if total_delta > 0.0 {
921 ((1.0 - idle_delta / total_delta) * 100.0).clamp(0.0, 100.0)
922 } else {
923 0.0
924 }
925 }
926 _ => 0.0,
927 };
928 Some(SensorReading::new("cpu_usage", SensorKind::Custom, usage))
929 }
930
931 fn is_available(&self) -> bool {
932 std::path::Path::new("/proc/stat").exists()
933 }
934}
935
936impl std::fmt::Debug for CpuUsageSensor {
937 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
938 f.debug_struct("CpuUsageSensor")
939 .field("prev_idle", &self.prev_idle)
940 .field("prev_total", &self.prev_total)
941 .finish()
942 }
943}
944
945pub struct DiskUsageSensor {
952 id: String,
953 path: String,
954 available: bool,
955}
956
957impl DiskUsageSensor {
958 #[must_use]
959 pub fn new(id: impl Into<String>, path: impl Into<String>) -> Self {
960 let path = path.into();
961 let available = std::path::Path::new(&path).exists();
962 Self {
963 id: id.into(),
964 path,
965 available,
966 }
967 }
968
969 #[must_use]
971 pub fn root() -> Self {
972 Self::new("disk_root", "/")
973 }
974}
975
976impl SensorDevice for DiskUsageSensor {
977 fn id(&self) -> &str {
978 &self.id
979 }
980
981 fn kind(&self) -> SensorKind {
982 SensorKind::Custom
983 }
984
985 fn read(&self) -> Option<SensorReading> {
986 if !self.available {
987 return None;
988 }
989 let output = std::process::Command::new("df")
990 .arg("-P")
991 .arg(&self.path)
992 .output()
993 .ok()?;
994 let stdout = String::from_utf8(output.stdout).ok()?;
995 let line = stdout.lines().nth(1)?;
996 let fields: Vec<&str> = line.split_whitespace().collect();
997 if fields.len() < 5 {
998 return None;
999 }
1000 let used: f64 = fields[2].parse().ok()?;
1001 let total: f64 = fields[1].parse().ok()?;
1002 if total > 0.0 {
1003 let pct = (used / total) * 100.0;
1004 Some(SensorReading::new(&self.id, SensorKind::Custom, pct))
1005 } else {
1006 None
1007 }
1008 }
1009
1010 fn is_available(&self) -> bool {
1011 self.available
1012 }
1013}
1014
1015impl std::fmt::Debug for DiskUsageSensor {
1016 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1017 f.debug_struct("DiskUsageSensor")
1018 .field("id", &self.id)
1019 .field("path", &self.path)
1020 .field("available", &self.available)
1021 .finish()
1022 }
1023}
1024
1025pub struct NetworkThroughputSensor {
1032 id: String,
1033 interface: String,
1034 prev_rx_bytes: Option<u64>,
1035 prev_tx_bytes: Option<u64>,
1036 prev_time: Option<Instant>,
1037}
1038
1039impl NetworkThroughputSensor {
1040 #[must_use]
1041 pub fn new(interface: impl Into<String>) -> Self {
1042 let iface = interface.into();
1043 Self {
1044 id: format!("net_{iface}"),
1045 interface: iface,
1046 prev_rx_bytes: None,
1047 prev_tx_bytes: None,
1048 prev_time: None,
1049 }
1050 }
1051
1052 #[must_use]
1055 pub fn default_interface() -> Self {
1056 let iface = if let Ok(content) = std::fs::read_to_string("/proc/net/dev") {
1057 content
1058 .lines()
1059 .skip(2)
1060 .find_map(|line| {
1061 let name = line.split(':').next()?.trim();
1062 if name != "lo" && !name.is_empty() {
1063 Some(name.to_string())
1064 } else {
1065 None
1066 }
1067 })
1068 .unwrap_or_else(|| "eth0".to_string())
1069 } else {
1070 "eth0".to_string()
1071 };
1072 Self::new(iface)
1073 }
1074}
1075
1076impl SensorDevice for NetworkThroughputSensor {
1077 fn id(&self) -> &str {
1078 &self.id
1079 }
1080
1081 fn kind(&self) -> SensorKind {
1082 SensorKind::Custom
1083 }
1084
1085 fn read(&self) -> Option<SensorReading> {
1086 let content = std::fs::read_to_string("/proc/net/dev").ok()?;
1087 for line in content.lines().skip(2) {
1088 let mut parts = line.split(':');
1089 let name = parts.next()?.trim();
1090 if name != self.interface {
1091 continue;
1092 }
1093 let stats: Vec<u64> = parts
1094 .next()?
1095 .split_whitespace()
1096 .filter_map(|s| s.parse::<u64>().ok())
1097 .collect();
1098 if stats.len() < 9 {
1099 return None;
1100 }
1101 let rx_bytes = stats[0];
1102 let tx_bytes = stats[8];
1103 let now = Instant::now();
1104 let throughput = match (self.prev_rx_bytes, self.prev_tx_bytes, self.prev_time) {
1105 (Some(prev_rx), Some(prev_tx), Some(prev_t)) => {
1106 let elapsed = now.duration_since(prev_t).as_secs_f64();
1107 if elapsed > 0.0 {
1108 let rx_delta = rx_bytes.saturating_sub(prev_rx) as f64;
1109 let tx_delta = tx_bytes.saturating_sub(prev_tx) as f64;
1110 (rx_delta + tx_delta) / elapsed
1111 } else {
1112 0.0
1113 }
1114 }
1115 _ => 0.0,
1116 };
1117 return Some(SensorReading::new(&self.id, SensorKind::Custom, throughput));
1118 }
1119 None
1120 }
1121
1122 fn is_available(&self) -> bool {
1123 std::path::Path::new("/proc/net/dev").exists()
1124 }
1125}
1126
1127impl std::fmt::Debug for NetworkThroughputSensor {
1128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1129 f.debug_struct("NetworkThroughputSensor")
1130 .field("id", &self.id)
1131 .field("interface", &self.interface)
1132 .finish_non_exhaustive()
1133 }
1134}
1135
1136pub struct CpuFreqSensor {
1143 id: String,
1144 path: String,
1145 available: bool,
1146}
1147
1148impl CpuFreqSensor {
1149 #[must_use]
1150 pub fn new(core: usize) -> Self {
1151 let path = format!("/sys/devices/system/cpu/cpu{core}/cpufreq/scaling_cur_freq");
1152 let available = std::path::Path::new(&path).exists();
1153 Self {
1154 id: format!("cpu{core}_freq"),
1155 path,
1156 available,
1157 }
1158 }
1159}
1160
1161impl SensorDevice for CpuFreqSensor {
1162 fn id(&self) -> &str {
1163 &self.id
1164 }
1165
1166 fn kind(&self) -> SensorKind {
1167 SensorKind::Custom
1168 }
1169
1170 fn read(&self) -> Option<SensorReading> {
1171 if !self.available {
1172 return None;
1173 }
1174 let raw = std::fs::read_to_string(&self.path).ok()?;
1175 let khz: f64 = raw.trim().parse().ok()?;
1176 Some(SensorReading::new(
1177 &self.id,
1178 SensorKind::Custom,
1179 khz / 1000.0,
1180 ))
1181 }
1182
1183 fn is_available(&self) -> bool {
1184 self.available
1185 }
1186}
1187
1188impl std::fmt::Debug for CpuFreqSensor {
1189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1190 f.debug_struct("CpuFreqSensor")
1191 .field("id", &self.id)
1192 .field("path", &self.path)
1193 .field("available", &self.available)
1194 .finish()
1195 }
1196}
1197
1198pub struct SysfsActuator {
1210 id: String,
1211 kind: ActuatorKind,
1212 path: String,
1213 scale: f64,
1214 available: bool,
1215}
1216
1217impl SysfsActuator {
1218 #[must_use]
1223 pub fn new(
1224 id: impl Into<String>,
1225 kind: ActuatorKind,
1226 path: impl Into<String>,
1227 scale: f64,
1228 ) -> Self {
1229 let path = path.into();
1230 let available = std::path::Path::new(&path).exists();
1231 Self {
1232 id: id.into(),
1233 kind,
1234 path,
1235 scale,
1236 available,
1237 }
1238 }
1239
1240 #[must_use]
1242 pub fn fan_pwm(hwmon: usize, pwm: usize) -> Self {
1243 Self::new(
1244 format!("fan_pwm{hwmon}_{pwm}"),
1245 ActuatorKind::Motor,
1246 format!("/sys/class/hwmon/hwmon{hwmon}/pwm{pwm}"),
1247 1.0,
1248 )
1249 }
1250
1251 #[must_use]
1253 pub fn led(name: &str) -> Self {
1254 Self::new(
1255 format!("led_{name}"),
1256 ActuatorKind::Display,
1257 format!("/sys/class/leds/{name}/brightness"),
1258 1.0,
1259 )
1260 }
1261}
1262
1263impl ActuatorDevice for SysfsActuator {
1264 fn id(&self) -> &str {
1265 &self.id
1266 }
1267
1268 fn kind(&self) -> ActuatorKind {
1269 self.kind
1270 }
1271
1272 fn command(&self, cmd: &ActuatorCommand) -> Result<(), String> {
1273 if !self.available {
1274 return Err(format!("actuator '{}' path not available", self.id));
1275 }
1276 let raw_value = cmd.value * self.scale;
1277 let output = format!("{}", raw_value.round() as i64);
1278 std::fs::write(&self.path, output)
1279 .map_err(|e| format!("failed to write to {}: {e}", self.path))
1280 }
1281
1282 fn is_available(&self) -> bool {
1283 self.available
1284 }
1285
1286 fn e_stop(&self) -> Result<(), String> {
1287 if !self.available {
1288 return Err(format!("actuator '{}' path not available", self.id));
1289 }
1290 std::fs::write(&self.path, "0").map_err(|e| format!("failed to e-stop {}: {e}", self.path))
1292 }
1293}
1294
1295impl std::fmt::Debug for SysfsActuator {
1296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 f.debug_struct("SysfsActuator")
1298 .field("id", &self.id)
1299 .field("kind", &self.kind)
1300 .field("path", &self.path)
1301 .field("scale", &self.scale)
1302 .field("available", &self.available)
1303 .finish()
1304 }
1305}
1306
1307#[must_use]
1313pub fn discover_fan_actuators() -> Vec<SysfsActuator> {
1314 let mut actuators = Vec::new();
1315 for hwmon in 0..8 {
1316 for pwm in 1..=4 {
1317 let actuator = SysfsActuator::fan_pwm(hwmon, pwm);
1318 if actuator.is_available() {
1319 actuators.push(actuator);
1320 }
1321 }
1322 }
1323 actuators
1324}
1325
1326#[must_use]
1330pub fn discover_led_actuators() -> Vec<SysfsActuator> {
1331 let mut actuators = Vec::new();
1332 for name in [
1333 "input0::scrolllock",
1334 "input0::numlock",
1335 "input0::capslock",
1336 "mmc0::",
1337 "phy0-led",
1338 "eth0-link",
1339 "power",
1340 "charging",
1341 "disk-activity",
1342 ] {
1343 let actuator = SysfsActuator::led(name);
1344 if actuator.is_available() {
1345 actuators.push(actuator);
1346 }
1347 }
1348 actuators
1349}
1350
1351#[must_use]
1355pub fn discover_thermal_sensors() -> Vec<SysfsSensor> {
1356 let mut sensors = Vec::new();
1357 for i in 0..16 {
1358 let sensor = SysfsSensor::thermal(i);
1359 if sensor.is_available() {
1360 sensors.push(sensor);
1361 }
1362 }
1363 sensors
1364}
1365
1366#[must_use]
1370pub fn discover_battery_sensors() -> Vec<SysfsSensor> {
1371 let mut sensors = Vec::new();
1372 for name in ["BAT0", "BAT1", "BAT2", "BAT3", "BATC", "BATT"] {
1373 let sensor = SysfsSensor::battery(name);
1374 if sensor.is_available() {
1375 sensors.push(sensor);
1376 }
1377 }
1378 sensors
1379}
1380
1381#[must_use]
1385pub fn linux_hardware_bus() -> SensorimotorBus {
1386 let mut bus = SensorimotorBus::new(256);
1387
1388 for sensor in discover_thermal_sensors() {
1390 bus.register_sensor(Box::new(sensor));
1391 }
1392
1393 for sensor in discover_battery_sensors() {
1395 bus.register_sensor(Box::new(sensor));
1396 }
1397
1398 if cfg!(target_os = "linux") {
1400 if std::path::Path::new("/proc/loadavg").exists() {
1401 bus.register_sensor(Box::new(ProcfsSensor::loadavg()));
1402 }
1403 if std::path::Path::new("/proc/meminfo").exists() {
1404 bus.register_sensor(Box::new(ProcfsSensor::mem_pressure()));
1405 }
1406 if std::path::Path::new("/proc/stat").exists() {
1407 bus.register_sensor(Box::new(CpuUsageSensor::new()));
1408 }
1409 if std::path::Path::new("/proc/net/dev").exists() {
1410 bus.register_sensor(Box::new(NetworkThroughputSensor::default_interface()));
1411 }
1412 }
1413
1414 if std::path::Path::new("/").exists() {
1416 bus.register_sensor(Box::new(DiskUsageSensor::root()));
1417 }
1418
1419 for core in 0..16 {
1421 let sensor = CpuFreqSensor::new(core);
1422 if sensor.is_available() {
1423 bus.register_sensor(Box::new(sensor));
1424 }
1425 }
1426
1427 for actuator in discover_fan_actuators() {
1429 bus.register_actuator(Box::new(actuator));
1430 }
1431
1432 for actuator in discover_led_actuators() {
1434 bus.register_actuator(Box::new(actuator));
1435 }
1436
1437 bus
1438}
1439
1440fn now_secs() -> f64 {
1444 std::time::SystemTime::now()
1445 .duration_since(std::time::UNIX_EPOCH)
1446 .map_or(0.0, |d| d.as_secs_f64())
1447}
1448
1449#[cfg(test)]
1452mod tests {
1453 use super::*;
1454
1455 #[test]
1456 fn sensor_kind_as_str() {
1457 assert_eq!(SensorKind::Temperature.as_str(), "temperature");
1458 assert_eq!(SensorKind::Imu.as_str(), "imu");
1459 assert_eq!(SensorKind::Camera.as_str(), "camera");
1460 }
1461
1462 #[test]
1463 fn actuator_kind_as_str() {
1464 assert_eq!(ActuatorKind::Motor.as_str(), "motor");
1465 assert_eq!(ActuatorKind::Relay.as_str(), "relay");
1466 }
1467
1468 #[test]
1469 fn sensor_reading_new() {
1470 let r = SensorReading::new("cpu_temp", SensorKind::Temperature, 55.0);
1471 assert_eq!(r.sensor_id, "cpu_temp");
1472 assert_eq!(r.kind, SensorKind::Temperature);
1473 assert!((r.value - 55.0).abs() < f64::EPSILON);
1474 assert!((r.confidence - 1.0).abs() < f64::EPSILON);
1475 assert!(r.extra.is_empty());
1476 }
1477
1478 #[test]
1479 fn sensor_reading_with_extra() {
1480 let r = SensorReading::new("imu0", SensorKind::Imu, 0.0).with_extra(vec![1.0, 2.0, 3.0]);
1481 assert_eq!(r.extra, vec![1.0, 2.0, 3.0]);
1482 }
1483
1484 #[test]
1485 fn sensor_reading_with_confidence() {
1486 let r = SensorReading::new("cam0", SensorKind::Camera, 128.0).with_confidence(0.8);
1487 assert!((r.confidence - 0.8).abs() < f64::EPSILON);
1488 }
1489
1490 #[test]
1491 fn sensor_reading_confidence_clamped() {
1492 let r = SensorReading::new("s0", SensorKind::Custom, 0.0).with_confidence(1.5);
1493 assert!((r.confidence - 1.0).abs() < f64::EPSILON);
1494 }
1495
1496 #[test]
1497 fn actuator_command_new() {
1498 let c = ActuatorCommand::new("motor_l", ActuatorKind::Motor, 0.5);
1499 assert_eq!(c.actuator_id, "motor_l");
1500 assert_eq!(c.kind, ActuatorKind::Motor);
1501 assert!((c.value - 0.5).abs() < f64::EPSILON);
1502 }
1503
1504 #[test]
1505 fn actuator_command_with_params() {
1506 let c =
1507 ActuatorCommand::new("motor_r", ActuatorKind::Motor, 1.0).with_params(vec![0.1, 2.0]);
1508 assert_eq!(c.params, vec![0.1, 2.0]);
1509 }
1510
1511 #[test]
1512 fn stub_sensor_read() {
1513 let s = StubSensor::new("temp0", SensorKind::Temperature, 42.0);
1514 assert_eq!(s.id(), "temp0");
1515 assert_eq!(s.kind(), SensorKind::Temperature);
1516 let r = s.read().unwrap();
1517 assert!((r.value - 42.0).abs() < f64::EPSILON);
1518 }
1519
1520 #[test]
1521 fn stub_actuator_command() {
1522 let a = StubActuator::new("motor0", ActuatorKind::Motor);
1523 let cmd = ActuatorCommand::new("motor0", ActuatorKind::Motor, 0.7);
1524 a.command(&cmd).unwrap();
1525 let last = a.last_command().unwrap();
1526 assert!((last.value - 0.7).abs() < f64::EPSILON);
1527 }
1528
1529 #[test]
1530 fn bus_register_and_poll() {
1531 let mut bus = SensorimotorBus::new(64);
1532 bus.register_sensor(Box::new(StubSensor::new(
1533 "s1",
1534 SensorKind::Temperature,
1535 50.0,
1536 )));
1537 bus.register_sensor(Box::new(StubSensor::new("s2", SensorKind::Distance, 1.5)));
1538
1539 assert_eq!(bus.sensor_count(), 2);
1540 let readings = bus.poll_all();
1541 assert_eq!(readings.len(), 2);
1542 assert_eq!(bus.readings_collected(), 2);
1543 }
1544
1545 #[test]
1546 fn bus_send_command() {
1547 let mut bus = SensorimotorBus::new(64);
1548 bus.register_actuator(Box::new(StubActuator::new("m1", ActuatorKind::Motor)));
1549
1550 let cmd = ActuatorCommand::new("m1", ActuatorKind::Motor, 0.5);
1551 bus.send_command(&cmd).unwrap();
1552 assert_eq!(bus.commands_sent(), 1);
1553 }
1554
1555 #[test]
1556 fn bus_command_unknown_actuator() {
1557 let mut bus = SensorimotorBus::new(64);
1558 let cmd = ActuatorCommand::new("unknown", ActuatorKind::Motor, 0.0);
1559 assert!(bus.send_command(&cmd).is_err());
1560 }
1561
1562 #[test]
1563 fn bus_history() {
1564 let mut bus = SensorimotorBus::new(3);
1565 bus.register_sensor(Box::new(StubSensor::new(
1566 "s1",
1567 SensorKind::Temperature,
1568 50.0,
1569 )));
1570
1571 bus.poll_all();
1572 bus.poll_all();
1573 bus.poll_all();
1574 bus.poll_all(); let h = bus.history();
1577 assert_eq!(h.len(), 3);
1578 }
1579
1580 #[test]
1581 fn bus_sensor_ids() {
1582 let mut bus = SensorimotorBus::new(64);
1583 bus.register_sensor(Box::new(StubSensor::new("a", SensorKind::Temperature, 0.0)));
1584 bus.register_sensor(Box::new(StubSensor::new("b", SensorKind::Imu, 0.0)));
1585
1586 let ids = bus.sensor_ids();
1587 assert!(ids.contains(&"a".to_string()));
1588 assert!(ids.contains(&"b".to_string()));
1589 }
1590
1591 #[test]
1592 fn bus_actuator_ids() {
1593 let mut bus = SensorimotorBus::new(64);
1594 bus.register_actuator(Box::new(StubActuator::new("m1", ActuatorKind::Motor)));
1595
1596 let ids = bus.actuator_ids();
1597 assert!(ids.contains(&"m1".to_string()));
1598 }
1599
1600 #[test]
1601 fn bus_e_stop_all() {
1602 let mut bus = SensorimotorBus::new(64);
1603 bus.register_actuator(Box::new(StubActuator::new("m1", ActuatorKind::Motor)));
1604 bus.register_actuator(Box::new(StubActuator::new("m2", ActuatorKind::Motor)));
1605
1606 let errors = bus.e_stop_all();
1607 assert!(errors.is_empty()); }
1609
1610 #[test]
1611 fn reflex_rule_above() {
1612 let rule = ReflexRule::above("temp0", "fan0", ActuatorKind::Motor, 70.0, 1.0, 5.0);
1613 let reading_high = SensorReading::new("temp0", SensorKind::Temperature, 75.0);
1614 let reading_low = SensorReading::new("temp0", SensorKind::Temperature, 60.0);
1615
1616 assert!(rule.is_triggered(&reading_high));
1617 assert!(!rule.is_triggered(&reading_low));
1618 }
1619
1620 #[test]
1621 fn reflex_rule_below() {
1622 let rule = ReflexRule::below("battery", "led", ActuatorKind::Display, 20.0, 1.0, 10.0);
1623 let reading_low = SensorReading::new("battery", SensorKind::Power, 15.0);
1624 let reading_ok = SensorReading::new("battery", SensorKind::Power, 80.0);
1625
1626 assert!(rule.is_triggered(&reading_low));
1627 assert!(!rule.is_triggered(&reading_ok));
1628 }
1629
1630 #[test]
1631 fn reflex_loop_evaluate() {
1632 let mut loop_ = ReflexLoop::new();
1633 loop_.add_rule(ReflexRule::above(
1634 "temp0",
1635 "fan0",
1636 ActuatorKind::Motor,
1637 70.0,
1638 1.0,
1639 0.0, ));
1641
1642 let readings = vec![SensorReading::new("temp0", SensorKind::Temperature, 75.0)];
1643 let commands = loop_.evaluate(&readings);
1644 assert_eq!(commands.len(), 1);
1645 assert_eq!(commands[0].actuator_id, "fan0");
1646 }
1647
1648 #[test]
1649 fn reflex_loop_no_trigger() {
1650 let mut loop_ = ReflexLoop::new();
1651 loop_.add_rule(ReflexRule::above(
1652 "temp0",
1653 "fan0",
1654 ActuatorKind::Motor,
1655 70.0,
1656 1.0,
1657 0.0,
1658 ));
1659
1660 let readings = vec![SensorReading::new("temp0", SensorKind::Temperature, 60.0)];
1661 let commands = loop_.evaluate(&readings);
1662 assert!(commands.is_empty());
1663 }
1664
1665 #[test]
1666 fn reflex_loop_cooldown() {
1667 let mut loop_ = ReflexLoop::new();
1668 loop_.add_rule(ReflexRule::above(
1669 "temp0",
1670 "fan0",
1671 ActuatorKind::Motor,
1672 70.0,
1673 1.0,
1674 100.0, ));
1676
1677 let readings = vec![SensorReading::new("temp0", SensorKind::Temperature, 80.0)];
1678
1679 let cmds1 = loop_.evaluate(&readings);
1681 assert_eq!(cmds1.len(), 1);
1682
1683 let cmds2 = loop_.evaluate(&readings);
1685 assert!(cmds2.is_empty());
1686 }
1687
1688 #[test]
1689 fn reflex_loop_multiple_rules() {
1690 let mut loop_ = ReflexLoop::new();
1691 loop_.add_rule(ReflexRule::above(
1692 "temp0",
1693 "fan0",
1694 ActuatorKind::Motor,
1695 70.0,
1696 1.0,
1697 0.0,
1698 ));
1699 loop_.add_rule(ReflexRule::below(
1700 "battery",
1701 "led0",
1702 ActuatorKind::Display,
1703 20.0,
1704 1.0,
1705 0.0,
1706 ));
1707
1708 let readings = vec![
1709 SensorReading::new("temp0", SensorKind::Temperature, 75.0),
1710 SensorReading::new("battery", SensorKind::Power, 15.0),
1711 ];
1712
1713 let commands = loop_.evaluate(&readings);
1714 assert_eq!(commands.len(), 2);
1715 }
1716
1717 #[test]
1718 fn reflex_loop_rule_count() {
1719 let mut loop_ = ReflexLoop::new();
1720 assert_eq!(loop_.rule_count(), 0);
1721 loop_.add_rule(ReflexRule::above(
1722 "s",
1723 "a",
1724 ActuatorKind::Motor,
1725 1.0,
1726 1.0,
1727 1.0,
1728 ));
1729 assert_eq!(loop_.rule_count(), 1);
1730 }
1731
1732 #[test]
1733 fn bus_default() {
1734 let bus = SensorimotorBus::default();
1735 assert_eq!(bus.sensor_count(), 0);
1736 assert_eq!(bus.actuator_count(), 0);
1737 }
1738
1739 #[test]
1740 fn bus_read_specific_sensor() {
1741 let mut bus = SensorimotorBus::new(64);
1742 bus.register_sensor(Box::new(StubSensor::new(
1743 "s1",
1744 SensorKind::Temperature,
1745 42.0,
1746 )));
1747
1748 let r = bus.read_sensor("s1").unwrap();
1749 assert!((r.value - 42.0).abs() < f64::EPSILON);
1750
1751 assert!(bus.read_sensor("nonexistent").is_none());
1752 }
1753
1754 #[test]
1755 fn sensor_kind_display() {
1756 assert_eq!(format!("{}", SensorKind::Temperature), "temperature");
1757 assert_eq!(format!("{}", ActuatorKind::Motor), "motor");
1758 }
1759
1760 #[test]
1763 fn sysfs_sensor_thermal_construction() {
1764 let sensor = SysfsSensor::thermal(0);
1765 assert_eq!(sensor.id(), "thermal_zone0");
1766 assert_eq!(sensor.kind(), SensorKind::Temperature);
1767 }
1768
1769 #[test]
1770 fn sysfs_sensor_battery_construction() {
1771 let sensor = SysfsSensor::battery("BAT0");
1772 assert_eq!(sensor.id(), "battery_BAT0");
1773 assert_eq!(sensor.kind(), SensorKind::Power);
1774 }
1775
1776 #[test]
1777 fn sysfs_sensor_fan_construction() {
1778 let sensor = SysfsSensor::fan(0, 1);
1779 assert_eq!(sensor.id(), "fan0_1");
1780 assert_eq!(sensor.kind(), SensorKind::Custom);
1781 }
1782
1783 #[test]
1784 fn sysfs_sensor_nonexistent_path() {
1785 let sensor = SysfsSensor::new(
1786 "test",
1787 SensorKind::Temperature,
1788 "/nonexistent/path/that/does/not/exist",
1789 1000.0,
1790 );
1791 assert!(!sensor.is_available());
1792 assert!(sensor.read().is_none());
1793 }
1794
1795 #[test]
1796 fn sysfs_sensor_debug_format() {
1797 let sensor = SysfsSensor::thermal(0);
1798 let debug = format!("{sensor:?}");
1799 assert!(debug.contains("SysfsSensor"));
1800 assert!(debug.contains("thermal_zone0"));
1801 }
1802
1803 #[test]
1806 fn procfs_sensor_loadavg_construction() {
1807 let sensor = ProcfsSensor::loadavg();
1808 assert_eq!(sensor.id(), "cpu_loadavg");
1809 assert_eq!(sensor.kind(), SensorKind::Custom);
1810 }
1811
1812 #[test]
1813 fn procfs_sensor_mem_pressure_construction() {
1814 let sensor = ProcfsSensor::mem_pressure();
1815 assert_eq!(sensor.id(), "mem_pressure");
1816 assert_eq!(sensor.kind(), SensorKind::Custom);
1817 }
1818
1819 #[test]
1820 fn procfs_sensor_nonexistent_path() {
1821 let sensor = ProcfsSensor::new(
1822 "test",
1823 SensorKind::Custom,
1824 "/nonexistent/proc/path",
1825 Box::new(|_| Some(1.0)),
1826 );
1827 assert!(!sensor.is_available());
1828 assert!(sensor.read().is_none());
1829 }
1830
1831 #[test]
1832 fn procfs_sensor_debug_format() {
1833 let sensor = ProcfsSensor::loadavg();
1834 let debug = format!("{sensor:?}");
1835 assert!(debug.contains("ProcfsSensor"));
1836 assert!(debug.contains("cpu_loadavg"));
1837 }
1838
1839 #[test]
1840 fn procfs_sensor_custom_parser() {
1841 let sensor = ProcfsSensor::new(
1842 "test_parser",
1843 SensorKind::Custom,
1844 "/proc/loadavg",
1845 Box::new(|content: &str| {
1846 content.split_whitespace().next().and_then(|s| {
1847 let v: f64 = s.parse().ok()?;
1848 Some(v * 2.0)
1849 })
1850 }),
1851 );
1852 if sensor.is_available() {
1854 let reading = sensor.read().unwrap();
1855 assert!(reading.value > 0.0);
1856 }
1857 }
1858
1859 #[test]
1862 fn discover_thermal_sensors_returns_vec() {
1863 let sensors = discover_thermal_sensors();
1864 for s in &sensors {
1867 assert!(s.is_available());
1868 assert_eq!(s.kind(), SensorKind::Temperature);
1869 }
1870 }
1871
1872 #[test]
1873 fn discover_battery_sensors_returns_vec() {
1874 let sensors = discover_battery_sensors();
1875 for s in &sensors {
1876 assert!(s.is_available());
1877 assert_eq!(s.kind(), SensorKind::Power);
1878 }
1879 }
1880
1881 #[test]
1884 fn linux_hardware_bus_creation() {
1885 let bus = linux_hardware_bus();
1886 let sensor_ids = bus.sensor_ids();
1888 assert!(bus.sensor_count() <= 50); for id in &sensor_ids {
1891 assert!(!id.is_empty());
1892 }
1893 }
1894
1895 #[test]
1896 fn linux_hardware_bus_poll_all() {
1897 let mut bus = linux_hardware_bus();
1898 let readings = bus.poll_all();
1899 for r in &readings {
1901 assert!(!r.sensor_id.is_empty());
1902 }
1903 }
1904
1905 #[test]
1908 fn parse_proc_kb_extracts_value() {
1909 assert_eq!(
1910 parse_proc_kb("MemTotal: 16384000 kB"),
1911 Some(16_384_000)
1912 );
1913 assert_eq!(parse_proc_kb("MemAvailable: 8192000 kB"), Some(8_192_000));
1914 assert_eq!(parse_proc_kb("garbage"), None);
1915 }
1916
1917 #[test]
1920 fn cpu_usage_sensor_construction() {
1921 let sensor = CpuUsageSensor::new();
1922 assert_eq!(sensor.id(), "cpu_usage");
1923 assert_eq!(sensor.kind(), SensorKind::Custom);
1924 }
1925
1926 #[test]
1927 fn cpu_usage_sensor_debug() {
1928 let sensor = CpuUsageSensor::new();
1929 let debug = format!("{sensor:?}");
1930 assert!(debug.contains("CpuUsageSensor"));
1931 }
1932
1933 #[test]
1934 fn cpu_usage_sensor_read_on_linux() {
1935 if !std::path::Path::new("/proc/stat").exists() {
1936 return;
1937 }
1938 let sensor = CpuUsageSensor::new();
1939 assert!(sensor.is_available());
1940 let reading = sensor.read().unwrap();
1941 assert_eq!(reading.sensor_id, "cpu_usage");
1942 assert!(reading.value >= 0.0 && reading.value <= 100.0);
1943 }
1944
1945 #[test]
1948 fn disk_usage_sensor_construction() {
1949 let sensor = DiskUsageSensor::root();
1950 assert_eq!(sensor.id(), "disk_root");
1951 assert_eq!(sensor.kind(), SensorKind::Custom);
1952 }
1953
1954 #[test]
1955 fn disk_usage_sensor_nonexistent() {
1956 let sensor = DiskUsageSensor::new("test", "/nonexistent/mount/point");
1957 assert!(!sensor.is_available());
1958 assert!(sensor.read().is_none());
1959 }
1960
1961 #[test]
1962 fn disk_usage_sensor_debug() {
1963 let sensor = DiskUsageSensor::root();
1964 let debug = format!("{sensor:?}");
1965 assert!(debug.contains("DiskUsageSensor"));
1966 }
1967
1968 #[test]
1971 fn network_sensor_construction() {
1972 let sensor = NetworkThroughputSensor::new("eth0");
1973 assert_eq!(sensor.id(), "net_eth0");
1974 assert_eq!(sensor.kind(), SensorKind::Custom);
1975 }
1976
1977 #[test]
1978 fn network_sensor_default_interface() {
1979 let sensor = NetworkThroughputSensor::default_interface();
1980 assert!(sensor.id().starts_with("net_"));
1981 }
1982
1983 #[test]
1984 fn network_sensor_debug() {
1985 let sensor = NetworkThroughputSensor::new("wlan0");
1986 let debug = format!("{sensor:?}");
1987 assert!(debug.contains("NetworkThroughputSensor"));
1988 assert!(debug.contains("wlan0"));
1989 }
1990
1991 #[test]
1992 fn network_sensor_read_on_linux() {
1993 if !std::path::Path::new("/proc/net/dev").exists() {
1994 return;
1995 }
1996 let sensor = NetworkThroughputSensor::default_interface();
1997 assert!(sensor.is_available());
1998 let reading = sensor.read();
1999 if let Some(r) = reading {
2001 assert!(r.value >= 0.0);
2002 }
2003 }
2004
2005 #[test]
2008 fn cpu_freq_sensor_construction() {
2009 let sensor = CpuFreqSensor::new(0);
2010 assert_eq!(sensor.id(), "cpu0_freq");
2011 assert_eq!(sensor.kind(), SensorKind::Custom);
2012 }
2013
2014 #[test]
2015 fn cpu_freq_sensor_nonexistent() {
2016 let sensor = CpuFreqSensor::new(999);
2017 assert!(!sensor.is_available());
2018 assert!(sensor.read().is_none());
2019 }
2020
2021 #[test]
2022 fn cpu_freq_sensor_debug() {
2023 let sensor = CpuFreqSensor::new(0);
2024 let debug = format!("{sensor:?}");
2025 assert!(debug.contains("CpuFreqSensor"));
2026 }
2027
2028 #[test]
2031 fn sysfs_actuator_construction() {
2032 let actuator = SysfsActuator::fan_pwm(0, 1);
2033 assert_eq!(actuator.id(), "fan_pwm0_1");
2034 assert_eq!(actuator.kind(), ActuatorKind::Motor);
2035 }
2036
2037 #[test]
2038 fn sysfs_actuator_led_construction() {
2039 let actuator = SysfsActuator::led("power");
2040 assert_eq!(actuator.id(), "led_power");
2041 assert_eq!(actuator.kind(), ActuatorKind::Display);
2042 }
2043
2044 #[test]
2045 fn sysfs_actuator_nonexistent_path() {
2046 let actuator = SysfsActuator::new(
2047 "test",
2048 ActuatorKind::Motor,
2049 "/nonexistent/path/that/does/not/exist",
2050 1.0,
2051 );
2052 assert!(!actuator.is_available());
2053 let cmd = ActuatorCommand::new("test", ActuatorKind::Motor, 1.0);
2054 assert!(actuator.command(&cmd).is_err());
2055 }
2056
2057 #[test]
2058 fn sysfs_actuator_e_stop_nonexistent() {
2059 let actuator = SysfsActuator::new("test", ActuatorKind::Motor, "/nonexistent/path", 1.0);
2060 assert!(actuator.e_stop().is_err());
2061 }
2062
2063 #[test]
2064 fn sysfs_actuator_debug() {
2065 let actuator = SysfsActuator::fan_pwm(0, 1);
2066 let debug = format!("{actuator:?}");
2067 assert!(debug.contains("SysfsActuator"));
2068 assert!(debug.contains("fan_pwm0_1"));
2069 }
2070
2071 #[test]
2072 fn sysfs_actuator_scale_applied() {
2073 let tmp = tempfile::NamedTempFile::new().unwrap();
2075 let path = tmp.path().to_str().unwrap();
2076 let actuator = SysfsActuator::new("test", ActuatorKind::Display, path, 255.0);
2077 assert!(actuator.is_available());
2078 let cmd = ActuatorCommand::new("test", ActuatorKind::Display, 0.5);
2079 actuator.command(&cmd).unwrap();
2080 let written = std::fs::read_to_string(path).unwrap();
2081 assert_eq!(written.trim(), "128"); }
2083
2084 #[test]
2087 fn discover_fan_actuators_returns_vec() {
2088 let actuators = discover_fan_actuators();
2089 for a in &actuators {
2090 assert!(a.is_available());
2091 assert_eq!(a.kind(), ActuatorKind::Motor);
2092 }
2093 }
2094
2095 #[test]
2096 fn discover_led_actuators_returns_vec() {
2097 let actuators = discover_led_actuators();
2098 for a in &actuators {
2099 assert!(a.is_available());
2100 assert_eq!(a.kind(), ActuatorKind::Display);
2101 }
2102 }
2103
2104 #[test]
2107 fn linux_hardware_bus_includes_new_sensors() {
2108 let bus = linux_hardware_bus();
2109 let ids = bus.sensor_ids();
2110 if std::path::Path::new("/proc/stat").exists() {
2112 assert!(ids.iter().any(|id| id == "cpu_usage"));
2113 }
2114 assert!(ids.iter().any(|id| id == "disk_root"));
2116 }
2117
2118 #[test]
2119 fn linux_hardware_bus_includes_actuators() {
2120 let bus = linux_hardware_bus();
2121 let actuator_ids = bus.actuator_ids();
2123 for id in &actuator_ids {
2124 assert!(!id.is_empty());
2125 }
2126 }
2127}