Skip to content

Instantly share code, notes, and snippets.

@betzrhodes
Last active October 14, 2015 23:02
Show Gist options
  • Save betzrhodes/21e0b8518d2472bdcc89 to your computer and use it in GitHub Desktop.
Save betzrhodes/21e0b8518d2472bdcc89 to your computer and use it in GitHub Desktop.
Nora Basic Example

#Nora Basic Example

##Overview

Nora is a low power sensor module that runs off 2 AA batteries. It includes 6 sensors that all have basic driver code written for use with the Electric Imp platform.

Sensor Function Interrupt Driver Code
Texas Instruments TMP102 Temperature Yes TMP1x2
Silicon Labs SI7021 Temperature, Humidity No Si702x
Silicon Labs SI1145 Ambient Light, Proximity No Si114x
STMicroelectronics LIS3DH Accelerometer Yes LIS3DH
STMicroelectronics LPS25H Air Pressure Yes LPS25H
STMicroelectronics LISMDL Magnetometer Yes LIS3MDL

This example code is meant to be a starting point for Nora users. As written this example takes readings, listens for events, and uses Firebase to store the data collected. To conserve power the device is put to sleep between reading cycles. If an event happens the imp is woken up to record that event. After a set ammount of time the device connects to the server and uploads the collected data which is then written to Firebase.

To get started you will need a Nora with two double AA batteries, an Electric Imp account, and a Firebase account. Copy and paste the example agent and device code into a new model in the Electric Imp IDE, and assign the Nora to that model. Create an app in Firebase. You will need to set the FIREBASENAME & FIREBASESECRET in the agent code to match your Firebase app name and Firebase secret (found under the secrets tab in your app). Hit Build and Run in the IDE to load the code, and your Nora will be up and running.

All device and agent code in this example should be adjusted to suit specific applications. This example is for getting started quickly. Please note, if you decide not to use all the sensors, but are using interrupts you must configure the magnetometer. For the wake pin to work properly on Nora, the magnetometer must be initialized and the interrupt pin set to active low.

Magnetometer Setup:

 // Must do this for Interrupt Pins to work on Nora
 
 // Magnetometer 
 #require "LIS3MDL.class.nut:1.0.1"

 i2c <- hardware.i2c89;
 i2c.configure(CLOCK_SPEED_400_KHZ);
 LIS3MDL_ADDR <- 0x3C;
 
 // Initialize magnetometer
 mag <- LIS3MDL(i2c, LIS3MDL_ADDR);
 // configures magnetometer interrupt active low
 mag.configureInterrupt(true);

##Device Code

###Require Libraries The device code utilizes a number of Electric Imp's libraries. To add a library to your porject you must require it at the top of your device code. This example uses libraries for each of the sensors on Nora (see the chart in the overview section above) and Bullwinkle, a framework for asynchronous agent and device communication.

###Management Classes For readability this example code will also use two custom built management classes that can be adjusted for different applications.

####HardwareManager
This is a custom built class to handle the sensors on Nora. You should modify this class to suit your specific needs.

Current functionality includes:

  • Sensor configuration
  • Interrupt configuration
  • Sensor readings
  • Low power & Wake management

#####Constructor This class requires driver code for all the sensors on Nora, since the constructor initializes all sensors.

 // 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.0"
 // Magnetometer
 #require "LIS3MDL.class.nut:1.0.1"
 // Temperature
 #require "TMP1x2.class.nut:1.0.3"
 // Accelerometer
 #require "LIS3DH.class.nut:1.0.2"

 nora <- HardwareManager();

#####Configuration and Power Management ######setLowPowerMode(interruptsState) Use before putting the device to sleep, to put sensors into a low power state. This funciton takes one parameter, a table showing if the interrupt is enabled for each sensor. This function will disable continous mode for sensors that do not have an interrupt enabled.

local interrupts = {"temp": true, "accel": true, "pressure": false, "mag": false};
nora.setLowPowerMode(interrupts);

######onWake(interrupts) Use when device wakes from sleep, to put sensors into a mode where readings can be taken. This funciton takes one parameter, a table showing if the interrupt is enabled for each sensor. This function will enable sensors that were disabled using #setLowPowerMode.

local interrupts = {"temp": true, "accel": true, "pressure": false, "mag": false};
nora.onWake(interrupts);

######configureSensors() Use when device boots to configure sensor settings.

#####Sensors

#####TMP102 - Temperature Sensor w/ Interrupt

######tempRead(callback) Takes a reading and passes the result to the callback. The callback takes two parameters, error and result. If no error has occured err will be null and the result will be a table with a "temperature" slot containing the temperature reading in celsius.

######tempConfigureInterrupts(opts) Uses the options table passed in to set up the temperature interrupt. The options table should contain the following slots: mode set to interrupt or comparator, low set to the low threshold in celsius, and high set to the high threshold in celsius.

local opts = {"mode" : "interrupt", "low" : 20, "high" : 30};
nora.tempConfigureInterrupts(opts);

######tempSetLowPower() Puts sensor into a low power sleep mode. Note that calling this function will turn off continuous mode, and the interrupt will no longer wake the device.

######tempWakeFromLowPower() Puts sensor into continous converstion state.

#####SI7021 - Temperature and Humidity Sensor

######tempHumidRead(callback) Takes a reading and passes the result to the callback. The callback takes two parameters, error and result. If no error has occured err will be null and the result will be a table with a "temperature" slot containing the temperature reading in celsius and a "humidity" slot containing the relative humidity.

#####SI1145 - Ambient Light and Proximity Sensor

######lightRead(callback) Takes a reading and passes the result to the callback. The callback takes two parameters, error and result. If no error has occured err will be null and the result will be a table with a "visible" slot containing a visible light reading in lux, an "ir" slot containing the ir light reading in lux, and a "uv" slot containing the uv index.

######proximityRead(callback) Takes a reading and passes the result to the callback. The callback takes two parameters, error and result. If no error has occured err will be null and the result will be a table with a "proximity" slot containing a proximity reading with a value between 0 (far) to 65535 (near).

######alsSetLowPower() Puts sensor into a low power mode by disabling autonomous mode, light and proximity sensors.

#####LIS3DH - Accelerometer Sensor w/ Interrupt

######configureAccel() Configures the accelerometer with basic settings.

######accelRead(callback) Gets the latest reading and passes it to the callback. The callback takes two parameters, error and result. If no error has occured err will be null and the result will be a table with an "accelerometer" slot containing a table with x, y and z axis readings.

######accelConfigureFreeFallInterrupt(state, [threshold, duration]) Enables or disables free fall interrupt. Set state to true to enable, false to disable. Optional parameters can be passed in to change the threshold in Gs (default = 0.5) and duration (number of samples, default = 15) to trigger interrupt. Interrupt is set to latch, so event can be determined upon wake.

######accelGetInterruptTable() Returns a table that containing information on interrupt. After an accelerometer event this function must be called to release the interrupt latch.

{
    "int1": bool,           // true if INT1 created the interrupt
    "xLow": bool,           // true if a xLow condition is present
    "yLow": bool,           // true if a yLow condition is present
    "zLow": bool,           // true if a zLow condition is present
    "xHigh": bool,          // true if a xHigh condition is present
    "yHigh": bool,          // true if a yHigh condition is present
    "zHigh": bool,          // true if a zHigh condition is present
    "click": bool,          // true if any click created the interrupt
    "singleClick": bool,    // true if a single click created the interrupt
    "doubleClick": bool     // true if a double click created the interrupt
}

######accelSetLowPower() Puts sensor into a low power mode by disabling sensor. Note after calling this function the interrupt will no longer wake the device.

######accelWakeFromLowPower() Enables the sensor, and resets the data rate.

#####LPS25H - Air Pressure Sensor w/ Interrupt

######configurePressure()
Configures the air pressure sensor with basic settings.

######pressureRead(callback) Takes a reading and passes the result to the callback. The callback takes two parameters, error and result. If no error has occured err will be null and the result will be a table with a "pressure" slot containing a pressure reading in hPa.

######pressureConfigureInterrupt(state, [threshold, options]) Enables or disables air pressure interrupt. Set state to true to enable, false to disable. Optional parameters can be passed in to set the threshold in hPa (default - no threshold set) and options (default - no options set). Options are an array containing the string "high" if you want to trigger an event when a pressure reading is above the default no threshold set, and/or "low" if you want to trigger an events when a pressure reading is below the threshold. Interrupt is set to latch, so event can be determined upon wake.

######pressureGetInterruptTable() Returns a table that containing information on interrupt. After a pressure event this function must be called to release the interrupt latch.

key Description Notes
int_active true if an interrupt is currently active or latched
high_pressure true if the active or latched interrupt was due to a high pressure event
low_pressure true if the active or latched interrupt was due to a low pressure event

######pressureSetLowPower() Disables the sensor. No readings can be taken if sensor is disabled.

######pressureWakeFromLowPower() Enables the sensor. Sensor can take readings.

#####LISMDL - Magnetometer Sensor w/ Interrupt

######configureMagnetometer()
Configures the magnetometer with basic settings.

######magetometerRead(callback) Takes a reading and passes the result to the callback. The callback takes two parameters, error and result. If no error has occured err will be null and the result will be a table with a "magnetometer" slot containing a table with x, y and z axis readings.

######magnetometerConfigureInterrupt(state, [threshold, axes]) Enables or disables magnetometer interrupt. Set state to true to enable, false to disable. Optional parameters can be passed in to set the threshold and the axes. By default no threshold or axes are set. The threshold is an absolute value. An event will trigger if reading is above the positive or below the negative value of the threshold. Axes is an array containing the string "x", "y, and/or "z", any axis included in this array will trigger an event for that axis. Interrupt is set to latch, so event can be determined upon wake.

######magnetometerGetInterruptTable() Returns a table that containing information on interrupt. After a magnetometer event this function must be called to release the interrupt latch.

Key Description
x_positive The X-axis value exceeded the threshold on the positive side.
x_negative The X-axis value exceeded the threshold on the negative side.
y_positive The Y-axis value exceeded the threshold on the positive side.
y_negative The Y-axis value exceeded the threshold on the negative side.
z_positive The Z-axis value exceeded the threshold on the positive side.
z_negative The Z-axis value exceeded the threshold on the negative side.
overflow A value overflowed the internal measurement range.
interrupt Value is the interrupt pin state.

######magSetLowPower() Disables the sensor. No readings can be taken if sensor is disabled.

######magWakeFromLowPower() Enables the sensor. Sensor can take readings.

####LocalDataManager
This is a custom built class to handle data storage on the device. You should modify this class to suit your specific needs. This class uses the NV table to persist data between wakeups. Data stored in NV does not persist if device is rebooted.

#####Constructor The class constructor takes two parameters, a reading interval (the number of seconds between scheduled readings) and a reporting interval (number of seconds between scheduled connections to the server).

local READING_INTERVAL = 60;
local REPORTING_INTERVAL = 300;

ldm <- LocalDataManager(READING_INTERVAL, REPORTING_INTERVAL);

#####Connection and Wake

######setReadingInt(newReadingInt) Sets the reading interval to the one passed in.

######setReportingInt(newReportingInt) Sets the reporting interval to the one passed in.

######getReadingInt() Returns the reading interval.

######getReportingInt() Returns the reporting interval.

######setNextWake() Uses the reading interval to store the timestamp for the next reading.

######setNextConnect() Uses the reporting interval to store the timestamp for the next connection.

######readingTimerExpired() Returns a boolean. True if current time is after the stored wake timestamp, otherwise false.

######reportingTimerExpired() Returns a boolean. True if current time is after the stored connection timestamp, otherwise false.

#####Data storage

######storeData(sensorName, Data) Takes the name of the sensor and the sensor's data and stores them.

######getData() Returns all stored sensor data.

######clearNVReadings() Erases all stored sensor data.

######storeInterruptState(interrupts) Stores the interrupt state table.

######getInterruptState() Returns the interrupt state table.

###Application Code Here is where we use bullwinkle and the management classes to collect readings, listen for events, and send the data collected to the agent. This code should be modified for your specific application needs.

#####Application Functions

######bootSensorSetup() Configures sensors & enables interrupts. This funciton should be called anytime the device boots.

######wakeSensorSetup() Configures sensors to take readings. This should be called before taking sensor readings after waking from sleep.

######enableInterrupts() Configures interrupts and stores a table with the interrupt state (enabled = true, disabled = false). This is called from #bootSensorSetup.

######checkTimers() Checks to see if it is time to take readings or connect. If timer expired then takes readings and/or connects, and resets timer(s). If no timer is expired then puts the device to sleep. This should be called when the device is woken by a timer or event.

######checkEvents() Checks to see which event pin(s) are triggered. Stores record of that event. Checks wake timers & sleeps if event pin has been cleared. This should be called when the device is woken by the alert pin.

######takeReadings(connect) Wakes sensors and takes readings from each. This method takes a boolean parameter that determines if device should connect(true) or sleep(false) when readings are completed. This should be called when device boots or wakes after timer.

######readingsDone(connect) Loops until all readings have completed or reading timer has expired. This method takes a boolean parameter that determines if device should connect(true) or sleep(false) when loop completes. This is called from #takeReadings.

######sendData() Sends stored sensor data to the agent. When agent has acknowleged receipt, sensor data storage is cleared and device is put to sleep. This is called when the connection timer expires, from #checkTimers.

######onWake(wakeReason) Determines action based on given a wake reason. This should be called to start the application.

######sleep() Puts the sensors into low power mode then sleeps if no event is triggered. This is called from a number of application functions.

##Agent Code

###Require Libraries To add a library to your porject you must require it at the top of your agent code. This example uses the Firebase library, for cloud based data storage, and the Bullwinkle library, a framework for asynchronous agent and device communication.

###Application Code This example code listens for data from the device and then stores that data to firebase. Set the Firebase constants to your Firebase app name and secret.

#####Application Functions

######storeData(msg, reply) Extracts data sent from device, logs it, and queue's it to be written to Firebase. Acknowleges receipt of data.

######buildQue(sensor, readings, callback) Sorts a sensor's data by timestamp, and passes sorted data to #writeQueToFB.

######writeQueToFB(sensor, que) A loop that pushes the sorted data to FB one item at a time to ensure the sorted order.

The MIT License (MIT)
Copyright (c) 2015 Electric Imp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
// Firebase Database
#require "Firebase.class.nut:1.0.0"
// Agent/Device Communications Helper
#require "bullwinkle.class.nut:2.0.0"
// Constants
const FIREBASENAME = "<-- Your Firebase Name -->";
const FIREBASESECRET = "<-- Your Firebase Secret Key -->";
///////// Application Code ///////////
///// VARIABLES
local agentID = split(http.agenturl(), "/").pop();
///// INITIALIZE CLASSES
local bull = Bullwinkle();
local fb = Firebase(FIREBASENAME, FIREBASESECRET);
///// FIREBASE STORAGE FUNCTIONS
// Store data to Firebase
function storeData(msg, reply) {
foreach(sensor, readings in msg.data) {
server.log(sensor + " " + http.jsonencode(readings));
buildQue(sensor, readings, writeQueToFB)
}
reply("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 Firebase 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
bull.on("data", storeData);
// 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.0"
// Magnetometer
#require "LIS3MDL.class.nut:1.0.1"
// Temperature
#require "TMP1x2.class.nut:1.0.3"
// Accelerometer
#require "LIS3DH.class.nut:1.0.2"
// Agent/Device Communications Helper
#require "bullwinkle.class.nut:2.0.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
// Magnetometer sensor interrupt
// Pressure Sensor 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 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 ACCEL_EVENT = 1;
static AIR_PRESS_EVENT = 1;
static MAG_EVENT = 0;
static ALERT_EVENT = 1;
// Variables to store initialized sensors
_temp = null;
_tempHumid = null;
_ambLight = null;
_accel = null;
_airPress = null;
_mag = null;
_i2c = null;
constructor() {
_configureI2C();
_configureWakePins();
_initializeSensors();
}
/////////// 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);
TEMP_PIN.configure(DIGITAL_IN);
ALERT_PIN.configure(DIGITAL_IN_WAKEUP);
}
///////// Config, Sleep & Wake Functions //////////
// Set LowPower Before Sleep
function setLowPowerMode(interrupts) {
alsSetLowPower();
if (!("temp" in interrupts) || !interrupts.temp) { tempSetLowPower(); }
if (!("accel" in interrupts) || !interrupts.accel) { accelSetLowPower(); }
if (!("pressure" in interrupts) || !interrupts.pressure) { pressureSetLowPower(); }
if (!("mag" in interrupts) || !interrupts.mag) {magSetLowPower(); }
}
function onWake(interrupts) {
if (!("temp" in interrupts) || !interrupts.temp) { tempWakeFromLowPower(); }
if (!("accel" in interrupts) || !interrupts.accel) { accelWakeFromLowPower(); }
if (!("pressure" in interrupts) || !interrupts.pressure) { pressureWakeFromLowPower(); }
if (!("mag" in interrupts) || !interrupts.mag) { magWakeFromLowPower(); }
}
// Sensor setup on warm & cold boot only
function configureSensors() {
// Sensor settings
configureMagnetometer();
configureAccel();
configurePressure();
}
//////// Temp sensor Functions ////////
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) {
_temp.setActiveLow();
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());
}
}
function tempSetLowPower() {
_temp.setShutdown(1);
}
function tempWakeFromLowPower() {
_temp.setShutdown(0);
}
///////// 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 alsSetLowPower() {
_ambLight.enableALS(false);
_ambLight.enableProximity(false);
_ambLight.setDataRate(0);
}
//////// 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 accelConfigureFreeFallInterrupt(state, threshold = 0.5, duration = 15) {
_accel.configureInterruptLatching(true);
_accel.configureFreeFallInterrupt(state, threshold, duration);
}
function accelGetInterruptTable() {
return _accel.getInterruptTable();
}
function accelSetLowPower() {
_accel.setDataRate(0);
_accel.enable(false);
}
function accelWakeFromLowPower() {
_accel.setDataRate(50);
_accel.enable(true);
}
///////// 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 pressureConfigureInterrupt(state, threshold=null, opts=0) {
if (state) {
_airPress.setDataRate(1);
_airPress.configureInterrupt(state, threshold, _buildOpts(opts));
} else {
_airPress.setDataRate(0);
_airPress.configureInterrupt(state);
}
}
function pressureGetInterruptTable() {
return _airPress.getInterruptSrc();
}
function pressureSetLowPower() {
_airPress.enable(false);
}
function pressureWakeFromLowPower() {
_airPress.enable(true);
}
function _buildOpts(opts) {
local high = opts.find("high") != null;
local low = opts.find("low") != null;
if (high && low) {
return LPS25H.INT_LATCH | LPS25H.INT_HIGH_PRESSURE | LPS25H.INT_LOW_PRESSURE;
} else if (high) {
return LPS25H.INT_LATCH | LPS25H.INT_HIGH_PRESSURE;
} else if (low) {
return LPS25H.INT_LATCH | LPS25H.INT_LOW_PRESSURE;
} else {
return 0;
}
}
///////// Magnetometer Sensor //////////
function configureMagnetometer() {
_mag.enable(true);
_mag.setLowPower(true);
// sets mag interrupt pin low, but will not trigger events
// without this Alert Pin will always be asserted
_mag.configureInterrupt(true);
}
function magetometerRead(callback) {
_mag.readAxes(function(result) {
if ("err" in result) {
callback(result.err, null);
} else {
callback(null, {"magnetometer" : result});
}
});
}
function magnetometerConfigureInterrupt(state, threshold, axes) {
if (state) {
_mag.setConversionType(LIS3MDL.CONVERSION_TYPE_CONTINUOUS);
_mag.configureInterrupt(state, threshold, _setAxisListeners(axes));
} else {
_mag.setConversionType(LIS3MDL.CONVERSION_TYPE_SINGLE);
_mag.configureInterrupt(true);
}
}
function magnetometerGetInterruptTable() {
return _mag.readInterruptStatus();
}
function magSetLowPower() {
_mag.enable(false);
}
function magWakeFromLowPower() {
_mag.enable(true);
}
function _setAxisListeners(axes) {
local x = axes.find("x") != null;
local y = axes.find("y") != null;
local z = axes.find("z") != null;
if (x && y && z) {
return LIS3MDL.AXIS_X | LIS3MDL.AXIS_Y | LIS3MDL.AXIS_Z;
} else if (x && y) {
return LIS3MDL.AXIS_X | LIS3MDL.AXIS_Y;
} else if (x && z) {
return LIS3MDL.AXIS_X | LIS3MDL.AXIS_Z;
} else if (y && z) {
return LIS3MDL.AXIS_Y | LIS3MDL.AXIS_Z;
} else if (x) {
return LIS3MDL.AXIS_X;
} else if (y) {
return LIS3MDL.AXIS_Y;
} else if (z) {
return LIS3MDL.AXIS_Z;
} else {
return 0x00;
}
}
}
// Custom built class to handle locally stored data.
// Constructor takes 2 parameters:
// 1st: reading interval - number of seconds between scheduled readings
// 2nd: reporting interval - number of seconds between scheduled
// connections that 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 getData() {
return nv.data;
}
function storeInterruptState(interrupts) {
nv.interrupts <- interrupts;
}
function getInterruptState() {
return nv.interrupts;
}
function clearNVReadings() {
nv.data <- {};
}
function _configureNV() {
local root = getroottable();
if ( !("nv" in root) ) { root.nv <- {}; }
if ( !("data" in nv) ) { nv.data <- {}; }
if ( !("interrupts" in nv) ) { nv.interrupts <- {}; }
if ( !("nextWake" in nv) ) { setNextWake(); }
if ( !("nextConnect" in nv) ) { setNextConnect(); }
}
}
///////// Application Code ///////////
// Constants
const READING_INTERVAL = 60;
const REPORTING_INTERVAL = 300;
const READINGS_TIMEOUT = 2;
const BLINKUP_TIMEOUT = 10;
// Interrupt thresholds
const TEMP_THRESH_LOW = 26;
const TEMP_THRESH_HIGH = 29;
const MAG_THRESH = 2;
const PRESS_THRESH = 1300;
///// VARIABLES
local readingsCounter = 0;
local numReadings = 7;
local readingsTimer = null;
local readingsFlag = false;
local eventFlag = false;
///// INITIALIZE CLASSES
local bull = Bullwinkle();
local nora = HardwareManager();
local ldm = LocalDataManager(READING_INTERVAL, REPORTING_INTERVAL);
///// APPLICATION FUNCTIONS
// Configure Sensors
function bootSensorSetup() {
nora.configureSensors();
enableInterrupts();
}
// Prep sensors for readings
function wakeSensorSetup() {
nora.onWake(ldm.getInterruptState());
}
// Temperature and Accelerometer Interrupts
function enableInterrupts() {
// Configure interrupts
nora.tempConfigureInterrupt({"mode" : "interrupt", "low" : TEMP_THRESH_LOW, "high" : TEMP_THRESH_HIGH});
nora.accelConfigureFreeFallInterrupt(true);
nora.pressureConfigureInterrupt(true, PRESS_THRESH, ["high"]);
// nora.magnetometerConfigureInterrupt(true, MAG_THRESH, ["x", "y", "z"]);
// Store state of enabled interrupts
// if true - intertupt configured, so sensor stays in low power continuous mode between wakes
// if false - interrupt not configured, so sensor shuts off between wakes
ldm.storeInterruptState({"temp" : true, "accel": true, "mag": false, "pressure": true});
}
// Check if time to take readings and/or connect
function checkTimers() {
if (ldm.reportingTimerExpired()) {
takeReadings(true);
ldm.setNextWake();
ldm.setNextConnect();
} else if (ldm.readingTimerExpired()) {
takeReadings();
ldm.setNextWake();
} else {
sleep();
}
}
// Check events and store triggered event(s) locally
function checkEvents() {
// If alert pin asserted 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.ACCEL_PIN.read() == nora.ACCEL_EVENT) {
// server.log("accelerometer event fired");
local data = nora.accelGetInterruptTable();
if (data.int1) {
local event = { "event" : "Free Fall"};
ldm.storeData("accelerometerSensor", event);
}
}
if (nora.AIR_PRESS_PIN.read() == nora.AIR_PRESS_EVENT) {
// server.log("air pressure event fired");
local data = nora.pressureGetInterruptTable();
if (data.high_pressure) {
local event = {"event" : "High Pressure Event"};
ldm.storeData("pressureSensor", event);
}
if (data.low_pressure) {
local event = {"event" : "Low Pressure Event"};
ldm.storeData("pressureSensor", event);
}
}
if (nora.MAG_PIN.read() == nora.MAG_EVENT) {
// server.log("magnetometer event fired");
local data = nora.magnetometerGetInterruptTable();
if (data.x_positive || data.x_negative) {
local event = { "event" : "X-axis event"};
ldm.storeData("magnetometerSensor", event);
}
if (data.y_positive || data.y_negative) {
local event = { "event" : "Y-axis event"};
ldm.storeData("magnetometerSensor", event);
}
if (data.z_positive|| data.z_negative) {
local event = { "event" : "Z-axis event"};
ldm.storeData("magnetometerSensor", event);
}
}
}
// Check if event has cleared
if (nora.ALERT_PIN.read() == nora.ALERT_EVENT) {
eventFlag = true;
imp.wakeup(5, function() { checkEvents(); });
} else {
eventFlag = false;
}
// Check if time for reading
checkTimers();
}
// Take readings from each sensor and store locally
function takeReadings(connect = false) {
// Wake powered down sensors
wakeSensorSetup();
nora.tempRead(function(err, reading) {
if (err) { server.log(err); }
readingsCounter++;
ldm.storeData("tempSensor", reading);
});
nora.tempHumidRead(function(err, reading) {
if (err) { server.log(err); }
readingsCounter++;
ldm.storeData("tempHumidSensor", reading);
});
nora.lightRead(function(err, reading) {
if (err) { server.log(err); }
readingsCounter++;
ldm.storeData("ambLightSensor", reading);
});
nora.proximityRead(function(err, reading) {
if (err) { server.log(err); }
readingsCounter++;
ldm.storeData("ambLightSensor", reading);
});
nora.accelRead(function(err, reading) {
if (err) { server.log(err); }
readingsCounter++;
ldm.storeData("accelerometerSensor", reading);
});
nora.pressureRead(function(err, reading) {
if (err) { server.log(err); }
readingsCounter++;
ldm.storeData("pressureSensor", reading);
});
nora.magetometerRead(function(err, reading) {
if (err) { server.log(err); }
readingsCounter++;
ldm.storeData("magnetometerSensor", reading);
});
readingsTimer = imp.wakeup(READINGS_TIMEOUT, function() {
readingsFlag = true;
})
readingsDone(connect);
}
// Put device to sleep after all readings received
function readingsDone(connect) {
if(readingsFlag || readingsCounter == numReadings) {
imp.cancelwakeup(readingsTimer);
readingsFlag = false;
readingsCounter = 0;
readingsTimer = null;
if(connect) {
sendData();
} else {
sleep();
}
} else {
imp.wakeup(0.2, function() {
readingsDone(connect);
});
}
}
// Send Data to agent
function sendData() {
bull.send("data", ldm.getData()).onReply(function(msg) {
ldm.clearNVReadings();
sleep();
});
}
// Handles wakeup logic
function onWake(wakeReason) {
switch(wakeReason) {
case WAKEREASON_TIMER:
// server.log("WOKE UP B/C TIMER EXPIRED");
checkTimers();
break;
case WAKEREASON_PIN:
// server.log("WOKE UP B/C ALERT PIN HIGH");
checkEvents();
break;
case WAKEREASON_POWER_ON:
// server.log("COLD BOOT");
bootSensorSetup();
imp.wakeup(BLINKUP_TIMEOUT, takeReadings);
break;
default:
// server.log("WOKE UP B/C RESTARTED DEVICE, LOADED NEW CODE, ETC");
bootSensorSetup();
takeReadings();
}
}
// Put Imp into Low power state
// If we are ready sleep until scheduled wakeup
function sleep() {
// Don't go to sleep if interrupt enabled or waiting for agent response
if (eventFlag) { return; }
// Power down sensors
nora.setLowPowerMode(ldm.getInterruptState());
// Calculate sleep timer
local timer = nv.nextWake - time();
// 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);
}
}
///// RUNTIME
onWake( hardware.wakereason() );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment