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

2
.gitignore vendored
View File

@@ -75,8 +75,6 @@ android/keystores/debug.keystore
# Turborepo # Turborepo
.turbo/ .turbo/
# generated by bob
lib/
# React Native Codegen # React Native Codegen
ios/generated ios/generated

View File

@@ -1,2 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <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> </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 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 import com.facebook.react.module.annotations.ReactModule
@ReactModule(name = BplScaleModule.NAME) @ReactModule(name = BplScaleModule.NAME)
class BplScaleModule(reactContext: ReactApplicationContext) : class BplScaleModule(
NativeBplScaleSpec(reactContext) { reactContext: ReactApplicationContext
) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String { private val bluetoothAdapter: BluetoothAdapter? by lazy {
return NAME val manager =
} reactContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
manager.adapter
}
// Example method private var scanner: BluetoothLeScanner? = null
// See https://reactnative.dev/docs/native-modules-android private var scanCallback: ScanCallback? = null
override fun multiply(a: Double, b: Double): Double {
return a * b
}
companion object { override fun getName(): String = NAME
const val NAME = "BplScale"
} 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 package com.bplscale
import com.facebook.react.BaseReactPackage import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.uimanager.ViewManager
import com.facebook.react.module.model.ReactModuleInfoProvider
import java.util.HashMap
class BplScalePackage : BaseReactPackage() { class BplScalePackage : ReactPackage {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == BplScaleModule.NAME) {
BplScaleModule(reactContext)
} else {
null
}
}
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return ReactModuleInfoProvider { // Return a list with your BplScaleModule
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap() return listOf(BplScaleModule(reactContext))
moduleInfos[BplScaleModule.NAME] = ReactModuleInfo( }
BplScaleModule.NAME,
BplScaleModule.NAME, override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
false, // canOverrideExistingModule // No custom view managers for this module
false, // needsEagerInit return emptyList()
false, // isCxxModule
true // isTurboModule
)
moduleInfos
} }
}
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } pluginManagement { includeBuild("../../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") } plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'bplscale.example' rootProject.name = 'bplscale.example'
include ':app' include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin') includeBuild('../../node_modules/@react-native/gradle-plugin')

8978
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,8 @@
"name": "react-native-bpl-scale", "name": "react-native-bpl-scale",
"version": "0.1.0", "version": "0.1.0",
"description": "React Native Bluetooth BPL weighing scale library", "description": "React Native Bluetooth BPL weighing scale library",
"main": "./lib/module/index.js", "main": "lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"exports": {
".": {
"source": "./src/index.tsx",
"types": "./lib/typescript/src/index.d.ts",
"default": "./lib/module/index.js"
},
"./package.json": "./package.json"
},
"files": [ "files": [
"src", "src",
"lib", "lib",
@@ -32,7 +24,7 @@
"!**/.*" "!**/.*"
], ],
"scripts": { "scripts": {
"example": "yarn workspace react-native-bpl-scale-example", "example": "npm --prefix example run android",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
"prepare": "bob build", "prepare": "bob build",
"typecheck": "tsc" "typecheck": "tsc"
@@ -46,7 +38,7 @@
"type": "git", "type": "git",
"url": "git+.git" "url": "git+.git"
}, },
"author": " <> ()", "author": "",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "/issues" "url": "/issues"
@@ -62,7 +54,6 @@
"react": "19.2.0", "react": "19.2.0",
"react-native": "0.83.0", "react-native": "0.83.0",
"react-native-builder-bob": "^0.40.13", "react-native-builder-bob": "^0.40.13",
"turbo": "^2.5.6",
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -0,0 +1,27 @@
apply plugin: "com.android.library"
apply plugin: "org.jetbrains.kotlin.android"
android {
namespace "com.bplscale"
compileSdk 34
defaultConfig {
minSdk 24
targetSdk 34
}
buildFeatures {
buildConfig true
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib"
}

View File

@@ -0,0 +1 @@
<manifest package="com.bplscale" />

View File

@@ -0,0 +1,19 @@
package com.bplscale
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.turbomodule.core.interfaces.TurboModule
@ReactModule(name = BPLScaleModule.NAME)
class BPLScaleModule(
reactContext: ReactApplicationContext
) : NativeBPLScaleSpec(reactContext), TurboModule {
override fun hello(name: String): String {
return "Hello $name from Kotlin TurboModule"
}
companion object {
const val NAME = "BPLScale"
}
}

View File

@@ -0,0 +1,30 @@
package com.bplscale
import com.facebook.react.bridge.*
import com.facebook.react.turbomodule.core.TurboReactPackage
class BPLScalePackage : TurboReactPackage() {
override fun getModule(
name: String,
context: ReactApplicationContext
): NativeModule? {
return if (name == BPLScaleModule.NAME) {
BPLScaleModule(context)
} else null
}
override fun getReactModuleInfoProvider() =
ReactModuleInfoProvider {
mapOf(
BPLScaleModule.NAME to ReactModuleInfo(
BPLScaleModule.NAME,
BPLScaleModule.NAME,
false,
false,
true,
false
)
)
}
}

7721
react-native-bpl-scale/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
{
"name": "react-native-bpl-scale",
"version": "1.0.0",
"main": "src/index.ts",
"codegenConfig": {
"name": "BPLScale",
"type": "modules",
"jsSrcsDir": "src",
"android": {
"javaPackageName": "com.bplscale"
}
},
"peerDependencies": {
"react-native": ">=0.73"
},
"devDependencies": {
"react-native": "^0.73.0",
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1,8 @@
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
hello(name: string): string;
}
export default TurboModuleRegistry.getEnforcing<Spec>('BPLScale');

View File

@@ -0,0 +1 @@
export { default } from './NativeBPLScale';

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "ESNext",
"lib": ["ES2019"],
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"types": ["react-native"],
"baseUrl": "."
},
"include": ["src"]
}

5
react-native.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
name: 'react-native-bpl-scale',
version: '0.1.0',
dependencies: {},
};

View File

@@ -1,7 +1,15 @@
import { TurboModuleRegistry, type TurboModule } from 'react-native'; import { NativeModules } from 'react-native';
export interface Spec extends TurboModule { export interface NativeBplScale {
multiply(a: number, b: number): number; startDiscovery(): void;
cancelDiscovery(): void;
getBondedDevices(): Promise<any[]>;
parseBluetoothScaleData(data: string): any;
// ✅ ADD THIS
scanDevices(scanDurationMs: number): Promise<any[]>;
} }
export default TurboModuleRegistry.getEnforcing<Spec>('BplScale'); const { BplScale } = NativeModules;
export default BplScale as NativeBplScale;

View File

@@ -1,5 +1,37 @@
import BplScale from './NativeBplScale'; import { NativeModules, Platform } from 'react-native';
export function multiply(a: number, b: number): number { export type BLEDevice = {
return BplScale.multiply(a, b); name: string;
} address: string;
weight: number;
impedance: number;
};
type BplScaleModuleType = {
scanDevices(scanDurationMs: number): Promise<BLEDevice[]>;
};
const { BplScale } = NativeModules as { BplScale: BplScaleModuleType };
/**
* Scan BLE devices and return weight and impedance.
* @param scanDurationMs Duration of scan in milliseconds
* @returns Promise resolving to a list of BLE devices with weight & impedance
*/
export const scanDevices = async (scanDurationMs: number): Promise<BLEDevice[]> => {
if (Platform.OS !== 'android') {
throw new Error('BplScale module is only supported on Android.');
}
try {
const devices = await BplScale.scanDevices(scanDurationMs);
return devices;
} catch (error) {
console.error('BLE Scan Error:', error);
throw error;
}
};
export default {
scanDevices,
};