@signalk/vedirect-serial-usb 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -7
- package/index.js +130 -70
- package/lib/Parser.js +11 -11
- package/lib/fields.js +32 -0
- package/lib/productIds.json +3 -1
- package/lib/serial.js +41 -31
- package/lib/tcp.js +25 -23
- package/lib/udp.js +11 -11
- package/migration.js +54 -0
- package/package.json +5 -4
- package/standalone.js +67 -0
- package/test/index.js +3 -3
package/README.md
CHANGED
|
@@ -2,30 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
# vedirect-serial-usb
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This code is a [Signal K Node Server](https://github.com/SignalK/signalk-server-node) plugin. It reads and parses Victron VE.Direct data.
|
|
6
6
|
|
|
7
|
+
The common way to connect to a Victron product is with the Victron [VE.Direct to USB interface](https://www.victronenergy.com/accessories/ve-direct-to-usb-interface) cable.
|
|
8
|
+
Alternatively, this plugin also supports receiving the data via UDP and on a TCP socket.
|
|
9
|
+
|
|
10
|
+
Compatible Victron products:
|
|
11
|
+
|
|
12
|
+
- BMV-700 series of Battery Monitors (reported to work also on the BMV-600)
|
|
13
|
+
- SmartShunt series of Battery Monitors
|
|
14
|
+
- SmartSolar Chargers
|
|
15
|
+
- BlueSolar Chargers
|
|
16
|
+
- Phoenix Inverters, including the Smarts, having a VE.Direct comms port
|
|
17
|
+
|
|
18
|
+
Note that above list may not be complete.
|
|
19
|
+
|
|
20
|
+
The TCP and UDP connection methods are to allow connecting to a Victron product
|
|
21
|
+
too far away to run a serial cable. Ie. using a bridge to LAN/Wi-Fi that takes
|
|
22
|
+
the serial data and makes it available on a TCP socket or sends it out as UDP
|
|
23
|
+
packets. There are many of such devices available, as well as example DIY projects.
|
|
24
|
+
A good example on how to make it yourself is explained here:
|
|
25
|
+
https://pysselilivet.blogspot.com/2021/02/victron-vedirect-with-raspberry.html.
|
|
26
|
+
|
|
27
|
+
Lastly, note that when having a Victron GX Device, you won't be needing this
|
|
28
|
+
plugin. Details for that [here](https://github.com/sbender9/signalk-venus-plugin).
|
|
7
29
|
|
|
8
30
|
### Installation
|
|
9
31
|
|
|
10
32
|
Use the Signal K app store or install via NPM in the Signal K server root directory: `npm install @signalk/vedirect-serial-usb`
|
|
11
33
|
|
|
12
|
-
|
|
13
34
|
### Usage
|
|
14
35
|
|
|
15
36
|
Set up the appropiate device on the settings page of this plugin in the Signal K server admin UI, for instance to `/dev/ttyUSB0` and enable the plugin. Your VE.Direct data will be available in Signal K format via various clients and apps.
|
|
16
37
|
|
|
17
38
|
### Connections
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
40
|
+
**Select device**
|
|
41
|
+
|
|
42
|
+
- Serial, UDP or TCP
|
|
43
|
+
|
|
44
|
+
**Connection details**
|
|
45
|
+
|
|
46
|
+
- Serial: Enter device path e.g `/dev/ttyUSB0`
|
|
47
|
+
- UDP: _`ignored`_
|
|
48
|
+
- TCP: Enter host `IP address`
|
|
49
|
+
|
|
50
|
+
**Port**
|
|
22
51
|
|
|
23
|
-
|
|
52
|
+
- Serial: _`ignored`_
|
|
53
|
+
- UDP/TCP: `port`
|
|
54
|
+
|
|
55
|
+
**Ignore Checksum**
|
|
56
|
+
|
|
57
|
+
- If you want to ignore checksum, use this option. Default `ON`
|
|
58
|
+
|
|
59
|
+
**SK Paths**
|
|
60
|
+
|
|
61
|
+
- Give each device unique SK paths
|
|
62
|
+
|
|
63
|
+
### Usage as a library
|
|
64
|
+
|
|
65
|
+
It's possible to use this plugin as a library, in other Node.js code. This feature simply wraps the plugin in an easy-to-consume manner, so functionality is identical. Example:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
const VEDirect = require('@signalk/vedirect-serial-usb/standalone')
|
|
69
|
+
const consumer = new VEDirect({
|
|
70
|
+
device: '/dev/ttyUSB0',
|
|
71
|
+
ignoreChecksum: true,
|
|
72
|
+
mainBatt: 'House',
|
|
73
|
+
auxBatt: 'Starter',
|
|
74
|
+
solar: 'Main',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
consumer.on('delta', (delta) => console.log('[onDelta]', delta))
|
|
78
|
+
consumer.stop() // stop the plugin, destruct the connections
|
|
79
|
+
consumer.start() // (re-)start the plugin
|
|
80
|
+
```
|
|
24
81
|
|
|
25
82
|
### License
|
|
26
83
|
|
|
27
84
|
```
|
|
28
|
-
Copyright 2018 Joachim Bakke <github@heiamoss.com
|
|
85
|
+
Copyright 2018 Joachim Bakke <github@heiamoss.com>, Fabian Tollenaar <fabian@decipher.industries> and
|
|
86
|
+
Karl-Erik Gustafsson <ke.gustafsson@gmail.com>
|
|
29
87
|
|
|
30
88
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
31
89
|
you may not use this file except in compliance with the License.
|
package/index.js
CHANGED
|
@@ -2,92 +2,152 @@ const serial = require('./lib/serial')
|
|
|
2
2
|
const udp = require('./lib/udp')
|
|
3
3
|
const tcp = require('./lib/tcp')
|
|
4
4
|
const Parser = require('./lib/Parser')
|
|
5
|
+
const VEDirect = require('./standalone')
|
|
5
6
|
|
|
6
7
|
module.exports = function (app) {
|
|
7
|
-
let parser =
|
|
8
|
+
let parser = []
|
|
9
|
+
let shaddow = null
|
|
8
10
|
const plugin = {}
|
|
9
|
-
|
|
10
11
|
plugin.id = 'vedirect-signalk'
|
|
11
12
|
plugin.name = 'VE.Direct to Signal K'
|
|
12
13
|
plugin.description = plugin.name
|
|
13
14
|
|
|
14
|
-
plugin.schema = {
|
|
15
|
-
type: 'object',
|
|
16
|
-
required: [
|
|
17
|
-
'device'
|
|
18
|
-
],
|
|
19
|
-
properties: {
|
|
20
|
-
device: {
|
|
21
|
-
type: 'string',
|
|
22
|
-
title: 'USB device',
|
|
23
|
-
description: '/dev/ttyUSB0 for example'
|
|
24
|
-
},
|
|
25
|
-
udpPort: {
|
|
26
|
-
type: 'number',
|
|
27
|
-
title: 'UDP port',
|
|
28
|
-
description: 'Leave USB device and TCP empty to use UDP',
|
|
29
|
-
default: 7878
|
|
30
|
-
},
|
|
31
|
-
host: {
|
|
32
|
-
type: 'string',
|
|
33
|
-
title: 'TCP host',
|
|
34
|
-
description: 'Leave USB device and UDP empty to use TCP'
|
|
35
|
-
},
|
|
36
|
-
tcpPort: {
|
|
37
|
-
type: 'number',
|
|
38
|
-
title: 'TCP port',
|
|
39
|
-
description: 'Leave USB device and UDP empty to use TCP',
|
|
40
|
-
},
|
|
41
|
-
ignoreChecksum: {
|
|
42
|
-
type: 'boolean',
|
|
43
|
-
title: 'Ignore Checksum',
|
|
44
|
-
default: true
|
|
45
|
-
},
|
|
46
|
-
mainBatt: {
|
|
47
|
-
type: 'string',
|
|
48
|
-
title: 'Main Battery name in SK path',
|
|
49
|
-
default: 'House'
|
|
50
|
-
},
|
|
51
|
-
auxBatt: {
|
|
52
|
-
type: 'string',
|
|
53
|
-
title: 'Aux Battery name in SK path',
|
|
54
|
-
default: 'Starter'
|
|
55
|
-
},
|
|
56
|
-
solar: {
|
|
57
|
-
type: 'string',
|
|
58
|
-
title: 'Solar name in SK path',
|
|
59
|
-
default: 'Main'
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
15
|
plugin.start = function (options) {
|
|
65
|
-
|
|
16
|
+
shaddow = options;
|
|
17
|
+
|
|
18
|
+
if (typeof options.vedirect !== 'undefined') {
|
|
19
|
+
Object.keys(options.vedirect).forEach(items => {
|
|
20
|
+
parser[items] = new Parser(options)
|
|
66
21
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
22
|
+
parser[items].on('delta', delta => {
|
|
23
|
+
app.handleMessage('pluginId', delta)
|
|
24
|
+
})
|
|
70
25
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
26
|
+
let type = options.vedirect[items].device;
|
|
27
|
+
let connection = options.vedirect[items].connection;
|
|
28
|
+
let port = options.vedirect[items].port;
|
|
29
|
+
if (type == 'Serial') {
|
|
30
|
+
serial.open(connection, parser, app.debug, items)
|
|
31
|
+
} else if (type == 'UDP') {
|
|
32
|
+
udp.listen(port, parser, app.debug, items)
|
|
33
|
+
} else if (type == 'TCP') {
|
|
34
|
+
tcp.connect(connection, port, parser, app.debug, items)
|
|
35
|
+
}
|
|
36
|
+
});
|
|
77
37
|
} else {
|
|
78
|
-
|
|
79
|
-
|
|
38
|
+
if (options.device) {
|
|
39
|
+
oldConfig = {vedirect: [{device: 'Serial', connection: options.device, ignoreChecksum: options.ignoreChecksum,
|
|
40
|
+
mainBatt: options.mainBatt, auxBatt: options.auxBatt, solar: options.solar}]}
|
|
41
|
+
} else if (options.udpPort) {
|
|
42
|
+
oldConfig = {vedirect: [{device: 'UDP', port: options.udpPort, ignoreChecksum: options.ignoreChecksum,
|
|
43
|
+
mainBatt: options.mainBatt, auxBatt: options.auxBatt, solar: options.solar}]}
|
|
44
|
+
} else if (options.host) {
|
|
45
|
+
oldConfig = {vedirect: [{device: 'TCP', connection: options.host, port: options.tcpPort, ignoreChecksum: options.ignoreChecksum,
|
|
46
|
+
mainBatt: options.mainBatt, auxBatt: options.auxBatt, solar: options.solar}]}
|
|
47
|
+
}
|
|
48
|
+
parser[0] = new Parser(oldConfig)
|
|
80
49
|
|
|
50
|
+
parser[0].on('delta', delta => {
|
|
51
|
+
app.handleMessage('pluginId', delta)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (options.device) {
|
|
55
|
+
serial.open(options.device, parser, app.debug, 0)
|
|
56
|
+
} else if (options.udpPort) {
|
|
57
|
+
udp.listen(options.udpPort, parser, app.debug, 0)
|
|
58
|
+
} else if (options.host) {
|
|
59
|
+
tcp.connect(options.host, options.tcpPort, parser, app.debug, 0)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
81
62
|
}
|
|
82
63
|
|
|
83
64
|
plugin.stop = function () {
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
65
|
+
if (shaddow) {
|
|
66
|
+
if (typeof shaddow.vedirect !== 'undefined') {
|
|
67
|
+
Object.keys(shaddow.vedirect).forEach(items => {
|
|
68
|
+
parser[items].removeAllListeners()
|
|
69
|
+
parser[items] = null
|
|
70
|
+
let type = shaddow.vedirect[items].device;
|
|
71
|
+
if (type == 'Serial') {
|
|
72
|
+
serial.close(app.debug, items)
|
|
73
|
+
} else if (type == 'UDP') {
|
|
74
|
+
udp.close(app.debug, items)
|
|
75
|
+
} else {
|
|
76
|
+
tcp.close(app.debug, items)
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
parser[0].removeAllListeners()
|
|
81
|
+
parser[0] = null
|
|
82
|
+
if (shaddow.device) {
|
|
83
|
+
serial.close(app.debug, 0)
|
|
84
|
+
} else if (shaddow.udpPort) {
|
|
85
|
+
udp.close(app.debug, 0)
|
|
86
|
+
} else if (shaddow.host) {
|
|
87
|
+
tcp.close(app.debug, 0)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
shaddow = null
|
|
87
91
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
plugin.schema = {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
vedirect: {
|
|
98
|
+
type: 'array',
|
|
99
|
+
title: 'Connections',
|
|
100
|
+
description: 'Connections to VE.Direct devices',
|
|
101
|
+
items: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
required: [],
|
|
104
|
+
properties: {
|
|
105
|
+
device: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
default: 'Serial',
|
|
108
|
+
title: 'Select device',
|
|
109
|
+
enum: [
|
|
110
|
+
'Serial',
|
|
111
|
+
'UDP',
|
|
112
|
+
'TCP',
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
connection: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
title: 'Connection details',
|
|
118
|
+
description: 'Serial: e.g. /dev/ttyUSB0, UDP: ignored or TCP: IP address',
|
|
119
|
+
default: '/dev/ttyUSB0'
|
|
120
|
+
},
|
|
121
|
+
port: {
|
|
122
|
+
type: 'number',
|
|
123
|
+
title: 'Port',
|
|
124
|
+
description: 'Serial: ignored, UDP/TCP: port',
|
|
125
|
+
default: 7878
|
|
126
|
+
},
|
|
127
|
+
ignoreChecksum: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
title: 'Ignore Checksum',
|
|
130
|
+
default: true
|
|
131
|
+
},
|
|
132
|
+
mainBatt: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
title: 'Main Battery name in SK path',
|
|
135
|
+
default: 'House'
|
|
136
|
+
},
|
|
137
|
+
auxBatt: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
title: 'Aux Battery name in SK path',
|
|
140
|
+
default: 'Starter'
|
|
141
|
+
},
|
|
142
|
+
solar: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
title: 'Solar name in SK path',
|
|
145
|
+
default: 'Main'
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
91
151
|
}
|
|
92
152
|
|
|
93
153
|
return plugin
|
package/lib/Parser.js
CHANGED
|
@@ -22,7 +22,7 @@ class VEDirectParser extends EventEmitter {
|
|
|
22
22
|
this.sum = 0
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
addChunk (buf) {
|
|
25
|
+
addChunk (buf, items) {
|
|
26
26
|
if (!Buffer.isBuffer(buf)) {
|
|
27
27
|
return this.warn('addChunk: incoming data is not a buffer: ' + typeof buf)
|
|
28
28
|
}
|
|
@@ -36,7 +36,7 @@ class VEDirectParser extends EventEmitter {
|
|
|
36
36
|
this.cache += chunk
|
|
37
37
|
if (chunk.toLowerCase().includes('checksum')) {
|
|
38
38
|
// Last line of block. Verify checksum of block in cache and parse line-by-line if checksum is correct.
|
|
39
|
-
this._verifyCacheAndParse()
|
|
39
|
+
this._verifyCacheAndParse(items)
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -75,9 +75,9 @@ class VEDirectParser extends EventEmitter {
|
|
|
75
75
|
return Object.assign({}, this.data)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
_verifyCacheAndParse () {
|
|
78
|
+
_verifyCacheAndParse (items) {
|
|
79
79
|
// Verify checksum if ignoreChecksum isn't true.
|
|
80
|
-
if (this.options.ignoreChecksum !== true && this.sum % 256 !== 0) {
|
|
80
|
+
if (this.options.vedirect[items].ignoreChecksum !== true && this.sum % 256 !== 0) {
|
|
81
81
|
this.warn(`block checksum doesn't equal 0: ${this.sum % 256}`)
|
|
82
82
|
this.cache = ''
|
|
83
83
|
this.sum = 0
|
|
@@ -96,7 +96,7 @@ class VEDirectParser extends EventEmitter {
|
|
|
96
96
|
|
|
97
97
|
this.cache = ''
|
|
98
98
|
this.sum = 0
|
|
99
|
-
this.generateDelta()
|
|
99
|
+
this.generateDelta(items)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
_parse () {
|
|
@@ -309,11 +309,11 @@ class VEDirectParser extends EventEmitter {
|
|
|
309
309
|
return this.options.productIds[pid]
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
generateDelta () {
|
|
312
|
+
generateDelta (items) {
|
|
313
313
|
const keys = Object.keys(this.data)
|
|
314
314
|
|
|
315
315
|
const values = keys.map(name => {
|
|
316
|
-
const path = this.getPath(name)
|
|
316
|
+
const path = this.getPath(name, items)
|
|
317
317
|
if (path === null) {
|
|
318
318
|
return null
|
|
319
319
|
}
|
|
@@ -342,7 +342,7 @@ class VEDirectParser extends EventEmitter {
|
|
|
342
342
|
})
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
getPath (name) {
|
|
345
|
+
getPath (name, items) {
|
|
346
346
|
return Object.keys(this.fields).reduce((found, key) => {
|
|
347
347
|
const field = this.fields[key]
|
|
348
348
|
if (field.name === name && field.hasOwnProperty('path')) {
|
|
@@ -351,15 +351,15 @@ class VEDirectParser extends EventEmitter {
|
|
|
351
351
|
|
|
352
352
|
if (field.hasOwnProperty('unitId')) {
|
|
353
353
|
if (field.unitId === 'mainBatt' && this.options.hasOwnProperty('mainBatt')) {
|
|
354
|
-
unitID = this.options.mainBatt
|
|
354
|
+
unitID = this.options.vedirect[items].mainBatt
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
if (field.unitId === 'auxBatt' && this.options.hasOwnProperty('auxBatt')) {
|
|
358
|
-
unitID = this.options.auxBatt
|
|
358
|
+
unitID = this.options.vedirect[items].auxBatt
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
if (field.unitId === 'solar' && this.options.hasOwnProperty('solar')) {
|
|
362
|
-
unitID = this.options.solar
|
|
362
|
+
unitID = this.options.vedirect[items].solar
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
if ((!unitID || unitID === '') && this.options.hasOwnProperty('defaultUnitId') && typeof this.options.defaultUnitId === 'string') {
|
package/lib/fields.js
CHANGED
|
@@ -83,6 +83,22 @@ module.exports = {
|
|
|
83
83
|
units: 'V',
|
|
84
84
|
type: 'metric'
|
|
85
85
|
},
|
|
86
|
+
V2: {
|
|
87
|
+
name: 'auxBattVoltage',
|
|
88
|
+
path: 'electrical.batteries.*.voltage',
|
|
89
|
+
unitId: 'auxBatt',
|
|
90
|
+
value: common.mV,
|
|
91
|
+
units: 'V',
|
|
92
|
+
type: 'metric'
|
|
93
|
+
},
|
|
94
|
+
V3: {
|
|
95
|
+
name: 'aux2BattVoltage',
|
|
96
|
+
path: 'electrical.batteries.*.voltage',
|
|
97
|
+
unitId: 'aux2Batt',
|
|
98
|
+
value: common.mV,
|
|
99
|
+
units: 'V',
|
|
100
|
+
type: 'metric'
|
|
101
|
+
},
|
|
86
102
|
VS: {
|
|
87
103
|
name: 'auxBatteryVoltage',
|
|
88
104
|
path: 'electrical.batteries.*.voltage',
|
|
@@ -126,6 +142,22 @@ module.exports = {
|
|
|
126
142
|
units: 'A',
|
|
127
143
|
type: 'metric'
|
|
128
144
|
},
|
|
145
|
+
I2: {
|
|
146
|
+
name: 'auxbatteryCurrent',
|
|
147
|
+
path: 'electrical.batteries.*.current',
|
|
148
|
+
unitId: 'auxBatt',
|
|
149
|
+
value: common.mA,
|
|
150
|
+
units: 'A',
|
|
151
|
+
type: 'metric'
|
|
152
|
+
},
|
|
153
|
+
I3: {
|
|
154
|
+
name: 'aux2batteryCurrent',
|
|
155
|
+
path: 'electrical.batteries.*.current',
|
|
156
|
+
unitId: 'aux2Batt',
|
|
157
|
+
value: common.mA,
|
|
158
|
+
units: 'A',
|
|
159
|
+
type: 'metric'
|
|
160
|
+
},
|
|
129
161
|
IL: {
|
|
130
162
|
name: 'loadCurrent',
|
|
131
163
|
path: 'electrical.solar.*.loadCurrent',
|
package/lib/productIds.json
CHANGED
|
@@ -78,5 +78,7 @@
|
|
|
78
78
|
"0xA274": "Phoenix Inverter 48V 1200VA 230V",
|
|
79
79
|
"0xA279": "Phoenix Inverter 12V 1200VA 120V",
|
|
80
80
|
"0xA27A": "Phoenix Inverter 24V 1200VA 120V",
|
|
81
|
-
"0xA27C": "Phoenix Inverter 48V 1200VA 120V"
|
|
81
|
+
"0xA27C": "Phoenix Inverter 48V 1200VA 120V",
|
|
82
|
+
"0xA345": "Phoenix Smart Charger 12V 30A 3 rev2",
|
|
83
|
+
"0xA381": "BMV-712 Smart"
|
|
82
84
|
}
|
package/lib/serial.js
CHANGED
|
@@ -1,38 +1,48 @@
|
|
|
1
|
-
const SerialPort = require('serialport')
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
let
|
|
5
|
-
|
|
6
|
-
exports
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
const { SerialPort } = require('serialport')
|
|
2
|
+
const { DelimiterParser } = require('@serialport/parser-delimiter')
|
|
3
|
+
let port = []
|
|
4
|
+
let delim = []
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
open: (device, parser, debug, items) => {
|
|
8
|
+
if (typeof port[items] !== 'undefined' ) {
|
|
9
|
+
try {
|
|
10
|
+
port[items].close()
|
|
11
|
+
port[items] = undefined
|
|
12
|
+
} catch (e) {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
debug(`Serial: connecting to ${device}`)
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
port[items] = new SerialPort({ path: device, baudRate: 19200 }) // @NOTE FT: should this be configurable?
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
port[items].on('open', function() {
|
|
20
|
+
debug(`Connected to ${device}`)
|
|
21
|
+
})
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
23
|
+
port[items].on('data', chunk => {
|
|
24
|
+
debug(`${chunk}`)
|
|
25
|
+
})
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
// I assume the serial port only closes if the physical line is broken/disconnected?
|
|
27
|
+
delim[items] = port[items].pipe(new DelimiterParser({ delimiter: '\r', includeDelimiter: true }))
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
29
|
+
delim[items].on('data', chunk => {
|
|
30
|
+
// Chunk is a node.js Buffer
|
|
31
|
+
parser[items].addChunk(chunk, items)
|
|
32
|
+
debug(`${chunk}`)
|
|
33
|
+
})
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
port[items].on('error', err => {
|
|
36
|
+
debug(`SerialPort error: ` + err.message)
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
close: (debug, items) => {
|
|
40
|
+
if (port[items]) {
|
|
41
|
+
try {
|
|
42
|
+
port[items].close()
|
|
43
|
+
port[items] = undefined
|
|
44
|
+
debug(`Serial port closed`);
|
|
45
|
+
} catch (e) {}
|
|
46
|
+
}
|
|
37
47
|
}
|
|
38
|
-
}
|
|
48
|
+
}
|
package/lib/tcp.js
CHANGED
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
const Net = require('net');
|
|
2
|
-
let client;
|
|
2
|
+
let client = [];
|
|
3
3
|
|
|
4
|
-
function makeConnection(host, port, debug) {
|
|
5
|
-
client = new Net.Socket();
|
|
6
|
-
client.connect({ port: port, host: host });
|
|
4
|
+
function makeConnection(host, port, debug, items) {
|
|
5
|
+
client[items] = new Net.Socket();
|
|
6
|
+
client[items].connect({ port: port, host: host });
|
|
7
7
|
debug(`Connection to ${host}:${port}`);
|
|
8
|
-
return client
|
|
8
|
+
return client[items]
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function onData(msg, parser, debug) {
|
|
12
|
-
parser.addChunk(msg);
|
|
11
|
+
function onData(msg, parser, debug, items) {
|
|
12
|
+
parser[items].addChunk(msg, items);
|
|
13
13
|
debug(`${msg}`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function onClose(host, port, parser, debug) {
|
|
17
|
-
if(client) {
|
|
16
|
+
function onClose(host, port, parser, debug, items) {
|
|
17
|
+
if(client[items]) {
|
|
18
18
|
setTimeout(() => {
|
|
19
19
|
debug('Trying to reconnect');
|
|
20
|
-
client.destroy();
|
|
21
|
-
module.exports.connect(host, port, parser, debug);
|
|
20
|
+
client[items].destroy();
|
|
21
|
+
module.exports.connect(host, port, parser, debug, items);
|
|
22
22
|
}, 10000)
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
module.exports = {
|
|
27
|
-
connect: (host, port, parser, debug) => {
|
|
28
|
-
client = makeConnection(host, port, debug);
|
|
29
|
-
client.on('data', (msg) => {
|
|
30
|
-
onData(msg, parser, debug);
|
|
27
|
+
connect: (host, port, parser, debug, items) => {
|
|
28
|
+
client[items] = makeConnection(host, port, debug, items);
|
|
29
|
+
client[items].on('data', (msg) => {
|
|
30
|
+
onData(msg, parser, debug, items);
|
|
31
31
|
});
|
|
32
|
-
client.on('close', () => {
|
|
33
|
-
debug('
|
|
34
|
-
onClose(host, port, parser, debug);
|
|
32
|
+
client[items].on('close', () => {
|
|
33
|
+
debug('TCP connection closed');
|
|
34
|
+
onClose(host, port, parser, debug, items);
|
|
35
35
|
});
|
|
36
|
-
client.on('error', () => {
|
|
37
|
-
debug('
|
|
36
|
+
client[items].on('error', () => {
|
|
37
|
+
debug('TCP connection error');
|
|
38
38
|
});
|
|
39
39
|
},
|
|
40
|
-
close: (debug) => {
|
|
41
|
-
if (client) {
|
|
42
|
-
client.destroy();
|
|
40
|
+
close: (debug, items) => {
|
|
41
|
+
if (client[items]) {
|
|
42
|
+
client[items].destroy();
|
|
43
|
+
client[items] = null;
|
|
44
|
+
debug(`TCP port closed`);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
}
|
package/lib/udp.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
const dgram = require('dgram');
|
|
2
|
-
let socket;
|
|
2
|
+
let socket = [];
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
|
-
listen: (port, parser, debug) => {
|
|
6
|
-
socket = dgram.createSocket('udp4');
|
|
5
|
+
listen: (port, parser, debug, items) => {
|
|
6
|
+
socket[items] = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
7
7
|
|
|
8
|
-
socket.on('listening', () => {
|
|
8
|
+
socket[items].on('listening', () => {
|
|
9
9
|
debug(`listening on UDP ${port}`);
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
socket.on('message', (msg, rinfo) => {
|
|
12
|
+
socket[items].on('message', (msg, rinfo) => {
|
|
13
13
|
debug(`${rinfo.address}:${rinfo.port}:${msg}`);
|
|
14
|
-
parser.addChunk(msg);
|
|
14
|
+
parser[items].addChunk(msg, items);
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
socket.bind(port);
|
|
17
|
+
socket[items].bind(port);
|
|
18
18
|
},
|
|
19
|
-
close: (debug) => {
|
|
20
|
-
if (socket) {
|
|
21
|
-
socket.close();
|
|
22
|
-
socket = null;
|
|
19
|
+
close: (debug, items) => {
|
|
20
|
+
if (socket[items]) {
|
|
21
|
+
socket[items].close();
|
|
22
|
+
socket[items] = null;
|
|
23
23
|
debug(`UDP port closed`);
|
|
24
24
|
}
|
|
25
25
|
}
|
package/migration.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
jsonPath = __dirname + '/../../../plugin-config-data';
|
|
5
|
+
fs.readFile(path.resolve(jsonPath, 'vedirect-signalk.json'), 'UTF-8', (err, data) => {
|
|
6
|
+
if (err) {
|
|
7
|
+
console.error(err)
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let obj = JSON.parse(data);
|
|
12
|
+
|
|
13
|
+
if (typeof obj.configuration.vedirect == 'undefined' ) {
|
|
14
|
+
if (obj.configuration.device) {
|
|
15
|
+
obj.configuration.vedirect = [{
|
|
16
|
+
'device':'Serial',
|
|
17
|
+
'connection': obj.configuration.device,
|
|
18
|
+
'port': '',
|
|
19
|
+
'ignoreChecksum': obj.configuration.ignoreChecksum,
|
|
20
|
+
'mainBatt': obj.configuration.mainBatt,
|
|
21
|
+
'auxBatt': obj.configuration.auxBatt,
|
|
22
|
+
'solar': obj.configuration.solar
|
|
23
|
+
}]
|
|
24
|
+
} else if (obj.configuration.udpPort) {
|
|
25
|
+
obj.configuration.vedirect = [{
|
|
26
|
+
'device':'UDP',
|
|
27
|
+
'connection': 'localhost',
|
|
28
|
+
'port': obj.configuration.udpPort,
|
|
29
|
+
'ignoreChecksum': obj.configuration.ignoreChecksum,
|
|
30
|
+
'mainBatt': obj.configuration.mainBatt,
|
|
31
|
+
'auxBatt': obj.configuration.auxBatt,
|
|
32
|
+
'solar': obj.configuration.solar
|
|
33
|
+
}]
|
|
34
|
+
} else if (obj.configuration.host) {
|
|
35
|
+
obj.configuration.vedirect = [{
|
|
36
|
+
'device':'TCP',
|
|
37
|
+
'connection': obj.configuration.host,
|
|
38
|
+
'port': obj.configuration.tcpPort,
|
|
39
|
+
'ignoreChecksum': obj.configuration.ignoreChecksum,
|
|
40
|
+
'mainBatt': obj.configuration.mainBatt,
|
|
41
|
+
'auxBatt': obj.configuration.auxBatt,
|
|
42
|
+
'solar': obj.configuration.solar
|
|
43
|
+
}]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let dataOut = JSON.stringify(obj, null, 2)
|
|
47
|
+
fs.writeFile(path.resolve(jsonPath, 'vedirect-signalk.json'), dataOut, 'utf8', function(err) {
|
|
48
|
+
if (err) {
|
|
49
|
+
console.error(err)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalk/vedirect-serial-usb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Signal K Node.js server plugin to read VE.Direct data via serial USB",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "mocha",
|
|
8
|
-
"push": "npm publish --access public --scope @signalk ."
|
|
8
|
+
"push": "npm publish --access public --scope @signalk .",
|
|
9
|
+
"postinstall": "node ./migration.js"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
11
12
|
"signalk-node-server-plugin"
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"debug": "^4.0.1",
|
|
26
27
|
"package-lock.json": "^1.0.0",
|
|
27
|
-
"serialport": "^
|
|
28
|
+
"serialport": "^11.0.0"
|
|
28
29
|
},
|
|
29
30
|
"standard": {
|
|
30
31
|
"globals": [
|
|
@@ -37,6 +38,6 @@
|
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"chai": "^4.1.2",
|
|
39
40
|
"chai-things": "^0.2.0",
|
|
40
|
-
"mocha": "^
|
|
41
|
+
"mocha": "^10.2.0"
|
|
41
42
|
}
|
|
42
43
|
}
|
package/standalone.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* standalone.js
|
|
3
|
+
*
|
|
4
|
+
* @description exports a class that shims the Signal K server, in order to make this plugin available as a library in other software
|
|
5
|
+
* @module @signalk/vedirect-serial-usb
|
|
6
|
+
* @author Fabian Tollenaar <fabian@essense.ai> (https://essense.ai)
|
|
7
|
+
* @license Apache_2.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const EventEmitter = require('events')
|
|
11
|
+
const SKPlugin = require('./index')
|
|
12
|
+
class VEDirect extends EventEmitter {
|
|
13
|
+
constructor (config = {}, _debug = false) {
|
|
14
|
+
super()
|
|
15
|
+
|
|
16
|
+
this._debug = _debug
|
|
17
|
+
this.app = {
|
|
18
|
+
handleMessage: (kind, data) => {
|
|
19
|
+
if (kind === 'pluginId') {
|
|
20
|
+
this.emit('delta', data)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
this.emit(kind, data)
|
|
24
|
+
},
|
|
25
|
+
debug: (args) => this.debug(args),
|
|
26
|
+
options: {
|
|
27
|
+
device: 'Serial',
|
|
28
|
+
connection: '/dev/ttyUSB0',
|
|
29
|
+
port: 7878,
|
|
30
|
+
ignoreChecksum: true,
|
|
31
|
+
mainBatt: 'House',
|
|
32
|
+
auxBatt: 'Starter',
|
|
33
|
+
solar: 'Main',
|
|
34
|
+
...(config || {}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.plugin = SKPlugin(this.app)
|
|
39
|
+
this.start()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
start () {
|
|
43
|
+
if (!this.plugin || !this.plugin.hasOwnProperty('start')) {
|
|
44
|
+
this.debug(`Plugin not initialised, can't start`)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.plugin.start(this.app.options)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
stop () {
|
|
52
|
+
if (!this.plugin || !this.plugin.hasOwnProperty('stop')) {
|
|
53
|
+
this.debug(`Plugin not initialised, can't stop`)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.plugin.stop()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
debug (...args) {
|
|
61
|
+
if (this._debug) {
|
|
62
|
+
console.log.apply(console, args)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = VEDirect
|
package/test/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const Parser = require('../lib/Parser')
|
|
|
5
5
|
const expect = chai.expect
|
|
6
6
|
|
|
7
7
|
const parser = new Parser({
|
|
8
|
-
ignoreChecksum: true
|
|
8
|
+
vedirect: [ { device: 'UDP', connection: 'localhost', port: 7878, ignoreChecksum: true, mainBatt: 'House', auxBatt: 'Starter', solar: 'Main' } ]
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
let lines = []
|
|
@@ -30,7 +30,7 @@ describe('VE.Direct parser', () => {
|
|
|
30
30
|
it('Parses each line that has a Signal K representation withouth error', done => {
|
|
31
31
|
try {
|
|
32
32
|
let parsed = []
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
const listener = delta => {
|
|
35
35
|
parsed.push(delta)
|
|
36
36
|
}
|
|
@@ -38,7 +38,7 @@ describe('VE.Direct parser', () => {
|
|
|
38
38
|
parser.on('delta', listener)
|
|
39
39
|
|
|
40
40
|
lines.forEach(line => {
|
|
41
|
-
parser.addChunk(Buffer.from(line))
|
|
41
|
+
parser.addChunk(Buffer.from(line), 0)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
parser.removeListener('delta', listener)
|