package org.altbeacon.beacon;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothDevice;
import android.os.Build;
import android.util.Log;
import org.altbeacon.beacon.logging.LogManager;
import org.altbeacon.bluetooth.BleAdvertisement;
import org.altbeacon.bluetooth.Pdu;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by dyoung on 7/21/14.
*
* <p>A <code>BeaconParser</code> may be used to tell the library how to decode a beacon's fields
* from a Bluetooth LE advertisement by specifying what byte offsets match what fields, and what
* byte sequence signifies the beacon. Defining a parser for a specific beacon type may be handled
* via subclassing ({@link AltBeaconParser see AltBeaconParser}) or by simply constructing an instance and calling the
* <code>setLayout</code> method. Either way, you will then need to tell the BeaconManager about
* it like so:</p>
*
* <pre><code>
* BeaconManager.getBeaconParsers().add(new BeaconParser()
* .setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"));
* </pre></code>
*
* <p>
* For more information on how to set up parsing of a beacon,
* {@link #setBeaconLayout(String) see setBeaconLayout(String)}
* </p>
*
*/
public class BeaconParser implements Serializable {
private static final String TAG = "BeaconParser";
public static final String ALTBEACON_LAYOUT = "m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25";
public static final String EDDYSTONE_TLM_LAYOUT = "x,s:0-1=feaa,m:2-2=20,d:3-3,d:4-5,d:6-7,d:8-11,d:12-15";
public static final String EDDYSTONE_UID_LAYOUT = "s:0-1=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19";
public static final String EDDYSTONE_URL_LAYOUT = "s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-21v";
public static final String URI_BEACON_LAYOUT = "s:0-1=fed8,m:2-2=00,p:3-3:-41,i:4-21v";
private static final Pattern I_PATTERN = Pattern.compile("i\\:(\\d+)\\-(\\d+)([blv]*)?");
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 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";
private static final String VARIABLE_LENGTH_SUFFIX = "v";
protected String mBeaconLayout;
private Long mMatchingBeaconTypeCode;
protected final List<Integer> mIdentifierStartOffsets = new ArrayList<Integer>();
protected final List<Integer> mIdentifierEndOffsets = new ArrayList<Integer>();
protected final List<Boolean> mIdentifierLittleEndianFlags = new ArrayList<Boolean>();
protected final List<Integer> mDataStartOffsets = new ArrayList<Integer>();
protected final List<Integer> mDataEndOffsets = new ArrayList<Integer>();
protected final List<Boolean> mDataLittleEndianFlags = new ArrayList<Boolean>();
protected final List<Boolean> mIdentifierVariableLengthFlags = new ArrayList<Boolean>();
protected Integer mMatchingBeaconTypeCodeStartOffset;
protected Integer mMatchingBeaconTypeCodeEndOffset;
protected Integer mServiceUuidStartOffset;
protected Integer mServiceUuidEndOffset;
protected Long mServiceUuid;
protected Boolean mExtraFrame;
protected Integer mPowerStartOffset;
protected Integer mPowerEndOffset;
protected Integer mDBmCorrection;
protected Integer mLayoutSize;
protected Boolean mAllowPduOverflow = true;
protected String mIdentifier;
protected int[] mHardwareAssistManufacturers = new int[] { 0x004c };
protected List<BeaconParser> extraParsers = new ArrayList<BeaconParser>();
/**
* Makes a new BeaconParser. Should normally be immediately followed by a call to #setLayout
*/
public BeaconParser() {
}
/**
* Makes a new BeaconParser with an identifier that can be used to identify beacons decoded with
* this parser
*/
public BeaconParser(String identifier) {
mIdentifier = identifier;
}
/**
* <p>Defines a beacon field parsing algorithm based on a string designating the zero-indexed
* offsets to bytes within a BLE advertisement.</p>
*
* <p>If you want to see examples of how other folks have set up BeaconParsers for different
* kinds of beacons, try doing a Google search for "getBeaconParsers" (include the quotes in
* the search.)</p>
*
* <p>Four prefixes are allowed in the string:</p>
*
* <pre>
* m - matching byte sequence for this beacon type to parse (exactly one required)
* s - ServiceUuid for this beacon type to parse (optional, only for Gatt-based beacons)
* i - identifier (at least one required, multiple allowed)
* p - power calibration field (exactly one required)
* d - data field (optional, multiple allowed)
* x - extra layout. Signifies that the layout is secondary to a primary layout with the same
* matching byte sequence (or ServiceUuid). Extra layouts do not require power or
* identifier fields and create Beacon objects without identifiers.
* </pre>
*
* <p>Each prefix is followed by a colon, then an inclusive decimal byte offset for the field from
* the beginning of the advertisement. In the case of the m prefix, an = sign follows the byte
* offset, followed by a big endian hex representation of the bytes that must be matched for
* this beacon type. When multiple i or d entries exist in the string, they will be added in
* order of definition to the identifier or data array for the beacon when parsing the beacon
* advertisement. Terms are separated by commas.</p>
*
* <p>All offsets from the start of the advertisement are relative to the first byte of the
* two byte manufacturer code. The manufacturer code is therefore always at position 0-1</p>
*
* <p>All data field and identifier expressions may be optionally suffixed with the letter l, which
* indicates the field should be parsed as little endian. If not present, the field will be presumed
* to be big endian. Note: serviceUuid fields are always little endian.
*
* <p>Identifier fields may be optionally suffixed with the letter v, which
* indicates the field is variable length, and may be shorter than the declared length if the
* parsed PDU for the advertisement is shorter than needed to parse the full identifier.
*
* <p>If the expression cannot be parsed, a <code>BeaconLayoutException</code> is thrown.</p>
*
* <p>Example of a parser string for AltBeacon:</p>
*
* </pre>
* "m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"
* </pre>
*
* <p>This signifies that the beacon type will be decoded when an advertisement is found with
* 0xbeac in bytes 2-3, and a three-part identifier will be pulled out of bytes 4-19, bytes
* 20-21 and bytes 22-23, respectively. A signed power calibration value will be pulled out of
* byte 24, and a data field will be pulled out of byte 25.</p>
*
* Note: bytes 0-1 of the BLE manufacturer advertisements are the two byte manufacturer code.
* Generally you should not match on these two bytes when using a BeaconParser, because it will
* limit your parser to matching only a transmitter made by a specific manufacturer. Software
* and operating systems that scan for beacons typically ignore these two bytes, allowing beacon
* manufacturers to use their own company code assigned by Bluetooth SIG. The default parser
* implementation will already pull out this company code and store it in the
* beacon.mManufacturer field. Matcher expressions should therefore start with "m2-3:" followed
* by the multi-byte hex value that signifies the beacon type.
*
* <p>
* Extra layouts can also be added by using:
* @see #addExtraDataParser(BeaconParser)
* This is the preferred method and matching BeaconLayouts by serviceUUID will be deprecated in
* the future.
* </p>
*
* @param beaconLayout
* @return the BeaconParser instance
*/
public BeaconParser setBeaconLayout(String beaconLayout) {
mBeaconLayout = beaconLayout;
Log.d(TAG, "Parsing beacon layout: "+beaconLayout);
String[] terms = beaconLayout.split(",");
mExtraFrame = false; // this is not an extra frame by default
for (String term : terms) {
boolean found = false;
Matcher matcher = I_PATTERN.matcher(term);
while (matcher.find()) {
found = true;
try {
int startOffset = Integer.parseInt(matcher.group(1));
int endOffset = Integer.parseInt(matcher.group(2));
Boolean littleEndian = matcher.group(3).contains(LITTLE_ENDIAN_SUFFIX);
mIdentifierLittleEndianFlags.add(littleEndian);
Boolean variableLength = matcher.group(3).contains(VARIABLE_LENGTH_SUFFIX);
mIdentifierVariableLengthFlags.add(variableLength);
mIdentifierStartOffsets.add(startOffset);
mIdentifierEndOffsets.add(endOffset);
} catch (NumberFormatException e) {
throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
}
}
matcher = D_PATTERN.matcher(term);
while (matcher.find()) {
found = true;
try {
int startOffset = Integer.parseInt(matcher.group(1));
int endOffset = Integer.parseInt(matcher.group(2));
Boolean littleEndian = matcher.group(3).contains("l");
mDataLittleEndianFlags.add(littleEndian);
mDataStartOffsets.add(startOffset);
mDataEndOffsets.add(endOffset);
} catch (NumberFormatException e) {
throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
}
}
matcher = P_PATTERN.matcher(term);
while (matcher.find()) {
found = true;
try {
int startOffset = Integer.parseInt(matcher.group(1));
int endOffset = Integer.parseInt(matcher.group(2));
int dBmCorrection = 0;
if (matcher.group(3) != null) {
dBmCorrection = Integer.parseInt(matcher.group(3));
}
mDBmCorrection=dBmCorrection;
mPowerStartOffset=startOffset;
mPowerEndOffset=endOffset;
} catch (NumberFormatException e) {
throw new BeaconLayoutException("Cannot parse integer power byte offset in term: " + term);
}
}
matcher = M_PATTERN.matcher(term);
while (matcher.find()) {
found = true;
try {
int startOffset = Integer.parseInt(matcher.group(1));
int endOffset = Integer.parseInt(matcher.group(2));
mMatchingBeaconTypeCodeStartOffset = startOffset;
mMatchingBeaconTypeCodeEndOffset = endOffset;
} catch (NumberFormatException e) {
throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
}
String hexString = matcher.group(3);
try {
mMatchingBeaconTypeCode = Long.decode("0x"+hexString);
}
catch (NumberFormatException e) {
throw new BeaconLayoutException("Cannot parse beacon type code: "+hexString+" in term: " + term);
}
}
matcher = S_PATTERN.matcher(term);
while (matcher.find()) {
found = true;
try {
int startOffset = Integer.parseInt(matcher.group(1));
int endOffset = Integer.parseInt(matcher.group(2));
mServiceUuidStartOffset = startOffset;
mServiceUuidEndOffset = endOffset;
} catch (NumberFormatException e) {
throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
}
String hexString = matcher.group(3);
try {
mServiceUuid = Long.decode("0x"+hexString);
}
catch (NumberFormatException e) {
throw new BeaconLayoutException("Cannot parse serviceUuid: "+hexString+" in term: " + term);
}
}
matcher = X_PATTERN.matcher(term);
while (matcher.find()) {
found = true;
mExtraFrame = true;
}
if (!found) {
LogManager.d(TAG, "cannot parse term %s", term);
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;
}
/**
* Adds a <code>BeaconParser</code> used for parsing extra BLE beacon advertisement packets for
* beacons that send multiple different advertisement packets (for example, Eddystone-TLM)
*
* @param extraDataParser a parser that must be configured with an "extra layout" prefix
* @return true when the extra parser is added successfully
*/
public boolean addExtraDataParser(BeaconParser extraDataParser) {
//add an extra data parser only if it is not null and it is an extra frame parser
return extraDataParser != null && extraDataParser.mExtraFrame && extraParsers.add(extraDataParser);
}
/**
* Gets a list of extra parsers configured for this <code>BeaconParser</code>.
*
* @see #addExtraDataParser
* @see #setBeaconLayout
* @return
*/
public List<BeaconParser> getExtraDataParsers() {
return new ArrayList<>(extraParsers);
}
/**
* Gets an optional identifier field that may be used to identify this parser. If set, it will
* be passed along to any beacons decoded with this parser.
* @return
*/
public String getIdentifier() {
return mIdentifier;
}
/**
* Returns a list of Bluetooth manufacturer codes which will be used for hardware-assisted
* accelerated looking for this beacon type
*
* The possible codes are defined on this list:
* https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
*
* @return manufacturers
*/
public int[] getHardwareAssistManufacturers() {
return mHardwareAssistManufacturers;
}
/**
* Sets a list of Bluetooth manufacturer codes which will be used for hardware-assisted
* accelerated looking for this beacon type
*
* The possible codes are defined on this list:
* https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
*
*/
public void setHardwareAssistManufacturerCodes(int[] manufacturers) {
mHardwareAssistManufacturers = manufacturers;
}
/**
* Setting to true indicates that packets should be rejected if the PDU length is too short for
* the fields. Some beacons transmit malformed PDU packets that understate their length, so
* this defaults to false.
* @param enabled
*/
public void setAllowPduOverflow(Boolean enabled) {
mAllowPduOverflow = enabled;
}
/**
* @see #mMatchingBeaconTypeCode
* @return
*/
public Long getMatchingBeaconTypeCode() {
return mMatchingBeaconTypeCode;
}
/**
* see #mMatchingBeaconTypeCodeStartOffset
* @return
*/
public int getMatchingBeaconTypeCodeStartOffset() {
return mMatchingBeaconTypeCodeStartOffset;
}
/**
* see #mMatchingBeaconTypeCodeEndOffset
* @return
*/
public int getMatchingBeaconTypeCodeEndOffset() {
return mMatchingBeaconTypeCodeEndOffset;
}
/**
* @see #mServiceUuid
* @return
*/
public Long getServiceUuid() {
return mServiceUuid;
}
/**
* see #mServiceUuidStartOffset
* @return
*/
public int getMServiceUuidStartOffset() {
return mServiceUuidStartOffset;
}
/**
* see #mServiceUuidEndOffset
* @return
*/
public int getServiceUuidEndOffset() {
return mServiceUuidEndOffset;
}
/**
* Construct a Beacon from a Bluetooth LE packet collected by Android's Bluetooth APIs,
* including the raw Bluetooth device info
*
* @param scanData The actual packet bytes
* @param rssi The measured signal strength of the packet
* @param device The Bluetooth device that was detected
* @return An instance of a <code>Beacon</code>
*/
public Beacon fromScanData(byte[] scanData, int rssi, BluetoothDevice device) {
return fromScanData(scanData, rssi, device, new Beacon());
}
protected Beacon fromScanData(byte[] bytesToProcess, int rssi, BluetoothDevice device, Beacon beacon) {
BleAdvertisement advert = new BleAdvertisement(bytesToProcess);
boolean parseFailed = false;
Pdu pduToParse = null;
int startByte = 0;
ArrayList<Identifier> identifiers = new ArrayList<Identifier>();
ArrayList<Long> dataFields = new ArrayList<Long>();
for (Pdu pdu: advert.getPdus()) {
if (pdu.getType() == Pdu.GATT_SERVICE_UUID_PDU_TYPE ||
pdu.getType() == Pdu.MANUFACTURER_DATA_PDU_TYPE) {
pduToParse = pdu;
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Processing pdu type %02X: %s with startIndex: %d, endIndex: %d", pdu.getType(), bytesToHex(bytesToProcess), pdu.getStartIndex(), pdu.getEndIndex());
}
break;
}
else {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Ignoring pdu type %02X", pdu.getType());
}
}
}
if (pduToParse == null) {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "No PDUs to process in this packet.");
}
parseFailed = true;
}
else {
byte[] serviceUuidBytes = null;
byte[] 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;
}
} else {
if (byteArraysMatch(bytesToProcess, startByte + mServiceUuidStartOffset, serviceUuidBytes) &&
byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) {
patternFound = true;
}
}
if (patternFound == false) {
// This is not a beacon
if (getServiceUuid() == null) {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "This is not a matching Beacon advertisement. (Was expecting %s. "
+ "The bytes I see are: %s", byteArrayToString(typeCodeBytes),
bytesToHex(bytesToProcess));
}
} else {
if (LogManager.isVerboseLoggingEnabled()) {
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,
bytesToHex(bytesToProcess));
}
}
parseFailed = true;
beacon = null;
} else {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "This is a recognized beacon advertisement -- %s seen",
byteArrayToString(typeCodeBytes));
LogManager.d(TAG, "Bytes are: %s", bytesToHex(bytesToProcess));
}
}
if (patternFound) {
if (bytesToProcess.length <= startByte+mLayoutSize && mAllowPduOverflow) {
// If the layout size is bigger than this PDU, and we allow overflow. Make sure
// the byte buffer is big enough by zero padding the end so we don't try to read
// outside the byte array of the advertisement
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Expanding buffer because it is too short to parse: "+bytesToProcess.length+", needed: "+(startByte+mLayoutSize));
}
bytesToProcess = ensureMaxSize(bytesToProcess, startByte+mLayoutSize);
}
for (int i = 0; i < mIdentifierEndOffsets.size(); i++) {
int endIndex = mIdentifierEndOffsets.get(i) + startByte;
if (endIndex > pduToParse.getEndIndex() && mIdentifierVariableLengthFlags.get(i)) {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Need to truncate identifier by "+(endIndex-pduToParse.getEndIndex()));
}
// If this is a variable length identifier, we truncate it to the size that
// is available in the packet
Identifier identifier = Identifier.fromBytes(bytesToProcess, mIdentifierStartOffsets.get(i) + startByte, pduToParse.getEndIndex()+1, mIdentifierLittleEndianFlags.get(i));
identifiers.add(identifier);
}
else if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
parseFailed = true;
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Cannot parse identifier "+i+" because PDU is too short. endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex());
}
}
else {
Identifier identifier = Identifier.fromBytes(bytesToProcess, mIdentifierStartOffsets.get(i) + startByte, endIndex+1, mIdentifierLittleEndianFlags.get(i));
identifiers.add(identifier);
}
}
for (int i = 0; i < mDataEndOffsets.size(); i++) {
int endIndex = mDataEndOffsets.get(i) + startByte;
if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Cannot parse data field "+i+" because PDU is too short. endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex()+". Setting value to 0");
}
dataFields.add(new Long(0l));
}
else {
String dataString = byteArrayToFormattedString(bytesToProcess, mDataStartOffsets.get(i) + startByte, endIndex, mDataLittleEndianFlags.get(i));
dataFields.add(Long.decode(dataString));
}
}
if (mPowerStartOffset != null) {
int endIndex = mPowerEndOffset + startByte;
int txPower = 0;
try {
if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
parseFailed = true;
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Cannot parse power field because PDU is too short. endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex());
}
}
else {
String powerString = byteArrayToFormattedString(bytesToProcess, mPowerStartOffset + startByte, mPowerEndOffset + startByte, false);
txPower = Integer.parseInt(powerString)+mDBmCorrection;
// make sure it is a signed integer
if (txPower > 127) {
txPower -= 256;
}
beacon.mTxPower = txPower;
}
}
catch (NumberFormatException e1) {
// keep default value
}
catch (NullPointerException e2) {
// keep default value
}
}
}
}
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 manufacturer = 0;
String manufacturerString = byteArrayToFormattedString(bytesToProcess, startByte, startByte+1, true);
manufacturer = Integer.parseInt(manufacturerString);
String macAddress = null;
String name = null;
if (device != null) {
macAddress = device.getAddress();
name = device.getName();
}
beacon.mIdentifiers = identifiers;
beacon.mDataFields = dataFields;
beacon.mRssi = rssi;
beacon.mBeaconTypeCode = beaconTypeCode;
if (mServiceUuid != null) {
beacon.mServiceUuid = (int) mServiceUuid.longValue();
}
else {
beacon.mServiceUuid = -1;
}
beacon.mBluetoothAddress = macAddress;
beacon.mBluetoothName= name;
beacon.mManufacturer = manufacturer;
beacon.mParserIdentifier = mIdentifier;
beacon.mMultiFrameBeacon = extraParsers.size() > 0 || mExtraFrame;
}
return beacon;
}
/**
* Get BLE advertisement bytes for a Beacon
* @param beacon the beacon containing the data to be transmitted
* @return the byte array of the advertisement
*/
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public byte[] getBeaconAdvertisementData(Beacon beacon) {
byte[] advertisingBytes;
if (beacon.getIdentifiers().size() != getIdentifierCount()) {
throw new IllegalArgumentException("Beacon has "+beacon.getIdentifiers().size()+" identifiers but format requires "+getIdentifierCount());
}
int lastIndex = -1;
if (mMatchingBeaconTypeCodeEndOffset != null && mMatchingBeaconTypeCodeEndOffset > lastIndex) {
lastIndex = mMatchingBeaconTypeCodeEndOffset;
}
if (mPowerEndOffset != null && mPowerEndOffset > lastIndex) {
lastIndex = mPowerEndOffset;
}
for (int identifierNum = 0; identifierNum < this.mIdentifierEndOffsets.size(); identifierNum++) {
if (this.mIdentifierEndOffsets.get(identifierNum) != null && this.mIdentifierEndOffsets.get(identifierNum) > lastIndex) {
lastIndex = this.mIdentifierEndOffsets.get(identifierNum);
}
}
for (int identifierNum = 0; identifierNum < this.mDataEndOffsets.size(); identifierNum++) {
if (this.mDataEndOffsets.get(identifierNum) != null && this.mDataEndOffsets.get(identifierNum) > lastIndex) {
lastIndex = this.mDataEndOffsets.get(identifierNum);
}
}
// we must adjust the lastIndex to account for variable length identifiers, if there are any.
int adjustedIdentifiersLength = 0;
for (int identifierNum = 0; identifierNum < this.mIdentifierStartOffsets.size(); identifierNum++) {
if (mIdentifierVariableLengthFlags.get(identifierNum)) {
int declaredIdentifierLength = (this.mIdentifierEndOffsets.get(identifierNum) - this.mIdentifierStartOffsets.get(identifierNum)+1);
int actualIdentifierLength = beacon.getIdentifier(identifierNum).getByteCount();
adjustedIdentifiersLength += actualIdentifierLength;
adjustedIdentifiersLength -= declaredIdentifierLength;
}
}
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;
}
// set identifiers
for (int identifierNum = 0; identifierNum < this.mIdentifierStartOffsets.size(); identifierNum++) {
byte[] identifierBytes = beacon.getIdentifier(identifierNum).toByteArrayOfSpecifiedEndianness(!this.mIdentifierLittleEndianFlags.get(identifierNum));
// If the identifier we are trying to stuff into the space is different than the space available
// adjust it
if (identifierBytes.length < getIdentifierByteCount(identifierNum)) {
if (!mIdentifierVariableLengthFlags.get(identifierNum)) {
// Pad it, but only if this is not a variable length identifier
if (mIdentifierLittleEndianFlags.get(identifierNum)) {
// this is little endian. Pad at the end of the array
identifierBytes = Arrays.copyOf(identifierBytes,getIdentifierByteCount(identifierNum));
}
else {
// this is big endian. Pad at the beginning of the array
byte[] newIdentifierBytes = new byte[getIdentifierByteCount(identifierNum)];
System.arraycopy(identifierBytes, 0, newIdentifierBytes, getIdentifierByteCount(identifierNum)-identifierBytes.length, identifierBytes.length);
identifierBytes = newIdentifierBytes;
}
}
LogManager.d(TAG, "Expanded identifier because it is too short. It is now: "+byteArrayToString(identifierBytes));
}
else if (identifierBytes.length > getIdentifierByteCount(identifierNum)) {
if (mIdentifierLittleEndianFlags.get(identifierNum)) {
// Truncate it at the beginning for big endian
identifierBytes = Arrays.copyOfRange(identifierBytes, getIdentifierByteCount(identifierNum)-identifierBytes.length, getIdentifierByteCount(identifierNum));
}
else {
// Truncate it at the end for little endian
identifierBytes = Arrays.copyOf(identifierBytes,getIdentifierByteCount(identifierNum));
}
LogManager.d(TAG, "Truncated identifier because it is too long. It is now: "+byteArrayToString(identifierBytes));
}
else {
LogManager.d(TAG, "Identifier size is just right: "+byteArrayToString(identifierBytes));
}
for (int index = this.mIdentifierStartOffsets.get(identifierNum); index <= this.mIdentifierStartOffsets.get(identifierNum)+identifierBytes.length-1; index ++) {
advertisingBytes[index-2] = (byte) identifierBytes[index-this.mIdentifierStartOffsets.get(identifierNum)];
}
}
// set power
if (this.mPowerStartOffset != null && this.mPowerEndOffset != null) {
for (int index = this.mPowerStartOffset; index <= this.mPowerEndOffset; index ++) {
advertisingBytes[index-2] = (byte) (beacon.getTxPower() >> (8*(index - this.mPowerStartOffset)) & 0xff);
}
}
// set data fields
for (int dataFieldNum = 0; dataFieldNum < this.mDataStartOffsets.size(); dataFieldNum++) {
long dataField = beacon.getDataFields().get(dataFieldNum);
int dataFieldLength = this.mDataEndOffsets.get(dataFieldNum) - this.mDataStartOffsets.get(dataFieldNum);
for (int index = 0; index <= dataFieldLength; index ++) {
int endianCorrectedIndex = index;
if (!this.mDataLittleEndianFlags.get(dataFieldNum)) {
endianCorrectedIndex = dataFieldLength-index;
}
advertisingBytes[this.mDataStartOffsets.get(dataFieldNum)-2+endianCorrectedIndex] = (byte) (dataField >> (8*index) & 0xff);
}
}
return advertisingBytes;
}
public BeaconParser setMatchingBeaconTypeCode(Long typeCode) {
mMatchingBeaconTypeCode = typeCode;
return this;
}
/**
* Caclculates the byte size of the specified identifier in this format
* @param identifierNum
* @return bytes
*/
public int getIdentifierByteCount(int identifierNum) {
return mIdentifierEndOffsets.get(identifierNum) - mIdentifierStartOffsets.get(identifierNum) + 1;
}
/**
* @return the number of identifiers in this beacon format
*/
public int getIdentifierCount() {
return mIdentifierStartOffsets.size();
}
/**
* @return the number of data fields in this beacon format
*/
public int getDataFieldCount() {
return mDataStartOffsets.size();
}
/**
* @return the layout string for the parser
*/
public String getLayout() {
return mBeaconLayout;
}
/**
* @return the correction value in dBm to apply to the calibrated txPower to get a 1m calibrated value.
* Some formats like Eddystone use a 0m calibrated value, which requires this correction
*/
public int getPowerCorrection() { return mDBmCorrection; }
protected static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
int v;
for ( int j = 0; j < bytes.length; j++ ) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
public static class BeaconLayoutException extends RuntimeException {
public BeaconLayoutException(String s) {
super(s);
}
}
public static byte[] longToByteArray(long longValue, int length) {
return longToByteArray(longValue, length, true);
}
public static byte[] longToByteArray(long longValue, int length, boolean bigEndian) {
byte[] array = new byte[length];
for (int i = 0; i < length; i++){
int adjustedI = bigEndian ? i : length - i -1;
long mask = 0xffl << (length-adjustedI-1)*8;
long shift = (length-adjustedI-1)*8;
long value = ((longValue & mask) >> shift);
array[i] = (byte) value;
}
return array;
}
private int calculateLayoutSize() {
int lastEndOffset = 0;
if (mIdentifierEndOffsets != null) {
for (int endOffset : mIdentifierEndOffsets) {
if (endOffset > lastEndOffset) {
lastEndOffset = endOffset;
}
}
}
if (mDataEndOffsets != null) {
for (int endOffset : mDataEndOffsets) {
if (endOffset > lastEndOffset) {
lastEndOffset = endOffset;
}
}
}
if (mPowerEndOffset != null && mPowerEndOffset > lastEndOffset ) {
lastEndOffset = mPowerEndOffset;
}
if (mServiceUuidEndOffset != null && mServiceUuidEndOffset > lastEndOffset) {
lastEndOffset = mServiceUuidEndOffset;
}
return lastEndOffset+1;
}
private boolean byteArraysMatch(byte[] source, int offset, byte[] expected) {
int length = expected.length;
if (source.length - offset < length) {
return false;
}
for (int i = 0; i < length; i++) {
if (source[offset + i] != expected[i]) {
return false;
}
}
return true;
}
private String byteArrayToString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
sb.append(String.format("%02x", bytes[i]));
sb.append(" ");
}
return sb.toString().trim();
}
private String byteArrayToFormattedString(byte[] byteBuffer, int startIndex, int endIndex, boolean littleEndian) {
byte[] bytes = new byte[endIndex-startIndex+1];
if (littleEndian) {
for (int i = 0; i <= endIndex-startIndex; i++) {
bytes[i] = byteBuffer[startIndex+bytes.length-1-i];
}
}
else {
for (int i = 0; i <= endIndex-startIndex; i++) {
bytes[i] = byteBuffer[startIndex+i];
}
}
int length = endIndex-startIndex +1;
// We treat a 1-4 byte number as decimal string
if (length < 5) {
long number = 0l;
for (int i = 0; i < bytes.length; i++) {
long byteValue = (long) (bytes[bytes.length - i-1] & 0xff);
long positionValue = (long) Math.pow(256.0,i*1.0);
long calculatedValue = (byteValue * positionValue);
number += calculatedValue;
}
return Long.toString(number);
}
// We treat a 7+ byte number as a hex string
String hexString = bytesToHex(bytes);
// And if it is a 12 byte number we add dashes to it to make it look like a standard UUID
if (bytes.length == 16) {
StringBuilder sb = new StringBuilder();
sb.append(hexString.substring(0,8));
sb.append("-");
sb.append(hexString.substring(8,12));
sb.append("-");
sb.append(hexString.substring(12,16));
sb.append("-");
sb.append(hexString.substring(16,20));
sb.append("-");
sb.append(hexString.substring(20,32));
return sb.toString();
}
return "0x"+hexString;
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private byte[] ensureMaxSize(byte[] array, int requiredLength) {
if (array.length >= requiredLength) {
return array;
}
return Arrays.copyOf(array, requiredLength);
}
@Override
public int hashCode() {
return Arrays.hashCode(new Object[] {
mMatchingBeaconTypeCode,
mIdentifierStartOffsets,
mIdentifierEndOffsets,
mIdentifierLittleEndianFlags,
mDataStartOffsets,
mDataEndOffsets,
mDataLittleEndianFlags,
mIdentifierVariableLengthFlags,
mMatchingBeaconTypeCodeStartOffset,
mMatchingBeaconTypeCodeEndOffset,
mServiceUuidStartOffset,
mServiceUuidEndOffset,
mServiceUuid,
mExtraFrame,
mPowerStartOffset,
mPowerEndOffset,
mDBmCorrection,
mLayoutSize,
mAllowPduOverflow,
mIdentifier,
mHardwareAssistManufacturers,
extraParsers
}
);
}
@Override
public boolean equals(Object o) {
BeaconParser that = null;
try {
that = (BeaconParser) o;
if (that.mBeaconLayout != null && that.mBeaconLayout.equals(this.mBeaconLayout)) {
if (that.mIdentifier != null && that.mIdentifier.equals(this.mIdentifier)) {
return true;
}
}
}
catch (ClassCastException e ) { }
return false;
}
}