Skip to content

Commit

Permalink
Async CEC devices scan
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafostar committed Jul 24, 2019
1 parent 2cd13a1 commit ff06e76
Showing 1 changed file with 67 additions and 51 deletions.
118 changes: 67 additions & 51 deletions index.js
Original file line number Diff line number Diff line change
@@ -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');

Expand All @@ -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 =>
{
Expand Down Expand Up @@ -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;
Expand All @@ -108,55 +116,63 @@ 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;
}

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:`))
{
Expand All @@ -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);
}
Expand All @@ -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) =>
{
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down

0 comments on commit ff06e76

Please sign in to comment.