摸鱼成果展示

一直摸鱼一直爽,直到有一天我发现我的课设离ddl就剩两天了,瞬间清醒了!

半天买材料,半天撸代码,然后再花半天组装一下写个报告,哈哈哈还剩半天时间摸个鱼,优化代码那是不可能的,不如给我这个万年不更新的长草站灌点水好了!

开题给的项目是造一个车,后来嘎嘎造出来之后老师认为太水不给过,必须得加个什么东西,然后就加了个口罩贩卖机在上面,本质上还是完全没区别。。。。

这个项目更倾向于是一个想法,话不多说,先上个成品 这就完全就不能算成品好吗!!!

项目完全开源,下面是仓库地址,需要自取即可!

Github : https://github.com/techfens/esp32mqttcar

Gitee : https://gitee.com/techfens/esp32mqttcar

整体流程

webconsolefc

shopconsolefc

硬件选择

我主要是学嵌入式Linux的,但是做个车也上Linux开发板属实不明智,毕竟最便宜的Linux开发板也要上三位数。

将图吧精神发扬光大,对于本次硬件的选择,我就两个原则:

  • 能用就行,极致的省钱!!!
  • 会用就行,极速的开发!!!

因此在硬件选择上,直接选择MCU裸机开发就完事了,本次硬件核心就是ESP32+L298N,都是究极无敌成熟的方案,有一点点数电基础就能玩。

ESP32-CAM

ESP32-CAM是一个很常见的摄像头模块,本质上是一个ESP32+OV2640摄像头

外观

不出意外的话都是长这个样子:

ESP32CAM

从理论上来说,最好买原厂安信可的板子,但是官方已经下架了,原价60+左右,质量有保证。淘宝上20几块钱的ESP32-CAM全是寨板,质量比较差,做工粗糙,但是能用。这次项目我用的就是寨版。

OV2640

I/O引脚

为了进一步压缩成本,我直接把主控也砍了,毕竟ESP32-CAM本质上也是个ESP32,就没必要再买一个了。ESP32-CAM所有I/O引脚都已经被我霍霍光了,真正的物尽其用!

ESP32CAMGPIO

其中GPIO 2/12/13/14/15支持PWM输出因此被用于电机驱动相关的设计中,GPIO 4也支持PWM输出,是本设计中灯光(开发板自带)的控制口。GPIO 1/3 做为串行通讯口与电脑进行通信,主要用作开发调试使用,在极致的压榨情况下也可以当I/O口用,但是不方便调试。其他I/O引脚不建议用,容易出事(比如GPIO 0)。

另外的,GPIO 33是一个比较特殊的口,是用来控制主板上的DEBUG灯的,是一盏红色的小灯,控制逻辑与其他LED相反。

接线

着重警告,请务必尊重就近接地,共用接地的原则,如果你使用3.3v供电,请使用3.3v供电的接地,如果你使用5v供电,请使用5v供电设计,网上的接线图很多都是错的,纯纯误导小白,比如下面这个:(并无恶意,只是提醒)

图源CSDN:https://blog.csdn.net/yunddun/article/details/114193859

esp32powwrong

也不是说不能用,或许正版安信可原厂的板子是可以的,但是如果你买的寨版,他没给你设置好接地共用,那你等一万年也烧不进去。

说一句题外话,能用5v尽量就不要用3.3v,电压越高驱动能力越好,实际上他的5v是直连的,理论上来说可以给到12v以内都可以。

再说一句题外话,能不用下载底板就不要用,USB-TTL才是原汁原味,保证不出问题!

L298N

L298N,是一款接受高电压的电机驱动器,直流电机和步进电机都可以驱动。一片驱动芯片可同时控制两个直流减速电机做不同动作,在6V到46V的电压范围内,提供2安培的电流,并且具有过热自断和反馈检测功能。L298N可对电机进行直接控制,通过主控芯片的I/O输入对其控制电平进行设定,就可为电机进行正转反转驱动,操作简单、稳定性好,可以满足直流电机的大电流驱动条件。

L298N

我买大概7块钱左右一个,我买的是普通板,有一个散热片。你也可以进一步压榨成本买mini板。

img

直流减速电机

我使用的减速电机减速比为1:48,采用直拉双轴减速马达,重量70g,有强磁带扛干扰设计,扭矩大,适合用在有一定负载的小车上做驱动使用。淘宝连电机带轮子7块钱一套左右。

一点小提示:不要用太粗的线,这样不管你后续接电机还是接L298N都很痛苦,杜邦线那种线径已经完全够用了,你买个0.5mm的红黑线纯属折腾自己。

img

双供电设计

在本项目中,一共用到两个电源,一路是稳压5V电源,一路是电机驱动电源。直流电机的启动会导致瞬时电流需求很大,而且电机要求的电压与ESP32不同,如果将电机驱动电源直接与ESP32连接起来,会造成很大的波动和干扰。因此比较简单的解决办法是将电源隔离。

ESP32POWFC

在5V稳压电源中,我选择的是HW-131电源模块。这是一个很常用的面包板调试电源模块,适用于MB102等大部分面包板。HW-131的输入电压DC 6.5~12V,或者采用USB供电,最大输出电流小于700MA,可以同时输出两路5V/3.3V电源。我自己采用的方案是充电宝+USB输出。

选择HW-131电源模块是因为本项目中有两个地方需要5V输出,一个是ESP32,一个是售货机电机驱动。ESP32可以通过3.3V或者5V供电,这里选择5V供电,因为摄像头模块对电压要求较高。同时,售货机驱动也使用5V供电,因为售货机相对来说并不是很经常使用,是常闭状态,因此和ESP32公用一个电源是可以接受的。我自己采用的方案是充电宝+USB输出,因为充电宝本身也具有电压稳压输出功能,因此比较适合驱动对电压要求高的项目。实际上是为了省一个电池钱

HW-131

另一路用到是两个18650串连,电压7.6~8.2v之间,买一个2槽18650盒子就好了,不要买尖头电池,大部分电池盒都是只能用平头电池!最好买带开关的18650电池盒!

开发环境

ESP32

ESP32开发环境比较多选择,主流的有ESP-IDF原生开发(基于C/C++),platfromIO(基于C/C++),Arduino(基于C/C++),Eclpse(基于Java),MircoPython(基于Python)等。综合社区支持以及个人技术栈等因素,我们最终选择了MircoPython作为开发环境。MircoPython相比其他环境最大的特点就是代码免编译,支持面向对象开发,同时语法相对简单,缺点是虽然性能释放远不如C/C++的好,这也是Python这种解释型语言的通病,但是对于本项目来说完全是足够的。

对于MircoPython,有很多IDE 都支持,比较出名的有Upycraft,Upyloader,Thonny等。本次项目开发主要是使用Thonny开发,基于windows 10 使用。

image-20221110145106940

好了,客套话说完了,实际上总结下来就是一句:人生苦短,我用python。

如果你想认真学习MCU开发,请不要走偏门,老老实实用C++吧!推荐使用platfromIO,这是一个基于vscode的插件,支持完善,代码提示友好,谁用谁知道。

原版ESP32的MicroPython底包没有Camera依赖,请务必使用我提供的固件!

原版ESP32的MicroPython底包没有Camera依赖,请务必使用我提供的固件!

原版ESP32的MicroPython底包没有Camera依赖,请务必使用我提供的固件!

MQTT协议

MQTT协议是专门为物联网设备打造的通讯协议,本质上是TCP协议中在应用层的一种实现。是一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在机器与机器(M2M)通信,物联网(IoT)等方面有较广泛的应用。

mqttfc

对于应用层物联网的协议,并不只有MQTT协议,还有基于UDP协议CoAP协议,基于XMP标记语言的XMPP协议等,或者直接采用HTTP轮询的方式也可以实现类似的效果,但是综合了可靠性,实时性,易开发性等特点,最终选择以MQTT协议为核心通讯协议。关于MQTT协议和其他流行的物联网协议选择,可以参考IEEE这篇文章,写的很详细(可能需要科学):https://ieeexplore.ieee.org/abstract/document/8088251

你可能会好奇,普通的websocket不就可以实现遥控功能吗,绕这么一大圈是为了啥?

事实上websocket是针对长连接环境的,物联网的环境没有这么可靠,很难一直保持良好的长连接。MQTT实际上也是websoket发展来的7层协议(我自己理解的,不一定对),具体可以参考下面的层级图:

mqttcengji

使用MQTT最大的好处就是可以在不可靠的环境下提供可靠的连接,有点我全都要的感觉。而且MQTT协议很简洁,非常方便二次开发。

服务器上我选择用EMQX搭建MQTT环境,网上有很多搭建教程,非常简单。值得注意的是,EMQX对于websocket的支持使用的是路径挂载,如果你需要使用websocket服务连接MQTT服务器,请不要直连8083端口或者1883端口,要使用挂载点的形式访问。比如http://xxx.xxx.xxx.xxx:8083/mqtt

mqttserver

当然,如果你没有服务器,或者完全不想花钱,你也可以使用公共免费的MQTT服务器,比如以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
然也物联 (国内)
官网地址:http://www.ranye-iot.net
MQTT服务器地址:test.ranye-iot.net
TCP 端口:1883
TCP/TLS 端口:8883

EMQ(国内)
MQTT服务器地址:broker-cn.emqx.io
TCP 端口:1883
WebSocket 端口:8083
SSL/TLS 端口:8883
WebSocket Secure 端口:8084

Mosquitto (国外)
官网地址:http://www.mosquitto.org
MQTT服务器地址:test.mosquitto.org
TCP 端口:1883
TCP/TLS 端口:8883
WebSockets 端口:8080
Websocket/TLS 端口:8081

HiveMQ (国外)
官网地址:https://www.hivemq.com
MQTT服务器地址:broker.hivemq.com
TCP 端口:1883
WebSockets 端口:8000

如果你使用公共云,请务必使用有一定复杂度的Client ID,最好遵循命名规则,可以采用user/project/…的形式,举个栗子:zhishixuebao/esp32car/led,zhishixuebao/esp32car/car,zhishixuebao/esp32car/shop。这样也有一个好处,如果你的客户端支持,你可以通过订阅zhishixuebao/esp32car/*来监听整一个项目下的消息了。

服务器环境

由于小车完全使用4G进行远程遥控,因此远端服务器的支持是必不可少的。服务器上主要运行的程序有MQTT通信协议(用于小车控制),websocket服务(用于远程图传),API服务(用于服务对接)。

本项目用到的服务器是一台位于上海的KVM架构虚拟云服务器(腾讯云),运行的系统是Linux CentOS 7.6。虽然地域位于上海,但是因为有公网IP,在4G网络环境下直连实测下来延迟依然非常不错,在操作层面可以做到几乎与本地通讯相同的体验。

我个人并不推荐宝塔装环境,因为不是那么安全。但是如果你不想折腾只想耍一耍,那我还是很推荐用宝塔的。

esp32server

上位机环境

考虑到上位机更多是给用户使用的,因此用户友好的界面是必不可少的,不能只依赖串口通信的日志和控制台进行生产环境的控制。为了尽可能的适应大部分设备的使用环境,选择了Web作为上位机界面开发,不仅有良好的跨设备适用性,同时也能保证大部分人的使用体验是一致的。

最初的想法是使用动态框架进行开发,这样不仅开发迅速,而且技术栈一致(比如一开始计划用Python的Flask框架开发,可以让整一个项目都处于基于Python的技术内),但是考虑到动态框架依赖环境,迁移环境复杂,而且无法脱离服务器使用,最后使用了纯静态纯前端实现。这样的好处是稳定性极高,不依赖任何环境,缺点是拓展性较差,难以实现复杂的逻辑控制功能。

本项目所构建的两个上位机程序都是使用Vue.js和MQTT.js开发。区别在于前端样式的不同,请求和接收数据的方式是相同的。Vue.js是目前前端开发中一个非常流行的框架,不管是小程序,APP还是网页都可以开发。在本项目中主要用到Vue.js中两个比较重要的功能,分别是双向数据绑定以及数据代理。通过Vue.js可以很轻松的实现按钮操作和输入框的绑定,并将数据通过MQTT.js发送到ESP32上。

mqttvue

具体实现

ESP32

执行逻辑

在MicroPython运行环境下,ESP32从上电开机开始执行文件的顺序如下(以本项目为例子):

ESP32BOOT

其中,boot.py是启动文件,开机默认执行,一般用于初始化项目。本项目采用默认配置启动,因此没有修改boot.py文件。

在启动后会默认进入main.py文件,main.py是主文件,用于控制主要的运行逻辑,线程处理,初始化处理等,并调用其他库的对象和方法。所有的程序最终都会汇集到主程序被调用,包括系统固件依赖库,以及本项目需要的其他文件。

固件依赖是指常用的依赖包已经在固件中依赖好,属于系统自带的函数库。本项目用到的系统函数库有:usocket,ustruct,ubinascii,machine,time,network,camera,ujson。其中usocket,ustruct是负责处理MQTT消息的依赖库,ubinascii是负责处理与ASCII码字符转义相关的依赖库,machine是定义引脚电平输出、PWM输出和系统控制的依赖库,network是负责网络配置的依赖库,time是负责定时器相关的依赖库,camera是负责摄像头模块相关的依赖库,ujson是处理json数据的依赖库。

在MicroPython中,很多包都是阉割的,因此都用的是u+包名,比如usocket,ujson。如果你使用了标准包,比如import json ,并不会报错,但是会执行出一堆你不知道发生了什么的东西,个人不建议使用标准包。

除了固件依赖外,还有mqtt.py,control.py两个文件,是本项目中需要自己编写的对象,将在下面说明。

main.py

在main文件中,主要执行ESP32的初始化和控制流程,初始化包括摄像头初始化,网络配置初始化,MQTT通信初始化等。控制流程主要是调用mqtt.py库接收MQTT消息,并通过消息的内容调用control.py内的方法驱动小车硬件。同时具有抓取错误,日志输出,机器保活等功能。以下是一些核心代码,关键部分已使用注释说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import time
import machine
import network
from mqtt import MQTTClient
import socket
import camera
import ujson
import control

lightvalue = 55 # 初始化灯光亮度
speedvalue = 50 # 初始化速度控制

def do_connect(): # 初始化网络
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('connecting to network...')
wlan.connect('WIFI_SSID', 'PASSWORD')
i = 1
while not wlan.isconnected():
print("正在链接...{}".format(i))
i += 1
time.sleep(1)
print('network config:', wlan.ifconfig())

def sub_cb(topic, msg): # 回调函数,收到服务器消息后会调用这个函数
print(topic, msg)
global lightvalue
global speedvalue
if topic.decode("utf-8") == "carctl":
try:
setvalue = ujson.loads(msg)
if "lightvalue" in setvalue:
lightvalue = setvalue["lightvalue"]
print(lightvalue, "灯光亮度")
control.led(lightvalue)
elif "speedvalue" in setvalue:
speedvalue = setvalue["speedvalue"]
print(speedvalue, "速度控制")
except:
pass
if msg.decode("utf-8") == "forward":
control.forward(speedvalue)
elif msg.decode("utf-8") == "back":
control.back(speedvalue)
elif msg.decode("utf-8") == "left":
control.left(speedvalue)
elif msg.decode("utf-8") == "right":
control.right(speedvalue)
elif msg.decode("utf-8") == "stop":
control.stop()
elif msg.decode("utf-8") == "paysuccess":
control.paysuccess()

def connect():
# 1. 联网
do_connect()
# 2. 实例化MQTT对象
c = MQTTClient("my_esp32cam", "150.158.214.32") # 建立一个MQTT客户端
c.set_callback(sub_cb) # 设置回调函数
c.connect() # 建立连接
c.subscribe(b"carctl") # 监控ledctl这个通道,接收控制命令
return c
c = connect()

try: # 初始化摄像头
camera.init(0, format=camera.JPEG)
except Exception as e:
camera.deinit()
camera.init(0, format=camera.JPEG)
# 其他设置:
camera.flip(0) # 上翻下翻
camera.mirror(1) # 左/右
camera.framesize(camera.FRAME_HVGA) # 分辨率
camera.speffect(camera.EFFECT_NONE) # 特效
camera.whitebalance(camera.WB_HOME) # 白平衡
camera.saturation(0) # 饱和
camera.brightness(0) # 亮度
camera.contrast(0) # 对比度
camera.quality(10) # 质量

# socket UDP 的创建
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)

def main(): # 主函数入口
i = 0
while True:
c.check_msg()
buf = camera.capture() # 获取图像数据
s.sendto(buf, ("xxx.xxx.xxx.xxx", 5904)) # 向服务器发送图像数据
i += 1
if i >= 30:
if c.ping() is None:
print("alive")
i = 0

try: # 错误执行并重载
main()
except OSError as e:
print('Failed to connect to MQTT broker. Reconnecting...')
time.sleep(1)
machine.reset()
do_connect()
time.sleep(5)
main()
finally:
camera.deinit()

mqtt.py

这是一个专门处理MQTT消息的方法,包括实例化网络对象,创建socket服务,定义端口,判断MQTT消息类型,心跳保持和遗嘱消息等。主函数通过调用该文件下的MQTTClient类,执行与MQTT消息相关的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import usocket as socket
import ustruct as struct
from ubinascii import hexlify

class MQTTException(Exception):
pass
class MQTTClient: # MQTT对象默认配置
def __init__(
self,
client_id,
server,
port=0,
user=None,
password=None,
keepalive=0,
ssl=False,
ssl_params={},
):
if port == 0: # 如果你的服务器端口不是这两个 请修改
port = 8883 if ssl else 1883
self.client_id = client_id
self.sock = None
self.server = server
self.port = port
self.ssl = ssl
self.ssl_params = ssl_params
self.pid = 0
self.cb = None
self.user = user
self.pswd = password
self.keepalive = keepalive
self.lw_topic = None
self.lw_msg = None
self.lw_qos = 0
self.lw_retain = False

def _send_str(self, s):
self.sock.write(struct.pack("!H", len(s)))
self.sock.write(s)

def _recv_len(self):
n = 0
sh = 0
while 1:
b = self.sock.read(1)[0]
n |= (b & 0x7F) << sh
if not b & 0x80:
return n
sh += 7

def set_callback(self, f):
self.cb = f

def set_last_will(self, topic, msg, retain=False, qos=0):
assert 0 <= qos <= 2
assert topic
self.lw_topic = topic
self.lw_msg = msg
self.lw_qos = qos
self.lw_retain = retain

def connect(self, clean_session=True):
self.sock = socket.socket()
addr = socket.getaddrinfo(self.server, self.port)[0][-1]
self.sock.connect(addr)
if self.ssl:
import ussl

self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
premsg = bytearray(b"\x10\0\0\0\0\0")
msg = bytearray(b"\x04MQTT\x04\x02\0\0")

sz = 10 + 2 + len(self.client_id)
msg[6] = clean_session << 1
if self.user is not None:
sz += 2 + len(self.user) + 2 + len(self.pswd)
msg[6] |= 0xC0
if self.keepalive:
assert self.keepalive < 65536
msg[7] |= self.keepalive >> 8
msg[8] |= self.keepalive & 0x00FF
if self.lw_topic:
sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
msg[6] |= self.lw_retain << 5

i = 1
while sz > 0x7F:
premsg[i] = (sz & 0x7F) | 0x80
sz >>= 7
i += 1
premsg[i] = sz

self.sock.write(premsg, i + 2)
self.sock.write(msg)
# print(hex(len(msg)), hexlify(msg, ":"))
self._send_str(self.client_id)
if self.lw_topic:
self._send_str(self.lw_topic)
self._send_str(self.lw_msg)
if self.user is not None:
self._send_str(self.user)
self._send_str(self.pswd)
resp = self.sock.read(4)
assert resp[0] == 0x20 and resp[1] == 0x02
if resp[3] != 0:
raise MQTTException(resp[3])
return resp[2] & 1

def disconnect(self):
self.sock.write(b"\xe0\0")
self.sock.close()

def ping(self):
self.sock.write(b"\xc0\0")

def publish(self, topic, msg, retain=False, qos=0):
pkt = bytearray(b"\x30\0\0\0")
pkt[0] |= qos << 1 | retain
sz = 2 + len(topic) + len(msg)
if qos > 0:
sz += 2
assert sz < 2097152
i = 1
while sz > 0x7F:
pkt[i] = (sz & 0x7F) | 0x80
sz >>= 7
i += 1
pkt[i] = sz
# print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt, i + 1)
self._send_str(topic)
if qos > 0:
self.pid += 1
pid = self.pid
struct.pack_into("!H", pkt, 0, pid)
self.sock.write(pkt, 2)
self.sock.write(msg)
if qos == 1:
while 1:
op = self.wait_msg()
if op == 0x40:
sz = self.sock.read(1)
assert sz == b"\x02"
rcv_pid = self.sock.read(2)
rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
if pid == rcv_pid:
return
elif qos == 2:
assert 0

def subscribe(self, topic, qos=0):
assert self.cb is not None, "Subscribe callback is not set"
pkt = bytearray(b"\x82\0\0\0")
self.pid += 1
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
# print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt)
self._send_str(topic)
self.sock.write(qos.to_bytes(1, "little"))
while 1:
op = self.wait_msg()
if op == 0x90:
resp = self.sock.read(4)
# print(resp)
assert resp[1] == pkt[2] and resp[2] == pkt[3]
if resp[3] == 0x80:
raise MQTTException(resp[3])
return

def wait_msg(self): #等待单个传入的MQTT消息并对其进行处理。
res = self.sock.read(1)
self.sock.setblocking(True)
if res is None:
return None
if res == b"":
raise OSError(-1)
if res == b"\xd0": # PINGRESP
sz = self.sock.read(1)[0]
assert sz == 0
return None
op = res[0]
if op & 0xF0 != 0x30:
return op
sz = self._recv_len()
topic_len = self.sock.read(2)
topic_len = (topic_len[0] << 8) | topic_len[1]
topic = self.sock.read(topic_len)
sz -= topic_len + 2
if op & 6:
pid = self.sock.read(2)
pid = pid[0] << 8 | pid[1]
sz -= 2
msg = self.sock.read(sz)
self.cb(topic, msg)
if op & 6 == 2:
pkt = bytearray(b"\x40\x02\0\0")
struct.pack_into("!H", pkt, 2, pid)
self.sock.write(pkt)
elif op & 6 == 4:
assert 0

def check_msg(self): #检查服务可用性并等待消息
self.sock.setblocking(False)
return self.wait_msg()

control.py

这是一个专门用于控制小车的控制文件,包括定义输出引脚,定义PWM控制波,通过L298N的逻辑IN1~IN4口的电平配置来实现前进后退和制动操作,同时通过PWM波控制使能EN口进行直流电机的调速驱动。同时也驱动商品成功下单后出货的电机流程。

值得一提的是,由于ESP32-CAM提供的I/O接口十分紧缺,导致小车没有多余的接口安装转向舵机,因此使用左右轮差速的方式进行转弯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from machine import Pin, PWM
import time
#定义电平控制引脚和PWM控制引脚
IO12_INT1 = Pin(12, mode=Pin.OUT)
IO13_INT2 = Pin(13, mode=Pin.OUT)
IO14_INT3 = Pin(14, mode=Pin.OUT)
IO15_INT4 = Pin(15, mode=Pin.OUT)
IO02_PWM1 = Pin(2, mode=Pin.OUT)
IO02_PWM1 = PWM(IO02_PWM1, 78125)
#初始化电平控制引脚和PWM控制引脚
IO12_INT1.value(0)
IO13_INT2.value(0)
IO14_INT3.value(0)
IO15_INT4.value(0)
#定义和初始化LED灯引脚,另一种写法,可供参考
led_pwm = PWM(Pin(4))
led_pwm.freq(78125)

def led(lightvalue): #LED
print("light", lightvalue)
led_pwm.duty(lightvalue * 10)

def forward(speedvalue): #前进
print("forward", speedvalue)
IO02_PWM1.duty(500 + speedvalue * 5)
IO12_INT1.value(0)
IO13_INT2.value(1)
IO14_INT3.value(0)
IO15_INT4.value(1)

def back(speedvalue): #后退
print("back", speedvalue)
IO02_PWM1.duty(500 + speedvalue * 5)
IO12_INT1.value(1)
IO13_INT2.value(0)
IO14_INT3.value(1)
IO15_INT4.value(0)

def left(speedvalue): #左转
print("left", speedvalue)
IO02_PWM1.duty(600 + speedvalue)
IO12_INT1.value(0)
IO13_INT2.value(1)
IO14_INT3.value(1)
IO15_INT4.value(0)

def right(speedvalue): #右转
print("right", speedvalue)
IO02_PWM1.duty(600 + speedvalue)
IO12_INT1.value(1)
IO13_INT2.value(0)
IO14_INT3.value(0)
IO15_INT4.value(1)

def stop(): #制动
print("stop")
IO12_INT1.value(0)
IO13_INT2.value(0)
IO14_INT3.value(0)
IO15_INT4.value(0)

def paysuccess(): #付款成功
print("pay success")
for i in range(3)
for i in range(0, 1024):
led_pwm.duty(i)
time.sleep_ms(1)

for i in range(1023, -1, -1):
led_pwm.duty(i)
time.sleep_ms(1)

有一个小细节,因为电机的驱动需要最低电压,因此PWM调速从0电压开始调是没有意义的。举个栗子,在forward函数里,IO02_PWM1.duty(500 + speedvalue * 5),500是驱动的最低电压,PWM占空比在0-1023之间,因此对于电机的调速从500-1000是合理的。如果直接采用speedvalue * 10,那么调速低于50将没有意义。

代码整体风格都是简单粗暴,有时间的话可以优化以下,比如运动中差速转弯之类的。

websocket图传

偷懒找了一个大佬写的项目:https://blog.csdn.net/qq_26700087/article/details/125435597

关于如何使用,大佬有详细的教程,你只需要改一下端口就能用,当然不改也行。

我在这里补充一点点在服务器端部署的小技巧,主要是设置进程防杀。个人非常喜好采用PM2管理器对项目进行监控管理。PM2管理器是开源的基于Node.js的进程管理器,包括守护进程,监控,日志的一整套完整的功能,基本是Node.js应用程序不二的守护进程选择,事实上它并不仅仅可以启动Node.js的程序,也可以守护其他脚本程序(比如本程序),并且带有负载均衡控制,可以实现0秒切换重载服务。

首先你需停安装PM2管理器,不想折腾的话一键命令就行(需要有Node.js环境),或者直接通过宝塔软件商店安装。

1
npm install pm2@latest -g

然后你只需要cd到在run.sh文件所在目录,执行下列命令,即可实现进程保活:

1
2
3
pm2 start ./run.sh
pm2 list
pm2 save

如果你看到你的终端显示online,那么你就成功了:

image-20221110164506293

如果你想比较方便的访问日志,你可以这样启动:

1
2
3
pm2 start ./run.sh -o ./logs/out.log -e ./logs/error.log
pm2 list
pm2 save

这样你的运行日志将直接保存到当前目录下。

如果你想查看日志,只需要输入:

1
pm2 logs run

下面是一些常用PM2命令,可供参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 启动进程/应用
pm2 start bin/www

# 重命名进程/应用
pm2 start app.js --name wb123、

# 添加进程/应用
pm2 start bin/www

# 结束进程/应用
pm2 stop www

# 结束所有进程/应用
pm2 stop all

# 删除进程/应用 pm2
pm2 delete www

# 删除所有进程/应用
pm2 delete all

# 列出所有进程/应用
pm2 list

# 查看某个进程/应用具体情况
pm2 describe www

# 查看进程/应用的资源消耗情况
pm2 monit

# 查看pm2的日志
pm2 logs 序号/名称

# 若要查看某个进程/应用的日志,使用
pm2 logs www

# 重新启动进程/应用
pm2 restart www

# 重新启动所有进程/应用
pm2 restart all

上位机

没有什么比网页作为上位机更省钱的了,成本为0。

主要采用了vue2作为开发,因为验收问题没有办法使用脚手架开发,直接裸跑了,因此前端写的一坨屎,不具备参考价值。

这里只展示一下如何连接MQTT服务器的写法,如果你真的想看看完全的项目代码,直接到git仓库下载就行,开箱即用。

首先引入vue.js和mqtt.js,一个标签就行,详情请参考官网。

vue2官方文档:https://v2.cn.vuejs.org/v2/guide/installation.html

mqtt.js教程:https://www.emqx.com/zh/blog/mqtt-js-tutorial

vue中使用mqtt教程:https://www.emqx.com/zh/blog/how-to-use-mqtt-in-vue

1
2
<script type="text/javascript" src="./js/vue.js"></script>
<script type="text/javascript" src="./js/mqtt.js"></script>

创建一个按钮(前进):

没有设置长按,这里偷懒了,按下去就是前进,抬起来就是刹车。

1
2
<button class="shiny" id="forward" v-on:touchstart="forward"
v-on:touchend="stop">前进 ↑</button>

创建一个输入框(灯光):

1
2
3
4
5
<div class="inputBox">
<input type="number" v-model.number="lightvalue" :max='100' :min='0' required="required">
<span>灯光亮度 0~100%</span>
</div>
<button @click="setLight">提交</button>

暴力清除长按复制默认样式,保证按钮按下去不会自己抬起来:

1
2
3
4
5
6
7
8
9
<style>
* {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>

Vue有专门对按钮事件有详细的定义,这只是个demo,真正开发千万别学我这样写,否则你就会知道什么叫暴毙。

下面是Vue部分,同属于一个script标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const myVue = new Vue({
el: '#app',
data: {
lightvalue: "50",
speedvalue: "55",
carControl: {
status: 0,
client: null,
options: {
url: 'ws://xxx.xxx.xxx.xxx:8083/mqtt',
topic: 'carctl',
connectTimeout: 5000,
clientId: 'connect_all_esp32_mqtt_led_' + new Date().getTime(),
clean: false,
keepAliveInterval: 30
},
},
},
methods: { // 精简了其他方法,大同小异
forward() {
let status = "forward";
this.mqttPublish(status);
},
stop() {
let status = "stop";
this.mqttPublish(status);
},
setLight() {
console.log("set light");
console.log(this.lightvalue)
let status = '{"lightvalue": ' + this.lightvalue + '}'
console.log(status)
this.mqttPublish(status);
alert("确认提交亮度为:" + this.lightvalue + "%");
},
mqttPublish(status) {
// 向指定topic发送消息,topic要保持一致
this.carControl.client.publish(this.carControl.options.topic, status.toString(), {
qos: 1
});
},
mqttConf() {
// 链接mqtt
this.carControl.client = mqtt.connect(this.carControl.options.url, this.carControl.options);
}
},
mounted() {
this.mqttConf();
},
});