Add FanControl.ChromeboxEC plugin, project background, ectool docs

- Add FanControl.ChromeboxEC plugin for Fan Control
- Add project background/缘由 in README (EN/zh)
- Add docs/ectool-commands-zh.md
- Update sln, appsettings, gitignore

Made-with: Cursor
This commit is contained in:
2026-03-22 15:15:02 +08:00
parent e8c8a759e0
commit 111eb22824
15 changed files with 928 additions and 3 deletions

View File

@@ -0,0 +1,107 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace FanControl.ChromeboxEC;
/// <summary>ectool 命令封装,参见 docs/ectool-commands-zh.md</summary>
internal static class EctoolRunner
{
private static readonly Regex NumberRegex = new(@"\d+", RegexOptions.Compiled);
/// <summary>执行 ectool 命令,返回 (成功, stdout, stderr)</summary>
public static (bool ok, string stdout, string stderr) Run(
string exePath,
IReadOnlyList<string> args,
int timeoutMs = 15_000)
{
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
return (false, "", "ectool 未找到");
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 stdout = proc.StandardOutput.ReadToEnd();
var stderr = proc.StandardError.ReadToEnd();
if (!proc.WaitForExit(timeoutMs))
{
try { proc.Kill(entireProcessTree: true); } catch { /* ignore */ }
return (false, "", "ectool 超时");
}
return (proc.ExitCode == 0, stdout, stderr);
}
/// <summary>解析 pwmgetfanrpm 输出中的转速 (50050000 RPM)</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;
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 >= 400 and <= 50_000)
return n;
}
return null;
}
/// <summary>解析 temps 输出为摄氏温度。支持 "315 K (= 42 C)" 等格式,多传感器时取最高值。</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))
{
if (int.TryParse(m.Groups[1].Value, out var k) && k is >= 250 and <= 400)
{
var c = k - 273.15f;
if (best == null || c > best)
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;
}
}