Цель
Мне нужно было взаимодействовать с микроконтроллерами (ESP8266 & ESP32) через последовательный интерфейс, поэтому я написал небольшую интерактивную оболочку с шаблон команды.
Определенные команды могут либо не принимать никаких параметров, либо иметь один параметр int32.
API должен быть понятным и простым в использовании, код должен полагаться только на Arduino.h
, и данные не должны храниться в куче, чтобы избегать фрагментации.
Код
Полный код доступен на этом Репозиторий GitHub.
Заголовок
В command_invoker.h
заголовок:
#ifndef COMMAND_INVOKER_H_INCLUDED
#define COMMAND_INVOKER_H_INCLUDED
#include <Arduino.h>
#define MAX_COMMAND_SIZE 30
/** Other scripts can use this invoker, in order to define commands, via callbacks.
* Those callbacks can then be used to send commands to the sensor (e.g. reset, calibrate, night mode, ...)
* The callbacks can either have no parameter, or one int32_t parameter.
*/
namespace command_invoker {
void defineCommand(const char *name, void (*function)(void), const __FlashStringHelper *doc_fstring);
void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring);
void execute(const char *command_line);
}
#endif
Библиотека
соответствующий command_invoker.cpp
является:
#include "command_invoker.h"
namespace command_invoker {
const uint8_t MAX_COMMANDS = 20;
uint8_t commands_count = 0;
struct Command {
const char *name;
union {
void (*intFunction)(int32_t);
void (*voidFunction)(void);
};
const char *doc;
bool has_parameter;
};
Command commands[MAX_COMMANDS];
//NOTE: Probably possible to DRY (with templates?)
void defineCommand(const char *name, void (*function)(void), const __FlashStringHelper *doc_fstring) {
const char *doc = (const char*) doc_fstring;
if (commands_count < MAX_COMMANDS) {
commands[commands_count].name = name;
commands[commands_count].voidFunction = function;
commands[commands_count].doc = doc;
commands[commands_count].has_parameter = false;
commands_count++;
} else {
Serial.println(F("Too many commands have been defined."));
}
}
void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring) {
const char *doc = (const char*) doc_fstring;
if (commands_count < MAX_COMMANDS) {
commands[commands_count].name = name;
commands[commands_count].intFunction = function;
commands[commands_count].doc = doc;
commands[commands_count].has_parameter = true;
commands_count++;
} else {
Serial.println(F("Too many commands have been defined."));
}
}
/*
* Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into a function_name and an argument.
* Returns 0 if both are found, 1 if there is a problem and 2 if no argument is found.
*/
uint8_t parseCommand(const char *command, char *function_name, int32_t &argument) {
char split_command[MAX_COMMAND_SIZE];
strlcpy(split_command, command, MAX_COMMAND_SIZE);
char *arg;
char *part1;
part1 = strtok(split_command, " ");
if (!part1) {
Serial.println(F("Received empty command"));
// Empty string
return 1;
}
strlcpy(function_name, part1, MAX_COMMAND_SIZE);
arg = strtok(NULL, " ");
uint8_t code = 0;
if (arg) {
char *end;
argument = strtol(arg, &end, 10);
if (*end) {
// Second argument isn't a number
code = 2;
}
} else {
// No argument
code = 2;
}
return code;
}
int compareCommandNames(const void *s1, const void *s2) {
struct Command *c1 = (struct Command*) s1;
struct Command *c2 = (struct Command*) s2;
return strcmp(c1->name, c2->name);
}
void listAvailableCommands() {
qsort(commands, commands_count, sizeof(commands[0]), compareCommandNames);
for (uint8_t i = 0; i < commands_count; i++) {
Serial.print(" ");
Serial.print(commands[i].name);
Serial.print(commands[i].doc);
Serial.println(".");
}
}
/*
* Tries to find the corresponding callback for a given command. Name and number of arguments should fit.
*/
void execute(const char *command_line) {
char function_name[MAX_COMMAND_SIZE];
int32_t argument = 0;
bool has_argument;
has_argument = (parseCommand(command_line, function_name, argument) == 0);
for (uint8_t i = 0; i < commands_count; i++) {
if (!strcmp(function_name, commands[i].name) && has_argument == commands[i].has_parameter) {
Serial.print(F("Calling : "));
Serial.print(function_name);
if (has_argument) {
Serial.print(F("("));
Serial.print(argument);
Serial.println(F(")"));
commands[i].intFunction(argument);
} else {
Serial.println(F("()"));
commands[i].voidFunction();
}
return;
}
}
Serial.print(F("'"));
Serial.print(command_line);
Serial.println(F("' not supported. Available commands :"));
listAvailableCommands();
}
}
Эскиз
Наконец, вот скетч Arduino, чтобы использовать эту библиотеку:
/***************************************************************************************************
* Small interactive shell via Serial interface for ESP32 and ESP8266, based on the command pattern.
***************************************************************************************************/
#include "command_invoker.h"
/**
* Some example functions, which could be defined in separate libraries.
*/
void multiplyBy2(int32_t x) {
Serial.print(x);
Serial.print(" * 2 = ");
Serial.println(2 * x);
}
void controlLED(int32_t onOff) {
digitalWrite(LED_BUILTIN, onOff);
}
/**
* Setup
*/
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
// Define commands. Could be done in separate libraries (e.g. MQTT, LoRa, Webserver, ...)
command_invoker::defineIntCommand("led", controlLED, F(" 1/0 (LED on/off)"));
command_invoker::defineIntCommand("double", multiplyBy2, F(" 123 (Doubles the input value)"));
// Commands can also be created with lambdas.
command_invoker::defineCommand("reset", []() {
ESP.restart();
}, F(" (restarts the microcontroller)"));
// Simple example. Turn LED on at startup.
command_invoker::execute("led 1");
Serial.println(F("Console is ready!"));
Serial.print(F("> "));
}
/*
* Saves bytes from Serial.read() until enter is pressed, and tries to run the corresponding command.
* http://www.gammon.com.au/serial
*/
void processSerialInput(const byte input_byte) {
static char input_line[MAX_COMMAND_SIZE];
static unsigned int input_pos = 0;
switch (input_byte) {
case 'n': // end of text
Serial.println();
input_line[input_pos] = 0;
command_invoker::execute(input_line);
input_pos = 0;
Serial.print(F("> "));
break;
case 'r': // discard carriage return
break;
case 'b': // backspace
if (input_pos > 0) {
input_pos--;
Serial.print(F("b b"));
}
break;
default:
// keep adding if not full ... allow for terminating null byte
if (input_pos < (MAX_COMMAND_SIZE - 1)) {
input_line[input_pos++] = input_byte;
Serial.print((char) input_byte);
}
break;
}
}
/**
* Loop and wait for serial input. Commands could also come from webserver or MQTT, for example.
*/
void loop() {
while (Serial.available() > 0) {
processSerialInput(Serial.read());
}
delay(50);
}
Применение
Теперь можно отправлять команды непосредственно в мониторе PlatformIo или в последовательном мониторе Arduino IDE:
Console is ready!
> test
'test' not supported. Available commands :
double 123 (Doubles value).
led 1/0 (LED on/off).
reset (restarts the microcontroller).
> led 1
Calling : led(1)
> led 0
Calling : led(0)
> double 12345
Calling : double(12345)
12345 * 2 = 24690
> double
'double' not supported. Available commands :
double 123 (Doubles value).
led 1/0 (LED on/off).
reset (restarts the microcontroller).
> reset
Calling : reset()
Вопросов
Эта библиотека работает нормально, но я все еще новичок в C / C ++ и был бы признателен любой Обратная связь.
В частности, я буду рад:
- удалите повторяющийся код в
defineCommand
а такжеdefineIntCommand
- разрешить командам иметь строковый аргумент
- разрешить командам иметь определенный числовой аргумент (например,
bool
илиuint8_t
и не только гипсint32_t
) - убедитесь, что нигде нет переполнения буфера
- используйте как можно меньше памяти.
К сожалению, много хороших предложений C ++ (например, с std::string
, std::vector
или же std::map
), похоже, не применимы к этому проекту, потому что типы данных недоступны в Arduino. Вот почему код может больше походить на C, чем на C ++.
1 ответ
#определять
Не использовать #define
для констант!
#define MAX_COMMAND_SIZE 30
должно быть:
constexpr size_t MAX_COMMAND_SIZE = 30;
(У меня есть обсуждение этого вопроса на Сообщение проекта кода)
string_view
void defineCommand(const char *name,
Рассмотрите возможность использования std::string_view
вместо этого, что может эффективно возьмите лексический строковый литерал, например "MyName"
или std::string
объект. Его также намного проще использовать, например, сопоставить с ==
скорее, чем strcmp
.
зарезервированные имена
__FlashStringHelper
— это зарезервированное имя, которое должно использоваться только внутренними компонентами компилятора, такими как реализация стандартной библиотеки и скрытые встроенные функции. Я не вижу, чтобы вы определяли этот символ, так что, возможно, это не ваша вина, а в прилагаемом заголовке. Это все еще Undefined Behavior, если только компилятор, который вы используете, не был написан теми же людьми, которые предоставили заголовок, и даже в этом случае это не должно быть пользовательским именем.
прагма один раз
#ifndef COMMAND_INVOKER_H_INCLUDED
#define COMMAND_INVOKER_H_INCLUDED
Каждый компилятор, который я использовал, принимает #pragma once
хотя официально это не входит в стандарт. Я использую это вместо такой блокировки и полагаю, что, если это когда-нибудь понадобится, его можно заменить на #ifndef
и т.д. простым скриптом; и за 30 лет в этом не было необходимости.
какие #includes вам нужны?
Вы не в том числе <cstdint>
и я не вижу using std::int32_t;
либо. Я полагаю Arduino.h
не только включает некоторые стандартные заголовки, но и загрязняет глобальное пространство имен. Это задокументированный как тяга в определенный стандарт входит? Заголовки обычно не определяют это официально и могут точно изменить то, что они втягивают. Вы должны включить стандартные заголовки, которые вам нужны в этот файл.
(пустота)
void (*function)(void)
По словам Бьярна Страструпа, «(пустота) — это мерзость». Пишутся пустые списки параметров ()
. В (void)
конструкция предназначена для совместимости с ANSI C, который ввел ее по причинам, не относящимся к C ++.
контейнеры
if (commands_count < MAX_COMMANDS) {
commands[commands_count].name = name;
commands[commands_count].voidFunction = function;
commands[commands_count].doc = doc;
commands[commands_count].has_parameter = false;
commands_count++;
} else {
Serial.println(F("Too many commands have been defined."));
Не обращайтесь к commands[commands_count]
чтобы добавить вещи в список. Использовать std::vector
а не массив фиксированного размера! У вас не должно быть отдельного commands
а также commands_count
переменные, поскольку они являются одним объектом, и код коллекции знает, как себя поддерживать. А также MAX_COMMAND_SIZE
просто не нужен, так как vector
растет по мере необходимости.
Используйте конструктор или агрегатный инициализатор, чтобы заполнить Command
экземпляр, а затем push_back
к vector
.
Ваш has_parameter
член привязан к использованию объединения, поэтому сделайте разные формы разными конструкторами Command
а не отдельные функции, как у вас. Эти функции неявно имеют дело с вашей «коллекцией», о чем я и говорил здесь изначально. Он должен быть автономным — конструктор не знает о дальнейшем использовании объекта, например о том, какие глобальные переменные у вас есть в программе!
Command (const char *name, void (*function)(void), const __FlashStringHelper *doc_fstring); // one constructor
Command (const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring); // another constructor
// ...
std::vector<Command> commands;
// ...
commands.push_back (Command{"led", controlLED, F(" 1/0 (LED on/off)")});
// or use `emplace_back`
commands.emplace_back ("double", multiplyBy2, F(" 123 (Doubles the input value)");
союзы
Вы создали свой собственный дискриминированный союз с простым C union
и has_parameter
член действует как ключ. Вместо этого используйте std::variant
который делает это за вас.
параметры и указатели
uint8_t parseCommand(const char *command, char *function_name, int32_t &argument) {
Является function_name
выход? Четко argument
является параметром «out». И у вас тоже есть функция return, но не совсем понятно, для чего она используется.
У вас должны быть результаты функции returned
в качестве возвращаемых значений, а не использовать так называемые «исходящие» параметры.
В function_name
заполняет строковый буфер, предоставленный вызывающей стороной, который подвержен проблемам с длиной и распределением. Просто верните эту часть результата как std::string
! Это C ++, а не C.
char split_command[MAX_COMMAND_SIZE];
strlcpy(split_command, command, MAX_COMMAND_SIZE);
char *arg;
char *part1;
part1 = strtok(split_command, " ");
да … тебе не нужно MAX_COMMAND_SIZE
вообще, и копирование строк с функциями C не должно быть необходимым. Я думаю, вам нужно было сделать копию, потому что strtok
изменяет ввод … не используйте эту функцию, а используйте функции стандартной библиотеки C ++. Есть члены std::string
найти конкретного персонажа, а также «STL» алгоритмы которые определены отдельно от любого контейнера.
сравнить имена
int compareCommandNames (const void * s1, const void * s2) {struct Command c1 = (struct Command) s1; struct Command c2 = (struct Command) s2; return strcmp (c1-> name, c2-> name); }
Почему вы проходите в Command
аргументы как void*
? Не делай этого!
Ах … qsort(commands, commands_count, sizeof(commands[0]), compareCommandNames);
Использовать std::sort
вместо. Это не только типобезопасно, но и намного быстрее! Кроме того, если вы начали использовать вещи в Command
struct, которые не являются простыми C «PODS», тогда это приведет к серьезной неисправности.
просто найди это
for (uint8_t i = 0; i < commands_count; i++) {
if (!strcmp(function_name, commands[i].name) && has_argument == commands[i].has_parameter) {
Просто используйте std::find
найти нужную команду по имени. Возможно, вместо vector
чтобы заменить массив, вы хотите использовать map
вместо? Затем сделайте имя ключом, а не частью структуры Command.
Если вы измените
MAX_COMMAND_SIZE
, рекомендуется переименовать его. Установленное соглашение заключается в том, чтоALL_CAPS
имена для макросов.— Тоби Спейт
Большое спасибо. Я попытался заменить
#define MAX_COMMAND_SIZE
с участиемconst size_t MAX_COMMAND_SIZE
, но компилятор жаловался наinput_line[MAX_COMMAND_SIZE];
. Вот почемуconstexpr
нужно использовать, не так ли?— Эрик Думинил
std::string
часто предлагается для проектов C ++, которые я видел, но, по-видимому, недоступен на Arduinos.String
s (с заглавной буквой S) доступны на Arduino, но их не следует использовать, поскольку они приводят к фрагментации кучи и могут привести к сбою микроконтроллера (см. здесь или же здесь). В основном нетmalloc
должен вызываться где угодно, и я застрял со статическими массивами и c-строками.— Эрик Думинил
Мне очень жаль: все твои
std::....
предложения звучат действительно хорошо для общих проектов C ++, но, насколько я могу судить, не относятся к эскизам Arduino или библиотекам микроконтроллеров.— Эрик Думинил
const
работал бы, если бы инициализатор был постоянным выражением.constexpr
гарантирует, что это так, и сразу же выдает ошибку, если это не так.— JDłuosz
Показывать 3 больше комментариев