本节目录
- 前置准备
- 1.1 硬件准备
- 1.2 软件准备
- 组装 & 开发攻略
- 2.1 组装舵机和支架
- 2.2 开发板“归中”和舵机“归中”
- 2.3 组装底座和拼装
- 2.4 安装屏幕
- 2.5 软件测试
- 代码走读
- 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
惯例结尾放一棵树