Files
ms1reader-engine/chronotrack_server_au.py
2025-12-09 11:27:02 -05:00

403 lines
12 KiB
Python

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