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 max_reading_age_secs: f64,
559}
560
561impl ReflexLoop {
562 pub const DEFAULT_MAX_READING_AGE_SECS: f64 = 5.0;
567
568 #[must_use]
570 pub fn new() -> Self {
571 Self {
572 rules: Vec::new(),
573 last_trigger: HashMap::new(),
574 max_reading_age_secs: Self::DEFAULT_MAX_READING_AGE_SECS,
575 }
576 }
577
578 #[must_use]
583 pub const fn with_max_reading_age(mut self, secs: f64) -> Self {
584 self.max_reading_age_secs = secs;
585 self
586 }
587
588 fn reading_is_fresh(&self, reading: &SensorReading, now: f64) -> bool {
593 if self.max_reading_age_secs <= 0.0 {
594 return true;
595 }
596 let age = now - reading.timestamp;
597 age.is_finite() && (0.0..=self.max_reading_age_secs).contains(&age)
598 }
599
600 pub fn add_rule(&mut self, rule: ReflexRule) {
602 self.rules.push(rule);
603 }
604
605 pub fn evaluate(&mut self, readings: &[SensorReading]) -> Vec<ActuatorCommand> {
608 let mut commands = Vec::new();
609 let now = Instant::now();
610 let wall_now = now_secs();
611
612 for rule in &self.rules {
613 if let Some(&last) = self.last_trigger.get(&rule.sensor_id) {
615 let elapsed = now.duration_since(last).as_secs_f64();
616 if elapsed < rule.cooldown_secs {
617 continue;
618 }
619 }
620
621 if let Some(reading) = readings.iter().find(|r| r.sensor_id == rule.sensor_id) {
623 if !self.reading_is_fresh(reading, wall_now) {
624 continue;
625 }
626 if rule.is_triggered(reading) {
627 commands.push(ActuatorCommand::new(
628 &rule.actuator_id,
629 rule.actuator_kind,
630 rule.command_value,
631 ));
632 self.last_trigger.insert(rule.sensor_id.clone(), now);
633 }
634 }
635 }
636
637 commands
638 }
639
640 #[must_use]
642 pub fn rule_count(&self) -> usize {
643 self.rules.len()
644 }
645}
646
647impl Default for ReflexLoop {
648 fn default() -> Self {
649 Self::new()
650 }
651}
652
653pub struct SysfsSensor {
665 id: String,
666 kind: SensorKind,
667 path: String,
668 scale: f64,
669 available: bool,
670}
671
672impl SysfsSensor {
673 #[must_use]
678 pub fn new(
679 id: impl Into<String>,
680 kind: SensorKind,
681 path: impl Into<String>,
682 scale: f64,
683 ) -> Self {
684 let path = path.into();
685 let available = std::path::Path::new(&path).exists();
686 Self {
687 id: id.into(),
688 kind,
689 path,
690 scale,
691 available,
692 }
693 }
694
695 #[must_use]
697 pub fn thermal(zone: usize) -> Self {
698 Self::new(
699 format!("thermal_zone{zone}"),
700 SensorKind::Temperature,
701 format!("/sys/class/thermal/thermal_zone{zone}/temp"),
702 1000.0,
703 )
704 }
705
706 #[must_use]
708 pub fn battery(name: &str) -> Self {
709 Self::new(
710 format!("battery_{name}"),
711 SensorKind::Power,
712 format!("/sys/class/power_supply/{name}/capacity"),
713 1.0,
714 )
715 }
716
717 #[must_use]
719 pub fn fan(hwmon: usize, fan: usize) -> Self {
720 Self::new(
721 format!("fan{hwmon}_{fan}"),
722 SensorKind::Custom,
723 format!("/sys/class/hwmon/hwmon{hwmon}/fan{fan}_input"),
724 1.0,
725 )
726 }
727}
728
729impl SensorDevice for SysfsSensor {
730 fn id(&self) -> &str {
731 &self.id
732 }
733
734 fn kind(&self) -> SensorKind {
735 self.kind
736 }
737
738 fn read(&self) -> Option<SensorReading> {
739 if !self.available {
740 return None;
741 }
742 let raw = std::fs::read_to_string(&self.path).ok()?;
743 let trimmed = raw.trim();
744 let value: f64 = trimmed.parse().ok()?;
745 let scaled = if self.scale > 0.0 {
746 value / self.scale
747 } else {
748 value
749 };
750 Some(SensorReading::new(&self.id, self.kind, scaled))
751 }
752
753 fn is_available(&self) -> bool {
754 self.available
755 }
756}
757
758impl std::fmt::Debug for SysfsSensor {
759 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
760 f.debug_struct("SysfsSensor")
761 .field("id", &self.id)
762 .field("kind", &self.kind)
763 .field("path", &self.path)
764 .field("scale", &self.scale)
765 .field("available", &self.available)
766 .finish()
767 }
768}
769
770pub type SensorParser = Box<dyn Fn(&str) -> Option<f64> + Send + Sync>;
772
773pub struct ProcfsSensor {
781 id: String,
782 kind: SensorKind,
783 path: String,
784 parser: SensorParser,
786 available: bool,
787}
788
789impl ProcfsSensor {
790 #[must_use]
792 pub fn new(
793 id: impl Into<String>,
794 kind: SensorKind,
795 path: impl Into<String>,
796 parser: SensorParser,
797 ) -> Self {
798 let path = path.into();
799 let available = std::path::Path::new(&path).exists();
800 Self {
801 id: id.into(),
802 kind,
803 path,
804 parser,
805 available,
806 }
807 }
808
809 #[must_use]
811 pub fn loadavg() -> Self {
812 Self::new(
813 "cpu_loadavg",
814 SensorKind::Custom,
815 "/proc/loadavg",
816 Box::new(|content: &str| {
817 content
818 .split_whitespace()
819 .next()
820 .and_then(|s| s.parse::<f64>().ok())
821 }),
822 )
823 }
824
825 #[must_use]
829 pub fn mem_pressure() -> Self {
830 Self::new(
831 "mem_pressure",
832 SensorKind::Custom,
833 "/proc/meminfo",
834 Box::new(|content: &str| {
835 let mut mem_total = None;
836 let mut mem_avail = None;
837 for line in content.lines() {
838 if line.starts_with("MemTotal:") {
839 mem_total = parse_proc_kb(line);
840 } else if line.starts_with("MemAvailable:") {
841 mem_avail = parse_proc_kb(line);
842 }
843 }
844 match (mem_total, mem_avail) {
845 (Some(total), Some(avail)) if total > 0 => {
846 Some(1.0 - (avail as f64 / total as f64).min(1.0))
847 }
848 _ => None,
849 }
850 }),
851 )
852 }
853}
854
855impl SensorDevice for ProcfsSensor {
856 fn id(&self) -> &str {
857 &self.id
858 }
859
860 fn kind(&self) -> SensorKind {
861 self.kind
862 }
863
864 fn read(&self) -> Option<SensorReading> {
865 if !self.available {
866 return None;
867 }
868 let content = std::fs::read_to_string(&self.path).ok()?;
869 let value = (self.parser)(&content)?;
870 Some(SensorReading::new(&self.id, self.kind, value))
871 }
872
873 fn is_available(&self) -> bool {
874 self.available
875 }
876}
877
878impl std::fmt::Debug for ProcfsSensor {
879 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
880 f.debug_struct("ProcfsSensor")
881 .field("id", &self.id)
882 .field("kind", &self.kind)
883 .field("path", &self.path)
884 .field("available", &self.available)
885 .finish_non_exhaustive()
886 }
887}
888
889fn parse_proc_kb(line: &str) -> Option<u64> {
891 line.split(':')
892 .nth(1)
893 .and_then(|s| s.split_whitespace().next())
894 .and_then(|s| s.parse::<u64>().ok())
895}
896
897pub struct CpuUsageSensor {
905 prev_idle: Option<u64>,
906 prev_total: Option<u64>,
907}
908
909impl CpuUsageSensor {
910 #[must_use]
911 pub const fn new() -> Self {
912 Self {
913 prev_idle: None,
914 prev_total: None,
915 }
916 }
917}
918
919impl Default for CpuUsageSensor {
920 fn default() -> Self {
921 Self::new()
922 }
923}
924
925impl SensorDevice for CpuUsageSensor {
926 fn id(&self) -> &str {
927 "cpu_usage"
928 }
929
930 fn kind(&self) -> SensorKind {
931 SensorKind::Custom
932 }
933
934 fn read(&self) -> Option<SensorReading> {
935 let content = std::fs::read_to_string("/proc/stat").ok()?;
936 let first_line = content.lines().next()?;
937 if !first_line.starts_with("cpu ") {
938 return None;
939 }
940 let fields: Vec<u64> = first_line
941 .split_whitespace()
942 .skip(1)
943 .filter_map(|s| s.parse::<u64>().ok())
944 .collect();
945 if fields.len() < 4 {
946 return None;
947 }
948 let idle = fields[3];
949 let total: u64 = fields.iter().sum();
950 let usage = match (self.prev_idle, self.prev_total) {
951 (Some(prev_idle), Some(prev_total)) => {
952 let idle_delta = idle.saturating_sub(prev_idle) as f64;
953 let total_delta = total.saturating_sub(prev_total) as f64;
954 if total_delta > 0.0 {
955 ((1.0 - idle_delta / total_delta) * 100.0).clamp(0.0, 100.0)
956 } else {
957 0.0
958 }
959 }
960 _ => 0.0,
961 };
962 Some(SensorReading::new("cpu_usage", SensorKind::Custom, usage))
963 }
964
965 fn is_available(&self) -> bool {
966 std::path::Path::new("/proc/stat").exists()
967 }
968}
969
970impl std::fmt::Debug for CpuUsageSensor {
971 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
972 f.debug_struct("CpuUsageSensor")
973 .field("prev_idle", &self.prev_idle)
974 .field("prev_total", &self.prev_total)
975 .finish()
976 }
977}
978
979pub struct DiskUsageSensor {
986 id: String,
987 path: String,
988 available: bool,
989}
990
991impl DiskUsageSensor {
992 #[must_use]
993 pub fn new(id: impl Into<String>, path: impl Into<String>) -> Self {
994 let path = path.into();
995 let available = std::path::Path::new(&path).exists();
996 Self {
997 id: id.into(),
998 path,
999 available,
1000 }
1001 }
1002
1003 #[must_use]
1005 pub fn root() -> Self {
1006 Self::new("disk_root", "/")
1007 }
1008}
1009
1010impl SensorDevice for DiskUsageSensor {
1011 fn id(&self) -> &str {
1012 &self.id
1013 }
1014
1015 fn kind(&self) -> SensorKind {
1016 SensorKind::Custom
1017 }
1018
1019 fn read(&self) -> Option<SensorReading> {
1020 if !self.available {
1021 return None;
1022 }
1023 let output = std::process::Command::new("df")
1024 .arg("-P")
1025 .arg(&self.path)
1026 .output()
1027 .ok()?;
1028 let stdout = String::from_utf8(output.stdout).ok()?;
1029 let line = stdout.lines().nth(1)?;
1030 let fields: Vec<&str> = line.split_whitespace().collect();
1031 if fields.len() < 5 {
1032 return None;
1033 }
1034 let used: f64 = fields[2].parse().ok()?;
1035 let total: f64 = fields[1].parse().ok()?;
1036 if total > 0.0 {
1037 let pct = (used / total) * 100.0;
1038 Some(SensorReading::new(&self.id, SensorKind::Custom, pct))
1039 } else {
1040 None
1041 }
1042 }
1043
1044 fn is_available(&self) -> bool {
1045 self.available
1046 }
1047}
1048
1049impl std::fmt::Debug for DiskUsageSensor {
1050 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1051 f.debug_struct("DiskUsageSensor")
1052 .field("id", &self.id)
1053 .field("path", &self.path)
1054 .field("available", &self.available)
1055 .finish()
1056 }
1057}
1058
1059pub struct NetworkThroughputSensor {
1066 id: String,
1067 interface: String,
1068 prev_rx_bytes: Option<u64>,
1069 prev_tx_bytes: Option<u64>,
1070 prev_time: Option<Instant>,
1071}
1072
1073impl NetworkThroughputSensor {
1074 #[must_use]
1075 pub fn new(interface: impl Into<String>) -> Self {
1076 let iface = interface.into();
1077 Self {
1078 id: format!("net_{iface}"),
1079 interface: iface,
1080 prev_rx_bytes: None,
1081 prev_tx_bytes: None,
1082 prev_time: None,
1083 }
1084 }
1085
1086 #[must_use]
1089 pub fn default_interface() -> Self {
1090 let iface = if let Ok(content) = std::fs::read_to_string("/proc/net/dev") {
1091 content
1092 .lines()
1093 .skip(2)
1094 .find_map(|line| {
1095 let name = line.split(':').next()?.trim();
1096 if name != "lo" && !name.is_empty() {
1097 Some(name.to_string())
1098 } else {
1099 None
1100 }
1101 })
1102 .unwrap_or_else(|| "eth0".to_string())
1103 } else {
1104 "eth0".to_string()
1105 };
1106 Self::new(iface)
1107 }
1108}
1109
1110impl SensorDevice for NetworkThroughputSensor {
1111 fn id(&self) -> &str {
1112 &self.id
1113 }
1114
1115 fn kind(&self) -> SensorKind {
1116 SensorKind::Custom
1117 }
1118
1119 fn read(&self) -> Option<SensorReading> {
1120 let content = std::fs::read_to_string("/proc/net/dev").ok()?;
1121 for line in content.lines().skip(2) {
1122 let mut parts = line.split(':');
1123 let name = parts.next()?.trim();
1124 if name != self.interface {
1125 continue;
1126 }
1127 let stats: Vec<u64> = parts
1128 .next()?
1129 .split_whitespace()
1130 .filter_map(|s| s.parse::<u64>().ok())
1131 .collect();
1132 if stats.len() < 9 {
1133 return None;
1134 }
1135 let rx_bytes = stats[0];
1136 let tx_bytes = stats[8];
1137 let now = Instant::now();
1138 let throughput = match (self.prev_rx_bytes, self.prev_tx_bytes, self.prev_time) {
1139 (Some(prev_rx), Some(prev_tx), Some(prev_t)) => {
1140 let elapsed = now.duration_since(prev_t).as_secs_f64();
1141 if elapsed > 0.0 {
1142 let rx_delta = rx_bytes.saturating_sub(prev_rx) as f64;
1143 let tx_delta = tx_bytes.saturating_sub(prev_tx) as f64;
1144 (rx_delta + tx_delta) / elapsed
1145 } else {
1146 0.0
1147 }
1148 }
1149 _ => 0.0,
1150 };
1151 return Some(SensorReading::new(&self.id, SensorKind::Custom, throughput));
1152 }
1153 None
1154 }
1155
1156 fn is_available(&self) -> bool {
1157 std::path::Path::new("/proc/net/dev").exists()
1158 }
1159}
1160
1161impl std::fmt::Debug for NetworkThroughputSensor {
1162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1163 f.debug_struct("NetworkThroughputSensor")
1164 .field("id", &self.id)
1165 .field("interface", &self.interface)
1166 .finish_non_exhaustive()
1167 }
1168}
1169
1170pub struct CpuFreqSensor {
1177 id: String,
1178 path: String,
1179 available: bool,
1180}
1181
1182impl CpuFreqSensor {
1183 #[must_use]
1184 pub fn new(core: usize) -> Self {
1185 let path = format!("/sys/devices/system/cpu/cpu{core}/cpufreq/scaling_cur_freq");
1186 let available = std::path::Path::new(&path).exists();
1187 Self {
1188 id: format!("cpu{core}_freq"),
1189 path,
1190 available,
1191 }
1192 }
1193}
1194
1195impl SensorDevice for CpuFreqSensor {
1196 fn id(&self) -> &str {
1197 &self.id
1198 }
1199
1200 fn kind(&self) -> SensorKind {
1201 SensorKind::Custom
1202 }
1203
1204 fn read(&self) -> Option<SensorReading> {
1205 if !self.available {
1206 return None;
1207 }
1208 let raw = std::fs::read_to_string(&self.path).ok()?;
1209 let khz: f64 = raw.trim().parse().ok()?;
1210 Some(SensorReading::new(
1211 &self.id,
1212 SensorKind::Custom,
1213 khz / 1000.0,
1214 ))
1215 }
1216
1217 fn is_available(&self) -> bool {
1218 self.available
1219 }
1220}
1221
1222impl std::fmt::Debug for CpuFreqSensor {
1223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1224 f.debug_struct("CpuFreqSensor")
1225 .field("id", &self.id)
1226 .field("path", &self.path)
1227 .field("available", &self.available)
1228 .finish()
1229 }
1230}
1231
1232pub struct SysfsActuator {
1244 id: String,
1245 kind: ActuatorKind,
1246 path: String,
1247 scale: f64,
1248 available: bool,
1249}
1250
1251impl SysfsActuator {
1252 #[must_use]
1257 pub fn new(
1258 id: impl Into<String>,
1259 kind: ActuatorKind,
1260 path: impl Into<String>,
1261 scale: f64,
1262 ) -> Self {
1263 let path = path.into();
1264 let available = std::path::Path::new(&path).exists();
1265 Self {
1266 id: id.into(),
1267 kind,
1268 path,
1269 scale,
1270 available,
1271 }
1272 }
1273
1274 #[must_use]
1276 pub fn fan_pwm(hwmon: usize, pwm: usize) -> Self {
1277 Self::new(
1278 format!("fan_pwm{hwmon}_{pwm}"),
1279 ActuatorKind::Motor,
1280 format!("/sys/class/hwmon/hwmon{hwmon}/pwm{pwm}"),
1281 1.0,
1282 )
1283 }
1284
1285 #[must_use]
1287 pub fn led(name: &str) -> Self {
1288 Self::new(
1289 format!("led_{name}"),
1290 ActuatorKind::Display,
1291 format!("/sys/class/leds/{name}/brightness"),
1292 1.0,
1293 )
1294 }
1295}
1296
1297impl ActuatorDevice for SysfsActuator {
1298 fn id(&self) -> &str {
1299 &self.id
1300 }
1301
1302 fn kind(&self) -> ActuatorKind {
1303 self.kind
1304 }
1305
1306 fn command(&self, cmd: &ActuatorCommand) -> Result<(), String> {
1307 if !self.available {
1308 return Err(format!("actuator '{}' path not available", self.id));
1309 }
1310 let raw_value = cmd.value * self.scale;
1311 let output = format!("{}", raw_value.round() as i64);
1312 std::fs::write(&self.path, output)
1313 .map_err(|e| format!("failed to write to {}: {e}", self.path))
1314 }
1315
1316 fn is_available(&self) -> bool {
1317 self.available
1318 }
1319
1320 fn e_stop(&self) -> Result<(), String> {
1321 if !self.available {
1322 return Err(format!("actuator '{}' path not available", self.id));
1323 }
1324 std::fs::write(&self.path, "0").map_err(|e| format!("failed to e-stop {}: {e}", self.path))
1326 }
1327}
1328
1329impl std::fmt::Debug for SysfsActuator {
1330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1331 f.debug_struct("SysfsActuator")
1332 .field("id", &self.id)
1333 .field("kind", &self.kind)
1334 .field("path", &self.path)
1335 .field("scale", &self.scale)
1336 .field("available", &self.available)
1337 .finish()
1338 }
1339}
1340
1341#[must_use]
1347pub fn discover_fan_actuators() -> Vec<SysfsActuator> {
1348 let mut actuators = Vec::new();
1349 for hwmon in 0..8 {
1350 for pwm in 1..=4 {
1351 let actuator = SysfsActuator::fan_pwm(hwmon, pwm);
1352 if actuator.is_available() {
1353 actuators.push(actuator);
1354 }
1355 }
1356 }
1357 actuators
1358}
1359
1360#[must_use]
1364pub fn discover_led_actuators() -> Vec<SysfsActuator> {
1365 let mut actuators = Vec::new();
1366 for name in [
1367 "input0::scrolllock",
1368 "input0::numlock",
1369 "input0::capslock",
1370 "mmc0::",
1371 "phy0-led",
1372 "eth0-link",
1373 "power",
1374 "charging",
1375 "disk-activity",
1376 ] {
1377 let actuator = SysfsActuator::led(name);
1378 if actuator.is_available() {
1379 actuators.push(actuator);
1380 }
1381 }
1382 actuators
1383}
1384
1385#[must_use]
1389pub fn discover_thermal_sensors() -> Vec<SysfsSensor> {
1390 let mut sensors = Vec::new();
1391 for i in 0..16 {
1392 let sensor = SysfsSensor::thermal(i);
1393 if sensor.is_available() {
1394 sensors.push(sensor);
1395 }
1396 }
1397 sensors
1398}
1399
1400#[must_use]
1404pub fn discover_battery_sensors() -> Vec<SysfsSensor> {
1405 let mut sensors = Vec::new();
1406 for name in ["BAT0", "BAT1", "BAT2", "BAT3", "BATC", "BATT"] {
1407 let sensor = SysfsSensor::battery(name);
1408 if sensor.is_available() {
1409 sensors.push(sensor);
1410 }
1411 }
1412 sensors
1413}
1414
1415#[must_use]
1419pub fn linux_hardware_bus() -> SensorimotorBus {
1420 let mut bus = SensorimotorBus::new(256);
1421
1422 for sensor in discover_thermal_sensors() {
1424 bus.register_sensor(Box::new(sensor));
1425 }
1426
1427 for sensor in discover_battery_sensors() {
1429 bus.register_sensor(Box::new(sensor));
1430 }
1431
1432 if cfg!(target_os = "linux") {
1434 if std::path::Path::new("/proc/loadavg").exists() {
1435 bus.register_sensor(Box::new(ProcfsSensor::loadavg()));
1436 }
1437 if std::path::Path::new("/proc/meminfo").exists() {
1438 bus.register_sensor(Box::new(ProcfsSensor::mem_pressure()));
1439 }
1440 if std::path::Path::new("/proc/stat").exists() {
1441 bus.register_sensor(Box::new(CpuUsageSensor::new()));
1442 }
1443 if std::path::Path::new("/proc/net/dev").exists() {
1444 bus.register_sensor(Box::new(NetworkThroughputSensor::default_interface()));
1445 }
1446 }
1447
1448 if std::path::Path::new("/").exists() {
1450 bus.register_sensor(Box::new(DiskUsageSensor::root()));
1451 }
1452
1453 for core in 0..16 {
1455 let sensor = CpuFreqSensor::new(core);
1456 if sensor.is_available() {
1457 bus.register_sensor(Box::new(sensor));
1458 }
1459 }
1460
1461 for actuator in discover_fan_actuators() {
1463 bus.register_actuator(Box::new(actuator));
1464 }
1465
1466 for actuator in discover_led_actuators() {
1468 bus.register_actuator(Box::new(actuator));
1469 }
1470
1471 bus
1472}
1473
1474fn now_secs() -> f64 {
1478 std::time::SystemTime::now()
1479 .duration_since(std::time::UNIX_EPOCH)
1480 .map_or(0.0, |d| d.as_secs_f64())
1481}
1482
1483#[cfg(test)]
1486mod tests {
1487 use super::*;
1488
1489 #[test]
1490 fn sensor_kind_as_str() {
1491 assert_eq!(SensorKind::Temperature.as_str(), "temperature");
1492 assert_eq!(SensorKind::Imu.as_str(), "imu");
1493 assert_eq!(SensorKind::Camera.as_str(), "camera");
1494 }
1495
1496 #[test]
1497 fn actuator_kind_as_str() {
1498 assert_eq!(ActuatorKind::Motor.as_str(), "motor");
1499 assert_eq!(ActuatorKind::Relay.as_str(), "relay");
1500 }
1501
1502 #[test]
1503 fn sensor_reading_new() {
1504 let r = SensorReading::new("cpu_temp", SensorKind::Temperature, 55.0);
1505 assert_eq!(r.sensor_id, "cpu_temp");
1506 assert_eq!(r.kind, SensorKind::Temperature);
1507 assert!((r.value - 55.0).abs() < f64::EPSILON);
1508 assert!((r.confidence - 1.0).abs() < f64::EPSILON);
1509 assert!(r.extra.is_empty());
1510 }
1511
1512 #[test]
1513 fn sensor_reading_with_extra() {
1514 let r = SensorReading::new("imu0", SensorKind::Imu, 0.0).with_extra(vec![1.0, 2.0, 3.0]);
1515 assert_eq!(r.extra, vec![1.0, 2.0, 3.0]);
1516 }
1517
1518 #[test]
1519 fn sensor_reading_with_confidence() {
1520 let r = SensorReading::new("cam0", SensorKind::Camera, 128.0).with_confidence(0.8);
1521 assert!((r.confidence - 0.8).abs() < f64::EPSILON);
1522 }
1523
1524 #[test]
1525 fn sensor_reading_confidence_clamped() {
1526 let r = SensorReading::new("s0", SensorKind::Custom, 0.0).with_confidence(1.5);
1527 assert!((r.confidence - 1.0).abs() < f64::EPSILON);
1528 }
1529
1530 #[test]
1531 fn actuator_command_new() {
1532 let c = ActuatorCommand::new("motor_l", ActuatorKind::Motor, 0.5);
1533 assert_eq!(c.actuator_id, "motor_l");
1534 assert_eq!(c.kind, ActuatorKind::Motor);
1535 assert!((c.value - 0.5).abs() < f64::EPSILON);
1536 }
1537
1538 #[test]
1539 fn actuator_command_with_params() {
1540 let c =
1541 ActuatorCommand::new("motor_r", ActuatorKind::Motor, 1.0).with_params(vec![0.1, 2.0]);
1542 assert_eq!(c.params, vec![0.1, 2.0]);
1543 }
1544
1545 #[test]
1546 fn stub_sensor_read() {
1547 let s = StubSensor::new("temp0", SensorKind::Temperature, 42.0);
1548 assert_eq!(s.id(), "temp0");
1549 assert_eq!(s.kind(), SensorKind::Temperature);
1550 let r = s.read().unwrap();
1551 assert!((r.value - 42.0).abs() < f64::EPSILON);
1552 }
1553
1554 #[test]
1555 fn stub_actuator_command() {
1556 let a = StubActuator::new("motor0", ActuatorKind::Motor);
1557 let cmd = ActuatorCommand::new("motor0", ActuatorKind::Motor, 0.7);
1558 a.command(&cmd).unwrap();
1559 let last = a.last_command().unwrap();
1560 assert!((last.value - 0.7).abs() < f64::EPSILON);
1561 }
1562
1563 #[test]
1564 fn bus_register_and_poll() {
1565 let mut bus = SensorimotorBus::new(64);
1566 bus.register_sensor(Box::new(StubSensor::new(
1567 "s1",
1568 SensorKind::Temperature,
1569 50.0,
1570 )));
1571 bus.register_sensor(Box::new(StubSensor::new("s2", SensorKind::Distance, 1.5)));
1572
1573 assert_eq!(bus.sensor_count(), 2);
1574 let readings = bus.poll_all();
1575 assert_eq!(readings.len(), 2);
1576 assert_eq!(bus.readings_collected(), 2);
1577 }
1578
1579 #[test]
1580 fn bus_send_command() {
1581 let mut bus = SensorimotorBus::new(64);
1582 bus.register_actuator(Box::new(StubActuator::new("m1", ActuatorKind::Motor)));
1583
1584 let cmd = ActuatorCommand::new("m1", ActuatorKind::Motor, 0.5);
1585 bus.send_command(&cmd).unwrap();
1586 assert_eq!(bus.commands_sent(), 1);
1587 }
1588
1589 #[test]
1590 fn bus_command_unknown_actuator() {
1591 let mut bus = SensorimotorBus::new(64);
1592 let cmd = ActuatorCommand::new("unknown", ActuatorKind::Motor, 0.0);
1593 assert!(bus.send_command(&cmd).is_err());
1594 }
1595
1596 #[test]
1597 fn bus_history() {
1598 let mut bus = SensorimotorBus::new(3);
1599 bus.register_sensor(Box::new(StubSensor::new(
1600 "s1",
1601 SensorKind::Temperature,
1602 50.0,
1603 )));
1604
1605 bus.poll_all();
1606 bus.poll_all();
1607 bus.poll_all();
1608 bus.poll_all(); let h = bus.history();
1611 assert_eq!(h.len(), 3);
1612 }
1613
1614 #[test]
1615 fn bus_sensor_ids() {
1616 let mut bus = SensorimotorBus::new(64);
1617 bus.register_sensor(Box::new(StubSensor::new("a", SensorKind::Temperature, 0.0)));
1618 bus.register_sensor(Box::new(StubSensor::new("b", SensorKind::Imu, 0.0)));
1619
1620 let ids = bus.sensor_ids();
1621 assert!(ids.contains(&"a".to_string()));
1622 assert!(ids.contains(&"b".to_string()));
1623 }
1624
1625 #[test]
1626 fn bus_actuator_ids() {
1627 let mut bus = SensorimotorBus::new(64);
1628 bus.register_actuator(Box::new(StubActuator::new("m1", ActuatorKind::Motor)));
1629
1630 let ids = bus.actuator_ids();
1631 assert!(ids.contains(&"m1".to_string()));
1632 }
1633
1634 #[test]
1635 fn bus_e_stop_all() {
1636 let mut bus = SensorimotorBus::new(64);
1637 bus.register_actuator(Box::new(StubActuator::new("m1", ActuatorKind::Motor)));
1638 bus.register_actuator(Box::new(StubActuator::new("m2", ActuatorKind::Motor)));
1639
1640 let errors = bus.e_stop_all();
1641 assert!(errors.is_empty()); }
1643
1644 #[test]
1645 fn reflex_rule_above() {
1646 let rule = ReflexRule::above("temp0", "fan0", ActuatorKind::Motor, 70.0, 1.0, 5.0);
1647 let reading_high = SensorReading::new("temp0", SensorKind::Temperature, 75.0);
1648 let reading_low = SensorReading::new("temp0", SensorKind::Temperature, 60.0);
1649
1650 assert!(rule.is_triggered(&reading_high));
1651 assert!(!rule.is_triggered(&reading_low));
1652 }
1653
1654 #[test]
1655 fn reflex_rule_below() {
1656 let rule = ReflexRule::below("battery", "led", ActuatorKind::Display, 20.0, 1.0, 10.0);
1657 let reading_low = SensorReading::new("battery", SensorKind::Power, 15.0);
1658 let reading_ok = SensorReading::new("battery", SensorKind::Power, 80.0);
1659
1660 assert!(rule.is_triggered(&reading_low));
1661 assert!(!rule.is_triggered(&reading_ok));
1662 }
1663
1664 #[test]
1665 fn reflex_loop_evaluate() {
1666 let mut loop_ = ReflexLoop::new();
1667 loop_.add_rule(ReflexRule::above(
1668 "temp0",
1669 "fan0",
1670 ActuatorKind::Motor,
1671 70.0,
1672 1.0,
1673 0.0, ));
1675
1676 let readings = vec![SensorReading::new("temp0", SensorKind::Temperature, 75.0)];
1677 let commands = loop_.evaluate(&readings);
1678 assert_eq!(commands.len(), 1);
1679 assert_eq!(commands[0].actuator_id, "fan0");
1680 }
1681
1682 #[test]
1683 fn reflex_loop_no_trigger() {
1684 let mut loop_ = ReflexLoop::new();
1685 loop_.add_rule(ReflexRule::above(
1686 "temp0",
1687 "fan0",
1688 ActuatorKind::Motor,
1689 70.0,
1690 1.0,
1691 0.0,
1692 ));
1693
1694 let readings = vec![SensorReading::new("temp0", SensorKind::Temperature, 60.0)];
1695 let commands = loop_.evaluate(&readings);
1696 assert!(commands.is_empty());
1697 }
1698
1699 #[test]
1700 fn reflex_loop_cooldown() {
1701 let mut loop_ = ReflexLoop::new();
1702 loop_.add_rule(ReflexRule::above(
1703 "temp0",
1704 "fan0",
1705 ActuatorKind::Motor,
1706 70.0,
1707 1.0,
1708 100.0, ));
1710
1711 let readings = vec![SensorReading::new("temp0", SensorKind::Temperature, 80.0)];
1712
1713 let cmds1 = loop_.evaluate(&readings);
1715 assert_eq!(cmds1.len(), 1);
1716
1717 let cmds2 = loop_.evaluate(&readings);
1719 assert!(cmds2.is_empty());
1720 }
1721
1722 #[test]
1723 fn reflex_loop_multiple_rules() {
1724 let mut loop_ = ReflexLoop::new();
1725 loop_.add_rule(ReflexRule::above(
1726 "temp0",
1727 "fan0",
1728 ActuatorKind::Motor,
1729 70.0,
1730 1.0,
1731 0.0,
1732 ));
1733 loop_.add_rule(ReflexRule::below(
1734 "battery",
1735 "led0",
1736 ActuatorKind::Display,
1737 20.0,
1738 1.0,
1739 0.0,
1740 ));
1741
1742 let readings = vec![
1743 SensorReading::new("temp0", SensorKind::Temperature, 75.0),
1744 SensorReading::new("battery", SensorKind::Power, 15.0),
1745 ];
1746
1747 let commands = loop_.evaluate(&readings);
1748 assert_eq!(commands.len(), 2);
1749 }
1750
1751 #[test]
1752 fn reflex_loop_rule_count() {
1753 let mut loop_ = ReflexLoop::new();
1754 assert_eq!(loop_.rule_count(), 0);
1755 loop_.add_rule(ReflexRule::above(
1756 "s",
1757 "a",
1758 ActuatorKind::Motor,
1759 1.0,
1760 1.0,
1761 1.0,
1762 ));
1763 assert_eq!(loop_.rule_count(), 1);
1764 }
1765
1766 #[test]
1767 fn reflex_loop_refuses_stale_readings() {
1768 let mut loop_ = ReflexLoop::new();
1769 loop_.add_rule(ReflexRule::above(
1770 "temp0",
1771 "fan0",
1772 ActuatorKind::Motor,
1773 70.0,
1774 1.0,
1775 0.0,
1776 ));
1777
1778 let mut reading = SensorReading::new("temp0", SensorKind::Temperature, 90.0);
1779 reading.timestamp = now_secs() - 3600.0;
1780 assert!(
1781 loop_.evaluate(&[reading]).is_empty(),
1782 "a one-hour-old reading must not actuate"
1783 );
1784 }
1785
1786 #[test]
1787 fn reflex_loop_refuses_future_timestamps() {
1788 let mut loop_ = ReflexLoop::new();
1789 loop_.add_rule(ReflexRule::above(
1790 "temp0",
1791 "fan0",
1792 ActuatorKind::Motor,
1793 70.0,
1794 1.0,
1795 0.0,
1796 ));
1797
1798 let mut reading = SensorReading::new("temp0", SensorKind::Temperature, 90.0);
1799 reading.timestamp = now_secs() + 3600.0;
1800 assert!(
1801 loop_.evaluate(&[reading]).is_empty(),
1802 "a future-dated reading (clock skew) must not actuate"
1803 );
1804 }
1805
1806 #[test]
1807 fn reflex_loop_max_age_is_configurable() {
1808 let mut loop_ = ReflexLoop::new().with_max_reading_age(0.0);
1809 loop_.add_rule(ReflexRule::above(
1810 "temp0",
1811 "fan0",
1812 ActuatorKind::Motor,
1813 70.0,
1814 1.0,
1815 0.0,
1816 ));
1817
1818 let mut reading = SensorReading::new("temp0", SensorKind::Temperature, 90.0);
1819 reading.timestamp = now_secs() - 3600.0;
1820 assert_eq!(
1821 loop_.evaluate(&[reading]).len(),
1822 1,
1823 "a non-positive bound disables the freshness check (tests only)"
1824 );
1825 }
1826
1827 #[test]
1828 fn bus_default() {
1829 let bus = SensorimotorBus::default();
1830 assert_eq!(bus.sensor_count(), 0);
1831 assert_eq!(bus.actuator_count(), 0);
1832 }
1833
1834 #[test]
1835 fn bus_read_specific_sensor() {
1836 let mut bus = SensorimotorBus::new(64);
1837 bus.register_sensor(Box::new(StubSensor::new(
1838 "s1",
1839 SensorKind::Temperature,
1840 42.0,
1841 )));
1842
1843 let r = bus.read_sensor("s1").unwrap();
1844 assert!((r.value - 42.0).abs() < f64::EPSILON);
1845
1846 assert!(bus.read_sensor("nonexistent").is_none());
1847 }
1848
1849 #[test]
1850 fn sensor_kind_display() {
1851 assert_eq!(format!("{}", SensorKind::Temperature), "temperature");
1852 assert_eq!(format!("{}", ActuatorKind::Motor), "motor");
1853 }
1854
1855 #[test]
1858 fn sysfs_sensor_thermal_construction() {
1859 let sensor = SysfsSensor::thermal(0);
1860 assert_eq!(sensor.id(), "thermal_zone0");
1861 assert_eq!(sensor.kind(), SensorKind::Temperature);
1862 }
1863
1864 #[test]
1865 fn sysfs_sensor_battery_construction() {
1866 let sensor = SysfsSensor::battery("BAT0");
1867 assert_eq!(sensor.id(), "battery_BAT0");
1868 assert_eq!(sensor.kind(), SensorKind::Power);
1869 }
1870
1871 #[test]
1872 fn sysfs_sensor_fan_construction() {
1873 let sensor = SysfsSensor::fan(0, 1);
1874 assert_eq!(sensor.id(), "fan0_1");
1875 assert_eq!(sensor.kind(), SensorKind::Custom);
1876 }
1877
1878 #[test]
1879 fn sysfs_sensor_nonexistent_path() {
1880 let sensor = SysfsSensor::new(
1881 "test",
1882 SensorKind::Temperature,
1883 "/nonexistent/path/that/does/not/exist",
1884 1000.0,
1885 );
1886 assert!(!sensor.is_available());
1887 assert!(sensor.read().is_none());
1888 }
1889
1890 #[test]
1891 fn sysfs_sensor_debug_format() {
1892 let sensor = SysfsSensor::thermal(0);
1893 let debug = format!("{sensor:?}");
1894 assert!(debug.contains("SysfsSensor"));
1895 assert!(debug.contains("thermal_zone0"));
1896 }
1897
1898 #[test]
1901 fn procfs_sensor_loadavg_construction() {
1902 let sensor = ProcfsSensor::loadavg();
1903 assert_eq!(sensor.id(), "cpu_loadavg");
1904 assert_eq!(sensor.kind(), SensorKind::Custom);
1905 }
1906
1907 #[test]
1908 fn procfs_sensor_mem_pressure_construction() {
1909 let sensor = ProcfsSensor::mem_pressure();
1910 assert_eq!(sensor.id(), "mem_pressure");
1911 assert_eq!(sensor.kind(), SensorKind::Custom);
1912 }
1913
1914 #[test]
1915 fn procfs_sensor_nonexistent_path() {
1916 let sensor = ProcfsSensor::new(
1917 "test",
1918 SensorKind::Custom,
1919 "/nonexistent/proc/path",
1920 Box::new(|_| Some(1.0)),
1921 );
1922 assert!(!sensor.is_available());
1923 assert!(sensor.read().is_none());
1924 }
1925
1926 #[test]
1927 fn procfs_sensor_debug_format() {
1928 let sensor = ProcfsSensor::loadavg();
1929 let debug = format!("{sensor:?}");
1930 assert!(debug.contains("ProcfsSensor"));
1931 assert!(debug.contains("cpu_loadavg"));
1932 }
1933
1934 #[test]
1935 fn procfs_sensor_custom_parser() {
1936 let sensor = ProcfsSensor::new(
1937 "test_parser",
1938 SensorKind::Custom,
1939 "/proc/loadavg",
1940 Box::new(|content: &str| {
1941 content.split_whitespace().next().and_then(|s| {
1942 let v: f64 = s.parse().ok()?;
1943 Some(v * 2.0)
1944 })
1945 }),
1946 );
1947 if sensor.is_available() {
1949 let reading = sensor.read().unwrap();
1950 assert!(reading.value > 0.0);
1951 }
1952 }
1953
1954 #[test]
1957 fn discover_thermal_sensors_returns_vec() {
1958 let sensors = discover_thermal_sensors();
1959 for s in &sensors {
1962 assert!(s.is_available());
1963 assert_eq!(s.kind(), SensorKind::Temperature);
1964 }
1965 }
1966
1967 #[test]
1968 fn discover_battery_sensors_returns_vec() {
1969 let sensors = discover_battery_sensors();
1970 for s in &sensors {
1971 assert!(s.is_available());
1972 assert_eq!(s.kind(), SensorKind::Power);
1973 }
1974 }
1975
1976 #[test]
1979 fn linux_hardware_bus_creation() {
1980 let bus = linux_hardware_bus();
1981 let sensor_ids = bus.sensor_ids();
1983 assert!(bus.sensor_count() <= 50); for id in &sensor_ids {
1986 assert!(!id.is_empty());
1987 }
1988 }
1989
1990 #[test]
1991 fn linux_hardware_bus_poll_all() {
1992 let mut bus = linux_hardware_bus();
1993 let readings = bus.poll_all();
1994 for r in &readings {
1996 assert!(!r.sensor_id.is_empty());
1997 }
1998 }
1999
2000 #[test]
2003 fn parse_proc_kb_extracts_value() {
2004 assert_eq!(
2005 parse_proc_kb("MemTotal: 16384000 kB"),
2006 Some(16_384_000)
2007 );
2008 assert_eq!(parse_proc_kb("MemAvailable: 8192000 kB"), Some(8_192_000));
2009 assert_eq!(parse_proc_kb("garbage"), None);
2010 }
2011
2012 #[test]
2015 fn cpu_usage_sensor_construction() {
2016 let sensor = CpuUsageSensor::new();
2017 assert_eq!(sensor.id(), "cpu_usage");
2018 assert_eq!(sensor.kind(), SensorKind::Custom);
2019 }
2020
2021 #[test]
2022 fn cpu_usage_sensor_debug() {
2023 let sensor = CpuUsageSensor::new();
2024 let debug = format!("{sensor:?}");
2025 assert!(debug.contains("CpuUsageSensor"));
2026 }
2027
2028 #[test]
2029 fn cpu_usage_sensor_read_on_linux() {
2030 if !std::path::Path::new("/proc/stat").exists() {
2031 return;
2032 }
2033 let sensor = CpuUsageSensor::new();
2034 assert!(sensor.is_available());
2035 let reading = sensor.read().unwrap();
2036 assert_eq!(reading.sensor_id, "cpu_usage");
2037 assert!(reading.value >= 0.0 && reading.value <= 100.0);
2038 }
2039
2040 #[test]
2043 fn disk_usage_sensor_construction() {
2044 let sensor = DiskUsageSensor::root();
2045 assert_eq!(sensor.id(), "disk_root");
2046 assert_eq!(sensor.kind(), SensorKind::Custom);
2047 }
2048
2049 #[test]
2050 fn disk_usage_sensor_nonexistent() {
2051 let sensor = DiskUsageSensor::new("test", "/nonexistent/mount/point");
2052 assert!(!sensor.is_available());
2053 assert!(sensor.read().is_none());
2054 }
2055
2056 #[test]
2057 fn disk_usage_sensor_debug() {
2058 let sensor = DiskUsageSensor::root();
2059 let debug = format!("{sensor:?}");
2060 assert!(debug.contains("DiskUsageSensor"));
2061 }
2062
2063 #[test]
2066 fn network_sensor_construction() {
2067 let sensor = NetworkThroughputSensor::new("eth0");
2068 assert_eq!(sensor.id(), "net_eth0");
2069 assert_eq!(sensor.kind(), SensorKind::Custom);
2070 }
2071
2072 #[test]
2073 fn network_sensor_default_interface() {
2074 let sensor = NetworkThroughputSensor::default_interface();
2075 assert!(sensor.id().starts_with("net_"));
2076 }
2077
2078 #[test]
2079 fn network_sensor_debug() {
2080 let sensor = NetworkThroughputSensor::new("wlan0");
2081 let debug = format!("{sensor:?}");
2082 assert!(debug.contains("NetworkThroughputSensor"));
2083 assert!(debug.contains("wlan0"));
2084 }
2085
2086 #[test]
2087 fn network_sensor_read_on_linux() {
2088 if !std::path::Path::new("/proc/net/dev").exists() {
2089 return;
2090 }
2091 let sensor = NetworkThroughputSensor::default_interface();
2092 assert!(sensor.is_available());
2093 let reading = sensor.read();
2094 if let Some(r) = reading {
2096 assert!(r.value >= 0.0);
2097 }
2098 }
2099
2100 #[test]
2103 fn cpu_freq_sensor_construction() {
2104 let sensor = CpuFreqSensor::new(0);
2105 assert_eq!(sensor.id(), "cpu0_freq");
2106 assert_eq!(sensor.kind(), SensorKind::Custom);
2107 }
2108
2109 #[test]
2110 fn cpu_freq_sensor_nonexistent() {
2111 let sensor = CpuFreqSensor::new(999);
2112 assert!(!sensor.is_available());
2113 assert!(sensor.read().is_none());
2114 }
2115
2116 #[test]
2117 fn cpu_freq_sensor_debug() {
2118 let sensor = CpuFreqSensor::new(0);
2119 let debug = format!("{sensor:?}");
2120 assert!(debug.contains("CpuFreqSensor"));
2121 }
2122
2123 #[test]
2126 fn sysfs_actuator_construction() {
2127 let actuator = SysfsActuator::fan_pwm(0, 1);
2128 assert_eq!(actuator.id(), "fan_pwm0_1");
2129 assert_eq!(actuator.kind(), ActuatorKind::Motor);
2130 }
2131
2132 #[test]
2133 fn sysfs_actuator_led_construction() {
2134 let actuator = SysfsActuator::led("power");
2135 assert_eq!(actuator.id(), "led_power");
2136 assert_eq!(actuator.kind(), ActuatorKind::Display);
2137 }
2138
2139 #[test]
2140 fn sysfs_actuator_nonexistent_path() {
2141 let actuator = SysfsActuator::new(
2142 "test",
2143 ActuatorKind::Motor,
2144 "/nonexistent/path/that/does/not/exist",
2145 1.0,
2146 );
2147 assert!(!actuator.is_available());
2148 let cmd = ActuatorCommand::new("test", ActuatorKind::Motor, 1.0);
2149 assert!(actuator.command(&cmd).is_err());
2150 }
2151
2152 #[test]
2153 fn sysfs_actuator_e_stop_nonexistent() {
2154 let actuator = SysfsActuator::new("test", ActuatorKind::Motor, "/nonexistent/path", 1.0);
2155 assert!(actuator.e_stop().is_err());
2156 }
2157
2158 #[test]
2159 fn sysfs_actuator_debug() {
2160 let actuator = SysfsActuator::fan_pwm(0, 1);
2161 let debug = format!("{actuator:?}");
2162 assert!(debug.contains("SysfsActuator"));
2163 assert!(debug.contains("fan_pwm0_1"));
2164 }
2165
2166 #[test]
2167 fn sysfs_actuator_scale_applied() {
2168 let tmp = tempfile::NamedTempFile::new().unwrap();
2170 let path = tmp.path().to_str().unwrap();
2171 let actuator = SysfsActuator::new("test", ActuatorKind::Display, path, 255.0);
2172 assert!(actuator.is_available());
2173 let cmd = ActuatorCommand::new("test", ActuatorKind::Display, 0.5);
2174 actuator.command(&cmd).unwrap();
2175 let written = std::fs::read_to_string(path).unwrap();
2176 assert_eq!(written.trim(), "128"); }
2178
2179 #[test]
2182 fn discover_fan_actuators_returns_vec() {
2183 let actuators = discover_fan_actuators();
2184 for a in &actuators {
2185 assert!(a.is_available());
2186 assert_eq!(a.kind(), ActuatorKind::Motor);
2187 }
2188 }
2189
2190 #[test]
2191 fn discover_led_actuators_returns_vec() {
2192 let actuators = discover_led_actuators();
2193 for a in &actuators {
2194 assert!(a.is_available());
2195 assert_eq!(a.kind(), ActuatorKind::Display);
2196 }
2197 }
2198
2199 #[test]
2202 fn linux_hardware_bus_includes_new_sensors() {
2203 let bus = linux_hardware_bus();
2204 let ids = bus.sensor_ids();
2205 if std::path::Path::new("/proc/stat").exists() {
2207 assert!(ids.iter().any(|id| id == "cpu_usage"));
2208 }
2209 assert!(ids.iter().any(|id| id == "disk_root"));
2211 }
2212
2213 #[test]
2214 fn linux_hardware_bus_includes_actuators() {
2215 let bus = linux_hardware_bus();
2216 let actuator_ids = bus.actuator_ids();
2218 for id in &actuator_ids {
2219 assert!(!id.is_empty());
2220 }
2221 }
2222}