|
|
@@ -7,6 +7,17 @@ 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":
|
|
|
@@ -33,62 +44,192 @@ currentScore.set(0)
|
|
|
|
|
|
names = []
|
|
|
scores = []
|
|
|
+nameEntry = None
|
|
|
+enterButton = None
|
|
|
+statusLabel = None
|
|
|
+dinger = None
|
|
|
+bell_image = None # Store the image reference
|
|
|
|
|
|
-highestScore = 100
|
|
|
+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)
|
|
|
|
|
|
-# Punch control
|
|
|
def startPunch():
|
|
|
global punch_on
|
|
|
punch_on = True
|
|
|
- print("STARTED PUNCH")
|
|
|
+ updateStatusLabel()
|
|
|
+ update_dinger_position()
|
|
|
|
|
|
def stopPunch():
|
|
|
- global punch_on, highestScore
|
|
|
+ global punch_on
|
|
|
punch_on = False
|
|
|
- print("STOPPED PUNCH")
|
|
|
-
|
|
|
- if currentScore.get() > highestScore:
|
|
|
- highestScore = currentScore.get()
|
|
|
+ updateStatusLabel()
|
|
|
+ update_dinger_position(currentScore.get())
|
|
|
+ if stop_sound:
|
|
|
try:
|
|
|
- with open("scores.txt", "w") as f: # Fixed mode from "rw" to "w"
|
|
|
- f.write(f"{highestScore} {highScoreName}")
|
|
|
+ stop_sound.play()
|
|
|
except Exception as e:
|
|
|
- print("Error writing to file:", e)
|
|
|
-
|
|
|
- scores.append(highestScore)
|
|
|
- names.append(highScoreName)
|
|
|
+ print(f"Error playing sound: {str(e)}")
|
|
|
|
|
|
def resetPunch():
|
|
|
global currentScore
|
|
|
currentScore.set(0)
|
|
|
- print("reset punch")
|
|
|
+ update_dinger_position(0)
|
|
|
|
|
|
-dinger = ctk.CTkLabel(width=40, height=30, text="", fg_color="gray77", master=app)
|
|
|
-dinger.place(x=1405, y=900)
|
|
|
+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 i in range(len(names)):
|
|
|
- label1 = ctk.CTkLabel(width=124, height=39, text_color="white", text=names[i], font=("Arial", 15), master=app)
|
|
|
+ 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=scores[i], font=("Arial", 15), master=app)
|
|
|
+ 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)
|
|
|
- ctk.CTkButton(width=0, height=150, text="START", fg_color="green", corner_radius=50, font=("Arial", 50),
|
|
|
- text_color="gray99", master=app, command=startPunch).place(x=50, y=300)
|
|
|
- ctk.CTkButton(width=0, height=150, text="STOP", fg_color="orange2", corner_radius=50, font=("Arial", 50),
|
|
|
- text_color="gray99", master=app, command=stopPunch).place(x=50, y=500)
|
|
|
- ctk.CTkButton(width=0, height=150, text="RESET", fg_color="red1", corner_radius=50, font=("Arial", 50),
|
|
|
- text_color="gray99", master=app, command=resetPunch).place(x=50, y=700)
|
|
|
+
|
|
|
+ # 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)
|
|
|
@@ -96,12 +237,23 @@ def placeLeaderboard():
|
|
|
nameEntry = ctk.CTkEntry(width=300, height=50, master=app, font=("Arial", 50))
|
|
|
nameEntry.place(x=370, y=200)
|
|
|
|
|
|
- ctk.CTkButton(width=50, height=50, text="ENTER", fg_color="green", font=("Arial", 20),
|
|
|
- text_color="gray99", master=app).place(x=670, y=205)
|
|
|
+ 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)
|
|
|
|
|
|
- image = Image.open("./BellDrawing.png")
|
|
|
- photo = ImageTk.PhotoImage(image)
|
|
|
- ctk.CTkLabel(image=photo, text="", width=10, master=app).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():
|
|
|
@@ -114,30 +266,6 @@ def start_ble_thread():
|
|
|
thread = threading.Thread(target=ble_thread_func, daemon=True)
|
|
|
thread.start()
|
|
|
|
|
|
-def update_dinger_position():
|
|
|
- global dinger
|
|
|
- score = currentScore.get()
|
|
|
-
|
|
|
- # Example logic: map score (0–100) to y-position (900 to 300)
|
|
|
- # You can adjust the ranges to suit your needs
|
|
|
- max_score = 100
|
|
|
- min_y = 300
|
|
|
- max_y = 900
|
|
|
-
|
|
|
- # Clamp score between 0 and max_score
|
|
|
- score = min(max(score, 0), max_score)
|
|
|
-
|
|
|
- # Calculate the new y position
|
|
|
- new_y = max_y - int((score / max_score) * (max_y - min_y))
|
|
|
-
|
|
|
- # Move the label
|
|
|
- dinger.place_configure(y=new_y)
|
|
|
-
|
|
|
- # Call this again after a short delay
|
|
|
- app.after(100, update_dinger_position)
|
|
|
-
|
|
|
-
|
|
|
-# Data processing loop in background thread
|
|
|
def data_loop():
|
|
|
last_data = None
|
|
|
last_value = 0.00
|
|
|
@@ -178,6 +306,7 @@ def start_data_loop_thread():
|
|
|
thread.start()
|
|
|
|
|
|
# Run UI setup
|
|
|
+load_scores() # Load existing scores before setting up UI
|
|
|
placeLeaderboard()
|
|
|
|
|
|
# Start threads after UI initializes
|