From 0008000d2a72d254167605b0eca21ca74adb635e Mon Sep 17 00:00:00 2001 From: Evan Mulawski <1044791+EvanMulawski@users.noreply.github.com> Date: Thu, 15 Jun 2023 00:15:36 -0400 Subject: [PATCH] [psu] initial psu support --- README.md | 99 +++-- src/CorsairLink.Abstractions/ILogger.cs | 3 + .../SupportedDeviceCollection.cs | 24 -- src/CorsairLink.Abstractions/Utils.cs | 25 ++ src/CorsairLink/DeviceManager.cs | 37 +- src/CorsairLink/HardwareIds.cs | 45 ++ src/CorsairLink/PowerSupplyUnitDevice.cs | 386 ++++++++++++++++++ .../CorsairLinkPlugin.cs | 23 +- .../CorsairLinkPluginLogger.cs | 16 + 9 files changed, 559 insertions(+), 99 deletions(-) delete mode 100644 src/CorsairLink.Abstractions/SupportedDeviceCollection.cs create mode 100644 src/CorsairLink/PowerSupplyUnitDevice.cs diff --git a/README.md b/README.md index 64bc275..e77e772 100644 --- a/README.md +++ b/README.md @@ -7,47 +7,60 @@ The unofficial CorsairLink plugin for [Fan Control](https://github.com/Rem0o/Fan ## Device Support -| Device | PID | Status | Read Fan/Pump RPM | Set Fan/Pump Power | Read Temp Sensor | -| ------------------------------- | ---------- | --------------------------- | ----------------- | ------------------ | ---------------- | -| Commander PRO | `0c10` | Full Support 1 | ✅ | ✅ | ✅ | -| Commander PRO (Obsidian 1000D) | `1d00` | Full Support 1 | ✅ | ✅ | ✅ | -| Commander CORE XT | `0c2a` | Full Support 1,2 | ✅ | ✅ | ✅ | -| Commander CORE (ELITE CAPELLIX) | `0c1c` | Full Support 1,2 | ✅ | ✅ | ✅ | -| Commander CORE | `0c32` | Full Support 1,2 | ✅ | ✅ | ✅ | -| Commander Mini | `0c04(3d)` | Full Support 1 | ✅ | ✅ | ✅ | -| Hydro H60i Elite | `0c34` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H60i Pro XT | `0c29` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H60i Pro XT | `0c30` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Elite | `0c35` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Platinum | `0c18` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Platinum SE | `0c19` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Pro XT | `0c20` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H100i Pro XT | `0c2d` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Elite | `0c36` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Platinum | `0c17` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Pro XT | `0c21` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H115i Pro XT | `0c2e` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H150i Elite | `0c37` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H150i Pro XT | `0c22` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H150i Pro XT | `0c2f` | Full Support 1 | ✅ | ✅ 4 | ✅ 5 | -| Hydro H80i | `0c04(3b)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H100i | `0c04(3c)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H100i GT | `0c04(40)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H110i | `0c04(42)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Hydro H110i GT | `0c04(41)` | Full Support 1 | ✅ | ✅ | ✅ 5 | -| Cooling Node | `0c04(38)` | Support Upon Request | | | | -| Hydro H80 | `0c04(37)` | Support Upon Request | | | | -| Hydro H100 | `0c04(3a)` | Support Upon Request | | | | -| Hydro H80i GT | `0c02` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H80i GT V2 | `0c08` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H80i Pro | `0c16` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H100i GT V2 | `0c09` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H100i GTX | `0c03` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H100i Pro | `0c15` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H110i GT V2 | `0c0a` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H110i GTX | `0c07` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H115i Pro | `0c13` | No Support 6 | ❌ | ❌ | ❌ | -| Hydro H150i Pro | `0c12` | No Support 6 | ❌ | ❌ | ❌ | +| 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` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| HX650i | PSU | `1c04` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| HX750i | PSU | `1c05` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| HX850i | PSU | `1c06` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| HX1000i | PSU | `1c07` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| HX1200i | PSU | `1c08` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| HX1000i (2021) | PSU | `1c1e` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| HX1500i (2021) | PSU | `1c1f` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| RM550i | PSU | `1c09` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| RM650i | PSU | `1c0a` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| RM750i | PSU | `1c0b` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| RM850i | PSU | `1c0c` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| RM1000i | PSU | `1c0d` | 1.3.x Pre-release 3,8 | ✅ | ✅ 7 | ✅ | +| Cooling Node | Controller | `0c04(38)` | Support Upon Request | | | | +| Hydro H80 | AIO | `0c04(37)` | Support Upon Request | | | | +| Hydro H100 | AIO | `0c04(3a)` | Support Upon Request | | | | +| Hydro H80i GT | AIO | `0c02` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H80i GT V2 | AIO | `0c08` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H80i Pro | AIO | `0c16` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H100i GT V2 | AIO | `0c09` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H100i GTX | AIO | `0c03` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H100i Pro | AIO | `0c15` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H110i GT V2 | AIO | `0c0a` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H110i GTX | AIO | `0c07` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H115i Pro | AIO | `0c13` | No Support 6 | ❌ | ❌ | ❌ | +| Hydro H150i Pro | AIO | `0c12` | No Support 6 | ❌ | ❌ | ❌ | 1. Software mode only. Device lighting will be software-based. @@ -67,6 +80,10 @@ The unofficial CorsairLink plugin for [Fan Control](https://github.com/Rem0o/Fan 6. The USB device class is not HID and support cannot be added. +7. The minimum fan duty is 30%. When the fan power is set to 0%, control of the fan will be returned to the PSU allowing zero-RPM operation. When the fan power is set to 1% or higher, control of the fan will be returned to Fan Control. + +8. The LibreHardwareMonitor "PSU (Corsair)" sensor source must be disabled in Fan Control's Sensor Settings. + ## Installation ⚠ This plugin will not function correctly if Corsair iCUE (specifically, the "Corsair Service" service) is running. This service should be stopped before running Fan Control. Running other programs that attempt to communicate with these devices while Fan Control is running is not currently a supported scenario. diff --git a/src/CorsairLink.Abstractions/ILogger.cs b/src/CorsairLink.Abstractions/ILogger.cs index 2c98d0c..28f175a 100644 --- a/src/CorsairLink.Abstractions/ILogger.cs +++ b/src/CorsairLink.Abstractions/ILogger.cs @@ -3,4 +3,7 @@ public interface ILogger { void Log(string message); + void Normal(string deviceName, string message); + void Error(string deviceName, string message); + void Debug(string deviceName, string message); } \ No newline at end of file diff --git a/src/CorsairLink.Abstractions/SupportedDeviceCollection.cs b/src/CorsairLink.Abstractions/SupportedDeviceCollection.cs deleted file mode 100644 index 647a36e..0000000 --- a/src/CorsairLink.Abstractions/SupportedDeviceCollection.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections; - -namespace CorsairLink; - -public sealed class SupportedDeviceCollection : IEnumerable -{ - public List CommanderProDevices { get; } = new List(1); - public List CommanderCoreDevices { get; } = new List(1); - public List HydroDevices { get; } = new List(1); - public List CoolitDevices { get; } = new List(1); - - private IEnumerator GetEnumeratorImpl() - { - return CommanderProDevices - .Union(CommanderCoreDevices) - .Union(HydroDevices) - .Union(CoolitDevices) - .GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumeratorImpl(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumeratorImpl(); -} diff --git a/src/CorsairLink.Abstractions/Utils.cs b/src/CorsairLink.Abstractions/Utils.cs index d44ce1c..56f07d5 100644 --- a/src/CorsairLink.Abstractions/Utils.cs +++ b/src/CorsairLink.Abstractions/Utils.cs @@ -35,4 +35,29 @@ public static string ToHexString(this ReadOnlySpan bytes) } public static string ToHexString(this byte[] bytes) => ToHexString(bytes.AsSpan()); + + public static float FromLinear11(ReadOnlySpan bytes) + { + int value = bytes[1] << 8 | bytes[0]; + + int mantissa = value & 0x7FF; + if (mantissa > 1023) + mantissa -= 2048; + + int exponent = value >> 11; + if (exponent > 15) + exponent -= 32; + + return mantissa * (float)Math.Pow(2, exponent); + } + + public static bool GetEnvironmentFlag(string flagName) + { + var variableValue = Environment.GetEnvironmentVariable(flagName); + if (string.IsNullOrEmpty(variableValue)) + { + variableValue = Environment.GetEnvironmentVariable(flagName, EnvironmentVariableTarget.Machine); + } + return !string.IsNullOrEmpty(variableValue) && (variableValue.ToLower() == "true" || variableValue == "1"); + } } diff --git a/src/CorsairLink/DeviceManager.cs b/src/CorsairLink/DeviceManager.cs index e521815..741704b 100644 --- a/src/CorsairLink/DeviceManager.cs +++ b/src/CorsairLink/DeviceManager.cs @@ -5,7 +5,7 @@ namespace CorsairLink; public static class DeviceManager { - public static SupportedDeviceCollection GetSupportedDevices(IDeviceGuardManager deviceGuardManager, ILogger? logger) + public static IReadOnlyCollection GetSupportedDevices(IDeviceGuardManager deviceGuardManager, ILogger? logger) { var corsairDevices = DeviceList.Local .GetHidDevices(vendorID: HardwareIds.CorsairVendorId) @@ -20,31 +20,28 @@ public static SupportedDeviceCollection GetSupportedDevices(IDeviceGuardManager var supportedDevicesByProductId = supportedDevices .ToLookup(x => x.ProductID); - var collection = new SupportedDeviceCollection(); + var collection = new List(); - collection.CommanderProDevices - .AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CommanderPro) - .Select(x => new CommanderProDevice(new HidSharpDeviceProxy(x), deviceGuardManager, logger))); + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CommanderPro) + .Select(x => new CommanderProDevice(new HidSharpDeviceProxy(x), deviceGuardManager, logger))); - collection.CommanderCoreDevices - .AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CommanderCore) - .Select(x => new CommanderCoreDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new CommanderCoreDeviceOptions { IsFirstChannelExt = false }, logger))); + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CommanderCore) + .Select(x => new CommanderCoreDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new CommanderCoreDeviceOptions { IsFirstChannelExt = false }, logger))); - collection.CommanderCoreDevices - .AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CommanderCoreWithDesignatedPump) - .Select(x => new CommanderCoreDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new CommanderCoreDeviceOptions { IsFirstChannelExt = true }, logger))); + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CommanderCoreWithDesignatedPump) + .Select(x => new CommanderCoreDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new CommanderCoreDeviceOptions { IsFirstChannelExt = true }, logger))); - collection.HydroDevices - .AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.Hydro2Fan) - .Select(x => new HydroDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new HydroDeviceOptions { FanChannelCount = 2 }, logger))); + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.Hydro2Fan) + .Select(x => new HydroDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new HydroDeviceOptions { FanChannelCount = 2 }, logger))); - collection.HydroDevices - .AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.Hydro3Fan) - .Select(x => new HydroDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new HydroDeviceOptions { FanChannelCount = 3 }, logger))); + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.Hydro3Fan) + .Select(x => new HydroDevice(new HidSharpDeviceProxy(x), deviceGuardManager, new HydroDeviceOptions { FanChannelCount = 3 }, logger))); - collection.CoolitDevices - .AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CoolitFamily) - .Select(x => new CoolitDevice(new HidSharpDeviceProxy(x), deviceGuardManager, logger))); + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.CoolitFamily) + .Select(x => new CoolitDevice(new HidSharpDeviceProxy(x), deviceGuardManager, logger))); + + collection.AddRange(supportedDevices.InDeviceDriverGroup(HardwareIds.DeviceDriverGroups.PowerSupplyUnits) + .Select(x => new PowerSupplyUnitDevice(new HidSharpDeviceProxy(x), deviceGuardManager, logger))); return collection; } diff --git a/src/CorsairLink/HardwareIds.cs b/src/CorsairLink/HardwareIds.cs index a6394e6..c17dac8 100644 --- a/src/CorsairLink/HardwareIds.cs +++ b/src/CorsairLink/HardwareIds.cs @@ -24,6 +24,19 @@ public static class HardwareIds public static readonly int CorsairHydroH100iEliteProductId = 0x0c35; public static readonly int CorsairHydroH115iEliteProductId = 0x0c36; public static readonly int CorsairHydroH150iEliteProductId = 0x0c37; + public static readonly int CorsairPsuHX550iProductId = 0x1c03; + public static readonly int CorsairPsuHX650iProductId = 0x1c04; + public static readonly int CorsairPsuHX750iProductId = 0x1c05; + public static readonly int CorsairPsuHX850iProductId = 0x1c06; + public static readonly int CorsairPsuHX1000iProductId = 0x1c07; + public static readonly int CorsairPsuHX1200iProductId = 0x1c08; + public static readonly int CorsairPsuHX1000i2021ProductId = 0x1c1e; + public static readonly int CorsairPsuHX1500i2021ProductId = 0x1c1f; + public static readonly int CorsairPsuRM550iProductId = 0x1c09; + public static readonly int CorsairPsuRM650iProductId = 0x1c0a; + public static readonly int CorsairPsuRM750iProductId = 0x1c0b; + public static readonly int CorsairPsuRM850iProductId = 0x1c0c; + public static readonly int CorsairPsuRM1000iProductId = 0x1c0d; public static readonly int CorsairObsidian1000DCommanderProProductId = 0x1d00; public static readonly IReadOnlyCollection SupportedProductIds = new List @@ -58,6 +71,21 @@ public static class HardwareIds // CoolIT Product Family CorsairCoolitFamilyProductId, + + // HID PSU + CorsairPsuHX550iProductId, + CorsairPsuHX650iProductId, + CorsairPsuHX750iProductId, + CorsairPsuHX850iProductId, + CorsairPsuHX1000iProductId, + CorsairPsuHX1200iProductId, + CorsairPsuHX1000i2021ProductId, + CorsairPsuHX1500i2021ProductId, + CorsairPsuRM550iProductId, + CorsairPsuRM650iProductId, + CorsairPsuRM750iProductId, + CorsairPsuRM850iProductId, + CorsairPsuRM1000iProductId, }; public static class DeviceDriverGroups @@ -106,5 +134,22 @@ public static class DeviceDriverGroups { CorsairCoolitFamilyProductId, }; + + public static readonly IReadOnlyCollection PowerSupplyUnits = new List + { + CorsairPsuHX550iProductId, + CorsairPsuHX650iProductId, + CorsairPsuHX750iProductId, + CorsairPsuHX850iProductId, + CorsairPsuHX1000iProductId, + CorsairPsuHX1200iProductId, + CorsairPsuHX1000i2021ProductId, + CorsairPsuHX1500i2021ProductId, + CorsairPsuRM550iProductId, + CorsairPsuRM650iProductId, + CorsairPsuRM750iProductId, + CorsairPsuRM850iProductId, + CorsairPsuRM1000iProductId, + }; } } diff --git a/src/CorsairLink/PowerSupplyUnitDevice.cs b/src/CorsairLink/PowerSupplyUnitDevice.cs new file mode 100644 index 0000000..e976fc6 --- /dev/null +++ b/src/CorsairLink/PowerSupplyUnitDevice.cs @@ -0,0 +1,386 @@ +using System.Text; + +namespace CorsairLink; + +public sealed class PowerSupplyUnitDevice : IDevice +{ + private static class CommandModes + { + public static readonly byte Read = 0x03; + public static readonly byte Write = 0x02; + } + + private static class FanControlModes + { + public static readonly byte Normal = 0x00; + public static readonly byte Manual = 0x01; + } + + private static class Commands + { + public static readonly byte ReadFirmwareVersion = 0xd4; + public static readonly byte Handshake = 0xfe; + public static readonly byte WriteFanControlMode = 0xf0; + public static readonly byte ReadTemperature1 = 0x8d; + public static readonly byte ReadTemperature2 = 0x8e; + public static readonly byte ReadFanSpeed = 0x90; + public static readonly byte WriteFanPower = 0x3b; + } + + private const int REQUEST_LENGTH = 65; + private const int RESPONSE_LENGTH = 64; + private const int TEMP_CHANNEL_COUNT = 2; + private const int DEFAULT_SPEED_CHANNEL_POWER = 50; + private const int SPEED_CHANNEL = 0; + private const byte PERCENT_MIN = 0x1e; // 30% is the minimum for the manual fan control mode + private const byte PERCENT_MAX = 0x64; + + private readonly IHidDeviceProxy _device; + private readonly IDeviceGuardManager _guardManager; + private readonly ILogger? _logger; + private readonly SpeedChannelPowerTrackingStore _requestedChannelPower = new(); + private readonly SpeedChannelPowerTrackingStore _fanControlModeStore = new(); + private readonly Dictionary _speedSensors = new(); + private readonly Dictionary _temperatureSensors = new(); + + private string _name; + private readonly string _serialNumber; + + public PowerSupplyUnitDevice(IHidDeviceProxy device, IDeviceGuardManager guardManager, ILogger? logger) + { + _device = device; + _guardManager = guardManager; + _logger = logger; + + var deviceInfo = device.GetDeviceInfo(); + _serialNumber = deviceInfo.SerialNumber; + _name = "Corsair PSU"; + + Name = GetName(); + UniqueId = deviceInfo.DevicePath; + } + + private string GetName() => $"{_name} ({_serialNumber})"; + + public string UniqueId { get; } + + public string Name { get; private set; } + + public IReadOnlyCollection SpeedSensors => _speedSensors.Values; + + public IReadOnlyCollection TemperatureSensors => _temperatureSensors.Values; + + private void LogNormal(string message) => _logger?.Normal(Name, message); + private void LogError(string message) => _logger?.Error(Name, message); + private void LogDebug(string message) => _logger?.Debug(Name, message); + + public bool Connect() + { + Disconnect(); + + var (opened, exception) = _device.Open(); + if (opened) + { + Initialize(); + return true; + } + + if (exception is not null) + { + LogError(exception.ToString()); + } + + return false; + } + + public void Disconnect() + { + try + { + SetFanControlMode(FanControlModes.Normal); + } + catch + { + // ignore + } + + _device.Close(); + } + + public string GetFirmwareVersion() + { + var request = CreateRequest(CommandModes.Read, Commands.ReadFirmwareVersion); + var response = WriteAndRead(request); + + if (response.IsError) + { + return "UNKNOWN"; + } + + var data = response.GetData(); + var v1 = (int)data[0]; + var v2 = (int)data[1]; + var v3 = (int)data[2]; + var v4 = (int)data[3]; + + return $"{v1}.{v2}.{v3}.{v4}"; + } + + private void Initialize() + { + UpdateDeviceName(); + InitializeSpeedChannelStores(); + Refresh(); + } + + private void UpdateDeviceName() + { + using (_guardManager.AwaitExclusiveAccess()) + { + var response = PerformHandshake(); + response.ThrowIfError(); + + var modelNameData = response.GetData(); + var lastCharIndex = modelNameData.IndexOf((byte)0); + + _name = Encoding.ASCII.GetString(modelNameData.Slice(0, lastCharIndex).ToArray()); + } + + Name = GetName(); + } + + public void Refresh() + { + WriteRequestedSpeeds(); + RefreshTemperatures(); + RefreshSpeeds(); + } + + public void SetChannelPower(int channel, int percent) + { + _requestedChannelPower[SPEED_CHANNEL] = (byte)Utils.Clamp(percent, PERCENT_MIN, PERCENT_MAX); + + // When the user sets the power to 0%, set the fan control mode to Normal. This allows the device + // to control the fan speed and allows for zero-RPM operation. + // Set the fan control mode to Manual if the user sets the power to 1% or higher. + // Note: From 1-30%, the device will set the fan speed to 30% (the minimum duty). + _fanControlModeStore[SPEED_CHANNEL] = percent == 0 ? FanControlModes.Normal : FanControlModes.Manual; + } + + private void InitializeSpeedChannelStores() + { + _requestedChannelPower.Clear(); + _requestedChannelPower[SPEED_CHANNEL] = DEFAULT_SPEED_CHANNEL_POWER; + _fanControlModeStore[SPEED_CHANNEL] = FanControlModes.Manual; + } + + private void RefreshTemperatures() + { + var sensors = GetTemperatureSensors(); + + foreach (var sensor in sensors) + { + if (!_temperatureSensors.TryGetValue(sensor.Channel, out var existingSensor)) + { + _temperatureSensors[sensor.Channel] = sensor; + continue; + } + + existingSensor.TemperatureCelsius = sensor.TemperatureCelsius; + } + } + + private void RefreshSpeeds() + { + var sensors = GetSpeedSensors(); + + foreach (var sensor in sensors) + { + if (!_speedSensors.TryGetValue(sensor.Channel, out var existingSensor)) + { + _speedSensors[sensor.Channel] = sensor; + continue; + } + + existingSensor.Rpm = sensor.Rpm; + } + } + + private int? GetFanRpm() + { + var request = CreateRequest(CommandModes.Read, Commands.ReadFanSpeed); + var response = WriteAndRead(request); + + if (response.IsError) + { + return default; + } + + return (int)Utils.FromLinear11(response.GetData()); + } + + private void SetFanPower(byte percent) + { + var request = CreateRequest(CommandModes.Write, Commands.WriteFanPower, percent); + _ = WriteAndRead(request); + } + + private void SetFanControlMode(byte mode) + { + var request = CreateRequest(CommandModes.Write, Commands.WriteFanControlMode, mode); + _ = WriteAndRead(request); + } + + private void WriteRequestedSpeeds() + { + if (_requestedChannelPower.Dirty) + { + SetFanPower(_requestedChannelPower[SPEED_CHANNEL]); + _requestedChannelPower.ResetDirty(); + } + + if (_fanControlModeStore.Dirty) + { + var mode = _fanControlModeStore[SPEED_CHANNEL]; + LogDebug($"Changing fan control mode ({mode:X2})"); + SetFanControlMode(mode); + _fanControlModeStore.ResetDirty(); + } + } + + private float? GetTemperatureSensorValue(int channelId) + { + var request = CreateRequest(CommandModes.Read, channelId == 0 ? Commands.ReadTemperature1 : Commands.ReadTemperature2); + var response = WriteAndRead(request); + + if (response.IsError) + { + return default; + } + + return Utils.FromLinear11(response.GetData()); + } + + private IReadOnlyCollection GetSpeedSensors() + { + var sensors = new List(); + + var rpm = GetFanRpm(); + sensors.Add(new SpeedSensor("Fan #1", SPEED_CHANNEL, rpm, supportsControl: true)); + + return sensors; + } + + private IReadOnlyCollection GetTemperatureSensors() + { + var sensors = new List(); + + for (int ch = 0; ch < TEMP_CHANNEL_COUNT; ch++) + { + var temp = GetTemperatureSensorValue(ch); + sensors.Add(new TemperatureSensor($"Temp #{ch + 1}", ch, temp)); + } + + return sensors; + } + + private DeviceResponse WriteAndRead(byte[] buffer) + { + var response = CreateResponse(); + + using (_guardManager.AwaitExclusiveAccess()) + { + PerformHandshake(); + + Write(buffer); + Read(response); + } + + return new DeviceResponse(buffer, response); + } + + private DeviceResponse PerformHandshake() + { + var request = CreateHandshakeRequest(); + var response = CreateResponse(); + + Write(request); + Read(response); + + return new DeviceResponse(request, response); + } + + private void Write(byte[] buffer) + { + LogDebug($"WRITE: {buffer.ToHexString()}"); + _device.Write(buffer); + } + + private void Read(byte[] buffer) + { + _device.Read(buffer); + LogDebug($"READ: {buffer.ToHexString()}"); + } + + private static byte[] CreateRequest(byte commandMode, byte command, byte data = default) + { + // [0] report id (always zero) + // [1] command mode (read/write) + // [2] command + // [3..] data + + var writeBuf = new byte[REQUEST_LENGTH]; + writeBuf[1] = commandMode; + writeBuf[2] = command; + writeBuf[3] = data; + return writeBuf; + } + + private static byte[] CreateHandshakeRequest() + { + // handshake request swaps positions of command and command mode + return CreateRequest(Commands.Handshake, CommandModes.Read); + } + + private static byte[] CreateResponse() + { + return new byte[RESPONSE_LENGTH]; + } + + internal sealed class DeviceResponse + { + private const byte RESPONSE_ERROR_CODE = 0xfe; + + 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 new CorsairLinkDeviceException("Response was invalid."); + } + } + + public bool IsValid() + { + return Response[1] switch + { + RESPONSE_ERROR_CODE when Response[3] == RESPONSE_ERROR_CODE => false, + _ when Response[2] == RESPONSE_ERROR_CODE => false, + _ when Request[1] == Response[1] && Request[2] == Response[2] => true, + _ => false + }; + } + } +} diff --git a/src/FanControl.CorsairLink/CorsairLinkPlugin.cs b/src/FanControl.CorsairLink/CorsairLinkPlugin.cs index 6bb24cf..643c930 100644 --- a/src/FanControl.CorsairLink/CorsairLinkPlugin.cs +++ b/src/FanControl.CorsairLink/CorsairLinkPlugin.cs @@ -45,8 +45,8 @@ private void OnTimerTick(object sender, ElapsedEventArgs e) } catch (Exception ex) { - Log($"An exception occurred refreshing device '{device.Name}' ({device.UniqueId}):"); - Log(ex.ToString()); + _logger?.Error(device.Name, $"An exception occurred refreshing device '{device.Name}' ({device.UniqueId}):"); + _logger?.Error(device.Name, ex.ToString()); } } } @@ -60,11 +60,6 @@ private void OnTimerTick(object sender, ElapsedEventArgs e) } } - private void Log(string message) - { - _logger?.Log(message); - } - void IPlugin.Close() { CloseImpl(); @@ -103,7 +98,7 @@ void IPlugin.Initialize() { if (!device.Connect()) { - Log($"Device '{device.Name}' ({device.UniqueId}) failed to connect! This device will not be available."); + _logger?.Error(device.Name, $"Device '{device.Name}' ({device.UniqueId}) failed to connect! This device will not be available."); continue; } @@ -111,8 +106,8 @@ void IPlugin.Initialize() } catch (Exception ex) { - Log($"An exception occurred attempting to initialize device '{device.Name}' ({device.UniqueId}):"); - Log(ex.ToString()); + _logger?.Error(device.Name, $"An exception occurred attempting to initialize device '{device.Name}' ({device.UniqueId}):"); + _logger?.Error(device.Name, ex.ToString()); } } @@ -130,7 +125,7 @@ void IPlugin.Load(IPluginSensorsContainer container) foreach (var device in _devices) { - Log($"Device '{device.Name}' ({device.UniqueId}, FW: {device.GetFirmwareVersion()}):"); + _logger?.Normal(device.Name, $"Device '{device.Name}' ({device.UniqueId}, FW: {device.GetFirmwareVersion()}):"); AddDeviceSpeedSensors(container, device); AddDeviceSpeedControllers(container, device); @@ -144,7 +139,7 @@ private void AddDeviceSpeedSensors(IPluginSensorsContainer container, IDevice de { var pluginSensor = new CorsairLinkSpeedSensor(device, sensor); container.FanSensors.Add(pluginSensor); - Log($" added {pluginSensor.Id}"); + _logger.Normal(device.Name, $" added {pluginSensor.Id}"); } } @@ -154,7 +149,7 @@ private void AddDeviceSpeedControllers(IPluginSensorsContainer container, IDevic { var pluginController = new CorsairLinkSpeedController(device, sensor); container.ControlSensors.Add(pluginController); - Log($" added {pluginController.Id}"); + _logger.Normal(device.Name, $" added {pluginController.Id}"); } } @@ -164,7 +159,7 @@ private void AddDeviceTemperatureSensors(IPluginSensorsContainer container, IDev { var pluginSensor = new CorsairLinkTemperatureSensor(device, sensor); container.TempSensors.Add(pluginSensor); - Log($" added {pluginSensor.Id}"); + _logger.Normal(device.Name, $" added {pluginSensor.Id}"); } } } diff --git a/src/FanControl.CorsairLink/CorsairLinkPluginLogger.cs b/src/FanControl.CorsairLink/CorsairLinkPluginLogger.cs index 4d99b80..3a7da76 100644 --- a/src/FanControl.CorsairLink/CorsairLinkPluginLogger.cs +++ b/src/FanControl.CorsairLink/CorsairLinkPluginLogger.cs @@ -6,11 +6,27 @@ namespace FanControl.CorsairLink; internal class CorsairLinkPluginLogger : ILogger { private readonly IPluginLogger _pluginLogger; + private readonly bool _debugEnabled; public CorsairLinkPluginLogger(IPluginLogger pluginLogger) { _pluginLogger = pluginLogger; + _debugEnabled = Utils.GetEnvironmentFlag("FANCONTROL_CORSAIRLINK_DEBUG_LOGGING_ENABLED"); } + public void Debug(string deviceName, string message) + { + if (!_debugEnabled) + { + return; + } + + Log($"(debug) {deviceName}: {message}"); + } + + public void Error(string deviceName, string message) => Log($"(error) {deviceName}: {message}"); + + public void Normal(string deviceName, string message) => Log($"{deviceName}: {message}"); + public void Log(string message) => _pluginLogger.Log($"[CorsairLink] {message}"); }