Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔵🦷 UI changes for BLE integration #1145

Merged
merged 22 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
26c388d
📋 Prep for saving new BLE Bluetooth objects
shankari Apr 1, 2024
5f53e53
include 'sections' in UnprocessedTrips
JGreenlee Apr 3, 2024
76d1131
update types relating to `UnprocessedTrip`s
JGreenlee Apr 3, 2024
12db91a
rename functions related to unprocessed trips
JGreenlee Apr 3, 2024
826190a
flesh out more types
JGreenlee Apr 3, 2024
ba3ef43
Fill in a "beacons" array for the range callback
shankari Apr 11, 2024
6ca810d
Prettier fixes + highlight that the buttons simulate behavior
shankari Apr 11, 2024
7cb3882
✨ Simulate BLE entries and transitions through the UI
shankari Apr 12, 2024
2cc07a0
add type defs for BLE data + new config fields
JGreenlee Apr 12, 2024
a586bb7
retrieve + match BLE scans to timelineEntries
JGreenlee Apr 12, 2024
bff97ae
use labels OR bluetooth vehicle identity to determine "confirmed mode"
JGreenlee Apr 12, 2024
463c2a5
🤡 Switch the simulation code in the UI to use the mock objects
shankari Apr 13, 2024
fbedee4
during bluetooth_ble matching, convert major and minor to hexadecimal
JGreenlee Apr 14, 2024
3fd2863
add "Refresh App Configuration" row to Profile tab
JGreenlee Apr 14, 2024
d61a4af
Merge branch 'simulate_ble_scans' of https://github.com/shankari/e-mi…
JGreenlee Apr 14, 2024
8de3ca0
Merge branch 'unprocessed-trip-sections' of https://github.com/JGreen…
JGreenlee Apr 14, 2024
6cd4e6b
allow android emulator to download locally hosted configs by using 10…
JGreenlee Apr 14, 2024
933f9a4
CSP allow 10.0.2.2 for android emulator
JGreenlee Apr 15, 2024
fcaec5c
fix typo: "background_ble" -> "bluetooth_ble"
JGreenlee Apr 15, 2024
b2d22b7
revert "CSP allow 10.0.2.2 for android emulator"
JGreenlee Apr 15, 2024
0b6b844
add "confirmedMode" to derived properties & use for conditional surveys
JGreenlee Apr 15, 2024
37fbb85
only use "ranging" scans for BLE matching
JGreenlee Apr 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width, viewport-fit=cover">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com https://nominatim.openstreetmap.org https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' https://tile.openstreetmap.org 'unsafe-inline' data: 'unsafe-eval'">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com https://nominatim.openstreetmap.org https://raw.githubusercontent.com http://10.0.2.2:9090; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' https://tile.openstreetmap.org 'unsafe-inline' data: 'unsafe-eval'">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add this, we should also add http://localhost:9090. Although I really don't think we should change this in the index.html since it can be a (very farfetched) security hole in production. Ideally, we would add these (and the edit-config tag in config.xml as part of npm run build-dev and remove them as part of npm build.

For now, I have the changes implemented locally to allow testing but have not checked them in.
I also made https://github.com/e-mission/e-mission-phone/pull/1145/files#diff-d7df3a5aedf99d611872de9affe54d6bd62531488711022ee9f33d1ac86baab6R148 locally but didn't check it in 😄

<title></title>
<script type="text/javascript" src="cordova.js"></script>
</head>
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
Loading