277 Zeilen
9.4 KiB
C#
277 Zeilen
9.4 KiB
C#
using System.Collections.Concurrent;
|
|
using PrinterMonitor.Configuration;
|
|
using PrinterMonitor.Interfaces;
|
|
using PrinterMonitor.Models;
|
|
using PrinterMonitor.Monitors;
|
|
using PrinterMonitor.Tcp;
|
|
|
|
namespace PrinterMonitor.Services;
|
|
|
|
/// <summary>
|
|
/// Verwaltet alle konfigurierten Drucker:
|
|
/// - Erzeugt Monitor pro Drucker, pollt mit festem Intervall (300 ms)
|
|
/// - Aggregiert alle Druckerzustände per ODER-Verknüpfung
|
|
/// - Pusht bei Zustandsänderung sofort, sendet alle 500 ms einen Heartbeat
|
|
/// - Ein einziger TCP-Client verbindet sich auf localhost:TcpTargetPort (Default 12164)
|
|
/// </summary>
|
|
public class PrinterService : IAsyncDisposable
|
|
{
|
|
private const int PollingIntervalMs = 300;
|
|
private const int ReconnectDelayMs = 5000;
|
|
private const int HeartbeatIntervalMs = 500;
|
|
|
|
private readonly AppSettings _settings;
|
|
private readonly ConcurrentDictionary<string, IPrinterMonitor> _monitors = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly ConcurrentDictionary<string, PrinterStatus> _statusCache = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly ConcurrentDictionary<string, SimplePrinterState?> _lastStates = new(StringComparer.OrdinalIgnoreCase);
|
|
private TcpPushClient? _tcpClient;
|
|
private SimplePrinterState? _lastAggregatedState;
|
|
private readonly object _aggregationLock = new();
|
|
private CancellationTokenSource? _cts;
|
|
private readonly List<Task> _pollingTasks = new();
|
|
|
|
public PrinterService(AppSettings settings)
|
|
{
|
|
_settings = settings;
|
|
}
|
|
|
|
public async Task StartAsync()
|
|
{
|
|
_cts = new CancellationTokenSource();
|
|
|
|
_tcpClient = new TcpPushClient("localhost", _settings.TcpTargetPort);
|
|
_tcpClient.Start();
|
|
|
|
foreach (var config in _settings.Printers.Where(p => p.Enabled))
|
|
{
|
|
var monitor = CreateMonitor(config);
|
|
_monitors[config.Name] = monitor;
|
|
|
|
_statusCache[config.Name] = new PrinterStatus
|
|
{
|
|
PrinterName = config.Name,
|
|
PrinterType = config.Type,
|
|
IsOnline = false,
|
|
ErrorMessage = "Noch nicht abgefragt"
|
|
};
|
|
_lastStates[config.Name] = null;
|
|
|
|
_pollingTasks.Add(RunPollingLoop(config, monitor, _cts.Token));
|
|
}
|
|
|
|
_pollingTasks.Add(PeriodicBroadcastLoop(_cts.Token));
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public async Task StopAsync()
|
|
{
|
|
_cts?.Cancel();
|
|
|
|
try { await Task.WhenAll(_pollingTasks); }
|
|
catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Fehler beim Stoppen der Polling-Tasks: {ex.Message}"); }
|
|
_pollingTasks.Clear();
|
|
|
|
foreach (var monitor in _monitors.Values)
|
|
{
|
|
try { await monitor.DisconnectAsync(); }
|
|
catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Fehler beim Trennen von {monitor.PrinterName}: {ex.Message}"); }
|
|
}
|
|
_monitors.Clear();
|
|
_lastStates.Clear();
|
|
_statusCache.Clear();
|
|
_lastAggregatedState = null;
|
|
|
|
if (_tcpClient != null)
|
|
{
|
|
try { await _tcpClient.DisposeAsync(); }
|
|
catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Fehler beim Stoppen des TCP-Clients: {ex.Message}"); }
|
|
_tcpClient = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Startet das Monitoring mit der aktuellen Konfiguration neu.
|
|
/// Wird nach dem Speichern der Einstellungen aufgerufen.
|
|
/// </summary>
|
|
public async Task RestartAsync()
|
|
{
|
|
await StopAsync();
|
|
await StartAsync();
|
|
}
|
|
|
|
public IReadOnlyList<string> GetPrinterNames()
|
|
=> _statusCache.Keys.ToList().AsReadOnly();
|
|
|
|
public PrinterStatus? GetStatus(string printerName)
|
|
{
|
|
_statusCache.TryGetValue(printerName, out var status);
|
|
return status;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Setzt den Zustand eines Simulation-Druckers manuell.
|
|
/// </summary>
|
|
public void SetSimulationState(string printerName, bool ltsSensor, bool druckerklappe, bool keineEtiketten)
|
|
{
|
|
if (_monitors.TryGetValue(printerName, out var monitor) && monitor is SimulationMonitor sim)
|
|
{
|
|
sim.SetState(ltsSensor, druckerklappe, keineEtiketten);
|
|
}
|
|
}
|
|
|
|
private IPrinterMonitor CreateMonitor(PrinterConfig config)
|
|
{
|
|
// Defensiv: alten korrupten Wert "System.Windows.Controls.ComboBoxItem: X" bereinigen
|
|
var type = config.Type ?? "";
|
|
if (type.Contains(':'))
|
|
type = type.Split(':').Last().Trim();
|
|
|
|
return type.ToLowerInvariant() switch
|
|
{
|
|
"cabsquix" => new CabSquixMonitor(config),
|
|
"zebra" => new ZebraMonitor(config),
|
|
"honeywell" => new HoneywellMonitor(config),
|
|
"simulation" => new SimulationMonitor(config),
|
|
_ => throw new NotSupportedException($"Druckertyp '{config.Type}' wird nicht unterstützt.")
|
|
};
|
|
}
|
|
|
|
private async Task RunPollingLoop(PrinterConfig config, IPrinterMonitor monitor, CancellationToken ct)
|
|
{
|
|
var name = config.Name;
|
|
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
if (!monitor.IsConnected)
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {name}: Verbinde...");
|
|
await monitor.ConnectAsync(ct);
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {name}: Verbunden.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {name}: Verbindung fehlgeschlagen – {ex.Message}");
|
|
UpdateStatus(name, config.Type, online: false, error: $"Verbindungsfehler: {ex.Message}");
|
|
await SafeDelay(ReconnectDelayMs, ct);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
var newState = await monitor.PollStateAsync(ct);
|
|
UpdateStatus(name, config.Type, online: true, state: newState);
|
|
_lastStates[name] = newState;
|
|
|
|
await BroadcastIfChangedAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {name}: Polling-Fehler – {ex.Message}");
|
|
UpdateStatus(name, config.Type, online: false, error: ex.Message);
|
|
_lastStates[name] = null;
|
|
|
|
// Sofort aggregieren & senden damit der offline-State nicht im Puffer bleibt
|
|
await BroadcastIfChangedAsync();
|
|
|
|
try { await monitor.DisconnectAsync(); }
|
|
catch (Exception dex) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {name}: Fehler beim Trennen – {dex.Message}"); }
|
|
await SafeDelay(ReconnectDelayMs, ct);
|
|
continue;
|
|
}
|
|
|
|
await SafeDelay(PollingIntervalMs, ct);
|
|
}
|
|
}
|
|
|
|
private async Task BroadcastIfChangedAsync()
|
|
{
|
|
string? payload;
|
|
|
|
lock (_aggregationLock)
|
|
{
|
|
SimplePrinterState? aggregated = null;
|
|
foreach (var s in _lastStates.Values)
|
|
{
|
|
if (s == null) continue;
|
|
if (aggregated == null)
|
|
{
|
|
aggregated = s;
|
|
continue;
|
|
}
|
|
aggregated = new SimplePrinterState
|
|
{
|
|
LtsSensor = aggregated.LtsSensor || s.LtsSensor,
|
|
Druckerklappe = aggregated.Druckerklappe || s.Druckerklappe,
|
|
KeineEtiketten = aggregated.KeineEtiketten || s.KeineEtiketten
|
|
};
|
|
}
|
|
|
|
if (aggregated == null)
|
|
{
|
|
_lastAggregatedState = null;
|
|
return;
|
|
}
|
|
|
|
if (_lastAggregatedState != null && aggregated.Equals(_lastAggregatedState))
|
|
return;
|
|
|
|
_lastAggregatedState = aggregated;
|
|
payload = aggregated.ToTcpString();
|
|
}
|
|
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Aggregiert: Zustandsänderung -> {payload}");
|
|
|
|
if (_tcpClient != null)
|
|
await _tcpClient.SendStateAsync(payload);
|
|
}
|
|
|
|
private async Task PeriodicBroadcastLoop(CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
await SafeDelay(HeartbeatIntervalMs, ct);
|
|
|
|
string? payload;
|
|
lock (_aggregationLock)
|
|
{
|
|
if (_lastAggregatedState == null) continue;
|
|
payload = _lastAggregatedState.ToTcpString();
|
|
}
|
|
|
|
if (_tcpClient != null)
|
|
await _tcpClient.SendStateAsync(payload);
|
|
}
|
|
}
|
|
|
|
private void UpdateStatus(string name, string type, bool online,
|
|
SimplePrinterState? state = null, string? error = null)
|
|
{
|
|
_statusCache[name] = new PrinterStatus
|
|
{
|
|
PrinterName = name,
|
|
PrinterType = type,
|
|
IsOnline = online,
|
|
LastUpdated = DateTime.Now,
|
|
ErrorMessage = error,
|
|
LtsSensor = state?.LtsSensor,
|
|
Druckerklappe = state?.Druckerklappe,
|
|
KeineEtiketten = state?.KeineEtiketten
|
|
};
|
|
}
|
|
|
|
private static async Task SafeDelay(int ms, CancellationToken ct)
|
|
{
|
|
try { await Task.Delay(ms, ct); }
|
|
catch (OperationCanceledException) { }
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await StopAsync();
|
|
_cts?.Dispose();
|
|
}
|
|
}
|