timer NATA Kazan - нужно проверить, возможно поправил гашение 24 секунд

This commit is contained in:
2025-11-13 10:48:05 +03:00
parent a59a299d73
commit 80beb85366

View File

@@ -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.81.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()