import platform import sys import customtkinter as ctk from PIL import Image, ImageTk import time import threading import tkinter as tk import dobluetooth import math import pygame # Initialize pygame and mixer specifically for MP3 pygame.init() pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=2048) try: stop_sound = pygame.mixer.Sound("ding.mp3") print("Successfully loaded sound file") except Exception as e: print(f"Warning: Could not load sound file: {str(e)}") stop_sound = None # If on Windows, allow STA for Bleak if platform.system() == "Windows": try: from bleak.backends.winrt.util import allow_sta allow_sta() except ImportError: pass # UI setup ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") app = ctk.CTk() app.geometry("1440x1024") canvas = ctk.CTkCanvas(app, width=250, height=250, highlightthickness=0) canvas.config(background="white") canvas.place(x=0, y=0) # Score variables currentScore = tk.DoubleVar() currentScore.set(0) names = [] scores = [] nameEntry = None enterButton = None statusLabel = None dinger = None bell_image = None # Store the image reference highestScore = 0 highScoreName = "" punch_on = False current_dinger_y = 900 def move_dinger_step(start_y, target_y, steps_left, total_steps): global current_dinger_y if steps_left > 0: # Calculate progress (0 to 1) progress = (total_steps - steps_left) / total_steps # Calculate the current position directly between start and target new_y = start_y + (target_y - start_y) * progress # Update position current_dinger_y = new_y dinger.place_configure(y=int(new_y)) # Schedule next step app.after(10, lambda: move_dinger_step(start_y, target_y, steps_left - 1, total_steps)) else: # Ensure we end up exactly at target current_dinger_y = target_y dinger.place_configure(y=int(target_y)) def update_dinger_position(final_score=None): global current_dinger_y start_y = current_dinger_y # Remember where we're starting from if final_score is None: target_y = 900 # Bottom position else: # Calculate target position based on score max_movement = 600 score_threshold = 100 # Calculate new position movement = (final_score / score_threshold) * max_movement movement = min(movement, max_movement) target_y = 900 - movement # Calculate from top position target_y = max(100, min(target_y, 900)) # Ensure within bounds # Start the animation move_dinger_step(start_y, target_y, 60, 60) def startPunch(): global punch_on punch_on = True updateStatusLabel() update_dinger_position() def stopPunch(): global punch_on punch_on = False updateStatusLabel() update_dinger_position(currentScore.get()) if stop_sound: try: stop_sound.play() except Exception as e: print(f"Error playing sound: {str(e)}") def resetPunch(): global currentScore currentScore.set(0) update_dinger_position(0) def load_scores(): global names, scores, highestScore, highScoreName try: with open("all_scores.txt", "r") as f: lines = f.readlines() for line in lines: name, score = line.strip().split(",") score = float(score) names.append(name) scores.append(score) if score > highestScore: highestScore = score highScoreName = name except FileNotFoundError: # Create the file if it doesn't exist with open("all_scores.txt", "w") as f: pass def save_new_score(name, score): with open("all_scores.txt", "a") as f: f.write(f"{name},{score}\n") def updateStatusLabel(): global statusLabel if punch_on: statusLabel.configure(text="RECORDING", fg_color="green", text_color="white") else: statusLabel.configure(text="NOT RECORDING", fg_color="gray70", text_color="black") def submitScore(): global highestScore, highScoreName # Get the name from the entry field entered_name = nameEntry.get().strip() if entered_name: # Only proceed if a name was entered current_score = currentScore.get() names.append(entered_name) scores.append(current_score) # Save the new score to file save_new_score(entered_name, current_score) if current_score > highestScore: highestScore = current_score highScoreName = entered_name # Clear the entry field after recording nameEntry.delete(0, 'end') # Refresh the leaderboard display placeLeaderboard() # Leaderboard & UI setup def placeLeaderboard(): global nameEntry, enterButton, statusLabel, dinger, bell_image canvas.create_rectangle(0, 0, 500, 500, fill="grey20", width=2) # Get top 5 scores score_pairs = list(zip(names, scores)) score_pairs.sort(key=lambda x: x[1], reverse=True) top_5_pairs = score_pairs[:5] intx = 0 inty = 60 for name, score in top_5_pairs: label1 = ctk.CTkLabel(width=124, height=39, text_color="white", text=name, font=("Arial", 15), master=app) label1.place(x=intx, y=inty) label2 = ctk.CTkLabel(width=124, height=39, text_color="white", text=f"{score:.2f}", font=("Arial", 15), master=app) label2.place(x=intx + 125, y=inty) inty += 40 ctk.CTkLabel(width=249, height=39, text_color="white", text="Leaderboard", font=("Arial", 50), master=app).place(x=0, y=0) # Create status indicator statusLabel = ctk.CTkLabel( master=app, width=120, height=40, text="NOT RECORDING", fg_color="gray70", text_color="black", corner_radius=10, font=("Arial", 16) ) statusLabel.place(x=50, y=450) ctk.CTkButton(master=app, width=120, height=80, text="START", fg_color="green", text_color="gray99", corner_radius=25, font=("Arial", 30), command=startPunch).place(x=50, y=500) ctk.CTkButton(master=app, width=120, height=80, text="STOP", fg_color="orange2", text_color="gray99", corner_radius=25, font=("Arial", 30), command=stopPunch).place(x=50, y=600) ctk.CTkButton(master=app, width=120, height=80, text="RESET", fg_color="red1", text_color="gray99", corner_radius=25, font=("Arial", 30), command=resetPunch).place(x=50, y=700) ctk.CTkLabel(width=50, height=20, text_color="white", text="Current Score", font=("Arial", 50), master=app).place(x=350, y=20) ctk.CTkLabel(width=50, height=20, text_color="white", textvariable=currentScore, font=("Arial", 50), master=app).place(x=500, y=100) nameEntry = ctk.CTkEntry(width=300, height=50, master=app, font=("Arial", 50)) nameEntry.place(x=370, y=200) enterButton = ctk.CTkButton(width=50, height=50, text="ENTER", fg_color="green", font=("Arial", 20), text_color="gray99", master=app, command=submitScore) enterButton.place(x=670, y=205) # Load and display the bell image bell_image = Image.open("./BellDrawing.png") photo = ImageTk.PhotoImage(bell_image) image_label = ctk.CTkLabel(image=photo, text="", width=10, master=app) image_label.image = photo # Keep a reference to prevent garbage collection image_label.place(x=800, y=0) # Create the dinger after the image (so it appears on top) dinger = ctk.CTkLabel(width=40, height=30, text="", fg_color="gray77", master=app) dinger.place(x=1005, y=900) # Start updating dinger position update_dinger_position() # BLE thread setup def ble_thread_func(): import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(dobluetooth.getData()) def start_ble_thread(): thread = threading.Thread(target=ble_thread_func, daemon=True) thread.start() def data_loop(): last_data = None last_value = 0.00 while True: bledata = dobluetooth.getDataArr() if punch_on: if last_data == None: last_data = bledata current_x_accel = bledata[3] current_y_accel = bledata[4] last_x_accel = last_data[3] last_y_accel = last_data[4] value = math.sqrt((current_x_accel - last_y_accel)**2 + ((current_y_accel - last_y_accel)**2))/4 print(value) if(value < 0.26): pass else: if(last_value - value > 10.00): app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val))) else: if(value - last_value > 10.00): if(value == last_value): app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val*10))) else: app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val))) else: app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val*25))) app.after(0, update_dinger_position) last_data = bledata last_value = value time.sleep(0.1) def start_data_loop_thread(): thread = threading.Thread(target=data_loop, daemon=True) thread.start() # Run UI setup load_scores() # Load existing scores before setting up UI placeLeaderboard() # Start threads after UI initializes app.after(500, start_ble_thread) app.after(1000, start_data_loop_thread) app.mainloop()