1020 lines
33 KiB
TypeScript
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;
|
||
|
}
|
||
|
}
|