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)