[Из песочницы] Консолька в роботе на Ардуине
Однако с развитием проекта область вспомогательного кода, который отвечает за связь с внешним миром, раздувается: появляется логика, отделяющая один пакет данных от другого, разрастается лес проверок, что за команда пришла, какие у нее параметры, как её правильно выполнить, что делать, если пакет данных не корректен, если данные пришли не полностью, если они не умещаются в отведенных для них буфер памяти и так далее. Код, обслуживающий вспомогательную логику, переплетается с главным кодом, выполняющим интересную и полезную работу. Заменить один канал связи на другой (например, добавить к последовательному порту вайфай) без переработки накопившейся кодовой базы становится весьма проблематично.
Долгое время я таскал такой код из одного проекта в другой, новые улучшения и исправления приходилось обновлять во всех проектах параллельно. Это утомительно, чревато появлением новых ошибок и не всегда возможно. Наконец пришло время вынести всю эту логику в отдельную библиотеку.
Исходная задача: упростить процесс создания прошивки для роботов, которые будут работать в режиме «вопрос-ответ». Главный скетч должен содержать полезный код (что, собственно, должен делать робот) и минимальное количество вспомогательных конструкций. Все вспомогательные транспортно-протокольные блоки окуклить в библиотеку и вынести за пределы внимания инженера.
В качестве побочного эффекта получилась своеобразная командная строка, работающая внутри Ардуины, если подключиться к ней через монитор последовательного порта и отправлять команды вручную:
Особенности библиотеки
— Работа в режиме вопрос-ответ
— Максимальные размеры входящей команды и ответа ограничены размером буферов (задаются в настройках в скетче)
— Каналы связи (последовательный порт, вайфай, блютус) взаимозаменяемы, реализованы в виде отдельных подмодулей
— Нет жестких требований к деталям протокола (строится поверх модулей связи)
— Новые команды добавляются в виде отдельных функций (подпрограмм) и регистрируются в системе по уникальному имени
— Механизмы передачи информации об исключительных ситуациях на сторону клиента
Архитектурно библиотека разбита на 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();
}
Прошиваем, открываем Инструменты>Монитор порта, вводим сообщения, получаем ответы:
Простой пример: работа с командами
Следующий простой пример — работа с командами. Регистрируем в прошивке две встроенные команды (определены в модуле 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();
}
Прошиваем, открываем Инструменты>Монитор порта, вводим команды, получаем ответы:
]help --list
help ping
]ping
ok
]help
Commands:
help
list available commands or show detailed help on selected command
ping
check if device is available
]help ping
ping - manual
NAME
ping - check if device is available
SYNOPSIS
ping
DESCRIPTION
Check if device is available, returns "ok" if device is ok
]help help
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();
}
Прошиваем, открываем Инструменты → Монитор порта, вводим команды, наблюдаем за лампочкой:
Вживую с железкой:
→ Пример команды с параметрами на самостоятельную работу.