Поделиться
Поделиться

Промышленные сети — это мир, где протоколы живут десятилетиями. Modbus появился в 1979 году и до сих пор работает на миллионах устройств. OPC UA вышел в 2008-м и стал стандартом для современной промышленной интеграции и Industry 4.0. На практике проекту нередко нужно работать с обоими — читать данные со старых устройств по Modbus и передавать их в облако или MES-систему через OPC UA. Разберём оба протокола до уровня байт и покажем, как их соединить.

Modbus: архитектура и карта регистров

Modbus работает по модели «мастер — ведомые». Мастер инициирует запросы, ведомые отвечают. Ни один ведомый не отправляет данные сам по себе.

RTU vs TCP

| Параметр | Modbus RTU | Modbus TCP | |---|---|---| | Физика | RS-485 (2 провода) | Ethernet / WiFi | | Фреймирование | Межсимвольные паузы + CRC16 | MBAP-заголовок + PDU | | Адрес устройства | 1 байт в каждом фрейме | Unit ID в MBAP, опционально | | Транспортная надёжность | Только CRC | TCP гарантирует доставку | | Типичная скорость | 9600–115200 бод | 100 Мбит/с и выше | | Латентность | 5–50 мс | 1–10 мс |

Четыре типа объектов данных

| Тип | Код | Адреса | Доступ | Описание | |---|---|---|---|---| | Coils | 0x | 00001–09999 | RW | Одноbitные выходы | | Discrete Inputs | 1x | 10001–19999 | RO | Одноbitные входы | | Input Registers | 3x | 30001–39999 | RO | 16-bit аналоговые входы | | Holding Registers | 4x | 40001–49999 | RW | 16-bit рабочие регистры |

Коды функций

0x01 — Read Coils
0x02 — Read Discrete Inputs
0x03 — Read Holding Registers
0x04 — Read Input Registers
0x05 — Write Single Coil
0x06 — Write Single Register
0x0F — Write Multiple Coils
0x10 — Write Multiple Registers

Пример фрейма Modbus RTU — запрос чтения 2 holding registers с адреса 0x0064 у устройства с адресом 0x01:

01 03 00 64 00 02 85 D5
│  │  ├──────┘ ├──────┘ └──── CRC16 (little-endian)
│  │  │        └──────── количество регистров = 2
│  │  └───────────────── начальный адрес = 0x0064 (100)
│  └──────────────────── функция 0x03
└─────────────────────── адрес ведомого = 0x01

Modbus на Python: библиотека pymodbus

from pymodbus.client import ModbusSerialClient, ModbusTcpClient
from pymodbus.exceptions import ModbusException
import struct

# RTU через RS-485
client = ModbusSerialClient(
    port='/dev/ttyUSB0',
    baudrate=9600,
    parity='N',
    stopbits=1,
    bytesize=8,
    timeout=1
)

# TCP
# client = ModbusTcpClient('192.168.1.100', port=502)

client.connect()

try:
    # Читаем 4 holding registers начиная с 0
    result = client.read_holding_registers(address=0, count=4, slave=1)
    if result.isError():
        raise ModbusException(f"Error: {result}")

    regs = result.registers
    # Интерпретируем reg[0]+reg[1] как float32 (big-endian)
    raw = struct.pack('>HH', regs[0], regs[1])
    temperature = struct.unpack('>f', raw)[0]
    print(f"Temperature: {temperature:.2f} °C")
    print(f"Status: {regs[2]:#06x}")

    # Запись single register
    client.write_register(address=10, value=0x0001, slave=1)

finally:
    client.close()

OPC UA: адресное пространство и узлы

OPC UA — это не просто протокол передачи данных, а полноценная информационная модель. Каждый объект в OPC UA описывается через узлы (Nodes) в адресном пространстве (Address Space).

Типы узлов

| NodeClass | Назначение | |---|---| | Object | Контейнер (аналог папки или устройства) | | Variable | Значение с типом данных (температура, давление) | | Method | Вызываемая функция | | ObjectType | Тип объекта (шаблон) | | VariableType | Тип переменной | | DataType | Определение типа (enum, struct) | | View | Подмножество адресного пространства | | ReferenceType | Тип связи между узлами |

Узлы связываются ссылками (References): HasComponent, HasProperty, Organizes, HasSubtype и др.

NodeId

Каждый узел идентифицируется NodeId:

  • i= — числовой (e.g. i=2258 — ServerStatus)
  • s= — строковый (e.g. s=Temperature)
  • g= — GUID
  • b= — бинарный

Namespace 0 зарезервирован для стандартных узлов OPC Foundation.

OPC UA клиент на Python (opcua-asyncio)

import asyncio
from asyncua import Client, ua

async def main():
    url = "opc.tcp://192.168.1.50:4840/freeopcua/server/"

    async with Client(url=url) as client:
        # Навигация по адресному пространству
        root = client.get_root_node()
        objects = client.get_objects_node()

        # Получение узла по NodeId
        temp_node = client.get_node("ns=2;s=Temperature")
        temp_value = await temp_node.read_value()
        print(f"Temperature: {temp_value} °C")

        # Подписка на изменения
        handler = SubHandler()
        sub = await client.create_subscription(500, handler)
        nodes = [temp_node]
        await sub.subscribe_data_change(nodes)

        await asyncio.sleep(10)
        await sub.delete()

class SubHandler:
    def datachange_notification(self, node, val, data):
        print(f"Node {node}: {val}")

asyncio.run(main())

OPC UA клиент на Node.js

const { OPCUAClient, AttributeIds, TimestampsToReturn } = require('node-opcua');

async function main() {
  const client = OPCUAClient.create({
    endpointMustExist: false,
    connectionStrategy: { maxRetry: 3 }
  });

  await client.connect('opc.tcp://192.168.1.50:4840');
  const session = await client.createSession();

  // Чтение значения
  const dataValue = await session.readVariableValue('ns=2;s=Pressure');
  console.log('Pressure:', dataValue.value.value, dataValue.statusCode.toString());

  // Запись
  await session.writeSingleNode('ns=2;s=SetPoint', {
    dataType: 'Double',
    value: 75.5
  });

  // Подписка на изменения
  const subscription = await session.createSubscription2({
    requestedPublishingInterval: 1000,
    requestedMaxKeepAliveCount: 10,
    maxNotificationsPerPublish: 10,
    publishingEnabled: true,
    priority: 10
  });

  const monitoredItem = await subscription.monitor(
    { nodeId: 'ns=2;s=Temperature', attributeId: AttributeIds.Value },
    { samplingInterval: 1000, discardOldest: true, queueSize: 10 },
    TimestampsToReturn.Both
  );

  monitoredItem.on('changed', (dataValue) => {
    console.log('Temperature changed:', dataValue.value.value);
  });

  await new Promise(resolve => setTimeout(resolve, 30000));
  await session.close();
  await client.disconnect();
}

main().catch(console.error);

Мост Modbus → OPC UA → MQTT → облако

Типичная архитектура промышленного шлюза:

[ПЛК / датчики]
      │ Modbus RTU/TCP
[Edge Gateway (Python/Node.js)]
      │         │
  OPC UA     MQTT
      │         │
[MES/SCADA] [Облако / Kafka]

Пример шлюза Modbus → MQTT на Python:

import time
import json
import paho.mqtt.client as mqtt
from pymodbus.client import ModbusTcpClient

MODBUS_HOST = '192.168.1.100'
MQTT_BROKER = 'mqtt.factory.local'
POLL_INTERVAL = 5  # секунд

modbus = ModbusTcpClient(MODBUS_HOST, port=502)
modbus.connect()

mqtt_client = mqtt.Client()
mqtt_client.connect(MQTT_BROKER, 1883)
mqtt_client.loop_start()

REGISTER_MAP = [
    {'name': 'temperature', 'address': 0, 'count': 2, 'type': 'float32'},
    {'name': 'pressure',    'address': 2, 'count': 2, 'type': 'float32'},
    {'name': 'status',      'address': 4, 'count': 1, 'type': 'uint16'},
]

def decode_float32(regs):
    import struct
    return struct.unpack('>f', struct.pack('>HH', regs[0], regs[1]))[0]

while True:
    payload = {}
    for reg in REGISTER_MAP:
        result = modbus.read_holding_registers(reg['address'], reg['count'], slave=1)
        if not result.isError():
            if reg['type'] == 'float32':
                payload[reg['name']] = round(decode_float32(result.registers), 2)
            else:
                payload[reg['name']] = result.registers[0]

    payload['timestamp'] = int(time.time() * 1000)
    mqtt_client.publish('factory/line1/sensors', json.dumps(payload))
    time.sleep(POLL_INTERVAL)

Безопасность OPC UA: сертификаты

OPC UA поддерживает три режима безопасности:

| Режим | Шифрование | Подпись | Применение | |---|---|---|---| | None | Нет | Нет | Только для тестирования | | Sign | Нет | Да | Защита от подмены | | SignAndEncrypt | AES-256 | Да | Продакшн |

Генерация самоподписанного сертификата для OPC UA сервера:

# Генерация RSA ключа и сертификата
openssl req -x509 -newkey rsa:2048 -keyout server_key.pem \
  -out server_cert.pem -days 3650 -nodes \
  -subj "/CN=OPC UA Server/O=MyFactory/C=RU" \
  -addext "subjectAltName=URI:urn:factory:opcua:server,IP:192.168.1.50"

# Конвертация в DER для OPC UA стека
openssl x509 -in server_cert.pem -out server_cert.der -outform DER

Клиент должен доверять сертификату сервера (поместить в trusted/certs/), сервер — сертификату клиента. В промышленной среде используйте корпоративный CA вместо самоподписанных сертификатов.

Типичные ошибки интеграции

  • Порядок байт (endianness): Modbus не стандартизирует порядок байт для float/int32 — разные устройства используют ABCD, CDAB, BADC, DCBA. Всегда уточняйте у производителя.
  • Адресация Modbus: некоторые производители используют адрес 1-based (регистр 1 → address 0), другие 0-based. Проверяйте документацию.
  • Таймауты OPC UA: слишком короткий таймаут сессии вызывает разрывы при медленных сетях. Рекомендуем ≥ 30 секунд.
  • Опрос vs подписка: OPC UA Subscriptions эффективнее постоянного polling — сервер сам уведомляет при изменении значения.

Для понимания работы с промышленными устройствами на низком уровне также смотрите статью о разработке прошивок STM32.

Итог

Modbus остаётся неизбежной реальностью для работы с полевым оборудованием, а OPC UA — современным языком промышленной интеграции. Умение работать с обоими и строить надёжные шлюзы между ними — ключевая компетенция для industrial IoT проектов. Приведённые примеры на Python и Node.js — готовая база для реального шлюза.