1i7 (1i7) wrote,
1i7
1i7

Categories:

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

Вторая часть про управление ардуиной из настольноо приложения на JavaScript/Node.js с библиотекой babbler_h/babbler.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, ведущий к роботу, а потом воткнуть его обратно.



подсветка синтаксиса
Tags: arduino, babbler, chipkit, роботы, типовые задачи, хабра
Subscribe

Posts from This Journal “типовые задачи” Tag

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 2 comments