Help us understand the problem. What is going on with this article?

Obnizと #toio をBLEで連携する際の備忘録 #おうちでロボット開発

やったこと

  • ObnizとtoioをBLEを介して繋げた
  • ObnizへtoioのPosition IDを読み込んだ
  • Obnizからtoioをラジコンみたいに前後左右に動かした
  • 開発者向けマット(仮)のサンプル”の絶対座標に向けてtoioを動かした

20200503_012056.jpg

今回はtoioが一台だけ電源ONになっていると想定して述べる。
(二台以上を動かす際には、後述する“Obnizとtoioをつなげる”にてUUIDを指定するようにすること。)

想定読者

  • BLEとかよくわからないけどもとりあえずObnizとtoioを連携したい方
  • ごま塩程度にjavaScriptなどのプログラミング言語を触ったことがある方
  • toioもObnizもガチ勢じゃないひと(!?)

使ったもの

  • Obniz(Basic Licenseを使用した。ただ、GPIOを使っていないのでM5StickC版Obnizや1Yとかでも問題ないはず)
  • toio(https://www.switch-science.com/catalog/6302/)
  • 開発者向けマット(仮)のサンプル(“「toio」ではじめよう、おうちでロボット開発キャンペーン”の第一弾プレゼント)

手順

基本的には、“toioコアキューブ技術仕様”の“通信概要”Obniz DocsのBLEのページを見ながらUUIDを埋めながら動かしていく。
ここでは
1. Obnizとtoioをつなげる(共通)
2. toioからObnizへ情報を読み込む(Position IDの読み込み)
3. Obnizからtoioへデータを書き込む(前後左右へ動かす、絶対座標に向かわせる)

と、三つに分けて説明する。

1. Obnizとtoioをつなげる

obniz.ble.scan.onfindで該当デバイスを探してUUIDから接続をかけるのが妥当だが、
toioが一台だけしか起動していないなら、このように横着してObnizとBLE接続することができる。
(“toioコアキューブ技術仕様”の“通信概要”のComplete Local Nameにも記載あり。)

接続例
var target = {
    localName: "toio Core Cube"
};
var peripheral = await obniz.ble.scan.startOneWait(target);
try {
    await peripheral.connectWait();
}catch(e) {
...
}

このperipheralから後述の操作を実施していく。

2. toioからObnizへ情報を読み込む

まず、“toioコアキューブ技術仕様”の“通信概要”から“Service UUID(Complete list of 128bit Service UUIDs)”をメモする。次に、操作したい内容のCharacteristicsをメモするのだが、ここではPosition IDが知りたいため、ID Information / 読み取りセンサーの“Characteristic UUID”をメモする。

2020/5/2(土)では、Service UUIDとCharacteristic UUIDの対応はこの通り。
このUUIDにカテゴライズされた様々な操作を、後述の命令を使って実行していく。

UUID
var serviceUUID = "10B20100-5B3B-4571-9508-CF3EFCD7BBAE";
var characteristicUUID = "10B20101-5B3B-4571-9508-CF3EFCD7BBAE";

得られたUUIDはgetServiceならびにgetCharacteristicに投げる。getServiceやgetCharacteristicでは当然それぞれのオブジェクトを取得することができるが、その際には、readWaitの呼び出しはawaitで非同期処理の応答待ちさせる所に注意する。

読み込み例
var readData = await peripheral.getService(serviceUUID).getCharacteristic(characteristicUUID).readWait();

このreadDataには12バイト分のデータが長さ12の配列として戻ってくる。このとき、必要なデータを仕様の読み取り操作に基づいて抽出するが、16byteのデータが送られる際には下1バイトが先に配列に格納されることに注意する。下記に例を示す。

読み込み例で値を取得した場合
//"読み取り操作"におけるキューブの中心の X 座標値 0x02c5(709)
readData[1] -> 0xc5
readData[2] -> 0x02
//"読み取り操作"におけるキューブの中心の Y 座標値 0x017f(383)
readData[3] -> 0x7f
readData[4] -> 0x01

このことから、プログラム上でX座標の値、Y座標の値として扱う際には、二つの値をシフト演算子で連結する。二進数が嫌いなら、上二文字が“<<”の左で、下二文字が“|”の右に与えられれば勝手に結合してくれるおまじないと考えておこう。

読み込み例で値を取得した場合の結合方法
var xPos = readData[2]<<8 | readData[1];
var yPos = readData[4]<<8 | readData[3];

このxPos,yPosがtoioの位置に該当する。

3. Obnizからtoioへデータを書き込む

Service UUIDとCharacteristic UUIDで操作していく点に関しては、"2. toioからObnizへ情報を読み込む"と同じ。
2020/5/2(土)現在の対応は以下の通り。

UUID
var serviceUUID = "10B20100-5B3B-4571-9508-CF3EFCD7BBAE";
var characteristicUUID = "10B20102-5B3B-4571-9508-CF3EFCD7BBAE";

次に、toioを動かすための情報を与える。いわゆるラジコンみたいに自由にtoioを操作する際には、“モータ”のページのモーター制御に記載されているフォーマットに基づいてデータを渡す。
例えば、左ホイールが前に100,右ホイールが後ろに20の出力で動いてほしい場合のデータをjavaScriptではこのように用意できる。

UUID
var writeData = [1,1,1,100,2,2,20];
//16進数でもOK
var writeData = [0x01,0x01,0x01,0x64,0x02,0x02,0x14];

実際にtoioに命令をかける際には、writeWaitを呼び出す。

getWriteの呼び出し
await peripheral.getService(serviceUUID).getCharacteristic(characteristicUUID).writeWait(writeData);

ちなみに、HTML側でボタンと連動させる際にはこのような形になる。このサンプルはボタンを押しているときに動いて、ボタンを離すと動きを止める。

ボタン操作サンプル
//ボタンを押した際の挙動
await $("#Button").on("touchstart mousedown",async function(){
    await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait(writeData);
});

//ボタンを離した際の挙動
await $("#Button").on("touchstart mouseup",async function(){
    await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,1,0,2,1,0]);
});

続いて、“開発者向けマット(仮)のサンプル”の絶対座標を用いてtoioを動かす際には目標指定機能付きモータ制御のデータフォーマットを参照する。なお、本機能はアップデートにより追加・修正されたものなため、先にtoioのファームウェアが最新であることを確認しておくこと。

ここでもuint16のデータがあるが、readDataと同様に下1バイトから配列に入れる。目標指定機能付きモータ制御の例で示すとこのような配列になる。

目標指定機能付きモータ制御の例
//ポイントは0から数えて7,8番目と9,10番目と11,12番目。必ず下位1バイトから書く。
writeData = [0x03,0x00,0x05,0x00,0x50,0x00,0x00,0xbc,0x02,0x82,0x01,0x5a,0x00];

書き込みは先ほどのラジコン操作のときと同様である。もっというとService UUIDもCharacteristic UUIDも同じである。

getWriteの呼び出し
writeData = [0x03,0x00,0x05,0x00,0x50,0x00,0x00,0xbc,0x02,0x82,0x01,0x5a,0x00];
await peripheral.getService(serviceUUID).getCharacteristic(characteristicUUID).writeWait(writeData);

動かしているときの写真を雰囲気づくり程度に掲載。

20200503_004007.jpg

サンプルコード

2020/5/2(土)にてObniz Developer Consoleで確認済み。
Obniz Developer ConsoleにてRepositoryを新規作成し、コードをコピペすれば動くはず。

