Skip to content

Add LittleMeatball R10M (YCBT) support#97

Open
saksham2001 wants to merge 2 commits into
mainfrom
claude/ycbt-r10m-port
Open

Add LittleMeatball R10M (YCBT) support#97
saksham2001 wants to merge 2 commits into
mainfrom
claude/ycbt-r10m-port

Conversation

@saksham2001

Copy link
Copy Markdown
Owner

Summary

iOS port of PulseLoopAndroid#31 — hardware-validated LittleMeatball R10M support (validated there on R10M FCF4, firmware 2.32).

The R10M speaks YCBT / SmartHealth, the same wire protocol iOS already implements for the TK5 and Colmi-SmartHealth families, so this is a delta port rather than a new protocol stack. Two things made it more than a catalog entry:

  1. The R10M needs its own family. It is not a Colmi ring and must not borrow Colmi art or Colmi capability claims. It also needs two firmware workarounds.
  2. The Android PR also fixed real bugs in the shared YCBT layer — bugs iOS had too, since both ports descend from the same vendor-SDK reading.

New family

  • RingDeviceType.ycbt (appended — it is persisted on Device.deviceTypeRaw), .full support level.
  • YCBTCoordinator — rejects a QRing service outright, then matches the catalog, the be940000 service, or ^R10M(?:[ _-][0-9A-Z]+)?$. Registered ahead of both Colmi coordinators, because ColmiSmartHealthCoordinator's <MODEL> <4 hex> convention accepts R10M FCF4. Deliberately does not claim TK5/R0x names or the shared 1078 company ID, so uncataloged rings still reach the family whose baseline was written for them.
  • WearableModel.r10mR10M (LittleMeatball), brand LittleMeatball, pattern ^R10M[ _][0-9A-F]{4}$ (both separators observed).
  • Baseline: HR, SpO₂, steps, sleep, REM, battery, manual HR/SpO₂, realtime HR/steps, measurement interval. Bitmap-gated: temperature, BP, manual BP, stress, fatigue, blood sugar, HRV, manual HRV, find device.
  • .spo2History is in neither set — that is what stops the 05 1a query being issued; the R10M's all-day SpO₂ rides the 05 09 combined record.

Firmware quirks — YCBTFamilyProfile

New profile struct threaded coordinator → driver → engine → encoder. TK5 and Colmi-SmartHealth pass both flags true, so their behaviour is byte-identical to before.

  • queryChipSchemeAtStartup: false — the R10M closes an otherwise healthy link with HCI 0x13 on the informational 02 1b.
  • supportsBloodPressureMonitor: false — the ring has the BP sensor but not the all-day 01 1c monitor, so the flag is separate from the capability.

R10M startup is now exactly 02 0002 0102 0701 1201 0401 0c01 2601 0303 09.

Shared YCBT fixes (these also improve TK5 / Colmi-SmartHealth)

File Fix
YCBTProtocol YCBTBytes.date resolved the UTC offset from today rather than the record's own date, silently shifting every record across a DST boundary by an hour. Now symmetric with ringSeconds, which was already date-aware.
YCBTHealthRecords Sleep segment durations are u24 — an all-ones field is 194 days, and the timeline expands positionally, so one bad 8-byte record allocated ~280 000 stage entries. Clamped to 24 h.
YCBTHealthRecords The 05 09 record no longer publishes its steps field. It is the ring's cumulative day counter and .activityUpdate is a per-day max ratchet, so the oldest record in a dump spanning midnight pinned today's total to yesterday's count for the rest of the day. Activity comes from the 05 02 buckets and the live 06 00 counter.
YCBTHistoryTransfer The terminal block only checked CRC, which cannot catch a short buffer. Now cross-checks the terminal's packet/byte counts against both the header's promise and the buffer actually built, NAKing into the existing retry-once path.
YCBTHistoryTransfer New append(types:) — extends the queue without disturbing the in-flight block, for capabilities the bitmap unlocks mid-walk.
YCBTDecoder 06 00 required only 2 payload bytes while reading 6; u16 returns 0 past the end, so a short frame published a valid-looking activity row with zeroed distance and calories. Now requires all three fields.
YCBTEncoder The five 01 xx monitors are capability-filtered — a ring is no longer asked to run a sensor it doesn't declare.
YCBTDriver Decoded events are filtered by capability before leaving the driver. A hidden UI card still accumulated rows that outlived the gate: they sync to HealthKit, feed coach summaries, and reappear as history the ring never measured.
YCBTSyncEngine History types are capability-gated, so a sync no longer spends 10 s of watchdog per absent type. R10M asks for exactly [0x02, 0x04, 0x06, 0x09].
YCBTSyncEngine 03 09 is re-issued once after the startup walk and ahead of pull-to-refresh — some R10M firmware ACKs the handshake's subscription without publishing until its dump finishes, leaving today's steps frozen.

