[Из песочницы] Консолька в роботе на Ардуине

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

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

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

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

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

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();
}

Прошиваем, открываем Инструменты>Монитор порта, вводим команды, получаем ответы:
]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();
}

Прошиваем, открываем Инструменты → Монитор порта, вводим команды, наблюдаем за лампочкой:

image

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

→ Пример команды с параметрами на самостоятельную работу.

Комментарии (0)

© Habrahabr.ru