The Tank Level Probe comes in three different versions. The only difference is the actual probe, which is calibrated for the fluid it is used on. All features and functions are the same for these versions.
1 kg/L
0.840 kg/L
0.755 kg/L
The Tank Level Probes are constructed from an Elsys ELTi and a calibrated level probe. To ensure they can be used with the standard Elsys firmware, the wires of the level probe needs to be connected in the following way:
B+
IN
Ground
To configure the Elsys ELTi, it is needed to use the standard smartphone App from Elsys:
This App is using NFC to transfer settings from the App to the Elsys ELTi. This means we need to physically be able to touch the ELTi with the back of the smartphone in order to read or write settings. This is different from a Bluetooth connection like we use with the Smart Parks Edge sensors, where there can be several meters distance between the device and the smartphone. Also note that NFC does not create a “connection” like with Bluetooth. Only when you “touch” the ELTi with the back of you smartphone where the NFC antenna is positioned, the App will read of write data (settings).
READ
the current sensor settings.600 seconds
(10 minutes)Analog input 0-3V
1
1
1
200 ms
25 ms
1
1
1
WRITE
to device button in the App and then hold the back of your smartphone to the back of the ELTi to WRITE
the current sensor settings.The LoRaWAN credentials and settings and can be found when reading the settings from the ELTi with the Elsys - Sensor settings App. Standard the device is set to use OTAA.
The tank level probe is using pressure measurements to estimate what the amount of fluid is pressing against the sensor. Different fluids have different weights, therefore the sensor is calibrated to a specific fluid (water, diesel, petrol). The pressure sensor is situated inside the level probe. To accurately measure the amount of liquid above the sensor, the sensor needs to places at the bottom of the tank.
Also ensure that the ELTi is placed on a place where it has line-of-sight to the nearest LoRaWAN gateway.
Maintenance needs to be done on two elements of the set-up:
The Tank Level Probe is using the standard Elsys ELTi firmware. Therefore is will work with the standard LoRaWAN decoders and encoders provides by Elsys.
Smart Parts users have been using custom versions of the decoder to ensure it can be used to output the actual tank filling level. For this it is needed to add some additional items to the standard decoder.
Note that this decoded is just an example and it is formatted to be used on ChirpStack v3. For TTN or other LoRaWAN servers it may need some adaptation.
Example tank level decoder:
/*
______ _ _______ _______
| ____| | / ____\ \ / / ____|
| |__ | | | (___ \ \_/ / (___
| __| | | \___ \ \ / \___ \
| |____| |____ ____) | | | ____) |
|______|______|_____/ |_| |_____/
ELSYS simple payload decoder.
Use it as it is or remove the bugs :)
www.elsys.se
peter@elsys.se
Smart Parks Combined Decoder:
- Rain Gauge = Pulse1
- Tank Level = Analog1
*/
var TYPE_TEMP = 0x01; //temp 2 bytes -3276.8°C -->3276.7°C
var TYPE_RH = 0x02; //Humidity 1 byte 0-100%
var TYPE_ACC = 0x03; //acceleration 3 bytes X,Y,Z -128 --> 127 +/-63=1G
var TYPE_LIGHT = 0x04; //Light 2 bytes 0-->65535 Lux
var TYPE_MOTION = 0x05; //No of motion 1 byte 0-255
var TYPE_CO2 = 0x06; //Co2 2 bytes 0-65535 ppm
var TYPE_VDD = 0x07; //VDD 2byte 0-65535mV
var TYPE_ANALOG1 = 0x08; //VDD 2byte 0-65535mV
var TYPE_GPS = 0x09; //3bytes lat 3bytes long binary
var TYPE_PULSE1 = 0x0A; //2bytes relative pulse count
var TYPE_PULSE1_ABS = 0x0B; //4bytes no 0->0xFFFFFFFF
var TYPE_EXT_TEMP1 = 0x0C; //2bytes -3276.5C-->3276.5C
var TYPE_EXT_DIGITAL = 0x0D; //1bytes value 1 or 0
var TYPE_EXT_DISTANCE = 0x0E; //2bytes distance in mm
var TYPE_ACC_MOTION = 0x0F; //1byte number of vibration/motion
var TYPE_IR_TEMP = 0x10; //2bytes internal temp 2bytes external temp -3276.5C-->3276.5C
var TYPE_OCCUPANCY = 0x11; //1byte data
var TYPE_WATERLEAK = 0x12; //1byte data 0-255
var TYPE_GRIDEYE = 0x13; //65byte temperature data 1byte ref+64byte external temp
var TYPE_PRESSURE = 0x14; //4byte pressure data (hPa)
var TYPE_SOUND = 0x15; //2byte sound data (peak/avg)
var TYPE_PULSE2 = 0x16; //2bytes 0-->0xFFFF
var TYPE_PULSE2_ABS = 0x17; //4bytes no 0->0xFFFFFFFF
var TYPE_ANALOG2 = 0x18; //2bytes voltage in mV
var TYPE_EXT_TEMP2 = 0x19; //2bytes -3276.5C-->3276.5C
var TYPE_EXT_DIGITAL2 = 0x1A; // 1bytes value 1 or 0
var TYPE_EXT_ANALOG_UV = 0x1B; // 4 bytes signed int (uV)
var TYPE_DEBUG = 0x3D; // 4bytes debug
function bin16dec(bin) {
var num = bin & 0xFFFF;
if (0x8000 & num)
num = -(0x010000 - num);
return num;
}
function bin8dec(bin) {
var num = bin & 0xFF;
if (0x80 & num)
num = -(0x0100 - num);
return num;
}
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
function DecodeElsysPayload(data) {
var obj = new Object();
for (i = 0; i < data.length; i++) {
//console.log(data[i]);
switch (data[i]) {
case TYPE_TEMP: //Temperature
var temp = (data[i + 1] << 8) | (data[i + 2]);
temp = bin16dec(temp);
obj.temperature = temp / 10;
i += 2;
break
case TYPE_RH: //Humidity
var rh = (data[i + 1]);
obj.humidity = rh;
i += 1;
break
case TYPE_ACC: //Acceleration
obj.x = bin8dec(data[i + 1]);
obj.y = bin8dec(data[i + 2]);
obj.z = bin8dec(data[i + 3]);
i += 3;
break
case TYPE_LIGHT: //Light
obj.light = (data[i + 1] << 8) | (data[i + 2]);
i += 2;
break
case TYPE_MOTION: //Motion sensor(PIR)
obj.motion = (data[i + 1]);
i += 1;
break
case TYPE_CO2: //CO2
obj.co2 = (data[i + 1] << 8) | (data[i + 2]);
i += 2;
break
case TYPE_VDD: //Battery level
obj.vdd = (data[i + 1] << 8) | (data[i + 2]);
i += 2;
break
case TYPE_ANALOG1: //Analog input 1
T=variables.tankType; //define tank orientation (1=vertical, else=horizontal)
D=variables.tankDia_m; //define tank diameter
L=variables.tankLength_m; //define tank length/height
Q=variables.tankQty; //define number of tanks
var analog1Raw = (data[i + 1] << 8) | (data[i + 2]);
obj.analog1 = analog1Raw;
//Smart Parks Tank Level Add_on
analog1Raw = bin16dec(analog1Raw);
if (analog1Raw <= 100) {
obj.tankDepth_m = 0;
} else {
obj.tankDepth_m = (analog1Raw - 100) / 580;
var h = obj.tankDepth_m;
if (T == 1) {
Capacity = Q * (3.14159265 * Math.pow((D / 2),2) * h)*1000; //calculate capacity
obj.fillpercentage = h / L * 100; //calculate fill %
obj.tankType = "Vertical"
} else {
var r = D / 2; //calculate radius
Capacity = Q * ((Math.acos((r - h)/r) * Math.pow(r,2) - (r - h) * Math.sqrt(2* r * h - Math.pow(h,2))) * L)*1000; //calculate capacity
obj.fillpercentage = (Capacity / (3.14159265 * Math.pow(r,2) * L * 1000) * 100) / Q; //calculate fill %
obj.tankType = "Horizontal"
}
obj.Capacity = Math.round(Capacity);
}
i += 2;
break
case TYPE_GPS: //gps
obj.lat = (data[i + 1] << 16) | (data[i + 2] << 8) | (data[i + 3]);
obj.long = (data[i + 4] << 16) | (data[i + 5] << 8) | (data[i + 6]);
i += 6;
break
case TYPE_PULSE1: //Pulse input 1
var pulse1Raw = (data[i + 1] << 8) | (data[i + 2]);
obj.pulse1 = pulse1Raw;
//Smart Parks Rain Gauge Add_on
obj.bucket_flops = pulse1Raw;
pulse1Raw = bin16dec(pulse1Raw);
obj.percipitation_mm = pulse1Raw * 0, 5; //change resolution if needed
i += 2;
break
case TYPE_PULSE1_ABS: //Pulse input 1 absolute value
var pulseAbs = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]);
obj.pulseAbs = pulseAbs;
i += 4;
break
case TYPE_EXT_TEMP1: //External temp
var temp = (data[i + 1] << 8) | (data[i + 2]);
temp = bin16dec(temp);
obj.externalTemperature = temp / 10;
i += 2;
break
case TYPE_EXT_DIGITAL: //Digital input
obj.digital = (data[i + 1]);
i += 1;
break
case TYPE_EXT_DISTANCE: //Distance sensor input
obj.distance = (data[i + 1] << 8) | (data[i + 2]);
i += 2;
break
case TYPE_ACC_MOTION: //Acc motion
obj.accMotion = (data[i + 1]);
i += 1;
break
case TYPE_IR_TEMP: //IR temperature
var iTemp = (data[i + 1] << 8) | (data[i + 2]);
iTemp = bin16dec(iTemp);
var eTemp = (data[i + 3] << 8) | (data[i + 4]);
eTemp = bin16dec(eTemp);
obj.irInternalTemperature = iTemp / 10;
obj.irExternalTemperature = eTemp / 10;
i += 4;
break
case TYPE_OCCUPANCY: //Body occupancy
obj.occupancy = (data[i + 1]);
i += 1;
break
case TYPE_WATERLEAK: //Water leak
obj.waterleak = (data[i + 1]);
i += 1;
break
case TYPE_GRIDEYE: //Grideye data
i += 65;
break
case TYPE_PRESSURE: //External Pressure
var temp = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]);
obj.pressure = temp / 1000;
i += 4;
break
case TYPE_SOUND: //Sound
obj.soundPeak = data[i + 1];
obj.soundAvg = data[i + 2];
i += 2;
break
case TYPE_PULSE2: //Pulse 2
obj.pulse2 = (data[i + 1] << 8) | (data[i + 2]);
i += 2;
break
case TYPE_PULSE2_ABS: //Pulse input 2 absolute value
obj.pulseAbs2 = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]);
i += 4;
break
case TYPE_ANALOG2: //Analog input 2
obj.analog2 = (data[i + 1] << 8) | (data[i + 2]);
i += 2;
break
case TYPE_EXT_TEMP2: //External temp 2
var temp = (data[i + 1] << 8) | (data[i + 2]);
temp = bin16dec(temp);
obj.externalTemperature2 = temp / 10;
i += 2;
break
case TYPE_EXT_DIGITAL2: //Digital input 2
obj.digital2 = (data[i + 1]);
i += 1;
break
case TYPE_EXT_ANALOG_UV: //Load cell analog uV
obj.analogUv = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]);
i += 4;
break
default: //somthing is wrong with data
i = data.length;
break
}
}
return obj;
}
// TTN decoder function, using the ChirpStack decoder
function Decoder(bytes) {
return Decode(port, bytes);
}
// ChirpStack decode function
function Decode(fPort, bytes) {
return DecodeElsysPayload(bytes);
}
If you want to understand more about the calculations used, please check this: