WebSockets: improve JS client and demo

add methods to callback.js
automatically reconnect js client on disconnection
mixer-demo do not recreate UI on reconnection
NO-OP: indentation in message.js
make client JS reconnection optional
fix mixer-demo scrolling
minor JS client refactor
improve mixer-demo readability
This commit is contained in:
Luciano Iam 2020-04-14 22:58:44 +02:00 committed by Robin Gareus
parent 612c71aa25
commit 50ba8dea96
No known key found for this signature in database
GPG key ID: A090BCE02CF57F04
9 changed files with 191 additions and 145 deletions

View file

@ -29,13 +29,13 @@ using namespace ARDOUR;
#define NODE_METHOD_PAIR(x) (Node::x, &WebsocketsDispatcher::x##_handler) #define NODE_METHOD_PAIR(x) (Node::x, &WebsocketsDispatcher::x##_handler)
WebsocketsDispatcher::NodeMethodMap WebsocketsDispatcher::NodeMethodMap
WebsocketsDispatcher::_node_to_method = boost::assign::map_list_of WebsocketsDispatcher::_node_to_method = boost::assign::map_list_of
NODE_METHOD_PAIR (tempo) NODE_METHOD_PAIR (tempo)
NODE_METHOD_PAIR (strip_gain) NODE_METHOD_PAIR (strip_gain)
NODE_METHOD_PAIR (strip_pan) NODE_METHOD_PAIR (strip_pan)
NODE_METHOD_PAIR (strip_mute) NODE_METHOD_PAIR (strip_mute)
NODE_METHOD_PAIR (strip_plugin_enable) NODE_METHOD_PAIR (strip_plugin_enable)
NODE_METHOD_PAIR (strip_plugin_param_value); NODE_METHOD_PAIR (strip_plugin_param_value);
void void
WebsocketsDispatcher::dispatch (Client client, const NodeStateMessage& msg) WebsocketsDispatcher::dispatch (Client client, const NodeStateMessage& msg)
@ -105,7 +105,6 @@ WebsocketsDispatcher::update_all_nodes (Client client)
val.push_back (std::string ("i")); val.push_back (std::string ("i"));
val.push_back (pd.lower); val.push_back (pd.lower);
val.push_back (pd.upper); val.push_back (pd.upper);
val.push_back (pd.integer_step);
} else { } else {
val.push_back (std::string ("d")); val.push_back (std::string ("d"));
val.push_back (pd.lower); val.push_back (pd.lower);

View file

@ -92,10 +92,10 @@ WebsocketsServer::WebsocketsServer (ArdourSurface::ArdourWebsockets& surface)
#if LWS_LIBRARY_VERSION_MAJOR < 3 #if LWS_LIBRARY_VERSION_MAJOR < 3
/* older libwebsockets does not define mime type for svg files */ /* older libwebsockets does not define mime type for svg files */
memset (&_lws_vhost_opt, 0, sizeof (lws_protocol_vhost_options)); memset (&_lws_vhost_opt, 0, sizeof (lws_protocol_vhost_options));
_lws_vhost_opt.name = ".svg"; _lws_vhost_opt.name = ".svg";
_lws_vhost_opt.value = "image/svg+xml"; _lws_vhost_opt.value = "image/svg+xml";
_lws_mnt_index.extra_mimetypes = &_lws_vhost_opt; _lws_mnt_index.extra_mimetypes = &_lws_vhost_opt;
_lws_mnt_user.extra_mimetypes = &_lws_vhost_opt; _lws_mnt_user.extra_mimetypes = &_lws_vhost_opt;
#endif #endif
} }

View file

@ -26,6 +26,7 @@ div {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
box-shadow: 0px 0px 10px #000; box-shadow: 0px 0px 10px #000;
} }
@ -76,6 +77,10 @@ div {
color: rgb(172,128,255); color: rgb(172,128,255);
} }
.info {
color: rgb(99,208,230);
}
.error { .error {
color: rgb(249,36,114); color: rgb(249,36,114);
} }

View file

