403 lines
12 KiB
Python
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()
|