Processingでセンサーのグラフを書く（ControlP5ライブラリ使用）

# 動機

Arduinoに加速度・ジャイロセンサーをつないでその値をリアルタイムにグラフ表示してみたい場合、Arduino IDEでもシリアルプロッタで見れますが、自前のプログラムを作って見れたらいろいろと凝った表示や操作のGUIができて便利ですね。

そういうのを作る場合の定番は Processingを使うというものだと思っていたのですが、意外と時系列グラフをサクッと作る情報がないように感じたので調べた結果のメモです。


# できあがりのイメージ

Arduinoを使って作成している倒立振子のセンサーデータをPC上で可視化するGUIを作ります。
加速度３軸、ジャイロ３軸の生データを折れ線グラフでリアルタイムに表示します。
ここでは説明していませんが、加速度データから簡易的に姿勢を計算してグラフィック表示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215650/448b92fa-b2e2-7880-cb5b-d582199174f7.png)


# 方針
Windows10で Proicessing3 が使える環境を前提としています。
Proicessingの詳しい使い方等は他のWEB等で良い説明がたくさんあるので割愛します。

グラフを描くためのライブラリとして ControlP5を使用します。
このライブラリはグラフ表示以外にも ボタンやスライダ等の入力 GUIを作るのが便利そうなので使い方を覚えておいたら後々いろいろと応用ができて良いのではないかと思います。

動機にも書いたようにセンサーの時系列データをリアルタイムにグラフ表示したいので Arduinoと連携した例も交えた説明とします。
Arduinoの使い方も必要最小限として Processing側との連携に関係する部分のみとしています。

# 材料

* Processing（Windows PCでセンサーデータを受信してグラフを描く）
    * Proicessing 3.4 をインストール
    * ライブラリ：ControlP5 （Proicessingのメニューからインストールできる）
* Arduino（センサーデータを送信する）
  * マイコン：ESP32（NodeMCU-32S というマイコンモジュールを使用）
  * モーションセンサー：MPU-6050（6軸：加速度3軸、ジャイロ3軸）


# 実装
## Arduino：センサーデータの送信

グラフ描画の元データを出力するための Aruduino側のプログラムを準備します。
今回は Processingでグラフを描くことが主目的なので、Arduino側の詳しい説明は割愛します。

一定周期（20ms毎）で取得したセンサーデータ（加速度：AccX,AccY,AccZ、ジャイロ：GyroX,GyroY,GyroZ）を文字列にしてシリアルに出力します。
受信側でセンサーデータだとわかるように文字列の先頭に目印も入れておきます。

``` java:motion_sensor_sample.ino
void loop()
{
  Serial.print("@SENSOR:");
  Serial.print(AccX); Serial.print(",");
  Serial.print(AccY); Serial.print(",");
  Serial.print(AccZ); Serial.print(",");
  Serial.print(GyroX); Serial.print(",");
  Serial.print(GyroY); Serial.print(",");
  Serial.print(GyroZ); Serial.println("");

  delay(20);            
}
```

Arduinoのプログラムを実行すると、シリアルに以下のような文字列が表示されます。
人間でも読める形式なので処理効率は悪くなるのでしょうが、動けばOKです。

``` text:文字列の出力例
@SENSOR:-14972,1504,560,-21,-121,-74
@SENSOR:-14984,1388,460,-21,-149,-63
@SENSOR:-14980,1440,552,-25,-142,-78
@SENSOR:-15052,1496,572,-36,-166,-59
@SENSOR:-15008,1476,400,-30,-151,-52
@SENSOR:-15036,1404,428,-10,-132,-58
@SENSOR:-14964,1468,536,-23,-141,-38
@SENSOR:-14924,1448,520,-2,-132,-62
@SENSOR:-14964,1424,468,-20,-136,-55
```


## Processing：センサーデータを受信、グラフの描画

Arduinoから送信されるセンサーデータをシリアルで Processing側で受信します。
受信したデータをグラフに描画するのですが、簡単にキレイなグラフを描くために ControlP5 ライブラリを使用します。

### 初期化

シリアルでセンサーデータを受信するために processing.serial.* をインポートしています。

ControlP5ライブラリを利用する場合には "import controlp5;" として ControlP5クラスを利用可能にします。
（事前に ControlP5ライブラリのインストールが必要です）
利用するクラスのインスタンスを保持する変数も用意しておきます。

* ControlP5クラス： GUIパーツを産み出す大元のクラスを１つ用意します。
* Chartクラス： 折れ線グラフを描くクラス。加速度用とジャイロように２つ用意します。

折れ線グラフにいくつデータを保持しておくかを指定する必要があるので、とりあえず定数として NUM_GRAPH_DATA という変数に200を設定して宣言しています。

``` java:
import processing.serial.*;
import controlP5.*;

ControlP5 cp5;
Chart chart_accl, chart_gyro;
final int NUM_GRAPH_DATA = 200;  /* グラフ１軸あたりのデータ数 */
```

プログラムを実行して最初に呼ばれる setup()メソッドの中でグラフを描くための準備をします。
今回は加速度３軸とジャイロ３軸のグラフの２つのグラフを作ります。１つのグラフには3軸分のデータを表示します。

* まず ControlP5 クラスのインスタンスを１つ作ります（ControlP5 を new して cp5変数へ格納する）
    * ControlP5クラスのインスタンスからグラフ用の Chartクラスのインスタンスを産み出していくイメージです。
* 時系列グラフは addChart()で追加します。戻り値として Chartクラスのインスタンスが返ってくるので変数（chart_raw）へ格納しておき、それを使ってグラフの属性等を設定をしていきます。
    * addDataSet("軸の名前")：文字列に軸を特定する名前を付けてデータ列をグラフに追加します。
    * setView(Chart.Line)：グラフの種類を指定します。LINEを指定すると折れ線グラフになります。LINE以外にもいろいろできるようです。
    * setRange（min, max）：グラフの縦軸の値の範囲を設定します。
    * setSize(width, height)：グラフの画面上のサイズをピクセル単位で設定します。
    * setPosition(x, y)：グラフを表示する画面上の位置を設定します。（グラフの左上隅がどこにくるかを指定する）

これらの属性設定用のメソッドは Chartクラスを返してくるので、"." でつなげて連続的に呼び出す記述ができます。
加速度のグラフとジャイロのグラフで2回 addChart()をやってます。

それぞれのグラフの背景色や線の太さも設定しておきます。

``` java:
void setup()
{
  // 画面の表示サイズ
  size(650, 700);

  // シリアル通信の設定
  println(Serial.list());
  String portName = Serial.list()[3]; /* USB-Serial(COM9)を選択する */
  println(portName);
  m_myPort = new Serial(this, portName, 115200);

  // 画面表示の更新レート（Hz） 
  frameRate(200);

  // graph by ControlP5 library
  cp5 = new ControlP5(this);

  // 加速度のグラフ設定
  chart_accl = cp5.addChart("Accl sensor");
  chart_accl.setView(Chart.LINE)                             /* グラフの種類（折れ線グラフ） */
            .setRange(-20000, 20000)                         /* 値の範囲（最小値、最大値） */
            .setSize(600, 200)                               /* グラフの表示サイズ */
            .setPosition(10, 250)                            /* グラフの表示位置 */
            .setColorCaptionLabel(color(0,0,255))            /* キャプションラベルの色 */
            .setStrokeWeight(1.5)                            /* グラフの線の太さを設定する */
            .getColor().setBackground(color(224, 224, 224))  /* グラフの背景色を設定する */
            ;

  setupChartAttr(chart_accl, "AcclX", color(0, 0, 192));
  setupChartAttr(chart_accl, "AcclY", color(192, 0, 0));
  setupChartAttr(chart_accl, "AcclZ", color(0, 192, 0));

  // ジャイロのグラフ設定
  chart_gyro = cp5.addChart("Gyro sensor");
  chart_gyro.setView(Chart.LINE)
            .setRange(-20000, 20000)
            .setSize(600, 200)
            .setPosition(10, 480)
            .setColorCaptionLabel(color(0,0,255))
            .setStrokeWeight(1.5)
            .getColor().setBackground(color(224, 224, 224))
            ;

  setupChartAttr(chart_gyro, "GyroX", color(0, 0, 192));
  setupChartAttr(chart_gyro, "GyroY", color(192, 0, 0));
  setupChartAttr(chart_gyro, "GyroZ", color(0, 192, 0));
}
```

次に軸毎の設定です。
１つの軸に対する処理を加速度３軸、ジャイロ３軸の計６回やるのですが、同じ処理なので関数にまとめました。（setupChartAttr()）

* setData("軸の名前", 表示データを格納する配列)：表示に使用するデータの配列を設定します。
* setStrokeWeight(線の太さ)：グラフの線の太さを設定します。

この処理を軸の数だけ記述します。

``` java:
////////////////////////////////////////////////
void setupChartAttr(Chart chart, String axis_name, int col)
{
  chart.addDataSet(axis_name);
  chart.setData(axis_name, new float[NUM_GRAPH_DATA]);
  chart.setColors(axis_name, col, color(255,255,128));
}
```


### センサーデータの受信

センサーデータを格納するクラスを作っておきます。
簡単に加速度３軸とジャイロ３軸の値を１セット分保持しておくだけの構造体のような感じです。

``` java:
class SensorData
{
  int AcclX, AcclY, AcclZ;
  int GyroX, GyroY, GyroZ;

  // constructor
  SensorData()
  {
    AcclX = AcclY = AcclZ = 0;
    GyroX = GyroY = GyroZ = 0;
  }

  //
  void setPacket(int[] packet)
  {
    if (packet.length < 6)
    {
      return;
    }
    
    AcclX = packet[0];
    AcclY = packet[1];
    AcclZ = packet[2];
    GyroX = packet[3];
    GyroY = packet[4];
    GyroZ = packet[5];
  }
};
```

Processingではシリアルのデータを受信したら serialEvent()メソッドが呼ばれます。
シリアルでデータを受信したら、以下のようなフォーマットの文字列になっているので、これを分解してセンサーデータを抽出します。

``` text:
@SENSOR:-14972,1504,560,-21,-121,-74
```


* １行分のデータを読み込む
* 文字列を解析する
    * 文字列を ":" で分割する（"@SENSOR" と "-14972,1504,560,-21,-121,-74" になる）
    * センサーデータかどうかを確認する
        * 分割した文字列の１番目が "@SENSOR" ならセンサーデータと判断する。
    * センサーデータを取り出す
        * 分割した文字列の２番目をさらに "," で分割する
        * それぞれを整数値へ変換して m_sensor_data へ格納する

``` java:
////////////////////////////////////////////////
void serialEvent(Serial myPort) { 
   String mystr = myPort.readStringUntil('\n');
   mystr = trim(mystr);

  if ((mystr != null) && (m_sensor_data == null))
  {
     String[] str_array = split(mystr, ":");
     if (str_array[0].equals("@SENSOR"))
     {
       SensorData s_data = new SensorData();
       s_data.setPacket(int(split(str_array[1], ",")));
       m_sensor_data = s_data;
     }
  }
}
```

センサーデータを受け取ったので、ここでグラフ描画処理をしてしまいたくなりますが、それだとうまくいかないようでした。
ここでは変数にセンサーデータを格納するだけにして、グラフ描画はメイン処理の loop()内で行うことにします。



### グラフの描画
センサーから受け取ったデータを逐次グラフへ投入していきます。
Processingでは周期的にdraw()メソッドが呼ばれるので、その中でChartクラスのインスタンスに対して unshift()メソッドでセンサーデータを追加していくとグラフ描画に反映されます。
（グラフ描画のために明示的に chart_accl.draw(); などとする必要はないようです）
chart.addDataSet(string) で指定した文字列と同じ軸にデータを追加することになるので、unshift()の1番目の引数に指定する文字列はそれと同じ文字列を指定します。

``` java:
void draw()
{
  background(#EEEEEE);

  if (m_sensor_data != null)
  {
    chart_accl.unshift("AcclX", m_sensor_data.AcclX);
    chart_accl.unshift("AcclY", m_sensor_data.AcclY);
    chart_accl.unshift("AcclZ", m_sensor_data.AcclZ);

    chart_gyro.unshift("GyroX", m_sensor_data.GyroX);
    chart_gyro.unshift("GyroY", m_sensor_data.GyroY);
    chart_gyro.unshift("GyroZ", m_sensor_data.GyroZ);

    m_sensor_data = null;
  }
}
```

# 動作確認

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-theme="light"><p lang="ja" dir="ltr">加速度、ジャイロセンサーの値をグラフ表示してみた。<br>ProcessingでControlP5というライブラリを使ったら簡単にグラフが描ける。<br>機体が傾く絵はがんばって自前で描いてるけどProcessingなら回転させるのは楽にできるな。 <a href="https://t.co/aPueTQD7DG">pic.twitter.com/aPueTQD7DG</a></p>&mdash; TK303 (@TK303_tw) <a href="https://twitter.com/TK303_tw/status/1343689334533177344?ref_src=twsrc%5Etfw">December 28, 2020</a></blockquote>


# 最後に
この記事がQiita初投稿です。
冬休み中というのもあったし、いつかはQiitaに投稿したいとは思っていたから良い機会だと思って記事を書いてみましたが、誰かに見られるかと思うと書くのに結構時間がかかってしまいますね。
とりあえずブン投げて後から修正していくっていうスタイルでもいいのかな？

# 参考

* ControlP5のリファレンスマニュアル： http://www.sojamo.de/libraries/controlP5/reference/index.html
    * ある程度動かせるようになったら公式の情報を見ながら試行錯誤するのが良いと思います。
* サンプルコード :  https://github.com/L0stSoul/Processing/blob/master/libraries/controlP5/examples/controllers/ControlP5chart/ControlP5chart.pde
    * Chartクラスで時系列グラフを描いているサンプルとして参照しました。ControlP5は色の指定や描画の更新が独特なので参考になりました。