-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
1200 lines (1060 loc) · 36.3 KB
/
index.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
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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// BMV
//
//'use strict';
// TODO:
// - introduce force cmds that repeat + reset until done
// - introduce optional read from cache
// - first parse: parse until checksum ok then create objects from it for cache - only then do the up/download of config
// - further parse: replace callback function by final parse function to do updates
// - register function has switch statement to create each object after each other (only those appearing in update packet)
// - add a on function for CHECKSUM that sends collection of all changes
// - remove map key from objects and do it like addressCache
// - iterate over bmvdata rather than map as bmvdata shall have all entries
// - response from BMV to set command: also compare returned value
// - make address class that handles adresses for display and use (endian swapping)
// - make on a list so many callbacks can be called
const Math = require('mathjs');
var fs = require('fs');
//var util = require('util');
//var log_file = fs.createWriteStream(__dirname + '/debug.log', {flags: 'w'});
var record_file = fs.createWriteStream(__dirname + '/serial-test-data.log', {flags: 'w'});
var log_stdout = process.stdout;
var serialport = require('serialport');
var conv = require('./hexconv');
var log4js = require('log4js');
var deviceCache = require('./device_cache.js');
log4js.configure({
appenders: {
everything: { type: 'file', filename: '/var/log/debug.log' }
},
categories: {
default: { appenders: [ 'everything' ], level: 'trace' }
}
});
// set isRecording true to record the incoming data stream to record_file
var isRecording = false;
// e.g. set relay on, off, on, off, on, off mostly does not make
// sense so it is enough to send the last command "off"
var cmdCompression = true;
var isSerialOperational = false;
const logger = log4js.getLogger();
logger.level = 'debug';
var date = new Date();
logger.debug("Service started at " + date.toLocaleString('en-GB', { timeZone: 'UTC' }));
// Data model:
//
// Each value (volt, current, power, state of charge...) owns a register
// on the device. All registers are cached in objects of this application.
// These objects also provide conversions, formatters, units, callbacks
// on change, descriptions etc.
// The BMV sends some of these register values in one package every
// 1 second (1-second-updates). Among them are the history values
// (H1, H2, ...) and the actual values like voltage, current,
// power, state of charge.
//
// For convenience there are 3 maps pointing to the same objects:
//
// addressCache: maps addresses of device registers (keys) to objects,
// e.g. the voltage 'V' is stored in register
// at address 0xED8D. When reading the value
// directly from the register it needs to be
// multiplied by 10 to get millivolts.
// bmvdata: maps human readable names to the same objects,
// e.g. 'V' is the upper voltage, hence bmvdata.upperVoltage
// map: maps the keys of the 1-second-updates to the same objects
// e.g. in the package is a string "V<tab>24340" which
// will be written to map['V'].value and
// maps contains more values than contained in the packages
// bmvdata maps human readable keys to objects
var bmvdata = deviceCache.bmvdata;
// map's keys correspond to the keys used in the frequent 1-second-updates
var map = deviceCache.map;
// addressCache's keys map the register's addresses to the objects
var addressCache = deviceCache.addressCache;
// measured avg response time approx. 3873ms
const cmdResponseTimeoutMS = 5000; // 5 seconds
const cmdMaxRetries = 3;
// // overloade console.log
// console.log = function(msg)
// {
// let d = new Date();
// //log_file.write('[' + d.toUTCString() + '] ' + util.format(msg) + '\n');
// log_file.write('[' + d.getTime() + '] ' + util.format(msg) + '\n');
// //log_stdout.write(util.format(msg) + '\n');
// }
// List of getters and setters for the BMV (do your own setters and getters for
// MPPT, other BMV, Phoenix Inverter):
//
// NOTE: you can add your own getter and setters, however keep in mind:
//
// 1 reading from the 1-second-updates device cache is much faster for
// values contained in the update package. Getting a value
// from a register takes in avg 3.8 seconds. So you better get
// it from the cache within less than 1 second.
// 2 setting certain values does not make sense and you should not
// try to do so. Why would you want to set the current or the
// voltage? They are measurements and should actually not be
// writable at all
// 3 setting the alarm and provoking and alarm sound or resetting
// the alarm simply does not work.
// 4 what makes sense is to do a better calculation of the SOC and
// the TTG. They need to be updated regularly because the BMV
// algorithm makes them drift off quickly with its own calculations.
// These calculations of the state of charge are not very accurate.
// I was always above 90% even if the battery was choking.
// Setting SOC and TTG is demonstrated here.
//
// For reason 2 above you will not find an implementation of the
// following setters:
// 0xED8D 'V'
// 0xED8F 'I'
// 0xED8E 'P'
// 0x0382 'VM'
// 0x030E 'H15'
// 0x030F 'H16'
//
// For reason 1 getters are only implemented for values that
// are not delivered with the 1-second-update package. I.e.
// the following:
const getBatteryCapacity = function()
{
logger.debug("getBatteryCapacity");
get("0x1000");
}
exports.setBatteryCapacity = function(capacity) {
logger.debug("setBatteryCapacity to " + capacity);
let strCapacity = toEndianHexStr(capacity, 2);
set("0x1000", strCapacity);
};
const getChargedVoltage = function()
{
logger.debug("getChargedVoltage");
get("0x1001");
}
exports.setChargedVoltage = function(voltage)
{
logger.debug("setChargedVoltage to " + voltage);
let strVoltage = toEndianHexStr(voltage, 2);
set("0x1001", strVoltage);
}
const getTailCurrent = function()
{
logger.debug("getTailCurrent");
get("0x1002");
}
exports.setTailCurrent = function(current)
{
logger.debug("setTailCurrent to " + current);
let strCurrent = toEndianHexStr(current, 2);
set("0x1002", strCurrent);
}
const getChargedDetectTime = function ()
{
logger.debug("getChargedDetectTime");
get("0x1003");
}
exports.setChargedDetectTime = function(time)
{
logger.debug("setChargedDetectTime to " + time);
let strTime = toEndianHexStr(time, 2);
set("0x1003", strTime);
}
const getChargeEfficiency = function ()
{
logger.debug("getChargeEfficiency");
get("0x1004");
}
exports.setChargeEfficiency = function(percent)
{
logger.debug("setChargeEfficiency to " + percent);
let strPerc = toEndianHexStr(percent, 2);
set("0x1004", strPerc);
}
const getPeukertCoefficient = function ()
{
logger.debug("getPeukertCoefficient");
get("0x1005");
}
exports.setPeukertCoefficient = function(coeff)
{
logger.debug("setPeukertCoefficient to " + coeff);
let strCoeff = toEndianHexStr(coeff, 2);
set("0x1005", strCoeff);
}
const getCurrentThreshold = function ()
{
logger.debug("getCurrentThreshold");
get("0x1006");
}
exports.setCurrentThreshold = function(current)
{
logger.debug("setCurrentThreshold to " + current);
let strCurrent = toEndianHexStr(current, 2);
set("0x1006", strCurrent);
}
const getTimeToGoDelta = function ()
{
logger.debug("getTimeToGoDelta");
get("0x1007");
}
exports.setTimeToGoDelta = function(time)
{
logger.debug("setTimeToGoDelta to " + time);
let strTime = toEndianHexStr(time, 2);
set("0x1007", strTime);
}
const getRelayLowSOC = function()
{
logger.debug("getRelayLowSOC");
get("0x1008");
}
exports.setRelayLowSOC = function(percent)
{
logger.debug("setRelayLowSOC to " + percent);
let strPercent = toEndianHexStr(percent, 2);
set("0x1008", strPercent);
}
const getRelayLowSOCClear = function ()
{
logger.debug("getRelayLowSOCClear");
get("0x1009");
}
exports.setRelayLowSOCClear = function(percent)
{
logger.debug("setRelayLowSOCClear to " + percent);
let strPercent = toEndianHexStr(percent, 2);
set("0x1009", strPercent);
}
const getUserCurrentZero = function ()
{
logger.debug("getUserCurrentZero");
get("0x1034");
}
exports.setUserCurrentZero = function(count)
{
logger.debug("setUserCurrentZero to " + count);
let strCount = toEndianHexStr(count, 2);
set("0x1034", strCount);
}
exports.registerListener = function(bmvdataKey, listener)
{
bmvdata[bmvdataKey].on = listener;
}
var writeDeviceConfig = function(newCurrent, oldCurrent, precision)
{
logger.trace("writeDeviceConfig");
// writeDeviceConfig is called by relayLowSOCClear's
// on function. Until then updateValuesAndValueListeners
// has written all newValue's to value with the exception
// of
let config = {
BatteryCapacity: bmvdata.capacity.value,
ChargedVoltage: bmvdata.chargedVoltage.value,
TailCurrent: bmvdata.tailCurrent.value,
ChargedDetectTime: bmvdata.chargedDetectTime.value,
ChargeEfficiency: bmvdata.chargeEfficiency.value,
PeukertCoefficient: bmvdata.peukertCoefficient.value,
CurrentThreshold: bmvdata.currentThreshold.value,
TimeToGoDelta: bmvdata.timeToGoDelta.value,
RelayLowSOC: bmvdata.relayLowSOC.value,
RelayLowSOCClear: bmvdata.relayLowSOCClear.value
};
logger.debug("Stringify to JSON format");
let jsonConfig = JSON.stringify(config, null, 2);
logger.debug(jsonConfig);
logger.debug("Writing config to file");
let file = __dirname + '/config.json';
// check: exist and writable
fs.access(file, fs.constants.F_OK | fs.constants.W_OK, (err) =>
{
if (err) {
logger.error(
`cannot write ${file} (${err.code === 'ENOENT' ? 'does not exist' : 'is read-only'})`);
} else {
fs.writeFile(file, jsonConfig, (err) => {
if (err) logger.error(err)
else logger.info(file + " saved!");
});
//let config_file = fs.createWriteStream(__dirname + '/config.json', {flags: 'w'});
//config_file.write(jsonConfig);
}
});
console.log("deleting relayLowSOCClear listener");
exports.registerListener('relayLowSOCClear', null);
}
exports.getDeviceConfig = function(doSave)
{
logger.trace("getDeviceConfig");
getBatteryCapacity();
getChargedVoltage();
getTailCurrent();
getChargedDetectTime();
getChargeEfficiency();
getPeukertCoefficient();
getCurrentThreshold();
getTimeToGoDelta();
getRelayLowSOC();
if (doSave) {
// prepare for saving the data:
console.log("registering writeDeviceConfig listener for relayLowSOCClear");
exports.registerListener('relayLowSOCClear', writeDeviceConfig);
getRelayLowSOCClear();
}
}
// EEE functions (switch on/off display values)
// FIXME: the following shown functions need addressCache to be created before calling them
const isVoltageShown = function ()
{
logger.debug("isVoltageShown");
get("0xEEE0");
}
exports.setShowVoltage = function(onOff)
{
logger.debug("setShowVoltage to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE0", strOnOff);
}
const isAuxiliaryVoltageShown = function ()
{
logger.debug("isAuxiliaryVoltageShown");
get("0xEEE1");
}
exports.setShowAuxiliaryVoltage = function(onOff)
{
logger.debug("setShowAuxiliaryVoltage to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE1", strOnOff);
}
const isMidVoltageShown = function ()
{
logger.debug("isMidVoltageShown");
get("0xEEE2");
}
exports.setShowMidVoltage = function(onOff)
{
logger.debug("setShowMidVoltage to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE2", strOnOff);
}
const isCurrentShown = function ()
{
logger.debug("isCurrentShown");
get("0xEEE3");
}
exports.setShowCurrent = function(onOff)
{
logger.debug("setShowCurrent to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE3", strOnOff);
}
const isConsumedAhShown = function ()
{
logger.debug("isConsumedAhShown");
get("0xEEE4");
}
exports.setShowConsumedAh = function(onOff)
{
logger.debug("setShowConsumedAh to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE4", strOnOff);
}
const isStateOfChargeShown = function ()
{
logger.debug("isStateOfChargeShown");
get("0xEEE5");
}
exports.setShowStateOfCharge = function(onOff)
{
logger.debug("setShowStateOfCharge to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE5", strOnOff);
}
const isTimeToGoShown = function ()
{
logger.debug("isTimeToGoShown");
get("0xEEE6");
}
exports.setShowTimeToGo = function(onOff)
{
logger.debug("setShowTimeToGo to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE6", strOnOff);
}
const isTemperatureShown = function ()
{
logger.debug("isTemperatureShown");
get("0xEEE7");
}
exports.setShowTemperature = function(onOff)
{
logger.debug("setShowTemperature to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE7", strOnOff);
}
const isPowerShown = function ()
{
logger.debug("isPowerShown");
get("0xEEE8");
}
exports.setShowPower = function(onOff)
{
logger.debug("setShowPower to " + onOff);
let strOnOff = toEndianHexStr(onOff, 1);
set("0xEEE8", strOnOff);
}
var checksum = 0;
function updateChecksum(line) {
let res = line.split("\t");
// each frame starts with a linefeed and carriage return
// they are swollowed by readline('\r\n')
checksum += '\n'.charCodeAt(0);
checksum += '\r'.charCodeAt(0);
let c;
// count all characters as uint8_t from first
// value pair name until and including Checksum - tab- and the
// uint8_t checksum value. Also include all the tabs between
// name and value pair and the \n and \r after each line.
// This checksum must be 0 (% 256)
let lineLength = line.length;
if (res[0] === "Checksum")
{
lineLength = res[0].length + 2; // plus \t and the checksum value
}
else {
}
for (c = 0; c < lineLength; ++c)
{
checksum += line.charCodeAt(c);
}
return checksum;
}
function updateValuesAndValueListeners(doLog) {
for (const [key, obj] of Object.entries(map)) {
if (obj.newValue != null && obj.value !== obj.newValue)
{
let oldValue = obj.value;
obj.value = obj.newValue; // accept new values
// send event to listeners with values
// on() means if update applied,
if (obj.on !== null) // && Math.abs(obj.value - obj.newValue) >= obj.precision)
{
obj.on(obj.newValue, oldValue, obj.precision);
}
if (doLog) logger.debug(obj.shortDescr
+ " = " + oldValue
+ " updated with " + obj.newValue);
}
obj.newValue = null;
}
}
function discardValues() {
// FIXME: should only discard values that are coming from regular updates
for (const [key, obj] of Object.entries(map)) {
obj.newValue = null; // dump new values
}
}
function isCommandValid(cmd) {
// colon : was swallowed by split(':')
var rcs = append_checksum(":" + cmd.substring(0, cmd.length-2));
var expectedCS = rcs.substring(rcs.length-2, rcs.length);
var actualCS = cmd.substring(cmd.length-2, cmd.length);
if (actualCS !== expectedCS)
{
logger.error("ERROR: command checksum: " + actualCS
+ " - expected: " + expectedCS);
return false;
}
return true;
}
function processCommand(cmd) {
cmd = cmd.split('\n')[0];
logger.trace("processCommand: response received " + cmd);
if (!isCommandValid(cmd)) return;
var cmdRegisterPrefix = cmd.substring(0, 5);
if (cmdRegisterPrefix in responseMap && responseMap[cmdRegisterPrefix] !== undefined)
{
clearTimeout(responseMap[cmdRegisterPrefix].timerId)
logger.debug(cmdRegisterPrefix + " in responseMap ==> clear timeout");
// the standard parser splits line by '\r\n'
// while a command response ends with '\n' only.
// I.e. there may be a chunk of stuff after the \n
// that needs to be split away.
responseMap[cmdRegisterPrefix].func(cmd);
delete responseMap[cmdRegisterPrefix];
}
else if (cmdRegisterPrefix == "40000") // reply after restart
{
logger.debug("restart successful");
}
else if (cmdRegisterPrefix == "AAAA")
{
logger.error("Framing error");
}
else
{
logger.warn("unwarrented command " + cmdRegisterPrefix + " received - ignored");
}
// TODO: check regularly for left overs in responseMap and cmdMessageQ
}
function parse_serial(line) {
let res = line.split("\t");
if (!res[0] || res[0] === "") return; // empty string
let oldChecksum = checksum;
checksum = updateChecksum(line);
if (res[0] === "Checksum")
{
checksum = checksum % 256;
if (checksum === 0) // valid checksum for periodic frames
{
updateValuesAndValueListeners(false);
}
else // checksum invalid
{
discardValues();
// (oldChecksum + 828 + expectedCS) % 256 === 0!
// ==> (oldChecksum % 256) + (828 % 256) + expectedCS === 0
// ==> expectedCS = -(oldChecksum % 256) - 60
// = 256 - (oldChecksum % 256) + 196
let expectedCS = (256 - (oldChecksum % 256) + 196) % 256; // Checksum+\t
if (expectedCS < 0) { expectedCS = expectedCS + 256; }
logger.error("ERROR: data set checksum: "
+ res[1].charCodeAt(0).toString(16) + ' ('
+ res[1].charCodeAt(0)
+ ") - expected: " + expectedCS.toString(16)
+ ' (' + expectedCS + ')');
}
checksum = 0; // checksum field read => reset checksum
// frame always finishes before another frame
// or before a command response arrives.
// Check for command response now:
if (res[1].length === 0) return;
// checksum value is followed by optional garbage and
// optional command response all in res[1].
// First char of res[1] contains checksum value so start from 1:
var cmdSplit = res[1].substring(1, res[1].length).split(':');
// none, one or several command responses can follow a frame.
// Command responses always start with : and end with \n.
var cmdIndex;
for (cmdIndex = 1; cmdIndex < cmdSplit.length; ++cmdIndex) {
processCommand(cmdSplit[cmdIndex]);
}
}
else
{
if (res[0] === undefined) return;
if (res[0] in map && map[res[0]] !== undefined) map[res[0]].newValue = res[1];
else logger.warn("parse_serial: " + res[0] + " is not registered and has value " + res[1]);
}
};
var port;
exports.open = function(ve_port) {
port = new serialport(ve_port, {
baudrate: 19200,
parser: serialport.parsers.readline('\r\n', 'binary')});
port.on('data', function(line) {
isSerialOperational = true;
if (isRecording)
{
record_file.write(line + '\r\n');
}
parse_serial(line);
});
}
// \pre cmd must be a command without the checksum i.e. start with : and
// be hexadecimal
function append_checksum(cmd) {
var command = "0" + cmd.substring(1, cmd.length);
const byteInHex = command.split('').map(c => parseInt(c, 16));
var checksum = byteInHex.reduce((total, hex, index) =>
(index % 2 === 0 ? total + hex * 16 : total + hex), 0);
checksum = (0x55 - checksum) % 256;
if (checksum < 0) checksum += 256;
return cmd + ("0" + checksum.toString(16)).slice(-2).toUpperCase();
}
var responseMap = {};
function responseTimeoutHandler(cmdFrame, cmdRegisterPrefix) {
logger.error("ERROR: timeout - no response to "
+ cmdFrame + " within "
+ cmdResponseTimeoutMS + "ms");
if (cmdRegisterPrefix in responseMap && responseMap[cmdRegisterPrefix] !== undefined)
{
if (responseMap[cmdRegisterPrefix].doRetry <= 0)
{
delete responseMap[cmdRegisterPrefix];
//reject(new Error('timeout - no response received within 30 secs'));
cmdMessageQ.shift(); // finished work on this message - dump
logger.debug("Cmd Q: " + cmdMessageQ.length);
exports.restart(); // FIXME: after restart the following response received: 4000051
}
else if (cmdMessageQ.length > 0)
{
//exports.restart();
logger.debug("Repeating command ("
+ responseMap[cmdRegisterPrefix].doRetry + ") "
+ cmdMessageQ[0].substring(0, cmdMessageQ[0].length-1));
}
}
if (cmdMessageQ.length > 0)
{
const nextCmd = cmdMessageQ[0];
logger.debug("Send next command in Q: " + nextCmd.substring(0, nextCmd.length-1));
runMessageQ(false);
}
}
function getResponse(cmdFrame) {
return new Promise(function(resolve, reject)
{
// cmdRegisterPrefix is without leading : and ending \n
let cmdRegisterPrefix = cmdFrame.substring(1, 6);
logger.debug("Adding " + cmdRegisterPrefix + " to reponseMap");
var tid = setTimeout(responseTimeoutHandler, cmdResponseTimeoutMS, cmdFrame, cmdRegisterPrefix);
logger.debug("Timeout set to " + cmdResponseTimeoutMS
+ "ms for " + cmdRegisterPrefix);
let newRetries = cmdMaxRetries;
if (cmdRegisterPrefix in responseMap && responseMap[cmdRegisterPrefix] != undefined)
newRetries = responseMap[cmdRegisterPrefix].doRetry-1;
responseMap[cmdRegisterPrefix] = {
func: resolve,
timerId: tid,
doRetry: newRetries,
};
port.write(cmdFrame);
});
}
var cmdMessageQ = [];
function get(address, priority) {
logger.debug("get address: " + address);
const message = createMessage('7', address, '');
sendMsg(message, priority);
}
// \param value must be a string of 4 or 8 characters in hexadecimal
// with byte pairs swapped, i.e. 1B83 => 831B
function set(address, value, priority) {
logger.debug("set address: " + address);
const message = createMessage('8', address, value);
sendMsg(message, priority);
}
// \param address is a string and has the format 0x???? (uint16 with leading zeros if needed)
// \param value as string, little endianed and filled with 0 from the left
function createMessage(cmd, address, value) {
logger.debug("===================================");
logger.debug("cmd: " + cmd);
logger.debug("address: " + address);
logger.debug("value: " + value);
// remove 0x prefix
const flag = '00'; // flag always 00 for outgoing get and set
leAddress = address.substring(4, 6) + address.substring(2, 4) // address in little endian
//FIXME: value needs to be endian "swapped"
let command = ':' + cmd + leAddress + flag + value;
command = append_checksum(command) + '\n';
return command;
}
// \param message is a command starting with : and ending with the checksum
// \param priority is 0 or 1, 1 is prefered execution
function Q_push_back(message, priority) {
const l = cmdMessageQ.length;
if (priority !== undefined && priority === 1)
{
if (l > 0)
{
logger.debug("Prioritizing " + message);
// first is currently executed --> leave at position 0
let first = cmdMessageQ.shift();
// insert message at position 1
cmdMessageQ.unshift(first, message);
// it is possible that cmdMessageQ and message are the same command
// (with same or different parameters). However we cannot compress
// because we do not know at which execution state cmdMessageQ[0] is.
logger.debug("Cmd Q: " + cmdMessageQ.length);
}
else // l == 0
{
cmdMessageQ.push(message);
logger.debug("Cmd Q: " + cmdMessageQ.length);
}
return;
}
// check: current command is same as previous but with possibly different
// parameter ==> if cmdCompression, execute only current command and
// skip previous
if (cmdCompression
&& (l > 0) && (cmdMessageQ[l-1].substring(0, 6) == message.substring(0, 6)))
{ // replace last command
cmdMessageQ[l-1] = message;
logger.debug("Last cmd in Q replaced: " + cmdMessageQ.length);
}
else
{
// never execute the very same command twice as it is like bouncing
if (l === 0 || cmdMessageQ[l-1] != message)
{
cmdMessageQ.push(message);
logger.debug("Cmd Q: " + cmdMessageQ.length);
}
else
{
logger.debug("Repeated message ignored: " + message);
}
}
}
// \param response without leading : and trailing \n
function messageState(response) {
const state = parseInt(response.substring(5, 7));
switch (state) {
default:
case 0: // OK
break;
case 1: // Unknown ID
logger.error("Specific Id " + id + "does not exist");
break;
case 2: // Not supported
logger.error("Attempting to write to a read only value at " + id);
break;
case 4: // Parameter Error
logger.error("The new value " + value + " of " + id + " is out of range or inconsistent");
break;
}
return state;
}
var sendMessageDeferTimer = null;
// \brief Creates the endianess needed by the device
// \detail The bytes of the incoming hex string's words are
// filled with leading 0 and swapped
// e.g. 0xBCD becomes 0xCD0B
// \param hexStr the number as string in hexadecimal format (no leading 0x)
// \param lengthInBytes of the number
// \pre hexStr's length must be even!
function endianSwap(hexStr, lengthInBytes)
{
while (hexStr.length < 2 * lengthInBytes)
{
hexStr = '0' + hexStr;
}
if (hexStr.length >= 4)
hexStr = hexStr.substring(2, 4) + hexStr.substring(0, 2);
if (hexStr.length >= 8)
hexStr = hexStr.substring(6, 8) + hexStr.substring(4, 6);
if (hexStr.length >= 12)
logger.warn("endianSwap() not implemented for 12 bytes");
return hexStr.toUpperCase();
}
function responseHandler(response) {
logger.debug("responseHandler(" + response + ")");
// response contains the message without leading : and trailing \n
if (response == "AAAA")
{
logger.error("framing error");
return -1;
}
if (response.substring(0, 5) !== cmdMessageQ[0].substring(1, 6))
{
logger.error("response " + response
+ " does not map queued command: "
+ cmdMessageQ[0]);
return -2;
}
// check flag
let flag = "00";
if (response !== undefined) {
if (messageState(response) !== 0) return -1;
let strValue = response.substring(7, response.length-2);
let valuesNumberOfBytes = strValue.length/2;
strValue = endianSwap(strValue, valuesNumberOfBytes);
logger.debug("endianed hex value: " + strValue);
const address = "0x" + response.substring(3, 5) + response.substring(1, 3);
if (address in addressCache)
{
addressCache[address].newValue = addressCache[address].fromHexStr(strValue);
logger.debug("response for "
+ addressCache[address].shortDescr + ' (old: ' +
+ addressCache[address].value + ") - new value: " +
+ addressCache[address].newValue);
//copied from updateValuesAndValueListeners(true); since it cannot be used !!! causes issues
if (addressCache[address].value !== addressCache[address].newValue)
{
let oldValue = addressCache[address].value;
addressCache[address].value = addressCache[address].newValue; // accept new values
// send event to listeners with values
// on() means if update applied,
if (addressCache[address].on !== null) // && Math.abs(obj.value - obj.newValue) >= obj.precision)
{
addressCache[address].on(addressCache[address].newValue, oldValue, addressCache[address].precision);
}
addressCache[address].newValue = null;
}
}
else {
logger.warn(address + " is not in addressCache");
addressCache[address] = new Object();
addressCache[address].newValue = conv.hexToUint(strValue);
}
}
else logger.warn("Response is undefined");
//TODO: if response does not match expected response sendMsg(message, priority) again.
const lastCmd = cmdMessageQ[0];
// take last char off which is \n
logger.debug(lastCmd.substring(0, lastCmd.length-1) + "\\n processed - dequeing");
cmdMessageQ.shift(); // finished work on this message - dump
logger.debug("Cmd Q: " + cmdMessageQ.length);
runMessageQ(false);
}
function runMessageQ(isTimeout)
{
if (cmdMessageQ.length > 0) {
if (isSerialOperational)
{
if (sendMessageDeferTimer != null) {
clearTimeout(sendMessageDeferTimer);
//sendMessageDeferTimer = null;
}
const nextCmd = cmdMessageQ[0];
// TODO:
// const address = "0x" + nextCmd.substring(4, 6) + nextCmd.substring(2, 4);
// const value = nextCmd.substring(6, nextCmd.length-2);
// // TODO: endianian value
// if (addressCache[address].value ==
// addressCache[address].fromHexStr(strValue)) // FIXME: does convert do the job?
// {
// logger.debug("Cached value same as command value - ignoring");
// return;
// }
logger.debug("Sending " + nextCmd.substring(0, nextCmd.length-1));
getResponse(nextCmd).then(responseHandler)
.catch(function(reject) {
logger.warn("Reject: " + reject.message);
});
}
else
{
if (! isTimeout && cmdMessageQ.length === 1) {
logger.debug("==> sendMessage deferred by 1 second");
clearTimeout(sendMessageDeferTimer);
sendMessageDeferTimer = setTimeout(runMessageQ, 1000, true ) ;
} else {
// if timeout happend the timer expired and we start a new one
logger.debug("==> deferred another time by 1 second");
clearTimeout(sendMessageDeferTimer);
sendMessageDeferTimer = setTimeout(runMessageQ, 1000, true ) ;
}
// else a new message came in but port not yet operational
// ==> don't start another timer
}
}
else
logger.debug("MessageQ empty");
}
function sendMsg(message, priority) {
message = message.toUpperCase();
logger.trace("sendMsg: " + message.substring(0, message.length-1) + "\\n");
if (cmdMessageQ.length === 0)
{
// TODO: cmdMessageQ empty ==> clear responseMap
Q_push_back(message, priority);
runMessageQ(false);
}
else
{
Q_push_back(message, priority);
// FIXME: message times out and is removed from responseMap but then
// may still arrive (not being found in repsonseMap -> TODO: timeoutmap???
// FIXME: 2 or more same commands fired right after each other with
// cmdCompression switched off ==> only one responseMap entry
// while it should be 2 or more, i.e. 1 response deletes
// this entry and later incoming don't work
let firstCmdInQ = cmdMessageQ[0];
setTimeout(function()
{
// if after the max timeout firstCmdInQ is still the first in Q
if (firstCmdInQ === cmdMessageQ[0])
{
// FIXME: firstCmdInQ could match a later cmd in Q which is no
// in pos 0
cmdMessageQ.shift();
runMessageQ(false); // FIXME: this runs occassionally two msg in parallel
}
}, 2 * (cmdMaxRetries + 1) * cmdResponseTimeoutMS);
}
}
setTimeout( function()
{
//get("0xED8D"); // V - ok
// get('0xED7D'); // 'VS'; response: -1 or FFFF
//get('0xED8F'); // 'I' - ok
// get('0xED8C'); // 'I'; existiert nicht - Error flag 01 unknown ID
//get('0xED8E'); // 'P' - ok
//get('0xEEFF'); // 'CE' -ok
// todo : continue from here:
// get('0x0FFF'); // 'SOC'
//get('0x0FFE'); // 'TTG'; ok
// get('0xEDEC'); // 'T'; response 65535 or FFFF, bmvdata = null
//get('0x0382'); // 'VM' - ok
//get('0x0383'); // 'DM'; diff by factor 10
//get('0xEEB6'] = 'SOC' - ok