Skip to content

Commit e50b005

Browse files
kfennerfh1ch
authored andcommitted
feat(encoding): add full read/write support for ISO 8859-1 charset
Transcoding from JavaScript internal 16 bit representation to ISO 8859-1 is done using iconv-lite library. Alternatively, NodeJS >=7.0.1 supports transcoding via Buffer out of the box. BREAKING CHANGE: rename enum BacnetCharacterStringEncodings value CHARACTER_ISO8859 to CHARACTER_ISO8859_1 which conforms with the BACnet specification.
1 parent 0ce7538 commit e50b005

6 files changed

Lines changed: 115 additions & 72 deletions

File tree

lib/bacnet-asn1.js

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
var baEnum = require('./bacnet-enum');
1+
var iconv = require('iconv-lite');
2+
var baEnum = require('./bacnet-enum');
23

34
var BACNET_MAX_OBJECT = module.exports.BACNET_MAX_OBJECT = 0x3FF;
45
var BACNET_INSTANCE_BITS = module.exports.BACNET_INSTANCE_BITS = 22;
@@ -9,6 +10,26 @@ var BACNET_NO_PRIORITY = module.exports.BACNET_NO_PRIORITY = 0;
910
var BACNET_MIN_PRIORITY = module.exports.BACNET_MIN_PRIORITY = 1;
1011
var BACNET_MAX_PRIORITY = module.exports.BACNET_MAX_PRIORITY = 16;
1112

13+
var getEncodingType = function(encoding, decodingBuffer, decodingOffset) {
14+
switch (encoding) {
15+
case baEnum.BacnetCharacterStringEncodings.CHARACTER_UTF8:
16+
return 'utf8';
17+
case baEnum.BacnetCharacterStringEncodings.CHARACTER_UCS2:
18+
if ((decodingBuffer[decodingOffset] === 0xFF) && (decodingBuffer[decodingOffset + 1] === 0xFE)) {
19+
return 'ucs2';
20+
}
21+
return; //UCS-2BE
22+
case baEnum.BacnetCharacterStringEncodings.CHARACTER_ISO8859_1:
23+
return 'latin1';
24+
case baEnum.BacnetCharacterStringEncodings.CHARACTER_UCS4:
25+
case baEnum.BacnetCharacterStringEncodings.CHARACTER_MS_DBCS:
26+
case baEnum.BacnetCharacterStringEncodings.CHARACTER_JISX_0208:
27+
return;
28+
default:
29+
return 'utf8';
30+
}
31+
};
32+
1233
var encodeUnsigned = function(buffer, value, length) {
1334
buffer.buffer.writeUIntBE(value, buffer.offset, length, true);
1435
buffer.offset += length;
@@ -460,7 +481,7 @@ var bacappEncodeApplicationData = module.exports.bacappEncodeApplicationData = f
460481
encodeApplicationOctetString(buffer, value.value, 0, value.value.length);
461482
break;
462483
case baEnum.BacnetApplicationTags.BACNET_APPLICATION_TAG_CHARACTER_STRING:
463-
encodeApplicationCharacterString(buffer, value.value);
484+
encodeApplicationCharacterString(buffer, value.value, value.encoding);
464485
break;
465486
case baEnum.BacnetApplicationTags.BACNET_APPLICATION_TAG_BIT_STRING:
466487
encodeApplicationBitstring(buffer, value.value);
@@ -699,11 +720,14 @@ var bacappDecodeApplicationData = module.exports.bacappDecodeApplicationData = f
699720
var len = result.len;
700721
result = bacappDecodeData(buffer, offset + len, maxOffset, result.tagNumber, result.value);
701722
if (!result) return;
702-
return {
723+
var resObj = {
703724
len: len + result.len,
704725
type: result.type,
705726
value: result.value
706727
};
728+
// HACK: Drop string specific handling ASAP
729+
if (result.encoding !== undefined) resObj.encoding = result.encoding;
730+
return resObj;
707731
}
708732
} else {
709733
return bacappDecodeContextApplicationData(buffer, offset, maxOffset, objectType, propertyId);
@@ -779,7 +803,13 @@ var decodeReadAccessResult = module.exports.decodeReadAccessResult = function(bu
779803
var localResult = bacappDecodeApplicationData(buffer, offset + len, apduLen + offset - 1, value.objectIdentifier.type, newEntry.propertyIdentifier);
780804
if (!localResult) return;
781805
len += localResult.len;
782-
localValues.push({value: localResult.value, type: localResult.type});
806+
var resObj = {
807+
value: localResult.value,
808+
type: localResult.type
809+
};
810+
// HACK: Drop string specific handling ASAP
811+
if (localResult.encoding !== undefined) resObj.encoding = localResult.encoding;
812+
localValues.push(resObj);
783813
}
784814
if (!decodeIsClosingTagNumber(buffer, offset + len, 4)) return;
785815
if ((localValues.count === 2) && (localValues[0].tag === baEnum.BacnetApplicationTags.BACNET_APPLICATION_TAG_DATE) && (localValues[1].tag === baEnum.BacnetApplicationTags.BACNET_APPLICATION_TAG_TIME)) {
@@ -888,32 +918,12 @@ var decodeContextOctetString = function(buffer, offset, maxLength, tagNumber, oc
888918
};
889919

890920
var multiCharsetCharacterstringDecode = function(buffer, offset, maxLength, encoding, length) {
891-
var nodeEncoding;
892-
switch (encoding) {
893-
case baEnum.BacnetCharacterStringEncodings.CHARACTER_UTF8:
894-
nodeEncoding = 'utf8';
895-
break;
896-
case baEnum.BacnetCharacterStringEncodings.CHARACTER_UCS2:
897-
if ((buffer[offset] === 0xFF) && (buffer[offset + 1] === 0xFE)) {
898-
nodeEncoding = 'ucs2';
899-
} else {
900-
return;
901-
}
902-
break;
903-
case baEnum.BacnetCharacterStringEncodings.CHARACTER_ISO8859:
904-
nodeEncoding = 'latin1';
905-
break;
906-
case baEnum.BacnetCharacterStringEncodings.CHARACTER_UCS4:
907-
case baEnum.BacnetCharacterStringEncodings.CHARACTER_MS_DBCS:
908-
case baEnum.BacnetCharacterStringEncodings.CHARACTER_JISX_0208:
909-
return;
910-
default:
911-
nodeEncoding = 'latin1';
912-
break;
913-
}
921+
var stringBuf = Buffer.alloc(length);
922+
buffer.copy(stringBuf, 0, offset, offset + length);
914923
return {
915-
value: buffer.toString(nodeEncoding, offset, offset + length),
916-
len: length + 1
924+
value: iconv.decode(stringBuf, getEncodingType(encoding, buffer, offset)),
925+
len: length + 1,
926+
encoding: encoding
917927
};
918928
};
919929

@@ -1092,6 +1102,7 @@ var bacappDecodeData = function(buffer, offset, maxLength, tagDataType, lenValue
10921102
result = decodeCharacterString(buffer, offset, maxLength, lenValueType);
10931103
value.len += result.len;
10941104
value.value = result.value;
1105+
value.encoding = result.encoding;
10951106
break;
10961107
case baEnum.BacnetApplicationTags.BACNET_APPLICATION_TAG_BIT_STRING:
10971108
result = decodeBitstring(buffer, offset, lenValueType);
@@ -1571,10 +1582,13 @@ var bacappDecodeContextApplicationData = function(buffer, offset, maxOffset, obj
15711582
var bacappResult = bacappDecodeData(buffer, offset + len + subResult.len, maxOffset, subResult.tagNumber, subResult.value);
15721583
if (!bacappResult) return;
15731584
if (bacappResult.len === subResult.value) {
1574-
list.push({
1585+
var resObj = {
15751586
value: bacappResult.value,
15761587
type: bacappResult.type
1577-
});
1588+
};
1589+
// HACK: Drop string specific handling ASAP
1590+
if (bacappResult.encoding !== undefined) resObj.encoding = bacappResult.encoding;
1591+
list.push(resObj);
15781592
len += subResult.len + subResult.value;
15791593
} else {
15801594
list.push({
@@ -1642,7 +1656,8 @@ var decodeContextCharacterString = module.exports.decodeContextCharacterString =
16421656
len += result.value;
16431657
return {
16441658
len: len,
1645-
value: decodedValue.value
1659+
value: decodedValue.value,
1660+
encoding: decodedValue.encoding
16461661
};
16471662
};
16481663

@@ -1709,28 +1724,30 @@ var bacappDecodeContextData = function(buffer, offset, maxApduLen, propertyTag)
17091724
};
17101725
};
17111726

1712-
var encodeBacnetCharacterString = function(buffer, value) {
1713-
buffer.buffer[buffer.offset++] = baEnum.BacnetCharacterStringEncodings.CHARACTER_UTF8;
1714-
buffer.offset += buffer.buffer.write(value, buffer.offset, undefined, 'utf8');
1727+
var encodeBacnetCharacterString = function(buffer, value, encoding) {
1728+
encoding = encoding || baEnum.BacnetCharacterStringEncodings.CHARACTER_UTF8;
1729+
buffer.buffer[buffer.offset++] = encoding;
1730+
var bufEncoded = iconv.encode(value, getEncodingType(encoding));
1731+
buffer.offset += bufEncoded.copy(buffer.buffer, buffer.offset);
17151732
};
17161733

1717-
var encodeApplicationCharacterString = function(buffer, value) {
1734+
var encodeApplicationCharacterString = function(buffer, value, encoding) {
17181735
var tmp = {
17191736
buffer: Buffer.alloc(1472),
17201737
offset: 0
17211738
};
1722-
encodeBacnetCharacterString(tmp, value);
1739+
encodeBacnetCharacterString(tmp, value, encoding);
17231740
encodeTag(buffer, baEnum.BacnetApplicationTags.BACNET_APPLICATION_TAG_CHARACTER_STRING, false, tmp.offset);
17241741
tmp.buffer.copy(buffer.buffer, buffer.offset, 0, tmp.offset);
17251742
buffer.offset += tmp.offset;
17261743
};
17271744

1728-
var encodeContextCharacterString = module.exports.encodeContextCharacterString = function(buffer, tagNumber, value) {
1745+
var encodeContextCharacterString = module.exports.encodeContextCharacterString = function(buffer, tagNumber, value, encoding) {
17291746
var tmp = {
17301747
buffer: Buffer.alloc(1472),
17311748
offset: 0
17321749
};
1733-
encodeBacnetCharacterString(tmp, value);
1750+
encodeBacnetCharacterString(tmp, value, encoding);
17341751
encodeTag(buffer, tagNumber, true, tmp.offset);
17351752
tmp.buffer.copy(buffer.buffer, buffer.offset, 0, tmp.offset);
17361753
buffer.offset += tmp.offset;

lib/bacnet-enum.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1237,7 +1237,7 @@ module.exports.BacnetCharacterStringEncodings = {
12371237
CHARACTER_JISX_0208: 2,
12381238
CHARACTER_UCS4: 3,
12391239
CHARACTER_UCS2: 4,
1240-
CHARACTER_ISO8859: 5
1240+
CHARACTER_ISO8859_1: 5
12411241
};
12421242

12431243
module.exports.BacnetReadRangeRequestTypes = {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
},
3333
"homepage": "https://github.com/fh1ch/node-bacstack#readme",
3434
"dependencies": {
35-
"debug": "^2.6.0"
35+
"debug": "^2.6.0",
36+
"iconv-lite": "^0.4.18"
3637
},
3738
"devDependencies": {
3839
"chai": "^3.5.0",

test/integration/read-property-multiple.spec.js

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ describe('bacstack - readPropertyMultiple integration', function() {
2525
client.readPropertyMultiple('127.0.0.1', requestArray, function(err, response) {
2626
var object = utils.propertyFormater(response.values[0].values);
2727
expect(response.values[0].objectIdentifier).to.deep.equal({type: 29, instance: 31});
28-
expect(object[28]).to.deep.equal([{value: 'Collection of lighting presence detector', type: 7}]);
28+
expect(object[28]).to.deep.equal([{value: 'Collection of lighting presence detector', type: 7, encoding: 0}]);
2929
expect(object[75]).to.deep.equal([{value: {type: 29, instance: 31}, type: 12}]);
30-
expect(object[77]).to.deep.equal([{value: 'Zählerweg\'Floor 4\'RSegTV\'Lgt\'PscOp\'LgtPscDetCol', type: 7}]);
30+
expect(object[77]).to.deep.equal([{value: 'Zählerweg\'Floor 4\'RSegTV\'Lgt\'PscOp\'LgtPscDetCol', type: 7, encoding: 0}]);
3131
expect(object[79]).to.deep.equal([{value: 29, type: 9}]);
32-
expect(object[168]).to.deep.equal([{value: '7-BA-RDS1-041-SBCv13.20', type: 7}]);
33-
expect(object[207]).to.deep.equal([{value: '', type: 7}]);
32+
expect(object[168]).to.deep.equal([{value: '7-BA-RDS1-041-SBCv13.20', type: 7, encoding: 0}]);
33+
expect(object[207]).to.deep.equal([{value: '', type: 7, encoding: 0}]);
3434
expect(object[208]).to.deep.equal([{value: 8, type: 9}]);
3535
expect(object[210]).to.deep.equal([
36-
{value: 'LgtPscDetRs', type: 7},
37-
{value: 'PscDet(*)', type: 7}
36+
{value: 'LgtPscDetRs', type: 7, encoding: 0},
37+
{value: 'PscDet(*)', type: 7, encoding: 0}
3838
]);
3939
expect(object[211]).to.deep.equal([
4040
{value: {value: {type: 5, instance: 0}, type: 12}, type: 118},
@@ -65,39 +65,39 @@ describe('bacstack - readPropertyMultiple integration', function() {
6565
client.readPropertyMultiple('127.0.0.1', requestArray, function(err, response) {
6666
expect(response.values[0].objectIdentifier).to.deep.equal({type: 19, instance: 10});
6767
var object = utils.propertyFormater(response.values[0].values);
68-
expect(object[28]).to.deep.equal([{value: 'Sensor', type: 7}]);
68+
expect(object[28]).to.deep.equal([{value: 'Sensor', type: 7, encoding: 0}]);
6969
expect(object[36]).to.deep.equal([{value: 0, type: 9}]);
7070
expect(object[74]).to.deep.equal([{value: 8, type: 2}]);
7171
expect(object[75]).to.deep.equal([{value: {type: 19, instance: 10}, type: 12}]);
72-
expect(object[77]).to.deep.equal([{value: 'Zählerweg\'Floor 4\'RSegTV\'SenDev\'Sen', type: 7}]);
72+
expect(object[77]).to.deep.equal([{value: 'Zählerweg\'Floor 4\'RSegTV\'SenDev\'Sen', type: 7, encoding: 0}]);
7373
expect(object[79]).to.deep.equal([{value: 19, type: 9}]);
7474
expect(object[81]).to.deep.equal([{value: false, type: 1}]);
7575
expect(object[85]).to.deep.equal([{value: 1, type: 2}]);
7676
expect(object[103]).to.deep.equal([{value: 0, type: 9}]);
7777
expect(object[110]).to.deep.equal([
78-
{value: 'Operational', type: 7},
79-
{value: 'Device stopped', type: 7},
80-
{value: 'Device not assigned', type: 7},
81-
{value: 'Device missing', type: 7},
82-
{value: 'Configuring device', type: 7},
83-
{value: 'Unused', type: 7},
84-
{value: 'Missing or wrong configuration', type: 7},
85-
{value: 'Searching', type: 7},
78+
{value: 'Operational', type: 7, encoding: 0},
79+
{value: 'Device stopped', type: 7, encoding: 0},
80+
{value: 'Device not assigned', type: 7, encoding: 0},
81+
{value: 'Device missing', type: 7, encoding: 0},
82+
{value: 'Configuring device', type: 7, encoding: 0},
83+
{value: 'Unused', type: 7, encoding: 0},
84+
{value: 'Missing or wrong configuration', type: 7, encoding: 0},
85+
{value: 'Searching', type: 7, encoding: 0},
8686
]);
8787
expect(object[111]).to.deep.equal([{value: {value: [0], bitsUsed: 4}, type: 8}]);
88-
expect(object[168]).to.deep.equal([{value: '7-BA-RDS1-024-SBCv13.20', type: 7}]);
88+
expect(object[168]).to.deep.equal([{value: '7-BA-RDS1-024-SBCv13.20', type: 7, encoding: 0}]);
8989
expect(object[4930]).to.deep.equal([{value: 0, type: 9}]);
9090
expect(object[4941]).to.deep.equal([{value: 5, type: 9}]);
9191
expect(object[5000]).to.deep.equal([{value: 0, type: 9}]);
9292
expect(object[5001]).to.deep.equal([{value: 0, type: 9}]);
93-
expect(object[5039]).to.deep.equal([{value: '0.2.249', type: 7}]);
94-
expect(object[5087]).to.deep.equal([{value: '5WG1258-2DB12', type: 7}]);
95-
expect(object[5092]).to.deep.equal([{value: 'PL-1:DL=1;', type: 7}]);
93+
expect(object[5039]).to.deep.equal([{value: '0.2.249', type: 7, encoding: 0}]);
94+
expect(object[5087]).to.deep.equal([{value: '5WG1258-2DB12', type: 7, encoding: 0}]);
95+
expect(object[5092]).to.deep.equal([{value: 'PL-1:DL=1;', type: 7, encoding: 0}]);
9696
expect(object[5094]).to.deep.equal([{value: 3, type: 2}]);
97-
expect(object[5100]).to.deep.equal([{value: '00010043eddc', type: 7}]);
98-
expect(object[5101]).to.deep.equal([{value: 'PL:DDT=0586.0001.00.01.00;FW=0.1.13;MODE=PL;', type: 7}]);
97+
expect(object[5100]).to.deep.equal([{value: '00010043eddc', type: 7, encoding: 0}]);
98+
expect(object[5101]).to.deep.equal([{value: 'PL:DDT=0586.0001.00.01.00;FW=0.1.13;MODE=PL;', type: 7, encoding: 0}]);
9999
expect(object[5102]).to.deep.equal([{value: 24, type: 9}]);
100-
expect(object[5103]).to.deep.equal([{value: '', type: 7}]);
100+
expect(object[5103]).to.deep.equal([{value: '', type: 7, encoding: 0}]);
101101
expect(object[5104]).to.deep.equal([{value: 0, type: 9}]);
102102
expect(object[5107]).to.deep.equal([{value: 1, type: 2}]);
103103
client.close();

test/integration/who-is.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('bacstack - whoIs integration', function() {
66
var client = new utils.bacnetClient({adpuTimeout: 200});
77
client.on('iAm', function(address, deviceId, maxAdpu, segmentation, vendorId) {
88
client.close();
9-
next(new Erro('Unallowed Callback'));
9+
next(new Error('Unallowed Callback'));
1010
});
1111
setTimeout(function() {
1212
client.close();

test/unit/bacnet-services.spec.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
var expect = require('chai').expect;
22
var utils = require('./utils');
33
var baServices = require('../../lib/bacnet-services');
4+
var baEnum = require('../../lib/bacnet-enum');
45

56
describe('bacstack - Services layer', function() {
67
describe('Iam', function() {
@@ -241,7 +242,7 @@ describe('bacstack - Services layer', function() {
241242
var buffer = utils.getBuffer();
242243
baServices.encodeReadPropertyAcknowledge(buffer, {type: 8, instance: 40000}, 81, 0xFFFFFFFF, [
243244
{tag: 7, value: ''},
244-
{tag: 7, value: 'Test1234$'}
245+
{tag: 7, value: 'Test1234$äöü'}
245246
]);
246247
var result = baServices.decodeReadPropertyAcknowledge(buffer.buffer, 0, buffer.offset);
247248
delete result.len;
@@ -255,8 +256,32 @@ describe('bacstack - Services layer', function() {
255256
propertyIdentifier: 81
256257
},
257258
valueList: [
258-
{type: 7, value: '', len: 2},
259-
{type: 7, value: 'Test1234$', len: 12}
259+
{type: 7, value: '', encoding: 0, len: 2},
260+
{type: 7, value: 'Test1234$äöü', encoding: 0, len: 18}
261+
]
262+
});
263+
});
264+
265+
it('should successfully encode and decode a character-string value with ISO-8859-1 encoding', function() {
266+
var buffer = utils.getBuffer();
267+
baServices.encodeReadPropertyAcknowledge(buffer, {type: 8, instance: 40000}, 81, 0xFFFFFFFF, [
268+
{tag: 7, value: '', encoding: baEnum.BacnetCharacterStringEncodings.CHARACTER_ISO8859_1},
269+
{tag: 7, value: 'Test1234$äöü', encoding: baEnum.BacnetCharacterStringEncodings.CHARACTER_ISO8859_1}
270+
]);
271+
var result = baServices.decodeReadPropertyAcknowledge(buffer.buffer, 0, buffer.offset);
272+
delete result.len;
273+
expect(result).to.deep.equal({
274+
objectId: {
275+
type: 8,
276+
instance: 40000
277+
},
278+
property: {
279+
propertyArrayIndex: 0xFFFFFFFF,
280+
propertyIdentifier: 81
281+
},
282+
valueList: [
283+
{type: 7, value: '', encoding: baEnum.BacnetCharacterStringEncodings.CHARACTER_ISO8859_1, len: 2},
284+
{type: 7, value: 'Test1234$äöü', encoding: baEnum.BacnetCharacterStringEncodings.CHARACTER_ISO8859_1, len: 15}
260285
]
261286
});
262287
});
@@ -525,7 +550,7 @@ describe('bacstack - Services layer', function() {
525550
{type: 3, value: -1000000000},
526551
{type: 4, value: 0},
527552
{type: 5, value: 100.121212},
528-
{type: 7, value: 'Test1234$'},
553+
{type: 7, value: 'Test1234$', encoding: 0},
529554
{type: 9, value: 4},
530555
{type: 10, value: date},
531556
{type: 11, value: time},
@@ -781,7 +806,7 @@ describe('bacstack - Services layer', function() {
781806
{type: 5, value: 100.121212, len: 10},
782807
// FIXME: correct octet-string implementation
783808
// {type: 6, value: [1, 2, 100, 200]},
784-
{type: 7, value: 'Test1234$', len: 12},
809+
{type: 7, value: 'Test1234$', encoding: 0, len: 12},
785810
// FIXME: correct bit-string implementation
786811
// {type: 8, value: {bitsUsed: 0, value: []}},
787812
// {type: 8, value: {bitsUsed: 24, value: [0xAA, 0xAA, 0xAA]}},
@@ -820,7 +845,7 @@ describe('bacstack - Services layer', function() {
820845
propertyId: 81
821846
},
822847
value: [
823-
{type: 7, value: 'Test1234$', len: 12}
848+
{type: 7, value: 'Test1234$', encoding: 0, len: 12}
824849
]
825850
}
826851
]
@@ -852,7 +877,7 @@ describe('bacstack - Services layer', function() {
852877
propertyId: 81
853878
},
854879
value: [
855-
{type: 7, value: 'Test1234$', len: 12}
880+
{type: 7, value: 'Test1234$', encoding: 0, len: 12}
856881
]
857882
}
858883
]

0 commit comments

Comments
 (0)