johnny-five
Version:
The JavaScript Robotics and Hardware Programming Framework. Use with: Arduino (all models), Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, pcDuino3, Raspberry Pi, Particle/Spark Core & Photon, Tessel 2, TI Launchpad and more!
1,030 lines (895 loc) • 25.9 kB
JavaScript
const Board = require("./board");
const EventEmitter = require("events");
const Withinable = require("./mixins/withinable");
const {
toFixed,
POW_2_16,
} = require("./fn");
const {
log,
round,
trunc,
} = Math;
const CELSIUS_TO_KELVIN = 273.15;
function analogHandler(options, callback) {
const pin = options.pin;
this.io.pinMode(pin, this.io.MODES.ANALOG);
this.io.analogRead(pin, data => {
callback.call(this, data);
});
}
const activeDrivers = new Map();
const Drivers = {
MAX31850K: {
initialize: {
value(board, options) {
const CONSTANTS = {
TEMPERATURE_FAMILY: 0x3B,
CONVERT_TEMPERATURE_COMMAND: 0x44,
READ_SCRATCHPAD_COMMAND: 0xBE,
READ_COUNT: 9
};
const pin = options.pin;
const freq = options.freq || 100;
const getAddress = device => {
// 64-bit device code
// device[0] => Family Code
// device[1..6] => Serial Number (device[1] is LSB)
// device[7] => CRC
let result = 0;
for (let i = 6; i > 0; i--) {
result = result * 256 + device[i];
}
return result;
};
board.io.sendOneWireConfig(pin, true);
board.io.sendOneWireSearch(pin, (err, devices) => {
if (err) {
this.emit("error", err);
return;
}
this.devices = devices.filter(device => device[0] === CONSTANTS.TEMPERATURE_FAMILY, this);
if (devices.length === 0) {
this.emit("error", new Error("FAILED TO FIND TEMPERATURE DEVICE"));
return;
}
this.devices.forEach(device => {
this.emit("initialized", getAddress(device));
});
let getAddresses = () => {
if (this.addresses) {
return this.devices.filter(function(device) {
const address = getAddress(device);
return this.addresses.includes(address);
}, this);
} else {
return [this.devices[0]];
}
};
let readTemperature = () => {
let result;
// request tempeature conversion
let devicesToWait = getAddresses();
let devicesToRead = getAddresses();
devicesToRead.forEach(device => {
board.io.sendOneWireReset(pin);
board.io.sendOneWireWrite(pin, device, CONSTANTS.CONVERT_TEMPERATURE_COMMAND);
});
let isConversionAvailable = done => {
let nextDevice;
if (devicesToWait.length === 0) {
return done();
}
nextDevice = devicesToWait.pop();
board.io.sendOneWireReset(pin);
board.io.sendOneWireWriteAndRead(pin, nextDevice, CONSTANTS.READ_SCRATCHPAD_COMMAND, CONSTANTS.READ_COUNT, (err, data) => {
if (!data[0]) {
devicesToWait.push(nextDevice);
if (data[1] !== 0) { //*****checks if second data bit is 0, if not its an error and gets thrown out
return done();
}
}
isConversionAvailable(done);
});
};
let readOne = () => {
let device;
if (devicesToRead.length === 0) {
setTimeout(readTemperature, freq);
return;
}
device = devicesToRead.pop();
// read from the scratchpad
board.io.sendOneWireReset(pin);
board.io.sendOneWireWriteAndRead(pin, device, CONSTANTS.READ_SCRATCHPAD_COMMAND, CONSTANTS.READ_COUNT, (error, data) => {
if (error) {
this.emit("error", error);
return;
}
result = (data[1] << 8) | data[0];
this.emit("data", getAddress(device), result);
readOne();
});
};
isConversionAvailable(readOne);
};
readTemperature();
});
}
},
register: {
value(address) {
if (!this.addresses) {
this.addresses = [];
}
this.addresses.push(address);
}
}
},
DS18B20: {
initialize: {
value(board, options) {
const CONSTANTS = {
TEMPERATURE_FAMILY: 0x28,
CONVERT_TEMPERATURE_COMMAND: 0x44,
READ_SCRATCHPAD_COMMAND: 0xBE,
READ_COUNT: 2
};
const pin = options.pin;
const freq = options.freq || 100;
let getAddress;
let readThermometer;
let readOne;
getAddress = device => {
// 64-bit device code
// device[0] => Family Code
// device[1..6] => Serial Number (device[1] is LSB)
// device[7] => CRC
let i;
let result = 0;
for (i = 6; i > 0; i--) {
result = result * 256 + device[i];
}
return result;
};
board.io.sendOneWireConfig(pin, true);
board.io.sendOneWireSearch(pin, (err, devices) => {
if (err) {
this.emit("error", err);
return;
}
this.devices = devices.filter(device => device[0] === CONSTANTS.TEMPERATURE_FAMILY, this);
if (devices.length === 0) {
this.emit("error", new Error("FAILED TO FIND TEMPERATURE DEVICE"));
return;
}
this.devices.forEach(device => {
this.emit("initialized", getAddress(device));
});
readThermometer = () => {
let devicesToRead;
let result;
// request tempeature conversion
if (this.addresses) {
devicesToRead = this.devices.filter(function(device) {
const address = getAddress(device);
return this.addresses.includes(address);
}, this);
} else {
devicesToRead = [this.devices[0]];
}
devicesToRead.forEach(device => {
board.io.sendOneWireReset(pin);
board.io.sendOneWireWrite(pin, device, CONSTANTS.CONVERT_TEMPERATURE_COMMAND);
});
// the delay gives the sensor time to do the calculation
board.io.sendOneWireDelay(pin, 1);
readOne = () => {
let device;
if (devicesToRead.length === 0) {
setTimeout(readThermometer, freq);
return;
}
device = devicesToRead.pop();
// read from the scratchpad
board.io.sendOneWireReset(pin);
board.io.sendOneWireWriteAndRead(pin, device, CONSTANTS.READ_SCRATCHPAD_COMMAND, CONSTANTS.READ_COUNT, (err, data) => {
if (err) {
this.emit("error", err);
return;
}
result = (data[1] << 8) | data[0];
this.emit("data", getAddress(device), result);
readOne();
});
};
readOne();
};
readThermometer();
});
}
},
register: {
value(address) {
if (!this.addresses) {
this.addresses = [];
}
this.addresses.push(address);
}
}
}
};
Drivers.get = (board, driverName, options) => {
let drivers;
let driver;
if (!activeDrivers.has(board)) {
activeDrivers.set(board, {});
}
drivers = activeDrivers.get(board);
const key = `${driverName}_${options.pin}`;
if (!drivers[key]) {
driver = new EventEmitter();
Object.defineProperties(driver, Drivers[driverName]);
driver.initialize(board, options);
drivers[key] = driver;
}
return drivers[key];
};
Drivers.clear = () => {
activeDrivers.clear();
};
// References
//
const Controllers = {
// Generic thermistors. See datasheet for each device.
ANALOG: {
initialize: {
value: analogHandler
}
},
LM35: {
initialize: {
value: analogHandler
},
toCelsius: {
value(raw) {
// VOUT = 1500 mV at 150°C
// VOUT = 250 mV at 25°C
// VOUT = –550 mV at –55°C
const mV = this.aref * 1000 * raw / 1023;
// 10mV = 1°C
//
// Page 1
return round(mV / 10);
}
}
},
LM335: {
initialize: {
value: analogHandler
},
toCelsius: {
value(raw) {
// OUTPUT 10mV/°K
const mV = this.aref * 1000 * raw / 1023;
// Page 1
return round((mV / 10) - CELSIUS_TO_KELVIN);
}
}
},
TMP36: {
initialize: {
value: analogHandler
},
toCelsius: {
value(raw) {
// Analog Reference Voltage
const mV = this.aref * 1000 * raw / 1023;
// tempC = (mV / 10) - 50
//
// Page 3
// Table 1
// Accuracy 1°C
return round((mV / 10) - 50);
}
}
},
TMP102: {
ADDRESSES: {
value: [0x48]
},
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
this.io.i2cConfig(options);
// Addressing is unclear.
this.io.i2cRead(address, 0x00, 2, data => {
// Based on the example code from https://www.sparkfun.com/products/11931
let raw = ((data[0] << 8) | data[1]) >> 4;
// The tmp102 does twos compliment but has the negative bit in the wrong spot, so test for it and correct if needed
if (raw & (1 << 11)) {
raw |= 0xF800; // Set bits 11 to 15 to 1s to get this reading into real twos compliment
}
// twos compliment
raw = raw >> 15 ? ((raw ^ 0xFFFF) + 1) * -1 : raw;
callback(raw);
});
}
},
toCelsius: {
value(raw) {
// 6.5 Electrical Characteristics
// –25°C to 85°C ±0.5
return toFixed(raw / 16, 1);
}
},
},
MAX31850K: {
initialize: {
value(options, callback) {
const state = priv.get(this);
const address = options.address;
const driver = Drivers.get(this.board, "MAX31850K", options);
if (address) {
state.address = address;
driver.register(address);
} else {
if (driver.addressless) {
this.emit("error", "You cannot have more than one MAX31850K without an address");
}
driver.addressless = true;
}
driver.once("initialized", dataAddress => {
if (!state.address) {
state.address = dataAddress;
}
});
driver.on("data", (dataAddress, data) => {
if (!address || dataAddress === address) {
callback(data);
}
});
}
},
toCelsius: {
// Page 4
// Thermocouple Temperature Data Resolution
value(value) {
return toFixed(value / 16, 2);
}
},
address: {
get() {
return priv.get(this).address || 0x00;
}
}
},
// Based on code from Westin Pigott:
// https://github.com/westinpigott/one-wire-temps
// And the datasheet:
// OneWire protocol. The device needs to be issued a "Convert Temperature"
// command which can take up to 10 microseconds to compute, so we need
// tell the board to delay 1 millisecond before issuing the "Read Scratchpad" command
//
// This device requires the OneWire support enabled via ConfigurableFirmata
DS18B20: {
initialize: {
value(options, callback) {
const state = priv.get(this);
const address = options.address;
const driver = Drivers.get(this.board, "DS18B20", options);
if (address) {
state.address = address;
driver.register(address);
} else {
if (driver.addressless) {
this.emit("error", "You cannot have more than one DS18B20 without an address");
}
driver.addressless = true;
}
driver.once("initialized", dataAddress => {
if (!state.address) {
state.address = dataAddress;
}
});
driver.on("data", (dataAddress, data) => {
if (!address || dataAddress === address) {
callback(data);
}
});
}
},
toCelsius: {
value(value) {
// ±0.5°C accuracy from -10°C to +85°C
//
// Temp resolution is as follows:
// 9b, 10b 11b, 12b
// 0.5°C, 0.25°C, 0.125°C, 0.0625°C
//
// I'm not sure which we're reading, so default to 4
// fractional digits until we can verify
return toFixed(value / 16, 4);
}
},
address: {
get() {
return priv.get(this).address || 0x00;
}
}
},
SHT31D: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "SHT31D", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(value) {
// Page 4, Table 1.2 Temperature Sensor Performance
// Resolution: 0.015
//
// Page 14
// 4.13 Conversion of Signal Output
// T[C] = -45 + 175 * (St / ((2 ** 26) - 1))
// St = Sensor raw temperature
return toFixed(-45 + (175 * (value / (POW_2_16 - 1))), 3);
}
}
},
HTU21D: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "HTU21D", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(value) {
// Page 5
// Digital Relative Humidity sensor with Temperature output
// Resolution shows 0.01-0.04
//
// Page 15
// CONVERSION OF SIGNAL OUTPUTS
// T = -46.85 + 175.72 * (Stemp / (2 ** 16))
// Stemp = Sensor raw temperature
return toFixed(-46.85 + (175.72 * (value / POW_2_16)), 2);
}
}
},
HIH6130: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "HIH6130", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// Page 3
// 5.0 Calculation of Optional Temperature
// from the Digital Output
//
// -40 C = 0
// 125 C = 2 ** 14 - 1
return round(raw / 1000);
}
}
},
DHT_I2C_NANO_BACKPACK: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "DHT_I2C_NANO_BACKPACK", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// Page 2
// 5. Product parameters
// Range: ... ±2°C
return round(raw / 100);
}
}
},
TH02: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "TH02", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// Page 8, Table 5
// Temperature Sensor
// Accuracy Typical at 25 °C — ±0.5 ±1.0 °C
return toFixed(raw, 1);
}
}
},
MPU6050: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "MPU6050", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// No sub-degree/fractional parts illustrated in datasheet
return round((raw / 340.00) + 36.53);
}
}
},
BNO055: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "BNO055", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// Page 37, Table 3-37
// Temperature data representation
// 1°C = 1 LSB
// raw is already C
return trunc(raw);
}
}
},
MPL115A2: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "MPL115A2", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// No description, so removing fractional parts
return trunc((raw - 498) / -5.35 + 25);
}
}
},
MPL3115A2: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "MPL3115A2", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// Page 5
// Table 2 Mechanical Characteristics
// Accuracy @ 25 °C ±1°C
return round(raw / 16);
}
}
},
MS5611: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "MS5611", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(raw) {
// Page 1
// TECHNICAL DATA
// Resolution <0.01 °C
return toFixed(raw, 2);
}
}
},
GROVE: {
initialize: {
value: analogHandler
},
toCelsius: {
value(raw) {
// http://www.seeedstudio.com/wiki/Grove_-_Temperature_Sensor
const adcres = 1023;
// Beta parameter
const beta = 3975;
// 10 kOhm (sensor resistance)
const rb = 10000;
// Ginf = 1/Rinf
// var ginf = 120.6685;
// Reference Temperature 25°C
const tempr = 298.15;
const rthermistor = (adcres - raw) * rb / raw;
const tempc = 1 / (log(rthermistor / rb) / beta + 1 / tempr) - CELSIUS_TO_KELVIN;
return round(tempc);
}
}
},
// MF52A103J3470
TINKERKIT: {
initialize: {
value: analogHandler
},
toCelsius: {
value(value) {
const adcres = 1023;
const beta = 3950;
const rb = 10000; // 10 kOhm
const ginf = 120.6685; // Ginf = 1/Rinf
const rthermistor = rb * (adcres / value - 1);
const tempc = beta / (log(rthermistor * ginf));
return round(tempc - CELSIUS_TO_KELVIN);
}
}
},
BMP180: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "BMP180", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(value) {
// Page 6, Table 1
// Operating conditions, output signal and mechanical characteristics
//
// Resolution of output data
// pressure 0.01 hPa
// temperature 0.1 °C
return toFixed(value, 1);
}
}
},
BMP280: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "BMP280", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(value) {
// Page 8
//
// Resolution of output data in ultra high resolution mode*
// Pressure 0.0016 hPa
// Temperature 0.01 °C
//
// * resolution mode is currently not configurable.
//
return toFixed(value, 2);
}
}
},
BME280: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "BME280", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(value) {
// Page 23
// Resolution is 0.01 DegC.
return toFixed(value, 2);
}
}
},
SI7020: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "SI7020", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(value) {
// Page 9, Table 5. Temperature Sensor
// Accuracy1 –10 °C< tA < 85 °C — ±0.3 ±0.4 °C
//
// Page 23
// (See temperature conversion expression)
return toFixed((175.72 * value / 65536) - 46.85, 1);
}
}
},
MCP9808: {
ADDRESSES: {
value: [0x18]
},
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
this.io.i2cConfig(options);
// Page 17
// Register 0x05 = Ta (Temp, Ambient)
this.io.i2cRead(address, 0x05, 2, data => {
// Page 24
// 5.1.3 AMBIENT TEMPERATURE REGISTER (TA)
// Page 25
let value = (((data[0] << 8) | data[1]) & 0x0FFF) / 16;
if (value & 0x1000) {
value -= 256;
}
callback(value);
});
}
},
toCelsius: {
value(value) {
// Page 1
// Microchip Technology Inc.s MCP9808 digital
// temperature sensor converts temperatures between
// -20°C and +100°C to a digital word with
// ±0.25°C/±0.5°C (typical/maximum) accuracy.
return toFixed(value, 2);
}
},
},
LSM303C: {
initialize: {
value(options, callback) {
const { Drivers } = require("./sip");
Drivers.get(this.board, "LSM303C", options)
.on("data", ({temperature}) => callback(temperature));
}
},
toCelsius: {
value(value) {
// int16 resolution, 8 bits per C, 0 = 25 C
return toFixed((value / 8) + 25, 1);
}
}
},
};
Controllers.BMP085 = Controllers.BMP180;
Controllers.GY521 = Controllers.MPU6050;
Controllers.SI7021 = Controllers.SI7020;
Controllers.DHT11_I2C_NANO_BACKPACK = Controllers.DHT_I2C_NANO_BACKPACK;
Controllers.DHT21_I2C_NANO_BACKPACK = Controllers.DHT_I2C_NANO_BACKPACK;
Controllers.DHT22_I2C_NANO_BACKPACK = Controllers.DHT_I2C_NANO_BACKPACK;
Controllers.DEFAULT = Controllers.ANALOG;
var priv = new Map();
class Thermometer extends Withinable {
constructor(options) {
super();
let last = null;
let raw = null;
Board.Component.call(
this, options = Board.Options(options)
);
Board.Controller.call(this, Controllers, options);
const state = {
enabled: typeof options.enabled === "undefined" ? true : options.enabled,
intervalId: null,
freq: options.freq || 25,
previousFreq: options.freq || 25,
};
priv.set(this, state);
// Analog Reference Voltage (default to board.io.aref || 5)
this.aref = options.aref || this.io.aref || 5;
if (!this.toCelsius) {
this.toCelsius = options.toCelsius || (x => x);
}
// TODO: Move this out of the constructor
const eventProcessing = () => {
if (raw == null) {
return;
}
const data = {};
data.C = data.celsius = this.celsius;
data.F = data.fahrenheit = this.fahrenheit;
data.K = data.kelvin = this.kelvin;
this.emit("data", data);
if (this.celsius !== last) {
last = this.celsius;
this.emit("change", data);
}
};
const descriptors = {
celsius: {
get() {
return this.toCelsius(raw);
}
},
fahrenheit: {
get() {
return toFixed((this.celsius * 9 / 5) + 32, 2);
}
},
kelvin: {
get() {
return toFixed(this.celsius + CELSIUS_TO_KELVIN, 2);
}
},
freq: {
get() {
return state.freq;
},
set(newFreq) {
state.freq = newFreq;
if (state.intervalId) {
clearInterval(state.intervalId);
}
if (state.freq !== null) {
state.intervalId = setInterval(eventProcessing, newFreq);
}
}
},
};
// Convenience aliases
descriptors.C = descriptors.celsius;
descriptors.F = descriptors.fahrenheit;
descriptors.K = descriptors.kelvin;
Object.defineProperties(this, descriptors);
if (typeof this.initialize === "function") {
this.initialize(options, data => raw = data);
}
// Set the freq property only after the get and set functions are defined
// and only if the sensor is not `enabled: false`
if (state.enabled) {
this.freq = state.freq;
}
}
/**
* enable Enable a disabled thermometer.
*
* @return {Object} instance
*
*/
enable() {
const state = priv.get(this);
/* istanbul ignore else */
if (!state.enabled) {
this.freq = state.freq || state.previousFreq;
}
return this;
}
/**
* disable Disable an enabled thermometer.
*
* @return {Object} instance
*
*/
disable() {
const state = priv.get(this);
/* istanbul ignore else */
if (state.enabled) {
state.enabled = false;
state.previousFreq = state.freq;
this.freq = null;
}
return this;
}
}
Thermometer.Drivers = Drivers;
/* istanbul ignore else */
if (!!process.env.IS_TEST_MODE) {
Thermometer.Controllers = Controllers;
Thermometer.purge = () => {
priv.clear();
};
}
module.exports = Thermometer;