ambient-attx4
Version:
Library to run the Ambient Module for Tessel. Detects ambient light and sound levels
366 lines (293 loc) • 10.3 kB
JavaScript
// Copyright 2014 Technical Machine, Inc. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Attiny = global.IS_TEST_ENV ? require('./test/common/attiny-common-mock') : require('attiny-common');
var MODULE_ID = 0x08;
var TINY44_SIGNATURE = 0x9207;
var FIRMWARE_FILE = __dirname + '/firmware/src/ambient-attx4.hex';
// Max 10 (16 bit) data chunks
var AMBIENT_BUF_SIZE = 10 * 2;
var AMBIENT_SINGLE_BYTE = 1 * 2;
var MAX_AMBIENT_VALUE = 1024;
// Confirmation Signals
var PACKET_CONF = 0x55;
// var ACK_CONF = 0x33; // Not used?
var FIN_CONF = 0x16;
var LIGHT_CMD = 2;
var SOUND_CMD = 3;
var LIGHT_TRIGGER_CMD = 4;
var SOUND_TRIGGER_CMD = 5;
var FETCH_TRIGGER_CMD = 6;
// These should be updated with each firmware release
var FIRMWARE_VERSION = 0x03;
var CRC = 0x1eb5;
var REBOOT_TIME = 100; // Amount of time needed to reboot and configure registers
function Ambient(hardware, callback) {
// Create a new instance of an attiny
this.attiny = new Attiny(hardware);
// Global connected. We may use this in the future
this.connected = false;
this.lightTriggerLevel = null;
this.soundTriggerLevel = null;
// milliSeconds between reads
this.pollingFrequency = 500;
this.lightPolling = false;
this.soundPolling = false;
this.pollInterval = undefined;
var self = this;
// Store our firmware checking and updating options
var firmwareOptions = {
firmwareFile: FIRMWARE_FILE,
firmwareVersion: FIRMWARE_VERSION,
moduleID: MODULE_ID,
signature: TINY44_SIGNATURE,
crc: CRC,
};
// Make sure we can communicate with the module
self.attiny.initialize(REBOOT_TIME, firmwareOptions, function(err) {
if (err) {
if (callback) {
callback(err);
} else {
self.emit('error', err);
}
} else {
self.connected = true;
// If someone starts listening
self.on('newListener', function(event) {
// and there weren't listeners before
if (!self.listeners(event).length) {
// start retrieving data for this type of buffer
self._setListening(true, event);
}
});
// if someone stops listening
self.on('removeListener', function(event) {
// and there are none left
if (!self.listeners(event).length) {
// stop retrieving data
self._setListening(false, event);
}
});
setImmediate(function() {
// Emit a ready event
self.emit('ready');
// Start listening for IRQ interrupts
self.irqwatcher = self._fetchTriggerValues.bind(self);
self.attiny.setIRQCallback(self.irqwatcher);
});
if (callback) {
callback(null, self);
}
}
});
}
// We want the ability to emit events
util.inherits(Ambient, EventEmitter);
Ambient.prototype._fetchTriggerValues = function() {
var self = this;
// cmd, cmd_echo, light_val (16 bits), sound_val (16 bits)
var packet = new Buffer([FETCH_TRIGGER_CMD, 0x00, 0x00, 0x00, 0x00, 0x00]);
// transceive the command
self.attiny.transceive(packet, function spiComplete(err, response) {
if (self.attiny._validateResponse(response, [PACKET_CONF, FETCH_TRIGGER_CMD])) {
// make a buffer with the cmd and cmd_echo spliced out
var data = new Buffer(response.slice(2, response.length));
// Read values
var lightTriggerValue = self._normalizeValue(data.readUInt16BE(0));
var soundTriggerValue = self._normalizeValue(data.readUInt16BE(2));
if (lightTriggerValue && self.lightTriggerLevel) {
self.emit('light-trigger', lightTriggerValue);
}
if (soundTriggerValue && self.soundTriggerLevel) {
self.emit('sound-trigger', soundTriggerValue);
}
setImmediate(function() {
self.irqwatcher = self._fetchTriggerValues.bind(self);
self.attiny.setIRQCallback(self.irqwatcher);
});
} else {
console.warn('Warning... Invalid trigger values fetched...');
}
});
};
Ambient.prototype._getSingleDatum = function(command, callback) {
// Read the buffer but only 1 16-bit wordbyte
this._readBuffer(command, AMBIENT_SINGLE_BYTE, callback);
};
Ambient.prototype._normalizeBuffer = function(buf) {
var numUInt16 = buf.length / 2;
var ret = new Array(numUInt16);
for (var i = 0; i < numUInt16; i++) {
ret[i] = this._normalizeValue(buf.readUInt16BE(i * 2));
}
return ret;
};
Ambient.prototype._normalizeValue = function(value) {
return (value / MAX_AMBIENT_VALUE);
};
Ambient.prototype._pollBuffers = function(cb) {
var self = this;
if (!self.connected) {
self._establishCommunication(5, function(err) {
if (err) {
self.emit('error', new Error('Can\'t communicate with Ambient module...'));
}
});
}
if (self.lightPolling) {
self.getLightBuffer(cb);
}
if (self.soundPolling) {
self.getSoundBuffer(cb);
}
};
Ambient.prototype._readBuffer = function(command, readLen, callback) {
var self = this;
// Create a packet with header, data bytes (16 bits) and stop byte
var header = new Buffer([command, readLen / 2, 0x00]);
var bytes = new Buffer(readLen);
bytes.fill(0);
var stop = new Buffer(1);
stop.writeUInt8(FIN_CONF, 0);
var packet = Buffer.concat([header, bytes, stop]);
// Synchronously transceive command to read
self.attiny.transceive(packet, function spiComplete(err, data) {
// If the response is valid
if (self.attiny._validateResponse(data, [PACKET_CONF, command, readLen / 2]) &&
data[data.length - 1] === FIN_CONF) {
data = self._normalizeBuffer(data.slice(header.length, data.length - 1));
if (data.length === 1) {
data = data[0];
}
var event = (command === LIGHT_CMD ? 'light' : 'sound');
self.emit(event, data);
// Return data
if (callback) {
callback(null, data);
}
return data;
} else {
var invalidErr = new Error('Invalid response from Ambient module');
if (callback) {
callback(invalidErr);
}
}
});
};
Ambient.prototype._setListening = function(enable, event) {
var self = this;
if (event === 'light') {
this.lightPolling = enable;
} else if (event === 'sound') {
this.soundPolling = enable;
} else {
return;
}
// if the other buffer is not already polling
if (
event === 'light' && !this.soundPolling ||
event === 'sound' && !this.lightPolling
) {
var pollBuffers = function() {
self._pollBuffers(function() {
self._pollTimeout = setTimeout(pollBuffers, self.pollingFrequency);
});
};
if (enable && !self._pollTimeout) {
// if we want to enable polling & there isn't already a poll
pollBuffers();
} else if (!enable && self._pollTimeout) {
// stop polling
clearTimeout(self._pollTimeout);
self._pollTimeout = null;
}
}
};
Ambient.prototype.disable = function() {
this._setListening(false, 'light');
this._setListening(false, 'sound');
};
Ambient.prototype._setTrigger = function(triggerCmd, triggerVal, callback) {
var self = this;
// Make the packet
triggerVal = Math.ceil(triggerVal * MAX_AMBIENT_VALUE);
var dataBuffer = new Buffer(2);
dataBuffer.writeUInt16BE(triggerVal, 0);
var packet = new Buffer([triggerCmd, dataBuffer.readUInt8(0), dataBuffer.readUInt8(1), 0x00]);
// Send it over SPI
self.attiny.transceive(packet, function spiComplete(err, data) {
// If it's a valud response
if (self.attiny._validateResponse(data, [PACKET_CONF, triggerCmd, dataBuffer.readUInt8(0), dataBuffer.readUInt8(1)])) {
// Get the event title
var event = triggerCmd === LIGHT_TRIGGER_CMD ? 'light-trigger-set' : 'sound-trigger-set';
// Store the trigger value locally
if (triggerCmd === LIGHT_TRIGGER_CMD) {
self.lightTriggerLevel = triggerVal;
} else {
self.soundTriggerLevel = triggerVal;
}
// Emit the event
self.emit(event, triggerVal / MAX_AMBIENT_VALUE);
// Return data
if (callback) {
callback(null, triggerVal / MAX_AMBIENT_VALUE);
}
// Return the value
return triggerVal;
} else {
if (callback) {
callback(new Error('Invalid response from Ambient module for trigger set.'));
}
}
});
};
// Clears trigger listener for light trigger
Ambient.prototype.clearLightTrigger = function(callback) {
this.setLightTrigger(0, callback);
};
// Gets trigger listener for sound trigger
Ambient.prototype.clearSoundTrigger = function(callback) {
this.setSoundTrigger(0, callback);
};
// Gets the last 20 light readings
Ambient.prototype.getLightBuffer = function(callback) {
this._readBuffer(LIGHT_CMD, AMBIENT_BUF_SIZE, callback);
};
// Gets a single data point of light level
Ambient.prototype.getLightLevel = function(callback) {
// Grab a single data point
this._getSingleDatum(LIGHT_CMD, callback);
};
// Gets the last 20 sound readings
Ambient.prototype.getSoundBuffer = function(callback) {
this._readBuffer(SOUND_CMD, AMBIENT_BUF_SIZE, callback);
};
// Gets a single data point of sound level
Ambient.prototype.getSoundLevel = function(callback) {
// Grab a single data point
this._getSingleDatum(SOUND_CMD, callback);
};
// Sets a trigger to emit a 'light-trigger' event when triggerVal is reached
Ambient.prototype.setLightTrigger = function(triggerVal, callback) {
this._setTrigger(LIGHT_TRIGGER_CMD, triggerVal, callback);
};
// Sets a trigger to emit a 'sound-trigger' event when triggerVal is reached
Ambient.prototype.setSoundTrigger = function(triggerVal, callback) {
this._setTrigger(SOUND_TRIGGER_CMD, triggerVal, callback);
};
function use(hardware, callback) {
if (!hardware) {
console.warn(new Error('Improperly specified Tessel port.'));
}
return new Ambient(hardware, callback);
}
exports.Ambient = Ambient;
exports.use = use;