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

в 9:01, , рубрики: diy или сделай сам, javascript, JS, modbus, OpenWrt, shell, TL-MR3020, умный дом, метки: , , ,

Аннотация

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

Решение

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

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

printf "x00x03x00x00x00x01x85xDB" > $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. Ошибка при приёме.

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

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

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

Заключение

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

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

Ссылки

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

Автор: ViacheslavMezentsev

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js