Wiki

Technical reference for system integrators and engineers.

CAN-FD daisy chain

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.

Topology and addressing

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 IDMeaning
0x0000Broadcast — every anchor accepts the frame
0xFFFFMaster
0x00010xFFFESlave 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.

CAN identifier — 29-bit extended

Every frame uses a 29-bit extended identifier:

BitsFieldWidth
[28:25]Message type4 bits
[24:9]Anchor ID (source or target)16 bits
[8:0]Reserved (always 0)9 bits
29-bit extended identifier msg_type 4 bits anchor_id 16 bits — source or target reserved 9 bits — always 0 bit 28 25 24 9 8 0 Example — RTLS Data from anchor 1528 0x1 0x05F8 (1528) 0
The message type occupies the most-significant bits, so it also sets bus priority. Full identifier for the example above: 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.

Controller variants

The anchor ships in two hardware variants, selected by parameter 0x511E:

  • CAN-FD — the full frame set below (up to 48-byte payloads).
  • Classic CAN 2.0B — 8-byte frames only. Range frames carry a 16-bit distance and no timestamp; the heartbeat omits the reboot counter; the time-sync frame is unchanged (it is 8 bytes by design).

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.

Physical bus

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.

Deployment scale

Chains in real deployments run to 8 anchors — one master and seven slaves:

Anchors per chainup to 8 — 1 master + 7 slaves
Spacing between anchorsapproximately 40–50 m
Resulting bus lengthroughly 280–350 m end to end

Choosing the bit rate

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.

Worked example

A deployed factory chain:

Anchors8 — 1 master, 7 slaves
Equipment units tracked7
Ranging rate2 Hz (UWB TWR)
Range frames on the bus7 × 7 × 2 = 98 frames/s
Bit rate125 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.

Message types

Seven types are defined; 715 are reserved.

TypeNameDirectionPayload
0SyncReserved (time-sync pulse; not used)
1RTLS Dataslave → master24 B
2Keep Aliveslave → master12 B (16 B with reboot count)
3Param Read Requestmaster → slave12 B
4Param Write Requestmaster → slave48 B
5Param Responseslave → master48 B
6Time Configmaster → all8 B

Frame formats

All multi-byte integers are little-endian unless noted.

RTLS Data — type 1, 24 B

Payload — 24 bytes tag_mac 6 B · big-endian distance_cm int32 · LE utc_ms uint64 · LE padding 6 B · zero byte 0 6 10 18 23 Sample — tag at 4.37 m DE AD BE EF 00 17 B5 01 00 00 8B D0 D0 AD 98 01 00 00 00 … tag de:ad:be:ef:00:17 437 cm 1755262800011 ms
Note the deliberate mixed endianness: tag_mac is big-endian so it reads in the same order as a printed MAC address, while the numeric fields are little-endian.
OffsetFieldTypeNotes
0..5tag_mac48-bitUWB MAC, big-endian
6..9distance_cmint32centimetres; negative = error
10..17utc_msuint64ms since Unix epoch
18..23zero-padded

Keep Alive — type 2, 12 B

Payload — 12 bytes (16 B with reboot count) status_code uint16 · fault mask error_count uint16 · cumulative utc_ms uint64 · LE reboot_count uint32 · CAN-FD only byte 0 2 4 12 15 Sample — UWB module fault on a slave 10 00 07 00 8B D0 D0 AD 98 01 00 00 03 00 00 00 0x0010 = UWB error 7 error onsets 1755262800011 ms 3 boots
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.
OffsetFieldTypeNotes
0..1status_codeuint16fault bitmask, 0 = OK
2..3error_countuint16cumulative error-onset counter since boot
4..11utc_msuint64ms 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.

Param Read Request — type 3, 12 B

Payload — 12 bytes anchor_id uint16 · target slave param_id uint16 utc_ms uint64 · LE byte 0 2 4 11 Sample — read CAN bit rate from anchor 1528 F8 05 1F 00 8B D0 D0 AD 98 01 00 00 anchor 1528 0x1F = CAN bit rate
The target anchor also appears in the CAN identifier; the copy in the payload is available to the application layer for validation.
OffsetFieldType
0..1anchor_id (target slave)uint16
2..3param_iduint16
4..11utc_msuint64

Param Write Request & Param Response — types 4 & 5, 48 B

Payload — 48 bytes (identical layout for both types) anchor_id uint16 param_id uint16 param_value 36 B · zero-padded utc_ms uint64 · LE byte 0 2 4 40 47 Sample — set CAN bit rate to 500 k on anchor 1528 F8 05 1F 00 01 00 00 … 00 8B D0 D0 AD … value 1 = 500 kbps, remaining 35 bytes zero
The slave replies with a Param Response carrying the same param_id and the value now in effect, after either a read or a write.
OffsetFieldTypeNotes
0..1anchor_iduint16target (write) or responder (response)
2..3param_iduint16echoed in the response
4..39param_value36 Bvalue bytes, zero-padded
40..47utc_msuint64

Time Config — type 6, 8 B

Payload — 8 bytes (classic CAN compatible) utc_ms uint64 · little-endian · ms since Unix epoch byte 0 7 Drift-triggered rebroadcast master slave any slave frame carrying utc_ms |Δt| ≥ 30 s → Time Config broadcast
Beyond periodic retransmission, the master broadcasts automatically when a received slave timestamp differs from its own by 30 s or more. Type 6 has the lowest bus priority.
OffsetFieldType
0..7utc_msuint64

Keep-Alive status bitmask

status_code is an OR-combined fault mask (0x0000 = all nominal):

BitValueCondition
00x0001NTP not synchronised (master)
10x0002WiFi error (master)
20x0004Server / MQTT error (master)
30x0008CAN error
40x0010UWB module error
60x0040BLE module error
70x0080Configuration 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.

Time synchronisation

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.

Remote parameter management

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.

IDParameter
0x05Device name
0x06UWB RTLS method (1 TWR, 2 TDoA)
0x07Adaptive transmit
0x08, 0x09Active / inactive blink rate
0x0A0x0EUWB channel, data rate, preamble, PRF, PAN
0x0F, 0x10Enable search TWR, max anchors (TWR)
0x130x15WiFi SSID, WiFi password, server address
0x16Boarding mode
0x17, 0x18MAC filter 1 / 2
0x19Anchor role (master / slave)
0x1B, 0x1COperating mode, tag/anchor role
0x1DConnectivity mode (0 WiFi, 1 RS-485, 2 CAN)
0x1E, 0x1FCAN controller variant, CAN bit rate
0x20Keep-alive period (s); 0 = firmware default
0x64Restart
0x70TDMA / BLE-DM status (read-only diagnostics)
0x710x76TDMA: enable, master, superframe µs, slot count, CAP slots, sensor slots
0x77, 0x78Sensor enable, sensor mode (bit0 IMU raw, bit1 baro raw)
0x79BLE ranging mode (0 RTT, 1 MCPD)
0x7ARanging technology (0 UWB, 1 UWB+BLE, 2 BLE)
0x7B, 0x7CBLE DM slots, sensor carrier (0 UWB, 1 BLE ext-adv)
0x7DNode mode — combined operating mode + role
0x84Battery 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 (0x710x7D), 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.

CAN-to-MQTT gateway

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 frameBecomes
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, 6nothing — 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 is not a special case

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.

Identity

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.

Timestamps

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_msPublished
plausiblethe frame's own value
implausible, gateway has network timethe gateway's time
implausible, gateway has none either0

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.

Worked example

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.