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

NoodlとMicro:bitの連携で地球をグルグル回してみた

はじめに

この記事はNoodlもくもく会|GWアドベントカレンダーの7日目の記事となります。
4日目の記事でCanvas上にThree.jsで3Dオブジェクトを表示しましたので、今回の記事ではMicro:bitにWebBluetooth接続して、取得した加速度情報でCanvas上の3Dオブジェクトを操作してみます。

目次

  1. サンプルデモ
  2. GitHub上のサンプルデータと構築手順
  3. Micro:bitプログラム
  4. Noodlプログラム
  5. まとめ

1. サンプルデモ

  1. micro:bitのサンプルコードをお持ちのMicro:bitにダウンロードしてください。
    成功するとLEDに「□」四角が表示されます。
    ※micro:bitの設定で、「No Pairing Required: Anyone can connect via Bluetooth.」にチェックを入れておいてください。

  2. 次に、Noodlのデモページを開いてください。
    ※WebBluetoothが実装されているブラウザ・バージョンは限定されています。なるべく最新のChromeを使うといいでしょう。ブラウザの実装状況はこちらを確認ください。

  3. Noodlのデモページが表示されたら、「connect」ボタンをクリックしブラウザ上からmicro:bitにペアリング要求をします。ここでブラウザ・micro:bitの設定が正しい場合はダイアログに「BBC micro:bit」と表示されているはずです。micro:bitを選択して「ペア設定」ボタンを押します。

image635.png

ブラウザとmicro:bitとの接続が完了すると、micro:bitの傾きに合わせてXYZ値が変化しているのを確認できます。
image670.png

2. GitHub上のサンプルデータと構築手順

ソースを下記に公開しています。Githubプロジェクトを対象PCにクローンし/Noodl2_MicrobitThreeDemoをNoodlプロジェクトにインポートしてください。

ローカル上での確認

WebBluetoothはブラウザのセキュリティの問題で、保護された通信(https)か、ローカル(localhost)で実行しなければ使用できません。
自身のPC上で確認する場合はURLの「192.168.XXX.XXX」を「localhost」に変更してください。

3. Micro:bitプログラム - Bluetooth加速度サービス -

micro:bitのサンプルコードから説明します。

Micro:bit側のプログラムはBluetoothの加速度、温度計、LED、ボタンサービスを読込んでいるだけです。
正しく起動できたことを確認できるようにLEDで「□」四角を表示し、Bluetoothに接続できると「レ」チェック、切断時に「X」バツを表示します。
Micro:bitの使い方についてはこちらで詳しく説明されています。
image652.png

ここでは加速度データの扱いについて説明します。
加速度センサーは下記図に示す通り、XYZ軸の加速度を-2G~+2Gの範囲で知ることができます。
各軸は
- X軸:-2032~2048 右に傾けるとプラス、左に傾けるとマイナス
- Y軸:-2032~2048 手前に傾けるとプラス、奥に傾けるとマイナス
- Z軸:-2048~2032 上(LED表面)がプラス、下(裏面)がマイナス

例えばmicro:bitのLED面を上に水平にした状態ではX軸・Y軸の値が0、Z軸の値が-1023(1G)となります。※測定誤差があるため数値は常にブレがあります。

image639.png

4. Noodlプログラム

Noodlのノードとノードの接続

各ノードは下記のようにつなぎます
image654.png
左側にJavascriptノードのMicro:bitとの通信部、ThreeJS表示部
右側にCanvas、画像・テキスト、接続/切断ボタン、XYZ軸表示
が並んでいます。

ノードとノードの接続は
1. Micro:bit通信部では「connect/disconect」ボタンのclickを
 JavascritpノードのmySignalとして入力
2. Micro:bitから取得した加速度をNumberRemapperで変換:-1G~1Gの値から-180°~180°にします
3. 変換された角度をThreeJS表示用のJavascriptノードに入力
4. 入力された角度を使って3D空間上のカメラの位置を移動
5. CanvasのDomElementに3Dの地球モデルを描画しています

ノード設定は下記のようになっています
image653.png

