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

от автора

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

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

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

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

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

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

Вживую с железкой:

Пример команды с параметрами на самостоятельную работу.
ссылка на оригинал статьи https://habrahabr.ru/post/315084/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *