更新
This commit is contained in:
@@ -615,19 +615,90 @@
|
|||||||
<Border Grid.Column="1" Style="{StaticResource PanelBorderStyle}">
|
<Border Grid.Column="1" Style="{StaticResource PanelBorderStyle}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="1.2*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Grid.Row="0" Style="{StaticResource SectionTitleStyle}" Text="系统事件" />
|
<DockPanel Grid.Row="0" Margin="0,0,0,6">
|
||||||
<DataGrid Grid.Row="1" ItemsSource="{Binding EventLog}">
|
<TextBlock DockPanel.Dock="Left" Style="{StaticResource SectionTitleStyle}" Text="每次摩擦系数数据" />
|
||||||
|
<TextBlock DockPanel.Dock="Right" Style="{StaticResource HintStyle}" Text="{Binding ReciprocatingRecordSummary, Mode=OneWay}" />
|
||||||
|
</DockPanel>
|
||||||
|
<ScrollViewer Grid.Row="1"
|
||||||
|
Margin="0,0,0,10"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsControl ItemsSource="{Binding ReciprocatingRecordSections}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<ItemsControl ItemsSource="{Binding Rows}" Margin="0,0,0,8">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid Height="22">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="58" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
Background="#D6EFC3"
|
||||||
|
BorderBrush="#AEB69D"
|
||||||
|
BorderThickness="1,1,1,0">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="11"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{Binding Header}" />
|
||||||
|
</Border>
|
||||||
|
<ItemsControl Grid.Column="1"
|
||||||
|
ItemsSource="{Binding Cells}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<UniformGrid Rows="1" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border x:Name="HistoryRecordCellBorder"
|
||||||
|
Background="#FFFFFF"
|
||||||
|
BorderBrush="#AEB69D"
|
||||||
|
BorderThickness="0,1,1,0">
|
||||||
|
<TextBlock x:Name="HistoryRecordCellText"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="4,0,6,0"
|
||||||
|
FontSize="11"
|
||||||
|
Text="{Binding Value}" />
|
||||||
|
</Border>
|
||||||
|
<DataTemplate.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsHeader}" Value="True">
|
||||||
|
<Setter TargetName="HistoryRecordCellBorder" Property="Background" Value="#BDE7A2" />
|
||||||
|
<Setter TargetName="HistoryRecordCellText" Property="HorizontalAlignment" Value="Center" />
|
||||||
|
<Setter TargetName="HistoryRecordCellText" Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter TargetName="HistoryRecordCellText" Property="Margin" Value="0" />
|
||||||
|
</DataTrigger>
|
||||||
|
</DataTemplate.Triggers>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Style="{StaticResource SectionTitleStyle}" Text="系统事件" />
|
||||||
|
<DataGrid Grid.Row="3" ItemsSource="{Binding EventLog}">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="时间" Binding="{Binding TimestampLabel}" Width="1.1*" />
|
<DataGridTextColumn Header="时间" Binding="{Binding TimestampLabel}" Width="1.1*" />
|
||||||
<DataGridTextColumn Header="级别" Binding="{Binding LevelLabel}" Width="0.7*" />
|
<DataGridTextColumn Header="级别" Binding="{Binding LevelLabel}" Width="0.7*" />
|
||||||
<DataGridTextColumn Header="内容" Binding="{Binding Message}" Width="2.5*" />
|
<DataGridTextColumn Header="内容" Binding="{Binding Message}" Width="2.5*" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
<Border Grid.Row="2" Style="{StaticResource InsetCardStyle}">
|
<Border Grid.Row="4" Style="{StaticResource InsetCardStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock FontWeight="SemiBold" TextWrapping="Wrap" Text="{Binding DeviceConnectionSummary, Mode=OneWay}" />
|
<TextBlock FontWeight="SemiBold" TextWrapping="Wrap" Text="{Binding DeviceConnectionSummary, Mode=OneWay}" />
|
||||||
<TextBlock Margin="0,6,0,0" Style="{StaticResource HintStyle}" Text="{Binding PlcCommandSummary, Mode=OneWay}" />
|
<TextBlock Margin="0,6,0,0" Style="{StaticResource HintStyle}" Text="{Binding PlcCommandSummary, Mode=OneWay}" />
|
||||||
|
|||||||
@@ -259,8 +259,18 @@ public sealed class TestDataRepository
|
|||||||
using var command = connection.CreateCommand();
|
using var command = connection.CreateCommand();
|
||||||
command.CommandText =
|
command.CommandText =
|
||||||
"""
|
"""
|
||||||
SELECT run_id, run_index, completed_at, batch_number, test_mode, static_coefficient,
|
SELECT run_id, run_index, completed_at, batch_number, test_mode,
|
||||||
static_coefficient_1, static_coefficient_2, kinetic_coefficient,
|
COALESCE(NULLIF(static_coefficient, 0), (
|
||||||
|
SELECT AVG(static_coefficient)
|
||||||
|
FROM reciprocating_records
|
||||||
|
WHERE run_id = test_runs.run_id AND static_coefficient IS NOT NULL
|
||||||
|
), static_coefficient) AS static_coefficient,
|
||||||
|
static_coefficient_1, static_coefficient_2,
|
||||||
|
COALESCE(NULLIF(kinetic_coefficient, 0), (
|
||||||
|
SELECT AVG(kinetic_coefficient)
|
||||||
|
FROM reciprocating_records
|
||||||
|
WHERE run_id = test_runs.run_id AND kinetic_coefficient IS NOT NULL
|
||||||
|
), kinetic_coefficient) AS kinetic_coefficient,
|
||||||
kinetic_coefficient_1, kinetic_coefficient_2, standard_deviation,
|
kinetic_coefficient_1, kinetic_coefficient_2, standard_deviation,
|
||||||
standard_deviation_1, standard_deviation_2, peak_force_n, average_force_n,
|
standard_deviation_1, standard_deviation_2, peak_force_n, average_force_n,
|
||||||
judgement, csv_export_path, report_export_path, sample_count
|
judgement, csv_export_path, report_export_path, sample_count
|
||||||
@@ -291,8 +301,20 @@ public sealed class TestDataRepository
|
|||||||
SELECT run_id, run_index, completed_at, batch_number, product_code, test_mode,
|
SELECT run_id, run_index, completed_at, batch_number, product_code, test_mode,
|
||||||
counterface_material, direction, sled_mass_grams, lift_speed_mm_per_min,
|
counterface_material, direction, sled_mass_grams, lift_speed_mm_per_min,
|
||||||
lift_displacement_mm, speed_mm_per_min, travel_mm, replicate_count,
|
lift_displacement_mm, speed_mm_per_min, travel_mm, replicate_count,
|
||||||
reciprocating_count, specimen_description, static_coefficient, static_coefficient_1,
|
reciprocating_count, specimen_description,
|
||||||
static_coefficient_2, kinetic_coefficient, kinetic_coefficient_1,
|
COALESCE(NULLIF(static_coefficient, 0), (
|
||||||
|
SELECT AVG(static_coefficient)
|
||||||
|
FROM reciprocating_records
|
||||||
|
WHERE run_id = test_runs.run_id AND static_coefficient IS NOT NULL
|
||||||
|
), static_coefficient) AS static_coefficient,
|
||||||
|
static_coefficient_1,
|
||||||
|
static_coefficient_2,
|
||||||
|
COALESCE(NULLIF(kinetic_coefficient, 0), (
|
||||||
|
SELECT AVG(kinetic_coefficient)
|
||||||
|
FROM reciprocating_records
|
||||||
|
WHERE run_id = test_runs.run_id AND kinetic_coefficient IS NOT NULL
|
||||||
|
), kinetic_coefficient) AS kinetic_coefficient,
|
||||||
|
kinetic_coefficient_1,
|
||||||
kinetic_coefficient_2, standard_deviation, standard_deviation_1, standard_deviation_2,
|
kinetic_coefficient_2, standard_deviation, standard_deviation_1, standard_deviation_2,
|
||||||
peak_force_n, average_force_n, judgement, csv_export_path,
|
peak_force_n, average_force_n, judgement, csv_export_path,
|
||||||
report_export_path, sample_count
|
report_export_path, sample_count
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using LiveChartsCore;
|
using LiveChartsCore;
|
||||||
using LiveChartsCore.Drawing;
|
using LiveChartsCore.Drawing;
|
||||||
@@ -34,6 +35,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
|
|
||||||
private const string EventLevelInfo = "INFO";
|
private const string EventLevelInfo = "INFO";
|
||||||
private const string EventLevelWarn = "WARN";
|
private const string EventLevelWarn = "WARN";
|
||||||
|
private static readonly Regex PlcAddressPattern = new(@"\b[DM]\d+(?:=[^\s,。:;]*)?", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
private const string ModbusTcpHost = "192.168.1.10";
|
private const string ModbusTcpHost = "192.168.1.10";
|
||||||
private const int ModbusTcpPort = 502;
|
private const int ModbusTcpPort = 502;
|
||||||
private const byte ModbusSlaveAddress = 1;
|
private const byte ModbusSlaveAddress = 1;
|
||||||
@@ -1025,6 +1027,19 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
|
|
||||||
private void FinalizeRun()
|
private void FinalizeRun()
|
||||||
{
|
{
|
||||||
|
var finalStaticCoefficient = ResolveCompletedMeasurementValue(
|
||||||
|
CurrentStaticCoefficient,
|
||||||
|
ReciprocatingRecords.Select(record => record.StaticCoefficient));
|
||||||
|
var finalKineticCoefficient = ResolveCompletedMeasurementValue(
|
||||||
|
CurrentKineticCoefficient,
|
||||||
|
ReciprocatingRecords.Select(record => record.KineticCoefficient));
|
||||||
|
var finalStandardDeviation = ResolveCompletedMeasurementValue(
|
||||||
|
ResolveRepresentativeValue(GetActiveReplicateCount(), StandardDeviation1, StandardDeviation2),
|
||||||
|
BuildStandardDeviationFallback(ReciprocatingRecords.Select(record => record.StaticCoefficient)));
|
||||||
|
|
||||||
|
CurrentStaticCoefficient = finalStaticCoefficient;
|
||||||
|
CurrentKineticCoefficient = finalKineticCoefficient;
|
||||||
|
|
||||||
var record = new RunRecord
|
var record = new RunRecord
|
||||||
{
|
{
|
||||||
RunId = _activeRunId,
|
RunId = _activeRunId,
|
||||||
@@ -1032,13 +1047,13 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
CompletedAt = DateTime.Now,
|
CompletedAt = DateTime.Now,
|
||||||
BatchNumber = Recipe.BatchNumber,
|
BatchNumber = Recipe.BatchNumber,
|
||||||
TestMode = Recipe.TestMode,
|
TestMode = Recipe.TestMode,
|
||||||
StaticCoefficient = CurrentStaticCoefficient,
|
StaticCoefficient = finalStaticCoefficient,
|
||||||
StaticCoefficient1 = StaticCoefficient1,
|
StaticCoefficient1 = StaticCoefficient1,
|
||||||
StaticCoefficient2 = StaticCoefficient2,
|
StaticCoefficient2 = StaticCoefficient2,
|
||||||
KineticCoefficient = CurrentKineticCoefficient,
|
KineticCoefficient = finalKineticCoefficient,
|
||||||
KineticCoefficient1 = KineticCoefficient1,
|
KineticCoefficient1 = KineticCoefficient1,
|
||||||
KineticCoefficient2 = KineticCoefficient2,
|
KineticCoefficient2 = KineticCoefficient2,
|
||||||
StandardDeviation = ResolveRepresentativeValue(GetActiveReplicateCount(), StandardDeviation1, StandardDeviation2),
|
StandardDeviation = finalStandardDeviation,
|
||||||
StandardDeviation1 = StandardDeviation1,
|
StandardDeviation1 = StandardDeviation1,
|
||||||
StandardDeviation2 = StandardDeviation2,
|
StandardDeviation2 = StandardDeviation2,
|
||||||
PeakForceN = CurrentPeakForceN,
|
PeakForceN = CurrentPeakForceN,
|
||||||
@@ -1054,11 +1069,11 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
_lastCompletedReciprocatingRecords = CloneReciprocatingRecords(ReciprocatingRecords);
|
_lastCompletedReciprocatingRecords = CloneReciprocatingRecords(ReciprocatingRecords);
|
||||||
PersistLastCompletedRun();
|
PersistLastCompletedRun();
|
||||||
ResetActiveRunContext();
|
ResetActiveRunContext();
|
||||||
|
_machineRuntimeState = MachineRuntimeState.Idle;
|
||||||
SelectedRunRecord = record;
|
SelectedRunRecord = record;
|
||||||
NextRunIndex++;
|
NextRunIndex++;
|
||||||
UpdateDerivedValues();
|
UpdateDerivedValues();
|
||||||
|
|
||||||
_machineRuntimeState = MachineRuntimeState.Idle;
|
|
||||||
MachineState = "待机";
|
MachineState = "待机";
|
||||||
StateBrush = BrushFromHex("#6C8E78");
|
StateBrush = BrushFromHex("#6C8E78");
|
||||||
InterlockMessage = $"试验完成。COFs={record.StaticCoefficient:F3}, COFk={record.KineticCoefficient:F3}";
|
InterlockMessage = $"试验完成。COFs={record.StaticCoefficient:F3}, COFk={record.KineticCoefficient:F3}";
|
||||||
@@ -1243,6 +1258,41 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
return IsFinite(selectedValue) ? selectedValue : 0;
|
return IsFinite(selectedValue) ? selectedValue : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double ResolveCompletedMeasurementValue(double value, IEnumerable<double?> fallbackValues)
|
||||||
|
{
|
||||||
|
if (IsUsableMeasurementValue(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = fallbackValues
|
||||||
|
.Where(value => value is { } number && IsUsableMeasurementValue(number))
|
||||||
|
.Select(value => value!.Value)
|
||||||
|
.ToArray();
|
||||||
|
return values.Length == 0 ? 0 : values.Average();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<double?> BuildStandardDeviationFallback(IEnumerable<double?> values)
|
||||||
|
{
|
||||||
|
var samples = values
|
||||||
|
.Where(value => value is { } number && IsUsableMeasurementValue(number))
|
||||||
|
.Select(value => value!.Value)
|
||||||
|
.ToArray();
|
||||||
|
if (samples.Length <= 1)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mean = samples.Average();
|
||||||
|
var variance = samples.Sum(value => Math.Pow(value - mean, 2)) / samples.Length;
|
||||||
|
yield return Math.Sqrt(variance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUsableMeasurementValue(double value)
|
||||||
|
{
|
||||||
|
return IsFinite(value) && Math.Abs(value) > 0.0000001;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateCurrentPoint(double x, double y)
|
private void UpdateCurrentPoint(double x, double y)
|
||||||
{
|
{
|
||||||
var sample = new ObservablePoint(x, y);
|
var sample = new ObservablePoint(x, y);
|
||||||
@@ -1530,7 +1580,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
_nextReciprocatingRecordIndex++;
|
_nextReciprocatingRecordIndex++;
|
||||||
_lastReciprocatingCycleCompleteSignal = true;
|
_lastReciprocatingCycleCompleteSignal = true;
|
||||||
_reciprocatingRecordReadFailureLogged = false;
|
_reciprocatingRecordReadFailureLogged = false;
|
||||||
AddInfoEvent($"已记录第 {recordIndex} 次往复数据: COFs={record.StaticCoefficientLabel}, COFk={record.KineticCoefficientLabel}, Fs={record.StaticForceLabel}N, Fk={record.KineticForceLabel}N");
|
AddInfoEvent($"已记录第 {recordIndex} 次往复结果。");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -1986,8 +2036,12 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
StaticCoefficient2 = data.Run.StaticCoefficient2;
|
StaticCoefficient2 = data.Run.StaticCoefficient2;
|
||||||
KineticCoefficient2 = data.Run.KineticCoefficient2;
|
KineticCoefficient2 = data.Run.KineticCoefficient2;
|
||||||
StandardDeviation2 = data.Run.StandardDeviation2;
|
StandardDeviation2 = data.Run.StandardDeviation2;
|
||||||
CurrentStaticCoefficient = ResolveRepresentativeValue(data.Recipe.ReplicateCount, StaticCoefficient1, StaticCoefficient2);
|
CurrentStaticCoefficient = ResolveCompletedMeasurementValue(
|
||||||
CurrentKineticCoefficient = ResolveRepresentativeValue(data.Recipe.ReplicateCount, KineticCoefficient1, KineticCoefficient2);
|
data.Run.StaticCoefficient,
|
||||||
|
data.ReciprocatingRecords.Select(record => record.StaticCoefficient));
|
||||||
|
CurrentKineticCoefficient = ResolveCompletedMeasurementValue(
|
||||||
|
data.Run.KineticCoefficient,
|
||||||
|
data.ReciprocatingRecords.Select(record => record.KineticCoefficient));
|
||||||
TrialProgressPercent = Math.Min(100, CurrentDisplacementMm / Math.Max(data.Recipe.TravelMm, 1) * 100);
|
TrialProgressPercent = Math.Min(100, CurrentDisplacementMm / Math.Max(data.Recipe.TravelMm, 1) * 100);
|
||||||
|
|
||||||
UpdateReferenceLines();
|
UpdateReferenceLines();
|
||||||
@@ -2583,11 +2637,17 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
|
|
||||||
private void AddEvent(string level, string message)
|
private void AddEvent(string level, string message)
|
||||||
{
|
{
|
||||||
|
var displayMessage = SanitizeSystemEventMessage(message);
|
||||||
|
if (string.IsNullOrWhiteSpace(displayMessage))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
EventLog.Insert(0, new SystemEvent
|
EventLog.Insert(0, new SystemEvent
|
||||||
{
|
{
|
||||||
Timestamp = DateTime.Now,
|
Timestamp = DateTime.Now,
|
||||||
Level = level,
|
Level = level,
|
||||||
Message = message
|
Message = displayMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
while (EventLog.Count > 20)
|
while (EventLog.Count > 20)
|
||||||
@@ -2596,6 +2656,17 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SanitizeSystemEventMessage(string message)
|
||||||
|
{
|
||||||
|
var sanitized = PlcAddressPattern.Replace(message, string.Empty);
|
||||||
|
sanitized = sanitized
|
||||||
|
.Replace(" ", " ", StringComparison.Ordinal)
|
||||||
|
.Replace(" : ", ": ", StringComparison.Ordinal)
|
||||||
|
.Replace(",。", "。", StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
return sanitized.TrimEnd(':', ':');
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<string> ValidateRecipe()
|
private IEnumerable<string> ValidateRecipe()
|
||||||
{
|
{
|
||||||
if (!_deviceConnectionService.IsConnected)
|
if (!_deviceConnectionService.IsConnected)
|
||||||
|
|||||||
Reference in New Issue
Block a user