モノ創りで国造りを

ハード/ソフト問わず知見をまとめてます

Processingでオシロスコープを作成(Draft)

GUIアプリ開発ツールについて

最近Procesisngにハマってます。

仕事で使うGUIの評価ツールを自作しようとして、何かいいツールはないか調査していました。
メジャーどころは以下

他にもあれこれあるかと思いますが、私の認識では上記4つがメジャーなGUIアプリ開発ツールです。
このどれを使うべきか調査する過程で、ProcessingにControlP5なるライブラリがあることを知りました。
ControlP5を使えば、ボタンとかスライダーとか評価ツールに必要なものはサクッと実装できるようです。
またProcessingはデザイナー用のツールということもあって、カッコいい印象があり
個人的に魅力を感じていました。 そんな中で、評価ツールも簡単に作れそうだということで、
Processingを採用することにしました。

この記事では、実際に制作過程を図示しながらProcessingとContorlP5の使い方を簡単に説明したいと思います。

制作物

最終的な完成品はこんな感じのUIです。
f:id:yuji2yuji:20180816220716p:plain

外部からPCへ入力された信号をDisplay上に表示します。
Display上のドットは入力信号の値を示しています(この図ではrandom()で疑似信号を生成しています) 。
Displayの縦軸のスケールは、ノブを回せば変化します。
入力信号は最大で4chです。
*シリアルポートは未実装ですが、現段階でのソースをページの末尾に添付します。

ちなみにアナログの入力信号をデジタル値に変換するのは、Processingではできませんので悪しからず。
AD変換の役目はArduinoに任せようと思います。Arduino側のソースコードも近いうちに作成して添付します。

Processingの始め方

Processingはココからダウンロードしてインストールできます。
ご使用のOSにあったものを選択してください。

Processingの使い方

Processingを起動すると以下の画面が表示されます。
f:id:yuji2yuji:20180818134126p:plain
白いところにコードを書いて、
再生ボタンf:id:yuji2yuji:20180818134342p:plainを押すか、Ctrl + R(Run)で プログラムが実行されます。
エラーやコンソールは下部のWindowに表示されます。
停止ボタンf:id:yuji2yuji:20180818134612p:plainを押すと、プログラムが止まります。
Ctrl + Tでソースコードを自動でフォーマットしてくれます。便利!!

Processingのプログラミングの基本

Processingではvoid setup()void draw()の関数を必ず使用します。
setup()は起動時に1度だけ実行される関数で、draw()は常時ループする関数です。
setup()には実行時のwindowサイズやフレームレート、起動時の画面の背景色や描画図形を記載します。
例えば

size(400,300);//windowサイズ 400 x 300に設定
frameRate(60);//フレームレートを60に設定
background(255, 0, 0);//背景色を赤色に設定

これでwindowとフレームレートが設定できます。

Processingの描画方法の基本

Processingでの図形の描画は、端的にまとめると以下の進め方になります。

  • 描画する色を決める
  • 描画する図形の枠線を決める
  • 描画する図形の形状と位置を決める

各図形について、この3つを繰り返すだけです。
例えばこれで四角形と楕円を描画できます。

fill(0);//これ以降の描画は黒色で塗りつぶす
noStroke();//枠線なし
rect(100, 200, 40, 50);//座標(100,200)の位置にサイズ(40,50)の四角形を描く

fill(255);//これ以降の描画は白色で塗りつぶす
stroke(0,0,255);//枠線は青色
strokeWeight(2);//枠線の太さは2ピクセル
ellipse(40, 80, 30, 40);//座標(40,80)の位置にサイズ(30,40)の楕円を描く

このオシロスコープの画像では、display部と各chのラベル部の合計5箇所に四角形を描画しています。
f:id:yuji2yuji:20180816220716p:plain
display上のドットは小さくてわかりにくいですが楕円を描画しています。
その他の図形はContorlP5によるトグルスイッチとノブです。

Processingで図形を動かす方法

通常のアプリ開発を経験したことのある方にとって少し違和感があるのは画像を動かす方法です。
通常のアプリ開発では、オブジェクトの位置を移動させるという手段をとりますが、
Processingでは図形を移動させる手段はなく、画面上の図形を背景色で一旦上塗りして
再度異なる位置に図形を描画することになります。
パラパラ漫画のように1枚1枚異なる絵を描くイメージですね。
例えば以下のコードでは、画面上の黒い四角形が右に移動していきます。

int xPos = 0;
void setup(){
  size(300,200);
  background(200);
  frameRate(60);
}

void draw(){
  background(20);
  fill(0);
  rect(xPos,100, 20, 20);
  xPos++;
}

結果
f:id:yuji2yuji:20180818083904p:plain
簡単に説明します。
void draw()にあるbackground(255);によって全画面が白色に塗られます。
次のコードで、黒い四角形が画面上に描画されます。

  fill(0);
  rect(xPos,100, 20, 20);

xPos++;で次に描画する四角形の位置を右に1ピクセルずらします。

background(255);の記述がない場合、画面が初期化されないため、
四角形がどんどん上書きされ続けてしまいます。
f:id:yuji2yuji:20180818084142p:plain

ちなみに画面の描画はvoid draw()の最後に実行されます。
以下のように、background(200);の記載を以下の箇所にすると画面上には何も表示されません。

void draw(){
  fill(0);
  rect(xPos,100, 20, 20);
  xPos++;
  background(200);
}

各関数の詳細な使い方については書き出すときりがないので、
適当な書籍・サイトを参照してください。
私が参考にしたのは以下の書籍です。

Processingをはじめよう 第2版 (Make: PROJECTS)

Processingをはじめよう 第2版 (Make: PROJECTS)

ControlP5の使い方

ControlP5の使い方は近いうちに。

オシロスコープUI作成による実例

前置きが長くなりましたが
ここからオシロスコープUIの作り方を説明します。
今までの説明で画面上への図形描画の方法を理解できたと思いますので、 ひとまず以下の図形の生成まで説明します。 f:id:yuji2yuji:20180818135250p:plain
コードは以下です。

//Set channel number
final int chNum = 4;

//Set microphones color
color[] ch = new color[chNum];

//Display Setting
final float dispSizeX = 480;
final float dispSizeY = 300;
final float disPosX = 20;
final float disPosY = 20;

//Label Setting
final float labelPosInitX = 540;
final float labelPosInitY = 50;
final float labelStpX = 120;;
final float labelStpY = 150;
final float labelSizeX = 50;
final float labelSizeY = 20;
final float txtSize = 14;

//Label step
final int[] stepX = {0,1,0,1};
final int[] stepY = {0,0,1,1};


void setup(){
  //Set window
   size(800,400);
   background(200,200,150);
   frameRate(60);
   
   setColor();
   
   //Display Setting  
   fill(10,10,10);
   stroke(150,100,0);
   strokeWeight(3);
   rect(disPosX,disPosY,dispSizeX,dispSizeY);
   
   //Label Setting
   for(int i=0;i<chNum;i++){               
    stroke(100);
    strokeWeight(1);
    fill(ch[i]);
    float labelPosX =  labelPosInitX+ labelStpX*stepX[i] + 15;
    float labelPosY = labelPosInitY + labelStpY*stepY[i] + 90;
    rect(labelPosX, labelPosY, labelSizeX, labelSizeY);
    String s = "  CH:" + i;
    textSize(14);
    fill(255);
    text(s, labelPosX, labelPosY + txtSize+2);
   }
}

void setColor(){
  ch[0] = color(200, 200, 0);
  ch[1] = color(50, 50, 255);
  ch[2] = color(50, 200, 50);
  ch[3] = color(255, 50, 255);
}

簡単に説明します。
以下は各定数をまとめて記載したものになります。
プログラムを書き進める中で随時追加していきます。

//Set channel number
final int chNum = 4;

//Set microphones color
color[] ch = new color[chNum];

//Display Setting
final float dispSizeX = 480;
final float dispSizeY = 300;
final float disPosX = 20;
final float disPosY = 20;

//Label Setting
final float labelPosInitX = 540;
final float labelPosInitY = 50;
final float labelStpX = 120;;
final float labelStpY = 150;
final float labelSizeX = 50;
final float labelSizeY = 20;
final float txtSize = 14;

//Label step
final int[] stepX = {0,1,0,1};
final int[] stepY = {0,0,1,1};

後々図形の位置と大きさを微調整することになるので、
定数はまとめて一か所に記載することをお勧めします。
以下のコードで、ウィンドウサイズ、色、フレームレートの設定を行います。

  //Set window
   size(800,400);
   background(200,200,150);
   frameRate(60);

以下のコードで、各chのラベルの色を作成します。

void setColor(){
  ch[0] = color(200, 200, 0);
  ch[1] = color(50, 50, 255);
  ch[2] = color(50, 200, 50);
  ch[3] = color(255, 50, 255);
}

以下のコードで、オシロスコープのディスプレイ部の設定をします。
色と枠線と四角形の位置とサイズを指定します。

   //Display Setting  
   fill(10,10,10);
   stroke(150,100,0);
   strokeWeight(3);
   rect(disPosX,disPosY,dispSizeX,dispSizeY);

以下のコードで、オシロスコープの各chのラベル設定をします。

 //Label Setting
   for(int i=0;i<chNum;i++){               
    stroke(100);
    strokeWeight(1);
    fill(ch[i]);
    float labelPosX =  labelPosInitX+ labelStpX*stepX[i] + 15;
    float labelPosY = labelPosInitY + labelStpY*stepY[i] + 90;
    rect(labelPosX, labelPosY, labelSizeX, labelSizeY);

    String s = "  CH:" + i;
    textSize(14);
    fill(255);
    text(s, labelPosX, labelPosY + txtSize+2);
   }

ラベルは4つあるので、for文で色、位置、サイズを指定しています。
ラベル上に記載するテキストは以下のコードで記載します。

    String s = "  CH:" + i;
    textSize(14);
    fill(255);
    text(s, labelPosX, labelPosY + txtSize+2);

注意すべきは、Rectは左上が(0,0)に設定されているのに対して
テキストは左下が(0,0)に設定されている点です。
ラベル上にテキストを表示するには、テキストのY座標を調整する必要があります。

ひとまずこれで動かない静的な図形が描画できました。

ControlP5の使い方

ProcessingにデフォルトではControlP5は実装されていません。
ContorlP5を使用するために、まずはライブラリをインストールします。
Tool > Toolを追加 をクリックし、ControlP5を選択します。
これだけでContorlP5を使う準備が整います。

ProcessingでContorlP5を使うためには以下のコードを記述し、
インスタンスを生成します。
ControlP5 cp5;

ContorlP5クラスには多数のUIがあります。

Toggleの実装

Toggleを実装します。
ControlP5クラスに

Knobの実装

Knobを実装していきます。

現状のソースコード

Draft時点でのソースを添付します。
今後実装予定な機能は
* シリアルポートへの対応 表示する信号をドットではなく線でつなげる ノブの数値の表示を、ノブの値ではなく、入力信号の最大値にする * トグルスイッチの処理

import processing.serial.*;
import controlP5.*;

Serial myPort;

ControlP5 cp5;
Toggle toggle;


final int chNum = 4;

//Set microphones color
color[] ch = new color[chNum];

//Display Setting
final float dispSizeX = 480;
final float dispSizeY = 300;
final float disPosX = 20;
final float disPosY = 20;

final float graphDotSize = 2;

//Graphsetting
final float labelSizeX = 50;
final float labelSizeY = 20;
final float txtSize = 14;
//Set Knob.
Knob[] knob = new Knob[chNum];
final float knobPosX = dispSizeX + disPosX+ 40;
final float knobPosY = 50;
final float knobStepX = 120;
final float knobStepY = 150;
final float knobSize = 40;
final int knobTickNum = 5;
final int[] stepX = {0,1,0,1};
final int[] stepY = {0,0,1,1};

int[] dataYMax = new int[chNum];

int cnt = 0;

void setup(){
  size(800,400);
  background(200,200,150);
  frameRate(60);
  setColor();
  //myPort = new Serial(this, portName, 9600); 
  
  cp5 = new ControlP5(this);
  
  //Set Display
  initDisp();

  //Set On/Off Switch.
  toggle = cp5.addToggle("SW1")
              .setLabel("ON/OFF")
              .setPosition(50,350)
              .setSize(40,20)
              .setValue(false)
              .setMode(ControlP5.SWITCH);
   
   for(int i=0;i<chNum;i++){
     knob[i] = cp5.addKnob("Knob"+i)
                  .setLabel("")
                  .setRange(0,5)
                  .setPosition(knobPosX + knobStepX*stepX[i], knobPosY + knobStepY*stepY[i])
                  .setRadius(knobSize)
                  .setNumberOfTickMarks(knobTickNum)
                  .setTickMarkLength(2)
                  .snapToTickMarks(true);
               
    stroke(100);
    strokeWeight(1);
    fill(ch[i]);
    float labelPosX = knobPosX + knobStepX*stepX[i] + 15;
    float labelPosY = knobPosY + knobStepY*stepY[i] + 90;
    rect(labelPosX, labelPosY, labelSizeX, labelSizeY);
    String s = "  CH:" + i;
    textSize(14);
    fill(255);
    text(s, labelPosX, labelPosY + txtSize+2);
   }
}

void draw(){
  for(int i=0;i<chNum;i++){
    float dotX = disPosX + cnt;
    float dotY = map(random(1000), 0, dataYMax[i], disPosY + dispSizeY, disPosY);
    if(dotY < disPosY){
      dotY = -100;
    }
    fill(ch[i]);
    noStroke();
    ellipse(dotX, dotY, graphDotSize, graphDotSize);
  }
  if(cnt>dispSizeX){
    //initialize display;
    initDisp();
  }
  cnt++;
}

void setColor(){
  ch[0] = color(200, 200, 0);
  ch[1] = color(50, 50, 255);
  ch[2] = color(50, 200, 50);
  ch[3] = color(255, 50, 255);
}

void initDisp(){
   fill(10,10,10);
   stroke(150,100,0);
   strokeWeight(3);
   rect(disPosX,disPosY,dispSizeX,dispSizeY);
   cnt = 0;
}

//Read Serial Data.
void serialEvent(Serial p){
  //x = p.read();
}

void Knob0(){
  getKnobValue(0);
}

void Knob1(){
  getKnobValue(1);
}

void Knob2(){
  getKnobValue(2);
}

void Knob3(){
  getKnobValue(3);
}

void getKnobValue(int ch){
  if(knob[ch].getValue() == 0.00){  
    dataYMax[ch] = 0x00;
  }else if(knob[ch].getValue() == 1.00){
    dataYMax[ch] = 0x0f;
  }else if(knob[ch].getValue() == 2.00){
    dataYMax[ch] = 0x7f;
  }else if(knob[ch].getValue() == 3.00){
    dataYMax[ch] = 0xff;
  }else if(knob[ch].getValue() == 4.00){
    dataYMax[ch] = 0x7ff;
  }else if(knob[ch].getValue() == 5.00){
    dataYMax[ch] = 0xfff;
  }
}

void SW1(){
  if(toggle.getBooleanValue()){
  //SW1 ON
  
  }else{
 //SW1 OFF
 
  }
}