1. 程式人生 > 其它 >基於MQTT協議實現遠端控制的"智慧"車

基於MQTT協議實現遠端控制的"智慧"車

智慧,但不完全智慧

雖然我不覺得這玩意兒有啥智慧的,但都這麼叫就跟著叫嘍。

時隔好幾天才寫的

其實在寫這篇博文的時候我已經在做升級了,並且已經到了中後期階段了。

主要是業餘時間做著玩,看時間了。

規格 & 實拍

  • ESP32
  • 遠端控制
  • 兩驅動輪+一萬向輪

所需硬體

  • 繼電器*4 或 雙路電機2驅動模組 *1

  • 電機*2

  • 輪子*2

  • 萬向輪*1

  • 電源*1

  • MCU *1

  • 導線若干 (我就是因為沒買夠線只能用杜邦線了)

……

推薦使用電機驅動模組,或者自己用mos管。

直接使用雙路繼電器控制的缺點有:

  • 體積大
  • 不支援pwm調速
  • 等等等

ESP32端開發

由於我目前正在升級的版本程式碼也是基於這個版本程式碼進行開發的,所以現在說的是我的新版本程式碼,從程式碼中體現出來的就是多了兩個輪子

,Copy時注意刪減,雖然不影響。

開發基於:

PaltformIO IDE

引入MQTT庫

256dpi/MQTT@^2.5.0

這個庫是老版本的車身控制用的,現在新版本換了個庫,因為這個庫不支援傳送uint8_t資料。但是這個庫,簡單好用。

推薦使用庫 (用了,但沒測試):

knolleary/PubSubClient@^2.8

繼電器訊號IO口管理

/**
    右輪雙路繼電器
*/
// 14 右輪一號繼電器IO串列埠號 (吸合前進)
int RIGHT_ONE_A = 14;
// 12 右輪二號繼電器IO串列埠號 (吸合後退)
int RIGHT_TWO_A = 12;

//右輪一號繼電器IO串列埠號 (吸合前進)
int RIGHT_ONE_B = 14;
//右輪二號繼電器IO串列埠號 (吸合後退)
int RIGHT_TWO_B = 12;

//=====================================================

// 17 左輪二號繼電器IO串列埠號 (吸合前進)
int LEFT_ONE_A = 17;
// 16 左輪二號繼電器IO串列埠號 (吸合後退)
int LEFT_TWO_A = 16;

//左輪二號繼電器IO串列埠號 (吸合前進)
int LEFT_ONE_B = 17;
//左輪二號繼電器IO串列埠號 (吸合後退)
int LEFT_TWO_B = 16;

繼電器狀態管理

/*
  已更換使用基於內建mos管的驅動模組
*/
//右輪1號繼電器吸合狀態
boolean RIGHT_ONE_A_STATUS = false;
//右輪2號繼電器吸合狀態
boolean RIGHT_TWO_A_STATUS = false;
//右後輪1號繼電器吸合狀態
boolean RIGHT_ONE_B_STATUS = false;
//右後輪2號繼電器吸合狀態
boolean RIGHT_TWO_B_STATUS = false;

//左輪1號繼電器吸合狀態
boolean LEFT_ONE_A_STATUS = false;
//左輪2號繼電器吸合狀態
boolean LEFT_TWO_A_STATUS = false;
//左後輪1號繼電器吸合狀態
boolean LEFT_ONE_B_STATUS = false;
//左後輪2號繼電器吸合狀態
boolean LEFT_TWO_B_STATUS = false;

//採用差速轉向
//右轉向動力鎖
boolean RIGHT_TURN_LOCK = false;
//左轉向動力鎖
boolean LEFT_TURN_LOCK = false;

繼電器注意事項

繼電器這裡要說一下,有的像我一樣的萌新一開始不知道繼電器要怎麼用,知道個大概邏輯卻不知道怎麼接線,所以這裡提一下,

敲黑板

繼電器介面有:

VCC、GND、IN;

NC、COM、ON;

六個介面

這裡要注意的是:

  • VCC、GND是給繼電器供電用的!只是給繼電器供電用!控制開合後VCC並不會連線到COM;
  • ON或NC接用電器的電源正極;
  • COM接到用電器,這時候對於NC、ON來說COM是負極,對於用電器是正極;
  • 用電器負極接電源負極,形成通路;

比如電源正極接到了ON,那麼繼電器吸合後的電路如下:

電源正極——ON——COM——用電器——電源負極

鬼知道我經歷了什麼,問了學這個專業朋友都表示”我沒用過“,淦哦

核心控制

在loop中呼叫;

該控制邏輯可實現有:

  • 前進/後退
  • 轉彎時彎內側輪反轉縮小轉彎半徑
  • 前進/後退同時轉彎
/**
 * @brief 根據狀態值為繼電器輸出高低電平
 */
void relayOnStatus()
{
  if ((RIGHT_ONE_A_STATUS || LEFT_TURN_LOCK) && RIGHT_TURN_LOCK == false)
  {
    digitalWrite(RIGHT_ONE_A, HIGH);
    digitalWrite(RIGHT_ONE_B, HIGH);
  }
  else
  {
    digitalWrite(RIGHT_ONE_A, LOW);
    digitalWrite(RIGHT_ONE_B, LOW);
  }
  if (RIGHT_TWO_A_STATUS || RIGHT_TURN_LOCK)
  {
    digitalWrite(RIGHT_TWO_A, HIGH);
    digitalWrite(RIGHT_TWO_B, HIGH);
  }
  else
  {
    digitalWrite(RIGHT_TWO_A, LOW);
    digitalWrite(RIGHT_TWO_B, LOW);
  }

  if ((LEFT_ONE_A_STATUS || RIGHT_TURN_LOCK) && LEFT_TURN_LOCK == false)
  {
    digitalWrite(LEFT_ONE_A, HIGH);
    digitalWrite(LEFT_ONE_B, HIGH);
  }
  else
  {
    digitalWrite(LEFT_ONE_A, LOW);
    digitalWrite(LEFT_ONE_B, LOW);
  }
  if (LEFT_TWO_A_STATUS || LEFT_TURN_LOCK)
  {
    digitalWrite(LEFT_TWO_A, HIGH);
    digitalWrite(LEFT_TWO_B, HIGH);
  }
  else
  {
    digitalWrite(LEFT_TWO_A, LOW);
    digitalWrite(LEFT_TWO_B, LOW);
  }
}

MQTT使用

MQTTClient client;
WiFiClient net;
//mqtt接收到訊息的回撥
void messageReceived(String &topic, String &payload)
{
    //這個方法裡的allRun()這種的函式我就不多說了,只是控制一下繼電器狀態管理那裡變數的值
  Serial.println("incoming: " + topic + " - " + payload);
  if (payload.equals("\"run\""))
  {
    allRun();
  }
  if (payload.equals("\"stop\""))
  {
    allStop();
  }
  if (payload.equals("\"back\""))
  {
    allBack();
  }
  if (payload.equals("\"leftStart\""))
  {
    turnLeftStart();
  }

  if (payload.equals("\"rightStart\""))
  {
    turnRightStart();
  }

  if (payload.equals("\"leftStop\""))
  {
    turnLeftStop();
  }

  if (payload.equals("\"rightStop\""))
  {
    turnRightStop();
  }
}
//mqtt連線封裝函式
void connect()
{
  while (!client.connect("car-client"))
  {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("\nconnected!");
}

void setup()
{
  client.begin("***.***.***.***", net);
  client.onMessage(messageReceived);
  connect();
}
void loop()
{
  //mqtt訊息處理
  client.loop();

  if (!client.connected())
  {
    connect();
  }
  //控制核心邏輯
  relayOnStatus();
}

Java 伺服器端開發

可以說是一箇中轉,可以不要,只是可以避免控制端直接在ESP32端訂閱的主題中直接釋出控制命令;

引入依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-mqtt</artifactId>
            <version>5.3.2.RELEASE</version>
        </dependency>

MQTT Client工廠

小聲bb: copy來的

package cn.b0x0.carserver.common.factory;

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;

public class MqttFactory {

    private static MqttClient client;

    /**
     *   獲取客戶端例項
     *   單例模式, 存在則返回, 不存在則初始化
     */
    public static MqttClient getInstance() {
        if (client == null) {
            init();
        }
        return client;
    }

    /**
     *   初始化客戶端
     */
    public static void init() {
        try {
            client = new MqttClient("tcp://***.***.***.***:1883", "car-****-" + System.currentTimeMillis());
            // MQTT配置物件
            MqttConnectOptions options = new MqttConnectOptions();
            // 設定自動重連, 其它具體引數可以檢視MqttConnectOptions
            options.setAutomaticReconnect(true);
            if (!client.isConnected()) {
                client.connect(options);
            }
        } catch (MqttException e) {
            throw new RuntimeException("MQTT: 連線訊息伺服器失敗");
        }
    }

}

MQTT Util

package cn.b0x0.carserver.common.util;

import cn.b0x0.carserver.common.factory.MqttFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.nio.charset.StandardCharsets;

public class MqttUtil {

    /**
     *   傳送訊息
     *   @param topic 主題
     *   @param data 訊息內容
     */
    public static void send(String topic, Object data) {
        // 獲取客戶端例項
        MqttClient client = MqttFactory.getInstance();
        ObjectMapper mapper = new ObjectMapper();
        try {
            // 轉換訊息為json字串
            String json = mapper.writeValueAsString(data);
            MqttMessage message = new MqttMessage(json.getBytes(StandardCharsets.UTF_8));
            //小車控制要求,訊息級別固定2
            message.setQos(2);
            client.publish(topic, message);
        } catch (JsonProcessingException | MqttException ignored) {
        }
    }

    /**
     * 訂閱主題
     * @param topic 主題
     * @param listener 訊息監聽處理器
     */
    public static void subscribe(String topic, IMqttMessageListener listener) {
        MqttClient client = MqttFactory.getInstance();
        try {
            client.subscribe(topic, listener);
        } catch (MqttException ignored) {
        }
    }

}

Controller

簡單點

package cn.b0x0.carserver.controller;

import cn.b0x0.carserver.common.util.MqttUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
public class CarControlController {
    @RequestMapping("/all/run")
    public String run(){
        MqttUtil.send("car-client","run");
        return "success";
    }
    @RequestMapping("/all/stop")
    public String stop(){
        MqttUtil.send("car-client","stop");
        return "success";
    }
    @RequestMapping("/all/back")
    public String back(){
        MqttUtil.send("car-client","back");
        return "success";
    }
    @RequestMapping("/turn/left/start")
    public String leftStart(){
        MqttUtil.send("car-client","leftStart");
        return "success";
    }
    @RequestMapping("/turn/right/start")
    public String rightStart(){
        MqttUtil.send("car-client","rightStart");
        return "success";
    }
    @RequestMapping("/turn/left/stop")
    public String leftStop(){
        MqttUtil.send("car-client","leftStop");
        return "success";
    }
    @RequestMapping("/turn/right/stop")
    public String rightStop(){
        MqttUtil.send("car-client","rightStop");
        return "success";
    }
}

控制端開發

使用的web頁面進行控制,主要是跨平臺,因為我不會寫IOSApp這些。

之前web頁面是要在ESP32執行的,所以基本都使用了原生JS,現在沒這個必要了

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>ESP32 WebController</title>
	</head>
	<script src="https://unpkg.com/[email protected]/dist/mqtt.min.js"></script>
	<style>
		* {
			-webkit-touch-callout: none;
			-webkit-user-select: none;
			-khtml-user-select: none;
			-moz-user-select: none;
			-ms-user-select: none;
			user-select: none;
		}

		/* ========================== */
		body {
			height: 100vh;
			width: 100%;
		}

		#title {
			flex-grow: 4;
			width: 100%;

			display: flex;
			flex-direction: row;
		}

		.cam-but-left {
			display: flex;
			flex-direction: column;
			flex-grow: 1;
			padding: 10px;
		}

		.cam-but-right {
			flex-grow: 1;
			display: flex;
			flex-direction: row;
		}

		.cam-video {
			flex-grow: 5;
			background-color: #8b8b8b;
		}
		.cam-but{
			background-color: #dedede;
			margin: 5px; 
			height: 100%;
			width: 100%;
		}

		.controller-content {
			height: 100%;
			width: 100%;
			display: flex;
			display: -webkit-flex;
			flex-direction: column;
			justify-content: flex-start;
			align-items: center;
		}

		#controller-but {
			width: 100%;
			flex-grow: 2;
			display: flex;
			display: -webkit-flex;
			flex-direction: row;
			justify-content: center;
			align-items: center;
		}

		#runAndBack {
			height: 100%;
			width: 100%;
			display: flex;
			flex-direction: column;
		}

		#leftAndRight {
			height: 100%;
			width: 100%;
			display: flex;
			display: -webkit-flex;
			flex-direction: row;
		}

		#run {
			/* border-left: 385px  solid transparent;
			    border-right: 385px  solid transparent;
			    border-bottom: 350px solid #d9d9d9;
			    background-color: #dedede; */
			background-color: #dedede;
			height: 100%;
			/* width: 100%; */
			margin: 10px;
		}

		#back {
			background-color: #dedede;
			height: 100%;
			margin: 10px;
		}

		#left {
			background-color: #dedede;
			height: 100%;
			width: 100%;
			margin: 10px;
			margin-right: 0px;
		}
		#right {
			background-color: #dedede;
			height: 100%;
			width: 100%;
			margin: 10px;
		}

		#right:active {
			background-color: #d5d5d5;
		}

		.controller-but {
			border-radius: 5px;
		}
	</style>
	<body>

		<div class="controller-content">
			<div id="title">
				<!-- <h1>Web Controller</h1>
				控制按鈕功能只可意會不可言傳 -->
				<div class="cam-but-left">
					<div class="cam-but" id="cam-but-left-up">

					</div>
					<div class="cam-but" id="cam-but-left-down">

					</div>
				</div>
				<div class="cam-video">
				</div>
				<div class="cam-but-right">
					<div class="cam-but" id="cam-but-right-left">

					</div>
					<div class="cam-but" id="cam-but-right-right">

					</div>
				</div>
			</div>
			<div id="controller-but">
				<div id="runAndBack">
					<div id="run" class="controller-but"></div>
					<div id="back" class="controller-but"></div>
				</div>
				<div id="leftAndRight">
					<div id="left" class="controller-but"></div>
					<div id="right" class="controller-but"></div>
				</div>
			</div>

		</div>
	</body>
	<script type="text/javascript">
	const options = {
	      // 認證資訊
	      clientId: 'car-***-****'
	}
	const client = mqtt.connect('ws://**.**.**.**:8083/mqtt', options);
	client.subscribe('car-cam-images-view');
	client.on('message', function (topic, message) {
		//在這裡處理	
		var p1 = message.toString();
		console.log(p1);
	})
	client.on('reconnect', (error) => {
	    console.log('正在重連:', error)
	})
	client.on('connect', (error) => {
	    console.log('連線成功:', error)
	})
	client.on('error', (error) => {
	    console.log('連線失敗:', error)
	})
		function createXHR() {
			if (typeof XMLHttpRequest != "undefined") {
				return new XMLHttpRequest();
			} else if (typeof ActiveXObject != "undefined") {
				if (typeof arguments.callee.activeXString != "string") {
					var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"],
						i, len;
					for (i = 0, len = versions.length; i < len; i++) {
						try {
							new ActiveXObject(versions[i]);
							arguments.callee.activeXString = versions[i];
							break;
						} catch (ex) {
							//跳過
						}
					}
				}
				return new ActiveXObject(arguments.callee.activeXString);
			} else {
				throw new Error("No XHR object available.");
			}
		}

		function send(url) {
			var xhr = createXHR();
			xhr.onreadystatechange = function() {
				if (xhr.readyState == 4) {
					if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
						console.log(xhr.responseText);
					} else {
						console.log("Request was unsuccessful: " + xhr.status);
					}
				}
			};
			xhr.open("get", "http://iot.b0x0.cn/"+url, true);
			xhr.send(null);
		}
		var runBut = document.getElementById("run");
		var backBut = document.getElementById("back");
		var leftBut = document.getElementById("left");
		var rightBut = document.getElementById("right");
		
		var camLeftUp = document.getElementById("cam-but-left-up");
		var camLeftDown = document.getElementById("cam-but-left-down");
		var camRightLeft = document.getElementById("cam-but-right-left");
		var camRightRight = document.getElementById("cam-but-right-right");
		camLeftUp.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("cam/left/up");
			runBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("camLeftUp touchstart");
		});
		camLeftDown.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("cam/left/down");
			runBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("camLeftDown touchstart");
		});
		camRightLeft.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("cam/right/left");
			runBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("camRightLeft touchstart");
		});
		camRightRight.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("cam/right/right");
			runBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("camRightRight touchstart");
		});
		
		runBut.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("all/run");
			runBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("runBut touchstart");
		});
		leftBut.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("turn/left/start");
			leftBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("leftBut touchstart");
		});
		backBut.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("all/back");
			backBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("backBut touchstart");
		});
		rightBut.addEventListener("touchstart", function(event) {
			event.preventDefault();
			send("turn/right/start");
			rightBut.style.cssText = 'background-color: #d5d5d5;'
			console.log("rightBut touchstart");
		});

		runBut.addEventListener("touchend", function() {
			send("all/stop");
			runBut.style.cssText = 'background-color: #dedede;'
			console.log("runBut touchend");
		});
		leftBut.addEventListener("touchend", function() {
			send("turn/left/stop");
			leftBut.style.cssText = 'background-color: #dedede;'
			console.log("leftBut touchend");
		});
		backBut.addEventListener("touchend", function() {
			send("all/stop");
			backBut.style.cssText = 'background-color: #dedede;'
			console.log("backBut touchend");
		});
		rightBut.addEventListener("touchend", function() {
			send("turn/right/stop");
			rightBut.style.cssText = 'background-color: #dedede;'
			console.log("rightBut touchend");
		});
		camLeftUp.addEventListener("touchend", function() {
			send("cam/left/stop");
			rightBut.style.cssText = 'background-color: #dedede;'
			console.log("camLeftUp touchend");
		});
		camLeftDown.addEventListener("touchend", function() {
			send("cam/left/stop");
			rightBut.style.cssText = 'background-color: #dedede;'
			console.log("camLeftDown touchend");
		});
		camRightLeft.addEventListener("touchend", function() {
			send("cam/right/stop");
			rightBut.style.cssText = 'background-color: #dedede;'
			console.log("camRightLeft touchend");
		});
		camRightRight.addEventListener("touchend", function() {
			send("cam/right/stop");
			rightBut.style.cssText = 'background-color: #dedede;'
			console.log("camRightRight touchend");
		});
	</script>
</html>

麻了,複製程式碼複製麻了

待我新版本搞好,到時候用git分享這些。

因為公司在用其他的,家裡電腦剛換沒多久,都沒裝git相關的東西,麻了我都