Seeeduino XIAOによるCO2モニター(5) フロントエンドProcessing編

2020年5月10日日曜日

CO2モニター Processing Seeeduino XIAO

t f B! P L
CO2モニター Processingフロントエンドアプリ
Processingはコンピューターアート系方面で人気のあるJavaによるGUI環境です。今回の用途ではグラフ描画 + シリアル通信によるデバイス設定程度であるため、例えばPython + Tkinterにmatplotlibでグラフ描画のほうがお手軽だったのですが、新しい環境の使い勝手の確認の意味も含めてProcessingを使用してみることにしました。またProcessingにはスケッチからの実行だけでなく、他のプラットフォーム向けにアプリケーションとしてエクスポートする機能がありますので、あくまでJava VMを前提とした動作になるはずですが、その簡便性についても試してみることにします。

フロントエンドアプリの機能

Arduinoのみのスタンドアロンではなくフロントエンド側を準備するので、シンプルな範囲でも最低限これくらいはということで、以下の機能を実装しました。
  • CO2濃度、温度、湿度の現在値の数値表示
  • CO2濃度、温度、湿度のグラフ表示
  • CO2濃度、温度、湿度のデータのCSVダンプ及びCSVロードからのグラフ表示
  • 設定メニュー(COMポート関連含む)

Processingの環境設定

Processingのダウンロードはこちらから。
素のProcessingは通常のウィンドウアプリケーションのようなコントロール系のGUIをやるにはあまり現実的ではないので、Processingを立ち上げたら外部ライブラリをインストールしていきます。

ControlP5

ControlP5はコントロール系のGUIを簡単に扱うためのライブラリです。Processingのスケッチ → ライブラリをインポート → ライブラリを追加 から ConrolP5で検索してインポートします。

grafica

ControlP5にはグラブ描画を扱うChartクラスがあるのですが、これがなかなか思い通りに使えなかったため、グラフ描画専用のgraficaをインポートして使いました。同様にProcessingのスケッチ → ライブラリをインポート → ライブラリを追加 から graficaで検索してインポートします。

なおライブラリをインポートすると、Processingの ヘルプ → ライブラリ・リファレンスから、ローカルにインストールされたリファレンスマニュアルを参照することができるのが非常に便利でした。
フロントエンドアプリの機能要件的に、Processingを使ったというよりは、ControlP5とgraficaのライブラリでアプリを構築したという感じですので、Processingの真価を知るにはやはりコンピューターアート系の用途で使うべきなのかも知れません。

Processingスケッチ

Processingスケッチも同様にエレホビカのGitHubのprocessing_CO2monitorディレクトリにありますのでgit cloneで取得してください。

また一部アイコンで使用するため、fontawesome-webfont.ttfを別途取得して、processing_CO2monitorディレクトリに配置してください。

