Skip to content

Commit

Permalink
✨ Kraken: Start implementing support for HW control curves.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Jan 2, 2025
1 parent 83cc9a3 commit 62d349e
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 20 deletions.
40 changes: 36 additions & 4 deletions src/Exo/Core/Exo.Core/Cooling/ICooler.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;

namespace Exo.Cooling;
Expand Down Expand Up @@ -86,10 +88,40 @@ public interface IManualCooler : IConfigurableCooler
public interface IHardwareCurveCooler<T> : IConfigurableCooler
where T: struct, INumber<T>
{
// TODO:
// ImmutableArray<CoolingInput> AvailableInputs;
// void SetControlCurve(Guid inputId, ControlCurve<T, byte> curve);
// bool TryGetControlCurve(out ControlCurve<T, byte>);
/// <summary>Gets a list of all available input sensors.</summary>
/// <remarks>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.</remarks>
ImmutableArray<Guid> AvailableInputSensors { get; }

/// <summary>Sets the control curve to be used by this cooler.</summary>
/// <remarks>
/// <para>Only the sensors referenced by <see cref="AvailableInputSensors"/> can be used as control curve input.</para>
/// <para>
/// 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 <see cref="TryGetControlCurve(out Guid, out IControlCurve{T, byte}?)"/>,
/// using data that fit their internal representation the best.
/// </para>
/// </remarks>
/// <param name="inputId">The sensor to use as an input.</param>
/// <param name="curve">The control curve to apply.</param>
void SetControlCurve(Guid inputId, IControlCurve<T, byte> curve);

/// <summary>Gets the applied cooling curve.</summary>
/// <remarks>
/// The curve returned can be different than the one provided to <see cref="SetControlCurve(Guid, IControlCurve{T, byte})"/> 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.
/// </remarks>
/// <param name="inputId">The sensor used as input of this source, if any.</param>
/// <param name="curve">The curve currently applied on the cooler, if any.</param>
/// <returns></returns>
bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve<T, byte>? curve);
}

public readonly struct HardwareCoolingInput
{
/// <summary>Gets the built-in sensor ID, if any is available.</summary>
public Guid SensorId { get; }
}

/// <summary>Identifies the type of a cooler.</summary>
Expand Down
100 changes: 84 additions & 16 deletions src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -62,6 +63,8 @@ public class KrakenDriver :
57, 58, 59, 61, 62,
];

private static readonly ImmutableArray<Guid> InputSensors = [LiquidTemperatureSensorId];

private const int NzxtVendorId = 0x1E71;

[DiscoverySubsystem<HidDiscoverySubsystem>]
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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 ?
Expand Down Expand Up @@ -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<Exception>? exceptions = null;
bool operationCanceled = false;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<byte, byte> 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<byte, byte>? curve)
{
if ((state & CoolingStateCurve) != 0)
{
inputId = LiquidTemperatureSensorId;
var dataPoints = new DataPoint<byte, byte>[src.Length];
for (int i = 0; i < src.Length; i++)
{
dataPoints[i] = new((byte)(20 + i), src[i]);
}
curve = new InterpolatedSegmentControlCurve<byte, byte>(ImmutableCollectionsMarshal.AsImmutableArray(dataPoints), MonotonicityValidators<byte>.IncreasingUpTo100);
return true;
}
inputId = default;
curve = null;
return false;
}

private sealed class FanCooler : ICooler, IManualCooler, IHardwareCurveCooler<byte>
{
private readonly KrakenDriver _driver;

Expand All @@ -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<Guid> AvailableInputSensors => InputSensors;

public void SetControlCurve(Guid inputId, IControlCurve<byte, byte> curve)
=> KrakenDriver.SetControlCurve(_driver._fanCoolingCurve, ref _driver._fanState, inputId, curve);

public bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve<byte, byte>? curve)
=> KrakenDriver.TryGetControlCurve(_driver._fanCoolingCurve, _driver._fanState, out inputId, out curve);
}

private sealed class PumpCooler : ICooler, IManualCooler
private sealed class PumpCooler : ICooler, IManualCooler, IHardwareCurveCooler<byte>
{
private readonly KrakenDriver _driver;

Expand All @@ -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<Guid> AvailableInputSensors => InputSensors;

public void SetControlCurve(Guid inputId, IControlCurve<byte, byte> curve)
=> KrakenDriver.SetControlCurve(_driver._pumpCoolingCurve, ref _driver._pumpState, inputId, curve);

public bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve<byte, byte>? curve)
=> KrakenDriver.TryGetControlCurve(_driver._pumpCoolingCurve, _driver._pumpState, out inputId, out curve);
}
}

0 comments on commit 62d349e

Please sign in to comment.