Add sensor delay for opening and closing valves
Test / Run tests (push) Successful in 43s

This commit is contained in:
2026-10-16 10:26:17 +02:00
parent d8c2292b24
commit d0c328d97e
10 changed files with 52 additions and 31 deletions
+2
View File
@@ -1,6 +1,8 @@
[plc] [plc]
port = COM1 port = COM1
sensor_delay_open = 0 ; seconds
sensor_delay_close = 2 ; seconds
[update] [update]
url = https://elwig.at/files/pamhagen-sysctrl/latest url = https://elwig.at/files/pamhagen-sysctrl/latest
+4
View File
@@ -9,6 +9,8 @@ namespace PamhagenSysCtrl.Helpers {
private readonly string FileName; private readonly string FileName;
public string PlcPort = "COM1"; public string PlcPort = "COM1";
public int PlcSensorDelayOpen = 0;
public int PlcSensorDelayClose = 0;
public string? UpdateUrl; public string? UpdateUrl;
public bool UpdateAuto; public bool UpdateAuto;
@@ -24,6 +26,8 @@ namespace PamhagenSysCtrl.Helpers {
try { try {
var config = new ConfigurationBuilder().AddIniFile(FileName).Build(); var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
PlcPort = config["plc:port"] ?? "COM1"; PlcPort = config["plc:port"] ?? "COM1";
PlcSensorDelayOpen = int.Parse(config["plc:sensor_delay_open"] ?? "0");
PlcSensorDelayClose = int.Parse(config["plc:sensor_delay_close"] ?? "0");
UpdateUrl = config["update:url"]; UpdateUrl = config["update:url"];
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower()); UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
var xbe24 = config.GetSection("xbe24"); var xbe24 = config.GetSection("xbe24");
+1 -1
View File
@@ -488,7 +488,7 @@ namespace PamhagenSysCtrl.Helpers {
} }
protected ControlledValve CreateValve(int n, double x, double y, int o) { protected ControlledValve CreateValve(int n, double x, double y, int o) {
return new(n, x, y, o, () => App.Plant?.State.Valve_IsOpen(n) ?? false, (v) => App.Plant?.ManualControlMode ?? false ? App.Plant.State.Valve_WantOpen(n) : SelectedPaths.Any(p => p.Contains(v))); return new(n, x, y, o, () => App.Plant?.State.Valve_OpenState(n), (v) => App.Plant?.ManualControlMode ?? false ? App.Plant.State.Valve_WantOpen(n) : SelectedPaths.Any(p => p.Contains(v)));
} }
protected static Tank CreateTank(int n, double x, double y, int capacity) { protected static Tank CreateTank(int n, double x, double y, int capacity) {
+1 -1
View File
@@ -97,7 +97,7 @@ namespace PamhagenSysCtrl.Helpers {
PlcThread = null; PlcThread = null;
PamhagenPlc? tmp = null; PamhagenPlc? tmp = null;
try { try {
tmp = new PamhagenPlc(PlcComPort); tmp = new PamhagenPlc(PlcComPort, App.Config.PlcSensorDelayOpen * 1000, App.Config.PlcSensorDelayClose * 1000);
await tmp.Initialize(); await tmp.Initialize();
Plc = tmp; Plc = tmp;
PlcConnectionError = null; PlcConnectionError = null;
@@ -13,7 +13,7 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
protected Func<Valve, bool> CallbackWantOpen; protected Func<Valve, bool> CallbackWantOpen;
public ControlledValve(int nr, double x, double y, int orientation, Func<bool> cbOpen, Func<Valve, bool> cbWant) : public ControlledValve(int nr, double x, double y, int orientation, Func<bool?> cbOpen, Func<Valve, bool> cbWant) :
base(x, y, orientation, cbOpen) { base(x, y, orientation, cbOpen) {
Nr = nr; Nr = nr;
CallbackWantOpen = cbWant; CallbackWantOpen = cbWant;
@@ -23,9 +23,9 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
if (App.Plant?.ManualControlMode ?? false) { if (App.Plant?.ManualControlMode ?? false) {
_text?.Visibility = Visibility.Visible; _text?.Visibility = Visibility.Visible;
_triangles?.Fill = _triangles?.Fill =
!WantOpen && !IsOpen && !isHovering ? PamhagenBrushes.Red : !WantOpen && IsClosed && !isHovering ? PamhagenBrushes.Red :
!WantOpen && !IsOpen ? PamhagenBrushes.Green : !WantOpen && IsClosed ? PamhagenBrushes.Green :
!WantOpen && IsOpen ? PamhagenBrushes.DimRed : !WantOpen && !IsClosed ? PamhagenBrushes.DimRed :
WantOpen && !IsOpen ? PamhagenBrushes.DimGreen : WantOpen && !IsOpen ? PamhagenBrushes.DimGreen :
WantOpen && IsOpen && !isHovering ? PamhagenBrushes.Green : WantOpen && IsOpen && !isHovering ? PamhagenBrushes.Green :
WantOpen && IsOpen ? PamhagenBrushes.Red : WantOpen && IsOpen ? PamhagenBrushes.Red :
@@ -33,13 +33,13 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
} else { } else {
_text?.Visibility = isHovering ? Visibility.Visible : Visibility.Hidden; _text?.Visibility = isHovering ? Visibility.Visible : Visibility.Hidden;
_triangles?.Fill = _triangles?.Fill =
IsLocked && !WantOpen && !IsOpen ? PamhagenBrushes.DimOrange : IsLocked && !WantOpen && IsClosed ? PamhagenBrushes.DimOrange :
IsLocked && !WantOpen && IsOpen ? PamhagenBrushes.Red : IsLocked && !WantOpen && !IsClosed ? PamhagenBrushes.Red :
IsLocked && WantOpen && !IsOpen ? PamhagenBrushes.Yellow : IsLocked && WantOpen && !IsOpen ? PamhagenBrushes.Yellow :
IsLocked && WantOpen && IsOpen ? PamhagenBrushes.Green : IsLocked && WantOpen && IsOpen ? PamhagenBrushes.Green :
Highlight ?? (isHovering ? Brushes.LightGray : Brushes.DarkGray); Highlight ?? (isHovering ? Brushes.LightGray : Brushes.DarkGray);
} }
if (IsOpen) { if (!IsClosed) {
_topLine?.Points[1] = _middlePoints.Open.Top; _topLine?.Points[1] = _middlePoints.Open.Top;
_bottomLine?.Points[1] = _middlePoints.Open.Bottom; _bottomLine?.Points[1] = _middlePoints.Open.Bottom;
} else { } else {
@@ -6,7 +6,7 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
public override string Label { get; } public override string Label { get; }
public ExternalValve(string label, double x, double y, int orientation, Func<bool> cbOpen) : public ExternalValve(string label, double x, double y, int orientation, Func<bool?> cbOpen) :
base(x, y, orientation, cbOpen) { base(x, y, orientation, cbOpen) {
Label = label; Label = label;
} }
@@ -17,7 +17,7 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
!IsOpen ? PamhagenBrushes.Red : !IsOpen ? PamhagenBrushes.Red :
IsOpen ? PamhagenBrushes.Green : IsOpen ? PamhagenBrushes.Green :
Highlight ?? Brushes.DarkGray; Highlight ?? Brushes.DarkGray;
if (IsOpen) { if (!IsClosed) {
_topLine?.Points[1] = _middlePoints.Open.Top; _topLine?.Points[1] = _middlePoints.Open.Top;
_bottomLine?.Points[1] = _middlePoints.Open.Bottom; _bottomLine?.Points[1] = _middlePoints.Open.Bottom;
} else { } else {
+2 -2
View File
@@ -1,7 +1,7 @@
namespace PamhagenSysCtrl.Helpers.Pipeline { namespace PamhagenSysCtrl.Helpers.Pipeline {
public record struct Path( public record struct Path(
INode Start, INode Start,
IEnumerable<(IEdge Edge, INode Node)> Hops (IEdge Edge, INode Node)[] Hops
) { ) {
public readonly int Bends => Hops.Sum(h => (h.Node is not Valve && h.Node is not PipeJoin ? 1 : 0) + (h.Edge.Start.CenterX != h.Edge.End.CenterX && h.Edge.Start.CenterY != h.Edge.End.CenterY ? 1 : 0)); public readonly int Bends => Hops.Sum(h => (h.Node is not Valve && h.Node is not PipeJoin ? 1 : 0) + (h.Edge.Start.CenterX != h.Edge.End.CenterX && h.Edge.Start.CenterY != h.Edge.End.CenterY ? 1 : 0));
public readonly double Length => Hops.Sum(h => Math.Abs(h.Edge.Start.CenterX - h.Edge.End.CenterX) + Math.Abs(h.Edge.Start.CenterY - h.Edge.End.CenterY)); public readonly double Length => Hops.Sum(h => Math.Abs(h.Edge.Start.CenterX - h.Edge.End.CenterX) + Math.Abs(h.Edge.Start.CenterY - h.Edge.End.CenterY));
@@ -26,7 +26,7 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
public readonly IEnumerable<ControlledValve> BorderValves => InnerAndBorderValves.Except(InnerValves); public readonly IEnumerable<ControlledValve> BorderValves => InnerAndBorderValves.Except(InnerValves);
public readonly bool HasActivePump => Nodes.Any(n => n is Pump p && p.IsActive); public readonly bool HasActivePump => Nodes.Any(n => n is Pump p && p.IsActive);
public readonly bool IsReady => InnerValves.All(v => v.IsOpen && v.WantOpen && v.IsLocked) && BorderValves.All(v => !v.IsOpen && !v.WantOpen && v.IsLocked); public readonly bool IsReady => InnerValves.All(v => v.IsOpen && v.WantOpen && v.IsLocked) && BorderValves.All(v => v.IsClosed && !v.WantOpen && v.IsLocked);
public readonly void LockValves() { public readonly void LockValves() {
foreach (var v in InnerAndBorderValves) { foreach (var v in InnerAndBorderValves) {
+5 -3
View File
@@ -16,11 +16,13 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
public int Orientation { get; set; } public int Orientation { get; set; }
public bool IsOpen => CallbackIsOpen(); public bool IsOpen => CallbackIsOpen() == true;
public bool IsClosed => CallbackIsOpen() == false;
public bool IsTransitioning => CallbackIsOpen() == null;
public ISet<IEdge> Inputs { get; init; } public ISet<IEdge> Inputs { get; init; }
public ISet<IEdge> Outputs { get; init; } public ISet<IEdge> Outputs { get; init; }
protected Func<bool> CallbackIsOpen; protected Func<bool?> CallbackIsOpen;
protected Rectangle? _background; protected Rectangle? _background;
protected Polygon? _triangles; protected Polygon? _triangles;
@@ -29,7 +31,7 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
protected ((Point Top, Point Bottom) Closed, (Point Top, Point Bottom) Open) _middlePoints; protected ((Point Top, Point Bottom) Closed, (Point Top, Point Bottom) Open) _middlePoints;
protected TextBlock? _text; protected TextBlock? _text;
public Valve(double x, double y, int orientation, Func<bool> cbOpen) { public Valve(double x, double y, int orientation, Func<bool?> cbOpen) {
CenterX = x; CenterX = x;
CenterY = y; CenterY = y;
CallbackIsOpen = cbOpen; CallbackIsOpen = cbOpen;
+15 -8
View File
@@ -27,12 +27,16 @@ namespace PamhagenSysCtrl.Helpers.Plc {
internal LegacyPamhagenPlc.ActuatorStates Actuators; internal LegacyPamhagenPlc.ActuatorStates Actuators;
internal LegacyPamhagenPlc.ActuatorStates LastActuators; internal LegacyPamhagenPlc.ActuatorStates LastActuators;
internal LegacyPamhagenPlc.SensorStates Sensors = new(default, default, default, default, default, default, default, [ internal LegacyPamhagenPlc.SensorStates[] LastSensors = [];
FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown
], default);
public PamhagenPlc(string comPort, Parity parity = Parity.Odd) { internal readonly int ConsiderLastSensorsOpen;
internal readonly int ConsiderLastSensorsClose;
public PamhagenPlc(string comPort, int sensorDelayOpenMs, int sensorDelayCloseMs, Parity parity = Parity.Odd) {
Plc = new(comPort, parity); Plc = new(comPort, parity);
ConsiderLastSensorsOpen = sensorDelayOpenMs / 125 + 1;
ConsiderLastSensorsClose = sensorDelayCloseMs / 125 + 1;
LastSensors = new LegacyPamhagenPlc.SensorStates[Math.Max(ConsiderLastSensorsOpen, ConsiderLastSensorsClose)];
} }
public void Dispose() { public void Dispose() {
@@ -44,8 +48,9 @@ namespace PamhagenSysCtrl.Helpers.Plc {
await Plc.TestConnection(); await Plc.TestConnection();
Actuators = await Plc.ReadOutputs(); Actuators = await Plc.ReadOutputs();
LastActuators = Actuators; LastActuators = Actuators;
Sensors = await Plc.ReadInputs(); var sensors = await Plc.ReadInputs();
State = new(Sensors, this); LastSensors[0] = sensors;
State = new(LastSensors, this);
} }
public async Task Shutdown() { public async Task Shutdown() {
@@ -59,8 +64,10 @@ namespace PamhagenSysCtrl.Helpers.Plc {
LastActuators = Actuators; LastActuators = Actuators;
await Plc.WriteOutputs(LastActuators); await Plc.WriteOutputs(LastActuators);
} }
Sensors = await Plc.ReadInputs(); var sensors = await Plc.ReadInputs();
State = new(Sensors, this); Array.Copy(LastSensors, 0, LastSensors, 1, LastSensors.Length - 1);
LastSensors[0] = sensors;
State = new(LastSensors, this);
UpdateInterlocks(); UpdateInterlocks();
} }
+13 -7
View File
@@ -10,7 +10,8 @@ namespace PamhagenSysCtrl.Helpers.Plc {
public bool OverrideDryRunClearances => Plc != null && Plc.OverrideDryRunClearances; public bool OverrideDryRunClearances => Plc != null && Plc.OverrideDryRunClearances;
public long Valves => Actuators.Valves; public long Valves => Actuators.Valves;
public long PressureSensors => Sensors.PressureSensors; public readonly long OpenValves { get; }
public readonly long ClosedValves { get; }
public bool MW1_IsSelected => Sensors.TroughSelector == 1; public bool MW1_IsSelected => Sensors.TroughSelector == 1;
public bool MW2_IsSelected => Sensors.TroughSelector == 2; public bool MW2_IsSelected => Sensors.TroughSelector == 2;
@@ -114,10 +115,13 @@ namespace PamhagenSysCtrl.Helpers.Plc {
public bool P5_HasClearance => P4_HasDryRunClearance && P5_HasPathClearance; public bool P5_HasClearance => P4_HasDryRunClearance && P5_HasPathClearance;
public bool P6_HasClearance => P6_HasDryRunClearance && P6_HasPathClearance; public bool P6_HasClearance => P6_HasDryRunClearance && P6_HasPathClearance;
public PamhagenState(LegacyPamhagenPlc.SensorStates sensors, PamhagenPlc plc) { public PamhagenState(IEnumerable<LegacyPamhagenPlc.SensorStates> sensors, PamhagenPlc plc) {
Sensors = sensors; Sensors = sensors.First();
Plc = plc; Plc = plc;
OpenValves = sensors.Take(Plc.ConsiderLastSensorsOpen).Aggregate(-1L, (m, s) => m & ~s.PressureSensors);
ClosedValves = sensors.Take(Plc.ConsiderLastSensorsClose).Aggregate(-1L, (m, s) => m & s.PressureSensors);
MP1_WantsPathClearance = WantsPathClearnace(this, Paths.MP1); MP1_WantsPathClearance = WantsPathClearnace(this, Paths.MP1);
MP2_WantsPathClearance = WantsPathClearnace(this, Paths.MP2); MP2_WantsPathClearance = WantsPathClearnace(this, Paths.MP2);
P12_WantsPathClearance = WantsPathClearnace(this, Paths.P12); P12_WantsPathClearance = WantsPathClearnace(this, Paths.P12);
@@ -143,8 +147,10 @@ namespace PamhagenSysCtrl.Helpers.Plc {
P6_DryRunClearance = true; P6_DryRunClearance = true;
} }
public bool Valve_IsClosed(int n) => Sensors.GetDS(n); public bool Valve_IsClosed(int n) => (ClosedValves & (1L << (n - 1))) != 0;
public bool Valve_IsOpen(int n) => !Valve_IsClosed(n); public bool Valve_IsTransitioning(int n) => !Valve_IsClosed(n) && !Valve_IsOpen(n);
public bool Valve_IsOpen(int n) => (OpenValves & (1L << (n - 1))) != 0;
public bool? Valve_OpenState(int n) => Valve_IsOpen(n) ? true : Valve_IsClosed(n) ? false : null;
public bool Valve_WantOpen(int n) => Actuators.GetV(n); public bool Valve_WantOpen(int n) => Actuators.GetV(n);
public bool Valve_WantClosed(int n) => !Valve_WantOpen(n); public bool Valve_WantClosed(int n) => !Valve_WantOpen(n);
@@ -164,11 +170,11 @@ namespace PamhagenSysCtrl.Helpers.Plc {
} }
private static bool HasSinglePathClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) { private static bool HasSinglePathClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) {
return masks.Any(m => state.Valves == m && ~state.PressureSensors == m); return masks.Any(m => state.Valves == m && state.OpenValves == m && ~state.ClosedValves == m);
} }
private static bool HasAnyPathClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) { private static bool HasAnyPathClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) {
return masks.Any(m => state.Valves >= m && ~state.PressureSensors >= m); return masks.Any(m => state.Valves >= m && state.OpenValves >= m && ~state.ClosedValves >= m);
} }
private static bool WantsSinglePathClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) { private static bool WantsSinglePathClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) {