13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

toio™(ロボットトイ | toio(トイオ))Advent Calendar 2019

Day 8

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

Last updated at Posted at 2019-12-07

これは「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文での力技しか方法が思いつきませんでした。


const keypress = require('keypress')
const { NearestScanner } = require('@toio/scanner')

const TONE = 64
const TONE_SILENT = 127
const DURATION_SHORT = 200
const DURATION_LONG = DURATION_SHORT * 3

var morse_short = [
  { durationMs: DURATION_SHORT, noteName: TONE }, 
  { durationMs: DURATION_SHORT, noteName: TONE_SILENT }, 
]
var morse_long = [
  { durationMs: DURATION_LONG, noteName: TONE }, 
  { durationMs: DURATION_SHORT, noteName: TONE_SILENT }, 
]

var morse

async function main() {
  // start a scanner to find nearest cube
  const cube = await new NearestScanner().start()

  // connect to the cube
  await cube.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)
        break
      case 'b':
        morse = morse_long.concat(morse_short).concat(morse_short).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'c':
        morse = morse_long.concat(morse_short).concat(morse_long).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'd':
        morse = morse_long.concat(morse_short).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'e':
        morse = morse_short
        cube.playSound(morse ,1)
        break
      case 'f':
        morse = morse_short.concat(morse_short).concat(morse_long).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'g':
        morse = morse_long.concat(morse_long).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'h':
        morse = morse_short.concat(morse_short).concat(morse_short).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'i':
        morse = morse_short.concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'j':
        morse = morse_short.concat(morse_long).concat(morse_long).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'k':
        morse = morse_long.concat(morse_short).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'l':
        morse = morse_short.concat(morse_long).concat(morse_short).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'm':
        morse = morse_long.concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'n':
        morse = morse_long.concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'o':
        morse = morse_long.concat(morse_long).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'p':
        morse = morse_short.concat(morse_long).concat(morse_long).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 'q':
        morse = morse_long.concat(morse_long).concat(morse_short).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'r':
        morse = morse_short.concat(morse_long).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 's':
        morse = morse_short.concat(morse_short).concat(morse_short)
        cube.playSound(morse ,1)
        break
      case 't':
        morse = morse_long
        cube.playSound(morse ,1)
        break
      case 'u':
        morse = morse_short.concat(morse_short).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'v':
        morse = morse_short.concat(morse_short).concat(morse_short).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'w':
        morse = morse_short.concat(morse_long).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'x':
        morse = morse_long.concat(morse_short).concat(morse_short).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case 'y':
        morse = morse_long.concat(morse_short).concat(morse_long).concat(morse_long)
        cube.playSound(morse ,1)
        break
      case '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の接続ができました。


const keypress = require('keypress')
const { NearScanner } = require('@toio/scanner')

var midi_note = new Array()
var data_norm = new Array()
data_norm[0] = {x:0,y:0}

const DURATION = 3000

var MIDI_SCALE_C = [0,0,2,4,5,7,9,11,12,12,12]

const X_INI_TOICOLE = 555.5
const X_END_TOICOLE = 946.95
const Y_INI_TOICOLE = 53
const Y_END_TOICOLE = 44.95
const UNIT_TOICOLE = 43.2

var cube_number = 2

function cube_control(cube){
  var lastData = {x:0, y:0}
  var flag = 0

  cube.on('id:position-id', data1 => {
      var tmp = {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 = 0
      if (tmp.y != lastData.y) flag = 0

      midi_note = MIDI_SCALE_C[tmp.x] + (tmp.y -1)* 12

      if (flag==0){
        cube.playSound([{durationMs: DURATION, noteName: midi_note}] ,1)
        flag = 1
      }

      lastData = tmp
      console.log('[X_STEP]', tmp.x)
      console.log('[Y_STEP]', tmp.y)
      console.log('MIDI',midi_note)
    }
  )
  cube.on('id:position-id-missed', () => {
      flag = 0
      cube.stopSound()
      console.log('[POS ID MISSED]')
    }
  )
}

async function cube_connect(cube_number){
  // start a scanner to find the nearest cube
  const cubes = await new NearScanner(cube_number).start()

  // connect to the cube
  for(var i = 0; i < cube_number; i++) {await cubes[i].connect()}
  return cubes
}

async function main() {

  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 process
    if(key){
      if ((key && key.ctrl && key.name === 'c') || (key && key.name === 'q')) {
        process.exit()
      }
    }else{
      console.log('[Ch]',ch)
      cube_number = ch
      const cubes = await cube_connect(ch)
      for(var i = 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 のやたら長い構造体はこのファンファーレの音データです。

const keypress = require('keypress')
const { NearScanner } = require('@toio/scanner')

var midi_note = new Array()
var data_norm = new Array()
var ledData = new Array()
var target = new Array()
var diff = new Array()

data_norm[0] = {x:0,y:0}
const DURATION = 0

ledData = {durationMs:DURATION, red:255, green:255, blue:255}

const X_INI_TOICOLE = 555.5
const Y_INI_TOICOLE = 53
const UNIT_TOICOLE = 43.2
const X_BEGIN_TOICOLE = 45
const X_END_TOICOLE = 455
const Y_BEGIN_TOICOLE = 45
const Y_END_TOICOLE = 455
const ANGLE_FULLSCALE = 360

var cube_number = 2

target = {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}
 
var melody_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 }, 
];

var melody_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 }, 
];

var flag_gloval = 0
var winnerCubeId =0

