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()