diff --git a/index.js b/index.js index 5f74806..b637c38 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -const { exec, execSync, spawn } = require('child_process'); +const { exec, spawn } = require('child_process'); const { EventEmitter } = require('events'); const getKeyName = require('./keymap'); @@ -10,43 +10,51 @@ module.exports = class Client this.osdString = (typeof opts.osdString === 'string' && opts.osdString.length < 13) ? opts.osdString : 'CEC-Control'; + this.type = (typeof opts.type === 'string') ? opts.type.charAt(0) : 'p'; this.hdmiPorts = (opts.hdmiPorts > 0) ? opts.hdmiPorts : 3; this.broadcast = (opts.broadcast === false) ? false : true; - this.devices = this._scanDevices(); - if(Object.keys(this.devices).length === 0) return null; - - this.myDevice = this._getMyDevice(); - if(!this.myDevice) return null; - this.controlledDevice = 'dev0'; this.sourceNumber = 0; this.keyReleaseTimeout = null; - for(var deviceId in this.devices) - this.devices[deviceId] = { ...this.devices[deviceId], ...this._getDeviceFunctions(deviceId) }; + this.cec = { ...EventEmitter.prototype, ...this._getGlobalFunctions() }; + this.myDevice = null; - this.devices = { ...this.devices, ...this._getGlobalFunctions(), ...EventEmitter.prototype }; + this._scanDevices(); - this._createClient(); - - return this.devices; + return this.cec; } _scanDevices() { - var outStr; - var outArray = []; + exec(`echo scan | cec-client -s -t ${this.type} -o ${this.osdString} -d 1`, + { maxBuffer: 5 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => + { + if(error) return this.cec.emit('error', new Error('App cec-client had an error!')); - try { - outStr = execSync(`echo 'scan' | cec-client -s -t p -o ${this.osdString} -d 1`, - { stdio: 'pipe', windowsHide: true }).toString(); + const scannedDevices = this._parseScanOutput(String(stdout)); + const scannedKeys = Object.keys(scannedDevices); + if(scannedKeys.length === 0) + return this.cec.emit('error', new Error('CEC scan did not find any devices!')); - outArray = outStr.split('device #'); - } - catch(err) {} + this.myDevice = this._getMyDevice(scannedDevices); + if(!this.myDevice) + return this.cec.emit('error', new Error(`Could not obtain this CEC adapter info!`)); + + this.cec = { ...this.cec, ...scannedDevices }; + + for(var deviceId in this.cec) + this.cec[deviceId] = { ...this.cec[deviceId], ...this._getDeviceFunctions(deviceId) }; + this._createClient(); + }); + } + + _parseScanOutput(outStr) + { var devicesObject = {}; + var outArray = outStr.split('device #'); outArray.forEach(device => { @@ -85,13 +93,13 @@ module.exports = class Client return devicesObject; } - _getMyDevice() + _getMyDevice(devices) { - var keys = Object.keys(this.devices); + var keys = Object.keys(devices); for(var key of keys) { - var currObj = this.devices[key]; + var currObj = devices[key]; if(typeof currObj !== 'object') continue; @@ -108,20 +116,27 @@ module.exports = class Client _createClient() { this.doneInit = false; - this.cecClient = spawn('cec-client', ['-t', 'p', '-o', this.osdString, '-d', 8], { stdio: ['pipe', 'pipe', 'ignore'] }); + this.cecClient = spawn('cec-client', + ['-t', this.type, '-o', this.osdString, '-d', 8], + { stdio: ['pipe', 'pipe', 'ignore'] }); + this.cecClient.stdin.setEncoding('utf8'); this.cecClient.stdout.on('data', (data) => this._parseClientOutput(String(data))); - this.cecClient.once('close', () => this._createClient()); + this.cecClient.once('close', (code) => + { + if(this.doneInit) this._createClient(); + else this.cec.emit('error', new Error(`App cec-client exited with code: ${code}`)); + }); } _parseClientOutput(line) { if(!this.doneInit) { - if(line.includes('waiting for input')) + if(this.myDevice && line.includes('waiting for input')) { this.doneInit = true; - this.devices.emit('ready'); + this.cec.emit('ready'); } return; @@ -129,34 +144,35 @@ module.exports = class Client if(line.startsWith('power status:')) { - var logicalAddress = this.devices[this.controlledDevice].logicalAddress; + var logicalAddress = this.cec[this.controlledDevice].logicalAddress; var value = this._getLineValue(line); - this.devices[this.controlledDevice].powerStatus = value; - this.devices.emit(`${logicalAddress}:powerStatus`, value); + this.cec[this.controlledDevice].powerStatus = value; + this.cec.emit(`${logicalAddress}:powerStatus`, value); } - else if(line.startsWith('active source:') || (line.startsWith('logical address') && line.includes('active'))) + else if(line.startsWith('active source:') + || (line.startsWith('logical address') && line.includes('active'))) { - var logicalAddress = this.devices[this.myDevice].logicalAddress; + var logicalAddress = this.cec[this.myDevice].logicalAddress; var value = null; if(line.startsWith('logical address')) value = (line.includes('not')) ? 'no' : 'yes'; else value = this._getLineValue(line); - this.devices[this.myDevice].activeSource = value; - this.devices.emit(`${logicalAddress}:activeSource`, value); + this.cec[this.myDevice].activeSource = value; + this.cec.emit(`${logicalAddress}:activeSource`, value); } else if(line.startsWith('TRAFFIC:') && line.includes('>>')) { - var destAddress = this.devices[this.myDevice].logicalAddress; + var destAddress = this.cec[this.myDevice].logicalAddress; var value = this._getLineValue(line).toUpperCase(); if(line.includes(`>> 0${destAddress}:44:`)) { - this.devices.emit('keypress', getKeyName(value)); + this.cec.emit('keypress', getKeyName(value)); if(!this.keyReleaseTimeout) - this.devices.emit('keydown', getKeyName(value)); + this.cec.emit('keydown', getKeyName(value)); } else if(line.includes(`>> 0${destAddress}:8b:`)) { @@ -165,7 +181,7 @@ module.exports = class Client this.keyReleaseTimeout = setTimeout(() => { - this.devices.emit('keyup', getKeyName(value)); + this.cec.emit('keyup', getKeyName(value)); this.keyReleaseTimeout = null; }, 600); } @@ -185,7 +201,7 @@ module.exports = class Client turnOff: this.changePower.bind(this, deviceId, 'standby') }; - if(this.devices[deviceId].name === 'TV') + if(this.cec[deviceId].name === 'TV') { func.changeSource = (number) => { @@ -195,8 +211,8 @@ module.exports = class Client number = this.sourceNumber; } - var srcAddress = this.devices[this.myDevice].logicalAddress; - var destAddress = (this.broadcast === false) ? this.devices[deviceId].logicalAddress : 'F'; + var srcAddress = this.cec[this.myDevice].logicalAddress; + var destAddress = (this.broadcast === false) ? this.cec[deviceId].logicalAddress : 'F'; return this.command(`tx ${srcAddress}${destAddress}:82:${number}0:00`, null); } @@ -244,7 +260,7 @@ module.exports = class Client else if(value === powerStatus) resolve(powerStatus); else { - this.command(powerStatus, this.devices[deviceId].logicalAddress); + this.command(powerStatus, this.cec[deviceId].logicalAddress); var timedOut = false; var actionTimeout = setTimeout(() => timedOut = true, 40000); @@ -313,16 +329,16 @@ module.exports = class Client return new Promise((resolve, reject) => { - var logicalAddress = this.devices[deviceId].logicalAddress; + var logicalAddress = this.cec[deviceId].logicalAddress; var statusTimeout = setTimeout(() => { - this.devices[deviceId].powerStatus = 'Unknown'; - this.devices.emit(`${logicalAddress}:powerStatus`, null); + this.cec[deviceId].powerStatus = 'Unknown'; + this.cec.emit(`${logicalAddress}:powerStatus`, null); }, 10000); this.command(`pow ${logicalAddress}`); - this.devices.once(`${logicalAddress}:powerStatus`, (value) => + this.cec.once(`${logicalAddress}:powerStatus`, (value) => { clearTimeout(statusTimeout); resolve(value); @@ -336,16 +352,16 @@ module.exports = class Client return new Promise((resolve, reject) => { - var logicalAddress = this.devices[deviceId].logicalAddress; + var logicalAddress = this.cec[deviceId].logicalAddress; var activeTimeout = setTimeout(() => { - this.devices[deviceId].activeSource = 'Unknown'; - this.devices.emit(`${logicalAddress}:activeSource`, null); + this.cec[deviceId].activeSource = 'Unknown'; + this.cec.emit(`${logicalAddress}:activeSource`, null); }, 10000); this.command(`ad ${logicalAddress}`); - this.devices.once(`${logicalAddress}:activeSource`, (value) => + this.cec.once(`${logicalAddress}:activeSource`, (value) => { clearTimeout(activeTimeout); resolve(value);