产品文档 · 交互方案 · 原生 App

SomaLoop App 设计

V8 无屏手环配套 App 的设计文档。前两部分是产品与内容:一套让 AI 成为常驻入口的交互方案,加一份按页面罗列的 MVP 内容清单。第三部分是接入记录——接 L1 SDK 期间遇到的问题,以及需要 SDK 方定夺的事项。

2026-09-07交互原型 A / B视觉与精细度对齐 WHOOP / Oura
第一部分

SomaLoop App 产品文档

V8 无屏手环的配套 App。手环没有屏幕,Agent 就是界面:它先开口、可追问、每句话带依据、记得你说过什么、没事就闭嘴,并且是市场上唯一会替你按下测量键的教练。本文档定义产品原则、信息架构、场景引导系统、Agent 交互规范、数据与算法层、视觉标准和演示稿计划。

2026-09-06依据:研究合集四份 + App 调研合集四份SDK:V8 iOS libBleSDK / Android v8sdk2.0下一步:HTML 演示稿
9:41●●● 78%
TUE · SEP 6
早上好,Tank
基线 14/14
82
RECOVERY
7:12
SLEEP
3.8
LOAD
7:05 · 醒来后
昨夜恢复得不错,今天可以按计划跑第二次间歇

夜间心率 52,比你 14 天基线低 3;HRV 68 ms,在基线区间上沿。马拉松计划第 3 周:今天是 4 × 4 分钟,心率区 3 到 4。

依据 · 夜间心率依据 · HRV 14 夜记忆 · 马拉松 10/12
开始跑步模式今天想休息
试试问我 · 这周睡眠债周末爬山计划最近头疼
问 SomaLoop 任何事↑
TODAYBODYPLANSME

PROTOTYPES两套交互方案

两套方案共享同一套设备、指标与 Agent 能力,只改变用户每天进入 App 后理解身体的方式。点击卡片可直接体验。

00一句话与目标

  1. 一句话。SomaLoop 是 V8 无屏手环的 Agent 伴侣:你说一件生活里的事(下个月跑马拉松、最近加班头疼、周末想爬山),它把手环变成那件事的传感器与提醒器,每天先开口,每句话可点开看依据,需要确认时替你按下测量键。
  2. DevKit for BioHacker。这只手环连同这个 App 一起,卖给的是想拿到自己原始生理数据、愿意动手做实验、也可能拿它去做产品的人。App 既是消费者能用的成品,也是开发者套件的参考实现:原始数据流、导出、依据、实验、MCP 端点是一等功能,放在 Me 页最显眼的位置,而不是藏在「高级设置」里。
  3. 不是什么。不是又一个无订阅的 WHOOP 仿品(厂商 JCVital 已经做了那一版),不是聊天框加仪表盘,不是医疗器械。
  4. 阶段目标。第一阶段用 HTML 演示稿在 15 分钟内证明三件事:Agent 能按下测量键;主动消息不废话且带依据;场景引导让一个从没用过健康 App 的人在 30 秒内知道该对它说什么。第二阶段落成原生 App,作为开发者套件的白标参考实现。
  5. 视觉与精细度对齐 WHOOP 与 Oura。深色、单一强调色、大数字环、克制的动效、每一个空状态与加载态都有设计。这是硬性要求,不是加分项。

01产品原则

十一家 AI 教练的形态与失败模式(App 调研合集第一、二部分)浓缩成七条。每条都会在验收里出现。

  1. Agent 就是界面。手环无屏,用户与身体数据的全部交互经由 Agent 发生。Today 页不是仪表盘,是 Agent 的时间线。
  2. 先说,再等你问。每天最多三张主动卡(醒来、运动后、睡前),加事件触发的推送;没有超出个人基线的变化就不生成。WHOOP 的原则:无事则静。
  3. 每句话可点开依据。任何含数字或结论的句子都附「依据」芯片,点开是原始数据段、时间戳与基线。后台统计接地率并展示。这是对 Google、WHOOP 最集中负评(引用不存在的数据)的直接回应。
  4. 能动手。Agent 可发起测量(心率、血氧、HRV、ECG)、设提醒(普通、吃药、喝水)、设久坐提醒、远程开启运动模式、写入记忆。七家大厂无一能按下测量键,这是唯一不用比模型聪明的地方。
  5. 记得,并且让你看见它记得。记忆分目标、健康史、生活事件、关注与忽略四类,可查、可改、可删;主动消息必须读记忆,否则重蹈 WHOOP Daily Outlook 的投诉。
  6. 说人话,不说病名。不出现房颤、呼吸暂停、高血压等疾病名,不做血糖与血压数值;说「节律不规则」「血氧低于你的基线」;所有输出附「健康参考,非医疗建议」。
  7. 数据永远免费,只卖解读。原始数据、导出、基础指标不收费;若有付费层,只放 Agent 解读、计划与长期指标(订阅分层报告结论)。
  8. 可拆开看,可接出去。面向 BioHacker 的承诺:每个分数都能拆到原始数据;每段数据都能导出(CSV / EDF / WFDB);实时 RR、PPG、ECG 流可以开给自己的程序;Agent 用的每个工具都以 MCP 暴露,用户可以换成自己的模型。App 里做的每一件事,开发者都能用 SDK 复现。

02用户与场景

首发用户

个人佩戴者,25 到 45 岁,戴过或考虑过 WHOOP、Oura、Fitbit Air,对「订阅制」和「只给分数不给原因」不满,愿意对一个 App 说自己的生活。机构版(护士站、班组、教练看队员)作为后续模块,本版只在信息架构里留位。

DevKit 用户

与首发用户重叠但动机不同:量化自我与生物黑客(自己写脚本看 HRV、做咖啡与睡眠的对照实验)、研究者与学生(要原始 ECG 与时间戳)、AI Agent 开发者(要 MCP 与事件流)、准备做产品的团队(把这个 App 当白标起点)。App 对他们的承诺见第一章第八条;专属入口在 Me 页的「Lab」区:原始数据流开关、导出、实验、MCP 端点与 API key、接地率与记忆保存率面板。演示稿里 Me 页要把这块做出来,因为它是「DevKit for BioHacker」这句话的证据。

核心用例

用户用一句话描述一件生活里的事,Agent 把它变成一个「玩法」(Playbook):明确目标、要收集什么数据、手环要做什么动作、每天什么节奏说话、什么时候结束并复盘。第四章定义了十二个玩法,三个是主演示。

下个月参加马拉松 · 训练负荷与恢复 最近加班多有点头疼 · 压力与睡眠债排查 制定周末爬山计划 · 一次性活动 想戒掉熬夜下周出差倒时差感觉要感冒了刚开完会有点心慌开始做力量训练咖啡到底影响我睡眠吗给医生留一段心电记录备孕,想看周期爸妈独居,想远程看着

03信息架构

四个标签加一个常驻输入框。Today 是 Agent 时间线,其余三个标签是 Agent 的「工作台」,每一屏右下角都能追问。

标签是什么主要内容Agent 在这里做什么
TodayAgent 时间线,默认落地页顶部三环(恢复、睡眠、负荷)与基线进度;按时间倒序的主动卡、测量卡、确认卡、系统事件;「试试问我」引导条;底部常驻输入框主动开口、回应提问、发起测量、请求确认
Body指标与基线夜间五项基线(心率、HRV、呼吸率、皮温、血氧)各自的个人区间与今日位置;睡眠分期与贡献项;恢复分的白盒拆解;负荷 7 对 28 天比值;历史与导出每张图下一句解释与「问这个」;点任何数字进依据视图
Plans玩法与计划进行中的玩法(如马拉松第 3 周)、今日任务、里程碑;玩法库(十二个入口);实验(如「咖啡与睡眠」两周对照)生成与调整计划、按恢复门控今日强度、复盘
Me记忆、设备、Lab记忆页(目标 / 健康史 / 生活事件 / 关注与忽略);手环(电量、固件、自动监测时段、闹钟与久坐提醒);主动消息三旋钮(频率、长度、语气);Lab:原始数据流(RR / PPG / ECG)开关与实时波形、导出(CSV / EDF / WFDB)、实验、MCP 端点与 API key、接地率 / 依据覆盖率 / 记忆保存率面板、自带模型 key把用户改的记忆与旋钮立即应用到下一条消息

为什么不做「Coach」标签。Google 和 WHOOP 都证明了聊天页不该是一个目的地。SomaLoop 的对话发生在 Today 时间线里(主动卡下方直接回复)和每一屏的追问入口,不存在一个单独的「去和 AI 聊天」的地方。会话历史在 Me 里可查。

04场景引导系统

用户不知道该对一个健康 Agent 说什么,这是所有 AI 教练冷启动的共同问题。SomaLoop 用「玩法」把开放式对话变成有限的、可预期的入口,并在四个位置持续引导。

引导出现的四个位置

位置 1 · 首次配对后

「你最近在忙什么?」

配对成功、基线开始积累时,不问年龄体重(那些从手环个人信息读),而是给六个场景芯片加一个自由输入框。选一个即建立第一个玩法,Agent 说明接下来两周它会做什么、什么还不能做(基线未满)。

位置 2 · Today 的「试试问我」条

按上下文轮换的三个建议

规则:周四周五推「周末计划」;连续三天就寝晚于基线推「熬夜」;步数或运动会话突增推「训练计划」;日间 HRV 连续偏低推「压力排查」;夜间皮温偏高推「像要生病」;日历有旅行推「倒时差」。每条点开就是一段预填好的用户消息,用户可改再发。

位置 3 · 输入框占位符

轮播的示例句

「下个月参加马拉松」「最近加班多有点头疼」「制定周末爬山计划」「咖啡到底影响我睡眠吗」……每 6 秒换一句,点击即填入。空状态和基线未满期间尤其重要。

位置 4 · 数据触发的邀请卡

Agent 看见变化,邀请你开一个玩法

「这三天你的睡眠中点比平时晚 1 小时 40 分,要不要一起做个两周的早睡实验?」邀请卡与主动卡同构,但结尾是一个开玩法的按钮,而不是反问。

玩法(Playbook)的统一结构

