node-red-contrib-lorawan-bacnet-server 1.2.0 → 1.2.2
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/bacnet-scanner.tgz +0 -0
- package/node-red-contrib-lorawan-bacnet-server-1.2.1-bundle.tgz +0 -0
- package/node-red-contrib-lorawan-bacnet-server-1.2.1.tgz +0 -0
- package/nodes/bacnet-server/bacnet-server.js +2 -1
- package/package.json +6 -2
- 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
|
|
Binary file
|
|
@@ -160,8 +160,9 @@ 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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-lorawan-bacnet-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Custom Node-RED nodes to interface LoRaWAN devices with BACnet protocol. ",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"LoRaWAN",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/
|
|
37
|
+
"url": "git+https://github.com/UnitYX-GT/node-red-contrib-lorawan-bacnet.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/UnitYX-GT/node-red-contrib-lorawan-bacnet#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/UnitYX-GT/node-red-contrib-lorawan-bacnet/issues"
|
|
38
42
|
}
|
|
39
43
|
}
|
|
@@ -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');
|