Add PamhagenSysCtrl

This commit is contained in:
2026-07-07 13:53:16 +02:00
parent db22c87ebe
commit 9b64609817
16 changed files with 825 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
obj/
bin/
*.user
.vs
.idea
*.exe
+3
View File
@@ -0,0 +1,3 @@
<Solution>
<Project Path="PamhagenSysCtrl/PamhagenSysCtrl.csproj" />
</Solution>
+9
View File
@@ -0,0 +1,9 @@
<Application x:Class="PamhagenSysCtrl.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PamhagenSysCtrl"
StartupUri="Windows/MainWindow.xaml"
Exit="Application_Exit">
<Application.Resources>
</Application.Resources>
</Application>
+24
View File
@@ -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();
}
}
}
+10
View File
@@ -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)
)]
+64
View File
@@ -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<string?> ReadUntilAsync(this StreamReader reader, string delimiter) {
return ReadUntilAsync(reader, delimiter.ToCharArray());
}
public static Task<string?> ReadUntilAsync(this StreamReader reader, char delimiter) {
return ReadUntilAsync(reader, [delimiter]);
}
public static async Task<string?> 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;
}
}
}
+3
View File
@@ -0,0 +1,3 @@
namespace PamhagenSysCtrl.Helpers {
public enum FillState { Empty = 0, Filled = 1, Half = 2, Full = 3, Invalid = -1, Unknown = -2 }
}
+3
View File
@@ -0,0 +1,3 @@
namespace PamhagenSysCtrl.Helpers {
public enum MotorState { Halt = 0, Forward = 1, Backward = 2, Invalid = -1 }
}
+171
View File
@@ -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<EventArgs>? 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;
}
}
}
+221
View File
@@ -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<SensorStates> 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<ActuatorStates> 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();
}
}
}
+132
View File
@@ -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<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'));
}
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;
}
public Task<ushort[]> DataRead(string register, int length) {
return DataRead((register, length));
}
public async Task<ushort[]> 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<Status> DataWrite(string register, params ushort[] values) {
return DataWrite((register, values));
}
public async Task<Status> 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'));
}
}
}
+73
View File
@@ -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,
};
}
}
}
+4
View File
@@ -0,0 +1,4 @@
namespace PamhagenSysCtrl.Helpers {
public class T1Exception(string msg) : Exception(msg) {
}
}
+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="10.0.9" />
</ItemGroup>
</Project>
+19
View File
@@ -0,0 +1,19 @@
<Window x:Class="PamhagenSysCtrl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PamhagenSysCtrl"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TextBox x:Name="PortInput" Text="COM1"
Margin="10,10,10,10" VerticalAlignment="Top" HorizontalAlignment="Left" Height="25" Width="80" FontSize="14" Padding="2,2,2,2"/>
<Button x:Name="ConnectButton" Content="Verbinden"
Margin="100,10,10,10" VerticalAlignment="Top" HorizontalAlignment="Left" Height="25" Width="80" FontSize="14"
Click="ConnectButton_Click"/>
<Grid x:Name="ButtonPanel" Margin="5,50,5,5">
</Grid>
</Grid>
</Window>
@@ -0,0 +1,67 @@
using PamhagenSysCtrl.Helpers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace PamhagenSysCtrl {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
for (int i = 1; i <= 60; i++) {
var b = new Button() {
Content = $"V{i}",
Margin = new(10 + ((i - 1) / 10) * 50, 10 + ((i - 1) % 10) * 40, 10, 10),
BorderThickness = new(2),
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
Height = 30,
Width = 40,
FontSize = 14,
};
var borderStyle = new Style(typeof(Border));
borderStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5)));
b.Resources.Add(typeof(Border), borderStyle);
b.Click += (sender, evt) => {
if (App.Plant == null) return;
if (App.Plant.WantValveOpen(i)) {
App.Plant.CloseV(i);
} else {
App.Plant.OpenV(i);
}
App.Plant.CommitChanges();
};
ButtonPanel.Children.Add(b);
}
}
public async void ConnectButton_Click(object? sender, RoutedEventArgs evt) {
try {
App.Plant = new(PortInput.Text);
App.Plant.Update += OnUpdate;
await App.Plant.StartThread();
} catch (Exception exc) {
App.Plant?.Dispose();
App.Plant = null;
var str = "Beim Aufbauen einer 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 OnUpdate(object? sender, EventArgs evt) {
if (sender is not PamhagenPlant plant) return;
for (int i = 1; i <= 60; i++) {
var target = plant.GetV(i);
var actual = plant.GetDS(i);
if (ButtonPanel.Children[i - 1] is not Button b)
continue;
b.BorderBrush = target ? Brushes.DarkRed : Brushes.SlateGray;
b.Foreground = target ? Brushes.DarkRed : Brushes.Black;
b.Background = actual ? Brushes.MistyRose : Brushes.WhiteSmoke;
}
}
}
}