История изменений (первая версия, опубликована 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.
flowchart TB
accTitle: Чем OPC Classic отличается от OPC UA
accDescr: Схема показывает, что OPC Classic построен на COM и DCOM Windows и разделён на отдельные спецификации DA, A&E и HDA, а OPC UA объединяет их функции в одной спецификации, не зависящей от платформы.
C["OPC Classic"] --> D["Работает поверх COM/DCOM (Windows)"]
D --> DA["Отдельные спецификации DA, A&E, HDA"]
DA -->|"Объединяет функции"| U["OPC UA (2008)"]
U --> P["Не зависит от конкретной платформы"]
P --> S["Стандартизирован как 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.
flowchart TB
accTitle: Проверенная конфигурация
accDescr: Схема конфигурации, в которой клиент на C# через библиотеку .NET от OPC Foundation подключается по зашифрованному соединению к имитации сервера оборудования на asyncua, ожидающей подключений на порту 4840.
A["Клиент на C# (LineCollector)"] -->|"opc.tcp, SignAndEncrypt"| B["Имитация сервера оборудования (asyncua)"]
A -.-> L["Библиотека .NET от OPC Foundation 2.0.1"]
B -.-> V["Четыре переменные под 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 по иерархическим ссылкам и нашёл четыре переменные.
flowchart TB
accTitle: Адресное пространство имитации оборудования
accDescr: Схема показывает, что под папкой Objects находится объект Line1, а под ним четыре переменные Temperature, PartCount, Running и Setpoint.
O["Objects"] --> L["Line1 (объект)"]
L --> T["Temperature (переменная)"]
L --> P["PartCount (переменная)"]
L --> R["Running (переменная)"]
L --> S["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.
flowchart TB
accTitle: Как из URI пространства имён получается NodeId
accDescr: Схема показывает, что клиент ищет URI пространства имён в NamespaceArray сервера, получает индекс и составляет NodeId из этого индекса и идентификатора.
U["URI пространства имён"] --> A["NamespaceArray сервера"]
A --> I["Индекс (в этом примере 2)"]
I --> N["NodeId ns=2;s=Line1.Temperature"]
K["Идентификатор Line1.Temperature"] --> N
Рис. 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, потом можно будет отличить.
flowchart TB
accTitle: Содержимое DataValue
accDescr: Схема показывает, что DataValue, возвращаемый при чтении и в уведомлениях, содержит четыре элемента: значение, код состояния, метку времени источника и метку времени сервера, и что при коде состояния Bad значение игнорируется.
D["DataValue"] --> V["Значение"]
D --> S["Код состояния"]
D --> TS["SourceTimestamp (время источника)"]
D --> TV["ServerTimestamp (время сервера)"]
S -->|"если Bad"| X["Игнорировать значение"]
Рис. 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.
flowchart TB
accTitle: Как работают подписка и MonitoredItem
accDescr: Схема показывает, что MonitoredItem делает выборку значений источника с интервалом выборки, ставит прошедшие фильтр уведомления в очередь, подписка отправляет их клиенту пакетом с интервалом публикации, а если уведомлений нет, отправляет keep-alive.
S["Источник (значения оборудования)"] --> M["MonitoredItem"]
M -->|"Интервал выборки"| F["Фильтр (по умолчанию изменение значения или состояния)"]
F --> Q["Очередь (при переполнении Overflow)"]
Q --> SUB["Подписка"]
SUB -->|"Интервал публикации"| C["Клиент"]
SUB -.->|"если уведомлений нет"| K["Keep-alive"]
Рис. 6: MonitoredItem делает выборку и накапливает в очереди, а подписка отправляет клиенту всё вместе.
В коде выше я подписался на Temperature и PartCount с интервалом выборки 500 миллисекунд и размером очереди 515. В выводе температура приходит примерно каждые 0,5 секунды, количество изделий — примерно каждые 2 секунды. Первые две записи — значения, которые были у этих переменных в момент начала подписки. 13:40:50.801 у количества изделий — это время, когда значение изменилось ещё до начала подписки. Для уведомлений выводится метка времени источника, поэтому показано не время получения уведомления, а время изменения значения.
Когда повторять чтение, а когда подписываться
При повторном чтении интервал определяет клиент, и механизм прост. Зато запрос и ответ передаются каждый раз, даже если значение не изменилось, а изменения короче интервала пропускаются. При подписке сервер делает выборку и отправляет только изменения, поэтому даже для редко меняющихся значений трафик почти не растёт. Однако и при подписке об изменениях короче интервала выборки уведомление может не прийти. Спецификация разрешает делать выборку быстрее заданного, но не требует этого11.
flowchart TB
accTitle: Разница между повторным чтением и подпиской
accDescr: Схема показывает, что при повторном чтении клиент определяет интервал и каждый раз отправляет запрос, при подписке сервер делает выборку с интервалом выборки и отправляет только изменения, и что в обоих случаях изменения короче интервала могут быть пропущены.
A["Отслеживать изменения значения"] --> R["Повторять чтение"]
A --> S["Подписаться"]
R --> R1["Клиент определяет интервал и запрашивает каждый раз"]
S --> S1["Сервер делает выборку и отправляет только изменения"]
R1 --> X["Изменения короче интервала могут быть пропущены"]
S1 --> X
Рис. 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.
flowchart TB
accTitle: Путь к доверию сертификату
accDescr: Схема показывает, что при первом подключении клиент отклоняет сертификат сервера и сохраняет его в хранилище rejected, а после сверки отпечатка и переноса в хранилище trusted при следующем подключении доверяет ему и подключается.
A["Первое подключение"] --> B["BadCertificateUntrusted"]
B --> C["Сертификат сохранён в хранилище rejected"]
C --> D["Сверить отпечаток с администратором сервера"]
D --> E["Перенести в хранилище trusted"]
E --> F["Следующее подключение: доверие и подключение"]
Рис. 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. Этот случай в эксперименте я не проверял. В обоих случаях записывайте время разрыва и переподключения, чтобы было видно, за какой период значения не приходили. Переподключение и проектирование журналов для оборудования с последовательной связью я разбираю и в статье «Подводные камни приложений последовательной связи».
flowchart TB
accTitle: Записи при остановке сервера
accDescr: Схема показывает, что если во время подписки сервер останавливается, клиент раз за разом пытается переподключиться, когда сервер возвращается, возвращаются и уведомления, но значения за время остановки не приходят, поэтому время разрыва и переподключения записывают в журнал.
A["Во время подписки"] --> B["Сервер останавливается"]
B --> C["Клиент раз за разом пытается переподключиться"]
C --> D["Сервер возвращается, уведомления тоже"]
B -.-> E["Значения за время остановки не приходят"]
E -.-> F["Записать время разрыва и переподключения"]
Рис. 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 и бизнес-приложения».
flowchart TB
accTitle: Где спотыкается сбор данных
accDescr: Схема делит места, где спотыкается сбор данных с оборудования, на содержимое записей (значения за время остановки сервера не приходят, какую метку времени записывать, как обращаться со значениями, отличными от Good) и подготовку подключения (порт в брандмауэре, срок действия сертификатов).
A["Где спотыкается сбор данных"] --> R["Содержимое записей"]
A --> N["Подготовка подключения"]
R --> B["Значения за время остановки не приходят"]
B ~~~ C["Какая метка времени"]
C ~~~ D["Значения, отличные от Good"]
N --> E["Порт в брандмауэре"]
E ~~~ F["Срок действия сертификатов"]
Рис. 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».
flowchart TB
accTitle: Как выбрать способ связи с оборудованием
accDescr: Схема показывает, что если у оборудования есть сервер OPC UA, переменные просматривают и собирают через OPC UA, если есть только Modbus/TCP, адреса и форматы уточняют по документации и читают регистры, а если есть только протокол производителя, следуют его документации.
A["Связь, которую предоставляет оборудование"] --> B{"Что есть?"}
B -->|"Сервер OPC UA"| C["Просмотреть и собрать переменные"]
B -->|"Только Modbus/TCP"| D["Уточнить адреса и форматы по документации"]
B -->|"Только протокол производителя"| E["Следовать документации и SDK производителя"]
D --> F["Прочитать регистры и пересчитать в значения"]
Рис. 11: Чем собирать данные, чаще всего определяется тем, что предоставляет оборудование.
Похожие статьи
- Dropped Frames with GigE Vision and USB3 Vision Cameras — Locating the Cause and the Windows Settings to Review
- Подводные камни приложений последовательной связи: переподключение и логи
- Лучшие практики проверки и отображения состояния внешних устройств
- How to Work with USB Devices from a Windows App
- Брандмауэр Windows и бизнес-приложения
- Хранилище сертификатов Windows на практике
- Windows Time Synchronization (w32time) and Business Systems
Справочные ссылки
-
OPC Foundation, Classic. То, что OPC Classic основан на COM/DCOM, спецификации DA, A&E и HDA, публикация OPC UA в 2008 году и причины переработки. ↩ ↩2
-
IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Часть 1 серии IEC 62541, посвящённая понятиям и обзору OPC UA (заменена более новой редакцией). ↩
-
IANA, Service Name and Transport Protocol Port Number Registry. Регистрация
opcua-tcpна 4840/TCP иmbapна 502/TCP. ↩ ↩2 ↩3 -
OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Как писать клиент линейки 2.0, то, что
ReadValueAsyncпри Bad выбрасывает исключение, хранилища отклонённых и доверенных сертификатов, то, что серверы по умолчанию не предлагаютNone, и номера портов в примерах. ↩ ↩2 ↩3 ↩4 ↩5 -
FreeOpcUa, asyncua (PyPI). Библиотека OPC UA для Python, на которой построена имитация сервера оборудования. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Сервис, проходящий по ссылкам узла. ↩
-
OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. То, что внутри сервисов URI пространств имён представляются числами и это число соответствует позиции в NamespaceArray. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Сервисы Read и Write. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Определения значения, кода состояния, SourceTimestamp и ServerTimestamp. ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain и Bad, обозначаемые двумя старшими битами. ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. То, что интервал выборки — цикл «по возможности» (best effort). ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. То, что без фильтра уведомление отправляется при изменении значения или состояния. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. Размер очереди, правило отбрасывания и бит Overflow. ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. Интервал публикации и keep-alive. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set.
SubscribeDataChangesAsync, а также интервал выборки и размер очереди вMonitoredItemOptions. ↩ -
OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Значения None, Sign и SignAndEncrypt. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Certificates. Перенос сертификатов из хранилища rejected в доверенное хранилище и то, что
AutoAcceptUntrustedCertificatesне следует использовать в рабочей среде. ↩ ↩2 -
OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Четыре вида: анонимный, имя пользователя, X.509 и выданный токен. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. Список пользовательских токенов, которые принимает конечная точка (userIdentityTokens). ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. То, что в
ManagedSessionвстроен механизм переподключения. ↩ -
OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Сервис, позволяющий запросить повторную отправку сообщений уведомлений из очереди повторной передачи подписки. ↩
-
Modbus-IDA, MODBUS Application Protocol Specification V1.1b (копия на сервере Чикагского университета). Модель данных в разделе 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Почему ломаются аргументы ── правила аргументов командной строки Windows
В Windows массива аргументов нет: в CreateProcess уходит одна строка, делит её принимающая сторона. Правила деления CommandLineToArgvW, C...
Практические рекомендации по многопоточности: .NET — что решить до добавления потоков
Проверенные приёмы проектирования на .NET/C#, чтобы код не «иногда падал или зависал»: не создавать потоки вручную и опираться на Task, с...
WMI/CIM из C# и PowerShell — практическое руководство по сведениям об оборудовании, мониторингу процессов и удалённым запросам
WMI/CIM — стандартный способ получить серийный номер ПК, следить за свободным местом на диске и ловить запуск процессов. Разбираем команд...
Глубины ввода-вывода Windows (часть 4) — диспетчер кэша: когда WriteFile оказывается на диске
Четвёртая часть серии со схемами диспетчера кэша Windows. Разбираем кэш как проекцию файла, упреждающее чтение и отложенную запись, когда...
Глубины ввода-вывода Windows (часть 3) — порты завершения ввода-вывода (IOCP) и пул потоков .NET: подвал под async/await
Третья часть цикла статей со схемами о портах завершения ввода-вывода (IOCP). Разбираются очередь вместе с управлением числом потоков, зн...
Связанные темы
Страницы тем, близких к этой. Начав со статьи, можно перейти к связанным услугам и другим статьям.
Технические темы Windows
Входная страница с техническими темами о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Эта статья ведёт к следующим страницам услуг. Начните с ближайшей к вашей задаче.
Разработка приложений для Windows
Приложение Windows, которое собирает данные с оборудования, включая настройку подписок, переподключение и запись меток времени и состояния, можно реализовать в рамках разработки приложений для Windows.
Технические консультации и ревью дизайна
Если нужно решить, собирать ли данные через OPC UA или через Modbus/TCP либо протокол производителя и как управлять сертификатами, это можно разобрать в рамках технической консультации и ревью проекта.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Чем 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, техническими консультациями и поиском причин неисправностей; сильные стороны — проекты, где остаются существующие активы, и расследование сбоев, причину которых трудно увидеть.