Skip to content

Commit

Permalink
Add ConnectionCustomizer class.
Browse files Browse the repository at this point in the history
  • Loading branch information
danielwippermann committed Feb 12, 2014
1 parent 425ce92 commit 53bdb1a
Showing 1 changed file with 398 additions and 0 deletions.
398 changes: 398 additions & 0 deletions src/connection-customizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,398 @@
/*! resol-vbus | Copyright (c) 2013-2014, Daniel Wippermann | MIT license */
'use strict';



var _ = require('lodash');
var Q = require('q');


var utils = require('./utils');

var Customizer = require('./customizer');



var optionKeys = [
'connection',
'maxRounds',
'triesPerValue',
'timeoutPerValue',
'masterTimeout',
];



var ConnectionCustomizer = Customizer.extend(/** @lends ConnectionCustomizer# */ {

/**
* The connection to use for transfer of the configuration values.
* @type {Connection}
*/
connection: null,

/**
* Maximum number of optimization rounds for {@link transceiveConfiguration}.
* @type {number}
* @default 10
*/
maxRounds: 10,

/**
* Amount of retries to transceive one value.
* Between two tries the VBus is released and then re-acquired.
* @type {number}
* @default 2
*/
triesPerValue: 2,

/**
* Timeout in milliseconds after which the transceive times out.
* @type {number}
* @default 30000
*/
timeoutPerValue: 30000,

/**
* Interval in milliseconds in which
* the VBus master is contacted to reissue the VBus clearance.
* @type {number}
* @default 8000
*/
masterTimeout: 8000,

/**
* Constructs a new ConnectionCustomizer instance and optionally initializes its
* members with the given values.
*
* @constructs
* @augments Customizer
* @param {object} [options] Initialization values for this instance's members
* @param {number} [options.connection] {@link ConnectionCustomizer#connection}
* @param {number} [options.maxRounds] {@link ConnectionCustomizer#maxRounds}
* @param {number} [options.triesPerValue] {@link ConnectionCustomizer#triesPerValue}
* @param {number} [options.timeoutPerValue] {@link ConnectionCustomizer#timeoutPerValue}
* @param {number} [options.masterTimeout] {@link ConnectionCustomizer#masterTimeout}
*
* @classdesc
* A ConnectionCustomizer uses an established connection to a device
* to transfer sets of configuration values over it.
*/
constructor: function(options) {
Customizer.apply(this, arguments);

_.extend(this, _.pick(options, optionKeys));
},

loadConfiguration: function() {
var _this = this;

var options = {
action: 'get',
};

var callback = function(config, round) {
if (round === 1) {
return _this._getInitialConfiguration(config);
} else {
return _this._optimizeConfiguration(config);
}
};

return this.transceiveConfiguration(options, callback);
},

saveConfiguration: function(configuration) {
var _this = this;

var options = {
action: 'set',
actionOptions: {
save: true,
},
};

var callback = function(config, round) {
if (round === 1) {
return _this._getInitialConfiguration(config);
} else {
return _this._optimizeConfiguration(config);
}
};

return this.transceiveConfiguration(options, callback);
},

/**
* Transceives a controller configuration set, handling timeouts, retries etc.
*
* @param {object} options Options
* @param {number} [options.maxRounds] {@link ConnectionCustomizer#maxRounds}
* @param {number} [options.triesPerValue] {@link ConnectionCustomizer#triesPerValue}
* @param {number} [options.timeoutPerValue] {@link ConnectionCustomizer#timeoutPerValue}
* @param {number} [options.masterTimeout] {@link ConnectionCustomizer#masterTimeout}
* @param {number} options.action Action to perform, can be `'get'` or `'set'`.
* @param {number} [options.actionOptions] Options object to forward to the action to perform.
* @return {object} Promise that resolves to the configuration or `null` on timeout.
*/
transceiveConfiguration: function(options, callback) {
var _this = this;

if (_.isFunction(options)) {
callback = options;
options = null;
}

options = _.defaults({}, options, {
maxRounds: this.maxRounds,
triesPerValue: this.triesPerValue,
timeoutPerValue: this.timeoutPerValue,
masterTimeout: this.masterTimeout,
action: null,
actionOptions: null,
});

var connection = this.connection;
var address = this.deviceAddress;

return utils.cancelablePromise(function(resolve, reject, notify, checkCanceled) {
var config = null;

var state = {
masterAddress: null,
masterLastContacted: null,
};

var round = 0;

var reportProgress = function(progress) {
if (_.isString(progress)) {
progress = {
message: progress,
};
}

_.extend(progress, {
round: round,
});

notify(progress);
};

var nextRound = function() {
if (round < options.maxRounds) {
round++;

Q.fcall(checkCanceled).then(function() {
reportProgress('OPTIMIZING_VALUES');

return callback(config, round);
}).then(checkCanceled).then(function(newConfig) {
config = newConfig;

var pendingValues = _.filter(config, function(value) {
return value.pending;
});

var index = 0;

var nextValue = function() {
if (index < pendingValues.length) {
var valueInfo = pendingValues [index++];

Q.fcall(checkCanceled).then(function() {
return _this.transceiveValue(valueInfo.valueIndex, valueInfo.value, {
triesPerValue: options.triesPerValue,
timeoutPerValue: options.timeoutPerValue,
action: options.action,
actionOptions: options.actionOptions,
}, state).progress(function(progress) {
reportProgress(progress);
});
}).then(function(datagram) {
valueInfo.pending = false;
valueInfo.transceived = !!datagram;

if (datagram) {
valueInfo.value = datagram.value;
}

nextValue();
}).done();
} else {
nextRound(config);
}
};

if (pendingValues.length > 0) {
process.nextTick(nextValue);
} else {
Q.fcall(function() {
if (state.masterLastContacted !== null) {
reportProgress('RELEASING_BUS');

return connection.releaseBus(address);
}
}).then(function() {
resolve(config);
}).done();
}
}).fail(reject).done();
} else {
resolve(null);
}
};

process.nextTick(nextRound);
});
},

/**
* Transceive a controller value over this connection, handling
* timeouts, retries etc.
*
* @param {number} valueIndex Value index
* @param {number} value Value
* @param {object} options Options
* @param {number} options.triesPerValue {@link ConnectionCustomizer#triesPerValue}
* @param {number} options.timeoutPerValue {@link ConnectionCustomizer#timeoutPerValue}
* @param {number} options.masterTimeout {@link ConnectionCustomizer#masterTimeout}
* @param {object} state State to share between multiple calls to this method.
* @returns {object} Promise that resolves with the datagram received or `null` on timeout.
*/
transceiveValue: function(valueIndex, value, options, state) {
if (state === undefined) {
state = {};
}

options = _.defaults({}, options, {
triesPerValue: this.triesPerValue,
timeoutPerValue: this.timeoutPerValue,
masterTimeout: this.masterTimeout,
action: null,
actionOptions: null,
});

state = _.defaults(state, {
masterAddress: this.deviceAddress,
masterLastContacted: Date.now(),
});

var connection = this.connection;
var address = this.deviceAddress;

return utils.cancelablePromise(function(resolve, reject, notify, checkCanceled) {
var timer;

var done = function(err, result) {
if (timer) {
clearTimeout(timer);
timer = null;
}

if (err) {
reject(err);
} else {
resolve(result);
}
};

var tries = 0;

var reportProgress = function(message) {
notify({
message: message,
tries: tries,
});
};

var nextTry = function() {
if (tries < options.triesPerValue) {
tries++;

Q.fcall(checkCanceled).then(function() {
if ((tries > 1) && (state.masterLastContacted !== null)) {
reportProgress('RELEASING_BUS');

state.masterLastContacted = null;

return connection.releaseBus(state.masterAddress);
}
}).then(checkCanceled).then(function() {
if (state.masterLastContacted === null) {
reportProgress('WAITING_FOR_FREE_BUS');

return connection.waitForFreeBus().then(function(datagram) { // TODO: optional timeout?
if (datagram) {
state.masterAddress = datagram.sourceAddress;
} else {
state.masterAddress = null;
}
});
}
}).then(checkCanceled).then(function() {
var contactMaster;
if (state.masterAddress === null) {
contactMaster = false;
} else if (state.masterAddress === address) {
contactMaster = false;
} else if (state.masterLastContacted === null) {
contactMaster = true;
} else if ((Date.now() - state.masterLastContacted) >= options.masterTimeout) {
contactMaster = true;
} else {
contactMaster = false;
}
if (contactMaster) {
reportProgress('CONTACTING_MASTER');

state.masterLastContacted = Date.now();

return connection.getValueById(state.masterAddress, 0, {
timeout: 500,
tries: 1,
});
}
}).then(checkCanceled).then(function() {
if (state.masterAddress === address) {
state.masterLastContacted = Date.now();
}

if (options.action === 'get') {
reportProgress('GETTING_VALUE');

return connection.getValueById(address, valueIndex, options.actionOptions);
} else if (options.action === 'set') {
reportProgress('SETTING_VALUE');

return connection.setValueById(address, valueIndex, value, options.actionOptions);
} else {
throw new Error('Unknown action "' + options.action + '"');
}
}).then(function(datagram) {
if (datagram) {
done(null, datagram);
} else {
return nextTry();
}
}).fail(function(reason) {
done(reason);
}).done();
} else {
done(null, null);
}
};

var onTimeout = function() {
done(null, null);
};

process.nextTick(nextTry);
timer = setTimeout(onTimeout, options.timeoutPerValue);
});
}
});



module.exports = ConnectionCustomizer;

0 comments on commit 53bdb1a

Please sign in to comment.