更新122

This commit is contained in:
GukSang.Jin
2026-06-15 14:27:52 +08:00
parent 3eadf6c075
commit ffdaf36dd9
3 changed files with 274 additions and 72 deletions

View File

@@ -49,6 +49,7 @@ public sealed class MainWindowViewModel : ObservableObject
private const ushort SpeedTorqueEnabledCoil = 81;
private const ushort SpeedTorqueDoneCoil = 82;
private const ushort SpeedTorqueStopCoil = 83;
private const ushort SpeedTorqueStableCoil = 87;
private const ushort SpeedTorqueResetCoil = 90;
private const ushort SpeedTorqueResetEnabledCoil = 91;
private const ushort SpeedTorqueResetDoneCoil = 92;
@@ -155,6 +156,7 @@ public sealed class MainWindowViewModel : ObservableObject
AxialForceModeCoil,
AxialDoneCoil,
SpeedTorqueDoneCoil,
SpeedTorqueStableCoil,
SpeedTorqueResetEnabledCoil,
SpeedTorqueResetDoneCoil,
AxialResetEnabledCoil,
@@ -787,7 +789,11 @@ public sealed class MainWindowViewModel : ObservableObject
_realtimeTorque = realtimeTorque;
_realtimeSpeed = realtimeSpeed;
DateTime sampledAt = DateTime.Now;
AppendTorqueSample(GetScaledTorque(), _realtimeSpeed, sampledAt);
AppendTorqueSample(
GetScaledTorque(),
_realtimeSpeed,
ReadCoilValue(coilValues, SpeedTorqueStableCoil),
sampledAt);
if (_isDisplacementRunning)
{
_maxDisplacement = Math.Max(_maxDisplacement, Math.Abs(_relativeDisplacement));
@@ -1330,6 +1336,7 @@ public sealed class MainWindowViewModel : ObservableObject
private ProjectPayload CreateSpeedTorqueRealtimeProject()
{
TestRunPayload? run = GetLatestCompletedRun("转速/扭矩测试");
bool hasRun = run is not null;
TestParameterConfig parameters = run?.ParameterSnapshot ?? _parameterConfig;
RealtimeSamplePayload? lastSample = run?.Samples.LastOrDefault();
TorqueCurvePayload curve = run is null ? CreateTorqueCurvePayload() : CreateTorqueCurvePayload(run);
@@ -1337,8 +1344,8 @@ public sealed class MainWindowViewModel : ObservableObject
? run.Samples.Max(static sample => Math.Abs(sample.SpeedTorqueDisplacementMm))
: _maxSpeedTorqueDisplacement;
double peakTorque = run?.Samples.Count > 0
? run.Samples.Max(static sample => sample.SpeedTorquePeakTorqueMilliNewtonMeters)
: _speedTorquePeakTorque;
? run.Samples.Max(static sample => sample.RealtimeTorqueMilliNewtonMeters)
: GetScaledTorque();
double? finalDisplacement = run?.FinalDisplacementMm ?? _finalSpeedTorqueDisplacement;
double? finalSpeed = run?.FinalSpeedRpm ?? _finalSpeed;
double? finalTorque = run?.FinalTorqueMilliNewtonMeters ?? _finalTorque;
@@ -1346,7 +1353,7 @@ public sealed class MainWindowViewModel : ObservableObject
{
Name = "转速/扭矩实时测试",
Requirement = "实时记录转速和扭矩;预留设备采集,不参与合格判定",
Result = "记录",
Result = hasRun ? "记录" : "未测试",
TorqueCurve = curve,
Points =
[
@@ -1360,14 +1367,14 @@ public sealed class MainWindowViewModel : ObservableObject
CreateRecordPoint("转速/扭矩实时测试", "转速系数", FormatConfigNumber(parameters.SpeedCoefficient), string.Empty),
CreateRecordPoint("转速/扭矩实时测试", "低速停止设置", $"{FormatSpeed(parameters.SpeedStopThreshold)} r/min", "r/min"),
CreateRecordPoint("转速/扭矩实时测试", "压力系数", FormatConfigNumber(parameters.PressureCoefficient), string.Empty),
CreateRecordPoint("转速/扭矩实时测试", "末次采样转速", $"{FormatSpeed(lastSample?.RealtimeSpeedRpm ?? _realtimeSpeed)} r/min", "r/min"),
CreateRecordPoint("转速/扭矩实时测试", "末次采样扭矩", $"{FormatTorque(lastSample?.RealtimeTorqueMilliNewtonMeters ?? GetScaledTorque())} {TorqueUnit}", TorqueUnit),
CreateRecordPoint("转速/扭矩实时测试", "末次采样压力", $"{FormatPressure(lastSample?.RealtimePressureKpa ?? _realtimePressure)} kPa", "kPa"),
CreateRecordPoint("转速/扭矩实时测试", "最大扭矩采集", $"{FormatTorque(peakTorque)} {TorqueUnit}", TorqueUnit),
CreateRecordPoint("转速/扭矩实时测试", "最大位移", $"{FormatDisplacement(maxDisplacement)} mm", "mm"),
CreateRecordPoint("转速/扭矩实时测试", "最终位移", finalDisplacement.HasValue ? $"{FormatDisplacement(finalDisplacement.Value)} mm" : "--", "mm", finalDisplacement.HasValue ? "记录" : "待停止"),
CreateRecordPoint("转速/扭矩实时测试", "最终转速", finalSpeed.HasValue ? $"{FormatSpeed(finalSpeed.Value)} r/min" : "--", "r/min", finalSpeed.HasValue ? "记录" : "待停止"),
CreateRecordPoint("转速/扭矩实时测试", "最终扭矩", finalTorque.HasValue ? $"{FormatTorque(finalTorque.Value)} {TorqueUnit}" : "--", TorqueUnit, finalTorque.HasValue ? "记录" : "待停止"),
CreateRecordPoint("转速/扭矩实时测试", "末次采样转速", hasRun ? $"{FormatSpeed(lastSample?.RealtimeSpeedRpm ?? _realtimeSpeed)} r/min" : "--", "r/min", hasRun ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "末次采样扭矩", hasRun ? $"{FormatTorque(lastSample?.RealtimeTorqueMilliNewtonMeters ?? GetScaledTorque())} {TorqueUnit}" : "--", TorqueUnit, hasRun ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "末次采样压力", hasRun ? $"{FormatPressure(lastSample?.RealtimePressureKpa ?? _realtimePressure)} kPa" : "--", "kPa", hasRun ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "实时采样最大扭矩", hasRun ? $"{FormatTorque(peakTorque)} {TorqueUnit}" : "--", TorqueUnit, hasRun ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "最大位移", hasRun ? $"{FormatDisplacement(maxDisplacement)} mm" : "--", "mm", hasRun ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "最终位移", hasRun && finalDisplacement.HasValue ? $"{FormatDisplacement(finalDisplacement.Value)} mm" : "--", "mm", hasRun && finalDisplacement.HasValue ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "最终转速", hasRun && finalSpeed.HasValue ? $"{FormatSpeed(finalSpeed.Value)} r/min" : "--", "r/min", hasRun && finalSpeed.HasValue ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "最终扭矩", hasRun && finalTorque.HasValue ? $"{FormatTorque(finalTorque.Value)} {TorqueUnit}" : "--", TorqueUnit, hasRun && finalTorque.HasValue ? "记录" : "未测试"),
CreateRecordPoint("转速/扭矩实时测试", "转速/扭矩保持时间关系曲线判定", curve.Result, string.Empty, "记录"),
CreateRecordPoint("转速/扭矩实时测试", "扭矩变化阈值", $"{FormatTorque(curve.ChangeThresholdMilliNewtonMeters)} {TorqueUnit}", TorqueUnit),
CreateRecordPoint("转速/扭矩实时测试", "转速变化阈值", $"{FormatSpeed(curve.SpeedChangeThresholdRpm)} r/min", "r/min"),
@@ -1392,7 +1399,7 @@ public sealed class MainWindowViewModel : ObservableObject
{
Name = "空载转速测试",
Requirement = "记录 PLC 空载转速及转速误差率",
Result = run is null ? "待记录" : "记录",
Result = run is null ? "未测试" : "记录",
Points =
[
CreateRecordPoint("空载转速测试", "空载转速设置", $"{FormatSpeedSetting(parameters.NoLoadSpeedSetting)} r/min", "r/min"),
@@ -1405,6 +1412,7 @@ public sealed class MainWindowViewModel : ObservableObject
private ProjectPayload CreateDisplacementProject()
{
TestRunPayload? run = GetLatestCompletedRun("轴向位移动量测试");
bool hasRun = run is not null;
TestParameterConfig parameters = run?.ParameterSnapshot ?? _parameterConfig;
RealtimeSamplePayload? lastSample = run?.Samples.LastOrDefault();
double maxDisplacement = run?.Samples.Count > 0
@@ -1416,16 +1424,16 @@ public sealed class MainWindowViewModel : ObservableObject
{
Name = "轴向位移动量测试",
Requirement = "主轴位移动量测试;第一版记录实时千分表相对位移,不参与合格判定",
Result = "记录",
Result = hasRun ? "记录" : "未测试",
Points =
[
CreateRecordPoint("轴向位移动量测试", "零点读数", $"{FormatDisplacement((lastSample?.DialIndicatorMm ?? _dialZero + _relativeDisplacement) - (lastSample?.RelativeDisplacementMm ?? _relativeDisplacement))} mm", "mm"),
CreateRecordPoint("轴向位移动量测试", "2号当前位置", $"{FormatDisplacement(lastSample?.AxialAxisPositionMm ?? _axialAxisPosition)} mm", "mm"),
CreateRecordPoint("轴向位移动量测试", "采集数据1-1", $"{FormatDisplacement(lastSample?.AxialSampleStartMm ?? _axialSampleStart)} mm", "mm"),
CreateRecordPoint("轴向位移动量测试", "采集数据1-2", $"{FormatDisplacement(lastSample?.AxialSampleEndMm ?? _axialSampleEnd)} mm", "mm"),
CreateRecordPoint("轴向位移动量测试", "数据差值1", $"{FormatDisplacement(lastSample?.AxialSampleDifferenceMm ?? _axialSampleDifference)} mm", "mm"),
CreateRecordPoint("轴向位移动量测试", "最大位移", $"{FormatDisplacement(maxDisplacement)} mm", "mm"),
CreateRecordPoint("轴向位移动量测试", "最终位移", finalDisplacement.HasValue ? $"{FormatDisplacement(finalDisplacement.Value)} mm" : "--", "mm", finalDisplacement.HasValue ? "记录" : "待停止"),
CreateRecordPoint("轴向位移动量测试", "零点读数", hasRun ? $"{FormatDisplacement((lastSample?.DialIndicatorMm ?? _dialZero + _relativeDisplacement) - (lastSample?.RelativeDisplacementMm ?? _relativeDisplacement))} mm" : "--", "mm", hasRun ? "记录" : "未测试"),
CreateRecordPoint("轴向位移动量测试", "轴向位移当前位置", hasRun ? $"{FormatDisplacement(lastSample?.AxialAxisPositionMm ?? _axialAxisPosition)} mm" : "--", "mm", hasRun ? "记录" : "未测试"),
CreateRecordPoint("轴向位移动量测试", "采集数据1-1", hasRun ? $"{FormatDisplacement(lastSample?.AxialSampleStartMm ?? _axialSampleStart)} mm" : "--", "mm", hasRun ? "记录" : "未测试"),
CreateRecordPoint("轴向位移动量测试", "采集数据1-2", hasRun ? $"{FormatDisplacement(lastSample?.AxialSampleEndMm ?? _axialSampleEnd)} mm" : "--", "mm", hasRun ? "记录" : "未测试"),
CreateRecordPoint("轴向位移动量测试", "数据差值1", hasRun ? $"{FormatDisplacement(lastSample?.AxialSampleDifferenceMm ?? _axialSampleDifference)} mm" : "--", "mm", hasRun ? "记录" : "未测试"),
CreateRecordPoint("轴向位移动量测试", "最大位移", hasRun ? $"{FormatDisplacement(maxDisplacement)} mm" : "--", "mm", hasRun ? "记录" : "未测试"),
CreateRecordPoint("轴向位移动量测试", "最终位移", hasRun && finalDisplacement.HasValue ? $"{FormatDisplacement(finalDisplacement.Value)} mm" : "--", "mm", hasRun && finalDisplacement.HasValue ? "记录" : "未测试"),
CreateRecordPoint("轴向位移动量测试", "位移极限", $"{FormatDisplacement(parameters.AxialDisplacementLimit)} mm", "mm"),
CreateRecordPoint("轴向位移动量测试", "手/自动速度", $"{FormatSpeedSetting(parameters.AxialSpeed)} mm/min", "mm/min"),
CreateRecordPoint("轴向位移动量测试", "手动位移", $"{FormatDisplacement(parameters.AxialManualDisplacement)} mm", "mm"),
@@ -1436,7 +1444,7 @@ public sealed class MainWindowViewModel : ObservableObject
CreateRecordPoint("轴向位移动量测试", "轴向拉力设置", $"{FormatForce(parameters.AxialForceSetpoint)} N", "N", parameters.UseAxialPullForceSetpoint ? "当前" : "备用"),
CreateRecordPoint("轴向位移动量测试", "轴向力保护", $"{FormatForce(parameters.AxialForceProtection)} N", "N"),
CreateRecordPoint("轴向位移动量测试", "轴向力保持时间设置", $"{FormatConfigNumber(parameters.AxialForceHoldTime)} s", "s"),
CreateRecordPoint("轴向位移动量测试", "最终轴向力", finalAxialForce.HasValue ? $"{FormatForce(finalAxialForce.Value)} N" : "--", "N", finalAxialForce.HasValue ? "记录" : "待停止")
CreateRecordPoint("轴向位移动量测试", "最终轴向力", hasRun && finalAxialForce.HasValue ? $"{FormatForce(finalAxialForce.Value)} N" : "--", "N", hasRun && finalAxialForce.HasValue ? "记录" : "未测试")
]
};
}
@@ -1556,13 +1564,14 @@ public sealed class MainWindowViewModel : ObservableObject
{
var sheet = workbook.Worksheets.Add("测试运行记录");
sheet.Cell(1, 1).Value = "测试运行记录";
sheet.Range(1, 1, 1, 14).Merge().Style.Font.SetBold().Font.SetFontSize(16);
sheet.Range(1, 1, 1, 18).Merge().Style.Font.SetBold().Font.SetFontSize(16);
string[] headers =
[
"运行编号", "测试类型", "开始时间", "完成时间", "完成状态", "采样数",
"最终位移(mm)", "最终轴向力(N)", "最终转速(r/min)", $"最终扭矩({TorqueUnit})",
"空载转速(r/min)", "转速误差率(%)", "参数快照时间", "数据来源"
"空载转速(r/min)", "转速误差率(%)", "参数快照时间", "数据来源",
"保持段采样数", "平均采样间隔(ms)", "最大采样间隔(ms)", "采样质量"
];
WriteHeaderRow(sheet, 2, headers);
@@ -1584,6 +1593,12 @@ public sealed class MainWindowViewModel : ObservableObject
SetOptionalNumber(sheet.Cell(row, 12), run.NoLoadSpeedErrorRatePercent);
sheet.Cell(row, 13).Value = run.StartedAt.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
sheet.Cell(row, 14).Value = "PLC实时采样 + 测试停止最终值";
SamplingQuality quality = CalculateSamplingQuality(run.Samples);
TorqueCurvePayload? curve = run.TestType == "转速/扭矩测试" ? CreateTorqueCurvePayload(run) : null;
sheet.Cell(row, 15).Value = curve?.EvaluationSampleCount ?? 0;
sheet.Cell(row, 16).Value = quality.AverageIntervalMilliseconds;
sheet.Cell(row, 17).Value = quality.MaximumIntervalMilliseconds;
sheet.Cell(row, 18).Value = quality.Status;
}
sheet.SheetView.FreezeRows(2);
@@ -1594,18 +1609,18 @@ public sealed class MainWindowViewModel : ObservableObject
{
var sheet = workbook.Worksheets.Add("完整实时数据");
sheet.Cell(1, 1).Value = "完整实时数据(测试运行期间每次有效 PLC 轮询均记录)";
sheet.Range(1, 1, 1, 25).Merge().Style.Font.SetBold().Font.SetFontSize(16);
sheet.Range(1, 1, 1, 26).Merge().Style.Font.SetBold().Font.SetFontSize(16);
string[] headers =
[
"运行编号", "测试类型", "采样序号", "采样时间",
"千分表显示(mm)", "相对位移(mm)", "2号当前位置(mm)",
"千分表显示(mm)", "相对位移(mm)", "轴向位移当前位置(mm)",
"采集数据1-1(mm)", "采集数据1-2(mm)", "数据差值1(mm)",
"轴向力显示(N)", "1号当前位置(mm)", "1号相对位移(mm)",
"轴向力显示(N)", "转速/扭矩当前位置(mm)", "转速/扭矩相对位移(mm)",
$"最大扭矩采集({TorqueUnit})", $"扭矩显示({TorqueUnit})",
"转速显示(r/min)", "压力显示(kPa)", "空载转速记录(r/min)",
"转速误差率(%)", "扭矩完成", "复位使能1号", "复位完成1号",
"复位使能2号", "复位完成2号", "参数快照"
"转速误差率(%)", "扭矩完成", "转速/扭矩复位使能", "转速/扭矩复位完成",
"轴向位移复位使能", "轴向位移复位完成", "扭矩稳定保持", "参数快照"
];
WriteHeaderRow(sheet, 2, headers);
@@ -1638,7 +1653,8 @@ public sealed class MainWindowViewModel : ObservableObject
sheet.Cell(row, 22).Value = sample.SpeedTorqueResetDone ? 1 : 0;
sheet.Cell(row, 23).Value = sample.AxialResetEnabled ? 1 : 0;
sheet.Cell(row, 24).Value = sample.AxialResetDone ? 1 : 0;
sheet.Cell(row, 25).Value = run.StartedAt.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
sheet.Cell(row, 25).Value = sample.SpeedTorqueStable ? 1 : 0;
sheet.Cell(row, 26).Value = run.StartedAt.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
row++;
}
}
@@ -1747,6 +1763,48 @@ public sealed class MainWindowViewModel : ObservableObject
throw new InvalidOperationException(
$"报表校验失败:测试运行 {actualRunCount}/{expectedRunCount},完整采样 {actualSampleCount}/{expectedSampleCount}。");
}
TestRunPayload? speedTorqueRun = payload.Runs.LastOrDefault(static run => run.TestType == "转速/扭矩测试");
if (speedTorqueRun is not null)
{
if (!workbook.TryGetWorksheet("转速扭矩曲线", out IXLWorksheet? curveSheet))
{
throw new InvalidOperationException("报表校验失败:缺少转速扭矩曲线工作表。");
}
TorqueCurvePayload expectedCurve = CreateTorqueCurvePayload(speedTorqueRun);
ValidateCurveNumber(curveSheet.Cell(6, 2), expectedCurve.MinTorqueMilliNewtonMeters, expectedCurve.EvaluationSampleCount, "最小扭矩");
ValidateCurveNumber(curveSheet.Cell(7, 2), expectedCurve.MaxTorqueMilliNewtonMeters, expectedCurve.EvaluationSampleCount, "最大扭矩");
ValidateCurveNumber(curveSheet.Cell(8, 2), expectedCurve.AverageTorqueMilliNewtonMeters, expectedCurve.EvaluationSampleCount, "平均扭矩");
ValidateCurveNumber(curveSheet.Cell(9, 2), expectedCurve.FluctuationMilliNewtonMeters, expectedCurve.EvaluationSampleCount, "扭矩波动值");
}
foreach (IXLCell cell in workbook.Worksheets.SelectMany(static sheet => sheet.CellsUsed()))
{
string value = cell.GetFormattedString();
if (value.Contains("1号", StringComparison.Ordinal) || value.Contains("2号", StringComparison.Ordinal))
{
throw new InvalidOperationException($"报表校验失败:{cell.Worksheet.Name}!{cell.Address} 仍包含内部设备编号。");
}
}
}
private static void ValidateCurveNumber(IXLCell cell, double expected, int evaluationSampleCount, string name)
{
if (evaluationSampleCount < 2)
{
if (!cell.IsEmpty())
{
throw new InvalidOperationException($"报表校验失败:保持段采样不足时{name}应为空。");
}
return;
}
if (!cell.TryGetValue(out double actual) || Math.Abs(actual - expected) > 0.000001)
{
throw new InvalidOperationException($"报表校验失败:{name}与保持段原始采样不一致。");
}
}
private static void WriteHeaderRow(IXLWorksheet sheet, int row, IReadOnlyList<string> headers)
@@ -1807,13 +1865,17 @@ public sealed class MainWindowViewModel : ObservableObject
sheet.Cell(4, 2).Value = curve.ChangeThresholdMilliNewtonMeters;
sheet.Cell(5, 1).Value = "曲线判定";
sheet.Cell(5, 2).Value = curve.Result;
sheet.Cell(6, 1).Value = $"最小扭矩({TorqueUnit})";
sheet.Cell(10, 1).Value = "保持段起点(s)";
SetOptionalNumber(sheet.Cell(10, 2), curve.EvaluationStartSeconds);
sheet.Cell(10, 4).Value = "保持段终点(s)";
SetOptionalNumber(sheet.Cell(10, 5), curve.EvaluationEndSeconds);
sheet.Cell(6, 1).Value = $"保持时间内最小扭矩({TorqueUnit})";
sheet.Cell(6, 2).Value = curve.EvaluationSampleCount >= 2 ? curve.MinTorqueMilliNewtonMeters : string.Empty;
sheet.Cell(7, 1).Value = $"最大扭矩({TorqueUnit})";
sheet.Cell(7, 1).Value = $"保持时间内最大扭矩({TorqueUnit})";
sheet.Cell(7, 2).Value = curve.EvaluationSampleCount >= 2 ? curve.MaxTorqueMilliNewtonMeters : string.Empty;
sheet.Cell(8, 1).Value = $"平均扭矩({TorqueUnit})";
sheet.Cell(8, 1).Value = $"保持时间内平均扭矩({TorqueUnit})";
sheet.Cell(8, 2).Value = curve.EvaluationSampleCount >= 2 ? curve.AverageTorqueMilliNewtonMeters : string.Empty;
sheet.Cell(9, 1).Value = $"扭矩波动值({TorqueUnit})";
sheet.Cell(9, 1).Value = $"保持时间内扭矩波动值({TorqueUnit})";
sheet.Cell(9, 2).Value = curve.EvaluationSampleCount >= 2 ? curve.FluctuationMilliNewtonMeters : string.Empty;
sheet.Range(3, 1, 9, 1).Style.Font.SetBold();
@@ -1823,29 +1885,31 @@ public sealed class MainWindowViewModel : ObservableObject
sheet.Cell(4, 5).Value = $"左:扭矩({TorqueUnit});右:转速(r/min)";
sheet.Cell(5, 4).Value = "转速变化阈值(r/min)";
sheet.Cell(5, 5).Value = curve.SpeedChangeThresholdRpm;
sheet.Cell(6, 4).Value = "最小转速(r/min)";
sheet.Cell(6, 4).Value = "保持时间内最小转速(r/min)";
sheet.Cell(6, 5).Value = curve.EvaluationSampleCount >= 2 ? curve.MinSpeedRpm : string.Empty;
sheet.Cell(7, 4).Value = "最大转速(r/min)";
sheet.Cell(7, 4).Value = "保持时间内最大转速(r/min)";
sheet.Cell(7, 5).Value = curve.EvaluationSampleCount >= 2 ? curve.MaxSpeedRpm : string.Empty;
sheet.Cell(8, 4).Value = "平均转速(r/min)";
sheet.Cell(8, 4).Value = "保持时间内平均转速(r/min)";
sheet.Cell(8, 5).Value = curve.EvaluationSampleCount >= 2 ? curve.AverageSpeedRpm : string.Empty;
sheet.Cell(9, 4).Value = "转速波动值(r/min)";
sheet.Cell(9, 4).Value = "保持时间内转速波动值(r/min)";
sheet.Cell(9, 5).Value = curve.EvaluationSampleCount >= 2 ? curve.SpeedFluctuationRpm : string.Empty;
sheet.Range(3, 4, 9, 4).Style.Font.SetBold();
sheet.Cell(11, 1).Value = "时间(s)";
sheet.Cell(11, 2).Value = $"扭矩({TorqueUnit})";
sheet.Cell(11, 3).Value = "转速(r/min)";
sheet.Range(11, 1, 11, 3).Style.Fill.SetBackgroundColor(XLColor.FromHtml("#D9EAF7"));
sheet.Range(11, 1, 11, 3).Style.Font.SetBold();
sheet.Cell(12, 1).Value = "时间(s)";
sheet.Cell(12, 2).Value = $"扭矩({TorqueUnit})";
sheet.Cell(12, 3).Value = "转速(r/min)";
sheet.Cell(12, 4).Value = "稳定保持";
sheet.Range(12, 1, 12, 4).Style.Fill.SetBackgroundColor(XLColor.FromHtml("#D9EAF7"));
sheet.Range(12, 1, 12, 4).Style.Font.SetBold();
for (int i = 0; i < curve.Samples.Count; i++)
{
TorqueSamplePayload sample = curve.Samples[i];
int row = 12 + i;
int row = 13 + i;
sheet.Cell(row, 1).Value = sample.ElapsedSeconds;
sheet.Cell(row, 2).Value = sample.TorqueMilliNewtonMeters;
sheet.Cell(row, 3).Value = sample.SpeedRpm;
sheet.Cell(row, 4).Value = sample.IsStableHold ? 1 : 0;
}
if (curve.Samples.Count > 0)
@@ -3453,7 +3517,7 @@ public sealed class MainWindowViewModel : ObservableObject
NoLoadSpeedErrorRateText = $"{FormatErrorRate(_noLoadSpeedErrorRate)} %";
}
private void AppendTorqueSample(double torque, double speed, DateTime sampledAt)
private void AppendTorqueSample(double torque, double speed, bool isStableHold, DateTime sampledAt)
{
if (!_isSpeedTorqueRunning
|| !_speedTorqueStartedAt.HasValue
@@ -3470,7 +3534,8 @@ public sealed class MainWindowViewModel : ObservableObject
{
ElapsedSeconds = elapsedSeconds,
SpeedRpm = speed,
TorqueMilliNewtonMeters = torque
TorqueMilliNewtonMeters = torque,
IsStableHold = isStableHold
});
_cachedTorqueCurve = null;
}
@@ -3508,7 +3573,8 @@ public sealed class MainWindowViewModel : ObservableObject
{
ElapsedSeconds = sample.ElapsedSeconds,
SpeedRpm = sample.SpeedRpm,
TorqueMilliNewtonMeters = sample.TorqueMilliNewtonMeters
TorqueMilliNewtonMeters = sample.TorqueMilliNewtonMeters,
IsStableHold = sample.IsStableHold
})
.ToList();
@@ -3528,7 +3594,8 @@ public sealed class MainWindowViewModel : ObservableObject
{
ElapsedSeconds = Math.Max(0, (sample.SampledAt - run.StartedAt).TotalSeconds),
SpeedRpm = sample.RealtimeSpeedRpm,
TorqueMilliNewtonMeters = sample.RealtimeTorqueMilliNewtonMeters
TorqueMilliNewtonMeters = sample.RealtimeTorqueMilliNewtonMeters,
IsStableHold = sample.SpeedTorqueStable
})
.ToList();
@@ -3545,16 +3612,11 @@ public sealed class MainWindowViewModel : ObservableObject
double torqueThreshold,
double speedThreshold)
{
List<TorqueSamplePayload> evaluationSamples = samples
.Where(sample => sample.ElapsedSeconds <= holdTime)
.ToList();
if (holdTime <= 0)
{
return new TorqueCurvePayload
{
HoldTimeSeconds = holdTime,
EvaluationSampleCount = evaluationSamples.Count,
ChangeThresholdMilliNewtonMeters = torqueThreshold,
SpeedChangeThresholdRpm = speedThreshold,
Result = "未设置保持时间,未判定",
@@ -3562,15 +3624,44 @@ public sealed class MainWindowViewModel : ObservableObject
};
}
int stableStartIndex = samples.FindIndex(static sample => sample.IsStableHold);
if (stableStartIndex < 0)
{
return new TorqueCurvePayload
{
HoldTimeSeconds = holdTime,
ChangeThresholdMilliNewtonMeters = torqueThreshold,
SpeedChangeThresholdRpm = speedThreshold,
Result = "未检测到稳定保持段,未判定",
Samples = samples
};
}
double evaluationStart = samples[stableStartIndex].ElapsedSeconds;
double evaluationLimit = evaluationStart + holdTime;
var evaluationSamples = new List<TorqueSamplePayload>();
for (int i = stableStartIndex; i < samples.Count; i++)
{
TorqueSamplePayload sample = samples[i];
if (!sample.IsStableHold || sample.ElapsedSeconds > evaluationLimit)
{
break;
}
evaluationSamples.Add(sample);
}
if (evaluationSamples.Count < 2)
{
return new TorqueCurvePayload
{
HoldTimeSeconds = holdTime,
EvaluationSampleCount = evaluationSamples.Count,
EvaluationStartSeconds = evaluationStart,
EvaluationEndSeconds = evaluationSamples.LastOrDefault()?.ElapsedSeconds,
ChangeThresholdMilliNewtonMeters = torqueThreshold,
SpeedChangeThresholdRpm = speedThreshold,
Result = "采样不足,未判定",
Result = "保持段采样不足,未判定",
Samples = samples
};
}
@@ -3595,6 +3686,8 @@ public sealed class MainWindowViewModel : ObservableObject
{
HoldTimeSeconds = holdTime,
EvaluationSampleCount = evaluationSamples.Count,
EvaluationStartSeconds = evaluationStart,
EvaluationEndSeconds = evaluationSamples[^1].ElapsedSeconds,
ChangeThresholdMilliNewtonMeters = torqueThreshold,
SpeedChangeThresholdRpm = speedThreshold,
MinTorqueMilliNewtonMeters = minTorque,
@@ -3645,6 +3738,7 @@ public sealed class MainWindowViewModel : ObservableObject
NoLoadSpeedRpm = _noLoadSpeedRecord,
NoLoadSpeedErrorRatePercent = _noLoadSpeedErrorRate,
SpeedTorqueDone = ReadCoilValue(coilValues, SpeedTorqueDoneCoil),
SpeedTorqueStable = ReadCoilValue(coilValues, SpeedTorqueStableCoil),
SpeedTorqueResetEnabled = ReadCoilValue(coilValues, SpeedTorqueResetEnabledCoil),
SpeedTorqueResetDone = ReadCoilValue(coilValues, SpeedTorqueResetDoneCoil),
AxialResetEnabled = ReadCoilValue(coilValues, AxialResetEnabledCoil),
@@ -4019,11 +4113,49 @@ public sealed class MainWindowViewModel : ObservableObject
StatusText = $"当前整体验收:{CalculateOverallResult()}。完成测试后可导出报表。";
}
private static string CalculateOverallResult()
private string CalculateOverallResult()
{
return "记录";
int completedTypes = new[]
{
"轴向位移动量测试",
"转速/扭矩测试",
"空载转速测试"
}.Count(testType => GetLatestCompletedRun(testType) is not null);
return completedTypes switch
{
0 => "未记录",
3 => "全部测试已记录",
_ => "部分测试已记录"
};
}
private static SamplingQuality CalculateSamplingQuality(IReadOnlyList<RealtimeSamplePayload> samples)
{
if (samples.Count < 2)
{
return new SamplingQuality(0, 0, samples.Count == 0 ? "无采样" : "采样不足");
}
double totalMilliseconds = 0;
double maximumMilliseconds = 0;
for (int i = 1; i < samples.Count; i++)
{
double interval = Math.Max(0, (samples[i].SampledAt - samples[i - 1].SampledAt).TotalMilliseconds);
totalMilliseconds += interval;
maximumMilliseconds = Math.Max(maximumMilliseconds, interval);
}
double averageMilliseconds = totalMilliseconds / (samples.Count - 1);
string status = maximumMilliseconds > 300 ? "异常存在超过300ms采样间隔" : "正常";
return new SamplingQuality(averageMilliseconds, maximumMilliseconds, status);
}
private sealed record SamplingQuality(
double AverageIntervalMilliseconds,
double MaximumIntervalMilliseconds,
string Status);
private static double ReadFloatValue(IReadOnlyDictionary<ushort, float> values, ushort registerAddress, string label)
{
if (!values.TryGetValue(registerAddress, out float value)

View File

@@ -36,6 +36,9 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
private readonly object _transactionLock = new();
private readonly SemaphoreSlim _transportLock = new(1, 1);
private ushort _transactionId;
private TcpClient? _client;
private NetworkStream? _stream;
private string? _connectionKey;
public async Task PulseCoilAsync(PlcConnectionConfig config, ushort coilAddress, CancellationToken cancellationToken = default)
{
@@ -209,12 +212,9 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
NetworkStream stream = await GetConnectedStreamAsync(config, timeoutCts.Token);
ushort transactionId = NextTransactionId();
byte[] request = BuildWriteSingleCoilRequest(transactionId, config.UnitId, coilAddress, value);
@@ -239,6 +239,11 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
throw new InvalidOperationException("PLC 写线圈响应地址或值不匹配。");
}
}
catch
{
ResetConnection();
throw;
}
finally
{
_transportLock.Release();
@@ -250,12 +255,9 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
NetworkStream stream = await GetConnectedStreamAsync(config, timeoutCts.Token);
ushort transactionId = NextTransactionId();
byte[] request = BuildReadHoldingRegistersRequest(transactionId, config.UnitId, startAddress, numberOfPoints);
@@ -293,6 +295,11 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
return registers;
}
catch
{
ResetConnection();
throw;
}
finally
{
_transportLock.Release();
@@ -304,12 +311,9 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
NetworkStream stream = await GetConnectedStreamAsync(config, timeoutCts.Token);
ushort transactionId = NextTransactionId();
byte[] request = BuildReadCoilsRequest(transactionId, config.UnitId, startAddress, numberOfPoints);
@@ -348,6 +352,11 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
return coils;
}
catch
{
ResetConnection();
throw;
}
finally
{
_transportLock.Release();
@@ -359,12 +368,9 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
await _transportLock.WaitAsync(cancellationToken);
try
{
using var client = new TcpClient();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(config.TimeoutMilliseconds));
await client.ConnectAsync(config.IpAddress, config.Port, timeoutCts.Token);
await using NetworkStream stream = client.GetStream();
NetworkStream stream = await GetConnectedStreamAsync(config, timeoutCts.Token);
ushort transactionId = NextTransactionId();
byte[] request = BuildWriteMultipleRegistersRequest(transactionId, config.UnitId, startAddress, values);
@@ -391,6 +397,11 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
throw new InvalidOperationException("PLC 写寄存器响应地址或数量不匹配。");
}
}
catch
{
ResetConnection();
throw;
}
finally
{
_transportLock.Release();
@@ -409,6 +420,57 @@ public sealed class ModbusTcpPlcCoilService : IPlcCoilService, IPlcRegisterServi
return remainingLength;
}
private async Task<NetworkStream> GetConnectedStreamAsync(PlcConnectionConfig config, CancellationToken cancellationToken)
{
string connectionKey = $"{config.IpAddress}:{config.Port}";
if (_client is not null
&& _stream is not null
&& _client.Connected
&& string.Equals(_connectionKey, connectionKey, StringComparison.OrdinalIgnoreCase))
{
return _stream;
}
ResetConnection();
var client = new TcpClient();
try
{
await client.ConnectAsync(config.IpAddress, config.Port, cancellationToken);
_client = client;
_stream = client.GetStream();
_connectionKey = connectionKey;
return _stream;
}
catch
{
client.Dispose();
throw;
}
}
private void ResetConnection()
{
try
{
_stream?.Dispose();
}
catch
{
}
try
{
_client?.Dispose();
}
catch
{
}
_stream = null;
_client = null;
_connectionKey = null;
}
private static void ValidateFixedResponseHeader(byte[] response, byte[] request)
{
ValidateMbapHeader(response, request);

View File

@@ -206,6 +206,8 @@ public sealed class TorqueSamplePayload
public double SpeedRpm { get; init; }
public double TorqueMilliNewtonMeters { get; init; }
public bool IsStableHold { get; init; }
}
public sealed class TorqueCurvePayload
@@ -214,6 +216,10 @@ public sealed class TorqueCurvePayload
public int EvaluationSampleCount { get; init; }
public double? EvaluationStartSeconds { get; init; }
public double? EvaluationEndSeconds { get; init; }
public double ChangeThresholdMilliNewtonMeters { get; init; }
public double SpeedChangeThresholdRpm { get; init; }
@@ -306,6 +312,8 @@ public sealed class RealtimeSamplePayload
public bool SpeedTorqueDone { get; init; }
public bool SpeedTorqueStable { get; init; }
public bool SpeedTorqueResetEnabled { get; init; }
public bool SpeedTorqueResetDone { get; init; }