Help us understand the problem. What is going on with this article?

Arduinoでテンプレートライブラリを使って、シリアル通信で動くシェルを作る

More than 3 years have passed since last update.

せっかくC++で書けるのだから、ちょっと大きめのスケッチやライブラリを作っていると、やはりSTL(Standard Template Library)が使いたくなりますよね?
そんな人のためのSTLのAVRマイコンへの移植版が、この

The STL for AVR with C++ streams

です。
C++の標準ライブラリの元にもなったSG I STLをベースに移植を行ったらしく、SGI STLにはストリームクラスが無いのでわざわざuClibc++から取ってきたり、更にはArduinoユーザーのためにシリアル入出力ストリームクラスや、LCD出力クラスなども用意してくれる親切っぷり。
これを使わない手はありません。

1. インストール

トップページからDownloads -> Arduino downloads と進んだ先にあります。2015/5/25時点での最新版は1.1.1です。
開発環境がeclipseなら、適当な場所に解凍してavr-stl/includeをインクルードパスに追加しましょう。
Arduino IDEの場合は、ヘッダファイルを丸ごと${arduinoのインストール先}/hardware/tools/avr/avr/includeにぶちこめばOKです。

2. シェルクラスの作成

Arduino向けのシリアル入出力ストリームクラスを使って、ホストとシリアル通信しながら動く簡単なシェルを作ってみます。
プロンプトを表示し、入力されたコマンドをパースして引数のリストを取得できるようにします。

2-1. 前準備

シェルクラスを書く前に、ちょこっとこのAVR-STLに書き足します。
Arduinoでシリアル通信で入力を行う時は、HardwareSerialクラスのインスタンスであるSerialを使ってこのように書くと思います。

HardwareSerialクラスを使う場合
void loop() {
    char c;
    if (Serial.available() > 0) {
        c = Serial.read();
        Serial.print("input: ");
        Serial.println(c);
    }
}

実は、serstreamに定義されている入力シリアルストリームクラスihserialstreamには、このSerial.available()に相当するメンバ関数がありません。これだと、「何か入力があった時だけ処理する」ということができず大変不便なので、以下のように追記してやります。

serstream
/*
 * Input stream
 */

    template <class charT, class traits, class Tserial> class basic_iserialstream
        : public basic_istream<charT,traits>
    {
    public:

    ~~~ここから~~~

    /*
     * get how many characters available
     */
        int available() {
            return sb.serial().available();
        }

    ~~~ここまで~~~

    }

ihserialstreamが内部で保持しているHardwareSerialのインスタンスからavailable()を呼ぶようにしました。
これを用いて先ほどのコードを書き換えると、こうなります。

入出力ストリームクラスを使う場合
// 定義済みのHardwareSerialインスタンスのSerialを使って初期化
std::ohserialstream serout(Serial);
std::ihserialstream serin(Serial);

void loop() {

    char c;
    if (serin.available() > 0) {
        serin >> c;
        serout << "input: " << c << std::crlf;
    }
}

2-2. SerialShellクラス

SerialShellクラスの中身はこんな感じです。

  • キーボード入力を受け付け、リターンが押されるまでの文字列を保持
  • 空白文字で分割し、引数として配列に格納

が主な動作になります。

SerialShell.h
#ifndef SERIAL_SHELL_
#define SERIAL_SHELL_

class SerialShell {
private:
    std::ohserialstream& serout;    // 出力用ストリームの参照
    std::ihserialstream& serin;     // 入力用ストリームの参照
    std::string buffer;
    std::vector<std::string> args;
    bool is_prompting;

public:
    SerialShell(std::ohserialstream* serout, std::ihserialstream* serin);
    virtual ~SerialShell();

    int readCommand();
    std::vector<std::string> getArgs();
    void printArgs();

private:
    void parseCommand();
};

#endif /* SERIAL_SHELL_ */
SerialShell.cpp
#include <Arduino.h>
#include <serstream>
#include <iterator>
#include <vector>
#include <string>
#include "SerialShell.h"


SerialShell::SerialShell(std::ohserialstream* serout, std::ihserialstream* serin)
    : serout(*serout),
      serin(*serin),
      buffer(""),
      args(0),
      is_prompting(false) {

}

SerialShell::~SerialShell() {
}

// コマンドの引数を表示する
void SerialShell::printArgs()
{
    serout << args.size() << " args: ";
    for (std::vector<std::string>::iterator it = args.begin() ; it  < args.end() ; ++it) {
        serout << *it;
        if (it < args.end() - 1) {
            serout << ", ";
        }
    }
    serout << std::crlf;
}

// コマンドの引数のリストを返す
std::vector<std::string> SerialShell::getArgs()
{
    return args;
}

// コマンドを読み込む
int SerialShell::readCommand() {

    char c = 0;

    // プロンプトの表示
    if (! is_prompting) {
        serout << "# ";
        is_prompting = true;
    }

    if (serin.available() > 0) {
        serin.get(c);
        if (c == '\r') {
            // CRをNLで置き換えて、ヌル文字を追加
            buffer += "\n\0";
            // コマンドのパース
            parseCommand();
            // バッファをクリア
            buffer = "";
            // エコーバック
            serout << std::crlf;
            is_prompting = false;
            return 1;
        }
        else {
            // バッファに追加
            buffer += c;
            // エコーバック
            serout << c;
            return 0;
        }
    }
    else {
        return -1;
    }
}

// コマンドをパースして引数をvectorに格納する
void SerialShell::parseCommand() {

    unsigned int start =0, end = 0;

    args.clear();
    while (end < buffer.length()) {
        start = buffer.find_first_not_of(" \t\n", end);
        end = buffer.find_first_of(" \t\n", start);
        if (end-start > 0) {
            args.push_back(buffer.substr(start, end-start));
        }
    }
}

3. 使い方

使い方はこんな感じです。
readCommand()の返り値を見て0より大きかったら、getArgs()で引数のリストが得られます。

SerialShellTest.cpp
#include <Arduino.h>
#include <pnew.cpp>
#include <serstream>
#include <iterator>
#include <vector>
#include "SerialShell.h"
#include "SerialShellTest.h"

// 定義済みのHardwareSerialインスタンスのSerialを使って初期化
std::ohserialstream serout(Serial);
std::ihserialstream serin(Serial);

SerialShell* shell;

void setup()
{
    Serial.begin(9600);
    // 入出力シリアルストリームクラスの参照で初期化
    shell = new SerialShell(&serout, &serin);
}

void loop()
{
    int ret;
    std::vector<std::string> args;  // コマンド引数のリスト

    // コマンド読み取り
    ret = shell->readCommand();
    // ret > 0 なら有効データあり
    if (ret > 0) {
        // 引数のリストを表示
        shell->printArgs();
        // 引数のリストをGet
        args = shell->getArgs();
        // 第1引数で処理分岐
        if (args[0] == "hoge") {
            serout << "func hoge" << std::crlf;
        }
        else if (args[0] == "piyo") {
            serout << "func piyo" << std::crlf;
        }
    }
}

4. 実行結果

$ sudo cu -l /dev/cu.usbmodem1411  -s 9600
Connected.
# ai ue   o
3 args: ai, ue, o
# The quick brown fox jumps over the lazy dog
9 args: The, quick, brown, fox, jumps, over, the, lazy, dog
# 
0 args:

入力したコマンドの引数が表示されます。

# hoge
1 args: hoge
func hoge
# piyo 123 
2 args: piyo, 123
func piyo

処理分岐もできていますね。

5. 所感

うーんやっぱりSTLは便利ですね!Serial.println()だと文字列と変数を出力したい時に

Serial.print("The value is: ");
Serial.println(value);

みたいに複数行書かなきゃいけませんが、ストリームクラスのおかげですっきり書けました。
std::crlfはAVR-STLの独自定義で、キャリッジリターン(CR)とラインフィード(LF)を表しています。
vector, stringクラスも特に問題なく普通に使えます。

GitHubの方にソースコードをあげときましたので、よろしければどうぞ。
arduino-serial-shell

動作環境

  • mac OS X Yosemite 10.10.3
  • Arduino IDE 1.0.5
  • Arduino eclipse extensionsArduino plugin 1.2.4
  • eclipse LUNA 4.4.2
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした