MENU

溶けかけてるうさぎ HP GALLERY BLOG TOP RECENT ARTICLES POPULAR ARTICLES ABOUT THIS BLOG

CATEGORY

大学 (140) 仕事 (17) 航空宇宙 (104) 写真 (78) 旅行 (32) 飯・酒 (17) コンピュータ (119) その他 (44)

TAG

ARCHIVE

RECENT

【写真】撮影写真を Map 上に表示できるようにした 【カメラ】X100 シリーズが好きすぎる(主にリーフシャッタ) 【カメラ】X100V から X100VI に買い替えました 【自宅サーバー】Google Domains から Cloudflare にドメインを移管 【カメラ】FUJIFILM XF レンズのサイズ比較ができるようにしてみた

【WebSocket】Raspberry Piロボットを遠隔PCから中継サーバーを経由して遠隔操縦する

事象発生日:2018-12-26

記事公開日:2018-12-26

アクセス数:6149

Raspberry Piで動いているロボットを,遠隔地のPCから遠隔操縦するためのインターフェイスをつくる.

中継サーバーを介してロボットとPCは通信される.

これによって,中継サーバーのみがグローバルIPをもてば,PCとロボットそれぞれが中継サーバーにつなぎに行くことによって通信経路が確立されるというシステムとなる.

 

前記事「」の続きである.

1.開発環境

前記事「」の続きである.

ロボット

Raspberry Pi Mouse

Raspberry Pi 3

Ubuntu Server 16.04.5 LTS (Xenial Xerus)

Node.js v10.14.2

ROS kinetic

UVC対応カメラ:LOAS MCM-15W

中継サーバー

Raspberry Pi 3 Model B

Ubuntu Server 16.04.5 LTS (Xenial Xerus)

Node.js v10.14.2

遠隔PC

Microsoft Windows 10 Home 1803 (64bit)

Google Chrome 71.0.3578.80 (Official Build) (64bit)

ネットワーク

ロボット,中継サーバー,遠隔PC,ともにWiFiによって同一LANにいる.

(原理的には,中継サーバーのみグローバルIPを持っていればよい.)

2.中継サーバーの環境構築

からイメージファイルをダウンロードしてきて,このページにそってRaspberry PiにUbuntu Serverをインストールすることにより,中継サーバーとする.

 

ここでは,以下のようにNode.jpの環境を構築しておく.


$ git clone git://github.com/creationix/nvm.git ~/nvm
$ ./nvm/install.sh

再ログイン

$ nvm install --lts
$ npm install socket.io

3.実装

概要

概要図はこの通り.

 

ロボットと遠隔PCの間に中継サーバーを挟んでWebSocketで通信する.

これには,

ロボット,遠隔PCはグローバルIPを必要としない.
中継サーバー1台あれば,複数の通信セッションも開設できる.
データを中央で管理できる.
LANをまたいだ遠隔操作も可能である.

のようなメリットがある.

 

グローバルIPを持つのは中継サーバーのみなので,必然的に,ロボットと遠隔PCがそれぞれクライアントとなり,中継サーバーへ繋げにいくことになる.

遠隔PCの実装

遠隔PCは,中継サーバーへブラウザでhttp接続しに行くだけである.

ブラウザに,http://{$server_ip}:3000/と打てば,次のような画面が出てくる.

 

中継サーバーの実装

中継サーバーの役割は,

ロボットと遠隔PCの通信を仲介する
遠隔PCのブラウザに対してはWebサーバーとして働く
ロボットと遠隔PCのセッションを管理する(未実装)

である.

Node.jsのスクリプトである,node_app_relay_server.js

$ node node_app_relay_server.js

で走らせ,同ディレクトリにWebサーバーとしてのファイル3つを配置する.

 

それぞれのコードの概要を載せる.

全て載せると煩雑になるので,詳細実装は「」の過去記事を参照のこと.

(もうちょっと作り込んだら,そのうちGitHubにでも上げます.)

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<title>RaspPi Mouse Console</title>
	<link rel="stylesheet"href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
	<script type="text/javascript" src="/socket.io/socket.io.js"></script>
	<script type="text/javascript" src="./pimouse.js"></script>
	<link href="./pimouse.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="container">
	<h1>RaspPi Mouse Console</h1>
	<h4>LED:</h4>
	<div class="row">
		<button type="button" id="ledOn"  class="col-md-1 btn btn-danger">LED ON</button>
		<button type="button" id="ledOff" class="col-md-1 btn btn-danger">LED OFF</button>
	</div>
	<h4>カメラ:</h4>
	<div class="row">
		<button type="button" id="cameraOn"  class="col-md-1 btn btn-danger">ON</button>
		<button type="button" id="cameraOff" class="col-md-1 btn btn-danger">OFF</button>
	</div>
	<img id="cameraCapture">
	<h4>測距センサ:</h4>
	<div class="row">
		<button type="button" id="lightSensorSingle"   class="col-md-1 btn btn-danger">シングル</button>
		<button type="button" id="lightSensorSeqBegin" class="col-md-1 btn btn-danger">連続開始</button>
		<button type="button" id="lightSensorSeqEnd"   class="col-md-1 btn btn-danger">連続終了</button>
	</div>
	<div id="lightSensorLogs" class="log"></div>
</div>
</body>
</html>
pimouse.html
$(function() {
	var socket = io.connect();
	socket.on("s2c_LS_DATA",      function(data){AppendLsLog(data.value)});
	socket.on("s2c_CAMERA_DATA",  function(data){UpdateCamera(data.value)});

	$("button#ledOn").on('click', function() {
		console.log("LED ON");
		socket.emit("c2s_LED_ON", null);
	});

	$("button#ledOff").on('click', function() {
		console.log("LED OFF");
		socket.emit("c2s_LED_OFF", null);
	});

	... 中略 ...
});


function AppendLsLog(text) {
	console.log(text);
	$("#lightSensorLogs").append("

" + text + "

"); } function UpdateCamera(data) { console.log(data); $("#cameraCapture").attr('src', data); }
pimouse.js
@charset "utf-8";

... 中略 ...
pimouse.css
var http     = require('http');
var socketio = require('socket.io');
var path     = require('path');
var fs       = require('fs');
var mime     = {
	".html": "text/html",
	".js":   "application/javascript",
	".css":  "text/css",
	// 読み取りたいMIMEタイプはここに追記
};


var server_ws_main = http.createServer(function(req, res) {
	if (req.url == '/') {
		filePath = '/pimouse.html';
	} else {
		filePath = req.url;
	}

	var fullPath = __dirname + filePath;
	console.log('fullPath : ' + fullPath);

	res.writeHead(200, {"Content-Type": mime[path.extname(fullPath)] || "text/plain"});
	fs.readFile(fullPath, function(err, data) {
		if (err) {
			// エラー時の応答
		} else {
			res.end(data, 'UTF-8');
		}
	});
}).listen(3000);
console.log('WS Main Server running at http://localhost:3000/');


var io = socketio.listen(server_ws_main);

io.sockets.on('connection', function(socket) {

	socket.on('c2s_LED_ON', function(data) {
		console.log('c2s_LED_ON');
		io.sockets.emit('s2p_LED_ON', null);
	});

	socket.on('c2s_LED_OFF', function(data) {
		console.log('c2s_LED_OFF');
		io.sockets.emit('s2p_LED_OFF', null);
	});

	... 中略 ...

	socket.on('p2s_LS_DATA', function(data) {
		io.sockets.emit('s2c_LS_DATA', {value : data.value});
	});

	socket.on('p2s_CAMERA_DATA', function(data) {
		io.sockets.emit('s2c_CAMERA_DATA', {value : data.value});
	});

});
node_app_relay_server.js

ロボット側のNode.jsの実装

ロボットは,ブラウザではないがNode.jsでWebSocketのクライアントとして働く.

 

$ node node_app_pimouse.js

 

と走らせておけばよい.

 

コードは以下の通りである.

// web camera
var NodeWebcam  = require('node-webcam');
var opts_camera = {
	width:          320,
	height:         240,
	callbackReturn: "base64"
};

var socket = require('socket.io-client')('http://${server_ip}:3000');


// タイマー変数の初期化
var timer_lt = {
	id : null,
	is_on : 0,
}
var timer_camera = {
	id : null,
	is_on : 0,
}


socket.on('connection', function(socket) {
});

socket.on('s2p_LED_ON', function(data) {
	OnLed();
});

socket.on('s2p_LED_OFF', function(data) {
	OffLed();
});

socket.on('s2p_CAMERA_ON', function(data) {
	if (timer_camera.is_on == 0) {
		timer_camera.id = setInterval(SendCameraCapture, 200);
	}
	timer_camera.is_on = 1;
});

socket.on('s2p_CAMERA_OFF', function(data) {
	if (timer_camera.is_on == 1) {
		clearInterval(timer_camera.id);
	}
	timer_camera.is_on = 0;
});

socket.on('s2p_LS_SINGLE', function(data) {
	SendLtValue();
});

socket.on('s2p_LS_SEQ_BEGIN', function(data) {
	if (timer_lt.is_on == 0) {
		timer_lt.id = setInterval(SendLtValue, 500);
	}
	timer_lt.is_on = 1;
});

socket.on('s2p_LS_SEQ_END', function(data) {
	if (timer_lt.is_on == 1) {
		clearInterval(timer_lt.id);
	}
	timer_lt.is_on = 0;
});

socket.on('disconnect', function() {
	if (timer_lt.is_on == 1) {
		clearInterval(timer_lt.id);
		clearInterval(timer_camera.id);
	}
	OffLed();
});


// LED ON
function OnLed() {
	... 中略 ...
}

// LED OFF
function OffLed() {
	... 中略 ...
}

// 光センサの値を読み取って返す
function SendLtValue() {
	... 中略 ...
	socket.emit('p2s_LS_DATA', {value : ret.value});
}

// Camera
function SendCameraCapture() {
	console.log("Capture! outer");
	NodeWebcam.capture( "test_picture", opts_camera, function( err, data ) {
	... 中略 ...
		socket.emit('p2s_CAMERA_DATA', {value : ret.value});
	});
}
node_app_pimouse.js

4.今後

今回は全データをブロードキャストしてしまっているので,不要な通信を省き,また複数台通信に対応できるようにしていく.

 

また,動画配信もどうにかしたい.

もっと高画質で,高フレームレートで通信できる方法を考えていきたい.

5.出典

ubuntu wiki. RaspberryPi. Retrieved December 26, 2018, from https://wiki.ubuntu.com/ARM/RaspberryPi
npm. socket.io-client. Retrieved December 26, 2018, from https://www.npmjs.com/package/socket.io-client

関連記事

コメントを投稿

名前

Email (※公開されることはありません)

コメント