Skip to content

Commit

Permalink
feat(bluetooth-classic): apply Kalman filter to RSSI values
Browse files Browse the repository at this point in the history
Bluetooth Classic devices occasionally have some jumps in the measured
signal strengths, which this filter hopefully cleans up a bit. This
should lead to more consistent results, with less random "jumping".
  • Loading branch information
mKeRix committed Jan 26, 2020
1 parent 4d9a2ec commit f3566f5
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 3 deletions.
Expand Up @@ -13,10 +13,18 @@ import {
} from './bluetooth-classic.const';
import { NewRssiEvent } from './new-rssi.event';
import { RoomPresenceDistanceSensor } from '../room-presence/room-presence-distance.sensor';
import KalmanFilter from 'kalmanjs';

jest.mock('child_process');
jest.mock('util');
jest.mock('../room-presence/room-presence-distance.sensor');
jest.mock('kalmanjs', () => {
return jest.fn().mockImplementation(() => {
return {
filter: z => z
};
});
});

describe('BluetoothClassicService', () => {
let service: BluetoothClassicService;
Expand Down Expand Up @@ -191,7 +199,7 @@ describe('BluetoothClassicService', () => {
'test-instance',
10
);
expect(sensorInstance.timeout).toBe(30);
expect(sensorInstance.timeout).toBe(35);
});

it('should not distribute inquiries if not the leader', () => {
Expand Down Expand Up @@ -329,4 +337,33 @@ describe('BluetoothClassicService', () => {
expect(nodes.find(node => node.id === 'def')).not.toBeUndefined();
expect(nodes.find(node => node.id === 'xyz')).toBeUndefined();
});

it('should filter the RSSI of inquired devices before publishing', async () => {
jest.spyOn(service, 'inquireRssi').mockResolvedValue(-3);
const handleRssiMock = jest
.spyOn(service, 'handleNewRssi')
.mockImplementation(() => undefined);
const filterRssiMock = jest
.spyOn(service, 'filterRssi')
.mockReturnValue(-5.2);

const address = 'ab:cd:01:23:00:70';
const expectedEvent = new NewRssiEvent('test-instance', address, -5.2);

await service.handleRssiRequest(address);
expect(filterRssiMock).toHaveBeenCalled();
expect(clusterService.publish).toHaveBeenCalledWith(
NEW_RSSI_CHANNEL,
expectedEvent
);
expect(handleRssiMock).toHaveBeenCalledWith(expectedEvent);
});

it('should reuse Kalman filters for the same address', () => {
service.filterRssi('D6:AB:CD:10:DA:31', -1);
service.filterRssi('D6:AB:CD:10:DA:41', -4);
service.filterRssi('D6:AB:CD:10:DA:31', -2);

expect(KalmanFilter).toHaveBeenCalledTimes(2);
});
});
27 changes: 25 additions & 2 deletions src/integrations/bluetooth-classic/bluetooth-classic.service.ts
Expand Up @@ -22,11 +22,16 @@ import {
REQUEST_RSSI_CHANNEL
} from './bluetooth-classic.const';
import { RoomPresenceDistanceSensor } from '../room-presence/room-presence-distance.sensor';
import KalmanFilter from 'kalmanjs';

@Injectable()
export class BluetoothClassicService
implements OnModuleInit, OnApplicationBootstrap {
private readonly config: BluetoothClassicConfig;
private filterMap: Map<string, KalmanFilter> = new Map<
string,
KalmanFilter
>();
private rotationOffset: number = 0;
private logger: Logger;

Expand Down Expand Up @@ -73,9 +78,10 @@ export class BluetoothClassicService
* @param address - Bluetooth MAC address
*/
async handleRssiRequest(address: string): Promise<void> {
const rssi = await this.inquireRssi(address);
let rssi = await this.inquireRssi(address);

if (rssi !== undefined) {
rssi = _.round(this.filterRssi(address, rssi), 1);
const event = new NewRssiEvent(
this.configService.get('global').instanceName,
address,
Expand Down Expand Up @@ -166,6 +172,23 @@ export class BluetoothClassicService
return matches?.length > 0 ? parseInt(matches[0], 10) : undefined;
}

/**
* Applies the Kalman filter based on the historic values with the same address.
*
* @param address - Bluetooth MAC address
* @param rssi - Signal strength measurement for the given address
* @returns Smoothed signal strength value
*/
filterRssi(address: string, rssi: number): number {
if (this.filterMap.has(address)) {
return this.filterMap.get(address).filter(rssi);
} else {
const kalman = new KalmanFilter({ R: 1.4, Q: 0.4 });
this.filterMap.set(address, kalman);
return kalman.filter(rssi);
}
}

/**
* Queries for the name of a Bluetooth device using the hcitool shell command.
*
Expand Down Expand Up @@ -241,7 +264,7 @@ export class BluetoothClassicService
protected calculateCurrentTimeout(): number {
const nodes = this.getParticipatingNodes();
const addresses = Object.values(this.config.addresses); // workaround for node-config deserializing to an Array-like object
return (Math.max(nodes.length, addresses.length) + 1) * 10;
return (Math.max(nodes.length, addresses.length) + 1.5) * 10;
}

/**
Expand Down

0 comments on commit f3566f5

Please sign in to comment.