2 minute read

前回の記事の続きです。

前回、javascriptでゲームパッドの入力取得に成功した。こいつをリアルタイムでRaspberry Piへ突っ込んだのち、シリアル通信でArduinoへ叩き込めばDC Motorの制御をゲームパッドからやることができる目論見である。

今回はRaspberry Piへ突っ込むところまで攻略する。 (本記事はChromeでしか稼動確認をしていないのでご注意ください)

Javascriptで取得したGamepadの値を、どうやってサーバ(=Raspberry Pi)へ突っ込むか

今回の最終ゴールはなるべくスムーズにDCモータへ値を反映することである。

  • といってもどうせRPI+Arduino内でリアルタイムにデータを処理するノウハウなど持っていないので、おそらく適当にiframeを使えば十分なんだろう
  • ただ、今更iframeやらXMLHttpRequestなどと10年前の技術でやるのもつまらないので、WebSocketを使って見ることにした。
    • ただ、WebSocketは使いやすそうなんだが本番向けにはプロキシ通らなそうとか、ロードバランスとか、フェールオーバとか、フェールオーバ後の再接続がすごいことになりそうとか容易に予測がつく
    • のでどこかでHTTP/2の上に乗っかるgRPCも試してみたいとは思う
  • というような知識はこちらで勉強させていただいた。ありがとうございます

WebSocketをどうつかうか

nodejs + socket.ioがもっとも簡単そうであったので、こちらをつかう

mkdir noderoot
cd noderoot
git clone https://github.com/creationix/nvm.git ~/.nvm
source ~/.nvm/nvm.sh
nvm install 8.9.4
npm install socket.io
npm install express

コード自体は、前回つくったものと、Socket.IOのサンプルをニコイチすると出来上がる

  • Socket.IOの出来がいいので、gamepadの値をStringにしてnodejsへ送信するところと、nodejsから戻ってきたデータをObjectに復号するところくらいである。作るのは
  • なお、まだやっぱりJSONのデータ作成のベストプラクティスはよくわからず。。。いつまでもゴリゴリ配列作ってていいんだろうか。。。多分違う気がするがまあいい
  • こちらを参考にさせていただきました。ありがとうございます

Nodejsで動かすコードと、クライアントで動かすhtmlの2ファイルをつくる

DCconsoleSrv.js

var app  = require('express')();
var http = require('http').Server(app);
var io   = require('socket.io')(http);

app.get('/', function(req, res){
  if(req.url == '/favicon.ico'){
    return;
  }
  res.sendFile(__dirname + '/DCconsole.html');
});

io.on('connection', function(socket){
  console.log('[user connected]');
  socket.on('disconnect', function(){
    console.log('[user disconnected]');
  });
  socket.on('gpFromClient', function(msg){
    //console.log('gpFromClient: ' + msg);
    io.emit('gpFromSrv', msg);
  });
});

http.listen(3000, function(){
  console.log('listening on *:3000');
});

DCconsole.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script src="/socket.io/socket.io.js"></script>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
      <style>
@import url('https://fonts.googleapis.com/css?family=Press+Start+2P');
body{
  color: rgba(255,255,255,.75);
  font-family: 'Press Start 2P', cursive;
  background-color: rgb(25,25,25);
  font-size: 1.0em;
  padding: 2.0em;
  line-height: 1.8rem;
}
.buttons{
}
.button{
  background-color: #4CAF50;
  border:rgba(255,255,255,.75) solid;
  color: white;
  padding: 15px 32px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
}
.bb {
  background-color: rgb(39, 28, 192); 
}
.by {
  background-color: rgb(241, 255, 44); 
  border-top:rgba(255,255,255,.75) solid;
}
.br {
  background-color: rgb(247, 68, 68); 
}
.bg {
  background-color:  #4CAF50;
}
.parent {
  display: flex;
  justify-content:left;
}
.child1{
    width: 1000px;
}
.child2 {
    border: rgb(248, 45, 45) solid;
    border: #4CAF50 solid;
    padding:10px;
    font-size: 90%;
}

