package com.weightscalebridge import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.annotations.ReactModule import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager import android.bluetooth.le.* import android.content.Context import android.content.pm.PackageManager import android.os.Build 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 class WeightScaleModule(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 candidateKg: Double = Double.NaN private var stableSince: Long = 0L 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(eventName: String, params: WritableMap) { reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } @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 } 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() } @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") }) } private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult?) { result ?: return val scanRecord = result.scanRecord ?: return val manufacturerData = scanRecord.manufacturerSpecificData 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 } }