Rapiroの機能をひとまとめにしたインターフェイスを作成して、ブラウザから操作できるようにした。
全体の構成と機能ごとの実装解説記事へのリンクをまとめた。
「Rapiroをブラウザで動かしてみた 【準備編】」の記事はこちら

Rapiroをブラウザで動かしてみた 【準備編】
WebIOPiを使ってRapiroをブラウザから操作するためのWebサーバーを立てる方法を解説。
とりあえず完成形

- カメラの映像
- 顔の向きを操作するスライダー
- 歩行用コントローラーボタン
- 目の色を変えるボタン
- 歩行以外のプリセットの動作をするボタン
- 入力したテキストをしゃべるフォーム
全体の構成
HTML・CSS・JavaScript・Pythonの4つの要素で構成されている。
それぞれの役割は以下の通り。
HTML・CSS
ブラウザ上の見た目の部分。そのまま使ってもよいし、自分好みにデザインを変更しても構わない。
ただし、HTML内のidやclassはJavaScriptから参照している部分があるので、変更する際は影響がでないように注意。
JavaScript
ブラウザでの操作内容を受け取り、Pythonに値を渡す部分。
冒頭に書かれているwebiopi().readyは、WebIOPiライブラリの初期化処理。
これにより、GPIO制御などの機能が使えるようになる。
ready関数の先頭で宣言されている変数statusは、今どんな動作を行っているかを管理するためのもの。
各動作が終わったら、statusは必ずstopに戻すようにする。
change_rapiro関数は、各動作の値を渡す共通関数。
マクロ名(動作名)を指定して、WebIOPi経由でPythonに値を渡している。
Python
Rapiroを制御する部分。
音声の再生、シリアル通信でコマンドの送信を行う。
Rapiroに動作を指示するコマンドには、次の2種類がある。
- Mコマンド(#M)
あらかじめRapiroに設定された動作を呼び出す。 - Pコマンド(#P)
サーボモーターの角度やLEDの色を、特定の形式で細かく制御できる。
歩行・LED・プリセット動作は動作ごとに関数を分けているが、やっていることは同じなので、まとめてしまってもよい。
cmd_input関数は、コマンド送信処理を共通化した関数。
渡されたコマンドをエンコードしてシリアル通信でRapiroに送信している。
ソースコード一式
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Rapiroコントローラー</title>
<script type="text/javascript" src="/webiopi.js"></script>
<script type="text/javascript" src="/rapiro.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="wrapper">
<div class="group_wrap">
<div class="btn_wrap">
<img class="movie_area" src="http://192.168.95.198:8080/?action=stream">
</div>
</div>
<div class="group_wrap">
<div class="btn_wrap">
<input type="range" class="face_direction" id="face_direction" min="0" max="180" step="10">
</div>
</div>
<div class="group_wrap">
<button type="button" class="move_btn upper_triangle" id="forward"></button>
<div class="btn_wrap">
<button type="button" class="move_btn left_triangle" id="left"></button>
<button type="button" class="move_btn right_triangle" id="right"></button>
</div>
<button type="button" class="move_btn lower_triangle" id="backward"></button>
</div>
<div class="group_wrap">
<div class="btn_wrap">
<button type="button" class="led_btn led_red" id="led_red"></button>
<button type="button" class="led_btn led_green" id="led_green"></button>
<button type="button" class="led_btn led_blue" id="led_blue"></button>
<button type="button" class="led_btn led_yellow" id="led_yellow"></button>
<button type="button" class="led_btn" id="led_white"></button>
<button type="button" class="led_btn led_black" id="led_black"></button>
</div>
</div>
<div class="group_wrap">
<div class="btn_wrap">
<button type="button" class="preset_btn" id="stop">停止</button>
<button type="button" class="preset_btn" id="wave_both">両手を振る</button>
<button type="button" class="preset_btn" id="wave_right">右手を振る</button>
</div>
<div class="btn_wrap">
<button type="button" class="preset_btn" id="wave_left">左手を振る</button>
<button type="button" class="preset_btn" id="grip_both">両手を握る</button>
<button type="button" class="preset_btn" id="extend_right">右手を伸ばす</button>
</div>
</div>
<div class="group_wrap">
<div class="btn_wrap">
<input type="text" class="talk_text" id="talk_text">
<button type="button" class="talk_btn" id="talk_btn">話す</button>
</div>
</div>
<div>
</body>
</html>
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video, button {
margin:0;
padding:0;
border:0;
outline:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
body {
line-height:1;
}
article,aside,details,figcaption,figure,
footer,header,hgroup,menu,nav,section {
display:block;
}
ul {
list-style:none;
}
blockquote, q {
quotes:none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content:'';
content:none;
}
a {
margin:0;
padding:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
text-decoration: none;
}
ins {
background-color:#fff;
color:#000;
text-decoration:none;
}
mark {
background-color:#fff;
color:#000;
font-style:italic;
font-weight:bold;
}
del {
text-decoration: line-through;
}
abbr[title], dfn[title] {
border-bottom:1px dotted;
cursor:help;
}
table {
border-collapse:collapse;
border-spacing:0;
}
hr {
display:block;
height:1px;
border:0;
border-top:1px solid #cccccc;
margin:1em 0;
padding:0;
}
input, select {
vertical-align:middle;
}
.wrapper {
width: 360px;
margin: 0 auto;
text-align: center;
}
button {
margin-top: 5px;
margin-bottom: 5px;
}
.group_wrap {
margin-bottom: 20px;
}
.btn_wrap {
display: flex;
justify-content: center;
}
.upper_triangle {
width: 0;
height: 0;
border-style: solid;
border-right: 50px solid transparent;
border-left: 50px solid transparent;
border-bottom: 87px solid #666;
border-top: 0;
background: #555;
}
.lower_triangle {
width: 0;
height: 0;
border-style: solid;
border-right: 50px solid transparent;
border-left: 50px solid transparent;
border-top: 87px solid #666;
border-bottom: 0;
background: #555;
}
.right_triangle {
width: 0;
height: 0;
border-style: solid;
border-top: 50px solid transparent;
border-bottom: 50px solid transparent;
border-left: 87px solid #666;
border-right: 0;
margin-left: 55px;
background: #555;
}
.left_triangle {
width: 0;
height: 0;
border-style: solid;
border-top: 50px solid transparent;
border-bottom: 50px solid transparent;
border-right: 87px solid #666;
border-left: 0;
margin-right: 55px;
background: #555;
}
.led_btn {
margin-right: 5px;
margin-left: 5px;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px #555 solid;
}
.led_red {
background: red;
}
.led_green {
background: green;
}
.led_blue {
background: blue;
}
.led_yellow {
background: gold;
}
.led_black {
background: black;
}
.preset_btn, .talk_btn, .powor_btn {
margin-right: 5px;
margin-left: 5px;
width: 100px;
height: 40px;
color: #fff;
background: #555;
}
.talk_text {
width: 200px;
}
.movie_area {
width: 100%;
margin-top: 5px;
}
.face_direction {
-webkit-appearance: none;
appearance: none;
outline: none;
width: 100%;
height: 8px;
border-radius: 8px;
background: #aaa;
transform:rotate(180deg);
}
.face_direction::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: 20px;
width: 20px;
background-color: #555;
border-radius: 50%;
}
webiopi().ready(function() {
var status = 'stop';
// 「顔の向き」スライダー
document.getElementById('face_direction').addEventListener('change', function() {
change_rapiro('face_direction', this.value);
status = 'stop';
});
// 「前進」「後退」「右」「左」ボタン
document.querySelectorAll('.move_btn').forEach(button => {
button.addEventListener('pointerdown', function () {
if (status === 'stop') {
change_rapiro('move', this.id);
}
});
button.addEventListener('pointerup', function () {
change_rapiro('move', 'stop');
status = 'stop';
});
});
// 「LED」ボタン
document.querySelectorAll('.led_btn').forEach(button => {
button.addEventListener('pointerup', function () {
change_rapiro('led', this.id);
status = 'stop';
});
});
// 「プリセット動作」ボタン
document.querySelectorAll('.preset_btn').forEach(button => {
button.addEventListener('pointerup', function () {
change_rapiro('preset', this.id);
status = 'stop';
});
});
// 「話す」ボタン
document.getElementById('talk_btn').addEventListener('pointerup', function() {
change_rapiro('talk', document.getElementById('talk_text').value);
document.getElementById('talk_text').value = '';
status = 'stop';
});
// ラピロを動かすマクロ呼び出し
function change_rapiro(type, param) {
status = type;
webiopi().callMacro(type, param);
}
});
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# rapiro.py
import sys
import serial
import time
import random
import subprocess
import urllib.parse
import webiopi
ser = serial.Serial('/dev/ttyAMA0', 57600, timeout = 10)
# -------------------------------
# 顔の向き
# -------------------------------
@webiopi.macro
def face_direction(value):
cmd = "PS00A" + value.zfill(3) + "T005"
cmd_input(cmd)
# -------------------------------
# 歩行
# -------------------------------
@webiopi.macro
def move(value):
direction = {
"stop": "M0", # 停止
"forward": "M1", # 前進
"backward": "M2", # 後退
"left": "M3", # 左に曲がる
"right": "M4", # 右に曲がる
}
if value in direction:
cmd_input(direction[value])
# -------------------------------
# 目の色
# -------------------------------
@webiopi.macro
def led(value):
color = {
"led_red": "PR255G000B000T002", # 赤
"led_green": "PR000G255B000T002", # 緑
"led_blue": "PR000G000B255T002", # 青
"led_yellow": "PR200G255B000T002", # 黄
"led_white": "PR100G255B100T002", # 白
"led_black": "PR000G000B000T002", # 黒
}
if value in color:
cmd_input(color[value])
# -------------------------------
# プリセット動作
# -------------------------------
@webiopi.macro
def preset(value):
actions = {
"stop": "M0", # 停止
"wave_both": "M5", # 両手を振る
"wave_right": "M6", # 右手を振る
"wave_left": "M8", # 左手を振る
"grip_both": "M7", # 両手を握る
"extend_right": "M9", # extend_right
}
if value in actions:
cmd_input(actions[value])
# -------------------------------
# コマンド実行
# -------------------------------
@webiopi.macro
def cmd_input(value):
cmd = "#" + value
ser.write(cmd.encode("utf-8"))
# -------------------------------
# しゃべらせる
# -------------------------------
@webiopi.macro
def talk(value):
wav = "/home/panda/py_code/rapiro_talk.wav"
cmd = [
"open_jtalk",
"-x", "/var/lib/mecab/dic/open-jtalk/naist-jdic",
"-m", "/usr/share/hts-voice/mei/mei_normal.htsvoice",
"-a", "0.4",
"-r", "0.7",
"-ow", wav
]
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write(urllib.parse.unquote(value).encode("utf-8"))
proc.stdin.close()
proc.wait()
subprocess.Popen(["aplay", "-q", wav])
各機能の実装解説記事リンク

Rapiroをブラウザで動かしてみた 【カメラ表示編】
Rapiroのカメラ映像をブラウザコントローラーに表示する仕組みを解説。配信から表示、起動時の設定までの流れを紹介。

Rapiroをブラウザで動かしてみた 【顔の向き操作編】
直感的なスライダー操作でRapiroの顔の向きを動かす方法を解説。

Rapiroをブラウザで動かしてみた 【歩行コントローラー編】
ブラウザ操作の大本命。ラジコン感覚でRapiroの歩行を操作する方法を解説。

Rapiroをブラウザで動かしてみた 【目の色変更編】
Rapiroの目の色を変えて表情豊かに。ボタン操作で目の色を変える方法を解説。

Rapiroをブラウザで動かしてみた 【プリセット動作編】
Rapiroに元から実装されている機能もフル活用。ボタン操作でプリセット動作をする方法を解説。

Rapiroをブラウザで動かしてみた 【おしゃべり編】
自分の代わりにRapiroが会話。入力した文字をしゃべらせる方法を解説。