SSPC-Tester/SSPCTester.UI/App.xaml.cs
2026-07-13 03:51:13 +08:00

357 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.IO;
using System.Windows;
using System.Windows.Threading;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog;
using Serilog.Extensions.Logging;
using Pcie8586Probe.Hardware;
using SSPCTester.Devices.Config;
using SSPCTester.Devices.Drivers.Mock;
using SSPCTester.Devices.Drivers.Real;
using SSPCTester.Devices.Drivers;
using SSPCTester.Devices.Interfaces;
using SSPCTester.Logic.Report;
using SSPCTester.Logic.Storage;
using SSPCTester.Logic.Testing;
using SSPCTester.UI.Services;
using SSPCTester.UI.ViewModels;
using SSPCTester.UI.Views;
namespace SSPCTester.UI;
public partial class App : Application
{
private ServiceProvider? _services;
public static IServiceProvider Services => ((App)Current)._services!;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
DispatcherUnhandledException += OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException;
try
{
var builder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddInMemoryCollection(DefaultSettings())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile(UserSettingsStore.SettingsPath, optional: true, reloadOnChange: false);
var configuration = builder.Build();
ExpandEnvPaths(configuration);
// Serilog fail-safe配置加载失败时退回到代码直接配置。
try
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
}
catch
{
var logFile = Environment.ExpandEnvironmentVariables(
"%LOCALAPPDATA%\\SSPCTester\\Logs\\app-.log");
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Debug()
.WriteTo.File(logFile, rollingInterval: Serilog.RollingInterval.Day, retainedFileCountLimit: 7)
.CreateLogger();
}
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging(b =>
{
b.ClearProviders();
b.AddProvider(new SerilogLoggerProvider(Log.Logger, dispose: false));
});
ConfigureServices(configuration, services);
_services = services.BuildServiceProvider();
Log.Information("UI 启动DI 构建完成。");
var main = _services.GetRequiredService<MainWindow>();
main.Show();
Log.Information("UI 启动MainWindow.Show 已调用。");
}
catch (Exception ex)
{
FatalCrash(ex);
}
}
internal static void DiagTrace(string tag)
{
try
{
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SSPCTester", "Logs", "diag.log");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.AppendAllText(path, $"{DateTime.Now:HH:mm:ss.fff} {tag}{Environment.NewLine}");
}
catch { /* ignore */ }
}
protected override void OnExit(ExitEventArgs e)
{
if (_services != null)
{
try
{
_services.GetRequiredService<ISspc>().DisconnectAsync().GetAwaiter().GetResult();
_services.GetRequiredService<IPowerSupply>().DisconnectAsync().GetAwaiter().GetResult();
_services.GetRequiredService<ILoad>().DisconnectAsync().GetAwaiter().GetResult();
_services.GetRequiredService<IDaq>().DisconnectAsync().GetAwaiter().GetResult();
_services.GetRequiredService<ISafetyDio>().DisconnectAsync().GetAwaiter().GetResult();
}
catch (Exception ex) { Log.Warning(ex, "退出时保存配置或断开设备失败。"); }
}
try { _services?.Dispose(); } catch { /* swallow */ }
Log.CloseAndFlush();
base.OnExit(e);
}
private static void ConfigureServices(IConfiguration cfg, IServiceCollection services)
{
var devOpts = cfg.GetSection(DeviceOptions.SectionName).Get<DeviceOptions>() ?? new DeviceOptions();
NormalizeDaqOptions(devOpts.Daq);
if (cfg["Devices:Sspc:UseMock"] == null)
{
devOpts.Sspc.UseMock = devOpts.MockMode;
devOpts.PowerSupply.UseMock = devOpts.MockMode;
devOpts.Load.UseMock = devOpts.MockMode;
devOpts.Daq.UseMock = devOpts.MockMode;
devOpts.Pci2312.UseMock = devOpts.MockMode;
}
var storageOpts = cfg.GetSection(StorageOptions.SectionName).Get<StorageOptions>() ?? new StorageOptions();
var reportOpts = cfg.GetSection(ReportOptions.SectionName).Get<ReportOptions>() ?? new ReportOptions();
// Preset collections must be loaded as complete arrays. ConfigurationBinder
// merges array indexes, which brings deleted default presets back on restart.
var consoleOpts = UserSettingsStore.LoadConsoleOptions()
?? cfg.GetSection(ConsoleOptions.SectionName).Get<ConsoleOptions>()
?? new ConsoleOptions();
EnsureConsoleOptions(consoleOpts);
services.AddSingleton<IOptions<DeviceOptions>>(Options.Create(devOpts));
services.AddSingleton<IOptions<StorageOptions>>(Options.Create(storageOpts));
services.AddSingleton<IOptions<ReportOptions>>(Options.Create(reportOpts));
services.AddSingleton<IOptions<ConsoleOptions>>(Options.Create(consoleOpts));
services.AddSingleton<MockSspc>();
services.AddSingleton<SspcModbus>();
services.AddSingleton<ISspc, ConfigurableSspc>();
services.AddSingleton<MockPowerSupply>();
services.AddSingleton<Udp5080>();
services.AddSingleton<IPowerSupply, ConfigurablePowerSupply>();
services.AddSingleton<MockLoad>();
services.AddSingleton<It87xxLoad>();
services.AddSingleton<ILoad, ConfigurableLoad>();
services.AddSingleton<MockDaq>();
services.AddSingleton<IDigitizer, Pcie8586Digitizer>();
services.AddSingleton<Pcie8586Daq>();
services.AddSingleton<IDaq, ConfigurableDaq>();
services.AddSingleton<MockSafetyDio>();
services.AddSingleton<Pci2312SafetyDio>();
services.AddSingleton<ISafetyDio, ConfigurableSafetyDio>();
services.AddSingleton<UserSettingsStore>();
services.AddSingleton<ProjectStore>(sp =>
{
var opt = sp.GetRequiredService<IOptions<StorageOptions>>().Value;
if (!string.IsNullOrEmpty(opt.ProjectsRoot)) Directory.CreateDirectory(opt.ProjectsRoot);
return new ProjectStore { ProjectsRoot = opt.ProjectsRoot };
});
services.AddSingleton<ReportGenerator>();
services.AddSingleton<WordReportGenerator>();
services.AddSingleton<TestRunner>();
services.AddTransient<BasicTest>();
services.AddTransient<CurveTest>(sp => new CurveTest(sp.GetRequiredService<IOptions<DeviceOptions>>()));
services.AddTransient<PowerTest>();
services.AddTransient<ProtectTest>();
services.AddSingleton<UiLogSink>();
services.AddSingleton<SafetyMonitorService>();
services.AddSingleton<MainViewModel>();
services.AddTransient<HomeViewModel>();
services.AddTransient<ProjectInfoViewModel>();
services.AddTransient<TestHostViewModel>();
// The experiment console controls application-wide hardware and is not
// owned by a project. Keep one instance so switching project pages does
// not replace its state or interrupt its refresh loop.
services.AddSingleton<ExperimentConsoleViewModel>();
services.AddTransient<BasicTestViewModel>();
services.AddTransient<CurveTestViewModel>();
services.AddTransient<PowerTestViewModel>();
services.AddTransient<ProtectViewModel>();
services.AddTransient<SettingsViewModel>();
services.AddTransient<ReportViewModel>();
services.AddSingleton<MainWindow>();
}
private static void ExpandEnvPaths(IConfiguration cfg)
{
string[] keys =
{
"Storage:ProjectsRoot",
"Report:SavePath",
"Serilog:WriteTo:1:Args:path",
};
foreach (var k in keys)
{
var v = cfg[k];
if (!string.IsNullOrEmpty(v))
cfg[k] = Environment.ExpandEnvironmentVariables(v);
}
}
/// <summary>当 appsettings.json 缺失时使用的内置默认配置(与该文件等价)。</summary>
private static IEnumerable<KeyValuePair<string, string?>> DefaultSettings()
{
return new Dictionary<string, string?>
{
["Devices:MockMode"] = "false",
["Devices:Sspc:UseMock"] = "false",
["Devices:Sspc:AutoConnect"] = "true",
["Devices:Sspc:PortName"] = "COM1",
["Devices:Sspc:BaudRate"] = "9600",
["Devices:Sspc:DataBits"] = "8",
["Devices:Sspc:Parity"] = "None",
["Devices:Sspc:StopBits"] = "One",
["Devices:Sspc:ReadTimeoutMs"] = "500",
["Devices:Sspc:Retry"] = "2",
["Devices:Sspc:DeviceProfilePath"] = "Config/device-profile.json",
["Devices:Sspc:CloseAllRelaysOnDisconnect"] = "false",
["Devices:Sspc:LogRawFrames"] = "true",
["Devices:Sspc:ModuleAddresses:0"] = "1",
["Devices:Sspc:ModuleAddresses:1"] = "2",
["Devices:Sspc:ModuleAddresses:2"] = "3",
["Devices:Sspc:ChannelsPerModule"] = "8",
["Devices:PowerSupply:PortName"] = "COM2",
["Devices:PowerSupply:UseMock"] = "false",
["Devices:PowerSupply:AutoConnect"] = "true",
["Devices:PowerSupply:BaudRate"] = "9600",
["Devices:PowerSupply:Host"] = "192.168.6.5",
["Devices:PowerSupply:Port"] = "5025",
["Devices:PowerSupply:TimeoutSeconds"] = "2",
["Devices:PowerSupply:RequirePingBeforeConnect"] = "true",
["Devices:PowerSupply:Terminator"] = "\n",
["Devices:PowerSupply:IdnCommand"] = "*IDN?",
["Devices:PowerSupply:VoltageCommand"] = "MEASure:VOLTage?",
["Devices:PowerSupply:CurrentCommand"] = "MEASure:CURRent?",
["Devices:PowerSupply:SetVoltageCommand"] = "VOLT {0}",
["Devices:PowerSupply:SetCurrentLimitCommand"] = "CURR {0}",
["Devices:PowerSupply:ErrorCommand"] = "SYST:ERR?",
["Devices:Load:PortName"] = "COM3",
["Devices:Load:UseMock"] = "false",
["Devices:Load:AutoConnect"] = "true",
["Devices:Load:BaudRate"] = "9600",
["Devices:Load:Host"] = "192.168.200.100",
["Devices:Load:Port"] = "30000",
["Devices:Load:TimeoutSeconds"] = "3",
["Devices:Load:Terminator"] = "\n",
["Devices:Load:Channel"] = "1",
["Devices:Load:UseFetch"] = "true",
["Devices:Load:QueryMode"] = "Combined",
["Devices:Load:SetModeCommand"] = "",
["Devices:Load:SetValueCommand"] = "{0} {1}",
["Devices:Load:ResetCommand"] = "*RST",
["Devices:Load:ErrorCommand"] = "SYST:ERR?",
["Devices:Daq:LogicalDeviceId"] = "0",
["Devices:Daq:UseMock"] = "false",
["Devices:Daq:AutoConnect"] = "true",
["Devices:Daq:ChannelCount"] = "1",
["Devices:Daq:InputRange"] = "PlusMinus5V",
["Devices:Daq:ClockDivider"] = "1000",
["Devices:Daq:PreTriggerMs"] = "3",
["Devices:Daq:PostTriggerMs"] = "100",
["Devices:Daq:Channels:0:PhysicalChannel"] = "0",
["Devices:Daq:Channels:0:Role"] = "OutputVoltage",
["Devices:Daq:Channels:0:Name"] = "输出电压",
["Devices:Daq:Channels:0:Scale"] = "1",
["Devices:Daq:Channels:0:Offset"] = "0",
["Devices:Daq:Channels:0:IsCalibrated"] = "true",
["Devices:Daq:Channels:1:PhysicalChannel"] = "1",
["Devices:Daq:Channels:1:Role"] = "Unconfigured",
["Devices:Daq:Channels:1:Name"] = "CH1",
["Devices:Daq:Channels:1:Scale"] = "1",
["Devices:Daq:Channels:1:Offset"] = "0",
["Devices:Daq:Channels:1:IsCalibrated"] = "false",
["Devices:Daq:Channels:2:PhysicalChannel"] = "2",
["Devices:Daq:Channels:2:Role"] = "Unconfigured",
["Devices:Daq:Channels:2:Name"] = "CH2",
["Devices:Daq:Channels:2:Scale"] = "1",
["Devices:Daq:Channels:2:Offset"] = "0",
["Devices:Daq:Channels:2:IsCalibrated"] = "false",
["Devices:Daq:Channels:3:PhysicalChannel"] = "3",
["Devices:Daq:Channels:3:Role"] = "Unconfigured",
["Devices:Daq:Channels:3:Name"] = "CH3",
["Devices:Daq:Channels:3:Scale"] = "1",
["Devices:Daq:Channels:3:Offset"] = "0",
["Devices:Daq:Channels:3:IsCalibrated"] = "false",
["Devices:Pci2312:Enabled"] = "true",
["Devices:Pci2312:UseMock"] = "false",
["Devices:Pci2312:AutoConnect"] = "true",
["Devices:Pci2312:DeviceId"] = "0",
["Devices:Pci2312:EmergencyInputChannel"] = "0",
["Devices:Pci2312:EmergencyInputActiveHigh"] = "true",
["Devices:Pci2312:AlarmOutputChannel"] = "0",
["Devices:Pci2312:AlarmOutputActiveHigh"] = "true",
["Devices:Pci2312:PollIntervalMs"] = "100",
["Storage:ProjectsRoot"] = "%LOCALAPPDATA%\\SSPCTester\\Projects",
["Report:SavePath"] = "%LOCALAPPDATA%\\SSPCTester\\Reports",
["Report:Format"] = "Pdf",
["Serilog:MinimumLevel:Default"] = "Information",
["Serilog:WriteTo:0:Name"] = "Debug",
["Serilog:WriteTo:1:Name"] = "File",
["Serilog:WriteTo:1:Args:path"] = "%LOCALAPPDATA%\\SSPCTester\\Logs\\app-.log",
["Serilog:WriteTo:1:Args:rollingInterval"] = "Day",
["Serilog:WriteTo:1:Args:retainedFileCountLimit"] = "7",
};
}
private static void NormalizeDaqOptions(DaqOptions options)
{
options.Channels = DaqChannelOptionsNormalizer.Normalize(options.Channels);
}
private static void EnsureConsoleOptions(ConsoleOptions options)
{
if (options.PowerPresets == null)
options.PowerPresets = ConsoleOptions.DefaultPowerPresets();
if (options.LoadPresets == null)
options.LoadPresets = ConsoleOptions.DefaultLoadPresets();
}
private static void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
FatalCrash(e.Exception);
e.Handled = true;
}
private static void OnDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception ex) FatalCrash(ex);
}
private static void FatalCrash(Exception ex)
{
try
{
Log.Error(ex, "Fatal");
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SSPCTester", "Logs", $"crash-{DateTime.Now:yyyyMMdd_HHmmss}.txt");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, ex.ToString());
}
catch { /* swallow */ }
MessageBox.Show(ex.ToString(), "SSPCTester 启动失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
}