Kom i gang med OPC UA ── Indsamling af data fra PLC'er og udstyr i en Windows-app

· · Windows, OPC UA, Fremstilling, PLC, C#, .NET, Kommunikation, Certifikater

Del på X
Ændringshistorik (kun første version, udgivet 01.11.2026)
Første udgivelse
Citér denne artikel(DOI: 10.5281/zenodo.23968424)

Denne artikel er arkiveret på Zenodo. Nedenfor ser du både den DOI, der altid peger på den nyeste version, og den DOI, der er knyttet til den version, du læser nu.

Go Komura (2026). Kom i gang med OPC UA ── Indsamling af data fra PLC'er og udstyr i en Windows-app. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968424 https://comcomponent.com/da/blog/opc-ua-windows-data-collection/

DOI (seneste version)
10.5281/zenodo.23968424
DOI (denne version)
10.5281/zenodo.23968425

Når du indsamler data fra PLC’er eller udstyr i en Windows-app, og udstyret har en OPC UA-server, kan du måske undgå at skrive en separat protokol for hver producent. Det er dog ikke nok bare at oprette forbindelse og læse værdier. Hvis du ikke kender til den status og de tidsstempler, der følger med hver værdi, abonnementer, der melder ændringer, godkendelse af forbindelser med certifikater og hvordan man kommer sig efter en afbrydelse, kan du ikke stole på de indsamlede data bagefter.

Denne artikel forklarer de dele af OPC UA, der er nødvendige for dataindsamling, sammen med resultater fra faktiske kørsler af en C#-klient. Klienten bruger OPC Foundations .NET-bibliotek, og modparten er en simuleret udstyrsserver. Jeg har ikke afprøvet det mod en bestemt PLC eller enhed. Kontrollér enhedsspecifikke indstillinger i den pågældende enheds dokumentation.

Indsamling af billeder fra GigE Vision- og USB3 Vision-kameraer behandles i “Dropped Frames with GigE Vision and USB3 Vision Cameras”, og enheder med seriel kommunikation i “Serial Communication App Pitfalls”.

Forskellen mellem OPC UA og OPC Classic

OPC Classic-specifikationerne udveksler data mellem softwarekomponenter ved hjælp af Windows COM/DCOM. OPC DA til procesdata, OPC A&E til alarmer og OPC HDA til historik er defineret som separate specifikationer1.

OPC Foundation offentliggjorde OPC UA (Unified Architecture) i 2008. OPC UA er en serviceorienteret arkitektur, der ikke er bundet til en bestemt platform, og som samler funktionerne fra hver af Classic-specifikationerne. Som årsager til omlægningen nævner OPC Foundation blandt andet, at Microsoft begyndte at prioritere platformsuafhængige mekanismer frem for COM/DCOM, at løsningen skulle kunne køre på andre systemer end Microsofts og på indlejrede enheder, og at der skulle kunne oprettes sikre forbindelser gennem firewalls1.

OPC UA-specifikationen er også standardiseret af IEC som IEC 62541-serien2. Som kommunikationsport har IANA registreret 4840/TCP som opcua-tcp3.

Forskellen mellem OPC Classic og OPC UAEt diagram, der viser, at OPC Classic bygger på Windows COM og DCOM og er opdelt i separate DA-, A&E- og HDA-specifikationer, mens OPC UA samler deres funktioner i én platformsuafhængig specifikation.Funktionerne samlesOPC ClassicKører oven på COM/DCOM (Windows)Opdelt i DA-, A&E- og HDA-specifikationerOPC UA (2008)Ikke bundet til en bestemt platformStandardiseret som IEC 62541

Figur 1: OPC Classic består af separate specifikationer oven på COM/DCOM, og OPC UA samler dem i én.

Hvis eksisterende udstyr kun har en OPC DA-server, skal du bruge en løsning, der anvender COM/DCOM. Denne artikel behandler kun OPC UA.

I diagrammet markerer en fuldt optrukket linje en relation, der altid gælder, og en stiplet linje en betinget relation (betingelserne står for hver relation på detaljesiden). Den fulde liste over relationer (13 i alt, med belæg og sikkerhed) og definitionerne af de vigtigste begreber er samlet på detaljesiden for vidensoversigten (på japansk). Data: JSON-LD / Turtle

Den opsætning, jeg afprøvede

Dette er den opsætning, jeg kørte.

Rolle Det brugte jeg
Klient C#, .NET 10 SDK 10.0.114, NuGet-pakken OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
Server En simuleret enhed lavet med Python 3.13 og asyncua 2.1.0 (port 4840 på samme pc)
Operativsystem Linux (kørsel på Windows blev ikke afprøvet)

Klientbiblioteket er den .NET-implementering, som OPC Foundation har offentliggjort. Til brugen af 2.0-serien fulgte jeg fremgangsmåden i Getting started i samme repository4.

Den opsætning, jeg afprøvedeEt diagram, der viser en C#-klient, som bruger OPC Foundations .NET-bibliotek og via en krypteret forbindelse opretter forbindelse til en simuleret udstyrsserver lavet med asyncua, der lytter på port 4840.opc.tcp, SignAndEncryptC#-klient (LineCollector)Simuleret udstyrsserver (asyncua)OPC Foundations .NET-bibliotek 2.0.1Fire variabler under Line1

Figur 2: C#-klienten oprettede forbindelse til den simulerede udstyrsserver på samme pc.

Den simulerede udstyrsserver

I stedet for rigtigt udstyr lavede jeg en lille server med asyncua5. Under et objekt ved navn Line1 er der fire variabler: temperatur (Temperature), antal emner (PartCount), kører (Running) og sætpunkt (Setpoint). Temperaturen ændres hvert 0,5 sekund og antallet af emner hvert 2. sekund. Som sikkerhed tilbød jeg to muligheder: ingen kryptering (None) og signering plus kryptering (SignAndEncrypt med Basic256Sha256).

# En simuleret enhed til test. Den erstatter ikke produktionsudstyr eller PLC'er.
import asyncio, math, random, socket
from pathlib import Path
from cryptography.x509.oid import ExtendedKeyUsageOID
from asyncua import Server, ua
from asyncua.crypto.cert_gen import setup_self_signed_certificate

HERE = Path(__file__).parent
APP_URI = "urn:komurasoft:demo-plc"


async def main():
    key, cert = HERE / "server_key.pem", HERE / "server_cert.der"
    await setup_self_signed_certificate(
        key, cert, APP_URI, socket.gethostname(),
        [ExtendedKeyUsageOID.SERVER_AUTH], {"commonName": "DemoPlcServer"})

    server = Server()
    await server.init()
    server.set_endpoint("opc.tcp://0.0.0.0:4840/demo/")
    server.set_server_name("DemoPlcServer")
    await server.set_application_uri(APP_URI)
    server.set_security_policy([
        ua.SecurityPolicyType.NoSecurity,
        ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
    ])
    await server.load_certificate(str(cert))
    await server.load_private_key(str(key))

    ns = await server.register_namespace("urn:komurasoft:demo-plc:line1")
    line1 = await server.nodes.objects.add_object(ua.NodeId("Line1", ns), "Line1")
    temperature = await line1.add_variable(ua.NodeId("Line1.Temperature", ns), "Temperature", 25.0)
    part_count = await line1.add_variable(
        ua.NodeId("Line1.PartCount", ns), "PartCount", ua.Variant(0, ua.VariantType.UInt32))
    await line1.add_variable(ua.NodeId("Line1.Running", ns), "Running", True)
    setpoint = await line1.add_variable(ua.NodeId("Line1.Setpoint", ns), "Setpoint", 30.0)
    await setpoint.set_writable()

    async with server:
        print("listening", flush=True)
        t, count = 0, 0
        while True:
            await asyncio.sleep(0.5)
            t += 1
            value = 25.0 + 2.0 * math.sin(t / 4) + random.uniform(-0.05, 0.05)
            await temperature.write_value(round(value, 2))
            if t % 4 == 0:
                count += 1
                await part_count.write_value(ua.Variant(count, ua.VariantType.UInt32))


asyncio.run(main())

Denne server accepterer klienters certifikater uden at kontrollere dem. Det er en testindstilling og adskiller sig fra, hvordan produktionsudstyr opfører sig.

C#-klienten

Jeg tilføjede disse to pakker til projektfilen.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="2.0.1" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
  </ItemGroup>

</Project>

Klienten opretter forbindelse, gennemser, læser, skriver og abonnerer i den rækkefølge. De følgende afsnit forklarer, hvad hver del betyder.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions;
using Opc.Ua.Client.Subscriptions.Streaming;
using MonitoredItemOptions = Opc.Ua.Client.Subscriptions.MonitoredItems.MonitoredItemOptions;

string url = args.Length > 0 ? args[0] : "opc.tcp://localhost:4840/demo/";
int notifications = args.Length > 1 ? int.Parse(args[1]) : 6;
const string LineNamespace = "urn:komurasoft:demo-plc:line1";

HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.SetMinimumLevel(LogLevel.Warning);

builder.Services
    .AddOpcUa()
    .AddClient(options =>
    {
        options.ApplicationName = "LineCollector";
        options.ApplicationUri = "urn:localhost:LineCollector";
        options.ProductUri = "urn:komurasoft:LineCollector";
        options.PkiRoot = Path.Combine(AppContext.BaseDirectory, "pki");
    })
    .AddDiscoveryAndConnect(options =>
    {
        options.DiscoveryUrl = url;
        options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
        options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
    });

using IHost host = builder.Build();
await host.StartAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2));
CancellationToken ct = timeout.Token;

try
{
    var connect = host.Services
        .GetRequiredService<Func<CancellationToken, Task<ManagedSession>>>();
    ManagedSession session = await connect(ct);
    await using (session)
    {
        Console.WriteLine($"接続: {session.ConfiguredEndpoint?.EndpointUrl}");

        // Serveren bestemmer navnerummets indeks. Slå det op ud fra URI'en, og byg NodeIds ud fra det
        ushort ns = (ushort)session.NamespaceUris.GetIndex(LineNamespace);
        var line1 = new NodeId("Line1", ns);
        var temperature = new NodeId("Line1.Temperature", ns);
        var partCount = new NodeId("Line1.PartCount", ns);
        var setpoint = new NodeId("Line1.Setpoint", ns);

        // Gennemsøgning: list variablerne under Line1
        var browser = new Browser(session)
        {
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
            IncludeSubtypes = true,
            NodeClassMask = (uint)NodeClass.Variable,
        };
        Console.WriteLine("Line1 の変数:");
        foreach (ReferenceDescription child in await browser.BrowseAsync(line1, ct))
        {
            Console.WriteLine($"  {child.DisplayName}  {child.NodeId}");
        }

        // Læsning: se på status og de to tidsstempler, ikke kun værdien
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // Læsning af en node, der ikke findes, returnerer en Bad-status i stedet for en værdi
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Skrivning: sæt Setpoint til 32.5
        var write = new WriteValue
        {
            NodeId = setpoint,
            AttributeId = Attributes.Value,
            Value = new DataValue(new Variant(32.5)),
        };
        WriteResponse wr = await session.WriteAsync(null, new[] { write }, ct);
        Console.WriteLine($"Setpoint の書き込み: {wr.Results[0]}");

        // Abonnement: bed serveren om at sample hvert 500. ms og melde ændringer
        Console.WriteLine($"購読({notifications}件まで):");
        await foreach (DataValueChange change in session.DefaultStreaming
            .SubscribeDataChangesAsync(new[] { temperature, partCount }, options: new MonitoredItemOptions
            {
                SamplingInterval = TimeSpan.FromMilliseconds(500),
                QueueSize = 5,
            }, ct: ct)
            .TakeAsync(notifications)
            .WithCancellation(ct))
        {
            DataValue v = change.Value;
            string name = change.MonitoredItem?.Name.Split("s=")[^1] ?? "?";
            Console.WriteLine($"  {Hms(v.SourceTimestamp)}  {name,-17} {v.WrappedValue,6}  {v.StatusCode}");
        }
    }
}
catch (ServiceResultException ex)
{
    Console.Error.WriteLine($"OPC UA エラー {ex.StatusCode}: {ex.Message}");
    Environment.ExitCode = 1;
}
finally
{
    await host.StopAsync();
}

static string Hms(DateTimeUtc time) => time.ToDateTime().ToString("HH:mm:ss.fff");

Første gang jeg kørte den, kunne den ikke oprette forbindelse. Årsagen forklares i afsnittet “Godkendelse af forbindelser med certifikater”. Her er outputtet, efter at serverens certifikat var godkendt. Bibliotekets advarselslogge er udeladt.

接続: opc.tcp://localhost:4840/demo/
Line1 の変数:
  Temperature  ns=2;s=Line1.Temperature
  PartCount  ns=2;s=Line1.PartCount
  Running  ns=2;s=Line1.Running
  Setpoint  ns=2;s=Line1.Setpoint
Temperature = 23.09  状態 Good [0x00000000]
  Source 13:40:51.302 / Server 13:40:51.302
Line1.Pressure の読み取り: BadNodeIdUnknown [0x80340000]
Setpoint の書き込み: Good [0x00000000]
購読(6件まで):
  13:40:51.302  Line1.Temperature  23.09  Good [0x00000000]
  13:40:50.801  Line1.PartCount       67  Good [0x00000000]
  13:40:51.802  Line1.Temperature  23.05  Good [0x00000000]
  13:40:52.304  Line1.Temperature  23.07  Good [0x00000000]
  13:40:52.806  Line1.Temperature   23.2  Good [0x00000000]
  13:40:52.806  Line1.PartCount       68  Good [0x00000000]

Tidspunkterne er i UTC.

Udstyrets data vises som noder i adresserummet

En OPC UA-server udstiller de data, den har, som en samling af noder. Denne samling kaldes adresserummet. Noder findes i forskellige typer, f.eks. objekter, der repræsenterer mapper eller udstyr, og variabler, der indeholder værdier, og noderne er forbundet med hinanden via referencer. Klienten følger referencerne med Browse-tjenesten for at finde ud af, hvilke noder der findes6.

I outputtet ovenfor følger klienten hierarkiske referencer fra Line1 og finder fire variabler.

Den simulerede enheds adresserumEt diagram, der viser et Line1-objekt under Objects-mappen med fire variabler under sig: Temperature, PartCount, Running og Setpoint.ObjectsLine1 (objekt)Temperature (variabel)PartCount (variabel)Running (variabel)Setpoint (variabel, skrivbar)

Figur 3: Den simulerede enheds adresserum. Klienten finder noder ved at følge referencer med Browse.

Et NodeId består af et navnerum og en identifikator

Noder identificeres med et NodeId. I outputtet betyder ns=2;s=Line1.Temperature, at navnerummets indeks er 2, og at identifikatoren er strengen Line1.Temperature.

Et navnerum er i virkeligheden en URI. Ifølge specifikationen repræsenteres navnerums-URI’er med tal i tjenesterne, og tallet svarer til positionen i serverens NamespaceArray. Indeks 0 er OPC UA-navnerummet http://opcfoundation.org/UA/7.

Derfor skal du ikke skrive indeksnummeret ind i koden, men slå indekset op ud fra URI’en. Koden ovenfor bruger session.NamespaceUris.GetIndex(LineNamespace) til at få 2 ud fra URI’en og bygger sine NodeIds ud fra det.

Opbygning af et NodeId ud fra en navnerums-URIEt diagram, der viser, at klienten slår navnerums-URI'en op i serverens NamespaceArray for at få indekset og kombinerer indekset med identifikatoren for at bygge et NodeId.Navnerums-URIServerens NamespaceArrayIndeks (2 i dette eksempel)NodeId ns=2;s=Line1.TemperatureIdentifikator Line1.Temperature

Figur 4: Navnerummets indeks slås op ud fra URI’en og kombineres med identifikatoren til et NodeId.

Læsning og skrivning returnerer status og tidsstempler sammen med værdien

Variablernes værdier læses og skrives med tjenesterne Read og Write8. En læsning returnerer mere end blot værdien. Den returnerer en struktur kaldet DataValue, som indeholder værdien, en statuskode, et kildetidsstempel og et servertidsstempel9.

Element Betydning
Værdi Variablens værdi. Når statuskoden angiver en fejl, ignoreres værdien, og serveren sætter den til null
Statuskode (StatusCode) Om serveren kunne hente og levere værdien
SourceTimestamp Det tidspunkt, datakilden knyttede til værdien
ServerTimestamp Det tidspunkt, serveren modtog værdien eller fik at vide, at den var korrekt

Statuskodens to øverste bit angiver Good (lykkedes), Uncertain (lykkedes delvist og kan være uegnet til visse formål) eller Bad (mislykkedes)10. Good [0x00000000] i outputtet er Good.

Da jeg læste Line1.Pressure, som ikke findes, kom BadNodeIdUnknown [0x80340000] tilbage i stedet for en værdi. Dette biblioteks ReadValueAsync kaster en undtagelse, når status er Bad4. Hvis et system, der registrerer værdier, også gemmer statuskoden ved siden af hver værdi, kan du bagefter skelne de værdier, der blev registreret, mens status var Bad.

Hvad en DataValue indeholderEt diagram, der viser, at den DataValue, en læsning eller en notifikation returnerer, indeholder fire ting, værdien, statuskoden, kildetidsstemplet og servertidsstemplet, og at værdien ignoreres, når statuskoden er Bad.Hvis BadDataValueVærdiStatuskodeSourceTimestamp (tid ved kilden)ServerTimestamp (tid ved serveren)Ignorér værdien

Figur 5: Resultatet af en læsning indeholder ikke kun værdien, men også en statuskode og to tidsstempler.

Ved skrivningen skrev jeg 32.5 til Setpoint, og Good kom tilbage som statuskode. Serveren bestemmer, hvilke variabler der kan skrives til. I denne simulerede server kan der kun skrives til Setpoint. Om udstyrets sætpunkter må ændres udefra, er et spørgsmål om udstyrets sikkerhedsdesign, så det behandler denne artikel ikke.

Få besked om ændringer: abonnementer og MonitoredItems

For at følge ændringer i værdier kan klienten gentage Read med et fast interval. Derudover har OPC UA en mekanisme, hvor overvågningen af værdier overlades til serveren, som så melder ændringerne. Den mekanisme er abonnementer (Subscription) og MonitoredItems.

Et MonitoredItem er ét element, der overvåges, og det har et samplingsinterval. Samplingsintervallet er den hurtigste takt, hvormed serveren sampler værdier fra kilden, og specifikationen beskriver det som en “best effort”-cyklus11. Når en sample opfylder filterets betingelser, oprettes en notifikation. Hvis der ikke er angivet noget filter, sendes en notifikation, når værdien eller status ændres12.

Notifikationerne lægges først i MonitoredItem’ets kø. Du bestemmer på forhånd, om den ældste eller den nyeste kasseres, når køen løber over, og hvis køstørrelsen er 2 eller mere, sættes Overflow-bitten, der angiver kassering, i statuskoden13.

Et abonnement samler flere MonitoredItems og sender ved hvert publiceringsinterval de notifikationer, der endnu ikke er sendt, til klienten. Når der går cyklusser uden notifikationer at sende, sender det en keep-alive for at vise, at abonnementet stadig er aktivt14.

