مدخل إلى OPC UA ── جمع بيانات وحدات PLC والأجهزة في تطبيق Windows

· · Windows, OPC UA, التصنيع, PLC, C#, .NET, الاتصالات, الشهادات

النشر على X المشاركة على Telegram المشاركة على WhatsApp
سجل التعديلات (النسخة الأولى، نُشرت في 1 Nov، 2026)
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.23968385)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

غو كومورا (2026). مدخل إلى OPC UA ── جمع بيانات وحدات PLC والأجهزة في تطبيق Windows. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.23968385 https://comcomponent.com/ar/blog/opc-ua-windows-data-collection/

DOI (أحدث نسخة)
10.5281/zenodo.23968385
DOI (هذه النسخة)
10.5281/zenodo.23968386

عندما تجمع بيانات وحدة PLC أو جهاز في تطبيق Windows، ويكون لدى الجهاز خادم OPC UA، فقد تستغني عن كتابة اتصال مختلف لكل شركة مصنّعة. لكنّ الاتصال وقراءة القيم وحدهما لا يكفيان. إن لم تعرف الحالة والطوابع الزمنيّة التي تعود مع كل قيمة، والاشتراك الذي يُخطرك بالتغييرات، والسماح بالاتصال عبر الشهادات، وكيفيّة التعافي من انقطاع الاتصال، فلن تستطيع لاحقاً أن تثق بالبيانات التي جمعتها.

يشرح هذا المقال أجزاء OPC UA التي تلزم لجمع البيانات، مع نتائج تشغيل عميل مكتوب بلغة C# فعلياً. يستخدم العميل مكتبة OPC Foundation لـ.NET، والطرف الآخر خادم يحاكي جهازاً. لم أجرّب أيّ وحدة PLC أو جهاز بعينه. تحقّق من إعدادات كل جهاز في وثائقه.

جمع الصور من كاميرات GigE Vision وUSB3 Vision مشروح في «Dropped Frames with GigE Vision and USB3 Vision Cameras»، والأجهزة التي تتواصل عبر الاتصال التسلسليّ في «مزالق تطبيقات الاتصالات التسلسلية».

الفرق بين OPC UA وOPC Classic

تتبادل مواصفات OPC Classic البيانات بين مكوّنات البرمجيات باستخدام COM/DCOM في Windows. وقد عُرِّفت OPC DA لبيانات العمليّات، وOPC A&E للإنذارات، وOPC HDA للبيانات التاريخيّة، كمواصفات منفصلة1.

نشرت OPC Foundation عام 2008 مواصفة OPC UA (Unified Architecture). وOPC UA معماريّة موجّهة نحو الخدمات لا تعتمد على منصّة بعينها، وتجمع وظائف كل واحدة من مواصفات Classic. ومن الأسباب التي تذكرها OPC Foundation لإعادة البناء أنّ Microsoft صارت تقدّم الآليّات متعدّدة المنصّات على COM/DCOM، وأنّه كان لا بدّ من العمل على أنظمة غير أنظمة Microsoft وعلى الأجهزة المدمجة، وأنّه كان لا بدّ من الاتصال الآمن عبر جدران الحماية1.

ومواصفة OPC UA معتمدة أيضاً معياراً لدى IEC ضمن السلسلة IEC 625412. أمّا منفذ الاتصال، فقد سجّلت IANA المنفذ 4840/TCP باسم opcua-tcp3.

الفرق بين OPC Classic وOPC UAمخطّط يبيّن أنّ OPC Classic مبنيّ على COM وDCOM في Windows ومقسّم إلى مواصفات منفصلة هي DA وA&E وHDA، وأنّ OPC UA يجمع وظائفها في مواصفة واحدة لا تعتمد على منصّة بعينها.يجمع الوظائفOPC Classicيعمل فوق COM/DCOM (Windows)مواصفات منفصلة: DA وA&E وHDAOPC UA (2008)لا يعتمد على منصّة بعينهامعيار IEC 62541

الشكل 1: OPC Classic مواصفات منفصلة فوق COM/DCOM، وOPC UA يجمعها في مواصفة واحدة.

إذا لم يكن لدى جهاز قائم سوى خادم OPC DA، فيلزم في جهة العميل آليّة تستخدم COM/DCOM. ولا يتناول هذا المقال إلّا OPC UA.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 13، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

الإعداد الذي جرّبته

هذا هو الإعداد الذي شغّلته.

الدور ما استخدمته
العميل C#، و.NET 10 SDK 10.0.114، وحزمة NuGet ‏OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
الخادم جهاز محاكى مبنيّ بـPython 3.13 وasyncua 2.1.0 (المنفذ 4840 على الحاسوب نفسه)
نظام التشغيل Linux (لم أجرّب التشغيل على Windows)

مكتبة العميل هي تنفيذ .NET الذي تنشره OPC Foundation. وفي طريقة استخدام إصدارات 2.0 اتّبعت الأسلوب الوارد في Getting started في المستودع نفسه4.

الإعداد الذي جرّبتهمخطّط يبيّن عميلاً بلغة C# يستخدم مكتبة OPC Foundation لـ.NET ويتصل اتصالاً مشفّراً بخادم asyncua يحاكي جهازاً ويستمع على المنفذ 4840.opc.tcp، SignAndEncryptعميل C# (LineCollector)خادم جهاز محاكى (asyncua)مكتبة OPC Foundation لـ.NET 2.0.1أربعة متغيّرات تحت Line1

الشكل 2: على الحاسوب نفسه، اتصل عميل C# بخادم الجهاز المحاكى.

خادم الجهاز المحاكى

بدلاً من جهاز حقيقيّ، بنيت خادماً صغيراً باستخدام asyncua5. تحت كائن اسمه Line1 توجد أربعة متغيّرات: درجة الحرارة (Temperature)، وعدد القطع المنتجة (PartCount)، وحالة التشغيل (Running)، والقيمة المضبوطة (Setpoint). تتغيّر درجة الحرارة كل 0.5 ثانية، وعدد القطع كل ثانيتين. وللأمان وفّرت خيارين: بلا تشفير (None)، وتوقيع مع تشفير (SignAndEncrypt من Basic256Sha256).

# جهاز محاكى للتحقّق من التشغيل. ليس بديلاً عن الأجهزة أو وحدات PLC في بيئة الإنتاج.
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())

يقبل هذا الخادم شهادات العملاء المتّصلين به دون فحصها. هذا إعداد للتجربة، ويختلف عن سلوك الأجهزة في بيئة الإنتاج.

عميل C#

أضفت إلى ملفّ المشروع الحزمتين التاليتين.

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

يتصل العميل، ثم يستعرض، ثم يقرأ، ثم يكتب، ثم يشترك، بهذا الترتيب. وتشرح الأقسام التالية معنى كل جزء.

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

        // الخادم هو الذي يحدّد فهرس فضاء الأسماء. نستخرجه من الـURI ونبني منه NodeId
        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);

        // الاستعراض (Browse): سرد المتغيّرات الموجودة تحت 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}");
        }

        // القراءة: ننظر إلى الحالة والطابعين الزمنيّين، لا إلى القيمة وحدها
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // قراءة عقدة غير موجودة تعيد حالة Bad بدلاً من قيمة
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // الكتابة: نجعل Setpoint يساوي 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]}");

        // الاشتراك: نطلب من الخادم أخذ عيّنة كل 500ms وإخطارنا بالتغييرات
        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");

في التشغيل الأوّل لم يتمّ الاتصال. والسبب مشروح في قسم «السماح بالاتصال عبر الشهادات». وهذا هو الناتج بعد الوثوق بشهادة الخادم، مع حذف سجلّات التحذير الخاصّة بالمكتبة.

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

الأوقات بتوقيت UTC.

بيانات الجهاز تظهر عقداً في فضاء العناوين

يعرض خادم OPC UA البيانات التي يحملها مجموعةً من العقد. وتُسمّى هذه المجموعة فضاء العناوين (address space). وللعقد أنواع، منها الكائنات التي تمثّل المجلّدات أو الأجهزة، والمتغيّرات التي تحمل القيم، وترتبط العقد بعضها ببعض عبر المراجع (references). ويتتبّع العميل المراجع بخدمة Browse ليعرف ما العقد الموجودة6.

في الناتج أعلاه، يتتبّع العميل المراجع الهرميّة انطلاقاً من Line1 فيجد أربعة متغيّرات.

فضاء العناوين في الجهاز المحاكىمخطّط يبيّن وجود كائن Line1 تحت المجلّد Objects، وتحته أربعة متغيّرات هي Temperature وPartCount وRunning وSetpoint.ObjectsLine1 (كائن)Temperature (متغيّر)PartCount (متغيّر)Running (متغيّر)Setpoint (متغيّر، قابل للكتابة)

الشكل 3: فضاء العناوين في الجهاز المحاكى. يجد العميل العقد بتتبّع المراجع عبر Browse.

