среда, 8 января 2014 г.

Arduino + web server (node.js + serialport + mongo db + express + d3.js)

В прошлый раз удалось наладить передачу данных от Arduino на веб сервер.
Теперь следующий шаг, буду записывать данные от датчиков в БД и результат смотреть через браузер.
Для примера сделал такой скетч для Arduino. Просто раз в секунду вычисляю квадратный корень следующего по счёту целого числа.
int i = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  i++;
  Serial.println(sqrt(i));
  delay(1000);
}

С Arduino разобрался. Далее - работа с веб сервером и СУБД.

Веб сервер

В качестве фреймворка для веб сервера взял express для node.js. Установил express так, как советуют тут.
$ sudo npm install -g express
$ express arduino-web
$ cd arduino-web
$ npm install
$ node app

После этого пришлось исправить doctype в файле views/layout.jade, для этого заменил первую строку doctype 5 на doctype.
По ссылке http://localhost:3000/ открывается стартовая страничка. Отлично, можно отправить на неё данные с Arduino.

На клиенте данные буду строить с помощью D3.js, с помощью этой библиотеки данные легко визуализировать на клиенте, разгрузив сервер.

В качестве СУБД будет MongoDB, у неё дешёвая запись. Это может пригодиться, если понадобится часто логировать показания датчиков.

Запись в базу

У меня Ubuntu, поэтому устанавливаю MongoDB, как описано тут.
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
$ echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
$ sudo apt-get install mongodb-10gen

Теперь устанавливаю mongodb для nodejs.
$ npm install mongodb

Данные от Arduino буду записывать в коллекцию sqrt с атрибутами time (текущее время), value (значение).
Для этого делаю такого демона getdata.js, воспользовавшись этой удачной статьёй.
/**
 * Сохранение в БД данных от Arduino
 */

console.log("Connect to database...");
var _getData = {
  database: {
    mongo: require('mongodb'),
    host: 'localhost',
    name: 'sqrt',
    port: null,
    db: null
  },
  arduino: {
    port: "/dev/ttyACM0",
    baudrate: 9600
  }
};
_getData.database.port = _getData.database.mongo.Connection.DEFAULT_PORT;
_getData.database.db = new _getData.database.mongo.Db(_getData.database.name, 
    new _getData.database.mongo.Server(
    _getData.database.host, _getData.database.port, {}), {safe: false});

// Соединяемся с БД
_getData.database.db.open(function(err, db) {
  console.log("Connected to database.");
  
  // Подключаемся к порту
  var serialport = require("serialport");
  var SerialPort = serialport.SerialPort;
  var serialPort = new SerialPort(_getData.arduino.port, {
    baudrate: _getData.arduino.baudrate
  });
  serialPort.on("open", function() {
    // Читаем данные из порта
    var readData = "";
    var first = true;
    console.log("Waiting for data from Arduino...");
    serialPort.on('data', function(data) {
      var dataStr = data.toString();

      // Парсим данные (делим на строки)
      for (var i = 0, l = dataStr.length; i < l; i++) {
        if (dataStr.charAt(i) != "\n" && dataStr.charAt(i) != "\r") {
          readData += dataStr.charAt(i);
        }
        else if (readData) {
          // Пропускаем первое полученное значение
          if (!first) {
            db.collection('sqrt', function(err, collection) {
              var date = new Date();

              // Сохраняем значение в базу
              collection.insert({
                time: date,
                value: readData
              });

              console.log(date + ': ' + readData + ';');
            });
          }
          else {
            first = false;
          }
          readData = "";
        }
      }
    });
  });
});

Чтобы он работал, устанавливаю ещё serialport и запускаю демона.
$ npm install serialport
$ node getdata.js

Отображение данных в браузере

Данные, которые присылает Arduino, сохраняются в БД. Теперь можно написать кусок кода, с помощью которого эти данные можно будет увидеть в браузере. Далее я буду каждые 5 секунд отправлять на сервер AJAX запрос для получения информации за последние 5 минут.

Вот какие манипуляции для этого совершаю.
Один. Прописываю маршрут для express. В файле app.js добавляю строки
var data = require('./routes/get-data');
...
app.get('/get-data', data.get_data);

Два. Забираю данные из БД (в routes/get-data.js.).
/*
 * GET home page.
 */

exports.get_data = function(req, res){
  // Читаем данные из БД
  var _getData = {
    database: {
      mongo: require('mongodb'),
      host: 'localhost',
      name: 'sqrt',
      port: null,
      db: null
    }
  };
  _getData.database.port = _getData.database.mongo.Connection.DEFAULT_PORT;
  _getData.database.db = new _getData.database.mongo.Db(_getData.database.name,
      new _getData.database.mongo.Server(
      _getData.database.host, _getData.database.port, {}), {safe: false});

  // Соединяемся с БД
  _getData.database.db.open(function(err, db) {

    db.collection('sqrt', function(err, collection) {

      // Выбераем из таблицы всё, что не старее 10 минут
      var dateFrom = new Date(Date.parse(new Date()) - 1000 * 60 * 10);
      collection.find({time: {'$gt': dateFrom}}).toArray(function(err, docs) {
        console.log(docs.length);
        res.send(JSON.stringify(docs));
        db.close();
      });

    });
  });

};

Три. Отображаю данные в браузере, по аналогии с этим примером (в javascripts/getdata.js).
var margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.time.scale()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var line = d3.svg.line()
    .x(function(d) { return x(d.time); })
    .y(function(d) { return y(d.value); });

var svg = d3.select("#data").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

getData();

setInterval(function() { getData(); }, 1000*5);

function getData() {
  d3.json("get-data", function(error, data) {
    data.forEach(function(d) {
      d.time = Date.parse(d.time);
      d.value = +d.value;
    });

    x.domain(d3.extent(data, function(d) { return d.time; }));
    y.domain(d3.extent(data, function(d) { return d.value; }));

    svg.selectAll("g")
      .remove();
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)
      .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Value");

    svg.selectAll("path")
      .remove();
    svg.append("path")
        .datum(data)
        .attr("class", "line")
        .attr("d", line);
  });
}

В итоге получаю такую картинку:


Каждые 5 секунд она обновляется.

Всё. За качество кода не боролся, главная задача тут - отработать подход и убедиться, что нет серьёзных подводных камней. Код этого примера можно посмотреть на Гитхабе.