2015年9月13日

[nfc/ble]NFCペアリングがまだまだだめだ

nRF52でNFCペアリングがうまくいかないシリーズ。

[nrf52][nfc]NFC Pairingサンプルを動かす (1)
[nrf52][nfc]NFC Pairingサンプルを動かす (2)
[nrf52]NFCペアリングサンプルがまだうまくいかない
[nrf52]まだNFCペアリングできないのだ

  • Peripheral
    • nRF52 Preview DK
    • SoftDevice S132 v1.0.0-3alpha
    • nRF52 SDK v0.9.1
  • Central
    • Nexus5
    • Android 5.1.1 (Build : LMY48l)

やりたいことは、「Nexus5でnRF52 Preview DKにNFCタッチしてペアリングしたい」だ。

  • できていること
    • nRF52 Preview DKがNFCタグで動く
    • NFC ForumのSimple Secure Paringに書いてあるタグの内容が読める
    • タグの内容をNexus5で読んで「ペアリングしますか?」と聞いてくる
    • nRF52 Preview DKでAdvertisingが開始する
  • できていないこと
    • AndroidのBluetooth設定画面で「ペアリングされたデバイス」に出てこない

nRF52 Preview DK側の動き

  1. NFCタッチ待ち
  2. タッチされてタグが読まれるとAdvertisingを開始する
  3. Connectしたら、HRSサンプルと同じ動きをする

Androidに載っているHRSアプリとしては、Connectしてしまえば使うことができる。
nRF52 Preview DKのアプリも同じだ。
だから、ペアリングしようとすまいと、HRS自体は動くのだと思う。
実際に動いてるし。

そもそも、ペアリングって何だ?


Interface誌2015年8月号のBLE特集を読むと、暗号通信をするときには鍵交換する必要があり、その鍵交換の手順をペアリング(Bonding)と呼ぶ、とある。
なるほど、では、暗号化されたデータを使わないならペアリングは不要ということか。
ただ、「暗号化しない」という情報を交換するかもしれない。

このNFCペアリングサンプルがどういうやりかたをしているのか、調べる必要がありそうだ。
ソースを読むしかないかと思ったが、Note欄の下に書いてあった。

このアプリはデフォルトではJust Worksペアリングを行う(Android端末でテストを行う準備をするため)。

"prepared for testing with Android device"は、「Android端末でテストする準備」でよい?
ペアリングする準備とかじゃ無くて、テストする準備?
もう少し先を読んでみよう。

OOBペアリングを行うようにするには、main.cに適切なTK値を定義し、NFC_BLE_PAIRING_TYPEとOOB_AUTH_KEYを使うようにする。

ソースを見ると、NFC_BLE_JUST_WORKS_PAIRINGとNFC_BLE_OOB_PAIRINGがコンパイルオプションとしてあり、JUST_WORKSの方はTK値が常に0、OOB_PAIRINGはTK値が設定できる、とコメントにある。
OOB_PAIRING有効時の設定を見ると、SEC_PARAM_OOBが有効になって、OOB_AUTH_KEYが定義されるようになっていた。

じゃあ、とコンパイルオプションを変更したが、結果としては同じだった。
そうですか。

ただ、NFCタグの中身は増えていた。
キーを渡しているのだろうね。

 

ちゃんと、ペアリングしよう、という意思はあるようなのだ。
こんなパケットが出てる。

image

ただ、ここから先、MasterがRETRYを繰り返しているだけで終わってしまう。
MasterはNexus5のはずだから、nRF52 Preview DKが返事をしていないっぽい。
これって、Nordicのシーケンスでも一番最初のところですな。

main.cを見ても、sd_ble_gap_sec_params_reply()を使っているところが無いのだけど、それがよくないのかい?
SECなイベントとしては、BLE_GAP_EVT_AUTH_KEY_REQUESTだけをさばいているようなのだけど、シーケンスに書いてあるBLE_GAP_EVT_SEC_PARAMS_REQUESTはやってない。

main.cではやってないのだけど、device_manager_peripheral.cではやっていそうだ。
dm_ble_evt_handler()にcase文としてあるし、main.cはble_evt_dispatch()でdm_ble_evt_handler()を呼んでいる。
じゃあ、ここがうまく動いていないということか?

 

