SSPC-Tester/SSPCTester.UI/App.xaml.cs

357 lines
16 KiB
C#
Raw Normal View History

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>();
2026-07-13 03:51:13 +08:00
// 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);
}
}