Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8835

ソフトウェア初心者がtoio.jsで作ってみた 5つの作例紹介

$
0
0

これは「toio™(ロボットトイ | toio(トイオ)) Advent Calendar 2019」の8日目の記事になります。

はじめに

はじめまして。ヒラノユウヤです。
普段はハードウェアエンジニア(電気)として暮らしています。
この記事では、ソフトウェア初心者の私がtoio.jsを使って作ってみたtoio作品を紹介したいと思います。

ソフトウェアスキル

  • C言語
    • 学校の授業では真面目に取り組んでいました
    • 社会人になってからも、Arduinoを使いこなすくらいには使っていた感じ

以上。なんとも貧弱で泣けてきます。
なんですが、toio core cubeを使ったプログラミングがどうしてもやりたくて。
toio.jsの環境を友人に手伝って構築してもらったところからスタートしました。
始めてみると、サンプルコードもあるので、苦労はしながらも意外といろんなものができました。

参考にしたもの

1にも2にも、公式情報が命でした。
用意されているtoio.jsの使い方はtoio.jsのページで。
buzzerの音階やtoio IDの情報など、toio自体に対しての情報は技術仕様のページで。

あとはサンプルプログラムの読み解きと、ちょい変でのトライ&エラーを繰り返しました。

作例紹介

早速紹介始めます。
実際に作ってtwitterに上げたのは結構昔なので、記憶を辿りながら文章書いてみます。
ソースコードもまんま貼り付けるので、批判称賛なんでもコメントいただければ嬉しいです。

1.モールス信号発生器

パソコンのキーボード入力の取得と、toio.jsのplaySound()の組み合わせです。

キーボード入力の取得はtoio.jsのサンプルプログラム keyboard-control から拝借しました。

入力されたアルファベットをcase文で場合分けします。
対応するモールス信号の構造体を生成して、それをCubeのブザーから鳴らしています。

モールス信号は法則性がないので、このようなcase文での力技しか方法が思いつきませんでした。

constkeypress=require('keypress')const{NearestScanner}=require('@toio/scanner')constTONE=64constTONE_SILENT=127constDURATION_SHORT=200constDURATION_LONG=DURATION_SHORT*3varmorse_short=[{durationMs:DURATION_SHORT,noteName:TONE},{durationMs:DURATION_SHORT,noteName:TONE_SILENT},]varmorse_long=[{durationMs:DURATION_LONG,noteName:TONE},{durationMs:DURATION_SHORT,noteName:TONE_SILENT},]varmorseasyncfunctionmain(){// start a scanner to find nearest cubeconstcube=awaitnewNearestScanner().start()// connect to the cubeawaitcube.connect()keypress(process.stdin)process.stdin.on('keypress',(ch,key)=>{if((key&&key.ctrl&&key.name==='c')){process.exit()}switch(key.name){case'a':morse=morse_short.concat(morse_long)cube.playSound(morse,1)breakcase'b':morse=morse_long.concat(morse_short).concat(morse_short).concat(morse_short)cube.playSound(morse,1)breakcase'c':morse=morse_long.concat(morse_short).concat(morse_long).concat(morse_short)cube.playSound(morse,1)breakcase'd':morse=morse_long.concat(morse_short).concat(morse_short)cube.playSound(morse,1)breakcase'e':morse=morse_shortcube.playSound(morse,1)breakcase'f':morse=morse_short.concat(morse_short).concat(morse_long).concat(morse_short)cube.playSound(morse,1)breakcase'g':morse=morse_long.concat(morse_long).concat(morse_short)cube.playSound(morse,1)breakcase'h':morse=morse_short.concat(morse_short).concat(morse_short).concat(morse_short)cube.playSound(morse,1)breakcase'i':morse=morse_short.concat(morse_short)cube.playSound(morse,1)breakcase'j':morse=morse_short.concat(morse_long).concat(morse_long).concat(morse_long)cube.playSound(morse,1)breakcase'k':morse=morse_long.concat(morse_short).concat(morse_long)cube.playSound(morse,1)breakcase'l':morse=morse_short.concat(morse_long).concat(morse_short).concat(morse_short)cube.playSound(morse,1)breakcase'm':morse=morse_long.concat(morse_long)cube.playSound(morse,1)breakcase'n':morse=morse_long.concat(morse_short)cube.playSound(morse,1)breakcase'o':morse=morse_long.concat(morse_long).concat(morse_long)cube.playSound(morse,1)breakcase'p':morse=morse_short.concat(morse_long).concat(morse_long).concat(morse_short)cube.playSound(morse,1)breakcase'q':morse=morse_long.concat(morse_long).concat(morse_short).concat(morse_long)cube.playSound(morse,1)breakcase'r':morse=morse_short.concat(morse_long).concat(morse_short)cube.playSound(morse,1)breakcase's':morse=morse_short.concat(morse_short).concat(morse_short)cube.playSound(morse,1)breakcase't':morse=morse_longcube.playSound(morse,1)breakcase'u':morse=morse_short.concat(morse_short).concat(morse_long)cube.playSound(morse,1)breakcase'v':morse=morse_short.concat(morse_short).concat(morse_short).concat(morse_long)cube.playSound(morse,1)breakcase'w':morse=morse_short.concat(morse_long).concat(morse_long)cube.playSound(morse,1)breakcase'x':morse=morse_long.concat(morse_short).concat(morse_short).concat(morse_long)cube.playSound(morse,1)breakcase'y':morse=morse_long.concat(morse_short).concat(morse_long).concat(morse_long)cube.playSound(morse,1)breakcase'z':morse=morse_long.concat(morse_long).concat(morse_short).concat(morse_short)cube.playSound(morse,1)break}})process.stdin.setRawMode(true)process.stdin.resume()}main()

2.電子ピアノ

1.でBuzzerが鳴らせたので、今度は読み取りセンサと合わせたものが作りたいと言うことで、作ったものです。

読み取りセンサでトイオ・コレクションのマット座標を読み取って、対応する音をブザーから鳴らしています。
読み取りセンサの値はそのまま使うのではなく、トイオ・コレクションのマットの格子単位の単位で検出するように丸めています。
ここの丸めかた、実物合わせで採寸しながらやりました。

Cubeがマットに触れている間だけ音が鳴るように、
Cubeがマットに載った時に動く関数 cube.on('id:position-id' で音を鳴らして
Cubeがマットから離れた時に動く関数 cube.on('id:position-id-missed' で音を消す処理を入れています。

実はここで複数Cube接続できるようにコードを修正しています。
起動時にキーボード入力で入力した数自分のCubeを接続できるようにしています。
私の環境では最大6台までのCubeの接続ができました。

constkeypress=require('keypress')const{NearScanner}=require('@toio/scanner')varmidi_note=newArray()vardata_norm=newArray()data_norm[0]={x:0,y:0}constDURATION=3000varMIDI_SCALE_C=[0,0,2,4,5,7,9,11,12,12,12]constX_INI_TOICOLE=555.5constX_END_TOICOLE=946.95constY_INI_TOICOLE=53constY_END_TOICOLE=44.95constUNIT_TOICOLE=43.2varcube_number=2functioncube_control(cube){varlastData={x:0,y:0}varflag=0cube.on('id:position-id',data1=>{vartmp={x:Math.floor((data1.x-X_INI_TOICOLE)/UNIT_TOICOLE)+1,y:Math.floor((data1.y-Y_INI_TOICOLE)/UNIT_TOICOLE)+1}if(tmp.x!=lastData.x)flag=0if(tmp.y!=lastData.y)flag=0midi_note=MIDI_SCALE_C[tmp.x]+(tmp.y-1)*12if(flag==0){cube.playSound([{durationMs:DURATION,noteName:midi_note}],1)flag=1}lastData=tmpconsole.log('[X_STEP]',tmp.x)console.log('[Y_STEP]',tmp.y)console.log('MIDI',midi_note)})cube.on('id:position-id-missed',()=>{flag=0cube.stopSound()console.log('[POS ID MISSED]')})}asyncfunctioncube_connect(cube_number){// start a scanner to find the nearest cubeconstcubes=awaitnewNearScanner(cube_number).start()// connect to the cubefor(vari=0;i<cube_number;i++){awaitcubes[i].connect()}returncubes}asyncfunctionmain(){console.log('USE Rhythm and Go Mat')console.log('Press connect cube number')keypress(process.stdin)process.stdin.on('keypress',async(ch,key)=>{// ctrl+c or q -> exit processif(key){if((key&&key.ctrl&&key.name==='c')||(key&&key.name==='q')){process.exit()}}else{console.log('[Ch]',ch)cube_number=chconstcubes=awaitcube_connect(ch)for(vari=0;i<cube_number;i++){cube_control(cubes[i])}}})process.stdin.setRawMode(true)process.stdin.resume()}main()

3.宝探しゲーム

今度はLEDの点灯と組み合わせを試してみた作品です。
ランダムに生成されるゴール位置をLEDの色を見ながら手探りで探し当てるといったゲームを作りました。

マット上にCubeを置くと、座標(X,Y)と姿勢(Θ)が取得できます。
ゴールの場所(X,Y,Θ)から遠ざかるほどLED色が強くなり、Target場所に一致すると消える という仕様。
つまり、LEDの光が消える場所をさがす というゲームです。

X方向は赤、Y方向は緑、Θ方向は青
といったように各軸で別の色のLEDが反応するので、色味を見ながらどっちの方向に動かすかを考えます。

ゴールの位置にみごCubeを持っていくことができたら勝利判定し、勝利のファンファーレを鳴らすようにしています。
melody_win, melody_lose のやたら長い構造体はこのファンファーレの音データです。

constkeypress=require('keypress')const{NearScanner}=require('@toio/scanner')varmidi_note=newArray()vardata_norm=newArray()varledData=newArray()vartarget=newArray()vardiff=newArray()data_norm[0]={x:0,y:0}constDURATION=0ledData={durationMs:DURATION,red:255,green:255,blue:255}constX_INI_TOICOLE=555.5constY_INI_TOICOLE=53constUNIT_TOICOLE=43.2constX_BEGIN_TOICOLE=45constX_END_TOICOLE=455constY_BEGIN_TOICOLE=45constY_END_TOICOLE=455constANGLE_FULLSCALE=360varcube_number=2target={x:Math.round(Math.random()*(X_END_TOICOLE-X_BEGIN_TOICOLE))+X_BEGIN_TOICOLE,y:Math.round(Math.random()*(X_END_TOICOLE-X_BEGIN_TOICOLE))+X_BEGIN_TOICOLE,angle:Math.round(Math.random()*ANGLE_FULLSCALE)}diff={x:0,y:0,angle:0}varmelody_win=[{durationMs:400,noteName:127},{durationMs:400,noteName:60},{durationMs:100,noteName:72},{durationMs:100,noteName:127},{durationMs:100,noteName:67},{durationMs:100,noteName:127},{durationMs:100,noteName:72},{durationMs:100,noteName:127},{durationMs:600,noteName:75},{durationMs:100,noteName:77},{durationMs:100,noteName:127},{durationMs:100,noteName:77},{durationMs:100,noteName:127},{durationMs:100,noteName:77},{durationMs:100,noteName:127},{durationMs:1600,noteName:79},];varmelody_lose=[{durationMs:5000,noteName:127},{durationMs:3000,noteName:127},{durationMs:150,noteName:71},{durationMs:150,noteName:77},{durationMs:150,noteName:127},{durationMs:150,noteName:77},{durationMs:200,noteName:77},{durationMs:200,noteName:76},{durationMs:200,noteName:74},{durationMs:200,noteName:72},];varflag_gloval=0varwinnerCubeId=0functioncube_control(cube){varlastData={x:0,y:0,angle:0}varlastData2={x:0,y:0,angle:0}varflag=0varflag_2=0cube.on('id:position-id',data1=>{vartmp={x:Math.floor((data1.x-X_INI_TOICOLE)/UNIT_TOICOLE)+1,y:Math.floor((data1.y-Y_INI_TOICOLE)/UNIT_TOICOLE)+1,angle:data1.angle}//angle calcdiff.angle=Math.abs(target.angle-data1.angle)if(diff.angle>180)diff.angle=360-diff.angle//xy calcdiff.x=Math.abs(target.x-data1.x)diff.y=Math.abs(target.y-data1.y)//Thinningif(Math.abs(data1.x-lastData2.x)>3)flag_2=0if(Math.abs(data1.y-lastData2.y)>3)flag_2=0if(Math.abs(data1.angle-lastData2.angle)>3)flag_2=0if(flag_gloval==1&&flag==0){if(cube.id==winnerCubeId)cube.playSound(melody_win,1)elsecube.playSound(melody_lose,1)console.log('[WIN!]')flag=1}if(flag_2==0){ledData.red=Math.floor(diff.angle/360*20)*25ledData.green=Math.floor(diff.x/410*20)*25ledData.blue=Math.floor(diff.y/410*20)*25//winner judgeif((ledData.red+ledData.green+ledData.blue)==0){winnerCubeId=cube.idflag_gloval=1}cube.turnOnLight(ledData)flag_2=1//position storelastData2=data1}console.log('[Winner,cubeID]',winnerCubeId,cube.id)console.log(target)console.log(ledData)console.log(diff)console.log(data1)console.log(lastData2)})cube.on('id:position-id-missed',()=>{flag=0flag_2=0flag_gloval=0cube.stopSound()//cube.turnOffLight()console.log('[POS ID MISSED]')})}asyncfunctioncube_connect(cube_number){// start a scanner to find the nearest cubeconstcubes=awaitnewNearScanner(cube_number).start()// connect to the cubefor(vari=0;i<cube_number;i++){awaitcubes[i].connect()}returncubes}asyncfunctionmain(){console.log('USE Craft fighter Mat')console.log('Press connect cube number')keypress(process.stdin)process.stdin.on('keypress',async(ch,key)=>{// ctrl+c or q -> exit processif(key){if((key&&key.ctrl&&key.name==='c')||(key&&key.name==='q')){process.exit()}}else{console.log('[Ch]',ch)cube_number=ch//connect cubeconstcubes=awaitcube_connect(ch)//control cubefor(vari=0;i<cube_number;i++){cube_control(cubes[i])}}})process.stdin.setRawMode(true)process.stdin.resume()}main()

4.和音プレイヤー

複数Cubeの連携制御に挑戦したく、作った作品です。
1つのCubeがマットに触れると、格子ごとに他の3つのCubeが異なるコードを演奏します。

和音なので、3台のCubeでタイミングを合わせた音再生をするのをどうしたらいいか? といろいろ考えましたが、
今回は
Cube1のマットON判定の関数の中でCube2/3/4のBuzzer音再生を行う
ことでこれを実現できました。

const{NearScanner}=require('@toio/scanner')varmidi_note=newArray()vardata_norm=newArray()data_norm[0]={x:0,y:0}constDURATION=3000varMIDI_SCALE_C=[0,0,2,4,5,7,9,11,12,12,12]varscaleList=["C","C","D","E","F","G","A","B","C","D","D","D"]varcodeList=["M","m","7","sus4","M7","m7-5","aug","add9","6"]constX_INI_TOICOLE=555.5constX_END_TOICOLE=946.95constY_INI_TOICOLE=53constY_END_TOICOLE=44.95constUNIT_TOICOLE=43.2varcube_number=4varscale=0vartype=0varmidi_note=[{uno:60,dos:64,tre:67},//C major{uno:60,dos:63,tre:67},//m{uno:58,dos:64,tre:67},//7{uno:60,dos:65,tre:67},//sus4{uno:59,dos:64,tre:67},//M7{uno:60,dos:63,tre:66},//m7-5{uno:60,dos:64,tre:68},//aug{uno:60,dos:62,tre:67},//add9{uno:60,dos:64,tre:69},//6]functioncodeController(cubes){varlastData={x:0,y:0}varflag=0cubes[0].on('id:position-id',data1=>{vartmp={x:Math.floor((data1.x-X_INI_TOICOLE)/UNIT_TOICOLE)+1,y:Math.floor((data1.y-Y_INI_TOICOLE)/UNIT_TOICOLE)+1}if(tmp.x!=lastData.x)flag=0if(tmp.y!=lastData.y)flag=0if(flag==0){scale=tmp.ytype=tmp.x-1cubes[1].playSound([{durationMs:DURATION,noteName:midi_note[type].uno+MIDI_SCALE_C[scale]}],1)cubes[2].playSound([{durationMs:DURATION,noteName:midi_note[type].dos+MIDI_SCALE_C[scale]}],1)cubes[3].playSound([{durationMs:DURATION,noteName:midi_note[type].tre+MIDI_SCALE_C[scale]}],1)cubes[1].turnOnLight({durationMs:DURATION,red:0,green:255,blue:255})cubes[2].turnOnLight({durationMs:DURATION,red:255,green:0,blue:255})cubes[3].turnOnLight({durationMs:DURATION,red:255,green:255,blue:0})flag=1console.log('[CODE]',scaleList[scale],codeList[type])}lastData=tmp})cubes[0].on('id:standard-id',data2=>console.log('[STD ID]',data2))cubes[0].on('id:position-id-missed',()=>{flag=0cubes[1].stopSound()cubes[2].stopSound()cubes[3].stopSound()cubes[1].turnOffLight()cubes[2].turnOffLight()cubes[3].turnOffLight()})cubes[0].on('id:standard-id-missed',()=>console.log('[STD ID MISSED]'))}functioninit(cubes){cubes[0].turnOnLight({durationMs:DURATION,red:100,green:100,blue:100})}asyncfunctionmain(){console.log('4cubes')console.log('USE Rhythm and Go Mat')// start a scanner to find the nearest cubeconstcubes=awaitnewNearScanner(cube_number).start()// connect to the cubefor(vari=0;i<cube_number;i++){awaitcubes[i].connect()}init(cubes)codeController(cubes)}main()

5.マスゲーム

Cubeはやはり動かなきゃ!ということで、モーター制御が使いたくて作った作品です。
モーターを動かすところはtoio.jsのサンプルプログラム chase を参考にしています。

動きとしては極めて単純で、一定時間ごとに異なる目的地へCubeを制御しているだけ。
ただ、この「一定時間ことに」が曲者でした。
toio.jsはイベントドリブンなサンプルコードになっているので、「一定時間ごとに」実行するためのコードの書き方がわかりませんでした。

ここは友人に頼りまして、最強の武器
setinterval()
を教えてもらいました。これを使うことで「一定時間ごと」の処理が記述できました。

単純な動きでも、4つ組み合わさると、面白味が生まれますね。

const{NearScanner}=require('@toio/scanner')varmidi_note=newArray()vardata_norm=newArray()varledData=newArray()vartarget=newArray()vardiff=newArray()varcubePos=newArray()constX_INI_TOICOLE=555.5constY_INI_TOICOLE=53constUNIT_TOICOLE=43.2constX_BEGIN_TOICOLE=45constX_END_TOICOLE=455constY_BEGIN_TOICOLE=45constY_END_TOICOLE=455constANGLE_FULLSCALE=360constCUBE_WIDTH=32target[0]={x:145,y:145,angle:90}target[1]={x:355,y:145,angle:0}target[2]={x:355,y:355,angle:270}target[3]={x:145,y:355,angle:180}cubePos[0]={x:0,y:0,angle:0}cubePos[1]={x:0,y:0,angle:0}cubePos[2]={x:0,y:0,angle:0}cubePos[3]={x:0,y:0,angle:0}data_norm[0]={x:0,y:0}constDURATION=0varcube_number=4/* Cube sound converted from MIDI file */varflag_gloval=0varwinnerCubeId=0functionMoveToTarget(target,mine){constdiffX=target.x-mine.xconstdiffY=target.y-mine.yconstdistance=Math.sqrt(diffX*diffX+diffY*diffY)//  console.log(diffX,diffY)//calc anglevarrelAngle=(Math.atan2(diffY,diffX)*180)/Math.PI-mine.anglerelAngle=relAngle%360if(relAngle<-180){relAngle+=360}elseif(relAngle>180){relAngle-=360}constratio=1-Math.abs(relAngle)/90letspeed=60*distance/210if(distance<10){return[0,0]// stop}if(relAngle>0){return[speed,speed*ratio]}else{return[speed*ratio,speed]}}functioncube_control(cube,cubePosition){varlastData={x:0,y:0,angle:0}varlastData2={x:0,y:0,angle:0}varflag=0varflag_2=0cube.on('id:position-id',data1=>{cubePosition.x=data1.xcubePosition.y=data1.ycubePosition.angle=data1.angle})}functionsetTarget(){vartmp=target[0]target[0]=target[1]target[1]=target[2]target[2]=target[3]target[3]=tmp}asyncfunctionmain(){console.log('4cubes')console.log('USE Craft fighter Mat')// start a scanner to find the nearest cubeconstcubes=awaitnewNearScanner(cube_number).start()// connect to the cubefor(vari=0;i<cube_number;i++){awaitcubes[i].connect()}for(vari=0;i<cube_number;i++){cube_control(cubes[i],cubePos[i])}// loopsetInterval(()=>{for(vari=0;i<cube_number;i++)cubes[i].move(...MoveToTarget(target[i],cubePos[i]),100)},50)setInterval(()=>{setTarget()},3000)}main()

さいごに

友人達のサポートも多々ありましたが、初心者でもやればできるものですね。
javascriptはC言語と違って、イベントドリブンでの処理を書くのがとても簡単に出来ているように感じました。
C言語だと、割り込み処理で書かなきゃいけないところが、関数宣言しとけば勝手に実行される みたいな。
処理を『置いておく』感覚で簡単にプログラミングできるのが良かったです。

またいろいろと面白い動きを作っていきたいと思います。


Viewing all articles
Browse latest Browse all 8835

Trending Articles