diff --git a/Elwig/App.xaml.cs b/Elwig/App.xaml.cs index 2644bce..bb674d4 100644 --- a/Elwig/App.xaml.cs +++ b/Elwig/App.xaml.cs @@ -5,6 +5,9 @@ using System.Windows; using System.Windows.Controls; using System.IO; using Elwig.Helpers; +using Elwig.Helpers.Weighing; +using System.Collections.Generic; +using System.Windows.Threading; namespace Elwig { public partial class App : Application { @@ -12,15 +15,17 @@ namespace Elwig { public static readonly string DataPath = @"C:\ProgramData\Elwig\"; public static readonly string ExePath = @"C:\Program Files\Elwig\"; public static readonly Config Config = new(DataPath + "config.ini"); + public static IEnumerable Scales { get; private set; } public static bool IsPrintingReady => Documents.Html.IsReady && Documents.Pdf.IsReady; - public static System.Windows.Threading.Dispatcher MainDispatcher { get; private set; } + public static Dispatcher MainDispatcher { get; private set; } public App() : base() { System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "Elwig")); Directory.CreateDirectory(DataPath); MainDispatcher = Dispatcher; + Scales = Array.Empty(); } protected override void OnStartup(StartupEventArgs evt) { @@ -32,6 +37,25 @@ namespace Elwig { } Utils.RunBackground("HTML Initialization", () => Documents.Html.Init(PrintingReadyChanged)); Utils.RunBackground("PDF Initialization", () => Documents.Pdf.Init(PrintingReadyChanged)); + + var list = new LinkedList(); + foreach (var s in Config.Scales) { + try { + var scaleNr = int.Parse(s[0]); + var type = s[1].ToLower(); + var model = s[2]; + var cnx = s[3]; + var empty = s[4]; + var filling = s[5]; + if (type == "systec") { + list.AddLast(new SystecScale(scaleNr, model, cnx, empty, filling)); + } + } catch (Exception e) { + MessageBox.Show($"Unable to create scale {s[0]}:\n\n{e.Message}", "Scale Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + Scales = list; + base.OnStartup(evt); } diff --git a/Elwig/Elwig.csproj b/Elwig/Elwig.csproj index 9c40515..2ebd116 100644 --- a/Elwig/Elwig.csproj +++ b/Elwig/Elwig.csproj @@ -24,6 +24,7 @@ + diff --git a/Elwig/Helpers/AppDbContext.cs b/Elwig/Helpers/AppDbContext.cs index 0aa3824..b83b5af 100644 --- a/Elwig/Helpers/AppDbContext.cs +++ b/Elwig/Helpers/AppDbContext.cs @@ -43,7 +43,7 @@ namespace Elwig.Helpers { AutoFlush = true }; } catch (Exception e) { - MessageBox.Show($"Unable to open database log file:\n\n{e}", "Database Log", MessageBoxButton.OK, MessageBoxImage.Error); + MessageBox.Show($"Unable to open database log file:\n\n{e.Message}", "Database Log", MessageBoxButton.OK, MessageBoxImage.Error); } } } diff --git a/Elwig/Helpers/Config.cs b/Elwig/Helpers/Config.cs index 3c4f411..4d7e47a 100644 --- a/Elwig/Helpers/Config.cs +++ b/Elwig/Helpers/Config.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using IniParser; using IniParser.Model; @@ -9,9 +11,12 @@ namespace Elwig.Helpers { private readonly string FileName; public string DatabaseFile = App.DataPath + "database.sqlite3"; public string? DatabaseLog = null; + public IEnumerable Scales; + private readonly LinkedList ScaleList = new(); public Config(string filename) { FileName = filename; + Scales = ScaleList; Read(); } @@ -37,12 +42,27 @@ namespace Elwig.Helpers { } else { DatabaseLog = App.DataPath + log; } + + ScaleList.Clear(); + Scales = ScaleList; + if (ini != null) { + foreach (var s in ini.Sections.Where(s => s.SectionName.StartsWith("scale."))) { + ScaleList.AddLast(new string[] { + s.SectionName[6..], s.Keys["type"], s.Keys["model"], s.Keys["connection"], s.Keys["empty"], s.Keys["filling"] + }); + } + } } public void Write() { using var file = new StreamWriter(FileName, false, Encoding.UTF8); file.Write($"\r\n[database]\r\nfile = {DatabaseFile}\r\n"); if (DatabaseLog != null) file.Write($"log = {DatabaseLog}\r\n"); + foreach (var s in ScaleList) { + file.Write($"\r\n[scale.{s[0]}]\r\ntype = {s[1]}\r\nmodel = {s[2]}\r\nconnection = {s[3]}\r\n"); + if (s[4] != null) file.Write($"empty = {s[4]}\r\n"); + if (s[5] != null) file.Write($"filling = {s[5]}\r\n"); + } } } } diff --git a/Elwig/Helpers/Weighing/IScale.cs b/Elwig/Helpers/Weighing/IScale.cs new file mode 100644 index 0000000..51b2e5d --- /dev/null +++ b/Elwig/Helpers/Weighing/IScale.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; + +namespace Elwig.Helpers.Weighing { + /// + /// Interface for controlling a industrial scale + /// + public interface IScale : IDisposable { + /// + /// Manufacturer of the scale + /// + string Manufacturer { get; } + + /// + /// Model of the scale + /// + string Model { get; } + + /// + /// Unique number of the scale + /// + int ScaleNr { get; } + + /// + /// Internal identifying number of the scale in its system + /// + int InternalScaleNr { get; } + + /// + /// Indicates if the scale is currently processing a request or not + /// + bool IsReady { get; } + + /// + /// Indicates if the the clearance for filling the scale container has been granted + /// + bool HasFillingClearance { get; } + + /// + /// Get the current weight on the scale without performing a weighing process + /// + /// Result of the weighing process (probably without a weighing id) + Task GetCurrentWeight(); + + /// + /// Perform a weighing process + /// + /// Result of the weighing process (including a weighing id) + Task Weigh(); + + /// + /// Empty the scale container or grant clearance to do so + /// + Task Empty(); + + /// + /// Grant clearance to fill the scale container + /// + Task GrantFillingClearance(); + + /// + /// Revoke clearance to fill the scale container + /// + Task RevokeFillingClearance(); + } +} diff --git a/Elwig/Helpers/Weighing/SystecScale.cs b/Elwig/Helpers/Weighing/SystecScale.cs new file mode 100644 index 0000000..10bcf83 --- /dev/null +++ b/Elwig/Helpers/Weighing/SystecScale.cs @@ -0,0 +1,227 @@ +using System; +using System.IO; +using System.IO.Ports; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Elwig.Helpers.Weighing { + public class SystecScale : IScale { + + protected enum Output { RTS, DTR, OUT1, OUT2 }; + + protected SerialPort? Serial = null; + protected TcpClient? Tcp = null; + protected StreamReader Reader; + protected StreamWriter Writer; + + protected readonly Output? EmptyMode = null; + protected readonly Output? FillingClearanceMode = null; + protected readonly int EmptyDelay; + + protected static readonly Regex SerialRe = new(@"^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\.5|2|))?$", RegexOptions.Compiled); + protected static readonly Regex TcpRe = new(@"^tcp://[A-Za-z0-9:._-]+(:[0-9]+)?$", RegexOptions.Compiled); + + public string Manufacturer => "SysTec"; + public int InternalScaleNr => 1; + public string Model { get; private set; } + public int ScaleNr { get; private set; } + public bool IsReady { get; private set; } + public bool HasFillingClearance { get; private set; } + + public SystecScale(int scaleNr, string model, string connection, string? empty = null, string? fill = null) { + ScaleNr = scaleNr; + Model = model; + IsReady = true; + HasFillingClearance = false; + + Stream stream; + + if (connection.StartsWith("serial:")) { + var m = SerialRe.Match(connection); + if (!m.Success) + throw new ArgumentException("Invalid connection string for scheme \"serial\""); + + var stop = m.Groups[6].Value; + var parity = m.Groups[5].Value.ToUpper(); + var data = m.Groups[4].Value; + Serial = new() { + PortName = m.Groups[1].Value, + BaudRate = int.Parse(m.Groups[2].Value), + Parity = parity == "E" ? Parity.Even : + parity == "O" ? Parity.Odd : + parity == "M" ? Parity.Mark : + parity == "S" ? Parity.Space : + Parity.None, + DataBits = data == "" ? 8 : int.Parse(data), + StopBits = (StopBits)(stop == "" ? 1 : stop == "1.5" ? 3 : stop[0] - '0'), + Handshake = Handshake.None, + }; + Serial.Open(); + stream = Serial.BaseStream; + } else if (connection.StartsWith("tcp:")) { + var m = TcpRe.Match(connection); + if (!m.Success) + throw new ArgumentException("Invalid connection string for scheme \"tcp\""); + + Tcp = new() { + SendTimeout = 250, + ReceiveTimeout = 250, + }; + Tcp.Connect(m.Groups[1].Value, int.Parse(m.Groups[2].Value)); + stream = Tcp.GetStream(); + } else { + throw new ArgumentException("Unsupported scheme"); + } + + if (empty != null) { + var parts = empty.Split(':'); + EmptyMode = ConvertOutput(parts[0]); + EmptyDelay = int.Parse(parts[1]); + } + FillingClearanceMode = ConvertOutput(fill); + + Writer = new(stream, Encoding.ASCII, -1, true); + Reader = new(stream, Encoding.ASCII, false, -1, true); + } + + public void Dispose() { + Writer.Close(); + Reader.Close(); + Serial?.Close(); + Tcp?.Close(); + GC.SuppressFinalize(this); + } + + protected static Output? ConvertOutput(string? value) { + if (value == null) return null; + value = value.ToUpper(); + if (value == "RTS") { + return Output.RTS; + } else if (value == "DTR") { + return Output.DTR; + } else if (value == "OUT1") { + return Output.OUT1; + } else if (value == "OUT2") { + return Output.OUT2; + } + throw new ArgumentException("Invalid value for argument empty"); + } + + public async Task SendCommand(string command) { + await Writer.WriteAsync($"<{command}>"); + } + + public async Task ReceiveResponse() { + var line = await Reader.ReadLineAsync(); + if (line == null || line.Length < 4 || !line.StartsWith("<") || !line.EndsWith(">")) { + throw new IOException("Invalid response from scale"); + } + + var error = line[1..3]; + if (error[0] == '0') { + if (error[1] != '0') { + throw new IOException($"Invalid response from scale (error code {error}"); + } + } else if (error[0] == '1') { + string msg = $"Unbekannter Fehler (Fehler code {error})"; + switch (error[1]) { + case '1': msg = "Allgemeiner Waagenfehler"; break; + case '2': msg = "Waage in Überlast"; break; + case '3': msg = "Waage in Bewegung"; break; + case '5': msg = "Tarierungs- oder Nullsetzfehler"; break; + case '6': msg = "Drucker nicht bereit"; break; + case '7': msg = "Druckmuster enthält ungültiges Kommando"; break; + } + throw new IOException($"Waagenfehler {error}: {msg}"); + } else if (error[0] == '3') { + string msg = $"Unbekannter Fehler (Fehler code {error})"; + switch (error[1]) { + case '1': msg = "Übertragunsfehler"; break; + case '2': msg = "Ungültiger Befehl"; break; + case '3': msg = "Ungültiger Parameter"; break; + } + throw new IOException($"Kommunikationsfehler {error}: {msg}"); + } else { + throw new IOException($"Invalid response from scale (error code {error})"); + } + + return line[3..(line.Length - 1)]; + } + + protected async Task Weigh(bool incIdentNr) { + await SendCommand(incIdentNr ? $"RM{InternalScaleNr}" : $"RN{InternalScaleNr}"); + string line = await ReceiveResponse(); + + var status = line[ 0.. 2]; + var date = line[ 2..10]; + var time = line[10..15]; + var identNr = line[15..19]; + var scaleNr = line[19..20]; + var brutto = line[20..28]; + var tara = line[28..36]; + var netto = line[36..44]; + var unit = line[44..46]; + var taraCode = line[46..48]; + var zone = line[48..49]; + var terminalNr = line[49..52]; + var crc16 = line[52..60]; + + return new() { + Weight = int.TryParse(netto.Trim(), out int w) ? w : null, + WeighingId = identNr.Trim().Length > 0 ? identNr.Trim() : null, + Date = date, + Time = time, + }; + } + + public async Task Weigh() { + return await Weigh(true); + } + + public async Task GetCurrentWeight() { + return await Weigh(false); + } + + public async Task Empty() { + if (EmptyMode == Output.RTS && Serial != null) { + Serial.RtsEnable = true; + await Task.Delay(EmptyDelay); + Serial.RtsEnable = false; + } else if (EmptyMode == Output.DTR && Serial != null) { + Serial.DtrEnable = true; + await Task.Delay(EmptyDelay); + Serial.DtrEnable = false; + } else if (EmptyMode == Output.OUT1 || EmptyMode == Output.OUT2) { + int output = EmptyMode == Output.OUT1 ? 1 : 2; + await SendCommand($"OS{output:02i}"); + await ReceiveResponse(); + await Task.Delay(EmptyDelay); + await SendCommand($"OC{output:02i}"); + await ReceiveResponse(); + } + } + + protected async Task SetFillingClearance(bool status) { + if (FillingClearanceMode == Output.RTS && Serial != null) { + Serial.RtsEnable = status; + } else if (FillingClearanceMode == Output.DTR && Serial != null) { + Serial.DtrEnable = status; + } else if (FillingClearanceMode == Output.OUT1 || FillingClearanceMode == Output.OUT2) { + string cmd = status ? "OS" : "OC"; + int output = FillingClearanceMode == Output.OUT1 ? 1 : 2; + await SendCommand($"{cmd}{output:02i}"); + await ReceiveResponse(); + } + } + + public async Task GrantFillingClearance() { + await SetFillingClearance(true); + } + + public async Task RevokeFillingClearance() { + await SetFillingClearance(false); + } + } +} diff --git a/Elwig/Helpers/Weighing/WeighingResult.cs b/Elwig/Helpers/Weighing/WeighingResult.cs new file mode 100644 index 0000000..3ba98e7 --- /dev/null +++ b/Elwig/Helpers/Weighing/WeighingResult.cs @@ -0,0 +1,26 @@ +namespace Elwig.Helpers.Weighing { + /// + /// Result of a weighing process on an industrial scale + /// + public class WeighingResult { + /// + /// Measured net weight in kg + /// + public int? Weight = null; + + /// + /// Weighing id (or IdentNr) provided by the scale + /// + public string? WeighingId = null; + + /// + /// Date string provided by the scale + /// + public string? Date = null; + + /// + /// Time string provided by the scale + /// + public string? Time = null; + } +}