Sending temperature readings from the TG-BT5-OUT tag to ThingsBoard

我们的 TG-BT5-OUT Bluetooth 标签型号内置了温度传感器。这意味着它可用于测量周围温度。

简介

我们的 TG-BT5-OUT Bluetooth 标签型号内置了温度传感器。这意味着它可用于测量周围温度。

温度值被"写入"到标签将通过 Bluetooth 广播信道广播的广播负载中。根据你配置标签的方式,标签可以周期性地广播负载,也可以在检测到触发时广播,或者两者兼有。你可以在相关章节中找到有关标签配置选项的更多信息。

你可以使用我们的 KNOT 型号在 IoT>Bluetooth>Advertising reports 选项卡下查看/显示这些负载:

负载将显示在 "Advertisement (hex)" 字段中,并且采用十六进制格式。

当标签配置为 MikroTik 格式时,负载的第 14 和第 15 个八位组表示广播时刻"感知"到的温度(以摄氏度为单位的周围温度,采用有符号 16 位整数 [二进制补码] 8.8 定点格式)。

下方展示了 MikroTik 格式的负载示例:

15ff4f09010032a500000000ffff 5019 bd5954000061

请注意,负载中的所有多字节值都采用小端序。因此,如果第 14 和第 15 个八位组将温度表示为 "50 19" → 你将需要交换八位组的位置,改为 0x1950。

正温度:

我们将温度捕获为 0x1950(十六进制)→ 6480(十进制)。要获得实际温度值,请将结果除以 256 → 6480/256=25.31 C。

负温度:

我们将温度捕获为 0xFE80(十六进制)。请记住使用的是二进制补码格式 → -384(有符号 2 的补码的十进制值)。要获得实际温度值,请将结果除以 256 → -384/256=-1.5 C。

有了这些知识,我们就可以利用 RouterOS 脚本从接收到的负载中构造消息,并将其发送到 MQTT/HTTP 服务器。建议由服务器负责将数据从十六进制格式转换为十进制格式。但是!如果你愿意加载 RouterOS 设备,你可以使用 RouterOS 脚本执行十六进制值到十进制格式的转换(这会加载设备并可能影响 RouterOS 性能 → 我们建议在服务器上而非 RouterOS 上转换值)。在本指南中,我们将通过向 ThingsBoard 进行 MQTT 提交来实现这些结果。

ThingsBoard 服务器和 MQTT 配置

你需要有一个运行中的 ThingsBoard 服务器。你可以通过此链接找到有关如何设置服务器的有用信息。你可以将服务器本地安装到机器上,或使用他们的云解决方案。

服务器准备好后,你需要将其配置为能够接收 MQTT 消息。请按照 ThingsBoard 配置链接来设置你的"设备"。

RouterOS 配置

请查看此处的链接,了解如何根据 ThingsBoard 服务器使用的设备凭据设置你的 MQTT broker。

设置好 broker 后,确认 Bluetooth 标签正在广播信息,如上方 IoT>Bluetooth>Advertising reports 选项卡下的图片所示。

脚本

粘贴下面的脚本(在 System>Scripts>Add 下):

## Required packages: iot

################################ Configuration ################################
## Name of an existing MQTT broker that should be used for publishing
 :local broker "tb"

## MQTT topic where the message should be published
 :local topic "v1/devices/me/telemetry"

 # Interface whose MAC should be used as 'Locator ID'
:local locatorIface "ether1"

## 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 ""

####The lines below are used for the data conversion from hex to dec###

    :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 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)
    }

################################## Bluetooth ##################################
:put ("[*] Gathering Bluetooth info...")
: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 btProcessingStart [/system clock get time]
:local advertisements [/iot bluetooth scanners advertisements print detail \
    as-value where \
        epoch > $btOldestAdvertisementTimestamp and \
        address ~ $addressRegex and \
        data ~ $advertisingDataRegex and \
        rssi > $rssiThreshold
]
:local advJson ""
:local advCount 0
:local advSeparator ""
:local lastAdvTimestamp 0
## 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 ts ($adv->"epoch")
    :local rssi ($adv->"rssi")
    :local ad ($adv->"data")
    :local tempf [:pick ($adv->"data") 28 32]

#The line below will set temperature in hexadecimal format, like "0x1950"
    :local temp ([:pick $tempf 2 4].[:pick $tempf 0 2])

#The line below will set temperature in decimal format, like "25313". It is commented with "#", so if you wish to use it instead of the hexadecimal format value, comment the line above and uncomment the line below > remove "#" from the line below and add "#" to the line above.

##    :local temp [$from88 [:pick $ad 28 32]]

    :local obj "{\
        \
           \"ts\":$ts,\
           \"values\":{\
            \"id\":\"$[$minimizeMac address=$address]\",\
            \"rssi\":\"$rssi\",\
            \"temp\":\"$temp\",\
            \"ad\":\"$ad\"\
            }\
        }"
    :set $advCount ($advCount + 1)
    :set $lastAdvTimestamp $ts
    # Ensure that the last object is not terminated by a comma
    :set $advJson "$advJson$advSeparator$obj"
    :if ($advSeparator = "") do={
        :set $advSeparator ","}
}

:if ($advCount > 0) do={

    :set $btOldestAdvertisementTimestamp $lastAdvTimestamp

}

:put ("[*] Found $advCount new advertisements \
    (processing time: $[([/system clock get time] - $btProcessingStart)])")

#################################### MQTT #####################################
:local message \
    "[$advJson]"
:log info "$message";
:put ("[*] Total message size: $[:len $message] bytes")
:put ("[*] Sending message to MQTT broker...")
/iot mqtt publish broker=$broker topic=$topic message=$message
:put ("[*] Done")

在引号内更改你为 MQTT broker 选择的 broker 名称(在我们的示例中,我们将其命名为 tb):

 :local broker "tb"

ThingsBoard 使用的默认 topic 是(你无需更改它):

 :local topic "v1/devices/me/telemetry"

更改你希望监控的标签的 MAC 地址(在我们的示例中,MAC 地址为 2C:C8:1B:4B:BB:0A):

:local addressRegex "2C:C8:1B:4B:BB:0A"

下方各行将获取负载的第 14 和第 15 个八位组,并将值从 "ABCD" "重新构造"为 "CDAB"(按照我们在简介部分中的示例,从 "5019" 到 "1950")→ 基本上构造出以十六进制格式表示温度的消息:

    :local tempf [:pick ($adv->"data") 28 32]
    :local temp ([:pick $tempf 2 4].[:pick $tempf 0 2])

你的第二个选择是使用 RouterOS 将温度从十六进制格式转换为十进制格式(如果你想保留十六进制格式 - 请忽略此步骤)。这可以通过"注释掉"(添加 #)前一行(或完全删除它)并"取消注释"(移除 #)即将到来的行来实现,如下所示:

#  :local temp ([:pick $tempf 2 4].[:pick $tempf 0 2])
    :local temp [$from88 [:pick $ad 28 32]]

由于不支持浮点 → 小数点后的每次计算都会被"向上舍入"为整数。这就是脚本会以缩放 1000 倍(乘以 1000)的方式计算温度的原因,结果温度看起来会像 "25546"(这表示 25.546 C)。你可以添加额外的脚本以设置小数点(但请记住,额外的操作会加载设备),你也可以在服务器本身上执行此操作。

我们提交的 MQTT 消息在以下部分中构造(你无需更改它):

    :local obj "{\
        \
           \"ts\":$ts,\
           \"values\":{\
            \"id\":\"$[$minimizeMac address=$address]\",\
            \"rssi\":\"$rssi\",\
            \"temp\":\"$temp\",\
            \"ad\":\"$ad\"\
            }\
        }"

按照上方示例构造的消息将以 JSON 显示(我们唯一真正关注的值为 "timestamp"→ "ts" 和 "temperature"→ "temp",因此可以随意移除其他参数)。如果温度以十六进制格式计算,消息将如下所示:

[
  {
    "ts": 1661333913375,
    "values": {
      "id": "2CC81B4BBB0A",
      "rssi": "-43",
      "temp": "1939",
      "ad": "15ff4f0901002de0ffff010000003919877c54000061"
    }
  }
]

如果温度以十进制格式计算,则如下所示:

[
  {
    "ts": 1661333913375,
    "values": {
      "id": "2CC81B4BBB0A",
      "rssi": "-43",
      "temp": "25222",
      "ad": "15ff4f0901002de0ffff010000003919877c54000061"
    }
  }
]

保存脚本并运行它:

[admin@MikroTik] > system script run name_of_the_script
[*] Gathering system info...
[*] Gathering Bluetooth info...
[*] Found 1 new advertisements (processing time: 00:00:00)
[*] Total message size: 134 bytes
[*] Sending message to MQTT broker...

[*] Done
[admin@MikroTik] > 

调度器

对脚本应用调度器以按所需间隔运行它。**请注意!**如果你设置了调度器来运行脚本,例如每 2 分钟一次,这将意味着在 2 分钟间隔内接收到的每个负载(由标签广播)都将被"构造"到 MQTT 消息中。你很可能不需要每 2-5 秒知道一次标签的温度,因此你可以更改构造的 MQTT 消息以仅提交最新的温度读数,或者只需将 IoT>Bluetooth>Scanners 设置 Filter duplicates 更改为 "keep-newest"(这样,Bluetooth 扫描器将覆盖来自每个单独标签 MAC 地址的每个新报告),脚本将只提交在脚本运行时"扫描"到的 1 个负载。

结果

你将能够在为 ThingsBoard 创建的设备下的 Latest telemetry 部分验证结果(在我们的示例中,我们将该"设备"命名为 KNOT):

结果是成功的 MQTT 提交!从上方的截图中,我们可以看到 temp 参数采用十六进制格式。

但是!我们希望以十进制格式显示温度(temp 参数),并且我们还希望以更易于感知的方式显示数据。

而且,如果我们以十进制格式提交了温度,我们还希望设置小数点。

这就是 Widgets 发挥作用的地方。

ThingsBoard 小组件

在 Latest telemetry 部分收到值后,你可以在 Widget 上显示它。

小组件中使用的 Javascript 应能够将数据从十六进制格式转换为十进制格式。

选择你希望在小组件中显示的值(在我们的示例中为 temp)并点击 "Show on widget" 按钮:

你将能够在弹出的窗口中浏览小组件:

点击 "Add to dashboard" 按钮:

选择要创建的新仪表板,或选择你已预装/预先制作好的现有仪表板。点击 "Add"。

通过勾选 "Open dashboard" 复选框,你将被重定向到仪表板菜单,或者你也可以手动前往那里。

我们还添加了第二个表格,因此,我们的测试仪表板看起来如下:

如果我们以十六进制格式提交数据

两个表格都将以十六进制格式显示值 "17f9"(我们从未自 KNOT 发送的原始值 → 表格的 javascript 尚未将其转换为十进制值)。

由于我们标签负载中的值采用有符号 8.8 定点格式(记住二进制补码格式),我们将需要添加额外的 javascript 代码来将数据"转换"为十进制值。

进入 "Edit" 模式并点击 "Edit widget" 按钮:

点击 "edit" 按钮以编辑参数 temp:

在弹出的窗口 "Data key configuration" 中启用复选框 "Use data post-processing function" 并输入下方所示的代码:

代码:

function f(p){

this.p = p;

}

f.prototype.calculate = function(mes){

let intValue = parseInt(mes, 16);

let sign = 1;

if(intValue & 0x8000) {

intValue = (~intValue + 1) & 0xFFFF;

sign = -1;

}

intValue = sign * intValue / Math.pow(2, this.p);

return intValue;

}

let res = new f(8);

return (res.calculate(value));

保存并应用更改。

对第二个/第一个表格执行相同的步骤。

如果我们以十进制格式提交数据

你将看到结果如下:

执行上方示例中完全相同的步骤,但在这种情况下,将结果除以 "1000":

保存并应用更改。

验证

最终结果应如下所示:

现在两个表格都显示十进制温度。

信息

你还可以在将遥测数据保存到 ThingsBoard 之前对其进行转换。使用这种方案,每次打开仪表板时数据都不会被后处理。

以下指南可作为转换数据的通用方法:Transformation nodes。