Showing posts with label сети и протоколы. Show all posts
Showing posts with label сети и протоколы. Show all posts

December 1, 2020

Мы замкнули шину K-Bus

Чтобы проверить информацию из истории про K-Bus-Interbus, коллега А по моей просьбе взял BK9050, подключил к нему модуль дискретных входов-выходов KL1859... и не стал подключать терминальный модуль.

Все испытания проводились в целях испытания. Ни один из модулей не пострадал. Животные участие не принимали.
Не пытайтесь повторить, потеряете гарантию!

Сканируем… Устройства на шине недоступны. Проверяем индикацию коплера. На картинке выше виден единственный красный индикатор. Документация говорит, что это "K-Bus ERR", то есть ошибка шины K-Bus. В данном случае отсутствует терминальный модуль.

Вспоминаем результаты препарации из предыдущей статьи и замыкаем шину:

Все индикаторы зеленые, шина доступна, устройства сканируются и обнаруживаются. Никогда не делайте так в реальных условиях! Всегда используйте терминальный модуль KL9010. 

November 17, 2020

Переменная через указатель, через ADS

Значение переменной можно прочитать через указатель по ADS.

PROGRAM MAIN
VAR
    state:  UINT;
    pState: POINTER TO UINT;
END_VAR

pState := ADR(state);


Прочитаем значение переменной `state` через ее указатель `pState`. Нужно разыменовать указатель, добавив спец. символ `^` к имени указателя. Получим `pState^`. Программа на C# для текущей версии 4.4.10 библиотеки Beckhoff.TwinCAT.Ads:

ushort state = (ushort) tcAdsClient.ReadSymbol( "MAIN.pState^", typeof( ushort ), false );


Значение переменной читается. Теперь возьмем пробную версию новой библиотеки 5.0.1-preview.12 под .NET Core 3.1.0. Код поподробнее:


Программа читает переменную и в зависимости от ее значения рисует замысловатые узоры. Имея указатель можно вытягивать значение переменной. Это полезное свойство и дополнительные телодвижения здесь не нужны.


Для новой библиотеки под .NET Core требуется создать файл роутинга `StaticRoutes.xml`. В этом файле настраиваются соединения между целевым контроллером и ПК. Если контроллер  и ПК находятся на одной и той же машине, то настраивать этот файл не нужно. Можно просто удалить его.

November 10, 2020

Секретный сервис 700

Не все сервисы TwinCAT описаны в документации. В старых исходниках библиотеки AdsClient, в файле AdsSpecial.cs, есть любопытная функция public string GetTargetDesc(). Описание функции гласит: "Get an xml description of the plc...". Посмотрим, какое именно описание отдаст нам контроллер.

Начнем с системного сервиса, порт номер R3_CTRLPROG = 10000. Официальное описание списка функций заканчивается номером 600 (см. таблицу System Service Index Groups). Мы же прочитаем индекс 700 (0x02BChex). Настраиваем роутинг и начинаем читать. Код C# программы:

 

Первый запрос отправляем с параметром typeof(int). Где-то внутри библиотеки это транслируется в параметр команды равный 4 — это длина типа данных int в байтах. В ответ, запрос вернет длину XML текста с описанием устройства. Зная размер, мы отправляем второй запрос в тот же порт-индекс-смещение, но в качестве параметра передаем длину описания, полученного на предыдущем шаге. Результат парсим. С помощью LINQ ищем и вытаскиваем интересующие нас поля описания.

Контроллер CX9020 вернул такой XML:

 

Сравним описания от CX9020 (Win CE) и ноутбука (Windows 10). Чтобы отследить различия, сохраним XML в отдельный файл: xdoc.Save( "cx9020.xml" ); и воспользуемся программой WinMerge:


TС/BSD

Новая, перспективная и все еще недоступная операционная система выдает следующее описание:

 

Пропали элементы `ImageDevice`, `ImageLevel`. Значение `CPUArchitecture` стало более осмысленным, но что такое 9 все еще непонятно.


Тоже сервисы

Можно найти еще несколько интересных сервисов, если копнуть глубже. Для раскопок пригодится какой-нибудь HEX-вьюер (VSCode hexdump) и следующая строка: File.WriteAllBytes( $"idx_{READDEVDESCRIPTION_IDX}.bin", adsStream.ToArray() ); // Начинайте копать.

Индекс 701 выдаст список всех сетевых интерфейсов доступных на устройстве. IP-адреса, маски подсети и что-то еще. Формат неизвестен, но можно разобраться самостоятельно. Копайте.

Из любопытного, для TC/BSD сервис возвращает название сетевого интерфейса в юникс стиле — `em0`.
А для Windows возвращает GUID: "{7D8FDCBA-6250-8DFF-4089-AB0845B12EDC} Qualcomm Atheros AR5BWB222 Wireless Network Adapter 192.168.2.177 255.255.255.0".

Индекс 702 отдает имя целевой машины: PC-8E5B1A, CX-3F5BC9... Строка заканчивается '\0', не забывайте про .TrimEnd('\0'); Продолжайте копать.


November 6, 2020

Агент данных OPC UA

Настроим агент данных на трансляцию данных из OPC UA сервера. Одновременно оставим без изменения трансляцию из ADS переменных. Пусть ADS работает параллельно с OPC UA. Активируйте конфигурацию каждый раз, когда изменяете что-либо в схеме агента. Он перезапустится с новой схемой автоматически.

ПКМ по пустому месту → Add Gate (OpcUaDevice). В окне `Properties` ввести в поле`Url` адрес своего OPC UA сервера: opc.tcp://192.168.1.100:4840. Добавьте канал подписчика: Add Channel (Subscriber), стрелка вниз 🡇.

Теперь открываем окно `Target Browser`, закладка `OpcUa`, добавляем OPC UA сервер контроллера, выбираем необходимые объекты-переменные и тащим их на "подписчика".

Возможно понадобится донастроить переменные подписчика. В моем случае Агент добавил мусор в имя переменной ns=4;s=MAIN.nCounter. Исправляется в окне `Properties`, поле `URN` и превращается в MAIN.nCounter. Ниже в примере исправлена только одна переменная-символ:

>>> {"Timestamp":"2020-11-06T11:48:08.151","GroupName":"_MQTT Broker_28","MAIN.nCounter":-1946,"ns=4;s=MAIN.rCounter":784486.0}
>>> {"Timestamp":"2020-11-06T11:48:09.150","GroupName":"_MQTT Broker_28","MAIN.nCounter":-1846,"ns=4;s=MAIN.rCounter":784586.0}
>>> {"Timestamp":"2020-11-06T11:48:10.150","GroupName":"_MQTT Broker_28","MAIN.nCounter":-1746,"ns=4;s=MAIN.rCounter":784686.0}

November 3, 2020

Начало работы с агентом данных

Агент позволяет передавать переменные программы и другие данные из одного места в другое. Например, есть группа контроллеров CX8090. На отдельном ПК устанавливается TC3 IoT Data Agent. Он настраивается на проброс данных через интернет на сервер-брокер MQTT. Из брокера данные забираются в базу данных. Позже аналитики анализируют, а сервисный отдел мониторит и бдит. Версия TwinCAT, разрядность и тип процессора, древность контроллеров — все это не важно. Переменные из контроллера можно передавать куда угодно, в обе стороны.

Изображение: Beckhoff Automation

Современные протоколы типа MQTT–AMQTT–RabbitMQ не требуют входящего подключения. Агент и контроллеры могут находится за NAT, файерволом или другой сетевой инфраструктурой. IP-адрес может быть серым и динамическим, но подключение к брокеру всегда исходящее. Поэтому переменные контроллера легко отдавать и легко забирать. В обе стороны.


Лицензии

Для ПК, на котором установливается Агент, необходимы минимум две лицензии: TC1000 | TC3 ADS и TF6720 | TC3 IoT Data Agent. Доступна пробная лицензия на 7 дней.

Лицензирование основано на группах порталов. Порталы объединяются в пакеты (Gate packs). Порталом называют одно подключение. Например, подключение к устройству через ADS или OPC UA. Лицензия TF6720 обеспечивает работу с четырьмя порталами. Большее количество порталов можно получить после покупки дополнительных лицензий (TF6721-TF6724). Количество порталов складываются: TF6720 + TF6721 = 8 порталов.


Принципы работы

Open local, Save local работают со схемой в локальной конфигурации Агента C:\TwinCAT\3.1\Boot\TcIotDataAgentConfig.xml. Эта схема будет использована при старте Агента на этом локальном ПК. Во время работы рядом будет лежать лог TcIotDataAgent.log. По нему можно проводить диагностику работы Агента.

Open file, Save file импорт/экспорт схемы из отдельного файла.

С помощью кнопки "активировать конфигурацию", можно активировать схему на удаленном контроллере. На кнопке изображена традиционная горка кубиков (Save to selected target and activate).

В окне `Topology` создаем схему передачи данных. Нужно запомнить два простых принципа: создаем правой кнопкой мыши (ПКМ), а затем соединяем элементы с помощью Ctrl + тащим и бросаем. Например:

  • ПКМ по пустому месту → Add Gate (ADS) → получился круг — это ADS-портал, ведущий к переменным контроллера.
  • Затем, ПКМ → Add Gate (MQTT) → появилось облако — это брокер MQTT, источник данных.
  • ПКМ ADS портал → Add Channel (Subscriber) → создается подписчик (subscriber) в виде прямоугольника. Стрелка вниз 🡇 указывает направление подключения.
  • ПКМ Подписчик → Add Symbol → добавляется новая переменная (symbol) для чтения из контроллера. Можно сделать проще: открыть окно `Target Browser`, перетащить и бросить переменную на "подписчика".
Схему можно создавать и редактировать через другие окна программы. Исследуйте их. Выберите удобный способ работы с программой.

Аналогично поступаем с порталом MQTT Broker, где вместо `Add Gate` доступен `Add Channel`. Брокер работает не с переменными, а с каналами. Через них идут потоки переменных.

Дальше тащим прямоугольник подписчика ADS: Ctrl + левая клавиша мыши (ЛКМ). Бросаем его на прямоугольник канала MQTT. Между элементами появляются связи.

Настройки всех элементов собраны в окне `Properties`.


Пример программы

Необходимо проработать четыре момента:

  • ПЛК программу как источник данных. Подойдет любая версия TwinCAT. Я брал как вторую, так и третью версию TwinCAT. Меняется номер порта ADS 801 → 851, но принципы создания схемы остается прежним.
  • Создать схему передачи данных для Агента.
  • Выбрать MQTT брокер данных.
  • Создать клиента для брокера MQTT. Я напишу простую программу на C#. Она будет читать данные из брокера. Здесь можно воспользоваться готовыми клиентами MQTT и запустить их на смартфоне.


ПЛК программа примитивная:

PROGRAM MAIN
VAR
    iCount: INT;
    rCount: REAL;
END_VAR

iCount := iCount + 1;
rCount := rCount + 0.1;


Схема Агента

Пора выбрать бесплатного брокера на тестирование. Мне понравился HiveMQ. Кроме него проверил Mosquitto. Он работал, но значительно медленнее.

Пришло время создать схему:


Разбивка по каналам позволяет устанавливать единые сетевые настройки для нескольких переменных сразу. Можно создать один единственный канал и транслировать в нем сразу несколько переменных. Чтобы было интереснее, я разбил трансляцию от брокера на два канала (см. картинку выше, правая часть):

  • Ads_Mqtt_11_19 — транслирует переменную MAIN.rCount;
  • Ads_Mqtt_11_17 — передает целое число из переменной MAIN.iCount.

Внутри канала данные брокера можно раскидать по темам (Topic). Это настраивается в окне `Properties`. Например, пусть `rCount` как бы передается из жилой комнаты GOT/TWINCAT/ROOM, а переменная `iCount` приходит из офиса GOT/TWINCAT/OFFICE. В клиенте брокера я смогу выбрать или одну конкретную, интересующую меня тему, или сразу несколько тем. Темы фильтруются с помощью спец. символов `*`, `?` или `#`. Например, я хочу в одном канале получать данные ROOM+OFFICE: GOT/TWINCAT/#.


Клиент брокера

Для C# я использовал библиотеку MQTTnet. Она легко устанавливается из NuGet. Раскомментируйте строку и подставьте название своего топика-комнаты в константу `MQTT_TOPIC`.

 

Результат работы клиента:


Одновременно я установил на телефон бесплатный клиент `MQTT Dash` и он также смог отображать данные с ПЛК. Трансляция идет через интернет, можно сходить на обед и одним глазом посматривать как контроллер продолжает работать:

August 20, 2020

Сокеты не реального времени

Из ПЛК задачи можно сделать TCP/UDP клиент или даже сервер. Это позволяет работать с данными современной периферии без разработки промежуточных слоев. У нас есть выбор между TF6310 (обычная и давно практикуемая) и TF6311 (модная, современная, риалтаймовая). Обе-две работают как на PC/CX (x86), так и на CX (ARM). В этом посте будет практика работы с обычной библиотекой 6310, а с новой разберемся как-нибудь позже.

Изображение: Beckhoff Automation

TF6311 TCP/UDP (realtime)

Полное описание доступно в инфосисе. Обе библиотеки доступны из ПЛК задачи, но 6311 работает "рядом" с ядром TwinCAT 3, на том же системном уровне.

Здесь заострю внимание на особенностях, плюсах и минусах.

  • Только TwinCAT 3.
  • Функционал не требует установки, все компоненты уже встроены в TwinCAT 3.
  • Требуется лицензия TC3 TCP/UDP RT.
  • Есть возможность использовать временную (trial) лицензию на время разработки.
  • Работает напрямую с сетевой картой, минуя большинство механизмов операционной системы.
  • TF6311 настраивается в проекте через TCP/UDP RT TcCom Parameter. Это требует отдельного рассмотрения.

Минусы

  • Не рассчитан на большие и незнакомые сети. Вероятно подразумевается интернет, либо большой интранет.
  • Не для больших объемов данных.
  • Не поддерживает мультикаст в UDP.
  • Windows Compact CE только начиная с версии 7.
  • Windows Firewall отсутствует в цепочке передачи пакетов (менее защищенные соединения).
  • Только TwinCAT-совместимые карты. Список доступен в Supported Network Controller by Beckhoff Ethernet Driver.
  • Нет связи с локальным, стандартным сетевым интерфейсом Windows. Можно реализовать через стороннего посредника.
  • Сетевые коммутаторы (эзернет свичи) EL6601 и EL6614 не могут использоваться совместно с этой библиотекой.

Плюсы

  • Очень детерминированный и предсказуем (подтвердить не могу).
  • Поддержка С++ (похоже, что это основное назначение библиотеки).
  • Поддерживает ARP/Ping.


TF6310 TCP/IP


Работает через специальный ADS-сервер, а дальше через стандартный WinSock, по сути копируя стандартный функционал сетевого ввода-вывода. Как и что устанавливать вменяемо описано в справке, и проблем это обычно не вызывает.

Для TwinCAT 2 мы устанавливаем TS6310. Он приносит с собой следующие библиотеки в каталог TwinCAT\Plc\Lib:
  • TcpIp.lib — базовые функции TCP/IP и UDP;
  • TcSocketHelper.lib — вспомогательные функции TCP/IP, упрощающие жизнь разработчика. Содержит готовые ФБ с полным циклом клиент-сервер и сервер-клиент.
  • TcSnmp.lib — протокол SNMPv1, вспомогательные функции.
  • TwinCAT TCP/IP Connection Server — по сути это мост ADS ↔ TCP/IP.


Практика 6310


TCP-клиент рассчитан на передачу больших объемов данных в виде непрерывных потоков. Данные текут непрерывно, но ничто не мешает разбить их на пакеты. Перед началом работы устанавливается соединения, которое по окончании работы разрывается. Длительность передачи данных после соединения не оговаривается: минуты, часы, дни, года. Так было задумано и это обычная практика работы с TCP/IP протоколом.

Минимальная реализация TCP-клиента:
  • FB_SocketConnect и FB_SocketClose — для подключения и разрыва сессии.
  • FB_ClientServerConnection — включает в себя оба предыдущих блока и упрощает работу с ними.
  • FB_SocketSend и/или FB_SocketReceive — для обмена данными.

Минимальная реализация TCP-сервера:
  • FB_SocketListen — открывает сокет на прослушивание в режиме ожидания клиента.
  • FB_SocketAccept и FB_SocketClose — открывают и соответственно закрывают соединение.
  • FB_ServerClientConnection — умеет все вышеперечисленное вместе и упрощает работу.
  • FB_SocketSend и/или FB_SocketReceive — для обмена данными.

UDP-клиент пакетный, работает без установки постоянного соединения: отправил пакет данных и забыл. Пакет может и не дойти, а может дойти фрагментированным, то есть разбитым на несколько частей. Иногда задняя часть может придти раньше передней (если ползли разными маршрутами). И так тоже было задумано, во славу производительности.

Минимальная реализация UDP-клиента:
  • FB_SocketUdpCreate и FB_SocketClose — открыть/закрыть сокет.
  • FB_SocketUdpSendTo и/или FB_SocketUdpReceiveFrom — прием-отправка данных.

Для UDP-пакета ограничен максимальный размер отправляемых данных. По умолчанию он равен 8192 байт (это число задается константой TCPADS_MAXUDP_BUFFSIZE). Поэтому стоит обратить внимание на аргумент cbLen функции FB_SocketUdpSendTo. Ограничение служит для экономии памяти.

Для всех типов связи FB_SocketCloseAll закрыть всё открытое и закончить любые работы в пределах текущего рантайма. Это который имеет определенный порт, например, 801 для первого рантайма TwinCAT 2.
FB_SocketAccept, FB_SocketReceive, FB_SocketUdpReceiveFrom — вызываются циклически (polling), то есть каждый цикл. Остальные блоки вызываются по мере необходимости.


Производительность


TwinCAT 2


CX8090 (TC2, WinCE 7, ARMv4) — идеально когда происходит один вызов сетевой функции за цикл. 4 соединения за цикл работают нормально, при большем числе идут таймауты.

CX9020 (TC2, WinCE 7, Arm Cortex™-A8) — 4 соединения одновременно за цикл работают нормально, а дальше идут таймауты. Соединения-подключения все равно необходимо выполнять последовательно: сначала устанавливается одно соединение и только затем выполняем следующее подключение.


TwinCAT 3


CX8190 (TC3, WinCE 7, ARM Cortex™-A9) — 10 соединений одновременно за цикл работают нормально. Соединения-подключения необходимо выполнять последовательно.

CX2030 (TC3, Windows 7 Emb Std, Core-i7) — 10 соединений одновременно за цикл работают нормально. Соединения-подключения необходимо выполнять последовательно.


Не больше 10 клиентов

  • Не больше 10 клиентов. Ограничение в 10 клиентских подключений на рантайм, а возможно и на весь ПЛК, но это не точно.
  • Разные циклы — разные подключения. Открывать соединение лучше друг за другом по одному за цикл.
  • Функции всегда работают как минимум один цикл. После первого вызова, в последующих циклах можно спокойно вызывать ФБ с bExecute := FALSE, ожидая когда функция переключится в NOT bBusy, что означает поступление данных либо ошибку.
  • При обрыве связи возвращает bError = FALSE и nRecByte = 0. Для определения обрыва необходимо самостоятельно использовать собственный таймер для контроля таймаута. Великая вещь, что функции здесь неблокирующие (разработчики на C++ и другие поймут).
  • Протокол поточный, если читаете данные пачками, то полученные данные необходимо накапливать в буфере, десериализовать или просто сканировать этот буфер на предмет специального тэга или какой-либо другой ситуации предусмотренной протоколом (это уже зависит от специфики протокола).

Мне стало интересно откуда это волшебное и круглое десятичное число — десять. И почему нельзя взять и подключаться ко всему и сразу. Я начал следить за количеством соединений и количеством системных потоков (threads).
Соединения - потоки
 2 - 8
 3 - 10
 4 - 12
 . . ..
 6 - 16
Просматривается явная зависимость — на каждое новое соединение создается два новых потока. Зачем два? Заглянем в лог TcpIpServer.log:

Видно, что сначала создается ADS-сокет CTcpAdsSocket::CTcpAdsSocket(); Он будет принимать команды и данные из ФБ ПЛК-задачи, а затем создается требуемый TCP-сокет CTcpSocket::Create(); теперь уже для непосредственной передачи данных. Поэтому каждый цикл можно открыть только одно новое соединение — на запрос создания сокета создается только одна связка ADS ↔ TCP|UDP. Такая вот архитектура, упрощенно.


Не больше 10, но можно меньше


Под Windows CE можно поиграть с ключами реестра: Start → Run... → regedit. Создать ключ Registry → New → Key: HKLM\SOFTWARE\Beckhoff\TwinCAT TcpIp Server. Внутри раздела доступны несколько значений-параметров типа DWORD. Что удалось выяснить:

MaxTcpSocketCount
0 = вероятно стандартные 10 сокетов-соединений.
1 = запретить вообще все подключения. Теперь функции FB_SocketListen и FB_SocketConnect возвращают код ошибки TCPADSERROR_NOMOREENTRIES (0x0000800132769).
2 = 1 доступное подключение.
3 = 2 доступных подключения.
[...]
11 = 10 максимально доступных подключений. Всё, больше нельзя.

MaxUdpSocketCount — аналогично MaxTcpSocketCount, но для UDP протокола.
AdsServerCommTimeout — возможно таймаут ADS-сервера. Единицы измерения вероятно миллисекунды.
DisableKeepAlive — запретить постоянные KeepAlive подключения?
ThreadPriority — приоритет системного потока? Значения не известны.

LogLevel
0 = отключен.
1 = включен. Логировать будет сюда: \Hard Disk\TwinCAT\TcpIpServer.log


TcSocketHelper


TcSocketHelper.lib выполняет за нас все трудоемкие операции по клиентским подключениям к серверу и в обратную сторону — отслеживает клиентские подключения к серверу. Доступные примеры лежат в справочной системе.

FB_ServerClientConnection — выполняет функции TCP-сервера. Внутри себя содержит и выполняет FB_SocketListen, FB_SocketAccept и FB_SocketClose. На выходе выдает идентификатор сокета hSocket для подключившегося клиента. Дальше передаем его в FB_SocketSend и/или FB_SocketReceive.

FB_ClientServerConnection и FB_ConnectionlessSocket — первый служит для создания TCP-клиентов, а второй для UDP. Оба умеют создавать и закрывать соединения. При успешном соединении выдают на выходе hSocket для передачи в FB_SocketSend и/или FB_SocketReceive.

Из интересного все функции, связанные с получением данных (Receive), внутри себя содержат механизм регулировки скорости обновления ФБ (пропуск тактов, троттлинг, throttling). Ничего особенного, это обычный подход в такой ситуации. Кратко выглядит так:

TYPE T_ThrottleTimes: ARRAY[0..MAX_THROTTLE_MODE] OF TIME;
END_TYPE

throttleTimes: T_ThrottleTimes := T#0s, T#10ms, T#20ms, T#40ms, T#60ms, T#80ms, T#100ms,
                                  T#200ms, T#400ms, T#600ms, T#800ms, T#1s, T#2s;

И скрытая, только для внутреннего использования, функция-обертка над таймером FB_ThrottleTimer, которая состоит всего-лишь из одной строки с вызовом таймера:

timer(
    IN := bIn,
    PT := throttleTimes[LIMIT(0, selector, MAX_THROTTLE_MODE)],
    Q  => bOut,
    ET => tElapsed );

Здесь `selector` задает текущий режим, а его значение изменяется через вызов одного из четыре экшенов (Action):
  • MaxSpeed — selector = 0.
  • MinSpeed — selector = MAX_THROTTLE_MODE.
  • SlowDown — увеличивает задержку, уменьшает скорость опроса.
  • SpeedUp — уменьшает задержку, увеличивает скорость опроса.

Суть этого действа в автоматическом регулировании интервал ожидания: увеличивать интервал если сообщений нет, и снижать интервал ожидания, если сообщения пошли часто-часто.

November 6, 2019

Производительность ReadAny и Reactive в 5.0.0-preview1

В продолжении темы измерения скорости чтения, посмотрим что там с превью версией под .NET Core 3. Заодно увидим как обстоят дела с реактивностью.


ReadAny


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();
    var connection = session.Connection;

    const int HOWMUCH = 1000;
    List results = new List();

    uint hMain_CycleCount = connection.CreateVariableHandle("MAIN.CycleCount");

    for (int i = 0; i < HOWMUCH; i++)
        results.Add(
            (uint)connection.ReadAny(hMain_CycleCount, typeof(uint)));

    foreach (var cycleNumber in results)
        Console.WriteLine(cycleNumber);
}


.NET Core 3, AdsRouterConsole



Для сравнения повтор картинки из предыдущего поста для .NET 4.7.1, TwinCAT.IO Router



Reactive


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();

    const int HOWMUCH = 1000;

    List<uint> results = new List<uint>();

    var valueObserver = Observer.Create(
        val => { // on next
            results.Add(val);
            //Console.WriteLine(val);
        },

        exc => { // on error
            Console.WriteLine(exc.Message);
        },

        () => {  //on completed
            Console.WriteLine($"Read {results.Count} cycles");

            foreach (var cycleNumber in results)
                Console.WriteLine(cycleNumber);
        });

    var subscription = session.Connection
        .WhenNotification("MAIN.CycleCount", NotificationSettings.ImmediatelyOnChange)
        .Take(HOWMUCH)
        .Subscribe(valueObserver);

    Console.WriteLine("ENTER to stop...");
    Console.ReadLine();
}


Все совсем плохо:



Отрицательные значения показывают, что пакеты прилетали не по порядку, а как-то иначе. Причем, если убрать условие в получении N-значений .Take(1000), и просто попытаться выбрать требуемое количество, то получится такая же точно каша с непоследовательными элементами. Что нарушает уверенность в целостности системы.

При времени цикла меньше одной секунды, перестает работать делегат отвечающий за onComplеte часть обозревателя. Именно поэтому пришлось добавить в onNext вывод промежуточных результатов.

Пока что очень сыро и сильно напрягает непоследовательность приходящих элементов.

November 5, 2019

InvokeRpcMethod — вызов удаленного метода

С помощью ADS можно не только передавать данные, но и вызывать методы ПЛК задачи.

