Управление роботом на Ардуино из приложения на 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, ведущий к роботу, а потом воткнуть его обратно.