Du hast auf den Branch log-test-branch 2026-04-12 20:57:35 +00:00 gepusht
Neuer Pull-Request
Du hast auf den Branch anon-test2 2026-04-12 20:50:35 +00:00 gepusht
Neuer Pull-Request
Dateien
Soft-LTS/Monitors/SquixOpcUaClient.cs
2026-04-12 17:39:45 +02:00

205 Zeilen
7.7 KiB
C#

using System.IO;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
namespace PrinterMonitor.Monitors;
/// <summary>
/// Minimaler OPC UA Client für CAB Squix Etikettendrucker.
/// Liest ausschließlich die drei überwachten Sensorzustände:
/// LTS-Sensor (ns=3;i=22002), Druckkopf-Klappe (ns=3;i=10076), Papier leer (ns=3;i=10019).
///
/// Konfiguration und CertificateValidator werden einmalig im Konstruktor
/// erstellt (nicht bei jedem ConnectAsync neu), um unnötige Allokationen
/// und Event-Leaks zu vermeiden.
/// </summary>
internal sealed class SquixOpcUaClient : IAsyncDisposable
{
private const int OperationTimeoutMs = 15000;
private const int SessionTimeoutMs = 60000;
private readonly string _endpointUrl;
private readonly ApplicationConfiguration _appConfig;
private ISession? _session;
private bool _disposed;
// Namespace-Indizes – werden beim Connect dynamisch ermittelt
private ushort _nsPrinter = 3; // cab.de/Printer
public bool IsConnected => _session?.Connected == true;
public SquixOpcUaClient(string host, int port = 4840)
{
_endpointUrl = $"opc.tcp://{host}:{port}";
// Einmalige Konfiguration – kein Re-Allokieren bei jedem ConnectAsync
_appConfig = new ApplicationConfiguration
{
ApplicationName = "PrinterMonitor",
ApplicationUri = "urn:PrinterMonitor",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(AppContext.BaseDirectory, "pki", "own")
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(AppContext.BaseDirectory, "pki", "issuer")
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(AppContext.BaseDirectory, "pki", "trusted")
},
RejectedCertificateStore = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(AppContext.BaseDirectory, "pki", "rejected")
},
AutoAcceptUntrustedCertificates = true,
AddAppCertToTrustedStore = false
},
TransportConfigurations = new TransportConfigurationCollection(),
TransportQuotas = new TransportQuotas { OperationTimeout = OperationTimeoutMs },
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = SessionTimeoutMs }
};
// CertificateValidator einmalig erstellen und Event einmalig subscriben (kein Leak).
// Obsolete-Warning für parameterlosem Ctor unterdrückt – ITelemetryContext ist
// in dieser Anwendung nicht verfügbar, die OPC UA Lib nutzt diesen Pfad selbst.
#pragma warning disable CS0618
_appConfig.CertificateValidator = new CertificateValidator();
#pragma warning restore CS0618
_appConfig.CertificateValidator.CertificateValidation += OnCertificateValidation;
}
private static void OnCertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
=> e.Accept = true;
public async Task ConnectAsync(CancellationToken ct = default)
{
try
{
await _appConfig.ValidateAsync(ApplicationType.Client);
// SelectEndpoint ist synchron-blockierend ohne CancellationToken-Support.
// Task.Run verhindert, dass der aufrufende Thread blockiert wird.
#pragma warning disable CS0618
var endpoint = await Task.Run(() =>
{
ct.ThrowIfCancellationRequested();
var ep = CoreClientUtils.SelectEndpoint(_appConfig, _endpointUrl, useSecurity: false);
ct.ThrowIfCancellationRequested();
return ep;
}, ct);
var configuredEndpoint = new ConfiguredEndpoint(
null, endpoint, EndpointConfiguration.Create(_appConfig));
_session = await Session.Create(
_appConfig, configuredEndpoint,
updateBeforeConnect: false,
sessionName: "PrinterMonitorSession",
sessionTimeout: SessionTimeoutMs,
identity: new UserIdentity(new AnonymousIdentityToken()),
preferredLocales: null);
#pragma warning restore CS0618
ResolveNamespaceIndices();
}
catch
{
// Bei Fehler Session sauber wegräumen, damit interne OPC UA Threads nicht leaken
await DisconnectAsync();
throw;
}
}
public async Task DisconnectAsync()
{
if (_session != null)
{
try
{
if (_session.Connected)
await _session.CloseAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] OPC UA: Fehler beim Schließen der Session – {ex.Message}");
}
finally
{
_session.Dispose();
_session = null;
}
}
}
/// <summary>
/// Liest LTS-Sensor, Druckkopf-Klappe und Papier-Leer in einem einzigen OPC UA Batch-Call.
/// </summary>
public async Task<(bool LtsSensor, bool PrintheadOpen, bool PaperLow)> ReadAllSensorsAsync(
CancellationToken ct = default)
{
var nodeIds = new[]
{
new NodeId(22002, _nsPrinter), // LTS Sensor
new NodeId(10076, _nsPrinter), // Printhead Open
new NodeId(10019, _nsPrinter), // Paper Low
};
var results = await ReadValuesAsync(nodeIds, ct);
return (
LtsSensor: results[0] is bool b0 ? b0 : Convert.ToBoolean(results[0] ?? false),
PrintheadOpen: results[1] is bool b1 ? b1 : Convert.ToBoolean(results[1] ?? false),
PaperLow: results[2] is bool b2 ? b2 : Convert.ToBoolean(results[2] ?? false)
);
}
private void ResolveNamespaceIndices()
{
if (_session == null) return;
var nsTable = _session.NamespaceUris;
for (int i = 0; i < nsTable.Count; i++)
{
string uri = nsTable.GetString((uint)i).TrimEnd('/');
if (uri.EndsWith("cab.de/Printer", StringComparison.OrdinalIgnoreCase))
_nsPrinter = (ushort)i;
}
}
private async Task<object?[]> ReadValuesAsync(NodeId[] nodeIds, CancellationToken ct)
{
if (_session == null) throw new InvalidOperationException("Nicht verbunden.");
var nodesToRead = new ReadValueIdCollection();
foreach (var nid in nodeIds)
nodesToRead.Add(new ReadValueId { NodeId = nid, AttributeId = Attributes.Value });
var response = await _session.ReadAsync(
null, 0, TimestampsToReturn.Neither, nodesToRead, ct);
var dataValues = response.Results;
var results = new object?[dataValues.Count];
for (int i = 0; i < dataValues.Count; i++)
results[i] = dataValues[i].StatusCode == StatusCodes.Good ? dataValues[i].Value : null;
return results;
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_appConfig.CertificateValidator.CertificateValidation -= OnCertificateValidation;
await DisconnectAsync();
_disposed = true;
}
}