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