Консолька в роботі на Ардуине

Переслати роботу на Ардуине кілька байт через вайфай, блютус, послідовний порт або будь-який інший канал зв'язку у вигляді команди, а потім прийняти кілька байт в якості відповіді не складно: достатньо завантажити скетч з прикладом обміну даними «здрастуй світ» і вставити в нього кілька рядків свого коду, який буде виконувати потрібні дії.

Однак з розвитком проекту область допоміжного коду, який відповідає за зв'язок із зовнішнім світом, роздувається: з'являється логіка, що відокремлює один пакет даних від іншого, розростається ліс перевірок, що за команда прийшла, які в неї параметри, як її правильно виконати, що робити, якщо пакет даних не коректний, якщо дані прийшли не повністю, якщо вони не поміщаються у відведених для них буфер пам'яті і так далі. Код, обслуговуючий допоміжну логіку, переплітається з головним кодом, який виконує цікаву і корисну роботу. Замінити один канал зв'язку на інший (наприклад, додати до послідовного порту вайфай) без переробки накопичилася кодової бази стає досить проблематично.

Довгий час я тягав такий код з одного проекту в інший, нові поліпшення і виправлення доводилося оновлювати у всіх проектах паралельно. Це втомлює, загрожує появою нових помилок і не завжди можливо. Нарешті настав час винести всю цю логіку в окрему бібліотеку.

Вихідна завдання: спростити процес створення прошивки для роботів, які будуть працювати в режимі «питання-відповідь». Головний скетч повинен містити корисний код (що, власне, повинен робити робот) і мінімальна кількість допоміжних конструкцій. Всі допоміжні транспортно-протокольні блоки окуклить в бібліотеку і винести за межі уваги інженера.

В якості побічного ефекту вийшла своєрідна командний рядок, що працює всередині Ардуины, якщо підключитися до неї через монітор послідовного порту і відправляти команди вручну:

image


Особливості бібліотеки
— Робота в режимі питання-відповідь
— Максимальні розміри входить команди і відповіді обмежені розміром буферів (задаються в налаштуваннях у скетчі)
— Канали зв'язку (послідовний порт, вайфай, блютус) взаємозамінні, реалізовані у вигляді окремих подмодулей
— Немає жорстких вимог до деталей протоколу (будується поверх модулів зв'язку)
— Нові команди додаються у вигляді окремих функцій (підпрограм) і реєструються в системі по унікальному імені
— Механізми передачі інформації про виняткових ситуаціях на сторону клієнта

Архітектурно бібліотека розбита на 3 рівня:

— Модулі каналів зв'язку (реалізована робота через послідовний порт, вайфай і блютус в середньостроковий плани): встановлення та обслуговування сполуки, вичленення пакетів потоку вхідних даних, відправка відповіді.
— Модуль реєстрації та виконання команд: реєстрація функції (підпрограми) у вигляді команди, пошук команди по імені, виконання команди.
— Допоміжні контейнерні протоколи: для отримання команд і упаковки відповідей в пакети у форматі JSON.

Канал зв'язку через послідовний порт: babbler_serial
Модуль роботи з командами: babbler_h
Модуль JSON: babbler_json
Модулі щодо незалежні друг від друга: можна використовувати тільки модуль каналу зв'язку для обміну сирими даними і вибудувати з його допомогою власний протокол, до модуля роботи з командами можна підключати інші реалізації каналів зв'язку, модуль JSON можна взагалі не використовувати або поставити на його місце реалізацію модуля роботи з пакетами XML і так далі.

Далі приклади.

Установка бібліотеки
Проект на гітхабі: babbler_h

git clone https://github.com/1i7/babbler_h.git

Або завантажити черговий реліз в архіві

далі помістити підкаталоги babbler_h, babbler_serial, babbler_json в каталог до бібліотек Arduino $HOME/Arduino/libraries, повинно вийти:

$HOME/Arduino/libraries/babbler_h
$HOME/Arduino/libraries/babbler_serial
$HOME/Arduino/libraries_babbler_json

Всі.

Запустити середовище розробки Ардуїнов, в меню Файл/Приклади/babbler_h з'являться приклади:

_1_babbler_hello: проста прошивка: настройка каналу зв'язку, реєстрація команд (вбудовані команди ping і help)
_2_babbler_custom_cmd: додавання власних команд (ввімкнути/вимкнути лампочку)
_3_babbler_cmd_params: команди з параметрами (транспорт для pin_mode/digital_write)
_4_babbler_cmd_devino: набір команд для отримання інформації про пристрій
_5_babbler_custom_handler: власний обробник вхідних даних (те ж, що і _1_babbler_hello, тільки нутрощі зовні)
_6_babbler_reply_json: введення/виведення упакований JSON
_7_babbler_reply_xml: введення рядком, відповідь в XML
babbler_basic_io: сирої питання-відповідь через послідовний порт без інфраструктури модуля команд

Простий приклад: відлуння через послідовний порт
Без використання інфраструктури роботи з командами.

Файл/Приклади/babbler_h/babbler_basic_io.ino

Нам потрібен тільки модуль babbler_serial:

#include "babbler_serial.h"

Буфери для отримання вхідних даних і відправки відповіді. Вхідний пакет (команда параметри) повинен повністю уміщатися в буфер serial_read_buffer (плюс один байт резервуємо на один завершальний нуль). Відповідь повинен повністю уміщатися в буфер serial_write_buffer.

// Розміри буферів для команд читання та запису відповідей
#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];

Функція-обробник вхідних даних: приймає дані в буфері input_buffer, вирішує, що з ними робити, записує відповідь в буфер reply_buffer, повертає кількість байт, записаних в буфер відповіді. Тут весь користувальницький код.

int handle_input(char* input_buffer, int input_len, char* reply_buffer, int reply_buf_size) {
// додамо до вхідних даних завершальний нуль, 
// щоб розглядати їх як коректну рядок
input_buffer[input_len] = 0;

// як-небудь відреагуємо на запит - нехай буде просте ехо
if(reply_buf_size > input_len + 10)
sprintf(reply_buffer, "you say: %s\n", input_buffer);
else
sprintf(reply_buffer, "you are too verbose, dear\n");

return strlen(reply_buffer);
}

Попередні налаштування модуля зв'язку через послідовний порт:

babbler_serial_setup: передаємо буфери для вхідних команд і вихідних відповідей,
packet_filter_newline: фільтр нових пакетів пакети відокремлені переведенням рядка
babbler_serial_set_input_handler: покажчик на функцію-обробник вхідних даних в коді користувача (наш handle_input)

void setup() {
Serial.begin(9600);
Serial.println("Starting babbler-powered device, type something to have a talk");

babbler_serial_set_packet_filter(packet_filter_newline);
babbler_serial_set_input_handler(handle_input);
//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);
}

У головний цикл поміщаємо babbler_serial_tasks: постійно стежимо за послідовним портом, чекаємо вхідні дані. Виклик babbler_serial_tasks не блокує, після нього можна розміщувати будь-яку іншу логіку.

void loop() {
// постійно стежимо за послідовним портом, чекаємо вхідні дані
babbler_serial_tasks();
}

Прошиваємо, відкриваємо Інструменти>Монітор порту, вводимо повідомлення, отримуємо відповіді:

image

Простий приклад: робота з командами
Наступний простий приклад — робота з командами. Реєструємо в прошивці дві вбудовані команди (визначені в модулі babbler_cmd_core.h):

help (список команд, переглянути довідку щодо вибраної команди) та
ping (перевірити, жваво пристрій).

ping:

ping

Повертає «ok»

Команда help:

help

Вивести список команд:

--help list

Вивести список команд з коротким описом

help ім'я_команди

Вивести докладну довідку по команді.

Файл/Приклади/babbler_h/_1_babbler_hello.ino

Тут інфраструктура для реєстрації, пошуку та виконання команд по імені:

#include "babbler.h"

Тут розбір входить командного рядка: рядок розбивається на елементи прогалин, перший елемент — ім'я команди, всі інші параметри.

#include "babbler_simple.h"

Тут визначення команд: help і ping

#include "babbler_cmd_core.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];

Реєструємо команди — додаємо структури CMD_HELP CMD_PING (вони визначені в babbler_cmd_core.h) у глобальний масив BABBLER_COMMANDS. Попутно фіксуємо кількість зареєстрованих команд BABBLER_COMMANDS_COUNT — кількість елементів у масиві BABBLER_COMMANDS (в Сі можна дізнатися розмір масиву, визначеного таким чином, динамічно в тому місці, де це нам потрібно).

/** Зареєстровані команди */
extern const babbler_cmd_t BABBLER_COMMANDS[] = {
// команди з babbler_cmd_core.h
CMD_HELP,
CMD_PING
};

/** Кількість зареєстрованих команд */
extern const int BABBLER_COMMANDS_COUNT = sizeof(BABBLER_COMMANDS)/sizeof(babbler_cmd_t);

За цією ж схемою реєструємо человекочитаемые керівництва для зареєстрованих команд у масиві BABBLER_MANUALS — їх виводить команда help (можете визначити порожній масив без елементів, якщо хочете зекономити пам'ять, але тоді не буде працювати команда help).

/** Керівництва для зареєстрованих команд */
extern const babbler_man_t BABBLER_MANUALS[] = {
// команди з babbler_cmd_core.h
MAN_HELP,
MAN_PING
};

/** Кількість посібників для зареєстрованих команд */
extern const int BABBLER_MANUALS_COUNT = sizeof(BABBLER_MANUALS)/sizeof(babbler_man_t);

Налаштовуємо модуль:

babbler_serial_set_packet_filter babbler_serial_setup — все, як і раніше
— в babbler_serial_set_input_handler відправляємо вказівник на функцію handle_input_simple (з babbler_simple.h, замість власного handle_input) — вона робить всю необхідну роботу: розбирає вхідні рядок за прогалин, відокремлює ім'я команди від параметрів, виконує команду, записує відповідь.

void setup() {
Serial.begin(9600);
Serial.println("Starting babbler-powered device, type help for list of commands");

babbler_serial_set_packet_filter(packet_filter_newline);
babbler_serial_set_input_handler(handle_input_simple);
//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);
}

Головний цикл без змін:

void loop() {
// постійно стежимо за послідовним портом, чекаємо вхідні дані
babbler_serial_tasks();
}

Прошиваємо, відкриваємо Інструменти>Монітор порту, вводимо команди, отримуємо відповіді:

<b>] --help list</b>
help ping
<b>]ping</b>
ok
<b>]help</b>
Commands: 
help
list available commands or show detailed help on selected command
ping
check if device is available
<b>]help ping</b>
ping - manual
NAME
ping - check if device is available
SYNOPSIS
ping
DESCRIPTION
Check if device is available, returns "ok" if device is ok
<b>]help help</b>
help - manual
NAME
help - list available commands or show detailed help on selected command
SYNOPSIS
help
help [cmd_name]
--help list
DESCRIPTION
List available commands or show detailed help on selected command. Running help with no options would list commands with short description.
OPTIONS
cmd_name - command name to show detailed help for
--list - list all available commands separated by space

