せっかく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
を使ってこのように書くと思います。
void loop() {
char c;
if (Serial.available() > 0) {
c = Serial.read();
Serial.print("input: ");
Serial.println(c);
}
}
実は、serstream
に定義されている入力シリアルストリームクラスihserialstream
には、このSerial.available()
に相当するメンバ関数がありません。これだと、「何か入力があった時だけ処理する」ということができず大変不便なので、以下のように追記してやります。
/*
* 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クラスの中身はこんな感じです。
- キーボード入力を受け付け、リターンが押されるまでの文字列を保持
- 空白文字で分割し、引数として配列に格納
が主な動作になります。
#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_ */
#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()
で引数のリストが得られます。
#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