Add ReconnectThread to reconnect to PLC and Keyboard on the fly
Test / Run tests (push) Successful in 26s

This commit is contained in:
2026-10-13 17:26:35 +02:00
parent c3f4acaf07
commit 4b9bcc6ad3
7 changed files with 158 additions and 129 deletions
-6
View File
@@ -37,12 +37,6 @@ namespace PamhagenSysCtrl {
Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0"); Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0");
Plant = new(Config.PlcPort); Plant = new(Config.PlcPort);
try {
await Plant.StartThread();
} catch (Exception exc) {
// TODO
}
base.OnStartup(evt); base.OnStartup(evt);
if (Config.UpdateUrl != null && Config.UpdateAuto) { if (Config.UpdateUrl != null && Config.UpdateAuto) {
+129 -95
View File
@@ -32,40 +32,79 @@ namespace PamhagenSysCtrl.Helpers {
protected Xbe24Keyboard? Keyboard; protected Xbe24Keyboard? Keyboard;
public PamhagenGraph Graph; public PamhagenGraph Graph;
public bool IsPlcConnected => Plc != null;
public bool IsKeyboardConnected => Keyboard != null;
protected bool IsRunning = true; protected bool IsRunning = true;
protected Thread? BackgroundThread; protected Thread? ReconnectThread;
protected Thread? PlcThread;
public event EventHandler<PlantEventArgs>? Update; public event EventHandler<PlantEventArgs>? Update;
public string? PlcPortName => Plc?.PortName; public readonly string PlcComPort;
public string? PlcConnectionError { get; private set; }
public T1.Status? LastPlcStatus => Plc?.LastStatus; public T1.Status? LastPlcStatus => Plc?.LastStatus;
public T1Error? LastPlcError => Plc?.LastError; public T1Error? LastPlcError => Plc?.LastError;
public long? LastRttNs { get; private set; } public long? LastRttNs { get; private set; }
public PamhagenState State => Plc?.State ?? default; // FIXME public PamhagenState State => Plc?.State ?? default;
public bool ManualControlMode { get => Plc?.ManualControlMode ?? false; set => Plc?.ManualControlMode = value; } public bool ManualControlMode { get => Plc?.ManualControlMode ?? false; set => Plc?.ManualControlMode = value; }
public bool OverridePathClearances { get => Plc?.OverridePathClearances ?? false; set => Plc?.OverridePathClearances = value; } public bool OverridePathClearances { get => Plc?.OverridePathClearances ?? false; set => Plc?.OverridePathClearances = value; }
public bool OverrideDryRunClearances { get => Plc?.OverrideDryRunClearances ?? false; set => Plc?.OverrideDryRunClearances = value; } public bool OverrideDryRunClearances { get => Plc?.OverrideDryRunClearances ?? false; set => Plc?.OverrideDryRunClearances = value; }
public PamhagenPlant(string portName) { public PamhagenPlant(string comPort) {
try { PlcComPort = comPort;
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(); 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 += UpdateKeyboardBacklights; Update += UpdateKeyboardBacklights;
ReconnectThread = new Thread(new ParameterizedThreadStart(ReconnectLoop));
ReconnectThread.Start();
}
protected async void ReconnectLoop(object? parameters) {
while (IsRunning) {
if (Plc == null) {
PlcThread?.Join();
PlcThread = null;
PamhagenPlc? tmp = null;
try {
tmp = new PamhagenPlc(PlcComPort);
await tmp.Initialize();
Plc = tmp;
PlcConnectionError = null;
PlcThread = new Thread(new ParameterizedThreadStart(PlcLoop));
PlcThread.Start();
} catch (Exception exc) {
tmp?.Dispose();
PlcConnectionError = $"{exc.GetType().Name}: {exc.Message}";
if (exc is FileNotFoundException) {
// COM port does not exist - ignore
PlcConnectionError = "Port existiert nicht";
} else if (exc is UnauthorizedAccessException) {
// COM port exists, but is used by other process - ignore
PlcConnectionError = "Port wird bereits verwendet";
} else if (exc is TimeoutException) {
// COM port exists, but PLC not responding - ignore
PlcConnectionError = "Keine Rückmeldung von SPS";
} else {
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);
}
}
RaiseUpdateEvent(new());
}
if (Keyboard == null) {
try {
Keyboard = new();
Keyboard.KeyDown += Keyboard_KeyDown;
Keyboard.KeysChanged += Keyboard_KeysChanged;
Keyboard.SetFashFrequency(250);
} catch { }
}
await Task.Delay(100);
}
} }
protected virtual void RaiseUpdateEvent(PlantEventArgs evt) { protected virtual void RaiseUpdateEvent(PlantEventArgs evt) {
@@ -132,22 +171,17 @@ namespace PamhagenSysCtrl.Helpers {
k.SetBacklight((int)Xbe24Key.STOP_MP1, highlightOffMP1 ? Color.FromRgb(255, 32, 0) : !State.MP1_IsActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4)); k.SetBacklight((int)Xbe24Key.STOP_MP1, highlightOffMP1 ? Color.FromRgb(255, 32, 0) : !State.MP1_IsActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4));
} }
public async Task StartThread() {
if (Plc == null) throw new Exception();
await Plc.Initialize();
BackgroundThread = new Thread(new ParameterizedThreadStart(BackgroundLoop));
BackgroundThread.Start();
}
public void Dispose() { public void Dispose() {
IsRunning = false; IsRunning = false;
BackgroundThread?.Join(); ReconnectThread?.Join();
PlcThread?.Join();
Plc?.Dispose(); Plc?.Dispose();
Keyboard?.Dispose(); Keyboard?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
protected async void BackgroundLoop(object? parameters) { protected async void PlcLoop(object? parameters) {
App.MainDispatcher?.Invoke(InitializePaths);
while (IsRunning && Plc != null) { while (IsRunning && Plc != null) {
var start = DateTime.Now; var start = DateTime.Now;
try { try {
@@ -160,22 +194,24 @@ namespace PamhagenSysCtrl.Helpers {
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message; if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
if (exc is TimeoutException or IOException) { if (exc is TimeoutException or IOException) {
PlcConnectionError = $"{exc.GetType().Name}: {exc.Message}";
Plc?.Dispose(); Plc?.Dispose();
Plc = null; Plc = null;
IsRunning = false;
} else { } else {
await Task.Delay(1000); await Task.Delay(1000);
} }
} }
} }
try { if (Plc != null) {
if (Plc != null) try {
await Plc.Shutdown(); await Plc.Shutdown();
} catch (Exception exc) { } catch (Exception exc) {
var str = "Beim Schließen der Verbindung zur SPS ist ein Fehler aufgetreten:\n\n" + exc.Message; var str = "Beim Schließen der Verbindung zur SPS ist ein Fehler aufgetreten:\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message; if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
RaiseUpdateEvent(new());
} }
public void InitializePaths() { public void InitializePaths() {
@@ -219,69 +255,67 @@ namespace PamhagenSysCtrl.Helpers {
} }
private void Keyboard_KeyDown(object? sender, int keyIndex) { private void Keyboard_KeyDown(object? sender, int keyIndex) {
if (Plc == null) return;
var key = (Xbe24Key)keyIndex; var key = (Xbe24Key)keyIndex;
App.MainDispatcher?.BeginInvoke(() => { try {
if (Plc == null) return; if (key == Xbe24Key.START_REBLER) {
try { Plc.StartRebler();
if (key == Xbe24Key.START_REBLER) { } else if (key == Xbe24Key.STOP_REBLER) {
Plc.StartRebler(); if (State.Rebler_IsActive) {
} else if (key == Xbe24Key.STOP_REBLER) { Plc.StopRebler();
if (State.Rebler_IsActive) { } else {
Plc.StopRebler(); Plc.StopAuger1();
} else { if (!State.PressCrossAuger_IsActive) {
Plc.StopAuger1(); Plc.StopAuger2();
if (!State.PressCrossAuger_IsActive) { Plc.StopAuger3();
Plc.StopAuger2();
Plc.StopAuger3();
}
} }
} else if (key == Xbe24Key.START_TROUGH_AUGER) {
Plc.StartTroughAuger();
} else if (key == Xbe24Key.STOP_TROUGH_AUGER) {
Plc.StopTroughAuger();
} else if (key == Xbe24Key.SELECT_MW2) {
Plc.SelectMW2();
} else if (key == Xbe24Key.SELECT_MW1) {
Plc.SelectMW1();
} else if (key == Xbe24Key.START_MP2) {
Plc.StartMP2Forward();
} else if (key == Xbe24Key.STOP_MP2) {
Plc.StopMP2();
} else if (key == Xbe24Key.START_MP1) {
Plc.StartMP1Forward();
} else if (key == Xbe24Key.STOP_MP1) {
Plc.StopMP1();
} else if (key == Xbe24Key.FAST_SELECT_MW1_1) {
FastSelect(Graph.MW1, Graph.Press1);
} else if (key == Xbe24Key.FAST_SELECT_MW1_2) {
FastSelect(Graph.MW1, Graph.Press2);
} else if (key == Xbe24Key.FAST_SELECT_MW1_3) {
FastSelect(Graph.MW1, Graph.Tank1);
} else if (key == Xbe24Key.FAST_SELECT_MW1_4) {
FastSelect(Graph.MW1, Graph.Tank2);
} else if (key == Xbe24Key.FAST_SELECT_MW1_5) {
FastSelect(Graph.MW1, Graph.Tank3);
} else if (key == Xbe24Key.FAST_SELECT_MW1_6) {
FastSelect(Graph.MW1, Graph.Tank4);
} else if (key == Xbe24Key.FAST_SELECT_MW2_1) {
FastSelect(Graph.MW2, Graph.Press1);
} else if (key == Xbe24Key.FAST_SELECT_MW2_2) {
FastSelect(Graph.MW2, Graph.Press2);
} else if (key == Xbe24Key.FAST_SELECT_MW2_3) {
FastSelect(Graph.MW2, Graph.Tank1);
} else if (key == Xbe24Key.FAST_SELECT_MW2_4) {
FastSelect(Graph.MW2, Graph.Tank2);
} else if (key == Xbe24Key.FAST_SELECT_MW2_5) {
FastSelect(Graph.MW2, Graph.Tank3);
} else if (key == Xbe24Key.FAST_SELECT_MW2_6) {
FastSelect(Graph.MW2, Graph.Tank4);
} }
} catch (NoClearanceException) { } else if (key == Xbe24Key.START_TROUGH_AUGER) {
// Keyboard?.SetBacklight(keyIndex, Color.FromRgb(255, 0, 0)); Plc.StartTroughAuger();
} catch (Exception) { } else if (key == Xbe24Key.STOP_TROUGH_AUGER) {
// ignore Plc.StopTroughAuger();
} else if (key == Xbe24Key.SELECT_MW2) {
Plc.SelectMW2();
} else if (key == Xbe24Key.SELECT_MW1) {
Plc.SelectMW1();
} else if (key == Xbe24Key.START_MP2) {
Plc.StartMP2Forward();
} else if (key == Xbe24Key.STOP_MP2) {
Plc.StopMP2();
} else if (key == Xbe24Key.START_MP1) {
Plc.StartMP1Forward();
} else if (key == Xbe24Key.STOP_MP1) {
Plc.StopMP1();
} else if (key == Xbe24Key.FAST_SELECT_MW1_1) {
FastSelect(Graph.MW1, Graph.Press1);
} else if (key == Xbe24Key.FAST_SELECT_MW1_2) {
FastSelect(Graph.MW1, Graph.Press2);
} else if (key == Xbe24Key.FAST_SELECT_MW1_3) {
FastSelect(Graph.MW1, Graph.Tank1);
} else if (key == Xbe24Key.FAST_SELECT_MW1_4) {
FastSelect(Graph.MW1, Graph.Tank2);
} else if (key == Xbe24Key.FAST_SELECT_MW1_5) {
FastSelect(Graph.MW1, Graph.Tank3);
} else if (key == Xbe24Key.FAST_SELECT_MW1_6) {
FastSelect(Graph.MW1, Graph.Tank4);
} else if (key == Xbe24Key.FAST_SELECT_MW2_1) {
FastSelect(Graph.MW2, Graph.Press1);
} else if (key == Xbe24Key.FAST_SELECT_MW2_2) {
FastSelect(Graph.MW2, Graph.Press2);
} else if (key == Xbe24Key.FAST_SELECT_MW2_3) {
FastSelect(Graph.MW2, Graph.Tank1);
} else if (key == Xbe24Key.FAST_SELECT_MW2_4) {
FastSelect(Graph.MW2, Graph.Tank2);
} else if (key == Xbe24Key.FAST_SELECT_MW2_5) {
FastSelect(Graph.MW2, Graph.Tank3);
} else if (key == Xbe24Key.FAST_SELECT_MW2_6) {
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) { private void Keyboard_KeysChanged(object? sender, Xbe24Keyboard.KeyState state) {
@@ -34,12 +34,12 @@ namespace PamhagenSysCtrl.Helpers.Plc {
protected readonly T1 Plc; protected readonly T1 Plc;
public string PortName => Plc.PortName; public string ComPort => Plc.PortName;
public T1.Status LastStatus => Plc.LastStatus; public T1.Status LastStatus => Plc.LastStatus;
public T1Error LastError => Plc.LastError; public T1Error LastError => Plc.LastError;
public LegacyPamhagenPlc(string portName, Parity parity = Parity.Odd) { public LegacyPamhagenPlc(string comPort, Parity parity = Parity.Odd) {
Plc = new T1(portName, parity); Plc = new T1(comPort, parity);
} }
public void Dispose() { public void Dispose() {
+6 -5
View File
@@ -13,7 +13,7 @@ namespace PamhagenSysCtrl.Helpers.Plc {
protected LegacyPamhagenPlc Plc; protected LegacyPamhagenPlc Plc;
public PamhagenState State { get; private set; } public PamhagenState State { get; private set; }
public string? PortName => Plc?.PortName; public string? ComPort => Plc?.ComPort;
public T1.Status? LastStatus => Plc?.LastStatus; public T1.Status? LastStatus => Plc?.LastStatus;
public T1Error? LastError => Plc?.LastError; public T1Error? LastError => Plc?.LastError;
@@ -23,8 +23,8 @@ namespace PamhagenSysCtrl.Helpers.Plc {
FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown
], default); ], default);
public PamhagenPlc(string portName, Parity parity = Parity.Odd) { public PamhagenPlc(string comPort, Parity parity = Parity.Odd) {
Plc = new(portName, parity); Plc = new(comPort, parity);
} }
public void Dispose() { public void Dispose() {
@@ -36,6 +36,8 @@ 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();
State = new(Sensors, () => Actuators, OverridePathClearances, OverrideDryRunClearances);
} }
public async Task Shutdown() { public async Task Shutdown() {
@@ -46,9 +48,8 @@ namespace PamhagenSysCtrl.Helpers.Plc {
UpdateInterlocks(); UpdateInterlocks();
if (Actuators != LastActuators) { if (Actuators != LastActuators) {
// FIXME race condition!
await Plc.WriteOutputs(Actuators);
LastActuators = Actuators; LastActuators = Actuators;
await Plc.WriteOutputs(LastActuators);
} }
Sensors = await Plc.ReadInputs(); Sensors = await Plc.ReadInputs();
State = new(Sensors, () => Actuators, OverridePathClearances, OverrideDryRunClearances); State = new(Sensors, () => Actuators, OverridePathClearances, OverrideDryRunClearances);
+1 -1
View File
@@ -15,7 +15,7 @@ namespace PamhagenSysCtrl.Helpers.Plc {
public FillState MW1_FillLevel => Sensors.FillLevelMW1; public FillState MW1_FillLevel => Sensors.FillLevelMW1;
public FillState MW2_FillLevel => Sensors.FillLevelMW2; public FillState MW2_FillLevel => Sensors.FillLevelMW2;
public FillState[] Tanks_FillLevel => Sensors.FillLevelTanks; public FillState[] Tanks_FillLevel => Sensors.FillLevelTanks ?? [FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown];
public FillState T1_FillLevel => Sensors.FillLevelTanks[0]; public FillState T1_FillLevel => Sensors.FillLevelTanks[0];
public FillState T2_FillLevel => Sensors.FillLevelTanks[1]; public FillState T2_FillLevel => Sensors.FillLevelTanks[1];
public FillState T3_FillLevel => Sensors.FillLevelTanks[2]; public FillState T3_FillLevel => Sensors.FillLevelTanks[2];
+4
View File
@@ -39,9 +39,13 @@ namespace PamhagenSysCtrl.Helpers {
throw new IOException($"Unable to SetDataCallback for XBE keyboard: {res}"); throw new IOException($"Unable to SetDataCallback for XBE keyboard: {res}");
if ((res = XbeDevice.SetErrorCallback(this)) != 0) if ((res = XbeDevice.SetErrorCallback(this)) != 0)
throw new IOException($"Unable to SetErrorCallback for XBE keyboard: {res}"); throw new IOException($"Unable to SetErrorCallback for XBE keyboard: {res}");
SetBacklight(Color.FromRgb(1, 1, 1));
} }
public void Dispose() { public void Dispose() {
GC.SuppressFinalize(this);
SetBacklight(Color.FromRgb(1, 1, 1));
XbeDevice?.CloseInterface(); XbeDevice?.CloseInterface();
} }
@@ -68,8 +68,10 @@ namespace PamhagenSysCtrl.Windows {
[FastSelectButton_MW1_Tank5] = (Graph.MW1, Graph.MP1, Graph.Tank5), [FastSelectButton_MW1_Tank5] = (Graph.MW1, Graph.MP1, Graph.Tank5),
[FastSelectButton_MW2_Tank5] = (Graph.MW2, Graph.MP2, Graph.Tank5), [FastSelectButton_MW2_Tank5] = (Graph.MW2, Graph.MP2, Graph.Tank5),
}; };
App.Plant?.Update += OnUpdate; if (App.Plant != null) {
App.Plant?.InitializePaths(); Status_Connection.Text = $"Nein ({App.Plant.PlcComPort})";
App.Plant.Update += OnUpdate;
}
} }
private void OnClosed(object sender, EventArgs evt) { private void OnClosed(object sender, EventArgs evt) {
@@ -584,27 +586,21 @@ namespace PamhagenSysCtrl.Windows {
public void OnUpdate(object? sender, PlantEventArgs? evt) { public void OnUpdate(object? sender, PlantEventArgs? evt) {
if (App.Plant is PamhagenPlant p) { if (App.Plant is PamhagenPlant p) {
Status_Connection.Text = $"Ja ({p.PlcPortName}" + (p.LastRttNs != null ? $", {p.LastRttNs / 1000000.0:N0} ms" : "") + ")"; if (p.IsPlcConnected) {
Status_Plc.Text = $"{p.LastPlcStatus?.Mode} ({p.LastPlcStatus?.Flags:X3}) / {p.LastPlcError?.GetName()} ({(int?)p.LastPlcError:0000})"; Status_Connection.Text = $"Ja ({p.PlcComPort}" + (p.LastRttNs != null ? $", {p.LastRttNs / 1000000.0:N0} ms" : "") + ")";
Status_CompressedAir.Text = Enumerable.Range(1, 57).Any(p.State.Valve_IsClosed) ? "Ein" : Enumerable.Range(1, 57).Any(p.State.Valve_WantClosed) ? "Aus" : "?"; Status_Plc.Text = $"{p.LastPlcStatus?.Mode} ({p.LastPlcStatus?.Flags:X3}) / {p.LastPlcError?.GetName()} ({(int?)p.LastPlcError:0000})";
Status_ValvesOpen.Text = Enumerable.Range(1, 57).Count(p.State.Valve_IsOpen) + " / " + Enumerable.Range(1, 57).Count(p.State.Valve_WantOpen) + " / 57"; Status_CompressedAir.Text = Enumerable.Range(1, 57).Any(p.State.Valve_IsClosed) ? "Ein" : Enumerable.Range(1, 57).Any(p.State.Valve_WantClosed) ? "Aus" : "?";
Status_ValvesOpen.Text = Enumerable.Range(1, 57).Count(p.State.Valve_IsOpen) + " / " + Enumerable.Range(1, 57).Count(p.State.Valve_WantOpen) + " / 57";
} else {
Status_Connection.Text = p.PlcConnectionError is string err ? $"Nein ({p.PlcComPort}, {err})" : $"Nein ({p.PlcComPort})";
Status_Plc.Text = "-";
Status_CompressedAir.Text = "-";
Status_ValvesOpen.Text = "-";
}
} }
UpdateScheme(Mouse.GetPosition(SchemeCanvas), Mouse.LeftButton == MouseButtonState.Pressed); 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 ControlledValve v && v.IsLocked)) continue;
Graph.AddPath(p);
}
}
public IEnumerable<Path> ReconstructSelectedPaths() { public IEnumerable<Path> ReconstructSelectedPaths() {
if (App.Plant is not PamhagenPlant p) yield break; if (App.Plant is not PamhagenPlant p) yield break;
var sources = Graph.Nodes.Where(n => n is ISource).ToList(); var sources = Graph.Nodes.Where(n => n is ISource).ToList();