form input {
    color: rgba(255,255,255,.75);
    font-family: 'Press Start 2P', cursive;
    background-color: rgb(25,25,25);
    font-size: 1.0em;
    padding: 2.0em;
    line-height: 1.8rem;
    border: 0; padding: 10px; 
    border-top:rgba(255,255,255,.75) solid;
    border-left:rgba(255,255,255,.75) solid;
    border-bottom:rgba(255,255,255,.75) solid;

}
form button {
    color: rgba(255,255,255,.75);
    font-family: 'Press Start 2P', cursive;
    background-color: rgb(224, 34, 81);
    font-size: 1.0em;
    padding: 2.0em;
    line-height: 1.8rem;
    border: 0; padding: 10px; 
    border-top:rgba(255,255,255,.75) solid;
    border-right:rgba(255,255,255,.75) solid;
    border-bottom:rgba(255,255,255,.75) solid;
}


    </style>
</head>
<body>
<script>
    function setMessage(selector,text) {
        $(selector).html(text);
    }
    function addMessage(selector,text) {
        $(selector).html($(selector).html()+"<br/>"+text);
    }
    function invertColors(elem) {
        var color = $(elem).css('color');
        $(elem).css('color') = $(elem).css('background-color');
        $(elem).css('background-color') = color;
    }
    function pickGPtoJSON(gp){
        var bi;
        var json = [];
        for( bi=0; bi < gp.buttons.length; bi++ ){
            json.push({ "btnNo" : "b" + bi, "value" : gp.buttons[bi].value});
        }
        for( bi=0; bi < gp.axes.length; bi++ ){
            json.push({ "btnNo" : "a" + bi, "value" : gp.axes[bi]});
        }
        return json;
    }

    function mainLoop(){
        var bi;
        var gp = navigator.getGamepads()[0];
        if (gp) {
            /*
            setMessage("#status","---status-------")
            for( bi=0; bi < gp.buttons.length; bi++ ){
                addMessage("#status", "b:"+bi + "> " + gp.buttons[bi].value + ":" + gp.buttons[bi].pressed)
            }
            for( bi=0; bi < gp.axes.length; bi++ ){
                addMessage("#status", "a:"+bi + "> " + gp.axes[bi])
            }
            */
            socket.emit('gpFromClient', JSON.stringify(pickGPtoJSON(gp)));
        }
        lp = raf(mainLoop);
    }
    var abcString;
    var gp = false;
    var lp;
    var raf  = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
    var rafS = window.mozCancelRequestAnimationFrame || window.webkitCancelRequestAnimationFrame || window.cancelRequestAnimationFrame;
    var socket;
    var parsedMsg;
    $(document).ready(function() {

        window.addEventListener("gamepadconnected", function(e) {
            gp = navigator.getGamepads()[e.gamepad.index];
            addMessage("#console","[NAME:"+ gp.id +"], [# OF BUTTONS:"+gp.buttons.length+"], [# OF AXES:"+gp.axes.length+"]");
            mainLoop();
        });
        window.addEventListener("gamepaddisconnected", function(e) {
            gp = false;
            addMessage("#console","connection terminated");
            rafs(lp);
        });
        
        socket = io();
        socket.on('gpFromSrv', function(msg){
            var bi;
            parsedMsg = JSON.parse(msg);
            setMessage("#status","--status of Gamepad via Srv--")
            for (bi=0; bi < parsedMsg.length; bi++) {
                addMessage("#status", parsedMsg[bi].btnNo + "> " + parsedMsg[bi].value );
            }
        });

    });
</script>
<div class="parent">
<div class="child1">
        <div class="buttons">
                <span id="btnX" class="button bb">X</span><span id="btnX" class="button by">Y</span><span id="btnX" class="button br">A</span><span id="btnX" class="button bg">B</span>
        </div>
                <span id="status">---status-------<br/>
                b:0> -:-<br/></span>
</div>
<div class="child2">
        <form id="frmMes" action="">
          <input id="m" autocomplete="off" /><button>Send</button>
        </form>
            <span id="console">console<br/>------------------------------------<br/></span>
</div>

</body>
</html>

上の2ファイルを同一フォルダに配置したら、以下で実行する

pi@raspberrypihostname:~/noderoot$ node ./DCconsoleSrv.js 
listening on *:3000

Chromeで以下にアクセスして、Gamepadを接続し操作して見る

感想

Socket.ioは作りが自然でとてもわかりやすくていい。サーバでListen開始ー>クライアント/サーバ双方emitでイベント+Valueを送り合うのはとても自然。リアルタイムの通信性能も抜群で、ゲームパッドの動きを伝えるのに十分すぎるくらいと感じる。

今回の制作でこれでゲームパッドの値がついにRaspberry PIへ入力できた。次回はいよいよこれをシリアル経由でArduinoへ叩き込む予定である。

参考にさせていただいたサイト

  • 本文中に記載

Updated: