Skip to content

How to support a new scale

OliE edited this page Dec 8, 2025 · 1 revision

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).


Part 1: Analyzing the Bluetooth Protocol

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.

  1. Discover the Bluetooth services and characteristic

    • Enable File Logging in the openScale app, go to Settings > General and enable the File logging option.
    • Add the Scale as a Debug Device, go to Settings > Bluetooth and find your scale in the list. Tap the three-dot menu to its right and select Save as debug device. This assigns the generic Debug GATT handler to it.
    • Capture the Bluetooth services and characteristic, return to the openScale Overview screen 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 the Export log file button. This will create a shareable .txt file.
    • Open this log file. It will contain a list of all discovered services and characteristics
  2. Enable Bluetooth HCI Snoop Log

    • In Android Developer Options, enable Bluetooth HCI snoop log.
    • Toggle Bluetooth off and on to start logging.
  3. 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)
  4. Extract the Log File

    • Disable the HCI snoop log.
    • The btsnoop_hci.log file is usually located in /data/misc/bluetooth/logs.
    • Use adb bugreport to extract it as a zip file.
  5. Analyze the Log

    • Open the .log file 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.

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 Write requests. 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 Notification or Indication packets. Analyze the byte payload structure to decode weight, impedance, and other values

Part 2: Implementing the ScaleDeviceHandler

Once you decode the protocol, you can implement the handler. ScaleDeviceHandler encapsulates all device-specific logic.

1. Create a New Handler File

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
}

2. Implement Device Discovery (supportFor)

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 use CONNECT_GATT.

3. Implement Connection Logic (onConnected)

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)
}

4. Implement Data Parsing (onNotification)

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()
        }
    }
}

5. Register the Handler

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
)

6. Compile and Test

Compile and run the app on your Android device.

  1. Go to Settings > Bluetooth within openScale.
  2. Tap the search button to scan for devices.
  3. If your supportFor implementation is correct, your scale should appear in the list with the displayName you specified.
  4. Select it, attempt to connect, and step on the scale.
  5. 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.

7. Contributing Your Handler

Once your handler is working reliably, sharing it with the openScale community is a fantastic way to give back.

  1. Fork the Repository: Create your own fork of the official openScale repository on GitHub.
  2. Create a Branch: In your fork, create a new branch for your changes (e.g., feat/add-my-awesome-scale-handler).
  3. Commit Your Changes: Commit the new MyNewScaleHandler.kt file and the modifications to ScaleFactory.kt. Write a clear commit message, like feat: Add support for My Awesome Scale.
  4. Open a Pull Request (PR): Push your branch to your fork and open a new Pull Request against the master branch of the oliexdev/openScale repository.
  5. 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.log file 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.

Conclusion

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.

Clone this wiki locally