В прошлый раз мы рассмотрели, как сделать свой мини-терминал с режимом «вопрос-ответ» на роботе с Ардуиной с библиотекой babbler_h. Сегодня посмотрим, как эту же библиотеку использовать для управления роботом из настольного приложения на JavaScript+Node.js.
Чтобы меняться данными с роботом, в клиентской части на JavaScript+Node.js используем специально написанную по такому случаю библиотеку Babbler.js. Для работы с последовательным портом Babbler.js использует стандартную библиотеку node-serialport, но строит поверх нее некоторые дополнительные удобства.
Особенности библиотеки
— Библиотека позволяет подключиться к устройству, отправлять ему команды, получать ответы.
— Библиотека сама обслуживает подключение, прячет внутри все технические нюансы: следит за разрывами, извещает обо всех изменениях статуса подключения, разрешает разрывать связь и подключаться заново.
— Команды добавляются в очередь на отправку, посылаются на устройство одна за одной.
— Библиотека следит за каждым пакетом с командой от момента добавления в очередь до получения ответа или появления ошибки; генерирует публичные события, которые могут быть полезны для отображения статуса устройства или отладки.
— Пользовательский код всегда получит извещение о завершении жизненного пути команды: ответ от устройства или сообщение об ошибке.
— Библиотека обрабатывает любые возможные исключительные ситуации, которые могут произойти с командой на пути к устройству, и генерирует соответствующие сообщения об ошибках. Например, можно добавить команду в очередь на отправку, а затем выдернуть шнур подключения: пользовательский код получит сообщение об ошибке выполнения команды (связь разорвана до отправки команды роботу/связь разорвана после отправки команды роботу), после чего приложение заново подключится к устройству (если робот, конечно, будет опять подключен проводом) и продолжит работу.
— Библиотека терпима к некорректному поведению устройства: робот может забывать отправлять ответы, отправлять ответы не вовремя, отправлять некорректные ответы или вообще сыпать в канал связи (последовательный порт) всякий отладочный мусор. Библиотека в лучшем случае проигнорирует некорректные пакеты, дождавшись нужного, в худшем — отправит в пользовательский код сообщение о том, что робот не выполнил команду (т.е. ответ не получен).
— Устройство считается подключенным после выполнения двух условий: открыт канал связи, устройство прислало корректный ответ «ok» на команду ping.
Дополнительные ограничения на прошивку робота:
— Робот должен принимать команды и отправлять ответы в формате JSON с поддержкой клиентских идентификаторов команды.
— Прошивка робота должна обязательно включать команду ping (без неё не будет установлено соединение).
— Устройство должно прислать ответ на полученную команду не позднее, чем через 5 секунд, иначе клиентский код сочтет команду не выполненной (получит ошибку BBLR_ERROR_REPLY_TIMEOUT).
— Может сложиться ситуация, когда робот по команде должен выполнить некое продолжительное действие, которое может длиться более 5ти секунд (пройти путь из точки А в точку Б), а потом сообщить на пульт управления о том, что действие выполнено. В таком случае следует завести в прошивке робота две команды: "запустить процесс выполнения действия" (возвращается мгновенно с кодом «ок») и "получить статус выполнения запущенного действия" («в процессе»/«готово»). Пульт будет запускать процесс выполнения действия по первой команде, а потом периодически проверять его статус, раз за разом отправляя вторую команду.
Главные ссылки:
— Библиотека для робота: babbler_h
— Библиотека для Node.js: babbler-js
— Примеры для babbler-js: babbler-js-demo
Протокол
Робот должен принимать команды и отправлять ответы в формате JSON. Пакет данных — строка JSON, содержащая команду или ответ. Пакеты данных отделяются символом переноса строки.
Робот должен принимать команды в формате JSON вида
{"cmd": "help", "id": "34", "params":["--list"]}
здесь:
— cmd — имя команды, строка
— params — параметры команды, массив строк
— id — клиентский идентификатор команды, строка (необязательный)
Имя команды и параметры понятно. Клиентский идентификатор — произвольное значение, генерируется клиентом и отправляется вместе с командой, робот отправляет его же с ответом. Идентификатор команды позволит клиенту легко определить, к какой именно из отправленных команд пришел ответ. Уникальность значения обеспечивается на стороне клиента, робот просто копирует пришедшее значение в ответ и больше никак его не анализирует.
ответ должен упаковывать в формат JSON вида
{"cmd": "help", "id": "34", "reply": "help ping ledon ledoff"}
здесь:
— cmd — исходная команда, строка
— id — клиентский идентификатор команды (копируется исходное значение), строка
— reply — ответ (результат выполнения команды), строка
Возможно, в новых версиях внутри ответа появится значение params с копией исходных параметров команды. Может быть, это не очень эффективный расход ресурсов, зато дополнительное удобство для отладки.
Прошивка для робота
Про установку библиотеки babbler_h для Ардуино и особенности её применения рекомендую посмотреть в предыдущей статье. Здесь сразу привожу пример скетча, который умеет принимать команды и отправлять ответы в формате JSON. Необходимые для работы с JSON функции реализованы в модуле babbler_json.
Смотрим код
Возьмем за основу пример с двумя пользовательскими командами ledon и ledoff для мигания лампочками _2_babbler_custom_cmd.ino и сделаем так, что он принимал запросы и отправлял ответы в формате JSON. По сравнению с исходным вариантом с командной строкой ровно два отличия:
1. Подключаем библиотеку babbler_json.h в заголовке:
#include "babbler_json.h"
2. Заменяем обработчик handle_input_simple на handle_input_json в babbler_serial_set_input_handler в предварительных настройках в setup.
babbler_serial_set_input_handler(handle_input_json);
вместо
babbler_serial_set_input_handler(handle_input_simple);
Больше никаких отличий, в том числе (и в первую очередь) в коде пользовательских команд, нет вообще.
Файл → Примеры → babbler_h → babbler_json_io.ino
#include "babbler.h" #include "babbler_cmd_core.h" #include "babbler_simple.h" #include "babbler_json.h" #include "babbler_serial.h" // Размеры буферов для чтения команд и записи ответов #define SERIAL_READ_BUFFER_SIZE 128 #define SERIAL_WRITE_BUFFER_SIZE 512 // Буферы для обмена данными с компьютером через последовательный порт. // +1 байт в конце для завершающего нуля char serial_read_buffer[SERIAL_READ_BUFFER_SIZE+1]; char serial_write_buffer[SERIAL_WRITE_BUFFER_SIZE]; #define LED_PIN 13 /** Реализация команды ledon (включить лампочку) */ int cmd_ledon(char* reply_buffer, int reply_buf_size, int argc=0, char *argv[]=NULL) { digitalWrite(LED_PIN, HIGH); // команда выполнена strcpy(reply_buffer, REPLY_OK); return strlen(reply_buffer); } /** Реализация команды ledoff (включить лампочку) */ int cmd_ledoff(char* reply_buffer, int reply_buf_size, int argc=0, char *argv[]=NULL) { digitalWrite(LED_PIN, LOW); // команда выполнена strcpy(reply_buffer, REPLY_OK); return strlen(reply_buffer); } babbler_cmd_t CMD_LEDON = { /* имя команды */ "ledon", /* указатель на функцию с реализацией команды */ &cmd_ledon }; babbler_man_t MAN_LEDON = { /* имя команды */ "ledon", /* краткое описание */ "turn led ON", /* руководство */ "SYNOPSIS\n" " ledon\n" "DESCRIPTION\n" "Turn led ON." }; babbler_cmd_t CMD_LEDOFF = { /* имя команды */ "ledoff", /* указатель на функцию с реализацией команды */ &cmd_ledoff }; babbler_man_t MAN_LEDOFF = { /* имя команды */ "ledoff", /* краткое описание */ "turn led OFF", /* руководство */ "SYNOPSIS\n" " ledoff\n" "DESCRIPTION\n" "Turn led OFF." }; /** Зарегистрированные команды */ extern const babbler_cmd_t BABBLER_COMMANDS[] = { // команды из babbler_cmd_core.h CMD_HELP, CMD_PING, // пользовательские команды CMD_LEDON, CMD_LEDOFF }; /** Количество зарегистрированных команд */ extern const int BABBLER_COMMANDS_COUNT = sizeof(BABBLER_COMMANDS)/sizeof(babbler_cmd_t); /** Руководства для зарегистрированных команд */ extern const babbler_man_t BABBLER_MANUALS[] = { // команды из babbler_cmd_core.h // commands from babbler_cmd.core.h MAN_HELP, MAN_PING, // пользовательские команды // custom commands MAN_LEDON, MAN_LEDOFF }; /** Количество руководств для зарегистрированных команд */ extern const int BABBLER_MANUALS_COUNT = sizeof(BABBLER_MANUALS)/sizeof(babbler_man_t); void setup() { Serial.begin(9600); Serial.println("Starting babbler-powered device with JSON i/o," " type {\"cmd\": \"help\", \"id\": \"34\", \"params\":[]} for list of commands"); // попробуйте отправить через монитор последовательного порта // {"cmd": "help", "id": "34", "params":[]} babbler_serial_set_packet_filter(packet_filter_newline); babbler_serial_set_input_handler(handle_input_json); //babbler_serial_setup( // serial_read_buffer, SERIAL_READ_BUFFER_SIZE, // serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE, // 9600); babbler_serial_setup( serial_read_buffer, SERIAL_READ_BUFFER_SIZE, serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE, BABBLER_SERIAL_SKIP_PORT_INIT); pinMode(LED_PIN, OUTPUT); } void loop() { // постоянно следим за последовательным портом, ждем входные данные babbler_serial_tasks(); }
Для быстрого теста можно открыть всё тот же Инструменты → Монитор порта и отправить роботу команду вида
{"cmd": "help", "id": "34", "params":["--list"]}
ответом будет
{"cmd": "help", "id": "34", "reply": "help ping ledon ledoff"}
Конечно, вручную набирать строки в формате JSON не очень удобно, зато для приложения на JavaScript такой канал связи будет как родной.
Настройка клиентской части на Node.js
— Библиотека babbler.js на гитхабе.
— Примеры babbler-js-demo
Для ручной настройки нового проекта — устанавливаем пакет babbler-js
npm install babbler-js
или для готового проекта с примерами выполняем
git clone https://github.com/1i7/babbler-js-demo.git cd babbler-js-demo/babbler-basic npm install
Простой пример: подключаемся к устройству, выполняем команды ping и help —list.
babbler-js-demo/babbler-basic/babbler-basic.js
var BabblerDevice = require('babbler-js'); var babbler = new BabblerDevice(); babbler.on('connected', function() { console.log("connected"); console.log("send cmd: ping"); babbler.sendCmd("ping", [], // onReply function(cmd, params, reply) { console.log("got reply on '" + cmd + " " + params + "': " + reply); }, // onError function(cmd, params, err) { console.log("fail with '" + cmd + " " + params + "': " + err); } ); console.log("send cmd: help --list"); babbler.sendCmd("help", ["--list"], // onReply function(cmd, params, reply) { console.log("got reply on '" + cmd + " " + params + "': " + reply); }, // onError function(cmd, params, err) { console.log("fail with '" + cmd + " " + params + "': " + err); } ); }); babbler.on('disconnected', function(error) { console.log("disconnected" + (error != undefined ? ": " + error : "")); }); babbler.connect("/dev/ttyUSB0"); //babbler.connect("/dev/ttyUSB0", {baudRate: 9600});
запускаем
node babbler-basic.js
в терминале наблюдаем
connected
send cmd: ping
send cmd: help --list
got reply on 'ping ': ok
got reply on 'help --list': help ping ledon ledoff
Выдергиваем шнур USB с роботом, программа пишет последнее сообщение и завершается
disconnected: Device unplugged
Пример чуть интереснее:
— программа подключается к устройству и начинает включать (команда leodon) и выключать (команда ledoff) лампочку каждые 2 секунды;
— в случае отключения устройства, программа пытается переподключиться каждые 3 секунды до тех пор, пока не подключится, после этого снова начинает мигать лампочкой.
babbler-basic/babbler-basic-blink.js
var BabblerDevice = require('babbler-js'); var babbler = new BabblerDevice(); var blinkIntervalId; babbler.on('connected', function() { console.log("connected"); // мигаем лампочкой каждые 2 секунды var ledstatus = "off"; blinkIntervalId = setInterval(function() { if(ledstatus === "on") { console.log("send cmd: ledoff"); babbler.sendCmd("ledoff", [], // onReply function(cmd, params, reply) { console.log("got reply on '" + cmd + " " + params + "': " + reply); ledstatus = "off"; }, // onError function(cmd, params, err) { console.log("fail with '" + cmd + " " + params + "': " + err); } ); } else { // ledstatus === "off" console.log("send cmd: ledon"); babbler.sendCmd("ledon", [], // onReply function(cmd, params, reply) { console.log("got reply on '" + cmd + " " + params + "': " + reply); ledstatus = "on"; }, // onError function(cmd, params, err) { console.log("fail with '" + cmd + " " + params + "': " + err); } ); } }, 3000); }); babbler.on('connecting', function() { console.log("connecting..."); }); babbler.on('disconnected', function(error) { console.log("disconnected" + (error != undefined ? ": " + error : "")); // перестаём мигать, пока не подключены clearInterval(blinkIntervalId); // повторная попытка подключиться через 3 секунды setTimeout(function() { babbler.connect("/dev/ttyUSB0"); }, 3000); }); babbler.connect("/dev/ttyUSB0"); //babbler.connect("/dev/ttyUSB0", {baudRate: 9600});
запускаем
node babbler-basic-blink.js
наблюдаем за мигающей лампочкой
connecting...
connected
send cmd: ledon
got reply on 'ledon ': ok
send cmd: ledoff
got reply on 'ledoff ': ok
send cmd: ledon
got reply on 'ledon ': ok
send cmd: ledoff
got reply on 'ledoff ': ok
send cmd: ledon
got reply on 'ledon ': ok
disconnected: Device unplugged
connecting...
disconnected: Error: Error: No such file or directory, cannot open /dev/ttyUSB0
connecting...
disconnected: Error: Error: No such file or directory, cannot open /dev/ttyUSB0
connecting...
connected
send cmd: ledon
got reply on 'ledon ': ok
send cmd: ledoff
got reply on 'ledoff ': ok
send cmd: ledon
got reply on 'ledon ': ok
disconnected: Device unplugged
в процессе можно выдернуть провод USB, ведущий к роботу, а потом воткнуть его обратно.
ссылка на оригинал статьи https://habrahabr.ru/post/315480/
Добавить комментарий