@emitucom/bacnet-connector
v0.1.0
Published
Native TypeScript BACnet client library for reading and writing BACnet objects over IP and MSTP
Maintainers
Readme
@emitucom/bacnet-connector
Native TypeScript BACnet client library with no external BACnet dependencies. Supports BACnet/IP (UDP), MS/TP (serial), and Ethernet (ISO 8802-3) transports.
Requirements
- Node.js >= 8.17.0
- For MS/TP: a USB-to-RS485 adapter and the
serialportnpm package - For Ethernet:
raw-socketnpm package andCAP_NET_RAWcapability (Linux) or root (macOS/Windows)
Installation
npm install @emitucom/bacnet-connectorBuild from source:
git clone https://github.com/EmituCom/BACnetConnector.git
cd BACnetConnector
npm install
npm run buildQuick Start
const { BACnetClient, BACnetIPTransport } = require('@emitucom/bacnet-connector');
const client = new BACnetClient({
transport: new BACnetIPTransport({ broadcastAddress: '192.168.1.255' }),
});
async function main() {
await client.open();
// Discover all devices on the local network
const devices = await client.whoIs();
console.log(`Found ${devices.length} device(s)`);
devices.forEach(d => console.log(d.deviceIdentifier.instance, d.vendorId));
await client.close();
}
main().catch(console.error);Transports
BACnet/IP (UDP)
The most common transport. Uses UDP port 47808 (0xBAC0).
const { BACnetIPTransport } = require('@emitucom/bacnet-connector');
const transport = new BACnetIPTransport({
localAddress: '0.0.0.0', // bind address (default: 0.0.0.0)
localPort: 47808, // bind port (default: 47808)
broadcastAddress: '192.168.1.255',// subnet broadcast (default: 255.255.255.255)
broadcastPort: 47808, // broadcast port (default: 47808)
});Tip: Always use your subnet broadcast address (e.g.
192.168.1.255) rather than255.255.255.255. On Linux the latter may not route to the correct interface.
MS/TP (Serial RS-485)
For BACnet MS/TP field buses. Requires the serialport package.
const { MstpTransport } = require('@emitucom/bacnet-connector');
const transport = new MstpTransport({
port: '/dev/ttyUSB0', // serial port path (required)
baudRate: 76800, // baud rate (default: 76800)
macAddress: 0, // this node's MAC (default: 0, range 0–127)
maxMaster: 127, // highest master MAC to poll (default: 127)
maxInfoFrames: 1, // frames per token pass (default: 1)
});Ethernet (ISO 8802-3)
Raw Ethernet frames using EtherType 0x82DC. Requires raw-socket and elevated privileges.
const { EthernetTransport } = require('@emitucom/bacnet-connector');
const transport = new EthernetTransport({
interface: 'eth0', // network interface (required)
sourceMac: Buffer.from([0x00,0x11,0x22,0x33,0x44,0x55]), // optional
});On Linux, grant raw socket access without running as root:
sudo setcap cap_net_raw+eip $(which node)API Reference
new BACnetClient(options)
| Option | Type | Default | Description |
|---|---|---|---|
| transport | ITransport | required | Transport instance to use |
| timeoutMs | number | 3000 | Confirmed request timeout (ms) |
| maxRetries | number | 2 | Retransmissions before rejection |
| whoIsTimeoutMs | number | 3000 | Who-Is collection window (ms) |
client.open() → Promise<void>
Opens the underlying transport (binds socket / opens serial port).
client.close() → Promise<void>
Closes the transport and rejects all pending requests.
client.whoIs(lowLimit?, highLimit?, opts?) → Promise<IAmResult[]>
Broadcasts a Who-Is and collects I-Am replies for whoIsTimeoutMs milliseconds.
| Parameter | Type | Description |
|---|---|---|
| lowLimit | number | Filter: only collect devices with instance ≥ lowLimit |
| highLimit | number | Filter: only collect devices with instance ≤ highLimit |
| opts.remoteNetwork | number | NPDU destination network. Use 0xFFFF for global broadcast (reaches devices behind BACnet routers) |
| opts.routerAddress | BACnetAddress | Send the Who-Is to a specific router instead of broadcasting |
// Local network only
const devices = await client.whoIs();
// Global broadcast — reaches devices on all routed networks
const allDevices = await client.whoIs(undefined, undefined, { remoteNetwork: 0xFFFF });
// Specific instance range
const subset = await client.whoIs(1000, 2000);IAmResult
{
deviceIdentifier: { objectType: number; instance: number };
maxApduLength: number;
segmentationSupported: number;
vendorId: number;
source?: BACnetAddress; // transport-layer source (IP/port of sender or router)
networkSource?: BACnetAddress; // NPDU-layer source (net + MAC for routed devices)
}client.readProperty(dest, request) → Promise<ReadPropertyResult>
Reads a single property from a BACnet object.
const { ObjectType, PropertyIdentifier } = require('@emitucom/bacnet-connector');
const result = await client.readProperty(
{ ip: '192.168.1.11', port: 47808 },
{
objectType: ObjectType.Device,
instance: 1973,
propertyId: PropertyIdentifier.ObjectName,
}
);
console.log(result.value[0].value); // e.g. "enteliWEB"For devices behind a BACnet router, combine the router IP and the device's network address:
// Device on BACnet network 2, MAC 0x214E000000, reachable via router at 192.168.1.8
const result = await client.readProperty(
{ ip: '192.168.1.8', port: 47808, net: 2, adr: Buffer.from([0x21, 0x4e, 0x00, 0x00, 0x00, 0x00]) },
{ objectType: ObjectType.Device, instance: 20001, propertyId: PropertyIdentifier.ObjectName }
);client.writeProperty(dest, request) → Promise<void>
Writes a value to a BACnet property.
const { BACnetValue } = require('@emitucom/bacnet-connector');
await client.writeProperty(
{ ip: '192.168.1.11', port: 47808 },
{
objectType: ObjectType.AnalogValue,
instance: 1,
propertyId: PropertyIdentifier.PresentValue,
value: [{ type: 'Real', value: 21.5 }],
priority: 8, // optional, 1–16
}
);client.readPropertyMultiple(dest, request) → Promise<ReadPropertyMultipleResult>
Reads multiple properties from multiple objects in one request.
const results = await client.readPropertyMultiple(
{ ip: '192.168.1.11', port: 47808 },
[
{
objectType: ObjectType.Device,
instance: 1973,
properties: [
{ propertyId: PropertyIdentifier.ObjectName },
{ propertyId: PropertyIdentifier.VendorName },
{ propertyId: PropertyIdentifier.SystemStatus },
],
},
{
objectType: ObjectType.AnalogInput,
instance: 1,
properties: [
{ propertyId: PropertyIdentifier.PresentValue },
{ propertyId: PropertyIdentifier.Units },
],
},
]
);client.subscribeCOV(dest, request) → Promise<void>
Subscribes to Change-of-Value notifications for an object.
await client.subscribeCOV(
{ ip: '192.168.1.11', port: 47808 },
{
subscriberProcessId: 42,
monitoredObjectType: ObjectType.BinaryInput,
monitoredObjectInstance: 1,
issueConfirmedNotifications: false,
lifetime: 300, // seconds; 0 = cancel
}
);
client.on('covNotification', notification => {
console.log('COV from device', notification.initiatingDeviceIdentifier.instance);
notification.listOfValues.forEach(v => {
console.log(` property ${v.propertyId}:`, v.value);
});
});Events
client.on('iAm', result => { /* IAmResult */ });
client.on('covNotification', result => { /* COVNotificationResult */ });
client.on('error', err => { /* Error */ });Types
BACnetAddress
{
ip?: string; // IPv4 dotted-decimal, e.g. "192.168.1.10"
port?: number; // UDP port, default 47808
mac?: number; // MS/TP MAC (0–127)
ethernetMac?: Buffer; // Ethernet MAC (6 bytes)
net?: number; // BACnet network number
adr?: Buffer; // BACnet device address bytes
}BACnetValue
Union of all BACnet application-layer data types:
{ type: 'Null' }
{ type: 'Boolean'; value: boolean }
{ type: 'Unsigned'; value: number }
{ type: 'Signed'; value: number }
{ type: 'Real'; value: number }
{ type: 'Double'; value: number }
{ type: 'OctetString'; value: Buffer }
{ type: 'CharString'; value: string; encoding: number }
{ type: 'BitString'; unusedBits: number; bits: Buffer }
{ type: 'Enumerated'; value: number }
{ type: 'Date'; year: number; month: number; day: number; dayOfWeek: number }
{ type: 'Time'; hour: number; minute: number; second: number; hundredths: number }
{ type: 'ObjectIdentifier'; objectType: number; instance: number }Constants
const { ObjectType, PropertyIdentifier } = require('@emitucom/bacnet-connector');
ObjectType.Device // 8
ObjectType.AnalogInput // 0
ObjectType.BinaryInput // 3
// ... full ASHRAE 135 table
PropertyIdentifier.ObjectName // 77
PropertyIdentifier.PresentValue // 85
PropertyIdentifier.VendorName // 121
// ... full ASHRAE 135 tableExamples
examples/
scan.js Discover all BACnet devices on the network (local + routed)Running the scanner
npm run build
# Auto-detect broadcast address
node examples/scan.js
# Specify subnet broadcast explicitly (recommended)
node examples/scan.js --broadcast 192.168.1.255
# Wider collection window, skip property reads
node examples/scan.js --timeout 5000 --no-props
# Filter by device instance range
node examples/scan.js --low 1000 --high 9999
# Verbose mode — prints each I-Am as it arrives
node examples/scan.js --verboseProject Structure
src/
client/
BACnetClient.ts Main client class
BACnetClientOptions.ts Client configuration
InvokeIdManager.ts Invoke ID allocation (0–255, rolling)
PendingRequestManager.ts Timeout / retry logic for confirmed requests
transport/
ip/
BACnetIPTransport.ts BACnet/IP UDP transport
BACnetIPOptions.ts
mstp/
MstpTransport.ts MS/TP serial transport
MstpFramer.ts MS/TP frame encoder/decoder
MstpCrc.ts CRC-8 / CRC-16 for MS/TP
MstpOptions.ts
ethernet/
EthernetTransport.ts Raw Ethernet transport
EthernetOptions.ts
codec/
bvlc/ BACnet Virtual Link Control (BACnet/IP header)
npdu/ Network PDU (routing header)
apdu/ Application PDU (service type + invoke ID)
tag/ BACnet application tag encoder/decoder
services/ Individual service encoders and decoders
constants/
ObjectType.ts BACnet object type enumeration
PropertyIdentifier.ts BACnet property identifier enumeration
ConfirmedService.ts Confirmed service choice codes
UnconfirmedService.ts Unconfirmed service choice codes
ErrorCodes.ts BACnet error class and error code enumerations
types/
BACnetDataTypes.ts BACnetAddress, BACnetValue, TransportMessage
ServiceShapes.ts Request/result interfaces for all services
index.ts Public API barrel export
tests/
unit/ Unit tests (Jest)
integration/ Integration tests
examples/
scan.js Network scannerBuilding
npm run build # compile TypeScript → dist/
npm test # run unit tests
npm run test:coverage # run tests with coverage report
npm run lint # type-check without emittingContributing
Contributions are welcome. Please open an issue or pull request on GitHub.
License
BSD 3-Clause © 2026 Emitu
