ラズベリーパイ(Raspberry Pi Zero WH)でロボットをラジコン操作しよう

Raspberry Pi Zero WHで作ったロボットをブラウザ画面からラジコン操作するイメージ

前回の『ラズベリーパイ(Raspberry Pi Zero WH)でScratch ロボットと遊ぼう!』では、Raspberry Pi Zero WHとScratchを使って、チェーンプログラムロボットを操作しました。

今回は、そのロボットをさらに発展させて、PythonとJavaScriptを使ったブラウザ操作に進みます。

スマホやパソコンのブラウザに操作画面を表示し、円形コントローラーをタッチまたはクリックして、ロボットを前進・後退・左右旋回させます。

さらに、ロボットの前方にカメラを付けて、カメラ映像を見ながら操作できるようにします。

Scratchの画面から一歩進んで、Webページ・JavaScript・Raspberry Pi・カメラ映像がつながるところまで作るので、かなりロボットらしい操作感になります。

目次

今回の制作内容

この記事では、ブラウザ上に作った操作画面からRaspberry Pi Zero WHへ指示を送り、WebIOPiを通してGPIOをPWM制御します。

その信号をモータードライバ TA7291P に送り、左右のモーターを制御してロボットを動かします。

さらに、MJPG-Streamerを使ってRaspberry Piカメラの映像をブラウザに表示します。

ブラウザの円形コントローラーからRaspberry Pi、WebIOPi、モータードライバを通ってロボットが動く流れ

大まかな流れは、次の通りです。

  1. ブラウザに円形コントローラーを表示する
  2. JavaScriptとSVGで、タッチまたはクリックされた操作エリアを判定する
  3. WebIOPiを使って、ブラウザからRaspberry Piへコマンドを送る
  4. Raspberry Pi Zero WHのGPIOからPWM信号を出力する
  5. モータードライバ TA7291Pで左右のモーターを制御する
  6. ロボットを前進・後退・左右旋回させる
  7. MJPG-Streamerでカメラ映像をブラウザに表示する

ただのボタン操作ではなく、ロボットの前方カメラの映像を見ながら操作できるところが、この制作の面白いところです。

ブラウザ、JavaScript、Raspberry Pi、カメラ、モーターが一つにつながるので、電子工作だけでなくWebアプリづくりにも近い内容になります。

準備するもの・配線方法

ロボット本体、モータードライバ、ブレッドボード、モバイルバッテリー、配線方法は、前回のScratchロボットの記事と同じです。

前回のロボットに、今回はRaspberry Piカメラを追加します。

Raspberry Piカメラ

ロボットの前方映像をブラウザに表示するために、Raspberry Pi用のカメラモジュールを使います。

透明のアクリルホルダーが付いているタイプの場合、ロボットの前面にカメラを固定しやすくなります。

ホルダーの組み立ては、こちらのYoutubeの動画で、分かりやすく解説されています。

画像・画質も十分ですし、Raspberry Pi Zero WHに接続するケーブルに加えて、組み立て用のミニドライバーまで付属していました。

コスパ◎で、とってもおすすめです。

カメラを有効にする

まずは、Raspberry Piのカメラ設定を有効にします。

ターミナルで raspi-config を起動します。

$ sudo raspi-config

Interface Options を選択します。

raspi-configでInterface Optionsを選択する画面

Camera を選択します。

raspi-configでCameraを選択する画面

enabled?はい を選びます。

raspi-configでカメラを有効にする画面

設定後、Raspberry Piを再起動します。

sudo reboot

再起動したら、カメラが検出されているか確認します。

vcgencmd get_camera

次のように detected=1 になっていれば、カメラが検出されています。

supported=1 detected=1

この後の手順で、ストリーミングがうまく表示されない場合は、カメラケーブルが抜けていないかを確認します。

試行錯誤している途中に抜けてしまうこともあるため、ためしにこのコマンドでチェックして、「detected=0」になっていないことを確認してください。

操作画面

今回は、ブラウザに次のような操作画面を作ります。

スマホとパソコンで使うロボット操作画面の見方

画面の左側に円形コントローラー、右側にカメラ映像を表示します。

円形コントローラーは、方向と速度をひとつの図形で操作できるようにしたものです。

  • 上:前進
  • 下:後退
  • 左:左旋回
  • 右:右旋回
  • 中央:停止
  • 外側:速い
  • 内側:ゆっくり

スマホではタッチ操作、パソコンではクリック操作でロボットを動かします。

実際に作る操作画面は、左側に円形コントローラー、右側にストリーミング画像を並べた構成です。

ロボットを操作するブラウザ画面の全体

右側のカメラ映像を確認しながら、左側の円形コントローラーでロボットを操作します。

スクロールON ボタンをクリックすると、スクロールON/OFFを切り替えられます。

スマホで操作する場合、アドレスバーを非表示にしたり、表示サイズを調整したりするために、スクロールが必要になる場面があります。

一方で、円形コントローラーをタッチしてロボットを操作する場面では、画面そのものが動かないようにスクロールをOFFにします。

raspi-robot1

操作開始から数秒間は、「前進」「後退」「左旋回」「右旋回」といったガイドの文字列を表示します。

最初だけガイドを表示しておくと、どのエリアを押せばどの方向に動くのか確認しやすくなります。

SVGコントローラーのしくみ

円形コントローラーは、SVGで描画しています。

SVGを使うと、拡大・縮小しても線や図形が荒れにくく、スマホとパソコンの両方に対応しやすくなります。

SVGの扇形を重ねて円形コントローラーを作るしくみ

円形コントローラーは、扇形のエリアを組み合わせて作ります。

前進、後退、左旋回、右旋回の4方向に分け、それぞれの方向に3段階のエリアを用意しました。

  • 内側:ゆっくり
  • 中央:ふつう
  • 外側:はやい

このようにしておくと、ボタンをいくつも並べなくても、ひとつの円形コントローラーで方向と速度をまとめて指定できます。

SVGの基本

SVGは Scalable Vector Graphics の略です。

JPEGやPNGのような点の集まりではなく、座標や線の情報で図形を描画する形式です。

そのため、拡大・縮小してもギザギザになりにくく、レスポンシブな操作画面を作りやすくなります。

たとえば、円や四角は次のように記述できます。

SVGのコマンド

例えば、円や四角を描画する場合、このようにSVGで記述します。

// 円を描画
<circle id="maru" cx = "100" cy = "100" r = "50" stroke = "black" fill = "#fff" stroke-width = "2" />

// 四角を描画
<rect id="shikaku" x="0" y="0" width="100" height="100" stroke="black" fill="#fff" stroke-width="1" />

描いた図形に id を付けておくと、JavaScriptからクリック処理などを設定できます。

$("#shikaku").click(function() {
  // ここに処理を書く
});

SVGでコントローラー作成する

このSVGで円のコントローラーを描いていきます。

このコントローラーは扇形を組み合わせて作っています。

例えば「前進」の部分は、3つの大きさの扇形を重ねて、3段階の速さを選択できるようにしました。

fwd

SVGで扇形を描画する

SVGで扇形を描く場合は、「パス描画」というコマンドで記述します。

M150 330 L44 224 A150 150 0 0 1 256 224 Z
SVGのpathコマンドで扇形を描く説明図

コマンドの意味を解説します。
① M(Move to) x y : 円の中心点の座標(150,330)移動します
② L(Line to) x y :座標(44,224)まで線を引きます
③ A(arc) rx ry x-axis-rotation large-arc-flag sweep-flag x y:指定したパラメータで円弧を描きます。
rx:x軸の半径 (150)
ry:y軸の半径 (150)
x-axis-rotation:円弧の回転度 (0)
large-arc-flag:中心角が180度以上なら1、未満なら0 (0)
sweep-flag:時計回りで円弧を描く場合1、反時計回りなら0 (1)
x y: 円弧を描き終える座標(256,224)
④ Z(Close path):最初の座標(150,330)まで線を引いて閉じます

ここでは、扇形をただの画像として置くのではなく、SVGの図形として描いているため、クリックされた図形の id をJavaScriptで取得し、その id に応じてロボットの動きを変えられます。

扇形の座標を計算する

扇形を描くには、円弧の始点と終点の座標が必要です。

その座標は、円の中心、半径、角度から三角関数で計算します。

円弧の始点と終点を三角関数で計算する説明図

ここでは、次の式で座標を求めています。

x’ = cx + r * sin θ
y’ = cy – r * cos θ

JavaScriptでは、Math.sin()Math.cos() を使います。

ただし、JavaScriptの三角関数では角度をラジアンで指定するため、度数法から弧度法に変換して計算します。

(45°・60°といった度数法に、(π/180)を掛けて弧度法に変換します。)

度数法を弧度法に変換して三角関数で座標を求める説明図

SVGコマンドを取得するJavaScript

扇形の座標計算ができたので、前進・後退・左旋回・右旋回の各3サイズ分のSVGパスを作ります。

コードを見る
// 扇形のPathを取得(円の中心x, y, 半径r, 開始中心角(度数法), 終了中心角) 
var path = getPath(150, 330, 150, 315,  45) // 前進
         + getPath(150, 330, 110, 315,  45)
         + getPath(150, 330,  70, 315,  45)
         + getPath(150, 330, 150,  45, 135) // 右旋回
         + getPath(150, 330, 110,  45, 135)
         + getPath(150, 330,  70,  45, 135)
         + getPath(150, 330, 150, 135, 225) // 後退
         + getPath(150, 330, 110, 135, 225)
         + getPath(150, 330,  70, 135, 225)
         + getPath(150, 330, 150, 225, 315) // 左旋回
         + getPath(150, 330, 110, 225, 315)
         + getPath(150, 330,  70, 225, 315);

function getPath(cx, cy, r, startDegree, finishDegree) {

  // 円弧始まりの座標を算出(小数点以下四捨五入)
  var startX = Math.round(cx + r * Math.sin(startDegree / 180 * Math.PI));
  var startY = Math.round(cy - r * Math.cos(startDegree / 180 * Math.PI));

  // 円弧終わりの座標を算出
  var finishX = Math.round(cx + r * Math.sin(finishDegree / 180 * Math.PI));
  var finishY = Math.round(cy - r * Math.cos(finishDegree / 180 * Math.PI));

  // 扇形の中心角は180度以上?
  var largeArcFlag = (finishDegree - startDegree >= 180) ? 1 : 0;


  // cx, cyに移動
  var path = 'M' + cx + ' ' + cy + ' '

  // startX, startYまで線を引く
  + 'L' + startX + ' ' + startY + ' ' 

  // 円弧を描く  
  + 'A' + r + ' ' + r + ' ' + 0 + ' ' + largeArcFlag 
  + ' ' + 1 + ' ' +  finishX + ' ' + finishY+ ' '

  // 最初のcx,cy座標まで線を引く
  + 'Z<br>';

  return path;

}

このプログラムで取得した扇形を描くSVGコマンドをindex.htmlに埋め込んでいきます。

扇形を描くのにこちらのサイトを参考にさせていただきました。ありがとうございます。
SVGで円グラフを描く
三角関数と弧度法についてはこちらのサイトで分かりやすく解説されています。
三角関数の基礎知識。sinθ cosθ tanθ の覚え方・弧度法・三角比の表まとめ

MJPG-Streamer

次に、ロボット前方のカメラ映像をブラウザに表示します。

今回は、Raspberry Piでよく使われているライブカメラ用ソフト MJPG-Streamer を使います。

MJPG-Streamerのインストール

ネット上にも、いろいろな方法が紹介されていますが、うまく起動してくれなかったり、思うようにPathが通せなかったりして、はまってしまいました。

結局、こちらのラズベリーパイのバイブル『Raspberry Piで学ぶ電子工作』に載ってる方法でインストールしたら、すんなり起動しました。やっぱりすごいです。

こちらの本は、ほんとにすばらしいです。初心者の人にもやさしく基礎から解説されていて、ラズベリーパイをさわる方は最初に読んだ方がいいと思います。

まず、必要なパッケージを入れてから、MJPG-Streamerのソースを取得してビルドします。

$ sudo apt-get update
$ sudo apt-get install libjpeg8-dev cmake
$ git clone https://github.com/jacksonliam/mjpg-streamer.git
$ cd mjpg-streamer/mjpg-streamer-experimental
$ make
$ cd
$ sudo mv mjpg-streamer/mjpg-streamer-experimental /opt/mjpg-streamer

ここで行っているのは、カメラ映像をブラウザから見られる形で配信する準備です。

git clone でソースコードを取得し、make でビルドします。そのあと /opt/mjpg-streamer に移しておくと、起動コマンドのパスを決めておけるので扱いやすくなります。

MJPG-Streamerの起動

本の中では自動起動させるシェルが紹介されていますが、忘れてしまいそうなので少し変えた次のコマンドを手動で起動させています。

$ LD_LIBRARY_PATH=/opt/mjpg-streamer/ /opt/mjpg-streamer/mjpg_streamer -i "input_raspicam.so -fps 15 -q 50" -o "output_http.so -p 9000 -w /opt/mjpg-streamer/www"

オプションは、デフォルトから変更したいものだけを指定します。

-r解像度 (def.640×480)
-fフレームレート(def.5)
-qJPEGのクオリティ(1-100)
-pポート(def.8080)
-wwwwフォルダのパス

MJPG-Streamerの動作確認

ブラウザで次のURLにアクセスして、MJPG-Streamerの画面が表示されれば成功です。

http://raspberrypi.local:9000/
MJPG-Streamerの動作確認画面

ストリーミング画像は、次のようなURLで取得できます。

http://raspberrypi.local:9000/?action=stream

このURLを index.html 側で指定すると、操作画面の中にカメラ映像(モーションJPEG形式の動画)を表示できます。

Motion-JPEGとは、画像の圧縮方式であるJPEGを応用して動画データを生成する圧縮・記録方式のことである。圧縮されたJPEG画像を各コマに配置して、アニメーションのように再生する。

引用元:IT用語辞典バイナリ

プログラムとソフトの役割

ここまでで、操作画面、WebIOPi、Python、MJPG-Streamerが出てきました。

それぞれの役割を整理すると、次のようになります。

index.html、WebIOPi、script.py、MJPG-Streamerがブラウザ操作とカメラ映像をつなぐ流れ
名前役割
index.html操作画面を表示する。SVGコントローラー、JavaScript、カメラ映像表示をまとめる
JavaScriptタッチ・クリックされた場所を判定し、WebIOPiへ指示を送る
WebIOPiブラウザからRaspberry PiのGPIO/PWMを操作する
script.pyGPIOをPWMに設定し、モーター制御の準備をする
MJPG-StreamerRaspberry Piカメラの映像をブラウザへ配信する

ここで混乱しやすいのは、ロボットを動かす流れと、カメラ映像を表示する流れが別になっているところです。

操作の流れは、ブラウザからロボットへ向かいます。

一方、映像の流れは、ロボット前方カメラからブラウザへ戻ってきます。

この2つを同じ画面にまとめることで、スマホやパソコンからロボットの視界を見ながら操作できるようになります。

プログラム

ラズベリーパイ(Raspberry Pi Zero WH)をスマホから操作する方法(WebIOPi)」と同じ要領でプログラムを作っていきます。

WebIOPiをベースに、SVGで作った円形コントローラーと、MJPG-Streamerのストリーミング映像を組み合わせます。

/home/pi/webiopi/test/ に、次の2つのファイルを保存します。

  • index.html
  • script.py

index.html

プログラムの目的

index.html は、ブラウザに操作画面を表示するためのファイルです。

パソコンからはクリックで、スマホからはタッチ操作で動かすようにしました。

この中で、次の処理をまとめています。

  • SVGで円形コントローラーを表示する
  • JavaScriptでタッチ・クリック位置を取得する
  • 押されたエリアに応じてWebIOPiへPWM値を送る
  • スマホ操作用にスクロールON/OFFを切り替える
  • MJPG-Streamerのカメラ映像を画面に表示する

処理の流れ

index.html の主な流れは、次の通りです。

  1. WebIOPiの準備ができたら、GPIO番号や操作表示エリアを用意する
  2. タッチまたはクリックされた座標を取得する
  3. document.elementFromPoint() で、押されたSVGパーツの id を取得する
  4. id に応じて、前進・後退・左旋回・右旋回・停止のPWM値を決める
  5. webiopi().pulseRatio() でGPIOへPWM値を送る
  6. カメラ映像のURLを設定して、画面に表示する

index.html のコード

index.html のコードを見る
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Raspberry Pi | Robot</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript" src="/webiopi.js"></script>
<script type="text/javascript">
 
