[#71] Weighing: Fix reconnection behaviour when COM port is connected/disconnected
Some checks failed
Test / Run tests (push) Failing after 2m47s

This commit is contained in:
2026-01-03 16:22:37 +01:00
parent c45800099c
commit d2bc2f894f
7 changed files with 112 additions and 54 deletions

View File

@@ -149,7 +149,7 @@ namespace Elwig {
} catch (Exception e) { } catch (Exception e) {
list.Add(new InvalidScale(s.Id)); list.Add(new InvalidScale(s.Id));
if (s.Required) if (s.Required)
MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagen-Fehler", MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error); MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -249,15 +249,19 @@ namespace Elwig {
private void OnSerialPortConnected(object? sender, string name) { private void OnSerialPortConnected(object? sender, string name) {
for (var i = 0; i < Config.Scales.Count; i++) { for (var i = 0; i < Config.Scales.Count; i++) {
var s = Config.Scales[i]; var s = Config.Scales[i];
if ((s.Connection?.StartsWith($"serial://{name}:") ?? false) && Scales[i] is InvalidScale) { if (s.Connection?.StartsWith($"serial://{name}:") ?? false) {
if (Scales[i] is InvalidScale) {
try { try {
Scales[i] = Scale.FromConfig(s); Scales[i] = Scale.FromConfig(s);
MessageBox.Show($"Verbindung zu Waage {s.Id} wieder hergestellt!", $"Waage {s.Id}", MessageBoxButton.OK, MessageBoxImage.Information); MessageBox.Show($"Verbindung zu Waage {s.Id} wieder hergestellt!", $"Waage {s.Id}", MessageBoxButton.OK, MessageBoxImage.Information);
} catch (Exception e) { } catch (Exception e) {
Scales[i] = new InvalidScale(s.Id); Scales[i] = new InvalidScale(s.Id);
MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagen-Fehler", MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error); MessageBoxButton.OK, MessageBoxImage.Error);
} }
} else if (Scales[i] is IEventScale) {
MessageBox.Show($"Verbindung zu Waage {s.Id} wieder hergestellt!", $"Waage {s.Id}", MessageBoxButton.OK, MessageBoxImage.Information);
}
} }
} }
UpdateScales(); UpdateScales();
@@ -268,6 +272,7 @@ namespace Elwig {
var s = Config.Scales[i]; var s = Config.Scales[i];
if ((s.Connection?.StartsWith($"serial://{name}:") ?? false) && Scales[i] is not InvalidScale) { if ((s.Connection?.StartsWith($"serial://{name}:") ?? false) && Scales[i] is not InvalidScale) {
MessageBox.Show($"Verbindung zu Waage {s.Id} unterbrochen!", $"Waagen {s.Id}", MessageBoxButton.OK, MessageBoxImage.Warning); MessageBox.Show($"Verbindung zu Waage {s.Id} unterbrochen!", $"Waagen {s.Id}", MessageBoxButton.OK, MessageBoxImage.Warning);
if (Scales[i] is ICommandScale) {
try { try {
Scales[i].Dispose(); Scales[i].Dispose();
} catch { } catch {
@@ -276,6 +281,7 @@ namespace Elwig {
Scales[i] = new InvalidScale(s.Id); Scales[i] = new InvalidScale(s.Id);
} }
} }
}
UpdateScales(); UpdateScales();
} }

View File

@@ -17,28 +17,34 @@ namespace Elwig.Helpers {
public SerialPortWatcher() { public SerialPortWatcher() {
_knownPorts = SerialPort.GetPortNames(); _knownPorts = SerialPort.GetPortNames();
_deviceArrivalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 2"); _deviceArrivalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 2");
_deviceArrivalWatcher.EventArrived += (s, e) => OnDeviceArrived(); _deviceArrivalWatcher.EventArrived += OnDeviceArrived;
_deviceRemovalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 3"); _deviceRemovalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 3");
_deviceRemovalWatcher.EventArrived += (s, e) => OnDeviceRemoved(); _deviceRemovalWatcher.EventArrived += OnDeviceRemoved;
_deviceArrivalWatcher.Start(); _deviceArrivalWatcher.Start();
_deviceRemovalWatcher.Start(); _deviceRemovalWatcher.Start();
} }
private void OnDeviceArrived() { private void OnDeviceArrived(object sender, EventArrivedEventArgs evt) {
App.MainDispatcher.Invoke(() => {
// "synchronized"
string[] currentPorts = SerialPort.GetPortNames(); string[] currentPorts = SerialPort.GetPortNames();
var newPorts = currentPorts.Except(_knownPorts).ToArray(); var newPorts = currentPorts.Except(_knownPorts).ToArray();
foreach (var port in newPorts) foreach (var port in newPorts)
SerialPortConnected?.Invoke(this, port); SerialPortConnected?.Invoke(this, port);
_knownPorts = currentPorts; _knownPorts = currentPorts;
});
} }
private void OnDeviceRemoved() { private void OnDeviceRemoved(object sender, EventArrivedEventArgs evt) {
App.MainDispatcher.Invoke(() => {
// "synchronized"
string[] currentPorts = SerialPort.GetPortNames(); string[] currentPorts = SerialPort.GetPortNames();
var removedPorts = _knownPorts.Except(currentPorts).ToArray(); var removedPorts = _knownPorts.Except(currentPorts).ToArray();
foreach (var port in removedPorts) foreach (var port in removedPorts)
SerialPortDisconnected?.Invoke(this, port); SerialPortDisconnected?.Invoke(this, port);
_knownPorts = currentPorts; _knownPorts = currentPorts;
});
} }
public void Dispose() { public void Dispose() {

View File

@@ -15,15 +15,17 @@ namespace Elwig.Helpers.Weighing {
public bool IsReady { get; private set; } public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; } public bool HasFillingClearance { get; private set; }
public event IEventScale.EventHandler<WeighingEventArgs> WeighingEvent; public event IEventScale.EventHandler<WeighingEventArgs>? WeighingEvent;
private bool IsRunning = true; private bool IsRunning = true;
private readonly Thread BackgroundThread; private readonly Thread BackgroundThread;
private readonly string Connection;
public AveryEventScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) : public AveryEventScale(string id, string model, string cnx, string? log = null, bool required = true) :
base(cnx, empty, filling, limit, log) { base(cnx, null, null, null, log, true, !required) {
ScaleId = id; ScaleId = id;
Model = model; Model = model;
Connection = cnx;
IsReady = true; IsReady = true;
HasFillingClearance = false; HasFillingClearance = false;
Stream.WriteTimeout = -1; Stream.WriteTimeout = -1;
@@ -50,19 +52,49 @@ namespace Elwig.Helpers.Weighing {
var data = await Receive(); var data = await Receive();
if (data != null) if (data != null)
RaiseWeighingEvent(new WeighingEventArgs(data.Value)); RaiseWeighingEvent(new WeighingEventArgs(data.Value));
} catch (ThreadInterruptedException) {
// ignore
} catch (IOException) {
await Task.Delay(500);
await Reconnect();
} catch (TimeoutException) {
await Task.Delay(500);
await Reconnect();
} catch (Exception ex) { } catch (Exception ex) {
MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message}", "Waagenfehler", MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message} ({ex.GetType().Name})", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error); MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
} }
protected async Task Reconnect() {
try { Reader.Close(); } catch { }
try { Stream.Close(); } catch { }
try { Serial?.Close(); } catch { }
while (IsRunning) {
try {
if (Connection.StartsWith("serial:")) {
Serial = Utils.OpenSerialConnection(Connection);
Stream = Serial.BaseStream;
} else if (Connection.StartsWith("tcp:")) {
Tcp = Utils.OpenTcpConnection(Connection);
Stream = Tcp.GetStream();
}
Reader = new(Stream, Encoding.ASCII, false, 512);
break;
} catch {
// ignore
}
await Task.Delay(1000);
}
}
protected async Task<WeighingResult?> Receive() { protected async Task<WeighingResult?> Receive() {
var line = ""; var line = "";
while (line.Length < 33) { while (line.Length < 33) {
var ch = Reader.Read(); var ch = Reader.Read();
if (ch == -1) { if (ch == -1) {
return null; throw new IOException("Connection closed");
} else if (line.Length > 0 || ch == ' ') { } else if (line.Length > 0 || ch == ' ') {
line += char.ToString((char)ch); line += char.ToString((char)ch);
} }
@@ -71,7 +103,7 @@ namespace Elwig.Helpers.Weighing {
if (line == null || line == "") { if (line == null || line == "") {
return null; return null;
} else if (line.Length != 33 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') { } else if (line.Length != 33 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') {
throw new IOException($"Invalid event from scale: '{line}'"); throw new FormatException($"Invalid event from scale: '{line}'");
} }
var date = line[ 1.. 9]; var date = line[ 1.. 9];
@@ -81,7 +113,7 @@ namespace Elwig.Helpers.Weighing {
var unit = line[30..32]; var unit = line[30..32];
if (unit != "kg") { if (unit != "kg") {
throw new IOException($"Unsupported unit in weighing event: '{unit}'"); throw new WeighingException($"Unsupported unit in weighing event: '{unit}'");
} }
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null; identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;

View File

@@ -36,7 +36,7 @@ namespace Elwig.Helpers.Weighing {
var line = await Reader.ReadUntilAsync('\x03'); var line = await Reader.ReadUntilAsync('\x03');
if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n"); if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n");
if (line == null || line.Length < 4 || !line.StartsWith('\x02')) { if (line == null || line.Length < 4 || !line.StartsWith('\x02')) {
throw new IOException("Invalid response from scale"); throw new FormatException("Invalid response from scale");
} }
var status = line[1..3]; var status = line[1..3];
@@ -45,9 +45,9 @@ namespace Elwig.Helpers.Weighing {
switch (status[1]) { switch (status[1]) {
case 'M': msg = "Waage in Bewegung"; break; case 'M': msg = "Waage in Bewegung"; break;
} }
throw new IOException($"Waagenfehler {status}: {msg}"); throw new WeighingException($"Waagenfehler {status}: {msg}");
} else if (status[0] != ' ') { } else if (status[0] != ' ') {
throw new IOException($"Invalid response from scale (error code {status})"); throw new WeighingException($"Invalid response from scale (error code {status})");
} }
return line[1..^1]; return line[1..^1];
@@ -57,7 +57,7 @@ namespace Elwig.Helpers.Weighing {
await SendCommand(incIdentNr ? '\x05' : '?'); await SendCommand(incIdentNr ? '\x05' : '?');
string record = await ReceiveResponse(); string record = await ReceiveResponse();
if (record.Length != 45) if (record.Length != 45)
throw new IOException("Invalid response from scale: Received record has invalid size"); throw new FormatException("Invalid response from scale: Received record has invalid size");
var line = record[2..]; var line = record[2..];
var brutto = line[ 0.. 7].Trim(); var brutto = line[ 0.. 7].Trim();

View File

@@ -1,8 +1,9 @@
using System.IO.Ports;
using System.IO;
using System.Net.Sockets;
using System; using System;
using System.IO;
using System.IO.Ports;
using System.Net.Sockets;
using System.Text; using System.Text;
using System.Windows;
namespace Elwig.Helpers.Weighing { namespace Elwig.Helpers.Weighing {
public abstract class Scale : IDisposable { public abstract class Scale : IDisposable {
@@ -27,7 +28,7 @@ namespace Elwig.Helpers.Weighing {
if (config.Type == "SysTec-IT") { if (config.Type == "SysTec-IT") {
return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else if (config.Type == "Avery-Async") { } else if (config.Type == "Avery-Async") {
return new AveryEventScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); return new AveryEventScale(config.Id, config.Model!, config.Connection!, config.Log, config.Required);
} else if (config.Type == "Gassner") { } else if (config.Type == "Gassner") {
return new GassnerScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); return new GassnerScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else { } else {
@@ -35,10 +36,17 @@ namespace Elwig.Helpers.Weighing {
} }
} }
protected Scale(string cnx, string? empty, string? filling, int? limit, string? log) { protected Scale(string cnx, string? empty, string? filling, int? limit, string? log, bool softFail = false, bool failSilent = false) {
if (cnx.StartsWith("serial:")) { if (cnx.StartsWith("serial:")) {
try {
Serial = Utils.OpenSerialConnection(cnx); Serial = Utils.OpenSerialConnection(cnx);
Stream = Serial.BaseStream; } catch (Exception e) {
if (!softFail) throw;
if (!failSilent)
MessageBox.Show($"Verbindung zu Waage konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
Stream = Serial?.BaseStream ?? Stream.Null;
} else if (cnx.StartsWith("tcp:")) { } else if (cnx.StartsWith("tcp:")) {
Tcp = Utils.OpenTcpConnection(cnx); Tcp = Utils.OpenTcpConnection(cnx);
Stream = Tcp.GetStream(); Stream = Tcp.GetStream();

View File

@@ -34,14 +34,14 @@ namespace Elwig.Helpers.Weighing {
var line = await Reader.ReadUntilAsync("\r\n"); var line = await Reader.ReadUntilAsync("\r\n");
if (LogPath != null) await File.AppendAllTextAsync(LogPath, line); if (LogPath != null) await File.AppendAllTextAsync(LogPath, line);
if (line == null || line.Length < 4 || !line.StartsWith('<') || !line.EndsWith(">\r\n")) { if (line == null || line.Length < 4 || !line.StartsWith('<') || !line.EndsWith(">\r\n")) {
throw new IOException("Invalid response from scale"); throw new FormatException("Invalid response from scale");
} }
var error = line[1..3]; var error = line[1..3];
string msg = $"Unbekannter Fehler (Fehler code {error})"; string msg = $"Unbekannter Fehler (Fehler code {error})";
if (error[0] == '0') { if (error[0] == '0') {
if (error[1] != '0') { if (error[1] != '0') {
throw new IOException($"Invalid response from scale (error code {error})"); throw new WeighingException($"Invalid response from scale (error code {error})");
} }
} else if (error[0] == '1') { } else if (error[0] == '1') {
switch (error[1]) { switch (error[1]) {
@@ -52,21 +52,21 @@ namespace Elwig.Helpers.Weighing {
case '6': msg = "Drucker nicht bereit"; break; case '6': msg = "Drucker nicht bereit"; break;
case '7': msg = "Druckmuster enthält ungültiges Kommando"; break; case '7': msg = "Druckmuster enthält ungültiges Kommando"; break;
} }
throw new IOException($"Waagenfehler {error}: {msg}"); throw new WeighingException($"Waagenfehler {error}: {msg}");
} else if (error[0] == '2') { } else if (error[0] == '2') {
switch (error[1]) { switch (error[1]) {
case '0': msg = "Brutto negativ"; break; case '0': msg = "Brutto negativ"; break;
} }
throw new IOException($"Fehler {error}: {msg}"); throw new WeighingException($"Fehler {error}: {msg}");
} else if (error[0] == '3') { } else if (error[0] == '3') {
switch (error[1]) { switch (error[1]) {
case '1': msg = "Übertragunsfehler"; break; case '1': msg = "Übertragunsfehler"; break;
case '2': msg = "Ungültiger Befehl"; break; case '2': msg = "Ungültiger Befehl"; break;
case '3': msg = "Ungültiger Parameter"; break; case '3': msg = "Ungültiger Parameter"; break;
} }
throw new IOException($"Kommunikationsfehler {error}: {msg}"); throw new WeighingException($"Kommunikationsfehler {error}: {msg}");
} else { } else {
throw new IOException($"Invalid response from scale (error code {error})"); throw new WeighingException($"Invalid response from scale (error code {error})");
} }
return line[1..^3]; return line[1..^3];
@@ -76,7 +76,7 @@ namespace Elwig.Helpers.Weighing {
await SendCommand(incIdentNr ? $"RN{InternalScaleNr}" : $"RM{InternalScaleNr}"); await SendCommand(incIdentNr ? $"RN{InternalScaleNr}" : $"RM{InternalScaleNr}");
string record = await ReceiveResponse(); string record = await ReceiveResponse();
if (record.Length != 62) if (record.Length != 62)
throw new IOException("Invalid response from scale: Received record has invalid size"); throw new FormatException("Invalid response from scale: Received record has invalid size");
var line = record[2..]; var line = record[2..];
var status = line[ 0.. 2]; var status = line[ 0.. 2];
@@ -94,9 +94,9 @@ namespace Elwig.Helpers.Weighing {
var crc16 = line[52..60].Trim(); var crc16 = line[52..60].Trim();
if (Utils.CalcCrc16Modbus(record[..54]) != ushort.Parse(crc16)) { if (Utils.CalcCrc16Modbus(record[..54]) != ushort.Parse(crc16)) {
throw new IOException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54])})"); throw new WeighingException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54])})");
} else if (unit != "kg") { } else if (unit != "kg") {
throw new IOException($"Unsupported unit in weighing response: '{unit}'"); throw new WeighingException($"Unsupported unit in weighing response: '{unit}'");
} }
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null; identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;

View File

@@ -0,0 +1,6 @@
using System;
namespace Elwig.Helpers.Weighing {
class WeighingException(string? message = null) : Exception(message) {
}
}