系统组成
Parent Master 由四部分组成,彼此之间只靠 HTTPS 和一个身份标识相连:
M5Stack CoreS3 上的 ESP32-S3 固件。内含小智对话客户端、待机界面、家长端功能模块(计时、听写、噪音、使用时长)、局域网控制台服务(端口 8032)。
语音识别、大模型、语音合成都在这里。设备通过 WebSocket 连它。我们不改它,只通过"工具"(MCP)把设备能力暴露给它。
Cloudflare Worker + D1 数据库 + R2 存储,域名 parentmaster.tankxu.com。存计时、听写、事件、使用时长,下发配置,分发固件。
设备控制台 /(家长和交付人员用,含家长端页面)与群控后台 /admin(项目方用,看全部设备)。
身份:设备 MAC 就是账号
设备每个请求都带 X-Device-Mac,服务端以此归档数据;家长端和群控后台按 MAC 查询。这意味着目前没有"家长账号",谁知道 MAC 谁就能看这台设备的数据(见 已知边界)。群控后台另有管理员令牌保护。
数据流一眼看
开机引导与配网
全新设备(或刷了一体包的设备)第一次开机进入引导。引导的目标是三件事:连上家里 Wi-Fi、绑到 xiaozhi.me 账号、告诉家长去哪里装家长端。每一步都有语音提示(edge-tts 合成的女声,与后来听写提醒同一音色)。
- 连 Wi-Fi。设备先试 NVS 里存过的网络;没有就开一个名为
ParentMaster-xxxx的热点,手机连上后浏览器打开配网页,把家里 Wi-Fi 告诉它。这一步走的是小智的配网组件,不是我们自己写的,所以和小智官方固件的体验一致。 - 绑定小智。屏幕显示 6 位配对码,家长在 xiaozhi.me 的"添加设备"里输入。设备每隔几秒问一次服务器有没有被认领;认领成功语音提示"配对成功"。这一步不可跳过,没有它就没有对话能力。
- 家长端。显示 parentmaster.cn 文字与二维码,纯提示,可跳过。
第 3 步的页面上还显示设备的本机 IP,家长在设备控制台里连接设备时要填它。以后随时可在设备的设置 → Network 里看到 IP、网关、DNS、WiFi 名称与信号、MAC,或者直接问小智"你的 IP 是多少"。
为什么不用蓝牙配网
小智官方 App 的蓝牙配对流程依赖官方 App 版本,项目方不可控;热点配网只要手机有浏览器就行,交付时不需要额外装 App。
小智对话与设备工具(MCP)
理解这一节,后面的计时、听写、噪音才好理解。小智云端的大模型能调用设备注册的"工具";工具带一段描述,这段描述会被云端拼进系统提示词。所以工具描述就是我们写给大模型的行为规范,客户不需要在 xiaozhi.me 上改任何提示词。
| 工具 | 做什么 | 什么时候被调 |
|---|---|---|
self.device.read_message | 取出设备想告诉小智的完整消息(噪音介入指引、新听写提醒) | 设备念出触发短语后,大模型按描述立刻调它 |
self.study.start_timer / stop_timer / timer_status | 学习计时 | 孩子说"开始学习 / 学完了 / 学了多久" |
self.dictation.get_pending | 只返回听写清单(标题、条数、进度),屏幕显示清单画面 | 孩子问"有没有听写" |
self.dictation.start | 返回某一份的全部词句,屏幕进入听写画面 | 孩子说"开始第几个";每开始一份都必须调 |
self.dictation.mark_progress / complete / stop | 记进度、标完成、中途停止 | 每念 5 个 / 全部念完 / 孩子说不写了 |
self.learning.show_page | 在屏幕上显示一段文字给孩子看着读:古诗、课文句子、组词造句、成语释义、应用题原文、英语句子。设备内置了覆盖全部常用汉字的位图字体,任意中文都能直接排版,不依赖服务器 | 孩子要"看着读"而不只是"听着"的时候 |
self.learning.show_formula | 把公式排版显示在屏幕上:分数、根号、上下标、π 等数学符号、希腊字母、带反应条件的化学箭头(\xrightarrow{点燃})和等号(\xlongequal{高温}),支持多行。模型写 LaTeX 子集,设备自己排版(不出图、不依赖服务器);标题一行、底部一行变量说明可选;约 30 秒后自动收起,多行更久 | 孩子问"某某公式是什么/怎么算"、问化学式或反应方程式、小智要讲带分数/根号/上下标的算式或解题步骤 —— 凡是嘴上要念式子,先显示再念 |
self.homework.get_state | 陪写作业流程的状态。孩子说"开始写作业/继续写作业"时小智先调它,按返回的 next_step 从当前步骤接着走。流程:①问今天在学校感觉怎么样 ②简短回应情绪 ③问有哪些作业科目 ④问每科难度、预计用时、最拿手或最想先做的 ⑤设备按"中等→最难→擅长或轻松"算出顺序(孩子累了先从容易的开始)由小智提议 ⑥同意后开始计时并告诉孩子"想休息、听不懂或要调顺序直接说" | 每次孩子提到写作业;会话被打断再开新会话时,状态还在设备里,不会从第一步重新问 |
self.homework.set_mood / set_subjects / start / finish_subject / reset | 流程各步的记录:心情、科目及难度用时、开始(自动开学习计时)、某科写完(全部完成自动停计时)、清空。状态存在设备 NVS,断电重启不丢,最后一次更新超过 10 小时自动清零。每次到达"提出顺序""开始""完成"时设备会向服务器发一条 homework 事件,群控后台事件流里能看到当天的科目、难度、顺序和总用时 | 由小智在流程中自动调用,家长不用操作 |
self.reminder.add / list / done / today | 提醒事项。聊到未来某一天的事(考试、比赛、交作业、约定、生日)小智就记下"日期 + 到那天该做什么"(如 2026-09-22 询问考试情况)。设备把它存在本地并同步到家长端;到期那天设备把"今天要做的事"写进 self.reminder.today 这个工具的描述里 —— 工具描述每次会话开始都会被服务端拼进小智的系统提示词,这就是"设备动态改提示词"的通道;同时设备在空闲时用"读设备消息"让小智主动开口问。问过后小智调用 done 记为完成,不重复问。家长在控制台 08 面板可以看、加、改、删 | 聊天中自动;家长也可以手动添加"提醒带跳绳"之类 |
self.learning.show_text / show_stroke_order / clear | 把汉字、词语或英文单词放大显示在屏幕上(带拼音或音标、音节);播放单个汉字的笔顺动画;关闭展示。图片是离线预渲染好放在 R2 的(常用汉字全部、词库里的英文词),Worker 按设备放行;不在库里的英文词由设备自己画简卡;听写进行中不可用 | 孩子问"这个字怎么读 / 怎么写 / 这个词怎么拼",或家长在教认字 |
self.device.noise_verdict | 大模型听完屋里声音后给结论:是不是争吵 | 噪音介入流程的最后一步 |
设备怎么"主动开口"
小智的架构里只有人能唤醒它,设备自己不能让它说话。我们绕了一道:
- 设备把完整情境(比如"家长刚布置了《成语听写》,共 3 个词,问孩子要不要开始")存在本地。
- 等设备空闲(不在听、不在说)时,通过唤醒词通道念一句固定短语,默认"读设备消息"。这个通道只有约 37 字节且不接受英文和标点,所以内容不能直接塞进去。
- 大模型收到短语,按
read_message工具描述里的约定调它,拿到完整内容,照着做。
"静默动作"
工具描述里反复出现的"静默动作"是告诫大模型:调工具前后别说"我来记录一下"。否则孩子会听到一堆系统旁白。这是提示词层面的约束,偶尔仍会失效。
同步模型:用到才同步
设备不轮询、服务端不推送。设备只在下面几种时刻向服务端取一次状态(计时、听写待办、配置),并把攒着的上报一起发出去:
联网对时后同步一次,顺带补报断电前没结束的会话。
孩子一开口就同步(60 秒内不重复),所以"进小智问有没有听写"总能拿到最新的。
家长端页面会直接敲设备的局域网接口让它立刻同步,新听写几秒内就到。
平时每 60 分钟一次;学习计时进行中每 5 分钟一次(家长远程结束计时也能被感知)。两个间隔可由服务端按设备下发。
服务端下发配置
每次同步的响应里带 config:两个同步间隔,加上三项家长可改的设备偏好 —— 音乐播放开关(关了以后孩子让小智放歌,小智会告知"家长关闭了音乐";开机时就是关的则连音乐工具都不注册)、待机屏保模式(名言/单词/混合)、单词等级(1~6 的子集)。设备夹到安全范围后存 NVS 立刻生效,和本地一致的项不重复写。家长端控制台「设备偏好」面板改单台,群控后台可批量下发。
为什么这么设计
- 请求量。每台每天几十次请求,100 台设备远在 Cloudflare 免费额度内。若改成 10 秒轮询,100 台一天要 86 万次。
- 时效。家长在家操作时是即时的;不在家时,新布置的听写最多等一小时,而孩子一开口就会拿到。这是有意的取舍。
- 不挂长连接。设备内部内存紧张,一条常驻 TLS 连接会挤占小智对话本身的资源。
"在线"是怎么判定的
没有心跳,只有同步。服务端以"最后一次同步距今不超过 该设备的平时间隔 + 5 分钟"判为在线。所以一台平时 60 分钟同步的设备,断电后最长 65 分钟才会显示离线。想更灵敏就把该设备间隔调短,代价是请求量。
学习计时
状态以服务端为准。设备只是显示和转发:孩子对小智说"开始学习",设备调服务端开始一条计时;家长端也能直接开始或结束。设备右上角显示"学习中 12:34"药丸,待机界面和小智界面都有。
边界情况
- 忘了结束。服务端在任何请求到来时把超过 4 小时未结束的计时自动收尾,标记为"超时收尾"。
- 设备断电。计时进行中设备每分钟往 NVS 记一次"还活着"。下次开机若发现上次计时没结束:距最后存活超过 10 分钟,就以那一刻结束它(标 power_loss);10 分钟以内(比如刷固件重启)视为还在学,不动。
- 已有计时时再开始。不会新建,原样返回进行中的那条。
统计口径:跨午夜的计时按重叠部分分到两天。今日累计、次数在家长端概览和群控后台都能看到。
听写
听写是最复杂的一条链,涉及服务端、设备待机界面、小智对话和一个专用画面。完整时序:
get_pending,只拿到清单;屏幕切到清单画面:"待做的听写 · 3 份",逐行列出标题、词数、已写到第几个。start(id) 才拿到词句;屏幕进入听写画面,只显示任务名和"2 / 3"进度条,绝不显示词句。每个词念两遍、慢速,等孩子说"好了"再念下一个;每念 5 个记一次进度。complete,画面显示 Done 停 4 秒,还有别的待办就退回清单画面,没有就关掉。孩子说"不写了"调 stop,进度保留,下次从断点继续。为什么要一个专用画面
小智说的每个字都会显示在对话气泡里。不盖住,孩子照着抄,听写就没意义了。这个画面整屏不透明盖在最上层,只在小智模式下存在,退出小智自动消失;5 分钟没有任何进度动作也自动关(清单画面 2 分钟)。
为什么拆成"清单"和"开始"两步
早期版本一拿清单就把全部词句交给大模型,结果是孩子只是问一句"有没有听写",屏幕就进了听写画面;做完第一份再做第二份时,大模型手里已经有词句,不再调任何工具,画面就再没出现过,进度也没记。现在词句只能从 start 拿,每一份都必须显式开始,画面自然每份都出现。
噪音检测(争吵介入)
目标不是测环境噪音,而是识别"家里在吵架"并让小智温和地介入一句。只在小智模式下有效,因为只有那时麦克风是开着的。
怎么算分贝
固件在小智的音频输入链路上加了一个钩子,只取麦克风声道(不取回放声道,否则会把设备自己放的声音算进去),按秒算能量得到 dBFS,再加一个校准值(默认 90)换算成"估算分贝"。参考:安静房间约 43,正常说话约 60,大声争执 70 以上。校准值可在设备设置里改,建议交付时用手机分贝仪对一次。
触发条件
滚动 20 秒窗口内,估算分贝超过阈值(默认 72)的秒数达到 6 秒就触发。阈值、秒数、校准值、开关都在设备设置里,家长端噪音面板也能改(需同一局域网)。
触发后的流程
- 设备抬头做出"注意到了"的动作,等待小智空闲。
- 让小智说一声"咦?"然后进入聆听,听屋里在说什么。
- 大模型根据听到的内容调
noise_verdict给结论。是争吵:说一句温和的劝解,事件按 argument 上报,之后 5 分钟内不再触发;不是争吵(电视、音乐、正常热闹):什么都不说,事件按 not_argument 上报,之后 10 分钟内静默;3 分钟没给结论按 unknown 上报。
事件字段:平均分贝、峰值分贝、持续秒数、阈值、判定结果、大模型给的一句理由。
使用时长
三类时长,口径各不相同,别混着比较:
孩子或家长显式开始/结束的计时。见第 05 节。
孩子和小智的一轮对话:设备离开空闲算开始,回到空闲 30 秒没再动算结束。只有孩子真的说过话的会话才记,开机问候、主动提醒、噪音介入不算。
设备通着电运行的时间。只在群控后台展示,家长端不展示。
断电怎么补
对话进行中每分钟往 NVS 记一次存活时间;意外断电后下次开机按最后存活时间补报一条,标记 power_loss,误差不超过 1 分钟。开机时长同理:设备每次请求都带开机秒数,服务端把同一次开机的请求合并成一段;设备每分钟记存活时间,下次开机把上一段的精确结束时刻补上,并按复位原因区分"断电"与"软重启"。
按天统计以北京时间零点切分,跨午夜的记录按重叠部分分到两天。
待机屏保
设备空闲时的屏保有两种内容,都由服务端提供、设备缓存:
- 名人名言。中英双语,英文条目带中文译文。设备一次拉 200 条,缓存 3 天,离线也能显示。
- 英语单词卡。按难度分 1~6 级(由低到高,最高覆盖大学四级词汇),词库来自开源词典。每张卡显示单词、音标、释义和音节拆分(如
BEAU · TI · FUL),配一段整词点亮再逐音节高亮的动画,帮助孩子按音节记拼写。
屏保模式与单词等级在设备控制台"设置 → 屏保"里改,存 NVS。词库改动只需改服务端,设备 3 天内自然更新。
设备控制台(/)
控制台页面本身由服务端提供,但页面里的功能分两类,理解这一点能解释大部分"为什么不好使":
| 页面 | 数据来自 | 要求 |
|---|---|---|
| 总览 / 控制 / 设置 / 诊断 / OTA | 设备局域网接口(8032 端口) | 浏览器和设备在同一局域网。摄像头画面、截图、设备设置、实时分贝都在这里。 |
| 家长端 · 学习计时 / 使用时长 / 对话记录 / 听写 / 事件 | Parent Master 服务端 | 任何网络都能看。输入设备 MAC 加载;设备已连接时自动填入。 |
| 家长端 · 噪音的"设备侧设置"与实时读数 | 设备局域网接口 | 需同一局域网。 |
家长端里"开始计时 / 结束计时 / 新建听写"写的是服务端;若浏览器同时连着设备,会顺手让设备立刻同步一次,右上角计时条和听写提醒几秒内出现。
群控后台(/admin)
给项目方运营用,一屏看全部设备。功能与机制:
- 设备总览。每台的在线状态(按第 04 节的规则)、固件版本(带构建 sha)、最后同步、进行中的计时、今日学习/对话/开机时长、待办听写数、今日噪音事件、同步间隔。设备第一次同步就会自动出现,不需要登记。
- 批量下发。勾选若干台(或全部/在线的),下发同步间隔,或给每台各布置一份听写。写进服务端,设备下次同步时生效。
- 事件流。跨设备的噪音/拍照事件,带判定理由。
- 单台详情。最近 30 份听写(可撤回待做的)、7 天计时与对话、按天使用时长、最近 50 条事件;改名;删除设备(连历史数据一起,不可逆)。
访问控制
一把管理员令牌,存在 Cloudflare 的 secret 里,代码和仓库里都没有。页面登录后令牌只保存在该浏览器;所有后台接口都要求 Authorization: Bearer 头,不对就 401。换令牌:wrangler secret put ADMIN_TOKEN,旧令牌即刻失效。目前是单一共享令牌,没有多账号和操作日志。
固件分发与升级
版本号是真的了
早期为了防止小智云端的固件升级把我们的固件覆盖掉,版本号被写成 9.9.9。现在上游那条升级路径在固件里被整个关闭(激活、配置照常拿,永不接受它下发的固件),版本号恢复为真实的 x.y.z,来自固件仓库的 VERSION 文件。设备上报 1.0.1 fdaa2e17c 这样的"版本 + 构建 sha",群控后台、一体包文件名、下载接口都用它。
设备默认不自己升级
这是有意的产品决定。设备每次同步会拿到"可用版本"和"目标版本"两样信息:可用版本只用来在家长端显示"有新版本";只有目标版本被写入时设备才会动。写入目标版本的入口有两个:
- 家长端「固件更新」面板。看到"有新版本可更新"后点"立即更新",页面会提示保持设备空闲。浏览器与设备在同一局域网时会立刻通知设备,否则等设备下次同步。
- 群控后台「固件」页。运营可以对勾选的设备或全部设备强制推送某一版;对历史版本的同一操作就是"强制回滚"。
还有第三个入口:设备自己的设置 → Firmware → Check for Updates。它同样只认 Parent Master 通道:向服务端要一次最新版,比自己新就申请安装并在屏幕上显示进度。早期这个入口走的是小智上游,版本号恢复真实后会被官方固件覆盖,已改。小智对话里"按 URL 升级固件"的工具也被关闭。
设备侧怎么装
- 只在待机界面、没在放音乐、没有听写画面时开始;小智模式下内存紧张,不做。
- 屏幕提示"正在更新固件,请让我待在这里",同时向服务端报"下载中"并带进度。4MB 镜像走 HTTPS 从 R2 下载,约 1 分钟,写进另一个应用槽位。
- 写完后按服务端给的镜像摘要校验(注意是不含文件末尾 32 字节自身哈希的摘要),对不上就撤回、继续跑当前版本。
- 校验通过重启。新固件起来后联网同步成功才被标记为有效,起不来会自动回滚到旧版(bootloader 层面的保护)。
- 设备同步时报上来的 sha 和目标一致,服务端就清掉目标、记为"更新完成"。同一目标最多尝试 3 次,失败原因会显示在家长端和群控后台。
渠道:正式与 beta
群控后台上传镜像时选"正式版"或"beta"。设备有个配置 ota_channel(默认 stable):stable 设备只把最新的正式版当"可用版本";beta 设备把正式和 beta 里最新的一版当可用版本。这样可以先把新版发成 beta、把几台设备切到 beta 渠道试几天,再在列表里一键"设为正式"。上传本身不会推送到任何设备。
一体刷机包只给新机器和救砖
https://parentmaster.tankxu.com/ota/latest 下载,会跳到带版本号的文件名;/ota/latest.json 给出 sha、md5、大小。包含 bootloader、分区表、应用、资源,从地址 0x0 整块烧录,NVS 留空,烧完开机进引导。
esptool --chip esp32s3 -p <串口> -b 921600 write-flash 0x0 parentmaster-full-xxx.bin
M5Burner:Custom 固件 → 选本地 bin → 地址 0x0 → Burn,不需要发布到社区。刷过其他固件的机器建议先整片擦除。已部署的设备日常升级不需要碰 USB,走上面的通道。
稳定性机制
这台设备要连续开机几个月,固件里有几道自愈,项目方知道它们存在就够了:
- 网络看门狗。每 60 秒 ping 一次路由器;连续 3 次不通再用 TCP 敲路由器确认;确认网络死了就主动断开 Wi-Fi 触发重连;重连后 5 分钟内又死则重启设备。设备在群控后台显示离线但通着电时,通常几分钟内会自己回来。
- 断电补报。对话、计时、开机时段都按 NVS 里的分钟级存活时间补报,见第 08 节。
- 接收缓冲加大。原厂配置在 HTTPS 突发下会让网络整段死掉,已调大;这是今年 9 月定位并修复的一个稳定性问题。
- 内存红线。设备内部内存非常紧张(小智对话时可用不到 40KB),新增功能都要先算内存账。这解释了为什么很多能力放在服务端而不是设备上。
数据与接口速查
数据库(D1)
| 表 | 存什么 |
|---|---|
pm_devices | 设备:MAC、名字、首次/最后同步、固件、按设备配置 |
pm_study_sessions | 学习计时:开始、结束、标签、结束原因 |
pm_dictation_lists | 听写:标题、词句、进度、状态(待做/完成/撤回) |
pm_events | 事件:噪音、拍照等,带 JSON 载荷 |
pm_chat_sessions | 对话时长:开始、结束、时长、结束原因 |
pm_power_sessions | 开机时段:开机时刻、最后见到、结束、结束原因 |
接口分组
| 前缀 | 谁调 | 鉴权 |
|---|---|---|
/api/v1/device/*、/api/v1/timer/*、/api/v1/dictation/*、/api/v1/usage/*、/api/v1/events | 设备 | X-Device-Mac 头 |
/api/v1/parent/{mac}/* | 家长端页面 | 暂无(见下一节) |
/api/admin/* | 群控后台 | Bearer 管理员令牌 |
/ota/* | 任何人 | 无,只读 |
/quotes、/words | 设备屏保 | 无,只读 |
完整字段说明在代码仓库 stackchan-worker/docs/API.md。
已知边界与待办
- 家长端接口没有鉴权。知道设备 MAC 就能读写这台设备的计时和听写。上线给真实家庭前必须补家长账号或每设备令牌。这是当前最重要的待办。
- 噪音阈值未按户校准。默认值来自一台设备的实测,不同房间、不同摆放位置差异很大;误报和漏报都会有。
- 主动开口有延迟且可能失败。受小智架构限制(第 03 节),设备正在对话时不能插话。
- "静默动作"靠提示词。大模型偶尔仍会念出"我来记录一下"。
- 在线判定最长滞后 65 分钟。由同步模型决定(第 04 节)。
- 待机下的听写提醒每小时重复一次,直到有人处理。若嫌频繁可改为只提醒一次。
- 单一管理员令牌,无操作审计。
- 听写不批改,只负责念和记进度。
排查手册
| 现象 | 先看什么 | 常见原因 |
|---|---|---|
| 家长布置了听写,设备没反应 | 群控后台该设备"最后同步"时间;设备是否在夜间静音时段;是否正在对话 | 不在同一局域网时要等下一次同步(最长一小时)或孩子开口;静音时段不提醒;对话中不能插话 |
| 群控后台显示离线,但设备亮着 | 等 5 分钟再看;局域网里能否 ping 到 | 同步间隔内的正常滞后;网络看门狗正在自愈;真正断网 |
| 小智说"我来记录一下"之类的旁白 | 无需排查 | 大模型偶发不守"静默动作"约定 |
| 噪音一直不触发 / 频繁误触发 | 家长端噪音面板的实时分贝(需局域网) | 校准值不合适;阈值 72 对当前房间过高或过低;设备不在小智模式 |
| 控制台摄像头、截图、设置不可用 | 浏览器和设备是否同一局域网;设备 IP 是否变了 | 这些功能只走局域网 |
| 刷了一体包开机没进引导 | 是否从 0x0 烧录;旧机器是否先擦除 | 地址写错;旧 NVS 里还有引导完成标记 |
| 对话时长里没有某次聊天 | 那次孩子是否真的说过话 | 只有用户开口的会话才记;开机问候、主动提醒不算 |
| 学习计时显示"超时收尾" | 无需排查 | 超过 4 小时没结束,服务端自动收尾 |
再往下查
设备控制台"诊断"页有内存、重启原因、最近崩溃记录;设备 8032 端口的 /study 能看到同步与看门狗状态。需要固件日志时接 USB 看串口(115200)。