Loiter 造岛记:48 小时,六座彩虹岛

Loiter 造岛记:48 小时,六座彩虹岛

接上回:Loiter 招魂记:当大屏上的小人集体蒸发。前四篇讲的是怎么从零搭一个卡片机社交厅;这一篇讲的是怎么把它变成另一个东西——同一副骨架,换一整套灵魂。

6 月 14 日早上,loiter.polly.wang 的大屏上还是水墨山水。狐灵 NPC 飘在草原上等你搭话,两个人碰面要走"配对→接头暗号→技能比拼"的仙侠流程,地图配色是墨绿和金。

48 小时后,同一个域名、同一台 Azure VM、同一块 240×135 的 Cardputer 屏幕——

六座彩虹岛屿的大屏地图,两个小人分别站在不同颜色的岛上,一道彩虹弧连接它们

水墨变成了海洋,六座岛屿各自一种 Pride 色,两个人碰面只需要键入对方的名字然后按回车。

器官移植,不是推倒重建

事情的起因是一个 Pride Month workshop 的需求:75 分钟,约 16 个人,每人一台 M5 Cardputer,走一段从"你是谁"到"你遇到谁"到"你成了谁"的故事弧。六座彩虹岛屿,各自对应一种颜色——红橙黄绿蓝紫。三道 quiz 题决定你属于哪座岛。之后是跨岛握手:你去找别人说 HI,对方接受了,你们各自收集到对方的岛色。终场时,AI 根据你的岛屿、收集的颜色、回答过的问题,给你写一首九行短诗。

这个流程和 v1 仙侠社交厅的"配对→比武→收技能"几乎毫无重合。但底下那根管子——WiFi 配网、MQTT 传输、WebSocket 大屏、OTA 空中升级、服务端权威状态机——完全一样。

所以我们做了一件类似器官移植的事:血管不断,心脏换了。

具体来说,保留的是:

  • Azure VM 上 mosquitto + uvicorn + cloudflared 三个 systemd 服务,一行配置都没改
  • 固件里 WiFi STA 连接 + NVS 配网 + MQTT PubSubClient + 非阻塞重连 + OTA 双分区升级,一行也没动
  • WebSocket 传输层的 snapshot/join/leave/move/ping,连心跳间隔都没换

删掉的是全部 v1 玩法层:配对引擎、技能系统、成就树、NPC 对话、任务链。十几个 Python 模块归档到 _v1_archive/

新建的是 Islands of Color 的玩法核心:岛屿分配(quiz 映射 6 种色)、光谱收集(5 格 Pride 色)、HI 握手(跨岛换色 + 同岛共鸣)、AI reading(双语短诗)。

8000 多行代码,6 套测试,48 小时。

三段故事弧

Pride 隐喻在这个产品里不是标签,是结构。

Phase 1:你是谁? 给自己起名、换装、回答三道性格问题。quiz 的答案决定你属于哪座岛——EMBER(红)、HEARTH(橙)、SPARK(黄)、GROVE(绿)、TIDE(蓝)、MIST(紫)。登岛的瞬间,你的光谱第一格自动填上本色。这一步叫自我认领

Phase 2:你遇到谁? 键入别人的名字按回车,对方设备弹出邀请,按 Enter 接受、按 DEL 拒绝。如果你们在不同岛,各自收集到对方的颜色,大屏上飞出一道跨海彩虹弧。如果你们在同一座岛——颜色不加,但动画不同:那叫"共鸣"。你还可以倾斜设备在地图上走动、猛摇设备触发集体跳跃、用匿名频道往大屏发消息。这一段叫接触多元

Phase 3:你成了谁? 大屏暗下来,设备进入 loading 界面。云端的 Claude 根据你的岛屿、收集到的颜色、握手次数和 quiz 原始选择,写出一首双语短诗——九行英文,九行中文,附一个身份标签。"EMBER KEEPER / 守火者"。大屏上的小人脚下亮起五格光谱,hover 上去能读到完整的诗。这一段叫多元共在

M5 Cardputer 显示 ISLANDS OF COLOR 欢迎屏,彩虹条纹横跨 240×135 像素

最惊险的 bug

说"48 小时",听起来像流水线。真相是最后 8 小时几乎全耗在一个 bug 上。

Phase 3 的进度条满了,但屏幕定格不跳。设备能收到数据——我在 serial 里看到了 [RD] RX len=719on_reading SET ready=1,状态变量切到了 P3-02,goto 也执行了,screen=15 白纸黑字——但屏幕纹丝不动

这个症状有两个独立的根因,任何一个单独修掉另一个都会继续作祟。

第一个是中文字符膨胀json.dumps 默认 ensure_ascii=True,每个汉字变成 \uXXXX——6 个字节替代 UTF-8 的 3 个。9 行英文 + 9 行中文的 reading payload 从 690 字节膨胀到 966 字节,超过了设备 MQTT 库的 1024 字节缓冲区。超限的包被静默丢弃,没有任何报错。

诊断神器是一行 shell:mosquitto_sub -t 'reading/#' -C 1 -v | wc -c。看到 966 的瞬间就明白了。修复:服务端 ensure_ascii=False,设备缓冲区从 1024 扩到 2048。

但改完之后,进度条依然卡住

第二个是dirty flag 被同帧吃掉——一个状态机经典 bug。loop 里原来的写法是:

if (g_dirty) {
    dispatch_redraw();   // 画屏,内部可能 goto_screen → 设 g_dirty=true
    g_dirty = false;     // 紧跟着就清掉了
}

draw_p3_01 发现 reading 到了,调用 goto_screen(P3_02)g_dirty=true。但下一行立刻 g_dirty=false。下一帧 g_screen 已经是 P3-02 了,dirty 却是 false,而 P3-02 不在需要逐帧重绘的列表里——永远不会再画,定格在进度条最后一帧。

修复只有两个字符的差距:先清后画

if (g_dirty) {
    g_dirty = false;     // 先清
    dispatch_redraw();   // 再画——draw 内部 goto 重设的 dirty 不会被吃
}

这个教训适用于所有状态机:凡是 draw() 内部能改 dirty flag,清 dirty 就必须在 draw 之前。否则转场信号会被同帧的清理吞掉。

AI 写的诗

Phase 3 每个人看到的诗都不一样。服务端把你的岛屿、光谱、握手次数和 quiz 选择打包成 prompt,交给同一台 VM 上跑的 CopilotX(Copilot API 代理)去调 Claude 生成。9 行英文叙事诗 + 9 行中文逐行对应,再加一个英文身份标签和中文翻译。

设备上看到的效果是这样的:

M5 Cardputer 显示 AI 生成的 reading——EMBER KEEPER / 守火者,五格光谱色块,中文短诗

"EMBER KEEPER / 守火者"。五格光谱里两格有色,三格还空着——这个人只跟两个不同岛的人打过招呼。

如果 AI 超时了怎么办?每座岛有一套手写的 fallback 诗,调性和 AI 一致。设备端 30 秒没收到回复就用本地兜底,体验不崩。

那条不变的管子

写到这里回头看,最有意思的反而不是新功能,而是那些什么都没改的部分

WiFi 配网的代码是 v1 写的,NVS 存储凭据、扫描网络、画配网页面——一行没动。MQTT 的重连逻辑、LWT 遗言机制、OTA 双分区升级——一行没动。大屏的 WebSocket 重连、指数退避、快照恢复——一行没动。甚至上一篇里刚加的心跳自愈,也直接在 v2 里生效了。

这些"无聊的管道"在 v1 的真机测试里反复被打磨过——音频冲突卡死、PubSubClient 缓冲区溢出、cloudflared 杀空闲连接、OTA semver 字典序排序 bug——踩过的坑全在 CLAUDE_v1_lab.md 里躺着,v2 一个都没再踩。

这大概是做 v2 最省心的部分:不用从零证明"这根管子是通的"。你只需要证明"换了心脏之后它还通"。

收束

同一块比信用卡大不了多少的屏幕。

第一篇里,它显示的是墨绿色的草原、一只名叫 Vix 的狐灵、和一个问你"你喜欢哪种食物"的 NPC 对话框。

现在它显示的是六色彩虹、一个叫 ISLANDS OF COLOR 的标题、和一句 welcome to your color

变的是皮肤。不变的是从 240×135 像素到 Azure 云端再到大屏的那条链路——以及凌晨两点还在 Review 里挑毛病的两个 AI。

下一步是真正面对 16 个人的现场。dry run 过了两台,但两台和十六台是两个物种。

不过那是下一篇的事了。


这是 Loiter 系列的第五篇。第一篇讲 Loiter 是什么,第二篇讲 AI 协作故事,第三篇讲硬件踩坑,第四篇讲心跳自愈。

Comments