forked from yaacov/node-modbus-serial
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathasciiport.js
280 lines (236 loc) · 8.73 KB
/
asciiport.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
"use strict";
/* eslint-disable no-ternary */
const events = require("events");
const EventEmitter = events.EventEmitter || events;
const SerialPort = require("serialport").SerialPort;
const modbusSerialDebug = require("debug")("modbus-serial");
const crc16 = require("../utils/crc16");
const calculateLrc = require("./../utils/lrc");
/* TODO: const should be set once, maybe */
const MIN_DATA_LENGTH = 6;
/**
* Ascii encode a 'request' buffer and return it. This includes removing
* the CRC bytes and replacing them with an LRC.
*
* @param {Buffer} buf the data buffer to encode.
* @return {Buffer} the ascii encoded buffer
* @private
*/
function _asciiEncodeRequestBuffer(buf) {
// replace the 2 byte crc16 with a single byte lrc
buf.writeUInt8(calculateLrc(buf.slice(0, -2)), buf.length - 2);
// create a new buffer of the correct size
const bufAscii = Buffer.alloc(buf.length * 2 + 1); // 1 byte start delimit + x2 data as ascii encoded + 2 lrc + 2 end delimit
// create the ascii payload
// start with the single start delimiter
bufAscii.write(":", 0);
// encode the data, with the new single byte lrc
bufAscii.write(buf.toString("hex", 0, buf.length - 1).toUpperCase(), 1);
// end with the two end delimiters
bufAscii.write("\r", bufAscii.length - 2);
bufAscii.write("\n", bufAscii.length - 1);
return bufAscii;
}
/**
* Ascii decode a 'response' buffer and return it.
*
* @param {Buffer} bufAscii the ascii data buffer to decode.
* @return {Buffer} the decoded buffer, or null if decode error
* @private
*/
function _asciiDecodeResponseBuffer(bufAscii) {
// create a new buffer of the correct size (based on ascii encoded buffer length)
const bufDecoded = Buffer.alloc((bufAscii.length - 1) / 2);
// decode into new buffer (removing delimiters at start and end)
for (let i = 0; i < (bufAscii.length - 3) / 2; i++) {
bufDecoded.write(String.fromCharCode(bufAscii.readUInt8(i * 2 + 1), bufAscii.readUInt8(i * 2 + 2)), i, 1, "hex");
}
// check the lrc is true
const lrcIn = bufDecoded.readUInt8(bufDecoded.length - 2);
if(calculateLrc(bufDecoded.slice(0, -2)) !== lrcIn) {
// return null if lrc error
const calcLrc = calculateLrc(bufDecoded.slice(0, -2));
modbusSerialDebug({ action: "LRC error", LRC: lrcIn.toString(16), calcLRC: calcLrc.toString(16) });
return null;
}
// replace the 1 byte lrc with a two byte crc16
bufDecoded.writeUInt16LE(crc16(bufDecoded.slice(0, -2)), bufDecoded.length - 2);
return bufDecoded;
}
/**
* check if a buffer chunk can be a modbus answer
* or modbus exception
*
* @param {AsciiPort} modbus
* @param {Buffer} buf the buffer to check.
* @return {boolean} if the buffer can be an answer
* @private
*/
function _checkData(modbus, buf) {
// check buffer size
if (buf.length !== modbus._length && buf.length !== 5) {
modbusSerialDebug({ action: "length error", recive: buf.length, expected: modbus._length });
return false;
}
// check buffer unit-id and command
return (buf[0] === modbus._id &&
(0x7f & buf[1]) === modbus._cmd);
}
class AsciiPort extends EventEmitter {
/**
* Simulate a modbus-ascii port using serial connection.
*
* @param path
* @param options
* @constructor
*/
constructor(path, options) {
super();
const modbus = this;
// options
options = options || {};
// select char for start of slave frame (usually :)
this._startOfSlaveFrameChar =
(options.startOfSlaveFrameChar === undefined)
? 0x3A
: options.startOfSlaveFrameChar;
// disable auto open, as we handle the open
options.autoOpen = false;
// internal buffer
this._buffer = Buffer.from("");
this._id = 0;
this._cmd = 0;
this._length = 0;
// create the SerialPort
this._client = new SerialPort(Object.assign({}, { path }, options));
// register the port data event
this._client.on("data", function(data) {
// add new data to buffer
modbus._buffer = Buffer.concat([modbus._buffer, data]);
modbusSerialDebug({ action: "receive serial ascii port", data: data, buffer: modbus._buffer });
modbusSerialDebug(JSON.stringify({ action: "receive serial ascii port strings", data: data, buffer: modbus._buffer }));
// check buffer for start delimiter
const sdIndex = modbus._buffer.indexOf(modbus._startOfSlaveFrameChar);
if(sdIndex === -1) {
// if not there, reset the buffer and return
modbus._buffer = Buffer.from("");
return;
}
// if there is data before the start delimiter, remove it
if(sdIndex > 0) {
modbus._buffer = modbus._buffer.slice(sdIndex);
}
// do we have the complete message (i.e. are the end delimiters there)
if(modbus._buffer.includes("\r\n", 1, "ascii") === true) {
// check there is no excess data after end delimiters
const edIndex = modbus._buffer.indexOf(0x0A); // ascii for '\n'
if(edIndex !== modbus._buffer.length - 1) {
// if there is, remove it
modbus._buffer = modbus._buffer.slice(0, edIndex + 1);
}
// we have what looks like a complete ascii encoded response message, so decode
const _data = _asciiDecodeResponseBuffer(modbus._buffer);
modbusSerialDebug({ action: "got EOM", data: _data, buffer: modbus._buffer });
if(_data !== null) {
// check if this is the data we are waiting for
if (_checkData(modbus, _data)) {
modbusSerialDebug({ action: "emit data serial ascii port", data: data, buffer: modbus._buffer });
modbusSerialDebug(JSON.stringify({ action: "emit data serial ascii port strings", data: data, buffer: modbus._buffer }));
// emit a data signal
modbus.emit("data", _data);
}
}
// reset the buffer now its been used
modbus._buffer = Buffer.from("");
} else {
// otherwise just wait for more data to arrive
}
});
}
/**
* Check if port is open.
*
* @returns {boolean}
*/
get isOpen() {
return this._client.isOpen;
}
/**
* Simulate successful port open.
*
* @param callback
*/
open(callback) {
this._client.open(callback);
}
/**
* Simulate successful close port.
*
* @param callback
*/
close(callback) {
this._client.close(callback);
this.removeAllListeners();
}
/**
* Send data to a modbus slave.
*
* @param data
*/
write(data) {
if(data.length < MIN_DATA_LENGTH) {
modbusSerialDebug("expected length of data is to small - minimum is " + MIN_DATA_LENGTH);
return;
}
let length = null;
// remember current unit and command
this._id = data[0];
this._cmd = data[1];
// calculate expected answer length (this is checked after ascii decoding)
switch (this._cmd) {
case 1:
case 2:
length = data.readUInt16BE(4);
this._length = 3 + parseInt((length - 1) / 8 + 1) + 2;
break;
case 3:
case 4:
length = data.readUInt16BE(4);
this._length = 3 + 2 * length + 2;
break;
case 5:
case 6:
case 15:
case 16:
this._length = 6 + 2;
break;
default:
// raise and error ?
modbusSerialDebug({ action: "unknown command", id: this._id.toString(16), command: this._cmd.toString(16) });
this._length = 0;
break;
}
// ascii encode buffer
const _encodedData = _asciiEncodeRequestBuffer(data);
// send buffer to slave
this._client.write(_encodedData);
modbusSerialDebug({
action: "send serial ascii port",
data: _encodedData,
unitid: this._id,
functionCode: this._cmd
});
modbusSerialDebug(JSON.stringify({
action: "send serial ascii port",
data: _encodedData,
unitid: this._id,
functionCode: this._cmd
}));
}
}
/**
* ASCII port for Modbus.
*
* @type {AsciiPort}
*/
module.exports = AsciiPort;