diff --git a/src/Exo/Core/Exo.Core/Cooling/ICooler.cs b/src/Exo/Core/Exo.Core/Cooling/ICooler.cs
index 34587ada..24ee8138 100644
--- a/src/Exo/Core/Exo.Core/Cooling/ICooler.cs
+++ b/src/Exo/Core/Exo.Core/Cooling/ICooler.cs
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
+using Exo.Sensors;
namespace Exo.Cooling;
@@ -87,14 +88,40 @@ public interface IManualCooler : IConfigurableCooler
public interface IHardwareCurveCooler : IConfigurableCooler
{
- /// Gets a list of all available input sensors.
- /// Currently the sensors must be present in a readable form on the device, because some informations are missing in external metadata. This can be alleviated later.
- ImmutableArray AvailableInputSensors { get; }
+ /// Gets a list of all possible source sensors.
+ ///
+ ///
+ /// For each internal sensor source that can be used with this cooler, one separate instance of will be provided.
+ ///
+ ///
+ /// Internal sensors would generally also be exposed as sensors through the sensor API surface, but it is not required by the cooling API.
+ /// It is assumed that some sensors used for hardware cooling curves might not be readable or easily readable, and as such, metadata for the sensor is exposed in .
+ ///
+ ///
+ ImmutableArray AvailableSensors { get; }
+
+ bool TryGetActiveSensor([NotNullWhen(true)] out IHardwareCurveCoolerSensorCurveControl? sensor);
}
-public interface IHardwareCurveCooler : IHardwareCurveCooler
- where T: struct, INumber
+/// Allows controlling a hardware cooling curve based on a sensor.
+///
+/// Instances of must also implement for exactly one datatype.
+/// Said datatype must be the one returned by .
+///
+public interface IHardwareCurveCoolerSensorCurveControl
+{
+ Guid SensorId { get; }
+ SensorUnit Unit { get; }
+ Type ValueType { get; }
+}
+
+///
+///
+public interface IHardwareCurveCoolerSensorCurveControl : IHardwareCurveCoolerSensorCurveControl
+ where TInput : struct, INumber
{
+ Type IHardwareCurveCoolerSensorCurveControl.ValueType => typeof(TInput);
+
/// Sets the control curve to be used by this cooler.
///
/// Only the sensors referenced by can be used as control curve input.
@@ -105,9 +132,8 @@ public interface IHardwareCurveCooler : IHardwareCurveCooler
/// using data that fit their internal representation the best.
///
///
- /// The sensor to use as an input.
/// The control curve to apply.
- void SetControlCurve(Guid inputId, IControlCurve curve);
+ void SetControlCurve(IControlCurve curve);
/// Gets the applied cooling curve.
///
@@ -115,10 +141,9 @@ public interface IHardwareCurveCooler : IHardwareCurveCooler
/// For simplicity, device drivers are allowed to return the curve that best matches their internal representation, meaning that data points can be truncated or interpolated,
/// and values can be rounded accordingly.
///
- /// The sensor used as input of this source, if any.
/// The curve currently applied on the cooler, if any.
///
- bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve? curve);
+ bool TryGetControlCurve([NotNullWhen(true)] out IControlCurve? curve);
}
public readonly struct HardwareCoolingInput
diff --git a/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs b/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs
index 4350dc8b..4278af93 100644
--- a/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs
+++ b/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs
@@ -502,10 +502,8 @@ public FanSpeedSensor(KrakenDriver driver) : base(driver) { }
protected override ushort ReadValue(KrakenReadings readings) => readings.FanSpeed;
}
- private static void SetControlCurve(byte[] dest, ref byte state, Guid inputId, IControlCurve curve)
+ private static void SetControlCurve(byte[] dest, ref byte state, IControlCurve curve)
{
- if (inputId != LiquidTemperatureSensorId) throw new ArgumentException();
-
for (int i = 0; i < dest.Length; i++)
{
dest[i] = curve[(byte)(20 + i)];
@@ -513,11 +511,10 @@ private static void SetControlCurve(byte[] dest, ref byte state, Guid inputId, I
state = CoolingStateChanged | CoolingStateCurve;
}
- public static bool TryGetControlCurve(byte[] src, byte state, out Guid inputId, [NotNullWhen(true)] out IControlCurve? curve)
+ public static bool TryGetControlCurve(byte[] src, byte state, [NotNullWhen(true)] out IControlCurve? curve)
{
if ((state & CoolingStateCurve) != 0)
{
- inputId = LiquidTemperatureSensorId;
var dataPoints = new DataPoint[src.Length];
for (int i = 0; i < src.Length; i++)
{
@@ -526,16 +523,20 @@ public static bool TryGetControlCurve(byte[] src, byte state, out Guid inputId,
curve = new InterpolatedSegmentControlCurve(ImmutableCollectionsMarshal.AsImmutableArray(dataPoints), MonotonicityValidators.IncreasingUpTo100);
return true;
}
- inputId = default;
curve = null;
return false;
}
- private sealed class FanCooler : ICooler, IManualCooler, IHardwareCurveCooler
+ private sealed class FanCooler : ICooler, IManualCooler, IHardwareCurveCooler, IHardwareCurveCoolerSensorCurveControl
{
private readonly KrakenDriver _driver;
+ private readonly ImmutableArray _availableSensors;
- public FanCooler(KrakenDriver driver) => _driver = driver;
+ public FanCooler(KrakenDriver driver)
+ {
+ _driver = driver;
+ _availableSensors = [this];
+ }
public Guid CoolerId => FanCoolerId;
public CoolerType Type => CoolerType.Fan;
@@ -543,6 +544,9 @@ private sealed class FanCooler : ICooler, IManualCooler, IHardwareCurveCooler FanSpeedSensorId;
public CoolingMode CoolingMode => CoolingMode.Manual;
+ Guid IHardwareCurveCoolerSensorCurveControl.SensorId => LiquidTemperatureSensorId;
+ SensorUnit IHardwareCurveCoolerSensorCurveControl.Unit => SensorUnit.Celsius;
+
// NB: From testing, the speed starts increasing at 21%, for about 500 RPM.
public byte MinimumPower => 20;
public byte MaximumPower => 100;
@@ -566,20 +570,36 @@ public bool TryGetPower(out byte power)
return false;
}
- public ImmutableArray AvailableInputSensors => InputSensors;
+ public ImmutableArray AvailableSensors { get; }
+
+ public bool TryGetActiveSensor([NotNullWhen(true)] out IHardwareCurveCoolerSensorCurveControl? sensor)
+ {
+ if ((_driver._fanState & CoolingStateCurve) != 0)
+ {
+ sensor = this;
+ return true;
+ }
+ sensor = null;
+ return false;
+ }
- public void SetControlCurve(Guid inputId, IControlCurve curve)
- => KrakenDriver.SetControlCurve(_driver._fanCoolingCurve, ref _driver._fanState, inputId, curve);
+ public void SetControlCurve(IControlCurve curve)
+ => KrakenDriver.SetControlCurve(_driver._fanCoolingCurve, ref _driver._fanState, curve);
- public bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve? curve)
- => KrakenDriver.TryGetControlCurve(_driver._fanCoolingCurve, _driver._fanState, out inputId, out curve);
+ public bool TryGetControlCurve([NotNullWhen(true)] out IControlCurve? curve)
+ => KrakenDriver.TryGetControlCurve(_driver._fanCoolingCurve, _driver._fanState, out curve);
}
- private sealed class PumpCooler : ICooler, IManualCooler, IHardwareCurveCooler
+ private sealed class PumpCooler : ICooler, IManualCooler, IHardwareCurveCooler, IHardwareCurveCoolerSensorCurveControl
{
private readonly KrakenDriver _driver;
+ private readonly ImmutableArray _availableSensors;
- public PumpCooler(KrakenDriver driver) => _driver = driver;
+ public PumpCooler(KrakenDriver driver)
+ {
+ _driver = driver;
+ _availableSensors = [this];
+ }
public Guid CoolerId => PumpCoolerId;
public CoolerType Type => CoolerType.Pump;
@@ -587,6 +607,9 @@ private sealed class PumpCooler : ICooler, IManualCooler, IHardwareCurveCooler PumpSpeedSensorId;
public CoolingMode CoolingMode => CoolingMode.Manual;
+ Guid IHardwareCurveCoolerSensorCurveControl.SensorId => LiquidTemperatureSensorId;
+ SensorUnit IHardwareCurveCoolerSensorCurveControl.Unit => SensorUnit.Celsius;
+
// NB: From testing, the speed starts increasing at 32%, and the effective minimum speed seems to map to about 41% of the maximum. (~1150 RPM / ~2800 RPM)
public byte MinimumPower => 30;
public byte MaximumPower => 100;
@@ -596,7 +619,7 @@ public void SetPower(byte power)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(power, 100);
_driver._pumpSpeedTarget = power;
- _driver._fanState = CoolingStateChanged;
+ _driver._pumpState = CoolingStateChanged;
}
public bool TryGetPower(out byte power)
@@ -610,12 +633,23 @@ public bool TryGetPower(out byte power)
return false;
}
- public ImmutableArray AvailableInputSensors => InputSensors;
+ public ImmutableArray AvailableSensors { get; }
+
+ public bool TryGetActiveSensor([NotNullWhen(true)] out IHardwareCurveCoolerSensorCurveControl? sensor)
+ {
+ if ((_driver._pumpState & CoolingStateCurve) != 0)
+ {
+ sensor = this;
+ return true;
+ }
+ sensor = null;
+ return false;
+ }
- public void SetControlCurve(Guid inputId, IControlCurve curve)
- => KrakenDriver.SetControlCurve(_driver._pumpCoolingCurve, ref _driver._pumpState, inputId, curve);
+ public void SetControlCurve(IControlCurve curve)
+ => KrakenDriver.SetControlCurve(_driver._pumpCoolingCurve, ref _driver._pumpState, curve);
- public bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve? curve)
- => KrakenDriver.TryGetControlCurve(_driver._pumpCoolingCurve, _driver._pumpState, out inputId, out curve);
+ public bool TryGetControlCurve([NotNullWhen(true)] out IControlCurve? curve)
+ => KrakenDriver.TryGetControlCurve(_driver._pumpCoolingCurve, _driver._pumpState, out curve);
}
}
diff --git a/src/Exo/Service/Exo.Service.Core/CoolingService.CoolerChange.cs b/src/Exo/Service/Exo.Service.Core/CoolingService.CoolerChange.cs
index d2edbdf7..b3e949b0 100644
--- a/src/Exo/Service/Exo.Service.Core/CoolingService.CoolerChange.cs
+++ b/src/Exo/Service/Exo.Service.Core/CoolingService.CoolerChange.cs
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
+using System.Numerics;
using Exo.Cooling;
namespace Exo.Service;
@@ -37,6 +38,10 @@ public static CoolerChange CreateManual(IManualCooler cooler, byte power)
return change;
}
+ public static CoolerChange CreateHardwareCurve(IHardwareCurveCoolerSensorCurveControl sensorControl, IControlCurve curve)
+ where T : struct, INumber
+ => new HardwareCurveCoolerChange(sensorControl, curve);
+
private sealed class AutomaticCoolerChange : CoolerChange
{
private IAutomaticCooler? _cooler;
@@ -81,5 +86,23 @@ public override void Execute()
}
}
}
+
+ private sealed class HardwareCurveCoolerChange : CoolerChange
+ where T : struct, INumber
+ {
+ private readonly IHardwareCurveCoolerSensorCurveControl _sensorControl;
+ private readonly IControlCurve _curve;
+
+ public HardwareCurveCoolerChange(IHardwareCurveCoolerSensorCurveControl sensorControl, IControlCurve curve)
+ {
+ _sensorControl = sensorControl;
+ _curve = curve;
+ }
+
+ public override void Execute()
+ {
+ _sensorControl.SetControlCurve(_curve);
+ }
+ }
}
}
diff --git a/src/Exo/Service/Exo.Service.Core/CoolingService.cs b/src/Exo/Service/Exo.Service.Core/CoolingService.cs
index df9d658d..f186aeb0 100644
--- a/src/Exo/Service/Exo.Service.Core/CoolingService.cs
+++ b/src/Exo/Service/Exo.Service.Core/CoolingService.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using System.Threading.Channels;
@@ -280,7 +281,7 @@ private async ValueTask HandleArrivalAsync(DeviceWatchNotification notification,
if (cooler is IHardwareCurveCooler hardwareCurveCooler)
{
coolingModes |= CoolingModes.HardwareControlCurve;
- hardwareCurveInputSensorIds = hardwareCurveCooler.AvailableInputSensors;
+ hardwareCurveInputSensorIds = ImmutableArray.CreateRange(hardwareCurveCooler.AvailableSensors, s => s.SensorId);
}
var info = new CoolerInformation(cooler.CoolerId, cooler.SpeedSensorId, cooler.Type, coolingModes, powerLimits, hardwareCurveInputSensorIds);
addedCoolerInfosById.Add(info.CoolerId, info);
@@ -420,6 +421,7 @@ public async IAsyncEnumerable WatchDevicesAsync([Enume
private sealed class CoolerState
{
private static readonly object AutomaticPowerState = new();
+ private static readonly object HardwareCurveState = new();
private readonly SensorService _sensorService;
private readonly ICooler _cooler;
@@ -493,6 +495,79 @@ CancellationToken cancellationToken
}
}
+ public async ValueTask SetHardwareCurveAsync(Guid sensorId, InterpolatedSegmentControlCurve controlCurve, CancellationToken cancellationToken)
+ where TInput : struct, INumber
+ {
+ if (_cooler is not IHardwareCurveCooler cooler) throw new InvalidOperationException("This cooler does not support hardware curves.");
+ var sensor = FindSensor(cooler, sensorId);
+
+ switch (SensorService.GetSensorDataType(sensor.ValueType))
+ {
+ case SensorDataType.UInt8:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.UInt16:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.UInt32:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.UInt64:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.UInt128:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.SInt8:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.SInt16:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.SInt32:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.SInt64:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.SInt128:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.Float16:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.Float32:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ case SensorDataType.Float64:
+ await SetHardwareCurveAsync(sensor, controlCurve.CastInput(), cancellationToken).ConfigureAwait(false);
+ break;
+ default:
+ throw new InvalidOperationException("Unsupported sensor data type.");
+ }
+ }
+
+ private async ValueTask SetHardwareCurveAsync(IHardwareCurveCoolerSensorCurveControl sensor, InterpolatedSegmentControlCurve controlCurve, CancellationToken cancellationToken)
+ where TInput : struct, INumber
+ {
+ if (sensor is not IHardwareCurveCoolerSensorCurveControl typedSensor) throw new InvalidOperationException("This sensor has an incompatible value type.");
+ using (await _lock.WaitAsync(cancellationToken).ConfigureAwait(false))
+ {
+ if (_activeState is IAsyncDisposable disposable) await disposable.DisposeAsync();
+ _activeState = HardwareCurveState;
+ _changeWriter.TryWrite(CoolerChange.CreateHardwareCurve(typedSensor, controlCurve));
+ }
+ }
+
+ private static IHardwareCurveCoolerSensorCurveControl FindSensor(IHardwareCurveCooler cooler, Guid sensorId)
+ {
+ foreach (var sensor in cooler.AvailableSensors)
+ {
+ if (sensor.SensorId == sensorId) return sensor;
+ }
+ throw new InvalidOperationException("The specified sensor ID is not a valid hardware control curve input source.");
+ }
+
public void SendManualPowerUpdate(byte power) => _changeWriter.TryWrite(CoolerChange.CreateManual(Unsafe.As(_cooler), power));
}
@@ -656,6 +731,15 @@ public async ValueTask SetSoftwareControlCurveAsync(Guid coolingDeviceId
}
}
+ public async ValueTask SetHardwareControlCurveAsync(Guid coolingDeviceId, Guid coolerId, Guid sensorId, InterpolatedSegmentControlCurve controlCurve, CancellationToken cancellationToken)
+ where TInput : struct, INumber
+ {
+ if (TryGetCoolerState(coolingDeviceId, coolerId, out var state))
+ {
+ await state.SetHardwareCurveAsync(sensorId, controlCurve, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
private bool TryGetCoolerState(Guid deviceId, Guid coolerId, [NotNullWhen(true)] out CoolerState? state)
{
if (_deviceStates.TryGetValue(deviceId, out var deviceState))
diff --git a/src/Exo/Service/Exo.Service.Core/SensorService.cs b/src/Exo/Service/Exo.Service.Core/SensorService.cs
index 191225ba..0334d19a 100644
--- a/src/Exo/Service/Exo.Service.Core/SensorService.cs
+++ b/src/Exo/Service/Exo.Service.Core/SensorService.cs
@@ -190,6 +190,7 @@ private static class SensorDataTypes
}
public static SensorDataType GetSensorDataType() where T : INumber => SensorDataTypes.DataType;
+ public static SensorDataType GetSensorDataType(Type type) => TypeToSensorDataTypeMapping[type];
// Helper method that will ensure a cancellation token source is wiped out properly and exactly once. (Because the Dispose method can throw if called twice…)
private static void ClearAndDisposeCancellationTokenSource(ref CancellationTokenSource? cancellationTokenSource)