@ -17,8 +17,7 @@
*/ */
// This example does not call the API methods in ardour.js, // This example does not call the API methods in ardour.js,
// instead it interacts at a lower level by coupling the widgets // instead it couples the widgets directly to the message stream
// tightly to the message stream
import { ANode, Message } from '/shared/message.js'; import { ANode, Message } from '/shared/message.js';
import { ArdourClient } from '/shared/ardour.js'; import { ArdourClient } from '/shared/ardour.js';
@ -29,8 +28,6 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
(() => { (() => {
const MAX_LOG_LINES = 1000; const MAX_LOG_LINES = 1000;
const FEEDBACK_NODES = [ANode.STRIP_GAIN, ANode.STRIP_PAN, ANode.STRIP_METER,
ANode.STRIP_PLUGIN_ENABLE, ANode.STRIP_PLUGIN_PARAM_VALUE];
const ardour = new ArdourClient(location.host); const ardour = new ArdourClient(location.host);
const widgets = {}; const widgets = {};
@ -43,101 +40,95 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
div.innerHTML = `${manifest.name.toUpperCase()} v${manifest.version}${manifest.description}`; div.innerHTML = `${manifest.name.toUpperCase()} v${manifest.version}${manifest.description}`;
}); });
ardour.addCallback({ ardour.addCallbacks({
onMessage: (msg) => { onConnected: (error) => { log('Client connected', 'info'); },
log(`${msg}`, 'message-in'); onDisconnected: (error) => { log('Client disconnected', 'error'); },
onMessage: processMessage,
if (msg.node == ANode.STRIP_DESC) { onStripDesc: createStrip,
createStrip (msg.addr, ...msg.val); onStripPluginDesc: createStripPlugin,
} else if (msg.node == ANode.STRIP_PLUGIN_DESC) { onStripPluginParamDesc: createStripPluginParam
createStripPlugin (msg.addr, ...msg.val);
} else if (msg.node == ANode.STRIP_PLUGIN_PARAM_DESC) {
createStripPluginParam (msg.addr, ...msg.val);
} else if (FEEDBACK_NODES.includes(msg.node)) {
if (widgets[msg.hash]) {
widgets[msg.hash].value = msg.val[0];
}
}
},
onError: () => {
log('Client error', 'error');
}
}); });
ardour.open(); ardour.connect();
} }
function createStrip (addr, name) { function createStrip (stripId, name) {
const id = `strip-${addr[0]}`; const domId = `strip-${stripId}`;
if (document.getElementById(domId) != null) {
return;
}
const strips = document.getElementById('strips'); const strips = document.getElementById('strips');
const div = createElem(`<div class="strip" id="${id}"></div>`, strips); const div = createElem(`<div class="strip" id="${domId}"></div>`, strips);
createElem(`<label class="comp-name" for="${id}">∿&emsp;&emsp;${name}</label>`, div); createElem(`<label class="comp-name" for="${domId}">∿&emsp;&emsp;${name}</label>`, div);
// meter // meter
const meter = new StripMeter(ANode.STRIP_METER, addr); const meter = new StripMeter();
meter.el.classList.add('slider-meter'); meter.el.classList.add('slider-meter');
meter.attach(div); meter.appendTo(div);
register(meter); connectWidget(meter, ANode.STRIP_METER, stripId);
// gain // gain
let holder = createElem(`<div class="strip-slider"></div>`, div); let holder = createElem(`<div class="strip-slider"></div>`, div);
createElem(`<label>Gain</label>`, holder); createElem(`<label>Gain</label>`, holder);
const gain = new StripGainSlider(ANode.STRIP_GAIN, addr); const gain = new StripGainSlider();
gain.attach(holder, (val) => send(gain)); gain.appendTo(holder);
register(gain); connectWidget(gain, ANode.STRIP_GAIN, stripId);
// pan // pan
holder = createElem(`<div class="strip-slider"></div>`, div); holder = createElem(`<div class="strip-slider"></div>`, div);
createElem(`<label>Pan</label>`, holder); createElem(`<label>Pan</label>`, holder);
const pan = new StripPanSlider(ANode.STRIP_PAN, addr); const pan = new StripPanSlider();
pan.attach(holder, (val) => send(pan)); pan.appendTo(holder);
register(pan); connectWidget(pan, ANode.STRIP_PAN, stripId);
} }
function createStripPlugin (addr, name) { function createStripPlugin (stripId, pluginId, name) {
const strip = document.getElementById(`strip-${addr[0]}`); const domId = `plugin-${stripId}-${pluginId}`;
const id = `plugin-${addr[0]}-${addr[1]}`; if (document.getElementById(domId) != null) {
const div = createElem(`<div class="plugin" id="${id}"></div>`, strip); return;
}
const strip = document.getElementById(`strip-${stripId}`);
const div = createElem(`<div class="plugin" id="${domId}"></div>`, strip);
createElem(`<label class="comp-name">⨍&emsp;&emsp;${name}</label>`, div); createElem(`<label class="comp-name">⨍&emsp;&emsp;${name}</label>`, div);
const enable = new Switch(ANode.STRIP_PLUGIN_ENABLE, addr);
const enable = new Switch();
enable.el.classList.add('plugin-enable'); enable.el.classList.add('plugin-enable');
enable.attach(div, (val) => send(enable)); enable.appendTo(div);
register(enable); connectWidget(enable, ANode.STRIP_PLUGIN_ENABLE, stripId, pluginId);
} }
function createStripPluginParam (addr, name, dataType, min, max, isLog) { function createStripPluginParam (stripId, pluginId, paramId, name, valueType, min, max, isLog) {
const domId = `param-${stripId}-${pluginId}-${paramId}`;
if (document.getElementById(domId) != null) {
return;
}
let param, cssClass; let param, cssClass;
if (dataType == 'b') { if (valueType == 'b') {
cssClass = 'boolean'; cssClass = 'boolean';
param = new Switch(ANode.STRIP_PLUGIN_PARAM_VALUE, addr); param = new Switch();
} else if (dataType == 'i') { } else if (valueType == 'i') {
cssClass = 'discrete'; cssClass = 'discrete';
param = new DiscreteSlider(ANode.STRIP_PLUGIN_PARAM_VALUE, addr, min, max); param = new DiscreteSlider(min, max);
} else if (dataType == 'd') { } else if (valueType == 'd') {
cssClass = 'continuous'; cssClass = 'continuous';
if (isLog) { if (isLog) {
param = new LogarithmicSlider(ANode.STRIP_PLUGIN_PARAM_VALUE, addr, min, max); param = new LogarithmicSlider(min, max);
} else { } else {
param = new ContinuousSlider(ANode.STRIP_PLUGIN_PARAM_VALUE, addr, min, max); param = new ContinuousSlider(min, max);
} }
} }
const plugin = document.getElementById(`plugin-${addr[0]}-${addr[1]}`); const plugin = document.getElementById(`plugin-${stripId}-${pluginId}`);
const id = `param-${addr[0]}-${addr[1]}-${addr[2]}`; const div = createElem(`<div class="plugin-param ${cssClass}" id="${domId}"></div>`, plugin);
const div = createElem(`<div class="plugin-param ${cssClass}" id="${id}"></div>`, plugin); createElem(`<label for="${domId}">${name}</label>`, div);
createElem(`<label for="${id}">${name}</label>`, div);
param.attach(div, (val) => send(param)); param.el.name = domId;
param.el.name = id; param.appendTo(div);
register(param); connectWidget(param, ANode.STRIP_PLUGIN_PARAM_VALUE, stripId, pluginId, paramId);
}
function send (widget) {
const msg = new Message(widget.node, widget.addr, [widget.value]);
log(`${msg}`, 'message-out');
ardour.send(msg);
} }
function createElem (html, parent) { function createElem (html, parent) {
@ -153,8 +144,24 @@ import { Switch, DiscreteSlider, ContinuousSlider, LogarithmicSlider,
return elem; return elem;
} }
function register (widget) { function connectWidget (widget, node, ...addr) {
widgets[widget.hash] = widget; const nodeAddrId = Message.nodeAddrId(node, addr);
widgets[nodeAddrId] = widget;
widget.callback = (val) => {
const msg = new Message(node, addr, [val]);
log(`${msg}`, 'message-out');
ardour.send(msg);
};
}
function processMessage (msg) {
log(`${msg}`, 'message-in');
if (widgets[msg.nodeAddrId]) {
widgets[msg.nodeAddrId].value = msg.val[0];
}
} }
function log (message, className) { function log (message, className) {

View file

@ -20,36 +20,26 @@ import { Message } from '/shared/message.js';
export class Widget { export class Widget {
constructor (node, addr, html) { constructor (html) {
this.node = node;
this.addr = addr;
const template = document.createElement('template'); const template = document.createElement('template');
template.innerHTML = html; template.innerHTML = html;
this.el = template.content.firstChild; this.el = template.content.firstChild;
} }
attach (parent, callback) { appendTo (parent) {
parent.appendChild(this.el); parent.appendChild(this.el);
if (callback) {
this.callback = callback;
}
} }
callback (value) { callback (value) {
// do nothing by default // do nothing by default
} }
get hash () {
return Message.hash(this.node, this.addr);
}
} }
export class Switch extends Widget { export class Switch extends Widget {
constructor (node, addr) { constructor () {
super (node, addr, `<input type="checkbox" class="widget-switch">`); super (`<input type="checkbox" class="widget-switch">`);
this.el.addEventListener('input', (ev) => this.callback(this.value)); this.el.addEventListener('input', (ev) => this.callback(this.value));
} }
@ -65,10 +55,10 @@ export class Switch extends Widget {
export class Slider extends Widget { export class Slider extends Widget {
constructor (node, addr, min, max, step) { constructor (min, max, step) {
const html = `<input type="range" class="widget-slider" const html = `<input type="range" class="widget-slider"
min="${min}" max="${max}" step="${step}">`; min="${min}" max="${max}" step="${step}">`;
super(node, addr, html); super(html);
this.min = min; this.min = min;
this.max = max; this.max = max;
this.el.addEventListener('input', (ev) => this.callback(this.value)); this.el.addEventListener('input', (ev) => this.callback(this.value));
@ -86,24 +76,24 @@ export class Slider extends Widget {
export class DiscreteSlider extends Slider { export class DiscreteSlider extends Slider {
constructor (node, addr, min, max) { constructor (min, max, step) {
super(node, addr, min, max, 1); super(min, max, step || 1);
} }
} }
export class ContinuousSlider extends Slider { export class ContinuousSlider extends Slider {
constructor (node, addr, min, max) { constructor (min, max) {
super(node, addr, min, max, 0.001); super(min, max, 0.001);
} }
} }
export class LogarithmicSlider extends ContinuousSlider { export class LogarithmicSlider extends ContinuousSlider {
constructor (node, addr, min, max) { constructor (min, max) {
super(node, addr, 0, 1.0); super(0, 1.0);
this.minVal = Math.log(min); this.minVal = Math.log(min);
this.maxVal = Math.log(max); this.maxVal = Math.log(max);
this.scale = this.maxVal - this.minVal; this.scale = this.maxVal - this.minVal;
@ -121,16 +111,16 @@ export class LogarithmicSlider extends ContinuousSlider {
export class StripPanSlider extends ContinuousSlider { export class StripPanSlider extends ContinuousSlider {
constructor (node, addr) { constructor () {
super(node, addr, -1.0, 1.0); super(-1.0, 1.0);
} }
} }
export class StripGainSlider extends ContinuousSlider { export class StripGainSlider extends ContinuousSlider {
constructor (node, addr) { constructor () {
super(node, addr, 0, 1.0) super(0, 1.0)
this.minVal = -58.0; this.minVal = -58.0;
this.maxVal = 6.0; this.maxVal = 6.0;
this.scale = (this.maxVal - this.minVal); this.scale = (this.maxVal - this.minVal);
@ -148,8 +138,8 @@ export class StripGainSlider extends ContinuousSlider {
export class StripMeter extends Widget { export class StripMeter extends Widget {
constructor (node, addr) { constructor () {
super(node, addr, `<label></label>`); super(`<label></label>`);
} }
set value (val) { set value (val) {

View file

@ -16,17 +16,19 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/ */
import { MetadataMixin } from './metadata.js';
import { ControlMixin } from './control.js'; import { ControlMixin } from './control.js';
import { MetadataMixin } from './metadata.js';
import { Message } from './message.js'; import { Message } from './message.js';
import { MessageChannel } from './channel.js'; import { MessageChannel } from './channel.js';
// See *Mixin for the available APIs // See ControlMixin and MetadataMixin for available APIs
// See ArdourCallback for an example callback implementation
class BaseArdourClient { class BaseArdourClient {
constructor () { constructor () {
this._callbacks = []; this._callbacks = [];
this._connected = false;
this._pendingRequest = null; this._pendingRequest = null;
this._channel = new MessageChannel(location.host); this._channel = new MessageChannel(location.host);
@ -39,21 +41,30 @@ class BaseArdourClient {
}; };
} }
addCallback (callback) { addCallbacks (callbacks) {
this._callbacks.push(callback); this._callbacks.push(callbacks);
} }
async open () { async connect (autoReconnect) {
this._channel.onClose = () => { this._channel.onClose = async () => {
this._fireCallbacks('error', new Error('Message channel unexpectedly closed')); if (this._connected) {
this._fireCallbacks('disconnected');
this._connected = false;
}
if ((autoReconnect == null) || autoReconnect) {
await this._sleep(1000);
await this._connect();
}
}; };
await this._channel.open(); this._connect();
} }
close () { disconnect () {
this._channel.onClose = () => {}; this._channel.onClose = () => {};
this._channel.close(); this._channel.close();
this._connected = false;
} }
send (msg) { send (msg) {
@ -61,6 +72,12 @@ class BaseArdourClient {
} }
// Private methods // Private methods
async _connect () {
await this._channel.open();
this._connected = true;
this._fireCallbacks('connected');
}
_send (node, addr, val) { _send (node, addr, val) {
const msg = new Message(node, addr, val); const msg = new Message(node, addr, val);
@ -75,6 +92,10 @@ class BaseArdourClient {
}); });
} }
async _sendRecvSingle (node, addr, val) {
return await this._sendAndReceive (node, addr, val)[0];
}
_onChannelMessage (msg) { _onChannelMessage (msg) {
if (this._pendingRequest && (this._pendingRequest.hash == msg.hash)) { if (this._pendingRequest && (this._pendingRequest.hash == msg.hash)) {
this._pendingRequest.resolve(msg.val); this._pendingRequest.resolve(msg.val);
@ -91,9 +112,9 @@ class BaseArdourClient {
return s[0].toUpperCase() + s.slice(1).toLowerCase(); return s[0].toUpperCase() + s.slice(1).toLowerCase();
}).join(''); }).join('');
for (const callback of this._callbacks) { for (const callbacks of this._callbacks) {
if (method in callback) { if (method in callbacks) {
callback[method](...args) callbacks[method](...args)
} }
} }
} }
@ -102,15 +123,19 @@ class BaseArdourClient {
return new Error(`HTTP response status ${status}`); return new Error(`HTTP response status ${status}`);
} }
async _sleep (t) {
return new Promise(resolve => setTimeout(resolve, 1000));
}
} }
export class ArdourClient extends mixin(BaseArdourClient, ControlMixin, MetadataMixin) {} export class ArdourClient extends mixin(BaseArdourClient, ControlMixin, MetadataMixin) {}
function mixin (dstClass, ...classes) { function mixin (dstClass, ...classes) {
for (const srcClass of classes) { for (const srcClass of classes) {
for (const methName of Object.getOwnPropertyNames(srcClass.prototype)) { for (const propName of Object.getOwnPropertyNames(srcClass.prototype)) {
if (methName != 'constructor') { if (propName != 'constructor') {
dstClass.prototype[methName] = srcClass.prototype[methName]; dstClass.prototype[propName] = srcClass.prototype[propName];
} }
} }
} }

View file

@ -20,14 +20,34 @@
export class ArdourCallback { export class ArdourCallback {
onTempo (bpm) {} // Connection status
onStripGain (stripId, db) {} onConnected () {}
onStripPan (stripId, value) {} onDisconnected () {}
onStripMute (stripId, value) {}
onStripPluginEnable (stripId, pluginId, value) {}
onStripPluginParamValue (stripId, pluginId, paramId, value) {}
// All messages and errors
onMessage (msg) {} onMessage (msg) {}
onError (error) {} onError (error) {}
} // Globals
onTempo (bpm) {}
// Strips
onStripDesc (stripId, name) {}
onStripMeter (stripId, db) {}
onStripGain (stripId, db) {}
onStripPan (stripId, value) {}
onStripMute (stripId, value) {}
// Strip plugins
onStripPluginDesc (stripId, pluginId, name) {}
onStripPluginEnable (stripId, pluginId, value) {}
// Strip plugin parameters
// valueType
// 'b' : boolean
// 'i' : integer
// 'd' : double
onStripPluginParamDesc (stripId, pluginId, paramId, name, valueType, min, max, isLog) {}
onStripPluginParamValue (stripId, pluginId, paramId, value) {}
}

View file

@ -23,27 +23,27 @@ import { ANode } from './message.js';
export class ControlMixin { export class ControlMixin {
async getTempo () { async getTempo () {
return (await this._sendAndReceive(ANode.TEMPO))[0]; return await this._sendRecvSingle(ANode.TEMPO);
} }
async getStripGain (stripId) { async getStripGain (stripId) {
return (await this._sendAndReceive(ANode.STRIP_GAIN, [stripId]))[0]; return await this._sendRecvSingle(ANode.STRIP_GAIN, [stripId]);
} }
async getStripPan (stripId) { async getStripPan (stripId) {
return (await this._sendAndReceive(ANode.STRIP_PAN, [stripId]))[0]; return await this._sendRecvSingle(ANode.STRIP_PAN, [stripId]);
} }
async getStripMute (stripId) { async getStripMute (stripId) {
return (await this._sendAndReceive(ANode.STRIP_MUTE, [stripId]))[0]; return await this._sendRecvSingle(ANode.STRIP_MUTE, [stripId]);
} }
async getStripPluginEnable (stripId, pluginId) { async getStripPluginEnable (stripId, pluginId) {
return (await this._sendAndReceive(ANode.STRIP_PLUGIN_ENABLE, [stripId, pluginId]))[0]; return await this._sendRecvSingle(ANode.STRIP_PLUGIN_ENABLE, [stripId, pluginId]);
} }
async getStripPluginParamValue (stripId, pluginId, paramId) { async getStripPluginParamValue (stripId, pluginId, paramId) {
return (await this._sendAndReceive(ANode.STRIP_PLUGIN_PARAM_VALUE, [stripId, pluginId, paramId]))[0]; return await this._sendRecvSingle(ANode.STRIP_PLUGIN_PARAM_VALUE, [stripId, pluginId, paramId]);
} }
setTempo (bpm) { setTempo (bpm) {

View file

@ -19,15 +19,15 @@
export const JSON_INF = 1.0e+128; export const JSON_INF = 1.0e+128;
export const ANode = Object.freeze({ export const ANode = Object.freeze({
TEMPO: 'tempo', TEMPO: 'tempo',
STRIP_DESC: 'strip_desc', STRIP_DESC: 'strip_desc',
STRIP_METER: 'strip_meter', STRIP_METER: 'strip_meter',
STRIP_GAIN: 'strip_gain', STRIP_GAIN: 'strip_gain',
STRIP_PAN: 'strip_pan', STRIP_PAN: 'strip_pan',
STRIP_MUTE: 'strip_mute', STRIP_MUTE: 'strip_mute',
STRIP_PLUGIN_DESC: 'strip_plugin_desc', STRIP_PLUGIN_DESC: 'strip_plugin_desc',
STRIP_PLUGIN_ENABLE: 'strip_plugin_enable', STRIP_PLUGIN_ENABLE: 'strip_plugin_enable',
STRIP_PLUGIN_PARAM_DESC: 'strip_plugin_param_desc', STRIP_PLUGIN_PARAM_DESC: 'strip_plugin_param_desc',
STRIP_PLUGIN_PARAM_VALUE: 'strip_plugin_param_value' STRIP_PLUGIN_PARAM_VALUE: 'strip_plugin_param_value'
}); });
@ -49,7 +49,7 @@ export class Message {
} }
} }
static hash (node, addr) { static nodeAddrId (node, addr) {
return [node].concat(addr || []).join('_'); return [node].concat(addr || []).join('_');
} }
@ -74,8 +74,8 @@ export class Message {
return JSON.stringify({node: this.node, addr: this.addr, val: val}); return JSON.stringify({node: this.node, addr: this.addr, val: val});
} }
get hash () { get nodeAddrId () {
return Message.hash(this.node, this.addr); return Message.nodeAddrId(this.node, this.addr);
} }
toString () { toString () {