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

🛜 ᛒ Adding BLE Beacon Scanner #1135

Merged
merged 16 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion package.cordovabuild.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@
"ANDROID_SUPPORT_V4_VERSION": "27.+"
},
"cordova-plugin-bluetooth-classic-serial-port": {},
"cordova-custom-config": {}
"cordova-custom-config": {},
"cordova-plugin-ibeacon": {}
}
},
"dependencies": {
Expand Down Expand Up @@ -136,6 +137,7 @@
"cordova-plugin-x-socialsharing": "6.0.4",
"cordova-plugin-bluetooth-classic-serial-port": "git+https://github.com/louisg1337/cordova-plugin-bluetooth-classic-serial-port.git",
"cordova-custom-config": "^5.1.1",
"cordova-plugin-ibeacon": "git+https://github.com/louisg1337/cordova-plugin-ibeacon.git",
"core-js": "^2.5.7",
"enketo-core": "^6.1.7",
"enketo-transformer": "^4.0.0",
Expand Down
2 changes: 1 addition & 1 deletion setup/setup_shared_native.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ sed -i -e "s|/usr/bin/env node|/usr/bin/env node --unhandled-rejections=strict|"

npx cordova prepare

EXPECTED_COUNT=25
EXPECTED_COUNT=26
INSTALLED_COUNT=`npx cordova plugin list | wc -l`
echo "Found $INSTALLED_COUNT plugins, expected $EXPECTED_COUNT"
if [ $INSTALLED_COUNT -lt $EXPECTED_COUNT ];
Expand Down
15 changes: 13 additions & 2 deletions www/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,23 @@
"list-datepicker-set": "Set",

"bluetooth": {
"scan-debug-title": "Bluetooth Scanner",
"scan-for-bluetooth": "Scan for Devices",
"title": {
"ble": "BLE Beacon Scanner",
"classic": "Bluetooth Classic Scanner"
},
"scan": {
"for-ble": "Scan for BLE Beacons",
"for-bluetooth": "Scan for Classic Devices",
"stop": "Stop Scanning"
},
"is-scanning": "Scanning...",
"device-info": {
"id": "ID",
"name": "Name"
},
"switch-to": {
"classic": "Switch to Classic",
"ble": "Switch to BLE"
}
},

Expand Down
31 changes: 25 additions & 6 deletions www/js/bluetooth/BluetoothCard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import React from 'react';
import { Card, List } from 'react-native-paper';
import { Card, List, useTheme } from 'react-native-paper';
import { StyleSheet } from 'react-native';

type Props = any;
const BluetoothCard = ({ device }: Props) => {
const BluetoothCard = ({ device, isClassic, isScanningBLE }: Props) => {
const { colors } = useTheme();
if (isClassic) {
return (
<Card style={cardStyles.card}>
<Card.Title
title={`Name: ${device.name}`}
titleVariant="titleLarge"
subtitle={`ID: ${device.id}`}
left={() => <List.Icon icon={device.is_paired ? 'bluetooth' : 'bluetooth-off'} />}
/>
</Card>
);
}

let bgColor = colors.onPrimary; // 'rgba(225,225,225,1)'
if (isScanningBLE) {
bgColor = device.in_range ? `rgba(200,250,200,1)` : `rgba(250,200,200,1)`;
}

return (
<Card style={cardStyles.card}>
<Card style={{ backgroundColor: bgColor, ...cardStyles.card }}>
<Card.Title
title={`Name: ${device.name}`}
title={`Name: ${device.identifier}`}
titleVariant="titleLarge"
subtitle={`ID: ${device.id}`}
left={() => <List.Icon icon={device.is_paired ? 'bluetooth' : 'bluetooth-off'} />}
subtitle={`UUID: ...${device.uuid.slice(-13)}`} // e.g.,
left={() => <List.Icon icon={device.in_range ? 'access-point' : 'access-point-off'} />}
/>
</Card>
);
Expand Down
223 changes: 198 additions & 25 deletions www/js/bluetooth/BluetoothScanPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Modal, ScrollView, SafeAreaView, View } from 'react-native';
import gatherBluetoothData from './bluetoothScanner';
import { logWarn, displayError, displayErrorMsg } from '../plugin/logger';
import { StyleSheet, Modal, ScrollView, SafeAreaView, View, Text } from 'react-native';
import { gatherBluetoothClassicData } from './bluetoothScanner';
import { logWarn, displayError, displayErrorMsg, logDebug } from '../plugin/logger';
import BluetoothCard from './BluetoothCard';
import { Appbar, useTheme, Button } from 'react-native-paper';
import {
BLEBeaconDevice,
BLEPluginCallback,
BluetoothClassicDevice,
BLEDeviceList,
} from '../types/bluetoothDevices';

/**
* The implementation of this scanner page follows the design of
Expand All @@ -16,12 +22,36 @@ import { Appbar, useTheme, Button } from 'react-native-paper';

const BluetoothScanPage = ({ ...props }: any) => {
const { t } = useTranslation();
const [logs, setLogs] = useState<string[]>([]);
const [isScanning, setIsScanning] = useState(false);
const [bluetoothClassicList, setBluetoothClassicList] = useState<BluetoothClassicDevice[]>([]);
const [sampleBLEDevices, setSampleBLEDevices] = useState<BLEDeviceList>({
'426C7565-4368-6172-6D42-6561636F6E74': {
identifier: 'Katie_BLEBeacon',
minor: 4949,
major: 3838,
in_range: false,
},
'426C7565-4368-6172-6D42-6561636F6E73': {
identifier: 'Louis-Beacon',
minor: 4949,
major: 3838,
in_range: false,
},
});
const [isScanningClassic, setIsScanningClassic] = useState(false);
const [isScanningBLE, setIsScanningBLE] = useState(false);
const [isClassic, setIsClassic] = useState(false);
const { colors } = useTheme();

// Flattens the `sampleBeacons` into an array of BLEBeaconDevices
function beaconsToArray() {
return Object.entries(sampleBLEDevices).map(([uuid, device]) => ({
uuid,
...device,
}));
}

// Function to run Bluetooth Classic test and update logs
const runBluetoothTest = async () => {
async function runBluetoothClassicTest() {
// Classic not currently supported on iOS
if (window['cordova'].platformId == 'ios') {
displayErrorMsg('Sorry, iOS is not supported!', 'OSError');
Expand All @@ -40,26 +70,160 @@ const BluetoothScanPage = ({ ...props }: any) => {
}

try {
setIsScanning(true);
const newLogs = await gatherBluetoothData(t);
setLogs(newLogs);
setIsScanningClassic(true);
const newLogs = await gatherBluetoothClassicData(t);
setBluetoothClassicList(newLogs);
} catch (error) {
logWarn(error);
} finally {
setIsScanning(false);
setIsScanningClassic(false);
}
}

function setRangeStatus(uuid: string, status: boolean) {
setSampleBLEDevices((prevDevices) => ({
...prevDevices,
[uuid]: {
...prevDevices[uuid],
in_range: status,
},
}));
}

// BLE LOGIC
async function startBeaconScanning() {
setIsScanningBLE(true);

let delegate = new window['cordova'].plugins.locationManager.Delegate();

delegate.didDetermineStateForRegion = function (pluginResult: BLEPluginCallback) {
// `stateInside`is returned when the user enters the beacon region
// `StateOutside` is either (i) left region, or (ii) started scanner (outside region)
if (pluginResult.state == 'CLRegionStateInside') {
// need toUpperCase(), b/c callback returns with only lowercase values...
setRangeStatus(pluginResult.region.uuid.toUpperCase(), true);
} else if (pluginResult.state == 'CLRegionStateOutside') {
setRangeStatus(pluginResult.region.uuid.toUpperCase(), false);
}
logDebug('[BLE] didDetermineStateForRegion');
logDebug(JSON.stringify(pluginResult, null, 2));
window['cordova'].plugins.locationManager.appendToDeviceLog(
'[DOM] didDetermineStateForRegion: ' + JSON.stringify(pluginResult, null, 2),
);
};

delegate.didStartMonitoringForRegion = function (pluginResult) {
logDebug('[BLE] didStartMonitoringForRegion');
logDebug(JSON.stringify(pluginResult));
};

delegate.didRangeBeaconsInRegion = function (pluginResult) {
// Not seeing this called...
logDebug('[BLE] didRangeBeaconsInRegion');
logDebug(JSON.stringify(pluginResult));
};

window['cordova'].plugins.locationManager.setDelegate(delegate);

// Setup regions for each beacon
beaconsToArray().forEach((sampleBeacon: BLEBeaconDevice) => {
// Use NULL for wildcard
// Need UUID value on iOS only, not Android (2nd parameter)
// https://stackoverflow.com/questions/38580410/how-to-scan-all-nearby-ibeacons-using-coordova-based-hybrid-application
const beaconRegion = new window['cordova'].plugins.locationManager.BeaconRegion(
sampleBeacon.identifier,
sampleBeacon.uuid,
sampleBeacon.major,
sampleBeacon.minor,
);
window['cordova'].plugins.locationManager
.startMonitoringForRegion(beaconRegion)
.fail(function (e) {
logWarn(e);
})
.done();
});
}

async function stopBeaconScanning() {
setIsScanningBLE(false);

beaconsToArray().forEach((sampleBeacon: BLEBeaconDevice) => {
setRangeStatus(sampleBeacon.uuid, false); // "zero out" the beacons
const beaconRegion = new window['cordova'].plugins.locationManager.BeaconRegion(
sampleBeacon.identifier,
sampleBeacon.uuid,
sampleBeacon.major,
sampleBeacon.minor,
);
window['cordova'].plugins.locationManager
.stopMonitoringForRegion(beaconRegion)
.fail(function (e) {
logWarn(e);
})
.done();
});
}

const switchMode = () => {
setIsClassic(!isClassic);
};

const BluetoothCardList = ({ devices }) => {
if (isClassic) {
// When in classic mode, render devices as normal
return (
<div>
{devices.map((device) => {
if (device) {
return <BluetoothCard device={device} isClassic={isClassic} />;
}
return null;
})}
</div>
);
}
const beaconsAsArray = beaconsToArray();
return (
<div>
{beaconsAsArray.map((beacon) => {
if (beacon) {
return <BluetoothCard device={beacon} isScanningBLE={isScanningBLE} />;
}
})}
</div>
);
};

const BluetoothCardList = ({ devices }) => (
<div>
{devices.map((device) => {
if (device) {
return <BluetoothCard device={device} />;
}
return null;
})}
</div>
);
const ScanButton = () => {
if (isClassic) {
return (
<View style={s.btnContainer}>
<Button
mode="elevated"
onPress={runBluetoothClassicTest}
textColor={isScanningClassic ? colors.onPrimary : colors.primary}
buttonColor={isScanningClassic ? colors.primary : colors.onPrimary}
style={s.btn}>
{isScanningClassic ? t('bluetooth.is-scanning') : t('bluetooth.scan.for-bluetooth')}
</Button>
</View>
);
}
// else, if BLE
return (
<View style={s.btnContainer}>
<Button
mode="elevated"
onPress={isScanningBLE ? stopBeaconScanning : startBeaconScanning}
textColor={isScanningBLE ? colors.onPrimary : colors.primary}
buttonColor={isScanningBLE ? colors.primary : colors.onPrimary}
style={s.btn}>
{isScanningBLE ? t('bluetooth.scan.stop') : t('bluetooth.scan.for-ble')}
</Button>
</View>
);
};

const BlueScanContent = () => (
<div style={{ height: '100%' }}>
Expand All @@ -72,14 +236,23 @@ const BluetoothScanPage = ({ ...props }: any) => {
props.onDismiss?.();
}}
/>
<Appbar.Content title={t('bluetooth.scan-debug-title')} titleStyle={{ fontSize: 17 }} />
<Appbar.Content
title={isClassic ? t('bluetooth.title.classic') : t('bluetooth.title.ble')}
titleStyle={{ fontSize: 17 }}
/>
</Appbar.Header>
<View style={s.btnContainer}>
<Button mode="elevated" onPress={runBluetoothTest} textColor={colors.primary} style={s.btn}>
{isScanning ? t('bluetooth.is-scanning') : t('bluetooth.scan-for-bluetooth')}
<Button
mode="elevated"
onPress={switchMode}
textColor={colors.primary}
style={s.btn}
buttonColor={colors.onPrimary}>
{isClassic ? t('bluetooth.switch-to.ble') : t('bluetooth.switch-to.classic')}
</Button>
</View>
<BluetoothCardList devices={logs} />
<ScanButton />
<BluetoothCardList devices={isClassic ? bluetoothClassicList : sampleBLEDevices} />
</div>
);

Expand All @@ -98,7 +271,7 @@ const BluetoothScanPage = ({ ...props }: any) => {

const s = StyleSheet.create({
btnContainer: {
padding: 16,
padding: 8,
justifyContent: 'center',
},
btn: {
Expand Down
4 changes: 2 additions & 2 deletions www/js/bluetooth/bluetoothScanner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { logDebug, displayError } from '../plugin/logger';
import { BluetoothClassicDevice } from '../types/bluetoothTypes';
import { BluetoothClassicDevice } from '../types/bluetoothDevices';

/**
* gatherBluetoothData scans for viewable Bluetooth Classic Devices
* @param t is the i18next translation function
* @returns an array of strings containing device data, formatted ['ID: id Name: name']
*/
export default function gatherBluetoothData(t): Promise<string[]> {
export function gatherBluetoothClassicData(t): Promise<BluetoothClassicDevice[]> {
return new Promise((resolve, reject) => {
logDebug('Running bluetooth discovery test!');

Expand Down
Loading
Loading