From 52456e8744bf5648f0f4d74c19fffe5c74f89e6d Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Sat, 19 Oct 2024 01:10:15 +0300 Subject: [PATCH 1/4] Support for nameless OKOK scales (Myria MY4836) --- .../core/bluetooth/BluetoothFactory.java | 15 +- .../core/bluetooth/BluetoothOKOK2.java | 189 ++++++++++++++++++ .../gui/preferences/BluetoothPreferences.java | 4 + .../BluetoothSettingsFragment.java | 9 +- 4 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java 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 1cee9735..a6d28383 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 @@ -82,7 +82,7 @@ public class BluetoothFactory { if (name.equals("Health Scale".toLowerCase(Locale.US))) { return new BluetoothOneByone(context); } - if(name.equals("1byone scale".toLowerCase(Locale.US))){ + if(name.equals("1byone scale".toLowerCase(Locale.US))) { return new BluetoothOneByoneNew(context); } @@ -114,10 +114,13 @@ public class BluetoothFactory { } if (deviceName.equals("Hoffen BS-8107")) { return new BluetoothHoffenBBS8107(context); - } + } if (deviceName.equals("ADV") || deviceName.equals("Chipsea-BLE")) { return new BluetoothOKOK(context); } + if (deviceName.isEmpty()) { + return new BluetoothOKOK2(context); + } if (deviceName.equals("BF105") || deviceName.equals("BF720")) { return new BluetoothBeurerBF105(context); } @@ -130,16 +133,16 @@ public class BluetoothFactory { if (deviceName.equals("SBF72") || deviceName.equals("BF915") || deviceName.equals("SBF73")) { return new BluetoothSanitasSBF72(context, deviceName); } - if (deviceName.equals("Weight Scale")){ + if (deviceName.equals("Weight Scale")) { return new BluetoothSinocare(context); } - if (deviceName.equals("CH100")){ + if (deviceName.equals("CH100")) { return new BluetoothHuaweiAH100(context); } - if (deviceName.equals("Yoda1")){ + if (deviceName.equals("Yoda1")) { return new BluetoothYoda1Scale(context); } - if (deviceName.equals("AAA002") || deviceName.equals("AAA007")){ + if (deviceName.equals("AAA002") || deviceName.equals("AAA007")) { return new BluetoothBroadcastScale(context); } return null; diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java new file mode 100644 index 00000000..71a71f99 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java @@ -0,0 +1,189 @@ +/* Copyright (C) 2024 olie.xdev + * + * 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 android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.SparseArray; + +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.core.utils.Converters; +import com.welie.blessed.BluetoothCentralManager; +import com.welie.blessed.BluetoothCentralManagerCallback; +import com.welie.blessed.BluetoothPeripheral; + +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedList; +import java.util.List; + +import timber.log.Timber; + +import static com.health.openscale.core.utils.Converters.WeightUnit.LB; +import static com.health.openscale.core.utils.Converters.WeightUnit.ST; + +public class BluetoothOKOK2 extends BluetoothCommunication { + private static final int IDX_WEIGHT_MSB = 0; + private static final int IDX_WEIGHT_LSB = 1; + private static final int IDX_IMPEDANCE_MSB = 2; + private static final int IDX_IMPEDANCE_LSB = 3; + private static final int IDX_PRODUCTID_MSB = 4; + private static final int IDX_PRODUCTID_LSB = 5; + private static final int IDX_ATTRIB = 6; + private static final int IDX_MAC_1 = 7; + private static final int IDX_MAC_2 = 8; + private static final int IDX_MAC_3 = 9; + private static final int IDX_MAC_4 = 10; + private static final int IDX_MAC_5 = 11; + private static final int IDX_MAC_6 = 12; + + private static final int UNIT_KG = 0; + private static final int UNIT_LB = 2; + private static final int UNIT_STLB = 3; + + private BluetoothCentralManager central; + private String mMacAddress; + private float mLastWeight = 0f; + + public BluetoothOKOK2(Context context) { + super(context); + central = new BluetoothCentralManager(context, btCallback, new Handler(Looper.getMainLooper())); + } + + private final BluetoothCentralManagerCallback btCallback = new BluetoothCentralManagerCallback() { + @Override + public void onDiscoveredPeripheral(@NotNull BluetoothPeripheral peripheral, @NotNull ScanResult scanResult) { + SparseArray manufacturerSpecificData = scanResult.getScanRecord().getManufacturerSpecificData(); + int vendorIndex = -1; + for (int i = 0; i < manufacturerSpecificData.size(); i++) { + int vendorId = manufacturerSpecificData.keyAt(i); + if ((vendorId & 0xff) == 0xc0) { // 0x00c0-->0xffc0 + vendorIndex = vendorId; + break; + } + } + if (vendorIndex == -1) { + return; + } + byte[] data = manufacturerSpecificData.get(vendorIndex); + + StringBuilder sb = new StringBuilder(data.length * 3); + for (byte b : data) { + sb.append(String.format("%02x ", b)); + } + Timber.d("manufacturerSpecificData: [VID=%04x] %s", vendorIndex, sb.toString()); + + if (data[IDX_MAC_1] != (byte) ((Character.digit(mMacAddress.charAt(0), 16) << 4) + Character.digit(mMacAddress.charAt(1), 16)) + || data[IDX_MAC_2] != (byte) ((Character.digit(mMacAddress.charAt(3), 16) << 4) + Character.digit(mMacAddress.charAt(4), 16)) + || data[IDX_MAC_3] != (byte) ((Character.digit(mMacAddress.charAt(6), 16) << 4) + Character.digit(mMacAddress.charAt(7), 16)) + || data[IDX_MAC_4] != (byte) ((Character.digit(mMacAddress.charAt(9), 16) << 4) + Character.digit(mMacAddress.charAt(10), 16)) + || data[IDX_MAC_5] != (byte) ((Character.digit(mMacAddress.charAt(12), 16) << 4) + Character.digit(mMacAddress.charAt(13), 16)) + || data[IDX_MAC_6] != (byte) ((Character.digit(mMacAddress.charAt(15), 16) << 4) + Character.digit(mMacAddress.charAt(16), 16))) + return; + + if ((data[IDX_ATTRIB] & 1) == 0) // in progress + return; + + float divider = 10f; + switch ((data[IDX_ATTRIB] >> 1) & 3) { + case 0: + divider = 10f; + break; + case 1: + divider = 1f; + break; + case 2: + divider = 100f; + break; + } + + float weight = 0f; + switch ((data[IDX_ATTRIB] >> 3) & 3) { + case UNIT_KG: { + float val = ((data[IDX_WEIGHT_MSB] & 0xff) << 8) | (data[IDX_WEIGHT_LSB] & 0xff); + weight = val / divider; + break; + } + case UNIT_LB: { + float val = ((data[IDX_WEIGHT_MSB] & 0xff) << 8) | (data[IDX_WEIGHT_LSB] & 0xff); + weight = Converters.toKilogram(val / divider, LB); + break; + } + case UNIT_STLB: { + float val = data[IDX_WEIGHT_MSB] /*ST*/ + data[IDX_WEIGHT_LSB] /*LB*/ / divider / 14f; + weight = Converters.toKilogram(val, ST); + break; + } + } + + if (mLastWeight != weight) { + ScaleMeasurement entry = new ScaleMeasurement(); + entry.setWeight(weight); + addScaleMeasurement(entry); + mLastWeight = weight; + // disconnect(); + } + } + }; + + @Override + public void connect(String macAddress) { + mMacAddress = macAddress; + List filters = new LinkedList<>(); + + byte[] data = new byte[13]; + data[IDX_MAC_1] = (byte) ((Character.digit(macAddress.charAt(0), 16) << 4) + Character.digit(macAddress.charAt(1), 16)); + data[IDX_MAC_2] = (byte) ((Character.digit(macAddress.charAt(3), 16) << 4) + Character.digit(macAddress.charAt(4), 16)); + data[IDX_MAC_3] = (byte) ((Character.digit(macAddress.charAt(6), 16) << 4) + Character.digit(macAddress.charAt(7), 16)); + data[IDX_MAC_4] = (byte) ((Character.digit(macAddress.charAt(9), 16) << 4) + Character.digit(macAddress.charAt(10), 16)); + data[IDX_MAC_5] = (byte) ((Character.digit(macAddress.charAt(12), 16) << 4) + Character.digit(macAddress.charAt(13), 16)); + data[IDX_MAC_6] = (byte) ((Character.digit(macAddress.charAt(15), 16) << 4) + Character.digit(macAddress.charAt(16), 16)); + byte[] mask = new byte[13]; + mask[IDX_MAC_1] = mask[IDX_MAC_2] = mask[IDX_MAC_3] = mask[IDX_MAC_4] = mask[IDX_MAC_5] = mask[IDX_MAC_6] = (byte) 0xff; + + // TODO: verify setAdvertisingDataTypeWithData on API33+ + // b.setAdvertisingDataTypeWithData(ScanRecord.DATA_TYPE_MANUFACTURER_SPECIFIC_DATA, data, mask); + for (int i = 0x00; i <= 0xff; i++) { + ScanFilter.Builder b = new ScanFilter.Builder(); + b.setDeviceAddress(macAddress); + b.setManufacturerData((i << 8) | 0xc0, data, mask); + filters.add(b.build()); + } + + central.scanForPeripheralsUsingFilters(filters); + } + + @Override + public void disconnect() { + if (central != null) + central.stopScan(); + central = null; + super.disconnect(); + } + + @Override + public String driverName() { + return "OKOK (nameless)"; + } + + @Override + protected boolean onNextStep(int stepNr) { + return false; + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java index 05287222..1fecb99b 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java @@ -17,6 +17,7 @@ package com.health.openscale.gui.preferences; import android.content.SharedPreferences; import android.os.Bundle; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -41,6 +42,9 @@ public class BluetoothPreferences extends PreferenceFragmentCompat { private Preference btScanner; private static final String formatDeviceName(String name, String address) { + if (TextUtils.isEmpty(name) && !address.isEmpty()) { + return String.format("[%s]", address); + } if (name.isEmpty() || address.isEmpty()) { return "-"; } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java index 20fbe7aa..90cbcad4 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java @@ -42,6 +42,7 @@ import android.preference.PreferenceManager; import android.provider.Settings; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.view.Gravity; @@ -228,6 +229,9 @@ public class BluetoothSettingsFragment extends Fragment { } private static final String formatDeviceName(String name, String address) { + if (TextUtils.isEmpty(name) && !address.isEmpty()) { + return String.format("[%s]", address); + } if (name.isEmpty() || address.isEmpty()) { return "-"; } @@ -314,14 +318,15 @@ public class BluetoothSettingsFragment extends Fragment { BluetoothDevice device = bleScanResult.getDevice(); Context context = getContext(); - if (device.getName() == null || foundDevices.containsKey(device.getAddress()) || context == null) { + if (foundDevices.containsKey(device.getAddress()) || context == null) { return; } BluetoothDeviceView deviceView = new BluetoothDeviceView(context); deviceView.setDeviceName(formatDeviceName(bleScanResult.getDevice())); - BluetoothCommunication btDevice = BluetoothFactory.createDeviceDriver(context, device.getName()); + String name = device.getName() != null ? device.getName() : ""; + BluetoothCommunication btDevice = BluetoothFactory.createDeviceDriver(context, name); if (btDevice != null) { Timber.d("Found supported device %s (driver: %s)", formatDeviceName(device), btDevice.driverName()); From c17fff3a87ec7576bb84ce2728fbfe4992938cb5 Mon Sep 17 00:00:00 2001 From: oliexdev Date: Sun, 24 Nov 2024 14:31:55 +0100 Subject: [PATCH 2/4] gives a nameless scale a fake device name --- .../core/bluetooth/BluetoothFactory.java | 10 ++++++++- .../core/bluetooth/BluetoothOKOK2.java | 21 ++++++++++++++++--- .../BluetoothSettingsFragment.java | 8 +++++-- 3 files changed, 33 insertions(+), 6 deletions(-) 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 abe3ad66..8028e5c3 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 @@ -17,6 +17,7 @@ package com.health.openscale.core.bluetooth; import android.content.Context; +import android.util.SparseArray; import java.util.Locale; @@ -118,7 +119,7 @@ public class BluetoothFactory { if (deviceName.equals("ADV") || deviceName.equals("Chipsea-BLE")) { return new BluetoothOKOK(context); } - if (deviceName.isEmpty()) { + if (deviceName.equals("NoName OkOk")) { return new BluetoothOKOK2(context); } if (deviceName.equals("BF105") || deviceName.equals("BF720")) { @@ -150,4 +151,11 @@ public class BluetoothFactory { } return null; } + + public static String convertNoNameToDeviceName(SparseArray manufacturerSpecificData) { + String deviceName = null; + deviceName = BluetoothOKOK2.convertNoNameToDeviceName(manufacturerSpecificData); + + return deviceName; + } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java index 71a71f99..7a131b57 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java @@ -15,6 +15,9 @@ */ package com.health.openscale.core.bluetooth; +import static com.health.openscale.core.utils.Converters.WeightUnit.LB; +import static com.health.openscale.core.utils.Converters.WeightUnit.ST; + import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.content.Context; @@ -35,9 +38,6 @@ import java.util.List; import timber.log.Timber; -import static com.health.openscale.core.utils.Converters.WeightUnit.LB; -import static com.health.openscale.core.utils.Converters.WeightUnit.ST; - public class BluetoothOKOK2 extends BluetoothCommunication { private static final int IDX_WEIGHT_MSB = 0; private static final int IDX_WEIGHT_LSB = 1; @@ -66,6 +66,21 @@ public class BluetoothOKOK2 extends BluetoothCommunication { central = new BluetoothCentralManager(context, btCallback, new Handler(Looper.getMainLooper())); } + static String convertNoNameToDeviceName(SparseArray manufacturerSpecificData) { + int vendorIndex = -1; + for (int i = 0; i < manufacturerSpecificData.size(); i++) { + int vendorId = manufacturerSpecificData.keyAt(i); + if ((vendorId & 0xff) == 0xc0) { // 0x00c0-->0xffc0 + vendorIndex = vendorId; + } + } + if (vendorIndex == -1) { + return null; + } + + return "NoName OkOk"; + } + private final BluetoothCentralManagerCallback btCallback = new BluetoothCentralManagerCallback() { @Override public void onDiscoveredPeripheral(@NotNull BluetoothPeripheral peripheral, @NotNull ScanResult scanResult) { diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java index 90cbcad4..7bea7c1c 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java @@ -325,8 +325,12 @@ public class BluetoothSettingsFragment extends Fragment { BluetoothDeviceView deviceView = new BluetoothDeviceView(context); deviceView.setDeviceName(formatDeviceName(bleScanResult.getDevice())); - String name = device.getName() != null ? device.getName() : ""; - BluetoothCommunication btDevice = BluetoothFactory.createDeviceDriver(context, name); + String deviceName = device.getName(); + if (deviceName == null) { + deviceName = BluetoothFactory.convertNoNameToDeviceName(bleScanResult.getScanRecord().getManufacturerSpecificData()); + } + + BluetoothCommunication btDevice = BluetoothFactory.createDeviceDriver(context, deviceName); if (btDevice != null) { Timber.d("Found supported device %s (driver: %s)", formatDeviceName(device), btDevice.driverName()); From b232995d47e83cc7ad61bfcddd60d5955afaa9a2 Mon Sep 17 00:00:00 2001 From: oliexdev Date: Fri, 29 Nov 2024 10:56:23 +0100 Subject: [PATCH 3/4] applied patch by Florin9doi, see https://github.com/oliexdev/openScale/pull/1088#issuecomment-2496265764 --- .../BluetoothSettingsFragment.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java index 7bea7c1c..8a4ee30a 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java @@ -322,14 +322,18 @@ public class BluetoothSettingsFragment extends Fragment { return; } - BluetoothDeviceView deviceView = new BluetoothDeviceView(context); - deviceView.setDeviceName(formatDeviceName(bleScanResult.getDevice())); - String deviceName = device.getName(); if (deviceName == null) { deviceName = BluetoothFactory.convertNoNameToDeviceName(bleScanResult.getScanRecord().getManufacturerSpecificData()); + } + if (deviceName == null) { + return; } + BluetoothDeviceView deviceView = new BluetoothDeviceView(context); + deviceView.setDeviceName(formatDeviceName(deviceName, device.getAddress())); + deviceView.setAlias(deviceName); + BluetoothCommunication btDevice = BluetoothFactory.createDeviceDriver(context, deviceName); if (btDevice != null) { Timber.d("Found supported device %s (driver: %s)", @@ -398,6 +402,7 @@ public class BluetoothSettingsFragment extends Fragment { private TextView deviceName; private ImageView deviceIcon; private String deviceAddress; + private String deviceAlias; public BluetoothDeviceView(Context context) { super(context); @@ -427,6 +432,14 @@ public class BluetoothSettingsFragment extends Fragment { addView(deviceName); } + public void setAlias(String alias) { + deviceAlias = alias; + } + + public String getAlias() { + return deviceAlias; + } + public void setDeviceAddress(String address) { deviceAddress = address; } @@ -485,10 +498,10 @@ public class BluetoothSettingsFragment extends Fragment { prefs.edit() .putString(PREFERENCE_KEY_BLUETOOTH_HW_ADDRESS, device.getAddress()) - .putString(PREFERENCE_KEY_BLUETOOTH_DEVICE_NAME, device.getName()) + .putString(PREFERENCE_KEY_BLUETOOTH_DEVICE_NAME, getAlias()) .apply(); - Timber.d("Saved Bluetooth device " + device.getName() + " with address " + device.getAddress()); + Timber.d("Saved Bluetooth device " + getAlias() + " with address " + device.getAddress()); stopBluetoothDiscovery(); From 8eca3a052915e50e15b6ff65d92da458a5fc07f4 Mon Sep 17 00:00:00 2001 From: oliexdev Date: Wed, 4 Dec 2024 18:04:48 +0100 Subject: [PATCH 4/4] see comment at https://github.com/oliexdev/openScale/pull/1088#issuecomment-2509195401 --- .../com/health/openscale/core/bluetooth/BluetoothOKOK2.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java index 7a131b57..48d72498 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK2.java @@ -172,8 +172,6 @@ public class BluetoothOKOK2 extends BluetoothCommunication { byte[] mask = new byte[13]; mask[IDX_MAC_1] = mask[IDX_MAC_2] = mask[IDX_MAC_3] = mask[IDX_MAC_4] = mask[IDX_MAC_5] = mask[IDX_MAC_6] = (byte) 0xff; - // TODO: verify setAdvertisingDataTypeWithData on API33+ - // b.setAdvertisingDataTypeWithData(ScanRecord.DATA_TYPE_MANUFACTURER_SPECIFIC_DATA, data, mask); for (int i = 0x00; i <= 0xff; i++) { ScanFilter.Builder b = new ScanFilter.Builder(); b.setDeviceAddress(macAddress);