Rapiroをブラウザで動かしてみた 【全体構成】

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

「Rapiroをブラウザで動かしてみた 【準備編】」の記事はこちら

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

とりあえず完成形

  1. カメラの映像
  2. 顔の向きを操作するスライダー
  3. 歩行用コントローラーボタン
  4. 目の色を変えるボタン
  5. 歩行以外のプリセットの動作をするボタン
  6. 入力したテキストをしゃべるフォーム

全体の構成

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が会話。入力した文字をしゃべらせる方法を解説。