diff --git a/.gitignore b/.gitignore
index c701bc1..44b901e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,9 @@ obj/
*.user
*.suo
dist/
+dist-svc/
+dist-installer/
*.log
+wix/obj/
+wix/Harvested.wxs
+wix/Harvested-Service.wxs
diff --git a/ChromeboxFanControl.sln b/ChromeboxFanControl.sln
index 3666aad..92d589f 100644
--- a/ChromeboxFanControl.sln
+++ b/ChromeboxFanControl.sln
@@ -3,6 +3,8 @@ 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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -13,5 +15,9 @@ Global
{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
EndGlobalSection
EndGlobal
diff --git a/ChromeboxFanControl/AppConfig.cs b/ChromeboxFanControl/AppConfig.cs
index abe51a1..25221dc 100644
--- a/ChromeboxFanControl/AppConfig.cs
+++ b/ChromeboxFanControl/AppConfig.cs
@@ -5,6 +5,8 @@ namespace ChromeboxFanControl;
public sealed class AppConfig
{
+ /// UI language: "auto" (system), "en", "zh-Hans", "zh-Hant". Restart to apply.
+ public string Language { get; set; } = "auto";
public string EctoolPath { get; set; } = @"C:\Program Files\crosec\ectool.exe";
public int PollIntervalMs { get; set; } = 1500;
/// Call ectool for RPM every N control cycles (1 = every cycle).
@@ -33,7 +35,9 @@ public sealed class AppConfig
public static string UserConfigPath =>
Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ Environment.UserInteractive
+ ? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
+ : Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"ChromeboxFanControl",
"config.json");
@@ -66,6 +70,8 @@ public sealed class AppConfig
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);
diff --git a/ChromeboxFanControl/ChromeboxFanControl.csproj b/ChromeboxFanControl/ChromeboxFanControl.csproj
index 8df6c90..b3ae4d5 100644
--- a/ChromeboxFanControl/ChromeboxFanControl.csproj
+++ b/ChromeboxFanControl/ChromeboxFanControl.csproj
@@ -1,6 +1,7 @@
+ $(NoWarn);NU1701
WinExe
net8.0-windows
enable
@@ -24,4 +25,8 @@
+
+ en;zh-Hans;zh-Hant
+
+
diff --git a/ChromeboxFanControl/MainForm.cs b/ChromeboxFanControl/MainForm.cs
index 168b935..a70b4b0 100644
--- a/ChromeboxFanControl/MainForm.cs
+++ b/ChromeboxFanControl/MainForm.cs
@@ -1,3 +1,4 @@
+using ChromeboxFanControl.Properties;
using ScottPlot;
using ScottPlot.WinForms;
@@ -18,6 +19,7 @@ public sealed class MainForm : Form
private readonly TextBox _txtFanDutyArgs = new();
private readonly TextBox _txtAutoFanArgs = new();
private readonly ComboBox _cmbTempSource = new();
+ private readonly ComboBox _cmbLanguage = new();
private readonly NumericUpDown _nudFailCount = new();
private readonly NumericUpDown _nudFailPct = new();
private readonly CheckBox _chkFailAuto = new();
@@ -25,7 +27,7 @@ public sealed class MainForm : Form
private readonly NumericUpDown _nudChartPoints = new();
private readonly NotifyIcon _tray = new();
private readonly ContextMenuStrip _trayMenu = new();
- private readonly ToolStripMenuItem _miPause = new("暂停控制");
+ private readonly ToolStripMenuItem _miPause;
private readonly object _seriesLock = new();
private readonly List _tSec = new();
private readonly List _temps = new();
@@ -37,7 +39,8 @@ public sealed class MainForm : Form
public MainForm()
{
- Text = "Chromebox 风扇温控";
+ _miPause = new ToolStripMenuItem(Resources.TrayPause);
+ Text = Resources.AppTitle;
Size = new Size(960, 700);
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(800, 500);
@@ -62,9 +65,9 @@ public sealed class MainForm : Form
{
var tabs = new TabControl { Dock = DockStyle.Fill };
- var tabMonitor = new TabPage("监控");
- var tabCurve = new TabPage("曲线");
- var tabAdv = new TabPage("高级");
+ var tabMonitor = new TabPage(Resources.TabMonitor);
+ var tabCurve = new TabPage(Resources.TabCurve);
+ var tabAdv = new TabPage(Resources.TabAdvanced);
tabs.TabPages.Add(tabMonitor);
tabs.TabPages.Add(tabCurve);
@@ -75,7 +78,7 @@ public sealed class MainForm : Form
_lblStatus.Dock = DockStyle.Bottom;
_lblStatus.Padding = new Padding(8, 4, 8, 4);
_lblStatus.AutoEllipsis = true;
- _lblStatus.Text = "启动中…";
+ _lblStatus.Text = Resources.StatusStarting;
var panelChart = new Panel { Dock = DockStyle.Fill };
panelChart.Controls.Add(_chart);
@@ -91,14 +94,14 @@ public sealed class MainForm : Form
_gridCurve.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colTemp",
- HeaderText = "温度 (°C)",
+ HeaderText = Resources.GridColTemp,
ReadOnly = true,
Width = 80
});
_gridCurve.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colDuty",
- HeaderText = "占空比 0–100",
+ HeaderText = Resources.GridColDuty,
Width = 100
});
for (var i = 0; i < 14; i++)
@@ -112,9 +115,9 @@ public sealed class MainForm : Form
FlowDirection = FlowDirection.LeftToRight,
Padding = new Padding(8)
};
- var btnSave = new Button { Text = "保存并应用", AutoSize = true };
+ var btnSave = new Button { Text = Resources.BtnSaveApply, AutoSize = true };
btnSave.Click += (_, _) => SaveFromUi();
- var btnDefault = new Button { Text = "恢复默认曲线", AutoSize = true };
+ var btnDefault = new Button { Text = Resources.BtnRestoreDefault, AutoSize = true };
btnDefault.Click += (_, _) =>
{
var def = new AppConfig();
@@ -138,41 +141,45 @@ public sealed class MainForm : Form
{
Dock = DockStyle.Fill,
ColumnCount = 2,
- RowCount = 12,
+ RowCount = 13,
Padding = new Padding(12)
};
flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 35));
flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 65));
var row = 0;
- AddRow(flp, row++, "ectool 路径", _txtEctool);
- AddRow(flp, row++, "轮询间隔 (ms)", _nudPoll);
+ AddRow(flp, row++, Resources.LblLanguage, _cmbLanguage);
+ _cmbLanguage.DropDownStyle = ComboBoxStyle.DropDownList;
+ _cmbLanguage.Items.AddRange([Resources.LangAuto, Resources.LangEn, Resources.LangZhHans, Resources.LangZhHant]);
+
+ AddRow(flp, row++, Resources.LblEctoolPath, _txtEctool);
+ AddRow(flp, row++, Resources.LblPollInterval, _nudPoll);
_nudPoll.Minimum = 250;
_nudPoll.Maximum = 60_000;
_nudPoll.Increment = 250;
- AddRow(flp, row++, "每 N 轮读 RPM", _nudRpmEvery);
+ AddRow(flp, row++, Resources.LblRpmEvery, _nudRpmEvery);
_nudRpmEvery.Minimum = 1;
_nudRpmEvery.Maximum = 100;
- AddRow(flp, row++, "读转速参数 (空格分隔)", _txtFanRpmArgs);
- AddRow(flp, row++, "读占空比参数 (空=跳过)", _txtFanDutyArgs);
+ AddRow(flp, row++, Resources.LblFanRpmArgs, _txtFanRpmArgs);
+ AddRow(flp, row++, Resources.LblFanDutyArgs, _txtFanDutyArgs);
- AddRow(flp, row++, "恢复自动风扇参数", _txtAutoFanArgs);
+ AddRow(flp, row++, Resources.LblAutoFanArgs, _txtAutoFanArgs);
- AddRow(flp, row++, "温度来源", _cmbTempSource);
+ AddRow(flp, row++, Resources.LblTempSource, _cmbTempSource);
_cmbTempSource.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbTempSource.Items.AddRange(["AverageCore", "MaxCore"]);
- AddRow(flp, row++, "连续失败进入安全模式 (次)", _nudFailCount);
+ AddRow(flp, row++, Resources.LblFailCount, _nudFailCount);
_nudFailCount.Minimum = 1;
_nudFailCount.Maximum = 100;
- AddRow(flp, row++, "安全模式固定占空比 (%)", _nudFailPct);
+ AddRow(flp, row++, Resources.LblFailPct, _nudFailPct);
_nudFailPct.Minimum = 0;
_nudFailPct.Maximum = 100;
- _chkFailAuto.Text = "安全模式使用 autofanctrl(不用固定占空比)";
+ _chkFailAuto.Text = Resources.ChkFailAuto;
_chkFailAuto.AutoSize = true;
var failPanel = new FlowLayoutPanel
{
@@ -182,19 +189,19 @@ public sealed class MainForm : Form
WrapContents = false
};
failPanel.Controls.Add(_chkFailAuto);
- flp.Controls.Add(new System.Windows.Forms.Label { Text = "安全模式策略", AutoSize = true, Anchor = AnchorStyles.Left }, 0, row);
+ flp.Controls.Add(new System.Windows.Forms.Label { Text = Resources.LblFailStrategy, AutoSize = true, Anchor = AnchorStyles.Left }, 0, row);
flp.Controls.Add(failPanel, 1, row++);
- AddRow(flp, row++, "图表保留时长 (分钟)", _nudChartMin);
+ AddRow(flp, row++, Resources.LblChartMin, _nudChartMin);
_nudChartMin.Minimum = 1;
_nudChartMin.Maximum = 120;
- AddRow(flp, row++, "图表最大点数", _nudChartPoints);
+ AddRow(flp, row++, Resources.LblChartPoints, _nudChartPoints);
_nudChartPoints.Minimum = 100;
_nudChartPoints.Maximum = 10_000;
_nudChartPoints.Increment = 100;
- var btnSaveAdv = new Button { Text = "保存高级设置", AutoSize = true, Dock = DockStyle.Bottom };
+ var btnSaveAdv = new Button { Text = Resources.BtnSaveAdvanced, AutoSize = true, Dock = DockStyle.Bottom };
btnSaveAdv.Click += (_, _) => SaveFromUi();
tabAdv.Controls.Add(btnSaveAdv);
tabAdv.Controls.Add(flp);
@@ -211,7 +218,7 @@ public sealed class MainForm : Form
private void WireTray()
{
- _tray.Text = "Chromebox 风扇温控";
+ _tray.Text = Resources.AppTitle;
_tray.Visible = true;
_tray.Icon = SystemIcons.Application;
_tray.DoubleClick += (_, _) => ShowFromTray();
@@ -219,13 +226,11 @@ public sealed class MainForm : Form
{
TogglePause();
};
- _trayMenu.Items.Add("打开主窗口", null, (_, _) => ShowFromTray());
+ _trayMenu.Items.Add(Resources.TrayOpen, null, (_, _) => ShowFromTray());
_trayMenu.Items.Add(_miPause);
- _trayMenu.Items.Add("关于", null, (_, _) =>
- MessageBox.Show(this,
- "Chromebox 风扇温控\n\n使用 LibreHardwareMonitor 读取 CPU 温度,通过 crosec ectool 控制风扇。\n\n请以管理员身份运行。",
- Text, MessageBoxButtons.OK, MessageBoxIcon.Information));
- _trayMenu.Items.Add("退出", null, (_, _) => Close());
+ _trayMenu.Items.Add(Resources.TrayAbout, null, (_, _) =>
+ MessageBox.Show(this, Resources.AboutMessage, Text, MessageBoxButtons.OK, MessageBoxIcon.Information));
+ _trayMenu.Items.Add(Resources.TrayExit, null, (_, _) => Close());
_tray.ContextMenuStrip = _trayMenu;
}
@@ -242,11 +247,16 @@ public sealed class MainForm : Form
if (_fan == null)
return;
_fan.Paused = !_fan.Paused;
- _miPause.Text = _fan.Paused ? "恢复控制" : "暂停控制";
+ _miPause.Text = _fan.Paused ? Resources.TrayResume : Resources.TrayPause;
}
+ private static readonly string[] LanguageValues = ["auto", "en", "zh-Hans", "zh-Hant"];
+
private void ApplyConfigToUi()
{
+ var langIdx = Array.IndexOf(LanguageValues, _config.Language);
+ _cmbLanguage.SelectedIndex = langIdx >= 0 ? langIdx : 0;
+
_txtEctool.Text = _config.EctoolPath;
_nudPoll.Value = _config.PollIntervalMs;
_nudRpmEvery.Value = _config.FanRpmPollEveryNCycles;
@@ -268,8 +278,12 @@ public sealed class MainForm : Form
private AppConfig CloneConfigFromUi()
{
+ var langIdx = _cmbLanguage.SelectedIndex;
+ var lang = langIdx >= 0 && langIdx < LanguageValues.Length ? LanguageValues[langIdx] : "auto";
+
var c = new AppConfig
{
+ Language = lang,
EctoolPath = _txtEctool.Text.Trim(),
PollIntervalMs = (int)_nudPoll.Value,
FanRpmPollEveryNCycles = (int)_nudRpmEvery.Value,
@@ -313,8 +327,8 @@ public sealed class MainForm : Form
var plt = _curveChart.Plot;
plt.Clear();
- plt.Axes.Left.Label.Text = "占空比 (%)";
- plt.Axes.Bottom.Label.Text = "温度 (°C)";
+ plt.Axes.Left.Label.Text = Resources.CurveChartDuty;
+ plt.Axes.Bottom.Label.Text = Resources.CurveChartTemp;
plt.Axes.SetLimitsX(0, 100);
plt.Axes.SetLimitsY(0, 100);
@@ -326,6 +340,7 @@ public sealed class MainForm : Form
scatter.MarkerSize = 6;
}
+ plt.Font.Automatic();
_curveChart.Refresh();
}
@@ -336,21 +351,25 @@ public sealed class MainForm : Form
var rpmArgs = SplitArgs(_txtFanRpmArgs.Text);
if (rpmArgs.Length == 0)
{
- MessageBox.Show(this, "读转速参数不能为空。", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ MessageBox.Show(this, Resources.ErrRpmArgsEmpty, Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var autoArgs = SplitArgs(_txtAutoFanArgs.Text);
if (autoArgs.Length == 0)
{
- MessageBox.Show(this, "恢复自动风扇参数不能为空。", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ MessageBox.Show(this, Resources.ErrAutoFanArgsEmpty, Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
+ var langBefore = _config.Language;
_config = CloneConfigFromUi();
_config.SaveUser();
_fan?.UpdateConfig(_config);
- MessageBox.Show(this, "已保存。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
+ var msg = Resources.MsgSaved;
+ if (langBefore != _config.Language)
+ msg = Resources.MsgRestartToApplyLanguage;
+ MessageBox.Show(this, msg, Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
@@ -388,11 +407,11 @@ public sealed class MainForm : Form
var dVal = e.ActualDutyPercent ?? e.TargetDutyPercent;
var dStr = dVal.HasValue ? $"{dVal} %" : "—";
var rStr = e.Rpm.HasValue ? $"{e.Rpm} RPM" : "—";
- var state = e.Paused ? "已暂停" : e.FailSafe ? "安全模式" : "运行中";
+ var state = e.Paused ? Resources.StatusPaused : e.FailSafe ? Resources.StatusFailSafe : Resources.StatusRunning;
_lblStatus.Text =
- $"{state} 温度: {tStr} 目标占空比: {dStr} 转速: {rStr} ectool: {e.LastEctoolMessage ?? "—"}";
+ $"{state} {Resources.LabelTemp} {tStr} {Resources.LabelTargetDuty} {dStr} {Resources.LabelRpm} {rStr} ectool: {e.LastEctoolMessage ?? "—"}";
- _tray.Text = $"Chromebox {tStr} {dStr}";
+ _tray.Text = $"{Resources.AppTitle} {tStr} {dStr}";
RebuildChart();
}
@@ -435,14 +454,14 @@ public sealed class MainForm : Form
var plt = _chart.Plot;
plt.Clear();
- plt.Axes.Left.Label.Text = "Temperature (°C)";
- plt.Axes.Bottom.Label.Text = "Time (s)";
+ plt.Axes.Left.Label.Text = Resources.ChartTemp;
+ plt.Axes.Bottom.Label.Text = Resources.ChartTime;
plt.Axes.Right.IsVisible = true;
- plt.Axes.Right.Label.Text = "Duty (%)";
+ plt.Axes.Right.Label.Text = Resources.ChartDuty;
if (_yRpmAxis == null)
_yRpmAxis = plt.Axes.AddRightAxis();
- _yRpmAxis.Label.Text = "RPM";
+ _yRpmAxis.Label.Text = Resources.ChartRpm;
if (x.Length > 0)
{
@@ -509,12 +528,13 @@ public sealed class MainForm : Form
LegendItem[] leg =
[
- new() { LineColor = Colors.Blue, LineWidth = 2, LabelText = "°C" },
- new() { LineColor = Colors.Orange, LineWidth = 2, LabelText = "Duty %" },
- new() { LineColor = Colors.Green, LineWidth = 2, LabelText = "RPM" }
+ new() { LineColor = Colors.Blue, LineWidth = 2, LabelText = Resources.LegendCelsius },
+ new() { LineColor = Colors.Orange, LineWidth = 2, LabelText = Resources.LegendDuty },
+ new() { LineColor = Colors.Green, LineWidth = 2, LabelText = Resources.ChartRpm }
];
plt.ShowLegend(leg, Alignment.UpperRight);
plt.Legend.Orientation = ScottPlot.Orientation.Horizontal;
+ plt.Font.Automatic();
_chart.Refresh();
}
diff --git a/ChromeboxFanControl/Program.cs b/ChromeboxFanControl/Program.cs
index 81aa215..296d9c8 100644
--- a/ChromeboxFanControl/Program.cs
+++ b/ChromeboxFanControl/Program.cs
@@ -6,6 +6,22 @@ internal static class Program
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());
}
}
diff --git a/ChromeboxFanControl/Properties/Resources.Designer.cs b/ChromeboxFanControl/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..f3901a7
--- /dev/null
+++ b/ChromeboxFanControl/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+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 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.";
+}
diff --git a/ChromeboxFanControl/Properties/Resources.resx b/ChromeboxFanControl/Properties/Resources.resx
new file mode 100644
index 0000000..45d11df
--- /dev/null
+++ b/ChromeboxFanControl/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Chromebox Fan Control
+ Monitoring
+ Curve
+ Advanced
+ Starting…
+ Running
+ Paused
+ Fail-safe mode
+ Temp (°C)
+ Duty 0–100
+ Save & Apply
+ Restore Default Curve
+ ectool path
+ Poll interval (ms)
+ Read RPM every N cycles
+ RPM args (space-separated)
+ Duty args (empty=skip)
+ Restore auto fan args
+ Temp source
+ Consecutive errors before fail-safe
+ Fail-safe fixed duty (%)
+ Use autofanctrl in fail-safe (not fixed duty)
+ Fail-safe strategy
+ Chart history (minutes)
+ Chart max points
+ Save Advanced Settings
+ Show window
+ Pause control
+ Resume control
+ About
+ Exit
+ Chromebox Fan Control
+
+Uses LibreHardwareMonitor to read CPU temperature, controls fan via crosec ectool.
+
+Run as Administrator.
+ RPM args cannot be empty.
+ Restore auto fan args cannot be empty.
+ Saved.
+ Temp:
+ Target duty:
+ RPM:
+ Temperature (°C)
+ Time (s)
+ Duty (%)
+ RPM
+ °C
+ Duty %
+ Duty (%)
+ Temp (°C)
+ Language
+ Auto (follow system)
+ English
+ Simplified Chinese
+ Traditional Chinese
+ Language changed. Restart the application to apply.
+
diff --git a/ChromeboxFanControl/Properties/Resources.zh-Hans.resx b/ChromeboxFanControl/Properties/Resources.zh-Hans.resx
new file mode 100644
index 0000000..e4e7edf
--- /dev/null
+++ b/ChromeboxFanControl/Properties/Resources.zh-Hans.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Chromebox 风扇温控
+ 监控
+ 曲线
+ 高级
+ 启动中…
+ 运行中
+ 已暂停
+ 安全模式
+ 温度 (°C)
+ 占空比 0–100
+ 保存并应用
+ 恢复默认曲线
+ ectool 路径
+ 轮询间隔 (ms)
+ 每 N 轮读 RPM
+ 读转速参数 (空格分隔)
+ 读占空比参数 (空=跳过)
+ 恢复自动风扇参数
+ 温度来源
+ 连续失败进入安全模式 (次)
+ 安全模式固定占空比 (%)
+ 安全模式使用 autofanctrl(不用固定占空比)
+ 安全模式策略
+ 图表保留时长 (分钟)
+ 图表最大点数
+ 保存高级设置
+ 打开主窗口
+ 暂停控制
+ 恢复控制
+ 关于
+ 退出
+ Chromebox 风扇温控
+
+使用 LibreHardwareMonitor 读取 CPU 温度,通过 crosec ectool 控制风扇。
+
+请以管理员身份运行。
+ 读转速参数不能为空。
+ 恢复自动风扇参数不能为空。
+ 已保存。
+ 温度:
+ 目标占空比:
+ 转速:
+ 温度 (°C)
+ 时间 (s)
+ 占空比 (%)
+ 转速
+ °C
+ 占空比 %
+ 占空比 (%)
+ 温度 (°C)
+ 语言
+ 跟随系统
+ English
+ 简体中文
+ 繁体中文
+ 语言已更改,请重启应用以生效。
+
diff --git a/ChromeboxFanControl/Properties/Resources.zh-Hant.resx b/ChromeboxFanControl/Properties/Resources.zh-Hant.resx
new file mode 100644
index 0000000..d62e940
--- /dev/null
+++ b/ChromeboxFanControl/Properties/Resources.zh-Hant.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Chromebox 風扇溫控
+ 監控
+ 曲線
+ 進階
+ 啟動中…
+ 運行中
+ 已暫停
+ 安全模式
+ 溫度 (°C)
+ 占空比 0–100
+ 儲存並套用
+ 恢復預設曲線
+ ectool 路徑
+ 輪詢間隔 (ms)
+ 每 N 輪讀 RPM
+ 讀轉速參數 (空格分隔)
+ 讀占空比參數 (空=略過)
+ 恢復自動風扇參數
+ 溫度來源
+ 連續失敗進入安全模式 (次)
+ 安全模式固定占空比 (%)
+ 安全模式使用 autofanctrl(不用固定占空比)
+ 安全模式策略
+ 圖表保留時長 (分鐘)
+ 圖表最大點數
+ 儲存進階設定
+ 開啟主視窗
+ 暫停控制
+ 恢復控制
+ 關於
+ 結束
+ Chromebox 風扇溫控
+
+使用 LibreHardwareMonitor 讀取 CPU 溫度,透過 crosec ectool 控制風扇。
+
+請以系統管理員身分執行。
+ 讀轉速參數不能為空。
+ 恢復自動風扇參數不能為空。
+ 已儲存。
+ 溫度:
+ 目標占空比:
+ 轉速:
+ 溫度 (°C)
+ 時間 (s)
+ 占空比 (%)
+ 轉速
+ °C
+ 占空比 %
+ 占空比 (%)
+ 溫度 (°C)
+ 語言
+ 跟隨系統
+ English
+ 簡體中文
+ 繁體中文
+ 語言已變更,請重新啟動應用程式以生效。
+
diff --git a/ChromeboxFanControlService/ChromeboxFanControlService.csproj b/ChromeboxFanControlService/ChromeboxFanControlService.csproj
new file mode 100644
index 0000000..d56dcac
--- /dev/null
+++ b/ChromeboxFanControlService/ChromeboxFanControlService.csproj
@@ -0,0 +1,27 @@
+
+
+
+ $(NoWarn);NU1701
+ net8.0-windows
+ enable
+ enable
+ ChromeboxFanControlService
+ ChromeboxFanControlService
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ChromeboxFanControlService/FanControlWorker.cs b/ChromeboxFanControlService/FanControlWorker.cs
new file mode 100644
index 0000000..b9f9619
--- /dev/null
+++ b/ChromeboxFanControlService/FanControlWorker.cs
@@ -0,0 +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);
+ }
+}
diff --git a/ChromeboxFanControlService/Program.cs b/ChromeboxFanControlService/Program.cs
new file mode 100644
index 0000000..ce209b5
--- /dev/null
+++ b/ChromeboxFanControlService/Program.cs
@@ -0,0 +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();
+ })
+ .Build();
+
+await host.RunAsync();
diff --git a/ChromeboxFanControlService/appsettings.json b/ChromeboxFanControlService/appsettings.json
new file mode 100644
index 0000000..2996c3f
--- /dev/null
+++ b/ChromeboxFanControlService/appsettings.json
@@ -0,0 +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 ]
+}
diff --git a/README.md b/README.md
index a850c37..f912c20 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
`C:\Program Files\crosec\ectool.exe`
若路径不同,在程序「高级」选项卡中修改。
2. **以管理员身份运行**本程序(清单已要求提升权限;LHM 与 EC 通信通常需要管理员)。
-3. 本机安装 [.NET 8 运行时](https://dotnet.microsoft.com/download/dotnet/8.0)(若使用 `dotnet publish --self-contained false` 发布,目标机需运行时)。
+3. **客户机**:`.\build.ps1 -publish`(及带 `-msi` 时)默认**自包含单文件**发布,**已内含 .NET 8 运行时**,`dist\` 仅含数个 exe,无需再装运行时。若需多 DLL 或依赖系统 .NET 8 以减小体积,可加 `-multiFile` 或 `-frameworkDependent`。
## 编译
@@ -16,28 +16,49 @@
.\build.ps1
```
-发布到 `dist\`:
+### 发布 MSI 安装包(推荐)
```powershell
-.\build.ps1 -publish
+.\build.ps1 -publish -msi
```
-需已安装 [.NET 8 SDK](https://dotnet.microsoft.com/download)。
+`-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)。服务模式下配置从 `%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`**:保存后会覆盖同名项(用户配置)。
+- **`%AppData%\ChromeboxFanControl\config.json`**:GUI 模式下保存后覆盖(用户配置)。
+- **`%ProgramData%\ChromeboxFanControl\config.json`**:Windows 服务模式下读取的配置;可与 GUI 共用同一份配置逻辑。
重要字段:
| 项 | 说明 |
|----|------|
+| `Language` | 界面语言:`auto`(跟随系统)、`en`、`zh-Hans`、`zh-Hant`。修改后需重启生效。 |
| `FanRpmArgs` | 读取转速时传给 ectool 的参数(默认 `pwmgetfanrpm` `0`),请用本机 `ectool help` 核对。 |
| `FanDutyArgs` | 读取占空比时传给 ectool 的参数(如 `pwmget` `0`);空则跳过,显示目标占空比。 |
| `AutoFanCtrlArgs` | 退出程序时恢复 EC 自动风扇(默认 `autofanctrl`),若命令名不同请修改。 |
| `TempSource` | `AverageCore` 或 `MaxCore`。 |
-| `CurvePoints` | 13 个 0–100 的占空比控制点(与 Chrultrabook-Tools 曲线逻辑一致)。 |
+| `CurvePoints` | 14 个 0–100 的占空比控制点(对应温度断点 0,40,45,…,100°C,线性插值)。 |
## 使用说明
diff --git a/build.ps1 b/build.ps1
index 624c484..96059f8 100644
--- a/build.ps1
+++ b/build.ps1
@@ -22,6 +22,13 @@ if (-not $exe) {
Write-Error "dotnet SDK not found. Install .NET 8 SDK: https://dotnet.microsoft.com/download"
}
+# -frameworkDependent: no bundled runtime (fewer files, needs .NET 8 on target)
+# -multiFile: disable single-file (default is single exe to reduce DLL count)
+$frameworkDependent = $args -contains "-frameworkDependent" -or $args -contains "--frameworkDependent"
+$multiFile = $args -contains "-multiFile" -or $args -contains "--multiFile"
+$selfContained = if ($frameworkDependent) { "false" } else { "true" }
+$singleFile = -not $multiFile
+
function Invoke-Dotnet {
if ($exe -eq "dotnet") {
dotnet @args
@@ -30,28 +37,122 @@ function Invoke-Dotnet {
}
}
+$doPublish = $args -contains "-publish" -or $args -contains "--publish"
+$doPublishGui = $args -contains "-publishGui" -or $args -contains "--publishGui"
+$doPublishSvc = $args -contains "-publishService" -or $args -contains "--publishService"
+# -publish without split = GUI + Service both
+if ($doPublish -and -not $doPublishGui -and -not $doPublishSvc) {
+ $doPublishGui = $true
+ $doPublishSvc = $true
+}
+$doPublishAny = $doPublishGui -or $doPublishSvc
+# -msi requires WiX; -publish only generates dist, MSI optional
+$doMsi = $args -contains "-msi" -or $args -contains "--msi"
Write-Host ">> dotnet restore"
-Invoke-Dotnet restore "$root\ChromeboxFanControl.sln"
+if ($doPublishAny -or $doMsi) {
+ Invoke-Dotnet restore "$root\ChromeboxFanControl.sln" -r win-x64
+} else {
+ Invoke-Dotnet restore "$root\ChromeboxFanControl.sln"
+}
Write-Host ">> dotnet build Release"
Invoke-Dotnet build "$root\ChromeboxFanControl.sln" -c Release --no-restore
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
-$doPublish = $args -contains "-publish" -or $args -contains "--publish"
-if ($doPublish) {
+if ($doPublishAny) {
$out = Join-Path $root "dist"
- if (Test-Path $out) { Remove-Item $out -Recurse -Force }
- New-Item -ItemType Directory -Path $out | Out-Null
- Write-Host ">> dotnet publish -> $out"
- Invoke-Dotnet publish "$root\ChromeboxFanControl\ChromeboxFanControl.csproj" `
- -c Release `
- -o $out `
- -r win-x64 `
- --self-contained false `
- -p:PublishSingleFile=false `
- --no-restore
- Write-Host "Done: $out\ChromeboxFanControl.exe"
-} else {
- Write-Host "Done: $root\ChromeboxFanControl\bin\Release\net8.0-windows\ChromeboxFanControl.exe"
- Write-Host "(Run .\build.ps1 -publish to output to dist\)"
+ if ($doPublishGui -and $doPublishSvc) {
+ if (Test-Path $out) { Remove-Item $out -Recurse -Force }
+ New-Item -ItemType Directory -Path $out | Out-Null
+ } elseif ($doPublishGui -and -not (Test-Path $out)) {
+ New-Item -ItemType Directory -Path $out | Out-Null
+ }
+ if ($doPublishGui) {
+ Write-Host ">> dotnet publish GUI -> $out"
+ $guiArgs = @("-c", "Release", "-o", $out, "-r", "win-x64", "--self-contained", "false", "--no-restore")
+ if ($singleFile) { $guiArgs += "-p:PublishSingleFile=true", "-p:IncludeNativeLibrariesForSelfExtract=true" }
+ Invoke-Dotnet publish "$root\ChromeboxFanControl\ChromeboxFanControl.csproj" @guiArgs
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ }
+ if ($doPublishSvc) {
+ Write-Host ">> dotnet publish Service -> dist-svc"
+ $svcOut = Join-Path $root "dist-svc"
+ $svcArgs = @("-c", "Release", "-o", $svcOut, "-r", "win-x64", "--self-contained", $selfContained, "--no-restore")
+ if ($singleFile) { $svcArgs += "-p:PublishSingleFile=true", "-p:IncludeNativeLibrariesForSelfExtract=true" }
+ Invoke-Dotnet publish "$root\ChromeboxFanControlService\ChromeboxFanControlService.csproj" @svcArgs
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ if (-not (Test-Path $out)) { New-Item -ItemType Directory -Path $out | Out-Null }
+ Copy-Item "$svcOut\*" $out -Recurse -Force -Exclude "appsettings.json"
+ if (-not $doMsi) { Remove-Item $svcOut -Recurse -Force }
+ }
+ if ($doPublishGui -and $doPublishSvc) {
+ Write-Host "Done: $out\ChromeboxFanControl.exe (GUI), $out\ChromeboxFanControlService.exe (Service)"
+ } elseif ($doPublishGui) {
+ Write-Host "Done (GUI only): $out\ChromeboxFanControl.exe"
+ } else {
+ Write-Host "Done (Service only): $out\ChromeboxFanControlService.exe (merged into dist\)"
+ }
+}
+
+if ($doMsi) {
+ $dist = Join-Path $root "dist"
+ $distSvc = Join-Path $root "dist-svc"
+ if (-not (Test-Path $dist)) {
+ Write-Error "dist\ not found. Run .\build.ps1 -publish first."
+ }
+ if (-not (Test-Path $distSvc)) {
+ Write-Host ">> dist-svc missing, publishing Service for service MSI"
+ $svcArgs = @("-c", "Release", "-o", $distSvc, "-r", "win-x64", "--self-contained", $selfContained, "--no-restore")
+ if ($singleFile) { $svcArgs += "-p:PublishSingleFile=true", "-p:IncludeNativeLibrariesForSelfExtract=true" }
+ Invoke-Dotnet publish "$root\ChromeboxFanControlService\ChromeboxFanControlService.csproj" @svcArgs
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ }
+ $wixBin = $null
+ foreach ($v in @("3.11", "3.14", "3.10")) {
+ $p = "${env:ProgramFiles(x86)}\WiX Toolset v$v\bin"
+ if (Test-Path "$p\candle.exe") { $wixBin = $p; break }
+ }
+ if (-not $wixBin) {
+ Write-Warning "WiX Toolset not found. Skipping MSI. Install from https://wixtoolset.org/docs/wix3/ to build installers."
+ } else {
+ $wixDir = Join-Path $root "wix"
+ $outMsi = Join-Path $root "dist-installer"
+ if (-not (Test-Path $outMsi)) { New-Item -ItemType Directory -Path $outMsi | Out-Null }
+ $objDir = Join-Path $root "wix\obj"
+ if (-not (Test-Path $objDir)) { New-Item -ItemType Directory -Path $objDir | Out-Null }
+ $ext = "-ext", "$wixBin\WixUtilExtension.dll", "-ext", "$wixBin\WixUIExtension.dll"
+
+ Write-Host ">> WiX: Desktop MSI"
+ $harvested = Join-Path $root "wix\Harvested.wxs"
+ & "$wixBin\heat.exe" dir $dist -cg AppFiles -gg -sf -srd -sreg -scom -dr INSTALLFOLDER -out $harvested -t "$wixDir\exclude-service.xsl" -sw5151
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & "$wixBin\candle.exe" -arch x64 -out "$objDir\\" @ext "$wixDir\Product.wxs" $harvested
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & "$wixBin\light.exe" -out "$outMsi\ChromeboxFanControl-Setup.msi" -b $dist @ext "$objDir\Product.wixobj" "$objDir\Harvested.wixobj"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ Remove-Item $harvested -Force -ErrorAction SilentlyContinue
+
+ Write-Host ">> WiX: Service MSI"
+ $harvestedSvc = Join-Path $root "wix\Harvested-Service.wxs"
+ & "$wixBin\heat.exe" dir $distSvc -cg ServiceFiles -gg -sf -srd -sreg -scom -dr INSTALLFOLDER -out $harvestedSvc -t "$wixDir\exclude-service.xsl" -sw5151
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & "$wixBin\candle.exe" -arch x64 -out "$objDir\\" @ext "$wixDir\Product-Service.wxs" $harvestedSvc
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & "$wixBin\light.exe" -out "$outMsi\ChromeboxFanControlService-Setup.msi" -b $distSvc @ext "$objDir\Product-Service.wixobj" "$objDir\Harvested-Service.wixobj"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ Remove-Item $harvestedSvc -Force -ErrorAction SilentlyContinue
+
+ if (Test-Path $distSvc) { Remove-Item $distSvc -Recurse -Force }
+ Write-Host "Done: $outMsi\ChromeboxFanControl-Setup.msi (Desktop), $outMsi\ChromeboxFanControlService-Setup.msi (Service)"
+ }
+}
+
+if (-not $doPublishAny -and -not $doMsi) {
+ Write-Host "Done: $root\ChromeboxFanControl\bin\Release\net8.0-windows\ChromeboxFanControl.exe"
+ Write-Host " .\build.ps1 -publish -> dist\ GUI + Service (self-contained)"
+ Write-Host " .\build.ps1 -publishGui -> GUI only"
+ Write-Host " .\build.ps1 -publishService -> Service only (merge into dist\)"
+ Write-Host " .\build.ps1 -publish -msi -> dist + two MSI (needs WiX)"
+ Write-Host " .\build.ps1 -publish -frameworkDependent -> no bundled runtime (needs .NET 8)"
+ Write-Host " .\build.ps1 -publish -multiFile -> keep multiple DLLs (default: single exe)"
}
diff --git a/wix/Product-Service.wxs b/wix/Product-Service.wxs
new file mode 100644
index 0000000..7ce7bf4
--- /dev/null
+++ b/wix/Product-Service.wxs
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wix/Product.wxs b/wix/Product.wxs
new file mode 100644
index 0000000..cd727da
--- /dev/null
+++ b/wix/Product.wxs
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wix/exclude-service.xsl b/wix/exclude-service.xsl
new file mode 100644
index 0000000..3b71a9f
--- /dev/null
+++ b/wix/exclude-service.xsl
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wix314.exe b/wix314.exe
new file mode 100644
index 0000000..5e45d5c
Binary files /dev/null and b/wix314.exe differ