This is a design for a sensor node powered by two AA cells. It includes the following sensors:
Sensor | Function | Interrupts? | Driver Library |
---|---|---|---|
Texas Instruments TMP102 | Temperature | ✔ | TMP1x2 |
Silicon Labs SI7021 | Temperature, Humidity | Si702x | |
Silicon Labs SI1145 | Ambient Light Sensor, Proximity Sensor | Si114x | |
STMicroelectronics LIS3DH | Accelerometer | ✔ | LIS3DH |
STMicroelectronics LPS25H | Air Pressure | ✔ | LPS25H |
STMicroelectronics LIS3MDL | Magnetometer | ✔ | LIS3MDL |
Nora uses a boost supply to provide a minimum voltage of 2.75V when WiFi is enabled. Since the imp can run user code down to 1.8V, and all of the sensors on the board have a minimum supply voltage of 1.9V or less, when WiFi isn’t needed the boost is disabled. This allows for additional power savings. An undervoltage lockout circuit will disable the device if the battery voltage falls below 1.9V, to prevent the device from continuously rebooting when the battery is low.
Battery voltage is also connected to a GPIO pin on the imp002 so it can be monitored.
Note The following was completed using a Nora version 3 design. The current design, version 5, should have equal or better battery life. This section will be updated once testing is complete.
To estimate battery life, we programmed a Nora to wake up, connect to Wi-Fi and report its battery voltage. We graphed the battery voltage over thousands of wakes to estimate the number of wake cycles you could anticipate with different types of batteries. In this test the boost voltage was set to 2.85V to maximize the number of wakes. As you can see, the Energizer L91 Lithium AA batteries provided the greatest number of wakes, then Duracell Alkaline and then Eneloop NiMH rechargable.
Note that wake-time will depend on many variables, including WiFi AP and network performance, and this will affect battery life. There have been further optimizations since these tests were run.
#require "Firebase.class.nut:1.1.1" | |
///////// Application Code /////////// | |
// VARIABLES | |
agentID <- split(http.agenturl(), "/").pop(); | |
// INITIALIZE CLASSES | |
const FIREBASENAME = "<--YOUR FIREBASE NAME-->"; | |
const FIREBASESECRET = "<--YOUR FIREBASE SECRET KEY-->"; | |
fb <- Firebase(FIREBASENAME, FIREBASESECRET); | |
// APPLICATION FUNCTIONS | |
// Save settings to local storage | |
function saveSettings(settings) { | |
server.save({ "settings" : settings }); | |
} | |
// Check local storage for settings and sync with device | |
function getSettings(dummy) { | |
local persist = server.load(); | |
// If no settings request from device | |
if (!("settings" in persist)) { device.send("getDeviceSettings", null); } | |
// If have settings send to device | |
if ("settings" in persist) { device.send("agentSettings", persist.settings); } | |
} | |
// Overwrite default reading/reporting interval settings is a table | |
function updateSettings(settings) { | |
local persist = server.load(); | |
if ("settings" in persist) { | |
if ( !("readingInt" in settings) ) { | |
settings.readingInt <- persist.settings.readingInt; | |
} | |
if ( !("reportingInt" in settings) ) { | |
settings.reportingInt <- persist.settings.reportingInt; | |
} | |
} | |
saveSettings(settings); | |
device.send("agentSettings", settings); | |
} | |
// Store data to Firebase | |
function storeData(data) { | |
foreach(sensor, readings in data) { | |
server.log(sensor + " " + http.jsonencode(readings)); | |
buildQue(sensor, readings, writeQueToFB) | |
} | |
device.send("ack", "OK"); | |
} | |
// Sort readings by timestamp | |
function buildQue(sensor, readings, callback) { | |
readings.sort(function (a, b) { return b.ts <=> a.ts }); | |
callback(sensor, readings); | |
} | |
// Loop that writes readings to db in order by timestamp | |
function writeQueToFB(sensor, que) { | |
if (que.len() > 0) { | |
local reading = que.pop(); | |
fb.push("/data/"+agentID+"/"+sensor, reading, null, function(res) { writeQueToFB(sensor, que); }.bindenv(this)); | |
} | |
} | |
// DEVICE LISTENERS | |
device.on("deviceSettings", saveSettings); | |
device.on("getAgentSettings", getSettings); | |
device.on("data", storeData); | |
// Uncomment lines below if you want to update reading, reporting intervals | |
// local newSettings = {"reportingInt" : 600}; | |
// updateSettings(newSettings); |
// Temperature/Humidity | |
#require "Si702x.class.nut:1.0.0" | |
// Light/Proximity | |
#require "Si114x.class.nut:1.0.0" | |
// Air Pressure | |
#require "LPS25H.class.nut:2.0.1" | |
// Magnetometer | |
#require "LIS3MDL.class.nut:2.0.0" | |
// Temperature | |
#require "TMP1x2.class.nut:1.0.3" | |
// Accelerometer | |
#require "LIS3DH.class.nut:1.2.0" | |
// Custom built class to handle the sensors on Nora. | |
// This class does not have all the functionality for each sensor implemented. | |
// Current functionality includes: | |
// All sensors can take readings | |
// Temperature sensor interrupt | |
// Accelerometer sensor free fall interrupt | |
// You should modify this class to suit your specific needs. | |
class HardwareManager { | |
// 8-bit left-justified I2C address for sensors on Nora | |
// Temp | |
static TMP1x2_ADDR = 0x92; | |
// Temp/Humid | |
static Si702x_ADDR = 0x80; | |
// Amb Light | |
static Si1145_ADDR = 0xC0; | |
// Accelerometer | |
static LIS3DH_ADDR = 0x32; | |
// Air Pressure | |
static LPS25H_ADDR = 0xB8; | |
// Magnetometer | |
static LIS3MDL_ADDR = 0x3C; | |
// Wake Pins for sensors on Nora | |
static TEMP_PIN = hardware.pinE; | |
static AMBLIGHT_PIN = hardware.pinD; | |
static ACCEL_PIN = hardware.pinB; | |
static AIR_PRESS_PIN = hardware.pinA; | |
static MAG_PIN = hardware.pinC; | |
static ALERT_PIN = hardware.pin1; | |
// Wake Pin Polarity if Event is Triggered | |
static TEMP_EVENT = 0; | |
static AMBLIGHT_EVENT = 0; | |
static ACCEL_EVENT = 1; | |
static AIR_PRESS_EVENT = 1; | |
static MAG_EVENT = 0; | |
static ALERT_EVENT = 1; | |
// LED red pin4 | |
// LED green pin3 | |
// Variables to store initialized sensors | |
_temp = null; | |
_tempHumid = null; | |
_ambLight = null; | |
_accel = null; | |
_airPress = null; | |
_mag = null; | |
_i2c = null; | |
constructor() { | |
_configureI2C(); | |
_initializeSensors(); | |
_configureWakePins(); | |
} | |
/////////// Private Functions //////////// | |
function _configureI2C() { | |
_i2c = hardware.i2c89; | |
_i2c.configure(CLOCK_SPEED_400_KHZ); | |
} | |
function _initializeSensors() { | |
_temp = TMP1x2(_i2c, TMP1x2_ADDR); | |
_tempHumid = Si702x(_i2c, Si702x_ADDR); | |
_ambLight = Si114x(_i2c, Si1145_ADDR); | |
_accel = LIS3DH(_i2c, LIS3DH_ADDR); | |
_airPress = LPS25H(_i2c, LPS25H_ADDR); | |
_mag = LIS3MDL(_i2c, LIS3MDL_ADDR); | |
} | |
function _configureWakePins() { | |
AIR_PRESS_PIN.configure(DIGITAL_IN); | |
ACCEL_PIN.configure(DIGITAL_IN); | |
MAG_PIN.configure(DIGITAL_IN); | |
AMBLIGHT_PIN.configure(DIGITAL_IN); | |
TEMP_PIN.configure(DIGITAL_IN); | |
ALERT_PIN.configure(DIGITAL_IN_WAKEUP); | |
// disable unused interrupts | |
ambLightDisableInterrupt(); | |
pressureDisableInterrupt(); | |
// mag needs to be configured | |
// for wake pins on nora to work properly | |
_mag.configureInterrupt(true); | |
} | |
///////// Sleep & Wake Functions ////////// | |
function setLowPowerMode() { | |
// ambLight low power mode | |
_ambLight.enableALS(false); | |
_ambLight.enableProximity(false); | |
_ambLight.setDataRate(0); | |
// mag | |
_mag.enable(false); | |
// TMP102 | |
if (_temp.getShutdown() == 0) _temp.setShutdown(1); | |
} | |
function configureSensors() { | |
configureAccel(); | |
configurePressure(); | |
configureMagnetometer(); | |
configureTemp(); | |
} | |
//////// Temp Sensor (TMP1x2) Functions //////// | |
function configureTemp() { | |
_temp.setActiveLow(); | |
// Shut sensor down until required to save power | |
_temp.setShutdown(1); | |
} | |
function tempRead(callback) { | |
_temp.read(function(result) { | |
if("err" in result) { | |
callback(result.err, null); | |
} else { | |
callback(null, {"temperature" : result.temp}); | |
} | |
}) | |
} | |
// opts format - {"mode" : "interrupt", "low" : 20, "high" : 30} | |
function tempConfigureInterrupt(opts) { | |
if ("mode" in opts) { | |
if (opts.mode == "comparator") { | |
_temp.setModeComparator(); | |
} | |
if (opts.mode == "interrupt") { | |
_temp.setModeInterrupt(); | |
} | |
} | |
if ("high" in opts) { | |
_temp.setHighThreshold(opts.high); | |
server.log("Temp high threshold set: " + _temp.getHighThreshold()); | |
} | |
if ("low" in opts) { | |
_temp.setLowThreshold(opts.low); | |
server.log("Temp low threshold set: " + _temp.getLowThreshold()); | |
} | |
} | |
///////// Temp/Humid Sensor Functions ///////// | |
function tempHumidRead(callback) { | |
_tempHumid.read(function(result) { | |
if ("err" in result) { | |
callback(result.err, null); | |
} else { | |
callback(null, result); | |
} | |
}); | |
} | |
//////// Light/Proximity Sensor Functions /////// | |
function lightRead(callback) { | |
_ambLight.enableALS(true); | |
_ambLight.forceReadALS(function(result) { | |
if ("err" in result) { | |
callback(result.err, null); | |
} else { | |
// result table contains: visible, ir and uv | |
callback(null, result); | |
} | |
}); | |
} | |
function proximityRead(callback) { | |
_ambLight.enableProximity(true); | |
_ambLight.forceReadProximity(function(result) { | |
if ("err" in result) { | |
callback(result.err, null); | |
} else { | |
callback(null, result) | |
} | |
}); | |
} | |
function ambLightDisableInterrupt() { | |
_ambLight.configureDataReadyInterrupt(false); | |
} | |
//////// Accelerometer Sensor Functions /////// | |
function configureAccel() { | |
_accel.init(); | |
_accel.enable(true); | |
_accel.setDataRate(50); | |
_accel.setLowPower(true); | |
} | |
function accelRead(callback) { | |
_accel.getAccel(function(result) { | |
if ("err" in result) { | |
callback(result.err, null); | |
} else { | |
callback(null, {"accelerometer": result}); | |
} | |
}); | |
} | |
function configureAccelFreeFallInterrupt(state, threshhold = 0.5, duration = 15) { | |
_accel.configureInterruptLatching(true); | |
_accel.configureFreeFallInterrupt(state, threshhold, duration); | |
} | |
function getAccelInterruptTable() { | |
return _accel.getInterruptTable(); | |
} | |
///////// Pressure Sensor ////////// | |
function configurePressure() { | |
_airPress.softReset(); | |
_airPress.enable(true); | |
} | |
function pressureRead(callback) { | |
_airPress.read(function(result) { | |
if ("err" in result) { | |
callback(result.err, null); | |
} else { | |
callback(null, result); | |
} | |
}); | |
} | |
function pressureDisableInterrupt() { | |
_airPress.configureInterrupt(false); | |
} | |
///////// Magnetometer Sensor ////////// | |
function configureMagnetometer() { | |
_mag.enable(true); | |
} | |
function magetometerRead(callback) { | |
_mag.readAxes(function(result) { | |
if ("err" in result) { | |
callback(result.err, null); | |
} else { | |
callback(null, {"magnetometer" : result}); | |
} | |
}); | |
} | |
} | |
// Custom built class to handle locaally stored data, | |
// wakeup, connection, and sending data. | |
// Constructor takes 2 parameters: | |
// 1st: reading interval - number of seconds between scheduled readings | |
// 2nd: reporting interval - number of seconds between scheduled | |
// connections to send data to the agent. | |
// You should modify this class to suit your specific needs. | |
class localDataManager { | |
readingInt = null; | |
reportingInt = null; | |
constructor(_reading, _reporting) { | |
readingInt = _reading; | |
reportingInt = _reporting; | |
_configureNV(); | |
} | |
function setReadingInt(newReadingInt) { | |
readingInt = newReadingInt; | |
} | |
function setReportingInt(newReportingInt) { | |
reportingInt = newReportingInt; | |
} | |
function getReadingInt() { | |
return readingInt; | |
} | |
function getReportingInt() { | |
return reportingInt; | |
} | |
function setNextWake() { | |
nv.nextWake <- (time() + readingInt); | |
} | |
function setNextConnect() { | |
nv.nextConnect <- (time() + reportingInt); | |
} | |
function readingTimerExpired() { | |
if (time() > nv.nextWake) { return true; } | |
return false | |
} | |
function reportingTimerExpired() { | |
if (time() > nv.nextConnect) { return true; } | |
return false | |
} | |
function storeData(sensorName, data) { | |
// add time stamp to data | |
data.ts <- time(); | |
// make sure sensor has a slot in nv | |
if (!(sensorName in nv.data)) { | |
nv.data[sensorName] <- []; | |
} | |
// add data to sensor's data array | |
nv.data[sensorName].push(data); | |
} | |
function sendData() { | |
agent.send("data", nv.data) | |
} | |
function clearNVReadings() { | |
nv.data <- {}; | |
} | |
function _configureNV() { | |
local root = getroottable(); | |
if ( !("nv" in root) ) { root.nv <- {}; } | |
if ( !("nextWake" in nv) ) { setNextWake(); } | |
if ( !("nextConnect" in nv) ) { setNextConnect(); } | |
if ( !("data" in nv) ) { nv.data <- {}; } | |
} | |
} | |
///////// Application Code /////////// | |
// CONSTANTS | |
const DEFAULT_READING_INTERVAL = 60; | |
const DEFAULT_REPORTING_INTERVAL = 300; | |
const READINGS_TIMEOUT = 2; | |
const BLINKUP_TIMEOUT = 10; | |
const TEMP_THRESH_LOW = 26; | |
const TEMP_THRESH_HIGH = 29; | |
// INITIALIZE CLASSES | |
nora <- HardwareManager(); | |
ldm <- localDataManager(DEFAULT_READING_INTERVAL, DEFAULT_REPORTING_INTERVAL); | |
// APPLICATION FUNCTIONS | |
// Temperature and Accelerometer Interrupts | |
function setUpInterrupts() { | |
nora.tempConfigureInterrupt({"mode" : "interrupt", "low" : TEMP_THRESH_LOW, "high" : TEMP_THRESH_HIGH}); | |
nora.configureAccelFreeFallInterrupt(true); | |
} | |
// Set up sensors | |
function setUpSensors() { | |
nora.configureSensors(); | |
setUpInterrupts(); | |
} | |
// Take readings from each sensor and store in NV | |
function takeReadings() { | |
nora.tempRead(function(err, reading) { | |
if (err) { server.log(err); } | |
ldm.storeData("tempSensor", reading); | |
}); | |
nora.tempHumidRead(function(err, reading) { | |
if (err) { server.log(err); } | |
ldm.storeData("tempHumidSensor", reading); | |
}); | |
nora.lightRead(function(err, reading) { | |
if (err) { server.log(err); } | |
ldm.storeData("ambLightSensor", reading); | |
}); | |
nora.proximityRead(function(err, reading) { | |
if (err) { server.log(err); } | |
ldm.storeData("ambLightSensor", reading); | |
}); | |
nora.accelRead(function(err, reading) { | |
if (err) { server.log(err); } | |
ldm.storeData("accelerometerSensor", reading); | |
}); | |
nora.pressureRead(function(err, reading) { | |
if (err) { server.log(err); } | |
ldm.storeData("pressureSensor", reading); | |
}); | |
nora.magetometerRead(function(err, reading) { | |
if (err) { server.log(err); } | |
ldm.storeData("magnetometerSensor", reading); | |
}); | |
} | |
// Update next Wake and Connect times | |
function setTimers() { | |
ldm.setNextWake(); | |
ldm.setNextConnect(); | |
} | |
// Put Imp into Low power state | |
// then sleep until next scheduled Wake time | |
function sleep() { | |
local timer = nv.nextWake - time(); | |
nora.setLowPowerMode(); | |
// put imp to sleep | |
server.log("going to sleep for " + timer + " sec"); | |
if (server.isconnected()) { | |
imp.onidle(function() { server.sleepfor(timer); }); | |
} else { | |
imp.deepsleepfor(timer); | |
} | |
} | |
// Check if time to take readings and/or connect | |
// takes a parameter boolean value | |
// if true then sleep after checks | |
function checkTimers(ready) { | |
// check reading timer | |
if(ldm.readingTimerExpired()) { | |
takeReadings(); | |
ldm.setNextWake(); | |
// wait for readings, then check reporting timer | |
imp.wakeup(READINGS_TIMEOUT, function() { | |
// check reporting | |
if(ldm.reportingTimerExpired()) { | |
ldm.sendData(); | |
ldm.setNextConnect(); | |
} else { | |
if (ready) { sleep(); } | |
} | |
}); | |
} else { | |
if (ready) { sleep(); } | |
} | |
} | |
// Store reading and reporting interval on agent | |
function sendSettings(dummy) { | |
local settings = {"readingInt" : ldm.readingInt, "reportingInt" : ldm.reportingInt}; | |
agent.send("deviceSettings", settings); | |
} | |
// Update reading and/or reporting intervals and update timers | |
function updateSettings(settings) { | |
if ("readingInt" in settings) { ldm.setReadingInt(settings.readingInt); } | |
if ("reportingInt" in settings) { ldm.setReportingInt(settings.reportingInt); } | |
setTimers(); | |
} | |
// Check which event triggered and store event to local storage | |
function checkEvents() { | |
// If alert Pin triggered check for events | |
if (nora.ALERT_PIN.read() == nora.ALERT_EVENT) { | |
if (nora.TEMP_PIN.read() == nora.TEMP_EVENT) { | |
server.log("temp event fired"); | |
nora.tempRead(function(err, reading) { | |
if (err) { server.log(err); } | |
if (reading.temperature > TEMP_THRESH_LOW) { | |
ldm.storeData("tempSensor", {"event" : "Temp Too High"}); | |
} else { | |
ldm.storeData("tempSensor", {"event" : "Temp Too Low"}); | |
} | |
}); | |
} | |
if (nora.AMBLIGHT_PIN.read() == nora.AMBLIGHT_EVENT) { | |
// not configured, so just log if triggered | |
server.log("proximity event fired"); | |
} | |
if (nora.ACCEL_PIN.read() == nora.ACCEL_EVENT) { | |
server.log("accelerometer event fired"); | |
local data = nora.getAccelInterruptTable(); | |
if (data.int1) { | |
local event = { "event" : "Free Fall"}; | |
ldm.storeData("accelerometerSensor", event); | |
} | |
} | |
if (nora.AIR_PRESS_PIN.read() == nora.AIR_PRESS_EVENT) { | |
// not configured, so just log if triggered | |
server.log("air pressure event fired"); | |
} | |
if (nora.MAG_PIN.read() == nora.MAG_EVENT) { | |
// not configured, so just log if triggered | |
server.log("magnetometer event fired"); | |
} | |
} | |
// Events are cleared - Setup sensors | |
setUpSensors(); | |
// Check if time for reading | |
// Sleep or check events again in 5s | |
if (nora.ALERT_PIN.read() == nora.ALERT_EVENT) { | |
checkTimers(false); | |
imp.wakeup(5, function() { checkEvents(); }); | |
} else { | |
checkTimers(true); | |
} | |
} | |
// AGENT LISTENERS | |
agent.on("agentSettings", updateSettings); | |
agent.on("getDeviceSettings", sendSettings); | |
agent.on("ack", function(res) { | |
ldm.clearNVReadings(); | |
sleep(); | |
}); | |
// WAKEUP LOGIC | |
switch(hardware.wakereason()) { | |
case WAKEREASON_TIMER: | |
server.log("WOKE UP B/C TIMER EXPIRED"); | |
setUpSensors(); | |
checkTimers(true); | |
break; | |
case WAKEREASON_PIN: | |
server.log("WOKE UP B/C ALERT PIN HIGH"); | |
checkEvents(); | |
break; | |
case WAKEREASON_POWER_ON: | |
server.log("COLD BOOT"); | |
setUpSensors(); | |
agent.send("getAgentSettings", null); | |
takeReadings(); | |
// Wait reasonable time for a blink up before going to sleep | |
imp.wakeup(BLINKUP_TIMEOUT, sleep); | |
break; | |
default: | |
server.log("WOKE UP B/C RESTARTED DEVICE, LOADED NEW CODE, ETC"); | |
setUpSensors(); | |
agent.send("getAgentSettings", null); | |
takeReadings(); | |
// Wait for readings then sleep | |
imp.wakeup(READINGS_TIMEOUT, sleep); | |
} |