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.
@@ -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.request.objectId;
177
- const propertyId = data.request.property.id;
178
- const arrayIndex = data.request.property.index;
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.address,
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.address,
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.address,
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.request.objectId;
221
- const propertyId = data.request.property.id;
222
- const priority = data.request.priority || 16;
223
- const value = data.request.value;
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.address,
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.address,
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.address,
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.request.properties;
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.address,
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.request.subscriberProcessId;
321
- const objectId = data.request.monitoredObjectId;
322
- const issueConfirmedNotifications = data.request.issueConfirmedNotifications;
323
- const lifetime = data.request.lifetime || 0;
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.address,
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 subscriptionKey = `${data.address}:${subscriberProcessId}:${objectKey}`;
340
+ const senderAddress = data.header?.sender?.address || 'unknown';
341
+ const subscriptionKey = `${senderAddress}:${subscriberProcessId}:${objectKey}`;
340
342
 
341
- if (lifetime === 0 && data.request.cancellationRequest) {
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.address,
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.address,
362
+ data.header.sender,
361
363
  bacnet.enum.ConfirmedService.SUBSCRIBE_COV,
362
364
  data.invokeId
363
365
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-lorawan-bacnet-server",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Custom Node-RED nodes to interface LoRaWAN devices with BACnet protocol. ",
5
5
  "keywords": [
6
6
  "LoRaWAN",
@@ -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');