1byone (classic): stop silently discarding valid weigh-ins - #1468
1byone (classic): stop silently discarding valid weigh-ins#1468forrcaho wants to merge 3 commits into
Conversation
The 1byone "Health Scale" dropped roughly half its readings, with no log line
and no user-visible error, so a working connection was indistinguishable from a
broken one. Two independent causes, both found by diffing openScale session logs
against the decompiled vendor app ("New iWellness 4.0", com.lefu.es.*).
Coalesced duplicate frames were parsed as history. The scale sends its final
measurement twice, and the two copies can arrive in a single notification: the
ATT payload caps at 20 bytes, so the buffer is one complete 11-byte frame
followed by the first 9 bytes of its duplicate. parseMeasurementFrame decided
history-vs-live on `size >= 18` alone, so it read bytes 11..17 of that buffer as
a timestamp, produced year 53138 / day 156 / hour 39, and the non-lenient
Calendar threw into a `catch { return }` that discarded the reading. Decide on
the XOR checksum at byte 10 plus a second 0xCF marker at byte 11 instead. A
genuine history frame carries the year high byte (0x07) at byte 11 and
measurement data at byte 10, so the two cases separate cleanly and history reads
on the Eufy C1/P1/A1 models sharing this handler are unaffected.
Zero-impedance frames lost the weight as well. The scale reports impedance 0
when it cannot run the bioimpedance measurement -- socks, shoes, poor foot
contact -- and the handler returned early, losing an otherwise valid weigh-in.
Publish the weight and skip only the derived body composition, matching what
EufyP2Handler and EufyC20Handler already do.
Two smaller fixes in the same paths:
Gate live frames on the lock status (byte 9 in {0x00, 0x36}, the vendor app's
"locked" values) so in-progress readings are not recorded. This replaces the
older `b9 != 1` guess with the protocol's own rule.
Arm a 3s fallback for the `F1 00` clock ACK. This scale never sends it -- the
vendor app never even sends F1 on this model -- yet both the history request and
the "step on the scale" prompt hung off it, so the user got no feedback at all.
waitAckClock is deliberately left set so a late ACK still starts the history
read; only the prompt is forced, once per connection.
Confirmed on hardware with a 1byone Health Scale: the same 103.00 kg weigh-in
taken a minute apart in socks (impedance 0, weight now saved instead of lost)
and barefoot (coalesced 20-byte frame, impedance 351 ohm, full body composition
derived). The unit-test vectors are the captured frames from those sessions plus
the three earlier coalesced frames that were being dropped.
Known gap: when a reading has no impedance the user is told nothing, because a
snackbar emitted at publish time is dismissed by BleConnector's saved-measurement
snackbar ~700 ms later. Fixing that properly belongs in the save path rather
than in a handler, so it is left out of this change and raised separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Two things before merging, both about the Eufy C1/P1/A1 that supportFor also matches and neither of us can test:
|
Removes the weight-without-impedance path added in the previous commit, along
with the byte-9 lock gate that existed to make it safe. What remains is the
coalescing fix and the clock-ACK fallback, both confirmed on hardware.
Publishing a weight with no body composition looked like a strict improvement
over discarding the weigh-in. It is not. BleConnector drops any value that is
not > 0f, so the six body-composition fields reach the database as *missing*
rather than zero, and MeasurementTransformationUseCase.applyValueInheritance
then fills them in from the preceding measurement:
Carry-over: inherited 6 value(s) from measurement 8.
Carry-over: inherited 6 value(s) from measurement 10.
The second line is the tell: measurement 10 is itself a weight-only record, so
the stale values propagate forward from weigh-in to weigh-in. The user is shown
a complete-looking reading whose body fat, water and bone mass are days old. A
discarded reading is visibly absent; this one is quietly wrong.
The same capture also produced two records for one weigh-in. With socks on the
scale retries the bioimpedance measurement, and it re-sent the settled frame
nine seconds later, well outside any plausible de-duplication window.
Both review points are answered by this removal rather than by more machinery.
The lock-status gate is gone, so nothing guesses at Eufy's status byte values.
The rate limiter is untouched from master, so weight-only frames can no longer
consume its window -- there are none.
Weight-only measurements are still worth supporting, but not from a handler:
the carry-over interaction affects every handler that already publishes them
(EufyP2Handler, EufyC20Handler, VitafitVT701Handler's fallback). Raised
separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The clock-ACK fallback fires 3s after connect, but a reading can arrive sooner than that -- 2.4s in one capture. The prompt then lands after the weigh-in has already completed and, because a new snackbar dismisses the current one, it replaces the "measurement saved" notice with an instruction the user has just carried out. Any CF measurement frame proves the user is on the scale, so cancel the pending prompt when one arrives. Done before parsing rather than at publish, so it also covers frames that are subsequently discarded: a frame without impedance still means someone is standing there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
After messing around with trying to get the weight with socks on to work correctly, we decided to drop that part and just have the reassembling frames part in this PR. As Claude puts it: Thanks — both points led somewhere I didn't expect. I've dropped the feature they were about rather than defending it. What changedThe second commit removes weight-only publishing entirely, along with the byte-9 lock gate that existed to make it safe. What's left is the coalescing fix and the clock-ACK fallback, both confirmed on hardware. WhyYour first point sent me to check what a weight-only measurement actually looks like once it's saved, and the answer is: not weight-only. The second line is the problem — measurement 10 is itself a weight-only record, so the stale values propagate forward from weigh-in to weigh-in. The reading looks complete, and its body fat, water and bone mass are days old. That's worse than the bug I set out to fix. A discarded reading is visibly absent; this one is quietly wrong. The same capture also produced two records for one weigh-in: with socks on, the scale retries the bioimpedance measurement and re-sent the settled frame nine seconds later, well outside any plausible de-duplication window. So the hold-and-flush scheme I'd written for your second point wouldn't have collapsed it either. Where that leaves your two pointsThe lock gate is gone rather than scoped. Nothing guesses at Eufy's status byte values now, so the failure mode you identified can't occur. Eufy is byte-identical to master apart from the coalescing fix, which is protocol-level — it stops a duplicated frame being misread as history and doesn't depend on vendor semantics. The rate limiter is untouched from master. It only became a problem because weight-only frames started reaching it; there are none now. What's left, and how well it's tested
Nothing speculative remains, and nothing in the change now depends on hardware I can't test. Follow-upWeight-only measurements are worth supporting — my scale produces them whenever bioimpedance fails, and simply losing the weigh-in isn't great either. But the carry-over interaction isn't handler-specific: |
The following was generated by Claude, as was the code in this PR. As a user, I can say that my scale was receiving a reading only about a third of the time before, and after this fix it works every time.
Summary
My 1byone "Health Scale" needed about three attempts per reading. It turned out the connection was never the problem — every connection succeeded on the first try. The readings were arriving, being parsed, and then silently discarded by
OneByoneHandler, with no log line and no user-visible error, so a working connection looked identical to a broken one.There were two independent causes. Both are fixed here, with regression tests built from the captured frames.
Cause 1: coalesced duplicate frames parsed as history
The scale sends its final measurement twice. Sometimes the copies arrive as two separate 11-byte notifications, and sometimes coalesced into one — the ATT payload caps at 20 bytes, so the buffer is a complete 11-byte frame followed by the first 9 bytes of its duplicate:
parseMeasurementFramedecided history-vs-live on length alone:20 ≥ 18, so it read bytes 11–17 as a timestamp, got year 53138, month 14, day 156, 39:29:19, and the non-lenient
Calendarthrew into acatch { return }that dropped the reading.The fix decides on the protocol's own XOR checksum at byte 10 plus a second
0xCFmarker at byte 11. A genuine history frame carries the year high byte (0x07) at byte 11 and measurement data at byte 10, so the two cases separate cleanly — history reads on the Eufy C1/P1/A1 models sharing this handler are unaffected.Cause 2: zero impedance threw away the weight
The scale reports impedance
0when it cannot run the bioimpedance measurement — socks, shoes, poor foot contact. The weight is still perfectly good, but the whole weigh-in was lost.Now the weight is published and only the derived body composition is skipped, matching what
EufyP2HandlerandEufyC20Handleralready do:Two smaller fixes in the same paths
0x00,0x36} — the vendor app's "locked" values — so in-progress readings are not recorded. This replaces the olderb9 != 1heuristic with the protocol's actual rule.onConnectedgated both the history request and the "step on the scale" prompt on anF1 00ACK. This scale never sends one — the vendor app never even sendsF1on this model — so the user got no feedback whatsoever. A 3 s fallback now prompts anyway.waitAckClockis deliberately left set so a late ACK still starts the history read.Verification
Confirmed on hardware — the same 103.00 kg weigh-in taken a minute apart:
CF 00 00 3C 28 00 00 00 01 00 DACF B6 0D 3C 28 B4 B5 99 01 00 F9 CF B6 …OneByoneHandlerTestcovers both, plus the three earlier coalesced frames that were being dropped and a synthetic history frame confirming history detection still works. All frame vectors are verbatim from session logs, not synthesised.Reverse-engineering was cross-checked against the decompiled vendor app ("New iWellness 4.0",
com.lefu.es.*) — that's the source for the byte-9 lock values and for the fact thatF1is never sent on this model.Deliberately not included
When a reading has no impedance, the user is still told nothing. A snackbar emitted at publish time is dismissed by
BleConnector's saved-measurement snackbar ~700 ms later, so it never really appears. Fixing that properly belongs in the save path rather than in a handler, so I've raised it separately rather than working around it here.Two other things I looked at and left alone, both mentioned in case they're of interest:
MGBHandler.tryPublishStreaminghas the same "drop the weight if impedance never arrives" bug this PR fixes (val impedanceOhm = streamingImpedanceOhm ?: return), with no timeout fallback. I have no Dr. Trust hardware to test against, so I haven't touched it.connectGatt(autoConnect = true)and rescans every 10 s indefinitely, where openScale uses a direct connect after a 650 ms delay. That's a real robustness difference, but the logs show it costing nothing here, so it isn't part of this change.🤖 Generated with Claude Code