RingBLEClient — required-subscription gate

WearableDriver gains requiredSubscriptionsBeforeConnected (default []) and immediatePostSubscriptionCommands() (default []), so jring/Colmi/LuckRing are untouched.

.connected previously fired on the first notifying characteristic. YCBT splits its protocol across two — command replies on be940001, live/history on be940003 — so the handshake could begin half-subscribed and silently lose whichever half was still pending. The client now waits for every declared channel, and prependWrites puts the driver's own handshake (02 03 name read, then 01 00 SetTime) ahead of the engine's startup sequence. The clock leads deliberately: records are stamped from the ring's RTC in local wall-clock, so a clock written after the walk begins mis-stamps everything read before it.

A ring missing a declared channel now fails the 20 s connect-attempt timeout with the existing user-facing message instead of hanging half-connected.

Ring art

PulseLoop/Assets.xcassets/r10m.imageset/ — 600×600 transparent RGBA PNG matching the tk5 / colmi-* convention, background-removed by edge-connected flood fill (a global white threshold would punch holes through the ring's own white enamel pattern). Verified composited on the dark theme for fringing. Note the Android PR shipped no art for this ring and falls back to Colmi R10 art on its hero card, which contradicts its own catalog comment; iOS gives the R10M its own.

Deliberate divergences from the Android PR

Not gaps — either Android catching up to behaviour iOS already had, or moot under CoreBluetooth:

  • GATT op serialization, op timeouts, recoverWedgedLink, MTU/priority skip, stale-GATT guards, process-wide GATT ownership — Android GATT-stack plumbing. CoreBluetooth queues internally and the iOS path already makes no MTU/priority request; the Android PR cites iOS as its reference for exactly this.
  • SubscriptionMode.INDICATION CCCD valuessetNotifyValue picks notify-vs-indicate from the characteristic's properties. Only the readiness half of the gate is portable.
  • isHistory flags on BP/temperature/stress events — iOS already separates history from live by which case is emitted (.historyMeasurement upserts, .bloodPressureSample appends). Adding the flag would be a second mechanism for the same thing.
  • PulseEventBus channel rework — the iOS bus is already an actor with an AsyncStream.
  • MeasurementRejected promoted to a real event — iOS already decodes .measurementRejected(mode:) and the spot-measurement path already consumes it.
  • Day-rollover view-model refresh — app-wide UI concern, orthogonal to R10M, and iOS resolves the current day differently. Out of scope here.

Verification

  • xcodebuild test756 tests, 0 failures (was 723; +33 new). Every existing TK5 / Colmi-SmartHealth / LuckRing / jring suite stays green.
  • swiftlint — 478 violations, unchanged from main (no new ones).
  • New YCBTCoordinatorTests; extended YCBTEncoderTests (quirk suppression, handshake order, monitor filtering), YCBTHistoryTransferTests (terminal cross-check, append, gated queue, live-status re-issue), YCBTHealthRecordsTests (sleep clamp, no stale steps row), YCBTDecoderTests (DST-correct timestamps, short 06 00), YCBTDriverTests (capability filtering across the bitmap and a reconnect).
  • Four existing tests were updated rather than worked around, because they pinned contracts this PR deliberately changes: the startup order (clock/name moved to the post-subscription handshake), and the 05 09 record's step row.

No R10M hardware was available for this port. The wire-level claims are carried by the Android PR's hardware validation plus the golden-vector tests above — not by a device session on iOS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant