想给没有硬件、不装 PyQt6 的人一个能直接打开的入口——包括只是路过这个仓库、只有一个浏览器的人。
一台设备、三个通道撑不起一块大屏。地图、翻牌器、3D 这类组件堆上去只是装饰。
改为做一个控制台,重心放在这个项目别处看不到的两样东西上:看得见的串口链路,与可追溯的问答。
| 页签 | 内容 |
|---|---|
| 监测 | 三通道读数、越限状态、滚动曲线;实时模式下可切换风扇模式、改通风阈值 |
| 串口链路 | 每一帧的判定:正常、重同步、CRC 失败、格式错误、忽略、载荷错误;点开一帧,逐字段拆开,并把帧里声明的 CRC 与浏览器现场重算的 CRC-32 并列 |
| 环境问答 | 每个回答的全过程,按发生顺序逐步点亮:规则识别、取数层给出的事实、模板原句、模型收到了什么、模型原文、复核或分类的标签、五道出口检查逐道的结果、重试、执行与最终答案 |
| 历史 | 整段曲线 |
放在顶级目录 web/,不放进网关包。 网关是无界面服务,Android 客户端也不在网关里。
把页面塞进网关,等于让一个呈现端住进另一个呈现端的包里。页面由启动脚本挂载到 /web/,网关包本身不知道它存在。
不引入前端构建链。 原生 JavaScript 与 Canvas,没有 npm、没有打包。一个要先 npm install 才能打开的演示,
对访客来说是多出来的故障点。
普通脚本,不用 ES 模块。 浏览器禁止 file:// 页面加载模块脚本,而这个页面要能直接双击打开。
所以用普通 <script> 加一个全局命名空间;回放数据也是一个 .js 文件,用动态插入 <script> 加载——fetch 在 file:// 下同样不可用。
不让网页直连串口。 浏览器有 Web Serial API,可以直接读串口。不用它:那会让一个呈现端绕过全部分层、感知底层通信方式。
| 打开方式 | 数据从哪来 |
|---|---|
网关托管(http://<PC>:8000/web/) |
实时:REST + WebSocket,断线按 1→32 秒指数退避重连 |
双击 web/index.html |
先尝试连本机网关,连不上就进入回放 |
| GitHub Pages | 直接回放,不探测网关 |
视图不区分数据源。两种数据源推给状态仓库的是同一种消息;能力不同的地方(回放里不能改设置)由数据源声明, 视图据此把控件置灰并写明原因。
回放数据由 scripts/build_web_replay.py 离线生成,生成过程就是在线系统本身:
录制的读数 ─► 按协议重新编码成帧 ─► 字节管道 ─► 真实的 HardwareDeviceReceiver + LinkMonitor
─► 真实的报警、统计、通风判定 ─► 网关自己的序列化函数 ─► 与在线时 WebSocket 收到的一模一样的消息
只有时间戳被改写成读数当时的时刻。所以回放不是一份用 JavaScript 另写的演示逻辑,而是换了数据源的在线管线。 脚本里有断言:一小时的会话必须是 3489 帧、零校验错误;故障注入会话里,判出的重同步数必须等于注入的杂散字节数, CRC 失败数必须等于翻转数。
一个诚实的说明:一小时实验采集时只保存了读数、没有保存原始字节,所以回放里的帧是按协议重新编码的,页面上照实写明。
| 改动 | 为什么 |
|---|---|
| 网关加通风接口 | 直译已有的 ApiInterface 方法 |
LinkMonitor + ApiInterface 两个新方法 |
链路健康原本只是接收器内部的两个整数,任何呈现端都看不到。监视器只听不判:拼帧与校验仍由原来的代码完成,不接监视器时行为逐字节不变 |
| 问答答案附带追溯记录 | 出口检查从”只给结论”拆出一个”给结论与判定代码”的版本;一组逐项对照的测试保证两者对任何输入给出相同的文本与来源——只增加可见性,不改变任何判定 |
字节管道与 --mode virtual |
模拟模式没有字节流,链路页无从显示。要在没有开发板时看到真实的帧,需要一个走真实接收链路的模式(见 01) |
追溯记录不违反”模型不接触数值”(02):它是对已经发生之事的只读记录, 模型没有因此多拿到任何输入或权力;页面显示的模型原文,正是出口检查已经采纳或拒绝过的那一句。
最初的追溯是事后的:问答结束才推一条完整记录,中间发生了什么——被拦下之后的重试、规则没认出时模型回了哪个标签、 指令复核一致与否——页面上只剩一个结果。后来改成按发生顺序、带时间推送:
AnswerStep),每步带问题编号与距提问的毫秒数。组合根每轮用
drain_steps() 取一次,网关推成 assistant_step 消息,一步一条。沿用整个系统一律轮询的形态,不向服务层注入回调,
服务层也就不知道网关存在。explain_checks(),把五道各算一次并给出依据
(找不到出处的数字、命中的词、长度与上限)。真正做判定的 judge() 不动;一项测试保证展示出来的”第一道不通过”就是 judge() 实际依据的那一道。question_id,REST 返回、迟到的答案和每一步都带着它,页面据此把步骤挂到对应的问题下。判定不变是这样确认的:改动前后,用固定读数、固定时钟和确定性的脚本模型,把全部题库 340 句在接模型与不接模型两种模式下各跑一遍, 答案文本、来源、意图、执行结果、改写与判定逐字节相同。
回放里有一组用真实模型录下的实时流程(experiment-data/assistant/),按录制时的原始耗时重演,
没有模型也能看到一句回答是怎么来的。模型的思考链(thinking 输出)没有做:它要打开模型的思考模式,改变输出分布,
已有的实测数据就都不再适用。
写这个控制台的同一天,新增的依赖规则测试第一次运行就查出一处违规:网关为了链路事件的两个值类型直接 import 了 application。
值类型随即移入 core。见 architecture.md。
浏览器里逐页试用时修掉的问题,典型的几个: