The Fieldbus Anchor links anchors into a CAN-FD daisy chain for factory RTLS and automation, with no per-anchor network drop.
One anchor is the master; it aggregates range data and status from the whole chain and uplinks it over WiFi / MQTT. Every other anchor is a slave that reports to the master over the bus. (The Vehicle Anchor uses the same chain in CAS-PDS installations.)
The transport is selected per anchor by the connectivity-mode parameter (0x511D): 0 WiFi → MQTT, 1 RS-485 daisy chain, 2 CAN daisy chain.
Each anchor has a 16-bit anchor ID, taken from the last four decimal digits of its LocatorID — so the CAN address, the device's BLE name and its MQTT LocatorID all stay aligned. Reserved values:
| Anchor ID | Meaning |
|---|---|
0x0000 | Broadcast — every anchor accepts the frame |
0xFFFF | Master |
0x0001–0xFFFE | Slave anchors |
Only the master publishes to MQTT. A slave reaches the broker through the master and still appears under its own LocatorID, so every anchor is a distinct reporter regardless of bus position — see CAN-to-MQTT gateway.
Every frame uses a 29-bit extended identifier:
| Bits | Field | Width |
|---|---|---|
[28:25] | Message type | 4 bits |
[24:9] | Anchor ID (source or target) | 16 bits |
[8:0] | Reserved (always 0) | 9 bits |
0x020BF000.A lower numeric CAN id wins arbitration, so putting the message type in the top four bits makes it the primary bus priority — a time-sync frame outranks range data, which outranks a heartbeat, and so on. Within one type, the lower anchor ID wins.
The anchor ships in two hardware variants, selected by parameter 0x511E:
All frames use the extended identifier with bit-rate switching disabled (brs = false). Bus bit rate is set by parameter 0x511F: 0 = 1 Mbps, 1 = 500 k, 2 = 250 k, 3 = 125 k (default), 4 = 50 k.
Every chain must be terminated with 120 Ω at both ends. Not one end, and not a resistor somewhere in the middle — one at each physical extremity of the bus. Without correct termination a chain may be intermittently unstable or may not communicate at all, and the symptom is easily mistaken for a firmware or configuration fault. Check termination first when a chain misbehaves.
An unterminated bus reflects each edge back from the open end. The reflection collides with the signal still on the wire, and whether it corrupts a given bit depends on cable length, temperature and which node is transmitting — which is why a badly terminated chain often works on the bench and fails once installed.
Chains in real deployments run to 8 anchors — one master and seven slaves:
| Anchors per chain | up to 8 — 1 master + 7 slaves |
| Spacing between anchors | approximately 40–50 m |
| Resulting bus length | roughly 280–350 m end to end |
Two independent constraints set the bit rate, and the lower of the two wins.
Bus length. CAN arbitration requires a signal to reach the far end of the bus and return within a single bit time, so the maximum length falls as the bit rate rises. This is why a rate that works on a short bench setup can fail on an installed chain of the same anchors — and why the default is the slowest useful setting rather than the fastest.
Traffic. Only slaves put range data on the bus; the master's own measurements go straight to the uplink. The frame rate is therefore:
frames/s = slaves × tracked units × TWR rate
Keep-alives and parameter traffic add little — a heartbeat every 15 seconds per anchor is negligible beside range data.
A deployed factory chain:
| Anchors | 8 — 1 master, 7 slaves |
| Equipment units tracked | 7 |
| Ranging rate | 2 Hz (UWB TWR) |
| Range frames on the bus | 7 × 7 × 2 = 98 frames/s |
| Bit rate | 125 kbit/s |
This configuration runs correctly. It is a useful reference point in both directions: a chain with more anchors, more tracked units or a higher ranging rate multiplies the frame count and may need a faster bus, while a longer run may not tolerate one — at which point the ranging rate, not the bit rate, is the parameter to reduce.
Seven types are defined; 7–15 are reserved.
| Type | Name | Direction | Payload |
|---|---|---|---|
0 | Sync | — | Reserved (time-sync pulse; not used) |
1 | RTLS Data | slave → master | 24 B |
2 | Keep Alive | slave → master | 12 B (16 B with reboot count) |
3 | Param Read Request | master → slave | 12 B |
4 | Param Write Request | master → slave | 48 B |
5 | Param Response | slave → master | 48 B |
6 | Time Config | master → all | 8 B |
All multi-byte integers are little-endian unless noted.
tag_mac is big-endian so it reads in the same order as a printed MAC address, while the numeric fields are little-endian.| Offset | Field | Type | Notes |
|---|---|---|---|
0..5 | tag_mac | 48-bit | UWB MAC, big-endian |
6..9 | distance_cm | int32 | centimetres; negative = error |
10..17 | utc_ms | uint64 | ms since Unix epoch |
18..23 | — | — | zero-padded |
status_code is the anchor's current fault mask; error_count counts error onsets since boot. A reboot_count of 0 indicates the sender does not report the field.| Offset | Field | Type | Notes |
|---|---|---|---|
0..1 | status_code | uint16 | fault bitmask, 0 = OK |
2..3 | error_count | uint16 | cumulative error-onset counter since boot |
4..11 | utc_ms | uint64 | ms since Unix epoch |
On the CAN-FD variant the heartbeat may extend to 16 B, appending reboot_count (uint32) at offset 12. A value of 0 means the sender's firmware does not report it — not that the device never rebooted.
| Offset | Field | Type |
|---|---|---|
0..1 | anchor_id (target slave) | uint16 |
2..3 | param_id | uint16 |
4..11 | utc_ms | uint64 |
param_id and the value now in effect, after either a read or a write.| Offset | Field | Type | Notes |
|---|---|---|---|
0..1 | anchor_id | uint16 | target (write) or responder (response) |
2..3 | param_id | uint16 | echoed in the response |
4..39 | param_value | 36 B | value bytes, zero-padded |
40..47 | utc_ms | uint64 |
| Offset | Field | Type |
|---|---|---|
0..7 | utc_ms | uint64 |
status_code is an OR-combined fault mask (0x0000 = all nominal):
| Bit | Value | Condition |
|---|---|---|
| 0 | 0x0001 | NTP not synchronised (master) |
| 1 | 0x0002 | WiFi error (master) |
| 2 | 0x0004 | Server / MQTT error (master) |
| 3 | 0x0008 | CAN error |
| 4 | 0x0010 | UWB module error |
| 6 | 0x0040 | BLE module error |
| 7 | 0x0080 | Configuration error |
Bits 0–2 concern the master's uplink (WiFi / NTP / server); a CAN slave has none of those, so they stay 0 on a slave. Bit 5 is reserved.
The master distributes UTC with a Time Config broadcast. Beyond periodic retransmission, it auto-broadcasts whenever a received slave frame carries a UTC that differs from the master's by 30 s or more, pulling drifting slaves back into sync without an application timer. Time Config is the lowest-priority type, so it never delays ranging.
A slave's configuration is read and written over the bus with the Param Read / Write / Response frames. This is one parameter-ID space shared across GATT, CAN and MQTT — a setting has the same ID and the same validation path on every transport.
| ID | Parameter |
|---|---|
0x05 | Device name |
0x06 | UWB RTLS method (1 TWR, 2 TDoA) |
0x07 | Adaptive transmit |
0x08, 0x09 | Active / inactive blink rate |
0x0A–0x0E | UWB channel, data rate, preamble, PRF, PAN |
0x0F, 0x10 | Enable search TWR, max anchors (TWR) |
0x13–0x15 | WiFi SSID, WiFi password, server address |
0x16 | Boarding mode |
0x17, 0x18 | MAC filter 1 / 2 |
0x19 | Anchor role (master / slave) |
0x1B, 0x1C | Operating mode, tag/anchor role |
0x1D | Connectivity mode (0 WiFi, 1 RS-485, 2 CAN) |
0x1E, 0x1F | CAN controller variant, CAN bit rate |
0x20 | Keep-alive period (s); 0 = firmware default |
0x64 | Restart |
0x70 | TDMA / BLE-DM status (read-only diagnostics) |
0x71–0x76 | TDMA: enable, master, superframe µs, slot count, CAP slots, sensor slots |
0x77, 0x78 | Sensor enable, sensor mode (bit0 IMU raw, bit1 baro raw) |
0x79 | BLE ranging mode (0 RTT, 1 MCPD) |
0x7A | Ranging technology (0 UWB, 1 UWB+BLE, 2 BLE) |
0x7B, 0x7C | BLE DM slots, sensor carrier (0 UWB, 1 BLE ext-adv) |
0x7D | Node mode — combined operating mode + role |
0x84 | Battery present |
Node mode (0x7D) is the preferred single-write control that sets operating mode and role together: 0 RTLS anchor, 1 RTLS tag, 2 CAS-PDS anchor, 3 CAS-PDS tag. It takes effect after restart.
Parameter writes persist to flash and survive a power cycle, except the TDMA and sensor runtime parameters (0x71–0x7D), which live in RAM and revert to their defaults on reboot — so a bad commissioning value can never strand a remote anchor; a power cycle always returns it to plain ranging. UWB radio parameters (channel, data rate, preamble, PRF) persist immediately but apply only after the next restart.
Only the master anchor has a network uplink. Slaves have no route to the broker of their own — they put their measurements on the CAN bus, and the master relays them. That relay is the CAN-to-MQTT gateway.
The master polls the bus and forwards three frame types; the rest are bus-local and produce no MQTT traffic:
| CAN frame | Becomes |
|---|---|
| RTLS Data — type 1 | {prefix}/Locators/{LocatorID}/RTLS |
| Keep Alive — type 2 | {prefix}/Locators/{LocatorID}/KeepAlive |
| Param Response — type 5 | {prefix}/Locators/{LocatorID}/ParamResponse |
| Sync, Time Config — types 0, 6 | nothing — bus-local |
Parameter requests travel the other way: the master subscribes to ParamRead and ParamWrite, and relays each onto the bus as a type 3 or type 4 frame addressed to the target anchor.
The master's own UWB measurements go through the same path as a slave's, published under its own anchor ID rather than through a separate code path. A backend therefore sees one schema for every anchor in the chain, and nothing in the message reveals which anchor happened to hold the uplink. Moving the uplink to a different anchor changes no payload a consumer sees.
A CAN frame carries only a 16-bit anchor ID, which is the last four decimal digits of the anchor's LocatorID. The gateway reverses that to rebuild the serial-style identity for the topic:
anchor_id 1528 → "ZP1528" → {prefix}/Locators/ZP1528/RTLS
Without this step every slave would be published under the master's identity and the chain would collapse into one apparent device. The ZP prefix is fixed by convention.
Each frame carries the measuring anchor's own utc_ms. The gateway does not overwrite it — a measurement keeps the time of the anchor that took it. It is validated rather than trusted:
Frame utc_ms | Published |
|---|---|
| plausible | the frame's own value |
| implausible, gateway has network time | the gateway's time |
| implausible, gateway has none either | 0 |
The sentinel matters: an unsynced anchor would otherwise publish milliseconds-since-boot, which is a small number that parses as 1970 and silently lands decades in the past. 0 is unambiguous — a consumer treats it as no time rather than as a date.
Slave anchor ZP1528 ranges a tag at 4.37 m and puts one type 1 frame on the bus:
CAN ID 0x020BF000 type 1 (RTLS Data), anchor 1528, 29-bit extended
payload DE AD BE EF 00 17 tag_mac de:ad:be:ef:00:17
B5 01 00 00 distance_cm 437
8B D0 D0 AD 98 01 00 00
utc_ms 1755262800011
00 00 00 00 00 00 padding
The master receives it, rebuilds ZP1528 from anchor ID 1528, and publishes:
topic {prefix}/Locators/ZP1528/RTLS
{
"LocatorID": "ZP1528",
"TagID": "de:ad:be:ef:00:17",
"DeviceType": "uwb",
"Distance_cm": 437,
"TimestampMs": 1755262800011
}
Note the endianness change across the boundary: on the bus tag_mac is big-endian while distance_cm and utc_ms are little-endian. In JSON all three are already decoded, so a consumer never deals with byte order — that is confined to the CAN side.