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

View File

@@ -44,6 +44,9 @@ public partial class MainViewModel
private const double FlowStabilizationDeadbandLpm = 0.05d;
private const int FlowStabilizationMaxRawStep = 5;
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 FlowStabilizationMaxConsecutiveLimitHits = 3;
private const int FlowStabilizationMaxUnavailableCycles = 3;
@@ -686,6 +689,7 @@ public partial class MainViewModel
{
pump.FlowStabilizationStatusText = "等待泵运行";
ResetFlowStabilizationProtectionCounters(pump);
ResetFlowStabilizationControllerState(pump);
return false;
}
@@ -703,6 +707,7 @@ public partial class MainViewModel
{
pump.FlowStabilizationStatusText = "等待确认目标流量";
ResetFlowStabilizationProtectionCounters(pump);
ResetFlowStabilizationControllerState(pump);
return false;
}
@@ -716,13 +721,15 @@ public partial class MainViewModel
private async Task TryAdjustRs485FlowStabilizationAsync(PumpControlChannel pump)
{
var now = DateTime.UtcNow;
var targetFlow = pump.ConfirmedSetpointFlowValue;
var flowError = targetFlow - pump.FlowValue;
if (Math.Abs(flowError) <= FlowStabilizationDeadbandLpm)
{
ResetFlowStabilizationProtectionCounters(pump);
pump.FlowStabilizationIntegralError = 0;
pump.FlowStabilizationStatusText = $"稳流保持:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
pump.LastFlowStabilizationAdjustmentUtc = now;
return;
}
@@ -732,7 +739,7 @@ public partial class MainViewModel
if (targetRaw <= 0)
{
DisableFlowStabilizationForProtection(pump, "稳流保护:目标流量换算值无效");
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
pump.LastFlowStabilizationAdjustmentUtc = now;
return;
}
@@ -744,17 +751,42 @@ public partial class MainViewModel
var maxTrim = Math.Max(FlowStabilizationMaxRawStep, (int)Math.Round(targetRaw * FlowStabilizationMaxRelativeTrim, MidpointRounding.AwayFromZero));
var minRaw = Math.Max(1, 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 appliedRawStep = nextRaw - currentRaw;
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.Name,
targetFlow,
pump.FlowValue,
flowError,
elapsedSeconds,
proportionalRaw,
integralRaw,
nextIntegralError,
controllerOutputRaw,
targetRaw,
currentRaw,
rawStep,
appliedRawStep,
nextRaw,
minRaw,
maxRaw);
@@ -778,12 +810,12 @@ public partial class MainViewModel
maxRaw,
pump.ConsecutiveFlowStabilizationLimitCount);
DisableFlowStabilizationForProtection(pump, $"稳流保护:{message},已连续 {pump.ConsecutiveFlowStabilizationLimitCount} 次无法继续调节");
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
pump.LastFlowStabilizationAdjustmentUtc = now;
return;
}
pump.FlowStabilizationStatusText = $"{message}{pump.ConsecutiveFlowStabilizationLimitCount}/{FlowStabilizationMaxConsecutiveLimitHits}";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
pump.LastFlowStabilizationAdjustmentUtc = now;
return;
}
@@ -818,14 +850,15 @@ public partial class MainViewModel
pump.ConsecutiveFlowStabilizationFailureCount = 0;
pump.ConsecutiveFlowStabilizationLimitCount = 0;
pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
pump.FlowStabilizationIntegralError = nextIntegralError;
pump.FlowStabilizationRawSetpoint = nextRaw;
CacheResolvedRs485Setpoint(pump, nextRaw);
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
{
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
pump.LastFlowStabilizationAdjustmentUtc = now;
EndRs485PumpOperation(pump);
}
}
@@ -855,6 +888,20 @@ public partial class MainViewModel
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)
{
pump.ConsecutiveFlowStabilizationFailureCount++;
@@ -901,6 +948,12 @@ public partial class MainViewModel
pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
}
private static void ResetFlowStabilizationControllerState(PumpControlChannel pump)
{
pump.FlowStabilizationIntegralError = 0;
pump.FlowStabilizationRawSetpoint = 0;
}
private void RaiseRs485CalibrationSummaryChanges()
{
OnPropertyChanged(nameof(Rs485EnabledPumpCount));
@@ -1508,7 +1561,7 @@ public partial class MainViewModel
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;
Rs485StatusText = resolveMessage;
@@ -1531,6 +1584,7 @@ public partial class MainViewModel
return Rs485StartExecutionResult.Failed;
}
CacheConfirmedRs485Setpoint(pump, rawMotorSpeed, resolvedTargetFlow);
CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
ApplyPostCommandPumpState(pump, directResult, expectedRunning: true);
await RefreshTelemetryAsync();
@@ -1736,9 +1790,14 @@ public partial class MainViewModel
private bool IsRs485ManagedPump(PumpControlChannel 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;
targetFlowLpm = 0;
message = string.Empty;
if (!pump.HasSetpointCalibration)
@@ -1753,7 +1812,13 @@ public partial class MainViewModel
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
&& double.TryParse(
pump.PendingSetpointText,
@@ -1764,6 +1829,7 @@ public partial class MainViewModel
&& pendingFlow <= pump.Rs485MaxFlowLpm)
{
candidateRaw = ConvertFlowToRawSpeed(pump, pendingFlow);
targetFlowLpm = pendingFlow;
}
if (candidateRaw <= 0)
@@ -1772,6 +1838,17 @@ public partial class MainViewModel
return false;
}
if (targetFlowLpm <= 0 && pump.HasSetpointCalibration)
{
targetFlowLpm = ConvertRawSpeedToFlow(pump, candidateRaw);
}
if (targetFlowLpm <= 0)
{
message = $"{pump.Name} 启动前请输入大于 0 的目标流量";
return false;
}
if (candidateRaw > MaxRs485MotorCommand)
{
message = $"{pump.Name} 的直接启停控制值超过 {MaxRs485MotorCommand}";
@@ -1793,6 +1870,7 @@ public partial class MainViewModel
pump.ConfirmedRawSetpointValue = rawMotorSpeed;
pump.ConfirmedSetpointFlowValue = flowLpm;
pump.FlowStabilizationRawSetpoint = rawMotorSpeed;
pump.FlowStabilizationIntegralError = 0;
}
private bool TryBeginRs485PumpOperation(PumpControlChannel pump, string operationName)
@@ -1847,6 +1925,11 @@ public partial class MainViewModel
pump.IsRunning = result.RunStatus.Value == 1;
pump.StateAvailable = result.RunStatus.Value is 0 or 1;
pump.Rs485RunStatusCode = result.RunStatus.Value;
if (!expectedRunning || result.RunStatus.Value != 1)
{
ResetFlowStabilizationAfterPumpStopped(pump);
}
return;
}
@@ -1856,8 +1939,7 @@ public partial class MainViewModel
pump.IsRunning = false;
pump.StateAvailable = true;
pump.Rs485RunStatusCode = 0;
pump.FlowStabilizationRawSetpoint = 0;
ResetFlowStabilizationProtectionCounters(pump);
ResetFlowStabilizationAfterPumpStopped(pump);
return;
}
@@ -1867,6 +1949,16 @@ public partial class MainViewModel
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)
{
if (!ShouldUseRs485DirectPumpControl(pump))
@@ -1894,7 +1986,7 @@ public partial class MainViewModel
{
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;
Rs485StatusText = resolveMessage;
@@ -1911,6 +2003,7 @@ public partial class MainViewModel
return Rs485ToggleExecutionResult.Failed;
}
CacheConfirmedRs485Setpoint(pump, rawMotorSpeed, resolvedTargetFlow);
CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
ApplyPostCommandPumpState(pump, startResult, expectedRunning: true);
await RefreshTelemetryAsync();