////////////////////////////////////////////////
const fs = require('fs');
const util = require('util');
////////////////////////////////////////////////
function tryRequire(path) {
try {
return require(path);
} catch (e) {
return {};
}
}
////////////////////////////////////////////////
// The default Selector constructor.
// This is an optional requirement since when used in a web context
// it would fail because of further USB-related dependencies.
// Browserify won't pick it up since the `require` call is encapsulated in `tryRequire`.
// If DefaultSelectorCtor is null, then it's a mandatory option to the Commander ctor.
const DefaultSelectorCtor = tryRequire('./selectors/physical-traffic-light-selector').SelectorCtor;
////////////////////////////////////////////////
function makeDefaultInterpreter() {
const {Interpreter} = require('./commands/interpreter');
const interpreter = new Interpreter();
// define all commands
require('./traffic-light/traffic-light-commands').defineCommands(interpreter);
require('./traffic-light/multi-traffic-light-commands').defineCommands(interpreter);
return interpreter;
}
////////////////////////////////////////////////
function makeDefaultFormatter() {
const {MetaFormatter} = require('./commands/meta-formatter');
return new MetaFormatter();
}
////////////////////////////////////////////////
/**
* Issues commands to control a traffic light.
*/
class Commander {
/**
* Creates a new Commander instance.
* @param {object} [options] - Commander options.
* @param {object} [options.logger=console] - A Console-like object for logging,
* with a log and an error function.
* @param {commands.MetaFormatter} [options.formatter] - A formatter for the help text of
* a command.
* @param {commands.Interpreter} [options.interpreter] - The Command Interpreter to use.
* @param {object} [options.selector] - The traffic light selector to use.
* Takes precedence over `options.SelectorCtor`.
* @param {function} [options.SelectorCtor] - The constructor of a traffic
* light selector to use. Will be passed the entire `options` object.
* Ignored if `options.selector` is set.
*/
constructor(options = {}) {
let {
logger = console,
formatter = makeDefaultFormatter(),
interpreter = makeDefaultInterpreter(),
selector = null,
SelectorCtor = DefaultSelectorCtor
} = options;
this.logger = logger;
this.formatter = formatter;
this.interpreter = interpreter;
this.selector = selector || new SelectorCtor(options);
this.selector.on('enabled', () => this._resumeIfNeeded());
this.selector.on('disabled', () => this.cancel());
this.selector.on('interrupted', () => this._interrupt());
}
/**
* Called to close this instance.
* Should be done as the last operation before exiting the process.
*/
close() {
this.selector.close();
}
/**
* Cancels any currently executing command.
*/
cancel() {
this.interpreter.cancel();
}
_interrupt() {
if (!this.running) return;
this.isInterrupted = true;
this.interpreter.cancel();
}
/**
* Executes a file with command definitions asynchronously.
* @param {string} filePath - Path to the file to execute.
* Should only contain command definitions (`define` or `def`).
* @param {string} [encoding='utf8'] - Encoding of the file.
*/
async runDefinitionsFile(filePath, encoding = 'utf8') {
let command = await this._readFile(filePath, encoding);
if (command) return this.runDefinitions(command);
}
/**
* Executes a command with definitions asynchronously.
* @param {string} command - Command to execute. Should only contain command
* definitions (`define` or `def`).
*/
async runDefinitions(command) {
try {
this.logger.log('running definitions');
await this.interpreter.execute(command); // no context, only for definitions
this.logger.log('finished definitions');
} catch (e) {
this.logger.error('error in definitions');
this.logger.error(e.message);
}
}
/**
* Executes a command file asynchronously.
* If the same command is already running, does nothing.
* If another command is running, cancels it, resets the traffic light,
* and runs the new command.
* If no command is running, executes the given command, optionally
* resetting the traffic light based on the `reset` parameter.
* If there's no traffic light to run the command, stores it for later when
* one becomes available. Logs messages appropriately.
* @param {string} filePath - Path to the file to execute.
* @param {boolean} [reset=false] - Whether to reset the traffic light
* before executing the command.
* @param {string} [encoding='utf8'] - Encoding of the file.
*/
async runFile(filePath, reset = false, encoding = 'utf8') {
let command = await this._readFile(filePath, encoding);
if (command) return this.run(command, reset);
}
async _readFile(filePath, encoding) {
try {
if (!fs.readFileAsync) fs.readFileAsync = util.promisify(fs.readFile);
return await fs.readFileAsync(filePath, encoding);
} catch (e) {
this.logger.error(`error accessing file '${filePath}'`);
this.logger.error(e.message);
return null;
}
}
/**
* Executes a command asynchronously.
* If the same command is already running, does nothing.
* If another command is running, cancels it, resets the traffic light,
* and runs the new command.
* If no command is running, executes the given command, optionally
* resetting the traffic light based on the `reset` parameter.
* If there's no traffic light to run the command, stores it for later when
* one becomes available. Logs messages appropriately.
* @param {string} command - Command to execute.
* @param {boolean} [reset=false] - Whether to reset the traffic light
* before executing the command.
*/
async run(command, reset = false) {
let tl = this.selector.resolveTrafficLight();
if (!tl) {
this.suspended = command;
this.logger.log('no traffic light available');
return;
}
try {
if (this._skipIfRunningSame(command, tl)) return;
await this._cancelIfRunningDifferent(command, tl);
return await this._execute(command, tl, reset);
} catch (e) {
this._errorInExecution(command, tl, e);
}
}
async _cancelIfRunningDifferent(command, tl) {
if (!this.running || this.running === command) return;
this.interpreter.cancel();
await tl.reset();
}
_skipIfRunningSame(command, tl) {
if (this.running !== command) return false;
return true;
}
async _execute(command, tl, reset) {
if (reset) await tl.reset();
this.logger.log(`${tl}: running`);
this.running = command;
let res = await this.interpreter.execute(command, {tl});
if (command === this.running) this.running = null;
this._finishedExecution(command, tl);
return res;
}
_finishedExecution(command, tl) {
if (this.isInterrupted || !tl.isEnabled) {
let state = this.isInterrupted ? 'interrupted' : 'disabled';
this.logger.log(`${tl}: ${state}, suspending running command`);
this.suspended = command;
this.isInterrupted = false;
this._resumeIfNeeded(); // try to resume in another traffic light
} else {
this.suspended = null;
this.logger.log(`${tl}: finished`);
}
}
_errorInExecution(command, tl, error) {
if (command === this.running) this.running = null;
this.logger.error(`${tl}: error in command`);
this.logger.error(error.message);
}
_resumeIfNeeded() {
let command = this.suspended;
if (!command) return;
this.suspended = null;
this.run(command, true); // no await
}
/**
* All supported command names.
* @type {string[]}
*/
get commandNames() {
return this.interpreter.commandNames;
}
/**
* All supported commands indexed by their names.
* @type {object.<string, commands.Command>}
*/
get commands() {
return this.interpreter.commands;
}
/**
* Logs the help info for the given command name.
* @param {string} commandName - Name of the command to log help info.
*/
help(commandName) {
let command = this.interpreter.lookup(commandName);
if (!command) {
this.logger.error(`Command not found: "${commandName}"`);
return;
}
this.logger.log(this.formatter.format(command.meta));
}
/**
* Logs information about known traffic lights.
*/
logInfo() {
this.selector.logInfo(this.logger);
}
}
////////////////////////////////////////////////
/**
* Factory for a Commander that deals with a single physical traffic light.
* It will get the first available traffic light for use.
* @param {object} [options] - Commander options.
* @param {object} [options.logger=console] - A Console-like object for logging,
* with a log and an error function.
* @param {commands.MetaFormatter} [options.formatter] - A formatter for the help text of
* a command.
* @param {commands.Interpreter} [options.interpreter] - The Command Interpreter to use.
* @param {physical.DeviceManager} [options.manager] - The Device Manager to use.
* @param {string|number} [options.serialNum] - The serial number of the
* traffic light to use, if available. Cleware USB traffic lights have
* a numeric serial number.
* @returns {Commander} A single traffic light commander.
*/
Commander.single = (options = {}) => {
const {SelectorCtor} = tryRequire('./selectors/physical-traffic-light-selector');
const selector = new SelectorCtor(options);
const commander = new Commander({...options, selector});
commander.manager = selector.manager;
return commander;
};
////////////////////////////////////////////////
/**
* Factory for a Commander that deals with multiple physical traffic lights.
* It will greedily get all available traffic lights for use.
* @param {object} [options] - Commander options.
* @param {object} [options.logger=console] - A Console-like object for logging,
* with a log and an error function.
* @param {commands.MetaFormatter} [options.formatter] - A formatter for the help text of
* a command.
* @param {commands.Interpreter} [options.interpreter] - The Command Interpreter to use.
* @param {physical.DeviceManager} [options.manager] - The physical Device Manager to use.
* @returns {Commander} A multiple traffic lights commander.
*/
Commander.multi = (options = {}) => {
const {SelectorCtor} = tryRequire('./selectors/physical-multi-traffic-light-selector');
const selector = new SelectorCtor(options);
const commander = new Commander({...options, selector});
commander.manager = selector.manager;
return commander;
};
////////////////////////////////////////////////
module.exports = {Commander};