TcAdsClient client = new TcAdsClient();
client.Connect(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
short result = -1;
var args = new object[]{ result, (short)1, (short)2 };
try {
    result = (short)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
}
catch { }


PROGRAM / ACTION


Проверим странное...
...оно не работает. Нет такого символа
Value cannot be null.
Parameter name: symbol
Работоспособность не зависит от размещения атрибута: что внутри экшена, что над объявлением программы, что и там, и здесь. Результат отрицательный.


FB


Action

Опять нестандартное. И оно тоже не работает.
The RPC method 'A_Func01' is not supported on symbol 'MAIN.fbBox!
Кстати, опечатка в сообщении эксепшена — не хватает кавычки.


Method

Для метода, ожидаемо, работает. Причем, внезапно, вызов метода работает даже когда Runtime в состоянии Stop. Иначе говоря, когда порт создан (851), но ПЛК задача еще не запущена.

Команда __NEW при RPC вызовах также работает, даже в состоянии "стоп", и по прежнему выжирает память роутера. Контроль памяти роутера описан в New, Delete и память роутера. Поэтому если память выделяется внутри RPC метода, а освобождается в другом месте — получится Ахтунг и протечка памяти!

Итоги:
  • Уровень доступа метода не влияет на доступность извне. Любой из PUBLIC, PRIVATE, PROTECTED, INTERNAL доступен для внешнего вызова.
  • Вызов требует наличия всех параметров объявленных в ФБ как VAR_INPUT
  • Точки останова внутри методов не срабатывают при RPC вызовах.


Насколько быстро?


Тестировать будем на CX9020, x32, TwinCAT 3.1.4022.25 и на десктопе Core i7-3630QM x64 TwinCAT 3.1.4024.0. Для интереса сравним производительность относительно обычного чтения символа.

results[i] = (uint)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
// VS
results[i] = (uint)client.ReadSymbol("MAIN.CycleCount", typeof(uint), false);


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

Начну с обычного чтения ReadSymbol. По горизонтали — номер выборки. По вертикали — время в миллисекундах, затраченное на чтение символа. Цветом обозначено время цикла ПЛК-задачи. Первым будет CX9020, вторая картинка — десктоп:

ReadSymbol, CX9020



ReadSymbol, десктоп Core i7 x64


Даже "на глаз" видно, что десктоп справляется "стабильнее" и быстрее. К тому же, в случае десктопа все происходит на локалхосте и нет сетевых прослоек, привносящих дополнительные лаги. Краткий вывод: для циклов от 4 миллисекунд данные возвращаются стабильно. Меньше 4мс могут быть задержки (видимо запросы попадают на границу цикла).

Кроме этого есть запросы с нулевым временем выполнения. Это озорует роутер. При отправке запросов чаще, чем время одного цикла, роутер делает вид, что значение еще не изменилось и быстро шлет обратный ответ с тем же, старым значением. Ну и отлично.

Теперь про RPC запросы. Порядок контроллеров прежний:

RPC, CX9020



RPC, десктоп Core i7 x64


RPC выполняется дольше чем запрос данных одной переменной. Это ожидаемо, так как нужно принять запрос с аргументами, выполнить код, и только затем отправить ответ. 3-4-5 циклов, в зависимости от того, куда упадет запрос относительно границы кванта системного времени.

Правило 4мс по прежнему работает.


Выводы


RPC-запрос может легко выполнить код, который нарушит экосистему рантайма. Например, может вызвать утечку памяти и т. п. В то же время можно попытаться организовать автоматическую систему юнит-тестирования, так как RPC вызовы работают даже на приостановленном рантайме.

Если же необходимо реализовать контурные режимы или передавать данные чаще чем раз в 4мс, то необходимо реализовать промежуточный буфер и отправлять несколько значений за раз, используя обычные функции для работы с переменными ReadAny, WriteSymbol, etc.


PS: Инструменты


Для проведения тестирования и рисования результатов, иногда достаточно обычного экселя (Excel), что и было использовано:




October 21, 2019

Linux + TwinCAT 3 + .Net Core 3 + ADS 5

Система реального времени по прежнему работает под Windows, но реализовать клиентскую сторону, визуализацию или массивную обработку данных (дата саенс, ага) под Linux уже можно.

Не то чтобы раньше нельзя было. Например, родные Бекхоффские реализации протокола ADS на C++, с неродным интерфейсом на QT. Python+Ads для обработки данных, например в блокнотах Jupiter и AdsClient (.NET Standart) для C# под гуем из WPF. Протокол открытый, почему бы и нет?

Теперь же Бекхофф выкатил в публичное превью библиотеки для работы с ADS под .NET Core 3. В первую очередь это означает кросс-платформенность для приложений на C# под Linux, Windows, Mac. О последнем я говорить не хочу, а для первых двух вырисовывается полный стек интересных технологий (или интересный стек полных технологий):
  • .NET Core 3.0
  • AvaloniaUI
  • Beckhoff.TwinCAT.Ads 5

Я еще раз подчеркну, сразу для двух операционных систем: Windows - Linux. Одно и то же приложение. Возможно необходимо пересобрать, тыкнув мышой в выбор операционки, но переписывать код уже не нужно. Выглядеть интерфейс будет одинаково на обеих операционках. Это благодаря Avalonia, которая почти как WPF.


TwinCAT.Ads 5.0.0-preview1


Итак, не далее как неделю назад, Бекхофф выложила превью библиотек. Нас ждут интриги, расследования и гонки на багги. "This is a prerelease version of Beckhoff.TwinCAT.Ads", — говорят нам RaHe, sveno, MichaelKn и Beckhoff со страниц NuGet. Далее идут инструкции как со всем этим жить, но это не точно. Потому что инструкции не работают. Как "нада жить" будет позже, а сейчас про состав выложенного:
  • TwinCAT.Ads 5.0.0-preview1
    Сборка для доступа к данным ПЛК через ADS. Совместима с предыдущими версиями библиотеки. Состав прежний: Client, Session, Read-Write Any Symbol, и тому подобное. Появились асинхронные операции. TcAdsClient переименован в AdsClient; AdsStream заменен на модные и современные Span+Memory.
  • Ads.AdsRouterConsole
    Под Linux нет рантайма и ADS-роутера соответственно. Поэтому Бекхофф предлагает нам нечто вроде выполняющее функции роутера. Запускается как консоль отдельно и самостоятельно, в том числе и под Linux.
  • Ads.TcpRouter
    А это уже не самостоятельная штука, зато можно засунуть себе в программу. Это как бы переносной роутер, который будет внутри вашей программы. Вокруг этой же штуки построена консоль AdsRouterConsole.
  • Ads.Reactive
    Тоже сборка для доступа к данным, но доступ "реактивным" способом.
  • AdsServerAbstractions
    Сервер и всё что сборки делят между собой. Про сервер как-нибудь в другой раз: где еще делать сервер как не под Linux?


Запускаем AdsRouterConsole


Сборка консоли роутера будет лежать в 
%USERPROFILE%\.nuget\packages\beckhoff.twincat.ads.adsrouterconsole\5.0.0-preview1\lib\netcoreapp3.0\TwinCAT.Ads.AdsRouterConsole.dll

...но(!) в инструкции, которая лежит в разделе Documentation NuGet, есть опечатка. На самом деле запускать нужно так:
dotnet TwinCAT.Ads.AdsRouterConsole.dll

...но(!) опять-таки  не запустится, так как этой сборке для запуска не хватает других сборок. Поэтому мы установим сборку TwinCAT.Ads.AdsRouterConsole в свой проект, и тогда .dll файл сборки будет лежать в папке /bin/Debug/netcoreapp3/, а в ней уже автоматом будут все необходимые для запуска консоли роутера сборки.

...но(!) при запуске мы получим следующую ошибку:
A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in '...\bin\Debug\netcoreapp3\'.
Failed to run as a self-contained app. If this should be a framework-dependent app, add the ...\TwinCAT.Ads.AdsRouterConsole.runtimeconfig.json file specifying the appropriate framework.
Это связано с безопасностью в системе, поэтому возвращаемся к
%USERPROFILE%\.nuget\packages\beckhoff.twincat.ads.adsrouterconsole\5.0.0-preview1\lib\netcoreapp3.0
(да-да, именно для этого он нам и нужен был) и кладем файл TwinCAT.Ads.AdsRouterConsole.runtimeconfig.json рядом со сборкой консоли роутера, то есть в каталог /bin/Debug/netcoreapp3/ или /bin/Release/netcoreapp3/ проекта. Выбор конечного пути зависит от этапа разработки и конфигурации проекта.

Файл можно создать самостоятельно, вот его содержимое:

{
    "runtimeOptions": {
        "tfm": "netcoreapp3.0",
        "framework": {
            "name": "Microsoft.NETCore.App",
            "version": "3.0.0"
        }
    }
}

После этого, может запуститься, а может не запуститься и вылететь с ошибкой:
Error: An attempt was made to access a socket in a way forbidden by its access permissions.
...и это всё потому, что у вас уже запущен роутер. Приложение кросплатформенное, можно запускать как под Linux так и под Windows. Прибить работающий роутер можно остановив сервис TcSysSrv из командной строки PowerShell от имени Администратора или через Computer Management.


Таблица роутинга


На самом деле и сейчас не запустится: любому роутеру нужны настройки роутинга. Со стороны ПЛК придется настроить самостоятельно и вручную, а со стороны ПК сформировать XML файл StaticRoutes.xml, расположив его рядом со сборкой консоли роутера:

<?xml version="1.0" encoding="utf-8"?>
<TcConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="C:\TwinCAT3\Config\TcConfig.xsd">
  <Local>
      <Name>RALFH03</Name>
      <NetId>172.17.60.215.1.1</NetId>
  </Local>
  <RemoteConnections>
    <Route>
      <Name>CX-1CD661</Name>
      <Address>169.254.116.30</Address>
      <NetId>5.28.214.97.1.1</NetId>
      <Type>TCP_IP</Type>
    </Route>
  </RemoteConnections>
</TcConfig>

Секция Local — это имя и локальный Ams NetId адрес ПК. Выбираются произвольно, но в роутерах с обеих сторон, в записях касающихся ПК, этот адрес должен совпадать с обеих сторон, то есть во всех роутерах сразу. RemoteConnections — это про удаленную сторону, то есть о ПЛК. Сравним с настройками роутера на ПЛК.

Зайдем на ПЛК через web-конфигуратор: http://169.254.112.234/Config. Далее: TwinCAT → Connectivity → Add TwinCAT Route. Внизу списка жмем кнопки [+] и создаем запись роутинг для удаленного ПК (или ноутбука разработчика), удаленного теперь уже относительно ПЛК:

Route Name: RALFH03
AMS Net ID: 172.17.60.215.1.1
Transport Type: TCP/IP
Address: 169.254.7.118
Connection Timeout (ms): 3000

Сравните "тут" и "там" и станет немного понятнее.


Virtual Box


Я тестировал под виртуальной машиной на x64 Ubuntu 19.04. Для Virtual Box необходимо... краткое объяснение на пальцах — обязательно выставить все три сетевые параметра сетевого коннекта Убунты: IP адрес, маску и обязательно gateway (гейтвей можно произвольный, я выставлял 192.168.0.1). Не забыть установить исполняемому файлу атрибут исполняемого фалйа: chmod +x имя-исп-файла. Установить в системе dotnet core 3 (читайте справку Майкрософта).

И наконец прибить роутер TwinCAT в операционной системе хосте (Windows), иначе будут заняты сетевые порты. Переключения в режим конфигурации (синий) недостаточно, нужен серый значок полного аута.


Багги


А дальше начинаются скачки по дюнам и прочим неровностям превью бета версии библиотек. На любых функциях типа client.ReadSymbol("MAIN.Counter") я получал ошибку:
Stack overflow.
Aborted (core dumped)
...и приложение падает. Интересно, что точно такую же ошибку я получал когда пытался использовать независимую сборку независимого разработчика AdsClient. Видимо, корень проблемы одинаковый.

В то же время можно создать хэндл client.CreateVarHandle("MAIN.Counter"), а затем нормально работать на функциях чтения-записи через хэндлы. Также нормально работает чтение по имени переменной через реактивную подписку, которая к тому же неплохо ложится на реактивность Avalonia:

var valueObserver = Observer.Create(val => {
    Dispatcher.UIThread.InvokeAsync(() => {
        var mwvm = (MainWindowViewModel)DataContext;
        mwvm.Counter = val.ToString();
    });
});

IDisposable subscription =
    session.Connection
        .WhenNotification("MAIN.Counter", NotificationSettings.Default)
        .Subscribe(valueObserver);


То есть как-то работать уже можно и что-то рабочее уже вырисовывается:



Среда разработки


Теперь попробуем поработать под Linux — посмотрим в сторону среды разработки.

Как таковой среды еще нет, зато есть кроссплатформенный редактор Visual Studio Code, а к нему расширение vscode-st за авторством Сергея Романова. Расширение помогает работать с кодом на ST. Проект открытый, исходный код есть на GitHub. Самое главное, что проект не заброшен и развивается: я добавил туда пару новых штук, и внезапно разработчик принял пул-реквест, и так же мгновенно пересобрал расширение.


Подсветка синтаксиса, подсказка по переменным и ФБ, снипеты и прочие плюшки. Еще раз: это не отладчик и не среда разработки, а редактор кода, но это уже что-то. Остается дождаться чего-нибудь подобного TcXaeMgmt, ведь PowerShell Core уже работает под Linux, а роутероподобное ПО мы только что потрогали палочкой. Да что там, TcXaeMgmt уже можно установить, но работать командлеты не будут: отсутствует TwinCAT Automation Interface который построен на COM объектах.

Поэтому — ждем. Редактор файлов уже есть, остается прослойка для работы с загрузкой-выгрузкой файлов-проектов и пуск-стопом рантаймов, а там и до дебага недалеко (на самом деле, далеко).

May 30, 2019

Обмен данными через ADS между ПЛК-задачами

Уже во втором твинкате была библиотека TcDataExchange и, соответственно сейчас в третьем есть библиотека DataAccess → Tc2_DataExchange. Основное назначение — она умеет отправлять переменные ПЛК через ADS. При этом одновременно можно использовать как имя переменной, так и пару индекс-смещение. Получается этакий комбайн, умеющий все и сразу, и сильно упрощающий жизнь и движение между ПЛК задачами.
И там действительно есть полезный набор функций, но(!) нужно быть осторожным. В случае регулярной или частой пересылки данных, возможны непредвиденные нагрузки на систему. Причина в том, что внутри комбайна прячется комбайнер CASE с рядом скрытых от разработчика действий, которые активно перемалывают системные ресурсы.

При работе через ADS, для начала необходимо получить дескриптор переменной или, иначе говоря, занять какой-то системный ресурс. А раз занять, то по окончании требуется вернуть или сохранить до следующей обработки той же переменной. В библиотеках ADS для программирования вне системы реального времени уже предусмотрено кэширование хэндлов: существует скрытая внутренняя таблица, позволяющая системе не заниматься слишком частым занятием-освобождением хэндлов и соответственно ресурсов.

Функции чтения-записи FB_ReadAdsSymByName и FB_WriteAdsSymByName в зависимости от значения параметра eComMode также могут запоминать и повторно использовать полученный дескриптор, но только один. Соответственно, экземпляр функция с параметром eComMode = E_AdsComMode.eAdsComModeFastCom должен работать только с одной переменной. Это обязательно необходимо учесть, так как по умолчанию используется безопасный режим eComMode : E_AdsComMode := eAdsComModeSecureCom, который фактически становится опасным из-за постоянного передергивания ресурсов системы.

Delta-функции библиотеки TcDataExchange для одной единственной операции будут раз за разом получать, обрабатывать и освобождать системные ресурсы. И это логично, так как они изначально созданы для отправки данных только при условии выхода значения переменной за пределы, заданные разработчиком. Что естественно не должно происходить слишком часто. Насколько часто — разработчик должен определить самостоятельно. Очень похоже на событийную модель.

В итоге, библиотека оказывается действительно полезной, ведь кроме перечисленного выше, на вход функций поступает не сами данные, а указатель на них, следовательно можно отслеживать и передавать не только одиночную переменную, но и строку, и массив и, в конце концов, целую структуру.

May 14, 2017

Мастер Modbus RTU на шине RS485

Ненадолго станем мастером Modbus RTU на шине RS485, будем читать регистры устройств, например, ряда электросчетчиков или насосов, связанных двумя проводами и контролируемых ПЛК.
Если вам нужно наоборот — побыть подчиненным, выставляя данные наружу, читайте — Сервер Modbus TCP, про адресацию — Взгляд со стороны клиента ModbusTCP.
Для удобства и отсутствия пайки возьмем EL6021, который оснащен клемником, а не D-sub разъемом, хотя это вряд ли имеет значения — все модули организованы одинаково, включая системные, расположенные на "теле" контроллера, такие как -N031. Для RS232 и RS422 все аналогично, кроме линии связи и электрической части.

Для модулей под разъем (D-sub, 9-pin) характерно:
  • RS232, разъем на модуле — "папа" (plug), на кабеле — "мама" (socket).
  • RS485 / RS422, разъем на модуле — "мама" (socket), на кабеле — "папа" (plug).

Бекхофф пропагандирует первенство программной части, поэтому начнем от программы. Подробно про линковку переменных и прочие пошаговые прохождения квеста можно узнать из русскоязычной методички — Modbus_Step-by-step.pdf


Лицензии и библиотеки


Для TwinCAT 2 библиотека поставляется отдельно и за деньги: TS6255 | TwinCAT PLC Modbus RTU. Результатом будет библиотека TwinCAT — ModbusRTU.lib.

Библиотека для TwinCAT 3 TF6255 | TC3 Modbus RTU — "Already included in the basis setup", — то есть она уже включена в стандартную поставку, но потребует дальнейшего лицензирования при выводе проекта в "поле". Резюмирую: пока пишете проект или "щупаете" возможности библиотеки — можно не покупать и пользоваться возобновляемой семидневной лицензией. После готовности проекта, заказываете лицензию и активируете ее на постоянной основе.

Буферы данных и PDO


Первое, что необходимо сделать, это связать программу технологического процесса и "железо". Для этого объявляется ФБ типа:

VAR
    mb : ModbusRtuMasterV2_KL6x22B;
END_VAR

Тип ФБ выбирается, исходя из размера буфера данных модуля связи:
  • ModbusRtuMasterV2_KL6x22B — с буфером в 22 байта.
  • ModbusRtuMasterV2_KL6x5B — с буфером в 5 байт.
  • ModbusRtuMasterV2_PcCOM — с буфером в 64 байта, рассчитано на системный порт контроллера.
  • ModbusRtuMasterV2 — для использования только внутри библиотеки, но можно попробовать задействовать как универсальный ФБ для буферов произвольных размеров (буферы адресуются через указатели pComIn и pComOut).

Размер буфера данных можно изменять в разделе PDO, но не нужно — это востребовано только при портировании старых проектов. Поэтому часть наборов помечено как Legacy — это наследие старых систем, а часть как Standart — это нормальные, современные возможности по трансляции данных в реальном времени.

Для примера, возьмем EL6022 (EL6021). Справочная система говорит, что этот модуль содержит:

    Data buffer 864 bytes receive buffer, 128 bytes transmit buffer
    Bit width in the process image 22 x 8 bit input, 22 x 8 bit output, 16 bit control, 16 bit status

Это означает, что на борту модуля есть аппаратный буфер на прием и отправку данных (Data buffer 864 bytes...), часть аппаратного буфера отражается (или мапится) в PDO-образы TwinCAT (process image 22 x 8 bit input...).



В случае с так называемым системным портом, например, модули -N031 или порт панели P205|CP6606-..., с буферами немного сложнее: необходимо вручную добавить последовательный порт Miscellaneous → Serial Communication Port в устройства ввода-вывода I/O Devices. После этого, по умолчанию, будет включен режим совместимости с коплерами BK8xx0 Mode. Нам же необходим режим совместимости с KL6xx1 Mode (Emulation). KL-EL — без разницы: разная шина — одинаковый принцип работы.

В режиме совместимости с KL6xx1 мы работаем с системным портом, как с модулем расширения EL6022, где Data Bytes — это размер PDO области, а Buffer Size — как бы аппаратный буфер. но не обязательно по настоящему аппаратный — он может эмулироваться операционной системой. Размер в Data Bytes будет влиять на тип структуры данных для линковки с ПЛК-программой. Для стандартных 64-байт подойдет ModbusRtuMasterV2_PcCOM. Как такового, требования не существует, но пусть аппаратный буфер будет как минимум в два раза больше буфера PDO. Помните, что все это не точно и требует плотного тестирования на вашей конкретной системе.
Модули EL60xx умеют становиться виртуальными COM-портами операционной системы: соседняя вкладка EL60xx → Virtual Com Port. После установки драйвера и активирования конфигурации, Windows обнаружит новый последовательный порт, к которому можно будет подключить произвольное внешнее устройство, не имеющее никакого отношения к TwinCAT (сканер штрих кодов, например).
После линковки Input/Output (COM TxPDO-Map Inputs и COM RxPDO-Map Outputs) частей со структурами типа:

TYPE MB_KL6inData22B
STRUCT
    Status: WORD;
    D: ARRAY [0..21] OF BYTE;
END_STRUCT
END_TYPE

...которые различаются только размером буфера, дальнейшая работа целиком ведется через экшены функционального блока. В TwinCAT 3 библиотека по прежнему работает через экшены: модных классов и методов, к сожалению, не завезли.


Экшены


ReadCoils — функция модбас #1, считать дискретный выход подчиненного (Read Coils), результат будет упакован (8 бит в байте).
ReadInputStatus — #2, прочитать дискретный вход подчиненного (Read Input Status), результат будет упакован (8 бит в байте).
ReadRegs — #3, прочитать регистр хранения (Read Holding Registers).
ReadInputRegs — #4, прочитать регистр ввода (Read Input Registers).
WriteSingleCoil — #5, записать (задать) состояние дискретного выхода подчиненного (Write Single Coil), данные должны быть упакованы (8 бит в байте).
WriteSingleRegister — #6, записать в регистр хранения (Write SingleRegister).
WriteMultipleCoils — #15, записать в несколько дискретных выходов подчиненного (Write Multiple Coils), данные должны быть упакованы (8 бит в байте).
WriteRegs — #16, записать в несколько регистров подчиненного (Preset Multiple Registers).
Diagnostics — #8, диагностика (Diagnostics).



Диагностика


Поле Error в ModbusRtuMasterV2_KL6x22B сигнализирует об ошибке, а код ошибки содержится в поле ErrorId. Заниматься диагностикой удобно где-нибудь за пределами приема-передачи данных, заодно подсчитывая статистику пакетов.

VAR
    LastError    : MODBUS_ERRORS;
    CountError   : DINT;
    CountSuccess : DINT;
    Error        : BOOL;
    trigBusy     : F_TRIG;
END_VAR

// обмен данными начинается
// [...]
// обмен данными заканчивается
END_CASE

trigBusy(CLK := mb.BUSY );
IF trigBusy.Q THEN
    IF mb.Error THEN
        Error := TRUE;
        LastError := mb.ErrorId;          // Запоминаем код последней ошибки
        CountError := CountError + 1;     // Подсчитываем количество ошибок
    ELSE
        Error := FALSE;
        CountSuccess := CountSuccess + 1; // Количество удачных пакетов
    END_IF
END_IF

Список всех ошибок есть в статье Modbus RTU Error Codes. Все названия констант "говорящие" и вполне понятные.


Практика


Для обмена данными понадобятся как минимум два буфера, которые могут быть как массивами, так и структурами. Можно читать пачки регистров в структуры, но не забывайте — Modbus оперирует регистрами размером в "слово" (WORD, uint, sint, int).

VAR
    ReadData  : ARRAY [1..10] OF WORD; // массив из данных десяти регистров
    WriteData : ARRAY [1..10] OF WORD; // массив данных для десяти регистров
END_VAR
VAR CONSTANT
    WORDS_TO_READ  : // [...] сколько слов (WORD) прочитать
    WORDS_TO_WRITE : // [...] сколько слов (WORD) отправить
END_VAR


Сбрасываем блоки перед применением, и после применения тоже сбрасываем:

CASE state OF
    INIT_STATE:
        mb.ReadRegs(Execute := FALSE);
        mb.WriteRegs(Execute := FALSE);


Читаем:

READ_STATE:
    mb.ReadRegs(
        UnitID      := 1,                 // адрес подчиненного, начинается с 1
        Quantity    := WORDS_TO_READ,     // сколько слов (WORD) прочитать
        MBAddr      := 0,                 // адрес модбас, может отсчитываться как от нуля,
                                          // так и от единицы, читайте документацию на подчиненного
        cbLength    := WORDS_TO_READ * 2, // сколько прочитать, но уже в байтах
        pMemoryAddr := ADR(ReadData),     // локальный буфер куда будут записаны прочитанные регистры
        Execute     := TRUE,
        Timeout     := T#5s               // таймаут на выполнение операции
    );

    IF NOT mb.BUSY THEN
        mb.ReadRegs(Execute := FALSE);
        IF mb.Error THEN
            state := INIT_STATE;      // обрабатываем ошибки
        ELSE
            state := // [...]         // продолжаем работу 
        END_IF
    END_IF


Пишем:

WRITE_STATE:
    mb.WriteRegs(
        UnitID      := 1,
        Quantity    := WORDS_TO_WRITE,     // сколько слов (WORD) записать
        MBAddr      := 0,
        cbLength    := WORDS_TO_WRITE * 2, // сколько записать, но уже в байтах
        pMemoryAddr := ADR(WriteData),     // локальный буфер данные которого
                                           // будут записаны в регистры подчиненного
        Execute     := TRUE,
        Timeout     := T#5s                // таймаут на выполнение операции
    );

    IF NOT mb.BUSY THEN
        mb.WriteRegs(Execute := FALSE);
        IF mb.Error THEN
            state := INIT_STATE;           // обрабатываем ошибки
        ELSE
            state := // [...]              // продолжаем работу 
        END_IF
    END_IF


И не забывайте про время обработки цикла ПЛК-задачи: его продолжительность должна быть достаточной для вычитывания буферов, иначе начнет происходить периодическое переполнение и вы будете терять актуальную информацию.