1use serde::{Deserialize, Serialize};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37#[repr(u8)]
38pub enum Availability {
39 Observed = 0,
41 NotApplicable = 1,
45 Unknown = 2,
48 Unsupported = 3,
51 Unavailable = 4,
54 PermissionDenied = 5,
58 Stale = 6,
60}
61
62impl Availability {
63 pub fn as_u8(self) -> u8 {
64 self as u8
65 }
66
67 pub fn from_u8(value: u8) -> Availability {
68 match value {
69 0 => Availability::Observed,
70 1 => Availability::NotApplicable,
71 2 => Availability::Unknown,
72 3 => Availability::Unsupported,
73 4 => Availability::Unavailable,
74 5 => Availability::PermissionDenied,
75 6 => Availability::Stale,
76 _ => Availability::Unknown,
79 }
80 }
81
82 pub fn label(self) -> &'static str {
83 match self {
84 Availability::Observed => "observed",
85 Availability::NotApplicable => "not_applicable",
86 Availability::Unknown => "unknown",
87 Availability::Unsupported => "unsupported",
88 Availability::Unavailable => "unavailable",
89 Availability::PermissionDenied => "permission_denied",
90 Availability::Stale => "stale",
91 }
92 }
93
94 pub fn explain(self) -> &'static str {
101 match self {
102 Availability::Observed => "the value was read",
103 Availability::NotApplicable => "this measurement does not apply to this entity",
104 Availability::Unknown => "no reason was recorded",
105 Availability::Unsupported => "this hardware or kernel does not expose it",
106 Availability::Unavailable => "the source exists but returned nothing",
107 Availability::PermissionDenied => "this process lacks the privilege to read it",
108 Availability::Stale => "the last reading is too old to be trusted",
109 }
110 }
111
112 pub fn is_observed(self) -> bool {
114 self == Availability::Observed
115 }
116
117 pub fn is_privilege_problem(self) -> bool {
119 self == Availability::PermissionDenied
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct AvailabilityMatrix {
126 rows: usize,
127 cols: usize,
128 codes: Vec<u8>,
129}
130
131impl AvailabilityMatrix {
132 pub fn new(rows: usize, cols: usize) -> AvailabilityMatrix {
134 AvailabilityMatrix {
135 rows,
136 cols,
137 codes: vec![Availability::NotApplicable.as_u8(); rows * cols],
138 }
139 }
140
141 pub fn rows(&self) -> usize {
142 self.rows
143 }
144
145 pub fn cols(&self) -> usize {
146 self.cols
147 }
148
149 pub fn clear(&mut self) {
152 self.codes.fill(Availability::NotApplicable.as_u8());
153 }
154
155 #[inline]
156 pub fn get(&self, row: usize, col: usize) -> Availability {
157 if row >= self.rows || col >= self.cols {
158 return Availability::NotApplicable;
159 }
160 Availability::from_u8(self.codes[row * self.cols + col])
161 }
162
163 #[inline]
164 pub fn set(&mut self, row: usize, col: usize, availability: Availability) {
165 if row < self.rows && col < self.cols {
166 self.codes[row * self.cols + col] = availability.as_u8();
167 }
168 }
169
170 pub fn as_slice(&self) -> &[u8] {
171 &self.codes
172 }
173
174 pub fn copy_from_slice(&mut self, codes: &[u8]) {
175 assert_eq!(codes.len(), self.codes.len(), "availability shape mismatch");
176 self.codes.copy_from_slice(codes);
177 }
178
179 pub fn tally(&self) -> Vec<(Availability, usize)> {
181 let mut counts = [0usize; 8];
182 for code in &self.codes {
183 counts[(*code as usize).min(7)] += 1;
184 }
185 (0..7u8)
186 .map(|code| (Availability::from_u8(code), counts[code as usize]))
187 .filter(|(_, count)| *count > 0)
188 .collect()
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
194#[serde(transparent)]
195pub struct SensorId(pub u16);
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203#[repr(u8)]
204pub enum Perturbation {
205 None = 0,
208 Negligible = 1,
211 Low = 2,
215 Material = 3,
219}
220
221impl Perturbation {
222 pub fn as_u8(self) -> u8 {
223 self as u8
224 }
225
226 pub fn from_u8(value: u8) -> Perturbation {
227 match value {
228 1 => Perturbation::Negligible,
229 2 => Perturbation::Low,
230 3 => Perturbation::Material,
231 _ => Perturbation::None,
232 }
233 }
234
235 pub fn label(self) -> &'static str {
236 match self {
237 Perturbation::None => "none",
238 Perturbation::Negligible => "negligible",
239 Perturbation::Low => "low",
240 Perturbation::Material => "material",
241 }
242 }
243
244 pub fn is_passive(self) -> bool {
246 self != Perturbation::Material
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
252pub struct Uncertainty {
253 pub absolute: Option<f64>,
255 pub relative: Option<f64>,
257 pub basis: &'static str,
259}
260
261impl Uncertainty {
262 pub const fn absolute(value: f64, basis: &'static str) -> Uncertainty {
263 Uncertainty {
264 absolute: Some(value),
265 relative: None,
266 basis,
267 }
268 }
269
270 pub const fn relative(value: f64, basis: &'static str) -> Uncertainty {
271 Uncertainty {
272 absolute: None,
273 relative: Some(value),
274 basis,
275 }
276 }
277
278 pub const fn unknown(basis: &'static str) -> Uncertainty {
279 Uncertainty {
280 absolute: None,
281 relative: None,
282 basis,
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub struct SensorReport {
292 pub id: SensorId,
293 pub key: String,
294 pub perturbation: Perturbation,
295 pub last_cost_ns: u64,
297 pub sampling_latency_ns: u64,
300 pub sample_age_ns: u64,
303 pub samples: u32,
305 pub errors: u32,
307 pub confidence: f64,
309 pub availability: Availability,
311 pub inactive: bool,
314}
315
316impl SensorReport {
317 pub fn pending(
319 id: SensorId,
320 key: impl Into<String>,
321 perturbation: Perturbation,
322 ) -> SensorReport {
323 SensorReport {
324 id,
325 key: key.into(),
326 perturbation,
327 last_cost_ns: 0,
328 sampling_latency_ns: 0,
329 sample_age_ns: 0,
330 samples: 0,
331 errors: 0,
332 confidence: 1.0,
333 availability: Availability::Unknown,
334 inactive: false,
335 }
336 }
337
338 pub fn unavailable(
340 id: SensorId,
341 key: impl Into<String>,
342 perturbation: Perturbation,
343 availability: Availability,
344 ) -> SensorReport {
345 SensorReport {
346 inactive: true,
347 confidence: 0.0,
348 availability,
349 ..SensorReport::pending(id, key, perturbation)
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn availability_codes_round_trip_and_degrade() {
360 for availability in [
361 Availability::Observed,
362 Availability::NotApplicable,
363 Availability::Unknown,
364 Availability::Unsupported,
365 Availability::Unavailable,
366 Availability::PermissionDenied,
367 Availability::Stale,
368 ] {
369 assert_eq!(Availability::from_u8(availability.as_u8()), availability);
370 }
371 assert_eq!(Availability::from_u8(200), Availability::Unknown);
373 }
374
375 #[test]
376 fn permission_denied_is_distinguishable_from_unsupported() {
377 assert!(Availability::PermissionDenied.is_privilege_problem());
380 assert!(!Availability::Unsupported.is_privilege_problem());
381 assert!(!Availability::Observed.is_privilege_problem());
382 }
383
384 #[test]
385 fn a_new_availability_matrix_claims_nothing() {
386 let matrix = AvailabilityMatrix::new(3, 4);
387 assert_eq!(matrix.get(0, 0), Availability::NotApplicable);
388 assert_eq!(matrix.tally(), vec![(Availability::NotApplicable, 12)]);
389 }
390
391 #[test]
392 fn reasons_are_recorded_per_cell() {
393 let mut matrix = AvailabilityMatrix::new(2, 2);
394 matrix.set(0, 0, Availability::Observed);
395 matrix.set(0, 1, Availability::PermissionDenied);
396 matrix.set(1, 0, Availability::Unsupported);
397 assert_eq!(matrix.get(0, 0), Availability::Observed);
398 assert_eq!(matrix.get(0, 1), Availability::PermissionDenied);
399 assert_eq!(matrix.get(1, 0), Availability::Unsupported);
400 assert_eq!(matrix.get(1, 1), Availability::NotApplicable);
401
402 let tally = matrix.tally();
403 assert_eq!(tally.len(), 4, "four distinct reasons: {tally:?}");
404 }
405
406 #[test]
407 fn out_of_range_access_does_not_panic() {
408 let mut matrix = AvailabilityMatrix::new(2, 2);
409 matrix.set(99, 99, Availability::Observed);
410 assert_eq!(matrix.get(99, 99), Availability::NotApplicable);
411 }
412
413 #[test]
414 fn a_failed_sensor_reports_why_rather_than_zero() {
415 let report = SensorReport::unavailable(
416 SensorId(4),
417 "power",
418 Perturbation::Negligible,
419 Availability::PermissionDenied,
420 );
421 assert!(report.inactive);
422 assert_eq!(report.confidence, 0.0);
423 assert!(report.availability.is_privilege_problem());
424 assert_eq!(report.samples, 0);
425 }
426
427 #[test]
428 fn material_perturbation_is_not_passive() {
429 assert!(!Perturbation::Material.is_passive());
430 assert!(Perturbation::Low.is_passive());
431 assert!(Perturbation::None < Perturbation::Negligible);
432 assert!(Perturbation::Low < Perturbation::Material);
433 }
434}