ScriptDownloaderではThree.jsをCDNより読込んでいます。
External scripts>script0にhttps://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.jsと設定しています。

Javascriptノード

NoodlではNoodlとmicro:bitをつないで、メッセージのやり取りをするを参考に、加速度データを取得します。
Noodl上では各軸の値を1/1000としています。1000mG⇒1G表示。

Microbit通信部
//micro:bit BLE UUID
var ACCELEROMETERSERVICE_SERVICE_UUID       = 'e95d0753-251d-470a-a062-fa1922dfa9a8';
var ACCELEROMETERDATA_CHARACTERISTIC_UUID   = 'e95dca4b-251d-470a-a062-fa1922dfa9a8';
var accelerometer_device;
var accelerometer_characteristic;
var accX,accY,accZ;

define({
    // The input ports of the Javascript node, name of input and type
    inputs:{
        // ExampleInput:'number',
        // Available types are 'number', 'string', 'boolean', 'color' and 'signal',
        connect:'signal',
        disconnect:'signal',
        mySignal:"signal"
    },

    // The output ports of the Javascript node, name of output and type
    outputs:{
        // ExampleOutput:'string',
        accX: "number",
        accY: "number",
        accZ: "number"
    },

    connect:function(inputs,outputs) {
            navigator.bluetooth.requestDevice({
            filters: [{
              namePrefix: 'BBC micro:bit',
            }],
            optionalServices: [ACCELEROMETERSERVICE_SERVICE_UUID],

          })
          .then(device => {
            accelerometer_device = device;
            console.log("device", device);
            return device.gatt.connect();
          })
          //ACCELEROMETER
          .then(server =>{
            console.log("server", server)
            return server.getPrimaryService(ACCELEROMETERSERVICE_SERVICE_UUID);
          })
          .then(service => {
            console.log("service", service)
            return service.getCharacteristic(ACCELEROMETERDATA_CHARACTERISTIC_UUID)
          })
          .then(chara => {
            console.log("ACCELEROMETER:", chara)
            console.log("BLE接続が完了しました。");
            characteristic = chara;
            characteristic.startNotifications();
            characteristic.addEventListener('characteristicvaluechanged',onAccelerometerValueChanged);
          })  
          .catch(error => {
            console.log("BLE接続に失敗しました。もう一度試してみてください");
            console.log(error);
          });    
    },

    disconnect:function(inputs,outputs) {
        if (!accelerometer_device || !accelerometer_device.gatt.connected) return ;
        accelerometer_device.gatt.disconnect();
        console.log("BLE接続を切断しました。");
    },

    change:function(inputs,outputs) {
        this.runNextFrame();
        outputs.accX = accX;
        this.flagOutputDirty("accX");
        outputs.accY = accY;
        this.flagOutputDirty("accY");
        outputs.accZ = accZ;
        this.flagOutputDirty("accZ");
    }
})

function onAccelerometerValueChanged(event) {
    //AcceleratorX = event.target.value.getUint16(0)/1000.0;
    AcceleratorX = event.target.value.getInt16(0,true)/1000.0;
    accX = AcceleratorX;
    console.log("TargetValue="+event.target.value);

    //AcceleratorY = event.target.value.getUint16(2)/1000.0;
    AcceleratorY = event.target.value.getInt16(2,true)/1000.0;
    accY = AcceleratorY;

    //AcceleratorZ = event.target.value.getUint16(4)/1000.0;
    AcceleratorZ = event.target.value.getInt16(4,true)/1000.0;
    accZ = AcceleratorZ;
    console.log("XYZ="+Math.round(AcceleratorX)+"Y="+Math.round(AcceleratorY)+"Z="+Math.round(AcceleratorZ));
} 

ハマったところ 

AcceleratorX = event.target.value.getInt16(0,true)/1000.0;

getInt16(0,true)で符号付き16bit整数値で読み込むんですが、第2引数を省略した場合こちらの環境では数値が正しく取れていませんでした。リファレンスでは

第2引数には、任意でデータの配置方式を指定する。trueならLittle endian、falseならBig endian。省略した場合はtrueになる。

