3 Commits
Author SHA1 Message Date
lorenz.stechauner 4071cc2f2c [WIP] XBE-24
Test / Run tests (push) Failing after 35s
2026-09-11 23:09:26 +02:00
lorenz.stechauner 82d7a5b626 [WIP] PipeJoins 2026-09-09 15:41:46 +02:00
lorenz.stechauner 00100a7dc2 [WIP] Status bar 2026-09-09 15:41:46 +02:00
11 changed files with 481 additions and 113 deletions
+2 -6
View File
@@ -36,15 +36,11 @@ namespace PamhagenSysCtrl {
protected override async void OnStartup(StartupEventArgs evt) {
Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0");
Plant = new(Config.PlcPort);
try {
Plant = new(Config.PlcPort);
await Plant.StartThread();
} catch (Exception exc) {
Plant?.Dispose();
Plant = null;
var str = $"Beim Aufbauen einer Verbindung zur SPS ist ein Fehler aufgetreten:\n\n{exc.GetType().Name}: {exc.Message}";
if (exc.InnerException != null) str += $"\n\n{exc.InnerException.GetType().Name}: {exc.InnerException.Message}";
MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
// TODO
}
base.OnStartup(evt);
+7 -1
View File
@@ -512,6 +512,7 @@ namespace PamhagenSysCtrl.Helpers {
HashSet<IEdge> visited = [];
foreach (var (edge, node) in path.Hops) {
edge.Highlight = color1;
(edge.Start as PipeJoin)?.Highlight = color1;
if (flowArrows > MotorState.Halt) {
var dir = edge.End == node;
if (flowArrows == MotorState.Backward) dir = !dir;
@@ -520,7 +521,10 @@ namespace PamhagenSysCtrl.Helpers {
}
visited.Add(edge);
if (node is Pump) color = color2;
TraverseSubgraph(node, (n) => n is Valve || n is ISink || n is Pump, visited, edgeAction: (s, r, e) => e.Highlight = color);
TraverseSubgraph(node, (n) => n is Valve || n is ISink || n is Pump, visited, edgeAction: (s, r, e) => {
(s as PipeJoin)?.Highlight = color;
e.Highlight = color;
});
}
}
@@ -530,6 +534,8 @@ namespace PamhagenSysCtrl.Helpers {
edgeAction: (s, r, e) => {
if (((e.Start as Valve)?.IsOpen ?? true) && ((e.End as Valve)?.IsOpen ?? true)) {
e.Highlight = color1;
(e.Start as PipeJoin)?.Highlight = color1;
(e.End as PipeJoin)?.Highlight = color1;
if (flowArrows == MotorState.Forward || flowArrows == MotorState.Backward) {
var dir = e.Start == s;
if (r) dir = !dir;
+210 -3
View File
@@ -1,10 +1,15 @@
using PamhagenSysCtrl.Helpers.Pipeline;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace PamhagenSysCtrl.Helpers {
public class PamhagenPlant : IDisposable {
protected PamhagenPlc? Plc;
protected Xbe24Keyboard? Keyboard;
public PamhagenGraph Graph;
protected bool IsRunning = true;
protected Thread? BackgroundThread;
@@ -21,6 +26,11 @@ namespace PamhagenSysCtrl.Helpers {
FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown
], default);
public string? PlcPortName => Plc?.PortName;
public T1.Status? LastPlcStatus => Plc?.LastPlcStatus;
public T1Error? LastPlcError => Plc?.LastPlcError;
public long? LastRttNs { get; private set; }
public bool IsValveOpen(int n) => !IsValveClosed(n);
public bool IsValveClosed(int n) => Sensors.GetDS(n);
public bool WantValveClosed(int n) => !WantValveOpen(n);
@@ -73,8 +83,25 @@ namespace PamhagenSysCtrl.Helpers {
public bool IsP6Active => Actuators.P6 == MotorState.Forward;
public PamhagenPlant(string portName) {
Plc = new(portName);
try {
Plc = new(portName);
} catch (Exception exc) {
var str = $"Beim Aufbauen einer Verbindung zur SPS ist ein Fehler aufgetreten:\n\n{exc.GetType().Name}: {exc.Message}";
if (exc.InnerException != null) str += $"\n\n{exc.InnerException.GetType().Name}: {exc.InnerException.Message}";
MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Graph = new();
try {
Keyboard = new();
Keyboard.KeyDown += Keyboard_KeyDown;
Keyboard.KeysChanged += Keyboard_KeysChanged;
Keyboard.SetFashFrequency(250);
Keyboard.SetBacklight(Color.FromRgb(4, 4, 4));
} catch { }
Update += OnUpdate;
Update += UpdateKeyboardBacklights;
}
protected virtual void RaiseUpdateEvent(PlantEventArgs evt) {
@@ -91,6 +118,10 @@ namespace PamhagenSysCtrl.Helpers {
if (p.IsAuger2Active && !p.IsAuger3Active) p.StartAuger3();
if (p.IsTroughAugerActive && !p.TroughAugerHasClearance)
p.StopTroughAuger();
if (!p.IsPresseQuerSchneckeActive && !p.IsAuger1Active) {
p.StopAuger2();
p.StopAuger3();
}
}
if (p.IsMP1Active && !p.MP1HasClearance) p.StopMP1();
@@ -116,6 +147,66 @@ namespace PamhagenSysCtrl.Helpers {
if (p.IsP6Active && !p.P6HasClearance) p.StopP6();
}
protected void UpdateKeyboardBacklights(object? sender, PlantEventArgs evt) {
if (sender is not PamhagenPlant plant || plant.Keyboard is not Xbe24Keyboard k) return;
k.SetBacklight(0, plant.IsReblerActive ? Color.FromRgb(0, 255, 0) : plant.IsAuger1Active ? Color.FromRgb(255, 255, 0) : plant.IsAuger2Active || plant.IsAuger3Active ? Color.FromRgb(32, 32, 4) : Color.FromRgb(4, 32, 4));
k.SetBacklight(1, !plant.IsReblerActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4));
k.SetBacklight(6, !plant.TroughAugerHasClearance ? k.IsKeyPressed(6) ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4) : plant.IsTroughAugerActive ? Color.FromRgb(0, 255, 0) : Color.FromRgb(4, 32, 4));
k.SetBacklight(7, !plant.IsTroughAugerActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4));
k.SetBacklight(2, plant.WantMW2Selected || plant.IsMW2Selected ? Color.FromRgb(0, 0, 255) : Color.FromRgb(4, 4, 16), plant.WantMW2Selected && !plant.IsMW2Selected);
k.SetBacklight(8, plant.WantMW1Selected || plant.IsMW1Selected ? Color.FromRgb(0, 0, 255) : Color.FromRgb(4, 4, 16), plant.WantMW1Selected && !plant.IsMW1Selected);
var buttons = new Dictionary<int, (ISource Source, Pump Pump, ISink Sink)> {
[12] = (Graph.MW1, Graph.MP1, Graph.Press1),
[18] = (Graph.MW2, Graph.MP2, Graph.Press1),
[13] = (Graph.MW1, Graph.MP1, Graph.Press2),
[19] = (Graph.MW2, Graph.MP2, Graph.Press2),
[14] = (Graph.MW1, Graph.MP1, Graph.Tank1),
[20] = (Graph.MW2, Graph.MP2, Graph.Tank1),
[15] = (Graph.MW1, Graph.MP1, Graph.Tank2),
[21] = (Graph.MW2, Graph.MP2, Graph.Tank2),
[16] = (Graph.MW1, Graph.MP1, Graph.Tank3),
[22] = (Graph.MW2, Graph.MP2, Graph.Tank3),
[17] = (Graph.MW1, Graph.MP1, Graph.Tank4),
[23] = (Graph.MW2, Graph.MP2, Graph.Tank4),
};
bool preparingMP1 = false, preparingMP2 = false;
bool highlightOffMP1 = false, highlightOffMP2 = false;
foreach (var (idx, d) in buttons) {
var isRunning = d.Pump.IsActive;
if (plant.Graph.SelectedPaths.Select(p => (Pipeline.Path?)p).FirstOrDefault(p => p?.Start == d.Source && p?.Hops.Last().Node == d.Sink) is Pipeline.Path path) {
// TODO
var isReady = path.Hops.All(h => h.Node is not Valve v || v.IsOpen == v.WantOpen);
if (!isReady && d.Pump == Graph.MP1) preparingMP1 = true;
if (!isReady && d.Pump == Graph.MP2) preparingMP2 = true;
var isDefault = !path.Hops.SkipLast(1).Any(h => h.Node is not Valve && h.Node is not PipeJoin && h.Node is not Pump && h.Node is not CenterValve);
k.SetBacklight(idx, isRunning ? isDefault ? Color.FromRgb(0, 255, 255) : Color.FromRgb(255, 64, 255) : isDefault ? Color.FromRgb(0, 0, 255) : Color.FromRgb(128, 0, 128), !isReady);
} else {
var isFree = Graph.GetFreePath([d.Source, d.Sink], overrideStart: true) != null;
if (isRunning && k.IsKeyPressed(idx) && d.Pump == Graph.MP1) highlightOffMP1 = true;
if (isRunning && k.IsKeyPressed(idx) && d.Pump == Graph.MP2) highlightOffMP2 = true;
k.SetBacklight(idx, isRunning ? k.IsKeyPressed(idx) ? Color.FromRgb(255, 0, 0) : Color.FromRgb(4, 4, 4) : isFree ? Color.FromRgb(8, 8, 32) : k.IsKeyPressed(idx) ? Color.FromRgb(255, 0, 0) : Color.FromRgb(4, 4, 4));
}
}
if (!plant.MP2HasPathClearance && plant.MP2HasDryRunClearance && preparingMP2) {
k.SetBacklight(4, Color.FromRgb(4, 32, 4), true);
} else {
k.SetBacklight(4, !plant.MP2HasClearance ? k.IsKeyPressed(4) ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4) : plant.IsMP2Active ? Color.FromRgb(0, 255, 0) : Color.FromRgb(4, 32, 4));
}
if (!plant.MP1HasPathClearance && plant.MP1HasDryRunClearance && preparingMP1) {
k.SetBacklight(10, Color.FromRgb(4, 32, 4), true);
} else {
k.SetBacklight(10, !plant.MP1HasClearance ? k.IsKeyPressed(10) ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4) : plant.IsMP1Active ? Color.FromRgb(0, 255, 0) : Color.FromRgb(4, 32, 4));
}
k.SetBacklight(5, highlightOffMP2 ? Color.FromRgb(255, 64, 0) : !plant.IsMP2Active ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4));
k.SetBacklight(11, highlightOffMP1 ? Color.FromRgb(255, 64, 0) : !plant.IsMP1Active ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4));
}
public async Task StartThread() {
if (Plc == null) throw new Exception();
await Plc.TestConnection();
@@ -128,19 +219,23 @@ namespace PamhagenSysCtrl.Helpers {
IsRunning = false;
BackgroundThread?.Join();
Plc?.Dispose();
Keyboard?.Dispose();
GC.SuppressFinalize(this);
}
protected async void BackgroundLoop(object? parameters) {
while (IsRunning && Plc != null) {
var start = DateTime.Now;
try {
if (ActuatorsChanged) {
await Plc.WriteOutputs(Actuators);
ActuatorsChanged = false;
}
Sensors = await Plc.ReadInputs();
LastRttNs = (long)(DateTime.Now - start).TotalNanoseconds;
RaiseUpdateEvent(new());
} catch (Exception exc) {
LastRttNs = null;
var str = "Bei der SPS ist ein Fehler aufgetreten:\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
@@ -163,6 +258,114 @@ namespace PamhagenSysCtrl.Helpers {
}
}
public void InitializePaths() {
var add = ReconstructSelectedPaths().ToList();
var remove = Graph.SelectedPaths.ToList();
foreach (var p in remove) Graph.RemovePath(p);
for (int i = 1; i <= 57; i++) {
App.Plant?.CloseV(i);
}
foreach (var p in add) {
if (p.Hops.Any(h => h.Node is Valve v && v.IsLocked)) continue;
Graph.AddPath(p);
}
}
public IEnumerable<Pipeline.Path> ReconstructSelectedPaths() {
if (App.Plant is not PamhagenPlant p) yield break;
var sources = Graph.Nodes.Where(n => n is ISource).ToList();
var sinks = Graph.Nodes.Where(n => n is ISink).ToList();
foreach (var src in sources) {
var paths = new List<Pipeline.Path>();
foreach (var sink in sinks) {
paths.AddRange(Graph.GetPaths(src, sink, null, (n) => n is not ISink && (n is not Valve v || p.WantValveOpen(v.Nr))));
}
if (paths.Count == 1)
yield return paths.First();
}
}
public void FastSelect(ISource source, ISink sink) {
if (Graph.SelectedPaths.Select(p => (Pipeline.Path?)p).FirstOrDefault(p => p?.Start == source, null) is Pipeline.Path p1) {
if (p1.Hops.Any(h => h.Node is Pump p && p.IsActive))
throw new NoClearanceException("Pumpe aktiv");
Graph.RemovePath(p1);
if (p1.Hops.Last().Node == sink)
return;
}
if (Graph.GetFreePath([source, sink]) is Pipeline.Path p2) {
Graph.AddPath(p2);
}
}
private void Keyboard_KeyDown(object? sender, int keyIndex) {
App.MainDispatcher?.BeginInvoke(() => {
try {
if (keyIndex == 0) {
StartRebler();
} else if (keyIndex == 1) {
if (IsReblerActive) {
StopRebler();
} else {
StopAuger1();
if (!IsPresseQuerSchneckeActive) {
StopAuger2();
StopAuger3();
}
}
} else if (keyIndex == 6) {
StartTroughAuger();
} else if (keyIndex == 7) {
StopTroughAuger();
} else if (keyIndex == 2) {
SelectMW2();
} else if (keyIndex == 8) {
SelectMW1();
} else if (keyIndex == 4) {
StartMP2Forward();
} else if (keyIndex == 5) {
StopMP2();
} else if (keyIndex == 10) {
StartMP1Forward();
} else if (keyIndex == 11) {
StopMP1();
} else if (keyIndex == 12) {
FastSelect(Graph.MW1, Graph.Press1);
} else if (keyIndex == 13) {
FastSelect(Graph.MW1, Graph.Press2);
} else if (keyIndex == 14) {
FastSelect(Graph.MW1, Graph.Tank1);
} else if (keyIndex == 15) {
FastSelect(Graph.MW1, Graph.Tank2);
} else if (keyIndex == 16) {
FastSelect(Graph.MW1, Graph.Tank3);
} else if (keyIndex == 17) {
FastSelect(Graph.MW1, Graph.Tank4);
} else if (keyIndex == 18) {
FastSelect(Graph.MW2, Graph.Press1);
} else if (keyIndex == 19) {
FastSelect(Graph.MW2, Graph.Press2);
} else if (keyIndex == 20) {
FastSelect(Graph.MW2, Graph.Tank1);
} else if (keyIndex == 21) {
FastSelect(Graph.MW2, Graph.Tank2);
} else if (keyIndex == 22) {
FastSelect(Graph.MW2, Graph.Tank3);
} else if (keyIndex == 23) {
FastSelect(Graph.MW2, Graph.Tank4);
}
} catch (NoClearanceException) {
// Keyboard?.SetBacklight(keyIndex, Color.FromRgb(255, 0, 0));
} catch (Exception) {
// ignore
}
});
}
private void Keyboard_KeysChanged(object? sender, Xbe24Keyboard.KeyState state) {
RaiseUpdateEvent(new());
}
protected bool HasWTClearanceAA =>
(AreValvesOpen(12) && AreValvesClosed(15, 16, 20)) || // bypass
(AreValvesOpen(15, 18, 16) && AreValvesClosed(11, 12, 14, 17, 19, 20)) || // WT1
@@ -233,7 +436,9 @@ namespace PamhagenSysCtrl.Helpers {
(a && ((HasWTClearanceAA && HasT9ClearanceA) || (HasWTClearanceAB && HasT9ClearanceB))) ||
(b && ((HasWTClearanceBA && HasT9ClearanceA) || (HasWTClearanceBB && HasT9ClearanceB)));
public bool MP1HasClearance => (OverrideDryRunClearances || FillLevelMW1 != FillState.Empty) && (
public bool MP1HasClearance => MP1HasDryRunClearance && MP1HasPathClearance;
public bool MP1HasDryRunClearance => (OverrideDryRunClearances || FillLevelMW1 != FillState.Empty);
public bool MP1HasPathClearance => (
OverridePathClearances ||
(MP1HasClearancePress1 && Press1HasClearance) ||
(MP1HasClearancePress2 && Press2HasClearance) ||
@@ -261,7 +466,9 @@ namespace PamhagenSysCtrl.Helpers {
set => Actuators.MP1 = (value != MotorState.Halt && value != MotorState.Forward && value != MotorState.Backward) ? throw new ArgumentException("Invalid state for MP1") : value;
}
public bool MP2HasClearance => (OverrideDryRunClearances || FillLevelMW2 != FillState.Empty) && (
public bool MP2HasClearance => MP2HasDryRunClearance && MP2HasPathClearance;
public bool MP2HasDryRunClearance => (OverrideDryRunClearances || FillLevelMW2 != FillState.Empty);
public bool MP2HasPathClearance => (
OverridePathClearances ||
(MP2HasClearancePress1 && Press1HasClearance) ||
(MP2HasClearancePress2 && Press2HasClearance) ||
+4
View File
@@ -34,6 +34,10 @@ namespace PamhagenSysCtrl.Helpers {
protected readonly T1 Plc;
public string PortName => Plc.PortName;
public T1.Status LastPlcStatus => Plc.LastStatus;
public T1Error LastPlcError => Plc.LastError;
public PamhagenPlc(string portName, Parity parity = Parity.Odd) {
Plc = new T1(portName, parity);
}
+63 -40
View File
@@ -1,7 +1,6 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace PamhagenSysCtrl.Helpers.Pipeline {
public class PipeJoin : INode {
@@ -12,11 +11,12 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
public double CenterX { get; set; }
public double CenterY { get; set; }
public string Label => "";
public Brush? Highlight { get; set; }
public ISet<IEdge> Inputs { get; init; }
public ISet<IEdge> Outputs { get; init; }
private Polygon? _rect;
private System.Windows.Shapes.Path? _rect;
public PipeJoin(double x, double y) {
CenterX = x;
@@ -26,8 +26,8 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
}
public void Draw(Canvas canvas) {
_rect = new Polygon() {
Fill = Brushes.Black,
_rect = new System.Windows.Shapes.Path() {
Fill = Brushes.DarkGray,
Stroke = Brushes.Black,
};
canvas.Children.Add(_rect);
@@ -43,43 +43,66 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
var il = Inputs.Any(e => (e.Start.CenterX < CenterX && (e.Orientation || e.Start.CenterY == CenterY)) || (e.IsTwoWay && e.End.CenterX < CenterX && (e.Orientation || e.End.CenterY == CenterY)));
var ol = Outputs.Any(e => (e.End.CenterX < CenterX && (!e.Orientation || e.End.CenterY == CenterY)) || (e.IsTwoWay && e.Start.CenterX < CenterX && (!e.Orientation || e.Start.CenterY == CenterY)));
var points = new List<Point> {
new(0, 0)
};
if (ot && it) {
points.AddRange([new(SIZE / 2 - POINT, 0), new(SIZE / 2, -POINT / 2), new(SIZE / 2, POINT / 2), new(SIZE / 2 + POINT, 0)]);
} else if (ot) {
points.AddRange([new(SIZE / 2 - POINT, 0), new(SIZE / 2, -POINT), new(SIZE / 2 + POINT, 0)]);
} else if (it) {
points.AddRange([new(SIZE / 2 - POINT, 0), new(SIZE / 2, POINT), new(SIZE / 2 + POINT, 0)]);
}
points.Add(new(SIZE, 0));
if (or && ir) {
points.AddRange([new(SIZE, SIZE / 2 - POINT), new(SIZE + POINT / 2, SIZE / 2), new(SIZE - POINT / 2, SIZE / 2), new(SIZE, SIZE / 2 + POINT)]);
} else if (or) {
points.AddRange([new(SIZE, SIZE / 2 - POINT), new(SIZE + POINT, SIZE / 2), new(SIZE, SIZE / 2 + POINT)]);
} else if (ir) {
points.AddRange([new(SIZE, SIZE / 2 - POINT), new(SIZE - POINT, SIZE / 2), new(SIZE, SIZE / 2 + POINT)]);
}
points.Add(new(SIZE, SIZE));
if (ob && ib) {
points.AddRange([new(SIZE / 2 + POINT, SIZE), new(SIZE / 2, SIZE + POINT / 2), new(SIZE / 2, SIZE - POINT / 2), new(SIZE / 2 - POINT, SIZE)]);
} else if (ob) {
points.AddRange([new(SIZE / 2 + POINT, SIZE), new(SIZE / 2, SIZE + POINT), new(SIZE / 2 - POINT, SIZE)]);
} else if (ib) {
points.AddRange([new(SIZE / 2 + POINT, SIZE), new(SIZE / 2, SIZE - POINT), new(SIZE / 2 - POINT, SIZE)]);
}
points.Add(new(0, SIZE));
if (ol && il) {
points.AddRange([new(0, SIZE / 2 + POINT), new(-POINT / 2, SIZE / 2), new(POINT / 2, SIZE / 2), new(0, SIZE / 2 - POINT)]);
} else if (ol) {
points.AddRange([new(0, SIZE / 2 + POINT), new(-POINT, SIZE / 2), new(0, SIZE / 2 - POINT)]);
} else if (il) {
points.AddRange([new(0, SIZE / 2 + POINT), new(POINT, SIZE / 2), new(0, SIZE / 2 - POINT)]);
var size = new Size(SIZE / 2 * scale, SIZE / 2 * scale);
var geo = new StreamGeometry();
using (var ctx = geo.Open()) {
var points = new List<Point> {
new(0, 0)
};
ctx.BeginFigure(new((SIZE / 2 - POINT) * scale, 0), true, false);
if (ot && it) {
points.AddRange([new(SIZE / 2, -POINT / 2), new(SIZE / 2, POINT / 2), new(SIZE / 2 + POINT, 0)]);
} else if (ot) {
ctx.PolyLineTo([new(SIZE / 2 * scale, -POINT * scale), new((SIZE / 2 + POINT) * scale, 0)], true, false);
} else if (it) {
ctx.PolyLineTo([new(SIZE / 2 * scale, POINT * scale), new((SIZE / 2 + POINT) * scale, 0)], true, false);
} else {
ctx.ArcTo(new((SIZE / 2 + POINT) * scale, 0), size, 0, false, SweepDirection.Clockwise, true, false);
}
ctx.ArcTo(new(SIZE * scale, (SIZE / 2 - POINT) * scale), size, 0, false, SweepDirection.Clockwise, true, false);
points.Add(new(SIZE, 0));
if (or && ir) {
points.AddRange([new(SIZE, SIZE / 2 - POINT), new(SIZE + POINT / 2, SIZE / 2), new(SIZE - POINT / 2, SIZE / 2), new(SIZE, SIZE / 2 + POINT)]);
} else if (or) {
ctx.PolyLineTo([new((SIZE + POINT) * scale, SIZE / 2 * scale), new(SIZE * scale, (SIZE / 2 + POINT) * scale)], true, false);
} else if (ir) {
ctx.PolyLineTo([new((SIZE - POINT) * scale, SIZE / 2 * scale), new(SIZE * scale, (SIZE / 2 + POINT) * scale)], true, false);
} else {
ctx.ArcTo(new(SIZE * scale, (SIZE / 2 + POINT) * scale), size, 0, false, SweepDirection.Clockwise, true, false);
}
ctx.ArcTo(new((SIZE / 2 + POINT) * scale, SIZE * scale), size, 0, false, SweepDirection.Clockwise, true, false);
points.Add(new(SIZE, SIZE));
if (ob && ib) {
points.AddRange([new(SIZE / 2 + POINT, SIZE), new(SIZE / 2, SIZE + POINT / 2), new(SIZE / 2, SIZE - POINT / 2), new(SIZE / 2 - POINT, SIZE)]);
} else if (ob) {
ctx.PolyLineTo([new(SIZE / 2 * scale, (SIZE + POINT) * scale), new((SIZE / 2 - POINT) * scale, SIZE * scale)], true, false);
} else if (ib) {
ctx.PolyLineTo([new(SIZE / 2 * scale, (SIZE - POINT) * scale), new((SIZE / 2 - POINT) * scale, SIZE * scale)], true, false);
} else {
ctx.ArcTo(new((SIZE / 2 - POINT) * scale, SIZE * scale), size, 0, false, SweepDirection.Clockwise, true, false);
}
ctx.ArcTo(new(0, (SIZE / 2 + POINT) * scale), size, 0, false, SweepDirection.Clockwise, true, false);
points.Add(new(0, SIZE));
if (ol && il) {
points.AddRange([new(0, SIZE / 2 + POINT), new(-POINT / 2, SIZE / 2), new(POINT / 2, SIZE / 2), new(0, SIZE / 2 - POINT)]);
} else if (ol) {
ctx.PolyLineTo([new(-POINT * scale, SIZE / 2 * scale), new(0, (SIZE / 2 - POINT) * scale)], true, false);
} else if (il) {
ctx.PolyLineTo([new(POINT * scale, SIZE / 2 * scale), new(0, (SIZE / 2 - POINT) * scale)], true, false);
} else {
ctx.ArcTo(new(0, (SIZE / 2 - POINT) * scale), size, 0, false, SweepDirection.Clockwise, true, false);
}
ctx.ArcTo(new((SIZE / 2 - POINT) * scale, 0), size, 0, false, SweepDirection.Clockwise, true, false);
}
_rect?.Points = [.. points.Select(p => new Point(p.X * scale, p.Y * scale))];
_rect?.StrokeThickness = 2;
_rect?.Data = geo;
// [.. points.Select(p => new Point(p.X * scale, p.Y * scale))];
_rect?.StrokeThickness = Graph.ToBorder(scale);
_rect?.SetValue(Canvas.LeftProperty, (CenterX - SIZE / 2) * scale);
_rect?.SetValue(Canvas.TopProperty, (CenterY - SIZE / 2) * scale);
}
@@ -89,7 +112,7 @@ namespace PamhagenSysCtrl.Helpers.Pipeline {
}
public void Update(bool isHovering, bool isPressed) {
_rect?.Stroke = isHovering ? Brushes.Black : Brushes.Black;
_rect?.Fill = Highlight ?? Brushes.DarkGray;
}
}
}
+10 -4
View File
@@ -10,12 +10,16 @@ namespace PamhagenSysCtrl.Helpers {
protected SerialPort Serial;
public string PortName => Serial.PortName;
public Status LastStatus { get; private set; }
public T1Error LastError { get; private set; }
public T1(string portName, Parity parity = Parity.Odd) {
Serial = new SerialPort(portName, 9600, parity, 8, StopBits.One) {
Handshake = Handshake.None,
NewLine = "\r",
ReadTimeout = 1000,
WriteTimeout = 1000,
ReadTimeout = 500,
WriteTimeout = 500,
DtrEnable = true,
RtsEnable = true,
Encoding = Encoding.ASCII,
@@ -97,14 +101,16 @@ namespace PamhagenSysCtrl.Helpers {
public async Task<Status> StatusRead() {
await SendCommand("ST", checksum: false);
var (_, res) = await ReceiveResponse("ST");
return new Status((Mode)(res[3] - '0'), ((res[0] - '0') << 4) | (res[1] - '0'));
LastStatus = new Status((Mode)(res[3] - '0'), ((res[0] - '0') << 4) | (res[1] - '0'));
return LastStatus;
}
public async Task<T1Error> ErrorStatusRead() {
await SendCommand("ER", checksum: false);
var (_, res) = await ReceiveResponse("ER");
var code = Convert.ToInt32(res, 10);
return Enum.IsDefined(typeof(T1Error), code) ? (T1Error)code : T1Error.UNKNOWN_ERROR;
LastError = Enum.IsDefined(typeof(T1Error), code) ? (T1Error)code : T1Error.UNKNOWN_ERROR;
return LastError;
}
public Task<ushort[]> DataRead(string register, int length) {
+114
View File
@@ -0,0 +1,114 @@
using PIEHidNetCore;
using System.IO;
using System.Windows.Media;
namespace PamhagenSysCtrl.Helpers {
public class Xbe24Keyboard : IDisposable, PIEDataHandler, PIEErrorHandler {
public enum LedBank { UPPER = 0, LOWER = 1 }
public record struct KeyState(byte[] Rows) {
public readonly bool GetKey(int n) => (Rows[n / 6] & (1 << (n % 6))) != 0;
}
protected readonly PIEDevice XbeDevice;
private KeyState LastKeyState = new(new byte[4]);
private readonly (Color Color, bool Flashing)[] LastStates = new (Color, bool)[48];
public bool IsKeyPressed(int keyIndex) => LastKeyState.GetKey(keyIndex);
public event EventHandler<int>? KeyDown;
public event EventHandler<int>? KeyUp;
public event EventHandler<KeyState>? KeysChanged;
public Xbe24Keyboard(int pid = 0x0555) {
foreach (var device in PIEDevice.EnumeratePIE()) {
if (device.Pid == pid && device.HidUsagePage == 0x0C && device.HidUsage == 1 && device.WriteLength > 1) {
XbeDevice = device;
break;
}
}
if (XbeDevice == null)
throw new ArgumentException($"No XBE keyboard with id 05f3:{pid:x4}");
int res;
if ((res = XbeDevice.SetupInterface()) != 0)
throw new IOException($"Unable to SetupInterface for XBE keyboard: {res}");
if ((res = XbeDevice.SetDataCallback(this)) != 0)
throw new IOException($"Unable to SetDataCallback for XBE keyboard: {res}");
if ((res = XbeDevice.SetErrorCallback(this)) != 0)
throw new IOException($"Unable to SetErrorCallback for XBE keyboard: {res}");
}
public void Dispose() {
XbeDevice?.CloseInterface();
}
public void HandlePIEHidData(byte[] data, PIEDevice sourceDevice, int error) {
if (sourceDevice != XbeDevice)
return;
var current = new KeyState(data[3..8]);
var last = LastKeyState;
LastKeyState = current;
HandleKeyStateChange(current, last);
}
public void HandlePIEHidError(PIEDevice sourceDevice, int error) {
if (sourceDevice != XbeDevice)
return;
// TODO
}
protected void HandleKeyStateChange(KeyState current, KeyState last) {
for (int i = 0; i < 24; i++) {
if (!last.GetKey(i) && current.GetKey(i)) {
KeyDown?.Invoke(this, i);
} else if (last.GetKey(i) && !current.GetKey(i)) {
KeyUp?.Invoke(this, i);
}
}
KeysChanged?.Invoke(this, current);
}
protected void SendCommand(byte command, byte[] payload) {
byte[] data = new byte[XbeDevice.WriteLength];
data[1] = command;
Array.Copy(payload, 0, data, 2, Math.Min(payload.Length, data.Length - 2));
int res;
do {
res = XbeDevice.WriteData(data);
} while (res == 404);
if (res != 0)
throw new IOException($"Unable to WriteData for XBE keyboard: {res}");
}
public void SetBacklight(int keyIndex, Color color, bool flashing = false) {
SetBacklight(keyIndex, LedBank.UPPER, color, flashing);
SetBacklight(keyIndex, LedBank.LOWER, color, flashing);
}
public void SetBacklight(int keyIndex, LedBank bank, Color color, bool flashing = false) {
if (LastStates[keyIndex * 2 + (int)bank] == (color, flashing))
return;
SendCommand(165, [(byte)keyIndex, (byte)bank, color.R, color.G, color.B, (byte)(flashing ? 1 : 0)]);
LastStates[keyIndex * 2 + (int)bank] = (color, flashing);
}
public void SetBacklight(Color color) {
SetBacklight(LedBank.UPPER, color);
SetBacklight(LedBank.LOWER, color);
}
public void SetBacklight(LedBank bank, Color color) {
SendCommand(166, [(byte)bank, color.R, color.G, color.B]);
for (int i = 0; i < LastStates.Length; i++)
LastStates[i] = (color, false);
}
public void SetFashFrequency(int ms) {
SendCommand(180, [(byte)(ms / 16)]);
}
}
}
Binary file not shown.
+3
View File
@@ -18,6 +18,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="10.0.11" />
<PackageReference Include="System.IO.Ports" Version="10.0.11" />
<Reference Include="PIEHidNetCore">
<HintPath>PIEHidNetCore.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+38 -1
View File
@@ -227,7 +227,44 @@
</Grid>
<StatusBar Grid.Row="2" BorderThickness="0,1,0,0" BorderBrush="Gray">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem>
<TextBlock>
SPS-Verbindung: <Run x:Name="Status_Connection" Text="-"/>
</TextBlock>
</StatusBarItem>
<Separator Grid.Column="1"/>
<StatusBarItem Grid.Column="2">
<TextBlock>
SPS-Status: <Run x:Name="Status_Plc" Text="-"/>
</TextBlock>
</StatusBarItem>
<Separator Grid.Column="3"/>
<StatusBarItem Grid.Column="4">
<TextBlock>
Druckluft: <Run x:Name="Status_CompressedAir" Text="-"/>
</TextBlock>
</StatusBarItem>
<Separator Grid.Column="5"/>
<StatusBarItem Grid.Column="6">
<TextBlock>
Ventile offen: <Run x:Name="Status_ValvesOpen" Text="-"/>
</TextBlock>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
@@ -11,7 +11,7 @@ namespace PamhagenSysCtrl.Windows {
private const double _minMove = 50;
private readonly PamhagenGraph Graph;
private static PamhagenGraph Graph => App.Plant!.Graph;
private Point? _lastMousePos;
private bool _mouseDragging;
@@ -50,26 +50,25 @@ namespace PamhagenSysCtrl.Windows {
public PlantSchemeWindow() {
InitializeComponent();
Menu_Help_CheckForUpdates.IsEnabled = App.Config.UpdateUrl != null;
Graph = new();
Graph.Draw(SchemeCanvas, RoundedScale);
FastSelectPaths = new Dictionary<Button, (ISource, Pump, ISink)> {
[FastSelectButton_MW1_Press1] = (Graph.MW1, Graph.MP1, Graph.Press1),
[FastSelectButton_MW2_Press1] = (Graph.MW2, Graph.MP2, Graph.Press1),
[FastSelectButton_MW1_Press2] = (Graph.MW1, Graph.MP1, Graph.Press2),
[FastSelectButton_MW2_Press2] = (Graph.MW2, Graph.MP2, Graph.Press2),
[FastSelectButton_MW1_Tank1] = (Graph.MW1, Graph.MP1, Graph.Tank1),
[FastSelectButton_MW2_Tank1] = (Graph.MW2, Graph.MP2, Graph.Tank1),
[FastSelectButton_MW1_Tank2] = (Graph.MW1, Graph.MP1, Graph.Tank2),
[FastSelectButton_MW2_Tank2] = (Graph.MW2, Graph.MP2, Graph.Tank2),
[FastSelectButton_MW1_Tank3] = (Graph.MW1, Graph.MP1, Graph.Tank3),
[FastSelectButton_MW2_Tank3] = (Graph.MW2, Graph.MP2, Graph.Tank3),
[FastSelectButton_MW1_Tank4] = (Graph.MW1, Graph.MP1, Graph.Tank4),
[FastSelectButton_MW2_Tank4] = (Graph.MW2, Graph.MP2, Graph.Tank4),
[FastSelectButton_MW1_Tank5] = (Graph.MW1, Graph.MP1, Graph.Tank5),
[FastSelectButton_MW2_Tank5] = (Graph.MW2, Graph.MP2, Graph.Tank5),
[FastSelectButton_MW1_Tank1] = (Graph.MW1, Graph.MP1, Graph.Tank1),
[FastSelectButton_MW2_Tank1] = (Graph.MW2, Graph.MP2, Graph.Tank1),
[FastSelectButton_MW1_Tank2] = (Graph.MW1, Graph.MP1, Graph.Tank2),
[FastSelectButton_MW2_Tank2] = (Graph.MW2, Graph.MP2, Graph.Tank2),
[FastSelectButton_MW1_Tank3] = (Graph.MW1, Graph.MP1, Graph.Tank3),
[FastSelectButton_MW2_Tank3] = (Graph.MW2, Graph.MP2, Graph.Tank3),
[FastSelectButton_MW1_Tank4] = (Graph.MW1, Graph.MP1, Graph.Tank4),
[FastSelectButton_MW2_Tank4] = (Graph.MW2, Graph.MP2, Graph.Tank4),
[FastSelectButton_MW1_Tank5] = (Graph.MW1, Graph.MP1, Graph.Tank5),
[FastSelectButton_MW2_Tank5] = (Graph.MW2, Graph.MP2, Graph.Tank5),
};
App.Plant?.Update += OnUpdate;
InitializePaths();
App.Plant?.InitializePaths();
}
private void OnClosed(object sender, EventArgs evt) {
@@ -96,7 +95,7 @@ namespace PamhagenSysCtrl.Windows {
Menu_Settings_OverridePathClearances.IsChecked = false;
Menu_Settings_OverrideDryRunClearances.IsChecked = false;
InitializePaths();
App.Plant?.InitializePaths();
}
OnUpdate(null, null);
}
@@ -211,19 +210,12 @@ namespace PamhagenSysCtrl.Windows {
UpdateScheme(evt.GetPosition(SchemeCanvas), evt.LeftButton == MouseButtonState.Pressed);
}
private void FastSelectButton_Click(object sender, RoutedEventArgs evt) {
private void FastSelectButton_Click(object sender, RoutedEventArgs? evt) {
if (sender is not Button b || !FastSelectPaths.TryGetValue(b, out var val))
return;
if (Graph.SelectedPaths.Select(p => (Path?)p).FirstOrDefault(p => p?.Start == val.Source, null) is Path p1) {
Graph.RemovePath(p1);
if (p1.Hops.Last().Node == val.Sink)
return;
}
if (Graph.GetFreePath([val.Source, val.Sink]) is Path p2) {
Graph.AddPath(p2);
_lastSource = null;
}
App.Plant?.FastSelect(val.Source, val.Sink);
OnUpdate(null, null);
_lastSource = null;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs evt) {
@@ -492,6 +484,7 @@ namespace PamhagenSysCtrl.Windows {
SetFastSelectButton(b,
paths.Any(p => p.Start == e.Source && p.Hops.Last().Node == e.Sink) ? (hoverPathDeactivate?.Start == e.Source && hoverPathDeactivate?.Hops.Last().Node == e.Sink) ? 2 : 1 : 0,
e.Pump.IsActive || Graph.GetFreePath([e.Source, e.Sink], overrideStart: true) == null);
}
private ISet<INode> UpdateScheme(Point pos, bool down) {
@@ -502,6 +495,8 @@ namespace PamhagenSysCtrl.Windows {
foreach (var n in Graph.Nodes) {
if (n is Pump p) {
p.Highlight = null;
} else if (n is PipeJoin j) {
j.Highlight = null;
}
}
@@ -509,13 +504,12 @@ namespace PamhagenSysCtrl.Windows {
SetFastSelectButton(FastSelectButton_EntryTrough_Stop, App.Plant?.IsTroughAugerActive ?? false ? 0 : 2);
SetFastSelectButton(FastSelectButton_MW1, App.Plant?.IsMW1Selected ?? false ? 1 : 0);
SetFastSelectButton(FastSelectButton_MW2, App.Plant?.IsMW2Selected ?? false ? 1 : 0);
SetFastSelectButton(FastSelectButton_MP1_Forward, App.Plant?.MP1 == MotorState.Forward ? 1 : 0, !(App.Plant?.MP1HasClearance ?? false));
SetFastSelectButton(FastSelectButton_MP1_Stop, App.Plant?.MP1 == MotorState.Halt ? 2 : 0);
SetFastSelectButton(FastSelectButton_MP2_Forward, App.Plant?.MP2 == MotorState.Forward ? 1 : 0, !(App.Plant?.MP2HasClearance ?? false));
SetFastSelectButton(FastSelectButton_MP2_Stop, App.Plant?.MP2 == MotorState.Halt ? 2 : 0);
foreach (var b in FastSelectPaths) {
SetFastSelectButton(b.Key, 0, false);
}
_hover = Graph.GetHover(pos.X, pos.Y, RoundedScale);
var hoverButtons = FastSelectPaths.Where(b => b.Key.IsMouseOver).ToDictionary();
@@ -584,8 +578,8 @@ namespace PamhagenSysCtrl.Windows {
var paths = Graph.SelectedPaths;
if (HoveringPath is Path hp) paths = paths.Append(hp);
foreach (var b in FastSelectPaths.Keys) {
UpdateFastSelectPathButton(paths, hoverPathDeactivate, b);
foreach (var b in FastSelectPaths) {
UpdateFastSelectPathButton(paths, hoverPathDeactivate, b.Key);
}
Graph.Update(_hover, down);
@@ -593,35 +587,13 @@ namespace PamhagenSysCtrl.Windows {
}
public void OnUpdate(object? sender, PlantEventArgs? evt) {
if (sender is not PamhagenPlant plant) return;
if (App.Plant is PamhagenPlant p) {
Status_Connection.Text = $"Ja ({p.PlcPortName}" + (p.LastRttNs != null ? $", {p.LastRttNs / 1000000.0:N0} ms" : "") + ")";
Status_Plc.Text = $"{p.LastPlcStatus?.Mode} ({p.LastPlcStatus?.Flags:X3}) / {p.LastPlcError?.GetName()} ({(int?)p.LastPlcError:0000})";
Status_CompressedAir.Text = Enumerable.Range(1, 57).Any(p.IsValveClosed) ? "Ein" : Enumerable.Range(1, 57).Any(p.WantValveClosed) ? "Aus" : "?";
Status_ValvesOpen.Text = Enumerable.Range(1, 57).Count(p.IsValveOpen) + " / " + Enumerable.Range(1, 57).Count(p.WantValveOpen) + " / 57";
}
UpdateScheme(Mouse.GetPosition(SchemeCanvas), Mouse.LeftButton == MouseButtonState.Pressed);
}
public void InitializePaths() {
var add = ReconstructSelectedPaths().ToList();
var remove = Graph.SelectedPaths.ToList();
foreach (var p in remove) Graph.RemovePath(p);
for (int i = 1; i <= 57; i++) {
App.Plant?.CloseV(i);
}
foreach (var p in add) {
if (p.Hops.Any(h => h.Node is Valve v && v.IsLocked)) continue;
Graph.AddPath(p);
}
}
public IEnumerable<Path> ReconstructSelectedPaths() {
if (App.Plant is not PamhagenPlant p) yield break;
var sources = Graph.Nodes.Where(n => n is ISource).ToList();
var sinks = Graph.Nodes.Where(n => n is ISink).ToList();
foreach (var src in sources) {
var paths = new List<Path>();
foreach (var sink in sinks) {
paths.AddRange(Graph.GetPaths(src, sink, null, (n) => n is not ISink && (n is not Valve v || p.WantValveOpen(v.Nr))));
}
if (paths.Count == 1)
yield return paths.First();
}
}
}
}