يتكوّن NodeId من فضاء أسماء ومعرّف

تُعرَّف العقد بواسطة NodeId. وفي الناتج يعني ns=2;s=Line1.Temperature أنّ فهرس فضاء الأسماء هو 2، وأنّ المعرّف هو السلسلة النصّيّة Line1.Temperature.

فضاء الأسماء (namespace) في حقيقته URI. وتنصّ المواصفة على أنّ URI فضاء الأسماء يُمثَّل داخل الخدمات برقم، وأنّ هذا الرقم يقابل الموضع في NamespaceArray الخاصّ بالخادم. والفهرس 0 هو فضاء أسماء OPC UA ‏http://opcfoundation.org/UA/7.

لذلك لا تكتب رقم الفهرس في الشيفرة، بل استخرجه من الـURI. تحصل الشيفرة أعلاه على الرقم 2 من الـURI عبر session.NamespaceUris.GetIndex(LineNamespace)، ثم تبني منه NodeId.

بناء NodeId من URI فضاء الأسماءمخطّط يبيّن أنّ العميل يبحث عن URI فضاء الأسماء في NamespaceArray الخاصّ بالخادم ليحصل على الفهرس، ثم يجمع الفهرس مع المعرّف ليبني NodeId.URI فضاء الأسماءNamespaceArray الخاصّ بالخادمالفهرس (2 في هذا المثال)NodeId ns=2;s=Line1.Temperatureالمعرّف Line1.Temperature

الشكل 4: يُستخرج فهرس فضاء الأسماء من الـURI، ثم يُجمع مع المعرّف ليصبح NodeId.

القراءة والكتابة تعيدان الحالة والطوابع الزمنيّة مع القيمة

تُقرأ قيم المتغيّرات وتُكتب بخدمتي Read وWrite8. ولا تعيد القراءة القيمة وحدها، بل تعيد بنية تُسمّى DataValue تضمّ القيمة، ورمز الحالة، والطابع الزمنيّ للمصدر، والطابع الزمنيّ للخادم9.

العنصر المعنى
القيمة قيمة المتغيّر. عندما يدلّ رمز الحالة على خطأ تُهمَل القيمة، ويجعلها الخادم null
رمز الحالة (StatusCode) هل تمكّن الخادم من الحصول على القيمة وتقديمها
SourceTimestamp الوقت الذي أرفقه مصدر البيانات بالقيمة
ServerTimestamp الوقت الذي استلم فيه الخادم القيمة، أو عرف فيه أنّها صحيحة

يدلّ البتّان الأعليان في رمز الحالة على Good (نجاح)، أو Uncertain (نجاح جزئيّ، وقد لا تصلح القيمة لبعض الاستخدامات)، أو Bad (فشل)10. وGood [0x00000000] في الناتج تعني Good.

عندما قرأت Line1.Pressure غير الموجود، عاد BadNodeIdUnknown [0x80340000] بدلاً من قيمة. والدالّة ReadValueAsync في هذه المكتبة ترمي استثناءً عندما تكون الحالة Bad4. وإذا كان نظام تسجيل القيم يحفظ رمز الحالة بجانب كل قيمة، فيمكن لاحقاً تمييز القيم التي سُجّلت أثناء كون الحالة Bad.

محتوى DataValueمخطّط يبيّن أنّ DataValue العائدة من القراءة أو الإخطار تضمّ أربعة عناصر هي القيمة ورمز الحالة والطابع الزمنيّ للمصدر والطابع الزمنيّ للخادم، وأنّ القيمة تُهمَل عندما يكون رمز الحالة Bad.إن كانت BadDataValueالقيمةرمز الحالةSourceTimestamp (وقت المصدر)ServerTimestamp (وقت الخادم)تُهمَل القيمة

الشكل 5: نتيجة القراءة لا تضمّ القيمة وحدها، بل رمز الحالة وطابعين زمنيّين أيضاً.

في الكتابة كتبت 32.5 في Setpoint، فعاد رمز الحالة Good. والخادم هو الذي يقرّر أيّ المتغيّرات قابلة للكتابة. وفي هذا الخادم المحاكى جعلت Setpoint وحده قابلاً للكتابة. أمّا السماح بتغيير القيم المضبوطة للجهاز من الخارج أو عدمه فيتعلّق بتصميم سلامة الجهاز، ولذلك لا يتناوله هذا المقال.

تلقّي الإخطار بالتغييرات: الاشتراك وMonitoredItem

