基于树莓派的遥控小车

一、引言

最近在准备一个计算机类比赛,需要用树莓派做成一个智能乒乓球捡球车,做过一些人工智能项目的也知道,所谓人工智能,只有人工,没有智能(实力吐槽),只要你知道实现的原理,做很多人工智能的东西都能得心应手。在准备比赛之际遇到瓶颈,停了下来做了一个简单的遥控小车。(老实说,智能捡球车还没有我这个遥控车好玩)接下来进入正题:

利用树莓派等相关硬件,制作安卓端控制遥控小车。通过安卓手机与树莓派小车建立网络连接,*控制小车的运动。

二、准备工作

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);
        }
    };
}