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

feat(useBluetooth): new function #1694

Merged
merged 1 commit into from Jun 16, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -47,6 +47,7 @@
"@types/prettier": "^2.6.3",
"@types/semver": "^7.3.10",
"@types/sharp": "^0.30.4",
"@types/web-bluetooth": "^0.0.14",
"@vitest/ui": "^0.15.1",
"@vue/compiler-sfc": "^3.2.37",
"@vue/composition-api": "^1.6.2",
Expand Down
1 change: 1 addition & 0 deletions packages/core/index.ts
Expand Up @@ -12,6 +12,7 @@ export * from './useAsyncQueue'
export * from './useAsyncState'
export * from './useBase64'
export * from './useBattery'
export * from './useBluetooth'
export * from './useBreakpoints'
export * from './useBroadcastChannel'
export * from './useBrowserLocation'
Expand Down
44 changes: 44 additions & 0 deletions packages/core/useBluetooth/demo.vue
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { useBluetooth } from '.'

const {
isConnected,
isSupported,
device,
requestDevice,
error,
} = useBluetooth({
acceptAllDevices: true,
})
</script>

<template>
<div class="grid grid-cols-1 gap-x-4 gap-y-4">
<div>{{ isSupported ? 'Bluetooth Web API Supported' : 'Your browser does not support the Bluetooth Web API' }}</div>

<div v-if="isSupported">
<button @click="requestDevice()">
Request Bluetooth Device
</button>
</div>

<div v-if="device">
<p>Device Name: {{ device.name }}</p>
</div>

<div v-if="isConnected" class="bg-green-500 text-white p-3 rounded-md">
<p>Connected</p>
</div>

<div v-if="!isConnected" class="bg-orange-800 text-white p-3 rounded-md">
<p>Not Connected</p>
</div>

<div v-if="error">
<div>Errors:</div>
<pre>
<code class="block p-5 whitespace-pre">{{ error }}</code>
</pre>
</div>
</div>
</template>
109 changes: 109 additions & 0 deletions packages/core/useBluetooth/index.md
@@ -0,0 +1,109 @@
---
category: Browser
---

# useBluetooth

A reactive for working with the [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) which provides the ability to connect and interact with Bluetooth Low Energy peripherals.

The Web Bluetooth API lets websites discover and communicate with devices over the Bluetooth 4 wireless standard using the Generic Attribute Profile (GATT).

N.B. It is currently partially implemented in Android M, Chrome OS, Mac, and Windows 10. For a full overview of browser compatibility please see [Web Bluetooth API Browser Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility)

N.B. There are a number of caveats to be aware of with the web bluetooth API specification. Please refer to the [Web Bluetooth W3C Draft Report](https://webbluetoothcg.github.io/web-bluetooth/) for numerous caveats around device detection and connection.

N.B. This API is not available in Web Workers (not exposed via WorkerNavigator).

## Usage Default

```ts
import { useBluetooth } from '@vueuse/core'

const {
isSupported,
isConnected,
device,
requestDevice,
server,
} = useBluetooth({
acceptAllDevices: true,
})
```

```vue
<template>
<button @click="requestDevice()">
Request Bluetooth Device
</button>
</template>
```

When the device has paired and is connected, you can then work with the server object as you wish.

## Usage Battery Level Example

This sample illustrates the use of the Web Bluetooth API to read battery level and be notified of changes from a nearby Bluetooth Device advertising Battery information with Bluetooth Low Energy.

Here, we use the characteristicvaluechanged event listener to handle reading battery level characteristic value. This event listener will optionally handle upcoming notifications as well.

```ts
import { pausableWatch, useBluetooth } from '@vueuse/core'

const {
isSupported,
isConnected,
device,
requestDevice,
server,
} = useBluetooth({
acceptAllDevices: true,
optionalServices: [
'battery_service',
],
})

const batteryPercent = ref<undefined | number>()

const isGettingBatteryLevels = ref(false)

const getBatteryLevels = async () => {
isGettingBatteryLevels.value = true

// Get the battery service:
const batteryService = await server.getPrimaryService('battery_service')

// Get the current battery level
const batteryLevelCharacteristic = await batteryService.getCharacteristic(
'battery_level',
)

// Listen to when characteristic value changes on `characteristicvaluechanged` event:
batteryLevelCharacteristic.addEventListener('characteristicvaluechanged', (event) => {
batteryPercent.value = event.target.value.getUint8(0)
})

// Convert received buffer to number:
const batteryLevel = await batteryLevelCharacteristic.readValue()

batteryPercent.value = await batteryLevel.getUint8(0)
}

const { stop } = pausableWatch(isConnected, (newIsConnected) => {
if (!newIsConnected || !server.value || isGettingBatteryLevels.value)
return
// Attempt to get the battery levels of the device:
getBatteryLevels()
// We only want to run this on the initial connection, as we will use a event listener to handle updates:
stop()
})
```

```vue
<template>
<button @click="requestDevice()">
Request Bluetooth Device
</button>
</template>
```

More samples can be found on [Google Chrome's Web Bluetooth Samples](https://googlechrome.github.io/samples/web-bluetooth/).
7 changes: 7 additions & 0 deletions packages/core/useBluetooth/index.test.ts
@@ -0,0 +1,7 @@
import { useBluetooth } from '.'

describe('useBluetooth', () => {
it('should be defined', () => {
expect(useBluetooth).toBeDefined()
})
})
133 changes: 133 additions & 0 deletions packages/core/useBluetooth/index.ts
@@ -0,0 +1,133 @@
import { computed, ref, watch } from 'vue-demi'
import { tryOnMounted, tryOnScopeDispose } from '@vueuse/shared'
import type { ConfigurableNavigator } from '../_configurable'

import { defaultNavigator } from '../_configurable'

export interface UseBluetoothRequestDeviceOptions {
/**
*
* An array of BluetoothScanFilters. This filter consists of an array
* of BluetoothServiceUUIDs, a name parameter, and a namePrefix parameter.
*
*/
filters?: BluetoothLEScanFilter[] | undefined
/**
*
* An array of BluetoothServiceUUIDs.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/BluetoothRemoteGATTService/uuid
*
*/
optionalServices?: BluetoothServiceUUID[] | undefined
}

export interface UseBluetoothOptions extends UseBluetoothRequestDeviceOptions, ConfigurableNavigator {
/**
*
* A boolean value indicating that the requesting script can accept all Bluetooth
* devices. The default is false.
*
* !! This may result in a bunch of unrelated devices being shown
* in the chooser and energy being wasted as there are no filters.
*
*
* Use it with caution.
*
* @default false
*
*/
acceptAllDevices?: boolean
}

export function useBluetooth(options?: UseBluetoothOptions) {
let {
acceptAllDevices = false,
} = options || {}

const {
filters = undefined,
optionalServices = undefined,
navigator = defaultNavigator,
} = options || {}

const isSupported = navigator && 'bluetooth' in navigator

const device = ref<undefined | BluetoothDevice>(undefined)

const error = ref<unknown | null>(null)

watch(device, () => {
connectToBluetoothGATTServer()
})

async function requestDevice(): Promise<void> {
// This is the function can only be called if Bluetooth API is supported:
if (!isSupported)
return

// Reset any errors we currently have:
error.value = null

// If filters specified, we need to ensure we don't accept all devices:
if (filters && filters.length > 0)
acceptAllDevices = false

try {
device.value = await navigator?.bluetooth.requestDevice({
acceptAllDevices,
filters,
optionalServices,
})
}
catch (err) {
error.value = err
}
}

const server = ref<undefined | BluetoothRemoteGATTServer>()

const isConnected = computed((): boolean => {
return server.value?.connected || false
})

async function connectToBluetoothGATTServer() {
// Reset any errors we currently have:
error.value = null

if (device.value && device.value.gatt) {
// Add callback to gattserverdisconnected event:
device.value.addEventListener('gattserverdisconnected', () => {})

try {
// Connect to the device:
server.value = await device.value.gatt.connect()
}
catch (err) {
error.value = err
}
}
}

tryOnMounted(() => {
if (device.value)
device.value.gatt?.connect()
})

tryOnScopeDispose(() => {
if (device.value)
device.value.gatt?.disconnect()
})

return {
isSupported,
isConnected,
// Device:
device,
requestDevice,
// Server:
server,
// Errors:
error,
}
}