自学内容网 自学内容网

如何手工DIV一个小车:基于树莓派和总线舵机的智能小车实现

成品演示:bilibili - 悄悄的魔法书
代码仓库:github - flying forever 或者 gitee - 清风莫追

1 引言

1.1 课题背景

在嵌入式实训课程的要求下,我们需要完成一个和嵌入式硬件搭边的作品设计。鉴于我对stm32不算熟悉,但手里有一块树莓派和一只6自由度机械臂,而且在一个无聊的深夜,我机缘巧合之下用牙签和胶带粘合出了一只轮子,并成功绑定在舵机转轴上。因此我决定,组装一个小车。

早在几个月之前开发web网站时,我就在考虑做语音指令控制的功能。然而当时觉得过于高级,对我技术可行性不高,只是一个妄想,故一直未有尝试。但最近一番折腾之下略有了解,我决定把语音控制功能加到小车上去,变成“智能小车”。

1.2 课题意义

每每看见别人的光彩夺目,便为自己的大学旅途深感羞愧,仿佛光阴虚过,学问毫无,碌碌于一次次考试的应付。然而,理论学习脱离了综合性的实践,它就找不着自己的位置,人就没有自己的方向。

虽然没有涉及动手编写高级的算法,但这是一个综合性较强的作品。涉及技术攘括了我大学以来学习的许多课程与领域:python语言,人工智能,Web后端框架,前端UI与逻辑,计算机网络,Linux操作系统,以及与嵌入式相关的——舵机硬件控制。而将它们融于一体的智能小车,算我交上的一份小小答卷。

1.3 课题目的

实现一个支持客户端按键、语音输入两种控制方式的智能小车。并在制作过程中,体会这些零散的课程与技术,是如何支撑起一个综合性的系统,它们各自作为怎样的角色,相互之间又如何地协作。

2 课题相关知识与开发环境

在后文相应部分也有一些说明,这里做下汇总。

  1. 硬件-嵌入式材料:树莓派3B,树莓派扩展板,电源,总线舵机两个,舵机总线两根。
  2. 硬件-DIY材料:牙签、胶带、橡皮筋若干,小木块两个,可乐瓶,水果网套。(并不固定,缺少的材料可自行寻找替代品。
  3. 软件技术-客户端:html+css+javascript, bootstrap, remixicon图标库。
  4. 软件技术-服务器-树莓派:python3.7.3,flask,ZLSDK*(众灵科技提供,控制舵机,没有也没关系,可自己想办法向串口发字符串)*
  5. 软件技术-服务器-语音识别:
  6. 调试工具:(以下所有都不是必需的,理论上cmd命令行就能干)
    • Xftp7。用于windows电脑与树莓派之间传输文件。
    • Xshell7。ssh远程连接树莓派的命令行终端。
    • RealVNC Viewer。连接树莓派图形界面。(众灵科技提供)
    • nmap。搜索局域网中的活动设备。
    • gpt。人手一个,不懂就问。
    • Remote Development(VSCode插件)。远程连接树莓派,然后你可以像在本地写代码一样流畅。
  7. 相关资料:
    • 树莓派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 信号控制。只需要通过串口发送字符串指令即可控制舵机。舵机内部的芯片也可以检测舵机的工作状态,所以通过串口也可以读取舵机的角度,切换工作状态。

舵机参数

  1. 舵机供电范围 4.8-8.4V。
  2. 扭力 15kg/cm。
  3. 八种角度工作模式, 270 度角度控制正反转、180 度角度控制正反转、360 度定圈连续旋转正反转、360 度定时连续旋转正反转八种工作模式可切换,同一个舵机可在这八种角度工作模式下
    供用户切换。
  4. 单总线通讯,波特率 115200,舵机之间通过总线串联。 每个舵机都有自己 ID 号,舵机默认 ID 为 0,用户可通过命令改变舵机 ID,255 代表广播地址。
  5. 可回读角度,用户可读取舵机当前实时位置。
  6. 串口指令控制,无需用户编写舵机 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(舵机运动模式)等与具体硬件性质相关的参数。

我接下来将舵机作为小车的轮子,进行更抽象的封装。下面包含了movestop两个动作,进行传入参数的解析,并向串口发送相应的指令。

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工具,进行局域网活动设备扫描。

运行命令如下,图中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周因为时而要准备期末考试,为避免挂科,实在没法安心搞实训。但最后一周时成功集中精神,也让人久违地体会到了动手制作的乐趣。

坦而言之,在不搞嵌入式的同学们心里,这实训会是一门“水课”。在不远的独木桥头,花这样的时间当然是件奢侈的事情。也许出于爱玩的天性,我还是想动手做点东西。

事情时而会从巧合里开始,在埋头中结束。这次实训中我算是用上了身边能想到的各种材料,在面对问题反复琢磨时,时间逃得很快。而一旦成功,心里涌起的兴奋也是藏不住的,甚至觉得不真实:还真让我给干成了。直到最后成品做出来,我才敢把自己的选题告诉老师。

大学的最后一次实训课了,日后回头一看,应该也不算空空如也。



原文地址:https://blog.csdn.net/m0_63238256/article/details/140265972

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!