From 2ef8a2e8cd445bbe78199db7b95b683be0e5734e Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Sun, 19 Apr 2020 14:56:23 -0400 Subject: [PATCH 1/3] Add support for covid beacon proposal from Apple and Google --- README.md | 1 - .../org/altbeacon/beacon/BeaconParser.java | 105 ++++++++++------ .../service/scanner/ScanFilterUtils.java | 28 +++-- .../org/altbeacon/beacon/CBeaconTest.java | 114 ++++++++++++++++++ 4 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 lib/src/test/java/org/altbeacon/beacon/CBeaconTest.java diff --git a/README.md b/README.md index b59ac0483..8aa914220 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ Key Gradle build targets: ./gradlew test # run unit tests ./gradlew build # development build ./gradlew release -Prelease # release build - ./gradlew generateReleaseJavadoc ## License diff --git a/lib/src/main/java/org/altbeacon/beacon/BeaconParser.java b/lib/src/main/java/org/altbeacon/beacon/BeaconParser.java index d0d590298..7d15d9210 100644 --- a/lib/src/main/java/org/altbeacon/beacon/BeaconParser.java +++ b/lib/src/main/java/org/altbeacon/beacon/BeaconParser.java @@ -48,7 +48,7 @@ public class BeaconParser implements Serializable { private static final Pattern M_PATTERN = Pattern.compile("m\\:(\\d+)-(\\d+)\\=([0-9A-Fa-f]+)"); private static final Pattern S_PATTERN = Pattern.compile("s\\:(\\d+)-(\\d+)\\=([0-9A-Fa-f]+)"); private static final Pattern D_PATTERN = Pattern.compile("d\\:(\\d+)\\-(\\d+)([bl]*)?"); - private static final Pattern P_PATTERN = Pattern.compile("p\\:(\\d+)\\-(\\d+)\\:?([\\-\\d]+)?"); + private static final Pattern P_PATTERN = Pattern.compile("p\\:(\\d+)?\\-(\\d+)?\\:?([\\-\\d]+)?"); private static final Pattern X_PATTERN = Pattern.compile("x"); private static final char[] HEX_ARRAY = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; private static final String LITTLE_ENDIAN_SUFFIX = "l"; @@ -207,20 +207,26 @@ public BeaconParser setBeaconLayout(String beaconLayout) { } } matcher = P_PATTERN.matcher(term); + while (matcher.find()) { found = true; + String correctionString = "none"; try { - int startOffset = Integer.parseInt(matcher.group(1)); - int endOffset = Integer.parseInt(matcher.group(2)); + if (matcher.group(1) != null && matcher.group(2) != null) { + int startOffset = Integer.parseInt(matcher.group(1)); + int endOffset = Integer.parseInt(matcher.group(2)); + mPowerStartOffset=startOffset; + mPowerEndOffset=endOffset; + } + int dBmCorrection = 0; if (matcher.group(3) != null) { - dBmCorrection = Integer.parseInt(matcher.group(3)); + correctionString = matcher.group(3); + dBmCorrection = Integer.parseInt(correctionString); } mDBmCorrection=dBmCorrection; - mPowerStartOffset=startOffset; - mPowerEndOffset=endOffset; } catch (NumberFormatException e) { - throw new BeaconLayoutException("Cannot parse integer power byte offset in term: " + term); + throw new BeaconLayoutException("Cannot parse integer power byte offset ("+correctionString+") in term: " + term); } } matcher = M_PATTERN.matcher(term); @@ -272,18 +278,6 @@ public BeaconParser setBeaconLayout(String beaconLayout) { throw new BeaconLayoutException("Cannot parse beacon layout term: " + term); } } - if (!mExtraFrame) { - // extra frames do not have to have identifiers or power fields, but other types do - if (mIdentifierStartOffsets.size() == 0 || mIdentifierEndOffsets.size() == 0) { - throw new BeaconLayoutException("You must supply at least one identifier offset with a prefix of 'i'"); - } - if (mPowerStartOffset == null || mPowerEndOffset == null) { - throw new BeaconLayoutException("You must supply a power byte offset with a prefix of 'p'"); - } - } - if (mMatchingBeaconTypeCodeStartOffset == null || mMatchingBeaconTypeCodeEndOffset == null) { - throw new BeaconLayoutException("You must supply a matching beacon type expression with a prefix of 'm'"); - } mLayoutSize = calculateLayoutSize(); return this; } @@ -360,6 +354,9 @@ public void setAllowPduOverflow(Boolean enabled) { * @return */ public Long getMatchingBeaconTypeCode() { + if (mMatchingBeaconTypeCode == null) { + return -1l; + } return mMatchingBeaconTypeCode; } @@ -368,6 +365,9 @@ public Long getMatchingBeaconTypeCode() { * @return */ public int getMatchingBeaconTypeCodeStartOffset() { + if (mMatchingBeaconTypeCodeStartOffset == null) { + return -1; + } return mMatchingBeaconTypeCodeStartOffset; } @@ -376,6 +376,9 @@ public int getMatchingBeaconTypeCodeStartOffset() { * @return */ public int getMatchingBeaconTypeCodeEndOffset() { + if (mMatchingBeaconTypeCodeEndOffset == null) { + return -1; + } return mMatchingBeaconTypeCodeEndOffset; } @@ -450,21 +453,35 @@ protected Beacon fromScanData(byte[] bytesToProcess, int rssi, BluetoothDevice d } else { byte[] serviceUuidBytes = null; - byte[] typeCodeBytes = longToByteArray(getMatchingBeaconTypeCode(), mMatchingBeaconTypeCodeEndOffset - mMatchingBeaconTypeCodeStartOffset + 1); + byte[] typeCodeBytes = {}; + if (mMatchingBeaconTypeCodeEndOffset != null && mMatchingBeaconTypeCodeStartOffset >= 0) { + typeCodeBytes = longToByteArray(getMatchingBeaconTypeCode(), mMatchingBeaconTypeCodeEndOffset - mMatchingBeaconTypeCodeStartOffset + 1); + } if (getServiceUuid() != null) { serviceUuidBytes = longToByteArray(getServiceUuid(), mServiceUuidEndOffset - mServiceUuidStartOffset + 1, false); } startByte = pduToParse.getStartIndex(); boolean patternFound = false; - if (getServiceUuid() == null) { - if (byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) { - patternFound = true; + if (getServiceUuid() == null || getServiceUuid() == -1) { + if (mMatchingBeaconTypeCodeEndOffset != null) { + if (byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) { + patternFound = true; + } } } else { - if (byteArraysMatch(bytesToProcess, startByte + mServiceUuidStartOffset, serviceUuidBytes) && - byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) { - patternFound = true; + if (byteArraysMatch(bytesToProcess, startByte + mServiceUuidStartOffset, serviceUuidBytes)) { + if (mMatchingBeaconTypeCodeEndOffset != null) { + if (byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) { + patternFound = true; + } + } + else { + if (pduToParse.getType() == Pdu.GATT_SERVICE_UUID_PDU_TYPE) { + patternFound = true; + } + } + } } @@ -479,12 +496,16 @@ protected Beacon fromScanData(byte[] bytesToProcess, int rssi, BluetoothDevice d } } else { if (LogManager.isVerboseLoggingEnabled()) { + int offset = 0; + if (mMatchingBeaconTypeCodeStartOffset != null) { + offset = mMatchingBeaconTypeCodeStartOffset; + } LogManager.d(TAG, "This is not a matching Beacon advertisement. Was expecting %s at offset %d and %s at offset %d. " + "The bytes I see are: %s", byteArrayToString(serviceUuidBytes), startByte + mServiceUuidStartOffset, byteArrayToString(typeCodeBytes), - startByte + mMatchingBeaconTypeCodeStartOffset, + startByte + offset, bytesToHex(bytesToProcess)); } } @@ -578,6 +599,11 @@ else if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) { // keep default value } } + else { + if (mDBmCorrection != null) { + beacon.mTxPower = mDBmCorrection; + } + } } } @@ -585,10 +611,11 @@ else if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) { beacon = null; } else { - int beaconTypeCode = 0; - String beaconTypeString = byteArrayToFormattedString(bytesToProcess, mMatchingBeaconTypeCodeStartOffset+startByte, mMatchingBeaconTypeCodeEndOffset+startByte, false); - beaconTypeCode = Integer.parseInt(beaconTypeString); - // TODO: error handling needed on the parse + int beaconTypeCode = -1; + if (mMatchingBeaconTypeCodeEndOffset != null) { + String beaconTypeString = byteArrayToFormattedString(bytesToProcess, mMatchingBeaconTypeCodeStartOffset+startByte, mMatchingBeaconTypeCodeEndOffset+startByte, false); + beaconTypeCode = Integer.parseInt(beaconTypeString); + } int manufacturer = 0; String manufacturerString = byteArrayToFormattedString(bytesToProcess, startByte, startByte+1, true); @@ -667,12 +694,13 @@ public byte[] getBeaconAdvertisementData(Beacon beacon) { lastIndex += adjustedIdentifiersLength; advertisingBytes = new byte[lastIndex+1-2]; - long beaconTypeCode = this.getMatchingBeaconTypeCode(); - - // set type code - for (int index = this.mMatchingBeaconTypeCodeStartOffset; index <= this.mMatchingBeaconTypeCodeEndOffset; index++) { - byte value = (byte) (this.getMatchingBeaconTypeCode() >> (8*(this.mMatchingBeaconTypeCodeEndOffset-index)) & 0xff); - advertisingBytes[index-2] = value; + if (mMatchingBeaconTypeCodeEndOffset != null) { + long beaconTypeCode = this.getMatchingBeaconTypeCode(); + // set type code + for (int index = this.mMatchingBeaconTypeCodeStartOffset; index <= this.mMatchingBeaconTypeCodeEndOffset; index++) { + byte value = (byte) (this.getMatchingBeaconTypeCode() >> (8*(this.mMatchingBeaconTypeCodeEndOffset-index)) & 0xff); + advertisingBytes[index-2] = value; + } } // set identifiers @@ -717,8 +745,7 @@ else if (identifierBytes.length > getIdentifierByteCount(identifierNum)) { } // set power - - if (this.mPowerStartOffset != null && this.mPowerEndOffset != null) { + if (this.mPowerStartOffset != null && this.mPowerEndOffset != null && this.mPowerStartOffset >= 2) { for (int index = this.mPowerStartOffset; index <= this.mPowerEndOffset; index ++) { advertisingBytes[index-2] = (byte) (beacon.getTxPower() >> (8*(index - this.mPowerStartOffset)) & 0xff); } diff --git a/lib/src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java b/lib/src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java index dab1925f1..10a2fb5e7 100644 --- a/lib/src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java +++ b/lib/src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java @@ -44,19 +44,25 @@ public List createScanFilterDataForBeaconParser(BeaconParser bea // Note: the -2 here is because we want the filter and mask to start after the // two-byte manufacturer code, and the beacon parser expression is based on offsets // from the start of the two byte code - byte[] filter = new byte[endOffset + 1 - 2]; - byte[] mask = new byte[endOffset + 1 - 2]; - byte[] typeCodeBytes = BeaconParser.longToByteArray(typeCode, endOffset-startOffset+1); - for (int layoutIndex = 2; layoutIndex <= endOffset; layoutIndex++) { - int filterIndex = layoutIndex-2; - if (layoutIndex < startOffset) { - filter[filterIndex] = 0; - mask[filterIndex] = 0; - } else { - filter[filterIndex] = typeCodeBytes[layoutIndex-startOffset]; - mask[filterIndex] = (byte) 0xff; + int length = endOffset + 1 - 2; + byte[] filter = new byte[0]; + byte[] mask = new byte[0]; + if (length > 0) { + filter = new byte[length]; + mask = new byte[length]; + byte[] typeCodeBytes = BeaconParser.longToByteArray(typeCode, endOffset-startOffset+1); + for (int layoutIndex = 2; layoutIndex <= endOffset; layoutIndex++) { + int filterIndex = layoutIndex-2; + if (layoutIndex < startOffset) { + filter[filterIndex] = 0; + mask[filterIndex] = 0; + } else { + filter[filterIndex] = typeCodeBytes[layoutIndex-startOffset]; + mask[filterIndex] = (byte) 0xff; + } } } + ScanFilterData sfd = new ScanFilterData(); sfd.manufacturer = manufacturer; sfd.filter = filter; diff --git a/lib/src/test/java/org/altbeacon/beacon/CBeaconTest.java b/lib/src/test/java/org/altbeacon/beacon/CBeaconTest.java new file mode 100644 index 000000000..c1fbbe7cb --- /dev/null +++ b/lib/src/test/java/org/altbeacon/beacon/CBeaconTest.java @@ -0,0 +1,114 @@ +package org.altbeacon.beacon; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.os.Parcel; +import android.util.Log; + +import org.altbeacon.beacon.logging.LogManager; +import org.altbeacon.beacon.logging.Loggers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; + +@Config(sdk = 28) + +/** + * Created by dyoung on 4/19/20. + */ +@RunWith(RobolectricTestRunner.class) +public class CBeaconTest { + + @Test + public void testDetectsCBeacon() { + org.robolectric.shadows.ShadowLog.stream = System.err; + byte[] bytes = hexStringToByteArray("02010603036ffd15166ffd0102030405060708090a0b0c0d0e0f100000000000000000000000000000000000000000000000000000000000000000"); + BeaconParser parser = new BeaconParser(); + parser.setBeaconLayout("s:0-1=fd6f,p:0-0:63,i:2-17"); + Beacon beacon = parser.fromScanData(bytes, -55, null, 0l); + assertNotNull("CBeacon should be not null if parsed successfully", beacon); + assertEquals("id should be parsed", "01020304-0506-0708-090a-0b0c0d0e0f10", beacon.getId1().toString()); + assertEquals("txPower should be parsed", -82, beacon.getTxPower()); + } + + @Test + public void testDetectsCBeaconWithoutPower() { + org.robolectric.shadows.ShadowLog.stream = System.err; + byte[] bytes = hexStringToByteArray("02010603036ffd15166ffd0102030405060708090a0b0c0d0e0f100000000000000000000000000000000000000000000000000000000000000000"); + BeaconParser parser = new BeaconParser(); + parser.setBeaconLayout("s:0-1=fd6f,p:-:-59,i:2-17"); + Beacon beacon = parser.fromScanData(bytes, -55, null, 0l); + assertNotNull("CBeacon should be not null if parsed successfully", beacon); + assertEquals("id should be parsed", "01020304-0506-0708-090a-0b0c0d0e0f10", beacon.getId1().toString()); + assertEquals("txPower should be set to value specified", -59, beacon.getTxPower()); + } + + @Test + public void doesNotDetectManufacturerAdvert() { + LogManager.setLogger(Loggers.verboseLogger()); + org.robolectric.shadows.ShadowLog.stream = System.err; + byte[] bytes = hexStringToByteArray("02011a1bff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c50900"); + BeaconParser parser = new BeaconParser(); + parser.setBeaconLayout("s:0-1=fd6f,p:0-0:63,i:2-17"); + Beacon beacon = parser.fromScanData(bytes, -55, null, 0l); + assertNull("CBeacon should not be parsed", beacon); + } + + //@Test + public void testBeaconAdvertisingBytes() { + org.robolectric.shadows.ShadowLog.stream = System.err; + Context context = RuntimeEnvironment.application; + + Beacon beacon = new Beacon.Builder() + .setId1("01020304-0506-0708-090a-0b0c0d0e0f10") + .build(); + BeaconParser beaconParser = new BeaconParser() + .setBeaconLayout("s:0-1=fd6f,p:-:-59,i:2-17"); + byte[] data = beaconParser.getBeaconAdvertisementData(beacon); + + String byteString = ""; + for (int i = 0; i < data.length; i++) { + byteString += String.format("%02X", data[i]); + byteString += " "; + } + assertEquals("Advertisement bytes should be as expected", "01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 ", byteString); + } + + @Test + public void testBeaconAdvertisingBytesForLegacyFormat() { + org.robolectric.shadows.ShadowLog.stream = System.err; + Context context = RuntimeEnvironment.application; + + Beacon beacon = new Beacon.Builder() + .setId1("01020304-0506-0708-090a-0b0c0d0e0f10") + .build(); + BeaconParser beaconParser = new BeaconParser() + .setBeaconLayout("s:0-1=fd6f,p:0-0:63,i:2-17"); + byte[] data = beaconParser.getBeaconAdvertisementData(beacon); + + String byteString = ""; + for (int i = 0; i < data.length; i++) { + byteString += String.format("%02X", data[i]); + byteString += " "; + } + assertEquals("Advertisement bytes should be as expected", "01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 ", byteString); + } + + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } +} \ No newline at end of file From e1c789ac8d4ab358eee13bf9b482eb430b4f8ad8 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Sun, 19 Apr 2020 15:01:49 -0400 Subject: [PATCH 2/3] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b4225cb..064d7da31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Development +- Make BeaconParser more flexible so as to support covid beacon proposal (#965, David G. Young) - Add timestamps of precsely when first and last packet was detected for beacon (#956, Rémi Latapy) ### 2.16.4 / 2020-01-26 From 6272bd21ba7f8516a444bdf1cb2d369952817cba Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Fri, 24 Apr 2020 15:26:09 -0400 Subject: [PATCH 3/3] update changelog for release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 064d7da31..88152c9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### Development +### 2.17 / 2020-04-19 - Make BeaconParser more flexible so as to support covid beacon proposal (#965, David G. Young) - Add timestamps of precsely when first and last packet was detected for beacon (#956, Rémi Latapy)