Skip to content

Huawei AH100/CH100: fix the user record sent to the scale - #1450

Open
martinriedel wants to merge 4 commits into
oliexdev:masterfrom
martinriedel:fix/huawei-ah100-user-record-weight
Open

Huawei AH100/CH100: fix the user record sent to the scale#1450
martinriedel wants to merge 4 commits into
oliexdev:masterfrom
martinriedel:fix/huawei-ah100-user-record-weight

Conversation

@martinriedel

@martinriedel martinriedel commented Aug 10, 2026

Copy link
Copy Markdown

Closes #1449.

Two independent defects in the USER_INFO record this handler pushes to the
scale. Both verified on real hardware (advert name CH100, Chipsea CST34M97).

Correction to the original description. I first claimed the weight fix
collapsed the USER_CHANGED loop from 127 cycles to 5. That was wrong. The
cycle count mostly measures how long the user takes to step on — the loop
runs until a measurement arrives. Measuring the poll rate over long idle
sessions instead shows the weight has no effect and the length byte has a
large one. The original claim is struck below; the fix stays, on the grounds
that it was always about sending correct data.

1. Never send a zero weight

sendUserInfo falls back to user.initialWeight when no measurement has been
taken yet in the session, and that field is 0f on a fresh profile. Decrypted
from the wire:

11 22 33 44 55 10 01 | 29 | B9 | 00 | 00 00 | FF FF | 1C E2
auth token             41y  185cm pad  weight  resist  const
                                       = 0 kg

ScaleDeviceHandler.fallbackWeightKg() walks last stored measurement → profile
initial weight → BMI-22 estimate from body height, and never returns 0.
HuaweiCH100SHandler had the identical line and is fixed with it.

USER_CHANGED cycles: 127 → 5. No measurable effect on scale behaviour: in
matched idle sessions the poll rate is 245 ms with weight 0 and 266 ms with a
correct weight. This is a data-correctness fix, not a behaviour fix.

2. Declare 14, not 16, in the length byte

Huawei's own app sends DC 0E 09 followed by 16 encrypted bytes — the trailing
2-byte constant is outside the declared length. v2.5.4's "Total = 14 bytes" note
says the same; the 3.x port pulled the trailer into the payload and shifted the
length byte with it.

Two idle sessions, connected, nobody standing on the scale, length byte the only
variable:

length byte polls duration median interval
0x10 (16) 1026 274 s 266 ms
0x0E (14) 533 532 s 1001 ms

Four times slower once the record is declared the way the vendor declares it.

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 (33.8% → 33.9% across the user-record fix). StandardImpedanceLib cannot do
better: against Huawei's own numbers it reads 10–14 points low on every sample I
have, including the vendor-app captures in #547.

Tests

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 — they would stay green if the hardware disagreed. Added a
Real hardware capture section with frames taken off a scale and checked against
its display.

That section also pins the 16th byte the documented layout does not mention.
Decoding the btsnoop captures attached to #547 gives 13 measurements of one AH100
over 11 days: the byte is 0x4E on every one while weight (87.3–93.2 kg), body
fat (22.9–25.3 %) and impedance (373–451 Ω) all move. A per-device constant, so
dropping it is correct.

Those same captures answer a question that comes up periodically: AH100 and
CH100 do not need separate handlers.
A 2020 AH100 driven by the Huawei app uses
the same frame start bytes, the same opcodes, the same field order and the same
scaling as a 2026 CH100 driven by openScale. Splitting on the advert name would
also split on the wrong key — AH100 hardware advertises as CH100 (see #1313 and
my own unit).

Full unit test suite passes.

🤖 Generated with Claude Code

martinriedel and others added 3 commits August 10, 2026 10:53
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>
@martinriedel martinriedel changed the title Huawei AH100/CH100: never send a zero weight in the user record Huawei AH100/CH100: fix the user record sent to the scale Aug 10, 2026
@oliexdev

Copy link
Copy Markdown
Owner

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)
in each of the two Huawei handlers. Behaviour is identical — lastMeasurementFor() is already protected on the base class, so nothing had to be added there.

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.

@martinriedel

Copy link
Copy Markdown
Author

Will check and reply

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.

Huawei AH100/CH100: user record is sent with weight 0, scale re-requests it 127x per session

2 participants