diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a2f47b6..6cac27a 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,2 +1,14 @@ + + + + + + + + + + + + diff --git a/android/src/main/java/com/weightscalebridge/MainApplication.kt b/android/src/main/java/com/weightscalebridge/MainApplication.kt new file mode 100644 index 0000000..f4f3a38 --- /dev/null +++ b/android/src/main/java/com/weightscalebridge/MainApplication.kt @@ -0,0 +1,6 @@ +// 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 9285a00..8fcb905 100644 --- a/android/src/main/java/com/weightscalebridge/WeightScaleBridgeModule.kt +++ b/android/src/main/java/com/weightscalebridge/WeightScaleBridgeModule.kt @@ -2,22 +2,192 @@ 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 -@ReactModule(name = WeightScaleBridgeModule.NAME) -class WeightScaleBridgeModule(reactContext: ReactApplicationContext) : - NativeWeightScaleBridgeSpec(reactContext) { +class WeightScaleModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - override fun getName(): String { - return NAME - } + override fun getName(): String = "WeightScale" - // Example method - // See https://reactnative.dev/docs/native-modules-android - override fun multiply(a: Double, b: Double): Double { - return a * b - } + private var bluetoothAdapter: BluetoothAdapter? = null + private var scanner: BluetoothLeScanner? = null + private var isScanning = false + private var timer: CountDownTimer? = null - companion object { - const val NAME = "WeightScaleBridge" - } -} + 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 + } +} \ 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 b5a984a..364be9b 100644 --- a/android/src/main/java/com/weightscalebridge/WeightScaleBridgePackage.kt +++ b/android/src/main/java/com/weightscalebridge/WeightScaleBridgePackage.kt @@ -1,33 +1,16 @@ -package com.weightscalebridge +package com.cureselect.weightpluse -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 WeightScaleBridgePackage : BaseReactPackage() { - override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { - return if (name == WeightScaleBridgeModule.NAME) { - WeightScaleBridgeModule(reactContext) - } else { - null +class WeightScalePackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(WeightScaleModule(reactContext)) } - } - override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { - return ReactModuleInfoProvider { - val moduleInfos: MutableMap = HashMap() - moduleInfos[WeightScaleBridgeModule.NAME] = ReactModuleInfo( - WeightScaleBridgeModule.NAME, - WeightScaleBridgeModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // isCxxModule - true // isTurboModule - ) - moduleInfos + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() } - } -} +} \ No newline at end of file