@senzil/cec-monitor
Version:
HDMI-CEC library with a simple monitor written on ES6 to make cec enabled apps.
912 lines (812 loc) • 28.9 kB
JavaScript
/**
* Created by pablo on 6/13/17.
*/
'use strict'
import {spawn} from 'child_process'
import {EventEmitter} from 'events'
import estream from 'event-stream'
import deasync from 'deasync'
import CEC from './HDMI-CEC.1.4'
import ON_DEATH from 'death'
import CECAdapterNotReadyError from './CECAdapterNotReadyError'
import CECTimeoutError from './CECTimeoutError'
import StateManager from './StateManager'
import Convert from './Convert'
import Validate from './Validate'
export default class CECMonitor extends EventEmitter {
OSDName;
com_port;
debug;
client;
ready;
no_serial;
auto_restart;
auto_restarting;
reconnect_intent;
params;
state_manager;
cache;
command_timeout;
user_control_hold_interval;
user_control_hold_interval_ref;
constructor(OSDName, options) {
super()
this.setMaxListeners(300)
this.ready = false
this.auto_restarting = false
this.reconnect_intent = false
this.no_serial = {
reconnect: false,
wait_time: 30, //in seconds
trigger_stop: false
}
this.cache = {
enable: true,
autorefresh: true,
timeout: 30 //in seconds
}
this.OSDName = OSDName || 'cec-monitor'
this.com_port = options.com_port || ''
this.auto_restart = options.auto_restart ? options.auto_restart : true
this.no_serial = Object.assign(this.no_serial, options.no_serial)
this.cache = Object.assign(this.cache, options.cache)
this.debug = options.debug
this.command_timeout = options.command_timeout || 3
this.user_control_hold_interval = options.user_control_hold_interval || 1000
this.user_control_hold_interval_ref = null
process.on('beforeExit', this.Stop)
process.on('exit', this.Stop)
if (!options.processManaged) {
ON_DEATH({uncaughtException: true})((signal, err) => {
if (err){
console.error(err)
}
return process.exit()
})
}
this.params = []
let earlyPrimaryAddress
if (options.recorder !== false){
this.params.push('-t', 'r')
earlyPrimaryAddress = 1
}
if (options.player === true){
this.params.push('-t', 'p')
earlyPrimaryAddress = earlyPrimaryAddress || 4
}
if (options.tuner === true){
this.params.push('-t', 't')
earlyPrimaryAddress = earlyPrimaryAddress || 3
}
if (options.audio === true){
this.params.push('-t', 'a')
earlyPrimaryAddress = earlyPrimaryAddress || 5
}
this.params.push('-o', this.OSDName, '-d', '31', '-p', options.hdmiport || 1, this.com_port)
// Cache of data about logical addresses
this.state_manager = new StateManager()
this.state_manager.primary = earlyPrimaryAddress
_initCecClient.call(this)
}
static get EVENTS() {
return {
_DATA: '_data',
_DEBUG: '_debug',
_ERROR: '_error',
_NOTICE: '_notice',
_PACKET: '_packet',
_READY: '_ready',
_RECEIVED: '_received',
_SENDED: '_sended',
_STOP: '_stop',
_TRAFFIC: '_traffic',
_OPCODE: '_opcode',
_WARNING: '_warning',
_NOSERIALPORT: '_no_serial_port',
_NOHDMICORD: '_no_hdmi_cord',
_EXPIREDCACHE: '_expired_cache',
_UPDATEDCACHE: '_updated_cache',
_USERCONTROLHOLD: '_user_control_hold',
ABORT: 'ABORT',
ACTIVE_SOURCE: 'ACTIVE_SOURCE',
CEC_VERSION: 'CEC_VERSION',
CLEAR_ANALOGUE_TIMER: 'CLEAR_ANALOGUE_TIMER',
CLEAR_DIGITAL_TIMER: 'CLEAR_ANALOGUE_TIMER',
CLEAR_EXTERNAL_TIMER: 'CLEAR_EXTERNAL_TIMER',
DECK_CONTROL: 'DECK_CONTROL',
DECK_STATUS: 'DECK_STATUS',
DEVICE_VENDOR_ID: 'DEVICE_VENDOR_ID',
FEATURE_ABORT: 'FEATURE_ABORT',
GET_CEC_VERSION: 'GET_CEC_VERSION',
GET_MENU_LANGUAGE: 'GET_MENU_LANGUAGE',
GIVE_AUDIO_STATUS: 'GIVE_AUDIO_STATUS',
GIVE_DECK_STATUS: 'GIVE_DECK_STATUS',
GIVE_DEVICE_POWER_STATUS: 'GIVE_DEVICE_POWER_STATUS',
GIVE_DEVICE_VENDOR_ID: 'GIVE_DEVICE_VENDOR_ID',
GIVE_OSD_NAME: 'GIVE_OSD_NAME',
GIVE_PHYSICAL_ADDRESS: 'GIVE_PHYSICAL_ADDRESS',
GIVE_SYSTEM_AUDIO_MODE_STATUS: 'GIVE_SYSTEM_AUDIO_MODE_STATUS',
GIVE_TUNER_DEVICE_STATUS: 'GIVE_TUNER_DEVICE_STATUS',
IMAGE_VIEW_ON: 'IMAGE_VIEW_ON',
INACTIVE_SOURCE: 'INACTIVE_SOURCE',
MENU_REQUEST: 'MENU_REQUEST',
MENU_STATUS: 'MENU_STATUS',
PLAY: 'PLAY',
POLLING_MESSAGE: 'POLLING_MESSAGE',
RECORD_OFF: 'RECORD_OFF',
RECORD_ON: 'RECORD_ON',
RECORD_STATUS: 'RECORD_STATUS',
RECORD_TV_SCREEN: 'RECORD_TV_SCREEN',
REPORT_AUDIO_STATUS: 'REPORT_AUDIO_STATUS',
REPORT_PHYSICAL_ADDRESS: 'REPORT_PHYSICAL_ADDRESS',
REPORT_POWER_STATUS: 'REPORT_POWER_STATUS',
REQUEST_ACTIVE_SOURCE: 'REQUEST_ACTIVE_SOURCE',
ROUTING_CHANGE: 'ROUTING_CHANGE',
ROUTING_INFORMATION: 'ROUTING_INFORMATION',
SELECT_ANALOGUE_SERVICE: 'SELECT_ANALOGUE_SERVICE',
SELECT_DIGITAL_SERVICE: 'SELECT_DIGITAL_SERVICE',
SET_ANALOGUE_TIMER: 'SET_ANALOGUE_TIMER',
SET_AUDIO_RATE: 'SET_AUDIO_RATE',
SET_DIGITAL_TIMER: 'SET_DIGITAL_TIMER',
SET_EXTERNAL_TIMER: 'SET_EXTERNAL_TIMER',
SET_MENU_LANGUAGE: 'SET_MENU_LANGUAGE',
SET_OSD_NAME: 'SET_OSD_NAME',
SET_OSD_STRING: 'SET_OSD_STRING',
SET_STREAM_PATH: 'SET_STREAM_PATH',
SET_SYSTEM_AUDIO_MODE: 'SET_SYSTEM_AUDIO_MODE',
SET_TIMER_PROGRAM_TITLE: 'SET_TIMER_PROGRAM_TITLE',
STANDBY: 'STANDBY',
SYSTEM_AUDIO_MODE_REQUEST: 'SYSTEM_AUDIO_MODE_REQUEST',
SYSTEM_AUDIO_MODE_STATUS: 'SYSTEM_AUDIO_MODE_STATUS',
TEXT_VIEW_ON: 'TEXT_VIEW_ON',
TIMER_CLEARED_STATUS: 'TIMER_CLEARED_STATUS',
TIMER_STATUS: 'TIMER_STATUS',
TUNER_DEVICE_STATUS: 'TUNER_DEVICE_STATUS',
TUNER_STEP_DECREMENT: 'TUNER_STEP_DECREMENT',
TUNER_STEP_INCREMENT: 'TUNER_STEP_INCREMENT',
USER_CONTROL_PRESSED: 'USER_CONTROL_PRESSED',
USER_CONTROL_RELEASE: 'USER_CONTROL_RELEASE',
VENDOR_COMMAND: 'VENDOR_COMMAND',
VENDOR_COMMAND_WITH_ID: 'VENDOR_COMMAND_WITH_ID',
VENDOR_REMOTE_BUTTON_DOWN: 'VENDOR_REMOTE_BUTTON_DOWN',
VENDOR_REMOTE_BUTTON_UP: 'VENDOR_REMOTE_BUTTON_UP'
}
}
/**
* Get copy of internal state information on CEC bus
*
* @param {number|string} [address] Optional address to request state for.
* (physical, logical or CEC.LogicalAddress names)
* If omitted, return an array of all addresses indexed by logical address
*
* @return {object|array[object]} An object or array of objects
* @see WriteMessage
* with index as the logical device address and/or values an object
* representing state of the logical address
*/
GetState = function(address) {
address = _parseAddress.call(this, address, undefined)
// Return copy of our state information
if (address === undefined) {
return JSON.parse(JSON.stringify(_getUpdatedStateManager.call(this)))
}
return JSON.parse(JSON.stringify(_getUpdatedDeviceState.call(this, address)))
}.bind(this)
/**
* Get physical address of this instance
* @return {string} Physical address used by this instance
*/
GetPhysicalAddress = function() {
return this.state_manager.primary.route
}.bind(this)
/**
* Get first logical address of this device
* @return {number} First logical address used by this instance
*/
GetLogicalAddress = function() {
return this.state_manager.primary.logical
}.bind(this)
/**
* Get all logical addresses of this device
* @return {array[number]} Primary logical address used by this instance
*/
GetLogicalAddresses = function() {
return this.state_manager.owns.map(S => S.logical)
}.bind(this)
/**
* Get the physical address from logical address
* @param {number} logical
* @return {string|null}
*/
Logical2Physical = function (logical) {
logical = _parseAddress.call(this, logical, null)
if (logical === null) {
return null
}
return _getUpdatedDeviceState.call(this, logical).route
}.bind(this)
/**
* Get logical address from physical address
* @param {string} physical
* @return {number|null}
*/
Physical2Logical = function (physical) {
physical = _parseAddress.call(this, physical, null)
return physical
}.bind(this)
/**
* Get OSD name for given address
* @param {string|number} address (physical, logical or CEC.LogicalAddress names)
* @see WriteMessage
* @return {string}
*/
GetOSDName = function(address) {
address = _parseAddress.call(this, address, null)
if (address === null) {
return ''
}
return _getUpdatedDeviceState.call(this, address).osdname
}.bind(this)
/**
* Get power status for given address
* @param {string|number} address (physical, logical or CEC.LogicalAddress names)
* @see WriteMessage
* @return {number|null}
*/
GetPowerStatus = function(address) {
address = _parseAddress.call(this, address, null)
if (address === null) {
return null
}
return _getUpdatedDeviceState.call(this, address).status
}.bind(this)
/**
* Get power status for given address as string
* @param {string|number} address (physical, logical or CEC.LogicalAddress names)
* @see WriteMessage
* @return {string|null}
*/
GetPowerStatusName = function(address) {
address = _parseAddress.call(this, address, null)
if (address === null) {
return null
}
return _getUpdatedDeviceState.call(this, address).power
}.bind(this)
/**
* Retrieve physical address of currently selected source
*
* @return {string} Physical address of the source currently selected
*/
GetActiveSource = function() {
return this.state_manager.active_source
}.bind(this)
WriteRawMessage = async function(raw) {
if (this.client && this.ready) {
try {
return this.client.stdin.write(raw + '\n')
} catch (e) {
console.log('Is not possible write messages to the cec adapter right now')
console.log(e)
throw new CECAdapterNotReadyError(e)
}
}
console.log('the cec adapter is not ready')
throw new CECAdapterNotReadyError()
}.bind(this)
WriteMessage = function(source, target, opcode, args) {
let msg = `tx ${[((source << 4) + target), opcode].concat(args || []).map(h => `0${h.toString(16)}`.substr(-2)).join(':')}`
return this.WriteRawMessage(msg)
}.bind(this);
/**
* Send a 'tx' message on CEC bus
*
* @param {string|number|null} source Logical address for source of message (defaults to own address if null)
* @param {string|number} target Logical address for target of message (defaults to broadcast if null)
* @param {string|number} opcode Opcode for message expressed as a byte value or STRING label
* @param {string|number|array[number]} [args] Optional arguments for opcode, type depending on opcode
* @example
* monitor.SendMessage(CEC.LogicalAddress.PLAYBACKDEVICE1, CEC.LogicalAddress.BROADCAST, CEC.Opcode.SET_OSD_NAME,[0x46,0x72,0x69,0x73,0x62,0x65,0x65]);
* @example
* monitor.SendMessage(4, 15, 70, [70,114,105,115,98,101,101];
* @example
* monitor.SendMessage('0x4', '0xF', '0x46', [0x46,0x72,0x69,0x73,0x62,0x65,0x65]);
* @example
* monitor.SendMessage('PLAYBACKDEVICE1','BROADCAST','SET_OSD_NAME','Frisbee');
* @example
* monitor.SendMessage('playbackdevice1', 'broadcast', 'set_osd_name','Frisbee');
* @example
* // Can specify physical address as string, using dot notation
* monitor.SendMessage(CEC.LogicalAddress.UNREGISTERED, CEC.LogicalAddress.BROADCAST, CEC.Opcode.ACTIVE_SOURCE,'2.0.0.0');
* // Or as an array of bytes
* monitor.SendMessage(CEC.LogicalAddress.UNREGISTERED, CEC.LogicalAddress.BROADCAST, CEC.Opcode.ACTIVE_SOURCE,[0x20,0x0]);
* @example
* // Default source is the client - default destination is broadcast
* monitor.SendMessage(null,null, 'set_osd_name','Frisbee');
* @see cec
* @see WriteMessage
* @return {Promise} When promise is resolved, the message is sent, otherwise if rejected, the cec adapter is not ready
* @throws CECAdapterNotReadyError
*/
SendMessage = function(source, target, opcode, args) {
source = _parseAddress.call(this, source, this.GetLogicalAddress())
target = _parseAddress.call(this, target, CEC.LogicalAddress.BROADCAST)
if (typeof opcode === 'string') {
if (Validate.isHexaNumber(opcode)){
opcode = Number.parseInt(opcode, 16)
}
else if (CEC.Opcode.hasOwnProperty(opcode.toLocaleUpperCase())) {
opcode = CEC.Opcode[opcode.toLocaleUpperCase()]
}
}
if (typeof args === 'string') {
// If a phyiscal address
if (Validate.isRoute(args)) {
args = Convert.routeToArgs(args)
}
else if (Validate.isHexaNumber(args)){
args = Number.parseInt(args, 16)
}
// Otherwise treat as string argument
else {
args = args.split('').map(s => s.charCodeAt(0))
}
}
// todo: Create classes for complex operations (EG. SELECT_DIGITAL_SERVICE), that can be provided and generate their own arguments array
// else if(typeof args === 'object' && args instanceof Command)
return this.WriteMessage(source, target, opcode, args)
}.bind(this)
/**
* Send a 'tx' message on CEC bus and wait for a event like response or timeout
* @async
* @param {String|Number|null} source Logical address for source of message (defaults to own address if null)
* @param {String|Number} target Logical address for target of message (defaults to broadcast if null)
* @param {String|Number} opcode Opcode for message expressed as a byte value or STRING label
* @param {String} event to wait for a response
* @param {String|Number|Array[]} [args] Optional arguments for opcode, type depending on opcode
* @example
* monitor.SendCommand(CEC.LogicalAddress.RECORDER1, CEC.LogicalAddress.TV, CEC.Opcode.GIVE_DEVICE_POWER_STATUS, CECMonitor.EVENTS.REPORT_POWER_STATUS);
* source, logical, opcode and args work like SendMessage function
* @see SendMessage
* @return {Promise} When promise is resolved, the message is sent and get the packet from the event as response, otherwise if rejected, the cec adapter is not ready or the event timeouted
* @throws CECAdapterNotReadyError
* @throws CECTimeoutError
*/
SendCommand = function(source, target, opcode, event, args){
source = _parseAddress.call(this, source, this.GetLogicalAddress())
target = _parseAddress.call(this, target, CEC.LogicalAddress.BROADCAST)
return this.SendMessage(source, target, opcode, args)
.then(() => _eventPromise.call(this, target, event, this.command_timeout * 1000))
}.bind(this)
/**
* Async function what resolve only when the adapter is ready to receive messages
* @returns {Promise} Resolved promise with true as resolved value
* @public
*/
WaitForReady = async function() {
deasync.loopWhile(() => !this.ready)
return this.ready
}.bind(this);
Stop = function() {
if (this.client) {
this.client.kill('SIGINT')
_onClose.call(this)
}
}.bind(this);
}
/*** BEGIN INTERNAL FUNCTIONS
*
* THEY SHOULD NOT BE USED BY END USERS AND SHOULD NOT BE EXPORTED
*
* ***/
const _getUpdatedStateManager = function() {
if (this.cache.enable && this.cache.timeout > 0 && this.state_manager.timestamp < Date.now() - this.cache.timeout * 1000) {
return this.state_manager.map(ds => _getUpdatedDeviceState.call(this, ds.logical))
}
return this.state_manager
}
const _getUpdatedDeviceState = function (address) {
const currentState = this.state_manager[address]
if (this.cache.enable && this.cache.timeout > 0 && currentState.timestamp < Date.now() - this.cache.timeout * 1000) {
this.emit(CECMonitor.EVENTS._EXPIREDCACHE, currentState)
if (this.cache.autorefresh) {
let done = false
_getUpdatedDeviceStateAsync.call(this, address).then(() => done = true)
deasync.loopWhile(() => !done)
return this.state_manager[address]
}
}
return currentState
}
const _getUpdatedDeviceStateAsync = function(address) {
const primary = this.GetLogicalAddress()
return this.SendCommand(primary, address, CEC.Opcode.GIVE_DEVICE_POWER_STATUS, CECMonitor.EVENTS.REPORT_POWER_STATUS)
.catch(() => this.state_manager[address].status = CEC.PowerStatus.UNKNOWN)
.then(() => {
this.emit(CECMonitor.EVENTS._UPDATEDCACHE, this.state_manager[address])
return this.state_manager[address]
})
}
const _eventPromise = function(target, event, milisecondsToWait) {
let __listener, __timeout
const __removeListener = function __removeListener() {
this.removeListener(event, __listener)
}.bind(this)
return new Promise((resolve, reject) => {
__listener = packet => {
if (packet.source === target){
clearTimeout(__timeout)
__removeListener()
return resolve(packet)
}
}
__timeout = setTimeout(() => {
__removeListener()
return reject(new CECTimeoutError(target, milisecondsToWait))
}, milisecondsToWait).unref()
this.on(event, __listener)
})
}
/**
* Start the cec-client process in monitor mode.
* @returns {spawn} cec-client process
* @private
*/
const _initCecClient = function() {
this.client = spawn('cec-client', this.params)
this.client.stdout
.pipe(estream.split())
.pipe(estream.map(_processStdOut.bind(this)))
this.client.on('close', _onClose.bind(this))
return this.client
}
/**
* On Close time. Prepare restart cec-client and/or launch _STOP event
* @type {function(this:CECMonitor)}
* @private
*/
const _onClose = function() {
this.client = null
if (this.auto_restarting) {
setTimeout(_initCecClient.bind(this), 15000)
}
else if (this.no_serial.trigger_stop || !this.reconnect_intent) {
clearInterval(this.user_control_hold_interval_ref)
return this.emit(CECMonitor.EVENTS._STOP)
} else if (this.reconnect_intent) {
setTimeout(_initCecClient.bind(this), this.no_serial.wait_time * 1000)
}
}
/**
* Process the standard out of cec-client
* @type {function(this:CECMonitor)}
* @private
*/
const _processStdOut = function(data, cb) {
if (/^TRAFFIC:.*/g.test(data)){
_processTraffic.call(this, data)
} else if (/^DEBUG:.*/g.test(data)) {
_processDebug.call(this, data)
} else if (/^NOTICE:.*/g.test(data)){
_processNotice.call(this, data)
} else if (/^waiting for input.*/g.test(data)) {
this.auto_restarting = false
this.reconnect_intent = false
this.ready = true
this.emit(CECMonitor.EVENTS._READY)
} else if (/^WARNING:.*/g.test(data)){
_processWarning.call(this, data)
} else if (/^ERROR:.*/g.test(data)){
_processError.call(this, data)
} else if (/(^no serial port given\. trying autodetect: FAILED)|(^Connection lost)/gu.test(data)) {
if (this.no_serial.reconnect) {
this.reconnect_intent = true
this.ready = false
}
this.emit(CECMonitor.EVENTS._NOSERIALPORT)
}
this.emit(CECMonitor.EVENTS._DATA, data)
cb(null, data)
}
/**
* Parse the packet info from stdout
* @type {function(this:CECMonitor)}
* @param {String} plain stdout text
* @private
*/
const _readPacket = function(plain) {
const regex = /^(TRAFFIC|DEBUG):\s\[\s*(\d*)\]\s(<<|>>)\s(([\d\w]{2}[:]?)+)$/gu
let match = regex.exec(plain)
if (match) {
let tokens = match[4].split(':').map(h => parseInt(h, 16))
let packet = {
type: match[1],
number: match[2],
flow: match[3] === '>>' ? 'IN' : 'OUT',
source: (tokens[0] & 0xF0) >> 4,
target: tokens[0] & 0x0F,
opcode: tokens[1],
args: tokens.slice(2)
}
this.emit(CECMonitor.EVENTS._PACKET, packet)
return packet
}
return null
}
/**
*
* @type {function(this:CECMonitor)}
* @param {String} plain stdout text
* @private
*/
const _processTraffic = function(plain){
this.emit(CECMonitor.EVENTS._TRAFFIC, plain)
let packet = _readPacket.call(this, plain)
if (packet) {
if (packet.flow === 'IN') {
this.emit(CECMonitor.EVENTS._RECEIVED, packet)
} else {
this.emit(CECMonitor.EVENTS._SENDED, packet)
}
if (!packet.opcode){
this.emit(CECMonitor.EVENTS.POLLING_MESSAGE, packet)
} else {
_processEvents.call(this, packet)
}
}
}
/**
* Process all received events
* @type {function(this:CECMonitor)}
* @param {Object} packet
* @private
*/
const _processEvents = function(packet) {
let data = {}
let physical, source, status, from, to
// Store opcode name as event property
packet.event = CEC.OpcodeNames[packet.opcode]
switch (packet.opcode) {
case CEC.Opcode.ACTIVE_SOURCE:
if (packet.args.length !== 2) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command ACTIVE_SOURCE with bad formated address')
}
source = Convert.argsToPhysical(packet.args)
physical = Convert.physicalToRoute(source)
// Update our records
this.state_manager.active_source = physical
data = {
val: source,
str: physical
}
break
case CEC.Opcode.CEC_VERSION:
if (packet.args.length !==1) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command CEC_VERSION without version')
}
this.state_manager[packet.source].cec = packet.args[0]
data = {
val: this.state_manager[packet.source].cec,
str: this.state_manager[packet.source].cecversion
}
break
case CEC.Opcode.DECK_STATUS:
if (packet.args.length !== 2) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command DECK_STATUS without Deck Info')
}
status = packet.args[0] << 8 | packet.args[1]
data = {
val: status,
str: CEC.DeckStatusNames[status]
}
break
case CEC.Opcode.DEVICE_VENDOR_ID:
if (packet.args.length !== 3) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command DEVICE_VENDOR_ID with bad arguments')
}
this.state_manager[packet.source].vendorid = Convert.argsToVendorID(packet.args)
data = {
val: this.state_manager[packet.source].vendorid,
str: this.state_manager[packet.source].vendor
}
break
case CEC.Opcode.REPORT_PHYSICAL_ADDRESS:
if (packet.args.length !== 3) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command REPORT_PHYSICAL_ADDRESS with bad formated address or device type')
}
// Update our records
this.state_manager[packet.source].physical = Convert.argsToPhysical(packet.args)
data = {
val: this.state_manager[packet.source].physical,
str: this.state_manager[packet.source].route
}
break
case CEC.Opcode.REPORT_POWER_STATUS:
if (packet.args.length !== 1) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command REPORT_POWER_STATUS with bad formated power status')
}
// Update our records
this.state_manager[packet.source].status = packet.args[0]
data = {
val: this.state_manager[packet.source].status,
str: this.state_manager[packet.source].power
}
break
case CEC.Opcode.ROUTING_CHANGE:
if (packet.args.length !== 4) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command ROUTING_CHANGE with bad formated addresses')
}
from = Convert.argsToPhysical(packet.args.slice(0, 2))
to = Convert.argsToPhysical(packet.args.slice(2, 4))
physical = Convert.physicalToRoute(to)
// Update our records
this.state_manager.active_source = physical
data = {
from: {
val: from,
str: Convert.physicalToRoute(from)
},
to: {
val: to,
str: physical
}
}
break
case CEC.Opcode.SET_OSD_NAME:
if (!packet.args.length) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command SET_OSD_NAME without OSD NAME')
}
// Update our records
this.state_manager[packet.source].osdname = Convert.argsToOSDName(packet.args)
data = {
val: this.state_manager[packet.source].osdname,
str: this.state_manager[packet.source].osdname
}
break
case CEC.Opcode.STANDBY:
if (packet.args.length !== 0) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command STANDBY with bad args')
}
break
case CEC.Opcode.IMAGE_VIEW_ON:
case CEC.Opcode.TEXT_VIEW_ON:
if (packet.args.length !== 0) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command IMAGE_VIEW_ON with bad args')
}
break
case CEC.Opcode.MENU_REQUEST:
if (packet.args.length !== 1) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command MENU_REQUEST with bad args')
}
data = {
val: packet.args[0],
str: CEC.MenuRequestTypeNames[packet.args[0]]
}
break
case CEC.Opcode.USER_CONTROL_PRESSED:
if (packet.args.length !== 1) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command USER_CONTROL_PRESSED with bad args')
}
data = {
val: packet.args[0],
str: CEC.UserControlCodeNames[packet.args[0]]
}
clearInterval(this.user_control_hold_interval_ref)
this.user_control_hold_interval_ref = setInterval(() => this.emit(CECMonitor.EVENTS._USERCONTROLHOLD, data), this.user_control_hold_interval).unref()
break
case CEC.Opcode.USER_CONTROL_RELEASE:
clearInterval(this.user_control_hold_interval_ref)
if (packet.args.length !== 0) {
return this.emit(CECMonitor.EVENTS._ERROR, 'opcode command USER_CONTROL_RELEASE with bad args')
}
break
}
packet.data = data
if (packet.event !== null) {
// Emit all OPCODE events to '_opcode' event
this.emit(CECMonitor.EVENTS._OPCODE, packet)
return this.emit(packet.event, packet)
}
}
/**
* Process notice text from cec-client
* @type {function(this:CECMonitor)}
* @param {String} data: notice stdout text
* @private
*/
const _processNotice = function(data) {
const regexLogical = /logical\saddress\(es\)\s=\s((Recorder|Playback|Tuner|Audio)\s\d?\s?\(\d\)\s){1,4}/gu
let match = regexLogical.exec(data)
if (match) {
const regexAddresses = /(Recorder\s\d\s|Playback\s\d\s|Tuner\s\d\s|Audio\s)\(?(\d)\)/gu
let innerMatch = regexAddresses.exec(match[0])
this.state_manager.primary = Number.parseInt(innerMatch[2], 10)
while (innerMatch){
let logical_address = Number.parseInt(innerMatch[2], 10)
this.state_manager[logical_address].osdname = this.OSDName
this.state_manager[logical_address].own = true
innerMatch = regexAddresses.exec(match[0])
}
}
const regexDevice = /base\sdevice:\s\w+\s\((\d{1,2})\),\sHDMI\sport\snumber:\s(\d{1,2}),/gu
match = regexDevice.exec(data)
if (match) {
this.state_manager.base = parseInt(match[1], 10)
this.state_manager.hdmi = parseInt(match[2], 10)
}
const regexPhysical = /physical\saddress:\s([\w.]+)/gu
match = regexPhysical.exec(data)
if (match) {
this.state_manager.owns.forEach(S => {
S.route = match[1]
})
}
return this.emit(CECMonitor.EVENTS._NOTICE, data)
}
/**
* Process debug text from cec-client
* @type {function(this:CECMonitor)}
* @param {String} data: debug stdout text
* @private
*/
const _processDebug = function(data){
if (/TRANSMIT_FAILED_ACK/gu.test(data)){
return this.emit(CECMonitor.EVENTS._NOHDMICORD)
}
if (this.debug) {
return this.emit(CECMonitor.EVENTS._DEBUG, data)
}
}
/**
* Process warning text from cec-client
* @type {function(this:CECMonitor)}
* @param {String} data: warning stdout text
* @private
*/
const _processWarning = function(data){
if (/COMMAND_REJECTED/gu.test(data)){
this.ready = false
this.auto_restarting = true
this.Stop()
}
return this.emit(CECMonitor.EVENTS._WARNING, data)
}
/**
* Process error text from cec-client
* @type {function(this:CECMonitor)}
* @param {String} data: error stdout text
* @private
*/
const _processError = function(data){
return this.emit(CECMonitor.EVENTS._ERROR, data)
}
/**
* Parse any address format and convert to logical address
* @param {number|string} address Address to parse/convert to logical address
* @param {number} def Default logical address if address invalid
*/
const _parseAddress = function(address, def) {
if (typeof address === 'string') {
if (Validate.isHexaNumber(address)){
address = Number.parseInt(address, 16)
}
else if (Validate.isRoute(address)) {
const state = this.state_manager.GetByRoute(address)
address = state ? state.logical : def
}
else if (CEC.LogicalAddress.hasOwnProperty(address.toLocaleUpperCase())) {
address = CEC.LogicalAddress[address.toLocaleUpperCase()]
}
else {
address = Number.parseInt(address, 10)
}
}
if (Number.isNaN(address) || address === null) {
address = def
}
else if (address > 15 || address < 0) {
address = def
}
return address
}
/*** END INTERNAL FUNCTIONS***/