TG-BT5-XX tags can operate in 4 different modes:
In advertising mode, the tag will broadcast information about itself in Bluetooth advertising packets. The information depends on the advertising packet type.
At the moment, these are all the supported types that can be configured using the MikroTik Beacon Manager app:
Eddystone-TLM, Eddystone-UID, MikroTik, and iBeacon.
Bluetooth technology uses 2 types of channels (each using different frequencies) during the data exchange.
There are 40 unique bands (channels) and each band has a 2 MHz separation. 37, 38, and 39 channels are used for advertising, and 0-36 are used for data transmission.
During the advertising process, the BLE advertising packet is broadcasted. This packet contains the Preamble, Access Address, PDU and CRS fields.
The Preamble and Access Address fields help the receiver detect frames. CRS field is used to check errors. PDU defines the packet itself.
MikroTik tags support legacy Non-Connectable Non-Scannable Undirected advertising (ADV_NOCONN_IND
). The Payload, in this case, consists of "AdvA" (a field that contains information about the advertiser's address) and "AdvData" (a field that contains data information) fields.
1 octet = 1 byte = 8 bits
Preamble | 1 octet |
Access-Address | 4 octets |
PDU |
|
CRS | 3 octets |
"AdvData" field structure (max 31 octets/bytes):
ManufacturerData | company identifier | 4 octets (15FF4F09 ) |
Version | the version of this advertisement structure | 1 octet (uint) |
UserData | user-configured part of the payload | 1 octet (uint) |
Secret | optionally encrypted (AES-ECB) part of the payload |
|
Please note that all multi-byte values are in little-endian. Meaning, that if, for example, you want to get the temperature value and #14 and #15 octets indicate the temperature as "a1 19" ("plus" temperature) → the real temperature value is going to be (0x19a1)/256 = 25.6 C. |
A detailed example of how you can convert hexadecimal payload's values to decimal values will be shown later in the PDU payload structure section.
UserData and Secret fields are configured with the help of flags
. In the "UserData" section, the parameter that controls whether the "Secret" is encrypted or not is called FLAG_ENCRYPTED
. When FLAG_ENCRYPTED=0
, it means the secret is not encrypted (1st bit in 6th octet would be set to 0), and when FLAG_ENCRYPTED=1
, it means the secret is encrypted (1st bit in 6th octet would be set to 1).
In the "Secret" section, there are 6 flags
(21st octet):
FLAG_REED_SWITCH
(1st bit - if set to 1, shows that the reed switch was closed at the moment of advertising)FLAG_ACCEL_TILT
(2nd bit - if set to 1, shows that the advertisement was sent by tilting the device)FLAG_ACCEL_FREE_FALL
(3d bit - if set to 1, shows that the advertisement was sent by dropping the device)FLAG_IMPACT_X
(4th bit - if set to 1, shows that there was an impact on the x-axis at the moment of advertising)FLAG_IMPACT_Y
(5th bit - if set to 1, shows that there was an impact on the y-axis at the moment of advertising)FLAG_IMPACT_Z
(6th bit - if set to 1, shows that there was an impact on the z-axis at the moment of advertising)For example, if you see that the 21st octet of the hex message is "02" (when converting the value "02" from hexadecimal to binary it is "0010" → 2nd bit is set to 1) → it means that the device was tilted. If you see "04" (hex "04" to bin is "0100" → 3d bit is set to 1) → it means that the device was dropped (freefalling triggered). If you see "38" (hex to bin it is "00111000" → 4th, 5th, and 6th bits are set to 1) → it means that when the advertisement was sent, the accelerometer detected an impact/wake-up on all 3 x/y/z-axis.
More examples (for 21 octet's value):
0 | 15 | ManufacturerData | company identifier |
1 | FF | ManufacturerData | company identifier |
2 | 4F | ManufacturerData | company identifier |
3 | 09 | ManufacturerData | company identifier |
4 | 01 | Version | the version of this advertisement structure |
5 | 00 | UserData | user-configured part of the payload |
6 | xx* | Secret | secret: salt |
7 | xx* | Secret | secret: salt |
8 | xx* | Secret | secret: acceleration on the X-axis |
9 | xx* | Secret | secret: acceleration on the X-axis |
10 | xx* | Secret | secret: acceleration on the Y-axis |
11 | xx* | Secret | secret: acceleration on the Y-axis |
12 | xx* | Secret | secret: acceleration on the Z-axis |
13 | xx* | Secret | secret: acceleration on the Z-axis |
14 | xx* | Secret | secret: temperature |
15 | xx* | Secret | secret: temperature |
16 | xx* | Secret | secret: uptime |
17 | xx* | Secret | secret: uptime |
18 | xx* | Secret | secret: uptime |
19 | xx* | Secret | secret: uptime |
20 | 00 | Secret | secret: flags |
21 | 64 | Secret | secret: batteryPercentage |
* - can vary
An example of the payload configured in MikroTik's format (non-encrypted) would be:
15ff4f090100cea6000000000200a01c91085700005f |
---|
15ff4f09 (first 4 octets) → ManufacturerData. Is the same (constant data) for every MikroTik format's payload.
01 (4th octet) → Current version of the payload's structure. Should be the same for every payload (constant data).
00 (5th octet) → Indicates that the payload is not encrypted. "01" would mean it is encrypted.
cea6 (6th and 7th octets) → Salt. Each new payload should have a different salt value generated. You can use this value to check whether the identical payloads are encrypted differently. The value itself does not contain any useful information. If you see that the salt value is identical for two payloads received during different time intervals, it would mean that the two payloads received are exactly identical. You can calculate the salt value using the same principle that applies to the uptime calculation (17th to 20th octets) - see below.
0000 (8th and 9th octets) → acceleration on the X-axis at the moment of the broadcast = 0 m/s2. Check acceleration calculation for the Z-axis below.
0000 (10th and 11th octets) → acceleration on the Y-axis at the moment of the broadcast = 0 m/s2. Check acceleration calculation for the Z-axis below.
0200 (12th and 13th octets) → acceleration on the Z-axis at the moment of the broadcast = 0.0078 m/s2. To get the decimal value out of the hex format you will need to follow the steps:
a01c (14th and 15th octets) → temperature detected by the tag in Celsius = 28.625 C. Temperature is in little-endian (as it is a multi-byte value) and it is in signed 16-bit integer [twos complement] 8.8 fixed point format, so the same "formula" applies here as well:
91085700 (16th to 19th octets) → tag's uptime in seconds = 5703825 s. 0x91085700 is in little-endian, so just swap the octets to 0x00570891 and the result is 5703825 in decimal. That is 1584.395833 hours or 66-day uptime.
00 (20th octet) → trigger (flag) that sent the payload. If it is "00" it means that no trigger was detected and that it is just a periodically broadcasted payload (based on the advertisement interval configured for the tag). If the value would be "04" it would mean that the device was dropped (freefalling triggered). You can find more information on the "flags" and the "Secret" section above in the packet structure section.
5f (21st octet) → battery percentage of the tag = 95 %. 0x5f from hex to dec is 95.
Add a new script under the "System>Scripts" tab and import the script there (for non-encrypted payloads).
# POSIX regex for filtering advertisement Bluetooth addresses. E.g. "^BC:33:AC"
# would only include addresses which start with those 3 octets.
# To disable this filter, set it to ""
:local addressRegex "2C:C8:1B:4B:BB:0A"# POSIX regex for filtering Bluetooth advertisements based on their data. Same
# usage as with 'addressRegex'.
:local advertisingDataRegex ""# Signal strength filter. E.g. -40 would only include Bluetooth advertisements
# whose signal strength is stronger than -40dBm.
# To disable this filter, set it to ""
:local rssiThreshold ""################################## Bluetooth ##################################
:global invertU16 do={
:local inverted 0
:for idx from=0 to=15 step=1 do={
:local mask (1 << $idx)
:if ($1 & $mask = 0) do={
:set $inverted ($inverted | $mask)
}
}
return $inverted
}:global le16ToHost do={
:local lsb [:pick $1 0 2]
:local msb [:pick $1 2 4]:return [:tonum "0x$msb$lsb"]
}:local le32ToHost do={
:local lsb [:pick $1 0 2]
:local midL [:pick $1 2 4]
:local midH [:pick $1 4 6]
:local msb [:pick $1 6 8]:return [:tonum "0x$msb$midH$midL$lsb"]
}:local from88 do={
:global invertU16
:global le16ToHost
:local num [$le16ToHost $1]# Handle negative numbers
:if ($num & 0x8000) do={
:set num (-1 * ([$invertU16 $num] + 1))
}# Convert from 8.8. Scale by 1000 since floating point is not supported
:return (($num * 125) / 32)
}:local flagStr do={
:local str "":if ($1 & 0x01) do={ :set $str " switch" }
:if ($1 & 0x02) do={ :set $str "$str tilt" }
:if ($1 & 0x04) do={ :set $str "$str free_fall" }
:if ($1 & 0x08) do={ :set $str "$str impact_x" }
:if ($1 & 0x10) do={ :set $str "$str impact_y" }
:if ($1 & 0x20) do={ :set $str "$str impact_z" }:if ([:len $str] = 0) do={ :return "" }
:return [:pick $str 1 [:len $str]]
}# Find fresh Bluetooth advertisements
:global btOldestAdvertisementTimestamp
:if ([:typeof $btOldestAdvertisementTimestamp] = "nothing") do={
# First time this script has been run since booting, need to initialize
# persistent variables
:set $btOldestAdvertisementTimestamp 0
}
:local advertisements [/iot bluetooth scanners advertisements print detail \
as-value where \
epoch > $btOldestAdvertisementTimestamp and \
address ~ $addressRegex and \
data ~ $advertisingDataRegex and \
rssi > $rssiThreshold
]
:local advCount 0
:local lastAdvTimestamp 0
:local advJson ""
:local advSeparator ""# Remove semicolons from MAC/Bluetooth addresses
:local minimizeMac do={
:local minimized
:local lastIdx ([:len $address] - 1)
:for idx from=0 to=$lastIdx step=1 do={
:local char [:pick $address $idx]
:if ($char != ":") do={
:set $minimized "$minimized$char"
}
}
:return $minimized
}:foreach adv in=$advertisements do={
:local address ($adv->"address")
:local rssi ($adv->"rssi")
:local epoch ($adv->"epoch")
:local ad ($adv->"data")
:local version [:tonum "0x$[:pick $ad 8 10]"]
:local encrypted [:tonum "0x$[:pick $ad 10 12]"]
:local salt [$le16ToHost [:pick $ad 12 16]]
:local accelX [$from88 [:pick $ad 16 20]]
:local accelY [$from88 [:pick $ad 20 24]]
:local accelZ [$from88 [:pick $ad 24 28]]
:local temp [$from88 [:pick $ad 28 32]]
:local uptime [$le32ToHost [:pick $ad 32 40]]
:local flags [:tonum "0x$[:pick $ad 40 42]"]
:local bat [:tonum "0x$[:pick $ad 42 44]"]:put ("$advCount: \
address=$address \
ts=$epoch \
rssi=$rssi \
version=$version \
encrypted=$encrypted \
salt=$salt \
accelX=$accelX \
accelY=$accelY \
accelZ=$accelZ \
temp=$temp \
uptime=$uptime \
flags=\"$[$flagStr $flags]\" \
bat=$bat" \
)
:set $advCount ($advCount + 1)
:set $lastAdvTimestamp $epoch
}
:if ($advCount > 0) do={
:set $btOldestAdvertisementTimestamp $lastAdvTimestamp
}
The only line that you need to alter is the:
:local addressRegex "2C:C8:1B:4B:BB:0A"
line, where you need to input the MAC address of the tag.
Save the script with whichever name you like, for example, decode.
Run the script via the command line interface ("New Terminal" button in Winbox/Webfig):
[admin@MikroTik] > system script run decode 0: address=2C:C8:1B:4B:BB:0A ts=1662553431348 rssi=-45 version=1 encrypted=0 salt=57919 accelX=3 accelY=-35 accelZ=-70 temp=25535 uptime=1046174 flags="" bat=99 1: address=2C:C8:1B:4B:BB:0A ts=1662553436349 rssi=-40 version=1 encrypted=0 salt=24154 accelX=-19 accelY=-23 accelZ=0 temp=25546 uptime=1046179 flags="" bat=99 2: address=2C:C8:1B:4B:BB:0A ts=1662553446351 rssi=-37 version=1 encrypted=0 salt=37822 accelX=-15 accelY=35 accelZ=15 temp=25550 uptime=1046189 flags="" bat=99 |
As you can see from the example above, the script will "translate" all payloads from a hexadecimal format to a decimal format and print them into the terminal.
You can also alter the script further to structure a message out of the "already decoded" values and post it to an EMAIL, MQTT, or HTTP server of your choice but! please keep in mind that it might load the device more. So you need to test the performance when running the script. It will be easier on RouterOS resources when the decoding is done on the server side.
Because of the fact that floating point is not supported → every calculation behind a decimal point will be "rounded up" to a whole number. This is why the script will calculate the temperature and acceleration values scaled by 1000 (multiplied by 1000). |
iBeacon is one of the supported advertising packet types. You can find more information about the protocol following the link.
The PDU Payload, in this case, consists of "AdvA" (that is 6 octets long) and "AdvData" (a field that contains data information) fields. Legacy Bluetooth devices can only support 31 byte-long beacon messages. UUID is 16 byte-long (MikroTik default UID=b2b98de4-c81c-47c2-b14e-791b3e5587ec).
"AdvData" field structure:
ManufacturerData | company identifier | 4 octets (1aff4c00 ) |
BeaconType | a secondary identifier | 1 octet (const) |
RemainingDataLength | defines the remaining length for the payload in bytes | 1 octet (const) |
UserData | user-configured part of the payload |
|
TxPower | indicates the signal strength at one meter from the device | 1 octet (int) |
0 | 1a | ManufacturerData | company identifier |
1 | ff | ManufacturerData | company identifier |
2 | 4c | ManufacturerData | company identifier |
3 | 00 | ManufacturerData | company identifier |
4 | 02 | BeaconType | a secondary identifier |
5 | 15 | RemainingDataLength | defines the remaining length for the payload in bytes |
6 | b2 | UserData | Proximity UUID |
7 | b9 | UserData | Proximity UUID |
8 | 8d | UserData | Proximity UUID |
9 | e4 | UserData | Proximity UUID |
10 | c8 | UserData | Proximity UUID |
11 | 1c | UserData | Proximity UUID |
12 | 47 | UserData | Proximity UUID |
13 | c2 | UserData | Proximity UUID |
14 | b1 | UserData | Proximity UUID |
15 | 4e | UserData | Proximity UUID |
16 | 79 | UserData | Proximity UUID |
17 | 1b | UserData | Proximity UUID |
18 | 3e | UserData | Proximity UUID |
19 | 55 | UserData | Proximity UUID |
20 | 87 | UserData | Proximity UUID |
21 | ec | UserData | Proximity UUID |
22 | xx* | UserData | Major Number |
23 | xx* | UserData | Major Number |
24 | xx* | UserData | Minor Number |
25 | xx* | UserData | Minor Number |
26 | xx* | TxPower | indicates the signal strength at one meter from the device |
* - can vary
Eddystone-TLM is one of the supported advertising packet types. You can find more information about the protocol following the link.
The PDU Payload, in this case, consists of "AdvA" (that is 6 octets long) and "AdvData" (a field that contains data information) fields. MikroTik default CompleteUUID=03 03 aa fe; ServiceData=11 16 aa fe.
"AdvData" field structure:
CommonPayload | part of the advertisement payload that is common for all Eddystone's frame types |
|
TlmPayload | Eddystone-TLM frame payload |
|
0 | 03 | CommonPayload | CompleteUUID |
1 | 03 | CommonPayload | CompleteUUID |
2 | aa | CommonPayload | CompleteUUID |
3 | fe | CommonPayload | CompleteUUID |
4 | 11 | CommonPayload | ServiceData |
5 | 16 | CommonPayload | ServiceData |
6 | aa | CommonPayload | ServiceData |
7 | fe | CommonPayload | ServiceData |
8 | 20 | CommonPayload | FrameType |
9 | 00 | TlmPayload | Version |
10 | xx* | TlmPayload | BatteryVoltageMv |
11 | xx* | TlmPayload | BatteryVoltageMv |
12 | xx* | TlmPayload | TemperatureC |
13 | xx* | TlmPayload | TemperatureC |
14 | xx* | TlmPayload | AdvertisementCount |
15 | xx* | TlmPayload | AdvertisementCount |
16 | xx* | TlmPayload | AdvertisementCount |
17 | xx* | TlmPayload | AdvertisementCount |
18 | xx* | TlmPayload | UptimeCounter |
19 | xx* | TlmPayload | UptimeCounter |
20 | xx* | TlmPayload | UptimeCounter |
21 | xx* | TlmPayload | UptimeCounter |
* - can vary
Eddystone-UID is one of the supported advertising packet types. You can find more information about the protocol following the link.
The PDU Payload, in this case, consists of "AdvA" (that is 6 octets long) and "AdvData" (a field that contains data information) fields. MikroTik default CompleteUUID=03 03 aa fe; ServiceData=17 16 aa fe.
"AdvData" field structure:
CommonPayload | part of the advertisement payload that is common for all Eddystone's frame types |
|
UidPayload | Eddystone-UID frame payload |
|
0 | 03 | CommonPayload | CompleteUUID |
1 | 03 | CommonPayload | CompleteUUID |
2 | aa | CommonPayload | CompleteUUID |
3 | fe | CommonPayload | CompleteUUID |
4 | 17 | CommonPayload | ServiceData |
5 | 16 | CommonPayload | ServiceData |
6 | aa | CommonPayload | ServiceData |
7 | fe | CommonPayload | ServiceData |
8 | 00 | CommonPayload | FrameType |
9 | xx* | UidPayload | Ranging Data |
10 | b2 | UidPayload | Nspace |
11 | b9 | UidPayload | Nspace |
12 | 8d | UidPayload | Nspace |
13 | e4 | UidPayload | Nspace |
14 | c8 | UidPayload | Nspace |
15 | 1c | UidPayload | Nspace |
16 | 47 | UidPayload | Nspace |
17 | c2 | UidPayload | Nspace |
18 | b1 | UidPayload | Nspace |
19 | 4e | UidPayload | Nspace |
20 | xx* | UidPayload | Instance |
21 | xx* | UidPayload | Instance |
22 | xx* | UidPayload | Instance |
23 | xx* | UidPayload | Instance |
24 | xx* | UidPayload | Instance |
25 | xx* | UidPayload | Instance |
26 | 00 | UidPayload | RFU1 |
27 | 00 | UidPayload | RFU2 |
* - can vary