Skip to content

Commit

Permalink
✨ Hardware cooling curves: Implementation and design refinement.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Jan 3, 2025
1 parent 712950b commit b869e23
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 31 deletions.
43 changes: 34 additions & 9 deletions src/Exo/Core/Exo.Core/Cooling/ICooler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Exo.Sensors;

namespace Exo.Cooling;

Expand Down Expand Up @@ -87,14 +88,40 @@ public interface IManualCooler : IConfigurableCooler

public interface IHardwareCurveCooler : IConfigurableCooler
{
/// <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>Gets a list of all possible source sensors.</summary>
/// <remarks>
/// <para>
/// For each internal sensor source that can be used with this cooler, one separate instance of <see cref="IHardwareCurveCoolerSensorCurveControl"/> will be provided.
/// </para>
/// <para>
/// 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 <see cref="IHardwareCurveCoolerSensorCurveControl"/>.
/// </para>
/// </remarks>
ImmutableArray<IHardwareCurveCoolerSensorCurveControl> AvailableSensors { get; }

bool TryGetActiveSensor([NotNullWhen(true)] out IHardwareCurveCoolerSensorCurveControl? sensor);
}

public interface IHardwareCurveCooler<T> : IHardwareCurveCooler
where T: struct, INumber<T>
/// <summary>Allows controlling a hardware cooling curve based on a sensor.</summary>
/// <remarks>
/// Instances of <see cref="IHardwareCurveCoolerSensorCurveControl"/> must also implement <see cref="IHardwareCurveCoolerSensorCurveControl{T}"/> for exactly one datatype.
/// Said datatype must be the one returned by <see cref="ValueType"/>.
/// </remarks>
public interface IHardwareCurveCoolerSensorCurveControl
{
Guid SensorId { get; }
SensorUnit Unit { get; }
Type ValueType { get; }
}

/// <summary></summary>
/// <typeparam name="TInput"></typeparam>
public interface IHardwareCurveCoolerSensorCurveControl<TInput> : IHardwareCurveCoolerSensorCurveControl
where TInput : struct, INumber<TInput>
{
Type IHardwareCurveCoolerSensorCurveControl.ValueType => typeof(TInput);

/// <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>
Expand All @@ -105,20 +132,18 @@ public interface IHardwareCurveCooler<T> : IHardwareCurveCooler
/// 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);
void SetControlCurve(IControlCurve<TInput, 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);
bool TryGetControlCurve([NotNullWhen(true)] out IControlCurve<TInput, byte>? curve);
}

public readonly struct HardwareCoolingInput
Expand Down
76 changes: 55 additions & 21 deletions src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -502,22 +502,19 @@ 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<byte, byte> curve)
private static void SetControlCurve(byte[] dest, ref byte state, 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)
public static bool TryGetControlCurve(byte[] src, byte state, [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++)
{
Expand All @@ -526,23 +523,30 @@ public static bool TryGetControlCurve(byte[] src, byte state, out Guid inputId,
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 sealed class FanCooler : ICooler, IManualCooler, IHardwareCurveCooler, IHardwareCurveCoolerSensorCurveControl<byte>
{
private readonly KrakenDriver _driver;
private readonly ImmutableArray<IHardwareCurveCoolerSensorCurveControl> _availableSensors;

public FanCooler(KrakenDriver driver) => _driver = driver;
public FanCooler(KrakenDriver driver)
{
_driver = driver;
_availableSensors = [this];
}

public Guid CoolerId => FanCoolerId;
public CoolerType Type => CoolerType.Fan;

public Guid? SpeedSensorId => 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;
Expand All @@ -566,27 +570,46 @@ public bool TryGetPower(out byte power)
return false;
}

public ImmutableArray<Guid> AvailableInputSensors => InputSensors;
public ImmutableArray<IHardwareCurveCoolerSensorCurveControl> 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<byte, byte> curve)
=> KrakenDriver.SetControlCurve(_driver._fanCoolingCurve, ref _driver._fanState, inputId, curve);
public void SetControlCurve(IControlCurve<byte, byte> curve)
=> KrakenDriver.SetControlCurve(_driver._fanCoolingCurve, ref _driver._fanState, curve);

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

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

public PumpCooler(KrakenDriver driver) => _driver = driver;
public PumpCooler(KrakenDriver driver)
{
_driver = driver;
_availableSensors = [this];
}

public Guid CoolerId => PumpCoolerId;
public CoolerType Type => CoolerType.Pump;

public Guid? SpeedSensorId => 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;
Expand All @@ -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)
Expand All @@ -610,12 +633,23 @@ public bool TryGetPower(out byte power)
return false;
}

public ImmutableArray<Guid> AvailableInputSensors => InputSensors;
public ImmutableArray<IHardwareCurveCoolerSensorCurveControl> 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<byte, byte> curve)
=> KrakenDriver.SetControlCurve(_driver._pumpCoolingCurve, ref _driver._pumpState, inputId, curve);
public void SetControlCurve(IControlCurve<byte, byte> curve)
=> KrakenDriver.SetControlCurve(_driver._pumpCoolingCurve, ref _driver._pumpState, curve);

public bool TryGetControlCurve(out Guid inputId, [NotNullWhen(true)] out IControlCurve<byte, byte>? curve)
=> KrakenDriver.TryGetControlCurve(_driver._pumpCoolingCurve, _driver._pumpState, out inputId, out curve);
public bool TryGetControlCurve([NotNullWhen(true)] out IControlCurve<byte, byte>? curve)
=> KrakenDriver.TryGetControlCurve(_driver._pumpCoolingCurve, _driver._pumpState, out curve);
}
}
23 changes: 23 additions & 0 deletions src/Exo/Service/Exo.Service.Core/CoolingService.CoolerChange.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Numerics;
using Exo.Cooling;

namespace Exo.Service;
Expand Down Expand Up @@ -37,6 +38,10 @@ public static CoolerChange CreateManual(IManualCooler cooler, byte power)
return change;
}

public static CoolerChange CreateHardwareCurve<T>(IHardwareCurveCoolerSensorCurveControl<T> sensorControl, IControlCurve<T, byte> curve)
where T : struct, INumber<T>
=> new HardwareCurveCoolerChange<T>(sensorControl, curve);

private sealed class AutomaticCoolerChange : CoolerChange
{
private IAutomaticCooler? _cooler;
Expand Down Expand Up @@ -81,5 +86,23 @@ public override void Execute()
}
}
}

private sealed class HardwareCurveCoolerChange<T> : CoolerChange
where T : struct, INumber<T>
{
private readonly IHardwareCurveCoolerSensorCurveControl<T> _sensorControl;
private readonly IControlCurve<T, byte> _curve;

public HardwareCurveCoolerChange(IHardwareCurveCoolerSensorCurveControl<T> sensorControl, IControlCurve<T, byte> curve)
{
_sensorControl = sensorControl;
_curve = curve;
}

public override void Execute()
{
_sensorControl.SetControlCurve(_curve);
}
}
}
}
86 changes: 85 additions & 1 deletion src/Exo/Service/Exo.Service.Core/CoolingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -420,6 +421,7 @@ public async IAsyncEnumerable<CoolingDeviceInformation> 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;
Expand Down Expand Up @@ -493,6 +495,79 @@ CancellationToken cancellationToken
}
}

public async ValueTask SetHardwareCurveAsync<TInput>(Guid sensorId, InterpolatedSegmentControlCurve<TInput, byte> controlCurve, CancellationToken cancellationToken)
where TInput : struct, INumber<TInput>
{
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<byte>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.UInt16:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<ushort>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.UInt32:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<uint>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.UInt64:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<ulong>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.UInt128:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<UInt128>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.SInt8:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<sbyte>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.SInt16:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<short>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.SInt32:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<int>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.SInt64:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<long>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.SInt128:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<Int128>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.Float16:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<Half>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.Float32:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<float>(), cancellationToken).ConfigureAwait(false);
break;
case SensorDataType.Float64:
await SetHardwareCurveAsync(sensor, controlCurve.CastInput<double>(), cancellationToken).ConfigureAwait(false);
break;
default:
throw new InvalidOperationException("Unsupported sensor data type.");
}
}

private async ValueTask SetHardwareCurveAsync<TInput>(IHardwareCurveCoolerSensorCurveControl sensor, InterpolatedSegmentControlCurve<TInput, byte> controlCurve, CancellationToken cancellationToken)
where TInput : struct, INumber<TInput>
{
if (sensor is not IHardwareCurveCoolerSensorCurveControl<TInput> 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<IManualCooler>(_cooler), power));
}

Expand Down Expand Up @@ -656,6 +731,15 @@ public async ValueTask SetSoftwareControlCurveAsync<TInput>(Guid coolingDeviceId
}
}

public async ValueTask SetHardwareControlCurveAsync<TInput>(Guid coolingDeviceId, Guid coolerId, Guid sensorId, InterpolatedSegmentControlCurve<TInput, byte> controlCurve, CancellationToken cancellationToken)
where TInput : struct, INumber<TInput>
{
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))
Expand Down
1 change: 1 addition & 0 deletions src/Exo/Service/Exo.Service.Core/SensorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ private static class SensorDataTypes<T>
}

public static SensorDataType GetSensorDataType<T>() where T : INumber<T> => SensorDataTypes<T>.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)
Expand Down

0 comments on commit b869e23

Please sign in to comment.