device_manager_peripheral.cはログを出すようなので、見てみよう。
(これでやっていたのが「hiro99ma blog: [nrf52]app_uart_fifo.cが2つある」だ。)

[DM]: >> BLE_GAP_EVT_SEC_PARAMS_REQUEST

このログは出た。
出たなら、sd_ble_gap_sec_params_reply()も実行しているはずなのだ。

[DM]:[CI 0x00]:[DI 0x00]: Bonded!

こんなログも出てるし。
わからん、わからんのだぁぁぁぁ

[nrf52]app_uart_fifo.cが2つある

NFCペアリングがうまくいかないので、ログを出そうとした。
UARTログだ。

サンプルをそのままログ有効にするだけだと、文字が化け化けだった。
結構悩んだのだが、UARTのみのサンプルを見るとapp_uart.cではなく、FIFOを使うapp_uart_fifo.cを使っていた。
ではまねしよう、とdrivers_nrf/uart/app_uart_fifo.cをプロジェクトに組み込んだのだが、なんだかえらくビルドが通らない。

UARTサンプルを細かく見ると、drivers_nrfではなくてlibraries/uart/app_uart_fifo.cを使っていた。
プロジェクトでnRF_Driversのツリーに入っていたから、だまされた・・・。

そっちに差し替えると、ビルドも通って、文字化けもしなくなった。
もうFIFO版だけでいいんじゃないのかって思うけど、やっぱりバッファを食うからかねぇ。

 

なお、nRF51の方は、drivers_nrfの方にしかapp_uartがいない。
傾向からすると、nRF52もいずれはそうなっていくんだろうな。


なお、SEGGER経由でログを出す方法もあるらしい。いつか試したいものだ。
https://devzone.nordicsemi.com/tutorials/6/debugging-with-real-time-terminal/

[keil]Packを整理したらビルドできなくなった

KeilでnRF52のサンプルをビルドしようとした。

最近、Nordicからメールで更新通知が来ていて、Packとかが新しくなったようだった。
Packとか何とかはよくわからないけど、とりあえず更新しておけばいいんじゃなかろうか、くらいでやっておいた。

ふっと、KeilのPack Installerで見てみると、過去バージョンが残ったままになっているようだった。
いつインストールしたかとか、どういう管理していたかも記憶にないので、とりえあず最新版以外はPackごとRemoveした。

そしたらですな、以前ビルドできていたサンプルソースがビルドできなくなったのだ。
コンパイルエラーとかじゃ無くて、プロジェクト自体のエラーみたいなものだ。
CMSIS COREとDevice Startupが無い、とおっしゃる。

「Manage Run-Time Environment」で見てみると、確かに赤くなってエラーっぽい。

image

よくわからんので「Select Software Packs」のダイアログを出すと、ARM::CMSISとDeviceFamilyが赤くなっていた。
まあ、さっき消したから、ないんだろうな。
でも最新版は入っているはずと、画面の上にある「Use latest versions of all installed Software Packs」にチェックをした。
これで最新版になっているはず。

はずだったのだが・・・だめだった。
Manage Run-Time Environmentにも新しいのが出てこないので、どうしようもない。
該当するPackを入れればいいんだろうが、それは負けた気がする。

よくわからぬまま、もう一度Nordicからメールできていた更新ファイルをインストールし直した。
今日は8.0.4とかいうのが最新っぽい。

  • nRF5x_MDK_8_0_4_Keil4.msi
  • NordicSemiconductor.nRF_DeviceFamilyPack.8.0.4.pack

そうしたら、新しいのが使えるようになっていた。

image

インストール→消す、とやったのがまずかったのだろう。
消してからインストール、ですかね。


別のサンプルプロジェクトを立ち上げたところ、また同じことが起こった。
今度は、インストールし直さずとも、Select Software PacksでLatestにチェックを入れ、Run-Time Environmentで選び直せばうまくいった。

2015年9月12日

[smc]イベント内容を定義していないとDefault

あたし一日中状態遷移のことばかり考えてる・・・と、いうわけでもないのだが、こういうのは勢いで調べておかないと。
というわけで、まだ状態遷移のsmcについてだ。

サンプルのsmファイルを見ると、各状態のイベントごとの動作が入っていないことがある。
たとえば、EX1はイベントで「Zero, One, Unknown, EOS」があるけど、OK状態のときにはどのイベントについても記述が無い。
さて、イベント内容を定義していないと何が起きるのだろう?

 

cygwinで実行させると、コアダンプした。
assertionログが出ているので、assert()が引っかかったようだ。
これは「Default」という特殊イベントが発生したのと同じことになるようである。

自動生成されたソースは、こうなっている。

static void AppClassState_Default(struct AppClassContext *const fsm)
{
    if (getDebugFlag(fsm) != 0) {
        TRACE("TRANSITION   : %s.%s\n", getName(getState(fsm)), getTransition(fsm));
    }
    State_Default(fsm);
}

そしてState_Default()は、statemap.hにあった。

#define State_Default(fsm) \
    assert(0)

あまり調べてないけど、たぶんこれが動いているのだろう。

 

では、自分で何も無いときの動作を定義したい場合はどうするかというと、2つあるようだ。
「Default状態」(Dは大文字)に遷移するか、「Defaultイベント」が発生するかだ。

じゃあ、両方書いた場合はどちらが優先されるのか?
やってみると、Default状態の方が実行された。
うーん、私のイメージでは、各状態に書かれたDefaultイベントの方が強い、だったのだが。

Zeros状態のZeroイベントを無くした状態で実行すると、こんな動きになっていた。

image

引数は"000"で、1番目の"0"でStart→Zerosに遷移後、次の"0"でZeroイベントを発行(AppClassContext_Zero())。
でも、そのときの遷移定義がないので、Map1_DefaultState_Zero()が実行されている(最後のAppClass_DefaultState()はActionとして実装した関数)。

Map1_DefaultState_Zero()はこんな感じで自動生成されていた。
赤文字が、Actionで実装した関数を呼んでいる箇所。

static void Map1_DefaultState_Zero(struct AppClassContext *const fsm)
{
    struct AppClass *ctxt = getOwner(fsm);
    const struct AppClassState* EndStateName = getState(fsm);
    if (getDebugFlag(fsm) != 0) {
        TRACE("LEAVING STATE   : Map1_DefaultState)\n");
    }
    if (getDebugFlag(fsm) != 0) {
        TRACE("ENTER TRANSITION: Map1_&Map1_DefaultState.Zero()\n");
    }
    clearState(fsm);
    AppClass_DefaultState(ctxt);
    if (getDebugFlag(fsm) != 0) {
        TRACE("EXIT TRANSITION : Map1_&Map1_DefaultState.Zero()\n");
    }
    setState(fsm, EndStateName);
}

2015年9月9日

[smc]idはgetId(state)でとれる

プリプロセスを展開したらわかりやすいかと思ったのだけど、そもそもソース中でidを使っている箇所が無いためか、_idで展開されるものが無かった。
うう・・・あきらめてファイルを読むことにしよう。

lib/C/statemap.hの中に、getId(state)というマクロがある。
中身は、state->_id。
では、このstateはなんだよってなると、ここでプリプロセスを展開したものが役立つ。

struct AppClassState {
    void(*EOS)(struct AppClassContext *const fsm);
    void(*One)(struct AppClassContext *const fsm);
    void(*Unknown)(struct AppClassContext *const fsm);
    void(*Zero)(struct AppClassContext *const fsm);
    void(*Default)(struct AppClassContext *const fsm);
    int _id;
};

これは、AppClass_sm.hの中にいた。

struct AppClassState {
    void(*EOS)(struct AppClassContext *const fsm);
    void(*One)(struct AppClassContext *const fsm);
    void(*Unknown)(struct AppClassContext *const fsm);
    void(*Zero)(struct AppClassContext *const fsm);
    void(*Default)(struct AppClassContext *const fsm);
    STATE_MEMBERS
};

つまりまあ、STATE_MEMBERSがint _idだってことだ。

それはよいとして、AppClassStateの本体はどこにあるのだ?
ヘッダの中では、こうなっていた。

