🚗

ラズパイで作ったラジコンをスマホで操作する

2022/06/20に公開

ラズパイで作ったラジコンをスマホで操作する

ラジコンの制御する仕組みを考えたので忘れないようにここに残します。
ラジコン制御の仕組みは、ソケット通信やROS (Robot Operating System)を使う方法があるようですが、ここではAPIを使った方法を紹介します。

動作デモ

まずは作ったもののデモを紹介します。

デモ①

https://youtu.be/2kKqzkS6_nA

向かって左の緑の画面はスマホの画面です。スマホの画面にタップした位置(始点)からドラッグした位置(終点)まで赤線が描画されます。赤線の長さや角度によって、ラズパイに接続した2つのサーボモーターが回転します。2つのサーボモーターの回転スピードや回転方向は個別に指定しています。

デモ②

https://youtu.be/T1CMn3n5XcU

さきほどのデモ①をラジコンにしての動作テストです。スマホがラジコンのコントローラーになっているのがわかると思います。

構成と仕組み概要

全体の構成図と処理フローの概要です。

機器と役割

  • ラズパイ Raspberry Pi 3 Model B
    • 役割
      • サーボモーターの制御
      • API ApiMotorControle.py
    • 環境
      • Python 3.10.4
      • fastapi 0.78.0
      • uvicorn 0.17.6
      • gpiozero 1.6.2
      • pigpio 1.78
  • サーボモーター
    • 役割
      • ラズパイからの信号を受けて回転/停止
    • ※360度連続回転のサーボモーターを使用しています
  • Windows11
    • 役割
      • Webサーバー
      • ラジコンのコントローラーとなるWebサイト Motor-con.html を提供
    • 環境
      • Python 3.10.5
  • スマホ
    • ラジコンのコントローラーとなるWebサイト Motor-con.html を開く

ラズパイとサーボモーターの配線

配線図を紹介します。わかりやすくするため、サーボモーターごとに配線図を用意しました。

黄線のGPIO番号は、後でソースを紹介するスクリプト ApiMotorControle.py の変数 pinNo1 , pinNo2 に該当します。配線図の通りに接続した場合、変数 pinNo1 , pinNo2の設定は以下になります。

pinNo1 = 2
pinNo2 = 3

処理フローの概要

処理フローの概要です。このような順序でモーターを制御します。

①スマホでブラウザを開く → コントローラーとなるWebサイトが開く
②コントローラーを操作する
③コントローラーを操作したことでAPIがコールされる
④ラズパイがAPIを受信する → 受信したパラメータを取得する
⑤ラズパイが取得したパラメータを元にモーターを制御する

コントローラーについて

コントローラーはスマホの画面に指をなぞることで操作します。

サーボモーターへの出力について

前述のとおり、スマホの画面にタップした位置(始点 A)からドラッグした位置(終点 B)まで赤線が描画されます。赤線の距離 POW がサーボモーターに与える出力の大きさです。距離 POW が大きいほど出力は高くなります。

各サーボモーターへの出力の割合と回転方向について

赤線の角度によって2つのサーボモーターの回転方向や各モーターに与える出力の大きさが変わります。それにより、赤線の角度とその角度によりラジコンがどのように動くか変わります。

スクリプト

Windows11環境にあるコントローラーとなるWebサイトのスクリプトです。変数 urlHead にラズパイのIPアドレスを修正します。

修正箇所
// http://{{ラズパイのIPアドレス}}:{{ポート番号}}
let urlHead = "http://192.168.32.200:8000"; //ラズパイAPIのURL
Motor-con.html
<html>
<head>
<style>
#CanvasArea { position: absolute; z-index: 2; }
html, body, #container { width: 100%; height: 100%; background-color: #1de75a;}
</style>
</head>
<body ontouchmove="event.preventDefault()" onload="createImageLayer();">
<div id="container">
    <canvas id="CanvasArea"></canvas>
</div>

<script>
let urlHead = "http://192.168.32.200:8000"; //ラズパイAPIのURL

let el_canvas = document.getElementById('CanvasArea');
el_canvas.width  = container.offsetWidth;
el_canvas.height = container.offsetHeight;
let x0 = 0;
let y0 = 0;
let div_width = el_canvas.clientWidth;
let div_height= el_canvas.clientHeight;
let div_x = el_canvas.offsetLeft + div_width/3;
let div_y = el_canvas.offsetTop  + div_height/4;

let updateXY = function(event) {
    let x1 = event.changedTouches[0].pageX;
    let y1 = event.changedTouches[0].pageY;
    showController("CanvasArea", x1, y1);
};

el_canvas.addEventListener('touchmove', function(event) {
    updateXY(event);
}, false);

el_canvas.addEventListener('touchstart', function(event) {
    x0 = event.changedTouches[0].pageX;
    y0 = event.changedTouches[0].pageY;
}, false);

el_canvas.addEventListener('touchend', function(event) {
    //画面から指をはなしたときにサーボモーターを停止する
    setCanvasClear("CanvasArea");
    urlVal = urlHead + "/stop/0/0";
    fetch( urlVal );
}, false);

function showController(argElement, argX, argY){
    setCanvasClear(argElement);
    setLine(argElement, argX, argY);
    setArc(argElement, x0, y0, 10, "rgba(100,100,100,1.0)");
    setArc(argElement, argX, argY, 20, "rgba(255,0,0,1.0)");

    let rtnArr = new Array();
    let mSin = 0;
    let mCos = 0;
    let mpow  = Math.round( 0.5 * Math.sqrt( Math.pow( x0-argX, 2 ) + Math.pow( y0-argY, 2 ) ) ) ; // 2点間の距離
    let angle = ( Math.atan2( y0 - argY, x0 - argX ) * 180 / 3.1415 ).toFixed(0);
    let x3 = x0;
    let y3 = y0 - 10;
    let sVal = cVal = 0;
    let csVal = 0;
    let sPow = cPow = 0;
    let VMAX = 300; // 最大値
    let urlVal;     // APIのURL

    rtnArr.length = 0; //初期化

    if( angle <= -157.5 ){       sVal=  1; cVal= 1; // 右回転
    }else if( angle <= -112.5 ){ sVal=  0.5; cVal= -1; // 右後ろ旋回
    }else if( angle <=  -67.5 ){ sVal=  1; cVal= -1; // 後方
    }else if( angle <=  -22.5 ){ sVal=  1; cVal= -0.5; // 左後ろ旋回
    }else if( angle <=   22.5 ){ sVal=  -1; cVal= -1; // 左回転
    }else if( angle <=   67.5 ){ sVal=  -1; cVal= 0.5; // 左旋回
    }else if( angle <=  112.5 ){ sVal=  -1; cVal= 1; // 前進
    }else if( angle <=  157.5 ){ sVal=  -0.5; cVal= 1; // 右旋回
    }else{                       sVal=  1; cVal= 1; // 右回転
    }

    sPow = Math.round( sVal *mpow /VMAX *100 )/100;
    cPow = Math.round( cVal *mpow /VMAX *100 )/100;

    setChar(argElement, x3, y3, "POW: " + mpow);
    y3 = y0 - 40;
    setChar(argElement, x3, y3, "Angle: " + angle);
    y3 = y0 - 80;
    setChar(argElement, x3, y3, "sVal: " + sVal);
    y3 = y0 - 120;
    setChar(argElement, x3, y3, "cVal:" + cVal);

    //ベクトル最大値超過対応
    if(sPow  >=  1.0) sPow = 1.0;
    if(cPow  >=  1.0) cPow = 1.0;

    rtnArr.push(mpow);
    rtnArr.push(angle);

    urlVal = urlHead + "/move/" + sPow + "/" + cPow;
    fetch( urlVal );
    return rtnArr ;
}

function setChar(argElement, argX, argY, argString){
    let element = document.getElementById(argElement) ;
    let context = element.getContext( "2d" ) ;
    context.font = '20pt Arial';
    context.fillText(argString, argX, argY);
}

function setLine(argElement, argX, argY){
    let element = document.getElementById(argElement) ;
    let context = element.getContext( "2d" ) ;
    context.moveTo(x0,y0);
    context.lineTo(argX,argY);
    context.stroke();
}

function setCanvasClear(argElement){
    let element = document.getElementById(argElement) ;
    let context = element.getContext( "2d" ) ;
    context.clearRect(0, 0, element.width, element.height); // canvas 初期化
}

function setArc( argElement, argX, argY, argRadius, argColor)
{
    let element = document.getElementById(argElement) ;
    let context = element.getContext( "2d" ) ;
    context.beginPath () ; // パスをリセット
    context.arc( argX, argY, argRadius, 0 * Math.PI / 180, 360 * Math.PI / 180, false ) ;
        // 円の中心座標: (100,100)
        // 半径: 50
        // 開始角度: 0度 (0 * Math.PI / 180)
        // 終了角度: 360度 (360 * Math.PI / 180)
        // 方向: true=反時計回りの円、false=時計回りの円
    context.fillStyle = argColor; // 塗りつぶしの色
    context.fill() ; // 塗りつぶしを実行
    context.strokeStyle = "purple" ; // 線の色
    context.lineWidth = 4 ; // 線の太さ
    context.stroke() ; // 線を描画を実行
}
</script>
</body>
</html>

ラズパイ環境にあるAPIを作るスクリプトです。変数 pinNo1, pinNo2 には各サーボモータのGPIOを記載します

ApiMotorControle.py
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware # CORS
from gpiozero import Servo
from gpiozero.pins.pigpio import PiGPIOFactory

pinNo1 = 2
pinNo2 = 3
factory = PiGPIOFactory()
servo1 = Servo(pinNo1, min_pulse_width=0.5/1000, max_pulse_width=2.5/1000, pin_factory=factory)
servo2 = Servo(pinNo2, min_pulse_width=0.5/1000, max_pulse_width=2.5/1000, pin_factory=factory)

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)

@app.get("/{comFlg}/{powerVal1}/{powerVal2}")
async def mcontrole(comFlg: str, powerVal1: float, powerVal2: float):
    controleMoter(comFlg, powerVal1, powerVal2)
    return {"command" : comFlg}

def controleMoter(comFlg, powerVal1, powerVal2):
    if comFlg == "move":
        servo1.value = float(powerVal1)
        servo2.value = float(powerVal2)
    else:
        servo1.mid()
        servo2.mid()

環境の準備

一部のセットアップや参考情報を記載します。

Windows11の環境準備

簡易のWebサーバーを用意するための環境準備が必要です。今回はPythonを使いました。

ラズパイの環境準備

Pythonに fastapi などのインストールが必要です。細かいことは省きますが以下3つをインストールします。

PS C:\> pip install fastapi uvicorn
PS C:\> pip install gpiozero
PS C:\> pip install pigpio

コマンドsudo pigpiod実行で pigpio デーモンを起動します。
Raspberry Pi OS(systemd)の場合、OS起動時に pigpioデーモン を自動実行したければ、コマンド systemctl enable pigpiod を実行します。

環境の起動方法

APIの起動

ラズパイで以下のコマンドを実行しAPIを起動します。(rpiMotorControle.py があるディレクトリをカレントディレクトリにして実行します)

$ uvicorn rpiMotorControle:app --host 0.0.0.0 --reload

※注意※ このコマンドはpigpioデーモンが起動している状態で実行します。

Webサーバーの起動

PowerShellを起動し、motor-con.html があるディレクトをカレントディレクトリにし、以下を実行し簡易のWebサーバーを起動します。

PS C:\Users\hoge\test>python -m http.server 8000

※簡易のWebサーバーを起動した後、Windows11のIPが 192.168.32.100 の場合、URL http://192.168.32.100:8000/motor-con.html を開くとコントローラーのWebサイトが開きます。

使用方法

スマホでコントローラーのWebサイト(*1)を開き、緑色の画面が表示されれば操作できます。

  • (*1)
    • http://{{Windows11のIPアドレス}}:{{ポート番号}}/Motor-con.html
    • 例)http://192.168.32.100:8000/Motor-con.html

今回はこれでおわりです。


次はラジコンにカメラをのせてラジコン目線で操作できるようにします

次回は(たぶん)カメラ視点の映像を見ながら操作するラジコンの作りかたを紹介します。

https://youtu.be/k6dsYhLZa_g

Discussion