Espruino:Примеры/Сбор данных при помощи Espruino

Материал из Онлайн справочника
Перейти к навигации Перейти к поиску

Перевод: Максим Кузьмин (Cubewriter) Контакты:</br>* Skype: cubewriter</br>* E-mail: cubewriter@gmail.com</br>* Максим Кузьмин на freelance.ru
Проверка/Оформление/Редактирование: Мякишев Е.А.


Сбор данных при помощи Espruino[1]

Зачастую требуется собрать устройство, которое будет просто «сидеть» в одном месте и собирать (регистрировать) данные. Espruino-устройства особенно хорошо подходят для этой задачи, т.к. все они оснащены часами реального времени (RTC-часами) и в состоянии простоя расходуют очень мало энергии.

Примечание: Пример сбора данных с помощью Bangle.js можно найти тут.

На простейшем уровне ваш код должен выглядеть как-то так:

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 её лучше сначала извлечь программно. Это делается при помощи функции E.unmountSD().

Извлечение данных

Теперь, когда данные сохранены, их можно прочесть.

Если вы используете SD-карту, то ничего сложного – просто выньте её из Espruino и вставьте в ПК.

При использовании других методов вывод данных лучше делать по USB-соединению (или через Bluetooth, если вы подключены к Puck.js). Вы не сможете загрузить всё в RAM-память за раз, поэтому вам нужно будет разбить этот процесс на несколько итераций.

Должно сработать что-то вроде этого:

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

Примечание: При использовании способа с типизированным массивом итерации лучше делать по направлению вперёд при помощи logIndex+1 – чтобы сохранить правильный порядок.

Простой пример

Для этого примера мы воспользуемся функциями, которыми, во-первых, легко пользоваться, и которые, во-вторых, есть на всех платах – температурным датчиком и типизированными массивами.

Просто скопируйте и вставьте код ниже в правую часть 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");
  });
});

См.также

Ссылки на полезные ресурсы

Внешние ссылки