diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 182212b49..c3f7ec8d3 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -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": { @@ -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", diff --git a/setup/setup_shared_native.sh b/setup/setup_shared_native.sh index 87e652561..dfc52f072 100644 --- a/setup/setup_shared_native.sh +++ b/setup/setup_shared_native.sh @@ -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 ]; diff --git a/www/i18n/en.json b/www/i18n/en.json index ac4c87469..ffbfb8ea8 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -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" } }, diff --git a/www/js/bluetooth/BluetoothCard.tsx b/www/js/bluetooth/BluetoothCard.tsx index a8c2a4e08..4af8240c4 100644 --- a/www/js/bluetooth/BluetoothCard.tsx +++ b/www/js/bluetooth/BluetoothCard.tsx @@ -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 ( + + } + /> + + ); + } + + 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 ( - + } + subtitle={`UUID: ...${device.uuid.slice(-13)}`} // e.g., + left={() => } /> ); diff --git a/www/js/bluetooth/BluetoothScanPage.tsx b/www/js/bluetooth/BluetoothScanPage.tsx index e3c21858e..52996b321 100644 --- a/www/js/bluetooth/BluetoothScanPage.tsx +++ b/www/js/bluetooth/BluetoothScanPage.tsx @@ -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 @@ -16,12 +22,36 @@ import { Appbar, useTheme, Button } from 'react-native-paper'; const BluetoothScanPage = ({ ...props }: any) => { const { t } = useTranslation(); - const [logs, setLogs] = useState([]); - const [isScanning, setIsScanning] = useState(false); + const [bluetoothClassicList, setBluetoothClassicList] = useState([]); + const [sampleBLEDevices, setSampleBLEDevices] = useState({ + '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'); @@ -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 ( +
+ {devices.map((device) => { + if (device) { + return ; + } + return null; + })} +
+ ); } + const beaconsAsArray = beaconsToArray(); + return ( +
+ {beaconsAsArray.map((beacon) => { + if (beacon) { + return ; + } + })} +
+ ); }; - const BluetoothCardList = ({ devices }) => ( -
- {devices.map((device) => { - if (device) { - return ; - } - return null; - })} -
- ); + const ScanButton = () => { + if (isClassic) { + return ( + + + + ); + } + // else, if BLE + return ( + + + + ); + }; const BlueScanContent = () => (
@@ -72,14 +236,23 @@ const BluetoothScanPage = ({ ...props }: any) => { props.onDismiss?.(); }} /> - + - - + +
); @@ -98,7 +271,7 @@ const BluetoothScanPage = ({ ...props }: any) => { const s = StyleSheet.create({ btnContainer: { - padding: 16, + padding: 8, justifyContent: 'center', }, btn: { diff --git a/www/js/bluetooth/bluetoothScanner.ts b/www/js/bluetooth/bluetoothScanner.ts index 2e77c30b0..d7cb2d297 100644 --- a/www/js/bluetooth/bluetoothScanner.ts +++ b/www/js/bluetooth/bluetoothScanner.ts @@ -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 { +export function gatherBluetoothClassicData(t): Promise { return new Promise((resolve, reject) => { logDebug('Running bluetooth discovery test!'); diff --git a/www/js/types/BluetoothDevices.ts b/www/js/types/BluetoothDevices.ts new file mode 100644 index 000000000..e628731e2 --- /dev/null +++ b/www/js/types/BluetoothDevices.ts @@ -0,0 +1,32 @@ +// Device data, as defined in BluetoothClassicSerial's docs +export type BluetoothClassicDevice = { + class: number; + id: string; + address: string; + name: string; + is_paired?: boolean; // We keep track of this, because BCS doesn't +}; + +/* Config File containg BLEBeaconData, mapped in the format + * UID_KEY: {Device_Info} + * + * This is set up for how a JSON file would store this data; we + * will most likely change this later on! + */ + +export type BLEBeaconDevice = { + identifier: string; + uuid: string; + major: number; + minor: number; + type_name?: string; // e.g., "BeaconRegion"; used for callback +}; +export type BLEDeviceList = { + [key: string]: { identifier: string; minor: number; major: number; in_range: boolean }; +}; + +export type BLEPluginCallback = { + region: BLEBeaconDevice; + eventType: string; + state: string; +}; diff --git a/www/js/types/bluetoothTypes.ts b/www/js/types/bluetoothTypes.ts deleted file mode 100644 index c812966c2..000000000 --- a/www/js/types/bluetoothTypes.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Device data, as defined in BluetoothClassicSerial's docs -export type BluetoothClassicDevice = { - class: number; - id: string; - address: string; - name: string; - is_paired?: boolean; // We keep track of this, BCS doesn't -};