Add BLE BPL scale native module and BMI integration
This commit is contained in:
@@ -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>
|
||||
|
||||
105
android/src/main/java/com/bplscale/BodyFat.kt
Normal file
105
android/src/main/java/com/bplscale/BodyFat.kt
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
android/src/main/jniLibs/arm64-v8a/libbodyfat.so
Normal file
BIN
android/src/main/jniLibs/arm64-v8a/libbodyfat.so
Normal file
Binary file not shown.
BIN
android/src/main/jniLibs/armeabi-v7a/libbodyfat.so
Normal file
BIN
android/src/main/jniLibs/armeabi-v7a/libbodyfat.so
Normal file
Binary file not shown.
BIN
android/src/main/jniLibs/x86/libbodyfat.so
Normal file
BIN
android/src/main/jniLibs/x86/libbodyfat.so
Normal file
Binary file not shown.
BIN
android/src/main/jniLibs/x86_64/libbodyfat.so
Normal file
BIN
android/src/main/jniLibs/x86_64/libbodyfat.so
Normal file
Binary file not shown.
Reference in New Issue
Block a user