468 lines
18 KiB
Python
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()
|
|
|