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

Промышленные сети — это мир, где протоколы живут десятилетиями. 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 — готовая база для реального шлюза.