每个玩法是一份声明式配置,Agent 依此执行;开发者未来可以用同样结构写自己的玩法。

playbook:
  id: marathon-4w
  trigger: ["马拉松", "跑马", "半马", "全马", "参赛"]      # 命中即建议开启
  intake: [赛事日期, 目标完赛时间, 每周可训练天数, 最近一次长跑距离]   # 一次问完,写入记忆
  needs: [夜间心率, HRV, 睡眠分期, 运动会话心率, 步数]   # 手环数据
  actions: [开启跑步模式, 训练日晨间恢复门控, 长跑日前夜提醒, 赛前 3 天减量, 赛日 5:30 闹钟]
  cadence: {morning: 训练建议, post_workout: 负荷与区间复盘, evening: 明日预告, weekly: 周报}
  guardrails: [负荷比 > 1.4 强制降级, 夜间心率高于基线 8 bpm 连续两晚建议休息]
  memory_writes: [目标: 10/12 马拉松 4:00, 生活事件: 训练周期]
  end: {at: 赛后 3 天, deliver: 复盘卡 + 是否续接「恢复两周」玩法}

三个主演示玩法

下个月参加马拉松

触发词:马拉松 / 半马 / 全马 / 参赛 · 时长 4 到 6 周 · 数据充足度要求:基线 ≥ 7 夜
用户说
「下个月 12 号跑马拉松,目标 4 小时,一周能练四天。」
Agent 回
「记下了:10 月 12 日,目标 4:00,每周 4 次。我会按你每晚的恢复决定第二天练多重。今天先做一件事:跑 20 分钟轻松跑,让我拿到你的心率区间。」附「开始跑步模式」按钮(SDK 运动模式 Run)。
每日节奏
醒来:恢复门控后的今日训练(如「4 × 4 分钟,区 3 到 4」或「今天休息,夜间心率高于基线 6」)。运动后:本次负荷、区间分布、与计划的偏差,一句话。睡前:明日预告与就寝建议(基于睡眠需求)。每周日:周报,负荷 7 对 28 天比值曲线、HRV 趋势、下周计划。
手环动作
运动模式远程开始 / 结束;长跑日前夜 22:30 闹钟(普通型);赛前一周每晚 60 秒 HRV 测量(SetDeviceMeasurementWithType);赛日 5:30 闹钟。
护栏
负荷比 > 1.4 或夜间心率连续两晚高于基线 8 bpm,自动降级并说明原因;用户可否决,否决写入记忆。
结束
赛后 3 天复盘卡:完赛时间与目标、四周 HRV 与夜间心率走势、哪几天的降级是对的。询问是否开启「赛后恢复两周」。
演示重点
恢复门控的那一张早卡;运动后一句话复盘;周报里的负荷曲线。视觉上是全 App 最像 WHOOP 的一页,要做到位。

最近加班多有点头疼

触发词:头疼 / 加班 / 累 / 压力大 / 睡不好 · 时长 7 到 14 天 · 基线 ≥ 7 夜(未满则只做即时测量与记录)
用户说
「这两周加班多,下午老是有点头疼。」
Agent 回
先给已有证据:「这两周你的睡眠债累计 4 小时 20 分,日间 HRV 比基线低 22%,工作日 14 到 17 点久坐最长 3 小时 40 分。头疼是否与这些相关,我不能下结论,但可以帮你把线索排清。」然后请求一次即时测量:「现在方便的话,我让手环测 60 秒心率和 HRV,看看此刻的状态。」
每日节奏
醒来:睡眠债与昨夜恢复,一句话。工作日 14:30:久坐与喝水提醒(久坐提醒 + 喝水型闹钟)。睡前:建议就寝窗口(按睡眠需求与时型)。第 7 天:小结卡,把头疼记录(用户回复「今天疼 / 不疼」的快速芯片)与 HRV、睡眠、久坐三条线并排。
手环动作
60 秒 HR + HRV 测量;久坐提醒 14:00 到 18:00 每 60 分钟;喝水闹钟 10:30、15:30;可选每日一次 SpO2。
护栏
用户提到「剧烈头疼」「视物模糊」「持续加重」等词,Agent 停止排查,给出就医建议与「把最近一周数据打包给医生」按钮。不出现任何疾病名。
结束
两周后复盘:头疼天数与三条线的对照图;询问是否转成「早睡实验」玩法。
演示重点
「我说不舒服,Agent 让手环测一次」的即时测量流程(测量卡、进度环、结果回到对话);快速回复芯片收集症状;一周小结的并排对照图。

制定周末爬山计划

触发词:爬山 / 徒步 / 登山 / 周末 / 户外 · 一次性活动 · 无基线要求
用户说
「周六想去爬山,大概 6 小时,海拔 2000 多。」
Agent 回
「好。周五我会看你的恢复情况给出出发前建议;周六出发时帮你开徒步模式,每 90 分钟提醒喝水,到高处我会请你测一次血氧;周日看恢复。」写入生活事件「9/13 爬山」。
节奏
周五睡前:明日预告,建议就寝时间。周六出发:「开始徒步模式」按钮(运动模式 Hiking);每 90 分钟喝水闹钟;用户说「到山顶了」或心率持续高位后,请求一次 30 秒 SpO2 测量并解读(「92%,低于你平时的 96 到 98,海拔影响,休息 10 分钟再看」)。结束:本次时长、心率区分布、负荷。周日醒来:恢复与昨日负荷的关系。
手环动作
运动模式 Hiking 开始 / 暂停 / 结束;喝水闹钟 3 个;SpO2 即时测量;周日不做动作。
护栏
SpO2 低于本人基线 6 个百分点以上且心率持续高位:建议停下休息并再测;不说「高原反应」。
演示重点
一次性活动的完整闭环只用五张卡;血氧测量卡是 Agent 主动请求测量的第二个例子;周日的恢复卡把「昨天做了什么」和「今天身体怎样」连起来。

其余九个玩法(摘要)

玩法触发核心数据手环动作结束与复盘
想戒掉熬夜熬夜 / 早睡 / 睡眠中点晚就寝时刻、睡眠中点、规律性、睡眠债就寝前 45 分钟提醒闹钟;周末不同时刻14 天:中点前移分钟数、规律性分
出差倒时差出差 / 时差 / 航班睡眠时刻、日间 HRV、皮温节律按目标时区分日推进的闹钟;抵达日 SpO2回程后 3 天:节律恢复曲线
感觉要感冒了感冒 / 不舒服 / 发烫夜间皮温偏差、RHR、HRV、呼吸率推算当夜加密皮温与 SpO2 自动监测;次晨 60 秒 HRV症状消失后:身体负荷信号回落图。措辞只说「身体负荷信号」
刚开完会有点心慌心慌 / 心跳快 / 紧张近 30 分钟心率与 HRV、RR 间期60 秒 HR + HRV;不规则时建议 5 分钟 ECG 并保存即时;ECG 记录进 Body 可导出
开始做力量训练健身 / 力量 / 增肌运动会话心率、恢复分、睡眠运动模式 Workout;训练日次晨恢复门控4 周:训练日与恢复的关系
咖啡到底影响我睡眠吗咖啡 / 酒 / 实验入睡潜伏期、深睡、夜间心率、HRV无;每日一条「今天喝了吗」快速芯片14 天 A/B 对照卡,标注样本量不足时不下结论
给医生留一段心电记录心电 / ECG / 医生 / 检查ECG 原始波形、RR、夜间心率5 分钟 ECG 引导(按电极、保持静止、进度)即时;导出 PDF 与原始数据;不做任何判读
备孕,想看周期周期 / 备孕 / 经期夜间皮温偏差、静息心率、手动经期记录无两个周期后:皮温双相图。声明不可用于避孕或受孕判断
爸妈独居,想远程看着父母 / 老人 / 远程被看护者的夜间五项基线、SOS、久坐SOS 长按上报;夜间偏离先请求测量再通知家属持续;进入机构版能力,本版只到「一个被看护者」

05Agent 交互规范

消息卡类型

类型何时出现结构操作
主动卡醒来 / 运动后 / 睡前,或事件触发时间戳与触发原因 · 一句判断式标题 · 正文含加粗数字(≤ 3 句) · 1 到 3 条行动项 · 迷你图(可选) · 依据芯片 · 结尾反问(可选)拇指上下、回复、行动按钮(开运动模式 / 设提醒 / 今天休息)
测量卡Agent 请求测量,或用户要求为什么测 · 测什么与时长 · 姿势提示(ECG:把手指按在电极上) · 进度环 · 信号质量 · 结果与解读开始 / 跳过 / 重测;结果自动进依据库
确认卡Agent 要做会影响他人或不可逆的事(发给医生、联系家属、改用药提醒)将要做什么 · 依据 · 两个明确按钮确认 / 取消;留痕
邀请卡数据变化建议开一个玩法观察到的变化 · 建议的玩法 · 两周内会做什么开始 / 以后再说(写入忽略 30 天)
记忆回执Agent 从对话中写入记忆「记下了:目标 · 10/12 马拉松 4:00」一行编辑 / 删除
拒答卡用户问诊断类问题诚实说明手环数据不能回答 · 至少一个可执行动作(做 ECG 记录 / 打包数据给医生)动作按钮;不出现单独的免责声明卡
系统卡基线进度、固件、电量、断连一行,弱化样式—

依据(Evidence)

每个含数据的句子绑定一个依据对象:指标、时间范围、数值、基线区间、图表类型。渲染为芯片,点开为底部抽屉,显示原始数据段(可缩放)、采样点、基线阴影区、数据来源(自动监测 / 即时测量)。没有依据的句子在开发者面板里标灰并计入未接地率。目标:含数据句子的依据覆盖率 100%,整体回答接地率 ≥ 80%(WHOOP 自报 56.6%)。

记忆

  • 四类:目标、健康史、生活事件、关注与忽略。每条记录来源对话与时间,可编辑、可删除,删除后下一条消息立即不再引用。
  • 写入回执必须出现在时间线里,用户看得见 Agent 记了什么。
  • 主动卡生成前必须读取记忆;开发者面板显示「应保存上下文的实际保存率」与「主动卡引用记忆的比例」。

三个旋钮与静默原则

  • 频率:少(只在偏离基线时)/ 标准(每天最多 3 张)/ 多(加上每次运动后与每周报)。默认标准。
  • 长度:一句话 / 标准 / 详细。默认标准(标题 + 3 句 + 3 条要点)。
  • 语气:直接 / 温和。默认直接。参照 Oura 的 direct / conversational。
  • 静默:夜间 23:00 到 7:00 不推送(SOS 与用户设置的例外除外);近 7 天已说过的结论不重复;用户点过「今天休息」后当日不再催训练。

语言规范

  • 数字先于形容词:「夜间心率 52,比基线低 3」而不是「你的心率很好」。
  • 比较对象永远是本人基线,不是人群;基线未满时明说「还不知道」,不用人群区间冒充。
  • 不做结论性判断(正常 / 没问题),只说相对变化与可能的生活因素。
  • 一张卡最多一个反问句。
  • 拒答后必须给动作。

06数据与算法层

全部确定性计算在手机侧完成,LLM 只做解读与编排。手环不跑 AI。

V8 能给什么

数据粒度用途
连续心率每 5 秒(自动监测时段内)夜间心率基线、运动区间、TRIMP 负荷
HRV 包(含压力、血压估算、血管老化度)约 30 分钟一条HRV 基线、日间压力;血压与血管老化度隐藏或标「参考」
SpO2自动周期 + 即时夜间基线、高海拔场景
皮温约 10 分钟一条夜间偏差、生病信号、周期
睡眠等级 + 活动量每分钟;单条最长 2 小时需拼接分期、效率、中点、规律性、睡眠债
步数 / 距离 / 卡路里每分钟久坐、活动量、负荷近似
PPI / RR 间期、原始 PPG、原始 ECG流式 / 会话节律不规则检测、呼吸率推算、ECG 记录导出;RR 流影响续航
运动会话18 种模式训练负荷与区间分布

没有的:呼吸率字段(从 PPI 的呼吸性窦性心律不齐推算)、原始加速度流(做不了肌肉负荷)、GPS(配速用手机 GPS 补)、震动马达指令(「震一下」用一分钟后的闹钟替代,演示时如实说明)。

指标层(第一版范围)

  • 夜间五项基线(心率、HRV、呼吸率推算、皮温、SpO2):7 到 30 天个人范围,超出即事件。这是 Agent 主动消息的主要事件源,也是 Body 页的第一屏。
  • 睡眠分:参照 Apple 公开权重(时长 50 / 就寝规律 30 / 中断 20)做参考实现,贡献项全部可见。
  • 恢复分:白盒,六项贡献(RHR 偏离、HRV 平衡、皮温偏离、恢复指数、睡眠、前日活动)各 0 到 100 并给量化说明。
  • 负荷:TRIMP 加 7 对 28 天比值,分档抄 Garmin / Polar 公开阈值;不做 EPOC 与肌肉负荷。
  • 日间压力:HRV 包加步数排除运动;粒度 30 分钟,标注。
  • 校准期:所有分数 14 夜;期间显示「已积累 N / 14 夜」与已能看的原始趋势;第 14 夜主动推送「从今天起可以看恢复分了」并解释组成。

Agent 架构

  • 内联注入(WHOOP 做法):当天摘要、基线、最近事件、记忆在 prompt 渲染时直接注入;只有「发起测量、设提醒、开运动模式、查历史、写记忆」走工具调用。
  • 确定性优先(Google 做法):事件检测、基线、负荷比全部是算法输出,LLM 不做数值计算。
  • 拒答门禁:合成 Persona 输入集覆盖疾病名提问,通过率作为发版前置。
  • 模型可换:Claude、GPT、Gemini、DeepSeek、本地模型均可;开发者可自带 key。

07屏幕与关键流程

这是演示稿要做出来的清单。带「★」的是三个主演示流程必经的屏。

屏内容要点状态
★ 配对与首次引导找到手环、连接动画、读取个人信息;「你最近在忙什么」六个场景芯片 + 输入框;说明未来两周会发生什么连接中 / 失败 / 成功
★ Today三环 + 基线进度;时间线(主动卡、测量卡、确认卡、邀请卡、记忆回执、系统卡);「试试问我」条;常驻输入框基线未满态 / 常态 / 手环未连接态 / 空状态
★ 测量流程测量卡 → 姿势提示(ECG 电极示意)→ 进度环与实时信号质量 → 结果卡 → 回到对话。心率 / HRV 60 秒,SpO2 30 秒,ECG 5 分钟进行中 / 质量差重测 / 用户取消
★ 依据抽屉原始数据段、采样点、基线阴影、来源、时间戳;可切换到 Body 对应页—
Body 总览夜间五项基线卡(今日值落在个人区间条上);睡眠、恢复、负荷入口基线未满态
Body 详情(睡眠 / 恢复 / 负荷 / 五项)大数字环 + 贡献项 + 7 / 30 / 90 天趋势;每张图下一句解释与「问这个」—
★ Plans进行中玩法卡(进度、今日任务、护栏状态);玩法库;实验无玩法空状态
玩法详情目标、节奏、手环动作清单、周报、复盘—
Me · 记忆四类列表,每条带来源与时间,编辑 / 删除空状态
Me · 手环电量与充电、固件、自动监测时段、闹钟与久坐提醒清单、SOS 联系人未连接
Me · 主动消息三个旋钮、静默时段、每日上限预览—
Me · 开发者面板接地率、依据覆盖率、记忆保存率、主动卡点开率、测量请求接受率;MCP 端点与 API key—

08设计语言

对齐 WHOOP 与 Oura 的标准,具体到可以验收的规则。页首的小样是按这套规则做的 Today 页。

--bg #0E1416
底色,近黑带青灰,不用纯黑
--card #151D1F
卡片,比底色亮一档,1px 描边 #223032
--accent #4FB8C4
唯一强调色(Agent、按钮、依据)
--recovery #7BE0A8
恢复环;黄 #E0A94A 负荷;红 #F27B6E 仅偏离
--ink #E9EEEC
正文;次级 #C3CFCC;弱化 #8A9A97
agent-dot
Agent 身份:青到绿的小圆点,不做人像

字体与数字

正文 IBM Plex Sans + Noto Sans SC;所有数值与时间戳用 IBM Plex Mono 等宽并开 tabular-nums;环内大数字 28 到 40 px,字重 600,字距 -0.02em;标签全大写 9 到 11 px 加 0.14em 字距。

图形

环:7 px 描边,背景轨道 #1C2628,进度用单色不用渐变;只有恢复、睡眠、负荷三个环,其他指标一律用「个人区间条 + 今日点」。迷你图:柱高不超过 44 px,当前值高亮为强调色,其余 #2B3A3D。基线区间用 8% 透明度的强调色阴影。

动效

环在页面出现时 600 ms 从 0 画到值,缓动 cubic-bezier(.2,.8,.2,1);测量进行中用呼吸式脉冲(1.6 s 周期);卡片进入 240 ms 淡入上移 8 px;尊重 reduce motion。不做粒子、不做 3D。

精细度清单

  • 每一屏都有空状态、加载态、错误态、未连接态四种设计。
  • 基线未满是常态不是异常:进度条、能看什么、还不能看什么,三样必须同屏。
  • 文字层级不超过四级;一张卡上最多两种字号。
  • 触控目标 ≥ 44 px;输入框常驻但不遮挡最后一张卡。
  • 深色为默认,提供浅色但两套都要过对比度 4.5:1。
14:32●●● 78%
测量 · HRV 与心率
保持坐姿,别说话
0:41 / 1:00
92
BPM · 实时

信号质量 良好 · 手环贴合正常

为什么现在测

你说「刚开完会有点心慌」。近 30 分钟自动监测的心率均值 89,比你日间基线 71 高 18,但采样稀疏,我需要一段连续 60 秒的数据才能判断。

依据 · 近 30 分钟心率依据 · 日间基线 14 天

测完我会告诉你:此刻心率与 HRV 相对你基线的位置,以及更像应激还是身体异常。我不会用疾病名下结论。

取消测量
TODAYBODYPLANSME

09边界与合规

  • 非医疗器械,仅供健康参考与研发;所有 Agent 输出附「健康参考,非医疗建议」。
  • 不出现疾病名;不做血糖、血压数值;HRV 包里的血压与血管老化度隐藏或标「参考」。
  • 不用人群阈值冒充个人基线;基线未满时明说。
  • 发给医生、联系家属、改用药提醒必须人确认并留痕。
  • 数据默认留在手机;云端可选且可自建;明示不用于训练。健康数据按敏感个人信息处理,单独同意。
  • 拒答门禁通过率作为发版前置;接地率作为发版门禁指标。

10度量与验收

指标定义目标为什么
依据覆盖率含数据句子中绑定依据对象的比例100%原则 3
接地率Agent 回答中引用了用户真实数据的比例(人工抽检 + 自动)≥ 80%WHOOP 自报 56.6%
记忆保存率应保存的上下文实际写入记忆的比例≥ 70%WHOOP 自测 46%
主动卡引用记忆率主动卡中至少引用一条记忆的比例(有记忆时)≥ 90%WHOOP Daily Outlook 投诉
测量请求接受率Agent 请求测量后用户开始的比例≥ 60%验证「能动手」被接受
主动卡点开率 / 关闭率被展开或回复的比例 / 被点「少一点」的比例≥ 40% / ≤ 5%避免 Garmin 式废话卡
拒答门禁通过率合成 Persona 疾病名提问被正确拒答并给动作的比例100%发版前置
D14 留存基线满 14 夜时仍每日佩戴的用户比例≥ 70%校准期是流失高峰
玩法开启率7 天内开启至少一个玩法的新用户比例≥ 60%场景引导是否有效

11路线与演示稿计划

阶段

  1. HTML 演示稿(本文档确认后开始):手机尺寸的可点击原型,深色,按第八章规则实现;三条主演示流程可完整走通;数据为脚本化的假数据,但结构与真实 SDK 字段一致;可部署到 ecg-devband-report.pages.dev/app/demo。
  2. 原生 App:建议 Flutter 一套代码,通过平台通道桥接 V8 的 iOS 静态库与 Android aar;BLE 连接与历史同步(每批 50 组、断点续传)在原生层;指标层与 Agent 编排在 Dart 层;多设备并发演示前必须真机实测。若团队更熟原生,则 iOS Swift + Android Kotlin 双写,SDK 直接调用。
  3. 白标化:品牌、配色、玩法库、旋钮默认值全部配置化,成为开发者套件的参考 App。

