Bevezetés az OPC UA-ba ── PLC- és berendezésadatok gyűjtése Windows-alkalmazással

· · Windows, OPC UA, Gyártás, PLC, C#, .NET, Kommunikáció, Tanúsítványok

Közzététel az X-en
Módosítási előzmények (csak az első változat, közzétéve: 2026. 11. 01.)
Első közzététel
Hivatkozás erre a cikkre(DOI: 10.5281/zenodo.23968446)

Ez a cikk a Zenodón van archiválva. Alább egyaránt megtalálja a mindig a legfrissebb változatra mutató DOI-t és az Ön által éppen olvasott változathoz rögzített DOI-t.

Go Komura (2026): Bevezetés az OPC UA-ba ── PLC- és berendezésadatok gyűjtése Windows-alkalmazással. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968446 https://comcomponent.com/hu/blog/opc-ua-windows-data-collection/

DOI (legfrissebb változat)
10.5281/zenodo.23968446
DOI (ez a változat)
10.5281/zenodo.23968447

Ha PLC-k vagy berendezések adatait gyűjtjük egy Windows-alkalmazásba, és a berendezésnek van OPC UA-szervere, előfordulhat, hogy nem kell gyártónként külön kommunikációt megírnunk. Csatlakozni és értékeket olvasni azonban nem elég. Ha nem ismerjük az értékekkel együtt visszaadott állapotot és időbélyegeket, a változásokról értesítő előfizetést, a tanúsítványokkal történő csatlakozásengedélyezést és a kapcsolatbontás utáni helyreállást, később nem bízhatunk meg a gyűjtött adatokban.

Ez a cikk az OPC UA működésének az adatgyűjtéshez szükséges részeit mutatja be, egy C#-kliens tényleges futtatásának eredményeivel együtt. A kliens az OPC Foundation .NET-es könyvtárát használja, a másik oldalon pedig egy szimulált berendezésszerver áll. Konkrét PLC-n vagy berendezésen nem próbáltam ki. Az egyes berendezések beállításait azok dokumentációjában ellenőrizze.

A GigE Vision- és USB3 Vision-kamerák képeinek gyűjtéséről a „Dropped Frames with GigE Vision and USB3 Vision Cameras”, a soros kommunikációt használó berendezésekről pedig a „Serial Communication App Pitfalls” című cikkben írtam.

Az OPC UA és az OPC Classic különbsége

Az OPC Classic specifikációi a Windows COM/DCOM technológiájával cserélnek adatokat a szoftverkomponensek között. A folyamatadatokat kezelő OPC DA, a riasztásokat kezelő OPC A&E és az előzményadatokat kezelő OPC HDA külön specifikációként van meghatározva1.

Az OPC Foundation 2008-ban tette közzé az OPC UA-t (Unified Architecture). Az OPC UA egy adott platformtól független, szolgáltatásorientált architektúra, amely egységesen kezeli a Classic egyes specifikációinak funkcióit. Az újratervezés okaként az OPC Foundation többek között azt említi, hogy a Microsoft a COM/DCOM helyett a platformfüggetlen megoldásokra kezdett nagyobb hangsúlyt helyezni, hogy a szabványnak a Microsofttól eltérő rendszereken és beágyazott eszközökön is futnia kellett, és hogy biztonságosan kellett csatlakozni tűzfalakon keresztül is1.

Az OPC UA specifikációját az IEC is szabványosította az IEC 62541 sorozatként2. A kommunikációs portot illetően az IANA a 4840/TCP portot opcua-tcp néven tartja nyilván3.

Az OPC Classic és az OPC UA különbségeAz ábra azt mutatja, hogy az OPC Classic a Windows COM és DCOM technológiájára épül, és külön DA, A&E és HDA specifikációra oszlik, az OPC UA pedig ezek funkcióit egyetlen, platformtól független specifikációban egyesíti.Funkciók egyesítéseOPC ClassicCOM/DCOM (Windows) felett futDA, A&E és HDA külön specifikációkOPC UA (2008)Nem kötődik adott platformhozIEC 62541 szabványként

1. ábra: Az OPC Classic a COM/DCOM feletti különálló specifikációkból áll, az OPC UA pedig ezeket egyesíti.

Ha a meglévő berendezésnek csak OPC DA-szervere van, a kliens oldalán a COM/DCOM-ot használó megoldásra van szükség. Ez a cikk csak az OPC UA-val foglalkozik.

Az ábrán a folytonos vonal mindig fennálló kapcsolatot, a szaggatott vonal feltételes kapcsolatot jelöl (a feltételeket a részletoldal kapcsolatonként ismerteti). A kapcsolatok teljes listája (összesen 13, bizonyítékokkal és megbízhatósági szinttel) és a fő fogalmak meghatározása a tudástérkép részletoldalán található (japánul). Adatok: JSON-LD / Turtle

A kipróbált konfiguráció

A következő konfigurációt futtattam.

Szerep Amit használtam
Kliens C#, .NET 10 SDK 10.0.114, OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1 NuGet-csomag
Szerver Python 3.13 és asyncua 2.1.0 segítségével készített szimulált berendezés (ugyanazon a gépen, a 4840-es porton)
Operációs rendszer Linux (Windowson nem próbáltam ki a futtatást)

A kliens könyvtára az OPC Foundation által közzétett .NET-es megvalósítás. A 2.0-s sorozat használatában ugyanannak a tárolónak a Getting started leírását követtem4.

A kipróbált konfigurációAz ábra egy olyan konfigurációt mutat, amelyben az OPC Foundation .NET-es könyvtárát használó C#-kliens titkosított kapcsolaton csatlakozik a 4840-es porton figyelő, asyncua-val készített szimulált berendezésszerverhez.opc.tcp, SignAndEncryptC#-kliens (LineCollector)Szimulált berendezésszerver (asyncua)Az OPC Foundation .NET-es könyvtára, 2.0.1A Line1 négy változója

2. ábra: Ugyanazon a gépen belül a C#-kliens a szimulált berendezésszerverhez csatlakozott.

A szimulált berendezésszerver

A berendezés helyett egy kis szervert készítettem asyncua-val5. A Line1 nevű objektum alatt négy változó található: a hőmérséklet (Temperature), a gyártott darabszám (PartCount), az üzemállapot (Running) és az alapjel (Setpoint). A hőmérséklet 0,5 másodpercenként, a darabszám 2 másodpercenként változik. Biztonsági beállításként kettőt biztosítottam: titkosítás nélküli kapcsolatot (None), valamint aláírást és titkosítást (Basic256Sha256 SignAndEncrypt módban).

# Működésellenőrzésre szolgáló szimulált berendezés. Nem helyettesíti az éles berendezéseket vagy PLC-ket.
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())

Ez a szerver ellenőrzés nélkül elfogadja a csatlakozó kliensek tanúsítványát. Ez tesztbeállítás, és eltér az éles berendezések viselkedésétől.

A C#-kliens

A projektfájlba a következő két csomagot vettem fel.

<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>

A kliens sorban csatlakozik, böngészik, olvas, ír és előfizet. Az egyes részek jelentését a következő szakaszok sorra elmagyarázzák.

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}");

        // A névtérindexet a szerver határozza meg. Az URI alapján keressük ki, és abból készítjük a NodeId-ket
        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);

        // Böngészés: a Line1 alatti változók listázása
        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}");
        }

        // Olvasás: nemcsak az értéket, hanem az állapotot és a két időbélyeget is nézzük
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // Nem létező csomópont olvasásakor érték helyett Bad állapot jön vissza
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Írás: a Setpoint értékét 32,5-re állítjuk
        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]}");

        // Előfizetés: 500 ms-onkénti mintavételt kérünk a szervertől, és értesítést a változásokról
        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");

Az első futtatáskor nem sikerült csatlakozni. Ennek okát „A csatlakozás engedélyezése tanúsítványokkal” című szakasz magyarázza el. A szerver tanúsítványának megbízhatóvá tétele utáni kimenet a következő. A könyvtár figyelmeztető naplóbejegyzéseit kihagytam.

接続: 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]

Az időpontok UTC szerintiek.

A berendezés adatai csomópontokként jelennek meg a címtérben

Az OPC UA-szerver a birtokában lévő adatokat csomópontok (node-ok) halmazaként teszi közzé. Ezt a halmazt címtérnek (address space) nevezzük. A csomópontoknak különféle fajtái vannak, például a mappákat vagy berendezéseket jelölő objektumok és az értéket tároló változók, a csomópontokat pedig hivatkozások kötik össze. A kliens a Browse szolgáltatással követi a hivatkozásokat, és így deríti fel, milyen csomópontok léteznek6.

A fenti kimenetben a kliens a Line1 objektumtól kiindulva követi a hierarchikus hivatkozásokat, és négy változót talál.

A szimulált berendezés címtereAz ábra azt mutatja, hogy az Objects mappa alatt egy Line1 objektum található, alatta pedig négy változó: Temperature, PartCount, Running és Setpoint.ObjectsLine1 (objektum)Temperature (változó)PartCount (változó)Running (változó)Setpoint (változó, írható)

3. ábra: A szimulált berendezés címtere. A kliens a Browse segítségével a hivatkozásokat követve találja meg a csomópontokat.

A NodeId névtérből és azonosítóból áll

A csomópontokat a NodeId azonosítja. A kimenetben szereplő ns=2;s=Line1.Temperature azt jelenti, hogy a névtérindex 2, az azonosító pedig a Line1.Temperature karakterlánc.

A névtér valójában egy URI. A specifikáció szerint a szolgáltatásokon belül a névtér URI-ját szám jelöli, és ez a szám a szerver NamespaceArray tömbjében elfoglalt pozíciónak felel meg. A 0-s index az OPC UA névtere, a http://opcfoundation.org/UA/7.

Ezért az index számát nem érdemes beírni a kódba; ehelyett az URI alapján kell kikeresni. A fenti kód a session.NamespaceUris.GetIndex(LineNamespace) hívással kapja meg az URI-ból a 2-es indexet, és abból készíti el a NodeId-ket.

NodeId előállítása a névtér URI-jábólAz ábra azt mutatja, hogy a kliens a névtér URI-ját a szerver NamespaceArray tömbjében kikeresve megkapja az indexet, majd az indexet az azonosítóval kombinálva előállítja a NodeId-t.A névtér URI-jaA szerver NamespaceArray tömbjeIndex (ebben a példában 2)NodeId ns=2;s=Line1.TemperatureAzonosító: Line1.Temperature

4. ábra: A névtérindexet az URI alapján keressük ki, és az azonosítóval kombinálva kapjuk a NodeId-t.

Olvasáskor és íráskor az értékkel együtt állapot és időbélyeg is visszajön

A változók értékét a Read és a Write szolgáltatással olvassuk és írjuk8. Olvasáskor nemcsak az érték jön vissza. Egy DataValue nevű struktúra érkezik, amely az értéket, az állapotkódot, a forrásidőbélyeget és a szerveridőbélyeget tartalmazza9.

Elem Jelentés
Érték A változó értéke. Ha az állapotkód hibát jelez, az értéket figyelmen kívül kell hagyni, és a szerver null értékre állítja
Állapotkód (StatusCode) Sikerült-e a szervernek megszereznie és szolgáltatnia az értéket
SourceTimestamp Az az időpont, amelyet az adat forrása rendelt az értékhez
ServerTimestamp Az az időpont, amikor a szerver megkapta az értéket, vagy amikor kiderült, hogy az érték helyes

Az állapotkód felső két bitje Good (sikeres), Uncertain (részben sikeres, egyes célokra esetleg nem használható) vagy Bad (sikertelen) állapotot jelez10. A kimenetben szereplő Good [0x00000000] Good állapotot jelent.

A nem létező Line1.Pressure olvasásakor érték helyett BadNodeIdUnknown [0x80340000] jött vissza. Ennek a könyvtárnak a ReadValueAsync metódusa kivételt dob, ha az állapot Bad4. Ha az értékeket rögzítő rendszer az értékek mellett az állapotkódot is megőrzi, később meg lehet különböztetni a Bad állapot idején rögzített értékeket.

A DataValue tartalmaAz ábra azt mutatja, hogy az olvasáskor vagy értesítéskor visszaadott DataValue négy elemet tartalmaz – az értéket, az állapotkódot, a forrásidőbélyeget és a szerveridőbélyeget –, és hogy Bad állapotkód esetén az értéket figyelmen kívül kell hagyni.Ha BadDataValueÉrtékÁllapotkódSourceTimestamp (a forrás időpontja)ServerTimestamp (a szerver időpontja)Az értéket figyelmen kívül hagyjuk

5. ábra: Az olvasás eredménye nemcsak az értéket, hanem az állapotkódot és két időbélyeget is tartalmaz.

Íráskor 32,5-öt írtam a Setpoint változóba, és eredményként Good állapotkód jött vissza. Azt, hogy melyik változó írható, a szerver dönti el. Ebben a szimulált szerverben csak a Setpoint írható. Az, hogy a berendezés alapjeleit kívülről át lehessen-e írni, a berendezés biztonsági tervezésének kérdése, ezért ez a cikk nem foglalkozik vele.

Értesítés a változásokról: előfizetés és MonitoredItem

Az értékek változásának követésére az is lehetséges, hogy a kliens rendszeres időközönként ismételten Read hívást küld. Az OPC UA-ban ettől függetlenül van egy olyan mechanizmus is, amely az értékek figyelését a szerverre bízza, és értesítést kér a változásokról. Ez az előfizetés (subscription) és a MonitoredItem.

A MonitoredItem egyetlen figyelt elem, amelynek mintavételi időköze van. A mintavételi időköz a leggyorsabb ütem, amellyel a szerver a forrásból mintát vesz az értékekből, és a specifikáció ezt „best effort” ciklusként írja le11. Ha a minta megfelel a szűrő feltételeinek, értesítés jön létre. Ha nincs megadva szűrő, akkor az érték vagy az állapot változásakor jön értesítés12.

Az értesítések először a MonitoredItem várakozási sorába kerülnek. Előre meg kell határozni, hogy a sor megtelésekor a legrégebbi vagy a legújabb elem kerüljön-e eldobásra, és ha a sor mérete legalább 2, az állapotkódban beáll az eldobást jelző Overflow bit13.

Az előfizetés több MonitoredItemet fog össze, és minden közzétételi időközben elküldi a kliensnek a még el nem küldött értesítéseket. Ha egymás után több cikluson át nincs küldendő értesítés, keep-alive üzenetet küld annak jelzésére, hogy az előfizetés még él14.

Az előfizetés és a MonitoredItem működéseAz ábra azt mutatja, hogy a MonitoredItem a mintavételi időközzel mintát vesz a forrás értékéből, a szűrőn átjutott értesítéseket várakozási sorba helyezi, az előfizetés pedig a közzétételi időközzel összegyűjtve elküldi őket a kliensnek, értesítés hiányában pedig keep-alive üzenetet küld.Mintavételi időközKözzétételi időközHa nincs értesítésForrás (a berendezés értéke)MonitoredItemSzűrő (alapértelmezés: az érték vagy az állapot változása)Várakozási sor (megteléskor Overflow)ElőfizetésKliensKeep-alive

6. ábra: A MonitoredItem mintát vesz és sorba állít, az előfizetés pedig összegyűjtve küldi el az értesítéseket a kliensnek.

A fenti kód 500 ezredmásodperces mintavételi időközzel és 5-ös sormérettel fizet elő a Temperature és a PartCount változóra15. A kimenetben a hőmérséklet nagyjából 0,5 másodpercenként, a darabszám nagyjából 2 másodpercenként érkezik. Az első két értesítés azt az értéket tartalmazta, amelyet az egyes változók az előfizetés kezdetekor tartalmaztak. A darabszámnál szereplő 13:40:50.801 az az időpont, amikor az érték az előfizetés kezdete előtt megváltozott. Mivel az értesítések a forrásidőbélyeget jelenítik meg, nem az értesítés beérkezésének, hanem az érték megváltozásának időpontja látható.

Ismételt olvasás vagy előfizetés

Az ismételt olvasás esetén az időközt a kliens oldalán lehet meghatározni, és a mechanizmus egyszerű. Cserébe minden alkalommal oda-vissza megy egy kérés és egy válasz akkor is, ha az érték nem változott, az időköznél rövidebb változások pedig elsikkadnak. Előfizetéskor a szerver vesz mintát, és csak a változásokat küldi el, így a ritkán változó értékeknél sem nő sokat a forgalom. Előfizetéskor azonban a mintavételi időköznél rövidebb változásokról sem feltétlenül érkezik értesítés. A specifikáció megengedi, hogy a szerver a kértnél gyorsabban vegyen mintát, de nem követeli meg11.

Az ismételt olvasás és az előfizetés különbségeAz ábra azt mutatja, hogy ismételt olvasáskor a kliens határozza meg az időközt, és minden alkalommal kérést küld, előfizetéskor a szerver a mintavételi időközzel mintát vesz, és csak a változásokat küldi el, és hogy mindkét módszer elszalaszthatja az időköznél rövidebb változásokat.Az értékváltozások követéseIsmételt olvasásElőfizetésA kliens határozza meg az időközt, és minden alkalommal kérA szerver vesz mintát, és csak a változásokat küldiAz időköznél rövidebb változások elveszhetnek

7. ábra: Mindkét módszernél előfordulhat, hogy az időköznél rövidebb változások nem érzékelhetők.

A csatlakozás engedélyezése tanúsítványokkal

Az OPC UA-kapcsolatban az üzenetek védelmének mértékét a MessageSecurityMode határozza meg. Lehetséges értékei: a védelmet nem nyújtó None, a csak aláíró Sign, valamint az aláíró és titkosító SignAndEncrypt16. Az aláíráshoz és titkosításhoz használt algoritmusok kombinációját a biztonsági házirend (security policy) határozza meg; ez a cikk a Basic256Sha256 házirendet használta.

Az aláíráshoz és a titkosításhoz minden alkalmazásnak saját tanúsítványa van. A kliens és a szerver ellenőrzi egymás tanúsítványát, és elutasítja a kapcsolatot azokkal, akikben nem bízik. Az OPC Foundation .NET-es könyvtárával készített szerver alapértelmezés szerint nem kínálja a titkosítás nélküli None házirendet4.

Az első csatlakozás BadCertificateUntrusted hibával állt le

Az első futtatáskor a kliens nem bízott meg a szerver tanúsítványában, ezért a kapcsolatot elutasította. A kimenet egy része:

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.'

Ez a könyvtár az elutasított tanúsítványokat a rejected tárolóba menti. Ha a rendszergazda onnan áthelyezi őket a trusted tárolóba, a könyvtár megbízik az adott félben17. Ezúttal a pki/rejected/certs mappában lévő szervertanúsítványt áthelyeztem a pki/trusted/certs mappába, újra lefuttattam a programot, és sikerült csatlakozni. Az áthelyezés előtt egyeztesse a szerver rendszergazdájával a fájlnévben is szereplő ujjlenyomatot4.

A tanúsítvány megbízhatóvá tételének lépéseiAz ábra azt mutatja, hogy az első csatlakozáskor a kliens elutasítja a szerver tanúsítványát, és a rejected tárolóba menti, majd az ujjlenyomat egyeztetése és a trusted tárolóba helyezés után a következő csatlakozáskor már megbízik benne, és a csatlakozás sikerül.Első csatlakozásBadCertificateUntrustedA tanúsítvány a rejected tárolóba kerülAz ujjlenyomat egyeztetése a szerver rendszergazdájávalÁthelyezés a trusted tárolóbaA következő csatlakozás megbízhatóként sikerül

8. ábra: Az elutasított tanúsítvány ellenőrzése és a trusted tárolóba helyezése után a kliens megbízik az adott félben.

Létezik egy AutoAcceptUntrustedCertificates nevű beállítás is, amely automatikusan elfogadja a nem megbízható tanúsítványokat. Az alapértelmezés false, és a könyvtár dokumentációja szerint fejlesztői környezetben hasznos, éles környezetben azonban nem szabad használni17.

A naplóban szereplő NotAfter=2027-11-01 a szimulált szerver tanúsítványának lejárati dátuma. Ez egy asyncua-függvénnyel készített önaláírt tanúsítvány, amely a létrehozása után egy évvel jár le. A berendezések tanúsítványainak is van lejárati idejük, ezért az üzemeltetés részeként előre meg kell határozni, hogyan cseréljük le őket a lejárat előtt, és hogyan tesszük az újakat megbízhatóvá a másik fél számára. A Windows tanúsítványtárolójának kezeléséről a „The Windows Certificate Store in Practice” című cikkben írtam.

A felhasználók hitelesítése

A tanúsítványok az alkalmazásokat ellenőrzik egymás között. Ettől függetlenül a munkamenet megnyitásakor a felhasználót azonosító tokent is átadunk. A specifikáció négyféle tokent határoz meg: névtelen, felhasználónév és jelszó, X.509-tanúsítvány, valamint külső fél által kiadott token18. Az itt bemutatott kliens névtelenül csatlakozott. Azt, hogy a szerver milyen tokeneket fogad el, a szerver végpontadatai tartalmazzák19.

Ahol a helyszíni adatgyűjtés elakad

A következőkben a berendezésadatokat gyűjtő rendszerekben gyakran előforduló problémákat sorolom fel, a mostani kísérlet és a specifikáció alapján.

A szerver leállása alatti értékek nem érkeznek meg

Az előfizetés közben leállítottam a szervert, majd körülbelül 15 másodperc múlva újraindítottam. Ennek a könyvtárnak a ManagedSession osztálya a kapcsolat megszakadásakor automatikusan újracsatlakozik20. Amíg a szerver le volt állítva, a kliens folyamatosan próbált újracsatlakozni.

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.

