Pull to refresh

Modbus-RTU на скриптах

Reading time4 min
Views24K

Аннотация


Здесь описан способ реализации протокола Modbus-RTU при помощи shell-скрипта и обвязки в виде js-кода. Обсуждаемый метод может быть использован для реализации других потоковых протоколов, где нужно оперировать массивами байт в ограниченном окружении (роутер).



Идея в трёх строчках


Для нетерпеливых показываю основную идею:

printf "\x00\x03\x00\x00\x00\x01\x85\xDB" > $tty
( dd if=$tty of=$ans count=256 2> /dev/null ) & /usr/bin/sleep $timeout; kill $!
echo "[`hexdump -ve '1/1 "%d,"' $ans | sed 's/\(.*\),/\1/'`]"

Задача


Для начала определимся с целями. Предположим, что у нас имеется роутер с прошивкой OpenWrt типа TL-MR3020 и нужно с его помощью управлять сторонним устройством по протоколу modbus-rtu. Не будем рассматривать варианты подключения такого устройства к роутеру (их несколько), а рассмотрим возможные способы написания управляющего ПО для такой связки.

Первое, что приходит на ум — использование libmodbus, но для этого нужно писать программу на C, компилировать её. Любое решение с компиляцией требует продвинутых навыков работы, наличие соответствующего ПО и даже ОС. В общем, это не вариант, как метод, для широкого употребления.

Второе, что можно попробовать — скриптовые движки, доступные в OpenWrt. Например, lua. Есть там и другие, но опять проблемы. Их нужно изучать, если не знаешь, но это полбеды. На роутере TL-MR3020 очень мало свободного места, буквально до 1 Мб. Если установить скриптовые пакеты с зависимостями, то может просто не хватить места для чего-то ещё.

Опытным путём, перебирая разные варианты, я обратил внимание вот сюда: Some black magic: bash, cgi and file uploads. В этой небольшой статье приведён пример загрузки файла при помощи shell скрипта с такими же как у меня ограничениями. Если кратко, то мы видим использование команды dd для сброса бинарного потока из запроса в файл напрямую без использования временных файлов. Этот код просто идеальный кандидат для решения нашей задачи.

Решение


Теперь разберём те три строчки, что я привёл выше.

Шаг 1. Для реализации протокола modbus-rtu нам нужно формировать запрос и принимать ответ. Этот запрос должен быть оформлен как массив байт. Для этой цели мы используем printf и перенаправление вывода:

printf "\x00\x03\x00\x00\x00\x01\x85\xDB" > $tty

Шаг 2. Хорошо, запрос мы отправили, а как получить ответ? Мы не сможем использовать read для этих целей, т.к. с нулевыми байтами эта команда не дружит. Воспользуемся приёмом с командой dd, указанным выше, и сохраним принимаемые данные в файл. Но тут есть одно но, т.к. нужно указывать точное количество принимаемых байт. По-байтно в цикле разобрать посылку в скрипте мы не сможем (размер можно узнать из принимаемых данных), т.к. просто не успеем скорее всего. Можно выйти из положения, указав максимальный размер посылки (256 байт), но dd зависнет и будет ожидать приёма, если пришло меньшее количество. И тут мы делаем последний финт: Timeout a command in bash without unnecessary delay

( dd if=$tty of=$ans count=256 2> /dev/null ) & /usr/bin/sleep $timeout; kill $!

или так:

timeout $timeout dd if=$tty of=$ans count=256 2> /dev/null

Второй вариант требует около 60 Кб для использования timeout и мы его использовать не будем, когда есть «бесплатное» решение. В результате работы такой команды мы получим файл с принятыми данными.

Шаг 3. Выводим принятый массив байт в каком-нибудь удобном формате:

echo "[`hexdump -ve '1/1 "%d,"' $ans | sed 's/\(.*\),/\1/'`]"

Этот код представляет каждый байт в десятичном виде, вставляет запятые между ними, удаляя последнюю запятую, и обёртывает квадратными скобками. Это массив в json и его легко перевести в js-массив (JSON.parse() или вообще автоматически для $.post() с параметром 'json').

Если у вас есть указанный роутер и доступ к терминалу, то вы можете проверить эти шаги, подключив роутер через usb-com переходники и нуль-модем к ПК. В качества modbus устройства можно использовать эмулятор, например такой: Modbus Slave.

Причём тут JavaScript?


Наблюдательный читатель может спросить: «А как считать crc для посылаемых данных в shell-скрипте?» Думаю, что никак (я находил расчёт только для строк и то на bash, а мы имеем усечённую версию интерпретатора). Этой задачей у нас будет заниматься «верхний» уровень, а именно, вызывающая скрипт при помощи post-запроса html-страничка. Делается это несложно, вот кусок кода из примера, о котором я скажу ниже, отвечающий за выполнение запроса (используется jQuery):

Post: function( slaveid, func, bytes ) {

    var self = this;

    // Добавляем CRC к запросу.
    var crc = this.crc16( bytes );

    bytes.push( crc & 0xFF );
    bytes.push( crc >> 8 );

    // Преобразуем массив в строку.
    var adu = '';

    for ( var b in bytes ) adu += '\\x' + dec2hex( bytes[b] );

    // Выводим application data unit (ADU).
    $('#console').val( adu );

    return $.post( this.Url, { action: 'query', serial: this.Serial, data: adu },
        function( data ) { self.OnReceive( slaveid, func, data ); }, 'json' );

},


Function: function( slaveid, func, address, value ) {

    var bytes = [];

    try {

        bytes.push( slaveid );
        bytes.push( func );
        bytes.push( address >> 8 );
        bytes.push( address & 0xFF );
        bytes.push( value >> 8 );
        bytes.push( value & 0xFF );

        return this.Post( slaveid, func, bytes );

    } catch ( ex ) {

        console.error( ex );
    }

},

Саму контрольную сумму считаем табличным методом. Не буду приводить таблицы, они есть и в сети, и в примере, а сам код стандартный:

crc16: function( data ) {

    var hi = 0xFF;
    var lo = 0xFF;
    var i;

    for (var j = 0, l = data.length; j < l; ++j) {

        i = lo ^ data[j];
        lo = hi ^ CRC_HI[i];
        hi = CRC_LO[i];
    }

    return hi << 8 | lo;
}

Пример


Осталось только показать конкретный пример. Наглядно это сделать не просто, поэтому я отсылаю к своему модулю для альтернативной прошивки CyberWrt: CyberWrt модуль «Modbus». Там можно скачать последний архив с исходниками модуля, а также прочую сопутствующую документацию.

Выглядит же пример вот так:

1. Ошибка при приёме.



2. Считываем 10 регистров.



Заключение


В архиве к примеру будет находиться исходник modbus.js, в котором реализован весь функционал работы по протоколу. Принимаемые данные пока располагаются в свойстве Modbus.Register[]. Такой вариант работы я сделал по аналогии с ActiveX компонентом MBAXP Modbus RTU/ASCII ActiveX Control. Если вы прочитаете справку к нему, то поймёте организацию кода.

Пример ещё дорабатывается, поэтому текущее описание может устареть.

Дополнение [11.06.2014]

Добавил поддержку задач и периодическое их выполнение. Возникла проблема с их наложением.

image

Ссылки


1. Modbus Application Protocol V1.1b3 (pdf)
2. Описание протокола Modbus на русском (doc)
3. CyberWrt модуль «Modbus» (пример)
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+3
Comments1

Articles