From 443e1115947c8517bca3b5eaaeb9753d2e2b3bb5 Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Sat, 13 Apr 2024 18:00:17 +0200 Subject: [PATCH] Weighing: Add Gassner scale and update tests --- Elwig/Helpers/Extensions.cs | 63 +++++++ Elwig/Helpers/Weighing/AveryEventScale.cs | 18 +- Elwig/Helpers/Weighing/GassnerScale.cs | 122 +++++++++++++ Elwig/Helpers/Weighing/Scale.cs | 6 + Elwig/Helpers/Weighing/SysTecITScale.cs | 13 +- Tests/WeighingTests/MockScale.cs | 40 ++++- Tests/WeighingTests/ScaleTestBadenL320.cs | 70 ++++++++ .../ScaleTestGrInzersdorfL246.cs | 144 ++++++++++++++++ ...stWolkersdorf.cs => ScaleTestHaugsdorf.cs} | 2 +- ...estMatzen.cs => ScaleTestMatzenIT3000A.cs} | 4 +- ...GrInzersdorf.cs => ScaleTestSitzendorf.cs} | 2 +- .../ScaleTestWolkersdorfIT6000E.cs | 163 ++++++++++++++++++ 12 files changed, 626 insertions(+), 21 deletions(-) create mode 100644 Elwig/Helpers/Extensions.cs create mode 100644 Elwig/Helpers/Weighing/GassnerScale.cs create mode 100644 Tests/WeighingTests/ScaleTestBadenL320.cs create mode 100644 Tests/WeighingTests/ScaleTestGrInzersdorfL246.cs rename Tests/WeighingTests/{ScaleTestWolkersdorf.cs => ScaleTestHaugsdorf.cs} (65%) rename Tests/WeighingTests/{ScaleTestMatzen.cs => ScaleTestMatzenIT3000A.cs} (99%) rename Tests/WeighingTests/{ScaleTestGrInzersdorf.cs => ScaleTestSitzendorf.cs} (65%) create mode 100644 Tests/WeighingTests/ScaleTestWolkersdorfIT6000E.cs diff --git a/Elwig/Helpers/Extensions.cs b/Elwig/Helpers/Extensions.cs new file mode 100644 index 0000000..0d17c29 --- /dev/null +++ b/Elwig/Helpers/Extensions.cs @@ -0,0 +1,63 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Elwig.Helpers { + 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, new char[] { 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, new char[] { 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/Elwig/Helpers/Weighing/AveryEventScale.cs b/Elwig/Helpers/Weighing/AveryEventScale.cs index 56e0796..48526c3 100644 --- a/Elwig/Helpers/Weighing/AveryEventScale.cs +++ b/Elwig/Helpers/Weighing/AveryEventScale.cs @@ -26,6 +26,8 @@ namespace Elwig.Helpers.Weighing { Model = model; IsReady = true; HasFillingClearance = false; + Stream.WriteTimeout = -1; + Stream.ReadTimeout = -1; BackgroundThread = new Thread(new ParameterizedThreadStart(BackgroundLoop)); BackgroundThread.Start(); } @@ -46,7 +48,8 @@ namespace Elwig.Helpers.Weighing { while (IsRunning) { try { var data = await Receive(); - RaiseWeighingEvent(new WeighingEventArgs(data)); + if (data != null) + RaiseWeighingEvent(new WeighingEventArgs(data.Value)); } catch (Exception ex) { MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message}", "Waagenfehler", MessageBoxButton.OK, MessageBoxImage.Error); @@ -54,13 +57,12 @@ namespace Elwig.Helpers.Weighing { } } - protected async Task Receive() { - string? line = null; - using (var reader = new StreamReader(Stream, Encoding.ASCII, false, -1, true)) { - line = await reader.ReadLineAsync(); - if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n"); - } - if (line == null || line.Length != 33 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') { + protected async Task Receive() { + var line = await Reader.ReadUntilAsync("\r\n"); + if (LogPath != null) await File.AppendAllTextAsync(LogPath, line); + if (line == null || line == "") { + return null; + } else if (line.Length != 35 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') { throw new IOException($"Invalid event from scale: '{line}'"); } diff --git a/Elwig/Helpers/Weighing/GassnerScale.cs b/Elwig/Helpers/Weighing/GassnerScale.cs new file mode 100644 index 0000000..9f9b6df --- /dev/null +++ b/Elwig/Helpers/Weighing/GassnerScale.cs @@ -0,0 +1,122 @@ +using System; +using System.IO; +using System.IO.Ports; +using System.Text; +using System.Threading.Tasks; + +namespace Elwig.Helpers.Weighing { + public class GassnerScale : Scale, ICommandScale { + + public string Manufacturer => "Gassner"; + public int InternalScaleNr => 1; + public string Model { get; private set; } + public string ScaleId { get; private set; } + public bool IsReady { get; private set; } + public bool HasFillingClearance { get; private set; } + + public GassnerScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) : + base(cnx, empty, filling, limit, log) { + ScaleId = id; + Model = model; + IsReady = true; + HasFillingClearance = false; + Stream.WriteTimeout = 250; + Stream.ReadTimeout = 11000; + } + + protected Task SendCommand(char command) => SendCommand(Convert.ToByte(command)); + + protected async Task SendCommand(byte command) { + byte[] cmd = [command]; + await Stream.WriteAsync(cmd); + if (LogPath != null) await File.AppendAllTextAsync(LogPath, Encoding.ASCII.GetString(cmd)); + } + + protected async Task ReceiveResponse() { + var line = await Reader.ReadUntilAsync('\x03'); + if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n"); + if (line == null || line.Length < 4 || !line.StartsWith('\x02')) { + throw new IOException("Invalid response from scale"); + } + + var status = line[1..3]; + if (status[0] == 'E' || status[1] != 'S') { + string msg = $"Unbekannter Fehler (Fehler code {status})"; + switch (status[1]) { + case 'M': msg = "Waage in Bewegung"; break; + } + throw new IOException($"Waagenfehler {status}: {msg}"); + } else if (status[0] != ' ') { + throw new IOException($"Invalid response from scale (error code {status})"); + } + + return line[1..^1]; + } + + protected async Task Weigh(bool incIdentNr) { + await SendCommand(incIdentNr ? '\x05' : '?'); + string record = await ReceiveResponse(); + if (record.Length != 45) + throw new IOException("Invalid response from scale: Received record has invalid size"); + var line = record[2..]; + + var status = line[ 0.. 2]; + var brutto = line[ 2.. 9].Trim(); + var tara = line[ 9..16].Trim(); + var netto = line[16..23].Trim(); + var scaleNr = line[23..25].Trim(); + var identNr = line[25..31].Trim(); + var date = line[31..39]; + var time = line[39..45]; + + identNr = identNr.Length > 0 && identNr != "0" ? identNr : null; + var parsedDate = DateOnly.ParseExact(date, "yyyyMMdd"); + return new() { + Weight = int.Parse(netto), + WeighingId = identNr, + FullWeighingId = identNr, + Date = parsedDate, + Time = TimeOnly.ParseExact(time, "HHmmss"), + }; + } + + public async Task Weigh() { + return await Weigh(true); + } + + public async Task GetCurrentWeight() { + return await Weigh(false); + } + + public async Task Empty() { + SerialPort? p = ControlSerialEmpty ?? Serial; + if (EmptyMode == Output.RTS && p != null) { + p.RtsEnable = true; + await Task.Delay(EmptyDelay); + p.RtsEnable = false; + } else if (EmptyMode == Output.DTR && p != null) { + p.DtrEnable = true; + await Task.Delay(EmptyDelay); + p.DtrEnable = false; + } + } + + protected Task SetFillingClearance(bool status) { + SerialPort? p = ControlSerialFilling ?? Serial; + if (FillingClearanceMode == Output.RTS && p != null) { + p.RtsEnable = status; + } else if (FillingClearanceMode == Output.DTR && p != null) { + p.DtrEnable = status; + } + return Task.CompletedTask; + } + + public async Task GrantFillingClearance() { + await SetFillingClearance(true); + } + + public async Task RevokeFillingClearance() { + await SetFillingClearance(false); + } + } +} diff --git a/Elwig/Helpers/Weighing/Scale.cs b/Elwig/Helpers/Weighing/Scale.cs index 5981f3b..08fc0ac 100644 --- a/Elwig/Helpers/Weighing/Scale.cs +++ b/Elwig/Helpers/Weighing/Scale.cs @@ -2,6 +2,7 @@ using System.IO; using System.Net.Sockets; using System; +using System.Text; namespace Elwig.Helpers.Weighing { public abstract class Scale : IDisposable { @@ -12,6 +13,7 @@ namespace Elwig.Helpers.Weighing { protected SerialPort? ControlSerialEmpty = null, ControlSerialFilling = null; protected TcpClient? Tcp = null; protected Stream Stream; + protected StreamReader Reader; protected readonly Output? EmptyMode = null; protected readonly Output? FillingClearanceMode = null; @@ -26,6 +28,8 @@ namespace Elwig.Helpers.Weighing { return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); } else if (config.Type == "Avery-Async") { return new AveryEventScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); + } else if (config.Type == "Gassner") { + return new GassnerScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); } else { throw new ArgumentException($"Invalid scale type: \"{config.Type}\""); } @@ -41,6 +45,7 @@ namespace Elwig.Helpers.Weighing { } else { throw new ArgumentException($"Unsupported scheme: \"{cnx.Split(':')[0]}\""); } + Reader = new(Stream, Encoding.ASCII, false, 512); LogPath = log; @@ -73,6 +78,7 @@ namespace Elwig.Helpers.Weighing { } public void Dispose() { + Reader.Close(); Stream.Close(); Serial?.Close(); ControlSerialEmpty?.Close(); diff --git a/Elwig/Helpers/Weighing/SysTecITScale.cs b/Elwig/Helpers/Weighing/SysTecITScale.cs index ec0b96b..24400be 100644 --- a/Elwig/Helpers/Weighing/SysTecITScale.cs +++ b/Elwig/Helpers/Weighing/SysTecITScale.cs @@ -31,12 +31,9 @@ namespace Elwig.Helpers.Weighing { } protected async Task ReceiveResponse() { - string? line = null; - using (var reader = new StreamReader(Stream, Encoding.ASCII, false, -1, true)) { - line = await reader.ReadLineAsync(); - if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n"); - } - if (line == null || line.Length < 4 || !line.StartsWith('<') || !line.EndsWith('>')) { + var line = await Reader.ReadUntilAsync("\r\n"); + if (LogPath != null) await File.AppendAllTextAsync(LogPath, line); + if (line == null || line.Length < 4 || !line.StartsWith('<') || !line.EndsWith(">\r\n")) { throw new IOException("Invalid response from scale"); } @@ -68,7 +65,7 @@ namespace Elwig.Helpers.Weighing { throw new IOException($"Invalid response from scale (error code {error})"); } - return line[1..^1]; + return line[1..^3]; } protected async Task Weigh(bool incIdentNr) { @@ -82,7 +79,7 @@ namespace Elwig.Helpers.Weighing { var date = line[ 2..10]; var time = line[10..15]; var identNr = line[15..19].Trim(); - var scaleNr = line[19..20]; + var scaleNr = line[19..20].Trim(); var brutto = line[20..28].Trim(); var tara = line[28..36].Trim(); var netto = line[36..44].Trim(); diff --git a/Tests/WeighingTests/MockScale.cs b/Tests/WeighingTests/MockScale.cs index 9da6698..ad3bade 100644 --- a/Tests/WeighingTests/MockScale.cs +++ b/Tests/WeighingTests/MockScale.cs @@ -1,6 +1,7 @@ using System.Net.Sockets; using System.Net; using System.Text; +using System.IO; namespace Tests.WeighingTests { public abstract class MockScale : IDisposable { @@ -63,9 +64,46 @@ namespace Tests.WeighingTests { } public class EventMockScale : MockScale { + + private readonly Func Handler; + private readonly Thread ServerThread; + private TcpClient? Client; + private bool IsRunning = true; + public EventMockScale(int port, Func handler) : base(port) { - // TODO + Handler = handler; + ServerThread = new Thread(new ParameterizedThreadStart(Serve)); + ServerThread.Start(); + } + + private async void Serve(object? parameters) { + while (IsRunning) { + try { + Client = await Server.AcceptTcpClientAsync(); + } catch (Exception) { + // ignore + } + } + } + + public async Task Weigh(int weight) { + Weight = weight; + await Weigh(); + } + + public async Task Weigh() { + var (res, inc) = Handler(Weight, Error, IdentNr + 1); + if (inc) IdentNr++; + await Client!.GetStream().WriteAsync(Encoding.ASCII.GetBytes(res)); + } + + public new void Dispose() { + Client?.Dispose(); + IsRunning = false; + ServerThread.Interrupt(); + ServerThread.Join(); + base.Dispose(); } } } diff --git a/Tests/WeighingTests/ScaleTestBadenL320.cs b/Tests/WeighingTests/ScaleTestBadenL320.cs new file mode 100644 index 0000000..1b36d49 --- /dev/null +++ b/Tests/WeighingTests/ScaleTestBadenL320.cs @@ -0,0 +1,70 @@ +using Elwig.Helpers.Weighing; + +namespace Tests.WeighingTests { + [TestFixture] + public class ScaleTestBadenL320 { + + private EventMockScale? Mock; + private AveryEventScale? Scale; + + private static (string, bool) ScaleHandler(int weight, string? error, int identNr) { + var modes = error?.Split(';') ?? []; + var invalid = modes.Contains("invalid"); + var unit = modes.Contains("unit"); + + if (invalid) { + return ("abcd\r\n", false); + } + + bool incr = true; + return ($" {new DateTime(2020, 9, 28, 9, 8, 0):dd.MM.yy HH:mm} {identNr,4} {weight,9}{(unit ? "lb" : "kg")} \r\n", incr); + } + + [OneTimeSetUp] + public void SetupScale() { + Mock = new EventMockScale(12345, ScaleHandler); + Scale = new("1", "L320", "tcp://127.0.0.1:12345"); + } + + [OneTimeTearDown] + public void TeardownScale() { + Mock?.Dispose(); + Scale?.Dispose(); + } + + [SetUp] + public void ResetScale() { + Mock!.IdentNr = 0; + Mock!.Weight = 0; + Mock!.Error = null; + } + + [Test] + public async Task Test_01_Normal() { + WeighingResult? res = null; + Scale!.WeighingEvent += (sender, evt) => { + res = evt.Result; + }; + + await Mock!.Weigh(2345); + await Task.Delay(100); + Assert.That(res, Is.Not.Null); + Assert.That(res, Is.EqualTo(new WeighingResult { + Weight = 2345, WeighingId = "1", + FullWeighingId = $"2020-09-28/1", + Date = new DateOnly(2020, 9, 28), Time = new TimeOnly(9, 8), + })); + + await Mock!.Weigh(4215); + await Task.Delay(100); + Assert.That(res, Is.Not.Null); + Assert.That(res, Is.EqualTo(new WeighingResult { + Weight = 4215, + WeighingId = "2", + FullWeighingId = $"2020-09-28/2", + Date = new DateOnly(2020, 9, 28), + Time = new TimeOnly(9, 8), + })); + } + } +} diff --git a/Tests/WeighingTests/ScaleTestGrInzersdorfL246.cs b/Tests/WeighingTests/ScaleTestGrInzersdorfL246.cs new file mode 100644 index 0000000..b733d45 --- /dev/null +++ b/Tests/WeighingTests/ScaleTestGrInzersdorfL246.cs @@ -0,0 +1,144 @@ +using Elwig.Helpers.Weighing; + +namespace Tests.WeighingTests { + [TestFixture] + class ScaleTestGrInzersdorfL246 { + + private MockScale? Mock; + private SysTecITScale? Scale; + + private static (string, bool) ScaleHandler(string req, int weight, string? error, int identNr) { + var modes = error?.Split(';') ?? []; + var overloaded = modes.Contains("overloaded"); + var moving = modes.Contains("moving"); + var invalid = modes.Contains("invalid"); + var crc = modes.Contains("crc"); + var unit = modes.Contains("unit"); + + if (invalid) { + return ("abcd\r\n", false); + } else if (!req.StartsWith('<') || !req.EndsWith('>')) { + return ("<31>\r\n", false); + } + req = req[1..^1]; + + bool incr; + if (req.Length > 3) { + return ("<32>\r\n", false); + } else if (req.StartsWith("RN")) { + incr = true; + } else if (req.StartsWith("RM")) { + incr = false; + } else { + return ("<32>\r\n", false); + } + + if (overloaded) + return ("<12>\r\n", false); + + if (moving && incr) + return ("<13>\r\n", false); + + string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 17, 14, 23, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" + + $"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {"001",3}"; + ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data); + if (crc) checksum += 10; + return ($"<{data}{checksum,8}>\r\n", incr); + } + + [OneTimeSetUp] + public void SetupScale() { + Mock = new CommandMockScale(12345, ScaleHandler); + Scale = new("1", "L246", "tcp://127.0.0.1:12345"); + } + + [OneTimeTearDown] + public void TeardownScale() { + Mock?.Dispose(); + Scale?.Dispose(); + } + + [SetUp] + public void ResetScale() { + Mock!.IdentNr = 0; + Mock!.Weight = 0; + Mock!.Error = null; + } + + [Test] + public async Task Test_01_CurrentWeight() { + Mock!.Weight = 1235; + Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult { + Weight = 1235, Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23), + })); + Mock!.Weight = 1240; + Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult { + Weight = 1240, Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23), + })); + Mock!.Weight = 1245; + Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult { + Weight = 1245, Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23), + })); + } + + [Test] + public async Task Test_02_Normal() { + Mock!.Weight = 1235; + Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult { + Weight = 1235, WeighingId = "1", + FullWeighingId = $"2020-10-17/1", + Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23), + })); + Mock!.Weight = 3335; + Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult { + Weight = 3335, WeighingId = "2", + FullWeighingId = $"2020-10-17/2", + Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23), + })); + Mock!.Weight = 6420; + Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult { + Weight = 6420, WeighingId = "3", + FullWeighingId = $"2020-10-17/3", + Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23), + })); + } + + [Test] + public void Test_03_Moving() { + Mock!.Weight = 1_000; + Mock!.Error = "moving"; + IOException ex = Assert.ThrowsAsync(async () => await Scale!.Weigh()); + Assert.That(ex.Message, Contains.Substring("Waage in Bewegung")); + } + + [Test] + public void Test_04_Overloaded() { + Mock!.Weight = 10_000; + Mock!.Error = "overloaded"; + IOException ex = Assert.ThrowsAsync(async () => await Scale!.Weigh()); + Assert.That(ex.Message, Contains.Substring("Waage in Überlast")); + } + + [Test] + public void Test_05_InvalidResponse() { + Mock!.Weight = 1_000; + Mock!.Error = "invalid"; + Assert.ThrowsAsync(async () => await Scale!.Weigh()); + } + + [Test] + public void Test_06_InvalidCrc() { + Mock!.Weight = 1_000; + Mock!.Error = "crc"; + IOException ex = Assert.ThrowsAsync(async () => await Scale!.Weigh()); + Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum")); + } + + [Test] + public void Test_07_InvalidUnit() { + Mock!.Weight = 1_000; + Mock!.Error = "unit"; + IOException ex = Assert.ThrowsAsync(async () => await Scale!.Weigh()); + } + } +} diff --git a/Tests/WeighingTests/ScaleTestWolkersdorf.cs b/Tests/WeighingTests/ScaleTestHaugsdorf.cs similarity index 65% rename from Tests/WeighingTests/ScaleTestWolkersdorf.cs rename to Tests/WeighingTests/ScaleTestHaugsdorf.cs index 1faa39e..d898724 100644 --- a/Tests/WeighingTests/ScaleTestWolkersdorf.cs +++ b/Tests/WeighingTests/ScaleTestHaugsdorf.cs @@ -1,6 +1,6 @@ namespace Tests.WeighingTests { [TestFixture] - public class ScaleTestWolkersdorf { + public class ScaleTestHaugsdorf { // TODO } } diff --git a/Tests/WeighingTests/ScaleTestMatzen.cs b/Tests/WeighingTests/ScaleTestMatzenIT3000A.cs similarity index 99% rename from Tests/WeighingTests/ScaleTestMatzen.cs rename to Tests/WeighingTests/ScaleTestMatzenIT3000A.cs index dfc7d32..6ab91f8 100644 --- a/Tests/WeighingTests/ScaleTestMatzen.cs +++ b/Tests/WeighingTests/ScaleTestMatzenIT3000A.cs @@ -2,7 +2,7 @@ namespace Tests.WeighingTests { [TestFixture] - class ScaleTestMatzen { + class ScaleTestMatzenIT3000A { private MockScale? Mock; private SysTecITScale? Scale; @@ -43,7 +43,7 @@ namespace Tests.WeighingTests { return ("<13>\r\n", false); string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 15, 12, 34, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" + - $"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {1,3}"; + $"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {"1",3}"; ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data); if (crc) checksum += 10; return ($"<{data}{checksum,8}>\r\n", incr); diff --git a/Tests/WeighingTests/ScaleTestGrInzersdorf.cs b/Tests/WeighingTests/ScaleTestSitzendorf.cs similarity index 65% rename from Tests/WeighingTests/ScaleTestGrInzersdorf.cs rename to Tests/WeighingTests/ScaleTestSitzendorf.cs index f96fc27..513ac1d 100644 --- a/Tests/WeighingTests/ScaleTestGrInzersdorf.cs +++ b/Tests/WeighingTests/ScaleTestSitzendorf.cs @@ -1,6 +1,6 @@ namespace Tests.WeighingTests { [TestFixture] - public class ScaleTestGrInzersdorf { + public class ScaleTestSitzendorf { // TODO } } diff --git a/Tests/WeighingTests/ScaleTestWolkersdorfIT6000E.cs b/Tests/WeighingTests/ScaleTestWolkersdorfIT6000E.cs new file mode 100644 index 0000000..709108f --- /dev/null +++ b/Tests/WeighingTests/ScaleTestWolkersdorfIT6000E.cs @@ -0,0 +1,163 @@ +using Elwig.Helpers.Weighing; + +namespace Tests.WeighingTests { + [TestFixture] + class ScaleTestWolkersdorfIT6000E { + + private MockScale? MockA; + private MockScale? MockB; + private SysTecITScale? ScaleA; + private SysTecITScale? ScaleB; + + private static (string, bool) ScaleHandler(string req, int weight, string? error, int identNr, string terminalNr) { + var modes = error?.Split(';') ?? []; + var overloaded = modes.Contains("overloaded"); + var moving = modes.Contains("moving"); + var invalid = modes.Contains("invalid"); + var crc = modes.Contains("crc"); + var unit = modes.Contains("unit"); + + if (invalid) { + return ("abcd\r\n", false); + } else if (!req.StartsWith('<') || !req.EndsWith('>')) { + return ("<31>\r\n", false); + } + req = req[1..^1]; + + bool incr; + if (req.Length > 3) { + return ("<32>\r\n", false); + } else if (req.StartsWith("RN")) { + incr = true; + } else if (req.StartsWith("RM")) { + incr = false; + } else { + return ("<32>\r\n", false); + } + + if (overloaded) + return ("<12>\r\n", false); + + if (moving && incr) + return ("<13>\r\n", false); + + string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 8, 8, 47, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" + + $"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {terminalNr,3}"; + ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data); + if (crc) checksum += 10; + return ($"<{data}{checksum,8}>\r\n", incr); + } + + [OneTimeSetUp] + public void SetupScale() { + MockA = new CommandMockScale(12345, (req, weight, error, identNr) => ScaleHandler(req, weight, error, identNr, "A")); + MockB = new CommandMockScale(12346, (req, weight, error, identNr) => ScaleHandler(req, weight, error, identNr, "B")); + ScaleA = new("A", "IT3000E", "tcp://127.0.0.1:12345"); + ScaleB = new("B", "IT3000E", "tcp://127.0.0.1:12346"); + } + + [OneTimeTearDown] + public void TeardownScale() { + MockA?.Dispose(); + MockB?.Dispose(); + ScaleA?.Dispose(); + ScaleB?.Dispose(); + } + + [SetUp] + public void ResetScale() { + MockA!.IdentNr = 0; + MockA!.Weight = 0; + MockA!.Error = null; + MockB!.IdentNr = 0; + MockB!.Weight = 0; + MockB!.Error = null; + } + + [Test] + public async Task Test_01_CurrentWeight() { + MockA!.Weight = 1234; + Assert.That(await ScaleA!.GetCurrentWeight(), Is.EqualTo(new WeighingResult { + Weight = 1234, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + MockB!.Weight = 3456; + Assert.That(await ScaleB!.GetCurrentWeight(), Is.EqualTo(new WeighingResult { + Weight = 3456, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + MockA!.Weight = 1236; + Assert.That(await ScaleA!.GetCurrentWeight(), Is.EqualTo(new WeighingResult { + Weight = 1236, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + MockB!.Weight = 3457; + Assert.That(await ScaleB!.GetCurrentWeight(), Is.EqualTo(new WeighingResult { + Weight = 3457, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + } + + [Test] + public async Task Test_02_Normal() { + MockA!.Weight = 1234; + Assert.That(await ScaleA!.Weigh(), Is.EqualTo(new WeighingResult { + Weight = 1234, WeighingId = "1", + FullWeighingId = $"2020-10-08/1", + Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + MockB!.Weight = 3456; + Assert.That(await ScaleB!.Weigh(), Is.EqualTo(new WeighingResult { + Weight = 3456, WeighingId = "1", + FullWeighingId = $"2020-10-08/1", + Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + MockA!.Weight = 4321; + Assert.That(await ScaleA!.Weigh(), Is.EqualTo(new WeighingResult { + Weight = 4321, WeighingId = "2", + FullWeighingId = $"2020-10-08/2", + Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + MockB!.Weight = 3333; + Assert.That(await ScaleB!.Weigh(), Is.EqualTo(new WeighingResult { + Weight = 3333, WeighingId = "2", + FullWeighingId = $"2020-10-08/2", + Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47), + })); + } + + [Test] + public void Test_03_Moving() { + MockA!.Weight = 1_000; + MockA!.Error = "moving"; + IOException ex = Assert.ThrowsAsync(async () => await ScaleA!.Weigh()); + Assert.That(ex.Message, Contains.Substring("Waage in Bewegung")); + } + + [Test] + public void Test_04_Overloaded() { + MockA!.Weight = 10_000; + MockA!.Error = "overloaded"; + IOException ex = Assert.ThrowsAsync(async () => await ScaleA!.Weigh()); + Assert.That(ex.Message, Contains.Substring("Waage in Überlast")); + } + + [Test] + public void Test_05_InvalidResponse() { + MockA!.Weight = 1_000; + MockA!.Error = "invalid"; + Assert.ThrowsAsync(async () => await ScaleA!.Weigh()); + } + + [Test] + public void Test_06_InvalidCrc() { + MockA!.Weight = 1_000; + MockA!.Error = "crc"; + IOException ex = Assert.ThrowsAsync(async () => await ScaleA!.Weigh()); + Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum")); + } + + [Test] + public void Test_07_InvalidUnit() { + MockA!.Weight = 1_000; + MockA!.Error = "unit"; + IOException ex = Assert.ThrowsAsync(async () => await ScaleA!.Weigh()); + } + } +}