Додавання власних команд
І, нарешті, додавання власної команди так, щоб її можна легко викликати по імені. Для прикладу додамо дві команди:

ledon (включити лампочку) та
ledoff (вимкнути лампочку)

для включення і виключення світлодіода, підключеного до обраної ніжці мікроконтролера.

Тут все без змін:

#include "babbler.h"
#include "babbler_simple.h"
#include "babbler_cmd_core.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

А ось і відразу корисний код — для кожної команди повинна бути визначена функція з параметрами:

reply_buffer — буфер для запису відповіді
reply_buf_size — розмір буфера reply_buffer (відповідь має в нього вміститися, інакше повідомити про помилку)
argc — кількість аргументів (параметрів) команди
argv — значення аргументів команди (перший аргумент завжди ім'я команди, все за аналогією зі звичайною main)

Варіант для ledon:

/** Реалізація команди 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);
}

Структура babbler_cmd_t для реєстрації команди: ім'я команди і покажчик на функцію:

babbler_cmd_t CMD_LEDON = {
/* ім'я команди */ 
"ledon",
/* покажчик на функцію з реалізацією команди */ 
&cmd_ledon
};

Керівництво для команди — структура babbler_man_t: ім'я команди, короткий опис, докладний опис.

babbler_man_t MAN_LEDON = {
/* ім'я команди */ 
/* command name */
"ledon",
/* короткий опис */ 
/* short description */
"turn led ON",
/* керівництво */ 
/* manual */
"SYNOPSIS\n"
" ledon\n"
"DESCRIPTION\n"
"Turn led ON."
};

Все те ж саме для ledoff:

/** Реалізація команди 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_LEDOFF = {
/* ім'я команди */ 
/* command name */
"ledoff",
/* покажчик на функцію з реалізацією команди */ 
/* pointer to function with command implementation*/ 
&cmd_ledoff
};

babbler_man_t MAN_LEDOFF = {
/* ім'я команди */ 
/* command name */
"ledoff",
/* короткий опис */ 
/* short description */
"turn led OFF",
/* керівництво */ 
/* manual */
"SYNOPSIS\n"
" ledoff\n"
"DESCRIPTION\n"
"Turn led OFF."
};

Реєструємо нові CMD_LEDON CMD_LEDOFF разом з вже знайомим CMD_HELP CMD_PING, аналогічно керівництва.

/** Зареєстровані команди */
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
MAN_HELP,
MAN_PING,

// користувальницькі команди
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, type help for list of commands");

babbler_serial_set_packet_filter(packet_filter_newline);
babbler_serial_set_input_handler(handle_input_simple);
//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();
}

Прошиваємо, відкриваємо Інструменти → Монітор порту, вводимо команди, спостерігаємо за лампочкою:

image

Наживо з залізякою:



Приклад команди з параметрами на самостійну роботу.
Джерело: Хабрахабр

0 коментарів

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