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

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

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

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

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

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

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



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

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


  1. jMas
    13.11.2016 13:45

    Можно ведь экономя ценное место на плате
    int ledPin = 13;
    
    int readline(int readch, char *buffer, int len)
    {
      static int pos = 0;
      int rpos;
    
      if (readch > 0) {
        switch (readch) {
          case '\n': // Ignore new-lines
            break;
          case '\r': // Return on CR
            rpos = pos;
            pos = 0;  // Reset position index ready for next time
            return rpos;
          default:
            if (pos < len-1) {
              buffer[pos++] = readch;
              buffer[pos] = 0;
            }
        }
      }
      // No end of line has been found, so return -1.
      return -1;
    }
    
    void setup()
    {
      Serial.begin(9600);
      pinMode(ledPin, OUTPUT);
    }
    
    void loop()
    {
      static char buffer[80];
      if (readline(Serial.read(), buffer, 80) > 0) {
        if (strcmp(buffer, "ledon") == 0) {
            digitalWrite(ledPin, HIGH);
            Serial.println("13 pin LED is switched ON.");
        } else if (strcmp(buffer, "ledoff") == 0) {
            digitalWrite(ledPin, LOW);
            Serial.println("13 pin LED is switched OFF.");
        } else if (strcmp(buffer, "help") == 0) {
            Serial.println("ledon - switch on 13 pin LED.");
            Serial.println("ledoff - switch off 13 pin LED.");
        }
      }
    }