Good day i am needing some help I have a bluetooth sensor that i have connected and whitelisted it to a MikroTik KNOT LR8 router but I am not sure as to where or how to script. I have the info that the company gave me is there any one that could help me out.
bool parse_packet(bd_addr addr, byte[] scanData)
{
// Check for expected length
if (scanData.Length != 18) return false;
// Manufacturer specific
if (scanData[1] != 0xFF) return false;
// Length of trailing packet
if (scanData[0] != 13) return false;
// Check data in address matches mac - part of our protocol for iOS fix
if (scanData[9] != addr.Address[2]) return false;
if (scanData[10] != addr.Address[1]) return false;
if (scanData[11] != addr.Address[0]) return false;
// Check for manufacturer code
if (scanData[2] != 0x59 || scanData[3] != 0x00) return false;
// Read sensor version – set as appropriate
prod_version = scanData[4] & 0x7F;
if (prod_version != 0x03 && prod_version != 0x05) return false;
// Check that this is our "sync"ed sensor
if (addr != synced_mac) return false;
return true;
}
bool is_sync_pressed(uint8_t *scanData)
{
if (scanData[6] & 0x80) {
return true;
}
return false;
}
getAcceloY(rawByte) {
// Return value in “G’s” which can be scaled to an angle
return (char)scanData[12] / 1024;
}
getAcceloX(rawByte) {
// Return value in “G’s” which can be scaled to an angle
return (char)scanData[13] / 1024
// Returns the measured fluid height (or air height), in mm
// scanData - represents the array of raw bytes for the manufacturing data
uint32_t get_tank_height(const uint8_t* scanData)
{
// For LPG (Liquid propane) use the following coefficients
const double coef[3] = { 0.573045, -0.002822, -0.00000535 };
// Retrieve raw level from data. Only the LS 14-bits represent the level
uint16_t raw = (scanData[8] * 256) + scanData[7];
double raw_level = raw & 0x3FFF;
// add the range 16384us to 81916us with 4us resolution
if (scanData[4] & 0x80) {
raw_level = 16384.0 + raw_level * 4.0;
}
// Retrieve unscaled temperature from advert packet
double raw_t = (scanData[6] & 0x7F);
// MSbit is button state - ignored
// Apply 2nd order polynomial to compensating the raw TOF into mm of LPG
return (uint32_t)(raw_level * (coef[0] + coef[1] * raw_t + coef[2] * raw_t *
raw_t));
}
float get_battery_voltage(uint8_t *scanData)
{
// Return battery voltage – note that MSbit is reserved and should be set 0
return (float)(scanData[5] & 0x7F) / 32.0f;
}
// Arbitrary scaling of battery voltage to percent for CR2032
float get_battery_percentage(float voltage)
{
float percent = (voltage - 2.2f) / 0.65f * 100.0f;
if (percent < 0.0f) {
return 0.0f;
}
if (percent > 100.0f) {
return 100.0f;
}
return percent;
}
int get_temperature_reading(uint8_t *scanData)
{
uint8_t temp = scanData[6] & 0x7F;
return ((int)temp - 40);
}
Most of that is portable to RouterOS script. The tricker part is the floats. So are you getting data from if you check “/iot bluetooth scanners advertisements print”?
RouterOS can decode some formats, did you try some data from scanner in a “/iot bluetooth decode-ad data=”?
I don’t have a KNOT handy or your device… but as partial UNTESTED example to loop through the adverts. The part is the [:pick $pktdata ] to get bytes from the ->“data” of each advertising packet.
As you can see they do have interpolation and :pick is the accessor for array of chars (which can be hexcodes), so that main thing to use to get your data out. Now why the C code gets tricky since there are NO floating point numbers. So all the /1024 and coefficients pose a bit more of a challenge. Still possible, but you need to use multiplication to increase the scale to be able still get whole numbers that can be converted to string.
But… see if you’re actually getting data, that be step one here.
And, the decode-ad only support some known data structures mention in docs. Your device apparently isn’t one, or close enough.
To access the raw bytes, it not so simple in RouterOS script. Since you’re starting with hexstring (e.g. base-16 bytes encoded as ASCII chars), you’d need to convert it to an array of ints to make it easy. Something like this function may help to convert the hex values to an array of type int.
Since that will get you an array, you can access the bytes and do any math like the C code. For example, the accelleration X/Y and temp using bit-wise operators from C (and the the slightly different index accessor ->)
And if you store the results of print an a variables – by using an “as-value” at end to cause print to return an array – you use a :foreach to get the ->data and then call the hex2ints function shown above to get them as indexed array of bytes. An index array makes “porting” the C code a bit easier since you can access the individual bytes in the BT message. RouterOS arrays are also 0-index so the first check in C to see if byte 1 is 0xFF looks like this:
{
:global hex2ints
:local adverts [ /iot bluetooth scanners advertisements print as-value]
:foreach advert in=$adverts do={
:local bytes [$hex2ints ($advert->"data")]
:put "debug: $[:tostr $bytes]"
:if (($bytes->0) != 13) do={:put "ERROR: 1st byte must be 13 or 0x0D"}
:if (($bytes->1) != 0xFF) do={:put "ERROR: 2nd byte must be FF"}
:local syncPressed [:tobool (($bytes->6) & 0x80)]
:put "sync pressed $syncPressed"
}
}
Now that LPG one from the C code… That’s a bit trickier to port. There are no floating point numbers, that involve some additional multiplication and division, to achieve the desired scale. e.g. RouterOS will round to nearest int if division results in a floating point number.
this is what it gives me back
[admin@MikroTik] > system script run test
13;255;89;0;3;94;66;0;0;36;104;78;41;232;3;2;229;254;
y=66 x=0 temp=26
now the one that is confusing me is
// Returns the measured fluid height (or air height), in mm
// scanData - represents the array of raw bytes for the manufacturing data
uint32_t get_tank_height(const uint8_t* scanData)
{
// For LPG (Liquid propane) use the following coefficients
const double coef[3] = { 0.573045, -0.002822, -0.00000535 };
// Retrieve raw level from data. Only the LS 14-bits represent the level
uint16_t raw = (scanData[8] * 256) + scanData[7];
double raw_level = raw & 0x3FFF;
// Check for presence of extension bit on certain hardware/firmware -
// it will always be 0 on old firmware/hardware and raw value saturates
// at 16383. When extension bit is set, the resolution changes to 4us
// with 16384us offet for wider range. Thus legacy sensors and
// firmware still have 0 to 16383us range with 1us, and new versions
// add the range 16384us to 81916us with 4us resolution
if (scanData[4] & 0x80) {
raw_level = 16384.0 + raw_level * 4.0;
}
// Retrieve unscaled temperature from advert packet
double raw_t = (scanData[6] & 0x7F); // MSbit is button state - ignored
// Apply 2nd order polynomial to compensating the raw TOF into mm of LPG
return (uint32_t)(raw_level * (coef[0] + coef[1] * raw_t + coef[2] * raw_t *
raw_t));
}
Hmm, the sample data here doesn’t actually the raw data for the needed math. e.g.
scanData[4] is 3 or bin 00000011
scanData[7] and scanData[8] are both 0
If you had another sample, you might try this function. More for example — you check the math. The hard part is there is no “double” type (e.g. floating point numbers). So in RouterOS you need to scale the variables so there integers, then do the math.
# returns num in mm, arg is num-array from BT advert data
:global gettankheight do={
:local bytes $1
# Get raw level from data. Only the LS 14-bits represent the level
:local u16raw (($bytes->8) * 256 + ($bytes->7))
:put "u16raw= $u16raw"
:local rawlevel ($u16raw & 0x3FFF)
:put "rawlevel= $rawlevel"
# Check for presence of extension bit on certain hardware/firmware
:if (($bytes->4) & 0x80) do={
:set rawlevel (16384 + $rawlevel * 4)
:put "rawlevel= $rawlevel"
}
# Retrieve unscaled temperature from advert packet
:local rawtemp (($bytes->6) & 0x7F)
:put "rawtemp= $rawtemp"
# Apply 2nd order polynomial to compensating the raw TOF into mm of LPG
:local scale 100000
:put "scale= $scale"
:local coef { 57304500000, -2822000, -535 }
:put "coefs= $[:tostr $coef]"
:local lpgtof1 [:tonum (($coef->0) + ($coef->1) * ($rawtemp*$scale) + ($coef->2))]
:put "lpgtof1= $lpgtof1"
:local lpgtof2 ($lpgtof1 * [:tonum ($rawtemp*$scale)])
:put "lpgtof2= $lpgtof2"
:local lpgtof3 ($lpgtof2 * [:tonum ($rawtemp*$scale)])
:put "lpgtof3= $lpgtof3"
:local rv ($rawlevel * ($lpgtof3/$scale))
:put "lpgtofX= $lpgtof1 $lpgtof2 $lpgtof3"
:put "rv= $rv"
:return $rv
}
Which you can call with the “bytes” from the hex2ints functions
Like I said, just an example. And the sample data you provided here do NOT have “raw_level” needed to actually calculate mm from the TOF read from sensors.
# Main function to decode Bluetooth advertisement data
/iot bluetooth scanners advertisements {
:local adids [find]
:foreach adid in=$adids do={
:local pkt [get $adid]
:local pktdata ($pkt->"data")
:if ([:len $pktdata]!=36 || [:pick $pktdata 1 2]="\FF") do={:error "bad packet: got $pktdata with length $[:len $pktdata]"}
# Extract fields from advertisement data
:local batteryVoltage [:pick $pktdata 10 12]
:local temperature [:pick $pktdata 12 14]
:local tankLevel [:pick $pktdata 14 18]
:put "payload:$pktdata"
:put "batteryVoltage: $batteryVoltage"
:put "temperature: $temperature"
:put "tankLevel: $tankLevel"
}
}
I get this when I run it
[admin@MikroTik] > system script run test7
payload:0dff5900035f42000024684e2d000302e5fe
batteryVoltage: 5f
temperature: 42
tankLevel: 0000
[admin@MikroTik] >
is it possible to convert the hex to decimal per item batteryVoltage, temperature and tankLevel or must the whole payload be converted to decimal
Essentially the conversion from hex-in-a-string to an int take the following form today: [:tonum “0x$[:pick $pktdata ( + ) ]”]
So in your code it look like this:
# Main function to decode Bluetooth advertisement data
/iot bluetooth scanners advertisements {
:local adids [find]
:foreach adid in=$adids do={
:local pkt [get $adid]
:local pktdata ($pkt->"data")
:if ([:len $pktdata]!=36 || [:pick $pktdata 1 2]="\FF") do={:error "bad packet: got $pktdata with length $[:len $pktdata]"}
# Extract fields from advertisement data
:local batteryVoltage [:tonum "0x$[:pick $pktdata 10 (10 + 2) ]"]
:local temperature [:tonum "0x$[:pick $pktdata 12 (12 + 2) ]"]
:local tankLevel [:tonum "0x$[:pick $pktdata 14 (10 + 4) ]"]
:put "payload:$pktdata"
:put "batteryVoltage: $batteryVoltage"
:put "temperature: $temperature"
:put "tankLevel: $tankLevel"
}
}
Now the tank level has more complex final calculation, per the C code, but I suspose it will always be relative. As noted above, the math involves floating point numbers. That’s where this get more complex. I’ve been using the example string, which has a value of 0 for the tank level. If you had the same raw string from BT with tank data & some result from another tool that did this calculation – that help to test the tank level “coefficient” math in the posting a few up.
[admin@MikroTik] > system script run test10
payload: 0dff5900035f41000024684e2dfd0302e5fe
batteryVoltage: 2.96 V
temperature: 25 C
tankLevel: 0 MM
[admin@MikroTik] >
You put your code INTO scheduler’s on-event. It would just need to be added once. The scheduler (aka cron) will then run your script on the interval= set.
Using winbox, you should be able to cut-and-paste your code above to the “On Event”, set Start Time as “startup”, and interval as 00:00:10. Or via CLI:
. But in either case, you make further update to the code in the scheduler.
Theoretically, you can write new code that add a scheduler to do some specific work in future (without a interval=) and the when that code run, it remove old scheduler and add a new scheduled item (without interval=). But this approach is complex and unneeded if your want it to run “every 10 seconds” as interval= will do all the work for you.
@Amm0 will give it a try and I got a payload with the sensor on a gas bottle 0dff5900035e41f5c124684e20000302e5fe, f5c1 is the two parts of the payload for the gas bottle volume with the app for the sensor it was saying there was 20,2cm of gas in the bottle. now I need to figure out the math of that.
@Amm0 would it be possible to send the bluetooth data packet with MQTT, if it is possible is it done by subscription topic or by script?
I am thinking something like this but note to sure where i am going wrong.