-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsofar-KTL-x-request.json
396 lines (396 loc) · 34.1 KB
/
sofar-KTL-x-request.json
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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
[
{
"id": "072561a1188f3103",
"type": "subflow",
"name": "sofar KTL-X request",
"info": "TCP modbus request for LSW3 wifi logger \r\nused with Sofar solar KTL-X inverters\r\n\r\n### Outputs\r\n1. JSON data\r\n: payload (object | buffer | string) : parsed data object, raw buffer if can't decode data or error message.\r\n: topic (string) : `decoded data` | `unknown data` | `unexpected data` | `error`.\r\n: debug (object) : debug data object.\r\n: error (object) : error object if there was an error in tcp request node\r\n\r\n2. raw data\r\n: payload (buffer) : raw data frames.\r\n: topic (string) : `raw request` | `raw response`\r\n\r\n### Details\r\n\r\nDefault logger port: \r\n`8899`\r\n\r\nLogger SN: \r\n`17xxxxxxxx`\r\n\r\nDefault first register address in hex: \r\n`0x0000`\r\n\r\nDefault last register address in hex: \r\n`0x0027`\r\n\r\nDefault log level: \r\n`warning`\r\n\r\n### References\r\n\r\n - [GitHub](https://github.com/serek4/node-red-sofar-ktl-x-request) - the node github repository\r\n",
"category": "",
"in": [
{
"x": 120,
"y": 240,
"wires": [
{
"id": "fc59a9b86be541d3"
}
]
}
],
"out": [
{
"x": 1130,
"y": 160,
"wires": [
{
"id": "2af8772ff413b097",
"port": 0
},
{
"id": "2264ac720797774c",
"port": 0
}
]
},
{
"x": 1120,
"y": 280,
"wires": [
{
"id": "fc59a9b86be541d3",
"port": 0
},
{
"id": "41143463e6946012",
"port": 0
}
]
}
],
"env": [
{
"name": "loggerIP",
"type": "str",
"value": "",
"ui": {
"icon": "font-awesome/fa-cloud-download",
"label": {
"en-US": "logger IP"
},
"type": "input",
"opts": {
"types": [
"str",
"env"
]
}
}
},
{
"name": "loggerPort",
"type": "num",
"value": "8899",
"ui": {
"icon": "font-awesome/fa-plug",
"label": {
"en-US": "logger port"
},
"type": "spinner",
"opts": {
"min": 1
}
}
},
{
"name": "loggerSN",
"type": "num",
"value": "1700000000",
"ui": {
"label": {
"en-US": "logger SN"
},
"type": "spinner",
"opts": {
"min": 1700000000,
"max": 1799999999
}
}
},
{
"name": "firstReg",
"type": "str",
"value": "0x0000",
"ui": {
"label": {
"en-US": "first register"
},
"type": "input",
"opts": {
"types": [
"str",
"env"
]
}
}
},
{
"name": "lastReg",
"type": "str",
"value": "0x0027",
"ui": {
"label": {
"en-US": "last register"
},
"type": "input",
"opts": {
"types": [
"str",
"env"
]
}
}
},
{
"name": "logLvl",
"type": "str",
"value": "1",
"ui": {
"label": {
"en-US": "log level"
},
"type": "select",
"opts": {
"opts": [
{
"l": {
"en-US": "disabled"
},
"v": "0"
},
{
"l": {
"en-US": "warning"
},
"v": "1"
},
{
"l": {
"en-US": "info"
},
"v": "2"
},
{
"l": {
"en-US": "debug"
},
"v": "3"
}
]
}
}
}
],
"meta": {
"version": "0.6.0",
"author": "serek4",
"desc": "sofar KTL-X inverter with LSW3 wifi stick",
"license": "MIT"
},
"color": "#20c800",
"outputLabels": [
"JSON data",
"raw data"
],
"icon": "node-red/bridge.svg",
"status": {
"x": 720,
"y": 40,
"wires": [
{
"id": "25689a1f1b08d720",
"port": 0
}
]
}
},
{
"id": "fc59a9b86be541d3",
"type": "function",
"z": "072561a1188f3103",
"name": "request data from inverter",
"func": "/** inverter data modbus request data command in hex\n* a5 |1700 |1045|0000|xxxxxxxx|020000000000000000000000000000|0103 0000 0028 xxxx|xx |15\n* a5 |1700 |1045|0000|xxxxxxxx|020000000000000000000000000000|0103 0105 0010 xxxx|xx |15\n* begin |data length| 2b | 2b | SN | 15b | modbus command |checksum |end\n*/\n\nconst requestNr = flow.get('requestNr') > 255 ? 0 : flow.get('requestNr') || 0;\nconst responseNr = flow.get('responseNr') || 0;\nconst serialNumber = env.get('loggerSN'); // in hex it is 32LE\nconst firstReg = parseInt(env.get('firstReg'), 16);\nconst lastReg = parseInt(env.get('lastReg'), 16);\n\nconst frame = Buffer.alloc(36);\nframe.writeUInt8(0xa5, 0); // frame begin\nframe.writeUInt16BE(0x1700, 1); // data length\nframe.writeUInt16BE(0x1045, 3); // control code\nframe.writeUInt8(requestNr, 5); // request number\nframe.writeUInt8(responseNr, 6); // server response number\nframe.writeUInt32LE(serialNumber, 7); // logger SN\nframe.writeUInt8(0x02, 11); // frame type\nframe.writeUInt16LE(0x0000, 12); // sensor type\nframe.writeUInt32LE(0x0000, 14); // total working time\nframe.writeUInt32LE(0x0000, 18); // power on time\nframe.writeUInt32LE(0x0000, 22); // offset time\n// modbus command\nframe.writeUInt16BE(0x0103, 26) // get data code\n// 1st register range\nframe.writeUInt16BE(firstReg, 28) // first register\nframe.writeUInt16BE(lastReg - firstReg + 1, 30) // number of registers to read\n// 2nd register range\n// frame.writeUInt16BE(0x0105, 28) // reg start\n// frame.writeUInt16BE(0x0010, 30) // number of registers to read\n// crc\nlet commandCrcModbus = crc16modbus(frame.subarray(26, 32)); //! uInt16LE\nframe.writeUInt16LE(commandCrcModbus, 32); //command crc\n\nframe.writeUInt8(frameChecksum(frame), 34); //frame checksum\nframe.writeUInt8(0x15, 35); // frame end\n// node.warn(frame.toString('hex'));\n\nmsg = {\n payload: frame,\n topic: 'raw request'\n};\nflow.set('waitingForResponse', true);\nreturn msg;\n\nfunction frameChecksum(frame) {\n frame = frame.subarray(1, -2);\n let sum = 0x0;\n for (const byte of frame) {\n sum += byte;\n }\n sum = sum % 256;\n return sum;\n}\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "crc16modbus",
"module": "crc/crc16modbus"
}
],
"x": 340,
"y": 240,
"wires": [
[
"750e4979aad7f48f"
]
]
},
{
"id": "750e4979aad7f48f",
"type": "tcp request",
"z": "072561a1188f3103",
"name": "Modbus request",
"server": "${loggerIP}",
"port": "${loggerPort}",
"out": "time",
"ret": "buffer",
"splitc": "1000",
"newline": "",
"trim": false,
"tls": "",
"x": 640,
"y": 240,
"wires": [
[
"41143463e6946012",
"b36ae6ac1f32590f"
]
]
},
{
"id": "25689a1f1b08d720",
"type": "status",
"z": "072561a1188f3103",
"name": "",
"scope": [
"750e4979aad7f48f"
],
"x": 580,
"y": 40,
"wires": [
[]
]
},
{
"id": "41143463e6946012",
"type": "change",
"z": "072561a1188f3103",
"name": "",
"rules": [
{
"t": "set",
"p": "topic",
"pt": "msg",
"to": "raw response",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 910,
"y": 240,
"wires": [
[]
]
},
{
"id": "2af8772ff413b097",
"type": "function",
"z": "072561a1188f3103",
"name": "decode response",
"func": "let decodedMsg = handleMsg(msg.payload);\ndecodedMsg.debug = msg.debug;\nlet extraMsg = null;\nif (typeof msg.payload.extraData !== \"undefined\") {\n if (isFrameValid(msg.payload.extraData)) {\n log(\"info\", \"decoding extra data\");\n if (decodedMsg.topic !== \"decoded data\") { flow.set(\"waitingForResponse\", true); }\n extraMsg = handleMsg(msg.payload.extraData);\n }\n}\nreturn [[decodedMsg, extraMsg]];\n\nfunction handleMsg(input) {\n const message = { payload: null };\n if (isFrameValid(input)) {\n message.payload = decodeData(input.data); // decode response data\n } else { log(\"warn\", \"invalid data, skipping decoding\"); }\n if (message.payload !== null) {\n flow.set(\"waitingForResponse\", false);\n message.topic = \"decoded data\";\n } else { // decode failed send raw buffer\n message.payload = input.data;\n if (!flow.get(\"waitingForResponse\")) {\n message.topic = \"unexpected data\";\n } else {\n flow.set(\"waitingForResponse\", false);\n message.topic = \"unknown data\";\n }\n }\n return message;\n}\n\nfunction isFrameValid(frame) {\n return frame.isValidV5frame && frame.frameChecksum === \"OK\" && frame.data.modbusFrameCrc === \"OK\";\n}\n\nfunction decodeData(inputBuffer) { // real time data(Function Code 0x03) response\n const data = {};\n try {\n const solarman = inputBuffer.solarmanHeader;\n\n const modbusBuffer = inputBuffer.modbusFrame;\n const registersDataLength = modbusBuffer[2]; // first 3 bytes are slave address, function code, data length\n const modbusRegisters = modbusBuffer.subarray(3, 3 + registersDataLength); // registers data begins at 3rd byte\n\n // inverter status\n data.inverterStatus = decodeInverterStatus(modbusRegisters.readUInt16BE(0x00));\n //errors\n data.errors = errorDecoder(modbusRegisters.subarray(0x01 * 2, 0x06 * 2));\n\n //PV Input Message\n data.PV = {};\n data.PV.string1 = {};\n data.PV.string1.voltage = modbusRegisters.readUInt16BE(0x06 * 2) / 10; // V\n data.PV.string1.current = modbusRegisters.readInt16BE(0x07 * 2) / 100; // A\n data.PV.string1.power = modbusRegisters.readUInt16BE(0x0a * 2) * 10; // W (/ 100 -> kW)\n data.PV.string2 = {};\n data.PV.string2.voltage = modbusRegisters.readUInt16BE(0x08 * 2) / 10; // V\n data.PV.string2.current = modbusRegisters.readInt16BE(0x09 * 2) / 100; // A\n data.PV.string2.power = modbusRegisters.readUInt16BE(0x0b * 2) * 10; // W (/ 100 -> kW)\n log(\"debug\", \"PV Input data successfully decoded\");\n\n // Output Grid Message\n data.grid = {};\n data.grid.activePower = modbusRegisters.readUInt16BE(0x0c * 2) * 10; // W (/ 100 -> kW)\n data.grid.reactivePower = modbusRegisters.readInt16BE(0x0d * 2) / 100; // kVar\n data.grid.frequency = modbusRegisters.readUInt16BE(0x0e * 2) / 100; // Hz\n data.grid.L1_V = modbusRegisters.readUInt16BE(0x0f * 2) / 10; // V\n data.grid.L1_A = modbusRegisters.readUInt16BE(0x10 * 2) / 100; // A\n data.grid.L2_V = modbusRegisters.readUInt16BE(0x11 * 2) / 10; // V\n data.grid.L2_A = modbusRegisters.readUInt16BE(0x12 * 2) / 100; // A\n data.grid.L3_V = modbusRegisters.readUInt16BE(0x13 * 2) / 10; // V\n data.grid.L3_A = modbusRegisters.readUInt16BE(0x14 * 2) / 100; // A\n log(\"debug\", \"Output Grid data successfully decoded\");\n\n // Inverter Generation message\n data.production = {};\n data.production.TotalEnergy = modbusRegisters.readUInt32BE(0x15 * 2); // kW\n data.production.TotalTime = modbusRegisters.readUInt32BE(0x17 * 2); // hours\n data.production.TodayEnergy = modbusRegisters.readUInt16BE(0x19 * 2) / 100; // kW (* 10 -> W)\n data.production.TodayTime = modbusRegisters.readUInt16BE(0x1a * 2); // minutes\n log(\"debug\", \"Inverter Generation data successfully decoded\");\n\n // Inverter inner message\n data.info = {};\n data.info.loggerTemp = modbusRegisters.readInt16BE(0x1b * 2); // °C\n data.info.inverterTemp = modbusRegisters.readInt16BE(0x1c * 2); // °C\n data.info.Vbus = modbusRegisters.readUInt16BE(0x1d * 2) / 10; // V\n data.info.PV1VCPU = modbusRegisters.readUInt16BE(0x1e * 2) / 10; // V\n data.info.PV1ACPU = modbusRegisters.readUInt16BE(0x1f * 2) / 100; // A\n data.info.countdownTime = modbusRegisters.readUInt16BE(0x20 * 2); // seconds\n data.info.inverterAlarmInfo = modbusRegisters.readUInt16BE(0x21 * 2);\n data.info.inputMode = inputModeDecoder(modbusRegisters.readUInt16BE(0x22 * 2));\n data.info.inverterInnerInfo = modbusRegisters.readUInt16BE(0x23 * 2);\n data.info.PV1insulationResistance = modbusRegisters.readUInt16BE(0x24 * 2); // kOhm\n data.info.PV2insulationResistance = modbusRegisters.readUInt16BE(0x25 * 2); // kOhm\n data.info.cathode_groundInsulationImpedance = modbusRegisters.readUInt16BE(0x26 * 2); // KOhm\n data.info.country = countryDecoder(modbusRegisters.readUInt16BE(0x27 * 2));\n log(\"debug\", \"Inverter inner data successfully decoded\");\n\n // data from solarman frame header\n data.loggerInfo = {};\n data.loggerInfo.frameType = solarman.readUint8(0x00);\n data.loggerInfo.dataType = solarman.readUint8(0x01);\n data.loggerInfo.totalWorkingTime = solarman.readUInt32LE(0x02); // seconds\n data.loggerInfo.powerOnTime = solarman.readUInt32LE(0x06); // seconds\n data.loggerInfo.offsetTime = solarman.readUInt32LE(0x0A); // seconds\n data.loggerInfo.totalOperationTime = data.loggerInfo.totalWorkingTime - data.loggerInfo.powerOnTime; // seconds\n log(\"debug\", \"solarman frame header successfully decoded\");\n\n data.timestamp = data.loggerInfo.totalWorkingTime + data.loggerInfo.offsetTime; // timestamp in seconds\n log(\"info\", \"data successfully decoded\");\n } catch (error) {\n // node.warn(error);\n log(\"warn\", \"decode data failed\");\n return null;\n };\n\n return data;\n}\n\nfunction decodeInverterStatus(status) {\n switch (status) {\n case 0x00:\n return `standby`;\n case 0x02:\n return `normal`;\n case 0x03:\n return `fault`;\n case 0x04:\n return `permanent`;\n\n default:\n return `0x${`0${(status.toString(16))}`.slice(-2)}`;\n }\n}\n\nfunction inputModeDecoder(mode) {\n switch (mode) {\n case 0x00:\n return 'parallel';\n case 0x01:\n return 'independent';\n\n default:\n return `0x${`0${(mode.toString(16))}`.slice(-2)}`;\n }\n}\n\nfunction countryDecoder(countryCode) {\n switch (countryCode) {\n case 0:\n return \"Germany VDE AR-N4105\";\n case 1:\n return \"Italy CE10-21\";\n case 2:\n return \"Australia\";\n case 3:\n return \"Spain RD1699\";\n case 4:\n return \"Turkey\";\n case 5:\n return \"Denmark\";\n case 6:\n return \"Greece Continent\";\n case 7:\n return \"Netherland\";\n case 8:\n return \"Belgium\";\n case 9:\n return \"UK-G59\";\n case 10:\n return \"China\";\n case 11:\n return \"France\";\n case 12:\n return \"Poland\";\n case 13:\n return \"Germany BDEW\";\n case 14:\n return \"Germany VDE 0126\";\n case 15:\n return \"Italy CE10-16\";\n case 16:\n return \"UK-G83\";\n case 17:\n return \"Greece Island\";\n case 18:\n return \"EU EN50438\";\n case 19:\n return \"IEC EN61727\";\n case 20:\n return \"Korea\";\n case 21:\n return \"Sweden\";\n case 22:\n return \"Europe General\";\n case 23:\n return \"CE10-21 External\";\n case 24:\n return \"Cyprus\";\n case 25:\n return \"India\";\n case 26:\n return \"Philippines\";\n case 27:\n return \"New Zealand\";\n case 28:\n return \"Brazil\";\n case 29:\n return \"Slovakia VSD\";\n case 30:\n return \"Slovakia SSE\";\n case 31:\n return \"Slovakia ZSD\";\n case 32:\n return \"CE10-21 In Areti\";\n default:\n return `Unknown code: ${countryCode}`;\n\n }\n}\n/**\n * read faults(5) every fault is UInt16BE\n * every bit in fault is an error if bit = 1, 16 errors in one fault\n * @param {Buffer} errorBuffer 5 faults 2bytes each = 10bytes long buffer\n * @return array of errors\n */\nfunction errorDecoder(errorBuffer) { // 10bytes in hex\n const errors = [\n [\n \"ID09 - PVOVP(PV Over Voltage Protection)\",\n \"ID10 - IpvUnbalance(PV Input Current Unbalance)\",\n \"ID11 - PvConfigSetWrong(PV Input Mode Configure wrong)\",\n \"ID12 - GFCIFault(Ground - Fault circuit interrupters Fault)\",\n \"ID13 - PhaseSequenceFault(Phase sequence Fault)\",\n \"ID14 - HwBoostOCP(hardware boost over current protection)\",\n \"ID15 - HwAcOCP(Hardware AC over current protection)\",\n \"ID16 - AcRmsOCP(The Grid current is too high)\",\n ], [\n \"ID01 - GridOVP(grid over voltage protection)\",\n \"ID02 - GridUVP(grid under voltage protection)\",\n \"ID03 - GridOFP(grid over frequency protection)\",\n \"ID04 - GridUFP(grid under frequency protection)\",\n \"ID05 - PVUVP(PV Under Voltage Protection)\",\n \"ID06 - GridLVRT(Grid Low Voltage Ride through)\",\n \"ID07 - reserved\",\n \"ID08 - reserved\",\n ], [\n \"ID25 - BusUVP(Bus under voltage protection)\",\n \"ID26 - BusOVP(Bus over voltage protection)\",\n \"ID27 - VbusUnbalance(Bus voltage unbalance)\",\n \"ID28 - DciOCP(The DCI is too high)\",\n \"ID29 - SwOCPInstant(The Grid current is too high)\",\n \"ID30 - SwBOCPInstant(The input current is too high)\",\n \"ID31 - reserved\",\n \"ID32 - reserved\",\n ], [\n \"ID17 - HwADFaultIGrid(The Grid current sampling is error)\",\n \"ID18 - HwADFaultDCI(The DCI sampling is error)\",\n \"ID19 - HwADFaultVGrid(The Grid voltage sampling is error)\",\n \"ID20 - GFCIDeviceFault(GFCI device sampling is error)\",\n \"ID21 - MChip_Fault(Main chip fault)\",\n \"ID22 - HwAuxPowerFault(Hardware auxiliary power fault)\",\n \"ID23 - BusVoltZeroFault(Bus voltage zero fault)\",\n \"ID24 - IacRmsUnbalance(The output current is not balanced)\",\n ], [\n \"ID41\",\n \"ID42\",\n \"ID43\",\n \"ID44\",\n \"ID45\",\n \"ID46\",\n \"ID47\",\n \"ID48\",\n ], [\n \"ID33\",\n \"ID34\",\n \"ID35\",\n \"ID36\",\n \"ID37\",\n \"ID38\",\n \"ID39\",\n \"ID40\",\n ], [\n \"ID57 - OverTempFault_Inv(The inverter temp is too high)\",\n \"ID58 - OverTempFault_Boost(The boost temp is too high)\",\n \"ID59 - OverTempFault_Env(The environment temp is too high)\",\n \"ID60 - PEConnectFault(The inverter is not connect the PE wire)\",\n \"ID61 - reserved\",\n \"ID62 - reserved\",\n \"ID63 - reserved\",\n \"ID64 - reserved\",\n\n ], [\n \"ID49 - ConsistentFault_VGrid(The grid voltage sampling value between the master and slave DSP is Vary widely)\",\n \"ID50 - ConsistentFault_FGrid(The grid frequency sampling value between the master and slave DSP is Vary widely)\",\n \"ID51 - ConsistentFault_DC(The DCI sampling value between the master and slave DSP is Vary widely)\",\n \"ID52 - ConsistentFault_GFCI(The GFCI sampling value between the master and slave DSP is Vary widely)\",\n \"ID53 - SpiCommLose(The communication between the master and slave DSP is fail)\",\n \"ID54 - SciCommLose(The communication between the slave and communication board is fail)\",\n \"ID55 - RelayTestFail(The relay is faulty)\",\n \"ID56 - PvIsoFault(The insulation resistance between the PV array and the earth is too low)\",\n\n ], [\n \"ID73 - reserved\",\n \"ID74 - unrecoverIPVInstant(The input current is too high.and has cause unrecoverable fault)\",\n \"ID75 - unrecoverWRITEEEPROM(The EEPROM is faulty)\",\n \"ID76 - unrecoverREADEEPROM(The EEPROM is faulty)\",\n \"ID77 - unrecoverRelayFail(The relay is faulty, and has cause unrecoverable fault)\",\n \"ID78 - reserved\",\n \"ID79 - reserved\",\n \"ID80 - reserved\",\n ], [\n \"ID65 - unrecoverHwAcOCP(The grid current is too high,and has cause unrecoverable fault)\",\n \"ID66 - unrecoverBusOVP(The bus voltage is too high,and has cause unrecoverable fault)\",\n \"ID67 - unrecoverIacRmsUnbalance(The grid current is unbalance,and has cause unrecoverable fault)\",\n \"ID68 - unrecoverIpvUnbalance(The input current is unbalance,and has cause unrecoverable fault)\",\n \"ID69 - unrecoverVbusUnbalance(The bus voltage is unbalance,and has cause unrecoverable fault)\",\n \"ID70 - unrecoverOCPInstant(The grid current is too high,and has cause unrecoverable fault)\",\n \"ID71 - unrecoverPvConfigSetWrong(PV Input Mode Configure wrong,and has cause unrecoverable fault)\",\n \"ID72 - reserved\",\n\n ],\n ];\n let currentErrors = [];\n for (let byteNr = 0; byteNr < errorBuffer.length / 2; byteNr += 2) {\n const faultBuffer = errorBuffer.subarray(byteNr, byteNr + 2);\n for (let faultByte = 0; faultByte < faultBuffer.length; faultByte++) {\n const byteData = faultBuffer.readUint8(faultByte);\n const byteDataStr = `0000000${byteData.toString(2)}`.slice(-8); // byte data 8 bits string\n let bitArray = Array.from(byteDataStr, x => Number.parseInt(x, 2));\n bitArray.reverse(); // flip array so bit 0 is @ bitArray[0]\n let bitErrors = errors[byteNr + faultByte].filter((element, index) => {\n return bitArray[index] == 1;\n });\n currentErrors.push(bitErrors);\n }\n }\n return currentErrors.flat();\n\n}\n\nfunction log(level, message) {\n switch (level) {\n case \"warn\":\n if (env.get(\"logLvl\") >= 1) { node.warn(\"[w] \" + message); }\n break;\n case \"info\":\n if (env.get(\"logLvl\") >= 2) { node.log(\"[i] \" + message); }\n break;\n case \"debug\":\n if (env.get(\"logLvl\") >= 3) { node.log(\"[d] \" + message); }\n break;\n }\n}\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 890,
"y": 160,
"wires": [
[]
]
},
{
"id": "fe207705d8d77038",
"type": "function",
"z": "072561a1188f3103",
"name": "decode header",
"func": "if (typeof msg.debug === 'undefined') { msg.debug = {} };\nmsg.debug.header = decodeHeader(msg.payload.header);\nflow.set('requestNr', (msg.debug.header.requestNr >= 255) ? 1 : (msg.debug.header.requestNr + 1));\nflow.set('responseNr', msg.debug.header.responseNr);\nreturn msg;\n\nfunction decodeHeader(buffer) {\n const header = {};\n header.dataLength = buffer.readUInt16LE(1);\n header.requestNr = buffer.readUInt8(5);\n header.responseNr = buffer.readUInt8(6);\n header.loggerSN = buffer.readUInt32LE(7);\n return header;\n}\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 640,
"y": 160,
"wires": [
[
"2af8772ff413b097"
]
]
},
{
"id": "b36ae6ac1f32590f",
"type": "function",
"z": "072561a1188f3103",
"name": "check frame",
"func": "msg.payload = checkFrame(msg.payload);\nmsg.debug = {};\nmsg.debug.frame = msg.payload;\nmsg.topic = \"frame\";\nmsg.timestamp = Date.now(); // timestamp in ms\nreturn msg;\n\nfunction checkFrame(frameBuffer) {\n log(\"debug\", `checking frame buffer(${frameBuffer.length})`);\n const headerSize = 11;\n const footerSize = 2;\n if (frameBuffer.length < (headerSize + footerSize)) {\n log(\"warn\", `response frame too short(got: ${frameBuffer.length}, minimum: ${headerSize + footerSize})`);\n return frameBuffer;\n }\n const frame = {};\n const frameDataLength = frameBuffer.readUInt16LE(1);\n const calculatedLength = headerSize + frameDataLength + footerSize;\n if (frameBuffer.length > calculatedLength) {\n if (frameBuffer[calculatedLength - 1] === 0x15) {\n frame.length = calculatedLength;\n log(\"warn\", `extra data after frame end(${frame.length}), +${frameBuffer.length - calculatedLength})`);\n frame.extraData = checkFrame(frameBuffer.subarray(calculatedLength));\n } else {\n log(\"warn\", `frame longer than expected(got: ${frame.length}, expected: ${calculatedLength})`);\n frame.length = frameBuffer.length;\n }\n } else if (frameBuffer.length < calculatedLength) {\n log(\"warn\", `frame shorter than expected(got: ${frame.length}, expected: ${calculatedLength})`);\n frame.length = frameBuffer.length;\n } else {\n log(\"info\", `frame length OK(${calculatedLength})`);\n frame.length = calculatedLength;\n }\n frame.isValidV5frame = frameBuffer[0] === 0xA5 && frameBuffer[frame.length - 1] === 0x15;\n if (!frame.isValidV5frame) { log(\"warn\", \"invalid V5 frame\"); }\n else { log(\"info\", \"valid V5 frame\"); }\n const calculatedChecksum = frameChecksum(frameBuffer, frame.length);\n const checksum = frameBuffer.readUInt8(frame.length - footerSize);\n const data = frameBuffer.subarray(headerSize, frame.length - footerSize);\n if (calculatedChecksum === checksum) {\n frame.frameChecksum = \"OK\";\n log(\"info\", \"frame checksum OK\");\n } else {\n frame.frameChecksum = \"mismatch\";\n log(\"warn\", \"frame checksum mismatch\");\n }\n frame.buffer = frameBuffer;\n frame.header = frameBuffer.subarray(0, 11);\n frame.data = checkData(data);\n frame.footer = frameBuffer.subarray(frame.length - 2, frame.length);\n frame.timestamp = Math.floor(Date.now() / 1000); // unixtimestamp in seconds\n return frame;\n}\n\nfunction frameChecksum(frame, length) {\n frame = frame.subarray(1, length - 2);\n let sum = 0x0;\n for (const byte of frame) {\n sum += byte;\n }\n sum = sum % 256;\n return sum;\n}\n\nfunction checkData(dataBuffer) {\n log(\"debug\", `checking data buffer(${dataBuffer.length})`);\n const solarmanHeaderLength = 14;\n if (dataBuffer.length >= solarmanHeaderLength) {\n const data = {};\n data.solarmanHeader = dataBuffer.subarray(0, solarmanHeaderLength);\n if (dataBuffer.length === solarmanHeaderLength) {\n log(\"warn\", \"frame data empty\");\n return data\n }\n log(\"debug\", \"data buffer OK\");\n data.modbusFrame = dataBuffer.subarray(solarmanHeaderLength);\n data.modbusFrameCrc = checkModbusFrame(data.modbusFrame);\n return data;\n } else if (dataBuffer.length === 1 && dataBuffer[0] === 0x00) {\n log(\"warn\", \"null data\");\n return dataBuffer\n } else {\n log(\"warn\", `data too short(got: ${dataBuffer.length}, minimum: ${solarmanHeaderLength})`);\n return dataBuffer\n }\n}\n\nfunction checkModbusFrame(frame) {\n log(\"debug\", `checking modbus frame(${frame.length})`);\n let modbusFrameCrc = undefined;\n const headerLength = 3; // first 3 bytes are: slave address, function code, data length\n const crcLength = 2; // int 16 LE\n if (frame.length >= (headerLength + crcLength)) {\n const calculatedCrc = crc16modbus(frame.subarray(0, frame.length - crcLength));\n const crc = frame.readUInt16LE(frame.length - crcLength);\n if (calculatedCrc === crc) {\n modbusFrameCrc = \"OK\";\n log(\"info\", \"modbus frame crc OK\");\n const registersDataLength = frame[2]; // data length\n const calculatedDataLength = frame.length - (headerLength + crcLength);\n if (frame.length === (headerLength + crcLength)) {\n log(\"warn\", \"modbus frame data empty\");\n } else if (registersDataLength !== calculatedDataLength) {\n log(\"warn\", `wrong modbus registers data length(got: ${calculatedDataLength}, expected: ${registersDataLength})`);\n } else {\n log(\"debug\", `modbus registers data length OK(${registersDataLength})`);\n }\n } else {\n modbusFrameCrc = \"mismatch\";\n log(\"warn\", \"modbus frame crc mismatch\");\n }\n } else {\n log(\"warn\", `modbus frame too short(got: ${frame.length}, minimum: ${headerLength + crcLength})`);\n }\n return modbusFrameCrc;\n}\n\nfunction log(level, message) {\n switch (level) {\n case \"warn\":\n if (env.get(\"logLvl\") >= 1) { node.warn(\"[w] \" + message); }\n break;\n case \"info\":\n if (env.get(\"logLvl\") >= 2) { node.log(\"[i] \" + message); }\n break;\n case \"debug\":\n if (env.get(\"logLvl\") >= 3) { node.log(\"[d] \" + message); }\n break;\n }\n}\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "crc16modbus",
"module": "crc/crc16modbus"
}
],
"x": 410,
"y": 160,
"wires": [
[
"fe207705d8d77038"
]
],
"outputLabels": [
"data"
]
},
{
"id": "2264ac720797774c",
"type": "change",
"z": "072561a1188f3103",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "error.message",
"tot": "msg"
},
{
"t": "set",
"p": "topic",
"pt": "msg",
"to": "error",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 900,
"y": 200,
"wires": [
[]
]
},
{
"id": "a69591f381e80034",
"type": "catch",
"z": "072561a1188f3103",
"name": "modbus request errors",
"scope": [
"750e4979aad7f48f"
],
"uncaught": false,
"x": 620,
"y": 200,
"wires": [
[
"2264ac720797774c"
]
]
}
]