樹莓派 - 實戰篇 [基於 websocket 實現手機遠端控制樹莓派小車]

Groot發表於2020-05-11

前言

立下的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 協議》,轉載必須註明作者和本文連結
死磕,不要放棄,終將會有所收穫。

相關文章