Ring:bit Carで遊ぶぞ

かわいくて楽しそうなRing:bit Car v2を購入しました。

https://akiba-pc.watch.impress.co.jp/docs/news/news/1244961.html
![image.png](https://akiba-pc.watch.impress.co.jp/img/ah/docs/1244/961/1.jpg)


360度回転サーボが2つ付いていて、筐体はアクリルです。これは犬？猫？
そして心臓部は、micro:bitですが、ちょうどLEDマトリクスが顔の部分になります。

これをPCのブラウザからBLEで接続して、PCにUSB接続したSony DUALSHOCK4から動かしてみようと思います。
ついでに、micro:bitには加速度センサがついているので、それもリアルタイムに表示させます。
今まで蓄積してきた知識が役に立ちそうです。

　[micro:bitをSensorTagとしてブラウザから使う：with Chromeブラウザ](https://qiita.com/poruruba/items/0ff28407f3d8a005928d)
　[戦車ラジコン：ブラウザ＋GamePadで動かす](https://qiita.com/poruruba/items/7814dad663d7269e3a19)

ソース一式は以下に置いておきました。
　https://github.com/poruruba/ringbitcar

Webページとしても公開しておきました。
　https://poruruba.github.io/ringbitcar/

# micro:bitコーディング

MakeCodeを使います。

Microsoft MakeCode for micro:bit
　https://makecode.microbit.org/

まず、拡張機能として以下の2つを使います。
・bluetooth
・servo

bluetoothを使いますので、デフォルトで入っている無線は使えなくなります。
また、拡張機能に、「ringbitcar」というまさにそのものがあるのですが、残念ながら、なぜかbluetoothと共存できないようで、使うのを諦めました。ですが、処理内容は単純で、サーボを操作しているだけなので、拡張機能servoで代用できます。

また、プロジェクトの設定で、「JustWorks pairing (default): Pairing is automatic once the pairing is initiated.」ではなく、「No Pairing Required: Anyone can connect via Bluetooth.」の方を有効にしておいてください。

作成したブロックは、全体でこんな感じなんですが、Javascriptで表示した方がわかりやすいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e84c98c9-8593-32f4-d511-6173ace398fd.png)


```js
function processCommand () {
    if (cmd == "r" || cmd == "f") {
        value = parseInt(param)
        if (right != value) {
            right = value
            servos.P1.run(right)
        }
    }
    if (cmd == "l" || cmd == "f") {
        value = parseInt(param)
        if (left != value) {
            left = value
            servos.P2.run(0 - left)
        }
    }
    if (cmd == "t") {
        value = parseInt(param)
        servos.P1.run(50)
        servos.P2.run(50)
        basic.pause(value * 6)
        servos.P1.run(0)
        servos.P2.run(0)
        right = 0
        left = 0
    }
    else if (cmd == "T") {
        value = parseInt(param)
        servos.P1.run(-50)
        servos.P2.run(-50)
        basic.pause(value * 6)
        servos.P1.run(0)
        servos.P2.run(0)
        right = 0
        left = 0
    }
    else if (cmd == "s") {
        value = parseInt(param)
        servos.P1.run(50)
        servos.P2.run(-50)
        basic.pause(value * 143)
        servos.P1.run(0)
        servos.P2.run(0)
        right = 0
        left = 0
    }
    else if (cmd == "S") {
        value = parseInt(param)
        servos.P1.run(-50)
        servos.P2.run(50)
        basic.pause(value * 143)
        servos.P1.run(0)
        servos.P2.run(0)
        right = 0
        left = 0
    }
    else if (cmd == "i") {
        value = parseInt(param)
        if (value == 1) {
            basic.showIcon(IconNames.Heart)
        } else if (value == 2) {
            basic.showIcon(IconNames.EigthNote)
        } else if (value == 3) {
            basic.showIcon(IconNames.No)
        } else if (value == 4) {
            basic.showLeds(`
                . # # # .
                # . . . #
                # . . . #
                # . . . #
                . # # # .
                `)
        } else {
            basic.showIcon(IconNames.Happy)
        }
    }
}
bluetooth.onBluetoothConnected(function () {
    basic.showIcon(IconNames.Happy)
})
bluetooth.onBluetoothDisconnected(function () {
    right = 0
    servos.P1.run(right)
    left = 0
    servos.P2.run(left)
    basic.showIcon(IconNames.Asleep)
})
bluetooth.onUartDataReceived(serial.delimiters(Delimiters.Fullstop), function () {
    parseString(bluetooth.uartReadUntil(serial.delimiters(Delimiters.Fullstop)))
    processCommand()
})
function parseString (text: string) {
    cmd = text.charAt(0)
    param = text.substr(1, text.length)
}
let left = 0
let right = 0
let param = ""
let value = 0
let cmd = ""
bluetooth.startAccelerometerService()
bluetooth.startUartService()
basic.showIcon(IconNames.Yes)
```

左右の車輪にサーボがつながっており、それが、P1ピンとP2ピンに接続されています。
サーボは、「普通のサーボモーター」と「回転サーボモーター」の2種類がありますが、後者の方です。

車輪の操作は上記の通りなのですが、次は、ブラウザとのBLE通信の設定です。
ブラウザは、BLEでの仮想UARTを使うのですが、単に、キャラクタリスティックに文字列を渡すだけです。

＜最初だけ＞
Bluetooth加速度計サービスとBluetooth UARTサービスを使います。

＜Bluetooth接続されたとき＞
接続されたことがわかるように、ドット絵をHappyにしています。

＜Bluetooth接続が切断されたとき＞
車輪の回転を止めて、寂しいドット絵にしています。

＜Bluetoothデータを受信したとき＞
これがまさにブラウザから受信するデータの処理部分です。
受信文字列を解析したのち、processCommand関数を呼び出しています。

<関数processCommand>
受信文字列のフォーマットとしては、こんな感じに定義しました。「.(ドット)」終わりにしています。

　cmd(1文字)|param(任意長)|.

先頭1バイトで、ブラウザからの要求の種類を判別します。
paramには数字の文字列を指定します。

|cmd|処理内容|
|:--:|:--|
|r| 左側車輪の回転速度を設定します。paramは回転速度(-100～100)です。|
|l| 左側車輪の回転速度を設定します。paramは回転速度(-100～100)です。|
|f| 直進します。paramは回転速度(-100～100)です。|
|t| 車体を右旋回します。paramは回転角度(°)です。|
|T| 車体を左旋回します。paramは回転角度(°)です。|
|s| 前進します。paramは移動距離(cm)です。|
|S| 後退します。paramは移動距離(cm)です。|
|i| LEDマトリクスにドット絵を表示します。paramはドット絵の種類です。|

回転速度の実際の速さは、サーボの能力に依存します。
回転角度や移動距離は、適当に決めたので、環境に合わせて微調整してください。

あとは、micro:bitをUSBケーブルで接続して、出てきたドライブにhexファイルを書き込むだけです。
書き込みが終わると、チェックマークのドット絵が表示されたかと思います。

#ブラウザ側

ブラウザでは、Web Bluetooth APIとHTML5 GamePad APIを利用します。
Bluetoothを使うので、HTTPSでホスティングしている必要があります。
HTML5 GamePad APIに対応していればなんでもよいので、Sony DUALSHOCK4である必要はありません。

まずは、HTMLファイルから。
こんな画面です。
非常にシンプルで、Ring:bitに接続するためのボタンと受信した加速度の表示ぐらいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a09d1097-d7e1-82cd-5e8e-030674fb5036.png)
 
特に複雑なものはありません。Bootstrap(v3.4.1)とVue v2を使っています。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>Ring:bit Car Controller</title>

  <link rel="stylesheet" href="css/start.css">
  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>Ring:bit Car Controller</h1>

        <button class="btn btn-primary" v-on:click="ring_connect">Connect</button>
        <br>
        <label>isConected</label> {{isConnected}}
        <label>deviceName</label> {{deviceName}}
        <br>
        <h3>accelerometer</h3>
        <label class="col-xs-1">X</label><p class="col-xs-1 text-right">{{accel_x}}</p>&nbsp;
        <label class="col-xs-1">Y</label><p class="col-xs-1 text-right">{{accel_y}}</p>&nbsp;
        <label class="col-xs-1">Z</label><p class="col-xs-1 text-right">{{accel_z}}</p>&nbsp;


        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>
```

肝心のJavascriptです。

```js:start.js
'use strict';

//var vConsole = new VConsole();

const UUID_SERVICE_ACCEL = "e95d0753-251d-470a-a062-fa1922dfa9a8";
const UUID_CHAR_ACCEL = "e95dca4b-251d-470a-a062-fa1922dfa9a8";
const UUID_SERVICE_UART = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const UUID_CHAR_UART_RX = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
const UUID_CHAR_UART_TX = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';

const POWER_MAX = 50;
const CHECK_INTERVAL = 50;

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        characteristics : new Map(),
        deviceName: '',
        isConnected : false,
        encoder : new TextEncoder('utf-8'),
        control_pressed : { top: false, down: false, left: false, right: false, a: false, b: false, x: false, y: false },
        power_prev: { right: 0, left: 0 },
        accel_x: 0,
        accel_y: 0,
        accel_z: 0,
    },
    computed: {
    },
    methods: {
        ring_connect: async function(){
            var device = await navigator.bluetooth.requestDevice({
                filters: [{
                   namePrefix: "BBC micro:bit" 
                }],
                optionalServices: [
                    UUID_SERVICE_UART,
                    UUID_SERVICE_ACCEL,
                ]
            });
            console.log("requestDevice OK");
            this.characteristics.clear();
            this.bluetoothDevice = device;
            this.bluetoothDevice.addEventListener('gattserverdisconnected', (event) => {
                this.onDisconnect(event)
            });

            var server = await this.bluetoothDevice.gatt.connect();
            console.log('Execute : getPrimaryService');
            var service = await server.getPrimaryService(UUID_SERVICE_UART);
            console.log('Execute : getCharacteristic');
        
            await this.setCharacteristic(service, UUID_CHAR_UART_TX);
            await this.setCharacteristic(service, UUID_CHAR_UART_RX);
            await this.startNotify(UUID_CHAR_UART_RX);

            var service = await server.getPrimaryService(UUID_SERVICE_ACCEL);
            console.log('Execute : getCharacteristic');
        
            await this.setCharacteristic(service, UUID_CHAR_ACCEL);
            await this.startNotify(UUID_CHAR_ACCEL);

            this.deviceName = this.bluetoothDevice.name;
            this.isConnected = true;

            this.ring_start();
        },
        onDisconnect: function(event){
            console.log('onDisconnect');
            this.isConnected = false;
            this.characteristics.clear();
        },
        startNotify(uuid) {
            if( this.characteristics.get(uuid) === undefined )
                throw "Not Connected";
        
            console.log('Execute : startNotifications');
            return this.characteristics.get(uuid).startNotifications();
        },
        async setCharacteristic(service, characteristicUuid) {
            var characteristic = await service.getCharacteristic(characteristicUuid)
            console.log('setCharacteristic : ' + characteristicUuid);
            this.characteristics.set(characteristicUuid, characteristic);
            var _this_ = this;
            characteristic.addEventListener('characteristicvaluechanged', function(event){ _this_.onDataChanged(event); });
//            characteristic.addEventListener('characteristicvaluechanged', this.onDataChanged);
            return service;
        },
        writeChar(uuid, array_value) {
            if( this.characteristics.get(uuid) === undefined )
                throw "Not Connected";
        
//            console.log('Execute : writeValue');
            let data = Uint8Array.from(array_value);
            return this.characteristics.get(uuid).writeValue(data);
        },
        onDataChanged(event){
//            console.log('onDataChanged');
            let characteristic = event.target;
    
            var value = characteristic.value;
            switch(characteristic.uuid){
                case UUID_CHAR_ACCEL: {
                    this.accel_x = value.getInt16(0, true);
                    this.accel_y = value.getInt16(2, true);
                    this.accel_z = value.getInt16(4, true);
                    break;
                }
            }
        },
        ring_start: async function(){
            await this.check_gamepad();
        },
        check_gamepad: async function(){
            var gamepadList = navigator.getGamepads();
            for(var i = 0; i < gamepadList.length; i++){
                var gamepad = gamepadList[i];
                if(gamepad){
                    var power_right = Math.floor(-gamepad.axes[1] * POWER_MAX);
                    if( this.power_prev.right != power_right ){
                        this.power_prev.right = power_right;
                        var cmd = 'r' + power_right + '.';
//                        console.log(cmd);
                        await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode(cmd));
                    }
                    var power_left = Math.floor(-gamepad.axes[3] * POWER_MAX);
                    if( this.power_prev.left != power_left ){
                        this.power_prev.left = power_left;
                        var cmd = 'l' + power_left + '.';
//                        console.log(cmd);
                        await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode(cmd));
                    }

                    if( gamepad.buttons[0].pressed ){
                        if( !this.control_pressed.a ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i3.'));
                            this.control_pressed.a = true;
                        }
                    }else{
                        if( this.control_pressed.a ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i0.'));
                            this.control_pressed.a = false;
                        }
                    }
                    if( gamepad.buttons[1].pressed ){
                        if( !this.control_pressed.b ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i4.'));
                            this.control_pressed.b = true;
                        }
                    }else{
                        if( this.control_pressed.b ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i0.'));
                            this.control_pressed.b = false;
                        }
                    }
                    if( gamepad.buttons[2].pressed ){
                        if( !this.control_pressed.x ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i1.'));
                            this.control_pressed.x = true;
                        }
                    }else{
                        if( this.control_pressed.x ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i0.'));
                            this.control_pressed.x = false;
                        }
                    }
                    if( gamepad.buttons[3].pressed ){
                        if( !this.control_pressed.y ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i2.'));
                            this.control_pressed.y = true;
                        }
                    }else{
                        if( this.control_pressed.y ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('i0.'));
                            this.control_pressed.y = false;
                        }
                    }
                    if( gamepad.buttons[12].pressed ){
                        if( !this.control_pressed.top ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('s5.'));
                            this.control_pressed.top = true;
                        }
                    }else{
                        this.control_pressed.top = false;
                    }
                    if( gamepad.buttons[13].pressed ){
                        if( !this.control_pressed.down ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('S5.'));
                            this.control_pressed.down = true;
                        }
                    }else{
                        this.control_pressed.down = false;
                    }
                    if( gamepad.buttons[14].pressed ){
                        if( !this.control_pressed.left ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('T90.'));
                            this.control_pressed.left = true;
                        }
                    }else{
                        this.control_pressed.left = false;
                    }
                    if( gamepad.buttons[15].pressed ){
                        if( !this.control_pressed.right ){
                            await this.writeChar(UUID_CHAR_UART_TX, this.encoder.encode('t90.'));
                            this.control_pressed.right = true;
                        }
                    }else{
                        this.control_pressed.right = false;
                    }

                    break;
                }
            }

            setTimeout(this.check_gamepad, CHECK_INTERVAL);
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        setInterval
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );
```

〇関数ring_connect

Ring:bit CardとBLE接続します。
ここら辺はお決まりです。

各UUIDは以下の通りです。

・加速度センサのプライマリサービス： e95d0753-251d-470a-a062-fa1922dfa9a8
・加速度センサのキャラクタリスティック：e95dca4b-251d-470a-a062-fa1922dfa9a8
※キャラクタリスティックはNotificationです。

・仮想UARTのプライマリサービス： 6e400001-b5a3-f393-e0a9-e50e24dcca9e
・仮想UARTのRXのキャラクタリスティック： 6e400002-b5a3-f393-e0a9-e50e24dcca9e
・仮想UARTのTXのキャラクタリスティック： 6e400003-b5a3-f393-e0a9-e50e24dcca9e
※TXのキャラクタリスティックを使っており、RXは今回使っていません。

加速度センサからNotificationを受信するため、StartNotifyしています。
その最後に、ring_startを呼び出しています。

〇関数ring_start

check_gamepadを呼び出して、GamePadの状態を取得しています。

〇関数check_gamepad

やっていることは、PCに接続されているGamePadを探して、コントローラやボタン押下状態に応じた処理を行っています。

|対象|処理内容|
|:--:|:--|
|左側アナログコントローラ|左側車輪の加速|
|右側アナログコントローラ|右側車輪の加速|
|左側十字キー|(上)前進、(下)後退、(左)左旋回、(右)右旋回|
|右側ボタン|押している間LEDマトリクスのドット絵を変更|

左右のアナログコントローラで、左右の車輪を独立で操作します。
戦車を操作する感覚です。具体的には、
左右両方を上に倒すと前に前進し、両方を下に倒すと後退します。
右より左が上であれば右に曲がり、左より右が上であれば左に曲がります。

CHECK_INTERVAL=50msecの間隔でチェックしているので、BLE通信過多になるかもしれず、前回チェック時と同じ値だったら送信しないようにしています。

〇コールバック関数onDataChanged

micro:bitからNotificationが受信されると、呼び出されます。
加速度センサーからのNotificationであれば、int16がリトルエンディアンで3つありますので、それぞれ取得しています。

#終わりに

すんなり実装できました。
BeetleCより断然楽でした。

以上