本帖最后由 HonestQiao 于 2023-1-3 22:12 编辑
目录:
零、前言
一、设计规划
二、六轴传感器
三、麦克风与语音识别
四、按键处理
五、BB-8机器人的控制方式
六、行空板控制BB-8机器人
七、BB-8机器人控制器系统
八、实际操作效果演示
九、总结
零、前言
在申请行空板 的时候,我提交的测评计划是:
准备基于此开发板,使用Python语言,基于板载的加速度传感器和麦克风,构建一个星球大战之BB-8机器人控制系统,其具体功能如下:
- 将星空开发板变身手持控制器,通过保持平衡,来控制BB-8的运动
- 使用麦克风,实现语音识别功能,通过语音识别,来控制BB-8的运动,以及BB-8内置灯光系统。
- 屏幕上将会同步显示当前的控制状态。
之前因为疫情原因,我的星球大战BB-8机器人一直不在身边。疫情放开后,终于把心爱的BB-8拿到了。
既然拿到手了,那就要把之前的计划给好好完成了。
一、设计规划
结合之前学习了解到的知识,以及高手们在行空板上的研究结果,最终确定实现如下的功能:
- 1. 利用行空板上的六轴传感器,来控制BB-8的运动
- 2. 使用行空板上的麦克风,实现语音识别功能,通过语音识别,来控制BB-8的灯的颜色
- 3. 通过按键来启用或者停止对BB-8的控制,以及切换颜色,及退出程序
- 4. 屏幕上会同步显示当前的控制状态。
二、六轴传感器
在行空板上,板载了一颗六轴传感器,具体型号为ICM20689 三轴加速度三轴陀螺仪,具体位置如下:
ICM20689据说是新一代陀螺仪,比MPU6xxx要更好用。
通过这个传感器,我们可以编写程序,来获取传感器的姿态,从而去控制外部的运动设备。
在行空板官方网站上,有关于该传感器的具体使用说明:4.4-加速度传感器
在DFRobot官方社区,还有一位高手的分享:[项目]上手行空板第一帖
这样,就能在行空板上,通过Python程序,来获取运动传感器的数据。
# -*- coding: utf-8 -*-
# 获取运动传感器数据
import time
from pinpong.board import *
from pinpong.extension.unihiker import *
Board().begin() #初始化
while True:
print(accelerometer.get_x()) #读取加速度X的值
print(accelerometer.get_y()) #读取加速度Y的值
print(accelerometer.get_z()) #读取加速度Z的值
print(accelerometer.get_strength()) #读取加速度强度(x、y、z方向的合力)
print(gyroscope.get_x()) #读取陀螺仪X的值
print(gyroscope.get_y()) #读取陀螺仪Y的值
print(gyroscope.get_z()) #读取陀螺仪Z的值
print("------------------")
time.sleep(1)
为了更好的呈现当前控制的具体情况,在BB-8机器人控制器系统中,使用了一个小气泡图片,根据坐标信息,会自动设定该小气泡的显示位置,这样可以实现较为直观的控制。
ICM20689获取的加速度值基本原理如下:
在实际控制中,绕X轴的加速度,转换为XY平面坐标中的Y轴坐标变化;而绕Y轴的加速度,转换为XY平面坐标中的X轴坐标变化。
根据该变化,从而实现BB-8在地面(平面)上的运动参数。
三、麦克风与语音识别
在行空板上,也提供了一颗麦克风,具体位置如下:
在官方教程中,也有关于麦克风录音的详细说明:录音record
在DFRobot社区,也有一个高手的分享:语音识别助手
参考该分享,就可以在行空板上,实现录音,并通过百度语音接口,将录音识别为文字了。
不过,上述的演示和分享,都需要手动开启录音,然后录音一段时间,然后才进行识别。
在很多智能音箱设备中,都是有一个唤醒词,通过唤醒词来掉起语音识别功能进行识别,这样更能提高效率。
通过研究官方资料,可以试用环境音量,做一个简单的唤醒功能。
在官方资料中,有这个说明:4.6-环境声音强度
在 BB-8机器人控制器 这个项目中,需要手持行空板进行操作,那么行空板离操作者很近。
那么操作这说话的时候,相对能够检测到较大的环境音量。
因此,可以设置当使用 Audio对象.sound_level() 返回值大于 40或者50的时候,再启动录音,并进行语音识别。
# -*- coding: utf-8 -*-
# 环境音量检测并录音
from unihiker import Audio
import time
audio = Audio() #实例化音频
while True:
if audio.sound_level()>40:
print("环境音量够大,准备录音")
print("开始录音")
audio.start_record('3s.wav') #后台开始录音,存到文件3s.wav中
print("等待3秒")
time.sleep(3) #等待6秒
audio.stop_record() #停止录音
print("停止录音")
录音后,调用百度语音识别的API,就能进行识别了:
from aip import AipSpeech
""" 你的 APPID AK SK """
APP_ID = '12345678'
API_KEY = '********'
SECRET_KEY = '********'
client = AipSpeech(APP_ID, API_KEY, SECRET_KEY)
# 读取文件(固定代码)
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
# 识别本地文件
def voiceToText():
res = client.asr(get_file_content('3s.wav'), 'wav', 16000, {
'dev_pid': 1537,
})
print(res)
return res.get('result')[0]
msg = voiceToText()
print("识别结果:%s" % msg )
四、按键处理
在行空板上,一共有三个按键,具体如下:
其中A、B按键,行空板的GUI系统提供了调用支持,具体可见:7.2-键盘事件 on_key_click
这两个按键,直接通过on_key_click设置回调即可。
在本项目中,A键设置为启用或者停止对BB-8的运动控制,B键设置为切换BB-8的LED颜色。
行空板上HOME键,在行空板的GUI系统中,不能直接调用。
不过,该按键实际上直接连接到主CPU上而非协处理器上,因此通过GPIO可以直接控制,而不是通过pinpong或unihiker库控制。
其对应的系统GPIO端口号为32,使用python的GPIO库,调用方式如下:
# -*- coding: utf-8 -*-
import sys
sys.path.append(".pip")
import time
import gpio as GPIO
BUTTON_HOME = 32
GPIO.setup(BUTTON_HOME, GPIO.IN)
value_prev = 0
while True:
value = GPIO.input(BUTTON_HOME)
if not value == value_prev:
value_prev = value
print("value=%d" % value)
time.sleep(0.1)
运行该程序后,按住或者松开HOME按键,将获得如下的输出:
当按住时,输出为0,松开时输出为1。
通过这样的方式,在BB-8机器人控制器系统中,按HOME键,则程序将会自动退出。
五、BB-8机器人的控制方式
BB-8机器人通过专用的通讯协议进行通讯,最开始需要使用手机App进行通讯,后来开放了可编程接口,但是需要专用的变成软件。
经过一番学习研究之后,我找到了国外高手破解协议后实现的网页基础控制功能,并在此基础上进行了扩展,源码和详细的说明,发布到了:星球大战BB-8机器人控制测试与转发工具
星球大战BB-8机器人的控制,需要通过专有的App进行控制和二次开发。
operasoftware/bb8实现了一个网页界面的基础控制功能,HonestQiao/bb8在此基础上进行了完善,增加了方向键按键控制功能,并实现了WebSocket、MQTT支持,从而使得其他设备,可以通过WebSocket和MQTT,来间接控制BB-8。
进一步的,HonestQiao开发了本转发工具,为 HonestQiao/bb8 提供完整WEB运行环境,并对外提供TCP Socket、WebSocket、MQTT控制能力,任何能够使用TCP Socket、WebSocket、MQTT的设备、程序等,均可以控制BB-8。
通过该工具,可以让嵌入式设备,以多种方式控制BB-8。
以下为网页控制界面:
以下为控制转发测试界面:
在 BB-8机器人控制器系统 中,使用了WebSocket的方式连接控制。
在这个项目中,最终使用了如下的两个指令:
- 运动指令:{"action":"move","x":150,"y":50,"target":"BB-8"}
- 颜色指令:{"action":"color","c":[0,0,255],"target":"BB-8"}
在运动指令中,是给控制系统发送了一个坐标(x, y),然后通过这个坐标(x, y),转化为角度,以及加速度,然后让BB-8以该角度和加速度运动。
需要注意的是,每次运动,都是以BB-8自身为原点(150, 150)来进行控制的。
而在颜色指令中,则是通过发送一个RGB数据,去控制BB-8内置LED。
六、行空板控制BB-8机器人
前面说了,在 BB-8机器人控制器系统 中,使用了WebSocket的方式连接控制。
而在Python中,使用WebSocket是非常方便的,可以使用如下的方法:
# -*- coding: utf-8 -*-
# WebSocket
import time
import json
import websocket
def on_open(wsapp):
print("on_open")
json_cmd = {"action":"move","x":150,"y":150,"target":"BB-8"}
wsapp.send(json.dumps(json_cmd))
time.sleep(0.1)
while True:
json_cmd = {"action":"color","c":[255,0,0],"target":"BB-8"}
wsapp.send(json.dumps(json_cmd))
time.sleep(1)
json_cmd = {"action":"color","c":[0,255,0],"target":"BB-8"}
wsapp.send(json.dumps(json_cmd))
time.sleep(1)
def on_message(wsapp, message):
print("on_message:", message)
def on_close(wsapp):
print("on_close")
wsapp = websocket.WebSocketApp("ws://192.168.1.15:1234",
on_open=on_open,
on_message=on_message,
on_close=on_close)
wsapp.run_forever()
在上述代码中,WebSocket服务器为前一步的BB-8控制转发工具提供的服务,端口号为1234。
连接成功后,先发送一个移动指令,再交替发送红色、绿色的指令让BB-8闪烁。
七、BB-8机器人控制器系统
经过以上的各项学习研究和准备工作,最终完成的实际代码如下:
# -*- coding: utf-8 -*-
# 星球大战BB-8机器人控制器
# 将当前路径下的.pip添加到系统目录
import sys
sys.path.append(".pip")
import threading
import time
import json
import re
import websocket
from pinpong.extension.unihiker import accelerometer
from pinpong.board import Board,Pin
from unihiker import GUI,Audio
from aip import AipSpeech
import gpio as GPIO
""" 你的 APPID AK SK """
APP_ID = '12345678'
API_KEY = '********'
SECRET_KEY = '********'
AUDIO_FILE = '2s.wav'
AUDIO_TIME = 2
BUTTON_HOME = 32
COLORS = {
'黑色': [0,0,0],
'红色': [255,0,0],
'绿色': [0,255,0],
'蓝色': [0,0,255],
'黄色': [255,255,0],
'青色': [0,255,255],
'深红': [255,0,255],
'白色': [255,255,255]
}
COLOR_NAMES = list(COLORS.keys())
SKIP_PATTERN = re.compile(r'(啊|啦|唉|呢|吧|哇|呀|吗|哦|嗯|哎|么|了|的|地|,|。|、|?|!)')
u_gui=GUI()
Board().begin()
GPIO.setup(BUTTON_HOME, GPIO.IN)
client = AipSpeech(APP_ID, API_KEY, SECRET_KEY)
audio = Audio() #实例化音频
ws_status = False
stop_threads = False
exit_time = 0
bb8_enable = False
color_index = 0
def numberMap(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
def icm20689_process():
global stop_threads
global exit_time
global bb8_enable
global wsapp
send_time = 0
x_prev = 150
y_prev = 150
while True:
if stop_threads:
exit_time = time.time()
break
#y方向数据处理
yShangXian = ((accelerometer.get_y() * -120) + 120)
#x方向数据处理
xShangXian = ((accelerometer.get_x() * 160) + 160)
#将x方向的角度映射在-90到90区间中
JiaoDuX = round((numberMap(xShangXian, 0, 314, -90, 90)),)
#将y方向的角度映射在-90到90区间中
JiaoDuy = round((numberMap(yShangXian, 3, 237, -90, 90)),)
# ZhongHeJiaoDu = (JiaoDuX + JiaoDuy)
# ZhongHeJiaoDu = round((numberMap(ZhongHeJiaoDu, -180, 180, -90, 90)),)
角度X.config(text=(str((JiaoDuX)) + str((str("度")))))
角度Y.config(text=(str((JiaoDuy)) + str((str("度")))))
# 角度.config(text=(str((ZhongHeJiaoDu * 2)) + str((str("度")))))
气泡.config(x=yShangXian-25)
气泡.config(y=xShangXian-25)
# print((str((str("变量y"))) + str(yShangXian)))
# print((str((str("变量x"))) + str(xShangXian)))
if time.time()-send_time>1:
x_will = int(yShangXian * 300/240)
y_will = int(xShangXian * 300/320)
if abs(x_prev-x_will)>3 or abs(y_prev-y_will)>3:
# 发送指令
json_cmd = {"action":"move","x":150,"y":150,"target":"BB-8"}
json_cmd["x"] = x_will
json_cmd["y"] = y_will
BB8_X.config(text=str(json_cmd["x"]))
BB8_Y.config(text=str(json_cmd["y"]))
if bb8_enable:
print("cmd: %s" % json.dumps(json_cmd))
wsapp.send(json.dumps(json_cmd))
x_prev = x_will
y_prev = y_will
time.sleep(0.01)
# 读取文件(固定代码)
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
# 识别本地文件
def voiceToText():
res = client.asr(get_file_content(AUDIO_FILE), 'wav', 16000, {
'dev_pid': 1537,
})
if res:
# print(res)
return res.get('result')[0]
def audio_process():
while True:
global stop_threads
global bb8_enable
global color_index
if stop_threads:
break
lv= audio.sound_level()
if lv>40:
print("环境音量%d,开始录音:" % lv, end="",flush=True)
audio.start_record(AUDIO_FILE) #后台开始录音,存到文件6s.wav中
print("等待%d秒" % AUDIO_TIME, end="",flush=True)
time.sleep(AUDIO_TIME) #等待6秒
audio.stop_record() #停止录音
print(",停止录音", end="",flush=True)
msg = voiceToText()
print(";识别结果:%s" % msg, end="",flush=True)
msg = re.sub(SKIP_PATTERN, '', msg)
print(";清洗结果:%s" % msg)
if len(msg)<2:
continue
if "停止" in msg:
bb8_enable = False
BB8_控制.config(text="正常" if bb8_enable else "停止")
continue
if "开始" in msg:
bb8_enable = True
BB8_控制.config(text="正常" if bb8_enable else "停止")
continue
name_select = None
if '熄灭' in msg:
name_select = '黑色'
else:
for name in COLOR_NAMES:
if name in msg:
name_select = name
if not name_select == None:
print("选中颜色: %s" % name_select)
color_index = COLOR_NAMES.index(name_select)
json_cmd = {"action":"color","c":COLORS[name_select],"target":"BB-8"}
BB8_颜色.config(text=name_select)
bg_color = tuple(COLORS['白色']) if name_select == '黑色' else tuple(COLORS[name_select])
背板.config(color=bg_color)
print("cmd: %s" % json.dumps(json_cmd))
wsapp.send(json.dumps(json_cmd))
def on_open(wsapp):
print("on_open")
json_cmd = {"action":"move","x":150,"y":150,"target":"BB-8"}
wsapp.send(json.dumps(json_cmd))
time.sleep(0.1)
for i in range(1,len(COLOR_NAMES)):
for n in range(2):
json_cmd = {"action":"color","c":COLORS[COLOR_NAMES],"target":"BB-8"}
bg_color = tuple(json_cmd['c'])
背板.config(color=bg_color)
print("cmd: %s" % json.dumps(json_cmd))
wsapp.send(json.dumps(json_cmd))
time.sleep(0.5)
json_cmd["c"] = COLORS['黑色']
bg_color = tuple(json_cmd['c'])
背板.config(color=bg_color)
print("cmd: %s" % json.dumps(json_cmd))
wsapp.send(json.dumps(json_cmd))
time.sleep(0.5)
bg_color =tuple(COLORS['白色'])
背板.config(color=bg_color)
控制器_状态.config(text="")
threading.Thread(target=icm20689_process).start()
threading.Thread(target=audio_process).start()
global exit_time
global stop_threads
while True:
if exit_time>0 and time.time()-exit_time>1:
print("程序退出.")
wsapp.close()
quit()
if stop_threads == False and GPIO.input(BUTTON_HOME) == 0:
stop_threads = True
print("正在退出...")
控制器_状态.config(text="正在退出...")
time.sleep(1)
def on_message(wsapp, message):
print("on_message:", message)
def on_close(wsapp):
print("on_close")
def on_a_click():
global bb8_enable
bb8_enable = not bb8_enable
BB8_控制.config(text="正常" if bb8_enable else "停止")
def on_b_click():
global color_index
color_index = color_index + 1
if color_index >= len(COLOR_NAMES):
color_index = 0
name_select = COLOR_NAMES[color_index]
print("切换颜色: %s" % name_select)
json_cmd = {"action":"color","c":COLORS[name_select],"target":"BB-8"}
BB8_颜色.config(text=name_select)
bg_color = tuple(COLORS['白色']) if name_select == '黑色' else tuple(COLORS[name_select])
背板.config(color=bg_color)
print("cmd: %s" % json.dumps(json_cmd))
wsapp.send(json.dumps(json_cmd))
u_gui.on_a_click(on_a_click)
u_gui.on_b_click(on_b_click)
#初始化背板颜色
背板=u_gui.draw_rect(x=0,y=0,w=240,h=320,width=320,color="#FFFFFF")
#初始化气泡位置
气泡 = u_gui.draw_image(x=120-25, y=160-25, w=50, h=50, image="./pop.png")
# 控制名称
标题=u_gui.draw_text(text="BB-8机器人控制器",x=40,y=20,font_size=13, color="#000000")
#初始化角度
角度X_标题=u_gui.draw_text(text="X轴:",x=80,y=100,font_size=13, color="#000000")
角度X=u_gui.draw_text(text="0度",x=120,y=100,font_size=13, color="#000000")
角度Y_标题=u_gui.draw_text(text="Y轴:",x=80,y=120,font_size=13, color="#000000")
角度Y=u_gui.draw_text(text="0度",x=120,y=120,font_size=13, color="#000000")
# 角度_标题=u_gui.draw_text(text="综合:",x=80,y=140,font_size=13, color="#000000")
# 角度=u_gui.draw_text(text="0",x=120,y=140,font_size=13, color="#000000")
BB8_坐标_标题=u_gui.draw_text(text="坐标:",x=50,y=160,font_size=13, color="#000000")
BB8_X=u_gui.draw_text(text="150",x=100,y=160,font_size=13, color="#000000")
BB8_Y=u_gui.draw_text(text="150",x=140,y=160,font_size=13, color="#000000")
BB8_颜色_标题=u_gui.draw_text(text="颜色:",x=50,y=180,font_size=13, color="#000000")
BB8_颜色=u_gui.draw_text(text="待设置",x=110,y=180,font_size=13, color="#000000")
BB8_控制_标题=u_gui.draw_text(text="控制:",x=50,y=200,font_size=13, color="#000000")
BB8_控制=u_gui.draw_text(text="停止",x=110,y=200,font_size=13, color="#000000")
控制器_状态=u_gui.draw_text(text="正在启动自测",x=80,y=280,font_size=13, color="#000000")
wsapp = websocket.WebSocketApp("ws://192.168.1.15:1234",
on_open=on_open,
on_message=on_message,
on_close=on_close)
wsapp.run_forever()
运行以上程序后,界面如下:
程序的主要逻辑如下:
- 定义要使用的颜色及其他基础信息
- 初始化GUI系统、Board硬件、HOME按键、百度语音调用、音频
- 设置A、B按键回调,界面元素初始化显示
- 开启WebSocket连接
- 连接成功后,先发送move原点指令,再依次发送给中颜色的指令,让BB-8依次闪烁各种颜色,最后启动ICM20689处理线程和语音识别处理线程,最后再循环中,判断是否需要退出
在ICM20689处理线程 icm20689_process 中,基本逻辑如下:
- 获取ICM20689当前数据,并根据实际需要进行数据转换
- 将处理的结果,再屏幕上进行显示更新
- 再根据获取的数据,判断是否需要控制BB-8移动,需要的话,就发送移动指令
在语音识别处理线程audio_process中,基本逻辑如下:
- 先检查环境音量
- 如果环境音量大于40,则启动录音
- 录音完毕,调用百度语音识别接口进行识别
- 根据识别结果,检测包含某个颜色的关键词,则发送指令控制BB-8的LED颜色。
- 如果识别到的是熄灭指令,则发送指令控制BB-8的LED不亮
- 如果识别到的是开始指令,则启用BB-8运动控制
- 如果识别到的是停止指令,则停用BB-8运动控制
实际使用到的颜色如下:
八、实际操作效果演示
运行程序后,可以按找如下的方式,来进行控制:
- 运动控制:以不同角度,倾斜行空板,可以看到屏幕上的小气泡位置发生了而变化,同时对应的各轴角度,以及坐标都对应变化
- 语音识别:大声说出“小B红色”,语音识别后,屏幕背景变为红色;BB-8的LED颜色也会对应变化;如此说出其他颜色,则屏幕背景和BB-8LED会对应变化;
- 语音识别:大声说出“小B细末”,语音识别后,屏幕背景变为白色;BB-8的LED会熄灭;
- 语音识别:大声说出“小B开始”,语音识别后,就会启用BB-8运动控制,BB-8会根据坐标信息,运动起来
- 语音识别:大声说出“小B停止”,语音识别后,就会停止BB-8运动控制,运动信息只会在屏幕更新,而不会发送到BB-8
- 按A键后,将会启用BB-8运动控制,BB-8会根据坐标信息,运动起来
- 按B键后,BB-8的颜色将会发生变化,顺序为之前颜色定义的顺序循环切换。
- 按HOME键后,程序将会退出
将BB-8放置在充能座上,进行控制的操作:
视频演示如下:
1. 启动及按键控制颜色:
17-启动及按键控制颜色
2. 语音控制:
18-语音控制颜色
3. 运动控制:
19-运动控制
九、总结
行空板由DFRobot出品,延续了DFRobot一贯的精品策略。这块板子,带有屏幕,结构紧凑,小巧精致,但是集成的功能很丰富。
这次的项目,使用到了行空板上的屏幕、六轴传感器、麦克风、全部按键以及网络功能,而这一切,都没有添加任何外部模块。
所以这个BB-8机器人控制器制作起来,调用了多方面的资源和功能,使用起来也是非常的方便,一块板子就搞定一切。
同时,得益于行空板对Python的良好支持,既包括专有功能库,也包括能够方便的使用外部Python库,使得各项功能的开发得以顺利进行。
总的来说,这次的试用体验非常好,今后还会继续挖掘这块板子的强大功能,玩出更多的花样来。