205 Zeilen
7.7 KiB
C#
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;
|
|
}
|
|
}
|