The sensor reports a raw register value that is linear in water column height. Turning it into something a human or a control rule can use is two calibration points and a straight line: the raw value with the tank empty, and the raw value with it full.
Four lines of arithmetic. There is still one interesting decision hiding in it.
pub fn level(&self, raw: i32) -> Option<Level> {
let span = self.raw_full - self.raw_empty;
if span == 0 {
return None; // uncalibrated - not "zero"
}
let fraction = ((raw - self.raw_empty) as f32 / span as f32)
.clamp(0.0, 1.0);
Some(Level {
fraction,
percent: fraction * 100.0,
liters: fraction * self.tank_liters as f32,
mm: fraction * self.tank_height_mm as f32,
})
}What should it return when the two points are equal?
An uncalibrated device has raw_empty == raw_full, so the span is zero.
In C# that division does not throw. Floating-point division by zero yields Infinity, and a tank reported as infinitely full is exactly the sort of value a control rule will act on — it will happily decide the tank is over the high mark and start draining.
Returning Option<Level> moves that decision to the one place that can make it, and makes it impossible to skip. The dashboard shows "uncalibrated"; the control rules refuse to run.
The degenerate calibration
C# - Infinity, then silently clamped to 100%
public double Level(int raw)
{
var span = RawFull - RawEmpty;
// span == 0 does not throw for double division:
// (raw - RawEmpty) / 0.0 -> Infinity
return Math.Clamp((raw - RawEmpty) / (double)span, 0, 1) * 100;
}Rust - absent is not a number
pub fn level(&self, raw: i32) -> Option<Level> {
let span = self.raw_full - self.raw_empty;
if span == 0 {
return None;
}
// ...
}Math.Clamp rescues the C# here by accident — clamping Infinity gives 100 %, so an uncalibrated tank silently reads full. That is worse than a crash: it is a plausible number that no one will question. The Rust version cannot be read as a level at all until someone handles the None.
Nullable does not help here
Returning double? would express the same thing, but nothing forces the caller to unwrap it — level.Value compiles fine and throws at run time. Option<T> has no such escape hatch: you cannot read the value without saying what happens when it is absent.
The calibration on the real device is raw_empty = 4, raw_full = 985, over a 1000 L tank one metre tall. The offset of 4 is why calibration is done against the real tank rather than trusted from the datasheet — an uncalibrated unit would read 0.4 % in an empty tank, and a control rule watching for "empty" would never fire.
The nicest test in the whole crate does not assert a magic number:
It asserts the physical property that a 1000 L tank one metre tall must gain ten litres per centimetre. That test survives a rewrite of the function, and it fails loudly if anyone confuses millimetres with centimetres — which is the single most likely mistake in this file.
#[test]
fn ten_liters_per_cm() {
let c = Calibration::default();
let a = c.level(400).unwrap();
let b = c.level(410).unwrap(); // +10 mm = +1 cm
assert!((b.liters - a.liters - 10.0).abs() < 1e-3);
}Check yourself
Not graded — just to see whether it landed.
1.An uncalibrated device has raw_empty equal to raw_full. What does the C# version above report?
2.Why assert ten litres per centimetre rather than a specific litre value?