nodequad

#!/usr/bin/env node
var PACKAGE  = require('../package.json'),
    path     = require('path'),
    objPath  = require('object-path'),
    global   = this,
    Nodequad = require('../index');

var argv = require('yargs')
    .options('driver', {
        alias : 'd',
        default : 'serial',
        string: true,
        requiresArg: true,
        describe: "The name of the driver from Aeroquad.drivers or a path to a file that implements a driver interface. Defaults to 'serial' driver."
    })
    .options('protocol', {
        alias : 'p',
        default : 'AQ32',
        string: true,
        requiresArg: true,
        describe: "The name of the protocol from Aeroquad.protocols or a path to a file that implements a protocol interface. Defaults to 'AQ32' driver."
    })
    .options('list', {
        alias : 'l',
        boolean: true,
        describe: "Returns json array of available ports given a communications driver."
    })
    .options('probe', {
        alias : 'r',
        boolean: true,
        describe: "Returns json array of available vehicles on all ports based on a heuristic."
    })
    .options('connect', {
        alias : 'c',
        string: true,
        describe: "Attempts connection to a given URI, or uses the first vehicle URI from a successful probe when used in combination with the `--probe` option."
    })
    .options('on', {
        alias : 'o',
        string: true,
        requiresArg: true,
        describe: "Subscribe to an event and receive notifications, JSON output, newline delimited."
    })
    .options('sync', {
        alias : 's',
        string: true,
        describe: "Read or write key data. If a value is not specified, it is read from the vehicle. To set a value provide the value in JSON."
    })
    .options('stream', {
        alias : 't',
        string: true,
        requiresArg: true,
        describe: "Streams real-time state data from the vehicle for a given key."
    })
    .options('file', {
        alias : 'f',
        config: true,
        string: true,
        requiresArg: true,
        describe: "Use a JSON file as options instead of putting them on the command line"
    })
    .options('pretty', {
        alias : 'e',
        boolean: true,
        describe: "Pretty-print JSON output"
    })
    .check(function(parsed, opts) {
        //if(parsed.subscribe) {
        //  return false;
        //}
    }).
    example('$0 --list', "List JSON array of available ports").
    example('$0 --probe --connect', "Probe and automatically connect to first found vehicle").
    example('$0 --connect serial://115200@/dev/ttyACM0 --sync config.vehicle.*', "Connect to serial on /dev/ttyACM0 with baud 115200, and read all config.vehicle flight parameters").
    example('$0 --probe --connect --sync config.vehicle.pid.roll [1.1, 1.2, 1.3]', "Probe, connect and set vehicle roll pid").
    example('$0 --probe --connect --on driver.state.connected', "Probe, connect and monitor driver.state.connected events").
    example('$0 --probe --connect --stream state.gps.*', "Probe, connect and stream GPS state events")
    .version(PACKAGE.version, 'version')
    .options('v', {
        describe: "Show version number"
    })
    .help('help')
    .wrap(90)
    .showHelpOnFail(true, "View the man page for nodequad for more information and examples.")
    .argv;

// handle piped-in JSON
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(data) {
    var options = undefined;

    try {
        options = JSON.parse(data);
    } catch(error) {
        out(['error', error]);
        process.exit(0);
    }

    try {
    	if(options != undefined) handleOptions(options);
    	process.exit(1);
    } catch(error) {
    	out(['error', error]);
    	process.exit(0);
    }
});

var nq  = new Nodequad(),
    uri = argv.connect && argv.connect.length ? argv.connect : undefined,
    events = {};

// handle command line options
try {
	handleOptions(argv);
	//process.exit(1);
} catch(error) {
	out(['error', error]);
    //process.exit(0);
}

function handleOptions(options) {
    ['driver', 'protocol'].forEach(function(item) {
        if(Nodequad[item + 's'][options[item]] != undefined) {
            createCustom(item, Nodequad[item + 's'][options[item]]);
            return;
        }
        
        if(options[item].length) {
            try {
                var custom = require(path.resolve(process.cwd(), options[item]));
                if(custom != undefined) {
                    createCustom(item, custom);
                }
            } catch(e) {
                console.log(['Failed to require custom', item, ':', e.message].join(' '));
            }
        }

        function createCustom(key, obj) {
            if(obj === undefined) return;
            if(key === 'driver') {
                global[key] = new obj(uri);
            } else if(key === 'protocol') {
                global[key] = new obj(nq, { config: nq.config, state: nq.state, methods: nq.methods });
            }
        }

    });

    // pretty-print JSON
    if(options.pretty) {
        out = function(obj) {
            console.log(JSON.stringify(obj, null, 4));
        }
    }

    // handle listing communication ports
    if(options.list) {
        nq.getDriver().list(function (err, ports) {
            out(ports.map(function filter(port) {
                return {
                    type: port.type,
                    name: port.name,
                    extra: port.extra,
                    uri: port.uri
                };
            }));
            process.exit(0);
        });
        return;
    }

    //process.exit(0);

    // handle probe and connecting
    if(options.probe && options.connect) {
        nq.probe.andConnect();
    } else if(options.probe && uri === undefined) {
        nq.probe(function (ports) {
            out(ports.map(function filter(port) {
                return {
                    type: port.type,
                    name: port.name,
                    extra: port.extra,
                    uri: port.uri
                };
            }));
            process.exit(0);
        });
        return;
    }

    // handle subscribed events
    if(options.on) {
        if(!Array.isArray(options.on)) options.on = [options.on];
        options.on.forEach(function(key) {
            monitorEvent(key);
        });
    }

    // handle (one or more) sync'd properties
    if(options.sync) {
        if(!Array.isArray(options.sync)) options.sync = [options.sync];
        options.sync.forEach(function(key, i) {
            if(options._[i]) {
                // setting a value
                objPath.set(nq, key, JSON.parse(options._[i]));
            } else {
                // retreieving a value
                monitorEvent(key);
                nq.sync(key);
            }
        });
    }

    // handle (one or more) streams
    if(options.stream) {
        if(!Array.isArray(options.stream)) options.stream = [options.stream];
        options.stream.forEach(function(key) {
            // streaming data
            monitorEvent(key);
            nq.stream(key);
        });
    }
}

function out(obj) {
    console.log(JSON.stringify(obj));
}

function monitorEvent(key) {
    if(events[key]) return;
    nq.on(key, function() {
        out({
            on: key,
            event: this.event,
            arguments: arguments
        });
    });
    events[key] = true;
}