Аннотация
Здесь описан способ реализации протокола 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]
Добавил поддержку задач и периодическое их выполнение. Возникла проблема с их наложением.
Ссылки
1. Modbus Application Protocol V1.1b3 (pdf)
2. Описание протокола Modbus на русском (doc)
3. CyberWrt модуль «Modbus» (пример)