@kandy-io/kandy-hid-sdk
Kandy HID SDK
The Kandy HID SDK enables application developers to handle HID device call operations.
The Kandy HID SDK abstracts HID device functions to the application developer in standard desktop and VDI environments. It is currently supported for use in Electron-based WebRTC applications running on Windows and Mac desktops and VDI environments. Note that use in an eLux VDI environments requires the Kandy VDI Toolkit.
The Kandy HID SDK currently supports the following Jabra headsets and configurations, with support for HID devices from other vendors possible.
Device Make / Model | Desktop | VDI eLux | VDI Windows | VDI Mac | Firmware |
---|---|---|---|---|---|
Jabra Engage 65 | ☑ | ☑ | ☑ | ☑ | 2.0.5, 3.4.1 |
Jabra Engage 50 | ☑ | ☑ | ☑ | ☑ | 1.24.0, 2.3.1 |
Jabra Speak 750 | ☑ | ☑ | ☑ | ☑ | 2.24.0 |
Jabra Evolve2 40 | ☑ | ☑ | ☑ | ☑ | 1.19.0 |
As of version 2.0.0, the Kandy HID SDK is to be used in Electron's Renderer process, rather than the Main process as in 1.x releases. There is still a requirement to require
the SDK in Electron's Main Process to maintain backwards-compatibility for configurations where WebHID is not yet used. See Installation and initializeHIDDevices
below.
Refer to the README for version 1.x for instructions on using 1.x versions of the SDK in Electron's main process.
Installation
The SDK is now shipped as a single package with 3 components:
1- one to be used in the local Electron-based app running on the Windows server in Electron's Main process
2- one to be used in the local Electron-based app running on the Windows server in Electron's Renderer process
3- one to be used in the remote application - the one running within the VDI Toolkit software on the client PC (eLux Thin Client, Windows/Mac personal PC, etc.)
Only the top-level package must be added to your package.json (same as in 1.x)
1- yarn add file:<path_to_file>/distant-kandy-hid-v2.0.0.tgz
Logging
All logs generated by kandy-hid have prefix HID:
or VDIHID:
and are sent to console.
API
- All of the API’s below are to be called from Electron’s Renderer process in the local app (as opposed to the remote) unless otherwise noted
- These are listed in logical order of typical usage
Initialization (initializeHIDDevices)
As in version 1.x, initializeHIDDevices
is used to initialize the SDK.
In order to initialize the local application, the first 2 libraries in the list above must be imported:
require
the top-level library in Electron's Main process (similar to 1.x)
require('@distant/kandy-hid')
In the local web application (Renderer), import initializeHIDDevices from '@distant/kandy-hid/local'.
import { initializeHIDDevices } from '@distant/kandy-hid/local'
The local library must then be initialized with an object.
- A
mode
key is required. Valid values formode
are'VDI'
or'desktop'
. - A
driverInfo
object is required in VDI mode. The value fordriverInfo
is the object returned from issuinggetInfo()
on the vchannel in the Main process.driverInfo
can be ignored in Desktop mode.
Examples:
Initializing the local instance for Desktop
If your app is operating in a Desktop environment, you can call initializeHIDDevices
as early as you like during your app's initialization. In this case, mode = 'desktop'
and driverInfo
need not be included.
const kandyHID = initializeHIDDevices({ mode: 'desktop' })
Initializing the local instance for VDI
If your app is operating in a VDI environment, initialization of the SDK must be deferred until after the VDI channel has been opened. This is necessary because the value of driverInfo
must be retrieved from the remote client. The driverInfo
data is the information provided by the Kandy Distant Driver for VDI via the getInfo()
API.
For example:
In your Main process code, where the channel used by the main VDI session is opened:
const { ipcMain } = require('electron')
const vchannel = require('@distant/vchannel')
let driverInfo
async function openChannel() {
const channel = await vchannel.openVirtualChannel('RIBBON')
driverInfo = await channel.getInfo()
}
ipcMain.on('getDriverInfo', event => {
event.returnValue = driverInfo
})
In the Renderer process code, at the point where KandyHID will be initialized:
const mode = 'VDI'
// retrieve driverInfo from the Main process
const driverInfo = ipcRenderer.sendSync('getDriverInfo')
const kandyHID = initializeHIDDevices({ mode, driverInfo })
Return value
Regardless of mode, initializeHIDDevices
returns an object that contains the following:
1- an event emitter
This emitter emits HIDFunctionRequest
events, replacing ipcRenderer.on('HIDFunctionRequest', ...)
in 1.x. See HIDFunctionRequest
below.
2- Functions allowHIDDeviceOpens
, invokeHIDFunction
, isSupportedDevice
, selectHIDDevice
, setChannel
, storeMainWindowID
. See their usage below.
Initializing the Remote (VDI only)
All that's required to initialize the remote is to call setChannel
with a valid channel object. See below.
setChannel (VDI only)
It's necessary to call this function on both the local and remote sides when in a VDI environment. In both cases, the function parameter is an object that contains a send
function that will send a message over the channel to the far end. The object should also contain a key called receive
that has a value of undefined
. The KandyHID SDK will insert its own receive function in its place. Note this is the same method used to initialize the KandyJS SDK.
Calling this function in a desktop environment has no effect.
Local
On the local side, the setChannel
function is part of the object returned by initializeHIDDevices
.
const { setChannel } = kandyHID
Remote
On the remote side, setChannel
is imported directly from the remote library:
import { setChannel } from '@distant/kandy-hid/remote'
Usage is the same on both local and remote sides:
const HIDChannel = {
send: message => {
yourSendFunction('hid', message)
},
receive: undefined
}
setChannel(HIDChannel)
// handle incoming messages from the far end
yourReceiveFunction(messageType, ...data) {
switch (messageType) {
case 'hid':
HIDChannel.receive(...data)
break
...
}
}
storeMainWindowID(id)
Parameters:
Type: number
Default: none
kandy-hid emits actions/events in the Electron Renderer on the HIDFunctionRequest
event. In order to do this, it needs the window id of the Electron renderer process that should receive these messages.
Example:
In Renderer process:
const electronRemote = require('electron').remote
kandyHID.storeMainWindowID(
elecronRemote.getCurrentWindow().id
)
isSupportedDevice(label)
Parameters
Type: string
Returns: boolean
Allows the app to query kandy-hid whether a device is supported for use or not. Returns true/false.
Example:
In the Renderer process:
function selectMicrophone(deviceObject) {
const result = kandyHID.isSupportedDevice(deviceObject.label);
if (!result) {
console.log('unsupported device selected...')
}
kandyHID.selectHIDDevice('microphone', deviceObject);
}
This API will return true for supported devices listed in the introductory section and for 'Jabra Evolve 80'. It will return false for any other string.
Note that the device label passed in is compared against the device names as they appear in the introduction. The label must contain one of these names in order for this API to return true - it does not have to match exactly.
Examples:
kandyHID.isSupportedDevice('G/N Audio Jabra Speak 750'); // true
kandyHID.isSupportedDevice('abra Speak 7'); // false
Also note that isSupportedDevice()
should not be used to filter devices before selecting them via selectHIDDevice()
. For proper operation the SDK should be informed when user selections change, whether a supported HID device is selected or not.
selectHIDDevice(deviceType, deviceInformation)
Parameters
deviceType:
Type: string
Options: 'microphone' || 'speakers' || 'alert_speakers'
deviceInformation:
Type: object
Registers an association between a given HID device (e.g. Jabra Engage 65) and a media device type (i.e. microphone). Once associated, HID functions (see invokeHIDFunction()
) related to microphone (e.g. mute) will be performed using the specified device.
The deviceInformation
object must contain a label
property.
This label
(string) must contain one of the supported device make/models exactly as listed in the table at the top of this document (the label can contain other text too, but this string must exist).
allowHIDDeviceOpens(boolean)
Parameters
Type: boolean
Default: true
Other options: false
Allow or disallow HID devices from being opened. Defaults to true. Devices should be closed before exiting an application; attempting to exit an app with an open device may cause issues. Given that, depending on your app design it may be necessary to prevent devices from being opened (after they've been closed) during app shutdown procedures.
This is included for backwards-compatibility only. The SDK closes devices automatically during app exit.
invokeHIDFunction(operation)
Parameters
Type: string
Default: none
Options:
'call_start': Tells kandy-hid an outgoing call has started; causes the device to go offHook.
'call_accept': Tells kandy-hid an incoming call has started; causes the device to go offHook.
'call_reject': Tells kandy-hid an incoming call has been rejected; returns the device to its previous state (idle, on call, ...)
'call_end': Tells kandy-hid an active call has ended; returns the device to default state (from offHook, muted)
'call_mute': Instructs kandy-hid to mute the HID device. On some devices this causes visual or audible alerts.
'call_unmute': Instructs kandy-hid to unmute the HID device. On some devices this causes visual or audible alerts.
'start_ringing': Instructs kandy-hid to cause the HID device to start ringing (incoming call)
'stop_ringing': Instructs kandy-hid to cause the HID device to stop ringing. It's not necessary to call 'stop_ringing' when a call is answered, ended or rejected, ringing is stopped inherently by those actions.
'call_hold': Instructs the HID device to perform a call hold action. On some devices this causes visual or audible alerts.
'call_resume': Instructs the HID device to perform a call unhold action. On some devices this causes visual or audible alerts.
'offhook': Instructs the HID device to go offhook.
'onhook': Instructs the HID device to go onhook.
'reset': Resets the device to default state.
'calls_on_hold': Takes a true/false parameter. Informs kandy-hid when calls are put on hold in the app (1)
The above are instructions from your app to kandy-hid, requesting that the HID device enter a certain state or perform a specific state change.
1 See details regarding 'calls_on_hold' in call swap documentation.
HIDFunctionRequest(operation)
When a user performs an action on a HID device, kandy-hid will send an interpretation of that action to the app. For example, if the user presses the mute button during an active call, kandy-hid will send a 'call_mute' operation to the app.
These messages are emitted in the Electron renderer process window identified by storeMainWindowID()
as an event. The web app must have an handler to receive these events. The device-initiated events have identifier HIDFunctionRequest
. These events are emitted on the object returned by initializeHIDDevices()
.
const kandyHID = initializeHIDDevices({ mode })
kandyHID.on('HIDFunctionRequest', operation => {
switch (operation) {
case 'call_start':
// start a call in your app
break;
case 'call_mute':
// mute an active call in your app
break;
...
}
})
window.addEventListener('beforeunload', () => {
kandyHID.off('HIDFunctionRequest')
})
'operation' will be one of the following:
Type: string
Default: none
Options:
'call_start': kandy-hid is telling your app the device has gone offhook, in an attempt to start a call (see Known Issues)
'call_accept': kandy-hid is telling your app that a ringing call has been answered on the device
'call_reject': an incoming ringing call has been rejected by the device
'call_end': an active call has been ended by the device (the device has gone onhook)
'call_mute': an active call has been muted on the HID device
'call_unmute': an active muted call has been unmuted on the HID device
'call_hold': an active call has been put on hold from the device
'call_resume': a held call has been taken off of hold from the device
'call_swap': the user has indicated the desire to swap between an active and a held call (1)
'device_error': (VDI eLux/Mac only) kandy-hid has detected a previously connected device has been disconnected (power loss or physical disconnection)
'channel_error': (VDI eLux only) kandy-hid has detected a loss of communication with the Thin Client
IMPORTANT you'll notice that the list of operations sent up to your app as a result of someone having taken an action on the device are a subset of the operations your app sends to kandy-hid via invokeHIDFunction()
. That is not coincidental!
When kandy-hid notifies your app of a state change, it's important that you take whatever actions are necessary within your app (i.e. invoke your mute function), but also replay the operation back to kandy-hid to update the device's state1. This is necessary to keep your app, the SDK and the device in sync.
For example, if a user presses the mute button on a HID device during an active call, the device does not enter the mute state immediately; to complete a mute operation, your app should mute the active call and then send a 'call_mute' instruction to kandy-hid to mute the device. If for whatever reason your app fails to mute the active call, it should not replay the mute instruction to kandy-hid, again keeping everything in sync.
Your app is in charge of updating device state via this SDK. This SDK does not modify device state unless instructed to do so (other than in error scenarios).
1 Do not replay operations 'device_error', 'channel_error' or 'call_swap' operation back to kandy-hid. For a device-initiated call swap, your app should send kandy-hid 'call_hold' followed by 'call_resume'. See details regarding 'call_swap' in call swap documentation.
Use Cases
Actions taken from within the app:
- For an incoming call, sending 'start_ringing' will cause the device (base + headset (if connected)) to perform its alerting function (ringing, blinking, etc.)
- Ringing will stop if any subsequent call state change occurs (answer, reject, etc.)
- You can instead send 'stop_ringing' explicitly if no call state transition will occur (e.g. as a test of device alerting)
- Starting (originating) a call: sending 'call_start' to the device will cause the device to go offhook and attach to the active call.
- Answering an incoming call: sending 'call_accept' to the device will cause the device to go offhook and attach to the active call.
- Rejecting an incoming call: while the device is in ringing state, sending 'call_reject' will return the device to its previous state
- Ending a call: sending 'call_end' to the device will cause the device to hang up.
- Muting / unmuting an active call: sending 'call_mute' / 'call_unmute' will cause the device to perform the requested operation.
- Holding / resuming a call: sending 'call_hold' / 'call_resume' will cause the device to perform the requested operation.
- Performing a call swap: when preconditions are met, sending a 'call_hold' followed immediately by 'call_resume' will cause the device to perform swap between active and held calls. See call swap documentation.
Actions performed on the device:
Answering an incoming call
An incoming ringing call can be answered by:
- undocking the headset from the base (if present)
- pressing the MultiFunction button on the headset
- pressing the green Call Start/Answer button on the base or the Call button for single-button devices
- lowering the microphone boom (Evolve2 40 only) (providing the device is not already on a call)
The answer action will be passed up to the app as a 'call_accept' operation on the HIDFunctionRequest event.
In order to answer an incoming call while the device is already active on a call, the device's call hold/resume action must be performed. See Hold/Resume below.
Originating an outgoing call
If the device goes off hook using any of the methods described previously, it will send a 'call_start' operation on the HIDFunctionRequest event. It's then up to your app to take the appropriate action to start a call. If your app cannot successfully start a call in that case, you should send 'call_failure', followed by 'call_failure_finish' 1 second later to return the device to default state (since it may be in the offhook state).
NOTE that the Engage 65 base must be in "Soft Phone Mode" prior to going offhook. This can be accomplished by pressing the MultiFunction button or the Call Answer button for 1 second prior to removing the headset from the base. See device User Manuals for more details.
kandyHID.on('HIDFunctionRequest', (operation) => {
switch (operation) {
case 'call_start':
if (allConditionsMet)
yourStartCallFunction();
else {
kandyHID.invokeHIDFunction('call_failure')
setTimeout(() => kandyHID.invokeHIDFunction('call_failure_finish', 1000));
}
break;
}
});
Ending an active call
An active call can be ended by:
- replacing the headset on the base
- pressing the MultiFunction button on the headset
- pressing the red Call End button on the base or the Call button for single-button devices
Muting / unmuting an active call
Once on an active call, the call can be muted or unmuted from the device by pressing the Mute button on the base. On the Evolve2 40, the call can also be muted by raising the microphone boom and unmuted by lowering it.
The new mute state will be passed up to the app as 'call_mute' or 'call_unmute'.
Hold / Resume
When the device is engaged in an active call, the call can be placed on hold or resumed by:
Jabra Engage 65
- pressing the green Call Answer button on the base
- pressing and holding the Multi-Function button on the headset for 1-2 seconds
Jabra Engage 50
- pressing and holding the Call Answer / End button on the base for 1-2 seconds
Jabra Speak 750
- pressing the green Call Answer button on the base
Jabra Evolve2 40
- pressing and holding the Multi-Function button on the headset for 1-2 seconds
Call Reject
Rejecting an incoming call can be accomplished by:
Jabra Engage 65
- pressing the red Call End button on the base
- double-clicking the Multi-Function button on the headset
Jabra Engage 50
- double-clicking the Call Answer / End button on the base
Jabra Speak 750
- pressing the red Call End button on the base
Jabra Evolve2 40
- double-clicking the Multi-Function button on the headset
Call Swap
- When preconditions are met, performing a 'call_hold' action on the device (see above) will signal the controlling application to swap between the active and a held call. See call swap documentation.
Error Handling
Most errors are handled gracefully within kandy-hid - device connection, disconnection, power off, etc. Keep an eye on logs.
It's natural during app development that you may put the device into a state that is out of sync with your app. In that case, send it a 'reset' to return kandy-hid and the device to default state.
Device Error
In eLux and Mac VDI, if the device is powered off or disconnected from the Client kandy-hid will emit device_error
on the HIDFunctionRequest
event.
kandyHID.on('HIDFunctionRequest', operation => {
switch (operation) {
case 'device_error':
// Take whatever actions you deem appropriate - e.g. alert the user
console.error('kandy-hid has reported a device error);
break;
}
}
Channel Error
In VDI eLux mode, if communication between kandy-hid software running within the Electron app and the Kandy HID Driver for VDI running on the Thin Client is lost for any reason over the virtual communication channel, kandy-hid will emit channel_error
on the HIDFunctionRequest
event.
The kandy-hid channel will remain down and not automatically attempt to reconnect. Once your app chooses to reestablish communication, reissue initializeHIDDevices()
(mode
-- 'desktop'
vs 'VDI'
is not required in this case), followed by all necessary selectHIDDevice
's.
kandyHID.on('HIDFunctionRequest', operation => {
switch (operation) {
case 'channel_error':
console.error('kandy-hid has reported a communication error over the virtual channel');
break;
}
}
Known Issues / Limitations
- The same device must be selected as active microphone, speakers and alert speakers via
selectHIDDevice()
. - Going offhook on the Jabra Engage 65, Speak 750 or Evolve 2/40 may not cause 'call_start' to be sent up to the app due to non-deterministic behaviour of these devices in this scenario. Support tickets (272, 277) have been created with the device vendor.
- In VDI eLux, the Jabra Speak 750 is known to conflict with either the mouse or keyboard when offhook. The issue has been addressed by the vendor in the RP6 / 64-bit version of the eLux OS image. There are no plans to address it in the RP5 / 32-bit version.
- See limitations relating to use of older versions of Kandy HID Driver for VDI in compatibility documentation.
- In Windows VDI, the LEDs on the Jabra Engage 50 may not be responsive if the device is not at factory default settings. If the Engage 50 LEDs are not changing state during call operations, first disconnect and reconnect the device. If the condition persists, reset the device using the latest available version of Jabra Direct. Note that kandy-hid always assumes devices are at factory default settings.
- In eLux VDI, it has been found that connecting a HID device to the Thin Client may cause eLux to select it as the system default device. As well, the previously selected device may be muted. Both of these state changes may affect in-progress and future calls until rectified. Resolution is to unmute and re-select devices in the eLux system menu. This is not related to the Kandy HID libraries but standard eLux behaviour.
- If a Jabra Engage 50 is using firmware version 2.3.1, it will not respond correctly to the 'stop_ringing' instruction; the headset will stop blinking, but the Link device will continue to blink indefinitely. The only known way to resolve the situation is to unplug and reconnect the device from the host PC. This issue affects all configurations when firmware 2.3.1 is used. A support ticket has been created with the device vendor (724), who confirm that a fix will be provided in a future version of firmware, currently scheduled for late 2021. Firmware version 1.24 works correctly and so is recommended instead.
- Issuing a call hold action from a HID device (e.g. a long-press on a single-buttoned device) may cause Apple Music on the client to start in a Mac VDI environment. This is native MacOS behaviour and not related to the Kandy HID libraries.
- Use of Microsoft Teams will prevent this SDK from reliably controlling HID devices. To ensure predictable behaviour, MS Teams should not be installed on the same PC where an application using this SDK is in use.
Backwards Compatibility
When used in a Citrix eLux VDI environment, this SDK is backwards-compatible with the most recent and one (1) previous version of Kandy HID Driver for VDI (DLL) (official releases only). This is intended to allow Kandy HID Driver for VDI upgrades on the Thin Client installed-base to lag behind application updates.
Example:
compatibility matrix for actual Kandy HID Driver for VDI and Kandy HID SDK version compatibility information.
Note these are fictional release values for purposes of illustration only; see theKandy HID Driver for VDI Version | Kandy HID SDK Version | Explanation |
---|---|---|
1.0 | 1.0 | Initial release |
- | 1.1 |
|
1.2 | 1.2 |
|
1.3 | 1.3 |
|
Electron Security
The kandy-hid SDK is compatible with recent Electron security measures such as context-isolation. To use context-isolation with this SDK, a one-line change is required to your preload script. See details here.
Enabling or disabling other Electron security features such as nodeIntegration, webSecurity and enableRemoteModule have no effect on this SDK's operation.
CHANGELOG
See CHANGELOG.