using PamhagenSysCtrl.Helpers.Pipeline; using PamhagenSysCtrl.Helpers.Plc; using System.IO; using System.Windows; using System.Windows.Media; namespace PamhagenSysCtrl.Helpers { public class PamhagenPlant : IDisposable { public enum ShortcutKey { START_REBLER, STOP_REBLER, TOGGLE_REBLER, INDICATE_REBLER, START_WASTE_DISPOSAL, STOP_WASTE_DISPOSAL, TOGGLE_WASTE_DISPOSAL, INDICATE_WASTE_DISPOSAL, START_AUGER1, STOP_AUGER1, TOGGLE_AUGER1, INDICATE_AUGER1, START_AUGER2, STOP_AUGER2, TOGGLE_AUGER2, INDICATE_AUGER2, START_AUGER3, STOP_AUGER3, TOGGLE_AUGER3, INDICATE_AUGER3, INDICATE_PRESS_CROSS_AUGER, START_TROUGH_AUGER, STOP_TROUGH_AUGER, TOGGLE_TROUGH_AUGER, INDICATE_TROUGH_AUGER, SELECT_MW1, INDICATE_MW1_SELECTED, SELECT_MW2, INDICATE_MW2_SELECTED, INDICATE_MW1, INDICATE_MW1_EMPTY, INDICATE_MW1_FILLED, INDICATE_MW1_HALF, INDICATE_MW1_FULL, INDICATE_MW2, INDICATE_MW2_EMPTY, INDICATE_MW2_FILLED, INDICATE_MW2_HALF, INDICATE_MW2_FULL, START_STIRRER_MW1, STOP_STIRRER_MW1, TOGGLE_STIRRER_MW1, INDICATE_STIRRER_MW1, START_STIRRER_MW2, STOP_STIRRER_MW2, TOGGLE_STIRRER_MW2, INDICATE_STIRRER_MW2, START_MP1, STOP_MP1, TOGGLE_MP1, INDICATE_MP1, START_MP2, STOP_MP2, TOGGLE_MP2, INDICATE_MP2, ENABLE_P12, DISABLE_P12, TOGGLE_P12, INDICATE_P12, ENABLE_P3, DISABLE_P3, TOGGLE_P3, INDICATE_P3, ENABLE_P4, DISABLE_P4, TOGGLE_P4, INDICATE_P4, ENABLE_P5, DISABLE_P5, TOGGLE_P5, INDICATE_P5, ENABLE_P6, DISABLE_P6, TOGGLE_P6, INDICATE_P6, INDICATE_PRESS1_CLEARANCE, INDICATE_PRESS2_CLEARANCE, PATH_MW1_PRESS1, PATH_MW1_LTG3_PRESS1, PATH_MW1_LTG4_PRESS1, PATH_MW1_PRESS2, PATH_MW1_LTG3_PRESS2, PATH_MW1_LTG4_PRESS2, PATH_MW1_T1, PATH_MW1_LTG1_T1, PATH_MW1_LTG2_T1, PATH_MW1_T2, PATH_MW1_LTG1_T2, PATH_MW1_LTG2_T2, PATH_MW1_T3, PATH_MW1_LTG1_T3, PATH_MW1_LTG2_T3, PATH_MW1_T4, PATH_MW1_LTG1_T4, PATH_MW1_LTG2_T4, PATH_MW1_T5, PATH_MW1_LTG1_T5, PATH_MW1_LTG2_T5, PATH_MW1_T6, PATH_MW1_LTG1_T6, PATH_MW1_LTG2_T6, PATH_MW1_T7, PATH_MW1_LTG1_T7, PATH_MW1_LTG2_T7, PATH_MW1_T8, PATH_MW1_LTG1_T8, PATH_MW1_LTG2_T8, PATH_MW1_T9, PATH_MW1_LTG1_T9, PATH_MW1_LTG2_T9, PATH_MW2_PRESS1, PATH_MW2_LTG3_PRESS1, PATH_MW2_LTG4_PRESS1, PATH_MW2_PRESS2, PATH_MW2_LTG3_PRESS2, PATH_MW2_LTG4_PRESS2, PATH_MW2_T1, PATH_MW2_LTG1_T1, PATH_MW2_LTG2_T1, PATH_MW2_T2, PATH_MW2_LTG1_T2, PATH_MW2_LTG2_T2, PATH_MW2_T3, PATH_MW2_LTG1_T3, PATH_MW2_LTG2_T3, PATH_MW2_T4, PATH_MW2_LTG1_T4, PATH_MW2_LTG2_T4, PATH_MW2_T5, PATH_MW2_LTG1_T5, PATH_MW2_LTG2_T5, PATH_MW2_T6, PATH_MW2_LTG1_T6, PATH_MW2_LTG2_T6, PATH_MW2_T7, PATH_MW2_LTG1_T7, PATH_MW2_LTG2_T7, PATH_MW2_T8, PATH_MW2_LTG1_T8, PATH_MW2_LTG2_T8, PATH_MW2_T9, PATH_MW2_LTG1_T9, PATH_MW2_LTG2_T9, PATH_T12_PRESS1, PATH_T12_PRESS2, PATH_T12_T1, PATH_T12_T2, PATH_T12_T3, PATH_T12_T4, PATH_T12_T5, PATH_T12_T6, PATH_T12_T7, PATH_T12_T8, PATH_T12_T9, PATH_T3_PRESS1, PATH_T3_PRESS2, PATH_T3_T1, PATH_T3_T2, PATH_T3_T3, PATH_T3_T4, PATH_T3_T5, PATH_T3_T6, PATH_T3_T7, PATH_T3_T8, PATH_T3_T9, PATH_T4_PRESS1, PATH_T4_PRESS2, PATH_T4_T1, PATH_T4_T2, PATH_T4_T3, PATH_T4_T4, PATH_T4_T5, PATH_T4_T6, PATH_T4_T7, PATH_T4_T8, PATH_T4_T9, PATH_T5_PRESS1, PATH_T5_PRESS2, PATH_T5_T1, PATH_T5_T2, PATH_T5_T3, PATH_T5_T4, PATH_T5_T5, PATH_T5_T6, PATH_T5_T7, PATH_T5_T8, PATH_T5_T9, PATH_P6_PRESS1, PATH_P6_PRESS2, } protected PamhagenPlc? Plc; protected Xbe24Keyboard? Keyboard; public PamhagenGraph Graph; public bool IsPlcConnected => Plc != null; public bool IsKeyboardConnected => Keyboard != null; protected bool IsRunning = true; protected Thread? ReconnectThread; protected Thread? PlcThread; public event EventHandler? Update; public readonly string PlcComPort; public string? PlcConnectionError { get; private set; } public T1.Status? LastPlcStatus => Plc?.LastStatus; public T1Error? LastPlcError => Plc?.LastError; public long? LastRttNs { get; private set; } public PamhagenState State => Plc?.State ?? default; public bool ManualControlMode { get => Plc?.ManualControlMode ?? false; set => Plc?.ManualControlMode = value; } public bool OverridePathClearances { get => Plc?.OverridePathClearances ?? false; set => Plc?.OverridePathClearances = value; } public bool OverrideDryRunClearances { get => Plc?.OverrideDryRunClearances ?? false; set => Plc?.OverrideDryRunClearances = value; } public PamhagenPlant(string comPort) { PlcComPort = comPort; Graph = new(); 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) { App.MainDispatcher?.BeginInvoke(() => Update?.Invoke(this, evt)); } protected void UpdateKeyboardBacklights(object? sender, PlantEventArgs evt) { if (sender is not PamhagenPlant plant || plant.Keyboard is not Xbe24Keyboard k) return; // TODO update all keys k.SetBacklight(ShortcutKey.START_REBLER, State.Rebler_IsActive ? Color.FromRgb(0, 255, 0) : State.Auger1_IsActive ? Color.FromRgb(255, 255, 0) : State.WasteDisposal_IsActive ? Color.FromRgb(32, 32, 4) : Color.FromRgb(4, 32, 4)); k.SetBacklight(ShortcutKey.STOP_REBLER, !State.Rebler_IsActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4)); k.SetBacklight(ShortcutKey.START_TROUGH_AUGER, !State.TroughAuger_HasClearance ? k.IsKeyPressed(ShortcutKey.START_TROUGH_AUGER) ? Color.FromRgb(255, 32, 0) : Color.FromRgb(32, 4, 4) : State.TroughAuger_IsActive ? Color.FromRgb(0, 255, 0) : Color.FromRgb(4, 32, 4)); k.SetBacklight(ShortcutKey.STOP_TROUGH_AUGER, !State.TroughAuger_IsActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4)); k.SetBacklight(ShortcutKey.SELECT_MW2, State.MW2_WantSelected || State.MW2_IsSelected ? Color.FromRgb(0, 0, 255) : Color.FromRgb(4, 4, 16), State.MW2_WantSelected && !State.MW2_IsSelected); k.SetBacklight(ShortcutKey.SELECT_MW1, State.MW1_WantSelected || State.MW1_IsSelected ? Color.FromRgb(0, 0, 255) : Color.FromRgb(4, 4, 16), State.MW1_WantSelected && !State.MW1_IsSelected); bool prepMP1 = false, prepMP2 = false, prepP12 = false, prepP3 = false, prepP4 = false, prepP5 = false, prepP6 = false; bool hiOffMP1 = false, hiOffMP2 = false, hiOffP12 = false, hiOffP3 = false, hiOffP4 = false, hiOffP5 = false, hiOffP6 = false; foreach (var (key, d) in Graph.PathShortcuts) { if (App.Config.MapKey(key) is not int idx) continue; var isRunning = d.Pump.IsActive; // TODO consider intermediate 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 also account for valves guarding path var isReady = path.Hops.All(h => h.Node is not ControlledValve v || v.IsOpen == v.WantOpen); if (!isReady) { if (d.Pump == Graph.MP1) prepMP1 = true; if (d.Pump == Graph.MP2) prepMP2 = true; if (d.Pump == Graph.P12) prepP12 = true; if (d.Pump == Graph.P3) prepP3 = true; if (d.Pump == Graph.P4) prepP4 = true; if (d.Pump == Graph.P5) prepP5 = true; if (d.Pump == Graph.P6) prepP6 = true; } var isDefault = !path.Hops.SkipLast(1).Any(h => h.Node is not Valve && h.Node is not PipeJoin && h.Node is not Pump); 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)) { if (d.Pump == Graph.MP1) hiOffMP1 = true; if (d.Pump == Graph.MP2) hiOffMP2 = true; if (d.Pump == Graph.P12) hiOffP12 = true; if (d.Pump == Graph.P3) hiOffP3 = true; if (d.Pump == Graph.P4) hiOffP4 = true; if (d.Pump == Graph.P5) hiOffP5 = true; if (d.Pump == Graph.P6) hiOffP6 = true; } k.SetBacklight(idx, isRunning ? k.IsKeyPressed(idx) ? Color.FromRgb(255, 16, 0) : Color.FromRgb(4, 4, 4) : isFree ? Color.FromRgb(8, 8, 32) : k.IsKeyPressed(idx) ? Color.FromRgb(255, 32, 0) : Color.FromRgb(4, 4, 4)); } } if (!plant.State.MP2_HasPathClearance && plant.State.MP2_HasDryRunClearance && prepMP2) { k.SetBacklight(ShortcutKey.START_MP2, Color.FromRgb(4, 32, 4), true); } else { k.SetBacklight(ShortcutKey.START_MP2, !State.MP2_HasClearance ? k.IsKeyPressed(ShortcutKey.START_MP2) ? Color.FromRgb(255, 32, 0) : Color.FromRgb(32, 4, 4) : State.MP2_IsActive ? Color.FromRgb(0, 255, 0) : Color.FromRgb(4, 32, 4)); } if (!plant.State.MP1_HasPathClearance && plant.State.MP1_HasDryRunClearance && prepMP1) { k.SetBacklight(ShortcutKey.START_MP1, Color.FromRgb(4, 32, 4), true); } else { k.SetBacklight(ShortcutKey.START_MP1, !State.MP1_HasClearance ? k.IsKeyPressed(ShortcutKey.START_MP1) ? Color.FromRgb(255, 32, 0) : Color.FromRgb(32, 4, 4) : State.MP1_IsActive ? Color.FromRgb(0, 255, 0) : Color.FromRgb(4, 32, 4)); } k.SetBacklight(ShortcutKey.STOP_MP2, hiOffMP2 ? Color.FromRgb(255, 32, 0) : !State.MP2_IsActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4)); k.SetBacklight(ShortcutKey.STOP_MP1, hiOffMP1 ? Color.FromRgb(255, 32, 0) : !State.MP1_IsActive ? Color.FromRgb(255, 0, 0) : Color.FromRgb(32, 4, 4)); } public void Dispose() { IsRunning = false; ReconnectThread?.Join(); PlcThread?.Join(); Plc?.Dispose(); Keyboard?.Dispose(); GC.SuppressFinalize(this); } protected async void PlcLoop(object? parameters) { App.MainDispatcher?.Invoke(InitializePaths); while (IsRunning && Plc != null) { var start = DateTime.Now; try { await Plc.Update(); 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); if (exc is TimeoutException or IOException) { PlcConnectionError = $"{exc.GetType().Name}: {exc.Message}"; Plc?.Dispose(); Plc = null; } else { await Task.Delay(1000); } } } if (Plc != null) { try { await Plc.Shutdown(); } catch (Exception exc) { 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; MessageBox.Show(str, "SPS-Fehler", MessageBoxButton.OK, MessageBoxImage.Error); } } RaiseUpdateEvent(new()); } 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?.Plc?.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 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(); foreach (var sink in sinks) { paths.AddRange(Graph.GetPaths(src, sink, null, (n) => n is not ISink && (n is not ControlledValve v || State.Valve_WantOpen(v.Nr)))); } if (paths.Count == 1) yield return paths.First(); } } public void FastSelect(ISource source, INode? intermediate, 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 && (intermediate == null || p1.Hops.Any(h => h.Node == intermediate))) return; } if (Graph.GetFreePath(intermediate == null ? [source, sink] : [source, intermediate, sink]) is Pipeline.Path p2) { Graph.AddPath(p2); } } private void Keyboard_KeyDown(object? sender, int keyIndex) { if (Plc == null || App.Config.MapKey(keyIndex) is not ShortcutKey key) return; try { if (key == ShortcutKey.START_REBLER) { Plc.StartRebler(); } else if (key == ShortcutKey.STOP_REBLER) { if (State.Rebler_IsActive) { Plc.StopRebler(); } else { Plc.StopAuger1(); if (!State.PressCrossAuger_IsActive) { Plc.StopAuger2(); Plc.StopAuger3(); } } } else if (key == ShortcutKey.START_TROUGH_AUGER) { Plc.StartTroughAuger(); } else if (key == ShortcutKey.STOP_TROUGH_AUGER) { Plc.StopTroughAuger(); } else if (key == ShortcutKey.SELECT_MW2) { Plc.SelectMW2(); } else if (key == ShortcutKey.SELECT_MW1) { Plc.SelectMW1(); } else if (key == ShortcutKey.START_MP2) { Plc.StartMP2Forward(); } else if (key == ShortcutKey.STOP_MP2) { Plc.StopMP2(); } else if (key == ShortcutKey.START_MP1) { Plc.StartMP1Forward(); } else if (key == ShortcutKey.STOP_MP1) { Plc.StopMP1(); } else if (Graph.PathShortcuts.TryGetValue(key, out var path)) { FastSelect(path.Source, path.Intermediate, path.Sink); } else { return; } RaiseUpdateEvent(new()); } catch (NoClearanceException) { // Keyboard?.SetBacklight(keyIndex, Color.FromRgb(255, 0, 0)); } catch (Exception) { // ignore } } private void Keyboard_KeysChanged(object? sender, Xbe24Keyboard.KeyState state) { UpdateKeyboardBacklights(this, new()); } public void OpenV(int n) => Plc?.OpenV(n); public void CloseV(int n) => Plc?.CloseV(n); public void SelectMW1() => Plc?.SelectMW1(); public void SelectMW2() => Plc?.SelectMW2(); public void StartTroughAuger() => Plc?.StartTroughAuger(); public void StopTroughAuger() => Plc?.StopTroughAuger(); public void StartRebler() => Plc?.StartRebler(); public void StopRebler() => Plc?.StopRebler(); public void StartStirrerMW1() => Plc?.StartStirrerMW1(); public void StopStirrerMW1() => Plc?.StopStirrerMW1(); public void StartStirrerMW2() => Plc?.StartStirrerMW2(); public void StopStirrerMW2() => Plc?.StopStirrerMW2(); public void StartAuger1() => Plc?.StartAuger1(); public void StopAuger1() => Plc?.StopAuger1(); public void StartAuger2() => Plc?.StartAuger2(); public void StopAuger2() => Plc?.StopAuger2(); public void StartAuger3() => Plc?.StartAuger3(); public void StopAuger3() => Plc?.StopAuger3(); public void StartMP1Forward() => Plc?.StartMP1Forward(); public void StartMP1Backward() => Plc?.StartMP1Backward(); public void StopMP1() => Plc?.StopMP1(); public void StartMP2Forward() => Plc?.StartMP2Forward(); public void StartMP2Backward() => Plc?.StartMP2Backward(); public void StopMP2() => Plc?.StopMP2(); public void EnableP12() => Plc?.EnableP12(); public void DisableP12() => Plc?.DisableP12(); public void EnableP3() => Plc?.EnableP3(); public void DisableP3() => Plc?.DisableP3(); public void EnableP4() => Plc?.EnableP4(); public void DisableP4() => Plc?.DisableP4(); public void EnableP5() => Plc?.EnableP5(); public void DisableP5() => Plc?.DisableP5(); public void EnableP6() => Plc?.EnableP6(); public void DisableP6() => Plc?.DisableP6(); } }