Currency note. Figures in this article are quoted in CNY. Where a source price was published in another currency, the CNY equivalent is converted at the fixed October 2026 reference rate of 1 USD ≈ 6.71 CNY, with the original currency shown alongside. Publicly listed prices come from different sources with inconsistent scopes; they indicate order of magnitude and are not a sourcing recommendation.


A LoRaWAN project is finished. The sensors are mounted, the gateway is online, and the platform shows clean curves. Then the customer asks a single question: can this data get into the building management system we already have?

What follows that question is work nobody particularly wants. Building management speaks BACnet. The plant floor speaks Modbus. LoRaWAN speaks neither. So you either buy another protocol gateway, or you pay an integrator to write a bridge service that translates MQTT messages into point values. That work produces no new data — it only makes data you already have readable by a system that cannot read it. And yet it has a price, a schedule, and a change-management process attached to it that runs for the life of the project.

ThinkLink ships a built-in BACnet/IP service and a built-in Modbus TCP Server in its TKE (edge server) and TKG (in-gateway) forms, and that is exactly what they are for. This article walks the integration in the order it actually happens: when you can use them, how fields become points, what has to be checked before you hand over a point list, and why "the point table saved successfully" is not the same thing as "the BMS is reading values."


1. Scope First: BACnet and Modbus Are TKE / TKG Only

This is the first thing to settle, because it is a hard boundary rather than a configuration issue.

The official documentation states the same thing in three places: the BACnet service is supported only on independently deployed TKE devices and gateways running TKL (TKG); the cloud version does not provide it. The Modbus TCP Server carries an identical restriction.

Deployment form BACnet/IP Modbus TCP Server
Cloud (public SaaS) Not supported Not supported
TKE (local server / industrial PC) Supported Supported
TKG (embedded in gateway, e.g. GDO51 / GDI51) Supported Supported

This is not a feature that was withheld. It follows from the protocol. BACnet/IP runs on UDP 47808 (0xBAC0) by default, and device discovery depends on local broadcast: a workstation sends a Who-Is broadcast and every device that hears it answers with an I-Am. That works inside one broadcast domain. IP routers do not forward broadcasts, so crossing subnets requires a BBMD (BACnet Broadcast Management Device). A multi-tenant public cloud has neither a customer's on-premise broadcast domain nor a fixed UDP port into their internal network. This is architecture, not backlog.

So align sales and pre-sales on this before the proposal goes out: if the customer is on Cloud, the data leaves via the MQTT forwarder or the REST API, and the integrator lands it into points on their side. Promising "BACnet points straight from the cloud into your BMS" is a commitment that fails during implementation.

There is a second operational constraint that is easy to miss: a single backend process runs the BACnet service for one organisation only. Starting BACnet in another organisation stops the previous organisation's instance; you cannot run multiple organisations in parallel on different ports within one process. The Modbus TCP Server behaves the same way — one instance serves one organisation, and switching service organisation replaces the running instance. A multi-tenant edge deployment (one TKE serving several subsidiaries, say) has to be designed for explicitly. Parallel operation is not the default.


2. What This Segment Actually Costs: The Industry Prices by the Point

Building automation and industrial integration share a unit of account: the point. One temperature, one valve state, one meter reading — each is a point. Once you internalise that, it becomes clear what a built-in protocol service removes.

Two sets of publicly listed figures (2026):

China — BA system software pricing, software and commissioning included

Scope Listed price Per point
200 control points ~CNY 18,000 ~CNY 90/point
500 control points ~CNY 26,000 ~CNY 52/point
1,000 control points ~CNY 38,000 ~CNY 38/point
2,000 control points ~CNY 46,000 ~CNY 23/point

The broader industry range for medium projects (roughly 2,000–5,000 points) is about CNY 80–200 per point, covering controllers, sensors, software licences and commissioning; high-spec environments such as hospital clean rooms and data centres push past CNY 300 per point. The two sets differ in scope — the first is mostly software platform, the second includes hardware and installation — so treat both as order-of-magnitude only.

Overseas retrofit pricing (original currency; CNY at 1 USD ≈ 6.71)

Retrofit type Listed price Converted
Equipment with native BACnet/Modbus, connected directly $200–500 / point ~CNY 1,342–3,355 / point
Equipment needing a protocol gateway $500–1,500 / point ~CNY 3,355–10,065 / point
Equipment over 20 years old, controller replacement $1,500–4,000 / point ~CNY 10,065–26,840 / point

The middle row is the one that matters: points that need a protocol gateway cost 2.5 to 3 times what directly connected points cost. A 2026 UK BMS cost guide lands in the same place, quoting £300–600 per connected point and naming point count as the primary cost driver.

That is the value of an embedded protocol service. It moves LoRaWAN data out of the "needs a protocol gateway" bucket and into the "connects directly" bucket. What it saves is not sensor money — it is the 2.5× multiplier on every point, plus the recurring cost of re-running the change process every time the point list moves.


3. BACnet vs Modbus: You Are Delivering Objects, or You Are Delivering Addresses

Both protocols expose platform data to a third-party system. What you hand over is completely different, and that is where selection usually goes wrong.

Dimension BACnet/IP Modbus TCP Server
Data model Object model: Analog / Binary Input, Output, Value Register model: address plus bits or words
Self-describing Yes — object name, description, units, data type No — everything is convention, carried in a spreadsheet
Discovery Broadcast Who-Is / I-Am; the BMS can find devices None; the client must be pre-configured with addresses
Change notification COV subscription (cov_increment threshold) None; the client polls
Write path Write to an Output/Value object → bound RPC fires Write to a register → bound RPC fires
Counterpart Building management systems, DDC controllers SCADA, HMI, PLC
Transport UDP, default 47808 TCP, default 502

One line separates them: BACnet for building automation, Modbus for the industrial side. You can run both at once — a single TKE can publish BACnet objects to the BMS and Modbus registers to SCADA independently. They have separate service permissions and separate ports.

The test is simple. If the other system can scan the network and list point names by itself, you are in the BACnet world. If the other engineer opens with "send me the point list and tell me what 40001 is," you are in the Modbus world.


4. BACnet in Five Steps, in This Order

Step 1 — the system administrator enables the service permission

From 2.00.077, System Platform → System Configuration carries two independent switches, ModbusTcpServer and BACnet, stored in system_param.service_enable (not as a sub-field of config).

Two details matter:

  • Only a JSON boolean false disables it. A missing key, null, or an empty object all default to allowed. Do not leave the field blank.
  • Disabling stops the service, blocks configuration and point-table operations, and prevents both manual start and automatic recovery. Configuration and point tables are retained; re-enabling requires a manual restart — nothing starts on its own.

Step 2 — configure the BACnet fields in the thing model

Before enabling the BACnet service, you must configure BACnet attributes for the fields you want to expose, inside the thing model. The thing model field's unit maps to the BACnet object's units.

This is the most common source of rework: a deliverable thing model has four parts — the parser script, the display fields, the Modbus mapping, and the BACnet fields. A working script that parses data correctly is not the same as having the BACnet fields configured. Article 21 covered the parser script and article 22 covered triggers; this is a fourth, separate block of configuration work.

Step 3 — enable BACnet on the device

Device details → Basic Information, enable BACnet (on the Modbus side the equivalent is setting modbus_tcp to TRUE).

Only devices with this switch on get mapped. When you generate a point table by thing model, it covers only devices already bound to that thing model and already enabled for BACnet — it does not generate objects for every device in the tenant.

Step 4 — generate the point table

Operations → BACnet → Incremental Generate → select the thing model → confirm.

You can also add objects manually. Constraints for the manual form:

Field Requirement
Eui Required; select the device to map
data_from From 2.00.085, free text; defaults to telemetry_data
thing_model Required for telemetry sources only; attribute sources save it as an empty string
field_name Required; a thing model field for telemetry, an attribute path for attribute sources
object_type Defaults to analogOutput; choose according to the actual read/write purpose
object_id Required; 0–4194303
object_name Required; the name the integrator will see
default_value Defaults to 0; lower bound 0
units Defaults to noUnits; mapped from the thing model field's unit when one is selected
cov_increment Required; defaults to 0, lower bound 0; change-notification threshold
description, RPC Optional; bind an RPC when you need write control

From 2.00.077 the "Apply point table" button is gone from the UI. Saving, deleting, importing, incrementally generating and renumbering objects automatically synchronise the running service. The reverse is also true: saving a point table does not start a stopped service — starting is a separate action in the service configuration.

Step 5 — configure service parameters and restart

System Management → Server Configuration → BACnet. Fill in the network and device parameters, click Submit, then click Restart.

Parameter Meaning Reference value Editable
IP address Local IP of the host running TKL; 0.0.0.0 binds all interfaces 0.0.0.0 Yes
Port BACnet listening port 47808 (UDP, i.e. 0xBAC0) Yes
Device ID Unique BACnet device identifier, must be unique in the system 1 Yes
Vendor Identifier Vendor identification number Per actual vendor Yes
Device Name Name shown in the BMS TKE Yes
Max APDU Length Accepted Maximum accepted APDU length 1024 Yes
Segmentation Supported Current version does not enable segmentation through this field As shown in the form Confirm compatibility with the BMS party

Reference values are for field configuration only — they do not mean the new-entry form is pre-filled. Read the existing configuration and confirm each item with the BMS integrator. Changing listening parameters still requires Submit followed by Restart.


5. Three Point-Table Constraints With Long Reach

1. object_id runs 0–4194303, and renumbering changes what the integrator is using

The list offers a "renumber object id" action. It looks like housekeeping. It changes the object identifiers the integrator is already using — the BMS addresses points by object instance number, so one renumbering pass on your side misaligns every point on theirs. After renumbering you must reissue the delivered point list and tell the integrator to reconfigure.

The permitted range is 0–4194303, which reflects the 22-bit object instance number field in BACnet. BACnet device instance numbers must also be unique across the entire BACnet internetwork; duplicates produce intermittent faults that are genuinely unpleasant to diagnose, because the symptom is "sometimes it does not read," not "it never reads."

2. data_from is free text, not an enum

From 2.00.085, BACnet fields carry data_from. Its type is a free string (varchar(32)), not a fixed enum. Three values are suggested:

data_from Meaning and configuration
telemetry_data Default; thing_model required, field read from device.telemetry_data[thing_model]
server_attrs Reads an attribute path from device.server_attrs; no thing model, saved as ""
shared_attrs Reads an attribute path from device.shared_attrs; saved as ""
Any other string Reads from the device's own attributes; falls back to default_value if the source does not exist

Memorise the fallback rule: unknown source, missing value, null, empty string or whitespace all fall back to default_value; but numeric 0 and boolean false are preserved.

Which leads to the important conclusion: a default value is not evidence of the device's real state. A fallback to 0 may mean "the value is 0" or "the source does not exist," and the BMS displays both identically.

3. The unique key is a four-tuple, not a field name

An object's business key is (eui, data_from, thing_model, field_name). BACnet fields in the thing model use the "source + field name" composite key. Same-named fields from different sources can coexist — so do not deduplicate on field name alone; you will delete the wrong one.

Related: adding an attribute object directly does not require a thing model, but incremental generation by thing model still only covers devices bound to that model with BACnet enabled, and will not generate status objects for all devices automatically. If you want an "online" point, add it manually.


6. Turning "Offline" Into a Point the BMS Can Read

A BMS needs to know whether a sensor is still alive. On the ThinkLink side, you expose online status as an attribute point.

Three steps:

  1. In the device's basic information, set notify_offline = "TRUE" and configure the real heartbeat period (confirm the source of the detection state).
  2. Add a BACnet object manually: pick the real device EUI and a non-conflicting object ID/name, set source to server_attrs, field to _online, type to binaryInput, units to noUnits.
  3. Save the point table, confirm the running service synchronised, then check online, timeout-offline and recovery from the BMS.

Key fields when creating it (EUI, object identifier and other required fields also apply):

"data_from": "server_attrs", "thing_model": "", "field_name": "_online", "object_type": "binaryInput", "units": "noUnits", "default_value": 0

One warning belongs in the handover document: a default_value of 0 does not prove the detection result is offline — a missing attribute also returns 0. With offline notification disabled, _online can freeze at its last state.

Online status also has three non-interchangeable meanings on the platform (covered in article 22): the activity time in the device list, server_attrs.online (a number 1/0 maintained by the detector), and the boolean data.online in MQTT notifications. notify_offline is a string switch on the device or template, "TRUE"/"FALSE", disabled by default — when it is not enabled, or missing, you may not treat the device as offline.


7. Modbus TCP: Four Register Types and Ten Encodings

Three configuration steps

  1. The system administrator enables ModbusTcpServer under organisation service permissions.
  2. System Management → Server Configuration → Modbus TCP Server: set the listening port (usually 502), Submit, then Restart.
  3. Device details → Basic Information: set modbus_tcp to TRUE.

Register mapping entries

Operations → Modbus TCP → register list; add mappings for enabled devices:

Field Meaning
Device EUI The device whose data is being mapped
Register type coils (1 bit, R/W) / discrete_inputs (1 bit, read-only) / holding_registers (16 bit, R/W) / input_registers (16 bit, read-only)
Register address Hex; the platform detects conflicts, addresses must be unique within a type
Data source telemetry_data / shared_attrs / server_attrs
Field path (name_path) The field name; multi-level paths supported, e.g. temperature, sensor.value
Value type string, boolean, Int16LE, Int16BE, Int32LE, Int32BE, FloatLE, FloatBE, DoubleLE, DoubleBE; there is no generic number option
RPC binding Optional; when the client writes this register, the bound RPC fires

Three form-filling traps

Addresses are hex, register counts are decimal. Do not mix the two.

The register count is computed from the type and cannot be edited. Selecting FloatBE shows a count of 2. When you allocate the following address, account for the full length occupied — a FloatBE consumes two 16-bit registers, so the next point must start two addresses later. Checking only the start address will overlap them.

LE/BE is an encoding choice and must match how the reader interprets bytes. Choose FloatBE on the platform while the SCADA decodes little-endian and you get a completely wrong number, with no error raised.

Read and write behaviour

Reading: when the client reads a register, ThinkLink returns the current value of the mapped field. The value refreshes on each device uplink — no polling configuration is needed.

Writing: the register must have an RPC bound. ThinkLink calls the bound RPC with the written value as a parameter, in the form { _eui, [field_name]: value }, with the call source marked bacnet_server (the same mechanism applies on the Modbus side).

Typical use: SCADA writes a holding_registers value to change the sampling interval; ThinkLink fires a "set sampling interval" RPC down to the device.


8. The Write Path: Three Things to Accept Separately

This is where integration disputes come from. The documentation is deliberately careful in its wording:

A successful BACnet write does not mean the device has executed it; check the device reply or a subsequent uplink. Write accepted, device execution succeeded, and attribute persistence completed must be verified separately.

The three stages:

  1. Write accepted — the platform received the request and the object value in the point table may already be updated. This completes in seconds.
  2. Device execution succeeded — the RPC reaches the device and the device actually changes the parameter. For a Class A LoRaWAN end device, a downlink can only be sent in the receive windows after an uplink (RX1 at roughly 1 s, RX2 at roughly 2 s). A device that reports every ten minutes may take ten minutes to receive the command.
  3. Attribute persistence completed — the device reports the new value back, it is written to attributes, and the BMS sees the real result on its next read.

A failure at any stage presents as "the BMS shows the write succeeded but the device did not change." If the RPC fails or the device reports its original value, a later attribute notification can push the true cached value back — but that push arrives on the next attribute update, not immediately.

So on the acceptance sheet, write control should be specified as "read back and confirm within N reporting cycles," not "write operation returned success."


9. Network and Ports: 47808 Does Not Work Just Because You Configured It

The networking requirements for BACnet/IP are more specific than most people expect.

UDP 47808, both directions. Firewall rules must permit UDP 47808 inbound and outbound. An outbound-only rule means the workstation can send Who-Is and will never see an I-Am come back.

Broadcast only works inside one broadcast domain. The workstation sends a Who-Is broadcast and only devices on the same Layer-2 segment answer. Put an IP router in between and the router does not forward broadcasts, so discovery fails silently — the workstation reports "nothing found," never "network unreachable." The standard fix for crossing subnets is a BBMD: one per subnet holding devices, each carrying a Broadcast Distribution Table naming its peers. A broadcast is unicast to those peers and re-broadcast locally by each.

A few hard rules when configuring BBMDs: the distribution table must match on every BBMD — a subnet listed in one table and omitted from the others is reachable in one direction only; the BBMD's own address must be static, because foreign-device registrations and peer table entries point at it by address; and two BBMDs on the same subnet is a configuration error, not redundancy, producing duplicate traffic and duplicate I-Am answers.

NAT breaks BACnet/IP. BACnet/IP carries the originating address inside the message payload — in the Forwarded-NPDU a BBMD sends, and in a foreign device registration. NAT rewrites the IP header and leaves the payload alone, so requests get out and replies never arrive. The workable path is a routed or VPN connection, not address translation.

Site-to-site VPNs and wireless bridges frequently drop broadcast. Unicast working is not evidence that broadcast works; supervision across such a link normally needs foreign-device registration rather than plain discovery.

One deployment-level note. The TKE Docker deployment guide lists host inbound ports (80/8080, 443, 18083, 18084, 1883, 8883, 8083, 8084, 1700 UDP, 9000, 9001) and does not include 47808 or 502. The installer does not create mappings or allow rules for these two services; the deployment team has to confirm container port mapping, host firewall and security group rules themselves. Miss this and the symptom is "the platform says the service is running, but the BMS finds nothing."

One last item is a security requirement rather than an option: BACnet was designed for isolated control networks, and much of the installed base has little or no authentication. Once the control network is reachable from the business network, that assumption no longer holds. Both UDP 47808 and TCP 502 should be reachable only from the building automation or SCADA segment — never from the public internet.


10. Verifying With YABE, and Delivering the Point List

A five-minute loop with YABE

