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

Ring:bit Carで遊ぶぞ

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

https://akiba-pc.watch.impress.co.jp/docs/news/news/1244961.html
image.png

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

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

 micro:bitをSensorTagとしてブラウザから使う:with Chromeブラウザ
 戦車ラジコン:ブラウザ+GamePadで動かす

ソース一式は以下に置いておきました。
 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

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

特に複雑なものはありません。Bootstrap(v3.4.1)とVue v2を使っています。

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です。

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より断然楽でした。

以上

poruruba
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
ユーザーは見つかりませんでした