using System.Text.Json; using System.Text.Json.Serialization; 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). public int FanRpmPollEveryNCycles { get; set; } = 1; public string[] FanRpmArgs { get; set; } = ["pwmgetfanrpm", "0"]; /// Ectool args to read fan duty (0-100). Empty = skip, use commanded duty. 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(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)); } }