-
-
Notifications
You must be signed in to change notification settings - Fork 349
How to support a new scale
Adding support for a new Bluetooth scale in openScale is a two-part process: first, you need to reverse-engineer the scale's communication protocol, and then implement a new handler within the app to manage this protocol. This guide will walk you through creating a new ScaleDeviceHandler in openScale's modern Kotlin-based architecture.
Note
Prerequisites: A foundational understanding of Android development with Kotlin and basics of Bluetooth Low Energy (GATT).
Before writing code, you must understand how the scale communicates. The key is to capture the Bluetooth traffic between the scale and its official manufacturer's app.
-
Discover the Bluetooth services and characteristic
- Enable File Logging in the openScale app, go to
Settings > Generaland enable theFile loggingoption. - Add the Scale as a Debug Device, go to
Settings > Bluetoothand find your scale in the list. Tap the three-dot menu to its right and selectSave as debug device. This assigns the genericDebug GATThandler to it. - Capture the Bluetooth services and characteristic, return to the openScale
Overviewscreen and tap the Bluetooth icon in the top bar to initiate a connection to your debug device. - Export and Analyze the Log, go back to
Settings > General, tap theExport log filebutton. This will create a shareable.txtfile. - Open this log file. It will contain a list of all discovered services and characteristics
- Enable File Logging in the openScale app, go to
-
Enable Bluetooth HCI Snoop Log
- In Android Developer Options, enable Bluetooth HCI snoop log.
- Toggle Bluetooth off and on to start logging.
-
Capture Traffic
- Use the manufacturer's app to weigh yourself multiple times.
- Record the exact weight and any other metrics (e.g., body fat, water percentage) along with timestamps.
- Repeat for several various measurements to capture varied data along with the user information (age, activity level, height and so on)
-
Extract the Log File
- Disable the HCI snoop log.
- The
btsnoop_hci.logfile is usually located in/data/misc/bluetooth/logs. - Use
adb bugreportto extract it as a zip file.
-
Analyze the Log
- Open the
.logfile with Wireshark. - Look for relevant Bluetooth packets:
- Search for recorded weight values (often hexadecimal, sometimes scaled by 10 or 100, e.g., 75.5 kg → 7550).
- Focus on Write and Indication/Notification packets.
- Open the
Questions to answer:
- What are the key Service and Characteristic UUIDs? You'll need them for reading, writing, and subscribing to notifications.
- What commands does the app send to the scale? Look for
Writerequests. What do their byte payloads mean (start measurement, sync user profile) - In what format does the scale send its data? Look at the
Handle Value NotificationorIndicationpackets. Analyze the byte payload structure to decode weight, impedance, and other values
Once you decode the protocol, you can implement the handler. ScaleDeviceHandler encapsulates all device-specific logic.
In app/src/main/java/com/health/openscale/core/bluetooth/scales/, create a Kotlin file, e.g., MyNewScaleHandler.kt.
package com.health.openscale.core.bluetooth.scales
import com.health.openscale.core.bluetooth.data.ScaleMeasurement
import com.health.openscale.core.bluetooth.data.ScaleUser
import com.health.openscale.core.service.ScannedDeviceInfo
import java.util.UUID
class MyNewScaleHandler : ScaleDeviceHandler() {
// Implementation will go here
}The first and most crucial method is supportFor. This is called for every scanned Bluetooth device to check if your handler can support it. The decision is usually based on the device's advertised name or the service UUIDs it broadcasts.
override fun supportFor(device: ScannedDeviceInfo): DeviceSupport? {
// Check based on the advertised name or service UUIDs.
// Using uppercase is a robust way to handle device name variations.
val name = device.name.uppercase()
if (name.startsWith("MY_SCALE_BT_NAME")) {
// This handler supports the device. Return a description.
return DeviceSupport(
displayName = "My Awesome Scale",
capabilities = setOf(
DeviceCapability.LIVE_WEIGHT_STREAM,
DeviceCapability.BODY_COMPOSITION,
DeviceCapability.HISTORY_READ
),
implemented = setOf( // Be honest about what you have implemented so far.
DeviceCapability.LIVE_WEIGHT_STREAM
),
linkMode = LinkMode.CONNECT_GATT // Or BROADCAST_ONLY, CLASSIC_SPP
)
}
// This handler does not support the device.
return null
}Notes:
-
displayName: The user-friendly name shown in the app's device list. -
capabilities: A Set of all features the scale hardware theoretically supports. -
implemented: A subset of capabilities that your handler actually implements. This helps the UI show what's working. -
linkMode: Defines the communication type. Most modern scales useCONNECT_GATT.
This method is called after a GATT connection is successfully established. Use it to send any initialization commands you identified in Part 1, such as setting the time, syncing the user, or—most commonly—enabling notifications on the characteristic that will deliver measurement data.
override fun onConnected(user: ScaleUser) {
logI("Starting connection sequence for MyNewScale.")
// Example: Enable notifications on the measurement characteristic.
// Replace these UUIDs with the ones you found during your analysis.
val serviceUUID = uuid16(0x181B) // e.g., Body Composition Service
val measurementCharUUID = uuid16(0x2A9C) // e.g., Weight Measurement
setNotifyOn(serviceUUID, measurementCharUUID)
// Example: Send a command to the scale to prepare it for a measurement.
// This is highly device-specific.
val startCommand = byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())
val commandCharUUID = uuid16(0x1542) // A custom command characteristic
writeTo(serviceUUID, commandCharUUID, startCommand)
// Inform the user about the next step.
userInfo(R.string.bt_info_waiting_for_measurement)
}When the scale sends data (typically via GATT notifications), this method is invoked. Your job is to parse the data byte array according to the protocol you reverse-engineered and convert it into a ScaleMeasurement.
override fun onNotification(characteristic: UUID, data: ByteArray, user: ScaleUser) {
val measurementCharUUID = uuid16(0x2A9C) // Same UUID as in onConnected
if (characteristic == measurementCharUUID && data.isNotEmpty()) {
logD("Measurement data received: ${data.toHexPreview(24)}")
// -- This parsing logic is completely specific to your scale --
// Assumption: Weight is a 16-bit little-endian value at byte 2, scaled by 100.
// Consult the openScale source (e.g., RenphoES26BBHandler) for parsing examples.
val weightRaw = (data[3].toInt() and 0xFF shl 8) or (data[2].toInt() and 0xFF)
val weight = weightRaw / 100.0f
// Create a ScaleMeasurement object.
val measurement = ScaleMeasurement()
measurement.weight = weight
// The scale might send multiple "live" weight updates.
// You need to identify when the measurement is final or "stable".
// This flag is often a bit in the first byte of the payload.
val isStable = (data[0].toInt() and 0x01) == 1
if (isStable) {
logI("Stable measurement received, publishing to app.")
publish(measurement)
// Optional: Disconnect after a successful measurement to save battery.
requestDisconnect()
}
}
}To make openScale aware of your new handler, you must add it to the list of known handlers in ScaleFactory.kt.
Open app/src/main/java/com/health/openscale/core/bluetooth/ScaleFactory.kt and add an instance of your new handler to the modernKotlinHandlers list. It's good practice to add it near the top.
// In ScaleFactory.kt
private val modernKotlinHandlers: List<ScaleDeviceHandler> = listOf(
MyNewScaleHandler(), // Add your handler here
RenphoES26BBHandler(),
YunmaiHandler(isMini = false),
// ... other handlers
)Compile and run the app on your Android device.
- Go to
Settings > Bluetoothwithin openScale. - Tap the search button to scan for devices.
- If your
supportForimplementation is correct, your scale should appear in the list with thedisplayNameyou specified. - Select it, attempt to connect, and step on the scale.
- Use Logcat in Android Studio to monitor the log messages (filtered by your handler's TAG) or log using the in app logging under
Settings->General->File Logging. This is essential for debugging your connection and parsing logic.
Once your handler is working reliably, sharing it with the openScale community is a fantastic way to give back.
- Fork the Repository: Create your own fork of the official openScale repository on GitHub.
-
Create a Branch: In your fork, create a new branch for your changes (e.g.,
feat/add-my-awesome-scale-handler). -
Commit Your Changes: Commit the new
MyNewScaleHandler.ktfile and the modifications toScaleFactory.kt. Write a clear commit message, likefeat: Add support for My Awesome Scale. -
Open a Pull Request (PR): Push your branch to your fork and open a new Pull Request against the
masterbranch of theoliexdev/openScalerepository. -
Describe Your PR: In the Pull Request description, provide the following information:
- The exact model name of the scale you added.
- A brief summary of what features are working (e.g., "Live weight and historical data import work. Body composition is not yet implemented.").
- If possible, attach the
btsnoop_hci.logfile you captured. This is incredibly valuable for the maintainers to verify the protocol and assist with debugging.
By contributing your handler, you help make openScale better for everyone.
Adding a new scale handler is a methodical process. The reverse-engineering phase is often the most challenging. Once you understand the protocol, the ScaleDeviceHandler architecture in openScale provides a clean and robust framework for sending commands, receiving data, and integrating the device's functionality into the rest of the application.