// 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), 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; } } }