diff --git a/README.md b/README.md index e112677..23305cd 100644 --- a/README.md +++ b/README.md @@ -7,66 +7,66 @@ The unofficial CorsairLink plugin for [Fan Control](https://github.com/Rem0o/Fan ## Device Support -| Device | Type | PID | Status | Read Fan/Pump RPM | Set Fan/Pump Power | Read Temp Sensor | -| ------------------------------- | ---------- | ---------- | --------------------------- | ----------------- | ------------------ | ---------------- | -| Commander PRO | Controller | `0c10` | Full Support 1 | ✅ | ✅ | ✅ | -| Commander PRO (Obsidian 1000D) | Controller | `1d00` | Full Support 1 | ✅ | ✅ | ✅ | -| Commander CORE XT | Controller | `0c2a` | Full Support 1,2 | ✅ | ✅ | ✅ | -| Commander CORE (ELITE CAPELLIX) | Controller | `0c1c` | Full Support 1,2 | ✅ | ✅ | ✅ | -| Commander CORE | Controller | `0c32` | Full Support 1,2 | ✅ | ✅ | ✅ | -| Commander Mini | Controller | `0c04(3d)` | Full Support 1 | ✅ | ✅ | ✅ | -| Hydro H60i Elite | AIO | `0c34` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H60i Pro XT | AIO | `0c29` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H60i Pro XT | AIO | `0c30` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Elite | AIO | `0c35` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Platinum | AIO | `0c18` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Platinum SE | AIO | `0c19` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Pro XT | AIO | `0c20` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Pro XT | AIO | `0c2d` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Elite | AIO | `0c36` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Platinum | AIO | `0c17` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Pro XT | AIO | `0c21` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Pro XT | AIO | `0c2e` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H150i Elite | AIO | `0c37` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H150i Pro XT | AIO | `0c22` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H150i Pro XT | AIO | `0c2f` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H80i | AIO | `0c04(3b)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H100i | AIO | `0c04(3c)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H100i GT | AIO | `0c04(40)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H110i | AIO | `0c04(42)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H110i GT | AIO | `0c04(41)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| HX550i | PSU | `1c03` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| HX650i | PSU | `1c04` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| HX750i | PSU | `1c05` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| HX850i | PSU | `1c06` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| HX1000i | PSU | `1c07` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| HX1200i | PSU | `1c08` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| HX1000i (2021) | PSU | `1c1e` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| HX1500i (2021) | PSU | `1c1f` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| RM550i | PSU | `1c09` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| RM650i | PSU | `1c0a` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| RM750i | PSU | `1c0b` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| RM850i | PSU | `1c0c` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| RM1000i | PSU | `1c0d` | Full Support 7 | ✅ | ✅ 6 | ✅ | -| AX850i | PSU | `1c0e` | Full Support 8 | ✅ | ✅ 6 | ✅ | -| AX1000i | PSU | `1c0f` | Full Support 8 | ✅ | ✅ 6 | ✅ | -| AX1300i | PSU | `1c10` | Full Support 8 | ✅ | ✅ 6 | ✅ | -| AX1500i | PSU | `1c02` | Full Support 8 | ✅ | ✅ 6 | ✅ | -| AX1600i | PSU | `1c11` | Full Support 8 | ✅ | ✅ 6 | ✅ | -| AX760i/AX860i/AX1200i | PSU | `1c00` | Full Support 8 | ✅ | ✅ 6 | ✅ | -| Hydro H80i GT | AIO | `0c02` | In Development 8 | | | | -| Hydro H80i GT V2 | AIO | `0c08` | In Development 8 | | | | -| Hydro H80i Pro | AIO | `0c16` | In Development 8 | | | | -| Hydro H100i GT V2 | AIO | `0c09` | In Development 8 | | | | -| Hydro H100i GTX | AIO | `0c03` | In Development 8 | | | | -| Hydro H100i Pro | AIO | `0c15` | In Development 8 | | | | -| Hydro H110i GT V2 | AIO | `0c0a` | In Development 8 | | | | -| Hydro H110i GTX | AIO | `0c07` | In Development 8 | | | | -| Hydro H115i Pro | AIO | `0c13` | In Development 8 | | | | -| Hydro H150i Pro | AIO | `0c12` | In Development 8 | | | | -| Cooling Node | Controller | `0c04(38)` | Support Upon Request | | | | -| Hydro H80 | AIO | `0c04(37)` | Support Upon Request | | | | -| Hydro H100 | AIO | `0c04(3a)` | Support Upon Request | | | | +| Device | Type | PID | Status | Read Fan/Pump RPM | Set Fan/Pump Power | Read Temp Sensor | +| ------------------------------- | ---------- | ---------- | ---------------------------------- | ----------------- | ------------------ | ---------------- | +| Commander PRO | Controller | `0c10` | Full Support 1 | ✅ | ✅ | ✅ | +| Commander PRO (Obsidian 1000D) | Controller | `1d00` | Full Support 1 | ✅ | ✅ | ✅ | +| Commander CORE XT | Controller | `0c2a` | Full Support 1,2 | ✅ | ✅ | ✅ | +| Commander CORE (ELITE CAPELLIX) | Controller | `0c1c` | Full Support 1,2 | ✅ | ✅ | ✅ | +| Commander CORE | Controller | `0c32` | Full Support 1,2 | ✅ | ✅ | ✅ | +| Commander Mini | Controller | `0c04(3d)` | Full Support 1 | ✅ | ✅ | ✅ | +| Hydro H60i Elite | AIO | `0c34` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H60i Pro XT | AIO | `0c29` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H60i Pro XT | AIO | `0c30` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H100i Elite | AIO | `0c35` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H100i Platinum | AIO | `0c18` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H100i Platinum SE | AIO | `0c19` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H100i Pro XT | AIO | `0c20` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H100i Pro XT | AIO | `0c2d` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H115i Elite | AIO | `0c36` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H115i Platinum | AIO | `0c17` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H115i Pro XT | AIO | `0c21` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H115i Pro XT | AIO | `0c2e` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H150i Elite | AIO | `0c37` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H150i Pro XT | AIO | `0c22` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H150i Pro XT | AIO | `0c2f` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | +| Hydro H80i | AIO | `0c04(3b)` | Full Support 1 | ✅ | ✅ | ✅ 5 | +| Hydro H100i | AIO | `0c04(3c)` | Full Support 1 | ✅ | ✅ | ✅ 5 | +| Hydro H100i GT | AIO | `0c04(40)` | Full Support 1 | ✅ | ✅ | ✅ 5 | +| Hydro H110i | AIO | `0c04(42)` | Full Support 1 | ✅ | ✅ | ✅ 5 | +| Hydro H110i GT | AIO | `0c04(41)` | Full Support 1 | ✅ | ✅ | ✅ 5 | +| HX550i | PSU | `1c03` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| HX650i | PSU | `1c04` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| HX750i | PSU | `1c05` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| HX850i | PSU | `1c06` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| HX1000i | PSU | `1c07` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| HX1200i | PSU | `1c08` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| HX1000i (2021) | PSU | `1c1e` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| HX1500i (2021) | PSU | `1c1f` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| RM550i | PSU | `1c09` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| RM650i | PSU | `1c0a` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| RM750i | PSU | `1c0b` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| RM850i | PSU | `1c0c` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| RM1000i | PSU | `1c0d` | Full Support 7 | ✅ | ✅ 6 | ✅ | +| AX850i | PSU | `1c0e` | Full Support 8 | ✅ | ✅ 6 | ✅ | +| AX1000i | PSU | `1c0f` | Full Support 8 | ✅ | ✅ 6 | ✅ | +| AX1300i | PSU | `1c10` | Full Support 8 | ✅ | ✅ 6 | ✅ | +| AX1500i | PSU | `1c02` | Full Support 8 | ✅ | ✅ 6 | ✅ | +| AX1600i | PSU | `1c11` | Full Support 8 | ✅ | ✅ 6 | ✅ | +| AX760i/AX860i/AX1200i | PSU | `1c00` | Full Support 8 | ✅ | ✅ 6 | ✅ | +| Hydro H80i GT | AIO | `0c02` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H80i GT V2 | AIO | `0c08` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H80i Pro | AIO | `0c16` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H100i GT V2 | AIO | `0c09` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H100i GTX | AIO | `0c03` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H100i Pro | AIO | `0c15` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H110i GT V2 | AIO | `0c0a` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H110i GTX | AIO | `0c07` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H115i Pro | AIO | `0c13` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Hydro H150i Pro | AIO | `0c12` | 1.4.x Pre-release 1,3,8 | ✅ | ✅ | ✅ 5 | +| Cooling Node | Controller | `0c04(38)` | Support Upon Request | | | | +| Hydro H80 | AIO | `0c04(37)` | Support Upon Request | | | | +| Hydro H100 | AIO | `0c04(3a)` | Support Upon Request | | | | 1. Software mode only. Device lighting will be software-based. diff --git a/src/CorsairLink.Asetek/AsetekCoolerProtocol.cs b/src/CorsairLink.Asetek/AsetekCoolerProtocol.cs new file mode 100644 index 0000000..d82c8cc --- /dev/null +++ b/src/CorsairLink.Asetek/AsetekCoolerProtocol.cs @@ -0,0 +1,68 @@ +using CorsairLink.SiUsbXpress; +using System; + +namespace CorsairLink.Asetek; + +public class AsetekCoolerProtocol : IAsetekDeviceProxy, IDisposable +{ + private bool _disposedValue; + + public AsetekCoolerProtocol(ISiUsbXpressDevice device) + { + Device = device; + } + + protected ISiUsbXpressDevice Device { get; } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Device?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public virtual AsetekDeviceInfo GetDeviceInfo() + { + return new AsetekDeviceInfo( + Device.DeviceInfo.DevicePath, + Device.DeviceInfo.VendorId, + Device.DeviceInfo.ProductId, + Device.DeviceInfo.Name, + Device.DeviceInfo.SerialNumber); + } + + public virtual (bool Opened, Exception? Exception) Open() + { + try + { + Device.Open(); + return (true, default); + } + catch (Exception ex) + { + return (false, ex); + } + } + + public virtual void Close() + { + Device.Close(); + } + + public virtual byte[] WriteAndRead(byte[] buffer) + { + return Device.WriteAndRead(buffer); + } +} diff --git a/src/CorsairLink.Asetek/AsetekDeviceInfo.cs b/src/CorsairLink.Asetek/AsetekDeviceInfo.cs new file mode 100644 index 0000000..29fbc0a --- /dev/null +++ b/src/CorsairLink.Asetek/AsetekDeviceInfo.cs @@ -0,0 +1,19 @@ +namespace CorsairLink.Asetek; + +public sealed class AsetekDeviceInfo +{ + public AsetekDeviceInfo(string devicePath, int vendorId, int productId, string productName, string serialNumber) + { + DevicePath = devicePath; + VendorId = vendorId; + ProductId = productId; + ProductName = productName; + SerialNumber = serialNumber; + } + + public string DevicePath { get; } + public int VendorId { get; } + public int ProductId { get; } + public string ProductName { get; } + public string SerialNumber { get; } +} diff --git a/src/CorsairLink.Asetek/CorsairLink.Asetek.csproj b/src/CorsairLink.Asetek/CorsairLink.Asetek.csproj new file mode 100644 index 0000000..bd235d4 --- /dev/null +++ b/src/CorsairLink.Asetek/CorsairLink.Asetek.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + enable + Latest + + + + + + + + diff --git a/src/CorsairLink.Asetek/IAsetekDeviceProxy.cs b/src/CorsairLink.Asetek/IAsetekDeviceProxy.cs new file mode 100644 index 0000000..c0fee6c --- /dev/null +++ b/src/CorsairLink.Asetek/IAsetekDeviceProxy.cs @@ -0,0 +1,11 @@ +using System; + +namespace CorsairLink.Asetek; + +public interface IAsetekDeviceProxy +{ + AsetekDeviceInfo GetDeviceInfo(); + (bool Opened, Exception? Exception) Open(); + void Close(); + byte[] WriteAndRead(byte[] buffer); +} diff --git a/src/CorsairLink.SiUsbXpress.Driver/AsetekSiUsbXpressDevice.cs b/src/CorsairLink.SiUsbXpress.Driver/AsetekSiUsbXpressDevice.cs new file mode 100644 index 0000000..8d3f7c3 --- /dev/null +++ b/src/CorsairLink.SiUsbXpress.Driver/AsetekSiUsbXpressDevice.cs @@ -0,0 +1,22 @@ +namespace CorsairLink.SiUsbXpress.Driver; + +public class AsetekSiUsbXpressDevice : SiUsbXpressDevice +{ + private const uint DEFAULT_TIMEOUT = 500U; + private const uint READ_BUFFER_SIZE = 32; + + public AsetekSiUsbXpressDevice(SiUsbXpressDeviceInfo deviceInfo) + : base(deviceInfo) + { + } + + protected override uint ReadBufferSize { get; } = READ_BUFFER_SIZE; + + public override void Open() + { + base.Open(); + + _ = SiUsbXpressDriver.SI_SetTimeouts(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT); + FlushBuffers(); + } +} diff --git a/src/CorsairLink.SiUsbXpress.Driver/FlexSiUsbXpressDevice.cs b/src/CorsairLink.SiUsbXpress.Driver/FlexSiUsbXpressDevice.cs new file mode 100644 index 0000000..1e147ea --- /dev/null +++ b/src/CorsairLink.SiUsbXpress.Driver/FlexSiUsbXpressDevice.cs @@ -0,0 +1,66 @@ +namespace CorsairLink.SiUsbXpress.Driver; + +public sealed class FlexSiUsbXpressDevice : SiUsbXpressDevice +{ + private const uint MAX_BAUD_RATE = 115200U; + private const uint DEFAULT_TIMEOUT = 200U; + private const uint READ_BUFFER_SIZE = 16; + + public FlexSiUsbXpressDevice(SiUsbXpressDeviceInfo deviceInfo) + : base(deviceInfo) + { + } + + protected override uint ReadBufferSize { get; } = READ_BUFFER_SIZE; + + public override void Open() + { + base.Open(); + + _ = SiUsbXpressDriver.SI_SetTimeouts(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT); + FlushBuffers(); + _ = SiUsbXpressDriver.SI_SetBaudRate(DeviceHandle!.DangerousGetHandle(), MAX_BAUD_RATE); + } + + protected override byte[] GetReadBuffer(byte[] originalBuffer) + { + var buffer = base.GetReadBuffer(originalBuffer); + return !EncodingHelper.HasError(buffer) + ? EncodingHelper.DecodeData(buffer) + : throw new SiUsbXpressException("Failed to read - data error."); + } + + protected override byte[] GetWriteBuffer(byte[] originalBuffer) + { + var buffer = base.GetWriteBuffer(originalBuffer); + return EncodingHelper.EncodeData(buffer); + } + + private AckStatus ReadAckStatus() + => AckParser.Parse(Read()); + + public override void WriteAndValidate(byte[] buffer) + { + ThrowIfDeviceNotReady(); + + Write(buffer); + AckStatus ackStatus = ReadAckStatus(); + if (ackStatus != AckStatus.Ok) + throw new SiUsbXpressDeviceAckException(ackStatus); + } + + public override void WriteWhileBusy(byte[] buffer) + { + ThrowIfDeviceNotReady(); + + AckStatus ackStatus; + do + { + Write(buffer); + ackStatus = ReadAckStatus(); + } + while (ackStatus == AckStatus.Busy); + if (ackStatus != AckStatus.Ok) + throw new SiUsbXpressDeviceAckException(ackStatus); + } +} diff --git a/src/CorsairLink.SiUsbXpress.Driver/SiUsbXpressDevice.cs b/src/CorsairLink.SiUsbXpress.Driver/SiUsbXpressDevice.cs index 0aabfc6..b03dbab 100644 --- a/src/CorsairLink.SiUsbXpress.Driver/SiUsbXpressDevice.cs +++ b/src/CorsairLink.SiUsbXpress.Driver/SiUsbXpressDevice.cs @@ -1,17 +1,13 @@ -using Microsoft.Win32.SafeHandles; +using Microsoft.Win32.SafeHandles; using System; using System.Linq; using System.Runtime.InteropServices; namespace CorsairLink.SiUsbXpress.Driver; -public sealed class SiUsbXpressDevice : ISiUsbXpressDevice +public abstract class SiUsbXpressDevice : ISiUsbXpressDevice { - private const uint MAX_BAUD_RATE = 115200U; - private const uint DEFAULT_TIMEOUT = 200U; - private const uint READ_BUFFER_SIZE = 16; - - private uint _deviceNumber = 0; + protected uint _deviceNumber = 0; public SiUsbXpressDevice(SiUsbXpressDeviceInfo deviceInfo) { @@ -22,11 +18,13 @@ public SiUsbXpressDevice(SiUsbXpressDeviceInfo deviceInfo) public SiUsbXpressDeviceInfo DeviceInfo { get; } + protected abstract uint ReadBufferSize { get; } + public bool IsOpen => DeviceHandle != null && !DeviceHandle.IsInvalid && !DeviceHandle.IsClosed; - public void Dispose() => Close(); + public virtual void Dispose() => Close(); - public void Close() + public virtual void Close() { if (!IsOpen) return; @@ -42,7 +40,7 @@ public void Close() } } - public void Open() + public virtual void Open() { if (IsOpen) return; @@ -60,78 +58,51 @@ public void Open() _deviceNumber = (uint)deviceNumber.Value; DeviceHandle = new SafeFileHandle(handle, true); - - _ = SiUsbXpressDriver.SI_SetTimeouts(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT); - FlushBuffers(); - _ = SiUsbXpressDriver.SI_SetBaudRate(DeviceHandle!.DangerousGetHandle(), MAX_BAUD_RATE); - } - private static void WriteDataImpl(SafeHandle handle, byte[] data) + protected void WriteInternal(byte[] data) { uint lpdwBytesWritten = 0; - SiUsbXpressDriver.SI_STATUS code = SiUsbXpressDriver.SI_Write(handle.DangerousGetHandle(), data, (uint)data.Length, ref lpdwBytesWritten, IntPtr.Zero); + SiUsbXpressDriver.SI_STATUS code = SiUsbXpressDriver.SI_Write(DeviceHandle!.DangerousGetHandle(), data, (uint)data.Length, ref lpdwBytesWritten, IntPtr.Zero); if (code.IsError() || lpdwBytesWritten != data.Length) throw new SiUsbXpressDriverException(code); } - private static byte[] ReadDataImpl(SafeHandle handle) + protected byte[] ReadInternal() { - byte[] buffer = new byte[READ_BUFFER_SIZE]; + byte[] buffer = new byte[ReadBufferSize]; uint lpdwBytesReturned = 0; - SiUsbXpressDriver.SI_STATUS code = SiUsbXpressDriver.SI_Read(handle.DangerousGetHandle(), buffer, (uint)buffer.Length, ref lpdwBytesReturned, IntPtr.Zero); + SiUsbXpressDriver.SI_STATUS code = SiUsbXpressDriver.SI_Read(DeviceHandle!.DangerousGetHandle(), buffer, (uint)buffer.Length, ref lpdwBytesReturned, IntPtr.Zero); if (code.IsError()) throw new SiUsbXpressDriverException(code); return buffer.Take((int)lpdwBytesReturned).ToArray(); } - public void Write(byte[] buffer) + protected virtual byte[] GetWriteBuffer(byte[] originalBuffer) { - ThrowIfDeviceNotReady(); - - byte[] encodedData = EncodingHelper.EncodeData(buffer); - WriteDataImpl(DeviceHandle!, encodedData); + return originalBuffer; } - public byte[] Read() + public virtual void Write(byte[] buffer) { ThrowIfDeviceNotReady(); - byte[] encodedData = ReadDataImpl(DeviceHandle!); - return !EncodingHelper.HasError(encodedData) - ? EncodingHelper.DecodeData(encodedData) - : throw new SiUsbXpressException("Failed to read - data error."); + WriteInternal(GetWriteBuffer(buffer)); } - public AckStatus ReadAckStatus() - => AckParser.Parse(Read()); - - public void WriteAndValidate(byte[] buffer) + protected virtual byte[] GetReadBuffer(byte[] originalBuffer) { - ThrowIfDeviceNotReady(); - - Write(buffer); - AckStatus ackStatus = ReadAckStatus(); - if (ackStatus != AckStatus.Ok) - throw new SiUsbXpressDeviceAckException(ackStatus); + return originalBuffer; } - public void WriteWhileBusy(byte[] buffer) + public virtual byte[] Read() { ThrowIfDeviceNotReady(); - AckStatus ackStatus; - do - { - Write(buffer); - ackStatus = ReadAckStatus(); - } - while (ackStatus == AckStatus.Busy); - if (ackStatus != AckStatus.Ok) - throw new SiUsbXpressDeviceAckException(ackStatus); + return GetReadBuffer(ReadInternal()); } - public byte[] WriteAndRead(byte[] buffer) + public virtual byte[] WriteAndRead(byte[] buffer) { ThrowIfDeviceNotReady(); @@ -139,20 +110,30 @@ public byte[] WriteAndRead(byte[] buffer) return Read(); } - public void FlushBuffers() + public virtual void WriteAndValidate(byte[] buffer) + { + throw new NotSupportedException(); + } + + public virtual void WriteWhileBusy(byte[] buffer) + { + throw new NotSupportedException(); + } + + public virtual void FlushBuffers() { ThrowIfDeviceNotReady(); _ = SiUsbXpressDriver.SI_FlushBuffers(DeviceHandle!.DangerousGetHandle(), 0x01, 0x01); } - private void ThrowIfDeviceNotReady() + protected void ThrowIfDeviceNotReady() { if (!IsOpen) throw new SiUsbXpressException("Device not ready."); } - public string GetProductString(ProductString productString) + public virtual string GetProductString(ProductString productString) { byte[] data = new byte[SiUsbXpressDriver.SI_MAX_DEVICE_STRLEN]; return SiUsbXpressDriver.SI_GetProductString(_deviceNumber, data, (byte)productString).IsError() diff --git a/src/FanControl.CorsairLink.sln b/src/FanControl.CorsairLink.sln index 560cd1f..c1b1bef 100644 --- a/src/FanControl.CorsairLink.sln +++ b/src/FanControl.CorsairLink.sln @@ -33,6 +33,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorsairLink.SiUsbXpress", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorsairLink.SiUsbXpress.Driver", "CorsairLink.SiUsbXpress.Driver\CorsairLink.SiUsbXpress.Driver.csproj", "{F779E75A-2B08-4800-9892-2907647AE117}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorsairLink.Devices.HydroAsetek", "devices\hydro_asetek\CorsairLink.Devices.HydroAsetek.csproj", "{02778AD1-7CA2-4F8E-8513-56FE49EF8D79}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorsairLink.Asetek", "CorsairLink.Asetek\CorsairLink.Asetek.csproj", "{0433FDB8-F465-4C62-9B96-803723B7D9BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +99,14 @@ Global {F779E75A-2B08-4800-9892-2907647AE117}.Debug|Any CPU.Build.0 = Debug|Any CPU {F779E75A-2B08-4800-9892-2907647AE117}.Release|Any CPU.ActiveCfg = Release|Any CPU {F779E75A-2B08-4800-9892-2907647AE117}.Release|Any CPU.Build.0 = Release|Any CPU + {02778AD1-7CA2-4F8E-8513-56FE49EF8D79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02778AD1-7CA2-4F8E-8513-56FE49EF8D79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02778AD1-7CA2-4F8E-8513-56FE49EF8D79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02778AD1-7CA2-4F8E-8513-56FE49EF8D79}.Release|Any CPU.Build.0 = Release|Any CPU + {0433FDB8-F465-4C62-9B96-803723B7D9BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0433FDB8-F465-4C62-9B96-803723B7D9BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0433FDB8-F465-4C62-9B96-803723B7D9BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0433FDB8-F465-4C62-9B96-803723B7D9BB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -106,6 +118,7 @@ Global {26EE84F3-556F-4391-8925-58E28E9E7127} = {C44527E6-C6F5-4957-B8A9-E8388416E54E} {7EF049F2-EAA7-4DF4-B724-0B00D062984A} = {C44527E6-C6F5-4957-B8A9-E8388416E54E} {6202EE32-877F-40C0-B71F-02761B20DF02} = {C44527E6-C6F5-4957-B8A9-E8388416E54E} + {02778AD1-7CA2-4F8E-8513-56FE49EF8D79} = {C44527E6-C6F5-4957-B8A9-E8388416E54E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5ADA02C2-70F3-4CCD-93EA-684AA98C6A9E} diff --git a/src/FanControl.CorsairLink/FanControl.CorsairLink.csproj b/src/FanControl.CorsairLink/FanControl.CorsairLink.csproj index f4a9ad4..469f2f0 100644 --- a/src/FanControl.CorsairLink/FanControl.CorsairLink.csproj +++ b/src/FanControl.CorsairLink/FanControl.CorsairLink.csproj @@ -22,6 +22,7 @@ + @@ -38,6 +39,7 @@ + @@ -48,6 +50,7 @@ + diff --git a/src/FanControl.CorsairLink/HardwareIds.cs b/src/FanControl.CorsairLink/HardwareIds.cs index e525cd1..533533f 100644 --- a/src/FanControl.CorsairLink/HardwareIds.cs +++ b/src/FanControl.CorsairLink/HardwareIds.cs @@ -4,8 +4,18 @@ public static class HardwareIds { public static readonly int CorsairVendorId = 0x1b1c; + public static readonly int CorsairH80iGTProductId = 0x0c02; + public static readonly int CorsairH100iGTXProductId = 0x0c03; public static readonly int CorsairCoolitFamilyProductId = 0x0c04; + public static readonly int CorsairH110iGTXProductId = 0x0c07; + public static readonly int CorsairH80iGTv2ProductId = 0x0c08; + public static readonly int CorsairH100iGTv2ProductId = 0x0c09; + public static readonly int CorsairH110iGTv2ProductId = 0x0c0a; public static readonly int CorsairCommanderProProductId = 0x0c10; + public static readonly int CorsairHydroH150iProProductId = 0x0c12; + public static readonly int CorsairHydroH115iProProductId = 0x0c13; + public static readonly int CorsairHydroH100iProProductId = 0x0c15; + public static readonly int CorsairHydroH80iProProductId = 0x0c16; public static readonly int CorsairHydroH115iPlatinumProductId = 0x0c17; public static readonly int CorsairHydroH100iPlatinumProductId = 0x0c18; public static readonly int CorsairHydroH100iPlatinumSEProductId = 0x0c19; @@ -122,6 +132,32 @@ public static class DeviceDriverGroups CorsairPsuAX1300iProductId, CorsairPsuAX1600iProductId, }; + + public static readonly IReadOnlyCollection HydroAsetekPro2Fan = new List + { + CorsairHydroH115iProProductId, + CorsairHydroH100iProProductId, + CorsairHydroH80iProProductId, + }; + + public static readonly IReadOnlyCollection HydroAsetekPro3Fan = new List + { + CorsairHydroH150iProProductId, + }; + + public static readonly IReadOnlyCollection HydroAsetekVersion1 = new List + { + CorsairH80iGTProductId, + CorsairH100iGTXProductId, + CorsairH110iGTXProductId, + }; + + public static readonly IReadOnlyCollection HydroAsetekVersion2 = new List + { + CorsairH80iGTv2ProductId, + CorsairH100iGTv2ProductId, + CorsairH110iGTv2ProductId, + }; } public static IReadOnlyCollection GetSupportedProductIds() => @@ -134,5 +170,9 @@ public static IReadOnlyCollection GetSupportedProductIds() => .Concat(DeviceDriverGroups.HidPowerSupplyUnits) .Concat(DeviceDriverGroups.FlexDongleUsbPowerSupplyUnits) .Concat(DeviceDriverGroups.FlexModernUsbPowerSupplyUnits) + .Concat(DeviceDriverGroups.HydroAsetekPro2Fan) + .Concat(DeviceDriverGroups.HydroAsetekPro3Fan) + .Concat(DeviceDriverGroups.HydroAsetekVersion1) + .Concat(DeviceDriverGroups.HydroAsetekVersion2) .ToList(); } diff --git a/src/FanControl.CorsairLink/SiUsbXpressDeviceManager.cs b/src/FanControl.CorsairLink/SiUsbXpressDeviceManager.cs index 47c8bfe..36f45a4 100644 --- a/src/FanControl.CorsairLink/SiUsbXpressDeviceManager.cs +++ b/src/FanControl.CorsairLink/SiUsbXpressDeviceManager.cs @@ -1,4 +1,6 @@ -using CorsairLink.Devices.FlexUsbPsu; +using CorsairLink.Asetek; +using CorsairLink.Devices; +using CorsairLink.Devices.FlexUsbPsu; using CorsairLink.FlexUsb; using CorsairLink.SiUsbXpress; using CorsairLink.SiUsbXpress.Driver; @@ -25,10 +27,22 @@ public static IReadOnlyCollection GetSupportedDevices(IDeviceGuardManag var collection = new List(); collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.FlexDongleUsbPowerSupplyUnits) - .Select(x => new FlexUsbPsuDevice(new FlexDongleUsbPsuProtocol(new SiUsbXpressDevice(x)), deviceGuardManager, logger))); + .Select(x => new FlexUsbPsuDevice(new FlexDongleUsbPsuProtocol(new FlexSiUsbXpressDevice(x)), deviceGuardManager, logger))); collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.FlexModernUsbPowerSupplyUnits) - .Select(x => new FlexUsbPsuDevice(new ModernPsuProtocol(new SiUsbXpressDevice(x)), deviceGuardManager, logger))); + .Select(x => new FlexUsbPsuDevice(new ModernPsuProtocol(new FlexSiUsbXpressDevice(x)), deviceGuardManager, logger))); + + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.HydroAsetekPro2Fan) + .Select(x => new HydroAsetekProDevice(new AsetekCoolerProtocol(new AsetekSiUsbXpressDevice(x)), deviceGuardManager, new HydroAsetekProDeviceOptions { FanChannelCount = 2 }, logger))); + + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.HydroAsetekPro3Fan) + .Select(x => new HydroAsetekProDevice(new AsetekCoolerProtocol(new AsetekSiUsbXpressDevice(x)), deviceGuardManager, new HydroAsetekProDeviceOptions { FanChannelCount = 3 }, logger))); + + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.HydroAsetekVersion1) + .Select(x => new HydroAsetekDevice(new AsetekCoolerProtocol(new AsetekSiUsbXpressDevice(x)), deviceGuardManager, logger))); + + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.HydroAsetekVersion2) + .Select(x => new HydroAsetekDevice(new AsetekCoolerProtocol(new AsetekSiUsbXpressDevice(x)), deviceGuardManager, logger))); return collection; } diff --git a/src/devices/hydro_asetek/CorsairLink.Devices.HydroAsetek.csproj b/src/devices/hydro_asetek/CorsairLink.Devices.HydroAsetek.csproj new file mode 100644 index 0000000..5c8f07a --- /dev/null +++ b/src/devices/hydro_asetek/CorsairLink.Devices.HydroAsetek.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + enable + enable + Latest + + + + + + + + diff --git a/src/devices/hydro_asetek/HydroAsetekDevice.cs b/src/devices/hydro_asetek/HydroAsetekDevice.cs new file mode 100644 index 0000000..0b706e1 --- /dev/null +++ b/src/devices/hydro_asetek/HydroAsetekDevice.cs @@ -0,0 +1,316 @@ +using CorsairLink.Asetek; +using System.Buffers.Binary; +using System.Text; + +namespace CorsairLink.Devices; + +public sealed class HydroAsetekDevice : DeviceBase +{ + internal static class Commands + { + public static readonly byte SetConfiguration = 0x10; + public static readonly byte SetFanCurve = 0x11; + public static readonly byte SetPumpPower = 0x13; + } + + // exact model name is not stored on-device + // device returns "Corsair Hydro Series 7289 USB Device" + internal static readonly IReadOnlyDictionary ModelNames = new Dictionary + { + { 0x0c02, "Hydro H80i GT" }, + { 0x0c03, "Hydro H100i GTX" }, + { 0x0c07, "Hydro H110i GTX" }, + { 0x0c08, "Hydro H80i GT V2" }, + { 0x0c09, "Hydro H100i GT V2" }, + { 0x0c0a, "Hydro H110i GT V2" }, + }; + + private const int DEFAULT_SPEED_CHANNEL_POWER = 50; + private const byte PERCENT_MIN = 0; + private const byte PERCENT_MAX = 100; + private const int PUMP_CHANNEL = -1; + private const int FAN_CHANNEL = 0; + + private readonly IAsetekDeviceProxy _device; + private readonly IDeviceGuardManager _guardManager; + private readonly ChannelTrackingStore _requestedChannelPower = new(); + private readonly Dictionary _speedSensors = new(); + private readonly Dictionary _temperatureSensors = new(); + + private string _firmwareVersion = string.Empty; + + public HydroAsetekDevice(IAsetekDeviceProxy device, IDeviceGuardManager guardManager, ILogger logger) + : base(logger) + { + _device = device; + _guardManager = guardManager; + + var deviceInfo = device.GetDeviceInfo(); + UniqueId = deviceInfo.DevicePath; + Name = $"{ModelNames[deviceInfo.ProductId]} ({Utils.ToMD5HexString(UniqueId)})"; + } + + public override string UniqueId { get; } + + public override string Name { get; } + + public override IReadOnlyCollection SpeedSensors => _speedSensors.Values; + + public override IReadOnlyCollection TemperatureSensors => _temperatureSensors.Values; + + public override bool Connect() + { + LogDebug("Connect"); + + Disconnect(); + + var (opened, exception) = _device.Open(); + if (opened) + { + Initialize(); + return true; + } + + if (exception is not null) + { + LogError(exception); + } + + return false; + } + + public override void Disconnect() + { + LogDebug("Disconnect"); + + _device.Close(); + } + + public override string GetFirmwareVersion() + { + return _firmwareVersion; + } + + private void Initialize() + { + var state = SetFanTypeToPwm(); + _firmwareVersion = $"{state.FirmwareVersionMajor}.{state.FirmwareVersionMinor}.{state.FirmwareVersionRevision1}.{state.FirmwareVersionRevision2}"; + + InitializeSpeedChannelStores(); + Refresh(); + } + + public override void Refresh() + { + var state = WriteRequestedSpeeds(); + _speedSensors[PUMP_CHANNEL].Rpm = state.PumpRpm; + _speedSensors[FAN_CHANNEL].Rpm = state.FanRpm; + _temperatureSensors[PUMP_CHANNEL].TemperatureCelsius = state.LiquidTempCelsius; + + if (CanLogDebug) + { + LogDebug(GetStateStringRepresentation()); + } + } + + public override void SetChannelPower(int channel, int percent) + { + LogDebug($"SetChannelPower {channel} {percent}%"); + + _requestedChannelPower[channel] = (byte)Utils.Clamp(percent, PERCENT_MIN, PERCENT_MAX); + } + + private void InitializeSpeedChannelStores() + { + LogDebug("InitializeSpeedChannelStores"); + + _requestedChannelPower.Clear(); + SetChannelPower(PUMP_CHANNEL, DEFAULT_SPEED_CHANNEL_POWER); + _speedSensors[PUMP_CHANNEL] = new SpeedSensor("Pump", PUMP_CHANNEL, default, supportsControl: true); + SetChannelPower(FAN_CHANNEL, DEFAULT_SPEED_CHANNEL_POWER); + _speedSensors[FAN_CHANNEL] = new SpeedSensor("Fan", FAN_CHANNEL, default, supportsControl: true); + _temperatureSensors[PUMP_CHANNEL] = new TemperatureSensor("Liquid Temp", PUMP_CHANNEL, default); + } + + private State SetFanPower(byte percent) + { + LogDebug($"SetFanPower {percent}%"); + + var requestData = new byte[13]; + requestData[1] = 0x00; // 0C (min temp) + requestData[2] = 0x64; // 100C (max temp) + requestData[7] = percent; + requestData[8] = percent; + var response = WriteAndRead(CreateRequest(Commands.SetFanCurve, requestData)); + response.ThrowIfError(); + return response.GetState(); + } + + private State SetPumpPower(byte percent) + { + LogDebug($"SetPumpPower {percent}%"); + + var requestData = new byte[1] { percent }; + var response = WriteAndRead(CreateRequest(Commands.SetPumpPower, requestData)); + response.ThrowIfError(); + return response.GetState(); + } + + private State SetFanTypeToPwm() + { + LogDebug("SetFanTypeToPwm"); + + var requestData = new byte[18]; + requestData[17] = 0x01; // PWM + var response = WriteAndRead(CreateRequest(Commands.SetConfiguration, requestData)); + response.ThrowIfError(); + return response.GetState(); + } + + private State WriteRequestedSpeeds() + { + LogDebug("WriteRequestedSpeeds"); + + if (_requestedChannelPower.ApplyChanges()) + { + _ = SetPumpPower(_requestedChannelPower[PUMP_CHANNEL]); + } + + // have to write to read anyway + return SetFanPower(_requestedChannelPower[FAN_CHANNEL]); + } + + private string GetStateStringRepresentation() + { + var sb = new StringBuilder().AppendLine("STATE"); + + foreach (var channel in _requestedChannelPower.Channels) + { + sb.AppendLine($"Requested power for channel {channel}: {_requestedChannelPower[channel]} %"); + } + + foreach (var sensor in SpeedSensors) + { + sb.AppendLine(sensor.ToString()); + } + + foreach (var sensor in TemperatureSensors) + { + sb.AppendLine(sensor.ToString()); + } + + return sb.ToString(); + } + + private DeviceResponse WriteAndRead(byte[] buffer) + { + DeviceResponse response; + + if (CanLogDebug) + { + LogDebug($"WRITE: {buffer.ToHexString()}"); + } + + using (_guardManager.AwaitExclusiveAccess()) + { + var data = _device.WriteAndRead(buffer); + + if (CanLogDebug) + { + LogDebug($"READ: {data.ToHexString()}"); + } + + response = new DeviceResponse(buffer, data); + } + + return response; + } + + private byte[] CreateRequest(byte command, ReadOnlySpan data = default) + { + var buffer = new byte[data.Length + 1]; + buffer[0] = command; + data.CopyTo(buffer.AsSpan(1)); + return buffer; + } + + internal sealed class DeviceResponse + { + public DeviceResponse(byte[] request, byte[] response) + { + Request = request; + Response = response; + IsError = !IsValid(); + } + + public byte[] Request { get; } + private byte[] Response { get; } + public bool IsError { get; } + + public State GetState() + { + // [0..1] fan rpm + // [8..9] pump rpm + // [10] liquid temp whole part + // [14] liquid temp fractional part + // [23..26] firmware version + + return new State + { + FirmwareVersionMajor = Response[23], + FirmwareVersionMinor = Response[24], + FirmwareVersionRevision1 = Response[25], + FirmwareVersionRevision2 = Response[26], + FanRpm = BinaryPrimitives.ReadUInt16BigEndian(Response.AsSpan(0, 2)), + PumpRpm = BinaryPrimitives.ReadUInt16BigEndian(Response.AsSpan(8, 2)), + LiquidTempCelsius = Response[10] + Response[14] * 0.1f, + }; + } + + public void ThrowIfError() + { + if (IsError) + { + throw CreateException("Response was invalid.", Request, Response); + } + } + + public void Throw(string message) + { + throw CreateException(message, Request, Response); + } + + private static CorsairLinkDeviceException CreateException(string message, ReadOnlySpan request, ReadOnlySpan response) + { + var exception = new CorsairLinkDeviceException(message); + exception.Data[nameof(request)] = request.ToHexString(); + exception.Data[nameof(response)] = response.ToHexString(); + return exception; + } + + public bool IsValid() + { + return Response[11] == Request[0]; + } + } + + internal sealed class State + { + public int FirmwareVersionMajor { get; set; } + public int FirmwareVersionMinor { get; set; } + public int FirmwareVersionRevision1 { get; set; } + public int FirmwareVersionRevision2 { get; set; } + public int FanRpm { get; set; } + public int PumpRpm { get; set; } + public float LiquidTempCelsius { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("fanRpm={0}, ", FanRpm); + sb.AppendFormat("pumpRpm={0}, ", PumpRpm); + sb.AppendFormat("liquidTempCelsius={0}", LiquidTempCelsius); + return sb.ToString(); + } + } +} diff --git a/src/devices/hydro_asetek/HydroAsetekProDevice.cs b/src/devices/hydro_asetek/HydroAsetekProDevice.cs new file mode 100644 index 0000000..b6dce5b --- /dev/null +++ b/src/devices/hydro_asetek/HydroAsetekProDevice.cs @@ -0,0 +1,357 @@ +using CorsairLink.Asetek; +using System.Buffers.Binary; +using System.Text; + +namespace CorsairLink.Devices; + +public sealed class HydroAsetekProDevice : DeviceBase +{ + internal static class Commands + { + public static readonly byte SetPumpPower = 0x30; + public static readonly byte GetPumpSpeed = 0x31; + public static readonly byte GetFanSpeed = 0x41; + public static readonly byte SetFanPower = 0x42; + public static readonly byte SetFanSafetyProfile = 0x4A; + public static readonly byte GetLiquidTemperature = 0xA9; + public static readonly byte GetFirmwareVersion = 0xAA; + } + + private const int DEFAULT_SPEED_CHANNEL_POWER = 50; + private const byte PERCENT_MIN = 0; + private const byte PERCENT_MAX = 100; + private const int PUMP_CHANNEL = -1; + + private readonly IAsetekDeviceProxy _device; + private readonly AsetekDeviceInfo _deviceInfo; + private readonly IDeviceGuardManager _guardManager; + private readonly uint _fanCount; + private readonly ChannelTrackingStore _requestedChannelPower = new(); + private readonly Dictionary _speedSensors = new(); + private readonly Dictionary _temperatureSensors = new(); + + public HydroAsetekProDevice(IAsetekDeviceProxy device, IDeviceGuardManager guardManager, HydroAsetekProDeviceOptions options, ILogger logger) + : base(logger) + { + _device = device; + _deviceInfo = device.GetDeviceInfo(); + _guardManager = guardManager; + _fanCount = options.FanChannelCount; + + UniqueId = _deviceInfo.DevicePath; + Name = $"{_deviceInfo.ProductName} ({Utils.ToMD5HexString(UniqueId)})"; + } + + public override string UniqueId { get; } + + public override string Name { get; } + + public override IReadOnlyCollection SpeedSensors => _speedSensors.Values; + + public override IReadOnlyCollection TemperatureSensors => _temperatureSensors.Values; + + public override bool Connect() + { + LogDebug("Connect"); + + Disconnect(); + + var (opened, exception) = _device.Open(); + if (opened) + { + Initialize(); + return true; + } + + if (exception is not null) + { + LogError(exception); + } + + return false; + } + + public override void Disconnect() + { + LogDebug("Disconnect"); + + _device.Close(); + } + + public override string GetFirmwareVersion() + { + var response = WriteAndRead(CreateRequest(Commands.GetFirmwareVersion)); + response.ThrowIfError(); + var data = response.GetData(); + return $"{data[0]}.{data[1]}.{data[2]}.{data[3]}"; + } + + private void Initialize() + { + InitializeSpeedChannelStores(); + Refresh(); + } + + public override void Refresh() + { + WriteRequestedSpeeds(); + RefreshTemperatures(); + RefreshSpeeds(); + + if (CanLogDebug) + { + LogDebug(GetStateStringRepresentation()); + } + } + + public override void SetChannelPower(int channel, int percent) + { + LogDebug($"SetChannelPower {channel} {percent}%"); + _requestedChannelPower[channel] = (byte)Utils.Clamp(percent, PERCENT_MIN, PERCENT_MAX); + } + + private void InitializeSpeedChannelStores() + { + LogDebug("InitializeSpeedChannelStores"); + + _requestedChannelPower.Clear(); + SetChannelPower(PUMP_CHANNEL, DEFAULT_SPEED_CHANNEL_POWER); + _speedSensors[PUMP_CHANNEL] = new SpeedSensor("Pump", PUMP_CHANNEL, default, supportsControl: true); + _temperatureSensors[PUMP_CHANNEL] = new TemperatureSensor("Liquid Temp", PUMP_CHANNEL, default); + + for (var i = 0; i < _fanCount; i++) + { + SetChannelPower(i, DEFAULT_SPEED_CHANNEL_POWER); + _speedSensors[i] = new SpeedSensor($"Fan #{i + 1}", i, default, supportsControl: true); + } + } + + private void RefreshTemperatures() + { + _temperatureSensors[PUMP_CHANNEL].TemperatureCelsius = GetLiquidTemperature(); + } + + private void RefreshSpeeds() + { + _speedSensors[PUMP_CHANNEL].Rpm = GetPumpRpm(); + + for (var i = 0; i < _fanCount; i++) + { + _speedSensors[i].Rpm = GetFanRpm(i); + } + } + + private int GetFanRpm(int channel) + { + LogDebug($"GetFanRpm {channel}"); + + var requestData = new byte[1] { (byte)channel }; + var response = WriteAndRead(CreateRequest(Commands.GetFanSpeed, requestData)); + response.ThrowIfError(); + return BinaryPrimitives.ReadUInt16BigEndian(response.GetData().Slice(1)); + } + + private int GetPumpRpm() + { + LogDebug("GetPumpRpm"); + + var response = WriteAndRead(CreateRequest(Commands.GetPumpSpeed)); + response.ThrowIfError(); + return BinaryPrimitives.ReadUInt16BigEndian(response.GetData()); + } + + private float GetLiquidTemperature() + { + LogDebug("GetLiquidTemperature"); + + var response = WriteAndRead(CreateRequest(Commands.GetLiquidTemperature)); + response.ThrowIfError(); + + var data = response.GetData(); + var wholePart = (float)(sbyte)data[0]; + var fracData = data[1]; + + if (fracData > 9) + { + response.Throw($"{nameof(GetLiquidTemperature)} encountered a data error: fractional data overflow"); + } + + var fracPart = fracData * ((double)wholePart < 0.0 ? -0.1f : 0.1f); + return wholePart + fracPart; + } + + private void SetFanPower(int channel, byte percent) + { + LogDebug($"SetFanPower {channel} {percent}%"); + + var requestData = new byte[2] { (byte)channel, percent }; + var response = WriteAndRead(CreateRequest(Commands.SetFanPower, requestData)); + response.ThrowIfError(); + } + + private void SetPumpPower(byte percent) + { + LogDebug($"SetPumpPower {percent}%"); + + var requestData = new byte[1] { percent }; + var response = WriteAndRead(CreateRequest(Commands.SetPumpPower, requestData)); + response.ThrowIfError(); + } + + private void WriteRequestedSpeeds() + { + LogDebug("WriteRequestedSpeeds"); + + if (_requestedChannelPower.ApplyChanges()) + { + SetPumpPower(_requestedChannelPower[PUMP_CHANNEL]); + + for (var i = 0; i < _fanCount; i++) + { + SetFanPower(i, _requestedChannelPower[i]); + } + } + } + + private string GetStateStringRepresentation() + { + var sb = new StringBuilder().AppendLine("STATE"); + + foreach (var channel in _requestedChannelPower.Channels) + { + sb.AppendLine($"Requested power for channel {channel}: {_requestedChannelPower[channel]} %"); + } + + foreach (var sensor in SpeedSensors) + { + sb.AppendLine(sensor.ToString()); + } + + foreach (var sensor in TemperatureSensors) + { + sb.AppendLine(sensor.ToString()); + } + + return sb.ToString(); + } + + private DeviceResponse WriteAndRead(byte[] buffer) + { + DeviceResponse response; + + if (CanLogDebug) + { + LogDebug($"WRITE: {buffer.ToHexString()}"); + } + + using (_guardManager.AwaitExclusiveAccess()) + { + var data = _device.WriteAndRead(buffer); + + if (CanLogDebug) + { + LogDebug($"READ: {data.ToHexString()}"); + } + + response = new DeviceResponse(buffer, data); + } + + return response; + } + + private byte[] CreateRequest(byte command, ReadOnlySpan data = default) + { + var buffer = new byte[data.Length + 1]; + buffer[0] = command; + data.CopyTo(buffer.AsSpan(1)); + return buffer; + } + + internal sealed class DeviceResponse + { + public DeviceResponse(byte[] request, byte[] response) + { + Request = request; + Response = response; + IsError = !IsValid(); + } + + public byte[] Request { get; } + private byte[] Response { get; } + public bool IsError { get; } + + public ReadOnlySpan GetData() => Response.AsSpan(3); + + public void ThrowIfError() + { + if (IsError) + { + throw CreateException("Response was invalid.", Request, Response); + } + } + + public void Throw(string message) + { + throw CreateException(message, Request, Response); + } + + private static CorsairLinkDeviceException CreateException(string message, ReadOnlySpan request, ReadOnlySpan response) + { + var exception = new CorsairLinkDeviceException(message); + exception.Data[nameof(request)] = request.ToHexString(); + exception.Data[nameof(response)] = response.ToHexString(); + return exception; + } + + public bool IsValid() + { + return Response[0] == Request[0]; + } + } + + internal static ushort GenerateChecksum(ReadOnlySpan data) + { + ushort result = 0; + for (int i = 0; i < data.Length; i++) + { + result = (ushort)((result >> 8) ^ CRC16_CCITT_TABLE[(result ^ data[i]) & 0xFF]); + } + return result; + } + + private static readonly ushort[] CRC16_CCITT_TABLE = new ushort[256] + { + 0, 4489, 8978, 12955, 17956, 22445, 25910, 29887, + 35912, 40385, 44890, 48851, 51820, 56293, 59774, 63735, + 4225, 264, 13203, 8730, 22181, 18220, 30135, 25662, + 40137, 36160, 49115, 44626, 56045, 52068, 63999, 59510, + 8450, 12427, 528, 5017, 26406, 30383, 17460, 21949, + 44362, 48323, 36440, 40913, 60270, 64231, 51324, 55797, + 12675, 8202, 4753, 792, 30631, 26158, 21685, 17724, + 48587, 44098, 40665, 36688, 64495, 60006, 55549, 51572, + 16900, 21389, 24854, 28831, 1056, 5545, 10034, 14011, + 52812, 57285, 60766, 64727, 34920, 39393, 43898, 47859, + 21125, 17164, 29079, 24606, 5281, 1320, 14259, 9786, + 57037, 53060, 64991, 60502, 39145, 35168, 48123, 43634, + 25350, 29327, 16404, 20893, 9506, 13483, 1584, 6073, + 61262, 65223, 52316, 56789, 43370, 47331, 35448, 39921, + 29575, 25102, 20629, 16668, 13731, 9258, 5809, 1848, + 65487, 60998, 56541, 52564, 47595, 43106, 39673, 35696, + 33800, 38273, 42778, 46739, 49708, 54181, 57662, 61623, + 2112, 6601, 11090, 15067, 20068, 24557, 28022, 31999, + 38025, 34048, 47003, 42514, 53933, 49956, 61887, 57398, + 6337, 2376, 15315, 10842, 24293, 20332, 32247, 27774, + 42250, 46211, 34328, 38801, 58158, 62119, 49212, 53685, + 10562, 14539, 2640, 7129, 28518, 32495, 19572, 24061, + 46475, 41986, 38553, 34576, 62383, 57894, 53437, 49460, + 14787, 10314, 6865, 2904, 32743, 28270, 23797, 19836, + 50700, 55173, 58654, 62615, 32808, 37281, 41786, 45747, + 19012, 23501, 26966, 30943, 3168, 7657, 12146, 16123, + 54925, 50948, 62879, 58390, 37033, 33056, 46011, 41522, + 23237, 19276, 31191, 26718, 7393, 3432, 16371, 11898, + 59150, 63111, 50204, 54677, 41258, 45219, 33336, 37809, + 27462, 31439, 18516, 23005, 11618, 15595, 3696, 8185, + 63375, 58886, 54429, 50452, 45483, 40994, 37561, 33584, + 31687, 27214, 22741, 18780, 15843, 11370, 7921, 3960, + }; +} diff --git a/src/devices/hydro_asetek/HydroAsetekProDeviceOptions.cs b/src/devices/hydro_asetek/HydroAsetekProDeviceOptions.cs new file mode 100644 index 0000000..f2f021e --- /dev/null +++ b/src/devices/hydro_asetek/HydroAsetekProDeviceOptions.cs @@ -0,0 +1,6 @@ +namespace CorsairLink.Devices; + +public class HydroAsetekProDeviceOptions +{ + public uint FanChannelCount { get; set; } +} \ No newline at end of file