You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
426 lines
11 KiB
426 lines
11 KiB
using System.IO.Ports;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Housing.Services;
|
|
|
|
public sealed class BarcodeScanReceivedEventArgs : EventArgs
|
|
{
|
|
public BarcodeScanReceivedEventArgs(string portName, string text, bool isConfirmedPort)
|
|
{
|
|
PortName = portName;
|
|
Text = text;
|
|
IsConfirmedPort = isConfirmedPort;
|
|
}
|
|
|
|
public string PortName { get; }
|
|
public string Text { get; }
|
|
public bool IsConfirmedPort { get; }
|
|
}
|
|
|
|
public sealed class SerialBarcodeScanner : IDisposable
|
|
{
|
|
private readonly BarcodeScannerSettings _settings;
|
|
private readonly HashSet<string> _excludedPortNames;
|
|
private readonly List<PortSession> _sessions = new();
|
|
private readonly object _sync = new();
|
|
private bool _disposed;
|
|
private string? _confirmedPortName;
|
|
|
|
public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable<string> excludedPortNames)
|
|
{
|
|
_settings = settings;
|
|
_excludedPortNames = excludedPortNames
|
|
.Where(portName => !string.IsNullOrWhiteSpace(portName))
|
|
.Select(NormalizePortName)
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public event EventHandler<BarcodeScanReceivedEventArgs>? ScanReceived;
|
|
|
|
public string? ConfirmedPortName
|
|
{
|
|
get
|
|
{
|
|
lock (_sync)
|
|
{
|
|
return _confirmedPortName;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
var candidatePortNames = GetCandidatePortNames();
|
|
if (candidatePortNames.Length == 0)
|
|
{
|
|
throw new InvalidOperationException("No available COM ports for the barcode scanner.");
|
|
}
|
|
|
|
var openErrors = new List<string>();
|
|
foreach (var portName in candidatePortNames)
|
|
{
|
|
PortSession? session = null;
|
|
try
|
|
{
|
|
var port = CreateSerialPort(portName);
|
|
session = new PortSession(port, CommitBufferedScan);
|
|
|
|
port.DataReceived += SerialPort_DataReceived;
|
|
lock (_sync)
|
|
{
|
|
_sessions.Add(session);
|
|
}
|
|
|
|
port.Open();
|
|
port.DiscardInBuffer();
|
|
port.DiscardOutBuffer();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (session is not null)
|
|
{
|
|
lock (_sync)
|
|
{
|
|
_sessions.Remove(session);
|
|
}
|
|
|
|
CloseSession(session);
|
|
}
|
|
|
|
openErrors.Add($"{portName}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
int sessionCount;
|
|
lock (_sync)
|
|
{
|
|
sessionCount = _sessions.Count;
|
|
}
|
|
|
|
if (sessionCount == 0)
|
|
{
|
|
var detail = openErrors.Count == 0 ? string.Empty : $" ({string.Join("; ", openErrors)})";
|
|
throw new InvalidOperationException($"Could not open a barcode scanner COM port.{detail}");
|
|
}
|
|
|
|
if (!BarcodeScannerSettings.IsAutoPort(_settings.PortName) || sessionCount == 1)
|
|
{
|
|
lock (_sync)
|
|
{
|
|
_confirmedPortName = _sessions[0].Port.PortName;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ConfirmPort(string portName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(portName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
List<PortSession> sessionsToClose;
|
|
lock (_sync)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var normalizedPortName = NormalizePortName(portName);
|
|
if (!string.IsNullOrWhiteSpace(_confirmedPortName) &&
|
|
!string.Equals(_confirmedPortName, normalizedPortName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var confirmedSession = _sessions.FirstOrDefault(session =>
|
|
string.Equals(session.Port.PortName, normalizedPortName, StringComparison.OrdinalIgnoreCase));
|
|
if (confirmedSession is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_confirmedPortName = confirmedSession.Port.PortName;
|
|
sessionsToClose = _sessions
|
|
.Where(session => !ReferenceEquals(session, confirmedSession))
|
|
.ToList();
|
|
|
|
_sessions.RemoveAll(session => !ReferenceEquals(session, confirmedSession));
|
|
}
|
|
|
|
foreach (var session in sessionsToClose)
|
|
{
|
|
CloseSession(session);
|
|
}
|
|
}
|
|
|
|
private SerialPort CreateSerialPort(string portName)
|
|
{
|
|
return new SerialPort(portName, _settings.BaudRate)
|
|
{
|
|
Encoding = Encoding.ASCII,
|
|
NewLine = "\r\n",
|
|
ReadTimeout = _settings.ReadTimeout,
|
|
WriteTimeout = _settings.ReadTimeout,
|
|
DtrEnable = _settings.DtrEnable,
|
|
RtsEnable = _settings.RtsEnable
|
|
};
|
|
}
|
|
|
|
private string[] GetCandidatePortNames()
|
|
{
|
|
if (!BarcodeScannerSettings.IsAutoPort(_settings.PortName))
|
|
{
|
|
return new[] { NormalizePortName(_settings.PortName) };
|
|
}
|
|
|
|
return SerialPort.GetPortNames()
|
|
.Select(NormalizePortName)
|
|
.Where(portName => !_excludedPortNames.Contains(portName))
|
|
.OrderBy(GetPortNumber)
|
|
.ThenBy(portName => portName, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
}
|
|
|
|
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
|
|
{
|
|
if (sender is not SerialPort port)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PortSession? session;
|
|
lock (_sync)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
session = _sessions.FirstOrDefault(item => ReferenceEquals(item.Port, port));
|
|
}
|
|
|
|
if (session is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string text;
|
|
try
|
|
{
|
|
text = port.ReadExisting();
|
|
}
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var scans = AppendReceivedText(session, text);
|
|
foreach (var scan in scans)
|
|
{
|
|
PublishScan(session, scan);
|
|
}
|
|
}
|
|
|
|
private List<string> AppendReceivedText(PortSession session, string text)
|
|
{
|
|
lock (session.Sync)
|
|
{
|
|
session.Buffer.Append(text);
|
|
var scans = DrainCompleteScans(session.Buffer);
|
|
|
|
if (session.Buffer.Length > 0)
|
|
{
|
|
var delay = Math.Max(50, _settings.IdleCommitMilliseconds);
|
|
session.CommitTimer.Change(delay, Timeout.Infinite);
|
|
}
|
|
else
|
|
{
|
|
session.CommitTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
|
|
return scans;
|
|
}
|
|
}
|
|
|
|
private void CommitBufferedScan(PortSession session)
|
|
{
|
|
string scan;
|
|
lock (session.Sync)
|
|
{
|
|
scan = NormalizeScanText(session.Buffer.ToString());
|
|
session.Buffer.Clear();
|
|
session.CommitTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
|
|
PublishScan(session, scan);
|
|
}
|
|
|
|
private void PublishScan(PortSession session, string scan)
|
|
{
|
|
scan = NormalizeScanText(scan);
|
|
if (string.IsNullOrWhiteSpace(scan))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool isConfirmedPort;
|
|
lock (_sync)
|
|
{
|
|
if (_disposed || !_sessions.Contains(session))
|
|
{
|
|
return;
|
|
}
|
|
|
|
isConfirmedPort = string.Equals(
|
|
_confirmedPortName,
|
|
session.Port.PortName,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scan, isConfirmedPort));
|
|
}
|
|
|
|
private static List<string> DrainCompleteScans(StringBuilder buffer)
|
|
{
|
|
var scans = new List<string>();
|
|
var text = buffer.ToString();
|
|
var start = 0;
|
|
|
|
for (var index = 0; index < text.Length; index++)
|
|
{
|
|
if (text[index] != '\r' && text[index] != '\n')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var line = NormalizeScanText(text[start..index]);
|
|
if (line.Length > 0)
|
|
{
|
|
scans.Add(line);
|
|
}
|
|
|
|
if (text[index] == '\r' && index + 1 < text.Length && text[index + 1] == '\n')
|
|
{
|
|
index++;
|
|
}
|
|
|
|
start = index + 1;
|
|
}
|
|
|
|
if (start > 0)
|
|
{
|
|
buffer.Clear();
|
|
if (start < text.Length)
|
|
{
|
|
buffer.Append(text[start..]);
|
|
}
|
|
}
|
|
|
|
return scans;
|
|
}
|
|
|
|
private static string NormalizeScanText(string text)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var builder = new StringBuilder(text.Length);
|
|
foreach (var character in text.Trim())
|
|
{
|
|
if (!char.IsControl(character))
|
|
{
|
|
builder.Append(character);
|
|
}
|
|
}
|
|
|
|
return builder.ToString().Trim();
|
|
}
|
|
|
|
private static string NormalizePortName(string portName)
|
|
{
|
|
return portName.Trim().ToUpperInvariant();
|
|
}
|
|
|
|
private static int GetPortNumber(string portName)
|
|
{
|
|
var match = Regex.Match(portName, @"\d+");
|
|
return match.Success && int.TryParse(match.Value, out var number)
|
|
? number
|
|
: int.MaxValue;
|
|
}
|
|
|
|
private void ThrowIfDisposed()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
}
|
|
|
|
private void CloseSession(PortSession session)
|
|
{
|
|
try
|
|
{
|
|
session.Port.DataReceived -= SerialPort_DataReceived;
|
|
}
|
|
catch
|
|
{
|
|
// Best effort during shutdown.
|
|
}
|
|
|
|
session.Dispose();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
List<PortSession> sessionsToClose;
|
|
lock (_sync)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
sessionsToClose = _sessions.ToList();
|
|
_sessions.Clear();
|
|
}
|
|
|
|
foreach (var session in sessionsToClose)
|
|
{
|
|
CloseSession(session);
|
|
}
|
|
}
|
|
|
|
private sealed class PortSession : IDisposable
|
|
{
|
|
private readonly Action<PortSession> _commitBufferedScan;
|
|
|
|
public PortSession(SerialPort port, Action<PortSession> commitBufferedScan)
|
|
{
|
|
Port = port;
|
|
_commitBufferedScan = commitBufferedScan;
|
|
CommitTimer = new Timer(_ => _commitBufferedScan(this), null, Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
|
|
public SerialPort Port { get; }
|
|
public StringBuilder Buffer { get; } = new();
|
|
public object Sync { get; } = new();
|
|
public Timer CommitTimer { get; }
|
|
|
|
public void Dispose()
|
|
{
|
|
CommitTimer.Dispose();
|
|
Port.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|