Skip to content
Browse files

initial commit

  • Loading branch information...
0 parents commit 3c8904f5547174eefd838cd9b3eb7d83f2bfa145 @mharsch committed Mar 7, 2013
Showing with 908 additions and 0 deletions.
  1. +93 −0 Device.js
  2. +121 −0 README.md
  3. +58 −0 discover.js
  4. +149 −0 examples/channelscan.js
  5. +115 −0 examples/cli.js
  6. +32 −0 examples/vidstream.js
  7. +17 −0 index.js
  8. +39 −0 package.json
  9. +231 −0 protocol.js
  10. +28 −0 test/test-device.js
  11. +25 −0 test/test-discover.js
93 Device.js
@@ -0,0 +1,93 @@
+var net = require('net');
+var util = require('util');
+var events = require('events');
+var proto = require('./protocol.js');
+
+module.exports = Device;
+
+function Device(conf) {
+ events.EventEmitter.call(this, conf);
+ var self = this;
+
+ this.backlog = [];
+
+ this.device_id = conf.device_id;
+ this.device_ip = conf.device_ip;
+
+ this.control_sock = net.createConnection(65001, this.device_ip,
+ function () {
+ self.emit('connected');
+ });
+
+ this.control_sock.on('readable', function () {
+ var buf, msg, item, cb, err;
+ while ((buf = self.control_sock.read()) !== null) {
+ msg = proto.decode_pkt(buf);
+ if (msg.error_message)
+ err = new Error(msg.error_message);
+
+ if (self.backlog.length > 0) {
+ item = self.backlog.shift();
+ cb = item.callback;
+ clearTimeout(item.timeout);
+ cb(err, {name: msg.getset_name,
+ value: msg.getset_value});
+ } else {
+ console.log('unexpected packet: ' +
+ util.inspect(msg));
+ }
+ }
+ });
+
+ this.on('CTS', function () {
+ if (self.backlog.length > 0) {
+ self.control_sock.write(self.backlog[0].packet);
+ } else {
+ console.log('CTS with empty backlog');
+ }
+ });
+}
+
+util.inherits(Device, events.EventEmitter);
+
+Device.prototype.get = function (name, cb) {
+ get_set.call(this, name, cb);
+}
+
+Device.prototype.set = function (name, value, cb) {
+ get_set.call(this, name, value, cb);
+}
+
+function get_set(name, value, cb) {
+ var which = 'set';
+ if ((arguments.length == 2) && (typeof (value) == 'function')) {
+ cb = value;
+ value = null;
+ which = 'get';
+ }
+
+ var body = (which === 'set') ? {set: name, value: value} : {get: name};
+ body.type = 'getset';
+
+ msg = proto.gen_msg(body);
+ pkt = proto.encode_msg(msg);
+
+ this._request(pkt, cb);
+}
+
+Device.prototype._request = function (pkt, cb) {
+ var self = this;
+ var to = setTimeout(function () {
+ var unanswered = self.backlog.shift();
+ var cb = unanswered.callback;
+ var err = new Error('request timeout');
+ cb(err);
+ if (self.backlog.length > 0)
+ self.emit('CTS');
+ }, 2500);
+
+ this.backlog.push({packet: pkt, callback: cb, timeout: to});
+
+ if (this.backlog.length === 1)
+ this.emit('CTS');
+}
121 README.md
@@ -0,0 +1,121 @@
+#hdhomerun
+
+Control your [SiliconDust](http://www.silicondust.com) HDHomeRun network-attached digital TV tuner from node.js. This module provides a JavaScript interface to discover devices, and to get and set device control variables. While not a complete solution in itself, hdhomerun provides the low-level functionality necessary to build higher level tools and applications (such as DVR). Users of hdhomerun will certainly want to refer to [libhdhomerun](http://github.com/mharsch/libhdhomerun) (the C library provided by the manufacturer) for details on how the low-level get/set API can be used to implement higher-level functionality. See [examples/cli.js](examples/cli.js) for a node version of the libhdhomerun 'hdhomerun\_config' CLI utility.
+
+##Installation:
+
+ npm install hdhomerun
+
+##Usage:
+
+Use the discover() method to find devices on the local network:
+```javascript
+var hdhr = require('hdhomerun');
+
+hdhr.discover(function (err, res) {
+ console.log(res);
+};
+> [ { device_id: '1038A145',
+device_type: 'tuner',
+tuner_count: 2,
+device_ip: '192.168.2.123' } ]
+```
+
+Now create a device object:
+```javascript
+var device = hdhr.create({device_id: '1038A145',
+ device_ip: '192.168.2.123'});
+```
+
+Using the device object, you can get and set named control variables:
+```javascript
+device.get('/sys/model', function (err, res) {
+ console.log(res);
+}
+> { name: '/sys/model', value: 'hdhomerun3_atsc' }
+```
+
+Control variables are used to interact with the device. Most importantly, they let you change channels and initiate a video stream:
+```javascript
+device.set('/tuner0/channel', 'auto:9', function (err, res) {
+ console.log(res);
+}
+> { name: '/tuner0/channel', value: 'auto:9' }
+
+device.get('/tuner0/streaminfo', ...
+> 1: 9.1 KUSA-DT
+> 2: 9.2 9News N
+> tsid=0x01CF
+
+device.set('/tuner0/program', 1, ...
+device.get('/tuner0/status', ...
+> value: 'ch=auto:9 lock=8vsb ss=92 snq=77 seq=100 bps=19394080 pps=0'
+
+device.set('/tuner0/target', 'udp://localhost:54321', ...
+> // Now MPEG2 data is streaming to the specified host/port
+```
+
+##API:
+
+### discover([search\_id], callback)
+* `search_id` {String} Optional device\_id of device we wish to discover
+* `callback` {Function} Callback function
+ * `err` {Error Object} not yet implemented
+ * `found` {Array} Array of discovered device objects
+The `found` array contains objects representing discovered devices. Each
+object contains the following fields:
+ * `device_id` {String}
+ * `device_type` {String} Will always be 'tuner'
+ * `tuner_count` {Number} number of tuners in the device
+ * `device_ip` {String} IP address of the device
+
+When called without a `search_id` argument, discover() will accept responses from all devices on the local network that respond to it's broadcast request within the timeout period (currently 500ms).
+
+If `search_id` is specified, only a matching device will respond to the broadcast. Once a matching response is received, callback() is immediately called.
+
+### create(conf)
+* `conf` {Object}
+ * `device_id` {String}
+ * `device_ip` {String}
+* Returns: {Object} new Device object
+
+### Class: Device
+Device objects maintain a TCP connection with their corresponding physical device. This control socket is used to send get/set messages and receive responses. Device instances initiate the control socket connection immediately and emit `connected` when the connection is ready.
+
+### Device.get(variable, callback)
+* `variable` {String} named control variable to retrieve
+* `callback` {Function} called when a response from the device arrives
+ * `err` {Error Object}
+ * `res` {Object} response object with the following members:
+ * `name` {String} the requested control variable
+ * `value` {String} the value of the requested variable
+
+### Device.set(variable, value, callback)
+* `variable` {String} named control variable to set
+* `value` {String} value to set the control variable
+* `callback` {Function} called when a response from the device arrives
+ * `err` {Error Object}
+ * `res` {Object} response object with the following members:
+ * `name` {String} the requested control variable
+ * `value` {String} the (updated) value of the requested variable
+
+##Examples:
+
+[examples/cli.js](examples/cli.js) implements (roughly) the same functionality as [libhdhomerun](http://github.com/mharsch/libhdhomerun) 'hdhomerun\_config'. This toy program demonstrates how one could implement common functionality such as channel scanning and saving video streams (in a node-y way).
+
+##Testing:
+
+Currently, an actual HDHomeRun device is required to execute the tests. Set an environment variable 'MY\_HDHR' to the device\_id (aka serial #) of your local device before running 'npm test'.
+
+##Supported Devices
+
+* HDHR3-US (Dual Tuner ATSC)
+
+##TODO:
+
+* proper tests
+* additional device support
+* more complete and standardized error handling
+
+##License:
+MIT.
58 discover.js
@@ -0,0 +1,58 @@
+var dgram = require('dgram');
+var proto = require('./protocol.js');
+
+module.exports = function (search_id, cb) {
+ if ((arguments.length == 1) && (typeof (search_id) == 'function')) {
+ var cb = search_id;
+ search_id = null;
+ } else if ((arguments.length != 2) ||
+ (typeof (search_id) !== 'string') ||
+ (typeof (cb) !== 'function')) {
+ throw new Error('invalid discover() args');
+ }
+
+ var limit = 64;
+
+ var err = null;
+ var found = [];
+ var disc_pkt, disc_msg;
+ var disc_obj;
+ var timer;
+
+ var sock = dgram.createSocket('udp4', function (pkt, remote) {
+ disc_obj = proto.decode_pkt(pkt);
+ // XXX: add error checking
+
+ delete disc_obj.type;
+
+ disc_obj.device_ip = remote.address;
+
+ if (search_id) {
+ if (search_id == disc_obj.device_id)
+ found.push(disc_obj);
+ } else {
+ found.push(disc_obj);
+ }
+
+ if ((found.length >= limit) ||
+ (disc_obj.device_id == search_id)) {
+ clearTimeout(timer);
+ sock.close();
+ cb(err, found);
+ }
+ });
+
+ timer = setTimeout(function () {
+ sock.close();
+ cb(err, found);
+ }, 500);
+
+ disc_msg = proto.gen_msg({type: 'discover', device_id:
+ parseInt(search_id, 16)});
+ disc_pkt = proto.encode_msg(disc_msg);
+
+ sock.send(disc_pkt, 0, disc_pkt.length, 65001, '255.255.255.255',
+ function (err, bytes) {
+ if (err) throw new Error('problem sending discover req dgram');
+ });
+}
149 examples/channelscan.js
@@ -0,0 +1,149 @@
+var events = require('events');
+var util = require('util');
+
+module.exports = { create: create };
+
+function create(conf) {
+ return (new Scanner(conf));
+}
+
+function Scanner(conf) {
+ events.EventEmitter.call(this);
+ this.device = conf.device;
+ this.tuner = conf.tuner || 0;
+ this.first = conf.first || 2;
+ this.last = conf.last || 69;
+}
+
+util.inherits(Scanner, events.EventEmitter);
+
+Scanner.prototype.scan = function () {
+ var self = this;
+ var num_found = 0;
+ var list = [];
+
+ // scan channels from start to end
+ for (var i = this.first; i < this.last; i++)
+ list.push(i);
+
+ function series(channel) {
+ if (channel) {
+ console.log('probing channel: %d', channel);
+ var conf = {device: self.device, channel: channel,
+ tuner: self.tuner};
+
+ test_channel(conf, function (err, res) {
+ if (err) throw err;
+
+ if (res) {
+ self.emit('found', res);
+ num_found ++;
+ }
+
+ return (series(list.shift()));
+ });
+ } else {
+ self.emit('done', num_found);
+ }
+
+ }
+ series(list.shift());
+}
+
+function poll_lock(device, tuner, cb) {
+ // poll status until lock changes from 'none' to <modulation>
+ var timeout, poll;
+
+ timeout = setTimeout(function () {
+ clearTimeout(poll);
+ cb(null, null);
+ }, 2500);
+
+ function check_lock() {
+ device.get('/tuner' + tuner + '/status',
+ function (err, res) {
+ if (err) throw err;
+
+ var lock = res.value.split(' ')[1].split('=')[1];
+ if (lock !== 'none') {
+ clearTimeout(timeout);
+ cb(null, res.value);
+ } else {
+ poll = setTimeout(check_lock, 350);
+ }
+ });
+ }
+ check_lock();
+}
+
+function poll_seq(device, tuner, cb) {
+ // poll status until symbol quality reaches 100
+ var timeout, poll;
+
+ timeout = setTimeout(function () {
+ clearTimeout(poll);
+ cb(null, null);
+ }, 5000);
+
+ function check_seq() {
+ device.get('/tuner' + tuner + '/status',
+ function (err, res) {
+ if (err) throw err;
+
+ var seq = res.value.split(' ')[4].split('=')[1];
+ if (seq == '100') {
+ clearTimeout(timeout);
+ cb(null, res.value);
+ } else {
+ poll = setTimeout(check_seq, 250);
+ }
+ });
+ }
+ check_seq();
+}
+
+function test_channel(conf, cb) {
+ // do the poll thing and call cb with results
+ var device = conf.device;
+ var tuner = conf.tuner;
+ var channel = conf.channel;
+
+ var ret = {};
+
+ device.set('/tuner' + tuner + '/channel', 'auto:' + channel,
+ function (err, res) {
+ if (err) throw err;
+
+ poll_lock(device, tuner, function (err, res) {
+ if (err) throw err;
+
+ if (res) {
+ poll_seq(device, tuner, function (err, res) {
+ if (err) throw err;
+
+ if (res) {
+ ret.status = res;
+ device.get('/tuner' + tuner +
+ '/streaminfo',
+ function (err, res) {
+ if (err) throw err;
+
+ ret.streaminfo =
+ res.value;
+
+ cb(null, ret);
+ });
+ } else {
+ cb(null, null);
+ }
+ });
+ } else {
+ cb(null, null);
+ }
+ });
+ });
+}
+
+function scan_summary(num_found) {
+ console.log('found %d channels', num_found);
+}
115 examples/cli.js
@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+
+/*
+ * This script implements a node.js/JavaScript version of the hdhomerun_config
+ * utility that ships with the libhdhomerun C library.
+ */
+
+var hdhr = require('../index.js');
+var util = require('util');
+var fs = require('fs');
+var channelscan = require('./channelscan.js');
+var vidstream = require('./vidstream.js');
+
+var argv = process.argv;
+var argc = argv.length - 2;
+
+function usage() {
+ console.log('Usage:\n' +
+ '\tcli.js discover\n' +
+ '\tcli.js <id> get help\n' +
+ '\tcli.js <id> get <item>\n' +
+ '\tcli.js <id> set <item> <value>\n' +
+ '\tcli.js <id> scan <tuner> [<filename>]\n' +
+ '\tcli.js <id> save <tuner> [<filename>]\n');
+ process.exit(0);
+}
+
+if ((argc == 0) || (argv[2].match(/^[-]*help/)))
+ usage();
+
+if (argv[2] == 'discover') {
+ hdhr.discover(function (err, res) {
+ if (err) throw new Error('discover error');
+
+ res.forEach(function (dev) {
+ console.log('hdhomerun device %s found at %s',
+ dev.device_id, dev.device_ip);
+ });
+ process.exit(0);
+ });
+} else if (argv[2].match(/^[0-9a-fA-F]{8}$/)) {
+ hdhr.discover(argv[2], function (err, res) {
+ if (err) throw err;
+
+ var device = hdhr.create(res[0]);
+
+ switch (argv[3]) {
+ case ('get'):
+ device.get(argv[4], function (err, res) {
+ if (err) throw err;
+
+ console.log(res.value)
+ process.exit(0);
+ });
+ break;
+ case ('set'):
+ device.set(argv[4], argv[5], function (err, res) {
+ if (err) throw err;
+
+ process.exit(0);
+ });
+ break;
+ case ('scan'):
+ if (argv[4]) {
+ var scanner = channelscan.create(
+ {device: device, tuner: argv[4]});
+ scanner.on('found', function (channel) {
+ console.log('--- channel found ---');
+ console.log(channel.status);
+ console.log(channel.streaminfo);
+ });
+ scanner.on('done', function (num_found) {
+ console.log('found %d channels',
+ num_found);
+ process.exit(0);
+ });
+ scanner.scan();
+ } else {
+ usage();
+ }
+ break;
+ case ('save'):
+ if (argv[4] && argv[5]) {
+ var outfile = fs.createWriteStream(argv[5]);
+ var progress = 0;
+ vidstream.start(device, argv[4]).on('message',
+ function (msg, rinfo) {
+ outfile.write(msg);
+ if (progress >= 2000000) {
+ progress = 0;
+ util.print('.');
+ } else {
+ progress += msg.length
+ }
+ });
+ process.on('SIGINT', function () {
+ vidstream.stop(device, argv[4],
+ function () {
+ outfile.end();
+ console.log('%d bytes received',
+ outfile.bytesWritten);
+ process.exit(0);
+ });
+ });
+ } else {
+ usage();
+ }
+ break;
+ default:
+ usage();
+ }
+ });
+} else {
+ usage();
+}
32 examples/vidstream.js
@@ -0,0 +1,32 @@
+var util = require('util');
+var dgram = require('dgram');
+
+module.exports = {
+ start: start,
+ stop: stop
+};
+
+var sock = null;
+
+function start(device, tuner) {
+ sock = dgram.createSocket('udp4').on('listening', function () {
+ var addr = device.control_sock.localAddress;
+ var port = sock.address().port;
+ var name = util.format('/tuner%d/target', tuner);
+ var value = util.format('udp://%s:%d', addr, port);
+ device.set(name, value, function (err, res) {
+ if (err) throw err;
+
+ console.log('target set to: ' + res.value);
+ });
+ });
+ sock.bind();
+ return (sock);
+}
+
+function stop(device, tuner, cb) {
+ device.set('/tuner' + tuner + '/target', 'none',
+ function (err, res) {
+ cb();
+ });
+}
17 index.js
@@ -0,0 +1,17 @@
+var util = require('util');
+var Device = require('./Device.js');
+var discover = require('./discover.js');
+
+module.exports = {
+ create: _create,
+ discover: discover
+};
+
+function _create(conf) {
+ if (typeof (conf) === 'object') {
+ return (new Device(conf));
+ } else {
+ throw new Error('invalid create() argument:' +
+ util.inspect(conf));
+ }
+}
39 package.json
@@ -0,0 +1,39 @@
+{
+ "name": "hdhomerun",
+ "version": "0.0.1",
+ "description": "low-level control of HDHomeRun TV tuner devices",
+ "main": "index.js",
+ "directories": {
+ "example": "examples",
+ "test": "test"
+ },
+ "bugs": {
+ "url": "http://github.com/mharsch/node-hdhomerun/issues"
+ },
+ "dependencies": {
+ "buffer-crc32": ">=0.2.1"
+ },
+ "engines": {
+ "node": ">=0.9.4"
+ },
+ "devDependencies": {
+ "nodeunit": ">=0.7.4"
+ },
+ "scripts": {
+ "test": "nodeunit ./test/test-discover.js ./test/test-device.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/mharsch/node-hdhomerun.git"
+ },
+ "keywords": [
+ "hdhomerun",
+ "tv",
+ "tuner",
+ "video",
+ "dvr"
+ ],
+ "author": "Michael Harsch <mike@harschsystems.com>",
+ "license": "MIT",
+ "readmeFilename": "README.md"
+}
231 protocol.js
@@ -0,0 +1,231 @@
+var crc = require('buffer-crc32');
+
+module.exports = {
+ decode_pkt: decode_pkt,
+ encode_msg: encode_msg,
+ gen_msg: gen_msg
+};
+
+// these come from libhdhomerun/hdhomerun_pkt.h
+HDHOMERUN_TYPE_DISCOVER_REQ = 0x0002
+HDHOMERUN_TYPE_DISCOVER_RPY = 0x0003
+HDHOMERUN_TYPE_GETSET_REQ = 0x0004
+HDHOMERUN_TYPE_GETSET_RPY = 0x0005
+HDHOMERUN_TAG_DEVICE_TYPE = 0x01
+HDHOMERUN_TAG_DEVICE_ID = 0x02
+HDHOMERUN_TAG_GETSET_NAME = 0x03
+HDHOMERUN_TAG_GETSET_VALUE = 0x04
+HDHOMERUN_TAG_GETSET_LOCKKEY = 0x15
+HDHOMERUN_TAG_ERROR_MESSAGE = 0x05
+HDHOMERUN_TAG_TUNER_COUNT = 0x10
+HDHOMERUN_DEVICE_TYPE_WILDCARD = 0xFFFFFFFF
+HDHOMERUN_DEVICE_TYPE_TUNER = 0x00000001
+HDHOMERUN_DEVICE_ID_WILDCARD = 0xFFFFFFFF
+
+// shorthand versions of the above constants
+var types = {
+ disc_req: HDHOMERUN_TYPE_DISCOVER_REQ,
+ disc_rpy: HDHOMERUN_TYPE_DISCOVER_RPY,
+ getset_req: HDHOMERUN_TYPE_GETSET_REQ,
+ getset_rpy: HDHOMERUN_TYPE_GETSET_RPY
+}
+
+var tags = {
+ device_type: { value: HDHOMERUN_TAG_DEVICE_TYPE, size: 4 },
+ device_id: { value: HDHOMERUN_TAG_DEVICE_ID, size: 4 },
+ getset_name: { value: HDHOMERUN_TAG_GETSET_NAME, size: undefined },
+ getset_value: { value: HDHOMERUN_TAG_GETSET_VALUE, size: undefined },
+ getset_lockkey: { value: HDHOMERUN_TAG_GETSET_LOCKKEY, size: 4 },
+ error_message: { value: HDHOMERUN_TAG_ERROR_MESSAGE, size: undefined },
+ tuner_count: { value: HDHOMERUN_TAG_TUNER_COUNT, size: 1 }
+}
+
+var dev_values = {
+ device_type_tuner: HDHOMERUN_DEVICE_TYPE_TUNER,
+ device_type_any: HDHOMERUN_DEVICE_TYPE_WILDCARD,
+ device_id_any: HDHOMERUN_DEVICE_ID_WILDCARD
+}
+
+function encode_msg(msg) {
+ // XXX: need some kind of error handling here
+
+ var pkt = new Buffer(3072); // max buffer size from libhdhomerun
+ var pos = 0;
+
+ pkt.writeUInt16BE(msg.type, 0); pos += 2;
+ // leave room for the length
+ pos += 2;
+
+ Object.keys(msg).forEach(function (field) {
+ if (field === 'type') {
+ return;
+ }
+ pos = encode_tlv({ tag: field, value: msg[field] }, pkt, pos);
+ });
+
+ // trim buffer to size; leave room for checksum
+ pkt = pkt.slice(0, pos + 4);
+
+ // write packet size to header
+ pkt.writeUInt16BE(pkt.length - 8, 2);
+
+ var cksum = crc.unsigned(pkt.slice(0, pkt.length - 4));
+ pkt.writeUInt32LE(cksum, pkt.length - 4);
+
+ return (pkt);
+}
+
+
+function decode_pkt(pkt) {
+ var tag, tag_len, tag_val;
+ var pos = 0;
+ var msg = {};
+
+ var pkt_type = pkt.readUInt16BE(pos); pos += 2;
+ var pkt_len = pkt.readUInt16BE(pos); pos += 2;
+
+ msg.type = pkt_type;
+
+ while (pos < (pkt_len + 4)) {
+ tag = pkt.readUInt8(pos); pos += 1;
+ tag_len = decode_varlen(pkt.slice(pos, pos + 2));
+ (tag_len < 128) ? pos += 1 : pos += 2;
+
+ switch (tag) {
+ case (tags.device_type.value):
+ var devtype = pkt.readUInt32BE(pos); pos += tag_len;
+ (devtype == HDHOMERUN_DEVICE_TYPE_TUNER) ?
+ msg.device_type = 'tuner' :
+ msg.device_type = 'unknown';
+ break;
+ case (tags.device_id.value):
+ var devid = pkt.readUInt32BE(pos); pos += tag_len;
+ msg.device_id = devid.toString(16).toUpperCase();
+ break;
+ case (tags.tuner_count.value):
+ var count = pkt.readUInt8(pos); pos += tag_len;
+ msg.tuner_count = count;
+ break;
+ case (tags.getset_name.value):
+ var gs_name = pkt.toString('ascii', pos, pos +
+ tag_len - 1);
+ pos += tag_len;
+ msg.getset_name = gs_name;
+ break;
+ case (tags.getset_value.value):
+ var gs_val = pkt.toString('ascii', pos, pos +
+ tag_len - 1);
+ pos += tag_len;
+ msg.getset_value = gs_val;
+ break;
+ case (tags.getset_lockkey.value):
+ var lockkey = pkt.readUInt32BE(pos); pos += tag_len;
+ msg.lockkey = lockkey.toString(10).toUpperCase();
+ break;
+ case (tags.error_message.value):
+ var err_msg = pkt.toString('ascii', pos, pos +
+ tag_len - 1);
+ pos += tag_len;
+ msg.error_message = err_msg;
+ break;
+ default:
+ throw new Error('unknown tag type: ' + tag);
+ }
+ }
+
+ return (msg);
+}
+
+function gen_msg(conf) {
+ var msg = {};
+ switch (conf.type) {
+ case ('discover'):
+ msg.type = types.disc_req;
+ msg.device_type = dev_values.device_type_tuner;
+ msg.device_id = conf.device_id || dev_values.device_id_any;
+ return (msg);
+ case ('getset'):
+ msg.type = types.getset_req;
+ msg.getset_name = conf.get || conf.set;
+ if (conf.set)
+ msg.getset_value = conf.value;
+
+ return (msg);
+ default:
+ throw new Error('unsupported message type');
+ }
+}
+
+function encode_varlen(length) {
+ var varlen, tmpbuf;
+ if (length <= 127) {
+ varlen = new Buffer(1);
+ varlen.writeUInt8(length, 0);
+ return (varlen);
+ } else {
+ varlen = new Buffer(2);
+ tmpbuf = new Buffer(2);
+ tmpbuf.writeUInt16BE(length, 0);
+
+ varlen[0] = 0x80 | tmpbuf[1];
+ varlen[1] = (tmpbuf[0] << 1) | (tmpbuf[1] >> 7);
+
+ return (varlen);
+ }
+}
+
+function decode_varlen(varlen) {
+ var tmpbuf;
+ var fb;
+ var length;
+
+ fb = varlen.readUInt8(0);
+
+ if (fb <= 127) {
+ return (fb);
+ } else {
+ tmpbuf = new Buffer(2);
+ tmpbuf[1] = (varlen[0] & 0x7f) | (varlen[1] << 7);
+ tmpbuf[0] = varlen[1] >> 1;
+
+ length = tmpbuf.readUInt16BE(0);
+ return (length);
+ }
+}
+
+
+function encode_tlv(tlv_obj, buf, offset) {
+ // write the tag
+ buf.writeUInt8(tags[tlv_obj.tag].value, offset); offset += 1;
+
+ if (tags[tlv_obj.tag].size) {
+ buf.writeUInt8(tags[tlv_obj.tag].size, offset); offset += 1;
+ }
+
+ switch (tags[tlv_obj.tag].value) {
+ case (tags.device_type.value):
+ case (tags.device_id.value):
+ case (tags.getset_lockkey.value):
+ buf.writeUInt32BE(tlv_obj.value, offset);
+ offset += 4;
+ return (offset);
+ break;
+ case (tags.tuner_count.value):
+ buf.writeUInt8(tlv_obj.value, offset);
+ offset += 1;
+ return (offset);
+ break;
+ case (tags.getset_name.value):
+ case (tags.getset_value.value):
+ var tmpbuf = new Buffer(tlv_obj.value, 'ascii');
+ var tmplen = tmpbuf.length;
+ var varlen = encode_varlen(tmplen);
+ tmpbuf = Buffer.concat([varlen, tmpbuf]);
+ tmpbuf.copy(buf, offset);
+ offset += tmpbuf.length;
+ return (offset);
+ default:
+ throw new Error('cannot encode tag type ' +
+ tags[tlv_obj.tag].value);
+ }
+}
28 test/test-device.js
@@ -0,0 +1,28 @@
+var nodeunit = require('nodeunit');
+var hdhr = require('../index.js');
+var util = require('util');
+
+exports['check for $MY_HDHR'] = function (test) {
+ test.ok(process.env.MY_HDHR, 'env var MY_HDHR should be set to ' +
+ 'device_id of local online device');
+ test.done();
+};
+
+exports['create device object'] = function (test) {
+ hdhr.discover(process.env.MY_HDHR,
+ function (err, found) {
+ test.ifError(err);
+
+ if (found[0]) {
+ var device = hdhr.create(found[0]);
+ device.get('/sys/model', function (err, res) {
+ test.ifError(err);
+ test.equal(res.value, 'hdhomerun3_atsc');
+ test.done();
+ device.control_sock.destroy();
+ });
+ } else {
+ throw new Error('no device found');
+ }
+ });
+};
25 test/test-discover.js
@@ -0,0 +1,25 @@
+var hdhr = require('../index.js');
+
+exports['discover all devices'] = function (test) {
+ hdhr.discover(function (err, results) {
+ test.ifError(err);
+ test.ok(Array.isArray(results));
+ test.ok(results.length >= 1, 'found at least one device');
+ test.done();
+ });
+};
+
+exports['discover one particular device'] = function (test) {
+ if (process.env.MY_HDHR) {
+ hdhr.discover(process.env.MY_HDHR,
+ function (err, results) {
+ test.ifError(err);
+ test.ok(results.length == 1);
+ test.equal(results[0].device_id, process.env.MY_HDHR);
+ test.done();
+ });
+ } else {
+ throw new Error(
+ 'set env var MY_HDHR to valid (online) device_id');
+ }
+};

0 comments on commit 3c8904f

Please sign in to comment.
Something went wrong with that request. Please try again.