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(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(); 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().DisconnectAsync().GetAwaiter().GetResult(); _services.GetRequiredService().DisconnectAsync().GetAwaiter().GetResult(); _services.GetRequiredService().DisconnectAsync().GetAwaiter().GetResult(); _services.GetRequiredService().DisconnectAsync().GetAwaiter().GetResult(); _services.GetRequiredService().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() ?? 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() ?? new StorageOptions(); var reportOpts = cfg.GetSection(ReportOptions.SectionName).Get() ?? 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() ?? new ConsoleOptions(); EnsureConsoleOptions(consoleOpts); services.AddSingleton>(Options.Create(devOpts)); services.AddSingleton>(Options.Create(storageOpts)); services.AddSingleton>(Options.Create(reportOpts)); services.AddSingleton>(Options.Create(consoleOpts)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => { var opt = sp.GetRequiredService>().Value; if (!string.IsNullOrEmpty(opt.ProjectsRoot)) Directory.CreateDirectory(opt.ProjectsRoot); return new ProjectStore { ProjectsRoot = opt.ProjectsRoot }; }); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddTransient(sp => new CurveTest(sp.GetRequiredService>())); services.AddTransient(); services.AddTransient(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddSingleton(); } 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); } } /// 当 appsettings.json 缺失时使用的内置默认配置(与该文件等价)。 private static IEnumerable> DefaultSettings() { return new Dictionary { ["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); } }