import sys import json from socket import * from datetime import datetime import logging 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 # --- 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"}) def hexspace(string, length): return " ".join(string[i : i + length] for i in range(0, len(string), length)) 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": _VMIX_INPUT, "SelectedName": name, "Value": value, } # короткий таймаут, чтобы не блокировать 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: cdata = binascii.hexlify(line) ddata = cdata.decode("utf-8").upper() edata = hexspace(ddata, 2) except Exception: return # Разделяем поток на пакеты по сигнатуре начала 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) send_data("SCORE1.Text", score1) send_data("SCORE2.Text", score2) 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 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(): 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 ", 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(): if not os.path.isdir("LOGS"): os.mkdir("LOGS") LogFileName = "LOGS/timer_SARATOV.log" logging.basicConfig( level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s", filename=LogFileName, filemode="w", ) try: # run_client() # ← онлайн-режим с авто-реконнектом read_logs() # ← оставь для оффлайн-прогона логов (ручной переключатель) except KeyboardInterrupt: sys.exit(1) if __name__ == "__main__": main()