Управление роботом на Ардуино из приложения на Node.js

от автора

В прошлый раз мы рассмотрели, как сделать свой мини-терминал с режимом «вопрос-ответ» на роботе с Ардуиной с библиотекой 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *