更新20260606

This commit is contained in:
GukSang.Jin
2026-06-06 17:10:23 +08:00
parent 4c6a403a5b
commit c76838c24c
8 changed files with 1307 additions and 298 deletions

View File

@@ -1,14 +1,96 @@
using System.Configuration; using System.IO;
using System.Data;
using System.Windows; using System.Windows;
using System.Windows.Threading;
using Serilog;
using Serilog.Events;
namespace DentistryHandpieces;
namespace DentistryHandpieces
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application 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();
}
} }

View File

@@ -11,8 +11,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" /> <PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <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="NModbusAsync" Version="3.0.2" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -13,8 +13,8 @@ public sealed class WpfFileDialogService : IFileDialogService
{ {
var dialog = new SaveFileDialog var dialog = new SaveFileDialog
{ {
Title = "导出 Excel", Title = "导出报表",
FileName = $"牙科手机验收记录_{createdAt:yyyyMMddHHmmss}.xlsx", FileName = $"牙科手机测试报表_{createdAt:yyyyMMddHHmmss}.xlsx",
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx", Filter = "Excel 工作簿 (*.xlsx)|*.xlsx",
DefaultExt = ".xlsx", DefaultExt = ".xlsx",
AddExtension = true, AddExtension = true,

View File

@@ -301,10 +301,60 @@
<Setter Property="CornerRadius" Value="5" /> <Setter Property="CornerRadius" Value="5" />
<Setter Property="Padding" Value="12,9" /> <Setter Property="Padding" Value="12,9" />
</Style> </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> </Window.Resources>
<Grid> <Grid>
<DockPanel LastChildFill="True"> <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"> <TabControl x:Name="MainTabs">
<TabItem Header="轴向位移动量测试"> <TabItem Header="轴向位移动量测试">
<Grid> <Grid>
@@ -1168,56 +1218,6 @@
</Grid> </Grid>
</TabItem> </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> </TabControl>
</DockPanel> </DockPanel>

View File

@@ -2,6 +2,7 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading; using System.Windows.Threading;
using Serilog;
namespace DentistryHandpieces; namespace DentistryHandpieces;
@@ -24,6 +25,7 @@ public partial class MainWindow : Window
}; };
_hiddenSettingsPressTimer.Tick += HiddenSettingsPressTimer_Tick; _hiddenSettingsPressTimer.Tick += HiddenSettingsPressTimer_Tick;
Deactivated += (_, _) => ReleaseAllManualMotionTargetsAsync(); Deactivated += (_, _) => ReleaseAllManualMotionTargetsAsync();
Log.Information("主窗口创建完成");
} }
private void HiddenSettingsHotspot_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void HiddenSettingsHotspot_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@@ -107,6 +109,7 @@ public partial class MainWindow : Window
if (DataContext is MainWindowViewModel viewModel) if (DataContext is MainWindowViewModel viewModel)
{ {
Log.Information("打开隐藏速度设置窗口");
var window = new HiddenSpeedSettingsWindow(_plcService, viewModel.GetPlcConnectionConfig()) var window = new HiddenSpeedSettingsWindow(_plcService, viewModel.GetPlcConnectionConfig())
{ {
Owner = this Owner = this

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
using System.Net.Sockets; using System.Net.Sockets;
using System.IO; using System.IO;
using System.Buffers.Binary; using System.Buffers.Binary;
using Serilog;
namespace DentistryHandpieces; namespace DentistryHandpieces;
@@ -36,15 +37,26 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
private ushort _transactionId; private ushort _transactionId;
public async Task PulseCoilAsync(PlcConnectionConfig config, ushort coilAddress, CancellationToken cancellationToken = default) public async Task PulseCoilAsync(PlcConnectionConfig config, ushort coilAddress, CancellationToken cancellationToken = default)
{
await ExecuteWriteAsync(
config,
$"M{coilAddress}",
$"脉冲 {config.PulseMilliseconds} ms",
async () =>
{ {
await WriteSingleCoilAsync(config, coilAddress, true, cancellationToken); await WriteSingleCoilAsync(config, coilAddress, true, cancellationToken);
await Task.Delay(config.PulseMilliseconds, cancellationToken); await Task.Delay(config.PulseMilliseconds, cancellationToken);
await WriteSingleCoilAsync(config, coilAddress, false, cancellationToken); await WriteSingleCoilAsync(config, coilAddress, false, cancellationToken);
});
} }
public async Task WriteCoilAsync(PlcConnectionConfig config, ushort coilAddress, bool value, CancellationToken cancellationToken = default) 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) 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] ? [highWord, lowWord]
: [lowWord, highWord]; : [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) 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) 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] ? [highWord, lowWord]
: [lowWord, highWord]; : [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) private async Task WriteSingleCoilAsync(PlcConnectionConfig config, ushort coilAddress, bool value, CancellationToken cancellationToken)

View File

@@ -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 sealed class AcceptancePayload
{ {
public int SchemaVersion { get; init; } = 2;
public string SessionId { get; init; } = string.Empty;
public DateTime CreatedAt { get; init; } public DateTime CreatedAt { get; init; }
public DateTime LastUpdatedAt { get; init; }
public string SampleCode { get; init; } = string.Empty; public string SampleCode { get; init; } = string.Empty;
public string OperatorName { get; init; } = string.Empty; public string OperatorName { get; init; } = string.Empty;
public string OverallResult { 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<ProjectPayload> Projects { get; init; } = [];
public List<TestRunPayload> Runs { get; init; } = [];
} }
public sealed class ProjectPayload public sealed class ProjectPayload
@@ -234,6 +225,82 @@ public sealed class TorqueCurvePayload
public List<TorqueSamplePayload> Samples { get; init; } = []; 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 sealed class TestParameterConfig
{ {
public double AxialDisplacementLimit { get; init; } public double AxialDisplacementLimit { get; init; }
@@ -278,6 +345,8 @@ public sealed class TestParameterConfig
public double PressureCoefficient { get; init; } public double PressureCoefficient { get; init; }
public double NoLoadSpeedSetting { get; init; }
public string PlcIpAddress { get; init; } = "192.168.1.10"; public string PlcIpAddress { get; init; } = "192.168.1.10";
public int PlcPort { get; init; } = 502; public int PlcPort { get; init; } = 502;
@@ -328,6 +397,7 @@ public sealed class TestParameterConfig
SpeedCoefficient = 1, SpeedCoefficient = 1,
SpeedStopThreshold = 0, SpeedStopThreshold = 0,
PressureCoefficient = 1, PressureCoefficient = 1,
NoLoadSpeedSetting = 0,
PlcIpAddress = "192.168.1.10", PlcIpAddress = "192.168.1.10",
PlcPort = 502, PlcPort = 502,
PlcUnitId = 1, PlcUnitId = 1,