Quand une mesure échoue dans OpenHTF, le comportement par défaut est de marquer la phase comme échouée et de continuer l'exécution des phases suivantes. En production, vous avez besoin d'un contrôle délibéré : quand arrêter prématurément, quand retenter, et comment préserver les données d'échec dans TofuPilot pour l'analyse.
Prérequis
- Client Python TofuPilot installé :
pip install tofupilot - OpenHTF installé :
pip install openhtf - Familiarité de base avec les phases et mesures OpenHTF
Comportement par défaut en cas d'échec de phase
Les phases OpenHTF retournent un PhaseResult. Quand une mesure échoue, la phase est marquée FAIL mais l'exécution continue sauf si vous l'arrêtez explicitement.
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
@htf.measures(
htf.Measurement('rail_3v3_voltage').in_range(3.2, 3.4).with_units(units.VOLT),
htf.Measurement('rail_5v_voltage').in_range(4.85, 5.15).with_units(units.VOLT),
)
def check_power_rails(test):
test.measurements.rail_3v3_voltage = 3.1 # échoue : en dessous du minimum
test.measurements.rail_5v_voltage = 5.02 # réussit
@htf.measures(
htf.Measurement('uart_echo').equals('OK'),
)
def check_uart(test):
# Cette phase s'exécute même si check_power_rails a échoué
test.measurements.uart_echo = 'OK'
def main():
test = htf.Test(check_power_rails, check_uart)
with TofuPilot(test):
test.execute(test_start=lambda: 'SN-001')Les deux phases s'exécutent. L'exécution est envoyée à TofuPilot comme FAIL avec la mesure défaillante spécifique enregistrée.
Arrêt au premier échec
Pour les tests matériels où un rail d'alimentation défaillant rend les tests suivants sans objet, arrêtez prématurément avec PhaseResult.STOP.
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
@htf.measures(
htf.Measurement('rail_3v3_voltage').in_range(3.2, 3.4).with_units(units.VOLT),
)
def check_power_rails(test):
test.measurements.rail_3v3_voltage = read_adc_channel(0)
if not test.measurements.rail_3v3_voltage.is_pass:
test.logger.error('Rail d\'alimentation hors spécification, abandon de la séquence')
return htf.PhaseResult.STOP
@htf.measures(
htf.Measurement('uart_echo').equals('OK'),
)
def check_uart(test):
response = send_uart_command('PING')
test.measurements.uart_echo = response
def main():
test = htf.Test(check_power_rails, check_uart)
with TofuPilot(test):
test.execute(test_start=lambda: 'SN-001')Quand check_power_rails retourne PhaseResult.STOP, check_uart est ignorée. TofuPilot enregistre l'exécution comme FAIL avec les phases check_uart marquées comme non exécutées.
Retenter une phase en cas d'échec
Les nouvelles tentatives sont utiles pour les échecs transitoires : timeouts de communication, tensions en stabilisation ou contacts intermittents. Utilisez PhaseResult.REPEAT avec un compteur pour éviter les boucles infinies.
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
MAX_RETRIES = 3
@htf.measures(
htf.Measurement('i2c_device_present').equals(True),
)
def check_i2c_device(test, retries=[0]):
found = probe_i2c_address(0x48)
test.measurements.i2c_device_present = found
if not found and retries[0] < MAX_RETRIES:
retries[0] += 1
test.logger.warning('Périphérique I2C non trouvé, tentative %d/%d', retries[0], MAX_RETRIES)
return htf.PhaseResult.REPEAT
retries[0] = 0 # réinitialiser pour les prochaines exécutions
@htf.measures(
htf.Measurement('i2c_temperature').in_range(-10, 85).with_units(units.DEGREE_CELSIUS),
)
def read_i2c_temperature(test):
temp = read_temperature_sensor(0x48)
test.measurements.i2c_temperature = temp
def main():
test = htf.Test(check_i2c_device, read_i2c_temperature)
with TofuPilot(test):
test.execute(test_start=lambda: 'SN-002')L'utilisation d'un argument par défaut mutable (retries=[0]) permet de persister le compteur entre les appels REPEAT au sein de la même exécution de phase. Réinitialisez-le avant de retourner pour éviter un état obsolète sur le prochain DUT.
Nouvelle tentative avec backoff via un plug
Pour les lignes de production avec plusieurs montages, un plug permet de réutiliser la logique de nouvelle tentative et de garder le code des phases propre.
import time
import openhtf as htf
from openhtf.plugs import BasePlug
class CommPlug(BasePlug):
def setUp(self):
import serial
self._port = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
def send_with_retry(self, command, expected, attempts=3, delay=0.5):
response = ''
for attempt in range(attempts):
self._port.write(f'{command}
'.encode())
response = self._port.readline().decode().strip()
if response == expected:
return response, attempt + 1
time.sleep(delay)
return response, attempts
def tearDown(self):
if self._port:
self._port.close()import openhtf as htf
from tofupilot.openhtf import TofuPilot
from plugs.comms_plug import CommPlug
@htf.plug(comm=CommPlug)
@htf.measures(
htf.Measurement('firmware_version').matches_regex(r'v\d+\.\d+\.\d+'),
htf.Measurement('version_retry_count').in_range(minimum=1),
)
def check_firmware_version(test, comm):
response, attempts = comm.send_with_retry('VERSION', expected='v2.1.0', attempts=3)
test.measurements.firmware_version = response
test.measurements.version_retry_count = attempts
def main():
test = htf.Test(check_firmware_version)
with TofuPilot(test):
test.execute(test_start=lambda: 'SN-003')Enregistrer version_retry_count comme mesure permet à TofuPilot de mettre en évidence la fréquence des nouvelles tentatives sur l'ensemble de votre parc de production. Un pic de tentatives sur une station spécifique indique un problème de montage avant qu'il ne devienne un problème de rendement.
Teardown propre après les échecs
Les plugs avec une méthode tearDown sont toujours appelés par OpenHTF, même quand une phase échoue ou que le test s'arrête prématurément. Utilisez cela pour libérer les ressources matérielles de manière fiable.
import openhtf as htf
from openhtf.plugs import BasePlug
class FixturePlug(BasePlug):
def setUp(self):
self._power_on = False
self._relay_closed = False
def power_on_dut(self):
enable_power_rail()
self._power_on = True
def close_test_relay(self):
close_relay(1)
self._relay_closed = True
def tearDown(self):
# S'exécute toujours, même en cas de STOP ou d'exception
if self._relay_closed:
open_relay(1)
if self._power_on:
disable_power_rail()import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
from plugs.fixture_plug import FixturePlug
@htf.plug(fixture=FixturePlug)
@htf.measures(
htf.Measurement('leakage_current').in_range(maximum=50).with_units(units.AMPERE),
)
def measure_leakage(test, fixture):
fixture.power_on_dut()
fixture.close_test_relay()
current_ua = read_current_meter() * 1e6
test.measurements.leakage_current = current_ua
if not test.measurements.leakage_current.is_pass:
test.logger.error('Courant de fuite %.1f uA dépasse la limite de 50 uA', current_ua)
return htf.PhaseResult.STOP
# fixture.tearDown() appelé automatiquement par OpenHTF
def main():
test = htf.Test(measure_leakage)
with TofuPilot(test):
test.execute(test_start=lambda: 'SN-004')Comparaison des stratégies de gestion des échecs
| Stratégie | Quand l'utiliser | PhaseResult | Phases suivantes |
|---|---|---|---|
| Continuer en cas d'échec | Les échecs sont indépendants, collecter toutes les données | (par défaut) | Exécutées |
| Arrêt au premier échec | Les tests suivants sont invalides après un échec | STOP | Ignorées |
| Retenter en cas d'erreur transitoire | L'échec peut être un problème de montage | REPEAT | Exécutées après la tentative |
| Retenter avec limite | Idem, mais avec un nombre maximum de tentatives | REPEAT + compteur | Exécutées ou STOP |
Enregistrement des échecs pour l'analyse
TofuPilot capture automatiquement chaque mesure échouée. Pour rendre les échecs exploitables en analyse, ajoutez du contexte structuré avec test.logger.
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
@htf.measures(
htf.Measurement('oscillator_freq_hz').in_range(7_990_000, 8_010_000).with_units(units.HERTZ),
)
def check_oscillator(test):
freq = measure_frequency(channel=2)
test.measurements.oscillator_freq_hz = freq
if not test.measurements.oscillator_freq_hz.is_pass:
deviation_ppm = abs(freq - 8_000_000) / 8_000_000 * 1e6
test.logger.error(
'Oscillateur %.0f Hz (%.1f ppm de 8 MHz)',
freq, deviation_ppm,
)
@htf.measures(
htf.Measurement('adc_offset_mv').in_range(-5, 5),
htf.Measurement('adc_gain_error_pct').within_percent(1.0, 0.1),
)
def calibrate_adc(test):
offset = measure_adc_offset()
gain = measure_adc_gain()
test.measurements.adc_offset_mv = offset * 1000
test.measurements.adc_gain_error_pct = gain
def main():
test = htf.Test(check_oscillator, calibrate_adc, name='PCB-Rev-C')
with TofuPilot(test):
test.execute(test_start=lambda: 'SN-005')Les messages de log apparaissent dans la vue détaillée de l'exécution TofuPilot aux côtés de la mesure défaillante. Le contexte quantitatif (déviation en ppm, pas seulement réussite/échec) accélère l'analyse des causes racines lors de l'examen des échecs sur un lot.