Amikor a szerver visszatért, az értesítések is újra megérkeztek. Az előfizetés kimenetének egy része:

  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]

A 13:41:13 és 13:41:34 közötti időszakból egyetlen érték sem érkezett meg. A darabszám 78-ról 2-re esett vissza. Ennek az az oka, hogy a szimulált szerver újraindításakor a számlálás 0-ról indult újra.

Ebben a példában maga a szerver állt le, így az ez alatti értékek sehol sem maradtak meg. Az újracsatlakozási mechanizmus helyreállítja a kapcsolatot, de utólag nem pótolja a leállás alatti értékeket.

Más a helyzet, ha csak a kommunikáció szakad meg, és a szerver tovább fut. A specifikációban szerepel a Republish szolgáltatás, amellyel a szerver újraküldési sorából ismét el lehet kérni a meg nem érkezett értesítési üzeneteket21. A MonitoredItem várakozási sorában is felgyűlnek az értesítések, de a sor méretét meghaladó rész eldobásra kerül13. Ezt az esetet a mostani kísérletben nem próbáltam ki. Mindkét esetben rögzítse a kapcsolat megszakadásának és helyreállásának időpontját, hogy utólag kiderüljön, melyik időszakban nem érkeztek értékek. A soros kommunikációt használó berendezések újracsatlakozásáról és naplózásának tervezéséről a „Serial Communication App Pitfalls” című cikk is szól.

Rögzítés a szerver leállásakorAz ábra azt mutatja, hogy ha előfizetés közben leáll a szerver, a kliens folyamatosan próbál újracsatlakozni, a szerver visszatérésekor az értesítések is újra megérkeznek, a leállás alatti értékek azonban nem érkeznek meg, ezért a megszakadás és az újracsatlakozás időpontját rögzíteni kell.Előfizetés folyamatbanA szerver leállA kliens folyamatosan próbál újracsatlakozniA szerver visszatér, az értesítések újraindulnakA leállás alatti értékek nem érkeznek megA megszakadás és az újracsatlakozás időpontjának rögzítése

9. ábra: Az újracsatlakozás helyreállítja a kapcsolatot, de a szerver leállása alatti értékek nem érkeznek meg.

Melyik időbélyeget rögzítsük?

A specifikáció szerint a SourceTimestamp az az időpont, amelyet az adat forrása rendelt az értékhez, és annak az időpontnak kell lennie, amikor az érték megváltozott. A ServerTimestamp az az időpont, amikor a szerver megkapta az értéket, vagy amikor kiderült, hogy az érték helyes9. A mostani szimulált szerver maga állítja elő az értékeket, ezért a két időpont megegyezett.

Ha azt szeretné tudni, mikor változott meg egy érték a berendezésen, a SourceTimestamp értéket használja. Ezt az időpontot azonban a forrás órája adja, így ha a berendezés vagy a szerver órája eltér, a rögzített adat is eltér. Ha a gyűjtött adatokat más nyilvántartásokkal vetné össze, mentse el mellettük a fogadó számítógép idejét is, így később ellenőrizhető az eltérés. A Windows oldali időszinkronizálásról a „Windows Time Synchronization (w32time) and Business Systems” című cikkben írtam.

A nem Good állapotú értékeket ne kezelje egyszerű számként

Ha az állapotkód Bad, az értéket figyelmen kívül kell hagyni9. Uncertain állapot esetén az érték egyes célokra nem használható10. Mielőtt az értékek grafikonokba vagy összesítésekbe kerülnének, válogassa szét őket állapotkód szerint, és határozza meg, mi történjen a nem Good állapotú értékekkel. A várakozási sor megtelését jelző Overflow bit is az állapotkódban áll be13. A berendezések állapotának képernyőn való megjelenítéséről a „Best Practices for Checking and Displaying External Device State” című cikk szól.

Nyissa meg a portot a tűzfalon

Az IANA-nál nyilvántartott OPC UA-port a 4840/TCP3. Nem minden szerver használja azonban ezt a számot. Az OPC Foundation .NET-es mintái például a 62541-es portot is használják4. Ellenőrizze a portszámot a berendezés végpontjának URL-jében, és engedélyezze az erre a portra irányuló kapcsolatokat. Ha a szerver Windowson fut, annak a gépnek a Windows tűzfalában, ha pedig hálózati eszköz van közöttük, azon az eszközön engedélyezze. A bejövő szabályok létrehozásáról a „The Windows Firewall and Business Applications” című cikkben írtam.

Ahol az adatgyűjtés elakadAz ábra a berendezésadatok gyűjtésének buktatóit mutatja két csoportra osztva: a rögzített adatok tartalma (a szerver leállása alatti értékek nem érkeznek meg, melyik időbélyeget rögzítsük, a nem Good állapotú értékek kezelése) és a csatlakozás előkészítése (tűzfalportok, a tanúsítványok lejárata).Ahol az adatgyűjtés elakadA rögzített adatok tartalmaA csatlakozás előkészítéseA leállás alatti értékek nem érkeznek megMelyik időbélyegA nem Good értékek kezeléseTűzfalportokA tanúsítványok lejárata

10. ábra: A sikeres csatlakozás után is ellenőrizze a hiányzó adatokat, az időpontokat, az állapotot, a portokat és a tanúsítványokat.

Választás az OPC UA, a Modbus/TCP és a gyártók saját kommunikációja között

A PLC-kkel és berendezésekkel való kommunikációra az OPC UA mellett a Modbus/TCP és a PLC-gyártók által biztosított kommunikáció is rendelkezésre áll.

A Modbus specifikációja négy táblában kezeli az adatokat: a csak olvasható, 1 bites Discrete Input, az írható-olvasható, 1 bites Coil, a csak olvasható, 16 bites Input Register és az írható-olvasható, 16 bites Holding Register táblában22. Az IANA a Modbus alkalmazásprotokollja (mbap) számára az 502/TCP portot tartja nyilván3.

Ezekbe a táblákba bitek és 16 bites szavak kerülnek, ezért azt, hogy melyik címen milyen érték milyen formátumban található, a berendezés dokumentációjában kell ellenőrizni, és a kliens oldalán kell leírni. Az OPC UA-ban, ahogy eddig láttuk, a szerver névvel ellátott változókat tesz közzé a címtérben, a kliens pedig a Browse segítségével fel tudja deríteni őket. Az értékekhez állapotkód és időbélyeg tartozik, a csatlakozást pedig tanúsítvány engedélyezi.

Hogy melyiket használjuk, azt sokszor az dönti el, mit biztosít a berendezés. Ha a berendezésnek van OPC UA-szervere, azt használjuk; ha csak Modbus/TCP-t támogat, címtérképet készítünk, és az alapján olvasunk. Ha csak a gyártó saját kommunikációja vagy SDK-ja áll rendelkezésre, annak dokumentációját követjük. Az USB-eszközökkel való kommunikáció módjának kiválasztásáról a „How to Work with USB Devices from a Windows App” című cikkben írtam.

A berendezéssel való kommunikáció módjának kiválasztásaAz ábra azt mutatja, hogy ha a berendezésnek van OPC UA-szervere, OPC UA-val böngésszük és gyűjtjük a változókat, ha csak Modbus/TCP-t támogat, a dokumentációban ellenőrizzük a címeket és a formátumokat, és regisztereket olvasunk, ha pedig csak a gyártó saját kommunikációja áll rendelkezésre, annak dokumentációját követjük.OPC UA-szerverCsak Modbus/TCPCsak gyártói kommunikációA berendezés által biztosított kommunikációMi áll rendelkezésre?A változók böngészése és gyűjtéseA címek és formátumok ellenőrzése a dokumentációbanA gyártó dokumentációjának és SDK-jának követéseA regiszterek kiolvasása és értékké alakítása

11. ábra: Hogy mivel gyűjtjük az adatokat, azt sokszor az dönti el, mit biztosít a berendezés.

Kapcsolódó cikkek

Források

  1. OPC Foundation, Classic. Hogy az OPC Classic a COM/DCOM-on alapul, a DA, A&E és HDA specifikációk, az OPC UA 2008-as közzététele és az újratervezés okai. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Az IEC 62541 sorozat 1. része, amely az OPC UA fogalmait és áttekintését tárgyalja (azóta újabb kiadás váltotta fel). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. Az opcua-tcp (4840/TCP) és az mbap (502/TCP) nyilvántartása. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. A 2.0-s sorozatú kliens megírásának módja, hogy a ReadValueAsync Bad állapotnál kivételt dob, az elutasított és a megbízható tanúsítványok tárolói, hogy a szerverek alapértelmezés szerint nem kínálják a None házirendet, valamint a mintákban használt portszámok. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). A szimulált berendezésszerverhez használt Python OPC UA-könyvtár. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. A csomópontok hivatkozásait követő szolgáltatás. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. Hogy a szolgáltatásokon belül a névtér URI-ját szám jelöli, és ez a szám a NamespaceArray egy pozíciójának felel meg. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. A Read és a Write szolgáltatás. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Az érték, az állapotkód, a SourceTimestamp és a ServerTimestamp meghatározása. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. A felső két bit által jelzett Good, Uncertain és Bad állapot. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. Hogy a mintavételi időköz „best effort” ciklus. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. Hogy szűrő nélkül az érték vagy az állapot változásakor jön értesítés. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. A várakozási sor mérete, az eldobás módja és az Overflow bit. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. A közzétételi időköz és a keep-alive. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. A SubscribeDataChangesAsync, valamint a MonitoredItemOptions mintavételi időköze és sormérete. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. A None, a Sign és a SignAndEncrypt jelentése. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. Az a gyakorlat, hogy a tanúsítványokat a rejected tárolóból a megbízható tárolóba helyezzük át, valamint hogy az AutoAcceptUntrustedCertificates beállítást éles környezetben nem szabad használni. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. A négyféle token: névtelen, felhasználónév, X.509 és kiadott token. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. A végpont által elfogadott felhasználói tokenek listája (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Hogy a ManagedSession beépített újracsatlakozási mechanizmussal rendelkezik. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Az a szolgáltatás, amellyel az előfizetés újraküldési sorából ismét el lehet kérni az értesítési üzeneteket. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (a Chicagói Egyetem szerverén tárolt másolat). A 4.3. szakasz adatmodellje (Discrete Input, Coil, Input Register, Holding Register). ↩

Azonos címkéket használó, legfrissebb cikkek. Mélyítse el ismereteit szorosan kapcsolódó témákban.

Ehhez a témához közel álló témaoldalak. A cikkből kiindulva eljuthat a kapcsolódó szolgáltatásokhoz és további cikkekhez.

Ez a cikk a következő szolgáltatási oldalakhoz kapcsolódik. Kezdje azzal, amelyik a leginkább illik az Ön helyzetéhez.

Gyakori kérdések

A cikk témájával kapcsolatban a konzultációk során gyakran felmerülő kérdések.

Mi a különbség az OPC UA és az OPC Classic (OPC DA) között?
Az OPC Classic olyan specifikáció, amely a Windows COM/DCOM technológiáján keresztül cserél adatokat. Az OPC UA egy 2008-ban közzétett, adott operációs rendszertől független specifikáció, amely egységesen kezeli a Classic funkcióit. Az OPC UA-kliensek Windowson kívül is futnak, és nincs szükség DCOM-beállításra.
Ha OPC UA-val gyűjtök értékeket, elég rendszeres időközönként kiolvasni őket?
Ha kevés értékről van szó, és hosszú az időköz, olvasással is gyűjthetők. Ha az értékek változását akarja követni, előfizetéssel a szerverre bízhatja a mintavételt, és értesítést kérhet a változásokról. Mindkét esetben rögzítse az értékkel együtt visszaadott állapotkódot és időbélyegeket is.
Miért kapok BadCertificateUntrusted hibát csatlakozáskor?
Azért, mert a kliens még nem bízik meg a szerver tanúsítványában. Az OPC Foundation .NET-es könyvtára az elutasított tanúsítványokat a rejected tárolóba menti. Egyeztesse a tanúsítvány ujjlenyomatát a szerver rendszergazdájával, majd helyezze át a trusted tárolóba. A tanúsítványokat automatikusan elfogadó beállítást csak fejlesztői környezetben használja.
Milyen portszámot használ az OPC UA?
Az IANA a 4840/TCP portot opcua-tcp néven tartja nyilván. A valódi szerverek azonban más portot is használhatnak, ezért ellenőrizze a berendezés beállításaiban. Ha a szerver Windowson fut, a Windows tűzfalban, ha pedig hálózati eszköz van közöttük, azon az eszközön kell engedélyezni az adott portra irányuló forgalmat.

A szerző bemutatása

A cikk szerzőjének bemutatkozó oldala.

Go Komura

A KomuraSoft LLC képviselője

Fő területei a Windows-szoftverfejlesztés, a műszaki konzultáció és a hibakeresés; különösen a meglévő megoldásokat érintő projektekben, valamint a nehezen felderíthető okú hibák kivizsgálásában erős.

Nyilvános hivatkozások

Vissza a blogra