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

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ dist-installer/
wix/obj/ wix/obj/
wix/Harvested.wxs wix/Harvested.wxs
wix/Harvested-Service.wxs wix/Harvested-Service.wxs
FanControl.ChromeboxEC/lib/FanControl.Plugins.dll

View File

@@ -5,6 +5,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControl", "Chro
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControlService", "ChromeboxFanControlService\ChromeboxFanControlService.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControlService", "ChromeboxFanControlService\ChromeboxFanControlService.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanControl.ChromeboxEC", "FanControl.ChromeboxEC\FanControl.ChromeboxEC.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -19,5 +21,9 @@ Global
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@@ -6,6 +6,8 @@
"FanDutyArgs": null, "FanDutyArgs": null,
"AutoFanCtrlArgs": [ "autofanctrl" ], "AutoFanCtrlArgs": [ "autofanctrl" ],
"TempSource": "AverageCore", "TempSource": "AverageCore",
"RampUpSteps": 3,
"RampUpMinDeltaPercent": 20,
"FailSafeAfterConsecutiveErrors": 5, "FailSafeAfterConsecutiveErrors": 5,
"FailSafeFanPercent": 100, "FailSafeFanPercent": 100,
"FailSafeRestoreAutoFan": false, "FailSafeRestoreAutoFan": false,

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

122
README.md
View File

@@ -1,7 +1,121 @@
# Chromebox 风扇温控(Windows # 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 Controls 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 0100% duty.
- Create curve **Board Temp**: temp source = Chromebox EC Temperature, output 0100% 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 programs **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 (110, default 3). |
| `RampUpMinDeltaPercent` | Change below this % is applied in one step (050, 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 0100 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**。主界面提供三条实时曲线:温度、目标占空比、转速。 在刷了 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」输出 0100% 占空比。
- 新建曲线 **Board Temp**温度源选「Chromebox EC Temperature」输出 0100% 占空比。
- 新建 **Mix** 曲线:函数选「最大」,输入上述两条曲线。
-**Chromebox EC Fan** 控制的曲线设为 **Mix**
详见 [FanControl.ChromeboxEC 说明](FanControl.ChromeboxEC/README.md)。
## 依赖 ## 依赖
1. 安装 [Coolstar CROS-EC 驱动](https://github.com/coolstar/crwindows)(或您当前使用的 crosec 安装包),确保存在默认可执行文件: 1. 安装 [Coolstar CROS-EC 驱动](https://github.com/coolstar/crwindows)(或您当前使用的 crosec 安装包),确保存在默认可执行文件:
@@ -31,7 +145,7 @@
**构建机**需 [.NET 8 SDK](https://dotnet.microsoft.com/download) 与 [WiX Toolset 3](https://wixtoolset.org/docs/wix3/)。**构建前请关闭正在运行的 ChromeboxFanControl.exe** **构建机**需 [.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)。服务模式下配置从 `%ProgramData%\ChromeboxFanControl\config.json` 读取。 以**管理员身份**运行 MSI。若尚未安装 ectool可访问 [Chrultrabook 的 ectool 安装说明](https://docs.chrultrabook.com/docs/installing/ectool.html)。另见 [ectool 命令参考](docs/ectool-commands-zh.md)。服务模式下配置从 `%ProgramData%\ChromeboxFanControl\config.json` 读取。
### 仅发布到 dist\(用于调试或手动打包) ### 仅发布到 dist\(用于调试或手动打包)
@@ -45,7 +159,7 @@
## 配置 ## 配置
- 安装目录旁的 **`appsettings.json`**:默认选项。 - 安装目录旁的 **`appsettings.json`**:默认选项。
- **`%AppData%\ChromeboxFanControl\config.json`**GUI 模式下保存后覆盖(用户配置)。 - **`%AppData%\ChromeboxFanControl\config.json`**GUI 模式下保存后覆盖(用户配置)。
- **`%ProgramData%\ChromeboxFanControl\config.json`**Windows 服务模式下读取的配置;可与 GUI 共用同一份配置逻辑。 - **`%ProgramData%\ChromeboxFanControl\config.json`**Windows 服务模式下读取的配置;可与 GUI 共用同一份配置逻辑。
@@ -54,6 +168,8 @@
| 项 | 说明 | | 项 | 说明 |
|----|------| |----|------|
| `Language` | 界面语言:`auto`(跟随系统)、`en``zh-Hans``zh-Hant`。修改后需重启生效。 | | `Language` | 界面语言:`auto`(跟随系统)、`en``zh-Hans``zh-Hant`。修改后需重启生效。 |
| `RampUpSteps` | 升/降速分步数110默认 3。 |
| `RampUpMinDeltaPercent` | 变化幅度低于此百分比则一步到位050默认 20。 |
| `FanRpmArgs` | 读取转速时传给 ectool 的参数(默认 `pwmgetfanrpm` `0`),请用本机 `ectool help` 核对。 | | `FanRpmArgs` | 读取转速时传给 ectool 的参数(默认 `pwmgetfanrpm` `0`),请用本机 `ectool help` 核对。 |
| `FanDutyArgs` | 读取占空比时传给 ectool 的参数(如 `pwmget` `0`);空则跳过,显示目标占空比。 | | `FanDutyArgs` | 读取占空比时传给 ectool 的参数(如 `pwmget` `0`);空则跳过,显示目标占空比。 |
| `AutoFanCtrlArgs` | 退出程序时恢复 EC 自动风扇(默认 `autofanctrl`),若命令名不同请修改。 | | `AutoFanCtrlArgs` | 退出程序时恢复 EC 自动风扇(默认 `autofanctrl`),若命令名不同请修改。 |

333
docs/ectool-commands-zh.md Normal file
View File

@@ -0,0 +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 设为固定占空比0100
```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 位065535
```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)