Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
33abe68
feat(client): add foreign device registration API with BVLC result ha…
EveGun Mar 2, 2026
79cc362
feat(bbmd): add foreign-device registration and who-is via BBMD with …
EveGun Mar 2, 2026
bd2f35f
Fix BVLC/I-Am decode edge cases and improve BBMD discover diagnostics
EveGun Mar 2, 2026
bb65131
Serialize BBMD foreign-device registrations per target and tighten FD…
EveGun Mar 2, 2026
beaf971
Handle queued FDR retries after failures and align BBMD example usage
EveGun Mar 2, 2026
39e2832
Apply lint formatting for BBMD/FDR changes
EveGun Mar 2, 2026
c1fde1f
fix(whois): preserve BBMD receiver when limits are provided
EveGun Mar 3, 2026
d478b08
Guard invalid BVLC msg length
EveGun Mar 3, 2026
c910b14
Guard BVLC_RESULT payload length before decode
EveGun Mar 3, 2026
e1ca664
Revert BVLC trailing-byte tolerance
EveGun Mar 3, 2026
2d7dff1
Merge branch 'master' into feat/foreign-device-registration
robertsLando Mar 3, 2026
da094bf
Merge branch 'master' into feat/foreign-device-registration
robertsLando Mar 3, 2026
542ff9c
Merge branch 'master' into feat/foreign-device-registration
robertsLando Mar 3, 2026
f181066
Merge branch 'master' into feat/foreign-device-registration
robertsLando Mar 3, 2026
2a85017
fix(whoIs): preserve explicit options in overload resolution
EveGun Mar 4, 2026
306fde7
fix(registerForeignDevice): defer buffer allocation until after dedupe
EveGun Mar 4, 2026
658cf76
refactor: centralize DEFAULT_BACNET_PORT constant
EveGun Mar 4, 2026
91f6e0a
fix(registerForeignDevice): unref timeout to avoid keeping event loop…
EveGun Mar 4, 2026
1161586
fix(whoIsThroughBBMD): encode NPDU as broadcast inside DBTN
EveGun Mar 4, 2026
638988a
fix(example): use valid BACNetAddress in FDR sample
EveGun Mar 4, 2026
25078d6
fix(registerForeignDevice): reject pending on close and remove recursion
EveGun Mar 4, 2026
c5ab2d2
fix(registerForeignDevice): fail fast after close for queued callers
EveGun Mar 4, 2026
2424441
chore: document BVLC success ambiguity and fix example formatting
EveGun Mar 4, 2026
fb685b3
chore: fix lint formatting in client and tests
EveGun Mar 4, 2026
5e5ff5c
chore: clean up IAm len and normalize trailing-colon addresses
EveGun Mar 4, 2026
07b52ea
refactor: remove redundant rejectRegistration assignment
EveGun Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions examples/discover-devices-via-bbmd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Discover devices through BBMD:
* 1) Register as foreign device
* 2) Send Who-Is using BVLC Distribute-Broadcast-To-Network (0x09)
*
* Usage:
* npx ts-node examples/discover-devices-via-bbmd.ts <bbmd-ip:port> [ttl-seconds]
*/

import Bacnet from '../src'

const bbmdAddress = process.argv[2]
const ttlSeconds = Number(process.argv[3] || 60)
const localPort = Number(process.env.BACNET_PORT || 47809)

if (!bbmdAddress) {
console.error('Missing BBMD address. Usage: <bbmd-ip:port> [ttl-seconds]')
process.exit(1)
}

const client = new Bacnet({
apduTimeout: 5000,
interface: '0.0.0.0',
port: localPort,
})

client.on('error', (err: Error) => {
console.error(`BACnet error: ${err.message}`)
})

client.on('iAm', (device: any) => {
console.log(
`iAm from ${device?.payload?.deviceId} via ${device?.header?.sender?.address} (forwardedFrom=${device?.header?.sender?.forwardedFrom ?? 'n/a'})`,
)
})

client.on('listening', async () => {
try {
console.log(`Listening on UDP ${localPort}`)
await client.registerForeignDevice({ address: bbmdAddress }, ttlSeconds)
console.log(`FDR success on ${bbmdAddress} (ttl=${ttlSeconds}s)`)
client.whoIsThroughBBMD({ address: bbmdAddress })
console.log('Who-Is sent through BBMD')
} catch (err) {
console.error(`Failed: ${String((err as Error)?.message || err)}`)
}
})

setTimeout(() => {
client.close()
console.log('Done')
}, 20000)
111 changes: 111 additions & 0 deletions examples/register-foreign-device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Register this BACnet client as a Foreign Device in a BBMD and periodically renew it.
*
* Usage:
* npx ts-node examples/register-foreign-device.ts <bbmd-ip:port> [ttl-seconds]
*
* Example:
* npx ts-node examples/register-foreign-device.ts 192.168.40.10:47808 900
*/

import Bacnet from '../src'

const bbmdAddress = process.argv[2] || process.env.BBMD_ADDRESS
const ttlSeconds = Number(process.argv[3] || process.env.FDR_TTL || 900)
const localPort = Number(process.env.BACNET_PORT || 47809)
const renewRatio = Number(process.env.FDR_RENEW_RATIO || 0.8)

if (!bbmdAddress) {
console.error(
'Missing BBMD address. Pass <bbmd-ip:port> or set BBMD_ADDRESS.',
)
process.exit(1)
}

if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > 0xffff) {
console.error('Invalid TTL. Expected integer in range 1..65535.')
process.exit(1)
}

const renewDelayMs = Math.max(
1000,
Math.floor(
ttlSeconds *
(renewRatio > 0 && renewRatio < 1 ? renewRatio : 0.8) *
1000,
),
)

const bacnetClient = new Bacnet({
apduTimeout: 5000,
interface: '0.0.0.0',
port: localPort,
})

let renewTimer: NodeJS.Timeout | null = null
let registerInFlight = false

const clearRenewTimer = () => {
if (renewTimer) clearTimeout(renewTimer)
renewTimer = null
}

const closeClient = () => {
clearRenewTimer()
bacnetClient.close()
}

const register = async () => {
if (registerInFlight) return
registerInFlight = true
try {
await bacnetClient.registerForeignDevice(
{ address: bbmdAddress },
ttlSeconds,
)
console.log(
`FDR success: bbmd=${bbmdAddress}, ttl=${ttlSeconds}s, next_renew_in=${Math.floor(renewDelayMs / 1000)}s`,
)
clearRenewTimer()
renewTimer = setTimeout(() => {
register().catch((err) =>
console.error(
`FDR renew failed: ${String((err as Error)?.message || err)}`,
),
)
}, renewDelayMs)
} catch (err) {
console.error(`FDR failed: ${String((err as Error)?.message || err)}`)
} finally {
registerInFlight = false
}
}

bacnetClient.on('listening', () => {
console.log(`BACnet transport listening on UDP ${localPort}`)
console.log(`Registering to BBMD ${bbmdAddress} ...`)
register().catch((err) =>
console.error(`FDR failed: ${String((err as Error)?.message || err)}`),
)
})

bacnetClient.on('bvlcResult', (content) => {
console.log(
`BVLC result from ${content?.header?.sender?.address}: ${content?.payload?.resultCode}`,
)
})

bacnetClient.on('error', (err: Error) => {
console.error(`BACnet error: ${err.message}`)
})

process.on('SIGINT', () => {
console.log('Stopping...')
closeClient()
process.exit(0)
})

process.on('SIGTERM', () => {
closeClient()
process.exit(0)
})
4 changes: 4 additions & 0 deletions src/lib/EventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ListElementOperationPayload,
PrivateTransferPayload,
RegisterForeignDevicePayload,
BvlcResultPayload,
WhoHasPayload,
TimeSyncPayload,
IHavePayload,
Expand Down Expand Up @@ -180,6 +181,9 @@ export interface BACnetClientEvents {
privateTransfer: (
content: BaseEventContent & { payload: PrivateTransferPayload },
) => void
bvlcResult: (
content: BaseEventContent & { payload: BvlcResultPayload },
) => void
registerForeignDevice: (
content: BaseEventContent & { payload: RegisterForeignDevicePayload },
) => void
Expand Down
7 changes: 5 additions & 2 deletions src/lib/bvlc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import {
BVLL_TYPE_BACNET_IP,
BvlcResultPurpose,
BVLC_HEADER_LENGTH,
DEFAULT_BACNET_PORT,
} from './enum'
import { BvlcPacket } from './types'

const DEFAULT_BACNET_PORT = 47808

export const encode = (
buffer: Buffer,
func: number,
Expand Down Expand Up @@ -50,9 +49,12 @@ export const decode = (
buffer: Buffer,
_offset: number,
): BvlcPacket | undefined => {
if (buffer.length < BVLC_HEADER_LENGTH) return undefined

let len: number
const func = buffer[1]
const msgLength = (buffer[2] << 8) | (buffer[3] << 0)
if (msgLength < BVLC_HEADER_LENGTH) return undefined
if (buffer[0] !== BVLL_TYPE_BACNET_IP || buffer.length !== msgLength)
return undefined
let originatingIP = null
Expand All @@ -71,6 +73,7 @@ export const decode = (
len = 4
break
case BvlcResultPurpose.FORWARDED_NPDU:
if (msgLength < 10) return undefined
// Work out where the packet originally came from before the BBMD
// forwarded it to us, so we can tell the BBMD where to send any reply to.
const port = (buffer[8] << 8) | buffer[9]
Expand Down
Loading