Історія змін (лише перша версія, опубліковано 01.11.2026)
- Перша публікація
Цитувати цю статтю(DOI: 10.5281/zenodo.23968433)
Цю статтю заархівовано в Zenodo. Нижче наведено обидва DOI: той, що завжди веде до останньої версії, і той, що закріплений за версією, яку ви зараз читаєте.
Ґо Комура (2026). «Вступ до OPC UA ── Збір даних із PLC і обладнання в застосунку для Windows». KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968433 https://comcomponent.com/uk/blog/opc-ua-windows-data-collection/
- DOI (остання версія)
- 10.5281/zenodo.23968433
- DOI (ця версія)
- 10.5281/zenodo.23968434
Коли ви збираєте дані з PLC або обладнання в застосунок для Windows і обладнання має сервер OPC UA, можливо, вдасться обійтися без окремого протоколу для кожного виробника. Однак підключитися й читати значення недостатньо. Якщо не знати про стан і часові позначки, що повертаються з кожним значенням, про підписки, які сповіщають про зміни, про дозвіл підключення за сертифікатом і про те, як відновлюватися після розриву зв’язку, зібраним даним потім не можна буде довіряти.
У цій статті я пояснюю ті частини OPC UA, які потрібні для збору даних, разом із результатами реального запуску клієнта на C#. Клієнт використовує бібліотеку OPC Foundation для .NET, а з іншого боку — імітований сервер обладнання. З конкретними PLC чи пристроями я це не перевіряв. Налаштування конкретного пристрою звіряйте з його документацією.
Про збір зображень із камер GigE Vision і USB3 Vision ідеться у статті «Dropped Frames with GigE Vision and USB3 Vision Cameras», а про пристрої з послідовним зв’язком — у статті «Serial Communication App Pitfalls».
Відмінність OPC UA від OPC Classic
Специфікації OPC Classic обмінюються даними між програмними компонентами за допомогою COM/DCOM у Windows. OPC DA для технологічних даних, OPC A&E для сигналізацій і OPC HDA для історії визначено як окремі специфікації1.
OPC Foundation опублікувала OPC UA (Unified Architecture) 2008 року. 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#, що використовує бібліотеку OPC Foundation для .NET, підключається зашифрованим з'єднанням до імітованого сервера обладнання asyncua, який слухає порт 4840.
A["Клієнт на C# (LineCollector)"] -->|"opc.tcp, SignAndEncrypt"| B["Імітований сервер обладнання (asyncua)"]
A -.-> L["Бібліотека OPC Foundation для .NET 2.0.1"]
B -.-> V["Чотири змінні в Line1"]
Рисунок 2: На тому самому ПК клієнт на C# підключився до імітованого сервера обладнання.
Імітований сервер обладнання
Замість справжнього обладнання я створив невеликий сервер на asyncua5. В об’єкті Line1 є чотири змінні: температура (Temperature), кількість виробів (PartCount), робота (Running) і задане значення (Setpoint). Температура змінюється кожні 0,5 секунди, а кількість виробів — кожні 2 секунди. Для безпеки я передбачив два варіанти: без шифрування (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);
// Перегляд: перелічи змінні в 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. Код вище отримує з URI значення 2 за допомогою session.NamespaceUris.GetIndex(LineNamespace) і з нього складає NodeId.
flowchart TB
accTitle: Складання NodeId з URI простору імен
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 є механізм, який доручає стеження за значеннями серверу й дає змогу отримувати від нього сповіщення про зміни. Цей механізм — підписки і 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.
Для підписування й шифрування використовують сертифікат кожного застосунку. Клієнт і сервер перевіряють сертифікати одне одного й відмовляють у підключенні сторонам, яким не довіряють. Сервер, створений за допомогою бібліотеки OPC Foundation для .NET, типово не пропонує політику без шифрування None4.
Перше підключення зупинилося на 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 ідеться у статті «The Windows Certificate Store in Practice».
Автентифікація користувачів
Сертифікати перевіряють застосунки. Незалежно від цього під час відкриття сеансу передається токен, що ідентифікує користувача. Специфікація визначає чотири види: анонімний, ім’я користувача й пароль, сертифікат 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. Цей випадок я в експерименті не перевіряв. В обох випадках записуйте час розриву й повторного підключення, щоб було видно, у який період значення не надходили. Повторне підключення й проєктування журналів для пристроїв з послідовним зв’язком розглянуто також у статті «Serial Communication App Pitfalls».
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. Про те, як показувати стан обладнання на екрані, ідеться у статті «Best Practices for Checking and Displaying External Device State».
Відкрийте порт у брандмауері
Порт OPC UA, зареєстрований в IANA, — 4840/TCP3. Проте не всі сервери використовують цей номер. Зразки OPC Foundation для .NET використовують, наприклад, порт 625414. Перевірте номер порту в URL кінцевої точки обладнання й дозвольте підключення до цього порту. Якщо сервер працює у Windows, дозвольте його в брандмауері Windows на цьому комп’ютері, а якщо між ними є мережеве обладнання — і на ньому. Про створення правил для вхідних підключень ідеться у статті «The Windows Firewall and Business Applications».
flowchart TB
accTitle: Де збір даних дає збої
accDescr: Рисунок показує місця, де збір даних з обладнання дає збої, поділені на вміст записів (значення з часу зупинки сервера не надходять, яку часову позначку записувати, обробка значень, що не є Good) і підготовку підключення (порти брандмауера, строк дії сертифікатів).
A["Де збір даних дає збої"] --> R["Вміст записів"]
A --> N["Підготовка підключення"]
R --> B["Значення з часу зупинки не надходять"]
B ~~~ C["Яка часова позначка"]
C ~~~ D["Обробка значень, що не є Good"]
N --> E["Порти брандмауера"]
E ~~~ F["Строк дії сертифікатів"]
Рисунок 10: Навіть після вдалого підключення перевіряйте прогалини в записах, час, стан, порти й сертифікати.
Вибір між OPC UA, Modbus/TCP і власними протоколами виробників
Для зв’язку з PLC і обладнанням, крім OPC UA, є також Modbus/TCP і протоколи, які надають виробники PLC.
Специфікація Modbus працює з даними у чотирьох таблицях: Discrete Input — однобітові значення лише для читання; Coil — однобітові значення для читання й запису; Input Register — 16-бітові слова лише для читання; і Holding Register — 16-бітові слова для читання й запису22. В IANA для прикладного протоколу Modbus (mbap) зареєстровано 502/TCP3.
У ці таблиці потрапляють біти й 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
- Serial Communication App Pitfalls - Through Reconnection and Log Design
- Best Practices for Checking and Displaying External Device State
- How to Work with USB Devices from a Windows App
- The Windows Firewall and Business Applications
- The Windows Certificate Store in Practice
- 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 генерує виняток, сховища rejected і trusted, те, що сервери типово не пропонують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 Update, OneDrive і програми з Microsoft Store
На лімітному підключенні Windows Update завантажує лише пріоритетні оновлення, а OneDrive призупиняє синхронізацію. Store, настройки, реж...
Чи можна видалити Windows.old і «Тимчасові файли»? ── Коли використовувати Датчик сховища, а коли Очищення диска
Чи можна видалити Windows.old і тимчасові файли, залежить від того, чи не втратите ви щось, що вже не вдасться повернути. За офіційними м...
Що змінює режим живлення Windows? ── Найкраща продуктивність, збалансований і найкраща енергоефективність: відмінності та вибір
Що змінюють режими живлення Windows 11 «Найкраща продуктивність», «Збалансований» і «Найкраща енергоефективність»: настройки процесора пі...
Що таке ігровий режим Windows? ── Що змінюється, коли його ввімкнути, і чи впливає він на щось, крім ігор?
Ігровий режим Windows під час гри зупиняє встановлення драйверів і сповіщення про перезапуск від Windows Update та допомагає утримувати с...
Що таке «виділена пам'ять GPU» і «спільна пам'ять GPU» в диспетчері завдань? ── Чому на вбудованих GPU цифри так сильно відрізняються і що відбувається, коли пам'яті не вистачає
Що означають виділена та спільна пам'ять GPU в диспетчері завдань, чому вбудовані GPU показують мало виділеної та багато спільної пам'яті...
Пов’язані теми
Сторінки з близькими темами. Від статті можна перейти до пов’язаних послуг та інших матеріалів.
Технічні теми про 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?
- Тому що клієнт ще не довіряє сертифікату сервера. Бібліотека OPC Foundation для .NET зберігає відхилені сертифікати у сховище rejected. Звірте відбиток сертифіката з адміністратором сервера і лише потім перенесіть його у сховище trusted. Налаштування, яке автоматично приймає сертифікати, використовуйте тільки в середовищі розробки.
- Який номер порту використовує OPC UA?
- В IANA порт 4840/TCP зареєстровано як opcua-tcp. Проте реальні сервери можуть використовувати й інший порт, тож перевірте це в налаштуваннях обладнання. Якщо сервер працює у Windows, потрібно дозволити трафік на цей порт у брандмауері Windows, а якщо між ними є мережеве обладнання, то й на ньому.
Профіль автора
Сторінка з профілем автора статті.
Ґо Комура
Представник компанії KomuraSoft LLC
Працює переважно над розробкою ПЗ для Windows, технічними консультаціями та розслідуванням дефектів. Особливо сильна сторона — проєкти з наявними активами та розслідування збоїв з неочевидною причиною.