From df9e0a5131a07de57c33a8b48a3f464745cf5e09 Mon Sep 17 00:00:00 2001 From: Blacktempel <6657796+Blacktempel@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:20:07 +0100 Subject: [PATCH] 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> --- .../Hardware/Gpu/GenericGpu.cs | 29 +- .../Gpu/PowerMonitor/IPowerMonitor.cs | 12 + .../Gpu/PowerMonitor/SharedSerialPort.cs | 88 +++ .../Hardware/Gpu/PowerMonitor/WireViewPro2.cs | 547 ++++++++++++++++++ .../Gpu/PowerMonitor/WireViewPro2Sensor.cs | 29 + LibreHardwareMonitorLib/Hardware/Hardware.cs | 4 +- .../Hardware/HardwareType.cs | 5 +- LibreHardwareMonitorLib/Hardware/Mutexes.cs | 13 + .../Interop/PowerMonitor/DeviceData.cs | 43 ++ .../Interop/PowerMonitor/Stm32PortFinder.cs | 131 +++++ .../Interop/PowerMonitor/WireViewPro2Enums.cs | 120 ++++ .../PowerMonitor/WireViewPro2Structures.cs | 96 +++ LibreHardwareMonitorLib/NativeMethods.txt | 4 + 13 files changed, 1115 insertions(+), 6 deletions(-) create mode 100644 LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/IPowerMonitor.cs create mode 100644 LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/SharedSerialPort.cs create mode 100644 LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2.cs create mode 100644 LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2Sensor.cs create mode 100644 LibreHardwareMonitorLib/Interop/PowerMonitor/DeviceData.cs create mode 100644 LibreHardwareMonitorLib/Interop/PowerMonitor/Stm32PortFinder.cs create mode 100644 LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Enums.cs create mode 100644 LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Structures.cs diff --git a/LibreHardwareMonitorLib/Hardware/Gpu/GenericGpu.cs b/LibreHardwareMonitorLib/Hardware/Gpu/GenericGpu.cs index cb4ff96..e70e246 100644 --- a/LibreHardwareMonitorLib/Hardware/Gpu/GenericGpu.cs +++ b/LibreHardwareMonitorLib/Hardware/Gpu/GenericGpu.cs @@ -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 _subHardware = new(); + /// /// Initializes a new instance of the class. /// @@ -14,10 +19,30 @@ public abstract class GenericGpu : Hardware /// Identifier that will be assigned to the device. Based on /// Additional settings passed by the . protected GenericGpu(string name, Identifier identifier, ISettings settings) : base(name, identifier, settings) - { } + { + TryAddSubHardware(); + } /// /// Gets the device identifier. /// public abstract string DeviceId { get; } -} \ No newline at end of file + + 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); + } + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/IPowerMonitor.cs b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/IPowerMonitor.cs new file mode 100644 index 0000000..1f69105 --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/IPowerMonitor.cs @@ -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 and Contributors. +// All Rights Reserved. + +namespace LibreHardwareMonitor.Hardware.Gpu.PowerMonitor; + +internal interface IPowerMonitor +{ + //To be filled +} diff --git a/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/SharedSerialPort.cs b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/SharedSerialPort.cs new file mode 100644 index 0000000..8bb3be6 --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/SharedSerialPort.cs @@ -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(); + } + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2.cs b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2.cs new file mode 100644 index 0000000..f97aa04 --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2.cs @@ -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; + +/// +/// Thermal Grizzly WireView Pro II power monitor. +/// +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 _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 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()); + + return bytes == null ? null : BytesToStructure(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()); + + return bytes == null ? null : BytesToStructure(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()); + + return bytes == null ? null : BytesToStructure(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(byte[] bytes) where T : struct + { + var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + + try + { + return Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + } + finally + { + handle.Free(); + } + } + + private byte[] StructureToBytes(T value) where T : struct + { + int size = Marshal.SizeOf(); + + 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); + } + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2Sensor.cs b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2Sensor.cs new file mode 100644 index 0000000..e074a59 --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/Gpu/PowerMonitor/WireViewPro2Sensor.cs @@ -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 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; + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Hardware.cs b/LibreHardwareMonitorLib/Hardware/Hardware.cs index b96e991..03d4be3 100644 --- a/LibreHardwareMonitorLib/Hardware/Hardware.cs +++ b/LibreHardwareMonitorLib/Hardware/Hardware.cs @@ -76,7 +76,7 @@ public abstract class Hardware : IHardware } /// - public IHardware[] SubHardware + public virtual IHardware[] SubHardware { get { return Array.Empty(); } } @@ -131,4 +131,4 @@ public abstract class Hardware : IHardware public event SensorEventHandler SensorRemoved; #pragma warning restore 67 -} \ No newline at end of file +} diff --git a/LibreHardwareMonitorLib/Hardware/HardwareType.cs b/LibreHardwareMonitorLib/Hardware/HardwareType.cs index 7a64171..e6fec96 100644 --- a/LibreHardwareMonitorLib/Hardware/HardwareType.cs +++ b/LibreHardwareMonitorLib/Hardware/HardwareType.cs @@ -23,5 +23,6 @@ public enum HardwareType Cooler, EmbeddedController, Psu, - Battery -} \ No newline at end of file + Battery, + GpuPowerMonitor, +} diff --git a/LibreHardwareMonitorLib/Hardware/Mutexes.cs b/LibreHardwareMonitorLib/Hardware/Mutexes.cs index b1de10e..3fdd5b8 100644 --- a/LibreHardwareMonitorLib/Hardware/Mutexes.cs +++ b/LibreHardwareMonitorLib/Hardware/Mutexes.cs @@ -11,6 +11,7 @@ internal static class Mutexes private static Mutex _isaBusMutex; private static Mutex _pciBusMutex; private static Mutex _razerMutex; + private static Mutex _usbSensors; /// /// 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) diff --git a/LibreHardwareMonitorLib/Interop/PowerMonitor/DeviceData.cs b/LibreHardwareMonitorLib/Interop/PowerMonitor/DeviceData.cs new file mode 100644 index 0000000..63d5d6e --- /dev/null +++ b/LibreHardwareMonitorLib/Interop/PowerMonitor/DeviceData.cs @@ -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 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; } +} diff --git a/LibreHardwareMonitorLib/Interop/PowerMonitor/Stm32PortFinder.cs b/LibreHardwareMonitorLib/Interop/PowerMonitor/Stm32PortFinder.cs new file mode 100644 index 0000000..715322a --- /dev/null +++ b/LibreHardwareMonitorLib/Interop/PowerMonitor/Stm32PortFinder.cs @@ -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 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; + +/// +/// Helper class to find STM32 COM ports based on VID and PID. +/// +public static class Stm32PortFinder +{ + private const int BufferSize = 1024; + + /// + /// Finds the names of all available COM ports that match the specified USB vendor ID (VID) and product ID (PID). + /// + /// The USB vendor ID to match. + /// The USB product ID to match. + /// + /// A list of strings containing the names of matching COM ports.
+ /// The list is empty if no matching ports are found. + ///
+ public static List FindMatchingComPorts(uint vid, uint pid) + { + return FindMatchingComPorts($"{vid:X4}", $"{pid:X4}"); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static List FindMatchingComPorts(string vid, string pid) + { + var result = new List(); + + //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() }; + + 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); + } +} diff --git a/LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Enums.cs b/LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Enums.cs new file mode 100644 index 0000000..5822ef8 --- /dev/null +++ b/LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Enums.cs @@ -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 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 +} diff --git a/LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Structures.cs b/LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Structures.cs new file mode 100644 index 0000000..8aab921 --- /dev/null +++ b/LibreHardwareMonitorLib/Interop/PowerMonitor/WireViewPro2Structures.cs @@ -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 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; +} diff --git a/LibreHardwareMonitorLib/NativeMethods.txt b/LibreHardwareMonitorLib/NativeMethods.txt index dd22d77..8e118ea 100644 --- a/LibreHardwareMonitorLib/NativeMethods.txt +++ b/LibreHardwareMonitorLib/NativeMethods.txt @@ -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