-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLinuxAsusWmi.cs
More file actions
1060 lines (926 loc) · 39.9 KB
/
LinuxAsusWmi.cs
File metadata and controls
1060 lines (926 loc) · 39.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
namespace GHelper.Linux.Platform.Linux;
/// <summary>
/// Linux implementation of IAsusWmi using the asus-wmi kernel module (sysfs).
/// Maps G-Helper's ATKACPI device IDs to Linux sysfs attributes.
///
/// Sysfs paths — resolved at runtime via SysfsHelper.ResolveAttrPath():
///
/// Legacy (kernel 6.2+ with CONFIG_ASUS_WMI_DEPRECATED_ATTRS=y):
/// /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy
/// /sys/devices/platform/asus-nb-wmi/panel_od
/// /sys/bus/platform/devices/asus-nb-wmi/dgpu_disable
/// /sys/bus/platform/devices/asus-nb-wmi/gpu_mux_mode
/// /sys/bus/platform/devices/asus-nb-wmi/mini_led_mode
/// /sys/devices/platform/asus-nb-wmi/ppt_*
/// /sys/devices/platform/asus-nb-wmi/nv_*
///
/// Firmware-attributes (kernel 6.8+ with asus_armoury module):
/// /sys/class/firmware-attributes/asus-armoury/attributes/{name}/current_value
///
/// Always at fixed paths:
/// /sys/class/hwmon/hwmon*/fan{1,2,3}_input
/// /sys/class/hwmon/hwmon*/pwm{1,2,3}_auto_point{1-8}_{temp,pwm}
/// /sys/class/power_supply/BAT0/charge_control_end_threshold
/// /sys/class/leds/asus::kbd_backlight/brightness
/// /sys/class/leds/asus::kbd_backlight/multi_intensity
/// </summary>
public class LinuxAsusWmi : IAsusWmi
{
private string? _asusFanRpmHwmonDir; // Hwmon with fan*_input files (RPM reading)
private string? _asusFanCurveHwmonDir; // Hwmon with pwm*_auto_point* files (fan curve control)
private string? _asusBaseHwmonDir; // Base ASUS hwmon (temps, etc.)
private string? _cpuTempHwmonDir; // CPU temperature hwmon (coretemp/k10temp)
private string? _batteryDir;
private Thread? _eventThread;
private volatile bool _eventListening;
private readonly List<FileStream> _eventStreams = new(); // Track open evdev streams for Dispose()
public event Action<int>? WmiEvent;
public LinuxAsusWmi()
{
// Discover hwmon devices — names vary by kernel version:
// Kernel <6.x: "asus_nb_wmi" (single hwmon for fans + temps + curves)
// Kernel 6.x+: "asus" (base, has fan*_input for RPM)
// "asus_custom_fan_curve" (has pwm*_auto_point* for curve control, NO fan RPM)
// "coretemp"/"k10temp" (CPU temp)
//
// Fan RPM: find hwmon that actually has fan1_input
_asusFanRpmHwmonDir = SysfsHelper.FindHwmonByNameWithFile("fan1_input",
"asus", "asus_nb_wmi", "asus_custom_fan_curve")
?? SysfsHelper.FindHwmonByName("asus_nb_wmi")
?? SysfsHelper.FindHwmonByName("asus");
// Fan curves: prefer asus_custom_fan_curve (has pwm*_auto_point*), fallback to RPM hwmon
_asusFanCurveHwmonDir = SysfsHelper.FindHwmonByName("asus_custom_fan_curve")
?? SysfsHelper.FindHwmonByName("asus_nb_wmi")
?? _asusFanRpmHwmonDir;
_asusBaseHwmonDir = SysfsHelper.FindHwmonByName("asus")
?? SysfsHelper.FindHwmonByName("asus_nb_wmi")
?? _asusFanRpmHwmonDir;
_cpuTempHwmonDir = SysfsHelper.FindHwmonByName("coretemp") // Intel
?? SysfsHelper.FindHwmonByName("k10temp"); // AMD
_batteryDir = SysfsHelper.FindBattery();
// Log discovery results
SysfsHelper.LogAllHwmon();
if (_asusFanRpmHwmonDir != null)
Helpers.Logger.WriteLine($"ASUS fan RPM hwmon: {_asusFanRpmHwmonDir}");
else
Helpers.Logger.WriteLine("WARNING: No hwmon with fan*_input found. Fan RPM unavailable.");
if (_asusFanCurveHwmonDir != null)
Helpers.Logger.WriteLine($"ASUS fan curve hwmon: {_asusFanCurveHwmonDir}");
else
Helpers.Logger.WriteLine("WARNING: ASUS fan curve hwmon not found. Fan curve features unavailable.");
if (_asusBaseHwmonDir != null)
Helpers.Logger.WriteLine($"ASUS base hwmon: {_asusBaseHwmonDir}");
if (_cpuTempHwmonDir != null)
Helpers.Logger.WriteLine($"CPU temp hwmon: {_cpuTempHwmonDir}");
if (_batteryDir != null)
Helpers.Logger.WriteLine($"Battery found: {_batteryDir}");
}
// ── Core ACPI-equivalent methods ──
public int DeviceGet(int deviceId)
{
// Map known device IDs to sysfs reads
// This is the translation layer: G-Helper device ID → Linux sysfs
return deviceId switch
{
0x00120075 => GetThrottleThermalPolicy(), // PerformanceMode
0x00120057 => GetBatteryChargeLimit(), // BatteryLimit
0x00050019 => GetPanelOverdrive() ? 1 : 0, // ScreenOverdrive
0x00090020 => GetGpuEco() ? 1 : 0, // GPUEcoROG
0x00090016 => GetGpuMuxMode(), // GPUMuxROG
0x0005001E => GetMiniLedMode(), // ScreenMiniled1
0x0005002E => GetMiniLedMode(), // ScreenMiniled2
0x00110013 => GetFanRpm(0), // CPU_Fan
0x00110014 => GetFanRpm(1), // GPU_Fan
0x00110031 => GetFanRpm(2), // Mid_Fan
0x00120094 => GetCpuTemp(), // Temp_CPU
0x00120097 => GetGpuTemp(), // Temp_GPU
0x00050021 => GetKeyboardBrightness(), // TUF_KB_BRIGHTNESS
_ => -1 // Unsupported device ID
};
}
public int DeviceSet(int deviceId, int value)
{
return deviceId switch
{
0x00120075 => SetAndReturn(() => SetThrottleThermalPolicy(value)),
0x00120057 => SetAndReturn(() => SetBatteryChargeLimit(value)),
0x00050019 => SetAndReturn(() => SetPanelOverdrive(value != 0)),
// GPU mode changes MUST go through GpuModeController — direct writes to
// dgpu_disable cause kernel panics if the NVIDIA/AMD driver is active.
// 0x00090020 (GPUEco) and 0x00090016 (GPUMux) intentionally removed.
0x0005001E => SetAndReturn(() => SetMiniLedMode(value)),
0x0005002E => SetAndReturn(() => SetMiniLedMode(value)),
0x00050021 => SetAndReturn(() => SetKeyboardBrightness(value)),
_ => -1
};
}
public byte[]? DeviceGetBuffer(int deviceId, int args = 0)
{
// Fan curve buffer read
return deviceId switch
{
0x00110024 => GetFanCurve(0), // DevsCPUFanCurve
0x00110025 => GetFanCurve(1), // DevsGPUFanCurve
0x00110032 => GetFanCurve(2), // DevsMidFanCurve
_ => null
};
}
// ── Performance mode ──
public int GetThrottleThermalPolicy()
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.ThrottleThermalPolicy, SysfsHelper.AsusWmiPlatform);
if (path != null)
return SysfsHelper.ReadInt(path, -1);
// Fallback: derive from platform_profile if throttle_thermal_policy doesn't exist
var profile = SysfsHelper.ReadAttribute(SysfsHelper.PlatformProfile);
if (profile != null)
{
return profile switch
{
"balanced" => 0,
"performance" => 1,
"low-power" or "quiet" => 2,
_ => -1
};
}
return -1;
}
public void SetThrottleThermalPolicy(int mode)
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.ThrottleThermalPolicy, SysfsHelper.AsusWmiPlatform);
if (path != null)
{
SysfsHelper.WriteInt(path, mode);
}
// If throttle_thermal_policy doesn't exist, ModeControl still sets platform_profile directly
}
// ── Fan control ──
public int GetFanRpm(int fanIndex)
{
// Use the hwmon that has fan*_input files (RPM sensors)
var hwmon = _asusFanRpmHwmonDir ?? _asusBaseHwmonDir;
if (hwmon != null)
{
int rpm = SysfsHelper.ReadInt(
Path.Combine(hwmon, $"fan{fanIndex + 1}_input"), -1);
if (rpm > 0) return rpm;
}
// For GPU fan (index 1), try nvidia-smi as fallback
// nvidia-smi returns percentage, we return -2 to indicate "percentage mode"
if (fanIndex == 1)
{
try
{
var output = SysfsHelper.RunCommand("nvidia-smi", "--query-gpu=fan.speed --format=csv,noheader,nounits");
if (!string.IsNullOrWhiteSpace(output) && int.TryParse(output.Trim(), out int fanPercent) && fanPercent >= 0)
return -2 - fanPercent; // Encode: -2 means "percentage", value is -(2 + percent)
}
catch { /* nvidia-smi not available */ }
}
return -1;
}
/// <summary>
/// Get GPU fan speed as percentage from nvidia-smi (0-100).
/// Returns null if unavailable.
/// </summary>
public int? GetGpuFanPercent()
{
try
{
var output = SysfsHelper.RunCommand("nvidia-smi", "--query-gpu=fan.speed --format=csv,noheader,nounits");
if (!string.IsNullOrWhiteSpace(output) && int.TryParse(output.Trim(), out int fanPercent) && fanPercent >= 0)
return fanPercent;
}
catch { }
return null;
}
public byte[]? GetFanCurve(int fanIndex)
{
if (_asusFanCurveHwmonDir == null) return null;
var curve = new byte[16];
int pwmIndex = fanIndex + 1;
for (int i = 0; i < 8; i++)
{
// Temperature: asus_custom_fan_curve sysfs uses raw degrees (NOT millidegrees).
// The kernel stores temps directly from the ACPI buffer: data->temps[i] = buf[i]
int temp = SysfsHelper.ReadInt(
Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_auto_point{i + 1}_temp"), -1);
if (temp < 0) return null;
curve[i] = (byte)temp;
// PWM 0-255 → percentage 0-100
int pwm = SysfsHelper.ReadInt(
Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_auto_point{i + 1}_pwm"), -1);
if (pwm < 0) return null;
curve[8 + i] = (byte)(pwm * 100 / 255);
}
// The kernel's default fan curves for CPU/GPU often have all-zero temperatures
// but valid PWM duty cycles. This happens because the asus-wmi driver only
// populates temps when the user explicitly writes custom curves; the EC's
// built-in default temps are not exposed through sysfs.
// Synthesize a reasonable temperature ramp so the UI has usable data.
bool allTempsZero = true;
bool anyPwmNonZero = false;
for (int i = 0; i < 8; i++)
{
if (curve[i] > 0) allTempsZero = false;
if (curve[8 + i] > 0) anyPwmNonZero = true;
}
if (allTempsZero && anyPwmNonZero)
{
byte[] synthTemps = { 30, 40, 50, 60, 70, 80, 90, 100 };
for (int i = 0; i < 8; i++)
curve[i] = synthTemps[i];
Helpers.Logger.WriteLine($"Fan {fanIndex}: synthesized temp ramp for zero-temp kernel defaults");
}
return curve;
}
public void SetFanCurve(int fanIndex, byte[] curve)
{
if (_asusFanCurveHwmonDir == null || curve.Length != 16) return;
int pwmIndex = fanIndex + 1;
// Write all curve data points FIRST.
// Each sysfs write to auto_point files updates the in-kernel fan_curve_data struct
// and sets data->enabled = false (preventing premature writes to EC).
for (int i = 0; i < 8; i++)
{
// Temperature: asus_custom_fan_curve sysfs uses raw degrees (NOT millidegrees)
SysfsHelper.WriteInt(
Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_auto_point{i + 1}_temp"),
curve[i]);
// Percentage 0-100 → PWM 0-255
SysfsHelper.WriteInt(
Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_auto_point{i + 1}_pwm"),
curve[8 + i] * 255 / 100);
}
// Enable custom curve: pwm_enable=1 sets data->enabled=true and calls
// fan_curve_write() which pushes the curve to the EC via WMI DEVS method.
SysfsHelper.WriteInt(Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_enable"), 1);
}
public void DisableFanCurve(int fanIndex)
{
if (_asusFanCurveHwmonDir == null) return;
int pwmIndex = fanIndex + 1;
// pwm_enable=2 disables the custom curve for this fan, returning to
// the firmware's thermal policy defaults for the current profile.
SysfsHelper.WriteInt(Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_enable"), 2);
Helpers.Logger.WriteLine($"Fan {fanIndex}: disabled custom curve (pwm_enable=2)");
}
public byte[]? ResetFanCurveToDefaults(int fanIndex)
{
if (_asusFanCurveHwmonDir == null) return null;
int pwmIndex = fanIndex + 1;
// pwm_enable=3 resets the fan curve to BIOS factory defaults for the
// currently active platform profile, then disables the custom curve.
SysfsHelper.WriteInt(Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_enable"), 3);
Helpers.Logger.WriteLine($"Fan {fanIndex}: reset to factory defaults (pwm_enable=3)");
// Read back the firmware defaults so the UI can display them
return GetFanCurve(fanIndex);
}
public bool IsFanCurveEnabled(int fanIndex)
{
if (_asusFanCurveHwmonDir == null) return false;
int pwmIndex = fanIndex + 1;
return SysfsHelper.ReadInt(
Path.Combine(_asusFanCurveHwmonDir, $"pwm{pwmIndex}_enable"), 0) == 1;
}
// ── Battery ──
public int GetBatteryChargeLimit()
{
if (_batteryDir == null) return -1;
return SysfsHelper.ReadInt(
Path.Combine(_batteryDir, "charge_control_end_threshold"), -1);
}
public void SetBatteryChargeLimit(int percent)
{
if (_batteryDir == null) return;
percent = Math.Clamp(percent, 40, 100);
// Some models only accept 60/80/100 as charge limits
if (Helpers.AppConfig.IsChargeLimit6080())
{
if (percent > 85) percent = 100;
else if (percent >= 80) percent = 80;
else if (percent < 60) percent = 60;
}
SysfsHelper.WriteInt(
Path.Combine(_batteryDir, "charge_control_end_threshold"), percent);
}
// ── GPU ──
public bool GetGpuEco()
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.DgpuDisable, SysfsHelper.AsusBusPlatform);
if (path == null) return false;
return SysfsHelper.ReadInt(path, 0) == 1;
}
/// <summary>
/// Check if the NVIDIA DRM driver is currently active (holding GPU resources).
/// Returns true if nvidia_drm is loaded AND refcnt > 0.
/// Used by SetGpuEco guard — prevents kernel panics from ACPI hot-removal.
/// </summary>
private static bool IsNvidiaDrmActive()
{
// Module not loaded → safe to disable dGPU
if (!Directory.Exists("/sys/module/nvidia_drm"))
return false;
int refcnt = SysfsHelper.ReadInt("/sys/module/nvidia_drm/refcnt", -1);
// Can't read refcnt → assume active for safety
if (refcnt < 0)
{
Helpers.Logger.WriteLine("SetGpuEco guard: nvidia_drm loaded but refcnt unreadable — assuming active");
return true;
}
return refcnt > 0;
}
/// <summary>
/// Check if ANY dGPU driver is currently active (NVIDIA or AMD).
/// Combined guard for SetGpuEco — prevents ACPI hot-removal crash for both vendors.
/// </summary>
private bool IsDgpuDriverActive()
{
if (IsNvidiaDrmActive())
return true;
if (IsAmdDgpuDriverActive())
return true;
return false;
}
/// <summary>
/// Check if the AMD dGPU driver (amdgpu) is currently active.
/// AMD has no module refcnt like NVIDIA — instead check PCI runtime_status.
/// Returns true if amdgpu module is loaded AND bound to the dGPU AND runtime_status != "suspended".
/// </summary>
private static bool IsAmdDgpuDriverActive()
{
// amdgpu module not loaded → safe
if (!Directory.Exists("/sys/module/amdgpu"))
return false;
// Module is loaded — find the AMD dGPU PCI device
string? pciAddr = FindAmdDgpuPciAddress();
if (pciAddr == null)
return false; // No AMD dGPU found
// Check if amdgpu driver is bound to this device
string driverLink = $"/sys/bus/pci/devices/{pciAddr}/driver";
try
{
if (Directory.Exists(driverLink))
{
string target = Path.GetFileName(
Directory.ResolveLinkTarget(driverLink, false)?.FullName ?? "");
if (target != "amdgpu")
return false; // Different driver bound (vfio-pci, etc.)
}
else
{
return false; // No driver bound
}
}
catch
{
// Can't read driver symlink — fall through to runtime_status check
}
// Check runtime power state
string statusPath = $"/sys/bus/pci/devices/{pciAddr}/power/runtime_status";
string? status = SysfsHelper.ReadAttribute(statusPath);
if (status == "suspended")
{
Helpers.Logger.WriteLine($"SetGpuEco guard: AMD dGPU {pciAddr} runtime_status=suspended — safe");
return false;
}
// "active" or any other value (including null/unreadable) → assume active for safety
Helpers.Logger.WriteLine($"SetGpuEco guard: AMD dGPU {pciAddr} runtime_status={status ?? "unreadable"} — active");
return true;
}
/// <summary>
/// Scan PCI bus for AMD discrete GPU.
/// Criteria: vendor=0x1002, class=0x0300xx or 0x0302xx, boot_vga=0 (not iGPU).
/// </summary>
private static string? FindAmdDgpuPciAddress()
{
try
{
string pciDir = "/sys/bus/pci/devices";
if (!Directory.Exists(pciDir)) return null;
foreach (var deviceDir in Directory.GetDirectories(pciDir))
{
string? vendor = SysfsHelper.ReadAttribute(Path.Combine(deviceDir, "vendor"));
if (vendor != "0x1002") continue;
string? cls = SysfsHelper.ReadAttribute(Path.Combine(deviceDir, "class"));
if (cls == null) continue;
if (!cls.StartsWith("0x0300") && !cls.StartsWith("0x0302")) continue;
string? bootVga = SysfsHelper.ReadAttribute(Path.Combine(deviceDir, "boot_vga"));
if (bootVga == "1") continue; // iGPU, not dGPU
return Path.GetFileName(deviceDir);
}
}
catch { }
return null;
}
public void SetGpuEco(bool enabled)
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.DgpuDisable, SysfsHelper.AsusBusPlatform);
if (path == null) return;
// Skip write if already in desired state — writing dgpu_disable can block
// in the kernel for 30-60 seconds while the GPU powers down via ACPI/WMI
int current = SysfsHelper.ReadInt(path, -1);
int desired = enabled ? 1 : 0;
if (current == desired)
{
Helpers.Logger.WriteLine($"SetGpuEco: dgpu_disable already {desired}, skipping write");
return;
}
if (enabled)
{
// ── SAFETY GUARD 1: Never disable dGPU when dGPU driver is active ──
// Writing dgpu_disable=1 triggers ACPI hot-removal (acpiphp_disable_and_eject_slot).
// If nvidia_drm or amdgpu is bound, hot-removal causes kernel panic / GPU fault.
if (IsDgpuDriverActive())
throw new InvalidOperationException(
"SAFETY: Cannot write dgpu_disable=1 — dGPU driver is active. " +
"This would cause a kernel panic via ACPI hot-removal.");
// ── SAFETY GUARD 2: Never disable dGPU when MUX=0 (Ultimate/dGPU-direct) ──
// MUX=0 means the dGPU is the sole display output. Disabling it = no display = black screen.
// This creates an impossible boot state that requires CMOS reset to recover.
int mux = GetGpuMuxMode();
if (mux == 0)
throw new InvalidOperationException(
"SAFETY: Cannot write dgpu_disable=1 — gpu_mux_mode=0 (Ultimate). " +
"This creates an impossible state: dGPU is sole display output but powered off.");
}
SysfsHelper.WriteInt(path, desired);
if (!enabled)
{
// ── PCI bus rescan after enabling dGPU ──
// After dgpu_disable=0, the dGPU needs to reappear in the PCI device tree.
// The kernel ACPI _ON method usually triggers re-enumeration, but an explicit
// rescan ensures reliability (supergfxctl pattern: special_asus.rs:145-149).
// Best-effort: /sys/bus/pci/rescan requires root, may fail for non-root users.
Helpers.Logger.WriteLine("SetGpuEco: dGPU enabled, triggering PCI bus rescan");
Thread.Sleep(50); // Brief settle time for hardware (supergfxctl uses 50ms)
if (!SysfsHelper.WriteAttribute("/sys/bus/pci/rescan", "1"))
Helpers.Logger.WriteLine("SetGpuEco: PCI rescan failed (may need root) — dGPU should re-enumerate via ACPI");
}
}
public int GetGpuMuxMode()
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.GpuMuxMode, SysfsHelper.AsusBusPlatform);
if (path == null) return -1;
return SysfsHelper.ReadInt(path, -1);
}
public void SetGpuMuxMode(int mode)
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.GpuMuxMode, SysfsHelper.AsusBusPlatform);
if (path == null) return;
int current = SysfsHelper.ReadInt(path, -1);
if (current == mode)
{
Helpers.Logger.WriteLine($"SetGpuMuxMode: gpu_mux_mode already {mode}, skipping write");
return;
}
// ── SAFETY GUARD 3: Never write gpu_mux_mode when dGPU is disabled ──
// Firmware rejects MUX changes when dgpu_disable=1 (returns ENODEV).
// The kernel write can hang for several seconds before returning the error.
// Refusing immediately is safer and faster.
if (GetGpuEco())
throw new InvalidOperationException(
"SAFETY: Cannot write gpu_mux_mode — dgpu_disable=1. " +
"Firmware rejects MUX changes when dGPU is powered off.");
if (!SysfsHelper.WriteInt(path, mode))
throw new IOException(
$"gpu_mux_mode write rejected by firmware (wrote {mode} to {path})");
}
// ── Display ──
public bool GetPanelOverdrive()
{
// AsusAttributes.PanelOd handles the alias: panel_od (legacy) → panel_overdrive (fw-attr)
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.PanelOd, SysfsHelper.AsusWmiPlatform);
if (path == null) return false;
return SysfsHelper.ReadInt(path, 0) == 1;
}
public void SetPanelOverdrive(bool enabled)
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.PanelOd, SysfsHelper.AsusWmiPlatform);
if (path != null)
SysfsHelper.WriteInt(path, enabled ? 1 : 0);
}
public int GetMiniLedMode()
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.MiniLedMode, SysfsHelper.AsusBusPlatform);
if (path == null) return -1;
return SysfsHelper.ReadInt(path, -1);
}
public void SetMiniLedMode(int mode)
{
var path = SysfsHelper.ResolveAttrPath(AsusAttributes.MiniLedMode, SysfsHelper.AsusBusPlatform);
if (path != null)
SysfsHelper.WriteInt(path, mode);
}
// ── PPT / Power limits ──
public void SetPptLimit(string attribute, int watts)
{
// PPT attributes: ppt_pl1_spl, ppt_pl2_sppt, ppt_fppt, nv_dynamic_boost, nv_temp_target
var path = SysfsHelper.ResolveAttrPath(attribute, SysfsHelper.AsusWmiPlatform);
if (path != null)
SysfsHelper.WriteInt(path, watts);
}
public int GetPptLimit(string attribute)
{
var path = SysfsHelper.ResolveAttrPath(attribute, SysfsHelper.AsusWmiPlatform);
if (path == null) return -1;
return SysfsHelper.ReadInt(path, -1);
}
// ── Keyboard ──
public int GetKeyboardBrightness()
{
var ledPath = Path.Combine(SysfsHelper.Leds, "asus::kbd_backlight", "brightness");
return SysfsHelper.ReadInt(ledPath, -1);
}
public void SetKeyboardBrightness(int level)
{
var ledPath = Path.Combine(SysfsHelper.Leds, "asus::kbd_backlight", "brightness");
SysfsHelper.WriteInt(ledPath, Math.Clamp(level, 0, 3));
}
public void SetKeyboardRgb(byte r, byte g, byte b)
{
var intensityPath = Path.Combine(SysfsHelper.Leds, "asus::kbd_backlight", "multi_intensity");
SysfsHelper.WriteAttribute(intensityPath, $"{r} {g} {b}");
}
/// <summary>
/// Set TUF keyboard RGB mode via sysfs kbd_rgb_mode attribute.
/// This is the primary RGB control for TUF Gaming keyboards.
/// Format: space-separated byte array "cmd mode R G B speed"
/// Learned from asusctl: rog-platform/src/keyboard_led.rs + asusd/src/aura_laptop/mod.rs
/// </summary>
public void SetKeyboardRgbMode(int mode, byte r, byte g, byte b, int speed)
{
var modePath = Path.Combine(SysfsHelper.Leds, "asus::kbd_backlight", "kbd_rgb_mode");
if (!SysfsHelper.Exists(modePath))
{
Helpers.Logger.WriteLine($"kbd_rgb_mode not available at {modePath}");
return;
}
// Protocol: [1, mode, R, G, B, speed] — matches asusctl's TUF write
string value = $"1 {mode} {r} {g} {b} {speed}";
SysfsHelper.WriteAttribute(modePath, value);
}
/// <summary>
/// Set TUF keyboard RGB power state via sysfs kbd_rgb_state attribute.
/// Controls which lighting states are active (boot/awake/sleep).
/// Format: space-separated byte array "cmd boot awake sleep keyboard"
/// Learned from asusctl: rog-aura/src/keyboard/power.rs TUF format
/// </summary>
public void SetKeyboardRgbState(bool boot, bool awake, bool sleep)
{
var statePath = Path.Combine(SysfsHelper.Leds, "asus::kbd_backlight", "kbd_rgb_state");
if (!SysfsHelper.Exists(statePath))
{
Helpers.Logger.WriteLine($"kbd_rgb_state not available at {statePath}");
return;
}
// Protocol: [1, boot, awake, sleep, 1] — matches asusctl's TUF power state
string value = $"1 {(boot ? 1 : 0)} {(awake ? 1 : 0)} {(sleep ? 1 : 0)} 1";
SysfsHelper.WriteAttribute(statePath, value);
}
/// <summary>
/// Check if TUF-specific kbd_rgb_mode sysfs attribute is available.
/// </summary>
public bool HasKeyboardRgbMode()
{
return SysfsHelper.Exists(
Path.Combine(SysfsHelper.Leds, "asus::kbd_backlight", "kbd_rgb_mode"));
}
// ── Temperature ──
private int GetCpuTemp()
{
// Try dedicated CPU temp hwmon (coretemp/k10temp) — package temp
if (_cpuTempHwmonDir != null)
{
int temp = SysfsHelper.ReadInt(Path.Combine(_cpuTempHwmonDir, "temp1_input"), -1);
if (temp > 0) return temp / 1000;
}
// Try ASUS base hwmon
if (_asusBaseHwmonDir != null)
{
int temp = SysfsHelper.ReadInt(Path.Combine(_asusBaseHwmonDir, "temp1_input"), -1);
if (temp > 0) return temp / 1000;
}
// Fallback to thermal zones
if (Directory.Exists(SysfsHelper.Thermal))
{
foreach (var zone in Directory.GetDirectories(SysfsHelper.Thermal))
{
var type = SysfsHelper.ReadAttribute(Path.Combine(zone, "type"));
if (type != null && type.Contains("x86_pkg_temp", StringComparison.OrdinalIgnoreCase))
{
int temp = SysfsHelper.ReadInt(Path.Combine(zone, "temp"), -1);
if (temp > 0) return temp / 1000;
}
}
// Last resort: first thermal zone
var fallback = Path.Combine(SysfsHelper.Thermal, "thermal_zone0", "temp");
int fallbackTemp = SysfsHelper.ReadInt(fallback, -1);
if (fallbackTemp > 0) return fallbackTemp / 1000;
}
return -1;
}
private int GetGpuTemp()
{
// Try NVIDIA hwmon (cached lookup, no repeated filesystem scan)
var nvidiaHwmon = SysfsHelper.FindHwmonByName("nvidia");
if (nvidiaHwmon != null)
{
int temp = SysfsHelper.ReadInt(Path.Combine(nvidiaHwmon, "temp1_input"), -1);
if (temp > 0) return temp / 1000;
}
// Try amdgpu hwmon
var amdHwmon = SysfsHelper.FindHwmonByName("amdgpu");
if (amdHwmon != null)
{
int temp = SysfsHelper.ReadInt(Path.Combine(amdHwmon, "temp1_input"), -1);
if (temp > 0) return temp / 1000;
}
// Fallback: try nvidia-smi (proprietary driver doesn't always expose hwmon)
try
{
var output = SysfsHelper.RunCommand("nvidia-smi", "--query-gpu=temperature.gpu --format=csv,noheader,nounits");
if (!string.IsNullOrWhiteSpace(output) && int.TryParse(output.Trim(), out int smiTemp) && smiTemp > 0)
return smiTemp;
}
catch { /* nvidia-smi not available */ }
return -1;
}
// ── Events ──
public void SubscribeEvents()
{
_eventListening = true;
_eventThread = new Thread(EventLoop)
{
Name = "AsusWmi-EventLoop",
IsBackground = true
};
_eventThread.Start();
}
private void EventLoop()
{
// Find all ASUS input devices.
// On newer kernels with the 'asus' HID driver, hotkey events come from the
// USB N-KEY Device ("Asus Keyboard" = event8), NOT from "Asus WMI hotkeys" (event9).
// We listen on ALL discovered ASUS input devices simultaneously using poll().
var devices = FindAsusInputDevices();
if (devices.Count == 0)
{
Helpers.Logger.WriteLine("WARNING: Could not find any ASUS input device for hotkey events");
return;
}
foreach (var dev in devices)
Helpers.Logger.WriteLine($"Listening for ASUS events on {dev}");
var streams = new List<FileStream>();
try
{
foreach (var dev in devices)
{
try
{
streams.Add(new FileStream(dev, FileMode.Open, FileAccess.Read, FileShare.Read));
}
catch (Exception ex)
{
Helpers.Logger.WriteLine($"Could not open {dev}: {ex.Message}");
}
}
if (streams.Count == 0)
{
Helpers.Logger.WriteLine("WARNING: Could not open any ASUS input devices");
return;
}
// Store references so Dispose() can close them to unblock reads
lock (_eventStreams)
{
_eventStreams.AddRange(streams);
}
// If only one device, use simple blocking read
if (streams.Count == 1)
{
ReadEventsFromStream(streams[0]);
}
else
{
// Multiple devices: read each in its own thread
var threads = new List<Thread>();
foreach (var stream in streams)
{
var s = stream; // capture for closure
var t = new Thread(() => ReadEventsFromStream(s))
{
Name = $"AsusWmi-Reader-{Path.GetFileName(s.Name)}",
IsBackground = true
};
t.Start();
threads.Add(t);
}
// Wait for all reader threads (they'll exit when _eventListening = false)
foreach (var t in threads)
t.Join();
}
}
catch (Exception ex)
{
if (_eventListening)
Helpers.Logger.WriteLine("Event loop error", ex);
}
finally
{
foreach (var fs in streams)
{
try { fs.Dispose(); } catch { }
}
}
}
private void ReadEventsFromStream(FileStream fs)
{
var buffer = new byte[24]; // sizeof(struct input_event) on 64-bit
try
{
while (_eventListening)
{
int bytesRead = fs.Read(buffer, 0, 24);
if (bytesRead == 24)
{
// struct input_event: {timeval(16 bytes), __u16 type, __u16 code, __s32 value}
ushort type = BitConverter.ToUInt16(buffer, 16);
ushort code = BitConverter.ToUInt16(buffer, 18);
int value = BitConverter.ToInt32(buffer, 20);
// EV_KEY = 1, key press = value 1
if (type == 1 && value == 1)
{
string mappedKey = MapLinuxKeyToBindingName(code);
if (mappedKey != "")
{
Helpers.Logger.WriteLine($"ASUS event: key={code} (0x{code:X}) → {mappedKey}");
KeyBindingEvent?.Invoke(mappedKey);
}
else
{
// Also fire legacy numeric event for non-configurable keys
int legacyEvent = MapLinuxKeyToLegacyEvent(code);
if (legacyEvent > 0)
{
Helpers.Logger.WriteLine($"ASUS event: key={code} (0x{code:X}) → legacy={legacyEvent}");
WmiEvent?.Invoke(legacyEvent);
}
else
{
// Don't log unmapped keys — regular keyboard presses
// also come through on ASUS vendor devices
}
}
}
}
}
}
catch (Exception ex)
{
if (_eventListening)
Helpers.Logger.WriteLine($"Reader error on {fs.Name}: {ex.Message}");
}
}
/// <summary>Find all ASUS input devices in /dev/input/.</summary>
private static List<string> FindAsusInputDevices()
{
var result = new List<string>();
try
{
if (!File.Exists("/proc/bus/input/devices")) return result;
var content = File.ReadAllText("/proc/bus/input/devices");
var sections = content.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
// Priority: USB keyboard first ("Asus Keyboard"), then WMI ("Asus WMI hotkeys")
foreach (var section in sections)
{
// Match sections containing "asus" (name or sysfs path) or USB vendor 0b05 (ASUSTek).
// The vendor match catches ITE-named ASUS HID devices like "ITE Tech. Inc. ITE Device(8910)".
bool isAsus = section.Contains("asus", StringComparison.OrdinalIgnoreCase)
|| section.Contains("Vendor=0b05", StringComparison.OrdinalIgnoreCase);
if (!isAsus) continue;
bool isKeyboard = section.Contains("keyboard", StringComparison.OrdinalIgnoreCase)
|| section.Contains("Vendor=0b05", StringComparison.OrdinalIgnoreCase);
bool isWmi = section.Contains("wmi", StringComparison.OrdinalIgnoreCase);
if (isKeyboard || isWmi)
{
string? eventDev = ExtractEventDevice(section);
if (eventDev != null)
{
// USB keyboard first in the list (higher priority)
if (isKeyboard && !isWmi)
result.Insert(0, eventDev);
else
result.Add(eventDev);
}
}
}
}
catch (Exception ex)
{
Helpers.Logger.WriteLine("FindAsusInputDevices failed", ex);
}
return result;
}
private static string? ExtractEventDevice(string section)
{
foreach (var line in section.Split('\n'))
{
if (line.StartsWith("H: Handlers="))
{
var parts = line.Split(' ');
foreach (var part in parts)
{
if (part.StartsWith("event"))
return $"/dev/input/{part.Trim()}";
}
}
}
return null;
}
/// <summary>
/// Fired for configurable keys (m4, fnf4, fnf5).
/// The string is the binding name that maps to AppConfig key.
/// </summary>
public event Action<string>? KeyBindingEvent;
/// <summary>
/// Map Linux KEY_* codes to configurable key binding names.
/// These are keys the user can assign actions to.
/// </summary>
private static string MapLinuxKeyToBindingName(ushort linuxKeyCode)
{
return linuxKeyCode switch
{
// KEY_PROG1 (148) = ROG/M5 key → configurable as "m4" (Windows G-Helper naming)
148 or 190 => "m4",
// KEY_PROG3 (202) = Fn+F4 Aura key → configurable as "fnf4"
202 => "fnf4",
// KEY_PROG4 (203) = Fn+F5 / M4 performance key → configurable as "fnf5"
203 => "fnf5",
_ => ""
};
}
/// <summary>
/// Map Linux KEY_* codes to legacy G-Helper event codes for non-configurable keys
/// (keyboard brightness, touchpad, etc.).
/// </summary>
private static int MapLinuxKeyToLegacyEvent(ushort linuxKeyCode)
{
return linuxKeyCode switch
{
// KEY_KBDILLUMUP (229) → Fn+F3 (196)
229 => 196,
// KEY_KBDILLUMDOWN (230) → Fn+F2 (197)
230 => 197,