SSPC-Tester/SSPCTester.UI/App.xaml.cs
2026-06-30 12:10:29 +08:00

316 lines
14 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 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
{
var store = _services.GetRequiredService<UserSettingsStore>();
store.SaveAsync().GetAwaiter().GetResult();
_services.GetRequiredService<ISspc>().DisconnectAsync().GetAwaiter().GetResult();
_services.GetRequiredService<IPowerSupply>().DisconnectAsync().GetAwaiter().GetResult();
_services.GetRequiredService<ILoad>().DisconnectAsync().GetAwaiter().GetResult();
_services.GetRequiredService<IDaq>().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;
}
var storageOpts = cfg.GetSection(StorageOptions.SectionName).Get<StorageOptions>() ?? new StorageOptions();
var reportOpts = cfg.GetSection(ReportOptions.SectionName).Get<ReportOptions>() ?? new ReportOptions();
services.AddSingleton<IOptions<DeviceOptions>>(Options.Create(devOpts));
services.AddSingleton<IOptions<StorageOptions>>(Options.Create(storageOpts));
services.AddSingleton<IOptions<ReportOptions>>(Options.Create(reportOpts));
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<Pcie8586Daq>();
services.AddSingleton<IDaq, ConfigurableDaq>();
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<MainViewModel>();
services.AddTransient<HomeViewModel>();
services.AddTransient<ProjectInfoViewModel>();
services.AddTransient<TestHostViewModel>();
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: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:Daq:LogicalDeviceId"] = "0",
["Devices:Daq:UseMock"] = "false",
["Devices:Daq:AutoConnect"] = "true",
["Devices:Daq:ChannelCount"] = "4",
["Devices:Daq:InputRange"] = "PlusMinus5V",
["Devices:Daq:ClockDivider"] = "1000",
["Devices:Daq:PreTriggerMs"] = "10",
["Devices:Daq:PostTriggerMs"] = "40",
["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"] = "OutputCurrent",
["Devices:Daq:Channels:1:Name"] = "输出电流",
["Devices:Daq:Channels:1:Scale"] = "1",
["Devices:Daq:Channels:1:Offset"] = "0",
["Devices:Daq:Channels:1:IsCalibrated"] = "true",
["Devices:Daq:Channels:2:PhysicalChannel"] = "2",
["Devices:Daq:Channels:2:Role"] = "InputVoltage",
["Devices:Daq:Channels:2:Name"] = "输入电压",
["Devices:Daq:Channels:2:Scale"] = "1",
["Devices:Daq:Channels:2:Offset"] = "0",
["Devices:Daq:Channels:2:IsCalibrated"] = "true",
["Devices:Daq:Channels:3:PhysicalChannel"] = "3",
["Devices:Daq:Channels:3:Role"] = "InputCurrent",
["Devices:Daq:Channels:3:Name"] = "输入电流",
["Devices:Daq:Channels:3:Scale"] = "1",
["Devices:Daq:Channels:3:Offset"] = "0",
["Devices:Daq:Channels:3:IsCalibrated"] = "true",
["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 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);
}
}