Espruino:Примеры/Сбор данных при помощи Espruino
Сбор данных при помощи Espruino[1]
Зачастую требуется собрать устройство, которое будет просто «сидеть» в одном месте и собирать (регистрировать) данные. Espruino-устройства особенно хорошо подходят для этой задачи, т.к. все они оснащены часами реального времени (RTC-часами) и в состоянии простоя расходуют очень мало энергии.
На простейшем уровне ваш код должен выглядеть как-то так:
function getData() {
var data = readMyData();
storeMyData(data);
}
setInterval(getData, 60*1000); // каждую минуту
Считывание данных
Во-первых, считывать данные с Espruino-устройств можно при помощи функции E.getTemperature() – она считывает температуру с помощью температурного датчика, который встроен во все Espruino-устройства.
Кроме того, множеством различных датчиков оснащено устройство Puck.js – вы тоже можете считывать с них данные.
Но вы также часто будете использовать analogRead(pin) для считывания аналоговых данных с контактов. Кроме того, вам могут потребоваться модули Espruino для взаимодействия с внешними датчиками (например, с температурным датчиком DS18B20).
Время
При записи данных через фиксированные интервалы времени вам может не понадобиться сохранять время каждого измерения (за исключением, возможно, стартового времени). Это позволит сэкономить память и соответственно – сохранить в неё больше данных.
Но иногда сохранение временной метки всё же нужно. Например, вы хотите знать моменты времени закрытия и открытия двери – в этой ситуации никаких данных, кроме момента времени, когда произошло событие, сохранять не надо.
Прочесть время в удобочитаемом формате можно при помощи функции (new Date()).toString(), а помощью функции Date.now() можно прочесть количество миллисекунд, прошедших с 1970 года.
Считываемое время соответствует времени RTC-часов, которые встроены в Espruino. Настроить их можно при помощи setTime(secondsSince1970). Вы также можете поставить галочку в пункте Set Current Time в онлайн-IDE (чтобы найти его, кликните на кнопку с шестерёнкой справа вверху, а затем на Communications – пункт Set Current Time будет в самом низу) – это автоматически задаст текущее время при следующей загрузке кода. Если вы используете библиотеку Web Bluetooth для Puck.js, вы также можете воспользоваться функцией Puck.setTime() на вебсайте с Web Bluetooth.
Хранение данных
Далее надо определиться, как вы будете хранить свои данные. У вас есть несколько вариантов. Если вы хотите пропустить этот этап и просто получить какой-то рабочий вариант, ищите абзац с require("Storage").open в разделе «Flash-память» ниже.
RAM-память – JavaScript-переменные
Простейший (и наименее эффективный) вариант – это просто сохранить данные в JavaScript-массиве. Например:
var log = [];
function storeMyData(data) {
log.push(data); // добавляем новый элемент в массив
}
Но в таком случае память расходуется неэффективно. С каждой записью данных будет использоваться всё больше памяти – до тех пор, пока не будет забита вся память Espruino.
Обойти это можно при помощи ограничения размера массива. Например:
function storeMyData(data) {
// Задаем, чтобы в массиве
// гарантированно было не более 500 элементов:
while (log.length >= 500) log.shift();
// Добавляем новый элемент в массив:
log.push(data);
}
Обычно каждое число, добавляемое в массив, занимает два слота переменных – один для самого числа, а второй для индекса массива (более подробно читайте в статье о производительности Espruino). При помощи process.memory().free можно узнать количество доступных слотов переменных, а следовательно – понять, насколько вам нужно ограничить свой массив (но вам также нужно будет оставить несколько слотов переменных для выполнения кода!).
RAM-память – типизированный массив
Хранение данных в JavaScript-переменных – это простой, но в то же очень требовательный к памяти метод (примерно 32 байта на один элемент). Но, имея определённые знания о том, что из себя представляют входные данные, мы можем хранить их в гораздо более компактной форме.
К примеру, если мы знаем, что каждый элемент считанных данных – это число, то можем использовать для их хранения фиксированный 32-битный буфер для чисел с плавающей точкой.
var log = new Float32Array(1000);
var logIndex = 0;
function storeMyData(data) {
logIndex++;
if (logIndex>=log.length) logIndex=0;
log[logIndex] = data;
}
В этом случае для одного элемента будет использоваться только 4 байта, а не 32, как раньше.
Кроме того, для хранения очень точных чисел можно воспользоваться объектом Float64Array (8 байт), а для хранения целых чисел – объектами Uint8Array/Int8Array (1 байт), а также Uint16Array/Int16Array/Uint32Array/Int32Array.
В коде выше используется принцип кольцевого буфера, а не сдвиг элементов внутри самого массива (поскольку методов push() и shift() в типизированных массивах вроде Float32Array нет). Это значит, что при выводе данных с переменной logIndex нужно будет работать задом наперёд, чтобы данные выводились в правильном порядке.
Но если вам нужен сдвиг данных внутри массива, то сделать это можно, применив к массиву метод set():
function storeMyData(data) {
// Сдвигаем элементы в обратном порядке.
// Обратите внимание на цифру «4»:
// мы используем её, потому что Float32 – это 4 байта.
log.set(new Float32Array(log.buffer, 4 /*bytes*/));
// Добавляем финальный элемент.
log[log.length-1] = data;
}
Это медленнее, чем при использовании logIndex выше, но так проще выводить данные и делать графики на их основе.
RAM-память – объект DataView
Метод с использованием объекта DataView очень похож на метод с типизированным массивом выше, но он также позволяет получить доступ к исходным неформатированным данным во множестве разных форм.
К примеру, ниже мы сохраняем дату в 4 байтах, а температуру – в одном байте со знаком, упаковывая всё как можно компактнее:
const EVENT_SIZE = 5;
/* Каждое событие будет состоять из:
Байты 0-3: секунды, прошедшие с 1970 года (хватит до 2106 года)
Байт 4: температура в градусах Цельсия
*/
var log = new DataView(new ArrayBuffer(EVENT_SIZE*1000));
...
// Для записи:
var o = indexToWrite*EVENT_SIZE;
log.setUint32(o+0,Date.now()/1000);
log.setInt8(o+4,E.getTemperature());
// Для чтения:
var o = indexToRead*EVENT_SIZE;
var event = {
time : new Date(log.getUint32(o+0)*1000),
temp : log.getInt8(o+4)
};
Flash-память
Выше описывались только способы с использованием RAM-памяти, но у некоторых Espruino-плат также есть flash-память, которую можно использовать для долговременного хранения данных.
Кроме того, вы можете воспользоваться require("Flash") для записи байтов прямиком на flash-память – но это очень продвинутый метод, требующий работы со страницами (включая стирание страниц).
На Espruino 1v97 и новее есть модуль Storage, который реализует в flash-памяти простую файловую систему и, помимо прочего, используется для сохранения вашего программного кода.
В модуле Storage реализован контроль равномерности износа, и он, кроме того, вместо вас выполняет задачи, связанные со страницами flash-памяти и их границами, так что сохранять данные с его помощью гораздо проще.
В коде ниже мы сохраняем данные в журнальный файл (и будем поочерёдно менять log1 от log2):
var storage = require("Storage");
var FILESIZE = 2048;
var file = {
name : "",
offset : FILESIZE, // сначала принудительно
// генерируемый новый файл
};
// Добавляем новые данные в журнальный файл
// или в переключаемые журнальные файлы:
function saveData(txt) {
var l = txt.length;
if (file.offset+l>FILESIZE) {
// Нужен новый файл...
file.name = file.name=="log2"?"log1":"log2";
// Записываем данные в файл –
// это перезапишет данные, которые были записаны туда ранее:
storage.write(file.name,txt,0,FILESIZE);
file.offset = l;
} else {
// Просто добавляем данные:
storage.write(file.name,txt,file.offset);
file.offset += l;
}
}
// Для записи данных:
setInterval(function() {
saveData(getTime()+","+E.getTemperature()+"\n");
}, 1000);
// Для чтения данных:
// storage.read("log1");
На Espruino 2v05 и новее также можно использовать функцию require("Storage").open, которая позволяет открывать Storage-файл для добавления в него данных и значительно упрощает этот процесс. Рекомендуем использовать именно его:
var f = require("Storage").open("log","a");
// Записываем данные:
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();
}
}
// Считываем данные при помощи: getData(print);
Внешняя flash- или EEPROM-память
Вы также можете подключить внешнюю память (flash или EEPROM) – как правило, через SPI или I2C.
SD-карта
Чтобы максимально увеличить место для хранения данных, можно воспользоваться SD-картой.
У платы Espruino Original есть предустановленный слот для карты Micro SD. У других Espruino-плат такого слота нет, но подключить SD-карту к ним другими способами совсем не сложно.
Если вам нужна простая регистрация данных, то данные можно записывать в виде обычного текста (вроде CSV-файлов), который в дальнейшем можно будет прочесть на ПК:
function storeMyData(data) {
var csvline = (new Date()).toString() + "," + data + "\n";
require("fs").appendFileSync("mydata.csv", csvline);
}
Извлечение данных
Теперь, когда данные сохранены, их можно прочесть.
Если вы используете SD-карту, то ничего сложного – просто выньте её из Espruino и вставьте в ПК.
При использовании других методов вывод данных лучше делать по USB-соединению (или через Bluetooth, если вы подключены к Puck.js). Вы не сможете загрузить всё в RAM-память за раз, поэтому вам нужно будет разбить этот процесс на несколько итераций.
Должно сработать что-то вроде этого:
function getData() {
for (var i=0;i<log.length;i++)
console.log(i+","+log[i]);
}
Простой пример
Для этого примера мы воспользуемся функциями, которыми, во-первых, легко пользоваться, и которые, во-вторых, есть на всех платах – температурным датчиком и типизированными массивами.
Просто скопируйте и вставьте код ниже в правую часть IDE Espruino, включите в IDE настройку Set Current Time (чтобы найти её, кликните на кнопку с шестерёнкой справа вверху, а потом на Communications – пункт Set Current Time будет в самом низу) и кликните на кнопку загрузки.
var log = new Float32Array(100); // наши сохранённые данные
var logIndex = 0; // индекс последнего сохранённого
// элемента данных
var timePeriod = 60*1000; // каждую минуту
var lastReadingTime; // время последнего считывания
// Сохраняем данные в RAM-память:
function storeMyData(data) {
logIndex++;
if (logIndex>=log.length) logIndex=0;
log[logIndex] = data;
}
// Считываем данные и сохраняем их в RAM-память:
function getData() {
var data = E.getTemperature();
storeMyData(data);
lastReadingTime = Date.now();
}
// Выгружаем наши данные в удобочитаемом формате:
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);
}
}
// Начинаем запись:
setInterval(getData, timePeriod);
Espruino сохранит первые данные спустя минуту после загрузки кода и продолжит сохранять их каждую последующую минуту. Поскольку длина объекта Float32Array составляет 100 элементов, в нём будет храниться только 100 последних записей. Вы можете без труда увеличить размер массива – большинство Espruino-плат способны обрабатывать не менее 5000 элементов, а многие и ещё больше.
Чтобы прочесть данные, просто впишите getData() в левую часть IDE и нажмите на ↵ Enter , а затем скопируйте данные из терминала. Поскольку мы в качестве разделителя используем символ табуляции (\t), обычно вы можете вставлять скопированные данные напрямую в электронную таблицу вроде «Google Таблиц».
Вы также можете загружать данные прямо в файл – кликните на кнопку «Попробуй!» под кодом в разделе «Автоматическое восстановление данных».
Как сделать ещё лучше
Возможно, вам нужно сохранять больше одного элемента данных – в этом случае вы можете либо сохранять по несколько элементов в массиве log, либо сделать по отдельному массиву log для каждого типа данных (например, один для температуры, второй – для световых значений и т.д.).
Кроме того, данные можно хранить более эффективно. Если вам нужны температурные данные с точностью только до градуса, Float32Array можно заменить на Int8Array – это позволит хранить в 4 раза больше данных (при условии, что диапазон температуры будет между -128 и 127 градусами Цельсия – именно такой диапазон вмещается в Int8Array).
Автоматическое восстановление данных
Если вы пытаетесь наладить передачу этих данных на ПК-приложение, вам нужно лишь открыть последовательное или Bluetooth-соединение, отправить строку "\x10getData()\n", а затем просто считывать присылаемые в ответ данные.
Для прямой коммуникации с веб-страницей можно воспользоваться библиотекой Puck.js (Web Bluetooth) или UART.js (Web Serial). Например: Кликните на кнопку «Попробуй!» под кодом ниже, чтобы опробовать его в действии и прочесть данные со своего Espruino-устройства.
Если вам нужен только Web Bluetooth, вы также можете также воспользоваться библиотекой Puck.js.
<html>
<head>
</head>
<body>
<script src="https://www.espruino.com/js/uart.js"></script>
<script>
// Выводим в консоль отладочную информацию о присланных данных:
UART.debug=3;
// Сохраняем CSV-файл на диск:
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);
}
// Считываем данные с Espruino:
function getData() {
UART.write('\x03\x10getData()\n', function(data) {
console.log("Received",JSON.stringify(data));
// Если getData() использует console.log, а не Bluetooth.println,
// символ приглашения ввода текста будет напечатан
// в конце выводимых данных.
// Здесь мы находим его и удаляем:
if (data.endsWith(">")) data = data.slice(0,-1);
saveFile(data,"info.csv");
});
}
</script>
<button onclick="getData()">Download Data</button>
</body>
</html>
В коде выше показан самый простейший случай – но в целях надёжности кода спустя 30 секунд загрузки у библиотек «uart.js» и «puck.js» сработает таймаут. Если у вас загрузка занимает больше времени, вам нужно будет вручную обработать каждую строчку данных по её приходу. Например:
function onLine(data) {
// Тут выполняется получение CSV-данных
}
var connection;
button.addEventListener("click", function() {
if (connection) {
connection.close();
connection = undefined;
}
Puck.connect(function(c) {
if (!c) {
alert("Не удалось подключиться!");
return;
}
connection = c;
// Обрабатываем полученные в ответ данные
// и вызываем 'onLine' при каждом получении новой строчки:
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");
}
});
// Запрашиваем данные у Puck.js:
connection.write("\x10getData()\n");
});
});
См.также
Внешние ссылки