function cube_control(cube){
  var lastData = {x:0, y:0, angle:0}
  var lastData2 = {x:0, y:0, angle:0}
  var flag = 0
  var flag_2 = 0
  
  cube.on('id:position-id', data1 => {
      var tmp = {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 calc
      diff.angle = Math.abs(target.angle - data1.angle)
      if(diff.angle > 180) diff.angle = 360 - diff.angle 

      //xy calc
      diff.x = Math.abs(target.x - data1.x)
      diff.y = Math.abs(target.y - data1.y)

      //Thinning
      if (Math.abs(data1.x - lastData2.x) > 3) flag_2 = 0
      if (Math.abs(data1.y - lastData2.y) > 3) flag_2 = 0
      if (Math.abs(data1.angle - lastData2.angle) > 3) flag_2 = 0

      if (flag_gloval==1 && flag ==0){
        if(cube.id == winnerCubeId) cube.playSound(melody_win,1)
        else cube.playSound(melody_lose,1)
        console.log('[WIN!]')
        flag = 1
      }
      
      if (flag_2==0){
        ledData.red = Math.floor(diff.angle / 360 *20)*25
        ledData.green = Math.floor(diff.x / 410 *20)*25
        ledData.blue = Math.floor(diff.y / 410 *20)*25

        //winner judge
        if ((ledData.red + ledData.green + ledData.blue) == 0) {
          winnerCubeId = cube.id
          flag_gloval = 1
        }
        cube.turnOnLight(ledData)
        flag_2 = 1

        //position store
        lastData2 = 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 = 0
    flag_2 = 0
    flag_gloval = 0
    cube.stopSound()
    //cube.turnOffLight()
      console.log('[POS ID MISSED]')
    }
  )
}

async function cube_connect(cube_number){
  // start a scanner to find the nearest cube
  const cubes = await new NearScanner(cube_number).start()

  // connect to the cube
  for(var i = 0; i < cube_number; i++) {await cubes[i].connect()}
  return cubes
}

async function main() {

  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 process
    if(key){
      if ((key && key.ctrl && key.name === 'c') || (key && key.name === 'q')) {
        process.exit()
      }
    }else{
      console.log('[Ch]',ch)
      cube_number = ch
      //connect cube
      const cubes = await cube_connect(ch)
      //control cube
      for(var i = 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')

var midi_note = new Array()
var data_norm = new Array()
data_norm[0] = {x:0,y:0}

const DURATION = 3000

var MIDI_SCALE_C = [0,0,2,4,5,7,9,11,12,12,12]
var scaleList = ["C","C","D","E","F","G","A","B","C","D","D","D"]
var codeList = ["M","m","7","sus4","M7","m7-5","aug","add9","6"]

const X_INI_TOICOLE = 555.5
const X_END_TOICOLE = 946.95
const Y_INI_TOICOLE = 53
const Y_END_TOICOLE = 44.95
const UNIT_TOICOLE = 43.2

var cube_number = 4

var scale = 0
var type = 0

var midi_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
]

function codeController(cubes){
var lastData = {x:0, y:0}
var flag = 0

  cubes[0].on('id:position-id', data1 => {
      var tmp = {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 = 0
      if (tmp.y != lastData.y) flag = 0

      if (flag==0){
        scale = tmp.y 
        type = tmp.x - 1
        cubes[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 = 1
        console.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 = 0
      cubes[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]'))
}

function init(cubes){
  cubes[0].turnOnLight({durationMs:DURATION, red:100, green:100, blue:100})
}
  
async function main() {

  console.log('4cubes')
  console.log('USE Rhythm and Go Mat')

  // start a scanner to find the nearest cube
  const cubes = await new NearScanner(cube_number).start()

  // connect to the cube
  for(var i = 0; i < cube_number; i++) {await cubes[i].connect()}

  init(cubes)
  codeController(cubes)

}

main()

5.マスゲーム

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

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

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

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

const { NearScanner } = require('@toio/scanner')

var midi_note = new Array()
var data_norm = new Array()
var ledData = new Array()
var target = new Array()
var diff = new Array()
var cubePos = new Array()

const X_INI_TOICOLE = 555.5
const Y_INI_TOICOLE = 53
const UNIT_TOICOLE = 43.2
const X_BEGIN_TOICOLE = 45
const X_END_TOICOLE = 455
const Y_BEGIN_TOICOLE = 45
const Y_END_TOICOLE = 455
const ANGLE_FULLSCALE = 360
const CUBE_WIDTH = 32

target[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}
const DURATION = 0

var cube_number = 4
var flag_gloval = 0

function MoveToTarget(target,mine){
  const diffX = target.x - mine.x
  const diffY = target.y - mine.y
  const distance = Math.sqrt(diffX * diffX + diffY * diffY)

  //calc angle
  var relAngle = (Math.atan2(diffY, diffX) * 180) / Math.PI - mine.angle
  relAngle = relAngle % 360
  if (relAngle < -180) {
    relAngle += 360
  } else if (relAngle > 180) {
    relAngle -= 360
  }

  const ratio = 1 - Math.abs(relAngle) / 90
  let speed = 60 * distance /210

  if (distance < 10) {
    return [0, 0] // stop
  }

  if (relAngle > 0) {
    return [speed, speed * ratio]
  } else {
    return [speed * ratio, speed]
  }
}

function cube_control(cube,cubePosition){
  var lastData = {x:0, y:0, angle:0}
  var lastData2 = {x:0, y:0, angle:0}
  var flag = 0
  var flag_2 = 0
  
  cube.on('id:position-id', data1 => {
    cubePosition.x = data1.x
    cubePosition.y = data1.y
    cubePosition.angle = data1.angle
    }
  )
}

function setTarget(){
  var tmp = target[0]
  target[0] = target[1]
  target[1] = target[2]
  target[2] = target[3]
  target[3] = tmp
}

async function main() {
  console.log('4cubes')
  console.log('USE Craft fighter Mat')

  // start a scanner to find the nearest cube
  const cubes = await new NearScanner(cube_number).start()

  // connect to the cube
  for(var i = 0; i < cube_number; i++) {await cubes[i].connect()}
  for(var i = 0; i < cube_number; i++) {cube_control(cubes[i],cubePos[i])}

  // loop
  setInterval(() => {
    for(var i = 0; i < cube_number; i++) 
    cubes[i].move(...MoveToTarget(target[i],cubePos[i]), 100)
  }, 50)
  setInterval(() => {
    setTarget()
  }, 3000)

}


main()

さいごに

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

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

13
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?