更新20260606
This commit is contained in:
@@ -1,14 +1,96 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace DentistryHandpieces
|
||||
namespace DentistryHandpieces;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
ConfigureLogging();
|
||||
DispatcherUnhandledException += OnDispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
|
||||
Log.Information(
|
||||
"应用启动,版本 {Version},系统 {OperatingSystem}",
|
||||
typeof(App).Assembly.GetName().Version?.ToString() ?? "unknown",
|
||||
Environment.OSVersion.VersionString);
|
||||
|
||||
base.OnStartup(e);
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
Log.Information("应用退出,退出代码 {ExitCode}", e.ApplicationExitCode);
|
||||
Log.CloseAndFlush();
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
private static void ConfigureLogging()
|
||||
{
|
||||
string logDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"DentistryHandpieces",
|
||||
"Logs");
|
||||
|
||||
try
|
||||
{
|
||||
CreateLogger(logDirectory);
|
||||
Log.Information("日志系统初始化完成,目录 {LogDirectory}", logDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string fallbackDirectory = Path.Combine(Path.GetTempPath(), "DentistryHandpieces", "Logs");
|
||||
try
|
||||
{
|
||||
CreateLogger(fallbackDirectory);
|
||||
Log.Warning(ex, "主日志目录初始化失败,已切换到临时目录 {LogDirectory}", fallbackDirectory);
|
||||
}
|
||||
catch (Exception fallbackException)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration().CreateLogger();
|
||||
System.Diagnostics.Debug.WriteLine($"Serilog initialization failed: {ex}; fallback failed: {fallbackException}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateLogger(string logDirectory)
|
||||
{
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Application", "DentistryHandpieces")
|
||||
.WriteTo.File(
|
||||
Path.Combine(logDirectory, "app-.log"),
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 30,
|
||||
fileSizeLimitBytes: 20 * 1024 * 1024,
|
||||
rollOnFileSizeLimit: true,
|
||||
shared: true,
|
||||
flushToDiskInterval: TimeSpan.FromSeconds(1),
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
private static void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
Log.Fatal(e.Exception, "UI线程发生未处理异常");
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object? sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Log.Fatal(e.ExceptionObject as Exception, "应用域发生未处理异常,IsTerminating={IsTerminating}", e.IsTerminating);
|
||||
}
|
||||
|
||||
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
Log.Error(e.Exception, "后台任务发生未观察异常");
|
||||
e.SetObserved();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||
<PackageReference Include="NModbusAsync" Version="3.0.2" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -13,8 +13,8 @@ public sealed class WpfFileDialogService : IFileDialogService
|
||||
{
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Title = "导出 Excel",
|
||||
FileName = $"牙科手机验收记录_{createdAt:yyyyMMddHHmmss}.xlsx",
|
||||
Title = "导出报表",
|
||||
FileName = $"牙科手机测试报表_{createdAt:yyyyMMddHHmmss}.xlsx",
|
||||
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx",
|
||||
DefaultExt = ".xlsx",
|
||||
AddExtension = true,
|
||||
|
||||
@@ -301,10 +301,60 @@
|
||||
<Setter Property="CornerRadius" Value="5" />
|
||||
<Setter Property="Padding" Value="12,9" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DataStatusBadge" TargetType="Border">
|
||||
<Setter Property="Background" Value="#ECFDF5" />
|
||||
<Setter Property="BorderBrush" Value="#A7DCC8" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Padding" Value="12,5" />
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<DockPanel LastChildFill="True">
|
||||
<Border DockPanel.Dock="Top"
|
||||
Style="{StaticResource PanelBorder}"
|
||||
BorderBrush="#AFC8D5"
|
||||
Background="#F8FBFD"
|
||||
Margin="0,0,0,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="190" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="测试数据与报表"
|
||||
FontSize="19"
|
||||
FontWeight="Bold"
|
||||
Foreground="#17384D" />
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="{Binding StatusText}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,4,0,0"
|
||||
Foreground="#52616F" />
|
||||
<Border Style="{StaticResource DataStatusBadge}"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,8,0,0">
|
||||
<TextBlock Text="{Binding DataCaptureStatusText}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#16624F" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Content="导出完整报表"
|
||||
Command="{Binding ExportCommand}"
|
||||
Style="{StaticResource StartButtonStyle}"
|
||||
MinHeight="58"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TabControl x:Name="MainTabs">
|
||||
<TabItem Header="轴向位移动量测试">
|
||||
<Grid>
|
||||
@@ -1168,56 +1218,6 @@
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="记录/导出">
|
||||
<Grid Margin="0,14,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Style="{StaticResource PanelBorder}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="150" />
|
||||
<ColumnDefinition Width="150" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="{Binding StatusText}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#52616F" />
|
||||
<Button Grid.Column="1"
|
||||
Content="保存记录"
|
||||
Command="{Binding SaveRecordCommand}"
|
||||
Margin="12,0,6,0" />
|
||||
<Button Grid.Column="2"
|
||||
Content="导出 Excel"
|
||||
Command="{Binding ExportCommand}"
|
||||
Margin="6,0,0,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<DataGrid Grid.Row="1"
|
||||
x:Name="RecordsGrid"
|
||||
ItemsSource="{Binding Records}"
|
||||
Margin="0,12,0,0"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
CanUserAddRows="False"
|
||||
HeadersVisibility="Column"
|
||||
RowHeight="44"
|
||||
FontSize="15">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="时间" Binding="{Binding CreatedAtText}" Width="180" />
|
||||
<DataGridTextColumn Header="结果" Binding="{Binding OverallResult}" Width="90" />
|
||||
<DataGridTextColumn Header="Excel文件" Binding="{Binding ExportPathText}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</DockPanel>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using Serilog;
|
||||
|
||||
namespace DentistryHandpieces;
|
||||
|
||||
@@ -24,6 +25,7 @@ public partial class MainWindow : Window
|
||||
};
|
||||
_hiddenSettingsPressTimer.Tick += HiddenSettingsPressTimer_Tick;
|
||||
Deactivated += (_, _) => ReleaseAllManualMotionTargetsAsync();
|
||||
Log.Information("主窗口创建完成");
|
||||
}
|
||||
|
||||
private void HiddenSettingsHotspot_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
@@ -107,6 +109,7 @@ public partial class MainWindow : Window
|
||||
|
||||
if (DataContext is MainWindowViewModel viewModel)
|
||||
{
|
||||
Log.Information("打开隐藏速度设置窗口");
|
||||
var window = new HiddenSpeedSettingsWindow(_plcService, viewModel.GetPlcConnectionConfig())
|
||||
{
|
||||
Owner = this
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
using System.Net.Sockets;
|
||||
using System.IO;
|
||||
using System.Buffers.Binary;
|
||||
using Serilog;
|
||||
|
||||
namespace DentistryHandpieces;
|
||||
|
||||
@@ -37,14 +38,25 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
|
||||
|
||||
public async Task PulseCoilAsync(PlcConnectionConfig config, ushort coilAddress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await WriteSingleCoilAsync(config, coilAddress, true, cancellationToken);
|
||||
await Task.Delay(config.PulseMilliseconds, cancellationToken);
|
||||
await WriteSingleCoilAsync(config, coilAddress, false, cancellationToken);
|
||||
await ExecuteWriteAsync(
|
||||
config,
|
||||
$"M{coilAddress}",
|
||||
$"脉冲 {config.PulseMilliseconds} ms",
|
||||
async () =>
|
||||
{
|
||||
await WriteSingleCoilAsync(config, coilAddress, true, cancellationToken);
|
||||
await Task.Delay(config.PulseMilliseconds, cancellationToken);
|
||||
await WriteSingleCoilAsync(config, coilAddress, false, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task WriteCoilAsync(PlcConnectionConfig config, ushort coilAddress, bool value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await WriteSingleCoilAsync(config, coilAddress, value, cancellationToken);
|
||||
await ExecuteWriteAsync(
|
||||
config,
|
||||
$"M{coilAddress}",
|
||||
value ? 1 : 0,
|
||||
() => WriteSingleCoilAsync(config, coilAddress, value, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<ushort, bool>> ReadCoilValuesAsync(PlcConnectionConfig config, IReadOnlyCollection<ushort> coilAddresses, CancellationToken cancellationToken = default)
|
||||
@@ -118,12 +130,20 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
|
||||
? [highWord, lowWord]
|
||||
: [lowWord, highWord];
|
||||
|
||||
await WriteMultipleRegistersAsync(config, registerAddress, registers, cancellationToken);
|
||||
await ExecuteWriteAsync(
|
||||
config,
|
||||
$"D{registerAddress}",
|
||||
value,
|
||||
() => WriteMultipleRegistersAsync(config, registerAddress, registers, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task WriteUInt16Async(PlcConnectionConfig config, ushort registerAddress, ushort value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await WriteMultipleRegistersAsync(config, registerAddress, [value], cancellationToken);
|
||||
await ExecuteWriteAsync(
|
||||
config,
|
||||
$"D{registerAddress}",
|
||||
value,
|
||||
() => WriteMultipleRegistersAsync(config, registerAddress, [value], cancellationToken));
|
||||
}
|
||||
|
||||
public async Task WriteInt32Async(PlcConnectionConfig config, ushort registerAddress, int value, CancellationToken cancellationToken = default)
|
||||
@@ -136,7 +156,38 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
|
||||
? [highWord, lowWord]
|
||||
: [lowWord, highWord];
|
||||
|
||||
await WriteMultipleRegistersAsync(config, registerAddress, registers, cancellationToken);
|
||||
await ExecuteWriteAsync(
|
||||
config,
|
||||
$"D{registerAddress}",
|
||||
value,
|
||||
() => WriteMultipleRegistersAsync(config, registerAddress, registers, cancellationToken));
|
||||
}
|
||||
|
||||
private static async Task ExecuteWriteAsync(PlcConnectionConfig config, string target, object value, Func<Task> writeAction)
|
||||
{
|
||||
try
|
||||
{
|
||||
await writeAction();
|
||||
Log.Information(
|
||||
"PLC写入成功,地址 {IpAddress}:{Port},站号 {UnitId},目标 {Target},值 {Value}",
|
||||
config.IpAddress,
|
||||
config.Port,
|
||||
config.UnitId,
|
||||
target,
|
||||
value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(
|
||||
ex,
|
||||
"PLC写入失败,地址 {IpAddress}:{Port},站号 {UnitId},目标 {Target},值 {Value}",
|
||||
config.IpAddress,
|
||||
config.Port,
|
||||
config.UnitId,
|
||||
target,
|
||||
value);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteSingleCoilAsync(PlcConnectionConfig config, ushort coilAddress, bool value, CancellationToken cancellationToken)
|
||||
|
||||
@@ -142,36 +142,27 @@ public sealed class TestPointRow : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestRecordRow
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public DateTime CreatedAt { get; init; }
|
||||
|
||||
public string CreatedAtText => CreatedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||
|
||||
public string SampleCode { get; init; } = string.Empty;
|
||||
|
||||
public string OperatorName { get; init; } = string.Empty;
|
||||
|
||||
public string OverallResult { get; init; } = string.Empty;
|
||||
|
||||
public string ExportPath { get; init; } = string.Empty;
|
||||
|
||||
public string ExportPathText => string.IsNullOrWhiteSpace(ExportPath) ? "未导出" : ExportPath;
|
||||
}
|
||||
|
||||
public sealed class AcceptancePayload
|
||||
{
|
||||
public int SchemaVersion { get; init; } = 2;
|
||||
|
||||
public string SessionId { get; init; } = string.Empty;
|
||||
|
||||
public DateTime CreatedAt { get; init; }
|
||||
|
||||
public DateTime LastUpdatedAt { get; init; }
|
||||
|
||||
public string SampleCode { get; init; } = string.Empty;
|
||||
|
||||
public string OperatorName { get; init; } = string.Empty;
|
||||
|
||||
public string OverallResult { get; init; } = string.Empty;
|
||||
|
||||
public TestParameterConfig ParameterSnapshot { get; init; } = TestParameterConfig.CreateDefault();
|
||||
|
||||
public List<ProjectPayload> Projects { get; init; } = [];
|
||||
|
||||
public List<TestRunPayload> Runs { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class ProjectPayload
|
||||
@@ -234,6 +225,82 @@ public sealed class TorqueCurvePayload
|
||||
public List<TorqueSamplePayload> Samples { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class TestRunPayload
|
||||
{
|
||||
public string RunId { get; init; } = string.Empty;
|
||||
|
||||
public string TestType { get; init; } = string.Empty;
|
||||
|
||||
public DateTime StartedAt { get; init; }
|
||||
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
public string CompletionStatus { get; set; } = "测试中";
|
||||
|
||||
public TestParameterConfig ParameterSnapshot { get; init; } = TestParameterConfig.CreateDefault();
|
||||
|
||||
public double? FinalDisplacementMm { get; set; }
|
||||
|
||||
public double? FinalAxialForceN { get; set; }
|
||||
|
||||
public double? FinalSpeedRpm { get; set; }
|
||||
|
||||
public double? FinalTorqueMilliNewtonMeters { get; set; }
|
||||
|
||||
public double? NoLoadSpeedRpm { get; set; }
|
||||
|
||||
public double? NoLoadSpeedErrorRatePercent { get; set; }
|
||||
|
||||
public List<RealtimeSamplePayload> Samples { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class RealtimeSamplePayload
|
||||
{
|
||||
public long Sequence { get; init; }
|
||||
|
||||
public DateTime SampledAt { get; init; }
|
||||
|
||||
public double DialIndicatorMm { get; init; }
|
||||
|
||||
public double RelativeDisplacementMm { get; init; }
|
||||
|
||||
public double AxialAxisPositionMm { get; init; }
|
||||
|
||||
public double AxialSampleStartMm { get; init; }
|
||||
|
||||
public double AxialSampleEndMm { get; init; }
|
||||
|
||||
public double AxialSampleDifferenceMm { get; init; }
|
||||
|
||||
public double AxialForceN { get; init; }
|
||||
|
||||
public double SpeedTorqueAxisPositionMm { get; init; }
|
||||
|
||||
public double SpeedTorqueDisplacementMm { get; init; }
|
||||
|
||||
public double SpeedTorquePeakTorqueMilliNewtonMeters { get; init; }
|
||||
|
||||
public double RealtimeTorqueMilliNewtonMeters { get; init; }
|
||||
|
||||
public double RealtimeSpeedRpm { get; init; }
|
||||
|
||||
public double RealtimePressureMpa { get; init; }
|
||||
|
||||
public double NoLoadSpeedRpm { get; init; }
|
||||
|
||||
public double NoLoadSpeedErrorRatePercent { get; init; }
|
||||
|
||||
public bool SpeedTorqueDone { get; init; }
|
||||
|
||||
public bool SpeedTorqueResetEnabled { get; init; }
|
||||
|
||||
public bool SpeedTorqueResetDone { get; init; }
|
||||
|
||||
public bool AxialResetEnabled { get; init; }
|
||||
|
||||
public bool AxialResetDone { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TestParameterConfig
|
||||
{
|
||||
public double AxialDisplacementLimit { get; init; }
|
||||
@@ -278,6 +345,8 @@ public sealed class TestParameterConfig
|
||||
|
||||
public double PressureCoefficient { get; init; }
|
||||
|
||||
public double NoLoadSpeedSetting { get; init; }
|
||||
|
||||
public string PlcIpAddress { get; init; } = "192.168.1.10";
|
||||
|
||||
public int PlcPort { get; init; } = 502;
|
||||
@@ -328,6 +397,7 @@ public sealed class TestParameterConfig
|
||||
SpeedCoefficient = 1,
|
||||
SpeedStopThreshold = 0,
|
||||
PressureCoefficient = 1,
|
||||
NoLoadSpeedSetting = 0,
|
||||
PlcIpAddress = "192.168.1.10",
|
||||
PlcPort = 502,
|
||||
PlcUnitId = 1,
|
||||
|
||||
Reference in New Issue
Block a user