- PVSM.RU - https://www.pvsm.ru -

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

Аннотация

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

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

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

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

printf "x00x03x00x00x00x01x85xDB" > $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 [1]. В этой небольшой статье приведён пример загрузки файла при помощи shell скрипта с такими же как у меня ограничениями. Если кратко, то мы видим использование команды dd для сброса бинарного потока из запроса в файл напрямую без использования временных файлов. Этот код просто идеальный кандидат для решения нашей задачи.

Решение

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

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

printf "x00x03x00x00x00x01x85xDB" > $tty

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

( 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 [3].

Причём тут 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» [4]. Там можно скачать последний архив с исходниками модуля, а также прочую сопутствующую документацию.

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

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

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

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

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

Заключение

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

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

Ссылки

1. Modbus Application Protocol V1.1b3 (pdf) [6]
2. Описание протокола Modbus на русском (doc) [7]
3. CyberWrt модуль «Modbus» (пример) [4]

Автор: ViacheslavMezentsev

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/62002

Ссылки в тексте:

[1] Some black magic: bash, cgi and file uploads: http://ncrmnt.org/wp/2013/04/18/someblack-magic-bash-cgi-and-file-uploads/

[2] Timeout a command in bash without unnecessary delay: http://stackoverflow.com/questions/687948/timeout-a-command-in-bash-without-unnecessary-delay

[3] Modbus Slave: http://www.modbustools.com/modbus_slave.asp

[4] CyberWrt модуль «Modbus»: http://cyber-place.ru/showthread.php?t=1305

[5] MBAXP Modbus RTU/ASCII ActiveX Control: http://www.modbustools.com/modbus_activex.asp

[6] Modbus Application Protocol V1.1b3 (pdf): http://yadi.sk/d/UolEm34wSr239

[7] Описание протокола Modbus на русском (doc): http://yadi.sk/d/TwPjEvkySr5j2

[8] Источник: http://habrahabr.ru/post/225833/