樹莓派 Raspberry Pi 使用 Node.js 與 WebSocket 呈現即時性的 MPU-6050 感測器資料

這裡我們在樹莓派(Raspberry Pi)上使用 Node.js 與 WebSocket 技術,以網頁來呈現即時性的 MPU-6050 加速規感測器資料。

在之前的文章中,我們使用 MPU-6050 的 DMP 來擷取精準的運動感測資料,而接下來我打算在樹莓派上面用 Node.js 架設一個間單的網頁伺服器,將 MPU-6050 的資料即時轉送到網頁上,讓使用者只要打開瀏覽器就可以立即看到目前所收集到的資料。


這是整個系統的架構圖,我們用 C 語言擷取 MPU-6050 的資料後,轉送到 Node.js 的網頁伺服器,再送到瀏覽器上呈現,整個資料的傳遞過程都是即時性的(real-time)串流,所以在瀏覽器上可以看到即時的資料。

display-real-time-mpu-6050-sensor-data-using-node-js-and-websocket-2

以下是整個系統的實作重點,當然如果您要實作這樣的系統,請不要用複製貼上的方式來做,由於整個系統的技術細節很多,我也很難把所有的東西都寫出來,只有提一些比較重要的,請看懂之後自己寫,所以如果只是直接複製貼上的話,是做不出來的喔。

感測資料擷取程式

這個部分就是單純延續我們之前寫的 DMP 擷取程式,然後再加上 socket 的資料傳輸功能,這裡同樣我只說明最關鍵的部分,首先宣告使用 socket 傳輸資料會用的一些變數:

// 使用 socket 傳輸資料會用的一些變數
unsigned char sendBuff[12];
int listenfd,connfd;
struct sockaddr_in serv_addr;
const int portNum = 6000;

其中 snedBuff 是用來儲存資料封包的緩衝區,由於資料的封包是自己設計的,所以就依照自己的需求自己決定長度。而 portNum 是傳輸資料用的連接埠號,這個也可以自己更改。

接著就是初始化 socket 的連線,開一個 TCP 的連接埠等待連線:

listenfd = 0;
connfd = 0;

listenfd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket retrieve success\n");

memset(&serv_addr, '0', sizeof(serv_addr));
memset(sendBuff, '0', sizeof(sendBuff));

serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(portNum);

bind(listenfd, (struct sockaddr*)&serv_addr,sizeof(serv_addr));

if(listen(listenfd, 10) == -1){
    printf("Failed to listen\n");
    return -1;
}

connfd = accept(listenfd, (struct sockaddr*)NULL ,NULL);

// 開始擷取 MPU-6050 的資料

我這裡的設計是先讓 TCP 連線建立好之後,再進行 MPU-6050 的初始化,然後才開始擷取資料,而如果連線尚未建立,就不會去動到 MPU-6050,當然您也可以先初始化 MPU-6050,先收資料再等待連線。

最後在資料擷取的迴圈函數 loop() 中,加上一小段用 socket 傳送資料的程式碼:

#ifdef OUTPUT_READABLE_REALACCEL
  // 實際的加速度(去除重力)
  mpu.dmpGetQuaternion(&q, fifoBuffer);
  mpu.dmpGetAccel(&aa, fifoBuffer);
  mpu.dmpGetGravity(&gravity, &q);
  mpu.dmpGetLinearAccel(&aaReal, &aa, &gravity);
  printf("areal %6d %6d %6d    ", aaReal.x, aaReal.y, aaReal.z);

  // 使用 socket 傳送資料
  *(uint16_t *)(sendBuff+1) = htons(*(uint16_t *)(&aaReal.x));
  *(uint16_t *)(sendBuff+3) = htons(*(uint16_t *)(&aaReal.y));
  *(uint16_t *)(sendBuff+5) = htons(*(uint16_t *)(&aaReal.z));
  write(connfd, sendBuff, 12);
#endif

這裡我將三個軸的加速度放進 sendBuff 中的第 2 個到第 7 個 byte,所以只用到了 6 個 bytes 而已,至於其他的位置就看自己的需求,看要放什麼都可以,當然表頭與表尾記得自己加。

網頁伺服器

我們的網頁伺服器是用 Node.js 寫的,所以要先安裝一下 Node.js。

安裝 Node.js

由於樹莓派官方所提供的 Node.js 版本太舊了,所以我使用別人已經編譯好的版本來快速安裝,首先下載放在 GibHub 上的壓縮檔:

wget https://gist.github.com/raw/3245130/v0.10.24/node-v0.10.24-linux-arm-armv6j-vfp-hard.tar.gz

解壓縮之後,放到適當的位置就可以直接使用了:

tar zxvf node-v0.10.24-linux-arm-armv6j-vfp-hard.tar.gz
sudo mv node-v0.10.24-linux-arm-armv6j-vfp-hard /opt/node

然後在自己的 ~/.bashrc 加上一行 PATH 的設定:

export PATH=$PATH:/opt/node/bin

這樣下次登入之後,就可以直接使用 Node.js 了,如果要直接使用的話,要先載入新的 ~/.bashrc 設定:

source ~/.bashrc

檢查一下 Node.js 的版本:

node -v

輸出為

v0.10.24

這樣基本的 Node.js 就安裝好了。

伺服器

以 Node.js 撰寫一個簡單的網頁伺服器,連線到感測資料擷取程式收資料,然後透過 WebSocket 轉送給瀏覽器。

var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server);

// 網頁伺服器連接埠
server.listen(8000);

app.use(express.static('public'));
var socket = require('net').Socket();

// 連線到感測資料擷取程式
socket.connect(6000, 'localhost');

// 接收資料,透過 WebSocket 轉送
socket.on('data', function(data){
  var buff = Buffer(data);
  var accel_x = data.readInt16BE(1);
  var accel_y = data.readInt16BE(3);
  var accel_z = data.readInt16BE(5);
  io.sockets.emit('sensor_data', {x:accel_x, y:accel_y, z:accel_z});
});

這裡有用到 socket.ioexpress 這兩個 Node.js 套件,要另外安裝:

npm install socket.io express

關於 Socket.IO 的技術部分,可以參考使用 Node.js 與 Socket.IO 建立即時性網頁應用程式

而在瀏覽器端,還要撰寫一段收資料並且畫圖的程式,這裡我們的圖形使用 Flot 來畫,首先引入必要的 JavaScript:

<script language="javascript" type="text/javascript" src="js/jquery.js"></script>
<script language="javascript" type="text/javascript" src="js/jquery.flot.js"></script>
<script language="javascript" type="text/javascript" src="js/jquery.flot.time.js"></script>
<script language="javascript" type="text/javascript" src="/socket.io/socket.io.js"></script>

然後用 Flot 畫出 WebSocket 收到的資料:

$(function() {
  var socket = io();
  var accXDataBuff = [];
  var accYDataBuff = [];
  var accZDataBuff = [];
  var totalShowPoints = 300;
  var totalPoints = totalShowPoints + 30;
  var updateInterval = 10;
  var plot = $.plot("#placeholder", [
      {label: "Acc. X", data: accXDataBuff},
      {label: "Acc. Y", data: accYDataBuff},
      {label: "Acc. Z", data: accZDataBuff} ], {
    series: {
      shadowSize: 0
    },
    yaxis: {
      min: -32767/2,
      max: 32768/2
    },
    xaxis: {
      mode: "time",
      timezone: "browser",
      show: true
    },
    legend: {
      show: true
    }
  });
  socket.on('sensor_data', function (data) {
    if (accXDataBuff.length >= totalPoints) {
      accXDataBuff.shift();
    }
    if (accYDataBuff.length >= totalPoints) {
      accYDataBuff.shift();
    }
    if (accZDataBuff.length >= totalPoints) {
      accZDataBuff.shift();
    }
    var now = new Date().getTime();
    accXDataBuff.push([now, data.x]);
    accYDataBuff.push([now, data.y]);
    accZDataBuff.push([now, data.z]);
    plot.setData([
        {label: "Acc. X", data: accXDataBuff},
        {label: "Acc. Y", data: accYDataBuff},
        {label: "Acc. Z", data: accZDataBuff}]);
    plot.getOptions().xaxes[0].min = now - totalShowPoints * updateInterval;
    plot.getOptions().xaxes[0].max = now;
    plot.setupGrid();
    plot.draw();
  });
});

這是實際測試的影片。

樹莓派, 物聯網

2 Comments

  1. AdamMao

    不好意思,請問如何打開編輯程式的環境?
    我已經跑完安裝指令了,但不知道到下一步該怎麼做
    我懂得使用putty,但不知道要開啟哪個程式進行編輯。

    • G. T. Wang

      您應該先從 Linux 基本操作開始學起。

Leave a Reply