首先声明本项目由本人与另一位博主合作完成,我们都是大一初学者,文章中或多或少会有一些错误,还望各位前辈指正,废话不多说,直入主题。
目录
一、材料清单
arduino uno开发板 |
L298N电机驱动板 |
双层4WD四驱小车(含直流驱动电机) |
两个18650锂电池和电池盒(别多了) |
机械臂套装 |
若干杜邦线与面包板 |
充电宝 |
二、机械臂篇
(1)机械臂组装
由于购买的商家的不同,机械臂的部分结构也会不一样,组装步骤同样也会不同,所以在此不跟大家讲解组装的步骤,建议大家直接向商家问组装视频,会方便很多, 这里博主用的是一个简单的四轴机械臂,图片如下:
(2)机械臂与arduino开发板的连接
该部分本人是跟着哔哩哔哩up主“太极创客”学习的,连接图如下:
(3)机械臂控制代码
建议根据自己的实际情况酌情修改
//机械臂控制
void servoCmd(char servoName, int toPos, int servoDelay){
Servo servo2go; //创建servo对象
int fromPos; //建立变量,存储电机起始运动角度值
switch(servoName){
case 'b':
toPos = map(toPos, 0, 63, 0, 180);//与之后esp32使用16进制控制有关,之后会提到,之后的也是
if(toPos >= baseMin && toPos <= baseMax){
servo2go = base;
break;
} else {
Serial.println("+Warning: Base Servo Value Out Of Limit!");
return;
}
case 'c':
if(toPos >= clawMin && toPos <= clawMax){
servo2go = claw;
break;
} else {
Serial.println("+Warning: Claw Servo Value Out Of Limit!");
return;
}
case 'f':
toPos = map(toPos, 0, 63, 35, 120);
if(toPos >= fArmMin && toPos <= fArmMax){
servo2go = fArm;
break;
} else {
Serial.println("+Warning: fArm Servo Value Out Of Limit!");
return;
}
case 'r':
toPos = map(toPos, 0, 63, 45, 180);
if(toPos >= rArmMin && toPos <= rArmMax){
servo2go = rArm;
break;
} else {
Serial.println("+Warning: rArm Servo Value Out Of Limit!");
return;
}
}
servo2go.write(toPos);
}
二、小车篇
(1)小车组装
与机械臂处的组装雷同。
(2)小车与开发板连接
首先,我们先利用L298N模块实现电机的正转、反转,从而驱动小车实现前进、后退及转向
(本来打算AFMotor电机驱动板来实现的,但是由于当时没意识到电压问题,用了四个18650锂电池,把电机烧爆了,有空的铁子们可以试一下)
因为只有两个马达输出口,想控制四个轮子,着需要将一边的两个轮子接到同一个输出口,大致如下(图片来自@南渊_):
需要注意的是,若在之后运行时出现同一边轮子转动方向相反,可能是两个电机的接线错位了。
(3)L298N模块科普
(3.1)控制原理
电机 | 运动状态 | IN1 | IN2 | IN3 | IN4 |
电机A | 正转 | 高 | 低 | / | / |
反转 | 低 | 高 | / | / | |
停止 | 低 | 低 | / | / | |
电机B | 正转 | / | / | 高 | 低 |
反转 | / | / | 低 | 高 | |
停止 | / | / | 低 | 低 |
(3.2)直接给arduino开发板供电
将模块的5V供电口接到arduino板的5V接口,同时使arduino的GND与模块GND口相接,可以实现单独供电,也要注意用两块18650锂电池即可,注意不要使用南孚电池,因为南孚电池电流过大会导致arduino开发板不定时启动,而且四个南孚电池的电压都不如两个18650来的实在。
(4)控制小车代码展示
个人建议大家对照的已经连好线路后的车子运行,自己修改一下“HIGH"与“LOW”在各个引脚的情况
int l1=A0;
int l2=A1;
int r1=A2;
int r2=A3;
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
pinMode(r1,OUTPUT);
pinMode(r2,OUTPUT);
pinMode(l1,OUTPUT);
pinMode(l2,OUTPUT);
}
//向前
void forward()
{
digitalWrite(l1,LOW);
digitalWrite(l2,HIGH);
digitalWrite(r1,LOW);
digitalWrite(r2,HIGH);
}
//后退
void back()
{
digitalWrite(l1,HIGH);
digitalWrite(l2,LOW);
digitalWrite(r1,HIGH);
digitalWrite(r2,LOW);
}
//左转
void left()
{
digitalWrite(l1,HIGH);
digitalWrite(l2,LOW);
digitalWrite(r1,LOW);
digitalWrite(r2,HIGH);
delay(250);//为了只转动一定的角度,过0.25秒后停止转动,可以自己再调整
digitalWrite(l1,LOW);
digitalWrite(l2,LOW);
digitalWrite(r1,LOW);
digitalWrite(r2,LOW);
return ;
}
//右转
void right()
{
digitalWrite(l1,LOW);
digitalWrite(l2,HIGH);
digitalWrite(r1,HIGH);
digitalWrite(r2,LOW);
delay(250);
digitalWrite(l1,LOW);
digitalWrite(l2,LOW);
digitalWrite(r1,LOW);
digitalWrite(r2,LOW);
return ;
}
//停止
void _stop()
{
digitalWrite(l1,LOW);
digitalWrite(l2,LOW);
digitalWrite(r1,LOW);
digitalWrite(r2,LOW);
}
四、esp32控制
(1)控制指令
控制小车与机械臂一共有9种指令
(2)mqtt简介
- 基于Publish/Subscribe(发布订阅)模式的物联网通信协议
- 简单易实现
- 支持Qos(服务质量)
- 报文精简
- 基于TCP/IP
(3)mqtt服务端搭建
本次搭建mqtt服务端的服务器系统为Centos7.3
下面为数据传输时的格式(均为二进制)
前进 | 后退 | 左转 | 右转 | 停止 |
00000110 | 00000011 | 00000010 | 00000001 | 00000100 |
大臂 | 小臂 | 底盘 | 钳子 |
010xxxxx | 10xxxxx | 11xxxxx | 00000101 |
1、添加 EPEL 软件库
yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
2、安装 Mosquitto
yum install mosquitto
3、配置Mosquitto
设置用户名和密码
创建一个Mosquitto将用于验证连接的密码文件。使用mosquitto_passwd来创建这个文件
sudo mosquitto_passwd -c /etc/mosquitto/passwd your-username
your-username:你自己设置的账号名
输入指令后,系统要求输入两次密码。
编辑Mosquitto配置文件:
进入目录:/etc/mosquitto,编辑mosquitto.conf,具体内容请参考文件内说明(全英文)。
4、运行Mosquitto
systemctl start mosquitto.service
(4)基于esp32实现mqtt协议
#include <WiFi.h>
#include <PubSubClient.h>
const char* ssid = "wifi名称";
const char* password = "wifi密码";
#define JDQ 16
const char* MQTT_SERVER = "服务器公网IP";
const int MQTT_PORT = 1883; //mqtt服务开放的端口
const char* MQTT_USRNAME = "用户名";
const char* MQTT_PASSWD = "密码";
const char* TOPIC = "car"; //订阅的频道
const char* CLIENT_ID = "scy-mqtt-client"; //当前设备的clientid标志
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
void setup()
{
Serial.begin(9600);
delay(10);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
pinMode(JDQ, OUTPUT);
client.setServer(MQTT_SERVER, MQTT_PORT); //设定MQTT服务器与使用的端口,1883是默认的MQTT端口
client.setCallback(callback); //设定回调方式,当ESP8266收到订阅消息时会调用此方法
}
int value = 0;
void reconnect() {
while (!client.connected()) {
if (client.connect(CLIENT_ID,MQTT_USRNAME,MQTT_PASSWD)) {
// 连接成功时订阅主题
client.subscribe(TOPIC);
} else {
delay(5000);
}
}
}
void callback(char* topic, byte* payload, unsigned int length) {
for (int i = 0; i < length; i++) {
Serial.write(payload[i]); // 通过串口向arduino传递指令
}
}
void loop()
{
if (!client.connected()) {
reconnect();
}
client.loop();
}
(5)基于安卓app实现软件操控并用mqtt协议通信
页面布局如下
xml代码如下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="66dp"
android:layout_height="57dp"
android:layout_marginEnd="68dp"
android:layout_marginBottom="9dp"
android:text="前进"
app:layout_constraintBottom_toTopOf="@+id/button2"
app:layout_constraintEnd_toEndOf="@+id/button3" />
<Button
android:id="@+id/button5"
android:layout_width="66dp"
android:layout_height="57dp"
android:layout_marginStart="104dp"
android:layout_marginBottom="104dp"
android:text="停止"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/button2"
android:layout_width="66dp"
android:layout_height="57dp"
android:layout_marginStart="32dp"
android:layout_marginBottom="104dp"
android:text="左转"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/button3"
android:layout_width="66dp"
android:layout_height="57dp"
android:layout_marginStart="72dp"
android:layout_marginBottom="5dp"
android:text="右转"
app:layout_constraintBottom_toTopOf="@+id/button4"
app:layout_constraintStart_toStartOf="@+id/button4" />
<Button
android:id="@+id/button4"
android:layout_width="66dp"
android:layout_height="57dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:text="后退"
app:layout_constraintStart_toStartOf="@+id/button2"
app:layout_constraintTop_toBottomOf="@+id/button2" />
<Button
android:id="@+id/button11"
android:layout_width="79dp"
android:layout_height="56dp"
android:layout_marginEnd="180dp"
android:layout_marginBottom="220dp"
android:text="关"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/textView12"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="208dp"
android:layout_marginBottom="144dp"
android:text="小臂"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<SeekBar
android:id="@+id/seekBar15"
android:layout_width="269dp"
android:layout_height="30dp"
android:max="63"
android:progress="32"
android:layout_marginEnd="88dp"
android:layout_marginBottom="104dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/textView14"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="208dp"
android:layout_marginBottom="20dp"
android:text="底座"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<SeekBar
android:id="@+id/seekBar16"
android:layout_width="269dp"
android:max="63"
android:progress="32"
android:layout_height="30dp"
android:layout_marginEnd="88dp"
android:layout_marginBottom="164dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<SeekBar
android:id="@+id/seekBar14"
android:layout_width="269dp"
android:max="63"
android:progress="32"
android:layout_height="30dp"
android:layout_marginEnd="88dp"
android:layout_marginBottom="44dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/textView13"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="208dp"
android:layout_marginBottom="80dp"
android:text="大臂"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
本实例使用了 org.eclipse.paho.client.mqttv3-1.2.0.jar 包作为mqtt通讯模块
MainActivity代码如下
package com.example.testapp1;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.Toast;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
private Button Button_forward;
private Button Button_back;
private Button Button_left;
private Button Button_right;
private Button Button_stop;
private Button Button_18dong; //夹子
private SeekBar sb_base;
private SeekBar sb_big;
private SeekBar sb_small;
private Handler handler;
private MqttClient client;
private MqttConnectOptions options;
private ScheduledExecutorService scheduler;
//HashMap<Integer, Integer> sb_value = new HashMap<Integer, Integer>();
@SuppressLint("HandlerLeak")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Toast.makeText(this, "hello sss", Toast.LENGTH_LONG).show();
Object_init();
Mqtt_init();
startReconnect();
// Mqtt_connect();
//Bt(R.id.button);
handler = new Handler() {
@SuppressLint("SetTextI18n")
public void handleMessage(Message msg) {
super.handleMessage(msg);
MqttMessage message = new MqttMessage();
byte[] data = new byte[1];
switch (msg.what){
case 1: //开机校验更新回传
//Toast.makeText(MainActivity.this, String.valueOf(msg.obj), Toast.LENGTH_SHORT).show();
try {
data[0] = (byte) Integer.valueOf(msg.obj.toString()).intValue();
message.setPayload(data);
client.publish("car", message);
} catch (MqttException e) {
Toast.makeText(MainActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
System.out.println(e.toString());
Log.e("aaaaa", e.toString());
throw new RuntimeException(e);
}
break;
case 31:
Toast.makeText(MainActivity.this, String.valueOf(msg.what), Toast.LENGTH_SHORT).show();
break;
case 30:
Toast.makeText(MainActivity.this, String.valueOf(msg.what), Toast.LENGTH_SHORT).show();
break;
}
}
};
}
private void Object_init(){
Bt(R.id.button4);
Bt(R.id.button2);
Bt(R.id.button3);
Bt(R.id.button5);
Bt(R.id.button11);
Sb(R.id.seekBar14);
Sb(R.id.seekBar15);
Sb(R.id.seekBar16);
}
private void Bt(int ID){
Button bt = findViewById(ID);
// setContentView(R.layout.activity_main);
bt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Message msg = new Message();
msg.what = 1;
if (ID == R.id.button){
// publishmessageplus("car","{\"set_led\":1}");
msg.obj = 0b00000110;
}
else if (ID == R.id.button3){
msg.obj = 0b00000001;
}
else if (ID == R.id.button2){
msg.obj = 0b00000010;
}
else if (ID == R.id.button4){
msg.obj = 0b00000011;
}
else if (ID == R.id.button5){
msg.obj = 0b00000100;
}
else if (ID == R.id.button11){
if (bt.getText() == "开"){
bt.setText("关");
}
else{
bt.setText("开");
}
msg.obj = 0b00000101;
}
handler.sendMessage(msg);
}
});
}
private void Sb(int ID){
SeekBar sb = findViewById(ID);
sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
private byte num;
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
num = (byte) i;
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
//Toast.makeText(MainActivity.this, String.valueOf(ID), Toast.LENGTH_SHORT).show();
//System.out.println("hello");
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
Message msg = new Message();
if (ID == R.id.seekBar14){
msg.what = 1;
msg.obj = num | 0b01000000;
}
else if(ID == R.id.seekBar15){
msg.what = 1;
msg.obj = num | 0b10000000;
}
else if(ID == R.id.seekBar16){
msg.what = 1;
msg.obj = num | 0b11000000;
}
handler.sendMessage(msg);
}
});
}
private void Mqtt_init()
{
try {
//host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
client = new MqttClient("tcp://您服务器的公网IP:服务器上mqtt服务所在的端口", String.valueOf(System.currentTimeMillis()),
new MemoryPersistence());
//MQTT的连接设置
options = new MqttConnectOptions();
//设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(false);
//设置连接的用户名
options.setUserName("您的用户名");
//设置连接的密码
options.setPassword("您的密码".toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
//设置回调
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
//连接丢失后,一般在这里面进行重连
System.out.println("connectionLost----------");
//startReconnect();
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
//publish后会执行到这里
System.out.println("deliveryComplete---------"
+ token.isComplete());
}
@Override
public void messageArrived(String topicName, MqttMessage message)
throws Exception {
//subscribe后得到的消息会执行到这里面
System.out.println("messageArrived----------");
Message msg = new Message();
//封装message包
msg.what = 3; //收到消息标志位
msg.obj = topicName + "---" + message.toString();
//发送messge到handler
handler.sendMessage(msg); // hander 回传
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void Mqtt_connect() {
new Thread(new Runnable() {
@Override
public void run() {
try {
if(!(client.isConnected()) ) //如果还未连接
{
client.connect(options);
Message msg = new Message();
msg.what = 31;
// 没有用到obj字段
handler.sendMessage(msg);
}
} catch (Exception e) {
e.printStackTrace();
Message msg = new Message();
msg.what = 30;
// 没有用到obj字段
handler.sendMessage(msg);
}
}
}).start();
}
private void startReconnect() {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (!client.isConnected()) {
Mqtt_connect();
}
}
}, 0 * 1000, 10 * 1000, TimeUnit.MILLISECONDS);
}
private void publishmessageplus(String topic,String message2)
{
if (client == null || !client.isConnected()) {
Toast.makeText(MainActivity.this, "client == null || !client.isConnected()", Toast.LENGTH_SHORT).show();
return;
}
MqttMessage message = new MqttMessage();
message.setPayload(message2.getBytes());
try {
client.publish(topic,message);
} catch (MqttException e) {
Toast.makeText(MainActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
}
最后AndroidManifest.xml文件为
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Testapp1"
tools:targetApi="31">
<service android:name="org.eclipse.paho.android.service.MqttService"/>
<activity
android:name=".MainActivity"
android:screenOrientation="landscape"
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>