// WebIOPiの準備が整ったときに呼び出す関数
webiopi().ready(function() {
 
  // モータードライバーに接続したGPIO端子番号
  var rFwdGpio  = 17;
  var rBackGpio = 27;
  var lFwdGpio  = 2;
  var lBackGpio = 3;
 
  // 操作を表示するエリア
  var txtOpe = $('#txt_ope').get(0);
  
  // タッチ可の端末ではtrue
  var isTouch = ('ontouchstart' in window);
 
  // 前回の操作を保存
  var prevOpe = '';
 
  // ロボットを操作する関数 (右前,右後,左前,左後,操作)
  function robot_move( rf, rb, lf, lb, ope ){
 
    // モータードライバーにパルスレートを設定
    webiopi().pulseRatio(rFwdGpio,  rf);
    webiopi().pulseRatio(rBackGpio, rb);
    webiopi().pulseRatio(lFwdGpio,  lf);
    webiopi().pulseRatio(lBackGpio, lb);
 
    // 操作を表示
    txtOpe.textContent = ope;
  }
 
  // タッチorクリックイベントを受け取って、ロボット操作関数を呼ぶ
  $(document).bind('click touchstart touchmove', function(e){
 
    // タッチorクリックされたXY座標を取得
    var x = (isTouch ? e.originalEvent.changedTouches[0].pageX : event.pageX);
    var y = (isTouch ? e.originalEvent.changedTouches[0].pageY : event.pageY);
 
    // XY座標から操作のIDを取得
    var elemId = (document.elementFromPoint(x, y)).id;
 
    // 前回の操作と違う操作なら
    if(prevOpe != elemId) {
 
      // 今回の操作を保存
      prevOpe = elemId;
 
      // 操作に応じたパラメータで、ロボット操作関数をよぶ
      switch(elemId){
        case 'fwd1'  : robot_move( 0.5,   0, 0.5,   0, "前進1"   ); break;
        case 'fwd2'  : robot_move( 0.7,   0, 0.7,   0, "前進2"   ); break;
        case 'fwd3'  : robot_move( 0.9,   0, 0.9,   0, "前進3"   ); break;
        case 'back1' : robot_move(   0, 0.5,   0, 0.5, "後退1"   ); break;
        case 'back2' : robot_move(   0, 0.7,   0, 0.7, "後退2"   ); break;
        case 'back3' : robot_move(   0, 0.9,   0, 0.9, "後退3"   ); break;
        case 'right1': robot_move(   0,   0, 0.5,   0, "右旋回1" ); break;
        case 'right2': robot_move(   0,   0, 0.7,   0, "右旋回2" ); break;
        case 'right3': robot_move(   0,   0, 0.9,   0, "右旋回3" ); break;
        case 'left1' : robot_move( 0.5,   0,   0,   0, "左旋回1" ); break;
        case 'left2' : robot_move( 0.7,   0,   0,   0, "左旋回2" ); break;
        case 'left3' : robot_move( 0.9,   0,   0,   0, "左旋回3" ); break;
        default      : robot_move(   0,   0,   0,   0, "ストップ"); break;
      }
    }  
  });
 
  // タッチ後、画面から指を離したらストップ
  $(document).bind('touchend', function(e){
    robot_move( 0, 0, 0, 0, "ストップ" );
  }); 
 
});
</script>
</head>
<body>
<!-- SVGをタグで埋め込む -->
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     viewBox="0, 0, 1040, 480" >
 
  <!-- スクロールON・OFFボタン -->
  <rect x="2" y="60" width="130" height="50" stroke="grey" stroke-width="2" fill="white" />
  <text id="txt_scroll" x="10px" y="90px">スクロールON</text>
 
  <!-- 操作内容 -->
  <text id="txt_ope"  x="10px" y="150px" ></text>
 
  <!-- コントローラー -->
  <!-- 前進 -->
  <path id="fwd3" d="M150 330 L44 224 A150 150 0 0 1 256 224 Z" fill="#ffdbff" />
  <path id="fwd2" d="M150 330 L72 252 A110 110 0 0 1 228 252 Z" fill="#ffe5ff" />
  <path id="fwd1" d="M150 330 L101 281 A70 70 0 0 1 199 281 Z"  fill="#ffefff" />
 
  <!-- 右旋回 -->
  <path id="right3" d="M150 330 L256 224 A150 150 0 0 1 256 436 Z" fill="#eddbff" />
  <path id="right2" d="M150 330 L228 252 A110 110 0 0 1 228 408 Z" fill="#f2e5ff" />
  <path id="right1" d="M150 330 L199 281 A70 70 0 0 1 199 379 Z"   fill="#f7efff" />
 
  <!-- 後退 -->
  <path id="back3" d="M150 330 L256 436 A150 150 0 0 1 44 436 Z" fill="#ffdbff" />
  <path id="back2" d="M150 330 L228 408 A110 110 0 0 1 72 408 Z" fill="#ffe5ff" />
  <path id="back1" d="M150 330 L199 379 A70 70 0 0 1 101 379 Z"  fill="#ffefff" />
 
  <!-- 左旋回 -->
  <path id="left3" d="M150 330 L44 436 A150 150 0 0 1 44 224 Z" fill="#eddbff" />
  <path id="left2" d="M150 330 L72 408 A110 110 0 0 1 72 252 Z" fill="#f2e5ff" />
  <path id="left1" d="M150 330 L101 379 A70 70 0 0 1 101 281 Z" fill="#f7efff" />
 
  <!-- 中心の円 -->
  <circle id="stop" cx="150" cy="330" r="30" fill="white" />
 
  <!-- コントローラーのガイド -->
  <text id="txt_fwd" x="135px" y="200px" >前進</text>
  <text id="txt_rgt" x="250px" y="350px" >右旋回</text>
  <text id="txt_bck" x="135px" y="470px" >後退</text>
  <text id="txt_lft" x="3px"   y="350px" >左旋回</text>
 
  <!-- ストリーミング画像を表示するエリア -->
  <image id="svg_img" x="320px" xlink:href="" />
</svg>
 
<script>
 
// スクロールのON・OFFを切り替え
var txt_scl = $('#txt_scroll').get(0);
 
// スクロールON・OFFフラグ
var flag = false;
 
// スクロールをキャンセル
var chgScl = function( e ){
  // デフォルトの処理をキャンセル
  e.preventDefault();
}
 
// スクロールのON・OFFを切り替える関数
function changeScroll(){
  if(flag) {
    //スクロール復帰(スクロールキャンセル関数をイベントリスナーから削除)
    document.removeEventListener('touchmove', chgScl, { passive: false });
    txt_scl.textContent = 'スクロールON';
  } else {
    //スクロール禁止(スクロールキャンセル関数をイベントリスナーに登録)
    document.addEventListener('touchmove', chgScl, { passive: false });
    txt_scl.textContent = 'スクロールOFF';
  }
  // スクロールフラグを切り替え
  flag = !flag;  
}
 
// スクロールON・OFFの文字列をクリックして切り替え
$("#txt_scroll").click(changeScroll);
 
 
// MJPG-streamerのStreamを設定(スマホ用にホスト名を動的に設定) 
var element = document.getElementById('svg_img');
element.setAttribute('xlink:href',
  'http://'+location.hostname+':9000/?action=stream');
 
// 表示した3秒後にコントローラーのガイドを消去
$(function(){
  var fTxt = $('#txt_fwd').get(0);
  var rTxt = $('#txt_rgt').get(0);
  var bTxt = $('#txt_bck').get(0);
  var lTxt = $('#txt_lft').get(0);
 
  setTimeout(function(){
    fTxt.textContent = "";
    rTxt.textContent = "";
    bTxt.textContent = "";
    lTxt.textContent = "";
  },3000);
});
 
</script> 
</body>
</html>

JavascriptやCSS(スタイルシート)などは、別のファイルにした方がいいのですが、ここでは内容を追いやすいように1つのファイルにまとめています。

コードのポイント

index.html では、SVGで描いた扇形それぞれに id を付けています。

たとえば、前進のエリアには fwd1fwd2fwd3 のようなIDを付け、内側・中央・外側で速度を変えています。

JavaScript側では、押されたエリアのIDを取得し、switch 文で操作内容を分けています。

case 'fwd1'  : robot_move( 0.5,   0, 0.5,   0, "前進1"   ); break;
case 'fwd2'  : robot_move( 0.7,   0, 0.7,   0, "前進2"   ); break;
case 'fwd3'  : robot_move( 0.9,   0, 0.9,   0, "前進3"   ); break;

このように、同じ前進でもPWM値を変えることで、3段階の速度を表現しています。

また、スマホで指を離したら停止するように、touchend で停止処理も入れています。

$(document).bind('touchend', function(e){
  robot_move( 0, 0, 0, 0, "ストップ" );
});

スマホで操作するときに重要なのが、スクロールのON/OFFです。

画面サイズを調整したいときはスクロールON、ロボットを操作するときはスクロールOFFにして、タッチ操作で画面が動かないようにしています。

script.py

プログラムの目的

script.py は、WebIOPi側でGPIOをPWM出力として使えるようにするためのPythonファイルです。

左右のモーターの前進・後退に対応するGPIOをPWMに設定し、初期値を0にしておきます。

処理の流れ

script.py の主な流れは、次の通りです。

  1. WebIOPiのGPIO機能を使えるようにする
  2. モータードライバにつないだGPIO番号を定義する
  3. setup() で各GPIOをPWMに設定する
  4. 初期値としてPWM出力を0にする
  5. destroy() で終了時にGPIOを入力状態へ戻す

script.py のコード

script.pyを見る
import webiopi
 
GPIO = webiopi.GPIO
 
R_FWD  = 2 
R_BACK = 3
L_FWD  = 27
L_BACK = 17

 
def setup():
    # GPIOをPWMに設定
    GPIO.setFunction(R_FWD,  GPIO.PWM)
    GPIO.setFunction(R_BACK, GPIO.PWM)
    GPIO.setFunction(L_FWD,  GPIO.PWM)
    GPIO.setFunction(L_BACK, GPIO.PWM)

    # 初期化(デューティー比0)
    GPIO.pwmWrite(R_FWD,  0)
    GPIO.pwmWrite(R_BACK, 0)
    GPIO.pwmWrite(L_FWD,  0)
    GPIO.pwmWrite(L_BACK, 0)

# WebIOPiにより繰り返される関数
def loop():
    webiopi.sleep(5)

# WebIOPi終了時に呼ばれる関数
def destroy():
   # GPIO関数のリセット
    GPIO.setFunction(R_FWD,  GPIO.IN)
    GPIO.setFunction(R_BACK, GPIO.IN)
    GPIO.setFunction(L_FWD,  GPIO.IN)
    GPIO.setFunction(L_BACK, GPIO.IN)

コードのポイント

script.py では、次の4つのGPIOを使っています。

R_FWD  = 2
R_BACK = 3
L_FWD  = 27
L_BACK = 17

この4つの信号で、左右のモーターの回転方向を制御します。

setup() では、各GPIOをPWM出力に設定します。

GPIO.setFunction(R_FWD,  GPIO.PWM)
GPIO.setFunction(R_BACK, GPIO.PWM)
GPIO.setFunction(L_FWD,  GPIO.PWM)
GPIO.setFunction(L_BACK, GPIO.PWM)

PWMを使うことで、単純なON/OFFだけでなく、モーターの回転の強さも変えられます。

この仕組みを使って、円形コントローラーの内側・中央・外側で速度を変えています。

動作確認

WebIOPiとMJPG-Streamerを起動し、パソコンやスマホのブラウザからアクセスして動作を確認します。

$ sudo systemctl start webiopi

$ LD_LIBRARY_PATH=/opt/mjpg-streamer/ /opt/mjpg-streamer/mjpg_streamer -i "input_raspicam.so -fps 15 -q 50" -o "output_http.so -p 9000 -w /opt/mjpg-streamer/www"

パソコンからアクセス

パソコンのブラウザから、次のURLにアクセスします。

http://raspberrypi.local:8000/test/

操作画面が表示されればOKです。

パソコンのブラウザに表示したロボット操作画面

スマホからアクセス

スマホからアクセスする場合は、raspberrypi.local で名前解決できないことがあるため、Raspberry PiのIPアドレスを調べてアクセスします。

ip addr show

たとえば、Raspberry PiのIPアドレスが 192.168.0.xx の場合は、次のようにアクセスします。

http://192.168.0.xx:8000/test/

スマホの画面に円形コントローラーとカメラ映像が表示されたら、タッチ操作でロボットを動かします。

ラジコン操作している動画

スマホでラジコン操作している動画です。

ロボットの前方カメラの映像を確認しながら、円形コントローラーで操作しています。

まとめ

WebIOPi、MJPG-Streamer、SVGを組み合わせて、ブラウザからロボットをラジコン操作できるようにしました。

今回の制作では、ブラウザで操作した内容がRaspberry Piに送られ、GPIOのPWM信号として出力されます。

その信号をモータードライバが受け取り、左右のモーターを制御してロボットが動きます。

さらに、ロボット前方のカメラ映像をブラウザに表示することで、ロボットの視点を見ながら操作できます。

画面の中で作ったUIが、目の前のロボットの動きにつながる。ここまでできると、Raspberry Piの電子工作というより、かなりWebアプリに近い手応えがあります。

スマホの画面からロボットを動かし、カメラ映像を見ながら進められるので、完成したときの楽しさもかなり大きいです。

このロボットはまだ操作が難しいところもありますが、コースを作ったり、カメラ映像を改善したり、操作画面を作り込んだりと、いろいろ拡張できそうです。

次は、このロボット制作で使った考え方をもとに、Web画面からものを動かす仕組みをさらに広げていきます。

関連するラズベリーパイ記事や制作記録はこちらです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次