From c14d41b3451dcd19309f7c6f6d3659384506547c Mon Sep 17 00:00:00 2001 From: Prathiyuman Date: Tue, 16 Dec 2025 16:39:05 +0530 Subject: [PATCH] refactor: update WeightScaleBridge integration and error handling --- .../com/weightscalebridge/MainApplication.kt | 6 - .../WeightScaleBridgeModule.kt | 332 +++++++++--------- .../WeightScaleBridgePackage.kt | 21 +- src/index.tsx | 27 +- 4 files changed, 202 insertions(+), 184 deletions(-) delete mode 100644 android/src/main/java/com/weightscalebridge/MainApplication.kt diff --git a/android/src/main/java/com/weightscalebridge/MainApplication.kt b/android/src/main/java/com/weightscalebridge/MainApplication.kt deleted file mode 100644 index f4f3a38..0000000 --- a/android/src/main/java/com/weightscalebridge/MainApplication.kt +++ /dev/null @@ -1,6 +0,0 @@ -// Inside the getPackages() method -override fun getPackages(): List { - val packages = PackageList(this).packages - packages.add(WeightScalePackage()) // Add this line - return packages -} \ No newline at end of file diff --git a/android/src/main/java/com/weightscalebridge/WeightScaleBridgeModule.kt b/android/src/main/java/com/weightscalebridge/WeightScaleBridgeModule.kt index 8fcb905..f3a6dc6 100644 --- a/android/src/main/java/com/weightscalebridge/WeightScaleBridgeModule.kt +++ b/android/src/main/java/com/weightscalebridge/WeightScaleBridgeModule.kt @@ -1,10 +1,11 @@ package com.weightscalebridge -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.module.annotations.ReactModule +import android.Manifest import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager -import android.bluetooth.le.* +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult import android.content.Context import android.content.pm.PackageManager import android.os.Build @@ -12,182 +13,181 @@ import android.os.CountDownTimer import androidx.core.content.ContextCompat import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule -import kotlin.math.abs +import kotlin.math.pow +import kotlin.math.round -class WeightScaleModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { +class WeightScaleModule( + private val reactContext: ReactApplicationContext +) : ReactContextBaseJavaModule(reactContext) { - override fun getName(): String = "WeightScale" + private var bluetoothAdapter: BluetoothAdapter? = null + private var scanner: BluetoothLeScanner? = null + private var isScanning = false + private var timer: CountDownTimer? = null + private var currentWeight = 0.0 - private var bluetoothAdapter: BluetoothAdapter? = null - private var scanner: BluetoothLeScanner? = null - private var isScanning = false - private var timer: CountDownTimer? = null + override fun getName(): String = "WeightScaleBridge" - private var candidateKg: Double = Double.NaN - private var stableSince: Long = 0L + init { + val manager = + reactContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + bluetoothAdapter = manager.adapter + scanner = bluetoothAdapter?.bluetoothLeScanner + } - private val STABLE_WINDOW_MS = 4000L - private val WEIGHT_TOL = 0.1 - private val MIN_VALID_KG = 5.0 - private val MAX_VALID_KG = 250.0 - private val MEASURE_WINDOW_MS = 20000L // 20 seconds timeout - private val TARGET_MAC = "34:5C:F3:40:2C:D8" // Your scale MAC + private fun sendEvent(name: String, params: WritableMap?) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(name, params) + } - private fun sendEvent(eventName: String, params: WritableMap) { - reactApplicationContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit(eventName, params) + @ReactMethod + fun isBluetoothEnabled(promise: Promise) { + try { + val enabled = bluetoothAdapter?.isEnabled ?: false + promise.resolve(enabled) + } catch (e: Exception) { + promise.reject("BLUETOOTH_CHECK_ERROR", e.message, e) + } + } + + @ReactMethod + fun startScanning(promise: Promise) { + try { + if (!hasPermissions()) { + promise.reject("PERMISSION", "Bluetooth permissions not granted") + return + } + + if (bluetoothAdapter?.isEnabled != true) { + promise.reject("BLUETOOTH", "Bluetooth disabled") + return + } + + if (isScanning) { + promise.resolve(true) + return + } + + isScanning = true + currentWeight = 0.0 + + scanner?.startScan(scanCallback) + + sendEvent( + "onWeightScaleStatus", + Arguments.createMap().apply { putString("status", "scanning") } + ) + + timer = object : CountDownTimer(20000, 1000) { + override fun onTick(ms: Long) {} + override fun onFinish() { + stopScanning(null) + } + }.start() + + promise.resolve(true) + } catch (e: Exception) { + promise.reject("SCAN_ERROR", e.message, e) + } + } + + @ReactMethod + fun stopScanning(promise: Promise?) { + try { + scanner?.stopScan(scanCallback) + } catch (_: Exception) {} + + timer?.cancel() + isScanning = false + + sendEvent( + "onWeightScaleStatus", + Arguments.createMap().apply { putString("status", "stopped") } + ) + + promise?.resolve(true) + } + + private val scanCallback = object : ScanCallback() { + override fun onScanResult(type: Int, result: ScanResult) { + val record = result.scanRecord ?: return + val data = record.manufacturerSpecificData + + if (data.size() == 0) return + + val raw = data.valueAt(0) + if (raw.size < 2) return + + val weight = (((raw[0].toInt() and 0xff) shl 8) or + (raw[1].toInt() and 0xff)) / 10.0 + + currentWeight = weight + + sendEvent( + "onWeightData", + Arguments.createMap().apply { + putDouble("weight", weight) + putBoolean("isStable", true) + } + ) + + sendEvent( + "onWeightScaleStatus", + Arguments.createMap().apply { + putString("status", "complete") + putString("message", "$weight kg") + } + ) + + stopScanning(null) } - @ReactMethod - fun startMeasuring() { - if (isScanning) return - - val bluetoothManager = reactApplicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - bluetoothAdapter = bluetoothManager.adapter - - if (bluetoothAdapter == null || !bluetoothAdapter!!.isEnabled) { - val params = Arguments.createMap().apply { - putString("message", "Bluetooth is not enabled. Please turn it on in settings.") - } - sendEvent("onScaleError", params) - return + override fun onScanFailed(errorCode: Int) { + super.onScanFailed(errorCode) + + sendEvent( + "onWeightScaleStatus", + Arguments.createMap().apply { + putString("status", "error") + putString("message", "Scan failed with code: $errorCode") } - - if (!hasRequiredPermissions()) { - val params = Arguments.createMap().apply { - putString("message", "Required permissions not granted.") - } - sendEvent("onScaleError", params) - return - } - - scanner = bluetoothAdapter!!.bluetoothLeScanner - - // Reset stability tracking - candidateKg = Double.NaN - stableSince = 0L - isScanning = true - - // Send initial status - sendEvent("onStatusChange", Arguments.createMap().apply { putString("status", "scanning") }) - - // Use filter for your specific scale MAC (more efficient) - val scanFilter = ScanFilter.Builder() - .setDeviceAddress(TARGET_MAC) - .build() - val scanFilters = listOf(scanFilter) - - val scanSettings = ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .build() - - try { - scanner!!.startScan(scanFilters, scanSettings, scanCallback) - } catch (e: Exception) { - val params = Arguments.createMap().apply { putString("message", e.message ?: "Scan start failed") } - sendEvent("onScaleError", params) - stopMeasuring() - return - } - - // 20-second timeout - timer = object : CountDownTimer(MEASURE_WINDOW_MS, 1000) { - override fun onTick(millisUntilFinished: Long) { - // Optional: send remaining seconds if needed - } - - override fun onFinish() { - stopMeasuring() - sendEvent("onStatusChange", Arguments.createMap().apply { putString("status", "timeout") }) - } - }.start() + ) + + isScanning = false } + } - @ReactMethod - fun stopMeasuring() { - if (!isScanning) return - - try { - scanner?.stopScan(scanCallback) - } catch (e: Exception) { - // Ignore - } - timer?.cancel() - timer = null - isScanning = false - - sendEvent("onStatusChange", Arguments.createMap().apply { putString("status", "stopped") }) + @ReactMethod + fun calculateBMI(weight: Double, heightCm: Double, promise: Promise) { + try { + if (heightCm <= 0) { + promise.reject("INVALID_HEIGHT", "Height must be greater than 0") + return + } + + val bmi = weight / (heightCm / 100).pow(2) + promise.resolve(round(bmi * 10) / 10) + } catch (e: Exception) { + promise.reject("BMI_ERROR", e.message, e) } + } - private val scanCallback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, result: ScanResult?) { - result ?: return - val scanRecord = result.scanRecord ?: return - val manufacturerData = scanRecord.manufacturerSpecificData + private fun hasPermissions(): Boolean { + val perms = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + arrayOf( + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT + ) + else + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION + ) - if (manufacturerData != null && manufacturerData.size() > 0) { - for (i in 0 until manufacturerData.size()) { - val data = manufacturerData.valueAt(i) ?: continue - if (data.size >= 2) { - val tenths = ((data[0].toInt() and 0xFF) shl 8) or (data[1].toInt() and 0xFF) - val kg = tenths / 10.0 - if (kg in MIN_VALID_KG..MAX_VALID_KG) { - processWeight(kg) - } - } - } - } - } - - override fun onScanFailed(errorCode: Int) { - val params = Arguments.createMap().apply { - putString("message", "Scan failed with code: $errorCode") - } - sendEvent("onScaleError", params) - stopMeasuring() - } - } - - private fun processWeight(kg: Double) { - // Send live weight update - val liveParams = Arguments.createMap().apply { putDouble("weight", kg) } - sendEvent("onLiveWeight", liveParams) - - // Update status to measuring - sendEvent("onStatusChange", Arguments.createMap().apply { putString("status", "measuring") }) - - val now = System.currentTimeMillis() - - // Stability detection - if (candidateKg.isNaN() || abs(kg - candidateKg) > WEIGHT_TOL) { - candidateKg = kg - stableSince = now - } else if (now - stableSince >= STABLE_WINDOW_MS) { - // Stable weight achieved - val stableParams = Arguments.createMap().apply { putDouble("weight", candidateKg) } - sendEvent("onStableWeight", stableParams) - sendEvent("onStatusChange", Arguments.createMap().apply { putString("status", "stable") }) - stopMeasuring() - } - } - - private fun hasRequiredPermissions(): Boolean { - val perms = mutableListOf() - perms.add(android.Manifest.permission.ACCESS_FINE_LOCATION) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - perms.add(android.Manifest.permission.BLUETOOTH_SCAN) - perms.add(android.Manifest.permission.BLUETOOTH_CONNECT) - } - - return perms.all { - ContextCompat.checkSelfPermission(reactApplicationContext, it) == PackageManager.PERMISSION_GRANTED - } - } - - override fun onCatalystInstanceDestroy() { - super.onCatalystInstanceDestroy() - stopMeasuring() // Cleanup when RN instance destroys + return perms.all { + ContextCompat.checkSelfPermission(reactContext, it) == + PackageManager.PERMISSION_GRANTED } + } } \ No newline at end of file diff --git a/android/src/main/java/com/weightscalebridge/WeightScaleBridgePackage.kt b/android/src/main/java/com/weightscalebridge/WeightScaleBridgePackage.kt index 364be9b..90fd8c0 100644 --- a/android/src/main/java/com/weightscalebridge/WeightScaleBridgePackage.kt +++ b/android/src/main/java/com/weightscalebridge/WeightScaleBridgePackage.kt @@ -1,16 +1,19 @@ -package com.cureselect.weightpluse +package com.weightscalebridge import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ViewManager -class WeightScalePackage : ReactPackage { - override fun createNativeModules(reactContext: ReactApplicationContext): List { - return listOf(WeightScaleModule(reactContext)) - } +class WeightScaleBridgePackage : ReactPackage { - override fun createViewManagers(reactContext: ReactApplicationContext): List> { - return emptyList() - } -} \ No newline at end of file + override fun createNativeModules( + reactContext: ReactApplicationContext + ): List { + return listOf(WeightScaleModule(reactContext)) + } + + override fun createViewManagers( + reactContext: ReactApplicationContext + ): List> = emptyList() +} diff --git a/src/index.tsx b/src/index.tsx index 34b188d..c3d4088 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,26 @@ -import WeightScaleBridge from './NativeWeightScaleBridge'; +import { NativeModules, Platform } from 'react-native'; -export function multiply(a: number, b: number): number { - return WeightScaleBridge.multiply(a, b); +const LINKING_ERROR = + `The package 'react-native-weight-scale-bridge' doesn't seem to be linked. Make sure:\n\n` + + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + + '- You rebuilt the app after installing the package\n' + + '- You are not using Expo Go\n'; + +// Changed to match the native module name +const WeightScaleBridge = NativeModules.WeightScaleBridge; + +if (!WeightScaleBridge) { + throw new Error(LINKING_ERROR); } + +export const startScanning = () => + WeightScaleBridge.startScanning(); + +export const stopScanning = () => + WeightScaleBridge.stopScanning(); + +export const calculateBMI = (weight: number, heightCm: number) => + WeightScaleBridge.calculateBMI(weight, heightCm); + +export const isBluetoothEnabled = () => + WeightScaleBridge.isBluetoothEnabled(); \ No newline at end of file