($ git clone https://github.com/elehobica/CO2monitor.git)
$ cd CO2monitor/processing_CO2monitor/
$ curl -O https://github.com/beakerbrowser/beakerbrowser.com/raw/master/fonts/fontawesome-webfont.ttf

上記が取得できたら、processing_CO2monitor.pdeをProcessingで開いて実行します。USBポートにSeeeduino XIAOを接続して、COMメニューからCOMポートを選択すれば表示が開始されます。
COMポート選択

Processingスケッチの説明

Processingスケッチの基本構造

Processingスケッチの基本構造は、いつも見ているあの景色って感じですね。というわけでArduinoユーザーとしては非常に親しみが持てます。

import java.util.*;
import java.text.*;

void setup() {
  size(640, 620);
  surface.setTitle("CO2 Monitor");
  //パーツの配置など初期化処理
}

void draw() { // 描画イベントごとコールされる
  background(128);
  // 動的に変わる部分など、ここに記述
}

//コールバック類
public void mouseReleased() {
  // マウスボタン離したときの処理
}

マウスボタンを離した場合などのイベントはProcessing側のコールバックメソッドでグローバルにキャッチすることができます。

シリアル受信

フロンドエンドアプリ側ではシリアル送信は行わず、シリアル受信のみを行います。シリアル通信はProcessingの標準ライブラリであるSerialクラスを用いて行います。

import java.util.*;
import java.text.*;
import processing.serial.*;

void setup() {
  String comPorts[] = Serial.list();
  // COM PortとSpeedは実際は設定メニューから選択する
  // ただしSeeeduino XIAOの場合はUSB CDCなのでSpeed値は何でも良い
  String comPort = comPorts[0]; 
  Port = new Serial(this, comPort, 115200);
  Port.bufferUntil(10);
}

public void serialEvent(Serial Port) {
  String comText = Port.readStringUntil(10);
  print(comText);
}

COMポート名とSpeedでSerialクラスを初期化すれば、あとは受信するたびにserialEvent(Serial Port)がコールバックされるので、コールバック内に処理を書けばよいです。このあたりのシンプルさは、例えばNational Instruments社のLabWindows / CVIのシリアル通信環境と同様の感覚で良いです。
なお、Serial.list()にて有効なシリアルポートのリストが取得できます。

ControlP5によるGUIパーツ配置

基本的にはControlP5のインスタンスを1つ生成して、そこからaddXXX()にてGUIパーツを追加して、Javaらしくメソッドチェーンで属性を記述していくスタイルになります。以下はシリアルポート関連の設定メニューを作成していく際の例です。

import java.util.*;
import java.text.*;
import processing.serial.*;
import controlP5.*;

ControlP5 cp5;
String comPorts[];

void setup() {
  size(640, 620);
  surface.setTitle("CO2 Monitor");

  cp5 = new ControlP5(this);
  ControlFont cf0 = new ControlFont(createFont("Arial", 12));

  cp5.addScrollableList("comPort")
      .setFont(cf0)
      .setPosition(10, 30)
      .setSize(100, 60)
      .setBarHeight(20)
      .setItemHeight(20)
      .addItems(comPorts)
      .setOpen(false)
      .addCallback(
        new CallbackListener() {
          public void controlEvent(CallbackEvent theEvent) {
            if (theEvent.getAction() == ControlP5.ACTION_PRESS) {
              ScrollableList comPort = (ScrollableList) theEvent.getController();
              comPorts = Serial.list();
              comPort.setItems(comPorts);
            }
          }
        }
      )
      // Label
      .getCaptionLabel()
      .setText("select Port")
      .toUpperCase(false)
      ;
  
  cp5.addScrollableList("comSpeed")
      .setFont(cf0)
      .setPosition(10, 100)
      .setSize(100, 60)
      .setBarHeight(20)
      .setItemHeight(20)
      .addItems(comSpeeds)
      .setOpen(false)
      // Label
      .getCaptionLabel()
      .setText("select Speed")
      .toUpperCase(false)
      ;
  // === Load Properties ===
  cp5.loadProperties("sensor.properties");

}

public void comPort(int theValue) {
  // ScrollableList("comPort")の値が変更された場合のコールバック処理
}

public void comSpeed(int theValue) {
  // ScrollableList("comSpeed")の値が変更された場合のコールバック処理
    // === Save Properties ===
    cp5.saveProperties("sensor.properties");
}
public void mouseReleased() {
  // グローバルなマウスボタンを離したときのコールバック
}

コールバックはGUIパーツの値変更イベントに関しては、特にメソッドの登録なしに定義した名前と同名のコールバックメソッドを記述するだけで良いです。それ以外のマウスオーバーやマウスクリックなど、よりプリミティブなイベントをキャッチしたい場合は、addCallback()にてそれぞれのパーツにCallbackListener()を登録しておく形になります。ただし、GUIパーツのクラスによってはaddCallback()メソッドがない種類のものもあったりして若干戸惑いました。必要に応じてProcessing自体からのイベントのコールバックと組み合わせて挙動をコーディングしていきます。
ControlP5のGUIパーツの振る舞いについてですが、コントロール系アプリケーションのGUIパーツが通常こうあってほしいという振る舞いにデフォルトでなっていなかったり、あるいは一般的な振る舞いをするように設定しようにもなかなか簡単にはできなかったりということが時々ありました。例えば設定メニューをマウスで開いたのち、マウスを別の位置に移動したら自動でメニューは閉じて欲しいのですが、おそらくやり方はきっとあるはずですが、力不足でできませんでした。
ControlP5の良い点としては、すべてのパーツではありませんが、状態保持系のパーツについては、json形式での値の保存(saveProperties)、復元(loadProperties)が簡単にできる点が挙げられます。この機能はあって本当に楽ができました。

graficaによるグラフ描画

graficaはきめ細かい部分まで設定ができ、なかなか気が利いていると思いました。基本的にはグラフのプロットエリアを設定するGPlotクラスと、プロットする値を保持しておくGPointsArrayクラスへの操作が軸になります。
実際のスケッチでは、CO2濃度、温度、湿度のそれぞれの値の追加とグラフ描画を統一的に扱うためのクラスを定義して使用しており、graficaのみの流れがわかりにくいので簡略化した例を以下に記載します。

import java.util.*;
import java.text.*;
import grafica.*;

GPlot plot;
GPointsArray points;

void setup() {
  size(640, 620);
  surface.setTitle("CO2 Monitor");

  plot = new GPlot(this);
  points = new GPointsArray(3600); // プロットする点の数

  // プロットエリアのスタイル設定
  plot.setPlotPos(30, 70);
  plot.setOuterDim(520, 120);
  plot.setDim(520, 120);
  plot.setPointSize(2.0);
  plot.setPointColor(plotColor);
  plot.setLineColor(plotColor);
  plot.setAxesOffset(0);
  plot.setTicksLength(-4);
  plot.setTitleText(title);
  plot.getTitle().setRelativePos(0.05);
  plot.getYAxis().setRotateTickLabels(false);
  plot.getXAxis().setDrawTickLabels(drawXTicksLabels);
  plot.setMar(50, 40, 40, 20);
  // プロット値の設定
  for (int i = 0; i < 3600; i++) {
    // x, y値以外にグラフには表示されないがテキストのラベルも設定可能
    points.add(i, i*0.1, String.valueOf(i));
  }
  // プロット値をプロットエリアに関連付け(まだ描画はされない)
  plot.setPoints(points);
}

void draw() {
  // グラフの描画
  plot.defaultDraw(); // 通常はこれ一つでOK
  /*
  // プロットエリアの各パーツの描画する/しない、描画順序を細かく定める場合
  plot.beginDraw();
  plot.drawBackground();
  plot.drawBox();
  plot.drawGridLines(GPlot.VERTICAL);
  plot.drawXAxis();
  plot.drawYAxis();
  plot.drawTitle();
  plot.drawPoints();
  plot.drawLines();
  plot.endDraw();
  */
}


なお、GPointsArrayクラスに追加するプロット値に関してはX値、Y値ともにfloat(32bit)での扱いとなります。今回、X値は時間を扱うのですが、getTime()で得られるミリ秒はlong (64bit)ですし、1/1000にして秒にしたとしてもint(32bit)には収まりますが、float(32bit)には有効桁数不足となります。そこで今回は苦肉の策として、起動した時間を基準時間として、そこからの差分値をfloatに格納して表示することにしました。グラフのX軸 = 時間軸の目盛り表示はひと工夫する必要がありましたが、graficaの豊富なメソッドのおかげで問題なく対応できました。一方でfloatの有効桁数があふれる115日?程度前のデータをプロットしようとすると問題が生じますので、今後の課題とします。
また、defaultDraw()などで描画処理を行っているときに、非同期にplot.setPoints(points)が行われたりすると例外が発生します。今回はシリアル通信にて値を取得しますので、値は描画とは非同期に更新していく必要がありますので、GPointsArrayインスタンスとは別にqueueを設けて非同期更新が発生しないように対策しました。

アプリケーションのエクスポート

Processingの ファイル → アプリケーションとしてエクスポート から各プラットフォーム向けに実行ファイルをエクスポートできます。Windows10 64bit環境からは、Mac OS Xを除く Windows 32bit / 64bit、Linux 32bit / 64bit / ARM64bit / armv6hfの計6プラットフォームが出力されました。
アプリケーションとしてエクスポート

エクスポートされたアプリケーション
application.linux-armv6hfフォルダ以下をRaspberry Pi 3 ModelB+上のRaspbianで実行したところ問題なく動作しました。特にWindowsでの動作と挙動が違う点もなく、全く問題なく使えそうです。

Raspberry Pi 3 ModelB+での実行


ということで5回にわたって記載してきたCO2モニターはこれで終了です。閲覧いただきありがとうございました。

自己紹介

自分の写真
電子工作&プログラミング、オーディオ・音楽

注目の投稿

Raspberry Pi Pico Wで電波時計を合わせる (JJY標準電波エミュレータ)

Raspberry Pi Pico Wのアプリケーションとして 最少の周辺部品で電波時計むけJJYエミュレータ(時刻合わせ用)を製作しました。 ※2023年6月6日: ソースコード修正の内容を反映させました。 時刻合わせ風景 概要 電波時計は電波が届くところで使...

QooQ