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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  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 && 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 || !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
  586. || !this.deviceInfo.device || !this.deviceInfo.device.opened || !this.isParseDescriptorsSuccess) {
  587. logger.warn('There are currently non-compliant conditions');
  588. return;
  589. }
  590. logger.warn(`sendDeviceReport data.command: ${data.command}`);
  591. if (data.command === COMMANDS.MUTE_ON || data.command === COMMANDS.MUTE_OFF) {
  592. if (!this.outputEventGenerators[DEVICE_USAGE.mute.usageId]) {
  593. logger.warn('current no parse mute event');
  594. return;
  595. }
  596. } else if (data.command === COMMANDS.ON_HOOK || data.command === COMMANDS.OFF_HOOK) {
  597. if (!this.outputEventGenerators[DEVICE_USAGE.offHook.usageId]) {
  598. logger.warn('current no parse offHook event');
  599. return;
  600. }
  601. } else if (data.command === COMMANDS.ON_RING || data.command === COMMANDS.OFF_RING) {
  602. if (!this.outputEventGenerators[DEVICE_USAGE.ring.usageId]) {
  603. logger.warn('current no parse ring event');
  604. return;
  605. }
  606. }
  607. let oldOffHook;
  608. let newOffHook;
  609. let newMuted;
  610. let newRing;
  611. let newHold;
  612. let offHookReport;
  613. let muteReport;
  614. let ringReport;
  615. let holdReport;
  616. let reportData = new Uint8Array();
  617. const reportId = this.matchReportId(data.command);
  618. if (reportId === 0) {
  619. logger.warn(`Unsupported command ${data.command}`);
  620. return;
  621. }
  622. /* keep old status. */
  623. const oldMuted = this.deviceInfo.muted;
  624. if (this.deviceInfo.hookStatus === HOOK_STATUS.OFF) {
  625. oldOffHook = true;
  626. } else if (this.deviceInfo.hookStatus === HOOK_STATUS.ON) {
  627. oldOffHook = false;
  628. } else {
  629. logger.warn('Invalid hook status');
  630. return;
  631. }
  632. const oldRing = this.deviceInfo.ring;
  633. const oldHold = this.deviceInfo.hold;
  634. logger.warn(
  635. `send device command: old_hook=${oldOffHook}, old_muted=${oldMuted}, old_ring=${oldRing}`
  636. );
  637. /* get new status. */
  638. switch (data.command) {
  639. case COMMANDS.MUTE_ON:
  640. newMuted = true;
  641. break;
  642. case COMMANDS.MUTE_OFF:
  643. newMuted = false;
  644. break;
  645. case COMMANDS.ON_HOOK:
  646. newOffHook = false;
  647. break;
  648. case COMMANDS.OFF_HOOK:
  649. newOffHook = true;
  650. break;
  651. case COMMANDS.ON_RING:
  652. newRing = true;
  653. break;
  654. case COMMANDS.OFF_RING:
  655. newRing = false;
  656. break;
  657. case COMMANDS.ON_HOLD:
  658. newHold = true;
  659. break;
  660. case COMMANDS.OFF_HOLD:
  661. newHold = false;
  662. break;
  663. default:
  664. logger.info(`Unknown command ${data.command}`);
  665. return;
  666. }
  667. logger.warn(
  668. `send device command: new_hook = ${newOffHook}, new_muted = ${newMuted},
  669. new_ring = ${newRing} new_hold = ${newHold}`
  670. );
  671. if (this.outputEventGenerators[DEVICE_USAGE.mute.usageId]) {
  672. if (newMuted === undefined) {
  673. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](oldMuted);
  674. } else {
  675. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](newMuted);
  676. }
  677. }
  678. if (this.outputEventGenerators[DEVICE_USAGE.offHook.usageId]) {
  679. if (newOffHook === undefined) {
  680. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](oldOffHook);
  681. } else {
  682. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](newOffHook);
  683. }
  684. }
  685. if (this.outputEventGenerators[DEVICE_USAGE.ring.usageId]) {
  686. if (newRing === undefined) {
  687. ringReport = this.outputEventGenerators[DEVICE_USAGE.ring.usageId](oldRing);
  688. } else {
  689. ringReport = this.outputEventGenerators[DEVICE_USAGE.ring.usageId](newRing);
  690. }
  691. }
  692. if (this.outputEventGenerators[DEVICE_USAGE.hold.usageId]) {
  693. holdReport = this.outputEventGenerators[DEVICE_USAGE.hold.usageId](oldHold);
  694. }
  695. if (reportId === this.deviceCommand.outputReport.mute.reportId) {
  696. reportData = new Uint8Array(muteReport);
  697. }
  698. if (reportId === this.deviceCommand.outputReport.offHook.reportId) {
  699. reportData = new Uint8Array(offHookReport);
  700. }
  701. if (reportId === this.deviceCommand.outputReport.ring.reportId) {
  702. reportData = new Uint8Array(ringReport);
  703. }
  704. if (reportId === this.deviceCommand.outputReport.hold.reportId) {
  705. reportData = new Uint8Array(holdReport);
  706. }
  707. logger.warn(`[sendDeviceReport] send device command (before call webhid API)
  708. ${data.command}: reportId=${reportId}, reportData=${reportData}`);
  709. logger.warn(`reportData is ${JSON.stringify(reportData, null, ' ')}`);
  710. await this.deviceInfo.device.sendReport(reportId, reportData);
  711. /* update new status. */
  712. this.updateDeviceStatus(data);
  713. }
  714. /**
  715. * Update device status.
  716. *
  717. * @private
  718. * @param {{ command: string; }} data -.
  719. * @returns {void}
  720. */
  721. private updateDeviceStatus(data: { command: string; }) {
  722. switch (data.command) {
  723. case COMMANDS.MUTE_ON:
  724. this.deviceInfo.muted = true;
  725. break;
  726. case COMMANDS.MUTE_OFF:
  727. this.deviceInfo.muted = false;
  728. break;
  729. case COMMANDS.ON_HOOK:
  730. this.deviceInfo.hookStatus = HOOK_STATUS.ON;
  731. break;
  732. case COMMANDS.OFF_HOOK:
  733. this.deviceInfo.hookStatus = HOOK_STATUS.OFF;
  734. break;
  735. case COMMANDS.ON_RING:
  736. this.deviceInfo.ring = true;
  737. break;
  738. case COMMANDS.OFF_RING:
  739. this.deviceInfo.ring = false;
  740. break;
  741. case COMMANDS.ON_HOLD:
  742. this.deviceInfo.hold = true;
  743. break;
  744. case 'offHold':
  745. this.deviceInfo.hold = false;
  746. break;
  747. default:
  748. logger.warn(`Unknown command ${data.command}`);
  749. break;
  750. }
  751. logger.warn(
  752. `[updateDeviceStatus] device status after send command: hook=${this.deviceInfo.hookStatus},
  753. muted=${this.deviceInfo.muted}, ring=${this.deviceInfo.ring}`
  754. );
  755. }
  756. /**
  757. * Math given command with known commands.
  758. *
  759. * @private
  760. * @param {string} command -.
  761. * @returns {number} ReportId.
  762. */
  763. private matchReportId(command: string) {
  764. switch (command) {
  765. case COMMANDS.MUTE_ON:
  766. case COMMANDS.MUTE_OFF:
  767. return this.deviceCommand.outputReport.mute.reportId;
  768. case COMMANDS.ON_HOOK:
  769. case COMMANDS.OFF_HOOK:
  770. return this.deviceCommand.outputReport.offHook.reportId;
  771. case COMMANDS.ON_RING:
  772. case COMMANDS.OFF_RING:
  773. return this.deviceCommand.outputReport.ring.reportId;
  774. case COMMANDS.ON_HOLD:
  775. case COMMANDS.OFF_HOLD:
  776. return this.deviceCommand.outputReport.hold.reportId;
  777. default:
  778. logger.info(`Unknown command ${command}`);
  779. return 0;
  780. }
  781. }
  782. /**
  783. * Send reply report to device.
  784. *
  785. * @param {number} inputReportId -.
  786. * @param {(string | boolean | undefined)} curOffHook -.
  787. * @param {(string | undefined)} curMuted -.
  788. * @returns {void} -.
  789. */
  790. private async sendReplyReport(
  791. inputReportId: number,
  792. curOffHook: string | boolean | undefined,
  793. curMuted: boolean | string | undefined
  794. ) {
  795. const reportId = this.retriveInputReportId(inputReportId);
  796. if (!this.deviceInfo?.device || !this.deviceInfo?.device?.opened) {
  797. logger.warn('[sendReplyReport] device is not opened or does not exist');
  798. return;
  799. }
  800. if (reportId === 0 || curOffHook === undefined || curMuted === undefined) {
  801. logger.warn(`[sendReplyReport] return, provided data not valid,
  802. reportId: ${reportId}, curOffHook: ${curOffHook}, curMuted: ${curMuted}`);
  803. return;
  804. }
  805. let reportData = new Uint8Array();
  806. let muteReport;
  807. let offHookReport;
  808. let ringReport;
  809. if (this.deviceCommand.outputReport.offHook.reportId === this.deviceCommand.outputReport.mute.reportId) {
  810. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
  811. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](curOffHook);
  812. reportData = new Uint8Array(offHookReport);
  813. for (const [ i, data ] of muteReport.entries()) {
  814. // eslint-disable-next-line no-bitwise
  815. reportData[i] |= data;
  816. }
  817. } else if (reportId === this.deviceCommand.outputReport.offHook.reportId) {
  818. offHookReport = this.outputEventGenerators[DEVICE_USAGE.offHook.usageId](curOffHook);
  819. reportData = new Uint8Array(offHookReport);
  820. } else if (reportId === this.deviceCommand.outputReport.mute.reportId) {
  821. muteReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
  822. reportData = new Uint8Array(muteReport);
  823. } else if (reportId === this.deviceCommand.outputReport.ring.reportId) {
  824. ringReport = this.outputEventGenerators[DEVICE_USAGE.mute.usageId](curMuted);
  825. reportData = new Uint8Array(ringReport);
  826. }
  827. logger.warn(`[sendReplyReport] send device reply: reportId=${reportId}, reportData=${reportData}`);
  828. await this.deviceInfo.device.sendReport(reportId, reportData);
  829. }
  830. /**
  831. * Retrieve input report id.
  832. *
  833. * @private
  834. * @param {number} inputReportId -.
  835. * @returns {number} ReportId -.
  836. */
  837. private retriveInputReportId(inputReportId: number) {
  838. let reportId = 0;
  839. if (this.deviceCommand.outputReport.offHook.reportId === this.deviceCommand.outputReport.mute.reportId) {
  840. reportId = this.deviceCommand.outputReport.offHook.reportId;
  841. } else if (inputReportId === this.deviceCommand.inputReport.hookSwitch.reportId) {
  842. reportId = this.deviceCommand.outputReport.offHook.reportId;
  843. } else if (inputReportId === this.deviceCommand.inputReport.phoneMute.reportId) {
  844. reportId = this.deviceCommand.outputReport.mute.reportId;
  845. }
  846. return reportId;
  847. }
  848. /**
  849. * Get the hexadecimal bytes.
  850. *
  851. * @param {number|string} data -.
  852. * @returns {string}
  853. */
  854. getHexByte(data: number | string) {
  855. let hex = Number(data).toString(16);
  856. while (hex.length < 2) {
  857. hex = `0${hex}`;
  858. }
  859. return hex;
  860. }
  861. }