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

Комментарии (0)

© Habrahabr.ru