NTShellを使おう
第1回TOPPERS活用アイデア・アプリケーション開発コンテスト銅賞作品「Natural Tiny Shell Task」を使って、組み込みソフトにコマンドラインインターフェイス(CLI)を付け加える方法を紹介します。
自前CLIの限界
組み込みソフトでは、デバッグや保守のためにシリアル通信を利用したCLIを作り、コマンド入力でデバッグしたい機能を実行したり、メモリダンプしたり、といった機能を、プロジェクトごとに自前で作成するとこがあります。
ちょっと確認したい機能を実行するコマンドであったり、ユーザーではなく開発者が使うデバッグ用に用意するコマンドであったりするので、作り込みはそこそこになります。
そんなCLIは、UNIXのシェルを使ってしまうと不便に思うことがあります。
Natural Tiny Shell(NTShell)を使用すると、それが改善します。
Back Spaceで入力した文字が消せます!
CLIではキータイプをミスすることがあります。UNIXのシェルではBack Spaceで入力文字を修正することが出来ますが、自作のCLIでは実装するのは面倒なので、我慢して使ってしまいます。Back Spaceを実装するにはエスケープシーケンスでカーソルを移動したり、表示されている文字を消したりする必要があり、少し手間がかかります。しかしNTShellを使えば解消します。
カーソルが矢印キーで移動します!
ターミナルからの矢印キーの入力は、エスケープシーケンスで送られてくるので、複数の文字を処理しないと実現しません。また、移動した先で文字を消されでもしたら、消した文字以降を詰めるためのコピーしなければなりません。文字の挿入でも同様にコピーが必要で、それはデバイス側のバッファの話で、PCなどのターミナル側の表示を更新するため、シリアル送信する必要があります。デバッグのためだけにこれを実装するのはやはり面倒です。
しかしそれも解決します。そう、NTShellを使えばね。
コマンドの入力履歴で再入力の手間が軽減!
いくつかのコマンドを組み合わせる必要がある場合、コマンドの入力履歴から以前のコマンドを呼び出し、それを編集してコマンド入力したいことがあります。UNIXのシェルでは単に上矢印キーを押せば出てきますが、デバッグに使うためだけではとても実装する気にはならないと思います。
これを実装するにはある程度設計を考えてからプログラムすることになるでしょう。プロジェクトを進めるためメインの機能の設計に集中したいところです。安易にコマンドを短くするなどの方法を考えると思います。
NTShellは入力履歴も持っています。
使ってみる
TOPPERSコンテストで受賞されたのは2011年度で、現在はそれよりも新しいものがリリースされているので、Version 0.3.1で説明します。
NTShellのダウンロードはこちらから行えます。
展開すると「src」の中に「lib」と「sample」のフォルダがあります。
「lib」は本体なので、この記事では見ません。「sample」フォルダの「target/nxp-lpc824/lpc_monitor/src」にあるサンプルを見ていきます。
main関数
まず、main関数から見てみます。受賞作品はこのmain関数をTOPPERS OSの1タスクとして実行しています。
int main(void)
{
void *extobj = 0;
ntshell_t nts;
SystemCoreClockUpdate();
uart_init();
uart_puts("User command example for NT-Shell.\r\n");
ntshell_init(&nts, serial_read, serial_write, user_callback, extobj);
ntshell_set_prompt(&nts, "LPC824>");
while (1) {
ntshell_execute(&nts);
}
return 0 ;
}
NTShell自体はターゲット依存のコードはありませんので、初めにデバイスの初期化や、シリアル通信に必要な初期化処理を行っています。
本題のNTShellの初期化はntshell_init
関数で行います。この関数には、作業領域としてntshell_t
型の実体nts
のポインタを渡します。続く引数にNTShellが文字を欲しい時に呼び出されるコールバック関数(serial_read
)で、次に、NTShellが文字を送信したい時に呼び出されるコールバック関数(serial_write
)、コマンド行の入力が完了した時に呼ばれるコールバック関数(user_callback
)、最後がコールバックの引数に渡す任意の値(extobj
)を渡します。
この例では最後の引数には0
を渡して使っていませんが、コールバック関数を複数のシリアルポートで共有したいときに、この値で区別することが出来ます。それぞれのシリアルポートで使う別々の作業領域へのポインタにも使えます。
ntshell_set_prompt
関数は、入力行の初めに表示されるプロンプトを変更するための関数で、この例では「LPC824>
」に変更しています。この関数は呼び出す必要はなく、デフォルトは「>
」になります。
ntshell_execute
関数が、コマンド行を処理する本体になります。中の実装は無限ループになっているので、呼び出したら戻ってきません。中身の処理はコールバック関数が呼ばれるのでそこで実行します。
serial_read関数、serial_write関数
NTShellがシリアル通信をしたいときに呼び出されるコールバック関数の実装です。
static int serial_read(char *buf, int cnt, void *extobj)
{
for (int i = 0; i < cnt; i++) {
buf[i] = uart_getc();
}
return cnt;
}
static int serial_write(const char *buf, int cnt, void *extobj)
{
for (int i = 0; i < cnt; i++) {
uart_putc(buf[i]);
}
return cnt;
}
この例では、1文字ごとに送信、受信する関数uart_putc
やusrt_getc
を呼び出すものになっています。引数で指定したcnt
分の送信または受信を行うよう、ユーザー側で実装します。
user_callback関数
コマンド行の入力が確定した時に呼び出されるのがこの関数で、この関数をユーザーが実装することで、コマンドの処理を行います。
static int user_callback(const char *text, void *extobj)
{
#if 0
/*
* This is a really simple example codes for the callback function.
*/
uart_puts("USERINPUT[");
uart_puts(text);
uart_puts("]\r\n");
#else
/*
* This is a complete example for a real embedded application.
*/
usrcmd_execute(text);
#endif
return 0;
}
#if 0
から#else
までは消されていますが、コマンド行そのものを表示しています。NTShellが正しく動いているかの確認に使用できます。
この例では#else
から#endif
までが有効で、usrcmd_execute
関数の呼び出しをしています。この関数はユーザーが実装する処理のサンプルで、渡された引数のtextをパースする処理が実装されています。この関数の中身は「usrcmd.c
」にあります。
usrcmd_execute関数
コマンド行をパースする処理もNTShellで用意されています。コマンドは「ls dir
」など、引数を取りたいことがあります。スペース区切りでコマンド行をパースしたい場合、ntopt_parse
関数が使えます。
int usrcmd_execute(const char *text)
{
return ntopt_parse(text, usrcmd_ntopt_callback, 0);
}
第一引数にコマンド行の文字列、パースした結果を受け取るコールバック関数(usrcmd_ntopt_callback
)、コールバック関数に渡す任意の値を渡します。
usrcmd_ntopt_callback関数
コマンド行がスペースで分割された形式で渡されます。
static int usrcmd_ntopt_callback(int argc, char **argv, void *extobj)
{
if (argc == 0) {
return 0;
}
const cmd_table_t *p = &cmdlist[0];
for (int i = 0; i < sizeof(cmdlist) / sizeof(cmdlist[0]); i++) {
if (ntlibc_strcmp((const char *)argv[0], p->cmd) == 0) {
return p->func(argc, argv);
}
p++;
}
uart_puts("Unknown command found.\r\n");
return 0;
}
argc
にコマンド引数の数、argv
にコマンド引数の文字列への配列、ntopt_parse
関数に渡した任意の値が渡されます。 この例では、cmdlist
というテーブルからコマンドを探して、テーブルに設定されたコールバック関数を呼び出しています。argv[0]
にはスペース区切りの先頭の文字列、つまりコマンド名が入っているので、テーブルのコマンド名と比較して、同じものを探しています。
static int usrcmd_help(int argc, char **argv);
static int usrcmd_info(int argc, char **argv);
typedef struct {
char *cmd;
char *desc;
USRCMDFUNC func;
} cmd_table_t;
static const cmd_table_t cmdlist[] = {
{ "help", "This is a description text string for help command.", usrcmd_help },
{ "info", "This is a description text string for info command.", usrcmd_info },
};
この例では「help
」と「info
」の2つのコマンドが用意されています。
それぞれusrcmd_help
関数とusrcmd_info
関数が対応するようcmdlist
が定義されているのが解ると思います。
コマンドを増やす場合、このテーブルに行を追加すればよいということです。また、コマンドを実行するコールバック関数の引数の形は、見覚えのある形だと思います。既存のコードもそのまま活用できそうです。
サンプルコードを眺めてみて
ここまで紹介したサンプルコードは、ターゲットデバイスやユーザーが実装したいコマンドに応じて、変更する必要のある部分ですが、ほとんど定型文として使えると思います。ユーザーは、シリアル通信のコールバック、コマンドテーブルとコマンドの処理関数を実装するだけで、初めに紹介したコマンドラインエディタを使用することが出来ます。
これだけでCLIの実装が充実したものになり、デバッグが楽に、テストの量も増え、品質が上がるのではないかと思います。