Add Keep S3 scale support - #1438
Conversation
Wiki follow-up after mergePlease add Keep S3 to the supported scales wiki after this PR is merged: <tr>
<td>Keep S3</td>
<td align="center">✓</td>
<td align="center">not verified</td>
<td align="center">✓</td>
<td>See <a href="https://github.com/oliexdev/openScale/pull/1438">#1438</a>. Weight, impedance, and heart rate are received from the scale; additional body metrics are calculated locally.</td>
</tr>History retrieval is marked not verified because the available captures do not establish whether the scale supports stored-history download; using A matching wiki commit has been prepared locally, but GitHub wiki repositories cannot be contributed to through the normal fork PR workflow and this account does not have write access to the upstream wiki. |
This change removes support for several recently added measurement types and reverts the database schema to version 15.
Key changes include:
* **Measurement Types:** Removed `PHASE_ANGLE`, `SKELETAL_MUSCLE`, `LEAN_SOFT_TISSUE`, `SUBCUTANEOUS_FAT`, `BODY_AGE`, `BMI_22_REFERENCE_WEIGHT`, and `DEVICE_IMPEDANCE` from `MeasurementTypeKey` and `ScaleMeasurement`.
* **Database:** Reverted `AppDatabase` version to 15, deleted `MIGRATION_15_16`, and removed the version 16 JSON schema.
* **Units & Strings:** Removed `UnitType.DEGREE` and associated string resources and localization logic.
* **Logic Cleanup:**
* Removed generalized percentage-to-mass unit conversion logic in `ConverterUtils` and `MeasurementTypeCrudUseCases`.
* Updated `KeepS3Handler` to stop publishing extended metrics.
* Removed `awaitPendingOperations` from the BLE transport interface.
|
Thanks a lot for this — the reverse engineering is impressive, and the test coverage on both the protocol decoder and the body-composition model is well above what I usually get for a new scale. I want this in. My only problem was scope. The PR was titled "Add Keep S3 scale support", but it also carried a new measurement-type system, a Where it stands nowThe PR is down to 5 files and one touched existing file:
What I removed, and whyThe 8 new measurement types. These are too specialised for openScale. They'd appear in the measurement-type list for every user of every supported scale, and I'd be committing to maintaining them — units, conversions, translations, insights, sync — for a single device. Two of them I'd have pushed back on regardless: In the handler I commented the assignments out rather than deleting them, together with a note on what re-enabling would require. Your decoding work is preserved and readable; only the persistence is gone. What still gets stored: weight, body fat, muscle, water, visceral fat, bone, LBM, BMR, protein, heart rate, impedance (100 kHz) and impedance low (50 kHz). That's a richer set than most scales in the tree deliver. The two changes that need hardware testing1. The transport barrier is gone. finishJob = scope.launch {
delay(DISCONNECT_DELAY_MS) // 800 ms
requestDisconnect()
}The 2. private fun previousDeviceImpedance(previous: ScaleMeasurement): Double {
if (previous.impedance.isFinite() && previous.impedance > 0.0) {
return previous.impedance
}
return 0.0
}So the profile payload now carries the 100 kHz impedance where it used to carry the vendor value. Your own comment says a Keep S3 accepted an all-zero previous record during validation, so I expect this to be harmless — but it's an unverified assumption on my side. Please check that the second and third measurements still produce correct body composition, since that's where the previous record actually gets sent. What I'd like from you
If that checks out, I'll merge it and add the wiki entry. Thanks again for the quality of the work here — the trimming is about openScale's scope, not about your implementation. |
|
Thanks for trimming the PR and for calling out the two runtime changes. I reviewed them against the captures and ran the requested consecutive hardware tests. Previous impedanceThe 100 kHz fallback was not protocol-correct. The captured second official-app session puts the previous Commit Queue drain and disconnectThe 800 ms delay was not sufficient on this device. In three initial validation sessions the scale emitted 406 The largest observed backlog at Two consecutive follow-up measurements then produced:
Muscle mapping and body-composition comparisonThe vendor's broad The other locally calculated values are SDK-compatible estimates, not exact Keep App report values. The nearby comparison was close for several fields but not identical, and the results were sensitive to measured impedance/contact, so I would keep the existing estimate wording rather than claim exact vendor-report equivalence. Automated validation
Raw logs, body measurements, device addresses, and generated user tokens remain private and are not included in the repository or this comment. |
Summary
KeepS3Handlerselected by the exactKeep_S3device name before generic LeFu/FFF0 handlersProtocol and field mapping
The handler uses service
0x00FF, notify characteristic0xFF01, and write-with-response characteristic0xFF02. Initialization follows38 → 0A → 01 → 05 → E7 → F5 → F5 → 03 → 20 → 32 → 36and advances only after the expected successful response.Every valid
0x57and0x58event is acknowledged. Stage0x29of0x57publishes at most once;0x58is the final-record fallback and ends the session. The handler then queues two stop commands and delays disconnection long enough for the serialized BLE operation queue to drain.The protocol impedance from
0x57is distinct from the 50/100 kHz values decoded from0x58. It is retained per user inDriverSettingsand reused for the next0x32profile only when its timestamp and weight match the last stored measurement. A missing or stale value produces an isolated all-zero previous record instead of substituting a different impedance band.The 100/50 kHz values map to openScale's existing high/low impedance fields. The offline composition model supplies body fat, water, skeletal-muscle percentage, visceral-fat level, bone mass, lean body mass, BMR, and protein percentage. These are local SDK-compatible estimates, not values directly measured by the scale or an official public formula. The vendor's broader muscle percentage is intentionally not published because openScale's existing
MUSCLEfield represents skeletal-muscle percentage.Validation
./gradlew :app:testDebugUnitTest :app:assembleDebug— passed (367 tests across 52 suites, no failures, errors, or skips)git diff --check— passedlintDebugreached the repository's existing baseline; no report entries referenced the Keep S3 implementationTwo additional hardware sessions validated the queued shutdown path without exposing measurement data:
0x57events were acknowledged successfully0x58events were acknowledged successfully0x36writes succeeded: one start and two stops per sessionEarlier hardware validation also confirmed that a fresh user with an all-zero previous record was accepted and that the generated ID24 remained stable across connections.
Known limitations
0x57stages remain sequence-based inferencesF5and20is unknown0x58is not publicly specifiedRaw Bluetooth captures, device identifiers, generated user tokens, health measurements, screenshots, and validation logs are intentionally excluded.