Bluetooth tag-tracking using MQTT and ThingsBoard
本页介绍使用 MikroTik RouterOS 结合 MQTT 和 ThingsBoard 进行 Bluetooth 标签追踪,详细说明 KNOT 设备如何捕获 Bluetooth 广告数据包、将其处理为 MQTT 消息,并在 ThingsBoard 中可视化,以追踪资产在多个区域之间的移动。
使用 MQTT 和 ThingsBoard 进行 Bluetooth 标签追踪
RouterOS 中的 Bluetooth 接口实现允许设备捕获通过信道 37、38 和 39 广播的 Bluetooth 广告数据包。更多信息可在此处指南中找到。
诸如 TG-BT5-IN 和 TG-BT5-OUT 之类的 Bluetooth 标签正是这样做的。它们通过上述信道广播广告负载。要了解负载中存储了哪种信息,请务必查看链接。这些标签可以(使用 MikroTik Beacon Manager 应用)配置为自动广播负载,可设置间隔和/或在检测到移动、倾斜或自由落体触发器时广播。简而言之,标签会定期 "告知"(广播给)周围所有 Bluetooth 扫描器(如 KNOT)有关自身的信息。
当标签广播负载,且标签处于 KNOT 的 Bluetooth 工作范围内时,KNOT 将捕获该负载并显示在其 "scanner" Bluetooth 接口部分下。它将如下所示:
/iot/bluetooth/scanners/advertisements/print
Columns: DEVICE, PDU-TYPE, TIME, ADDRESS-TYPE, ADDRESS, RSSI, LENGTH, DATA
# DEVICE PDU-TYPE TIME ADDRESS-TYPE ADDRESS RSSI LENGTH DATA
0 bt1 adv-noconn-ind 2023-03-07 12:11:57 public DC:2C:6E:0F:C0:3D -51dBm 22 15ff4f09010079100000ffff0000cf188a6b2b000064
1 bt1 adv-noconn-ind 2023-03-07 12:11:58 public 2C:C8:1B:4B:BB:0A -49dBm 22 15ff4f090100168dfefffffffeffa51ae1362200005e
上面的示例向我们展示,KNOT 在其工作范围内看到两个 Bluetooth 标签,MAC 地址分别为 "DC:2C:6E:0F:C0:3D" 和 "2C:C8:1B:4B:BB:0A",以及它们各自的负载("DATA" 字段)和信号强度("RSSI" 字段)。
信息
在本地测试 KNOT 能处理多少负载时,我们取得了以下结果 → 使用 300 个标签(出厂设置),散布在 KNOT 周围,并使用 Bluetooth 过滤器 "keep-newest"(它会用最新的负载覆盖每个唯一 MAC 地址之前收到的负载,因此 Bluetooth 列表在任何时候每个唯一标签 MAC 地址只显示 1 个负载),30-40 秒后所有 300 个 MAC 地址都出现在 KNOT 的范围内。这里你需要记住,所有 300 个标签同时在同一信道上广播会造成干扰(接收延迟)。当我们 "清空" Bluetooth 负载列表后,列表每秒新增 20 个条目,大约 ~15 秒后,列表有 250-290 个负载。然后再过 ~15 秒,列表显示了全部 300 个唯一标签负载。你的 KNOT 能够处理的实际标签数量在很大程度上取决于环境,因此最好在现场进行测试。 借助 RouterOS 脚本和调度,我们可以让 KNOT 自动定期扫描负载列表,并且如果在列表中发现特定负载或特定标签的 MAC 地址,我们可以让 KNOT(根据上方示例中打印的信息)构建一条 MQTT 消息,并通过 MQTT、电子邮件或 HTTP post 发送到所配置的服务器。脚本示例将在本指南后面展示。
正如标题所示,目标是实现一个 Bluetooth 标签追踪解决方案,其思路非常简单。当你有 2 个 KNOT(KNOT-A 和 KNOT-B),在调度器上运行相同的脚本,且标签在它们的 Bluetooth 工作范围之间移动时,服务器上的数据将指示是 KNOT-A 还是 KNOT-B 发送了标签的负载。这将帮助你判断标签的远近,即标签是在 KNOT-A 区域还是 KNOT-B 区域广播负载。
你需要一台用于存储和可视化数据的服务器。在本指南中,我们将展示一个名为 ThingsBoard 的平台,以及如何使用 MQTT 协议与其通信。
ThingsBoard 有云解决方案以及不同的本地安装选项(针对不同的操作系统)。由于我们添加了容器功能,现在也可以在 RouterOS 内运行该平台。为此,你需要一台至少富余 2 GB RAM(或富余 1 GB RAM 且负载极低)、可以扩展存储(例如额外的 USB 端口)、且为 ARM64 或 AMD64 架构的 RouterOS 设备。考虑使用 CHR 机器,它可能是一个不错的选择。
安装要求
- 一台正在运行的 ThingsBoard 服务器。
- 2 个或更多 KNOT,通过以太网、Wi-Fi 或蜂窝连接可访问服务器网络(所需单元数量取决于需要覆盖的区域大小)。
- 1 个或更多 Bluetooth TG-BT5-IN 和/或 TG-BT5-OUT 标签(取决于你需要追踪多少资产 - 每个资产 1 个标签)。
场景说明
让我们先看一个基本示例。我们有两个 KNOT(KNOT-A 和 KNOT-B)。我们在自己的环境中测试了 Bluetooth 范围,可以确认两个 KNOT 都能在 70 米距离内捕获标签。如果我们将 KNOT-A 和 KNOT-B 相隔 140 米安装,它们的 Bluetooth 范围将不会重叠或仅略微重叠。当标签移动到 KNOT-A 范围内时 → 被监控标签的负载将出现在 Bluetooth 扫描器列表下 → 脚本将按设定的计划启动 → 带有报告的 MQTT 消息将发送到服务器 → 最后,服务器将显示标签位于 KNOT-A 区域。当标签移动到 KNOT-B 范围内时,同样的事情会发生,服务器将显示标签位于 KNOT-B 区域内。
实际的 Bluetooth 工作距离会因现场而异,因为许多不同因素都会影响它,例如 2.4 GHz 干扰或周围使用的材料。例如,在视距、无干扰的情况下,KNOT 能捕获标签广播负载的距离可达 180 米(KNOT — ~180 米 — TG-BT5-OUT)。但你也必须记住,随着距离增加,途中会丢失更多数据包。在办公环境中,范围可能降至 30-100 米。
从逻辑上讲,如果 Bluetooth 工作范围重叠,且标签位于重叠区域内(同时处于 KNOT-A 和 KNOT-B 的 Bluetooth 范围内),两个 KNOT 都将发送数据,服务器将显示标签同时被两台设备报告。
当然,会存在两个或多个 KNOT 的 Bluetooth 范围重叠的区域,你可以对此加以利用。基本上,你将获得标签当前位于特定 KNOT 区域之间 Bluetooth 范围边缘的信息。换言之,当资产移动到重叠区域时,你将在服务器上获得资产位于两个 KNOT 工作范围之间某处的信息,这是很有用的信息,因为它提供了更高的精度。
此外,可以使用 Tx power 参数降低标签的输出功率。这意味着即使标签的负载广播得过远,被本不该看到这些负载(在更远距离)的其他 KNOT 捕获 → 你也可以降低标签的输出功率,从而缩短 KNOT 能够捕获它们的距离。这样,你就可以 "调整" 接收的 "范围",同时避免 "干扰" 其他区域的信号。
我们准备的脚本还允许你设置一个过滤器(稍后将展示),它将使 KNOT 忽略扫描器捕获的负载,除非信号强度(RSSI)强于指定值。在上方 "Introduction" 部分展示的 Bluetooth 扫描器示例打印中,我们可以看到 KNOT 看到其中一个标签的 RSSI 信号强度为 -51 dBm(MAC 地址为 "DC:2C:6E:0F:C0:3D" 的标签),另一个标签的 RSSI 信号强度为 -49 dBm(MAC 地址为 "2C:C8:1B:4B:BB:0A" 的标签)。因此,如果我们在脚本中应用过滤器以忽略所有接收到的信号强度(RSSI)弱于 -50 dBm 的负载,我们的 KNOT 将只报告标签 "2C:C8:1B:4B:BB:0A" 位于 Bluetooth 范围内,因为其 RSSI 为 -49 dBm,而第二个标签(RSSI 为 -51 dBm)将被忽略。这本质上意味着 "调整" 接收 "范围" 的第二种方法。实际信号强度会因不同位置而异(如前所述,因为干扰和周围材料),因此需要在现场进行测试。
示例 #1
其中一个用例显示在下方拓扑中:
![]()
说明
对象和 Bluetooth 工作范围的比例仅作为示例展示,以帮助直观地理解和想象该拓扑! 我们有一个仓库区域,我们有 3 个资产(托盘)想要追踪。我们还有 3 个区域:区域 A,用于存放新到达的托盘;区域 B,我们的资产被转移到那里接受检查;区域 C,资产在检查后被移到那里。要实现 Bluetooth 资产追踪,只需每个区域安装 1 个 KNOT,每个资产安装 1 个标签。
如果 TAG 1 和 TAG 2(托盘)到达 "到达" 区域 A,KNOT A 将向服务器报告两个资产都在其 Bluetooth 范围内。如果 TAG 3 被移动到区域 C,服务器将指示它位于 KNOT C 范围内。如果 TAG 1 和 TAG 2 向 B 区移动,并停留在 A 区和 B 区之间的边缘,服务器将显示它们位于重叠区域(同时处于 KNOT-A 和 KNOT-B 范围内)。如果标签移动到仓库中间,服务器将指示它们同时位于重叠区域内的全部 3 个区域中。
配置
在此示例中,我们将展示一个基本拓扑,即使用两个 KNOT,我们只想知道标签位于建筑物的一侧还是另一侧(是位于区域 A 内还是区域 B 内)。
ThingsBoard 准备
请查看此处的指南,了解如何设置 ThingsBoard 和 RouterOS 以使用 MQTT 通信。
提示
为简单起见,此示例将展示 access-token 场景,但你也可以使用其他可用选项。对于生产环境,建议使用 SSL-MQTT,因为非 SSL-MQTT 很容易被数据包捕获和检查。
要了解如何在通过容器运行的实例上实现 SSL-MQTT 通信,请查看此处链接的指南(Enabling HTTPS and SSL MQTT 部分)。 在 ThingsBoard GUI 下创建 2 个 KNOT,并将它们设为 "gateways"。
转到 "Devices" 部分,点击 "+" 按钮和 "Add new device":
![]()
为设备命名并勾选 "Is gateway" 选项:
![]()
对你拥有的每个 KNOT 执行此操作:
![]()
在刚创建的设备下,在 "Manage credentials" 选项卡中为每个 KNOT 设置唯一的 access token(唯一凭据):
![]()
RouterOS 配置
准备
在继续之前,我们需要确认我们的 Bluetooth 标签确实出现在 KNOT 的 Bluetooth 范围内,并且 KNOT 检测到它。为此,你可以执行命令 /iot/bluetooth/scanners/advertisements/print:
/iot/bluetooth/scanners/advertisements/print
# DEVICE PDU-TYPE TIME ADDRESS-TYPE ADDRESS RSSI LENGTH DATA
0 bt1 adv-noconn-ind 2023-03-08 12:35:15 public 2C:C8:1B:4B:BB:0A -50dBm 22 15ff4f090100b0110100ffff00000019d68d2300005d
1 bt1 adv-noconn-ind 2023-03-08 12:35:16 public DC:2C:6E:0F:C0:3D -39dBm 22 15ff4f0901008f3cfcfffbfffaff301783c22c000064
2 bt1 adv-noconn-ind 2023-03-08 12:35:35 public 2C:C8:1B:4B:BB:0A -50dBm 22 15ff4f09010084d500000400ffff0319ea8d2300005d
3 bt1 adv-noconn-ind 2023-03-08 12:35:45 public 2C:C8:1B:4B:BB:0A -50dBm 22 15ff4f090100e607faffffff03000319f48d2300005d
或者你可以在 IoT>Bluetooth>Advertising reports 选项卡下使用 Webfig 或 Winbox 检查。
该列表可能很杂乱。由于扫描器会捕获周围的一切,随机负载可能会出现在列表中。为了帮助减少列表内容,你可以使用标签的 MAC 地址进行过滤 /iot/bluetooth/scanners/advertisements/print where address=DC:2C:6E:0F:C0:3D:
/iot/bluetooth/scanners/advertisements/print where address=DC:2C:6E:0F:C0:3D
# DEVICE PDU-TYPE TIME ADDRESS-TYPE ADDRESS RSSI LENGTH DATA
0 bt1 adv-noconn-ind 2023-03-08 12:41:06 public DC:2C:6E:0F:C0:3D -49dBm 22 15ff4f0901005ab20100fdfffdff4017e1c32c000064
1 bt1 adv-noconn-ind 2023-03-08 12:41:26 public DC:2C:6E:0F:C0:3D -40dBm 22 15ff4f090100349704000000fcff4017f5c32c000064
2 bt1 adv-noconn-ind 2023-03-08 12:41:36 public DC:2C:6E:0F:C0:3D -49dBm 22 15ff4f09010073fb0000000000003017ffc32c000064
3 bt1 adv-noconn-ind 2023-03-08 12:41:46 public DC:2C:6E:0F:C0:3D -43dBm 22 15ff4f090100b88cffffffffffff401709c42c000064
要了解如何解读负载,请查看此处的指南。
MQTT broker
在每个 KNOT 上设置一个 MQTT broker。
对于 KNOT A:
/iot/mqtt/brokers/add name=tb address=x.x.x.x port=1883 username=knot-A_access_token
其中:
name是你希望为 broker 指定的名称,该名称稍后将在脚本中使用。address是 broker/ThingsBoard 服务器的 IP 地址。port是 broker 监听的 TCP 端口 → 对于非 SSL,通常为 TCP 1883。username由 MQTT broker 决定,在我们的例子中,它是在 ThingsBoard 管理门户中生成的 "access token"。
对于 KNOT B → 执行相同步骤。只需将 username 更改为为 KNOT B 设备(gateway)生成的相应 access token。
脚本
将下方显示的脚本导入到每个 KNOT。为此,只需复制下方显示的 "code",粘贴到新的终端窗口中,然后按 <kbd>Enter</kbd>:
/system/script/add dont-require-permissions=no name=tracking owner=admin policy=\
ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="# Requ\
ired packages: iot\r\
\n\r\
\n################################ Configuration ##############################\
##\r\
\n# Name of an existing MQTT broker that should be used for publishing\r\
\n:local broker \"tb\"\r\
\n\r\
\n# MQTT topic where the message should be published\r\
\n:local topic \"v1/gateway/telemetry\"\r\
\n\r\
\n# POSIX regex for filtering advertisement Bluetooth addresses. E.g. \"^BC:33:\
AC\"\r\
\n# would only include addresses which start with those 3 octets.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local addressRegex \"\"\r\
\n\r\
\n# POSIX regex for filtering Bluetooth advertisements based on their data. Sam\
e\r\
\n# usage as with 'addressRegex'.\r\
\n:local advertisingDataRegex \"\"\r\
\n\r\
\n# Signal strength filter. E.g. -40 would only include Bluetooth advertisement\
s\r\
\n# whose signal strength is stronger than -40dBm.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local rssiThreshold \"\"\r\
\n\r\
\n#Name the KNOT. Identity of the unit that will be sending the message. This n\
ame will be reported to the MQTT broker.\r\
\n:local gwName \"KNOT_A\"\r\
\n\r\
\n################################## Bluetooth ################################\
##\r\
\n:put (\"[*] Gathering Bluetooth info...\")\r\
\n\r\
\n:global makeRecord do={\r\
\n :local jsonStr \"{\\\"ts\\\":\$ts,\\\"values\\\":{\\\"reporter\\\":\\\"\$\
gwName\\\",\\\"rssi\\\":\$rssi}}\"\r\
\n :return \$jsonStr\r\
\n} \r\
\n\r\
\n# array of record strings collected for each advertising MAC address\r\
\n:global macRecords [:toarray \"\"]\r\
\n\r\
\n# process advertisements and update macRecords\r\
\n:local advertisements [/iot/bluetooth/scanners/advertisements/print detail as\
-value where \\\r\
\naddress ~ \$addressRegex and \\\r\
\ndata ~ \$advertisingDataRegex and \\\r\
\nrssi > \$rssiThreshold]\r\
\n\r\
\n/iot/bluetooth/scanners/advertisements/clear\r\
\n\r\
\n:foreach adv in=\$advertisements do={\r\
\n:local address (\$adv->\"address\")\r\
\n:local rssi (\$adv->\"rssi\")\r\
\n:local epoch (\$adv->\"epoch\")\r\
\n \r\
\n:local recordStr [\$makeRecord ts=\$epoch gwName=\$gwName rssi=\$rssi]\r\
\n\r\
\n:if ([:len (\$macRecords->\$address)] > 0) do={\r\
\n:local str (\$macRecords->\$address)\r\
\n:local newStr \"\$str,\$recordStr\"\r\
\n:set (\$macRecords->\$address) \$newStr} else={:set (\$macRecords->\$address)\
\_\$recordStr}}\r\
\n\r\
\n# TODO: add some logic to decide when we want to send data\r\
\n:local sendData true\r\
\n\r\
\n:if (\$sendData) do={\r\
\n:local jsonStr \"{\"\r\
\n\r\
\n:foreach addr,advRec in=\$macRecords do={\r\
\n:set jsonStr \"\$jsonStr\\\"\$addr\\\":[\$advRec],\"}\r\
\n\r\
\n:local payloadlength\r\
\n:set payloadlength [:len (\$jsonStr)]\r\
\n:local remcom\r\
\n:set remcom [:pick \$jsonStr 0 (\$payloadlength-1)]\r\
\n:set jsonStr \"\$remcom}\"\r\
\n:local message\r\
\n:set message \"\$jsonStr\"\r\
\n:log info \"\$message\";\r\
\n:put (\"[*] Message structured: \$message\")\r\
\n:put (\"[*] Total message size: \$[:len \$message] bytes\")\r\
\n:put (\"[*] Sending message to MQTT broker...\")\r\
\n/iot/mqtt/publish broker=\"\$broker\" topic=\"\$topic\" message=\$message}"
该脚本应出现在 "System>Scripts>Script List" 选项卡下,名称为 "tracking",或使用命令 /system/script/print 查看。
有几行脚本需要你特别注意。
broker 名称配置行,你需要在其中输入已设置的 MQTT broker 名称:
:local broker "tb"
你需要输入 MQTT broker 使用的正确 topic。有关更多详细信息,请查看 ThingsBoard 文档。默认情况下,topic 应为:
:local topic "v1/gateway/telemetry"
MAC 地址过滤选项位于脚本内部。你可以输入 MAC 地址的全部 6 个 octet(将过滤器应用于 1 个特定标签),也可以使用几个 octet 过滤列表,例如 "^BC:33:AC"(应用过滤器,以便仅处理以 "BC:33:AC:..." 开头的 MAC 地址):
:local addressRegex "DC:2C:6E:0F:C0:3D"
负载内容/数据行。它允许你根据特定负载内容过滤列表,例如 "manufacturer data"。例如,"15ff4f09" 将丢弃所有非 MikroTik 格式的负载:
:local advertisingDataRegex "15ff4f09"
RSSI 信号强度过滤选项。此过滤选项已在 "Scenario explanation" 部分中提到。此过滤器允许你忽略任何 RSSI 弱于所配置值的负载。例如,"-40" 将仅包含信号强度强于 -40dBm 的 Bluetooth 广告:
:local rssiThreshold "-40"
KNOT 标识符行。你需要为每个唯一的 KNOT 更改它。例如,将你的第一个 KNOT 命名为 → KNOT_A,将第二个 KNOT 命名为 → KNOT_B:
:local gwName "KNOT_A"
脚本的其余部分无需更改。
当你运行脚本时它如何工作?脚本使用所应用的过滤器 "检查" "Advertising reports" 选项卡(负载列表选项卡),并构建一条 JSON 消息。JSON 消息的示例如下:
{
"2C:C8:1B:4B:BB:0A": [
{
"ts": 1678967250600,
"values": {
"reporter": "KNOT_A",
"rssi": -47
}
}
],
"DC:2C:6E:0F:C0:3D": [
{
"ts": 1678967247850,
"values": {
"reporter": "KNOT_A",
"rssi": -59
}
},
{
"ts": 1678967257849,
"values": {
"reporter": "KNOT_A",
"rssi": -67
}
}
]
}
数据按照 ThingsBoard 指南构建,其中 2C:C8:1B:4B:BB:0A 和 DC:2C:6E:0F:C0:3D 是出现在 KNOT 范围内的标签的 MAC 地址,ts 是标签广播的每个负载的 Unix 时间戳(以毫秒为单位),reporter 指示哪个特定 KNOT 发送了消息,rssi 是标签广播的每个负载的信号强度(以 dBm 为单位)。
在 "搜索" 负载列表并构建 JSON 消息后,Bluetooth 接口负载列表会被 "清理"(或 "刷新"),先前构建的 JSON 消息将发送到 ThingsBoard MQTT broker。
要运行脚本,请使用命令:
/system/script/run tracking
调度器
为脚本应用一个调度器,以便 RouterOS 定期自行启动该脚本:
/system/scheduler/add name=bluetoothscheduler interval=30s on-event="/system/script/run tracking"
你可以设置更短和更长的间隔。如果你希望更频繁地发送数据,使数据 "更新鲜" → 设置更短的时间间隔(10-15 秒)。如果你希望发送更少的消息、频率更低 → 可以设置更长的时间间隔(30 分钟以上)。
使用脚本构建的 JSON 消息会为收到的每个负载分配一个 ts 值(时间戳)。这意味着 当脚本运行时,例如每分钟,使用 1 个标签,且该标签每 10 秒广播 1 个负载(即每分钟 6 个负载)→ ThingsBoard 数据(GUI)将每分钟更新一次,每分钟会出现 6 个新条目(每个条目将指示它是在前一个之后 10 秒收到的)。而如果你每 15 分钟发送一次消息,使用 1 个每 10 秒广播一个负载的标签(即每 15 分钟 6*15=90 个负载)→ ThingsBoard 数据(GUI)将每 15 分钟更新一次,但会出现 90 个条目。
ThingsBoard 数据可视化与结果验证
在使用 /system/script/run tracking 或通过调度器运行脚本并刷新 GUI 门户后,JSON 消息中发现的所有 MAC 地址(标签)都将在 ThingsBoard GUI 下创建为新设备:
![]()
为了帮助你可视化数据,你可以使用内置的 widgets 或创建自己的。
从设备列表中选择标签的 MAC 地址,转到 "Latest telemetry" 部分,勾选 "reporter" 参数,然后点击 "Show on widget" 按钮:
![]()
选择你想使用的 widget,例如在 "Cards" 包下的 "Timeseries table",然后点击 "Add to dashboard":
![]()
创建一个新仪表板并随意命名。点击 "Add":
![]()
对出现在 "Devices" 选项卡下的其他标签执行相同步骤。在同一仪表板下为每个唯一标签创建一个新 widget。
将 widget 的 "Timewindow" 从 "Realtime-last minute"(默认使用)更改为 "Realtime-current day":
![]()
结果是,如果两个标签都在 KNOT A 范围内,仪表板将显示:
![]()
如果它们移动到 KNOT B 范围,将显示:
![]()
如果标签移动到 重叠区域,即两个范围内,两个报告者(KNOT_A 和 KNOT_B)应在彼此相差几秒内出现,具体取决于调度器中使用的间隔:
![]()
示例 #2
在第二个示例中,我们将展示另一个拓扑:

我们有几个仓库、几辆公司送货车辆,以及 3 个我们想要追踪的资产。我们的资产是装载货物的托盘,目标是了解:
- 资产(配备标签)当前位于哪个特定仓库(配备 KNOT),以及它在特定仓库内停留了多长时间。
- 资产(配备标签)是否在道路上、在仓库之间运输,以及它在车辆(配备 KNOT)内停留了多长时间。
- (可选)如果使用 TG-BT5-OUT 标签,在此期间温度是多少?你还可以/或者监控可从广播负载中获得的其他参数,例如加速度。
- (可选) 查明 KNOT 的 GPS 位置。
要实现 Bluetooth 资产追踪,只需每个仓库安装 1 个 KNOT,每辆车安装 1 个 KNOT,每个资产安装 1 个标签。
我们可以看到 TAG 1 在车辆内,而这辆车刚停在仓库附近。KNOT 1 和 KNOT 4 都将向服务器报告资产在它们的范围内。这将告诉你资产已停放但尚未被运输。
我们可以看到 TAG 2 正在仓库之间运输,且仅在 KNOT 5 的 Bluetooth 范围内。在这种情况下,KNOT 5 将是唯一的报告者,服务器上显示的结果将意味着资产正在被运输。
我们可以看到 TAG 3 在仓库内。服务器将仅指示这一点。
服务器上的数据将显示 KNOT 发送的每份报告的时间戳,这将告诉你资产在特定设备的 Bluetooth 范围内停留了多长时间。
配置
在此示例中,我们将展示一个基本拓扑,包含 2 个仓库、1 辆在它们之间行驶的车辆/卡车,以及 1 个资产/托盘/标签。
ThingsBoard 准备
请查看此处的指南,了解如何设置 ThingsBoard 和 RouterOS 以使用 MQTT 通信。
提示
为简单起见,此示例将展示 access-token 场景,但你也可以使用其他可用选项。对于生产环境,建议使用 SSL-MQTT,因为非 SSL-MQTT 很容易被数据包捕获和检查。
要了解如何在通过容器运行的实例上实现 SSL-MQTT 通信,请查看此处链接的指南(Enabling HTTPS and SSL MQTT 部分)。 在 ThingsBoard GUI 下创建 3 个 KNOT,并将它们设为 "gateways"。
转到 "Devices" 部分,点击 "+" 按钮和 "Add new device":
![]()
为设备命名并勾选 "Is gateway" 选项:
![]()
对你拥有的每个 KNOT 执行此操作:
![]()
在刚创建的设备下,在 "Manage credentials" 选项卡中为每个 KNOT 设置唯一的 access token(唯一凭据):
![]()
RouterOS 配置
准备
在继续之前,我们需要确认我们的 Bluetooth 标签确实出现在 KNOT 的 Bluetooth 范围内,并且 KNOT 检测到它。为此,你可以执行命令 /iot/bluetooth/scanners/advertisements/print:
/iot/bluetooth/scanners/advertisements/print
# DEVICE PDU-TYPE TIME ADDRESS-TYPE ADDRESS RSSI LENGTH DATA
0 bt1 adv-noconn-ind 2023-03-08 12:35:15 public 2C:C8:1B:4B:BB:0A -50dBm 22 15ff4f090100b0110100ffff00000019d68d2300005d
1 bt1 adv-noconn-ind 2023-03-08 12:35:16 public DC:2C:6E:0F:C0:3D -39dBm 22 15ff4f0901008f3cfcfffbfffaff301783c22c000064
2 bt1 adv-noconn-ind 2023-03-08 12:35:35 public 2C:C8:1B:4B:BB:0A -50dBm 22 15ff4f09010084d500000400ffff0319ea8d2300005d
3 bt1 adv-noconn-ind 2023-03-08 12:35:45 public 2C:C8:1B:4B:BB:0A -50dBm 22 15ff4f090100e607faffffff03000319f48d2300005d
或者你可以在 IoT>Bluetooth>Advertising reports 选项卡下使用 Webfig 或 Winbox 检查。
该列表可能很杂乱。由于扫描器会捕获周围的一切,随机负载可能会出现在列表中。为了帮助减少列表内容,你可以使用标签的 MAC 地址进行过滤 /iot/bluetooth/scanners/advertisements/print where address=DC:2C:6E:0F:C0:3D:
/iot/bluetooth/scanners/advertisements/print where address=DC:2C:6E:0F:C0:3D
# DEVICE PDU-TYPE TIME ADDRESS-TYPE ADDRESS RSSI LENGTH DATA
0 bt1 adv-noconn-ind 2023-03-08 12:41:06 public DC:2C:6E:0F:C0:3D -49dBm 22 15ff4f0901005ab20100fdfffdff4017e1c32c000064
1 bt1 adv-noconn-ind 2023-03-08 12:41:26 public DC:2C:6E:0F:C0:3D -40dBm 22 15ff4f090100349704000000fcff4017f5c32c000064
2 bt1 adv-noconn-ind 2023-03-08 12:41:36 public DC:2C:6E:0F:C0:3D -49dBm 22 15ff4f09010073fb0000000000003017ffc32c000064
3 bt1 adv-noconn-ind 2023-03-08 12:41:46 public DC:2C:6E:0F:C0:3D -43dBm 22 15ff4f090100b88cffffffffffff401709c42c000064
要了解如何解读负载,请查看此处的指南。
MQTT broker
在每个 KNOT 上设置一个 MQTT broker。
对于 KNOT_1:
/iot/mqtt/brokers/add name=tb address=x.x.x.x port=1883 username=knot-1_access_token
其中:
name是你希望为 broker 指定的名称,该名称稍后将在脚本中使用。address是 broker/ThingsBoard 服务器的 IP 地址。port是 broker 监听的 TCP 端口 → 对于非 SSL,通常为 TCP 1883。username由 MQTT broker 决定,在我们的例子中,它是在 ThingsBoard 管理门户中生成的 "access token"。
对于 KNOT_2 和 KNOT_3 → 执行相同步骤。只需将 username 更改为生成的相应 access token。
脚本
将下方显示的脚本导入到每个 KNOT。为此,只需复制下方显示的 "code",粘贴到新的终端窗口中,然后按 <kbd>Enter</kbd>:
/system/script/add dont-require-permissions=no name=tracking owner=admin policy=\
ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="# Required package\
s: iot\r\
\n\r\
\n################################ Configuration ################################\r\
\n# Name of an existing MQTT broker that should be used for publishing\r\
\n:local broker \"tb\"\r\
\n\r\
\n# MQTT topic where the message should be published\r\
\n:local topic \"v1/gateway/telemetry\"\r\
\n\r\
\n# POSIX regex for filtering advertisement Bluetooth addresses. E.g. \"^BC:33:AC\"\r\
\n# would only include addresses which start with those 3 octets.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local addressRegex \"\"\r\
\n\r\
\n# POSIX regex for filtering Bluetooth advertisements based on their data. Same\r\
\n# usage as with 'addressRegex'.\r\
\n:local advertisingDataRegex \"\"\r\
\n\r\
\n# Signal strength filter. E.g. -40 would only include Bluetooth advertisements\r\
\n# whose signal strength is stronger than -40dBm.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local rssiThreshold \"\"\r\
\n\r\
\n#Name the KNOT. Identity of the unit that will be sending the message. This name will be \
reported to the MQTT broker.\r\
\n:local gwName \"1\"\r\
\n\r\
\n################################## Bluetooth ##################################\r\
\n:put (\"[*] Gathering Bluetooth info...\")\r\
\n\r\
\n:global makeRecord do={\r\
\n :local jsonStr \"{\\\"ts\\\":\$ts,\\\"values\\\":{\\\"KNOT_\$gwName\\\":\\\"\$gwName\
\\\",\\\"rssi\\\":\$rssi}}\"\r\
\n :return \$jsonStr\r\
\n} \r\
\n\r\
\n# array of record strings collected for each advertising MAC address\r\
\n:global macRecords [:toarray \"\"]\r\
\n\r\
\n# process advertisements and update macRecords\r\
\n:local advertisements [/iot/bluetooth/scanners/advertisements/print detail as-value where\
\_\\\r\
\naddress ~ \$addressRegex and \\\r\
\ndata ~ \$advertisingDataRegex and \\\r\
\nrssi > \$rssiThreshold]\r\
\n\r\
\n/iot/bluetooth/scanners/advertisements/clear\r\
\n\r\
\n:foreach adv in=\$advertisements do={\r\
\n:local address (\$adv->\"address\")\r\
\n:local rssi (\$adv->\"rssi\")\r\
\n:local epoch (\$adv->\"epoch\")\r\
\n \r\
\n:local recordStr [\$makeRecord ts=\$epoch gwName=\$gwName rssi=\$rssi]\r\
\n\r\
\n:if ([:len (\$macRecords->\$address)] > 0) do={\r\
\n:local str (\$macRecords->\$address)\r\
\n:local newStr \"\$str,\$recordStr\"\r\
\n:set (\$macRecords->\$address) \$newStr} else={:set (\$macRecords->\$address) \$recordStr\
}}\r\
\n\r\
\n# TODO: add some logic to decide when we want to send data\r\
\n:local sendData true\r\
\n\r\
\n:if (\$sendData) do={\r\
\n:local jsonStr \"{\"\r\
\n\r\
\n:foreach addr,advRec in=\$macRecords do={\r\
\n:set jsonStr \"\$jsonStr\\\"\$addr\\\":[\$advRec],\"}\r\
\n\r\
\n:local payloadlength\r\
\n:set payloadlength [:len (\$jsonStr)]\r\
\n:local remcom\r\
\n:set remcom [:pick \$jsonStr 0 (\$payloadlength-1)]\r\
\n:set jsonStr \"\$remcom}\"\r\
\n:local message\r\
\n:set message \"\$jsonStr\"\r\
\n:log info \"\$message\";\r\
\n:put (\"[*] Message structured: \$message\")\r\
\n:put (\"[*] Total message size: \$[:len \$message] bytes\")\r\
\n:put (\"[*] Sending message to MQTT broker...\")\r\
\n/iot/mqtt/publish broker=\"\$broker\" topic=\"\$topic\" message=\$message}"
该脚本应出现在 "System>Scripts>Script List" 选项卡下,名称为 "tracking",或使用命令 /system/script/print 查看。
有几行脚本需要你特别注意。
Broker 名称配置行,你需要输入已设置的 MQTT broker 名称:
:local broker "tb"
你需要输入 MQTT broker 使用的正确 topic。有关更多详细信息,请查看 ThingsBoard 文档,默认情况下,topic 应为:
:local topic "v1/gateway/telemetry"
MAC 地址过滤选项位于脚本内部。你可以输入 MAC 地址的全部 6 个 octet(将过滤器应用于 1 个特定标签),也可以使用几个 octet 过滤列表,例如 "^BC:33:AC"(应用过滤器,以便仅处理以 "BC:33:AC:..." 开头的 MAC 地址):
:local addressRegex "DC:2C:6E:0F:C0:3D"
负载内容/数据行。它允许你根据特定负载内容过滤列表,例如 "manufacturer data"。例如,"15ff4f09" 将丢弃所有非 MikroTik 格式的负载:
:local advertisingDataRegex "15ff4f09"
RSSI 信号强度过滤选项。此过滤选项已在 "Scenario explanation" 部分中提到。此过滤器允许你忽略任何 RSSI 弱于所配置值的负载。例如,"-40" 将仅包含信号强度强于 -40dBm 的 Bluetooth 广告:
:local rssiThreshold "-40"
KNOT 标识符行。你需要为每个唯一的 KNOT 更改它。例如,将你的第一个 KNOT 标识为 "1",第二个 KNOT 为 "2",第三个 KNOT 为 "3":
:local gwName "1"
脚本的其余部分无需更改。
当你运行脚本时它如何工作?脚本使用所应用的过滤器 "检查" "Advertising reports" 选项卡(负载列表选项卡),并构建一条 JSON 消息。JSON 消息的示例如下:
{
"2C:C8:1B:4B:BB:0A": [
{
"ts": 1680526087729,
"values": {
"KNOT_1": "1",
"rssi": -47
}
}
],
"DC:2C:6E:0F:C0:3D": [
{
"ts": 1680526065000,
"values": {
"KNOT_1": "1",
"rssi": -49
}
},
{
"ts": 1680526075001,
"values": {
"KNOT_1": "1",
"rssi": -50
}
}
]
}
数据按照 ThingsBoard 指南构建,其中 2C:C8:1B:4B:BB:0A 和 DC:2C:6E:0F:C0:3D 是出现在 KNOT 范围内的标签的 MAC 地址,ts 是标签广播的每个负载的 Unix 时间戳(以毫秒为单位),KNOT_X 指示哪个特定 KNOT 发送了消息,rssi 是标签广播的每个负载的信号强度(以 dBm 为单位)。
在 "搜索" 负载列表并构建 JSON 消息后,Bluetooth 接口负载列表会被 "清理"(或 "刷新"),先前构建的 JSON 消息将发送到 ThingsBoard MQTT broker。
要运行脚本,请使用命令:
/system/script/run tracking
包含温度数据的脚本(可选)
如果你希望将温度报告添加到构建的消息中,请使用以下脚本:
/system/script/add dont-require-permissions=no name=tracking+temp owner=admin policy=\
ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="# Required package\
s: iot\r\
\n\r\
\n################################ Configuration ################################\r\
\n# Name of an existing MQTT broker that should be used for publishing\r\
\n:local broker \"tb\"\r\
\n\r\
\n# MQTT topic where the message should be published\r\
\n:local topic \"v1/gateway/telemetry\"\r\
\n\r\
\n# POSIX regex for filtering advertisement Bluetooth addresses. E.g. \"^BC:33:AC\"\r\
\n# would only include addresses which start with those 3 octets.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local addressRegex \"\"\r\
\n\r\
\n# POSIX regex for filtering Bluetooth advertisements based on their data. Same\r\
\n# usage as with 'addressRegex'.\r\
\n:local advertisingDataRegex \"\"\r\
\n\r\
\n# Signal strength filter. E.g. -40 would only include Bluetooth advertisements\r\
\n# whose signal strength is stronger than -40dBm.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local rssiThreshold \"\"\r\
\n\r\
\n#Name the KNOT. Identity of the unit that will be sending the message. This name will be \
reported to the MQTT broker.\r\
\n:local gwName \"1\"\r\
\n\r\
\n################################## Bluetooth ##################################\r\
\n:global invertU16 do={\r\
\n :local inverted 0\r\
\n :for idx from=0 to=15 step=1 do={\r\
\n :local mask (1 << \$idx)\r\
\n :if (\$1 & \$mask = 0) do={\r\
\n :set \$inverted (\$inverted | \$mask)\r\
\n }\r\
\n }\r\
\n return \$inverted\r\
\n}\r\
\n\r\
\n:global le16ToHost do={\r\
\n :local lsb [:pick \$1 0 2]\r\
\n :local msb [:pick \$1 2 4]\r\
\n\r\
\n :return [:tonum \"0x\$msb\$lsb\"]\r\
\n}\r\
\n:local from88 do={\r\
\n :global invertU16\r\
\n :global le16ToHost\r\
\n :local num [\$le16ToHost \$1]\r\
\n\r\
\n # Handle negative numbers\r\
\n :if (\$num & 0x8000) do={\r\
\n :set num (-1 * ([\$invertU16 \$num] + 1))\r\
\n }\r\
\n\r\
\n # Convert from 8.8. Scale by 1000 since floating point is not supported\r\
\n :return ((\$num * 125) / 32)\r\
\n}\r\
\n:put (\"[*] Gathering Bluetooth info...\")\r\
\n\r\
\n:global makeRecord do={\r\
\n :local jsonStr \"{\\\"ts\\\":\$ts,\\\"values\\\":{\\\"KNOT_\$gwName\\\":\\\"\$gwName\
\\\",\\\"temp\\\":\$temp}}\"\r\
\n :return \$jsonStr\r\
\n} \r\
\n\r\
\n# array of record strings collected for each advertising MAC address\r\
\n:global macRecords [:toarray \"\"]\r\
\n\r\
\n# process advertisements and update macRecords\r\
\n:local advertisements [/iot/bluetooth/scanners/advertisements/print detail as-value where\
\_\\\r\
\naddress ~ \$addressRegex and \\\r\
\ndata ~ \$advertisingDataRegex and \\\r\
\nrssi > \$rssiThreshold]\r\
\n\r\
\n/iot/bluetooth/scanners/advertisements/clear\r\
\n\r\
\n:foreach adv in=\$advertisements do={\r\
\n:local address (\$adv->\"address\")\r\
\n:local ad (\$adv->\"data\")\r\
\n:local rssi (\$adv->\"rssi\")\r\
\n:local epoch (\$adv->\"epoch\")\r\
\n:local temp [\$from88 [:pick \$ad 28 32]]\r\
\n \r\
\n:local recordStr [\$makeRecord ts=\$epoch gwName=\$gwName temp=\$temp]\r\
\n\r\
\n:if ([:len (\$macRecords->\$address)] > 0) do={\r\
\n:local str (\$macRecords->\$address)\r\
\n:local newStr \"\$str,\$recordStr\"\r\
\n:set (\$macRecords->\$address) \$newStr} else={:set (\$macRecords->\$address) \$recordStr\
}}\r\
\n\r\
\n# TODO: add some logic to decide when we want to send data\r\
\n:local sendData true\r\
\n\r\
\n:if (\$sendData) do={\r\
\n:local jsonStr \"{\"\r\
\n\r\
\n:foreach addr,advRec in=\$macRecords do={\r\
\n:set jsonStr \"\$jsonStr\\\"\$addr\\\":[\$advRec],\"}\r\
\n\r\
\n:local payloadlength\r\
\n:set payloadlength [:len (\$jsonStr)]\r\
\n:local remcom\r\
\n:set remcom [:pick \$jsonStr 0 (\$payloadlength-1)]\r\
\n:set jsonStr \"\$remcom}\"\r\
\n:local message\r\
\n:set message \"\$jsonStr\"\r\
\n:log info \"\$message\";\r\
\n:put (\"[*] Message structured: \$message\")\r\
\n:put (\"[*] Total message size: \$[:len \$message] bytes\")\r\
\n:put (\"[*] Sending message to MQTT broker...\")\r\
\n/iot/mqtt/publish broker=\"\$broker\" topic=\"\$topic\" message=\$message}"
在这种情况下,JSON 消息将如下所示:
{
"2C:C8:1B:4B:BB:0A": [
{
"ts": 1680527467840,
"values": {
"KNOT_1": "1",
"temp": 26460
}
}
],
"DC:2C:6E:0F:C0:3D": [
{
"ts": 1680527464996,
"values": {
"KNOT_1": "1",
"temp": 24750
}
},
{
"ts": 1680527474996,
"values": {
"KNOT_1": "1",
"temp": 24750
}
}
]
}
说明
由于不支持浮点运算 → 小数点后的每次计算都将 "向上取整" 为整数。这就是脚本会将温度和加速度值 放大 1000 倍(乘以 1000)来计算的原因。
因此,如果你看到温度显示为 temp=24750,实际温度是 24.750 C。要添加小数点,你需要在服务器端进行额外的结果 "转换",或在 RouterOS 端进行额外的脚本处理,这会增加设备的 CPU 使用率。
包含 GPS 数据的脚本(可选)
如果你还希望包含 GPS 数据(来自 KNOT 的经度和纬度值),请使用以下脚本:
/system/script/add dont-require-permissions=no name=tracking+gps+temp owner=admin policy=\
ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="# Required package\
s: iot\r\
\n\r\
\n################################ Configuration ################################\r\
\n# Name of an existing MQTT broker that should be used for publishing\r\
\n:local broker \"tb\"\r\
\n\r\
\n# MQTT topic where the message should be published\r\
\n:local topic \"v1/gateway/telemetry\"\r\
\n:local gwtopic \"v1/devices/me/telemetry\"\r\
\n\r\
\n# POSIX regex for filtering advertisement Bluetooth addresses. E.g. \"^BC:33:AC\"\r\
\n# would only include addresses which start with those 3 octets.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local addressRegex \"\"\r\
\n\r\
\n# POSIX regex for filtering Bluetooth advertisements based on their data. Same\r\
\n# usage as with 'addressRegex'.\r\
\n:local advertisingDataRegex \"15ff\"\r\
\n\r\
\n# Signal strength filter. E.g. -40 would only include Bluetooth advertisements\r\
\n# whose signal strength is stronger than -40dBm.\r\
\n# To disable this filter, set it to \"\"\r\
\n:local rssiThreshold \"\"\r\
\n\r\
\n#Name the KNOT. Identity of the unit that will be sending the message. This name will be \
reported to the MQTT broker.\r\
\n:local gwName \"2\"\r\
\n\r\
\n###########GPS#############\r\
\n:global lat\r\
\n:global lon\r\
\n\r\
\n/interface/ppp-client/set ppp-out1 disabled=yes\r\
\n:log info (\"disabling WWAN to get GPS coordinates\")\r\
\n\r\
\n/interface/ppp-client/at-chat ppp-out1 input=\"AT+QGPSCFG=\\\"priority\\\",0\"\r\
\n:log info (\"enabling priority for GPS\")\r\
\n\r\
\n###the time in the delay below is the time that the device will wait for to get the coord\
inate fix\r\
\n:delay 32000ms\r\
\n:log info (\"reading GPS coordinates\")\r\
\n/system/gps/monitor once do={\r\
\n:set \$lat \$(\"latitude\")\r\
\n:set \$lon \$(\"longitude\")\r\
\n}\r\
\n:if (\$lat != \"none\") do={\\\r\
\n:log info (\"enabling priority back to WWAN\")\r\
\n/interface/ppp-client/at-chat ppp-out1 input=\"AT+QGPSCFG=\\\"priority\\\",1\"\r\
\n:log info (\"enabling WWAN\")\r\
\n/interface/ppp-client/set ppp-out1 disabled=no\r\
\n:delay 1000ms\r\
\n###if dial on demand is enabled\r\
\n/ping 1.1.1.1 count=1\r\
\n\r\
\n#the delay below waits for 5 seconds for the ppp connection to get established - this tim\
e can differ based on the signal strength\r\
\n:delay 5000ms\r\
\n:log info (\"posting coordinates via mqtt\")\r\
\n:local gpsmessage \\\r\
\n \"{\\\"latitude\\\":\$lat,\\\r\
\n \\\"longitude\\\":\$lon}\"\r\
\n/iot/mqtt/publish broker=\$broker topic=\$gwtopic message=\$gpsmessage} else={\\\r\
\n:log info (\"could not read GPS coordinates...enabling back WWAN\")\r\
\n/interface/ppp-client/at-chat ppp-out1 input=\"AT+QGPSCFG=\\\"priority\\\",1\"\r\
\n/interface/ppp-client/set ppp-out1 disabled=no\r\
\n:delay 1000ms\r\
\n###if dial on demand is enabled\r\
\n/ping 1.1.1.1 count=1\r\
\n:delay 5000ms\r\
\n}\r\
\n\r\
\n##################################Bluetooth##################################\r\
\n:global invertU16 do={\r\
\n :local inverted 0\r\
\n :for idx from=0 to=15 step=1 do={\r\
\n :local mask (1 << \$idx)\r\
\n :if (\$1 & \$mask = 0) do={\r\
\n :set \$inverted (\$inverted | \$mask)\r\
\n }\r\
\n }\r\
\n return \$inverted\r\
\n}\r\
\n\r\
\n:global le16ToHost do={\r\
\n :local lsb [:pick \$1 0 2]\r\
\n :local msb [:pick \$1 2 4]\r\
\n\r\
\n :return [:tonum \"0x\$msb\$lsb\"]\r\
\n}\r\
\n:local from88 do={\r\
\n :global invertU16\r\
\n :global le16ToHost\r\
\n :local num [\$le16ToHost \$1]\r\
\n\r\
\n # Handle negative numbers\r\
\n :if (\$num & 0x8000) do={\r\
\n :set num (-1 * ([\$invertU16 \$num] + 1))\r\
\n }\r\
\n\r\
\n # Convert from 8.8. Scale by 1000 since floating point is not supported\r\
\n :return ((\$num * 125) / 32)\r\
\n}\r\
\n:put (\"[*] Gathering Bluetooth info...\")\r\
\n\r\
\n:global makeRecord do={\r\
\n :local jsonStr \"{\\\"ts\\\":\$ts,\\\"values\\\":{\\\"KNOT_\$gwName\\\":\\\"\$gwName\
\\\",\\\"temp\\\":\$temp}}\"\r\
\n :return \$jsonStr\r\
\n} \r\
\n\r\
\n# array of record strings collected for each advertising MAC address\r\
\n:global macRecords [:toarray \"\"]\r\
\n\r\
\n# process advertisements and update macRecords\r\
\n:local advertisements [/iot/bluetooth/scanners/advertisements/print detail as-value where\
\_\\\r\
\naddress ~ \$addressRegex and \\\r\
\ndata ~ \$advertisingDataRegex and \\\r\
\nrssi > \$rssiThreshold]\r\
\n\r\
\n/iot/bluetooth/scanners/advertisements/clear\r\
\n\r\
\n:foreach adv in=\$advertisements do={\r\
\n:local address (\$adv->\"address\")\r\
\n:local ad (\$adv->\"data\")\r\
\n:local rssi (\$adv->\"rssi\")\r\
\n:local epoch (\$adv->\"epoch\")\r\
\n:local temp [\$from88 [:pick \$ad 28 32]]\r\
\n \r\
\n:local recordStr [\$makeRecord ts=\$epoch gwName=\$gwName temp=\$temp]\r\
\n\r\
\n:if ([:len (\$macRecords->\$address)] > 0) do={\r\
\n:local str (\$macRecords->\$address)\r\
\n:local newStr \"\$str,\$recordStr\"\r\
\n:set (\$macRecords->\$address) \$newStr} else={:set (\$macRecords->\$address) \$recordStr\
}}\r\
\n\r\
\n# TODO: add some logic to decide when we want to send data\r\
\n:local sendData true\r\
\n\r\
\n:if (\$sendData) do={\r\
\n:local jsonStr \"{\"\r\
\n\r\
\n:foreach addr,advRec in=\$macRecords do={\r\
\n:set jsonStr \"\$jsonStr\\\"\$addr\\\":[\$advRec],\"}\r\
\n\r\
\n:local payloadlength\r\
\n:set payloadlength [:len (\$jsonStr)]\r\
\n:local remcom\r\
\n:set remcom [:pick \$jsonStr 0 (\$payloadlength-1)]\r\
\n:set jsonStr \"\$remcom}\"\r\
\n:local message\r\
\n:set message \"\$jsonStr\"\r\
\n:log info \"\$message\";\r\
\n:put (\"[*] Message structured: \$message\")\r\
\n:put (\"[*] Total message size: \$[:len \$message] bytes\")\r\
\n:put (\"[*] Sending message to MQTT broker...\")\r\
\n/iot/mqtt/publish broker=\"\$broker\" topic=\"\$topic\" message=\$message}"
这里你需要记住 BG77 modem 行为 → KNOT 中使用的 BG77 蜂窝调制解调器无法同时保持蜂窝 CAT-M/NB-IoT 持续连接并获取 GPS 坐标。
当脚本运行时:
- PPP 接口被禁用,GPS 接收被设置为最高优先级(同时蜂窝 PPP 接口关闭)持续 32 秒(此时间可在脚本中更改)。
- (a) 如果设备在配置的 32 秒内成功获取了 GPS 纬度值(如果获取的值不等于 "none"),脚本将构建一条包含捕获的纬度和经度值的 JSON 消息,脚本将为 WWAN 设置最高优先级(将优先级从 GPS 改为 WWAN)并重新启用 PPP 接口(用于互联网访问)。之后,脚本将通过第一次 MQTT publish 发送 GPS 数据 JSON 消息,然后脚本将运行 Bluetooth 部分,在其中构建 Bluetooth 数据 JSON 消息并通过第二次 MQTT publish 发送(发送 x2 条 MQTT 消息 → GPS 数据 MQTT 消息和 Bluetooth 数据 MQTT 消息)。
- (b) 如果设备在 32 秒间隔内未能获取 GPS 纬度值(如果获取的值等于 "none"),脚本将为 WWAN 设置最高优先级(将优先级从 GPS 改为 WWAN),重新启用 PPP 接口(用于互联网访问),并仅处理 Bluetooth 部分,在其中构建 Bluetooth 数据 JSON 消息并通过 MQTT publish 发送(基本上,如果无法获取 GPS 数据,则发送 x1 条包含 Bluetooth 数据的 MQTT 消息)。
调度器
为脚本应用一个调度器,以便 RouterOS 定期自行启动该脚本:
/system/scheduler/add name=bluetoothscheduler interval=50s on-event="/system/script/run tracking"
你可以设置更短和更长的间隔。如果你希望更频繁地发送数据,使数据 "更新鲜" → 设置更短的时间间隔(10-15 秒)。如果你希望发送更少的消息、频率更低 → 可以设置更长的时间间隔(30 分钟以上)。
使用脚本构建的 JSON 消息会为收到的每个负载分配一个 ts 值(时间戳)。这意味着 当脚本运行时,例如每分钟,使用 1 个标签,且该标签每 10 秒广播 1 个负载(即每分钟 6 个负载)→ ThingsBoard 数据(GUI)将每分钟更新一次,每分钟会出现 6 个新条目(每个条目将指示它是在前一个之后 10 秒收到的)。而如果你每 15 分钟发送一次消息,使用 1 个每 10 秒广播一个负载的标签(即每 15 分钟 6*15=90 个负载)→ ThingsBoard 数据(GUI)将每 15 分钟更新一次,但会出现 90 个条目。
ThingsBoard 数据可视化与结果验证
在使用 /system/script/run tracking 或通过调度器运行脚本并刷新 GUI 门户后 → JSON 消息中发现的所有 MAC 地址(标签)都将在 ThingsBoard GUI 下创建为新设备:
![]()
为了帮助你可视化数据,你可以使用内置的 widgets 或创建自己的 widget。
从设备列表中选择标签的 MAC 地址,转到 "Latest telemetry" 部分,选择你希望监控的 KNOT ID,然后点击 "Show on widget" 按钮:
![]()
选择你想使用的 widget,例如在 "Charts" 包下的 "Timeseries Bar Chart",然后点击 "Add to dashboard":
![]()
创建一个新仪表板并随意命名。点击 "Add":
![]()
将 widget 的 "Timewindow" 从 "Realtime-last minute"(默认使用)更改为 "Realtime-last 5 hours",并禁用 "data aggregation function"(选择 "none"):
![]()
为了帮助你更好地可视化结果,编辑该 widget,然后编辑每个 "KNOT_X" 参数/键。为每个键启用 "Show points" 复选框:
![]()
查看 ThingsBoard widget 指南以了解你可用的更多选项。
最终结果将如下所示:
![]()
根据仪表板,我们可以看出:
- 从 ~11:00 到 ~11:30,我们的资产在 KNOT_1 Bluetooth 范围内(1 号仓库内)。
- 从 ~11:30 到 ~11:35,我们的资产被转移到停在仓库附近的车辆(KNOT_2)中(标签同时在两个 KNOT 的范围内)。
- 从 ~11:35 到 ~12:00,标签在卡车(KNOT_2)内 - 正在前往另一个仓库。
- 从 ~12:00 到 ~12:05,资产停在 2 号仓库外,且同时处于 KNOT_2 和 KNOT_3 范围内。
- 从 ~12:05 到 12:30,我们的资产存放在 2 号仓库(KNOT_3)内。
- 从 ~12:30 起,标签再次上路,在卡车(KNOT_2)内。
温度可视化(可选)
从设备列表中选择标签的 MAC 地址,转到 "Latest telemetry" 部分,勾选 temp 参数,然后点击 "Show on widget" 按钮:
![]()
选择你想使用的 widget,例如在 "Charts" 包下的 "Timeseries Line Chart"。点击 "Add to dashboard",然后选择你想添加 widget 的仪表板。
结果将如下所示:
![]()
现在你有一个额外的图表,显示标签温度在不同时间间隔内的变化。
GPS 坐标可视化(可选)
根据 Script that includes GPS data 部分中的脚本,脚本会发送 x2 条 MQTT 消息。每条消息发送到不同的 MQTT topic。GPS 坐标消息将发布到名为 "v1/devices/me/telemetry" 的 topic,而 Bluetooth 数据将发布到名为 "v1/gateway/telemetry" 的 topic。坐标将在 ThingsBoard 设备列表下、特定 gateway 下提供给你:
![]()
同时勾选 "latitude" 和 "longitude" 参数,点击 "Show on widget button",将 "Current bundle" 设置为 "Maps",然后选择 "Route Map - OpenStreetMap" widget:
![]()
最后,点击 "Add to dashboard" 按钮并选择你希望显示 widget 的仪表板。
将 3 个 widget 添加到 1 个仪表板(温度折线图、Bluetooth 报告者柱状图和 GPS 坐标地图)后,你将得到类似这样的结果:
![]()
- 你将有一个显示温度变化的图表(标签周围温度)。
- 你将有一个图表,指示哪个特定 KNOT 发送了报告,从而告诉你标签当前处于哪个 KNOT 的 Bluetooth 范围内。
- 你将有一张显示 KNOT GPS 位置的地图。