Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Tags: foureight84/PulseLoopAndroid

Tags

v2.9.1+54-rc1

Toggle v2.9.1+54-rc1's commit message
PulseLoop 2.9.1 (54) rc1 — JRing sleep history (#73), Connect button (#…

…72)

v2.9.0+53

Toggle v2.9.0+53's commit message
PulseLoop 2.9.0 (53)

v2.9.0+52-rc1

Toggle v2.9.0+52-rc1's commit message
PulseLoop 2.9.0 rc1 — features #65, #66, #67, #68, #70 plus the revie…

…w pass on them

v2.8.0+51

Toggle v2.8.0+51's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #62 from foureight84/fix/issues-58-59-60

fix: YCBT spot-HR completion (#59), delete individual readings (#60), R100 / CRP temperature history (#58)

v2.8.0+50-rc10

Toggle v2.8.0+50-rc10's commit message
fix(ycbt): the terminal block decides a transfer, not the header's es…

…timate (#69)

Hourly SpO2 stopped importing on a reporter's COLMI_SMART_HEALTH ring while HR
from the same hours stayed complete. Their diagnostics export has the answer,
and it is not where either of us looked first.

`handleTerminal` required the terminal block's packet count to equal the one
the header declared. This ring's firmware contradicts its own estimate one
frame later: the `05 09` header declares **5** packets for 840 bytes and the
ring then sends **6**, because it packs whole 20-byte records into each frame
(7 x 20 = 140) rather than filling it, so the header's `ceil(bytes / frame)`
is one short. Bytes and CRC were correct on all six transfers in the export.
We nacked every one, retried once, and skipped the type.

What that cost is larger than it sounds. The composite `05 18` record is the
only source of SpO2 history on this family — the dedicated `05 1a` query was
sent ten times in that session and never answered once — so SpO2 disappeared
while HR survived from `05 15`. Respiratory rate, HRV, blood pressure,
temperature and blood sugar ride in the same record.

It only bites above one packet, which is why the record imported while the
day was young and stopped once it had grown past one frame, and why it looked
like a regression from a release that had touched nothing nearby. Neither rc6
nor rc7 changed this path; the reporter was right to doubt the timing.

The vendor agrees with the ring rather than with us: at `Sync_Block_Verify`
(128) `DataUnpack` reads the two count bytes into locals it never compares,
sizes its buffer from the *terminal's* length, and accepts on
`crc16_compute(...) == crc` alone. So the header's totals now only size the
buffer, and a transfer is judged on the terminal's byte count against what
arrived, and then the CRC. A short transfer still nacks — that test is kept
and covered.

Worth recording for the next one of these: the frames were in the log the
whole time, marked `unknown`, which is what every accumulating history frame
looks like — 427 sleep frames in the same export say `unknown` and sleep
imports fine. A silent reject-and-skip on a self-consistency check leaves no
failure anywhere. When one metric is missing and its neighbours are not, look
at what the transfer did with the block before suspecting the parser.

1312 tests, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

v2.8.0+49-rc9

Toggle v2.8.0+49-rc9's commit message
fix(measure,sleep,diag): four review findings from the rc6 pass (#59, #…

…60, #63)

Recorded as open on PR #62 and each needing its own decision rather than
being a follow-on from the rc6 changes. Four of the five are fixed here;
the fifth is left alone deliberately (see below).

**A deleted `spot` reading came back under the ring's id (#60).** A `spot`
row carries a UUID, so `record` filed no tombstone for it — but that row
exists precisely because the ring logged the measurement itself and hands it
back as `history:<kind>:<ts>` on the next sync, so the reading returned and
had to be deleted twice. The ring stamps its log to the minute rather than to
our settled instant, so the tombstone cannot name the id it must suppress: a
`spot:<kind>:<ts>` row records the kind and our timestamp, and
`isSpotDeleted` matches it the way `adoptRingsCopy` matches the pair it
reconciles. A retake inside the window inherits the suppression — stored and
displayed, never adopting the ring's copy — which is the same ±90 s ambiguity
`adoptRingsCopy` already carries, failing towards keeping a reading the user
asked for.

**A spot HR leg and a workout cannot share the stream (#59).** The gate is
one switch per kind and the leg sampled it once at the start, so a workout
starting or stopping inside the leg left it wrong for the rest: the workout's
samples dropped, or the leg's converging samples stored as workout rows with
its settled value never published. A spot measure is refused while a workout
runs, a workout starting mid-leg aborts it, and the gate is now reopened
unconditionally — the leg closed it, so it owes the reopen even when it
aborts. No unit test: `RingSyncCoordinator` needs a BLE client and has no
harness.

**Two sleep records meeting with no gap read as one run (#63).** The
retirement run grew across any block abutting it end-to-start so a shortened
re-send could retire its own stale tail, but a block has no record identity —
`sessionId` is the merged row — so that is the same shape as the neighbouring
record of a split night. The reporter's ring closes one record and opens the
next 33 seconds later; whether they round to the same minute is a coin toss
and losing it wiped five hours. Retirement now requires real overlap. The
trade is a stale tail surviving a genuine shortening: minutes rather than
hours, and rarer than it was now that a re-send reproduces the record's
declared bounds instead of a drifted end.

**Diagnostics masking leaked two ways (#58).** It used the *connected* ring's
family for every stored packet, so a report exported after switching rings
masked the old family's frames with the wrong header length — CRP's is the
longest, so a Colmi frame masked as CRP kept five bytes of samples. The family
is recorded at capture time now (`raw_packets.deviceTypeRaw`, v25); a row from
before it existed masks from byte 1, which is less useful and never wrong in
the direction that leaks. Separately, a CRP reply spanning several
notifications only decodes when its last chunk lands, so the earlier chunks
reached the log as `unknown` — deliberately exported whole, since control and
pairing frames decode to nothing and are what most connection reports are
taken for. `RingDecodedEvent.FramePending` names them: `frame_start` keeps its
family's header, `frame_chunk` keeps byte 0 and nothing else, because the
middle of a frame is payload from byte 0 on.

**Not fixed: the Colmi sport-session blind window.** A ring that accepts
`0x77 01` but never pushes `0x78` shows nothing for ~120 s at the first
workout of a connection. No vendor support bit exists to gate on and no
hardware here has the firmware, and the reporter on #64 is mid-test on that
exact path — shortening the deadline now would invalidate the run without
telling us anything. It waits for their diagnostics export.

1310 tests, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

v2.8.0+48-rc8

Toggle v2.8.0+48-rc8's commit message
fix(sleep): a zero-length segment no longer shadows a real one (#63)

The vendor de-duplicates a sleep record's stage segments on `sleepStartTime`
(`DataUnpack` case 4 keeps the first it sees for a given start), and the ring
emits zero-length segments. So a zero-length segment sharing a start time with
a real one took its place and the real segment was dropped.

Every duplicate start in the reporter's fifteen-frame, thirteen-record night
dump is that exact shape — a zero-length LIGHT immediately ahead of a real
76–105 s segment — 351 s across the dump. Since `placeStages` reads a minute no
segment claims as wake, the dropped minutes did not merely go missing: they
surfaced as wake. That is the whole of the residual gap on the reporter's
Sept 9, where three of the four awake minutes the card showed are this artifact
and the ring's own `wakeDuration` for the night is 1.6 minutes.

The vendor de-duplicates the same way and does not care, because its headline
comes from the header's own totals (`SleepActivity:695` sums `deepSleepTotal +
lightSleepTotal + remTotal`) rather than from the segment array. Ours is counted
off the timeline, so it cannot afford to lose a segment. A zero-length segment
claims no minute either way, so skipping it before the de-duplication costs
nothing and keeps the vendor's rule for every segment that does.

Also recorded in AGENTS.md, from reading the dump against the vendor source:
when `deepSleepCount` (+12) reads `0xffff` the three following u16s are seconds
in the order rem (+14), deep (+16), light (+18) — REM first — and on the dump
deep+light+rem agrees with the timeline we count to within a minute on 12 of 13
records, which says the counted timeline is sound. The record length at +2 is
bytes, not minutes; it reads plausibly as minutes (244, 164, 268, 140) and was
misread that way for several rounds of this issue.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

v2.8.0+47-rc7

Toggle v2.8.0+47-rc7's commit message
fix(sleep): place stage segments against the record's declared bounds (

…#63)

A YCBT sleep record carries its own bounds in the header — DataUnpack reads
`startTime` at +4 and `endTime` at +8 — and every segment carries its own
`sleepStartTime`. We read neither: segment timestamps were used to de-duplicate
and then discarded, and the timeline was concatenated as `round(seconds / 60)`
minutes per segment from the first segment's start.

That drifts. On the captured night already in the tests the header declares a
474-minute span and totals 473.7 minutes of sleep; concatenation stored 470,
on a night with no wake at all. The ring leaves a one-second gap between every
consecutive segment (49 of them there) and each segment rounds independently
against a one-minute floor, so the derived end lands wherever the rounding
takes it — short there, long elsewhere.

One minute of long drift loses a whole session. `completeSessionSurvivors`
grows its retirement run across blocks that abut end-to-start exactly, so a
record ending at 05:58 instead of its declared 05:57 stops reading as a
session across a one-minute gap and becomes the block immediately before the
next one: the run swallows it, and the interval replace puts back only the
later record. That is the two-record night reported on rc6, where 5 h 22 went
missing while nights with nine- and twenty-minute gaps still merged correctly.

Each segment is now placed at its own `sleepStartTime` for its own `sleepLen`,
across a run spanning exactly the header's bounds. Minutes no segment claims
read as awake, which is honest — the ring reports wake as its own segment type
(0xf4). A record whose header carries no usable bounds keeps the concatenated
reading, there being nothing better to place against. The captured night
decodes to 94/251/129 deep/light/rem against the fixture's 93/249/130: the
timeline is unchanged, only its endpoints are now the ring's.

The merge rule itself is left alone. It is defensible once its input is
accurate, and loosening it would mask this rather than fix it.

Claude-Session: https://claude.ai/code/session_01VuWpdVvATRkaSja3SFFTQj

v2.8.0+46-rc6

Toggle v2.8.0+46-rc6's commit message
fix(measure,sleep,diag): address review findings on the rc6 changes a…

…nd the branch

From a review pass over fix/issues-58-59-60. Five findings fixed here; the
rest are recorded on the PR.

The ring's verdict on THIS run, not the family, owns the reading (#59/#60).
A YCBT HR run that hit the 45 s ceiling without a 04 0e was settled on its
last sample and stored as "spot", so the next sync could delete it in favour
of an unrelated all-day grid sample — the exact failure the ringWillLogIt gate
exists to prevent. The last-sample rule and the "spot" marking now both derive
from the run's own completion; a run the ring never ended falls back to the
consistency gate. The SpO2 leg gets the same ownership rule.

A tombstoned history sample adopts nothing. upsertUnlessDeleted skipped the
write but adoptRingsCopy still ran, so a deleted sample re-sent on every sync
kept retiring any spot reading within 90 s of it.

Sleep score denominators follow the two numbers a session now carries (#63).
Stage shares and the duration band are of time asleep; the awake share and
the "does this ring label awake" coverage heuristic are of the span — judged
against asleep time that heuristic was true for every ring. The one-time
repair recomputes the stored score with the duration, and runs as a single
transaction so it cannot overwrite a night the first sync just reconciled.
Archive restore restates every night from its own blocks the same way, since
a pre-#63 backup carries span totals and the repair will not run again.

Every Colmi 0x78 sport frame is tagged for masking (#64). A warm-up frame with
bpm 0 decoded to nothing, fell through to "unknown", and exported the
workout's live steps, distance and calories in clear.

v2.8.0+45-rc5

Toggle v2.8.0+45-rc5's commit message
fix(measure,workout): gate the ring-copy rule to rings that log, keep…

… sport mode after a ring-ended session

Review findings on this PR.

- `adoptRingsCopy` ran for every family, but only a ring that reports its own
  completion logs a spot measurement into its history. CRP and Colmi record
  all-day HR/SpO2 on a five-minute grid, so an unrelated grid sample landing
  within the 90 s match window would delete the reading the user asked for.
  The gate is now at write time: `ringWillLogIt` rides the settled-reading
  event, and only then is the row marked `"spot"`. Every other family stores a
  plain live row, as before this PR.
- `trigger_measurement` read `latestHRValue` / `latestSpO2Value` after calling
  the leg. Those hold the last raw streamed sample, are deliberately not the
  settled reading, and are not cleared on failure — so a failed measurement
  reported the pre-converged plateau as completed. It now uses what the leg
  returned.
- A `0x78` status 3 is the ring saying this sport session finished, not that it
  cannot run one. It no longer sets the sticky `sportRejected`: the rest of that
  workout runs on the plain stream and the next workout opens a fresh session.
  The refusal reply and exhausted watchdog silence stay sticky.
- The R100 name pattern matched `R100 1A2B`, which is the SmartHealth
  space-separated serial convention, and the R100 card precedes the SmartHealth
  one — so a SmartHealth ring would have taken the CRP driver and never synced.
  Tightened to the `_<hex>` serial the reports actually show.
- `Reading.fromHistory` came from `sourceRaw`, but Colmi stress and temperature
  history rows carry other sources while still using regenerable `history:` ids.
  The delete confirmation dropped its "stays deleted" line for exactly those.
  It now asks the same question the delete path does.