這裡我們在樹莓派(Raspberry Pi)上使用 Node.js 與 WebSocket 技術,以網頁來呈現即時性的 MPU-6050 加速規感測器資料。
在之前的文章中,我們使用 MPU-6050 的 DMP 來擷取精準的運動感測資料,而接下來我打算在樹莓派上面用 Node.js 架設一個間單的網頁伺服器,將 MPU-6050 的資料即時轉送到網頁上,讓使用者只要打開瀏覽器就可以立即看到目前所收集到的資料。
這是整個系統的架構圖,我們用 C 語言擷取 MPU-6050 的資料後,轉送到 Node.js 的網頁伺服器,再送到瀏覽器上呈現,整個資料的傳遞過程都是即時性的(real-time)串流,所以在瀏覽器上可以看到即時的資料。

以下是整個系統的實作重點,當然如果您要實作這樣的系統,請不要用複製貼上的方式來做,由於整個系統的技術細節很多,我也很難把所有的東西都寫出來,只有提一些比較重要的,請看懂之後自己寫,所以如果只是直接複製貼上的話,是做不出來的喔。
感測資料擷取程式
這個部分就是單純延續我們之前寫的 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.io 與 express 這兩個 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[].min = now - totalShowPoints * updateInterval;
plot.getOptions().xaxes[].max = now;
plot.setupGrid();
plot.draw();
});
});
這是實際測試的影片。
