Files
TIMERS/timer NATA Kazan.py

451 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.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"})
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()