Add support for Thermal Grizzly WireView Pro 2. (#2171)

* Add support for Thermal Grizzly WireView Pro 2.

* Process PR comments.

* PR comments.

* Add private keyword.

* PR comments.

* Replace WMI with SetupAPI.

Some first updates according to Thermal Grizzly code.

* Add report.

* Further improve implementation according to code of Thermal Grizzly (thanks !).

* No connection check for welcome message.

* Rename class + change SerialPort to SharedSerialPort for guard - fixing mutex issues.

* PR comments.

* Remove SP_DEVINFO_DATA from NativeMethods.txt

* Update WireViewPro2.cs

* Rename hasMutex to _hasMutex for consistency

* Formatting

* Fix build

---------

Co-authored-by: PhyxionNL <7643972+PhyxionNL@users.noreply.github.com>
This commit is contained in:
Blacktempel
2026-01-27 13:20:07 +01:00
committed by GitHub
co-authored by PhyxionNL
parent 6f9ac5aece
commit df9e0a5131
13 changed files with 1115 additions and 6 deletions
@@ -3,10 +3,15 @@
// Copyright (C) LibreHardwareMonitor and Contributors.
// All Rights Reserved.
using System.Collections.Generic;
using LibreHardwareMonitor.Hardware.Gpu.PowerMonitor;
namespace LibreHardwareMonitor.Hardware.Gpu;
public abstract class GenericGpu : Hardware
{
readonly List<Hardware> _subHardware = new();
/// <summary>
/// Initializes a new instance of the <see cref="GenericGpu" /> class.
/// </summary>
@@ -14,10 +19,30 @@ public abstract class GenericGpu : Hardware
/// <param name="identifier">Identifier that will be assigned to the device. Based on <see cref="Identifier" /></param>
/// <param name="settings">Additional settings passed by the <see cref="IComputer" />.</param>
protected GenericGpu(string name, Identifier identifier, ISettings settings) : base(name, identifier, settings)
{ }
{
TryAddSubHardware();
}
/// <summary>
/// Gets the device identifier.
/// </summary>
public abstract string DeviceId { get; }
}
public override IHardware[] SubHardware => _subHardware.ToArray();
public override void Close()
{
_subHardware.ForEach(h => h.Close());
base.Close();
}
private void TryAddSubHardware()
{
var wireViewPro2 = WireViewPro2.TryFindDevice(_settings);
if (wireViewPro2 != null && wireViewPro2.IsConnected)
{
_subHardware.Add(wireViewPro2);
}
}
}
@@ -0,0 +1,12 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.
namespace LibreHardwareMonitor.Hardware.Gpu.PowerMonitor;
internal interface IPowerMonitor
{
//To be filled
}
@@ -0,0 +1,88 @@
using System.IO.Ports;
namespace LibreHardwareMonitor.Hardware.Gpu.PowerMonitor;
internal sealed class SharedSerialPort : SerialPort
{
private bool _hasMutex;
public SharedSerialPort()
{ }
public SharedSerialPort(string portName) : base(portName)
{ }
public SharedSerialPort(string portName, int baudRate) : base(portName, baudRate)
{ }
public SharedSerialPort(string portName, int baudRate, Parity parity) : base(portName, baudRate, parity)
{ }
public SharedSerialPort(string portName, int baudRate, Parity parity, int dataBits)
: base(portName, baudRate, parity, dataBits)
{ }
public SharedSerialPort(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits)
: base(portName, baudRate, parity, dataBits, stopBits)
{ }
private int MutexTimeout { get; set; } = 500;
public new void Open()
{
_hasMutex = Mutexes.WaitUsbSensors(MutexTimeout);
if (_hasMutex)
{
base.Open();
}
}
public new void Close()
{
if (_hasMutex)
{
if (IsOpen)
{
BaseStream.Flush();
BaseStream.Close();
}
_hasMutex = false;
Mutexes.ReleaseUsbSensors();
}
}
public new void Write(byte[] buffer, int offset, int count)
{
if (_hasMutex)
{
base.Write(buffer, offset, count);
}
}
public new int Read(byte[] buffer, int offset, int count)
{
if (_hasMutex)
{
return base.Read(buffer, offset, count);
}
return 0;
}
public new void DiscardInBuffer()
{
if (_hasMutex)
{
base.DiscardInBuffer();
}
}
public new void DiscardOutBuffer()
{
if (_hasMutex)
{
base.DiscardOutBuffer();
}
}
}
@@ -0,0 +1,547 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Ports;
using System.Runtime.InteropServices;
using System.Text;
using LibreHardwareMonitor.Interop.PowerMonitor;
namespace LibreHardwareMonitor.Hardware.Gpu.PowerMonitor;
/// <summary>
/// Thermal Grizzly WireView Pro II power monitor.
/// </summary>
public sealed class WireViewPro2 : Hardware, IPowerMonitor
{
public const string WelcomeMessage = "Thermal Grizzly WireView Pro II";
private const byte VendorID = 0xEF;
private const byte ProductID = 0x05;
private readonly int _baudRate;
private readonly string _portName;
private readonly List<WireViewPro2Sensor> _sensors = [];
private SharedSerialPort _serialPort;
public WireViewPro2(string portName, ISettings settings, int baud = 115200)
: base("WireView Pro II", new Identifier("gpu-powermonitor", portName), settings)
{
_portName = portName;
_baudRate = baud;
Connect();
if (IsConnected)
{
CreateSensors();
}
}
public override HardwareType HardwareType => HardwareType.GpuPowerMonitor;
public bool IsConnected { get; private set; }
public string UniqueID { get; private set; }
public VendorDataStruct? VendorData { get; private set; }
public static WireViewPro2 TryFindDevice(ISettings settings)
{
if (!Software.OperatingSystem.IsWindows8OrGreater)
{
return null; //No Linux implementation yet
}
List<string> matches = Stm32PortFinder.FindMatchingComPorts(0x0483, 0x5740);
WireViewPro2 wireViewPro2 = null;
foreach (string port in matches)
{
try
{
wireViewPro2 = new WireViewPro2(port, settings);
if (wireViewPro2.IsConnected)
{
break;
}
wireViewPro2.Close();
wireViewPro2 = null;
}
catch
{
wireViewPro2?.Close();
wireViewPro2 = null;
}
}
return wireViewPro2;
}
public override string GetReport()
{
var sb = new StringBuilder();
sb.AppendLine(Name);
foreach (WireViewPro2Sensor sensor in _sensors)
{
sb.AppendLine($" Sensor: {sensor.Name} = {sensor.Value}");
}
return sb.ToString();
}
public override void Close()
{
Disconnect();
base.Close();
}
public override void Update()
{
SensorStruct? sensorValues = ReadSensorValues();
if (sensorValues.HasValue)
{
DeviceData deviceData = MapSensorStructure(sensorValues.Value);
_sensors.ForEach(wvps => wvps.Update(deviceData));
}
}
public DeviceConfigStruct? ReadConfig()
{
if (!IsConnected)
{
return null;
}
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.Write([(byte)UsbCmd.CMD_READ_CONFIG], 0, 1);
byte[] bytes = ReadExact(Marshal.SizeOf<DeviceConfigStruct>());
return bytes == null ? null : BytesToStructure<DeviceConfigStruct>(bytes);
}
finally
{
_serialPort.Close();
}
}
public void WriteConfig(DeviceConfigStruct config)
{
if (!IsConnected)
{
return;
}
byte[] payload = StructureToBytes(config);
byte[] frame = new byte[64];
frame[0] = (byte)UsbCmd.CMD_WRITE_CONFIG;
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
const int maxPayloadPerFrame = 62;
for (byte offset = 0; offset < payload.Length; offset += maxPayloadPerFrame)
{
int bytesToWrite = Math.Min(maxPayloadPerFrame, payload.Length - offset);
frame[1] = offset;
Buffer.BlockCopy(payload, offset, frame, 2, bytesToWrite);
_serialPort.Write(frame, 0, bytesToWrite + 2);
}
}
finally
{
_serialPort.Close();
}
}
public void NonVolatileMemoryCommand(NVM_CMD cmd)
{
if (!IsConnected)
{
return;
}
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.Write([
(byte)UsbCmd.CMD_NVM_CONFIG,
0x55, //Magic
0xAA, //Magic
0x55, //Magic
0xAA, //Magic
(byte)cmd
],
0,
6);
}
finally
{
_serialPort.Close();
}
}
public void ScreenCmd(SCREEN_CMD cmd)
{
if (!IsConnected)
{
return;
}
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.Write([(byte)UsbCmd.CMD_SCREEN_CHANGE, (byte)cmd], 0, 2);
}
finally
{
_serialPort.Close();
}
}
public void ClearFaults(int faultStatusMask = 0xFFFF, int faultLogMask = 0xFFFF)
{
if (!IsConnected)
{
return;
}
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.Write([
(byte)UsbCmd.CMD_CLEAR_FAULTS,
(byte)(faultStatusMask & 0xFF),
(byte)((faultStatusMask >> 8) & 0xFF),
(byte)(faultLogMask & 0xFF),
(byte)((faultLogMask >> 8) & 0xFF)
],
0,
5);
}
finally
{
_serialPort.Close();
}
}
private void CreateSensors()
{
//Onboard temperature sensors
AddSensor("Onboard Temperature In", 0, SensorType.Temperature, dd => (float)dd.OnboardTempInC);
AddSensor("Onboard Temperature Out", 1, SensorType.Temperature, dd => (float)dd.OnboardTempOutC);
//External temperature sensors, requires shipped temperature sensors to be connected
AddSensor("External Temperature 1", 2, SensorType.Temperature, dd => (float)dd.ExternalTemp1C);
AddSensor("External Temperature 2", 3, SensorType.Temperature, dd => (float)dd.ExternalTemp2C);
//Pin voltages
AddSensor("Pin 1 Voltage", 10, SensorType.Voltage, dd => (float)dd.PinVoltage[0]);
AddSensor("Pin 2 Voltage", 11, SensorType.Voltage, dd => (float)dd.PinVoltage[1]);
AddSensor("Pin 3 Voltage", 12, SensorType.Voltage, dd => (float)dd.PinVoltage[2]);
AddSensor("Pin 4 Voltage", 13, SensorType.Voltage, dd => (float)dd.PinVoltage[3]);
AddSensor("Pin 5 Voltage", 14, SensorType.Voltage, dd => (float)dd.PinVoltage[4]);
AddSensor("Pin 6 Voltage", 15, SensorType.Voltage, dd => (float)dd.PinVoltage[5]);
//Pin currents
AddSensor("Total Current", 20, SensorType.Current, dd => (float)dd.SumCurrentA);
AddSensor("Pin 1 Current", 21, SensorType.Current, dd => (float)dd.PinCurrent[0]);
AddSensor("Pin 2 Current", 22, SensorType.Current, dd => (float)dd.PinCurrent[1]);
AddSensor("Pin 3 Current", 23, SensorType.Current, dd => (float)dd.PinCurrent[2]);
AddSensor("Pin 4 Current", 24, SensorType.Current, dd => (float)dd.PinCurrent[3]);
AddSensor("Pin 5 Current", 25, SensorType.Current, dd => (float)dd.PinCurrent[4]);
AddSensor("Pin 6 Current", 26, SensorType.Current, dd => (float)dd.PinCurrent[5]);
//Power
AddSensor("Total Power", 30, SensorType.Power, dd => (float)dd.SumPowerW);
}
private void AddSensor(string name, int index, SensorType sensorType, GetWireViewPro2SensorValue getValue)
{
var sensor = new WireViewPro2Sensor(name, index, sensorType, this, _settings, getValue);
_sensors.Add(sensor);
ActivateSensor(sensor);
}
private void Connect()
{
if (IsConnected)
{
return;
}
try
{
_serialPort = new SharedSerialPort(_portName, _baudRate, Parity.None, 8, StopBits.One);
_serialPort.ReadTimeout = 1000;
_serialPort.WriteTimeout = 1000;
if (!ReadWelcomeMessage())
{
IsConnected = false;
return;
}
VendorDataStruct? vendorData = ReadVendorData();
if (vendorData.HasValue &&
vendorData.Value.VendorId == VendorID &&
vendorData.Value.ProductId == ProductID)
{
VendorData = vendorData.Value;
UniqueID = ReadUniqueID();
IsConnected = true;
}
}
catch
{
if (_serialPort != null)
{
try
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
}
catch
{
//Ignore exceptions during cleanup
}
finally
{
_serialPort.Dispose();
_serialPort = null;
}
}
IsConnected = false;
}
}
private void Disconnect()
{
if (!IsConnected)
{
return;
}
if (_serialPort != null)
{
_serialPort.Close();
_serialPort.Dispose();
_serialPort = null;
}
IsConnected = false;
VendorData = null;
UniqueID = null;
}
private bool ReadWelcomeMessage(bool sendCmd = false)
{
int size = WelcomeMessage.Length + 1;
try
{
_serialPort.Open();
_serialPort.RtsEnable = true;
if (sendCmd)
{
_serialPort.DiscardInBuffer();
_serialPort.Write([(byte)UsbCmd.CMD_WELCOME], 0, 1);
}
byte[] bytes = ReadExact(size);
_serialPort.RtsEnable = false;
return bytes == null ? false : Encoding.ASCII.GetString(bytes, 0, size).TrimEnd('\0').CompareTo(WelcomeMessage) == 0;
}
finally
{
_serialPort.Close();
}
}
private VendorDataStruct? ReadVendorData()
{
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.Write([(byte)UsbCmd.CMD_READ_VENDOR_DATA], 0, 1);
byte[] bytes = ReadExact(Marshal.SizeOf<VendorDataStruct>());
return bytes == null ? null : BytesToStructure<VendorDataStruct>(bytes);
}
finally
{
_serialPort.Close();
}
}
private string ReadUniqueID()
{
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.Write([(byte)UsbCmd.CMD_READ_UID], 0, 1);
byte[] bytes = ReadExact(12);
return bytes == null ? null : BitConverter.ToString(bytes).Replace("-", string.Empty);
}
finally
{
_serialPort.Close();
}
}
private SensorStruct? ReadSensorValues()
{
if (!IsConnected)
{
return null;
}
try
{
_serialPort.Open();
_serialPort.DiscardInBuffer();
_serialPort.Write([(byte)UsbCmd.CMD_READ_SENSOR_VALUES], 0, 1);
byte[] bytes = ReadExact(Marshal.SizeOf<SensorStruct>());
return bytes == null ? null : BytesToStructure<SensorStruct>(bytes);
}
finally
{
_serialPort.Close();
}
}
private DeviceData MapSensorStructure(SensorStruct sensorStruct)
{
var deviceData = new DeviceData
{
Connected = true,
HardwareRevision = $"{VendorData?.VendorId}{VendorData?.ProductId}",
FirmwareVersion = $"{VendorData?.FwVersion}",
OnboardTempInC = sensorStruct.Ts[(int)SensorTs.SENSOR_TS_IN] / 10.0,
OnboardTempOutC = sensorStruct.Ts[(int)SensorTs.SENSOR_TS_OUT] / 10.0,
ExternalTemp1C = sensorStruct.Ts[(int)SensorTs.SENSOR_TS3] / 10.0,
ExternalTemp2C = sensorStruct.Ts[(int)SensorTs.SENSOR_TS4] / 10.0,
FaultStatus = sensorStruct.FaultStatus,
FaultLog = sensorStruct.FaultLog,
};
switch (sensorStruct.HpwrCapability)
{
case HpwrCapability.PSU_CAP_600W:
deviceData.PsuCapabilityW = 600;
break;
case HpwrCapability.PSU_CAP_450W:
deviceData.PsuCapabilityW = 450;
break;
case HpwrCapability.PSU_CAP_300W:
deviceData.PsuCapabilityW = 300;
break;
case HpwrCapability.PSU_CAP_150W:
deviceData.PsuCapabilityW = 150;
break;
}
for (int i = 0; i < 6; ++i)
{
deviceData.PinVoltage[i] = sensorStruct.PowerReadings[i].Voltage / 1000.0;
deviceData.PinCurrent[i] = sensorStruct.PowerReadings[i].Current / 1000.0;
}
return deviceData;
}
private byte[] ReadExact(int size)
{
byte[] buffer = new byte[size];
int offset = 0;
var sw = Stopwatch.StartNew();
while (offset < size && sw.ElapsedMilliseconds < _serialPort.ReadTimeout)
{
try
{
if (_serialPort.BytesToRead > 0)
{
offset += _serialPort.Read(buffer, offset, size - offset);
}
}
catch (TimeoutException)
{
//Ignore timeout exceptions and continue reading
}
}
return offset != size ? null : buffer;
}
private T BytesToStructure<T>(byte[] bytes) where T : struct
{
var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
}
finally
{
handle.Free();
}
}
private byte[] StructureToBytes<T>(T value) where T : struct
{
int size = Marshal.SizeOf<T>();
byte[] bytes = new byte[size];
IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(value, ptr, false);
Marshal.Copy(ptr, bytes, 0, size);
return bytes;
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
}
@@ -0,0 +1,29 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.
using LibreHardwareMonitor.Interop.PowerMonitor;
namespace LibreHardwareMonitor.Hardware.Gpu.PowerMonitor;
internal delegate float GetWireViewPro2SensorValue(DeviceData wvp);
internal class WireViewPro2Sensor : Sensor
{
readonly GetWireViewPro2SensorValue _getValue;
public WireViewPro2Sensor(string name, int index, SensorType sensorType, Hardware hardware, ISettings settings, GetWireViewPro2SensorValue getValue)
: base(name, index, sensorType, hardware, settings)
{
_getValue = getValue;
}
internal void Update(DeviceData wvp)
{
float value = _getValue(wvp);
Value = value;
}
}
+2 -2
View File
@@ -76,7 +76,7 @@ public abstract class Hardware : IHardware
}
/// <inheritdoc />
public IHardware[] SubHardware
public virtual IHardware[] SubHardware
{
get { return Array.Empty<IHardware>(); }
}
@@ -131,4 +131,4 @@ public abstract class Hardware : IHardware
public event SensorEventHandler SensorRemoved;
#pragma warning restore 67
}
}
@@ -23,5 +23,6 @@ public enum HardwareType
Cooler,
EmbeddedController,
Psu,
Battery
}
Battery,
GpuPowerMonitor,
}
@@ -11,6 +11,7 @@ internal static class Mutexes
private static Mutex _isaBusMutex;
private static Mutex _pciBusMutex;
private static Mutex _razerMutex;
private static Mutex _usbSensors;
/// <summary>
/// Opens the mutexes.
@@ -21,6 +22,7 @@ internal static class Mutexes
_pciBusMutex = CreateOrOpenExistingMutex("Global\\Access_PCI");
_ecMutex = CreateOrOpenExistingMutex("Global\\Access_EC");
_razerMutex = CreateOrOpenExistingMutex("Global\\RazerReadWriteGuardMutex");
_usbSensors = CreateOrOpenExistingMutex("Global\\Access_USB_Sensors");
static Mutex CreateOrOpenExistingMutex(string name)
{
@@ -61,6 +63,7 @@ internal static class Mutexes
_pciBusMutex?.Close();
_ecMutex?.Close();
_razerMutex?.Close();
_usbSensors?.Close();
}
public static bool WaitIsaBus(int millisecondsTimeout)
@@ -103,6 +106,16 @@ internal static class Mutexes
_razerMutex?.ReleaseMutex();
}
public static bool WaitUsbSensors(int millisecondsTimeout)
{
return WaitMutex(_usbSensors, millisecondsTimeout);
}
public static void ReleaseUsbSensors()
{
_usbSensors?.ReleaseMutex();
}
private static bool WaitMutex(Mutex mutex, int millisecondsTimeout)
{
if (mutex == null)
@@ -0,0 +1,43 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.
using System;
using System.Linq;
namespace LibreHardwareMonitor.Interop.PowerMonitor;
internal sealed class DeviceData
{
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public bool Connected { get; set; }
public string HardwareRevision { get; set; } = "A0";
public string FirmwareVersion { get; set; } = "0.0.0";
public double[] PinVoltage { get; set; } = new double[6];
public double[] PinCurrent { get; set; } = new double[6];
public double OnboardTempInC { get; set; }
public double OnboardTempOutC { get; set; }
public double ExternalTemp1C { get; set; }
public double ExternalTemp2C { get; set; }
public int PsuCapabilityW { get; set; }
public double SumCurrentA => PinCurrent.Sum();
public double SumPowerW => PinVoltage.Zip(PinCurrent, (v, i) => v * i).Sum();
public ushort FaultStatus { get; set; }
public ushort FaultLog { get; set; }
}
@@ -0,0 +1,131 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32;
using Windows.Win32.Devices.DeviceAndDriverInstallation;
using Windows.Win32.Foundation;
namespace LibreHardwareMonitor.Interop.PowerMonitor;
/// <summary>
/// Helper class to find STM32 COM ports based on VID and PID.
/// </summary>
public static class Stm32PortFinder
{
private const int BufferSize = 1024;
/// <summary>
/// Finds the names of all available COM ports that match the specified USB vendor ID (VID) and product ID (PID).
/// </summary>
/// <param name="vid">The USB vendor ID to match.</param>
/// <param name="pid">The USB product ID to match.</param>
/// <returns>
/// A list of strings containing the names of matching COM ports.<br />
/// The list is empty if no matching ports are found.
/// </returns>
public static List<string> FindMatchingComPorts(uint vid, uint pid)
{
return FindMatchingComPorts($"{vid:X4}", $"{pid:X4}");
}
/// <summary>
/// <inheritdoc cref="FindMatchingComPorts(uint, uint)" />
/// </summary>
/// <param name="vid">
/// <inheritdoc cref="FindMatchingComPorts(uint, uint)" />
/// </param>
/// <param name="pid">
/// <inheritdoc cref="FindMatchingComPorts(uint, uint)" />
/// </param>
/// <returns>
/// <inheritdoc cref="FindMatchingComPorts(uint, uint)" />
/// </returns>
public static List<string> FindMatchingComPorts(string vid, string pid)
{
var result = new List<string>();
//Setup
SetupDiDestroyDeviceInfoListSafeHandle devInfo = PInvoke.SetupDiGetClassDevs(PInvoke.GUID_DEVCLASS_PORTS, null, HWND.Null, SETUP_DI_GET_CLASS_DEVS_FLAGS.DIGCF_PRESENT);
//Check handle
if (devInfo.IsInvalid)
{
return result;
}
try
{
SP_DEVINFO_DATA devInfoData = new() { cbSize = (uint)Marshal.SizeOf<SP_DEVINFO_DATA>() };
uint index = 0;
while (PInvoke.SetupDiEnumDeviceInfo(devInfo, index++, ref devInfoData))
{
//Get hardware ID
string hwId = GetProperty(devInfo, SETUP_DI_REGISTRY_PROPERTY.SPDRP_HARDWAREID, devInfoData);
if (string.IsNullOrWhiteSpace(hwId))
{
continue;
}
//Check if hardware ID contains VID and PID
if (!hwId.Contains($"VID_{vid}", StringComparison.OrdinalIgnoreCase) ||
!hwId.Contains($"PID_{pid}", StringComparison.OrdinalIgnoreCase))
{
continue;
}
//Get friendly name
string friendlyName = GetProperty(devInfo, SETUP_DI_REGISTRY_PROPERTY.SPDRP_FRIENDLYNAME, devInfoData);
//Extract COM port from friendly name
int start = friendlyName.LastIndexOf("(COM", StringComparison.OrdinalIgnoreCase);
int end = friendlyName.LastIndexOf(')');
//Check if valid and add to result
if (start >= 0 && end > start)
{
string com = friendlyName.Substring(start + 1, end - start - 1);
result.Add(com);
}
}
}
finally
{
//Cleanup
devInfo.Dispose();
}
return result;
static string GetProperty(SafeHandle hDevInfo, SETUP_DI_REGISTRY_PROPERTY property, SP_DEVINFO_DATA devInfoData)
{
byte[] buffer = new byte[BufferSize];
if (PInvoke.SetupDiGetDeviceRegistryProperty(hDevInfo, devInfoData, property, buffer))
{
//Take first string only, no need for the rest
return NormalizeString(Encoding.Unicode.GetString(buffer));
}
return null;
}
}
private static string NormalizeString(string str)
{
int end = str.IndexOf('\0');
if (end == -1)
{
end = 0;
}
return str.Substring(0, end);
}
}
@@ -0,0 +1,120 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.
namespace LibreHardwareMonitor.Interop.PowerMonitor;
public enum UsbCmd : byte
{
CMD_WELCOME,
CMD_READ_VENDOR_DATA,
CMD_READ_UID,
CMD_READ_DEVICE_DATA,
CMD_READ_SENSOR_VALUES,
CMD_READ_CONFIG,
CMD_WRITE_CONFIG,
CMD_READ_CALIBRATION,
CMD_WRITE_CALIBRATION,
CMD_SPI_FLASH_WRITE_PAGE,
CMD_SPI_FLASH_READ_PAGE,
CMD_SPI_FLASH_ERASE_SECTOR,
CMD_SCREEN_CHANGE,
CMD_READ_BUILD_INFO,
CMD_CLEAR_FAULTS,
CMD_RESET = 0xF0,
CMD_BOOTLOADER = 0xF1,
CMD_NVM_CONFIG = 0xF2,
CMD_NOP = 0xFF
}
public enum SensorTs
{
SENSOR_TS_IN,
SENSOR_TS_OUT,
SENSOR_TS3,
SENSOR_TS4,
}
public enum HpwrCapability : byte
{
PSU_CAP_600W = 0,
PSU_CAP_450W = 1,
PSU_CAP_300W = 2,
PSU_CAP_150W = 3
}
public enum FanMode : byte
{
FanModeCurve = 0,
FanModeFixed = 1
}
public enum TempSource : byte
{
TempSourceTsIn = 0,
TempSourceTsOut = 1,
TempSourceTs1 = 2,
TempSourceTs2 = 3,
TempSourceTmax = 4
}
public enum CurrentScale : byte
{
CurrentScale5A = 0,
CurrentScale10A = 1,
CurrentScale15A = 2,
CurrentScale20A = 3
}
public enum PowerScale : byte
{
PowerScaleAuto = 0,
PowerScale300W = 1,
PowerScale600W = 2
}
public enum Theme : byte
{
ThemeTg1 = 0,
ThemeTg2 = 1,
ThemeTg3 = 2
}
public enum DisplayRotation : byte
{
DisplayRotation0 = 0,
DisplayRotation180 = 1
}
public enum TimeoutMode : byte
{
TimeoutModeStatic = 0,
TimeoutModeCycle = 1,
TimeoutModeSleep = 2
}
public enum NVM_CMD : byte
{
NVM_CMD_NONE,
NVM_CMD_LOAD,
NVM_CMD_STORE,
NVM_CMD_RESET,
NVM_CMD_LOAD_CAL,
NVM_CMD_STORE_CAL,
NVM_CMD_LOAD_CAL_FACTORY,
NVM_CMD_STORE_CAL_FACTORY,
}
public enum SCREEN_CMD : byte
{
SCREEN_GOTO_MAIN = 0xE0,
SCREEN_GOTO_SIMPLE = 0xE1,
SCREEN_GOTO_CURRENT = 0xE2,
SCREEN_GOTO_TEMP = 0xE3,
SCREEN_GOTO_STATUS = 0xE4,
SCREEN_GOTO_SAME = 0xEF,
SCREEN_PAUSE_UPDATES = 0xF0,
SCREEN_RESUME_UPDATES = 0xF1
}
@@ -0,0 +1,96 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.
using System.Runtime.InteropServices;
namespace LibreHardwareMonitor.Interop.PowerMonitor;
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct VendorDataStruct
{
public byte VendorId;
public byte ProductId;
public byte FwVersion;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct PowerSensor
{
public short Voltage;
public uint Current;
public uint Power;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct SensorStruct
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public short[] Ts; // 0.1 °C
public ushort Vdd; // mV
public byte FanDuty; // %
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public PowerSensor[] PowerReadings;
public uint TotalPower; // mW
public uint TotalCurrent; // mA
public ushort AvgVoltage; // mV
public HpwrCapability HpwrCapability; // 8-bit enum
public ushort FaultStatus;
public ushort FaultLog;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct FanConfigStruct
{
public FanMode Mode;
public TempSource TempSource;
public byte DutyMin;
public byte DutyMax;
public short TempMin;
public short TempMax;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct UiConfigStruct
{
public CurrentScale CurrentScale;
public PowerScale PowerScale;
public Theme Theme;
public DisplayRotation DisplayRotation;
public TimeoutMode TimeoutMode;
public byte CycleScreens;
public byte CycleTime;
public byte Timeout;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct DeviceConfigStruct
{
public ushort Crc;
public byte Version;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] FriendlyName;
public FanConfigStruct FanConfig;
public byte BacklightDuty;
public ushort FaultDisplayEnable;
public ushort FaultBuzzerEnable;
public ushort FaultSoftPowerEnable;
public ushort FaultHardPowerEnable;
public short TsFaultThreshold; // 0.1 °C
public byte OcpFaultThreshold; // A
public byte WireOcpFaultThreshold; // 0.1A
public ushort OppFaultThreshold; // W
public byte CurrentImbalanceFaultThreshold; // %
public byte CurrentImbalanceFaultMinLoad; // A
public byte ShutdownWaitTime; // seconds
public byte LoggingInterval; // seconds
public UiConfigStruct Ui;
}
@@ -13,13 +13,17 @@ D3DKMT_ADAPTERTYPE
D3DKMT_SEGMENTSIZEINFO
D3DKMT_NODEMETADATA
D3DKMT_QUERYSTATISTICS_NODE_INFORMATION
SetupDiEnumDeviceInfo
SetupDiGetDeviceRegistryProperty
SetupDiDestroyDeviceInfoList
SetupDiGetDeviceInterfaceDetail
SetupDiEnumDeviceInterfaces
SetupDiGetClassDevs
SP_DEVICE_INTERFACE_DATA
SP_DEVICE_INTERFACE_DETAIL_DATA_W
GUID_DEVCLASS_PORTS
GUID_DEVICE_BATTERY
SETUP_DI_REGISTRY_PROPERTY
WIN32_ERROR
HRESULT_FROM_WIN32
GlobalMemoryStatusEx