Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions opt/pincabos/installer-gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ def toys_detection():
"auto": pco_dof.cartes_auto(det) if pco_dof else [],
"strips": pco_dof.controleurs_de_rubans(det) if pco_dof else [],
"arrangements": list(pco_dof.ARRANGEMENTS) if pco_dof else [], "color_orders": list(pco_dof.ORDRES_COULEUR) if pco_dof else [],
# PINCABOS_TOYS_ROLES_V1 : à quoi sert chaque sortie adressable
"roles": list(pco_dof.ROLE_IDS) if pco_dof else [],
"proposition": pco_dof.proposer_toys(det) if pco_dof else {"controllers": []}, "_det": det}


Expand Down
70 changes: 65 additions & 5 deletions opt/pincabos/installer-gui/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,19 @@
"inputs_more": "Voir toutes les actions",
"inputs_less": "Ne voir que l'essentiel",
"inputs_count": "%n bouton(s) reconnu(s) — ils seront posés à l'installation.",
"inputs_skip_ok": "Aucun bouton appuyé : vous pourrez le faire dans Map Commander."
"inputs_skip_ok": "Aucun bouton appuyé : vous pourrez le faire dans Map Commander.",
"toys_role": "Rôle",
"toys_role_output": "Sortie %n",
"toys_role_aucun": "— sans rôle —",
"toys_role_backboard": "Backboard",
"toys_role_sous-caisse": "Sous le meuble",
"toys_role_cote-gauche": "Côté gauche",
"toys_role_cote-droit": "Côté droit",
"toys_role_fronton": "Fronton",
"toys_role_flippers": "Flippers",
"toys_role_interieur": "Intérieur du meuble",
"toys_role_libre": "Libre",
"toys_role_hint": "Le rôle donne son nom à la sortie dans la configuration DOF. Sans lui, elle s'appellera « Ruban 1.2 » — exact, mais illisible le jour où vous relirez votre cabinet."
},
"en": {
"choose_lang": "Choose your language",
Expand Down Expand Up @@ -431,7 +443,19 @@
"inputs_more": "Show every action",
"inputs_less": "Show the essentials only",
"inputs_count": "%n button(s) recognised — they will be applied at install time.",
"inputs_skip_ok": "No button pressed: you will be able to do it in Map Commander."
"inputs_skip_ok": "No button pressed: you will be able to do it in Map Commander.",
"toys_role": "Role",
"toys_role_output": "Output %n",
"toys_role_aucun": "— no role —",
"toys_role_backboard": "Backboard",
"toys_role_sous-caisse": "Under the cabinet",
"toys_role_cote-gauche": "Left side",
"toys_role_cote-droit": "Right side",
"toys_role_fronton": "Backbox",
"toys_role_flippers": "Flippers",
"toys_role_interieur": "Inside the cabinet",
"toys_role_libre": "Spare",
"toys_role_hint": "The role names the output in the DOF configuration. Without it, it will be called “Ruban 1.2” — accurate, but unreadable the day you come back to your cabinet."
},
"de": {
"choose_lang": "Sprache auswählen",
Expand Down Expand Up @@ -648,7 +672,19 @@
"inputs_more": "Alle Aktionen anzeigen",
"inputs_less": "Nur das Wesentliche",
"inputs_count": "%n Taste(n) erkannt — sie werden bei der Installation übernommen.",
"inputs_skip_ok": "Keine Taste gedrückt: Sie können es in Map Commander nachholen."
"inputs_skip_ok": "Keine Taste gedrückt: Sie können es in Map Commander nachholen.",
"toys_role": "Rolle",
"toys_role_output": "Ausgang %n",
"toys_role_aucun": "— ohne Rolle —",
"toys_role_backboard": "Backboard",
"toys_role_sous-caisse": "Unter dem Gehäuse",
"toys_role_cote-gauche": "Linke Seite",
"toys_role_cote-droit": "Rechte Seite",
"toys_role_fronton": "Backbox",
"toys_role_flippers": "Flipper",
"toys_role_interieur": "Gehäuseinneres",
"toys_role_libre": "Frei",
"toys_role_hint": "Die Rolle benennt den Ausgang in der DOF-Konfiguration. Ohne sie heißt er „Ruban 1.2“ — korrekt, aber unlesbar, wenn Sie Ihr Cabinet später wieder ansehen."
},
"it": {
"choose_lang": "Scegli la tua lingua",
Expand Down Expand Up @@ -865,7 +901,19 @@
"inputs_more": "Mostra tutte le azioni",
"inputs_less": "Solo l'essenziale",
"inputs_count": "%n pulsante/i riconosciuto/i — verranno applicati all'installazione.",
"inputs_skip_ok": "Nessun pulsante premuto: potrai farlo in Map Commander."
"inputs_skip_ok": "Nessun pulsante premuto: potrai farlo in Map Commander.",
"toys_role": "Ruolo",
"toys_role_output": "Uscita %n",
"toys_role_aucun": "— senza ruolo —",
"toys_role_backboard": "Backboard",
"toys_role_sous-caisse": "Sotto il mobile",
"toys_role_cote-gauche": "Lato sinistro",
"toys_role_cote-droit": "Lato destro",
"toys_role_fronton": "Backbox",
"toys_role_flippers": "Flipper",
"toys_role_interieur": "Interno del mobile",
"toys_role_libre": "Libera",
"toys_role_hint": "Il ruolo dà il nome all'uscita nella configurazione DOF. Senza, si chiamerà «Ruban 1.2» — esatto, ma illeggibile il giorno in cui rileggerai il tuo cabinato."
},
"es": {
"choose_lang": "Elige tu idioma",
Expand Down Expand Up @@ -1082,6 +1130,18 @@
"inputs_more": "Ver todas las acciones",
"inputs_less": "Sólo lo esencial",
"inputs_count": "%n botón(es) reconocido(s) — se aplicarán en la instalación.",
"inputs_skip_ok": "Ningún botón pulsado: podrás hacerlo en Map Commander."
"inputs_skip_ok": "Ningún botón pulsado: podrás hacerlo en Map Commander.",
"toys_role": "Función",
"toys_role_output": "Salida %n",
"toys_role_aucun": "— sin función —",
"toys_role_backboard": "Backboard",
"toys_role_sous-caisse": "Bajo el mueble",
"toys_role_cote-gauche": "Lado izquierdo",
"toys_role_cote-droit": "Lado derecho",
"toys_role_fronton": "Backbox",
"toys_role_flippers": "Flippers",
"toys_role_interieur": "Interior del mueble",
"toys_role_libre": "Libre",
"toys_role_hint": "La función da nombre a la salida en la configuración DOF. Sin ella se llamará «Ruban 1.2» — exacto, pero ilegible el día que vuelvas a tu mueble."
}
}
21 changes: 19 additions & 2 deletions opt/pincabos/installer-gui/templates/wizard.html
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,11 @@ <h2 style="text-align:center" data-i18n="install_done"></h2>
if(!strips.length){box.innerHTML='<div class="card"><div class="hint" style="margin:0">'+esc(t("toys_strips_none"))+'</div></div>';refreshToysStatus();return}
box.innerHTML=S.toys.controllers.map((c,i)=>{const hw=strips.find(s=>s.serial===c.serial)||{};
const sel=(id,opts,val)=>'<select data-i="'+i+'" data-k="'+id+'">'+opts.map(o=>'<option value="'+o+'"'+(o===val?" selected":"")+'>'+o+'</option>').join("")+'</select>';
// PINCABOS_TOYS_ROLES_V1 : le rôle donne son nom au toy dans cabinet.xml.
// Sans rôle, on garde le nom générique d'avant (« Ruban 1.2 »).
const selRole=(k,val)=>'<select data-i="'+i+'" data-k="'+k+'">'
+'<option value=""'+(val?"":" selected")+'>'+esc(t("toys_role_aucun"))+'</option>'
+(d.roles||[]).map(r=>'<option value="'+esc(r)+'"'+(r===val?" selected":"")+'>'+esc(t("toys_role_"+r))+'</option>').join("")+'</select>';
const num=(k,val,min,max)=>'<input type="number" data-i="'+i+'" data-k="'+k+'" value="'+esc(val)+'" min="'+min+'" max="'+max+'" style="min-width:90px;flex:0 0 110px">';
return '<div class="card" style="margin-bottom:14px"><div class="row" style="margin-bottom:8px"><label>'+esc(hw.kind||c.type)+(c.serial?' #'+esc(c.serial):'')+'</label>'
+'<div class="seg"><button type="button" class="segb'+(c.enabled?" sel":"")+'" data-i="'+i+'" data-act="enable">'+esc(t("toys_used"))+'</button></div></div>'
Expand All @@ -659,10 +664,15 @@ <h2 style="text-align:center" data-i18n="install_done"></h2>
+'<button type="button" class="segb'+(c.mode==="rubans"?" sel":"")+'" data-i="'+i+'" data-act="mode" data-v="rubans">'+esc(t("toys_mode_strips"))+'</button></div></div>'
+(c.mode==="matrice"?'<div class="row"><label>'+esc(t("toys_size"))+'</label>'+num("width",c.width,1,1024)+' × '+num("height",c.height,1,1024)
+'<span class="hint" style="margin:0">= '+((+c.width)*(+c.height))+' LED</span></div>'
+'<div class="row"><label>'+esc(t("toys_arrangement"))+'</label>'+sel("arrangement",d.arrangements||[],c.arrangement)+'</div>':'')
+'<div class="row"><label>'+esc(t("toys_arrangement"))+'</label>'+sel("arrangement",d.arrangements||[],c.arrangement)+'</div>'
+'<div class="row"><label>'+esc(t("toys_role"))+'</label>'+selRole("role",c.role||"")+'</div>':'')
+'<div class="row"><label>'+esc(t("toys_outputs"))+'</label><input type="text" data-i="'+i+'" data-k="strips" value="'+esc((c.strips||[]).join(", "))+'" spellcheck="false">'
+'<span class="hint" style="margin:0">'+esc(t("toys_total"))+' '+stripsTotal(c)+'</span></div>'
+'<div class="hint" style="margin-top:0">'+esc(t(c.mode==="matrice"?"toys_outputs_hint_matrix":"toys_outputs_hint_strips"))+'</div>'
+(c.mode==="rubans"?(c.strips||[]).map((n,j)=>n>0
? '<div class="row"><label>'+esc(t("toys_role_output").replace("%n",j+1))+'</label>'+selRole("role"+j,(c.roles||[])[j]||"")
+'<span class="hint" style="margin:0">'+n+' LED</span></div>'
: "").join(""):"")
+'<div class="row"><label>'+esc(t("toys_color"))+'</label>'+sel("color_order",d.color_orders||[],c.color_order)
+'<label style="min-width:auto">'+esc(t("toys_brightness"))+'</label>'+num("brightness",c.brightness,1,100)
+'<label style="min-width:auto">'+esc(t("toys_ledwiz"))+'</label>'+num("ledwiz_number",c.ledwiz_number,1,128)+'</div>':'')
Expand All @@ -677,11 +687,18 @@ <h2 style="text-align:center" data-i18n="install_done"></h2>
renderToys()});
document.getElementById("toys-strips").addEventListener("input",e=>{const el=e.target;if(!el.dataset.k)return;const c=toysCtrl(+el.dataset.i);S.toys.touched=true;
if(el.dataset.k==="strips"){c.strips=el.value.split(/[\s,;]+/).filter(x=>x!=="").map(x=>Math.max(0,parseInt(x,10)||0)).slice(0,10)}
else if(/^role\d+$/.test(el.dataset.k)){const j=+el.dataset.k.slice(4);
c.roles=c.roles||[];while(c.roles.length<=j)c.roles.push("");c.roles[j]=el.value}
else if(["width","height","brightness","ledwiz_number"].includes(el.dataset.k)){c[el.dataset.k]=parseInt(el.value,10)||0}
else{c[el.dataset.k]=el.value}
refreshToysStatus();
const tot=el.closest(".card");if(tot){const sp=tot.querySelectorAll(".hint");}});
document.getElementById("toys-strips").addEventListener("change",e=>{if(e.target.dataset.k)renderToys()});
document.getElementById("toys-strips").addEventListener("change",e=>{
const el=e.target;if(!el.dataset.k)return;const c=toysCtrl(+el.dataset.i);S.toys.touched=true;
if(/^role\d+$/.test(el.dataset.k)){const j=+el.dataset.k.slice(4);
c.roles=c.roles||[];while(c.roles.length<=j)c.roles.push("");c.roles[j]=el.value}
else if(el.dataset.k==="role"){c.role=el.value}
renderToys()});
// PINCABOS_INSTALLEUR_RESEAU_V1 — étape Réseau : DHCP par défaut, IP fixe
// proposée depuis le bail, Wi-Fi seulement si un matériel est présent. Chaque
// application est immédiate dans la session (le résultat se voit) ; le réseau
Expand Down
142 changes: 142 additions & 0 deletions opt/pincabos/tests/test_toys_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Chaque sortie adressable a un rôle (PINCABOS_TOYS_ROLES_V1).

Le nom du toy dans cabinet.xml était générique : « Backboard HD » pour la
première matrice, « Matrice 2 », « Ruban 1.2 »… Exact, mais illisible le jour
où l'on relit sa configuration — et c'est ce nom que DOF cherche ensuite.

Le rôle (backboard, sous le meuble, côtés, fronton, flippers, intérieur) donne
son nom à la sortie. Il reste FACULTATIF : sans lui, le nom générique demeure,
donc rien ne change pour un cabinet déjà configuré.

« backboard » garde volontairement « Backboard HD » : c'est le nom qu'ont les
cabinets en service, et le changer casserait leur configuration DOF.
"""
import importlib.util
import json
import unittest
from pathlib import Path

from _charge import RACINE

R = Path(RACINE)
OUTIL = R / "opt/pincabos/tools/pincabos_dof.py"
GUI = R / "opt/pincabos/installer-gui"
WIZARD = GUI / "templates/wizard.html"
I18N = GUI / "i18n.json"
LANGUES = ("fr", "en", "de", "it", "es")

DETECTES = [{"dev": "/dev/ttyACM1", "serial": "TEENSY123", "auto_config": False,
"kind": "TeensyStripController (strip adressable)"}]


def _module():
spec = importlib.util.spec_from_file_location("pincabos_dof", OUTIL)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m


class LeCatalogue(unittest.TestCase):
def setUp(self):
self.m = _module()

def test_les_roles_attendus(self):
for r in ("backboard", "sous-caisse", "cote-gauche", "cote-droit",
"fronton", "flippers", "interieur", "libre"):
self.assertIn(r, self.m.ROLE_IDS)

def test_backboard_garde_son_nom_historique(self):
# les cabinets en service ont « Backboard HD » dans leur cabinet.xml
self.assertEqual(self.m.nom_toy("backboard", "peu importe"), "Backboard HD")

def test_sans_role_le_nom_generique_demeure(self):
self.assertEqual(self.m.nom_toy("", "Ruban 1.2"), "Ruban 1.2")
self.assertEqual(self.m.nom_toy("libre", "Ruban 1.2"), "Ruban 1.2")


class LaValidation(unittest.TestCase):
def setUp(self):
self.m = _module()

def _ctrl(self, **kw):
c = {"serial": "TEENSY123", "enabled": True, "mode": "rubans",
"strips": [144, 144], "arrangement": "LeftRightTopDown",
"color_order": "GRB", "brightness": 25, "ledwiz_number": 30}
c.update(kw)
return {"controllers": [c]}

def test_un_role_connu_passe(self):
erreurs, ok = self.m.valider_toys(self._ctrl(roles=["sous-caisse", "fronton"]), DETECTES)
self.assertEqual(erreurs, [])
self.assertEqual(ok["controllers"][0]["roles"], ["sous-caisse", "fronton"])

def test_un_role_inconnu_est_refuse(self):
erreurs, _ = self.m.valider_toys(self._ctrl(roles=["gyrophare"]), DETECTES)
self.assertTrue(any("rôle inconnu" in e for e in erreurs))

def test_le_role_reste_facultatif(self):
erreurs, ok = self.m.valider_toys(self._ctrl(), DETECTES)
self.assertEqual(erreurs, [])
self.assertEqual(ok["controllers"][0]["roles"], [])
self.assertEqual(ok["controllers"][0]["role"], "")

def test_la_proposition_n_avance_aucun_role(self):
# cohérent avec PINCABOS_TOYS_SANS_SUPPOSITION_V1
for c in self.m.proposer_toys(DETECTES)["controllers"]:
self.assertEqual(c["role"], "")
self.assertEqual(c["roles"], [])


class LesToysPortentLeNomDuRole(unittest.TestCase):
def setUp(self):
self.m = _module()

def test_en_rubans_chaque_sortie_est_nommee(self):
choix = {"controllers": [{"serial": "TEENSY123", "enabled": True, "mode": "rubans",
"strips": [144, 0, 72], "roles": ["sous-caisse", "", "fronton"],
"arrangement": "LeftRightTopDown", "color_order": "GRB",
"brightness": 25, "ledwiz_number": 30}]}
_, ok = self.m.valider_toys(choix, DETECTES)
inv = self.m.inventaire_json(ok, DETECTES)
noms = [t["name"] for t in inv["devices"][0]["toys"]]
self.assertEqual(noms, ["Undercab", "Backbox"],
"la sortie vide ne produit pas de toy, les autres portent leur rôle")

def test_une_sortie_sans_role_garde_le_nom_generique(self):
choix = {"controllers": [{"serial": "TEENSY123", "enabled": True, "mode": "rubans",
"strips": [144, 144], "roles": ["sous-caisse"],
"arrangement": "LeftRightTopDown", "color_order": "GRB",
"brightness": 25, "ledwiz_number": 30}]}
_, ok = self.m.valider_toys(choix, DETECTES)
noms = [t["name"] for t in self.m.inventaire_json(ok, DETECTES)["devices"][0]["toys"]]
self.assertEqual(noms, ["Undercab", "Ruban 1.2"])

def test_en_matrice_le_role_nomme_le_controleur(self):
choix = {"controllers": [{"serial": "TEENSY123", "enabled": True, "mode": "matrice",
"width": 144, "height": 16, "role": "fronton",
"strips": [512, 512, 512, 512, 256],
"arrangement": "TopDownAlternateLeftRight", "color_order": "GRB",
"brightness": 25, "ledwiz_number": 30}]}
_, ok = self.m.valider_toys(choix, DETECTES)
self.assertEqual(self.m.inventaire_json(ok, DETECTES)["devices"][0]["toy"]["name"], "Backbox")


class LAssistantLeDemande(unittest.TestCase):
def test_le_choix_est_dans_la_page(self):
h = WIZARD.read_text(encoding="utf-8")
self.assertIn("selRole", h)
self.assertIn('t("toys_role_"+r)', h)
self.assertIn("c.roles=c.roles||[]", h, "le tableau des rôles doit être alimenté")

def test_l_api_expose_les_roles(self):
s = (GUI / "app.py").read_text(encoding="utf-8")
self.assertIn('"roles": list(pco_dof.ROLE_IDS)', s)

def test_un_libelle_par_role_dans_les_cinq_langues(self):
d = json.loads(I18N.read_text(encoding="utf-8"))
m = _module()
tailles = {k: len(v) for k, v in d.items()}
self.assertEqual(len(set(tailles.values())), 1, "langues divergentes : %s" % tailles)
for lang in LANGUES:
for r in m.ROLE_IDS:
self.assertIn("toys_role_" + r, d[lang], "%s manque en %s" % (r, lang))
Loading
Loading