Minimal iOS 17+ app for scanning Xiaomi Body Composition Scale S400 BLE advertisements, decoding measurements locally, storing them on device, and optionally exporting finalized results to Apple Health.
The packet format and scaling in this project come from:
export2garmin delegates S400 decoding to xiaomi-ble, so both repositories were used to reproduce the same MiBeacon V5 parsing and 0x6E16 S400 payload decoding logic in Swift.
The S400 payload is encrypted. As in the upstream reference implementation, this app needs:
- the scale BLE bind key
- the scale BLE MAC address
You can retrieve both from Xiaomi Home using the workflow documented in PacketDecoding.md.
Short version:
- Add the S400 to Xiaomi Home and complete at least one measurement
- Run Xiaomi Cloud Tokens Extractor with the same Xiaomi account and region
- Find your
Xiaomi Body Composition Scale S400device in the output - Copy its MAC address and BLE encryption key
- Put them into
.envasDEFAULT_SCALE_MAC_ADDRESSandDEFAULT_BIND_KEY_HEX
Different tools use different names for the same secret. In this repo:
BLE keybind keyBLE encryption key
all refer to the same 16-byte key used to decrypt the scale advertisements.
The default bind key and scale MAC are intentionally not hardcoded in Swift source anymore.
- Copy
.env.exampleto.env - Set your own values for:
DEFAULT_BIND_KEY_HEXDEFAULT_SCALE_MAC_ADDRESS
- Keep
.envlocal only. It is ignored by git and should never be committed.
Expected formats:
DEFAULT_BIND_KEY_HEX: 32 hexadecimal charactersDEFAULT_SCALE_MAC_ADDRESS:AA:BB:CC:DD:EE:FF
During each build, the app generates BuildSecrets.swift from .env in a pre-build step. If .env is missing or malformed, the build fails fast with a clear error.
Important operational notes:
- If you remove and re-add the scale in Xiaomi Home, Xiaomi may generate a new BLE key. Update
.envif decoding suddenly stops working. - When testing this app, fully quit Xiaomi Home first. The scale often connects to Xiaomi Home immediately, which prevents local apps from seeing the short encrypted broadcast window reliably.
Because that generated file lives in the source tree for Xcode compilation, reset it back to the placeholder version before committing or pushing:
SRCROOT="$PWD" ./scripts/reset_build_secrets_placeholder.shBefore publishing the repository, verify these files are not carrying your real values:
.envS400Scale/Generated/BuildSecrets.swift
S400Scale/BLE: CoreBluetooth scanner and session aggregationS400Scale/ScalePacketDecoder: MiBeacon parsing, AES-CCM, S400 field extractionS400Scale/MeasurementModel: measurement and settings modelsS400Scale/MeasurementStore: Core Data persistenceS400Scale/BodyCompositionCalculator: local body composition estimatesS400Scale/HealthKitExporter: Apple Health exportS400Scale/UI: SwiftUI screens
cp .env.example .env
xcodegen generate
xcodebuild -project S400Scale.xcodeproj -scheme S400Scale -destination 'platform=iOS Simulator,name=iPhone 16' build