Skip to content

Commit

Permalink
✨ Add initial support for Corsair HX1200i.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Apr 9, 2024
1 parent 7978802 commit 238b149
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 18 deletions.
1 change: 1 addition & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<ProjectReference Update="..\Exo.Discovery.System\Exo.Discovery.System.csproj" Private="false" ExcludeAssets="runtime" />
<ProjectReference Update="..\Exo.Discovery.SmBios\Exo.Discovery.SmBios.csproj" Private="false" ExcludeAssets="runtime" />
<ProjectReference Update="..\DeviceTools.Core\DeviceTools.Core.csproj" Private="false" ExcludeAssets="runtime" />
<ProjectReference Update="..\DeviceTools.Numerics\DeviceTools.Numerics.csproj" Private="false" ExcludeAssets="runtime" />
<ProjectReference Update="..\DeviceTools.DeviceIds.Databases\DeviceTools.DeviceIds.Databases.csproj" Private="false" ExcludeAssets="runtime" />
<ProjectReference Update="..\DeviceTools.DeviceIds.Usb\DeviceTools.DeviceIds.Usb.csproj" Private="false" ExcludeAssets="runtime" />
<ProjectReference Update="..\DeviceTools.DisplayDevices\DeviceTools.DisplayDevices.csproj" Private="false" ExcludeAssets="runtime" />
Expand Down
11 changes: 2 additions & 9 deletions Exo.Core/Features/SensorFeatures.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
using System.Collections.Immutable;
using Exo.Sensors;

namespace Exo.Features;

public interface ISensorsFeature : ISensorDeviceFeature
{
IEnumerable<ISensor> Sensors { get; }
}

public interface IPolledSensorsFeature : ISensorDeviceFeature
{
}

public interface IStreamedSensorsFeature : ISensorDeviceFeature
{
ImmutableArray<ISensor> Sensors { get; }
}
298 changes: 289 additions & 9 deletions Exo.Devices.Corsair.PowerSupplies/CorsairLinkDriver.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,320 @@
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using System.Text;
using DeviceTools;
using DeviceTools.HumanInterfaceDevices;
using DeviceTools.Numerics;
using Exo;
using Exo.Discovery;
using Exo.Features;
using Exo.Sensors;
using Microsoft.Extensions.Logging;

namespace Exo.Devices.Corsair.PowerSupplies;

public sealed class CorsairLinkDriver : Driver
public sealed class CorsairLinkDriver : Driver, IDeviceDriver<ISensorDeviceFeature>, ISensorsFeature
{
private const ushort CorsairVendorId = 0x1B1C;

[DiscoverySubsystem<HidDiscoverySubsystem>]
[ProductId(VendorIdSource.Usb, 0x1b1c, 0x1c08)]
[ProductId(VendorIdSource.Usb, CorsairVendorId, 0x1C08)]
public static async ValueTask<DriverCreationResult<SystemDevicePath>?> CreateAsync
(
ILogger<CorsairLinkDriver> logger,
ILoggerFactory loggerFactory,
ImmutableArray<SystemDevicePath> keys,
ushort productId,
ushort version,
ImmutableArray<DeviceObjectInformation> deviceInterfaces,
ImmutableArray<DeviceObjectInformation> devices,
string topLevelDeviceName,
CancellationToken cancellationToken
)
{
return null;
if (devices.Length != 2) throw new InvalidOperationException("Expected exactly two devices.");
if (deviceInterfaces.Length != 2) throw new InvalidOperationException("Expected exactly two device interfaces.");

string? deviceName = null;
foreach (var deviceInterface in deviceInterfaces)
{
if (deviceInterface.Properties.TryGetValue(Properties.System.Devices.InterfaceClassGuid.Key, out Guid interfaceClassGuid) && interfaceClassGuid == DeviceInterfaceClassGuids.Hid)
{
deviceName = deviceInterface.Id;
}
}

if (deviceName is null) throw new InvalidOperationException("HID device interface not found.");

var stream = new HidFullDuplexStream(deviceName);
CorsairLinkHidTransport transport;
try
{
transport = await CorsairLinkHidTransport.CreateAsync(loggerFactory.CreateLogger<CorsairLinkHidTransport>(), stream, cancellationToken).ConfigureAwait(false);
string friendlyName = await transport.ReadStringAsync(0x9A, cancellationToken).ConfigureAwait(false);
return new DriverCreationResult<SystemDevicePath>
(
keys,
new CorsairLinkDriver
(
loggerFactory.CreateLogger<CorsairLinkDriver>(),
transport,
friendlyName,
new DeviceConfigurationKey("Corsair", topLevelDeviceName, $"{CorsairVendorId:X4}{productId:X4}", null)
)
);
}
catch
{
await stream.DisposeAsync().ConfigureAwait(false);
throw;
}
}

public CorsairLinkDriver
private readonly CorsairLinkHidTransport _transport;
private readonly IDeviceFeatureSet<ISensorDeviceFeature> _sensorFeatures;
private readonly ImmutableArray<ISensor> _sensors;
private readonly ILogger<CorsairLinkDriver> _logger;

public override DeviceCategory DeviceCategory => DeviceCategory.PowerSupply;

public IDeviceFeatureSet<ISensorDeviceFeature> Features => _sensorFeatures;
public ImmutableArray<ISensor> Sensors => _sensors;

private CorsairLinkDriver
(
HidFullDuplexStream stream,
ILogger<CorsairLinkDriver> logger,
CorsairLinkHidTransport transport,
string friendlyName,
DeviceConfigurationKey configurationKey
) : base(friendlyName, configurationKey)
{
_transport = transport;
_logger = logger;
_sensorFeatures = FeatureSet.Create<ISensorDeviceFeature, CorsairLinkDriver, ISensorsFeature>(this);
}

public override DeviceCategory DeviceCategory => DeviceCategory.PowerSupply;
public override async ValueTask DisposeAsync()
{
await _transport.DisposeAsync().ConfigureAwait(false);
}
}

// NB: This likely not the V1 protocol, but it is the one for HX1200i & similar devices.
internal sealed class CorsairLinkHidTransport : IAsyncDisposable
{
private interface IPendingCommand
{
void WriteRequest(Span<byte> buffer);
void ProcessResponse(ReadOnlySpan<byte> buffer);
Task WaitAsync(CancellationToken cancellationToken);
void Cancel();
}

private interface IPendingCommand<T> : IPendingCommand
{
new Task<T> WaitAsync(CancellationToken cancellationToken);
}

private abstract class ResultCommand<T> : TaskCompletionSource<T>, IPendingCommand<T>
{
public ResultCommand() { }

public abstract void WriteRequest(Span<byte> buffer);
public abstract void ProcessResponse(ReadOnlySpan<byte> buffer);

Task IPendingCommand.WaitAsync(CancellationToken cancellationToken) => Task.WaitAsync(cancellationToken);
public Task<T> WaitAsync(CancellationToken cancellationToken) => Task.WaitAsync(cancellationToken);

public void Cancel() => TrySetCanceled();
}

private sealed class HandshakeCommand : ResultCommand<string>
{
public HandshakeCommand() { }

public override void WriteRequest(Span<byte> buffer)
{
buffer[0] = 0xFE;
buffer[1] = 0x03;
}

public override void ProcessResponse(ReadOnlySpan<byte> buffer)
{
if (buffer[0] == 0xFE && buffer[1] == 0x03)
{
var data = buffer[2..];
int endIndex = data.IndexOf((byte)0);
endIndex = endIndex < 0 ? data.Length : endIndex;
TrySetResult(Encoding.UTF8.GetString(data[..endIndex]));
}
}
}

private abstract class ReadCommand<T> : ResultCommand<T>
{
private readonly byte _command;

public ReadCommand(byte command) => _command = command;

public sealed override void WriteRequest(Span<byte> buffer)
{
buffer[0] = 0x03;
buffer[1] = _command;
}

public sealed override void ProcessResponse(ReadOnlySpan<byte> buffer)
{
if (buffer[0] == 3 && buffer[1] == _command)
{
ProcessResult(buffer[2..]);
}
}

protected abstract void ProcessResult(ReadOnlySpan<byte> data);
}

private sealed class StringReadCommand : ReadCommand<string>
{
public StringReadCommand(byte command) : base(command) { }

protected override void ProcessResult(ReadOnlySpan<byte> data)
{
int endIndex = data.IndexOf((byte)0);
endIndex = endIndex < 0 ? data.Length : endIndex;
TrySetResult(Encoding.UTF8.GetString(data[..endIndex]));
}
}

private sealed class ByteReadCommand : ReadCommand<byte>
{
public ByteReadCommand(byte command) : base(command) { }

protected override void ProcessResult(ReadOnlySpan<byte> data) => TrySetResult(data[0]);
}

private sealed class Linear11ReadCommand : ReadCommand<Linear11>
{
public Linear11ReadCommand(byte command) : base(command) { }

protected override void ProcessResult(ReadOnlySpan<byte> data) => TrySetResult(Linear11.FromRawValue(LittleEndian.ReadUInt16(data[0])));
}

public static async ValueTask<CorsairLinkHidTransport> CreateAsync(ILogger<CorsairLinkHidTransport> logger, HidFullDuplexStream stream, CancellationToken cancellationToken)
{
var transport = new CorsairLinkHidTransport(logger, stream);
try
{
transport._handshakeDeviceName = await transport.HandshakeAsync(cancellationToken).ConfigureAwait(false);
}
catch
{
await transport.DisposeAsync().ConfigureAwait(false);
throw;
}
return transport;
}

// The message length is hardcoded to 64 bytes + report ID.
private const int MessageLength = 65;

private static readonly object DisposedSentinel = new();

private readonly HidFullDuplexStream _stream;
private readonly byte[] _buffers;
private object? _currentWaitState;
private readonly ILogger<CorsairLinkHidTransport> _logger;
private CancellationTokenSource? _cancellationTokenSource;
private readonly Task _readTask;
private string? _handshakeDeviceName;

private CorsairLinkHidTransport(ILogger<CorsairLinkHidTransport> logger, HidFullDuplexStream stream)
{
_stream = stream;
_logger = logger;
_buffers = GC.AllocateUninitializedArray<byte>(2 * MessageLength, true);
_buffers[65] = 0; // Zero-initialize the write report ID.
_cancellationTokenSource = new();
_readTask = ReadAsync(_cancellationTokenSource.Token);
}

public async ValueTask DisposeAsync()
{
if (Interlocked.Exchange(ref _cancellationTokenSource, null) is { } cts)
{
cts.Cancel();
if (Interlocked.Exchange(ref _currentWaitState, DisposedSentinel) is IPendingCommand pendingCommand)
{
pendingCommand.Cancel();
}
await _readTask.ConfigureAwait(false);
_stream.Dispose();
cts.Dispose();
}
}

private async Task ReadAsync(CancellationToken cancellationToken)
{
try
{
var buffer = MemoryMarshal.CreateFromPinnedArray(_buffers, 0, MessageLength);
while (true)
{
// Data is received in fixed length packets, so we expect to always receive exactly the number of bytes that the buffer can hold.
var remaining = buffer;
do
{
int count;
try
{
count = await _stream.ReadAsync(remaining, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}

if (count == 0)
{
return;
}

remaining = remaining[count..];
}
while (remaining.Length != 0);

(Volatile.Read(ref _currentWaitState) as IPendingCommand)?.ProcessResponse(buffer.Span[1..]);
}
}
catch
{
// TODO: Log
}
}

private async ValueTask<T> ExecuteCommandAsync<T>(IPendingCommand<T> waitState, CancellationToken cancellationToken)
{
if (Interlocked.CompareExchange(ref _currentWaitState, waitState, null) is { } oldState)
{
ObjectDisposedException.ThrowIf(ReferenceEquals(oldState, DisposedSentinel), typeof(CorsairLinkHidTransport));
throw new InvalidOperationException("An operation is already running.");
}

var buffer = MemoryMarshal.CreateFromPinnedArray(_buffers, MessageLength, MessageLength);
try
{
waitState.WriteRequest(buffer.Span[1..]);
await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
return await waitState.WaitAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
Interlocked.CompareExchange(ref _currentWaitState, null, waitState);
}
}

private ValueTask<string> HandshakeAsync(CancellationToken cancellationToken) => ExecuteCommandAsync(new HandshakeCommand(), cancellationToken);

public ValueTask<byte> ReadByteAsync(byte command, CancellationToken cancellationToken) => ExecuteCommandAsync(new ByteReadCommand(command), cancellationToken);

public ValueTask<Linear11> ReadLinear11Async(byte command, CancellationToken cancellationToken) => ExecuteCommandAsync(new Linear11ReadCommand(command), cancellationToken);

public override ValueTask DisposeAsync() => ValueTask.CompletedTask;
public ValueTask<string> ReadStringAsync(byte command, CancellationToken cancellationToken) => ExecuteCommandAsync(new StringReadCommand(command), cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<EnableDynamicLoading>true</EnableDynamicLoading>
<IsExoPluginAssembly>true</IsExoPluginAssembly>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand All @@ -12,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\DeviceTools.Core\DeviceTools.Core.csproj" />
<ProjectReference Include="..\DeviceTools.HumanInterfaceDevices\DeviceTools.HumanInterfaceDevices.csproj" />
<ProjectReference Include="..\DeviceTools.Numerics\DeviceTools.Numerics.csproj" />
<ProjectReference Include="..\Exo.Core\Exo.Core.csproj" />
<ProjectReference Include="..\Exo.Discovery.Hid\Exo.Discovery.Hid.csproj" />
<ProjectReference Include="..\Exo.Discovery.System\Exo.Discovery.System.csproj" />
Expand Down
1 change: 1 addition & 0 deletions Exo.Service/DebugAssemblyDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public DebugAssemblyDiscovery()
"Exo.Devices.Intel",
"Exo.Devices.NVidia",
"Exo.Devices.Asus.Aura",
"Exo.Devices.Corsair.PowerSupplies",
#if WITH_FAKE_DEVICES
"Exo.Debug",
#endif
Expand Down
2 changes: 2 additions & 0 deletions Exo.Service/Exo.Service.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<ProjectReference Include="..\DeviceTools.DisplayDevices\DeviceTools.DisplayDevices.csproj" />
<ProjectReference Include="..\DeviceTools.HumanInterfaceDevices\DeviceTools.HumanInterfaceDevices.csproj" />
<ProjectReference Include="..\DeviceTools.Logitech.HidPlusPlus\DeviceTools.Logitech.HidPlusPlus.csproj" />
<ProjectReference Include="..\DeviceTools.Numerics\DeviceTools.Numerics.csproj" />
<ProjectReference Include="..\Exo.Discovery.Root\Exo.Discovery.Root.csproj" />
<ProjectReference Include="..\Exo.Extensions.Hosting.WindowsServices\Exo.Extensions.Hosting.WindowsServices.csproj" />
<ProjectReference Include="..\Exo.Service.Core\Exo.Service.Core.csproj" />
Expand Down Expand Up @@ -67,6 +68,7 @@
<ProjectReference Include="..\Exo.Devices.Intel\Exo.Devices.Intel.csproj" Private="false" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Exo.Devices.NVidia\Exo.Devices.NVidia.csproj" Private="false" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Exo.Devices.Asus.Aura\Exo.Devices.Asus.Aura.csproj" Private="false" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Exo.Devices.Corsair.PowerSupplies\Exo.Devices.Corsair.PowerSupplies.csproj" Private="false" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Exo.Discovery.Hid\Exo.Discovery.Hid.csproj" Private="false" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Exo.Discovery.System\Exo.Discovery.System.csproj" Private="false" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Exo.Discovery.Pci\Exo.Discovery.Pci.csproj" Private="false" ReferenceOutputAssembly="false" />
Expand Down
Loading

0 comments on commit 238b149

Please sign in to comment.