JAvaScriptを使ったロボット作りの3回目。今回は小型Wi-Fi開発ボード”WioNode”を利用してロボットアームを操作してみます。

RaspberryPiからシリアル通信を利用してサーボモーターの制御が出来ないので、WiFiでの通信を利用する作戦です。

ローカルブラウザを利用してサーボモーターを取り付けたロボットアームを動かしています。名付けて「ポンボット2号」(そのまんま)。

ストリーミングカメラを取り付ければカメラアングルをWiFiで操作することが出来ます。

さて、この「ポンボット2号」、仕組みは意外とシンプルなので作り方をちょっと紹介してみます。

WioNode

Wio -Node - スイッチサイエンス

総務省の工事設計認証(いわゆる技適)取得済みのESP-WROOM-02を搭載した、小型Wi-Fi開発ボードです。AndroidとiOS用の専用アプリが提供されています。
GROVEコネクタを二つ搭載。GROVEモジュールを利用することにより、はんだづけも不要で、すぐに開発が可能です

GroveシステムとiOS(アンドロイド)アプリを利用して様々なセンサやモーター類をWiFiから操作出来る優れものの開発ボードです。

Wio Link - Itunes

今回はiOSアプリで設定してみました。アカウントを作成し、ログインします。

実行ボタンを4秒押すとWioNodeから電波が発せられます。

iPhoneからWiFi接続を確認しネットワーク接続します。

WiFiのパスワードを入力します。これだけでWioNodeがローカルネットワークに接続されました。

アプリにGrove接続するアイテムの選択画面が表示されます。ここでは両方ともサーボモーターを選択します。

下に「ファームウェアアップデート」の表示が出るので、ここをタップするとWioNodeにArduinoスケッチが自動的に書き込まれます。

OTA(Over-The-Air)と言うそうなのですが、ハードウェアソフトの書き込みがこんなに簡単に出来るとは驚きの技術です。

ファームウェアの書き込みが終わると「ビューAPI」の表示が出ますのでこれをタップします。

製造元のAPIに接続するチュートリアル画面が表示されます。サーボモーターの場合、リクエストメソッドに「角度」「動作時間」などを指定することが出来ます。

発行されたアクセストークンを指定して送信します。

動きました。ここまで所要時間5分ほどです。

Socket.ioを利用してPOSTする

