Skip to main content

Nora

The imp002 is no longer available


 

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

Power

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.

Battery Life

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.

Device Firmware and Example Code

#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);
}

Hardware Design Files