extern const struct AppClassState Map1_Start;
extern const struct AppClassState Map1_Zeros;
extern const struct AppClassState Map1_Ones;
extern const struct AppClassState Map1_OK;
extern const struct AppClassState Map1_Error;

これは、昨日見ていた「getState」マクロだ。
つまり、こういう使い方になるようだ。

 getId(getState(&this->_fsm))

サンプルEX1で引数を"00011"としたときは、こんな順番になっていた。

./checkstring 00011
getId : 0
getId : 1
getId : 1
getId : 1
getId : 2
The string "00011" is acceptable

"0"がStart、"1"がZeros、"2"がOnesなのだろう。最後のが出力されないのは、その前にwhile()を抜けるからだ。

 

最後は、この0とか1とかが#defineで定義されていればよいだけだ。
そういうのがないと、どれが何番目かわからないが・・・えっ、ないの??
ないのか・・・自分でJARを作ってやれば出力できなくも無いだろうが・・・。

では、せめてsmファイルに書いた順番になるのであれば、あきらめて自作もできるだろう。

0 : Start
1 : Zeros
2 : Ones
3 : OK
4 : Error

では、smファイルの並びを、OK --> Ones --> Error --> Start --> Zerosに変更。

0 : OK
1 : Ones
2 : Error
3 : Start
4 : Zeros

うん、間違いなかろう。
この定義値の生成も、前回のExcelファイルに組み込んでしまいたいところだが、後日だな。

[smc]Excelファイルで簡単なsmファイルを作る

まだ、もごもごやってるsmcだ。

昨日はアドレスで比較するような状態取得を考えていたけど、やっぱり格好が悪い。
定数になっていればswitch-caseでもいけるのに。

と思ったら、マクロの中に「_id」という変数を見つけた。
これは何か使えそうだ!
が、それにはマクロを展開して、どういう使われ方をしているか眺めておきたい。
吐き出すのはgcc -Eでできるけど、読むのに時間かかるなぁ。

 

というわけで、今回はsmcのフォーマットsmファイルをExcelで作った。
smファイルを作るExcelファイル
・・・と書くと数日で作ったように見えるが、2008年に作っていたものを今のsmcバージョンに合わせただけだ。
ガードとかは入れるところが無いのだけど、まあ簡単なsmファイルならこれでもよいだろう。
上書き防止のため、smファイルがあると作ってくれないのがうっとうしいかもしれない。

そんなに難しいことはしていないし、マクロもパスワードをかけているわけでは無いから、ガードを付けたりするのも簡単だろう。
ネットで検索すると、他にもExcelで作られた方もあるようだから、そちらの方が機能がよいかも(中身を見てないので)。

[smc]現状を知ろう

ふっと思ったのですよ。
「もしかして、なんか私の考え方が間違ってるんじゃ無かろうか?」と。

しかし、心配しても仕方ないので、あきらめて現在の状態を取得し、その状態によって条件を決める関数を作り、それを状態遷移のガードに使おう。


statemap.hに「getState」というマクロがあったので、printfで値を見てみた。

./checkstring 000
state : 40314c
state : 403134
state : 403134
The string "000" is acceptable

%xで表示させると、こんな感じ。
最初の0x40314cは、mapファイルではMap1_Startのアドレス、後はMap1_Zerosのアドレスだった。
つまり、今の状態がこれ、という変数を持ってるのでは無く、状態ごとのイベント関数群のポインタを持っているようだ。

たとえば、Map1_Startはこういう定義になっていた。

const struct AppClassState Map1_Start = {
    Map1_Start_EOS,
    Map1_Start_One,
    Map1_Start_Unknown,
    Map1_Start_Zero,
    Map1_Start_Default,
    0
};

なので、ここに独自関数を追加できるようになっていれば済むのだが・・・。


今日はここまで。

あと、前回のガードを追加すると、遷移条件を満たさなかったときにDefaultの遷移をするようだ。
定義していなくて実行時エラーになっていたから、たぶんそうだろう、というところだが。

2015年9月8日

[smc]状態ごとに条件を変えて遷移できるか?

状態遷移のsmcは、使ってみればわかるだろう、と思っていたのだが、いきなり詰まった。

やりたいのは、A/D変換の結果によって遷移先を変えたいというもの。
ただ、その条件は状態によって違うのだ。
ありがちだと思うのだが、どこまでsmcでできるのだろうか?

状態に遷移するイベントを作って置いて、自分で状態ごとに値を判断してイベントを呼ぶ、というのは無しにしたい。
それだったら、普通に組んだ方がわかりやすいだろうし。

smcのページを見ていくと、タイトルとしてはこれが近いような気がする。
http://smc.sourceforge.net/SmcManSec2.htm#TransGuards

UMLのガードって、条件みたいなものではないか。
だから、条件による遷移、みたいな意味だったらよいと思ったのだ。
日本語だと「守る」って感じがしてしまうんだけどね。


ガードにはif文に書けるような条件を書いておけ、というように読める。
ただ、私がやりたいのは静的な条件では無く、状態によって変わる条件なのだ。
そういうのはどう表すのだろう?

コンテキストクラスのメソッドを呼びたいなら、ctxtを自分で付けろ、とある。
でも、Cだしなぁ。
というよりも、ctxtってなんだ?
thisみたいなものなら、引数にして渡してやれば同じことができるかもしれない。

 

どうやら、ctxtはコンテキストクラスを表す変数のようだ。
Cでのサンプルを見る限りでは、%classにつけた構造体名がそれに当たりそうだ。
では、試してみよう。

extern int AppClass_IsAcceptable(struct AppClass *this);

こんなヘッダをAppClass.hに書き、ソースにはこう。

int AppClass_IsAcceptable(struct AppClass *this)
{
    return this->isAcceptable;
}

特に意味はない関数なのだけど、サンプルソースで使えそうなのを入れてみただけだ。

そして、smファイルは

Zero    [AppClass_IsAcceptable(ctxt)]    Zeros    {}

遷移条件なので、遷移先の前に書くのだろう。
これは、Start状態のZeroイベントに書いたのだが、AppClass_sm.cにはこう反映されていた。

static void Map1_Start_Zero(struct AppClassContext *const fsm)
{
    struct AppClass *ctxt = getOwner(fsm);
    if (AppClass_IsAcceptable(ctxt)) {
        /* No actions. */
        setState(fsm, &Map1_Zeros);
    }
    else {
        Map1_DefaultState_Zero(fsm);
    }
}

AppClass_IsAcceptable()が真ならばZerosに遷移するし、そうでなければ・・・デフォルト状態?に遷移するようだ。

 

なので、Context Classを表す構造体のメンバに、判断する材料を全部突っ込んでおけばよいということになるか。


でも、C++みたいにvirtualで持つわけではないので、もうちょっとうまいことやらないと作業が楽にならないな。

状態を表すint型の0から始まる変数があって、それを添字にして関数を用意する、みたいなのが楽なんだけど、そういう感じでもなさそうだ。
そもそも、AppClassState構造体は状態ごとに別の実装を持てるようになっていそうだから、間借りさせてくれるとよいのだが。

2015年9月6日

[nrf52]まだNFCペアリングできないのだ

タイトルには「できない」と書いたが、うまく行くときもある。
こういうときはだめ、というのがわからないのは、できないのと同じだ。

シーケンスは、これだろうと思う。
http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.s132.api.v0.9.0%2Fgroup___b_l_e___g_a_p___p_e_r_i_p_h___b_o_n_d_i_n_g___p_k___c_e_n_t_r_a_l___o_o_b___m_s_c.html&resultof=%22oob%22

Connection Establishedとあるから、まず接続して、Centralから「ペアリングしよう」と言われて動き出すようだ。
が、Centralへペアリングの応答を返した後でOOBの入力をやって、それ以降はやりとりがなさそうなのだ。
まあ、OOBだから、Bluetoothのところでは何もしないといわれればそれまでなんだけど。

では、Androidで失敗しているのは、何ができなかったからダメだと思ったのだろう?
ペアリングに成功した端末は、Android設定画面のBluetoothで「ペアリングされたデバイス」に現れている。
ペアリングはしてないが、検出できた端末は「使用可能なデバイス」に出てくるようだ。
TIのSensorTagも、ここに出てくる。
しかし、どうも「使用可能なデバイス」は常に見ているわけじゃなく、設定画面に入ったり、メニューで「更新」としたときにやっているようなのだ。

