树莓派 - 实战篇 [基于 websocket 实现手机远程控制树莓派小车]

前言

立下的flag总是要还的,这篇的文章可能比较长,基于websocket来远程控制树莓派小车。首先需要有一个公网服务器,我这边已经在公网启用了一个websocket服务,作为远程控制树莓派小车数据中转平台。

设计思路

1.当树莓派启动后,需要连接公网websocket服务,绑定设备ID和修改树莓派的在线状态
2.H5页面,页面根据设备ID查询设备的在线状态,在线则允许操作
3.H5页面websocket客户端处理客户端的前进、后退、停止、左转和右转操作,与树莓派数据互通达到控制树莓派的目的

树莓派小车组装

这个不多说,某宝上面搜索即可,买个套装带常用传感器的就可以,教程掌柜也会提供。我的教程会针对四轮驱动来讲,这里就跳过了。

Web端

web端操作很简单5个按钮,分别控制树莓派小车的前后左右停操作,直接上代码

<html>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
<style type="text/css">
    #car {
        position: fixed;
        width: 300px;
        bottom: 20px;
        margin: 20px auto;
        left: 0;
        right: 0;
    }

    button.top {
        display: block;
        height: 50px;
        width: 90px;
        margin: 10px auto;
    }

    button.left {
        height: 50px;
        width: 90px;
        margin-right: 10px;
    }

    button.right {
        height: 50px;
        width: 90px;
    }

    button.bottom {
        display: block;
        height: 50px;
        width: 90px;
        margin: 10px auto;

    }

    button.stop {
        height: 50px;
        width: 90px;
        margin-right: 10px;
    }
</style>
<head>
    <title>Smarty Car</title>
</head>
<body>
<p id="message"></p>
<div id="car">
    <button class="top" onclick="send('forward')">Forward</button>
    <button class="left" onclick="send('left')">Left</button>
    <button class="stop" onclick="send('stop')">Stop</button>
    <button class="right" onclick="send('right')">Right</button>
    <button class="bottom" onclick="send('back')">Back</button>
</div>
</body>
<script>
    // 初始化websocket客户端
    var socket, fd;
    var host = "ws://192.168.33.10:9511/";
    socket = new WebSocket(host);
    // 连接完成 询问树莓派是否在线
    socket.onopen = function () {
        var data = new Object();
        data.devices = 'smart_car';
        data.action = 'init';
        socket.send(JSON.stringify(data));
    };
    // 接收消息
    socket.onmessage = function (evt) {
        var jsonData = JSON.parse(evt.data);
        // 如果返回树莓派在线状态
        if (jsonData.err_code != 0) {
            fd = 0
            document.getElementById('message').textContent = jsonData.err_msg;
        } else {
            //返回在线 绑定树莓派的websocket fd
            fd = jsonData.fd;
            document.getElementById('message').textContent = '终端在线';
        }
    };
    // 连接关闭事件
    socket.onclose = function () {
    }

    //发送消息
    function send(msg) {
        if (!fd) {
            document.getElementById('message').textContent = '终端未在线!';
            return false;
        }
        var data = new Object();
        data.devices = fd;
        switch (msg) {
            case 'forward':
            case 'left':
            case 'stop':
            case 'right':
            case 'back':
                data.action = msg;
                break;
            default:
                document.getElementById('message').textContent = '错误函数调用';
                return false;
        }
        try {
            socket.send(JSON.stringify(data));
        } catch (ex) {

        }
    }
</script>
</html>

websocket服务端

我这边使用Laravel框架搭建web服务及websocket服务器,安装了swoole扩展,如果是php专业且用laravel框架搭建websocket的服务的,可以安装这个包来实现简单的websocket服务。也欢迎各位star和issues。如果不想搭建或没有公网IP可以私信我开放接口给各位。

 composer require wenjunting50779/laravel-fast

树莓派端

实现也简单,不足200行代码,起了两个线程。一个线程监听来自 Websocket 服务器的事件,一个线程使用超声波模块测量前面障碍物的距离,话不多说上代码。

import RPi.GPIO as GPIO
import websocket
import json
import threading
import time

'''
开机运行当前脚本
1.告知 WebSocket 服务器,我已准备就绪
2.监听来自服务器的消息
3.检测前方障碍物的距离
'''

# 共享全局变量 供多个线程使用,确定当前小车状态
action = 'stop'

