temptours+tours
This commit is contained in:
BIN
__pycache__/mysql_writer.cpython-313.pyc
Normal file
BIN
__pycache__/mysql_writer.cpython-313.pyc
Normal file
Binary file not shown.
16023
chronotrack.log
Normal file
16023
chronotrack.log
Normal file
File diff suppressed because it is too large
Load Diff
402
chronotrack_server_au.py
Normal file
402
chronotrack_server_au.py
Normal 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()
|
||||
0
engine - 20251204am/__init__.py
Normal file
0
engine - 20251204am/__init__.py
Normal file
BIN
engine - 20251204am/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
engine - 20251204am/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine - 20251204am/__pycache__/db.cpython-313.pyc
Normal file
BIN
engine - 20251204am/__pycache__/db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine - 20251204am/__pycache__/engine_loop.cpython-313.pyc
Normal file
BIN
engine - 20251204am/__pycache__/engine_loop.cpython-313.pyc
Normal file
Binary file not shown.
124
engine - 20251204am/config.svg
Normal file
124
engine - 20251204am/config.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 92 KiB |
74
engine - 20251204am/db.py
Normal file
74
engine - 20251204am/db.py
Normal 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()
|
||||
341
engine - 20251204am/engine_loop v1.py
Normal file
341
engine - 20251204am/engine_loop v1.py
Normal 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()
|
||||
330
engine - 20251204am/engine_loop.py
Normal file
330
engine - 20251204am/engine_loop.py
Normal 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()
|
||||
2
engine - 20251204am/info.txt
Normal file
2
engine - 20251204am/info.txt
Normal file
@ -0,0 +1,2 @@
|
||||
a valider
|
||||
attention sur le serveur mysql le % devrais enlever
|
||||
0
engine/__init__.py
Normal file
0
engine/__init__.py
Normal file
BIN
engine/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
engine/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/constants.cpython-313.pyc
Normal file
BIN
engine/__pycache__/constants.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/db.cpython-313.pyc
Normal file
BIN
engine/__pycache__/db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/engine.cpython-313.pyc
Normal file
BIN
engine/__pycache__/engine.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/engine_loop.cpython-313.pyc
Normal file
BIN
engine/__pycache__/engine_loop.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/logger.cpython-313.pyc
Normal file
BIN
engine/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/utils.cpython-313.pyc
Normal file
BIN
engine/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
124
engine/config.svg
Normal file
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
1
engine/constants.py
Normal file
@ -0,0 +1 @@
|
||||
BATCH_SIZE = 1000
|
||||
74
engine/db.py
Normal file
74
engine/db.py
Normal 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
33
engine/engine.py
Normal 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()
|
||||
0
engine/engine_components/__init__.py
Normal file
0
engine/engine_components/__init__.py
Normal file
BIN
engine/engine_components/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
engine/engine_components/__pycache__/classement.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/classement.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
engine/engine_components/__pycache__/lectures.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/lectures.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
engine/engine_components/__pycache__/tp_config.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/tp_config.cpython-313.pyc
Normal file
Binary file not shown.
7
engine/engine_components/active_courses.py
Normal file
7
engine/engine_components/active_courses.py
Normal 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]
|
||||
12
engine/engine_components/classement.py
Normal file
12
engine/engine_components/classement.py
Normal 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()
|
||||
57
engine/engine_components/course_config.py
Normal file
57
engine/engine_components/course_config.py
Normal 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
|
||||
}
|
||||
85
engine/engine_components/detail_writer.py
Normal file
85
engine/engine_components/detail_writer.py
Normal 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()
|
||||
16
engine/engine_components/engine_state.py
Normal file
16
engine/engine_components/engine_state.py
Normal 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()
|
||||
12
engine/engine_components/lectures.py
Normal file
12
engine/engine_components/lectures.py
Normal 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()
|
||||
197
engine/engine_components/process_course.py
Normal file
197
engine/engine_components/process_course.py
Normal 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
|
||||
162
engine/engine_components/tp_config.py
Normal file
162
engine/engine_components/tp_config.py
Normal 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
39
engine/info.txt
Normal 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
21
engine/logger.py
Normal 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
16
engine/utils.py
Normal 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}"
|
||||
BIN
engine/utils/__pycache__/time_utils.cpython-313.pyc
Normal file
BIN
engine/utils/__pycache__/time_utils.cpython-313.pyc
Normal file
Binary file not shown.
20
engine/utils/time_utils.py
Normal file
20
engine/utils/time_utils.py
Normal 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
125
info_serveur.txt
Normal 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
119
mysql_writer.py
Normal 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()
|
||||
Reference in New Issue
Block a user