UNPKG

aes70

Version:

A controller library for the AES70 protocol.

522 lines (451 loc) 14.4 kB
import { warn, error } from '../log.js'; import { Events } from '../events.js'; import { OcaDeviceManager } from './ControlClasses/OcaDeviceManager.js'; import { OcaSecurityManager } from './ControlClasses/OcaSecurityManager.js'; import { OcaFirmwareManager } from './ControlClasses/OcaFirmwareManager.js'; import { OcaSubscriptionManager } from './ControlClasses/OcaSubscriptionManager.js'; import { OcaPowerManager } from './ControlClasses/OcaPowerManager.js'; import { OcaNetworkManager } from './ControlClasses/OcaNetworkManager.js'; import { OcaMediaClockManager } from './ControlClasses/OcaMediaClockManager.js'; import { OcaLibraryManager } from './ControlClasses/OcaLibraryManager.js'; import { OcaAudioProcessingManager } from './ControlClasses/OcaAudioProcessingManager.js'; import { OcaDeviceTimeManager } from './ControlClasses/OcaDeviceTimeManager.js'; import { OcaTaskManager } from './ControlClasses/OcaTaskManager.js'; import { OcaCodingManager } from './ControlClasses/OcaCodingManager.js'; import { OcaDiagnosticManager } from './ControlClasses/OcaDiagnosticManager.js'; import { OcaBlock } from './ControlClasses/OcaBlock.js'; import { RemoteError } from './remote_error.js'; import { OcaStatus } from '../types/OcaStatus.js'; import tree_to_rolemap from './tree_to_rolemap.js'; import * as RemoteControlClasses from './ControlClasses.js'; import { OcaManagerDefaultObjectNumbers } from '../types/OcaManagerDefaultObjectNumbers.js'; import { OcaNotificationDeliveryMode } from '../types/OcaNotificationDeliveryMode.js'; const emptyUint8Array = new Uint8Array(0); function eventToKey(event) { const ono = event.EmitterONo; const id = event.EventID; return [ono, id.DefLevel, id.EventIndex].join(','); } const subscriberMethod = { ONo: 1055, MethodID: { DefLevel: 1, MethodIndex: 1, }, }; class EventSubscription { constructor(event, cb) { this.event = event; this.callbacks = []; this.cb = cb; this.subscribing = null; this.version = 0; } add_callback(cb) { this.callbacks.push(cb); } delete_callback(cb) { this.callbacks = this.callbacks.filter((entry) => entry !== cb); } emit(ok, notification) { this.callbacks.forEach((cb) => { try { cb(ok, notification); } catch (error) { console.log('Event handler threw an exception', error); } }); } emit_error(error) { this.emit(false, error); } has_subscribers() { return this.callbacks.length !== 0; } } /** * Controller class for a remote OCA device. * * This is the entry point for any interaction with a remote device. * Can be used to query the available object tree, or interact with the manager * classes. * * @param {ClientConnection} connection * The connection to use. */ export class RemoteDevice extends Events { constructor(connection, ...modules) { super(); this.objects = new Map(); this.connection = connection; this._stackDebug = false; this._supportsEV2 = undefined; this._checkEV2Promise = undefined; connection.on('error', (e) => { this.emit('error', e); }); connection.on('close', () => { this.emit('close'); }); this.modules = []; this.add_control_classes(Object.values(RemoteControlClasses)); modules.map((m) => this.add_control_classes(m)); /** * The device manager object. An instance of :class:`OcaDeviceManager`. */ this.DeviceManager = new OcaDeviceManager( OcaManagerDefaultObjectNumbers.DeviceManager, this ); /** * The Security manager object. An instance of :class:`OcaSecurityManager` */ this.SecurityManager = new OcaSecurityManager( OcaManagerDefaultObjectNumbers.SecurityManager, this ); /** * The Firmware manager object. An instance of :class:`OcaFirmwareManager` */ this.FirmwareManager = new OcaFirmwareManager( OcaManagerDefaultObjectNumbers.FirmwareManager, this ); /** * The Subscription manager object. An instance of :class:`OcaSubscriptionManager` */ this.SubscriptionManager = new OcaSubscriptionManager( OcaManagerDefaultObjectNumbers.SubscriptionManager, this ); /** * The Power manager object. An instance of :class:`OcaPowerManager` */ this.PowerManager = new OcaPowerManager( OcaManagerDefaultObjectNumbers.PowerManager, this ); /** * The Network manager object. An instance of :class:`OcaNetworkManager` */ this.NetworkManager = new OcaNetworkManager( OcaManagerDefaultObjectNumbers.NetworkManager, this ); /** * The MediaClock manager object. An instance of :class:`OcaMediaClockManager` */ this.MediaClockManager = new OcaMediaClockManager( OcaManagerDefaultObjectNumbers.MediaClockManager, this ); /** * The Library manager object. An instance of :class:`OcaLibraryManager` */ this.LibraryManager = new OcaLibraryManager( OcaManagerDefaultObjectNumbers.LibraryManager, this ); /** * The AudioProcessing manager object. An instance of :class:`OcaAudioProcessingManager` */ this.AudioProcessingManager = new OcaAudioProcessingManager( OcaManagerDefaultObjectNumbers.AudioProcessingManager, this ); /** * The DeviceTime manager object. An instance of :class:`OcaDeviceTimeManager` */ this.DeviceTimeManager = new OcaDeviceTimeManager( OcaManagerDefaultObjectNumbers.DeviceTimeManager, this ); /** * The Task manager object. An instance of :class:`OcaTaskManager` */ this.TaskManager = new OcaTaskManager( OcaManagerDefaultObjectNumbers.TaskManager, this ); /** * The Coding manager object. An instance of :class:`OcaCodingManager` */ this.CodingManager = new OcaCodingManager( OcaManagerDefaultObjectNumbers.CodingManager, this ); /** * The Diagnostic manager object. An instance of :class:`OcaDiagnosticManager` */ this.DiagnosticManager = new OcaDiagnosticManager( OcaManagerDefaultObjectNumbers.DiagnosticManager, this ); /** * The Root object. An instance of :class:`OcaBlock` */ this.Root = new OcaBlock(100, this); this.subscriptions = new Map(); } /** * Close the associated connection. */ close() { this.connection.close(); } send_command(cmd, returnType, callback, name) { const stack = this._stackDebug ? new Error().stack : null; return this.connection.send_command(cmd, returnType, callback, stack, name); } async _doSubscribe(event) { const { _checkEV2Promise } = this; if (_checkEV2Promise) await _checkEV2Promise; const { _supportsEV2, SubscriptionManager } = this; if (_supportsEV2 === undefined || _supportsEV2) { const p = SubscriptionManager.AddSubscription2( event, OcaNotificationDeliveryMode.Normal, emptyUint8Array ); try { if (_supportsEV2 === undefined) this._checkEV2Promise = p; await p; if (_supportsEV2 === undefined) { this._supportsEV2 = true; } return 2; } catch (err) { if (!(err instanceof RemoteError)) { throw err; } this._supportsEV2 = false; } finally { if (_supportsEV2 === undefined) { this._checkEV2Promise = undefined; } } } await SubscriptionManager.AddSubscription( event, subscriberMethod, emptyUint8Array, OcaNotificationDeliveryMode.Normal, emptyUint8Array ); return 1; } _doUnsubscribe(info, event) { if (info.version === 2) { return this.SubscriptionManager.RemoveSubscription2( event, OcaNotificationDeliveryMode.Normal, emptyUint8Array ); } else if (info.version === 1) { return this.SubscriptionManager.RemoveSubscription( event, subscriberMethod ); } else { // If this happens, the subscription failed. In this case // there is also nothing to do here. } } add_subscription(event, callback) { if (this.connection.is_closed()) throw new Error('Connection was closed.'); const key = eventToKey(event); const subscriptions = this.subscriptions; let info = subscriptions.get(key); if (info) { info.add_callback(callback); return; } /* do the actual subscription */ const dropSubscribers = () => { this.subscriptions.delete(key); this.connection._removeSubscriber(event); }; const cb = (ok, notification) => { const S = this.subscriptions.get(key); if (!S) { warn('Subscription lost.'); return; } S.emit(ok, notification); if (!ok || notification.exception) { dropSubscribers(); } else if (S.version > 0 && !S.has_subscribers()) { dropSubscribers(); this._doUnsubscribe(S, event).catch((error) => { console.error('Unsubscribe failed: ', error); }); } }; this.connection._addSubscriber(event, cb); info = new EventSubscription(event, cb); info.add_callback(callback); subscriptions.set(key, info); const p = this._doSubscribe(event); p.then( (version) => { info.version = version; }, (error) => { info.emit_error(error); dropSubscribers(); } ); } remove_subscription(event, callback) { const key = eventToKey(event); const info = this.subscriptions.get(key); if (!info) return Promise.reject('Callback not registered.'); info.delete_callback(callback); } find_best_class(id) { if (typeof id === 'object' && id.ClassID) id = id.ClassID; while (id.length) { const result = this.find_class_by_id(id); if (result) return result; id = id.substr(0, id.length - 1); } return null; } /** * Add a set of control classes. When communicating with a device the * objects created for remote control objects will be picked from the * ones added. The standard control classes are always added by * default. * * @param {Object|Array} module - The set of classes to add. Either an * object contains the control classes with the classid as key, or * an array of control classes. */ add_control_classes(module) { if (Array.isArray(module)) { const m = {}; for (let i = 0; i < module.length; i++) { const o = module[i]; m[o.ClassID] = o; } module = m; } else if (typeof module !== 'object') { throw new Error('Unsupported module.'); } this.modules.push(module); } find_class_by_id(id) { if (typeof id === 'object' && id.ClassID) id = id.ClassID; const modules = this.modules; for (let i = modules.length - 1; i >= 0; i--) { const ret = modules[i][id]; if (ret) return ret; } return null; } allocate(c, ono) { if (typeof ono === 'object') ono = ono.valueOf(); const objects = this.objects; if (!objects.has(ono)) { objects.set(ono, new c(ono, this)); } return objects.get(ono); } resolve_object(o) { // OcaBlockMember if ('MemberObjectIdentification' in o) return this.resolve_object(o.MemberObjectIdentification); // OcaObjectIdentification if ('ONo' in o && 'ClassIdentification' in o) { const ono = o.ONo; const id = o.ClassIdentification; return this.allocate(this.find_best_class(id), ono); } throw new TypeError('Expected OcaObjectIdentification or OcaBlockMember'); } GetDeviceTree() { const get_members = (block) => { return block.GetMembers().then((a) => { const ret = []; a = a.map(this.resolve_object, this); for (let i = 0; i < a.length; i++) { ret.push(Promise.resolve(a[i])); if (a[i].ClassID.startsWith(OcaBlock.ClassID)) { ret.push(get_members(a[i])); } } return Promise.all(ret); }); }; return get_members(this.Root); } /** * Discovers the device object tree. This are all objects starting at the Root * block. * * @returns {Promise} The object tree. A recursive tree structure consisting of arrays of objects. * Each block is followed by an array of it's children. */ get_device_tree() { return this.GetDeviceTree(); } /** * Returns a map of role paths to objects. This is a convenience function * which internally calls get_device_tree and then tree_to_rolemap. * If more than one object has the same role name on the same tree level, * their role names will be appended with numbers starting at 1. * * @param {String} [separator='/'] Optional argument used as a separator * for levels in the tree. * @returns {Promise<Map<string, Object>>} The map of role paths to control * objects. */ get_role_map(separator) { return this.get_device_tree().then(function (tree) { return tree_to_rolemap(tree, separator); }); } discover_all_fallback() { return this.GetDeviceTree().then((tree) => { const ret = []; const it = function (a) { for (let i = 0; i < a.length; i++) { if (Array.isArray(a[i])) { it(a[i]); } else { ret.push(a[i]); } } }; it(tree); return ret; }); } /** * Discovers the complete object tree of this device starting * from the root block. The root block itself will not be part * of the resulting list. * * @deprecated Use :func:`get_device_tree` instead. * @returns {Promise} The object list. */ discover_all() { return this.Root.GetMembersRecursive() .then((res) => res.map(this.resolve_object, this)) .catch(() => this.discover_all_fallback()); } /** * Set the keepalive interval. * @param {number} seconds - Keepalive interval in seconds. */ set_keepalive_interval(seconds) { this.connection.set_keepalive_interval(seconds); } /** * Enable or disable stack debug. * */ enable_stack_debug(enable) { this._stackDebug = !!enable; } }