どうも。
メイカー系イベント出展に入れ込むあまり、人生(の金と時間を)溶かし中の小野寺修(@SAMonodera)と申します。
#はじめに
本作は、obniz という高機能なワンボードマイコンを使い倒してみたくなり、トライしたものです。
内容は、イベント来場者のスマートフォンが、ただちにその場でラジコンのコントローラになるというものです。つまり、ひとつのラジコン模型を来場者みんなでシェアする、というコンセプトの作品です。
以前から、インテリジェントなマイコン&ラジコン模型(RC)という組み合わせで一度何かを作ってみたかったので、ひとつづつ機能を拡張しながら、それを楽しみながら実現してみることにしました。
自分勝手流でワガママ仕様なRCを実現するにあたっては、見た目に妥協しないことが方針のひとつでしたので、タミヤの1/35スケール戦車モデルならではのリアルな外観はそのままに、しかし内部にはひたすら手を入れています。
基本方針:
- 外見重視
- 1/35スケールというスモールスペースへの挑戦
- ブラウザを使い、スマホの傾きと角度でコントロール
- 「シェアリングRC」というアイディアの実現
- あくまで自分が欲しい仕様を追求
#その写真
シェアリングのデモをより分かり易くするために、現在は2台で運用しています。
###内部:
白い楕円形の物体は、Finger Powという極小のモバイルバッテリーです。
そして上に飛び出ている二個のピンは、車両の上蓋に付いているステレオ・スピーカーに接続するためのものです。
###背面:
中央にobnizのディスプレイと、それぞれ前後方に距離センサー用のふたつの穴が見えます。
#その動画
【動画1】 スマホがコントローラに早変わり! 1/35スケール戦車模型とobnizで、
スマホを傾けて操作する「シェアリングRC」を作ってみた
【動画2】 塗装完了バージョン (スピーカーを繋ぎ忘れたため効果音なし)
#どこまでも自分仕様な搭載機能の数々
本作はせっかく obniz という使い勝手のあるマイコンボードを搭載していますので、その利点を生かして色々とワガママな仕様にしてみました。
-
GUIを超越した直感的な操作フィールを求めた結果、スマホの傾きだけで前進/後退/左右移動/超信地旋回を行えるようにした。さらに、傾きの量(角度)でモーターの出力が0%~100%に調節される。
-
ひとつの車両を、その場にいる誰もが、自分のスマホを使ってコントロールできる「シェアリングRC」というアイディアを具現。
-
三軸センサーを搭載。車両が裏返されたことを検知したら、全ての動きをキャンセルする。
-
テーブルや机の上で遊ぶことを想定して、落下防止機能を搭載した。車両の前と後に距離センサーを装備して、前進または後退時に落差を検出すると自動的に緊急停止する。
-
最大出力4Wのステレオスピーカーを搭載。停止中はアイドリング音、走行中は走行音を、44,100Hz/32bitのサンプリングレートで再生。また、モーターの出力に合わせて走行音のボリュームもリアルタイムに変化する。
-
ソレノイドを搭載。始動/停止/車両裏返し時に、車両からの動作フィードバックとしてクリックな音と感触を表現してみた。
#「シェアリングRC」とは
本作はインテリジェントなRCとして実現したいことを複数実装していたりしますが、その中でコアとなるのが「シェアリングRC」というアイディアです。
ひとつのRCを、その場にいる誰でもが、自分のスマホを使ってコントロールすることが出来ます。
専用のコントローラ(プロポ)も要りません。
ただ認証に手間取ってしまっては、せっかくの企画も興ざめですので、可能な限り単純な手順にしています。
###ペアリングの方法:
① スマホからWebサイトにアクセス
② RCを裏返す
③ 裏側のディスプレイに表示される3桁のPINコードを、Webサイトに入力
これだけで、この模型は(あなたがリリースするまで)あなただけがコントロールできる状態になります。
#今回使ったもの
- タミヤ 1/35 WWIイギリス戦車 マークIVメール(シングルモーターライズ仕様)
- obniz
- 距離センサー「GP2Y0A21YK0F」×2
- 三軸センサー「KXR94-2050」
- MP3プレーヤーモジュール「Grove MP3 v2.0」
- ステレオアンプモジュール「KKHMF PAM8406」
- スピーカー(2W 8Ω) ×2
- ソレノイド「5V ZHO-0420S-05A4.5」
- モバイルバッテリー「FingerPow II」
#構成
###ソフトウェア:
全体の構成は、次のようになっています。
###ハードウェア:
obnizは標準で12個ものI/Oピンを搭載しており、また各ピンには5V1Aまでの電源も供給することが出来るため、通常はobniz本体だけで充分です。
しかし、今回は接続するパーツがテンコ盛りですので、とても12個のピン数では足りません。
そこで、各パーツへの電源供給用のピンを節約するために、電源分配用として8ピン×2列のピンソケットを追加することにしました。
#プログラム
このプログラム自身、obzizが用意するWeb上のエディタでそのまま編集と実行が可能です。
尚、プログラム内部にはいくつかの工夫を施しています。詳しくは次項で解説していきます。
※ 以下はobnizを駆動する部分(JavaScript)のみの解説となります(今後加筆していく予定です)。
サンプルコード
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
<script src="https://unpkg.com/obniz@3.1.0/obniz.js" crossorigin="anonymous"></script>
</head>
<body>
<div id="obniz-debug"></div>
<h1>obniz instant HTML</h1>
<button id="on" class="btn btn-primary">Engine Start</button>
<button id="off" class="btn btn-primary">Engine Stop</button>
<div id="result1"></div>
<div id="result2"></div>
<script>
const thresholdValueY = 10, thresholdValueX = 10; // スマホの水平位置の閾値
const thresholdDistanceF = 100, thresholdDistanceB = 150; // 前後方の距離センサの閾値
const thresholdTankZ = -0.5; // 車体の裏返しを判定する閾値
const maxLimitY = 20, maxLimitX = 40; // スマホで認める傾きの最大量
const soundBaseLevel = 20; // 標準(始動)時の音量
const perVolume = (31-soundBaseLevel)/100; // 音量の一単位
var emergencyStopF = true, emergencyStopB = true; // true: 緊急停止
var canMove = false;
var startedY = 0, startedX = 0; // スマホで駆動を開始した水平位置(あそび)
var allowedAngleY = false, allowedAngleX = false
var phoneY = 0, phoneX = 0;
var alreadySoundStarted = false;
var distanceF = 0, distanceFpast = 0, distanceFpastest = 0;
var distanceB = 0, distanceBpast = 0, distanceBpastest = 0;
var tankZ = 0, tankZpast = 0, tankZpastest = 0;
// スマホ傾き感知イベント
var alpha = 0, beta = 0, gamma = 0;
window.addEventListener('deviceorientation', function (event) {
alpha = event.alpha;
beta = event.beta;
gamma = event.gamma;
}, true);
var obniz = new Obniz("OBNIZ_ID_HERE");
obniz.onconnect = async function () {
// センサー類の定義とピン配列
var motorL = obniz.wired("DCMotor", { forward: 0, back: 1 });
var motorR = obniz.wired("DCMotor", { forward: 2, back: 3 });
var distanceSensorF = obniz.wired("GP2Y0A21YK0F", {signal:4});
var distanceSensorB = obniz.wired("GP2Y0A21YK0F", {signal:5});
var triaxialSensor = obniz.wired("KXR94-2050", { x:6, y:6, z:6 }); // Z軸の値があれば良く、ピンを節約したいため
var solenoid = obniz.wired('Solenoid', {signal:7});
var mp3 = obniz.wired("Grove_MP3", { vcc:9, mp3_rx:10, mp3_tx:11});
await mp3.initWait();
// 前後方距離センサの緊急停止判定
distanceSensorF.start(function( distance ){
distanceFpastest=distanceFpast;
distanceFpast=distanceF;
distanceF=distance;
emergencyStopF = distanceF > thresholdDistanceF && distanceFpast > thresholdDistanceF && distanceFpastest > thresholdDistanceF;
})
distanceSensorB.start(function( distance ){
distanceBpastest=distanceBpast;
distanceBpast=distanceB;
distanceB=distance;
emergencyStopB = distanceB > thresholdDistanceB && distanceBpast > thresholdDistanceB && distanceBpastest > thresholdDistanceB;
})
// スマホ「Engine Start」ボタン押下
$('#on').click(function () {
obniz.display.clear();
if (allowedAngleY && allowedAngleX) {
solenoid.click();
obniz.display.print("パンツァーフォー");
startedY = phoneY;
startedX = phoneX;
emergencyStopF=false;
emergencyStopB=false;
canMove = true;
mp3.setVolume(soundBaseLevel);
mp3.play(1); // アイドリング音
} else {
obniz.display.print("要スマホ画面確認");
}
});
// スマホ「Engine Stop」ボタン押下
$('#off').click(function () {
blackOut(motorL,motorR,mp3,solenoid);
});
obniz.display.clear();
obniz.display.print("始動準備完了");
// メインループ
obniz.repeat(async function () {
phoneY =Math.floor(beta);
phoneX = Math.floor(gamma);
allowedAngleY = phoneY<20 && phoneY>-20; // 出来るだけ水平に保ってもらう
allowedAngleX = phoneX<20 && phoneX>-20; // 出来るだけ水平に保ってもらう
let AllowedHighAngleY = phoneY<80 && phoneY>-80; // 90度反転に対処
if (canMove && AllowedHighAngleY) {
let motionY = phoneY - startedY;
let isBack = (motionY >= 0);
let adjustmentValueY = motionY + thresholdValueY * (isBack ? -1 : 1);
var valueY = (adjustmentValueY * (isBack ? 1 : -1) / maxLimitY * 100);
valueY = (isBack ? emergencyStopB || adjustmentValueY <= 0 : emergencyStopF || adjustmentValueY >= 0) ? 0 : (valueY > 100 ? 100 : Math.floor(valueY));
moterControl(motorL,motorR,valueY,adjustmentValueY);
soundControl(mp3,valueY);
// 車体が裏返った?
if (isTankFlip(triaxialSensor)){
blackOut(motorL,motorR,mp3,solenoid);
solenoid.click();
}
result1.innerHTML = "出力:" + valueY;
result2.innerHTML = "前進緊急停止:" + emergencyStopF + "<br>" + "後進緊急停止:" + emergencyStopB;
} else {
result1.innerHTML = (allowedAngleX?"":"X軸の角度を立て過ぎです");
result2.innerHTML = (allowedAngleY?"":"Y軸の角度を立て過ぎです");
}
},150)
mp3.stop();
}
// DCモーターの制御
function moterControl(motorL,motorR,Y,adjustmentY){
var powerL = Y;
var powerR = Y;
var spinTurn = false;
var forwordOrBackL = adjustmentY >= 0;
var forwordOrBackR = adjustmentY >= 0;
let motionX = Math.floor(gamma) - startedX;
let LeftOrRightX = (motionX < 0);
let AdjustmentValueX = motionX + thresholdValueX * (LeftOrRightX ? 1 : -1);
var X = (AdjustmentValueX * (LeftOrRightX ? -1 : 1) / maxLimitX * 100);
if (LeftOrRightX) { // 左
X = AdjustmentValueX >= 0 ? 0 : (X > 100 ? 100 : Math.floor(X));
spinTurn = X == 100;
powerL = Y - Math.floor(Y * X / 100);
if (spinTurn) {
powerL = Math.floor(Y * X / 100);
forwordOrBackL = !forwordOrBackL;
}
} else { // 右
X = AdjustmentValueX <= 0 ? 0 : (X > 100 ? 100 : Math.floor(X));
powerR = Y - Math.floor(Y * X / 100);
spinTurn = X == 100;
if (spinTurn) {
powerR = Math.floor(Y * X / 100);
forwordOrBackR = !forwordOrBackR;
}
}
motorL.power(powerL);
motorR.power(powerR);
motorL.move(forwordOrBackL);
motorR.move(forwordOrBackR);
}
// サウンドの制御
function soundControl(mp3,Y){
mp3.setVolume(Math.floor(Y * perVolume)+soundBaseLevel); // ボリュームのコントロール
// 再生音の切り替え
if (Y==0){
if (alreadySoundStarted) {
alreadySoundStarted=false;
mp3.play(1); // アイドリング音
}
} else {
if (!alreadySoundStarted) {
alreadySoundStarted=true;
mp3.play(2); // 駆動音
}
}
}
// 車体の裏返りの判定
function isTankFlip(triaxialSensor){
let values = triaxialSensor.get();
tankZpastest = tankZpast;
tankZpast = tankZ;
tankZ = values.z;
return tankZ < thresholdTankZ && tankZpast < thresholdTankZ && tankZpastest < thresholdTankZ;
}
// 緊急停止
function blackOut(motorL,motorR,mp3,solenoid) {
solenoid.click();
emergencyStopF = true;
emergencyStopB = true;
motorL.stop();
motorR.stop();
obniz.display.clear();
obniz.display.print("停止");
canMove = false;
mp3.stop();
}
</script>
</body>
</html>
#解説
以下、今回使用したパーツやプログラムについての解説です。
##obniz:
クラウドの仕組みを使って動作する新世代のワンボードマイコンです。
これひとつで通信(Wi-Fi)はもちろん、付属のディスプレイに漢字を表示させたり、モーターを直接つないで動かすことも簡単に出来てしまいます。
さらに付け加えると、obnizのI/Oピンはプログラマブルであり、かつ1Aまでの電流も流せるため、小型で柔軟性のある実装が可能になっています。
obnizが他のマイコンボードと大きく異なる点は、プログラムを内部に格納しないことです。
これは、接続されているクラウドの中にプログラムが格納されていて、かつ、プログラムもそこで動作していることを意味します。つまり、obnizとクラウドの間ではプログラムの命令と結果のやり取りだけが常に行われています。
非常にユニークな世界観を持ったプロダクトです。
そのような設計を選択したデメリットは常時接続が必須であることなのですが、対するメリットは、IoTシステムに組み込んだ後でもアップデートが簡単だったり、セキュアに運用し易いなど、実は数え切れないほど存在します。これから5Gの世界に急速に移行していくこともあり、もはや通信が途切れることが稀になっていくと思われます。そうすると今後、obnizの存在感は益々大きくなっていきそうです。
###スマホの傾きだけで、車両の動作とモーターの出力をコントロール
本作では、動作の種類(前進/後退/左右移動/超信地旋回)とモーターのパワーを、スマホの傾きと量のセンシングで決めているのですが、絶対的な水平位置を起点とした仕様にすると人の感覚はアバウト過ぎるため、そのとき「スタートボタン」を押した時点での位置を水平の起点とみなしています。
こうすることで、操縦者の感覚に寄り添った自然な操作感を実現することが出来ました。ただしそうは言ってもスマホを傾ける量にはHMI的に限界がありますので、プログラムでは許可できるスタート時の角度を逆算して限度を設けるようにしています。
そして、スマホの傾きだけで全ての動作を実現させるため、傾きの方向と動作を次のようにマッピングしました。
傾き | |||
---|---|---|---|
【左】 | 【なし】 | 【右】 | |
【前】 | 左前進 | 前進 | 右前進 |
【なし】 | 左旋回 | 停止 | 右旋回 |
【後】 | 左後退 | 後退 | 右後退 |
###モーター出力の変化について
傾きの角度(量)に連動する形で、モーターのパワーが0%から100%までリニアに変動します。
ただし、動かしやすい手首の角度には限界がありますので、検証結果を元に程よい範囲を設定することにしました。そこで手首の動作を観察した結果、縦方向(Y軸)の動きに対して横方向(X軸)の動きの方がアバウトな点に着目して、縦と横の可動許容量の比率を1:2に調整しています。
車両が左右方向に移動する時は、横の傾き量によって左右のモーターへの出力バランスを加減するようにしています。ただひとつ例外として、左または右側に振り切った場合のみ超信地旋回が発動します。
###電源分配のしかた
obnizの電源端子から直接、8ピン×2列のピンソケットへ電源を分配しています。
そしてピンソケットの上下列をそれぞれ+と-で区分して利用しています。
obniz表面(obniz左側の"もじゃもじゃ"は、車両本体に固定するためのマジックテープです)
obniz裏面(obnizの裏面には、サーキット保護等のための透明プラバンを張り付けています)
###距離センサー「GP2Y0A21YK0F」:
机上などで遊ぶときの操作ミスでの落下を防ぐために、車両の前後に実装しています。
このセンサーは、筐体がプラスチック製のため軽くて加工がしやすく、サイズが比較的小さく、精度もそれなりに良いためチョイスしました。
ピン配置は次の通りです。
車両前部
端子名 | GP2Y0A21YK0F | obniz |
---|---|---|
Vo | 1 | 4 |
VCC | 3 | 電源+ |
GND | 2 | 電源- |
車両後部
端子名 | GP2Y0A21YK0F | obniz |
---|---|---|
Vo | 1 | 5 |
VCC | 3 | 電源+ |
GND | 2 | 電源- |
車両前部のセンサーは斜めに取り付けることが可能でしたので、若干ですが地面とセンサー間の距離を稼ぐことができていますが、後部のセンサーについては、その距離がなんと1cm少々しかありません。
このセンサーについては「測距レンジは10cm~80cm」「10cmより近い部分は使えない」との情報が多く見受けられたため最初は使うことを躊躇いましたが、実験を繰り返した結果、閾値を設けて二値化するような用途であれば使えそうなことが分かったため導入しました。
数cm以内の計測については、このような大雑把な用途以外には向かないことを予め記しておきます。
ちなみに本プログラムでは、前部の閾値を「100cm」、後部の閾値を「150cm」と、実験の結果導き出した数値を元に個別に設定しています。
###三軸センサー「KXR94-2050」:
車体が裏返されている状態かどうかの判定に使います。
このセンサーではX/Y/Zの三軸の状態を取得できますが、今回はZ軸値の取得に特化してますのでピン配置は次のようになっています。
端子名 | KXR94-2050 | obniz |
---|---|---|
Z | 8 | 6 |
VCC | 1 | 電源+ |
PSD | 2 | 電源+ |
GND | 3 | 電源- |
KXR94-2050の1番と2番はショートさせています。
写真では、車体内部の左下隅についている小さなセンサーがKXR94-2050です。下部に左右のDCモーター、上部にはフロント側の距離センサー「GP2Y0A21YK0F」も見えます。
###MP3プレーヤーモジュール「Grove MP3 v2.0」:
停止時はエンジンのアイドリング音、走行時はキャタピラのノイズが混ざった駆動音と、状態によって再生する音源を切り替えています。
さらに、モーターパワーに正比例してボリュームを変化させています。
これを使う際の注意点として、ハード/ソフトの双方に注意を向ける必要があります。先ずは以下のページを読み込むことを強くお勧めします。
ちなみにハマりポイントは、SDカードの容量・ファイル名ならびにフォルダ名の命名あたりです。
ピン配置は次の通りです。
VCCをobniz本体から取っている理由は、obnizの起動に合わせてMP3モジュールへ電源を投入した方がエラーとなりにくい傾向が見受けられたため、その様にしています。
Grove MP3 | obniz |
---|---|
mp3_tx | 11 |
mp3_rx | 10 |
VCC | 9 |
GND | 電源- |
###ステレオアンプモジュール「KKHMF PAM8406」:
スピーカーが8Ωの場合、約2W+2Wの出力をステレオで鳴らすことができます。
ピン配置は次の通りです。
KKHMF PAM8406 | 接続先 |
---|---|
VCC | 電源+ |
GND | 電源- |
ROUT+ | 右スピーカー +極性 |
ROUT- | 右スピーカー -極性 |
LOUT+ | 左スピーカー +極性 |
LOUT- | 左スピーカー -極性 |
RIN | ステレオミニプラグ 右入力 |
GND | ステレオミニプラグ 接地 |
LIN | ステレオミニプラグ 左入力 |
###スピーカー(2W 8Ω):
小さくて軽く、なおかつそれなりの出力を持っているスピーカーをいくつか試した結果、今回の物に落ち着きました。ただこちらは中国からの取り寄せのため、入手まで時間が掛かりました。
下の写真は、MP3プレーヤーとステレオアンプとスピーカーを接続テストしているところです。
###ソレノイド「5V ZHO-0420S-05A4.5」:
車両側からのフィードバックの装置としてobnizのディスプレイがありますが、本作では車両背面へのレイアウトとしています。
スマホからの車両始動/停止の指令に対するフィードバックを、いちいち背面で確認しては興を削がれるため、obnizからの電源供給で作動可能な小型のソレノイドを使って、音と触覚で表現してみることにしました。
実際に使用してみると「カチッ」という明確なクリック感があり、非常に効果的でした。
ちなみに始動(エンジンスタート)と停止(エンジンストップ)をシングルクリック、車両の裏返りをダブルクリックで表現しています。
###モバイルバッテリー「FingerPow II」:
極小サイズのUSBバッテリーを探している時に見つけたものです。
ラジコンなどでよく用いられるリチウムポリマー電池は取り扱いが難しいため導入に慎重になってしまいますが、このバッテリーであれば何の心配もなくカジュアルに使い倒せます。
現在、こちらのサイトから購入できます。
###タミヤ 1/35 WWIイギリス戦車 マークIVメール(シングルモーターライズ仕様):
本キットはシングルモーターライズ仕様ですので、キャタピラを駆動するためのDCモーターとギアが1組しかセットされていません。前後進だけのコントロールであればこれで充分なのですが、今回は左右の移動も含めたフルオペレーションを実現するため、もう1組のモーターとギアのセットを要しました。
また本キットを用いる場合は、2組目のギアセットを取り付けるためのネジ穴が予め車両側に空いていないため、更に余計な加工も必要となります。これらのことから、本作では最初からラジコンモデル仕様をベースにした方が無難だったと言えるでしょう。
#まとめ
「IoTは総合格闘技」。
最近巷ではそのように表現されています。
確かにIoTに関してはソフトウェアとハードウェアが密に融合しているので、用意されたチュートリアルから一歩外に出て自分で何かを作り始めてみると、この件はすぐに実感できます。
そして今回のような精密模型を用いたIoT作品の場合は、更にひとつレイヤーが増えて、
「ソフトウェア × ハードウェア × 模型製作」
となります。これはもう、異種格闘技戦です。
実際に体験してみればスグに実感できることですが、いまキーボードを打っていたかと思えば、その手で半田ごてを握りしめ、また塗装筆に持ち替える…。
制作の後半は、こんなリーンサイクル(?)をクルクルと回していました。
そして、これらの各レイヤーで使う脳の部位がどうも違うみたいで、それぞれの作業をスイッチする瞬間に毎回ちょっとづつ気力(といいいますか決意)を使いますので、基本的にモノづくりが好きじゃないと続かないな~と、思うこともありました。
でも…。しかしです。
あなたがコーディングが好きで、電子工作が好きで、模型製作が好きであれば、まさに幸せな時間を濃密に堪能できることを保証します。
皆さまにとって、この記事が何かのお役に立てたら嬉しいです。
(ツイッターフォローはこちら)
"Engadget summer festival 2019" 出展
"オープンハードカンファレンス 2019 Tokyo/Fall" 出展
"Beijing-Tianjin-Hebei Maker Summit 2019" 出展
"Tsukuba Mini Maker Faire 2020" 出展
#参考
obniz公式:
現実をソフトウェア化する
obniz パーツライブラリ