# @kandy-io/kandy-hid-sdk

## Kandy HID SDK

The Kandy HID SDK enables application developers to handle HID device call operations.<br>

The Kandy HID SDK abstracts HID device functions to the application developer in standard desktop and VDI environments. It is currently supported for Electron based WebRTC applications running on Windows and Mac desktops. It is also supported in VDI environments using the [Kandy VDI Toolkit](https://github.com/Kandy-IO/kandy-vdi-toolkit).<br>

The Kandy HID SDK currently supports the following Jabra headsets, with support for HID devices from other vendors possible.<br>

| Device Make / Model | Desktop |   VDI eLux  |   VDI Windows   |   Firmware    |
| :-----------------: | :-----: | :---------: | :-------------: | :-----------: |
|   Jabra Engage 65   | &#9745; |   &#9745;   |     &#9745;     | 2.0.5, 3.4.1  |
|   Jabra Engage 50   | &#9745; |   &#9745;   |     &#9745;     | 1.24.0, 2.3.1 |
|   Jabra Speak 750   | &#9744; |   &#9744;   |     &#9745;     |     2.24.0    |
|   Jabra Evolve2 40  | &#9744; |   &#9744;   |     &#9744;     |     1.19.0    |

The README that follows contains instructions required to use the Kandy HID SDK in Electron's MAIN process. Refer to [the README for version 2.0](../README.md) to use the SDK in the Renderer process, or if so instructed by AVCt.

## Installation

Add the tgz file to your workspace and to your package.json. See more details in [CHANGELOG](../CHANGELOG.md).<br>

In order to use this SDK in a Citrix VDI eLux environment, the [corresponding driver](https://github.com/Kandy-IO/kandy-hid-vdi) is also required. No additional driver is required in a Citrix VDI Windows or Mac environment.

## Usage

Pull in all exported constants / functions (see Exports):

    const hid = require('@distant/kandy-hid');

or, only what you need:

    const { initializeHIDDevices } = require('@distant/kandy-hid');

## Logging

All logs generated by kandy-hid have prefix `HID:` or `VDIHID:`.

## API

- All of the API’s below are to be called from Electron’s main process unless otherwise noted
- Wherever possible a common API exists for both standard Desktop (non-VDI, aka node-hid) and VDI environments
- These are listed in logical order of typical usage

### setLogger(customLogger) (optional)

Parameters:

```
Type: object
Options: any logger such as `electron-log` that has .info and .error methods
Default: `console`
```

Specify an optional custom logger. If for example your Electron app uses `electron-log`, you may pass an instance of it to kandy-hid for it to use.

Defaults to `console` methods. If you want to use console methods (i.e. console.log, console.error), it's not necessary to call `setLogger()`.

For proper operation this should be called as early as possible in your app's startup, and before any other kandy-hid API's are called.

Example:

```
const log = require('electron-log');
const { setLogger } = require('@distant/kandy-hid');

setLogger(log);
initializeHIDDevices(mode);
```

### initializeHIDDevices(mode)

Parameters:

```
Type: string
Options: 'desktop' || 'VDI'
Default: none
```

Sets the operating mode and then performs environment-specific startup actions.

**NOTES**

- mode = 'desktop' indicates a standard desktop Windows or Mac environment, non-VDI/Citrix
- mode must be set before any other APIs are called! (other than setLogger())
- when calling initializeHIDDevices() following a VDI channel disconnection / reconnection, it is not necessary to specify the mode, it will persist until the app exits or restarts.

Example:

```
const { initializeHIDDevices } = require('@distant/kandy-hid');

const hidMode = (yourEnvironment === 'Citrix') ? 'VDI': 'desktop';

initializeHIDDevices(hidMode);
```

### storeMainWindowID(id)

Parameters:

```
Type: number
Default: none
```

kandy-hid sends actions/events to the Electron Renderer process directly via IPC messages (see `HIDFunctionRequest`). In order to do this, it needs the window id of the Electron renderer process that should receive these messages.

In Electron main process:

```
const { storeMainWindowID } = require('@distant/kandy-hid');
const { BrowserWindow } = require('electron');

const mainWindow = new BrowserWindow({
  width: 400,
  height: 600
});

storeMainWindowID(mainWindow.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 **synchronously**.

Example Usage:

From the Electron renderer process:

```
const { ipcRenderer } = require('electron');

function maybeSelectMicrophone(deviceObject) {
  const result = ipcRenderer.sendSync('isSupportedDevice', deviceObject.label);

  if (result) {
    ipcRenderer.send('selectHIDDevice', 'microphone', JSON.stringify(deviceObject));
  }
  // else, log, warn the user?
}
```

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:
```
ipcRenderer.sendSync('isSupportedDevice', 'G/N Audio Jabra PRO 9450 model 123'); // true
ipcRenderer.sendSync('isSupportedDevice', 'abra PRO 945'); // false
```

### selectHIDDevice(deviceType, deviceInformation)

Parameters

```
deviceType:
Type: string
Options: 'microphone' || 'speakers' || 'alert_speakers'

deviceInformation:
Type: object || stringified 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).

A stringified version of the object is accepted because typical usage would be to have a MediaDeviceInfo object sent from Electron's Renderer process to Electron's Main process over IPC, which requires objects be stringified, before finally being sent to kandy-hid.

### allowHIDDeviceOpens(boolean)

Parameters

```
Type: boolean
Default: true
Other options: false
```

##### Desktop

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 will 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.

##### VDI

Not applicable; devices are closed as part of `prepareToExit()`

### 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.

<sup>1</sup> See details regarding 'calls_on_hold' in [call swap documentation](./swap.md)

### 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 muting an active call, kandy-hid will send a 'call_mute' operation to the app. 

These messages are sent over IPC to the Electron renderer process window identified by `storeMainWindowID()` as an event. Therefore, the web app must have an appropriate event handler to receive these events. The device-initiated events have identifier `HIDFunctionRequest`.
```
const { ipcRenderer } = require('electron');

ipcRenderer.on('HIDFunctionRequest', (event, operation) => {
  switch (operation) {
    case 'call_start':
      // start a call in your app
      break;

    case 'call_mute':
      // mute an active call in your app
      break;
      ...
});
```

'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 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 (with the exception of 'call_swap' <sup>1</sup>) are a subset of the the operations your app sends to kandy-hid to perform device actions (via `invokeHIDFunction`). That is not coincidental!

When kandy-hid notifies your app of a status change, it's important that you take whatever actions are necessary in your app (i.e. invoke your mute function), but also **replay the operation back to kandy-hid to update the device's state**<sup>1</sup>.

This may seem strange, but only one software entity can be in charge, and it's your app, not kandy-hid. This is required to keep your app, kandy-hid and the device itself in sync. When a user presses the mute button on a HID device, nothing happens until your app instructs the device to mute via kandy-hid.

<sup>1</sup> Do not replay the 'call_swap' operation back to kandy-hid; instead, your app should send 'call_hold' followed by 'call_resume'. See details regarding 'call_swap' in [call swap documentation](./swap.md).

### readyToExit()

Parameters: `none`<br>
Returns: `boolean`

When your app is getting ready to shutdown, query kandy-hid to see if exit conditions are met. If they are not, call `prepareToExit()` to perform its required shutdown actions.

In Electron Main process:

```
const hid = require('@distant/kandy-hid');
const { app } = require('electron');

app.on('will-quit', event => {

    // be sure only to preventDefault() if exit conditions are not met!
    // calling preventDefault() every time will prevent app exit forever

    if (!hid.readyToExit()) {

        event.preventDefault();

        hid.prepareToExit()
            .then(app.quit);
    }
});
```

### prepareToExit()

Parameters: `none`<br>
Returns: `Promise`

Performs kandy-hid cleanup actions in preparation for exit.

##### Desktop and VDI Windows

Closes handle(s) to open device(s)

##### VDI eLux

Closes handle(s) to open device(s) attached to the client and closes the virtual channel. Since many actions in the VDI environment are asynchronous, this function returns a Promise (in both Desktop and VDI).

## Exports

In addition to the API functions listed previously, kandy-hid exports all of its constants in an attempt to make your integration task easier.

See the complete list of things that can be imported into your app [here](./constants.md).

## Use Cases

### Actions taken from within the app:

- For an incoming call, sending 'start_ringing' will cause the device (base + headset (if connected)) to start audibly ringing
- 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](./swap.md).

