You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

webhid-manager.ts 33KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. import logger from './logger';
  2. import {
  3. ACTION_HOOK_TYPE_NAME,
  4. COMMANDS,
  5. EVENT_TYPE,
  6. HOOK_STATUS,
  7. IDeviceInfo,
  8. INPUT_REPORT_EVENT_NAME
  9. } from './types';
  10. import {
  11. DEVICE_USAGE,
  12. TELEPHONY_DEVICE_USAGE_PAGE,
  13. requestTelephonyHID
  14. } from './utils';
  15. /**
  16. * WebHID manager that incorporates all hid specific logic.
  17. *
  18. * @class WebHidManager
  19. */
  20. export default class WebHidManager extends EventTarget {
  21. hidSupport: boolean;
  22. deviceInfo: IDeviceInfo;
  23. availableDevices: HIDDevice[];
  24. isParseDescriptorsSuccess: boolean;
  25. outputEventGenerators: { [key: string]: Function; };
  26. deviceCommand = {
  27. outputReport: {
  28. mute: {
  29. reportId: 0,
  30. usageOffset: -1
  31. },
  32. offHook: {
  33. reportId: 0,
  34. usageOffset: -1
  35. },
  36. ring: {
  37. reportId: 0,
  38. usageOffset: 0
  39. },
  40. hold: {
  41. reportId: 0,
  42. usageOffset: 0
  43. }
  44. },
  45. inputReport: {
  46. hookSwitch: {
  47. reportId: 0,
  48. usageOffset: -1,
  49. isAbsolute: false
  50. },
  51. phoneMute: {
  52. reportId: 0,
  53. usageOffset: -1,
  54. isAbsolute: false
  55. }
  56. }
  57. };
  58. private static instance: WebHidManager;
  59. /**
  60. * WebHidManager getInstance.
  61. *
  62. * @static
  63. * @returns {WebHidManager} - WebHidManager instance.
  64. */
  65. static getInstance(): WebHidManager {
  66. if (!this.instance) {
  67. this.instance = new WebHidManager();
  68. }
  69. return this.instance;
  70. }
  71. /**
  72. * Creates an instance of WebHidManager.
  73. *
  74. */
  75. constructor() {
  76. super();
  77. this.deviceInfo = {} as IDeviceInfo;
  78. this.hidSupport = this.isSupported();
  79. this.availableDevices = [];
  80. this.isParseDescriptorsSuccess = false;
  81. this.outputEventGenerators = {};
  82. }
  83. /**
  84. * Check support of hid in navigator.
  85. * - experimental API in Chrome.
  86. *
  87. * @returns {boolean} - True if supported, otherwise false.
  88. */
  89. isSupported(): boolean {
  90. // @ts-ignore
  91. return Boolean(window.navigator.hid?.requestDevice);
  92. }
  93. /**
  94. * Handler for requesting telephony hid devices.
  95. *
  96. * @returns {HIDDevice[]|null}
  97. */
  98. async requestHidDevices() {
  99. if (!this.hidSupport) {
  100. logger.warn('The WebHID API is NOT supported!');
  101. return null;
  102. }
  103. if (this.deviceInfo?.device?.opened) {
  104. await this.close();
  105. }
  106. // @ts-ignore
  107. const devices = await navigator.hid.requestDevice(requestTelephonyHID);
  108. if (!devices?.length) {
  109. logger.warn('No HID devices selected.');
  110. return false;
  111. }
  112. this.availableDevices = devices;
  113. return devices;
  114. }
  115. /**
  116. * Handler for listen to already connected hid.
  117. *
  118. * @returns {void}
  119. */
  120. async listenToConnectedHid() {
  121. const devices = await this.loadPairedDevices();
  122. if (!devices?.length) {
  123. logger.warn('No hid device found.');
  124. return;
  125. }
  126. const telephonyDevice = this.getTelephonyDevice(devices);
  127. if (!telephonyDevice) {
  128. logger.warn('No HID device to request');
  129. return;
  130. }
  131. await this.open(telephonyDevice);
  132. // restore the default state of hook and mic LED
  133. this.resetDeviceState();
  134. // switch headsets to OFF_HOOK for mute/unmute commands
  135. this.sendDeviceReport({ command: COMMANDS.OFF_HOOK });
  136. }
  137. /**
  138. * Get first telephony device from availableDevices.
  139. *
  140. * @param {HIDDevice[]} availableDevices -.
  141. * @returns {HIDDevice} -.
  142. */
  143. private getTelephonyDevice(availableDevices: HIDDevice[]) {
  144. if (!availableDevices?.length) {
  145. logger.warn('No HID device to request');
  146. return undefined;
  147. }
  148. return availableDevices?.find(device => this.findTelephonyCollectionInfo(device.collections));
  149. }
  150. /**
  151. * Find telephony collection info from a list of collection infos.
  152. *
  153. * @private
  154. * @param {HIDCollectionInfo[]} deviceCollections -.
  155. * @returns {HIDCollectionInfo} - Hid collection info.
  156. */
  157. private findTelephonyCollectionInfo(deviceCollections: HIDCollectionInfo[]) {
  158. return deviceCollections?.find(
  159. (collection: HIDCollectionInfo) => collection.usagePage === TELEPHONY_DEVICE_USAGE_PAGE
  160. );
  161. }
  162. /**
  163. * Open the hid device and start listening to inputReport events.
  164. *
  165. * @param {HIDDevice} telephonyDevice -.
  166. * @returns {void} -.
  167. */
  168. private async open(telephonyDevice: HIDDevice) {
  169. try {
  170. this.deviceInfo = { device: telephonyDevice } as IDeviceInfo;
  171. if (!this.deviceInfo?.device) {
  172. logger.warn('no HID device found');
  173. return;
  174. }
  175. if (!this.deviceInfo.device.opened) {
  176. await this.deviceInfo.device.open();
  177. }
  178. this.isParseDescriptorsSuccess = await this.parseDeviceDescriptors(this.deviceInfo.device);
  179. if (!this.isParseDescriptorsSuccess) {
  180. logger.warn('Failed to parse webhid');
  181. return;
  182. }
  183. this.dispatchEvent(new CustomEvent(EVENT_TYPE.INIT_DEVICE, { detail: {
  184. deviceInfo: {
  185. ...this.deviceInfo
  186. } as IDeviceInfo } }));
  187. // listen for input reports by registering an oninputreport event listener
  188. this.deviceInfo.device.oninputreport = await this.handleInputReport.bind(this);
  189. this.resetDeviceState();
  190. } catch (e) {
  191. logger.error(`Error content open device:${e}`);
  192. }
  193. }
  194. /**
  195. * Close device and reset state.
  196. *
  197. * @returns {void}
  198. */
  199. async close() {
  200. try {
  201. await this.resetDeviceState();
  202. if (this.availableDevices) {
  203. logger.info('clear available devices list');
  204. this.availableDevices = [];
  205. }
  206. if (!this.deviceInfo) {
  207. return;
  208. }
  209. if (this.deviceInfo?.device?.opened) {
  210. await this.deviceInfo.device.close();
  211. }
  212. if (this.deviceInfo.device) {
  213. this.deviceInfo.device.oninputreport = null;
  214. }
  215. this.deviceInfo = {} as IDeviceInfo;
  216. } catch (e) {
  217. logger.error(e);
  218. }
  219. }
  220. /**
  221. * Get paired hid devices.
  222. *
  223. * @returns {HIDDevice[]}
  224. */
  225. async loadPairedDevices() {
  226. try {
  227. // @ts-ignore
  228. const devices = await navigator.hid.getDevices();
  229. this.availableDevices = devices;
  230. return devices;
  231. } catch (e) {
  232. logger.error('loadPairedDevices error:', e);
  233. }
  234. }
  235. /**
  236. * Parse device descriptors - input and output reports.
  237. *
  238. * @param {HIDDevice} device -.
  239. * @returns {boolean} - True if descriptors have been parsed with success.
  240. */
  241. parseDeviceDescriptors(device: HIDDevice) {
  242. try {
  243. this.outputEventGenerators = {};
  244. if (!device?.collections) {
  245. logger.error('Undefined device collection');
  246. return false;
  247. }
  248. const telephonyCollection = this.findTelephonyCollectionInfo(device.collections);
  249. if (!telephonyCollection || Object.keys(telephonyCollection).length === 0) {
  250. logger.error('No telephony collection');
  251. return false;
  252. }
  253. if (telephonyCollection.inputReports) {
  254. if (!this.parseInputReports(telephonyCollection.inputReports)) {
  255. logger.warn('parse inputReports failed');
  256. return false;
  257. }
  258. logger.warn('parse inputReports success');
  259. }
  260. if (telephonyCollection.outputReports) {
  261. if (!this.parseOutputReports(telephonyCollection.outputReports)) {
  262. logger.warn('parse outputReports failed');
  263. return false;
  264. }
  265. logger.warn('parse outputReports success');
  266. return true;
  267. }
  268. logger.warn('parseDeviceDescriptors: returns false, end');
  269. return false;
  270. } catch (e) {
  271. logger.error(`parseDeviceDescriptors error:${JSON.stringify(e, null, ' ')}`);
  272. return false;
  273. }
  274. }
  275. /**
  276. * HandleInputReport.
  277. *
  278. * @param {HIDInputReportEvent} event -.
  279. * @returns {void} -.
  280. */
  281. handleInputReport(event: HIDInputReportEvent) {
  282. try {
  283. const { data, device, reportId } = event;
  284. if (reportId === 0) {
  285. logger.warn('handleInputReport: ignore invalid reportId');
  286. return;
  287. }
  288. const inputReport = this.deviceCommand.inputReport;
  289. logger.warn(`current inputReport:${JSON.stringify(inputReport, null, ' ')}, reporId: ${reportId}`);
  290. if (reportId !== inputReport.hookSwitch.reportId && reportId !== inputReport.phoneMute.reportId) {
  291. logger.warn('handleInputReport:ignore unknown reportId');
  292. return;
  293. }
  294. let hookStatusChange = false;
  295. let muteStatusChange = false;
  296. const reportData = new Uint8Array(data.buffer);
  297. const needReply = true;
  298. if (reportId === inputReport.hookSwitch.reportId) {
  299. const item = inputReport.hookSwitch;
  300. const byteIndex = Math.trunc(item.usageOffset / 8);
  301. const bitPosition = item.usageOffset % 8;
  302. // eslint-disable-next-line no-bitwise
  303. const usageOn = (data.getUint8(byteIndex) & (0x01 << bitPosition)) !== 0;
  304. logger.warn('recv hookSwitch ', usageOn ? HOOK_STATUS.OFF : HOOK_STATUS.ON);
  305. if (inputReport.hookSwitch.isAbsolute) {
  306. if (this.deviceInfo.hookStatus === HOOK_STATUS.ON && usageOn) {
  307. this.deviceInfo.hookStatus = HOOK_STATUS.OFF;
  308. hookStatusChange = true;
  309. } else if (this.deviceInfo.hookStatus === HOOK_STATUS.OFF && !usageOn) {
  310. this.deviceInfo.hookStatus = HOOK_STATUS.ON;
  311. hookStatusChange = true;
  312. }
  313. } else if (usageOn) {
  314. this.deviceInfo.hookStatus = this.deviceInfo.hookStatus === HOOK_STATUS.OFF
  315. ? HOOK_STATUS.ON : HOOK_STATUS.OFF;
  316. hookStatusChange = true;
  317. }
  318. }
  319. if (reportId === inputReport.phoneMute.reportId) {
  320. const item = inputReport.phoneMute;
  321. const byteIndex = Math.trunc(item.usageOffset / 8);
  322. const bitPosition = item.usageOffset % 8;
  323. // eslint-disable-next-line no-bitwise
  324. const usageOn = (data.getUint8(byteIndex) & (0x01 << bitPosition)) !== 0;
  325. logger.warn('recv phoneMute ', usageOn ? HOOK_STATUS.ON : HOOK_STATUS.OFF);
  326. if (inputReport.phoneMute.isAbsolute) {
  327. if (this.deviceInfo.muted !== usageOn) {
  328. this.deviceInfo.muted = usageOn;
  329. muteStatusChange = true;
  330. }
  331. } else if (usageOn) {
  332. this.deviceInfo.muted = !this.deviceInfo.muted;
  333. muteStatusChange = true;
  334. }
  335. }
  336. const inputReportData = {
  337. productName: device.productName,
  338. reportId: this.getHexByte(reportId),
  339. reportData,
  340. eventName: '',
  341. isMute: false,
  342. hookStatus: ''
  343. };
  344. if (hookStatusChange) {
  345. // Answer key state change
  346. inputReportData.eventName = INPUT_REPORT_EVENT_NAME.ON_DEVICE_HOOK_SWITCH;
  347. inputReportData.hookStatus = this.deviceInfo.hookStatus;
  348. logger.warn(`hook status change: ${this.deviceInfo.hookStatus}`);
  349. }
  350. if (muteStatusChange) {
  351. // Mute key state change
  352. inputReportData.eventName = INPUT_REPORT_EVENT_NAME.ON_DEVICE_MUTE_SWITCH;
  353. inputReportData.isMute = this.deviceInfo.muted;
  354. logger.warn(`mute status change: ${this.deviceInfo.muted}`);
  355. }
  356. const actionResult = this.extractActionResult(inputReportData);
  357. this.dispatchEvent(
  358. new CustomEvent(EVENT_TYPE.UPDATE_DEVICE, {
  359. detail: {
  360. actionResult,
  361. deviceInfo: this.deviceInfo
  362. }
  363. })
  364. );
  365. logger.warn(
  366. `hookStatusChange=${
  367. hookStatusChange
  368. }, muteStatusChange=${
  369. muteStatusChange
  370. }, needReply=${
  371. needReply}`
  372. );
  373. if (needReply && (hookStatusChange || muteStatusChange)) {
  374. let newOffHook;
  375. if (this.deviceInfo.hookStatus === HOOK_STATUS.OFF) {
  376. newOffHook = true;
  377. } else if (this.deviceInfo.hookStatus === HOOK_STATUS.ON) {
  378. newOffHook = false;
  379. } else {
  380. logger.warn('Invalid hook status');
  381. return;
  382. }
  383. this.sendReplyReport(reportId, newOffHook, this.deviceInfo.muted);
  384. } else {
  385. logger.warn(`Not sending reply report: needReply ${needReply},
  386. hookStatusChange: ${hookStatusChange}, muteStatusChange: ${muteStatusChange}`);
  387. }
  388. } catch (e) {
  389. logger.error(e);
  390. }
  391. }
  392. /**
  393. * Extract action result.
  394. *
  395. * @private
  396. * @param {*} data -.
  397. * @returns {{eventName: string}} - EventName.
  398. */
  399. private extractActionResult(data: any) {
  400. switch (data.eventName) {
  401. case INPUT_REPORT_EVENT_NAME.ON_DEVICE_HOOK_SWITCH:
  402. return {
  403. eventName: data.hookStatus === HOOK_STATUS.ON
  404. ? ACTION_HOOK_TYPE_NAME.HOOK_SWITCH_ON : ACTION_HOOK_TYPE_NAME.HOOK_SWITCH_OFF
  405. };
  406. case INPUT_REPORT_EVENT_NAME.ON_DEVICE_MUTE_SWITCH:
  407. return {
  408. eventName: data.isMute ? ACTION_HOOK_TYPE_NAME.MUTE_SWITCH_ON : ACTION_HOOK_TYPE_NAME.MUTE_SWITCH_OFF
  409. };
  410. case 'ondevicevolumechange':
  411. return {
  412. eventName: data.volumeStatus === 'up'
  413. ? ACTION_HOOK_TYPE_NAME.VOLUME_CHANGE_UP : ACTION_HOOK_TYPE_NAME.VOLUME_CHANGE_DOWN
  414. };
  415. default:
  416. break;
  417. }
  418. }
  419. /**
  420. * Reset device state.
  421. *
  422. * @returns {void} -.
  423. */
  424. resetDeviceState() {
  425. if (!this.deviceInfo?.device?.opened) {
  426. return;
  427. }
  428. this.deviceInfo.hookStatus = HOOK_STATUS.ON;
  429. this.deviceInfo.muted = false;
  430. this.deviceInfo.ring = false;
  431. this.deviceInfo.hold = false;
  432. this.sendDeviceReport({ command: COMMANDS.ON_HOOK });
  433. this.sendDeviceReport({ command: COMMANDS.MUTE_OFF });
  434. }
  435. /**
  436. * Parse input reports.
  437. *
  438. * @param {HIDReportInfo[]} inputReports -.
  439. * @returns {void} -.
  440. */
  441. private parseInputReports(inputReports: HIDReportInfo[]) {
  442. inputReports.forEach(report => {
  443. if (!report?.items?.length || report.reportId === undefined) {
  444. return;
  445. }
  446. let usageOffset = 0;
  447. report.items.forEach((item: HIDReportItem) => {
  448. if (
  449. item.usages === undefined
  450. || item.reportSize === undefined
  451. || item.reportCount === undefined
  452. || item.isAbsolute === undefined
  453. ) {
  454. logger.warn('parseInputReports invalid parameters!');
  455. return;
  456. }
  457. const reportSize = item.reportSize ?? 0;
  458. const reportId = report.reportId ?? 0;
  459. item.usages.forEach((usage: number, i: number) => {
  460. switch (usage) {
  461. case DEVICE_USAGE.hookSwitch.usageId:
  462. this.deviceCommand.inputReport.hookSwitch = {
  463. reportId,
  464. usageOffset: usageOffset + (i * reportSize),
  465. isAbsolute: item.isAbsolute ?? false
  466. };
  467. break;
  468. case DEVICE_USAGE.phoneMute.usageId:
  469. this.deviceCommand.inputReport.phoneMute = {
  470. reportId,
  471. usageOffset: usageOffset + (i * reportSize),
  472. isAbsolute: item.isAbsolute ?? false
  473. };
  474. break;
  475. default:
  476. break;
  477. }
  478. });
  479. usageOffset += item.reportCount * item.reportSize;
  480. });
  481. });
  482. if (!this.deviceCommand.inputReport.phoneMute || !this.deviceCommand.inputReport.hookSwitch) {
  483. logger.warn('parseInputReports - no phoneMute or hookSwitch. Skip. Returning false');
  484. return false;
  485. }
  486. return true;
  487. }
  488. /**
  489. * Parse output reports.
  490. *
  491. * @private
  492. * @param {HIDReportInfo[]} outputReports -.
  493. * @returns {void} -.
  494. */
  495. private parseOutputReports(outputReports: HIDReportInfo[]) {
  496. outputReports.forEach((report: HIDReportInfo) => {
  497. if (!report?.items?.length || report.reportId === undefined) {
  498. return;
  499. }
  500. let usageOffset = 0;
  501. const usageOffsetMap: Map<number, number> = new Map();
  502. report.items.forEach(item => {
  503. if (item.usages === undefined || item.reportSize === undefined || item.reportCount === undefined) {
  504. logger.warn('parseOutputReports invalid parameters!');
  505. return;
  506. }
  507. const reportSize = item.reportSize ?? 0;
  508. const reportId = report.reportId ?? 0;
  509. item.usages.forEach((usage: number, i: number) => {
  510. switch (usage) {
  511. case DEVICE_USAGE.mute.usageId:
  512. this.deviceCommand.outputReport.mute = {
  513. reportId,
  514. usageOffset: usageOffset + (i * reportSize)
  515. };
  516. usageOffsetMap.set(usage, usageOffset + (i * reportSize));
  517. break;
  518. case DEVICE_USAGE.offHook.usageId:
  519. this.deviceCommand.outputReport.offHook = {
  520. reportId,
  521. usageOffset: usageOffset + (i * reportSize)
  522. };
  523. usageOffsetMap.set(usage, usageOffset + (i * reportSize));
  524. break;
  525. case DEVICE_USAGE.ring.usageId:
  526. this.deviceCommand.outputReport.ring = {
  527. reportId,
  528. usageOffset: usageOffset + (i * reportSize)
  529. };
  530. usageOffsetMap.set(usage, usageOffset + (i * reportSize));
  531. break;
  532. case DEVICE_USAGE.hold.usageId:
  533. this.deviceCommand.outputReport.hold = {
  534. reportId,
  535. usageOffset: usageOffset = i * reportSize
  536. };
  537. usageOffsetMap.set(usage, usageOffset + (i * reportSize));
  538. break;
  539. default:
  540. break;
  541. }
  542. });
  543. usageOffset += item.reportCount * item.reportSize;
  544. });
  545. const reportLength = usageOffset;
  546. for (const [ usage, offset ] of usageOffsetMap) {
  547. this.outputEventGenerators[usage] = (val: number) => {
  548. const reportData = new Uint8Array(reportLength / 8);
  549. if (offset >= 0 && val) {
  550. const byteIndex = Math.trunc(offset / 8);
  551. const bitPosition = offset % 8;
  552. // eslint-disable-next-line no-bitwise
  553. reportData[byteIndex] = 1 << bitPosition;
  554. }
  555. return reportData;
  556. };
  557. }
  558. });
  559. let hook, mute, ring;
  560. for (const item in this.outputEventGenerators) {
  561. if (Object.prototype.hasOwnProperty.call(this.outputEventGenerators, item)) {
  562. let newItem = this.getHexByte(item);
  563. newItem = `0x0${newItem}`;
  564. if (DEVICE_USAGE.mute.usageId === Number(newItem)) {
  565. mute = this.outputEventGenerators[DEVICE_USAGE.mute.usageId];
  566. } else if (DEVICE_USAGE.offHook.usageId === Number(newItem)) {
  567. hook = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId];
  568. } else if (DEVICE_USAGE.ring.usageId === Number(newItem)) {
  569. ring = this.outputEventGenerators[DEVICE_USAGE.ring.usageId];
  570. }
  571. }
  572. }
  573. if (!mute && !ring && !hook) {
  574. return false;
  575. }
  576. return true;
  577. }
  578. /**
  579. * Send device report.
  580. *
  581. * @param {{ command: string }} data -.
  582. * @returns {void} -.
  583. */
  584. async sendDeviceReport(data: { command: string; }) {
  585. if (!data?.command || !this.deviceInfo?.device?.opened || !this.isParseDescriptorsSuccess) {
  586. logger.warn('There are currently non-compliant conditions');
  587. return;
  588. }
  589. logger.warn(`sendDeviceReport data.command: ${data.command}`);
  590. if (data.command === COMMANDS.MUTE_ON || data.command === COMMANDS.MUTE_OFF) {
  591. if (!this.outputEventGenerators[DEVICE_USAGE.mute.usageId]) {
  592. logger.warn('current no parse mute event');
  593. return;
  594. }
  595. } else if (data.command === COMMANDS.ON_HOOK || data.command === COMMANDS.OFF_HOOK) {
  596. if (!this.outputEventGenerators[DEVICE_USAGE.offHook.usageId]) {
  597. logger.warn('current no parse offHook event');
  598. return;
  599. }
  600. } else if (data.command === COMMANDS.ON_RING || data.command === COMMANDS.OFF_RING) {
  601. if (!this.outputEventGenerators[DEVICE_USAGE.ring.usageId]) {
  602. logger.warn('current no parse ring event');
  603. return;
  604. }
  605. }
  606. let oldOffHook;
  607. let newOffHook;
  608. let newMuted;
  609. let newRing;
  610. let newHold;
  611. let offHookReport;
  612. let muteReport;
  613. let ringReport;
  614. let holdReport;
  615. let reportData = new Uint8Array();
  616. const reportId = this.matchReportId(data.command);
  617. if (reportId === 0) {
  618. logger.warn(`Unsupported command ${data.command}`);
  619. return;
  620. }
  621. /* keep old status. */
  622. const oldMuted = this.deviceInfo.muted;
  623. if (this.deviceInfo.hookStatus === HOOK_STATUS.OFF) {
  624. oldOffHook = true;
  625. } else if (this.deviceInfo.hookStatus === HOOK_STATUS.ON) {
  626. oldOffHook = false;
  627. } else {
  628. logger.warn('Invalid hook status');
  629. return;
  630. }
  631. const oldRing = this.deviceInfo.ring;
  632. const oldHold = this.deviceInfo.hold;
  633. logger.warn(
  634. `send device command: old_hook=${oldOffHook}, old_muted=${oldMuted}, old_ring=${oldRing}`
  635. );
  636. /* get new status. */
  637. switch (data.command) {
  638. case COMMANDS.MUTE_ON:
  639. newMuted = true;
  640. break;
  641. case COMMANDS.MUTE_OFF:
  642. newMuted = false;
  643. break;
  644. case COMMANDS.ON_HOOK:
  645. newOffHook = false;
  646. break;
  647. case COMMANDS.OFF_HOOK:
  648. newOffHook = true;
  649. break;
  650. case COMMANDS.ON_RING:
  651. newRing = true;
  652. break;
  653. case COMMANDS.OFF_RING:
  654. newRing = false;
  655. break;
  656. case COMMANDS.ON_HOLD:
  657. newHold = true;
  658. break;
  659. case COMMANDS.OFF_HOLD:
  660. newHold = false;
  661. break;
  662. default:
  663. logger.info(`Unknown command ${data.command}`);
  664. return;
  665. }
  666. logger.warn(
  667. `send device command: new_hook = ${newOffHook}, new_muted = ${newMuted},
  668. new_ring = ${newRing} new_hold = ${newHold}`
  669. );
  670. if (this.outputEventGenerators[DEVICE_USAGE.mute.usageId]) {
  671. if (newMuted === undefined) {
  672. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](oldMuted);
  673. } else {
  674. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](newMuted);
  675. }
  676. }
  677. if (this.outputEventGenerators[DEVICE_USAGE.offHook.usageId]) {
  678. if (newOffHook === undefined) {
  679. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](oldOffHook);
  680. } else {
  681. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](newOffHook);
  682. }
  683. }
  684. if (this.outputEventGenerators[DEVICE_USAGE.ring.usageId]) {
  685. if (newRing === undefined) {
  686. ringReport = this.outputEventGenerators[DEVICE_USAGE.ring.usageId](oldRing);
  687. } else {
  688. ringReport = this.outputEventGenerators[DEVICE_USAGE.ring.usageId](newRing);
  689. }
  690. }
  691. if (this.outputEventGenerators[DEVICE_USAGE.hold.usageId]) {
  692. holdReport = this.outputEventGenerators[DEVICE_USAGE.hold.usageId](oldHold);
  693. }
  694. if (reportId === this.deviceCommand.outputReport.mute.reportId) {
  695. reportData = new Uint8Array(muteReport);
  696. }
  697. if (reportId === this.deviceCommand.outputReport.offHook.reportId) {
  698. reportData = new Uint8Array(offHookReport);
  699. }
  700. if (reportId === this.deviceCommand.outputReport.ring.reportId) {
  701. reportData = new Uint8Array(ringReport);
  702. }
  703. if (reportId === this.deviceCommand.outputReport.hold.reportId) {
  704. reportData = new Uint8Array(holdReport);
  705. }
  706. logger.warn(`[sendDeviceReport] send device command (before call webhid API)
  707. ${data.command}: reportId=${reportId}, reportData=${reportData}`);
  708. logger.warn(`reportData is ${JSON.stringify(reportData, null, ' ')}`);
  709. await this.deviceInfo.device.sendReport(reportId, reportData);
  710. /* update new status. */
  711. this.updateDeviceStatus(data);
  712. }
  713. /**
  714. * Update device status.
  715. *
  716. * @private
  717. * @param {{ command: string; }} data -.
  718. * @returns {void}
  719. */
  720. private updateDeviceStatus(data: { command: string; }) {
  721. switch (data.command) {
  722. case COMMANDS.MUTE_ON:
  723. this.deviceInfo.muted = true;
  724. break;
  725. case COMMANDS.MUTE_OFF:
  726. this.deviceInfo.muted = false;
  727. break;
  728. case COMMANDS.ON_HOOK:
  729. this.deviceInfo.hookStatus = HOOK_STATUS.ON;
  730. break;
  731. case COMMANDS.OFF_HOOK:
  732. this.deviceInfo.hookStatus = HOOK_STATUS.OFF;
  733. break;
  734. case COMMANDS.ON_RING:
  735. this.deviceInfo.ring = true;
  736. break;
  737. case COMMANDS.OFF_RING:
  738. this.deviceInfo.ring = false;
  739. break;
  740. case COMMANDS.ON_HOLD:
  741. this.deviceInfo.hold = true;
  742. break;
  743. case 'offHold':
  744. this.deviceInfo.hold = false;
  745. break;
  746. default:
  747. logger.warn(`Unknown command ${data.command}`);
  748. break;
  749. }
  750. logger.warn(
  751. `[updateDeviceStatus] device status after send command: hook=${this.deviceInfo.hookStatus},
  752. muted=${this.deviceInfo.muted}, ring=${this.deviceInfo.ring}`
  753. );
  754. }
  755. /**
  756. * Math given command with known commands.
  757. *
  758. * @private
  759. * @param {string} command -.
  760. * @returns {number} ReportId.
  761. */
  762. private matchReportId(command: string) {
  763. switch (command) {
  764. case COMMANDS.MUTE_ON:
  765. case COMMANDS.MUTE_OFF:
  766. return this.deviceCommand.outputReport.mute.reportId;
  767. case COMMANDS.ON_HOOK:
  768. case COMMANDS.OFF_HOOK:
  769. return this.deviceCommand.outputReport.offHook.reportId;
  770. case COMMANDS.ON_RING:
  771. case COMMANDS.OFF_RING:
  772. return this.deviceCommand.outputReport.ring.reportId;
  773. case COMMANDS.ON_HOLD:
  774. case COMMANDS.OFF_HOLD:
  775. return this.deviceCommand.outputReport.hold.reportId;
  776. default:
  777. logger.info(`Unknown command ${command}`);
  778. return 0;
  779. }
  780. }
  781. /**
  782. * Send reply report to device.
  783. *
  784. * @param {number} inputReportId -.
  785. * @param {(string | boolean | undefined)} curOffHook -.
  786. * @param {(string | undefined)} curMuted -.
  787. * @returns {void} -.
  788. */
  789. private async sendReplyReport(
  790. inputReportId: number,
  791. curOffHook: string | boolean | undefined,
  792. curMuted: boolean | string | undefined
  793. ) {
  794. const reportId = this.retriveInputReportId(inputReportId);
  795. if (!this.deviceInfo?.device?.opened) {
  796. logger.warn('[sendReplyReport] device is not opened or does not exist');
  797. return;
  798. }
  799. if (reportId === 0 || curOffHook === undefined || curMuted === undefined) {
  800. logger.warn(`[sendReplyReport] return, provided data not valid,
  801. reportId: ${reportId}, curOffHook: ${curOffHook}, curMuted: ${curMuted}`);
  802. return;
  803. }
  804. let reportData = new Uint8Array();
  805. let muteReport;
  806. let offHookReport;
  807. let ringReport;
  808. if (this.deviceCommand.outputReport.offHook.reportId === this.deviceCommand.outputReport.mute.reportId) {
  809. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
  810. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](curOffHook);
  811. reportData = new Uint8Array(offHookReport);
  812. for (const [ i, data ] of muteReport.entries()) {
  813. // eslint-disable-next-line no-bitwise
  814. reportData[i] |= data;
  815. }
  816. } else if (reportId === this.deviceCommand.outputReport.offHook.reportId) {
  817. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](curOffHook);
  818. reportData = new Uint8Array(offHookReport);
  819. } else if (reportId === this.deviceCommand.outputReport.mute.reportId) {
  820. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
  821. reportData = new Uint8Array(muteReport);
  822. } else if (reportId === this.deviceCommand.outputReport.ring.reportId) {
  823. ringReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
  824. reportData = new Uint8Array(ringReport);
  825. }
  826. logger.warn(`[sendReplyReport] send device reply: reportId=${reportId}, reportData=${reportData}`);
  827. await this.deviceInfo.device.sendReport(reportId, reportData);
  828. }
  829. /**
  830. * Retrieve input report id.
  831. *
  832. * @private
  833. * @param {number} inputReportId -.
  834. * @returns {number} ReportId -.
  835. */
  836. private retriveInputReportId(inputReportId: number) {
  837. let reportId = 0;
  838. if (this.deviceCommand.outputReport.offHook.reportId === this.deviceCommand.outputReport.mute.reportId) {
  839. reportId = this.deviceCommand.outputReport.offHook.reportId;
  840. } else if (inputReportId === this.deviceCommand.inputReport.hookSwitch.reportId) {
  841. reportId = this.deviceCommand.outputReport.offHook.reportId;
  842. } else if (inputReportId === this.deviceCommand.inputReport.phoneMute.reportId) {
  843. reportId = this.deviceCommand.outputReport.mute.reportId;
  844. }
  845. return reportId;
  846. }
  847. /**
  848. * Get the hexadecimal bytes.
  849. *
  850. * @param {number|string} data -.
  851. * @returns {string}
  852. */
  853. getHexByte(data: number | string) {
  854. let hex = Number(data).toString(16);
  855. while (hex.length < 2) {
  856. hex = `0${hex}`;
  857. }
  858. return hex;
  859. }
  860. }