queclink-parser 1.9.19 → 1.9.22

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/src/gl533cg.js ADDED
@@ -0,0 +1,523 @@
1
+ 'use strict'
2
+ const utils = require('./utils.js')
3
+
4
+ /*
5
+ Parses messages data from GL533CG devices
6
+ */
7
+ const parse = raw => {
8
+ let buf
9
+ if (typeof raw === 'string') {
10
+ buf = Buffer.from(raw, 'binary')
11
+ } else {
12
+ buf = raw
13
+ }
14
+
15
+ // Header 1 byte
16
+ // 00H 1 byte
17
+ // Frame Length 2 bytes
18
+ // Multi-packet Flag 1 byte
19
+ const multiPacketFlag = buf.readUInt8(4)
20
+ const isMultiPacket = multiPacketFlag >> 7 === 1
21
+ let offset = 5
22
+
23
+ if (isMultiPacket) {
24
+ offset += 2 // Frame Count & Frame Number
25
+ }
26
+
27
+ // IMEI 8 bytes (16 hex digits)
28
+ const imeiBuffer = buf.slice(offset, offset + 8)
29
+ const imei = imeiBuffer.toString('hex').replace(/^0+/, '') // e.g. 0123456789012345
30
+ offset += 8
31
+
32
+ // Device Type 2 bytes
33
+ const deviceTypeInt = buf.readUInt16BE(offset)
34
+ const deviceTypeStr = deviceTypeInt
35
+ .toString(16)
36
+ .padStart(4, '0')
37
+ .toUpperCase() // C301
38
+ offset += 2
39
+
40
+ // Protocol Version 2 bytes
41
+ const protocolVersionInt = buf.readUInt16BE(offset)
42
+ const protocolVersionStr = protocolVersionInt
43
+ .toString(16)
44
+ .padStart(4, '0')
45
+ .toUpperCase() // 0005
46
+ offset += 2
47
+
48
+ // Custom Version 1 byte
49
+ buf.readUInt8(offset)
50
+ offset += 1
51
+
52
+ // Reserved Field Length 1 byte
53
+ const reservedLen = buf.readUInt8(offset)
54
+ offset += 1 + reservedLen
55
+
56
+ let data = {
57
+ raw: typeof raw === 'string' ? raw : raw.toString('hex').toUpperCase(),
58
+ manufacturer: 'queclink',
59
+ device: 'Queclink-GL533CG',
60
+ type: 'data',
61
+ imei: imei,
62
+ protocolVersion: utils.getProtocolVersion(
63
+ `${deviceTypeStr}${protocolVersionStr}`
64
+ ),
65
+ temperature: null,
66
+ history: buf[0] === 0x2d, // 2DH means Buffer Report
67
+ sentTime: null,
68
+ serialId: null
69
+ }
70
+
71
+ // Records Part
72
+ // Read records
73
+ while (offset < buf.length - 4) {
74
+ // keep 4 bytes for Count, CRC, Tail
75
+ const recordLengthByte = buf.readUInt8(offset)
76
+ let recordLength
77
+ let recordLenSize
78
+ if (recordLengthByte >> 7 === 0) {
79
+ recordLength = recordLengthByte
80
+ recordLenSize = 1
81
+ } else {
82
+ recordLength = buf.readUInt16BE(offset) & 0x7fff
83
+ recordLenSize = 2
84
+ }
85
+ const recordEnd = offset + recordLenSize + recordLength
86
+ offset += recordLenSize
87
+
88
+ // Generated Time 4 bytes
89
+ const genTimeTimestamp = buf.readUInt32BE(offset)
90
+ data.sentTime = new Date(genTimeTimestamp * 1000)
91
+ offset += 4
92
+
93
+ // Record Count Number (Serial ID) 2 bytes
94
+ const serialId = buf.readUInt16BE(offset)
95
+ if (data.serialId === null) data.serialId = serialId
96
+ offset += 2
97
+
98
+ // Record ID 1 or 2 bytes
99
+ const recordIdByte = buf.readUInt8(offset)
100
+ let recordId
101
+ if (recordIdByte >> 3 === 0x1f) {
102
+ recordId = buf.readUInt16BE(offset) & 0x7fff
103
+ offset += 2
104
+ } else {
105
+ recordId = recordIdByte
106
+ offset += 1
107
+ }
108
+
109
+ // Event Code (1 byte)
110
+ const eventCode = buf.readUInt8(offset)
111
+ offset += 1
112
+
113
+ if (recordId === 0x50 || recordId === 0x04) {
114
+ // 50H / 04H Fixed Report Information
115
+ data.alarm = utils.getAlarm('GTFRI', null)
116
+ } else if (recordId === 0x01) {
117
+ // 01H Device Startup
118
+ data.alarm = utils.getAlarm('GTPNA', null)
119
+ } else if (recordId === 0x05) {
120
+ // 05H Device Shutdown
121
+ data.alarm = utils.getAlarm('GTPFA', null)
122
+ } else if (recordId === 0x10) {
123
+ // 10H SOS Notification
124
+ data.alarm = utils.getAlarm('GTSOS', null)
125
+ } else if (recordId === 0x11) {
126
+ // 11H Device Basic Information
127
+ data.alarm = utils.getAlarm('GTATI', null)
128
+ } else if (recordId === 0x12) {
129
+ // 12H Real Time Location
130
+ data.alarm = utils.getAlarm('GTRTL', null)
131
+ // } else if (recordId === 0x17) { // 17H Geo-fence events
132
+ // data.alarm = utils.getAlarm('GTGEO', null)
133
+ } else if (recordId === 0x20) {
134
+ // 20H Internal Battery Information
135
+ data.alarm = utils.getAlarm('GTBPL', null)
136
+ // } else if (recordId === 0x21) { // 21H Motion Information
137
+ // data.alarm = utils.getAlarm('GSENSOR', null)
138
+ } else if (recordId === 0x87) {
139
+ // 87H Light Intensity Alarm
140
+ data.alarm = utils.getAlarm('LIGHT', null)
141
+ } else if (recordId === 0x94) {
142
+ // 94H SVR Connection Notification
143
+ data.alarm = utils.getAlarm('GTSVR', eventCode)
144
+ } else if (recordId === 0xf3) {
145
+ // F3H GTC Status
146
+ data.alarm = utils.getAlarm('GTALM', null)
147
+ }
148
+
149
+ // Inside record, process Data IDs until recordEnd
150
+ while (offset < recordEnd && offset < buf.length - 4) {
151
+ const dataIdByte = buf.readUInt8(offset)
152
+ let dataId
153
+ if (dataIdByte >> 7 === 0) {
154
+ dataId = dataIdByte
155
+ offset += 1
156
+ } else {
157
+ dataId = buf.readUInt16BE(offset) & 0x7fff
158
+ offset += 2
159
+ }
160
+
161
+ const dataLenByte = buf.readUInt8(offset)
162
+ let dataLen
163
+ if (dataLenByte >> 7 === 0) {
164
+ dataLen = dataLenByte
165
+ offset += 1
166
+ } else {
167
+ dataLen = buf.readUInt16BE(offset) & 0x7fff
168
+ offset += 2
169
+ }
170
+
171
+ const dataContent = buf.slice(offset, offset + dataLen)
172
+ offset += dataLen
173
+
174
+ if (dataId === 0x01) {
175
+ // Profile ID
176
+ data.profileId = dataContent.readUInt8(0)
177
+ } else if (dataId === 0x02) {
178
+ // Device Name
179
+ // data.deviceName = dataContent.toString()
180
+ continue
181
+ } else if (dataId === 0x04) {
182
+ // Device Serial Number
183
+ data.serialNumber = dataContent.toString()
184
+ } else if (dataId === 0x07) {
185
+ // Current Working Mode
186
+ data.status = data.status || {}
187
+ const mode = dataContent.readUInt8(0)
188
+ data.connectionMode =
189
+ mode === 0x00
190
+ ? 'Power saving'
191
+ : mode === 0x01 ? 'Continuous' : 'Unknown'
192
+ } else if (dataId === 0x0a) {
193
+ // Internal Battery Percentage
194
+ data.voltage = data.voltage || {}
195
+ data.voltage.battery = dataContent.readUInt8(0)
196
+ data.voltage.inputCharge = false
197
+ } else if (dataId === 0x11) {
198
+ // RF433 Working Status
199
+ const rfState = dataContent.readUInt8(0)
200
+ data.rf433 =
201
+ rfState === 0
202
+ ? 'Normal'
203
+ : rfState === 1
204
+ ? 'Module abnormal'
205
+ : rfState === 2 ? 'Antenna abnormal' : 'Unknown'
206
+ } else if (dataId === 0x13) {
207
+ // Triggered Time
208
+ const triggeredTime = dataContent.readUInt32BE(0)
209
+ data.datetime = new Date(triggeredTime * 1000)
210
+ } else if (dataId === 0x16) {
211
+ // Record Count (History count)
212
+ data.recordCount = dataContent.readUInt16BE(0)
213
+ } else if (dataId === 0x17) {
214
+ // Motion Status
215
+ const motion = dataContent.readUInt8(0)
216
+ data.status = data.status || {}
217
+ data.status.motion =
218
+ motion === 0x00
219
+ ? 'Motionless'
220
+ : motion === 0x01 ? 'Moving' : 'Unknown'
221
+ data.alarm = utils.getAlarm('GSENSOR', parseInt(motion).toString(2))
222
+ // } else if (dataId === 0x22) { // Real-time Customization
223
+ // data.alarm = utils.getAlarm('GTINFO', null)
224
+ } else if (dataId === 0x2b) {
225
+ // Light Sensor
226
+ data.lightSensor =
227
+ dataContent.readUInt8(0) === 0x01 ? 'Triggered' : 'Normal'
228
+ } else if (dataId === 0x50) {
229
+ // Fixed Report Information
230
+ data.alarm = utils.getAlarm('GTFRI', null)
231
+ } else if (dataId === 0x51) {
232
+ // 81 Mini Location
233
+ let locOffset = 0
234
+ const fixStateMode = dataContent.readUInt8(locOffset)
235
+ const signalStrength = (fixStateMode >> 4) & 0x0f
236
+ const fixState = (fixStateMode >> 2) & 0x03
237
+ const fixMode = fixStateMode & 0x03
238
+ locOffset += 1
239
+
240
+ const lon = dataContent.readInt32BE(locOffset) / 1000000
241
+ locOffset += 4
242
+ const lat = dataContent.readInt32BE(locOffset) / 1000000
243
+ locOffset += 4
244
+
245
+ const utcTime = dataContent.readUInt32BE(locOffset)
246
+ data.datetime = new Date(utcTime * 1000)
247
+ locOffset += 4
248
+
249
+ const speed = dataContent.readUInt16BE(locOffset) * 0.1
250
+
251
+ data.loc = { type: 'Point', coordinates: [lon, lat] }
252
+ data.speed = parseFloat(speed.toFixed(1))
253
+ data.gpsStatus = utils.checkGps(lon, lat)
254
+ data.gpsSignalStrength = utils.getSignalStrength(
255
+ 'GPS',
256
+ signalStrength,
257
+ true
258
+ )
259
+ data.fixState =
260
+ fixState === 0
261
+ ? 'Off'
262
+ : fixState === 1 ? 'No fix' : fixState === 2 ? 'Fix' : 'Unknown'
263
+ data.fixMode = fixMode === 0 ? '2D' : fixMode === 1 ? '3D' : 'Unknown'
264
+ } else if (dataId === 0x52) {
265
+ // 82 Full Location
266
+ let locOffset = 0
267
+ const fixStateMode = dataContent.readUInt8(locOffset)
268
+ const signalStrength = (fixStateMode >> 4) & 0x0f
269
+ const fixState = (fixStateMode >> 2) & 0x03
270
+ const fixMode = fixStateMode & 0x03
271
+ locOffset += 1
272
+
273
+ const lon = dataContent.readInt32BE(locOffset) / 1000000
274
+ locOffset += 4
275
+ const lat = dataContent.readInt32BE(locOffset) / 1000000
276
+ locOffset += 4
277
+
278
+ const utcTime = dataContent.readUInt32BE(locOffset)
279
+ data.datetime = new Date(utcTime * 1000)
280
+ locOffset += 4
281
+
282
+ const speed = dataContent.readUInt16BE(locOffset) * 0.1
283
+ locOffset += 2
284
+
285
+ const hdopByte = dataContent.readUInt8(locOffset)
286
+ const hdop = hdopByte === 0xff ? 25.5 : hdopByte * 0.1
287
+ locOffset += 1
288
+
289
+ const azimuth = dataContent.readUInt16BE(locOffset)
290
+ locOffset += 2
291
+
292
+ const altHigh = dataContent.readInt8(locOffset)
293
+ const altMid = dataContent.readUInt8(locOffset + 1)
294
+ const altLow = dataContent.readUInt8(locOffset + 2)
295
+ const altitude = ((altHigh << 16) | (altMid << 8) | altLow) * 0.1
296
+ locOffset += 3
297
+
298
+ const satellites = dataContent.readUInt8(locOffset)
299
+
300
+ data.loc = { type: 'Point', coordinates: [lon, lat] }
301
+ data.speed = parseFloat(speed.toFixed(1))
302
+ data.hdop = parseFloat(hdop.toFixed(1))
303
+ data.azimuth = azimuth
304
+ data.altitude = parseFloat(altitude.toFixed(1))
305
+ data.gpsStatus = utils.checkGps(lon, lat)
306
+ data.gpsSignalStrength = utils.getSignalStrength(
307
+ 'GPS',
308
+ signalStrength,
309
+ true
310
+ )
311
+ data.fixState =
312
+ fixState === 0
313
+ ? 'Off'
314
+ : fixState === 1 ? 'No fix' : fixState === 2 ? 'Fix' : 'Unknown'
315
+ data.fixMode = fixMode === 0 ? '2D' : fixMode === 1 ? '3D' : 'Unknown'
316
+ data.satellites = satellites
317
+ } else if (dataId === 0x55) {
318
+ // 85 Registered Cell
319
+ const plmnLen = dataContent.readUInt8(0)
320
+ const plmnVal =
321
+ (dataContent.readUInt8(1) << 16) |
322
+ (dataContent.readUInt8(2) << 8) |
323
+ dataContent.readUInt8(3)
324
+ const plmnStr = plmnVal.toString().padStart(plmnLen, '0')
325
+ const mccStr = plmnStr.substring(0, 3)
326
+ const mncStr = plmnStr.substring(3)
327
+ data.mcc = utils.latamMcc[parseInt(mccStr, 10)] || null
328
+ data.mnc = utils.getMNC(mccStr, mncStr)
329
+ data.lac = dataContent.readUInt16BE(4)
330
+ data.cid = dataContent.readUInt32BE(6)
331
+ // Access Tech: Bit 7 Roaming & Bit 6 - 0 Technology
332
+ const techRoaming = dataContent.readUInt8(10)
333
+ const tech = techRoaming & 0x7f
334
+ data.accessTechnology =
335
+ tech === 0 ? 'GSM' : tech === 5 ? 'LTE Cat-1' : 'Unknown'
336
+ data.roaming = techRoaming >> 7 === 1
337
+
338
+ const rssi = dataContent.readUInt8(12)
339
+ if (rssi !== 0xff && rssi !== 99) {
340
+ data.rssi = rssi <= 31 ? rssi * 2 - 113 : null
341
+ }
342
+ } else if (dataId === 0x58) {
343
+ // 88 SIM Card
344
+ data.imsi = dataContent
345
+ .slice(0, 8)
346
+ .toString('hex')
347
+ .replace(/^0/, '')
348
+ .replace(/f$/i, '')
349
+ if (dataContent.length > 8) {
350
+ const iccidContent = dataContent.slice(8)
351
+ if (iccidContent[0] === 0xff) {
352
+ data.iccid = iccidContent.slice(2, 2 + iccidContent[1]).toString()
353
+ } else {
354
+ data.iccid = iccidContent.toString('hex').replace(/f$/i, '')
355
+ }
356
+ }
357
+ } else if (dataId === 0x59) {
358
+ // 89 GSV
359
+ const svCount = dataContent.readUInt8(0)
360
+ data.satellites_details = []
361
+ for (let i = 0; i < svCount; i++) {
362
+ const svOffset = 1 + i * 6
363
+ if (svOffset + 6 <= dataContent.length) {
364
+ data.satellites_details.push({
365
+ gnssId: dataContent.readUInt8(svOffset),
366
+ svId: dataContent.readUInt8(svOffset + 1),
367
+ snr: dataContent.readUInt8(svOffset + 2),
368
+ elevation: dataContent.readUInt8(svOffset + 3),
369
+ azimuth: dataContent.readUInt16BE(svOffset + 4)
370
+ })
371
+ }
372
+ }
373
+ } else if (dataId === 0x5a) {
374
+ // 90 Versions
375
+ const mask = dataContent.readUInt8(0)
376
+ if (mask === 0x80) {
377
+ let vOffset = 1
378
+ while (vOffset < dataContent.length) {
379
+ const type = dataContent.readUInt8(vOffset++)
380
+ const len = dataContent.readUInt8(vOffset++)
381
+ const val = dataContent.slice(vOffset, vOffset + len).toString()
382
+ vOffset += len
383
+ if (type === 0x01) data.hardwareVersion = val
384
+ else if (type === 0x02) data.appVersion = val
385
+ else if (type === 0x05) data.bootloaderVersion = val
386
+ else if (type === 0x06) data.firmwareVersion = val
387
+ else if (type === 0x09) data.bleBootloaderVersion = val
388
+ else if (type === 0x0a) data.bleFirmwareVersion = val
389
+ }
390
+ }
391
+ } else if (dataId === 0x5c) {
392
+ // 92 GEO Status
393
+ data.geofence = data.geofence || {}
394
+ data.geofence.id = dataContent.readUInt8(0)
395
+ const gStat = dataContent.readUInt8(1)
396
+ data.geofence.status =
397
+ gStat === 0x00 ? 'Inside' : gStat === 0x01 ? 'Outside' : 'Unknown'
398
+ } else if (dataId === 0x5d) {
399
+ // 93 All GEO Status
400
+ const geofenceCount = dataLen / 2
401
+ data.geofences = []
402
+ for (let i = 0; i < geofenceCount; i++) {
403
+ data.geofences.push({
404
+ id: dataContent.readUInt8(i * 2),
405
+ status:
406
+ dataContent.readUInt8(i * 2 + 1) === 0x00 ? 'Inside' : 'Outside'
407
+ })
408
+ }
409
+ } else if (dataId === 0x65 || dataId === 0x66 || dataId === 0x67) {
410
+ // 101/102/103 Upgrade/Update/Get Config
411
+ const getUpgradeStatus = (c, sc) => {
412
+ const codes = {
413
+ 1: 'Start download',
414
+ 2: 'Start update',
415
+ 3: 'Download successful',
416
+ 4: 'Download failed',
417
+ 5: 'Update successful',
418
+ 6: 'Update failed'
419
+ }
420
+ const subcodes = {
421
+ 0: 'Normal',
422
+ 16: 'Low battery',
423
+ 32: 'No network',
424
+ 33: 'Connection failed',
425
+ 48: 'File not found',
426
+ 49: 'CRC error',
427
+ 50: 'Type error',
428
+ 51: 'Size error',
429
+ 52: 'Incompatible version',
430
+ 240: 'In progress',
431
+ 255: 'Unknown'
432
+ }
433
+ return {
434
+ code: codes[c] || `Unknown (${c})`,
435
+ subcode: subcodes[sc] || `Unknown (${sc})`
436
+ }
437
+ }
438
+ const status = getUpgradeStatus(
439
+ dataContent.readUInt8(0),
440
+ dataContent.readUInt8(1)
441
+ )
442
+ const fieldName =
443
+ dataId === 0x65
444
+ ? 'upgradeInfo'
445
+ : dataId === 0x66 ? 'updateConfig' : 'getConfig'
446
+ data[fieldName] = {
447
+ status: status.code,
448
+ reason: status.subcode,
449
+ sequence: dataContent.readUInt16BE(5)
450
+ }
451
+ } else if (dataId === 0x61) {
452
+ // 97 Internal Battery Status
453
+ data.voltage = data.voltage || {}
454
+ data.voltage.connected = (dataContent.readUInt8(0) & 0x01) === 1
455
+ data.voltage.value = dataContent.readUInt16BE(1)
456
+ data.voltage.battery = dataContent.readUInt8(3)
457
+ const charging = dataContent.readUInt8(4)
458
+ data.voltage.inputCharge = charging !== 0x02 && charging !== 0x007
459
+ } else if (dataId === 0x78) {
460
+ // 120 Self Test
461
+ data.selfTestTime = new Date(dataContent.readUInt32BE(0) * 1000)
462
+ const conn = dataContent.readUInt32BE(4)
463
+ data.selfTest = {
464
+ imeiAbnormal: (conn & 0x04) !== 0,
465
+ simAbnormal: (conn & 0x08) !== 0,
466
+ gnssAbnormal: (conn & 0x10) !== 0,
467
+ gSensorAbnormal: (conn & 0x20) !== 0,
468
+ bluetoothAbnormal: (conn & 0x80) !== 0,
469
+ bluetoothMacAbnormal: (conn & 0x100) !== 0
470
+ }
471
+ } else if (dataId === 173) {
472
+ // 80ADH SVR Status (ID 173 dec)
473
+ const appendInfo = dataContent.slice(8).toString('hex') !== '00'
474
+ data.bluetooth = data.bluetooth || {}
475
+ data.bluetooth.connection =
476
+ dataContent.readUInt8(0) === 0x01 ? 'Normal' : 'Abnormal'
477
+ data.bluetooth.role =
478
+ dataContent.readUInt8(1) === 0x00 ? 'Master' : 'Slave'
479
+ data.bluetooth.targetMac = dataContent
480
+ .slice(2, 8)
481
+ .toString('hex')
482
+ .toUpperCase()
483
+ if (appendInfo) {
484
+ const lostReasonHex = dataContent
485
+ .slice(8, dataContent.length - 1)
486
+ .toString('hex')
487
+ data.bluetooth.lostReason =
488
+ lostReasonHex === '03'
489
+ ? 'Bluetooth connection failed'
490
+ : lostReasonHex === '07'
491
+ ? 'Bluetooth connection successfull'
492
+ : lostReasonHex === '08'
493
+ ? 'Failed to decrypt interactive data'
494
+ : lostReasonHex === '09'
495
+ ? 'Data check is incorrect (after decryption)'
496
+ : lostReasonHex === '0B'
497
+ ? 'No data interaction (after connection)'
498
+ : 'Unknown'
499
+ }
500
+ } else if (dataId === 159) {
501
+ // 809FH Bluetooth Info (ID 159 dec)
502
+ data.bluetooth = data.bluetooth || {}
503
+ const nameLength = dataContent.readUInt8(0)
504
+ data.bluetooth.name = dataContent.slice(1, nameLength + 1).toString()
505
+ data.bluetooth.mac = dataContent
506
+ .slice(nameLength + 1, nameLength + 7)
507
+ .toString('hex')
508
+ .toUpperCase()
509
+ }
510
+ }
511
+ }
512
+
513
+ data.status = data.status || null
514
+ data.odometer = data.odometer || null
515
+ data.hourmeter = data.hourmeter || null
516
+ data.gpsStatus = data.gpsStatus || false
517
+
518
+ return data
519
+ }
520
+
521
+ module.exports = {
522
+ parse: parse
523
+ }