diff --git a/opt/pincabos/script/iso-live.sh b/opt/pincabos/script/iso-live.sh index f62438f1..18d504d1 100755 --- a/opt/pincabos/script/iso-live.sh +++ b/opt/pincabos/script/iso-live.sh @@ -150,7 +150,10 @@ fi echo "=== 2) Live initrd ===" if [ -z "$KERNEL_VERSION" ]; then - KERNEL_VERSION="$(ls "$ROOTFS/lib/modules" | sort -V | tail -1)" + # PINCABOS_ISO_LIVE_KVER_V1 : le live prenait toujours le noyau le plus recent du + # rootfs. Des qu une reconstruction en installe un second, l ISO demarre sur un + # noyau que le parc n a jamais essaye (07/09/2026 : 7.0.0-31 par-dessus le 29). + KERNEL_VERSION="${PCO_LIVE_KVER:-$(ls "$ROOTFS/lib/modules" | sort -V | tail -1)}" fi [ -n "$KERNEL_VERSION" ] || die "no kernel found under $ROOTFS/lib/modules" echo " kernel: $KERNEL_VERSION" @@ -285,6 +288,12 @@ EXCLUDES=( "opt/pincabos/cache" "opt/pincabos/cache/*" "opt/pincabos/logs/*" "root/.cache" "root/.cache/*" + # PINCABOS_IMAGE_SANS_BROUILLONS_V1 : nos scripts de travail (pincab-batch-v31, + # v32, v32b, v33, v35b, v35c, v35d...) et les notes de conception de DEV/ + # partaient sur CHAQUE cabinet. Ce n'est pas une question de place — 1 Mo sur + # 2,8 Go — mais le cabinet d'un utilisateur n'a pas a porter notre historique. + "root/pincab-*" "root/pincabos-*" + "DEV" "DEV/*" "var/cache/apt/*" "var/lib/apt/lists/*" "usr/sbin/policy-rc.d" # Not needed at runtime on a cab, and it all comes back with apt if wanted. diff --git a/opt/pincabos/tests/doublons-connus.txt b/opt/pincabos/tests/doublons-connus.txt new file mode 100644 index 00000000..0a4476da --- /dev/null +++ b/opt/pincabos/tests/doublons-connus.txt @@ -0,0 +1,16 @@ +# PINCABOS_PAS_DE_NOUVEAU_DOUBLON_V1 +# Fichiers suivis strictement identiques — de VRAIES copies, pas des liens. +# Releve du 10/09/2026. +# +# Piege : sha256sum SUIT les liens symboliques. Un detecteur qui les inclut +# voit 54 groupes la ou le projet en a deja 51 en liens, et annonce 26 902 +# lignes dupliquees au lieu de 1523. On lit le mode git (120000 = lien). +# +# Cette liste ne doit que RETRECIR : un groupe qui n y est pas fait echouer +# test_hygiene. Quand un groupe est deduplique, retirez sa ligne. +# +# 3 groupes, 1523 lignes dupliquees. +# +opt/pincabos/install/modules/network/mod-dhcp4.sh opt/pincabos/modules/network/mod-dhcp4.sh +opt/pincabos/install/modules/network/mod-ssid.sh opt/pincabos/modules/network/mod-ssid.sh +opt/pincabos/install/modules/system/mod-splash.sh opt/pincabos/modules/system/mod-splash.sh diff --git a/opt/pincabos/tests/rm-rf-connus.txt b/opt/pincabos/tests/rm-rf-connus.txt new file mode 100644 index 00000000..756b8521 --- /dev/null +++ b/opt/pincabos/tests/rm-rf-connus.txt @@ -0,0 +1,31 @@ +# PINCABOS_PAS_DE_NOUVEAU_RM_RF_V1 +# Fichiers REELS (jamais les liens) ou « rm -rf "$VAR" » n est pas garde +# par :? ou :-. Releve du 10/09/2026. Une variable vide efface alors la +# racine du chemin parent. +# Cette liste ne doit que RETRECIR. +# +# 23 fichiers. +# +opt/pincabos/apps/PinCabShare/uninstall.sh +opt/pincabos/install/02-install-engine.sh +opt/pincabos/modules/system/mod-plymouth-install.sh +opt/pincabos/modules/system/mod-plymouth-load.sh +opt/pincabos/script/build-master.sh +opt/pincabos/script/installer/pincabos-install-payload +opt/pincabos/script/installer/pincabos-live-installer +opt/pincabos/script/iso-live.sh +opt/pincabos/script/iso/10-audit-nettoyage.sh +opt/pincabos/script/iso/40-payload.sh +opt/pincabos/script/iso/80-live-rootfs.sh +opt/pincabos/script/slim-master.sh +opt/pincabos/script/updatehelp.sh +opt/pincabos/tests/test_hygiene.py +opt/pincabos/tests/test_iso_etapes.py +opt/pincabos/tests/test_live_xorg.py +opt/pincabos/tools/launch-dmd-calibrator.sh +opt/pincabos/tools/launch-fulldmd-calibrator.sh +opt/pincabos/tools/launch-webapp-screen.sh +opt/pincabos/tools/pincabos-backglass-bridge.sh +opt/pincabos/tools/pincabos-gitpush-release-root.sh +opt/pincabos/tools/publy.sh +usr/local/sbin/pincabos-chat-backglass diff --git a/opt/pincabos/tests/test_doctor_installation.py b/opt/pincabos/tests/test_doctor_installation.py new file mode 100755 index 00000000..fb7a028c --- /dev/null +++ b/opt/pincabos/tests/test_doctor_installation.py @@ -0,0 +1,97 @@ +"""Le doctor dit si l'installation a vraiment atteint le cabinet. + +PINCABOS_DOCTOR_PREMIER_DEMARRAGE_V1 — le 09/09/2026, deux testeurs ont perdu +leur soirée sur des échecs qui étaient déjà journalisés : + + Flo « No displays found by libdmdutil », toutes les 10 s depuis le premier + démarrage. Il a soupçonné son routeur, sa passerelle, son câblage. + Patrick des rubans qui répondaient au menu et restaient noirs en table, faute + de directoutputconfig.ini pour le numéro LedWiz de son contrôleur. + Flo la rotation du playfield choisie à l'installation, jamais appliquée. + +Aucun de ces trois échecs n'était silencieux : ils étaient invisibles, ce qui +n'est pas pareil. Le doctor les remonte maintenant. +""" +import re +import unittest +from pathlib import Path + +from _charge import RACINE + +R = Path(RACINE) +DOCTOR = R / "usr/local/libexec/pincabos/doctor.d" +CHECK = DOCTOR / "85-installation.sh" + + +class LeControleExiste(unittest.TestCase): + def test_au_bon_endroit_et_dans_le_bon_ordre(self): + self.assertTrue(CHECK.is_file()) + # après VPX (80), avant le verdict final (99) + noms = sorted(p.name for p in DOCTOR.glob("*.sh")) + self.assertIn("85-installation.sh", noms) + self.assertLess(noms.index("80-vpx.sh"), noms.index("85-installation.sh")) + self.assertLess(noms.index("85-installation.sh"), noms.index("99-ready.sh")) + + def test_droits_alignes_sur_les_autres_modules(self): + # les modules du doctor sont SOURCÉS, pas exécutés + autres = [p.stat().st_mode & 0o777 for p in DOCTOR.glob("*.sh") + if p.name != "85-installation.sh"] + self.assertIn(CHECK.stat().st_mode & 0o777, set(autres), + "droits differents des autres modules du doctor") + + def test_ouvre_sa_section(self): + self.assertRegex(CHECK.read_text(encoding="utf-8"), r'^pco_section "INSTALLATION"') + + +class LesQuatreControles(unittest.TestCase): + def setUp(self): + self.s = CHECK.read_text(encoding="utf-8") + + def test_les_rejeux_en_attente(self): + # un drapeau qui traîne après plusieurs démarrages = le rejeu ne passe pas + for drapeau in ("inputs-installer.pending", "dmd-installer.pending"): + self.assertIn(drapeau, self.s) + self.assertIn("--list-boots", self.s, + "il faut distinguer « pas encore rejoué » de « ne passe jamais »") + + def test_le_dmd_materiel_vu_par_vpinfe(self): + self.assertIn("No displays found by libdmdutil", self.s, + "c'est la phrase exacte que VPinFE journalise") + self.assertIn("zedmd.json", self.s) + + def test_la_rotation_reellement_appliquee(self): + self.assertIn("playfield_rotation", self.s) + self.assertIn("xrandr", self.s) + # la correspondance degrés -> mot-clé xrandr doit être complète + for degre, mot in (("90", "right"), ("180", "inverted"), ("270", "left")): + self.assertRegex(self.s, r"%s\)\s*attendu=%s" % (degre, mot)) + + def test_les_definitions_d_effets_dof(self): + self.assertIn("directoutputconfig", self.s) + self.assertIn("ledwiz_number", self.s) + self.assertIn("hardware-inventory.json", self.s) + + +class LeControleNAvalePasSesErreurs(unittest.TestCase): + """C'est le constat 01 de la revue : 2 187 « || true » dans le projet. Un + contrôle dont le rôle est de rendre les échecs visibles ne peut pas en + ajouter.""" + + def test_pas_de_ou_true_hors_du_chargement_des_chemins(self): + lignes = [l for l in CHECK.read_text(encoding="utf-8").splitlines() + if "|| true" in l and not l.strip().startswith("#")] + # seule tolérance : le sourcing de pincabos-paths.sh, absent en test + self.assertEqual(len(lignes), 1, "un « || true » de trop : " + repr(lignes)) + self.assertIn("pincabos-paths.sh", lignes[0]) + + def test_chaque_controle_a_ses_trois_issues(self): + # un contrôle qui ne peut que réussir ne sert à rien + s = CHECK.read_text(encoding="utf-8") + self.assertGreaterEqual(len(re.findall(r"\bpco_fail\b", s)), 4) + self.assertGreaterEqual(len(re.findall(r"\bpco_go\b", s)), 5) + self.assertGreaterEqual(len(re.findall(r"\bpco_warn\b", s)), 2) + + def test_les_messages_disent_quoi_faire(self): + s = CHECK.read_text(encoding="utf-8") + for indice in ("Map Commander", "la page DMD", "page DOF"): + self.assertIn(indice, s, "aucun message ne renvoie vers " + indice) diff --git a/opt/pincabos/tests/test_hygiene.py b/opt/pincabos/tests/test_hygiene.py new file mode 100755 index 00000000..c2f6ed33 --- /dev/null +++ b/opt/pincabos/tests/test_hygiene.py @@ -0,0 +1,190 @@ +"""Trois cliquets d'hygiène, issus de la revue du 10/09/2026. + +Un cliquet ne demande pas de tout réparer aujourd'hui : il fige l'existant et +refuse que ça empire. Chacun des trois vient d'un piège qui a réellement mordu. + + doublons 3 groupes, 1 523 lignes. Attention au piège : `sha256sum` SUIT + les liens symboliques. Un détecteur naïf voit 54 groupes et + 26 902 lignes là où le projet a déjà posé 51 groupes de liens — + c'est l'erreur que j'ai commise en écrivant la revue. Seul le + mode git (120000) distingue un lien d'une copie. + rm -rf des fichiers effacent « $VAR » sans garde. J'ai écrit une de ces + lignes le 09/09 avec une variable qui n'existait pas ; seul le + « set -u » du script a évité le dégât. + [Install] l'unité de rotation du playfield vivait dans un .target.wants + sans WantedBy — donc jamais démarrée. Flo a perdu ses écrans + dans le bon sens pendant toute une soirée. +""" +import hashlib +import re +import subprocess +import unittest +from pathlib import Path + +from _charge import RACINE + +R = Path(RACINE) +TESTS = R / "opt/pincabos/tests" +DOUBLONS = TESTS / "doublons-connus.txt" +RMRF = TESTS / "rm-rf-connus.txt" +ISO_LIVE = R / "opt/pincabos/script/iso-live.sh" + + +def _lignes(fichier): + return [l.strip() for l in fichier.read_text(encoding="utf-8").splitlines() + if l.strip() and not l.startswith("#")] + + +def _fichiers_de_code(): + """Les fichiers RÉELS, jamais les liens symboliques. + + Piège pris le 10/09/2026 : `sha256sum` suit les liens. Un détecteur qui + les inclut voit 54 « groupes de copies » là où le projet a en réalité déjà + posé 51 groupes de liens — et annonce 26 902 lignes dupliquées au lieu de + 1 523. `git ls-files -s` donne le mode 120000 pour un lien : c'est la seule + façon fiable de les distinguer.""" + modes = {} + for l in subprocess.run(["git", "-C", str(R), "ls-files", "-s"], + capture_output=True, text=True).stdout.splitlines(): + m, _, _, f = l.split(None, 3) + modes[f] = m + out = [] + for f, mode in modes.items(): + if mode == "120000": # lien symbolique : ce n'est pas une copie + continue + p = R / f + if not p.is_file(): + continue + if f.endswith((".py", ".sh")): + out.append(f) + else: + try: + if p.open("rb").read(2) == b"#!": + out.append(f) + except OSError: + pass + return sorted(out) + + +class PasDeNouveauDoublon(unittest.TestCase): + """PINCABOS_PAS_DE_NOUVEAU_DOUBLON_V1 — la liste ne doit que rétrécir.""" + + @classmethod + def setUpClass(cls): + par_somme = {} + for f in _fichiers_de_code(): + h = hashlib.sha256((R / f).read_bytes()).hexdigest()[:16] + par_somme.setdefault(h, []).append(f) + cls.reels = {" ".join(sorted(v)) for v in par_somme.values() if len(v) > 1} + cls.connus = set(_lignes(DOUBLONS)) + + def test_la_base_existe(self): + self.assertTrue(DOUBLONS.is_file(), "la base de reference manque") + + def test_aucun_groupe_nouveau(self): + nouveaux = sorted(self.reels - self.connus) + self.assertEqual(nouveaux, [], + "copie(s) au lieu de lien(s) symbolique(s) :\n " + + "\n ".join(nouveaux)) + + def test_les_groupes_resolus_sortent_de_la_base(self): + # echec volontairement agreable : quelqu un vient de dedupliquer + resolus = sorted(self.connus - self.reels) + self.assertEqual(resolus, [], + "ces groupes ne sont plus dupliques — retirez leur ligne " + "de doublons-connus.txt :\n " + "\n ".join(resolus)) + + +class PasDeNouveauRmRf(unittest.TestCase): + """PINCABOS_PAS_DE_NOUVEAU_RM_RF_V1 — « rm -rf "$VIDE"/x » efface /x.""" + + MOTIF = re.compile(r'rm\s+-rf\s+"?\$(?!\{[A-Za-z_]+:[?-])') + + @classmethod + def setUpClass(cls): + cls.reels = {f for f in _fichiers_de_code() + if cls.MOTIF.search((R / f).read_text(encoding="utf-8", + errors="replace"))} + cls.connus = set(_lignes(RMRF)) + + def test_aucun_fichier_nouveau(self): + nouveaux = sorted(self.reels - self.connus) + self.assertEqual(nouveaux, [], + "utilisez \"${VAR:?}\" — une variable vide efface la racine :\n " + + "\n ".join(nouveaux)) + + def test_les_fichiers_corriges_sortent_de_la_base(self): + resolus = sorted(self.connus - self.reels) + self.assertEqual(resolus, [], + "corrige(s) — retirez leur ligne de rm-rf-connus.txt :\n " + + "\n ".join(resolus)) + + +class ToutCeQuiEstActiveEstActivable(unittest.TestCase): + """Aucune tolérance ici : le contrôle passe déjà strictement. + + Une unité posée dans un `*.target.wants/` sans `WantedBy` correspondant n'est + pas démarrée par cette cible — c'est le défaut exact de + pincabos-screen-hotplug, qui laissait le playfield à l'envers.""" + + def test_chaque_lien_a_son_WantedBy(self): + soucis = [] + for lien in sorted((R / "etc/systemd/system").glob("*.target.wants/*")): + cible_nom = lien.parent.name.replace(".wants", "") + unite = (lien.parent / lien.readlink()) if lien.is_symlink() else lien + if not unite.is_file(): + soucis.append("%s/%s : cible absente" % (lien.parent.name, lien.name)) + continue + texte = unite.read_text(encoding="utf-8", errors="replace") + if not re.search(r"(?m)^WantedBy=.*\b%s\b" % re.escape(cible_nom), texte): + soucis.append("%s/%s : pas de WantedBy=%s" + % (lien.parent.name, lien.name, cible_nom)) + self.assertEqual(soucis, [], "\n " + "\n ".join(soucis)) + + def test_reciproque_toute_unite_qui_se_declare_est_liee(self): + """PINCABOS_UNITE_DECLAREE_SANS_LIEN_V1 — cliquet, pas règle stricte. + + Onze unités déclarent `WantedBy=` sans lien d'activation. Elles ne sont + donc pas démarrées par la cible qu'elles nomment. Certaines le sont + autrement — `pincabos-dudescab-hotplug-recovery` par une règle udev, + `pincabos-splash-sync` par un `.path` du même nom, les trois + `firstboot-*` par un appel direct de l'installateur. D'autres, non : + `safe-batch-full`, `scoreview-x11-hq-preview` et `table-test` ne sont + appelées de nulle part. + + Trancher au cas par cas demande de savoir ce que chaque unité doit + faire ; ce test se contente d'empêcher la liste de grandir.""" + reels = set() + for unite in sorted((R / "etc/systemd/system").glob("*.service")): + m = re.search(r"(?m)^WantedBy=(\S+)", unite.read_text(encoding="utf-8", + errors="replace")) + if not m: + continue + for cible in m.group(1).split(): + if not (R / "etc/systemd/system" / (cible + ".wants") / unite.name).exists(): + reels.add("%s %s" % (unite.name, cible)) + connus = set(_lignes(TESTS / "unites-sans-lien.txt")) + nouvelles = sorted(reels - connus) + self.assertEqual(nouvelles, [], + "declaree(s) sans lien : elles ne demarreront pas.\n " + + "\n ".join(nouvelles)) + reglees = sorted(connus - reels) + self.assertEqual(reglees, [], + "reglee(s) — retirez leur ligne de unites-sans-lien.txt :\n " + + "\n ".join(reglees)) + + +class LImageNEmportePasNosBrouillons(unittest.TestCase): + """PINCABOS_IMAGE_SANS_BROUILLONS_V1 — 11 039 lignes de scripts de travail et + les notes de DEV/ partaient sur chaque cabinet.""" + + def test_les_exclusions_sont_posees(self): + s = ISO_LIVE.read_text(encoding="utf-8") + for motif in ('"root/pincab-*"', '"root/pincabos-*"', '"DEV" "DEV/*"'): + self.assertIn(motif, s, motif + " n'est pas exclu de l'image") + + def test_ce_qui_appartient_a_root_reste(self): + # .bashrc, .profile et .ssh sont legitimes : on n'exclut pas root/ en bloc + s = ISO_LIVE.read_text(encoding="utf-8") + self.assertNotRegex(s, r'"root"\s+"root/\*"', + "exclure root/ en bloc emporterait .ssh et .profile") diff --git a/opt/pincabos/tests/unites-sans-lien.txt b/opt/pincabos/tests/unites-sans-lien.txt new file mode 100644 index 00000000..5079d764 --- /dev/null +++ b/opt/pincabos/tests/unites-sans-lien.txt @@ -0,0 +1,21 @@ +# PINCABOS_UNITE_DECLAREE_SANS_LIEN_V1 +# Unites qui declarent WantedBy= sans lien dactivation, relevees le 10/09/2026. +# Une unite ici n est PAS demarree par la cible qu elle nomme. Certaines le sont +# autrement (udev, .path, appel direct par l installateur) ; d autres ne le sont +# par personne. C est le defaut exact de pincabos-screen-hotplug, qui laissait le +# playfield a l envers. +# Cette liste ne doit que RETRECIR. +# +# 11 unites. +# +pincabos-dashboard-live-capture.service multi-user.target +pincabos-dudescab-hotplug-recovery.service multi-user.target +pincabos-finalize-firstboot.service multi-user.target +pincabos-firstboot-initramfs-refresh.service multi-user.target +pincabos-firstboot-network-webapp-fix.service multi-user.target +pincabos-firstboot-vpinfe-packaged-runtime-fix.service graphical.target +pincabos-safe-batch-full.service multi-user.target +pincabos-scoreview-x11-hq-preview.service graphical.target +pincabos-splash-sync.service multi-user.target +pincabos-table-test.service graphical.target +pincabos-webapp-fallback.service multi-user.target diff --git a/usr/local/libexec/pincabos/doctor.d/85-installation.sh b/usr/local/libexec/pincabos/doctor.d/85-installation.sh new file mode 100644 index 00000000..b1ea168d --- /dev/null +++ b/usr/local/libexec/pincabos/doctor.d/85-installation.sh @@ -0,0 +1,118 @@ +pco_section "INSTALLATION" + +# PINCABOS_DOCTOR_PREMIER_DEMARRAGE_V1 +# Ce que l'assistant a promis doit avoir atteint le systeme qui tourne. Chaque +# controle ici correspond a une soiree perdue par un testeur le 09/09/2026 : +# l'echec existait, il etait journalise, et rien ne le remontait. + +. /opt/pincabos/tools/pincabos-paths.sh 2>/dev/null || true +CFG=/opt/pincabos/config +FLAGS=/opt/pincabos/flags + +# --- 1. les rejeux du premier demarrage ont-ils abouti ? ------------------- +# Ces drapeaux sont CONSERVES tant que l'application echoue : leur presence +# apres plusieurs demarrages veut dire que le rejeu ne passe pas. +for couple in "inputs-installer.pending|mappage des boutons|Map Commander" \ + "dmd-installer.pending|DMD materiel|la page DMD"; do + drapeau=${couple%%|*}; reste=${couple#*|}; quoi=${reste%%|*}; ou=${reste##*|} + if [ -f "$FLAGS/$drapeau" ]; then + pose=$(date -r "$FLAGS/$drapeau" '+%d/%m %H:%M' 2>/dev/null || echo "?") + demarrages=$(journalctl --list-boots --no-pager 2>/dev/null | wc -l) + if [ "${demarrages:-0}" -gt 2 ]; then + pco_fail "$quoi" "choisi a l'installation ($pose), toujours pas applique apres $demarrages demarrages — refaites-le dans $ou" + else + pco_warn "$quoi" "choisi a l'installation ($pose), sera applique au prochain demarrage" + fi + else + pco_go "$quoi" "applique (aucun rejeu en attente)" + fi +done + +# --- 2. le DMD materiel est-il vraiment vu ? ------------------------------- +# Flo, 09/09/2026 : ZeDMD Wi-Fi configure, adresse correcte, appareil joignable +# au ping — et VPinFE journalisait « No displays found by libdmdutil » toutes +# les dix secondes depuis le premier demarrage, sans que rien ne le dise. +mode=$(python3 - "$CFG/zedmd.json" <<'PY' 2>/dev/null +import json, sys +try: + print(json.load(open(sys.argv[1])).get("mode", "off")) +except Exception: + print("off") +PY +) +if [ "${mode:-off}" = "off" ]; then + pco_go "DMD materiel" "aucun declare" +else + refus=$(journalctl -u pincabos-vpinfe -b --no-pager 2>/dev/null \ + | grep -c "No displays found by libdmdutil") + if [ "${refus:-0}" -gt 0 ]; then + pco_fail "DMD materiel" "$mode declare, mais VPinFE ne trouve aucun afficheur ($refus refus depuis le demarrage) — verifiez l'appareil et la page DMD" + else + pco_go "DMD materiel" "$mode, VPinFE l'a ouvert" + fi +fi + +# --- 3. la rotation du playfield a-t-elle ete appliquee ? ------------------ +# La rotation vit dans screens.json et n'est posee que par xrandr, au boot. +voulue=$(python3 - "$CFG/screens/screens.json" <<'PY' 2>/dev/null +import json, sys +try: + print(str(json.load(open(sys.argv[1])).get("playfield_rotation", "0")).strip() or "0") +except Exception: + print("0") +PY +) +if [ "${voulue:-0}" = "0" ]; then + pco_go "Rotation du playfield" "aucune demandee" +else + attendu=normal + case "$voulue" in 90) attendu=right ;; 180) attendu=inverted ;; 270) attendu=left ;; esac + reel=$(pco_as_pinball xrandr --query 2>/dev/null \ + | awk '/ connected/ {for (i=1;i<=NF;i++) if ($i ~ /^(normal|left|right|inverted)$/) {print $i; exit}}') + if [ -z "$reel" ]; then + pco_warn "Rotation du playfield" "${voulue}° demandee, etat xrandr illisible" + elif [ "$reel" = "$attendu" ]; then + pco_go "Rotation du playfield" "${voulue}° appliquee ($reel)" + else + pco_fail "Rotation du playfield" "${voulue}° choisie a l'installation, xrandr est en « $reel » — la topologie n'a pas ete rejouee" + fi +fi + +# --- 4. les toys declares ont-ils des definitions d'effets ? --------------- +# Patrick, 09/09/2026 : ses rubans repondaient au menu mais restaient noirs en +# table. Un controleur porte un numero LedWiz ; sans directoutputconfig.ini, +# DOF n'a rien a jouer dessus. Ces fichiers viennent du DOF Config Tool. +inv="$CFG/dof/hardware-inventory.json" +if [ ! -s "$inv" ]; then + pco_go "Definitions d'effets DOF" "aucun controleur de rubans declare" +else + numeros=$(python3 - "$inv" <<'PY' 2>/dev/null +import json, sys +try: + d = json.load(open(sys.argv[1])) +except Exception: + raise SystemExit(0) +for a in d.get("devices", []): + if a.get("enabled"): + n = a.get("ledwiz_number") + if n: + print(n) +PY +) + manquants="" + for n in $numeros; do + trouve=0 + for d in /home/pinball/.local/share/VPinballX/*/directoutputconfig \ + /home/pinball/.pincabos/vpx/directoutputconfig; do + [ -f "$d/directoutputconfig$n.ini" ] && trouve=1 && break + done + [ "$trouve" = 0 ] && manquants="$manquants $n" + done + if [ -n "$manquants" ]; then + pco_fail "Definitions d'effets DOF" "LedWiz$manquants declare(s) sans directoutputconfig — ces sorties resteront noires en table. Importez votre configuration depuis la page DOF." + elif [ -n "$numeros" ]; then + pco_go "Definitions d'effets DOF" "LedWiz $(echo $numeros | tr '\n' ' ')couvert(s)" + else + pco_go "Definitions d'effets DOF" "aucun controleur actif" + fi +fi