Skip to content

[WIP] Overhaul of internals #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ The nRF24 radios use "logical channels" for communications within a physical cha

* Finally, via the `opts` parameter you can set a fixed payload `size` (in bytes, defaults to `'auto'`) or disable auto-acknowlegement with `autoAck:false` (defaults to `true`). Note that if you want to disable auto-acknowlegment, you *must* also set a payload size — for some reason these are linked in the nRF24 feature set.

* One other useful `opts` key is `reversePayloads`: the [RF24 library](https://github.com/maniacbug/RF24) commonly used on Arduino devices does not follow the datasheet's byte order for payload values. So normally, when talking between node-nrf and RF24 devices, both ends will see the reversed byte order. You can have node-nrf automatically correct this, but note it does add another layer of additional overhead. (If the data you write is subsequently unused, you can pass `{reversePayloads:'leave'}` to avoid some part of this overhead when sending — it will leave your buffer reversed.)

* For `'tx'` pipes, the `opts` parameter also lets you provide individual `retryCount`, `retryDelay`, `txPower` options instead of using the `radio.autoRetransmit` and `radio.transmitPower` methods; if you do this you should provide values for *every* `'tx`' pipe you open, to make sure the hardware configuration gets updated between each different transmission. [**TBD**: what is `ackPayloads` option supposed to do for `'tx`` pipes?]

Note that, while you can `.pipe()` to these streams as any other, `node-nrf` will not split data into packets for you, and will get upset if passed more than 32 bytes of data! Make sure all your `write`s to the stream fit the necessary MTU; **TBD** I imagine the common "transfer an arbitrarily large stream of data" case could be handled by a simple [object mode?] transform stream, find or provide a recommended module.
Expand Down
716 changes: 38 additions & 678 deletions index.js

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions logging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var util = require('util'),
debuglog = (1) ? _debuglog : util.debuglog('nrf');

var levels = ['error', 'warn', 'info', 'debug'];

exports.level = 'info';

exports.log = function (level, msg) {
if (levels.indexOf(level) > levels.indexOf(exports.level)) return;
else debuglog.apply(null, Array.prototype.slice.call(arguments, 1));
};

function _debuglog() {
var msg = util.format.apply(util, arguments);
process.stderr.write(msg+"\n");
}
25 changes: 25 additions & 0 deletions magicnums.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,28 @@ exports.TIMING = {
};

exports.TX_POWER = ['PA_MIN', 'PA_LOW', 'PA_HIGH', 'PA_MAX'];

exports.registersForMnemonics = function (list) {
var registersNeeded = Object.create(null);
list.forEach(function (mnem) {
var _r = exports.REGISTER_MAP[mnem];
if (!_r) return console.warn("Skipping uknown mnemonic '"+mnem+"'!");
if (_r.length === 1) _r.push(0,8);

var reg = _r[0],
howManyBits = _r[2] || 1,
iq = registersNeeded[reg] || (registersNeeded[reg] = {arr:[]});
iq.len = (howManyBits / 8 >> 0) || 1;
if (howManyBits < 8) iq.arr.push(mnem);
else iq.solo = mnem;
});
return registersNeeded;
};

exports.maskForMnemonic = function (mnem) {
var _r = exports.REGISTER_MAP[mnem],
howManyBits = _r[2] || 1,
rightmostBit = _r[1],
mask = 0xFF >> (8 - howManyBits) << rightmostBit;
return {mask:mask, rightmostBit:rightmostBit};
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"fifolock": "^1.0.0",
"pi-pins": "^1.0.0",
"pi-spi": "~0.8.1",
"queue-async": "~1.0.4"
"queue-async": "~1.0.4",
"xok": "^1.0.0"
}
}
281 changes: 281 additions & 0 deletions xcvr_api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
var util = require('util'),
_extend = require('xok');

var DEBUG = require("./logging").log.bind(null, 'debug'),
RawTransceiver = require("./xcvr_base"),
pipes = require("./xcvr_pipes"),
_m = require("./magicnums");


function Transceiver(hw) {
RawTransceiver.call(this, hw);

this._ready = false;
this._txQ = []; // TODO: is this still needed?
this._txPipes = [];
this._rxPipes = [];
this._rxP0 = null;
}

util.inherits(Transceiver, RawTransceiver);


/* CONFIGURATION WRAPPERS */

// NOTE: these rely on `getStates`/`setStates` for serialized-async

function _nop() {} // used when a cb is not provided

Transceiver.prototype.powerUp = function (val, cb) {
if (typeof val === 'function' || typeof val === 'undefined') {
cb = val || _nop;
this.getStates(['PWR_UP'], function (e,d) { cb(e, d && !!d.PWR_UP); });
} else this.setStates({PWR_UP:val}, cb);
return this;
};

