This commit is contained in:
GukSang.Jin
2026-06-04 19:35:31 +08:00
parent f0d03713a7
commit c9684c4398
2 changed files with 127 additions and 14 deletions

View File

@@ -118,6 +118,9 @@ public partial class PumpControlChannel : ObservableObject
[ObservableProperty] [ObservableProperty]
private int flowStabilizationRawSetpoint; private int flowStabilizationRawSetpoint;
[ObservableProperty]
private double flowStabilizationIntegralError;
[ObservableProperty] [ObservableProperty]
private string flowStabilizationStatusText = "稳流未启用"; private string flowStabilizationStatusText = "稳流未启用";
@@ -338,10 +341,26 @@ public partial class PumpControlChannel : ObservableObject
if (!value) if (!value)
{ {
FlowStabilizationStatusText = "稳流未启用"; FlowStabilizationStatusText = "稳流未启用";
FlowStabilizationRawSetpoint = 0;
FlowStabilizationIntegralError = 0;
ConsecutiveFlowStabilizationFailureCount = 0; ConsecutiveFlowStabilizationFailureCount = 0;
ConsecutiveFlowStabilizationLimitCount = 0; ConsecutiveFlowStabilizationLimitCount = 0;
ConsecutiveFlowStabilizationUnavailableCount = 0; ConsecutiveFlowStabilizationUnavailableCount = 0;
} }
else
{
FlowStabilizationIntegralError = 0;
ConsecutiveFlowStabilizationFailureCount = 0;
ConsecutiveFlowStabilizationLimitCount = 0;
ConsecutiveFlowStabilizationUnavailableCount = 0;
FlowStabilizationStatusText = !IsRunning || PendingRs485RunningState == false
? "等待泵运行"
: !FlowAvailable
? "等待流量反馈"
: !ConfirmedSetpointAvailable || ConfirmedSetpointFlowValue <= 0
? "等待确认目标流量"
: "等待稳流调节";
}
OnPropertyChanged(nameof(FlowStabilizationStateText)); OnPropertyChanged(nameof(FlowStabilizationStateText));
} }
@@ -392,6 +411,7 @@ public partial class PumpControlChannel : ObservableObject
ConfirmedRawSetpointValue = 0; ConfirmedRawSetpointValue = 0;
ConfirmedSetpointFlowValue = 0; ConfirmedSetpointFlowValue = 0;
FlowStabilizationRawSetpoint = 0; FlowStabilizationRawSetpoint = 0;
FlowStabilizationIntegralError = 0;
} }
partial void OnSetpointAvailableChanged(bool value) partial void OnSetpointAvailableChanged(bool value)

View File

