M5Stack

M5Stackとセンサで作るカラーリキッド(後編)

前編:M5Stackとセンサで作るカラーリキッド

 

ここではM5Stackを使ったユニークな電子工作を前編、後編の2回に分けて紹介していきたいと思います。

今回のユニークな電子工作を紹介してくれるのは、物事の関係性をテーマに活動するアーティストである平原真さんです。長岡造形大学准教授でもある平原さんは、これまでもコンピュータや電子デバイスを使ったメディアアートを多数制作されており、Device Plusでも「Arduinoを使ったソーラーパネルで動くデジタル飼育箱」や「ArduinoとTOF距離センサでつくるドーナツプレーヤ」「Arduinoと加速度センサで作るデジタルボール転がし迷路」「Arduinoと赤色レーザーで作るレーザーストリングス」といったオリジナリティ溢れる素晴らしい電子工作を紹介してくれています。それでは早速見ていくことにしましょう!

 

目次

  1. はじめに
  2. 筐体の作成
    1. 4.1. 筐体の設計
    2. 4.2. 部品の制作(レーザー加工機)
    3. 4.3. 圧力センサの取り付け
    4. 4.4. 板の接着
    5. 4.5. 部品の取り付け
    6. 4.6. M5Stackの取り付け
  3. M5Stackのプログラム
    1. 5.1. 色の読み取りと調整
    2. 5.2. 加速度の読み取り
    3. 5.3. 圧力の読み取り
    4. 5.4. 液晶ディスプレイへの表示
    5. 5.5. UDPで値の送信
    6. 5.6. スケッチ全体
    7. 5.7. 色の読み取りテスト
  4. Touch Designerのプログラム
    1. 6.1. TouchDesigner とは
    2. 6.2. 全体の構造
    3. 6.3. UDPの受け取り
    4. 6.4. 受け取った値の調整
    5. 6.5. 全体の構造
    6. 6.6. UDPの受け取り
    7. 6.7. 受け取った値の調整
  5. おわりに

 

はじめに

みなさん、こんにちは。平原です。

この記事では、身の回りの物から色を吸い取り、液体のように流す事ができる玩具を作ります。カラーセンサで色を読み取り、M5Stack から Wifi でPCにデータを送り、TouchDesigner で液体のようなグラフィックを表示します。

前回の前編では、M5Stackの開発環境のセットアップと、センサの配線をおこない、サンプルプログラムを試してみました。今回の後編は、レーザー加工機で筐体を作り、Wi-Fiでデータを送り、TouchDesignerで水が流れるようなビジュアルを作ります。それでは早速筐体の設計から見ていきましょう!

 

4. 筐体の作成

4.1. 筐体の設計

前編の「1.3. 筐体のデザイン」では、部品の位置関係や大体の大きさをスケッチしました。それを元に、実際にシナベニヤを切断するためのデータを作成します。今回は3DCADソフトのFusion360を使用して設計しました。実物のM5Stack Basicやセンサの寸法を測り、部品同士が干渉したり、ケーブルを取り回せるようにしています。

color-liquid-with-m5stack-02-01

平面図を書き出し、Adobe Illustrator でレーザー加工用のデータを作成します。配布ファイルの「CutData.ai」 を確認してください。赤い線が切断する箇所です。板の厚さは4mmです。

作成例を作るために必要なデータを以下のリンクからダウンロードしてください。
>> 配布ファイル

color-liquid-with-m5stack-02-02

 

4.2. 部品の制作(レーザー加工機)

厚み4mmのシナベニヤをレーザー加工機で切断します。厚みが一致していれば他の素材でも構いません。右上の丸い部品は、圧力センサに取り付けるので、捨てないように注意してください。

color-liquid-with-m5stack-02-03

 

4.3. 圧力センサの取り付け

圧力センサを前面に固定します。裏面についている両面テープの剥離紙を剥がして、裏板の中央に貼り付けてください。次に、センサが前面の穴から見えるように重ねて、なべネジで4箇所を固定してください。

 

小さく切った両面テープを押板に貼り、前面の穴からセンサに貼り付けてください。

 

4.4. 板の接着

左面を除く板を接着して筐体を作ります。接着する箇所に木工用接着剤を塗り、張り合わせてからマスキングテープなどで固定してください。はみ出した接着剤は乾燥する前に濡れた布で拭き取ってください。室温で24時間静置してください。

 

