这是一篇给新手的「从0手搓AI机器人」教程

本节目录

    1. 前置准备
    • 1.1 硬件准备
    • 1.2 软件准备
    1. 组装 & 开发攻略
    • 2.1 组装舵机和支架
    • 2.2 开发板“归中”和舵机“归中”
    • 2.3 组装底座和拼装
    • 2.4 安装屏幕
    • 2.5 软件测试
    1. 代码走读
    • 3.1 CmdClient
    • 3.2 Speaker
    • 3.3 Listener

欢迎关注同名公众号【陌北有棵树】,关注AI最新技术与资讯。

1. 前置准备

前置的物料准备,多亏了@Mark 和 @卓千寻 两位大佬帮统一采购,让我们到了之后直接就能组装&开发,再次感谢比心!

1.1 硬件准备

如果是想纯自己购买物料手搓,可以参考我们这次的,这篇文档里都有写:

https://hackathonweekly.feishu.cn/wiki/T5hhwrm1BiaBs4kaN8TcJoc7nhd?ignore_wx_jump=1

主要就是这些东西:

在这里插入图片描述

变身成图片:
在这里插入图片描述

1.2 软件准备

  • 去 Arduino 官网下载 IDE(https://www.arduino.cc/en/software/).
  • 从 Github (https://github.com/ideamark/desk-emoji)下载源码,这份源码是Mark大佬提供的,欢迎大家给个⭐️

Arduino IDE 是一款用于编写和上传 Arduino 程序的集成开发环境。简单来说,就是让你可以编写代码并上传到Arduino板子上,控制它执行各种任务。

2. 组装 & 开发攻略

这里两位主理人提供了PPT教程,大家可以去参考

https://hackathonweekly.feishu.cn/wiki/M6sTw0kg5ibtUOkOJVJcXe9jnqf?ignore_wx_jump=1

但如果作为纯新手,只看这个教程估计是搞不定,不要问我是怎么猜到的…所以接下来,我会结合着PPT的步骤,再做些补充,目的是为了更加方便于新手入门。

2.1 组装舵机和支架

先说说这个舵机是干嘛的,说实话我也是今天才知道…舵机是一种位置(角度)伺服的驱动器,适用于那些需要角度不断变化并可以保持的控制系统。简单点说,就是让东西按照你想要的角度转动。 比如在小机器人身上,让机器人的胳膊、腿按照我们想要的姿势弯曲或者伸直;或者在那种能转动摄像头的设备里,让摄像头转到想看的方向等等。

这是组装方式:
在这里插入图片描述
在这里插入图片描述

这个是两个组装舵机之后的图片,一个是x轴,一个是y轴:
在这里插入图片描述
在这里插入图片描述

这里我想说,到这一步的时候,我才理解了“手搓” 机器人的真正含义,我真后悔没有拍下那个搓刀的照片,以及搓那个舵机臂的过程…

还有就是螺丝不能拧太紧,我那个机器人最后只能左右转,不能上下转,大佬说是螺丝拧太紧了导致的。我怀疑我最近可能是牛奶喝多了,所以这个吃奶的劲儿使出来之后,机器人看了都连连摇头…

2.2 开发板“归中”和舵机“归中”

这个术语也是今天新学的,我感觉我今天就是个快乐的小学生…

开发板“归中”

在进行整体系统调试之前,需要对开发板进行一些基础设置的 “归中” 操作。比如,将开发板上用于控制舵机的 PWM(脉冲宽度调制)信号的输出设置为中间值,这样当舵机连接时,舵机就处于一个中间位置(如果舵机的角度控制是基于 PWM 信号,并且中间 PWM 值对应舵机的中间角度)。

这个操作不是在我电脑执行的,所以没有拍过程,流程就是把开发板插到电脑,然后在Arduino IDE 里选择 “Arduino Uno”,然后会弹出你这台机器的端口,选择,然后执行下面这段代码。

在这里插入图片描述

舵机“归中”

舵机归中就是让舵机的输出轴转动到中间位置。例如,对于 0 - 180 度的舵机,归中就是转动到 90 度的位置;对于 - 90 - 90 度的舵机,归中就是转动到 0 度的位置。

这个在操作之前需要先完成舵机和电路板的插线,对应PPT里这张图,我当时是没有完全懂,于是就看了几遍背下来了,照着插反正也能跑。回来之后又研究了一下,现在大概可以说清楚了,解释如下:

在开发板上,13、12 这样的数字是引脚编号。这些引脚是开发板与外部设备(如传感器、执行器等)进行电气连接的接口。

5V 代表开发板上提供的 5 伏特直流电压。这个 5V 电源引脚是开发板向外提供电能的接口之一。

GND 是零电位点,其实就是我们理解的地线。在开发板的电路系统中,所有的电压都是相对于 GND 而言的。

在这里插入图片描述

有了这些基础知识后,我们再来看怎么插。

舵机这边的线包括信号线(黄色的)、正极线(红色的)、负极线(棕色的)。

所以就是正极线连到5V上,负极线连到GND上,信号线连到 13/12引脚上。

连完之后再执行以下上面那套程序,就完成了舵机“归中”。

电路板上插完是这样的:

在这里插入图片描述

2.3 组装底座和拼装

这一步就是把那个白色的十字的舵机臂,无论用什么方式,塞到那个底座里,然后从底下把螺丝拧上来。我搓、我搓、我搓搓搓,我拧、我拧、我拧拧拧…

在这里插入图片描述

再然后就是把它们俩组装到一块儿,这里我真的强烈建议,如果是之前没弄过的人,一定一定得有一个参照物放在那儿,摆放的方向、顺序都得一模一样,但凡有一个不一样的,就等着返工重来吧,别问我是怎么知道的

在这里插入图片描述

2.4 安装屏幕

先把屏幕用热熔胶固定在上面那块板上,然后就是插线,具体插的方式,就按照屏幕上上面指示的四个插线位置「GND」「VCC」「SCL」「SDA」,依次插到开发板上即可。

在这里插入图片描述

都插完之后就可以进行测试了,用代码库里 oled_test.ino 那个文件进行测试,如果屏幕亮了,就说明都安装正确了。插完之后是这样的:

在这里插入图片描述

然后还要把 robot_base.ino 的代码也通过Arduino IDE 上传到开发板上。这个过程会将编译后的程序(一个二进制文件)通过选定的端口传输到Arduino板子的内存中。

2.5 软件测试

按照PPT中的如下步骤操作,执行 action.py 和 chat.py 就可以测试转动和语音聊天了。

在这里插入图片描述

最后我这边的进展是运行action.py能动了,但是语音最后有一个请求超时的报错,具体原因还要再看一下了,好在已经到了我比较可控的领域里,另外后面我还会根据我的想法做定制修改,再加上离开会场之后没有连接线能用了,接下来要等买的连接线到了之后才能继续调了,不过今天还是收获满满的,也算是基本上完成啦。

3. 代码走读

这里走读的代码是Mark大佬分享在github(https://github.com/ideamark/desk-emoji) 的代码,最开始大家可以clone下代码直接跑通就好,但后面如果想深入开发,还是需要了解代码里面的具体逻辑的。

common.py 里面提供了一些最基础的模块,是比较核心的,包括CmdClient、Speaker、Listener这三个比较主要的类。还有几个关于聊天和动作的基础方法,这几个其实大家可以后续根据自己的需求再去做改造的。

action.py 是关于机器人动作的测试。chat.py 是关于聊天的测试。

我下面主要是对CmdClient、Speaker、Listener这三个类做一个注释补充,便于大家理解。

3.1 CmdClient

CmdClient 类用于通过串口与设备进行通信。它可以列出可用的串口、选择一个串口、发送消息并等待响应。通过这些功能,可以方便地与串口设备进行交互。

下面是补充了注释的版本:

class CmdClient(object):

    def __init__(self, baud_rate=115200):
        self.baud_rate = baud_rate
        logger.info("Available serial ports:")
        # 列出可用的串口。
        available_ports = self.list_serial_ports()
        if not available_ports:
            logger.error("No serial ports found.")
            return
        # 选择一个串口。
        self.selected_port = self.select_serial_port(available_ports)
        self.ser = serial.Serial(self.selected_port, self.baud_rate, timeout=1)
        logger.info(f"Connected to {self.selected_port} at {self.baud_rate} baud rate.")
        time.sleep(7)

    # 列出所有可用的串口。
    # 返回值:一个包含可用串口设备名称的列表。
    def list_serial_ports(self):
        ports = serial.tools.list_ports.comports()
        available_ports = []
        for port in ports:
            if "serial" in port.device.lower():
                available_ports.append(port.device)
                logger.info(port.device)
        return available_ports

    # 从指定的串口取数据。
    def read_from_port(self, serial_port):
        # 持续检查串口是否有数据等待读取。
        while True:
            if serial_port.in_waiting:
                data = serial_port.read(serial_port.in_waiting)
                # 如果有数据,读取并解码为UTF-8字符串。
                result = data.decode('utf-8', errors='ignore')
                logger.debug("\nReceived:", result.strip('\n').strip())

    # 从可用的串口中选择一个
    def select_serial_port(self, available_ports):
        # 如果只有一个可用串口,直接返回该串口
        if len(available_ports) == 1:
            return available_ports[0]
        # 使用 inquirer 库提示用户选择一个串口
        questions = [
            inquirer.List('port',
                        message="Select a port",
                        choices=available_ports,
                        carousel=True)
        ]
        answers = inquirer.prompt(questions)
        return answers['port']

    # 发送消息到串口并等待响应。
    def send(self, msg):
        try:
            encode_msg = msg.encode('utf-8')
            self.ser.write(encode_msg)
            logger.debug(f"Sent: {msg}")
            ## 开始计时,等待响应。
            start_time = time.time()
            received_msg = ""
            while True:
                # 如果串口有数据等待读取,读取并解码为UTF-8字符串。
                if self.ser.in_waiting > 0:
                    received_msg += self.ser.read(self.ser.in_waiting).decode('utf-8')
                # 如果超过10秒没有响应,返回。
                if time.time() - start_time > 10: return
                # 如果接收到的消息包含发送的消息,记录并返回接收到的消息。
                if received_msg and msg in received_msg:
                    logger.debug(f"Received: {received_msg}")
                    return received_msg
                # 每0.1秒检查一次串口。
                time.sleep(0.1)
        except serial.SerialException as e:
            logger.error(f"Error: {e}")
            return

3.2 Speaker

Speaker类作用是将文本转换为语音并播放音频。这个类的设计使得文本转语音和音频播放可以异步进行,不会阻塞主线程的执行。


class Speaker(object):
    def __init__(self):
        self.executor = ThreadPoolExecutor(max_workers=1)
        # 初始化 pygame.mixer,这是 pygame 库中的一个模块,用于加载和播放音频
        pygame.mixer.init()
    # 播放指定路径的音频文件。
    def play_audio(self, audio_path):
        pygame.mixer.music.load(audio_path)
        pygame.mixer.music.play()
        # 循环检查音频是否正在播放。如果音频正在播放,线程将每秒休眠一次,直到播放完成。
        while pygame.mixer.music.get_busy():
            time.sleep(1)

    # 将文本转换为语音并播放。
    def say(self, text, model="tts-1", voice="onyx", audio_path='output.mp3'):
        # 将文本转换为语音。
        response = client.audio.speech.create(
            model=model,
            voice=voice,
            input=text
        )
        # 将生成的音频流保存到指定路径的文件中。
        response.stream_to_file(audio_path)
        # 提交一个任务到线程池,在单独的线程中执行 play_audio 方法播放音频文件。
        self.executor.submit(self.play_audio, audio_path)


3.3 Listener

Listener 主要用于通过麦克风录制音频并将其转换为文本,同样是异步执行,不会阻塞主线程的执行。

这里有一个小问题,是在活动过程中 笑斌 大佬提出并改进的,原来代码里录音的代码如下:audio_data = self.recognizer.listen(source)

这会有一个问题就是录音不结束,改成下面这行就会好,我下面的代码片段里用的也是改进后的:

audio_data = self.recognizer.listen(source, timeout=3,phrase_time_limit=5)


class Listener(object):

    def __init__(self, cmd):
        self.cmd = cmd
        # Recognizer 用于语音识别。
        self.recognizer = sr.Recognizer()
        self.executor = ThreadPoolExecutor(max_workers=1)

    def hear(self, audio_path='input.wav'):
        # 使用 sr.Microphone 作为音频源,打开麦克风。
        with sr.Microphone() as source:
            input("\n按回车开始说话 ")
            print("开始说话...")
            # 提交一个任务到线程池,在单独的线程中执行 act_random 函数
            self.executor.submit(act_random, self.cmd)
            # 录制音频。timeout 参数表示等待音频输入的超时时间,phrase_time_limit 参数表示单次录音的最长时间。
            audio_data = self.recognizer.listen(source, timeout=3,phrase_time_limit=5)

            print("录音已完成")
            # 将录制的音频数据保存到指定路径的文件中。
            with open(audio_path, "wb") as audio_file:
                audio_file.write(audio_data.get_wav_data())

            audio_file= open(audio_path, "rb")
            # 将音频文件转换为文本。
            transcription = client.audio.transcriptions.create(
                model="whisper-1",
                file=audio_file
            )
            # 返回转换后的文本。
            return transcription.text


惯例结尾放一棵树

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值