ペアリングするかどうかのダイアログは、NFCタッチのタイミングで表示される。
サンプルプログラムではNFCタッチのタイミングでAdvertisingをし始めるのだけど、そこら辺にずれがあるんじゃなかろうか。


ならば、だ。

  1. AndroidのBluetooth設定画面を開く or 「更新」を実行する
  2. Androidが端末を探している間にPreview nRF52 DKのNFCアンテナに軽くタッチし、Advertisingさせる
  3. 設定画面の「使用可能なデバイス」にNordic_HRMが表示されたら、ちゃんとタッチしてペアリング確認ダイアログを表示させる
  4. OKする

だとどうだろう?

・・・やってみたが、だめだった。
ペアリングされたデバイスになってくれない。

そもそも、ペアリングされたデバイスになっていなくても、HRMサンプルは動くようだ。
元のHRMサンプルにペアリングのところだけ付けたものだから、接続さえできればよいのだろう。
あれ、じゃあこのサンプルのペアリングって何のためにやってるのだ?


NXP Tag Infoアプリで読んだデータだ。
XMLも出力してくれるのだが、解析した画面の方がわかりやすい。

image

仕様は、これだろう。
http://members.nfc-forum.org/apps/group_public/download.php/18688/NFCForum-AD-BTSSP_1_1.pdf

まずNDEFレコードが1つしかない。
これはSimplified Tag Format for a Single Bluetooth Carrierというやつだろう。
そのときは、Bluetooth OOB情報だけをNDEFメッセージに入れるようだ。
以下、順にデータを切り出していく。AD Typeは2番目だ。

02 1c 00 : LE Role - Only Peripheral Role supported
08 1b 9a 7d b8 6a 25 cd 01 : LE Bluetooth Device Address
03 19 41 03 : Appearance - Heart Rate Sensor: Heart Rate Belt
02 01 04 : Flags - BR/EDR Not Supported
0b 09 4e 6f 72 64 69 63 5f 48 52 4d : Complete Local Name
- "Nordic_HRM"

Advertisingしているデータは、こんなのだった。

image

LE Bluetooth Device Addressを確認しよう。
CSS_v5.pdfによると、LE Bluetooth Device Addressのサイズは7byteで、6byteはCore_V4.2のVol.3, Part B, Section1.3と同じ。残りは、bit0がRandom Device AddressかPublic Device Addressか。

まず、最後のデータは01なので、これはRandom Device Address。
では、残りのデバイスアドレスをどう並べるか。Section 1.3だけを見ても、エンディアンなどは書いてない。でも、48bitって書いてあるので、何も考えずにリトルエンディアンにすればいいんじゃないかな。
ならば「cd:25:6a:b8:7d:9a」。
AdvAと一致するな。

暗号化に関することなど書かれているかと思ったが、NFCのデータにはそんな要素はなさそうだ。


結局、今回もペアリングのことはわからないままだった。
AndroidでOOBのペアリングができないというわけでは無く(PaSoRi RC-S390で試した)、NFCでのペアリングができないだけのような気がする。
私が「動いている」と思っていたのも、単にNFCタッチによってAdvertising開始して、アプリがConnectしただけのようだし。

状態遷移smc

組込み系の人間でいるつもりなのだが、状態遷移が苦手だ。
苦手というか、状態遷移図とか表を作るだけ作って、実装はちまちまifとかswitch-caseで分けていたりする。
でも、状態でバーンと動きが変わるのであれば、状態ごとに実施するイベントハンドラを切り替えるようなやり方の方が、実装としてはさっぱりと切り分けられてよいこともある。
まあ、同じような実装をあちこちに書いてしまいがちになるというのはあるがね(下手なだけか?)。


そういった悩みを解決してくれるわけではないのだが、「状態遷移を作るのがめんどくさい」ということについては、多少手助けしてくれるツールがある。

SMC: The State Machine Compiler

これを最初に知ったのは、いまはなきC Magazine誌の記事だった。
株式会社エス・スリー・フォー » SMC(State Map Compiler) の拡張
1999年ってことは、16年前か・・・。
当時の私は、まだインターネットの環境を持ってなかったか、ダイアルアップでちまちまやってた時代だと思う。
ファイルはあるのだが、当時のsmcのバージョンなどはよくわからない。
ただ、bisonとかflexとか、そんなのを使っていたようだ。

確か、お仕事でアプリっぽいものを作っていた時期がある。
そのときにも思い出して、ちょっと調べていたのがこちらだ。
StateMap - YMHRS
7年前か。我ながら適当すぎる・・・が、今とあまり変わらない気がする。
何か変更していたようで、ローカルに持っていた。
が、何をしたかったのかがよくわからん。。。

さて、時代は2015年9月。
smcのバージョンは6.6.0になっている。
bison/flexだったものは、JARファイルで実行できるようになっていた。
そして、対応言語がかなり増えている。
私としては、C言語対応になったのがありがたい。

 

SMCがどういうツールかというと、ルールに沿って状態遷移をテキストに書いておくと、ソースに変換してくれるのだ。
そのくらい自分で書けばいいやん、という気になってしまうかもしれないが、自動で書けそうだ、と思ってしまうと、自動でやらせてしまいたくなるではないか。


C言語でどうやっているか見てみよう。
きっと、ファイルの形式は言語に因らず共通なのだろうけど、C言語でのサンプルだけ見る。

ファイルの先頭は、こう。

%class        AppClass
%header        AppClass.h
%start        Map1::Start
%map        Map1

classとなっているが、これが関数のプレフィクスになるようだ。
あとは、各状態ごとにイベントと遷移先とやることを書いていく。

Start
{
    Zero    Zeros   {}
    One     Ones    {}
    Unknown Error   {}
    EOS     OK      {Acceptable();}
}

"Zero"というイベントを起こすと、状態がZerosに遷移する、という感じだ。
やることは、そのまま関数になるようだ。クラス内の関数を呼ぶ、ということのようで、これにもクラス名のプレフィクスが付いていた。
そうそう、状態を管理する変数としてContext構造体というのを生成してくれるのだが、それも引数に取るようになっている。

void AppClass_Acceptable(struct AppClass *this);

もちろん、呼ぶ関数は自分で実装しないといけないし、プロトタイプ宣言も自分で用意する。
そういうのを書いたヘッダファイルを、%headerで指定するようだ。
ちょっと残念なのは、Windowsで実行しているためかもしれないが、変換した結果のソースファイルを見ると、%headerで指定したファイルを#includeで書いた行がCR/LFで改行されているのだ。
cygwinでやったからLFになると思ったけど、JARだからJavaがそう動いているのかな?

 

さて、呼ぶ方だが、名前にプレフィクスとして%classで指定した名前+"Context_"が付いている。
例えば、上記のZeroイベントであればAppClassContext_Zero()となる。

初期化用にInit()が自動で作られるので、まずはこれを呼ぶ。
のだが、その前に状態を管理する構造体を自作する。名前は%classで指定したもの。
状態として持つべきデータなどは、ここに全部入れておくイメージなのかな?
少なくとも、状態遷移する判断に使うデータはここに入れそうだ。

struct AppClass {
    int isAcceptable;
    struct AppClassContext _fsm;
};

この構造体のインスタンスをInit()の引数で渡す。

struct AppClass thisContext;
AppClassContext_Init(&thisContext._fsm, &thisContext);

「Contextのポインタを渡すんだから第1引数はなくてもいいんじゃないの?」と思ったが、AppClassContextをAppClassの中に入れなくてもよいということだろう。

あとは、イベントが発生したときに関数を呼ぶだけだ。
「関数」と書いたが、デフォルトはマクロで、NO_APPCLASS_SM_MACROマクロを定義すると関数にもできる。

状態遷移するとき、文字列を使っているようなので、状態名がバイナリのサイズに影響しそうだ。
まあ、目くじらを立てるほどではないだろうが、S02みたいな割り切った名前の方がよいかも。

 

gcc -O2で関数化したら、こんな感じだった。

0x00401390                AppClassContext_Init
0x004013c0                AppClassContext_EOS
0x00401400                AppClassContext_One
0x00401440                AppClassContext_Unknown
0x00401480                AppClassContext_Zero

うん、何とも言えんね。