|
27| 0
|
[K10项目分享] 把 Claude 的愚人节彩蛋跑在 行空板K10上:BLE 应用与 ASCI... |
|
本帖最后由 topdog 于 2026-5-1 14:16 编辑 本文阐述《行空板K10调用Claude Buddy桌面宠物》的原理。以这个适配项目为载体,聊聊其中涉及的 BLE 应用开发和 ASCII 宠物动画系统设计。 BLE 通信:从扫描到双向传输 Nordic UART Service 协议 BLE 设备通过 GATT(Generic Attribute Profile)协议交互数据,其层级结构为 Service → Characteristic → Value + Descriptor。Claude Desktop 的彩蛋通过 Nordic UART Service (NUS) 向外暴露数据,这是一个广泛用于 BLE 串口透传的标准服务, 固定 UUID 如下: Service: 6e400001-b5a3-f393-e0a9-e50e24dcca9e RX (Write): 6e400002-... — 客户端写入,K10 接收 TX (Notify): 6e400003-... — K10 推送,客户端订阅后接收 选择 NUS 的好处是生态兼容性——nRF Connect、LightBlue 等调试工具直接识别,开发阶段调试方便。RX 配置了 PROPERTY_WRITE 和 PROPERTY_WRITE_NR(无响应写),TX 配置了 PROPERTY_NOTIFY 并附加 BLE2902 Descriptor(CCCD)。客户端必须先向 CCCD 写入 0x0001 才能收到 TX 推送——这是排查“已连接但收不到数据”问题的第一站。 连接生命周期 ble_bridge.cpp 实现的连接状态机如下: startAdvertising("Claude-XXXX") → Central 连接 → onConnect → 客户端订阅 TX Notify → 双向通信 → 断开 → onDisconnect → 自动重新 startAdvertising 设备名取 MAC 地址后两字节(Claude-XXYY),方便多设备区分。心跳保活由 bleKeepAlive() 每 5 秒推送一个 \n,防止部分 BLE 栈因长期无数据主动断开。 安全配置 配对安全使用 ESP-IDF Bluedroid 栈的静态配置: BLESecurity::setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND); BLESecurity::setCapability(ESP_IO_CAP_NONE); BLESecurity::setKeySize(16); ESP_LE_AUTH_REQ_SC_BOND 开启 LE Secure Connections + 密钥绑定,设备重启后自动恢复加密无需重配对。ESP_IO_CAP_NONE 声明设备无输入输出能力,BLE 栈退化为 Just Works 配对,无需 PIN 码——这是无键盘外设的实际可行选项。 配对码在三个回调中清零(onConnect、onDisconnect、onAuthenticationComplete),防止异常路径导致过期配对码残留在屏幕上。 MTU 与分包K10 将 MTU 协商到 517 字节(BLEDevice::setMTU(517)),但应用层在 bleWrite() 中主动限流到 180 字节,每次 notify() 后 delay(4),给接收端 BLE 栈时间将数据从 ATT 层推到应用层。 文件传输协议 除了状态同步,K10 还支持通过 BLE 上传自定义 GIF 角色到头像库。BLE MTU 限制下,xfer.h 实现了一个三阶段分块协议: char_begin → 擦除旧角色,检查 LittleFS 可用空间 file → chunk(Base64) × N → file_end (可重复多个文件) char_end → 加载新角色 空间检查在 char_begin 完成,将剩余空间与回收的旧角色空间合并计算(free + reclaimable),不足时传输开始前返回错误,不浪费用户等待时间。二进制数据经 Base64 编码后通过 mbedtls_base64_decode 解码写入 LittleFS。 ASCII 宠物动画: 18 种物种 × 7 个状态 物种总览固件内置了 18 种 ASCII 像素宠物,每种宠物通过统一的 Species 结构体定义: struct Species { const char* name; // 物种英文名 uint16_t bodyColor; // 主体颜色(RGB565) StateFn states[7]; // 7 个状态动画函数指针 }; 18 种宠物的完整列表如下:
属性与状态详解: 以「猫」(cat)为例 以猫为例,可以直观地理解 Species 结构体每个字段的含义和在动画系统中的作用: extern const Species CAT_SPECIES = { "cat", // name — BLE 协议中通过 "species":"cat" 远程切换 0xC2A6, // bodyColor — RGB565 浅棕色,勾边/主色调 { cat::doSleep, cat::doIdle, cat::doBusy, cat::doAttention, cat::doCelebrate, cat::doDizzy, cat::doHeart } // 7 状态函数指针 }; 三个属性:
P_SLEEP, // 睡眠 — 宠物闭眼、打鼾、冒 "Z" 粒子 P_IDLE, // 待机 — 缓慢眨眼、偶尔伸懒腰,占比最高的默认状态 P_BUSY, // 忙碌 — 快速敲击、打字、面部专注表情 P_ATTENTION, // 注意 — 警觉抬头、眼珠追随、等待用户操作 P_CELEBRATE, // 庆祝 — 跳起、挥舞、撒花/星粒子特效 P_DIZZY, // 眩晕 — 摇晃、眼冒金星、踉跄步态 P_HEART // 爱心 — 冒心形泡泡、脸红、幸福表情 }; 7 种状态由 BLE 实时驱动。 桌面端每次收到 Claude 的事件变更(开始生成、工具调用、审批完成等),通过 BLE 下发 JSON 更新 personaState,K10 固件调用 sp->states[personaState](tickCount) 切换到对应动画函数。整个过程仅需一行查表——无需 if-else 或 switch,18 种物种共享同一调度路径。 猫的动画帧序列示例: 猫的 IDLE 状态通过字节数组驱动 10 个帧姿态,(t/5) 将 200ms tick 降频为 1 秒节拍: REST (70%) 伸懒腰 (15%) 眨眼 (10%) 舔爪 (5%) /\_/\ /\_/\ /\_/\ /\_/\ ( o o ) → ( - - ) → ( - o ) → ( ~ ~ ) (> <) (> <) (> <) (> <) 帧索引在 SEQ 数组中的重复次数决定了各姿态的持续时长权重——REST 出现 14 次,特殊动作仅 1~2 次。 修改动画只需调整数组中帧索引的位置和重复次数,无需触及渲染逻辑。 两套渲染模式K10 支持两种角色渲染方式:
函数指针表驱动的多态18 种物种通过 Species 结构体和函数指针数组实现多态调度: typedef void (*StateFn)(uint32_t t); struct Species { const char* name; uint16_t bodyColor; StateFn states[7]; // SLEEP/IDLE/BUSY/ATTENTION/CELEBRATE/DIZZY/HEART }; static const Species* SPECIES_TABLE[] = { &CAPYBARA_SPECIES, &DUCK_SPECIES, ..., &CHONK_SPECIES }; // 调用时只需一行查表 const Species* sp = SPECIES_TABLE[currentSpeciesIdx]; sp->states[personaState](tickCount); 18×7 = 126 个动画函数共享全局 tickCount(每 200ms 自增),各函数内部通过帧序列数组驱动: // 猫 idle 状态的帧序列片段 static const uint8_t SEQ[] = { 0,0,0,3,0,1,0,2,0, 7,8,7,8,7, 0,5,0,6,0, 4,4,0, ... }; uint8_t beat = (t / 5) % sizeof(SEQ); buddyPrintSprite(P[SEQ[beat]], 5, 0, 0xC2A6); (t/5) 将 200ms tick 映射为 1 秒节拍。帧索引通过重复次数赋权重——REST(索引 0)在数组中占比最高形成常态,特殊动作如伸懒腰、眨眼只出现 1-2 次。 整个动画系统没有关键帧插值,纯粹由字节数组数据驱动,修改动画只需改数组。 脏标记优化 buddy.cpp 的 buddyTick() 在状态、物种、tick 三者均未变化时跳过整个渲染流程——避免 150KB 帧缓冲的擦除与 SPI DMA 重推。宠物处于 idle 状态时,连续数十秒零渲染开销。 GIF 模式GIF 角色存放在 /characters/{name}/ 下,通过 manifest.json 定义各状态对应的 GIF 文件列表。单状态可配置多个 GIF,characterTick() 单文件循环播放,多文件则轮换变体(切换间隔 ANIM_PAUSE_MS = 800ms)避免视觉疲劳。peekMode 通过隔行隔列子采样将 GIF 缩小到 1/4 尺寸,用于信息页面画中画渲染,无需额外 Sprite 缓冲。 成长系统:BLE 数据的持久化与统计 桌面端通过 BLE 推送的 JSON 包含累积 token 数、审批事件等数据,K10 端在 stats.h 中使用混合策略持久化。 低频事件(审批/拒绝)立即写 NVS Flash。高频事件(token 累积,每 5 秒心跳)只在跨越等级边界时写入——token 在 RAM 中累加,每 50K 升一级才触发 NVS 写入。最坏情况断电丢失 ≤50K token 进度。 桌面端发送的是脚本自启动以来的累积值,设备重启后基准归零但桌面值未变,直接计算会重复计。stats.h 用 _tokensSynced 标志实现首次看到锁存——第一次收到数据只记录基准不计算,第二次开始才做增量。若检测到 bridgeTotal < _lastBridgeTokens(桌面端重启),自动重置基准。 宠物心情(MOOD)基于审批响应时间的中位数而非平均值计算。statsMedianVelocity() 维护一个 8 元素环缓冲区,用插入排序(n≤8 时比快排高效)求中位数。中位数对离群值天然鲁棒——用户中途离开桌面 300 秒不会拖偏整体情绪。statsMoodTier() 在中位数基础上叠加拒绝率惩罚:拒绝 > 50% 扣 2 级,> 33% 扣 1 级。 I2S 音频与功放时序 beep() 函数使用 ESP-IDF 原生 I2S 驱动,16kHz 采样率生成方波: int periodSamples = 16000 / freq; buf = ((written + i) % periodSamples < periodSamples / 2) ? 6000 : -6000; 幅值 6000 约为 I2S 满幅度(±32768)的 18%。 功放使能采用先关后开的时序——PIN_AMP_GAIN 先 LOW 10ms 放电,再 HIGH 30ms 等待功放稳定,最后 i2s_channel_enable 输出音频。不同事件用不同频率:审批提醒 1200Hz/80ms,确认 2400Hz/30ms,拒绝 600Hz/60ms,均在代码各分支中硬编码。 时钟模式下的自主行为 当设备处于时钟模式(无工作会话、USB 连接、RTC 有效、BLE 未连接),宠物不再被动等桌面端 JSON,而由本地逻辑驱动: if (clocking && !bleConnected()) { int h = _clkTm.tm_hour; if (h >= 1 && h < 7) activeState = P_SLEEP; else if (weekend) activeState = P_HEART/P_SLEEP轮换; else if (friday && h >= 15) activeState = P_CELEBRATE/P_IDLE轮换; ... } 三个维度决定行为:小时(作息)、星期几(工作日/周末)、特定时段(周五下午、午饭)。每个时段内用 millis()/N % M 引入时间分片轮换,让宠物在没有桌面信号时呈现自主行为。 结语从一个终端里的 ASCII 彩蛋,到桌面上有物理屏幕、BLE 通信、音频反馈的实体伴侣,这个项目覆盖了 BLE 应用开发的完整链路——NUS 服务配置、安全配对、MTU 协商、分块文件传输、心跳保活。动画侧则展示了函数指针表驱动的多物种系统、字节数组驱动的帧序列、脏标记渲染优化等嵌入式图形常用模式。 笔者已经将项目分享到:https://gitee.com/pdtopdog/k10_claude_desktop_buddy |
沪公网安备31011502402448© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed