Skip to content

Commit

Permalink
feat(bluetooth): add configuration options for noise filter
Browse files Browse the repository at this point in the history
Should allow users to fine-tune the noise reduction mechanism for their
use case.

Refs #775
  • Loading branch information
mKeRix committed Mar 20, 2022
1 parent 6325937 commit 85981ee
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 57 deletions.
26 changes: 14 additions & 12 deletions docs/integrations/bluetooth-classic.md
Expand Up @@ -77,18 +77,20 @@ Try to pair your Apple Watch to a Bluetooth device such as headphones/speakers f

## Settings

| Name | Type | Default | Description |
| ------------------ | -------------------------------------------- | ------- | ------------------------------------------------------------ |
| `addresses` | Array | | List of Bluetooth MAC addresses that should be tracked. You can usually find them in the device settings. |
| `minRssi` | Number _or_ [detailed config](#minimum-rssi) | | Limits the RSSI at which a device is still reported if configured. Remember, the RSSI is the inverse of the sensor attribute distance, so for a cutoff at 10 you would configure -10. |
| `rssiFactor` | Number | `1` | Multiplier for the measured RSSI values. Allows you to fine-tune measurements if you use different Bluetooth adapters across your cluster. |
| `hciDeviceId` | Number | `0` | ID of the Bluetooth device to use for the inquiries, e.g. `0` to use `hci0`. |
| `interval` | Number | `10` | The interval at which the Bluetooth devices are queried in seconds. |
| `scanTimeLimit` | Number | `6` | The maximum time allowed for completing a device query in seconds. This should be set lower than the interval. |
| `timeoutCycles` | Number | `2` | The number of completed query cycles after which collected measurements are considered obsolete. The timeout in seconds is calculated as `max(addresses, clusterDevices) * interval * timeoutCycles`. |
| `preserveState` | Boolean | `false` | Whether the last recorded distance should be preserved when the inquiries switch is turned off or not. |
| `inquireFromStart` | Boolean | `true` | Whether the [Inquiries Switch](#inquiries-switch) is turned on when room-assistant is started or not. |
| `entityOverrides` | [Entity Overrides](#entity-overrides) | | Allows you to override some properties of the created entities. |
| Name | Type | Default | Description |
| ------------------------ | -------------------------------------------- | ------- | ------------------------------------------------------------ |
| `addresses` | Array | | List of Bluetooth MAC addresses that should be tracked. You can usually find them in the device settings. |
| `minRssi` | Number _or_ [detailed config](#minimum-rssi) | | Limits the RSSI at which a device is still reported if configured. Remember, the RSSI is the inverse of the sensor attribute distance, so for a cutoff at 10 you would configure -10. |
| `rssiFactor` | Number | `1` | Multiplier for the measured RSSI values. Allows you to fine-tune measurements if you use different Bluetooth adapters across your cluster. |
| `hciDeviceId` | Number | `0` | ID of the Bluetooth device to use for the inquiries, e.g. `0` to use `hci0`. |
| `interval` | Number | `10` | The interval at which the Bluetooth devices are queried in seconds. |
| `scanTimeLimit` | Number | `6` | The maximum time allowed for completing a device query in seconds. This should be set lower than the interval. |
| `timeoutCycles` | Number | `2` | The number of completed query cycles after which collected measurements are considered obsolete. The timeout in seconds is calculated as `max(addresses, clusterDevices) * interval * timeoutCycles`. |
| `preserveState` | Boolean | `false` | Whether the last recorded distance should be preserved when the inquiries switch is turned off or not. |
| `inquireFromStart` | Boolean | `true` | Whether the [Inquiries Switch](#inquiries-switch) is turned on when room-assistant is started or not. |
| `entityOverrides` | [Entity Overrides](#entity-overrides) | | Allows you to override some properties of the created entities. |
| `kalmanProcessNoise` | Number | `1.4` | Covariance of the process noise, used for measurement noise reduction via a [Kalman filter](https://en.wikipedia.org/wiki/Kalman_filter). |
| `kalmanMeasurementNoise` | Number | `1` | Covariance of the measurement noise, used for measurement noise reduction via a [Kalman filter](https://en.wikipedia.org/wiki/Kalman_filter). |

### Minimum RSSI

Expand Down
2 changes: 2 additions & 0 deletions docs/integrations/bluetooth-low-energy.md
Expand Up @@ -92,6 +92,8 @@ bluetoothLowEnergy:
| `instanceBeaconMajor` | Number | `1` | The major of the advertised iBeacon. |
| `instanceBeaconMinor` | Number | Random | The minor of the advertised iBeacon. |
| `minDiscoveryLogRssi` | Number | -999 | Only log newly discovered beacons if raw RSSI values are greater than this (useful to reduce log spam if on a busy street). |
| `kalmanProcessNoise` | Number | `0.0008` | Covariance of the process noise, used for measurement noise reduction via a [Kalman filter](https://en.wikipedia.org/wiki/Kalman_filter). |
| `kalmanMeasurementNoise` | Number | `4` | Covariance of the measurement noise, used for measurement noise reduction via a [Kalman filter](https://en.wikipedia.org/wiki/Kalman_filter). |

### Tag Overrides

Expand Down
4 changes: 4 additions & 0 deletions src/config/config.service.spec.fail.yml
Expand Up @@ -107,6 +107,8 @@ bluetoothLowEnergy:
ebef1234567890-55555-444:
name: iBeacon 2
timeout: 120 # FORMAT ERROR: Missplaced Property
kalmanProcessNoise: 1.1
kalmanMeasurementNoise: 2.2
bluetoothClassic:
hciDeviceId: 1
interval: 20
Expand All @@ -128,6 +130,8 @@ bluetoothClassic:
entityOverrides:
ebef1234567890-55555-333:
id: 333 # TYPE ERROR: String Required
kalmanProcessNoise: 1
kalmanMeasurementNoise: 2
omronD6t:
busNumber: 3
address: 0x1d
Expand Down
4 changes: 4 additions & 0 deletions src/config/config.service.spec.pass.yml
Expand Up @@ -101,6 +101,8 @@ bluetoothLowEnergy:
batteryMask: 0xFFFFFFFF
ebef1234567890-55555-444:
name: iBeacon 2
kalmanProcessNoise: 1.1
kalmanMeasurementNoise: 2.2
bluetoothClassic:
hciDeviceId: 1
interval: 20
Expand All @@ -119,6 +121,8 @@ bluetoothClassic:
entityOverrides:
ebef1234567890-55555-333:
id: "My Id"
kalmanProcessNoise: 1
kalmanMeasurementNoise: 2
omronD6t:
busNumber: 3
address: 0x1d
Expand Down
28 changes: 16 additions & 12 deletions src/integrations/bluetooth-classic/bluetooth-classic.config.ts
Expand Up @@ -7,33 +7,37 @@ const MAC_DEFAULT_REGEXP = new RegExp(MAC_REXEP_STRING + '|default');
const MAC_ERROR = '{#label} does not match the required MAC address format';

class BluetoothClassicEntityOverride {
@(jf.string().optional())
@jf.string().optional()
id?: string;
@(jf.string().optional())
@jf.string().optional()
name?: string;
}

export class BluetoothClassicConfig {
@(jf.array({ elementClass: String }).custom(validateMACAddress).required())
@jf.array({ elementClass: String }).custom(validateMACAddress).required()
addresses: string[] = [];
@(jf.any().custom(validateMinRSSI).optional())
@jf.any().custom(validateMinRSSI).optional()
minRssi?: number | { [macAddress: string]: number };
@(jf.number().required())
@jf.number().required()
rssiFactor = 1;
@(jf.number().integer().min(0).required())
@jf.number().integer().min(0).required()
hciDeviceId = 0;
@(jf.number().min(1).required())
@jf.number().min(1).required()
interval = 10;
@(jf.number().min(1).required())
@jf.number().min(1).required()
scanTimeLimit = 6;
@(jf.number().min(1).required())
@jf.number().min(1).required()
timeoutCycles = 2;
@(jf.boolean().required())
@jf.boolean().required()
preserveState = false;
@(jf.boolean().required())
@jf.boolean().required()
inquireFromStart = true;
@(jf.object().custom(validateEntityOverrides).required())
@jf.object().custom(validateEntityOverrides).required()
entityOverrides: { [entityId: string]: BluetoothClassicEntityOverride } = {};
@jf.number().positive().required()
kalmanProcessNoise = 1.4;
@jf.number().positive().required()
kalmanMeasurementNoise = 1;
}

function validateMACAddress(options: {
Expand Down
Expand Up @@ -37,7 +37,11 @@ const execPromise = util.promisify(exec);

@Injectable()
export class BluetoothClassicService
extends KalmanFilterable(Object, 1.4, 1)
extends KalmanFilterable(
Object,
'bluetoothClassic.kalmanProcessNoise',
'bluetoothClassic.kalmanMeasurementNoise'
)
implements OnModuleInit, OnApplicationBootstrap
{
private readonly config: BluetoothClassicConfig;
Expand Down
Expand Up @@ -3,63 +3,67 @@ import * as Joi from 'joi';
import * as jf from 'joiful';

class TagOverride {
@(jf.string().optional())
@jf.string().optional()
id?: string;
@(jf.string().optional())
@jf.string().optional()
name?: string;
@(jf.number().negative().optional())
@jf.number().negative().optional()
measuredPower?: number;
@(jf.number().integer().max(0xffffffff).optional())
@jf.number().integer().max(0xffffffff).optional()
batteryMask?: number;
}

export class BluetoothLowEnergyConfig {
@(jf.number().integer().min(0).required())
@jf.number().integer().min(0).required()
hciDeviceId = 0;
@(jf.array({ elementClass: String }).required())
@jf.array({ elementClass: String }).required()
whitelist: string[] = [];
@(jf.boolean().required())
@jf.boolean().required()
whitelistRegex = false;
@(jf.array({ elementClass: String }).required())
@jf.array({ elementClass: String }).required()
allowlist: string[] = [];
@(jf.boolean().required())
@jf.boolean().required()
allowlistRegex = false;
@(jf.array({ elementClass: String }).required())
@jf.array({ elementClass: String }).required()
blacklist: string[] = [];
@(jf.boolean().required())
@jf.boolean().required()
blacklistRegex = false;
@(jf.array({ elementClass: String }).required())
@jf.array({ elementClass: String }).required()
denylist: string[] = [];
@(jf.boolean().required())
@jf.boolean().required()
denylistRegex = false;
@(jf.boolean().required())
@jf.boolean().required()
processIBeacon = true;
@(jf.boolean().required())
@jf.boolean().required()
onlyIBeacon = false;
@(jf.number().integer().min(0).max(0xffff).required())
@jf.number().integer().min(0).max(0xffff).required()
majorMask = 0xffff;
@(jf.number().integer().min(0).max(0xffff).required())
@jf.number().integer().min(0).max(0xffff).required()
minorMask = 0xffff;
@(jf.number().integer().max(0xffffffff).required())
@jf.number().integer().max(0xffffffff).required()
batteryMask = 0x00000000;
@(jf.boolean().required())
@jf.boolean().required()
instanceBeaconEnabled = true;
@(jf.number().integer().min(0).max(65535).required())
@jf.number().integer().min(0).max(65535).required()
instanceBeaconMajor = 1;
@(jf.number().integer().min(0).max(65535).required())
@jf.number().integer().min(0).max(65535).required()
instanceBeaconMinor = randomInt(0, 65535);
@(jf.object().custom(validateTagOverrides).required())
@jf.object().custom(validateTagOverrides).required()
tagOverrides: { [entityId: string]: TagOverride } = {};
@(jf.number().min(0).required())
@jf.number().min(0).required()
timeout = 60;
@(jf.number().min(0).required())
@jf.number().min(0).required()
updateFrequency = 0;
@(jf.number().required())
@jf.number().required()
rssiFactor = 1;
@(jf.number().positive().optional())
@jf.number().positive().optional()
maxDistance?: number;
@(jf.number().max(0).required())
@jf.number().max(0).required()
minDiscoveryLogRssi = -999;
@jf.number().positive().required()
kalmanProcessNoise = 0.008;
@jf.number().positive().required()
kalmanMeasurementNoise = 4;
}

function validateTagOverrides(options: {
Expand Down
Expand Up @@ -36,7 +36,11 @@ const COMPANION_APP_CHARACTERISTIC_UUID = '21c46f33e813440786012ad281030052';

@Injectable()
export class BluetoothLowEnergyService
extends KalmanFilterable(Object, 0.008, 4)
extends KalmanFilterable(
Object,
'bluetoothLowEnergy.kalmanProcessNoise',
'bluetoothLowEnergy.kalmanMeasurementNoise'
)
implements OnModuleInit, OnApplicationBootstrap
{
private readonly config: BluetoothLowEnergyConfig;
Expand Down Expand Up @@ -487,7 +491,9 @@ export class BluetoothLowEnergyService
let appId: string;

if (!this.bluetoothService.acquireQueryMutex()) {
this.logger.debug(`Canceled discovery for tag ${tag.id} as BLE adapter is already in use`);
this.logger.debug(
`Canceled discovery for tag ${tag.id} as BLE adapter is already in use`
);
return tag;
}

Expand Down
9 changes: 6 additions & 3 deletions src/util/filters.ts
@@ -1,13 +1,14 @@
import KalmanFilter from 'kalmanjs';
import c from 'config';

// eslint-disable-next-line @typescript-eslint/ban-types
type Constructable = new (...args: any[]) => object;

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type,@typescript-eslint/explicit-module-boundary-types
export function KalmanFilterable<BC extends Constructable>(
Base: BC,
R = 1,
Q = 1
ProcessNoiseProperty: string,
MeasurementNoiseProperty: string
) {
return class extends Base {
kalmanFilterMap: Map<string, KalmanFilter> = new Map<
Expand All @@ -27,7 +28,9 @@ export function KalmanFilterable<BC extends Constructable>(
if (this.kalmanFilterMap.has(id)) {
return this.kalmanFilterMap.get(id).filter(value);
} else {
const kalman = new KalmanFilter({ R, Q });
const r = c.get<number>(ProcessNoiseProperty);
const q = c.get<number>(MeasurementNoiseProperty);
const kalman = new KalmanFilter({ R: r, Q: q });
this.kalmanFilterMap.set(id, kalman);
return kalman.filter(value);
}
Expand Down

0 comments on commit 85981ee

Please sign in to comment.