@@ -44,6 +44,9 @@ public partial class MainViewModel
private const double FlowStabilizationDeadbandLpm = 0.05d; private const double FlowStabilizationDeadbandLpm = 0.05d;
private const int FlowStabilizationMaxRawStep = 5; private const int FlowStabilizationMaxRawStep = 5;
private const double FlowStabilizationMaxRelativeTrim = 0.20d; private const double FlowStabilizationMaxRelativeTrim = 0.20d;
private const double FlowStabilizationProportionalGainRatio = 0.25d;
private const double FlowStabilizationIntegralGainRatioPerSecond = 0.04d;
private const double FlowStabilizationMaxIntegralRawTrimRatio = 0.10d;
private const int FlowStabilizationMaxConsecutiveFailures = 3; private const int FlowStabilizationMaxConsecutiveFailures = 3;
private const int FlowStabilizationMaxConsecutiveLimitHits = 3; private const int FlowStabilizationMaxConsecutiveLimitHits = 3;
private const int FlowStabilizationMaxUnavailableCycles = 3; private const int FlowStabilizationMaxUnavailableCycles = 3;
@@ -686,6 +689,7 @@ public partial class MainViewModel
{ {
pump.FlowStabilizationStatusText = "等待泵运行"; pump.FlowStabilizationStatusText = "等待泵运行";
ResetFlowStabilizationProtectionCounters(pump); ResetFlowStabilizationProtectionCounters(pump);
ResetFlowStabilizationControllerState(pump);
return false; return false;
} }
@@ -703,6 +707,7 @@ public partial class MainViewModel
{ {
pump.FlowStabilizationStatusText = "等待确认目标流量"; pump.FlowStabilizationStatusText = "等待确认目标流量";
ResetFlowStabilizationProtectionCounters(pump); ResetFlowStabilizationProtectionCounters(pump);
ResetFlowStabilizationControllerState(pump);
return false; return false;
} }
@@ -716,13 +721,15 @@ public partial class MainViewModel
private async Task TryAdjustRs485FlowStabilizationAsync(PumpControlChannel pump) private async Task TryAdjustRs485FlowStabilizationAsync(PumpControlChannel pump)
{ {
var now = DateTime.UtcNow;
var targetFlow = pump.ConfirmedSetpointFlowValue; var targetFlow = pump.ConfirmedSetpointFlowValue;
var flowError = targetFlow - pump.FlowValue; var flowError = targetFlow - pump.FlowValue;
if (Math.Abs(flowError) <= FlowStabilizationDeadbandLpm) if (Math.Abs(flowError) <= FlowStabilizationDeadbandLpm)
{ {
ResetFlowStabilizationProtectionCounters(pump); ResetFlowStabilizationProtectionCounters(pump);
pump.FlowStabilizationIntegralError = 0;
pump.FlowStabilizationStatusText = $"稳流保持:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min"; pump.FlowStabilizationStatusText = $"稳流保持:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow; pump.LastFlowStabilizationAdjustmentUtc = now;
return; return;
} }
@@ -732,7 +739,7 @@ public partial class MainViewModel
if (targetRaw <= 0) if (targetRaw <= 0)
{ {
DisableFlowStabilizationForProtection(pump, "稳流保护:目标流量换算值无效"); DisableFlowStabilizationForProtection(pump, "稳流保护:目标流量换算值无效");
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow; pump.LastFlowStabilizationAdjustmentUtc = now;
return; return;
} }
@@ -744,17 +751,42 @@ public partial class MainViewModel
var maxTrim = Math.Max(FlowStabilizationMaxRawStep, (int)Math.Round(targetRaw * FlowStabilizationMaxRelativeTrim, MidpointRounding.AwayFromZero)); var maxTrim = Math.Max(FlowStabilizationMaxRawStep, (int)Math.Round(targetRaw * FlowStabilizationMaxRelativeTrim, MidpointRounding.AwayFromZero));
var minRaw = Math.Max(1, targetRaw - maxTrim); var minRaw = Math.Max(1, targetRaw - maxTrim);
var maxRaw = Math.Min(MaxRs485MotorCommand, targetRaw + maxTrim); var maxRaw = Math.Min(MaxRs485MotorCommand, targetRaw + maxTrim);
var rawStep = Math.Sign(flowError) * FlowStabilizationMaxRawStep; var elapsedSeconds = CalculateFlowStabilizationElapsedSeconds(pump, now);
var proportionalRaw = flowError * pump.Rs485RawPerLitrePerMinute * FlowStabilizationProportionalGainRatio;
var integralGain = Math.Max(0.0001d, pump.Rs485RawPerLitrePerMinute * FlowStabilizationIntegralGainRatioPerSecond);
var maxIntegralRaw = Math.Max(FlowStabilizationMaxRawStep, targetRaw * FlowStabilizationMaxIntegralRawTrimRatio);
var integralLimit = maxIntegralRaw / integralGain;
var nextIntegralError = Math.Clamp(
pump.FlowStabilizationIntegralError + flowError * elapsedSeconds,
-integralLimit,
integralLimit);
var integralRaw = nextIntegralError * integralGain;
var controllerOutputRaw = proportionalRaw + integralRaw;
var rawStep = (int)Math.Round(controllerOutputRaw, MidpointRounding.AwayFromZero);
if (rawStep == 0)
{
rawStep = Math.Sign(flowError);
}
rawStep = Math.Clamp(rawStep, -FlowStabilizationMaxRawStep, FlowStabilizationMaxRawStep);
var nextRaw = Math.Clamp(currentRaw + rawStep, minRaw, maxRaw); var nextRaw = Math.Clamp(currentRaw + rawStep, minRaw, maxRaw);
var appliedRawStep = nextRaw - currentRaw;
Logger.Information( Logger.Information(
"RS485 稳流调节计算PumpKey={PumpKey}PumpName={PumpName}TargetFlowLpm={TargetFlowLpm}CurrentFlowLpm={CurrentFlowLpm}FlowErrorLpm={FlowErrorLpm}TargetRaw={TargetRaw}CurrentRaw={CurrentRaw}NextRaw={NextRaw}MinRaw={MinRaw}MaxRaw={MaxRaw}", "RS485 PI稳流调节计算PumpKey={PumpKey}PumpName={PumpName}TargetFlowLpm={TargetFlowLpm}CurrentFlowLpm={CurrentFlowLpm}FlowErrorLpm={FlowErrorLpm}ElapsedSeconds={ElapsedSeconds}P={ProportionalRaw}I={IntegralRaw}IntegralError={IntegralError}ControllerOutputRaw={ControllerOutputRaw}TargetRaw={TargetRaw}CurrentRaw={CurrentRaw}RawStep={RawStep}AppliedRawStep={AppliedRawStep}NextRaw={NextRaw}MinRaw={MinRaw}MaxRaw={MaxRaw}",
pump.Key, pump.Key,
pump.Name, pump.Name,
targetFlow, targetFlow,
pump.FlowValue, pump.FlowValue,
flowError, flowError,
elapsedSeconds,
proportionalRaw,
integralRaw,
nextIntegralError,
controllerOutputRaw,
targetRaw, targetRaw,
currentRaw, currentRaw,
rawStep,
appliedRawStep,
nextRaw, nextRaw,
minRaw, minRaw,
maxRaw); maxRaw);
@@ -778,12 +810,12 @@ public partial class MainViewModel
maxRaw, maxRaw,
pump.ConsecutiveFlowStabilizationLimitCount); pump.ConsecutiveFlowStabilizationLimitCount);
DisableFlowStabilizationForProtection(pump, $"稳流保护:{message},已连续 {pump.ConsecutiveFlowStabilizationLimitCount} 次无法继续调节"); DisableFlowStabilizationForProtection(pump, $"稳流保护:{message},已连续 {pump.ConsecutiveFlowStabilizationLimitCount} 次无法继续调节");
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow; pump.LastFlowStabilizationAdjustmentUtc = now;
return; return;
} }
pump.FlowStabilizationStatusText = $"{message}{pump.ConsecutiveFlowStabilizationLimitCount}/{FlowStabilizationMaxConsecutiveLimitHits}"; pump.FlowStabilizationStatusText = $"{message}{pump.ConsecutiveFlowStabilizationLimitCount}/{FlowStabilizationMaxConsecutiveLimitHits}";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow; pump.LastFlowStabilizationAdjustmentUtc = now;
return; return;
} }
@@ -818,14 +850,15 @@ public partial class MainViewModel
pump.ConsecutiveFlowStabilizationFailureCount = 0; pump.ConsecutiveFlowStabilizationFailureCount = 0;
pump.ConsecutiveFlowStabilizationLimitCount = 0; pump.ConsecutiveFlowStabilizationLimitCount = 0;
pump.ConsecutiveFlowStabilizationUnavailableCount = 0; pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
pump.FlowStabilizationIntegralError = nextIntegralError;
pump.FlowStabilizationRawSetpoint = nextRaw; pump.FlowStabilizationRawSetpoint = nextRaw;
CacheResolvedRs485Setpoint(pump, nextRaw); CacheResolvedRs485Setpoint(pump, nextRaw);
ApplyPostCommandPumpState(pump, result, expectedRunning: true); ApplyPostCommandPumpState(pump, result, expectedRunning: true);
pump.FlowStabilizationStatusText = $"稳流调节:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min / 控制值 {nextRaw}"; pump.FlowStabilizationStatusText = $"PI稳流调节:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min / 控制值 {nextRaw}";
} }
finally finally
{ {
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow; pump.LastFlowStabilizationAdjustmentUtc = now;
EndRs485PumpOperation(pump); EndRs485PumpOperation(pump);
} }
} }
@@ -855,6 +888,20 @@ public partial class MainViewModel
return false; return false;
} }
private static double CalculateFlowStabilizationElapsedSeconds(PumpControlChannel pump, DateTime now)
{
if (pump.LastFlowStabilizationAdjustmentUtc == DateTime.MinValue)
{
return FlowStabilizationAdjustmentInterval.TotalSeconds;
}
var elapsedSeconds = (now - pump.LastFlowStabilizationAdjustmentUtc).TotalSeconds;
return Math.Clamp(
elapsedSeconds,
FlowStabilizationAdjustmentInterval.TotalSeconds,
FlowStabilizationAdjustmentInterval.TotalSeconds * 3d);
}
private bool RegisterFlowStabilizationCommandFailure(PumpControlChannel pump, string message) private bool RegisterFlowStabilizationCommandFailure(PumpControlChannel pump, string message)
{ {
pump.ConsecutiveFlowStabilizationFailureCount++; pump.ConsecutiveFlowStabilizationFailureCount++;
@@ -901,6 +948,12 @@ public partial class MainViewModel
pump.ConsecutiveFlowStabilizationUnavailableCount = 0; pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
} }
private static void ResetFlowStabilizationControllerState(PumpControlChannel pump)
{
pump.FlowStabilizationIntegralError = 0;
pump.FlowStabilizationRawSetpoint = 0;
}
private void RaiseRs485CalibrationSummaryChanges() private void RaiseRs485CalibrationSummaryChanges()
{ {
OnPropertyChanged(nameof(Rs485EnabledPumpCount)); OnPropertyChanged(nameof(Rs485EnabledPumpCount));
@@ -1508,7 +1561,7 @@ public partial class MainViewModel
if (ShouldUseRs485DirectPumpControl(pump)) if (ShouldUseRs485DirectPumpControl(pump))
{ {
if (!TryResolveRs485MotorCommand(pump, out var rawMotorSpeed, out var resolveMessage)) if (!TryResolveRs485MotorCommand(pump, out var rawMotorSpeed, out var resolvedTargetFlow, out var resolveMessage))
{ {
pump.SetpointStatusText = resolveMessage; pump.SetpointStatusText = resolveMessage;
Rs485StatusText = resolveMessage; Rs485StatusText = resolveMessage;
@@ -1531,6 +1584,7 @@ public partial class MainViewModel
return Rs485StartExecutionResult.Failed; return Rs485StartExecutionResult.Failed;
} }
CacheConfirmedRs485Setpoint(pump, rawMotorSpeed, resolvedTargetFlow);
CacheResolvedRs485Setpoint(pump, rawMotorSpeed); CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
ApplyPostCommandPumpState(pump, directResult, expectedRunning: true); ApplyPostCommandPumpState(pump, directResult, expectedRunning: true);
await RefreshTelemetryAsync(); await RefreshTelemetryAsync();
@@ -1736,9 +1790,14 @@ public partial class MainViewModel
private bool IsRs485ManagedPump(PumpControlChannel pump) => private bool IsRs485ManagedPump(PumpControlChannel pump) =>
_pumpActuationService.IsManagedPump(pump); _pumpActuationService.IsManagedPump(pump);
private bool TryResolveRs485MotorCommand(PumpControlChannel pump, out short rawMotorSpeed, out string message) private bool TryResolveRs485MotorCommand(
PumpControlChannel pump,
out short rawMotorSpeed,
out double targetFlowLpm,
out string message)
{ {
rawMotorSpeed = 0; rawMotorSpeed = 0;
targetFlowLpm = 0;
message = string.Empty; message = string.Empty;
if (!pump.HasSetpointCalibration) if (!pump.HasSetpointCalibration)
@@ -1753,7 +1812,13 @@ public partial class MainViewModel
return false; return false;
} }
var candidateRaw = pump.ConfirmedSetpointAvailable ? pump.ConfirmedRawSetpointValue : 0; var candidateRaw = 0;
if (pump.ConfirmedSetpointAvailable && pump.ConfirmedRawSetpointValue > 0)
{
candidateRaw = pump.ConfirmedRawSetpointValue;
targetFlowLpm = pump.ConfirmedSetpointFlowValue;
}
if (candidateRaw <= 0 if (candidateRaw <= 0
&& double.TryParse( && double.TryParse(
pump.PendingSetpointText, pump.PendingSetpointText,
@@ -1764,6 +1829,7 @@ public partial class MainViewModel
&& pendingFlow <= pump.Rs485MaxFlowLpm) && pendingFlow <= pump.Rs485MaxFlowLpm)
{ {
candidateRaw = ConvertFlowToRawSpeed(pump, pendingFlow); candidateRaw = ConvertFlowToRawSpeed(pump, pendingFlow);
targetFlowLpm = pendingFlow;
} }
if (candidateRaw <= 0) if (candidateRaw <= 0)
@@ -1772,6 +1838,17 @@ public partial class MainViewModel
return false; return false;
} }
if (targetFlowLpm <= 0 && pump.HasSetpointCalibration)
{
targetFlowLpm = ConvertRawSpeedToFlow(pump, candidateRaw);
}
if (targetFlowLpm <= 0)
{
message = $"{pump.Name} 启动前请输入大于 0 的目标流量";
return false;
}
if (candidateRaw > MaxRs485MotorCommand) if (candidateRaw > MaxRs485MotorCommand)
{ {
message = $"{pump.Name} 的直接启停控制值超过 {MaxRs485MotorCommand}"; message = $"{pump.Name} 的直接启停控制值超过 {MaxRs485MotorCommand}";
@@ -1793,6 +1870,7 @@ public partial class MainViewModel
pump.ConfirmedRawSetpointValue = rawMotorSpeed; pump.ConfirmedRawSetpointValue = rawMotorSpeed;
pump.ConfirmedSetpointFlowValue = flowLpm; pump.ConfirmedSetpointFlowValue = flowLpm;
pump.FlowStabilizationRawSetpoint = rawMotorSpeed; pump.FlowStabilizationRawSetpoint = rawMotorSpeed;
pump.FlowStabilizationIntegralError = 0;
} }
private bool TryBeginRs485PumpOperation(PumpControlChannel pump, string operationName) private bool TryBeginRs485PumpOperation(PumpControlChannel pump, string operationName)
@@ -1847,6 +1925,11 @@ public partial class MainViewModel
pump.IsRunning = result.RunStatus.Value == 1; pump.IsRunning = result.RunStatus.Value == 1;
pump.StateAvailable = result.RunStatus.Value is 0 or 1; pump.StateAvailable = result.RunStatus.Value is 0 or 1;
pump.Rs485RunStatusCode = result.RunStatus.Value; pump.Rs485RunStatusCode = result.RunStatus.Value;
if (!expectedRunning || result.RunStatus.Value != 1)
{
ResetFlowStabilizationAfterPumpStopped(pump);
}
return; return;
} }
@@ -1856,8 +1939,7 @@ public partial class MainViewModel
pump.IsRunning = false; pump.IsRunning = false;
pump.StateAvailable = true; pump.StateAvailable = true;
pump.Rs485RunStatusCode = 0; pump.Rs485RunStatusCode = 0;
pump.FlowStabilizationRawSetpoint = 0; ResetFlowStabilizationAfterPumpStopped(pump);
ResetFlowStabilizationProtectionCounters(pump);
return; return;
} }
@@ -1867,6 +1949,16 @@ public partial class MainViewModel
pump.Rs485RunStatusCode = null; pump.Rs485RunStatusCode = null;
} }
private static void ResetFlowStabilizationAfterPumpStopped(PumpControlChannel pump)
{
ResetFlowStabilizationControllerState(pump);
ResetFlowStabilizationProtectionCounters(pump);
if (pump.IsFlowStabilizationEnabled)
{
pump.FlowStabilizationStatusText = "等待泵运行";
}
}
private async Task<Rs485ToggleExecutionResult> TryTogglePumpControlViaRs485(PumpControlChannel pump, bool nextState) private async Task<Rs485ToggleExecutionResult> TryTogglePumpControlViaRs485(PumpControlChannel pump, bool nextState)
{ {
if (!ShouldUseRs485DirectPumpControl(pump)) if (!ShouldUseRs485DirectPumpControl(pump))
@@ -1894,7 +1986,7 @@ public partial class MainViewModel
{ {
if (nextState) if (nextState)
{ {
if (!TryResolveRs485MotorCommand(pump, out var rawMotorSpeed, out var resolveMessage)) if (!TryResolveRs485MotorCommand(pump, out var rawMotorSpeed, out var resolvedTargetFlow, out var resolveMessage))
{ {
pump.SetpointStatusText = resolveMessage; pump.SetpointStatusText = resolveMessage;
Rs485StatusText = resolveMessage; Rs485StatusText = resolveMessage;
@@ -1911,6 +2003,7 @@ public partial class MainViewModel
return Rs485ToggleExecutionResult.Failed; return Rs485ToggleExecutionResult.Failed;
} }
CacheConfirmedRs485Setpoint(pump, rawMotorSpeed, resolvedTargetFlow);
CacheResolvedRs485Setpoint(pump, rawMotorSpeed); CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
ApplyPostCommandPumpState(pump, startResult, expectedRunning: true); ApplyPostCommandPumpState(pump, startResult, expectedRunning: true);
await RefreshTelemetryAsync(); await RefreshTelemetryAsync();