Files
CloseCallTallyGUIHTTP/CloseCallTallyGUIHTTP.py
2025-10-27 12:07:07 +00:00

468 lines
18 KiB
Python

import tkinter as tk
from tkinter import ttk
import configparser
from typing import Self
from threading import Thread
import threading
from datetime import datetime
import time
import socket
import os
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import platform
import subprocess
import re
class App(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.parent.title("Tally Triggers V1.2 HTTP")
# Create tabs
self.tabControl = ttk.Notebook(self.parent)
self.tab1 = ttk.Frame(self.tabControl)
self.tab2 = ttk.Frame(self.tabControl)
# Add tabs to the tab control
self.tabControl.add(self.tab1, text="Main")
self.tabControl.add(self.tab2, text="Settings")
self.tabControl.pack(expand=1, fill="both")
#Load Config
self.load_config()
# Start TCP1 listener thread
self.tcp_thread1 = threading.Thread(target=self.tcp_listener, args=(self.TCP_Port1,), daemon=True)
self.tcp_thread1.start()
# Start TCP2 listener thread
self.tcp_thread2 = threading.Thread(target=self.tcp_listener, args=(self.TCP_Port2,), daemon=True)
self.tcp_thread2.start()
# Create widgets
self.setup_widgets()
# Start Tally Box connectivity check thread
self.connection_thread = threading.Thread(target=self.check_tally_connection, daemon=True)
self.connection_thread.start()
def load_config(self):
# Load configuration
config = configparser.ConfigParser()
config.read('Config.ini')
# Courts
self.Relay_1_Name = config['Courts']['Relay1']
self.Relay_2_Name = config['Courts']['Relay2']
#TMCC IPs
self.Relay1_TMCCIP = config['TMMC']['Relay1']
self.Relay2_TMCCIP = config['TMMC']['Relay2']
#TCP Commands
self.Relay1_Command = config['TCP Commands']['Relay1']
self.Relay2_Command = config['TCP Commands']['Relay2']
#General
self.TallyBoxIP = config['General']['TallyBoxIP']
self.TCP_Port1 = int(config['General']['TCP_Port1'])
self.TCP_Port2 = int(config['General']['TCP_Port2'])
self.On_Time = int(config['General']['On_Time'])
self.PosDistanceThreshold = int(config['General']['Pos_Dist'])
self.NegDistanceThreshold = int(config['General']['Neg_Dist'])
def setup_widgets(self):
#####################################################################################
# Relay 1 Frame
self.Relay1_frame = ttk.LabelFrame(self.tab1, text="Relay 1", padding=(20, 10))
self.Relay1_frame.grid(
row=0, column=0, padx=(5, 5), pady=(20, 10), sticky="nsew"
)
# Relay 1
self.Relay1 = ttk.Label(self.Relay1_frame,text=self.Relay_1_Name,justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay1.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")
# Canvas for the circle
self.Relay1Status = tk.Canvas(self.Relay1_frame, width=45, height=50)
self.Relay1Status.grid(row=1, column=0, pady=(0, 10))
# Draw circle
self.oval_id1 = self.Relay1Status.create_oval(10, 10, 40, 40, fill="red", outline="grey")
# Relay 1 TMCC Connection
self.Relay1TMCCConnection = ttk.Label(self.Relay1_frame,text="TMCC",justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay1TMCCConnection.grid(row=2, column=0, padx=0, pady=(0, 20), sticky="ns")
# Relay 1 last trigger time
self.Relay1time = ttk.Label(self.Relay1_frame,text="Never",justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay1time.grid(row=3, column=0, padx=0, pady=(0, 20), sticky="ns")
# Test Button
self.Relay1button = ttk.Button(self.Relay1_frame, text="Test", style="Accent.TButton",width = 12, command=self.publish_relay1)
self.Relay1button.grid(row=4, column=0, padx=0, pady=10, sticky="nsew")
#####################################################################################
# Relay 2 Frame
self.Relay2_frame = ttk.LabelFrame(self.tab1, text="Relay 2", padding=(20, 10))
self.Relay2_frame.grid(
row=0, column=1, padx=(5, 5), pady=(20, 10), sticky="nsew"
)
# Entry Relay 2
self.Relay2 = ttk.Label(self.Relay2_frame,text=self.Relay_2_Name,justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay2.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")
# Canvas for the circle
self.Relay2Status = tk.Canvas(self.Relay2_frame, width=45, height=50)
self.Relay2Status.grid(row=1, column=0, pady=(0, 10))
# Draw circle
self.oval_id2 = self.Relay2Status.create_oval(10, 10, 40, 40, fill="red", outline="grey")
# Relay 2 TMCC Connection
self.Relay2TMCCConnection = ttk.Label(self.Relay2_frame,text="TMCC",justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay2TMCCConnection.grid(row=2, column=0, padx=0, pady=(0, 20), sticky="ns")
# Entry Relay 2 last trigger time
self.Relay2time = ttk.Label(self.Relay2_frame,text="Never",justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay2time.grid(row=3, column=0, padx=0, pady=(0, 20), sticky="ns")
# Test Button
self.Relay2button = ttk.Button(self.Relay2_frame, text="Test", style="Accent.TButton",width = 12, command=self.publish_relay2)
self.Relay2button.grid(row=4, column=0, padx=0, pady=10, sticky="nsew")
#####################################################################################
# Extras Frame
self.Extras_frame = ttk.LabelFrame(self.tab1, text="Extras", padding=(20, 10))
self.Extras_frame.grid(
row=1, column=0, columnspan = 8, padx=(5, 5), pady=(5, 5), sticky="nsew"
)
# Connection Status
self.ConnectionStatus = ttk.Label(self.Extras_frame,text="Connected to Tally Box:",justify="center",font=("-size", 10, "-weight", "bold"),
)
self.ConnectionStatus.grid(row=0, column=0, padx=0, pady=(0, 0), sticky="ew")
# Canvas for the circle
self.ConnectionStatusLight = tk.Canvas(self.Extras_frame, width=50, height=50)
self.ConnectionStatusLight.grid(row=0, column=1, pady=(0, 0))
# Draw circle
self.oval_id9 = self.ConnectionStatusLight.create_oval(10, 10, 40, 40, fill="red", outline="grey")
# Test All Button
self.TestAllButton = ttk.Button(self.Extras_frame, text="Test All", style="Accent.TButton",width = 12, command=self.publish_relay_all)
self.TestAllButton.grid(row=0, column=4, padx=(50,0), pady=0, sticky="nsew")
#######################################################################################
#####################################################################################
# Relay 1 Settings Frame
self.Relay1_frame = ttk.LabelFrame(self.tab2, text="Relay 1", padding=(20, 10))
self.Relay1_frame.grid(
row=0, column=0, padx=(5, 5), pady=(20, 10), sticky="nsew"
)
# Label Relay 1 Courtname
self.Relay1Name = ttk.Label(self.Relay1_frame,text="Court Name",justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay1Name.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")
# Entry Relay 1 Courtname
self.Relay1Entry = ttk.Entry(self.Relay1_frame, textvariable=self.Relay_1_Name, justify="center", font=("-size", 10, "-weight", "bold"))
self.Relay1Entry.insert(0, self.Relay_1_Name)
self.Relay1Entry.grid(row=1, column=0, padx=0, pady=(0, 20), sticky="ew")
# Save Button
self.Save1button = ttk.Button(self.Relay1_frame, text="Save", style="Accent.TButton",width = 12, command=self.save_relay_names)
self.Save1button.grid(row=2, column=0, padx=0, pady=10, sticky="nsew")
###########################################################################################################################################
# Relay 2 Settings Frame
self.Relay2_frame = ttk.LabelFrame(self.tab2, text="Relay 2", padding=(20, 10))
self.Relay2_frame.grid(
row=0, column=1, padx=(5, 5), pady=(20, 10), sticky="nsew"
)
# Label Relay 2 Courtname
self.Relay2Name = ttk.Label(self.Relay2_frame,text="Court Name",justify="center",font=("-size", 10, "-weight", "bold"),
)
self.Relay2Name.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")
# Entry Relay 2 Courtname
self.Relay2Entry = ttk.Entry(self.Relay2_frame, textvariable=self.Relay_2_Name, justify="center", font=("-size", 10, "-weight", "bold"))
self.Relay2Entry.insert(0, self.Relay_2_Name)
self.Relay2Entry.grid(row=1, column=0, padx=0, pady=(0, 20), sticky="ew")
# Save Button
self.Save2button = ttk.Button(self.Relay2_frame, text="Save", style="Accent.TButton",width = 12, command=self.save_relay_names)
self.Save2button.grid(row=2, column=0, padx=0, pady=10, sticky="nsew")
###########################################################################################################################################
# Function to send a TCP message to the TALLY BOX
def send_message(self, command, timeout=5):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
s.connect((self.TallyBoxIP, 17494))
# command may already be bytes, so handle both cases
if isinstance(command, str):
s.sendall(bytes.fromhex(command))
else:
s.sendall(command)
data = s.recv(1024)
return data # Return response bytes
except (socket.timeout, ConnectionRefusedError, socket.error) as e:
print(f"Error sending message: {e}")
return None
def publish_relay1(self):
def toggle_relay1():
relay_id = 1
# ---- Turn ON ----
message = bytes([0x20, relay_id, 0x00])
response = self.send_message(message)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
print(f"[{current_time}] Sent command: 20 {relay_id:02X} 00 - Relay {relay_id} ON")
if response:
resp_val = response[0]
print(f"[{current_time}] Device response: {resp_val}")
if resp_val == 0:
self.Relay1Status.itemconfig(self.oval_id1, fill="green")
self.Relay1time.config(text=current_time)
else:
print(f"[{current_time}] Relay {relay_id} ON failed (response={resp_val})")
return
else:
print(f"[{current_time}] No response from device for Relay {relay_id} ON")
return
time.sleep(self.On_Time)
# ---- Turn OFF ----
message = bytes([0x21, relay_id, 0x00])
response = self.send_message(message)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
print(f"[{current_time}] Sent command: 21 {relay_id:02X} 00 - Relay {relay_id} OFF")
if response:
resp_val = response[0]
print(f"[{current_time}] Device response: {resp_val}")
if resp_val == 0:
self.Relay1Status.itemconfig(self.oval_id2, fill="red")
else:
print(f"[{current_time}] Relay {relay_id} OFF failed (response={resp_val})")
else:
print(f"[{current_time}] No response from device for Relay {relay_id} OFF")
threading.Thread(target=toggle_relay1, daemon=True).start()
def publish_relay2(self):
def toggle_relay2():
relay_id = 2
# ---- Turn ON ----
message = bytes([0x20, relay_id, 0x00])
response = self.send_message(message)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
print(f"[{current_time}] Sent command: 20 {relay_id:02X} 00 - Relay {relay_id} ON")
if response:
resp_val = response[0]
print(f"[{current_time}] Device response: {resp_val}")
if resp_val == 0:
self.Relay2Status.itemconfig(self.oval_id2, fill="green")
self.Relay2time.config(text=current_time)
else:
print(f"[{current_time}] Relay {relay_id} ON failed (response={resp_val})")
return
else:
print(f"[{current_time}] No response from device for Relay {relay_id} ON")
return
time.sleep(self.On_Time)
# ---- Turn OFF ----
message = bytes([0x21, relay_id, 0x00])
response = self.send_message(message)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
print(f"[{current_time}] Sent command: 21 {relay_id:02X} 00 - Relay {relay_id} OFF")
if response:
resp_val = response[0]
print(f"[{current_time}] Device response: {resp_val}")
if resp_val == 0:
self.Relay2Status.itemconfig(self.oval_id2, fill="red")
else:
print(f"[{current_time}] Relay {relay_id} OFF failed (response={resp_val})")
else:
print(f"[{current_time}] No response from device for Relay {relay_id} OFF")
threading.Thread(target=toggle_relay2, daemon=True).start()
def publish_relay_all(self):
self.publish_relay1()
self.publish_relay2()
def check_tally_connection(self):
"""Continuously checks if the Tally Box is reachable and updates the status light."""
while True:
if self.ping_tally_box():
self.ConnectionStatusLight.itemconfig(self.oval_id9, fill="green") # Change to green if reachable
else:
self.ConnectionStatusLight.itemconfig(self.oval_id9, fill="red") # Change to red if unreachable
time.sleep(5) # Check every 5 seconds
def ping_tally_box(self):
"""Pings the Tally Box IP to check connectivity."""
param = "-n" if platform.system().lower() == "windows" else "-c"
command = ["ping", param, "1", self.TallyBoxIP]
try:
subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return True # Ping was successful
except subprocess.CalledProcessError:
return False # Ping failed
def save_relay_names(self):
"""Saves the Relay 1 and Relay 2 names to the Config.ini file and updates the UI labels."""
new_relay1_name = self.Relay1Entry.get()
new_relay2_name = self.Relay2Entry.get()
# Update labels in the UI
self.Relay1.config(text=new_relay1_name)
self.Relay2.config(text=new_relay2_name)
# Update the config file
config = configparser.ConfigParser()
config.read('Config.ini')
if 'Courts' not in config:
config['Courts'] = {}
config['Courts']['Relay1'] = new_relay1_name
config['Courts']['Relay2'] = new_relay2_name
with open('Config.ini', 'w') as configfile:
config.write(configfile)
print("Config updated successfully!")
def tcp_listener(self, port):
# TCP/IP socket setup
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', port)) # Bind to all interfaces
server_socket.listen(1) # Listen for incoming connections
print(f"TCP server listening on port {port} \n")
while True:
# Accept a connection
conn, addr = server_socket.accept()
print(f"Connection from {addr} on port {port}")
try:
# Receive the message
message = conn.recv(1024).decode('utf-8').strip()
print(f"Received raw message: {message}")
# Extract values using regular expressions
close_call_match = re.search(r'"Close Call":\s*"(\w+)"', message)
distance_match = re.search(r'"Distance":\s*(-?[\d\.]+)', message)
if not (close_call_match and distance_match):
print("Error: Invalid message format")
continue # Skip processing this message
# Convert extracted values
close_call = close_call_match.group(1) == "True"
distance = float(distance_match.group(1))
# Check if Close Call is True and within tolerance
if close_call:
if port == self.TCP_Port1:
self.publish_relay1()
elif port == self.TCP_Port2:
self.publish_relay2()
else:
print(f"Message received but conditions not met for port {port}")
except Exception as e:
print(f"Error processing message: {e}")
finally:
conn.close()
if __name__ == "__main__":
root = tk.Tk()
root.title("Tally Triggers V1.2 HTTP")
root.iconbitmap(r'Resources/icon1.ico')
# Simply set the theme
root.tk.call("source", "Resources/azure.tcl")
root.tk.call("set_theme", "dark")
app = App(root)
app.pack(fill="both", expand=True)
# Set a minsize for the window, and place it in the middle
root.update()
root.minsize(root.winfo_width(), root.winfo_height())
root.mainloop()