Data Collection with Espruino

Often you'll want to make a device that can sit in a place and will collect (log) data. Espruino is particularly good at this as all devices have a Real Time Clock (RTC) and don't draw much power when idle.

Note: There's a Bangle.js specific example of logging here

At its most simple, all you need to do is have some code that looks like this:

function getData() {
  var data = readMyData();
  storeMyData(data);
}

setInterval(getData, 60*1000); // every minute

Reading Data

Espruino devices have a built-in temperature sensor, accessible with E.getTemperature() - so you can easily use this for reading values.

Puck.js also contains various sensors you can read from.

However often you'll use analogRead(pin) to read analog values from pins, or will require one of Espruino's modules to interface to an external sensor (for instance the DS18B20 temperature sensor).

Time

If you're logging data at fixed time periods there may be no need to store the time of each measurement (apart from perhaps the start time). This will save you memory and will allow you to store more readings in memory.

However sometimes you will want a timestamp (perhaps you want to store whenever a door opens or closes) - in this case you have no need to store data, just the time at which an event happened.

You can get the current time in a human readable form with (new Date()).toString(), or Date.now() will give the number of milliseconds since 1970.

The time reported is according to Espruino's Real-Time Clock. You can set it with setTime(secondsSince1970), or can turn on Set Current Time in the Web IDE's Communication Settings and the time will be set automatically next time code is uploaded. (If you're using the Puck.js Web Bluetooth library you can also use Puck.setTime() on a Web Bluetooth website).

Data Storage

The next step is to figure out how you're going to store your data. There are a few options here. If you want to skip this and just want something that works, see require("Storage").open under Flash memory below.

RAM - JavaScript variables

The simplest (but most inefficient) option is just to store data in a JavaScript array, for instance:

var log = [];

function storeMyData(data) {
  log.push(data); // append a new item to the array
}

However this isn't a very efficient use of memory, and each time you log data more memory will be used up until finally all of Espruino's memory is used.

You can work around this by restricting the size of the array, for example:

function storeMyData(data) {
  // ensure there are less than 500 elements in the array
  while (log.length >= 500) log.shift();
  // append a new item to the array
  log.push(data);
}

Usually each number added to the array takes up two variable slots - one for the number, and one for the array index (see the performance section for more information). You can use process.memory().free to see how many variable slots you have available, so can see how long you should restrict the array to (you need to leave a few variable slots free for exection too!).

RAM - Typed Array

Storing as JavaScript variables is easy, but uses a lot of memory (~32 bytes per item). However if you know something about your input data you can store it in a much more compact form.

For instance, if we know that each reading is a number then we can use a fixed size 32 bit floating point buffer for it:

var log = new Float32Array(1000);
var logIndex = 0;

function storeMyData(data) {
  logIndex++;
  if (logIndex>=log.length) logIndex=0;
  log[logIndex] = data;
}

This will use up only 4 bytes per item instead of the previous solution's 32.

You can also use Float64Array (8 bytes) for very accurate numbers, Uint8Array/Int8Array (1 byte) for integer numbers, or Uint16Array/Int16Array/Uint32Array/Int32Array.

In the code above we rotate around the buffer writing data rather than shifting elements in the array itself (because the push and shift methods aren't available on Typed Arrays like Float32Array). It means that when you come to output the data you'll need to work backwards from logIndex to output data in the right order.

However if you do want to rotate data in the buffer itself, you can do it efficiently using the .set method on the array:

function storeMyData(data) {
  // shift elements backwards - note the 4, because a Float32 is 4 bytes
  log.set(new Float32Array(log.buffer, 4 /*bytes*/));
  // add ad final element
  log[log.length-1] = data;
}

This is still slower than the logIndex method above, but it does make outputting and graphing the data much easier.

RAM - DataView

DataView is very similar to the typed array method above, however it allows you to access the raw data in many different forms.

For example below we store a date as 4 bytes and a temperature in one signed byte, packing everything in as tightly as possible:

const EVENT_SIZE = 5;
/* Each event will be:
 byte 0-3 : # of seconds since 1970 (good up to 2106)
 byte 4   : temperature in C
*/
var log = new DataView(new ArrayBuffer(EVENT_SIZE*1000));
...

// To write
var o = indexToWrite*EVENT_SIZE;
log.setUint32(o+0,Date.now()/1000);
log.setInt8(o+4,E.getTemperature());

// To read
var o = indexToRead*EVENT_SIZE;
var event = {
  time : new Date(log.getUint32(o+0)*1000),
  temp : log.getInt8(o+4)
};

Flash memory

So far we've only written to RAM, however some Espruino boards have areas of flash memory that you can use to store nonvolatile data.

On Espruino 1v97 and above there is the Storage module which implements a simple filesystem in the area of flash memory that is also used to save your program code. The Storage module implements wear levelling and deals with flash page boundaries and pages for you, so is much easier to use than accessing flash memory directly.

Espruino 2v05 and later have require("Storage").open which allows you to append to a file in the storage area, as well as read back line by line. This is what we'd recommend you use if you just want to write text (it doesn't support writing char code 255 so can't be used for binary data):

var f = require("Storage").open("log","a");

// Write some data
setInterval(function() {
  f.write(getTime()+","+E.getTemperature()+"\n");
}, 1000);

function getData(callback) {
  var f = require("Storage").open("log","r")
  var l = f.readLine();
  while (l!==undefined) {
    callback(l);
    l = f.readLine();
  }
}
// Get data with: getData(print);

However, you can also use the Storage module at a lower level, allocating a fixed-size file (which starts of as all char code 255) and writing to it. The example below will write text into a log file, and will alternate between log1 and log2 as needed:

var storage = require("Storage");
var FILESIZE = 2048;
var file = {
  name : "",
  offset : FILESIZE, // force a new file to be generated at first
};

function getOtherFilename() {
  return file.name=="log1"?"log2":"log1";
}

// Add new data to a log file or switch log files
function saveData(txt) {
  var l = txt.length;
  if (file.offset+l>FILESIZE) {
    // need a new file...
    file.name = getOtherFilename();
    // write data to file - this will overwrite the last one
    storage.write(file.name,txt,0,FILESIZE);
    file.offset = l;
  } else {
    // just append
    storage.write(file.name,txt,file.offset);
    file.offset += l;
  }
}

// Write some data
setInterval(function() {
  saveData(getTime()+","+E.getTemperature()+"\n");
}, 1000);


// Read with:
// storage.read("log1");

It's also possible to write binary data using this method which allows you to store data much more compactly than text, you just have to decode it afterwards. For example in the example above you could instead do the following:

// Write some data as binary
setInterval(function() {
  var buf = new ArrayBuffer(5); // 5 = record size
  var d = new DataView(buf);
  d.setUint32(0, Math.round(getTime()));
  d.setInt8(4, Math.round(E.getTemperature()));
  saveData(buf);
}, 1000);

// Read the data
function getData() {
  var buf = E.toArrayBuffer(storage.read("log1"));
  var d = new DataView(buf);
  for (var i=0;i<buf.length;i+=5) { // 5 = record size
    if (d.getUint32(i+0)==0xFFFFFFFF)
      break; // time is all 0xFF, it's not been written yet
    print({
      time : d.getUint32(i+0),
      temp : d.getInt8(i+4)
    });
  }
}

NOTE: You can also use require("Flash") to write bytes straight to flash - but this is pretty advanced and requires you to deal with pages and page erasure.

External Flash/EEPROM Memory

You can also wire up external memory such as Flash or EEPROMs - usually via SPI or I2C.

SD card

For the maximum amount of storage, you can also write to SD cards.

The original Espruino board has a micro SD card slot pre-installed. While other Espruino boards don't have one, you can easily wire one up to any of the boards.

For simple logging, you might choose to use a text format that you can read on a PC like CSV:

function storeMyData(data) {
  var csvline = (new Date()).toString() + "," + data + "\n";
  require("fs").appendFileSync("mydata.csv", csvline);
}

Note: just like on a PC, you'll want to eject your SD card in software before removing it from Espruino. To do this, use E.unmountSD().

Extracting Data

Once you have all your data logged, you'll want to be able to get at it.

If you're using an SD card it's easy - you can just remove it and put it in a PC.

For most other methods you'll want to be able to output the data down USB (or Bluetooth if you're connected with Puck.js). You won't be able to load everything into RAM at once, so you'll want to iterate over it one step at a time.

Something like this would work:

function getData() {
  for (var i=0;i<log.length;i++)
    console.log(i+","+log[i]);
}

Note: When using the Typed Array example above you'll want to iterate from logIndex+1 forwards so that everything is kept in order.

Simple Example

For this example we'll use features that are easy to use and available in all boards: the Temperature Sensor, and Typed Arrays.

Simply copy and paste the following code in to the right hand side of the ide, turn on Set Current Time in the Web IDE Communication Settings, and click Upload.

var log = new Float32Array(100); // our logged data
var logIndex = 0; // index of last logged data item
var timePeriod = 60*1000; // every minute
var lastReadingTime; // time of last reading

// Store data into RAM
function storeMyData(data) {
  logIndex++;
  if (logIndex>=log.length) logIndex=0;
  log[logIndex] = data;
}

// Get Data and store it in RAM
function getData() {
  var data = E.getTemperature();
  storeMyData(data);
  lastReadingTime = Date.now();
}

// Dump our data in a human-readable format
function getData() {
  for (var i=1;i<=log.length;i++) {
    var time = new Date(lastReadingTime - (log.length-i)*timePeriod);
    var data = log[(i+logIndex)%log.length];
    console.log(time.toString()+"\t"+data);
  }
}

// Start recording
setInterval(getData, timePeriod);

The Espruino board will start logging one minute after upload, and will keep logging every minute after that. Because the Float32Array is 100 items long, it'll keep only the last 100 readings. You can easily increase the length of the array - most espruino boards will handle at least 5000 items, many will allow more.

To get your data, simply type getData() in the left-hand side of the IDE and hit enter, then copy the data out of the terminal. Because we're using tab (\t) as a separator character, you can usually paste the copied data directly into a spreadsheet like Google Sheets.

You can also download directly to a file - click the 'Try Me' button on the code blocks under 'Automatically recovering data'

Further Improvements

You may well want to store more than one data item - in which case you could either store multiple items in the log array, or could have one log array per type of data (eg. one for temperature, one for light).

We could also store our data more efficiently. If we only cared about temperature to the nearest degree we could swap Float32Array for Int8Array and store 4 times as much data (as long as the temperature was between -128 and 127 degrees C, as that is the range of Int8Array).

Automatically recovering data

If you're trying to interface this to an application on your computer, you just need to open the Serial/Bluetooth connection, send the string "\x10getData()\n", and then read the data that is sent in return.

You can use the Puck.js library or UART.js library to communicate using Web Bluetooth or Web Serial direct from a webpage. For example:

Click the 'Try Me' button on the code below to try it out and read data from your Espruino device

If you only need Web Bluetooth you can use the Puck.js library too.

<html>
 <head>
 </head>
 <body>
  <script src="https://www.espruino.com/js/uart.js"></script>
  <script>
// Output debug info about received data to the console
UART.debug=3;

// Save the CSV file to disk
function saveFile(csvText, fileName) {
  var saver = document.createElement("a");
  var blob = new Blob([csvText], {type : 'text/csv'});
  var blobURL = saver.href = URL.createObjectURL(blob),
      body = document.body;
  saver.download = fileName;
  body.appendChild(saver);
  saver.dispatchEvent(new MouseEvent("click"));
  body.removeChild(saver);
  URL.revokeObjectURL(blobURL);
}

// Actually get the data from Espruino
function getData() {
  UART.write('\x03\x10getData()\n', function(data) {
    console.log("Received",JSON.stringify(data));
    // If getData uses console.log rather than Bluetooth.println,
    // it'll cause a prompt to be written at the end of the output
    // we detect that here are remove it.
    if (data.endsWith(">")) data = data.slice(0,-1);
    saveFile(data,"info.csv");
  });
}
  </script>
  <button onclick="getData()">Download Data</button>
 </body>
</html>

The code above handles the simplest case - however to keep code reliable the uart.js and puck.js libraries will time out after 30s of downloads. If your download will take longer than that then you'll need to manually handle each line of data as it comes in - for instance:

function onLine(data) {
  // CSV data is received here
}

var connection;
button.addEventListener("click", function() {
  if (connection) {
    connection.close();
    connection = undefined;
  }
  Puck.connect(function(c) {
    if (!c) {
      alert("Couldn't connect!");
      return;
    }
    connection = c;
    // Handle the data we get back, and call 'onLine'
    // whenever we get a line
    var buf = "";
    connection.on("data", function(d) {
      buf += d;
      var i = buf.indexOf("\n");
      while (i>=0) {
        onLine(buf.substr(0,i));
        buf = buf.substr(i+1);
        i = buf.indexOf("\n");
      }
    });
    // Request data from Puck.js
    connection.write("\x10getData()\n");
  });
});

Want to do this automatically as soon as the devices are ready or in range? Check out the Automatic Data Download page.

This page is auto-generated from GitHub. If you see any mistakes or have suggestions, please let us know.