Transceiver.prototype.channel = function (val, cb) {
if (typeof val === 'function' || typeof val === 'undefined') {
cb = val || _nop;
this.getStates(['RF_CH'], function (e,d) { cb(e, d && d.RF_CH); });
} else this.setStates({RF_CH:val}, cb);
return this;
};

Transceiver.prototype.dataRate = function (val, cb) {
if (typeof val === 'function' || typeof val === 'undefined') {
cb = val || _nop;
this.getStates(['RF_DR_LOW', 'RF_DR_HIGH'], function (e,d) {
if (e) return cb(e);
else if (d.RF_DR_LOW) cb(null, '250kbps');
else if (d.RF_DR_HIGH) cb(null, '2Mbps');
else cb(null, '1Mbps');
});
} else {
switch (val) {
case '1Mbps':
val = {RF_DR_LOW:false,RF_DR_HIGH:false};
break;
case '2Mbps':
val = {RF_DR_LOW:false,RF_DR_HIGH:true};
break;
case '250kbps':
val = {RF_DR_LOW:true,RF_DR_HIGH:false};
break;
default:
throw Error("dataRate must be one of '1Mbps', '2Mbps', or '250kbps'.");
}
this.setStates(val, cb);
}
return this;
};

Transceiver.prototype.transmitPower = function (val, cb) {
if (typeof val === 'function' || typeof val === 'undefined') {
cb = val || _nop;
this.getStates(['RF_PWR'], function (e,d) { cb(e, d && _m.TX_POWER[d.RF_PWR]); });
} else {
val = _m.TX_POWER.indexOf(val);
if (val === -1) throw Error("Radio power must be 'PA_MIN', 'PA_LOW', 'PA_HIGH' or 'PA_MAX'.");
this.setStates({RF_PWR:val}, cb);
}
return this;
};

Transceiver.prototype.crcBytes = function (val, cb) {
if (typeof val === 'function' || typeof val === 'undefined') {
cb = val || _nop;
this.getStates(['EN_CRC, CRCO'], function (e,d) {
if (e) return cb(e);
else if (!d.EN_CRC) cb(null, 0);
else if (d.CRCO) cb(null, 2);
else cb(null, 1);
});
} else {
switch (val) {
case 0:
val = {EN_CRC:false,CRCO:0};
break;
case 1:
val = {EN_CRC:true,CRCO:0};
break;
case 2:
val = {EN_CRC:true,CRCO:1};
break;
default:
throw Error("crcBytes must be 1, 2, or 0.");
}
this.setStates(val, cb);
}
return this;
};

Transceiver.prototype.addressWidth = function (val, cb) {
if (typeof val === 'function' || typeof val === 'undefined') {
cb = val || _nop;
this.getStates(['AW'], function (e,d) { cb(e, d && d.AW+2); });
} else this.setStates({AW:val-2}, cb);
return this;
};

Transceiver.prototype.autoRetransmit = function (val, cb) {
if (typeof val === 'function' || typeof val === 'undefined') {
cb = val || _nop;
this.getStates(['ARD, ARC'], function (e,d) { cb(e, d && {count:d.ARC,delay:250*(1+d.ARD)}); });
} else {
var states = {};
if ('count' in val) states['ARC'] = val.count;
if ('delay' in val) states['ARD'] = val.delay/250 - 1;
this.setStates(states, cb);
}
return this;
};


/* PAYLOAD ROUTINES */

// caller must know pipe and provide its params (e.g. width stuff)
Transceiver.prototype.readPayload = function (opts, cb, _n) { cb = this._SERIAL_(cb, function () {
var self = this;
if (opts.width === 'auto') self.execCommand('R_RX_PL_WID', 1, function (e,d) {
if (e) return finish(e);
var width = d[0];
if (width > 32) self.execCommand('FLUSH_RX', function (e,d) {
finish(new Error("Invalid dynamic payload size, receive queue flushed.")); // per R_RX_PL_WID details, p.51
}, self._NESTED_); else read(width);
}, self._NESTED_); else read(opts.width);

function read(width) {
self.execCommand('R_RX_PAYLOAD', width, finish, self._NESTED_);
}

function finish(e,d) { // see footnote c, p.62
if (opts.leaveStatus) cb(e,d);
else self.setStates({RX_DR:true,TX_DS:false,MAX_RT:false}, function (e2) {
cb(e||e2,d);
}, self._NESTED_);
}
}, (_n === this._NESTED_)); };

// caller must set up any prerequisites (i.e. TX addr)
Transceiver.prototype.sendPayload = function (data, opts, cb, _n) { cb = this._SERIAL_(cb, function () {
if (data.length > 32) throw Error("Maximum packet size exceeded. Smaller writes, Dash!");
self._prevSender = null; // help PxX setup again if user sends data directly

var cmd;
if ('asAckTo' in opts) {
cmd = ['W_ACK_PAYLOAD',opts.asAckTo];
} else if (opts.ack) {
cmd = 'W_TX_PAYLOAD';
} else {
cmd = 'W_TX_PD_NOACK';
}

var self = this;
self.execCommand(cmd, data, function (e) {
if (e) return cb(e);
if (!opts.ceHigh) self.pulseCE('pece2csn');
// TODO: if _sendOpts.asAckTo we won't get MAX_RT interrupt — how to prevent a blocked TX FIFO? (see p.33)
self.once('interrupt', function (d) {
if (d.MAX_RT) self.execCommand('FLUSH_TX', function (e) { // see p.56
finish(new Error("Packet timeout, transmit queue flushed."));
}, self._NESTED_);
else if (!d.TX_DS) console.warn("Unexpected IRQ during transmit phase!");
else finish();

function finish(e) { // clear our interrupts, leaving RX_DR
self.setStates({TX_DS:true,MAX_RT:true,RX_DR:false}, function () {
cb(e||null);
}, self._NESTED_);
}
});
}, self._NESTED_);
}, (_n === this._NESTED_)); };


/* LIFECYCLE ROUTINES */

Transceiver.prototype.reset = function (states, cb, _n) {
if (typeof states === 'function' || typeof states === 'undefined') {
_n = cb;
cb = states;
states = _m.REGISTER_DEFAULTS;
}
cb = this._SERIAL_(cb, function () {
var self = this;
self.setCE('low','stby2a');
self.execCommand('FLUSH_TX', function (e) {
if (e) cb(e);
else self.execCommand('FLUSH_RX', function (e) {
if (e) cb(e);
else self.setStates(states, cb, self._NESTED_);
}, self._NESTED_);
}, self._NESTED_);
}, (_n === this._NESTED_)); };

Transceiver.prototype.begin = function (cb) {
// NOTE: this relies on `reset` for serialized-async
var self = this,
clearIRQ = {RX_DR:true, TX_DS:true, MAX_RT:true},
features = {EN_DPL:true, EN_ACK_PAY:true, EN_DYN_ACK:true};
self.setCE('low','stby2a');
self.reset(_extend({PWR_UP:true, PRIM_RX:false, EN_RXADDR:0x00},clearIRQ,features), function (e) {
if (e) return self.emit('error', e);
// TODO: revisit this setting (make advanced users manage themselves?)
self.monitorIRQ(true); // NOTE: on before any pipes to facilite lower-level sendPayload use
self._ready = true;
self.emit('ready');
});
if (cb) self.once('ready', cb);
};

Transceiver.prototype.end = function (cb) {
var self = this,
pipes = self._txPipes.concat(self._rxPipes);
pipes.forEach(function (pipe) { pipe.close(); });
self._txPipes.length = self._rxPipes.length = self._txQ.length = 0;
self._ready = false;
self.monitorIRQ(false);
self.setCE(false,'stby2a');
self.setStates({PWR_UP:false}, function (e) {
if (e) self.emit('error', e);
if (cb) cb(e);
});
};

Transceiver.prototype._slotForAddr = function (addr) {
var slot = Array(6), aw = Math.max(3,Math.min(addr.length, 5));
this._rxPipes.forEach(function (pipe) { slot[pipe._pipe] = pipe._addr; });
if (slot[1]) aw = slot[1].length; // address width already determined
if (addr.length === 1) { // find a place in last four pipes
for (var i = 2; i < 6; ++i) if (!slot[i]) return i;
throw Error("No more final-byte listener addresses available!");
} else if (addr.length === aw) { // use pipe 1 or 0
if (!slot[1]) return 1;
else if (!slot[0]) return 0; // NOTE: using pipe 0 has caveats!
else throw Error("No more "+aw+"-byte listener addresses available!");
} else {
throw Error("Address 0x"+addr.toString(16)+" is of unsuitable width for use.");
}
};

Transceiver.prototype.openPipe = function (rx_tx, addr, opts) {
if (!this._ready) throw Error("Radio .begin() must be finished before a pipe can be opened.");
if (typeof addr === 'number') addr = Buffer(addr.toString(16), 'hex');
opts || (opts = {});

var pipe;
if (rx_tx === 'rx') {
var s = this._slotForAddr(addr);
pipe = new pipes.PRX(this, s, addr, opts);
this._rxPipes.push(pipe);
} else if (rx_tx === 'tx') {
pipe = new pipes.PTX(this, addr, opts);
this._txPipes.push(pipe);
} else {
throw Error("Unknown pipe mode '"+rx_tx+"', must be 'rx' or 'tx'.");
}
return pipe;
};

module.exports = Transceiver;
Loading