Community discussions

MikroTik App
 
User avatar
DenSyo77
just joined
Topic Author
Posts: 24
Joined: Tue Jan 09, 2024 10:38 am
Contact:

Collecting telemetry from Bluetooth and RS485 sensors

Tue Jan 09, 2024 2:52 pm

Concept of collecting telemetry from multiple sensors. The script builds a global array of sensor states in the system and an arrays of reading history. Polling rules are currently implemented for Bluetooth Tags MikroTik TG-BT5-OUT and Teltonika Eye Sensor BTSMP1, and for Modbus temperature sensor with typical parameters. The script is designed to be extensible to support new devices.

Initialization script. This script describes the devices in the system and methods for polling them. Save this script with name iotInitEnvironment.
# iotInitEnvironment - init environment, set default values

# Array with sensors in the system.
# ->"device" - service field, the value must be equal to one of the keys from the iotDevices array.
# ->"address" - service field, the value must contain the address of the Bluetooth device in capital letters, the
# address of the RS485 device as a number or string, the addresses of system devices must be unique.
:global iotSensors {
  {
    "device"="RouterBOARD";
    "address"="board"
  };
  {
    "device"="RouterGPS";
    "address"="gps"
  };
  {
    "device"="RelsibDVT05";
    "role"="room.main";
    "address"=3;
    "name"="DVT_000009"
  };
  {
    "device"="TeltonikaBTSMP1";
    "role"="loggia.door";
    "address"="7C:D9:XX:XX:XX:XA"
  };
  {
    "device"="TeltonikaBTSMP1";
    "role"="automobile";
    "address"="7C:D9:XX:XX:XX:XB"
  };
  {
    "device"="MikroTikTGBT5OUT";
    "role"="nursery.window";
    "address"="DC:2C:XX:XX:XX:X1";
    "name"="TGBT5_XXXXX1"
  };
  {
    "device"="MikroTikTGBT5OUT";
    "role"="kitchen.window";
    "address"="DC:2C:XX:XX:XX:X2";
    "name"="TGBT5_XXXXX2"
  };
  {
    "device"="MikroTikTGBT5OUT";
    "role"="room.window";
    "address"="DC:2C:XX:XX:XX:X3";
    "name"="TGBT5_XXXXX3"
  }
}

# Array of sensors models properties. Fields from the device model array will be transferred to each sensor of the
# corresponding model, fields whose names begin with an underscore are considered as service and will not be transferred.
# ->"type" or "_type" - required field, contains the device type from: system, btl, rs485
# ->"_polling" - field containing the function executed when polling the sensor
# ->"_init" - field containing a function that is executed once when the sensors array is initialized
:global iotDevices {
  "RouterBOARD"={
    "type"="system"
  };
  "RouterGPS"={
    "type"="system"
  };
  "MikroTikTGBT5OUT"={
    "model"="MikroTik TG-BT5-OUT";
    "type"="btl"
  };
  "TeltonikaBTSMP1"={
    "model"="Teltonika Eye Sensor BTSMP1";
    "type"="btl"
  };
  "RelsibDVT05"={
    "model"="Relsib DVT-05";
    "type"="rs485"
  }
}

# MikroTik TG-BT5-OUT Bluetooth Tag fields values calculation
:set ($iotDevices->"MikroTikTGBT5OUT"->"_polling") do={
  :global iotSensors
  :global iotErrors
  :global from88
  :global le32ToHost
  
  :if ([:pick $sensorData 0 8] != "15ff4f09") do={ :return ($iotErrors->"header") }
  
  :set ($iotSensors->$sensorId->"flags") [:tonum "0x$[:pick $sensorData 40 42]"]
  :set ($iotSensors->$sensorId->"temperature") ([$from88 [:pick $sensorData 28 32]] / 10)
  :set ($iotSensors->$sensorId->"uptime") [$le32ToHost [:pick $sensorData 32 40]]
  :set ($iotSensors->$sensorId->"accelx") [$from88 [:pick $sensorData 16 20]]
  :set ($iotSensors->$sensorId->"accely") [$from88 [:pick $sensorData 20 24]]
  :set ($iotSensors->$sensorId->"accelz") [$from88 [:pick $sensorData 24 28]]
  :set ($iotSensors->$sensorId->"battery") [:tonum "0x$[:pick $sensorData 42 44]"]
  :set ($iotSensors->$sensorId->"low") (($iotSensors->$sensorId->"battery") <= 15)
  :set ($iotSensors->$sensorId->"magnet") ((($iotSensors->$sensorId->"flags") & 1) > 0)
  
  :return []
}

# Teltonika Eye Sensor BTSMP1 Bluetooth Tag fields values calculation
:set ($iotDevices->"TeltonikaBTSMP1"->"_polling") do={
  :global iotSensors
  :global iotErrors
  :global hexstr2chrstr
  
  :if ([:pick $sensorData 0 6] != "020106") do={ :return ($iotErrors->"header") }
  
  :local prevCounter ($iotSensors->$sensorId->"counter")
  :local startPacket (([:tonum "0x$[:pick $sensorData 6 8]"] + 4) * 2)
  :set ($iotSensors->$sensorId->"flags") [:tonum "0x$[:pick $sensorData ($startPacket + 10) ($startPacket + 12)]"]
  :set ($iotSensors->$sensorId->"name") [$hexstr2chrstr [:pick $sensorData 10 $startPacket]]
  :set ($iotSensors->$sensorId->"temperature") [:tonum "0x$[:pick $sensorData ($startPacket + 12) ($startPacket + 16)]"]
  :set ($iotSensors->$sensorId->"humidity") ([:tonum "0x$[:pick $sensorData ($startPacket + 16) ($startPacket + 18)]"] * 10)
  :set ($iotSensors->$sensorId->"state") [:tonum "0x$[:pick $sensorData ($startPacket + 18) ($startPacket + 20)]"]
  :set ($iotSensors->$sensorId->"counter") [:tonum "0x$[:pick $sensorData ($startPacket + 20) ($startPacket + 22)]"]
  :set ($iotSensors->$sensorId->"pitch") [:tonum "0x$[:pick $sensorData ($startPacket + 22) ($startPacket + 24)]"]
  :if (($iotSensors->$sensorId->"pitch") & 128) do={ :set ($iotSensors->$sensorId->"pitch") (($iotSensors->$sensorId->"pitch") - 256) }
  :set ($iotSensors->$sensorId->"roll") [:tonum "0x$[:pick $sensorData ($startPacket + 24) ($startPacket + 28)]"]
  :if (($iotSensors->$sensorId->"roll") & 32768) do={ :set ($iotSensors->$sensorId->"roll") (($iotSensors->$sensorId->"roll") - 65536) }
  :set ($iotSensors->$sensorId->"volt") ([:tonum "0x$[:pick $sensorData ($startPacket + 28) ($startPacket + 30)]"] * 10 + 2000)
  :set ($iotSensors->$sensorId->"low") ((($iotSensors->$sensorId->"flags") & 64) > 0)
  :set ($iotSensors->$sensorId->"magnet") ((($iotSensors->$sensorId->"flags") & 8) > 0)
  :set ($iotSensors->$sensorId->"moved") ([:typeof $prevCounter] != "nil" && [:typeof $prevCounter] != "nothing" \
    && ($iotSensors->$sensorId->"counter") != $prevCounter)
  
  :return []
}

# Relsib DVT-05 RS485 sensor fields values calculation
:set ($iotDevices->"RelsibDVT05"->"_polling") do={
  :global iotSensors
  :local sensorData [/iot modbus transceive address=$sensorAddress function=3 values=0,0,0,2 as-value]
  
  :if (($sensorData->"status") != "ok") do={ :return ($sensorData->"error-description") }
  
  :set ($iotSensors->$sensorId->"time") ($sensorData->"time")
  :set ($iotSensors->$sensorId->"temperature") (($sensorData->"values"->0) * 10)
  :set ($iotSensors->$sensorId->"humidity") ($sensorData->"values"->1)
  
  :return []
}

# MikroTik system GPS fields values calculation
:set ($iotDevices->"RouterGPS"->"_polling") do={
  :global iotSensors
  :global iotErrors
  :local sensorData [/system gps monitor as-value once]
  
  :if (![:tobool ($sensorData->"valid")]) do={ :return ($iotErrors->"lost") }
  
  :set ($iotSensors->$sensorId->"latitude") ($sensorData->"latitude")
  :set ($iotSensors->$sensorId->"longitude") ($sensorData->"longitude")
  :set ($iotSensors->$sensorId->"altitude") ($sensorData->"altitude")
  :set ($iotSensors->$sensorId->"time") ($sensorData->"date-and-time")
  :set ($iotSensors->$sensorId->"speed") ($sensorData->"speed")
  :set ($iotSensors->$sensorId->"satellites") ($sensorData->"satellites")
  
  :return []
}

# MikroTik RouterBOARD fields values calculation
:set ($iotDevices->"RouterBOARD"->"_polling") do={
  :global iotSensors
  :global iotErrors
  :local sensorData [/system resource print as-value]
  
  :if (($sensorData->"platform") != "MikroTik") do={ :return ($iotErrors->"lost") }
  
  :set ($iotSensors->$sensorId->"uptime") ($sensorData->"uptime")
  :set ($iotSensors->$sensorId->"architecture") ($sensorData->"architecture-name")
  :set ($iotSensors->$sensorId->"board") (($sensorData->"platform")." ".($sensorData->"board-name"))
  :set ($iotSensors->$sensorId->"version") ($sensorData->"version")
  :set ($iotSensors->$sensorId->"freemem") ($sensorData->"free-memory")
  :set ($iotSensors->$sensorId->"totalmem") ($sensorData->"total-memory")
  :set ($iotSensors->$sensorId->"cpu") (($sensorData->"cpu")." ".($sensorData->"cpu-frequency")."MHz")
  :set ($iotSensors->$sensorId->"freehdd") ($sensorData->"free-hdd-space")
  :set ($iotSensors->$sensorId->"totalhdd") ($sensorData->"total-hdd-space")
  :set ($iotSensors->$sensorId->"bad") ($sensorData->"bad-blocks")
  
  :set sensorData [/system health print as-value]
  
  :set ($iotSensors->$sensorId->"volt") ($sensorData->0->"value")
  :set ($iotSensors->$sensorId->"temperature") (($sensorData->1->"value") * 100)
  
  :return []
}

# A string containing all btl sensors addresses for using to search data. Querying from the package system for all devices at
# once is more faster than queryings for each device.
:global iotBtlSensorsRegular ""

# Array of btl devices from iotSensors
:global iotBtlSensorsIds [:toarray ""]

# Array of rs485 devices from iotSensors
:global iotRs485SensorsIds [:toarray ""]

# Array of system data from iotSensors
:global iotSystemSensorsIds [:toarray ""]

# Errors messages
:global iotErrors {
  "lost"="sensor is lost";
  "init"="error execute init";
  "polling"="error execute polling";
  "header"="wrong packet header"
}

# Number of steps of failed sensor polling attempts before a lost condition occurs.
:global iotLostCountSetError 6

# Array with storage of data from previous steps of sensors polling. At each step sensors polling specified by variable
# iotIntervalSmallStoring, previous sensors values are entered into the first element of array iotSmallStoring, previous
# entries in the array are shifted. The size of the array is set by the variable iotLenSmallStoring. iotCounterSmallStoring
# - steps counter, allways set as 0. When the sensors polling script is launched with an interval 10 seconds, the value of
# the variable iotLenSmallStoring is 29, the value of the variable iotIntervalSmallStoring is 1, this array stores sensors
# readings for the last 5 minutes every 10 seconds.
:global iotSmallStoring [:toarray ""]
:global iotLenSmallStoring 29
:global iotIntervalSmallStoring 1
:global iotCounterSmallStoring 0

# Array with storage of data from previous steps of sensors polling with big interval. At each step sensors polling specified
# by variable iotIntervalBigStoring, the element of array iotSmallStoring corresponding to the elapsed interval is entered
# into the first element of array iotBigStoring, previous entries in the array are shifted. When the sensors polling script
# is launched with an interval 10 seconds, the value of the variable iotLenBigStoring is 35, the value of the variable
# iotIntervalBigStoring is 30, this array stores sensors readings for the last 3 hours every 5 minutes.
:global iotBigStoring [:toarray ""]
:global iotLenBigStoring 35
:global iotIntervalBigStoring 30
:global iotCounterBigStoring 0

# Post-processing of devices array values
:foreach deviceId,deviceData in=$iotDevices do={
  :if ([:typeof ($iotDevices->$deviceId->"type")] != "nothing" && [:typeof ($iotDevices->$deviceId->"_type")] = "nothing") do={
    :set ($iotDevices->$deviceId->"_type") ($iotDevices->$deviceId->"type")
  }
}

# Creating fields in sensors arrays with empty or default values
:for idx from=0 to=([:len $iotSensors] - 1) step=1 do={
  :set ($iotSensors->$idx->"error") []
  
  :if (($iotDevices->($iotSensors->$idx->"device")->"_type") = "btl") do={
    :set ($iotSensors->$idx->"epoch") 0
    :set ($iotSensors->$idx->"time") []
    :set ($iotSensors->$idx->"lost") 0
    :set ($iotSensors->$idx->"rssi") []
    :set ($iotBtlSensorsIds->($iotSensors->$idx->"address")) $idx
    :if ($iotBtlSensorsRegular = "") do={
      :set iotBtlSensorsRegular ($iotSensors->$idx->"address")
    } else={
      :set iotBtlSensorsRegular ($iotBtlSensorsRegular.";".($iotSensors->$idx->"address"))
    }
  }
  
  :if (($iotDevices->($iotSensors->$idx->"device")->"_type") = "rs485") do={
    :set ($iotSensors->$idx->"lost") 0
    :set ($iotRs485SensorsIds->($iotSensors->$idx->"address")) $idx
  }
  
  :if (($iotDevices->($iotSensors->$idx->"device")->"_type") = "system") do={
    :set ($iotSystemSensorsIds->($iotSensors->$idx->"address")) $idx
  }
  
  :foreach k,v in=($iotDevices->($iotSensors->$idx->"device")) do={
    :if ([:pick $k 0] != "_") do={
      :set ($iotSensors->$idx->$k) $v
    } else={
      :if ($k = "_init") do={
        :do {
          :set ($iotSensors->$idx->"error") [$v sensorId=$idx]
        } on-error={
          :set ($iotSensors->$idx->"error") ($iotErrors->"init")
        }
      }
    }
  }
}

######################### General purpose functions #########################

# Thanks to Rextended
# https://forum.mikrotik.com/viewtopic.php?f=9&t=129693&p=871742#p871742
:global hexstr2chrstr do={
  :local hexstr $1
  :local hexlen [:len $hexstr]
  :local chk1 ""
  :local chk2 ""
  :local chrstr ""
  :local lowerarray {"a"="A";"b"="B";"c"="C";"d"="D";"e"="E";"f"="F"}
  :for x from=0 to=($hexlen - 2) step=2 do={
    :set chk1 [:pick $hexstr $x ($x + 1)]
    :set chk2 [:pick $hexstr ($x + 1) ($x + 2)]
    :if ($chk1~"[a-f]") do={ :set chk1 ($lowerarray->$chk1) }
    :if ($chk2~"[a-f]") do={ :set chk2 ($lowerarray->$chk2) }
    :if (($chk1~"[^0-9A-F]") || ($chk2~"[^0-9A-F]")) do={ :set chk1 "3"; :set chk2 "F" }
    :set chrstr "$chrstr$[[:parse "(\"\\$chk1$chk2\")"]]"
  }
  :return $chrstr
}

# MikroTik script of decoding
# https://help.mikrotik.com/docs/display/UM/MikroTik+Tag+advertisement+formats
: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"]
}

:global from88 do={
  :global le16ToHost
  :global invertU16
  :local num [$le16ToHost $1]
  :if ($num & 0x8000) do={
    :set num (-1 * ([$invertU16 $num] + 1))
  }
  :return (($num * 125) / 32)
}

:global 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"]
}

Script for polling devices and storing reading history. Save this script with name iotSensorsPolling and create a schedule to run this script every 10 seconds.
# iotSensorsPolling - polling sensors status
# With current settings the script is designed to run once every 10 seconds

:global iotDevices
:global iotSensors
:global iotBtlSensorsRegular
:global iotBtlSensorsIds
:global iotRs485SensorsIds
:global iotSystemSensorsIds
:global iotLostCountSetError
:global iotSmallStoring
:global iotLenSmallStoring
:global iotIntervalSmallStoring
:global iotCounterSmallStoring
:global iotBigStoring
:global iotLenBigStoring
:global iotIntervalBigStoring
:global iotCounterBigStoring
:global iotErrors

:if ([:typeof $iotSensors] = "nothing") do={
  /system script run iotInitEnvironment
}

:global iotIsSensorsStoring true

# iotBigStoring
:if ($iotCounterBigStoring = $iotIntervalBigStoring) do={
  :set iotCounterBigStoring 0
  :local idxSmallStoring (($iotIntervalBigStoring - 2) / $iotIntervalSmallStoring)
  :if ($idxSmallStoring > ($iotLenSmallStoring - 1)) do={ :set idxSmallStoring ($iotLenSmallStoring - 1) }
  
  :for idx from=($iotLenBigStoring - 1) to=1 step=-1 do={
    :set ($iotBigStoring->$idx) ($iotBigStoring->($idx - 1))
  }
  :set ($iotBigStoring->0) ($iotSmallStoring->$idxSmallStoring)
}
:set iotCounterBigStoring ($iotCounterBigStoring + 1)

# iotSmallStoring
:if ($iotCounterSmallStoring = $iotIntervalSmallStoring) do={
  :set iotCounterSmallStoring 0
  
  :for idx from=($iotLenSmallStoring - 1) to=1 step=-1 do={
    :set ($iotSmallStoring->$idx) ($iotSmallStoring->($idx - 1))
  }
  :set ($iotSmallStoring->0) $iotSensors
}
:set iotCounterSmallStoring ($iotCounterSmallStoring + 1)

:set iotIsSensorsStoring false
:global iotIsSensorsPolling true

# System devices
:foreach sensorAddress,idx in=$iotSystemSensorsIds do={
  :if ([:typeof $idx] != "nil") do={
    :do {
      :set ($iotSensors->$idx->"error") [($iotDevices->($iotSensors->$idx->"device")->"_polling") sensorId=$idx sensorAddress=$sensorAddress]
    } on-error={
      :set ($iotSensors->$idx->"error") ($iotErrors->"polling")
    }
  }
}

# RS485 sensors
:foreach sensorAddress,idx in=$iotRs485SensorsIds do={
  :if ([:typeof $idx] != "nil") do={
    :local isExecError false
    
    :do {
      :set ($iotSensors->$idx->"error") [($iotDevices->($iotSensors->$idx->"device")->"_polling") sensorId=$idx sensorAddress=$sensorAddress]
    } on-error={
      :set isExecError true
    }
    
    :if (!$isExecError) do={
      :set ($iotSensors->$idx->"lost") 0
    } else={
      :if (($iotSensors->$idx->"lost") <= $iotLostCountSetError) do={
        :set ($iotSensors->$idx->"lost") (($iotSensors->$idx->"lost") + 1)
        :set ($iotSensors->$idx->"error") ($iotErrors->"polling")
      }
    }
  }
}

# Bluetooth sensors
:if ([:len $iotBtlSensorsIds] > 0) do={
  :local lastEpoch 0
  
  :foreach sensorAddress,idx in=$iotBtlSensorsIds do={
    :if (($iotSensors->$idx->"epoch") > 0 && (($iotSensors->$idx->"epoch") < $lastEpoch || $lastEpoch = 0)) do={
      :set lastEpoch ($iotSensors->$idx->"epoch")
    }
    :if (($iotSensors->$idx->"lost") <= $iotLostCountSetError) do={
      :set ($iotSensors->$idx->"lost") (($iotSensors->$idx->"lost") + 1)
    }
  }
  
  :local sensorDetails [/iot bluetooth scanners advertisements print detail as-value where $iotBtlSensorsRegular~address and epoch>$lastEpoch]
  
  :foreach adv in=$sensorDetails do={
    :local sensorAddress ($adv->"address")
    :local idx ($iotBtlSensorsIds->$sensorAddress)
    
    :if (($iotSensors->$idx->"epoch") < ($adv->"epoch")) do={
      :set ($iotSensors->$idx->"lost") 0
      :set ($iotSensors->$idx->"epoch") ($adv->"epoch")
      :set ($iotSensors->$idx->"time") ($adv->"time")
      :set ($iotSensors->$idx->"rssi") ($adv->"rssi")
      
      :do {
        :set ($iotSensors->$idx->"error") [($iotDevices->($iotSensors->$idx->"device")->"_polling") sensorId=$idx sensorAddress=$sensorAddress sensorData=($adv->"data")]
      } on-error={
        :set ($iotSensors->$idx->"error") ($iotErrors->"polling")
      }
    }
  }
}

# Lost check
:for idx from=0 to=([:len $iotSensors] - 1) step=1 do={
  :if (($iotSensors->$idx->"lost") = $iotLostCountSetError) do={
    :set ($iotSensors->$idx->"error") ($iotErrors->"lost")
  }
}

:set iotIsSensorsPolling false

Script for outputting sensor readings as JSON.
# iot get sensors json

:global iotSensors
:global iotIsSensorsPolling

:if ([:typeof $iotSensors] = "nothing") do={
  /system script run iotSensorsPolling
}

:while ($iotIsSensorsPolling) do={ :delay 20ms }

:local outJson {
  "time"=[:totime ([/system clock get date]." ".[/system clock get time])];
  "sensors"=$iotSensors
}

:set outJson [serialize $outJson to=json]
:return $outJson

Script for outputting the history of short-period readings as JSON.
# iot get sensors small storing json

:global iotSmallStoring
:global iotIsSensorsStoring

:if ([:typeof $iotSmallStoring] = "nothing") do={
  /system script run iotSensorsPolling
}

:while ($iotIsSensorsStoring) do={ :delay 20ms }

:local outJson {
  "time"=[:totime ([/system clock get date]." ".[/system clock get time])];
  "storing"=$iotSmallStoring
}

:set outJson [serialize $outJson to=json]
:return $outJson

Script for outputting the history of long-period readings as JSON.
# iot get sensors big storing json

:global iotBigStoring
:global iotIsSensorsStoring

:if ([:typeof $iotBigStoring] = "nothing") do={
  /system script run iotSensorsPolling
}

:while ($iotIsSensorsStoring) do={ :delay 20ms }

:local outJson {
  "time"=[:totime ([/system clock get date]." ".[/system clock get time])];
  "storing"=$iotBigStoring
}

:set outJson [serialize $outJson to=json]
:return $outJson

Maintaining the history of sensor readings is intended for use in scripts on MikroTik as reference data for decision making. Temperature readings of all sensors are presented as an integer containing a hundredth part, for example, 24.65 as 2465. Humidity readings from all sensors are presented as an integer containing a decimal part.

script updated 2024-01-10

Who is online

Users browsing this forum: No registered users and 8 guests