node-red-contrib-lorawan-bacnet-server 1.2.1 → 1.2.3
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.
Potentially problematic release.
This version of node-red-contrib-lorawan-bacnet-server might be problematic. Click here for more details.
- package/node-red-contrib-lorawan-bacnet-server-1.2.2-bundle.tgz +0 -0
- package/node-red-contrib-lorawan-bacnet-server-1.2.2.tgz +0 -0
- package/nodes/bacnet-server/bacnet-server.js +29 -27
- package/package.json +1 -1
- package/tools/bacnet-scan.js +134 -0
- package/tools/bacnet-server-test.js +38 -0
- package/tools/test-server-unicast.js +22 -0
- package/tools/test-server.js +21 -0
|
Binary file
|
|
Binary file
|
|
@@ -160,29 +160,30 @@ module.exports = function (RED) {
|
|
|
160
160
|
if ((lowLimit === undefined || node.deviceId >= lowLimit) &&
|
|
161
161
|
(highLimit === undefined || node.deviceId <= highLimit)) {
|
|
162
162
|
|
|
163
|
-
// Respond with I-Am
|
|
163
|
+
// Respond with I-Am (null = broadcast)
|
|
164
164
|
node.client.iAmResponse(
|
|
165
|
+
null,
|
|
165
166
|
node.deviceId,
|
|
166
167
|
bacnet.enum.Segmentation.NO_SEGMENTATION,
|
|
167
168
|
node.vendorId
|
|
168
169
|
);
|
|
169
170
|
|
|
170
|
-
node.debug(`Responded to Who-Is from ${data.address}`);
|
|
171
|
+
node.debug(`Responded to Who-Is from ${data.header?.sender?.address || 'broadcast'}`);
|
|
171
172
|
}
|
|
172
173
|
};
|
|
173
174
|
|
|
174
175
|
// Handle Read Property requests
|
|
175
176
|
node.handleReadProperty = function (data) {
|
|
176
|
-
const objectId = data.
|
|
177
|
-
const propertyId = data.
|
|
178
|
-
const arrayIndex = data.
|
|
177
|
+
const objectId = data.payload.objectId;
|
|
178
|
+
const propertyId = data.payload.property.id;
|
|
179
|
+
const arrayIndex = data.payload.property.index;
|
|
179
180
|
|
|
180
181
|
try {
|
|
181
182
|
const value = node.getPropertyValue(objectId, propertyId, arrayIndex);
|
|
182
183
|
|
|
183
184
|
if (value !== null && value !== undefined) {
|
|
184
185
|
node.client.readPropertyResponse(
|
|
185
|
-
data.
|
|
186
|
+
data.header.sender,
|
|
186
187
|
data.invokeId,
|
|
187
188
|
objectId,
|
|
188
189
|
{ id: propertyId, index: arrayIndex },
|
|
@@ -196,7 +197,7 @@ module.exports = function (RED) {
|
|
|
196
197
|
}
|
|
197
198
|
} else {
|
|
198
199
|
node.client.errorResponse(
|
|
199
|
-
data.
|
|
200
|
+
data.header.sender,
|
|
200
201
|
bacnet.enum.ConfirmedService.READ_PROPERTY,
|
|
201
202
|
data.invokeId,
|
|
202
203
|
bacnet.enum.ErrorClass.PROPERTY,
|
|
@@ -206,7 +207,7 @@ module.exports = function (RED) {
|
|
|
206
207
|
} catch (err) {
|
|
207
208
|
node.error(`Read Property error: ${err.message}`);
|
|
208
209
|
node.client.errorResponse(
|
|
209
|
-
data.
|
|
210
|
+
data.header.sender,
|
|
210
211
|
bacnet.enum.ConfirmedService.READ_PROPERTY,
|
|
211
212
|
data.invokeId,
|
|
212
213
|
bacnet.enum.ErrorClass.OBJECT,
|
|
@@ -217,17 +218,17 @@ module.exports = function (RED) {
|
|
|
217
218
|
|
|
218
219
|
// Handle Write Property requests
|
|
219
220
|
node.handleWriteProperty = function (data) {
|
|
220
|
-
const objectId = data.
|
|
221
|
-
const propertyId = data.
|
|
222
|
-
const priority = data.
|
|
223
|
-
const value = data.
|
|
221
|
+
const objectId = data.payload.objectId;
|
|
222
|
+
const propertyId = data.payload.property.id;
|
|
223
|
+
const priority = data.payload.priority || 16;
|
|
224
|
+
const value = data.payload.value;
|
|
224
225
|
|
|
225
226
|
try {
|
|
226
227
|
const result = node.setPropertyValue(objectId, propertyId, value, priority);
|
|
227
228
|
|
|
228
229
|
if (result.success) {
|
|
229
230
|
node.client.simpleAckResponse(
|
|
230
|
-
data.
|
|
231
|
+
data.header.sender,
|
|
231
232
|
bacnet.enum.ConfirmedService.WRITE_PROPERTY,
|
|
232
233
|
data.invokeId
|
|
233
234
|
);
|
|
@@ -235,14 +236,14 @@ module.exports = function (RED) {
|
|
|
235
236
|
// Notify point node
|
|
236
237
|
const pointNode = node.getPointNode(objectId);
|
|
237
238
|
if (pointNode) {
|
|
238
|
-
pointNode.emitWriteEvent(result.value, priority, data.address);
|
|
239
|
+
pointNode.emitWriteEvent(result.value, priority, data.header.sender.address);
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
// Check COV notifications
|
|
242
243
|
node.checkCovNotifications(objectId);
|
|
243
244
|
} else {
|
|
244
245
|
node.client.errorResponse(
|
|
245
|
-
data.
|
|
246
|
+
data.header.sender,
|
|
246
247
|
bacnet.enum.ConfirmedService.WRITE_PROPERTY,
|
|
247
248
|
data.invokeId,
|
|
248
249
|
bacnet.enum.ErrorClass.PROPERTY,
|
|
@@ -252,7 +253,7 @@ module.exports = function (RED) {
|
|
|
252
253
|
} catch (err) {
|
|
253
254
|
node.error(`Write Property error: ${err.message}`);
|
|
254
255
|
node.client.errorResponse(
|
|
255
|
-
data.
|
|
256
|
+
data.header.sender,
|
|
256
257
|
bacnet.enum.ConfirmedService.WRITE_PROPERTY,
|
|
257
258
|
data.invokeId,
|
|
258
259
|
bacnet.enum.ErrorClass.OBJECT,
|
|
@@ -263,7 +264,7 @@ module.exports = function (RED) {
|
|
|
263
264
|
|
|
264
265
|
// Handle Read Property Multiple requests
|
|
265
266
|
node.handleReadPropertyMultiple = function (data) {
|
|
266
|
-
const properties = data.
|
|
267
|
+
const properties = data.payload.properties;
|
|
267
268
|
const results = [];
|
|
268
269
|
|
|
269
270
|
for (const prop of properties) {
|
|
@@ -309,7 +310,7 @@ module.exports = function (RED) {
|
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
node.client.readPropertyMultipleResponse(
|
|
312
|
-
data.
|
|
313
|
+
data.header.sender,
|
|
313
314
|
data.invokeId,
|
|
314
315
|
results
|
|
315
316
|
);
|
|
@@ -317,17 +318,17 @@ module.exports = function (RED) {
|
|
|
317
318
|
|
|
318
319
|
// Handle Subscribe COV requests
|
|
319
320
|
node.handleSubscribeCov = function (data) {
|
|
320
|
-
const subscriberProcessId = data.
|
|
321
|
-
const objectId = data.
|
|
322
|
-
const issueConfirmedNotifications = data.
|
|
323
|
-
const lifetime = data.
|
|
321
|
+
const subscriberProcessId = data.payload.subscriberProcessId;
|
|
322
|
+
const objectId = data.payload.monitoredObjectId;
|
|
323
|
+
const issueConfirmedNotifications = data.payload.issueConfirmedNotifications;
|
|
324
|
+
const lifetime = data.payload.lifetime || 0;
|
|
324
325
|
|
|
325
326
|
const objectKey = `${objectId.type}:${objectId.instance}`;
|
|
326
327
|
const obj = node.objects.get(objectKey);
|
|
327
328
|
|
|
328
329
|
if (!obj) {
|
|
329
330
|
node.client.errorResponse(
|
|
330
|
-
data.
|
|
331
|
+
data.header.sender,
|
|
331
332
|
bacnet.enum.ConfirmedService.SUBSCRIBE_COV,
|
|
332
333
|
data.invokeId,
|
|
333
334
|
bacnet.enum.ErrorClass.OBJECT,
|
|
@@ -336,16 +337,17 @@ module.exports = function (RED) {
|
|
|
336
337
|
return;
|
|
337
338
|
}
|
|
338
339
|
|
|
339
|
-
const
|
|
340
|
+
const senderAddress = data.header?.sender?.address || 'unknown';
|
|
341
|
+
const subscriptionKey = `${senderAddress}:${subscriberProcessId}:${objectKey}`;
|
|
340
342
|
|
|
341
|
-
if (lifetime === 0 && data.
|
|
343
|
+
if (lifetime === 0 && data.payload.cancellationRequest) {
|
|
342
344
|
// Cancel subscription
|
|
343
345
|
node.covSubscriptions.delete(subscriptionKey);
|
|
344
346
|
node.debug(`COV subscription cancelled: ${subscriptionKey}`);
|
|
345
347
|
} else {
|
|
346
348
|
// Create/update subscription
|
|
347
349
|
node.covSubscriptions.set(subscriptionKey, {
|
|
348
|
-
address: data.
|
|
350
|
+
address: data.header.sender,
|
|
349
351
|
processId: subscriberProcessId,
|
|
350
352
|
objectId: objectId,
|
|
351
353
|
confirmed: issueConfirmedNotifications,
|
|
@@ -357,7 +359,7 @@ module.exports = function (RED) {
|
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
node.client.simpleAckResponse(
|
|
360
|
-
data.
|
|
362
|
+
data.header.sender,
|
|
361
363
|
bacnet.enum.ConfirmedService.SUBSCRIBE_COV,
|
|
362
364
|
data.invokeId
|
|
363
365
|
);
|
package/package.json
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BACnet Scanner CLI Tool
|
|
4
|
+
* Scans the network for BACnet devices using Who-Is broadcast
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const bacnet = require('node-bacnet');
|
|
8
|
+
const dgram = require('dgram');
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const timeout = parseInt(args.find(a => a.startsWith('--timeout='))?.split('=')[1]) || 5000;
|
|
12
|
+
const target = args.find(a => a.startsWith('--target='))?.split('=')[1] || null;
|
|
13
|
+
|
|
14
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
15
|
+
console.log(`
|
|
16
|
+
BACnet Scanner - Discover BACnet devices on the network
|
|
17
|
+
|
|
18
|
+
Usage: node bacnet-scan.js [options]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--timeout=MS Scan timeout in milliseconds (default: 5000)
|
|
22
|
+
--target=IP Target specific IP address
|
|
23
|
+
--help, -h Show this help
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
node bacnet-scan.js
|
|
27
|
+
node bacnet-scan.js --timeout=10000
|
|
28
|
+
node bacnet-scan.js --target=192.168.1.100
|
|
29
|
+
`);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('========================================');
|
|
34
|
+
console.log(' BACnet Network Scanner');
|
|
35
|
+
console.log('========================================');
|
|
36
|
+
console.log(`Timeout: ${timeout}ms`);
|
|
37
|
+
if (target) console.log(`Target: ${target}`);
|
|
38
|
+
console.log('----------------------------------------');
|
|
39
|
+
console.log('Scanning for BACnet devices...\n');
|
|
40
|
+
|
|
41
|
+
const devices = new Map();
|
|
42
|
+
|
|
43
|
+
// Parse I-Am from raw packet
|
|
44
|
+
function parseIAm(buffer, rinfo) {
|
|
45
|
+
try {
|
|
46
|
+
// Check for valid BACnet/IP header (0x81)
|
|
47
|
+
if (buffer[0] !== 0x81) return null;
|
|
48
|
+
|
|
49
|
+
// Skip BVLC header (4 bytes) and NPDU
|
|
50
|
+
let offset = 4;
|
|
51
|
+
|
|
52
|
+
// Find NPDU length - skip to APDU
|
|
53
|
+
const npduLength = buffer[1] === 0x0b ? 0 : buffer[offset + 1];
|
|
54
|
+
offset += 2 + npduLength;
|
|
55
|
+
|
|
56
|
+
// Check for Unconfirmed Request (0x10) and I-Am service (0x00)
|
|
57
|
+
if (offset >= buffer.length) return null;
|
|
58
|
+
|
|
59
|
+
const apduType = buffer[offset] >> 4;
|
|
60
|
+
if (apduType !== 1) return null; // Not unconfirmed
|
|
61
|
+
|
|
62
|
+
const serviceChoice = buffer[offset + 1];
|
|
63
|
+
if (serviceChoice !== 0) return null; // Not I-Am
|
|
64
|
+
|
|
65
|
+
offset += 2;
|
|
66
|
+
|
|
67
|
+
// Parse Object Identifier (Device ID)
|
|
68
|
+
if (buffer[offset] !== 0xc4) return null; // Not object-id tag
|
|
69
|
+
offset++;
|
|
70
|
+
|
|
71
|
+
const objectIdRaw = buffer.readUInt32BE(offset);
|
|
72
|
+
const objectType = (objectIdRaw >> 22) & 0x3FF;
|
|
73
|
+
const instanceNumber = objectIdRaw & 0x3FFFFF;
|
|
74
|
+
|
|
75
|
+
if (objectType !== 8) return null; // Not a device object
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
deviceId: instanceNumber,
|
|
79
|
+
address: rinfo.address
|
|
80
|
+
};
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create raw UDP listener with reuseAddr
|
|
87
|
+
const listener = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
88
|
+
|
|
89
|
+
listener.on('message', (msg, rinfo) => {
|
|
90
|
+
const device = parseIAm(msg, rinfo);
|
|
91
|
+
if (device && !devices.has(device.deviceId)) {
|
|
92
|
+
devices.set(device.deviceId, device);
|
|
93
|
+
console.log(`[FOUND] Device ID: ${device.deviceId}`);
|
|
94
|
+
console.log(` Address: ${device.address}\n`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
listener.on('error', (err) => {
|
|
99
|
+
console.error(`[ERROR] ${err.message}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
listener.bind(47808, () => {
|
|
103
|
+
listener.setBroadcast(true);
|
|
104
|
+
|
|
105
|
+
// Use separate client to send Who-Is
|
|
106
|
+
const client = new bacnet({ port: 47810 + Math.floor(Math.random() * 100) });
|
|
107
|
+
|
|
108
|
+
client.on('error', () => {}); // Ignore client errors
|
|
109
|
+
|
|
110
|
+
// Send Who-Is request
|
|
111
|
+
if (target) {
|
|
112
|
+
client.whoIs({ address: target });
|
|
113
|
+
} else {
|
|
114
|
+
client.whoIs();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Wait for responses
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
console.log('----------------------------------------');
|
|
120
|
+
console.log(`Scan complete. Found ${devices.size} device(s).`);
|
|
121
|
+
|
|
122
|
+
if (devices.size > 0) {
|
|
123
|
+
console.log('\nSummary:');
|
|
124
|
+
let i = 1;
|
|
125
|
+
for (const [id, d] of devices) {
|
|
126
|
+
console.log(` ${i++}. Device ${id} at ${d.address}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
listener.close();
|
|
131
|
+
client.close();
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}, timeout);
|
|
134
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BACnet Server Test - Simple standalone server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const bacnet = require('node-bacnet');
|
|
7
|
+
|
|
8
|
+
const deviceId = 123456;
|
|
9
|
+
const port = 47808;
|
|
10
|
+
|
|
11
|
+
console.log('========================================');
|
|
12
|
+
console.log(' BACnet Server Test');
|
|
13
|
+
console.log('========================================');
|
|
14
|
+
console.log(`Device ID: ${deviceId}`);
|
|
15
|
+
console.log(`Port: ${port}`);
|
|
16
|
+
console.log('----------------------------------------');
|
|
17
|
+
|
|
18
|
+
const client = new bacnet({ port: port });
|
|
19
|
+
|
|
20
|
+
// Respond to Who-Is
|
|
21
|
+
client.on('whoIs', (data) => {
|
|
22
|
+
console.log(`[WHO-IS] Received from ${data.address}`);
|
|
23
|
+
|
|
24
|
+
client.iAmResponse(
|
|
25
|
+
deviceId,
|
|
26
|
+
bacnet.enum.Segmentation.NO_SEGMENTATION,
|
|
27
|
+
999 // vendor ID
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
console.log(`[I-AM] Responded with Device ID ${deviceId}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
client.on('error', (err) => {
|
|
34
|
+
console.error(`[ERROR] ${err.message}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log('\nServer running. Press Ctrl+C to stop.\n');
|
|
38
|
+
console.log('Test with: node bacnet-scan.js --target=<YOUR_IP>');
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const bacnet = require('node-bacnet');
|
|
2
|
+
const dgram = require('dgram');
|
|
3
|
+
|
|
4
|
+
const client = new bacnet({ port: 47808 });
|
|
5
|
+
|
|
6
|
+
console.log('BACnet server on port 47808 (with unicast reply)...\n');
|
|
7
|
+
|
|
8
|
+
client.on('whoIs', (data) => {
|
|
9
|
+
console.log('[WHO-IS] from', data.header?.sender?.address);
|
|
10
|
+
|
|
11
|
+
// Send broadcast I-Am
|
|
12
|
+
client.iAmResponse(123456, bacnet.enum.Segmentation.NO_SEGMENTATION, 999);
|
|
13
|
+
console.log('[I-AM] Broadcast sent');
|
|
14
|
+
|
|
15
|
+
// Also try unicast to sender
|
|
16
|
+
if (data.header?.sender?.address) {
|
|
17
|
+
const sender = data.header.sender.address;
|
|
18
|
+
console.log('[I-AM] Sending unicast to', sender);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
client.on('error', (err) => console.error('[ERROR]', err.message));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const bacnet = require('node-bacnet');
|
|
2
|
+
|
|
3
|
+
const client = new bacnet({ port: 47808 });
|
|
4
|
+
|
|
5
|
+
console.log('BACnet test server on port 47808...');
|
|
6
|
+
|
|
7
|
+
client.on('whoIs', (data) => {
|
|
8
|
+
console.log('[WHO-IS RECEIVED]', data);
|
|
9
|
+
client.iAmResponse(123456, bacnet.enum.Segmentation.NO_SEGMENTATION, 999);
|
|
10
|
+
console.log('[I-AM SENT]');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
client.on('error', (err) => console.error('[ERROR]', err.message));
|
|
14
|
+
|
|
15
|
+
// Broadcast I-Am now
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
client.iAmResponse(123456, bacnet.enum.Segmentation.NO_SEGMENTATION, 999);
|
|
18
|
+
console.log('[BROADCAST I-AM]');
|
|
19
|
+
}, 1000);
|
|
20
|
+
|
|
21
|
+
console.log('Waiting for Who-Is requests...\n');
|