Skip to content

Commit

Permalink
✨ Simulate BLE entries and transitions through the UI
Browse files Browse the repository at this point in the history
Consistent with the plans in:
e-mission/e-mission-docs#1062 (comment)

add support for storing simulated BLE responses and transitions in the
usercache

Concretely:
- when there is a monitor entry event, we store a `bluetooth_ble` entry with
  `REGION_ENTER`
- when there is a range event, we store a `bluetooth_ble` entry with
  `RANGE_UPDATE`. if we receive three consecutive range updates within 5 minutes, we generate a `BLE_BEACON_FOUND` transition
- when there is a monitor exit event, we store a `bluetooth_ble` entry with `REGION_EXIT` and generate a `BLE_BEACON_LOST` transition

Note that this has some fixes found while testing android:
e-mission/e-mission-docs#1062 (comment)

In addition, the callbacks exposed that the format of the range callback
that we were using earlier was incorrect. The `beacons` array is at the
same level as the `region`

Testing done on both iOS and android:
- Scan for BLE beacons
- Region enter
- Range (3-4 times); see `ble_found` transition
- Region exit; see `ble_lost` transition
  • Loading branch information
shankari committed Apr 12, 2024
1 parent 6ca810d commit 7cb3882
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 16 deletions.
9 changes: 5 additions & 4 deletions www/js/bluetooth/BluetoothCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const BluetoothCard = ({ device, isClassic, isScanningBLE }: Props) => {
const deviceWithBeacons = { ...device };
deviceWithBeacons.monitorResult = undefined;
deviceWithBeacons.rangeResult = undefined;
deviceWithBeacons.beacons = [
const beacons = [
{
uuid: device.uuid,
major: device.major | 4567,
Expand All @@ -55,6 +55,7 @@ const BluetoothCard = ({ device, isClassic, isScanningBLE }: Props) => {
deviceWithBeacons.minor = device.minor | 4567;
window['cordova'].plugins.locationManager.getDelegate().didRangeBeaconsInRegion({
region: deviceWithBeacons,
beacons: beacons,
eventType: 'didRangeBeaconsInRegion',
state: 'CLRegionStateInside',
});
Expand All @@ -77,18 +78,18 @@ const BluetoothCard = ({ device, isClassic, isScanningBLE }: Props) => {
</Text>
<Text
style={{ backgroundColor: colors.danger, color: colors.background }}
variant="bodyMedium">
variant="bodyLarge">
Simulate by sending UI transitions
</Text>
<Card.Actions style={{ backgroundColor: colors.danger, color: colors.background }}>
<Button mode="elevated" onPress={() => fakeMonitorCallback('CLRegionStateInside')}>
Enter
Region Enter
</Button>
<Button mode="elevated" onPress={fakeRangeCallback}>
Range
</Button>
<Button mode="elevated" onPress={() => fakeMonitorCallback('CLRegionStateOutside')}>
Exit
Region Exit
</Button>
</Card.Actions>
</Card.Content>
Expand Down
112 changes: 100 additions & 12 deletions www/js/bluetooth/BluetoothScanPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DateTime } from 'luxon';
import { StyleSheet, Modal, ScrollView, SafeAreaView, View, Text } from 'react-native';
import { gatherBluetoothClassicData } from './bluetoothScanner';
import { logWarn, displayError, displayErrorMsg, logDebug } from '../plugin/logger';
Expand All @@ -11,6 +12,7 @@ import {
BluetoothClassicDevice,
BLEDeviceList,
} from '../types/bluetoothDevices';
import { forceTransition } from '../control/ControlCollectionHelper';

/**
* The implementation of this scanner page follows the design of
Expand Down Expand Up @@ -95,6 +97,18 @@ const BluetoothScanPage = ({ ...props }: any) => {
in_range: status,
},
}));
// using putMessage instead of putSensorData as a temporary hack for now
// since putSensorData is not exposed through javascript
let { monitorResult: _, in_range: _, ...noResultDevice } = sampleBLEDevices[uuid];
window['cordova']?.plugins?.BEMUserCache.putMessage('background/bluetooth_ble', {
eventType: status ? 'REGION_ENTER' : 'REGION_EXIT',
ts: Date.now() / 1000, // our convention is to have timestamps in seconds
uuid: uuid,
...noResultDevice, // gives us uuid, major, minor
});
if (!status) {
forceTransition('BLE_BEACON_LOST');
}
}

function setRangeStatus(uuid: string, result: string) {
Expand All @@ -105,8 +119,57 @@ const BluetoothScanPage = ({ ...props }: any) => {
rangeResult: result,
},
}));
// we don't want to exclude monitorResult and rangeResult from the values
// that we save because they are the current or previous result, just
// in a different format
// https://stackoverflow.com/a/34710102
let {
monitorResult: _,
rangeResult: _,
in_range: _,
...noResultDevice
} = sampleBLEDevices[uuid];
let parsedResult = JSON.parse(result);
parsedResult.beacons.forEach((beacon) => {
window['cordova']?.plugins?.BEMUserCache.putMessage('background/bluetooth_ble', {
eventType: 'RANGE_UPDATE',
ts: Date.now() / 1000, // our convention is to have timestamps in seconds
uuid: uuid,
...noResultDevice, // gives us uuid, major, minor
...beacon, // gives us proximity, accuracy and rssi
});
});
// we only check for the transition on "real" callbacks to avoid excessive
// spurious callbacks on android
if (parsedResult.beacons.length > 0) {
// if we have received 3 range responses for the same beacon in the
// last 5 minutes, we generate the transition. we read without metadata
// (last param)
let nowSec = DateTime.now().toUnixInteger();
let tq = { key: 'write_ts', startTs: nowSec - 5 * 60, endTs: nowSec };
let readBLEReadingsPromise = window['cordova']?.plugins?.BEMUserCache.getMessagesForInterval(
'background/bluetooth_ble',
tq,
false,
);
readBLEReadingsPromise.then((bleResponses) => {
let lastThreeResponses = bleResponses.slice(0, 3);
if (!lastThreeResponses.every((x) => x.eventType == 'RANGE_UPDATE')) {
console.log(
'Last three entries ' +
lastThreeResponses.map((x) => x.eventType) +
' are not all RANGE_UPDATE, skipping transition',
);
return;
}

forceTransition('BLE_BEACON_FOUND');
});
}
}

async function simulateLocation(state: String) {}

// BLE LOGIC
async function startBeaconScanning() {
setIsScanningBLE(true);
Expand All @@ -128,18 +191,21 @@ const BluetoothScanPage = ({ ...props }: any) => {
window['cordova'].plugins.locationManager.appendToDeviceLog(
'[DOM] didDetermineStateForRegion: ' + pluginResultStr,
);
const beaconRegion = new window['cordova'].plugins.locationManager.BeaconRegion(
STATIC_ID,
pluginResult.region.uuid,
pluginResult.region.major,
pluginResult.region.minor,
);
window['cordova'].plugins.locationManager
.startRangingBeaconsInRegion(beaconRegion)
.fail(function (e) {
logWarn(e);
})
.done();
if (pluginResult.state == 'CLRegionStateInside') {
const beaconRegion = new window['cordova'].plugins.locationManager.BeaconRegion(
STATIC_ID,
pluginResult.region.uuid,
pluginResult.region.major,
pluginResult.region.minor,
);
console.log('About to start ranging beacons for region ', beaconRegion);
window['cordova'].plugins.locationManager
.startRangingBeaconsInRegion(beaconRegion)
.fail(function (e) {
logWarn(e);
})
.done();
}
};

delegate.didStartMonitoringForRegion = function (pluginResult) {
Expand Down Expand Up @@ -333,6 +399,28 @@ const BluetoothScanPage = ({ ...props }: any) => {
<Button disabled={!newUUID} onPress={() => addNewUUID(newUUID, newMajor, newMinor)}>
Add New Beacon To Scan
</Button>
<View
style={{
flexDirection: 'column',
alignItems: 'center',
backgroundColor: colors.danger,
color: colors.background,
}}>
<Text
style={{ backgroundColor: colors.danger, color: colors.background }}
variant="bodyLarge">
Simulate by sending UI transitions
</Text>

<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Button mode="elevated" onPress={() => simulateLocation('EXITED_GEOFENCE')}>
Geofence exit
</Button>
<Button mode="elevated" onPress={() => simulateLocation('STOPPED_MOVING')}>
Stopped Moving
</Button>
</View>
</View>
</SafeAreaView>
</Modal>
</>
Expand Down

0 comments on commit 7cb3882

Please sign in to comment.