ESP32で対話型コンソール

概要

pingのサンプルコードをみていたら、対話型コンソールを実現するesp_consoleを利用していて、使いやすそうだったので紹介したいと思います。

Consoleの概要

Console - ESP32 - — ESP-IDF Programming Guide latest documentation

上記に公式ドキュメントがあります。コマンドラインなどで利用できるようにカーソルやバックスペースなどのラインエディタの機能と、GNUっぽいコマンド引数を解析する部分があり好きに組み合わせて利用できるみたいです。

Arduinoでのスケッチ例

#include "esp_console.h"
#include "esp_event.h"
#include "argtable3/argtable3.h"
#include <WiFi.h>

static esp_console_repl_t *s_repl = NULL;

void setup() {
  esp_console_cmd_t command = {};
  esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();

#if CONFIG_ESP_CONSOLE_UART
  esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &s_repl));
#elif CONFIG_ESP_CONSOLE_USB_CDC
  esp_console_dev_usb_cdc_config_t cdc_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&cdc_config, &repl_config, &s_repl));
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
  esp_console_dev_usb_serial_jtag_config_t usbjtag_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&usbjtag_config, &repl_config, &repl));
#endif

  // reset
  command = {};
  command.command = "reset";
  command.help = "Resetting ESP32";
  command.func = [](int argc, char **argv) -> int {
    ESP.restart();
    return 0;
  };
  esp_console_cmd_register(&command);

  // wifi
  static struct {
    struct arg_str *ssid = arg_str0(NULL, NULL, "<ssid>", "SSID");
    struct arg_str *key = arg_str0(NULL, NULL, "<key>", "KEY");
    struct arg_end *end = arg_end(1);
  } wifi_args;
  command = {};
  command.argtable = &wifi_args;
  command.command = "wifi";
  command.help = "connect to wifi";
  command.func = [](int argc, char **argv) -> int {
    int nerrors = arg_parse(argc, argv, (void **)&wifi_args);
    if (wifi_args.ssid->count != 0) {
      printf("SSID = %s\n", wifi_args.ssid->sval[0]);
      printf("KEY = %s\n", wifi_args.key->sval[0]);
      WiFi.begin(wifi_args.ssid->sval[0], wifi_args.key->sval[0]);
    } else {
      printf("SSID = [last ssid]\n");
      printf("KEY = [last key]\n");
      WiFi.begin();
    }
    return 0;
  };
  esp_console_cmd_register(&command);

  // ip
  command = {};
  command.command = "ip";
  command.help = "IP Address";
  command.func = [](int argc, char **argv) -> int {
    printf("IP Address  : %s\n", WiFi.localIP().toString().c_str());
    return 0;
  };
  esp_console_cmd_register(&command);

  // time
  command = {};
  command.command = "time";
  command.help = "local time";
  command.func = [](int argc, char **argv) -> int {
    struct tm timeInfo;
    if (getLocalTime(&timeInfo)) {
      printf("Local Time  : ");
      printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
      printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
    }
    return 0;
  };
  esp_console_cmd_register(&command);

  // ntp
  static struct {
    struct arg_int *tz = arg_int0("tz", "tz,timezone", "<tz>", "TIME ZONE(-9)");
    struct arg_str *host = arg_str0(NULL, NULL, "<host>", "NTP Server");
    struct arg_end *end = arg_end(1);
  } ntp_args;
  command = {};
  command.argtable = &ntp_args;
  command.command = "ntp";
  command.help = "set ntp";
  command.func = [](int argc, char **argv) -> int {
    int nerrors = arg_parse(argc, argv, (void **)&ntp_args);
    printf("ntp_args.tz->count = %d\n", ntp_args.tz->count);
    printf("ntp_args.host->count = %d\n", ntp_args.host->count);
    int tz = 9;
    if (ntp_args.tz->count != 0) {
      tz = (uint32_t)(ntp_args.tz->ival[0]);
    }
    printf("timezone = %d\n", tz);
    if (ntp_args.host->count != 0) {
      printf("host = %s\n", ntp_args.host->sval[0]);
      configTime(tz * 60 * 60, 0, ntp_args.host->sval[0]);
    } else {
      printf("host = ntp.jst.mfeed.ad.jp, ntp.nict.jp, time.google.com\n");
      configTime(tz * 60 * 60, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "time.google.com");
    }
    return 0;
  };
  esp_console_cmd_register(&command);

  // start console REPL
  ESP_ERROR_CHECK(esp_console_start_repl(s_repl));
}

