jiti-meet/react/features/web-hid/webhid-manager.ts

1020 lines
33 KiB
TypeScript

import logger from './logger';
import {
ACTION_HOOK_TYPE_NAME,
COMMANDS,
EVENT_TYPE,
HOOK_STATUS,
IDeviceInfo,
INPUT_REPORT_EVENT_NAME
} from './types';
import {
DEVICE_USAGE,
TELEPHONY_DEVICE_USAGE_PAGE,
requestTelephonyHID
} from './utils';
/**
* WebHID manager that incorporates all hid specific logic.
*
* @class WebHidManager
*/
export default class WebHidManager extends EventTarget {
hidSupport: boolean;
deviceInfo: IDeviceInfo;
availableDevices: HIDDevice[];
isParseDescriptorsSuccess: boolean;
outputEventGenerators: { [key: string]: Function; };
deviceCommand = {
outputReport: {
mute: {
reportId: 0,
usageOffset: -1
},
offHook: {
reportId: 0,
usageOffset: -1
},
ring: {
reportId: 0,
usageOffset: 0
},
hold: {
reportId: 0,
usageOffset: 0
}
},
inputReport: {
hookSwitch: {
reportId: 0,
usageOffset: -1,
isAbsolute: false
},
phoneMute: {
reportId: 0,
usageOffset: -1,
isAbsolute: false
}
}
};
private static instance: WebHidManager;
/**
* WebHidManager getInstance.
*
* @static
* @returns {WebHidManager} - WebHidManager instance.
*/
static getInstance(): WebHidManager {
if (!this.instance) {
this.instance = new WebHidManager();
}
return this.instance;
}
/**
* Creates an instance of WebHidManager.
*
*/
constructor() {
super();
this.deviceInfo = {} as IDeviceInfo;
this.hidSupport = this.isSupported();
this.availableDevices = [];
this.isParseDescriptorsSuccess = false;
this.outputEventGenerators = {};
}
/**
* Check support of hid in navigator.
* - experimental API in Chrome.
*
* @returns {boolean} - True if supported, otherwise false.
*/
isSupported(): boolean {
// @ts-ignore
return !(!window.navigator.hid || !window.navigator.hid.requestDevice);
}
/**
* Handler for requesting telephony hid devices.
*
* @returns {HIDDevice[]|null}
*/
async requestHidDevices() {
if (!this.hidSupport) {
logger.warn('The WebHID API is NOT supported!');
return null;
}
if (this.deviceInfo?.device && this.deviceInfo.device.opened) {
await this.close();
}
// @ts-ignore
const devices = await navigator.hid.requestDevice(requestTelephonyHID);
if (!devices || !devices.length) {
logger.warn('No HID devices selected.');
return false;
}
this.availableDevices = devices;
return devices;
}
/**
* Handler for listen to already connected hid.
*
* @returns {void}
*/
async listenToConnectedHid() {
const devices = await this.loadPairedDevices();
if (!devices || !devices.length) {
logger.warn('No hid device found.');
return;
}
const telephonyDevice = this.getTelephonyDevice(devices);
if (!telephonyDevice) {
logger.warn('No HID device to request');
return;
}
await this.open(telephonyDevice);
// restore the default state of hook and mic LED
this.resetDeviceState();
// switch headsets to OFF_HOOK for mute/unmute commands
this.sendDeviceReport({ command: COMMANDS.OFF_HOOK });
}
/**
* Get first telephony device from availableDevices.
*
* @param {HIDDevice[]} availableDevices -.
* @returns {HIDDevice} -.
*/
private getTelephonyDevice(availableDevices: HIDDevice[]) {
if (!availableDevices || !availableDevices.length) {
logger.warn('No HID device to request');
return undefined;
}
return availableDevices?.find(device => this.findTelephonyCollectionInfo(device.collections));
}
/**
* Find telephony collection info from a list of collection infos.
*
* @private
* @param {HIDCollectionInfo[]} deviceCollections -.
* @returns {HIDCollectionInfo} - Hid collection info.
*/
private findTelephonyCollectionInfo(deviceCollections: HIDCollectionInfo[]) {
return deviceCollections?.find(
(collection: HIDCollectionInfo) => collection.usagePage === TELEPHONY_DEVICE_USAGE_PAGE
);
}
/**
* Open the hid device and start listening to inputReport events.
*
* @param {HIDDevice} telephonyDevice -.
* @returns {void} -.
*/
private async open(telephonyDevice: HIDDevice) {
try {
this.deviceInfo = { device: telephonyDevice } as IDeviceInfo;
if (!this.deviceInfo || !this.deviceInfo.device) {
logger.warn('no HID device found');
return;
}
if (!this.deviceInfo.device.opened) {
await this.deviceInfo.device.open();
}
this.isParseDescriptorsSuccess = await this.parseDeviceDescriptors(this.deviceInfo.device);
if (!this.isParseDescriptorsSuccess) {
logger.warn('Failed to parse webhid');
return;
}
this.dispatchEvent(new CustomEvent(EVENT_TYPE.INIT_DEVICE, { detail: {
deviceInfo: {
...this.deviceInfo
} as IDeviceInfo } }));
// listen for input reports by registering an oninputreport event listener
this.deviceInfo.device.oninputreport = await this.handleInputReport.bind(this);
this.resetDeviceState();
} catch (e) {
logger.error(`Error content open device:${e}`);
}
}
/**
* Close device and reset state.
*
* @returns {void}.
*/
async close() {
try {
await this.resetDeviceState();
if (this.availableDevices) {
logger.info('clear available devices list');
this.availableDevices = [];
}
if (!this.deviceInfo) {
return;
}
if (this.deviceInfo?.device?.opened) {
await this.deviceInfo.device.close();
}
if (this.deviceInfo.device) {
this.deviceInfo.device.oninputreport = null;
}
this.deviceInfo = {} as IDeviceInfo;
} catch (e) {
logger.error(e);
}
}
/**
* Get paired hid devices.
*
* @returns {HIDDevice[]}
*/
async loadPairedDevices() {
try {
// @ts-ignore
const devices = await navigator.hid.getDevices();
this.availableDevices = devices;
return devices;
} catch (e) {
logger.error('loadPairedDevices error:', e);
}
}
/**
* Parse device descriptors - input and output reports.
*
* @param {HIDDevice} device -.
* @returns {boolean} - True if descriptors have been parsed with success.
*/
parseDeviceDescriptors(device: HIDDevice) {
try {
this.outputEventGenerators = {};
if (!device || !device.collections) {
logger.error('Undefined device collection');
return false;
}
const telephonyCollection = this.findTelephonyCollectionInfo(device.collections);
if (!telephonyCollection || Object.keys(telephonyCollection).length === 0) {
logger.error('No telephony collection');
return false;
}
if (telephonyCollection.inputReports) {
if (!this.parseInputReports(telephonyCollection.inputReports)) {
logger.warn('parse inputReports failed');
return false;
}
logger.warn('parse inputReports success');
}
if (telephonyCollection.outputReports) {
if (!this.parseOutputReports(telephonyCollection.outputReports)) {
logger.warn('parse outputReports failed');
return false;
}
logger.warn('parse outputReports success');
return true;
}
logger.warn('parseDeviceDescriptors: returns false, end');
return false;
} catch (e) {
logger.error(`parseDeviceDescriptors error:${JSON.stringify(e, null, ' ')}`);
return false;
}
}
/**
* HandleInputReport.
*
* @param {HIDInputReportEvent} event -.
* @returns {void} -.
*/
handleInputReport(event: HIDInputReportEvent) {
try {
const { data, device, reportId } = event;
if (reportId === 0) {
logger.warn('handleInputReport: ignore invalid reportId');
return;
}
const inputReport = this.deviceCommand.inputReport;
logger.warn(`current inputReport:${JSON.stringify(inputReport, null, ' ')}, reporId: ${reportId}`);
if (reportId !== inputReport.hookSwitch.reportId && reportId !== inputReport.phoneMute.reportId) {
logger.warn('handleInputReport:ignore unknown reportId');
return;
}
let hookStatusChange = false;
let muteStatusChange = false;
const reportData = new Uint8Array(data.buffer);
const needReply = true;
if (reportId === inputReport.hookSwitch.reportId) {
const item = inputReport.hookSwitch;
const byteIndex = Math.trunc(item.usageOffset / 8);
const bitPosition = item.usageOffset % 8;
// eslint-disable-next-line no-bitwise
const usageOn = (data.getUint8(byteIndex) & (0x01 << bitPosition)) !== 0;
logger.warn('recv hookSwitch ', usageOn ? HOOK_STATUS.OFF : HOOK_STATUS.ON);
if (inputReport.hookSwitch.isAbsolute) {
if (this.deviceInfo.hookStatus === HOOK_STATUS.ON && usageOn) {
this.deviceInfo.hookStatus = HOOK_STATUS.OFF;
hookStatusChange = true;
} else if (this.deviceInfo.hookStatus === HOOK_STATUS.OFF && !usageOn) {
this.deviceInfo.hookStatus = HOOK_STATUS.ON;
hookStatusChange = true;
}
} else if (usageOn) {
this.deviceInfo.hookStatus = this.deviceInfo.hookStatus === HOOK_STATUS.OFF
? HOOK_STATUS.ON : HOOK_STATUS.OFF;
hookStatusChange = true;
}
}
if (reportId === inputReport.phoneMute.reportId) {
const item = inputReport.phoneMute;
const byteIndex = Math.trunc(item.usageOffset / 8);
const bitPosition = item.usageOffset % 8;
// eslint-disable-next-line no-bitwise
const usageOn = (data.getUint8(byteIndex) & (0x01 << bitPosition)) !== 0;
logger.warn('recv phoneMute ', usageOn ? HOOK_STATUS.ON : HOOK_STATUS.OFF);
if (inputReport.phoneMute.isAbsolute) {
if (this.deviceInfo.muted !== usageOn) {
this.deviceInfo.muted = usageOn;
muteStatusChange = true;
}
} else if (usageOn) {
this.deviceInfo.muted = !this.deviceInfo.muted;
muteStatusChange = true;
}
}
const inputReportData = {
productName: device.productName,
reportId: this.getHexByte(reportId),
reportData,
eventName: '',
isMute: false,
hookStatus: ''
};
if (hookStatusChange) {
// Answer key state change
inputReportData.eventName = INPUT_REPORT_EVENT_NAME.ON_DEVICE_HOOK_SWITCH;
inputReportData.hookStatus = this.deviceInfo.hookStatus;
logger.warn(`hook status change: ${this.deviceInfo.hookStatus}`);
}
if (muteStatusChange) {
// Mute key state change
inputReportData.eventName = INPUT_REPORT_EVENT_NAME.ON_DEVICE_MUTE_SWITCH;
inputReportData.isMute = this.deviceInfo.muted;
logger.warn(`mute status change: ${this.deviceInfo.muted}`);
}
const actionResult = this.extractActionResult(inputReportData);
this.dispatchEvent(
new CustomEvent(EVENT_TYPE.UPDATE_DEVICE, {
detail: {
actionResult,
deviceInfo: this.deviceInfo
}
})
);
logger.warn(
`hookStatusChange=${
hookStatusChange
}, muteStatusChange=${
muteStatusChange
}, needReply=${
needReply}`
);
if (needReply && (hookStatusChange || muteStatusChange)) {
let newOffHook;
if (this.deviceInfo.hookStatus === HOOK_STATUS.OFF) {
newOffHook = true;
} else if (this.deviceInfo.hookStatus === HOOK_STATUS.ON) {
newOffHook = false;
} else {
logger.warn('Invalid hook status');
return;
}
this.sendReplyReport(reportId, newOffHook, this.deviceInfo.muted);
} else {
logger.warn(`Not sending reply report: needReply ${needReply},
hookStatusChange: ${hookStatusChange}, muteStatusChange: ${muteStatusChange}`);
}
} catch (e) {
logger.error(e);
}
}
/**
* Extract action result.
*
* @private
* @param {*} data -.
* @returns {{eventName: string}} - EventName.
*/
private extractActionResult(data: any) {
switch (data.eventName) {
case INPUT_REPORT_EVENT_NAME.ON_DEVICE_HOOK_SWITCH:
return {
eventName: data.hookStatus === HOOK_STATUS.ON
? ACTION_HOOK_TYPE_NAME.HOOK_SWITCH_ON : ACTION_HOOK_TYPE_NAME.HOOK_SWITCH_OFF
};
case INPUT_REPORT_EVENT_NAME.ON_DEVICE_MUTE_SWITCH:
return {
eventName: data.isMute ? ACTION_HOOK_TYPE_NAME.MUTE_SWITCH_ON : ACTION_HOOK_TYPE_NAME.MUTE_SWITCH_OFF
};
case 'ondevicevolumechange':
return {
eventName: data.volumeStatus === 'up'
? ACTION_HOOK_TYPE_NAME.VOLUME_CHANGE_UP : ACTION_HOOK_TYPE_NAME.VOLUME_CHANGE_DOWN
};
default:
break;
}
}
/**
* Reset device state.
*
* @returns {void} -.
*/
resetDeviceState() {
if (!this.deviceInfo || !this.deviceInfo.device || !this.deviceInfo.device.opened) {
return;
}
this.deviceInfo.hookStatus = HOOK_STATUS.ON;
this.deviceInfo.muted = false;
this.deviceInfo.ring = false;
this.deviceInfo.hold = false;
this.sendDeviceReport({ command: COMMANDS.ON_HOOK });
this.sendDeviceReport({ command: COMMANDS.MUTE_OFF });
}
/**
* Parse input reports.
*
* @param {HIDReportInfo[]} inputReports -.
* @returns {void} -.
*/
private parseInputReports(inputReports: HIDReportInfo[]) {
inputReports.forEach(report => {
if (!report || !report.items?.length || report.reportId === undefined) {
return;
}
let usageOffset = 0;
report.items.forEach((item: HIDReportItem) => {
if (
item.usages === undefined
|| item.reportSize === undefined
|| item.reportCount === undefined
|| item.isAbsolute === undefined
) {
logger.warn('parseInputReports invalid parameters!');
return;
}
const reportSize = item.reportSize ?? 0;
const reportId = report.reportId ?? 0;
item.usages.forEach((usage: number, i: number) => {
switch (usage) {
case DEVICE_USAGE.hookSwitch.usageId:
this.deviceCommand.inputReport.hookSwitch = {
reportId,
usageOffset: usageOffset + (i * reportSize),
isAbsolute: item.isAbsolute ?? false
};
break;
case DEVICE_USAGE.phoneMute.usageId:
this.deviceCommand.inputReport.phoneMute = {
reportId,
usageOffset: usageOffset + (i * reportSize),
isAbsolute: item.isAbsolute ?? false
};
break;
default:
break;
}
});
usageOffset += item.reportCount * item.reportSize;
});
});
if (!this.deviceCommand.inputReport.phoneMute || !this.deviceCommand.inputReport.hookSwitch) {
logger.warn('parseInputReports - no phoneMute or hookSwitch. Skip. Returning false');
return false;
}
return true;
}
/**
* Parse output reports.
*
* @private
* @param {HIDReportInfo[]} outputReports -.
* @returns {void} -.
*/
private parseOutputReports(outputReports: HIDReportInfo[]) {
outputReports.forEach((report: HIDReportInfo) => {
if (!report || !report.items?.length || report.reportId === undefined) {
return;
}
let usageOffset = 0;
const usageOffsetMap: Map<number, number> = new Map();
report.items.forEach(item => {
if (item.usages === undefined || item.reportSize === undefined || item.reportCount === undefined) {
logger.warn('parseOutputReports invalid parameters!');
return;
}
const reportSize = item.reportSize ?? 0;
const reportId = report.reportId ?? 0;
item.usages.forEach((usage: number, i: number) => {
switch (usage) {
case DEVICE_USAGE.mute.usageId:
this.deviceCommand.outputReport.mute = {
reportId,
usageOffset: usageOffset + (i * reportSize)
};
usageOffsetMap.set(usage, usageOffset + (i * reportSize));
break;
case DEVICE_USAGE.offHook.usageId:
this.deviceCommand.outputReport.offHook = {
reportId,
usageOffset: usageOffset + (i * reportSize)
};
usageOffsetMap.set(usage, usageOffset + (i * reportSize));
break;
case DEVICE_USAGE.ring.usageId:
this.deviceCommand.outputReport.ring = {
reportId,
usageOffset: usageOffset + (i * reportSize)
};
usageOffsetMap.set(usage, usageOffset + (i * reportSize));
break;
case DEVICE_USAGE.hold.usageId:
this.deviceCommand.outputReport.hold = {
reportId,
usageOffset: usageOffset = i * reportSize
};
usageOffsetMap.set(usage, usageOffset + (i * reportSize));
break;
default:
break;
}
});
usageOffset += item.reportCount * item.reportSize;
});
const reportLength = usageOffset;
for (const [ usage, offset ] of usageOffsetMap) {
this.outputEventGenerators[usage] = (val: number) => {
const reportData = new Uint8Array(reportLength / 8);
if (offset >= 0 && val) {
const byteIndex = Math.trunc(offset / 8);
const bitPosition = offset % 8;
// eslint-disable-next-line no-bitwise
reportData[byteIndex] = 1 << bitPosition;
}
return reportData;
};
}
});
let hook, mute, ring;
for (const item in this.outputEventGenerators) {
if (Object.prototype.hasOwnProperty.call(this.outputEventGenerators, item)) {
let newItem = this.getHexByte(item);
newItem = `0x0${newItem}`;
if (DEVICE_USAGE.mute.usageId === Number(newItem)) {
mute = this.outputEventGenerators[DEVICE_USAGE.mute.usageId];
} else if (DEVICE_USAGE.offHook.usageId === Number(newItem)) {
hook = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId];
} else if (DEVICE_USAGE.ring.usageId === Number(newItem)) {
ring = this.outputEventGenerators[DEVICE_USAGE.ring.usageId];
}
}
}
if (!mute && !ring && !hook) {
return false;
}
return true;
}
/**
* Send device report.
*
* @param {{ command: string }} data -.
* @returns {void} -.
*/
async sendDeviceReport(data: { command: string; }) {
if (!data || !data.command || !this.deviceInfo
|| !this.deviceInfo.device || !this.deviceInfo.device.opened || !this.isParseDescriptorsSuccess) {
logger.warn('There are currently non-compliant conditions');
return;
}
logger.warn(`sendDeviceReport data.command: ${data.command}`);
if (data.command === COMMANDS.MUTE_ON || data.command === COMMANDS.MUTE_OFF) {
if (!this.outputEventGenerators[DEVICE_USAGE.mute.usageId]) {
logger.warn('current no parse mute event');
return;
}
} else if (data.command === COMMANDS.ON_HOOK || data.command === COMMANDS.OFF_HOOK) {
if (!this.outputEventGenerators[DEVICE_USAGE.offHook.usageId]) {
logger.warn('current no parse offHook event');
return;
}
} else if (data.command === COMMANDS.ON_RING || data.command === COMMANDS.OFF_RING) {
if (!this.outputEventGenerators[DEVICE_USAGE.ring.usageId]) {
logger.warn('current no parse ring event');
return;
}
}
let oldOffHook;
let newOffHook;
let newMuted;
let newRing;
let newHold;
let offHookReport;
let muteReport;
let ringReport;
let holdReport;
let reportData = new Uint8Array();
const reportId = this.matchReportId(data.command);
if (reportId === 0) {
logger.warn(`Unsupported command ${data.command}`);
return;
}
/* keep old status. */
const oldMuted = this.deviceInfo.muted;
if (this.deviceInfo.hookStatus === HOOK_STATUS.OFF) {
oldOffHook = true;
} else if (this.deviceInfo.hookStatus === HOOK_STATUS.ON) {
oldOffHook = false;
} else {
logger.warn('Invalid hook status');
return;
}
const oldRing = this.deviceInfo.ring;
const oldHold = this.deviceInfo.hold;
logger.warn(
`send device command: old_hook=${oldOffHook}, old_muted=${oldMuted}, old_ring=${oldRing}`
);
/* get new status. */
switch (data.command) {
case COMMANDS.MUTE_ON:
newMuted = true;
break;
case COMMANDS.MUTE_OFF:
newMuted = false;
break;
case COMMANDS.ON_HOOK:
newOffHook = false;
break;
case COMMANDS.OFF_HOOK:
newOffHook = true;
break;
case COMMANDS.ON_RING:
newRing = true;
break;
case COMMANDS.OFF_RING:
newRing = false;
break;
case COMMANDS.ON_HOLD:
newHold = true;
break;
case COMMANDS.OFF_HOLD:
newHold = false;
break;
default:
logger.info(`Unknown command ${data.command}`);
return;
}
logger.warn(
`send device command: new_hook = ${newOffHook}, new_muted = ${newMuted},
new_ring = ${newRing} new_hold = ${newHold}`
);
if (this.outputEventGenerators[DEVICE_USAGE.mute.usageId]) {
if (newMuted === undefined) {
muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](oldMuted);
} else {
muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](newMuted);
}
}
if (this.outputEventGenerators[DEVICE_USAGE.offHook.usageId]) {
if (newOffHook === undefined) {
offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](oldOffHook);
} else {
offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](newOffHook);
}
}
if (this.outputEventGenerators[DEVICE_USAGE.ring.usageId]) {
if (newRing === undefined) {
ringReport = this.outputEventGenerators[DEVICE_USAGE.ring.usageId](oldRing);
} else {
ringReport = this.outputEventGenerators[DEVICE_USAGE.ring.usageId](newRing);
}
}
if (this.outputEventGenerators[DEVICE_USAGE.hold.usageId]) {
holdReport = this.outputEventGenerators[DEVICE_USAGE.hold.usageId](oldHold);
}
if (reportId === this.deviceCommand.outputReport.mute.reportId) {
reportData = new Uint8Array(muteReport);
}
if (reportId === this.deviceCommand.outputReport.offHook.reportId) {
reportData = new Uint8Array(offHookReport);
}
if (reportId === this.deviceCommand.outputReport.ring.reportId) {
reportData = new Uint8Array(ringReport);
}
if (reportId === this.deviceCommand.outputReport.hold.reportId) {
reportData = new Uint8Array(holdReport);
}
logger.warn(`[sendDeviceReport] send device command (before call webhid API)
${data.command}: reportId=${reportId}, reportData=${reportData}`);
logger.warn(`reportData is ${JSON.stringify(reportData, null, ' ')}`);
await this.deviceInfo.device.sendReport(reportId, reportData);
/* update new status. */
this.updateDeviceStatus(data);
}
/**
* Update device status.
*
* @private
* @param {{ command: string; }} data -.
* @returns {void}
*/
private updateDeviceStatus(data: { command: string; }) {
switch (data.command) {
case COMMANDS.MUTE_ON:
this.deviceInfo.muted = true;
break;
case COMMANDS.MUTE_OFF:
this.deviceInfo.muted = false;
break;
case COMMANDS.ON_HOOK:
this.deviceInfo.hookStatus = HOOK_STATUS.ON;
break;
case COMMANDS.OFF_HOOK:
this.deviceInfo.hookStatus = HOOK_STATUS.OFF;
break;
case COMMANDS.ON_RING:
this.deviceInfo.ring = true;
break;
case COMMANDS.OFF_RING:
this.deviceInfo.ring = false;
break;
case COMMANDS.ON_HOLD:
this.deviceInfo.hold = true;
break;
case 'offHold':
this.deviceInfo.hold = false;
break;
default:
logger.warn(`Unknown command ${data.command}`);
break;
}
logger.warn(
`[updateDeviceStatus] device status after send command: hook=${this.deviceInfo.hookStatus},
muted=${this.deviceInfo.muted}, ring=${this.deviceInfo.ring}`
);
}
/**
* Math given command with known commands.
*
* @private
* @param {string} command -.
* @returns {number} ReportId.
*/
private matchReportId(command: string) {
switch (command) {
case COMMANDS.MUTE_ON:
case COMMANDS.MUTE_OFF:
return this.deviceCommand.outputReport.mute.reportId;
case COMMANDS.ON_HOOK:
case COMMANDS.OFF_HOOK:
return this.deviceCommand.outputReport.offHook.reportId;
case COMMANDS.ON_RING:
case COMMANDS.OFF_RING:
return this.deviceCommand.outputReport.ring.reportId;
case COMMANDS.ON_HOLD:
case COMMANDS.OFF_HOLD:
return this.deviceCommand.outputReport.hold.reportId;
default:
logger.info(`Unknown command ${command}`);
return 0;
}
}
/**
* Send reply report to device.
*
* @param {number} inputReportId -.
* @param {(string | boolean | undefined)} curOffHook -.
* @param {(string | undefined)} curMuted -.
* @returns {void} -.
*/
private async sendReplyReport(
inputReportId: number,
curOffHook: string | boolean | undefined,
curMuted: boolean | string | undefined
) {
const reportId = this.retriveInputReportId(inputReportId);
if (!this.deviceInfo || !this.deviceInfo.device || !this.deviceInfo.device.opened) {
logger.warn('[sendReplyReport] device is not opened or does not exist');
return;
}
if (reportId === 0 || curOffHook === undefined || curMuted === undefined) {
logger.warn(`[sendReplyReport] return, provided data not valid,
reportId: ${reportId}, curOffHook: ${curOffHook}, curMuted: ${curMuted}`);
return;
}
let reportData = new Uint8Array();
let muteReport;
let offHookReport;
let ringReport;
if (this.deviceCommand.outputReport.offHook.reportId === this.deviceCommand.outputReport.mute.reportId) {
muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](curOffHook);
reportData = new Uint8Array(offHookReport);
for (const [ i, data ] of muteReport.entries()) {
// eslint-disable-next-line no-bitwise
reportData[i] |= data;
}
} else if (reportId === this.deviceCommand.outputReport.offHook.reportId) {
offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](curOffHook);
reportData = new Uint8Array(offHookReport);
} else if (reportId === this.deviceCommand.outputReport.mute.reportId) {
muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
reportData = new Uint8Array(muteReport);
} else if (reportId === this.deviceCommand.outputReport.ring.reportId) {
ringReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
reportData = new Uint8Array(ringReport);
}
logger.warn(`[sendReplyReport] send device reply: reportId=${reportId}, reportData=${reportData}`);
await this.deviceInfo.device.sendReport(reportId, reportData);
}
/**
* Retrieve input report id.
*
* @private
* @param {number} inputReportId -.
* @returns {number} ReportId -.
*/
private retriveInputReportId(inputReportId: number) {
let reportId = 0;
if (this.deviceCommand.outputReport.offHook.reportId === this.deviceCommand.outputReport.mute.reportId) {
reportId = this.deviceCommand.outputReport.offHook.reportId;
} else if (inputReportId === this.deviceCommand.inputReport.hookSwitch.reportId) {
reportId = this.deviceCommand.outputReport.offHook.reportId;
} else if (inputReportId === this.deviceCommand.inputReport.phoneMute.reportId) {
reportId = this.deviceCommand.outputReport.mute.reportId;
}
return reportId;
}
/**
* Get the hexadecimal bytes.
*
* @param {number|string} data -.
* @returns {string}
*/
getHexByte(data: number | string) {
let hex = Number(data).toString(16);
while (hex.length < 2) {
hex = `0${hex}`;
}
return hex;
}
}