Skip to content

Instantly share code, notes, and snippets.

@blindman2k
Created October 8, 2014 00:29
Show Gist options
  • Save blindman2k/f80b92650f2b858f013a to your computer and use it in GitHub Desktop.
Save blindman2k/f80b92650f2b858f013a to your computer and use it in GitHub Desktop.
Simple Nora temp bug which records and displays temperature and humidity graphs.
// -----------------------------------------------------------------------------
const html = @"
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>TempBug</title>
<link href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.min.css' rel='stylesheet'>
<link href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap-theme.css' rel='stylesheet'>
<style>
body {
margin: 0px 10px;
}
.col-md-6 {
padding-left: 0px;
padding-right: 0px;
}
table, th, td {
border: none;
border-collapse: collapse;
padding: 6px;
}
th {
text-align: right;
}
.google-visualization-atl.container {
border: none !important;
}
div.centre table {
margin: auto !important;
}
</style>
</head>
<body>
<div class='container-fluid'>
<div class='row'>
<div class='col-md-6 col-md-offset-3'>
<h4></h4>
</div>
<div class='panel panel-primary col-md-6 col-md-offset-3'>
<div class='panel-heading'>Temperature</div>
<div id='tempchart' style='width: 100%; height: 380px; margin-bottom: 5px;'></div>
</div>
<div class='panel panel-primary col-md-6 col-md-offset-3'>
<div class='panel-heading'>Humidity</div>
<div id='humidchart' style='width: 100%; height: 380px; margin-bottom: 5px;'></div>
</div>
</div>
</div>
<div id='alerts'>
</div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/js/bootstrap.min.js'></script>
<script src='https://www.google.com/jsapi' type='text/javascript'></script>
<script>
google.load('visualization', '1.0', {'packages':['corechart', 'annotationchart']});
$(function() {
// Draw the graph
google.setOnLoadCallback(function() {
// .........................................................
// Prepare the options for this chart
var options = {
chartArea: {top: 10, width: '90%', height: '70%'},
scaleColumns: [1, 2],
scaleType: 'allmaximized',
displayExactValues: true,
};
function render() {
options.colors = ['red'];
options.allValuesSuffix = ' °C';
options.numberFormats = '0.0';
options.scaleFormat = '0.0';
tempchart.draw(tempdata, options);
options.colors = ['blue'];
options.allValuesSuffix = '';
options.numberFormats = '0.0 %';
options.scaleFormat = '0.0 %';
humidchart.draw(humiddata, options);
}
function tempchart_change(e) {
humidchart.setVisibleChartRange(e['start'], e['end']);
}
function humidchart_change(e) {
tempchart.setVisibleChartRange(e['start'], e['end']);
}
// Every time there is new data, redraw the chart
var tempchart = new google.visualization.AnnotationChart($('#tempchart')[0]);
var humidchart = new google.visualization.AnnotationChart($('#humidchart')[0]);
// Prepare a data store for the temperature data and another one for the humidity date
var tempdata = new google.visualization.DataTable();
tempdata.addColumn('datetime', 'When');
tempdata.addColumn('number', 'Temperature');
var humiddata = new google.visualization.DataTable();
humiddata.addColumn('datetime', 'When');
humiddata.addColumn('number', 'Humidity');
// Redraw the data if the orientation changes
window.addEventListener('orientationchange', render);
$(window).resize(render);
// Watch the two range change events
google.visualization.events.addListener(tempchart, 'rangechange', tempchart_change);
google.visualization.events.addListener(humidchart, 'rangechange', humidchart_change);
// Now load the data regularly
function loadData() {
var length = tempdata.getNumberOfRows();
if (length > 0) tempdata.removeRows(0, length);
$.get('data', function(newdata) {
for (var i in newdata) {
tempdata.addRow([
new Date(newdata[i].time * 1000),
newdata[i].temp
]);
humiddata.addRow([
new Date(newdata[i].time * 1000),
newdata[i].humid / 100.0
]);
}
render();
});
}
loadData();
setInterval(loadData, 60000);
});
})
</script>
</body>
</html>";
// -----------------------------------------------------------------------------
// Serve up web requests for / (redirect to /view), /view (html) and /data (json)
http.onrequest(function(req, res) {
if (req.path == "/") {
res.header("Location", http.agenturl() + "/view")
res.send(302, "Redirect")
} else if (req.path == "/view") {
res.send(200, html);
} else if (req.path == "/data") {
res.header("Content-Type", "application/json")
// Turn the readings blob into an array of tables
storereadings.seek(0);
local readings = [];
while (!storereadings.eos()) {
local time = storereadings.readn('i');
local temp = storereadings.readn('s') / 10.0;
local humid = storereadings.readn('s') / 10.0;
readings.push({time=time, temp=temp, humid=humid});
}
res.send(200, http.jsonencode(readings));
} else {
res.send(404, "Noone here");
}
})
// -----------------------------------------------------------------------------
// Add new readings
device.on("readings", function(readings) {
// server.save() doesn't store array's or tables as efficiently as blobs but it doesn't store blobs.
// So we are storing our data as a base64 encoded blob.
// Add the readings into the store
local log = "";
storereadings.seek(0, 'e')
foreach (reading in readings) {
log += format("%0.02f°C/%0.02f%%, ", reading.t, reading.h);
storereadings.writen(reading.s, 'i');
storereadings.writen((reading.t * 10).tointeger(), 's');
storereadings.writen((reading.h * 10).tointeger(), 's');
}
// Trim to the last 6000 readings (6000 entries * 6 bytes per entry * 4/3 base64 encoding < 64kb)
const MAX_READINGS_SIZE = 48000;
if (storereadings.len() > MAX_READINGS_SIZE) {
storereadings.seek(-MAX_READINGS_SIZE, 'e')
storereadings = storereadings.readblob(MAX_READINGS_SIZE);
}
// Convert the blob into an encoded string for server.save() to persist
store.readings = http.base64encode(storereadings);
server.save(store);
local estimate = (store.readings.len()/10.66).tointeger();
server.log(format("%d new reading(s) out of %d total: %s", readings.len(), estimate, log.slice(0, -2)));
})
// -----------------------------------------------------------------------------
// Load the old readings
// server.save({});
store <- server.load();
if ("readings" in store && store.readings.len() > 0) {
storereadings <- http.base64decode(store.readings);
} else {
store.readings <- [];
storereadings <- blob();
}
server.log("Started");
/*
Notes on limitations of this design:
- If the wifi is running but the agent is offline then we will send the readings
and drop the data. The better way to do this is to send the data and wait for
instructions from the agent to delete the data, and only deleting data that is
confirmed to be received at the agent.
- Non-volatile (nv) RAM on the imp will not survive through power outages and is
limited to a total of 4kb of serialised data. A "real" implementation of the
Temp Bug would be better served by external non-volatile flash storage.
--------[ Pin mux ]--------
1 -
2 -
5 -
6 -
7 -
8 - I2C clock
9 - I2C data (temperature, humidity)
A -
B -
C -
D -
E -
*/
// -----------------------------------------------------------------------------
class sensor {
i2c = null;
pin_en_l = null;
pin_drain = null;
addr = null;
ready = false;
constructor(_i2c=null, _pin_en_l=null, _pin_drain=null, _addr=null) {
i2c = _i2c;
pin_en_l = _pin_en_l;
pin_drain = _pin_drain;
addr = _addr;
::last_activity <- time();
if (i2c) i2c.configure(CLOCK_SPEED_400_KHZ);
if (pin_en_l) pin_en_l.configure(DIGITAL_OUT);
if (pin_drain) pin_drain.configure(DIGITAL_OUT);
// Test the sensor and if its alive then setup a handler to execute all functions of the class
test();
}
function enable() {
if (pin_en_l) pin_en_l.write(0);
if (pin_drain) pin_drain.write(1);
imp.sleep(0.001);
}
function disable() {
if (pin_en_l) pin_en_l.write(1);
if (pin_drain) pin_drain.write(0);
}
function test() {
if (i2c == null) {
ready = false;
} else {
enable();
local t = i2c.read(addr, "", 1);
ready = (t != null);
disable();
}
return ready;
}
function reset() {
if (i2c) {
i2c.write(0x00,format("%c",RESET_VAL));
imp.sleep(0.01);
}
}
}
// -----------------------------------------------------------------------------
class hih6131 extends sensor {
static WAIT = 80; // milliseconds
pin_en_l = null;
pin_drain = null;
constructor(_i2c, _pin_en_l = null, _pin_drain = null, _addr = 0x4E){
base.constructor(_i2c, _pin_en_l, _pin_drain, _addr);
}
function convert(th) {
local t = ((((th[2] << 6 ) | (th[3] >> 2)) * 165) / 16383.0) - 40;
local h = ((((th[0] & 0x3F) << 8 ) | (th[1] )) / 163.83 );
//Round to 2 decimal places
t = (t*100).tointeger() / 100.0;
h = (h*100).tointeger() / 100.0;
return { temperature = t, humidity = h};
}
function read(callback = null) {
if (!ready) return callback(null);;
enable();
i2c.write(addr, "");
// Do a non-blocking read
imp.wakeup(WAIT/1000.0, function() {
local th = i2c.read(addr, "", 4);
disable();
if (th == null) {
callback(null);
} else {
callback(convert(th));
}
}.bindenv(this));
}
}
// -----------------------------------------------------------------------------
class Connection {
static CONNECTION_TIMEOUT = 30;
static CHECK_TIMEOUT = 5;
static MAX_LOGS = 100;
connected = null;
connecting = false;
stayconnected = true;
reason = null;
callbacks = null;
blinkup_timer = null;
logs = null;
// .........................................................................
constructor(_do_connect = true) {
callbacks = {};
logs = [];
server.setsendtimeoutpolicy(RETURN_ON_ERROR, WAIT_TIL_SENT, CONNECTION_TIMEOUT);
connected = server.isconnected();
imp.wakeup(CHECK_TIMEOUT, _check.bindenv(this));
if (_do_connect && !connected) imp.wakeup(0, connect.bindenv(this));
else if (connected) imp.wakeup(0, _reconnect.bindenv(this));
}
// .........................................................................
function _check() {
imp.wakeup(CHECK_TIMEOUT, _check.bindenv(this));
if (!server.isconnected() && !connecting && stayconnected) {
// We aren't connected or connecting, so we should try
_disconnected(NOT_CONNECTED, true);
}
}
// .........................................................................
function _disconnected(_reason, _do_reconnect = false) {
local fireevent = connected;
connected = false;
connecting = false;
reason = _reason;
if (fireevent && "disconnected" in callbacks) callbacks.disconnected();
if (_do_reconnect) connect();
}
// .........................................................................
function _reconnect(_state = null) {
if (_state == SERVER_CONNECTED || _state == null) {
connected = true;
connecting = false;
// Dump the logs
while (logs.len() > 0) {
local logo = logs[0];
logs.remove(0);
local d = date(logo.ts);
local msg = format("%04d-%02d-%02d %02d:%02d:%02d UTC %s", d.year, d.month+1, d.day, d.hour, d.min, d.sec, logo.msg);
if (logo.err) server.error(msg);
else server.log(msg);
}
if ("connected" in callbacks) callbacks.connected(SERVER_CONNECTED);
} else {
connected = false;
connecting = false;
if ("disconnected" in callbacks) callbacks.disconnected();
connect();
}
}
// .........................................................................
function connect(withblinkup = true) {
stayconnected = true;
if (!connected && !connecting) {
server.connect(_reconnect.bindenv(this), CONNECTION_TIMEOUT);
connecting = true;
}
if (withblinkup) {
// Enable BlinkUp for 60 seconds
imp.enableblinkup(true);
if (blinkup_timer) imp.cancelwakeup(blinkup_timer);
blinkup_timer = imp.wakeup(60, function() {
blinkup_timer = null;
imp.enableblinkup(false);
}.bindenv(this))
}
}
// .........................................................................
function disconnect() {
stayconnected = false;
server.disconnect();
_disconnected(NOT_CONNECTED, false);
}
// .........................................................................
function isconnected() {
return connected == true;
}
// .........................................................................
function ondisconnect(_disconnected = null) {
if (_disconnected == null) delete callbacks["disconnected"];
else callbacks["disconnected"] <- _disconnected;
}
// .........................................................................
function onconnect(_connected = null) {
if (_connected == null) delete callbacks["connected"];
else callbacks["connected"] <- _connected;
}
// .........................................................................
function log(msg, err=false) {
if (server.isconnected()) server.log(msg);
else logs.push({msg=msg, err=err, ts=time()})
if (logs.len() > MAX_LOGS) logs.remove(0);
}
// .........................................................................
function error(msg) {
log(msg, true);
}
}
// -----------------------------------------------------------------------------
function send_readings() {
// Send the data to the agent
if (agent.send("readings", nv.readings) == 0) {
nv.readings = [];
}
}
// -----------------------------------------------------------------------------
function sleep() {
// Cap the number of readins by deleting the oldest
while (nv.readings.len() > MAX_SAMPLES) nv.readings.remove(0);
// Shut down everything and go back to sleep
if (server.isconnected()) {
imp.onidle(function() {
server.sleepfor(READING_INTERVAL);
})
} else {
imp.deepsleepfor(READING_INTERVAL);
}
}
// -----------------------------------------------------------------------------
// Configure the imp and all the devices
imp.setpowersave(true);
imp.enableblinkup(false);
temphumid <- hih6131(hardware.i2c89, hardware.pin2, hardware.pin5);
READING_INTERVAL <- 300; // Read a new sample every [READING_INTERVAL] seconds.
READING_SAMPLES <- 6; // When there are [READING_SAMPLES] come online and dump the results.
MAX_SAMPLES <- 100; // This is roughly how many readings we can store in 4k of nvram.
// -----------------------------------------------------------------------------
// Setup the basic memory and temperature sensor
if (!("nv" in getroottable())) nv <- { "readings": [], next_connect = time() };
// Take a reading as we always want to do this
temphumid.read(function(result) {
if (result != null) {
// server.log(format("Temp = %0.1f, Humidity = %f", result.temperature, result.humidity));
nv.readings.push({"t": result.temperature, "h": result.humidity, "s": time()});
}
// -----------------------------------------------------------------------------
if (hardware.wakereason() == WAKEREASON_TIMER && (time() < nv.next_connect || nv.readings.len() < READING_SAMPLES)) {
// After a timer, go immediately back to sleep
sleep();
} else {
// Make sure we don't come online again for another cycle
nv.next_connect = time() + (READING_INTERVAL * READING_SAMPLES);
// Now forward the results to the server
local cm = Connection();
// On connect, send readings and shutdown
cm.onconnect(function(reason=null) {
imp.enableblinkup(true);
send_readings();
if (hardware.wakereason() == WAKEREASON_PIN ||
hardware.wakereason() == WAKEREASON_POWER_ON ||
hardware.wakereason() == WAKEREASON_NEW_SQUIRREL) {
// The button was pressed to wake up the imp. Stay online for a while.
imp.wakeup(30, sleep);
} else {
// Go back to sleep
sleep();
}
});
// If connection fails, just go to sleep
cm.ondisconnect(function(reason=null) {
sleep();
});
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment