Guide développeur¶
Ce guide vise à permettre à d’autres développeurs (ou la FFTL) de contribuer au projet sans devoir relire tout le code.
Installation de l’environnement de développement¶
FletchTime n’a qu’une seule dépendance externe (websockets) ; tout le reste
est stdlib Python 3.10+ (Python 3.11+ pour config_store.py, qui utilise
tomllib).
git clone https://github.com/MrFanghoDev/fletchtime.git
cd fletchtime
pip install websockets # seule dépendance de runtime
pip install -e ".[docs]" # optionnel, pour construire cette doc
Aucun environnement virtuel n’est strictement nécessaire (le projet est volontairement minimal en dépendances, y compris pour rester installable depuis Pydroid 3 sur Android – voir Choix historiques plus bas), mais rien n’empêche d’en utiliser un si tu préfères isoler ton installation.
Lancer les tests¶
PYTHONPATH=src python3 -m unittest discover -s tests -v
ou, de façon équivalente et sans variable d’environnement à poser (pensé pour tourner directement depuis Pydroid) :
python3 run_tests.py
Tous les tests sont en unittest (stdlib), aucune dépendance de test à
installer. Le paquet websockets n’est pas nécessaire pour lancer les
tests : la logique serveur (fletchtime.server.match_server) est testée via
un faux client WebSocket (FakeWebSocket dans tests/test_match_server.py),
justement pour rester testable dans un environnement qui n’a pas accès au
réseau.
Options de la ligne de commande (mode terminal)¶
fletchtime --headless --help (ou python -m fletchtime --headless --help) affiche la liste complète, mais résumé ici pour référence
rapide – voir fletchtime/__main__.py, _build_arg_parser.
Option |
Effet |
|---|---|
|
Affiche l’aide et quitte. |
|
Affiche la version et quitte. |
|
Mode terminal, sans fenêtre graphique. |
|
Affiche les journaux applicatifs (commandes reçues, (dé)connexions…) dans le terminal, pas seulement dans le fichier. |
|
Journalisation la plus détaillée possible, fichier compris – implique |
|
Remplace le port HTTP configuré, pour cette exécution seulement. |
|
Remplace le port WebSocket configuré, pour cette exécution seulement. |
Note
--http-port/--ws-port ne modifient jamais config/gui.toml –
un remplacement ponctuel (utile pour un lancement scripté/CI, ou
plusieurs salles de compétition sur un même PC sans dossier séparé par
salle), pas un changement persistant. Pour un réglage durable, voir la
fenêtre graphique (section Ports) ou éditer config/gui.toml
directement.
Niveaux de journalisation, fichier et terminal indépendants l’un de
l’autre (voir fletchtime.logging_setup.configure_logging) : le
fichier de journal (logs/fletchtime.log) reste toujours à INFO par
défaut, quelle que soit la commande utilisée pour lancer FletchTime –
le diagnostic après-coup d’un concours ne doit pas dépendre de si
quelqu’un a pensé à ajouter -v. Seul le terminal respecte
-v/--verbose (silencieux par défaut, WARNING). --debug élève les
deux au niveau DEBUG.
Architecture générale¶
Voir Architecture technique pour le détail complet. En résumé :
src/fletchtime/engine/: le moteur de séquencement, pur Python, aucune dépendance. C’est la partie la plus testée et la plus stable du projet. Publiable seul (from fletchtime.engine import ...) sans le reste.src/fletchtime/server/: le serveur (WebSocket + HTTP statique + config TOML), construit par-dessus le moteur sans jamais le modifier.src/fletchtime/web/: les pages HTML/CSS/JS de l’application elles-mêmes (control.html, display.html…), incluses dans le paquet installé comme package data – servies en lecture seule, jamais modifiées par un club.src/fletchtime/__main__.py: point d’entrée (python -m fletchtime), résout où vivent les pages de l’appli (dans le paquet) et où vivent les données du club (répertoire courant, ou à côté de l’exécutable si PyInstaller) – voir sa docstring pour le détail de cette distinction.web/assets/(à la racine du dépôt, pas danssrc/) : les données propres à un club (logo, bannières, images de cible, packs de sons) – doivent rester modifiables, jamais embarquées dans le paquet lui-même.config/: fichiers TOML lus/écrits parconfig_store.py, même logique queweb/assets/(répertoire courant, pas dans le paquet).docs/: cette documentation (Sphinx + MyST).
Conventions de code¶
Pas de dépendance ajoutée à la légère : toute nouvelle dépendance doit fonctionner de façon fiable sur Pydroid 3 (Android). En cas de doute, privilégier la stdlib ou un fallback pur Python.
Docstrings en anglais dans le code (
src/), commentaires utilisateur (fichiers TOML, README destinés au club) en français.Un test qui échoue avant livraison n’est pas un problème – c’est le système qui fonctionne. Ne pas contourner un test qui échoue sans comprendre pourquoi.
Process de contribution¶
Projet encore jeune, pas de process lourd : ouvre une issue ou une PR sur
GitHub. Merci d’inclure des
tests pour tout changement de comportement dans src/.
Choix historiques et alternatives écartées¶
Pourquoi pas un fork d’ArcheryClock ? Le code source réel (Pascal, ~12 000 lignes dans un seul fichier, dispatch par chaînes de caractères scattered dans des dizaines d’endroits) rendait l’ajout d’un mode Flint risqué et difficile à maintenir. Repartir de zéro en Python a permis une architecture plus modulaire (un mode = une classe) dès le départ.
Pourquoi Pydroid 3 / développement mobile-only ? Contrainte réelle du club : pas de PC disponible. Toutes les décisions de dépendances (
websocketsplutôt que FastAPI/uvicorn,tomllibstdlib plutôt que PyYAML) découlent de cette contrainte.Pourquoi TOML plutôt que JSON/YAML pour la config ?
tomllibest dans la stdlib depuis Python 3.11 (donc disponible sur Pydroid sans rien installer) ; TOML est aussi plus lisible qu’un JSON pour un bénévole non développeur qui irait éditer le fichier à la main.Pourquoi pas de formulaire d’upload pour le logo/bannières/sons ? Simplicité : le dépôt de fichier dans un dossier + découverte automatique par listing de répertoire HTTP évite de construire et maintenir un formulaire d’upload, sans perdre en fonctionnalité pour l’usage réel du club.
Avertissement
Piège PyInstaller 6.0+ : depuis cette version, un build --onedir
place par défaut tout son contenu (hors l’exécutable) dans un sous-dossier
_internal/, plutôt que directement à côté de l’exe comme avant. Notre
code (fletchtime.__main__._app_web_dir) suppose l’ancien layout plat –
fletchtime.spec restaure ce comportement via contents_directory="."
sur l’appel à EXE(...). Si ce paramètre disparaît un jour d’une
réécriture du spec, le symptôme est trompeur : le serveur démarre, la
console affiche l’adresse normalement, mais naviguer vers cette adresse
n’affiche qu’un listing de répertoire avec juste assets/ dedans (le
dossier bootstrapé), sans les pages de l’appli – et ça touche même
127.0.0.1 sur la machine qui héberge le serveur, donc ce n’est pas un
problème de pare-feu/réseau si ce symptôme précis apparaît.
Avertissement
Piège PyInstaller + customtkinter : cette bibliothèque embarque ses
thèmes (.json) et polices (.otf) comme données de paquet, que
PyInstaller ne détecte pas automatiquement (piège documenté par le projet
customtkinter lui-même). fletchtime.spec les inclut explicitement via
collect_data_files("customtkinter") – sans ça, l’exécutable construit
plante au lancement de la fenêtre (thème introuvable), même si la
construction elle-même s’est terminée sans erreur apparente.
Avertissement
macOS retiré de la matrice de build après plusieurs tentatives de correction infructueuses – historique complet ci-dessous pour qui voudrait reprendre l’investigation un jour.
Symptôme constaté sur plusieurs runs réels : ModuleNotFoundError: No module named 'fletchtime.runtime' au lancement de l’exécutable macOS,
alors que ce module est importé sans condition en tête de
fletchtime/__main__.py. Windows et Linux n’ont jamais montré ce
problème.
Tentatives, dans l’ordre :
collect_submodules("fletchtime")danshiddenimports– sans effet, le journal de build ne montrait toujours aucune ligneAnalyzing hidden importpour nos modules.Ajout d’un
sys.path.insert(0, str(project_root / "src"))avant cet appel (hypothèse :collect_submodulesa besoin que le paquet soit importable au moment de l’interprétation du.spec, avant même la construction de l’objetAnalysis) – toujours sans effet visible dans le journal de build.Liste explicite de chaque sous-module en dur dans
hiddenimports, vérifiée par une comparaison automatique avecpkgutil.walk_packages(17 modules, correspondance exacte confirmée) – a corrigé Windows et Linux (qui n’avaient pourtant jamais montré le problème…), mais macOS a échoué à l’identique, avec exactement la même erreur.
Ce dernier point est la donnée la plus importante : si une liste exhaustive
et vérifiée ne change rien, le problème n’est probablement pas une
histoire de modules manquants dans hiddenimports du tout, mais quelque
chose de plus profond et spécifique à macOS/ARM64 – pistes non
explorées : la ré-écriture de version SDK et la re-signature ad-hoc que
PyInstaller effectue automatiquement sur EXE en fin de build macOS
(visibles dans le journal : « Rewriting the executable’s macOS SDK
version », « Re-signing the EXE »), qui pourraient corrompre ou exclure
une partie de l’archive Python embarquée sans que la construction
elle-même ne signale d’erreur.
Face à une cause qui résiste à plusieurs corrections ciblées et vérifiées,
et sans utilisateur macOS avéré à ce jour, la décision a été de revenir
à hiddenimports=["websockets", "customtkinter"] (la liste explicite
n’apportait aucun bénéfice réel à Windows/Linux, qui fonctionnaient déjà
sans elle – seulement un risque de régression silencieuse si un nouveau
module est ajouté sans être ajouté à la liste) et de retirer macOS de
la matrice plutôt que de continuer à itérer à l’aveugle sans jamais
pouvoir tester sur une vraie machine. Réintroduire macOS nécessiterait de
reprendre cette investigation, idéalement avec accès à une vraie machine
macOS pour tester en dehors du cycle CI complet.
Pourquoi le risque de module manquant reste important même sans
macOS : le même problème, survenant sur fletchtime.gui plutôt que
fletchtime.runtime, échouerait silencieusement – fletchtime.gui
est importé dans un try/except (voir main()), donc son absence
retomberait sur le mode terminal sans aucune erreur visible, sur Windows
et Linux aussi, sans que personne ne s’en aperçoive avant qu’un
utilisateur signale que la fenêtre ne s’ouvre jamais.
Pour déboguer ce genre de souci sans attendre une vraie release :
build.yml construit désormais aussi les exécutables sur chaque push
touchant à l’empaquetage (src/, fletchtime.spec, pyproject.toml),
sans publier de Release – artefacts téléchargeables directement depuis
la page du run (section « Artifacts »). fail-fast: false reste en place
sur la matrice (utile même à deux plateformes).
Avertissement
Ne pas écrire un thème customtkinter entièrement personnalisé (JSON
maison). Tenté une première fois pour reprendre la palette de marque de
l’appli – a cassé la construction de la fenêtre en conditions réelles
(KeyError: 'corner_radius') car le fichier fait à la main oubliait une
clé interne attendue par la version de customtkinter installée. Le
schéma exact de ces fichiers n’est pas garanti stable d’une version à
l’autre et est difficile à valider sans lancer réellement la fenêtre (pas
possible dans l’environnement où ce module est habituellement modifié,
voir plus bas). À la place, fletchtime.gui._apply_brand_colors part d’un
thème intégré (« dark-blue », garanti complet) et ne surcharge que les
valeurs de couleur déjà présentes – jamais de clé nouvelle – le tout
protégé par un try/except : en cas d’incompatibilité future, l’appli
retombe sur le thème intégré tel quel plutôt que de planter.
Note
Fenêtre graphique et tests : fletchtime.gui n’a pas pu être testé
visuellement (aucun affichage graphique disponible dans l’environnement où
ce module a été écrit). Ce qui est testé pour de vrai, c’est la logique
qu’elle pilote : fletchtime.runtime.ServerRuntime (voir
tests/test_runtime.py – démarrage, requête HTTP réelle, arrêt propre,
port libéré, redémarrage). Si tu modifies fletchtime/gui.py, un
lancement réel sur PC (et si possible sur Pydroid) reste nécessaire avant
de considérer le changement fiable – l’exécution de test.yml ne le
détecterait pas (ni GitHub Actions, dont les runners n’ont pas
d’affichage graphique, ce qui est justement pourquoi main() propose un
mode --headless, utilisé par les tests de fumée de build.yml).
Confirmé en conditions réelles : Pydroid 3 refuse catégoriquement
d’ouvrir une fenêtre Tk quand le script est lancé depuis son Terminal
(« GUI applications cannot be ran from terminal… Use IDE to run these
applications ») – l’échec se produit à la construction de CTk()
elle-même (AttributeError sur createcommand), pas à l’import de
customtkinter. main() intercepte donc une exception large (pas
seulement ImportError) autour de tout run_gui(), et
fletchtime.gui.run_gui arrête proprement tout serveur déjà démarré avant
de la laisser remonter – sans ça, le repli en mode terminal se heurterait
à un port déjà occupé. Sur Pydroid, seul le bouton ▶️ Run de l’éditeur
(pas le Terminal) permet réellement d’ouvrir la fenêtre graphique.
Note
macOS n’est plus dans la matrice de build – voir l’encadré d’avertissement plus haut pour l’historique complet de l’investigation et pourquoi.
Décision délibérée, pas un oubli, sur les architectures : ne pas
construire d’artefact séparé par architecture (Windows/Linux ARM64) tant
qu’aucun utilisateur réel n’en a exprimé le besoin. Windows/Linux x86_64
couvrent l’écrasante majorité des PC de club ; Windows sait émuler x86_64
sur ARM64 nativement ; un besoin Linux ARM (ex. Raspberry Pi) passerait de
toute façon plus naturellement par pip install fletchtime (fonctionne
sur n’importe quelle architecture) que par un exécutable PyInstaller
dédié.
Note
La publication automatique de cette doc sur GitHub Pages est déjà en place
(.github/workflows/docs.yml) – voir le README à la racine du dépôt pour
l’activer (une seule fois, côté réglages GitHub).
Comment ajouter un nouveau mode de tir (
ShootingMode)¶Crée
src/fletchtime/engine/modes/ton_mode.py, avec une classe qui hérite deShootingMode(voirmodes/base.py) et implémentebuild_sequence() -> List[Step]. Regardeindoor.py/flint.pycomme modèles – la logique de ton mode n’a besoin de rien connaître du réseau, du serveur, ni de l’affichage : elle ne fait que décrire, à l’avance, la liste ordonnée desStep(phases temporisées) du match.Ajoute un dataclass de config (ex.
TonModeConfig) avec__post_init__qui valide les valeurs (voir les validations existantes dansIndoorConfig/FlintConfigcomme exemples : cohérence des durées, des listes de distances, etc.).Écris les tests dans
tests/test_ton_mode.py: ce sont des tests sur la liste deStepproduite parbuild_sequence(), pas besoin de faire tourner de minuteur ni de serveur pour ça (voirtest_indoor_mode.py,test_flint_mode.py).Câble ton mode dans
src/fletchtime/server/match_server.py(actionsstart_<ton_mode>danshandle_command, plusload_<ton_mode>_config/save_<ton_mode>_configdansconfig_store.pysi tu veux qu’il soit configurable viaconfig.html).Ajoute les boutons/champs correspondants dans
src/fletchtime/web/control.htmletsrc/fletchtime/web/config.html.Pièges à éviter :
Ne mets aucun état mutable dans la classe
ShootingModeelle-même –build_sequence()doit être une fonction pure, appelée une seule fois. Tout l’état vivant (position courante, pause, urgence) appartient àMatchEngine, qui rejoue la séquence produite.Un
Stepavecduration=Nonesignifie « attente indéfinie » (utilisé pour la pause de fin de volée) – l’engine ne décompte jamais ce genre de step, seul unnext()manuel du responsable du chronométrage le fait avancer.Si ton mode a besoin d’un seuil d’alerte (orange) qui ne redémarre pas le décompte, utilise
orange_threshold/orange_sound_eventsur leStepplutôt que de créer un step séparé (voirengine.py,_maybe_emit_orange_threshold_event).