From 29dd632a8c0b406ce9929478f51ee848aa76846d Mon Sep 17 00:00:00 2001 From: Florin9doi <2995486+Florin9doi@users.noreply.github.com> Date: Sun, 17 Nov 2024 16:34:08 +0200 Subject: [PATCH] Support for nameless OKOK scales (Myria MY4836) (#1081) Co-authored-by: Florin9doi Co-authored-by: OliE --- .../core/bluetooth/BluetoothFactory.java | 13 +- .../core/bluetooth/BluetoothOKOK2.java | 189 ++++++++++++++++++ .../gui/preferences/BluetoothPreferences.java | 4 + .../BluetoothSettingsFragment.java | 9 +- 4 files changed, 208 insertions(+), 7 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 4cb92d1aa..abe3ad66e 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 static BluetoothCommunication createDeviceDriver(Context context, String 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 static BluetoothCommunication createDeviceDriver(Context context, String } 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,10 +133,10 @@ public static BluetoothCommunication createDeviceDriver(Context context, String 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("ES-26BB-B")){ @@ -142,7 +145,7 @@ public static BluetoothCommunication createDeviceDriver(Context context, String 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 000000000..71a71f99f --- /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 05287222c..1fecb99bb 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 @@ 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 20fbe7aa6..90cbcad47 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.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 void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { } 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 @@ private void onDeviceFound(final ScanResult bleScanResult) { 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());