====== p5.jsとGrove Beginner Kitで遊ぼう ====== このページではp5.jsからGrove Beginner Kitへのセンサ入力値やOLED出力等を実習するためのサンプルや解説をまとめます。なおこのページは最初のシリアル通信で利用するサンプルを雛形とし、各セクションで肉付けしつつ進めていきます。 {{page>p5js:12.serialport}} ==== 練習問題 ==== javascriptでは以下のように記述をすると任意の処理を定期的に呼び出すことができます。これを利用して、1秒おきにLEDが自動で点灯を繰り返すプログラムをjavascript側で実装してみましょう。 setInterval(function(){ // ここに実行したい処理を記述 },1000); 例えばsetup()の中にintervalを利用して記述をすると、errorになります。これはまだシリアル通信を開始していないのにシリアルポートにデータを書き込もうとしたためです。そこでユーザ側でintervalを開始するタイミングを任意で実行させることにします。keyPressed()関数を利用すると手軽にユーザ側からトリガーをつくれるので、「iキーを押したらsetIntervalを実行する」プログラムにしてみましょう。具体的には以下のようなコードをkeyPressed()関数に追加します。 setInterval(function(){ if( is_led_on )serial.write(1); else serial.write(2); is_led_on = !is_led_on; },1000); is_led_on変数はglobalで var is_led_on = false; 等として初期化して宣言しておきます。 ===== Rotary Potentiometer ===== {{:p5js:jun-28-2022_08-33-02.gif?nolink|}} シリアル通信を理解したところで、次はRotary Potentiometerを回し、その値をjs側でうけ、html要素にその結果を反映させてみます。 html要素には slider を利用してみます。Rotary Potentiometerを回すと、html上のスライダーが動くようにしてみます。先程の シリアル通信で gotSerialValuesから値が渡されるので、それに応じてslider要素を変更すればよいのですが、A0はanaloRead()関数を 利用しており、その値は 0 - 1023の間で変化します。シリアル通信での内容をひな形にして、sketch.jsとArduinoプログラムを以下のように書き換え、 まずは振る舞いを確認してみます。 var serial_values = [0]; var serial = new Serial(); function setup() { createCanvas(400, 400); } function draw() { background(220); textSize(48); textAlign(CENTER, CENTER); text(serial_values[0], width/2, height/2); } function gotSerialValues(values) { serial_values = values; } void setup() { // put your setup code here, to run once: Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: Serial.write(analogRead(0)); delay(30); } するとどうでしょうか?キャンバス上の数値は0-255の間で変化を繰り返している様子が観察できると思います。こちらで想定していた動作はそれぞれのツマミを回しきったところで0-1023まで変化するものでしたがどうやら何かおかしなことになっているようです。 この原因は、シリアル通信が8bit単位での通信を行っている為です。8bitは0-255までの表現が可能なわけですが、この値の範囲に対して、10bit(0-1023)を渡しているので、データは下位8bit分しか読み取られていないためです。そこでこの解決方法ですが、大きく分けて2種類あります。 * 値の範囲を0-255に変更してしまう。 * 別のやり方で0-1023を渡せるように送信方法を変更する まずは、簡単な値の範囲を狭めるやり方からです。そもそも0-1023の値を0-255に変更する場合は単純な値の比率を変更すればよいだけです。このような場合には便利なmap()という関数が用意されているので、そちらを利用します。具体的にはArduino側のコードを変更するだけです。 void setup() { // put your setup code here, to run once: Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: int a = analogRead(0); a = map(a, 0, 1023, 0, 255); Serial.write(a); delay(30); } 以上の処理で、右に回しきると0, 左に回しきると255となる動作になりました。2つめの方法に関してはプログラムがややこしくなってくるので、このページでは紹介しません。データを正しく取得するには、時間によってデータに切れ目が生じることや、データ長の最初と終わりを把握する必要があることなど、少し丁寧なプログラムが必要となります。このページではとにかくデータを1byte送信することの正しい理解をしておいてください。 ではここまで理解できたところで、最後にhtml上のスライダーを操作してみます。予めindex.htmlにスライダーを準備しておき、そのスライダーにjs側からアクセスします。
var serial_values = [0]; var serial = new Serial(); function setup() { createCanvas(400, 400); } function draw() { background(220); textSize(48); textAlign(CENTER, CENTER); text(serial_values[0],width/2, height/2); } function gotSerialValues(values) { serial_values = values; document.querySelector('#slider').value = serial_values[0]; }
===== Lightセンサー ===== {{:p5js:jun-28-2022_08-24-51.gif?nolink|}} では次はLightセンサーの値に基づいてp5jsキャンバスを暗くしたり明るくしたりしてみましょう。やり方は先程と同様になります。Lightセンサー値も0-1023なので、範囲を変換してPCに送信してみましょう。 var serial_values = [0]; var serial = new Serial(); function setup() { createCanvas(400, 400); } function draw() { background(serial_values[0]); textSize(48); textAlign(CENTER, CENTER); text(serial_values[0], width/2, height/2); } function gotSerialValues(values) { serial_values = values; } void setup() { Serial.begin(9600); } void loop() { int a = analogRead(6); a = map(a, 0,1023, 0,255); Serial.write(a); delay(30); } ===== マイクセンサー ===== {{ :p5js:jun-28-2022_08-57-54.gif?nolink |}} 音の入力を扱うことができますが、PCにもマイクがついているのであんまりp5.jsと連携させても面白くないかもしれませんが、コーディングの練習として実施します。今回は音声波形をp5.jsのキャンバスに表示してみたいと思います。これまで送信された最新のデータをhtml上に反映させてきましたが、それだけでは波形を見ることができないので、もう少し工夫してみます。まずは最新のデータを確認するプログラムにしてみます。
var serial_values = [0]; var serial = new Serial(); function setup() { createCanvas(400, 400); } function draw() { background(220); textSize(48); textAlign(CENTER, CENTER); text(serial_values[0],width/2, height/2); } function gotSerialValues(values) { serial_values = values; }
上記のコードに一旦戻し、Arduinoから読み取るアナログ値をA2に変更します。 int a = analogRead(2); 実行すると以下のようになります。 {{ :p5js:jun-28-2022_08-47-12.gif?nolink |}} というわけで、もう少し時系列を意識した表示になるよう sketch.js 側を工夫します。シリアル通信で取得したデータを400個分配列に追加していきます。配列のサイズが400個を超えたところでそれ以上配列が大きくならないように注意する必要があります。これを放っておくと起動すればするほどメモリ領域を消費して、最終的にはOS全体の挙動がカタカタしてしまうので注意が必要です。 var serial_values = [0]; var serial = new Serial(); function setup() { createCanvas(400, 400); } function draw() { background(220); noFill(); beginShape(); for( let i = 0; i < serial_values.length; i++){ vertex(i,height - serial_values[i] ); } endShape(); } function gotSerialValues(values) { for( let i = 0; i < values.length; i++){ serial_values.push(values[i]); } while(serial_values.length > 400){ serial_values.shift(); } } void setup() { // put your setup code here, to run once: Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: int a = analogRead(2); a = map(a, 0, 1023, 0, 255); Serial.write(a); delay(10); } 以上を実行すると本セクションの冒頭で紹介しているgifのような結果が閲覧できると思います。 ===== 加速度センサ ===== {{ :p5js:jul-05-2022_00-51-08.gif?nolink |}} このセクションでは加速度センサからのデータを基にp5js上での画面描画に反映してみます。具体的にはbeginner kitの傾きに応じてボールが転がるプログラムを一緒に記述していきましょう。seeed studio社が提供する[[https://wiki.seeedstudio.com/Grove-Beginner-Kit-For-Arduino/|Grove Beginner Kit for Arduino]]にある加速度センサ用のサンプルコードを参考にコードを記述していきます。 まずは以下のサンプルコードをArduinoに書き込みます。 //Gravity Acceleration #include "LIS3DHTR.h" #include LIS3DHTR LIS; //Hardware I2C #define WIRE Wire void setup() { Serial.begin(9600); while (!Serial) {}; LIS.begin(WIRE, 0x19); //IIC init delay(100); LIS.setOutputDataRate(LIS3DHTR_DATARATE_50HZ); } void loop() { if (!LIS) { Serial.println("LIS3DHTR didn't connect."); while (1); return; } //3 axis Serial.print(LIS.getAccelerationX()); Serial.print(","); Serial.print(LIS.getAccelerationY()); Serial.print(","); Serial.println(LIS.getAccelerationZ()); delay(500); } 上記のコードは3軸加速度センサの値を読み取って、500[ms]お気にカンマ区切りの文字列としてデータをPCに送信しています。さて、ここまでの学習ではセンサデータをmap関数を利用して扱いやすい数値(1byteに収まるように)にして値のやり取りを行っていました。今回は加速度値が小数点を含む値で送信されてきています。さらには値の種類はx,y,zと三種類あるのでこれまでとは少しデータ受信方法を考えないといけません。このような場合、ArrayBufferを利用する方法が正攻法になりますが、少し技術的な話が増えるので、ここでは文字列操作によって連続するデータを正しく取得する方法を検討します。 現在のArduinoコードでは、例えばx=0.05, y=0.00, z=1.00 の場合は 0.05,0.00,1.00 という文字列がPCに送信されています。さらに最後のzに関してはprintln関数を利用しているため、改行コードと呼ばれる改行を示すコード(13,10)が最後に付与されて送信されています。実際にPCがこのデータを受信すると次のような配列形式になっています。 [48, 46, 48, 53, 44, 48, 46, 48, 48, 44, 49, 46, 48, 48, 13, 10] ここで、数字を表す文字コードでは、以下の対応となっていますので、再度下記対応表をみて、上記文字コードを文字に置き換えてみてください。 * 44=',' * 46='.' * 48='0' * 49='1' * 53='5' いかがでしょうか?ただしく 0.05,0.00,1.00 が読めるようになったと思います。以上のことを理解したら、 - データが届くたびに順次配列に蓄える(push) - 改行コードの2つめである 10 が届いたらデータが一行分届いたことになる - ここまで蓄えたデータは文字コードが送られているので、文字に変換する - データはカンマ区切りになっているので、splitという関数を利用してカンマで区切られた文字列を配列に分ける - それぞれの文字列は順にx,y,zになっているので、parseFloat関数を利用して数値に変更して変数に保存する という手順で、p5.js上で扱えるようにします。 上記を踏まえて記述したコードは以下の通りです。 var serial_values = [0]; var serial = new Serial(); var x=0; var y=0 var z=0; function setup() { createCanvas(400, 400); } function draw() { background(220); textSize(18); textAlign(CENTER, CENTER); text(`${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)}`, width/2, height/2); } function gotSerialValues(values) { for( let i = 0; i < values.length; i++ ){ serial_values.push(values[i]); if( values[i] == 10){ let result = ""; for( s of serial_values){ result += String.fromCharCode(s); } //console.log("result:", result, "serial_values", serial_values); const splits = result.split(','); x = parseFloat(splits[0]); y = parseFloat(splits[1]); z = parseFloat(splits[2]); //console.log(x,y,z); serial_values = []; } } } //Gravity Acceleration #include "LIS3DHTR.h" #include LIS3DHTR LIS; //Hardware I2C #define WIRE Wire void setup() { Serial.begin(9600); while (!Serial) {}; LIS.begin(WIRE, 0x19); //IIC init delay(100); LIS.setOutputDataRate(LIS3DHTR_DATARATE_50HZ); } void loop() { if (!LIS) { Serial.println("LIS3DHTR didn't connect."); while (1); return; } //3 axis Serial.print(LIS.getAccelerationX()); Serial.print(","); Serial.print(LIS.getAccelerationY()); Serial.print(","); Serial.println(LIS.getAccelerationZ()); delay(30); } Arduino側のコードはdelayを30[ms]に変更しただけです。少しむずかしいと思いますが、じっくり読んでアルゴリズムを理解しましょう。 さあ、それではセンサデータがしっかりと取得できたのでこの値で画面上のボールを転がしてみます。400x400のキャンバス上の真ん中からスタートし、x,y軸の傾きに応じてボールの位置を移動させてみます。注意しておきたいのは、変数x,yに誤って浮動小数点にならないものが入力された場合はキャンセル処理をすること。もう一つは画面からはみ出さないように最大値、最小値の制約をつけておくことです。以下が動作するプログラムとなります。 var serial_values = [0]; var serial = new Serial(); var x=0; var y=0 var z=0; var ball = { x:200, y:200 } function setup() { createCanvas(400, 400); } function draw() { background(220); textSize(12); textAlign(LEFT, TOP); text(`${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)}`, 10,10); circle(ball.x, ball.y,20); } function gotSerialValues(values) { for( let i = 0; i < values.length; i++ ){ serial_values.push(values[i]); if( values[i] == 10){ let result = ""; for( s of serial_values){ result += String.fromCharCode(s); } const splits = result.split(','); x = parseFloat(splits[0]); if( !x )x = 0; y = parseFloat(splits[1]); if( !y )y = 0; z = parseFloat(splits[2]); ball.x -= y*5; ball.y -= x*5; if( ball.x <= 0)ball.x = 0; if( ball.x >= width)ball.x = width; if( ball.y <= 0)ball.y = 0; if( ball.y >= height)ball.y = height; serial_values = []; } } } ===== おまけ(マイク入力で丸の大きさも変化させる) ===== //Gravity Acceleration #include "LIS3DHTR.h" #include LIS3DHTR LIS; //Hardware I2C #define WIRE Wire void setup() { Serial.begin(9600); while (!Serial) {}; LIS.begin(WIRE, 0x19); //IIC init delay(100); LIS.setOutputDataRate(LIS3DHTR_DATARATE_50HZ); } void loop() { if (!LIS) { Serial.println("LIS3DHTR didn't connect."); while (1); return; } //3 axis Serial.print(LIS.getAccelerationX()); Serial.print(","); Serial.print(LIS.getAccelerationY()); Serial.print(","); Serial.print(LIS.getAccelerationZ()); Serial.print(","); Serial.println(analogRead(2)); delay(30); } var serial_values = []; var serial = new Serial(); var x=0; var y=0; var z=0; var mic = 0; function setup() { createCanvas(400, 400); } var ball = { x:0, y:0, r:0 } function draw() { background(220); textSize(18); textAlign(CENTER, CENTER); text(`${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)}`, width/2, height/2); circle(ball.x, ball.y, ball.r/10); }   function gotSerialValues(values) { for( let i = 0; i < values.length; i++ ){ serial_values.push(values[i]); if( values[i] == 10){ let result = ""; for( s of serial_values){ result += String.fromCharCode(s); } const splits = result.split(','); x = parseFloat(splits[0]); y = parseFloat(splits[1]); z = parseFloat(splits[2]); mic = parseFloat(splits[3]); serial_values = []; ball.x -= y*10; ball.y -= x*10; ball.r = mic; } } }