Skip to content
Test Types & Methods

Environmental Testing with TofuPilot

Learn how to log thermal cycling, humidity, HALT, and environmental screening test results in TofuPilot for reliability tracking.

JJulien Buteau
intermediate10 min readMarch 13, 2026

Environmental Testing with TofuPilot

Products fail in the field because of temperature, humidity, vibration, and altitude. Environmental testing simulates these conditions in the lab. TofuPilot tracks every environmental test result so you can build a reliability picture across your entire product lifecycle.

Types of Environmental Tests

TestWhat it simulatesStandards
Thermal cyclingTemperature extremes during shipping and operationIEC 60068-2-14, MIL-STD-810
Thermal shockRapid temperature transitionsIEC 60068-2-14
Humidity testingMoisture exposure, condensationIEC 60068-2-78
HALT (Highly Accelerated Life Test)Find design weaknesses fastNo formal standard
HASS (Highly Accelerated Stress Screen)Production screeningNo formal standard
VibrationMechanical stress during transport and operationIEC 60068-2-6, IEC 60068-2-64
AltitudeLow pressure environmentsIEC 60068-2-13
Salt sprayCorrosion resistanceIEC 60068-2-11

Logging Thermal Cycling Results

A thermal cycling test runs a product through temperature extremes for hundreds of cycles. After each cycle (or batch of cycles), perform a functional check and log results.

thermal_cycling_test.py
from tofupilot import TofuPilotClient

client = TofuPilotClient()

def log_thermal_cycle_check(serial, cycle_number, temp_profile, functional_results):
    """Log post-cycle functional check results."""
    measurements = [
        {"name": "cycle_number", "value": cycle_number, "unit": "cycles"},
        {"name": "temp_high_c", "value": temp_profile["high"], "unit": "°C"},
        {"name": "temp_low_c", "value": temp_profile["low"], "unit": "°C"},
        {"name": "dwell_time_min", "value": temp_profile["dwell"], "unit": "min"},
    ]

    # Add functional measurements taken after thermal exposure
    for name, result in functional_results.items():
        measurements.append({
            "name": name,
            "value": result["value"],
            "unit": result["unit"],
            "limit_low": result.get("limit_low"),
            "limit_high": result.get("limit_high"),
        })

    all_pass = all(
        r.get("limit_low", float("-inf")) <= r["value"] <= r.get("limit_high", float("inf"))
        for r in functional_results.values()
    )

    client.create_run(
        procedure_id="THERMAL-CYCLING-500",
        unit_under_test={"serial_number": serial},
        run_passed=all_pass,
        steps=[{
            "name": f"Post-Cycle {cycle_number} Functional Check",
            "step_type": "measurement",
            "status": all_pass,
            "measurements": measurements,
        }],
    )

# Example: check after every 50 cycles
for cycle in range(50, 501, 50):
    functional = {
        "vcc_3v3": {"value": measure_voltage(), "unit": "V", "limit_low": 3.25, "limit_high": 3.35},
        "current_ma": {"value": measure_current() * 1000, "unit": "mA", "limit_low": 30, "limit_high": 60},
        "comm_check": {"value": 1 if uart_ok() else 0, "unit": "bool", "limit_low": 1},
    }
    log_thermal_cycle_check(
        serial="EVT-UNIT-003",
        cycle_number=cycle,
        temp_profile={"high": 85, "low": -40, "dwell": 15},
        functional_results=functional,
    )

Tracking Degradation Over Cycles

The key question in environmental testing: "At what point does the product start degrading?"

TofuPilot's measurement trending shows how each parameter changes across thermal cycles:

  • Cycle 50: vcc_3v3 = 3.31V (nominal)
  • Cycle 200: vcc_3v3 = 3.30V (still nominal)
  • Cycle 350: vcc_3v3 = 3.28V (drifting)
  • Cycle 450: vcc_3v3 = 3.26V (approaching limit)
  • Cycle 500: vcc_3v3 = 3.24V (below limit, FAIL)

This degradation curve tells you the product survives 400+ cycles before the 3.3V rail drifts out of spec. If your spec requires 500 cycles, you need a design change.

HALT Testing

HALT finds design weaknesses by pushing products beyond their specifications. There are no pass/fail limits. The goal is to find the breaking point.

halt_test.py
# HALT step stress results
halt_steps = [
    {"stress": "Cold Step", "temp_c": -40, "functional": True},
    {"stress": "Cold Step", "temp_c": -50, "functional": True},
    {"stress": "Cold Step", "temp_c": -60, "functional": False},  # Lower operating limit found
    {"stress": "Hot Step", "temp_c": 85, "functional": True},
    {"stress": "Hot Step", "temp_c": 95, "functional": True},
    {"stress": "Hot Step", "temp_c": 105, "functional": False},  # Upper operating limit found
    {"stress": "Vibration", "grms": 10, "functional": True},
    {"stress": "Vibration", "grms": 20, "functional": True},
    {"stress": "Vibration", "grms": 30, "functional": False},  # Vibration limit found
]

steps = []
for h in halt_steps:
    steps.append({
        "name": f"{h['stress']} - {'temp_c' in h and f\"{h['temp_c']}°C\" or f\"{h['grms']}Grms\"}",
        "step_type": "measurement",
        "status": h["functional"],
        "measurements": [{
            "name": "functional_after_stress",
            "value": 1 if h["functional"] else 0,
            "unit": "pass/fail",
        }],
    })

client.create_run(
    procedure_id="HALT-STEP-STRESS",
    unit_under_test={"serial_number": "EVT-UNIT-001"},
    run_passed=True,  # HALT always "passes" - it's about finding limits
    steps=steps,
)

Environmental Test Matrix

Track which environmental tests have been completed across your product variants.

Product variantThermal cycleHumidityVibrationHALTESD
Rev A500 cycles ✓85/85 ✓10G ✓Done ✓8kV ✓
Rev B250/500PendingPendingDone ✓8kV ✓
Rev C (new layout)Not startedNot startedNot startedPendingNot started

TofuPilot tracks completion automatically. If THERMAL-CYCLING-500 has a passing run for "Rev A," it shows as complete. No spreadsheet maintenance needed.

Production Screening (HASS/ESS)

Environmental stress screening in production catches infant mortality failures. Run every unit through a short thermal cycle and vibration profile, then functional test.

production_screen.py
# Quick production screen: 10 thermal cycles + vibration
client.create_run(
    procedure_id="ESS-PRODUCTION-SCREEN",
    unit_under_test={"serial_number": "PROD-UNIT-4521"},
    run_passed=True,
    steps=[
        {
            "name": "Post-Thermal Functional",
            "step_type": "measurement",
            "status": True,
            "measurements": [
                {"name": "vcc_3v3", "value": 3.31, "unit": "V", "limit_low": 3.25, "limit_high": 3.35},
                {"name": "current_ma", "value": 44, "unit": "mA", "limit_low": 30, "limit_high": 60},
            ],
        },
        {
            "name": "Post-Vibration Functional",
            "step_type": "measurement",
            "status": True,
            "measurements": [
                {"name": "vcc_3v3", "value": 3.30, "unit": "V", "limit_low": 3.25, "limit_high": 3.35},
                {"name": "current_ma", "value": 45, "unit": "mA", "limit_low": 30, "limit_high": 60},
            ],
        },
    ],
)

Track ESS yield separately from functional test yield. If ESS catches 0.5% of units that passed functional test, it's paying for itself by preventing field failures.

More Guides

Put this guide into practice