Skip to content

Commit

Permalink
feat: add support for WebHID
Browse files Browse the repository at this point in the history
Update docs/api/session.md

Co-authored-by: Charles Kerr <charles@charleskerr.com>

update docs

add docs
  • Loading branch information
jkleinsc committed Sep 2, 2021
1 parent 5554de0 commit 096c418
Show file tree
Hide file tree
Showing 36 changed files with 2,037 additions and 3 deletions.
1 change: 1 addition & 0 deletions BUILD.gn
Expand Up @@ -379,6 +379,7 @@ source_set("electron_lib") {
"//ppapi/shared_impl",
"//printing/buildflags",
"//services/device/public/cpp/geolocation",
"//services/device/public/cpp/hid",
"//services/device/public/mojom",
"//services/proxy_resolver:lib",
"//services/video_capture/public/mojom:constants",
Expand Down
150 changes: 149 additions & 1 deletion docs/api/session.md
Expand Up @@ -180,6 +180,99 @@ Emitted when a hunspell dictionary file download fails. For details
on the failure you should collect a netlog and inspect the download
request.

#### Event: 'select-hid-device'

Returns:

* `event` Event
* `details` Object
* `deviceList` [HIDDevice[]](structures/hid-device.md)
* `webContents` [WebContents](web-contents.md)
* `callback` Function
* `deviceId` String

Emitted when a HID device needs to be selected when a call to
`navigator.hid.requestDevice` is made. `callback` should be called with
`deviceId` to be selected, passing an empty string to `callback` will
cancel the request. Additionally, permissioning on `navigator.hid` can
be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler).

```javascript
const { app, BrowserWindow } = require('electron')

let win = null

app.whenReady().then(() => {
win = new BrowserWindow({
width: 800,
height: 600
})

win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'hid') {
// Add logic here to determine if permission should be given to allow HID selection
return true
}
})

// Retrieve previously persisted devices from an (optional) store here
const grantedDevices = []

win.webContents.session.setDevicePermissionHandler((details) => {
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
if (details.device.vendorId === 123 && details.device.productId === 345) {
// Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
return true
}

// Search through the list of devices that have previously been granted permission
return grantedDevices.some((grantedDevice) => {
return grantedDevice.vendorId === details.device.vendorId &&
grantedDevice.productId === details.device.productId &&
grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
})
}
return false
})

win.webContents.session.on('select-hid-device', (event, details, callback) => {
event.preventDefault()
const selectedDevice = details.deviceList.find((device) => {
return device.vendorId === '9025' && device.productId === '67'
})
if (!selectedDevice) {
callback('')
} else {
grantedDevices.push(device)
callback(selectedDevice.deviceId)
}
})
})
```

#### Event: 'hid-device-added'

Returns:

* `event` Event
* `details` Object
* `device` [HIDDevice[]](structures/hid-device.md)
* `webContents` [WebContents](web-contents.md)

Emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired if a new HID device becomes available. For example, this event will fire when a new USB device is plugged in.

#### Event: 'hid-device-removed'

Returns:

* `event` Event
* `details` Object
* `device` [HIDDevice[]](structures/hid-device.md)
* `webContents` [WebContents](web-contents.md)

Emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired if a HID device has been removed. For example, this event will fire when a USB device is unplugged.

#### Event: 'select-serial-port'

Returns:
Expand Down Expand Up @@ -525,7 +618,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents

* `handler` Function\<Boolean> | null
* `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively.
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, or `serial`.
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`.
* `requestingOrigin` String - The origin URL of the permission check
* `details` Object - Some properties are only available on certain permission types.
* `embeddingOrigin` String (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks.
Expand Down Expand Up @@ -553,6 +646,61 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
})
```

#### `ses.setDevicePermissionHandler(handler)`

* `handler` Function\<Boolean> | null
* `details` Object
* `deviceType` String - The type of device that permission is being requested on, can be `hid`.
* `origin` String - The origin URL of the device permission check.
* `device` [HIDDevice](structures/hid-device.md) - the device that permission is being requested for.
* `webContents` [WebContents](web-contents.md) - WebContents checking the device permission.

Sets the handler which can be used to respond to device permission checks for the `session`.
Returning `true` will allow the device to be permitted and `false` will reject it.
To clear the handler, call `setDevicePermissionHandler(null)`.
This handler can be used to provide default permissioning to devices without first calling for permission
to devices (eg via `navigator.hid.requestDevice`). If this handler is not defined, the default device
permissions as granted through device selection (eg via `navigator.hid.requestDevice`) will be used.
Additionally, the default behavior of Electron is to store granted device permision through the lifetime
of the corresponding WebContents. If longer term storage is needed, a developer can store granted device
permissions (eg when handling the `select-hid-device` event) and then read from that storage with `setDevicePermissionHandler`.

```javascript
const { session } = require('electron')
const url = require('url')
const grantedDevices = // Retrieve previously persisted devices from an (optional) store here

session.fromPartition('some-partition').setDevicePermissionHandler((details) => {
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
if (details.device.vendorId === 123 && details.device.productId === 345) {
// Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
return true // Approved permission
}

// Search through the list of devices that have previously been granted permission
return grantedDevices.some((grantedDevice) => {
return grantedDevice.vendorId === details.device.vendorId &&
grantedDevice.productId === details.device.productId &&
grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
})
}
return false // Denied permission
})

session.fromPartition('some-partition').on('select-hid-device', (event, details, callback) => {
event.preventDefault()
const selectedDevice = details.deviceList.find((device) => {
return device.vendorId === '9025' && device.productId === '67'
})
if (!selectedDevice) {
callback('')
} else {
// Persist to grantedDevices here
callback(selectedDevice.deviceId)
}
})
```

#### `ses.clearHostResolverCache()`

Returns `Promise<void>` - Resolves when the operation is complete.
Expand Down
8 changes: 8 additions & 0 deletions docs/api/structures/hid-device.md
@@ -0,0 +1,8 @@
# HIDDevice Object

* `deviceId` String - Unique identifier for the device.
* `name` String - Name of the device.
* `vendorId` Integer - The USB vendor ID.
* `productId` Integer - The USB product ID.
* `serialNumber` String (optional) - The USB device serial number.
* `guid` String (optional) - Unique identifier for the HID interface. A device may have multiple HID interfaces.
17 changes: 17 additions & 0 deletions docs/fiddles/features/web-bluetooth/index.html
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Web Bluetooth API</title>
</head>
<body>
<h1>Web Bluetooth API</h1>

<button id="clickme">Test Bluetooth</button>

<p>Currently selected bluetooth device: <strong id="device-name""></strong></p>

<script src="./renderer.js"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions docs/fiddles/features/web-bluetooth/main.js
@@ -0,0 +1,30 @@
const {app, BrowserWindow} = require('electron')
const path = require('path')

function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
})

mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
event.preventDefault()
if (deviceList && deviceList.length > 0) {
callback(deviceList[0].deviceId)
}
})

mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
8 changes: 8 additions & 0 deletions docs/fiddles/features/web-bluetooth/renderer.js
@@ -0,0 +1,8 @@
async function testIt() {
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true
})
document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}`
}

document.getElementById('clickme').addEventListener('click',testIt)
21 changes: 21 additions & 0 deletions docs/fiddles/features/web-hid/index.html
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>WebHID API</title>
</head>
<body>
<h1>WebHID API</h1>

<button id="clickme">Test WebHID</button>

<h3>HID devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
<div id="granted-devices"></div>

<h3>HID devices automatically granted access via <i>select-hid-device</i></h3>
<div id="granted-devices2"></div>

<script src="./renderer.js"></script>
</body>
</html>
50 changes: 50 additions & 0 deletions docs/fiddles/features/web-hid/main.js
@@ -0,0 +1,50 @@
const {app, BrowserWindow} = require('electron')
const path = require('path')

function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
})

mainWindow.webContents.session.on('select-hid-device', (event, details, callback) => {
event.preventDefault()
if (details.deviceList && details.deviceList.length > 0) {
callback(details.deviceList[0].deviceId)
}
})

mainWindow.webContents.session.on('hid-device-added', (event, device) => {
console.log('hid-device-added FIRED WITH', device)
})

mainWindow.webContents.session.on('hid-device-removed', (event, device) => {
console.log('hid-device-removed FIRED WITH', device)
})

mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'hid' && details.securityOrigin === 'file:///') {
return true
}
})

mainWindow.webContents.session.setDevicePermissionHandler((details) => {
if (details.deviceType === 'hid' && details.origin === 'file://') {
return true
}
})

mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
19 changes: 19 additions & 0 deletions docs/fiddles/features/web-hid/renderer.js
@@ -0,0 +1,19 @@
async function testIt() {
const grantedDevices = await navigator.hid.getDevices()
let grantedDeviceList = ''
grantedDevices.forEach(device => {
grantedDeviceList += `<hr>${device.productName}</hr>`
})
document.getElementById('granted-devices').innerHTML = grantedDeviceList
const grantedDevices2 = await navigator.hid.requestDevice({
filters: []
})

grantedDeviceList = ''
grantedDevices2.forEach(device => {
grantedDeviceList += `<hr>${device.productName}</hr>`
})
document.getElementById('granted-devices2').innerHTML = grantedDeviceList
}

document.getElementById('clickme').addEventListener('click',testIt)
16 changes: 16 additions & 0 deletions docs/fiddles/features/web-serial/index.html
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Web Serial API</title>
<body>
<h1>Web Serial API</h1>

<button id="clickme">Test Web Serial API</button>

<p>Matching Arduino Uno device: <strong id="device-name""></strong></p>

<script src="./renderer.js"></script>
</body>
</html>

0 comments on commit 096c418

Please sign in to comment.