27浏览
查看: 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 种宠物的完整列表如下:
#
英文名
中文名
主体颜色 (RGB565)
视觉色相
1
capybara
水豚
0xC2A6
浅棕
2
duck
鸭子
0xFFE0
明黄
3
goose

0xFFFF
纯白
4
blob
史莱姆
0x07F0
亮绿
5
cat

0xC2A6
浅棕
6
dragon

0xF800
正红
7
octopus
章鱼
0xA01F
紫色
8
owl
猫头鹰
0x8430
橄榄
9
penguin
企鹅
0x041F
深蓝
10
turtle
乌龟
0x07E0
翠绿
11
snail
蜗牛
0xD8FE
乳白
12
ghost
幽灵
0xFFFF
纯白
13
axolotl
美西螈
0xFB1E
粉红
14
cactus
仙人掌
0x07E0
翠绿
15
robot
机器人
0xC618
银灰
16
rabbit
兔子
0xFFFF
纯白
17
mushroom
蘑菇
0xF810
朱红
18
chonk
胖墩
0xFD20
橙色

属性与状态详解:



以「猫」(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 状态函数指针
};


三个属性:
  • name(物种名):既是 BLE JSON 中 "species":"cat" 的匹配键,也是屏幕上物种名称的显示文本。桌面端可随时通过 BLE 下发物种切换指令。
  • bodyColor(主体颜色):16 位 RGB565 格式。猫用 0xC2A6,即 R=0x18、G=0x15、B=0x06 → 浅棕色。这个颜色用于像素精灵的轮廓/主体渲染。
  • states[7](状态动画表):7 个函数指针,每个对应一种角色状态。状态枚举定义在 persona.h 中:

enum PersonaState {
  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 支持两种角色渲染方式:
  • ASCII 像素动画(buddy.cpp + 18 个物种文件):用等宽字符在屏幕上绘制像素级宠物,每个字符 6×8 像素,6px 字符宽度在 240 宽屏幕上最多排 40 字符——对宠物绘制绰绰有余
  • GIF 角色渲染(character.cpp):从 LittleFS 加载 GIF 文件,通过 AnimatedGIF 库逐帧解码播放

系统启动时检查 /characters/ 目录,有 GIF 角色就用 GIF,没有就走 ASCII 模式。菜单里 "ascii pet" 选项可循环切换 18 种 ASCII 物种。

函数指针表驱动的多态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



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail