Skip to content

Commit f498ecb

Browse files
committed
Feature/arctic fan controller
* Fixed misspelling and added improvements - Fixed thread not starting (added _thread.Start()) - Added control sensor activation - Added PWM initialization from device - Improved PWM value reading for fast updates
1 parent 22be9f5 commit f498ecb

File tree

4 files changed

+494
-0
lines changed

4 files changed

+494
-0
lines changed

OpenHardwareMonitorLib/Hardware/Computer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using OpenHardwareMonitor.Hardware.Controller.AeroCool;
88
using OpenHardwareMonitor.Hardware.Controller.AquaComputer;
99
using OpenHardwareMonitor.Hardware.Controller.Heatmaster;
10+
using OpenHardwareMonitor.Hardware.Controller.Arctic;
1011
using OpenHardwareMonitor.Hardware.Controller.Nzxt;
1112
using OpenHardwareMonitor.Hardware.Controller.Razer;
1213
using OpenHardwareMonitor.Hardware.Controller.TBalancer;
@@ -121,6 +122,7 @@ public bool IsControllerEnabled
121122
Add(new AeroCoolGroup(_settings));
122123
Add(new NzxtGroup(_settings));
123124
Add(new RazerGroup(_settings));
125+
Add(new ArcticGroup(_settings));
124126
}
125127
else
126128
{
@@ -130,6 +132,7 @@ public bool IsControllerEnabled
130132
RemoveType<AeroCoolGroup>();
131133
RemoveType<NzxtGroup>();
132134
RemoveType<RazerGroup>();
135+
RemoveType<ArcticGroup>();
133136
}
134137
}
135138

@@ -541,6 +544,7 @@ private void AddGroups()
541544
Add(new AeroCoolGroup(_settings));
542545
Add(new NzxtGroup(_settings));
543546
Add(new RazerGroup(_settings));
547+
Add(new ArcticGroup(_settings));
544548
}
545549

546550
if (_storageEnabled)
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading;
4+
using HidSharp;
5+
6+
namespace OpenHardwareMonitor.Hardware.Controller.Arctic;
7+
8+
internal class ArcticFanController : Hardware
9+
{
10+
private const int CHANNEL_COUNT = 10;
11+
private const int PACKET_SIZE = 32;
12+
private const int CONTROL_VALUE_MIN = 0;
13+
private const int CONTROL_VALUE_MAX = 100;
14+
private const int CONTROL_RESET_VALUE = 30;
15+
private const int TIMEOUT_MS = 1000;
16+
17+
private HidStream _hidStream;
18+
private readonly object _hidLock = new();
19+
private readonly object _controlLock = new();
20+
private readonly Thread _thread;
21+
private readonly float[] _requestedFanSpeedsPercent = new float[CHANNEL_COUNT];
22+
private readonly float[] _currentFanRpms = new float[CHANNEL_COUNT];
23+
private readonly float[] _currentDevicePwmValues = new float[CHANNEL_COUNT]; // Current PWM values from device
24+
private bool _sendPwmRequested;
25+
private bool _pwmValuesInitialized = false; // Track if we've read initial PWM values from device
26+
private readonly System.Collections.Generic.List<Sensor> _rpmSensors = new();
27+
private readonly System.Collections.Generic.List<Sensor> _controlSensors = new();
28+
29+
public ArcticFanController(HidDevice dev, ISettings settings) : base("Arctic Fan Controller", new Identifier(dev), settings)
30+
{
31+
if (dev.TryOpen(out HidStream hidStream))
32+
{
33+
// Create fan sensors (RPM monitoring) - all 10 fans have RPM feedback
34+
for (int i = 1; i <= CHANNEL_COUNT; i++)
35+
{
36+
var fanSensor = new Sensor($"Arctic Controller Fan {i}", i, SensorType.Fan, this, settings);
37+
ActivateSensor(fanSensor);
38+
_rpmSensors.Add(fanSensor);
39+
}
40+
41+
// Create control sensors - all 10 fans can be controlled
42+
for (int i = 1; i <= CHANNEL_COUNT; i++)
43+
{
44+
var controlSensor = new Sensor($"Arctic Controller Fan Control {i}", i, SensorType.Control, this, settings);
45+
Control control = new(controlSensor, settings, CONTROL_VALUE_MIN, CONTROL_VALUE_MAX);
46+
control.ControlModeChanged += Control_ControlModeChanged;
47+
control.SoftwareControlValueChanged += Control_SoftwareControlValueChanged;
48+
49+
controlSensor.Control = control;
50+
ActivateSensor(controlSensor); // Activate the control sensor so it appears in the UI
51+
_controlSensors.Add(controlSensor);
52+
}
53+
54+
_hidStream = hidStream;
55+
_hidStream.ReadTimeout = TIMEOUT_MS;
56+
_hidStream.WriteTimeout = TIMEOUT_MS;
57+
58+
// create thread
59+
_thread = new Thread(ThreadHidLoop);
60+
_thread.IsBackground = true; // Allow app to close even if thread is running
61+
_thread.Start(); // Start the thread to read RPM data
62+
}
63+
}
64+
65+
private void ThreadHidLoop()
66+
{
67+
while (_hidStream != null)
68+
{
69+
lock (_hidLock)
70+
{
71+
if (_hidStream == null) return;
72+
73+
UpdateRpmAndPwmValues();
74+
SendPWMUpdateIfRequired();
75+
}
76+
77+
Thread.Sleep(500);
78+
}
79+
}
80+
81+
private void Control_SoftwareControlValueChanged(Control control)
82+
{
83+
// need PWM update
84+
lock (_controlLock)
85+
{
86+
var value = control.ControlMode switch
87+
{
88+
ControlMode.Software => Math.Max(Math.Min(control.SoftwareValue, CONTROL_VALUE_MAX), CONTROL_VALUE_MIN),
89+
_ => CONTROL_RESET_VALUE,
90+
};
91+
92+
_requestedFanSpeedsPercent[control.Sensor.Index - 1] = value;
93+
_sendPwmRequested = true;
94+
}
95+
96+
// update the sensor value
97+
if (control.Sensor is Sensor sensor)
98+
sensor.Value = control.ControlMode == ControlMode.Software ? control.SoftwareValue : null;
99+
}
100+
101+
private void Control_ControlModeChanged(Control control)
102+
{
103+
Control_SoftwareControlValueChanged(control);
104+
}
105+
106+
public override HardwareType HardwareType { get; } = HardwareType.Cooler;
107+
108+
public override void Update()
109+
{
110+
lock (_hidLock)
111+
{
112+
foreach (Sensor sensor in _rpmSensors)
113+
{
114+
sensor.Value = GetRPM(sensor.Index);
115+
}
116+
117+
// Update control sensor values to reflect current device PWM values
118+
// This ensures the UI shows the actual current values from the device quickly
119+
foreach (Sensor sensor in _controlSensors)
120+
{
121+
int idx = sensor.Index - 1;
122+
if (idx >= 0 && idx < CHANNEL_COUNT &&
123+
// Only update if sensor doesn't have a manual value set (to avoid overwriting user input)
124+
sensor.Control?.ControlMode != ControlMode.Software)
125+
{
126+
// Use current device PWM values for immediate display, fallback to requested if not initialized yet
127+
float displayValue = _pwmValuesInitialized ? _currentDevicePwmValues[idx] : _requestedFanSpeedsPercent[idx];
128+
sensor.Value = displayValue;
129+
}
130+
131+
}
132+
}
133+
}
134+
135+
public override void Close()
136+
{
137+
lock (_hidLock)
138+
{
139+
try
140+
{
141+
lock (_controlLock)
142+
{
143+
// Set all fans to 30% before closing (like JS does)
144+
for (int i = 0; i < CHANNEL_COUNT; i++)
145+
{
146+
_requestedFanSpeedsPercent[i] = CONTROL_RESET_VALUE;
147+
}
148+
149+
_sendPwmRequested = true;
150+
}
151+
152+
SendPWMUpdateIfRequired();
153+
154+
}
155+
catch { }
156+
157+
try
158+
{
159+
_hidStream?.Close();
160+
_hidStream?.Dispose();
161+
162+
}
163+
catch { }
164+
// make sure stream is null so the thread can exit
165+
finally
166+
{
167+
_hidStream = null;
168+
}
169+
}
170+
171+
// wait for thread to finish
172+
_thread?.Join(1000);
173+
174+
base.Close();
175+
}
176+
177+
private void UpdateRpmAndPwmValues()
178+
{
179+
if (_hidStream is null)
180+
{
181+
return;
182+
}
183+
184+
if (_hidStream is null)
185+
{
186+
return;
187+
}
188+
189+
try
190+
{
191+
// Try to read available data (device sends periodically)
192+
byte[] response = null;
193+
int attempts = 0;
194+
const int maxAttempts = 3;
195+
196+
while (attempts < maxAttempts)
197+
{
198+
try
199+
{
200+
// Set short timeout to check if data is available
201+
var originalTimeout = _hidStream.ReadTimeout;
202+
_hidStream.ReadTimeout = 200;
203+
204+
response = new byte[PACKET_SIZE];
205+
int bytesRead = _hidStream.Read(response, 0, PACKET_SIZE);
206+
207+
_hidStream.ReadTimeout = originalTimeout;
208+
209+
if (bytesRead >= PACKET_SIZE && response[0] == 0x01)
210+
{
211+
// Valid response with Report ID 0x01
212+
break;
213+
}
214+
}
215+
catch (TimeoutException)
216+
{
217+
// No data available yet, continue trying
218+
}
219+
catch
220+
{
221+
// Retry on other errors
222+
}
223+
224+
attempts++;
225+
if (attempts < maxAttempts) Thread.Sleep(50);
226+
}
227+
228+
ProcessResponse(response);
229+
}
230+
catch (Exception ex)
231+
{
232+
System.Diagnostics.Debug.WriteLine($"RPM update failed: {ex.Message}");
233+
}
234+
}
235+
236+
private void ProcessResponse(byte[] response)
237+
{
238+
if (response == null || response.Length < PACKET_SIZE || response[0] != 0x01)
239+
{
240+
return;
241+
}
242+
243+
// Parse current PWM values from bytes 1-10 (sent by device)
244+
// Format: [Report ID=0x01, PWM[1-10] (bytes 1-10), RPM[1-10] (bytes 11-30, 2 bytes each), padding]
245+
for (int i = 0; i < CHANNEL_COUNT; i++)
246+
{
247+
if (1 + i < response.Length)
248+
{
249+
_currentDevicePwmValues[i] = response[1 + i];
250+
}
251+
}
252+
253+
// Initialize requested PWM values with current device values on first read
254+
// This prevents other fans from resetting to 0% when one fan is set to manual
255+
if (!_pwmValuesInitialized)
256+
{
257+
lock (_controlLock)
258+
{
259+
for (int i = 0; i < CHANNEL_COUNT; i++)
260+
{
261+
_requestedFanSpeedsPercent[i] = _currentDevicePwmValues[i];
262+
}
263+
_pwmValuesInitialized = true;
264+
}
265+
}
266+
267+
// Parse RPM values from bytes 11-30 (10 RPM values as uint16 little-endian)
268+
for (int i = 0; i < CHANNEL_COUNT; i++)
269+
{
270+
int rpmIndex = 11 + i * 2;
271+
if (rpmIndex + 1 < response.Length)
272+
{
273+
int rpmLow = response[rpmIndex];
274+
int rpmHigh = response[rpmIndex + 1];
275+
_currentFanRpms[i] = rpmLow | (rpmHigh << 8);
276+
}
277+
}
278+
}
279+
280+
private void SendPWMUpdateIfRequired()
281+
{
282+
float[] values = null;
283+
lock (_controlLock)
284+
{
285+
if (!_sendPwmRequested)
286+
{
287+
return;
288+
}
289+
290+
_sendPwmRequested = false;
291+
values = _requestedFanSpeedsPercent.ToArray();
292+
}
293+
294+
try
295+
{
296+
// New format: [Report ID=0x01, PWM[0-9] (10 bytes), padding (21 bytes)]
297+
byte[] pwmPacket = new byte[PACKET_SIZE];
298+
pwmPacket[0] = 0x01; // Report ID
299+
300+
// Set all fan speeds (bytes 1-10)
301+
for (int i = 0; i < CHANNEL_COUNT; i++)
302+
{
303+
pwmPacket[1 + i] = (byte)Math.Round(values[i]);
304+
}
305+
// Rest are zeros (already initialized)
306+
307+
_hidStream.Write(pwmPacket);
308+
}
309+
catch (Exception ex)
310+
{
311+
System.Diagnostics.Debug.WriteLine($"PWM update failed: {ex.Message}");
312+
}
313+
}
314+
315+
private float GetRPM(int fanIndex)
316+
{
317+
if (fanIndex < 1 || fanIndex > CHANNEL_COUNT)
318+
{
319+
return 0;
320+
}
321+
322+
return _currentFanRpms[fanIndex - 1];
323+
}
324+
}

0 commit comments

Comments
 (0)