使用SerialPort庫進行Node物聯網專案開發
如果說Nodejs將JavaScript的應用從網頁端擴充套件到了伺服器和作業系統端,Electron為JavaScript實現了跨平臺應用的能力,那麼SerialPort就是打通JavaScript軟體與硬體的關鍵部件。著名的Johnny-Five物聯網平臺開發包的核心部件就是SerialPort,而Mozilla的WebThings
Gateway物聯閘道器也是在SerialPort基礎上實現的。這是因為,雖然已經歷經了幾十年光陰,串列埠在通訊傳輸速度上已經遠遠跟不上現代的通訊手段,但由於其廉價、簡便、穩定可靠且經歷時間驗證的特點,在當前的工業與民生中仍然具有相當重要的地位,而SerialPort正是作為串列埠與計算機系統連線的中樞,成為網際網路開發利器的JavaScript打通軟體與硬體系統的關鍵。
更準確來說,SerialPort是執行在Node平臺上的開發包,其安裝也是透過npm install完成的。在SerialPort官方首頁有一段簡單的應用例程。裝完SerialPort後,用這段程式就可以立即上手(當然,你還需要一個串列埠終端裝置並且編寫了終端部分的程式,如果沒有,也可以透過本文後面的模擬器模擬出來一個。)
const
SerialPort
=
require
(
‘serialport’
)
const
Readline
=
require
(
‘@serialport/parser-readline’
)
const
port
=
new
SerialPort
(
path
,
{
baudRate
:
256000
})
const
parser
=
new
Readline
()
port
。
pipe
(
parser
)
parser
。
on
(
‘data’
,
line
=>
console
。
log
(
`>
${
line
}
`
))
port
。
write
(
‘ROBOT POWER ON\n’
)
//> ROBOT ONLINE
SerialPort包由SerialPort,Bindings,Interfaces和Parsers幾部分組成,並且提供了一些比如串列埠列表等命令列工具(這些工具在舊版本里是SerialPort的一部分,但是目前版本中都可以獨立運行了)。
1。 Bindings
Bindings(繫結)是SerialPort連線軟體與硬體平臺的基礎,一般來說,SerialPort庫會自動探測並與平臺繫結(Binding),不需要人為去呼叫Bindings來繫結Linux、Windows或是mac平臺。當然,SerialPort也提供了一個修改Bindings的路徑,這是透過內建的Stream包實現的(按照開發者的意圖,使用者永遠不需要直接操作Bindings包)。
var
SerialPort
=
require
(
‘@serialport/stream’
);
SerialPort
。
Binding
=
MyBindingClass
;
通常來說,人為修改繫結在使用中並沒有太大便利,但對於除錯則非常重要,這是因為,在除錯時使用者可以修改繫結來呼叫一個模擬的串列埠。
2。 Stream Interfaces
SerialPort的介面介面是透過流(Stream)實現的,流也是Nodejs的核心部件之一。在新建SerialPort時,需要提供串列埠的常規引數,包括portName埠號,baudRate波特率等等,主要包括下面這些屬性。
/**
* @typedef {Object} openOptions
* @property {boolean} [autoOpen=true] 此選項為真時會在自動開啟串列埠。
* @property {number=} [baudRate=9600] 波特率110~115200,支援自定義(這一點太強大了)
* @property {number} [dataBits=8] 資料位,可以是8, 7, 6, or 5。
* @property {number} [highWaterMark=65536] 資料緩衝區大小,最大64k。
* @property {boolean} [lock=true] 鎖定串列埠禁止其他裝置使用,在windows平臺下只能為true。
* @property {number} [stopBits=1] 停止位: 1 or 2。
* @property {string} [parity=none] 校驗位:‘none’,‘even’,‘mark’,‘odd’,‘space’。
* @property {boolean} [rtscts=false] 流(flow)控制設定,以下幾個都是
* @property {boolean} [xon=false]
* @property {boolean} [xoff=false]
* @property {boolean} [xany=false]
* @property {object=} bindingOptions 繫結選項,一般不需要設定
* @property {Binding=} Binding 預設為靜態屬性`Serialport。Binding`。
* @property {number} [bindingOptions。vmin=1] 參見linux下 termios命令
* @property {number} [bindingOptions。vtime=0]
*/
一個SerialPort物件支援的屬性包括baudRate,binding,isOpen和path,其中除了baudRate可以透過update方法修改為,其他屬性均為只讀。
一個SerialPort物件支援的事件包括open,error,close,data,drain,使用時可透過監聽不同事件處理任務。
一個SrialPort物件支援open,update,close,read,write等方法,用於實現各種串列埠功能。
需要注意的是,隨著版本的不斷更迭,SerialPort的介面內容也不斷有所調整,程式設計時要格外小心。例如,通常在新建串列埠前需要先獲取可用的串列埠列表。官方提供了靜態方法SerialPort。list()可以返回當前所有串列埠列表。但隨著版本更新,官方文件中的SerialPort已經升級為Promise方式,文件中直接獲取的方法會返回錯誤,需要修改為類似以下形式才能使用。
SerialPort
。
list
()
。
then
((
ports
)
=>
{
ports
。
forEach
(
(
port
)=>
{
console
。
log
(
port
。
comName
);
console
。
log
(
port
。
pnpId
);
console
。
log
(
port
。
manufacturer
);
});
})
。
catch
((
err
)=>{
console
。
log
(
err
);
});
3。 Parsers
Parsers包繼承自Nodejs Transform Stream,提供了一些實用的串列埠協議解析介面,比如對收到的大量串列埠資料,要根據特定的標識進行分割、解析的時候,就可以用Delimiter解析器,類似的解析器還包括Readline(分行讀取),ByteLength(按長度擷取),InterByteTimeout(超時)以及功能強大的Regex(正則表示式)解析器等等。
const
SerialPort
=
require
(
‘serialport’
)
const
Readline
=
require
(
‘@serialport/parser-readline’
)
const
port
=
new
SerialPort
(
‘/dev/tty-usbserial1’
)
const
parser
=
new
Readline
()
port
。
pipe
(
parser
)
parser
。
on
(
‘data’
,
console
。
log
)
port
。
write
(
‘ROBOT PLEASE RESPOND\n’
)
//也可以簡化為
const
parser
=
port
。
pipe
(
new
Readline
());
SerialPort的Parsers大部分都是分割長資料,在實際使用中遇到了一個數據不足的問題,即由於傳輸速度低,一條訊息被分割成好幾條傳送,在收到之後需要組合在一起(有特定的字元表示完整資訊的結尾),在這種情況下,官方的Parsers均不合適,為了實現這一功能,自己手動寫了一個ConcatParser,來實現將幾段資料拼接的功能,ConcatParser同時提供了超時和超出緩衝區長度兩個選項,以保證埠不會處於無限等待的狀態。ConcatParser的實現如下。
‘use strict’
;
const
{
Transform
}
=
require
(
‘stream’
);
class
ConcatParser
extends
Transform
{
constructor
(
options
=
{})
{
super
(
options
);
try
{
if
(
typeof
options
。
boundary
===
‘undefined’
)
{
throw
new
TypeError
(
‘“boundary” is not a bufferable object’
);
}
if
(
options
。
boundary
。
length
===
0
)
{
throw
new
TypeError
(
‘“boundary” has a 0 or undefined length’
);
}
this
。
includeBoundary
=
typeof
options
。
includeBoundary
!==
‘undefined’
?
options
。
includeBoundary
:
true
;
this
。
interval
=
typeof
options
。
interval
!==
‘undefined’
?
options
。
interval
:
3000
;
this
。
maxBufferSize
=
typeof
options
。
maxBufferSize
!==
‘undefined’
?
options
。
maxBufferSize
:
65535
;
this
。
intervalID
=
-
1
;
this
。
boundary
=
Buffer
。
from
(
options
。
boundary
);
this
。
buffer
=
Buffer
。
alloc
(
0
);
}
catch
(
error
)
{
throw
new
Error
(
‘Init concatparser error’
);
}
}
_transform
(
chunk
,
encoding
,
cb
)
{
clearTimeout
(
this
。
intervalID
);
let
data
=
Buffer
。
concat
([
this
。
buffer
,
chunk
]),
dataLength
=
data
。
length
,
position
;
if
(
dataLength
>=
this
。
maxBufferSize
)
{
this
。
buffer
=
data
。
slice
(
0
,
this
。
maxBufferSize
);
data
=
Buffer
。
alloc
(
0
);
this
。
emitData
();
}
else
if
((
position
=
data
。
indexOf
(
this
。
boundary
))
!==
-
1
)
{
this
。
buffer
=
data
。
slice
(
0
,
position
+
(
this
。
includeBoundary
?
this
。
boundary
。
length
:
0
));
data
=
Buffer
。
alloc
(
0
);
this
。
emitData
();
}
this
。
buffer
=
data
;
this
。
intervalID
=
setTimeout
(
this
。
emitData
。
bind
(
this
),
this
。
interval
);
cb
();
}
emitData
()
{
clearTimeout
(
this
。
intervalID
);
if
(
this
。
buffer
。
length
>
0
)
{
this
。
push
(
this
。
buffer
);
}
this
。
buffer
=
Buffer
。
alloc
(
0
);
}
_flush
(
cb
)
{
this
。
emitData
();
cb
();
}
}
module
。
exports
=
ConcatParser
;
4。 命令列介面
SerialPort庫提供了幾個命令列介面,可以透過npx直接執行。幾個介面分別是SerialPort List,SerialPort REPL和SerialPort Terminal,使用方法為:
npx
@
serialport
/
list
[
options
]
//可能需要先安裝 npm @serialport/list
npx
@
serialport
/
repl
<
port
>
npx
@
serialport
/
terminal
-
p
<
port
>
[
options
]
@serialport/list 用來列出系統中所有串列埠,接受格式化、版本等選項,可以透過-h 檢視幫助。@serialport/repl提供了一個可以直接透過命令列操作串列埠的介面,可以透過命令列直接進行串列埠讀寫等操作。@serialport/terminal提供了一個簡單的介面可以獲取連線在串列埠上的終端裝置的基礎資訊。
5。 Mock串列埠模擬器
Mock是SerialPort庫中最好用的功能之一,它透過模擬的硬體串列埠介面,讓開發工作可以脫離硬體來完成測試。這在是DD或者TDD開發過程中是必不可少的。一個簡單的Mock串列埠模擬器使用方法如下:
const
SerialPort
=
require
(
‘@serialport/stream’
)
const
MockBinding
=
require
(
‘@serialport/binding-mock’
)
SerialPort
。
Binding
=
MockBinding
// Create a port and enable the echo and recording。
MockBinding
。
createPort
(
‘/dev/ROBOT’
,
{
echo
:
true
,
record
:
true
})
const
port
=
new
SerialPort
(
‘/dev/ROBOT’
)
Mock串列埠模擬器可以實現SerialPort庫中所有功能的測試,透過Mock的原始碼可以看出這些介面及測試時檢測的途徑。
const
AbstractBinding
=
require
(
‘@serialport/binding-abstract’
)
const
debug
=
require
(
‘debug’
)(
‘serialport/binding-mock’
)
let
ports
=
{}
let
serialNumber
=
0
function
resolveNextTick
(
value
)
{
return
new
Promise
(
resolve
=>
process
。
nextTick
(()
=>
resolve
(
value
)))
}
/**
* Mock包,用來模擬硬體串列埠的實現
*/
class
MockBinding
extends
AbstractBinding
{
//如果record為真,這個快取Buffer中會存入所有寫到虛擬串列埠中的內容,可以檢查串列埠傳出的資料
readonly
recording
:
Buffer
// 最後一次寫入串列埠的快取Buffer
readonly
lastWrite
:
null
|
Buffer
//靜態方法,用於建立一個虛擬串列埠
static
createPort
(
path
:
string
,
opt
:
{
echo
?:
boolean
,
record
?:
boolean
,
readyData
?:
Buffer
})
:
void
//靜態方法,用於復位所有虛擬串列埠
static
reset
()
:
void
// 靜態方法,用於列出所有虛擬串列埠
static
list
()
:
Promise
<
PortInfo
[]
>
// 從一個虛擬串列埠Emit(發出)指定資料
emitData
(
data
:
Buffer
|
string
|
number
[])
// 其他支援的標準串列埠介面方法
open
(
path
:
string
,
opt
:
OpenOpts
)
:
Promise
<
void
>
close
()
:
Promise
<
void
>
read
(
buffer
:
Buffer
,
offset
:
number
,
length
:
number
)
:
Promise
<
Buffer
>
write
(
buffer
:
Buffer
)
:
Promise
<
void
>
update
(
options
:
{
baudRate
:
number
})
:
Promise
<
void
>
set
(
options
)
:
Promise
<
void
>
get
()
:
Promise
<
Flags
>
getBaudRate
()
:
Promise
<
number
>
flush
()
:
Promise
<
void
>
drain
()
:
Promise
<
void
>
}