Add MSI controller for AIOs. (#2207)

* Add MSI controller for AIOs.
Fan RPM read, thermal sensor & fan control.

* PR comments & rename controller.

* PR review

* PR review

* PR review
This commit is contained in:
Blacktempel
2026-02-11 12:39:29 +01:00
committed by GitHub
parent 24e76153eb
commit 9af84004a5
9 changed files with 728 additions and 0 deletions
@@ -14,6 +14,7 @@ using LibreHardwareMonitor.Hardware.Controller.AeroCool;
using LibreHardwareMonitor.Hardware.Controller.AquaComputer;
using LibreHardwareMonitor.Hardware.Controller.Arctic;
using LibreHardwareMonitor.Hardware.Controller.Heatmaster;
using LibreHardwareMonitor.Hardware.Controller.MSI;
using LibreHardwareMonitor.Hardware.Controller.Nzxt;
using LibreHardwareMonitor.Hardware.Controller.Razer;
using LibreHardwareMonitor.Hardware.Controller.TBalancer;
@@ -128,6 +129,7 @@ public class Computer : IComputer
Add(new NzxtGroup(_settings));
Add(new RazerGroup(_settings));
Add(new ArcticGroup(_settings));
Add(new MsiGroup(_settings));
}
else
{
@@ -138,6 +140,7 @@ public class Computer : IComputer
RemoveType<NzxtGroup>();
RemoveType<RazerGroup>();
RemoveType<ArcticGroup>();
RemoveType<MsiGroup>();
}
}
@@ -526,6 +529,7 @@ public class Computer : IComputer
Add(new NzxtGroup(_settings));
Add(new RazerGroup(_settings));
Add(new ArcticGroup(_settings));
Add(new MsiGroup(_settings));
}
if (_storageEnabled)
@@ -0,0 +1,22 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
using System.Collections.Generic;
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
internal static class MsiConstants
{
public static readonly IReadOnlyList<MsiDevice> SupportedDevices = new List<MsiDevice>
{
new MsiDevice(MsiDeviceType.S280, 0x0DB0, 0x75B6, 0x6A04),
new MsiDevice(MsiDeviceType.S360, 0x0DB0, 0x9BA6, 0x6A05),
new MsiDevice(MsiDeviceType.S360MEG, 0x1462, 0x9BA6, 0x6A05),
new MsiDevice(MsiDeviceType.X360, 0x0DB0, 0x5259, 0x6A11),
new MsiDevice(MsiDeviceType.X240, 0x0DB0, 0xC7B2, 0x6A10),
new MsiDevice(MsiDeviceType.D360, 0x0DB0, 0x8DBF, 0x6A15),
new MsiDevice(MsiDeviceType.D240, 0x0DB0, 0xD085, 0x6A16),
};
}
@@ -0,0 +1,441 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using HidSharp;
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
internal class MsiCoreLiquidController : Hardware
{
private const int MutexTimeout = 500;
private const int DataLength = 64;
private readonly MsiDevice _msiDevice;
private readonly HidDevice _hidDevice;
private readonly List<MsiSensor> _sensors = [];
private Sensor _fan1;
private Sensor _fan2;
private Sensor _fan3;
private Sensor _fan4;
private Sensor _fan5;
public MsiCoreLiquidController(MsiDevice msiDevice, HidDevice hidDevice, ISettings settings)
: base(msiDevice.Name, new(hidDevice), settings)
{
_msiDevice = msiDevice;
_hidDevice = hidDevice;
CreateSensors();
}
public MsiDevice MsiDevice => _msiDevice;
public override HardwareType HardwareType => HardwareType.Cooler;
public override void Update()
{
var msi = new MsiFanControl();
if (GetCoolerStatus(msi) && GetFanConfigure(msi) && GetFanTemperatureConfigure(msi))
{
_sensors.ForEach(s => s.Update(msi));
}
}
public override string GetReport()
{
var sb = new StringBuilder();
sb.AppendLine($"{MsiDevice.Name} - ({MsiDevice.VendorId:X4}:{MsiDevice.ProductIdController:X4})");
foreach (var sensor in _sensors)
{
sb.AppendLine($"{sensor.Name}: {sensor.Value?.ToString() ?? "No value"}");
}
return sb.ToString();
}
private void CreateSensors()
{
if (MsiDevice.DeviceType == MsiDeviceType.S360)
{
_fan1 = AddSensor("Radiator Fan", 10, SensorType.Fan, m => m.Fan1.Speed);
_fan4 = AddSensor("Pump Fan", 13, SensorType.Fan, m => m.Fan4.Speed);
_fan5 = AddSensor("Pump", 14, SensorType.Fan, m => m.Fan5.Speed);
}
else //May need other mapping for different devices
{
_fan1 = AddSensor("Fan 1", 10, SensorType.Fan, m => m.Fan1.Speed);
_fan2 = AddSensor("Fan 2", 11, SensorType.Fan, m => m.Fan2.Speed);
_fan3 = AddSensor("Fan 3", 12, SensorType.Fan, m => m.Fan3.Speed);
_fan4 = AddSensor("Fan 4", 13, SensorType.Fan, m => m.Fan4.Speed);
_fan5 = AddSensor("Fan 5", 14, SensorType.Fan, m => m.Fan5.Speed);
}
AddSensor("Inlet Temperature", 0, SensorType.Temperature, m => m.TemperatureInlet);
AddSensor("Liquid Temperature", 1, SensorType.Temperature, m => m.TemperatureOutlet); //Msi: "Liquid_Temp"
AddSensor("Temperature Sensor 1", 2, SensorType.Temperature, m => m.TemperatureSensor1 == 125 ? -100 : m.TemperatureSensor1);
AddSensor("Temperature Sensor 2", 3, SensorType.Temperature, m => m.TemperatureSensor2 == 125 ? -100 : m.TemperatureSensor2);
void TryAddFanControl(Sensor fan)
{
if (fan != null)
{
var fanControl = new Control(fan, _settings, 0, 100);
fan.Control = fanControl;
fanControl.ControlModeChanged += OnFanControlModeChanged;
fanControl.SoftwareControlValueChanged += OnSoftwareControlValueChanged;
}
}
//Fan Controls
TryAddFanControl(_fan1);
TryAddFanControl(_fan2);
TryAddFanControl(_fan3);
TryAddFanControl(_fan4);
TryAddFanControl(_fan5);
}
private MsiSensor AddSensor(string name, int index, SensorType sensorType, GetMsiSensorValue getValue)
{
var sensor = new MsiSensor(name, index, sensorType, this, _settings, getValue);
_sensors.Add(sensor);
ActivateSensor(sensor);
return sensor;
}
private void OnFanControlModeChanged(Control control)
{
var msi = new MsiFanControl();
if (GetFanConfigure(msi) && GetFanTemperatureConfigure(msi))
{
var fan = GetFanFromControl(msi, control);
if (fan == null)
{
return;
}
switch (control.ControlMode)
{
case ControlMode.Software:
fan.ConfigureDuty.Mode = MsiFanMode.Custom;
break;
case ControlMode.Default:
fan.ConfigureDuty.Mode = MsiFanMode.Bios;
break;
default:
return;
}
SetFanConfigure(msi);
}
}
private void OnSoftwareControlValueChanged(Control control)
{
var msi = new MsiFanControl();
if (GetFanConfigure(msi) && GetFanTemperatureConfigure(msi))
{
var fan = GetFanFromControl(msi, control);
if (fan == null)
{
return;
}
byte value = (byte)control.SoftwareValue;
if (value < 0)
{
value = 0;
}
else if (value > 100)
{
value = 100;
}
//Set requested speed in % and CPU temperature in degrees Celsius
fan.ConfigureDuty.Item0 = value;
fan.ConfigureTemp.Item0 = 0; //Set temperature to 0 to use fixed duty instead of temperature curve
fan.ConfigureDuty.Item1 = value;
fan.ConfigureTemp.Item1 = 90;
//Add failsafe to prevent fan from stopping at high temperatures if user sets low duty
fan.ConfigureDuty.Item2 = 100; //Max fan
fan.ConfigureTemp.Item2 = 91;
fan.ConfigureDuty.Item3 = 100; //Max fan
fan.ConfigureTemp.Item3 = 95;
SetFanConfigure(msi);
SetFanTemperatureConfigure(msi);
}
}
private MsiFan GetFanFromControl(MsiFanControl msi, Control control)
{
MsiFan fan = null;
if (control.Sensor == _fan1)
fan = msi.Fan1;
else if (control.Sensor == _fan2)
fan = msi.Fan2;
else if (control.Sensor == _fan3)
fan = msi.Fan3;
else if (control.Sensor == _fan4)
fan = msi.Fan4;
else if (control.Sensor == _fan5)
fan = msi.Fan5;
return fan;
}
private bool GetCoolerStatus(MsiFanControl msi)
{
var buffer = GetBuffer();
buffer[0] = 0xD0;
buffer[1] = 0x31;
var data = GetData(buffer);
if (data == null || data[1] != buffer[1])
{
return false;
}
msi.Fan1.Speed = GetInt16(data, 2);
msi.Fan2.Speed = GetInt16(data, 4);
msi.Fan3.Speed = GetInt16(data, 6);
msi.Fan4.Speed = GetInt16(data, 8);
msi.Fan5.Speed = GetInt16(data, 10);
msi.TemperatureInlet = data[12];
msi.TemperatureOutlet = data[14];
msi.TemperatureSensor1 = GetInt16(data, 16);
msi.TemperatureSensor2 = GetInt16(data, 18);
msi.Fan1.Duty = GetInt16(data, 22);
msi.Fan2.Duty = GetInt16(data, 24);
msi.Fan3.Duty = GetInt16(data, 26);
msi.Fan4.Duty = GetInt16(data, 28);
msi.Fan5.Duty = GetInt16(data, 30);
return true;
}
private bool GetFanConfigure(MsiFanControl msi)
{
var buffer = GetBuffer();
buffer[0] = 0xD0;
buffer[1] = 0x32;
var data = GetData(buffer);
if (data == null || data[1] != buffer[1])
{
return false;
}
int startIndex = 2;
msi.Fan1.ConfigureDuty = BytesToStruct<MsiFanConfigure>(data, startIndex);
msi.Fan2.ConfigureDuty = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
msi.Fan3.ConfigureDuty = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
msi.Fan4.ConfigureDuty = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
msi.Fan5.ConfigureDuty = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
return true;
}
private bool GetFanTemperatureConfigure(MsiFanControl msi)
{
var buffer = GetBuffer();
buffer[0] = 0xD0;
buffer[1] = 0x33;
var data = GetData(buffer);
if (data == null || (data[1] != buffer[1] && data[1] != 0x32))
{
return false;
}
int startIndex = 2;
msi.Fan1.ConfigureTemp = BytesToStruct<MsiFanConfigure>(data, startIndex);
msi.Fan2.ConfigureTemp = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
msi.Fan3.ConfigureTemp = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
msi.Fan4.ConfigureTemp = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
msi.Fan5.ConfigureTemp = BytesToStruct<MsiFanConfigure>(data, startIndex += Marshal.SizeOf<MsiFanConfigure>());
return true;
}
private void SetFanConfigure(MsiFanControl msi)
{
var buffer = GetBuffer();
buffer[0] = 0xD0;
buffer[1] = 0x40;
int startIndex = 2;
StructToBytes(msi.Fan1.ConfigureDuty, buffer, startIndex);
StructToBytes(msi.Fan2.ConfigureDuty, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
StructToBytes(msi.Fan3.ConfigureDuty, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
StructToBytes(msi.Fan4.ConfigureDuty, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
StructToBytes(msi.Fan5.ConfigureDuty, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
SetData(buffer);
}
private void SetFanTemperatureConfigure(MsiFanControl msi)
{
var buffer = GetBuffer();
buffer[0] = 0xD0;
buffer[1] = 0x41;
int startIndex = 2;
StructToBytes(msi.Fan1.ConfigureTemp, buffer, startIndex);
StructToBytes(msi.Fan2.ConfigureTemp, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
StructToBytes(msi.Fan3.ConfigureTemp, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
StructToBytes(msi.Fan4.ConfigureTemp, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
StructToBytes(msi.Fan5.ConfigureTemp, buffer, startIndex += Marshal.SizeOf<MsiFanConfigure>());
SetData(buffer);
}
private static T BytesToStruct<T>(byte[] data, int startIndex) where T : struct
{
int size = Marshal.SizeOf<T>();
if (startIndex < 0 || startIndex + size > data.Length)
{
throw new ArgumentOutOfRangeException(nameof(startIndex));
}
var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject() + startIndex;
return Marshal.PtrToStructure<T>(ptr);
}
finally
{
handle.Free();
}
}
private static void StructToBytes<T>(in T value, byte[] buffer, int startIndex) where T : struct
{
int size = Marshal.SizeOf<T>();
if (startIndex < 0 || startIndex + size > buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(startIndex));
}
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject() + startIndex;
Marshal.StructureToPtr(value, ptr, false);
}
finally
{
handle.Free();
}
}
private byte[] GetData(byte[] inbuf)
{
HidStream stream = null;
if (!Mutexes.WaitUsbSensors(MutexTimeout))
{
return null;
}
try
{
var outBuf = new byte[DataLength];
if (!_hidDevice.TryOpen(out stream))
{
return null;
}
stream.Write(inbuf);
Thread.Sleep(10); //Msi is using that interval
var readResult = stream.Read(outBuf);
if (readResult <= 0)
{
return null;
}
return outBuf;
}
finally
{
stream?.Close();
stream?.Dispose();
Mutexes.ReleaseUsbSensors();
}
}
private void SetData(byte[] buf)
{
HidStream stream = null;
if (!Mutexes.WaitUsbSensors(MutexTimeout))
{
return;
}
try
{
if (!_hidDevice.TryOpen(out stream))
{
return;
}
stream.Write(buf);
}
finally
{
stream?.Close();
stream?.Dispose();
Mutexes.ReleaseUsbSensors();
}
}
private byte[] GetBuffer()
{
return new byte[DataLength];
}
private static int GetInt16(byte[] buffer, int offset)
{
return buffer[offset] + (buffer[offset + 1] << 8);
}
}
@@ -0,0 +1,80 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
public enum MsiDeviceType
{
S280,
S360,
S360MEG,
X360,
X240,
D360,
D240,
}
public class MsiDevice
{
public MsiDevice(MsiDeviceType msiDeviceType, int vendorId, int productId, int productIdController)
{
DeviceType = msiDeviceType;
VendorId = vendorId;
ProductId = productId;
ProductIdController = productIdController;
}
public MsiDeviceType DeviceType { get; }
public int VendorId { get; }
public int ProductId { get; }
public int ProductIdController { get; }
public string Name
{
get
{
switch (DeviceType)
{
case MsiDeviceType.S280:
return "MSI CoreLiquid S280";
case MsiDeviceType.S360:
return "MSI CoreLiquid S360";
case MsiDeviceType.S360MEG:
return "MSI CoreLiquid S360 MEG";
case MsiDeviceType.X360:
return "MSI CoreLiquid X360";
case MsiDeviceType.X240:
return "MSI CoreLiquid X240";
case MsiDeviceType.D360:
return "MSI CoreLiquid D360";
case MsiDeviceType.D240:
return "MSI CoreLiquid D240";
default:
return "Other";
}
}
}
//Relevant for further HWMonitoring later
public bool SupportsHWMonitorIndex13and14(uint firmwareVersion)
{
switch (DeviceType)
{
case MsiDeviceType.S280:
case MsiDeviceType.S360:
return (firmwareVersion & byte.MaxValue) >= 10;
case MsiDeviceType.S360MEG:
return (firmwareVersion & byte.MaxValue) >= 7;
case MsiDeviceType.X360:
case MsiDeviceType.X240:
return (firmwareVersion & byte.MaxValue) >= 3;
case MsiDeviceType.D360:
case MsiDeviceType.D240:
return true;
}
return false;
}
}
@@ -0,0 +1,48 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
using System;
using System.Runtime.InteropServices;
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
public class MsiFan
{
/// <summary>
/// Speed of Fan in RPM.
/// </summary>
public int Speed { get; set; }
public int Duty { get; set; }
/// <summary>
/// Speed of Fan in percentage 0-100. This can e.g. be used to set fan curve when <see cref="MsiFanConfigure.Mode"/> is <see cref="MsiFanMode.Custom"/>.
/// </summary>
public MsiFanConfigure ConfigureDuty;
/// <summary>
/// Temperature of Fan in degrees Celsius. This can e.g. be used to set fan curve when <see cref="MsiFanConfigure.Mode"/> is <see cref="MsiFanMode.Custom"/>.
/// </summary>
public MsiFanConfigure ConfigureTemp;
}
public struct MsiFanConfigure
{
public MsiFanConfigure()
{
if (Marshal.SizeOf<MsiFanConfigure>() != 8)
{
throw new InvalidOperationException($"{nameof(MsiFanConfigure)} struct size is invalid.");
}
}
public MsiFanMode Mode = MsiFanMode.Unknown;
public byte Item0;
public byte Item1;
public byte Item2;
public byte Item3;
public byte Item4;
public byte Item5;
public byte Item6;
}
@@ -0,0 +1,21 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
public class MsiFanControl
{
public MsiFan Fan1 { get; set; } = new();
public MsiFan Fan2 { get; set; } = new();
public MsiFan Fan3 { get; set; } = new();
public MsiFan Fan4 { get; set; } = new();
public MsiFan Fan5 { get; set; } = new();
public int TemperatureInlet { get; set; }
public int TemperatureOutlet { get; set; }
public int TemperatureSensor1 { get; set; }
public int TemperatureSensor2 { get; set; }
}
@@ -0,0 +1,16 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
public enum MsiFanMode : byte
{
Silent = 0,
Bios = 1,
Game = 2,
Custom = 3,
Unknown = 4,
Smart = 5,
}
@@ -0,0 +1,70 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using HidSharp;
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
internal class MsiGroup : IGroup
{
private readonly List<IHardware> _hardware = new();
private readonly StringBuilder _report = new();
public MsiGroup(ISettings settings)
{
_report.AppendLine("MSI Hardware:");
_report.AppendLine();
//Get all HID devices
var hidDevices = DeviceList.Local.GetHidDevices();
foreach (var hidDevice in hidDevices)
{
//Check if the device is in the supported devices list
var found = MsiConstants.SupportedDevices.FirstOrDefault(md => md.VendorId == hidDevice.VendorID && md.ProductIdController == hidDevice.ProductID);
if (found == null)
{
continue;
}
try
{
var coreLiquidController = new MsiCoreLiquidController(found, hidDevice, settings);
_hardware.Add(coreLiquidController);
_report.AppendLine($"MSI Controller for '{hidDevice.GetProductName()}' ({hidDevice.VendorID:X4}:{hidDevice.ProductID:X4}) initialized successfully");
}
catch (Exception e)
{
_report.AppendLine($"Msi Controller Plugin initialization failed: {e.Message}");
}
}
}
public IReadOnlyList<IHardware> Hardware => _hardware;
public void Close()
{
foreach (Hardware hw in _hardware.OfType<Hardware>())
{
hw.Close();
}
}
public string GetReport()
{
var sb = new StringBuilder();
sb.AppendLine(_report.ToString());
_hardware.ForEach(hw => sb.AppendLine(hw.GetReport()));
return sb.ToString();
}
}
@@ -0,0 +1,26 @@
// 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) Florian K. (Blacktempel)
// All Rights Reserved.
namespace LibreHardwareMonitor.Hardware.Controller.MSI;
internal delegate float GetMsiSensorValue(MsiFanControl msi);
internal class MsiSensor : Sensor
{
private readonly GetMsiSensorValue _getValue;
public MsiSensor(string name, int index, SensorType sensorType, Hardware hardware, ISettings settings, GetMsiSensorValue getValue)
: base(name, index, sensorType, hardware, settings)
{
_getValue = getValue;
}
internal void Update(MsiFanControl msi)
{
float value = _getValue(msi);
Value = value;
}
}