aes70
Version:
A controller library for the AES70 protocol.
2,061 lines (1,762 loc) • 920 kB
JavaScript
(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