diff --git a/docs/playtest/metrics_template.csv b/docs/playtest/metrics_template.csv new file mode 100644 index 0000000..82a10d4 --- /dev/null +++ b/docs/playtest/metrics_template.csv @@ -0,0 +1,3 @@ +session_id,date,group_size,target_duration,actual_duration,group_profile_detected,group_profile_actual,puzzles_solved,puzzles_skipped,hints_total,nps_score,notes +beta_001,,,,,,,,,,, +beta_002,,,,,,,,,,, diff --git a/docs/playtest/protocol.md b/docs/playtest/protocol.md new file mode 100644 index 0000000..f1f527f --- /dev/null +++ b/docs/playtest/protocol.md @@ -0,0 +1,187 @@ +# Protocole de Bêta-Test — Le Mystère du Professeur Zacus V3 + +> Version : 1.0 — 2026-04-03 +> Objectif : Valider l'expérience complète avec 2 groupes minimum (profil TECH + profil NON_TECH) + +--- + +## Sélection des Groupes + +| Critère | Groupe 1 (TECH) | Groupe 2 (NON_TECH) | +|---------|-----------------|---------------------| +| Taille | 4–6 personnes | 4–6 personnes | +| Profil | Développeurs, ingénieurs, makers | Artistes, enseignants, étudiants hors tech | +| Âge recommandé | 18–45 ans | 10–50 ans | +| Expérience escape room | Pas d'expérience (ou peu) | Pas d'expérience (ou peu) | +| Langue | Français | Français | + +--- + +## Brief Pré-Session (30 min avant chaque groupe) + +### Checklist déploiement complet + +- [ ] Checklist 15 minutes (`docs/deployment/15min_setup_checklist.md`) complétée +- [ ] Durée cible réglée à **60 minutes** pour le bêta-test +- [ ] Caméra d'observation positionnée (consentement joueurs signé) +- [ ] Grille d'observation imprimée pour chaque observateur (cette page, section "Points d'observation") +- [ ] Questionnaire NPS préparé (papier ou Typeform sur téléphone) +- [ ] Chronomètre prêt (ou timer BOX-3 activé) +- [ ] Maître du jeu en position d'observation silencieuse + +### Consignes aux observateurs + +L'observateur **ne participe pas** et **ne donne aucune aide** sauf urgence de sécurité. Il note en silence : +- Les temps de résolution +- Les comportements de groupe +- Les verbatims spontanés (noter mot pour mot) +- Les moments de blocage, frustration, découverte + +--- + +## Points d'Observation par Puzzle + +Pour chaque puzzle, l'observateur complète la grille suivante : + +### Grille d'observation (par puzzle) + +| Puzzle | Heure début | Heure fin | Durée | Indices demandés | Tentatives ratées | Verbatims clés | Observation | +|--------|------------|-----------|-------|-----------------|-------------------|----------------|-------------| +| P1 — Séquence Sonore | | | | | | | | +| P2 — Circuit LED | | | | | | | | +| P3 — QR Treasure | | | | | | | | +| P4 — Fréquence Radio | | | | | | | | +| P5 — Code Morse | | | | | | | | +| P6 — Symboles Alchimiques | | | | | | | | +| P7 — Coffre Final | | | | | | | | + +### Comportements à noter impérativement + +- **Confusion initiale :** "C'est quoi ça ?", "On fait quoi ?" sans lire les instructions +- **Blocage prolongé** : plus de 5 min sans progression visible +- **Moment "Eurêka"** : réaction physique ou verbale de découverte +- **Frustration** : abandon, verbalisation négative, regard vers l'observateur +- **Collaboration** : qui prend le leadership ? comment le groupe se coordonne-t-il ? +- **Réaction au Professeur Zacus** : rire, sourire, question sur la voix (est-ce un humain ?) + +--- + +## Vérification de la Classification NPC (Phase de Profilage) + +À noter après la résolution de P1 et P2 : + +| Donnée | Valeur mesurée | +|--------|---------------| +| Temps P1 (P1 solve time) | _______ secondes | +| Temps P2 (P2 solve time) | _______ secondes | +| Classification NPC affichée sur BOX-3 | TECH / NON_TECH / MIXED | +| Votre évaluation subjective du groupe | TECH / NON_TECH / MIXED | +| Classification correcte ? | OUI / NON | + +**Objectif bêta :** > 80 % de classifications correctes sur l'ensemble des sessions. + +--- + +## Suivi de la Précision Temporelle + +| Donnée | Valeur | +|--------|--------| +| Durée cible configurée | 60 minutes | +| Heure de début effective | ____:____ | +| Heure de fin effective | ____:____ | +| Durée réelle | _______ minutes | +| Écart (réel - cible) | _______ minutes (_______ %) | + +**Objectif bêta :** écart ≤ ± 10 % de la durée cible (soit ≤ 6 minutes sur 60 min). + +--- + +## Débrief Post-Session (30 min après la fin du jeu) + +### Phase 1 : Discussion libre (10 min) + +Laisser le groupe s'exprimer librement. Questions ouvertes : +- "Quelle a été votre première impression en arrivant ?" +- "Quel puzzle vous a le plus surpris ?" +- "Avez-vous cru que le Professeur était une vraie personne ?" +- "Qu'est-ce qui vous a le plus bloqués ?" +- "Si vous pouviez changer une chose, ce serait quoi ?" + +### Phase 2 : Enquête NPS (10 min) + +Distribuer le questionnaire NPS (voir section suivante). Anonyme, individuel, sans discussion. + +### Phase 3 : Questions techniques (10 min, optionnel) + +Pour les groupes curieux (souvent TECH) : +- Montrer l'architecture ESP-NOW sur BOX-3 +- Expliquer le clonage vocal XTTS-v2 +- Montrer le scoring adaptatif + +--- + +## Questionnaire NPS — 10 Questions (Français) + +> Instructions : Notez chacune des affirmations suivantes de 1 (pas du tout) à 10 (absolument). + +**Q1.** Dans quelle mesure recommanderiez-vous cette expérience à un ami ou collègue ? +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q2.** Le Professeur Zacus vous a semblé crédible et engageant comme personnage. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q3.** La difficulté des puzzles était adaptée à votre groupe. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q4.** Les instructions de chaque puzzle étaient claires et compréhensibles. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q5.** La qualité de la voix du Professeur Zacus était convaincante. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q6.** L'ambiance générale (décor, son, lumières) vous a immergé dans l'univers. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q7.** Vous avez ressenti une progression narrative cohérente du début à la fin. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q8.** Le matériel physique (boîtiers, tablettes, télégraphe) vous a semblé solide et bien fait. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q9.** L'expérience a favorisé la cohésion et la collaboration au sein de votre groupe. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Q10.** Vous seriez prêt(e) à participer à une prochaine version ou à un scénario différent. +`[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 10 ]` + +**Questions ouvertes :** +- Votre puzzle préféré et pourquoi : ___________________________________ +- Le moment le plus confus ou frustrant : ___________________________________ +- Suggestions libres : ___________________________________ + +--- + +## Critères de Succès du Bêta-Test + +| Critère | Objectif | Mesure | +|---------|----------|--------| +| Taux de complétion | Les 2 groupes atteignent GAME_OUTRO | BOX-3 session log | +| NPS moyen (Q1) | ≥ 8/10 | Questionnaire | +| Temps de setup | ≤ 15 minutes | Chronomètre | +| Précision durée | Écart ≤ ±10 % | Mesure directe | +| Classification NPC | Correcte pour les 2 groupes | Grille observation | +| Pannes matériel | Zéro pendant la session | Grille observation | +| Voix perçue comme humaine | ≥ 3/5 joueurs ne font pas la différence | Q5 + verbatims | + +--- + +## Checklist Observateur (pendant le jeu) + +- [ ] Timer démarré à `[GAME_START]` (synchro avec BOX-3) +- [ ] Grille observation imprimée, stylo en main +- [ ] Téléphone en mode silencieux +- [ ] Caméra en marche (si consentement obtenu) +- [ ] Ne pas répondre aux questions des joueurs (sourire et pointer BOX-3) +- [ ] Marquer toute panne matérielle immédiatement (heure + description) +- [ ] Marquer toute intervention NPC (heure + phrase + réaction groupe) +- [ ] À la fin : noter heure exacte de GAME_OUTRO et score affiché diff --git a/hardware/puzzles/BOM_V3_complete.csv b/hardware/puzzles/BOM_V3_complete.csv new file mode 100644 index 0000000..eaaf592 --- /dev/null +++ b/hardware/puzzles/BOM_V3_complete.csv @@ -0,0 +1,113 @@ +Puzzle,Reference,Value,Footprint,Quantity,LCSC_Part,Unit_Price_EUR,Total_Price_EUR,Datasheet_URL +P1 Séquence Sonore,MCU_P1,ESP32-S3-DevKitC-1-N16R8,Module_ESP32-S3:ESP32-S3-DevKitC-1,1,C2913202,12.00,12.00,https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html +P1 Séquence Sonore,U1_P1,MAX98357AEWL+T I2S Class-D Amp,Package_SON:WLCSP-9_1.595x1.415mm_Pitch0.4mm,1,C236813,2.50,2.50,https://datasheets.maximintegrated.com/en/ds/MAX98357A-MAX98357B.pdf +P1 Séquence Sonore,SP1_P1,Speaker 8ohm 2W 57mm,Connector:PinHeader_2.54mm_1x02,1,,2.80,2.80,https://www.tme.eu/Document/bc34b0283d78d58b7ec55d24a1e3fde4/KPSP-0520L.pdf +P1 Séquence Sonore,SW1_P1_RED,Arcade Button 30mm RED,Button_Switch_THT:SW_Sanwa_OBSF-30_Vertical,1,,2.80,2.80,https://www.sanwa.co.jp/product/game/pdf/obsf-30.pdf +P1 Séquence Sonore,SW2_P1_BLUE,Arcade Button 30mm BLUE,Button_Switch_THT:SW_Sanwa_OBSF-30_Vertical,1,,2.80,2.80,https://www.sanwa.co.jp/product/game/pdf/obsf-30.pdf +P1 Séquence Sonore,SW3_P1_YELLOW,Arcade Button 30mm YELLOW,Button_Switch_THT:SW_Sanwa_OBSF-30_Vertical,1,,2.80,2.80,https://www.sanwa.co.jp/product/game/pdf/obsf-30.pdf +P1 Séquence Sonore,SW4_P1_GREEN,Arcade Button 30mm GREEN,Button_Switch_THT:SW_Sanwa_OBSF-30_Vertical,1,,2.80,2.80,https://www.sanwa.co.jp/product/game/pdf/obsf-30.pdf +P1 Séquence Sonore,LED1_P1,WS2812B 4-LED Strip,LED_WS2812B:WS2812B_Strip,1,C2761795,1.20,1.20,https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf +P1 Séquence Sonore,U2_P1,SN74HCT125N Level Shifter,Package_DIP:DIP-14_W7.62mm,1,C5616,0.25,0.25,https://www.ti.com/lit/ds/symlink/sn74hct125.pdf +P1 Séquence Sonore,U3_P1,AMS1117-3.3 LDO SOT-223,Package_TO_SOT_SMD:SOT-223-3_TabPin2,1,C6186,0.08,0.08,https://www.advanced-monolithic.com/pdf/ds1117.pdf +P1 Séquence Sonore,R1_P1,10kΩ Pull-up 0603,Resistor_SMD:R_0603_1608Metric,4,C25804,0.01,0.04, +P1 Séquence Sonore,R2_P1,470Ω LED series 0603,Resistor_SMD:R_0603_1608Metric,1,C23179,0.01,0.01, +P1 Séquence Sonore,C1_P1,100nF Bypass Cap 0603,Capacitor_SMD:C_0603_1608Metric,4,C14663,0.01,0.04, +P1 Séquence Sonore,CONN1_P1,USB-C Power Connector 2A,Connector_USB:USB_C_Receptacle_GCT_USB4085,1,C167688,0.35,0.35,https://gct.co/files/specs/usb4085-spec.pdf +P1 Séquence Sonore,PCB_P1,Custom PCB 100x100mm 2L JLCPCB,PCB,1,,2.00,2.00,https://jlcpcb.com +P2 Circuit LED,MCU_P2,ESP32-DevKitC-32E,Module_ESP32:ESP32-DevKitC-32E,1,C3406749,6.50,6.50,https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-devkitc.html +P2 Circuit LED,SW_REED_P2,Reed Switch NC MK04,Connector:PinHeader_2.54mm_1x02,8,C90067,0.45,3.60,https://standexelectronics.com/wp-content/uploads/datasheet_reed_switch_MK04.pdf +P2 Circuit LED,LED2_P2,WS2812B 16-LED Strip,LED_WS2812B:WS2812B_Strip,1,C2761795,3.50,3.50,https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf +P2 Circuit LED,U1_P2,SN74HCT125N Level Shifter,Package_DIP:DIP-14_W7.62mm,1,C5616,0.25,0.25,https://www.ti.com/lit/ds/symlink/sn74hct125.pdf +P2 Circuit LED,U2_P2,AMS1117-3.3 LDO SOT-223,Package_TO_SOT_SMD:SOT-223-3_TabPin2,1,C6186,0.08,0.08,https://www.advanced-monolithic.com/pdf/ds1117.pdf +P2 Circuit LED,BZ1_P2,Active Buzzer 5V TMB12A05,Buzzer_Beeper:BZ_KPM-G1205,1,C96093,0.15,0.15, +P2 Circuit LED,LED_STATUS_P2,LED Green 5mm 470Ω,LED_THT:LED_D5.0mm,1,C434435,0.05,0.05, +P2 Circuit LED,Q1_P2,NPN 2N2222 Transistor,Package_TO_SOT_THT:TO-92_Inline,1,C2154,0.10,0.10,https://www.onsemi.com/pdf/datasheet/p2n2222a-d.pdf +P2 Circuit LED,R1_P2,10kΩ Pull-up 0603,Resistor_SMD:R_0603_1608Metric,8,C25804,0.01,0.08, +P2 Circuit LED,R2_P2,1kΩ Base resistor 0603,Resistor_SMD:R_0603_1608Metric,1,C21190,0.01,0.01, +P2 Circuit LED,R3_P2,470Ω LED series 0603,Resistor_SMD:R_0603_1608Metric,1,C23179,0.01,0.01, +P2 Circuit LED,C1_P2,1000µF 10V Bulk Cap,Capacitor_THT:C_Radial_D8.0mm_H11.5mm_P3.50mm,1,C2915,0.25,0.25, +P2 Circuit LED,C2_P2,100nF Bypass Cap 0603,Capacitor_SMD:C_0603_1608Metric,2,C14663,0.01,0.02, +P2 Circuit LED,CONN1_P2,USB-C Power Connector 2A,Connector_USB:USB_C_Receptacle_GCT_USB4085,1,C167688,0.35,0.35,https://gct.co/files/specs/usb4085-spec.pdf +P2 Circuit LED,MAG_COMPONENTS,Magnetic Component Set (resistors/LEDs/wires on magnetic board),Mechanical,1,,12.00,12.00,Custom kit — laser-cut magnetic folding board +P2 Circuit LED,PCB_P2,Custom PCB 100x100mm 2L JLCPCB,PCB,1,,2.00,2.00,https://jlcpcb.com +P3 QR Codes,CARDS_P3,Laminated A5 QR Code Cards,Mechanical,6,,0.30,1.80,Print locally — QR generated by BOX-3 app +P4 Radio Rétro,MCU_P4,ESP32-DevKitC-32E,Module_ESP32:ESP32-DevKitC-32E,1,C3406749,6.50,6.50,https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-devkitc.html +P4 Radio Rétro,ENC_P4,Rotary Encoder KY-040,Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm,1,C141881,0.60,0.60,https://www.alldatasheet.com/datasheet-pdf/pdf/1132088/ETC2/KY-040.html +P4 Radio Rétro,U1_P4,MAX98357AEWL+T I2S Class-D Amp,Package_SON:WLCSP-9_1.595x1.415mm_Pitch0.4mm,1,C236813,2.50,2.50,https://datasheets.maximintegrated.com/en/ds/MAX98357A-MAX98357B.pdf +P4 Radio Rétro,SP1_P4,Speaker 8ohm 2W 57mm,Connector:PinHeader_2.54mm_1x02,1,,2.80,2.80, +P4 Radio Rétro,DISP1_P4,SSD1306 OLED 128x64 I2C,Display_OLED:OLED_128x64,1,C125770,1.80,1.80,https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf +P4 Radio Rétro,U2_P4,AMS1117-3.3 LDO SOT-223,Package_TO_SOT_SMD:SOT-223-3_TabPin2,1,C6186,0.08,0.08,https://www.advanced-monolithic.com/pdf/ds1117.pdf +P4 Radio Rétro,U3_P4,TP4056 Li-Ion Charger,Package_SO:MSOP-8_3x3mm_P0.65mm,1,C16581,0.12,0.12,http://www.tp4056.com/d/tp4056.pdf +P4 Radio Rétro,U4_P4,MT3608 Boost Converter,Package_TO_SOT_SMD:SOT-23-6,1,C84817,0.18,0.18,https://www.aerosemi.com/upload/MT3608_DS_1.0.pdf +P4 Radio Rétro,BAT_P4,18650 Li-Ion 3000mAh Samsung INR18650-30Q,BatteryHolder:BatteryHolder_2x18650,2,C148263,5.50,11.00,https://www.mouser.com/datasheet/2/408/Samsung_SDI_INR18650_30Q_35E-1709614.pdf +P4 Radio Rétro,R1_P4,10kΩ Pull-up 0603,Resistor_SMD:R_0603_1608Metric,3,C25804,0.01,0.03, +P4 Radio Rétro,R2_P4,4.7kΩ I2C pull-up 0603,Resistor_SMD:R_0603_1608Metric,2,C23162,0.01,0.02, +P4 Radio Rétro,R3_P4,10kΩ RC filter 0603,Resistor_SMD:R_0603_1608Metric,1,C25804,0.01,0.01, +P4 Radio Rétro,C1_P4,100nF RC filter 0603,Capacitor_SMD:C_0603_1608Metric,1,C14663,0.01,0.01, +P4 Radio Rétro,C2_P4,100nF Bypass Cap 0603,Capacitor_SMD:C_0603_1608Metric,3,C14663,0.01,0.03, +P4 Radio Rétro,CONN1_P4,USB-C Power Connector 2A,Connector_USB:USB_C_Receptacle_GCT_USB4085,1,C167688,0.35,0.35,https://gct.co/files/specs/usb4085-spec.pdf +P4 Radio Rétro,PCB_P4,Custom PCB 100x100mm 2L JLCPCB,PCB,1,,2.00,2.00,https://jlcpcb.com +P5 Morse,MCU_P5,ESP32-DevKitC-32E,Module_ESP32:ESP32-DevKitC-32E,1,C3406749,6.50,6.50,https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-devkitc.html +P5 Morse,SW_TELEGRAPH_P5,Brass Morse Telegraph Key,Connector:PinHeader_2.54mm_1x02,1,,3.50,3.50,Custom brass/wood — Aliexpress morse key +P5 Morse,BZ1_P5,Active Buzzer 5V TMB12A05,Buzzer_Beeper:BZ_KPM-G1205,1,C96093,0.15,0.15, +P5 Morse,LED_RED_P5,LED Red 5mm,LED_THT:LED_D5.0mm,1,C434434,0.05,0.05, +P5 Morse,LED_GREEN_P5,LED Green 5mm,LED_THT:LED_D5.0mm,1,C434435,0.05,0.05, +P5 Morse,LED_WS_P5,WS2812B 1-LED (NON_TECH light mode),LED_WS2812B:WS2812B_2020,1,C2761795,0.30,0.30,https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf +P5 Morse,U1_P5,AMS1117-3.3 LDO SOT-223,Package_TO_SOT_SMD:SOT-223-3_TabPin2,1,C6186,0.08,0.08,https://www.advanced-monolithic.com/pdf/ds1117.pdf +P5 Morse,Q1_P5,NPN 2N2222 Transistor,Package_TO_SOT_THT:TO-92_Inline,1,C2154,0.10,0.10,https://www.onsemi.com/pdf/datasheet/p2n2222a-d.pdf +P5 Morse,R1_P5,10kΩ Pull-up 0603,Resistor_SMD:R_0603_1608Metric,1,C25804,0.01,0.01, +P5 Morse,R2_P5,1kΩ Base resistor 0603,Resistor_SMD:R_0603_1608Metric,1,C21190,0.01,0.01, +P5 Morse,R3_P5,470Ω LED series 0603,Resistor_SMD:R_0603_1608Metric,2,C23179,0.01,0.02, +P5 Morse,C1_P5,100nF Bypass Cap 0603,Capacitor_SMD:C_0603_1608Metric,2,C14663,0.01,0.02, +P5 Morse,CONN1_P5,USB-C Power Connector 2A,Connector_USB:USB_C_Receptacle_GCT_USB4085,1,C167688,0.35,0.35,https://gct.co/files/specs/usb4085-spec.pdf +P5 Morse,PCB_P5,Custom PCB 100x100mm 2L JLCPCB,PCB,1,,2.00,2.00,https://jlcpcb.com +P6 Symboles NFC,MCU_P6,ESP32-DevKitC-32E,Module_ESP32:ESP32-DevKitC-32E,1,C3406749,6.50,6.50,https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-devkitc.html +P6 Symboles NFC,U1_P6,MFRC522 NFC Reader Module,MFRC522:MFRC522_Module,1,C411208,1.20,1.20,https://www.nxp.com/docs/en/data-sheet/MFRC522.pdf +P6 Symboles NFC,NFC_TAGS_P6,NTAG213 NFC Stickers (25mm round),Mechanical,12,C7499,0.18,2.16,https://www.nxp.com/docs/en/data-sheet/NTAG213_215_216.pdf +P6 Symboles NFC,TABLET_P6,Wooden Tablet 300x200mm (laser cut),Mechanical,1,,8.00,8.00,12 slots for NFC symbol pieces — 3mm plywood +P6 Symboles NFC,SYMBOLS_P6,Symbol Pieces (laser cut plywood 40x40mm),Mechanical,12,,0.50,6.00,Alchemical symbols engraved — fit over NFC stickers +P6 Symboles NFC,BZ1_P6,Active Buzzer 5V TMB12A05,Buzzer_Beeper:BZ_KPM-G1205,1,C96093,0.15,0.15, +P6 Symboles NFC,LED_GREEN_P6,LED Green 5mm,LED_THT:LED_D5.0mm,1,C434435,0.05,0.05, +P6 Symboles NFC,LED_RED_P6,LED Red 5mm,LED_THT:LED_D5.0mm,1,C434434,0.05,0.05, +P6 Symboles NFC,U2_P6,AMS1117-3.3 LDO SOT-223,Package_TO_SOT_SMD:SOT-223-3_TabPin2,1,C6186,0.08,0.08,https://www.advanced-monolithic.com/pdf/ds1117.pdf +P6 Symboles NFC,Q1_P6,NPN 2N2222 Transistor,Package_TO_SOT_THT:TO-92_Inline,1,C2154,0.10,0.10, +P6 Symboles NFC,R1_P6,4.7kΩ SPI pull-up 0603,Resistor_SMD:R_0603_1608Metric,2,C23162,0.01,0.02, +P6 Symboles NFC,R2_P6,1kΩ Base resistor 0603,Resistor_SMD:R_0603_1608Metric,1,C21190,0.01,0.01, +P6 Symboles NFC,R3_P6,470Ω LED series 0603,Resistor_SMD:R_0603_1608Metric,2,C23179,0.01,0.02, +P6 Symboles NFC,C1_P6,100nF Bypass Cap 0603,Capacitor_SMD:C_0603_1608Metric,2,C14663,0.01,0.02, +P6 Symboles NFC,CONN1_P6,USB-C Power Connector 2A,Connector_USB:USB_C_Receptacle_GCT_USB4085,1,C167688,0.35,0.35,https://gct.co/files/specs/usb4085-spec.pdf +P6 Symboles NFC,PCB_P6,Custom PCB 100x100mm 2L JLCPCB,PCB,1,,2.00,2.00,https://jlcpcb.com +P7 Coffre Final,MCU_P7,ESP32-DevKitC-32E,Module_ESP32:ESP32-DevKitC-32E,1,C3406749,6.50,6.50,https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-devkitc.html +P7 Coffre Final,SERVO_P7,SG90 Servo Motor,Connector:PinHeader_2.54mm_1x03,1,C72037,1.20,1.20,https://www.towerpro.com.tw/product/sg90-7/ +P7 Coffre Final,KBD_P7,4x3 Membrane Keypad,Connector:PinHeader_2.54mm_1x07,1,C2935585,1.20,1.20, +P7 Coffre Final,DISP1_P7,SSD1306 OLED 128x64 I2C,Display_OLED:OLED_128x64,1,C125770,1.80,1.80,https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf +P7 Coffre Final,LED_RGB_P7,WS2812B RGB LED 1x,LED_WS2812B:WS2812B_2020,1,C2761795,0.30,0.30,https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf +P7 Coffre Final,U1_P7,AMS1117-3.3 LDO SOT-223,Package_TO_SOT_SMD:SOT-223-3_TabPin2,1,C6186,0.08,0.08,https://www.advanced-monolithic.com/pdf/ds1117.pdf +P7 Coffre Final,BZ1_P7,Active Buzzer 5V TMB12A05,Buzzer_Beeper:BZ_KPM-G1205,1,C96093,0.15,0.15, +P7 Coffre Final,Q1_P7,NPN 2N2222 Transistor,Package_TO_SOT_THT:TO-92_Inline,1,C2154,0.10,0.10, +P7 Coffre Final,R1_P7,4.7kΩ I2C pull-up 0603,Resistor_SMD:R_0603_1608Metric,2,C23162,0.01,0.02, +P7 Coffre Final,R2_P7,10kΩ Keypad pull-up 0603,Resistor_SMD:R_0603_1608Metric,3,C25804,0.01,0.03, +P7 Coffre Final,R3_P7,1kΩ Base resistor 0603,Resistor_SMD:R_0603_1608Metric,1,C21190,0.01,0.01, +P7 Coffre Final,C1_P7,100nF Bypass Cap 0603,Capacitor_SMD:C_0603_1608Metric,3,C14663,0.01,0.03, +P7 Coffre Final,BOX_P7,Wooden box 250x200x150mm,Mechanical,1,,12.00,12.00,Pine wood box — hardware store +P7 Coffre Final,CONN1_P7,USB-C Power Connector 2A,Connector_USB:USB_C_Receptacle_GCT_USB4085,1,C167688,0.35,0.35,https://gct.co/files/specs/usb4085-spec.pdf +P7 Coffre Final,PCB_P7,Custom PCB 100x100mm 2L JLCPCB,PCB,1,,2.00,2.00,https://jlcpcb.com +HUB,BOX3_HUB,ESP32-S3-BOX-3,Espressif Dev Kit,1,,49.00,49.00,https://www.espressif.com/en/products/devkits/esp32-s3-box-3 +HUB,RTC_PHONE_HUB,RTC_BL_PHONE Custom Board (ESP32 + SLIC),Custom PCB,1,,35.00,35.00,Voice hub — ESP32 + Si3220 SLIC + RJ11 connector +HUB,ROUTER_HUB,GL.iNet GL-MT3000 WiFi 6 Router,Network,1,,69.00,69.00,https://www.gl-inet.com/products/gl-mt3000/ +HUB,BAT1_HUB,Anker 20000mAh USB-C PowerBank A1287,Power,3,,25.00,75.00,https://www.anker.com/products/a1287 +HUB,SPEAKER_HUB,JBL Go 3 Bluetooth Speaker,Audio,1,,29.00,29.00,https://www.jbl.com/bluetooth-speakers/JBL+GO+3.html +HUB,LED_STRIP_HUB,USB LED Strip 3m Warm White,Lighting,1,,8.00,8.00,Ambient lighting for suitcase +HUB,CABLE_USB_HUB,USB-C Cable 1m Ugreen (Qty 10),Power,10,,3.00,30.00,All puzzle power cables +HUB,SUITCASE_1,Suitcase Rimowa 57L equivalent ABS,Mechanical,1,,45.00,45.00,Main hub suitcase — rigid +HUB,SUITCASE_2,Suitcase 42L softcase,Mechanical,1,,25.00,25.00,Puzzle objects suitcase A +HUB,SUITCASE_3,Suitcase 42L softcase,Mechanical,1,,25.00,25.00,Puzzle objects suitcase B +HUB,FOAM_CUT,EVA Foam sheets 10mm (for suitcase cutouts),Mechanical,3,,6.00,18.00,Custom foam per suitcase +HUB,SIGNAGE,Printed signage A4 laminated (puzzle labels),Print,10,,0.50,5.00,Puzzle identification labels +HUB,MORSE_REF,Morse code reference card A6 laminated,Print,10,,0.20,2.00,Hint card for P5 +HUB,PCB_HUB,RTC_PHONE Custom PCB JLCPCB,PCB,1,,8.00,8.00,4-layer — SLIC + ESP32 integration +,,,,,,,, +SUMMARY,Total Puzzle Electronics,,,,,,,207.00 +SUMMARY,Total Hub & Infrastructure,,,,,,,390.00 +SUMMARY,Total Enclosures & Mechanical,,,,,,,130.00 +SUMMARY,Total Kit,,,,,,,727.00 diff --git a/hardware/puzzles/assembly_checklist.md b/hardware/puzzles/assembly_checklist.md new file mode 100644 index 0000000..4c05a45 --- /dev/null +++ b/hardware/puzzles/assembly_checklist.md @@ -0,0 +1,315 @@ +# Guide d'Assemblage et Checklist Qualité — Kit V3 + +> Dernière mise à jour : 2026-04-03 +> Temps estimé (premier assemblage) : 8-12 heures +> Temps estimé (assemblage répété, kit connu) : 3-4 heures + +--- + +## Prérequis + +### Outillage +- Fer à souder 50 W + étain 0,8 mm +- Multimètre (continuité + tension) +- Pistolet à colle thermique +- Tournevis Torx T8 + cruciforme PH1 +- Pince coupante, pince à dénuder +- Imprimante 3D (PLA/ABS pour boîtiers) +- Découpeuse laser (optionnel — tablette P6 peut être achetée) + +### Logiciels requis sur le poste d'assemblage +- PlatformIO CLI (`pip install platformio`) +- Python 3.11+ +- `esptool.py` (`pip install esptool`) +- `idf.py` (ESP-IDF v5.x pour ESP32-S3-BOX-3) + +--- + +## Étape 1 : Impression 3D des Boîtiers (2-4 h) + +| Boîtier | Fichier STL | Matière | Paramètres | +|---------|------------|---------|-----------| +| P1 Boîte Sonore | `hardware/enclosure/p1_son/p1_son_box.stl` | PLA noir | 0,2 mm, 20% infill | +| P2 Support Breadboard | `hardware/enclosure/p2_circuit/p2_frame.stl` | PLA gris | 0,2 mm, 15% infill | +| P4 Poste Radio | `hardware/enclosure/p4_radio/p4_radio_retro.stl` | PLA marron | 0,2 mm, 30% infill | +| P5 Télégraphe base | `hardware/enclosure/p5_morse/p5_base.stl` | PLA bois (PHA) | 0,15 mm, 25% infill | +| P7 Coffre | `hardware/enclosure/p7_coffre/p7_coffre_body.stl` | ABS noir | 0,2 mm, 40% infill | + +- [ ] Tous les boîtiers imprimés sans warping +- [ ] Trous de fixation propres (percer si nécessaire : M3 pour PCB, M2 pour servo) +- [ ] Post-traitement : ponçage 120→240 grains si besoin esthétique + +--- + +## Étape 2 : Assemblage Électronique par Puzzle + +### P1 — Boîte Sonore (Séquence musicale) + +**Composants :** +- ESP32-S3 DevKit × 1 +- Haut-parleur 8 Ω 1 W × 1 +- MAX98357A (I2S amplificateur) × 1 +- Boutons arcade 30 mm (rouge, bleu, jaune, vert) × 4 +- LED 5 mm (couleurs correspondantes) × 4 +- Résistances 220 Ω × 4 + +**Câblage :** +``` +ESP32-S3 GPIO4 → BCLK (MAX98357A) +ESP32-S3 GPIO5 → LRCLK +ESP32-S3 GPIO6 → DIN +GPIO 12/13/14/15 → Boutons (INPUT_PULLUP) +GPIO 16/17/18/19 → LEDs via 220 Ω +``` + +- [ ] Souder MAX98357A sur mini-PCB stripboard +- [ ] Tester amplificateur seul (multimètre sur sortie : ~5V idle) +- [ ] Coller haut-parleur avec colle thermique +- [ ] Insérer boutons arcade dans les 4 trous du boîtier +- [ ] Câbler selon schéma `hardware/wiring/wiring.md` +- [ ] Test continuité toutes les connexions + +### P2 — Breadboard Magnétique (Circuit LED) + +**Composants :** +- ESP32 DevKit v1 × 1 +- Reed switches × 8 (détection positionnement composants) +- Magnets néodyme Ø5mm × 8 (intégrés aux composants pédagogiques) +- LED 5 mm rouge × 1 +- Panneau support MDF 60×40 cm (découpé laser) + +**Câblage :** +``` +GPIO 12-19 → Reed switches (INPUT_PULLUP) +GPIO 21 → LED témoin résolution +``` + +- [ ] Coller reed switches sous le panneau MDF aux positions définies +- [ ] Coller magnets dans les embases composants pédagogiques +- [ ] Câbler ESP32 et tester chaque reed switch individuellement +- [ ] Vérifier que la combinaison correcte allume LED GPIO21 + +### P4 — Poste Radio (Fréquence) + +**Composants :** +- ESP32 DevKit v1 × 1 +- Encodeur rotatif KY-040 × 1 +- Écran OLED 128×64 SSD1306 I2C × 1 +- 18650 Li-Ion + support + circuit de charge TP4056 × 1 + +**Câblage :** +``` +Encodeur CLK → GPIO 32 +Encodeur DT → GPIO 33 +Encodeur SW → GPIO 25 +SSD1306 SDA → GPIO 21 +SSD1306 SCL → GPIO 22 +TP4056 OUT → VIN ESP32 +``` + +- [ ] Souder support 18650 + TP4056 sur veroboard +- [ ] Monter encodeur sur face avant boîtier rétro +- [ ] Coller OLED sur fenêtre boîtier (cadre époxy) +- [ ] Charger 18650 à 100% via USB-C avant test + +### P5 — Télégraphe Morse + +**Composants :** +- ESP32 DevKit v1 × 1 +- Bouton télégraphe bois + laiton (artisanal ou impression 3D) × 1 +- Buzzer actif 5V × 1 +- LED ambre 5 mm × 1 (signal visuel mode non-tech) + +**Câblage :** +``` +Bouton télégraphe → GPIO 12 (INPUT_PULLUP, ACTIVE LOW) +Buzzer → GPIO 13 (via transistor NPN 2N2222) +LED ambre → GPIO 14 (via 220 Ω) +``` + +- [ ] Assembler mécanisme télégraphe (ressort de rappel, laiton contact) +- [ ] Tester durée de vie contact (appuyer 100× : doit être fiable) +- [ ] Régler seuil point/tiret dans firmware (défaut : 150 ms) + +### P6 — Tablette Symboles Alchimiques (NFC) + +**Composants :** +- ESP32 DevKit v1 × 1 +- Module NFC PN532 I2C × 1 +- NTAG213 (autocollant NFC) × 12 +- Tablette bois découpée laser 30×20 cm × 1 +- Emplacements gravés laser × 12 + +**Câblage :** +``` +PN532 SDA → GPIO 21 +PN532 SCL → GPIO 22 +PN532 IRQ → GPIO 19 (interruption lecture NFC) +``` + +- [ ] Programmer les 12 UIDs NTAG213 (voir section NVS ci-dessous) +- [ ] Coller symboles alchimiques sur les tuiles NTAG213 +- [ ] Tester lecture NFC de chaque tuile (distance max : 3 cm) +- [ ] Graver les 12 emplacements sur la tablette (laser ou Dremel) + +### P7 — Coffre Final (Verrou électronique) + +**Composants :** +- ESP32 DevKit v1 × 1 +- Clavier matriciel 4×4 × 1 +- Servo SG90 × 1 (verrou mécanique) +- OLED 128×32 SSD1306 × 1 +- Buzzer passif × 1 +- Coffre en bois 25×20×15 cm × 1 + +**Câblage :** +``` +Clavier lignes → GPIO 12/13/14/15 +Clavier colonnes→ GPIO 16/17/18/19 +Servo signal → GPIO 23 (PWM) +OLED SDA → GPIO 21 +OLED SCL → GPIO 22 +Buzzer → GPIO 25 +``` + +- [ ] Tester servo (0°=fermé, 90°=ouvert) +- [ ] Ajuster mécanisme de verrou (doit tenir sans alimentation — ressort) +- [ ] Tester clavier matriciel (toutes les 16 touches) + +--- + +## Étape 3 : Flash Firmware par Puzzle (PlatformIO) + +### Prérequis + +```bash +# Cloner le repo et les submodules +git clone --recurse-submodules https://github.com/electron-rare/le-mystere-professeur-zacus.git +cd le-mystere-professeur-zacus/ESP32_ZACUS + +# Vérifier que PlatformIO reconnaît les environnements +pio project config +``` + +### Flash par environnement + +```bash +# P1 — Boîte Sonore +pio run -e p1_son --target upload --upload-port /dev/ttyUSB0 + +# P2 — Breadboard Magnétique +pio run -e p2_circuit --target upload --upload-port /dev/ttyUSB0 + +# P4 — Poste Radio +pio run -e p4_radio --target upload --upload-port /dev/ttyUSB0 + +# P5 — Télégraphe Morse +pio run -e p5_morse --target upload --upload-port /dev/ttyUSB0 + +# P6 — Tablette Symboles +pio run -e p6_symboles --target upload --upload-port /dev/ttyUSB0 + +# P7 — Coffre Final +pio run -e p7_coffre --target upload --upload-port /dev/ttyUSB0 + +# BOX-3 (ESP32-S3 — utiliser idf.py) +cd ui_freenove_allinone +idf.py -p /dev/ttyUSB0 flash monitor +``` + +> **Note :** sur macOS, remplacer `/dev/ttyUSB0` par `/dev/cu.usbserial-XXXX` (vérifier avec `ls /dev/cu.*`). + +### Vérification post-flash + +```bash +# Monitor série pour vérifier le boot +pio device monitor -e p1_son --baud 115200 + +# Attendu dans les logs : +# [BOOT] P1_SON v3.1 — ESP-NOW MAC: AA:BB:CC:DD:EE:FF +# [ESPNOW] Waiting for BOX-3 master... +``` + +- [ ] P1 : boot OK, MAC affiché, LED clignotante (attente mesh) +- [ ] P2 : boot OK, reed switches testés (log: `REED[0..7]: 00000000`) +- [ ] P4 : boot OK, OLED affiche `FREQ: 800 Hz`, encodeur réactif +- [ ] P5 : boot OK, mode détecté (`TECH` ou `NON_TECH` selon profil) +- [ ] P6 : boot OK, NFC prêt (`PN532: READY, 12 UIDs loaded`) +- [ ] P7 : boot OK, OLED `LOCKED`, clavier actif, servo en position 0° + +--- + +## Étape 4 : Configuration NVS (Non-Volatile Storage) + +Le NVS stocke les paramètres persistants de chaque puzzle : UIDs NFC, credentials WiFi, paramètres maître du jeu. + +### Configuration NFC (P6) + +```bash +# Programmer les UIDs des 12 tuiles NTAG213 +# Créer le fichier nvs_p6.csv : +cat > /tmp/nvs_p6.csv << 'EOF' +key,type,encoding,value +nfc_uid_01,data,hex,04A1B2C3D4E500 +nfc_uid_02,data,hex,04B2C3D4E5F600 +# ... (12 UIDs au total) +nfc_correct_order,data,string,"7,2,11,4,9,1,8,3,12,6,10,5" +EOF + +# Flash NVS partition +python3 $IDF_PATH/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py \ + generate /tmp/nvs_p6.csv /tmp/nvs_p6.bin 0x6000 + +esptool.py -p /dev/ttyUSB0 write_flash 0x9000 /tmp/nvs_p6.bin +``` + +### Configuration WiFi (tous les ESP32) + +```bash +# Méthode simple : via serial monitor au premier démarrage +# Sur le terminal PlatformIO monitor, envoyer : +WIFI_SSID=ZacusVenue2026 +WIFI_PASS=motdepasse_lieu + +# Ou via nvs_partition_gen pour le déploiement en masse : +cat > /tmp/nvs_wifi.csv << 'EOF' +key,type,encoding,value +wifi_ssid,data,string,ZacusVenue2026 +wifi_pass,data,string,motdepasse_lieu +EOF +``` + +### Paramètres Maître du Jeu (BOX-3) + +Sur l'interface tactile BOX-3 au premier démarrage : +- `Settings → Game Master PIN` : définir un PIN à 4 chiffres (accès réglages en jeu) +- `Settings → Default Duration` : 60 min (modifiable à chaque session) +- `Settings → XTTS Host` : `kxkm-ai:5002` (ou laisser vide pour Piper/SD fallback) +- `Settings → Piper Host` : `192.168.0.120:8001` + +--- + +## Étape 5 : Checklist Qualité Avant Première Utilisation + +### Test fonctionnel complet (durée : ~30 min) + +- [ ] **Allumer tout le kit** — toutes batteries connectées, attendre 60 secondes +- [ ] **Mesh ESP-NOW** — BOX-3 affiche `MESH: 6/6 nodes connected` +- [ ] **P1 Séquence Sonore** — Mélodie jouée, appuyer séquence correcte → signal OK envoyé au BOX-3 +- [ ] **P2 Circuit** — Placer composants en position correcte → LED GPIO21 allumée → BOX-3 reçoit `P2_SOLVED` +- [ ] **P3 QR Treasure** — Scanner les 6 QR dans l'ordre → BOX-3 valide `P3_SOLVED` +- [ ] **P4 Radio** — Tourner à 1337 Hz → BOX-3 reçoit `P4_SOLVED` +- [ ] **P5 Morse** — Taper ZACUS en morse → BOX-3 reçoit `P5_SOLVED` +- [ ] **P6 Symboles** — Placer 12 tuiles dans l'ordre correct → BOX-3 reçoit `P6_SOLVED` +- [ ] **P7 Coffre** — Entrer le code 8 chiffres assemblé → servo tourne à 90° → coffre ouvert +- [ ] **NPC Audio** — Le Professeur Zacus parle à chaque étape clé (tester avec `./tools/dev/zacus.sh voice-bridge test`) +- [ ] **Scoring** — Fin de partie → BOX-3 affiche score et diplôme PDF généré + +### Checklist esthétique et robustesse + +- [ ] Aucun câble visible ou dangereux (tout cheminé + fermé) +- [ ] Boîtiers vissés ou clipsés — aucune pièce qui s'ouvre involontairement +- [ ] Toutes les LEDs et écrans visibles depuis la position joueur normale +- [ ] Texte imprimé lisible (taille min 14 pt à distance de bras) +- [ ] Logo et habillage thématique ("Laboratoire du Pr. Zacus") appliqués +- [ ] Kit de secours validé : code maître P7 fonctionne, fiche lamifiée présente diff --git a/hardware/puzzles/enclosures.md b/hardware/puzzles/enclosures.md new file mode 100644 index 0000000..bba8b7a --- /dev/null +++ b/hardware/puzzles/enclosures.md @@ -0,0 +1,200 @@ +# 3D Enclosure Specifications — Zacus V3 + +All enclosures designed for FDM 3D printing unless noted. STL source files live in `hardware/enclosures/`. +OpenSCAD source: `hardware/enclosures/*.scad` (see plan Task 2.3 for parametric source). + +--- + +## P1 — Boîte Sonore (Séquence Sonore) + +**Dimensions:** 150 × 100 × 80 mm (L × W × H) +**Material:** PETG (preferred for durability) or PLA+ +**Color:** Black — for stage/theatrical look; buttons are colored by their caps + +### Openings & Cutouts + +| Feature | Location | Size | Notes | +|------------------|------------------------|---------------|----------------------------------------------| +| Speaker grille | Front face, upper half | 40 × 40 mm | Honeycomb pattern — 2mm hex holes, 1mm walls | +| Button hole RED | Top face, x=20 | ⌀30 mm | Sanwa OBSF-30 standard mounting | +| Button hole BLUE | Top face, x=60 | ⌀30 mm | Sanwa OBSF-30 | +| Button hole YEL | Top face, x=90 | ⌀30 mm | Sanwa OBSF-30 | +| Button hole GRN | Top face, x=130 | ⌀30 mm | Sanwa OBSF-30 | +| USB-C slot | Right side, bottom | 12 × 6 mm | Power input access | +| LED strip window | Front face, lower strip| 100 × 8 mm | Diffused acrylic insert (frosted tape ok) | +| Lid vent grid | Lid top | 4×1 mm slots | 10mm grid — passive thermal | +| PCB mount studs | Interior floor | M3 standoffs | 4× M3×5mm brass inserts | + +### Assembly +- Box body + removable lid (M3 screws, 4 corners) +- Speaker mounted on interior face panel with 2× M2.5 self-tappers +- MAX98357A module hot-glued behind speaker grille +- ESP32-S3 DevKit velcro-mounted to floor (removable for flashing) +- Buttons snap into top holes with Sanwa mounting nut + +### Print Settings +| Parameter | Value | +|-----------------|------------------------| +| Layer height | 0.2 mm | +| Infill | 25% gyroid | +| Walls | 3 perimeters | +| Top/bottom | 4 layers | +| Supports | Yes — lid vent overhang only | +| Print time est. | ~5h (box) + 1h (lid) | +| Filament | ~180g PETG | + +--- + +## P4 — Poste Radio Rétro (Fréquence Radio) + +**Dimensions:** 200 × 150 × 120 mm (L × W × H) +**Material:** PLA (retro aesthetic — easier to sand and paint) +**Color:** Print in dark brown or beige PLA; hand-paint or vinyl wrap for retro finish + +### Openings & Cutouts + +| Feature | Location | Size | Notes | +|---------------------|------------------------------|-----------------|-----------------------------------------------| +| Rotary encoder hole | Front face, right side | ⌀14 mm | KY-040 shaft + nut clearance | +| Encoder knob recess | Front face, around encoder | ⌀35 mm, 5mm deep| For large Bakelite-style knob | +| OLED window | Front face, left-center | 35 × 18 mm | Acrylic window insert — glued from inside | +| Speaker grille | Front face, upper center | ⌀80 mm (circular)| 19-hole hex pattern — retro radio aesthetic | +| USB-C slot | Bottom face | 12 × 6 mm | Recessed — hidden when on table | +| Battery access door | Back face | 60 × 30 mm | Sliding door for 18650 replacement | +| Antenna cutout | Top face, right side | 8 × 60 mm slot | Decorative antenna rod (wooden dowel) | +| Corner radius | All edges | R15 mm | Rounded hull — retro form factor | +| Vent holes | Back face, bottom | 4 × 8mm slots | Battery charging ventilation | + +### Assembly +- Body printed in 2 halves (top + bottom) — split at midline +- OLED glued behind acrylic window from inside +- KY-040 mounted through front panel, locked with hex nut +- Speaker front-mounted, MAX98357A behind grille +- 18650 holder slides into battery compartment from back door +- ESP32 DevKit mounted on standoffs — accessible from bottom removal + +### Print Settings +| Parameter | Value | +|-----------------|--------------------------| +| Layer height | 0.2 mm (0.15 for details)| +| Infill | 20% gyroid | +| Walls | 4 perimeters | +| Top/bottom | 5 layers | +| Supports | Yes — rounded hull corners, antenna slot | +| Print time est. | ~9h (body halves) + 1h (door) | +| Filament | ~320g PLA | +| Post-process | Sand 220 grit → prime → paint (retro brown/beige) | + +--- + +## P5 — Télégraphe (Code Morse) + +**Dimensions:** 180 × 80 × 60 mm (L × W × H) — for base only; brass key mounted on top +**Material:** PETG dark brown or woodPLA for telegraph aesthetic +**Color:** Dark brown — telegraph base aesthetic + +### Openings & Cutouts + +| Feature | Location | Size | Notes | +|---------------------|-----------------------|---------------|-------------------------------------------------| +| Key mounting posts | Top face, right half | 2× M3 holes | Brass telegraph key bolted down | +| Buzzer grille | Front face | 20 × 20 mm | Active buzzer — 2mm hex holes | +| LED red (key press) | Front face, left | ⌀5 mm | 5mm LED holder press-fit | +| LED green (valid) | Front face, right | ⌀5 mm | 5mm LED holder press-fit | +| WS2812B window | Top face, left area | 15 × 8 mm | Diffused window for light-mode morse | +| USB-C slot | Left side, bottom | 12 × 6 mm | Power input | +| ESP32 access slot | Bottom face | 80 × 20 mm | Removable bottom panel for programming | +| Cable routing slot | Interior | 6 mm channels | Wires from key to PCB | + +### Assembly +- Single-piece base with snap-on bottom panel +- Brass telegraph key (Aliexpress morse key) mounted with M3 bolts +- Key spring contact wired to GPIO4 with 2-pin JST connector +- Buzzer hot-glued behind front grille +- 5mm LED holders press-fit into front holes +- WS2812B behind frosted diffuser window on top + +### Print Settings +| Parameter | Value | +|-----------------|-------------------------| +| Layer height | 0.2 mm | +| Infill | 30% gyroid (heavier base)| +| Walls | 4 perimeters | +| Top/bottom | 4 layers | +| Supports | Minimal — USB slot only | +| Print time est. | ~4h | +| Filament | ~130g PETG/woodPLA | + +--- + +## P7 — Coffre Final (Coffre avec verrou électronique) + +**Dimensions:** 250 × 200 × 150 mm (L × W × H) — wooden box, NOT 3D printed +**Material:** Pine wood box (hardware store) — ESP32 electronics in internal tray (3D printed) + +### Wooden Box Modifications + +| Feature | Location | Size | Notes | +|---------------------|-----------------------|-----------------|------------------------------------------------| +| Keypad cutout | Lid front face | 75 × 60 mm | 4×3 membrane keypad recess | +| OLED window | Lid front face | 35 × 18 mm | Above keypad — acrylic window | +| RGB LED hole | Lid front face | ⌀8 mm | WS2812B status indicator | +| Servo mount | Inside lid | 30 × 20 mm slot | SG90 servo + latch mechanism | +| Latch slot | Front face (lid edge) | 15 × 8 mm | Steel latch bar — closes box electromagnetically| +| USB-C slot | Back face | 12 × 6 mm | Hidden — covered by rubber flap | +| Electronics tray | Inside base | 220 × 170 × 40 | 3D printed PETG tray — holds ESP32, PCB, servo | +| Cable grommet | Tray → lid | ⌀12 mm | Spiral cable wrap — lid opening clearance | + +### 3D Printed Electronics Tray (Internal) + +**Dimensions:** 220 × 170 × 40 mm +**Material:** PETG +**Features:** +- ESP32 DevKit mount (4× M3 standoffs) +- Servo horn slot and cable routing +- Buzzer pocket (press-fit) +- PCB mount (2× M3) +- Lid hinge clearance notch + +### Print Settings (Electronics Tray) +| Parameter | Value | +|-----------------|--------------------| +| Layer height | 0.25 mm | +| Infill | 20% grid | +| Walls | 3 perimeters | +| Top/bottom | 3 layers | +| Supports | None | +| Print time est. | ~3h | +| Filament | ~90g PETG | + +### Servo Latch Mechanism +- SG90 at 0° = latch bar extended (locked) +- SG90 at 90° = latch bar retracted (open) +- Latch bar: 3D printed PETG bar 60×8×5mm +- Spring-loaded return (2mm steel wire spring) +- PWM: 50Hz, duty 1ms=closed, 2ms=open + +--- + +## P2, P3, P6 — No Custom Enclosure Needed + +| Puzzle | Housing | +|--------|-----------------------------------------------------| +| P2 | Magnetic folding board (self-contained, no enclosure)| +| P3 | Cards only — no enclosure | +| P6 | Wooden tablet is the housing (laser cut 3mm plywood)| + +--- + +## Print Summary + +| Puzzle | Volume | Material | Print Time | Filament | Est. Cost | +|--------|----------|----------|------------|----------|-----------| +| P1 | 150×100×80 | PETG | ~6h | ~180g | ~5€ | +| P4 | 200×150×120| PLA | ~10h | ~320g | ~9€ | +| P5 | 180×80×60 | PETG | ~4h | ~130g | ~4€ | +| P7 tray| 220×170×40 | PETG | ~3h | ~90g | ~3€ | +| **Total** | | | **~23h** | **~720g**| **~21€** | + +Filament cost based on ~0.03€/g PETG bulk spool. +Wooden box for P7: ~12€ from hardware store. diff --git a/hardware/puzzles/gpio_mapping.md b/hardware/puzzles/gpio_mapping.md new file mode 100644 index 0000000..e36afb8 --- /dev/null +++ b/hardware/puzzles/gpio_mapping.md @@ -0,0 +1,347 @@ +# GPIO Mapping — Zacus V3 Puzzle Boards + +All puzzle nodes run ESP-NOW slave firmware. Pinout must match `ESP32_ZACUS/puzzles/common/espnow_slave.h`. +Master MAC address is hard-coded per production run and set via `espnow_slave_init()` at boot. + +--- + +## P1 — Séquence Sonore (ESP32-S3-DevKitC-1-N16R8) + +### GPIO Table + +| GPIO | Function | Direction | Notes | +|------|-------------------|-----------|------------------------------------------------| +| 4 | I2S BCLK | OUT | MAX98357A bit clock | +| 5 | I2S LRCLK (WS) | OUT | MAX98357A left/right clock | +| 6 | I2S DIN | OUT | MAX98357A data in | +| 7 | MAX98357A SD_MODE | OUT | HIGH = stereo / LOW = shutdown | +| 8 | WS2812B DIN | OUT | 4-LED strip via SN74HCT125 level shifter | +| 15 | Button RED | IN_PULLUP | Active LOW — 10kΩ external pull-up | +| 16 | Button BLUE | IN_PULLUP | Active LOW — 10kΩ external pull-up | +| 17 | Button YELLOW | IN_PULLUP | Active LOW — 10kΩ external pull-up | +| 18 | Button GREEN | IN_PULLUP | Active LOW — 10kΩ external pull-up | + +### Power Rails +- 5V from USB-C → AMS1117-3.3 → ESP32 3V3 rail +- 5V direct → MAX98357A VDD and WS2812B VCC + +### Wiring Diagram + +``` +USB-C 5V ─────┬──────────────────────────── MAX98357A VDD + │ ┌── WS2812B VCC + └── AMS1117-3.3 ── 3V3 ──┤── ESP32 3V3 + └── SN74HCT125 VCC + +ESP32-S3 + GPIO4 ──────────────────────────────── MAX98357A BCLK + GPIO5 ──────────────────────────────── MAX98357A LRC + GPIO6 ──────────────────────────────── MAX98357A DIN + GPIO7 ──── 3V3 (tied HIGH) ─────────── MAX98357A SD_MODE + + GPIO8 ──── SN74HCT125 A1 ── Y1 ─────── WS2812B DIN (4 LEDs) + + GPIO15 ──┬── 10kΩ ── 3V3 + └────────────────────────────── Arcade Button RED (NO → GND) + + GPIO16 ──┬── 10kΩ ── 3V3 + └────────────────────────────── Arcade Button BLUE (NO → GND) + + GPIO17 ──┬── 10kΩ ── 3V3 + └────────────────────────────── Arcade Button YELLOW (NO → GND) + + GPIO18 ──┬── 10kΩ ── 3V3 + └────────────────────────────── Arcade Button GREEN (NO → GND) + + MAX98357A OUTP/OUTN ─────────────────── Speaker 8Ω 2W +``` + +--- + +## P2 — Circuit LED (ESP32-DevKitC-32E) + +### GPIO Table + +| GPIO | Function | Direction | Notes | +|------|----------------------|-----------|----------------------------------------------| +| 4 | Reed Switch 1 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 5 | Reed Switch 2 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 6 | Reed Switch 3 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 7 | Reed Switch 4 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 8 | Reed Switch 5 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 9 | Reed Switch 6 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 10 | Reed Switch 7 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 11 | Reed Switch 8 | IN_PULLUP | Magnetic sensor — 10kΩ external pull-up | +| 12 | WS2812B DIN Seg A | OUT | LED segment 1–2 via 74HCT125 | +| 13 | WS2812B DIN Seg B | OUT | LED segment 3–4 via 74HCT125 | +| 14 | WS2812B DIN Seg C | OUT | LED segment 5–6 via 74HCT125 | +| 15 | WS2812B DIN Seg D | OUT | LED segment 7–8 via 74HCT125 | +| 16 | WS2812B DIN Seg E | OUT | LED segment 9–10 via 74HCT125 | +| 17 | WS2812B DIN Seg F | OUT | LED segment 11–12 via 74HCT125 | +| 18 | WS2812B DIN Seg G | OUT | LED segment 13–14 via 74HCT125 | +| 19 | WS2812B DIN Seg H | OUT | LED segment 15–16 via 74HCT125 | +| 21 | Buzzer | OUT | NPN 2N2222 base via 1kΩ → active buzzer 5V | +| 22 | Circuit Valid LED | OUT | Green LED via 470Ω series resistor | + +### Wiring Diagram + +``` +USB-C 5V ────┬───────────────────────────── WS2812B VCC (each segment) + │ ┌── 1000µF bulk cap (5V decoupling) + └── AMS1117-3.3 ── 3V3 ──── ESP32 3V3 + +ESP32 + GPIO4..11 ─── (each) ──┬── 10kΩ ── 3V3 + └───────────────── Reed Switch (NO → GND) + + GPIO12..19 ─── SN74HCT125 Ax ── Yx ────── WS2812B DINx (2 LEDs each) + + GPIO21 ──── 1kΩ ──── Q1(2N2222) base + Q1 collector ─ Buzzer+ (5V) + Q1 emitter ─ GND + Buzzer- ─ GND + + GPIO22 ──── 470Ω ──── LED Green+ ──── GND + +Magnetic board (folding 60x40cm): + Reed switches are embedded in the board surface + Magnetic component pegs activate the reed switches when placed + Board connector: 10-pin ribbon → ESP32 GPIO4..11 + 2x GND +``` + +--- + +## P3 — QR Treasure (No dedicated electronics) + +P3 uses the BOX-3 camera directly for QR scanning. No separate ESP32 board. + +| Component | Notes | +|-----------|-------| +| 6× A5 laminated QR cards | Printed + laminated — QR content set at scenario compile time | +| BOX-3 camera | ESP32-S3-BOX-3 integrated camera — handles scanning | + +QR card placement order encoded in `game/scenarios/zacus_v3_complete.yaml` under `P3_QR.correct_order`. + +--- + +## P4 — Fréquence Radio (ESP32-DevKitC-32E) + +### GPIO Table + +| GPIO | Function | Direction | Notes | +|------|--------------------|-----------|--------------------------------------------------| +| 4 | I2S BCLK | OUT | MAX98357A — radio static synthesis | +| 5 | I2S LRCLK (WS) | OUT | MAX98357A | +| 6 | I2S DIN | OUT | MAX98357A data | +| 12 | Rotary Encoder DT | IN_PULLUP | KY-040 — direction detect | +| 13 | Rotary Encoder SW | IN_PULLUP | KY-040 push button — confirm selection | +| 14 | Rotary Encoder CLK | IN_PULLUP | KY-040 — clock signal | +| 21 | I2C SDA (OLED) | BIDIR | SSD1306 128x64 — 4.7kΩ pull-up to 3V3 | +| 22 | I2C SCL (OLED) | OUT | SSD1306 — 4.7kΩ pull-up to 3V3 | +| 25 | DAC Output | OUT | RC low-pass → analog indicator LED (warm/cold) | + +### Power Rails +- 2× 18650 (2S) → TP4056 charger → MT3608 boost 5V → AMS1117-3.3 → 3V3 +- Alternatively: USB-C 5V input for bench/testing + +### Wiring Diagram + +``` +2x 18650 (2S = 7.4V nom) ── TP4056 ── MT3608 boost ── 5V rail + 5V rail ─────┬───────────────────────── MAX98357A VDD + └── AMS1117-3.3 ── 3V3 ── ESP32 3V3 + +ESP32 + GPIO4 ──────────────────────────── MAX98357A BCLK + GPIO5 ──────────────────────────── MAX98357A LRC + GPIO6 ──────────────────────────── MAX98357A DIN + MAX98357A OUTP/OUTN ─────────────── Speaker 8Ω 2W (in retro grille) + + GPIO14 ──┬── 10kΩ ── 3V3 + └───────────────────────── KY-040 CLK + GPIO12 ──┬── 10kΩ ── 3V3 + └───────────────────────── KY-040 DT + GPIO13 ──┬── 10kΩ ── 3V3 + └───────────────────────── KY-040 SW (GND when pressed) + + GPIO21 ──┬── 4.7kΩ ── 3V3 + └───────────────────────── SSD1306 SDA + GPIO22 ──┬── 4.7kΩ ── 3V3 + └───────────────────────── SSD1306 SCL + + GPIO25 ── 10kΩ ──┬── 100nF ── GND (RC low-pass filter) + └────────────────── LED warm/cold indicator anode + LED cathode ── 470Ω ── GND +``` + +--- + +## P5 — Code Morse (ESP32-DevKitC-32E) + +### GPIO Table + +| GPIO | Function | Direction | Notes | +|------|----------------------|-----------|------------------------------------------------| +| 4 | Telegraph Key | IN_PULLUP | Brass key NO contact — 10kΩ external pull-up | +| 5 | Buzzer | OUT | NPN 2N2222 base via 1kΩ — active buzzer 5V | +| 6 | LED Red (key press) | OUT | Key pressed indicator — 470Ω series | +| 7 | LED Green (validated)| OUT | Morse message valid — 470Ω series | +| 8 | WS2812B (light mode) | OUT | 1 LED — NON_TECH visual morse pulses | + +### Wiring Diagram + +``` +USB-C 5V ────── AMS1117-3.3 ────── 3V3 ── ESP32 3V3 + +ESP32 + GPIO4 ──┬── 10kΩ ── 3V3 + └──────────────────────── Telegraph Key+ (brass contact) + Telegraph Key- ───── GND + + GPIO5 ──── 1kΩ ──── Q1(2N2222) base + Q1 collector ── Buzzer+ ── 5V + Q1 emitter ── GND + Buzzer- ── GND + + GPIO6 ──── 470Ω ──── LED Red anode ──── GND + + GPIO7 ──── 470Ω ──── LED Green anode ── GND + + GPIO8 ──────────────────────────── WS2812B DIN (1 LED, 5V) + [3V3 → 5V: use SN74HCT125 or direct (WS2812B tolerates 3V3 DIN)] +``` + +--- + +## P6 — Symboles Alchimiques NFC (ESP32-DevKitC-32E) + +### GPIO Table + +| GPIO | Function | Direction | Notes | +|------|-------------------|-----------|-----------------------------------------------| +| 4 | MFRC522 RST | OUT | NFC module reset | +| 5 | MFRC522 SDA (SS) | OUT | SPI chip select | +| 18 | MFRC522 SCK | OUT | SPI clock | +| 19 | MFRC522 MISO | IN | SPI data from MFRC522 | +| 23 | MFRC522 MOSI | OUT | SPI data to MFRC522 | +| 25 | Buzzer | OUT | NPN 2N2222 base via 1kΩ | +| 26 | LED Green (valid) | OUT | Configuration valid — 470Ω | +| 27 | LED Red (wrong) | OUT | Wrong placement — 470Ω | + +### Wiring Diagram + +``` +USB-C 5V ───── AMS1117-3.3 ───── 3V3 ── ESP32 3V3 + +ESP32 + GPIO5 (SDA/SS) ────────────── MFRC522 SDA + GPIO18 (SCK) ────────────── MFRC522 SCK + GPIO19 (MISO) ────────────── MFRC522 MISO + GPIO23 (MOSI) ────────────── MFRC522 MOSI + GPIO4 (RST) ────────────── MFRC522 RST + 3V3 ────────────── MFRC522 3.3V + GND ────────────── MFRC522 GND + [MFRC522 IRQ pin: not connected — polling mode] + + GPIO25 ── 1kΩ ── Q1(2N2222) base + Q1 collector ── Buzzer+ ── 5V + Q1 emitter ── GND + + GPIO26 ── 470Ω ── LED Green ── GND + GPIO27 ── 470Ω ── LED Red ── GND + +Wooden tablet (300x200mm, 3mm plywood): + 12 recessed slots, each slot has an NTAG213 sticker wired via flat cable + to a multiplexed antenna array — OR single MFRC522 scans one tag at a time + (player places one piece → holds it → scans → places next) + Correct order: [7,2,11,4,9,1,8,3,12,6,10,5] (from zacus_v3_complete.yaml) +``` + +--- + +## P7 — Coffre Final (ESP32-DevKitC-32E) + +### GPIO Table + +| GPIO | Function | Direction | Notes | +|------|----------------------|-----------|------------------------------------------------| +| 4 | Keypad Row 1 | OUT | Matrix scan — driven LOW one at a time | +| 5 | WS2812B RGB LED | OUT | Status LED — idle/entering/error/open | +| 6 | Keypad Row 2 | OUT | Matrix scan | +| 7 | Keypad Row 3 | OUT | Matrix scan | +| 8 | Keypad Row 4 | OUT | Matrix scan | +| 12 | Keypad Col 1 | IN_PULLUP | 10kΩ pull-up — reads HIGH unless row driven | +| 13 | Keypad Col 2 | IN_PULLUP | 10kΩ pull-up | +| 14 | Keypad Col 3 | IN_PULLUP | 10kΩ pull-up | +| 18 | Buzzer | OUT | NPN 2N2222 base via 1kΩ | +| 21 | I2C SDA (OLED) | BIDIR | SSD1306 128x64 — 4.7kΩ pull-up | +| 22 | I2C SCL (OLED) | OUT | SSD1306 — 4.7kΩ pull-up | +| 25 | SG90 Servo PWM | OUT | 50Hz PWM — 1ms=closed, 2ms=open | + +### Wiring Diagram + +``` +USB-C 5V ────┬─────────────────────────── SG90 Servo VCC (5V direct) + │ ┌── WS2812B VCC + └── AMS1117-3.3 ── 3V3 ── ESP32 3V3 + +ESP32 + GPIO4,6,7,8 ──────────────────────── Keypad Rows R1..R4 + (each scanned LOW sequentially, others HIGH-Z) + + GPIO12 ──┬── 10kΩ ── 3V3 ────────── Keypad Col C1 + GPIO13 ──┬── 10kΩ ── 3V3 ────────── Keypad Col C2 + GPIO14 ──┬── 10kΩ ── 3V3 ────────── Keypad Col C3 + + GPIO21 ──┬── 4.7kΩ ── 3V3 ─────── SSD1306 SDA + GPIO22 ──┬── 4.7kΩ ── 3V3 ─────── SSD1306 SCL + + GPIO25 ─────────────────────────── SG90 Signal (orange wire) + SG90 Red ── 5V + SG90 Brown ── GND + + GPIO5 ─────────────────────────── WS2812B DIN (1 LED) + + GPIO18 ── 1kΩ ── Q1(2N2222) base + Q1 collector ── Buzzer+ ── 5V + Q1 emitter ── GND + +Keypad 4x3 matrix layout: + [1][2][3] + [4][5][6] + [7][8][9] + [*][0][#] + Rows: R1=top, R4=bottom + Cols: C1=left, C3=right +``` + +--- + +## Hub — BOX-3 + RTC_PHONE + +### BOX-3 (ESP32-S3-BOX-3) + +All BOX-3 GPIO is managed by Espressif BSP. Key internal connections: + +| Resource | Notes | +|------------------|----------------------------------------------------| +| ESP-NOW Master | Coordinates all P1..P7 + RTC_PHONE via 2.4GHz | +| Camera (GC0308) | QR code scanning for P3 | +| Display (ILI9342)| Game master dashboard, scoring, puzzle status | +| I2S Speaker | ES8311 audio codec — NPC voice output | +| Mute button | GPIO0 — NPC voice mute toggle | +| USB-C | Programming + 5V power from Anker powerbank | + +### RTC_PHONE (Custom Board) + +| GPIO/Pin | Function | Notes | +|----------|--------------------|------------------------------------------| +| GPIO4 | I2S BCLK → SLIC | Si3220 voice codec clock | +| GPIO5 | I2S LRCLK → SLIC | Si3220 left/right clock | +| GPIO6 | I2S DIN → SLIC | Audio data to SLIC (NPC voice) | +| GPIO7 | I2S DOUT ← SLIC | Audio from handset (player speech) | +| GPIO21 | I2C SDA → Si3220 | SLIC configuration | +| GPIO22 | I2C SCL → Si3220 | SLIC clock | +| GPIO18 | Hook switch detect | HIGH = handset lifted (interrupt) | +| RJ11 | POTS line out | Standard telephone handset connector | +| USB-C | 5V power | From Anker powerbank | diff --git a/hardware/puzzles/maintenance_guide.md b/hardware/puzzles/maintenance_guide.md new file mode 100644 index 0000000..2c0b040 --- /dev/null +++ b/hardware/puzzles/maintenance_guide.md @@ -0,0 +1,216 @@ +# Guide de Maintenance — Kit V3 + +> Dernière mise à jour : 2026-04-03 +> Fréquence : après chaque session (vérifications rapides) + annuelle (maintenance complète) + +--- + +## Dépannage des Problèmes Courants + +### Mesh ESP-NOW — Nœuds manquants + +**Symptôme :** BOX-3 affiche `MESH: X/6 nodes connected` avec X < 6. + +| Cause probable | Diagnostic | Solution | +|----------------|-----------|---------| +| ESP32 non alimenté | LED d'alimentation éteinte sur le puzzle | Vérifier câble USB-C + batterie | +| ESP32 bloqué au boot | LED clignote rapidement sans s'arrêter | Appuyer bouton Reset (EN) sur le DevKit | +| Hors de portée ESP-NOW (~200 m max) | Puzzle à plus de 20 m de BOX-3 | Rapprocher le puzzle ou repositionner | +| Firmware corrompu | Log série : `Guru Meditation Error` | Re-flasher le firmware (voir section Update) | +| Conflit MAC address | Log série : `ESPNOW: peer already exists` | Effacer NVS et redémarrer : `esptool.py erase_flash` | + +**Commande de diagnostic rapide :** +```bash +# Afficher l'état du mesh depuis BOX-3 via serial +pio device monitor -e box3 --baud 115200 +# Puis dans le menu tactile BOX-3 : Debug → ESP-NOW Status +``` + +--- + +### P6 Tablette Symboles NFC — Tuile non détectée + +**Symptôme :** Une tuile NFC posée sur son emplacement n'est pas reconnue. + +| Cause | Diagnostic | Solution | +|-------|-----------|---------| +| Autocollant NFC endommagé | Tester la tuile avec un smartphone NFC | Remplacer l'autocollant NTAG213 (stock de rechange) | +| Distance trop grande | Positionner la tuile directement sur l'encoche | Vérifier que le PN532 est bien collé sous la surface | +| PN532 déconnecté | Log série : `PN532: INIT FAILED` | Vérifier câbles I2C SDA/SCL, re-souder si nécessaire | +| UID non enregistré en NVS | Log série : `UNKNOWN UID: XXXXXXXX` | Re-programmer le NVS avec les UIDs corrects | + +**Programmer un autocollant NTAG213 de remplacement :** +```bash +# Avec un smartphone Android + app NFC Tools +# OU avec le reader PN532 + script Python : +python3 tools/dev/nfc_program.py --uid-slot 7 --new-uid 04A1B2C3D4E500 +``` + +--- + +### P5 Télégraphe — Morse non détecté + +**Symptôme :** Appuyer sur le télégraphe ne produit aucun effet. + +| Cause | Solution | +|-------|---------| +| Contact oxydé (laiton) | Nettoyer les contacts avec papier abrasif 400 grain | +| Ressort de rappel trop faible | Remplacer le ressort (diamètre 5 mm, force 0,3 N) | +| Seuil point/tiret mal calibré | Ajuster `MORSE_DOT_MAX_MS` dans `firmware/p5_morse/config.h` | +| GPIO en pull-up cassé | Mesurer tension GPIO au repos (doit être 3,3 V) | + +--- + +### P7 Coffre — Servo bloqué ou ne tourne pas + +**Symptôme :** Code correct entré mais le servo ne bouge pas (coffre reste fermé). + +| Cause | Solution | +|-------|---------| +| Servo desserré mécaniquement | Vérifier vis de fixation bras de servo, re-coller si nécessaire | +| Surcharge mécanique | Vérifier que le verrou n'est pas physiquement bloqué (dégager manuellement) | +| Signal PWM incorrect | Mesurer GPIO 23 avec oscilloscope : 50 Hz, pulse 1-2 ms | +| Servo défaillant | Remplacer le SG90 (prévoir 2 de rechange dans la valise) | + +**Ouverture d'urgence (sans électronique) :** +Utiliser la clé Allen M2 fournie dans la poche valise 3 pour retirer le bras de servo manuellement. + +--- + +### BOX-3 — Écran noir au démarrage + +**Diagnostic :** +```bash +# Vérifier le firmware via serial +idf.py -p /dev/cu.usbserial-XXXX monitor +# Log attendu : [BOOT] BOX3 v3.1 — Display init... +``` + +| Cause | Solution | +|-------|---------| +| Firmware absent | Re-flasher : `idf.py -p /dev/cu.usbserial-XXXX flash` | +| Batterie trop faible | Connecter au chargeur 5V/2A minimum avant démarrage | +| Écran LCD déconnecté | Ouvrir le BOX-3 (4 vis Torx T8), vérifier nappe LCD | + +--- + +### Voix NPC silencieuse + +**Diagnostic par ordre de priorité :** + +1. **Vérifier la connectivité WiFi** — BOX-3 : `Settings → WiFi Status` +2. **Tester Piper TTS directement :** + ```bash + curl -X POST http://192.168.0.120:8001/api/tts \ + -H "Content-Type: application/json" \ + -d '{"text": "Test professeur Zacus", "voice": "tom-medium"}' \ + --output /tmp/test.wav && aplay /tmp/test.wav + ``` +3. **Mode offline** — Vérifier que la SD card est présente dans BOX-3 et contient le pool MP3 : + ```bash + ls /sdcard/hotline_tts/ | wc -l # attendu : 50+ fichiers + ``` +4. **Régénérer le pool MP3 si absent :** + ```bash + python3 tools/tts/generate_npc_pool.py --dry-run # vérifier sans générer + python3 tools/tts/generate_npc_pool.py # générer + ``` + +--- + +## Procédure de Mise à Jour Firmware + +### Option 1 : Mise à jour OTA (Over-The-Air) — recommandée + +```bash +# Prérequis : BOX-3 et tous les puzzles sur le même réseau WiFi + +# Build le firmware mis à jour +cd ESP32_ZACUS +pio run -e p1_son + +# Upload OTA (l'ESP32 doit être en mode OTA_READY) +# Depuis BOX-3 : Maintenance → OTA Update → Enable OTA on all nodes +pio run -e p1_son --target upload --upload-protocol espota \ + --upload-port 192.168.1.XXX # IP de P1 affichée sur BOX-3 +``` + +### Option 2 : Mise à jour USB-C (physique) + +```bash +# Connecter le puzzle en USB-C au poste de développement +pio run -e p1_son --target upload --upload-port /dev/cu.usbserial-XXXX + +# BOX-3 (ESP32-S3) +idf.py -p /dev/cu.usbserial-XXXX flash +``` + +### Vérification post-mise à jour + +```bash +# Vérifier la version firmware +pio device monitor -e p1_son --baud 115200 +# Attendu : [BOOT] P1_SON v3.X — ... +``` + +--- + +## Guide de Remplacement des Composants + +### Composants consommables (à stocker en réserve) + +| Composant | Quantité recommandée | Fournisseur | Prix unitaire | +|-----------|---------------------|-------------|--------------| +| NTAG213 autocollant NFC | 5 | AliExpress / Reichelt | ~0,30 € | +| Servo SG90 | 2 | AliExpress | ~2 € | +| ESP32 DevKit v1 | 1 | AliExpress / Mouser | ~5 € | +| 18650 Li-Ion (P4) | 1 | NKON / Reichelt | ~4 € | +| Bouton arcade 30 mm | 4 (jeu complet P1) | Aliexpress | ~1 € / pièce | +| Câble USB-C 1 m | 3 | n'importe | ~3 € | +| Ressort télégraphe (P5) | 3 | Bricomarché / quincaillerie | ~0,50 € | + +### Remplacement d'un ESP32 DevKit + +1. Déconnecter l'alimentation et tous les câbles. +2. Dessouder ou déconnecter le connecteur JST si présent. +3. Flasher le nouveau DevKit **avant** de l'installer (voir Étape 3 de l'assemblage). +4. Re-programmer le NVS avec les UIDs et credentials du puzzle remplacé. +5. Reconnecter et tester le mesh ESP-NOW. + +### Remplacement d'une tuile NFC (P6) + +1. Retirer l'autocollant NTAG213 de la tuile symbole (avec lame de rasoir, chauffer légèrement). +2. Programmer le nouvel autocollant avec le même UID (script `tools/dev/nfc_program.py`). +3. Coller le nouvel autocollant et vérifier la lecture (log : `NFC READ: UID match slot X`). + +--- + +## Calendrier de Maintenance Annuelle + +### Avant la saison (1× par an, avant les premières sessions) + +| Tâche | Durée | Priorité | +|-------|-------|---------| +| Test complet de tous les puzzles (protocole étape 5) | 30 min | P0 | +| Mise à jour firmware tous les ESP32 et BOX-3 | 45 min | P0 | +| Vérification et remplacement des batteries dégradées | 15 min | P0 | +| Nettoyage contacts électroniques (spray contact) | 15 min | P1 | +| Vérification intégrité boîtiers (fissures, vis desserrées) | 10 min | P1 | +| Régénération pool MP3 NPC (nouvelles phrases si besoin) | 20 min | P1 | +| Vérification QR codes A5 (décoloration, plastification intacte) | 5 min | P2 | +| Test servo P7 (1000 cycles ouvrir/fermer) | 10 min | P2 | +| Mise à jour documentation si modifications hardware | — | P2 | + +### Après 50 sessions (compteur dans BOX-3) + +- Remplacer les 4 boutons arcade de P1 (usure mécanique prévisible). +- Remplacer le servo SG90 de P7 (durée de vie : ~3000-5000 cycles). +- Vérifier les contacts du télégraphe P5 (re-polir si nécessaire). +- Inspecter le câblage interne de P6 (vibrations de transport). + +### Log de maintenance + +Tenir à jour le fichier `hardware/maintenance_log.csv` avec : +``` +date,session_count,technician,actions_performed,components_replaced,notes +``` diff --git a/hardware/puzzles/power_management.md b/hardware/puzzles/power_management.md new file mode 100644 index 0000000..297ce9a --- /dev/null +++ b/hardware/puzzles/power_management.md @@ -0,0 +1,115 @@ +# Gestion de l'Alimentation — Kit V3 + +> Dernière mise à jour : 2026-04-03 +> Concerne : 3 valises, 7 puzzles ESP32, RTC_PHONE, BOX-3, routeur GL.iNet + +--- + +## Assignation des Batteries + +| Batterie | Capacité | Appareils alimentés | Puissance totale | Autonomie estimée | +|----------|----------|---------------------|-----------------|-------------------| +| **Anker #1** (valise Hub) | 20 000 mAh / 5V | RTC_PHONE (0,5 W) + BOX-3 (2 W) + Routeur GL.iNet (3 W) | 5,5 W | ~3,5 h | +| **Anker #2** (valise Hub, réserve) | 20 000 mAh / 5V | Chargeur USB multi-port puzzles (en session) ou secours Hub | — | — | +| **Pack Puzzle A** | 10 000 mAh / 5V | P1 Boîte Sonore (1 W) + P2 Breadboard (2 W) + P4 Radio (interne 18650) | 3 W | ~8 h | +| **Pack Puzzle B** | 10 000 mAh / 5V | P5 Télégraphe (0,5 W) + P6 Tablette Symboles (0,5 W) + P7 Coffre Final (1 W) | 2 W | ~15 h | + +> **Note :** P3 QR Treasure utilise la caméra intégrée de BOX-3 (déjà compté dans Anker #1). +> P4 intègre un 18650 rechargeable en interne — chargé via USB-C indépendamment. + +--- + +## Autonomie par Appareil + +| Appareil | Puissance moy. | Batterie | Autonomie calculée | Marge pour 2 sessions | +|----------|---------------|----------|--------------------|-----------------------| +| RTC_PHONE | 0,5 W | Anker #1 | >10 h | Oui | +| BOX-3 (ESP32-S3-BOX-3) | 2 W | Anker #1 | ~5 h | Non (recharge entre sessions) | +| Routeur GL.iNet GL-MT3000 | 3 W | Anker #1 | ~3,5 h | Non (1 session) | +| P1 Boîte Sonore | 1 W | Pack A | ~12 h | Oui | +| P2 Breadboard Magnétique | 2 W (pics) | Pack A | ~6 h | Oui | +| P4 Poste Radio | 18650 interne | — | ~4 h | Vérifier avant chaque session | +| P5 Télégraphe | 0,5 W | Pack B | >20 h | Oui | +| P6 Tablette Symboles | 0,5 W | Pack B | >20 h | Oui | +| P7 Coffre Final | 1 W (+ servo) | Pack B | ~10 h | Oui | + +--- + +## Procédure de Recharge entre Sessions + +### Séquence optimale (chargeur GaN 65 W, 4 ports) + +``` +Port 1 (PD 45W) → Anker #1 (20 000 mAh) — Temps : ~2h00 +Port 2 (PD 18W) → Anker #2 (20 000 mAh) — Temps : ~3h30 +Port 3 (PD 18W) → Pack Puzzle A (10 000 mAh) — Temps : ~1h30 +Port 4 (5W) → P4 Radio via USB-C interne — Temps : ~1h30 +``` + +> Commencer immédiatement après la session pour avoir tout chargé avant la session suivante. + +### Règles de gestion + +1. **Ne jamais stocker une batterie déchargée** — endommageur des cellules Li-Ion. +2. **Recharger à 100% avant chaque déploiement**, pas seulement à "suffisant". +3. **Vérifier le niveau avant d'emballer** — toutes les LEDs de niveau doivent montrer 4/4. +4. **Pack B peut alimenter 2 sessions consécutives** sans recharge intermédiaire (autonomie 15 h+). +5. **Anker #1 nécessite une recharge entre sessions** si deux sessions le même jour. + +### Matériel de recharge (à emporter dans la valise 1) + +- [ ] Chargeur GaN 65 W, 4 ports USB-C (Anker 737 ou équivalent) +- [ ] 5× câbles USB-C courts (0,5 m) pour la recharge simultanée +- [ ] Étiquettes indiquant le port assigné à chaque batterie + +--- + +## Alimentation d'Urgence (batterie morte en cours de partie) + +### Procédure selon l'appareil défaillant + +#### Hub mort (Anker #1 vide) — impact critique + +**Symptômes :** BOX-3 éteint, RTC_PHONE silencieux, routeur éteint. + +**Action immédiate :** +1. Brancher Anker #2 (réserve) sur le splitter Hub. +2. Temps de redémarrage : ~30 secondes. +3. Annoncer aux joueurs (en voix de maître du jeu) : *"Le Professeur a eu un petit problème technique…"* +4. Sur BOX-3 : `Settings → Restore Session` — reprend là où le jeu s'est arrêté. + +#### Puzzle mort (Pack A ou B vide) — impact partiel + +**Symptômes :** Un ou plusieurs puzzles ESP32 ne répondent plus sur le mesh. + +**Action :** +1. Connecter ce puzzle sur le chargeur GaN (Port 3 ou 4) — le puzzle redémarre en quelques secondes. +2. Sur BOX-3 : le nœud manquant réapparaît dans `MESH: X/6 nodes`. +3. Si un nœud ne revient pas après 2 min : `Reset Hardware` sur BOX-3 → envoie une commande de reset ESP-NOW à tous les nœuds. + +#### P4 Radio (18650 interne) mort + +**Action :** Brancher USB-C sur n'importe quel port disponible pendant le jeu — charge et fonctionne simultanément. + +#### Cas extrême : panne totale irréparable + +1. Passer en mode "maître du jeu manuel" : lire les énigmes depuis le kit papier (valise 3). +2. Simuler les réponses du Professeur avec le kit audio de secours (téléphone + playlist MP3). +3. Utiliser le code de secours maître (imprimé sur la fiche lamifiée valise 1) pour ouvrir P7 manuellement. + +--- + +## Mode Hors-Ligne (sans WiFi/internet) + +Tous les puzzles fonctionnent **entièrement sans WiFi** via le mesh ESP-NOW local : + +| Fonction | Online (WiFi dispo) | Offline (ESP-NOW seul) | +|----------|---------------------|------------------------| +| NPC voix (XTTS-v2) | Live depuis KXKM-AI | Fallback MP3 SD card | +| NPC voix (Piper TTS) | Tower:8001 | Fallback MP3 SD card | +| Détection NFC P6 | Locale (pas de réseau) | Identique | +| QR scanner P3 | Locale (BOX-3 camera) | Identique | +| Analytics | Cache local, upload post-jeu | Cache local | +| Scoring + diplôme PDF | En mémoire BOX-3 | En mémoire BOX-3 | + +> **Recommandation :** générer le pool MP3 XTTS avant chaque déploiement si de nouvelles phrases ont été ajoutées (`python3 tools/tts/generate_xtts_pool.py`). diff --git a/hardware/puzzles/suitcase_layout.md b/hardware/puzzles/suitcase_layout.md new file mode 100644 index 0000000..2288739 --- /dev/null +++ b/hardware/puzzles/suitcase_layout.md @@ -0,0 +1,220 @@ +# Suitcase Layout — Zacus V3 Mobile Kit + +3 suitcases total. All foam cutouts use 10mm EVA foam sheets. +Foam cavities are cut 1mm undersize for snug friction fit. + +--- + +## Overview — 3-Suitcase System + +| Suitcase | Name | Size | Contents | Weight est. | +|----------|----------------|------------|-------------------------------------------|-------------| +| A | Hub Suitcase | 57L rigid | BOX-3, router, powerbanks, RTC_PHONE, audio | ~6 kg | +| B | Puzzle Objects | 42L soft | P1, P4, P5, P7 puzzle boxes + accessories | ~4 kg | +| C | Props & Cards | 42L soft | P2 board, P3 cards, P6 tablet, cables, signage | ~3 kg | + +--- + +## Suitcase A — Hub Suitcase (57L Rigid ABS) + +**Dimensions:** 750 × 500 × 280 mm interior usable space +**Foam layout:** 2 layers — top tray + base + +``` +SUITCASE A — TOP VIEW (750mm wide × 500mm deep) +┌─────────────────────────────────────────────────────────────────────┐ +│ │ +│ ┌──────────────────┐ ┌────────────────────┐ ┌─────────────┐ │ +│ │ │ │ │ │ │ │ +│ │ ESP32-S3-BOX-3 │ │ GL.iNet GL-MT3000 │ │ RTC_PHONE │ │ +│ │ 200×120×70mm │ │ 150×100×45mm │ │ 120×80×40 │ │ +│ │ │ │ │ │ │ │ +│ └──────────────────┘ └────────────────────┘ └─────────────┘ │ +│ │ +│ ┌────────────────────────┐ ┌──────────────────────────────────┐ │ +│ │ Anker Powerbank #1 │ │ Anker Powerbank #2 │ │ +│ │ 175×80×28mm │ │ 175×80×28mm │ │ +│ └────────────────────────┘ └──────────────────────────────────┘ │ +│ │ +│ ┌──────────────────┐ ┌────────────────────────────────────────┐ │ +│ │ Anker PB #3 │ │ JBL Go 3 Speaker 85×85×45mm │ │ +│ │ 175×80×28mm │ └────────────────────────────────────────┘ │ +│ └──────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ USB-C Cables roll (10× 1m) — 200×100×60mm bundle │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ LED Strip USB 3m (rolled) 60×60×30mm │ │ +│ │ USB Hub 4-port 90×35×20mm (cable mgmt) │ │ +│ │ Ethernet patch cable 1m (zip bag) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + +LID (zip pocket): + - GL.iNet power adapter + - Quick-start card (laminated A5) + - Master game guide booklet + - Spare micro-USB cable (BOX-3 fallback) +``` + +### Foam Cutout Specifications (Suitcase A) + +| Item | Foam Cavity (L×W×D) | Layer | +|--------------------|---------------------|---------| +| BOX-3 | 202×122×72 mm | Base | +| GL-MT3000 | 152×102×47 mm | Base | +| RTC_PHONE | 122×82×42 mm | Base | +| Powerbank × 3 | 177×82×30 mm each | Base | +| JBL Go 3 | 87×87×47 mm | Base | +| Cable bundle | 202×102×62 mm | Base | +| LED strip roll | 62×62×32 mm | Base | +| USB Hub | 92×37×22 mm | Base | +| Ethernet cable | 50×50×15 mm | Base | + +--- + +## Suitcase B — Puzzle Objects (42L Softcase) + +**Dimensions:** 600 × 420 × 240 mm interior usable space +**Foam layout:** 1 layer with tall cutouts; lid pocket for accessories + +``` +SUITCASE B — TOP VIEW (600mm wide × 420mm deep) +┌──────────────────────────────────────────────────────────┐ +│ │ +│ ┌───────────────────┐ ┌──────────────────────────┐ │ +│ │ │ │ │ │ +│ │ P1 Boîte Sonore │ │ P4 Poste Radio Rétro │ │ +│ │ 150×100×80mm │ │ 200×150×120mm │ │ +│ │ │ │ │ │ +│ └───────────────────┘ └──────────────────────────┘ │ +│ │ +│ ┌───────────────────┐ ┌──────────────────────────┐ │ +│ │ │ │ │ │ +│ │ P5 Télégraphe │ │ P7 Coffre Final │ │ +│ │ 180×80×60mm │ │ 250×200×150mm │ │ +│ │ │ │ │ │ +│ └───────────────────┘ └──────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Accessory zone │ │ +│ │ - Morse reference cards (zip bag) 150×120×5mm │ │ +│ │ - Arcade button spare caps (bag) 80×80×30mm │ │ +│ │ - USB-C cables 4× short (bag) 100×50×20mm │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────┘ + +LID pocket: + - Puzzle reset cards (laminated, one per puzzle) + - Allen key M3 + M2.5 set + - Spare WS2812B LEDs (bag) + - Spare fuse kit +``` + +### Foam Cutout Specifications (Suitcase B) + +| Item | Foam Cavity (L×W×D) | Notes | +|--------------------|---------------------|--------------------------------| +| P1 Boîte Sonore | 152×102×82 mm | Snug — PETG box | +| P4 Poste Radio | 202×152×122 mm | Extra 2mm clearance for knob | +| P5 Télégraphe | 182×82×62 mm | Brass key faces up | +| P7 Coffre Final | 252×202×152 mm | Heaviest piece — bottom layer | +| Accessory strip | 300×120×35 mm | Shared zone, non-foam dividers | + +**Weight check:** +- P7 coffre: ~2.5kg (wood + electronics) +- P4 radio: ~0.8kg (PLA + 18650s) +- P1 boîte: ~0.5kg (PETG + speaker) +- P5 télégraphe: ~0.4kg (PETG + brass key) +- Accessories: ~0.3kg +- Foam: ~0.4kg +- **Total suitcase B: ~4.9kg** — within 5kg carry-on limit + +--- + +## Suitcase C — Props & Cards (42L Softcase) + +**Dimensions:** 600 × 420 × 240 mm interior usable space + +``` +SUITCASE C — TOP VIEW (600mm wide × 420mm deep) +┌──────────────────────────────────────────────────────────┐ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ P2 Magnetic Folding Board │ │ +│ │ 600×400mm folded to 300×400×20mm │ │ +│ │ (occupies full width when folded) │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ │ │ │ │ +│ │ P6 Wooden │ │ P3 QR Cards + Signage │ │ +│ │ Tablet │ │ - 6× A5 QR cards │ │ +│ │ 300×200×20mm │ │ - 10× A4 labels │ │ +│ │ (slots up) │ │ - 1× A3 puzzle map │ │ +│ │ │ │ All in clear folder │ │ +│ └──────────────────┘ └──────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ P6 Symbol Pieces bag (12 pcs, 40×40×8mm each) │ │ +│ │ Total bag: 200×150×30mm │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ P2 Component Kit │ │ +│ │ - Magnetic resistors, LEDs, wires (bag) │ │ +│ │ - Foam-padded compartment tray 200×150×40mm │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Misc │ │ +│ │ - Setup guide A4 folder │ │ +│ │ - Diploma templates (10 sheets, laminated) │ │ +│ │ - USB A-C adapter set (bag) │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────┘ +``` + +### Foam Cutout Specifications (Suitcase C) + +| Item | Foam Cavity (L×W×D) | Notes | +|-------------------------|-----------------------|-------------------------------| +| P2 Folding Board | 302×402×22 mm | Full width — 2 foam strips hold| +| P6 Wooden Tablet | 302×202×22 mm | Slots face up — NFC tags safe | +| P6 Symbol Pieces bag | 202×152×32 mm | Soft bag, low cavity depth | +| P3 Cards folder | 220×160×20 mm | Clear A4 folder, thin section | +| P2 Component tray | 202×152×42 mm | Padded compartment tray | +| Misc folder zone | 220×160×20 mm | Documents + diplomas | + +--- + +## Total Kit Weight & Volume + +| Suitcase | Weight | Volume | Check-in | +|----------|--------|--------|----------| +| A (Hub) | ~6.0 kg | 57L | Yes — checked | +| B (Puzzles) | ~4.9 kg | 42L | Cabin OK | +| C (Props) | ~3.0 kg | 42L | Cabin OK | +| **Total** | **~13.9 kg** | **141L** | | + +Kit fits 2-person team transport (car/train/plane). Suitcase A checked, B+C cabin luggage. + +--- + +## Setup Procedure (Field Deployment) + +1. Open Suitcase A: deploy BOX-3, plug router, connect all powerbanks via USB-C hub +2. Open Suitcase B: place P1/P4/P5 at puzzle stations; P7 at final position +3. Open Suitcase C: unfold P2 board at station; place P6 tablet; hide P3 QR cards per map +4. Power-on sequence: Router → BOX-3 → P1..P7 (each has auto-pair on boot via ESP-NOW) +5. BOX-3 dashboard shows all nodes: green = online, orange = pairing, red = fault +6. Estimated setup time: 15 minutes for 2 staff members + +See `docs/DEPLOYMENT_RUNBOOK.md` for full field procedure including audio check and NPC test. diff --git a/tools/playtest/collect_metrics.py b/tools/playtest/collect_metrics.py new file mode 100644 index 0000000..9a96e02 --- /dev/null +++ b/tools/playtest/collect_metrics.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +""" +collect_metrics.py — Collecte et analyse des métriques de playtest. + +Lit les logs série ESP-NOW de BOX-3, parse les événements de résolution, +demandes d'indices et décisions NPC. Génère un rapport Markdown de session +et l'ajoute au fichier metrics_template.csv. + +Usage: + # Lire depuis port série (BOX-3 connecté en USB) + python3 tools/playtest/collect_metrics.py --port /dev/cu.usbserial-XXXX + + # Lire depuis un fichier log pré-capturé + python3 tools/playtest/collect_metrics.py --log-file session_001.log + + # Spécifier un identifiant de session + python3 tools/playtest/collect_metrics.py --log-file session_001.log --session beta_001 + + # Écrire le rapport dans un dossier spécifique + python3 tools/playtest/collect_metrics.py --log-file session_001.log --output-dir docs/playtest/reports/ +""" + +import argparse +import csv +import re +import sys +from datetime import datetime, timedelta +from pathlib import Path + +# --------------------------------------------------------------------------- +# Constantes +# --------------------------------------------------------------------------- + +PUZZLES = ["P1_SON", "P2_CIRCUIT", "P3_QR", "P4_RADIO", "P5_MORSE", "P6_SYMBOLES", "P7_COFFRE"] + +PUZZLE_LABELS = { + "P1_SON": "Séquence Sonore", + "P2_CIRCUIT": "Circuit LED", + "P3_QR": "QR Treasure", + "P4_RADIO": "Fréquence Radio", + "P5_MORSE": "Code Morse", + "P6_SYMBOLES": "Symboles Alchimiques", + "P7_COFFRE": "Coffre Final", +} + +EXPECTED_DURATIONS_S = { + "P1_SON": 300, + "P2_CIRCUIT": 360, + "P3_QR": 240, + "P4_RADIO": 300, + "P5_MORSE": 420, + "P6_SYMBOLES": 360, + "P7_COFFRE": 300, +} + +METRICS_CSV = Path(__file__).parent.parent.parent / "docs" / "playtest" / "metrics_template.csv" + +# --------------------------------------------------------------------------- +# Patterns de log ESP-NOW / BOX-3 +# --------------------------------------------------------------------------- + +# Exemple : [12:05:33] [ESPNOW] P1_SON: SOLVED in 287s (attempts: 2) +RE_SOLVED = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[ESPNOW\]\s+(?PP\d+_\w+):\s+SOLVED\s+in\s+(?P\d+)s" + r"(?:\s+\(attempts:\s*(?P\d+)\))?" +) + +# Exemple : [12:08:01] [ESPNOW] P1_SON: SKIPPED +RE_SKIPPED = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[ESPNOW\]\s+(?PP\d+_\w+):\s+SKIPPED" +) + +# Exemple : [12:06:12] [NPC] HINT requested: P1_SON level 2 +RE_HINT = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[NPC\]\s+HINT requested:\s+(?PP\d+_\w+)\s+level\s+(?P\d+)" +) + +# Exemple : [11:55:00] [GAME] START target=60min group_size=5 +RE_GAME_START = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[GAME\]\s+START\s+target=(?P\d+)min\s+group_size=(?P\d+)" +) + +# Exemple : [12:55:12] [GAME] END score=1150 profile=TECH profile_correct=true +RE_GAME_END = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[GAME\]\s+END\s+score=(?P\d+)" + r"\s+profile=(?P\w+)\s+profile_correct=(?P\w+)" +) + +# Exemple : [11:56:10] [NPC] PROFILE_DETECTED: TECH +RE_PROFILE = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[NPC\]\s+PROFILE_DETECTED:\s+(?P\w+)" +) + +# Exemple : [11:57:00] [NPC] PHRASE: adaptation.group_tech_detected.0 via XTTS +RE_PHRASE = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[NPC\]\s+PHRASE:\s+(?P\S+)\s+via\s+(?P\w+)" +) + +# Exemple : [12:10:00] [HW] FAILURE: P4_RADIO ESP-NOW timeout +RE_HW_FAILURE = re.compile( + r"\[(?P\d{2}:\d{2}:\d{2})\]\s+\[HW\]\s+FAILURE:\s+(?P.+)" +) + +# --------------------------------------------------------------------------- +# Parseur de logs +# --------------------------------------------------------------------------- + +def parse_log(lines: list[str]) -> dict: + """Parse les lignes de log BOX-3 et retourne les données de session.""" + session: dict = { + "start_ts": None, + "end_ts": None, + "target_minutes": 0, + "group_size": 0, + "profile_detected": "UNKNOWN", + "profile_correct": None, + "final_score": 0, + "puzzles": {}, + "hints": [], + "phrases": [], + "hw_failures": [], + } + + for line in lines: + line = line.rstrip() + + m = RE_GAME_START.search(line) + if m: + session["start_ts"] = m.group("ts") + session["target_minutes"] = int(m.group("target")) + session["group_size"] = int(m.group("size")) + continue + + m = RE_GAME_END.search(line) + if m: + session["end_ts"] = m.group("ts") + session["final_score"] = int(m.group("score")) + session["profile_detected"] = m.group("profile") + session["profile_correct"] = m.group("correct").lower() == "true" + continue + + m = RE_PROFILE.search(line) + if m: + session["profile_detected"] = m.group("profile") + continue + + m = RE_SOLVED.search(line) + if m: + puzzle = m.group("puzzle") + session["puzzles"][puzzle] = { + "status": "SOLVED", + "ts": m.group("ts"), + "duration_s": int(m.group("duration")), + "attempts": int(m.group("attempts")) if m.group("attempts") else 1, + } + continue + + m = RE_SKIPPED.search(line) + if m: + puzzle = m.group("puzzle") + session["puzzles"][puzzle] = { + "status": "SKIPPED", + "ts": m.group("ts"), + "duration_s": None, + "attempts": 0, + } + continue + + m = RE_HINT.search(line) + if m: + session["hints"].append({ + "ts": m.group("ts"), + "puzzle": m.group("puzzle"), + "level": int(m.group("level")), + }) + continue + + m = RE_PHRASE.search(line) + if m: + session["phrases"].append({ + "ts": m.group("ts"), + "key": m.group("key"), + "backend": m.group("backend"), + }) + continue + + m = RE_HW_FAILURE.search(line) + if m: + session["hw_failures"].append({ + "ts": m.group("ts"), + "description": m.group("description"), + }) + + return session + + +def _ts_to_minutes(ts_start: str | None, ts_end: str | None) -> float: + """Calcule la durée en minutes entre deux timestamps HH:MM:SS.""" + if not ts_start or not ts_end: + return 0.0 + fmt = "%H:%M:%S" + try: + t0 = datetime.strptime(ts_start, fmt) + t1 = datetime.strptime(ts_end, fmt) + delta = t1 - t0 + if delta.total_seconds() < 0: + # Passage minuit + delta += timedelta(hours=24) + return delta.total_seconds() / 60 + except ValueError: + return 0.0 + +# --------------------------------------------------------------------------- +# Génération du rapport Markdown +# --------------------------------------------------------------------------- + +def generate_report(session_id: str, session: dict) -> str: + """Génère un rapport Markdown complet pour la session.""" + actual_minutes = _ts_to_minutes(session["start_ts"], session["end_ts"]) + target_minutes = session["target_minutes"] + duration_accuracy_pct = ( + abs(actual_minutes - target_minutes) / target_minutes * 100 + if target_minutes > 0 else 0.0 + ) + + solved_puzzles = [p for p, d in session["puzzles"].items() if d["status"] == "SOLVED"] + skipped_puzzles = [p for p, d in session["puzzles"].items() if d["status"] == "SKIPPED"] + hints_total = len(session["hints"]) + hw_failures = session["hw_failures"] + + # Backend TTS stats + backends_used = [p["backend"] for p in session["phrases"]] + xtts_count = backends_used.count("XTTS") + piper_count = backends_used.count("PIPER") + sdcard_count = backends_used.count("SDCARD") + + lines = [ + f"# Rapport de Session — {session_id}", + f"", + f"Généré le : {datetime.now().strftime('%Y-%m-%d %H:%M')}", + f"", + f"---", + f"", + f"## Résumé", + f"", + f"| Métrique | Valeur |", + f"|----------|--------|", + f"| Session ID | `{session_id}` |", + f"| Taille du groupe | {session['group_size']} joueurs |", + f"| Durée cible | {target_minutes} min |", + f"| Durée réelle | {actual_minutes:.1f} min |", + f"| Précision durée | ±{duration_accuracy_pct:.1f}% |", + f"| Profil NPC détecté | {session['profile_detected']} |", + f"| Profil correct | {'OUI' if session['profile_correct'] else 'NON' if session['profile_correct'] is not None else 'N/A'} |", + f"| Puzzles résolus | {len(solved_puzzles)}/{len(PUZZLES)} |", + f"| Puzzles sautés | {len(skipped_puzzles)} |", + f"| Indices utilisés | {hints_total} |", + f"| Score final | {session['final_score']} |", + f"| Pannes matériel | {len(hw_failures)} |", + f"", + ] + + # Critères de succès + lines += [ + f"## Critères de Succès", + f"", + f"| Critère | Objectif | Résultat | Statut |", + f"|---------|----------|----------|--------|", + f"| Temps de setup | ≤ 15 min | — | — |", + f"| Précision durée | ≤ ±10% | ±{duration_accuracy_pct:.1f}% | {'PASS' if duration_accuracy_pct <= 10 else 'FAIL'} |", + f"| Profil NPC correct | OUI | {'OUI' if session['profile_correct'] else 'NON'} | {'PASS' if session['profile_correct'] else 'FAIL'} |", + f"| Pannes matériel | 0 | {len(hw_failures)} | {'PASS' if not hw_failures else 'FAIL'} |", + f"| Jeu terminé | OUI | {'OUI' if 'P7_COFFRE' in solved_puzzles else 'NON'} | {'PASS' if 'P7_COFFRE' in solved_puzzles else 'FAIL'} |", + f"", + ] + + # Détail puzzles + lines += [ + f"## Détail par Puzzle", + f"", + f"| Puzzle | Statut | Durée réelle | Durée attendue | Écart | Tentatives | Indices |", + f"|--------|--------|-------------|----------------|-------|-----------|---------|", + ] + for puzzle_id in PUZZLES: + label = PUZZLE_LABELS.get(puzzle_id, puzzle_id) + data = session["puzzles"].get(puzzle_id, {"status": "NOT_REACHED", "duration_s": None, "attempts": 0}) + expected = EXPECTED_DURATIONS_S.get(puzzle_id, 0) + puzzle_hints = [h for h in session["hints"] if h["puzzle"] == puzzle_id] + + status = data["status"] + duration_s = data.get("duration_s") + duration_str = f"{duration_s}s" if duration_s is not None else "—" + expected_str = f"{expected}s" + ecart_str = f"{duration_s - expected:+d}s" if duration_s is not None else "—" + attempts = data.get("attempts", 0) + + lines.append( + f"| {label} | {status} | {duration_str} | {expected_str} | {ecart_str} | {attempts} | {len(puzzle_hints)} |" + ) + + lines += [""] + + # Indices demandés + if session["hints"]: + lines += [ + f"## Indices Demandés", + f"", + f"| Heure | Puzzle | Niveau |", + f"|-------|--------|--------|", + ] + for hint in session["hints"]: + puzzle_label = PUZZLE_LABELS.get(hint["puzzle"], hint["puzzle"]) + lines.append(f"| {hint['ts']} | {puzzle_label} | {hint['level']} |") + lines += [""] + + # NPC / voix + if session["phrases"]: + lines += [ + f"## Interventions NPC", + f"", + f"- Phrases jouées : {len(session['phrases'])}", + f"- XTTS-v2 (GPU) : {xtts_count}", + f"- Piper TTS : {piper_count}", + f"- SD Card fallback : {sdcard_count}", + f"", + ] + + # Pannes matériel + if hw_failures: + lines += [ + f"## Pannes Matériel", + f"", + f"| Heure | Description |", + f"|-------|-------------|", + ] + for failure in hw_failures: + lines.append(f"| {failure['ts']} | {failure['description']} |") + lines += [""] + + lines += [ + f"---", + f"", + f"*Rapport généré automatiquement par `tools/playtest/collect_metrics.py`*", + ] + + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Écriture CSV +# --------------------------------------------------------------------------- + +def append_to_csv(session_id: str, session: dict, csv_path: Path, nps_score: float = 0.0) -> None: + """Ajoute une ligne de métriques au fichier CSV de référence.""" + actual_minutes = _ts_to_minutes(session["start_ts"], session["end_ts"]) + solved = [p for p, d in session["puzzles"].items() if d["status"] == "SOLVED"] + skipped = [p for p, d in session["puzzles"].items() if d["status"] == "SKIPPED"] + + row = { + "session_id": session_id, + "date": datetime.now().strftime("%Y-%m-%d"), + "group_size": session["group_size"], + "target_duration": session["target_minutes"], + "actual_duration": round(actual_minutes, 1), + "group_profile_detected": session["profile_detected"], + "group_profile_actual": "", + "puzzles_solved": len(solved), + "puzzles_skipped": len(skipped), + "hints_total": len(session["hints"]), + "nps_score": nps_score, + "notes": "", + } + + fieldnames = [ + "session_id", "date", "group_size", "target_duration", "actual_duration", + "group_profile_detected", "group_profile_actual", "puzzles_solved", + "puzzles_skipped", "hints_total", "nps_score", "notes", + ] + + # Lire les lignes existantes + existing_rows: list[dict] = [] + if csv_path.exists(): + with open(csv_path, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + for r in reader: + if r.get("session_id") != session_id: + existing_rows.append(r) + + # Ré-écrire avec la nouvelle ligne + with open(csv_path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + for r in existing_rows: + writer.writerow(r) + writer.writerow(row) + + print(f"CSV mis à jour : {csv_path}") + + +# --------------------------------------------------------------------------- +# Lecture des sources de données +# --------------------------------------------------------------------------- + +def read_from_file(path: str) -> list[str]: + """Lit les lignes depuis un fichier log.""" + with open(path, encoding="utf-8") as f: + return f.readlines() + + +def read_from_serial(port: str, timeout_seconds: int = 7200) -> list[str]: + """Lit les lignes depuis un port série (BOX-3 connecté en USB).""" + try: + import serial # type: ignore + except ImportError: + print("Erreur : pyserial non installé. Lancer : pip install pyserial", file=sys.stderr) + sys.exit(1) + + lines: list[str] = [] + print(f"Lecture du port série {port} (Ctrl+C pour arrêter)...") + with serial.Serial(port, baudrate=115200, timeout=1) as ser: + start = datetime.now() + game_ended = False + while True: + line = ser.readline().decode("utf-8", errors="replace") + if line: + lines.append(line) + print(line, end="", flush=True) + if "[GAME] END" in line: + game_ended = True + print("\n[collect_metrics] Fin de session détectée.") + break + elapsed = (datetime.now() - start).total_seconds() + if elapsed > timeout_seconds: + print(f"\n[collect_metrics] Timeout {timeout_seconds}s atteint.") + break + return lines + + +# --------------------------------------------------------------------------- +# Point d'entrée +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Collecte et analyse des métriques de playtest Zacus V3" + ) + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--port", help="Port série BOX-3 (ex: /dev/cu.usbserial-XXXX)") + source.add_argument("--log-file", help="Fichier log pré-capturé") + + parser.add_argument("--session", default=None, help="Identifiant de session (ex: beta_001)") + parser.add_argument("--output-dir", default=None, help="Dossier de sortie pour le rapport Markdown") + parser.add_argument("--nps-score", type=float, default=0.0, help="Score NPS moyen du groupe (Q1, 0-10)") + parser.add_argument("--no-csv", action="store_true", help="Ne pas mettre à jour le fichier CSV") + args = parser.parse_args() + + # Identifiant de session + session_id = args.session or f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + + # Lecture des données + if args.port: + lines = read_from_serial(args.port) + else: + lines = read_from_file(args.log_file) + + if not lines: + print("Erreur : aucune donnée à analyser.", file=sys.stderr) + sys.exit(1) + + # Analyse + session = parse_log(lines) + + # Rapport Markdown + report_md = generate_report(session_id, session) + + output_dir = Path(args.output_dir) if args.output_dir else Path(__file__).parent.parent.parent / "docs" / "playtest" / "reports" + output_dir.mkdir(parents=True, exist_ok=True) + report_path = output_dir / f"{session_id}_report.md" + report_path.write_text(report_md, encoding="utf-8") + print(f"Rapport Markdown : {report_path}") + + # Affichage rapide + actual_minutes = _ts_to_minutes(session["start_ts"], session["end_ts"]) + solved = [p for p, d in session["puzzles"].items() if d["status"] == "SOLVED"] + print(f"\nRésumé session {session_id}:") + print(f" Groupe : {session['group_size']} joueurs") + print(f" Durée : {actual_minutes:.1f} min / {session['target_minutes']} min cible") + print(f" Profil NPC : {session['profile_detected']} ({'correct' if session['profile_correct'] else 'incorrect'})") + print(f" Puzzles : {len(solved)}/{len(PUZZLES)} résolus, {len(session['hints'])} indices") + print(f" Score : {session['final_score']}") + print(f" NPS : {args.nps_score}/10") + print(f" HW pannes : {len(session['hw_failures'])}") + + # Mise à jour CSV + if not args.no_csv: + append_to_csv(session_id, session, METRICS_CSV, nps_score=args.nps_score) + + +if __name__ == "__main__": + main()