YABE (Yet Another BACnet Explorer) is the common tool for browsing and debugging BACnet devices. Once configured, connect YABE to ThinkLink to discover the published BACnet devices and their object properties, and confirm the data is being published correctly.

Work through the checks in this order; each one isolates a different class of problem:

Check Pass criterion First suspect if it fails
Device discovery YABE sees the Device ID and Device Name UDP 47808 blocked / no BBMD across subnets / service not restarted
Object list Count and object_ids match the point table BACnet not enabled on the device / thing model BACnet fields not configured
Read value presentValue matches platform telemetry Wrong field name or source / value fell back to default
Units units shows the expected unit (e.g. degreesCelsius) unit not filled in the thing model field
COV Notification arrives when change exceeds the threshold cov_increment set to 0
Write control Device read-back changes after writing an Output/Value No RPC bound / Class A downlink window not reached

Modbus has no general-purpose browser equivalent; you typically use a Modbus master utility (Modbus Poll and similar) to read addresses one by one against the point list. Focus on two things: the address convention (hex vs decimal) and byte order (LE vs BE).

Delivering the point list

Both BACnet and Modbus point lists export to Excel and import back for bulk configuration. The export contains object type, address, name, description, data type, units and read/write permissions — exactly what the integrator needs to configure their client.

One caveat on import: entries with an existing ID are overwritten. This is the same class of risk as the Excel migration traps covered in article 23 — export a backup before importing, because the overwrite cannot be rolled back.

Two delivery recommendations:

  1. Version the point list. Record the point list version alongside the thing model version and the platform version. If BACnet object identifiers shift, every point on the BMS side is misaligned, and retrospective investigation costs far more than keeping a record.
  2. Deliver first, then commission together. Hand the point list to the integrator, let them configure their client, then commission jointly. That is much faster than adjusting the point list during commissioning. Point-table saves do synchronise the running service automatically, but every change forces the other side to reconfigure.

11. Acceptance Checklist

# Item Pass criterion
1 Deployment form Confirmed TKE or TKG; Cloud exposes neither BACnet nor Modbus
2 Service permission Both BACnet and ModbusTcpServer explicitly true in system configuration
3 Thing model All four blocks done: parser script, display fields, Modbus mapping, BACnet fields
4 Device switch BACnet / modbus_tcp enabled on target devices
5 Service parameters Device ID unique network-wide; Port agreed with the BMS party; submitted and restarted
6 Port reachability UDP 47808 open both directions (BBMD deployed if crossing subnets); TCP 502 open to the control segment only
7 Point list completeness Object/address, name, data type, units, R/W permission all present; write points have RPCs bound
8 Read correctness YABE / Modbus master reads match platform telemetry, units correct
9 Status point _online point exists and online, timeout-offline and recovery have all been tested
10 COV cov_increment set to a real precision value, not 0
11 Write control Confirmed by read-back over reporting cycles, not by write success alone
12 Runtime monitoring No point-table sync errors or retries; service restarts automatically after abnormal exit, and does not resurrect after a manual stop
13 Deliverables Point list Excel exported and versioned; backup taken before any import

12. Closing

Getting LoRaWAN data into a BMS or SCADA is not fundamentally hard — the protocols are public and a bridge can always be written. The real cost is that every point has to be configured by someone, checked by someone, and changed again by someone throughout the project's life. The industry prices by the point; points that need a protocol gateway cost 2.5 to 3 times what direct points cost. That number prices integration labour, not software features.

ThinkLink's built-in BACnet/IP and Modbus TCP on TKE and TKG remove that segment. What they do not remove is engineering discipline: BACnet is available only in the self-hosted forms, one process serves one organisation, renumbering a point list breaks the integrator's configuration, a successful write is not a successful execution, and UDP 47808 needs two-way rules plus a BBMD once you cross a subnet.

All of these are documented. They are just spread across different pages. Collecting them into an acceptance checklist is the cheapest way to get the integration right the first time.


Related reading

  • Article 21: ThinkLink Thing Model Decoder Scripts in Practice — Turning 15 Bytes Into a Readable Table
  • Article 22: ThinkLink Trigger Models and Alarm Rules in Practice — On Time, Not Spammy, To the Right Person
  • Article 23: ThinkLink Edge On-Premises Deployment in Practice — What "Data Never Leaves the Plant" Actually Requires

Based on official ThinkLink documentation (versions 2.00.077–2.00.085). Configuration paths and parameters follow your deployed version. Official manuals: https://www.think-link.net/docs/en/Maintenance/BACnet.html https://www.think-link.net/docs/en/Maintenance/ModbusTCP.html https://www.think-link.net/docs/en/QuickStart/SystemManagement/ServerConfiguration.html