成品演示:bilibili - 悄悄的魔法书
代码仓库:github - flying forever 或者 gitee - 清风莫追
文章目录
- 1 引言
- 1.1 课题背景
- 1.2 课题意义
- 1.3 课题目的
- 2 课题相关知识与开发环境
- 3 课题的总体设计
- 4 课题的详细设计与实现
- 4.1 小车物理结构
- 4.1.1 轮子
- 4.1.2 舵机组装
- 4.1.3 轮子安装到舵机
- 4.1.3 防倾倒底盘
- 4.2 运动模块
- 4.2.1 嵌入式硬件介绍
- 4.2.2 单轮独立控制模式
- 4.2.3 双轮控制模式
- 4.2.4 动作序列模式
- 4.3 无线控制
- 4.3.1 网络结构
- 4.3.2 服务器程序搭建
- 4.3.3 客户端设计
- 4.4 带语义理解的语音控制
- 4.4.1 语音输入识别
- 1、WebSpeechAPI
- 2、stt开源语音识别模型
- 4.4.2 多API竞速并发
- 4.4.3 基于大模型的语义理解
- 4.4.4 进一步展望
- 5 课题测试
- 5.1 树莓派本机
- 5.2 语音控制
- 5.3 上手操作
- 6 总结
1 引言
1.1 课题背景
在嵌入式实训课程的要求下,我们需要完成一个和嵌入式硬件搭边的作品设计。鉴于我对stm32不算熟悉,但手里有一块树莓派和一只6自由度机械臂,而且在一个无聊的深夜,我机缘巧合之下用牙签和胶带粘合出了一只轮子,并成功绑定在舵机转轴上。因此我决定,组装一个小车。
早在几个月之前开发web网站时,我就在考虑做语音指令控制的功能。然而当时觉得过于高级,对我技术可行性不高,只是一个妄想,故一直未有尝试。但最近一番折腾之下略有了解,我决定把语音控制功能加到小车上去,变成“智能小车”。
1.2 课题意义
每每看见别人的光彩夺目,便为自己的大学旅途深感羞愧,仿佛光阴虚过,学问毫无,碌碌于一次次考试的应付。然而,理论学习脱离了综合性的实践,它就找不着自己的位置,人就没有自己的方向。
虽然没有涉及动手编写高级的算法,但这是一个综合性较强的作品。涉及技术攘括了我大学以来学习的许多课程与领域:python语言,人工智能,Web后端框架,前端UI与逻辑,计算机网络,Linux操作系统,以及与嵌入式相关的——舵机硬件控制。而将它们融于一体的智能小车,算我交上的一份小小答卷。
1.3 课题目的
实现一个支持客户端按键、语音输入两种控制方式的智能小车。并在制作过程中,体会这些零散的课程与技术,是如何支撑起一个综合性的系统,它们各自作为怎样的角色,相互之间又如何地协作。
2 课题相关知识与开发环境
在后文相应部分也有一些说明,这里做下汇总。
- 硬件-嵌入式材料:树莓派3B,树莓派扩展板,电源,总线舵机两个,舵机总线两根。
- 硬件-DIY材料:牙签、胶带、橡皮筋若干,小木块两个,可乐瓶,水果网套。(并不固定,缺少的材料可自行寻找替代品。
- 软件技术-客户端:html+css+javascript, bootstrap, remixicon图标库。
- 软件技术-服务器-树莓派:python3.7.3,flask,ZLSDK*(众灵科技提供,控制舵机,没有也没关系,可自己想办法向串口发字符串)*
- 软件技术-服务器-语音识别:
- stt开源模型。github开源地址:https://github.com/jianchang512/stt 。(可选的,你完全可以使用其它的语音识别,无论开源模型、网络API接口。)
- 讯飞星火大模型api,官方文档:星火认知大模型服务说明 | 讯飞开放平台文档中心 (xfyun.cn) 。
- 调试工具:(以下所有都不是必需的,理论上cmd命令行就能干)
- Xftp7。用于windows电脑与树莓派之间传输文件。
- Xshell7。ssh远程连接树莓派的命令行终端。
- RealVNC Viewer。连接树莓派图形界面。(众灵科技提供)
- nmap。搜索局域网中的活动设备。
- gpt。人手一个,不懂就问。
- Remote Development(VSCode插件)。远程连接树莓派,然后你可以像在本地写代码一样流畅。
- 相关资料:
- 树莓派AI视觉机械臂 Jibot3-PI 使用手册。(需要看树莓派及其扩展板介绍,以及总线舵机两个部分。这里不便提供文档,如果你买了众灵科技的舵机,自然会有,否则自行寻找舵机相关资料)
3 课题的总体设计
本作品以树莓派和总线舵机为硬件基础,通过手工DIV组装,最终实现的是一个网络远程控制的智能小车。提供了一个手机客户端的操作界面,可以直接控制小车进行前进、后退、转弯等等操作。同时提供了语音控制,让小车完成用户要求的操作。当然,这些操作需要是被预定义好的。
客户端运行在浏览器中,使用html+css设计界面元素及布局,javascrip编写控制逻辑,通过http请求与部署在树莓派上的后端Flask服务器通信,Flask根据请求参数生成舵机指令字符串,控制舵机的转动,舵机带动轮子,以实现小车运动控制。
4 课题的详细设计与实现
4.1 小车物理结构
小车主体的制作材料如下:树莓派3B,总线舵机两个,牙签、胶带、橡皮筋若干,小木块两个,可乐瓶,水果网套。
除了轮子有用到胶带粘合。其它部分都只是通过橡皮筋绑定,未来拆卸也会很方便。通过恰当的绑定方式,它具有“哈尔的移动城堡”一样,稍显混乱却具有弹性恢复力的稳定结构。
4.1.1 轮子
一共是四层结构。
使用牙签骨架,圈上柔软的纸巾胎面。对于轮子的圆度问题,加上了裁剪的塑料瓶。对于抓地力问题,加上了泡沫圈。使用胶带和橡皮筋多层绑定。
4.1.2 舵机组装
每个舵机是单轴的,小车使用两个舵机横向绑定。
使用单纯的橡皮筋力学绑定,缝隙夹住纸块以填充。整体结构具有弹性稳定性和恢复力。
4.1.3 轮子安装到舵机
制作的轮子使用胶带和橡皮筋的双层绑定,固定在舵机的转轴上。
4.1.3 防倾倒底盘
两轮装置必然产生平衡问题。因此在前后加入支撑结构,将牙签用橡皮筋以特殊方式绑在小木块上,受到地面向上支持力时,具有回弹能力。
成品如下。
4.2 运动模块
4.2.1 嵌入式硬件介绍
电子硬件包括:树莓派3B,树莓派扩展板,电源,总线舵机两个,相应总线两根。通过总线将控制板与舵机连上即可。
树莓派介绍
树莓派介绍Raspberry Pi,中文名为“树莓派”,简写为 RPi,或 RasPi/RPI,是一款只有信用卡大小的计算机。它是一款基于 ARM 的微型电脑主板,可连接键盘、鼠标和网线,同时拥有视频模拟信号的电视输出接口和 HDMI 高清视频输出接口。
扩展板介绍
此款树莓派扩展板是由杭州众灵科技有限公司研发的一款集PWM 舵机控制(标准舵机和 9g 小舵机控制)、本店总线设备控制(总线舵机,总线马达等)、传感器连接,手柄和红外控制的控制器。接口丰富,功能强大。
总线舵机介绍
传统 PWM 舵机是通过单片机发送 PWM 信号控制舵机转动,总线舵机是舵机内部带有一个主控芯片,内部已完成 PWM 信号控制。只需要通过串口发送字符串指令即可控制舵机。舵机内部的芯片也可以检测舵机的工作状态,所以通过串口也可以读取舵机的角度,切换工作状态。
舵机参数
- 舵机供电范围 4.8-8.4V。
- 扭力 15kg/cm。
- 八种角度工作模式, 270 度角度控制正反转、180 度角度控制正反转、360 度定圈连续旋转正反转、360 度定时连续旋转正反转八种工作模式可切换,同一个舵机可在这八种角度工作模式下
供用户切换。 - 单总线通讯,波特率 115200,舵机之间通过总线串联。 每个舵机都有自己 ID 号,舵机默认 ID 为 0,用户可通过命令改变舵机 ID,255 代表广播地址。
- 可回读角度,用户可读取舵机当前实时位置。
- 串口指令控制,无需用户编写舵机 PWM 驱动程序, 控制简单。
舵机提供的串口指令(节选)如下。
4.2.2 单轮独立控制模式
连接在总线上面的每个舵机可以分别独立接收和执行串口指令,独立运动。每个舵机有自己的id,通过指定id,就可以单独操作总线上的某个舵机。
我这里的舵机设置的id分别是左轮子3号,右轮子1号。当然,这个是可以在0~254自由设置的。
此外需要设置舵机的工作模式,这款总线舵机一共有8种工作模式。这里我采用的是模式7(马达模式360度定圈顺时针模式)和模式8(逆时针)。注意硬件连接时两个舵机的朝向是相反的,因此在小车整体向前运动时,左轮舵机采用模式7,则同时右轮舵机需要采用模式8。
class Car:
'''提供小车基本动作的封装。'''
leftId = 3
rightId = 1
leftForwardMod = 7 # 左轮(id=3):7前8后 | 右轮(id=2):8前7后
......
舵机控制指令封装。
以下代码将串口种使用的字符串指令中,我们需要用到的部分,封装成了函数。这样我们就方便地通过参数进行调用。轮子需要能够向前、向后运动,因此每次运动时需要先发送工作模式设置指令(控制顺时针、逆时针),然后再发送旋转指令。
class Cmds:
'''舵机控制指令封装'''
@staticmethod
def wheel_mod_cmd(id: int=255, mod: int=1):
'''左轮(id=3):7前8后 | 右轮(id=2):8前7后'''
return f'#{id:03d}PMOD{mod}!'
@staticmethod
def wheel_move_cmd(id: int=255, pwm: int=1700, time: int=1):
return f'#{id:03d}P{pwm:04d}T{time:04d}!'
@staticmethod
def stop(id: int=255):
return f'#{id:03d}PDST!'
小车动作封装。
上面的控制指令封装面向的对象仍然是舵机,使用id、mod(舵机运动模式)等与具体硬件性质相关的参数。
我接下来将舵机作为小车的轮子,进行更抽象的封装。下面包含了move
和stop
两个动作,进行传入参数的解析,并向串口发送相应的指令。
class Car:
'''提供小车基本动作的封装。'''
...
@staticmethod
def mod_reverse(mod: int):
return 8 if mod == 7 else 7
@staticmethod
def move(forward=True, left=True, pwm: int=1700, t: int=1, excute=True):
'''一个轮子的一次移动
@excute: False则不执行,仅仅返回命令字符串'''
whell_id = Car.leftId if left else Car.rightId
mod_id = Car.leftForwardMod
if not forward:
mod_id += 1
if not left:
mod_id = Car.mod_reverse(mod_id)
cmd1 = Cmds.wheel_mod_cmd(id=whell_id, mod=mod_id)
cmd2 = Cmds.wheel_move_cmd(id=whell_id, pwm=pwm, time=t)
if excute:
# 否则仅仅返回命令
myUart.uart_send_str(cmd1)
time.sleep(0.4) # 否则可能不转
myUart.uart_send_str(cmd2)
return f'{cmd1} {cmd2}'
@staticmethod
def stop(id: int=255):
cmd = Cmds.stop(id=id)
myUart.uart_send_str(cmd)
return cmd
...
4.2.3 双轮控制模式
单轮分别控制,理论上可以让小车做出非常灵活的动作,但是这对操作者提出了一定的要求——是有难度的。你有可能半天总在原地打转。
因此我决定进一步封装双轮同时控制。我们可以直接在前面单轮控制的基础上封装,并不复杂。但是,如果简单地顺序调用两次前面的move
指令,在实际操作时两个轮子动作之间会产生明显的延迟。
总线舵机支持动作组操作,将可以同时执行的多条指令连接起来,加上“{}”,就可以叠加同时控制多个舵机。比如:{G0000#000P1602T1000!#001P2500T0000!#002P1500T1000!}。
因此在前面的move
函数中设置了excute
参数,可以在调用时并不实际执行而仅返回相应的指令字符串。于是我们可以在move_double
函数中进行进一步的解析和组合,达到双轮同时运动的效果。
class Car:
'''提供小车基本动作的封装。'''
......
@staticmethod
def move_double(forward=True, pwml: int=1700, pwmr: int=1700, t: int=1, turn_left: bool=None):
'''两只轮子一起动,使用动作组。(可以差速转弯)'''
# 单向运动 | 原地转圈
if turn_left is None:
fl = fr = forward
elif turn_left:
fl, fr = False, True
else:
fl, fr = True, False
cmdl = Car.move(forward=fl, left=True, pwm=pwml, t=t, excute=False).split()
cmdr = Car.move(forward=fr, left=False, pwm=pwmr, t=t, excute=False).split()
group_mod = '{' + cmdl[0] + cmdr[0] + '}'
group_move = '{' + cmdl[1] + cmdr[1] + '}'
myUart.uart_send_str(group_mod)
time.sleep(0.4)
myUart.uart_send_str(group_move)
return f'{group_mod} {group_move}'
4.2.4 动作序列模式
很自然地,将一些预定义动作的连续执行封装起来,我们就可以在一次调用中让小车进行丰富的动作,比如:S形走位。然而并不能简单地连续把它们调用一遍。因为舵机的指令是打断式的,即:后面的指令并不会等待前面的执行完。
因此,在发送串口指令后,需要暂停相应的时间等待运动完成。
class CarShow:
'''小车的组合动作展示'''
@staticmethod
def S_move():
'''S形状走位'''
groups = [
{'forward': True, 'pwml': 1600, 'pwmr': 1600, 't': 1},
{'forward': True, 'pwml': 2000, 'pwmr': 2000, 't': 1, 'turn_left': False},
{'forward': True, 'pwml': 1800, 'pwmr': 2500, 't': 4},
{'forward': True, 'pwml': 2500, 'pwmr': 1800, 't': 4},
{'forward': True, 'pwml': 2500, 'pwmr': 2500, 't': 1, 'turn_left': True},
]
for g in groups:
Car.move_double(**g)
time.sleep(g['t'])
4.3 无线控制
4.3.1 网络结构
在本次实训中,我打算使用局域网无线通信,这样可以方便地将电脑、手机和树莓派三者拉到同一局域网中。而且实现简单,只需要将某一设备作为热点即可。电脑、手机、树莓派都可以作为热点,但只有将手机作为热点时,电脑和树莓派是可以访问互联网的,查找资料和调试更加方便。
(注意:如果将电脑连接手机热点,然后将树莓派连电脑热点,此时树莓派和手机并不在同一局域网。电脑、手机创建热点时,会分别创建一个独立的局域网。)
综上,将手机作为热点,树莓派和电脑连接它。
但问题是,手机通常并不能查看连接到它的设备的ip地址(仅仅显示mac地址)。我们可以使用nmap工具,进行局域网活动设备扫描。
- 工具下载:Download the Free Nmap Security Scanner for Linux/Mac/Windows
运行命令如下,图中192.168.43.1是作为热点的手机;在电脑命令行运行ipconfig
可以获取电脑ip,这里是192.168.43.166。因此,下面的192.168.43.91即为树莓派ip地址。
nmap -sn 192.168.43.0/24
4.3.2 服务器程序搭建
我选择了将树莓派作为服务器,收到请求后即可本地调用相应的动作函数,更为方便。服务器程序使用python语言,flask框架编写。服务器和客户端主要采用流行的json数据格式通信。
代码较长不全部贴出,详见附件。通常只需要对数据进行简单处理后,调用运动模块中相应类方法即可。(下面的INPI
变量用于调试,在配置文件config.py
中设置。因为在你的电脑上调试程序时,没法真正执行对应的动作控制指令。
@app.route('/car', methods=['POST'])
def car():
'''小车控制
@forward: bool, 前进 / 后退
@left: bool, 左轮 / 右轮
@pwm: int, 速度
@time: int, 持续时间'''
data = request.json
print('[car]', data)
forward, left, pwm, time = data['forward'], data['left'], data['pwm'], data['time']
if INPI:
cmd = Car.move(forward=forward, left=left, pwm=pwm, t=time)
return jsonify(cmd)
return jsonify(True)
注意将运行端口设置为0.0.0.0
,这样将可以接收局域网中其它设备的请求。
app.run(debug=True, host="0.0.0.0", port=5000)
4.3.3 客户端设计
我采用浏览器作为客户端。无论手机还是电脑,通常都会有浏览器。
这部分涉及的主要是一些web技术,如flask配套的jinja2模板引擎,jquery.js。使用javascript语言在用户操作时,向服务器发起相应的请求。作为小车的控制面板,相比按钮,图标的风格会更加适洽,因此用到了remixicon图标库。以及,使用bootstrap进行一些布局控制。
在手机端横屏界面效果如下。
客户端包含3个动态参数:左右轮分别的运行速度,以及每次运动的时间。两个轮子以不同的速度同时转动,即可执行转弯操作。在每个图标上监听用户的点击操作,根据js变量值即可向服务器发起请求,进行相应的运动控制。
下面是一段javascript代码示例,用于小车的整体前进或后退操作。
$('.run').on('click', function () {
var forward = true;
if ($(this).hasClass('backward')) {
forward = false;
}
fetch('/car_double', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
forward: forward,
pwml: left_pwm + 1500,
pwmr: right_pwm + 1500,
time: run_time
})
}).then(response => response.json()).then(data => {
printc(data)
})
})
4.4 带语义理解的语音控制
常见的语音控制方式,是关键词识别。然而,如果我们身边可以语音控制的设备躲起来,或者具有丰富的操作和指令。那么,记住繁杂的命令词会是我们沉重的负担。
为此本作中采用了两级识别结构,1)语音识别,从音频到说话内容的字符串;2)语义匹配,从字符串到小车能执行的命令词。
下面是语音识别中,整体的数据流程。更多的细节在后文介绍。
4.4.1 语音输入识别
1、WebSpeechAPI
本作采用浏览器客户端,现在许多浏览器已经提供了本地的识别接口,通过js调用即可。运行在浏览器本地。
改接口识别准确率较高,但识别时间不稳定,常常从不到1s至8~9s不等。
2、stt开源语音识别模型
开源仓库地址:https://github.com/jianchang512/stt
stt是一个离线运行的本地语音识别转文字工具,基于 fast-whipser 开源模型,可将视频/音频中的人类声音识别并转为文字,可输出json格式、srt字幕带时间戳格式、纯文字格式。可用于自行部署后替代 openai 的语音识别接口或百度语音识别等,准确率基本等同openai官方api接口。
将该模型部署在电脑本地,可以由客户端发起附带音频文件的请求,获取识别结果。
但由于我电脑算力受限,且配置gpu时遇到了障碍,只能运行较小的模型版本(base,141MB)。识别速度较稳定,在2s左右,但准确率相对低一些。
4.4.2 多API竞速并发
基于上述情况,我在客户端中实现了两种识别方式的并发。采用最快的那一个识别结果。(如果有一个又快有准的模型,则不必这样。)
如图,使用全局变量RACEID
进行识别任务的同步控制。当一种识别方式返回结果时,将RACEID
加一,那么另一种识别方式迟到的结果将不再被采用,以避免小车执行重复动作。
4.4.3 基于大模型的语义理解
前面的语音输入已经可以通过识别命令词,实现对小车的控制。是的,在简单的实验作品中,这已经足够了。但随着智能技术的持续渗透,我们身边会有越来越多的智能设备。如果对每个设备,以及设备的每个功能,我们都需要记住它具体的命令词——这无疑是一份巨大的负担。
而当今的大模型,为模糊指令的识别提供了可能。需要小车前进时,你可以说“往前走”,“向前跑”,而不必再纠结具体的指令词。
在本作品的实现中,利用了讯飞提供的星火大模型接口,以http进行调用。只需要通过编写恰当的prompt,即可让大模型完成指定任务。
平台地址:平台简介 | 讯飞开放平台文档中心 (xfyun.cn)
我使用prompt模板如下,可以传入语音识别阶段得到用户说话内容content
,和小车预定义的支持指令集results
两个参数。返回与content
匹配的相应指令。
def zl_http(content='别站个歪的!', results=['向左转', '向右转', '立正'], model='general'):
'''向spark模型发起文本转指令的请求'''
prompt = f"\
1. 你是一个自然语言转文本指令的助手,有如下文本指令:{results}。\
2. 你的职责是根据用户说的话,从上述文本指令中选择和用户的话最相似的一个。\
3. 你的输出应当仅仅是一个文本指令,不要说多余的话。\
4. 输出必须是完全一致的文本指令,不能包含任何额外的解释或说明。\
下面我说:{content}"
...
4.4.4 进一步展望
上述实现的“语义理解”,仍然依赖于小车预定义了哪些支持的指令。如果你想让它“托马斯旋转”而它并没有这个预定义的动作,那么它要么做出错误的行为,要么只会呆在原地。
如果可以将文本到舵机控制指令的环节直接打通,通过用户要求自动生成相应的舵机指令序列,它将会像一个真正的智能小车。然而基于对其技术难度的评估,在这次作品中并未实现。
5 课题测试
5.1 树莓派本机
因为涉及实际硬件的控制,一键将所有用例跑一遍的测试方式不很方便。下面采用交互的方式,运行在树莓派的终端*(我是通过vscode插件远程连接了)*,控制轮子的实际运动。终端打印了舵机执行的指令。这里不便展现出实际的运动效果。
控制正常。
5.2 语音控制
下面是在电脑浏览器,测试语音控制模块。首先启动程序。
# 在树莓派的项目目录
python3 app_pi.py
# 在电脑stt模型目录
python3 start.py
可以看到每次语音输入后,控制台有4行显示。前3行是识别的内容,第4行是匹配到的指令。下图中“前进。”、“广(往)前跑。”、“快跑!”都成功匹配到了指令“前进”。
这次测试中都是采纳了stt模型的识别结果,WebSpeech确实会速度不怎么稳定。
5.3 上手操作
可以流畅地操作控制,不会很明显的延迟感。实际操作效果可以参考b站视频:https://www.bilibili.com/video/BV1aMhCeLEi4/ (当时还没有做双轮控制模式)。只需要启动树莓派的程序即可。
python3 app_pi.py
6 总结
这次实训一共3周,然而前2周因为时而要准备期末考试,为避免挂科,实在没法安心搞实训。但最后一周时成功集中精神,也让人久违地体会到了动手制作的乐趣。
坦而言之,在不搞嵌入式的同学们心里,这实训会是一门“水课”。在不远的独木桥头,花这样的时间当然是件奢侈的事情。也许出于爱玩的天性,我还是想动手做点东西。
事情时而会从巧合里开始,在埋头中结束。这次实训中我算是用上了身边能想到的各种材料,在面对问题反复琢磨时,时间逃得很快。而一旦成功,心里涌起的兴奋也是藏不住的,甚至觉得不真实:还真让我给干成了。直到最后成品做出来,我才敢把自己的选题告诉老师。
大学的最后一次实训课了,日后回头一看,应该也不算空空如也。