Fix Huawei AH100 / CH100 measurement parsing (closes #1206, #1280)#1374
Fix Huawei AH100 / CH100 measurement parsing (closes #1206, #1280)#1374danieldorigatti wants to merge 2 commits into
Conversation
…liexdev#1280) The 3.x Kotlin rewrite of the Huawei AH100/CH100 handler regressed the measurement decoder in a few breaking ways. As reported in oliexdev#1206 and oliexdev#1280, real-world readings come back as 138 kg / 180 % / year 3084 for what the scale displays as 95.0 kg / 28.0 %. Root causes: * The two-half encrypted measurement frame is no longer AES-CTR decrypted; the handler reads XOR-deobfuscated bytes directly. * The byte layout was replaced with invented offsets and a fanciful `(1457 - byte) / 10` weight formula. * The plain-frame length byte was changed from v2.5.4's `payload.size + 1` to `payload.size`, which appears to break AUTH on at least some firmware revisions. * The handler matched advertised name `"CH100"` while declaring itself "Huawei AH100" — so a real AH100 scale (which advertises `"AH100"`) was not recognised at all. This commit restores the v2.5.4 wire protocol exactly, splits AH100 and CH100 into siblings sharing a common base, and locks the protocol behind unit tests so the regression cannot recur silently. Code changes ------------ * `core/bluetooth/libs/HuaweiAhCh100Protocol.kt` — new pure-Kotlin object holding all wire-level primitives: `obfuscate`, `aesCtr`, `xorChecksum`, `buildAuthToken`, `deriveMagicKey`, the plain / encrypted frame writers, and `parseMeasurement`. JVM-testable, no Android dependencies. `INITIAL_KEY` / `INITIAL_IV` are exposed as defensive copies so a stray mutation can't corrupt subsequent sessions. * `core/bluetooth/scales/HuaweiAhCh100ScaleHandler.kt` — new abstract base class containing the shared BLE state machine. Concrete subclasses only override the advertised name. `HISTORY_READ` is declared as a *capability* but not yet *implemented* — the legacy history-pull path is wired but not invoked from the lifecycle. * `core/bluetooth/scales/HuaweiAH100Handler.kt` rewritten as a thin subclass that matches advert `"AH100"`; new `HuaweiCH100Handler` matches `"CH100"`. The previous handler matched only `"CH100"` despite calling itself "Huawei AH100", so AH100 hardware was silently unsupported. * `core/bluetooth/ScaleFactory.kt` — register the new CH100 handler. Wire-protocol corrections ------------------------- * Plain `0xDB` writer restored to `lengthByte = payload.size + 1`. * Encrypted `0xDC` writer kept at `lengthByte = payload.size`. * Measurement decode: AES-CTR-decrypt the *first half's* deobfuscated tail (matching v2.5.4) and parse the documented layout `[user, weightLE, fatLE, dateTime, impedanceLE]`. * MTU resilience: if the first half already contains ≥ 15 plaintext bytes the handler publishes immediately without waiting for the `0x8E` second half, so the default Balanced tuning profile (which bumps MTU to 185) works as well as the legacy 23-byte MTU. We do not pin a tuning profile. * `supportFor` matches advertised names tolerantly: trailing NULs / whitespace and `HUAWEI ` / `HONOR ` prefixes are stripped before comparison. Some real-world devices advertise `'CH100'` followed by 15 NUL bytes, which the previous strict equality rejected. Verification ------------ * 18 new JVM unit tests in `HuaweiAhCh100ProtocolTest` lock every primitive plus three real-shaped fixture frames (97.0 kg / 28.0 %, 63.6 kg, 90.0 kg). Fixtures are generated by an independent Python re-implementation of the v2.5.4 protocol; the Kotlin tests are green only if both ports agree on the bytes. * `./gradlew :app:testDebugUnitTest` — 100/100 tests pass across all suites (no existing tests broken). * `./gradlew assembleDebug` — succeeds, same path the upstream `ci_master` workflow uses. * Tested end-to-end on physical Huawei CH100 hardware against the user that filed oliexdev#1280; reading is now correct. Note for users migrating from openScale 2.5.x or Huawei Health: you may need to "Forget" the scale once in Android Bluetooth settings before the first connection. The link-layer encryption keys from the previous bond don't survive this protocol's re-pair flow and otherwise cause a `CONNECTION_TERMINATED_MIC_FAILURE` after the first command. Refs: oliexdev#1206 oliexdev#1246 oliexdev#1276 oliexdev#1280
| private fun ts(d: Date) = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(d) | ||
| class HuaweiAH100Handler : HuaweiAhCh100ScaleHandler() { | ||
| override val supportedAdvertName: String = "AH100" | ||
| override val displayName: String = "Huawei AH100" |
There was a problem hiding this comment.
No need for that class anymore should be handled in HuaweiAhCh100Handler
There was a problem hiding this comment.
HuaweiAH100Handler and HuaweiCH100Handler are removed; the single HuaweiAhCh100Handler now matches both the AH100 and CH100 advertisement names.
| * previous bond don't survive this protocol's re-pair flow and otherwise | ||
| * cause `CONNECTION_TERMINATED_MIC_FAILURE` after the first command. | ||
| */ | ||
| abstract class HuaweiAhCh100ScaleHandler : ScaleDeviceHandler() { |
There was a problem hiding this comment.
Please rename the class name for Consistency to HuaweiAhCh100Handler
There was a problem hiding this comment.
HuaweiAhCh100ScaleHandler is renamed to HuaweiAhCh100Handler and is now a concrete class.
| * | ||
| * @see HuaweiAhCh100ProtocolTest | ||
| */ | ||
| object HuaweiAhCh100Protocol { |
There was a problem hiding this comment.
Please remove this protocol class from the libs and integrate them into the HuaweiAhCh100Handler
There was a problem hiding this comment.
HuaweiAhCh100Protocol is removed from libs/; its constants and wire-protocol functions are now an internal companion object inside HuaweiAhCh100Handler. It was in libs/ so it could be JVM-unit-tested without a real scale; HuaweiAhCh100ProtocolTest is retargeted as HuaweiAhCh100HandlerTest against the companion object, so the #1206/#1280 regression stays locked.
| * `HuaweiAhCh100ProtocolTest` lock the protocol so the regression cannot | ||
| * recur silently. | ||
| */ | ||
| class HuaweiCH100Handler : HuaweiAhCh100ScaleHandler() { |
There was a problem hiding this comment.
Should be better handled in HuaweiAhCh100Handler
There was a problem hiding this comment.
HuaweiCH100Handler is removed; HuaweiAhCh100Handler handles the CH100 advertisement name.
Addresses review feedback on oliexdev#1374: merge HuaweiAH100Handler, HuaweiCH100Handler, HuaweiAhCh100ScaleHandler and the libs HuaweiAhCh100Protocol object into one HuaweiAhCh100Handler class. The protocol primitives are now an internal companion object; HuaweiAhCh100ProtocolTest is retargeted as HuaweiAhCh100HandlerTest.
Fix Huawei AH100 / CH100 measurement parsing
Closes #1206 and #1280. Supersedes #1276.
Summary
0xFAA0).HuaweiAH100Handler(advertAH100) andHuaweiCH100Handler(advertCH100). Until nowHuaweiAH100HandlermatchedCH100only, so real AH100 hardware was never picked up.The bug
HuaweiAH100Handlerin 3.x stopped AES-CTR-decrypting the encrypted measurement frame, used invented field offsets, and computed weight as(1457 - byte) / 10. Reports from #1206 and #1280:First fixture from the new
HuaweiAhCh100ProtocolTest(matches the capture in #1280):Reconstructed from the v2.5.4 source and verified against captures from #1280.
0xFAA0with TX0xFAA1(write) and RX0xFAA2(notify).magicKeyderived from the auth token.op=0x0Ethenop=0x8E). AES-CTR-decrypt the deobfuscated payload of the first half only; the second half carries derived BIA fields that v2.5.4 did not decode.[user, weightLE, fatLE, yearLE, month, day, hour, min, sec, dow, impedanceLE].0xDBheader:lengthByte = payload.size + 1. Encrypted0xDCheader:lengthByte = payload.size. The 3.x rewrite had the0xDBlength off by one.USER_INFOpayload (CMD0x09) is the v2.5.4 14-byte layout:auth(7) || age | sexBit | height | 0x00 | weightLE | resistanceLE | 0x1C 0xE2.Changes
core/bluetooth/libs/HuaweiAhCh100Protocol.kt- pure-Kotlin object holdingobfuscate,aesCtr,xorChecksum,buildAuthToken,deriveMagicKey,buildPlainCommand,buildEncryptedCommand, andparseMeasurement.INITIAL_KEY/INITIAL_IVare exposed via getters returning defensive copies.HuaweiAhCh100ScaleHandlerwith the BLE state machine.HuaweiAH100Handlerrewritten as a thin subclass matching advertAH100.HuaweiCH100Handlermatching advertCH100. Both products use the same Chipsea CST34M97 hardware (confirmed in Fixed Huawei AH100/CH100 measurement parsing #1276); splitting just lets AH100 hardware be detected and leaves room for firmware-specific divergence later.HUAWEI/HONORprefixes so OEM rebrands still resolve to the right handler.Notes
If the first half already contains at least 15 plaintext bytes (i.e. the scale shipped the full record in one notification under the negotiated 185-byte MTU), the handler decodes immediately and ignores any later
0x8Ehalf. This means the default Balanced tuning works as well as the legacy 23-byte ATT MTU; no tuning profile needs to be pinned.