A device reports what its inputs measure: a voltage on an analogue input, a CAN odometer in metres, engine running time in seconds. Your customer wants litres in the tank, kilometres driven and engine hours. Between the two sit an expression, a calibration table, bounds and a set of report calculators.
Each piece has its own reference page. They do not show the order in which the pieces run, or which value each later stage receives. Get the order wrong and the numbers look plausible but are wrong. A fuel chart shows phantom drains, or a tank holds more than its capacity.
Sensors belong to the device, not the unit. You define them on the device's Sensors tab; see Device form and the devices overview. Each sensor has a type, and each type has fixed telemetry fields. Sensor types lists them.
The same sensor code runs at ingest, for the unit card, the map and alerts. The report engine reruns it over stored raw messages, so the two values cannot drift apart.
The report engine then feeds the resolved values to calculators. A calculator is one family of logic, for example movement (trips and parkings), fuel (drains, fillings, consumption) or counter (mileage and engine-hour deltas). Calculators write their results as intervals, and reports read the intervals.
When you save a sensor change, the engine recalculates the units that device was on. By default it reaches back 60 days. Older intervals keep the values the old configuration produced, so invoiced figures do not change under a customer.
For every message, each field of each sensor goes through the same stages:
{param} with this message's value. A parameter the message lacks becomes "no value".[Sensor#field] waits until that field has its final value, after its own table and bounds. Its expression runs next, followed by its own table and bounds.A condition from the Conditional tab is an expression with ? :, so it runs at stage 2. Sensor conditions covers it.
Harbor Fresh Foods runs Truck 12, with a 300 l saddle tank and an analogue probe. The device reports the probe voltage in millivolts as adc1. The probe gives 0.5 V empty and 9.5 V full, and the saddle shape makes the curve uneven.
Put the unit conversion in the expression and the tank shape in the table. On the FUEL sensor's level field, write:
{adc1} / 1000
The expression now gives volts. Measure the tank in volts against litres and load the points on the Calibration tab:
0.5;0
1.5;22
3.0;66
5.0;135
7.0;205
8.5;260
9.5;300
Generate coefficients turns seven points into six segments, Y = aX + b:
| From X (V) | A | B |
|---|---|---|
| 0.5 | 22 | -11 |
| 1.5 | 29.333333 | -22 |
| 3.0 | 34.5 | -37.5 |
| 5.0 | 35 | -40 |
| 7.0 | 36.666667 | -51.666667 |
| 8.5 | 40 | -80 |
Now trace three messages through the stages:
adc1 = 6230 -> 6.23 V -> segment from 5.0: 35 x 6.23 - 40 = 178.05 l
adc1 = 120 -> 0.12 V -> below the first X = 0 l
adc1 = 10240 -> 10.24 V -> last segment: 40 x 10.24 - 80 = 329.6 l
The second and third results are faults: a shorted wire and a saturated probe. The 0 l reading looks like a 178 l drain, and 329.6 l is more than the tank holds.
Bounds fix both. Set Lower Bound to 0.5 and Upper Bound to 9.6, and leave Apply after calculation off. The bounds then test the volts before the table converts them. The app discards both faulty messages, so the chart shows a gap instead of a false event.
The trade-off: bounds on the raw side catch broken hardware, bounds on the converted side catch impossible litres. Pick the side where your faults show up. Field details are in the Calibration table dialog.
Why not fold the division into the table? You can, with X in millivolts. Keeping units in the expression means a firmware change that reports volts needs one edit, and your measured points stay valid.
Each calculator reads one field per sensor type, and expects a fixed unit:
| Sensor type | Field key | Read as |
|---|---|---|
| FUEL | level |
The tank's volume, in the sensor's unit |
| ODOMETER, RELATIVE_ODOMETER | total_meter |
Kilometres, despite the key's name |
| ENGINE_HOURS | input |
Hours |
| IGNITION | v |
On when not zero |
The expression has to deliver those units. Truck 12's CAN odometer arrives in metres as can_odo, so its ODOMETER field is {can_odo} / 1000. An engine counter in seconds needs {eng_sec} / 3600.
With one FUEL sensor per tank, mark the one that counts as the unit's level with the sensor's report setting. Without the mark, the engine takes the first FUEL sensor on the device, whether or not it reports.
Follow Truck 12 through a morning:
07:10 trip starts fuel 178.05 l odometer 184250.4 km
09:40 trip ends fuel 151.31 l odometer 184412.9 km
10:05 depot fuel 292.40 l
The movement calculator stores the trip with 162.5 km and fuel at start and end. The fuel calculator smooths the level and finds no drain on the road. Consumption for the drive comes to about 26.7 l, roughly 16.5 l/100 km. At the depot it detects a filling of about 141 l. Smoothing and thresholds come from the Fuel Config tab; see Fuel detection.
The counter calculator turns ODOMETER and ENGINE_HOURS readings into hourly deltas. A drop is a counter reset and counts as zero, never as negative mileage. In the default configuration, a single step above 1,000 km or 1,000 hours counts as a glitch and the calculator drops it. The unit's running totals in Counters follow their own source rules; see Counters.
[Fuel tank#level] matches the sensor name exactly. Fix: update every expression that names the sensor.