1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Backend providing support for Intel PECI sensors.
//!
//! A la dbus-sensors's `intelcpusensor` daemon.

use std::{
	collections::HashMap,
	path::Path,
};

use crate::{
	DaemonState,
	powerstate::PowerState,
	sensor,
	sensor::{
		Sensor,
		SensorConfig,
		SensorIOCtx,
		SensorMode::ReadOnly,
	},
	threshold,
	sysfs,
	types::*,
	dbus_helpers::props::*,
};

/// Internal representation of the dbus config data for a PECI sensor.
///
/// Aside from what's here, there's also a `"PresenceGPIO"` key in a few E-M configs:
///
/// ```
/// "PresenceGPIO": [{ "Name": "...", "Polarity": "..."}]
/// ```
///
/// It seems to be ignored by `intelcpusensor` as far as I can tell though, so we're
/// ignoring it here too (for now at least).  (Also, it's not clear to me why the object
/// is wrapped in an array.)
#[derive(Debug)]
pub struct PECISensorConfig {
	/// Name of the sensor.
	///
	/// FIXME: Is this used for anything?  Need to figure this out.
	name: String,
	/// The ID number of the CPU.
	///
	/// FIXME: Is this used for anything?  Need to figure this out.
	cpuid: u64,
	/// The PECI bus number of the CPU.
	bus: u64,
	/// The PECI address of the CPU.
	address: u64,
	/// The DTS critical offset if provided, 0.0 by default.
	///
	/// This is used as a threshold hysteresis value that optionally overrides what
	/// the kernel provides.
	dts_crit_offset: f64,
	/// Threshold settings for the sensor.
	///
	/// At least in `intelcpusensor`, the threshold information in sysfs is ignored if
	/// any of these are provided.
	thresholds: Vec<threshold::ThresholdConfig>,
}

/// The top-level sysfs directory via which we interact with the kernel's PECI subsystem.
const PECI_BUS_DIR: &str = "/sys/bus/peci";

/// The kernel PECI subsystem doesn't expose a sysfs interface to instantiate a device for
/// a given bus/address; it simply allows triggering a global rescan operation, so this
/// isn't tied to any particular device.
fn rescan() -> ErrResult<()> {
	Ok(std::fs::write(Path::new(PECI_BUS_DIR).join("rescan"), "1")?)
}

impl PECISensorConfig {
	/// Construct a [`PECISensorConfig`] from raw dbus data.
	pub fn from_dbus(basecfg: &dbus::arg::PropMap, baseintf: &str,
	                 intfs: &HashMap<String, dbus::arg::PropMap>) -> ErrResult<Self> {
		let name: &String = prop_get_mandatory(basecfg, "Name")?;
		let cpuid: u64 = *prop_get_mandatory(basecfg, "CpuID")?;
		let bus: u64 = *prop_get_mandatory(basecfg, "Bus")?;
		let address: u64 = *prop_get_mandatory(basecfg, "Address")?;
		let dts_crit_offset = *prop_get_default(basecfg, "DtsCritOffset", &0.0f64)?;

		let thresholds = threshold::get_configs_from_dbus(baseintf, intfs);

		if !dts_crit_offset.is_finite() {
			let msg = format!("{}: DtsCritOffset must be finite (got {})", name,
			                  dts_crit_offset);
			return Err(err_invalid_data(msg));
		}

		Ok(Self {
			name: name.clone(),
			cpuid,
			bus,
			address,
			dts_crit_offset,
			thresholds,
		})
	}
}

/// Instantiate any active PECI sensors configured in `cfgmap`.
pub async fn instantiate_sensors(daemonstate: &DaemonState, dbuspaths: &FilterSet<InventoryPath>)
                                 -> ErrResult<()>
{
	let cfgmap = daemonstate.config.lock().await;
	let configs = cfgmap.iter()
		.filter_map(|(path, cfg)| {
			match cfg {
				SensorConfig::PECI(c) if dbuspaths.contains(path) => Some((path, c)),
				_ => None,
			}
		});

	// Doing this unconditionally on every update call is perhaps
	// a little heavy-handed; we could maybe relax things to
	// trigger it from the power signal handler instead.
	if let Err(e) = rescan() {
		eprintln!("Warning: PECI rescan failed: {}", e);
	}

	for (path, pecicfg) in configs {
		// Bleh...the only thing we don't know in advance here is the
		// CPU family (hsx, skx, icx, etc.) matched by the '*'.  Is
		// there some better way of finding this path?
		let devname = format!("{}-{:02x}", pecicfg.bus, pecicfg.address);
		let sysfs_dir_pat = format!("{}/devices/{}/peci_cpu.cputemp.*.{}", PECI_BUS_DIR,
		                            devname, pecicfg.address);
		let devdir = match sysfs::get_single_glob_match(&sysfs_dir_pat) {
			Ok(d) => d,
			Err(e) => {
				eprintln!("Failed to find cputemp subdirectory for PECI device {}: {}",
				          devname, e);
				continue;
			},
		};

		let inputs = match sysfs::scan_hwmon_input_files(&devdir, Some("temp")) {
			Ok(v) => v,
			Err(e) => {
				eprintln!("Error finding input files in {}: {}", devdir.display(), e);
				continue;
			},
		};

		for file in inputs {
			let label = match file.get_label() {
				Ok(s) => s,
				Err(e) => {
					eprintln!("{}: error finding label for {}, skipping entry: {}",
					          pecicfg.name, file.abspath.display(), e);
					continue;
				},
			};

			match label.as_str() {
				"Tcontrol" | "Tthrottle" | "Tjmax" => continue,
				_ => {},
			}

			let name = format!("{} {}", label, pecicfg.name);

			let mut sensors = daemonstate.sensors.lock().await;

			let Some(entry) = sensor::get_nonactive_sensor_entry(&mut sensors,
			                                                     name.clone()).await else {
				continue;
			};

			let io = match sysfs::SysfsSensorIO::new(&file).await {
				Ok(io) => sensor::SensorIO::Sysfs(io),
				Err(e) => {
					eprintln!("{}: skipping {}: {}", pecicfg.name,
					          file.abspath.display(), e);
					continue;
				},
			};

			let io = SensorIOCtx::new(io);

			let ctor = || {
				Sensor::new(path, &name, file.kind, &daemonstate.sensor_intfs,
				            &daemonstate.bus, ReadOnly)
					.with_power_state(PowerState::On) // FIXME: make configurable?
					.with_thresholds_from(&pecicfg.thresholds,
					                      &daemonstate.sensor_intfs.thresholds,
					                      &daemonstate.bus)
					.with_minval(-128.0)
					.with_maxval(127.0)
			};
			sensor::install_or_activate(entry, &daemonstate.crossroads, io,
			                            &daemonstate.sensor_intfs, ctor).await;
		}
	}

	Ok(())
}

/// Whether or not the given `cfgtype` is supported by the `peci` sensor backend.
pub fn match_cfgtype(cfgtype: &str) -> bool {
	cfgtype == "XeonCPU"
}