diff --git a/PamhagenSysCtrl/App.xaml.cs b/PamhagenSysCtrl/App.xaml.cs index d250919..4664813 100644 --- a/PamhagenSysCtrl/App.xaml.cs +++ b/PamhagenSysCtrl/App.xaml.cs @@ -37,12 +37,6 @@ namespace PamhagenSysCtrl { Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion.Split('+')[0] ?? "0.0.0"); Plant = new(Config.PlcPort); - try { - await Plant.StartThread(); - } catch (Exception exc) { - // TODO - } - base.OnStartup(evt); if (Config.UpdateUrl != null && Config.UpdateAuto) { diff --git a/PamhagenSysCtrl/Helpers/PamhagenPlant.cs b/PamhagenSysCtrl/Helpers/PamhagenPlant.cs index 6f8a54c..15b3260 100644 --- a/PamhagenSysCtrl/Helpers/PamhagenPlant.cs +++ b/PamhagenSysCtrl/Helpers/PamhagenPlant.cs @@ -32,40 +32,79 @@ namespace PamhagenSysCtrl.Helpers { protected Xbe24Keyboard? Keyboard; public PamhagenGraph Graph; + public bool IsPlcConnected => Plc != null; + public bool IsKeyboardConnected => Keyboard != null; + protected bool IsRunning = true; - protected Thread? BackgroundThread; + protected Thread? ReconnectThread; + protected Thread? PlcThread; public event EventHandler? Update; - public string? PlcPortName => Plc?.PortName; + 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; // FIXME + 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 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); - } + public PamhagenPlant(string comPort) { + PlcComPort = comPort; 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; + 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) { @@ -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)); } - public async Task StartThread() { - if (Plc == null) throw new Exception(); - await Plc.Initialize(); - BackgroundThread = new Thread(new ParameterizedThreadStart(BackgroundLoop)); - BackgroundThread.Start(); - } - public void Dispose() { IsRunning = false; - BackgroundThread?.Join(); + ReconnectThread?.Join(); + PlcThread?.Join(); Plc?.Dispose(); Keyboard?.Dispose(); GC.SuppressFinalize(this); } - protected async void BackgroundLoop(object? parameters) { + protected async void PlcLoop(object? parameters) { + App.MainDispatcher?.Invoke(InitializePaths); while (IsRunning && Plc != null) { var start = DateTime.Now; try { @@ -160,22 +194,24 @@ namespace PamhagenSysCtrl.Helpers { 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; - IsRunning = false; } else { await Task.Delay(1000); } } } - try { - if (Plc != null) + 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); + } 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() { @@ -219,69 +255,67 @@ namespace PamhagenSysCtrl.Helpers { } private void Keyboard_KeyDown(object? sender, int keyIndex) { + if (Plc == null) return; var key = (Xbe24Key)keyIndex; - App.MainDispatcher?.BeginInvoke(() => { - if (Plc == null) return; - try { - if (key == Xbe24Key.START_REBLER) { - Plc.StartRebler(); - } else if (key == Xbe24Key.STOP_REBLER) { - if (State.Rebler_IsActive) { - Plc.StopRebler(); - } else { - Plc.StopAuger1(); - if (!State.PressCrossAuger_IsActive) { - Plc.StopAuger2(); - Plc.StopAuger3(); - } + try { + if (key == Xbe24Key.START_REBLER) { + Plc.StartRebler(); + } else if (key == Xbe24Key.STOP_REBLER) { + if (State.Rebler_IsActive) { + Plc.StopRebler(); + } else { + Plc.StopAuger1(); + if (!State.PressCrossAuger_IsActive) { + 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) { - // Keyboard?.SetBacklight(keyIndex, Color.FromRgb(255, 0, 0)); - } catch (Exception) { - // ignore + } 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) { + // Keyboard?.SetBacklight(keyIndex, Color.FromRgb(255, 0, 0)); + } catch (Exception) { + // ignore + } } private void Keyboard_KeysChanged(object? sender, Xbe24Keyboard.KeyState state) { diff --git a/PamhagenSysCtrl/Helpers/Plc/LegacyPamhagenPlc.cs b/PamhagenSysCtrl/Helpers/Plc/LegacyPamhagenPlc.cs index 5b68d2a..348fb1b 100644 --- a/PamhagenSysCtrl/Helpers/Plc/LegacyPamhagenPlc.cs +++ b/PamhagenSysCtrl/Helpers/Plc/LegacyPamhagenPlc.cs @@ -34,12 +34,12 @@ namespace PamhagenSysCtrl.Helpers.Plc { protected readonly T1 Plc; - public string PortName => Plc.PortName; + public string ComPort => Plc.PortName; public T1.Status LastStatus => Plc.LastStatus; public T1Error LastError => Plc.LastError; - public LegacyPamhagenPlc(string portName, Parity parity = Parity.Odd) { - Plc = new T1(portName, parity); + public LegacyPamhagenPlc(string comPort, Parity parity = Parity.Odd) { + Plc = new T1(comPort, parity); } public void Dispose() { diff --git a/PamhagenSysCtrl/Helpers/Plc/PamhagenPlc.cs b/PamhagenSysCtrl/Helpers/Plc/PamhagenPlc.cs index ae7a216..ff3ca0b 100644 --- a/PamhagenSysCtrl/Helpers/Plc/PamhagenPlc.cs +++ b/PamhagenSysCtrl/Helpers/Plc/PamhagenPlc.cs @@ -13,7 +13,7 @@ namespace PamhagenSysCtrl.Helpers.Plc { protected LegacyPamhagenPlc Plc; public PamhagenState State { get; private set; } - public string? PortName => Plc?.PortName; + public string? ComPort => Plc?.ComPort; public T1.Status? LastStatus => Plc?.LastStatus; 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 ], default); - public PamhagenPlc(string portName, Parity parity = Parity.Odd) { - Plc = new(portName, parity); + public PamhagenPlc(string comPort, Parity parity = Parity.Odd) { + Plc = new(comPort, parity); } public void Dispose() { @@ -36,6 +36,8 @@ namespace PamhagenSysCtrl.Helpers.Plc { await Plc.TestConnection(); Actuators = await Plc.ReadOutputs(); LastActuators = Actuators; + Sensors = await Plc.ReadInputs(); + State = new(Sensors, () => Actuators, OverridePathClearances, OverrideDryRunClearances); } public async Task Shutdown() { @@ -46,9 +48,8 @@ namespace PamhagenSysCtrl.Helpers.Plc { UpdateInterlocks(); if (Actuators != LastActuators) { - // FIXME race condition! - await Plc.WriteOutputs(Actuators); LastActuators = Actuators; + await Plc.WriteOutputs(LastActuators); } Sensors = await Plc.ReadInputs(); State = new(Sensors, () => Actuators, OverridePathClearances, OverrideDryRunClearances); diff --git a/PamhagenSysCtrl/Helpers/Plc/PamhagenState.cs b/PamhagenSysCtrl/Helpers/Plc/PamhagenState.cs index 174f382..86f5bb2 100644 --- a/PamhagenSysCtrl/Helpers/Plc/PamhagenState.cs +++ b/PamhagenSysCtrl/Helpers/Plc/PamhagenState.cs @@ -15,7 +15,7 @@ namespace PamhagenSysCtrl.Helpers.Plc { public FillState MW1_FillLevel => Sensors.FillLevelMW1; 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 T2_FillLevel => Sensors.FillLevelTanks[1]; public FillState T3_FillLevel => Sensors.FillLevelTanks[2]; diff --git a/PamhagenSysCtrl/Helpers/Xbe24Keyboard.cs b/PamhagenSysCtrl/Helpers/Xbe24Keyboard.cs index 71db586..8668c5a 100644 --- a/PamhagenSysCtrl/Helpers/Xbe24Keyboard.cs +++ b/PamhagenSysCtrl/Helpers/Xbe24Keyboard.cs @@ -39,9 +39,13 @@ namespace PamhagenSysCtrl.Helpers { 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}"); + + SetBacklight(Color.FromRgb(1, 1, 1)); } public void Dispose() { + GC.SuppressFinalize(this); + SetBacklight(Color.FromRgb(1, 1, 1)); XbeDevice?.CloseInterface(); } diff --git a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs index 6ec87b2..6981deb 100644 --- a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs +++ b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs @@ -68,8 +68,10 @@ namespace PamhagenSysCtrl.Windows { [FastSelectButton_MW1_Tank5] = (Graph.MW1, Graph.MP1, Graph.Tank5), [FastSelectButton_MW2_Tank5] = (Graph.MW2, Graph.MP2, Graph.Tank5), }; - App.Plant?.Update += OnUpdate; - App.Plant?.InitializePaths(); + if (App.Plant != null) { + Status_Connection.Text = $"Nein ({App.Plant.PlcComPort})"; + App.Plant.Update += OnUpdate; + } } private void OnClosed(object sender, EventArgs evt) { @@ -584,27 +586,21 @@ namespace PamhagenSysCtrl.Windows { public void OnUpdate(object? sender, PlantEventArgs? evt) { 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.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"; + if (p.IsPlcConnected) { + Status_Connection.Text = $"Ja ({p.PlcComPort}" + (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.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); } - 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 ReconstructSelectedPaths() { if (App.Plant is not PamhagenPlant p) yield break; var sources = Graph.Nodes.Where(n => n is ISource).ToList();