這個階段在研究樹莓派,主要用 Node.js 開發。開源社區有許多非常優秀的[樹莓派 Node.js 庫](https://gist.github.com/jperkin/e1f0ce996c83ccf2bca9),但是沒有讓我覺得特別好用的。因為這些庫有的 API 還不錯,但是性能不好,有的性能很好,但是 API 不好用。還有一些庫沒跟上樹莓派的硬件升級,使用起來還要自己修改,作者也不處理 pull request,比較麻煩。因此我自己著手重新寫一個庫?[rpio2](https://github.com/akira-cn/rpio2)。
在寫庫的過程中,自然要充分測試每個 API,個人比較喜歡 TDD / BDD 開發,因此一邊開發功能,一邊寫各個 API 的單元測試。寫了單元測試,自然希望在代碼提交的過程中能夠持續集成,最好是直接使用?[travis-ci](https://www.h5jun.com/post/travis-ci.org)。
愿望是美好的,但是現實有點殘酷。因為樹莓派開發不同于其他軟件開發,它涉及到硬件,雖然說單元測試可以很簡單,主要是測試引腳的同步/異步輸入輸出和事件(中斷)響應,所以開發的時候可以在樹莓派環境里跑單元測試,這沒有問題,但在集成的時候,我們沒有辦法讓 travis-ci 用樹莓派系統環境來跑我們的 test case 吧。這樣的話,就需要我們自己實現對底層的 GPIO 的模擬。
## 實現對 GPIO 的模擬
聽起來不錯,那就開始干吧!
rpio2 庫是基于?[node-rpio](https://github.com/jperkin/node-rpio)?的,選擇這個庫為基礎的原因是,node-rpio 又是對?[bcm2835](http://www.airspayce.com/mikem/bcm2835/)?C 語言驅動庫的 Node 封裝,因為 bcm2835 是用 C 寫的針對 Broadcom BCM 2835 處理器(也就是樹莓派現在使用的 CPU)的底層驅動,因此它可以達到非常高的性能。
使用了 bcm2835 一個額外的好處是,C 語言驅動庫是模塊化的函數單元,這對于測試和模擬來說是友好的,因為只需要知道對應的輸入輸出就可以了。而很多輸入輸出查規格說明書就可以了。
比如:`rpio.open(pin, mode, state)`,當 mode 為 INPUT 的時候,如果不傳 state 參數,默認的輸入電阻是:當 pin <= 8 時,為 PULL_UP,當 8 < pin <= 27 時,為 PULL_DOWN。
這里面比較不好實現的一塊是輸入信號,因為樹莓派可以讓 GPIO 接輸入設備,而輸入設備的輸入信號是實時輸入的,如果在 JS 里用定時器模擬,并不能做到實時同步輸入(因為 JS 是單線程非阻塞模型),所以這里需要考慮多線程,最簡單的方法就是用 File API + child_process。
rpio_sim_helper.js
~~~
"use strict";
const fs = require("fs");
var pin = 0|process.argv[2];
var timers = process.argv[3].split(",").map(o => 0|o);
var fileName = "./test/gpio/gpio" + pin;
function writeAndWait(value, time){
fs.writeFileSync(fileName, new Buffer([value]));
return new Promise(function(resolve){
setTimeout(resolve, time);
});
}
var p = Promise.resolve();
for(var i = 0; i < timers.length; i += 2){
p = p.then(((i) => () => writeAndWait(timers[i], timers[i + 1]))(i));
}
p.then(function(){
var content = fs.readFileSync(fileName);
console.log(content);
}).catch(err => console.log(err));
~~~
上面實現一個簡單的程序來開進程寫文件,例如,要給 rpio21 引腳模擬發送一段半周期為 100 毫秒的脈沖信號,可以這么用:
~~~
node rpio_sim_helper.js 21 1,100,0,100,1,100,0,100...,1,100,0
~~~
這樣,將它封裝進模擬庫中:
~~~
helper: function (pin, signal){
if(config.mapping === "physical"){
pin = pinMap[pin];
}
return new Promise(function(resolve, reject){
child_process.exec("node ./test/rpio_sim_helper " + pin + " " + signal,
function(err, res){
if(err) reject(err);
else resolve(res);
});
});
~~~
就可以很方便地模擬輸入信號了。
## 對單元測試啟用模擬
這很容易實現,一開始,我打算采用?[proxyquire](https://github.com/thlorenz/proxyquire)?庫,這個庫可以“代理”一個被測試文件里正常 require 的庫,這樣我只要將 rpio2 的 require('rpio') 用自己模擬庫替代就行了。然而實際使用的時候發現并不行。
主要原因是,使用 proxyquire 還是會先加載 rpio 庫,然后才對加載的 rpio 庫進行替換,而 rpio 庫里面有這樣一段代碼:
~~~
var gpiomap;
function setup_board()
{
var cpuinfo, boardrev, match;
cpuinfo = fs.readFileSync("/proc/cpuinfo", "ascii", function(err) {
if (err)
throw err;
});
cpuinfo.toString().split(/\n/).forEach(function (line) {
match = line.match(/^Revision.*(.{4})/);
if (match) {
boardrev = parseInt(match[1], 16);
return;
}
});
switch (boardrev) {
case 0x2:
case 0x3:
gpiomap = "v1rev1";
break;
case 0x4:
case 0x5:
case 0x6:
case 0x7:
case 0x8:
case 0x9:
case 0xd:
case 0xe:
case 0xf:
gpiomap = "v1rev2";
break;
case 0x10:
case 0x12:
case 0x13:
case 0x15:
case 0x92:
case 0x1041:
case 0x2082:
gpiomap = "v2plus";
break;
default:
throw "Unable to determine board revision";
break;
}
}
setup_board();
~~~
上面這段代碼通過?`/proc/cpuinfo`?讀取樹莓派的版本信息,然而我本地 Mac 環境里并沒有?`/proc/cpuinfo`,因此加載的時候直接報錯了。而且這個文件單元測試時沒必要加載,直接在測試時加載模擬庫就行了。
因此,更簡單的方法是測試時,將 rpio2 中的?`require('rpio')`?直接替換成加載?`rpio_sim.js`。這一步很多部署工具都可以做,比如 gulp 就是很好的選擇。不過我[用 babel 插件來實現代碼測試覆蓋度檢查](https://www.h5jun.com/post/code-coverage-with-babel-plugin.html),所以我就直接用 babel 來做了,代碼更少,也很方便:
transform_rpio_sim.js
~~~
module.exports = function(babel) {
var t = babel.types;
return {visitor: {
CallExpression: function(path){
if(path.node.callee.name === "require"){
var module = path.node.arguments[0];
if(module && module.value === "rpio"){
module.value = "../rpio_sim.js";
}
}
}
}};
};
~~~
然后我們將這些結合到一起,在 package.json 中編寫測試命令:
~~~
"scripts": {
"test": "babel lib --out-dir test/lib --plugins ../test/transform_rpio_sim.js && mocha test/spec.js",
"printcov": "script/printcov.js lib/coverage.lcov lib",
"test-cov": "babel lib --out-dir test/lib --plugins ../test/transform_rpio_sim.js,transform-coverage && mocha test/spec.js --reporter=mocha-lcov-reporter > lib/coverage.lcov && npm run printcov"
},
~~~
這樣就能得到單元測試結果和測試覆蓋度結果:


## 開始集成
接下來我們要在代碼提交到 github 時啟用 travis-ci 集成,我們和以前一樣寫一個 .travis.yml 文件:
~~~
language: node_js
node_js:
- "4"
sudo: false
script:
- "npm run test-cov"
after_script: "npm install coveralls && cd lib && cat coverage.lcov | ../node_modules/coveralls/bin/coveralls.js && cd .."
~~~
但是這里有個問題,因為我們的 package.json 文件里有 rpio 的依賴:
~~~
"dependencies": {
"rpio": "^0.9.11"
},
"devDependencies": {
"consoler": "^0.2.0",
"chokidar": "^1.6.0",
"wait-promise": "0.4.1",
"babel-cli": "6.x.x",
"babel-runtime": "6.x.x",
"mocha": "^2.3.4",
"mocha-lcov-reporter": "^1.2.0",
"chai": "^3.4.1",
"babel-plugin-transform-coverage": "^0.1.5"
},
~~~
而這個?`rpio`?在 travis-ci 的集成環境里并不能編譯過去,而且實際上我們測試不需要它。因此,這里需要再寫一段 pre-install 腳本將它去掉:
travis_pre_install.sh
~~~
#!/bin/sh
sed -i "/"rpio": .*/d" ./package.json
~~~
然后將這個腳本 hook 到 .travis.yml 的命令中去:
~~~
language: node_js
node_js:
- "4"
sudo: false
before_install:
- sh travis_pre_install.sh
script:
- "npm run test-cov"
after_script: "npm install coveralls && cd lib && cat coverage.lcov | ../node_modules/coveralls/bin/coveralls.js && cd .."
~~~
最后,將代碼提交到 github,就能完成自動化的持續集成了:[](https://travis-ci.org/akira-cn/rpio2)
自動的單元測試代碼覆蓋度檢查也是正常的(如果現在測試覆蓋度低,請忽略,因為這個庫還在更新中~):[](https://coveralls.io/github/akira-cn/rpio2)
## 總結
我們要做的事情目標很明確:構建一套適合于樹莓派(實際上也適合于其他硬件開發)的純軟件 TDD 方法。在這里我用到了各種技術的組合來實現我的目的,包括:
* 用啟進程讀寫文件的方式來模擬信號輸入
* 用 Babel 插件實現模擬庫代替真實庫
* 用 bash 腳本來實現集成環境的編譯預處理
我們會發現,工程師自己實現一個代碼庫,尤其是涉及工程化的一些工作的時候,知識的廣度會很重要。
而且,合理使用各種知識最終解決問題也是一個有趣的過程,不是嗎?
有任何問題,歡迎留言區討論。
本文鏈接:[https://www.h5jun.com/post/pi-and-tdd.html](https://www.h5jun.com/post/pi-and-tdd.html)
--?EOF?--
作者?[`admin`?](https://www.h5jun.com/author/admin)發表于?*2016-07-28 15:19:53*?,并被添加「?[`JavaScript`?](https://www.h5jun.com/tags/JavaScript)[`TDD`?](https://www.h5jun.com/tags/TDD)[`Pi`?](https://www.h5jun.com/tags/Pi)」標簽 ,最后修改于?*2016-07-28 17:48:25*