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,41 @@
using FanControl.Plugins;
namespace FanControl.ChromeboxEC;
/// <summary>风扇控制ectool fanduty &lt;percent&gt;</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>设置风扇占空比 0100调用 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() { }
}

View File

@@ -0,0 +1,27 @@
using FanControl.Plugins;
namespace FanControl.ChromeboxEC;
/// <summary>风扇转速ectool pwmgetfanrpm [index|all]</summary>
public sealed class ChromeboxECFanSensor : IPluginSensor
{
private readonly string _ectoolPath;
private readonly string[] _rpmArgs;
public ChromeboxECFanSensor(string ectoolPath, string[] rpmArgs)
{
_ectoolPath = ectoolPath;
_rpmArgs = rpmArgs is { Length: > 0 } ? rpmArgs : ["pwmgetfanrpm", "0"];
}
public string Id => "ChromeboxEC_Fan";
public string Name => "Chromebox EC Fan RPM";
public string Origin => "ectool pwmgetfanrpm";
public float? Value { get; private set; }
public void Update()
{
var (ok, stdout, _) = EctoolRunner.Run(_ectoolPath, _rpmArgs);
Value = ok && EctoolRunner.TryParseFanRpm(stdout) is { } rpm ? rpm : null;
}
}

View File

@@ -0,0 +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;
}
}

View File

@@ -0,0 +1,27 @@
using FanControl.Plugins;
namespace FanControl.ChromeboxEC;
/// <summary>温度ectool temps &lt;sensorid&gt;</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;
}
}

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;
}
}

View File

@@ -0,0 +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>

View File

@@ -0,0 +1,6 @@
{
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe",
"TempArgs": ["temps", "0"],
"FanRpmArgs": ["pwmgetfanrpm", "0"],
"AutoFanCtrlArgs": ["autofanctrl"]
}

View File

@@ -0,0 +1,43 @@
using System.Text.Json;
namespace FanControl.ChromeboxEC;
/// <summary>插件配置,对应 ectool 命令参数。参见 docs/ectool-commands-zh.md</summary>
public sealed class PluginConfig
{
/// <summary>ectool 可执行文件路径,默认 Coolstar 安装路径</summary>
public string EctoolPath { get; set; } = @"C:\Program Files\crosec\ectool.exe";
/// <summary>读取温度ectool temps &lt;sensorid&gt;。默认 ["temps", "0"],可用 tempsinfo 查本机传感器 ID</summary>
public string[] TempArgs { get; set; } = ["temps", "0"];
/// <summary>读取转速ectool pwmgetfanrpm [index|all]。默认 ["pwmgetfanrpm", "0"]</summary>
public string[] FanRpmArgs { get; set; } = ["pwmgetfanrpm", "0"];
/// <summary>恢复自动风扇ectool autofanctrl。禁用控制时调用</summary>
public string[] AutoFanCtrlArgs { get; set; } = ["autofanctrl"];
public static PluginConfig Load()
{
var paths = new[]
{
Path.Combine(AppContext.BaseDirectory, "FanControl.ChromeboxEC.json"),
Path.Combine(Path.GetDirectoryName(typeof(PluginConfig).Assembly.Location) ?? "", "FanControl.ChromeboxEC.json"),
Path.Combine(AppContext.BaseDirectory, "Plugins", "FanControl.ChromeboxEC.json"),
};
foreach (var configPath in paths)
{
if (string.IsNullOrEmpty(configPath) || !File.Exists(configPath))
continue;
try
{
var json = File.ReadAllText(configPath);
var cfg = JsonSerializer.Deserialize<PluginConfig>(json);
if (cfg != null)
return cfg;
}
catch { /* try next path */ }
}
return new PluginConfig();
}
}

View File

@@ -0,0 +1,157 @@
# FanControl.ChromeboxEC
Plugin for [Fan Control](https://github.com/Rem0o/FanControl.Releases) that controls Chromebox fan via ectool. Reads EC temperature, sets fan duty. For Chromebox with coreboot.
## Requirements
- [Fan Control](https://github.com/Rem0o/FanControl.Releases) (Rémi Mercier)
- [Coolstar CROS-EC driver](https://github.com/coolstar/crwindows) / crosec with `ectool.exe`
- Run Fan Control **as Administrator**
No separate .NET installation needed — the plugin runs inside Fan Controls process.
## Build
1. Copy `FanControl.Plugins.dll` from your Fan Control installation to `lib/`:
- Typical path: `C:\Program Files (x86)\FanControl\FanControl.Plugins.dll`
2. Build from `FanControl.ChromeboxEC` directory:
```powershell
cd FanControl.ChromeboxEC
dotnet build -c Release
```
## Install
1. Copy `FanControl.ChromeboxEC.dll` to Fan Controls `Plugins` folder (same folder as `FanControl.exe`)
2. Optional: Copy `FanControl.ChromeboxEC.json.example` to `FanControl.ChromeboxEC.json` and edit
3. Start Fan Control as Administrator
## Configuration
Create `FanControl.ChromeboxEC.json` in the Plugins folder to override defaults. See [ectool commands reference](../docs/ectool-commands-zh.md).
| Field | Default | Description |
|-------|---------|-------------|
| `EctoolPath` | `C:\Program Files\crosec\ectool.exe` | Path to ectool |
| `TempArgs` | `["temps", "0"]` | Read temp: `ectool temps <sensorid>`. Use `ectool tempsinfo` for your board |
| `FanRpmArgs` | `["pwmgetfanrpm", "0"]` | Read RPM: `ectool pwmgetfanrpm [index\|all]` |
| `AutoFanCtrlArgs` | `["autofanctrl"]` | Restore auto fan when control disabled |
## Plugin sensors
| Sensor | ectool command | Description |
|--------|----------------|-------------|
| Chromebox EC Temperature | `temps <sensorid>` | EC temp for fan curves |
| Chromebox EC Fan RPM | `pwmgetfanrpm` | Fan speed |
| Chromebox EC Fan | `fanduty` / `autofanctrl` | Fan control, restores auto on disable |
## Recommended config
**CPU core temp + board temp (EC Temp) dual curves, take max to set fan.**
1. Create curve **CPU Package**: temp source = CPU Package, output 0100% duty
2. Create curve **Board Temp**: temp source = Chromebox EC Temperature, output 0100% duty
3. Create **Mix** curve: function = Max, inputs = both curves above
4. Set **Chromebox EC Fan** control to use **Mix** curve
The fan will respond to whichever sensor is hotter.
## Troubleshooting: fan sensor not found
If "Chromebox EC Fan" control cannot pair with a speed sensor:
1. **Run Fan Control as Administrator** (ectool requires it)
2. **Test ectool manually**: open CMD as admin, run
```cmd
"C:\Program Files\crosec\ectool.exe" pwmgetfanrpm 0
```
If it fails, try `pwmgetfanrpm` (no args) or `pwmgetfanrpm all`
3. **Create config file**: in Fan Controls `Plugins` folder, create `FanControl.ChromeboxEC.json` and adjust `FanRpmArgs`, e.g.:
```json
{
"FanRpmArgs": ["pwmgetfanrpm"],
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe"
}
```
or `["pwmgetfanrpm", "all"]`
4. **Check ectool path**: if crosec is elsewhere, set correct `EctoolPath` in config
---
# 中文 / Chinese
[Fan Control](https://github.com/Rem0o/FanControl.Releases) 插件,通过 ectool 读取 Chromebox EC 温度并控制风扇。适用于刷了 coreboot 的 Chromebox。
## 依赖
- [Fan Control](https://github.com/Rem0o/FanControl.Releases)Rémi Mercier
- [Coolstar CROS-EC 驱动](https://github.com/coolstar/crwindows) / crosec内含 `ectool.exe`
- **以管理员身份运行** Fan Control
无需单独安装 .NET插件在 Fan Control 进程内运行。
## 编译
1. 将 Fan Control 安装目录下的 `FanControl.Plugins.dll` 复制到 `lib/`
- 常见路径:`C:\Program Files (x86)\FanControl\FanControl.Plugins.dll`
2. 在 `FanControl.ChromeboxEC` 目录下编译:
```powershell
cd FanControl.ChromeboxEC
dotnet build -c Release
```
## 安装
1. 将 `FanControl.ChromeboxEC.dll` 复制到 Fan Control 的 `Plugins` 文件夹(与 `FanControl.exe` 同目录)
2. 可选:将 `FanControl.ChromeboxEC.json.example` 复制为 `FanControl.ChromeboxEC.json` 并修改配置
3. 以管理员身份启动 Fan Control
## 配置
在 Plugins 文件夹中创建 `FanControl.ChromeboxEC.json` 可覆盖默认值。ectool 命令说明见 [ectool 命令参考](../docs/ectool-commands-zh.md)。
| 字段 | 默认 | 说明 |
|------|------|------|
| `EctoolPath` | `C:\Program Files\crosec\ectool.exe` | ectool 路径 |
| `TempArgs` | `["temps", "0"]` | 读取温度:`ectool temps <sensorid>`。用 `ectool tempsinfo` 查看本机传感器 ID |
| `FanRpmArgs` | `["pwmgetfanrpm", "0"]` | 读取转速:`ectool pwmgetfanrpm [index\|all]` |
| `AutoFanCtrlArgs` | `["autofanctrl"]` | 禁用控制时恢复自动风扇 |
## 插件功能
| 传感器 | ectool 命令 | 说明 |
|--------|-------------|------|
| Chromebox EC Temperature | `temps <sensorid>` | EC 温度,可用于风扇曲线 |
| Chromebox EC Fan RPM | `pwmgetfanrpm` | 风扇转速 |
| Chromebox EC Fan | `fanduty` / `autofanctrl` | 风扇控制,禁用时恢复 EC 自动控速 |
## 推荐配置
**CPU 核心温度 + 板载温度EC Temp双曲线取最大值设置风扇。**
1. 新建曲线 **CPU Package**温度源选「CPU Package」按温度输出 0100% 占空比
2. 新建曲线 **Board Temp**温度源选「Chromebox EC Temperature」按温度输出 0100% 占空比
3. 新建 **Mix** 曲线:函数选「最大」,输入上述两条曲线
4. 将 **Chromebox EC Fan** 控制的曲线设为 **Mix**
这样风扇会以 CPU 和板载温度中较高者为依据控速。
## 故障排除:找不到风扇传感器
若「Chromebox EC Fan」控制无法配对到转速传感器配对时找不到风扇
1. **确认以管理员身份运行 Fan Control**ectool 需要)
2. **手动测试 ectool**:以管理员打开 CMD执行
```cmd
"C:\Program Files\crosec\ectool.exe" pwmgetfanrpm 0
```
若报错或输出异常,尝试 `pwmgetfanrpm`(无参数)或 `pwmgetfanrpm all`
3. **创建配置文件**:在 Fan Control 的 `Plugins` 目录新建 `FanControl.ChromeboxEC.json`,按本机 ectool 输出调整 `FanRpmArgs`,例如:
```json
{
"FanRpmArgs": ["pwmgetfanrpm"],
"EctoolPath": "C:\\Program Files\\crosec\\ectool.exe"
}
```
或 `["pwmgetfanrpm", "all"]`
4. **确认 ectool 路径**:若 crosec 安装在其他位置,在配置中设置正确的 `EctoolPath`

View File