Интерактивная оболочка для Arduino

Цель

Мне нужно было взаимодействовать с микроконтроллерами (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 ответ
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.

  • 2

    Если вы измените 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. Strings (с заглавной буквой S) доступны на Arduino, но их не следует использовать, поскольку они приводят к фрагментации кучи и могут привести к сбою микроконтроллера (см. здесь или же здесь). В основном нет malloc должен вызываться где угодно, и я застрял со статическими массивами и c-строками.

    — Эрик Думинил

  • Мне очень жаль: все твои std::.... предложения звучат действительно хорошо для общих проектов C ++, но, насколько я могу судить, не относятся к эскизам Arduino или библиотекам микроконтроллеров.

    — Эрик Думинил

  • const работал бы, если бы инициализатор был постоянным выражением. constexpr гарантирует, что это так, и сразу же выдает ошибку, если это не так.

    — JDłuosz

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

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