aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt
diff options
context:
space:
mode:
authorArslaan Pathan <[email protected]>2026-03-30 22:13:08 +1300
committerArslaan Pathan <[email protected]>2026-03-30 22:13:08 +1300
commite92a1f4a6babca0827410426ae59cc7b24f401da (patch)
treea9e93b75c41876368a4f66a7e4e9665c6832388f /app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt
downloadzwzn-freefit-android-e92a1f4a6babca0827410426ae59cc7b24f401da.tar.xz
zwzn-freefit-android-e92a1f4a6babca0827410426ae59cc7b24f401da.zip
chore: Initial commit
Diffstat (limited to 'app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt')
-rw-r--r--app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt121
1 files changed, 121 insertions, 0 deletions
diff --git a/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt b/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt
new file mode 100644
index 0000000..c138ead
--- /dev/null
+++ b/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt
@@ -0,0 +1,121 @@
+package com.arslaancodes.zwznfreefit
+
+import android.bluetooth.*
+import android.bluetooth.le.*
+import android.content.Context
+import android.os.ParcelUuid
+import java.util.UUID
+
+val SERVICE_UUID = UUID.fromString("6E40FC00-B5A3-F393-E0A9-E50E24DCCA9E")
+val WRITE_UUID = UUID.fromString("6E40FC20-B5A3-F393-E0A9-E50E24DCCA9E")
+val NOTIFY_UUID = UUID.fromString("6E40FC21-B5A3-F393-E0A9-E50E24DCCA9E")
+
+class BleManager(private val context: Context) {
+ companion object {
+ lateinit var instance: BleManager
+ }
+
+ init {
+ instance = this
+ }
+
+ private val adapter = BluetoothAdapter.getDefaultAdapter()
+ private var gatt: BluetoothGatt? = null
+ private var onNotify: ((ByteArray) -> Unit)? = null
+
+ fun scan(onFound: (BluetoothDevice) -> Unit) {
+ val scanner = adapter.bluetoothLeScanner
+ android.util.Log.d("BLE", "Starting scan, scanner=$scanner")
+ if (scanner == null) {
+ android.util.Log.e("BLE", "Scanner is null! Bluetooth off?")
+ return
+ }
+ val settings = ScanSettings.Builder().build()
+ scanner.startScan(null, settings, object : ScanCallback() {
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
+ android.util.Log.d("BLE", "Found: ${result.device.name} ${result.device.address}")
+ onFound(result.device)
+ }
+ override fun onScanFailed(errorCode: Int) {
+ android.util.Log.e("BLE", "Scan failed: $errorCode")
+ }
+ })
+ }
+
+ fun connect(device: BluetoothDevice, onConnected: (BluetoothGatt) -> Unit) {
+ gatt = device.connectGatt(context, false, object : BluetoothGattCallback() {
+ override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ g.discoverServices()
+ }
+ }
+ override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
+ val notifyChar = g.getService(SERVICE_UUID)?.getCharacteristic(NOTIFY_UUID)
+ if (notifyChar != null) {
+ g.setCharacteristicNotification(notifyChar, true)
+ val descriptor = notifyChar.getDescriptor(
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
+ )
+ descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
+ g.writeDescriptor(descriptor)
+ }
+ onConnected(g)
+ }
+ override fun onCharacteristicChanged(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
+ if (characteristic.uuid == NOTIFY_UUID) {
+ onNotify?.invoke(characteristic.value)
+ }
+ }
+ })
+ }
+
+ fun write(data: ByteArray) {
+ val char = gatt?.getService(SERVICE_UUID)?.getCharacteristic(WRITE_UUID)
+ char?.value = data
+ gatt?.writeCharacteristic(char)
+ }
+
+ fun syncTime() {
+ val now = System.currentTimeMillis() / 1000
+ val tzOffset = java.util.TimeZone.getDefault().getOffset(System.currentTimeMillis()) / 1000
+
+ val packet = ByteArray(12)
+ packet[0] = 0x01
+ packet[1] = (now shr 24).toByte()
+ packet[2] = (now shr 16).toByte()
+ packet[3] = (now shr 8).toByte()
+ packet[4] = now.toByte()
+ packet[5] = (tzOffset shr 24).toByte()
+ packet[6] = (tzOffset shr 16).toByte()
+ packet[7] = (tzOffset shr 8).toByte()
+ packet[8] = tzOffset.toByte()
+ packet[9] = 0x00
+ packet[10] = 0x00 // language code, 0 = english
+ packet[11] = 0x00
+
+ write(packet)
+ }
+
+ fun switchFindBand(flag: Int) {
+ // find/vibrate band
+ // these functions are usually named after their freefit/ferefit counterparts
+ val packet = ByteArray(2)
+ packet[0] = 0x51 // command
+ packet[1] = flag.toByte() // 0x01 = start, 0x00 = stop
+ write(packet)
+ }
+
+ fun configRealTimeMeasure(type: Int, start: Boolean, onResult: (ByteArray) -> Unit) {
+ // measure heart rate/other stuff
+ // confirmed working types so far is just 0x00 for heart rate, will RE more later
+ // yet again, function named after freefit counterparts from RE'd protocol/decompiled APKs
+ // all code in this file is intellectual property of Arslaan Pathan, no pieces of decompiled freefit code have been used anywhere in the app
+ // freefit decompilation was merely used as a reference for reverse-engineering the protocol
+ onNotify = onResult
+ val packet = ByteArray(3)
+ packet[0] = 0x60.toByte()
+ packet[1] = type.toByte()
+ packet[2] = if (start) 0x01.toByte() else 0x00.toByte()
+ write(packet)
+ }
+}