لمتابعة تغيّر القيم، يمكن للعميل أن يكرّر Read على فترات ثابتة. وإلى جانب ذلك، يوفّر OPC UA آليّة تترك للخادم مراقبة القيم وتجعله يُخطرك بالتغييرات، وهي الاشتراك (subscription) وMonitoredItem.

MonitoredItem عنصر واحد قيد المراقبة، وله فترة أخذ عيّنات (sampling interval). وفترة أخذ العيّنات هي أسرع معدّل يأخذ به الخادم عيّنات القيم من المصدر، وتصفها المواصفة بأنّها دورة «بأفضل جهد» (best effort)11. وعندما تستوفي العيّنة شروط المرشّح يُنشأ إخطار. وإذا لم يُحدَّد مرشّح، يُرسَل الإخطار عندما تتغيّر القيمة أو الحالة12.

تدخل الإخطارات أوّلاً في طابور MonitoredItem. ويُحدَّد مسبقاً هل يُتخلَّص من الأقدم أم من الأحدث عندما يمتلئ الطابور، وإذا كان حجم الطابور 2 أو أكثر يُضبط في رمز الحالة بتّ Overflow الذي يدلّ على التخلّص13.

يجمع الاشتراك عدّة عناصر MonitoredItem، ويرسل إلى العميل في كل فترة نشر (publishing interval) الإخطارات التي لم تُرسل بعد. وعندما تمرّ دورات دون إخطارات تُرسل، يرسل keep-alive ليدلّ على أنّ الاشتراك ما زال قائماً14.

آليّة الاشتراك وMonitoredItemمخطّط يبيّن أنّ MonitoredItem يأخذ عيّنات قيمة المصدر وفق فترة أخذ العيّنات، ويضع الإخطارات التي تجتاز المرشّح في طابور، وأنّ الاشتراك يرسلها مجتمعة إلى العميل وفق فترة النشر، ويرسل keep-alive إذا لم تكن هناك إخطارات.فترة أخذ العيّناتفترة النشرإن لم تكن هناك إخطاراتالمصدر (قيمة الجهاز)MonitoredItemالمرشّح (الافتراضيّ: تغيّر القيمة أو الحالة)الطابور (Overflow عند الامتلاء)الاشتراكالعميلKeep-alive

الشكل 6: يأخذ MonitoredItem العيّنات ويضعها في طابور، ويرسل الاشتراك الإخطارات مجتمعة إلى العميل.

اشتركت الشيفرة أعلاه في Temperature وPartCount بفترة أخذ عيّنات قدرها 500 ملّي ثانية وحجم طابور 515. في الناتج، تصل درجة الحرارة كل 0.5 ثانية تقريباً، وعدد القطع كل ثانيتين تقريباً. أوّل إخطارين كانا القيمتين اللتين كانتا لدى كل متغيّر عند بدء الاشتراك. و13:40:50.801 لعدد القطع هو الوقت الذي تغيّرت فيه القيمة قبل بدء الاشتراك. ولأنّ الإخطارات تعرض الطابع الزمنيّ للمصدر، فالظاهر هو وقت تغيّر القيمة لا وقت استلام الإخطار.

متى تكرّر القراءة ومتى تستخدم الاشتراك

تكرار القراءة يتيح للعميل أن يحدّد الفترة، والآليّة بسيطة. في المقابل، يتبادل الطرفان طلباً واستجابة في كل مرّة حتى لو لم تتغيّر القيمة، وتفوت التغييرات الأقصر من الفترة. أمّا في الاشتراك فيأخذ الخادم العيّنات ولا يرسل إلّا التغييرات، فلا تزداد حركة الاتصال كثيراً حتى للقيم قليلة التغيّر. ومع ذلك، قد لا يُخطَر بالتغييرات الأقصر من فترة أخذ العيّنات حتى في الاشتراك. فالمواصفة تسمح بأخذ العيّنات أسرع ممّا طُلب، لكنّها لا تفرضه11.

الفرق بين تكرار القراءة والاشتراكمخطّط يبيّن أنّ تكرار القراءة يجعل العميل يحدّد الفترة ويرسل طلباً في كل مرّة، وأنّ الاشتراك يجعل الخادم يأخذ العيّنات وفق فترة أخذ العيّنات ويرسل التغييرات فقط، وأنّ كلتا الطريقتين قد تفوّتان التغييرات الأقصر من الفترة.متابعة تغيّر القيمتكرار القراءةالاشتراكالعميل يحدّد الفترة ويطلب كل مرّةالخادم يأخذ العيّنات ويرسل التغييرات فقطقد تفوت التغييرات الأقصر من الفترة

الشكل 7: في كلتا الطريقتين، قد تفوت التغييرات الأقصر من الفترة.

السماح بالاتصال عبر الشهادات

في اتصال OPC UA تختار درجة حماية الرسائل بواسطة MessageSecurityMode. والقيم هي None بلا حماية، وSign للتوقيع فقط، وSignAndEncrypt للتوقيع والتشفير16. وتحدّد سياسة الأمان (security policy) مجموعة الخوارزميّات المستخدمة في التوقيع والتشفير، واستخدمت في هذا المقال Basic256Sha256.

يُستخدم في التوقيع والتشفير شهادة لكل تطبيق. ويتحقّق العميل والخادم كلّ منهما من شهادة الآخر، ويرفضان الاتصال بطرف لا يثقان به. والخادم المبنيّ بمكتبة OPC Foundation لـ.NET لا يوفّر افتراضياً السياسة None غير المشفّرة4.

توقّف الاتصال الأوّل عند BadCertificateUntrusted

في التشغيل الأوّل لم يكن العميل يثق بشهادة الخادم، فرُفض الاتصال. وهذا جزء من الناتج.

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

تحفظ هذه المكتبة الشهادات المرفوضة في مخزن rejected. وعندما ينقل المسؤول الشهادة من هناك إلى مخزن trusted، تصبح المكتبة تثق بذلك الطرف17. وفي هذه المرّة نقلت شهادة الخادم من pki/rejected/certs إلى pki/trusted/certs، ثم شغّلت العميل مرّة أخرى فتمّ الاتصال. وقبل النقل، تحقّق مع مسؤول الخادم من البصمة (thumbprint)، وهي موجودة أيضاً في اسم الملفّ4.

خطوات الوثوق بالشهادةمخطّط يبيّن أنّ العميل يرفض شهادة الخادم في الاتصال الأوّل ويحفظها في مخزن rejected، وأنّه بعد التحقّق من البصمة ونقلها إلى مخزن trusted يثق بها في الاتصال التالي فيتمّ الاتصال.الاتصال الأوّلBadCertificateUntrustedحفظ الشهادة في مخزن rejectedالتحقّق من البصمة مع مسؤول الخادمالنقل إلى مخزن trustedالثقة بها والاتصال في المرّة التالية

الشكل 8: بعد التحقّق من الشهادة المرفوضة ونقلها إلى trusted، يثق العميل بذلك الطرف.

يوجد أيضاً إعداد اسمه AutoAcceptUntrustedCertificates يقبل الشهادات غير الموثوق بها تلقائياً. قيمته الافتراضيّة false، وتذكر وثائق المكتبة أنّه مفيد في بيئات التطوير، لكن لا ينبغي استخدامه في بيئة الإنتاج17.

وNotAfter=2027-11-01 في السجلّ هو تاريخ انتهاء صلاحيّة شهادة الخادم المحاكى. وهي شهادة موقّعة ذاتياً أُنشئت بدالّة من asyncua، وكان تاريخ انتهائها بعد سنة من يوم إنشائها. ولشهادات الأجهزة أيضاً تاريخ انتهاء، فحدّد ضمن التشغيل خطوات استبدالها قبل انتهاء صلاحيّتها وجعل الطرف الآخر يثق بالجديدة. أمّا التعامل مع مخزن الشهادات في Windows فمشروح في «دليل مخزن شهادات Windows العمليّ».

مصادقة المستخدم

ما تتحقّق منه الشهادات هو التطبيقات بعضها من بعض. وبمعزل عن ذلك، يُمرَّر عند فتح الجلسة رمز (token) يدلّ على المستخدم. وتعرّف المواصفة أربعة أنواع: المجهول، واسم المستخدم وكلمة المرور، وشهادة X.509، والرمز الصادر من جهة خارجيّة18. واتصل العميل في هذا المقال بوصفه مجهولاً. أمّا الرموز التي يقبلها الخادم فموجودة في معلومات نقاط النهاية (endpoints) الخاصّة به19.

أين يتعثّر جمع البيانات في الميدان

من هنا أذكر المشكلات التي يكثر حدوثها في أنظمة جمع بيانات الأجهزة، استناداً إلى هذه التجربة وإلى المواصفة.

القيم التي تقع أثناء توقّف الخادم لا تصل

أثناء الاشتراك أوقفت الخادم، ثم أعدت تشغيله بعد نحو 15 ثانية. وتعيد ManagedSession في هذه المكتبة الاتصال تلقائياً عند انقطاعه20. وطوال مدّة التوقّف ظلّ العميل يكرّر محاولة إعادة الاتصال.

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.

عندما عاد الخادم عادت الإخطارات أيضاً. وهذا جزء من ناتج الاشتراك.

  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]

لم تصل أيّ قيمة بين الساعة 13:41:13 والساعة 13:41:34. وعاد عدد القطع من 78 إلى 2، لأنّ إعادة تشغيل الخادم المحاكى جعلته يبدأ العدّ من 0 من جديد.

في هذا المثال كان الخادم نفسه متوقّفاً، فلم تُحفَظ قيم تلك الفترة في أيّ مكان. تعيد آليّة إعادة الاتصال الاتصالَ، لكنّها لا تملأ لاحقاً قيم الفترة التي كان فيها الخادم متوقّفاً.

ويختلف الوضع إذا انقطع الاتصال وحده واستمرّ الخادم في العمل. ففي المواصفة خدمة Republish تجعل الخادم يعيد إرسال رسائل الإخطار التي لم تصل من طابور إعادة الإرسال الخاصّ به21. وتتراكم الإخطارات أيضاً في طابور MonitoredItem، لكنّ ما يتجاوز حجم الطابور يُتخلَّص منه13. ولم أجرّب هذه الحالة في هذه التجربة. وفي الحالتين، احفظ في السجلّ أوقات الانقطاع وإعادة الاتصال لتعرف الفترة التي لم تصل فيها القيم. وتصميم إعادة الاتصال والسجلّات للأجهزة التي تتواصل تسلسلياً مشروح أيضاً في «مزالق تطبيقات الاتصالات التسلسلية».

ما يُسجَّل عند توقّف الخادممخطّط يبيّن أنّه عندما يتوقّف الخادم أثناء الاشتراك يكرّر العميل محاولة إعادة الاتصال، وتعود الإخطارات عند عودة الخادم، لكنّ قيم فترة التوقّف لا تصل، ولذلك تُسجَّل أوقات الانقطاع وإعادة الاتصال.أثناء الاشتراكيتوقّف الخادميكرّر العميل إعادة الاتصاليعود الخادم وتعود الإخطاراتقيم فترة التوقّف لا تصلتسجيل أوقات الانقطاع وإعادة الاتصال

الشكل 9: تعيد إعادة الاتصال الاتصالَ، لكنّ القيم التي وقعت أثناء توقّف الخادم لا تصل.

أيّ الطابعين الزمنيّين تسجّل

تنصّ المواصفة على أنّ SourceTimestamp هو الوقت الذي أرفقه مصدر البيانات بالقيمة، وينبغي أن يدلّ على وقت تغيّر القيمة. أمّا ServerTimestamp فهو الوقت الذي استلم فيه الخادم القيمة، أو عرف فيه أنّها صحيحة9. والخادم المحاكى في هذا المقال يولّد القيم بنفسه، ولذلك كان الوقتان متطابقين.

إذا أردت أن تعرف متى تغيّرت القيمة في الجهاز فاستخدم SourceTimestamp. لكنّ هذا الوقت يُختم بساعة المصدر، فإذا انحرفت ساعة الجهاز أو الخادم انحرف السجلّ أيضاً. وإذا كنت ستطابق البيانات المجموعة مع سجلّات أخرى، فاحفظ بجانبها وقت الحاسوب الذي استلمها، لتتمكّن لاحقاً من التحقّق من الانحراف. أمّا مزامنة الوقت في جهة Windows فمشروحة في «Windows Time Synchronization (w32time) and Business Systems».

لا تعامل القيم التي حالتها ليست Good كأرقام عاديّة

عندما يكون رمز الحالة Bad تُهمَل القيمة9. وعندما يكون Uncertain فقد لا تصلح القيمة لبعض الاستخدامات10. وقبل إدخال القيم في الرسوم البيانيّة أو الإحصاءات، صنّفها حسب رمز الحالة، وحدّد كيف تتعامل مع القيم التي ليست Good. ويُضبط بتّ Overflow عند امتلاء الطابور داخل رمز الحالة أيضاً13. أمّا كيف تعرض حالة الجهاز على الشاشة فمشروح في «أفضل الممارسات للتحقّق من حالة الأجهزة الخارجيّة وعرضها».

افتح المنفذ في جدار الحماية

منفذ OPC UA المسجّل لدى IANA هو 4840/TCP3. لكن ليست كل الخوادم تستخدم هذا الرقم. فأمثلة OPC Foundation لـ.NET تستخدم منافذ مثل 625414. تحقّق من رقم المنفذ في عنوان URL لنقطة نهاية الجهاز، واسمح بالاتصال بذلك المنفذ. إذا كان الخادم يعمل على Windows فاسمح به في جدار حماية Windows على ذلك الحاسوب، وإذا كانت بينهما أجهزة شبكة فاسمح به عليها. أمّا طريقة إنشاء القواعد الواردة فمشروحة في «جدار حماية Windows وتطبيقات الأعمال».

أين يتعثّر جمع البياناتمخطّط يبيّن مواضع تعثّر جمع بيانات الأجهزة مقسّمة إلى محتوى السجلّات (عدم وصول القيم أثناء توقّف الخادم، وأيّ الطابعين الزمنيّين يُسجَّل، والتعامل مع القيم التي ليست Good) وتهيئة الاتصال (منافذ جدار الحماية، وانتهاء صلاحيّة الشهادات).أين يتعثّر جمع البياناتمحتوى السجلّاتتهيئة الاتصالقيم فترة التوقّف لا تصلأيّ الطابعين الزمنيّينالتعامل مع القيم غير Goodمنافذ جدار الحمايةانتهاء صلاحيّة الشهادات

الشكل 10: حتى بعد نجاح الاتصال، تحقّق من فجوات السجلّات والأوقات والحالة والمنافذ والشهادات.

متى تستخدم OPC UA أو Modbus/TCP أو بروتوكول الشركة المصنّعة

للاتصال بوحدات PLC والأجهزة، يوجد إلى جانب OPC UA بروتوكول Modbus/TCP والبروتوكولات التي توفّرها الشركات المصنّعة لوحدات PLC.

تتعامل مواصفة Modbus مع البيانات في أربعة جداول: Discrete Input، وهي بتّات مفردة للقراءة فقط؛ وCoil، وهي بتّات مفردة للقراءة والكتابة؛ وInput Register، وهي كلمات بطول 16 بتّاً للقراءة فقط؛ وHolding Register، وهي كلمات بطول 16 بتّاً للقراءة والكتابة22. وتسجّل IANA المنفذ 502/TCP بروتوكولاً تطبيقياً لـModbus ‏(mbap)3.

ما يدخل في هذه الجداول بتّات وكلمات بطول 16 بتّاً، ولذلك يجب التحقّق في وثائق الجهاز من أيّ قيمة توجد في أيّ عنوان وبأيّ صيغة، ثم كتابة ذلك في جهة العميل. أمّا في OPC UA، فكما رأينا، يعرض الخادم متغيّرات لها أسماء في فضاء العناوين، ويستطيع العميل اكتشافها عبر Browse. وترافق القيمَ رموزُ الحالة والطوابعُ الزمنيّة، ويُسمح بالاتصال عبر الشهادات.

وكثيراً ما يتحدّد الاختيار بما يوفّره الجهاز. فإذا كان لدى الجهاز خادم OPC UA فاستخدمه، وإذا لم يكن لديه إلّا Modbus/TCP فأنشئ جدول مطابقة للعناوين واقرأ وفقه. وإذا لم يكن هناك إلّا بروتوكول الشركة المصنّعة أو حزمة SDK الخاصّة بها، فاتّبع وثائقها. أمّا اختيار طريقة الاتصال بأجهزة USB فمشروح في «How to Work with USB Devices from a Windows App».

اختيار طريقة الاتصال بالجهازمخطّط يبيّن أنّه إذا كان لدى الجهاز خادم OPC UA تُستعرض المتغيّرات وتُجمع عبر OPC UA، وإذا لم يكن لديه إلّا Modbus/TCP يُتحقّق من العناوين والصيغ في الوثائق وتُقرأ السجلّات، وإذا لم يكن لديه إلّا بروتوكول الشركة المصنّعة تُتّبع وثائقها.خادم OPC UAModbus/TCP فقطبروتوكول الشركة المصنّعة فقطالاتصال الذي يوفّره الجهازما المتاح؟استعراض المتغيّرات وجمعهاالتحقّق من العناوين والصيغ في الوثائقاتّباع وثائق الشركة المصنّعة وSDKقراءة السجلّات وتحويلها إلى قيم

الشكل 11: كثيراً ما يتحدّد ما تجمع به البيانات بما يوفّره الجهاز.

مقالات ذات صلة

روابط مرجعيّة

  1. OPC Foundation, Classic. أنّ OPC Classic مبنيّ على COM/DCOM، ومواصفات DA وA&E وHDA، ونشر OPC UA عام 2008، وأسباب إعادة البناء. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. الجزء الأوّل من سلسلة IEC 62541، ويتناول مفاهيم OPC UA ونظرة عامّة عليه (حلّت محلّه طبعة لاحقة). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. تسجيل opcua-tcp على 4840/TCP، وmbap على 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. طريقة كتابة عميل لإصدارات 2.0، وأنّ ReadValueAsync ترمي استثناءً عند Bad، ومخزنا rejected وtrusted، وأنّ الخوادم لا توفّر None افتراضياً، وأرقام المنافذ في الأمثلة. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). مكتبة OPC UA للغة Python التي استُخدمت لخادم الجهاز المحاكى. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. الخدمة التي تتتبّع مراجع العقدة. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. أنّ URI فضاء الأسماء يُمثَّل داخل الخدمات برقم، وأنّ هذا الرقم يقابل الموضع في NamespaceArray. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. خدمتا Read وWrite. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. تعريفات القيمة ورمز الحالة وSourceTimestamp وServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good وUncertain وBad كما يدلّ عليها البتّان الأعليان. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. أنّ فترة أخذ العيّنات دورة بأفضل جهد. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. أنّ الإخطار يُرسَل عند تغيّر القيمة أو الحالة إذا لم يكن هناك مرشّح. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. حجم الطابور، وطريقة التخلّص، وبتّ Overflow. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. فترة النشر وkeep-alive. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync، وفترة أخذ العيّنات وحجم الطابور في MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. معاني None وSign وSignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. ممارسة نقل الشهادات من مخزن rejected إلى المخزن الموثوق، وعدم استخدام AutoAcceptUntrustedCertificates في بيئة الإنتاج. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. الأنواع الأربعة: المجهول، واسم المستخدم، وX.509، والرمز الصادر. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. قائمة رموز المستخدمين التي تقبلها نقطة النهاية (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. أنّ ManagedSession تتضمّن آليّة مدمجة لإعادة الاتصال. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. الخدمة التي تجعل رسائل الإخطار تُرسل مرّة أخرى من طابور إعادة الإرسال في الاشتراك. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (نسخة موضوعة على خادم تابع لجامعة شيكاغو). نموذج البيانات في القسم 4.3 (Discrete Input وCoil وInput Register وHolding Register). ↩

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

صفحات مواضيع قريبة من موضوع هذه المقالة. وانطلاقًا من المقالة يمكنكم الانتقال إلى الخدمات ذات الصلة وإلى مقالات أخرى.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة نتلقّاها عند الاستشارة حول موضوع هذه المقالة.

ما الفرق بين OPC UA وOPC Classic (OPC DA)؟
OPC Classic مجموعة مواصفات تتبادل البيانات عبر COM/DCOM في Windows. أمّا OPC UA الذي نُشر عام 2008 فلا يعتمد على نظام تشغيل بعينه، ويجمع وظائف Classic في مواصفة واحدة. تعمل عملاء OPC UA خارج Windows أيضاً، ولا تحتاج إلى إعداد DCOM.
عند جمع القيم عبر OPC UA، هل تكفي قراءتها على فترات ثابتة؟
إذا كانت القيم قليلة والفترة طويلة فالقراءة تكفي. ولمتابعة تغيّر القيم يمكن استخدام الاشتراك: تترك أخذ العيّنات للخادم، فيُخطرك بالتغييرات. وفي الحالتين، سجّل رمز الحالة والطوابع الزمنيّة التي تعود مع كل قيمة.
لماذا يظهر BadCertificateUntrusted عند الاتصال؟
لأنّ العميل لا يثق بعدُ بشهادة الخادم. تحفظ مكتبة OPC Foundation لـ.NET الشهادات المرفوضة في مخزن rejected. تحقّق من بصمة الشهادة مع مسؤول الخادم، ثم انقلها إلى مخزن trusted. اقصر الإعداد الذي يقبل الشهادات تلقائياً على بيئات التطوير.
ما رقم المنفذ الذي يستخدمه OPC UA؟
تسجّل IANA المنفذ 4840/TCP باسم opcua-tcp. لكنّ الخوادم الفعليّة تستخدم أحياناً منفذاً آخر، فتحقّق منه في إعدادات الجهاز. إذا كان الخادم يعمل على Windows فيلزم السماح بالاتصال بذلك المنفذ في جدار حماية Windows، وإذا كانت بينهما أجهزة شبكة فعليها أيضاً.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

ممثل شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة