ラズパイで作ったラジコンをスマホで操作する
ラズパイで作ったラジコンをスマホで操作する
ラジコンの制御する仕組みを考えたので忘れないようにここに残します。
ラジコン制御の仕組みは、ソケット通信やROS (Robot Operating System)を使う方法があるようですが、ここではAPIを使った方法を紹介します。
動作デモ
まずは作ったもののデモを紹介します。
デモ①
向かって左の緑の画面はスマホの画面です。スマホの画面にタップした位置(始点)からドラッグした位置(終点)まで赤線が描画されます。赤線の長さや角度によって、ラズパイに接続した2つのサーボモーターが回転します。2つのサーボモーターの回転スピードや回転方向は個別に指定しています。
デモ②
さきほどのデモ①をラジコンにしての動作テストです。スマホがラジコンのコントローラーになっているのがわかると思います。
構成と仕組み概要
全体の構成図と処理フローの概要です。
機器と役割
- ラズパイ
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
を開く
- ラジコンのコントローラーとなるWebサイト
ラズパイとサーボモーターの配線
配線図を紹介します。わかりやすくするため、サーボモーターごとに配線図を用意しました。
黄線の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
<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
を記載します
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
今回はこれでおわりです。
次はラジコンにカメラをのせてラジコン目線で操作できるようにします
次回は(たぶん)カメラ視点の映像を見ながら操作するラジコンの作りかたを紹介します。
Discussion