とのことでしたが、第2引数を省略せず、trueをしっかり記載すると動作が安定しました。

Canvas上にThree.jsで地球を描く

Noodl上で外部ライブラリを使ってCanvasに描画するときは下記の3連コンボが基本形になります。詳しくはこちらで紹介しています。

image603.png
図 Canvasを使う基本形

3Dモデルを表示するJavascriptノード

ThreeJS表示部
define({
    inputs:{
        mySignal:'signal',
        DOM:'domelement',
        scriptLoaded:'string',
        accX:'number',
        accY:'number',
        accZ:'number',
    },
    outputs:{
        // ExampleOutput:'string',
    },
    mySignal:function(inputs,outputs) {
        // レンダラーを作成
        // サイズを指定
        const width = window.innerWidth;
        const height = window.innerHeight;

        // 角度
        let rot = 0;

        // レンダラーを作成
        const renderer = new THREE.WebGLRenderer({
          canvas: inputs.DOM
        });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(width, height);

        // シーンを作成
        const scene = new THREE.Scene();

        // 画面上にグリッド(格子)を配置
        const grid = new THREE.GridHelper(1000, 10);
        scene.add(grid);

        // 画面上に3軸を配置
        const axes = new THREE.AxesHelper(1000);
        scene.add(axes);

        // カメラを作成
        const camera = new THREE.PerspectiveCamera(45, width / height);
        camera.position.set(0, 500, +1000);
        camera.lookAt(new THREE.Vector3(0, 0, 0));

        // 平行光源
        const directionalLight = new THREE.DirectionalLight(0xffffff);
        directionalLight.position.set(1, 1, 1);
        scene.add(directionalLight); // シーンに追加

        // 球体を作成
        const geometry = new THREE.SphereGeometry(300, 30, 30);
        // 画像を読み込む
        const loader = new THREE.TextureLoader();
        const texture = loader.load('imgs/earthmap1k.jpg');
        // マテリアルにテクスチャーを設定
        const material = new THREE.MeshStandardMaterial({
          map: texture
        });
        // メッシュを作成
        const mesh = new THREE.Mesh(geometry, material);
        // 3D空間にメッシュを追加
        scene.add(mesh);

        tick();

        // 毎フレーム時に実行されるループイベントです
        function tick() {
          rot = inputs.accX;
          pit = inputs.accY;

          // ラジアンに変換
          const radianX = (rot * Math.PI) / 180;
          const radianY = (pit * Math.PI) / 180;

          // ライトの座標に反映
          camera.position.x = 1000 *Math.cos(radianX);
          camera.position.y = 500;
          camera.position.z = 1000 * Math.sin(radianX);
          // ライトの座標に反映
          //camera.position.z = 1000 *Math.cos(radianX);
          //camera.position.x = 1000 *Math.sin(radianX)* Math.cos(radianY);
          //camera.position.y = 1000 * Math.sin(radianX)*Math.sin(radianY);

          // カメラは常に中央を向くように指定
          camera.lookAt(new THREE.Vector3(0, 0, 0));

          // レンダリング
          renderer.render(scene, camera);
          requestAnimationFrame(tick);
        }
    },

    // This function will be called when any of the inputs have changed
    change:function(inputs,outputs) {
        // ...
              console.log("change");
    }
})

5.まとめ

今回はMicro:bitとのBLE通信とCanvas上の3Dモデルの操作について説明しました。
ここまでくると結構複雑ですが、Noodl上ではJavascriptをノードとして機能分離できること。Viewとデータの接続が視覚的に見えるため見通しがいいです。

この内容はオンラインもくもく会で、もくもくしていたものです。はじめてもくもく会に参加させていいただきましたが、体感でいつもの2・3倍は集中できますね。主催されたもくもく会メンバーの方、参加された皆様ありがとうございました。

macole
Noodlを知り、アウトプットのし易さやコミュニティの盛り上がりに感化されています。 少しずつアウトプットをしていきたいです。
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
ユーザーは見つかりませんでした