Problem / motivation
Tag already carries a Units property (libs/SensorThreshold/Tag.m:54), but it is a pure label — there is no way to actually convert stored values into those units. Sensor engineers routinely log raw ADC counts, mV, or SI base units and then want to view and analyze them in engineering units via a simple linear calibration (y = raw * scale + offset) or a unit swap (Pa↔bar, °C↔°F, counts→%).
Today the only path is to wrap the sensor in a DerivedTag expression. That is heavyweight for a one-line scaling and forfeits the SensorTag identity: disk persistence, thresholds bound to the sensor, and live-ingestion behavior all live on the original tag, not the derived wrapper. So Units announces a unit the data isn't actually in — a real asymmetry.
Proposed feature
Two additive public scalar properties on the base Tag, applied on the read path only (stored data untouched):
DisplayScale (default 1)
DisplayOffset (default 0)
Applied at the output of getXY / getXYRange:
Y = Y * DisplayScale + DisplayOffset;
Because the defaults are the identity transform, every existing tag and every serialized tag behaves exactly as before.
Rough sketch
- Lib/class:
libs/SensorThreshold/Tag.m (property declarations + constructor name-value keys + struct serialization keys), applied in the getXY / getXYRange read path of the concrete kinds (SensorTag, DerivedTag, CompositeTag). StateTag is categorical and out of scope.
- Public API shape:
st = SensorTag('press_a', 'Units', 'bar', 'DisplayScale', 1e-5); % Pa -> bar
st.DisplayOffset = 0;
[t, y] = st.getXY(); % y already in bar
- Open question for triage: whether thresholds/analysis primitives (
getStats, violation checks) should read pre- or post-conversion values. Recommend documenting that the conversion is a display/analysis read transform and applying it consistently at the single getXY/getXYRange seam so downstream consumers inherit it for free.
Value
Opens a fresh dimension (unit / calibration handling) that none of the ~90 open issues touch — the current backlog clusters on analysis primitives, widget parity, and EventViewer. One-line calibration on any tag kind, no DerivedTag scaffolding, and Units finally means what it says.
Constraints check
- Toolbox-free: yes — scalar arithmetic only.
- Backward-compatible: yes — defaults
(1, 0) reproduce current output exactly; existing scripts and serialized tags are unaffected; two scalars round-trip trivially through the existing struct (de)serialization.
- Pure MATLAB / Octave: yes.
- Widget/Tag contract: works through the existing
Tag getXY/getXYRange contract; every subclass and every downstream widget inherits it without changes.
Effort estimate
S–M — property + serialization plumbing on Tag, plus the one-line transform at the getXY/getXYRange seam in each concrete kind, and round-trip tests.
AI-proposed via /feature-scout — needs a human product decision before implementation.
Problem / motivation
Tagalready carries aUnitsproperty (libs/SensorThreshold/Tag.m:54), but it is a pure label — there is no way to actually convert stored values into those units. Sensor engineers routinely log raw ADC counts, mV, or SI base units and then want to view and analyze them in engineering units via a simple linear calibration (y = raw * scale + offset) or a unit swap (Pa↔bar, °C↔°F, counts→%).Today the only path is to wrap the sensor in a
DerivedTagexpression. That is heavyweight for a one-line scaling and forfeits theSensorTagidentity: disk persistence, thresholds bound to the sensor, and live-ingestion behavior all live on the original tag, not the derived wrapper. SoUnitsannounces a unit the data isn't actually in — a real asymmetry.Proposed feature
Two additive public scalar properties on the base
Tag, applied on the read path only (stored data untouched):DisplayScale(default1)DisplayOffset(default0)Applied at the output of
getXY/getXYRange:Because the defaults are the identity transform, every existing tag and every serialized tag behaves exactly as before.
Rough sketch
libs/SensorThreshold/Tag.m(property declarations + constructor name-value keys + struct serialization keys), applied in thegetXY/getXYRangeread path of the concrete kinds (SensorTag,DerivedTag,CompositeTag).StateTagis categorical and out of scope.getStats, violation checks) should read pre- or post-conversion values. Recommend documenting that the conversion is a display/analysis read transform and applying it consistently at the singlegetXY/getXYRangeseam so downstream consumers inherit it for free.Value
Opens a fresh dimension (unit / calibration handling) that none of the ~90 open issues touch — the current backlog clusters on analysis primitives, widget parity, and EventViewer. One-line calibration on any tag kind, no
DerivedTagscaffolding, andUnitsfinally means what it says.Constraints check
(1, 0)reproduce current output exactly; existing scripts and serialized tags are unaffected; two scalars round-trip trivially through the existing struct (de)serialization.TaggetXY/getXYRangecontract; every subclass and every downstream widget inherits it without changes.Effort estimate
S–M — property + serialization plumbing on
Tag, plus the one-line transform at thegetXY/getXYRangeseam in each concrete kind, and round-trip tests.AI-proposed via /feature-scout — needs a human product decision before implementation.