### 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 will be in the offhook state).

**NOTE that the Engage 65 and PRO 9450 base must be in "Soft Phone Mode" prior to going offhook**. This can be accomplished by removing the headset from the base and pressing the MultiFunction button or the Call Answer button for 1 second. See device User Manuals for more details.

```
ipcRenderer.on('HIDFunctionRequest', (event, operation) => {
  switch (operation) {
    case 'call_start':
      if (allConditionsMet)
        yourStartCallFunction();
      else {
        ipcRenderer.send('invokeHIDFunction', KANDYHID_CONSTANTS.CALL_FAILURE);
        setTimeout(() => ipcRenderer.send('invokeHIDFunction', KANDYHID_CONSTANTS.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 PRO 9450
- pressing and holding the PC (call answer / end) button on the base for 1-2 seconds
- pressing and holding the Multi-Function button on the headset for 1-2 seconds

##### 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 710/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 PRO 9450
- double-clicking the PC (call answer / end) button on the base
- double-clicking the Multi-Function button on the headset

##### 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 710/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](./swap.md).

## 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' (KANDYHID_CONSTANTS.RESET) to return kandy-hid and the device to default state.

### Device Error

In VDI eLux mode, if the device is powered off or disconnected from the Thin Client, kandy-hid will receive an error from the client, which will in turn be passed up to the app on the `HIDFunctionRequest` event as a 'device_error' operation. It's passed up to the app so you have the opportunity to alert the user or perform other actions.

```
ipcRenderer.on('HIDFunctionRequest', (event, operation) => {
  switch (operation) {
    case KANDYHID_CONSTANTS.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 raise a "`channel_error`" on the `HIDFunctionRequest` event.

The 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.

```
ipcRenderer.on('HIDFunctionRequest', (event, operation) => {
  switch (operation) {
    case KANDYHID_CONSTANTS.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 a Jabra PRO 9450, Engage 65, Speak 710/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.
- The Jabra Speak 710 and Speak 750 cannot currently be used on a Mac in Desktop mode. A support ticket (299) has been created with the device vendor
- In VDI eLux, the Jabra Speak 710 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.
- When performing complex / multi-call operations such as call swapping, device mute state may get out of sync with the application. The out-of-sync condition can always be resolved by performing a mute or unmute action in the application. An enhancement will be delivered in an upcoming release to improve this behaviour.
- See limitations relating to use of older versions of Kandy HID Driver for VDI in [compatibility documentation](./compatibility.md).
- in VDI Windows, 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, reset the device using the latest available version of Jabra Direct. Note that kandy-hid always assumes devices are at factory default settings.

## Backwards Compatibility

When used in a Citrix VDI eLux 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:
#### Note these are fictional release values for purposes of illustration only; see the [compatibility matrix](./compatibility.md) for actual Kandy HID Driver for VDI and Kandy HID SDK version compatibility information.

| Kandy HID Driver for VDI Version  | Kandy HID SDK Version |                  Explanation                      |
| :-------------------------------: | :-------------------: | :------------------------------------------------ |
| 1.0                               | 1.0                   | Initial release                                   |
| -                                 | 1.1                   | <ul><li>the Kandy HID SDK is updated</li><li>the Kandy HID Driver for VDI is not updated</li></ul> |
| 1.2                               | 1.2                   | <ul><li>the Kandy HID SDK is updated</li><li>the Kandy HID Driver for VDI is also updated</li><li>version 1.2 of the Kandy HID SDK is compatible with Kandy HID Driver for VDI versions 1.0 and 1.2</li></ul> |
| 1.3                               | 1.3                   | <ul><li>the Kandy HID SDK is updated</li><li>the Kandy HID Driver for VDI is also updated</li><li>version 1.3 of the Kandy HID SDK is compatible with Kandy HID Driver for VDI versions 1.2 and 1.3</li></ul> |

## Windows VDI

Using the kandy-hid SDK in a VDI Windows environment requires that HID devices be "split" and then the HID portion of the device be redrected to the Virtual Machine. See [Required Citrix configuration for VDI Windows](./windows_vdi.md).

## CHANGELOG

See [CHANGELOG](../CHANGELOG.md).