child prosessを利用してcurlコマンドを叩いても良いのですが、今回はAPIにPOSTリクエストを送信してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var request = require('request');
io.sockets.on('connection', function (socket) {
socket.on('slide_servo_1', function (degree_1) {
console.log('サーボ1: ' + degree_1);
var options_1 = {
uri: 'https://us.wio.seeed.io/v1/node/GroveServoD0/angle/' + degree_1,
form: { access_token: '発行されたアクセストークン' },
json: true
};
request.post(options_1, function(error, response, body){
if (!error && response.statusCode == 200) {
console.log(body);
} else {
console.log('error: '+ response.statusCode);
}
});
});

クライアント側から送信された角度をrequestモジュールを利用してAPIに送信します。今回はスライダーでモーターの角度を送信するようにしてみました。

ちょっと長いですがサンプルコードはこんな感じです。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<html>
<head>
<meta charset="utf-8">
<title>PonBot02</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/9.2.0/css/bootstrap-slider.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Orbitron" rel="stylesheet">
<style>
body{
font-family: 'Orbitron', sans-serif;
background-color: #3D8EB9;
color: #f6f6f6;
max-width: 480px;
margin: 0 auto;;
padding: 20px;
}
.slider-box{
margin: 40px 0;
}
.btn-group-width{
max-width: 250px;
margin: 0 auto;
}
</style>
</head>
<body>
<article>
<h1>PonBot02</h1>
<h2>Servo</h2>
<div class="slider-box text-center">
<input id="servo_1" type="text" data-slider-id="servo_1_Slider" data-slider-min="30" data-slider-max="150" data-slider-step="1" data-slider-value="90" data-slider-tooltip="show" />
</div>
<div class="slider-box text-center">
<input id="servo_2" type="text" data-slider-id="servo_2_Slider" data-slider-min="30" data-slider-max="150" data-slider-step="1" data-slider-value="90" data-slider-tooltip="show" />
</div>
<p class="btn-group btn-group-justified btn-group-width" role="group">
<a id="servo_yes" type="button" class="btn btn-default btn-lg"> Yes </a> <a id="servo_no" type="button" class="btn btn-default btn-lg"> No </a>
</p>
</article>
<script src="https://cdn.socket.io/socket.io-1.2.0.js"></script>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/9.2.0/bootstrap-slider.js"></script>
<script>
$(document).ready(function() {
var socket = io.connect('http://192.168.0.12');
$('#servo_1').slider().on('slide', function(e) {
socket.emit('slide_servo_1',e.value);
});
$('#servo_2').slider().on('slide', function(e) {
socket.emit('slide_servo_2',e.value);
});
$('#servo_yes').click(function(e){
setTimeout(function(){
socket.emit('click_servo_yes',60);
setTimeout(function(){
socket.emit('click_servo_yes',120);
setTimeout(function(){
socket.emit('click_servo_yes',60);
setTimeout(function(){
socket.emit('click_servo_yes',120);
setTimeout(function(){
socket.emit('click_servo_yes',90);
},2000);
},1200);
},900);
},600);
},300);
});
$('#servo_no').click(function(e){
setTimeout(function(){
socket.emit('click_servo_no',60);
setTimeout(function(){
socket.emit('click_servo_no',120);
setTimeout(function(){
socket.emit('click_servo_no',60);
setTimeout(function(){
socket.emit('click_servo_no',120);
setTimeout(function(){
socket.emit('click_servo_no',90);
},2000);
},1200);
},900);
},600);
},300);
});
});
</script>
</body>
</html>

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
var express = require("express");
var app = express();
var path = require("path");
var server = require('http').Server(app);
var io = require('socket.io')(server);
var fs = require('fs');
var request = require('request');
server.listen(5000, function () {
console.log('listening on *:5000');
});
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname + '/index.html'));
});
io.sockets.on('connection', function (socket) {
socket.on('slide_servo_1', function (degree_1) {
console.log('サーボ1: ' + degree_1);
var options_1 = {
uri: 'https://us.wio.seeed.io/v1/node/GroveServoD0/angle/' + degree_1,
form: { access_token: '発行されたアクセストークン' },
json: true
};
request.post(options_1, function(error, response, body){
if (!error && response.statusCode == 200) {
console.log(body);
} else {
console.log('error: '+ response.statusCode);
}
});
});
socket.on('slide_servo_2', function (degree_2) {
console.log('サーボ2: ' + degree_2);
var options_2 = {
uri: 'https://us.wio.seeed.io/v1/node/GroveServoD1/angle/' + degree_2,
form: { access_token: '発行されたアクセストークン' },
json: true
};
request.post(options_2, function(error, response, body){
if (!error && response.statusCode == 200) {
console.log(body);
} else {
console.log('error: '+ response.statusCode);
}
});
});
socket.on('click_servo_yes', function (degree_y) {
console.log('サーボ2: ' + degree_y);
var options_y = {
uri: 'https://us.wio.seeed.io/v1/node/GroveServoD1/angle/' + degree_y,
form: { access_token: '発行されたアクセストークン' },
json: true
};
request.post(options_y, function(error, response, body){
if (!error && response.statusCode == 200) {
console.log(body);
} else {
console.log('error: '+ response.statusCode);
}
});
});
socket.on('click_servo_no', function (degree_n) {
console.log('サーボ2: ' + degree_n);
var options_n = {
uri: 'https://us.wio.seeed.io/v1/node/GroveServoD0/angle/' + degree_n,
form: { access_token: '発行されたアクセストークン' },
json: true
};
request.post(options_n, function(error, response, body){
if (!error && response.statusCode == 200) {
console.log(body);
} else {
console.log('error: '+ response.statusCode);
}
});
});
});

スライダーでの角度調整と合わせて「YES」ボタンで首を2回縦に振り、「NO」ボタンで首を横に2回振る様にしてみました。

まとめ

なんとか操作が出来る様になったものの、シリアル通信の様に滑らかには動作しません。

有線で接続しているのとは違い、APIリクエストからのレスポンスに若干遅延が発生するため、リクエスト数が多すぎるとエラーになって動きがカクカクします。

まあ、ともあれ「ポンボット1号」に引き続き「ポンボット2号」が動作する様になりました。

次回はこれらを合体させ、音声操作してみたいと思います(多分)。