#!/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()