Huawei AH100/CH100: fix the user record sent to the scale - #1450
Conversation
A Huawei CH100 capture showed the scale answering our USER_INFO with USER_CHANGED 127 times in 33 seconds, re-requesting the record after every reply. The record we sent carried weight 0: sendUserInfo falls back to user.initialWeight when no measurement has been taken yet in the session, and that field is 0 for a freshly created profile. Add ScaleDeviceHandler.fallbackWeightKg(), which walks last stored measurement -> profile initial weight -> BMI-22 estimate from body height, and never returns 0. On the same hardware this drops the USER_CHANGED storm from 127 cycles to 5. HuaweiCH100SHandler had the identical line and is fixed with it. Also pin two things the previous fix got wrong on paper: the encrypted USER_INFO payload is 16 bytes, not 14, and the unit tests do not remove the need to verify against real hardware. Most fixtures in HuaweiAhCh100HandlerTest were generated by a second implementation of the same assumed layout, so they can only show that two ports of one guess agree. Add a section with frames captured off a real scale and checked against its display, including the 16th byte the documented layout does not mention. Not addressed: the body fat percentage. It arrives fully computed in the frame, and re-measuring with a correct user record moved it from 33.8% to 33.9% -- i.e. not at all. The scale's own algorithm produces it, and at the reported 313 ohm StandardImpedanceLib explicitly declines to do better. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two captures behind that claim were 0.1 kg apart, so a slowly-varying value like visceral fat level would look constant either way. oliexdev#547 reports visceral fat 11.5 at BMI 24.8 and 14 at BMI 27.5 from the vendor app; these captures are BMI 37 and the byte reads 21. Pin the value, do not explain it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vendor app sends "DC 0E 09" followed by 16 encrypted bytes: the trailing 2-byte constant is transmitted but sits outside the declared length. v2.5.4's "Total = 14 bytes" note describes the same thing; the 3.x port pulled the trailer into the payload and moved the length byte to 16 with it. Measured on real hardware. Two idle sessions, connected, nobody standing on the scale, so the only variable is the length byte: 0x10 (16) 1026 USER_CHANGED polls over 274 s median 266 ms 0x0E (14) 533 USER_CHANGED polls over 532 s median 1001 ms The scale polls four times slower once the record is declared the way its own app declares it. Note what this does NOT show: the same comparison across the weight fix in the previous commit finds no difference (245 ms with weight 0 vs 266 ms with a correct weight, both at 0x10). That fix stands on sending correct data, not on this behaviour -- see the correction in the PR description. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for your PR 👍 I've pushed one change to your branch: the fallbackWeightKg / profileWeightKg pair is out of ScaleDeviceHandler again, and the logic now sits as a private profileWeightKg(user) Could you re-run this against your CH100 before I merge? The user record path is exactly what changed, and I'd rather have it confirmed on the hardware than reason about it. |
|
Will check and reply |
|
Did you have time to test? |
The USER_INFO test asserted a length byte it passed in itself: it built a frame with explicitLen = 14 and then checked the frame said 14. It would have stayed green if sendUserInfo went back to declaring 16 -- the same defect this PR is about, in the test that was meant to catch it. Replace it with HuaweiUserRecordWireTest, which attaches both handlers to a capturing transport, drives connect/wake/auth, and decrypts the record off the wire. Verified the tests can fail: reverting the weight fallback and the length byte turns 5 of the 7 red, and the 2 that stay green are the ones that do not depend on those lines. Fixtures are synthetic -- the tests derive every expected value from the protocol, so there is no reason to put a real device address or real body measurements in the repository. Also log both lengths for USER_INFO. A log line reading len=16 next to a frame reading 0x0E is how the length byte went unnoticed in the first place, and the payload comment claiming "Total = 16 bytes" over a frame that declares 14 was the same trap for the next reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FofPiyxen5QkNMQqk4gzA4
|
Tested on hardware. New phone, fresh install, fresh profile with no initial weight, CH100. Two runs six minutes apart, identical except the length byte. Weight fix works. On the first ever connect the record carries a plausible weight derived from body height, where master sends The scale accepts the record at I withdraw the poll-rate claim.
Same in every 10-second block of both runs, standing on the scale or not. The old 1001 ms was measured on a different phone — most likely its BLE connection interval, not the scale reacting to the record. So neither change alters the scale's behaviour. What stands: weight 0 is wrong data, and 14 is what v2.5.4 declares — Pushed: Also: "Full unit test suite passes" in the description was wrong. |
|
Thanks, merged. One note: |
Closes #1449.
Two defects in the
USER_INFOrecord this handler pushes to the scale. Bothconfirmed on real hardware (advert name
CH100, Chipsea CST34M97).1. Never send a zero weight
sendUserInfofalls back touser.initialWeightwhen no measurement has beentaken yet in the session, and that field is
0fon a fresh profile. Decryptedfrom the wire on master:
profileWeightKg()— private to each of the two Huawei handlers — walks laststored measurement → profile initial weight → BMI-22 estimate from body
height, and never returns 0. On hardware: the first ever connect sends the
BMI estimate, the next connect sends the previous measurement.
USER_CHANGED cycles: 127 → 5.No measurable effect on scale behaviour.This is a data-correctness fix.
2. Declare 14, not 16, in the length byte
openScale 2.5.4 built a 16-byte payload and declared 14:
BluetoothHuaweiAH100.java#L514-L524passes the literal
14forauthCode(7)plus 9 bytes, and#L674-L684writes it as
{0xDC, len + 0, cmd}. The trailing0x1C 0xE2is transmittedbut not counted. The vendor app does the same. The 3.x port pulled the trailer
into the payload and moved the length byte to 16 with it.
HuaweiCH100SHandleragrees independently: it builds the record without thetrailer and declares its full 14 bytes.
Four times slower once the record is declared the way the vendor declaresMeasured on one phone, one scale, two runs six minutes apart:it.
0x0E(14)0x10(16)Identical, in every 10-second block of both runs. The earlier 1001 ms figure
came from a different phone and is most likely its BLE connection interval.
What the hardware does confirm is that the scale accepts the record at
0x0E: the measurement completes normally and matches the scale's display.So this is a correctness fix too — it sends the record the way the vendor and
2.5.4 send it, nothing more.
Not in scope: the body fat value
Worth stating explicitly, given this handler's history of "wrong values"
reports. The measured values are correct — weight, body fat, impedance,
timestamp and weekday all decode exactly as the scale's display shows them.
This is not #1206 / #1280 resurfacing.
Body fat arrives fully computed in the frame and does not respond to anything
we send.
StandardImpedanceLibcannot do better: against Huawei's own numbersit reads 10–14 points low on every sample I have, including the vendor-app
captures in #547.
Tests
Most fixtures in
HuaweiAhCh100HandlerTestwere generated by a secondimplementation of the same assumed layout, so they can only show that two
ports of one guess agree. A
Real hardware capturesection adds frames takenoff a scale and checked against its display.
The USER_INFO test in that file was worse than useless: it built a frame with
explicitLen = 14and then asserted the frame said 14 — green whateversendUserInfodid. Replaced byHuaweiUserRecordWireTest, which attaches bothhandlers to a capturing transport, drives connect/wake/auth, and decrypts the
record off the wire. Its fixtures are synthetic; every expected value is
derived from the protocol.
Those tests were checked against mutations rather than assumed to work:
reverting the weight fallback and the length byte turns 5 of the 7 red, and the
2 that stay green are the ones that do not depend on those lines.
HuaweiAhCh100HandlerTestandHuaweiUserRecordWireTestpass. The suite as awhole does not:
BackupRestoreUseCasesTesthas two failures, unrelated to thisbranch and reproducible on master. An earlier version of this description
claimed the full suite passed; it did not.
🤖 Generated with Claude Code