diff --git a/LibreHardwareMonitorLib/Hardware/Computer.cs b/LibreHardwareMonitorLib/Hardware/Computer.cs index 8c3ef79..df0a803 100644 --- a/LibreHardwareMonitorLib/Hardware/Computer.cs +++ b/LibreHardwareMonitorLib/Hardware/Computer.cs @@ -14,6 +14,7 @@ using LibreHardwareMonitor.Hardware.Controller.AeroCool; using LibreHardwareMonitor.Hardware.Controller.AquaComputer; using LibreHardwareMonitor.Hardware.Controller.Heatmaster; using LibreHardwareMonitor.Hardware.Controller.Nzxt; +using LibreHardwareMonitor.Hardware.Controller.Razer; using LibreHardwareMonitor.Hardware.Controller.TBalancer; using LibreHardwareMonitor.Hardware.Cpu; using LibreHardwareMonitor.Hardware.Gpu; @@ -123,6 +124,7 @@ public class Computer : IComputer Add(new AquaComputerGroup(_settings)); Add(new AeroCoolGroup(_settings)); Add(new NzxtGroup(_settings)); + Add(new RazerGroup(_settings)); } else { @@ -131,6 +133,7 @@ public class Computer : IComputer RemoveType(); RemoveType(); RemoveType(); + RemoveType(); } } @@ -520,6 +523,7 @@ public class Computer : IComputer Add(new AquaComputerGroup(_settings)); Add(new AeroCoolGroup(_settings)); Add(new NzxtGroup(_settings)); + Add(new RazerGroup(_settings)); } if (_storageEnabled) diff --git a/LibreHardwareMonitorLib/Hardware/Controller/Razer/RazerFanController.cs b/LibreHardwareMonitorLib/Hardware/Controller/Razer/RazerFanController.cs new file mode 100644 index 0000000..01d3302 --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/Controller/Razer/RazerFanController.cs @@ -0,0 +1,408 @@ +// 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. +// All Rights Reserved. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using HidSharp; + +namespace LibreHardwareMonitor.Hardware.Controller.Razer; + +internal sealed class RazerFanController : Hardware +{ + private const int DEFAULT_SPEED_CHANNEL_POWER = 50; + private const byte PERCENT_MIN = 0; + private const byte PERCENT_MAX = 100; + private const int DEVICE_READ_DELAY_MS = 5; + private const int DEVICE_READ_TIMEOUT_MS = 500; + private const int CHANNEL_COUNT = 8; + //private const int FORCE_WRITE_SPEEDS_INTERVAL_MS = 2500; // TODO: Add timer + + private HidStream _stream; + private readonly HidDevice _device; + private readonly SequenceCounter _sequenceCounter = new(); + + private readonly float?[] _pwm = new float?[CHANNEL_COUNT]; + private readonly Sensor[] _pwmControls = new Sensor[CHANNEL_COUNT]; + private readonly Sensor[] _rpmSensors = new Sensor[CHANNEL_COUNT]; + + public RazerFanController(HidDevice dev, ISettings settings) : base("Razer PWM PC Fan Controller", new Identifier(dev.DevicePath), settings) + { + _device = dev; + + if (_device.TryOpen(out _stream)) + { + _stream.ReadTimeout = 5000; + + Packet packet = new Packet + { + SequenceNumber = _sequenceCounter.Next(), + DataLength = 0, + CommandClass = CommandClass.Info, + Command = 0x87, + }; + + if (Mutexes.WaitRazer(250)) + { + while (FirmwareVersion == null) + { + Thread.Sleep(DEVICE_READ_DELAY_MS); + + try + { + Packet response = TryWriteAndRead(packet); + FirmwareVersion = $"{response.Data[0]:D}.{response.Data[1]:D2}.{response.Data[2]:D2}"; + } + catch { } + } + + Mutexes.ReleaseRazer(); + } + + Name = "Razer PWM PC Fan Controller"; + + for (int i = 0; i < CHANNEL_COUNT; i++) + { + // Fan Control + _pwmControls[i] = new Sensor("Fan Control #" + (i + 1), i, SensorType.Control, this, Array.Empty(), settings); + Control fanControl = new(_pwmControls[i], settings, PERCENT_MIN, PERCENT_MAX); + _pwmControls[i].Control = fanControl; + fanControl.ControlModeChanged += FanSoftwareControlValueChanged; + fanControl.SoftwareControlValueChanged += FanSoftwareControlValueChanged; + //fanControl.SetDefault(); + FanSoftwareControlValueChanged(fanControl); + ActivateSensor(_pwmControls[i]); + + // Fan RPM + _rpmSensors[i] = new Sensor("Fan #" + (i + 1), i, SensorType.Fan, this, Array.Empty(), settings); + ActivateSensor(_rpmSensors[i]); + } + } + } + + public string FirmwareVersion { get; } + + public override HardwareType HardwareType => HardwareType.Cooler; + + public string Status => FirmwareVersion != "1.01.00" ? $"Status: Untested Firmware Version {FirmwareVersion}! Please consider Updating to Version 1.01.00" : "Status: OK"; + + private void FanSoftwareControlValueChanged(Control control) // TODO: Add timer here + { + if (control.ControlMode == ControlMode.Undefined || !Mutexes.WaitRazer(250)) + return; + + if (control.ControlMode == ControlMode.Software) + { + SetChannelModeToManual(control.Sensor.Index); + + float value = control.SoftwareValue; + byte fanSpeed = (byte)(value > 100 ? 100 : value < 0 ? 0 : value); + + var packet = new Packet + { + SequenceNumber = _sequenceCounter.Next(), + DataLength = 3, + CommandClass = CommandClass.Pwm, + Command = PwmCommand.SetChannelPercent, + }; + + packet.Data[0] = 0x01; + packet.Data[1] = (byte)(0x05 + control.Sensor.Index); + packet.Data[2] = fanSpeed; + + TryWriteAndRead(packet); + + _pwm[control.Sensor.Index] = value; + } + else if (control.ControlMode == ControlMode.Default) + { + SetChannelModeToManual(control.Sensor.Index); // TODO: switch to auto mode here if it enabled before + + var packet = new Packet + { + SequenceNumber = _sequenceCounter.Next(), + DataLength = 3, + CommandClass = CommandClass.Pwm, + Command = PwmCommand.SetChannelPercent, + }; + + packet.Data[0] = 0x01; + packet.Data[1] = (byte)(0x05 + control.Sensor.Index); + packet.Data[2] = DEFAULT_SPEED_CHANNEL_POWER; + + TryWriteAndRead(packet); + + _pwm[control.Sensor.Index] = DEFAULT_SPEED_CHANNEL_POWER; + } + + Mutexes.ReleaseRazer(); + } + + private int GetChannelSpeed(int channel) + { + Packet packet = new Packet + { + SequenceNumber = _sequenceCounter.Next(), + DataLength = 6, + CommandClass = CommandClass.Pwm, + Command = PwmCommand.GetChannelSpeed, + }; + + packet.Data[0] = 0x01; + packet.Data[1] = (byte)(0x05 + channel); + + Packet response = TryWriteAndRead(packet); + return (response.Data[4] << 8) | response.Data[5]; + } + + private void SetChannelModeToManual(int channel) + { + Packet packet = new Packet + { + SequenceNumber = _sequenceCounter.Next(), + DataLength = 3, + CommandClass = CommandClass.Pwm, + Command = PwmCommand.SetChannelMode, + }; + + packet.Data[0] = 0x01; + packet.Data[1] = (byte)(0x05 + channel); + packet.Data[2] = 0x04; + + TryWriteAndRead(packet); + } + + private void ThrowIfNotReady() + { + bool @throw; + try + { + @throw = _stream is null; + } + catch (ObjectDisposedException) + { + @throw = true; + } + + if (@throw) + { + throw new InvalidOperationException("The device is not ready."); + } + } + + private Packet TryWriteAndRead(Packet packet) + { + Packet readPacket = null; + int devTimeout = 400; + int devReconnectTimeout = 3000; + + do + { + try + { + byte[] response = Packet.CreateBuffer(); + byte[] buffer = packet.ToBuffer(); + + ThrowIfNotReady(); + _stream?.SetFeature(buffer, 0, buffer.Length); + Thread.Sleep(DEVICE_READ_DELAY_MS); + ThrowIfNotReady(); + _stream?.GetFeature(response, 0, response.Length); + readPacket = Packet.FromBuffer(response); + + if (readPacket.Status == DeviceStatus.Busy) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + while (stopwatch.ElapsedMilliseconds < DEVICE_READ_TIMEOUT_MS && readPacket.Status == DeviceStatus.Busy) + { + Thread.Sleep(DEVICE_READ_DELAY_MS); + ThrowIfNotReady(); + _stream?.GetFeature(response, 0, response.Length); + readPacket = Packet.FromBuffer(response); + } + } + } + catch (IOException) // Unexpected device disconnect or fan plug/unplug + { + if (devTimeout <= 0) + { + while (devReconnectTimeout > 0) + { + _stream?.Close(); + if (_device.TryOpen(out _stream)) + break; + + Thread.Sleep(1000); + devReconnectTimeout -= 500; + } + + if (devReconnectTimeout <= 0) // Device disconnected + { + for (int i = 0; i < CHANNEL_COUNT; i++) + { + _pwmControls[i].Control = null; + _pwmControls[i].Value = null; + _rpmSensors[i].Value = null; + _pwm[i] = null; + + DeactivateSensor(_pwmControls[i]); + DeactivateSensor(_rpmSensors[i]); + } + + Close(); + + Packet ret = new Packet(); + for (int i = 0; i < 80; i++) + ret.Data[i] = 0; + return ret; + } + + devTimeout = 400; + } + + Thread.Sleep(DEVICE_READ_DELAY_MS); + devTimeout -= DEVICE_READ_DELAY_MS; + } + } while (readPacket == null); + + return readPacket; + } + + public override void Close() + { + base.Close(); + _stream?.Close(); + } + + public override void Update() + { + if (!Mutexes.WaitRazer(250)) + return; + + for (int i = 0; i < CHANNEL_COUNT; i++) + { + _rpmSensors[i].Value = GetChannelSpeed(i); + _pwmControls[i].Value = _pwm[i]; + } + + Mutexes.ReleaseRazer(); + } + + private enum DeviceStatus : byte + { + Default = 0x00, + Busy = 0x01, + Success = 0x02, + Error = 0x03, + Timeout = 0x04, + Invalid = 0x05, + } + + private enum ProtocolType : byte + { + Default = 0x00, + } + + private static class CommandClass + { + public static readonly byte Info = 0x00; + public static readonly byte Pwm = 0x0d; + } + + private static class PwmCommand + { + public static readonly byte SetChannelPercent = 0x0d; + public static readonly byte SetChannelMode = 0x02; + public static readonly byte GetChannelSpeed = 0x81; + } + + private sealed class Packet + { + public byte ReportId { get; set; } + public DeviceStatus Status { get; set; } + public byte SequenceNumber { get; set; } + public short RemainingCount { get; set; } + public ProtocolType ProtocolType { get; set; } + public byte DataLength { get; set; } + public byte CommandClass { get; set; } + public byte Command { get; set; } + public byte[] Data { get; } = new byte[80]; + public byte CRC { get; set; } + public byte Reserved { get; set; } + + public byte[] ToBuffer() + { + byte[] buffer = CreateBuffer(); + buffer[0] = ReportId; + buffer[1] = (byte)Status; + buffer[2] = SequenceNumber; + buffer[3] = (byte)((RemainingCount >> 8) & 0xff); + buffer[4] = (byte)(RemainingCount & 0xff); + buffer[5] = (byte)ProtocolType; + buffer[6] = DataLength; + buffer[7] = CommandClass; + buffer[8] = Command; + + for (int i = 0; i < Data.Length; i++) + buffer[9 + i] = Data[i]; + + buffer[89] = GenerateChecksum(buffer); + buffer[90] = Reserved; + return buffer; + } + + public static Packet FromBuffer(byte[] buffer) + { + var packet = new Packet + { + ReportId = buffer[0], + Status = (DeviceStatus)buffer[1], + SequenceNumber = buffer[2], + RemainingCount = (short)((buffer[3] << 8) | buffer[4]), + ProtocolType = (ProtocolType)buffer[5], + DataLength = buffer[6], + CommandClass = buffer[7], + Command = buffer[8], + CRC = buffer[89], + Reserved = buffer[90] + }; + + for (int i = 0; i < packet.Data.Length; i++) + packet.Data[i] = buffer[9 + i]; + + return packet; + } + + public static byte[] CreateBuffer() => new byte[91]; + + internal static byte GenerateChecksum(byte[] buffer) + { + byte result = 0; + for (int i = 3; i < 89; i++) + { + result = (byte)(result ^ buffer[i]); + } + return result; + } + } + + private sealed class SequenceCounter + { + private byte _sequenceId = 0x00; + + public byte Next() + { + while (_sequenceId == 0x00) + { + _sequenceId += 0x08; + } + + return _sequenceId; + } + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Controller/Razer/RazerGroup.cs b/LibreHardwareMonitorLib/Hardware/Controller/Razer/RazerGroup.cs new file mode 100644 index 0000000..f81fc44 --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/Controller/Razer/RazerGroup.cs @@ -0,0 +1,69 @@ +// 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. +// All Rights Reserved. + +using System.Collections.Generic; +using System.Text; +using HidSharp; + +namespace LibreHardwareMonitor.Hardware.Controller.Razer; + +internal class RazerGroup : IGroup +{ + private readonly List _hardware = new(); + private readonly StringBuilder _report = new(); + + public RazerGroup(ISettings settings) + { + _report.AppendLine("Razer Hardware"); + _report.AppendLine(); + + foreach (HidDevice dev in DeviceList.Local.GetHidDevices(0x1532)) + { + string productName = dev.GetProductName(); + + switch (dev.ProductID) + { + case 0x0F3C: // Razer PWM PC fan controller + if (dev.GetMaxFeatureReportLength() <= 0) + break; + + var device = new RazerFanController(dev, settings); + _report.AppendLine($"Device name: {productName}"); + _report.AppendLine($"Firmware version: {device.FirmwareVersion}"); + _report.AppendLine($"{device.Status}"); + _report.AppendLine(); + _hardware.Add(device); + break; + + default: + _report.AppendLine($"Unknown Hardware PID: {dev.ProductID} Name: {productName}"); + _report.AppendLine(); + break; + } + } + + if (_hardware.Count == 0) + { + _report.AppendLine("No Razer Hardware found."); + _report.AppendLine(); + } + } + + public IReadOnlyList Hardware => _hardware; + + public void Close() + { + foreach (IHardware iHardware in _hardware) + { + if (iHardware is Hardware hardware) + hardware.Close(); + } + } + + public string GetReport() + { + return _report.ToString(); + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Mutexes.cs b/LibreHardwareMonitorLib/Hardware/Mutexes.cs index 7f77537..ec42e16 100644 --- a/LibreHardwareMonitorLib/Hardware/Mutexes.cs +++ b/LibreHardwareMonitorLib/Hardware/Mutexes.cs @@ -8,6 +8,7 @@ internal static class Mutexes private static Mutex _ecMutex; private static Mutex _isaBusMutex; private static Mutex _pciBusMutex; + private static Mutex _razerMutex; /// /// Opens the mutexes. @@ -17,6 +18,7 @@ internal static class Mutexes _isaBusMutex = CreateOrOpenExistingMutex("Global\\Access_ISABUS.HTP.Method"); _pciBusMutex = CreateOrOpenExistingMutex("Global\\Access_PCI"); _ecMutex = CreateOrOpenExistingMutex("Global\\Access_EC"); + _razerMutex = CreateOrOpenExistingMutex("Global\\RazerReadWriteGuardMutex"); static Mutex CreateOrOpenExistingMutex(string name) { @@ -48,6 +50,7 @@ internal static class Mutexes _isaBusMutex?.Close(); _pciBusMutex?.Close(); _ecMutex?.Close(); + _razerMutex?.Close(); } public static bool WaitIsaBus(int millisecondsTimeout) @@ -80,6 +83,16 @@ internal static class Mutexes _ecMutex?.ReleaseMutex(); } + public static bool WaitRazer(int millisecondsTimeout) + { + return WaitMutex(_razerMutex, millisecondsTimeout); + } + + public static void ReleaseRazer() + { + _razerMutex?.ReleaseMutex(); + } + private static bool WaitMutex(Mutex mutex, int millisecondsTimeout) { if (mutex == null)