Введение в OPC UA ── сбор данных с ПЛК и оборудования в приложении Windows

· · Windows, OPC UA, Производство, ПЛК, C#, .NET, Связь, Сертификаты

Опубликовать в X Поделиться ВКонтакте Поделиться в Telegram
История изменений (первая версия, опубликована 1 Nov 2026)
Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.23968392)

Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.

Го Комура (2026). Введение в OPC UA ── сбор данных с ПЛК и оборудования в приложении Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968392 https://comcomponent.com/ru/blog/opc-ua-windows-data-collection/

DOI (последняя версия)
10.5281/zenodo.23968392
DOI (эта версия)
10.5281/zenodo.23968393

Если при сборе данных с ПЛК и оборудования в приложение Windows у оборудования есть сервер OPC UA, иногда можно обойтись без отдельной реализации связи для каждого производителя. Но просто подключиться и читать значения недостаточно. Если не знать о состоянии и метках времени, которые возвращаются вместе со значением, о подписках, через которые приходят уведомления об изменениях, о разрешении подключения по сертификатам и о восстановлении после разрыва, собранным данным потом нельзя будет доверять.

В этой статье я объясняю те части OPC UA, которые нужны для сбора данных, вместе с результатами, полученными на реально запущенном клиенте на C#. Для клиента я использовал библиотеку .NET от OPC Foundation, а в роли второй стороны — имитацию сервера оборудования. Работу с конкретными ПЛК или оборудованием я не проверял. Настройки конкретного оборудования уточняйте в его документации.

О сборе изображений с камер 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.

В 2008 году OPC Foundation опубликовала 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# через библиотеку .NET от OPC Foundation подключается по зашифрованному соединению к имитации сервера оборудования на asyncua, ожидающей подключений на порту 4840.opc.tcp, SignAndEncryptКлиент на C# (LineCollector)Имитация сервера оборудования (asyncua)Библиотека .NET от OPC Foundation 2.0.1Четыре переменные под Line1

Рис. 2: На одном и том же ПК клиент на C# подключался к имитации сервера оборудования.

Имитация сервера оборудования

Вместо оборудования я сделал небольшой сервер на asyncua5. Под объектом Line1 находятся четыре переменные: температура (Temperature), количество изделий (PartCount), признак работы (Running) и уставка (Setpoint). Температура меняется каждые 0,5 секунды, количество изделий — каждые 2 секунды. Для безопасности я предусмотрел два варианта: без шифрования (None) и с подписью и шифрованием (SignAndEncrypt с Basic256Sha256).

# Имитация оборудования для проверки работы. Не заменяет реальное оборудование или ПЛК.
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>

Клиент по порядку выполняет подключение, обзор (Browse), чтение, запись и подписку. Смысл каждой части я объясняю по порядку в следующих разделах.

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

        // Обзор: перечисляем переменные под 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]}");

        // Подписка: просим сервер делать выборку каждые 500 мс и уведомлять нас об изменениях
        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 публикует имеющиеся у него данные как набор узлов. Этот набор называется адресным пространством. Узлы бывают разных видов, например объекты, представляющие папки или оборудование, и переменные, у которых есть значения, а между собой узлы связаны ссылками. Клиент проходит по ссылкам с помощью сервиса Browse и выясняет, какие узлы существуют6.

В выводе выше клиент прошёл от Line1 по иерархическим ссылкам и нашёл четыре переменные.

Адресное пространство имитации оборудованияСхема показывает, что под папкой Objects находится объект Line1, а под ним четыре переменные Temperature, PartCount, Running и Setpoint.ObjectsLine1 (объект)Temperature (переменная)PartCount (переменная)Running (переменная)Setpoint (переменная, доступна для записи)

Рис. 3: Адресное пространство имитации оборудования. Клиент находит узлы, проходя по ссылкам с помощью Browse.

NodeId состоит из пространства имён и идентификатора

Узлы идентифицируются по NodeId. ns=2;s=Line1.Temperature в выводе означает, что индекс пространства имён равен 2, а идентификатор — строка Line1.Temperature.

По сути пространство имён — это URI. Спецификация определяет, что внутри сервисов URI пространств имён представляются числами и что это число соответствует позиции в NamespaceArray сервера. Индекс 0 — это пространство имён OPC UA http://opcfoundation.org/UA/7.

Поэтому число индекса не прописывают в коде, а находят индекс по URI. В коде выше session.NamespaceUris.GetIndex(LineNamespace) по URI получает 2, и из него строятся NodeId.

Как из URI пространства имён получается NodeIdСхема показывает, что клиент ищет 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 этой библиотеки при состоянии Bad выбрасывает исключение4. Если механизм записи значений сохраняет код состояния рядом со значением, то значения, записанные во время состояния Bad, потом можно будет отличить.

Содержимое DataValueСхема показывает, что DataValue, возвращаемый при чтении и в уведомлениях, содержит четыре элемента: значение, код состояния, метку времени источника и метку времени сервера, и что при коде состояния Bad значение игнорируется.если BadDataValueЗначениеКод состоянияSourceTimestamp (время источника)ServerTimestamp (время сервера)Игнорировать значение

Рис. 5: Результат чтения содержит не только значение, но и код состояния и две метки времени.

При записи я записал в Setpoint значение 32.5, и в качестве кода состояния результата вернулось Good. Доступна ли переменная для записи, решает сервер. В этой имитации сервера для записи доступна только Setpoint. Разрешать ли менять уставки оборудования извне — вопрос проектирования безопасности оборудования, и в этой статье он не рассматривается.

Получать уведомления об изменениях: подписки и MonitoredItem

Чтобы отслеживать изменения значений, клиент может повторять Read с фиксированным интервалом. Помимо этого в OPC UA есть механизм, который поручает наблюдение за значениями серверу и получает от него уведомления об изменениях. Это подписки (Subscription) и MonitoredItem.

MonitoredItem — это один наблюдаемый объект, и у него есть интервал выборки. Интервал выборки — самый быстрый интервал, с которым сервер делает выборку значений из источника, и в спецификации он описан как цикл «по возможности» (best effort)11. Когда выборка удовлетворяет условию фильтра, создаётся уведомление. Если фильтр не задан, уведомление приходит при изменении значения или состояния12.

Уведомления сначала попадают в очередь MonitoredItem. На случай переполнения очереди заранее определяют, отбрасывать старые или новые уведомления, и если размер очереди 2 или больше, в коде состояния устанавливается бит Overflow, показывающий, что уведомления были отброшены13.

Подписка объединяет несколько MonitoredItem и в каждом интервале публикации отправляет клиенту ещё не отправленные уведомления. Если несколько циклов подряд отправлять нечего, она отправляет сообщения keep-alive, сообщающие, что подписка жива14.

Как работают подписка и MonitoredItemСхема показывает, что MonitoredItem делает выборку значений источника с интервалом выборки, ставит прошедшие фильтр уведомления в очередь, подписка отправляет их клиенту пакетом с интервалом публикации, а если уведомлений нет, отправляет keep-alive.Интервал выборкиИнтервал публикацииесли уведомлений нетИсточник (значения оборудования)MonitoredItemФильтр (по умолчанию изменение значения или состояния)Очередь (при переполнении Overflow)ПодпискаКлиентKeep-alive

Рис. 6: MonitoredItem делает выборку и накапливает в очереди, а подписка отправляет клиенту всё вместе.

В коде выше я подписался на Temperature и PartCount с интервалом выборки 500 миллисекунд и размером очереди 515. В выводе температура приходит примерно каждые 0,5 секунды, количество изделий — примерно каждые 2 секунды. Первые две записи — значения, которые были у этих переменных в момент начала подписки. 13:40:50.801 у количества изделий — это время, когда значение изменилось ещё до начала подписки. Для уведомлений выводится метка времени источника, поэтому показано не время получения уведомления, а время изменения значения.

Когда повторять чтение, а когда подписываться

При повторном чтении интервал определяет клиент, и механизм прост. Зато запрос и ответ передаются каждый раз, даже если значение не изменилось, а изменения короче интервала пропускаются. При подписке сервер делает выборку и отправляет только изменения, поэтому даже для редко меняющихся значений трафик почти не растёт. Однако и при подписке об изменениях короче интервала выборки уведомление может не прийти. Спецификация разрешает делать выборку быстрее заданного, но не требует этого11.

Разница между повторным чтением и подпискойСхема показывает, что при повторном чтении клиент определяет интервал и каждый раз отправляет запрос, при подписке сервер делает выборку с интервалом выборки и отправляет только изменения, и что в обоих случаях изменения короче интервала могут быть пропущены.Отслеживать изменения значенияПовторять чтениеПодписатьсяКлиент определяет интервал и запрашивает каждый разСервер делает выборку и отправляет только измененияИзменения короче интервала могут быть пропущены

Рис. 7: При любом способе изменения короче интервала могут быть пропущены.

Разрешение подключений по сертификатам

При подключении OPC UA степень защиты сообщений выбирают с помощью MessageSecurityMode. Значения: None без защиты, Sign только с подписью и SignAndEncrypt с подписью и шифрованием16. Сочетание алгоритмов подписи и шифрования задаёт политика безопасности; в этой статье я использовал Basic256Sha256.

Для подписи и шифрования используется собственный сертификат каждого приложения. Клиент и сервер проверяют сертификаты друг друга и отклоняют подключения от сторон, которым не доверяют. Сервер, созданный на библиотеке .NET от OPC Foundation, по умолчанию не предлагает политику 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, запустил клиент снова и смог подключиться. Перед переносом сверьте с администратором сервера отпечаток, который входит и в имя файла4.

Путь к доверию сертификатуСхема показывает, что при первом подключении клиент отклоняет сертификат сервера и сохраняет его в хранилище rejected, а после сверки отпечатка и переноса в хранилище trusted при следующем подключении доверяет ему и подключается.Первое подключениеBadCertificateUntrustedСертификат сохранён в хранилище rejectedСверить отпечаток с администратором сервераПеренести в хранилище trustedСледующее подключение: доверие и подключение

Рис. 8: Если проверить отклонённый сертификат и перенести его в trusted, клиент будет доверять этой стороне.

Есть и настройка AutoAcceptUntrustedCertificates, которая автоматически принимает недоверенные сертификаты. По умолчанию она равна false, и документация библиотеки пишет, что она полезна в среде разработки, но в рабочей среде её использовать не следует17.

NotAfter=2027-11-01 в журнале — срок действия сертификата имитации сервера. Это самоподписанный сертификат, созданный функцией asyncua, и срок его действия истекал через год после создания. У сертификатов оборудования тоже есть срок действия, поэтому заранее определите в эксплуатации порядок, по которому их заменяют до истечения срока и делают доверенными для второй стороны. Работу с хранилищем сертификатов Windows я описываю в статье «Хранилище сертификатов Windows на практике».

Аутентификация пользователя

Сертификаты проверяют приложения друг у друга. Отдельно от этого при открытии сеанса передаётся токен, указывающий пользователя. В спецификации есть четыре вида: анонимный, имя пользователя и пароль, сертификат X.509 и токен, выданный внешней стороной18. Клиент в этом примере подключался анонимно. Какие токены принимает сервер, указано в сведениях о конечных точках сервера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. Как показывать состояние оборудования на экране, я разбираю в статье «Лучшие практики проверки и отображения состояния внешних устройств».

Открыть порт в брандмауэре

Зарегистрированный в IANA порт OPC UA — 4840/TCP3. Однако не все серверы используют этот номер. Примеры .NET от OPC Foundation используют, например, порт 625414. Проверьте номер порта в URL конечной точки оборудования и разрешите подключения к этому порту. Если сервер работает под Windows, это делается в брандмауэре Windows этого компьютера, а если между ними есть сетевое оборудование — на этом оборудовании. Как создавать правила для входящих подключений, я описываю в статье «Брандмауэр Windows и бизнес-приложения».

Где спотыкается сбор данныхСхема делит места, где спотыкается сбор данных с оборудования, на содержимое записей (значения за время остановки сервера не приходят, какую метку времени записывать, как обращаться со значениями, отличными от Good) и подготовку подключения (порт в брандмауэре, срок действия сертификатов).Где спотыкается сбор данныхСодержимое записейПодготовка подключенияЗначения за время остановки не приходятКакая метка времениЗначения, отличные от GoodПорт в брандмауэреСрок действия сертификатов

Рис. 10: Даже после того, как подключение установлено, проверяйте пропуски в записях, время, состояние, порты и сертификаты.

Когда использовать Modbus/TCP или протоколы производителя

Для связи с ПЛК и оборудованием помимо OPC UA есть Modbus/TCP и протоколы, которые предоставляет производитель ПЛК.

Спецификация Modbus представляет данные в четырёх таблицах: однобитовые Discrete Input только для чтения, однобитовые Coil для чтения и записи, 16-битные Input Register только для чтения и 16-битные Holding Register для чтения и записи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 UAТолько Modbus/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. Часть 1 серии 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 выбрасывает исключение, хранилища отклонённых и доверенных сертификатов, то, что серверы по умолчанию не предлагают 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. То, что интервал выборки — цикл «по возможности» (best effort). ↩ ↩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?
Потому что клиент ещё не доверяет сертификату сервера. Библиотека .NET от OPC Foundation сохраняет отклонённые сертификаты в хранилище rejected. Сверьте отпечаток сертификата с администратором сервера и только потом перенесите его в хранилище trusted. Настройки, автоматически принимающие сертификаты, допустимы только в среде разработки.
Какой номер порта у OPC UA?
В IANA порт 4840/TCP зарегистрирован как opcua-tcp. Однако реальные серверы иногда используют другой порт, поэтому проверьте настройки оборудования. Если сервер работает под Windows, соединения с этим портом нужно разрешить в брандмауэре Windows, а если между ними есть сетевое оборудование — то и на нём.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Работает в основном с разработкой ПО для Windows, техническими консультациями и поиском причин неисправностей; сильные стороны — проекты, где остаются существующие активы, и расследование сбоев, причину которых трудно увидеть.

Публичные ссылки

Вернуться в блог