-
Notifications
You must be signed in to change notification settings - Fork 4
feat: foreign device registration #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
robertsLando
merged 26 commits into
bacnet-js:master
from
EveGun:feat/foreign-device-registration
Mar 4, 2026
Merged
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 79cc362
feat(bbmd): add foreign-device registration and who-is via BBMD with …
EveGun bd2f35f
Fix BVLC/I-Am decode edge cases and improve BBMD discover diagnostics
EveGun bb65131
Serialize BBMD foreign-device registrations per target and tighten FD…
EveGun beaf971
Handle queued FDR retries after failures and align BBMD example usage
EveGun 39e2832
Apply lint formatting for BBMD/FDR changes
EveGun c1fde1f
fix(whois): preserve BBMD receiver when limits are provided
EveGun d478b08
Guard invalid BVLC msg length
EveGun c910b14
Guard BVLC_RESULT payload length before decode
EveGun e1ca664
Revert BVLC trailing-byte tolerance
EveGun 2d7dff1
Merge branch 'master' into feat/foreign-device-registration
robertsLando da094bf
Merge branch 'master' into feat/foreign-device-registration
robertsLando 542ff9c
Merge branch 'master' into feat/foreign-device-registration
robertsLando f181066
Merge branch 'master' into feat/foreign-device-registration
robertsLando 2a85017
fix(whoIs): preserve explicit options in overload resolution
EveGun 306fde7
fix(registerForeignDevice): defer buffer allocation until after dedupe
EveGun 658cf76
refactor: centralize DEFAULT_BACNET_PORT constant
EveGun 91f6e0a
fix(registerForeignDevice): unref timeout to avoid keeping event loop…
EveGun 1161586
fix(whoIsThroughBBMD): encode NPDU as broadcast inside DBTN
EveGun 638988a
fix(example): use valid BACNetAddress in FDR sample
EveGun 25078d6
fix(registerForeignDevice): reject pending on close and remove recursion
EveGun c5ab2d2
fix(registerForeignDevice): fail fast after close for queued callers
EveGun 2424441
chore: document BVLC success ambiguity and fix example formatting
EveGun fb685b3
chore: fix lint formatting in client and tests
EveGun 5e5ff5c
chore: clean up IAm len and normalize trailing-colon addresses
EveGun 07b52ea
refactor: remove redundant rejectRegistration assignment
EveGun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.