Skip to content

Commit

Permalink
Merge pull request #1135 from the-bay-kay/ble_scanner
Browse files Browse the repository at this point in the history
πŸ›œ α›’  Adding BLE Beacon Scanner
  • Loading branch information
shankari authored Mar 25, 2024
2 parents de0ed4c + 71abee3 commit c61ed71
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 45 deletions.
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

0 comments on commit c61ed71

Please sign in to comment.