UNPKG

aes70

Version:

A controller library for the AES70 protocol.

2,061 lines (1,762 loc) 920 kB
(function () { 'use strict'; function warn(...args) { try { console.warn(...args); } catch (e) { // ignore error } } function log(...args) { try { console.log(...args); } catch (e) { // ignore error } } function error(...args) { try { console.error(...args); } catch (e) { // ignore error } } /** * Basic event handling class. */ class Events { constructor() { this.event_handlers = new Map(); this.event_handlers_cleared = false; } /** * Emit an event. * * @param {string} name - Name of the event. * @param {...*} args - Extra arguments. */ emit(name) { const handlers = this.event_handlers.get(name); const args = Array.prototype.slice.call(arguments, 1); if (!handlers) return; handlers.forEach((cb) => { try { cb.apply(this, args); } catch (e) { console.warn('ERROR when calling %o: %o', cb, e); } }); } /** * Subscribe to an event. * * @param {string} name - Name of the event. * @param {Function} cb - Callback function. */ on(name, cb) { if (typeof name !== 'string') throw new TypeError('Event name must be a string.'); if (typeof cb !== 'function') throw new TypeError('Event handler must be a function.'); let handlers = this.event_handlers.get(name); if (!handlers) { this.event_handlers.set(name, (handlers = new Set())); } handlers.add(cb); } addEventListener(name, cb) { this.on(name, cb); } /** * Removes an event handler. * * @param {string} name - Name of the event. * @param {Function} cb - Callback function. */ removeEventListener(name, cb) { const handlers = this.event_handlers.get(name); if (!handlers || !handlers.has(cb)) { if (!this.event_handlers_cleared) { console.warn('removeEventListeners(): not installed:', name, cb); } return; } handlers.delete(cb); } /** * Removes an event handler. * * @param {string} name * @param {Function} cb */ off(name, cb) { this.removeEventListener(name, cb); } /** * Removes all event listeners. */ removeAllEventListeners() { this.event_handlers.clear(); this.event_handlers_cleared = true; } /** * * @param {string} name * @param {Function} cb */ subscribe(name, cb) { this.on(name, cb); return () => { if (name === undefined) return; this.off(name, cb); name = undefined; }; } } class PDU { get messageType() { return this.constructor.messageType; } } function calculateByteLength(encoders, data) { let byteLength = 0; for (let i = 0; i < encoders.length; i++) { byteLength += encoders[i].encodedLength(data[i]); } return byteLength; } function encode(encoders, data, byteLength) { if (!byteLength) return null; const result = new ArrayBuffer(byteLength); const dataView = new DataView(result); for (let i = 0, pos = 0; i < encoders.length; i++) { pos = encoders[i].encodeTo(dataView, pos, data[i]); } return result; } class EncodedArguments { constructor(encoders, data) { this.encoders = encoders; this.data = data; this.byteLength = calculateByteLength(encoders, data); this.buffer = encode(encoders, data, this.byteLength); } encodeTo(dataView, pos) { pos = pos | 0; const { byteLength, buffer } = this; if (!byteLength) return pos; const src = new Uint8Array(buffer); const dst = new Uint8Array( dataView.buffer, dataView.byteOffset, dataView.byteLength ); dst.set(src, pos); return pos + byteLength; } } /** * Command packet. */ class Command extends PDU { constructor(target, method_level, method_index, param_count, parameters) { super(); this.target = +target; this.method_level = method_level | 0; this.method_index = method_index | 0; this.param_count = param_count | 0; this.parameters = parameters || null; this.handle = 0; } static get messageType() { return 0; } encode_to(dst, pos) { pos = pos | 0; dst.setUint32(pos, this.encoded_length()); pos += 4; dst.setUint32(pos, this.handle); pos += 4; dst.setUint32(pos, this.target); pos += 4; dst.setUint16(pos, this.method_level); pos += 2; dst.setUint16(pos, this.method_index); pos += 2; dst.setUint8(pos, this.param_count); pos++; if (this.param_count) { const parameters = this.parameters; if (parameters instanceof EncodedArguments) { pos = parameters.encodeTo(dst, pos); } else { new Uint8Array(dst.buffer).set( new Uint8Array(parameters), dst.byteOffset + pos ); pos += parameters.byteLength; } } return pos; } encoded_length() { return 17 + (this.param_count ? this.parameters.byteLength : 0); } decode_from(data, pos, data_len) { let len = data.getUint32(pos); pos += 4; this.handle = data.getUint32(pos); pos += 4; this.target = data.getUint32(pos); pos += 4; this.method_level = data.getUint16(pos); pos += 2; this.method_index = data.getUint16(pos); pos += 2; this.param_count = data.getUint8(pos); pos++; len -= 17; if (len < 0) throw new Error('Bad Command Length.'); if (len > 0) { if (!this.param_count) throw new Error('Expected no parameter bytes.'); this.parameters = data.buffer.slice( data.byteOffset + pos, data.byteOffset + pos + len ); pos += len; } return pos; } response(status_code, param_count, parameters) { return new Response(this.handle, status_code, param_count, parameters); } } /** * Command packet with response required. */ class CommandRrq extends Command { static get messageType() { return 1; } } function createType(Type) { if (!Type.isConstantLength) return Type; const encodedLength = Type.encodedLength(); const decode = Type.decode; const encodeTo = Type.encodeTo; const decodeFrom = Type.decodeFrom; return { isConstantLength: true, canEncode: Type.canEncode, encodedLength: Type.encodedLength, encodeTo: encodeTo, decode: decode, decodeFrom: decode ? function (dataView, pos) { const result = decode(dataView, pos); return [pos + encodedLength, result]; } : decodeFrom, decodeLength: function (dataView, pos) { return pos + encodedLength; }, }; } const OcaUint16 = createType({ isConstantLength: true, canEncode: function (value) { return typeof value === 'number'; }, encodedLength: function (value) { return 2; }, encodeTo: function (dataView, pos, value) { dataView.setUint16(pos, 0 | value, false); return pos + 2; }, decode: function (dataView, pos) { return dataView.getUint16(pos, false); }, }); function Struct(Types, DataType) { const countTypes = Object.keys(Types).length; if (!DataType) { DataType = class { constructor(...args) { if (args.length === countTypes) { let i = 0; for (const name in Types) { if (!Object.prototype.hasOwnProperty.call(Types, name)) continue; this[name] = args[i++]; } } else if (args.length === 1 && typeof args[0] === 'object') { const o = args[0]; for (const name in Types) { if (!Object.prototype.hasOwnProperty.call(Types, name)) continue; this[name] = o[name]; } } else throw new TypeError('Unexpected arguments.'); } }; } return createType({ type: DataType, isConstantLength: false, canEncode: function (value) { if (typeof value !== 'object') return false; for (const name in Types) { if (!Object.prototype.hasOwnProperty.call(Types, name)) continue; const Type = Types[name]; if (!Type.canEncode(value[name])) return false; } return true; }, encodedLength: function (value) { let result = 0; for (const name in Types) { if (!Object.prototype.hasOwnProperty.call(Types, name)) continue; const Type = Types[name]; result += Type.encodedLength(value[name]); } return result; }, encodeTo: function (dataView, pos, value) { for (const name in Types) { if (!Object.prototype.hasOwnProperty.call(Types, name)) continue; const Type = Types[name]; pos = Type.encodeTo(dataView, pos, value[name]); } return pos; }, decodeFrom: function (dataView, pos) { const args = new Array(Types.length); let i = 0; for (const name in Types) { if (!Object.prototype.hasOwnProperty.call(Types, name)) continue; const Type = Types[name]; let tmp; [pos, tmp] = Type.decodeFrom(dataView, pos); args[i++] = tmp; } return [pos, new DataType(...args)]; }, decodeLength: function (dataView, pos) { for (const name in Types) { if (!Object.prototype.hasOwnProperty.call(Types, name)) continue; const Type = Types[name]; pos = Type.decodeLength(dataView, pos); } return pos; }, }); } /* * This file has been generated. */ let OcaEventID$1 = class OcaEventID { /** * Representation of an OCA event ID. A class may define at most 255 events of * its own. Additional events may be inherited, so the total number may exceed * 255. * @class OcaEventID */ constructor(DefLevel, EventIndex) { /** * Level in tree of class which defines this event (1=root) * @type number */ this.DefLevel = DefLevel; /** * Index of the event (in the class description). * @type number */ this.EventIndex = EventIndex; } }; /* * This file has been generated. */ const OcaEventID = Struct( { DefLevel: OcaUint16, EventIndex: OcaUint16, }, OcaEventID$1 ); const OcaUint32 = createType({ isConstantLength: true, canEncode: function (value) { return typeof value === 'number'; }, encodedLength: function (value) { return 4; }, encodeTo: function (dataView, pos, value) { dataView.setUint32(pos, value, false); return pos + 4; }, decode: function (dataView, pos) { return dataView.getUint32(pos, false); }, }); /* * This file has been generated. */ let OcaEvent$1 = class OcaEvent { /** * Representation of an OCA event, i.e. the unique combination of emitter ONo * and the EventID. * @class OcaEvent */ constructor(EmitterONo, EventID) { /** * Object number of the emitter. * @type number */ this.EmitterONo = EmitterONo; /** * Event ID of the subscribed event. * @type OcaEventID */ this.EventID = EventID; } }; /* * This file has been generated. */ const OcaEvent = Struct( { EmitterONo: OcaUint32, EventID: OcaEventID, }, OcaEvent$1 ); /** * Notification packet. */ class Notification extends PDU { constructor( target, method_level, method_index, context, event, param_count, parameters ) { super(); this.target = target; this.method_level = method_level | 0; this.method_index = method_index | 0; this.context = context; this.event = event; this.param_count = param_count | 0; this.parameters = parameters || null; } static get messageType() { return 2; } encode_to(dst, pos) { dst.setUint32(pos, this.encoded_length()); pos += 4; dst.setUint32(pos, this.target); pos += 4; dst.setUint16(pos, this.method_level); pos += 2; dst.setUint16(pos, this.method_index); pos += 2; dst.setUint8(pos, this.param_count); pos++; const context = this.context; if (context) { const len = context.byteLength; dst.setUint16(pos, len); pos += 2; if (len > 0) { new Uint8Array(dst.buffer).set( new Uint8Array(this.context), dst.byteOffset + pos ); pos += len; } } else { dst.setUint16(pos, 0); pos += 2; } dst.setUint32(pos, this.event.EmitterONo); pos += 4; dst.setUint16(pos, this.event.EventID.DefLevel); pos += 2; dst.setUint16(pos, this.event.EventID.EventIndex); pos += 2; if (this.param_count > 1) { if (this.parameters instanceof EncodedArguments) { pos = this.parameters.encodeTo(dst, pos); } else { new Uint8Array(dst.buffer).set( new Uint8Array(this.parameters), dst.byteOffset + pos ); pos += this.parameters.byteLength; } } return pos; } encoded_length() { return ( 23 + (this.param_count > 1 ? this.parameters.byteLength : 0) + (this.context ? this.context.byteLength : 0) ); } decode_from(data, pos, data_len) { let len = data.getUint32(pos); pos += 4; this.target = data.getUint32(pos); pos += 4; this.method_level = data.getUint16(pos); pos += 2; this.method_index = data.getUint16(pos); pos += 2; this.param_count = data.getUint8(pos); pos++; const context_length = data.getUint16(pos); pos += 2; if (context_length) { this.context = data.buffer.slice( data.byteOffset + pos, data.byteOffset + pos + context_length ); pos += context_length; } else { this.context = null; } let event; [pos, event] = OcaEvent.decodeFrom(data, pos); this.event = event; len -= 23 + context_length; if (len < 0) throw new Error('Bad Notification Length.'); if (len > 0) { this.parameters = data.buffer.slice( data.byteOffset + pos, data.byteOffset + pos + len ); pos += len; } return pos; } } /** * Response packet. */ let Response$1 = class Response extends PDU { constructor(handle, status_code, param_count, parameters) { super(); this.handle = handle; this.status_code = status_code | 0; this.param_count = param_count | 0; this.parameters = parameters || null; } static get messageType() { return 3; } encoded_length() { return 10 + (this.param_count ? this.parameters.byteLength : 0); } decode_from(data, pos, data_len) { let len = data.getUint32(pos); pos += 4; this.handle = data.getUint32(pos); pos += 4; this.status_code = data.getUint8(pos); pos++; this.param_count = data.getUint8(pos); pos++; len -= 10; if (len < 0) throw new Error('Bad Response length.'); if (len > 0) { if (!this.param_count) throw new Error( 'Decoding response with parameterCount=0 but %o bytes of parameters', len ); this.parameters = data.buffer.slice( data.byteOffset + pos, data.byteOffset + pos + len ); pos += len; } return pos; } encode_to(dst, pos) { dst.setUint32(pos, this.encoded_length()); pos += 4; dst.setUint32(pos, this.handle); pos += 4; dst.setUint8(pos, this.status_code); pos++; dst.setUint8(pos, this.param_count); pos++; if (this.param_count) { if (this.parameters instanceof EncodedArguments) { pos = this.parameters.encodeTo(dst, pos); } else { new Uint8Array(dst.buffer).set( new Uint8Array(this.parameters), dst.byteOffset + pos ); pos += this.parameters.byteLength; } } return pos; } }; /** * Keepalive packet. */ class KeepAlive extends PDU { static get messageType() { return 4; } constructor(time) { super(); this.time = time || 0; } decode_from(data, pos, len) { if (len == 4) { this.time = data.getUint32(pos); pos += 4; } else if (len == 2) { this.time = data.getUint16(pos) * 1000; pos += 2; } else throw new Error('Bad keepalive timeout length.'); return pos; } encode_to(dst, pos) { if (this.time % 1000) { dst.setUint32(pos, this.time); pos += 4; } else { dst.setUint16(pos, this.time / 1000); pos += 2; } return pos; } encoded_length() { if (this.time % 1000) { return 4; } else { return 2; } } } /** * Notification packet. */ class Notification2 extends PDU { constructor(event, exception, data) { super(); this.event = event; this.exception = !!exception; this.parameters = data || null; } static get messageType() { return 5; } encode_to(dst, pos) { dst.setUint32(pos, this.encoded_length()); pos += 4; dst.setUint32(pos, this.event.EmitterONo); pos += 4; dst.setUint16(pos, this.event.EventID.DefLevel); pos += 2; dst.setUint16(pos, this.event.EventID.EventIndex); pos += 2; dst.setUint8(pos, this.exception ? 1 : 0); pos += 1; if (this.parameters) { if (this.parameters instanceof EncodedArguments) { pos = this.parameters.encodeTo(dst, pos); } else { new Uint8Array(dst.buffer).set( new Uint8Array(this.parameters), dst.byteOffset + pos ); pos += this.parameters.byteLength; } } return pos; } encoded_length() { return 13 + (this.parameters ? this.parameters.byteLength : 0); } decode_from(data, pos, data_len) { let len = data.getUint32(pos); pos += 4; let event; [pos, event] = OcaEvent.decodeFrom(data, pos); this.event = event; this.exception = data.getUint8(pos++) ? true : false; len -= 13; if (len < 0) throw new Error('Bad Notification Length.'); if (len > 0) { this.parameters = data.buffer.slice( data.byteOffset + pos, data.byteOffset + pos + len ); pos += len; } return pos; } } const PDUTypes = [ Command, CommandRrq, Notification, Response$1, KeepAlive, Notification2, ]; function decodeMessage(data, pos, ret) { if (data.byteLength < data.byteOffset + pos + 10) return -1; pos = pos | 0; if (data.getUint8(pos) != 0x3b) throw new Error('Bad sync value.'); pos++; //const protocolVersion = data.getUint16(pos); pos += 2; const messageSize = data.getUint32(pos); pos += 4; const messageType = data.getUint8(pos); pos++; const messageCount = data.getUint16(pos); pos += 2; // this is one index after this message const message_offset = data.byteOffset + pos - 9 + messageSize; if (message_offset > data.byteLength) return -1; ret.length = messageCount; const PDUType = PDUTypes[messageType]; if (PDUType === void 0) throw new Error('Bad Message Type'); if (PDUType === KeepAlive && messageCount !== 1) throw new Error('Bad KeepAlive message count.'); for (let i = 0; i < messageCount; i++) { ret[i] = new PDUType(); pos = ret[i].decode_from(data, pos, message_offset - data.byteOffset - pos); } if (pos != message_offset) throw new Error('Decode error: ' + pos + ' vs ' + message_offset); return pos; } const PROTOCOL_VERSION_2024 = 4; const messageHeaderSize = 10; function calculateMessageLength(pdus) { let len = messageHeaderSize; const type = pdus[0].messageType; for (let i = 0; i < pdus.length; i++) { const pdu = pdus[i]; if (pdu.messageType != type) throw new Error('Cannot combine different types in one message.'); len += pdu.encoded_length(); } return len; } const currentProtocolVersion = PROTOCOL_VERSION_2024; function encodeMessageTo(dst, pos, pdus, offset, end) { if (!offset) offset = 0; if (!end) end = pdus.length; const count = end - offset; if (!(count <= 0xffff)) throw new Error('Too many PDUs.'); dst.setUint8(pos, 0x3b); pos += 1; const startPos = pos; dst.setUint16(pos, currentProtocolVersion); pos += 2; const lenPos = pos; pos += 4; dst.setUint8(pos, pdus[offset].messageType); pos++; dst.setUint16(pos, end - offset); pos += 2; for (let i = offset; i < end; i++) { pos = pdus[i].encode_to(dst, pos); } dst.setUint32(lenPos, pos - startPos); return pos; } function encodeMessage(a) { if (!Array.isArray(a)) a = [a]; const len = calculateMessageLength(a); const buf = new ArrayBuffer(len); const dst = new DataView(buf); const pos = encodeMessageTo(dst, 0, a, 0, a.length); if (pos != len) throw new Error('Message length mismatch.'); return buf; } const pduTypeKeepAlive = 4; class MessageGenerator { constructor(batchSize, resultCallback) { if (!(batchSize <= 0xffffffff)) throw new TypeError('Invalid batch size.'); this._pdus = []; this._batchSize = batchSize; this._resultCallback = resultCallback; this._currentSize = 0; this._currentCount = 0; this._lastMessageType = -1; this._flushScheduled = false; this._flushCb = () => { this._flushScheduled = false; if (this._pdus === null) return; this.flush(); }; } get bufferedAmount() { return this._currentSize; } get batchSize() { return this._batchSize; } add(pdu) { const currentSize = this._currentSize; const encodedLength = pdu.encoded_length(); const messageType = pdu.messageType; // Can we add to the current message? const combine = this._lastMessageType === messageType && messageType !== pduTypeKeepAlive && this._currentCount < 0xffff; let additionalSize = encodedLength; if (!combine) additionalSize += messageHeaderSize; // The resulting buffer would become too large, we flush now. if (currentSize && currentSize + additionalSize > this._batchSize) { this.flush(); additionalSize = encodedLength + messageHeaderSize; } this._pdus.push(pdu); this._currentSize += additionalSize; if (combine) { this._currentCount++; } else { this._currentCount = 1; } /* Keepalive packets are never combined into one message. */ this._lastMessageType = messageType; if (this._currentSize + additionalSize > this._batchSize) { this.flush(); } else if (this._pdus.length === 1) { this.scheduleFlush(); } } scheduleFlush() { if (this._flushScheduled) return; this._flushScheduled = true; Promise.resolve() .then(this._flushCb) .catch((err) => { console.error(err); }); } flush() { if (!this._currentSize) return; const pdus = this._pdus; const buf = new ArrayBuffer(this._currentSize); const dst = new DataView(buf); const length = pdus.length; for (let i = 0, from = 0, pos = 0; i < length; i++) { const pdu = pdus[i]; const messageType = pdu.messageType; if ( i === length - 1 || i + 1 - from === 0xffff || messageType === pduTypeKeepAlive || messageType !== pdus[i + 1].messageType ) { pos = encodeMessageTo(dst, pos, pdus, from, i + 1); from = i + 1; } } this._currentSize = 0; this._lastMessageType = -1; this._currentCount = 0; this._pdus.length = 0; this._resultCallback(buf); } dispose() { this._pdus = null; } } /** * Error class raised when a connection is closed due to a timeout. */ class TimeoutError extends Error { constructor(error) { super(`Connection has timed out.`); this.name = 'aes70.TimeoutError'; } } function isItTime(target, now) { // We are ok with 1ms accuracy. return target - now < 1; } class Timer { constructor(callback, getNow) { this._callback = callback; this._getNow = getNow; this._targetTime = undefined; this._timerId = undefined; this._timerAt = undefined; } poll() { const now = this._getNow(); if (this._targetTime === undefined) return; if (isItTime(this._targetTime, now)) { this._targetTime = undefined; try { this._callback(); } catch (err) { console.error('Timer callback threw an exception', err); } } else { this._reschedule(); } } _reschedule() { const target = this._targetTime; const interval = target - this._getNow(); if (this._timerId !== undefined) { if (target >= this._timerAt) { // The timer will fire before target. We will then reschedule it. return; } clearTimeout(this._timerId); this._timerId = undefined; } this._timerAt = target; this._timerId = setTimeout(() => { this._timerId = undefined; this._timerAt = undefined; this.poll(); }, Math.max(0, interval)); } /** * * @param {number} interval * Interval in milliseconds. */ scheduleIn(interval) { if (!(interval >= 0)) { throw new TypeError(`Expected positive interval.`); } this._targetTime = this._getNow() + interval; this._reschedule(); } /** * Schedule the timer in a given number of milliseconds. If the timer * is already running and scheduled to run before, do not modify it. * * @param {number} interval */ scheduleDeadlineIn(interval) { if (!(interval >= 0)) { throw new TypeError(`Expected positive interval.`); } const target = this._getNow() + interval; if (this._targetTime !== undefined && this._targetTime <= target) { this.poll(); return; } this.scheduleAt(target); } /** * * @param {number} target * Target time in milliseconds. */ scheduleAt(target) { if (!(target >= 0)) { throw new TypeError(); } this._targetTime = target; this._reschedule(); } stop() { this._targetTime = undefined; } cancel() { this.stop(); this._clearTimeout(); } _clearTimeout() { if (this._timerId) { clearTimeout(this._timerId); this._timerId = undefined; this._timerAt = undefined; } } dispose() { this.cancel(); } } /** * Connection base class. It extends :class:`Events` and defines two events: * * - ``close`` is emitted when this connection has been closed. This event has * no parameters. * - ``error`` is emitted when this connection has been closed with an error. * This event emits the error object as a single parameter. * - ``send`` is emitted with each pdu that is sent. * - ``receive`` is emitted with each pdu that is received. * * @param {object} options * The options. * @param {number} [options.batch=65536] * AES70 messages are batched into single write calls up until this limit. * This can improve network performance and reduce packet overhead when * many small commands are send at the same time. This often happens e.g. * after initially connecting to a device and the device tree is * enumerated. Defaults to 64 kilobytes by default but is overwritten e.g. * by :class:`UDPConnection`. */ class Connection extends Events { constructor(options) { if (!options) options = {}; super(); const now = this._now(); this.options = options; const batchSize = options.batch >= 0 ? options.batch : 64 * 1024; this._message_generator = new MessageGenerator(batchSize, (buf) => this.write(buf) ); this.inbuf = null; this.inpos = 0; this.last_rx_time = now; this.last_tx_time = now; this.rx_bytes = 0; this.tx_bytes = 0; this._keepalive_timer = new Timer( () => this._check_keepalive(), () => this._now() ); this.keepalive_interval = -1; this._closed = false; this.on('close', () => { if (this._closed) return; this._closed = true; this.cleanup(); }); this.on('error', (e) => { if (this._closed) return; this._closed = true; this.emit('close'); this.cleanup(e); }); } get is_reliable() { return true; } get bufferedAmount() { return this._message_generator.bufferedAmount; } get batchSize() { return this._message_generator.batchSize; } get pendingWrites() { return this._message_generator.bufferedAmount > 0 ? 1 : 0; } send(pdu) { if (this.is_closed()) throw new Error('Connection is closed.'); this.emit('send', pdu); this._message_generator.add(pdu); } tx_idle_time() { return this._now() - this.last_tx_time; } rx_idle_time() { return this._now() - this.last_rx_time; } read(buf) { this.rx_bytes += buf.byteLength; this.last_rx_time = this._now(); if (this.inbuf) { const len = this.inbuf.byteLength - this.inpos; const tmp = new Uint8Array(new ArrayBuffer(len + buf.byteLength)); tmp.set(new Uint8Array(this.inbuf, this.inpos)); tmp.set(new Uint8Array(buf), len); this.inbuf = null; this.inpos = 0; buf = tmp.buffer; } let pos = 0; const view = new DataView(buf); try { do { const ret = []; const len = decodeMessage(view, pos, ret); if (len == -1) { this.inbuf = buf; this.inpos = pos; break; } pos = len; this.incoming(ret); } while (pos < buf.byteLength); } catch (e) { // If this is a reliably connection we close it. If not, // we print decoding errors and throw away the packet. if (this.is_reliable) { this.emit('error', e); return; } else { console.error(e); } } this.poll(); } incoming(a) {} write(buf) { this.last_tx_time = this._now(); this.tx_bytes += buf.byteLength; } is_closed() { return this._message_generator === null; } /** * Closes the connection. Overloaded by connection subclasses. */ close() { if (this.is_closed()) return; this.emit('close'); } error(err) { if (this.is_closed()) return; this.emit('error', err); } cleanup(error) { if (this.is_closed()) throw new Error('cleanup() called twice.'); // disable keepalive this._keepalive_timer.dispose(); this._message_generator.dispose(); this._message_generator = null; this.removeAllEventListeners(); } _check_keepalive() { if (this.is_closed()) return; const t = this.keepalive_interval; if (!(t > 0)) return; this._keepalive_timer.scheduleIn(t / 2 + 10); if (this.rx_idle_time() > t * 3) { this.emit('timeout'); this.error(new TimeoutError()); } else if (this.tx_idle_time() > t * 0.75) { /* Try to flush buffers before actually sending out anything. */ this.flush(); if (this.tx_idle_time() > t * 0.75) this.send(new KeepAlive(t)); } } /** * Check if some regular internal timers must run. */ poll() { this._keepalive_timer.poll(); } /** * Flush write buffers. This are usually PDUs or may also be unwritten * buffers. */ flush() { this._message_generator.flush(); } /** * Set the keepalive interval. Setting the keepalive interval to a * positive number ``N`` will make sure to send some packet (possibly a * keepalive command) at ``N`` seconds. * * @param {number} seconds * Keepalive interval in seconds. */ set_keepalive_interval(seconds) { if (!(seconds <= 10)) { console.warn( 'Unusually large keepalive interval %o seconds. Confusion of ms vs. seconds?' ); } const t = seconds * 1000; this.keepalive_interval = t; // Notify the other side about our new keepalive if (this.is_closed()) return; this.send(new KeepAlive(t)); if (t > 0) { // we check twice as often to make sure we stay within the timers this._keepalive_timer.scheduleIn(t / 2 + 10); } else { this._keepalive_timer.stop(); } } } /** * Error class raised by remote function calls. * * @property {Command} cmd - The command object. * @property {OcaStatus} status - The error code. */ class RemoteError extends Error { constructor(status, cmd) { super(`Call failed with OcaStatus ${status.name}`); this.name = 'aes70.RemoteError'; this.status = status; this.cmd = cmd; } static check_status(error, status) { return error instanceof this && error.status === status; } } /** * Class used to represent multiple return values. */ class Arguments { constructor(values) { this.values = values; } /** * Returns an item. * @param {integer} n - Index of the item. */ item(n) { return this.values[n]; } /** * The number of elements. */ get length() { return this.values.length; } [Symbol.iterator]() { return this.values[Symbol.iterator](); } } function hasOwnProperty(o, name) { return Object.prototype.hasOwnProperty.call(o, name); } /** * An interface implemented by all AES70 enum types. Each AES70 enum is * implemented by a class which implements this interface. Enum values are * implemented using singletons of this corresponding class. * * @interface Enum */ function Enum$1(values) { let names = null; function getName(value) { if (names === null) { names = new Map(); for (const name in values) { if (!hasOwnProperty(values, name)) continue; names.set(values[name], name); } } return names.get(value); } function getValue(name) { if (hasOwnProperty(values, name)) return values[name]; } let blueprints = null; function setBlueprint(value, o) { if (blueprints === null) { blueprints = new Map(); } blueprints.set(value, o); } const result = class { get isEnum() { return true; } constructor(value) { if (typeof value === 'string') { if (!hasOwnProperty(values, value)) throw new Error('No such enum value.'); return this.constructor[value]; } if (blueprints !== null && blueprints.has(value)) { return blueprints.get(value); } this.value = value; setBlueprint(value, this); } get name() { return getName(this.value); } /** * @function Enum#valueOf * @returns {number} The numeric enum value. */ valueOf() { return this.value; } /** * @function Enum#toString * @returns {string} The enum entry name. */ toString() { return this.name; } static getName(value) { const name = getName(value); if (name === void 0) throw new Error('No such enum value.'); return name; } static hasValue(value) { return getName(value) !== void 0; } static getValue(name) { const value = getValue(name); if (value === void 0) throw new Error('No such enum value.'); return value; } static hasName(name) { return getValue(name) !== void 0; } static values() { return values; } }; for (const name in values) { if (!hasOwnProperty(values, name)) continue; Object.defineProperty(result, name, { get: function () { return new this(values[name]); }, enumerable: false, configurable: true, }); } return result; } /* * This file has been generated. */ /** * Standard status codes returned from method calls. * @class OcaStatus */ let OcaStatus$1 = class OcaStatus extends Enum$1({ OK: 0, ProtocolVersionError: 1, DeviceError: 2, Locked: 3, BadFormat: 4, BadONo: 5, ParameterError: 6, ParameterOutOfRange: 7, NotImplemented: 8, InvalidRequest: 9, ProcessingFailed: 10, BadMethod: 11, PartiallySucceeded: 12, Timeout: 13, BufferOverflow: 14, PermissionDenied: 15, OutOfMemory: 16, Busy: 17, }) {}; /** * Singleton object corresponding to the entry with value ``0``. * @type {OcaStatus} * @member OK * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``1``. * @type {OcaStatus} * @member ProtocolVersionError * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``2``. * @type {OcaStatus} * @member DeviceError * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``3``. * @type {OcaStatus} * @member Locked * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``4``. * @type {OcaStatus} * @member BadFormat * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``5``. * @type {OcaStatus} * @member BadONo * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``6``. * @type {OcaStatus} * @member ParameterError * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``7``. * @type {OcaStatus} * @member ParameterOutOfRange * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``8``. * @type {OcaStatus} * @member NotImplemented * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``9``. * @type {OcaStatus} * @member InvalidRequest * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``10``. * @type {OcaStatus} * @member ProcessingFailed * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``11``. * @type {OcaStatus} * @member BadMethod * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``12``. * @type {OcaStatus} * @member PartiallySucceeded * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``13``. * @type {OcaStatus} * @member Timeout * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``14``. * @type {OcaStatus} * @member BufferOverflow * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``15``. * @type {OcaStatus} * @member PermissionDenied * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``16``. * @type {OcaStatus} * @member OutOfMemory * @memberof OcaStatus * @static */ /** * Singleton object corresponding to the entry with value ``17``. * @type {OcaStatus} * @member Busy * @memberof OcaStatus * @static */ /** * Error class raised when a connection is closed. * * @property {Error} [error] - The actual failure reason. May be undefined, * for instance if the connection was closed using close(). */ class CloseError extends Error { constructor(error) { super(`Connection has been closed.`); this.name = 'aes70.CloseError'; this.error = error; } } class Subscriptions { constructor() { this._callbacks = []; } /** * Add a subscription. * * @param {Function[]} cbs */ add(...cbs) { cbs.forEach((cb) => { this._callbacks.push(cb); }); } unsubscribe() { this._callbacks.forEach((cb) => { try { cb(); } catch (err) { console.error(err); } }); this._callbacks.length = 0; } } function addEvent(target, name, callback) { if (target.addEventListener) { target.addEventListener(name, callback); } else if (target.on) { target.on(name, callback); } else { throw new TypeError('Unsupported event target ', target); } } function removeEvent(target, name, callback) { if (target.removeEventListener) { target.removeEventListener(name, callback); } else if (target.off) { target.off(name, callback); } else { throw new TypeError('Unsupported event target ', target); } } /** * * @param {EventTarget|Events} target * @param {string} name * @param {callback} callback */ function subscribeEvent(target, name, callback) { addEvent(target, name, callback); return () => { removeEvent(target, name, callback); }; } class PendingCommand { get handle() { return this.command.handle; } constructor(resolve, reject, returnTypes, command, stack, name) { this.resolve = resolve; this.reject = reject; this.returnTypes = returnTypes; this.command = command; this.stack = stack; this.name = name; this.lastSent = 0; this.retries = 0; } get_arguments() { const parameters = this.command.parameters; if (parameters && parameters instanceof EncodedArguments) { return parameters.data; } } handleError(error) { if (this.stack && error instanceof Error) error.stack = this.stack; this.reject(error); } response(o) { const { resolve, reject, returnTypes, command } = this; if (o.status_code !== 0) { const error = new RemoteError(new OcaStatus$1(o.status_code), command); this.handleError(error); } else if (!returnTypes) { resolve(o); } else { try { const length = Math.min(o.param_count, returnTypes.length); if (length === 0) { resolve(); } else { const result = new Array(length); const dataView = new DataView(o.parameters); for (let i = 0, pos = 0; i < length; i++) { let tmp; [pos, tmp] = returnTypes[i].decodeFrom(dataView, pos); result[i] = tmp; } resolve(length === 1 ? result[0] : new Arguments(result)); } } catch (err) { reject(err); } } } } function eventToKey$1(event) { const ono = event.EmitterONo; const id = event.EventID; return [ono, id.DefLevel, id.EventIndex].join(','); } /** * Connection base class for clients (aka controllers). * * @param {object} options * Additional options are passed to :class:`Connection`. */ class ClientConnection extends Connection { constructor(options) { super(options); // All pending commands by id/handle this._pendingCommands = new Map(); // All pending commands scheduled to be sent. this._scheduledPendingCommands = new Set(); // All pending commands wich have been sent. this._sentPendingCommands = new Set(); this._nextCommandHandle = 0; this._subscribers = new Map(); this._sendCommandsTimer = new Timer( () => { this.sendCommands(); }, () => this._now() ); } shouldSendMoreCommands() { return this.is_reliable; } sendCommands() { const { _scheduledPendingCommands, _sentPendingCommands } = this; for (const pendingCommand of _scheduledPendingCommands) { if (!this.shouldSendMoreCommands()) break; _scheduledPendingCommands.delete(pendingCommand); _sentPendingCommands.add(pendingCommand); this.send(pendingCommand.command); pendingCommand.lastSent = this._now(); pendingCommand.retries++; } } scheduleSendCommands() { this._sendCommandsTimer.scheduleDeadlineIn(5); } poll() { super.poll(); this._sendCommandsTimer.poll(); } cleanup(error) { super.cleanup(error); this._sendCommandsTimer.dispose(); const subscribers = this._subscribers; this._subscribers = null; const pendingCommands = this._pendingCommands; this._pendingCommands = null; this._scheduledPendingCommands.clear(); this._sentPendingCommands.clear(); const e = new CloseError(error); pendingCommands.forEach((pendingCommand, id) => { pendingCommand.handleError(structuredClone(e)); }); subscribers.forEach((cb) => { cb(false, e); }); } _addSubscriber(event, callback) { const key = eventToKey$1(event); const subscribers = this._subscribers; if (subscribers.has(key)) throw new Error('Subscriber already exists.'); subscribers.set(key, callback); } _removeSubscriber(event) { if (this.is_closed()) return; const key = eventToKey$1(event); const subscribers = this._subscribers; if (!subscribers.has(key)) throw new Error('Unknown subscriber.'); subscribers.delete(key); } _getNextCommandHandle() { let handle; const pendingCommands = this._pendingCommands; if (pendingCommands === null) { throw new Error('Connection not open.'); } do { handle = this._nextCommandHandle; this._nextCommandHandle = (handle + 1) | 0; } while (pendingCommands.has(handle)); return handle; } _estimate_next_tx_time() { return this._now(); } find_pending_command(pdu) { const pendingCommands = this._pendingCommands; if (pdu instanceof CommandRrq) { return pendingCommands.get(pdu.handle); } else if (pdu instanceof Response$1) { return pendingCommands.get(pdu.handle); } else { throw new Error(`Expected command or response.`); } } send_command(command, returnTypes, callback, stack, name) { const executor = (resolve, reject) => { const handle = this._getNextCommandHandle(); command.handle = handle; const pendingCommand = new PendingCommand( resolve, reject, returnTypes, command, stack, name ); this._pendingCommands.set(handle, pendingCommand); this._scheduledPendingCommands.add(pendingCommand); this.scheduleSendCommands(); }; if (callback) { executor( (result) => callback(true, result), (error) => callback(false, error) ); } else { return new Promise(executor); } } _removePendingCommand(handle) { const pendingCommands = this._pendingCommands; const pendingCommand = pendingCommands.get(handle); if (!pendingCommand) return null; pendingCommands.delete(handle); if (!this._sentPendingCommands.delete(pendingCommand)) this._scheduledPendingCommands.delete(pendingCommand); return pendingCommand; } incoming(pdus) { for (let i = 0; i < pdus.length; i++) { // Connection may have been closed while receiving a command if (this._pendingCommands === null) { return; } const o = pdus[i]; this.emit('receive', o); if (o instanceof Response$1) { const pendingCommand = this._removePendingCommand(o.handle); if (pendingCommand === null) { if (this.is_reliable) { this.error(new Error('Unknown handle.')); return; } else { continue; } } pendingCo