From 9b64609817273df64e447a792f417945f9bd94bf Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Tue, 7 Jul 2026 13:53:16 +0200 Subject: [PATCH] Add PamhagenSysCtrl --- .gitignore | 6 + PamhagenSysCtrl.slnx | 3 + PamhagenSysCtrl/App.xaml | 9 + PamhagenSysCtrl/App.xaml.cs | 24 +++ PamhagenSysCtrl/AssemblyInfo.cs | 10 + PamhagenSysCtrl/Helpers/Extensions.cs | 64 ++++++ PamhagenSysCtrl/Helpers/FillState.cs | 3 + PamhagenSysCtrl/Helpers/MotorState.cs | 3 + PamhagenSysCtrl/Helpers/PamhagenPlant.cs | 171 ++++++++++++++++ PamhagenSysCtrl/Helpers/PamhagenPlc.cs | 221 +++++++++++++++++++++ PamhagenSysCtrl/Helpers/T1.cs | 132 ++++++++++++ PamhagenSysCtrl/Helpers/T1Error.cs | 73 +++++++ PamhagenSysCtrl/Helpers/T1Exception.cs | 4 + PamhagenSysCtrl/PamhagenSysCtrl.csproj | 16 ++ PamhagenSysCtrl/Windows/MainWindow.xaml | 19 ++ PamhagenSysCtrl/Windows/MainWindow.xaml.cs | 67 +++++++ 16 files changed, 825 insertions(+) create mode 100644 .gitignore create mode 100644 PamhagenSysCtrl.slnx create mode 100644 PamhagenSysCtrl/App.xaml create mode 100644 PamhagenSysCtrl/App.xaml.cs create mode 100644 PamhagenSysCtrl/AssemblyInfo.cs create mode 100644 PamhagenSysCtrl/Helpers/Extensions.cs create mode 100644 PamhagenSysCtrl/Helpers/FillState.cs create mode 100644 PamhagenSysCtrl/Helpers/MotorState.cs create mode 100644 PamhagenSysCtrl/Helpers/PamhagenPlant.cs create mode 100644 PamhagenSysCtrl/Helpers/PamhagenPlc.cs create mode 100644 PamhagenSysCtrl/Helpers/T1.cs create mode 100644 PamhagenSysCtrl/Helpers/T1Error.cs create mode 100644 PamhagenSysCtrl/Helpers/T1Exception.cs create mode 100644 PamhagenSysCtrl/PamhagenSysCtrl.csproj create mode 100644 PamhagenSysCtrl/Windows/MainWindow.xaml create mode 100644 PamhagenSysCtrl/Windows/MainWindow.xaml.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..389a498 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +obj/ +bin/ +*.user +.vs +.idea +*.exe diff --git a/PamhagenSysCtrl.slnx b/PamhagenSysCtrl.slnx new file mode 100644 index 0000000..897ed88 --- /dev/null +++ b/PamhagenSysCtrl.slnx @@ -0,0 +1,3 @@ + + + diff --git a/PamhagenSysCtrl/App.xaml b/PamhagenSysCtrl/App.xaml new file mode 100644 index 0000000..2a0d55b --- /dev/null +++ b/PamhagenSysCtrl/App.xaml @@ -0,0 +1,9 @@ + + + + diff --git a/PamhagenSysCtrl/App.xaml.cs b/PamhagenSysCtrl/App.xaml.cs new file mode 100644 index 0000000..fe10df3 --- /dev/null +++ b/PamhagenSysCtrl/App.xaml.cs @@ -0,0 +1,24 @@ +using PamhagenSysCtrl.Helpers; +using System.Windows; +using System.Windows.Threading; + +namespace PamhagenSysCtrl { + public partial class App : Application { + + public static PamhagenPlant? Plant; + public static Dispatcher MainDispatcher; + + public App() : + base() { + MainDispatcher = Dispatcher; + } + + protected override async void OnStartup(StartupEventArgs evt) { + base.OnStartup(evt); + } + + protected async void Application_Exit(object sender, ExitEventArgs evt) { + Plant?.Dispose(); + } + } +} diff --git a/PamhagenSysCtrl/AssemblyInfo.cs b/PamhagenSysCtrl/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/PamhagenSysCtrl/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/PamhagenSysCtrl/Helpers/Extensions.cs b/PamhagenSysCtrl/Helpers/Extensions.cs new file mode 100644 index 0000000..3b52fc3 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/Extensions.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Runtime.InteropServices; + +namespace PamhagenSysCtrl.Helpers { + public static partial class Extensions { + + [LibraryImport("msvcrt.dll")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static unsafe partial int memcmp(void* b1, void* b2, long count); + + public static unsafe int CompareBuffers(char[] buffer1, int offset1, char[] buffer2, int offset2, int count) { + fixed (char* b1 = buffer1, b2 = buffer2) { + return memcmp(b1 + offset1, b2 + offset2, count); + } + } + + + public static string? ReadUntil(this StreamReader reader, string delimiter) { + return ReadUntil(reader, delimiter.ToCharArray()); + } + + public static string? ReadUntil(this StreamReader reader, char delimiter) { + return ReadUntil(reader, [delimiter]); + } + + public static string? ReadUntil(this StreamReader reader, char[] delimiter) { + var buf = new char[512]; + int bufSize = 0, ret; + while (!reader.EndOfStream && bufSize < buf.Length - 1) { + if ((ret = reader.Read()) == -1) + return null; + buf[bufSize++] = (char)ret; + + if (bufSize >= delimiter.Length && CompareBuffers(buf, bufSize - delimiter.Length, delimiter, 0, delimiter.Length) == 0) + return new string(buf, 0, bufSize); + } + return null; + } + + public static Task ReadUntilAsync(this StreamReader reader, string delimiter) { + return ReadUntilAsync(reader, delimiter.ToCharArray()); + } + + public static Task ReadUntilAsync(this StreamReader reader, char delimiter) { + return ReadUntilAsync(reader, [delimiter]); + } + + public static async Task ReadUntilAsync(this StreamReader reader, char[] delimiter) { + var buf = new char[512]; + int bufSize = 0; + var tmpBuf = new char[1]; + while (!reader.EndOfStream && bufSize < buf.Length - 1) { + if ((await reader.ReadAsync(tmpBuf, 0, 1)) != 1) + return null; + buf[bufSize++] = tmpBuf[0]; + + if (bufSize >= delimiter.Length && CompareBuffers(buf, bufSize - delimiter.Length, delimiter, 0, delimiter.Length) == 0) + return new string(buf, 0, bufSize); + } + return null; + } + + } +} diff --git a/PamhagenSysCtrl/Helpers/FillState.cs b/PamhagenSysCtrl/Helpers/FillState.cs new file mode 100644 index 0000000..e5b6bc7 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/FillState.cs @@ -0,0 +1,3 @@ +namespace PamhagenSysCtrl.Helpers { + public enum FillState { Empty = 0, Filled = 1, Half = 2, Full = 3, Invalid = -1, Unknown = -2 } +} diff --git a/PamhagenSysCtrl/Helpers/MotorState.cs b/PamhagenSysCtrl/Helpers/MotorState.cs new file mode 100644 index 0000000..cf9ba9c --- /dev/null +++ b/PamhagenSysCtrl/Helpers/MotorState.cs @@ -0,0 +1,3 @@ +namespace PamhagenSysCtrl.Helpers { + public enum MotorState { Halt = 0, Forward = 1, Backward = 2, Invalid = -1 } +} diff --git a/PamhagenSysCtrl/Helpers/PamhagenPlant.cs b/PamhagenSysCtrl/Helpers/PamhagenPlant.cs new file mode 100644 index 0000000..461f7f7 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/PamhagenPlant.cs @@ -0,0 +1,171 @@ +using System.Windows; + +namespace PamhagenSysCtrl.Helpers { + public class PamhagenPlant : IDisposable { + + protected PamhagenPlc Plc; + + protected bool IsRunning = true; + protected Thread? BackgroundThread; + + public event EventHandler? Update; + + protected bool ActuatorsChanged = false; + protected PamhagenPlc.ActuatorStates Actuators; + protected PamhagenPlc.SensorStates Sensors; + + public bool GetDS(int n) => Sensors.GetDS(n >= 1 && n <= 60 ? n : throw new ArgumentException("Invalid value for n: 1 <= #DS <= 60")); + public bool GetV(int n) => Actuators.GetV(n >= 1 && n <= 60 ? n : throw new ArgumentException("Invalid value for n: 1 <= #V <= 60")); + + public bool IsValveClosed(int n) => GetDS(n); + public bool IsValveOpen(int n) => !IsValveClosed(n); + public bool WantValveClosed(int n) => GetV(n); + public bool WantValveOpen(int n) => !WantValveClosed(n); + + public bool IsA1Selected => Sensors.TroughSelector == 1; + public bool IsA2Selected => Sensors.TroughSelector == 2; + public FillState FillLevelA1 => Sensors.FillingLevels.A1; + public FillState FillLevelA2 => Sensors.FillingLevels.A2; + public FillState FillLevelPress1 => Sensors.FillingLevels.Press1; + public FillState FillLevelPress2 => Sensors.FillingLevels.Press2; + public FillState FillLevelT1 => Sensors.FillingLevelTanks[0]; + public FillState FillLevelT2 => Sensors.FillingLevelTanks[1]; + public FillState FillLevelT3 => Sensors.FillingLevelTanks[2]; + public FillState FillLevelT4 => Sensors.FillingLevelTanks[3]; + public FillState FillLevelT5 => Sensors.FillingLevelTanks[4]; + public FillState FillLevelT6 => Sensors.FillingLevelTanks[5]; + public FillState FillLevelT7 => Sensors.FillingLevelTanks[6]; + public FillState FillLevelT8 => Sensors.FillingLevelTanks[7]; + public FillState FillLevelT9 => Sensors.FillingLevelTanks[8]; + public double GradationOe => Sensors.GradationOe; + public MotorState PresseQuerSchnecke => Sensors.PresseQuerSchnecke; + + public bool WantA1Selected => Actuators.TroughSelector; + public bool WantA2Selected => !Actuators.TroughSelector; + + public bool IsTroughAugerActive => Actuators.TroughAuger; + public bool IsReblerActive => Actuators.Rebler; + public bool IsStirrerA1Active => Actuators.StirrerA1; + public bool IsStirrerA2Active => Actuators.StirrerA2; + public bool IsBeltActive => Actuators.Belt; + public bool IsAuger1Active => Actuators.Auger1; + public bool IsAuger2Active => Actuators.Auger2; + public bool IsGradationPumpActive => Actuators.GradationPump; + + public PamhagenPlant(string portName) { + Plc = new(portName); + } + + protected virtual void RaiseUpdateEvent(EventArgs evt) { + App.MainDispatcher.BeginInvoke(Update); + } + + public async Task StartThread() { + await Plc.TestConnection(); + Actuators = await Plc.ReadOutputs(); + BackgroundThread = new Thread(new ParameterizedThreadStart(BackgroundLoop)); + BackgroundThread.Start(); + } + + public void Dispose() { + IsRunning = false; + BackgroundThread?.Interrupt(); + BackgroundThread?.Join(); + Plc.Dispose(); + GC.SuppressFinalize(this); + } + + protected async void BackgroundLoop(object? parameters) { + while (IsRunning) { + try { + if (ActuatorsChanged) { + await Plc.WriteOutputs(Actuators); + ActuatorsChanged = false; + } + Sensors = await Plc.ReadInputs(); + RaiseUpdateEvent(EventArgs.Empty); + } catch (ThreadInterruptedException) { + // ignore + } catch (Exception exc) { + 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); + } + } + try { + await Plc.WriteOutputs(new()); + } 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); + } + } + + public void CommitChanges() { + ActuatorsChanged = true; + } + + public MotorState MP1 { + get => Actuators.MP1; + set => Actuators.MP1 = (value != MotorState.Halt && value != MotorState.Forward && value != MotorState.Backward) ? throw new ArgumentException("Invalid state for MP1") : value; + } + + public MotorState MP2 { + get => Actuators.MP2; + set => Actuators.MP2 = (value != MotorState.Halt && value != MotorState.Forward && value != MotorState.Backward) ? throw new ArgumentException("Invalid state for MP2") : value; + } + + public MotorState P12 { + get => Actuators.P12; + set => Actuators.P12 = (value != MotorState.Halt && value != MotorState.Forward) ? throw new ArgumentException("Invalid state for P12") : value; + } + + public MotorState P3 { + get => Actuators.P3; + set => Actuators.P3 = (value != MotorState.Halt && value != MotorState.Forward) ? throw new ArgumentException("Invalid state for P3") : value; + } + + public MotorState P4 { + get => Actuators.P4; + set => Actuators.P4 = (value != MotorState.Halt && value != MotorState.Forward) ? throw new ArgumentException("Invalid state for P4") : value; + } + + public MotorState P5 { + get => Actuators.P5; + set => Actuators.P5 = (value != MotorState.Halt && value != MotorState.Forward) ? throw new ArgumentException("Invalid state for P5") : value; + } + + public MotorState P6 { + get => Actuators.P6; + set => Actuators.P6 = (value != MotorState.Halt && value != MotorState.Forward) ? throw new ArgumentException("Invalid state for P6") : value; + } + + + public void OpenV(int n) => SetV(n, false); + public void CloseV(int n) => SetV(n, true); + + public void SetV(int n, bool val) { + if (n < 1 || n > 60) throw new ArgumentException("Invalid value for n: 1 <= #V <= 60"); + Actuators.SetV(n, val); + + } + + public void SelectA1() { + Actuators.TroughSelector = true; + //if (IsReblerActive) Actuators.StirrerA1 = true; + } + + public void SelectA2() { + Actuators.TroughSelector = false; + //if (IsReblerActive) Actuators.StirrerA2 = true; + } + + public void StartRebler() { + Actuators.Rebler = true; + } + + public void StopRebler() { + Actuators.Rebler = false; + } + } +} diff --git a/PamhagenSysCtrl/Helpers/PamhagenPlc.cs b/PamhagenSysCtrl/Helpers/PamhagenPlc.cs new file mode 100644 index 0000000..76e73c0 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/PamhagenPlc.cs @@ -0,0 +1,221 @@ +using System.IO.Ports; + +namespace PamhagenSysCtrl.Helpers { + public class PamhagenPlc : IDisposable { + + public record struct SensorStates( + long PressureSensors, + (FillState A1, FillState A2, FillState Press1, FillState Press2) FillingLevels, + int TroughSelector, + double GradationOe, + FillState[] FillingLevelTanks, + MotorState PresseQuerSchnecke + ) { + public readonly bool GetDS(int n) => (PressureSensors & (1 << (n - 1))) != 0; + } + + public record struct ActuatorStates( + long Valves, + MotorState MP1, MotorState MP2, MotorState P12, MotorState P3, MotorState P4, MotorState P5, MotorState P6, + bool TroughAuger, bool Rebler, bool TroughSelector, bool StirrerA1, bool StirrerA2, bool Belt, bool Auger1, bool Auger2, bool GradationPump + ) { + public readonly bool GetV(int n) => (Valves & (1L << (n - 1))) != 0; + public void SetV(int n, bool val) { + if (val) { + Valves |= 1L << (n - 1); + } else { + Valves &= ~(1L << (n - 1)); + } + } + public void CloseV(int n) => SetV(n, true); + public void OpenV(int n) => SetV(n, false); + } + + protected readonly T1 Plc; + + public PamhagenPlc(string portName, Parity parity = Parity.Odd) { + Plc = new T1(portName, parity); + } + + public void Dispose() { + Plc.Dispose(); + GC.SuppressFinalize(this); + } + + protected static FillState Int3ToFillState(int n) { + return n switch { + 7 => FillState.Full, + 6 => FillState.Half, + 4 => FillState.Filled, + 0 => FillState.Empty, + _ => FillState.Invalid, + }; + } + + protected static FillState Int2ToFillState(int n) { + return n switch { + 2 => FillState.Full, + 0 => FillState.Filled, + 1 => FillState.Empty, + _ => FillState.Invalid, + }; + } + + protected static FillState Int1ToFillState(int n) { + return n switch { + 1 => FillState.Full, + 0 => FillState.Unknown, + _ => FillState.Invalid, + }; + } + + protected static MotorState Int2ToPumpState(int n, bool inverse) { + return n switch { + 0 => MotorState.Halt, + 1 => inverse ? MotorState.Backward : MotorState.Forward, + 2 => inverse ? MotorState.Forward : MotorState.Backward, + _ => MotorState.Invalid, + }; + } + + protected static MotorState Int1ToPumpState(int n) { + return n switch { + 1 => MotorState.Forward, + _ => MotorState.Halt, + }; + } + + protected async Task<(ushort RW010, ushort RW011, ushort RW012, ushort RW013, ushort RW014, ushort RW015, ushort RW016)> ReadInputRegisters() { + var d = await Plc.DataRead("RW010", 7); + // RW010, RW011, RW012, RW013 Drucksensoren + // RW014 Füllstände A1, A2, Presse1, Presse2, Wannenselekt + // RW015 Messwert von Marselli Gerät + // RW016 Füllstände der Tanks + return (d[0], d[1], d[2], d[3], d[4], d[5], d[6]); + } + + public async Task ReadInputs() { + var (r1, r2, r3, r4, f1, v, f2) = await ReadInputRegisters(); + + long pressure = (long)r1 | ((long)r2 << 16) | ((long)r3 << 32) | ((long)r4 << 48); + + var a1 = Int3ToFillState((f1 ) & 0x7); // 111 011 001 000 + var a2 = Int3ToFillState((f1 >> 3) & 0x7); // 111 011 001 000 + var p1 = Int1ToFillState((f1 >> 6) & 0x1); // 1 0 + var p2 = Int1ToFillState((f1 >> 7) & 0x1); // 1 0 + var wp = ((f1 >> 8) & 0x3); // 10 01 + + // Masseligerät : 4mA => 50 Oe + // 20mA => 150 Oe + // ---> 0mA => 25 Oe + // Oe mA T1Wert + // 25 0 0 + // 50 4 800 + // 75 8 1600 + // 100 12 2400 + // 125 16 3200 + // 150 20 4000 + // -> factor: 0,03125 + (offset) 25 Oe + double oe = v * 0.03125 + 25; + + var t1 = Int2ToFillState((f2 ) & 0x3); // 10 01 00 + var t2 = Int2ToFillState((f2 >> 2) & 0x3); // 10 01 00 + var t3 = Int2ToFillState((f2 >> 4) & 0x3); // 10 01 00 + var t4 = Int2ToFillState((f2 >> 6) & 0x3); // 10 01 00 + var t5 = Int2ToFillState((f2 >> 8) & 0x3); // 10 01 00 + var t6 = Int1ToFillState((f2 >> 10) & 0x1); // 1 0 + var t7 = Int1ToFillState((f2 >> 11) & 0x1); // 1 0 + var t8 = Int1ToFillState((f2 >> 12) & 0x1); // 1 0 + var t9 = Int1ToFillState((f2 >> 13) & 0x1); // 1 0 + var q1 = Int1ToPumpState((f2 >> 15) & 0x1); // 1 0 + + return new SensorStates(pressure, (a1, a2, p1, p2), wp == 3 ? -1 : wp == 1 ? 2 : wp == 2 ? 1 : 0, oe, [t1, t2, t3, t4, t5, t6, t7, t8, t9], q1); + } + + protected async Task<(ushort RW000, ushort RW001, ushort RW002, ushort RW003, ushort RW004, ushort RW005, ushort RW006)> ReadOutputRegisters() { + var d = await Plc.DataRead("RW000", 7); + // RW000, RW001, RW002, RW003 Ventile + // RW004 Pumpensteuerung MP1, MP2, P1/2, P3, P4, P5, P6 + // RW005 Steuerung Wannenschnecke, Rebler, Förderung, Rührer + // RW006 MessungsSteuerung + return (d[0], d[1], d[2], d[3], d[4], d[5], d[6]); + } + + protected async Task WriteOutputRegisters(ushort rw000, ushort rw001, ushort rw002, ushort rw003, ushort rw004, ushort rw005, ushort rw006) { + // RW000, RW001, RW002, RW003 Ventile + // RW004 Pumpensteuerung MP1, MP2, P1/2, P3, P4, P5, P6 + // RW005 Steuerung Wannenschnecke, Rebler, Förderung, Rührer + // RW006 MessungsSteuerung + await Plc.DataWrite("RW000", rw000, rw001, rw002, rw003, rw004, rw005, rw006); + } + + public async Task ReadOutputs() { + var (v1, v2, v3, v4, p1, s1, s2) = await ReadOutputRegisters(); + + long valves = (long)v1 | ((long)v2 << 16) | ((long)v3 << 32) | ((long)v4 << 48); + + var mp1 = Int2ToPumpState((p1 ) & 0x3, true); + var mp2 = Int2ToPumpState((p1 >> 2) & 0x3, false); + var p12 = Int1ToPumpState((p1 >> 4) & 0x1); + var p3 = Int1ToPumpState((p1 >> 5) & 0x1); + var p4 = Int1ToPumpState((p1 >> 6) & 0x1); + var p5 = Int1ToPumpState((p1 >> 7) & 0x1); + var p6 = Int1ToPumpState((p1 >> 8) & 0x1); + + var m1 = (s1 & 0x01) != 0; + var m2 = (s1 & 0x02) != 0; + var m3 = (s1 & 0x04) != 0; + var m4 = (s1 & 0x08) != 0; + var m5 = (s1 & 0x10) != 0; + var m6 = (s1 & 0x20) != 0; + var m7 = (s1 & 0x40) != 0; + var m8 = (s1 & 0x80) != 0; + + var m9 = (s2 & 0x4) != 0; + var state = new ActuatorStates(valves, mp1, mp2, p12, p3, p4, p5, p6, m1, m2, m3, m4, m5, m6, m7, m8, m9); + state.SetV(59, (s2 & 0x1) != 0); + state.SetV(60, (s2 & 0x2) != 0); + + return state; + } + + public async Task WriteOutputs(ActuatorStates state) { + var v1 = (ushort)((state.Valves ) & 0xFFFF); + var v2 = (ushort)((state.Valves >> 16) & 0xFFFF); + var v3 = (ushort)((state.Valves >> 32) & 0xFFFF); + var v4 = (ushort)((state.Valves >> 48) & 0xFFFF); + + int pumps = + (state.MP1 == MotorState.Backward ? 0x001 : 0x000) | + (state.MP1 == MotorState.Forward ? 0x002 : 0x000) | + (state.MP2 == MotorState.Forward ? 0x004 : 0x000) | + (state.MP2 == MotorState.Backward ? 0x008 : 0x000) | + (state.P12 == MotorState.Forward ? 0x010 : 0x000) | + (state.P3 == MotorState.Forward ? 0x020 : 0x000) | + (state.P4 == MotorState.Forward ? 0x040 : 0x000) | + (state.P5 == MotorState.Forward ? 0x080 : 0x000) | + (state.P6 == MotorState.Forward ? 0x100 : 0x000); + + int devices = + (state.TroughAuger ? 0x01 : 0x00) | + (state.Rebler ? 0x02 : 0x00) | + (state.TroughSelector ? 0x04 : 0x00) | + (state.StirrerA1 ? 0x08 : 0x00) | + (state.StirrerA2 ? 0x10 : 0x00) | + (state.Belt ? 0x20 : 0x00) | + (state.Auger1 ? 0x40 : 0x00) | + (state.Auger2 ? 0x80 : 0x00); + + int measuring = + (state.GetV(59) ? 0x1 : 0x0) | + (state.GetV(60) ? 0x2 : 0x0) | + (state.GradationPump ? 0x4 : 0x0); + + await WriteOutputRegisters(v1, v2, v3, v4, (ushort)pumps, (ushort)devices, (ushort)measuring); + } + + public async Task TestConnection() { + await Plc.Test(); + } + } +} diff --git a/PamhagenSysCtrl/Helpers/T1.cs b/PamhagenSysCtrl/Helpers/T1.cs new file mode 100644 index 0000000..61aa4e8 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/T1.cs @@ -0,0 +1,132 @@ +using System.IO; +using System.IO.Ports; +using System.Text; + +namespace PamhagenSysCtrl.Helpers { + public class T1 : IDisposable { + + public enum Mode { HALT = 1, RUN = 2, RUN_F = 3, HOLD = 4, ERROR = 6 } + public readonly record struct Status(Mode Mode, int Flags); + + protected SerialPort Serial; + protected Stream Stream; + protected StreamReader Reader; + + public T1(string portName, Parity parity = Parity.Odd) { + Serial = new SerialPort(portName, 9600, parity, 8, StopBits.One); + Serial.Open(); + Stream = Serial.BaseStream ?? Stream.Null; + Stream.ReadTimeout = 250; + Stream.WriteTimeout = 250; + Reader = new(Stream, Encoding.ASCII, false, 256); + } + + public void Dispose() { + Reader.Dispose(); + Stream.Dispose(); + Serial.Dispose(); + GC.SuppressFinalize(this); + } + + protected async Task SendCommand(string cmd, string? data = null, bool checksum = true) { + // max 255 bytes + byte[] d = new byte[255]; + int n = Encoding.ASCII.GetBytes($"(A{1:02}{cmd}", d); + if (data != null) { + if (data.Length > 244) throw new ArgumentException("T1 playload too long"); + n += Encoding.ASCII.GetBytes(data, 0, data.Length, d, n); + } + if (checksum) { + d[n++] = (byte)'&'; + byte chk = d.Take(n).Aggregate((s, c) => s = (byte)((s + c) & 0xFF)); + n += Encoding.ASCII.GetBytes($"{chk:02X}", 0, 2, d, n); + } + d[n++] = (byte)')'; + d[n++] = (byte)'\r'; + await Stream.WriteAsync(d.AsMemory(0, n)); + } + + protected async Task<(string, string)> ReceiveResponse(string? expectedCmd = null) { + var line = await Reader.ReadUntilAsync('\r'); + if (line == null) { + throw new IOException("Verbindung zu SPS (T1) verloren"); + } else if (line.Length < 9 || !line.StartsWith("(A") || !line.EndsWith(")\r") || line[^5] != '&') { + throw new FormatException("Invalid response from PLC (T1)"); + } + + var chk1 = (byte)Convert.ToInt32(line[^4..^2], 16); + var chk2 = Encoding.ASCII.GetBytes(line).SkipLast(5).Aggregate((s, c) => (byte)((s + c) & 0xFF)); + if (chk1 != chk2) { + throw new IOException("Ungültige Prüfsumme von SPS (T1)"); + } + + var (cmd, data) = (line[3..5], line[5..^5]); + if (cmd == "ST" && data[4] == '6') { + // TODO check for other error bits? + await SendCommand("ER"); + var (_, res) = await ReceiveResponse("ER"); + int code = Convert.ToInt32(res); + var error = Enum.IsDefined(typeof(T1Error), code) ? (T1Error)code : T1Error.UNKNOWN_ERROR; + throw new T1Exception($"PLC Error ({data}): {error.GetName()}" + (error.GetDescription() != null ? $" ({error.GetDescription()})" : "")); + } else if (cmd == expectedCmd) { + return (cmd, data); + } else if (cmd == "CE") { + int code = Convert.ToInt32(data); + throw code switch { + 1 => new T1Exception($"Computer Link Error ({data}): Command error (Received command is illegal)"), + 2 => new T1Exception($"Computer Link Error ({data}): Format error (Received message format is invalid)"), + 3 => new T1Exception($"Computer Link Error ({data}): Checksum error (Checksum mismatch is detected)"), + _ => new T1Exception($"Computer Link Error ({data}): Unknown error"), + }; + } else if (cmd == "EE") { + int code = Convert.ToInt32(data); + var error = Enum.IsDefined(typeof(T1Error), code) ? (T1Error)code : T1Error.UNKNOWN_ERROR; + throw new T1Exception($"PLC Error ({data}): {error.GetName()}" + (error.GetDescription() != null ? $" ({error.GetDescription()})" : "")); + } else if (expectedCmd != null) { + throw new T1Exception($"Unexpected response from PLC (T1): {cmd} {data}"); + } + + return (cmd, data); + } + + public async Task Test(string data = "PINGPONG") { + await SendCommand("TS", data); + var (_, res) = await ReceiveResponse("TS"); + if (data.Replace(" ", "") != res) throw new T1Exception("Wrong test response from PLC (T1)"); + } + + public async Task 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')); + } + + public async Task 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; + } + + public Task DataRead(string register, int length) { + return DataRead((register, length)); + } + + public async Task DataRead(params (string Register, int Length)[] registers) { + await SendCommand("DR", string.Join(',', registers.Select(r => $"{r.Register},{r.Length}"))); + var (_, res) = await ReceiveResponse("DR"); + if (res.Length != registers.Sum(r => r.Length) * 4) throw new T1Exception("Invalid response length from PLC (T1)"); + return [.. Enumerable.Range(0, res.Length / 4).Select(i => Convert.ToUInt16(res.Substring(i * 4, 4), 16))]; + } + + public Task DataWrite(string register, params ushort[] values) { + return DataWrite((register, values)); + } + + public async Task DataWrite(params (string Register, ushort[] Values)[] registers) { + await SendCommand("DW", string.Join(',', registers.Select(r => $"{r.Register},{r.Values.Length}," + string.Join(',', r.Values.Select(v => $"{v:04X}"))))); + var (_, res) = await ReceiveResponse("ST"); + return new Status((Mode)(res[3] - '0'), ((res[0] - '0') << 4) | (res[1] - '0')); + } + } +} diff --git a/PamhagenSysCtrl/Helpers/T1Error.cs b/PamhagenSysCtrl/Helpers/T1Error.cs new file mode 100644 index 0000000..c8e67a5 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/T1Error.cs @@ -0,0 +1,73 @@ +namespace PamhagenSysCtrl.Helpers { + public enum T1Error { + UNKNOWN_ERROR = -1, + NO_ERROR = 0, + SYSTEM_POWER_ON = 10, + SYSTEM_POWER_OFF = 11, + RAM_CHECK_ERROR = 20, + PROGRAM_BCC_ERROR = 21, + BATTERY_VOLTAGE_DROP = 22, + EEPROM_BCC_ERROR = 23, + EEPROM_WARNING = 26, + SYSTEM_RAM_CHECK_ERROR = 30, + SYSTEM_ROM_BCC_ERROR = 31, + PERIPHERAL_LSI_ERROR = 32, + CLOCK_CALENDAR_CHECK_ERROR = 33, + ILLEGAL_SYSTEM_INTERRUPT = 34, + WD_TIMER_ERROR = 35, + IO_BUS_ERROR = 40, + IO_MISMATCH = 41, + IO_NO_ANSWER = 42, + IO_PARITY_ERROR = 43, + ILLEGAL_IO_REGISTER = 46, + COMMUNICATION_BUSY = 51, + FORMAT_ERROR = 52, + SCAN_TIME_OVER = 64, + NO_END_IRET_ERROR = 80, + PAIR_INSTRUCTION_ERROR = 81, + OPERAND_ERROR = 82, + INVALID_PROGRAM = 83, + NO_SUBROUTINE_ENTRY = 86, + NO_RET_ERROR = 87, + SUBROUTINE_NESTING_ERROR = 88, + LOOP_NESTING_ERROR = 89, + INVALID_FUNCTION_INSTRUCTION = 98, + PASSWORD_PROTECT = 106, + ILLEGAL_INSTRUCTION = 110, + REGISTER_ADDRESS_ERROR = 111, + BOUNDARY_ERROR = 112, + MEMORY_FULL = 113, + MODE_MISMATCH = 114, + REGISTER_ADDRESS_SIZE_ERROR = 115, + DUPLICATE_ENTRY_NR = 121, + } + + public static class T1ErrorExtension { + public static bool IsError(this T1Error error) { + return error switch { + T1Error.NO_ERROR => false, + T1Error.SYSTEM_POWER_ON => false, + T1Error.SYSTEM_POWER_OFF => false, + _ => true, + }; + } + public static string GetName(this T1Error error) { + return error switch { + T1Error.NO_ERROR => "No error", + T1Error.SYSTEM_POWER_ON => "System power on", + T1Error.SYSTEM_POWER_OFF => "System power off", + // TODO + _ => "Unknown error", + }; + } + public static string? GetDescription(this T1Error error) { + return error switch { + T1Error.NO_ERROR => "No error recorded", + T1Error.SYSTEM_POWER_ON => "Power on (no error)", + T1Error.SYSTEM_POWER_OFF => "Power off (no error)", + // TODO + _ => null, + }; + } + } +} diff --git a/PamhagenSysCtrl/Helpers/T1Exception.cs b/PamhagenSysCtrl/Helpers/T1Exception.cs new file mode 100644 index 0000000..5b44c73 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/T1Exception.cs @@ -0,0 +1,4 @@ +namespace PamhagenSysCtrl.Helpers { + public class T1Exception(string msg) : Exception(msg) { + } +} diff --git a/PamhagenSysCtrl/PamhagenSysCtrl.csproj b/PamhagenSysCtrl/PamhagenSysCtrl.csproj new file mode 100644 index 0000000..de2a92f --- /dev/null +++ b/PamhagenSysCtrl/PamhagenSysCtrl.csproj @@ -0,0 +1,16 @@ + + + + WinExe + net10.0-windows + enable + enable + true + true + + + + + + + diff --git a/PamhagenSysCtrl/Windows/MainWindow.xaml b/PamhagenSysCtrl/Windows/MainWindow.xaml new file mode 100644 index 0000000..4455a49 --- /dev/null +++ b/PamhagenSysCtrl/Windows/MainWindow.xaml @@ -0,0 +1,19 @@ + + + +