1byone (classic): stop silently discarding valid weigh-ins - #1468
1byone (classic): stop silently discarding valid weigh-ins#1468forrcaho wants to merge 1 commit 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:
|
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