4.5. 部品の取り付け

3つのユニットを筐体に固定します。裏側に両面テープを貼り、下面に貼り付けてください。カラーセンサユニットは、センサ穴と下面の穴の位置を合わせてください。

ミニブレッドボードの裏側の剥離紙を剥がして、右面に貼り付け、圧力センサを接続してください。

 

4.6. M5Stackの取り付け

M5Stack Basicを上面の穴から入れてください。中板に当たる高さまで入ると上面がフラットになります。

最後に配線を行います。カラーセンサユニットと加速度センサユニットをハブユニットにつなぎ、ハブユニットのケーブルをM5Stack Basic のGROVEコネクタに接続してください。

圧力センサーを繋いだミニブレッドボードから、Vcc, GND, VoutをM5Stack Basic の3.3v, GND,AD(36)に接続してください。

電源をつなぎ、サンプルプログラムを動作させ、先程と同じように、カラーセンサ、加速度センサ、圧力センサの値がディスプレイに表示されていれば、ハードウェアは完成です!お疲れさまでした。

 

5. M5Stackのプログラム

次にM5Stack のプログラムを見ていきましょう。今回の作品に必要な機能は、主に以下の5つです。

  1. 色の読み取りと調整
  2. 加速度の読み取り
  3. 圧力の読み取り
  4. 液晶ディスプレイへの表示
  5. UDPで値の送信

それぞれの機能について説明したあとで、全体のプログラムを掲載します。

 

5.1. 色の読み取りと調整

色を読み取る準備として、ライブラリの読み込み、オブジェクトの生成を行います。

#include "Adafruit_TCS34725.h"//ライブラリ読み込み
Adafruit_TCS34725 tcs = Adafruit_TCS34725(TCS34725_INTEGRATIONTIME_50MS, TCS34725_GAIN_16X);//カラーセンサーのオブジェクトを生成

setup関数ではカラーセンサとの通信を始め、感度や更新頻度の設定をします。

while (!tcs.begin()) {
  Serial.println("No TCS34725 found ... check your connections");
  M5.Lcd.drawString("No Found sensor.", 50, 100, 4);
  delay(1000);
}
tcs.setIntegrationTime(TCS34725_INTEGRATIONTIME_154MS);
tcs.setGain(TCS34725_GAIN_4X);

loop関数の中で、getRawDataを呼び出すとセンサの生の値が取得できます。

uint16_t red, green, blue, clear;
tcs.getRawData(&red, &green, &blue, &clear);//カラーセンサーから値を取得する

光源の色やセンサの個体差の影響などにより、正しく取れない場合があるので、補正をかけます。
まず、M5Stack Basicのボタンを押して、基準となる白色と黒色を設定します。

M5.update();
 if ( M5.BtnA.wasPressed() ) {//左ボタンが押されたら、現在の色を白の基準とする
   white[0] = red;
   white[1] = green;
   white[2] = blue;
 }

 if ( M5.BtnB.wasPressed() ) {//中ボタンが押されたら、現在の色を黒の基準とする
   black[0] = red;
   black[1] = green;
   black[2] = blue;
 }

次に、白を最大値、黒を最小値として、取得した色を補正します。

float adjustColor[3];
 adjustColor[0] = (red - black[0]) / (white[0] - black[0]) * 255; //白と黒を基準に色を調整する
 adjustColor[1] = (green - black[1]) / (white[1] - black[1]) * 255;
 adjustColor[2] = (blue - black[2]) / (white[2] - black[2]) * 255;
 adjustColor[0] = constrain( adjustColor[0], 0, 255 );//0~255に納める
 adjustColor[1] = constrain( adjustColor[1], 0, 255 );
 adjustColor[2] = constrain( adjustColor[2], 0, 255 );

 

5.2. 加速度の読み取り

加速度センサを使用するために、ライブラリの読み込み、オブジェクトの生成をおこないます。

#include <ADXL345.h>//ライブラリ読み込み
ADXL345 accel(ADXL345_ALT);//加速度センサーのオブジェクト生成

setup関数の中で、加速度センサとの通信を始め、各種設定をおこないます。

byte deviceID = accel.readDeviceID();

loop関数の中で、getX()関数などを使い、それぞれの軸の加速度の値を取得します。

if (accel.update()) {
  acceleration[0] = (int)(1000 * accel.getX());
  acceleration[1] = (int)(1000 * accel.getY());
  acceleration[2] = (int)(1000 * accel.getZ());
} else {
  Serial.println("update failed");
  while (1) {
    delay(100);
  }
}

 

5.3. 圧力の読み取り

圧力センサから入力するピン番号を指定します。

int PRESSURE_PIN = 36;//センサーを接続するPIN

loop関数の中で、指定されたピンの電圧に比例した値を取得します。基準電圧は3.3V、分解能は12bitなので最大値は4095になります。

pressure = analogRead(PRESSURE_PIN);

ボタンを一定以上の強さで押すと色が保持されます。

if ( pressure < pressureThreshold ) {//圧力センサーの値が閾値を超えた時
  captureColor[0] = adjustColor[0];//現在の色を保持
  captureColor[1] = adjustColor[1];
  captureColor[2] = adjustColor[2];
}

 

5.4. 液晶ディスプレイへの表示

M5Stack にはディスプレイに文字や図形を表示するための機能が用意されています。この作例では、まずM5.Lcd.clearという関数を使って、ディスプレイ前面を読み取った色で塗りつぶしています。

uint16_t capColor = M5.Lcd.color565( captureColor[0], captureColor[1], captureColor[2] );//uint16に変換
M5.Lcd.clear(capColor);

次に、M5.Lcd.fillCircleという関数で、圧力センサの値に応じた大きさの円を描画します。

uint16_t adjColor = M5.Lcd.color565( adjustColor[0], adjustColor[1], adjustColor[2] );//uint16に変換
int radius = int( ((float)pressure / pressureThreshold) * 120);
M5.Lcd.fillCircle(160, 120, radius, adjColor);

他にも、M5.Lcd.drawRect()、M5.Lcd.drawTriangle()など、文字や図形を表示する関数がいろいろあるので調べて使ってみてください。

 

5.5. UDPで値の送信

WiFiとUDP通信のためのライブラリを読み込みます。

#include <WiFi.h>//ライブラリの読み込み
#include <WiFiUDP.h>
WiFiUDP wifiUdp;//UDP通信のオブジェクトを生成

SSID、パスワード、PCのIPアドレスをお使いの環境に合わせて入力してください。

const char ssid[] = "XXXXXXXXXXX"; //WiFIのSSIDを入力
const char pass[] = "XXXXXXXXXXX"; // WiFiのパスワードを入力
const char *pc_addr = "xxx.xxx.xxx.xxx";//送信先のIPアドレス

setup関数内で初期化をおこない、通信を開始します。

WiFi.begin(ssid, pass);
while ( WiFi.status() != WL_CONNECTED) {
  delay(500);
  M5.Lcd.print(".");
}
wifiUdp.begin(my_port);

sendUDPという関数を使い、保持した色の値とZ軸の加速度センサの値をカンマ区切りの文字列として、PCの指定したポートへ送信します。

sendUDP( String( int(captureColor[0]) ) + "," + 
         String( int(captureColor[1]) ) + "," + 
         String( int(captureColor[2]) ) + "," + 
         String( acceleration[2] ) ,
         pc_port );

 

5.6. スケッチ全体

下記のソースコードを M5Stack Basic に書き込んでください。配布ファイルの「ColorLiquidM5.ino」です。

#include <M5Stack.h>//M5Stackの機能を読み込み

// Color =======================================
#include "Adafruit_TCS34725.h"//ライブラリ読み込み
Adafruit_TCS34725 tcs = Adafruit_TCS34725(TCS34725_INTEGRATIONTIME_50MS, TCS34725_GAIN_16X);//カラーセンサーのオブジェクトを生成

float captureColor[3] = {0, 0, 0}; //保存した色
float white[3] = {275, 395, 490};//白とする基準の値
float black[3] = {92, 123, 144};//黒とする基準の値

// Accel =======================================
#include <ADXL345.h>//ライブラリ読み込み
ADXL345 accel(ADXL345_ALT);//加速度センサーのオブジェクト生成
int acceleration[3] = {0,0,0};//加速度センサーの値

