IoT-For-Beginners 实战:在 Raspberry Pi 与虚拟 IoT 设备上调用无服务器 REST 端点并实现语音定时器
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
导读
本文是 IoT-For-Beginners 课程 "Set a timer and provide spoken feedback"(设置定时器并提供语音反馈)一课中面向单板计算机(Raspberry Pi)与虚拟 IoT 设备的任务指南。它承接上一课"语音转文字 + LUIS 语言理解 + 无服务器函数"的成果:当设备把用户语音转成文字后,你需要把这段文字 POST 到无服务器 REST 端点,由 LUIS 识别出set timer意图并返回定时秒数,然后在设备端用 Python 标准库threading.Timer在后台线程中真正把定时器"跑"起来。读完本文,你将掌握完整可运行的get_timer_time、announce_timer、create_timer等核心函数实现,理解threading.Timer的线程语义、HTTP 状态码约定,并能直接在 Raspberry Pi 或虚拟设备上运行整个smart-timer项目。
一、任务背景:从语音到定时器的完整调用链
在进入本任务之前,先回顾整条链路,明确本任务所处的位置:
- 语音转文字(Speech-to-Text):设备录制用户语音并交给 Azure Speech 服务,返回识别文本,例如
Set a two minute 27 second timer.; - 语言理解(LUIS):文本被发送到无服务器函数,函数调用 LUIS 预测 API,识别出
set timer意图与number、time unit实体; - 无服务器函数返回秒数:函数把"2 分钟 27 秒"换算成
147秒,以 JSON 形式返回; - 本任务(设备端设置定时器):设备调用该 REST 端点拿到秒数,用
threading.Timer创建后台定时器,在倒计时结束时报出"Times up on your ... timer."。
对应的无服务器端实现可在 text-to-timer/init.py 中查看,其核心逻辑是:从请求体读取text,调用client.prediction.get_slot_prediction(app_id, 'Staging', prediction_request)获取预测结果;当top_intent == 'set timer'时,按顺序把number实体与time unit实体一一配对,分钟数乘以 60 累加、秒数直接累加,最后返回:
payload = { 'seconds': total_seconds } return func.HttpResponse(json.dumps(payload), status_code=200)识别失败(意图不是set timer)则返回status_code=404。这一约定直接决定了设备端get_timer_time的写法:以 HTTP 状态码 200 作为调用成功的判据。上一课完整搭建 LUIS 模型与 HTTP 触发器的过程见 2-language-understanding/README.md。
二、任务 1:把文本发送给无服务器函数
设备端的第一步是新增一个get_timer_time(text)函数,负责把识别出的文本发给上一课创建好的 REST 端点,并解析出定时秒数。
2.1 声明函数与定义目标 URL
在process_text函数上方声明函数,并用<URL>占位符定义要调用的端点地址:
def get_timer_time(text): url = '<URL>'<URL>的取值取决于你的部署方式(详见上一课"make your function available to your IoT device"一节):
- 发布到云端:
https://<APP_NAME>.azurewebsites.net/api/text-to-timer?code=<FUNCTION_KEY>,其中<APP_NAME>是函数应用名,<FUNCTION_KEY>是用az functionapp keys list --resource-group smart-timer --name <APP_NAME>获取的functionKeys下default项的值; - 本机运行、设备同网访问:
http://<IP_ADDRESS>:7071/api/text-to-timer,<IP_ADDRESS>是运行函数应用的电脑在局域网中的 IP,端口固定为7071。这种方式要求 IoT 设备与电脑处于同一网络。
2.2 以 JSON 形式传递文本并发送请求
将文本放入text属性,通过requests.post发送:
body = { 'text': text } response = requests.post(url, json=body)json=body参数会由requests库自动把字典序列化为 JSON,并设置Content-Type: application/json,与服务端req.get_json()+req_body['text']的读取方式严格对应。
2.3 解析响应,失败返回 0
成功调用时,服务端返回 200 与形如{"seconds": 147}的 JSON;失败(如意图不是set timer)时返回 404。设备端据此做判断:
if response.status_code != 200: return 0 payload = response.json() return payload['seconds']这里的关键设计是用返回值0表达"调用失败或无法设置定时器",因为合法的定时时长必然大于 0——这为后续process_text中的if seconds > 0判断埋下了伏笔。用return 0而不是抛异常,也让主流程保持简单、健壮,即使云端不可用也不会让设备崩溃。
完整的get_timer_time实现在 code-timer/pi/smart-timer/app.py(Raspberry Pi 版)与 code-timer/virtual-iot-device/smart-timer/app.py(虚拟设备版)中保持一致。
三、任务 2:在后台线程中设置定时器
拿到秒数之后,就要让设备真正"计时"。Python 标准库的threading.Timer正是为此设计的:它接收一个延迟时间(秒)和一个回调函数,延迟结束后在一个新线程中执行该函数,不会阻塞主程序的其他逻辑。
3.1 导入 threading 并实现 say 占位函数
在文件顶部加入导入语句:
import threading随后在process_text上方定义一个say函数用于"说出"反馈。本任务阶段它只把文字打印到控制台;在本课稍后的"文本转语音(Text-to-Speech)"步骤中,say会被改为真正调用 Azure Speech 服务把文字朗读出来。先把接口定义好,后续只需改动这一处:
def say(text): print(text)3.2 announce_timer:定时器到点的播报函数
该函数由定时器在倒计时结束时回调,负责拼接"Times up on your ... timer."这句英文播报:
def announce_timer(minutes, seconds): announcement = 'Times up on your ' if minutes > 0: announcement += f'{minutes} minute ' if seconds > 0: announcement += f'{seconds} second ' announcement += 'timer.' say(announcement)值得注意的细节:函数会分别检查minutes与seconds,只有该时间单位数值大于 0 时才把对应部分拼入句子。例如分钟数为 0 时,播报只会包含秒数(Times up on your 27 second timer.),避免出现 "0 minute" 这类冗余表达。拼好的句子统一交给say输出。
3.3 create_timer:换算时分秒并启动线程定时器
create_timer是本任务的核心函数,接收总秒数,换算成"分 + 秒",并启动后台定时器:
def create_timer(total_seconds): minutes, seconds = divmod(total_seconds, 60) threading.Timer(total_seconds, announce_timer, args=[minutes, seconds]).start()逐行拆解:
divmod(total_seconds, 60)是 Python 内置函数,同时返回商与余数:divmod(147, 60)得到(2, 27),即 2 分钟 27 秒,分别解包给minutes与seconds;threading.Timer(total_seconds, announce_timer, args=[minutes, seconds])创建了一个"总秒数后执行announce_timer"的定时器对象,args列表中的两个元素会依次作为announce_timer的minutes与seconds位置参数传入;.start()立即启动该定时器线程。此时主线程继续运行,用户仍可进行下一次语音输入;倒计时结束时announce_timer在独立线程中被调用。
3.4 反馈"定时器已启动"
紧接着在create_timer末尾追加"timer started"播报的拼接逻辑,规则与announce_timer相同——只包含数值大于 0 的时间单位:
announcement = '' if minutes > 0: announcement += f'{minutes} minute ' if seconds > 0: announcement += f'{seconds} second ' announcement += 'timer started.' say(announcement)完整的create_timer(含启动播报)见 code-timer/pi/smart-timer/app.py。
3.5 接入 process_text 主流程
最后,在process_text末尾把两个任务串起来:先从服务端拿到秒数,秒数大于 0 才创建定时器:
seconds = get_timer_time(text) if seconds > 0: create_timer(seconds)只有当秒数大于 0 时才创建定时器,这一条件巧妙地复用了get_timer_time失败返回 0 的设计:无论调用失败还是 LUIS 没识别出set timer意图,都不会误创建定时器。process_text完整实现见 code-timer/pi/smart-timer/app.py。
四、运行验证与预期输出
运行前确保函数应用(Function App)也在运行(云端发布或本机运行均可),然后启动设备端程序:
pi@raspberrypi:~/smart-timer $ python3 app.py Set a two minute 27 second timer. 2 minute 27 second timer started. Times up on your 2 minute 27 second timer.三段输出的含义分别为:
Set a two minute 27 second timer.——来自语音转文字阶段的识别结果(process_text第一行print(text)打印);2 minute 27 second timer started.——create_timer中"定时器已启动"的播报;Times up on your 2 minute 27 second timer.——threading.Timer倒计时结束后announce_timer的到点播报。
五、两种硬件实现的差异对照
本任务的代码按运行平台分为两份,结构一致,仅在语音采集入口上有区别:
- Raspberry Pi 版code-timer/pi/smart-timer/app.py:使用 Grove 按键 + PyAudio 录音。
capture_audio()在按键按住期间以 16 kHz 采样率采集 PCM 帧并封装成 WAV 缓冲,之后convert_speech_to_text(buffer)调用 Azure Speech REST 接口,主循环while True中等待按键按下再开始一轮"录音 → 识别 → 处理"; - 虚拟设备版code-timer/virtual-iot-device/smart-timer/app.py:使用 Azure Speech SDK 的
SpeechRecognizer连续识别,通过recognizer.recognized.connect(recognized)订阅识别事件,在recognized回调中把args.result.text交给process_text,主循环仅time.sleep(1)保持进程存活。
两者中get_timer_time、say、announce_timer、create_timer、process_text的实现完全一致——这也印证了本任务的精髓:设备端差异被封装在"语音怎么进来"这一层,而"拿到文字后如何设置定时器"的逻辑与硬件解耦、可以复用。
六、关键设计与边界情况小结
- HTTP 状态码即业务信号:200 表示"识别成功并已换算秒数",404 表示"意图不是 set timer",设备端统一以
status_code != 200短路返回 0; - 0 的双重含义:
get_timer_time返回 0 既可能是网络/服务端错误,也可能是意图不匹配,process_text只需一个seconds > 0判断即可安全跳过; - 线程不阻塞:
threading.Timer让倒计时在后台线程进行,主线程可继续处理下一次语音指令,符合"智能音箱"式的交互模型; - 可读性优先的播报:分钟/秒分别判空拼接,保证英文播报语法自然,且该文案集中于
say函数,为后续接入文本转语音预留了唯一修改点。
七、后续步骤
本任务对应完整课程的中间环节,后续将在本课继续把say从print升级为真正的**文本转语音(Text-to-Speech)**播报(面向 Raspberry Pi 的步骤见 pi-text-to-speech.md,面向虚拟设备的见 virtual-device-text-to-speech.md);课程级挑战要求你利用 SSML 调整语音的重音、停顿与音调,练习项目则是为上一课添加的"取消定时器(cancel timer)"意图补齐设备端取消逻辑(见 assignment.md)。课程整体概览与原理(含文本转语音三阶段:文本分析、语言分析、波形生成)可参考 3-spoken-feedback/README.md。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考