Getting Started

Add Measurements with Limits

Add measurements with limits in TofuPilot: numeric, string and boolean validators, one-sided limits, units, and what makes a measurement useful later.

JJulien Buteau
beginner10 min readMarch 14, 2026
Keep the measurement with its verdict.
Measure
Apply limits
Record value + outcome

A measurement is a named value with units and limits, recorded against a unit. Getting this right is the single highest-leverage decision in a test system, because every analysis you will ever want (yield, capability, drift, Pareto) is computed from measurements and impossible without them.

The Shape of a Measurement

procedure.yaml
main:  - name: Power Rails    python: phases.power_rails    measurements:      - name: Rail 3V3        unit: V        description: TP-100 rev B section 3.1        validators:          - operator: ">="            expected_value: 3.2          - operator: "<="            expected_value: 3.4
phases/power_rails.py
def power_rails(measurements, dmm):    measurements.rail_3v3 = dmm.read_voltage()

Four parts, each doing a job:

FieldPurpose
nameDisplay name, and the source of the Python key
unitDisplayed everywhere, and prevents ambiguity between mV and V
descriptionWhere the spec paragraph reference goes
validatorsThe limits, stored as data

The Python key is derived from the name in snake_case: Rail 3V3 becomes rail_3v3. You can set key explicitly if you want a different one.

Numeric Limits

A two-sided range is two validators:

procedure.yaml
- name: Output Voltage        unit: V        validators:          - operator: ">="            expected_value: 4.75          - operator: "<="            expected_value: 5.25

The available numeric operators:

OperatorMeaning
==Equal to
!=Not equal to
>Greater than
>=Greater than or equal
<Less than
<=Less than or equal

One-Sided Limits

Plenty of measurements have only one meaningful bound. Leakage current has a maximum; insulation resistance has a minimum. Declare exactly one validator:

procedure.yaml
- name: Leakage Current        unit: uA        validators:          - operator: "<="            expected_value: 500      - name: Insulation Resistance        unit: "MΩ"        validators:          - operator: ">="            expected_value: 100

Do not invent a second limit to make the measurement look symmetric. A fabricated lower bound on leakage current produces a Cpk figure that describes nothing. See CPL and CPU.

String Measurements

procedure.yaml
- name: Firmware Version        validators:          - operator: "=="            expected_value: "2.1.0"

String operators include set membership and regex, which is what you want for serial formats and multi-version acceptance:

OperatorExpected valueExample
==String"PASS"
!=String"ERROR"
inArray of strings["v1.0.0", "v1.0.1"]
not inArray of strings["FAIL", "ERROR"]
matchesRegex"^SN[0-9]{8}$"
procedure.yaml
- name: Accepted Firmware        validators:          - operator: "in"            expected_value: ["2.1.0", "2.1.1", "2.2.0"]      - name: Serial Format        validators:          - operator: "matches"            expected_value: "^SN[0-9]{8}$"

Boolean Measurements

procedure.yaml
- name: Lid Closed        validators:          - operator: "=="            expected_value: true
phases/safety.py
def safety(measurements, fixture):    measurements.lid_closed = fixture.lid_closed()

Booleans are the right type for genuinely binary facts: an interlock, a link established, a self-test result. They are the wrong type for something that was measured and then thresholded in Python, which is the most common measurement mistake.

Record the Value, Not Your Verdict

bad/thresholded.py
def power_rails(measurements, dmm):    v = dmm.read_voltage()    measurements.rail_ok = 3.2 <= v <= 3.4   # The number is gone
good/measured.py
def power_rails(measurements, dmm):    measurements.rail_3v3 = dmm.read_voltage()   # Keep the number

The boolean version passes and fails correctly and is worthless a month later. It cannot tell you the rail has drifted from 3.31 to 3.38, cannot produce a Cpk, and cannot justify a limit change. Let the validators do the comparison.

Measurements Without Limits

A measurement with no validators is recorded and reported without a verdict. That is correct for context you want to keep but not judge:

procedure.yaml
- name: Ambient Temperature        unit: "°C"      - name: Chamber Setpoint        unit: "°C"

Useful for HALT steps, environmental context, and any diagnostic value whose acceptable range you do not yet know. Recording them now is what lets you set limits from data later.

Several Measurements in One Phase

A phase records as many measurements as it needs:

procedure.yaml
27 lines
- name: Power Checks    python: phases.power_checks    measurements:      - name: Rail 3V3        unit: V        validators:          - operator: ">="            expected_value: 3.2          - operator: "<="            expected_value: 3.4      - name: Rail 5V0        unit: V        validators:          - operator: ">="            expected_value: 4.8          - operator: "<="            expected_value: 5.2      - name: Quiescent Current        unit: mA        validators:          - operator: "<="            expected_value: 45      - name: Boot Time        unit: ms        validators:          - operator: "<="            expected_value: 2000
phases/power_checks.py
def power_checks(measurements, dmm, dut):    measurements.rail_3v3 = dmm.dc_voltage(1)    measurements.rail_5v0 = dmm.dc_voltage(2)    measurements.quiescent_current = dmm.dc_current_ma()    measurements.boot_time = dut.boot_time_ms()

The phase fails if any of its measurements fail, and the failure message names which one.

Measurements From Operator Input

A value an operator types is still a measurement. Bind the UI component directly to it:

procedure.yaml
- name: Torque Check    python: phases.torque_check    ui:      components:        - key: torque          type: number_input          label: "Screw torque (Nm)"          required: true          bind: measurements.screw_torque    measurements:      - name: Screw Torque        unit: Nm        validators:          - operator: ">="            expected_value: 0.45          - operator: "<="            expected_value: 0.55

A manually recorded torque then trends on a control chart like any instrument reading.

Naming Is Permanent

The name is the identity of the measurement across every run you will ever take. Renaming splits the history and breaks every trend spanning the change.

Poor nameBetterWhy
voltagerail_3v3Which voltage?
current1quiescent_currentMeaningless in six months
test2boot_timeDescribes nothing
tempchamber_temperatureAmbient, chamber, or die?

Specific names also survive the test growing a second, third and fourth voltage.

If You Already Run OpenHTF

The same concepts, expressed with decorators:

fct.py
26 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("rail_3v3")    .in_range(3.2, 3.4)    .with_units(units.VOLT)    .doc("TP-100 rev B section 3.1"),    htf.Measurement("leakage_current_uA")    .in_range(maximum=500)    .with_units(units.MICROAMPERE),    htf.Measurement("firmware_version").equals("2.1.0"),    htf.Measurement("lid_closed").equals(True),)def phase_checks(test):    test.measurements.rail_3v3 = 3.31    test.measurements.leakage_current_uA = 85.0    test.measurements.firmware_version = "2.1.0"    test.measurements.lid_closed = Truetest = htf.Test(phase_checks, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))

Install with pip install "tofupilot[openhtf]", or run it with tofupilot run.

Common Mistakes

MistakeConsequenceFix
Thresholding in Python into a booleanThe number is lost; no Cpk, no driftRecord the value, declare validators
Inventing a second limit for symmetryMeaningless capability figuresOne validator for one-sided limits
Generic namesAmbiguous in six months, unscalableSpecific names chosen once
Renaming laterHistory splits, trends breakTreat names as permanent
Omitting unitsmV and V confusion on a real benchAlways set unit
Discarding operator-entered valuesManual readings never trendbind: measurements.<key>
No descriptionNobody can find the governing spec paragraphReference the document section

More Guides

Put this guide into practice