Raspberry PiとPythonでリモコンカーを作成する
はじめに
Mac環境の記事ですが、Windows環境も同じ手順になります。環境依存の部分は読み替えてお試しください。
目的
スマートフォンからWEBアプリのコントローラに繋ぎ、Wi-FI介してリモコンカーを制御します。ラジコンをインターネットで動かすイメージです。
この記事を最後まで読むと、次のことができるようになります。
No. | 概要 | キーワード |
---|---|---|
1 | 電子回路 | |
2 | REST API | Flask |
3 | コントローラ制御 | HTML, JavaScript |
4 | モータ制御 | モータGPIO |
5 | サーボモータ制御 | サーボモータGPIO |
完成イメージ
コントローラ | 本体 |
---|---|
実行環境
環境 | Ver. |
---|---|
macOS Catalina | 10.15.6 |
Raspberry Pi 4 Model B 4GB RAM | - |
Raspberry Pi OS (Raspbian) | 10 |
Python | 3.7.3 |
Flask | 1.1.1 |
RPi.GPIO | 0.7.0 |
ソースコード
実際に実装内容やソースコードを追いながら読むとより理解が深まるかと思います。是非ご活用ください。
関連する記事
電子回路
モータ電子回路
サーボモータ電子回路
WEBアプリ構成
target.sh
/
├── Dockerfiles
│ ├── app
│ │ ├── Dockerfile
│ │ ├── docker-compose.yml
│ │ └── entrypoint.sh
│ ├── docker_compose_up.sh
│ └── docker_run.sh
├── app
│ ├── __init__.py
│ ├── apis
│ │ ├── __init__.py
│ │ ├── client
│ │ │ ├── __init__.py
│ │ │ ├── post_motor.py
│ │ │ └── post_servo.py
│ │ ├── models
│ │ │ └── __init__.py
│ │ ├── static
│ │ │ └── __init__.py
│ │ ├── templates
│ │ │ └── app_form.html
│ │ └── views
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── app.py
│ │ ├── handler.py
│ │ ├── motor.py
│ │ └── servo.py
│ ├── common
│ │ ├── __init__.py
│ │ └── utility.py
│ ├── config
│ │ ├── __init__.py
│ │ ├── docker.py
│ │ ├── localhost.py
│ │ └── production.py
│ ├── requirements.txt
│ ├── run.py
│ └── tests
│ ├── __init__.py
│ └── test_apis.py
└── config
├── docker
├── localhost
└── production
REST API
target.sh
/app
└─ apis
└─ views
└── handler.py
REST APIハンドラー
handler.py
"""app/apis/views/handler.py
"""
from flask import Blueprint, jsonify, request
from common.utility import err_response
from apis.views.api import handler as api_handler
from apis.views.app import handler as app_handler
apis = Blueprint(name='rasp-iccar', import_name=__name__,
url_prefix='/rasp-iccar')
@apis.route('/healthcheck', methods=['GET'])
def healthcheck():
"""healthcheck
"""
return jsonify({'status': 'healthy'}), 200
@apis.route('/api', methods=['GET', 'POST', 'PUT', 'DELETE'])
def api():
"""api
"""
if request.method == 'GET':
process = request.args.get('process')
req = {
'param1': request.args.get('request'),
'param2': request.args
}
if process == 'back_end':
return api_handler(req=req)
if process == 'front_end':
return app_handler(req=req)
if request.method == 'POST' or request.method == 'PUT' or request.method == 'DELETE':
payload = request.json
process = payload.get('process')
req = payload.get('request')
if process == 'back_end':
return api_handler(req=req)
return jsonify({'message': 'no route matched with those values'}), 200
@apis.errorhandler(404)
@apis.errorhandler(500)
def errorhandler(error):
"""errorhandler
"""
return err_response(error=error), error.code
コントローラ制御
target.sh
/app
└─ apis
├─ templates
│ └── app_form.html
└─ views
└── app.py
コントローラハンドラー
app.py
"""app/apis/views/app.py
"""
from flask import jsonify, render_template
def handler(req):
"""handler
"""
param1 = req.get('param1')
param2 = req.get('param2')
if param1 == 'app_form':
return _app_form(req=param2)
return jsonify({'message': 'no route matched with those values'}), 200
def _app_form(req):
"""_app_form
"""
if req.get('secret_key', '') != 'M7XvWE9fSFg3':
return jsonify({'message': 'no route matched with those values'}), 200
return render_template('app_form.html')
コントローラI/F
app_form.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>raspi-iccar</title>
<style type="text/css">
html,
body {
-webkit-user-select: none;
width: 100%;
height: 100%;
}
table {
width: 100%;
height: 100%;
}
table,
td {
border: 1px gray solid;
padding: 10px;
}
button.up-down {
touch-action: manipulation;
font-size: 5vh;
font-weight: bold;
width: 45%;
height: 95%;
}
button.right-left {
touch-action: manipulation;
font-size: 5vh;
font-weight: bold;
width: 95%;
height: 50%;
}
.button_option {
clip: rect(1px, 1px, 1px, 1px);
position: absolute !important;
}
.button_option_label {
font-weight: bold;
font-size: 5vh;
}
.button_option:checked+.button_option_label {
background: #4169e1;
color: #fff;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript">
let lastTouchEndTime = 0;
document.addEventListener('touchend', (event) => {
const now = new Date().getTime();
if ((now - lastTouchEndTime) < 350) {
event.preventDefault();
}
lastTouchEndTime = now;
});
</script>
<script type="text/javascript">
// var HOST = "http://127.0.0.1:5000/rasp-iccar/api";
var HOST = "http://192.168.0.77:5000/rasp-iccar/api";
var BUTTON_TIMER_LONG = 500;
var BUTTON_TIMER_REPEAT = 500;
var up_down_timer_id;
var right_left_timer_id;
$(function () {
var post = function (button_type, button_event, button_option) {
var data = {
"process": "back_end",
"request": {
"param1": "button",
"param2": [{
"button_type": button_type,
"button_event": button_event,
"button_option": button_option
}]
}
};
$.ajax({
type: "post",
url: HOST,
data: JSON.stringify(data),
contentType: "application/json",
dataType: "json",
scriptCharset: "utf-8",
success: function (data) {
console.log(JSON.stringify(data));
},
error: function (data) {
console.log(JSON.stringify(data));
}
});
};
var button_event_long = function (button_type, button_event, button_option, button_timer) {
post(button_type, button_event, button_option);
if (button_timer == "BUTTON_TIMER_UP_DOWN") {
clearTimeout(up_down_timer_id);
up_down_timer_id = setTimeout(button_event_repeat, BUTTON_TIMER_REPEAT, button_type, "BUTTON_EVENT_REPEAT", button_option, button_timer);
} else {
clearTimeout(right_left_timer_id);
right_left_timer_id = setTimeout(button_event_repeat, BUTTON_TIMER_REPEAT, button_type, "BUTTON_EVENT_REPEAT", button_option, button_timer);
}
};
var button_event_repeat = function (button_type, button_event, button_option, button_timer) {
post(button_type, button_event, button_option);
if (button_timer == "BUTTON_TIMER_UP_DOWN") {
clearTimeout(up_down_timer_id);
up_down_timer_id = setTimeout(button_event_repeat, BUTTON_TIMER_REPEAT, button_type, "BUTTON_EVENT_REPEAT", button_option, button_timer);
} else {
clearTimeout(right_left_timer_id);
right_left_timer_id = setTimeout(button_event_repeat, BUTTON_TIMER_REPEAT, button_type, "BUTTON_EVENT_REPEAT", button_option, button_timer);
}
};
// button up
$("#button_up").on("mousedown touchstart", function () {
var button_option = $('input[name="button_option"]:checked').val();
post("BUTTON_TYPE_UP", "BUTTON_EVENT_PRESS", button_option);
clearTimeout(up_down_timer_id);
up_down_timer_id = setTimeout(button_event_long, BUTTON_TIMER_LONG, "BUTTON_TYPE_UP", "BUTTON_EVENT_LONG", button_option, "BUTTON_TIMER_UP_DOWN");
}).on("mouseup mouseleave touchend", function () {
clearTimeout(up_down_timer_id);
post("BUTTON_TYPE_UP", "BUTTON_EVENT_RELEASE", 0)
});
// button down
$("#button_down").on("mousedown touchstart", function () {
var button_option = $('input[name="button_option"]:checked').val();
post("BUTTON_TYPE_DOWN", "BUTTON_EVENT_PRESS", button_option);
clearTimeout(up_down_timer_id);
up_down_timer_id = setTimeout(button_event_long, BUTTON_TIMER_LONG, "BUTTON_TYPE_DOWN", "BUTTON_EVENT_LONG", button_option, "BUTTON_TIMER_UP_DOWN");
}).on("mouseup mouseleave touchend", function () {
clearTimeout(up_down_timer_id);
post("BUTTON_TYPE_DOWN", "BUTTON_EVENT_RELEASE", 0)
});
// button right
$("#button_right").on("mousedown touchstart", function () {
post("BUTTON_TYPE_RIGHT", "BUTTON_EVENT_PRESS", -30);
clearTimeout(right_left_timer_id);
right_left_timer_id = setTimeout(button_event_long, BUTTON_TIMER_LONG, "BUTTON_TYPE_RIGHT", "BUTTON_EVENT_LONG", -30, "BUTTON_TIMER_RIGHT_LEFT");
}).on("mouseup mouseleave touchend", function () {
clearTimeout(right_left_timer_id);
post("BUTTON_TYPE_RIGHT", "BUTTON_EVENT_RELEASE", 0)
});
// button left
$("#button_left").on("mousedown touchstart", function () {
post("BUTTON_TYPE_LEFT", "BUTTON_EVENT_PRESS", 35);
clearTimeout(right_left_timer_id);
right_left_timer_id = setTimeout(button_event_long, BUTTON_TIMER_LONG, "BUTTON_TYPE_LEFT", "BUTTON_EVENT_LONG", 35, "BUTTON_TIMER_RIGHT_LEFT");
}).on("mouseup mouseleave touchend", function () {
clearTimeout(right_left_timer_id);
post("BUTTON_TYPE_LEFT", "BUTTON_EVENT_RELEASE", 0)
});
})
</script>
</head>
<body>
<table>
<tr height="20%">
<td colspan="2">
<table>
<tr align="center">
<td>
<input class="button_option" type="radio" id="button_high" name="button_option"
value="100" />
<label class="button_option_label" for="button_high">はやい</label>
</td>
<td>
<input class="button_option" type="radio" id="button_middle" name="button_option" value="50"
checked />
<label class="button_option_label" for="button_middle">ふつう</label>
</td>
<td>
<input class="button_option" type="radio" id="button_low" name="button_option" value="25" />
<label class="button_option_label" for="button_low">おそい</label>
</td>
</table>
</td>
</tr>
<tr>
<td width="50%">
<table>
<tr align="center">
<td width="50%" valign="bottom"><button class="up-down" type="button"
id="button_up">↑<br>まえ</button></td>
</tr>
<tr align="center">
<td valign="top"><button class="up-down" type="button" id="button_down">↓<br>うしろ</button></td>
</tr>
</table>
</td>
<td>
<table>
<tr align="center">
<td width="50%" align="right"><button class="right-left" type="button"
id="button_left">←<br>ひだり</button></td>
<td align="left"><button class="right-left" type="button" id="button_right">→<br>みぎ</button>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
モータ制御
target.sh
/app
└─ apis
└─ views
├── __init__.py
├── api.py
└── motor.py
モータハンドラー
__init__.py
"""app/apis/views/__init__.py
"""
from apis.views.motor import Raspi as RaspiMotor
from apis.views.servo import Raspi as RaspiServo
raspi_motor = RaspiMotor()
raspi_servo = RaspiServo()
api.py
"""app/apis/views/api.py
"""
from flask import jsonify
from apis.views import raspi_motor, raspi_servo
def handler(req):
"""handler
"""
param1 = req.get('param1')
param2 = req.get('param2')
if param1 == 'button':
return _button(payloads=param2[0])
return jsonify({'message': 'no route matched with those values'}), 200
def _button(payloads):
"""_button
"""
button_type = payloads.get('button_type')
button_event = payloads.get('button_event')
button_option = payloads.get('button_option')
if button_type in ['BUTTON_TYPE_UP', 'BUTTON_TYPE_DOWN']:
_motor(button_type, button_event, int(button_option))
elif button_type in ['BUTTON_TYPE_RIGHT', 'BUTTON_TYPE_LEFT']:
_servo(button_event, int(button_option))
response = {
'status': 'success',
'request': payloads
}
print(response)
return jsonify(response), 200
def _motor(button_type, button_event, button_option):
"""_motor
"""
if button_event in ['BUTTON_EVENT_PRESS', 'BUTTON_EVENT_LONG', 'BUTTON_EVENT_REPEAT']:
return _motor_on(button_type, button_option)
return _motor_off()
def _motor_on(button_type, button_option):
"""_motor_on
"""
raspi_motor.start(button_type, button_option)
def _motor_off():
"""_motor_off
"""
raspi_motor.stop()
モータGPIO
motor.py
"""app/apis/views/motor.py
"""
import time
import RPi.GPIO as GPIO
GPIO_BCM_L293D_EN1 = 17
GPIO_BCM_L293D_IN1 = 27
GPIO_BCM_L293D_IN2 = 22
class Raspi():
"""Raspi
"""
def __init__(self):
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_BCM_L293D_EN1, GPIO.OUT)
GPIO.setup(GPIO_BCM_L293D_IN1, GPIO.OUT)
GPIO.setup(GPIO_BCM_L293D_IN2, GPIO.OUT)
GPIO.output(GPIO_BCM_L293D_EN1, GPIO.LOW)
GPIO.output(GPIO_BCM_L293D_IN1, GPIO.LOW)
GPIO.output(GPIO_BCM_L293D_IN2, GPIO.LOW)
self.pwm_l293d_in1 = GPIO.PWM(GPIO_BCM_L293D_IN1, 50)
self.pwm_l293d_in2 = GPIO.PWM(GPIO_BCM_L293D_IN2, 50)
self.pwm_l293d_in1.start(0)
self.pwm_l293d_in2.start(0)
self.on = False
print('using pin {}, {}, {}'.format(GPIO_BCM_L293D_EN1,
GPIO_BCM_L293D_IN1, GPIO_BCM_L293D_IN2))
print('pulse width modulation {} Hz'.format(50))
def destroy(self):
"""destroy
"""
self.stop()
GPIO.cleanup()
def start(self, button_type, button_option):
"""start
"""
if self.on is True:
print('skip button event')
return
if button_type == 'BUTTON_TYPE_UP':
GPIO.output(GPIO_BCM_L293D_EN1, GPIO.HIGH)
self.pwm_l293d_in1.ChangeDutyCycle(button_option)
self.pwm_l293d_in2.ChangeDutyCycle(0)
self.on = True
elif button_type == 'BUTTON_TYPE_DOWN':
GPIO.output(GPIO_BCM_L293D_EN1, GPIO.HIGH)
self.pwm_l293d_in1.ChangeDutyCycle(0)
self.pwm_l293d_in2.ChangeDutyCycle(button_option)
self.on = True
else:
self.on = False
def stop(self):
"""stop
"""
GPIO.output(GPIO_BCM_L293D_EN1, GPIO.LOW)
self.pwm_l293d_in1.ChangeDutyCycle(0)
self.pwm_l293d_in2.ChangeDutyCycle(0)
self.on = False
def loop(self):
"""loop
"""
while True:
self.start('BUTTON_TYPE_UP', 50)
print('>>> BUTTON_TYPE_UP')
time.sleep(3)
self.stop()
print('>>> STOP')
time.sleep(3)
self.start('BUTTON_TYPE_DOWN', 50)
print('>>> BUTTON_TYPE_DOWN')
time.sleep(3)
self.stop()
print('>>> STOP')
time.sleep(3)
if __name__ == '__main__':
raspi = Raspi()
try:
print('start')
raspi.loop()
except KeyboardInterrupt:
raspi.destroy()
print('stop')
サーボモータ制御
target.sh
/app
└─ apis
└─ views
├── __init__.py
├── api.py
└── servo.py
サーボモータハンドラー
__init__.py
"""app/apis/views/__init__.py
"""
from apis.views.motor import Raspi as RaspiMotor
from apis.views.servo import Raspi as RaspiServo
raspi_motor = RaspiMotor()
raspi_servo = RaspiServo()
api.py
"""app/apis/views/api.py
"""
from flask import jsonify
from apis.views import raspi_motor, raspi_servo
def handler(req):
"""handler
"""
param1 = req.get('param1')
param2 = req.get('param2')
if param1 == 'button':
return _button(payloads=param2[0])
return jsonify({'message': 'no route matched with those values'}), 200
def _button(payloads):
"""_button
"""
button_type = payloads.get('button_type')
button_event = payloads.get('button_event')
button_option = payloads.get('button_option')
if button_type in ['BUTTON_TYPE_UP', 'BUTTON_TYPE_DOWN']:
_motor(button_type, button_event, int(button_option))
elif button_type in ['BUTTON_TYPE_RIGHT', 'BUTTON_TYPE_LEFT']:
_servo(button_event, int(button_option))
response = {
'status': 'success',
'request': payloads
}
print(response)
return jsonify(response), 200
def _servo(button_event, button_option):
"""_servo
"""
if button_event in ['BUTTON_EVENT_PRESS', 'BUTTON_EVENT_LONG', 'BUTTON_EVENT_REPEAT']:
return _servo_on(button_option)
return _servo_off(button_option)
def _servo_on(button_option):
"""_servo_on
"""
raspi_servo.start(button_option)
def _servo_off(button_option):
"""_servo_off
"""
raspi_servo.stop(button_option)
サーボモータGPIO
servo.py
"""app/apis/views/servo.py
"""
import time
import RPi.GPIO as GPIO
GPIO_BCM_SERVO = 18
class Raspi():
"""Raspi
"""
def __init__(self):
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_BCM_SERVO, GPIO.OUT)
GPIO.output(GPIO_BCM_SERVO, GPIO.LOW)
self.pwm_servo = GPIO.PWM(GPIO_BCM_SERVO, 50)
self.pwm_servo.start(0)
self.on = False
print('using pin {}'.format(GPIO_BCM_SERVO))
print('pulse width modulation {} Hz'.format(50))
def destroy(self):
"""destroy
"""
self.pwm_servo.stop()
GPIO.cleanup()
def angle(self, angle):
"""angle
"""
duty = 2.5 + (12.0 - 2.5) * (angle + 90) / 180
self.pwm_servo.ChangeDutyCycle(duty)
def start(self, angle):
"""start
"""
if self.on is True:
print('skip button event')
return
self.angle(angle)
self.on = True
def stop(self, angle):
"""stop
"""
self.angle(angle)
self.on = False
def loop(self):
"""loop
"""
while True:
self.start(-30)
print('>>> BUTTON_TYPE_RIGHT')
time.sleep(3)
self.stop(0)
print('>>> STOP')
time.sleep(3)
self.start(35)
print('>>> BUTTON_TYPE_LEFT')
time.sleep(3)
self.stop(0)
print('>>> STOP')
time.sleep(3)
if __name__ == '__main__':
raspi = Raspi()
try:
print('start')
raspi.loop()
except KeyboardInterrupt:
raspi.destroy()
print('stop')