老实说,最早看到“语音控制无人机”这种组合,我心里想过:直升机本来通过遥控器就能指挥,多一个语音识别折腾一遍,是不是自己给自己找麻烦?可真让我动手写基于 Python 的语音控制 Tello EDU 项目之后,我才发现这里面的技术链路远比“动嘴”更有意思:麦克风采集声音、语音识别转文字、自然语言转 SDK 指令、UDP 数据包在局域网里送达无人机,最后电机响应。整个过程是很完整的“语音交互+嵌入式控制”小闭环。
这个项目很适合三类人:一是准备学 Python 网络编程的,二是想接触无人机 SDK 的,三是想在家里轻松给 Tello EDU 下达指令的。读完这篇文章,你至少能收获一套可以直接跑的语音控制脚本框架,以及我在调试过程中踩过的坑和放弃掉的方案。我尽量把每一步背后的原理和“为什么要这么写”讲清楚,这样你以后换成别的型号或者别的语音引擎也能自己改。
1. 为什么我会把语音控制和Tello EDU放在一起?
1.1 项目起点:键盘控制玩腻了,想换一种交互方式
我之前自己写过基于 Python 的键盘控制脚本,用方向键控制 Tello EDU。说实话,用键盘飞无人机也算方便,但每次飞行都要坐在笔记本面前,视线要在屏幕、无人机和手机之间来回切换。更麻烦的是,键盘控制本质上是在发“增量指令”:按一下前进键就飞 20 厘米,再按一下又飞 20 厘米。这种控制模式虽然安全,却完全没有“人机协同”的感觉。
后来我玩到一个语音助手项目,突然想到:既然语音识别已经能做到把“打开空调”转成一串指令,那我把“起飞”、“降落”、“向前飞”这些中文命令转成 Tello EDU 能听懂的 UDP 指令不就行了?这样可以解放双手,同时验证一条挺完整的 AIoT 技术路线:语音入口 -> 语义解析 -> 设备控制。
1.2 语音控制真正要解决的问题
语音控制无人机,核心不是“把声音变成文字”,而是“把意图变成可执行动作”。比如你说“前进”,如果你在客厅里,无人机直接盲目往前飞 5 米大概率撞墙。所以一个成熟的项目必须在语音识别之后,加上距离参数、速度限制、方向限制和紧急降落指令。
我给自己定的项目目标是:
- 支持中文语音指令,覆盖起飞、降落、前后左右移动。
- 能设置默认飞行距离,确保每次动作幅度不会太大。
- 识别失败时绝对不能发送误指令,无人机必须保持悬停。
- 语音识别到“急停”或“降落”时,要优先执行安全动作。
后来实际做下来,这四个目标每一项都对应一个技术细节,并不是简单把识别 API 接进来就行。
1.3 Tello EDU 本身适不适合语音控制
Tello EDU 是一台面向教育和编程的飞行设备,几百克级别,在室内小空间里悬停比较稳定,螺旋桨有保护罩,适合做实验。它相比消费版的优势是支持更开放的 SDK 指令,而且有配套的 Python 生态。虽然它的算力不可能本地处理语音,但我们可以用电脑完成语音识别,再通过它自带的 Wi-Fi 热点把指令传过去,相当于把“耳朵”放在了电脑上,把“身体”留在无人机里。
这种“外脑控制”的方式,也是最安全、最容易调试的方式。哪怕某个环节崩了,你还能随时切回键盘模式接管。
2. 先搞明白 Tello EDU 控制命令到底怎么发
2.1 SDK 指令的本质:一句话 UDP 包
Tello EDU 的控制协议很简单,本质就是:你的电脑连上无人机发出的 Wi-Fi 热点之后,通过 UDP 向固定 IP 和端口发一段 ASCII 文本命令,然后无人机会回复一段文本,比如 “ok” 或者 “error”。这里的固定 IP 是192.168.10.1,固定端口是8889。
如果你连过它的 Wi-Fi,就会发现自己电脑被分配了一个192.168.10.x的地址,而无人机自己就是192.168.10.1。这就是一个标准的局域网 UDP 通信。需要注意,Tello EDU 对每条命令都会有一个回复,但这个回复不是马上来,有时候需要几百毫秒甚至更久,所以你的 socket 接收必须设置超时,不能无限等下去。
首次连接时,无人机默认不是处于可接受 SDK 指令的状态,必须先发送command这条“握手”指令。成功之后,它会回复ok,然后你才可以把takeoff、land等指令发过去。
2.2 常用控制指令表
下面是我在项目中用到的核心指令,新手可以先背熟这一小撮。
| 指令 | 参数说明 | 实际效果 |
|---|---|---|
command | 无 | 激活 SDK 模式 |
takeoff | 无 | 起飞并悬停约 1 米高度 |
land | 无 | 原地降落 |
up x | x 为 20~500 | 垂直上升 x 厘米 |
down x | x 为 20~500 | 垂直下降 x 厘米 |
forward x | x 为 20~500 | 前进 x 厘米 |
back x | x 为 20~500 | 后退 x 厘米 |
left x | x 为 20~500 | 向左平移 x 厘米 |
right x | x 为 20~500 | 向右平移 x 厘米 |
cw x | x 为 1~360 | 顺时针旋转 x 度 |
ccw x | x 为 1~360 | 逆时针旋转 x 度 |
flip l/flip r | l/r/f/b | 向左/右/前/后翻滚 |
stop | 无 | 立即悬停,紧急停止 |
battery? | 无 | 返回当前电量百分比 |
注意一个小细节:left和right是“横向平移”,而cw和ccw是“原地旋转”,这两个要分清楚。刚开始语音控制时,很多人会把“左转”和“左移”混为一谈,导致无人机突然改变机头朝向而不是平移,非常容易撞到障碍物。
2.3 用 Python 的最小控制代码
我先写了一个不掺任何语音识别内容的最小控制脚本,目的是验证 socket 通信链路。这一步搞不定,后面做语音识别也是白搭。
import socket TELLO_IP = "192.168.10.1" TELLO_PORT = 8889 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(5) def send_command(cmd: str) -> str: """ 发送一条 Tello 指令,并接收无人机回复。 如果没有回复,返回空字符串。 """ try: sock.sendto(cmd.encode("utf-8"), (TELLO_IP, TELLO_PORT)) data, _ = sock.recvfrom(1024) return data.decode("utf-8").strip() except socket.timeout: print(f"[超时] 指令 {cmd} 未收到回复") return "" print(send_command("command")) print(send_command("battery?"))跑通这个脚本,你会看到终端打印出ok和电量数字。如果卡在超时,基本就是没有连上无人机的 Wi-Fi,或者 IP 写错了。这一步我强烈建议先测试,因为后续所有语音功能都建立在这个 socket 基础上。
有个我一开始忽略的点:sock.sendto发送指令后,无人机确认回复的地址不一定每次都一样,有些固件版本会从一个临时的端口回数据。如果接收窗口写得不对,会出现“指令发出去了但收不到回复”的现象。最简单的做法是不要过早关闭 socket,保持同一个 UDP socket 实例循环使用。
3. 语音识别的关键选型:在线接口还是本地离线模型
3.1 不要一上来就选在线 API
很多人做语音控制,第一步就接一个在线语音识别 API,因为免费的方案看起来简单两行代码就能出结果。但用在无人机控制场景,在线识别有几个致命问题:
- 每一次识别都要把音频上传到云端,往返时间通常是 1~3 秒。
- 如果网络抖动,识别结果会延迟很久,无人机接收到指令时会“突然冲出去”。
- 免费限额和并发限制,在不同场景下表现差异很大。
- 在飞行实验中,外部网络本来就是可选资源,没有必要让飞行动作依赖它。
我并不是说在线 API 完全不能用,而是在“实时控制无人机”这个特定需求下,延迟是不可接受的。你愿意为一句“降落”多等两秒吗?我不愿意,我听到螺旋桨声音都觉得危险。
3.2 本地离线识别是更稳的选择
在离线方案里,Vosk 是一个比较成熟的开源语音识别工具,支持中文模型,不依赖外部网络。模型文件下载后在本地运行,解码速度远快于在线方案,实测在普通笔记本电脑上,识别一个词基本在 0.3~1 秒左右,这个延迟对无人机控制来说是可以接受的。
我在项目里用的就是 Vosk 的中文小模型。它体积不大,准确率在“有限指令集”里完全够用。你可以先安装 Vosk 和 SpeechRecognition 相关库:
pip install vosk speechrecognition pyaudiopyaudio在 Linux 和 macOS 上可能需要额外安装系统依赖,Windows 上建议直接安装带预编译的包。装完之后,关键是配置正确的音频流参数:采样率16000、单声道、位深16bit。Vosk 默认需要这个格式,否则识别结果会乱掉。
3.3 我不是直接把整段语音都丢给模型
语音控制无人机和语音输入法不一样,不需要识别长句,只需要识别“起飞”“降落”“前进五十”“左转”这类短指令。所以我设计了一个关键词匹配策略:
- 先把麦克风拾取的一小段音频送到 Vosk。
- 模型返回一个包含多个候选词的 JSON,我只关心里面出现的动作词。
- 如果动作词命中了预设指令表,再执行后续转换。
这种策略的好处是:即使模型把“前进”识别成“前尽”或“前金”,我也可以通过拼音相似度或者包含关系把它匹配到“前进”指令上。如果严格到必须一字不差,语音识别里的同音字问题会让你崩溃。
4. 实现语音到飞行动作映射:一份可以直接跑的代码
4.1 整体架构
我的脚本分三个模块:音频采集、语义指令匹配、UDP 飞行指令发送。用一个主循环把它们串起来,大致流程是:
- 初始化 UDP socket 并发送
command,进入 SDK 模式。 - 初始化语音识别器和麦克风流。
- 每轮循环从麦克风读取约 2 秒的音频。
- Vosk 识别出文本。
- 对文本做关键词映射,判断是否需要执行动作。
- 如果需要执行,解析距离/角度参数,调用
send_command发送 UDP 包。 - 每次指令之间加冷却时间,防止连续误触发。
4.2 主程序示例
下面的代码是我化简后的版本,不依赖任何外部云接口,适合直接理解核心逻辑:
import json import queue import socket import sys import time import pyaudio from vosk import Model, KaldiRecognizer TELLO_IP = "192.168.10.1" TELLO_PORT = 8889 DEFAULT_DISTANCE = 40 # 平移距离默认厘米数 DEFAULT_ANGLE = 90 # 旋转角度默认度数 COOLDOWN_SECONDS = 2 # 两条动作指令之间的冷却时间 # 这一份映射可以按自己需求调整 keyword_map = { "起飞": "takeoff", "降落": "land", "急停": "land", "前进": "forward", "后退": "back", "左转": "ccw", "右转": "cw", "左移": "left", "右移": "right", "上升": "up", "下降": "down", } sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(5) def send_command(cmd: str) -> str: try: sock.sendto(cmd.encode("utf-8"), (TELLO_IP, TELLO_PORT)) data, _ = sock.recvfrom(1024) return data.decode("utf-8").strip() except socket.timeout: return "" def build_tello_command(keyword: str) -> str: """把识别出来的关键词转成 Tello 指令;距离和角度目前用默认值。""" base = keyword_map.get(keyword) if base is None: return None if base in ("forward", "back", "left", "right", "up", "down"): return f"{base} {DEFAULT_DISTANCE}" if base in ("cw", "ccw"): return f"{base} {DEFAULT_ANGLE}" return base def main(): # 连接无人机 print("正在连接 Tello EDU ...") print(send_command("command")) # 初始化 Vosk 模型 model = Model("models/vosk-model-small-cn-0.3") recognizer = KaldiRecognizer(model, 16000) # 打开麦克风音频流 audio_queue = queue.Queue() def audio_callback(in_data, frame_count, time_info, status): audio_queue.put(in_data) return (None, pyaudio.paContinue) audio = pyaudio.PyAudio() stream = audio.open( format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=4000, stream_callback=audio_callback, ) stream.start_stream() last_command_time = 0 print("语音控制已就绪,等待指令……") while True: data = audio_queue.get() if not data: continue if recognizer.AcceptWaveform(data): result = json.loads(recognizer.Result()) text = result.get("text", "").strip() if not text: continue print(f"[识别] {text}") matched_keyword = None for word in keyword_map: if word in text: matched_keyword = word break if matched_keyword is None: continue now = time.time() if now - last_command_time < COOLDOWN_SECONDS: print("[忽略] 指令间隔太短") continue last_command_time = now cmd = build_tello_command(matched_keyword) if cmd: print(f"[发送] {cmd}") response = send_command(cmd) print(f"[回复] {response}") if __name__ == "__main__": main()代码本身并不复杂。要注意build_tello_command里用了固定距离和固定角度,这是故意的:对刚接触语音控制的用户来说,固定 40 厘米比每次语音解析数字更安全。我之前试图让用户说“前进三十”,然后识别数字,结果识别精度和汉字数字转换带来的麻烦远超收益。先把固定参数跑通,再慢慢加可变参数。
4.3 如何让距离参数也支持语音解析
等固定参数版本稳定之后,可以做进阶升级:从识别文本里提取中文数字。做法是先判断动作词命中的位置,然后在文本后半段找“二十”“五十”“一百”这种数字表达。
一个比较朴素的转换函数:
def chinese_to_number(text: str) -> int: """极简中文数字解析,只处理 0~999,适用于常见距离。""" digits = { "零": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, } units = {"十": 10, "百": 100} total = 0 temp = 0 for char in text: if char in digits: temp = digits[char] elif char in units: if char == "十" and temp == 0: temp = 1 total += temp * units[char] temp = 0 total += temp return total这种解析器不适合做复杂的金额转换,但应付“前进五十”“左转一百八”这种口语足够。然后在build_tello_command里,把最初的固定距离替换成解析结果即可。不过我还是要提醒:语音识别到“一百八十”时,中文数字解析很容易出错,实际测试时一定要先在日志里看清楚识别结果,再做映射。
5. 实测中的避坑记录:延迟、误识别、嘈杂环境、安全兜底
5.1 识别延迟比我预想的小,但代码阻塞问题却很大
我最初写版本时,语音识别是在主循环里同步执行的。也就是说麦克风采集到音频之后,要等 Vosk 返回结果再继续读下一段音频。如果模型识别稍慢,麦克风缓冲区里的后续语音就会被丢弃,直接后果是:你说“起飞”,它识别出来了,但你说完“降落”,它可能正好处于识别中的阻塞状态,漏掉了。
解决方式就是我在代码里用audio_queue异步采集。麦克风回调函数把音频流不断放入队列,主循环再从队列里取数据。这样即使某次识别慢一点,音频不会丢,只是处理顺序稍微延后。这套生产者-消费者模型在很多实时识别场景里都能用,建议记下来。
5.2 关键词冲突比误识别更隐蔽
我在测试中遇到一个很有意思的误触发:我本来想说“起飞”,但模型识别成了“集体的起飞”或者“请飞”。后来排查发现,问题不在识别,而在我的关键词匹配用了简单的if word in text,而中文分词把“起飞”嵌在了一个长句里。
对策是缩小匹配窗口。当识别文本很短时不担心,但如果文本较长,我会只取前 4 个字作为有效指令区域。比如“起飞谢谢”这种长尾干扰,直接裁掉前两个词再判断。另一个策略是增加置信度过滤,Vosk 会返回多个 token 和置信度信息,我实际项目里要求命中关键词的置信度不低于 0.6。
5.3 飞行环境的安全兜底
无人机语音控制最怕的就是“说了不该说的词”。比如你在聊天提到“左转走”,无人机恰好听到了,结果突然原地转过机身。这虽然不致命,但如果旁边有人,真的很吓人。
所以我的安全设计有三层:
- 程序只在非对话场景下运行,起飞后关掉无关音源。
- 所有平移距离参数限制在 30~60 厘米之间,不让单次动作幅度过大。
- 紧急“降落”关键词必须优先匹配,而且响应时间要比其他指令更快。
实际项目里我还把“急停”单独拉出来,不经过完整语音长句判断,直接在识别结果里查这个子串。只要出现“急停”“降落”,立刻发送land,然后下一次动作前强制冷却 5 秒。这种特殊待遇在异常情况下非常重要。
5.4 不要开着螺旋桨测试语音
这可能是最容易被忽略的一点。我第一轮测试时,把无人机放在地面,已经处于起飞状态但还没升起来,然后自己对着麦克风反复说“旋转”“旋转”,结果无人机突然原地轻微转了一下,差点从桌面上扫下去。
后来我把测试流程改成了“地面半托管模式”:先让无人机在手里拿着或者放在开阔地面,然后测试语音识别部分只打印日志,不实际发送飞行指令。等打印日志稳定了,再真机起飞测试。这种做法虽然慢一点,但能避免很多螺旋桨事故。
6. 还能怎么玩:语音回传、自定义指令和后续扩展
6.1 结合命令回传状态信息
Tello EDU 不是只会单向接收指令,它也会返回传感器数据、电池电量、姿态信息。你可以把这些信息转成语音回传,比如识别到“电量”时,请求battery?,然后用本地语音合成说“当前电量百分之八十”。这样一个简单的语音问答系统就出来了,也适合拿来做课堂演示。
我建议把 UDP 状态轮询和数据解析单独封装成一个函数,不要和飞行动作混在一起。比如:
def query_battery() -> str: resp = send_command("battery?") try: return f"当前电量 {int(resp)}%" except ValueError: return "电量查询失败"6.2 自定义手势和特殊动作
语音除了控制位移,还可以控制翻转、相机拍照、抛飞之类的高级动作。Tello EDU 支持flip指令,说“翻个跟头”让无人机向前翻,演示效果非常酷。但翻转对空间有要求,前后左右至少留出 1.5 米余量,而且要确认电池电量足够。翻转过低电量容易掉下来。
在指令映射里加一条:
keyword_map["翻跟头"] = "flip f" keyword_map["左翻"] = "flip l"注意翻转动作不是悬停指令,执行时会有一个明显的高度变化,不适合在桌面上空测试。
6.3 从语音控制升级到情景自动化
等语音控制稳定后,更进阶的方向是情景自动化。比如利用 Tello EDU 的编程接口,让无人机在识别到“开始巡检”后,按照预设路径飞行,同时在每个航点语音播报“到达一号位置”。这种组合非常适合教育场景:学生可以在一套系统里同时接触语音识别、路径规划、UDP 通信和状态回传。
我最后的体会是:语音控制无人机最难的不是识别模型,而是如何不让识别误差变成飞行事故。项目里真正花时间的部分,其实是设计关键词冲突规避、距离限制和急停优先级。把这些东西做好之后,你会发现基于 Python 的语音控制 Tello EDU 不仅酷,而且可以成为一个非常不错的 AIoT 教学与实验平台。