如果說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

>

}