154 Zeilen
5.0 KiB
C#
154 Zeilen
5.0 KiB
C#
using System.IO;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using PrinterMonitor.Configuration;
|
|
using PrinterMonitor.Interfaces;
|
|
using PrinterMonitor.Models;
|
|
|
|
namespace PrinterMonitor.Monitors;
|
|
|
|
/// <summary>
|
|
/// IPrinterMonitor-Implementierung für Zebra Drucker via TCP mit JSON-Nachrichten.
|
|
///
|
|
/// Protokoll (Zebra SGD über TCP, Standard-Port 9200):
|
|
/// Senden: {}{\"sensor.peeler\":null,\"head.latch\":null,\"media.status\":null}
|
|
/// Empfangen: {\"sensor.peeler\":\"clear\",\"head.latch\":\"ok\",\"media.status\":\"ok\"}
|
|
///
|
|
/// Werte-Mapping:
|
|
/// sensor.peeler: "clear" = LTS frei, "not clear" = LTS belegt
|
|
/// head.latch: "ok" = Klappe geschlossen, "open" = Klappe offen
|
|
/// media.status: "ok" = Etiketten vorhanden, "out" = Etiketten leer
|
|
/// </summary>
|
|
public class ZebraMonitor : IPrinterMonitor
|
|
{
|
|
private static readonly string Query =
|
|
"{}{\"sensor.peeler\":null,\"head.latch\":null,\"media.status\":null}";
|
|
|
|
// 256-Byte-Lesepuffer: reduziert System-Calls drastisch gegenüber 1-Byte-Buffer.
|
|
// Als Instanzfeld gecacht – keine Allokation pro Poll-Zyklus.
|
|
private readonly byte[] _readBuffer = new byte[256];
|
|
|
|
// StringBuilder als Instanzfeld: wird bei jedem Poll nur geleert, nie neu allokiert.
|
|
private readonly StringBuilder _responseBuilder = new(256);
|
|
|
|
private readonly PrinterConfig _config;
|
|
private TcpClient? _tcpClient;
|
|
private NetworkStream? _stream;
|
|
|
|
// Wird auf true gesetzt wenn ein Read/Write-Fehler oder EOF erkannt wird
|
|
private volatile bool _isDisconnected = true;
|
|
|
|
public string PrinterName => _config.Name;
|
|
public bool IsConnected => _tcpClient?.Connected == true && !_isDisconnected;
|
|
|
|
public ZebraMonitor(PrinterConfig config)
|
|
{
|
|
_config = config;
|
|
}
|
|
|
|
public async Task ConnectAsync(CancellationToken ct = default)
|
|
{
|
|
_isDisconnected = false;
|
|
var client = new TcpClient();
|
|
try
|
|
{
|
|
await client.ConnectAsync(_config.Host, _config.Port, ct);
|
|
_stream = client.GetStream();
|
|
_tcpClient = client;
|
|
}
|
|
catch
|
|
{
|
|
client.Dispose();
|
|
_isDisconnected = true;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public Task DisconnectAsync()
|
|
{
|
|
_stream?.Dispose();
|
|
_tcpClient?.Dispose();
|
|
_stream = null;
|
|
_tcpClient = null;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task<SimplePrinterState> PollStateAsync(CancellationToken ct = default)
|
|
{
|
|
if (_isDisconnected || _tcpClient == null || _stream == null || !_tcpClient.Connected)
|
|
throw new IOException("Verbindung zum Zebra-Drucker getrennt");
|
|
|
|
// Anfrage senden
|
|
var queryBytes = Encoding.UTF8.GetBytes(Query);
|
|
await _stream.WriteAsync(queryBytes, ct);
|
|
await _stream.FlushAsync(ct);
|
|
|
|
// Antwort lesen bis vollständiges JSON-Objekt
|
|
var json = await ReadJsonResponseAsync(ct);
|
|
|
|
// JSON parsen
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
|
|
var peeler = root.GetProperty("sensor.peeler").GetString() ?? "";
|
|
var latch = root.GetProperty("head.latch").GetString() ?? "";
|
|
var media = root.GetProperty("media.status").GetString() ?? "";
|
|
|
|
return new SimplePrinterState
|
|
{
|
|
LtsSensor = !peeler.Equals("clear", StringComparison.OrdinalIgnoreCase),
|
|
Druckerklappe = latch.Equals("open", StringComparison.OrdinalIgnoreCase),
|
|
KeineEtiketten = media.Equals("out", StringComparison.OrdinalIgnoreCase)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Liest die JSON-Antwort vom Stream bis die schließende Klammer } erreicht ist.
|
|
/// Nutzt einen 256-Byte-Puffer statt byteweisem Lesen, um System-Calls zu minimieren.
|
|
/// Zebra sendet CRLF innerhalb des JSON — das ist gültiger JSON-Whitespace.
|
|
/// </summary>
|
|
private async Task<string> ReadJsonResponseAsync(CancellationToken ct)
|
|
{
|
|
_responseBuilder.Clear();
|
|
int braceDepth = 0;
|
|
bool started = false;
|
|
bool done = false;
|
|
|
|
while (!ct.IsCancellationRequested && !done)
|
|
{
|
|
var bytesRead = await _stream!.ReadAsync(_readBuffer, ct);
|
|
if (bytesRead == 0)
|
|
{
|
|
_isDisconnected = true;
|
|
throw new IOException("Verbindung vom Zebra-Drucker geschlossen");
|
|
}
|
|
|
|
for (int i = 0; i < bytesRead && !done; i++)
|
|
{
|
|
char c = (char)_readBuffer[i];
|
|
_responseBuilder.Append(c);
|
|
|
|
if (c == '{')
|
|
{
|
|
braceDepth++;
|
|
started = true;
|
|
}
|
|
else if (c == '}')
|
|
{
|
|
braceDepth--;
|
|
if (started && braceDepth == 0)
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return _responseBuilder.ToString();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await DisconnectAsync();
|
|
}
|
|
}
|