NTS1カスタムコントロールパネルのプログラミング

前回の続きです。
NTS1カスタムコントロールパネルには、サンプルが2つだけ用意されています
1つは初期化とidle処理だけのメインループ(前回の記事参照)のBlank_Templateで、もう一つがシーケンサーを実装したSequencer_Templateです。

Blank_Templateで使われているAPIはinit()idle()のみ、Sequencer_Templateはこれに加えてnoteOn()noteOff()paramChange(k_param_id_osc_shape, k_invalid_param_subid, value)の3つを使っています。

逆に言うと、現状これら以外のAPIはサンプルがありません。

用意されているAPIの全体像は前回紹介しましたが、今回はメインボードに情報をリクエストするAPIを使ってみます。リクエストへの応答としてメインボードから受け取った情報は、シリアルポートに出力します。

情報をリクエストする関数はあらかじめ用意されています。
一方、応答として帰ってくる情報を受け取る関数は、応答メッセージの種別ごとのメッセージハンドラとして自分で実装する必要があります。

ということで、まずOSC情報メッセージ用メッセージハンドラの例を見てみます。

以下のメッセージハンドラは、受け取った構造体のメンバごとの値をシリアルポートに出力します。Arduinoはprintf()が無いので、fmtprint()というシリアルポートにprintfするような関数を定義して使っています。

void rx_unit_desc(const nts1_rx_unit_desc_t *desc) {
  fmtprint("unit_desc: %d %d %d %s\r\n", desc->main_id, desc->sub_id, desc->param_count, desc->name);  
}

メッセージハンドラと言っても、メインボードから受け取った情報が入っている構造体へのポインタを1つ渡されるだけです。ハンドラの扱うメッセージ種別ごとにパラメータのポインタが指す型が違うことを除けば、どのメッセージハンドラも形態は同じです。

次に、リクエストをメインボードに送信したら、リクエストに対応するメッセージハンドラが実際に呼び出されるまで「待つ」処理が必要です。

コントロールパネルとメインボードは並列に動作しますから、情報をリクエストしてもいつ応答が返ってくるかは分かりません。応答が返ってきたかどうかは、リクエストと対になるメッセージハンドラでしか判断できないのです。

今回は簡単にグローバル変数を使って、メッセージの到着を「待つ」関数wait_rx()を定義します。

int s_rx_avail = 0;

int wait_rx(unsigned int timeout) {
  int result;
  while(s_rx_avail == 0) {
    nts1.idle();
    delay(1);
    if (timeout-- == 0) {
      break;
    }
  }
  result = s_rx_avail;
  s_rx_avail = 0;
  return result;
}

void rx_unit_desc(const nts1_rx_unit_desc_t *desc) {
  s_rx_avail = 3;
  fmtprint("unit_desc: %d %d %d %s\r\n", desc->main_id, desc->sub_id, desc->param_count, desc->name);  
}

wait_rx()から1msecごとに繰り返しnts1.idle()を呼び出していることに注意してください。メッセージハンドラが呼び出されるのはnts1.idle()からですので、この関数を呼び出さない限りメッセージハンドラは呼び出されません。

使い方は、

    nts1.reqOscDesc(0);
    wait_rx(100);

のようになります。このコードは、メインボード側に0番目(先頭)のOSCの情報をリクエストします。wait_rx()はリクエストに対する応答があったか、または100msec経過してタイムアウトするまで待ちます。

実装としては、wait_rx()は100msecの間、whileループでnts1.idle()を繰り返し呼んでいます。
nts1.idle()はnts1_iface.c内の内部関数s_rx_msg_handler()を呼び、s_rx_msg_handler()はメインボードからの受信データを処理して、その中にOSCの情報リクエストに対する応答があればメッセージハンドラであるrx_unit_desc()を呼び出します。
rx_unit_desc()はグローバル変数s_rx_avail を非0値にしますので、wait_rx()は内部のwhileループから脱出します。

最後に、メッセージハンドラの登録ですが、これはnts1.init()の後で登録用の関数を使って登録します。

  nts1.setUnitDescEventHandler((nts1_unit_desc_event_handler) rx_unit_desc);

ここまでの例では、メッセージハンドラは単に変数の値をプリントアウトしているだけですが、実際には受け取ったデータを後で利用するには、データをグローバル変数等に保存する必要があります。

◆◆◆

それでは、メインボードから「OSCの一覧」を取得してシリアルポートに出力するスケッチを作成してみます。

それにはまず、OSCの個数を取得するreqOscCount(void)を使用します。結果はValueイベントハンドラ経由で返されます。
Valueイベントハンドラ内でOSCの個数の情報をグローバル変数に保存し、次にループでOSCの情報を1つずつOSCの個数分だけリクエストして受け取っていきます。

スケッチは記事の末尾に掲載しますが、ここで問題が生じました。
以下のようにハンドラ内で情報をシリアルポートに出力しようとすると、クラッシュしてしまいます。

void rx_value(const nts1_rx_value_t *val) {
  s_rx_avail = 2;
  fmtprint("value: %d %d %d %d\r\n", val->req_id, val->main_id, val->sub_id, val->value);  
}

しかし、このポインタvalが指している構造体全体をグローバル変数にコピーし、そのグローバル変数を参照すると問題ありません。

調べてみた結果、上のコードの最後のval->valueが怪しそうだということが分かりました。

このメンバ変数は16ビット整数ですので、メモリ上でアラインメントに従っていないとアクセスできません。ところが、この変数の実体はバイト列のバッファで、そのバッファ自体にはアラインメント指定がありませんでした。

1バイトずつアクセスする場合には、アライメント指定が無くていいのですが、それをキャストして16ビットでアクセスする場合はアラインメント指定が必要となります。

この問題はプルリクエストを上げておきました

というわけで最後にスケッチを掲載しておきます。シリアルポートの速度は9600bpsにしています。115200bpsではボードがついてこれないようです。
実行すると、

Start!
value: 16 0 127 15
Number of OSCs: 15
unit_desc: 0 0 0 SAw
unit_desc: 0 1 0 TRI
unit_desc: 0 2 0 SqR
unit_desc: 0 3 0 VPM
unit_desc: 0 4 6 waves

という具合にOSCの情報が表示されます。ただ、この出力例では”value: …”という2行目の出力はメッセージハンドラから出力していますので、nts1_iface.cに上述の修正を加えておかないと動作しません。
修正を加えずに動かす場合は、スケッチの37行目のfmtprint文をコメントアウトしてください。

コメント