Files
ms1reader-engine/engine - 20251204am/engine_loop v1.py
2025-12-09 11:27:02 -05:00

342 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()