Управління роботом на Ардуїнов з додатка 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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.