// Pressure =======================================
int PRESSURE_PIN = 36;//センサーを接続するPIN
int pressure = 0;//現在の圧力センサーの値
int prePressure = 0;//前の圧力センサーの値
int pressureThreshold = 3000;//センサーが反応するしきい値

// Wifi =========================================
#include <WiFi.h>//ライブラリの読み込み
#include <WiFiUDP.h>
WiFiUDP wifiUdp;//UDP通信のオブジェクトを生成

//ネットワーク情報
const char ssid[] = "Buffalo-G-068D"; //WiFIのSSIDを入力
const char pass[] = "oPfTxJ39sX5Fd"; // WiFiのパスワードを入力
const char *pc_addr = "192.168.11.43";//送信先のIPアドレス
const int pc_port = 50001;//送信先のポート
const int my_port = 50000;  //自身のポート

// Other =========================================
bool isDebug = false;//デバッグモードのフラグ


void setup() {
  // M5Stack =======================================
  M5.begin();
  M5.Power.begin();
  M5.Speaker.begin();//ノイズ対策
  M5.Speaker.mute();//ノイズ対策
  M5.lcd.setTextSize(2);
  pinMode(PRESSURE_PIN, ANALOG);

  // Color =======================================
  while (!tcs.begin()) {
    Serial.println("No TCS34725 found ... check your connections");
    M5.Lcd.drawString("No Found sensor.", 50, 100, 4);
    delay(1000);
  }
  tcs.setIntegrationTime(TCS34725_INTEGRATIONTIME_154MS);
  tcs.setGain(TCS34725_GAIN_4X);

  // Accel =======================================
  byte deviceID = accel.readDeviceID();
  if (deviceID != 0) {
    Serial.print("0x");
    Serial.print(deviceID, HEX);
    Serial.println("");
  } else {
    Serial.println("read device id: failed");
    while (1) {
      delay(100);
    }
  }

  if (!accel.writeRate(ADXL345_RATE_200HZ)) {
    Serial.println("write rate: failed");
    while (1) {
      delay(100);
    }
  }

  if (!accel.writeRange(ADXL345_RANGE_16G)) {
    Serial.println("write range: failed");
    while (1) {
      delay(100);
    }
  }

  if (!accel.start()) {
    Serial.println("start: failed");
    while (1) {
      delay(100);
    }
  }

  // UDP --------------------------------------------
  WiFi.begin(ssid, pass);
  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    M5.Lcd.print(".");
  }
  wifiUdp.begin(my_port);

}


void loop() {

  //Color ============================================
  uint16_t red, green, blue, clear;
  tcs.getRawData(&red, &green, &blue, &clear);//カラーセンサーから値を取得する

  M5.update();
  if ( M5.BtnA.wasPressed() ) {//左ボタンが押されたら、現在の色を白の基準とする
    white[0] = red;
    white[1] = green;
    white[2] = blue;
  }

  if ( M5.BtnB.wasPressed() ) {//中ボタンが押されたら、現在の色を黒の基準とする
    black[0] = red;
    black[1] = green;
    black[2] = blue;
  }

  float adjustColor[3];
  adjustColor[0] = (red - black[0]) / (white[0] - black[0]) * 255; //白と黒を基準に色を調整する
  adjustColor[1] = (green - black[1]) / (white[1] - black[1]) * 255;
  adjustColor[2] = (blue - black[2]) / (white[2] - black[2]) * 255;
  adjustColor[0] = constrain( adjustColor[0], 0, 255 );//0~255に納める
  adjustColor[1] = constrain( adjustColor[1], 0, 255 );
  adjustColor[2] = constrain( adjustColor[2], 0, 255 );

  // Pressure ==========================================
  pressure = analogRead(PRESSURE_PIN);

  //Accel ===============================================
  if (accel.update()) {
    acceleration[0] = (int)(1000 * accel.getX());
    acceleration[1] = (int)(1000 * accel.getY());
    acceleration[2] = (int)(1000 * accel.getZ());
  } else {
    Serial.println("update failed");
    while (1) {
      delay(100);
    }
  }

  // Processing  ==========================================
  if ( ( pressure < pressureThreshold ) && (prePressure > pressureThreshold ) ) {//圧力センサーの値が閾値を超え、もとに戻った時
    captureColor[0] = adjustColor[0];//現在の色を保持
    captureColor[1] = adjustColor[1];
    captureColor[2] = adjustColor[2];
  }
  prePressure = pressure;

  //取り込んだ色とZ軸の加速度の値をUDPで送信する
  sendUDP( String( int(captureColor[0]) ) + "," + 
           String( int(captureColor[1]) ) + "," + 
           String( int(captureColor[2]) ) + "," + 
           String( acceleration[2] ) ,
           pc_port );

  // Display ===================================================
  uint16_t capColor = M5.Lcd.color565( captureColor[0], captureColor[1], captureColor[2] );//uint16に変換
  M5.Lcd.clear(capColor);

  uint16_t adjColor = M5.Lcd.color565( adjustColor[0], adjustColor[1], adjustColor[2] );//uint16に変換
  int radius = int( ((float)pressure / pressureThreshold) * 120);
  M5.Lcd.fillCircle(160, 120, radius, adjColor);

  // Debug =====================================================
  if ( M5.BtnC.wasPressed() ) {//右ボタンが押されたら、数値の表示を切り替える
    isDebug = !isDebug;
  }
  
  if ( isDebug ) {
    M5.Lcd.setCursor(0, 10);

    M5.Lcd.print("IP address:");
    M5.Lcd.println(WiFi.localIP());

    M5.Lcd.print( "pressure:" );
    M5.Lcd.println( pressure );

    M5.Lcd.print( "raw R:" );
    M5.Lcd.println( red );
    M5.Lcd.print( "raw G:" );
    M5.Lcd.println( green );
    M5.Lcd.print( "raw B:" );
    M5.Lcd.println( blue );

    M5.Lcd.print( "adj R:" );
    M5.Lcd.println( adjustColor[0] );
    M5.Lcd.print( "adj G:" );
    M5.Lcd.println( adjustColor[1] );
    M5.Lcd.print( "adj B:" );
    M5.Lcd.println( adjustColor[2] );

    M5.Lcd.print( "accX:" );
    M5.Lcd.println( acceleration[0] );
    M5.Lcd.print( "accY:" );
    M5.Lcd.println( acceleration[1] );
    M5.Lcd.print( "accX:" );
    M5.Lcd.println( acceleration[2] );
  }

  delay(1);
}


void sendUDP(String _msg, int _port) {
  int len = _msg.length() + 1;//文字数
  char charArray[len];//Char型の配列を用意
  _msg.toCharArray( charArray, len );//StringからChar型配列に変換

  uint8_t message[len];//Uint8型の配列を用意
  for (int i = 0; i < len; i++) {
    message[i] = uint8_t(charArray[i]);//一つずつキャストしながら代入
  }

  wifiUdp.beginPacket(pc_addr, _port);//パケット開始
  wifiUdp.write(message, sizeof(message));
  wifiUdp.endPacket();//パケット終了
}

 

5.7. 色の読み取りテスト

書き込みが完了すると色を読み取れる状態になります。色見本(ColorSample.pdf)を印刷して、筐体を好きな色の上に乗せて、丸いボタンを押してください。圧力に応じて円が表示され、一定以上の力になると保持されます。

色がずれる場合は白と黒を設定してキャリブレーションをおこないます。まず白いものの上に乗せ、M5Stack Basicの「左ボタン」を押し、続いて黒いものの上に乗せ「中ボタン」を押してください。

 

6. Touch Designerのプログラム

6.1. TouchDesigner とは

カナダのDerivative社が開発するノードベースの開発・実行環境です。グラフィックの生成、デバイスの制御、多画面への出力などを得意とし、メディアアート、インタラクティブコンテンツ、映像演出等で広く利用されています。

公式サイト https://www.derivative.ca/

ライセンス

●NonCommercial:無料
最大解像度が1280 x 1280に制限された体験版。商用利用禁止。

●Commercial:600ドル
最大解像度の制限が無い。商用利用可。ライブパフォーマンスなどで使用するならこれ。

●Pro:2200ドル
プログラムの解析プロテクトが可能。展示品として納品するならこれ。

非商用向けの体験版ライセンスは、最大解像度が1280 x 1280に制限されているものの、それ以外の機能は製品版と同様に使えるので、今回は体験版をインストールしてください。

インストール

公式サイトのダウンロードページからファイルをダウンロード。ファイルを展開しインストールしてください。アプリを起動すると、ユーザ名とパスワードを求められます。アカウントを持っていなければ、Not registerted yet?をクリックして、公式サイトでアカウントを作ってください。

ユーザー名とパスワードを入力し、体験版用のキーを作成します。

 

インターフェース

 

画面の中央がメインの操作画面です。その中にある四角はオペレータと呼ばれる機能を表すものです。
オペレータ同士を線で繋いでプログラムをします。オペレータの左側は入力、右側は出力です。データは左から右に流れます。右側のパラメータウィンドウには、現在選択中のオペレータ(緑色の枠)の情報が表示されます。左側のパレットブラウザは、オペレータを組み合わせて作られた便利なプログラムが入っています。下側のタイムラインは、現在の再生状態を表しています。TouchDesignerは、映像ソフトのように再生/停止の概念があります。停止するとプログラムが停止した状態となります。

メイン操作画面上でTabキー 又は ダブルクリックすると、オペレータを追加するためのダイアログが開かれます。Opクリエイトダイアログでは、検索窓に入力すると該当するオペレータがハイライトされます。

 

6.2. 全体の構造

配布ファイルの「ColorLiquid.toe」をTouch Designerで開いてください。TouchDesigner はオペレータ同士を線で繋いでプログラムをつくるので、エリアごとに機能が分かれています。赤いエリアは通信、緑のエリアは値の調整、青いエリアはパーティクルの生成、黄色いエリアはゆらゆらする水面、紫のエリアは2Dのエフェクトをかけています。以下で順番に説明していきます。

 

6.3. UDPの受け取り

赤いエリアは、M5Stack Basic から送信されたUDPを受信する通信機能です。実行環境に合わせて設定をおこないます。「udppin1」というオペレータをクリックすると、右上にパラメータウィンドウが開きます。その中のLocal Addressの項目に、Touch Designerを実行するPCのIPアドレスを入力してください。M5Stackのプログラムの中で設定した送信先のIPアドレスと同じです。

 

6.4. 受け取った値の調整

緑のエリアは、受け取ったデータの調整をしています。「select1」「select2」でRGBと加速度に分け、次の工程で使いやすいように倍率を変えたり、上限を決めたりしています。

 

6.5. パーティクルの生成

青いエリアはパーティクルに関する処理です。「particle1」はパーティクルを生成するオペレータです。Z軸の傾きが大きいほどたくさんの粒子を発生するように、パラメータ「Birth」にZ軸の値をつないでいます。また、読み取った色をマテリアルの色に割り当てています。

 

6.6. 水面の表示

黄色いエリアは水面の描画を行っています。「grid2」で格子状の平面を水平に配置し、「noise1」でゆらゆらと動かしています。注ぎ込まれた色の3秒間の平均をとることで、徐々に変化させています。

 

6.7. 2Dの処理

紫のエリアは、ブラーやレベル補正など画像処理をしています。

 

おわりに

TouchDesignerを再生状態にすれば準備完了です。筐体を傾けると画面の上部から読み取った色が流れてきます。傾きに応じて流れる幅が変わります。

今回は、液晶ディスプレイや、Wi-Fiモジュールでの通信、手軽にユニットを接続できるGROVEコネクタなど、M5Stack Basicの機能を使ってみました。他にも多くの機能が搭載されているので、いろいろ試してみてください。
また、TouchDesigner ではパーティクルを使って水の表現を行いましたが、NVIDIAのグラフィックボードを搭載したPCであれば、流体や物理演算など、より高度な表現も可能です。今回の作例の改造に挑戦してみてはいかがでしょうか。

 

今回の連載の流れ

前編:M5Stackとセンサで作るカラーリキッド
後編:M5Stackとセンサで作るカラーリキッド(今回)

アバター画像

物事の関係性をテーマに活動するアーティスト。コンピュータや電子デバイスを使ったメディアアートを多数制作しているが、近年は木や石など自然の素材を使った立体作品を手掛ける。長岡造形大学准教授。Oggoroggo Products 代表。著書に『実践Arduino! 電子工作でアイデアを形にしよう』(オーム社)。 https://makotohirahara.com/

スマホでコントロールできるロボットを作ろう