基于MQTT协议实现远程控制的"智能"车

智能,但不完全智能

虽然我不觉得这玩意儿有啥智能的,但都这么叫就跟着叫喽。

时隔好几天才写的

其实在写这篇博文的时候我已经在做升级了,并且已经到了中后期阶段了。

主要是业余时间做着玩,看时间了。

规格 & 实拍

所需硬件

……

推荐使用电机驱动模块,或者自己用mos管。

直接使用双路继电器控制的缺点有:

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;

六个接口

这里要注意的是:

比如电源正极接到了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/mqtt@2.18.8/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相关的东西,麻了我都