diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBroadcastScale.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBroadcastScale.java new file mode 100644 index 00000000..fcaf8c8b --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBroadcastScale.java @@ -0,0 +1,177 @@ +/* Copyright (C) 2024 olie.xdev +* 2024 Duncan Overbruck +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see +*/ + +package com.health.openscale.core.bluetooth; + +import static android.content.Context.LOCATION_SERVICE; + +import android.Manifest; +import android.bluetooth.le.ScanRecord; +import android.bluetooth.le.ScanResult; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; +import android.os.Handler; +import android.os.Looper; +import android.util.SparseArray; + +import androidx.core.content.ContextCompat; + +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.welie.blessed.BluetoothCentralManager; +import com.welie.blessed.BluetoothCentralManagerCallback; +import com.welie.blessed.BluetoothPeripheral; + +import java.util.Date; + +import timber.log.Timber; + + +public class BluetoothBroadcastScale extends BluetoothCommunication { + + + private ScaleMeasurement measurement; + + private boolean connected = false; + + private final BluetoothCentralManager central; + + public BluetoothBroadcastScale(Context context) + { + this.context = context; + this.central = new BluetoothCentralManager(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper())); + } + + // Callback for central + private final BluetoothCentralManagerCallback bluetoothCentralCallback = new BluetoothCentralManagerCallback() { + + @Override + public void onDiscoveredPeripheral(BluetoothPeripheral peripheral, ScanResult scanResult) { + ScanRecord record = scanResult.getScanRecord(); + if (record == null) + return; + + SparseArray manufacturerData = record.getManufacturerSpecificData(); + if (manufacturerData.size() != 1) + return; + + int companyId = manufacturerData.keyAt(0); + byte[] data = manufacturerData.get(companyId); + if (data.length < 12) { + Timber.d("Unexpected data length, got %d, expected %d", data.length, 12); + return; + } + + // lower byte of the two byte companyId is the xor byte, + // its used on the last 6 bytes of the data, the first 6 bytes + // are just the mac address and can be ignored. + byte xor = (byte) (companyId >> 8); + byte[] buf = new byte[6]; + for (int i = 0; i < 6; i++) { + buf[i] = (byte) (data[i + 6] ^ xor); + } + + // chk is the sum of the first 5 bytes, its 5 lower bits are compared to the 5 lower + // bites of the last byte in the packet. + int chk = 0; + for (int i = 0; i < 5; i++) { + chk += buf[i]; + } + if ((chk & 0x1F) != (buf[5] & 0x1F)) { + Timber.d("Checksum error, got %x, expected %x", chk & 0x1F, buf[5] & 0x1F); + return; + } + + if (!connected) { + // "fake" a connection, since we've got valid data. + setBluetoothStatus(BT_STATUS.CONNECTION_ESTABLISHED); + connected = true; + } + + switch (buf[4]) { + case (byte) 0xAD: + int value = (((buf[3] & 0xFF) << 0) | ((buf[2] & 0xFF) << 8) | + ((buf[1] & 0xFF) << 16) | ((buf[0] & 0xFF) << 24)); + byte state = (byte)(value >> 0x1F); + int grams = value & 0x3FFFF; + Timber.d("Got weight measurement weight=%.2f state=%d", (float)grams/1000, state); + if (state != 0 && measurement == null) { + measurement = new ScaleMeasurement(); + measurement.setDateTime(new Date()); + measurement.setWeight((float)grams / 1000); + + // stop now since we don't support any further data. + addScaleMeasurement(measurement); + disconnect(); + measurement = null; + } + break; + case (byte) 0xA6: + // this is the impedance package, not yet supported. + break; + default: + StringBuilder sb = new StringBuilder(); + for (byte b : buf) { + sb.append(String.format("0x%02X ", b)); + } + Timber.d("Unsupported packet type %x, xor key %x data: %s", buf[4], xor, sb.toString()); + } + } + }; + + @Override + public void connect(String macAddress) { + + LocationManager locationManager = (LocationManager)context.getSystemService(LOCATION_SERVICE); + + if ((ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) || + (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED ) && + (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER))) + ) { + Timber.d("Do LE scan before connecting to device"); + central.scanForPeripheralsWithAddresses(new String[]{macAddress}); + } + else { + Timber.e("No location permission, can't do anything"); + } + } + + + @Override + public void disconnect() { + Timber.d("Bluetooth disconnect"); + setBluetoothStatus(BT_STATUS.CONNECTION_DISCONNECT); + try { + central.stopScan(); + } catch (Exception ex) { + Timber.e("Error on Bluetooth disconnecting " + ex.getMessage()); + } + connected = false; + } + + @Override + public String driverName() { + return "BroadcastScale"; + } + + @Override + protected boolean onNextStep(int stepNr) { + return false; + } + +} diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java index 86f2224f..51aa0c57 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java @@ -80,6 +80,10 @@ public abstract class BluetoothCommunication { this.central = new BluetoothCentralManager(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper())); } + public BluetoothCommunication() { + + } + protected boolean needReConnect() { if (callbackBtHandler == null) { return true; diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java index 1e68b85a..0a4aba56 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java @@ -139,6 +139,9 @@ public class BluetoothFactory { if (deviceName.equals("Yoda1")){ return new BluetoothYoda1Scale(context); } + if (deviceName.equals("AAA002")){ + return new BluetoothBroadcastScale(context); + } return null; } }