Skip to content

Commit

Permalink
Merge pull request #965 from AltBeacon/add-covid-beacon-support
Browse files Browse the repository at this point in the history
Add support for covid beacon proposal from Apple and Google
  • Loading branch information
davidgyoung authored Apr 24, 2020
2 parents 3d4a050 + 6272bd2 commit 3d8b0e0
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 52 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 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)

### 2.16.4 / 2020-01-26
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
105 changes: 66 additions & 39 deletions lib/src/main/java/org/altbeacon/beacon/BeaconParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -360,6 +354,9 @@ public void setAllowPduOverflow(Boolean enabled) {
* @return
*/
public Long getMatchingBeaconTypeCode() {
if (mMatchingBeaconTypeCode == null) {
return -1l;
}
return mMatchingBeaconTypeCode;
}

Expand All @@ -368,6 +365,9 @@ public Long getMatchingBeaconTypeCode() {
* @return
*/
public int getMatchingBeaconTypeCodeStartOffset() {
if (mMatchingBeaconTypeCodeStartOffset == null) {
return -1;
}
return mMatchingBeaconTypeCodeStartOffset;
}

Expand All @@ -376,6 +376,9 @@ public int getMatchingBeaconTypeCodeStartOffset() {
* @return
*/
public int getMatchingBeaconTypeCodeEndOffset() {
if (mMatchingBeaconTypeCodeEndOffset == null) {
return -1;
}
return mMatchingBeaconTypeCodeEndOffset;
}

Expand Down Expand Up @@ -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;
}
}

}
}

Expand All @@ -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));
}
}
Expand Down Expand Up @@ -578,17 +599,23 @@ else if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
// keep default value
}
}
else {
if (mDBmCorrection != null) {
beacon.mTxPower = mDBmCorrection;
}
}
}
}

if (parseFailed) {
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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,25 @@ public List<ScanFilterData> 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;
Expand Down
114 changes: 114 additions & 0 deletions lib/src/test/java/org/altbeacon/beacon/CBeaconTest.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 3d8b0e0

Please sign in to comment.