temptours+tours

This commit is contained in:
2025-12-09 11:27:02 -05:00
commit f28439c9bb
49 changed files with 18416 additions and 0 deletions

Binary file not shown.

16023
chronotrack.log Normal file

File diff suppressed because it is too large Load Diff

402
chronotrack_server_au.py Normal file
View File

@ -0,0 +1,402 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import threading
import queue
import time
import sys
import datetime
import binascii # laissé si tu en as besoin plus tard, ça garde le diff minimal
from mysql_writer import write_chrinfo, write_log
# ===== Debug =====
DEBUG = False # activé via paramètre --debug
# ===== Config =====
HOST = "0.0.0.0"
PORT = 20201
PROGRAM_NAME = "MS1Server"
PROGRAM_VERSION = "2.0-TCP"
STREAM_MODE = "push"
TIME_FORMAT = "unix"
LOG_FILE = "chronotrack.log"
# Credentials plaintext
AUTH_USER = "progiweb"
AUTH_PASS = "Airstream"
# ===== État global =====
running = True
contexts = {} # contexte par IP de feed (logique CTP01)
requested_evt = set()
packet_q = queue.Queue()
# Gestion des clients TCP
clients_lock = threading.Lock()
client_id_seq = 0 # compteur pour attribuer un id lisible à chaque connexion
last_client = None # dernier client ayant envoyé quelque chose (pour la console)
server_sock = None # socket d'écoute TCP
def now_iso():
return datetime.datetime.now().isoformat(timespec="seconds")
def send_line(message: str, client: dict):
"""
Envoie UNE LIGNE CTP01 sur la connexion TCP du client.
client = {"id": int, "conn": socket, "addr": (ip, port)}
"""
msg = (message.strip() + "\r\n").encode("utf-8", errors="ignore")
try:
client["conn"].sendall(msg)
if DEBUG:
print(f"📤 Envoyé → [C{client['id']}] {client['addr']}: {message}")
except Exception as e:
if DEBUG:
print(f"❌ Erreur send_line vers [C{client['id']}] {client['addr']}: {e}")
def map_type_transaction(tagcode_lc: str) -> str:
if tagcode_lc == "guntime":
return "GUN"
if tagcode_lc == "newlocation":
return "NEW LOCATION"
return "TAG READ"
def print_mysql_ct(client: dict, ctx, parts, time_unix, time_iso):
"""
Prépare les champs, affiche en console si debug, et pousse l'écriture
dans MySQL (via write_chrinfo).
"""
addr = client["addr"]
feed_ip, feed_port = addr
format_id, seq, location, tagcode, _t, gator, reader_id, lap = parts[:8]
event_id = ctx.get("event_id") or "UNKNOWN"
event_name = ctx.get("event_name") or ""
event_desc = ctx.get("event_desc") or ""
tag_lc = tagcode.lower()
type_transaction = map_type_transaction(tag_lc)
tag_display = "" if tag_lc in ("newlocation", "guntime") else tagcode
received_at = now_iso()
if DEBUG:
print("──────────────────────────────────────── CHAMPS MYSQL")
print(f" client_id = C{client['id']}")
print(f" chrinfo_feed_ip = {feed_ip}")
print(f" chrinfo_feed_port = {feed_port}")
print(f" chrinfo_event_id = {event_id}")
print(f" chrinfo_event_name = {event_name}")
print(f" chrinfo_event_desc = {event_desc}")
print(f" chrinfo_type_transaction = {type_transaction}")
print(f" chrinfo_format_id = {format_id}")
print(f" chrinfo_sequence = {seq}")
print(f" chrinfo_location = {location}")
print(f" chrinfo_tag = {tag_display}")
print(f" chrinfo_time = {time_unix}")
print(f" chrinfo_time_iso = {time_iso}")
print(f" chrinfo_gatornumber = {gator}")
print(f" chrinfo_readerid = {reader_id}")
print(f" chrinfo_lap = {lap}")
print(f" chrinfo_received_at_iso = {received_at}")
print("────────────────────────────────────────────────────\n")
# === Construction du dict MySQL ===
row = {
"chrinfo_feed_ip": feed_ip,
"chrinfo_feed_port": feed_port,
"chrinfo_event_id": event_id,
"chrinfo_event_name": event_name,
"chrinfo_event_desc": event_desc,
"chrinfo_type_transaction": type_transaction,
"chrinfo_format_id": format_id,
"chrinfo_sequence": seq,
"chrinfo_location": location,
"chrinfo_tag": tag_display,
"chrinfo_time": time_unix,
"chrinfo_time_iso": time_iso,
"chrinfo_gatornumber": gator,
"chrinfo_readerid": reader_id,
"chrinfo_lap": lap,
"chrinfo_received_at_iso": received_at,
}
try:
if type_transaction == "NEW LOCATION":
write_log("NEWLOCATION", f"[C{client['id']}] New location reçue : {row}")
write_chrinfo(row)
except Exception as e:
if DEBUG:
print(f"❌ Erreur écriture MySQL: {e}")
def print_mysql_control(client: dict, raw_line, tokens, ctx):
if not DEBUG:
return
addr = client["addr"]
feed_ip, feed_port = addr
event_id = ctx.get("event_id") or "UNKNOWN"
event_name = ctx.get("event_name") or ""
event_desc = ctx.get("event_desc") or ""
print("──────────────────────────────────────── CHAMPS MYSQL (CONTROL)")
print(f" client_id = C{client['id']}")
print(f" chrinfo_feed_ip = {feed_ip}")
print(f" chrinfo_feed_port = {feed_port}")
print(f" chrinfo_event_id = {event_id}")
print(f" chrinfo_event_name = {event_name}")
print(f" chrinfo_event_desc = {event_desc}")
print(f" raw_line = {raw_line}")
print(f" tokens = {tokens}")
print(f" received_at_iso = {now_iso()}")
print("────────────────────────────────────────────────────\n")
# ===================== PACKET HANDLING (CTP01) =====================
def handle_greeting(client: dict):
send_line(f"{PROGRAM_NAME}~{PROGRAM_VERSION}~6", client)
send_line(f"stream-mode={STREAM_MODE}", client)
send_line(f"time-format={TIME_FORMAT}", client)
send_line("pushmode-ack=true", client)
send_line("guntimes=true", client)
send_line("newlocations=true", client)
send_line("authentication=plaintext", client)
def handle_packet(text: str, client: dict):
global contexts
addr = client["addr"]
ip_key = addr[0]
ctx = contexts.setdefault(ip_key, {"event_id": None, "event_name": None, "event_desc": None})
if DEBUG:
print(f"\n📩 [{now_iso()}] Reçu de [C{client['id']}] {addr}: {text}")
# Greeting
if text.startswith("StreamManager"):
if DEBUG:
print(f"🔭 Greeting reçu de [C{client['id']}] {addr} → réponse CTP01")
handle_greeting(client)
return
# AUTH
if text.startswith("authorize~"):
parts = text.split("~")
user = parts[1] if len(parts) >= 2 else ""
pwd = parts[2] if len(parts) >= 3 else ""
if DEBUG:
print(f"🔑 AUTH reçu de [C{client['id']}] {addr} : user={user}, pass={pwd}")
if user == AUTH_USER and pwd == AUTH_PASS:
if DEBUG:
print("✅ AUTH OK")
send_line("ack~authorize", client)
send_line("start", client)
send_line("geteventinfo", client)
else:
if DEBUG:
print("❌ AUTH FAIL")
send_line("err~authorize~invalid password", client)
return
# EVENT INFO
if text.startswith("ack~geteventinfo~"):
parts = text.split("~")
if len(parts) >= 5:
ctx["event_name"] = parts[2]
ctx["event_id"] = parts[3]
ctx["event_desc"] = parts[4]
if DEBUG:
print(f"🎟️ EVENT INFO [C{client['id']}] {addr}{ctx}")
return
# ACK / ERR génériques
if text.startswith(("ack~", "err~")):
parts = text.split("~")
print_mysql_control(client, text, parts, ctx)
return
# CT01_xx
if text.startswith("CT"):
packet_q.put((client, text))
return
# Tout autre message
print_mysql_control(client, text, text.split("~"), ctx)
# ===================== THREADS =====================
def client_thread(client: dict):
global running, last_client
conn = client["conn"]
addr = client["addr"]
cid = client["id"]
if DEBUG:
print(f"🟢 Nouvelle connexion [C{cid}] depuis {addr}")
try:
f = conn.makefile("r", encoding="utf-8", newline="")
except Exception as e:
if DEBUG:
print(f"❌ Erreur makefile sur [C{cid}] {addr}: {e}")
try:
conn.close()
except Exception:
pass
return
try:
for line in f:
if not running:
break
text = line.strip("\r\n")
if not text:
continue
last_client = client
handle_packet(text, client)
except Exception as e:
if DEBUG:
print(f"❌ Erreur lecture sur [C{cid}] {addr}: {e}")
finally:
if DEBUG:
print(f"🛑 Connexion fermée [C{cid}] {addr}")
try:
f.close()
except Exception:
pass
try:
conn.close()
except Exception:
pass
def accept_thread():
global server_sock, running, client_id_seq
while running:
try:
conn, addr = server_sock.accept()
except OSError:
break
except Exception as e:
if DEBUG:
print(f"❌ Erreur accept(): {e}")
continue
with clients_lock:
client_id_seq += 1
cid = client_id_seq
client = {"id": cid, "conn": conn, "addr": addr}
threading.Thread(target=client_thread, args=(client,), daemon=True).start()
def parser_thread():
global running
with open(LOG_FILE, "a", encoding="utf-8") as log:
while running or not packet_q.empty():
try:
client, text = packet_q.get(timeout=1)
except queue.Empty:
continue
addr = client["addr"]
ip_key = addr[0]
ctx = contexts.get(ip_key, {})
parts = text.split("~")
if len(parts) < 5:
print_mysql_control(client, text, parts, ctx)
continue
try:
t = float(parts[4])
time_iso = datetime.datetime.fromtimestamp(t).isoformat(timespec="milliseconds")
except Exception:
print_mysql_control(client, text, parts, ctx)
continue
print_mysql_ct(client, ctx, parts, t, time_iso)
log.write(f"{now_iso()} | C{client['id']} | {addr} | {text}\n")
log.flush()
if DEBUG:
print("🛑 Parser arrêté.")
# ===================== MAIN =====================
def main():
global server_sock, running, last_client, DEBUG
# Active le mode debug via paramètre
if "--debug" in sys.argv:
DEBUG = True
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind((HOST, PORT))
server_sock.listen(100)
if DEBUG:
print(f"🟢 Serveur ChronoTrack TCP actif sur {server_sock.getsockname()[0]}:{server_sock.getsockname()[1]}")
print("Tape 'q' puis Entrée pour quitter proprement.\n")
t_accept = threading.Thread(target=accept_thread, daemon=True)
t_parse = threading.Thread(target=parser_thread, daemon=True)
t_accept.start()
t_parse.start()
try:
while True:
if DEBUG:
cmd = input(">>> ").strip()
if cmd.lower() in ("q", "quit", "exit"):
running = False
break
if last_client:
send_line(cmd, last_client)
else:
print("⚠️ Aucun client connecté pour recevoir la commande.")
else:
# En mode non-interactif (systemd), pas de input()
time.sleep(1)
except KeyboardInterrupt:
running = False
finally:
try:
server_sock.close()
except Exception:
pass
t_accept.join(timeout=2.0)
t_parse.join(timeout=2.0)
if DEBUG:
print("✅ Fermeture complète.")
sys.exit(0)
if __name__ == "__main__":
main()

View File

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

74
engine - 20251204am/db.py Normal file
View File

@ -0,0 +1,74 @@
"""
db.py
======
Module de gestion de la connexion MySQL pour le moteur de résultats.
Ce module est séparé pour éviter de dupliquer la logique de connexion
dans plusieurs fichiers Python. Si plus tard tu veux réutiliser cette
connexion dans d'autres scripts, tu n'auras qu'à importer get_connection().
"""
import mysql.connector
from mysql.connector import Error
from engine.logger import log_info, log_debug, log_warn, log_error
# ---------------------------------------------------------------------------
# CONFIGURATION DE LA BASE DE DONNÉES
# ---------------------------------------------------------------------------
# ⚠ IMPORTANT :
# - Adapte ces valeurs à TON environnement (dev / prod).
# - Idéalement, tu peux plus tard les lire à partir d'un fichier
# (ex: info_serveur.txt) pour éviter de hardcoder les mots de passe.
# ---------------------------------------------------------------------------
DB_CONFIG = {
"host": "serveur-prod.progiweb.net",
"user": "ms1reader_bd", # ajuste au besoin
"password": "X~=v]lg-l]4,T2FF", # ajuste au besoin
"database": "ms1reader_centrale",
"port": 3306
}
TABLE_NAME = "reader_lectures"
def get_connection():
"""
Crée et retourne une connexion MySQL.
Returns
-------
mysql.connector.connection_cext.CMySQLConnection
Connexion déjà ouverte vers la base configurée dans DB_CONFIG.
Raises
------
mysql.connector.Error
Si la connexion échoue (mauvais host, user, password, etc.).
"""
return mysql.connector.connect(**DB_CONFIG)
def test_connection():
"""
Petit utilitaire pour tester rapidement la connexion.
Tu peux exécuter ce fichier directement :
python -m engine.db
"""
try:
cnx = get_connection()
if cnx.is_connected():
log_info("[OK] Connexion MySQL réussie.")
else:
log_error("[ERREUR] Connexion MySQL NON établie.")
except Error as e:
log_error(f"[ERREUR] Mysql : {e}")
finally:
try:
if cnx and cnx.is_connected():
cnx.close()
except NameError:
# cnx n'existe pas si la connexion a échoué avant son affectation
pass
if __name__ == "__main__":
test_connection()

View File

@ -0,0 +1,341 @@
"""
Moteur de résultats Version TP + DETAIL brut
----------------------------------------------
Étapes couvertes :
1) Lire les courses actives (reader_config).
2) Charger en mémoire la configuration des timing points / contrôleurs
à partir de reader_config_timingpoint + reader_config_detail
(clef = 'clef_controleur').
3) Pour chaque course active :
- lire resultv2_<con_id>_lecture.last_processed_read_id
- lire les nouvelles lectures dans reader_lectures
- associer chaque lecture à un coureur (cla_list_tag) et à un TP
(via chrinfo_location → config_detail)
- écrire une ligne brute dans resultv2_<con_id>_detail
avec con_id, dossard, tp_id, tp_name, chrinfo_location, priorité, etc.
"""
from typing import Optional, Dict, List
from mysql.connector import Error
from engine.db import get_connection
BATCH_SIZE = 500
# ---------------------------------------------------------
# 1) COURSES ACTIVES
# ---------------------------------------------------------
def get_active_con_ids(cur) -> List[int]:
"""Retourne tous les con_id actifs."""
sql = "SELECT con_id FROM reader_config WHERE rea_active = 1"
cur.execute(sql)
rows = cur.fetchall()
return [int(r["con_id"]) for r in rows]
# ---------------------------------------------------------
# 2) CHARGER LA CONFIG TP / CONTRÔLEURS EN MÉMOIRE
# ---------------------------------------------------------
def load_tp_config(cur) -> Dict[str, List[Dict]]:
"""
Charge la configuration des timing points et contrôleurs.
On lit :
- reader_config_timingpoint (tp_id, con_id, tp_name)
- reader_config_detail (det_type=1, det_clef='clef_controleur')
On retourne un dict :
clé = chrinfo_location (ex: 'PRO5655')
valeur= liste de dicts { con_id, tp_id, tp_name, priority }
"""
sql = """
SELECT
tp.tp_id,
tp.con_id,
tp.tp_name,
d.det_value1 AS chr_location,
d.det_value2 AS priority
FROM reader_config_timingpoint tp
JOIN reader_config_detail d
ON d.det_type = 1
AND d.det_ref1_id = tp.tp_id
AND d.det_clef = 'clef_controleur'
"""
cur.execute(sql)
rows = cur.fetchall()
tp_map: Dict[str, List[Dict]] = {}
for row in rows:
loc = row["chr_location"]
if loc not in tp_map:
tp_map[loc] = []
# normaliser la priorité (det_value2) → int, défaut 1
try:
prio = int(row["priority"]) if row["priority"] is not None else 1
except ValueError:
prio = 1
tp_map[loc].append(
{
"con_id": int(row["con_id"]),
"tp_id": int(row["tp_id"]),
"tp_name": row["tp_name"],
"priority": prio,
}
)
# DEBUG : afficher ce qu'on a chargé
print("[DEBUG] TP config chargée :")
if not tp_map:
print(" (aucune ligne trouvée dans reader_config_detail avec det_clef='clef_controleur')")
else:
for loc, items in tp_map.items():
descs = [
f"con_id={it['con_id']} tp_id={it['tp_id']} "
f"tp_name={it['tp_name']} prio={it['priority']}"
for it in items
]
print(f" location={loc} -> " + " | ".join(descs))
return tp_map
def resolve_tp_for_lecture(tp_map: Dict[str, List[Dict]], con_id: int, chr_location: str) -> Optional[Dict]:
"""
À partir de chrinfo_location, trouve le TP applicable pour cette course.
- On cherche dans tp_map[chr_location]
- On filtre sur con_id
- S'il y a plusieurs TP possibles, on prend la meilleure priorité (plus petit chiffre).
"""
if chr_location not in tp_map:
return None
candidates = [tp for tp in tp_map[chr_location] if tp["con_id"] == con_id]
if not candidates:
return None
# Choisir la meilleure priorité
best = sorted(candidates, key=lambda t: t["priority"])[0]
return best
# ---------------------------------------------------------
# 3) LECTURE DE L'ÉTAT MOTEUR
# ---------------------------------------------------------
def get_last_processed_read_id(cur, engine_table: str) -> Optional[int]:
sql = f"SELECT last_processed_read_id FROM `{engine_table}` WHERE id = 1"
cur.execute(sql)
row = cur.fetchone()
return int(row["last_processed_read_id"]) if row else None
# ---------------------------------------------------------
# 4) LECTURES BRUTES
# ---------------------------------------------------------
def get_new_lectures(cur, last_processed: int):
"""Lit les nouvelles lectures à partir de rcourse_id."""
sql = """
SELECT rcourse_id, chrinfo_tag, chrinfo_time, chrinfo_location
FROM reader_lectures
WHERE rcourse_id > %s
ORDER BY rcourse_id ASC
LIMIT %s
"""
cur.execute(sql, (last_processed, BATCH_SIZE))
return cur.fetchall()
# ---------------------------------------------------------
# 5) MATCH TAG → COUREUR
# ---------------------------------------------------------
def get_classement_by_tag(cur, classement_table: str, tag: str) -> Optional[Dict]:
"""
Trouve le coureur correspondant à un tag RFID.
Retourne cla_info_dossard (ID officiel du coureur).
"""
pattern = f"%*{tag}*%"
sql = f"""
SELECT cla_info_dossard, con_id, cla_list_tag
FROM `{classement_table}`
WHERE cla_list_tag LIKE %s
LIMIT 1
"""
cur.execute(sql, (pattern,))
return cur.fetchone()
# ---------------------------------------------------------
# 6) ÉCRITURE DANS DETAIL
# ---------------------------------------------------------
def insert_detail_record(
cur,
cnx,
con_id: int,
dossard: str,
lecture: Dict,
tp_id: int,
tp_name: str,
controller_location: str,
priority: int,
):
"""
Insère une ligne brute dans resultv2_<con_id>_detail.
"""
detail_table = f"resultv2_{con_id}_detail"
sql = f"""
INSERT INTO `{detail_table}`
(con_id, tp_id, tp_name,
cla_info_dossard, rcourse_id,
timestamp_read, raw_tag,
reader_location, controller_priority,
source)
VALUES (%s, %s, %s,
%s, %s,
%s, %s,
%s, %s,
%s)
"""
data = (
con_id,
tp_id,
tp_name,
dossard,
lecture["rcourse_id"],
lecture["chrinfo_time"],
lecture["chrinfo_tag"],
controller_location,
priority,
"LIVE", # pour l'instant
)
cur.execute(sql, data)
cnx.commit()
# ---------------------------------------------------------
# 7) METTRE À JOUR LE POINTEUR
# ---------------------------------------------------------
def update_last_processed_read_id(cur, cnx, engine_table: str, new_value: int):
sql = f"""
UPDATE `{engine_table}`
SET last_processed_read_id = %s,
updated_at = NOW()
WHERE id = 1
"""
cur.execute(sql, (new_value,))
cnx.commit()
# ---------------------------------------------------------
# 8) TRAITER UNE COURSE
# ---------------------------------------------------------
def process_course(cur, cnx, con_id: int, tp_map: Dict[str, List[Dict]]):
"""Traite toutes les nouvelles lectures pour une course donnée."""
engine_table = f"resultv2_{con_id}_lecture"
classement_table = f"resultv2_{con_id}_classement"
last_processed = get_last_processed_read_id(cur, engine_table)
if last_processed is None:
print(f"[ERREUR] Pas de ligne id=1 dans {engine_table}.")
return
print(f"[INFO] CON_ID={con_id} last_processed_read_id={last_processed}")
lectures = get_new_lectures(cur, last_processed)
if not lectures:
print(f"[INFO] CON_ID={con_id} : aucune nouvelle lecture.")
return
max_rcourse_id = last_processed
for lecture in lectures:
rcourse_id = int(lecture["rcourse_id"])
tag = str(lecture["chrinfo_tag"])
chr_location = str(lecture["chrinfo_location"])
max_rcourse_id = max(max_rcourse_id, rcourse_id)
# 1) Trouver le coureur via TAG
coureur = get_classement_by_tag(cur, classement_table, tag)
if not coureur:
print(f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: tag {tag} introuvable dans classement")
continue
dossard = coureur["cla_info_dossard"]
# 2) Trouver le TP via chrinfo_location
tp_info = resolve_tp_for_lecture(tp_map, con_id, chr_location)
if not tp_info:
print(
f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: "
f"location {chr_location} introuvable dans config TP"
)
continue
tp_id = tp_info["tp_id"]
tp_name = tp_info["tp_name"]
priority = tp_info["priority"]
# DEBUG : affichage de ce qui est utilisé pour cette lecture
print(
f"[DEBUG] CON_ID={con_id} rcourse_id={rcourse_id} "
f"loc={chr_location} -> tp_id={tp_id} tp_name={tp_name} prio={priority}"
)
# 3) Écrire la ligne dans DETAIL
insert_detail_record(
cur,
cnx,
con_id,
dossard,
lecture,
tp_id,
tp_name,
chr_location,
priority,
)
print(
f"[OK] CON_ID={con_id} rcourse_id={rcourse_id} tag={tag} "
f"→ dossard={dossard}, tp={tp_name} (id={tp_id})"
)
update_last_processed_read_id(cur, cnx, engine_table, max_rcourse_id)
print(f"[OK] CON_ID={con_id} last_processed_read_id → {max_rcourse_id}")
# ---------------------------------------------------------
# 9) BOUCLE PRINCIPALE
# ---------------------------------------------------------
def run_once():
try:
cnx = get_connection()
cur = cnx.cursor(dictionary=True)
# Charger config TP/contrôleurs UNE FOIS
tp_map = load_tp_config(cur)
con_ids = get_active_con_ids(cur)
if not con_ids:
print("[INFO] Aucune course active.")
return
for con_id in con_ids:
process_course(cur, cnx, con_id, tp_map)
except Error as e:
print(f"[ERREUR MYSQL] {e}")
finally:
try:
cur.close()
cnx.close()
except:
pass
if __name__ == "__main__":
run_once()

View File

@ -0,0 +1,330 @@
"""
Moteur de résultats Version TP + DETAIL brut (patch final)
-----------------------------------------------------------
Étapes couvertes :
1) Lire les courses actives (reader_config).
2) Pour chaque course active :
- Charger la configuration TP / contrôleurs (seulement pour ce con_id !)
- Lire resultv2_<con_id>_lecture.last_processed_read_id
- Lire les nouvelles lectures dans reader_lectures
- Associer chaque lecture à un coureur (cla_list_tag)
- Associer chaque lecture à un TP (via chrinfo_location → config_detail)
- Écrire une ligne brute dans resultv2_<con_id>_detail
avec con_id, dossard, tp_id, tp_name, chrinfo_location, priorité,
controller_name, etc.
"""
from typing import Optional, Dict, List
from mysql.connector import Error
from engine.db import get_connection
BATCH_SIZE = 1000
# ---------------------------------------------------------
# 1) COURSES ACTIVES
# ---------------------------------------------------------
def get_active_con_ids(cur) -> List[int]:
"""Retourne tous les con_id actifs."""
sql = "SELECT con_id FROM reader_config WHERE rea_active = 1"
cur.execute(sql)
rows = cur.fetchall()
return [int(r["con_id"]) for r in rows]
# ---------------------------------------------------------
# 2) CHARGER LA CONFIG TP / CONTRÔLEURS (PAR COURSE)
# ---------------------------------------------------------
def load_tp_config_for_con(cur, con_id: int) -> Dict[str, List[Dict]]:
"""
Charge TOUTE la configuration TP pour UNE course (con_id).
Retourne un dict :
clé = chrinfo_location (ex: 'PRO5655')
valeur= liste de dicts { con_id, tp_id, tp_name, priority, controller_name }
"""
sql = """
SELECT
tp.tp_id,
tp.con_id,
tp.tp_name,
d.det_value1 AS chr_location,
d.det_value2 AS priority,
d.det_value3 AS controller_name
FROM reader_config_timingpoint tp
JOIN reader_config_detail d
ON d.det_type = 1
AND d.det_ref1_id = tp.tp_id
AND d.det_clef = 'clef_controleur'
WHERE tp.con_id = %s
"""
cur.execute(sql, (con_id,))
rows = cur.fetchall()
tp_map: Dict[str, List[Dict]] = {}
for row in rows:
loc = row["chr_location"]
if loc not in tp_map:
tp_map[loc] = []
try:
prio = int(row["priority"]) if row["priority"] is not None else 1
except ValueError:
prio = 1
tp_map[loc].append(
{
"con_id": int(row["con_id"]),
"tp_id": int(row["tp_id"]),
"tp_name": row["tp_name"],
"priority": prio,
"controller_name": row["controller_name"],
}
)
print(f"[DEBUG] TP config chargée pour CON_ID={con_id}:")
if not tp_map:
print(" (aucune ligne trouvée pour ce con_id)")
else:
for loc, items in tp_map.items():
parts = [
f"tp_id={tp['tp_id']} tp_name={tp['tp_name']} "
f"prio={tp['priority']} name={tp['controller_name']}"
for tp in items
]
print(f" location={loc} -> " + " | ".join(parts))
return tp_map
# ---------------------------------------------------------
# 3) RÉSOUDRE LE TP POUR UNE LECTURE
# ---------------------------------------------------------
def resolve_tp_for_lecture(tp_map: Dict[str, List[Dict]], con_id: int, chr_location: str) -> Optional[Dict]:
"""Trouve le TP applicable pour chrinfo_location avec ce con_id."""
if chr_location not in tp_map:
return None
candidates = [tp for tp in tp_map[chr_location] if tp["con_id"] == con_id]
if not candidates:
return None
return sorted(candidates, key=lambda t: t["priority"])[0]
# ---------------------------------------------------------
# 4) LECTURE DE L'ÉTAT MOTEUR
# ---------------------------------------------------------
def get_last_processed_read_id(cur, engine_table: str) -> Optional[int]:
sql = f"SELECT last_processed_read_id FROM `{engine_table}` WHERE id = 1"
cur.execute(sql)
row = cur.fetchone()
return int(row["last_processed_read_id"]) if row else None
# ---------------------------------------------------------
# 5) LECTURES BRUTES
# ---------------------------------------------------------
def get_new_lectures(cur, last_processed: int):
sql = """
SELECT rcourse_id, chrinfo_tag, chrinfo_time, chrinfo_location
FROM reader_lectures
WHERE rcourse_id > %s
ORDER BY rcourse_id ASC
LIMIT %s
"""
cur.execute(sql, (last_processed, BATCH_SIZE))
return cur.fetchall()
# ---------------------------------------------------------
# 6) MATCH TAG → COUREUR
# ---------------------------------------------------------
def get_classement_by_tag(cur, classement_table: str, tag: str) -> Optional[Dict]:
pattern = f"%*{tag}*%"
sql = f"""
SELECT cla_info_dossard, con_id, cla_list_tag
FROM `{classement_table}`
WHERE cla_list_tag LIKE %s
LIMIT 1
"""
cur.execute(sql, (pattern,))
return cur.fetchone()
# ---------------------------------------------------------
# 7) INSERT DANS DETAIL
# ---------------------------------------------------------
def insert_detail_record(
cur,
cnx,
con_id: int,
dossard: str,
lecture: Dict,
tp_id: int,
tp_name: str,
controller_location: str,
priority: int,
controller_name: str,
):
detail_table = f"resultv2_{con_id}_detail"
sql = f"""
INSERT INTO `{detail_table}`
(con_id, tp_id, tp_name,
cla_info_dossard, rcourse_id,
timestamp_read, raw_tag,
reader_location, controller_priority,
controller_name,
source)
VALUES (%s, %s, %s,
%s, %s,
%s, %s,
%s, %s,
%s,
%s)
"""
data = (
con_id,
tp_id,
tp_name,
dossard,
lecture["rcourse_id"],
str(lecture["chrinfo_time"]),
lecture["chrinfo_tag"],
controller_location,
priority,
controller_name,
"LIVE",
)
cur.execute(sql, data)
cnx.commit()
# ---------------------------------------------------------
# 8) METTRE À JOUR LE POINTEUR
# ---------------------------------------------------------
def update_last_processed_read_id(cur, cnx, engine_table: str, new_value: int):
sql = f"""
UPDATE `{engine_table}`
SET last_processed_read_id = %s,
updated_at = NOW()
WHERE id = 1
"""
cur.execute(sql, (new_value,))
cnx.commit()
# ---------------------------------------------------------
# 9) TRAITER UNE COURSE
# ---------------------------------------------------------
def process_course(cur, cnx, con_id: int):
engine_table = f"resultv2_{con_id}_lecture"
classement_table = f"resultv2_{con_id}_classement"
# CHARGER la config TP / contrôleurs **PAR course**
tp_map = load_tp_config_for_con(cur, con_id)
last_processed = get_last_processed_read_id(cur, engine_table)
if last_processed is None:
print(f"[ERREUR] Pas de ligne id=1 dans {engine_table}.")
return
print(f"[INFO] CON_ID={con_id} last_processed_read_id={last_processed}")
lectures = get_new_lectures(cur, last_processed)
if not lectures:
print(f"[INFO] CON_ID={con_id} : aucune nouvelle lecture.")
return
max_rcourse_id = last_processed
for lecture in lectures:
rcourse_id = int(lecture["rcourse_id"])
tag = str(lecture["chrinfo_tag"])
chr_location = str(lecture["chrinfo_location"])
max_rcourse_id = max(max_rcourse_id, rcourse_id)
# TAG → COUREUR
coureur = get_classement_by_tag(cur, classement_table, tag)
if not coureur:
print(f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: tag {tag} introuvable")
continue
dossard = coureur["cla_info_dossard"]
# LOCATION → TP
tp_info = resolve_tp_for_lecture(tp_map, con_id, chr_location)
if not tp_info:
print(
f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: "
f"location {chr_location} introuvable pour cette course"
)
continue
tp_id = tp_info["tp_id"]
tp_name = tp_info["tp_name"]
priority = tp_info["priority"]
controller_name = tp_info["controller_name"]
print(
f"[DEBUG] CON_ID={con_id} rcourse_id={rcourse_id} "
f"loc={chr_location} -> tp_id={tp_id} tp_name={tp_name} "
f"prio={priority} name={controller_name}"
)
# INSERT DETAIL
insert_detail_record(
cur,
cnx,
con_id,
dossard,
lecture,
tp_id,
tp_name,
chr_location,
priority,
controller_name,
)
print(
f"[OK] CON_ID={con_id} rcourse_id={rcourse_id} "
f"tag={tag} → dossard={dossard}, tp={tp_name} (id={tp_id})"
)
update_last_processed_read_id(cur, cnx, engine_table, max_rcourse_id)
print(f"[OK] CON_ID={con_id} last_processed_read_id → {max_rcourse_id}")
# ---------------------------------------------------------
# 10) BOUCLE PRINCIPALE
# ---------------------------------------------------------
def run_once():
try:
cnx = get_connection()
cur = cnx.cursor(dictionary=True)
con_ids = get_active_con_ids(cur)
if not con_ids:
print("[INFO] Aucune course active.")
return
for con_id in con_ids:
process_course(cur, cnx, con_id)
except Error as e:
print(f"[ERREUR MYSQL] {e}")
finally:
try:
cur.close()
cnx.close()
except:
pass
if __name__ == "__main__":
run_once()

View File

@ -0,0 +1,2 @@
a valider
attention sur le serveur mysql le % devrais enlever

0
engine/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

124
engine/config.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

1
engine/constants.py Normal file
View File

@ -0,0 +1 @@
BATCH_SIZE = 1000

74
engine/db.py Normal file
View File

@ -0,0 +1,74 @@
"""
db.py
======
Module de gestion de la connexion MySQL pour le moteur de résultats.
Ce module est séparé pour éviter de dupliquer la logique de connexion
dans plusieurs fichiers Python. Si plus tard tu veux réutiliser cette
connexion dans d'autres scripts, tu n'auras qu'à importer get_connection().
"""
import mysql.connector
from mysql.connector import Error
# ---------------------------------------------------------------------------
# CONFIGURATION DE LA BASE DE DONNÉES
# ---------------------------------------------------------------------------
# ⚠ IMPORTANT :
# - Adapte ces valeurs à TON environnement (dev / prod).
# - Idéalement, tu peux plus tard les lire à partir d'un fichier
# (ex: info_serveur.txt) pour éviter de hardcoder les mots de passe.
# ---------------------------------------------------------------------------
DB_CONFIG = {
"host": "serveur-prod.progiweb.net",
"user": "ms1reader_bd", # ajuste au besoin
"password": "X~=v]lg-l]4,T2FF", # ajuste au besoin
"database": "ms1reader_centrale",
"port": 3306
}
TABLE_NAME = "reader_lectures"
def get_connection():
"""
Crée et retourne une connexion MySQL.
Returns
-------
mysql.connector.connection_cext.CMySQLConnection
Connexion déjà ouverte vers la base configurée dans DB_CONFIG.
Raises
------
mysql.connector.Error
Si la connexion échoue (mauvais host, user, password, etc.).
"""
return mysql.connector.connect(**DB_CONFIG)
def test_connection():
"""
Petit utilitaire pour tester rapidement la connexion.
Tu peux exécuter ce fichier directement :
python -m engine.db
"""
try:
cnx = get_connection()
if cnx.is_connected():
print("[OK] Connexion MySQL réussie.")
else:
print("[ERREUR] Connexion MySQL NON établie.")
except Error as e:
print(f"[ERREUR] Mysql : {e}")
finally:
try:
if cnx and cnx.is_connected():
cnx.close()
except NameError:
# cnx n'existe pas si la connexion a échoué avant son affectation
pass
if __name__ == "__main__":
test_connection()

33
engine/engine.py Normal file
View File

@ -0,0 +1,33 @@
from mysql.connector import Error
from engine.db import get_connection
from engine.engine_components.active_courses import get_active_con_ids
from engine.engine_components.process_course import process_course
from engine.logger import log_info, log_debug, log_warn, log_error
def run_once():
try:
cnx = get_connection()
cur = cnx.cursor(dictionary=True)
con_ids = get_active_con_ids(cur)
if not con_ids:
log_info("[INFO] Aucune course active.")
return
for con_id in con_ids:
process_course(cur, cnx, con_id)
except Error as e:
log_error(f"[ERREUR MYSQL] {e}")
finally:
try:
cur.close()
cnx.close()
except:
pass
if __name__ == "__main__":
run_once()

View File

View File

@ -0,0 +1,7 @@
from typing import List
def get_active_con_ids(cur) -> List[int]:
sql = "SELECT con_id FROM reader_config WHERE rea_active = 1"
cur.execute(sql)
rows = cur.fetchall()
return [int(r["con_id"]) for r in rows]

View File

@ -0,0 +1,12 @@
from typing import Optional, Dict
def get_classement_by_tag(cur, classement_table: str, tag: str) -> Optional[Dict]:
pattern = f"%*{tag}*%"
sql = f"""
SELECT cla_info_dossard, con_id, cla_list_tag
FROM `{classement_table}`
WHERE cla_list_tag LIKE %s
LIMIT 1
"""
cur.execute(sql, (pattern,))
return cur.fetchone()

View File

@ -0,0 +1,57 @@
# engine/engine_components/course_config.py
from datetime import datetime
import pytz
from engine.logger import log_debug, log_warn
LOCAL_TZ = "America/Toronto"
def load_course_config(cur, con_id: int):
"""
Charge toutes les configs det_type = 2 (config de course)
et retourne start_time / end_time convertis en datetime LOCAL.
"""
sql = """
SELECT det_clef, det_value1
FROM reader_config_detail
WHERE det_type = 2
AND det_ref1_id = %s
"""
cur.execute(sql, (con_id,))
rows = cur.fetchall()
cfg = {}
for row in rows:
clef = row["det_clef"]
val = row["det_value1"]
cfg[clef] = val
tz = pytz.timezone(LOCAL_TZ)
start = None
end = None
# Convertir start_time si présent
if "clef_start_time" in cfg:
try:
start = tz.localize(datetime.fromtimestamp(int(cfg["clef_start_time"])))
except Exception as e:
log_warn(f"[CONFIG][CON {con_id}] start_time invalide: {e}")
# Convertir fin_time si présent
if "clef_fin_time" in cfg:
try:
end = tz.localize(datetime.fromtimestamp(int(cfg["clef_fin_time"])))
except Exception as e:
log_warn(f"[CONFIG][CON {con_id}] end_time invalide: {e}")
log_debug(f"[CONFIG COURSE] con_id={con_id} start={start} end={end}")
return {
"start_time": start,
"end_time": end,
"raw": cfg
}

View File

@ -0,0 +1,85 @@
def insert_detail_record(
cur,
cnx,
con_id: int,
dossard: str,
lecture: dict,
tp_id: int,
tp_name: str,
controller_location: str,
priority: int,
controller_name: str,
dt_local,
status=1,
reason_code=None,
reason_ref_id=None,
tour=None,
tempstour=None,
tempstour_str=None # <-- AJOUT OBLIGATOIRE
):
if isinstance(dt_local, tuple):
dt_local = dt_local[1]
detail_table = f"resultv2_{con_id}_detail"
sql = f"""
INSERT INTO `{detail_table}`
(con_id, tp_id, tp_name,
cla_info_dossard, rcourse_id,
timestamp_read,
timestamp_read_local,
timestamp_read_local_str,
raw_tag,
reader_location, controller_priority,
controller_name,
source,
status, reason_code, reason_ref_id,
tour, TempsTour, tempstour_str)
VALUES (%s, %s, %s,
%s, %s,
%s,
%s,
%s,
%s,
%s, %s,
%s,
%s,
%s, %s, %s,
%s, %s, %s)
"""
timestamp_read_local_unix = f"{dt_local.timestamp():.2f}"
timestamp_read_local_str = (
f"{dt_local.strftime('%Y-%m-%d %H:%M:%S')}."
f"{dt_local.microsecond // 10000:02d}"
)
if isinstance(tempstour, tuple):
tempstour = None
data = (
con_id,
tp_id,
tp_name,
dossard,
lecture["rcourse_id"],
lecture["chrinfo_time"],
timestamp_read_local_unix,
timestamp_read_local_str,
lecture["chrinfo_tag"],
controller_location,
priority,
controller_name,
"LIVE",
status,
reason_code,
reason_ref_id,
tour,
tempstour,
tempstour_str
)
cur.execute(sql, data)
cnx.commit()

View File

@ -0,0 +1,16 @@
def get_last_processed_read_id(cur, engine_table: str):
sql = f"SELECT last_processed_read_id FROM `{engine_table}` WHERE id = 1"
cur.execute(sql)
row = cur.fetchone()
return int(row["last_processed_read_id"]) if row else None
def update_last_processed_read_id(cur, cnx, engine_table: str, new_value: int):
sql = f"""
UPDATE `{engine_table}`
SET last_processed_read_id = %s,
updated_at = NOW()
WHERE id = 1
"""
cur.execute(sql, (new_value,))
cnx.commit()

View File

@ -0,0 +1,12 @@
from engine.constants import BATCH_SIZE
def get_new_lectures(cur, last_processed: int):
sql = """
SELECT rcourse_id, chrinfo_tag, chrinfo_time, chrinfo_location
FROM reader_lectures
WHERE rcourse_id > %s
ORDER BY rcourse_id ASC
LIMIT %s
"""
cur.execute(sql, (last_processed, BATCH_SIZE))
return cur.fetchall()

View File

@ -0,0 +1,197 @@
from engine.engine_components.tp_config import load_tp_config_for_con, resolve_tp_for_lecture
from engine.engine_components.course_config import load_course_config
from engine.engine_components.engine_state import get_last_processed_read_id, update_last_processed_read_id
from engine.engine_components.lectures import get_new_lectures
from engine.engine_components.classement import get_classement_by_tag
from engine.engine_components.detail_writer import insert_detail_record
from engine.logger import log_info, log_debug
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from decimal import Decimal
from engine.utils import format_temps_decimal
LOCAL_TZ = ZoneInfo("America/Toronto")
_last_valid_read = {} # (tp_id, dossard) -> datetime locale
def ts_to_utc(ts):
ts = Decimal(str(ts))
seconds = int(ts)
micro = int((ts - Decimal(seconds)) * Decimal(1_000_000))
return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(microsecond=micro)
def _normalize_course_time(dt):
if dt is None:
return None, None
dt_utc = dt.replace(tzinfo=timezone.utc)
dt_local = dt_utc.astimezone(LOCAL_TZ)
return dt_local, dt_utc
def process_course(cur, cnx, con_id: int):
tp_config = load_tp_config_for_con(cur, con_id)
course_cfg = load_course_config(cur, con_id)
raw_start = course_cfg["start_time"]
raw_end = course_cfg["end_time"]
start_local, start_utc = _normalize_course_time(raw_start)
end_local, end_utc = _normalize_course_time(raw_end)
engine_table = f"resultv2_{con_id}_lecture"
classement_table = f"resultv2_{con_id}_classement"
# last_processed
last_processed = get_last_processed_read_id(cur, engine_table)
if last_processed is None:
last_processed = 0
log_info(f"[STATE] con_id={con_id} last_processed={last_processed}")
lectures = get_new_lectures(cur, last_processed)
log_info(f"[NEW LECTURES] con_id={con_id} count={len(lectures)}")
if not lectures:
return
max_id = last_processed
for lec in lectures:
rcourse_id = int(lec["rcourse_id"])
chr_loc = lec["chrinfo_location"]
timestamp = Decimal(lec["chrinfo_time"])
max_id = max(max_id, rcourse_id)
dt_utc = ts_to_utc(timestamp)
dt_local = dt_utc.astimezone(LOCAL_TZ)
log_debug(f"[READ] id={rcourse_id} utc={dt_utc} loc={chr_loc}")
# 1) start/end
if start_utc and dt_utc < start_utc:
log_debug(f"[REJECT START] id={rcourse_id} dt_utc={dt_utc} < start_utc={start_utc}")
continue
if end_utc and dt_utc > end_utc:
log_debug(f"[REJECT END] id={rcourse_id} dt_utc={dt_utc} > end_utc={end_utc}")
continue
status = 1
reason_code = None
reason_ref_id = None
# 2) coureur (BIB doit exister dans le classement)
tag = lec["chrinfo_tag"]
coureur = get_classement_by_tag(cur, classement_table, tag)
if not coureur:
# IMPORTANT : on ignore complètement
continue
dossard = coureur["cla_info_dossard"]
# 3) TP obligatoire
tp_info = resolve_tp_for_lecture(tp_config, con_id, chr_loc)
if not tp_info:
continue
tp_id = tp_info["tp_id"]
tp_name = tp_info["tp_name"]
prio = tp_info["priority"]
controller = tp_info["controller_name"]
det_id = tp_info["det_id"]
# 4) interval rule
interval_cfg = (
tp_config[tp_id]["config"]
.get("config_tp_interval", {})
.get("clef_temp_minimum_read_entre", [])
)
interval_sec = 0
if interval_cfg:
try:
interval_sec = int(interval_cfg[0]["value1"])
except:
interval_sec = 0
last_dt = _last_valid_read.get((tp_id, dossard))
if last_dt and interval_sec > 0:
delta = (dt_local - last_dt).total_seconds()
if delta < interval_sec:
status = 0
reason_code = "SKIP_INTERVAL"
reason_ref_id = det_id
log_debug(f"[SKIP INTERVAL] id={rcourse_id} {delta:.1f}s < {interval_sec}s")
# 5) INSERT
detail_table = f"resultv2_{con_id}_detail"
# 5) tours + tempstour
tour, prev_ts = compute_tour(cur, detail_table, tp_id, dossard)
if prev_ts is None:
tempstour = Decimal("0")
else:
tempstour = timestamp - prev_ts
tempstour_str = format_temps_decimal(tempstour)
insert_detail_record(
cur, cnx, con_id,
dossard, lec,
tp_id, tp_name,
chr_loc, prio, controller,
dt_local,
status=status,
reason_code=reason_code,
reason_ref_id=reason_ref_id,
tour=tour,
tempstour=tempstour,
tempstour_str=tempstour_str
)
# Mise à jour du dernier read VALIDE
if status == 1:
_last_valid_read[(tp_id, dossard)] = dt_local
log_info(f"[OK] id={rcourse_id} tag={tag} dossard={dossard} tp={tp_name} prio={prio}")
update_last_processed_read_id(cur, cnx, engine_table, max_id)
def compute_tour(cur, detail_table, tp_id, dossard):
"""
Retourne (tour, prev_timestamp_decimal)
- tour = dernier_tour + 1
- prev_timestamp_decimal = timestamp_read du tour précédent (Decimal) ou None si premier tour
"""
cur.execute(f"""
SELECT tour, timestamp_read
FROM `{detail_table}`
WHERE tp_id = %s
AND cla_info_dossard = %s
AND status = 1
ORDER BY detail_id DESC
LIMIT 1
""", (tp_id, dossard))
row = cur.fetchone()
# Première lecture → tour 1, pas de temps précédent
if not row or row["tour"] is None:
return 1, None
last_tour = row["tour"]
prev_ts = Decimal(str(row["timestamp_read"]))
return last_tour + 1, prev_ts

View File

@ -0,0 +1,162 @@
from typing import Dict, Any, List
from engine.logger import log_debug, log_info
def load_tp_config_for_con(cur, con_id: int) -> Dict[int, Dict[str, Dict[str, List[Dict[str, Any]]]]]:
"""
Charge TOUTE la configuration TP (det_type = 1) pour un con_id.
Structure retournée :
{
tp_id: {
"tp_name": "...",
"config": {
"config_tp": {
"clef_controleur": [
{ det_id, value1, value2, value3 }
],
"clef_temp_read": [
...
]
},
"config_tp_interval": {
"clef_temp_minimum_read_entre": [
{ det_id, value1, value2, value3 }
]
}
}
}
}
"""
# 1) Lire la liste des TP pour ce con_id
sql_tp = """
SELECT tp_id, tp_name
FROM reader_config_timingpoint
WHERE con_id = %s
"""
cur.execute(sql_tp, (con_id,))
tps = cur.fetchall()
if not tps:
log_info(f"[CONFIG] Aucun TP trouvé pour con_id={con_id}")
return {}
# Préparer structure finale
tp_config: Dict[int, Dict] = {}
for tp in tps:
tp_id = tp["tp_id"]
tp_config[tp_id] = {
"tp_name": tp["tp_name"],
"config": {}
}
# 2) Lire toutes les configs det_type = 1 pour ce con_id
sql_conf = """
SELECT
d.det_id,
d.det_ref1_id AS tp_id,
d.det_groupe,
d.det_clef,
d.det_value1,
d.det_value2,
d.det_value3
FROM reader_config_detail d
JOIN reader_config_timingpoint tp ON tp.tp_id = d.det_ref1_id
WHERE tp.con_id = %s
AND d.det_type = 1
ORDER BY d.det_groupe, d.det_clef, d.det_id
"""
cur.execute(sql_conf, (con_id,))
rows = cur.fetchall()
# 3) Assembler
for row in rows:
tp_id = row["tp_id"]
group = row["det_groupe"] or "default"
clef = row["det_clef"]
if group not in tp_config[tp_id]["config"]:
tp_config[tp_id]["config"][group] = {}
if clef not in tp_config[tp_id]["config"][group]:
tp_config[tp_id]["config"][group][clef] = []
tp_config[tp_id]["config"][group][clef].append({
"det_id": row["det_id"],
"value1": row["det_value1"],
"value2": row["det_value2"],
"value3": row["det_value3"],
})
# 4) Affichage debug clair
log_info(f"[CONFIG] Chargé config TP pour con_id={con_id}")
for tp_id, data in tp_config.items():
log_info(f" TP {tp_id} ({data['tp_name']})")
for group, clefs in data["config"].items():
log_debug(f" [{group}]")
for clef, items in clefs.items():
for it in items:
log_debug(
f" {clef}: det_id={it['det_id']} "
f"v1={it['value1']} v2={it['value2']} v3={it['value3']}"
)
return tp_config
def resolve_tp_for_lecture(tp_config, con_id: int, chr_location: str):
"""
Retourne l'entrée TP correspondant à la location lue.
On cherche dans les configs TP la clef :
config_tp → clef_controleur → value1 = chr_location
On choisit automatiquement :
- la priorité la plus basse (1 est meilleur que 2)
- si ties : on prend la première
Retourne un dict complet :
{
"tp_id": ...,
"tp_name": "...",
"priority": ...,
"controller_name": "...",
"det_id": ...
}
"""
best = None
for tp_id, data in tp_config.items():
# Toute config pour ce timing point
groups = data.get("config", {})
# Groupe config_tp (celui des contrôleurs)
cfg_tp = groups.get("config_tp", {})
# Liste des clefs clef_controleur
controllers = cfg_tp.get("clef_controleur", [])
for ctrl in controllers:
if ctrl["value1"] != chr_location:
continue
prio = int(ctrl["value2"]) if ctrl["value2"] else 999
item = {
"tp_id": tp_id,
"tp_name": data["tp_name"],
"priority": prio,
"controller_name": ctrl["value3"],
"det_id": ctrl["det_id"],
}
# Choisir le meilleur contrôleur (prio la plus petite)
if best is None or prio < best["priority"]:
best = item
return best

39
engine/info.txt Normal file
View File

@ -0,0 +1,39 @@
a valider
attention sur le serveur mysql le % devrais enlever
config
TABLE reader_config_detail — RÈGLES GÉNÉRALES
---------------------------------------------
1) det_type
- 1 = règles associées aux Timing Points (TP)
- 2 = règles associées aux Courses
=> Toujours utiliser det_type pour savoir si la règle appartient à un TP ou à une course.
2) det_ref1_id
- Pour det_type = 1 → det_ref1_id = tp_id
- Pour det_type = 2 → det_ref1_id = con_id
=> Identifie exactement à quel TP ou à quelle course la règle appartient.
3) det_groupe
- Sert uniquement au regroupement logique / visuel.
- NE DOIT PAS être utilisé pour la logique du moteur.
=> Le moteur utilise seulement det_type + det_ref1_id + det_clef.
4) det_clef
- Nom symbolique de la règle (ex: clef_controleur, clef_temp_minimum_read_entre, clef_start_time).
5) det_value1, det_value2, det_value3
- Contiennent les valeurs de la règle.
- Le moteur doit toujours accepter 1, 2 ou 3 valeurs selon le type de règle.
6) det_trie
- Indique l'ordre de priorité entre plusieurs règles d'un même type pour un même TP ou course.
- Le moteur doit TOUJOURS trier par det_trie ASC avant d'interpréter.
=> Une règle avec det_trie = 1 est appliquée avant une règle avec det_trie = 10.
7) Source unique
- Toutes les informations de configuration (TP et course) doivent provenir de reader_config_detail.
- Aucune autre source de règle ne doit être utilisée dans le moteur.

21
engine/logger.py Normal file
View File

@ -0,0 +1,21 @@
import sys
# ENGINE_MODE = "DEV" ou "PROD"
ENGINE_MODE = "DEV" # change à PROD sur ton serveur
def log_debug(msg: str):
if ENGINE_MODE == "DEV":
print(f"[DEBUG] {msg}")
def log_info(msg: str):
if ENGINE_MODE == "DEV":
print(f"[INFO] {msg}")
def log_warn(msg: str):
# On affiche les warnings en DEV mais pas en PROD
if ENGINE_MODE == "DEV":
print(f"[WARN] {msg}")
def log_error(msg: str):
# Toujours afficher les erreurs, même en production
print(f"[ERROR] {msg}", file=sys.stderr)

16
engine/utils.py Normal file
View File

@ -0,0 +1,16 @@
from decimal import Decimal
def format_temps_decimal(d):
"""
Formatte un Decimal représentant un nombre de secondes
en HH:MM:SS.mmm — exemple : 00:06:52.820
"""
d = Decimal(d)
total_sec = float(d)
hours = int(total_sec // 3600)
minutes = int((total_sec % 3600) // 60)
seconds = total_sec % 60
return f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"

Binary file not shown.

View File

@ -0,0 +1,20 @@
# engine/utils/time_utils.py
from datetime import datetime, timezone, timedelta
import pytz
LOCAL_TZ = pytz.timezone("America/Toronto")
def utc_ts_to_local_datetime(ts: float):
"""Timestamp UNIX (UTC réel) → datetime locale America/Toronto."""
return datetime.fromtimestamp(ts, timezone.utc).astimezone(LOCAL_TZ)
def utc_ts_to_utc_datetime(ts: float):
"""Timestamp UNIX (UTC réel) → datetime UTC."""
return datetime.fromtimestamp(ts, timezone.utc)
def local_datetime_to_utc(dt):
"""Datetime locale → UTC."""
if dt is None:
return None
return dt.astimezone(timezone.utc)

125
info_serveur.txt Normal file
View File

@ -0,0 +1,125 @@
=====================================================
DOCUMENTATION — Installation & Gestion du service ms1_reader
=====================================================
1. Fichier wrapper stable : run.sh
Chemin :
/home/reader/udp_server/run.sh
Contenu du fichier :
#!/bin/bash
/usr/bin/python3 /home/reader/udp_server/chronotrack_server_au.py
Commande pour rendre exécutable :
chmod +x /home/reader/udp_server/run.sh
2. Installation des dépendances Python pour le service
Installer pip3 :
sudo dnf install python3-pip -y
Installer le module MySQL utilisé par le script :
sudo pip3 install mysql-connector-python
3. Fichier systemd : ms1_reader.service
Chemin :
/etc/systemd/system/ms1_reader.service
Contenu du service :
[Unit]
Description=MS1 Reader - Service Python
After=network.target
[Service]
User=reader
WorkingDirectory=/home/reader/udp_server
ExecStart=/home/reader/udp_server/run.sh
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
4. Commandes systemd obligatoires après installation
Recharger les fichiers systemd :
sudo systemctl daemon-reload
Activer le service au démarrage :
sudo systemctl enable ms1_reader.service
Démarrer le service :
sudo systemctl start ms1_reader.service
Vérifier létat :
sudo systemctl status ms1_reader.service
=====================================================
SECTION : GESTION ET DIAGNOSTICS DU SERVICE
=====================================================
5. Commandes de gestion
Arrêter :
sudo systemctl stop ms1_reader.service
Redémarrer :
sudo systemctl restart ms1_reader.service
Redémarrer + voir immédiatement le status :
sudo systemctl restart ms1_reader.service && sudo systemctl status ms1_reader.service
Désactiver le démarrage automatique :
sudo systemctl disable ms1_reader.service
6. Journalisation & logs
Voir les 50 dernières lignes :
sudo journalctl -u ms1_reader.service -n 50 --no-pager
Voir les logs en temps réel (streaming) :
sudo journalctl -u ms1_reader.service -f
Voir tous les logs depuis le début :
sudo journalctl -u ms1_reader.service --no-pager
Filtrer par date :
sudo journalctl -u ms1_reader.service --since "2025-12-01" --until "2025-12-02"
Effacer les logs journald (si nécessaire) :
sudo journalctl --rotate
sudo journalctl --vacuum-time=2d
7. Vérifications essentielles
Tester si le service tourne :
sudo systemctl is-active ms1_reader.service
Tester si le service est configuré pour démarrer au boot :
sudo systemctl is-enabled ms1_reader.service
Voir le PID du service :
systemctl show -p MainPID ms1_reader.service
Voir la mémoire utilisée par le service :
systemctl status ms1_reader.service
(la ligne "Memory:" affiche la valeur)
8. Dépannage rapide
Si le service plante immédiatement :
journalctl -u ms1_reader.service -n 30 --no-pager
Si le script ne démarre pas :
• vérifier que run.sh est exécutable
chmod +x /home/reader/udp_server/run.sh
• vérifier que le chemin vers le script Python est correct
/home/reader/udp_server/chronotrack_server_au.py
• vérifier les permissions du dossier :
ls -ld /home/reader/udp_server
• vérifier que le module MySQL est installé globalement
pip3 show mysql-connector-python

119
mysql_writer.py Normal file
View File

@ -0,0 +1,119 @@
# mysql_writer.py
import mysql.connector
from mysql.connector import Error
import threading
# =============================
# CONFIGURATION DE LA BD
# =============================
DB_CONFIG = {
"host": "serveur-prod.progiweb.net",
"user": "ms1reader_bd", # ajuste au besoin
"password": "X~=v]lg-l]4,T2FF", # ajuste au besoin
"database": "ms1reader_centrale"
}
TABLE_NAME = "reader_lectures"
# =============================
# CONNEXION PERSISTANTE
# =============================
_conn = None
_lock = threading.Lock()
def _get_connection():
"""Retourne une connexion MySQL persistante, reconnecte si nécessaire."""
global _conn
if _conn is None or not _conn.is_connected():
_conn = mysql.connector.connect(**DB_CONFIG)
return _conn
def write_chrinfo(row: dict):
insert_sql = f"""
INSERT IGNORE INTO `{TABLE_NAME}` (
chrinfo_feed_ip,
chrinfo_feed_port,
chrinfo_event_id,
chrinfo_event_name,
chrinfo_event_desc,
chrinfo_type_transaction,
chrinfo_format_id,
chrinfo_sequence,
chrinfo_location,
chrinfo_tag,
chrinfo_time,
chrinfo_time_iso,
chrinfo_gatornumber,
chrinfo_readerid,
chrinfo_lap,
chrinfo_received_at_iso
) VALUES (
%(chrinfo_feed_ip)s,
%(chrinfo_feed_port)s,
%(chrinfo_event_id)s,
%(chrinfo_event_name)s,
%(chrinfo_event_desc)s,
%(chrinfo_type_transaction)s,
%(chrinfo_format_id)s,
%(chrinfo_sequence)s,
%(chrinfo_location)s,
%(chrinfo_tag)s,
%(chrinfo_time)s,
%(chrinfo_time_iso)s,
%(chrinfo_gatornumber)s,
%(chrinfo_readerid)s,
%(chrinfo_lap)s,
%(chrinfo_received_at_iso)s
)
"""
with _lock:
cursor = None
try:
conn = _get_connection()
cursor = conn.cursor()
# 2) Pas de doublon → on insère
cursor.execute(insert_sql, row)
conn.commit()
except Error as e:
print(f"❌ Erreur MySQL: {e}")
except Exception as e:
print(f"❌ Erreur write_chrinfo: {e}")
finally:
if cursor is not None:
cursor.close()
def write_log(log_type: str, message: str):
"""
Écrit une ligne dans la table log.
Aucun impact sur la logique existante.
"""
sql = """
INSERT INTO log (log_created_at, log_type, log_message)
VALUES (NOW(), %s, %s)
"""
print( sql)
with _lock:
cursor = None
try:
conn = _get_connection()
cursor = conn.cursor()
cursor.execute(sql, (log_type, message))
conn.commit()
except Exception as e:
print(f"❌ Erreur MySQL (write_log): {e}")
finally:
if cursor:
cursor.close()