Переслать роботу на Ардуине несколько байт через вайфай, блютус, последовательный порт или любой другой канал связи в виде команды, а потом принять несколько байт в качестве ответа труда не составляет: достаточно скачать скетч с примером обмена данными «здравствуй мир» и вставить в него несколько строк своего кода, который будет выполнять желаемые действия.
Однако с развитием проекта область вспомогательного кода, который отвечает за связь с внешним миром, раздувается: появляется логика, отделяющая один пакет данных от другого, разрастается лес проверок, что за команда пришла, какие у нее параметры, как её правильно выполнить, что делать, если пакет данных не корректен, если данные пришли не полностью, если они не умещаются в отведенных для них буфер памяти и так далее. Код, обслуживающий вспомогательную логику, переплетается с главным кодом, выполняющим интересную и полезную работу. Заменить один канал связи на другой (например, добавить к последовательному порту вайфай) без переработки накопившейся кодовой базы становится весьма проблематично.
Долгое время я таскал такой код из одного проекта в другой, новые улучшения и исправления приходилось обновлять во всех проектах параллельно. Это утомительно, чревато появлением новых ошибок и не всегда возможно. Наконец пришло время вынести всю эту логику в отдельную библиотеку.
Исходная задача: упростить процесс создания прошивки для роботов, которые будут работать в режиме «вопрос-ответ». Главный скетч должен содержать полезный код (что, собственно, должен делать робот) и минимальное количество вспомогательных конструкций. Все вспомогательные транспортно-протокольные блоки окуклить в библиотеку и вынести за пределы внимания инженера.
В качестве побочного эффекта получилась своеобразная командная строка, работающая внутри Ардуины, если подключиться к ней через монитор последовательного порта и отправлять команды вручную:
Особенности библиотеки
— Работа в режиме вопрос-ответ
— Максимальные размеры входящей команды и ответа ограничены размером буферов (задаются в настройках в скетче)
— Каналы связи (последовательный порт, вайфай, блютус) взаимозаменяемы, реализованы в виде отдельных подмодулей
— Нет жестких требований к деталям протокола (строится поверх модулей связи)
— Новые команды добавляются в виде отдельных функций (подпрограмм) и регистрируются в системе по уникальному имени
— Механизмы передачи информации об исключительных ситуациях на сторону клиента
Архитектурно библиотека разбита на 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(); }
Прошиваем, открываем Инструменты>Монитор порта, вводим команды, получаем ответы:
<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(); }
Прошиваем, открываем Инструменты → Монитор порта, вводим команды, наблюдаем за лампочкой:
Вживую с железкой:
→ Пример команды с параметрами на самостоятельную работу.
ссылка на оригинал статьи https://habrahabr.ru/post/315084/
Добавить комментарий