Amener un script de test sur les stations de production de façon fiable est l'un des problèmes les moins discutés du test manufacturier. Les approches courantes, copie SSH, partage réseau, clé USB, partagent la même faiblesse : personne ne peut dire avec certitude quelle station exécute quelle version.
Le conseil vers lequel les ingénieurs convergent est de garder une configuration stable dans Git et de faire en sorte que les stations la récupèrent. Cet instinct est juste, et c'est ce que le modèle ci-dessous automatise.
Pourquoi le déploiement manuel échoue
Les modes de défaillance sont constants :
- Dérive de versions. La station 3 était hors ligne au dernier déploiement et tourne encore avec les limites du mois dernier. Personne ne le remarque avant que son rendement diverge.
- Déploiements partiels. Une boucle de copie échoue à mi-parcours. Certaines stations sont à jour, d'autres non, et le script ne dit pas lesquelles.
- Pas de rollback. Une mauvaise version atteint toutes les stations en même temps et revenir en arrière signifie refaire tout le processus sous pression.
- Modifications locales non suivies. Quelqu'un a corrigé quelque chose directement sur une station. Ce correctif n'existe nulle part ailleurs et disparaît au déploiement suivant.
- Version de Python différente. Le script marche sur le poste de développement et échoue sur une station.
Le problème de fond : l'artefact déployé n'est pas identifiable. Si vous ne pouvez pas nommer exactement ce qui tourne, vous ne pouvez pas raisonner dessus.
Déployer depuis un push Git
Connectez un dépôt à une procédure, et chaque push sur une branche suivie construit un déploiement. Les pushes sur la branche de production se déploient sur toutes les stations liées ; les autres branches produisent des déploiements de préversion que vous pouvez épingler à une seule station.
GitHub, GitLab et Bitbucket Data Center sont supportés.
Une procédure est un fichier .yaml qui déclare la séquence, les mesures et leurs limites. Les phases sont de simples fonctions Python, et les plugs connectent les instruments sous forme de classes Python.
procedure.yaml23 lines
name: Test fonctionnel PCBAunit: serial_number: default_value: "PCBA000001" part_number: default_value: "PCBA-200"plugs: - name: Multimeter python: plugs.dmm:Multimetermain: - name: Power Rail Test python: phases.power_rail measurements: - name: Rail 3V3 unit: V validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4def power_rail(measurements, multimeter): measurements.rail_3v3 = multimeter.read_voltage()Les limites vivant dans le YAML plutôt que dans le Python, resserrer une limite est un diff d'une ligne qui se relit proprement et reste visible dans l'historique des déploiements.
Commitez, poussez, les stations récupèrent :
git add procedure.yaml phases/power_rail.pygit commit -m "Resserrer les limites du rail 3V3 à 3,2-3,4 V"git push origin mainLe build produit un artefact immuable portant l'exécutable, le runtime résolu et tout ce dont la station a besoin. Une station exécutant dep_abc123 exécute toujours exactement ce code.
Déploiement progressif
Pour valider un changement avant qu'il n'atteigne tout l'atelier, épinglez une préversion à une station :
git checkout -b limites-resserreesgit push origin limites-resserrees# Épinglez le déploiement de préversion à une station depuis le dashboardFaites tourner une équipe sur cette station, comparez le rendement aux autres, puis fusionnez vers production.
Revenir en arrière
Les artefacts étant immuables et conservés, le rollback consiste à sélectionner un déploiement précédent plutôt qu'à reconstruire. La station le récupère à sa prochaine vérification.
Autres façons de créer un déploiement
- Déclenchement manuel depuis la page Déploiements, par SHA de commit ou branche.
- CLI, qui construit depuis votre copie de travail locale, avec ou sans dépôt connecté.
Déployer sans la plateforme
Si vous n'utilisez pas le déploiement TofuPilot, les mêmes principes s'appliquent et vous pouvez les construire vous-même.
Fixez vos dépendances
Les dépendances non fixées sont la cause la plus courante des échecs « ça marche sur mon poste ».
openhtf==1.6.1tofupilot[openhtf]==2.16.0pyserial==3.5numpy==1.26.4pyinstaller==6.10.0#!/bin/bashpip install -r requirements.txtpip freeze > requirements.lock.txt # dépendances transitives pour auditEmpaquetez en un exécutable unique
PyInstaller réunit le script et ses dépendances en un fichier, ce qui retire la version Python de la station de l'équation.
OpenHTF utilise des imports dynamiques que PyInstaller ne détecte pas, d'où les hidden imports explicites :
station_tests.spec48 lines
# -*- mode: python ; coding: utf-8 -*-from PyInstaller.utils.hooks import collect_data_files, collect_submodulesblock_cipher = Noneopenhtf_datas = collect_data_files("openhtf")openhtf_hiddenimports = collect_submodules("openhtf")a = Analysis( ["test_main.py"], pathex=["."], binaries=[], datas=openhtf_datas + [ ("plugs/", "plugs/"), ("config/", "config/"), ("VERSION", "."), ], hiddenimports=openhtf_hiddenimports + [ "openhtf.output.callbacks", "openhtf.output.callbacks.json_factory", "openhtf.util.logs", "tofupilot", "tofupilot.openhtf", ], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], cipher=block_cipher, noarchive=False,)pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)exe = EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name="station_tests", debug=False, strip=False, upx=False, runtime_tmpdir=None, console=True,)Dépannage PyInstaller :
| Symptôme | Cause | Correction |
|---|---|---|
ModuleNotFoundError: openhtf.util.logs | Hidden import manquant | Ajouter à hiddenimports |
Frontend vide sur localhost:4444 | Ressources web non empaquetées | Vérifier collect_data_files("openhtf") |
OSError: [Errno 2] sur la config | Fichier non inclus | Ajouter le répertoire à datas |
| Crash au deuxième lancement | _MEIPASS résiduel | Définir runtime_tmpdir |
Gardez l'identité de station hors du code
#!/bin/bash# Placer dans /etc/profile.d/tofupilot.shexport TOFUPILOT_API_KEY="tp_live_xxxxxxxxxxxxxxxxxxxx"export TOFUPILOT_STATION_ID="station-floor-1-cell-3"export STATION_SERIAL_PORT="/dev/ttyUSB0"export STATION_BAUD_RATE="115200"config/loader.py22 lines
import jsonimport osfrom pathlib import Pathdef load_station_config() -> dict: if os.environ.get("TOFUPILOT_API_KEY"): return { "api_key": os.environ["TOFUPILOT_API_KEY"], "station_id": os.environ.get("TOFUPILOT_STATION_ID", "unknown"), "serial_port": os.environ.get("STATION_SERIAL_PORT", "/dev/ttyUSB0"), "baud_rate": int(os.environ.get("STATION_BAUD_RATE", "115200")), } config_path = Path("/etc/tofupilot/station.json") if not config_path.exists(): raise FileNotFoundError( f"Configuration station introuvable à {config_path}." ) with config_path.open() as f: return json.load(f)Poussez vers les stations
deploy_manual.sh21 lines
#!/bin/bashset -eVERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")EXECUTABLE="dist/station_tests_v${VERSION}"DEPLOY_PATH="/opt/tofupilot/station_tests"STATIONS=( "operator@station-01.local" "operator@station-02.local" "operator@station-03.local")for STATION in "${STATIONS[@]}"; do echo "Déploiement v${VERSION} vers ${STATION}..." ssh "$STATION" "[ -f ${DEPLOY_PATH} ] && cp ${DEPLOY_PATH} ${DEPLOY_PATH}.bak || true" scp "$EXECUTABLE" "${STATION}:${DEPLOY_PATH}" ssh "$STATION" "chmod +x ${DEPLOY_PATH}" ssh "$STATION" "${DEPLOY_PATH} --version 2>&1 | head -1" echo " Terminé : ${STATION}"doneNotez la copie .bak avant écrasement. C'est votre rollback, et il ne remonte que d'une version.
Enregistrez la version à chaque run
Quelle que soit la méthode, enregistrez quelle version a produit chaque résultat. Sans cela, impossible de corréler un changement de rendement avec une release.
La version est une propriété du run : enregistrez-la là où vivent vos autres métadonnées. Avec OpenHTF, les métadonnées du test record la portent :
test_main.py29 lines
import importlib.metadataimport openhtf as htffrom tofupilot.openhtf import uploaddef get_version() -> str: try: return importlib.metadata.version("station-tests") except importlib.metadata.PackageNotFoundError: import os, sys base = getattr(sys, "_MEIPASS", os.path.dirname(__file__)) with open(os.path.join(base, "VERSION")) as f: return f.read().strip()def record_version(test): test.metadata["software_version"] = get_version()def main(): test = htf.Test( record_version, phase_power_on, phase_voltage_check, procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", part_number="PCBA-200", ) test.add_output_callbacks(upload()) test.execute(lambda: input("Numéro de série : ").strip())Avec le déploiement Git, vous l'obtenez gratuitement : l'identifiant de déploiement nomme déjà l'artefact exact, il n'y a rien à enregistrer à la main.
Comparer les options
| Méthode | Mise en place | Python requis | Rollback | Certitude de version |
|---|---|---|---|---|
| Déploiement Git | Connecter un dépôt | Non | Sélectionner un déploiement | Exacte, par station |
| PyInstaller via SSH | Moyenne | Non | Restaurer .bak, une version | Suivi manuel |
| Environnement virtuel | Faible | Oui | Remplacer le venv | Suivi manuel |
| Conteneur Docker | Élevée | Non | docker pull tag précédent | Exacte, par tag |
| Partage réseau | Faible | Oui | Restaurer le fichier | Aucune |
La ligne partage réseau est à éviter. Elle paraît la plus simple et c'est la seule méthode où une station peut récupérer silencieusement un fichier à moitié écrit en pleine équipe.
À vérifier après tout déploiement
- Chaque station rapporte la version attendue. Sinon, elle n'a pas été mise à jour.
- Les premiers runs après déploiement passent. Un déploiement cassé se voit surtout sur les premières unités.
- Le rendement après correspond au rendement avant. Si les limites ont changé volontairement, attendez-vous à une marche ; sinon, une marche signale un problème.
Le déploiement est l'un des rares endroits du test de production où pouvoir annuler vite compte plus que réussir du premier coup.