void loop() {
  delay(1);
}

かなりシンプルにしたスケッチ例になります。

  esp_console_cmd_t command = {};
  esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();

#if CONFIG_ESP_CONSOLE_UART
  esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &s_repl));
#elif CONFIG_ESP_CONSOLE_USB_CDC
  esp_console_dev_usb_cdc_config_t cdc_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&cdc_config, &repl_config, &s_repl));
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
  esp_console_dev_usb_serial_jtag_config_t usbjtag_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&usbjtag_config, &repl_config, &repl));

まず、ESP-IDFの機能なのでSerialクラスが利用できません。そのため呪文のようなコードはそのまま利用する必要がありました。Serialクラスで初期化するとバッティングするのでSerialは呼び出さないようにしてください。

  // reset
  command = {};
  command.command = "reset";
  command.help = "Resetting ESP32";
  command.func = [](int argc, char **argv) -> int {
    ESP.restart();
    return 0;
  };
  esp_console_cmd_register(&command);

上記がコマンドを登録する最小部分です。commandでコマンド名を設定して、helpにコマンドの概要を登録します。funcにコマンドの内容を定義して、esp_console_cmd_registerで登録します。

これでresetコマンドを呼び出すことができます。この仕組みで便利なのがhelpコマンドを打つことで呼び出すことができるコマンドの一覧と引数の情報も自動生成されることです。

  // wifi
  static struct {
    struct arg_str *ssid = arg_str0(NULL, NULL, "<ssid>", "SSID");
    struct arg_str *key = arg_str0(NULL, NULL, "<key>", "KEY");
    struct arg_end *end = arg_end(1);
  } wifi_args;
  command = {};
  command.argtable = &wifi_args;
  command.command = "wifi";
  command.help = "connect to wifi";
  command.func = [](int argc, char **argv) -> int {
    int nerrors = arg_parse(argc, argv, (void **)&wifi_args);
    if (wifi_args.ssid->count != 0) {
      printf("SSID = %s\n", wifi_args.ssid->sval[0]);
      printf("KEY = %s\n", wifi_args.key->sval[0]);
      WiFi.begin(wifi_args.ssid->sval[0], wifi_args.key->sval[0]);
    } else {
      printf("SSID = [last ssid]\n");
      printf("KEY = [last key]\n");
      WiFi.begin();
    }
    return 0;
  };
  esp_console_cmd_register(&command);

つぎにコマンドライン引数がある例になります。「wifi <ssid> <key>」でwifiに接続するコマンドになります。

  static struct {
    struct arg_str *ssid = arg_str0(NULL, NULL, "<ssid>", "SSID");
    struct arg_str *key = arg_str0(NULL, NULL, "<key>", "KEY");
    struct arg_end *end = arg_end(1);
  } wifi_args;

上記がコマンドライン引数の定義です。細かいところはあとで説明しますが、文字列を2つ受け取っています。最後のarg_endを設定しないとハングアップするので注意してください。

    int nerrors = arg_parse(argc, argv, (void **)&wifi_args);
    if (wifi_args.ssid->count != 0) {
      printf("SSID = %s\n", wifi_args.ssid->sval[0]);
      printf("KEY = %s\n", wifi_args.key->sval[0]);
      WiFi.begin(wifi_args.ssid->sval[0], wifi_args.key->sval[0]);
    } else {
      printf("SSID = [last ssid]\n");
      printf("KEY = [last key]\n");
      WiFi.begin();
    }

引数を取得するのはarg_parseを呼び出すだけです。ただし注意しないといけないのは複数のパラメータが指定される可能性があるので、まずは何個のパラメータがあるかをチェックして、個数分の処理を行う必要があります。「command -i 1 -i 2 -i 3」たとえばこのようなコマンドだと-iのパラメータが3つ設定されて呼び出されます。

引数の指定方法について

A Tutorial Introduction of Argtable3
Argtable: an ANSI C library for command-line parsing

上記が内部で利用しているargtableの公式サイトだと思われますが、わかりにくいです。

Just a moment...

上記のmanページが比較的わかりやすかったです。

引数の種類

種類備考
arg_litリテラル(オプションのみで引数はなし)
arg_int数値
arg_dbl浮動小数点(REAL/DOUBLE)
arg_str文字列
arg_rex正規表現にマッチした文字列
arg_filepathを含むファイル名の文字列
arg_date日付/時刻
arg_remコメント
arg_end引数の最後を表す

ちょっとわかりにくいのですが、arg_lit(リテラル)は-bとかオプションのみで引数を取らないものになります。ほかはなんとかわかるかな?

  // ntp
  static struct {
    struct arg_int *tz = arg_int0("tz", "tz,timezone", "<tz>", "TIME ZONE(-9)");
    struct arg_str *host = arg_str0(NULL, NULL, "<host>", "NTP Server");
    struct arg_end *end = arg_end(1);
  } ntp_args;

上記はntpの時刻合わせコマンドですが、intでタイムゾーンを受け取っていますが本来は一時間未満のタイムゾーンもありますのでintで受け取る場合は秒単位で受け取るか、arg_dblかarg_dateで受け取るのが正しいです。

引数のパラメータ

  // ntp
  static struct {
    struct arg_int *tz = arg_int0("tz", "tz,timezone", "<tz>", "TIME ZONE(-9)");
    struct arg_str *host = arg_str0(NULL, NULL, "<host>", "NTP Server");
    struct arg_end *end = arg_end(1);
  } ntp_args;
  command = {};
  command.argtable = &ntp_args;
  command.command = "ntp";
  command.help = "set ntp";

再びntpコマンドですが、上記の設定はhelpでは以下の表示となります。

ntp  [-t <tz>] [<host>]
  set ntp
  -t, -z, --tz, --timezone=<tz>  TIME ZONE(-9)
        <host>  NTP Server

1つ目の引数は-tで<tz>、2つ目の引数は<host>を表しています。タイムゾーンでは「”tz”, “tz,timezone”」と指定していますが、それが下の方にある「-t, -z, –tz, –timezone=<tz>」を表しています。”tz”は1文字での省略形になります。-tzではなく、-tと-zに対応しています。”tz,timezone”は省略していないオプション名になります。複数ある場合にはカンマで区切ります。–tzと–timezoneが指定されています。

パラメータの数について

arg_int0指定されないことがあるオプション
arg_int1必ず1つ指定されることを前提とするオプション
arg_intn複数個指定される可能性があるオプション

数値型のarg_intでもさらに3つオプションの指定があります。

  static struct {
    struct arg_int *i0 = arg_int0("i", NULL, "<i>", "i");
    struct arg_int *i1 = arg_int1("j", NULL, "<j>", "j");
    struct arg_int *in = arg_intn("k", NULL, "<k>", 2, 4, "k");
    struct arg_end *end = arg_end(1);
  } i_args;

上記の指定をした場合にはhelp出力は以下になります。

test  [-i <i>] -j <j> -k <k> -k <k> [-k <k>] [-k <k>]
  test
        -i <i>  i
        -j <j>  j
        -k <k>  k

上記のような出力になります。-iはarg_int0ですので、省略可能なオプションのため[-i <i>]とカッコで囲われて出力されています。-jはarg_int1で1つだけ指定しますので-j <j>と表示されています。-kはarg_intnで複数指定できますので複数個表示されています。ただし、arg_intn(“k”, NULL, “<k>”, 2, 4, “k”)と2と4を指定していますので、最低2個指定されて最大4個までのオプションになるようです。

まとめ

esp_consoleはいろいろできそうなのですが、結構癖があります。DOSっぽいシステムを作って、SDカードなどにプログラムを入れておいて、LovyanLauncherなどのようにプログラムを切り替えて実行するシェル部分に使い勝手が良さそうです。

esp-idf/examples/peripherals/i2c/i2c_tools at e4f167df2504544d6f46655228634549c3d0d9c2 · espressif/esp-idf
Espressif IoT Development Framework. Official development framework for Espressif SoCs. - espressif/esp-idf

ESP-IDFには複数のサンプルがあるので中をみてみるのもいいと思います。上記はI2Cを対話型で利用するためのツールになります。

コメント