基于树莓派的遥控小车
一、引言
最近在准备一个计算机类比赛,需要用树莓派做成一个智能乒乓球捡球车,做过一些人工智能项目的也知道,所谓人工智能,只有人工,没有智能(实力吐槽),只要你知道实现的原理,做很多人工智能的东西都能得心应手。在准备比赛之际遇到瓶颈,停了下来做了一个简单的遥控小车。(老实说,智能捡球车还没有我这个遥控车好玩)接下来进入正题:
利用树莓派等相关硬件,制作安卓端控制遥控小车。通过安卓手机与树莓派小车建立网络连接,*控制小车的运动。
二、准备工作
1.安卓手机一部
2.树莓派小车
3.网线
三、效果预览
ps:左图为我的树莓派小车,外形可以随意,其他树莓派小车同样适用该教程,主要区别在于我的树莓派只有两个电机驱动,其他小车可能会有四个电机驱动,代码实现中需要再加另外两个电机的驱动代码,这个没多大关系,会操纵两个电机同样的道理,就算来多少个电机也能随意操纵。右图是我的手机端界面,本来就做的丑,被压缩过的图片放上去更丑,这个无所谓,功能可以完美实现。
四、功能实现
1.建立连接:
利用网线连接树莓派3B+主板与电脑端,通过putty工具进行SSH连接,然后设置WIFI,将手机与树莓派连接,具体操作方法可以看我的另一篇博客:https://blog.****.net/qq_42109746/article/details/88537284
2.实现原理:安卓手机与树莓派之间通信主要是通过Socket套接字实现,在树莓派端运行Python程序,开启端口进行监听,安卓端向树莓派发送消息,树莓派端通过端口接收到安卓端发过来的消息,执行相应的动作。
3.树莓派端程序:
# coding:utf-8
import RPi.GPIO as GPIO
import time
import socket
import sys
HOST_IP = "192.168.43.212" # 树莓派IP地址
HOST_PORT = 7654 # 端口号
#########定义模式############
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
########电机驱动接口定义############
ENA = 13 # //L298使能A
ENB = 20 # //L298使能B
IN1 = 16 # //电机接口1
IN2 = 19 # //电机接口2
IN3 = 21 # //电机接口3
IN4 = 26 # //电机接口4
#########电机初始化为LOW##########
GPIO.setup(ENA, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(IN1, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(IN2, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(ENB, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(IN3, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(IN4, GPIO.OUT, initial=GPIO.LOW)
########定义左右电机初始速度#########
pwm1 = GPIO.PWM(ENA, 80) # 第一个参数为引脚号,第二个参数为频率(HZ)
pwm2 = GPIO.PWM(ENB, 80)
pwm1.start(60) # 以百分比设置速度
pwm2.start(60)
#########定义电机正转函数##########
def forward():
stop()
print('正在前进....')
GPIO.output(ENA, True)
GPIO.output(IN1, True)
GPIO.output(IN2, False)
GPIO.output(ENB, True)
GPIO.output(IN3, True)
GPIO.output(IN4, False)
#########定义电机反转函数##########
def back():
stop()
print('正在后退....')
GPIO.output(ENA, True)
GPIO.output(IN1, False)
GPIO.output(IN2, True)
GPIO.output(ENB, True)
GPIO.output(IN3, False)
GPIO.output(IN4, True)
#########定义电机停止函数##########
def stop():
print('停止....')
GPIO.output(ENA, False)
GPIO.output(ENB, False)
GPIO.output(IN1, False)
GPIO.output(IN2, False)
GPIO.output(IN3, False)
GPIO.output(IN4, False)
########左转弯函数#######
def left():
stop()
print('正在左转....')
GPIO.output(ENA, True)
GPIO.output(IN1, True)
GPIO.output(IN2, False)
GPIO.output(ENB, True)
GPIO.output(IN3, False)
GPIO.output(IN4, True)
########左转弯函数#######
def right():
stop()
print('正在右转....')
GPIO.output(ENA, True)
GPIO.output(IN1, False)
GPIO.output(IN2, True)
GPIO.output(ENB, True)
GPIO.output(IN3, True)
GPIO.output(IN4, False)
# pwm1.ChangeDutyCycle(100)
# pwm2.ChangeDutyCycle(100)
print("Starting socket: TCP...")
socket_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建socket
print("TCP server listen @ %s:%d!" % (HOST_IP, HOST_PORT))
host_addr = (HOST_IP, HOST_PORT)
socket_tcp.bind(host_addr) # 绑定我的树莓派的ip地址和端口号
socket_tcp.listen(1) # listen函数的参数是监听客户端的个数,这里只监听一个,即只允许与一个客户端创建连接
while True:
print('waiting for connection...')
socket_con, (client_ip, client_port) = socket_tcp.accept() # 接收客户端的请求
print("Connection accepted from %s." % client_ip)
info = "已连接"
socket_con.send(bytes(info, encoding="utf8")) # 发送数据
while True:
data = str(socket_con.recv(1024), encoding="utf8") # 接收数据
if data == "1":
forward()
if data == "2":
back()
if data == "3":
left()
if data == "4":
right()
if data == "5":
stop()
socket_tcp.close()
4.安卓端程序:
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.chen.control">
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<TextView
android:id="@+id/tv_connect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:textSize="25dp"
android:layout_marginTop="100dp"
android:text="连接状态:"/>
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:textSize="25dp"
android:layout_marginTop="20dp"
android:text="角度值:0"/>
<TextView
android:id="@+id/tv_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:textSize="25dp"
android:layout_marginTop="20dp"
android:text="运动状态:"/>
<Button
android:id="@+id/btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Stop"
android:layout_marginLeft="50dp"
android:background="@color/stop"/>
<com.example.chen.control.Rudder
android:id="@+id/rudder"
android:layout_marginLeft="50dp"
android:layout_width="500dp"
android:layout_marginTop="80dp"
android:layout_height="500dp" />
</LinearLayout>
Rudder.java(自定义摇杆组件):
package com.example.chen.control;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PorterDuff.Mode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.SurfaceHolder.Callback;
public class Rudder extends SurfaceView implements Runnable,Callback{
private SurfaceHolder mHolder;
private boolean isStop = false;
private Thread mThread;
private Paint mPaint;
private Point mRockerPosition; //摇杆位置
private Point mCtrlPoint = new Point(280,280);//摇杆起始位置
private int mRudderRadius = 80;//摇杆半径
private int mWheelRadius = 200;//摇杆活动范围半径
private RudderListener listener = null; //事件回调接口
public static final int ACTION_RUDDER = 1 ;
public Rudder(Context context) {
super(context);
}
public Rudder(Context context, AttributeSet as) {
super(context, as);
this.setKeepScreenOn(true);
mHolder = getHolder();
mHolder.addCallback(this);
mThread = new Thread(this);
mPaint = new Paint();
mPaint.setColor(Color.GREEN);
mPaint.setAntiAlias(true);//抗锯齿
mRockerPosition = new Point(mCtrlPoint);
setFocusable(true);
setFocusableInTouchMode(true);
setZOrderOnTop(true);
mHolder.setFormat(PixelFormat.TRANSPARENT);//设置背景透明
}
//设置回调接口
public void setRudderListener(RudderListener rockerListener) {
listener = rockerListener;
}
@Override
public void run() {
Canvas canvas = null;
while(!isStop) {
try {
canvas = mHolder.lockCanvas();
canvas.drawColor(Color.TRANSPARENT,Mode.CLEAR);//清除屏幕
mPaint.setColor(Color.CYAN);
canvas.drawCircle(mCtrlPoint.x, mCtrlPoint.y, mWheelRadius, mPaint);//绘制范围
mPaint.setColor(Color.RED);
canvas.drawCircle(mRockerPosition.x, mRockerPosition.y, mRudderRadius, mPaint);//绘制摇杆
} catch (Exception e) {
e.printStackTrace();
} finally {
if(canvas != null) {
mHolder.unlockCanvasAndPost(canvas);
}
}
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mThread.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
isStop = true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int len = MathUtils.getLength(mCtrlPoint.x, mCtrlPoint.y, event.getX(), event.getY());
if(event.getAction() == MotionEvent.ACTION_DOWN) {
//如果屏幕接触点不在摇杆挥动范围内,则不处理
if(len >mWheelRadius) {
return true;
}
}
if(event.getAction() == MotionEvent.ACTION_MOVE){
if(len <= mWheelRadius) {
//如果手指在摇杆活动范围内,则摇杆处于手指触摸位置
mRockerPosition.set((int)event.getX(), (int)event.getY());
}else{
//设置摇杆位置,使其处于手指触摸方向的 摇杆活动范围边缘
mRockerPosition = MathUtils.getBorderPoint(mCtrlPoint, new Point((int)event.getX(), (int)event.getY()), mWheelRadius);
}
if(listener != null) {
float radian = MathUtils.getRadian(mCtrlPoint, new Point((int)event.getX(), (int)event.getY()));
listener.onSteeringWheelChanged(ACTION_RUDDER,Rudder.this.getAngleCouvert(radian));
}
}
//如果手指离开屏幕,则摇杆返回初始位置
if(event.getAction() == MotionEvent.ACTION_UP) {
mRockerPosition = new Point(mCtrlPoint);
}
return true;
}
//获取摇杆偏移角度 0-360°
private int getAngleCouvert(float radian) {
int tmp = (int)Math.round(radian/Math.PI*180);
if(tmp < 0) {
return -tmp;
}else{
return 180 + (180 - tmp);
}
}
//回调接口
public interface RudderListener {
void onSteeringWheelChanged(int action,int angle);
}
}
MathUtils.java(工具类):
package com.example.chen.control;
import android.graphics.Point;
public class MathUtils {
//获取两点间直线距离
public static int getLength(float x1,float y1,float x2,float y2) {
return (int)Math.sqrt(Math.pow(x1-x2, 2) + Math.pow(y1-y2, 2));
}
/**
* 获取线段上某个点的坐标,长度为a.x - cutRadius
* @param a 点A
* @param b 点B
* @param cutRadius 截断距离
* @return 截断点
*/
public static Point getBorderPoint(Point a, Point b,int cutRadius) {
float radian = getRadian(a, b);
return new Point(a.x + (int)(cutRadius * Math.cos(radian)), a.x + (int)(cutRadius * Math.sin(radian)));
}
//获取水平线夹角弧度
public static float getRadian (Point a, Point b) {
float lenA = b.x-a.x;
float lenB = b.y-a.y;
float lenC = (float)Math.sqrt(lenA*lenA+lenB*lenB);
float ang = (float)Math.acos(lenA/lenC);
ang = ang * (b.y < a.y ? -1 : 1);
return ang;
}
}
MainActivity.java:
package com.example.chen.control;
import android.os.Build;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class MainActivity extends AppCompatActivity {
private TextView tv;
private TextView tv_connect;//连接状态
private TextView tv_play;//运动状态
private Button btn_stop;//停止按钮
private Rudder rudder;//摇杆控件
private Handler handler;//消息机制
private static String forward = "1";//前进
private static String back = "2";//后退
private static String left = "3";//左转
private static String right = "4";//右转
private static String stop="5";//停止
private String send_buff = null;//发送的消息
private String recv_buff = null;//接收的消息
private Socket socket = null;//套接字,用于树莓派与安卓之间通信
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//透明状态栏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window window = getWindow();
window.setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
init();
new Thread(new Runnable() {
@Override
public void run() {
try {
socket = new Socket("192.168.43.212", 7654);
if (socket != null) {
while (true) { //循环进行收发
recv();
Thread.sleep(50);
}
} else
System.out.println("socket is null");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
rudder.setRudderListener(new Rudder.RudderListener() {
@Override
public void onSteeringWheelChanged(int action, final int angle) {
if (action == Rudder.ACTION_RUDDER) {
handler.post(new Runnable() {
@Override
public void run() {
tv.setText(String.valueOf("角度值:" + angle));
}
});
if ((angle > 315 && angle < 360) || (angle > 0 && angle < 45)) {//右转
new Thread(new Runnable() {
@Override
public void run() {
send_buff = right;
OutputStream outputStream = null;
try {
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.write(send_buff.getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
handler.post(new Runnable() {
@Override
public void run() {
tv_play.setText("运动状态:正在右转....");
}
});
}
if (angle > 45 && angle < 135) {//前进
new Thread(new Runnable() {
@Override
public void run() {
send_buff = forward;
OutputStream outputStream = null;
try {
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.write(send_buff.getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
handler.post(new Runnable() {
@Override
public void run() {
tv_play.setText("运动状态:正在前进....");
}
});
}
if (angle > 135 && angle < 225) {//左转
new Thread(new Runnable() {
@Override
public void run() {
send_buff = left;
OutputStream outputStream = null;
try {
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.write(send_buff.getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
handler.post(new Runnable() {
@Override
public void run() {
tv_play.setText("运动状态:正在左转....");
}
});
}
if (angle > 225 && angle < 315) {//后退
new Thread(new Runnable() {
@Override
public void run() {
send_buff = back;
OutputStream outputStream = null;
try {
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.write(send_buff.getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
handler.post(new Runnable() {
@Override
public void run() {
tv_play.setText("运动状态:正在后退....");
}
});
}
}
}
});
btn_stop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
send_buff = stop;
OutputStream outputStream = null;
try {
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.write(send_buff.getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
handler.post(new Runnable() {
@Override
public void run() {
tv_play.setText("运动状态:停止....");
}
});
}
});
}
private void init() {
tv = findViewById(R.id.tv);
tv_connect = findViewById(R.id.tv_connect);
tv_play = findViewById(R.id.tv_play);
btn_stop=findViewById(R.id.btn_stop);
handler = new Handler();
rudder = findViewById(R.id.rudder);
}
private void recv() {
//单开一个线程循环接收来自服务器端的消息
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (inputStream != null) {
try {
byte[] buffer = new byte[1024];
int count = inputStream.read(buffer);//count是传输的字节数
recv_buff = new String(buffer);//socket通信传输的是byte类型,需要转为String类型
} catch (IOException e) {
e.printStackTrace();
}
}
//显示连接状态
if (recv_buff != null) {
handler.post(runnableUi);
}
}
//利用handler刷新UI
Runnable runnableUi = new Runnable() {
@Override
public void run() {
tv_connect.append(recv_buff);
}
};
}