基于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——用电器——电源负极

OVEQQ4J_C`6@5VU_W_0ZGKB.png

鬼知道我经历了什么,问了学这个专业朋友都表示”我没用过“,淦哦

核心控制

在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端订阅的主题中直接发布控制命令;

引入依赖


            org.springframework.boot
            spring-boot-configuration-processor
            true
        
        
            org.springframework.integration
            spring-integration-mqtt
            5.3.2.RELEASE
        

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,现在没这个必要了



	
		
		ESP32 WebController
	
	
	
	

		

麻了,复制代码复制麻了

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

因为公司在用其他的,家里电脑刚换没多久,都没装git相关的东西,麻了我都