def socket_client():
    # 创建 WebSocket 客户端连接服务器,并发送消息告知服务器准备就绪
    ws = websocket.create_connection("ws://192.168.33.10:9511")
    req = {"terminal": "smart_car"}
    ws.send(json.dumps(req))
    # 监听来自服务器的消息
    while True:
        res = ws.recv()
        res = json.loads(res)
        if res.__contains__('action') and res.__contains__('err_code') and res['err_code'] == 0:
            global action
            action = res['action']
            # 执行后退操作 设置各引脚的高低电位,设置占空比 30/100 控制速度
            if res['action'] == 'back':
                GPIO.output(35, GPIO.LOW)
                GPIO.output(37, GPIO.HIGH)
                GPIO.output(13, GPIO.LOW)
                GPIO.output(15, GPIO.HIGH)
                GPIO.output(12, GPIO.LOW)
                GPIO.output(16, GPIO.HIGH)
                GPIO.output(18, GPIO.LOW)
                GPIO.output(22, GPIO.HIGH)
                speed(30)
            # 执行前进操作 设置各引脚的高低电位,与后退相反,设置占空比 30/100 控制速度
            elif res['action'] == 'forward':
                GPIO.output(37, GPIO.LOW)
                GPIO.output(35, GPIO.HIGH)
                GPIO.output(15, GPIO.LOW)
                GPIO.output(13, GPIO.HIGH)
                GPIO.output(16, GPIO.LOW)
                GPIO.output(12, GPIO.HIGH)
                GPIO.output(22, GPIO.LOW)
                GPIO.output(18, GPIO.HIGH)
                speed(30)
            # 执行停止操作,设置占空比 0/100 控制速度
            elif res['action'] == 'stop':
                speed(0)
            # 执行右转操作 设置各引脚的高低电位,设置占空比 40/100 控制速度
            elif res['action'] == 'right':
                GPIO.output(35, GPIO.LOW)
                GPIO.output(37, GPIO.HIGH)
                GPIO.output(13, GPIO.LOW)
                GPIO.output(15, GPIO.HIGH)
                GPIO.output(12, GPIO.LOW)
                GPIO.output(16, GPIO.LOW)
                GPIO.output(18, GPIO.HIGH)
                GPIO.output(22, GPIO.LOW)
                speed(40)
            # 执行左转操作 设置各引脚的高低电位,设置占空比 40/100 控制速度
            elif res['action'] == 'left':
                GPIO.output(35, GPIO.HIGH)
                GPIO.output(37, GPIO.LOW)
                GPIO.output(13, GPIO.LOW)
                GPIO.output(15, GPIO.LOW)
                GPIO.output(12, GPIO.LOW)
                GPIO.output(16, GPIO.HIGH)
                GPIO.output(18, GPIO.LOW)
                GPIO.output(22, GPIO.HIGH)
                speed(40)
        else:
            print('error')


def check():
    '''
    使用超声波测距模块检测前方障碍物距离控制当前小车速度
    :return:
    '''
    while True:
        global action
        distance = Measure()
        # 如果距离小于10cm且当前状态为前进时执行停止操作
        if (distance < 10) and action == 'forward':
            speed(0)
        # 如果距离基于10~30cm之间且当前状态为前进时控制占空比 20/100 减速
        elif action == 'forward' and 10 < distance < 30:
            speed(20)
        else:
            pass


# 多线程执行 线程启动时连接服务器
class EventThread(threading.Thread):
    def run(self):
        socket_client()


# 多线程执行 线程启动时检测前方障碍物距离
class CheckThread(threading.Thread):
    def run(self):
        check()


# 初始化树莓派引脚
def init():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(35, GPIO.OUT)
    GPIO.setup(37, GPIO.OUT)
    GPIO.setup(13, GPIO.OUT)
    GPIO.setup(15, GPIO.OUT)
    GPIO.setup(12, GPIO.OUT)
    GPIO.setup(16, GPIO.OUT)
    GPIO.setup(18, GPIO.OUT)
    GPIO.setup(22, GPIO.OUT)
    GPIO.setup(11, GPIO.OUT)
    # 设置超声模块输入输出引脚
    GPIO.setup(31, GPIO.OUT)
    GPIO.setup(33, GPIO.IN)


# 控制超声波测距模块输出信号,获得输入信号,完成测距
def Measure():
    # send
    GPIO.output(31, True)
    time.sleep(0.00001)  # 1us
    GPIO.output(31, False)
    # start recording
    while GPIO.input(33) == 0:
        pass
    start = time.time()

    # end recording
    while GPIO.input(33) == 1:
        pass
    end = time.time()

    # compute distance
    distance = round((end - start) * 343 / 2 * 100)
    return distance


# 调整小车速度
def speed(s):
    p.ChangeDutyCycle(s)


#   初始化引脚
init()

# 初始小车速度
p = GPIO.PWM(11, 100)
p.start(0)

# 启动线程监听服务端消息
eventThread = EventThread()
eventThread.start()
# 启动线程检测前方障碍物距离
checkThread = CheckThread()
checkThread.start()

思考

俗话说:授人以鱼不如授人以渔。这些代码和思路只是提供一些参考,还是有一些问题没有考虑,比如:电机是怎么控制正反转的,如何控制速度的? 一些电子元器件的控制将在树莓派-传感器篇中讲解。源码也会在github上供大家参考。欢迎交流讨论。:neckbeard:

本作品采用《CC 协议》,转载必须注明作者和本文链接
死磕,不要放弃,终将会有所收获。
讨论数量: 7

本来想白漂你的前端呢,结果咋比我的还丑啊 :joy:。

3年前 评论

@tiansai 白嫖是不可能的,想想就好了

3年前 评论

大佬实现了吗 :see_no_evil:

3年前 评论

@zmxyzmxy1234 最近比较忙还没时间更新 :sweat_smile:

3年前 评论

生产队的驴都不敢这么消极怠工,赶紧写后续的!

2年前 评论

@191834552 上面的代码已经可以控制小车了,你想了解啥

2年前 评论

请问为什么import websocket 会提示ModuleNotFoundError: No module named 'websocket'呢

11个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!