這裡我們在樹莓派(Raspberry Pi)上使用 Node.js 與 WebSocket 技術,以網頁來呈現即時性的 MPU-6050 加速規感測器資料。
在之前的文章中,我們使用 MPU-6050 的 DMP 來擷取精準的運動感測資料,而接下來我打算在樹莓派上面用 Node.js 架設一個間單的網頁伺服器,將 MPU-6050 的資料即時轉送到網頁上,讓使用者只要打開瀏覽器就可以立即看到目前所收集到的資料。
以下是整個系統的實作重點,當然如果您要實作這樣的系統,請不要用複製貼上的方式來做,由於整個系統的技術細節很多,我也很難把所有的東西都寫出來,只有提一些比較重要的,請看懂之後自己寫,所以如果只是直接複製貼上的話,是做不出來的喔。
這個部分就是單純延續我們之前寫的 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 版本太舊了,所以我使用別人已經編譯好的版本來快速安裝,首先下載放在 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[0].min = now - totalShowPoints * updateInterval; plot.getOptions().xaxes[0].max = now; plot.setupGrid(); plot.draw(); }); });
這是實際測試的影片。