startLeScan with 128 bit UUIDs doesn't work on native Android BLE implementation

后端 未结 11 2252
别跟我提以往
别跟我提以往 2020-11-28 04:19

I am having trouble using startLeScan( new UUID[]{ MY_DESIRED_128_BIT_SERVICE_UUID }, callback ) on the new introduced BLE API of Android 4.3 on my Nexus 4.

The cal

相关标签:
11条回答
  • 2020-11-28 04:39

    Try this to retrieve/filter the device from the advertised 128-bit UUIDs:

    private List<UUID> parseUUIDs(final byte[] advertisedData) {
        List<UUID> uuids = new ArrayList<UUID>();
    
        int offset = 0;
        while (offset < (advertisedData.length - 2)) {
            int len = advertisedData[offset++];
            if (len == 0)
                break;
    
            int type = advertisedData[offset++];
            switch (type) {
            case 0x02: // Partial list of 16-bit UUIDs
            case 0x03: // Complete list of 16-bit UUIDs
                while (len > 1) {
                    int uuid16 = advertisedData[offset++];
                    uuid16 += (advertisedData[offset++] << 8);
                    len -= 2;
                    uuids.add(UUID.fromString(String.format(
                            "%08x-0000-1000-8000-00805f9b34fb", uuid16)));
                }
                break;
            case 0x06:// Partial list of 128-bit UUIDs
            case 0x07:// Complete list of 128-bit UUIDs
                // Loop through the advertised 128-bit UUID's.
                while (len >= 16) {
                    try {
                        // Wrap the advertised bits and order them.
                        ByteBuffer buffer = ByteBuffer.wrap(advertisedData,
                                offset++, 16).order(ByteOrder.LITTLE_ENDIAN);
                        long mostSignificantBit = buffer.getLong();
                        long leastSignificantBit = buffer.getLong();
                        uuids.add(new UUID(leastSignificantBit,
                                mostSignificantBit));
                    } catch (IndexOutOfBoundsException e) {
                        // Defensive programming.
                        Log.e(LOG_TAG, e.toString());
                        continue;
                    } finally {
                        // Move the offset to read the next uuid.
                        offset += 15;
                        len -= 16;
                    }
                }
                break;
            default:
                offset += (len - 1);
                break;
            }
        }
    
        return uuids;
    }
    
    0 讨论(0)
  • 2020-11-28 04:44

    128 bit UUID LE scan worked on Samsung S5, running Android 4.4.2; but yes it fails on Nexus 4, 7. Tested on 4.4.2, 4.4.3, 4.4.4.

    0 讨论(0)
  • 2020-11-28 04:46

    Although 4.3 doesn't seem to support filtering by 128-bit UUIDs, these UUIDs are likely present in the byte[] scanRecord returned by the LeScanCallback.

    There's probably a correct way to parse this data, but if you're getting the same data each time you can filter the results manually by finding the offsets of the UUIDs you're looking for. You might do this by printing the scan data to a log (as a hex string) and looking for the UUIDs you're interested in (they will probably follow a 0x06 or 0x07 and will be reversed). Once you find the offset, it shouldn't be too hard to set up a basic filter.

    Here is a simple example that filters by a single UUID (uses Apache Commons Lang for ArrayUtils and the bytes-to-hex method found here, but you can substitute your own code where necessary)

    public static boolean hasMyService(byte[] scanRecord) {
    
        // UUID we want to filter by (without hyphens)
        final String myServiceID = "0000000000001000800000805F9B34FB";
    
        // The offset in the scan record. In my case the offset was 13; it will probably be different for you
        final int serviceOffset = 13; 
    
        try{
    
            // Get a 16-byte array of what may or may not be the service we're filtering for
            byte[] service = ArrayUtils.subarray(scanRecord, serviceOffset, serviceOffset + 16);
    
            // The bytes are probably in reverse order, so we need to fix that
            ArrayUtils.reverse(service);
    
            // Get the hex string
            String discoveredServiceID = bytesToHex(service);
    
            // Compare against our service
            return myServiceID.equals(discoveredServiceID);
    
        } catch (Exception e){
            return false;
        }
    
    }
    
    0 讨论(0)
  • 2020-11-28 04:47

    The best method to list out the Service UUIDs from the scan result is to copy exactly the parseFromBytes method from ScanRecord.java which is located inside the android.bluetooth.le package (make sure you have the latest Android SDK), change the return to a List of ParcelUuid since that's the only thing we care about

        private static final int DATA_TYPE_FLAGS = 0x01;
        private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02;
        private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03;
        private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04;
        private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05;
        private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06;
        private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
        private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08;
        private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
        private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
        private static final int DATA_TYPE_SERVICE_DATA = 0x16;
        private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
    
        public static List<ParcelUuid> parseFromBytes(byte[] scanRecord) {
            if (scanRecord == null) {
                return null;
            }
    
            int currentPos = 0;
            int advertiseFlag = -1;
            List<ParcelUuid> serviceUuids = new ArrayList<ParcelUuid>();
            String localName = null;
            int txPowerLevel = Integer.MIN_VALUE;
    
            SparseArray<byte[]> manufacturerData = new SparseArray<byte[]>();
            Map<ParcelUuid, byte[]> serviceData = new HashMap<ParcelUuid, byte[]>();
    
            try {
                while (currentPos < scanRecord.length) {
                    // length is unsigned int.
                    int length = scanRecord[currentPos++] & 0xFF;
                    if (length == 0) {
                        break;
                    }
                    // Note the length includes the length of the field type itself.
                    int dataLength = length - 1;
                    // fieldType is unsigned int.
                    int fieldType = scanRecord[currentPos++] & 0xFF;
                    switch (fieldType) {
                        case DATA_TYPE_FLAGS:
                            advertiseFlag = scanRecord[currentPos] & 0xFF;
                            break;
                        case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL:
                        case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE:
                            parseServiceUuid(scanRecord, currentPos,
                                    dataLength, 2, serviceUuids);
                            break;
                        case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL:
                        case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE:
                            parseServiceUuid(scanRecord, currentPos, dataLength,
                                    BluetoothUuid.UUID_BYTES_32_BIT, serviceUuids);
                            break;
                        case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL:
                        case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE:
                            parseServiceUuid(scanRecord, currentPos, dataLength,
                                    BluetoothUuid.UUID_BYTES_128_BIT, serviceUuids);
                            break;
                        case DATA_TYPE_LOCAL_NAME_SHORT:
                        case DATA_TYPE_LOCAL_NAME_COMPLETE:
                            localName = new String(
                                    extractBytes(scanRecord, currentPos, dataLength));
                            break;
                        case DATA_TYPE_TX_POWER_LEVEL:
                            txPowerLevel = scanRecord[currentPos];
                            break;
                        case DATA_TYPE_SERVICE_DATA:
                            // The first two bytes of the service data are service data UUID in little
                            // endian. The rest bytes are service data.
                            int serviceUuidLength = BluetoothUuid.UUID_BYTES_16_BIT;
                            byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos,
                                    serviceUuidLength);
                            ParcelUuid serviceDataUuid = BluetoothUuid.parseUuidFrom(
                                    serviceDataUuidBytes);
                            byte[] serviceDataArray = extractBytes(scanRecord,
                                    currentPos + serviceUuidLength, dataLength - serviceUuidLength);
                            serviceData.put(serviceDataUuid, serviceDataArray);
                            break;
                        case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA:
                            // The first two bytes of the manufacturer specific data are
                            // manufacturer ids in little endian.
                            int manufacturerId = ((scanRecord[currentPos + 1] & 0xFF) << 8) +
                                    (scanRecord[currentPos] & 0xFF);
                            byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2,
                                    dataLength - 2);
                            manufacturerData.put(manufacturerId, manufacturerDataBytes);
                            break;
                        default:
                            // Just ignore, we don't handle such data type.
                            break;
                    }
                    currentPos += dataLength;
                }
    
                if (serviceUuids.isEmpty()) {
                    serviceUuids = null;
                }
    
    //            Log.i("SERVICE UUIDS", parcelUuidToString(serviceUuids));
            } catch (Exception e) {
                Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord));
                // As the record is invalid, ignore all the parsed results for this packet
                // and return an empty record with raw scanRecord bytes in results
            }
    
            return serviceUuids;
        }
    

    You would need to import BluetoothUuid.java from the same package as well:

    import java.util.UUID;
    import android.os.ParcelUuid;
    import java.nio.ByteBuffer;
    import java.nio.ByteOrder;
    import java.util.Arrays;
    import java.util.HashSet;
    /**
     * Static helper methods and constants to decode the ParcelUuid of remote devices.
     *  @hide
     */
    public final class BluetoothUuid {
        /* See Bluetooth Assigned Numbers document - SDP section, to get the values of UUIDs
         * for the various services.
         *
         * The following 128 bit values are calculated as:
         *  uuid * 2^96 + BASE_UUID
         */
        public static final ParcelUuid AudioSink =
                ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid AudioSource =
                ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid AdvAudioDist =
                ParcelUuid.fromString("0000110D-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid HSP =
                ParcelUuid.fromString("00001108-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid HSP_AG =
                ParcelUuid.fromString("00001112-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid Handsfree =
                ParcelUuid.fromString("0000111E-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid Handsfree_AG =
                ParcelUuid.fromString("0000111F-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid AvrcpController =
                ParcelUuid.fromString("0000110E-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid AvrcpTarget =
                ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid ObexObjectPush =
                ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb");
        public static final ParcelUuid Hid =
                ParcelUuid.fromString("00001124-0000-1000-8000-00805f9b34fb");
        public static final ParcelUuid Hogp =
                ParcelUuid.fromString("00001812-0000-1000-8000-00805f9b34fb");
        public static final ParcelUuid PANU =
                ParcelUuid.fromString("00001115-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid NAP =
                ParcelUuid.fromString("00001116-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid BNEP =
                ParcelUuid.fromString("0000000f-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid PBAP_PCE =
                ParcelUuid.fromString("0000112e-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid PBAP_PSE =
                ParcelUuid.fromString("0000112f-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid MAP =
                ParcelUuid.fromString("00001134-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid MNS =
                ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid MAS =
                ParcelUuid.fromString("00001132-0000-1000-8000-00805F9B34FB");
        public static final ParcelUuid SAP =
                ParcelUuid.fromString("0000112D-0000-1000-8000-00805F9B34FB");
    
        public static final ParcelUuid BASE_UUID =
                ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
        /** Length of bytes for 16 bit UUID */
        public static final int UUID_BYTES_16_BIT = 2;
        /** Length of bytes for 32 bit UUID */
        public static final int UUID_BYTES_32_BIT = 4;
        /** Length of bytes for 128 bit UUID */
        public static final int UUID_BYTES_128_BIT = 16;
        public static final ParcelUuid[] RESERVED_UUIDS = {
                AudioSink, AudioSource, AdvAudioDist, HSP, Handsfree, AvrcpController, AvrcpTarget,
                ObexObjectPush, PANU, NAP, MAP, MNS, MAS, SAP};
        public static boolean isAudioSource(ParcelUuid uuid) {
            return uuid.equals(AudioSource);
        }
        public static boolean isAudioSink(ParcelUuid uuid) {
            return uuid.equals(AudioSink);
        }
        public static boolean isAdvAudioDist(ParcelUuid uuid) {
            return uuid.equals(AdvAudioDist);
        }
        public static boolean isHandsfree(ParcelUuid uuid) {
            return uuid.equals(Handsfree);
        }
        public static boolean isHeadset(ParcelUuid uuid) {
            return uuid.equals(HSP);
        }
        public static boolean isAvrcpController(ParcelUuid uuid) {
            return uuid.equals(AvrcpController);
        }
        public static boolean isAvrcpTarget(ParcelUuid uuid) {
            return uuid.equals(AvrcpTarget);
        }
        public static boolean isInputDevice(ParcelUuid uuid) {
            return uuid.equals(Hid);
        }
        public static boolean isPanu(ParcelUuid uuid) {
            return uuid.equals(PANU);
        }
        public static boolean isNap(ParcelUuid uuid) {
            return uuid.equals(NAP);
        }
        public static boolean isBnep(ParcelUuid uuid) {
            return uuid.equals(BNEP);
        }
        public static boolean isMap(ParcelUuid uuid) {
            return uuid.equals(MAP);
        }
        public static boolean isMns(ParcelUuid uuid) {
            return uuid.equals(MNS);
        }
        public static boolean isMas(ParcelUuid uuid) {
            return uuid.equals(MAS);
        }
        public static boolean isSap(ParcelUuid uuid) {
            return uuid.equals(SAP);
        }
        /**
         * Returns true if ParcelUuid is present in uuidArray
         *
         * @param uuidArray - Array of ParcelUuids
         * @param uuid
         */
        public static boolean isUuidPresent(ParcelUuid[] uuidArray, ParcelUuid uuid) {
            if ((uuidArray == null || uuidArray.length == 0) && uuid == null)
                return true;
            if (uuidArray == null)
                return false;
            for (ParcelUuid element: uuidArray) {
                if (element.equals(uuid)) return true;
            }
            return false;
        }
        /**
         * Returns true if there any common ParcelUuids in uuidA and uuidB.
         *
         * @param uuidA - List of ParcelUuids
         * @param uuidB - List of ParcelUuids
         *
         */
        public static boolean containsAnyUuid(ParcelUuid[] uuidA, ParcelUuid[] uuidB) {
            if (uuidA == null && uuidB == null) return true;
            if (uuidA == null) {
                return uuidB.length == 0 ? true : false;
            }
            if (uuidB == null) {
                return uuidA.length == 0 ? true : false;
            }
            HashSet<ParcelUuid> uuidSet = new HashSet<ParcelUuid> (Arrays.asList(uuidA));
            for (ParcelUuid uuid: uuidB) {
                if (uuidSet.contains(uuid)) return true;
            }
            return false;
        }
        /**
         * Returns true if all the ParcelUuids in ParcelUuidB are present in
         * ParcelUuidA
         *
         * @param uuidA - Array of ParcelUuidsA
         * @param uuidB - Array of ParcelUuidsB
         *
         */
        public static boolean containsAllUuids(ParcelUuid[] uuidA, ParcelUuid[] uuidB) {
            if (uuidA == null && uuidB == null) return true;
            if (uuidA == null) {
                return uuidB.length == 0 ? true : false;
            }
            if (uuidB == null) return true;
            HashSet<ParcelUuid> uuidSet = new HashSet<ParcelUuid> (Arrays.asList(uuidA));
            for (ParcelUuid uuid: uuidB) {
                if (!uuidSet.contains(uuid)) return false;
            }
            return true;
        }
        /**
         * Extract the Service Identifier or the actual uuid from the Parcel Uuid.
         * For example, if 0000110B-0000-1000-8000-00805F9B34FB is the parcel Uuid,
         * this function will return 110B
         * @param parcelUuid
         * @return the service identifier.
         */
        public static int getServiceIdentifierFromParcelUuid(ParcelUuid parcelUuid) {
            UUID uuid = parcelUuid.getUuid();
            long value = (uuid.getMostSignificantBits() & 0x0000FFFF00000000L) >>> 32;
            return (int)value;
        }
        /**
         * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID,
         * but the returned UUID is always in 128-bit format.
         * Note UUID is little endian in Bluetooth.
         *
         * @param uuidBytes Byte representation of uuid.
         * @return {@link ParcelUuid} parsed from bytes.
         * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed.
         */
        public static ParcelUuid parseUuidFrom(byte[] uuidBytes) {
            if (uuidBytes == null) {
                throw new IllegalArgumentException("uuidBytes cannot be null");
            }
            int length = uuidBytes.length;
            if (length != UUID_BYTES_16_BIT && length != UUID_BYTES_32_BIT &&
                    length != UUID_BYTES_128_BIT) {
                throw new IllegalArgumentException("uuidBytes length invalid - " + length);
            }
            // Construct a 128 bit UUID.
            if (length == UUID_BYTES_128_BIT) {
                ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
                long msb = buf.getLong(8);
                long lsb = buf.getLong(0);
                return new ParcelUuid(new UUID(msb, lsb));
            }
            // For 16 bit and 32 bit UUID we need to convert them to 128 bit value.
            // 128_bit_value = uuid * 2^96 + BASE_UUID
            long shortUuid;
            if (length == UUID_BYTES_16_BIT) {
                shortUuid = uuidBytes[0] & 0xFF;
                shortUuid += (uuidBytes[1] & 0xFF) << 8;
            } else {
                shortUuid = uuidBytes[0] & 0xFF ;
                shortUuid += (uuidBytes[1] & 0xFF) << 8;
                shortUuid += (uuidBytes[2] & 0xFF) << 16;
                shortUuid += (uuidBytes[3] & 0xFF) << 24;
            }
            long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32);
            long lsb = BASE_UUID.getUuid().getLeastSignificantBits();
            return new ParcelUuid(new UUID(msb, lsb));
        }
        /**
         * Parse UUID to bytes. The returned value is shortest representation, a 16-bit, 32-bit or 128-bit UUID,
         * Note returned value is little endian (Bluetooth).
         *
         * @param uuid uuid to parse.
         * @return shortest representation of {@code uuid} as bytes.
         * @throws IllegalArgumentException If the {@code uuid} is null.
         */
        public static byte[] uuidToBytes(ParcelUuid uuid) {
            if (uuid == null) {
                throw new IllegalArgumentException("uuid cannot be null");
            }
            if (is16BitUuid(uuid)) {
                byte[] uuidBytes = new byte[UUID_BYTES_16_BIT];
                int uuidVal = getServiceIdentifierFromParcelUuid(uuid);
                uuidBytes[0] = (byte)(uuidVal & 0xFF);
                uuidBytes[1] = (byte)((uuidVal & 0xFF00) >> 8);
                return uuidBytes;
            }
            if (is32BitUuid(uuid)) {
                byte[] uuidBytes = new byte[UUID_BYTES_32_BIT];
                int uuidVal = getServiceIdentifierFromParcelUuid(uuid);
                uuidBytes[0] = (byte)(uuidVal & 0xFF);
                uuidBytes[1] = (byte)((uuidVal & 0xFF00) >> 8);
                uuidBytes[2] = (byte)((uuidVal & 0xFF0000) >> 16);
                uuidBytes[3] = (byte)((uuidVal & 0xFF000000) >> 24);
                return uuidBytes;
            }
            // Construct a 128 bit UUID.
            long msb = uuid.getUuid().getMostSignificantBits();
            long lsb = uuid.getUuid().getLeastSignificantBits();
            byte[] uuidBytes = new byte[UUID_BYTES_128_BIT];
            ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
            buf.putLong(8, msb);
            buf.putLong(0, lsb);
            return uuidBytes;
        }
        /**
         * Check whether the given parcelUuid can be converted to 16 bit bluetooth uuid.
         *
         * @param parcelUuid
         * @return true if the parcelUuid can be converted to 16 bit uuid, false otherwise.
         */
        public static boolean is16BitUuid(ParcelUuid parcelUuid) {
            UUID uuid = parcelUuid.getUuid();
            if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) {
                return false;
            }
            return ((uuid.getMostSignificantBits() & 0xFFFF0000FFFFFFFFL) == 0x1000L);
        }
        /**
         * Check whether the given parcelUuid can be converted to 32 bit bluetooth uuid.
         *
         * @param parcelUuid
         * @return true if the parcelUuid can be converted to 32 bit uuid, false otherwise.
         */
        public static boolean is32BitUuid(ParcelUuid parcelUuid) {
            UUID uuid = parcelUuid.getUuid();
            if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) {
                return false;
            }
            if (is16BitUuid(parcelUuid)) {
                return false;
            }
            return ((uuid.getMostSignificantBits() & 0xFFFFFFFFL) == 0x1000L);
        }
    

    From my testing the results yield exactly what I want.

    0 讨论(0)
  • 2020-11-28 04:49

    This is a reported bug at least in Android 4.3 JWR66Y:

    • Filtering works, if I provide my 16bit UUID
    • Filtering doesn't return any scan results, if I provide my 128bit UUID or if I provide both UUIDs

    My setting: My device offers 2 UUIDs on advertising (1 16bit and 1 128bit) and 4 UUIDs on service discovery (1 128bit and 3 16bit).

    Even if it gets fixed, I warn everybody against using the filter option provided by Android. For backward compatibility and since it's broken on Samsung Galaxy S3 with Android 4.3

    0 讨论(0)
  • 2020-11-28 04:50

    I have been running into the same issue with the SensorTag from TI using my N7 2013 with Android 4.3.

    What I have found to work is to start the LeScan, wait a second, stop it and then restart it.

            if(!mBluetoothAdapter.startLeScan(mBleScanCallback)){
                L.e("could not start scan action");
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            mBluetoothAdapter.stopLeScan(mBleScanCallback);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            mBluetoothAdapter.startLeScan(mBleScanCallback);
    

    Also I noticed that sometimes the connection is not established (this might or might not be related to the firmware implementation or closing the connection properly). In the same way it seems that trying to reconnect to the gatt seems to do the trick.

    Having to use these workarounds is really disappointing...

    0 讨论(0)
提交回复
热议问题