2026-06-30 12:10:29 +08:00
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
|
using SSPCTester.Devices.Config;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SSPCTester.UI.Services;
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
/// <summary>将机器本地配置保存到 LocalAppData,不修改发布目录。</summary>
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public sealed class UserSettingsStore : IDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
|
|
|
|
{
|
|
|
|
|
|
WriteIndented = true,
|
|
|
|
|
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private readonly DeviceOptions _devices;
|
|
|
|
|
|
private readonly StorageOptions _storage;
|
|
|
|
|
|
private readonly ReportOptions _report;
|
2026-07-12 12:07:41 +08:00
|
|
|
|
private readonly ConsoleOptions _console;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
|
|
|
|
|
|
|
|
|
|
|
public UserSettingsStore(
|
|
|
|
|
|
IOptions<DeviceOptions> devices,
|
|
|
|
|
|
IOptions<StorageOptions> storage,
|
2026-07-12 12:07:41 +08:00
|
|
|
|
IOptions<ReportOptions> report,
|
|
|
|
|
|
IOptions<ConsoleOptions> console)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
_devices = devices.Value;
|
|
|
|
|
|
_storage = storage.Value;
|
|
|
|
|
|
_report = report.Value;
|
2026-07-12 12:07:41 +08:00
|
|
|
|
_console = console.Value;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static string SettingsPath => Path.Combine(
|
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
|
|
|
"SSPCTester", "settings.json");
|
|
|
|
|
|
|
2026-07-12 12:07:41 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Loads console presets directly from the user settings JSON so preset arrays
|
|
|
|
|
|
/// replace the defaults as a whole. IConfiguration merges arrays by index and
|
|
|
|
|
|
/// cannot represent a deleted item or an explicitly empty preset list.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static ConsoleOptions? LoadConsoleOptions()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
string path = SettingsPath;
|
|
|
|
|
|
if (!File.Exists(path)) return null;
|
|
|
|
|
|
|
|
|
|
|
|
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
|
|
|
|
|
if (!document.RootElement.TryGetProperty(ConsoleOptions.SectionName, out var consoleElement))
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
|
|
var console = consoleElement.Deserialize<ConsoleOptions>(JsonOptions);
|
|
|
|
|
|
if (console is null) return null;
|
|
|
|
|
|
|
|
|
|
|
|
EnsureConsoleDefaults(console);
|
|
|
|
|
|
return console;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public async Task SaveAsync()
|
2026-07-12 12:07:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
await SaveAsync(_devices, _storage, _report, _console).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task SaveAsync(DeviceOptions devices, StorageOptions storage, ReportOptions report)
|
|
|
|
|
|
{
|
|
|
|
|
|
await SaveAsync(devices, storage, report, _console).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task SaveAsync(DeviceOptions devices, StorageOptions storage, ReportOptions report, ConsoleOptions console)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
await _writeLock.WaitAsync().ConfigureAwait(false);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
string path = SettingsPath;
|
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
|
|
|
|
string temp = path + ".tmp";
|
2026-07-12 12:07:41 +08:00
|
|
|
|
var snapshot = CreateSnapshot(devices, storage, report, console);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
var model = new
|
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
|
Devices = snapshot.Devices,
|
|
|
|
|
|
Storage = snapshot.Storage,
|
|
|
|
|
|
Report = snapshot.Report,
|
|
|
|
|
|
Console = snapshot.Console
|
2026-06-30 12:10:29 +08:00
|
|
|
|
};
|
|
|
|
|
|
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(model, JsonOptions)).ConfigureAwait(false);
|
|
|
|
|
|
File.Move(temp, path, true);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
WriteDaqSaveTrace(path, snapshot.Devices);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally { _writeLock.Release(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public SettingsSnapshot? LoadSnapshot()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
string path = SettingsPath;
|
|
|
|
|
|
if (!File.Exists(path)) return null;
|
|
|
|
|
|
var json = File.ReadAllText(path);
|
|
|
|
|
|
var payload = JsonSerializer.Deserialize<SettingsPayload>(json, JsonOptions);
|
|
|
|
|
|
if (payload is null) return null;
|
|
|
|
|
|
return CreateSnapshot(payload.Devices, payload.Storage, payload.Report, payload.Console);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static SettingsSnapshot CreateSnapshot(DeviceOptions devices, StorageOptions storage, ReportOptions report, ConsoleOptions console)
|
|
|
|
|
|
{
|
|
|
|
|
|
var copiedDevices = JsonSerializer.Deserialize<DeviceOptions>(JsonSerializer.Serialize(devices, JsonOptions)) ?? new DeviceOptions();
|
|
|
|
|
|
var copiedStorage = JsonSerializer.Deserialize<StorageOptions>(JsonSerializer.Serialize(storage, JsonOptions)) ?? new StorageOptions();
|
|
|
|
|
|
var copiedReport = JsonSerializer.Deserialize<ReportOptions>(JsonSerializer.Serialize(report, JsonOptions)) ?? new ReportOptions();
|
|
|
|
|
|
var copiedConsole = JsonSerializer.Deserialize<ConsoleOptions>(JsonSerializer.Serialize(console, JsonOptions)) ?? new ConsoleOptions();
|
|
|
|
|
|
copiedDevices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(copiedDevices.Daq.Channels);
|
|
|
|
|
|
EnsureConsoleDefaults(copiedConsole);
|
|
|
|
|
|
return new SettingsSnapshot(copiedDevices, copiedStorage, copiedReport, copiedConsole);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void EnsureConsoleDefaults(ConsoleOptions console)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (console.PowerPresets == null)
|
|
|
|
|
|
console.PowerPresets = ConsoleOptions.DefaultPowerPresets();
|
|
|
|
|
|
if (console.LoadPresets == null)
|
|
|
|
|
|
console.LoadPresets = ConsoleOptions.DefaultLoadPresets();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void WriteDaqSaveTrace(string settingsPath, DeviceOptions devices)
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
string logDir = Path.Combine(
|
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
|
|
|
"SSPCTester", "Logs");
|
|
|
|
|
|
Directory.CreateDirectory(logDir);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
string channels = string.Join(", ", devices.Daq.Channels
|
2026-06-30 12:10:29 +08:00
|
|
|
|
.OrderBy(x => x.PhysicalChannel)
|
|
|
|
|
|
.Select(x => $"CH{x.PhysicalChannel}:{x.Role}/{x.IsCalibrated}"));
|
|
|
|
|
|
File.AppendAllText(
|
|
|
|
|
|
Path.Combine(logDir, "settings-save.log"),
|
2026-07-12 12:07:41 +08:00
|
|
|
|
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {settingsPath} Pre={devices.Daq.PreTriggerMs:R} Post={devices.Daq.PostTriggerMs:R} Channels=[{channels}]{Environment.NewLine}");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
public void Dispose() => _writeLock.Dispose();
|
2026-07-12 12:07:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public sealed record SettingsSnapshot(DeviceOptions Devices, StorageOptions Storage, ReportOptions Report, ConsoleOptions Console);
|
|
|
|
|
|
|
|
|
|
|
|
internal sealed class SettingsPayload
|
|
|
|
|
|
{
|
|
|
|
|
|
public DeviceOptions Devices { get; set; } = new();
|
|
|
|
|
|
public StorageOptions Storage { get; set; } = new();
|
|
|
|
|
|
public ReportOptions Report { get; set; } = new();
|
|
|
|
|
|
public ConsoleOptions Console { get; set; } = new();
|
|
|
|
|
|
}
|