UNPKG

@agilatech/bme280

Version:

Use either I2C or SPI to drive the Bosch BME280 Pressure / Temperature / Humidity sensor

646 lines (547 loc) 19.1 kB
module.exports = class Bme280 { constructor(options) { let opts = options || {}; this.device = {}; this.device.name = (opts.hasOwnProperty('name')) ? opts.name : 'Bme280'; this.device.type = (opts.hasOwnProperty('type')) ? opts.type : 'sensor'; this.device.active = false; this.device.interface = (opts.hasOwnProperty('interface')) ? opts.interface : 'i2c'; this.device.bus = (opts.hasOwnProperty('bus')) ? opts.bus : 1; this.device.addr = (opts.hasOwnProperty('addr')) ? opts.addr : 0x76; this.device.device = (opts.hasOwnProperty('device')) ? opts.device : 0; this.device.elevation = (opts.hasOwnProperty('elevation')) ? Number(opts.elevation) : 0; this.device.mode = (opts.hasOwnProperty('mode')) ? opts.mode : 'forced'; this.device.refresh = (opts.hasOwnProperty('refresh')) ? opts.refresh : 10000; this.device.version = require('./package.json').version; this.device.parameters = [ { name: 'pressure', type: 'float', value: NaN }, { name: 'temperature', type: 'float', value: NaN }, { name: 'humidity', type: 'float', value: NaN } ]; this.isStale = true; this.timer = null; this.mutex = false; if (this.device.interface == 'spi') { const spi = require('spi-device'); this.bus = spi.openSync(this.device.bus, this.device.device); } else { const i2c = require('i2c-bus'); this.bus = i2c.openSync(this.device.bus); } if (opts.hasOwnProperty('spiaddr') && (opts.spiaddr >= 0) && (opts.spiaddr < 8)) { this._init_multispi(opts); } this._initialize(); } deviceName() { return this.device.name; } deviceType() { return this.device.type; } deviceVersion() { return this.device.version; } deviceNumValues() { return this.device.parameters.length; } typeAtIndex(idx) { return this.device.parameters[idx].type; } nameAtIndex(idx) { return this.device.parameters[idx].name; } deviceActive() { return this.device.active; } getDataFromDevice() { return new Promise(async (resolve, reject) => { if (!this.device.active) { reject("Device not active"); } // the device is sleeping if it is in either sleep or forced mode, so we need // to wake it up before a measurement is taken by selecting forced mode if ((this.device.mode === 'sleep') || (this.device.mode === 'forced')) { await this.setMode('forced'); } // read the entire data block at once and pry out the values as we need them this._readRegisters(this.register.PRESSUREDATA, 8).then( buffer=> { this._setTemperature(Bme280.uint20(buffer[3], buffer[4], buffer[5])); this._setPressure(Bme280.uint20(buffer[0], buffer[1], buffer[2])); this._setHumidity(Bme280.uint16(buffer[6], buffer[7])); resolve(); }).catch(err => { reject(err); }); }); } valueAtIndex(idx) { return new Promise ((resolve, reject) => { if (!this._isIdxInRange(idx)) { reject(`Bme280 Error: index ${idx} out of range`); } // no need to fetch all parameters from the device every single time someone // wants to access a single value. So check to see if the data is stale... else if (this.isStale) { this.getDataFromDevice().then(() => { this._resetStaleTimer(); resolve(this.device.parameters[idx].value); }).catch(err => { reject(err); }); } else { resolve(this.device.parameters[idx].value); } }); } getValueByName(name) { return new Promise((resolve, reject) => { const regexParam = /pressure|temperature|humidity/; if (!regexParam.test(name)) { reject(`getValueByName error : unknown parameter '${name}'`); } var idx; this.device.parameters.forEach((param, index) => { if (param.name === name) { idx = index; } }); this.valueAtIndex(idx).then(val => { resolve(val); }).catch(err => { reject(err); }); }); } setMode(newMode) { const regexMode = /^sleep|forced|normal$/; if (regexMode.test(newMode)) { this.device.mode = newMode; } else { return; } const ctrl_meas = (this.sampling.X1 << 5) | (this.sampling.X1 << 3) | this.mode[this.device.mode]; this._writeRegister(this.register.CONTROL, ctrl_meas).then(async () => { // wait until measurement has been completed, // otherwise we would read the values from the last measurement while ((await this._readRegister(this.register.STATUS) & 0b1000) !== 0) { await this._sleep(4); } }).catch(err => { this._logError(`Could not set mode ${newMode}`); }); } reset() { this.isStale = true; clearTimeout(this.timer); this.timer = null; this.device.parameters.forEach(param => { param.value = NaN; }) this._initialize(); } _initialize() { this.device.active = false; this._loadConstants(); this._readChipId().then( async res => { // reset the device using soft-reset // this makes sure the IIR is off, etc. this._writeRegister(this.register.SOFTRESET, this.constant.RESET).then( async () => { // As per data sheet, startup time is 2 ms, so we'll double that await this._sleep(4); await this._setCalibration(); // read trimming parameters, see DS 4.2.2 this._setSampling(); // if chip is still reading calibraion, delay while (await this._isReadingCalibration()) { await this._sleep(112); } this.device.active = true; }).catch(err => { this._logError(err); }); }).catch( err => { let errdev = `i2c device on bus ${this.device.bus} with address ${this.device.addr}`; if (this.device.interface == 'spi') { errdev = `spi device ${this.device.bus}.${this.device.device}`; } this._logError(`Could not initialize ${errdev} : ${err}`); }); } _init_multispi(opts) { this.device.multispi = true; this.gpio = require('onoff').Gpio; // The following options are only used to activate multiple SPI devices using a // 3-to-8 line decoder demultiplexer similar to the 74HC138. If only a single device // is being controlled by this module, then these options can be ignored. // The spiaddr is an address from 0-7 which is used by the demultiplexer to activate // the corresponding output, which in turn activates the sensor's chip select. this.device.spiaddr = opts.spiaddr; // The spiselect is a GPIO pin which acts as a hardware mutex. It must be set high to enable // any one of the multiple sensors. Because of this, before a sensor can be selected, it // first must check to see that this GPIO is low and then set it high to lock the mutex. if (opts.hasOwnProperty('spiselect') && (opts.spiselect > 0)) { this.device.spimutex = new this.gpio(opts.spiselect, 'out'); // The spibits are used to address a single sensor. To do this, the GPIOs which // are physically connected to the demultiplexer chip must be defined here. if ((opts.hasOwnProperty('spibit0') && (opts.spibit0 > 0)) && (opts.hasOwnProperty('spibit1') && (opts.spibit1 > 0)) && (opts.hasOwnProperty('spibit2') && (opts.spibit2 > 0))) { this.device.spibit0 = new this.gpio(opts.spibit0, 'out'); this.device.spibit1 = new this.gpio(opts.spibit1, 'out'); this.device.spibit2 = new this.gpio(opts.spibit2, 'out'); const exitHandler = (evtName) => { process.on(evtName, _ => { // this is a bit dangerous, but helps to avoid a stuck mutex if (this.mutex) { this.device.spimutex.writeSync(0); } process.exit(0); }); } exitHandler('SIGINT'); exitHandler('SIGTERM'); exitHandler('SIGHUP'); } else { this._logError('Multiplex SPI failed due to incompatible gpio spibit definition'); this.device.multispi = false; } } else { this._logError('Multiplex SPI failed due to incompatible spiselect definition'); this.device.multispi = false; } } _readChipId() { return new Promise((resolve, reject) => { this._readRegister(this.register.CHIPID).then(chipId => { if (chipId == this.constant.CHIP_ID) { resolve(true); } else { reject(`Unexpected chip ID ${chipId.toString(16)}`); } }).catch(err => { reject(err); }); }); } _writeRegister(addr, data) { return new Promise( (resolve, reject) => { if (this.device.interface == 'spi') { const message = [{ sendBuffer: Buffer.alloc(2), byteLength: 2 }]; addr = addr & 0x7F; message[0].sendBuffer[0] = addr; message[0].sendBuffer[1] = data; this._selectspi().then(() => { this.bus.transfer(message, (err, mesg) => { this._deselectspi(); if (err) { reject(err); } else { resolve(); } }); }).catch(err => { reject(`Could not select spi: ${err}`); }); } else { this.bus.writeByte(this.device.addr, addr, data, (err) => { if (err) { reject(err); } else { resolve(); } }); } }); } _readRegister(addr) { return new Promise( (resolve, reject) => { this._readRegisters(addr, 1).then(buf => { resolve(buf.readUInt8(0)); }).catch( err => { reject(err); }); }); } _readRegisters(addr, len) { addr = addr | 0x80; return new Promise( (resolve, reject) => { if (this.device.interface == 'spi') { const message = [{ sendBuffer: Buffer.alloc(len+1), receiveBuffer: Buffer.alloc(len+1), byteLength: len+1 }]; message[0].sendBuffer[0] = addr; this._selectspi().then(() => { this.bus.transfer(message, (err, mesg) => { this._deselectspi(); if (err) { reject(err); } else { resolve(mesg[0].receiveBuffer.slice(1)); } }); }).catch(err => { reject(`Could not select spi: ${err}`); }); } else { this.bus.readI2cBlock(this.device.addr, addr, len, Buffer.alloc(len), (err, bytesRead, buffer) => { if (err) { reject(err); } else { resolve(buffer); } }); } }); } _selectspi() { return new Promise(resolve => { if (this.device.multispi) { if (this.device.spimutex.readSync() == 0) { this._activatespi(); resolve(); } else { var locktime = setInterval( () => { if (this.device.spimutex.readSync() == 0) { this._activatespi(); clearInterval(locktime); resolve(); } }, 200); } } else { resolve(); } }); } _deselectspi() { if (this.device.multispi) { this.device.spimutex.writeSync(0); this.mutex = false; } } _activatespi() { this.device.spibit0.writeSync(this.device.spiaddr & 1); this.device.spibit1.writeSync(this.device.spiaddr >>> 1 & 1); this.device.spibit2.writeSync(this.device.spiaddr >>> 2 & 1); this.mutex = true; this.device.spimutex.writeSync(1); } _setPressure(adc_P) { let var1 = this.t_fine / 2 - 64000; let var2 = var1 * var1 * this.calibration.dig_P6 / 32768; var2 = var2 + var1 * this.calibration.dig_P5 * 2; var2 = var2 / 4 + this.calibration.dig_P4 * 65536; var1 = (this.calibration.dig_P3 * var1 * var1 / 524288 + this.calibration.dig_P2 * var1) / 524288; var1 = (1 + var1 / 32768) * this.calibration.dig_P1; // need to avoid division by zero if (var1 !== 0) { let p = 1048576 - adc_P; p = ((p - var2 / 4096) * 6250) / var1; var1 = this.calibration.dig_P9 * p * p / 2147483648; var2 = p * this.calibration.dig_P8 / 32768; p = (p + (var1 + var2 + this.calibration.dig_P7) / 16) / 100; if (this.device.elevation > 0) { p = this._seaLevelPressure(p); } this.device.parameters[0].value = Math.round(p * 100) / 100; } else { this.device.parameters[0].value = NaN; // uh oh, we must be in deep space } } _seaLevelPressure(pressure_mb) { return pressure_mb * Math.pow((1 - ((0.0065 * this.device.elevation) / (this.device.parameters[1].value + 0.0065 * this.device.elevation + 273.15))), -5.257); } _setTemperature(adc_T) { let var1 = ((((adc_T >> 3) - (this.calibration.dig_T1 << 1))) * this.calibration.dig_T2) >> 11; let var2 = (((((adc_T >> 4) - this.calibration.dig_T1) * ((adc_T >> 4) - this.calibration.dig_T1)) >> 12) * this.calibration.dig_T3) >> 14; this.t_fine = var1 + var2; // Temperature is pretty simple this.device.parameters[1].value = Math.round(((this.t_fine * 5 + 128) >> 8) / 10) / 10; } _setHumidity(adc_H) { let var1 = this.t_fine - 76800; var1 = (adc_H - (this.calibration.dig_H4 * 64 + this.calibration.dig_H5 / 16384 * var1)) * (this.calibration.dig_H2 / 65536 * (1 + this.calibration.dig_H6 / 67108864 * var1 * (1 + this.calibration.dig_H3 / 67108864 * var1))); var1 = var1 * (1 - this.calibration.dig_H1 * var1 / 524288); const hum = (var1 > 100) ? 100 : (var1 < 0 ? 0 : var1); this.device.parameters[2].value = Math.round(hum * 10) / 10; } _resetStaleTimer() { this.isStale = false; if (this.timer == null) { this.timer = setTimeout(() => { this.isStale = true; clearTimeout(this.timer); this.timer = null; }, this.device.refresh); } } _sleep(millis) { return new Promise(resolve => setTimeout(resolve, millis)); } _logError(err) { console.error(`${this.device.name} ERROR: ${err}`); } _isIdxInRange(idx) { if ((idx < 0) || (idx >= this.device.parameters.length)) { return false; } return true; } async _setCalibration() { let buffer = await this._readRegisters(this.register.DIG_T1, 24); let h1 = await this._readRegister(this.register.DIG_H1); let h2_buf = await this._readRegisters(this.register.DIG_H2, 2); let h2 = Bme280.int16(h2_buf[1], h2_buf[0]); let h3 = await this._readRegister(this.register.DIG_H3); let h4 = await this._readRegister(this.register.DIG_H4); let h5 = await this._readRegister(this.register.DIG_H5); let h5_1 = await this._readRegister(this.register.DIG_H5 + 1); let h6 = await this._readRegister(this.register.DIG_H6); this.calibration = { dig_T1: Bme280.uint16(buffer[1], buffer[0]), dig_T2: Bme280.int16(buffer[3], buffer[2]), dig_T3: Bme280.int16(buffer[5], buffer[4]), dig_P1: Bme280.uint16(buffer[7], buffer[6]), dig_P2: Bme280.int16(buffer[9], buffer[8]), dig_P3: Bme280.int16(buffer[11], buffer[10]), dig_P4: Bme280.int16(buffer[13], buffer[12]), dig_P5: Bme280.int16(buffer[15], buffer[14]), dig_P6: Bme280.int16(buffer[17], buffer[16]), dig_P7: Bme280.int16(buffer[19], buffer[18]), dig_P8: Bme280.int16(buffer[21], buffer[20]), dig_P9: Bme280.int16(buffer[23], buffer[22]), dig_H1: h1, dig_H2: h2, dig_H3: h3, dig_H4: (h4 << 4) | (h5 & 0xF), dig_H5: (h5_1 << 4) | (h5 >> 4), dig_H6: h6 } } _setSampling() { // TODO: allow sampling and standby parameters to be user-configured const ctrl_hum = this.sampling.X1; const config = (this.standby.MS_1000 << 5) | (this.filter.X1 << 3); // pressure sampling temperature sampling const ctrl_meas = (this.sampling.X1 << 5) | (this.sampling.X1 << 3) | this.mode[this.device.mode]; // you must make sure to also set register.CONTROL after setting the // CONTROLHUMID register, otherwise the values won't be applied (see DS 7.4.3) this._writeRegister(this.register.CONTROLHUMID, ctrl_hum).then( ()=> { this._writeRegister(this.register.CONFIG, config).then( () => { this._writeRegister(this.register.CONTROL, ctrl_meas).catch(err => { this._logError('setSampling register.CONTROL error'); }); }).catch(err => { this._logError('setSampling register.CONFIG error'); }); }).catch(err => { this._logError('setSampling register.CONTROLHUMID error'); }); } _isReadingCalibration() { return new Promise(resolve => { this._readRegister(this.register.STATUS).then(async status => { resolve((status & 1)); }); }); } _loadConstants() { this.register = { DIG_T1: 0x88, //Bme280_TEMP_PRESS_CALIB_DATA_ADDR DIG_T2: 0x8A, DIG_T3: 0x8C, DIG_P1: 0x8E, DIG_P2: 0x90, DIG_P3: 0x92, DIG_P4: 0x94, DIG_P5: 0x96, DIG_P6: 0x98, DIG_P7: 0x9A, DIG_P8: 0x9C, DIG_P9: 0x9E, DIG_H1: 0xA1, DIG_H2: 0xE1, //Bme280_HUMIDITY_CALIB_DATA_ADDR DIG_H3: 0xE3, DIG_H4: 0xE4, DIG_H5: 0xE5, DIG_H6: 0xE7, CHIPID: 0xD0, VERSION: 0xD1, SOFTRESET: 0xE0, CAL26: 0xE1, // R calibration stored in 0xE1-0xF0 CONTROLHUMID: 0xF2, //Bme280_CTRL_HUM_ADDR STATUS: 0XF3, CONTROL: 0xF4, //Bme280_PWR_CTRL_ADDR Bme280_CTRL_MEAS_ADDR CONFIG: 0xF5, //Bme280_CONFIG_ADDR PRESSUREDATA: 0xF7, //Bme280_DATA_ADDR TEMPDATA: 0xFA, // 0xF7 to 0xFE is burst for temp, pres, and hum HUMIDDATA: 0xFD } this.sampling = { NONE: 0b000, X1: 0b001, X2: 0b010, X4: 0b011, X8: 0b100, X16: 0b101 }; this.mode = { sleep: 0b00, forced: 0b01, normal: 0b11 }; this.filter = { OFF: 0b000, X2: 0b001, X4: 0b010, X8: 0b011, X16: 0b100 }; // inactive duration (standby time) in normal mode this.standby = { MS_0_5: 0b000, // 000 = 0.5 ms MS_62_5: 0b001, // 001 = 62.5 ms MS_125: 0b010, // 010 = 125 ms MS_250: 0b011, // 011 = 250 ms MS_500: 0b100, // 100 = 500 ms MS_1000: 0b101, // 101 = 1000 ms MS_10: 0b110, // 110 = 10 ms MS_20: 0b111 // 111 = 20 ms }; this.constant = { CHIP_ID: 0x60, RESET: 0xB6 } } static int16(msb, lsb) { let val = Bme280.uint16(msb, lsb); return val > 32767 ? (val - 65536) : val; } static uint16(msb, lsb) { return msb << 8 | lsb; } static uint20(msb, lsb, xlsb) { return ((msb << 8 | lsb) << 8 | xlsb) >> 4; } }