toioBLE.html
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="/css/starter-sample.css" />
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script
      src="https://unpkg.com/obniz@3.5.0/obniz.js"
      crossorigin="anonymous"
    ></script>
  </head>
  <body>

    <button id="up">up</button>
    <button id="down">down</button>
    <button id="R">R</button>
    <button id="L">L</button>

    <button id="move">move</button>

    <br/>
    <div id="status">Wait...</div>
    <br/>
     Pos X:<span id="posx">NaN</span> Pos Y:<span id="posy">NaN</span>

    <script>
      var serviceID = "10B20100-5B3B-4571-9508-CF3EFCD7BBAE";
      var characteristicIDMotor = "10B20102-5B3B-4571-9508-CF3EFCD7BBAE";
      var characteristicIDPos = "10B20101-5B3B-4571-9508-CF3EFCD7BBAE";

      var obniz = new Obniz("XXXX-XXXX");
      obniz.onconnect = async function () {
        await obniz.ble.initWait();

        var target = {
          localName: "toio Core Cube"
        };

        obniz.ble.scan.onfind = function(peripheral){
          console.log(peripheral.localName)
        };

        obniz.ble.scan.onfinish = async function(peripherals, error){
          console.log("scan timeout!")
        };

        var peripheral = await obniz.ble.scan.startOneWait(target);
        var readData;

        if(peripheral) {
          console.log("found");
          document.getElementById("status").innerText = "Toio is ready!";

          try {
            await peripheral.connectWait();
            console.log("connected");

            await obniz.wait(100);

            // UP
            await $("#up").on("touchstart mousedown",async function(){
              console.log("push");
              obniz.display.clear();
              obniz.display.print("UP!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,1,20,2,1,20]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);

            });
            await $("#up").on("touchstart mouseup",async function(){
              console.log("release");
              obniz.display.clear();
              obniz.display.print("STOP!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,1,0,2,1,0]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });

            // DOWN
            await $("#down").on("touchstart mousedown",async function(){
              console.log("push");
              obniz.display.clear();
              obniz.display.print("DOWN!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,2,20,2,2,20]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });
            await $("#down").on("touchstart mouseup",async function(){
              console.log("release");
              obniz.display.clear();
              obniz.display.print("STOP!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,2,0,2,2,0]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });

            // Left
            await $("#L").on("touchstart mousedown",async function(){
              console.log("push");
              obniz.display.clear();
              obniz.display.print("LEFT TURN!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,2,20,2,1,20]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });
            await $("#L").on("touchstart mouseup",async function(){
              console.log("release");
              obniz.display.clear();
              obniz.display.print("STOP TURN!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,2,0,2,1,0]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });

            // Right
            await $("#R").on("touchstart mousedown",async function(){
              console.log("push");
              obniz.display.clear();
              obniz.display.print("RIGHT TURN!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,1,20,2,2,20]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });
            await $("#R").on("touchstart mouseup",async function(){
              console.log("release");
              obniz.display.clear();
              obniz.display.print("STOP TURN!");

              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([1,1,1,0,2,2,0]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });

            await $("#move").on("touchstart mousedown",async function(){
              console.log("push");
              obniz.display.clear();
              obniz.display.print("MOVE");

              // uint16は下位1バイトから先に入れる。
              await peripheral.getService(serviceID).getCharacteristic(characteristicIDMotor).writeWait([0x03,0x00,0x05,0x00,0x50,0x00,0x00,0x5e,0x01,0x2c,0x01,0x5a,0x00]);
              readData = await peripheral.getService(serviceID).getCharacteristic(characteristicIDPos).readWait();
              console.log(readData);
              document.getElementById("posx").innerText = String(readData[2]<<8|readData[1]);
              document.getElementById("posy").innerText = String(readData[4]<<8|readData[3]);
            });

          } catch(e) {
            console.error(e);
            document.getElementById("status").innerText = "Toio is disconnected.";
          }
        }        
      }
    </script>
  </body>
</html>

画面は適当である。

sukusho.png

解決していないこと

  • 動いているか否かに関わらず、BLEが一分ぐらいで切れている。再接続の処理が必要な感じ?
  • Notifyがまだ動いていないので、どこかのタイミングで追記したい。

まとめ

  • ObnizとtoioをBLEを介して繋げ、Obnizからtoioに対して操作を実施した。
    • Obnizからtoioの状態を読みこんだ。
    • Obnizからtoioをラジコンみたいにして動かした。
    • “開発者向けマット(仮)のサンプル”の絶対座標に向けてtoioを動かした。
iotlt
IoT縛りの勉強会です。 毎月イベントを実施しているので是非遊びに来てください! 登壇者を中心にQiitaでも情報発信していきます。 https://iotlt.connpass.com
https://iotlt.connpass.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした