diff --git a/timer NATA Kazan.py b/timer NATA Kazan.py index d7e26bb..24857b2 100644 --- a/timer NATA Kazan.py +++ b/timer NATA Kazan.py @@ -7,15 +7,52 @@ import os import time import binascii import requests - +import ast +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry HOST = "192.168.127.254" PORT = 1993 -PATH = ( - r"D:\ГРАФИКА\БАСКЕТБОЛ\ЕДИНАЯ ЛИГА ВТБ 2022-2023\python\JSON\timer_basketball.json" -) +# --- vMix --- +_VMIX_URL = "http://127.0.0.1:8088/API/" +_VMIX_INPUT = "SCOREBUG" # или твой номер/имя входа, если жёстко задан +_VMIX_COOLDOWN = 5.0 +_vmix_suppress_until = 0.0 +_vmix_online = None # None/True/False +_vmix_state = { # сюда складываем последнее известное состояние + "TIMER.Text": None, + "24sec.Text": None, + "SCORE1.Text": None, + "SCORE2.Text": None, + # при желании добавь картинки и др. поля: + # "24SECBACKGROUND.Source": None, +} + +# --- MOXA reconnect/watchdog --- +_SOCK_RECV_TIMEOUT = 2.0 # socket recv() timeout +_IDLE_RECONNECT = 5.0 # если нет данных столько сек — реконнект +_RETRY_BACKOFF = 1.0 +_RETRY_BACKOFF_MAX = 10.0 +_last_rx_mono = 0.0 +_attack_last = None # последнее принятое значение в секундах (float) +_attack_last_t = 0.0 # когда его приняли (monotonic) +_attack_off_t = -1.0 # когда видели OFF (monotonic) +_ATTACK_OFF_GRACE = 0.8 # окно (сек) после OFF: первое новое значение принимаем всегда +_ATTACK_UP_GLITCH = 0.3 # если выросло больше чем на 0.3 c без OFF — игнорируем +# --- анти-дребезг основного таймера --- +_hold_zero_active = False # сейчас удерживаем "0.0" +_hold_zero_t0 = 0.0 # момент начала удержания +_HOLD_ZERO_SEC = 1.0 # сколько секунд держать "0.0" (можно 0.8–1.5) +_last_timer_text = None # последнее отправленное значение + + session = requests.Session() +adapter = HTTPAdapter( + max_retries=Retry(total=0, connect=0, read=0, redirect=0, status=0) +) +session.mount("http://", adapter) +session.mount("https://", adapter) session.headers.update({"Connection": "keep-alive"}) @@ -23,141 +60,378 @@ def hexspace(string, length): return " ".join(string[i : i + length] for i in range(0, len(string), length)) -def send_data(name, value): - url = "http://127.0.0.1:8088/API/" - par = "SetText" if name.split(".")[1] == "Text" else "SetImage" +def _vmix_resend_full_state(): + """Переотправляет ВСЕ последние значения, когда vMix вернулся онлайн.""" + for name, val in _vmix_state.items(): + if val is None: + continue + _send_to_vmix(name, val) + + +def _send_to_vmix(name: str, value): + """Низкоуровневый вызов vMix API (без защиты).""" + try: + kind = name.split(".")[1] + except Exception: + kind = "Text" + par = "SetText" if kind == "Text" else "SetImage" + params = { "Function": par, - "Input": 33, + "Input": _VMIX_INPUT, "SelectedName": name, "Value": value, } - session.get(url, params=params) + # короткий таймаут, чтобы не блокировать + session.get(_VMIX_URL, params=params, timeout=0.25) + + +def send_data(name: str, value): + """ + «Мягкая» отправка в vMix: не роняем процесс при оффлайне, + вводим cooldown, и при появлении онлайна переотправляем весь стейт. + """ + global _vmix_suppress_until, _vmix_online + + # запоминаем последнее значение + _vmix_state[name] = value + + now = time.monotonic() + if now < _vmix_suppress_until: + return False + + try: + _send_to_vmix(name, value) + # если раньше были оффлайн — объявим «online» и добьём состояние + if _vmix_online is not True: + print("[vMix] online") + _vmix_online = True + _vmix_resend_full_state() + return True + except requests.exceptions.RequestException as e: + # оффлайн + if _vmix_online is not False: + msg = f"[vMix] offline: {e.__class__.__name__}: {e}" + print(msg) + _vmix_online = False + _vmix_suppress_until = now + _VMIX_COOLDOWN + return False + def parse_new(line): + global _attack_last, _attack_last_t, _attack_off_t + global _hold_zero_active, _hold_zero_t0, _last_timer_text + + # Преобразуем входящие байты в читаемый HEX + if line == b"\xff": + return + try: - with open(PATH, "r", encoding="utf-8") as f: - new_data = json.load(f) - except json.decoder.JSONDecodeError: - new_data = [ - { - "timeGFX": "0:00", - "time_attackGFX": "", - "quarter": "0", - "points1": "0", - "points2": "0", - "foul1": "0", - "foul2": "0", - "foul_pic1": "", - "foul_pic2": "", - "time_attac_pic": "", - "timeout1": "0", - "timeout2": "0", - } - ] + cdata = binascii.hexlify(line) + ddata = cdata.decode("utf-8").upper() + edata = hexspace(ddata, 2) + except Exception: + return - cdata = binascii.hexlify(line) - ddata = cdata.decode("utf-8").upper() - edata = hexspace(ddata, 2) - temp = edata.split("FF 52") - # print(temp) - for i in temp: - if "7D 4A C0 0A" in i: #основной таймер - minutes = int(i.split()[-5], ) - seconds = int(i.split()[-4], ) - milliseconds = int(i.split()[-3], ) - timer_str = ( - f"{minutes}:{seconds:02d}" if minutes != 0 else f"{seconds}.{milliseconds}" - ) - send_data("TIMER.Text", timer_str) + # Разделяем поток на пакеты по сигнатуре начала + parts = [seg.strip() for seg in edata.split("FF FF 51 7E") if seg.strip()] + if not parts: + return + + for i in parts: + arr = i.split() + print(arr) + # ---------------------- 24 СЕКУНД – OFF / RESET ---------------------- + if "89 4E C8 05" in i: + print(arr, "возможно гашение/включение") + send_data("24sec.Text", "") + _attack_last = None + _attack_off_t = time.monotonic() + continue + if "79 84 C0 08" in i: + print(arr, '2/4 секунды значение') + idx = None + for k in range(len(arr) - 3): + if arr[k:k+4] == ["79", "84", "C0", "08"]: + idx = k + break + if idx is None or len(arr) <= idx + 13: + continue + + seconds_24 = int(arr[idx + 10]) + tenths_24 = int(arr[idx + 11]) + + # Гашение при > 24 или 0.0 + if seconds_24 > 24 or (seconds_24 == 0 and tenths_24 == 0): + send_data("24sec.Text", "") + _attack_last = None + continue + + # Формирование отображаемого значения + shown = f"{seconds_24}.{tenths_24}" if seconds_24 <= 4 else str(seconds_24) + if shown in ("0", "0.0"): + shown = "" + + # Анти-рывок: не принимаем «рост» без OFF + now = time.monotonic() + new_val = seconds_24 + (tenths_24 / 10.0 if seconds_24 <= 5 else 0.0) + accept = False + + if _attack_last is None: + accept = True + elif (now - _attack_off_t) <= _ATTACK_OFF_GRACE: + accept = True + else: + accept = new_val <= _attack_last + _ATTACK_UP_GLITCH + + if accept: + send_data("24sec.Text", shown) + _attack_last = new_val + _attack_last_t = now + continue + if "79 84 C8 06" in i: + idx = None + for k in range(len(arr) - 3): + if arr[k:k+4] == ["79", "84", "C8", "06"]: + idx = k + break + if idx is None or len(arr) <= idx + 13: + continue + + seconds_24 = int(arr[idx + 10]) + tenths_24 = int(arr[idx + 11]) + + # Гашение при > 24 или 0.0 + if seconds_24 > 24 or (seconds_24 == 0 and tenths_24 == 0): + send_data("24sec.Text", "") + _attack_last = None + continue + + # Формирование отображаемого значения + shown = f"{seconds_24}.{tenths_24}" if seconds_24 <= 4 else str(seconds_24) + if shown in ("0", "0.0"): + shown = "" + + # Анти-рывок: не принимаем «рост» без OFF + now = time.monotonic() + new_val = seconds_24 + (tenths_24 / 10.0 if seconds_24 <= 5 else 0.0) + accept = False + + if _attack_last is None: + accept = True + elif (now - _attack_off_t) <= _ATTACK_OFF_GRACE: + accept = True + else: + accept = new_val <= _attack_last + _ATTACK_UP_GLITCH + + if accept: + send_data("24sec.Text", shown) + _attack_last = new_val + _attack_last_t = now + continue + + # ---------------------- 24 СЕКУНД – VALUE ---------------------- + if "79 84 C0 0A" in i: + # print(arr, '24 секунды значение') + idx = None + for k in range(len(arr) - 3): + if arr[k:k+4] == ["79", "84", "C0", "0A"]: + idx = k + break + if idx is None or len(arr) <= idx + 13: + continue + + seconds_24 = int(arr[idx + 12]) + tenths_24 = int(arr[idx + 13]) + # Гашение при > 24 или 0.0 + if seconds_24 > 24 or (seconds_24 == 0 and tenths_24 == 0): + send_data("24sec.Text", "") + _attack_last = None + continue + + # Формирование отображаемого значения + shown = f"{seconds_24}.{tenths_24}" if seconds_24 <= 4 else str(seconds_24) + if shown in ("0", "0.0"): + shown = "" + + # Анти-рывок: не принимаем «рост» без OFF + now = time.monotonic() + new_val = seconds_24 + (tenths_24 / 10.0 if seconds_24 <= 5 else 0.0) + accept = False + + if _attack_last is None: + accept = True + elif (now - _attack_off_t) <= _ATTACK_OFF_GRACE: + accept = True + else: + accept = new_val <= _attack_last + _ATTACK_UP_GLITCH + + if accept: + send_data("24sec.Text", shown) + _attack_last = new_val + _attack_last_t = now + continue + + # ---------------------- ОСНОВНОЕ ВРЕМЯ ---------------------- + if "7D 4A C0 0A" in i: + try: + idx = None + for k in range(len(arr) - 3): + if arr[k:k+4] == ["7D", "4A", "C0", "0A"]: + idx = k + break + if idx is None or len(arr) <= idx + 13: + continue + + def bcd_to_dec(b): + return ((b >> 4) & 0x0F) * 10 + (b & 0x0F) + + # секунды и минуты + seconds_b = int(arr[idx + 12], 16) + minutes_b = int(arr[idx + 11], 16) + + # отбросить флаговые старшие биты минут + minutes_b = minutes_b & 0x0F | ((minutes_b >> 4) & 0x07) << 4 + + minutes = bcd_to_dec(minutes_b) + seconds_raw = bcd_to_dec(seconds_b) + + # нормальный таймер + if seconds_raw < 60: + timer_calc = f"{minutes}:{seconds_raw:02d}" + else: + timer_calc = f"{minutes}.{seconds_raw - 128}" + + # анти-дребезг конца периода + now = time.monotonic() + is_zero = (minutes == 0 and seconds_raw == 0) + is_two0 = (minutes == 2 and seconds_raw == 0) + + if is_zero: + _hold_zero_active = True + _hold_zero_t0 = now + timer_str = "0.0" + elif _hold_zero_active and is_two0: + timer_str = "0.0" + if now - _hold_zero_t0 > _HOLD_ZERO_SEC: + _hold_zero_active = False + else: + if _hold_zero_active and (now - _hold_zero_t0 > _HOLD_ZERO_SEC): + _hold_zero_active = False + timer_str = timer_calc + + send_data("TIMER.Text", timer_str) + _last_timer_text = timer_str + + except Exception: + continue + + + # ---------------------- СЧЁТ ---------------------- + if "7D 4A C0 07" in i: + try: + idx = None + for k in range(len(arr) - 3): + if arr[k:k+4] == ["7D", "4A", "C0", "07"]: + idx = k + break + if idx is None or len(arr) <= idx + 7: + continue + + score1 = int(arr[idx + 6], 16) + score2 = int(arr[idx + 7], 16) - # print(i.split()[-7], i) - # if i.split()[-7] == "13" or i.split()[-7] == "10": - # new_data[0]["timeGFX"] = timer_str - elif "7D 4A C0 07" in i: #Счет - if i.split()[-7] == "06": #Счет первой команды - score1 = int(i.split()[-4], 16) - # new_data[0]["points1"] = score1 send_data("SCORE1.Text", score1) - elif i.split()[-7] == "07": #Счет второй команды - score2 = int(i.split()[-4], 16) send_data("SCORE2.Text", score2) - # new_data[0]["points2"] = score2 - else: - print("[СЧЁТ] == НЕПОНЯТНО") - elif "7D 4A C0 06" in i: #Информация - if i.split()[-6] == "09": #фолы первой команды - foul1 = int(i.split()[-3], 16) - # new_data[0]["foul1"] = foul1 - send_data("fouls1.Source", f"D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\Away_{foul1}.png") - elif i.split()[-6] == "0A": #фолы второй команды - foul2 = int(i.split()[-3], 16) - send_data("fouls2.Source", f"D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\Away_{foul2}.png") - # new_data[0]["foul2"] = foul2 - elif i.split()[-6] == "0E": #тайм-аут первой команды - time_out1 = int(i.split()[-3], 16) - # new_data[0]["timeout1"] = time_out1 - elif i.split()[-6] == "0F": #тайм-аут второй команды - time_out2 = int(i.split()[-3], 16) - # new_data[0]["timeout2"] = time_out2 - elif i.split()[-6] == "08": #четверть - quarter = int(i.split()[-3], 16) - # new_data[0]["quarter"] = quarter + except Exception: + continue + + +def _connect_socket(): + s = socket(AF_INET, SOCK_STREAM) + s.settimeout(_SOCK_RECV_TIMEOUT) + try: + s.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1) + except OSError: + pass + s.connect((HOST, PORT)) + s.send(b"hello") # как у тебя + return s - elif "79 84 C0 0A" in i: #24 секунды - print(i) - seconds = int(i.split()[-4]) - milliseconds = int(i.split()[-3]) - if seconds < int(5): - time_attack = f"{seconds}.{milliseconds}" - timer_pic = "D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\24Sec_Red.png" - else: - time_attack = seconds - timer_pic = "D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\24Sec_Empty.png" - if time_attack == "0.0": - time_attack = "" - timer_pic = "D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\24Sec_Empty.png" - send_data("24sec.Text", time_attack) - # send_data("24SECBACKGROUND.Source", timer_pic) - # print(time_attack) - elif "89 4E C8 05" in i: - if i.split()[-3] == "05": #таймер 24 секунд выключен - time_attack = "" - timer_pic = "D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\24Sec_Empty.png" - send_data("24sec.Text", time_attack) - # send_data("24SECBACKGROUND.Source", timer_pic) - # data = { - # "TIMER.Text": timer_str, - # "ATTACK.Text": time_attack, - # "Score_Home.Text": score1, - # "Score_Away.Text": score2, - # "fouls1.Source": f"D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\Home_{foul1}.png", - # "fouls2.Source": f"D:\\Графика\\БАСКЕТБОЛ\\ЕДИНАЯ ЛИГА ВТБ 2022-2023\\Scorebug Indicators\\Away_{foul2}.png", - # "24SECBACKGROUND.Source": timer_pic, - # } +def run_client(): + """Главный цикл: читает данные; при ошибке или простое — переподключается.""" + global _last_rx_mono, _rx_buf + + backoff = _RETRY_BACKOFF + s = None + _rx_buf = bytearray() + _last_rx_mono = time.monotonic() + + while True: + try: + if s is None: + print(f"[MOXA] connecting to {HOST}:{PORT} ...") + s = _connect_socket() + print("[MOXA] connected") + backoff = _RETRY_BACKOFF + _last_rx_mono = time.monotonic() + + # idle-watchdog: давно нет данных — форсим реконнект + if time.monotonic() - _last_rx_mono > _IDLE_RECONNECT: + raise TimeoutError(f"idle {time.monotonic() - _last_rx_mono:.1f}s") + + try: + chunk = s.recv(1024) + if not chunk: + raise ConnectionResetError("socket closed by peer") + logging.debug(chunk) + parse_new(chunk) + except timeout: + # socket.timeout: просто следующая итерация; проверим idle + pass + + except (ConnectionError, OSError, TimeoutError) as e: + if s is not None: + try: + s.close() + except Exception: + pass + s = None + print(f"[MOXA] reconnect in {backoff:.1f}s due to: {e}") + time.sleep(backoff) + backoff = min(backoff * 2, _RETRY_BACKOFF_MAX) + continue + def read_logs(): - with open( - r"C:\Users\soule\Downloads\Telegram Desktop\timer_Megasport_Nport_2024-03-05_20-00-17.log", - "r", - ) as file: + path = r"C:\Code\timer\LOGS\timer_Nata_Nport_2025-04-25_14-53-51.log" + # path = r"C:\Code\timer\LOGS\timer_Nata_Nport_2025-04-25_15-15-42.log" + with open(path, "r", encoding="utf-8", errors="ignore") as file: for line in file: - parts = line.strip().split(" DEBUG ") - if len(parts) == 2: - timestamp = parts[0][1:] - data = eval(parts[1]) - if b"\xf83" in data or b"\xf88" in data or b"\xf87" in data: - parse(data) - time.sleep(0.1) + parts = line.strip().split(" DEBUG ", 1) + if len(parts) != 2: + continue + + payload = parts[1].strip() + + # читаем только байтовые строки + if payload.startswith("b'") or payload.startswith('b"'): + try: + data = ast.literal_eval(payload) + if isinstance(data, (bytes, bytearray)): + parse_new(bytes(data)) + except Exception: + pass + + time.sleep(0.005) def main(): - current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") if not os.path.isdir("LOGS"): os.mkdir("LOGS") - LogFileName = f"LOGS/timer_Nata_Nport_{current_time}.log" + LogFileName = "LOGS/timer_SARATOV.log" logging.basicConfig( level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s", @@ -166,24 +440,11 @@ def main(): ) try: - tcp_socket = socket(AF_INET, SOCK_STREAM) - tcp_socket.connect((HOST, PORT)) - data = str.encode("hello") - tcp_socket.send(data) - data = bytes.decode(data) - while True: - data = tcp_socket.recv(1024) - parse_new(data) - logging.debug(data) - + # run_client() # ← онлайн-режим с авто-реконнектом + read_logs() # ← оставь для оффлайн-прогона логов (ручной переключатель) except KeyboardInterrupt: - tcp_socket.close() sys.exit(1) if __name__ == "__main__": - try: - main() - # read_logs() - except KeyboardInterrupt: - sys.exit(1) + main()