1i7 (1i7) wrote,
1i7
1i7

Categories:

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

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

Первое демо - отправка команд роботу вручную через монитор последовательного порта из среды Ардуино (получилось нечто, похожее на встроенную консольку)




image

Запулил статью на Хабру, перепечатка под катом.



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

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

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

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

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

image

Особенности библиотеки

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

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

— Модули каналов связи (реализована работа через последовательный порт; вайфай и блютус в среднесрочный планах): установка и обслуживание соединения, вычленение пакетов из потока входных данных, отправка ответа.
— Модуль регистрации и исполнения команд: регистрация функции (подпрограммы) в виде команды, поиск команды по имени, выполнение команды.
— Вспомогательные контейнерные протоколы: форматы для получения команд и упаковки ответов (простая командная строка — имя команды и параметры разделены пробелами; команда и ответы в формате JSON).

Канал связи через последовательный порт: babbler_serial
Модуль работы с командами (API регистрации и исполнения команд): babbler_h
Модуль простой командной строки (формат входных данных): babbler_simple
Модуль 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 = {
    /* имя команды */ 
    "ledon",
    /* краткое описание */ 
    "turn led ON",
    /* руководство */ 
    "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 = {
    /* имя команды */ 
    "ledoff",
    /* указатель на функцию с реализацией команды */ 
    &cmd_ledoff
};

babbler_man_t MAN_LEDOFF = {
    /* имя команды */ 
    "ledoff",
    /* краткое описание */ 
    "turn led OFF",
    /* руководство */ 
    "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

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



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



подсветка синтаксиса
Tags: arduino, babbler, chipkit, роботы, типовые задачи, хабра
Subscribe

Posts from This Journal “arduino” Tag

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 9 comments