Sådan fungerer abonnementer og MonitoredItemsEt diagram, der viser, at et MonitoredItem sampler kildens værdi ved samplingsintervallet og lægger notifikationer, der passerer filteret, i en kø, og at abonnementet sender dem samlet til klienten ved publiceringsintervallet og sender en keep-alive, når der ikke er notifikationer.SamplingsintervalPubliceringsintervalHvis der ikke er notifikationerKilde (udstyrets værdi)MonitoredItemFilter (standard: ændring i værdi eller status)Kø (Overflow, når den er fuld)AbonnementKlientKeep-alive

Figur 6: MonitoredItem’et sampler og sætter i kø, og abonnementet sender notifikationerne samlet til klienten.

Koden ovenfor abonnerer på Temperature og PartCount med et samplingsinterval på 500 millisekunder og en køstørrelse på 515. I outputtet kommer temperaturen ca. hvert 0,5 sekund og antallet af emner ca. hvert 2. sekund. De første to notifikationer var de værdier, hver variabel havde, da abonnementet startede. Antallet af emners 13:40:50.801 er det tidspunkt, hvor værdien ændrede sig, før abonnementet startede. Da notifikationerne viser kildetidsstemplet, viser de det tidspunkt, værdien ændrede sig, ikke det tidspunkt, notifikationen blev modtaget.

Valget mellem gentagne læsninger og abonnementer

Ved gentagne læsninger bestemmer klienten selv intervallet, og mekanismen er enkel. Til gengæld går en forespørgsel og et svar frem og tilbage hver gang, også når værdien ikke har ændret sig, og ændringer, der er kortere end intervallet, overses. Med et abonnement sampler serveren og sender kun ændringer, så trafikken vokser ikke ret meget, selv for værdier, der sjældent ændres. Men også med et abonnement bliver ændringer, der er kortere end samplingsintervallet, måske ikke meldt. Specifikationen tillader hurtigere sampling end ønsket, men kræver det ikke11.

Forskellen mellem gentagne læsninger og abonnementerEt diagram, der viser, at ved gentagne læsninger bestemmer klienten intervallet og sender en forespørgsel hver gang, at ved abonnementer sampler serveren ved samplingsintervallet og sender kun ændringer, og at begge kan overse ændringer, der er kortere end intervallet.Følg ændringer i værdierGentag læsningerAbonnérKlienten bestemmer intervallet og spørger hver gangServeren sampler og sender kun ændringerÆndringer kortere end intervallet kan overses

Figur 7: Med begge metoder kan ændringer, der er kortere end intervallet, blive overset.

Godkendelse af forbindelser med certifikater

I en OPC UA-forbindelse vælger du graden af beskyttelse af meddelelser med MessageSecurityMode. Værdierne er None, som ikke beskytter, Sign, som kun signerer, og SignAndEncrypt, som signerer og krypterer16. Kombinationen af algoritmer til signering og kryptering bestemmes af sikkerhedspolitikken (security policy), og denne artikel bruger Basic256Sha256.

Signering og kryptering bruger et certifikat for hver applikation. Klienten og serveren kontrollerer hinandens certifikater og afviser forbindelser fra parter, de ikke har tillid til. En server, der er lavet med OPC Foundations .NET-bibliotek, tilbyder som standard ikke den ukrypterede None-politik4.

Den første forbindelse stoppede med BadCertificateUntrusted

Ved den første kørsel havde klienten ikke tillid til serverens certifikat, så forbindelsen blev afvist. Her er en del af outputtet.

fail: Opc.Ua.CertificateValidationCore[91]
      Certificate [Subject=CN=DemoPlcServer, CN=urn:komurasoft:demo-plc, Thumbprint=4E236DDA629FE2AB32D185BF681609FD6C525363, NotBefore=2026-11-01, NotAfter=2027-11-01, KeyAlgorithm=1.2.840.113549.1.1.1] validation failed with suppressible errors but was rejected. Reason=Id: BadCertificateUntrusted
...
OPC UA エラー BadCertificateUntrusted [0x801A0000]: [801A0000] (BadCertificateUntrusted) 'Could not verify security on OpenSecureChannel response.'

Dette bibliotek gemmer afviste certifikater i rejected-lageret. Når en administrator flytter et certifikat derfra til trusted-lageret, får biblioteket tillid til den pågældende part17. Denne gang flyttede jeg serverens certifikat fra pki/rejected/certs til pki/trusted/certs, kørte programmet igen og kunne oprette forbindelse. Før du flytter det, skal du bekræfte aftrykket (thumbprint), som også indgår i filnavnet, med serverens administrator4.

Trin til at få tillid til et certifikatEt diagram, der viser, at klienten ved den første forbindelse afviser serverens certifikat og gemmer det i rejected-lageret, og at når aftrykket er bekræftet og certifikatet flyttet til trusted-lageret, har den næste forbindelse tillid til det og lykkes.Første forbindelseBadCertificateUntrustedCertifikatet gemmes i rejected-lageretBekræft aftrykket med serverens administratorFlyt det til trusted-lageretDen næste forbindelse har tillid og lykkes

Figur 8: Når et afvist certifikat er kontrolleret og flyttet til trusted, har klienten tillid til den part.

Der findes også indstillingen AutoAcceptUntrustedCertificates, som automatisk accepterer certifikater, der ikke er tillid til. Standardværdien er false, og ifølge bibliotekets dokumentation er den nyttig i udviklingsmiljøer, men den bør ikke bruges i produktion17.

NotAfter=2027-11-01 i loggen er udløbsdatoen for den simulerede servers certifikat. Det er et selvsigneret certifikat, der er oprettet med en asyncua-funktion, og det udløb et år efter oprettelsesdagen. Udstyrets certifikater udløber også, så fastlæg som en del af driften, hvordan de udskiftes, før de udløber, og hvordan modparten får tillid til de nye. Håndtering af Windows’ certifikatlager behandles i “The Windows Certificate Store in Practice”.

Brugergodkendelse

Det, certifikaterne verificerer, er applikationerne. Derudover overføres et token, der identificerer brugeren, når en session åbnes. Specifikationen definerer fire typer: anonym, brugernavn og adgangskode, X.509-certifikat og eksternt udstedte tokens18. Klienten i denne artikel oprettede forbindelse anonymt. Hvilke tokens en server accepterer, fremgår af serverens endpointoplysninger19.

Hvor dataindsamling i praksis går galt

Herfra gennemgår jeg problemer, der ofte opstår i systemer, der indsamler udstyrsdata, baseret på dette eksperiment og specifikationen.

Værdier fra den periode, hvor serveren var stoppet, kommer ikke frem

Under abonnementet stoppede jeg serveren og genstartede den ca. 15 sekunder senere. Dette biblioteks ManagedSession genopretter automatisk forbindelsen, når den afbrydes20. Mens serveren var stoppet, blev klienten ved med at forsøge at genoprette forbindelsen.

warn: Opc.Ua.ChannelManager[6]
      Reconnect failed. Endpoint=opc.tcp://localhost:4840/demo/, Attempt=3, Outcome=transient-failure, Status=BadNotConnected, ErrorMessage=Could not connect to the remote endpoint.

Da serveren kom tilbage, kom notifikationerne også tilbage. Her er en del af abonnementets output.

  13:41:13.364  Line1.Temperature  25.61  Good [0x00000000]
  13:41:13.865  Line1.Temperature  25.12  Good [0x00000000]
  13:41:34.285  Line1.Temperature  26.55  Good [0x00000000]
  13:41:33.784  Line1.PartCount        2  Good [0x00000000]
  13:41:34.786  Line1.Temperature  26.16  Good [0x00000000]

Der kom ikke en eneste værdi fra tidsrummet 13:41:13 til 13:41:34. Antallet af emner gik tilbage fra 78 til 2. Det skyldes, at den simulerede server efter genstarten begyndte at tælle forfra fra 0.

I dette eksempel var selve serveren stoppet, så værdierne fra den periode er ikke gemt nogen steder. Genforbindelsesmekanismen genopretter forbindelsen, men den udfylder ikke bagefter værdierne fra den periode, hvor serveren var nede.

Situationen er en anden, når kun kommunikationen afbrydes, og serveren kører videre. Specifikationen har en Republish-tjeneste, som får serveren til at sende notifikationsmeddelelser, der ikke kom frem, igen fra sin genudsendelseskø21. Notifikationer ophobes også i MonitoredItem’ets kø, men alt ud over køstørrelsen kasseres13. Jeg afprøvede ikke dette tilfælde i eksperimentet. I begge tilfælde bør du registrere tidspunkterne for afbrydelse og genforbindelse, så du kan se, i hvilken periode der ikke kom værdier. Genforbindelse og logdesign for udstyr med seriel kommunikation behandles også i “Serial Communication App Pitfalls”.

Registrering, når serveren stopperEt diagram, der viser, at når serveren stopper under et abonnement, bliver klienten ved med at forsøge at genoprette forbindelsen, og notifikationerne kommer tilbage, når serveren vender tilbage, men værdierne fra den periode, hvor den var stoppet, kommer ikke frem, så tidspunkterne for afbrydelse og genforbindelse bør registreres.Abonnement aktivtServeren stopperKlienten forsøger gentagne gange at genoprette forbindelsenServeren vender tilbage, og notifikationerne genoptagesVærdier fra perioden, hvor den var stoppet, kommer ikke fremRegistrér tidspunkterne for afbrydelse og genforbindelse

Figur 9: Genforbindelse genopretter forbindelsen, men værdierne fra den periode, hvor serveren var stoppet, kommer ikke frem.

Hvilket tidsstempel skal registreres

Ifølge specifikationen er SourceTimestamp det tidspunkt, datakilden knyttede til værdien, og det bør angive, hvornår værdien ændrede sig. ServerTimestamp er det tidspunkt, serveren modtog værdien eller fik at vide, at den var korrekt9. Den simulerede server i denne artikel genererer selv værdierne, så de to tidspunkter var de samme.

Hvis du vil vide, hvornår en værdi ændrede sig i udstyret, skal du bruge SourceTimestamp. Tidspunktet sættes dog af kildens ur, så hvis udstyrets eller serverens ur går forkert, er registreringen også forkert. Hvis du vil sammenholde de indsamlede data med andre registreringer, så gem også tidspunktet på den modtagende pc ved siden af, så du senere kan kontrollere afvigelser. Tidssynkronisering på Windows-siden behandles i “Windows Time Synchronization (w32time) and Business Systems”.

Behandl ikke værdier, hvis status ikke er Good, som almindelige tal

Når statuskoden er Bad, skal værdien ignoreres9. Når den er Uncertain, kan værdien være uegnet til visse formål10. Før værdierne føres ind i grafer eller sammentællinger, så sortér dem efter statuskode, og fastlæg, hvordan værdier, der ikke er Good, skal håndteres. Overflow-bitten for en kø, der løber over, sættes også i statuskoden13. Hvordan udstyrets tilstand vises på skærmen, behandles i “Best Practices for Checking and Displaying External Device State”.

Åbn porten i firewallen

Den OPC UA-port, der er registreret hos IANA, er 4840/TCP3. Ikke alle servere bruger dog dette nummer. OPC Foundations .NET-eksempler bruger blandt andet port 625414. Kontrollér portnummeret i udstyrets endpoint-URL, og tillad forbindelser til den port. Hvis serveren kører på Windows, skal det tillades i den maskines Windows Firewall, og hvis der er netværksudstyr imellem, på det udstyr. Oprettelse af indgående regler behandles i “The Windows Firewall and Business Applications”.

Hvor dataindsamling går galtEt diagram, der viser de steder, hvor indsamling af udstyrsdata går galt, opdelt i registreringernes indhold (værdier fra den periode, hvor serveren var stoppet, kommer ikke frem, hvilket tidsstempel der registreres, håndtering af værdier, der ikke er Good) og forberedelse af forbindelsen (firewallporte, certifikaters udløb).Hvor dataindsamling går galtRegistreringernes indholdForberedelse af forbindelsenVærdier fra et stop kommer ikke fremHvilket tidsstempelHåndtering af værdier, der ikke er GoodFirewallporteCertifikaters udløb

Figur 10: Selv når forbindelsen virker, skal du kontrollere huller i registreringerne, tidspunkterne, status, portene og certifikaterne.

Valget mellem OPC UA, Modbus/TCP og producentspecifikke protokoller

Til kommunikation med PLC’er og udstyr findes der ud over OPC UA også Modbus/TCP samt protokoller, som PLC-producenterne stiller til rådighed.

Modbus-specifikationen håndterer data i fire tabeller: Discrete Input (en enkelt bit, der kun kan læses), Coil (en enkelt bit, der kan læses og skrives), Input Register (et 16-bit ord, der kun kan læses) og Holding Register (et 16-bit ord, der kan læses og skrives)22. Hos IANA er 502/TCP registreret som Modbus’ applikationsprotokol (mbap)3.

Det, der ligger i disse tabeller, er bit og 16-bit ord, så hvilken værdi der ligger på hvilken adresse og i hvilket format, skal kontrolleres i udstyrets dokumentation og skrives på klientsiden. Med OPC UA udstiller serveren, som vi har set, navngivne variabler i sit adresserum, og klienten kan finde dem med Browse. Værdierne følges af en statuskode og tidsstempler, og forbindelser godkendes med certifikater.

Hvad du skal bruge, afgøres ofte af, hvad udstyret tilbyder. Hvis udstyret har en OPC UA-server, så brug den. Hvis det kun har Modbus/TCP, så lav en adressetabel og læs ud fra den. Hvis der kun er producentens protokol eller SDK, så følg dokumentationen for den. Valg af kommunikationsmetode til USB-enheder behandles i “How to Work with USB Devices from a Windows App”.

Valg af kommunikationsmetode til udstyretEt diagram, der viser, at hvis udstyret har en OPC UA-server, gennemser og indsamler du variabler med OPC UA, hvis det kun har Modbus/TCP, kontrollerer du adresser og formater i dokumentationen og læser registre, og hvis det kun har producentens protokol, følger du dens dokumentation.OPC UA-serverKun Modbus/TCPKun producentens protokolKommunikation, som udstyret tilbyderHvad er der?Gennemse og indsaml variablerneKontrollér adresser og formater i dokumentationenFølg producentens dokumentation og SDKLæs registrene, og omregn dem til værdier

Figur 11: Hvad du indsamler med, afgøres ofte af, hvad udstyret tilbyder.

Relaterede artikler

Referencer

  1. OPC Foundation, Classic. At OPC Classic bygger på COM/DCOM, specifikationerne DA, A&E og HDA, offentliggørelsen af OPC UA i 2008 og årsagerne til omlægningen. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Del 1 af IEC 62541-serien, som behandler begreber og overblik for OPC UA (siden erstattet af en nyere udgave). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. Registreringerne af opcua-tcp på 4840/TCP og mbap på 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Hvordan en klient i 2.0-serien skrives, at ReadValueAsync kaster en undtagelse ved Bad, rejected- og trusted-lagrene, at servere som standard ikke tilbyder None, og de portnumre, eksemplerne bruger. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). Det Python-OPC UA-bibliotek, der blev brugt til den simulerede udstyrsserver. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Tjenesten, der følger en nodes referencer. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. At navnerums-URI’er repræsenteres med tal i tjenesterne, og at tallet svarer til en position i NamespaceArray. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Tjenesterne Read og Write. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Definitionerne af værdi, statuskode, SourceTimestamp og ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain og Bad, som angives af de to øverste bit. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. At samplingsintervallet er en best effort-cyklus. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. At der uden filter sendes en notifikation ved ændring i værdi eller status. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. Køstørrelsen, kasseringspolitikken og Overflow-bitten. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. Publiceringsintervallet og keep-alives. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync samt samplingsinterval og køstørrelse i MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Betydningerne af None, Sign og SignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. Praksis med at flytte certifikater fra rejected-lageret til trusted-lageret, og at AutoAcceptUntrustedCertificates ikke bruges i produktion. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. De fire typer: anonym, brugernavn, X.509 og udstedte tokens. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. Listen over brugertokens, som et endpoint accepterer (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. At ManagedSession har en indbygget genforbindelsesmekanisme. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Tjenesten, der får et abonnements genudsendelseskø til at sende notifikationsmeddelelser igen. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (en kopi på en server hos University of Chicago). Datamodellen i afsnit 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩

De nyeste artikler med de samme tags. Gå dybere ind i emnet med nært beslægtede artikler.

Emnesider, der ligger tæt på dette tema. Med artiklen som udgangspunkt kan du gå videre til relaterede ydelser og andre artikler.

Denne artikel er knyttet til følgende ydelsessider. Vælg den indgang, der ligger tættest på din sag.

Windows-appudvikling

Udvikling af en Windows-app, der indsamler data fra udstyr, herunder abonnementsindstillinger, genforbindelse og registrering af tidsstempler og status, kan håndteres som udvikling af Windows-apps.

Teknisk rådgivning og designreview

Hvis du vil afgøre, om data skal indsamles med OPC UA, eller om Modbus/TCP eller producentens egen protokol skal bruges, og hvordan certifikater skal administreres, kan det håndteres som teknisk rådgivning og designgennemgang.

Ofte stillede spørgsmål

Her er de spørgsmål, der ofte kommer op ved rådgivning om artiklens emne.

Hvad er forskellen på OPC UA og OPC Classic (OPC DA)?
OPC Classic er et sæt specifikationer, der udveksler data via Windows COM/DCOM. OPC UA, der blev offentliggjort i 2008, er ikke bundet til et bestemt operativsystem og samler funktionerne fra Classic. OPC UA-klienter kører også uden for Windows, og der kræves ingen DCOM-konfiguration.
Er det nok at læse værdierne med et fast interval, når de indsamles med OPC UA?
Hvis der er få værdier, og intervallet er langt, kan du indsamle dem ved at læse. Hvis du vil følge ændringer i værdierne, kan du i stedet bruge et abonnement, hvor serveren står for samplingen og melder ændringerne. Uanset metode skal du registrere den statuskode og de tidsstempler, der følger med hver værdi.
Hvorfor får jeg BadCertificateUntrusted, når jeg opretter forbindelse?
Fordi klienten endnu ikke har tillid til serverens certifikat. OPC Foundations .NET-bibliotek gemmer afviste certifikater i rejected-lageret. Bekræft certifikatets aftryk (thumbprint) med serverens administrator, og flyt det derefter til trusted-lageret. Brug kun indstillinger, der automatisk accepterer certifikater, i udviklingsmiljøer.
Hvilket portnummer bruger OPC UA?
Hos IANA er 4840/TCP registreret som opcua-tcp. Rigtige servere bruger dog nogle gange en anden port, så kontrollér udstyrets indstillinger. Hvis serveren kører på Windows, skal trafik til porten tillades i Windows Firewall, og hvis der er netværksudstyr imellem, også på det udstyr.

Forfatterprofil

Profilsiden for artiklens forfatter.

Go Komura

Repræsentant for KomuraSoft LLC

Fokus på udvikling af Windows-software, teknisk rådgivning og fejlfinding, med særlig erfaring i projekter, hvor eksisterende løsninger skal bevares, og i undersøgelse af fejl, hvor årsagen er svær at spore.

Tilbage til bloggen