演示稿的三条主线(15 分钟)

主线 A · 5 分钟

我说心慌,它让手环测一次

  1. Today 常态页,输入「刚开完会有点心慌」
  2. Agent 给已有证据 + 测量卡
  3. 60 秒进度环与信号质量
  4. 结果卡:92 对基线 71,HRV 低三成,「更像应激」
  5. 点依据芯片看原始曲线
  6. 选「设 20 分钟后喝水提醒」→ 记忆回执
主线 B · 5 分钟

下个月参加马拉松

  1. 点「试试问我 · 下个月参加马拉松」
  2. 一次性 intake 四问
  3. Plans 出现玩法卡与第一天任务
  4. 快进到第 3 周:早卡恢复门控(一天练、一天降级)
  5. 运动后一句话复盘
  6. 周报里的负荷曲线
主线 C · 3 分钟

周末爬山

  1. 周五睡前预告卡
  2. 周六「开始徒步模式」
  3. 山顶 SpO2 测量卡(30 秒)
  4. 周日恢复卡把昨天与今天连起来

剩余 2 分钟:Me 页展示记忆与三个旋钮,再进 Lab 打开一条实时 RR 流、导出一段 ECG、展示 MCP 端点与接地率,说明「这就是 DevKit for BioHacker:消费者能用,开发者能拆」。

可点击的 HTML 演示稿:ecg-devband-report.pages.dev/app/demo(核心页与核心交互,假数据,结构与 SDK 字段一致)。

需要你确认的四件事。① 四个标签的命名(Today / Body / Plans / Me)与不设单独 Coach 页;② 首发只做个人版,机构视角留到下一版;③ 十二个玩法的清单与三个主演示的选择;④ 设计语言的配色与「只有三个环」的克制。确认后我开始做演示稿。

第二部分

SomaLoop App 内容清单

按页面与板块,罗列 MVP 要展示的每一条内容:它从哪来、对应哪个字段、现在做到哪一步。写这份的起因是界面上开始出现「这个数到底是谁算的」的模糊地带——服务端算的分数、手环读的事实、用户自己填的偏好,三者必须在每一条上分得清。

2026-09-28 更新对照:团队 MVP 需求九大项数据契约:soma.daily/v1、soma.sample/v1实现:Basic(简版)

00三个来源

界面上任何一个数字,来源必须是下面三者之一。混着说就会出现「这个数到底是谁算的」。

来源给什么拿法现状
手环 SDK心率、夜间心率、HRV、血氧、皮温、步数、睡眠分期、原始包DeviceCoordinator.synchronise() 逐流读历史已接;睡眠流之前没在同步清单里(已修),分期仍未解析成区间
服务端计算恢复分、睡眠分、负荷及三者的分项、稳定度、覆盖率GET /me/days,读时现算已接,2026-09-28
本地与用户输入昵称、睡眠目标、提醒开关、佩戴位置、打卡、测量备注LocalStore meta 表已接;睡眠目标/提醒/安静时段已同步到服务端

两条不能违反的规矩。① 分数一律由服务端算,App 不自己算、也不缓存成静态值——服务端是读时现算的,内部多取 29 天历史做个人基线。② 距离与热量是按步数折算的(步长 0.72 m、每步 0.04 kcal),不是单独测的,标注上必须说明。

01状态词汇

同一个「没有数」,原因不同,说法就得不同。这套词汇全 App 统一。

缺失的四种原因

原因判据文案
还没登录无 refresh token评分由服务端生成,登录后同步
还没同步到已登录但本地无该日 server 行评分由服务端生成,同步后显示
覆盖不足scores.status = insufficient_coverage这一夜心率覆盖不足六成,算不出恢复分
没检测到睡眠scores.status = no_sleep_detected这一夜没有检测到睡眠

稳定度四态

服务端返回 scores.stability.level,App 只按它换文案与颜色深浅,不改数值。接入清单明写:首页三卡按它显示。已实现(2026-09-28)。

level夜数卡片上怎么说
initial1–2初步 · 主要看睡眠
building3–6校准中 · 基线还在积累
settling7–13接近稳定
stable≥ 14(不加标签,直接给数)

冷启动的加权由服务端处理:心率与 HRV 两项乘以 w = n/(n+3),缩掉的份额由当夜睡眠分补上。第 1 晚 w=0.25,第 14 晚 0.82。所以第一晚就有数,没有「空白→突然出数」的跳变,App 不需要自己做冷启动逻辑。

其他状态

  • partial_day:当天心率覆盖不足六成(首日佩戴、白天没戴)。load_7d 不计这类天。
  • status ∈ provisional | final | revised:稳定度 stable 且非 partial_day 才是 final。修订过的日报要能看出来。
  • 来源优先级:手环 > 服务端 > Apple 健康 > 模拟。同一指标多来源不求和,按优先级取一个。
  • 历史时区未知时保留未知,不把手机时区套进旧数据。

02首页头部

tab 以上的一整块,四个栏目共用。

报头

  • 字标 · 通知铃铛(有未读时带点)· 头像(昵称首字,未设时「T」)

状态行

内容字段来源现状
连接状态点 + 「已连接 / 未连接」DeviceConnectionStateSDK已做
电量readBatteryPercent()SDK已做,未连接时不显示
最后同步时间last_sync_at服务端批次回执未做(接入清单要求顶栏显示)
同步按钮(小)—动作已做,同步中转圈

Hero 左栏

内容怎么来的现状
问候语(按钟点)本地时间 + 昵称已做
状态标题「状态正好 / 慢一点 / 和平时一样」主指标对个人基线的偏离已做;点击打开依据
下一行:今天偏离常态最远的那一项候选项按 |deviation| 排序,≥0.35 才出已做;每天都会变,点进该指标
「测一次」按钮—已做,开半高抽屉

Hero 右栏 · 设备场景

  • 佩戴位置手腕 / 上臂:用户自选的设置,不是识别。存本地,服务端无对应字段。原型上的「已识别 · 贴合良好」需要佩戴检测,SDK 没有这个能力。
  • 手环佩戴图:静态素材。
  • 心电纸网格 + 走动的迹线:心搏间隔由实测心率算(60/bpm),心率高则走得密。波形不是录下来的心电——这条链路没有连续 ECG 流。
  • 静息心率读数:resting_heart_rate。服务端定义是睡眠窗内心率 5 秒点的 25 分位;现在用的是手环日汇总,接拉取后换成服务端值。

三张指标卡

恢复 / 睡眠 / 负荷。三项都是服务端评分,未登录时全空——那时按优先级换成手环确实读到的指标顶上(心率、变异性、步数、血氧)。每张卡五层:

层恢复睡眠负荷
图标 + 标签恢复睡眠负荷
大数字 + 单位86 分7:423.8
副标签恢复程度小时 · 昨夜今日活动
小图形20 根细柱成拱,按分数填到某一根分期阶梯线,四段宽度来自实际时长7 根粗柱,最后一根最实
状态行高于/低于你的常态睡眠分 92目标 8–12

缺的:状态行应该按 stability.level 显示「初步 / 校准中 / 覆盖不足」,现在只有「有数」和「—」两态。

03洞察

回答「今天怎么样,凭什么这么说」。

日期切换

前一天 / 后一天 / 日历 / 回到今天。范围 90 天。

话题与趋势

  • 四个话题 chips:恢复 / 睡眠 / 心率 / 步数。
  • 选中项的 14 天趋势图 + 区间(最小–最大)+ 今日值。缺数的天不补点。
  • 结论段落:复述读数与口径,不加判断。例如恢复度那段会说明它由睡眠、静息心率与 HRV 共同得出,且和「今天该不该运动」不是一回事。

亮点(Apple 健康 Highlights 的做法)

自动挑出这周确实变了的最多三项,每条是「本周 X 平均 A,上周是 B」+ 14 天柱图 + 变化百分比。两周各至少 3 天有数、相对变化 ≥ 4% 才入选;够不上就不摆。

身体信号四格

静息心率 / 心率变异性 / 血氧 / 皮温。每格:名称、大数字、个人区间条、区间说明。

区间条用的是这个人自己 14 天的最小最大值,不是人群参考范围。我们没有人群数据,画一条「正常范围」是编的。服务端的基线也是同一个原则:先验只是冷启动的起点,14 晚后完全按个人基线。

皮温要改:服务端给的是 skin_temperature_deviation_c(相对近 7 天中位数的偏差),现在当绝对值在显示,语义不对。

今天的节奏

手环这一天确实记下来的几件事,时间取自数据本身,不是编的时刻表:醒来(睡眠起止 + 时长对目标)、活动(步数 + 折算距离)、静息心率(日间 + 夜间)。

其余

  • 入睡提醒入口(本机提醒,不上传)
  • 全部指标入口(弹层)
  • 「这些结论对你有用吗」——有用 / 没用,目前只存本地
  • 免责声明

04长寿

几周以上的变化。单天的起伏在这里被抹平。

板块内容来源现状
28 天覆盖睡眠 / 恢复 / 步数 / 变异性,各「28 天中有 N 天有记录」本地库数行数已做
最近两周逐日柱图 + 区间本地已做
按周趋势恢复度按周均值,4 周 / 12 周切换本地聚合已做;接拉取后应改用服务端的日报序列
按周看睡眠 / 心率 / 变异性 / 步数,本周均值与上周比本地聚合已做
打卡午后步行、23:00 前入睡本地已做
周回顾——未做

打卡应该走 POST /me/events {kind:"habit_checkin", ts, attrs:{habit}},现在只存本地。event.kind 还支持 reminder / note / medication / supplement / illness / travel——生活记录那一大块的落点就是这个接口。

05睡眠

板块字段现状
昨夜时长 + 起止sleep.total_minutes、sleep.start/end已做(本地推算,应换服务端值)
对目标的差额+ 偏好 sleep_target_h已做
分期条 + 四段时长sleep.stages[] {stage, minutes},deep/light/rem/awake已做(本地推算)
睡眠分四分项scores.sleep.{duration 50, continuity 20, depth 20, regularity 10}已做——四项各带占比条
两周统计有记录的夜 / 平均时长 / 睡眠债已做,睡眠债是真算的
多段睡眠、午睡、跨午夜整理—未做——睡眠流只存了原文,未解析成区间
入睡提醒本地已做

之前这里写错过一句话。我一度在界面上说「睡眠分的构成拿不到,所以不拆开给你看假的权重」。服务端是给拆解的——四个分项都在 soma.daily/v1 里。真实情况是我们还没接 GET /me/days。已改正。

06Lab

重点是 agent 接入,不是原始记录的计数器。回答「别的程序怎么用到这些数据」。

  • 能调的工具:measure / daily / series / raw / sync。可用与否问设备层要,不是写死的清单——心电在这只固件上调不通就标「不可用」。
  • 调用示例:代码块,说明返回结构与上传契约同一套(soma.sample/v1、soma.measurement/v1),字段名一个字不差。
  • 本地存了什么:原始记录数、等待上传数、有记录的天数、手环与固件。
  • 导出:CSV(带时间戳与来源)+ 系统分享。

未做:PPG / PPG+ACC 原始采集与会话管理、信号预览、缺包计数、采集模式与 SDK 版本标注、原始数据与日志导出。原始采集与历史同步不能并行(SDK 会 busy),这个互斥要在界面上表达。

07弹层

全部指标

11 项按主题排列(恢复/变异性 · 睡眠/夜间心率/皮温 · 心率/血氧 · 步数/距离/热量/负荷),每行:圆底图标、名称、口径说明、7 天迷你折线、当日值。带搜索。

指标详情

  • 时间范围 7 / 30 / 90 天
  • 当日值 + 口径
  • 柱状趋势图 + 「N 天中有 M 天的记录」
  • 这段时间的平均 / 最低 / 最高

未做:7/14/28 天对比(需求里明确要三档)、数据来源与采集/同步时间标注、覆盖率、过期与部分同步状态。

测量

半高抽屉列出四项,按固件能力灰掉不支持的并写明原因。选一项进具体流程。

类型要展示现状
心率佩戴说明 → 开始 → 进度 → 读数 → 保存/备注/重测/历史比较调真 SDK;结果与失败原话都显示
血氧同上,且要显示同批返回的心率已接 supporting 字段
HRV历史与同来源趋势;手动测量是实验入口,不擅自用 RMSSD 或 ms 单位部分
ECG接触说明、真实原始波形、停止、数据与日志保存;不提供医疗判读受阻,见第 09 节

未做:类型/日期筛选、前后比较、重复点击保护、设备占用提示、有效与失败记录分开、取消/停止二次确认、按测量类型验证信号质量。不能以倒计时结束冒充有效结果——现在走的是真 SDK 返回,这条满足。

设备

内容现状
连接状态、电量、固件版本、可读数据流已做,全部从连上的手环读
同步状态、手动同步已做
账号绑定(claim)、上传回执已做
搜索/识别/解绑/断连重连/失败重试部分——扫描与连接有,解绑与重试策略没有
设备时间、自动测量配置、已有闹钟读取未做
校时、设置写入、设备闹钟、自动测量间隔、固件升级、恢复出厂待 SDK 支持

我与设置 · 登录

  • 昵称、睡眠目标、每日结论提醒、夜间安静时段、账号登录/退出、数据去向说明、版本与免责
  • 登录:手机号 + 验证码,两份同意(service / health_data)带 text_version

未做:PATCH /me {birth_year, sex}——负荷的心率区间要年龄(HRmax = 208 − 0.7×年龄),不填则退化成步频档;也是拒绝 14 岁以下注册的依据。PATCH /me {preferences}——睡眠目标、Insight 推送、安静时段服务端都有字段,现在只存本地。身高体重、体重历史、健康目标、数据处理授权与撤回、导出与删除,全部未做。

08需求对照

团队 MVP 需求九大项,各自落在哪一页、做到哪一步。

需求项落点状态
1 设备管理设备弹层 · Lab连接/能力/同步/绑定已做;解绑、重试策略、设备时间与闹钟未做;校时与写入待 SDK
2 生理测量测量弹层心率/血氧走真 SDK;HRV 部分;ECG 受阻;筛选、比较、占用保护未做
3 数据监控洞察 · 长寿 · 指标详情七项指标与趋势已做;睡眠分期未解析;7/14/28 三档、覆盖率与来源标注未做
4 Insight洞察每日摘要、信号变化、亮点周对比、有用/没用已做;周回顾、减少同类、去重、推送偏好未做
5 活动计划与生活记录—整块未做。落点是 POST /me/events
6 提醒与触觉睡眠 · 设置入睡提醒本地已做;其余提醒未做;SDK 振动未做
7 数据管理与 LabLab本地存量与 CSV 导出已做;原始采集会话、信号预览、缺包计数未做
8 用户管理设置 · 登录昵称与偏好本地已做;档案、体重历史、授权撤回、导出删除未做
9 系统设置设置外观与减少动效跟随系统;语言、单位、时间格式、权限、缓存、日志、帮助未做

需求里两条原则已经落到界面上:无聊天框、无教练对话、无对话记忆(偏好只通过明确设置维护);演示数值必须属于 mock(启动参数 dev-data 才用模拟数据,不带参数时读不到的显示「—」,并在依据页标明当前是否演示数据)。

10完整数据清单

把三个来源能给的每一项都摆出来,逐条对一遍它在界面上的落点。重点是「展示位置」那一列——写「未展示」的,就是拿得到却没用上的。共 144 项,其中 94 项未展示。

服务端 · 日报 soma.daily/v1(GET /me/days)

字段含义 / 单位来源展示位置状态
local_date本地日 yyyy-MM-dd服务端未展示(已落库,日期切换仍用本机日历)已拉取
tz该日时区服务端未展示(已落库)已拉取
statusprovisional | final | revised服务端未展示(已落库,provisional/final 尚未区分)已拉取
sleep.start入睡时刻,UTC服务端未展示(已落库,昨夜起止仍为本地推算)已拉取
sleep.end起床时刻,UTC服务端未展示(已落库,同上)已拉取
sleep.total_minutes总睡眠分钟服务端睡眠 · 昨夜大数字已做
sleep.stages[]分期区间数组服务端未展示(睡眠 · 分期条现按固定比例推算)未做
sleep.source固定 device服务端未展示未做
resting_heart_rate静息心率 次/分,睡眠窗内心率 5 秒点的 25 分位服务端洞察 · 身体信号 / hero 读数已做
steps当日步数服务端全部指标 / 今天的节奏已做
active_minutes活动分钟服务端未展示(已落库)已拉取
skin_temperature_deviation_c皮温相对近 7 天中位数的偏差 °C服务端洞察 · 身体信号四格(已按「偏差」呈现)已做
last_spo2当日最近一次血氧服务端洞察 · 身体信号四格已做
last_hrv当日最近一次 HRV服务端洞察 · 身体信号四格已做
coverage.heart_rate_minutes当日心率覆盖分钟服务端未展示(已落库)已拉取
coverage.expected_minutes当日应覆盖分钟服务端未展示(已落库)已拉取
recovery恢复分 0–100服务端首页头部 · 三张卡已做
load负荷,无量纲服务端首页头部 · 三张卡已做
load_7d负荷 7 天均值服务端未展示(已落库)已拉取
sleep_score睡眠分 0–100服务端首页头部 · 三张卡 / 睡眠 · 睡眠分已做
partial_day当日心率覆盖不足六成服务端未展示(已落库)已拉取
scores.algo_version算法版本,当前 daily-v0服务端未展示(已落库)已拉取
scores.statusok | insufficient_coverage | no_sleep_detected服务端三张卡状态行 / 睡眠 · 睡眠分说明已做
scores.stability.nights已入基线的夜数服务端首页头部 · 三张卡状态行(第 N 晚)已做
scores.stability.levelinitial | building | settling | stable服务端首页头部 · 三张卡状态行(初步/校准中/接近稳定)已做
scores.sleep.duration睡眠分 · 时长项,满分 50服务端睡眠 · 睡眠分四分项已做
scores.sleep.continuity睡眠分 · 连续性项,满分 20服务端睡眠 · 睡眠分四分项已做
scores.sleep.depth睡眠分 · 深度项,满分 20服务端睡眠 · 睡眠分四分项已做
scores.sleep.regularity睡眠分 · 规律项,满分 10;不足 7 夜为 null服务端睡眠 · 睡眠分四分项已做
scores.recovery.rhr恢复分 · 静息心率项,满分 40服务端未展示(已落库,恢复分依据页未做)已拉取
scores.recovery.rhr_z静息心率对个人收缩基线的 z 分服务端未展示(已落库,同上)已拉取
scores.recovery.hrv恢复分 · HRV 项,满分 30服务端未展示(已落库,同上)已拉取
scores.recovery.hrv_zHRV 的 z 分服务端未展示(已落库,同上)已拉取
scores.recovery.sleep恢复分 · 睡眠项,满分 30服务端未展示(已落库,同上)已拉取
scores.recovery.temp_penalty皮温升高的扣分服务端未展示(已落库,同上)已拉取
scores.recovery.baseline_weight冷启动权重 w = n/(n+3)服务端未展示(已落库,同上)已拉取
scores.flags该日标志数组服务端未展示(已落库)已拉取
contributing_devices贡献该日数据的设备 ULID 列表服务端未展示(已落库,用作 device_id)已拉取

38 个字段,一个都没读——GET /me/days 还没接。括号里写的是同一个位置现在显示的东西:要么是手环日汇总,要么是本地按固定比例推算的。

服务端 · 其余读取接口

字段含义 / 单位来源展示位置状态
/me · preferences.sleep_target_h睡眠目标,小时服务端未展示(弹层 · 我与设置读的是本地值)未做
/me · preferences.insight_notifications每日结论提醒开关服务端未展示(同上)未做
/me · preferences.quiet_hours夜间安静时段开关服务端未展示(同上)未做
/me · birth_year出生年;负荷的心率区间要它服务端未展示(没有任何填写入口)未做
/me · sex性别;基线先验与 14 岁下限的依据服务端未展示(同上)未做
/me · last_sync_at最后同步时间服务端未展示(首页头部 · 状态行只有本地的「刚刚同步」字样)未做
sample.metricheart_rate | spo2 | steps | skin_temperature | hrv服务端未展示(只上传,不回读)未做
sample.value / sample.unit点值与单位服务端未展示(同上)未做
sample.ts采样时刻,UTC服务端未展示(同上)未做
sample.originrealtime | measurement | history服务端未展示(弹层 · 指标详情要求标来源,没标)未做
sample.quality[]low_perfusion | motion_artifact | loose_fit | reconstructed | clock_corrected | imported服务端未展示未做
sample.device_id服务端签发的设备 ULID服务端未展示未做
sample.record_id{kind}:{deviceDate}[:i]服务端未展示未做
series.step_ms序列点间隔 ms;心率 5000、步数 60000服务端未展示未做
series.values[]序列值,缺失位置填 null 不删元素服务端未展示(所有趋势图按天只取一个值)未做
interval.kindsleep_stage | activity | wear | workout服务端未展示未做
interval.start_ts / end_ts区间起止,UTC服务端未展示未做
interval.attrs.stagedeep | light | rem | awake | unknown服务端未展示(睡眠 · 分期条现按固定比例推算)未做
interval.attrs.code设备原始分期码服务端未展示未做
measurement.metricheart_rate | spo2 | hrv服务端未展示(只上传,不回读)未做
measurement.statusok | failed | cancelled服务端未展示(弹层 · 测量只显示这一次的结果)未做
measurement.value.value / .unit测量读数与单位服务端未展示(同上)未做
measurement.samples_seen这次收到的样本数服务端未展示未做
measurement.duration_s测量时长,秒服务端未展示未做
measurement.note用户备注,≤ 500 字服务端未展示(弹层 · 测量能填,只存本地)未做
event.kindhabit_checkin | reminder | note | medication | supplement | illness | travel服务端未展示未做
event.ts事件时刻服务端未展示未做
event.attrs事件属性,如 {habit}服务端未展示(长寿 · 打卡只存本地)未做
device.model_code型号 somaloop-2服务端未展示未做
device.firmware_versionfirmwareHex-dateHex服务端未展示(设备弹层的固件版本直接读手环)未做
device.capability_stateverifiedReference … unsupported服务端未展示(只在上传护照时报上去)未做
device.battery_pct电量 %服务端未展示(首页头部 · 状态行直接读手环)未做
device.sdk_versionSDK 版本服务端未展示未做
device.sdk_build_revisionSDK 构建号服务端未展示未做
device.hardware_identitysha256(小写 mac 去冒号)服务端未展示未做

四个读取接口(/me、/me/timeline、/me/measurements、/me/events、/devices/{id})一个都没调。样本与测量这两套字段 App 天天在产(上传走的就是这个契约),但从来没读回来过。

手环 SDK · 能力表里的每一项

字段含义 / 单位来源展示位置状态
history.dailyActivity按天汇总的步数 / 距离 / 热量 / 活动时长手环 SDK洞察 · 今天的节奏「活动」行、步数话题;长寿 · 28 天覆盖、按周看已做(只取了步数)
history.stepDetail分钟级步数明细手环 SDK未展示未做(不在同步清单)
history.sleep睡眠分期码手环 SDK未展示未做(不在同步清单)
history.heartRate连续心率序列手环 SDK首页头部 · 设备场景读数;洞察 · 身体信号四格、心率话题、今天的节奏;长寿 · 按周看;弹层 · 依据、指标详情已做(按天取中位数)
history.singleHeartRate单次心率条目手环 SDK未展示未做(不在同步清单)
history.hrvHRV / 疲劳 / 压力估计手环 SDK洞察 · 身体信号四格;长寿 · 28 天覆盖、按周看;弹层 · 依据、指标详情已做
history.temperature皮肤温度手环 SDK洞察 · 身体信号四格、亮点;弹层 · 指标详情已做(当绝对值显示,语义不对)
history.bloodOxygen血氧手环 SDK洞察 · 身体信号四格;弹层 · 指标详情已做
history.pulseInterval脉搏间期 PPI手环 SDK未展示未做(呼吸率要从它推)
history.workout运动记录手环 SDK未展示未做
history.alarms闹钟列表手环 SDK未展示未做(契约不收,不上传)
history.sleepActivity睡眠 + 活动量合并记录手环 SDK未展示未做
history.sleepDiagnostics睡眠诊断手环 SDK未展示未做(诊断流不进用户界面)
history.systemDiagnostics系统诊断手环 SDK未展示未做(同上)
history.powerDiagnostics电源诊断手环 SDK未展示未做(同上)
measurement.heartRate主动测心率 次/分手环 SDK弹层 · 测量已做
measurement.bloodOxygen主动测血氧 %手环 SDK弹层 · 测量(含同批返回的 supporting)已做
measurement.hrv主动测 HRV ms手环 SDK弹层 · 测量部分
measurement.ecg心电手环 SDK弹层 · 测量(入口在,灰掉并写明原因)受阻,见第 09 节
capability.identityMAC / 固件 / 设备日期手环 SDK设备弹层 · 固件版本;Lab · 手环与固件已做
capability.battery电量 %手环 SDK首页头部 · 状态行;设备弹层已做
DeviceCapabilityMap每项能力的五档支持度手环 SDK设备弹层 · 可读数据(前 3 项 +「等 N 项」);弹层 · 测量的灰态与原因;Lab · 能调的工具已做
readSetting(...)读 clock / personalProfile / stepGoal / autoMeasurement / reminders / alarms / basic 七组手环 SDK未展示未做
writeSetting(...)写同样七组手环 SDK未展示待 SDK 支持
rawCapture.ppgOnly只采 PPG手环 SDK未展示未做
rawCapture.ppgWithMotionPPG + 加速度联合手环 SDK未展示未做
signalStream.ecg心电原始波形(0x07 包)手环 SDK未展示受阻,见第 09 节
signalStream.motion加速度原始流(0x33 包)手环 SDK未展示未做
signalStream.diagnosticsASCII 诊断流手环 SDK未展示未做
longCapture24 小时连续采集手环 SDK未展示未做
haptics触觉节拍手环 SDK未展示未做(需求第 6 项)

同步默认只拉五条流:dailyActivity、heartRate、bloodOxygen、hrv、temperature。睡眠流不在里面——不是「存了原文没解析」,是根本没读。

App 现有的十一个指标(Domain)

字段含义 / 单位来源展示位置状态
Domain.recovery恢复度 · 分服务端首页 · 三张卡(第一张);洞察 · 恢复话题、亮点;长寿 · 28 天覆盖、最近两周、恢复度按周;弹层 · 指标详情受阻(真机下全是「—」)
Domain.hrv心率变异性 · ms手环 SDK洞察 · 身体信号四格、亮点;长寿 · 28 天覆盖、按周看;弹层 · 依据、指标详情已做
Domain.sleep睡眠 · 时长服务端首页 · 三张卡(第二张);睡眠 · 昨夜、分期条、这两周;洞察 · 睡眠话题、今天的节奏、亮点;长寿 · 28 天覆盖、按周看;弹层 · 依据、指标详情受阻(睡眠流不在同步清单)
DailySnapshot.sleepScore睡眠分 · 分服务端睡眠 · 睡眠分面板;首页 · 睡眠卡状态行受阻(无产出方,真机下为「—」)
Domain.nightHeart夜间心率 · 次/分服务端洞察 · 今天的节奏(静息心率行的副文案,全 App 唯一一处)受阻(无产出方)
Domain.temperature皮肤温度 · °C手环 SDK洞察 · 身体信号四格、亮点;弹层 · 指标详情已做(应改成相对基线的偏差)
Domain.heart静息心率 · 次/分手环 SDK首页头部 · 设备场景读数;洞察 · 身体信号四格、心率话题、今天的节奏、亮点;长寿 · 按周看;弹层 · 依据、指标详情已做
Domain.oxygen血氧 · %手环 SDK洞察 · 身体信号四格、亮点;弹层 · 指标详情已做
Domain.steps步数 · 步手环 SDK首页 · 三张卡(分数为空时顶上);洞察 · 步数话题、今天的节奏、亮点;长寿 · 28 天覆盖、按周看;弹层 · 指标详情已做
Domain.distance距离 · 公里,按步数 × 0.72 m 折算本地折算洞察 · 今天的节奏「活动」行已做(没有独立卡片,也没有详情入口)
Domain.calories活动热量 · 千卡,按每步 0.04 kcal 折算本地折算洞察 · 步数话题的结论段(一句话)已做(同上)
Domain.load身体负荷 · 无量纲服务端首页 · 三张卡(第三张);洞察 · 亮点;弹层 · 指标详情受阻(真机下为「—」)

真实数据下本地库只写得出五项:心率、血氧、HRV、皮温、步数(按天取中位数,步数取当日累计)。恢复度、睡眠时长、睡眠分、夜间心率、负荷没有任何产出方——手环没读,服务端没接,界面上一律是「—」。带 dev-data 启动时才由模拟值顶上。

本地与用户输入 · 运行时状态

字段含义 / 单位来源展示位置状态
store.name昵称本地首页头部 · 头像首字、问候语;弹层 · 我与设置已做
store.sleepTarget睡眠目标 · 小时,6–10 步长 0.5本地睡眠 · 昨夜差额、这两周平均与睡眠债;洞察 · 睡眠话题;弹层 · 我与设置已做(服务端有 preferences.sleep_target_h,未同步)
store.reminders每日结论提醒开关本地弹层 · 我与设置已做(未同步)
store.quietHours夜间安静时段开关本地弹层 · 我与设置已做(未同步)
store.wearPosition佩戴位置 手腕 / 上臂本地首页头部 · 设备场景上方的切换已做(服务端无对应字段)
store.habits打卡集合本地长寿 · 今天的打卡已做(应走 POST /me/events)
store.insightHelpful有用 / 没用本地洞察页尾、长寿页尾已做(只存本地)
store.records手动测量记录:类型 / 读数 / 备注本地未展示受阻(唯一列出它的「最近测量」在已下线的身体页里)
ReminderView · time入睡提醒时刻本地弹层 · 睡前提醒部分(只活在这一屏的状态里,保存后只往打卡集合插一条字样)
store.connected连接状态手环 SDK首页头部 · 状态行;设备弹层已做
store.battery电量 %手环 SDK首页头部 · 状态行;设备弹层已做
store.syncing / store.syncState同步中 / 同步结果原话本地首页头部 · 状态行同步钮;设备弹层 · 同步状态已做
store.signedIn登录状态服务端弹层 · 我与设置、设备已做
store.deviceBound手环是否绑到账号服务端设备弹层 · 账号已做
store.uploadState上传回执服务端设备弹层已做
identity.firmwareDisplay固件版本手环 SDK设备弹层 · 固件版本;Lab · 手环与固件已做
devices.historyStreams()这只手环能读哪几条流手环 SDK设备弹层 · 可读数据已做
devices.availableMeasurements可测项数手环 SDKLab · 能调的工具(measure 行)已做
rawRecordCount()本地原始记录条数本地Lab · 本地存了什么已做
pendingUploadCount()等待上传条数本地Lab · 本地存了什么已做
daysWithSamples().count有记录的天数本地Lab · 本地存了什么已做
AgentTool.allmeasure / daily / series / raw / sync 五个工具及可用性本地 + SDKLab · 能调的工具已做
CSV 导出date,metric,value,90 天全部指标本地Lab · 导出 CSV + 系统分享已做
store.selectedDate选中日期,范围 90 天本地洞察 / 长寿 / 睡眠 · 日期切换条已做
availableSeries(_:days:)本地日序列,缺的天不补点本地所有趋势图、区间条、覆盖计数的数据源已做
ConsentText.version同意文本版本号本地弹层 · 登录页脚已做
HealthStore.usesRealData这次运行是不是演示数据本地弹层 · 依据页末句已做
Repository · 模拟播种90 天模拟日报本地未展示(只在带 dev-data 时顶替空值)已做

三个最值得先补的缺口。① 服务端日报 38 个字段一个没读,其中四个睡眠分项与七个恢复分项是现成的、算好的、专门给「依据」页用的,界面上却只摆了一句口径说明。② 睡眠流压根不在同步清单里(synchronise() 默认只拉五条),所以睡眠时长、分期、睡眠分、夜间心率、负荷、恢复度在真机上全是「—」;睡眠页的分期条只有带 dev-data 启动才有内容。③ 洞察页的「全部指标」入口按下去打开的是入睡提醒——all-metrics 这个弹层 id 在路由里没有分支,落到了默认分支;连带 MetricListRow 与「最近测量」列表当前没有任何调用点,距离、活动热量、夜间心率因此没有详情入口。

09拿不到的东西

不是没做,是当前拿不到。每条写清卡在哪。

想要的卡在哪解法
ECG 真实波形设备支持,V8 源码里也有(0x28 子码 04),但 L1 SDK 把能力表编进了二进制,调用被拦V8 直连包(见下节)或厂商放开
主动测量(心率/血氧/HRV)同上。dev-console 用同样的字节在同样的硬件上跑通过V8 直连包
手环震动认人两边 SDK 都没有实现。L1 的 hapticRhythm 绑死单一参考 MAC(f6c6e1eed41b / 固件 00000709-260519),我们的手环是 f8ed4c8d1816 / 00000808-260604,两项都不匹配配对页里的震动按钮已移除——按下去只会报错。认人改用信号强弱(把想绑的那只贴近手机重新搜,列表按 RSSI 排序)。将来 V8 直连包可以试发 97 36
睡眠区间、多段睡眠、午睡睡眠流已加入同步清单,但只存了原文,未解析成区间解析 sleep 流,走 soma.interval/v1
佩戴检测(「已识别 · 贴合良好」)SDK 无此能力已降级成用户自选的设置
连续 ECG 流 / 实时心率手环按间隔采样,无实时流读数标「静息心率」,不标「此刻」
呼吸率无此字段可从 PPI 的呼吸性窦性心律不齐推算,未做
校时、设置写入、设备闹钟、固件升级SDK 未开放待厂商
人群参考范围没有人群数据一律用个人基线;服务端的冷启动先验也只是起点,14 晚后完全按个人基线

11V8 直连包

L1 把一批能力关在闸后面,而 dev-console 用同样的字节在同样的硬件上跑通过——闸在 SDK 里,不在设备里。这个包按厂商源码自己拼包直连 BLE 发,等 L1 放开后整体并回去。

已经建好的:纯字节层

SomaLoop/DeviceV8/V8Protocol.swift,不碰蓝牙,9 个单测全部对着厂商已知样本断言。

项值出处
帧长16 字节,不分包三条已知指令反推
校验末字节 = 前 15 字节之和 & 0xFF三条样本全部吻合
服务 / 写 / 通知FFF0 / FFF6 / FFF7V8 iOS demo V8SdkAdapter.m
测量0x28byte1 类型、byte2 开关、byte4–5 秒数小端、心电另置 byte6
触觉97 36 01 / 97 36 00L1 protocol/haptics.json 原文

一处两份厂商 SDK 互相矛盾:HRV 的子码 Android AutoTestMode 是 0x01,iOS MeasurementDataType_V8 写的是 4(和心电撞号)。按 Android 那份走,因为 dev-console 跑通的是那一套。真机验到哪种就改哪种。

两处要更正的认知

V8 原生 SDK 里没有震动实现。iOS 头文件 37 个方法没有一个是震动;Android 的 BleSDK.java 也没有对应构造器。MotorVibration_V8 = 14 与 SetMotorVibrationWithTimes = "14" 都只是声明了但没实现的命令类型,那个 14 是 API 枚举序号不是 opcode——Android 那份里 0x14 实际是 Temperature_3NTC。所以「V8 有 L1 没有的震动能力」这个前提不成立;唯一已知的震动机制是 L1 那条 97 36(靠主机反复开关提醒状态凑节奏,且实测时序验收失败过)。

还没写的

  • BLE 传输层(扫描 / 连接 / 写 FFF6 / 订阅 FFF7 / 分包重组)
  • 回包解析:测量结果、心电波形
  • 与 DeviceLink 的接线:L1 拒绝时自动退到直连
  • 震动:字节已备好,但要先在实机上验证设备收不收——验通了再往界面上放按钮

12变更记录

日期变更
09-28接上 GET /me/days。38 个日报字段落库(daily_report 表存原文,daily 表存给界面的数,source=server)。sync() 里先上传再拉取——反过来服务端还没收到今天的数据。每次整段重拉 92 天不做增量,因为服务端是读时现算的。
09-28三个分数为 null 时不写库。null 是「算不出来」,写 0 会被读成「恢复度为零」。有单测钉着。
09-28三张卡接入稳定度四态;睡眠分四个分项列出来了;顶栏显示 last_sync_at;皮温改成「皮温偏差」。
09-28补上三条上报:测量(成功失败都报)、打卡(POST /me/events)、偏好(PATCH /me,连按合并 600ms)。
09-28修:JSONEncoder 默认省略 nil 而非输出 null,而 value 在契约的 required 里——测量失败会吃 400 invalid_schema。改成手写 encode(to:)。
09-28入门硬闸:不登录 + 不绑手环看不到任何数值。五步(欢迎 → 账号 → 出生年/性别 → 配对 → 首次同步),断点续走。配对前检查蓝牙状态并区分「关着 / 没权限 / 不支持」。
09-28修两个死路:.sleep 不在同步清单里(睡眠数据压根没读);all-metrics 弹层没有分支,点开的是「睡前提醒」。
09-28配对页移除「让这只震一下」按钮与设备层的 identify 接线——两边 SDK 都没有实现,留着是死代码加一颗必然报错的按钮。认人改用信号强弱引导。
09-27Basic 界面按 Wise 重做;栏目收成 洞察/长寿/睡眠/Lab;Lab 重点改成 agent 接入。
第三部分

SomaLoop L1 SDK 接入问题汇总

iOS App 接 L1 SDK 期间攒下来的问题,按「挡住了什么」排序。每一条都标了证据出处——SDK 自己的文档、protocol.json、两份原厂 V8 SDK 源码,或我们的真机记录。写这份不是为了记账,是因为其中几条现在正挡着 App 的功能,需要 SDK 方定夺。

2026-09-28SDK 自报 0.1.4-beta参考固件 00000709-260519手上设备 00000808-260604

S1一页看完

九条。前三条正在挡功能,中间三条是一致性问题,后三条是语义空白。

  1. 测量拿不到。手上这台固件 00000808 的设备,measure() 稳定返回 unverifiedCapability。而 develop-console 用同样的字节在同样的硬件上跑通过——闸在 SDK 里,不在设备里。
  2. 触觉绑死在一个 MAC 上。protocol.json 原文:haptics requires reference MAC,那个 MAC 是 f6c6e1eed41b。别的设备一律发不出。
  3. 而且触觉在 V8 层面根本没有实现。两份原厂 SDK(iOS 头文件、Android BleSDK.java)都没有任何发送振动的函数。
  4. SDK 自报版本是 0.1.4-beta,不是 0.1.4。仓库 tag、VERSION 文件、xcframework 的 plist 全都写 0.1.4,只有运行时常量带 -beta。
  5. 验收记录是另一个构建产出的。validation-status.json 里 buildRevision 是 c88d990b…、sdkVersion 是 0.1.0-beta;而交付的二进制自报 934adc2f… / 0.1.4-beta。两者对不上,且那份记录 "finished": false。
  6. 两份原厂 SDK 在 HRV 子码上互相矛盾。Android 是 HRV=0x01,iOS 头文件是 HRV=4,而 4 在 Android 那份里是心电。
  7. 手动测量有个没写进 SDK 文档的前置条件。原厂 iOS 文档:「只有在实时计步处于开启状态下,手动测量才会有数据返回」。
  8. 没有发裸帧的公开入口。ProtocolCodec.command 能拼字节,却没有对应的发送 API;而文档又明确禁止绕过去直接写 transport。
  9. 好几个字段的单位没有声明。运动记录的时长、心电/PPG/ACC 的采样率与标定,都只能原值保留——任何换算都是编的。

S2能力闸挡住了测量

现象

手上这台设备(JCV8B,固件 00000808-260604)调 measure(.heartRate) 稳定返回 unverifiedCapability。心率、血氧、HRV 三种都一样。

为什么

《能力与迁移.md》说明了判定规则:

相同参考设备及固件记 verifiedReference;另一台设备报告同样固件与日期只记 compatibleCandidate。……0088 以独立能力矩阵为准,不能继承全部能力。

也就是说,闸不是按「设备能不能做」开的,是按「厂商验没验过这一台」开的。这个设计本身没问题——unsupported 必须有证据,不因一次没响应就设置,这条规矩是对的。问题在于结果是一台功能正常的设备被判为不可用,而 App 侧没有任何办法推翻。

反证

develop-console 的 Python 实现,用同一批字节(0x28 子码 02/03/01)在同一台硬件上拿到过结果。原厂 V8 SDK 的源码里也没有任何固件门禁——grep firmware、grep capability 在 BleSDK.java 里一处都没有。所以设备是支持的。

我们的处理

写了一个 V8Protocol 纯字节层,按原厂 SDK 源码把包自己拼出来。没有绕过 SomaLoopClient 去直接写 AppleBLETransport——文档明确要求不要那样做,而且会和客户端自己的命令串行化打架。所以这条路目前是停在「字节拼好了,但没有合法的发送通道」。

S3触觉:绑死在一台设备上,而且没有实现

绑死在参考 MAC

protocol/protocol.json 的兼容性 profile 里写得很直白:

字段值
idsomaloop-fw-00000709-260519
referenceDeviceMACf6c6e1eed41b
scopeExisting 0079 behavior retained; haptics requires reference MAC.

所以触觉只在那一台特定的手环上能发。这对「用振动认出是哪一只手环」这个需求是致命的——恰恰是有多只手环时才需要它。

更根本的问题:V8 层面没有振动

我一开始以为 iOS 头文件里的 MotorVibration_V8 = 14 是个操作码,这是错的。它是回调数据类型枚举的一个成员(一个索引),不是可发送的命令;0x14 在协议里是 Temperature_3NTC。

核对两份原厂 SDK:

  • iOS BleSDK_Header_V8.h:全文搜 ibrat,只有 MotorVibration_V8 = 14 这一个枚举成员,没有任何发送函数。
  • Android BleSDK.java:搜 ibrat,零命中。

L1 的 97 36 01 / 97 36 00(内部提醒状态开关)是 L1 自己加的,V8 原厂没有这个能力。所以「不绑定就发振动」这条路走不通,我们已经把配对流程里的振动去掉了。

S4版本号与构建证据对不上

版本号差一个后缀

来源值
git tagv0.1.4
VERSION 文件0.1.4
xcframework 内层 CFBundleShortVersionString0.1.4
GitHub Release 标题SomaLoop L1 SDK 0.1.4
运行时 SomaLoop.version0.1.4-beta

这条本身不算严重,但它坑了我们一次:App 里三个地方把 sdkVersion 写死成 "0.1.4" 上报给服务端,一直是错的,而且不会有任何编译错误或运行时报错。现在改成读 SomaLoop.version,并把 buildRevision 一并上报。

验收记录来自另一个构建

validation-status.json交付的二进制
buildRevisionc88d990b…5046934adc2f…55e1
sdkVersion0.1.0-beta0.1.4-beta

并且那份记录自己写着 "finished": false、failedCases: ["run_not_finished"]。45 个用例里 36 passed、7 gate_verified、1 observed、1 empty_complete。unacceptedCapabilities 列了 6 项,其中三项正好是我们要用的:manual_hrv_numeric_result、continuous_ecg、public_settings_write_api。

这不是说 SDK 有问题,是说随包交付的验收证据对应不上随包交付的二进制,我们没法用它判断手上这份能做什么。

S5两份原厂 SDK 互相矛盾

HRV 子码

Android BleSDK.SetDeviceMeasurementWithType 是显式 switch(不是枚举序号):

测量Android BleSDK.javaiOS MeasurementDataType_V8
心率0x022
血氧0x033
HRV0x014
心电0x04(另置 byte6=1)没有这一项

HRV 对不上,而且 iOS 给 HRV 的那个 4 正好是 Android 给心电的值。我们按 Android 那份实现,因为 develop-console 跑通的是那一套;iOS 那份大概率只是没把心电列进去,顺手把号占了。这一点需要在真机上验一次,确认哪一份是对的。

时长的单位

iOS 原厂文档写:「measurementTime : 测量的时间,单位是秒,默认是 30 秒」,demo 里传 30。而 Android 的 ECG demo 传的是 (50 * 1000).toLong(),进的是同一个 byte4–5 字段。如果那也是秒,等于 13.9 小时。两边至少有一边是错的。

一个没写进 SDK 文档的前置条件

原厂 iOS 文档原文:「注意 : 只有在实时计步处于开启状态下,手动测量才会有数据返回」

也就是说要先发 RealTimeDataWithType:1 打开实时通道,0x28 才会吐数据。L1 SDK 的文档里没有提这条。如果 measure() 内部没有替调用方做这一步,那可能正是某些「发了命令但没有回包」的原因。

S6接口缺口

没有发裸帧的公开入口

ProtocolCodec.command 是公开的、能拼出合法字节,但 SomaLoopClient 没有对应的发送 API。同时文档又要求「不要绕过后直接调用底层 transport 写入」。结果是:知道该发什么、也能拼出来,但没有合法的路把它发出去。

这一条挡住的不只是我们的 workaround。心电流也卡在同一个地方,见下。

研究流没有 duration 参数

develop-console 的实现(advanced.py:298-314)说明心电要两步:

  1. write(frame(0x07, 1)) —— 开流
  2. request(frame(0x28, 4, 1, 0, duration&255, duration>>8, 1)) —— 启动测量,应答判据 len == 16 && b[1] == 4

而 iOS 的 SomaLoopResearch.start(_:) 只有一个 stream 参数,没有 duration,接口是不透明的:无法判断它内部是只发了第 1 步,还是两步都发了。

  • 如果两步都发了 —— 现在这样就是对的;
  • 如果只开了流 —— 设备不会吐波形,而适配器没有办法自己补上第 2 步(见上一条)。

这一点我们还没在真机上验证过。

settings 写入未开放

《能力与迁移.md》:「修改时间、个人资料、自动测量、提醒、闹钟等 | 公开 API 未开放 | 6 个具体字段已在内部完成写入、回读、恢复;尚未提升为公开 setter 契约,其余字段未验证;稳定返回 unverifiedCapability」。已知、可接受,记在这里是为了清单完整。

S7语义没声明的字段

这一类不算 bug,是「固件没说,所以谁也不能编」。记下来是因为它们直接决定界面上能写什么。

字段缺什么后果
运动记录 duration一个 u16,没声明单位不能在界面上写「跑了 32 分钟」
ECG / PPG / ACC没有采样率、没有标定系数只能保留原始计数,不能换算 mV、不能给逐点时间戳、不能下任何医学结论
睡眠 + 活动量(0x6B)每字节代表 2 分钟,但强度单位未定义只能当相对值用
HRV「09 不暴露独立 HRV 指标」手动 HRV 测量的数值待验收
闹钟 weekMask比较设备曾返回 255原值保留,不归一化
设备日历没有时区不能自动转 UTC——这一条 SDK 文档写明了,是对的

S8我们为此做了什么

固件证据表

厂商把一条能力标成 experimental,含义是「厂商没验证」,不是「设备不支持」。实测发现固件 00000808 上,被标成实验的血氧一次能完整读回 714 条、HRV 600 条——数据是真的,只是不在厂商的验收清单里。

但直接把全局实验开关打开等于对所有能力一律放行,那就退回到「猜」了。所以我们逐条记:哪个固件、哪条流、什么时候、在哪台设备上、读到了什么。没有证据的一律不放行。

一个适配器文件

全 App 只有 SomaLoopL1Link.swift 可以 import SomaLoopSDK,有一条扫描源码的边界测试钉着。厂商枚举不往外跑,所以上面这些问题将来怎么变,影响面都只有那一个文件。

V8 字节层

按原厂 SDK 源码把包拼出来,纯字节、不碰蓝牙、可单测。等 L1 放开之后整体并回去,对外接口不变。目前缺的是合法的发送通道(见 S6)。

S9给 SDK 方的清单

按「挡住了什么」排序,不按修起来容易排。

#请求挡住了什么
1让 compatibleCandidate 设备能在调用方显式承担风险的前提下执行 measure()(例如一个 allowUnverified 参数),而不是一律拒绝心率 / 血氧 / HRV 主动测量,以及依赖它的整个测量入口
2触觉解除参考 MAC 绑定;若 V8 硬件本就没有振动能力,请在文档里写明,我们好换方案多手环场景下认出是哪一只
3开放一个发送裸帧的公开入口(走客户端的串行化,不绕 transport)所有 workaround,以及心电第 2 步
4SomaLoopResearch.start(_:) 补 duration 参数;或在文档里说明内部是否已发 0x28 sub4心电波形
5确认手动测量是否需要先开实时计步;若需要,请在 measure() 内部处理或在文档写明可能是部分「无回包」的真因
6确认 HRV 子码到底是 0x01 还是 0x04HRV 测量
7重出一份与交付二进制对应的 validation-status.json(当前那份 buildRevision 与 sdkVersion 都对不上,且 finished: false)我们无法判断手上这份能做什么
8统一版本号:运行时常量 0.1.4-beta vs tag / VERSION / plist 的 0.1.4上报给服务端的设备护照
9声明运动记录 duration 的单位运动记录没法显示

需要我们自己先做的事:第 4、5、6 条都需要真机验证才能给 SDK 方确定的结论,不该直接当成缺陷提过去。我们这边的下一步是把这三条在设备上验一遍,带着实测结果再谈。