更新122
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user