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:00}{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) => (byte)((s + c) & 0xFF)); n += Encoding.ASCII.GetBytes($"{chk:X2}", 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(4).Aggregate((s, c) => (byte)((s + c) & 0xFF)); if (chk1 != chk2) { throw new IOException($"Ungültige Prüfsumme von SPS (T1)"); } var (cmd, data) = (line[4..6], line[6..^5]); if (cmd == "ST" && data[3] == '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:X4}"))))); var (_, res) = await ReceiveResponse("ST"); return new Status((Mode)(res[3] - '0'), ((res[0] - '0') << 4) | (res[1] - '0')); } } }