S400 : implement dual impedance measurements#1367
Conversation
96feaee to
70dc847
Compare
70dc847 to
4f4c9a6
Compare
|
Tested and works with my S400 ! Would love a review :-) |
|
Can you try the 4 possible combinations for BMR x Bone formula? Curious to see how they stack up to the numbers in Xiaomi |
This was kinda expected given the description of the PR:
CUNNINGHAM_1980 runs a bit higher than 1991 so the BMR measurements run with 1980 are a bit higher. So it looks like Xiaomi (based on the smallest delta in Openscale):
|
|
Curious to see the big differences in muscle %, fat % and visceral fat. For each of these variables, which number is closer to reality you reckon? And do you have Openscale numbers without this PR for comparison? |
|
I can only answer for the % fat, the reality is around 15%. So yes this PR could be considered an improvement for the body fat %. For the rest: I don't know. I don't know if the Xiaomi app is right or wrong. I don't even know if the Xiaomi app uses the 2 impedance values. I'd assume yes but you never know. As far as I can tell, only the % fat was meaningfully affected through this MR. Hence my initial question: which of the metrics are supposed to be affected by this PR? I suppose just the body fat? For the other metrics, there are differences between what Xiaomi and Openscale report. But I don't know which one is right, apart from the body fat %. |
| COMMENT(27, R.string.measurement_type_comment, listOf(UnitType.NONE), listOf(InputFieldType.TEXT)), | ||
| USER(28, R.string.measurement_type_user, listOf(UnitType.NONE), listOf(InputFieldType.USER)), | ||
| IMPEDANCE(29, R.string.measurement_type_impedance, listOf(UnitType.OHM), listOf(InputFieldType.FLOAT)), | ||
| IMPEDANCE_LOW(30, R.string.measurement_type_impedance_low, listOf(UnitType.OHM), listOf(InputFieldType.FLOAT)), |
There was a problem hiding this comment.
Could you please remove IMPEDANCE and IMPEDANCE_LOW as a separate measurement, normal user doesn't need that.
There was a problem hiding this comment.
I kept the DB rows and raw sync writes because the two impedance bands — 50 kHz and 250 kHz — are the only directly measured body-comp inputs. Everything else, including TBW, ECW, ICW, FFM, BF, SMM, BCM, and BMR via FFM, is derived from those bands plus user anthropometry.
I updated the PR to mark them as "internal" and not show that to user. I think it's relevant to keep raw impedance values in case we need reprocessing as method to derive the metric improve later
Feel free to tell me if you just want me to drop them instead.
|
Recomputed your values from impedance and analysed Expected differences (vendor convention vs literature) :
More surprising
As a reminder: metrics affected by this PR New or recomputed, driven by added dual-freq low-band impedance + literature pipeline:
|
|
Why is visceral fat not using impedance? Using just weight, height and age will result in lean & muscular body builders having just as high visceral fat as a high body fat % person with identical height, weight and age. I am currently still using the Mi body composition scale 2 with openscale, and AFAIK I have seen different visceral fat measurements at the same weight. And since I didn't change in height and age, it sounds like it is taking something else into account. |




The Xiaomi Body Composition Scale S400 is a dual-frequency BIA device. It broadcasts two BLE advertisements per weighing: a high-frequency packet (weight + heart rate + high-band impedance) and a low-frequency packet (low-band impedance). The pre-existing handler consumed only the first packet to arrive and discarded the second, then fed whichever impedance value happened to be in that packet into
MiScaleLib— the single-frequency formula port shared with the Mi Body Composition Scale 2 / Eufy C20.This produced two real problems:
What changed
Packet pipeline
S400Decryptornow disambiguates the impedance band per packet (weight != 0⇒ high-band,weight == 0⇒ low-band) and returns one of two complementary payloads.S400Aggregator(new) buffers per-MAC sessions until both packets land, with a 10 s timeout fallback so older firmware that only emits the high-band packet still finalizes, and a short dedup window so the scale's end-of-weighing re-broadcasts don't double-publish.MiScaleS400Handlerno longer returnsCONSUMED_STOPafter the first packet; it returnsCONSUMED_KEEP_SCANNINGuntil the aggregator finalizes.Body composition
S400BodyComposition(new) replacesMiScaleLibfor the S400 path. It implements a literature-grounded pipeline: Sun 2003 (TBW), De Lorenzo 1997 / Matthie 2005 Hanai mixture (ECW), Pace & Rathbun 1945 (FFM hydration constant), Janssen 2000 (SMM), Cunningham 1991/1980 (BMR), Bracco 1996 (foot-to-foot correction). Full reference list lives in the KDoc at the top of the file.S400BodyCompositionis the spec — there is no external design document.MiScaleLibis unchanged and still serves the Mi Scale 2 and Eufy C20 handlers.Per-device settings
MiScaleS400Handler.DeviceConfigurationUi()now exposes two radio pickers below the existing bind-key field:MI_LEGACY(impedance-based regression, matches the output range users see in the scale's companion app) vsHEYMSFIELD(anthropometric, clinically defensible, no impedance input).CUNNINGHAM_1991(literature default) vsCUNNINGHAM_1980(reproduces the BMR range users see in the scale's companion app; runs ~5–8 % higher than 1991).Defaults are
MI_LEGACY+CUNNINGHAM_1991. Persistence rides the existingDriverSettings.putStringchannel — noSettingsFacadechanges. Storage keys:s400_bone_formula,s400_bmr_formula. Enum names are preserved across this change so existing user preferences keep working.Schema
MeasurementTypeKeyentries:IMPEDANCE,IMPEDANCE_LOW,ECW,ICW,PROTEIN,BCM. NewUnitType.OHM.ScaleMeasurement.impedanceLowfield added; the existingimpedancefield is reused as the high-frequency band.MIGRATION_14_15seeds the six newMeasurementTyperows withisEnabled = falseso existing users don't see new charts appear after the update — they can opt in from the measurement-type settings screen. Six new translatable strings added tovalues/strings.xml.Tests
S400AggregatorTestcovers: in-order packets, out-of-order packets, timeout finalization with low-band still missing, dedup of a re-broadcast finalization, multi-device interleaving.S400BodyCompositionTestcovers: three reference subjects, label-swap edge case, unreliable-contact edge case, both bone options × both BMR options, validation rejects, foot-to-foot directional check. 15 tests.S400DecryptorTestextended with a high-band and a low-band reference payload to lock in the disambiguation rule.Edit: got confused with my previous Mi Scale 2 in PR name and commit (hence branch name) :-/