diff --git a/src/Exo/Core/Exo.Core/Cooling/ICooler.cs b/src/Exo/Core/Exo.Core/Cooling/ICooler.cs index 4875132e..7d5dae7a 100644 --- a/src/Exo/Core/Exo.Core/Cooling/ICooler.cs +++ b/src/Exo/Core/Exo.Core/Cooling/ICooler.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Numerics; namespace Exo.Cooling; @@ -86,10 +88,40 @@ public interface IManualCooler : IConfigurableCooler public interface IHardwareCurveCooler : IConfigurableCooler where T: struct, INumber { - // TODO: - // ImmutableArray AvailableInputs; - // void SetControlCurve(Guid inputId, ControlCurve curve); - // bool TryGetControlCurve(out ControlCurve); + /// 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; } + + /// Sets the control curve to be used by this cooler. + /// + /// Only the sensors referenced by can be used as control curve input. + /// + /// Upon receiving a valid request, the driver shall reinterpret the curve so as to fit the internal data model that suits the device. + /// Drivers are in no way expected to preserve the control curve reference provided as an input, and callers should ot assume that the data can be read back again. + /// Drivers are expected to return the active curve upon a call to , + /// 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); + + /// Gets the applied cooling curve. + /// + /// The curve returned can be different than the one provided to in an earlier call. + /// 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); +} + +public readonly struct HardwareCoolingInput +{ + /// Gets the built-in sensor ID, if any is available. + public Guid SensorId { get; } } /// Identifies the type of a cooler. diff --git a/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs b/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs index d3a1970e..28dde66f 100644 --- a/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs +++ b/src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs @@ -1,5 +1,6 @@ using System.Buffers.Binary; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -62,6 +63,8 @@ public class KrakenDriver : 57, 58, 59, 61, 62, ]; + private static readonly ImmutableArray InputSensors = [LiquidTemperatureSensorId]; + private const int NzxtVendorId = 0x1E71; [DiscoverySubsystem] @@ -228,6 +231,9 @@ static uint GetColor(byte r, byte g, byte b) return data; } + private const byte CoolingStateCurve = 0b01; + private const byte CoolingStateChanged = 0b10; + private readonly KrakenHidTransport _hidTransport; private readonly KrakenImageStorageManager? _storageManager; private readonly ISensor[] _sensors; @@ -245,10 +251,13 @@ static uint GetColor(byte r, byte g, byte b) private readonly ushort _imageWidth; private readonly ushort _imageHeight; - private byte _lastPumpSpeedTarget; - private byte _currentPumpSpeedTarget; - private byte _lastFanSpeedTarget; - private byte _currentFanSpeedTarget; + private byte _pumpSpeedTarget; + private byte _pumpState; + private byte _fanSpeedTarget; + private byte _fanState; + + private readonly byte[] _pumpCoolingCurve; + private readonly byte[] _fanCoolingCurve; public override DeviceCategory DeviceCategory => DeviceCategory.Cooler; DeviceId IDeviceIdFeature.DeviceId => DeviceId.ForUsb(NzxtVendorId, _productId, _versionNumber); @@ -285,6 +294,8 @@ DeviceConfigurationKey configurationKey _storageManager = storageManager; _productId = productId; _versionNumber = versionNumber; + _pumpCoolingCurve = (byte[])DefaultPumpCurve.Clone(); + _fanCoolingCurve = (byte[])DefaultFanCurve.Clone(); _sensors = [new LiquidTemperatureSensor(this), new PumpSpeedSensor(this), new FanSpeedSensor(this)]; _coolers = [new PumpCooler(this), new FanCooler(this)]; _genericFeatures = ConfigurationKey.UniqueId is not null ? @@ -350,8 +361,8 @@ async ValueTask ICoolingControllerFeature.ApplyChangesAsync(CancellationToken ca ValueTask pumpSetTask = ValueTask.CompletedTask; ValueTask fanSetTask = ValueTask.CompletedTask; - if (_lastPumpSpeedTarget != _currentPumpSpeedTarget) pumpSetTask = UpdatePumpPowerAsync(_currentPumpSpeedTarget, cancellationToken); - if (_lastFanSpeedTarget != _currentFanSpeedTarget) fanSetTask = UpdateFanPowerAsync(_currentFanSpeedTarget, cancellationToken); + if ((_pumpState & CoolingStateChanged) != 0) pumpSetTask = UpdatePumpPowerAsync(_pumpSpeedTarget, cancellationToken); + if ((_fanState & CoolingStateChanged) != 0) fanSetTask = UpdateFanPowerAsync(_fanSpeedTarget, cancellationToken); List? exceptions = null; bool operationCanceled = false; @@ -393,13 +404,13 @@ async ValueTask ICoolingControllerFeature.ApplyChangesAsync(CancellationToken ca private async ValueTask UpdatePumpPowerAsync(byte power, CancellationToken cancellationToken) { await _hidTransport.SetPumpPowerAsync(power, cancellationToken).ConfigureAwait(false); - _lastPumpSpeedTarget = power; + _pumpState = 0; } private async ValueTask UpdateFanPowerAsync(byte power, CancellationToken cancellationToken) { await _hidTransport.SetFanPowerAsync(power, cancellationToken).ConfigureAwait(false); - _lastFanSpeedTarget = power; + _fanState = 0; } private abstract class Sensor @@ -475,7 +486,36 @@ public FanSpeedSensor(KrakenDriver driver) : base(driver) { } protected override ushort ReadValue(KrakenReadings readings) => readings.FanSpeed; } - private sealed class FanCooler : ICooler, IManualCooler + private static void SetControlCurve(byte[] dest, ref byte state, Guid inputId, IControlCurve curve) + { + if (inputId != LiquidTemperatureSensorId) throw new ArgumentException(); + + for (int i = 0; i < dest.Length; i++) + { + dest[i] = curve[(byte)(20 + i)]; + } + state = CoolingStateChanged | CoolingStateCurve; + } + + public static bool TryGetControlCurve(byte[] src, byte state, out Guid inputId, [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++) + { + dataPoints[i] = new((byte)(20 + i), src[i]); + } + curve = new InterpolatedSegmentControlCurve(ImmutableCollectionsMarshal.AsImmutableArray(dataPoints), MonotonicityValidators.IncreasingUpTo100); + return true; + } + inputId = default; + curve = null; + return false; + } + + private sealed class FanCooler : ICooler, IManualCooler, IHardwareCurveCooler { private readonly KrakenDriver _driver; @@ -495,17 +535,31 @@ private sealed class FanCooler : ICooler, IManualCooler public void SetPower(byte power) { ArgumentOutOfRangeException.ThrowIfGreaterThan(power, 100); - _driver._currentFanSpeedTarget = power; + _driver._fanSpeedTarget = power; + _driver._fanState = CoolingStateChanged; } public bool TryGetPower(out byte power) { - power = _driver._currentFanSpeedTarget; - return true; + if ((_driver._fanState & CoolingStateCurve) == 0) + { + power = _driver._fanSpeedTarget; + return true; + } + power = 0; + return false; } + + public ImmutableArray AvailableInputSensors => InputSensors; + + public void SetControlCurve(Guid inputId, IControlCurve curve) + => KrakenDriver.SetControlCurve(_driver._fanCoolingCurve, ref _driver._fanState, inputId, curve); + + public bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve? curve) + => KrakenDriver.TryGetControlCurve(_driver._fanCoolingCurve, _driver._fanState, out inputId, out curve); } - private sealed class PumpCooler : ICooler, IManualCooler + private sealed class PumpCooler : ICooler, IManualCooler, IHardwareCurveCooler { private readonly KrakenDriver _driver; @@ -525,13 +579,27 @@ private sealed class PumpCooler : ICooler, IManualCooler public void SetPower(byte power) { ArgumentOutOfRangeException.ThrowIfGreaterThan(power, 100); - _driver._currentPumpSpeedTarget = power; + _driver._pumpSpeedTarget = power; + _driver._fanState = CoolingStateChanged; } public bool TryGetPower(out byte power) { - power = _driver._currentPumpSpeedTarget; - return true; + if ((_driver._pumpState & CoolingStateCurve) == 0) + { + power = _driver._pumpSpeedTarget; + return true; + } + power = 0; + return false; } + + public ImmutableArray AvailableInputSensors => InputSensors; + + public void SetControlCurve(Guid inputId, IControlCurve curve) + => KrakenDriver.SetControlCurve(_driver._pumpCoolingCurve, ref _driver._pumpState, inputId, curve); + + public bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve? curve) + => KrakenDriver.TryGetControlCurve(_driver._pumpCoolingCurve, _driver._pumpState, out inputId, out curve); } }