日常更新
This commit is contained in:
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,13 +1,13 @@
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
*.user
|
||||
*.suo
|
||||
dist/
|
||||
dist-svc/
|
||||
dist-installer/
|
||||
*.log
|
||||
wix/obj/
|
||||
wix/Harvested.wxs
|
||||
wix/Harvested-Service.wxs
|
||||
FanControl.ChromeboxEC/lib/FanControl.Plugins.dll
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
*.user
|
||||
*.suo
|
||||
dist/
|
||||
dist-svc/
|
||||
dist-installer/
|
||||
*.log
|
||||
wix/obj/
|
||||
wix/Harvested.wxs
|
||||
wix/Harvested-Service.wxs
|
||||
FanControl.ChromeboxEC/lib/FanControl.Plugins.dll
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControl", "ChromeboxFanControl\ChromeboxFanControl.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControlService", "ChromeboxFanControlService\ChromeboxFanControlService.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanControl.ChromeboxEC", "FanControl.ChromeboxEC\FanControl.ChromeboxEC.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControl", "ChromeboxFanControl\ChromeboxFanControl.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControlService", "ChromeboxFanControlService\ChromeboxFanControlService.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanControl.ChromeboxEC", "FanControl.ChromeboxEC\FanControl.ChromeboxEC.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,136 +1,136 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public sealed class AppConfig
|
||||
{
|
||||
/// <summary>UI language: "auto" (system), "en", "zh-Hans", "zh-Hant". Restart to apply.</summary>
|
||||
public string Language { get; set; } = "auto";
|
||||
public string EctoolPath { get; set; } = @"C:\Program Files\crosec\ectool.exe";
|
||||
public int PollIntervalMs { get; set; } = 1500;
|
||||
/// <summary>Call ectool for RPM every N control cycles (1 = every cycle).</summary>
|
||||
public int FanRpmPollEveryNCycles { get; set; } = 1;
|
||||
public string[] FanRpmArgs { get; set; } = ["pwmgetfanrpm", "0"];
|
||||
/// <summary>Ectool args to read fan duty (0-100). Empty = skip, use commanded duty.</summary>
|
||||
public string[]? FanDutyArgs { get; set; }
|
||||
public string[] AutoFanCtrlArgs { get; set; } = ["autofanctrl"];
|
||||
public string TempSource { get; set; } = "AverageCore";
|
||||
public int RampUpSteps { get; set; } = 3;
|
||||
public int RampUpMinDeltaPercent { get; set; } = 20;
|
||||
public int FailSafeAfterConsecutiveErrors { get; set; } = 5;
|
||||
public int FailSafeFanPercent { get; set; } = 100;
|
||||
public bool FailSafeRestoreAutoFan { get; set; }
|
||||
public int ChartHistoryMinutes { get; set; } = 15;
|
||||
public int ChartMaxPoints { get; set; } = 900;
|
||||
public int[] CurvePoints { get; set; } =
|
||||
[
|
||||
0, 0, 10, 25, 40, 55, 70, 80, 90, 95, 97, 98, 99, 100
|
||||
];
|
||||
|
||||
public static JsonSerializerOptions JsonOptions { get; } = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public static string UserConfigPath =>
|
||||
Path.Combine(
|
||||
Environment.UserInteractive
|
||||
? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
|
||||
: Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"ChromeboxFanControl",
|
||||
"config.json");
|
||||
|
||||
public static AppConfig LoadMerged()
|
||||
{
|
||||
var cfg = new AppConfig();
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var appSettings = Path.Combine(baseDir, "appsettings.json");
|
||||
if (File.Exists(appSettings))
|
||||
ApplyJson(File.ReadAllText(appSettings), cfg);
|
||||
|
||||
if (File.Exists(UserConfigPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplyJson(File.ReadAllText(UserConfigPath), cfg);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* keep base */
|
||||
}
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private static void ApplyJson(string json, AppConfig dst)
|
||||
{
|
||||
var src = JsonSerializer.Deserialize<AppConfig>(json, JsonOptions);
|
||||
if (src == null)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(src.Language))
|
||||
dst.Language = src.Language;
|
||||
dst.EctoolPath = src.EctoolPath;
|
||||
dst.PollIntervalMs = Math.Clamp(src.PollIntervalMs, 250, 60_000);
|
||||
dst.FanRpmPollEveryNCycles = Math.Max(1, src.FanRpmPollEveryNCycles);
|
||||
if (src.FanRpmArgs is { Length: > 0 })
|
||||
dst.FanRpmArgs = src.FanRpmArgs;
|
||||
dst.FanDutyArgs = src.FanDutyArgs;
|
||||
if (src.AutoFanCtrlArgs is { Length: > 0 })
|
||||
dst.AutoFanCtrlArgs = src.AutoFanCtrlArgs;
|
||||
dst.TempSource = string.Equals(src.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase) ? "MaxCore" : "AverageCore";
|
||||
dst.RampUpSteps = Math.Clamp(src.RampUpSteps, 1, 10);
|
||||
dst.RampUpMinDeltaPercent = Math.Clamp(src.RampUpMinDeltaPercent, 0, 50);
|
||||
dst.FailSafeAfterConsecutiveErrors = Math.Max(1, src.FailSafeAfterConsecutiveErrors);
|
||||
dst.FailSafeFanPercent = Math.Clamp(src.FailSafeFanPercent, 0, 100);
|
||||
dst.FailSafeRestoreAutoFan = src.FailSafeRestoreAutoFan;
|
||||
dst.ChartHistoryMinutes = Math.Clamp(src.ChartHistoryMinutes, 1, 120);
|
||||
dst.ChartMaxPoints = src.ChartMaxPoints > 0 ? src.ChartMaxPoints : 900;
|
||||
if (src.CurvePoints is { Length: 14 })
|
||||
dst.CurvePoints = (int[])src.CurvePoints.Clone();
|
||||
else if (src.CurvePoints is { Length: 13 } old13)
|
||||
dst.CurvePoints = MigrateCurveTo14(old13);
|
||||
else if (src.CurvePoints is { Length: 20 } old20)
|
||||
dst.CurvePoints = MigrateCurveTo14(old20, new[] { 0, 5, 11, 16, 21, 26, 32, 37, 42, 47, 53, 58, 63, 68, 74, 79, 84, 90, 95, 100 });
|
||||
}
|
||||
|
||||
private static int[] MigrateCurveTo14(int[] oldCurve, int[]? oldTemps = null)
|
||||
{
|
||||
var temps = oldTemps ?? (oldCurve.Length == 13 ? new[] { 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100 } : new[] { 0, 5, 11, 16, 21, 26, 32, 37, 42, 47, 53, 58, 63, 68, 74, 79, 84, 90, 95, 100 });
|
||||
var newTemps = FanCurve.TempBreakpoints;
|
||||
var result = new int[14];
|
||||
for (var i = 0; i < 14; i++)
|
||||
{
|
||||
var t = newTemps[i];
|
||||
if (t <= temps[0])
|
||||
result[i] = Math.Clamp(oldCurve[0], 0, 100);
|
||||
else if (t >= temps[^1])
|
||||
result[i] = Math.Clamp(oldCurve[^1], 0, 100);
|
||||
else
|
||||
{
|
||||
for (var j = 0; j < temps.Length - 1; j++)
|
||||
{
|
||||
if (t < temps[j + 1])
|
||||
{
|
||||
var frac = (t - temps[j]) / (double)(temps[j + 1] - temps[j]);
|
||||
result[i] = (int)Math.Round(Math.Clamp(oldCurve[j], 0, 100) + (Math.Clamp(oldCurve[j + 1], 0, 100) - Math.Clamp(oldCurve[j], 0, 100)) * frac);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SaveUser()
|
||||
{
|
||||
var dir = Path.GetDirectoryName(UserConfigPath);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(UserConfigPath, JsonSerializer.Serialize(this, JsonOptions));
|
||||
}
|
||||
}
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public sealed class AppConfig
|
||||
{
|
||||
/// <summary>UI language: "auto" (system), "en", "zh-Hans", "zh-Hant". Restart to apply.</summary>
|
||||
public string Language { get; set; } = "auto";
|
||||
public string EctoolPath { get; set; } = @"C:\Program Files\crosec\ectool.exe";
|
||||
public int PollIntervalMs { get; set; } = 1500;
|
||||
/// <summary>Call ectool for RPM every N control cycles (1 = every cycle).</summary>
|
||||
public int FanRpmPollEveryNCycles { get; set; } = 1;
|
||||
public string[] FanRpmArgs { get; set; } = ["pwmgetfanrpm", "0"];
|
||||
/// <summary>Ectool args to read fan duty (0-100). Empty = skip, use commanded duty.</summary>
|
||||
public string[]? FanDutyArgs { get; set; }
|
||||
public string[] AutoFanCtrlArgs { get; set; } = ["autofanctrl"];
|
||||
public string TempSource { get; set; } = "AverageCore";
|
||||
public int RampUpSteps { get; set; } = 3;
|
||||
public int RampUpMinDeltaPercent { get; set; } = 20;
|
||||
public int FailSafeAfterConsecutiveErrors { get; set; } = 5;
|
||||
public int FailSafeFanPercent { get; set; } = 100;
|
||||
public bool FailSafeRestoreAutoFan { get; set; }
|
||||
public int ChartHistoryMinutes { get; set; } = 15;
|
||||
public int ChartMaxPoints { get; set; } = 900;
|
||||
public int[] CurvePoints { get; set; } =
|
||||
[
|
||||
0, 0, 10, 25, 40, 55, 70, 80, 90, 95, 97, 98, 99, 100
|
||||
];
|
||||
|
||||
public static JsonSerializerOptions JsonOptions { get; } = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public static string UserConfigPath =>
|
||||
Path.Combine(
|
||||
Environment.UserInteractive
|
||||
? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
|
||||
: Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"ChromeboxFanControl",
|
||||
"config.json");
|
||||
|
||||
public static AppConfig LoadMerged()
|
||||
{
|
||||
var cfg = new AppConfig();
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var appSettings = Path.Combine(baseDir, "appsettings.json");
|
||||
if (File.Exists(appSettings))
|
||||
ApplyJson(File.ReadAllText(appSettings), cfg);
|
||||
|
||||
if (File.Exists(UserConfigPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplyJson(File.ReadAllText(UserConfigPath), cfg);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* keep base */
|
||||
}
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private static void ApplyJson(string json, AppConfig dst)
|
||||
{
|
||||
var src = JsonSerializer.Deserialize<AppConfig>(json, JsonOptions);
|
||||
if (src == null)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(src.Language))
|
||||
dst.Language = src.Language;
|
||||
dst.EctoolPath = src.EctoolPath;
|
||||
dst.PollIntervalMs = Math.Clamp(src.PollIntervalMs, 250, 60_000);
|
||||
dst.FanRpmPollEveryNCycles = Math.Max(1, src.FanRpmPollEveryNCycles);
|
||||
if (src.FanRpmArgs is { Length: > 0 })
|
||||
dst.FanRpmArgs = src.FanRpmArgs;
|
||||
dst.FanDutyArgs = src.FanDutyArgs;
|
||||
if (src.AutoFanCtrlArgs is { Length: > 0 })
|
||||
dst.AutoFanCtrlArgs = src.AutoFanCtrlArgs;
|
||||
dst.TempSource = string.Equals(src.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase) ? "MaxCore" : "AverageCore";
|
||||
dst.RampUpSteps = Math.Clamp(src.RampUpSteps, 1, 10);
|
||||
dst.RampUpMinDeltaPercent = Math.Clamp(src.RampUpMinDeltaPercent, 0, 50);
|
||||
dst.FailSafeAfterConsecutiveErrors = Math.Max(1, src.FailSafeAfterConsecutiveErrors);
|
||||
dst.FailSafeFanPercent = Math.Clamp(src.FailSafeFanPercent, 0, 100);
|
||||
dst.FailSafeRestoreAutoFan = src.FailSafeRestoreAutoFan;
|
||||
dst.ChartHistoryMinutes = Math.Clamp(src.ChartHistoryMinutes, 1, 120);
|
||||
dst.ChartMaxPoints = src.ChartMaxPoints > 0 ? src.ChartMaxPoints : 900;
|
||||
if (src.CurvePoints is { Length: 14 })
|
||||
dst.CurvePoints = (int[])src.CurvePoints.Clone();
|
||||
else if (src.CurvePoints is { Length: 13 } old13)
|
||||
dst.CurvePoints = MigrateCurveTo14(old13);
|
||||
else if (src.CurvePoints is { Length: 20 } old20)
|
||||
dst.CurvePoints = MigrateCurveTo14(old20, new[] { 0, 5, 11, 16, 21, 26, 32, 37, 42, 47, 53, 58, 63, 68, 74, 79, 84, 90, 95, 100 });
|
||||
}
|
||||
|
||||
private static int[] MigrateCurveTo14(int[] oldCurve, int[]? oldTemps = null)
|
||||
{
|
||||
var temps = oldTemps ?? (oldCurve.Length == 13 ? new[] { 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100 } : new[] { 0, 5, 11, 16, 21, 26, 32, 37, 42, 47, 53, 58, 63, 68, 74, 79, 84, 90, 95, 100 });
|
||||
var newTemps = FanCurve.TempBreakpoints;
|
||||
var result = new int[14];
|
||||
for (var i = 0; i < 14; i++)
|
||||
{
|
||||
var t = newTemps[i];
|
||||
if (t <= temps[0])
|
||||
result[i] = Math.Clamp(oldCurve[0], 0, 100);
|
||||
else if (t >= temps[^1])
|
||||
result[i] = Math.Clamp(oldCurve[^1], 0, 100);
|
||||
else
|
||||
{
|
||||
for (var j = 0; j < temps.Length - 1; j++)
|
||||
{
|
||||
if (t < temps[j + 1])
|
||||
{
|
||||
var frac = (t - temps[j]) / (double)(temps[j + 1] - temps[j]);
|
||||
result[i] = (int)Math.Round(Math.Clamp(oldCurve[j], 0, 100) + (Math.Clamp(oldCurve[j + 1], 0, 100) - Math.Clamp(oldCurve[j], 0, 100)) * frac);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SaveUser()
|
||||
{
|
||||
var dir = Path.GetDirectoryName(UserConfigPath);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(UserConfigPath, JsonSerializer.Serialize(this, JsonOptions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);NU1701</NoWarn>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<RootNamespace>ChromeboxFanControl</RootNamespace>
|
||||
<AssemblyName>ChromeboxFanControl</AssemblyName>
|
||||
<ApplicationIcon />
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.6" />
|
||||
<PackageReference Include="ScottPlot.WinForms" Version="5.0.55" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SatelliteResourceLanguages>en;zh-Hans;zh-Hant</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);NU1701</NoWarn>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<RootNamespace>ChromeboxFanControl</RootNamespace>
|
||||
<AssemblyName>ChromeboxFanControl</AssemblyName>
|
||||
<ApplicationIcon />
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.6" />
|
||||
<PackageReference Include="ScottPlot.WinForms" Version="5.0.55" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SatelliteResourceLanguages>en;zh-Hans;zh-Hant</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
using LibreHardwareMonitor.Hardware;
|
||||
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public enum CpuTempAggregation
|
||||
{
|
||||
AverageCore,
|
||||
MaxCore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CPU temperature via LibreHardwareMonitor only (no fan sensors).
|
||||
/// </summary>
|
||||
public sealed class CpuTempReader : IDisposable
|
||||
{
|
||||
private readonly Computer _computer = new()
|
||||
{
|
||||
IsCpuEnabled = true,
|
||||
IsMotherboardEnabled = true
|
||||
};
|
||||
private bool _open;
|
||||
|
||||
public CpuTempReader()
|
||||
{
|
||||
_computer.Open();
|
||||
_open = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_open)
|
||||
return;
|
||||
_computer.Close();
|
||||
_open = false;
|
||||
}
|
||||
|
||||
/// <summary>Returns null if no reading available.</summary>
|
||||
public double? ReadCpuTemp(CpuTempAggregation mode)
|
||||
{
|
||||
foreach (var hw in _computer.Hardware)
|
||||
{
|
||||
hw.Update();
|
||||
foreach (var sh in hw.SubHardware)
|
||||
sh.Update();
|
||||
}
|
||||
|
||||
var coreTemps = new List<double>();
|
||||
foreach (var hw in _computer.Hardware)
|
||||
{
|
||||
CollectCoreTemps(hw, coreTemps);
|
||||
foreach (var sh in hw.SubHardware)
|
||||
CollectCoreTemps(sh, coreTemps);
|
||||
}
|
||||
|
||||
if (coreTemps.Count > 0)
|
||||
{
|
||||
return mode == CpuTempAggregation.MaxCore
|
||||
? coreTemps.Max()
|
||||
: coreTemps.Average();
|
||||
}
|
||||
|
||||
// Fallback: any CPU temperature sensor
|
||||
var any = new List<double>();
|
||||
foreach (var hw in _computer.Hardware)
|
||||
{
|
||||
CollectAnyCpuTemp(hw, any);
|
||||
foreach (var sh in hw.SubHardware)
|
||||
CollectAnyCpuTemp(sh, any);
|
||||
}
|
||||
|
||||
if (any.Count == 0)
|
||||
return null;
|
||||
return mode == CpuTempAggregation.MaxCore ? any.Max() : any.Average();
|
||||
}
|
||||
|
||||
private static readonly string[] CoreTempPatterns =
|
||||
[
|
||||
"Core", "Package", "Tctl", "Tdie", "CCD", "CPU Package",
|
||||
"Core Average", "Core Max", "Core #"
|
||||
];
|
||||
|
||||
private static void CollectCoreTemps(IHardware hw, List<double> list)
|
||||
{
|
||||
foreach (var s in hw.Sensors)
|
||||
{
|
||||
if (s.SensorType != SensorType.Temperature || !s.Value.HasValue)
|
||||
continue;
|
||||
var name = s.Name;
|
||||
if (CoreTempPatterns.Any(p => name.Contains(p, StringComparison.OrdinalIgnoreCase)))
|
||||
list.Add(s.Value.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectAnyCpuTemp(IHardware hw, List<double> list)
|
||||
{
|
||||
foreach (var s in hw.Sensors)
|
||||
{
|
||||
if (s.SensorType == SensorType.Temperature && s.Value.HasValue)
|
||||
list.Add(s.Value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
using LibreHardwareMonitor.Hardware;
|
||||
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public enum CpuTempAggregation
|
||||
{
|
||||
AverageCore,
|
||||
MaxCore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CPU temperature via LibreHardwareMonitor only (no fan sensors).
|
||||
/// </summary>
|
||||
public sealed class CpuTempReader : IDisposable
|
||||
{
|
||||
private readonly Computer _computer = new()
|
||||
{
|
||||
IsCpuEnabled = true,
|
||||
IsMotherboardEnabled = true
|
||||
};
|
||||
private bool _open;
|
||||
|
||||
public CpuTempReader()
|
||||
{
|
||||
_computer.Open();
|
||||
_open = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_open)
|
||||
return;
|
||||
_computer.Close();
|
||||
_open = false;
|
||||
}
|
||||
|
||||
/// <summary>Returns null if no reading available.</summary>
|
||||
public double? ReadCpuTemp(CpuTempAggregation mode)
|
||||
{
|
||||
foreach (var hw in _computer.Hardware)
|
||||
{
|
||||
hw.Update();
|
||||
foreach (var sh in hw.SubHardware)
|
||||
sh.Update();
|
||||
}
|
||||
|
||||
var coreTemps = new List<double>();
|
||||
foreach (var hw in _computer.Hardware)
|
||||
{
|
||||
CollectCoreTemps(hw, coreTemps);
|
||||
foreach (var sh in hw.SubHardware)
|
||||
CollectCoreTemps(sh, coreTemps);
|
||||
}
|
||||
|
||||
if (coreTemps.Count > 0)
|
||||
{
|
||||
return mode == CpuTempAggregation.MaxCore
|
||||
? coreTemps.Max()
|
||||
: coreTemps.Average();
|
||||
}
|
||||
|
||||
// Fallback: any CPU temperature sensor
|
||||
var any = new List<double>();
|
||||
foreach (var hw in _computer.Hardware)
|
||||
{
|
||||
CollectAnyCpuTemp(hw, any);
|
||||
foreach (var sh in hw.SubHardware)
|
||||
CollectAnyCpuTemp(sh, any);
|
||||
}
|
||||
|
||||
if (any.Count == 0)
|
||||
return null;
|
||||
return mode == CpuTempAggregation.MaxCore ? any.Max() : any.Average();
|
||||
}
|
||||
|
||||
private static readonly string[] CoreTempPatterns =
|
||||
[
|
||||
"Core", "Package", "Tctl", "Tdie", "CCD", "CPU Package",
|
||||
"Core Average", "Core Max", "Core #"
|
||||
];
|
||||
|
||||
private static void CollectCoreTemps(IHardware hw, List<double> list)
|
||||
{
|
||||
foreach (var s in hw.Sensors)
|
||||
{
|
||||
if (s.SensorType != SensorType.Temperature || !s.Value.HasValue)
|
||||
continue;
|
||||
var name = s.Name;
|
||||
if (CoreTempPatterns.Any(p => name.Contains(p, StringComparison.OrdinalIgnoreCase)))
|
||||
list.Add(s.Value.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectAnyCpuTemp(IHardware hw, List<double> list)
|
||||
{
|
||||
foreach (var s in hw.Sensors)
|
||||
{
|
||||
if (s.SensorType == SensorType.Temperature && s.Value.HasValue)
|
||||
list.Add(s.Value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public sealed class EctoolRunner
|
||||
{
|
||||
private static readonly Regex NumberRegex = new(@"\d+", RegexOptions.Compiled);
|
||||
|
||||
public static (bool ok, string stdout, string stderr) RunSync(
|
||||
string exePath,
|
||||
IReadOnlyList<string> args,
|
||||
int timeoutMs = 15_000) =>
|
||||
RunAsync(exePath, args, CancellationToken.None, timeoutMs).GetAwaiter().GetResult();
|
||||
|
||||
public static async Task<(bool ok, string stdout, string stderr)> RunAsync(
|
||||
string exePath,
|
||||
IReadOnlyList<string> args,
|
||||
CancellationToken ct = default,
|
||||
int timeoutMs = 15_000)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
|
||||
return (false, "", "ectool executable not found.");
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
foreach (var a in args)
|
||||
psi.ArgumentList.Add(a);
|
||||
|
||||
using var proc = new Process { StartInfo = psi };
|
||||
try
|
||||
{
|
||||
proc.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, "", ex.Message);
|
||||
}
|
||||
|
||||
var stdoutTask = proc.StandardOutput.ReadToEndAsync(ct);
|
||||
var stderrTask = proc.StandardError.ReadToEndAsync(ct);
|
||||
var waitTask = proc.WaitForExitAsync(ct);
|
||||
var completed = await Task.WhenAny(waitTask, Task.Delay(timeoutMs, ct)).ConfigureAwait(false);
|
||||
if (completed != waitTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
proc.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
return (false, "", "ectool timed out.");
|
||||
}
|
||||
|
||||
await waitTask.ConfigureAwait(false);
|
||||
var stdout = await stdoutTask.ConfigureAwait(false);
|
||||
var stderr = await stderrTask.ConfigureAwait(false);
|
||||
var ok = proc.ExitCode == 0;
|
||||
return (ok, stdout, stderr);
|
||||
}
|
||||
|
||||
/// <summary>Pick the most plausible RPM (typically 500–20000) from ectool text output.</summary>
|
||||
public static int? TryParseFanRpm(string stdout)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stdout))
|
||||
return null;
|
||||
|
||||
int? best = null;
|
||||
foreach (Match m in NumberRegex.Matches(stdout))
|
||||
{
|
||||
if (!int.TryParse(m.Value, out var n))
|
||||
continue;
|
||||
if (n is < 400 or > 50_000)
|
||||
continue;
|
||||
// Prefer larger numbers in typical fan range (avoid fan index "0")
|
||||
if (n >= 800 && (best == null || n > best))
|
||||
best = n;
|
||||
}
|
||||
|
||||
if (best != null)
|
||||
return best;
|
||||
|
||||
foreach (Match m in NumberRegex.Matches(stdout))
|
||||
{
|
||||
if (int.TryParse(m.Value, out var n) && n is >= 0 and <= 50_000)
|
||||
return n;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Pick duty percent (0-100) from ectool text output.</summary>
|
||||
public static int? TryParseFanDuty(string stdout)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stdout))
|
||||
return null;
|
||||
|
||||
foreach (Match m in NumberRegex.Matches(stdout))
|
||||
{
|
||||
if (int.TryParse(m.Value, out var n) && n is >= 0 and <= 100)
|
||||
return n;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public sealed class EctoolRunner
|
||||
{
|
||||
private static readonly Regex NumberRegex = new(@"\d+", RegexOptions.Compiled);
|
||||
|
||||
public static (bool ok, string stdout, string stderr) RunSync(
|
||||
string exePath,
|
||||
IReadOnlyList<string> args,
|
||||
int timeoutMs = 15_000) =>
|
||||
RunAsync(exePath, args, CancellationToken.None, timeoutMs).GetAwaiter().GetResult();
|
||||
|
||||
public static async Task<(bool ok, string stdout, string stderr)> RunAsync(
|
||||
string exePath,
|
||||
IReadOnlyList<string> args,
|
||||
CancellationToken ct = default,
|
||||
int timeoutMs = 15_000)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
|
||||
return (false, "", "ectool executable not found.");
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
foreach (var a in args)
|
||||
psi.ArgumentList.Add(a);
|
||||
|
||||
using var proc = new Process { StartInfo = psi };
|
||||
try
|
||||
{
|
||||
proc.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, "", ex.Message);
|
||||
}
|
||||
|
||||
var stdoutTask = proc.StandardOutput.ReadToEndAsync(ct);
|
||||
var stderrTask = proc.StandardError.ReadToEndAsync(ct);
|
||||
var waitTask = proc.WaitForExitAsync(ct);
|
||||
var completed = await Task.WhenAny(waitTask, Task.Delay(timeoutMs, ct)).ConfigureAwait(false);
|
||||
if (completed != waitTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
proc.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
return (false, "", "ectool timed out.");
|
||||
}
|
||||
|
||||
await waitTask.ConfigureAwait(false);
|
||||
var stdout = await stdoutTask.ConfigureAwait(false);
|
||||
var stderr = await stderrTask.ConfigureAwait(false);
|
||||
var ok = proc.ExitCode == 0;
|
||||
return (ok, stdout, stderr);
|
||||
}
|
||||
|
||||
/// <summary>Pick the most plausible RPM (typically 500–20000) from ectool text output.</summary>
|
||||
public static int? TryParseFanRpm(string stdout)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stdout))
|
||||
return null;
|
||||
|
||||
int? best = null;
|
||||
foreach (Match m in NumberRegex.Matches(stdout))
|
||||
{
|
||||
if (!int.TryParse(m.Value, out var n))
|
||||
continue;
|
||||
if (n is < 400 or > 50_000)
|
||||
continue;
|
||||
// Prefer larger numbers in typical fan range (avoid fan index "0")
|
||||
if (n >= 800 && (best == null || n > best))
|
||||
best = n;
|
||||
}
|
||||
|
||||
if (best != null)
|
||||
return best;
|
||||
|
||||
foreach (Match m in NumberRegex.Matches(stdout))
|
||||
{
|
||||
if (int.TryParse(m.Value, out var n) && n is >= 0 and <= 50_000)
|
||||
return n;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Pick duty percent (0-100) from ectool text output.</summary>
|
||||
public static int? TryParseFanDuty(string stdout)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stdout))
|
||||
return null;
|
||||
|
||||
foreach (Match m in NumberRegex.Matches(stdout))
|
||||
{
|
||||
if (int.TryParse(m.Value, out var n) && n is >= 0 and <= 100)
|
||||
return n;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,296 +1,296 @@
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
/// <summary>
|
||||
/// Background loop: LHM temp → curve → ectool fanduty; ectool RPM; fail-safe handling.
|
||||
/// </summary>
|
||||
public sealed class FanController : IDisposable
|
||||
{
|
||||
private readonly object _cfgLock = new();
|
||||
private AppConfig _config;
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loop;
|
||||
private CpuTempReader? _cpu;
|
||||
private volatile bool _paused;
|
||||
private int _consecutiveTempErrors;
|
||||
private int _cycleIndex;
|
||||
private bool _failSafeActive;
|
||||
private byte? _lastCommandedDuty;
|
||||
private byte _rampStartDuty;
|
||||
private byte _rampTargetDuty;
|
||||
private int _rampStepIndex;
|
||||
|
||||
public FanController(AppConfig initialConfig)
|
||||
{
|
||||
_config = initialConfig;
|
||||
}
|
||||
|
||||
public event EventHandler<FanSampleEventArgs>? Sample;
|
||||
|
||||
public bool Paused
|
||||
{
|
||||
get => _paused;
|
||||
set => _paused = value;
|
||||
}
|
||||
|
||||
public void UpdateConfig(AppConfig config)
|
||||
{
|
||||
lock (_cfgLock)
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
_cpu = new CpuTempReader();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
_loop = Task.Run(() => LoopAsync(token), token);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
|
||||
private void StopInternal(bool waitForLoop)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
|
||||
if (waitForLoop && _loop != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_loop.Wait(8000);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
}
|
||||
|
||||
_cpu?.Dispose();
|
||||
_cpu = null;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_loop = null;
|
||||
}
|
||||
|
||||
public async Task RestoreAutoFanAsync()
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
if (!File.Exists(cfg.EctoolPath))
|
||||
return;
|
||||
|
||||
var (ok, _, err) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs)
|
||||
.ConfigureAwait(false);
|
||||
if (!ok)
|
||||
DebugLog($"autofanctrl: {err}");
|
||||
}
|
||||
|
||||
private async Task LoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
var mode = string.Equals(cfg.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase)
|
||||
? CpuTempAggregation.MaxCore
|
||||
: CpuTempAggregation.AverageCore;
|
||||
|
||||
double? tempC = null;
|
||||
try
|
||||
{
|
||||
tempC = _cpu?.ReadCpuTemp(mode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugLog($"LHM: {ex.Message}");
|
||||
}
|
||||
|
||||
if (tempC == null)
|
||||
_consecutiveTempErrors++;
|
||||
else
|
||||
_consecutiveTempErrors = 0;
|
||||
|
||||
if (_consecutiveTempErrors >= cfg.FailSafeAfterConsecutiveErrors)
|
||||
_failSafeActive = true;
|
||||
else if (tempC != null)
|
||||
_failSafeActive = false;
|
||||
|
||||
byte? targetDuty = null;
|
||||
string? ectoolMsg = null;
|
||||
int? rpm = null;
|
||||
int? actualDuty = null;
|
||||
|
||||
if (_paused)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "paused";
|
||||
}
|
||||
else if (_failSafeActive)
|
||||
{
|
||||
ClearRampState();
|
||||
if (cfg.FailSafeRestoreAutoFan)
|
||||
{
|
||||
var (okA, _, eA) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs, ct)
|
||||
.ConfigureAwait(false);
|
||||
targetDuty = null;
|
||||
ectoolMsg = okA ? "fail-safe: autofanctrl" : $"fail-safe autofanctrl: {eA}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = (byte)Math.Clamp(cfg.FailSafeFanPercent, 0, 100);
|
||||
targetDuty = d;
|
||||
_lastCommandedDuty = d;
|
||||
var (okF, _, eF) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okF ? $"fail-safe fanduty {d}%" : $"fail-safe: {eF}";
|
||||
}
|
||||
}
|
||||
else if (tempC == null)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "no CPU temperature (holding EC state)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var curveTarget = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
|
||||
var output = ComputeRampOutput(curveTarget, cfg);
|
||||
targetDuty = output;
|
||||
_lastCommandedDuty = output;
|
||||
var (okD, _, eD) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", output.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okD ? $"fanduty {output}%" : eD;
|
||||
}
|
||||
|
||||
_cycleIndex++;
|
||||
if (cfg.FanRpmPollEveryNCycles > 0 &&
|
||||
_cycleIndex % cfg.FanRpmPollEveryNCycles == 0 &&
|
||||
File.Exists(cfg.EctoolPath))
|
||||
{
|
||||
var (okR, stdoutR, errR) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanRpmArgs, ct).ConfigureAwait(false);
|
||||
if (okR)
|
||||
rpm = EctoolRunner.TryParseFanRpm(stdoutR);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"rpm: {errR}" : $"{ectoolMsg}; rpm: {errR}";
|
||||
|
||||
if (cfg.FanDutyArgs is { Length: > 0 })
|
||||
{
|
||||
var (okDuty, stdoutDuty, errDuty) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanDutyArgs, ct).ConfigureAwait(false);
|
||||
if (okDuty)
|
||||
actualDuty = EctoolRunner.TryParseFanDuty(stdoutDuty);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"duty: {errDuty}" : $"{ectoolMsg}; duty: {errDuty}";
|
||||
}
|
||||
}
|
||||
|
||||
RaiseSample(new FanSampleEventArgs
|
||||
{
|
||||
Time = DateTime.Now,
|
||||
TempC = tempC,
|
||||
TargetDutyPercent = targetDuty,
|
||||
ActualDutyPercent = actualDuty,
|
||||
Rpm = rpm,
|
||||
LastEctoolMessage = ectoolMsg,
|
||||
Paused = _paused,
|
||||
FailSafe = _failSafeActive
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Math.Clamp(cfg.PollIntervalMs, 250, 60_000), ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseSample(FanSampleEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Sample?.Invoke(this, e);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* UI must not kill loop */
|
||||
}
|
||||
}
|
||||
|
||||
private static void DebugLog(string msg)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ChromeboxFanControl] {msg}");
|
||||
}
|
||||
|
||||
private void ClearRampState()
|
||||
{
|
||||
_rampStepIndex = 0;
|
||||
}
|
||||
|
||||
private byte ComputeRampOutput(byte targetDuty, AppConfig cfg)
|
||||
{
|
||||
var n = Math.Clamp(cfg.RampUpSteps, 1, 10);
|
||||
var minDelta = Math.Clamp(cfg.RampUpMinDeltaPercent, 0, 50);
|
||||
|
||||
if (_lastCommandedDuty == null)
|
||||
return targetDuty;
|
||||
|
||||
var curr = _lastCommandedDuty.Value;
|
||||
var delta = Math.Abs(targetDuty - curr);
|
||||
|
||||
if (targetDuty == curr)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (delta < minDelta)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (_rampStepIndex == 0 || targetDuty != _rampTargetDuty)
|
||||
{
|
||||
_rampStartDuty = curr;
|
||||
_rampTargetDuty = targetDuty;
|
||||
_rampStepIndex = 1;
|
||||
}
|
||||
|
||||
if (_rampStepIndex >= n)
|
||||
{
|
||||
ClearRampState();
|
||||
return _rampTargetDuty;
|
||||
}
|
||||
|
||||
var step = (int)Math.Round((_rampTargetDuty - _rampStartDuty) * (double)_rampStepIndex / n);
|
||||
var output = (byte)Math.Clamp(_rampStartDuty + step, 0, 100);
|
||||
_rampStepIndex++;
|
||||
return output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
}
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
/// <summary>
|
||||
/// Background loop: LHM temp → curve → ectool fanduty; ectool RPM; fail-safe handling.
|
||||
/// </summary>
|
||||
public sealed class FanController : IDisposable
|
||||
{
|
||||
private readonly object _cfgLock = new();
|
||||
private AppConfig _config;
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loop;
|
||||
private CpuTempReader? _cpu;
|
||||
private volatile bool _paused;
|
||||
private int _consecutiveTempErrors;
|
||||
private int _cycleIndex;
|
||||
private bool _failSafeActive;
|
||||
private byte? _lastCommandedDuty;
|
||||
private byte _rampStartDuty;
|
||||
private byte _rampTargetDuty;
|
||||
private int _rampStepIndex;
|
||||
|
||||
public FanController(AppConfig initialConfig)
|
||||
{
|
||||
_config = initialConfig;
|
||||
}
|
||||
|
||||
public event EventHandler<FanSampleEventArgs>? Sample;
|
||||
|
||||
public bool Paused
|
||||
{
|
||||
get => _paused;
|
||||
set => _paused = value;
|
||||
}
|
||||
|
||||
public void UpdateConfig(AppConfig config)
|
||||
{
|
||||
lock (_cfgLock)
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
_cpu = new CpuTempReader();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
_loop = Task.Run(() => LoopAsync(token), token);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
|
||||
private void StopInternal(bool waitForLoop)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
|
||||
if (waitForLoop && _loop != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_loop.Wait(8000);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
}
|
||||
|
||||
_cpu?.Dispose();
|
||||
_cpu = null;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_loop = null;
|
||||
}
|
||||
|
||||
public async Task RestoreAutoFanAsync()
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
if (!File.Exists(cfg.EctoolPath))
|
||||
return;
|
||||
|
||||
var (ok, _, err) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs)
|
||||
.ConfigureAwait(false);
|
||||
if (!ok)
|
||||
DebugLog($"autofanctrl: {err}");
|
||||
}
|
||||
|
||||
private async Task LoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
var mode = string.Equals(cfg.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase)
|
||||
? CpuTempAggregation.MaxCore
|
||||
: CpuTempAggregation.AverageCore;
|
||||
|
||||
double? tempC = null;
|
||||
try
|
||||
{
|
||||
tempC = _cpu?.ReadCpuTemp(mode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugLog($"LHM: {ex.Message}");
|
||||
}
|
||||
|
||||
if (tempC == null)
|
||||
_consecutiveTempErrors++;
|
||||
else
|
||||
_consecutiveTempErrors = 0;
|
||||
|
||||
if (_consecutiveTempErrors >= cfg.FailSafeAfterConsecutiveErrors)
|
||||
_failSafeActive = true;
|
||||
else if (tempC != null)
|
||||
_failSafeActive = false;
|
||||
|
||||
byte? targetDuty = null;
|
||||
string? ectoolMsg = null;
|
||||
int? rpm = null;
|
||||
int? actualDuty = null;
|
||||
|
||||
if (_paused)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "paused";
|
||||
}
|
||||
else if (_failSafeActive)
|
||||
{
|
||||
ClearRampState();
|
||||
if (cfg.FailSafeRestoreAutoFan)
|
||||
{
|
||||
var (okA, _, eA) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs, ct)
|
||||
.ConfigureAwait(false);
|
||||
targetDuty = null;
|
||||
ectoolMsg = okA ? "fail-safe: autofanctrl" : $"fail-safe autofanctrl: {eA}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = (byte)Math.Clamp(cfg.FailSafeFanPercent, 0, 100);
|
||||
targetDuty = d;
|
||||
_lastCommandedDuty = d;
|
||||
var (okF, _, eF) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okF ? $"fail-safe fanduty {d}%" : $"fail-safe: {eF}";
|
||||
}
|
||||
}
|
||||
else if (tempC == null)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "no CPU temperature (holding EC state)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var curveTarget = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
|
||||
var output = ComputeRampOutput(curveTarget, cfg);
|
||||
targetDuty = output;
|
||||
_lastCommandedDuty = output;
|
||||
var (okD, _, eD) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", output.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okD ? $"fanduty {output}%" : eD;
|
||||
}
|
||||
|
||||
_cycleIndex++;
|
||||
if (cfg.FanRpmPollEveryNCycles > 0 &&
|
||||
_cycleIndex % cfg.FanRpmPollEveryNCycles == 0 &&
|
||||
File.Exists(cfg.EctoolPath))
|
||||
{
|
||||
var (okR, stdoutR, errR) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanRpmArgs, ct).ConfigureAwait(false);
|
||||
if (okR)
|
||||
rpm = EctoolRunner.TryParseFanRpm(stdoutR);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"rpm: {errR}" : $"{ectoolMsg}; rpm: {errR}";
|
||||
|
||||
if (cfg.FanDutyArgs is { Length: > 0 })
|
||||
{
|
||||
var (okDuty, stdoutDuty, errDuty) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanDutyArgs, ct).ConfigureAwait(false);
|
||||
if (okDuty)
|
||||
actualDuty = EctoolRunner.TryParseFanDuty(stdoutDuty);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"duty: {errDuty}" : $"{ectoolMsg}; duty: {errDuty}";
|
||||
}
|
||||
}
|
||||
|
||||
RaiseSample(new FanSampleEventArgs
|
||||
{
|
||||
Time = DateTime.Now,
|
||||
TempC = tempC,
|
||||
TargetDutyPercent = targetDuty,
|
||||
ActualDutyPercent = actualDuty,
|
||||
Rpm = rpm,
|
||||
LastEctoolMessage = ectoolMsg,
|
||||
Paused = _paused,
|
||||
FailSafe = _failSafeActive
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Math.Clamp(cfg.PollIntervalMs, 250, 60_000), ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseSample(FanSampleEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Sample?.Invoke(this, e);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* UI must not kill loop */
|
||||
}
|
||||
}
|
||||
|
||||
private static void DebugLog(string msg)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ChromeboxFanControl] {msg}");
|
||||
}
|
||||
|
||||
private void ClearRampState()
|
||||
{
|
||||
_rampStepIndex = 0;
|
||||
}
|
||||
|
||||
private byte ComputeRampOutput(byte targetDuty, AppConfig cfg)
|
||||
{
|
||||
var n = Math.Clamp(cfg.RampUpSteps, 1, 10);
|
||||
var minDelta = Math.Clamp(cfg.RampUpMinDeltaPercent, 0, 50);
|
||||
|
||||
if (_lastCommandedDuty == null)
|
||||
return targetDuty;
|
||||
|
||||
var curr = _lastCommandedDuty.Value;
|
||||
var delta = Math.Abs(targetDuty - curr);
|
||||
|
||||
if (targetDuty == curr)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (delta < minDelta)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (_rampStepIndex == 0 || targetDuty != _rampTargetDuty)
|
||||
{
|
||||
_rampStartDuty = curr;
|
||||
_rampTargetDuty = targetDuty;
|
||||
_rampStepIndex = 1;
|
||||
}
|
||||
|
||||
if (_rampStepIndex >= n)
|
||||
{
|
||||
ClearRampState();
|
||||
return _rampTargetDuty;
|
||||
}
|
||||
|
||||
var step = (int)Math.Round((_rampTargetDuty - _rampStartDuty) * (double)_rampStepIndex / n);
|
||||
var output = (byte)Math.Clamp(_rampStartDuty + step, 0, 100);
|
||||
_rampStepIndex++;
|
||||
return output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
/// <summary>
|
||||
/// Fan curve: 14 control points at 0 °C and 40–100 °C every 5 °C, linear interpolation.
|
||||
/// </summary>
|
||||
public static class FanCurve
|
||||
{
|
||||
private const int PointCount = 14;
|
||||
|
||||
/// <summary>Temperature breakpoints (°C): 0, 40, 45, 50, …, 100.</summary>
|
||||
public static readonly int[] TempBreakpoints = [0, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100];
|
||||
|
||||
/// <param name="tempC">CPU temperature in °C.</param>
|
||||
/// <param name="curve">Exactly 14 values 0–100 (fan duty).</param>
|
||||
public static byte CalculateFanPercent(double tempC, ReadOnlySpan<int> curve)
|
||||
{
|
||||
if (curve.Length != PointCount)
|
||||
throw new ArgumentException($"Curve must contain exactly {PointCount} points.", nameof(curve));
|
||||
|
||||
var t = Math.Clamp(tempC, 0, 100);
|
||||
if (t <= TempBreakpoints[0])
|
||||
return (byte)Math.Clamp(curve[0], 0, 100);
|
||||
if (t >= TempBreakpoints[PointCount - 1])
|
||||
return (byte)Math.Clamp(curve[PointCount - 1], 0, 100);
|
||||
|
||||
for (var i = 0; i < PointCount - 1; i++)
|
||||
{
|
||||
var t0 = TempBreakpoints[i];
|
||||
var t1 = TempBreakpoints[i + 1];
|
||||
if (t >= t0 && t <= t1)
|
||||
{
|
||||
var d0 = Math.Clamp(curve[i], 0, 100);
|
||||
var d1 = Math.Clamp(curve[i + 1], 0, 100);
|
||||
var frac = (t - t0) / (t1 - t0);
|
||||
return (byte)Math.Round(d0 + (d1 - d0) * frac);
|
||||
}
|
||||
}
|
||||
|
||||
return (byte)Math.Clamp(curve[PointCount - 1], 0, 100);
|
||||
}
|
||||
}
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
/// <summary>
|
||||
/// Fan curve: 14 control points at 0 °C and 40–100 °C every 5 °C, linear interpolation.
|
||||
/// </summary>
|
||||
public static class FanCurve
|
||||
{
|
||||
private const int PointCount = 14;
|
||||
|
||||
/// <summary>Temperature breakpoints (°C): 0, 40, 45, 50, …, 100.</summary>
|
||||
public static readonly int[] TempBreakpoints = [0, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100];
|
||||
|
||||
/// <param name="tempC">CPU temperature in °C.</param>
|
||||
/// <param name="curve">Exactly 14 values 0–100 (fan duty).</param>
|
||||
public static byte CalculateFanPercent(double tempC, ReadOnlySpan<int> curve)
|
||||
{
|
||||
if (curve.Length != PointCount)
|
||||
throw new ArgumentException($"Curve must contain exactly {PointCount} points.", nameof(curve));
|
||||
|
||||
var t = Math.Clamp(tempC, 0, 100);
|
||||
if (t <= TempBreakpoints[0])
|
||||
return (byte)Math.Clamp(curve[0], 0, 100);
|
||||
if (t >= TempBreakpoints[PointCount - 1])
|
||||
return (byte)Math.Clamp(curve[PointCount - 1], 0, 100);
|
||||
|
||||
for (var i = 0; i < PointCount - 1; i++)
|
||||
{
|
||||
var t0 = TempBreakpoints[i];
|
||||
var t1 = TempBreakpoints[i + 1];
|
||||
if (t >= t0 && t <= t1)
|
||||
{
|
||||
var d0 = Math.Clamp(curve[i], 0, 100);
|
||||
var d1 = Math.Clamp(curve[i + 1], 0, 100);
|
||||
var frac = (t - t0) / (t1 - t0);
|
||||
return (byte)Math.Round(d0 + (d1 - d0) * frac);
|
||||
}
|
||||
}
|
||||
|
||||
return (byte)Math.Clamp(curve[PointCount - 1], 0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public sealed class FanSampleEventArgs : EventArgs
|
||||
{
|
||||
public DateTime Time { get; init; }
|
||||
public double? TempC { get; init; }
|
||||
/// <summary>Desired fan duty from curve or fail-safe; null when paused or not applicable.</summary>
|
||||
public byte? TargetDutyPercent { get; init; }
|
||||
/// <summary>Actual duty read from ectool when FanDutyArgs configured; else null.</summary>
|
||||
public int? ActualDutyPercent { get; init; }
|
||||
public int? Rpm { get; init; }
|
||||
public string? LastEctoolMessage { get; init; }
|
||||
public bool Paused { get; init; }
|
||||
public bool FailSafe { get; init; }
|
||||
}
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
public sealed class FanSampleEventArgs : EventArgs
|
||||
{
|
||||
public DateTime Time { get; init; }
|
||||
public double? TempC { get; init; }
|
||||
/// <summary>Desired fan duty from curve or fail-safe; null when paused or not applicable.</summary>
|
||||
public byte? TargetDutyPercent { get; init; }
|
||||
/// <summary>Actual duty read from ectool when FanDutyArgs configured; else null.</summary>
|
||||
public int? ActualDutyPercent { get; init; }
|
||||
public int? Rpm { get; init; }
|
||||
public string? LastEctoolMessage { get; init; }
|
||||
public bool Paused { get; init; }
|
||||
public bool FailSafe { get; init; }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,27 @@
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
ApplicationConfiguration.Initialize();
|
||||
|
||||
var config = AppConfig.LoadMerged();
|
||||
var lang = config.Language;
|
||||
if (string.IsNullOrEmpty(lang) || lang.Equals("auto", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var culture = System.Globalization.CultureInfo.CurrentUICulture;
|
||||
if (culture.Name is "zh-TW" or "zh-HK" or "zh-MO" or "zh-Hant")
|
||||
lang = "zh-Hant";
|
||||
else if (culture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
|
||||
lang = "zh-Hans";
|
||||
else
|
||||
lang = "en";
|
||||
}
|
||||
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(lang);
|
||||
|
||||
Application.Run(new MainForm());
|
||||
}
|
||||
}
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
ApplicationConfiguration.Initialize();
|
||||
|
||||
var config = AppConfig.LoadMerged();
|
||||
var lang = config.Language;
|
||||
if (string.IsNullOrEmpty(lang) || lang.Equals("auto", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var culture = System.Globalization.CultureInfo.CurrentUICulture;
|
||||
if (culture.Name is "zh-TW" or "zh-HK" or "zh-MO" or "zh-Hant")
|
||||
lang = "zh-Hant";
|
||||
else if (culture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
|
||||
lang = "zh-Hans";
|
||||
else
|
||||
lang = "en";
|
||||
}
|
||||
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(lang);
|
||||
|
||||
Application.Run(new MainForm());
|
||||
}
|
||||
}
|
||||
|
||||
130
ChromeboxFanControl/Properties/Resources.Designer.cs
generated
130
ChromeboxFanControl/Properties/Resources.Designer.cs
generated
@@ -1,65 +1,65 @@
|
||||
using System.Resources;
|
||||
|
||||
namespace ChromeboxFanControl.Properties;
|
||||
|
||||
internal static class Resources
|
||||
{
|
||||
private static readonly ResourceManager _rm = new(
|
||||
"ChromeboxFanControl.Properties.Resources",
|
||||
typeof(Resources).Assembly);
|
||||
|
||||
public static string AppTitle => _rm.GetString("AppTitle") ?? "Chromebox Fan Control";
|
||||
public static string TabMonitor => _rm.GetString("TabMonitor") ?? "Monitoring";
|
||||
public static string TabCurve => _rm.GetString("TabCurve") ?? "Curve";
|
||||
public static string TabAdvanced => _rm.GetString("TabAdvanced") ?? "Advanced";
|
||||
public static string StatusStarting => _rm.GetString("StatusStarting") ?? "Starting…";
|
||||
public static string StatusRunning => _rm.GetString("StatusRunning") ?? "Running";
|
||||
public static string StatusPaused => _rm.GetString("StatusPaused") ?? "Paused";
|
||||
public static string StatusFailSafe => _rm.GetString("StatusFailSafe") ?? "Fail-safe mode";
|
||||
public static string GridColTemp => _rm.GetString("GridColTemp") ?? "Temp (°C)";
|
||||
public static string GridColDuty => _rm.GetString("GridColDuty") ?? "Duty 0–100";
|
||||
public static string BtnSaveApply => _rm.GetString("BtnSaveApply") ?? "Save & Apply";
|
||||
public static string BtnRestoreDefault => _rm.GetString("BtnRestoreDefault") ?? "Restore Default";
|
||||
public static string LblEctoolPath => _rm.GetString("LblEctoolPath") ?? "ectool path";
|
||||
public static string LblPollInterval => _rm.GetString("LblPollInterval") ?? "Poll interval (ms)";
|
||||
public static string LblRpmEvery => _rm.GetString("LblRpmEvery") ?? "Read RPM every N";
|
||||
public static string LblFanRpmArgs => _rm.GetString("LblFanRpmArgs") ?? "RPM args";
|
||||
public static string LblFanDutyArgs => _rm.GetString("LblFanDutyArgs") ?? "Duty args";
|
||||
public static string LblAutoFanArgs => _rm.GetString("LblAutoFanArgs") ?? "Auto fan args";
|
||||
public static string LblTempSource => _rm.GetString("LblTempSource") ?? "Temp source";
|
||||
public static string LblFailCount => _rm.GetString("LblFailCount") ?? "Fail count";
|
||||
public static string LblFailPct => _rm.GetString("LblFailPct") ?? "Fail-safe %";
|
||||
public static string ChkFailAuto => _rm.GetString("ChkFailAuto") ?? "Use autofanctrl";
|
||||
public static string LblFailStrategy => _rm.GetString("LblFailStrategy") ?? "Fail-safe strategy";
|
||||
public static string LblChartMin => _rm.GetString("LblChartMin") ?? "Chart minutes";
|
||||
public static string LblChartPoints => _rm.GetString("LblChartPoints") ?? "Chart points";
|
||||
public static string LblRampUpSteps => _rm.GetString("LblRampUpSteps") ?? "Ramp-up steps";
|
||||
public static string LblRampUpMinDeltaPercent => _rm.GetString("LblRampUpMinDeltaPercent") ?? "Min ramp delta (%)";
|
||||
public static string BtnSaveAdvanced => _rm.GetString("BtnSaveAdvanced") ?? "Save Advanced";
|
||||
public static string TrayOpen => _rm.GetString("TrayOpen") ?? "Show window";
|
||||
public static string TrayPause => _rm.GetString("TrayPause") ?? "Pause";
|
||||
public static string TrayResume => _rm.GetString("TrayResume") ?? "Resume";
|
||||
public static string TrayAbout => _rm.GetString("TrayAbout") ?? "About";
|
||||
public static string TrayExit => _rm.GetString("TrayExit") ?? "Exit";
|
||||
public static string AboutMessage => _rm.GetString("AboutMessage") ?? "";
|
||||
public static string ErrRpmArgsEmpty => _rm.GetString("ErrRpmArgsEmpty") ?? "RPM args required.";
|
||||
public static string ErrAutoFanArgsEmpty => _rm.GetString("ErrAutoFanArgsEmpty") ?? "Auto fan args required.";
|
||||
public static string MsgSaved => _rm.GetString("MsgSaved") ?? "Saved.";
|
||||
public static string LabelTemp => _rm.GetString("LabelTemp") ?? "Temp:";
|
||||
public static string LabelTargetDuty => _rm.GetString("LabelTargetDuty") ?? "Target duty:";
|
||||
public static string LabelRpm => _rm.GetString("LabelRpm") ?? "RPM:";
|
||||
public static string ChartTemp => _rm.GetString("ChartTemp") ?? "Temperature (°C)";
|
||||
public static string ChartTime => _rm.GetString("ChartTime") ?? "Time (s)";
|
||||
public static string ChartDuty => _rm.GetString("ChartDuty") ?? "Duty (%)";
|
||||
public static string ChartRpm => _rm.GetString("ChartRpm") ?? "RPM";
|
||||
public static string LegendCelsius => _rm.GetString("LegendCelsius") ?? "°C";
|
||||
public static string LegendDuty => _rm.GetString("LegendDuty") ?? "Duty %";
|
||||
public static string CurveChartDuty => _rm.GetString("CurveChartDuty") ?? "Duty (%)";
|
||||
public static string CurveChartTemp => _rm.GetString("CurveChartTemp") ?? "Temp (°C)";
|
||||
public static string LblLanguage => _rm.GetString("LblLanguage") ?? "Language";
|
||||
public static string LangAuto => _rm.GetString("LangAuto") ?? "Auto";
|
||||
public static string LangEn => _rm.GetString("LangEn") ?? "English";
|
||||
public static string LangZhHans => _rm.GetString("LangZhHans") ?? "Simplified Chinese";
|
||||
public static string LangZhHant => _rm.GetString("LangZhHant") ?? "Traditional Chinese";
|
||||
public static string MsgRestartToApplyLanguage => _rm.GetString("MsgRestartToApplyLanguage") ?? "Restart to apply.";
|
||||
}
|
||||
using System.Resources;
|
||||
|
||||
namespace ChromeboxFanControl.Properties;
|
||||
|
||||
internal static class Resources
|
||||
{
|
||||
private static readonly ResourceManager _rm = new(
|
||||
"ChromeboxFanControl.Properties.Resources",
|
||||
typeof(Resources).Assembly);
|
||||
|
||||
public static string AppTitle => _rm.GetString("AppTitle") ?? "Chromebox Fan Control";
|
||||
public static string TabMonitor => _rm.GetString("TabMonitor") ?? "Monitoring";
|
||||
public static string TabCurve => _rm.GetString("TabCurve") ?? "Curve";
|
||||
public static string TabAdvanced => _rm.GetString("TabAdvanced") ?? "Advanced";
|
||||
public static string StatusStarting => _rm.GetString("StatusStarting") ?? "Starting…";
|
||||
public static string StatusRunning => _rm.GetString("StatusRunning") ?? "Running";
|
||||
public static string StatusPaused => _rm.GetString("StatusPaused") ?? "Paused";
|
||||
public static string StatusFailSafe => _rm.GetString("StatusFailSafe") ?? "Fail-safe mode";
|
||||
public static string GridColTemp => _rm.GetString("GridColTemp") ?? "Temp (°C)";
|
||||
public static string GridColDuty => _rm.GetString("GridColDuty") ?? "Duty 0–100";
|
||||
public static string BtnSaveApply => _rm.GetString("BtnSaveApply") ?? "Save & Apply";
|
||||
public static string BtnRestoreDefault => _rm.GetString("BtnRestoreDefault") ?? "Restore Default";
|
||||
public static string LblEctoolPath => _rm.GetString("LblEctoolPath") ?? "ectool path";
|
||||
public static string LblPollInterval => _rm.GetString("LblPollInterval") ?? "Poll interval (ms)";
|
||||
public static string LblRpmEvery => _rm.GetString("LblRpmEvery") ?? "Read RPM every N";
|
||||
public static string LblFanRpmArgs => _rm.GetString("LblFanRpmArgs") ?? "RPM args";
|
||||
public static string LblFanDutyArgs => _rm.GetString("LblFanDutyArgs") ?? "Duty args";
|
||||
public static string LblAutoFanArgs => _rm.GetString("LblAutoFanArgs") ?? "Auto fan args";
|
||||
public static string LblTempSource => _rm.GetString("LblTempSource") ?? "Temp source";
|
||||
public static string LblFailCount => _rm.GetString("LblFailCount") ?? "Fail count";
|
||||
public static string LblFailPct => _rm.GetString("LblFailPct") ?? "Fail-safe %";
|
||||
public static string ChkFailAuto => _rm.GetString("ChkFailAuto") ?? "Use autofanctrl";
|
||||
public static string LblFailStrategy => _rm.GetString("LblFailStrategy") ?? "Fail-safe strategy";
|
||||
public static string LblChartMin => _rm.GetString("LblChartMin") ?? "Chart minutes";
|
||||
public static string LblChartPoints => _rm.GetString("LblChartPoints") ?? "Chart points";
|
||||
public static string LblRampUpSteps => _rm.GetString("LblRampUpSteps") ?? "Ramp-up steps";
|
||||
public static string LblRampUpMinDeltaPercent => _rm.GetString("LblRampUpMinDeltaPercent") ?? "Min ramp delta (%)";
|
||||
public static string BtnSaveAdvanced => _rm.GetString("BtnSaveAdvanced") ?? "Save Advanced";
|
||||
public static string TrayOpen => _rm.GetString("TrayOpen") ?? "Show window";
|
||||
public static string TrayPause => _rm.GetString("TrayPause") ?? "Pause";
|
||||
public static string TrayResume => _rm.GetString("TrayResume") ?? "Resume";
|
||||
public static string TrayAbout => _rm.GetString("TrayAbout") ?? "About";
|
||||
public static string TrayExit => _rm.GetString("TrayExit") ?? "Exit";
|
||||
public static string AboutMessage => _rm.GetString("AboutMessage") ?? "";
|
||||
public static string ErrRpmArgsEmpty => _rm.GetString("ErrRpmArgsEmpty") ?? "RPM args required.";
|
||||
public static string ErrAutoFanArgsEmpty => _rm.GetString("ErrAutoFanArgsEmpty") ?? "Auto fan args required.";
|
||||
public static string MsgSaved => _rm.GetString("MsgSaved") ?? "Saved.";
|
||||
public static string LabelTemp => _rm.GetString("LabelTemp") ?? "Temp:";
|
||||
public static string LabelTargetDuty => _rm.GetString("LabelTargetDuty") ?? "Target duty:";
|
||||
public static string LabelRpm => _rm.GetString("LabelRpm") ?? "RPM:";
|
||||
public static string ChartTemp => _rm.GetString("ChartTemp") ?? "Temperature (°C)";
|
||||
public static string ChartTime => _rm.GetString("ChartTime") ?? "Time (s)";
|
||||
public static string ChartDuty => _rm.GetString("ChartDuty") ?? "Duty (%)";
|
||||
public static string ChartRpm => _rm.GetString("ChartRpm") ?? "RPM";
|
||||
public static string LegendCelsius => _rm.GetString("LegendCelsius") ?? "°C";
|
||||
public static string LegendDuty => _rm.GetString("LegendDuty") ?? "Duty %";
|
||||
public static string CurveChartDuty => _rm.GetString("CurveChartDuty") ?? "Duty (%)";
|
||||
public static string CurveChartTemp => _rm.GetString("CurveChartTemp") ?? "Temp (°C)";
|
||||
public static string LblLanguage => _rm.GetString("LblLanguage") ?? "Language";
|
||||
public static string LangAuto => _rm.GetString("LangAuto") ?? "Auto";
|
||||
public static string LangEn => _rm.GetString("LangEn") ?? "English";
|
||||
public static string LangZhHans => _rm.GetString("LangZhHans") ?? "Simplified Chinese";
|
||||
public static string LangZhHant => _rm.GetString("LangZhHant") ?? "Traditional Chinese";
|
||||
public static string MsgRestartToApplyLanguage => _rm.GetString("MsgRestartToApplyLanguage") ?? "Restart to apply.";
|
||||
}
|
||||
|
||||
@@ -1,119 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTitle" xml:space="preserve"><value>Chromebox Fan Control</value></data>
|
||||
<data name="TabMonitor" xml:space="preserve"><value>Monitoring</value></data>
|
||||
<data name="TabCurve" xml:space="preserve"><value>Curve</value></data>
|
||||
<data name="TabAdvanced" xml:space="preserve"><value>Advanced</value></data>
|
||||
<data name="StatusStarting" xml:space="preserve"><value>Starting…</value></data>
|
||||
<data name="StatusRunning" xml:space="preserve"><value>Running</value></data>
|
||||
<data name="StatusPaused" xml:space="preserve"><value>Paused</value></data>
|
||||
<data name="StatusFailSafe" xml:space="preserve"><value>Fail-safe mode</value></data>
|
||||
<data name="GridColTemp" xml:space="preserve"><value>Temp (°C)</value></data>
|
||||
<data name="GridColDuty" xml:space="preserve"><value>Duty 0–100</value></data>
|
||||
<data name="BtnSaveApply" xml:space="preserve"><value>Save & Apply</value></data>
|
||||
<data name="BtnRestoreDefault" xml:space="preserve"><value>Restore Default Curve</value></data>
|
||||
<data name="LblEctoolPath" xml:space="preserve"><value>ectool path</value></data>
|
||||
<data name="LblPollInterval" xml:space="preserve"><value>Poll interval (ms)</value></data>
|
||||
<data name="LblRpmEvery" xml:space="preserve"><value>Read RPM every N cycles</value></data>
|
||||
<data name="LblFanRpmArgs" xml:space="preserve"><value>RPM args (space-separated)</value></data>
|
||||
<data name="LblFanDutyArgs" xml:space="preserve"><value>Duty args (empty=skip)</value></data>
|
||||
<data name="LblAutoFanArgs" xml:space="preserve"><value>Restore auto fan args</value></data>
|
||||
<data name="LblTempSource" xml:space="preserve"><value>Temp source</value></data>
|
||||
<data name="LblFailCount" xml:space="preserve"><value>Consecutive errors before fail-safe</value></data>
|
||||
<data name="LblFailPct" xml:space="preserve"><value>Fail-safe fixed duty (%)</value></data>
|
||||
<data name="ChkFailAuto" xml:space="preserve"><value>Use autofanctrl in fail-safe (not fixed duty)</value></data>
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>Fail-safe strategy</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>Chart history (minutes)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>Chart max points</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>Ramp-up steps</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>Min ramp delta (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>Save Advanced Settings</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>Show window</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>Pause control</value></data>
|
||||
<data name="TrayResume" xml:space="preserve"><value>Resume control</value></data>
|
||||
<data name="TrayAbout" xml:space="preserve"><value>About</value></data>
|
||||
<data name="TrayExit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="AboutMessage" xml:space="preserve"><value>Chromebox Fan Control
|
||||
|
||||
Uses LibreHardwareMonitor to read CPU temperature, controls fan via crosec ectool.
|
||||
|
||||
Run as Administrator.</value></data>
|
||||
<data name="ErrRpmArgsEmpty" xml:space="preserve"><value>RPM args cannot be empty.</value></data>
|
||||
<data name="ErrAutoFanArgsEmpty" xml:space="preserve"><value>Restore auto fan args cannot be empty.</value></data>
|
||||
<data name="MsgSaved" xml:space="preserve"><value>Saved.</value></data>
|
||||
<data name="LabelTemp" xml:space="preserve"><value>Temp:</value></data>
|
||||
<data name="LabelTargetDuty" xml:space="preserve"><value>Target duty:</value></data>
|
||||
<data name="LabelRpm" xml:space="preserve"><value>RPM:</value></data>
|
||||
<data name="ChartTemp" xml:space="preserve"><value>Temperature (°C)</value></data>
|
||||
<data name="ChartTime" xml:space="preserve"><value>Time (s)</value></data>
|
||||
<data name="ChartDuty" xml:space="preserve"><value>Duty (%)</value></data>
|
||||
<data name="ChartRpm" xml:space="preserve"><value>RPM</value></data>
|
||||
<data name="LegendCelsius" xml:space="preserve"><value>°C</value></data>
|
||||
<data name="LegendDuty" xml:space="preserve"><value>Duty %</value></data>
|
||||
<data name="CurveChartDuty" xml:space="preserve"><value>Duty (%)</value></data>
|
||||
<data name="CurveChartTemp" xml:space="preserve"><value>Temp (°C)</value></data>
|
||||
<data name="LblLanguage" xml:space="preserve"><value>Language</value></data>
|
||||
<data name="LangAuto" xml:space="preserve"><value>Auto (follow system)</value></data>
|
||||
<data name="LangEn" xml:space="preserve"><value>English</value></data>
|
||||
<data name="LangZhHans" xml:space="preserve"><value>Simplified Chinese</value></data>
|
||||
<data name="LangZhHant" xml:space="preserve"><value>Traditional Chinese</value></data>
|
||||
<data name="MsgRestartToApplyLanguage" xml:space="preserve"><value>Language changed. Restart the application to apply.</value></data>
|
||||
</root>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTitle" xml:space="preserve"><value>Chromebox Fan Control</value></data>
|
||||
<data name="TabMonitor" xml:space="preserve"><value>Monitoring</value></data>
|
||||
<data name="TabCurve" xml:space="preserve"><value>Curve</value></data>
|
||||
<data name="TabAdvanced" xml:space="preserve"><value>Advanced</value></data>
|
||||
<data name="StatusStarting" xml:space="preserve"><value>Starting…</value></data>
|
||||
<data name="StatusRunning" xml:space="preserve"><value>Running</value></data>
|
||||
<data name="StatusPaused" xml:space="preserve"><value>Paused</value></data>
|
||||
<data name="StatusFailSafe" xml:space="preserve"><value>Fail-safe mode</value></data>
|
||||
<data name="GridColTemp" xml:space="preserve"><value>Temp (°C)</value></data>
|
||||
<data name="GridColDuty" xml:space="preserve"><value>Duty 0–100</value></data>
|
||||
<data name="BtnSaveApply" xml:space="preserve"><value>Save & Apply</value></data>
|
||||
<data name="BtnRestoreDefault" xml:space="preserve"><value>Restore Default Curve</value></data>
|
||||
<data name="LblEctoolPath" xml:space="preserve"><value>ectool path</value></data>
|
||||
<data name="LblPollInterval" xml:space="preserve"><value>Poll interval (ms)</value></data>
|
||||
<data name="LblRpmEvery" xml:space="preserve"><value>Read RPM every N cycles</value></data>
|
||||
<data name="LblFanRpmArgs" xml:space="preserve"><value>RPM args (space-separated)</value></data>
|
||||
<data name="LblFanDutyArgs" xml:space="preserve"><value>Duty args (empty=skip)</value></data>
|
||||
<data name="LblAutoFanArgs" xml:space="preserve"><value>Restore auto fan args</value></data>
|
||||
<data name="LblTempSource" xml:space="preserve"><value>Temp source</value></data>
|
||||
<data name="LblFailCount" xml:space="preserve"><value>Consecutive errors before fail-safe</value></data>
|
||||
<data name="LblFailPct" xml:space="preserve"><value>Fail-safe fixed duty (%)</value></data>
|
||||
<data name="ChkFailAuto" xml:space="preserve"><value>Use autofanctrl in fail-safe (not fixed duty)</value></data>
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>Fail-safe strategy</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>Chart history (minutes)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>Chart max points</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>Ramp-up steps</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>Min ramp delta (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>Save Advanced Settings</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>Show window</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>Pause control</value></data>
|
||||
<data name="TrayResume" xml:space="preserve"><value>Resume control</value></data>
|
||||
<data name="TrayAbout" xml:space="preserve"><value>About</value></data>
|
||||
<data name="TrayExit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="AboutMessage" xml:space="preserve"><value>Chromebox Fan Control
|
||||
|
||||
Uses LibreHardwareMonitor to read CPU temperature, controls fan via crosec ectool.
|
||||
|
||||
Run as Administrator.</value></data>
|
||||
<data name="ErrRpmArgsEmpty" xml:space="preserve"><value>RPM args cannot be empty.</value></data>
|
||||
<data name="ErrAutoFanArgsEmpty" xml:space="preserve"><value>Restore auto fan args cannot be empty.</value></data>
|
||||
<data name="MsgSaved" xml:space="preserve"><value>Saved.</value></data>
|
||||
<data name="LabelTemp" xml:space="preserve"><value>Temp:</value></data>
|
||||
<data name="LabelTargetDuty" xml:space="preserve"><value>Target duty:</value></data>
|
||||
<data name="LabelRpm" xml:space="preserve"><value>RPM:</value></data>
|
||||
<data name="ChartTemp" xml:space="preserve"><value>Temperature (°C)</value></data>
|
||||
<data name="ChartTime" xml:space="preserve"><value>Time (s)</value></data>
|
||||
<data name="ChartDuty" xml:space="preserve"><value>Duty (%)</value></data>
|
||||
<data name="ChartRpm" xml:space="preserve"><value>RPM</value></data>
|
||||
<data name="LegendCelsius" xml:space="preserve"><value>°C</value></data>
|
||||
<data name="LegendDuty" xml:space="preserve"><value>Duty %</value></data>
|
||||
<data name="CurveChartDuty" xml:space="preserve"><value>Duty (%)</value></data>
|
||||
<data name="CurveChartTemp" xml:space="preserve"><value>Temp (°C)</value></data>
|
||||
<data name="LblLanguage" xml:space="preserve"><value>Language</value></data>
|
||||
<data name="LangAuto" xml:space="preserve"><value>Auto (follow system)</value></data>
|
||||
<data name="LangEn" xml:space="preserve"><value>English</value></data>
|
||||
<data name="LangZhHans" xml:space="preserve"><value>Simplified Chinese</value></data>
|
||||
<data name="LangZhHant" xml:space="preserve"><value>Traditional Chinese</value></data>
|
||||
<data name="MsgRestartToApplyLanguage" xml:space="preserve"><value>Language changed. Restart the application to apply.</value></data>
|
||||
</root>
|
||||
|
||||
@@ -1,119 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTitle" xml:space="preserve"><value>Chromebox 风扇温控</value></data>
|
||||
<data name="TabMonitor" xml:space="preserve"><value>监控</value></data>
|
||||
<data name="TabCurve" xml:space="preserve"><value>曲线</value></data>
|
||||
<data name="TabAdvanced" xml:space="preserve"><value>高级</value></data>
|
||||
<data name="StatusStarting" xml:space="preserve"><value>启动中…</value></data>
|
||||
<data name="StatusRunning" xml:space="preserve"><value>运行中</value></data>
|
||||
<data name="StatusPaused" xml:space="preserve"><value>已暂停</value></data>
|
||||
<data name="StatusFailSafe" xml:space="preserve"><value>安全模式</value></data>
|
||||
<data name="GridColTemp" xml:space="preserve"><value>温度 (°C)</value></data>
|
||||
<data name="GridColDuty" xml:space="preserve"><value>占空比 0–100</value></data>
|
||||
<data name="BtnSaveApply" xml:space="preserve"><value>保存并应用</value></data>
|
||||
<data name="BtnRestoreDefault" xml:space="preserve"><value>恢复默认曲线</value></data>
|
||||
<data name="LblEctoolPath" xml:space="preserve"><value>ectool 路径</value></data>
|
||||
<data name="LblPollInterval" xml:space="preserve"><value>轮询间隔 (ms)</value></data>
|
||||
<data name="LblRpmEvery" xml:space="preserve"><value>每 N 轮读 RPM</value></data>
|
||||
<data name="LblFanRpmArgs" xml:space="preserve"><value>读转速参数 (空格分隔)</value></data>
|
||||
<data name="LblFanDutyArgs" xml:space="preserve"><value>读占空比参数 (空=跳过)</value></data>
|
||||
<data name="LblAutoFanArgs" xml:space="preserve"><value>恢复自动风扇参数</value></data>
|
||||
<data name="LblTempSource" xml:space="preserve"><value>温度来源</value></data>
|
||||
<data name="LblFailCount" xml:space="preserve"><value>连续失败进入安全模式 (次)</value></data>
|
||||
<data name="LblFailPct" xml:space="preserve"><value>安全模式固定占空比 (%)</value></data>
|
||||
<data name="ChkFailAuto" xml:space="preserve"><value>安全模式使用 autofanctrl(不用固定占空比)</value></data>
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>图表保留时长 (分钟)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>图表最大点数</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>升速步数</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>最小 ramp 增幅 (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>保存高级设置</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>打开主窗口</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>暂停控制</value></data>
|
||||
<data name="TrayResume" xml:space="preserve"><value>恢复控制</value></data>
|
||||
<data name="TrayAbout" xml:space="preserve"><value>关于</value></data>
|
||||
<data name="TrayExit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="AboutMessage" xml:space="preserve"><value>Chromebox 风扇温控
|
||||
|
||||
使用 LibreHardwareMonitor 读取 CPU 温度,通过 crosec ectool 控制风扇。
|
||||
|
||||
请以管理员身份运行。</value></data>
|
||||
<data name="ErrRpmArgsEmpty" xml:space="preserve"><value>读转速参数不能为空。</value></data>
|
||||
<data name="ErrAutoFanArgsEmpty" xml:space="preserve"><value>恢复自动风扇参数不能为空。</value></data>
|
||||
<data name="MsgSaved" xml:space="preserve"><value>已保存。</value></data>
|
||||
<data name="LabelTemp" xml:space="preserve"><value>温度:</value></data>
|
||||
<data name="LabelTargetDuty" xml:space="preserve"><value>目标占空比:</value></data>
|
||||
<data name="LabelRpm" xml:space="preserve"><value>转速:</value></data>
|
||||
<data name="ChartTemp" xml:space="preserve"><value>温度 (°C)</value></data>
|
||||
<data name="ChartTime" xml:space="preserve"><value>时间 (s)</value></data>
|
||||
<data name="ChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="ChartRpm" xml:space="preserve"><value>转速</value></data>
|
||||
<data name="LegendCelsius" xml:space="preserve"><value>°C</value></data>
|
||||
<data name="LegendDuty" xml:space="preserve"><value>占空比 %</value></data>
|
||||
<data name="CurveChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="CurveChartTemp" xml:space="preserve"><value>温度 (°C)</value></data>
|
||||
<data name="LblLanguage" xml:space="preserve"><value>语言</value></data>
|
||||
<data name="LangAuto" xml:space="preserve"><value>跟随系统</value></data>
|
||||
<data name="LangEn" xml:space="preserve"><value>English</value></data>
|
||||
<data name="LangZhHans" xml:space="preserve"><value>简体中文</value></data>
|
||||
<data name="LangZhHant" xml:space="preserve"><value>繁体中文</value></data>
|
||||
<data name="MsgRestartToApplyLanguage" xml:space="preserve"><value>语言已更改,请重启应用以生效。</value></data>
|
||||
</root>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTitle" xml:space="preserve"><value>Chromebox 风扇温控</value></data>
|
||||
<data name="TabMonitor" xml:space="preserve"><value>监控</value></data>
|
||||
<data name="TabCurve" xml:space="preserve"><value>曲线</value></data>
|
||||
<data name="TabAdvanced" xml:space="preserve"><value>高级</value></data>
|
||||
<data name="StatusStarting" xml:space="preserve"><value>启动中…</value></data>
|
||||
<data name="StatusRunning" xml:space="preserve"><value>运行中</value></data>
|
||||
<data name="StatusPaused" xml:space="preserve"><value>已暂停</value></data>
|
||||
<data name="StatusFailSafe" xml:space="preserve"><value>安全模式</value></data>
|
||||
<data name="GridColTemp" xml:space="preserve"><value>温度 (°C)</value></data>
|
||||
<data name="GridColDuty" xml:space="preserve"><value>占空比 0–100</value></data>
|
||||
<data name="BtnSaveApply" xml:space="preserve"><value>保存并应用</value></data>
|
||||
<data name="BtnRestoreDefault" xml:space="preserve"><value>恢复默认曲线</value></data>
|
||||
<data name="LblEctoolPath" xml:space="preserve"><value>ectool 路径</value></data>
|
||||
<data name="LblPollInterval" xml:space="preserve"><value>轮询间隔 (ms)</value></data>
|
||||
<data name="LblRpmEvery" xml:space="preserve"><value>每 N 轮读 RPM</value></data>
|
||||
<data name="LblFanRpmArgs" xml:space="preserve"><value>读转速参数 (空格分隔)</value></data>
|
||||
<data name="LblFanDutyArgs" xml:space="preserve"><value>读占空比参数 (空=跳过)</value></data>
|
||||
<data name="LblAutoFanArgs" xml:space="preserve"><value>恢复自动风扇参数</value></data>
|
||||
<data name="LblTempSource" xml:space="preserve"><value>温度来源</value></data>
|
||||
<data name="LblFailCount" xml:space="preserve"><value>连续失败进入安全模式 (次)</value></data>
|
||||
<data name="LblFailPct" xml:space="preserve"><value>安全模式固定占空比 (%)</value></data>
|
||||
<data name="ChkFailAuto" xml:space="preserve"><value>安全模式使用 autofanctrl(不用固定占空比)</value></data>
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>图表保留时长 (分钟)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>图表最大点数</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>升速步数</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>最小 ramp 增幅 (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>保存高级设置</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>打开主窗口</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>暂停控制</value></data>
|
||||
<data name="TrayResume" xml:space="preserve"><value>恢复控制</value></data>
|
||||
<data name="TrayAbout" xml:space="preserve"><value>关于</value></data>
|
||||
<data name="TrayExit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="AboutMessage" xml:space="preserve"><value>Chromebox 风扇温控
|
||||
|
||||
使用 LibreHardwareMonitor 读取 CPU 温度,通过 crosec ectool 控制风扇。
|
||||
|
||||
请以管理员身份运行。</value></data>
|
||||
<data name="ErrRpmArgsEmpty" xml:space="preserve"><value>读转速参数不能为空。</value></data>
|
||||
<data name="ErrAutoFanArgsEmpty" xml:space="preserve"><value>恢复自动风扇参数不能为空。</value></data>
|
||||
<data name="MsgSaved" xml:space="preserve"><value>已保存。</value></data>
|
||||
<data name="LabelTemp" xml:space="preserve"><value>温度:</value></data>
|
||||
<data name="LabelTargetDuty" xml:space="preserve"><value>目标占空比:</value></data>
|
||||
<data name="LabelRpm" xml:space="preserve"><value>转速:</value></data>
|
||||
<data name="ChartTemp" xml:space="preserve"><value>温度 (°C)</value></data>
|
||||
<data name="ChartTime" xml:space="preserve"><value>时间 (s)</value></data>
|
||||
<data name="ChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="ChartRpm" xml:space="preserve"><value>转速</value></data>
|
||||
<data name="LegendCelsius" xml:space="preserve"><value>°C</value></data>
|
||||
<data name="LegendDuty" xml:space="preserve"><value>占空比 %</value></data>
|
||||
<data name="CurveChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="CurveChartTemp" xml:space="preserve"><value>温度 (°C)</value></data>
|
||||
<data name="LblLanguage" xml:space="preserve"><value>语言</value></data>
|
||||
<data name="LangAuto" xml:space="preserve"><value>跟随系统</value></data>
|
||||
<data name="LangEn" xml:space="preserve"><value>English</value></data>
|
||||
<data name="LangZhHans" xml:space="preserve"><value>简体中文</value></data>
|
||||
<data name="LangZhHant" xml:space="preserve"><value>繁体中文</value></data>
|
||||
<data name="MsgRestartToApplyLanguage" xml:space="preserve"><value>语言已更改,请重启应用以生效。</value></data>
|
||||
</root>
|
||||
|
||||
@@ -1,119 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTitle" xml:space="preserve"><value>Chromebox 風扇溫控</value></data>
|
||||
<data name="TabMonitor" xml:space="preserve"><value>監控</value></data>
|
||||
<data name="TabCurve" xml:space="preserve"><value>曲線</value></data>
|
||||
<data name="TabAdvanced" xml:space="preserve"><value>進階</value></data>
|
||||
<data name="StatusStarting" xml:space="preserve"><value>啟動中…</value></data>
|
||||
<data name="StatusRunning" xml:space="preserve"><value>運行中</value></data>
|
||||
<data name="StatusPaused" xml:space="preserve"><value>已暫停</value></data>
|
||||
<data name="StatusFailSafe" xml:space="preserve"><value>安全模式</value></data>
|
||||
<data name="GridColTemp" xml:space="preserve"><value>溫度 (°C)</value></data>
|
||||
<data name="GridColDuty" xml:space="preserve"><value>占空比 0–100</value></data>
|
||||
<data name="BtnSaveApply" xml:space="preserve"><value>儲存並套用</value></data>
|
||||
<data name="BtnRestoreDefault" xml:space="preserve"><value>恢復預設曲線</value></data>
|
||||
<data name="LblEctoolPath" xml:space="preserve"><value>ectool 路徑</value></data>
|
||||
<data name="LblPollInterval" xml:space="preserve"><value>輪詢間隔 (ms)</value></data>
|
||||
<data name="LblRpmEvery" xml:space="preserve"><value>每 N 輪讀 RPM</value></data>
|
||||
<data name="LblFanRpmArgs" xml:space="preserve"><value>讀轉速參數 (空格分隔)</value></data>
|
||||
<data name="LblFanDutyArgs" xml:space="preserve"><value>讀占空比參數 (空=略過)</value></data>
|
||||
<data name="LblAutoFanArgs" xml:space="preserve"><value>恢復自動風扇參數</value></data>
|
||||
<data name="LblTempSource" xml:space="preserve"><value>溫度來源</value></data>
|
||||
<data name="LblFailCount" xml:space="preserve"><value>連續失敗進入安全模式 (次)</value></data>
|
||||
<data name="LblFailPct" xml:space="preserve"><value>安全模式固定占空比 (%)</value></data>
|
||||
<data name="ChkFailAuto" xml:space="preserve"><value>安全模式使用 autofanctrl(不用固定占空比)</value></data>
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>圖表保留時長 (分鐘)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>圖表最大點數</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>升速步數</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>最小 ramp 增幅 (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>儲存進階設定</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>開啟主視窗</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>暫停控制</value></data>
|
||||
<data name="TrayResume" xml:space="preserve"><value>恢復控制</value></data>
|
||||
<data name="TrayAbout" xml:space="preserve"><value>關於</value></data>
|
||||
<data name="TrayExit" xml:space="preserve"><value>結束</value></data>
|
||||
<data name="AboutMessage" xml:space="preserve"><value>Chromebox 風扇溫控
|
||||
|
||||
使用 LibreHardwareMonitor 讀取 CPU 溫度,透過 crosec ectool 控制風扇。
|
||||
|
||||
請以系統管理員身分執行。</value></data>
|
||||
<data name="ErrRpmArgsEmpty" xml:space="preserve"><value>讀轉速參數不能為空。</value></data>
|
||||
<data name="ErrAutoFanArgsEmpty" xml:space="preserve"><value>恢復自動風扇參數不能為空。</value></data>
|
||||
<data name="MsgSaved" xml:space="preserve"><value>已儲存。</value></data>
|
||||
<data name="LabelTemp" xml:space="preserve"><value>溫度:</value></data>
|
||||
<data name="LabelTargetDuty" xml:space="preserve"><value>目標占空比:</value></data>
|
||||
<data name="LabelRpm" xml:space="preserve"><value>轉速:</value></data>
|
||||
<data name="ChartTemp" xml:space="preserve"><value>溫度 (°C)</value></data>
|
||||
<data name="ChartTime" xml:space="preserve"><value>時間 (s)</value></data>
|
||||
<data name="ChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="ChartRpm" xml:space="preserve"><value>轉速</value></data>
|
||||
<data name="LegendCelsius" xml:space="preserve"><value>°C</value></data>
|
||||
<data name="LegendDuty" xml:space="preserve"><value>占空比 %</value></data>
|
||||
<data name="CurveChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="CurveChartTemp" xml:space="preserve"><value>溫度 (°C)</value></data>
|
||||
<data name="LblLanguage" xml:space="preserve"><value>語言</value></data>
|
||||
<data name="LangAuto" xml:space="preserve"><value>跟隨系統</value></data>
|
||||
<data name="LangEn" xml:space="preserve"><value>English</value></data>
|
||||
<data name="LangZhHans" xml:space="preserve"><value>簡體中文</value></data>
|
||||
<data name="LangZhHant" xml:space="preserve"><value>繁體中文</value></data>
|
||||
<data name="MsgRestartToApplyLanguage" xml:space="preserve"><value>語言已變更,請重新啟動應用程式以生效。</value></data>
|
||||
</root>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTitle" xml:space="preserve"><value>Chromebox 風扇溫控</value></data>
|
||||
<data name="TabMonitor" xml:space="preserve"><value>監控</value></data>
|
||||
<data name="TabCurve" xml:space="preserve"><value>曲線</value></data>
|
||||
<data name="TabAdvanced" xml:space="preserve"><value>進階</value></data>
|
||||
<data name="StatusStarting" xml:space="preserve"><value>啟動中…</value></data>
|
||||
<data name="StatusRunning" xml:space="preserve"><value>運行中</value></data>
|
||||
<data name="StatusPaused" xml:space="preserve"><value>已暫停</value></data>
|
||||
<data name="StatusFailSafe" xml:space="preserve"><value>安全模式</value></data>
|
||||
<data name="GridColTemp" xml:space="preserve"><value>溫度 (°C)</value></data>
|
||||
<data name="GridColDuty" xml:space="preserve"><value>占空比 0–100</value></data>
|
||||
<data name="BtnSaveApply" xml:space="preserve"><value>儲存並套用</value></data>
|
||||
<data name="BtnRestoreDefault" xml:space="preserve"><value>恢復預設曲線</value></data>
|
||||
<data name="LblEctoolPath" xml:space="preserve"><value>ectool 路徑</value></data>
|
||||
<data name="LblPollInterval" xml:space="preserve"><value>輪詢間隔 (ms)</value></data>
|
||||
<data name="LblRpmEvery" xml:space="preserve"><value>每 N 輪讀 RPM</value></data>
|
||||
<data name="LblFanRpmArgs" xml:space="preserve"><value>讀轉速參數 (空格分隔)</value></data>
|
||||
<data name="LblFanDutyArgs" xml:space="preserve"><value>讀占空比參數 (空=略過)</value></data>
|
||||
<data name="LblAutoFanArgs" xml:space="preserve"><value>恢復自動風扇參數</value></data>
|
||||
<data name="LblTempSource" xml:space="preserve"><value>溫度來源</value></data>
|
||||
<data name="LblFailCount" xml:space="preserve"><value>連續失敗進入安全模式 (次)</value></data>
|
||||
<data name="LblFailPct" xml:space="preserve"><value>安全模式固定占空比 (%)</value></data>
|
||||
<data name="ChkFailAuto" xml:space="preserve"><value>安全模式使用 autofanctrl(不用固定占空比)</value></data>
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>圖表保留時長 (分鐘)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>圖表最大點數</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>升速步數</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>最小 ramp 增幅 (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>儲存進階設定</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>開啟主視窗</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>暫停控制</value></data>
|
||||
<data name="TrayResume" xml:space="preserve"><value>恢復控制</value></data>
|
||||
<data name="TrayAbout" xml:space="preserve"><value>關於</value></data>
|
||||
<data name="TrayExit" xml:space="preserve"><value>結束</value></data>
|
||||
<data name="AboutMessage" xml:space="preserve"><value>Chromebox 風扇溫控
|
||||
|
||||
使用 LibreHardwareMonitor 讀取 CPU 溫度,透過 crosec ectool 控制風扇。
|
||||
|
||||
請以系統管理員身分執行。</value></data>
|
||||
<data name="ErrRpmArgsEmpty" xml:space="preserve"><value>讀轉速參數不能為空。</value></data>
|
||||
<data name="ErrAutoFanArgsEmpty" xml:space="preserve"><value>恢復自動風扇參數不能為空。</value></data>
|
||||
<data name="MsgSaved" xml:space="preserve"><value>已儲存。</value></data>
|
||||
<data name="LabelTemp" xml:space="preserve"><value>溫度:</value></data>
|
||||
<data name="LabelTargetDuty" xml:space="preserve"><value>目標占空比:</value></data>
|
||||
<data name="LabelRpm" xml:space="preserve"><value>轉速:</value></data>
|
||||
<data name="ChartTemp" xml:space="preserve"><value>溫度 (°C)</value></data>
|
||||
<data name="ChartTime" xml:space="preserve"><value>時間 (s)</value></data>
|
||||
<data name="ChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="ChartRpm" xml:space="preserve"><value>轉速</value></data>
|
||||
<data name="LegendCelsius" xml:space="preserve"><value>°C</value></data>
|
||||
<data name="LegendDuty" xml:space="preserve"><value>占空比 %</value></data>
|
||||
<data name="CurveChartDuty" xml:space="preserve"><value>占空比 (%)</value></data>
|
||||
<data name="CurveChartTemp" xml:space="preserve"><value>溫度 (°C)</value></data>
|
||||
<data name="LblLanguage" xml:space="preserve"><value>語言</value></data>
|
||||
<data name="LangAuto" xml:space="preserve"><value>跟隨系統</value></data>
|
||||
<data name="LangEn" xml:space="preserve"><value>English</value></data>
|
||||
<data name="LangZhHans" xml:space="preserve"><value>簡體中文</value></data>
|
||||
<data name="LangZhHant" xml:space="preserve"><value>繁體中文</value></data>
|
||||
<data name="MsgRestartToApplyLanguage" xml:space="preserve"><value>語言已變更,請重新啟動應用程式以生效。</value></data>
|
||||
</root>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ChromeboxFanControl.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ChromeboxFanControl.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe",
|
||||
"PollIntervalMs": 1500,
|
||||
"FanRpmPollEveryNCycles": 1,
|
||||
"FanRpmArgs": [ "pwmgetfanrpm", "0" ],
|
||||
"FanDutyArgs": null,
|
||||
"AutoFanCtrlArgs": [ "autofanctrl" ],
|
||||
"TempSource": "AverageCore",
|
||||
"RampUpSteps": 3,
|
||||
"RampUpMinDeltaPercent": 20,
|
||||
"FailSafeAfterConsecutiveErrors": 5,
|
||||
"FailSafeFanPercent": 100,
|
||||
"FailSafeRestoreAutoFan": false,
|
||||
"ChartHistoryMinutes": 15,
|
||||
"ChartMaxPoints": 900,
|
||||
"CurvePoints": [ 0, 0, 10, 25, 40, 55, 70, 80, 90, 95, 97, 98, 99, 100 ]
|
||||
}
|
||||
{
|
||||
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe",
|
||||
"PollIntervalMs": 1500,
|
||||
"FanRpmPollEveryNCycles": 1,
|
||||
"FanRpmArgs": [ "pwmgetfanrpm", "0" ],
|
||||
"FanDutyArgs": null,
|
||||
"AutoFanCtrlArgs": [ "autofanctrl" ],
|
||||
"TempSource": "AverageCore",
|
||||
"RampUpSteps": 3,
|
||||
"RampUpMinDeltaPercent": 20,
|
||||
"FailSafeAfterConsecutiveErrors": 5,
|
||||
"FailSafeFanPercent": 100,
|
||||
"FailSafeRestoreAutoFan": false,
|
||||
"ChartHistoryMinutes": 15,
|
||||
"ChartMaxPoints": 900,
|
||||
"CurvePoints": [ 0, 0, 10, 25, 40, 55, 70, 80, 90, 95, 97, 98, 99, 100 ]
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);NU1701</NoWarn>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ChromeboxFanControlService</RootNamespace>
|
||||
<AssemblyName>ChromeboxFanControlService</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChromeboxFanControl\ChromeboxFanControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
<!-- appsettings comes from ChromeboxFanControl when merged in dist; service uses that or ProgramData config -->
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);NU1701</NoWarn>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ChromeboxFanControlService</RootNamespace>
|
||||
<AssemblyName>ChromeboxFanControlService</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChromeboxFanControl\ChromeboxFanControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
<!-- appsettings comes from ChromeboxFanControl when merged in dist; service uses that or ProgramData config -->
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
using ChromeboxFanControl;
|
||||
|
||||
namespace ChromeboxFanControlService;
|
||||
|
||||
public sealed class FanControlWorker : BackgroundService
|
||||
{
|
||||
private FanController? _fan;
|
||||
private AppConfig? _config;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_config = AppConfig.LoadMerged();
|
||||
_fan = new FanController(_config);
|
||||
_fan.Start();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
/* shutdown */
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_fan?.Stop();
|
||||
_fan?.Dispose();
|
||||
if (_config != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
EctoolRunner.RunSync(_config.EctoolPath, _config.AutoFanCtrlArgs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* best effort restore autofan on shutdown */
|
||||
}
|
||||
}
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
using ChromeboxFanControl;
|
||||
|
||||
namespace ChromeboxFanControlService;
|
||||
|
||||
public sealed class FanControlWorker : BackgroundService
|
||||
{
|
||||
private FanController? _fan;
|
||||
private AppConfig? _config;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_config = AppConfig.LoadMerged();
|
||||
_fan = new FanController(_config);
|
||||
_fan.Start();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
/* shutdown */
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_fan?.Stop();
|
||||
_fan?.Dispose();
|
||||
if (_config != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
EctoolRunner.RunSync(_config.EctoolPath, _config.AutoFanCtrlArgs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* best effort restore autofan on shutdown */
|
||||
}
|
||||
}
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using ChromeboxFanControlService;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.UseWindowsService(options =>
|
||||
{
|
||||
options.ServiceName = "ChromeboxFanControl";
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostedService<FanControlWorker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
using ChromeboxFanControlService;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.UseWindowsService(options =>
|
||||
{
|
||||
options.ServiceName = "ChromeboxFanControl";
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostedService<FanControlWorker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe",
|
||||
"PollIntervalMs": 1500,
|
||||
"FanRpmPollEveryNCycles": 1,
|
||||
"FanRpmArgs": [ "pwmgetfanrpm", "0" ],
|
||||
"FanDutyArgs": null,
|
||||
"AutoFanCtrlArgs": [ "autofanctrl" ],
|
||||
"TempSource": "AverageCore",
|
||||
"FailSafeAfterConsecutiveErrors": 5,
|
||||
"FailSafeFanPercent": 100,
|
||||
"FailSafeRestoreAutoFan": false,
|
||||
"ChartHistoryMinutes": 15,
|
||||
"ChartMaxPoints": 900,
|
||||
"CurvePoints": [ 0, 0, 10, 25, 40, 55, 70, 80, 90, 95, 97, 98, 99, 100 ]
|
||||
}
|
||||
{
|
||||
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe",
|
||||
"PollIntervalMs": 1500,
|
||||
"FanRpmPollEveryNCycles": 1,
|
||||
"FanRpmArgs": [ "pwmgetfanrpm", "0" ],
|
||||
"FanDutyArgs": null,
|
||||
"AutoFanCtrlArgs": [ "autofanctrl" ],
|
||||
"TempSource": "AverageCore",
|
||||
"FailSafeAfterConsecutiveErrors": 5,
|
||||
"FailSafeFanPercent": 100,
|
||||
"FailSafeRestoreAutoFan": false,
|
||||
"ChartHistoryMinutes": 15,
|
||||
"ChartMaxPoints": 900,
|
||||
"CurvePoints": [ 0, 0, 10, 25, 40, 55, 70, 80, 90, 95, 97, 98, 99, 100 ]
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
using FanControl.Plugins;
|
||||
|
||||
namespace FanControl.ChromeboxEC;
|
||||
|
||||
/// <summary>风扇控制:ectool fanduty <percent></summary>
|
||||
public sealed class ChromeboxECControlSensor : IPluginControlSensor2
|
||||
{
|
||||
private readonly string _ectoolPath;
|
||||
private readonly string[] _autoFanArgs;
|
||||
private float? _value;
|
||||
|
||||
public ChromeboxECControlSensor(string ectoolPath, string[] autoFanArgs)
|
||||
{
|
||||
_ectoolPath = ectoolPath;
|
||||
_autoFanArgs = autoFanArgs;
|
||||
}
|
||||
|
||||
public string Id => "ChromeboxEC_Control";
|
||||
public string Name => "Chromebox EC Fan";
|
||||
public string Origin => "ectool fanduty";
|
||||
public float? Value => _value;
|
||||
public string? PairedFanSensorId => "ChromeboxEC_Fan";
|
||||
|
||||
/// <summary>设置风扇占空比 0–100,调用 ectool fanduty</summary>
|
||||
public void Set(float val)
|
||||
{
|
||||
_value = val;
|
||||
var duty = (int)Math.Clamp(Math.Round(val), 0, 100);
|
||||
EctoolRunner.Run(_ectoolPath, ["fanduty", duty.ToString()]);
|
||||
}
|
||||
|
||||
/// <summary>禁用控制时恢复 EC 自动风扇,调用 ectool autofanctrl</summary>
|
||||
public void Reset()
|
||||
{
|
||||
_value = null;
|
||||
if (_autoFanArgs is { Length: > 0 })
|
||||
EctoolRunner.Run(_ectoolPath, _autoFanArgs);
|
||||
}
|
||||
|
||||
public void Update() { }
|
||||
}
|
||||
using FanControl.Plugins;
|
||||
|
||||
namespace FanControl.ChromeboxEC;
|
||||
|
||||
/// <summary>风扇控制:ectool fanduty <percent></summary>
|
||||
public sealed class ChromeboxECControlSensor : IPluginControlSensor2
|
||||
{
|
||||
private readonly string _ectoolPath;
|
||||
private readonly string[] _autoFanArgs;
|
||||
private float? _value;
|
||||
|
||||
public ChromeboxECControlSensor(string ectoolPath, string[] autoFanArgs)
|
||||
{
|
||||
_ectoolPath = ectoolPath;
|
||||
_autoFanArgs = autoFanArgs;
|
||||
}
|
||||
|
||||
public string Id => "ChromeboxEC_Control";
|
||||
public string Name => "Chromebox EC Fan";
|
||||
public string Origin => "ectool fanduty";
|
||||
public float? Value => _value;
|
||||
public string? PairedFanSensorId => "ChromeboxEC_Fan";
|
||||
|
||||
/// <summary>设置风扇占空比 0–100,调用 ectool fanduty</summary>
|
||||
public void Set(float val)
|
||||
{
|
||||
_value = val;
|
||||
var duty = (int)Math.Clamp(Math.Round(val), 0, 100);
|
||||
EctoolRunner.Run(_ectoolPath, ["fanduty", duty.ToString()]);
|
||||
}
|
||||
|
||||
/// <summary>禁用控制时恢复 EC 自动风扇,调用 ectool autofanctrl</summary>
|
||||
public void Reset()
|
||||
{
|
||||
_value = null;
|
||||
if (_autoFanArgs is { Length: > 0 })
|
||||
EctoolRunner.Run(_ectoolPath, _autoFanArgs);
|
||||
}
|
||||
|
||||
public void Update() { }
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
using FanControl.Plugins;
|
||||
|
||||
namespace FanControl.ChromeboxEC;
|
||||
|
||||
/// <summary>
|
||||
/// Fan Control 插件:通过 ectool 读取 Chromebox EC 温度、控制风扇。
|
||||
/// 命令参见 docs/ectool-commands-zh.md
|
||||
/// </summary>
|
||||
public sealed class ChromeboxECPlugin : IPlugin
|
||||
{
|
||||
private PluginConfig? _config;
|
||||
private bool _available;
|
||||
|
||||
public string Name => "Chromebox EC";
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_config = PluginConfig.Load();
|
||||
_available = !string.IsNullOrWhiteSpace(_config.EctoolPath) &&
|
||||
File.Exists(_config.EctoolPath);
|
||||
}
|
||||
|
||||
public void Load(IPluginSensorsContainer container)
|
||||
{
|
||||
if (!_available || _config == null)
|
||||
return;
|
||||
|
||||
var tempArgs = _config.TempArgs is { Length: > 0 } ta ? ta : ["temps", "0"];
|
||||
var rpmArgs = _config.FanRpmArgs is { Length: > 0 } ra ? ra : ["pwmgetfanrpm", "0"];
|
||||
|
||||
container.TempSensors.Add(new ChromeboxECTempSensor(_config.EctoolPath, tempArgs));
|
||||
container.FanSensors.Add(new ChromeboxECFanSensor(_config.EctoolPath, rpmArgs));
|
||||
container.ControlSensors.Add(new ChromeboxECControlSensor(_config.EctoolPath, _config.AutoFanCtrlArgs));
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_config = null;
|
||||
_available = false;
|
||||
}
|
||||
}
|
||||
using FanControl.Plugins;
|
||||
|
||||
namespace FanControl.ChromeboxEC;
|
||||
|
||||
/// <summary>
|
||||
/// Fan Control 插件:通过 ectool 读取 Chromebox EC 温度、控制风扇。
|
||||
/// 命令参见 docs/ectool-commands-zh.md
|
||||
/// </summary>
|
||||
public sealed class ChromeboxECPlugin : IPlugin
|
||||
{
|
||||
private PluginConfig? _config;
|
||||
private bool _available;
|
||||
|
||||
public string Name => "Chromebox EC";
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_config = PluginConfig.Load();
|
||||
_available = !string.IsNullOrWhiteSpace(_config.EctoolPath) &&
|
||||
File.Exists(_config.EctoolPath);
|
||||
}
|
||||
|
||||
public void Load(IPluginSensorsContainer container)
|
||||
{
|
||||
if (!_available || _config == null)
|
||||
return;
|
||||
|
||||
var tempArgs = _config.TempArgs is { Length: > 0 } ta ? ta : ["temps", "0"];
|
||||
var rpmArgs = _config.FanRpmArgs is { Length: > 0 } ra ? ra : ["pwmgetfanrpm", "0"];
|
||||
|
||||
container.TempSensors.Add(new ChromeboxECTempSensor(_config.EctoolPath, tempArgs));
|
||||
container.FanSensors.Add(new ChromeboxECFanSensor(_config.EctoolPath, rpmArgs));
|
||||
container.ControlSensors.Add(new ChromeboxECControlSensor(_config.EctoolPath, _config.AutoFanCtrlArgs));
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_config = null;
|
||||
_available = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
using FanControl.Plugins;
|
||||
|
||||
namespace FanControl.ChromeboxEC;
|
||||
|
||||
/// <summary>温度:ectool temps <sensorid></summary>
|
||||
public sealed class ChromeboxECTempSensor : IPluginSensor
|
||||
{
|
||||
private readonly string _ectoolPath;
|
||||
private readonly string[] _tempArgs;
|
||||
|
||||
public ChromeboxECTempSensor(string ectoolPath, string[] tempArgs)
|
||||
{
|
||||
_ectoolPath = ectoolPath;
|
||||
_tempArgs = tempArgs;
|
||||
}
|
||||
|
||||
public string Id => "ChromeboxEC_Temp";
|
||||
public string Name => "Chromebox EC Temperature";
|
||||
public string Origin => "ectool temps";
|
||||
public float? Value { get; private set; }
|
||||
|
||||
public void Update()
|
||||
{
|
||||
var (ok, stdout, _) = EctoolRunner.Run(_ectoolPath, _tempArgs);
|
||||
Value = ok && EctoolRunner.TryParseTemp(stdout) is { } temp ? temp : null;
|
||||
}
|
||||
}
|
||||
using FanControl.Plugins;
|
||||
|
||||
namespace FanControl.ChromeboxEC;
|
||||
|
||||
/// <summary>温度:ectool temps <sensorid></summary>
|
||||
public sealed class ChromeboxECTempSensor : IPluginSensor
|
||||
{
|
||||
private readonly string _ectoolPath;
|
||||
private readonly string[] _tempArgs;
|
||||
|
||||
public ChromeboxECTempSensor(string ectoolPath, string[] tempArgs)
|
||||
{
|
||||
_ectoolPath = ectoolPath;
|
||||
_tempArgs = tempArgs;
|
||||
}
|
||||
|
||||
public string Id => "ChromeboxEC_Temp";
|
||||
public string Name => "Chromebox EC Temperature";
|
||||
public string Origin => "ectool temps";
|
||||
public float? Value { get; private set; }
|
||||
|
||||
public void Update()
|
||||
{
|
||||
var (ok, stdout, _) = EctoolRunner.Run(_ectoolPath, _tempArgs);
|
||||
Value = ok && EctoolRunner.TryParseTemp(stdout) is { } temp ? temp : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,15 +76,27 @@ internal static class EctoolRunner
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>解析 temps 输出为摄氏温度。支持 "315 K (= 42 C)" 等格式,多传感器时取最高值。</summary>
|
||||
/// <summary>解析 temps 输出为摄氏温度。支持 "329 K (= 56 C)" 等格式,多传感器时取最高值。排除 ratio 行中的 (313 K and 333 K) 等阈值。</summary>
|
||||
public static float? TryParseTemp(string stdout)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stdout))
|
||||
return null;
|
||||
|
||||
float? best = null;
|
||||
// 匹配 "315 K" 或 "315 K (= 42 C)"
|
||||
foreach (Match m in Regex.Matches(stdout, @"(\d{2,3})\s*K\b", RegexOptions.IgnoreCase))
|
||||
// 匹配 "= 56 C":最可靠,且不会被 ratio 行的 (313 K and 333 K) 干扰
|
||||
foreach (Match m in Regex.Matches(stdout, @"=\s*(\d{1,3})\s*[Cc]\b"))
|
||||
{
|
||||
if (int.TryParse(m.Groups[1].Value, out var c) && c is >= 0 and <= 120)
|
||||
{
|
||||
if (best == null || c > best)
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
if (best != null)
|
||||
return best;
|
||||
|
||||
// 若无 "= X C",再匹配 "329 K",排除 ratio 中的 "313 K and" 与 "and 333 K"
|
||||
foreach (Match m in Regex.Matches(stdout, @"(?<!and\s)(\d{2,3})\s*K\b(?!\s+and)", RegexOptions.IgnoreCase))
|
||||
{
|
||||
if (int.TryParse(m.Groups[1].Value, out var k) && k is >= 250 and <= 400)
|
||||
{
|
||||
@@ -93,15 +105,6 @@ internal static class EctoolRunner
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
// 匹配 "= 42 C" 或 "42 C"
|
||||
foreach (Match m in Regex.Matches(stdout, @"(?:=\s*)?(\d{1,3})\s*[Cc]\b"))
|
||||
{
|
||||
if (int.TryParse(m.Groups[1].Value, out var c) && c is >= 0 and <= 120)
|
||||
{
|
||||
if (best == null || c > best)
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>FanControl.ChromeboxEC</AssemblyName>
|
||||
<RootNamespace>FanControl.ChromeboxEC</RootNamespace>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="FanControl.Plugins">
|
||||
<HintPath>lib\FanControl.Plugins.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>FanControl.ChromeboxEC</AssemblyName>
|
||||
<RootNamespace>FanControl.ChromeboxEC</RootNamespace>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="FanControl.Plugins">
|
||||
<HintPath>lib\FanControl.Plugins.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe",
|
||||
"TempArgs": ["temps", "0"],
|
||||
"FanRpmArgs": ["pwmgetfanrpm", "0"],
|
||||
"AutoFanCtrlArgs": ["autofanctrl"]
|
||||
}
|
||||
{
|
||||
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe",
|
||||
"TempArgs": ["temps", "0"],
|
||||
"FanRpmArgs": ["pwmgetfanrpm", "0"],
|
||||
"AutoFanCtrlArgs": ["autofanctrl"]
|
||||
}
|
||||
|
||||
382
README.md
382
README.md
@@ -1,191 +1,191 @@
|
||||
# Chromebox Fan Control (Windows)
|
||||
|
||||
Read **CPU core temperature** via **LibreHardwareMonitor**, set **`fanduty`** through Coolstar **crosec**'s **ectool**, and read **fan RPM** from ectool. The UI shows three real-time charts: temperature, target duty, and RPM. On Chromebox with coreboot.
|
||||
|
||||
### Background
|
||||
|
||||
My Acer Chromebox CXI4 has only a board-mounted temperature sensor; there is no sensor near the CPU. On Chrome OS, the kernel uses CPU core temperature for fan control and thermal management works well. After flashing coreboot, Linux also uses CPU core temp for fan control. On Windows, however, the system relies on the motherboard temperature sensor for fan control, not CPU temperature. During sudden high load, heat has not yet reached the board sensor when the CPU already overheats, causing Windows to shut down before the fan can respond. I tried configuring coreboot, recompiling the EC firmware, and other approaches—none succeeded in connecting CPU core temperature to Chromebox EC fan control. Hence this project.
|
||||
|
||||
**Also included:** [FanControl.ChromeboxEC](FanControl.ChromeboxEC/) — a plugin for [Fan Control](https://github.com/Rem0o/FanControl.Releases) (Rémi Mercier) that adds Chromebox EC fan control. Use this if you prefer Fan Control’s UI and curve editor.
|
||||
|
||||
### Fan Control setup
|
||||
|
||||
1. **Install plugin**: Copy `FanControl.ChromeboxEC.dll` to Fan Control's `Plugins` folder; start Fan Control as Administrator.
|
||||
2. **Recommended config**: CPU core temp + board temp (EC Temp) dual curves, take max to set fan.
|
||||
- Create curve **CPU Package**: temp source = CPU Package, output 0–100% duty.
|
||||
- Create curve **Board Temp**: temp source = Chromebox EC Temperature, output 0–100% duty.
|
||||
- Create **Mix** curve: function = Max, inputs = both curves above.
|
||||
- Set **Chromebox EC Fan** control to use **Mix** curve.
|
||||
|
||||
See [FanControl.ChromeboxEC README](FanControl.ChromeboxEC/README.md) for details.
|
||||
|
||||
## Dependencies
|
||||
|
||||
1. Install [Coolstar CROS-EC driver](https://github.com/coolstar/crwindows) (or your crosec package). The default executable is:
|
||||
`C:\Program Files\crosec\ectool.exe`
|
||||
Change it in the program’s **Advanced** tab if needed.
|
||||
2. **Run as Administrator** (manifest requests elevation; LHM and EC access usually need admin).
|
||||
3. **Deployment**: `.\build.ps1 -publish` (and with `-msi`) defaults to **self-contained single-file**, **including .NET 8 runtime**. `dist\` has only a few exes. Use `-multiFile` or `-frameworkDependent` to avoid bundling the runtime.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
.\build.ps1
|
||||
```
|
||||
|
||||
### Publish MSI installer (recommended)
|
||||
|
||||
```powershell
|
||||
.\build.ps1 -publish -msi
|
||||
```
|
||||
|
||||
`-publish` produces `dist\`; **`-msi`** uses WiX to build installers (requires WiX). Two MSIs are produced:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `dist-installer\ChromeboxFanControl-Setup.msi` | **Desktop**: GUI + optional Windows service (checkbox in feature tree) |
|
||||
| `dist-installer\ChromeboxFanControlService-Setup.msi` | **Service only**: background service for headless deployment |
|
||||
|
||||
Build machine needs [.NET 8 SDK](https://dotnet.microsoft.com/download) and [WiX Toolset 3](https://wixtoolset.org/docs/wix3/). **Close ChromeboxFanControl.exe before building.**
|
||||
|
||||
Run the MSI **as Administrator**. If ectool is not installed yet, see [Chrultrabook ectool installation](https://docs.chrultrabook.com/docs/installing/ectool.html). See also [ectool 命令参考(中文)](docs/ectool-commands-zh.md). In service mode, config is read from `%ProgramData%\ChromeboxFanControl\config.json`.
|
||||
|
||||
### Publish to dist\ only (debugging or manual packaging)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `.\build.ps1 -publish` | Publish both desktop and service to `dist\` (single-file by default) |
|
||||
| `.\build.ps1 -publishGui` | Desktop only |
|
||||
| `.\build.ps1 -publishService` | Service only, merged into `dist\` |
|
||||
| `.\build.ps1 -publish -multiFile` | Keep multiple DLLs (instead of single exe) |
|
||||
| `.\build.ps1 -publish -frameworkDependent` | Rely on system .NET 8 (no bundled runtime) |
|
||||
|
||||
## Configuration
|
||||
|
||||
- **`appsettings.json`** beside the install directory: default options.
|
||||
- **`%AppData%\ChromeboxFanControl\config.json`**: user config; saved from GUI in desktop mode.
|
||||
- **`%ProgramData%\ChromeboxFanControl\config.json`**: used in Windows service mode; can share config with GUI.
|
||||
|
||||
Important fields:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `Language` | UI language: `auto` (system), `en`, `zh-Hans`, `zh-Hant`. Restart to apply. |
|
||||
| `RampUpSteps` | Steps for gradual fan speed change (1–10, default 3). |
|
||||
| `RampUpMinDeltaPercent` | Change below this % is applied in one step (0–50, default 20). |
|
||||
| `FanRpmArgs` | ectool args to read RPM (default `pwmgetfanrpm` `0`). Check with `ectool help` for your board. |
|
||||
| `FanDutyArgs` | ectool args to read duty (e.g. `pwmget` `0`); empty = skip, show target duty. |
|
||||
| `AutoFanCtrlArgs` | Command to restore EC auto fan on exit (default `autofanctrl`). |
|
||||
| `TempSource` | `AverageCore` or `MaxCore`. |
|
||||
| `CurvePoints` | 14 duty values 0–100 for temp breakpoints 0,40,45,…,100°C (linear interpolation). |
|
||||
|
||||
## Usage
|
||||
|
||||
- Tray icon after startup. **Pause control** to stop sending `fanduty`; EC keeps the last state.
|
||||
- On **exit**, the program tries to run the **auto fan restore** command (see config).
|
||||
- After several consecutive CPU temp read failures, **fail-safe** mode activates (fixed duty or `autofanctrl`, see UI options).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Software is provided “as is”. Incorrect fan settings may cause overheating or hardware damage; use at your own risk.
|
||||
|
||||
## License
|
||||
|
||||
Original code written for this project. Fan curve logic follows the public description in Chrultrabook-Tools; no GPL source code was copied.
|
||||
|
||||
---
|
||||
|
||||
# 中文 / Chinese
|
||||
|
||||
在刷了 coreboot 的 Chromebox 上,用 **LibreHardwareMonitor** 读取 **CPU 核心温度**,通过 Coolstar **crosec** 自带的 **`ectool`** 设置 **`fanduty`**,并从 **ectool** 读取 **风扇转速(RPM)**。主界面提供三条实时曲线:温度、目标占空比、转速。
|
||||
|
||||
### 项目缘由
|
||||
|
||||
Acer Chromebox CXI4 仅配有板载温度传感器,CPU 附近没有独立测温点。Chrome OS 和刷 coreboot 后的 Linux 都能用 CPU 核心温度驱动风扇,温控正常;而 Windows 只认主板传感器,高负载下热量未传到主板时 CPU 就已过热,系统直接关机,风扇反应不及。我试过改 coreboot、重编 EC 固件等办法,仍无法让 CPU 温度直接参与 EC 控扇。加之本人曾重编 EC 解除长期 25W 功耗墙以跑高性能,发热压力更大,传统控扇更显不足。于是有了这个项目。
|
||||
|
||||
**另含** [FanControl.ChromeboxEC](FanControl.ChromeboxEC/) 插件,用于 [Fan Control](https://github.com/Rem0o/FanControl.Releases),可在 Fan Control 中控制 Chromebox EC 风扇。
|
||||
|
||||
### Fan Control 配置说明
|
||||
|
||||
1. **安装插件**:将 `FanControl.ChromeboxEC.dll` 复制到 Fan Control 的 `Plugins` 文件夹,以管理员身份启动 Fan Control。
|
||||
2. **推荐配置**:CPU 核心温度 + 板载温度(EC Temp)双曲线,取最大值设置风扇。
|
||||
- 新建曲线 **CPU Package**:温度源选「CPU Package」,输出 0–100% 占空比。
|
||||
- 新建曲线 **Board Temp**:温度源选「Chromebox EC Temperature」,输出 0–100% 占空比。
|
||||
- 新建 **Mix** 曲线:函数选「最大」,输入上述两条曲线。
|
||||
- 将 **Chromebox EC Fan** 控制的曲线设为 **Mix**。
|
||||
|
||||
详见 [FanControl.ChromeboxEC 说明](FanControl.ChromeboxEC/README.md)。
|
||||
|
||||
## 依赖
|
||||
|
||||
1. 安装 [Coolstar CROS-EC 驱动](https://github.com/coolstar/crwindows)(或您当前使用的 crosec 安装包),确保存在默认可执行文件:
|
||||
`C:\Program Files\crosec\ectool.exe`
|
||||
若路径不同,在程序「高级」选项卡中修改。
|
||||
2. **以管理员身份运行**本程序(清单已要求提升权限;LHM 与 EC 通信通常需要管理员)。
|
||||
3. **客户机**:`.\build.ps1 -publish`(及带 `-msi` 时)默认**自包含单文件**发布,**已内含 .NET 8 运行时**,`dist\` 仅含数个 exe,无需再装运行时。若需多 DLL 或依赖系统 .NET 8 以减小体积,可加 `-multiFile` 或 `-frameworkDependent`。
|
||||
|
||||
## 编译
|
||||
|
||||
```powershell
|
||||
.\build.ps1
|
||||
```
|
||||
|
||||
### 发布 MSI 安装包(推荐)
|
||||
|
||||
```powershell
|
||||
.\build.ps1 -publish -msi
|
||||
```
|
||||
|
||||
`-publish` 只生成 `dist\`;加上 **`-msi`** 才用 WiX 生成安装包(需已安装 WiX)。输出两个 MSI:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `dist-installer\ChromeboxFanControl-Setup.msi` | **桌面版**:GUI + 可选 Windows 服务(功能树中可勾选) |
|
||||
| `dist-installer\ChromeboxFanControlService-Setup.msi` | **服务版**:仅后台服务,适合无头部署 |
|
||||
|
||||
**构建机**需 [.NET 8 SDK](https://dotnet.microsoft.com/download) 与 [WiX Toolset 3](https://wixtoolset.org/docs/wix3/)。**构建前请关闭正在运行的 ChromeboxFanControl.exe**。
|
||||
|
||||
以**管理员身份**运行 MSI。若尚未安装 ectool,可访问 [Chrultrabook 的 ectool 安装说明](https://docs.chrultrabook.com/docs/installing/ectool.html)。另见 [ectool 命令参考](docs/ectool-commands-zh.md)。服务模式下配置从 `%ProgramData%\ChromeboxFanControl\config.json` 读取。
|
||||
|
||||
### 仅发布到 dist\(用于调试或手动打包)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `.\build.ps1 -publish` | 同时发布桌面版与服务版到 `dist\`(默认单文件) |
|
||||
| `.\build.ps1 -publishGui` | 仅发布 GUI |
|
||||
| `.\build.ps1 -publishService` | 仅发布服务并合并进 `dist\` |
|
||||
| `.\build.ps1 -publish -multiFile` | 保留多 DLL 输出(默认单 exe) |
|
||||
| `.\build.ps1 -publish -frameworkDependent` | 依赖系统 .NET 8(无 bundled 运行时) |
|
||||
|
||||
## 配置
|
||||
|
||||
- 安装目录旁的 **`appsettings.json`**:默认选项。
|
||||
- **`%AppData%\ChromeboxFanControl\config.json`**:GUI 模式下保存后覆盖(用户配置)。
|
||||
- **`%ProgramData%\ChromeboxFanControl\config.json`**:Windows 服务模式下读取的配置;可与 GUI 共用同一份配置逻辑。
|
||||
|
||||
重要字段:
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| `Language` | 界面语言:`auto`(跟随系统)、`en`、`zh-Hans`、`zh-Hant`。修改后需重启生效。 |
|
||||
| `RampUpSteps` | 升/降速分步数(1–10,默认 3)。 |
|
||||
| `RampUpMinDeltaPercent` | 变化幅度低于此百分比则一步到位(0–50,默认 20)。 |
|
||||
| `FanRpmArgs` | 读取转速时传给 ectool 的参数(默认 `pwmgetfanrpm` `0`),请用本机 `ectool help` 核对。 |
|
||||
| `FanDutyArgs` | 读取占空比时传给 ectool 的参数(如 `pwmget` `0`);空则跳过,显示目标占空比。 |
|
||||
| `AutoFanCtrlArgs` | 退出程序时恢复 EC 自动风扇(默认 `autofanctrl`),若命令名不同请修改。 |
|
||||
| `TempSource` | `AverageCore` 或 `MaxCore`。 |
|
||||
| `CurvePoints` | 14 个 0–100 的占空比控制点(对应温度断点 0,40,45,…,100°C,线性插值)。 |
|
||||
|
||||
## 使用说明
|
||||
|
||||
- 启动后任务栏托盘有图标;可「暂停控制」(不再下发 `fanduty`,EC 保持上次状态)。
|
||||
- **退出**或关闭前会尝试执行 **自动风扇恢复**命令(见配置)。
|
||||
- 若连续多次读不到 CPU 温度,将进入**安全模式**(固定占空比或 `autofanctrl`,见界面选项)。
|
||||
|
||||
## 免责声明
|
||||
|
||||
软件按「原样」提供。错误的风扇策略可能导致过热或硬件损坏;请自行承担使用风险。
|
||||
|
||||
## 许可
|
||||
|
||||
本项目为实现计划而编写的原创代码;风扇曲线算法行为参考 Chrultrabook-Tools 的公开逻辑描述,未复制其 GPL 源码。
|
||||
# Chromebox Fan Control (Windows)
|
||||
|
||||
Read **CPU core temperature** via **LibreHardwareMonitor**, set **`fanduty`** through Coolstar **crosec**'s **ectool**, and read **fan RPM** from ectool. The UI shows three real-time charts: temperature, target duty, and RPM. On Chromebox with coreboot.
|
||||
|
||||
### Background
|
||||
|
||||
My Acer Chromebox CXI4 has only a board-mounted temperature sensor; there is no sensor near the CPU. On Chrome OS, the kernel uses CPU core temperature for fan control and thermal management works well. After flashing coreboot, Linux also uses CPU core temp for fan control. On Windows, however, the system relies on the motherboard temperature sensor for fan control, not CPU temperature. During sudden high load, heat has not yet reached the board sensor when the CPU already overheats, causing Windows to shut down before the fan can respond. I tried configuring coreboot, recompiling the EC firmware, and other approaches—none succeeded in connecting CPU core temperature to Chromebox EC fan control. Hence this project.
|
||||
|
||||
**Also included:** [FanControl.ChromeboxEC](FanControl.ChromeboxEC/) — a plugin for [Fan Control](https://github.com/Rem0o/FanControl.Releases) (Rémi Mercier) that adds Chromebox EC fan control. Use this if you prefer Fan Control’s UI and curve editor.
|
||||
|
||||
### Fan Control setup
|
||||
|
||||
1. **Install plugin**: Copy `FanControl.ChromeboxEC.dll` to Fan Control's `Plugins` folder; start Fan Control as Administrator.
|
||||
2. **Recommended config**: CPU core temp + board temp (EC Temp) dual curves, take max to set fan.
|
||||
- Create curve **CPU Package**: temp source = CPU Package, output 0–100% duty.
|
||||
- Create curve **Board Temp**: temp source = Chromebox EC Temperature, output 0–100% duty.
|
||||
- Create **Mix** curve: function = Max, inputs = both curves above.
|
||||
- Set **Chromebox EC Fan** control to use **Mix** curve.
|
||||
|
||||
See [FanControl.ChromeboxEC README](FanControl.ChromeboxEC/README.md) for details.
|
||||
|
||||
## Dependencies
|
||||
|
||||
1. Install [Coolstar CROS-EC driver](https://github.com/coolstar/crwindows) (or your crosec package). The default executable is:
|
||||
`C:\Program Files\crosec\ectool.exe`
|
||||
Change it in the program’s **Advanced** tab if needed.
|
||||
2. **Run as Administrator** (manifest requests elevation; LHM and EC access usually need admin).
|
||||
3. **Deployment**: `.\build.ps1 -publish` (and with `-msi`) defaults to **self-contained single-file**, **including .NET 8 runtime**. `dist\` has only a few exes. Use `-multiFile` or `-frameworkDependent` to avoid bundling the runtime.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
.\build.ps1
|
||||
```
|
||||
|
||||
### Publish MSI installer (recommended)
|
||||
|
||||
```powershell
|
||||
.\build.ps1 -publish -msi
|
||||
```
|
||||
|
||||
`-publish` produces `dist\`; **`-msi`** uses WiX to build installers (requires WiX). Two MSIs are produced:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `dist-installer\ChromeboxFanControl-Setup.msi` | **Desktop**: GUI + optional Windows service (checkbox in feature tree) |
|
||||
| `dist-installer\ChromeboxFanControlService-Setup.msi` | **Service only**: background service for headless deployment |
|
||||
|
||||
Build machine needs [.NET 8 SDK](https://dotnet.microsoft.com/download) and [WiX Toolset 3](https://wixtoolset.org/docs/wix3/). **Close ChromeboxFanControl.exe before building.**
|
||||
|
||||
Run the MSI **as Administrator**. If ectool is not installed yet, see [Chrultrabook ectool installation](https://docs.chrultrabook.com/docs/installing/ectool.html). See also [ectool 命令参考(中文)](docs/ectool-commands-zh.md). In service mode, config is read from `%ProgramData%\ChromeboxFanControl\config.json`.
|
||||
|
||||
### Publish to dist\ only (debugging or manual packaging)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `.\build.ps1 -publish` | Publish both desktop and service to `dist\` (single-file by default) |
|
||||
| `.\build.ps1 -publishGui` | Desktop only |
|
||||
| `.\build.ps1 -publishService` | Service only, merged into `dist\` |
|
||||
| `.\build.ps1 -publish -multiFile` | Keep multiple DLLs (instead of single exe) |
|
||||
| `.\build.ps1 -publish -frameworkDependent` | Rely on system .NET 8 (no bundled runtime) |
|
||||
|
||||
## Configuration
|
||||
|
||||
- **`appsettings.json`** beside the install directory: default options.
|
||||
- **`%AppData%\ChromeboxFanControl\config.json`**: user config; saved from GUI in desktop mode.
|
||||
- **`%ProgramData%\ChromeboxFanControl\config.json`**: used in Windows service mode; can share config with GUI.
|
||||
|
||||
Important fields:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `Language` | UI language: `auto` (system), `en`, `zh-Hans`, `zh-Hant`. Restart to apply. |
|
||||
| `RampUpSteps` | Steps for gradual fan speed change (1–10, default 3). |
|
||||
| `RampUpMinDeltaPercent` | Change below this % is applied in one step (0–50, default 20). |
|
||||
| `FanRpmArgs` | ectool args to read RPM (default `pwmgetfanrpm` `0`). Check with `ectool help` for your board. |
|
||||
| `FanDutyArgs` | ectool args to read duty (e.g. `pwmget` `0`); empty = skip, show target duty. |
|
||||
| `AutoFanCtrlArgs` | Command to restore EC auto fan on exit (default `autofanctrl`). |
|
||||
| `TempSource` | `AverageCore` or `MaxCore`. |
|
||||
| `CurvePoints` | 14 duty values 0–100 for temp breakpoints 0,40,45,…,100°C (linear interpolation). |
|
||||
|
||||
## Usage
|
||||
|
||||
- Tray icon after startup. **Pause control** to stop sending `fanduty`; EC keeps the last state.
|
||||
- On **exit**, the program tries to run the **auto fan restore** command (see config).
|
||||
- After several consecutive CPU temp read failures, **fail-safe** mode activates (fixed duty or `autofanctrl`, see UI options).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Software is provided “as is”. Incorrect fan settings may cause overheating or hardware damage; use at your own risk.
|
||||
|
||||
## License
|
||||
|
||||
Original code written for this project. Fan curve logic follows the public description in Chrultrabook-Tools; no GPL source code was copied.
|
||||
|
||||
---
|
||||
|
||||
# 中文 / Chinese
|
||||
|
||||
在刷了 coreboot 的 Chromebox 上,用 **LibreHardwareMonitor** 读取 **CPU 核心温度**,通过 Coolstar **crosec** 自带的 **`ectool`** 设置 **`fanduty`**,并从 **ectool** 读取 **风扇转速(RPM)**。主界面提供三条实时曲线:温度、目标占空比、转速。
|
||||
|
||||
### 项目缘由
|
||||
|
||||
Acer Chromebox CXI4 仅配有板载温度传感器,CPU 附近没有独立测温点。Chrome OS 和刷 coreboot 后的 Linux 都能用 CPU 核心温度驱动风扇,温控正常;而 Windows 只认主板传感器,高负载下热量未传到主板时 CPU 就已过热,系统直接关机,风扇反应不及。我试过改 coreboot、重编 EC 固件等办法,仍无法让 CPU 温度直接参与 EC 控扇。加之本人曾重编 EC 解除长期 25W 功耗墙以跑高性能,发热压力更大,传统控扇更显不足。于是有了这个项目。
|
||||
|
||||
**另含** [FanControl.ChromeboxEC](FanControl.ChromeboxEC/) 插件,用于 [Fan Control](https://github.com/Rem0o/FanControl.Releases),可在 Fan Control 中控制 Chromebox EC 风扇。
|
||||
|
||||
### Fan Control 配置说明
|
||||
|
||||
1. **安装插件**:将 `FanControl.ChromeboxEC.dll` 复制到 Fan Control 的 `Plugins` 文件夹,以管理员身份启动 Fan Control。
|
||||
2. **推荐配置**:CPU 核心温度 + 板载温度(EC Temp)双曲线,取最大值设置风扇。
|
||||
- 新建曲线 **CPU Package**:温度源选「CPU Package」,输出 0–100% 占空比。
|
||||
- 新建曲线 **Board Temp**:温度源选「Chromebox EC Temperature」,输出 0–100% 占空比。
|
||||
- 新建 **Mix** 曲线:函数选「最大」,输入上述两条曲线。
|
||||
- 将 **Chromebox EC Fan** 控制的曲线设为 **Mix**。
|
||||
|
||||
详见 [FanControl.ChromeboxEC 说明](FanControl.ChromeboxEC/README.md)。
|
||||
|
||||
## 依赖
|
||||
|
||||
1. 安装 [Coolstar CROS-EC 驱动](https://github.com/coolstar/crwindows)(或您当前使用的 crosec 安装包),确保存在默认可执行文件:
|
||||
`C:\Program Files\crosec\ectool.exe`
|
||||
若路径不同,在程序「高级」选项卡中修改。
|
||||
2. **以管理员身份运行**本程序(清单已要求提升权限;LHM 与 EC 通信通常需要管理员)。
|
||||
3. **客户机**:`.\build.ps1 -publish`(及带 `-msi` 时)默认**自包含单文件**发布,**已内含 .NET 8 运行时**,`dist\` 仅含数个 exe,无需再装运行时。若需多 DLL 或依赖系统 .NET 8 以减小体积,可加 `-multiFile` 或 `-frameworkDependent`。
|
||||
|
||||
## 编译
|
||||
|
||||
```powershell
|
||||
.\build.ps1
|
||||
```
|
||||
|
||||
### 发布 MSI 安装包(推荐)
|
||||
|
||||
```powershell
|
||||
.\build.ps1 -publish -msi
|
||||
```
|
||||
|
||||
`-publish` 只生成 `dist\`;加上 **`-msi`** 才用 WiX 生成安装包(需已安装 WiX)。输出两个 MSI:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `dist-installer\ChromeboxFanControl-Setup.msi` | **桌面版**:GUI + 可选 Windows 服务(功能树中可勾选) |
|
||||
| `dist-installer\ChromeboxFanControlService-Setup.msi` | **服务版**:仅后台服务,适合无头部署 |
|
||||
|
||||
**构建机**需 [.NET 8 SDK](https://dotnet.microsoft.com/download) 与 [WiX Toolset 3](https://wixtoolset.org/docs/wix3/)。**构建前请关闭正在运行的 ChromeboxFanControl.exe**。
|
||||
|
||||
以**管理员身份**运行 MSI。若尚未安装 ectool,可访问 [Chrultrabook 的 ectool 安装说明](https://docs.chrultrabook.com/docs/installing/ectool.html)。另见 [ectool 命令参考](docs/ectool-commands-zh.md)。服务模式下配置从 `%ProgramData%\ChromeboxFanControl\config.json` 读取。
|
||||
|
||||
### 仅发布到 dist\(用于调试或手动打包)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `.\build.ps1 -publish` | 同时发布桌面版与服务版到 `dist\`(默认单文件) |
|
||||
| `.\build.ps1 -publishGui` | 仅发布 GUI |
|
||||
| `.\build.ps1 -publishService` | 仅发布服务并合并进 `dist\` |
|
||||
| `.\build.ps1 -publish -multiFile` | 保留多 DLL 输出(默认单 exe) |
|
||||
| `.\build.ps1 -publish -frameworkDependent` | 依赖系统 .NET 8(无 bundled 运行时) |
|
||||
|
||||
## 配置
|
||||
|
||||
- 安装目录旁的 **`appsettings.json`**:默认选项。
|
||||
- **`%AppData%\ChromeboxFanControl\config.json`**:GUI 模式下保存后覆盖(用户配置)。
|
||||
- **`%ProgramData%\ChromeboxFanControl\config.json`**:Windows 服务模式下读取的配置;可与 GUI 共用同一份配置逻辑。
|
||||
|
||||
重要字段:
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| `Language` | 界面语言:`auto`(跟随系统)、`en`、`zh-Hans`、`zh-Hant`。修改后需重启生效。 |
|
||||
| `RampUpSteps` | 升/降速分步数(1–10,默认 3)。 |
|
||||
| `RampUpMinDeltaPercent` | 变化幅度低于此百分比则一步到位(0–50,默认 20)。 |
|
||||
| `FanRpmArgs` | 读取转速时传给 ectool 的参数(默认 `pwmgetfanrpm` `0`),请用本机 `ectool help` 核对。 |
|
||||
| `FanDutyArgs` | 读取占空比时传给 ectool 的参数(如 `pwmget` `0`);空则跳过,显示目标占空比。 |
|
||||
| `AutoFanCtrlArgs` | 退出程序时恢复 EC 自动风扇(默认 `autofanctrl`),若命令名不同请修改。 |
|
||||
| `TempSource` | `AverageCore` 或 `MaxCore`。 |
|
||||
| `CurvePoints` | 14 个 0–100 的占空比控制点(对应温度断点 0,40,45,…,100°C,线性插值)。 |
|
||||
|
||||
## 使用说明
|
||||
|
||||
- 启动后任务栏托盘有图标;可「暂停控制」(不再下发 `fanduty`,EC 保持上次状态)。
|
||||
- **退出**或关闭前会尝试执行 **自动风扇恢复**命令(见配置)。
|
||||
- 若连续多次读不到 CPU 温度,将进入**安全模式**(固定占空比或 `autofanctrl`,见界面选项)。
|
||||
|
||||
## 免责声明
|
||||
|
||||
软件按「原样」提供。错误的风扇策略可能导致过热或硬件损坏;请自行承担使用风险。
|
||||
|
||||
## 许可
|
||||
|
||||
本项目为实现计划而编写的原创代码;风扇曲线算法行为参考 Chrultrabook-Tools 的公开逻辑描述,未复制其 GPL 源码。
|
||||
|
||||
@@ -1,333 +1,333 @@
|
||||
# ectool 命令参考(中文)
|
||||
|
||||
`ectool` 是 Chrome OS 嵌入式控制器(EC)的命令行工具,用于从用户空间与 EC 通信。在刷了 coreboot 的 Chromebox/Chromebook 上,可通过 Coolstar CROS-EC 驱动的 `ectool.exe` 使用。
|
||||
|
||||
## 基本用法
|
||||
|
||||
```text
|
||||
ectool [选项] <命令> [参数]
|
||||
```
|
||||
|
||||
### 常用选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `--dev=n` | 指定设备号 |
|
||||
| `--interface=dev\|i2c\|lpc` | 指定接口类型 |
|
||||
| `--i2c_bus=n` | 指定 I2C 总线号(如 `--i2c_bus=7` 使用 /dev/i2c-7),隐含 `--interface=i2c` |
|
||||
| `--device=vid:pid` | 指定 USB 端点(如 `18d1:5022`) |
|
||||
| `--name=cros_ec\|cros_fp\|cros_pd\|cros_scp\|cros_ish` | 指定 EC 类型 |
|
||||
| `--ascii` | 以 ASCII 格式输出 |
|
||||
|
||||
---
|
||||
|
||||
## 风扇相关命令
|
||||
|
||||
### fanduty [idx] \<percent\>
|
||||
|
||||
强制将风扇 PWM 设为固定占空比(0–100)。
|
||||
|
||||
```text
|
||||
ectool fanduty 75 # 所有风扇 75%
|
||||
ectool fanduty 0 50 # 风扇 0 设为 50%
|
||||
```
|
||||
|
||||
### autofanctrl \<on\>
|
||||
|
||||
开启 EC 自动风扇转速控制。取消手动 `fanduty` 后,应调用此命令恢复 EC 自动控速。
|
||||
|
||||
```text
|
||||
ectool autofanctrl
|
||||
```
|
||||
|
||||
### pwmgetfanrpm [\<index\> | all]
|
||||
|
||||
读取风扇转速(RPM)。
|
||||
|
||||
```text
|
||||
ectool pwmgetfanrpm 0 # 读取第 0 号风扇,输出如 "Fan 0 RPM: 2621"
|
||||
ectool pwmgetfanrpm all # 读取所有风扇
|
||||
```
|
||||
|
||||
### pwmgetnumfans
|
||||
|
||||
显示风扇数量。
|
||||
|
||||
### pwmsetfanrpm \<targetrpm\>
|
||||
|
||||
设定目标风扇转速(RPM)。部分 EC 支持基于目标转速的控制。
|
||||
|
||||
### pwmgetduty \<pwm_idx\> | kb | disp
|
||||
|
||||
读取当前 PWM 占空比(16 位,0–65535)。
|
||||
|
||||
```text
|
||||
ectool pwmgetduty 0 # 风扇 0,输出如 "Current PWM duty: 50462"(约 77%)
|
||||
```
|
||||
百分比 = 数值 / 65535 × 100
|
||||
|
||||
### pwmsetduty
|
||||
|
||||
设置 PWM 占空比(16 位)。需配合具体参数使用。
|
||||
|
||||
---
|
||||
|
||||
## 温度相关命令
|
||||
|
||||
### temps \<sensorid\>
|
||||
|
||||
读取指定温度传感器的温度,以及与 `fan_off` / `fan_max` 的比值。
|
||||
|
||||
```text
|
||||
ectool temps 0 # 输出如 "Core 336 K (= 63 C) 100% (313 K and 333 K)"
|
||||
```
|
||||
|
||||
输出为开尔文(K)及摄氏(C)。传感器 ID 因主板而异,用 `tempsinfo` 查看。
|
||||
|
||||
### tempsinfo \<sensorid\>
|
||||
|
||||
显示温度传感器信息,用于确认本机支持的传感器 ID 和名称。
|
||||
|
||||
```text
|
||||
ectool tempsinfo 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 电源与电池
|
||||
|
||||
### battery
|
||||
|
||||
显示电池信息。
|
||||
|
||||
### powerinfo
|
||||
|
||||
显示电源相关信息。
|
||||
|
||||
---
|
||||
|
||||
## 系统与固件
|
||||
|
||||
### chipinfo
|
||||
|
||||
显示 EC 芯片信息。
|
||||
|
||||
### version
|
||||
|
||||
显示 EC 固件版本。
|
||||
|
||||
### hello
|
||||
|
||||
检测与 EC 的基本通信是否正常。
|
||||
|
||||
### console
|
||||
|
||||
显示 EC 调试控制台最新输出。
|
||||
|
||||
### sysinfo [flags\|reset_flags\|firmware_copy]
|
||||
|
||||
显示系统信息。
|
||||
|
||||
---
|
||||
|
||||
## 其他常用命令
|
||||
|
||||
### switches
|
||||
|
||||
显示 EC 开关状态(如盖板、电源等)。
|
||||
|
||||
### boardversion
|
||||
|
||||
显示板级版本。
|
||||
|
||||
### flashinfo
|
||||
|
||||
显示 EC Flash 信息。
|
||||
|
||||
### rtcget / rtcset
|
||||
|
||||
读取/设置 EC 内部 RTC。
|
||||
|
||||
### led \<name\> \<query\|auto\|off\|color\>
|
||||
|
||||
控制 LED 颜色或查询亮度范围。
|
||||
|
||||
---
|
||||
|
||||
## 与 Chromebox 风扇温控的对应关系
|
||||
|
||||
本仓库中的 **ChromeboxFanControl** 和 **FanControl.ChromeboxEC** 插件主要使用以下命令:
|
||||
|
||||
| 功能 | 命令 | 说明 |
|
||||
|------|------|------|
|
||||
| 设定风扇占空比 | `ectool fanduty <0-100>` | 手动控速 |
|
||||
| 恢复自动控速 | `ectool autofanctrl` | 退出时恢复 |
|
||||
| 读取转速 | `ectool pwmgetfanrpm 0` | 显示 RPM |
|
||||
| 读取温度 | `ectool temps` | 从 EC 读取温度 |
|
||||
|
||||
不同主板的 `pwmgetfanrpm`、`temps` 参数可能不同,请用 `ectool help` 和 `ectool tempsinfo` 确认本机用法。
|
||||
|
||||
---
|
||||
|
||||
## 完整命令列表(英文字母序)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| adcread \<channel\> | 读取 ADC 通道 |
|
||||
| addentropy [reset] | 向设备秘密添加熵 |
|
||||
| apreset | 发起 AP 复位 |
|
||||
| autofanctrl \<on\> | 开启自动风扇控制 |
|
||||
| backlight \<enabled\> | 启用/禁用 LCD 背光 |
|
||||
| basestate [attach\|detach\|reset] | 强制底座状态 |
|
||||
| battery | 电池信息 |
|
||||
| batterycutoff [at-shutdown] | 切断电池输出 |
|
||||
| batteryparam | 读写板级电池参数 |
|
||||
| boardversion | 板级版本 |
|
||||
| button [vup\|vdown\|rec] \<Delay-ms\> | 模拟按键 |
|
||||
| cbi | 读写 Cros Board Info |
|
||||
| chargecurrentlimit | 设置最大充电电流 |
|
||||
| chargecontrol | 强制停止充电或放电 |
|
||||
| chargeoverride | 覆盖充电口选择逻辑 |
|
||||
| chargesplash | 充电动画相关 |
|
||||
| chargestate | 充电状态 v2+ |
|
||||
| chipinfo | 芯片信息 |
|
||||
| cmdversions \<cmd\> | 命令版本掩码 |
|
||||
| console | EC 调试控制台输出 |
|
||||
| cec | CEC 消息读写 |
|
||||
| echash [CMDS] | EC hash 相关 |
|
||||
| eventclear \<mask\> | 清除 EC 主机事件 |
|
||||
| eventclearb \<mask\> | 清除 EC 主机事件副本 B |
|
||||
| eventget | 原始 EC 主机事件标志 |
|
||||
| eventgetb | 原始 EC 主机事件标志副本 B |
|
||||
| eventgetscimask | SCI 掩码 |
|
||||
| eventgetsmimask | SMI 掩码 |
|
||||
| eventgetwakemask | 唤醒掩码 |
|
||||
| eventsetscimask \<mask\> | 设置 SCI 掩码 |
|
||||
| eventsetsmimask \<mask\> | 设置 SMI 掩码 |
|
||||
| eventsetwakemask \<mask\> | 设置唤醒掩码 |
|
||||
| extpwrlimit | 外部功率限制 |
|
||||
| fanduty \<percent\> | 固定风扇 PWM 占空比 |
|
||||
| flasherase \<offset\> \<size\> | 擦除 EC Flash |
|
||||
| flasheraseasync \<offset\> \<size\> | 异步擦除 EC Flash |
|
||||
| flashinfo | EC Flash 信息 |
|
||||
| flashspiinfo | EC SPI Flash 信息 |
|
||||
| flashpd \<dev_id\> \<port\> \<filename\> | 通过 PD 刷写 |
|
||||
| flashprotect [now] [enable\|disable] | Flash 写保护 |
|
||||
| flashread \<offset\> \<size\> \<outfile\> | 从 EC Flash 读取 |
|
||||
| flashwrite \<offset\> \<infile\> | 写入 EC Flash |
|
||||
| forcelidopen \<enable\> | 强制盖板为打开 |
|
||||
| fpcontext | 指纹传感器上下文 |
|
||||
| fpencstatus | 指纹加密引擎状态 |
|
||||
| fpframe | 获取指纹图像 |
|
||||
| fpinfo | 指纹传感器信息 |
|
||||
| fpmode [mode...] | 指纹传感器模式 |
|
||||
| fpseed | 设置 TPM seed |
|
||||
| fpstats | 指纹匹配时序统计 |
|
||||
| fptemplate [\<infile\>\|\<index\>] | 添加/导出指纹模板 |
|
||||
| gpioget \<GPIO name\> | 读取 GPIO |
|
||||
| gpioset \<GPIO name\> | 设置 GPIO |
|
||||
| hangdetect | 挂起检测定时器 |
|
||||
| hello | 检测 EC 通信 |
|
||||
| hibdelay [sec] | 休眠前延时 |
|
||||
| hostsleepstate | 主机睡眠状态 |
|
||||
| hostevent | 主机事件掩码 |
|
||||
| i2cprotect \<port\> | I2C 总线保护 |
|
||||
| i2cread | I2C 读取 |
|
||||
| i2cspeed \<port\> [speed] | I2C 总线速率 |
|
||||
| i2cwrite | I2C 写入 |
|
||||
| i2cxfer \<port\> \<addr\> \<read_count\> [write bytes...] | I2C 传输 |
|
||||
| infopddev \<port\> | USB-C 配件信息 |
|
||||
| inventory | 支持功能列表 |
|
||||
| kbfactorytest | 键盘工厂测试 |
|
||||
| kbid | 键盘 ID |
|
||||
| kbinfo | 键盘矩阵信息 |
|
||||
| kbpress | 模拟按键 |
|
||||
| keyscan \<beat_us\> \<filename\> | 按键扫描测试 |
|
||||
| led \<name\> \<query\|auto\|off\|color\> | LED 控制 |
|
||||
| lightbar [CMDS] | 灯条控制 |
|
||||
| locatechip \<type\> \<index\> | 查找芯片地址 |
|
||||
| mkbpget \<buttons\|switches\> | MKBP 按键/开关 |
|
||||
| mkbpwakemask | MKBP 唤醒掩码 |
|
||||
| motionsense [CMDS] | 运动传感器 |
|
||||
| panicinfo | 崩溃信息 |
|
||||
| pause_in_s5 [on\|off] | S5 关机时是否暂停 |
|
||||
| pchg [\<port\>] | 外设充电口 |
|
||||
| pdcontrol [suspend\|resume\|reset\|disable\|on] | PD 芯片控制 |
|
||||
| pdchipinfo \<port\> | PD 芯片信息 |
|
||||
| pdlog | PD 事件日志 |
|
||||
| pdwritelog \<type\> \<port\> | 写入 PD 日志 |
|
||||
| pdgetmode \<port\> | 获取 USB-PD 模式 |
|
||||
| pdsetmode \<port\> \<svid\> \<opos\> | 设置 USB-PD 模式 |
|
||||
| port80flood | 快速写 port 80 |
|
||||
| port80read | port 80 历史 |
|
||||
| powerinfo | 电源信息 |
|
||||
| protoinfo | EC 主机协议信息 |
|
||||
| pse | PoE PSE 端口功率 |
|
||||
| pstoreinfo | 持久存储信息 |
|
||||
| pstoreread \<offset\> \<size\> \<outfile\> | 读取持久存储 |
|
||||
| pstorewrite \<offset\> \<infile\> | 写入持久存储 |
|
||||
| pwmgetfanrpm [\<index\>\|all] | 风扇转速 |
|
||||
| pwmgetkblight | 键盘背光百分比 |
|
||||
| pwmgetnumfans | 风扇数量 |
|
||||
| pwmgetduty | 当前 PWM 占空比 |
|
||||
| pwmsetfanrpm \<targetrpm\> | 设定目标风扇转速 |
|
||||
| pwmsetkblight \<percent\> | 键盘背光百分比 |
|
||||
| pwmsetduty | 设置 PWM 占空比 |
|
||||
| rand \<num_bytes\> | 生成随机数 |
|
||||
| readtest | EC 读取测试 |
|
||||
| reboot_ec \<RO\|RW\|cold\|...\> | 重启 EC |
|
||||
| reboot_ap_on_g3 [\<delay\>] | G3 后自动重启 AP |
|
||||
| rgbkbd ... | RGB 键盘 |
|
||||
| rollbackinfo | 回滚块信息 |
|
||||
| rtcget | 读取 RTC |
|
||||
| rtcgetalarm | RTC 闹钟剩余秒数 |
|
||||
| rtcset \<time\> | 设置 RTC |
|
||||
| rtcsetalarm \<sec\> | 设置 RTC 闹钟 |
|
||||
| rwhashpd | PD MCU rw_hash |
|
||||
| rwsig \<info\|dump\|action\|status\> | RW 签名相关 |
|
||||
| sertest | 串口输出测试 |
|
||||
| smartdischarge | 智能放电参数 |
|
||||
| stress [reboot] [help] | 压力测试 |
|
||||
| sysinfo [flags\|reset_flags\|firmware_copy] | 系统信息 |
|
||||
| switches | EC 开关状态 |
|
||||
| temps \<sensorid\> | 温度传感器读数 |
|
||||
| tempsinfo \<sensorid\> | 温度传感器信息 |
|
||||
| thermalget | 读取热阈温 |
|
||||
| thermalset | 设置热阈温 |
|
||||
| tpselftest | 触摸板自检 |
|
||||
| tpframeget | 触摸板帧数据 |
|
||||
| tmp006cal | TMP006 校准 |
|
||||
| tmp006raw | TMP006 原始数据 |
|
||||
| typeccontrol \<port\> \<command\> | USB PD 策略 |
|
||||
| typecdiscovery \<port\> \<type\> | USB-C 发现信息 |
|
||||
| typecstatus \<port\> | USB-C 状态 |
|
||||
| uptimeinfo | EC 运行时长与 AP 复位 |
|
||||
| usbchargemode \<port\> \<mode\> | USB 充电模式 |
|
||||
| usbmux \<mux\> | USB Mux 状态 |
|
||||
| usbpd | USB PD 控制(已弃用) |
|
||||
| usbpddps [enable\|disable] | 动态 PDO 选择 |
|
||||
| usbpdmuxinfo [tsv] | USB-C SS Mux 信息 |
|
||||
| usbpdpower [port] | USB PD 功率 |
|
||||
| version | EC 版本 |
|
||||
| waitevent \<type\> [\<timeout\>] | 等待 MKBP 事件 |
|
||||
| wireless \<flags\> | WLAN/蓝牙无线控制 |
|
||||
|
||||
---
|
||||
|
||||
## 常用命令速查(实测 Coolstar ectool)
|
||||
|
||||
| 操作 | 命令 |
|
||||
|------|------|
|
||||
| 读取温度 | `ectool temps 0` |
|
||||
| 读取风扇转速 (RPM) | `ectool pwmgetfanrpm 0` 或 `ectool pwmgetfanrpm all` |
|
||||
| 读取占空比 (16 位) | `ectool pwmgetduty 0`,百分比 = 数值/65535×100 |
|
||||
| 设置风扇占空比 | `ectool fanduty 75`(所有风扇)或 `ectool fanduty 0 50`(风扇 0) |
|
||||
| 设置目标 RPM | `ectool pwmsetfanrpm 2000` |
|
||||
| 恢复 EC 自动控速 | `ectool autofanctrl` |
|
||||
|
||||
---
|
||||
|
||||
## 参考
|
||||
|
||||
- [Chrultrabook ectool 安装说明](https://docs.chrultrabook.com/docs/installing/ectool.html)
|
||||
- [Coolstar CROS-EC 驱动](https://github.com/coolstar/crwindows)
|
||||
# ectool 命令参考(中文)
|
||||
|
||||
`ectool` 是 Chrome OS 嵌入式控制器(EC)的命令行工具,用于从用户空间与 EC 通信。在刷了 coreboot 的 Chromebox/Chromebook 上,可通过 Coolstar CROS-EC 驱动的 `ectool.exe` 使用。
|
||||
|
||||
## 基本用法
|
||||
|
||||
```text
|
||||
ectool [选项] <命令> [参数]
|
||||
```
|
||||
|
||||
### 常用选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `--dev=n` | 指定设备号 |
|
||||
| `--interface=dev\|i2c\|lpc` | 指定接口类型 |
|
||||
| `--i2c_bus=n` | 指定 I2C 总线号(如 `--i2c_bus=7` 使用 /dev/i2c-7),隐含 `--interface=i2c` |
|
||||
| `--device=vid:pid` | 指定 USB 端点(如 `18d1:5022`) |
|
||||
| `--name=cros_ec\|cros_fp\|cros_pd\|cros_scp\|cros_ish` | 指定 EC 类型 |
|
||||
| `--ascii` | 以 ASCII 格式输出 |
|
||||
|
||||
---
|
||||
|
||||
## 风扇相关命令
|
||||
|
||||
### fanduty [idx] \<percent\>
|
||||
|
||||
强制将风扇 PWM 设为固定占空比(0–100)。
|
||||
|
||||
```text
|
||||
ectool fanduty 75 # 所有风扇 75%
|
||||
ectool fanduty 0 50 # 风扇 0 设为 50%
|
||||
```
|
||||
|
||||
### autofanctrl \<on\>
|
||||
|
||||
开启 EC 自动风扇转速控制。取消手动 `fanduty` 后,应调用此命令恢复 EC 自动控速。
|
||||
|
||||
```text
|
||||
ectool autofanctrl
|
||||
```
|
||||
|
||||
### pwmgetfanrpm [\<index\> | all]
|
||||
|
||||
读取风扇转速(RPM)。
|
||||
|
||||
```text
|
||||
ectool pwmgetfanrpm 0 # 读取第 0 号风扇,输出如 "Fan 0 RPM: 2621"
|
||||
ectool pwmgetfanrpm all # 读取所有风扇
|
||||
```
|
||||
|
||||
### pwmgetnumfans
|
||||
|
||||
显示风扇数量。
|
||||
|
||||
### pwmsetfanrpm \<targetrpm\>
|
||||
|
||||
设定目标风扇转速(RPM)。部分 EC 支持基于目标转速的控制。
|
||||
|
||||
### pwmgetduty \<pwm_idx\> | kb | disp
|
||||
|
||||
读取当前 PWM 占空比(16 位,0–65535)。
|
||||
|
||||
```text
|
||||
ectool pwmgetduty 0 # 风扇 0,输出如 "Current PWM duty: 50462"(约 77%)
|
||||
```
|
||||
百分比 = 数值 / 65535 × 100
|
||||
|
||||
### pwmsetduty
|
||||
|
||||
设置 PWM 占空比(16 位)。需配合具体参数使用。
|
||||
|
||||
---
|
||||
|
||||
## 温度相关命令
|
||||
|
||||
### temps \<sensorid\>
|
||||
|
||||
读取指定温度传感器的温度,以及与 `fan_off` / `fan_max` 的比值。
|
||||
|
||||
```text
|
||||
ectool temps 0 # 输出如 "Core 336 K (= 63 C) 100% (313 K and 333 K)"
|
||||
```
|
||||
|
||||
输出为开尔文(K)及摄氏(C)。传感器 ID 因主板而异,用 `tempsinfo` 查看。
|
||||
|
||||
### tempsinfo \<sensorid\>
|
||||
|
||||
显示温度传感器信息,用于确认本机支持的传感器 ID 和名称。
|
||||
|
||||
```text
|
||||
ectool tempsinfo 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 电源与电池
|
||||
|
||||
### battery
|
||||
|
||||
显示电池信息。
|
||||
|
||||
### powerinfo
|
||||
|
||||
显示电源相关信息。
|
||||
|
||||
---
|
||||
|
||||
## 系统与固件
|
||||
|
||||
### chipinfo
|
||||
|
||||
显示 EC 芯片信息。
|
||||
|
||||
### version
|
||||
|
||||
显示 EC 固件版本。
|
||||
|
||||
### hello
|
||||
|
||||
检测与 EC 的基本通信是否正常。
|
||||
|
||||
### console
|
||||
|
||||
显示 EC 调试控制台最新输出。
|
||||
|
||||
### sysinfo [flags\|reset_flags\|firmware_copy]
|
||||
|
||||
显示系统信息。
|
||||
|
||||
---
|
||||
|
||||
## 其他常用命令
|
||||
|
||||
### switches
|
||||
|
||||
显示 EC 开关状态(如盖板、电源等)。
|
||||
|
||||
### boardversion
|
||||
|
||||
显示板级版本。
|
||||
|
||||
### flashinfo
|
||||
|
||||
显示 EC Flash 信息。
|
||||
|
||||
### rtcget / rtcset
|
||||
|
||||
读取/设置 EC 内部 RTC。
|
||||
|
||||
### led \<name\> \<query\|auto\|off\|color\>
|
||||
|
||||
控制 LED 颜色或查询亮度范围。
|
||||
|
||||
---
|
||||
|
||||
## 与 Chromebox 风扇温控的对应关系
|
||||
|
||||
本仓库中的 **ChromeboxFanControl** 和 **FanControl.ChromeboxEC** 插件主要使用以下命令:
|
||||
|
||||
| 功能 | 命令 | 说明 |
|
||||
|------|------|------|
|
||||
| 设定风扇占空比 | `ectool fanduty <0-100>` | 手动控速 |
|
||||
| 恢复自动控速 | `ectool autofanctrl` | 退出时恢复 |
|
||||
| 读取转速 | `ectool pwmgetfanrpm 0` | 显示 RPM |
|
||||
| 读取温度 | `ectool temps` | 从 EC 读取温度 |
|
||||
|
||||
不同主板的 `pwmgetfanrpm`、`temps` 参数可能不同,请用 `ectool help` 和 `ectool tempsinfo` 确认本机用法。
|
||||
|
||||
---
|
||||
|
||||
## 完整命令列表(英文字母序)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| adcread \<channel\> | 读取 ADC 通道 |
|
||||
| addentropy [reset] | 向设备秘密添加熵 |
|
||||
| apreset | 发起 AP 复位 |
|
||||
| autofanctrl \<on\> | 开启自动风扇控制 |
|
||||
| backlight \<enabled\> | 启用/禁用 LCD 背光 |
|
||||
| basestate [attach\|detach\|reset] | 强制底座状态 |
|
||||
| battery | 电池信息 |
|
||||
| batterycutoff [at-shutdown] | 切断电池输出 |
|
||||
| batteryparam | 读写板级电池参数 |
|
||||
| boardversion | 板级版本 |
|
||||
| button [vup\|vdown\|rec] \<Delay-ms\> | 模拟按键 |
|
||||
| cbi | 读写 Cros Board Info |
|
||||
| chargecurrentlimit | 设置最大充电电流 |
|
||||
| chargecontrol | 强制停止充电或放电 |
|
||||
| chargeoverride | 覆盖充电口选择逻辑 |
|
||||
| chargesplash | 充电动画相关 |
|
||||
| chargestate | 充电状态 v2+ |
|
||||
| chipinfo | 芯片信息 |
|
||||
| cmdversions \<cmd\> | 命令版本掩码 |
|
||||
| console | EC 调试控制台输出 |
|
||||
| cec | CEC 消息读写 |
|
||||
| echash [CMDS] | EC hash 相关 |
|
||||
| eventclear \<mask\> | 清除 EC 主机事件 |
|
||||
| eventclearb \<mask\> | 清除 EC 主机事件副本 B |
|
||||
| eventget | 原始 EC 主机事件标志 |
|
||||
| eventgetb | 原始 EC 主机事件标志副本 B |
|
||||
| eventgetscimask | SCI 掩码 |
|
||||
| eventgetsmimask | SMI 掩码 |
|
||||
| eventgetwakemask | 唤醒掩码 |
|
||||
| eventsetscimask \<mask\> | 设置 SCI 掩码 |
|
||||
| eventsetsmimask \<mask\> | 设置 SMI 掩码 |
|
||||
| eventsetwakemask \<mask\> | 设置唤醒掩码 |
|
||||
| extpwrlimit | 外部功率限制 |
|
||||
| fanduty \<percent\> | 固定风扇 PWM 占空比 |
|
||||
| flasherase \<offset\> \<size\> | 擦除 EC Flash |
|
||||
| flasheraseasync \<offset\> \<size\> | 异步擦除 EC Flash |
|
||||
| flashinfo | EC Flash 信息 |
|
||||
| flashspiinfo | EC SPI Flash 信息 |
|
||||
| flashpd \<dev_id\> \<port\> \<filename\> | 通过 PD 刷写 |
|
||||
| flashprotect [now] [enable\|disable] | Flash 写保护 |
|
||||
| flashread \<offset\> \<size\> \<outfile\> | 从 EC Flash 读取 |
|
||||
| flashwrite \<offset\> \<infile\> | 写入 EC Flash |
|
||||
| forcelidopen \<enable\> | 强制盖板为打开 |
|
||||
| fpcontext | 指纹传感器上下文 |
|
||||
| fpencstatus | 指纹加密引擎状态 |
|
||||
| fpframe | 获取指纹图像 |
|
||||
| fpinfo | 指纹传感器信息 |
|
||||
| fpmode [mode...] | 指纹传感器模式 |
|
||||
| fpseed | 设置 TPM seed |
|
||||
| fpstats | 指纹匹配时序统计 |
|
||||
| fptemplate [\<infile\>\|\<index\>] | 添加/导出指纹模板 |
|
||||
| gpioget \<GPIO name\> | 读取 GPIO |
|
||||
| gpioset \<GPIO name\> | 设置 GPIO |
|
||||
| hangdetect | 挂起检测定时器 |
|
||||
| hello | 检测 EC 通信 |
|
||||
| hibdelay [sec] | 休眠前延时 |
|
||||
| hostsleepstate | 主机睡眠状态 |
|
||||
| hostevent | 主机事件掩码 |
|
||||
| i2cprotect \<port\> | I2C 总线保护 |
|
||||
| i2cread | I2C 读取 |
|
||||
| i2cspeed \<port\> [speed] | I2C 总线速率 |
|
||||
| i2cwrite | I2C 写入 |
|
||||
| i2cxfer \<port\> \<addr\> \<read_count\> [write bytes...] | I2C 传输 |
|
||||
| infopddev \<port\> | USB-C 配件信息 |
|
||||
| inventory | 支持功能列表 |
|
||||
| kbfactorytest | 键盘工厂测试 |
|
||||
| kbid | 键盘 ID |
|
||||
| kbinfo | 键盘矩阵信息 |
|
||||
| kbpress | 模拟按键 |
|
||||
| keyscan \<beat_us\> \<filename\> | 按键扫描测试 |
|
||||
| led \<name\> \<query\|auto\|off\|color\> | LED 控制 |
|
||||
| lightbar [CMDS] | 灯条控制 |
|
||||
| locatechip \<type\> \<index\> | 查找芯片地址 |
|
||||
| mkbpget \<buttons\|switches\> | MKBP 按键/开关 |
|
||||
| mkbpwakemask | MKBP 唤醒掩码 |
|
||||
| motionsense [CMDS] | 运动传感器 |
|
||||
| panicinfo | 崩溃信息 |
|
||||
| pause_in_s5 [on\|off] | S5 关机时是否暂停 |
|
||||
| pchg [\<port\>] | 外设充电口 |
|
||||
| pdcontrol [suspend\|resume\|reset\|disable\|on] | PD 芯片控制 |
|
||||
| pdchipinfo \<port\> | PD 芯片信息 |
|
||||
| pdlog | PD 事件日志 |
|
||||
| pdwritelog \<type\> \<port\> | 写入 PD 日志 |
|
||||
| pdgetmode \<port\> | 获取 USB-PD 模式 |
|
||||
| pdsetmode \<port\> \<svid\> \<opos\> | 设置 USB-PD 模式 |
|
||||
| port80flood | 快速写 port 80 |
|
||||
| port80read | port 80 历史 |
|
||||
| powerinfo | 电源信息 |
|
||||
| protoinfo | EC 主机协议信息 |
|
||||
| pse | PoE PSE 端口功率 |
|
||||
| pstoreinfo | 持久存储信息 |
|
||||
| pstoreread \<offset\> \<size\> \<outfile\> | 读取持久存储 |
|
||||
| pstorewrite \<offset\> \<infile\> | 写入持久存储 |
|
||||
| pwmgetfanrpm [\<index\>\|all] | 风扇转速 |
|
||||
| pwmgetkblight | 键盘背光百分比 |
|
||||
| pwmgetnumfans | 风扇数量 |
|
||||
| pwmgetduty | 当前 PWM 占空比 |
|
||||
| pwmsetfanrpm \<targetrpm\> | 设定目标风扇转速 |
|
||||
| pwmsetkblight \<percent\> | 键盘背光百分比 |
|
||||
| pwmsetduty | 设置 PWM 占空比 |
|
||||
| rand \<num_bytes\> | 生成随机数 |
|
||||
| readtest | EC 读取测试 |
|
||||
| reboot_ec \<RO\|RW\|cold\|...\> | 重启 EC |
|
||||
| reboot_ap_on_g3 [\<delay\>] | G3 后自动重启 AP |
|
||||
| rgbkbd ... | RGB 键盘 |
|
||||
| rollbackinfo | 回滚块信息 |
|
||||
| rtcget | 读取 RTC |
|
||||
| rtcgetalarm | RTC 闹钟剩余秒数 |
|
||||
| rtcset \<time\> | 设置 RTC |
|
||||
| rtcsetalarm \<sec\> | 设置 RTC 闹钟 |
|
||||
| rwhashpd | PD MCU rw_hash |
|
||||
| rwsig \<info\|dump\|action\|status\> | RW 签名相关 |
|
||||
| sertest | 串口输出测试 |
|
||||
| smartdischarge | 智能放电参数 |
|
||||
| stress [reboot] [help] | 压力测试 |
|
||||
| sysinfo [flags\|reset_flags\|firmware_copy] | 系统信息 |
|
||||
| switches | EC 开关状态 |
|
||||
| temps \<sensorid\> | 温度传感器读数 |
|
||||
| tempsinfo \<sensorid\> | 温度传感器信息 |
|
||||
| thermalget | 读取热阈温 |
|
||||
| thermalset | 设置热阈温 |
|
||||
| tpselftest | 触摸板自检 |
|
||||
| tpframeget | 触摸板帧数据 |
|
||||
| tmp006cal | TMP006 校准 |
|
||||
| tmp006raw | TMP006 原始数据 |
|
||||
| typeccontrol \<port\> \<command\> | USB PD 策略 |
|
||||
| typecdiscovery \<port\> \<type\> | USB-C 发现信息 |
|
||||
| typecstatus \<port\> | USB-C 状态 |
|
||||
| uptimeinfo | EC 运行时长与 AP 复位 |
|
||||
| usbchargemode \<port\> \<mode\> | USB 充电模式 |
|
||||
| usbmux \<mux\> | USB Mux 状态 |
|
||||
| usbpd | USB PD 控制(已弃用) |
|
||||
| usbpddps [enable\|disable] | 动态 PDO 选择 |
|
||||
| usbpdmuxinfo [tsv] | USB-C SS Mux 信息 |
|
||||
| usbpdpower [port] | USB PD 功率 |
|
||||
| version | EC 版本 |
|
||||
| waitevent \<type\> [\<timeout\>] | 等待 MKBP 事件 |
|
||||
| wireless \<flags\> | WLAN/蓝牙无线控制 |
|
||||
|
||||
---
|
||||
|
||||
## 常用命令速查(实测 Coolstar ectool)
|
||||
|
||||
| 操作 | 命令 |
|
||||
|------|------|
|
||||
| 读取温度 | `ectool temps 0` |
|
||||
| 读取风扇转速 (RPM) | `ectool pwmgetfanrpm 0` 或 `ectool pwmgetfanrpm all` |
|
||||
| 读取占空比 (16 位) | `ectool pwmgetduty 0`,百分比 = 数值/65535×100 |
|
||||
| 设置风扇占空比 | `ectool fanduty 75`(所有风扇)或 `ectool fanduty 0 50`(风扇 0) |
|
||||
| 设置目标 RPM | `ectool pwmsetfanrpm 2000` |
|
||||
| 恢复 EC 自动控速 | `ectool autofanctrl` |
|
||||
|
||||
---
|
||||
|
||||
## 参考
|
||||
|
||||
- [Chrultrabook ectool 安装说明](https://docs.chrultrabook.com/docs/installing/ectool.html)
|
||||
- [Coolstar CROS-EC 驱动](https://github.com/coolstar/crwindows)
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
<Product Id="*" Name="Chromebox Fan Control Service" Language="1033" Version="1.0.0" Manufacturer="ChromeboxFanControl"
|
||||
UpgradeCode="B2C3D4E5-F6A7-8901-BCDE-F12345678901">
|
||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" Platform="x64" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Feature Id="ServiceFeature" Title="Chromebox Fan Control Service (background, auto-start)" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
||||
<ComponentGroupRef Id="ServiceFiles" />
|
||||
<ComponentRef Id="ConfigFolderComponent" />
|
||||
<ComponentRef Id="ServiceComponent" />
|
||||
</Feature>
|
||||
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="Chromebox Fan Control" />
|
||||
</Directory>
|
||||
<Directory Id="CommonAppDataFolder">
|
||||
<Directory Id="ConfigFolder" Name="ChromeboxFanControl" />
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="ServiceComponent" Guid="C3D4E5F6-A7B8-9012-CDEF-F12345678902">
|
||||
<File Id="ServiceExe" Source="ChromeboxFanControlService.exe" KeyPath="yes" />
|
||||
<ServiceInstall Id="ChromeboxFanControlSvc" Name="ChromeboxFanControl"
|
||||
DisplayName="Chromebox Fan Control Service" Description="Fan control for Chromebox with Coreboot"
|
||||
Type="ownProcess" Start="auto" Account="LocalSystem" ErrorControl="normal" />
|
||||
<ServiceControl Id="StartService" Name="ChromeboxFanControl" Start="install" />
|
||||
<ServiceControl Id="StopService" Name="ChromeboxFanControl" Stop="both" Remove="uninstall" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="ConfigFolder">
|
||||
<Component Id="ConfigFolderComponent" Guid="D4E5F6A7-B8C9-0123-DEF0-123456789003">
|
||||
<CreateFolder>
|
||||
<util:PermissionEx User="Users" GenericAll="yes" />
|
||||
</CreateFolder>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
</Product>
|
||||
</Wix>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
<Product Id="*" Name="Chromebox Fan Control Service" Language="1033" Version="1.0.0" Manufacturer="ChromeboxFanControl"
|
||||
UpgradeCode="B2C3D4E5-F6A7-8901-BCDE-F12345678901">
|
||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" Platform="x64" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Feature Id="ServiceFeature" Title="Chromebox Fan Control Service (background, auto-start)" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
||||
<ComponentGroupRef Id="ServiceFiles" />
|
||||
<ComponentRef Id="ConfigFolderComponent" />
|
||||
<ComponentRef Id="ServiceComponent" />
|
||||
</Feature>
|
||||
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="Chromebox Fan Control" />
|
||||
</Directory>
|
||||
<Directory Id="CommonAppDataFolder">
|
||||
<Directory Id="ConfigFolder" Name="ChromeboxFanControl" />
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="ServiceComponent" Guid="C3D4E5F6-A7B8-9012-CDEF-F12345678902">
|
||||
<File Id="ServiceExe" Source="ChromeboxFanControlService.exe" KeyPath="yes" />
|
||||
<ServiceInstall Id="ChromeboxFanControlSvc" Name="ChromeboxFanControl"
|
||||
DisplayName="Chromebox Fan Control Service" Description="Fan control for Chromebox with Coreboot"
|
||||
Type="ownProcess" Start="auto" Account="LocalSystem" ErrorControl="normal" />
|
||||
<ServiceControl Id="StartService" Name="ChromeboxFanControl" Start="install" />
|
||||
<ServiceControl Id="StopService" Name="ChromeboxFanControl" Stop="both" Remove="uninstall" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="ConfigFolder">
|
||||
<Component Id="ConfigFolderComponent" Guid="D4E5F6A7-B8C9-0123-DEF0-123456789003">
|
||||
<CreateFolder>
|
||||
<util:PermissionEx User="Users" GenericAll="yes" />
|
||||
</CreateFolder>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
</Product>
|
||||
</Wix>
|
||||
|
||||
132
wix/Product.wxs
132
wix/Product.wxs
@@ -1,66 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
<Product Id="*" Name="Chromebox Fan Control" Language="1033" Version="1.0.0" Manufacturer="ChromeboxFanControl"
|
||||
UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890">
|
||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" Platform="x64" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Feature Id="MainFeature" Title="Chromebox Fan Control" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
||||
<ComponentGroupRef Id="AppFiles" />
|
||||
<ComponentRef Id="ConfigFolderComponent" />
|
||||
<ComponentRef Id="ShortcutComponent" />
|
||||
</Feature>
|
||||
|
||||
<Feature Id="ServiceFeature" Title="Windows Service (runs in background, auto-start)" Level="2">
|
||||
<ComponentRef Id="ServiceComponent" />
|
||||
</Feature>
|
||||
|
||||
<UIRef Id="WixUI_FeatureTree" />
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="Chromebox Fan Control" />
|
||||
</Directory>
|
||||
<Directory Id="CommonAppDataFolder">
|
||||
<Directory Id="ConfigFolder" Name="ChromeboxFanControl" />
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="AppProgramMenuFolder" Name="Chromebox Fan Control" />
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="ServiceComponent" Guid="B2C3D4E5-F6A7-8901-BCDE-F12345678902">
|
||||
<File Id="ServiceExe" Source="ChromeboxFanControlService.exe" KeyPath="yes" />
|
||||
<ServiceInstall Id="ChromeboxFanControlSvc" Name="ChromeboxFanControl"
|
||||
DisplayName="Chromebox Fan Control Service" Description="Fan control for Chromebox with Coreboot"
|
||||
Type="ownProcess" Start="auto" Account="LocalSystem" ErrorControl="normal" />
|
||||
<ServiceControl Id="StartService" Name="ChromeboxFanControl" Start="install" />
|
||||
<ServiceControl Id="StopService" Name="ChromeboxFanControl" Stop="both" Remove="uninstall" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="ConfigFolder">
|
||||
<Component Id="ConfigFolderComponent" Guid="C3D4E5F6-A7B8-9012-CDEF-123456789003">
|
||||
<CreateFolder>
|
||||
<util:PermissionEx User="Users" GenericAll="yes" />
|
||||
</CreateFolder>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="AppProgramMenuFolder">
|
||||
<Component Id="ShortcutComponent" Guid="D4E5F6A7-B8C9-0123-DEF0-234567890104">
|
||||
<Shortcut Id="AppShortcut" Name="Chromebox Fan Control" Description="Fan control for Chromebox"
|
||||
Target="[INSTALLFOLDER]ChromeboxFanControl.exe" WorkingDirectory="INSTALLFOLDER" />
|
||||
<Shortcut Id="UninstallShortcut" Name="Uninstall Chromebox Fan Control"
|
||||
Target="[System64Folder]msiexec.exe" Arguments="/x [ProductCode]" />
|
||||
<RemoveFolder Id="RemoveAppProgramMenuFolder" Directory="AppProgramMenuFolder" On="uninstall" />
|
||||
<RegistryValue Root="HKCU" Key="Software\ChromeboxFanControl" Name="installed" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
</Product>
|
||||
</Wix>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
<Product Id="*" Name="Chromebox Fan Control" Language="1033" Version="1.0.0" Manufacturer="ChromeboxFanControl"
|
||||
UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890">
|
||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" Platform="x64" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Feature Id="MainFeature" Title="Chromebox Fan Control" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
||||
<ComponentGroupRef Id="AppFiles" />
|
||||
<ComponentRef Id="ConfigFolderComponent" />
|
||||
<ComponentRef Id="ShortcutComponent" />
|
||||
</Feature>
|
||||
|
||||
<Feature Id="ServiceFeature" Title="Windows Service (runs in background, auto-start)" Level="2">
|
||||
<ComponentRef Id="ServiceComponent" />
|
||||
</Feature>
|
||||
|
||||
<UIRef Id="WixUI_FeatureTree" />
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="Chromebox Fan Control" />
|
||||
</Directory>
|
||||
<Directory Id="CommonAppDataFolder">
|
||||
<Directory Id="ConfigFolder" Name="ChromeboxFanControl" />
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="AppProgramMenuFolder" Name="Chromebox Fan Control" />
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="ServiceComponent" Guid="B2C3D4E5-F6A7-8901-BCDE-F12345678902">
|
||||
<File Id="ServiceExe" Source="ChromeboxFanControlService.exe" KeyPath="yes" />
|
||||
<ServiceInstall Id="ChromeboxFanControlSvc" Name="ChromeboxFanControl"
|
||||
DisplayName="Chromebox Fan Control Service" Description="Fan control for Chromebox with Coreboot"
|
||||
Type="ownProcess" Start="auto" Account="LocalSystem" ErrorControl="normal" />
|
||||
<ServiceControl Id="StartService" Name="ChromeboxFanControl" Start="install" />
|
||||
<ServiceControl Id="StopService" Name="ChromeboxFanControl" Stop="both" Remove="uninstall" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="ConfigFolder">
|
||||
<Component Id="ConfigFolderComponent" Guid="C3D4E5F6-A7B8-9012-CDEF-123456789003">
|
||||
<CreateFolder>
|
||||
<util:PermissionEx User="Users" GenericAll="yes" />
|
||||
</CreateFolder>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="AppProgramMenuFolder">
|
||||
<Component Id="ShortcutComponent" Guid="D4E5F6A7-B8C9-0123-DEF0-234567890104">
|
||||
<Shortcut Id="AppShortcut" Name="Chromebox Fan Control" Description="Fan control for Chromebox"
|
||||
Target="[INSTALLFOLDER]ChromeboxFanControl.exe" WorkingDirectory="INSTALLFOLDER" />
|
||||
<Shortcut Id="UninstallShortcut" Name="Uninstall Chromebox Fan Control"
|
||||
Target="[System64Folder]msiexec.exe" Arguments="/x [ProductCode]" />
|
||||
<RemoveFolder Id="RemoveAppProgramMenuFolder" Directory="AppProgramMenuFolder" On="uninstall" />
|
||||
<RegistryValue Root="HKCU" Key="Software\ChromeboxFanControl" Name="installed" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
</Product>
|
||||
</Wix>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wi="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<xsl:output omit-xml-declaration="yes" />
|
||||
<xsl:key name="service-comp" match="wi:Component[wi:File[contains(@Source,'ChromeboxFanControlService.exe')]]" use="@Id" />
|
||||
<xsl:template match="@*|node()">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()" />
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
<xsl:template match="wi:Component[wi:File[contains(@Source,'ChromeboxFanControlService.exe')]]" />
|
||||
<xsl:template match="wi:ComponentRef[key('service-comp', @Id)]" />
|
||||
</xsl:stylesheet>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wi="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<xsl:output omit-xml-declaration="yes" />
|
||||
<xsl:key name="service-comp" match="wi:Component[wi:File[contains(@Source,'ChromeboxFanControlService.exe')]]" use="@Id" />
|
||||
<xsl:template match="@*|node()">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()" />
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
<xsl:template match="wi:Component[wi:File[contains(@Source,'ChromeboxFanControlService.exe')]]" />
|
||||
<xsl:template match="wi:ComponentRef[key('service-comp', @Id)]" />
|
||||
</xsl:stylesheet>
|
||||
|
||||
Reference in New Issue
Block a user