Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add path loss distance calculation algorithm #251

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
37 changes: 37 additions & 0 deletions src/main/java/org/altbeacon/beacon/Beacon.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ public class Beacon implements Parcelable {
*/
protected int mBeaconTypeCode;

/**
* A count of the number of advertisements detected
*/
protected long mAdvertisementCount;

/**
* A two byte code indicating the beacon manufacturer. A list of registered manufacturer codes
* may be found here:
Expand Down Expand Up @@ -223,6 +228,10 @@ protected Beacon(Parcel in) {
}
mManufacturer = in.readInt();
mBluetoothName = in.readString();
// This nonsense is needed because we can't do readDouble on null values.
// See: http://stackoverflow.com/a/10769887/1461050
mRunningAverageRssi = (Double) in.readValue(Double.class.getClassLoader());
mAdvertisementCount = in.readLong();
}

/**
Expand Down Expand Up @@ -262,6 +271,18 @@ public void setRunningAverageRssi(double rssi) {
mDistance = null; // force calculation of accuracy and proximity next time they are requested
}

/**
* Gets the running average rssi, if available, otherwise get the latest rssi
*/
public double getRunningAverageRssi() {
if (mRunningAverageRssi != null) {
return mRunningAverageRssi;
}
else {
return getRssi();
}
}

/**
* Sets the most recently measured rssi for use in distance calculations if a running average is
* not available
Expand Down Expand Up @@ -367,6 +388,20 @@ public List<Identifier> getIdentifiers() {
}
}

/**
* @see #mAdvertisementCount
*/
public long getAdvertisementCount() {
return mAdvertisementCount;
}

/**
* @see #mAdvertisementCount
*/
public void setAdvertisementCount(long advertisementCount) {
mAdvertisementCount = advertisementCount;
}


/**
* Provides a calculated estimate of the distance to the beacon based on a running average of
Expand Down Expand Up @@ -526,6 +561,8 @@ public void writeToParcel(Parcel out, int flags) {
}
out.writeInt(mManufacturer);
out.writeString(mBluetoothName);
out.writeValue(mRunningAverageRssi);
out.writeLong(mAdvertisementCount);

}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/altbeacon/beacon/BeaconManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ public static void logDebug(String tag, String message, Throwable t) {

protected static BeaconSimulator beaconSimulator;

protected static String distanceModelUpdateUrl = "http://data.altbeacon.org/android-distance.json";
protected static String distanceModelUpdateUrl = "http://data.altbeacon.org/android-distance-r2.json";

public static String getDistanceModelUpdateUrl() {
return distanceModelUpdateUrl;
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/org/altbeacon/beacon/distance/AndroidModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,56 @@ public int matchScore(AndroidModel otherModel) {
return score;
}

/**
* Calculates a qualitative match score between two different Android device models for the
* purposes of how likely they are to have similar Bluetooth signal level responses
* This algorithm takes into account partial matches of model strings, as Samsung devices
* commonly have different model name suffixes
* @param otherModel
* @return match quality, higher numbers are a better match
*/
public double matchScoreWithPartialModel(AndroidModel otherModel) {
double score = 0;
if (this.mManufacturer.equalsIgnoreCase(otherModel.mManufacturer)) {
score = 1;
}
if (score == 1 ) {
score = 1+ratioOfMatchingPrefixCharacters(this.getModel(), otherModel.getModel());
}
LogManager.d(TAG, "Score is %s for %s compared to %s", score, toString(), otherModel);
return score;
}

/**
* Returns 1.0 if the string is a complete match, 0.0 if the first characters are different
* and 0.5 if the first halves of each string match. Not case sensitive.
* @param string1
* @param string2
* @return
*/
private double ratioOfMatchingPrefixCharacters(String string1, String string2) {
int maxLength = 0;
int minLength = 0;
int matchingChars = 0;
String lower1 = string1.toLowerCase();
String lower2 = string2.toLowerCase();
if (string2.length() >= string1.length()) {
maxLength = string2.length();
minLength = string1.length();
}
else {
maxLength = string1.length();
minLength = string2.length();
}

for (int i = 0; i < minLength; i++) {
if (lower1.charAt(i) == lower2.charAt(i)) {
matchingChars++;
}
}
return 1.0*matchingChars/maxLength;
}

@Override
public String toString() {
return ""+mManufacturer+";"+mModel+";"+mBuildNumber+";"+mVersion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@
*/
public class ModelSpecificDistanceCalculator implements DistanceCalculator {
Map<AndroidModel,DistanceCalculator> mModelMap;
private static final String CONFIG_FILE = "model-distance-calculations.json";
private static final String CONFIG_FILE = "model-distance-calculations-r2.json";
private static final String TAG = "ModelSpecificDistanceCalculator";
private AndroidModel mDefaultModel;
private DistanceCalculator mDistanceCalculator;
private AndroidModel mModel;
private AndroidModel mRequestedModel;
private String mRemoteUpdateUrlString = null;
private Context mContext;
private static Class sCalculatorClass = CurveFittedDistanceCalculator.class;
private final ReentrantLock mLock = new ReentrantLock();

/**
Expand All @@ -64,11 +65,20 @@ public ModelSpecificDistanceCalculator(Context context, String remoteUpdateUrlSt
this(context, remoteUpdateUrlString, AndroidModel.forThisDevice());
}

/**
* Configures the distance calculator to be used
* @param klass
*/
public static void setDistanceCalculatorClass(Class klass) {
sCalculatorClass = klass;
}

/**
* Obtains the best possible <code>DistanceCalculator</code> for the Android device passed
* as an argument
*/
public ModelSpecificDistanceCalculator(Context context, String remoteUpdateUrlString, AndroidModel model) {
LogManager.i(TAG, "Constructing model distance database");
mRequestedModel = model;
mRemoteUpdateUrlString = remoteUpdateUrlString;
mContext = context;
Expand Down Expand Up @@ -109,26 +119,26 @@ DistanceCalculator findCalculatorForModelWithLock(AndroidModel model) {
}

private DistanceCalculator findCalculatorForModel(AndroidModel model) {
LogManager.d(TAG, "Finding best distance calculator for %s, %s, %s, %s",
LogManager.i(TAG, "Finding best distance calculator for %s, %s, %s, %s",
model.getVersion(), model.getBuildNumber(), model.getModel(),
model.getManufacturer());

if (mModelMap == null) {
LogManager.d(TAG, "Cannot get distance calculator because modelMap was never initialized");
LogManager.e(TAG, "Cannot get distance calculator because modelMap was never initialized");
return null;
}

int highestScore = 0;
double highestScore = 0;
AndroidModel bestMatchingModel = null;
for (AndroidModel candidateModel : mModelMap.keySet()) {
if (candidateModel.matchScore(model) > highestScore) {
highestScore = candidateModel.matchScore(model);
if (candidateModel.matchScoreWithPartialModel(model) > highestScore) {
highestScore = candidateModel.matchScoreWithPartialModel(model);
bestMatchingModel = candidateModel;
}
}
if (bestMatchingModel != null) {
LogManager.d(TAG, "found a match with score %s", highestScore);
LogManager.d(TAG, "Finding best distance calculator for %s, %s, %s, %s",
LogManager.i(TAG, "Using best match distance calculator for %s, %s, %s, %s",
bestMatchingModel.getVersion(), bestMatchingModel.getBuildNumber(),
bestMatchingModel.getModel(), bestMatchingModel.getManufacturer());
mModel = bestMatchingModel;
Expand Down Expand Up @@ -274,21 +284,42 @@ private void buildModelMap(String jsonString) throws JSONException {
if (modelObject.has("default")) {
defaultFlag = modelObject.getBoolean("default");
}
Double coefficient1 = modelObject.getDouble("coefficient1");
Double coefficient2 = modelObject.getDouble("coefficient2");
Double coefficient3 = modelObject.getDouble("coefficient3");

String version = modelObject.getString("version");
String buildNumber = modelObject.getString("build_number");
String model = modelObject.getString("model");
String manufacturer = modelObject.getString("manufacturer");
AndroidModel androidModel = new AndroidModel(version, buildNumber, model, manufacturer);

CurveFittedDistanceCalculator distanceCalculator =
new CurveFittedDistanceCalculator(coefficient1,coefficient2,coefficient3);
DistanceCalculator distanceCalculator = null;
if (sCalculatorClass.equals(CurveFittedDistanceCalculator.class)) {
Double coefficient1 = modelObject.optDouble("coefficient1");
Double coefficient2 = modelObject.optDouble("coefficient2");
Double coefficient3 = modelObject.optDouble("coefficient3");

AndroidModel androidModel = new AndroidModel(version, buildNumber, model, manufacturer);
mModelMap.put(androidModel, distanceCalculator);
if (defaultFlag) {
mDefaultModel = androidModel;
if (!coefficient1.isNaN() && !coefficient2.isNaN() && !coefficient3.isNaN()) {
distanceCalculator =
new CurveFittedDistanceCalculator(coefficient1,coefficient2,coefficient3);
}
}
else if (sCalculatorClass.equals(PathLossDistanceCalculator.class)) {
Double receiverRssiOffset = modelObject.optDouble("receiver_rssi_offset");
Double receiverRssiSlope = modelObject.optDouble("receiver_rssi_slope");
if (!receiverRssiOffset.isNaN() && !receiverRssiSlope.isNaN()) {
distanceCalculator =
new PathLossDistanceCalculator(receiverRssiSlope, receiverRssiOffset);
}
}

if (distanceCalculator != null) {
mModelMap.put(androidModel, distanceCalculator);
if (defaultFlag) {
mDefaultModel = androidModel;
}
}
else {
LogManager.w(TAG, "No distance calculator may be constructed for model "+androidModel+
" because data are missing for configured calculator "+sCalculatorClass.getName());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.altbeacon.beacon.distance;

import org.altbeacon.beacon.logging.LogManager;

/**
* This class estimates the distance between the mobile device and a BLE beacon based on the measured
* RSSI and a txPower calibration value that represents the expected RSSI for an iPhone 5 receiving
* the signal when it is 1 meter away.
* <p/>
* This class uses a path loss equation with receiverRssiSlope and receiverRssiOffset parameter.
* The offset must be supplied by the caller and is specific to the Android device being used.
* See the <code>ModelSpecificDistanceCalculator</code> for more information on the offset.
* <p/>
* Created by dyoung on 7/26/15.
*/
public class PathLossDistanceCalculator implements DistanceCalculator {

public static final String TAG = "PathLossDistanceCalculator";
private double mReceiverRssiSlope;
private double mReceiverRssiOffset;

/**
* Construct a calculator with an offset specific for the device's antenna gain
*
* @param receiverRssiOffset
*/
public PathLossDistanceCalculator(double receiverRssiSlope, double receiverRssiOffset) {
mReceiverRssiSlope = receiverRssiSlope;
mReceiverRssiOffset = receiverRssiOffset;
}

/**
* Calculated the estimated distance in meters to the beacon based on a reference rssi at 1m
* and the known actual rssi at the current location
*
* @param txPower
* @param rssi
* @return estimated distance
*/
@Override
public double calculateDistance(int txPower, double rssi) {
if (rssi == 0) {
return -1.0; // if we cannot determine accuracy, return -1.
}

LogManager.d(TAG, "calculating distance based on mRssi of %s and txPower of %s", rssi, txPower);


double ratio = rssi * 1.0 / txPower;
double distance;
if (ratio < 1.0) {
distance = Math.pow(ratio, 10);
} else {
double adjustment = +mReceiverRssiSlope*rssi+mReceiverRssiOffset;
double adjustedRssi = rssi-adjustment;
System.out.println("Adjusting rssi by "+adjustment+" when rssi is "+rssi);
System.out.println("Adjusted rssi is now "+adjustedRssi);
distance = Math.pow(10.0, ((-adjustedRssi+txPower)/10*0.35));
}
LogManager.d(TAG, "avg mRssi: %s distance: %s", rssi, distance);
return distance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ protected Void doInBackground(ScanData... params) {
}
}
if (beacon != null) {
android.util.Log.d(TAG, "brssi, "+(new java.util.Date().getTime())+", "+scanData.rssi);
mDetectionTracker.recordDetection();
processBeaconFromScan(beacon);
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/altbeacon/beacon/service/RangedBeacon.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class RangedBeacon {
//kept here for backward compatibility
public static final long DEFAULT_SAMPLE_EXPIRATION_MILLISECONDS = 20000; /* 20 seconds */
private static long sampleExpirationMilliseconds = DEFAULT_SAMPLE_EXPIRATION_MILLISECONDS;
private static long mAdvertisementCount = 0;
private boolean mTracked = true;
protected long lastTrackedTimeMillis = 0;
Beacon mBeacon;
Expand Down Expand Up @@ -62,6 +63,10 @@ public void commitMeasurements() {
}

public void addMeasurement(Integer rssi) {
if (mAdvertisementCount < Long.MAX_VALUE) {
mAdvertisementCount++;
}
mBeacon.setAdvertisementCount(mAdvertisementCount);
// Filter out unreasonable values per
// http://stackoverflow.com/questions/30118991/rssi-returned-by-altbeacon-library-127-messes-up-distance
if (rssi != 127) {
Expand Down
Loading