Skip to content

EmituCom/BACnetConnector

Repository files navigation

@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 serialport npm package
  • For Ethernet: raw-socket npm package and CAP_NET_RAW capability (Linux) or root (macOS/Windows)

Installation

npm install @emitucom/bacnet-connector

Build from source:

git clone https://github.com/EmituCom/BACnetConnector.git
cd BACnetConnector
npm install
npm run build

Quick 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 than 255.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 table

Examples

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 --verbose

Project 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 scanner

Building

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 emitting

Contributing

Contributions are welcome. Please open an issue or pull request on GitHub.


License

BSD 3-Clause © 2026 Emitu

About

Native TypeScript BACnet client library with no external BACnet dependencies. Supports BACnet/IP (UDP), MS/TP (serial), and Ethernet (ISO 8802-3) transports.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors