Skip to content

Commit

Permalink
Merge pull request #1145 from JGreenlee/ble-ui-changes
Browse files Browse the repository at this point in the history
🔵🦷 UI changes for BLE integration
  • Loading branch information
shankari authored Apr 15, 2024
2 parents 76839de + 37fbb85 commit 9cfff85
Show file tree
Hide file tree
Showing 19 changed files with 510 additions and 173 deletions.
7 changes: 2 additions & 5 deletions www/__tests__/timelineHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ afterAll(() => {

describe('useGeojsonForTrip', () => {
it('work with an empty input', () => {
const testVal = useGeojsonForTrip({} as any, {} as any);
const testVal = useGeojsonForTrip({} as any);
expect(testVal).toBeFalsy;
});

Expand All @@ -43,10 +43,7 @@ describe('useGeojsonForTrip', () => {
};

it('works without labelMode flag', () => {
const testValue = useGeojsonForTrip(
mockTLH.mockCompDataTwo.phone_data[1].data,
mockTLH.mockLabelOptions,
) as GeoJSONData;
const testValue = useGeojsonForTrip(mockTLH.mockCompDataTwo.phone_data[1].data) as GeoJSONData;
expect(testValue).toBeTruthy;
checkGeojson(testValue);
expect(testValue.data.features.length).toBe(3);
Expand Down
6 changes: 5 additions & 1 deletion www/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
"reminders-time-of-day": "Time of Day for Reminders ({{time}})",
"upcoming-notifications": "Upcoming Notifications",
"dummy-notification": "Dummy Notification in 5 Seconds",
"log-out": "Log Out"
"log-out": "Log Out",
"refresh-app-config": "Refresh App Configuration",
"current-version": "Current version: {{version}}",
"refreshing-app-config": "Refreshing app configuration, please wait...",
"already-up-to-date": "Already up to date!"
},

"general-settings": {
Expand Down
46 changes: 35 additions & 11 deletions www/js/bluetooth/BluetoothCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const BluetoothCard = ({ device, isClassic, isScanningBLE }: Props) => {
bgColor = device.in_range ? `rgba(200,250,200,1)` : `rgba(250,200,200,1)`;
}

async function fakeMonitorCallback() {
async function fakeMonitorCallback(state: String) {
// If we don't do this, the results start accumulating in the device object
// first call, we put a result into the device
// second call, the device already has a result, so we put another one in...
Expand All @@ -33,18 +33,29 @@ const BluetoothCard = ({ device, isClassic, isScanningBLE }: Props) => {
window['cordova'].plugins.locationManager.getDelegate().didDetermineStateForRegion({
region: deviceWithoutResult,
eventType: 'didDetermineStateForRegion',
state: 'CLRegionStateInside',
state: state,
});
let timer: ReturnType<typeof setTimeout> = setTimeout(fakeRangeCallback, 500);
}

async function fakeRangeCallback() {
// If we don't do this, the results start accumulating in the device object
// first call, we put a result into the device
// second call, the device already has a result, so we put another one in...
const deviceWithMajorMinor = { ...device, major: 1234, minor: 4567 };
const deviceWithBeacons = { ...device };
deviceWithBeacons.monitorResult = undefined;
deviceWithBeacons.rangeResult = undefined;
const beacons = [
{
uuid: device.uuid,
major: device.major | 4567,
minor: device.minor | 1945,
proximity: 'ProximityNear',
accuracy: Math.random() * 1.33,
rssi: Math.random() * -62,
},
];
deviceWithBeacons.minor = device.minor | 4567;
deviceWithBeacons.minor = device.minor | 4567;
window['cordova'].plugins.locationManager.getDelegate().didRangeBeaconsInRegion({
region: deviceWithMajorMinor,
region: deviceWithBeacons,
beacons: beacons,
eventType: 'didRangeBeaconsInRegion',
state: 'CLRegionStateInside',
});
Expand All @@ -65,9 +76,22 @@ const BluetoothCard = ({ device, isClassic, isScanningBLE }: Props) => {
<Text style={{ backgroundColor: colors.secondaryContainer }} variant="bodyMedium">
{device.rangeResult}
</Text>
<Button mode="elevated" onPress={fakeMonitorCallback}>
Fake callback
</Button>
<Text
style={{ backgroundColor: colors.danger, color: colors.background }}
variant="bodyLarge">
Simulate by sending UI transitions
</Text>
<Card.Actions style={{ backgroundColor: colors.danger, color: colors.background }}>
<Button mode="elevated" onPress={() => fakeMonitorCallback('CLRegionStateInside')}>
Region Enter
</Button>
<Button mode="elevated" onPress={fakeRangeCallback}>
Range
</Button>
<Button mode="elevated" onPress={() => fakeMonitorCallback('CLRegionStateOutside')}>
Region Exit
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
Expand Down
138 changes: 115 additions & 23 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 @@ -90,10 +92,22 @@ const BluetoothScanPage = ({ ...props }: any) => {
...prevDevices,
[uuid]: {
...prevDevices[uuid],
monitorResult: result,
monitorResult: status ? result : undefined,
rangeResult: undefined,
in_range: status,
},
}));
let { monitorResult: _, in_range: _, ...noResultDevice } = sampleBLEDevices[uuid];
window['cordova']?.plugins?.BEMDataCollection.mockBLEObjects(
status ? 'REGION_ENTER' : 'REGION_EXIT',
uuid,
undefined,
undefined,
1,
);
if (!status) {
forceTransition('BLE_BEACON_LOST');
}
}

function setRangeStatus(uuid: string, result: string) {
Expand All @@ -104,6 +118,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?.BEMDataCollection.mockBLEObjects(
'RANGE_UPDATE',
uuid,
beacon.major,
beacon.minor,
5,
);
});
// 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.getSensorDataForInterval('background/bluetooth_ble', tq, false);
readBLEReadingsPromise.then((bleResponses) => {
// we add 5 entries at a time, so if we want 3 button presses,
// we really want 15 entries
let lastFifteenResponses = bleResponses.slice(0, 15);
if (!lastFifteenResponses.every((x) => x.eventType == 'RANGE_UPDATE')) {
console.log(
'Last three entries ' +
lastFifteenResponses.map((x) => x.eventType) +
' are not all RANGE_UPDATE, skipping transition',
);
return;
}

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

async function simulateLocation(state: String) {
forceTransition(state);
}

// BLE LOGIC
Expand All @@ -127,18 +192,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 @@ -317,19 +385,43 @@ const BluetoothScanPage = ({ ...props }: any) => {
value={newUUID || ''}
onChangeText={(t) => setNewUUID(t.toUpperCase())}
/>
<TextInput
label="Major (optional)"
value={newMajor || ''}
onChangeText={(t) => setNewMajor(t)}
/>
<TextInput
label="Minor (optional)"
value={newMinor || ''}
onChangeText={(t) => setNewMinor(t)}
/>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TextInput
label="Major (optional)"
value={newMajor || ''}
onChangeText={(t) => setNewMajor(t)}
/>
<TextInput
label="Minor (optional)"
value={newMinor || ''}
onChangeText={(t) => setNewMinor(t)}
/>
</View>
<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
13 changes: 9 additions & 4 deletions www/js/config/dynamicConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,21 @@ async function readConfigFromServer(studyLabel: string) {
*/
async function fetchConfig(studyLabel: string, alreadyTriedLocal?: boolean) {
logDebug('Received request to join ' + studyLabel);
const downloadURL = `https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/configs/${studyLabel}.nrel-op.json`;
let downloadURL = `https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/configs/${studyLabel}.nrel-op.json`;
if (!__DEV__ || alreadyTriedLocal) {
logDebug('Fetching config from github');
const r = await fetch(downloadURL);
const r = await fetch(downloadURL, { cache: 'reload' });
if (!r.ok) throw new Error('Unable to fetch config from github');
return r.json(); // TODO: validate, make sure it has required fields
} else {
logDebug('Running in dev environment, checking for locally hosted config');
try {
const r = await fetch('http://localhost:9090/configs/' + studyLabel + '.nrel-op.json');
if (window['cordova'].platformId == 'android') {
downloadURL = `http://10.0.2.2:9090/configs/${studyLabel}.nrel-op.json`;
} else {
downloadURL = `http://localhost:9090/configs/${studyLabel}.nrel-op.json`;
}
const r = await fetch(downloadURL, { cache: 'reload' });
if (!r.ok) throw new Error('Local config not found');
return r.json();
} catch (err) {
Expand Down Expand Up @@ -227,7 +232,7 @@ function extractSubgroup(token: string, config: AppConfig): string | undefined {
* @param existingVersion If the new config's version is the same, we won't update
* @returns boolean representing whether the config was updated or not
*/
function loadNewConfig(newToken: string, existingVersion?: number): Promise<boolean> {
export function loadNewConfig(newToken: string, existingVersion?: number): Promise<boolean> {
const newStudyLabel = extractStudyName(newToken);
return readConfigFromServer(newStudyLabel)
.then((downloadedConfig) => {
Expand Down
17 changes: 16 additions & 1 deletion www/js/control/ProfileSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import ControlCollectionHelper, {
helperToggleLowAccuracy,
forceTransition,
} from './ControlCollectionHelper';
import { resetDataAndRefresh } from '../config/dynamicConfig';
import { loadNewConfig, resetDataAndRefresh } from '../config/dynamicConfig';
import { AppContext } from '../App';
import { shareQR } from '../components/QrCode';
import { storageClear } from '../plugin/storage';
Expand Down Expand Up @@ -307,6 +307,16 @@ const ProfileSettings = () => {
}, 1500);
}

async function refreshConfig() {
AlertManager.addMessage({ text: t('control.refreshing-app-config') });
const updated = await loadNewConfig(authSettings.opcode, appConfig?.version);
if (updated) {
window.location.reload();
} else {
AlertManager.addMessage({ text: t('control.already-up-to-date') });
}
}

//Platform.OS returns "web" now, but could be used once it's fully a Native app
//for now, use window.cordova.platformId

Expand Down Expand Up @@ -434,6 +444,11 @@ const ProfileSettings = () => {
textKey="control.email-log"
iconName="email"
action={() => sendEmail('loggerDB')}></SettingRow>
<SettingRow
textKey="control.refresh-app-config"
desc={t('control.current-version', { version: appConfig?.version })}
iconName="cog-refresh"
action={refreshConfig}></SettingRow>
<ExpansionSection sectionTitle="control.dev-zone">
<BluetoothScanSettingRow />
<SettingRow
Expand Down
Loading

0 comments on commit 9cfff85

Please sign in to comment.