You can't get the "Exact Distance" because the RSSI you get is really bad and if the BLE Tag is > 1 meter of distance the RSSI oscillation become really high and more you will increase the distance and more the calculated distance will be wrong.
The RSSI you measure depends on a lot of factors, for example the environment (inside or outside), the kind of tag you are using, the kind of receiver you are using, if there are things between your tag and your receiver, if there are tags near your tag ecc ecc ecc
YOU CAN'T GET A PRECISE MEASURE OF THE DISTANCE USING BLUETOOTH
But you can get better measurement of the distance by implementing some operation to smooth the Gaussian you get (the oscillation of your measurements values).
For example if you know that the tag will be always at the same distance you can do an average of the Rssi values you get. For example you do the average of the Rssi you get in the last 1 minute.
In this way you will get a better distance value, but the tag must stay in same position.
Remember that in the way you are using to calculate the distance from the rssi and txPower:
(0.89976) * Math.pow(ratio, 7.7095) + 0.111;
In this formula you have 3 constants, these constants are tag & bluetooth receiver specific. So to increase the precision you should calculate these 3 constants for every kind of tag you are using and for every type of bluetooth module of the smartphones you're using.
If your tag moves you can implements others functions to smooth your values, for example a Kalman Filter.
(Credit:
wouterbulten - kalmanjs - GitHub)
Kalman Filter in Java (used on Android):
private class KalmanFilter implements Serializable {
private double R; // Process Noise
private double Q; // Measurement Noise
private double A; // State Vector
private double B; // Control Vector
private double C; // Measurement Vector
private Double x; // Filtered Measurement Value (No Noise)
private double cov; // Covariance
public KalmanFilter(double r, double q, double a, double b, double c) {
R = r;
Q = q;
A = a;
B = b;
C = c;
}
public KalmanFilter(double r, double q){
R = r;
Q = q;
A = 1;
B = 0;
C = 1;
}
/** Public Methods **/
public double applyFilter(double rssi){
return applyFilter(rssi, 0.0d);
}
/**
* Filters a measurement
*
* @param measurement The measurement value to be filtered
* @param u The controlled input value
* @return The filtered value
*/
public double applyFilter(double measurement, double u) {
double predX; // Predicted Measurement Value
double K; // Kalman Gain
double predCov; // Predicted Covariance
if (x == null) {
x = (1 / C) * measurement;
cov = (1 / C) * Q * (1 / C);
} else {
predX = predictValue(u);
predCov = getUncertainty();
K = predCov * C * (1 / ((C * predCov * C) + Q));
x = predX + K * (measurement - (C * predX));
cov = predCov - (K * C * predCov);
}
return x;
}
/** Private Methods **/
private double predictValue(double control){
return (A * x) + (B * control);
}
private double getUncertainty(){
return ((A * cov) * A) + R;
}
@Override
public String toString() {
return "KalmanFilter{" +
"R=" + R +
", Q=" + Q +
", A=" + A +
", B=" + B +
", C=" + C +
", x=" + x +
", cov=" + cov +
'}';
}
}
Usage:
private KalmanFilter mKalmanFilter; // Property of your class to store kalman filter values
mKalmanFilter = new KalmanFilter(KALMAN_R, KALMAN_Q); // init Kalman Filter
// Method Apply Filter
private void applyKalmanFilterToRssi(){
mFilteredRSSI = mKalmanFilter.applyFilter(mRSSI);
}
Constants Values:
// Kalman R & Q
private static final double KALMAN_R = 0.125d;
private static final double KALMAN_Q = 0.5d;
+ KALMAN_R is the Process Noise
+ KALMAN_Q is the Measurement Noise
You Should Change These 2 Values looking to your measurements and you case of use.
Changing these 2 values you will change how fast the filtered measurement value will change from a value to another.
So if you have values with a lot of noise and you want to slow down how fast the measurement value changes (to smooth the Gaussian) you should try to increment KALMAN_R & KALMAN_Q values.
The values here for KALMAN_R & KALMAN_Q, if I remember right, were the one I was using when I was programming for BLE Devices so these 2 values for KALMAN_R & KALMAN_Q are already "big" because the RSSI of BLE Devices changes a lot.
I Suggest you to use a Kalman Filter to smooth your values.
Hope this is helpful, have a nice day and nice coding!
Upload, these are my classes for TagBLE:
Base class for a Tag Bluetooth Low Energy:
public class TagBLE extends RealmObject implements Parcelable {
// Field Names
public static final String FIELD_ID = "id";
public static final String FIELD_MAC = FIELD_ID;
@Expose
@PrimaryKey
@SerializedName("tag_mac")
private String id;
@Expose
@SerializedName("tag_nome")
private String mName;
@Expose
@SerializedName("tx_power")
private int mTxPower;
public TagBLE(){}
public TagBLE(String mac, String name, int txPower){
id = mac;
mName = name;
mTxPower = txPower;
}
public TagBLE(TagBLE tag){
id = tag.getMAC();
mName = tag.getName();
mTxPower = tag.getTxPower();
}
/** Private Constructors **/
private TagBLE(Parcel in){
id = in.readString();
mName = in.readString();
mTxPower = in.readInt();
}
/** Public Static Factory Methods **/
public static TagBLE initInstanceFromScanResult(ScanResult result, int txPower){
BluetoothDevice bDevice = result.getDevice();
return new TagBLE(bDevice.getAddress(), bDevice.getName(), txPower);
}
/** Parcelling Methods **/
public static Parcelable.Creator<TagBLE> CREATOR = new TagBLECreator();
/** Override Parcelable Methods **/
@Override
public int describeContents(){
return 0x0;
}
@Override
public void writeToParcel(Parcel out, int flags){
out.writeString(id);
out.writeString(mName);
out.writeInt(mTxPower);
}
/** Getter Methods **/
public String getId(){
return id;
}
public String getMAC() {
return id;
}
public String getName() {
return mName;
}
public int getTxPower() {
return mTxPower;
}
/** Setter Methods **/
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
mName = name;
}
public void setTxPower(int txPower) {
mTxPower = txPower;
}
/** Public Methods **/
public double getDistance(int rssi){
return getDistance((double) rssi);
}
public double getDistance(double rssi){
return Math.pow(10, ((mTxPower - rssi) * 1.0) / 20);
}
@Override
public String toString() {
return "TagBLE{" +
"id='" + id + '\'' +
", mName='" + mName + '\'' +
", mTxPower=" + mTxPower +
'}';
}
/** Private Static Class - Parcelable Creator **/
private static class TagBLECreator implements Parcelable.Creator<TagBLE> {
@Override
public TagBLE createFromParcel(Parcel in) {
return new TagBLE(in);
}
@Override
public TagBLE[] newArray(int size) {
return new TagBLE[size];
}
}
}
Specific class to manage data (in my case I need to manage the distance, if it is near to a device or it is far, but I removed these parts from the class code) of a Tag BLE found (with KalmanFilter):
public class DataTagBLE extends RealmObject {
// Field Names
public static final String FIELD_ID = "id";
// Kalman R & Q
private static final double KALMAN_R = 0.125d;
private static final double KALMAN_Q = 0.5d;
@PrimaryKey
private String id;
@Expose
@SerializedName("tag")
private TagBLE mTag;
@Expose
@SerializedName("acquired")
private Date mAcquired;
@Expose
@SerializedName("rssi")
private int mRSSI;
@Expose
@SerializedName("filtered_rssi")
private double mFilteredRSSI;
@Ignore
private KalmanFilter mKalmanFilter;
private double mDistance;
public DataTagBLE(){}
public DataTagBLE(TagBLE tag){
id = UUID.randomUUID().toString();
mTag = tag;
mAcquired = new Date();
mRSSI = 0x0;
mFilteredRSSI = 0x0;
mKalmanFilter = new KalmanFilter(KALMAN_R, KALMAN_Q);
}
/** Private Constructors **/
private DataTagBLE(TagBLE tag, int rssi){
id = UUID.randomUUID().toString();
mTag = tag;
mAcquired = new Date();
mRSSI = rssi;
}
/** Public Static Factory Methods **/
public static DataTagBLE initInstanceDataTagFound(@NonNull ScanResult scanResult, int txPower){
return new DataTagBLE(TagBLE.initInstanceFromScanResult(scanResult, txPower));
}
/** Getter Methods **/
public TagBLE getTag(){
return mTag;
}
public Date getAcquired() {
return mAcquired;
}
public int getRSSI(){
return mRSSI;
}
public double getFilteredRSSI(){
return this.mFilteredRSSI;
}
public KalmanFilter getKalmanFilter() {
return mKalmanFilter;
}
/** Setter Methods **/
public void setTag(TagBLE tag){
mTag = tag;
}
public void setAcquired(Date acquired) {
this.mAcquired = acquired;
}
public void setRSSI(int rssi){
mRSSI = rssi;
}
public void setFilteredRSSI(int rssi){
this.mFilteredRSSI = rssi;
}
public void setKalmanFilter(KalmanFilter kalmanFilter) {
this.mKalmanFilter = kalmanFilter;
}
/** TagBLE Getter Methods **/
public String getTagMac() {
if (mTag != null) {
return mTag.getMAC();
} else {
return null;
}
}
/** TagBLE Setter Methods **/
public void setTagNameAndTxPower(String tagName, int txPower){
if(mTag != null){
mTag.setName(tagName);
mTag.setTxPower(txPower);
}
}
/** Public Methods **/
public void generateNewID(){
id = UUID.randomUUID().toString();
}
public void onNewDataTagAcquired(DataTagBLE dataTagFound){
setRSSI(dataTagFound.getRSSI());
applyKalmanFilterToRssi();
TagBLE tagFound = dataTagFound.getTag();
if(tagFound != null) {
setTagNameAndTxPower(tagFound.getName(), tagFound.getTxPower());
}
setAcquired(new Date());
}
public void store(){
generateNewID();
RealmHelper rHelper = new RealmHelper();
rHelper.saveUpdateRealmObject(this);
rHelper.close();
}
/** Distance & RSSI Filtering Methods **/
public double getDistanceFiltered(){
return mTag.getDistance(mFilteredRSSI);
}
public double getDistance(){
return mTag.getDistance(mRSSI);
}
/** Private Methods **/
private void applyKalmanFilterToRssi(){
mFilteredRSSI = mKalmanFilter.applyFilter(mRSSI);
}
@Override
public String toString() {
return "DataTagBLE{" +
"id='" + id + '\'' +
", mTag=" + mTag +
", mAcquired=" + mAcquired +
", mRSSI=" + mRSSI +
", mFilteredRSSI=" + mFilteredRSSI +
", mKalmanFilter=" + mKalmanFilter +
", mDistance=" + mDistance +
'}';
}
/** Private Classes **/
/*
SOURCE: https://github.com/wouterbulten/kalmanjs/blob/master/dist/kalman.js
*/
private class KalmanFilter implements Serializable {
private double R; // Process Noise
private double Q; // Measurement Noise
private double A; // State Vector
private double B; // Control Vector
private double C; // Measurement Vector
private Double x; // Filtered Measurement Value (No Noise)
private double cov; // Covariance
public KalmanFilter(double r, double q, double a, double b, double c) {
R = r;
Q = q;
A = a;
B = b;
C = c;
}
public KalmanFilter(double r, double q){
R = r;
Q = q;
A = 1;
B = 0;
C = 1;
}
/** Public Methods **/
public double applyFilter(double rssi){
return applyFilter(rssi, 0.0d);
}
/**
* Filters a measurement
*
* @param measurement The measurement value to be filtered
* @param u The controlled input value
* @return The filtered value
*/
public double applyFilter(double measurement, double u) {
double predX; // Predicted Measurement Value
double K; // Kalman Gain
double predCov; // Predicted Covariance
if (x == null) {
x = (0x1 / C) * measurement;
cov = (0x1 / C) * Q * (0x1 / C);
} else {
predX = predictValue(u);
predCov = getUncertainty();
K = predCov * C * (0x1 / (C * predCov * C + Q));
x = predX + K * (measurement - (C * predX));
cov = predCov - (K * C * predCov);
}
return x;
}
/** Private Methods **/
private double predictValue(double control){
return (A * x) + (B * control);
}
private double getUncertainty(){
return (A * cov * A) + R;
}
@Override
public String toString() {
return "KalmanFilter{" +
"R=" + R +
", Q=" + Q +
", A=" + A +
", B=" + B +
", C=" + C +
", x=" + x +
", cov=" + cov +
'}';
}
}
}
In my case when I found a unique MAC for a Tag BLE I instantiate a "DataTagBLE" (the class isn't complete, I removed all distance checks that I'm using).
I init the instance the first time by:
DataTagBLE initInstanceDataTagFound(@NonNull ScanResult scanResult, int txPower)
Then every time I found the same tag (which will have a different RSSI) I will get back the DataTagBLE for this tag by watching the MAC Address (I'm using an HashMap<MAC_STRING, DataTagBLE> on my service.).
Then when I have the instance I'll use:
myDataTagBLEInstance.onNewDataTagAcquired(DataTagBLE newDataTagBLEInstance)
(I have base services that always return to me a made DataTagBLE instance, so I will have the old instance updated with the new instance data using the method above)
That's just to answer to question below, the KalmanFilter must be the same instance for the same tag!
Get txPower from beacon:
It uses: 'com.neovisionaries:nv-bluetooth:1.8'
public static int getBeaconTxPower(ScanResult result){
if(result != null) {
// This part uses the library above
if(result.getScanRecord() != null) {
List<ADStructure> structures = ADPayloadParser.getInstance().parse(result.getScanRecord().getBytes());
if (structures != null && structures.size() > 0x0) {
for (ADStructure st : structures) {
if (st instanceof IBeacon) {
IBeacon beacon = (IBeacon) st;
if(beacon.getPower() != 0x0) {
return beacon.getPower();
}
}
}
}
}
// Add case here if the Tag doesn't have the txPower setted. For example: if(MAC ADDRESS contains "XX:XX:XX..." then txPower = Y"
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
return result.getTxPower();
}
}
return 0x0;
}
Pseudo Code KalmanFilter using an HashMap:
// Store here KalmanFilters associated to every MAC Address
HashMap<String, KalmanFilter> mKalmanFilters;
// When you find a tag:
if mKalmanFilters.keySet().contains(tagFound.mac){
KalmanFilter mKalman = mKalmanFilters.get(tagFound.mac());
// This will give you a smoothed RSSI value because 'x == lastRssi'
double smoothed = mKalman.applyFilter(tagFound.rssi);
// Do what you want with this rssi
} else {
KalmanFilter mKalman = new KalmanFilter(valR, valQ);
/* This will set the first measurement, so the 'x', of the KalmanFilter. Next time you find the tag and you use the 'if part' you will get a smoothed rssi value.
This rssi value will be smoothed depending on the 'C' value (so Measurement Vector) setted in your KalmanFilter instance. If C == 1 => smoothed == rssi. */
double smoothed = mKalman.applyFilter(tagFound.rssi);
mKalmanFilters.put(tagFound.mac, mKalmanFilter);
}
calculateDistance
return the distance in meter or what? – Ventilator