Add BLE BPL scale native module and BMI integration
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / build-library (push) Has been cancelled
CI / build-android (push) Has been cancelled
CI / build-ios (push) Has been cancelled

This commit is contained in:
2025-12-17 11:26:41 +05:30
parent a7a9b2646b
commit d96af7e4fa
24 changed files with 17129 additions and 64 deletions

View File

@@ -1,2 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

View File

@@ -0,0 +1,105 @@
package com.bplscale
/**
* BodyFat calculation wrapper
* Loads the native libbodyfat.so library
*/
object BodyFat {
init {
System.loadLibrary("bodyfat") // libbodyfat.so
}
external fun getVersion(): String
external fun getAuthToken(): String
/**
* Calculate body fat percentage
* @param sex Boolean - true for female, false for male
* @param age Float - user age
* @param cm Float - height in cm
* @param wtkg Float - weight in kg
* @param imp Float - body impedance (Ohm)
* @param althlevel Int - athletic level (keep 0)
* @return Float - body fat percentage
*/
external fun getFat(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate total body water (TBW)
*/
external fun getTbw(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate muscle mass percentage
*/
external fun getMus(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate bone mass percentage
*/
external fun getBone(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate basal metabolic rate (BMR) in Kcal
*/
external fun getKcal(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate visceral fat
*/
external fun getVfat(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate body age
*/
external fun getBage(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate best weight
*/
external fun getBestWeight(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate BMI (Body Mass Index)
*/
external fun getBMI(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate protein percentage
*/
external fun getProtein(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate weight without fat (lean body mass)
*/
external fun getWeightWithoutFat(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate obesity rate
* obeserate = (actual weight - best weight) / best weight
*
* Evaluation:
* < 10.0 - Healthy
* < 20.0 - Over weight
* < 30.0 - Mildly obese
* < 50.0 - Moderate obesity
* > 50 - Severe obesity
*/
external fun getObeseRate(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate body score (0-100)
*/
external fun getScore(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
/**
* Calculate body shape type
*
* < 0.5 - Lean type
* < 1.5 - Normal type
* < 2.5 - Over weight type
* < 3.5 - Over fat
* > 3.5 - Obese
*/
external fun getBodyShape(sex: Boolean, age: Float, cm: Float, wtkg: Float, imp: Float, althlevel: Int): Float
}

View File

@@ -1,23 +1,136 @@
package com.bplscale
import com.facebook.react.bridge.ReactApplicationContext
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.facebook.react.bridge.*
import com.facebook.react.module.annotations.ReactModule
@ReactModule(name = BplScaleModule.NAME)
class BplScaleModule(reactContext: ReactApplicationContext) :
NativeBplScaleSpec(reactContext) {
class BplScaleModule(
reactContext: ReactApplicationContext
) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String {
return NAME
}
private val bluetoothAdapter: BluetoothAdapter? by lazy {
val manager =
reactContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
manager.adapter
}
// Example method
// See https://reactnative.dev/docs/native-modules-android
override fun multiply(a: Double, b: Double): Double {
return a * b
}
private var scanner: BluetoothLeScanner? = null
private var scanCallback: ScanCallback? = null
companion object {
const val NAME = "BplScale"
}
override fun getName(): String = NAME
data class ScaleData(val weight: Float, val impedance: Float)
/**
* Scan BLE devices and return weight and impedance
*/
@ReactMethod
fun scanDevices(scanDurationMs: Int, promise: Promise) {
val adapter = bluetoothAdapter
if (adapter == null || !adapter.isEnabled) {
promise.reject("BLE_DISABLED", "Bluetooth is disabled")
return
}
scanner = adapter.bluetoothLeScanner
if (scanner == null) {
promise.reject("BLE_ERROR", "BLE scanner unavailable")
return
}
val devicesMap = HashMap<String, WritableMap>()
val resultArray = Arguments.createArray()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
processDevice(result, devicesMap)
}
override fun onBatchScanResults(results: MutableList<ScanResult>) {
results.forEach { processDevice(it, devicesMap) }
}
override fun onScanFailed(errorCode: Int) {
promise.reject("BLE_SCAN_FAILED", "Scan failed with code $errorCode")
}
private fun processDevice(result: ScanResult, map: HashMap<String, WritableMap>) {
val device = result.device ?: return
val address = device.address ?: return
val scanRecord = result.scanRecord?.bytes ?: return
val scaleData = parseScaleData(scanRecord) ?: return
if (!map.containsKey(address)) {
val deviceMap = Arguments.createMap()
deviceMap.putString("name", device.name ?: "")
deviceMap.putString("address", address)
deviceMap.putDouble("weight", scaleData.weight.toDouble())
deviceMap.putDouble("impedance", scaleData.impedance.toDouble())
map[address] = deviceMap
}
}
}
scanner?.startScan(null, settings, scanCallback)
Handler(Looper.getMainLooper()).postDelayed({
scanner?.stopScan(scanCallback)
devicesMap.values.forEach { resultArray.pushMap(it) }
promise.resolve(resultArray)
}, scanDurationMs.toLong())
}
// ────── Parse weight and impedance from advertising packet ──────
private fun parseScaleData(scanRecord: ByteArray): ScaleData? {
var i = 0
while (i < scanRecord.size - 2) {
val len = scanRecord[i].toInt() and 0xFF
if (len == 0) break
val type = scanRecord[i + 1].toInt() and 0xFF
if (type == 0xFF && i + len <= scanRecord.size) {
val mfg = scanRecord.copyOfRange(i + 2, i + 1 + len)
if (mfg.size >= 15) {
val flag = mfg[0].toInt() and 0xFF
return when (flag) {
0xC0 -> parseV30Protocol(mfg)
0xCA -> parseV21Protocol(mfg)
else -> null
}
}
}
i += len
}
return null
}
private fun parseV30Protocol(data: ByteArray): ScaleData {
val weight = ((data[2].toInt() and 0xFF) shl 8 or (data[3].toInt() and 0xFF)) / 10f
val impedance = ((data[4].toInt() and 0xFF) shl 8 or (data[5].toInt() and 0xFF)) / 10f
return ScaleData(weight, impedance)
}
private fun parseV21Protocol(data: ByteArray): ScaleData {
val weight = ((data[10].toInt() and 0xFF) shl 8 or (data[11].toInt() and 0xFF)) / 10f
val impedance = ((data[12].toInt() and 0xFF) shl 8 or (data[13].toInt() and 0xFF)) / 10f
return ScaleData(weight, impedance)
}
companion object {
const val NAME = "BplScale"
}
}

View File

@@ -1,33 +1,19 @@
package com.bplscale
import com.facebook.react.BaseReactPackage
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import java.util.HashMap
import com.facebook.react.uimanager.ViewManager
class BplScalePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == BplScaleModule.NAME) {
BplScaleModule(reactContext)
} else {
null
}
}
class BplScalePackage : ReactPackage {
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
return ReactModuleInfoProvider {
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
moduleInfos[BplScaleModule.NAME] = ReactModuleInfo(
BplScaleModule.NAME,
BplScaleModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCxxModule
true // isTurboModule
)
moduleInfos
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
// Return a list with your BplScaleModule
return listOf(BplScaleModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
// No custom view managers for this module
return emptyList()
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.